forgeplus-react/src/forge/Main/projecthome/v2/components/terminalAnimation1.jsx

275 lines
7.5 KiB
JavaScript

import React, { useEffect, useRef, useState } from 'react';
const TerminalAnimation = ({ startAnimation = false }) => {
const [animationState, setAnimationState] = useState({
lineI: 0,
charI: 0,
curOn: true
});
const animationRef = useRef(null);
const lines = [
'git clone https://gitlink.org.cn/Gitlink/forgeplus.git',
'cd forgeplus',
'git remote -v',
'git pull origin master',
'git add .',
'git commit -m "feat: add new feature"',
'git push origin master',
'git checkout -b feature/new-ui',
'git merge develop',
'git status ',
'git log --oneline -10',
];
const keyColor = {
'git clone': '#fff700',
'git commit': '#fff700',
'git log': '#fff700',
'git merge': '#fff700',
'git remote': '#fff700',
'git pull': '#d24a6f',
'git push': '#d24a6f',
'git status': '#2977ff',
'git fetch': '#2977ff',
'git add': '#9aff41',
'cd forgeplus': '#9aff41',
'git checkout': '#9aff41'
};
const fontSize = 15;
const lineH = 36;
const startY = 40;
const charGap = 30;
const lineGap = 300;
const lineNumberWidth = 40;
// 字符宽度估算函数
const getCharWidth = (charCount) => charCount * 9.6;
// 渲染带颜色的文本段
const renderColoredText = (text, x, y, availableChars) => {
const elements = [];
let currentX = x;
let charsRendered = 0;
const sortedKeys = Object.keys(keyColor).sort((a, b) => b.length - a.length);
let remainingText = text;
while (remainingText.length > 0 && charsRendered < availableChars) {
let colored = false;
for (const key of sortedKeys) {
if (remainingText.startsWith(key)) {
const charsToRender = Math.min(key.length, availableChars - charsRendered);
if (charsToRender > 0) {
const displayText = key.substring(0, charsToRender);
elements.push(
<text
key={`colored-${currentX}-${charsRendered}`}
x={currentX}
y={y}
fill={keyColor[key]}
fontFamily="'Courier New', monospace"
fontSize={fontSize}
dominantBaseline="middle"
fontWeight="bold"
>
{displayText}
</text>
);
currentX += getCharWidth(displayText.length);
charsRendered += charsToRender;
remainingText = remainingText.substring(charsToRender);
colored = true;
}
break;
}
}
if (!colored && remainingText.length > 0) {
const charsToRender = Math.min(1, availableChars - charsRendered);
if (charsToRender > 0) {
const char = remainingText[0];
elements.push(
<text
key={`normal-${currentX}-${charsRendered}`}
x={currentX}
y={y}
fill="#ffffff"
fontFamily="'Courier New', monospace"
fontSize={fontSize}
dominantBaseline="middle"
fontWeight="bold"
>
{char}
</text>
);
currentX += getCharWidth(1);
charsRendered += charsToRender;
remainingText = remainingText.substring(charsToRender);
}
}
if (charsRendered >= availableChars) break;
}
return { elements, currentX, charsRendered };
};
// 渲染文本行
const renderTextSegments = (line, charLimit, y, isCurrentLine, lineIndex) => {
let x = lineNumberWidth;
const lineNumberElement = (
<text
key="line-number"
x={lineNumberWidth / 2}
y={y + 1}
fill="#ffffff"
fontFamily="'Courier New', monospace"
fontSize={fontSize}
dominantBaseline="middle"
textAnchor="middle"
fontWeight="bold"
>
{lineIndex + 1}.
</text>
);
const availableChars = charLimit;
const { elements: commandElements } = renderColoredText(
line,
x,
y,
availableChars
);
const allElements = [lineNumberElement, ...commandElements];
if (isCurrentLine && animationState.curOn) {
const cursorX = x + getCharWidth(charLimit);
allElements.push(
<rect
key="cursor"
x={cursorX + 2}
y={y - fontSize/2 + 2}
width={2}
height={fontSize - 4}
fill="#ffffff"
/>
);
}
return allElements;
};
// 重置动画
const resetAnimation = () => {
setAnimationState({
lineI: 0,
charI: 0,
curOn: true
});
};
// 监听 startAnimation 变化
useEffect(() => {
if (startAnimation) {
resetAnimation();
}
}, [startAnimation]);
// 动画循环
useEffect(() => {
if (!startAnimation) return;
let lastCharTime = Date.now();
let lastLineTime = Date.now();
let lastBlinkTime = Date.now();
const animate = () => {
const now = Date.now();
let shouldUpdate = false;
let newState = { ...animationState };
// 光标闪烁
if (now - lastBlinkTime > 500) {
newState.curOn = !newState.curOn;
lastBlinkTime = now;
shouldUpdate = true;
}
// 第一行字符逐个显示
if (newState.lineI === 0 && newState.charI < lines[0].length && now - lastCharTime > charGap) {
newState.charI++;
lastCharTime = now;
shouldUpdate = true;
}
// 第一行完成后,后续行整行显示
else if (newState.lineI === 0 && newState.charI >= lines[0].length && now - lastLineTime > lineGap) {
newState.lineI++;
if (newState.lineI < lines.length) {
newState.charI = lines[newState.lineI].length;
}
lastLineTime = now;
shouldUpdate = true;
}
// 后续行之间的间隔
else if (newState.lineI > 0 && newState.lineI < lines.length && now - lastLineTime > lineGap) {
newState.lineI++;
if (newState.lineI < lines.length) {
newState.charI = lines[newState.lineI].length;
}
lastLineTime = now;
shouldUpdate = true;
}
if (shouldUpdate) {
setAnimationState(newState);
}
// 如果动画未完成,继续下一帧
if (newState.lineI < lines.length && startAnimation) {
animationRef.current = requestAnimationFrame(animate);
}
};
animationRef.current = requestAnimationFrame(animate);
return () => {
if (animationRef.current) {
cancelAnimationFrame(animationRef.current);
}
};
}, [animationState]); // 包含 animationState 以确保状态更新
return (
<div className='center pl15'>
<svg
width="755"
height="420"
viewBox="0 0 755 420"
xmlns="http://www.w3.org/2000/svg"
style={{ backgroundColor: 'transparent' }}
>
{/* 渲染已完成的行 */}
{lines.slice(0, animationState.lineI).map((line, index) => (
<g key={index}>
{renderTextSegments(line, line.length, startY + index * lineH, false, index)}
</g>
))}
{/* 渲染当前正在输入的行 */}
{animationState.lineI < lines.length && (
<g key="current-line">
{renderTextSegments(lines[animationState.lineI], animationState.charI, startY + animationState.lineI * lineH, true, animationState.lineI)}
</g>
)}
</svg>
</div>
);
};
export default TerminalAnimation;