silphius/app/react-components/dom/dialogue.jsx

141 lines
3.7 KiB
React
Raw Normal View History

2024-07-12 02:10:22 -05:00
import {useEffect, useRef, useState} from 'react';
import {RESOLUTION} from '@/constants.js';
import {useDomScale} from '@/context/dom-scale.js';
import styles from './dialogue.module.css';
const CARET_SIZE = 12;
export default function Dialogue({
camera,
dialogue,
onClose,
scale,
}) {
const domScale = useDomScale();
const ref = useRef();
const [dimensions, setDimensions] = useState({h: 0, w: 0});
useEffect(() => {
const {ttl = 5} = dialogue;
if (!ttl) {
return;
}
setTimeout(() => {
onClose();
}, ttl * 1000);
}, [dialogue, onClose]);
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.body}
</div>
);
}