2021-08-02 15:36:12 +02:00

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;