silphius/app/util/dialogue.js
2024-07-22 07:55:05 -05:00

136 lines
3.9 KiB
JavaScript

import {createElement} from 'react';
import mdx from 'remark-mdx';
import parse from 'remark-parse';
import {createNoise2D} from 'simplex-noise';
import {unified} from 'unified';
import {visitParents as visit} from 'unist-util-visit-parents';
import {TAU} from '@/util/math.js';
const rawNoise = createNoise2D();
const noise = (x, y) => (1 + rawNoise(x, y)) / 2;
const parser = unified().use(parse).use(mdx);
function computeParams(ancestors) {
const params = {};
for (let i = ancestors.length - 1; i >= 0; --i) {
const {dialogue} = ancestors[i];
if (dialogue) {
if (!(dialogue.name in params)) {
params[dialogue.name] = dialogue.params;
}
}
}
return params;
}
export function parseLetters(source) {
let letters = [];
try {
const tree = parser.parse(source);
tree.dialogue = {
name: 'rate',
params: {frequency: 0.05, length: 0},
};
visit(tree, (node, ancestors) => {
switch (node.type) {
case 'mdxJsxFlowElement':
case 'mdxJsxTextElement': {
node.dialogue = {name: node.name, params: {length: 0}};
for (const {name, value: {value}} of node.attributes) {
node.dialogue.params[name] = value;
}
break;
}
case 'text': {
const params = computeParams(ancestors);
const split = node.value.split('');
for (let i = 0; i < split.length; ++i) {
const indices = {};
for (const name in params) {
indices[name] = i + params[name].length;
}
letters.push({character: split[i], indices, params});
}
for (const name in params) {
params[name].length += split.length;
}
break;
}
}
});
}
catch (e) {
letters = source.split('').map((character) => (({
character,
indices: {},
params: {rate: {frequency: 0.05}},
})));
}
return letters;
}
export function render(letters, className) {
return (caret, radians) => (
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;
const r = radians + TAU * indices.shake / params.shake.length;
left += (noise(-Math.sin(r) * 32, Math.cos(r) * 32) * magnitude * 2) - magnitude;
top += (noise(Math.sin(r) * 32, -Math.cos(r) * 32) * magnitude * 2) - magnitude;
}
if (params.em) {
fontStyle = 'italic';
}
if (params.strong) {
fontWeight = 'bold';
}
return (
createElement(
'span',
{
className,
key: i,
style: {
color,
fontStyle,
fontWeight,
left: `${left}px`,
opacity: i <= caret ? opacity : 0,
position: 'relative',
top: `${top}px`,
transition: `opacity ${fade}s`,
},
},
character,
)
);
})
);
}