236 lines
6.7 KiB
JavaScript
236 lines
6.7 KiB
JavaScript
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 (
|
|
<div
|
|
ref={ref}
|
|
style={{
|
|
backgroundColor: '#00000044',
|
|
border: 'solid 1px white',
|
|
borderRadius: '8px',
|
|
color: 'white',
|
|
padding: '1em',
|
|
position: 'absolute',
|
|
left: `${left}px`,
|
|
margin: '0',
|
|
top: `${top}px`,
|
|
transform: 'translate(-50%, -50%)',
|
|
userSelect: 'none',
|
|
whiteSpace: 'nowrap',
|
|
}}
|
|
>
|
|
<svg
|
|
className={styles.caret}
|
|
viewBox="0 0 24 24"
|
|
width={CARET_SIZE}
|
|
height={CARET_SIZE}
|
|
style={{
|
|
transform: `
|
|
translate(
|
|
calc(-50% + ${caretPosition.x}px),
|
|
calc(-50% + ${caretPosition.y}px)
|
|
)
|
|
rotate(${caretRotation}rad)
|
|
`,
|
|
}}
|
|
>
|
|
<polygon points="0 0, 24 0, 12 24" />
|
|
</svg>
|
|
{
|
|
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 (
|
|
<span
|
|
className={styles.letter}
|
|
key={i}
|
|
style={{
|
|
color,
|
|
fontStyle,
|
|
fontWeight,
|
|
left: `${left}px`,
|
|
opacity: i <= caret ? opacity : 0,
|
|
top: `${top}px`,
|
|
transition: `opacity ${fade}s`,
|
|
}}
|
|
>
|
|
{character}
|
|
</span>
|
|
);
|
|
})
|
|
}
|
|
</div>
|
|
);
|
|
}
|