@@ -9,8 +9,28 @@ define([ | |||
renderer, | |||
config | |||
) { | |||
//Defaults | |||
const MOVE_SPEED = 0.5; | |||
const TTL = 35; | |||
const FONT_SIZE = 18; | |||
const FONT_SIZE_CRIT = 22; | |||
const LAYER_NAME = 'effects'; | |||
const PADDING = scaleMult; | |||
const POSITION = { | |||
BOTTOM_CENTER: 0, | |||
LEFT_BOTTOM: 1, | |||
RIGHT_BOTTOM: 2, | |||
TOP_CENTER: 3 | |||
}; | |||
//Internals | |||
const list = []; | |||
//Create an object of the form: { elementName: elementIntegerColor, ... } from corresponding variable values. | |||
// These variables are defiend in main.less and take the form: var(--color-element-elementName) | |||
// These variables are defined in main.less and take the form: var(--color-element-elementName) | |||
const elementColors = Object.fromEntries( | |||
['default', 'arcane', 'frost', 'fire', 'holy', 'poison'].map(e => { | |||
const variableName = `--color-element-${e}`; | |||
@@ -22,101 +42,188 @@ define([ | |||
}) | |||
); | |||
return { | |||
list: [], | |||
//Helpers | |||
const getColor = ({ color, element }) => { | |||
if (color) | |||
return color; | |||
return elementColors[element]; | |||
}; | |||
const getText = ({ amount, text, heal }) => { | |||
if (amount === undefined) | |||
return text; | |||
const div = ((~~(amount * 10) / 10) > 0) ? 10 : 100; | |||
let result = ~~(amount * div) / div; | |||
if (heal) | |||
result = `+${result}`; | |||
return result; | |||
}; | |||
const getPosition = ({ position, event: isEvent, heal }) => { | |||
if (position) | |||
return position; | |||
//Events render under the target, centered | |||
if (isEvent) | |||
return POSITION.BOTTOM_CENTER; | |||
else if (heal) | |||
return POSITION.LEFT_BOTTOM; | |||
return POSITION.RIGHT_BOTTOM; | |||
}; | |||
const getXY = (msg, position, sprite) => { | |||
let x = 0; | |||
let y = 0; | |||
if (position === POSITION.TOP_CENTER) { | |||
x = (scale / 2) - (sprite.width / 2); | |||
y = -PADDING - sprite.height; | |||
} else if (position === POSITION.BOTTOM_CENTER) { | |||
x = (scale / 2) - (sprite.width / 2); | |||
y = scale + PADDING; | |||
} else if (position === POSITION.RIGHT_BOTTOM) { | |||
x = scale; | |||
y = scale - sprite.height + (scaleMult * 2); | |||
} else if (position === POSITION.LEFT_BOTTOM) { | |||
x = -sprite.width - PADDING; | |||
y = scale - sprite.height + (scaleMult * 2); | |||
} | |||
return { x, y }; | |||
}; | |||
init: function () { | |||
events.on('onGetDamage', this.onGetDamage.bind(this)); | |||
}, | |||
const getMovementDelta = ({ movementDelta }, position) => { | |||
if (movementDelta) | |||
return movementDelta; | |||
onGetDamage: function (msg) { | |||
if (config.damageNumbers === 'off') | |||
if (position === POSITION.BOTTOM_CENTER) | |||
return [0, 1]; | |||
return [0, -1]; | |||
}; | |||
const getFontSize = ({ fontSize, crit }) => { | |||
if (fontSize) | |||
return fontSize; | |||
else if (crit) | |||
return FONT_SIZE_CRIT; | |||
return FONT_SIZE; | |||
}; | |||
//Events | |||
const onGetDamage = msg => { | |||
const { ttl = TTL } = msg; | |||
if (config.damageNumbers === 'off') | |||
return; | |||
const target = objects.objects.find(o => o.id === msg.id); | |||
if (!target || !target.isVisible) | |||
return; | |||
const sprite = renderer.buildText({ | |||
fontSize: getFontSize(msg), | |||
layerName: LAYER_NAME, | |||
text: getText(msg), | |||
color: getColor(msg), | |||
visible: false | |||
}); | |||
const position = getPosition(msg); | |||
const movementDelta = getMovementDelta(msg, position); | |||
const { x, y } = getXY(msg, position, sprite); | |||
sprite.x = (target.x * scale) + x; | |||
sprite.y = (target.y * scale) + y; | |||
sprite.visible = true; | |||
const numberObj = { | |||
obj: target, | |||
x, | |||
y, | |||
ttl, | |||
ttlMax: ttl, | |||
movementDelta, | |||
sprite | |||
}; | |||
list.push(numberObj); | |||
}; | |||
//Call this method from inside update to generate test numbers | |||
// around the player | |||
/* eslint-disable-next-line no-unused-vars */ | |||
const test = () => { | |||
objects.objects.forEach(o => { | |||
if (!o.player) | |||
return; | |||
let target = objects.objects.find(function (o) { | |||
return (o.id === msg.id); | |||
const amount = Math.random() < 0.5 ? ~~(Math.random() * 100) : undefined; | |||
const isEvent = amount ? false : Math.random() < 0.5; | |||
const text = amount ? undefined : 'text'; | |||
const heal = Math.random() < 0.5; | |||
let position; | |||
if (!amount) | |||
position = Math.random() < 0.5 ? POSITION.TOP_CENTER : POSITION.BOTTOM_CENTER; | |||
const element = ['default', 'arcane', 'frost', 'fire', 'holy', 'poison'][~~(Math.random() * 6)]; | |||
const crit = amount > 50; | |||
onGetDamage({ | |||
id: o.id, | |||
event: isEvent, | |||
text, | |||
amount, | |||
element, | |||
heal, | |||
position, | |||
crit | |||
}); | |||
if (!target || !target.isVisible) | |||
return; | |||
}); | |||
}; | |||
let ttl = 35; | |||
let numberObj = { | |||
obj: target, | |||
amount: msg.amount, | |||
x: (target.x * scale), | |||
y: (target.y * scale) + scale - (scale / 4), | |||
ttl: ttl, | |||
ttlMax: ttl, | |||
event: msg.event, | |||
text: msg.text, | |||
crit: msg.crit, | |||
heal: msg.heal, | |||
element: msg.element | |||
}; | |||
if (numberObj.event) { | |||
numberObj.x += (scale / 2); | |||
numberObj.y += (scale / 2); | |||
} else if (numberObj.heal) | |||
numberObj.x -= scale; | |||
else | |||
numberObj.x += scale; | |||
let text = numberObj.text; | |||
if (!numberObj.event) { | |||
let amount = numberObj.amount; | |||
let div = ((~~(amount * 10) / 10) > 0) ? 10 : 100; | |||
text = (numberObj.heal ? '+' : '') + (~~(amount * div) / div); | |||
} | |||
const update = () => { | |||
let lLen = list.length; | |||
for (let i = 0; i < lLen; i++) { | |||
const l = list[i]; | |||
const colorVariableName = config.damageNumbers === 'element' ? numberObj.element : 'default'; | |||
numberObj.sprite = renderer.buildText({ | |||
fontSize: numberObj.crit ? 22 : 18, | |||
layerName: 'effects', | |||
x: numberObj.x, | |||
y: numberObj.y, | |||
text: text, | |||
color: elementColors[colorVariableName] | |||
}); | |||
l.ttl--; | |||
if (l.ttl === 0) { | |||
list.splice(i, 1); | |||
lLen--; | |||
renderer.destroyObject({ | |||
layerName: 'effects', | |||
sprite: l.sprite | |||
}); | |||
this.list.push(numberObj); | |||
}, | |||
update: function () { | |||
let list = this.list; | |||
let lLen = list.length; | |||
for (let i = 0; i < lLen; i++) { | |||
let l = list[i]; | |||
l.ttl--; | |||
if (l.ttl === 0) { | |||
renderer.destroyObject({ | |||
layerName: 'effects', | |||
sprite: l.sprite | |||
}); | |||
list.splice(i, 1); | |||
i--; | |||
lLen--; | |||
continue; | |||
} | |||
if (l.event) | |||
l.y += 1; | |||
else | |||
l.y -= 1; | |||
let alpha = l.ttl / l.ttlMax; | |||
l.sprite.alpha = alpha; | |||
l.sprite.x = ~~(l.x / scaleMult) * scaleMult; | |||
l.sprite.y = ~~(l.y / scaleMult) * scaleMult; | |||
if (l.event) | |||
l.sprite.x -= (l.sprite.width) / 2; | |||
continue; | |||
} | |||
l.x += l.movementDelta[0] * MOVE_SPEED; | |||
l.y += l.movementDelta[1] * MOVE_SPEED; | |||
l.sprite.x = (l.obj.x * scale) + l.x; | |||
l.sprite.y = (l.obj.y * scale) + l.y; | |||
l.sprite.alpha = l.ttl / l.ttlMax; | |||
} | |||
}; | |||
const init = () => { | |||
events.on('onGetDamage', onGetDamage); | |||
}; | |||
//Exports | |||
return { | |||
init, | |||
update, | |||
onGetDamage, | |||
POSITION | |||
}; | |||
}); |
@@ -741,13 +741,12 @@ define([ | |||
}, | |||
addFilter: function (sprite) { | |||
let thickness = (sprite.width > scale) ? 8 : 16; | |||
const filter = new shaderOutline(); | |||
let filter = new shaderOutline(this.renderer.width, this.renderer.height, thickness, '0xffffff'); | |||
if (!sprite.filters) | |||
sprite.filters = [filter]; | |||
else | |||
sprite.filters.push(); | |||
sprite.filters.push(filter); | |||
return filter; | |||
}, | |||
@@ -758,19 +757,24 @@ define([ | |||
}, | |||
buildText: function (obj) { | |||
let textSprite = new PIXI.Text(obj.text, { | |||
const { text, visible, x, y, parent: spriteParent, layerName } = obj; | |||
const { fontSize = 14, color = 0xF2F5F5 } = obj; | |||
const textSprite = new PIXI.Text(text, { | |||
fontFamily: 'bitty', | |||
fontSize: (obj.fontSize || 14), | |||
fill: obj.color || 0xF2F5F5, | |||
fontSize: fontSize, | |||
fill: color, | |||
stroke: 0x2d2136, | |||
strokeThickness: 4, | |||
align: 'center' | |||
strokeThickness: 4 | |||
}); | |||
textSprite.x = obj.x - (textSprite.width / 2); | |||
textSprite.y = obj.y; | |||
if (visible === false) | |||
textSprite.visible = false; | |||
textSprite.x = x - (textSprite.width / 2); | |||
textSprite.y = y; | |||
let parentSprite = obj.parent || this.layers[obj.layerName]; | |||
const parentSprite = spriteParent ?? this.layers[layerName]; | |||
parentSprite.addChild(textSprite); | |||
return textSprite; | |||
@@ -2,62 +2,76 @@ define([ | |||
'js/rendering/shaders/outline/vert', | |||
'js/rendering/shaders/outline/frag' | |||
], function ( | |||
vert, | |||
frag | |||
vertex, | |||
fragment | |||
) { | |||
let OutlineFilter = function (viewWidth, viewHeight, thickness, color) { | |||
thickness = thickness || 1; | |||
PIXI.Filter.call(this, | |||
vert, | |||
frag.replace(/%THICKNESS%/gi, (1.0 / thickness).toFixed(7)) | |||
); | |||
this.uniforms.pixelWidth = 0.002; | |||
this.uniforms.pixelHeight = 1.0 / (viewHeight || 1); | |||
this.uniforms.thickness = thickness; | |||
this.uniforms.outlineColor = new Float32Array([0, 0, 0, 1]); | |||
this.alpha = 0; | |||
if (color) | |||
this.color = color; | |||
}; | |||
OutlineFilter.prototype = Object.create(PIXI.Filter.prototype); | |||
OutlineFilter.prototype.constructor = OutlineFilter; | |||
Object.defineProperties(OutlineFilter.prototype, { | |||
color: { | |||
get: function () { | |||
return PIXI.utils.rgb2hex(this.uniforms.outlineColor); | |||
}, | |||
set: function (value) { | |||
PIXI.utils.hex2rgb(value, this.uniforms.outlineColor); | |||
} | |||
}, | |||
alpha: { | |||
set: function (value) { | |||
this.uniforms.alpha = value; | |||
} | |||
}, | |||
viewWidth: { | |||
get: function () { | |||
return 1 / this.uniforms.pixelWidth; | |||
}, | |||
set: function (value) { | |||
this.uniforms.pixelWidth = 1 / value; | |||
} | |||
}, | |||
viewHeight: { | |||
get: function () { | |||
return 1 / this.uniforms.pixelHeight; | |||
}, | |||
set: function (value) { | |||
this.uniforms.pixelHeight = 1 / value; | |||
} | |||
} | |||
}); | |||
const _thickness = 1; | |||
const _alpha = 1.0; | |||
const _knockout = false; | |||
class OutlineFilter extends PIXI.Filter { | |||
constructor (thickness = 5, color = 0xFFFFFF, quality = 0.1, alpha = 1.0, knockout = false) { | |||
const angleStep = Math.PI / 2; | |||
color = PIXI.utils.hex2rgb(color); | |||
super(vertex, fragment.replace('$angleStep$', angleStep)); | |||
this.uniforms.uThickness = new Float32Array([thickness, thickness]); | |||
this.uniforms.uColor = new Float32Array([0, 0.0, 0.0, 1]); | |||
this.uniforms.uAlpha = alpha; | |||
this.uniforms.uKnockout = knockout; | |||
Object.assign(this, { thickness, color, quality, alpha, knockout }); | |||
} | |||
getAngleStep (quality) { | |||
const samples = Math.max( | |||
quality * MAX_SAMPLES, | |||
MIN_SAMPLES, | |||
); | |||
return (Math.PI * 2 / samples).toFixed(7); | |||
} | |||
apply (filterManager, input, output, clear) { | |||
this.uniforms.uThickness[0] = this.thickness / input._frame.width; | |||
this.uniforms.uThickness[1] = this.thickness / input._frame.height; | |||
this.uniforms.uAlpha = this.alpha; | |||
this.uniforms.uKnockout = this.knockout; | |||
this.uniforms.uColor = PIXI.utils.hex2rgb(this.color, this.uniforms.uColor); | |||
filterManager.applyFilter(this, input, output, clear); | |||
} | |||
get alpha () { | |||
return this._alpha; | |||
} | |||
set alpha (value) { | |||
this._alpha = value; | |||
} | |||
get color () { | |||
return PIXI.utils.rgb2hex(this.uniforms.uColor); | |||
} | |||
set color (value) { | |||
PIXI.utils.hex2rgb(value, this.uniforms.uColor); | |||
} | |||
get knockout () { | |||
return this._knockout; | |||
} | |||
set knockout (value) { | |||
this._knockout = value; | |||
} | |||
get thickness () { | |||
return this._thickness; | |||
} | |||
set thickness (value) { | |||
this._thickness = value; | |||
this.padding = value; | |||
} | |||
} | |||
return OutlineFilter; | |||
}); |
@@ -6,28 +6,41 @@ define([ | |||
return ` | |||
varying vec2 vTextureCoord; | |||
uniform sampler2D uSampler; | |||
uniform vec4 filterClamp; | |||
uniform float thickness; | |||
uniform vec4 outlineColor; | |||
uniform float pixelWidth; | |||
uniform float alpha; | |||
vec2 px = vec2(pixelWidth, pixelWidth); | |||
uniform float uAlpha; | |||
uniform vec2 uThickness; | |||
uniform vec4 uColor; | |||
uniform bool uKnockout; | |||
void main(void) { | |||
const float PI = 3.14159265358979323846264; | |||
vec4 ownColor = texture2D(uSampler, vTextureCoord); | |||
vec4 curColor; | |||
const float DOUBLE_PI = 2. * 3.14159265358979323846264; | |||
const float ANGLE_STEP = $angleStep$; | |||
float outlineMaxAlphaAtPos(vec2 pos) { | |||
if (uThickness.x == 0. || uThickness.y == 0.) { | |||
return 0.; | |||
} | |||
vec4 displacedColor; | |||
vec2 displacedPos; | |||
float maxAlpha = 0.; | |||
for (float angle = 0.; angle < PI * 2.; angle += %THICKNESS% ) { | |||
curColor = texture2D(uSampler, vec2(vTextureCoord.x + thickness * px.x * cos(angle), vTextureCoord.y + thickness * px.y * sin(angle))); | |||
maxAlpha = max(maxAlpha, curColor.a); | |||
for (float angle = 0.; angle <= DOUBLE_PI; angle += ANGLE_STEP) { | |||
displacedPos.x = vTextureCoord.x + uThickness.x * cos(angle); | |||
displacedPos.y = vTextureCoord.y + uThickness.y * sin(angle); | |||
displacedColor = texture2D(uSampler, clamp(displacedPos, filterClamp.xy, filterClamp.zw)); | |||
maxAlpha = max(maxAlpha, displacedColor.a); | |||
} | |||
if (maxAlpha > 0.1) | |||
maxAlpha = alpha; | |||
else | |||
maxAlpha = 0.0; | |||
float resultAlpha = max(maxAlpha, ownColor.a); | |||
gl_FragColor = vec4((ownColor.rgb + outlineColor.rgb * (1. - ownColor.a)) * resultAlpha, resultAlpha); | |||
return maxAlpha; | |||
} | |||
void main(void) { | |||
vec4 sourceColor = texture2D(uSampler, vTextureCoord); | |||
vec4 contentColor = sourceColor * float(!uKnockout); | |||
float outlineAlpha = uAlpha * outlineMaxAlphaAtPos(vTextureCoord.xy) * (1.-sourceColor.a); | |||
vec4 outlineColor = vec4(vec3(uColor) * outlineAlpha, outlineAlpha); | |||
gl_FragColor = contentColor + outlineColor; | |||
} | |||
`; | |||
}); |
@@ -5,14 +5,30 @@ define([ | |||
) { | |||
return ` | |||
attribute vec2 aVertexPosition; | |||
attribute vec2 aTextureCoord; | |||
uniform mat3 projectionMatrix; | |||
varying vec2 vTextureCoord; | |||
void main(void){ | |||
gl_Position = vec4((projectionMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0); | |||
vTextureCoord = aTextureCoord; | |||
uniform vec4 inputSize; | |||
uniform vec4 outputFrame; | |||
vec4 filterVertexPosition( void ) | |||
{ | |||
vec2 position = aVertexPosition * max(outputFrame.zw, vec2(0.)) + outputFrame.xy; | |||
return vec4((projectionMatrix * vec3(position, 1.0)).xy, 0.0, 1.0); | |||
} | |||
vec2 filterTextureCoord( void ) | |||
{ | |||
return aVertexPosition * (outputFrame.zw * inputSize.zw); | |||
} | |||
void main(void) | |||
{ | |||
gl_Position = filterVertexPosition(); | |||
vTextureCoord = filterTextureCoord(); | |||
} | |||
`; | |||
}); |