2025-10-13 20:20:37 +08:00

449 lines
20 KiB
TypeScript

import React from 'react';
/** Props for RenderLoading component */
interface RenderLoadingProps {
/** Size of the vinyl record */
recordSize?: string;
/** Apple silver light color for metallic effect */
appleSilverLight?: string;
/** Apple silver main color */
appleSilverMain?: string;
/** Apple silver dark color for shadows */
appleSilverDark?: string;
/** Core gradient start color (cyan) */
coreGradientStart?: string;
/** Core gradient middle color (purple) */
coreGradientMid?: string;
/** Core gradient end color (magenta) */
coreGradientEnd?: string;
/** Core glow color effect */
coreGlowColor?: string;
/** Scanner light color */
scannerColor?: string;
/** Record background color */
recordBg?: string;
/** Overall background color */
backgroundColor?: string;
/** Loading text */
loadingText?: string;
/** Failed state trigger */
isFailed?: boolean;
}
/**
* Vinyl record loading animation component with pulsing core and tonearm
* @param props - Component configuration props
* @returns React component with animated vinyl record loader
*/
const RenderLoading: React.FC<RenderLoadingProps> = ({
recordSize = '250px',
appleSilverLight = '#E5E5EA',
appleSilverMain = '#D1D1D6',
appleSilverDark = '#8A8A8E',
coreGradientStart = '#00E4E4',
coreGradientMid = '#8A2BE2',
coreGradientEnd = '#FF00FF',
coreGlowColor = 'rgba(138, 43, 226, 0.9)',
scannerColor = 'rgba(0, 228, 228, 0.35)',
recordBg = '#1A1A1A',
backgroundColor = '#000',
loadingText = 'Generating...',
isFailed = false,
}) => {
/** Dead metal color for failed state */
const deadMetalColor = '#3A3A3A';
const styles = `
@keyframes rotateRecord {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
@keyframes rotateScanner {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
@keyframes placeTonearm {
from { transform: rotate(-30deg); }
to { transform: rotate(0deg); }
}
@keyframes bobTonearm {
0%, 100% {
transform: rotate(0deg) translateY(0);
}
25% {
transform: rotate(0.5deg) translateY(2px);
}
75% {
transform: rotate(-0.5deg) translateY(-2px);
}
}
@keyframes retractTonearm {
from { transform: rotate(0deg); }
to { transform: rotate(-30deg); }
}
@keyframes breakTonearm {
0% {
transform: rotate(0deg);
animation-timing-function: cubic-bezier(0.3, 0, 0.8, 0.7); /* 加速下坠 */
}
60% {
transform: rotate(-55deg); /* 过冲到最大角度 */
animation-timing-function: cubic-bezier(0.1, 0.3, 0.4, 1.5); /* 回弹 */
}
100% {
transform: rotate(-90deg); /* 稳定在最终角度 */
}
}
@keyframes pulse {
0%, 100% {
transform: scale(1);
box-shadow: 0 0 15px 6px ${coreGlowColor},
inset 0 0 4px 1px rgba(255,255,255,0.4);
opacity: 0.9;
}
50% {
transform: scale(1.4);
box-shadow: 0 0 35px 15px ${coreGlowColor},
inset 0 0 8px 3px rgba(255,255,255,0.7);
opacity: 1;
}
}
`;
return (
<>
<style>{styles}</style>
<div
data-alt="vinyl-loading-container"
style={{
margin: 0,
width: '100%',
height: '100%',
display: 'flex',
flexDirection: 'column',
gap: '20px',
justifyContent: 'center',
alignItems: 'center',
backgroundColor,
fontFamily: 'sans-serif',
overflow: 'hidden',
}}
>
<div
data-alt="loading-wrapper"
style={{
position: 'relative',
width: recordSize,
height: recordSize,
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
}}
>
{/* Vinyl Record Container */}
<div
data-alt="vinyl-record"
style={{
width: '100%',
height: '100%',
position: 'relative',
animation: isFailed ? 'none' : 'rotateRecord 4s linear infinite',
transformStyle: 'preserve-3d',
}}
>
{/* Record Half - Top */}
<div
data-alt="record-half-top"
style={{
position: 'absolute',
width: '100%',
height: '100%',
backgroundColor: recordBg,
borderRadius: '50%',
boxShadow: `0 15px 35px rgba(0, 0, 0, 0.7),
inset 0 0 20px rgba(0,0,0,1),
inset 0 0 2px 1px rgba(255, 255, 255, 0.1)`,
overflow: 'hidden',
clipPath: isFailed
? 'polygon(0 0, 100% 0, 100% 50%, 85% 51%, 70% 49%, 55% 52%, 40% 50%, 25% 51.5%, 10% 49.5%, 0 50%)'
: 'polygon(0 0, 100% 0, 100% 50%, 0 50%)',
transition: 'transform 0.5s cubic-bezier(0.68, -0.55, 0.27, 1.55), clip-path 0.3s ease',
transform: isFailed ? 'translateY(-10px) rotate(-4deg)' : 'none',
}}
>
{/* Record grooves effect */}
<div
data-alt="record-grooves-top"
style={{
content: '""',
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: '100%',
borderRadius: '50%',
backgroundImage: `repeating-radial-gradient(
circle at center,
rgba(255, 255, 255, 0.1) 0,
rgba(255, 255, 255, 0.15) 1px,
transparent 1px,
transparent 7px
)`,
mixBlendMode: 'overlay',
zIndex: 1,
}}
/>
{/* Scanner light */}
<div
data-alt="scanner-light-top"
style={{
position: 'absolute',
width: '100%',
height: '100%',
borderRadius: '50%',
background: `conic-gradient(
from 90deg,
transparent 0%,
${scannerColor} 10%,
transparent 25%
)`,
animation: isFailed ? 'none' : 'rotateScanner 3s linear infinite',
zIndex: 2,
}}
/>
{/* Center label */}
<div
data-alt="center-label-top"
style={{
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
margin: 'auto',
width: '38%',
height: '38%',
background: '#111',
borderRadius: '50%',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
boxShadow: 'inset 0 0 20px rgba(0, 0, 0, 1)',
zIndex: 3,
}}
>
{/* Pulsing core */}
<div
data-alt="pulsing-core-top"
style={{
width: '12%',
height: '12%',
background: isFailed
? appleSilverDark
: `radial-gradient(circle at center, ${coreGradientStart} 0%, ${coreGradientMid} 50%, ${coreGradientEnd} 100%)`,
borderRadius: '50%',
animation: isFailed ? 'none' : 'pulse 1.5s ease-in-out infinite',
boxShadow: isFailed ? 'none' : undefined,
transition: 'background 0.3s, box-shadow 0.3s',
}}
/>
</div>
</div>
{/* Record Half - Bottom */}
<div
data-alt="record-half-bottom"
style={{
position: 'absolute',
width: '100%',
height: '100%',
backgroundColor: recordBg,
borderRadius: '50%',
boxShadow: `0 15px 35px rgba(0, 0, 0, 0.7),
inset 0 0 20px rgba(0,0,0,1),
inset 0 0 2px 1px rgba(255, 255, 255, 0.1)`,
overflow: 'hidden',
clipPath: isFailed
? 'polygon(0 50%, 10% 49.5%, 25% 51.5%, 40% 50%, 55% 52%, 70% 49%, 85% 51%, 100% 50%, 100% 100%, 0 100%)'
: 'polygon(0 50%, 100% 50%, 100% 100%, 0 100%)',
transition: 'transform 0.5s cubic-bezier(0.68, -0.55, 0.27, 1.55), clip-path 0.3s ease',
transform: isFailed ? 'translateY(10px) rotate(4deg)' : 'none',
}}
>
{/* Record grooves effect */}
<div
data-alt="record-grooves-bottom"
style={{
content: '""',
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: '100%',
borderRadius: '50%',
backgroundImage: `repeating-radial-gradient(
circle at center,
rgba(255, 255, 255, 0.1) 0,
rgba(255, 255, 255, 0.15) 1px,
transparent 1px,
transparent 7px
)`,
mixBlendMode: 'overlay',
zIndex: 1,
}}
/>
{/* Scanner light */}
<div
data-alt="scanner-light-bottom"
style={{
position: 'absolute',
width: '100%',
height: '100%',
borderRadius: '50%',
background: `conic-gradient(
from 90deg,
transparent 0%,
${scannerColor} 10%,
transparent 25%
)`,
animation: isFailed ? 'none' : 'rotateScanner 3s linear infinite',
zIndex: 2,
}}
/>
{/* Center label */}
<div
data-alt="center-label-bottom"
style={{
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
margin: 'auto',
width: '38%',
height: '38%',
background: '#111',
borderRadius: '50%',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
boxShadow: 'inset 0 0 20px rgba(0, 0, 0, 1)',
zIndex: 3,
}}
>
{/* Pulsing core */}
<div
data-alt="pulsing-core-bottom"
style={{
width: '12%',
height: '12%',
background: isFailed
? appleSilverDark
: `radial-gradient(circle at center, ${coreGradientStart} 0%, ${coreGradientMid} 50%, ${coreGradientEnd} 100%)`,
borderRadius: '50%',
animation: isFailed ? 'none' : 'pulse 1.5s ease-in-out infinite',
boxShadow: isFailed ? 'none' : undefined,
transition: 'background 0.3s, box-shadow 0.3s',
}}
/>
</div>
</div>
</div>
{/* Tonearm */}
<div
data-alt="tonearm"
style={{
position: 'absolute',
width: `calc(${recordSize} * 0.5)`,
height: `calc(${recordSize} * 0.1)`,
right: `calc(${recordSize} * -0.25)`,
top: '35%',
transformOrigin: `calc(100% - ${recordSize} * 0.06) center`,
zIndex: 10,
animation: isFailed
? 'breakTonearm 1.2s forwards'
: 'placeTonearm 1.5s ease-in-out forwards, bobTonearm 1s ease-in-out 1.5s infinite',
}}
>
{/* Arm */}
<div
data-alt="tonearm-arm"
style={{
width: '88%',
height: '30%',
background: isFailed
? deadMetalColor
: `linear-gradient(90deg,
${appleSilverDark} 0%,
${appleSilverLight} 50%,
${appleSilverDark} 100%)`,
position: 'absolute',
left: 0,
top: '50%',
transform: 'translateY(-50%)',
borderRadius: '10px',
transition: 'background 0.3s ease',
}}
/>
{/* Head */}
<div
data-alt="tonearm-head"
style={{
width: '20%',
height: '60%',
background: isFailed ? deadMetalColor : appleSilverMain,
position: 'absolute',
left: 0,
top: '50%',
transform: 'translateY(-50%)',
borderRadius: '4px',
boxShadow: 'inset 0 0 4px rgba(0,0,0,0.4)',
transition: 'background 0.3s ease',
}}
/>
{/* Pivot */}
<div
data-alt="tonearm-pivot"
style={{
width: `calc(${recordSize} * 0.12)`,
height: `calc(${recordSize} * 0.12)`,
background: isFailed
? deadMetalColor
: `radial-gradient(circle at 65% 35%, ${appleSilverLight}, ${appleSilverMain} 60%, ${appleSilverDark} 100%)`,
borderRadius: '50%',
position: 'absolute',
right: 0,
top: '50%',
transform: 'translateY(-50%)',
boxShadow: isFailed
? '2px 2px 5px rgba(0,0,0,0.4), inset 1px 1px 1px #222'
: `2px 2px 5px rgba(0,0,0,0.4), inset 1px 1px 1px ${appleSilverLight}`,
transition: 'background 0.3s ease, box-shadow 0.3s ease',
}}
/>
</div>
</div>
{/* 加载/失败文案 */}
<div data-alt="loading-text" className={`text-center ${isFailed ? 'text-[#d2c6c6]' : 'text-white'}`}>
{loadingText}
</div>
</div>
</>
);
};
export default RenderLoading;