forked from 77media/video-flow
449 lines
20 KiB
TypeScript
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;
|
|
|