178 lines
4.7 KiB
TypeScript
178 lines
4.7 KiB
TypeScript
import React from 'react';
|
|
import resolveClassName from '../../util/resolveClassName';
|
|
import classNames from './SplitFlapBoard.scss';
|
|
|
|
interface ISplitFlapBoardProps {
|
|
/** Message the split flap board shall display */
|
|
message: string;
|
|
/** True to start the animation */
|
|
start: boolean;
|
|
/** Size of one split flap module, default of 40px */
|
|
size?: number;
|
|
/** Duration of one transition animation */
|
|
duration?: number;
|
|
}
|
|
|
|
const SplitFlapBoard: React.FunctionComponent<ISplitFlapBoardProps> = ({
|
|
message,
|
|
start,
|
|
size,
|
|
duration,
|
|
}: ISplitFlapBoardProps) => {
|
|
const classesMain = {
|
|
name: 'split-flap',
|
|
modifiers: [],
|
|
};
|
|
const classesTopNext = {
|
|
name: 'top-next',
|
|
modifiers: [],
|
|
};
|
|
const classesTopCurrent = {
|
|
name: 'top-current',
|
|
modifiers: ['stop', 'animate'],
|
|
};
|
|
const classesBottomCurrent = {
|
|
name: 'bottom-current',
|
|
modifiers: [],
|
|
};
|
|
const classesBottomNext = {
|
|
name: 'bottom-next',
|
|
modifiers: ['stop', 'animate'],
|
|
};
|
|
|
|
const typeSet = [
|
|
' ',
|
|
'A',
|
|
'B',
|
|
'C',
|
|
'D',
|
|
'E',
|
|
'F',
|
|
'G',
|
|
'H',
|
|
'I',
|
|
'J',
|
|
'K',
|
|
'L',
|
|
'M',
|
|
'N',
|
|
'O',
|
|
'P',
|
|
'Q',
|
|
'R',
|
|
'S',
|
|
'T',
|
|
'U',
|
|
'V',
|
|
'W',
|
|
'X',
|
|
'Y',
|
|
'Z',
|
|
'1',
|
|
'2',
|
|
'3',
|
|
'4',
|
|
'5',
|
|
'6',
|
|
'7',
|
|
'8',
|
|
'9',
|
|
'0',
|
|
];
|
|
|
|
const [animate, setAnimate] = React.useState(false);
|
|
const currentCharacter = React.useRef<number[]>(Array(message.length).fill(0));
|
|
const nextCharacter = React.useRef<number[]>(Array(message.length).fill(1));
|
|
const flapStop = React.useRef<boolean[]>(Array(message.length).fill(false));
|
|
const timeout = React.useRef<NodeJS.Timeout>();
|
|
|
|
const animation = () => {
|
|
for (let i = 0; i < currentCharacter.current.length; i++) {
|
|
if (typeSet[currentCharacter.current[i]] !== message.split('')[i]) {
|
|
currentCharacter.current[i] = nextCharacter.current[i];
|
|
nextCharacter.current[i] = (nextCharacter.current[i] % 36) + 1;
|
|
if (typeSet[currentCharacter.current[i]] === message.split('')[i]) {
|
|
flapStop.current[i] = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
setAnimate(false);
|
|
|
|
if (!flapStop.current.reduce((acc, val) => acc && val)) {
|
|
timeout.current = setTimeout(() => {
|
|
setAnimate(true);
|
|
timeout.current = setTimeout(animation, duration ? duration * 1000 : 2000);
|
|
}, Math.max(25, Math.random() * 100));
|
|
}
|
|
};
|
|
|
|
React.useEffect(() => {
|
|
if (start) {
|
|
setAnimate(true);
|
|
timeout.current = setTimeout(animation, duration ? duration * 1000 : 2000);
|
|
}
|
|
}, [start]); //eslint-disable-line react-hooks/exhaustive-deps
|
|
|
|
React.useEffect(() => {
|
|
return () => clearTimeout(timeout.current);
|
|
}, []);
|
|
|
|
return (
|
|
<div className={classNames.container}>
|
|
{message.split('').map((char, index) => {
|
|
return (
|
|
<div
|
|
key={`${char}-${index}`}
|
|
className={resolveClassName(classNames, classesMain, {})}
|
|
style={{
|
|
width: size ?? null,
|
|
height: size ?? null,
|
|
lineHeight: size ? `${size}px` : null,
|
|
fontSize: size ?? null,
|
|
perspective: size ? size * 10 : null,
|
|
}}
|
|
>
|
|
{/* TOP NEXT CHAR */}
|
|
<div
|
|
className={resolveClassName(classNames, classesTopNext, {})}
|
|
style={{ borderRadius: size ? `${size / 8}px ${size / 8}px 0 0` : null }}
|
|
>
|
|
{typeSet[nextCharacter.current[index]]}
|
|
</div>
|
|
{/* TOP CUR CHAR */}
|
|
<div
|
|
className={resolveClassName(classNames, classesTopCurrent, { animate, stop: flapStop.current[index] })}
|
|
style={{
|
|
animationDuration: `${duration}s` ?? null,
|
|
borderRadius: size ? `${size / 8}px ${size / 8}px 0 0` : null,
|
|
}}
|
|
>
|
|
{typeSet[currentCharacter.current[index]]}
|
|
</div>
|
|
{/* BOTTOM CUR CHAR */}
|
|
<div
|
|
className={resolveClassName(classNames, classesBottomCurrent, {})}
|
|
style={{ borderRadius: size ? `0 0 ${size / 8}px ${size / 8}px` : null }}
|
|
>
|
|
<span>{typeSet[currentCharacter.current[index]]}</span>
|
|
</div>
|
|
{/* BOTTOM NEXT CHAR */}
|
|
<div
|
|
className={resolveClassName(classNames, classesBottomNext, { animate, stop: flapStop.current[index] })}
|
|
style={{
|
|
animationDuration: `${duration}s` ?? null,
|
|
borderRadius: size ? `0 0 ${size / 8}px ${size / 8}px` : null,
|
|
}}
|
|
>
|
|
<span>{typeSet[nextCharacter.current[index]]}</span>
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default SplitFlapBoard;
|