import {useEffect, useRef, useState} from 'react'; import {RESOLUTION} from '@/constants.js'; import {useDomScale} from '@/context/dom-scale.js'; import {TAU} from '@/util/math.js'; import styles from './dialogue.module.css'; const CARET_SIZE = 12; export default function Dialogue({ camera, dialogue, scale, }) { const domScale = useDomScale(); const ref = useRef(); const [dimensions, setDimensions] = useState({h: 0, w: 0}); const [caret, setCaret] = useState(0); const [radians, setRadians] = useState(0); useEffect(() => { setRadians(0); let handle; let last; const spin = (ts) => { if ('undefined' === typeof last) { last = ts; } const elapsed = (ts - last) / 1000; last = ts; setRadians((radians) => radians + (elapsed * TAU)); handle = requestAnimationFrame(spin); }; handle = requestAnimationFrame(spin); return () => { cancelAnimationFrame(handle); }; }, []); useEffect(() => { const {params} = dialogue.letters[caret]; let handle; if (caret >= dialogue.letters.length - 1) { const {linger = 5} = dialogue; if (!linger) { dialogue.onClose(); return; } handle = setTimeout(() => { dialogue.onClose(); }, linger * 1000); } else { let jump = caret; while (0 === dialogue.letters[jump].params.rate.frequency && jump < dialogue.letters.length - 1) { jump += 1; } setCaret(jump); if (jump < dialogue.letters.length - 1) { handle = setTimeout(() => { setCaret(caret + 1); }, params.rate.frequency * 1000) } } return () => { clearTimeout(handle); } }, [caret, dialogue]); useEffect(() => { let handle; function track() { if (ref.current) { const {height, width} = ref.current.getBoundingClientRect(); setDimensions({h: height / domScale, w: width / domScale}); } handle = requestAnimationFrame(track); } handle = requestAnimationFrame(track); return () => { cancelAnimationFrame(handle); }; }, [dialogue, domScale, ref]); const origin = 'function' === typeof dialogue.origin ? dialogue.origin() : dialogue.origin || {x: 0, y: 0}; const bounds = { x: dimensions.w / (2 * scale), y: dimensions.h / (2 * scale), }; const left = Math.max( Math.min( dialogue.position.x * scale - camera.x, RESOLUTION.x - bounds.x * scale - 16, ), bounds.x * scale + 16, ); const top = Math.max( Math.min( dialogue.position.y * scale - camera.y, RESOLUTION.y - bounds.y * scale - 16, ), bounds.y * scale + 88, ); const offsetPosition = { x: ((dialogue.position.x * scale - camera.x) - left) / scale, y: ((dialogue.position.y * scale - camera.y) - top) / scale, }; const difference = { x: origin.x - dialogue.position.x + offsetPosition.x, y: origin.y - dialogue.position.y + offsetPosition.y, }; const within = { x: Math.abs(difference.x) < bounds.x, y: Math.abs(difference.y) < bounds.y, }; const caretPosition = { x: Math.max(-bounds.x, Math.min(origin.x - dialogue.position.x + offsetPosition.x, bounds.x)), y: Math.max(-bounds.y, Math.min(origin.y - dialogue.position.y + offsetPosition.y, bounds.y)), }; let caretRotation = Math.atan2( difference.y - caretPosition.y, difference.x - caretPosition.x, ); caretRotation += Math.PI * 1.5; if (within.x) { caretPosition.y = bounds.y * Math.sign(difference.y); if (within.y) { if (Math.sign(difference.y) > 0) { caretRotation = Math.PI; } else { caretRotation = 0; } } } else if (within.y) { caretPosition.x = bounds.x * Math.sign(difference.x); } caretPosition.x *= scale; caretPosition.y *= scale; caretPosition.x += -Math.sin(caretRotation) * (CARET_SIZE / 2); caretPosition.y += Math.cos(caretRotation) * (CARET_SIZE / 2); return (
{ dialogue.letters .map(({character, indices, params}, i) => { let color = 'inherit'; let fade = 0; let fontStyle = 'normal'; let fontWeight = 'normal'; let left = 0; let opacity = 1; let top = 0; if (params.blink) { const {frequency = 1} = params.blink; opacity = (radians * (2 / frequency) % TAU) > (TAU / 2) ? opacity : 0; } if (params.fade) { const {frequency = 1} = params.fade; fade = frequency; } if (params.wave) { const {frequency = 1, magnitude = 3} = params.wave; top += magnitude * Math.cos((radians * (1 / frequency)) + TAU * indices.wave / params.wave.length); } if (params.rainbow) { const {frequency = 1} = params.rainbow; color = `hsl(${(radians * (1 / frequency)) + TAU * indices.rainbow / params.rainbow.length}rad 100 50)`; } if (params.shake) { const {magnitude = 1} = params.shake; left += (Math.random() * magnitude * 2) - magnitude; top += (Math.random() * magnitude * 2) - magnitude; } if (params.em) { fontStyle = 'italic'; } if (params.strong) { fontWeight = 'bold'; } return ( {character} ); }) }
); }