上傳檔案到「/」
This commit is contained in:
109
TypingEffect.jsx
Normal file
109
TypingEffect.jsx
Normal file
@@ -0,0 +1,109 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
|
||||
export default function TypingEffect({
|
||||
steps = [],
|
||||
loop = true,
|
||||
typingSpeed = [40, 120],
|
||||
deleteSpeed = [20, 60],
|
||||
}) {
|
||||
const [displayed, setDisplayed] = useState('');
|
||||
const [showCursor, setShowCursor] = useState(true);
|
||||
const [cursorBlink, setCursorBlink] = useState(true);
|
||||
|
||||
const configKey = JSON.stringify({ steps, loop, typingSpeed, deleteSpeed });
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
let timerId = null;
|
||||
|
||||
const sleep = (ms) =>
|
||||
new Promise((resolve) => {
|
||||
timerId = setTimeout(resolve, ms);
|
||||
});
|
||||
|
||||
const rand = ([min, max]) =>
|
||||
Math.floor(min + Math.random() * (max - min + 1));
|
||||
|
||||
const run = async () => {
|
||||
do {
|
||||
let chars = [];
|
||||
setDisplayed('');
|
||||
|
||||
for (const step of steps) {
|
||||
if (cancelled) return;
|
||||
|
||||
if ('text' in step) {
|
||||
const speed = step.speed || typingSpeed;
|
||||
for (const c of Array.from(step.text)) {
|
||||
if (cancelled) return;
|
||||
chars.push(c);
|
||||
setDisplayed(chars.join(''));
|
||||
await sleep(rand(speed));
|
||||
}
|
||||
} else if ('pause' in step) {
|
||||
await sleep(step.pause);
|
||||
} else if ('delete' in step) {
|
||||
const speed = step.speed || deleteSpeed;
|
||||
const count =
|
||||
step.delete === 'all'
|
||||
? chars.length
|
||||
: Math.min(step.delete, chars.length);
|
||||
for (let i = 0; i < count; i++) {
|
||||
if (cancelled) return;
|
||||
chars.pop();
|
||||
setDisplayed(chars.join(''));
|
||||
await sleep(rand(speed));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!loop) {
|
||||
setShowCursor(false);
|
||||
return;
|
||||
}
|
||||
|
||||
await sleep(500);
|
||||
} while (!cancelled);
|
||||
};
|
||||
|
||||
run();
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
if (timerId != null) clearTimeout(timerId);
|
||||
};
|
||||
}, [configKey]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!showCursor) return;
|
||||
const id = setInterval(() => setCursorBlink((v) => !v), 530);
|
||||
return () => clearInterval(id);
|
||||
}, [showCursor]);
|
||||
|
||||
const parts = displayed.split('\n');
|
||||
|
||||
return (
|
||||
<span>
|
||||
{parts.map((part, i) => (
|
||||
<React.Fragment key={i}>
|
||||
{i > 0 && <br />}
|
||||
{part}
|
||||
</React.Fragment>
|
||||
))}
|
||||
{showCursor && (
|
||||
<span
|
||||
aria-hidden="true"
|
||||
style={{
|
||||
display: 'inline-block',
|
||||
width: '2px',
|
||||
height: '1em',
|
||||
backgroundColor: 'currentColor',
|
||||
marginLeft: '1px',
|
||||
verticalAlign: 'text-bottom',
|
||||
opacity: cursorBlink ? 1 : 0,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user