diff --git a/src/client/js/rendering/numbers.js b/src/client/js/rendering/numbers.js index 4d1b059c..bb6ba405 100644 --- a/src/client/js/rendering/numbers.js +++ b/src/client/js/rendering/numbers.js @@ -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 + }; }); diff --git a/src/client/js/rendering/renderer.js b/src/client/js/rendering/renderer.js index cc4315b9..d365ce45 100644 --- a/src/client/js/rendering/renderer.js +++ b/src/client/js/rendering/renderer.js @@ -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; diff --git a/src/client/js/rendering/shaders/outline.js b/src/client/js/rendering/shaders/outline.js index 2fef7657..1adce52a 100644 --- a/src/client/js/rendering/shaders/outline.js +++ b/src/client/js/rendering/shaders/outline.js @@ -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; }); diff --git a/src/client/js/rendering/shaders/outline/frag.js b/src/client/js/rendering/shaders/outline/frag.js index a0b125c8..1649033f 100644 --- a/src/client/js/rendering/shaders/outline/frag.js +++ b/src/client/js/rendering/shaders/outline/frag.js @@ -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; } `; }); diff --git a/src/client/js/rendering/shaders/outline/vert.js b/src/client/js/rendering/shaders/outline/vert.js index 242a65e1..afeaf05c 100644 --- a/src/client/js/rendering/shaders/outline/vert.js +++ b/src/client/js/rendering/shaders/outline/vert.js @@ -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(); } `; });