YouTube 按讚訂閱通知動畫
模擬 YouTube CTA 互動列的完整動畫流程:按讚按鈕波紋點擊、訂閱按鈕狀態切換,以及鈴鐺搖擺通知,搭配音效與粒子特效。
YouTubeCTA互動音效粒子
提示詞(可直接修改內容)
import {
AbsoluteFill,
Audio,
Img,
Sequence,
interpolate,
spring,
staticFile,
useCurrentFrame,
useVideoConfig,
} from "remotion";
const colors = {
background: "#0B0F17",
text: "#FFFFFF",
accent: "#4DA3FF",
dimmed: "rgba(255, 255, 255, 0.6)",
cardBg: "rgba(255, 255, 255, 0.05)",
border: "rgba(77, 163, 255, 0.3)",
};
export const LIKE-SUBSCRIBE-BELL-DURATION-FRAMES = 300;
const ThumbsUpIcon: React.FC<{ size?: number; color?: string }> = ({
size = 32,
color = "#FFF",
}) => (
<svg width={size} height={size} viewBox="0 0 24 24" fill={color}>
<path d="M1 21h4V9H1v12zm22-11c0-1.1-.9-2-2-2h-6.31l.95-4.57.03-.32c0-.41-.17-.79-.44-1.06L14.17 1 7.59 7.59C7.22 7.95 7 8.45 7 9v10c0 1.1.9 2 2 2h9c.83 0 1.54-.5 1.84-1.22l3.02-7.05c.09-.23.14-.47.14-.73v-2z" />
</svg>
);
const BellIcon: React.FC<{ size?: number; color?: string }> = ({
size = 32,
color = "#FFF",
}) => (
<svg width={size} height={size} viewBox="0 0 24 24" fill={color}>
<path d="M12 22c1.1 0 2-.9 2-2h-4c0 1.1.89 2 2 2zm6-6v-5c0-3.07-1.63-5.64-4.5-6.32V4c0-.83-.67-1.5-1.5-1.5s-1.5.67-1.5 1.5v.68C7.64 5.36 6 7.92 6 11v5l-2 2v1h16v-1l-2-2z" />
</svg>
);
const Particle: React.FC<{
x: number;
y: number;
delay: number;
size: number;
}> = ({ x, y, delay, size }) => {
const frame = useCurrentFrame();
const t = Math.max(0, frame - delay);
const opacity = interpolate(t, [0, 20, 60], [0, 0.6, 0], {
extrapolateRight: "clamp",
});
const yOffset = interpolate(t, [0, 60], [0, -40], {
extrapolateRight: "clamp",
});
return (
<div
style={{
position: "absolute",
left: x,
top: y + yOffset,
width: size,
height: size,
borderRadius: "50%",
backgroundColor: colors.accent,
opacity,
}}
/>
);
};
export const LikeSubscribeBellScene: React.FC = () => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const clickSrc = staticFile("click.mp3");
const bellSrc = staticFile("bell-notification.mp3");
const EX = {
extrapolateRight: "clamp" as const,
extrapolateLeft: "clamp" as const,
};
const glowPulse = Math.sin(frame * 0.03) * 0.3 + 0.7;
const barSpring = spring({
frame: Math.max(0, frame - 15),
fps,
config: { damping: 14, stiffness: 80 },
});
const barY = interpolate(barSpring, [0, 1], [100, 0]);
const barOpacity = interpolate(frame, [15, 35], [0, 1], EX);
const likeStart = 90;
const likePressed = frame >= likeStart;
const likeBounce = spring({
frame: Math.max(0, frame - likeStart),
fps,
config: { damping: 5, stiffness: 200 },
});
const likeScale = likePressed ? 0.7 + likeBounce * 0.3 : 1;
const likeRippleProgress = interpolate(frame - likeStart, [0, 25], [0, 1], EX);
const likeRippleOpacity = likePressed
? interpolate(likeRippleProgress, [0, 1], [0.5, 0])
: 0;
const likeRippleScale = likePressed
? interpolate(likeRippleProgress, [0, 1], [0.8, 2.5])
: 0;
const subStart = 150;
const subPressed = frame >= subStart;
const subBounce = spring({
frame: Math.max(0, frame - subStart),
fps,
config: { damping: 8, stiffness: 120 },
});
const subScale = subPressed ? 0.85 + subBounce * 0.15 : 1;
const subWidth = subPressed
? interpolate(frame - subStart, [0, 15], [120, 160], EX)
: 120;
const bellStart = 210;
const bellActive = frame >= bellStart;
const bellBounce = spring({
frame: Math.max(0, frame - bellStart),
fps,
config: { damping: 6, stiffness: 150 },
});
const bellShaking = frame >= bellStart + 8 && frame < bellStart + 50;
const bellRotation = bellShaking
? Math.sin((frame - bellStart - 8) * 1.8) *
interpolate(frame - bellStart - 8, [0, 42], [25, 0], EX)
: 0;
const bellScale = bellActive ? 0.75 + bellBounce * 0.25 : 1;
const fadeOut = interpolate(frame, [275, 298], [1, 0], EX);
return (
<AbsoluteFill
style={{
backgroundColor: colors.background,
fontFamily: "'Noto Sans TC', 'Inter', sans-serif",
overflow: "hidden",
}}
>
<div style={{ opacity: fadeOut, width: "100%", height: "100%" }}>
<div
style={{
position: "absolute",
left: "50%",
top: "45%",
width: 1200,
height: 600,
borderRadius: "50%",
background: `radial-gradient(ellipse, ${colors.accent}08 0%, transparent 70%)`,
transform: `translate(-50%, -50%) scale(${glowPulse})`,
}}
/>
{[
{ x: 300, y: 200, delay: 0, size: 4 },
{ x: 800, y: 150, delay: 15, size: 3 },
{ x: 1400, y: 300, delay: 30, size: 5 },
{ x: 1600, y: 180, delay: 45, size: 3 },
{ x: 500, y: 800, delay: 60, size: 4 },
{ x: 1200, y: 850, delay: 75, size: 3 },
].map((p, i) => (
<Particle key={i} {...p} />
))}
<div
style={{
position: "absolute",
top: "50%",
left: "50%",
transform: `translate(-50%, -50%) translateY(${barY}px)`,
opacity: barOpacity,
backgroundColor: "rgba(255, 255, 255, 0.06)",
border: "1px solid rgba(255, 255, 255, 0.12)",
borderRadius: 24,
padding: "32px 48px",
display: "flex",
flexWrap: "nowrap",
whiteSpace: "nowrap",
alignItems: "center",
gap: 36,
}}
>
<div
style={{
width: 80,
height: 80,
borderRadius: "50%",
overflow: "hidden",
flexShrink: 0,
border: "3px solid rgba(255,255,255,0.15)",
}}
>
<Img
src={staticFile("avatar-2.png")}
style={{ width: "100%", height: "100%", objectFit: "cover" }}
/>
</div>
<div
style={{
fontSize: 32,
fontWeight: 700,
color: colors.text,
letterSpacing: 2,
marginRight: 16,
flexShrink: 0,
}}
>
Debug 土撥鼠
</div>
<div
style={{
width: 1,
height: 52,
backgroundColor: "rgba(255,255,255,0.15)",
flexShrink: 0,
}}
/>
<div style={{ position: "relative", flexShrink: 0 }}>
{likePressed && (
<div
style={{
position: "absolute",
top: "50%",
left: "50%",
width: 60,
height: 60,
borderRadius: "50%",
backgroundColor: colors.accent,
opacity: likeRippleOpacity,
transform: `translate(-50%, -50%) scale(${likeRippleScale})`,
pointerEvents: "none",
}}
/>
)}
<div
style={{
display: "flex",
alignItems: "center",
gap: 10,
padding: "14px 28px",
borderRadius: 28,
backgroundColor: likePressed
? "rgba(77, 163, 255, 0.2)"
: "rgba(255,255,255,0.08)",
border: `1.5px solid ${likePressed ? colors.accent + "60" : "rgba(255,255,255,0.12)"}`,
transform: `scale(${likeScale})`,
}}
>
<ThumbsUpIcon
size={30}
color={likePressed ? colors.accent : "rgba(255,255,255,0.5)"}
/>
<span
style={{
fontSize: 24,
fontWeight: 600,
color: likePressed ? colors.accent : "rgba(255,255,255,0.5)",
}}
>
按讚
</span>
</div>
</div>
<div
style={{
padding: "14px 40px",
borderRadius: 28,
backgroundColor: subPressed ? "rgba(255,255,255,0.12)" : "#CC0000",
transform: `scale(${subScale})`,
minWidth: subWidth,
textAlign: "center",
flexShrink: 0,
}}
>
<span
style={{ fontSize: 24, fontWeight: 700, color: "#FFFFFF", letterSpacing: 2 }}
>
{subPressed ? "已訂閱 ✓" : "訂閱"}
</span>
</div>
<div
style={{
width: 58,
height: 58,
flexShrink: 0,
borderRadius: "50%",
backgroundColor: bellActive
? "rgba(255, 200, 0, 0.15)"
: "rgba(255,255,255,0.08)",
border: `1.5px solid ${bellActive ? "#FFC80060" : "rgba(255,255,255,0.12)"}`,
display: "flex",
justifyContent: "center",
alignItems: "center",
transform: `scale(${bellScale}) rotate(${bellRotation}deg)`,
transformOrigin: "50% 15%",
}}
>
<BellIcon size={28} color={bellActive ? "#FFC800" : "rgba(255,255,255,0.5)"} />
</div>
</div>
</div>
<Sequence from={likeStart}>
<Audio src={clickSrc} volume={0.8} />
</Sequence>
<Sequence from={subStart}>
<Audio src={clickSrc} volume={0.8} />
</Sequence>
<Sequence from={bellStart}>
<Audio src={bellSrc} volume={0.85} />
</Sequence>
</AbsoluteFill>
);
};登入後查看完整程式碼