@@ -9,8 +9,28 @@ define([ | |||||
renderer, | renderer, | ||||
config | 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. | //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( | const elementColors = Object.fromEntries( | ||||
['default', 'arcane', 'frost', 'fire', 'holy', 'poison'].map(e => { | ['default', 'arcane', 'frost', 'fire', 'holy', 'poison'].map(e => { | ||||
const variableName = `--color-element-${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; | 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) { | 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) | if (!sprite.filters) | ||||
sprite.filters = [filter]; | sprite.filters = [filter]; | ||||
else | else | ||||
sprite.filters.push(); | |||||
sprite.filters.push(filter); | |||||
return filter; | return filter; | ||||
}, | }, | ||||
@@ -758,19 +757,24 @@ define([ | |||||
}, | }, | ||||
buildText: function (obj) { | 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', | fontFamily: 'bitty', | ||||
fontSize: (obj.fontSize || 14), | |||||
fill: obj.color || 0xF2F5F5, | |||||
fontSize: fontSize, | |||||
fill: color, | |||||
stroke: 0x2d2136, | 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); | parentSprite.addChild(textSprite); | ||||
return textSprite; | return textSprite; | ||||
@@ -2,62 +2,76 @@ define([ | |||||
'js/rendering/shaders/outline/vert', | 'js/rendering/shaders/outline/vert', | ||||
'js/rendering/shaders/outline/frag' | 'js/rendering/shaders/outline/frag' | ||||
], function ( | ], 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; | return OutlineFilter; | ||||
}); | }); |
@@ -6,28 +6,41 @@ define([ | |||||
return ` | return ` | ||||
varying vec2 vTextureCoord; | varying vec2 vTextureCoord; | ||||
uniform sampler2D uSampler; | 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.; | 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 ` | return ` | ||||
attribute vec2 aVertexPosition; | attribute vec2 aVertexPosition; | ||||
attribute vec2 aTextureCoord; | |||||
uniform mat3 projectionMatrix; | uniform mat3 projectionMatrix; | ||||
varying vec2 vTextureCoord; | 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(); | |||||
} | } | ||||
`; | `; | ||||
}); | }); |