diff --git a/app/react/components/dom/damage.jsx b/app/react/components/dom/damage.jsx deleted file mode 100644 index 920d29b..0000000 --- a/app/react/components/dom/damage.jsx +++ /dev/null @@ -1,58 +0,0 @@ -import {memo, useEffect, useState} from 'react'; - -import {DamageTypes} from '@/ecs/components/vulnerable.js'; - -import styles from './damage.module.css'; - -const damageTypeMap = { - [DamageTypes.PAIN]: styles.pain, - [DamageTypes.HEALING]: styles.healing, - [DamageTypes.MANA]: styles.mana, -}; - -function Damage({ - damage, - scale, - zIndex, -}) { - const [randomness] = useState({ - radians: Math.random() * (Math.PI / 16) - (Math.PI / 32), - x: 1 * (Math.random() - 0.5), - y: Math.random(), - }) - useEffect(() => { - const handle = setTimeout(() => { - damage.onClose(); - }, 1_500); - return () => { - clearTimeout(handle); - }; - }, [damage]); - const {amount, position} = damage; - return ( -
-

- {Math.abs(amount)} -

-
- ); -} - -export default memo(Damage); diff --git a/app/react/components/dom/damage.module.css b/app/react/components/dom/damage.module.css index 5dc575c..47d4493 100644 --- a/app/react/components/dom/damage.module.css +++ b/app/react/components/dom/damage.module.css @@ -13,16 +13,6 @@ inherits: false; syntax: ''; } -@property --uiScale { - initial-value: 0; - inherits: false; - syntax: ''; -} -@property --shimmer { - initial-value: ''; - inherits: false; - syntax: ''; -} @property --offsetX { initial-value: 0; inherits: false; @@ -52,14 +42,8 @@ --scale: 0.35; --background: hsl(var(--hue) 100% 12.5%); --foreground: hsl(var(--hue) 100% 50%); - animation: - fade 1.5s linear forwards, - grow 1.5s ease-in forwards, - move 1.5s cubic-bezier(0.5, 1, 0.89, 1), - var(--shimmer) 300ms infinite cubic-bezier(0.87, 0, 0.13, 1) - ; color: var(--foreground); - font-size: calc(5px + (var(--magnitude) * 6px)); + font-size: calc(10px + (var(--magnitude) * 12px)); opacity: var(--opacity); overflow-wrap: break-word; position: absolute; @@ -80,75 +64,8 @@ calc(-50% + (1px * var(--offsetY)) + var(--positionY)) ; user-select: none; + will-change: color, scale, opacity, translate, transform; p { margin: 0; } } - -@keyframes move { - 0% { - --offsetX: 0; - --offsetY: 0; - } - 33%, 91.6% { - --offsetX: calc(20 * var(--uiScale) * var(--randomnessX)); - --offsetY: calc(-1 * (10 + var(--randomnessY) * 22.5 * var(--uiScale))); - } -} - -@keyframes fade { - 0% { - --opacity: 0.75; - } - 25% { - --opacity: 1; - } - 91.6% { - --opacity: 1; - } - 100% { - --opacity: 0; - } -} - -@keyframes grow { - 0% { - --scale: 0.35; - } - 33% { - --scale: 1; - } - 45% { - --scale: 1; - } - 40% { - --scale: 1.5; - } - 45% { - --scale: 1; - } - 91.6% { - --scale: 1; - } - 95% { - --scale: 0; - } -} - -@keyframes pain { - 0% { --hue: 0 } - 50% { --hue: 30 } - 100% { --hue: 0 } -} - -@keyframes healing { - 0% { --hue: 120 } - 50% { --hue: 90 } - 100% { --hue: 120 } -} - -@keyframes mana { - 0% { --hue: 220 } - 50% { --hue: 215 } - 100% { --hue: 220 } -} diff --git a/app/react/components/dom/damages.jsx b/app/react/components/dom/damages.jsx index 79b70fd..59bf870 100644 --- a/app/react/components/dom/damages.jsx +++ b/app/react/components/dom/damages.jsx @@ -1,22 +1,172 @@ +import {memo, useCallback, useRef} from 'react'; + +import {DamageTypes} from '@/ecs/components/vulnerable.js'; +import useAnimationFrame from '@/react/hooks/use-animation-frame.js'; +import {easeInOutExpo, easeInQuint, easeOutQuad, linear} from '@/util/easing.js'; + import styles from './damages.module.css'; -import Damage from './damage.jsx'; - -export default function Damages({damages, scale}) { - const elements = []; - for (const key in damages) { - elements.push( - - ); +function damageHue(type) { + let hue; + switch(type) { + case DamageTypes.PAIN: { + hue = [0, 30]; + break; + } + case DamageTypes.HEALING: { + hue = [90, 120]; + break; + } + case DamageTypes.MANA: { + hue = [215, 220]; + break; + } } - if (0 === elements.length) { - return false; - } - return
{elements}
; + return hue; } +function createDamageNode() { + const damage = document.createElement('div'); + damage.classList.add(styles.damage); + damage.appendChild(document.createElement('p')); + return damage; +} + +function Damages({damages, scale}) { + const animations = useRef({}); + const pool = useRef([]); + const damagesRef = useRef(); + const frame = useCallback((elapsed) => { + if (!damagesRef.current) { + return; + } + if (0 === pool.current.length) { + for (let i = 0; i < 512; ++i) { + const damage = createDamageNode(); + damagesRef.current.appendChild(damage); + pool.current.push(damage); + } + } + const keys = Object.keys(animations.current); + for (const key of keys) { + const animation = animations.current[key]; + if (!damages[key]) { + if (animation.element) { + if (pool.current.length < 512) { + pool.current.push(animation.element); + } + animation.element = undefined; + } + delete animations.current[key]; + continue; + } + if (!animation.element) { + const {amount, position} = damages[key]; + let damage; + if (pool.current.length > 0) { + damage = pool.current.pop(); + } + else { + damage = createDamageNode(); + damagesRef.current.appendChild(damage); + } + const p = damage.querySelector('p'); + p.style.scale = scale / 2; + p.innerText = Math.abs(amount); + damage.style.setProperty('--randomnessX', animation['--randomnessX']); + damage.style.setProperty('--randomnessY', animation['--randomnessY']); + damage.style.setProperty('rotate', animation['rotate']); + damage.style.setProperty('rotate', animation['rotate']); + damage.style.setProperty('--magnitude', Math.max(1, Math.floor(Math.log10(Math.abs(amount))))); + damage.style.setProperty('--positionX', `${position.x * scale}px`); + damage.style.setProperty('--positionY', `${position.y * scale}px`); + damage.style.setProperty('zIndex', key); + animation.element = damage; + } + animation.elapsed += elapsed; + animation.step += elapsed; + const offsetX = 20 * scale * animation['--randomnessX']; + const offsetY = -1 * (10 + animation['--randomnessY'] * 45 * scale); + // offset + if (animation.elapsed <= 0.5) { + animation['--offsetX'] = easeOutQuad(animation.elapsed, 0, offsetX, 0.5); + animation['--offsetY'] = easeOutQuad(animation.elapsed, 0, offsetY, 0.5); + } + else if (animation.elapsed > 1.375) { + animation['--offsetX'] = offsetX - easeOutQuad(animation.elapsed - 1.375, 0, offsetX, 0.125); + animation['--offsetY'] = offsetY - easeOutQuad(animation.elapsed - 1.375, 0, offsetY, 0.125); + } + // scale + if (animation.elapsed <= 0.5) { + animation['--scale'] = easeOutQuad(animation.elapsed, 0, 1, 0.5); + } + else if (animation.elapsed > 0.5 && animation.elapsed < 0.6) { + animation['--scale'] = linear(animation.elapsed - 0.5, 1, 0.5, 0.1); + } + else if (animation.elapsed > 0.6 && animation.elapsed < 0.675) { + animation['--scale'] = 1.5 - easeInQuint(animation.elapsed - 0.6, 1, 0.5, 0.075); + } + else if (animation.elapsed > 0.675 && animation.elapsed < 1.375) { + animation['--scale'] = 1; + } + else if (animation.elapsed > 1.375) { + animation['--scale'] = 1 - easeOutQuad(animation.elapsed - 1.375, 0, 1, 0.125); + } + // fade + if (animation.elapsed <= 0.375) { + animation['--opacity'] = linear(animation.elapsed, 0.75, 0.25, 0.375); + } + else if (animation.elapsed > 0.375 && animation.elapsed < 1.375) { + animation['--opacity'] = 1; + } + else if (animation.elapsed > 1.375) { + animation['--opacity'] = 1 - linear(animation.elapsed - 1.375, 0, 1, 0.125); + } + // hue + const h = Math.abs((animation.elapsed % 0.3) - 0.15) / 0.15; + animation['--hue'] = easeInOutExpo(h, animation.hue[0], animation.hue[1], 1); + const step = keys.length > 150 ? (keys.length / 500) * 0.25 : elapsed; + if (animation.step > step) { + animation.step = animation.step % step; + animation.element.style.setProperty('--hue', animation['--hue']); + animation.element.style.setProperty('--opacity', animation['--opacity']); + animation.element.style.setProperty('--offsetX', animation['--offsetX']); + animation.element.style.setProperty('--offsetY', animation['--offsetY']); + animation.element.style.setProperty('--scale', animation['--scale']); + } + if (animation.elapsed > 1.5) { + if (pool.current.length < 512) { + pool.current.push(animation.element); + } + animation.element.style.setProperty('--opacity', 0); + animation.element = undefined; + damages[key].onClose(); + } + } + }, [damages, scale]); + useAnimationFrame(frame); + for (const key in damages) { + if (!animations.current[key]) { + animations.current[key] = { + elapsed: 0, + hue: damageHue(damages[key].type), + rotate: `${Math.random() * (Math.PI / 16) - (Math.PI / 32)}rad`, + step: 0, + '--scale': 0.35, + '--opacity': 0.75, + '--offsetX': 0, + '--offsetY': 0, + '--randomnessX': 1 * (Math.random() - 0.5), + '--randomnessY': Math.random(), + }; + } + } + return ( +
+ ); +} + +export default memo(Damages); diff --git a/app/react/components/dom/damages.module.css b/app/react/components/dom/damages.module.css index acd2d67..4ce6d57 100644 --- a/app/react/components/dom/damages.module.css +++ b/app/react/components/dom/damages.module.css @@ -1,3 +1,75 @@ +@property --hue { + initial-value: 0; + inherits: false; + syntax: ''; +} +@property --opacity { + initial-value: 0; + inherits: false; + syntax: ''; +} +@property --scale { + initial-value: 0; + inherits: false; + syntax: ''; +} +@property --offsetX { + initial-value: 0; + inherits: false; + syntax: ''; +} +@property --offsetY { + initial-value: 0; + inherits: false; + syntax: ''; +} +@property --randomnessX { + initial-value: 0; + inherits: false; + syntax: ''; +} +@property --randomnessY { + initial-value: 0; + inherits: false; + syntax: ''; +} + +.damage { + --hue: 0; + --opacity: 0.75; + --randomnessX: 0; + --randomnessY: 0; + --scale: 0.35; + --background: hsl(var(--hue) 100% 12.5%); + --foreground: hsl(var(--hue) 100% 50%); + color: var(--foreground); + font-size: calc(10px + (var(--magnitude) * 12px)); + opacity: var(--opacity); + overflow-wrap: break-word; + position: absolute; + margin: 0; + text-shadow: + 0px -1px 0px var(--background), + 1px 0px 0px var(--background), + 0px 1px 0px var(--background), + -1px 0px 0px var(--background), + 0px -2px 0px var(--background), + 2px 0px 0px var(--background), + 0px 2px 0px var(--background), + -2px 0px 0px var(--background) + ; + scale: var(--scale); + translate: + calc(-50% + (1px * var(--offsetX)) + var(--positionX)) + calc(-50% + (1px * var(--offsetY)) + var(--positionY)) + ; + user-select: none; + will-change: color, scale, opacity, translate, transform; + p { + margin: 0; + } +} + .damages { font-family: Joystix, 'Courier New', Courier, monospace; }