131 lines
3.7 KiB
JavaScript
131 lines
3.7 KiB
JavaScript
import {createElement} from 'react';
|
|
import mdx from 'remark-mdx';
|
|
import parse from 'remark-parse';
|
|
import {unified} from 'unified';
|
|
import {visitParents as visit} from 'unist-util-visit-parents';
|
|
|
|
import {TAU} from '@/util/math.js';
|
|
|
|
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;
|
|
left += (Math.random() * magnitude * 2) - magnitude;
|
|
top += (Math.random() * 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,
|
|
)
|
|
);
|
|
})
|
|
);
|
|
}
|