播放清單推薦片尾
深色背景上「你可能也喜歡」標題與播放全部按鈕淡入,四張影片卡片依序從下方彈性滑出,每張卡片顯示縮圖、播放按鈕、時長、標題、頻道名稱與觀看次數,底部進度條同步推進。
片尾播放清單影片推薦
提示詞(可直接修改內容)
import React from "react";
import {
AbsoluteFill,
interpolate,
spring,
useCurrentFrame,
useVideoConfig,
} from "remotion";
type VideoCard = {
title: string;
channel: string;
views: string;
duration: string;
};
const VIDEO-CARDS: VideoCard[] = [
{
title: "Remotion 動畫入門完整教學",
channel: "Remotion 中文社群",
views: "12,400 次觀看",
duration: "18:32",
},
{
title: "Spring 彈性動畫深度解析",
channel: "Remotion 中文社群",
views: "8,710 次觀看",
duration: "12:05",
},
{
title: "用 Remotion 製作動態標題",
channel: "Remotion 中文社群",
views: "5,280 次觀看",
duration: "9:47",
},
{
title: "Lambda 雲端渲染實戰指南",
channel: "Remotion 中文社群",
views: "3,950 次觀看",
duration: "22:18",
},
];
const CARD-WIDTH = 400;
const CARD-THUMB-HEIGHT = 225;
const CARD-INFO-HEIGHT = 80;
const CARD-GAP = 28;
const CARD-STARTS = [35, 50, 65, 80];
export const PlaylistOutro: React.FC = () => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
// 頂部標題 fade in + translateY(frame 10-35)
const titleOpacity = interpolate(frame, [10, 35], [0, 1], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
});
const titleY = interpolate(frame, [10, 35], [-20, 0], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
});
// 播放全部按鈕(frame 25-45)
const playAllOpacity = interpolate(frame, [25, 45], [0, 1], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
});
// 底部進度條(frame 0-180 線性)
const progressWidth = interpolate(frame, [0, 180], [0, 100], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
});
const totalWidth =
VIDEO-CARDS.length * CARD-WIDTH + (VIDEO-CARDS.length - 1) * CARD-GAP;
return (
<AbsoluteFill
style={{
background: "#0f0f0f",
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
}}
>
{/* 標題列 */}
<div
style={{
width: totalWidth,
display: "flex",
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
marginBottom: 32,
transform: `translateY(${titleY}px)`,
opacity: titleOpacity,
}}
>
{/* 左側標題 */}
<div
style={{
fontSize: 36,
fontWeight: 700,
color: "#ffffff",
fontFamily: "sans-serif",
}}
>
你可能也喜歡
</div>
{/* 右側播放全部 */}
<div
style={{
display: "flex",
alignItems: "center",
gap: 8,
opacity: playAllOpacity,
}}
>
{/* 播放圖示 */}
<div
style={{
display: "flex",
alignItems: "center",
gap: 4,
color: "#3b82f6",
fontFamily: "sans-serif",
fontSize: 14,
fontWeight: 600,
}}
>
<span style={{ fontSize: 16 }}>▶</span>
<div
style={{
display: "flex",
flexDirection: "column",
gap: 3,
marginLeft: 2,
}}
>
{[0, 1, 2].map((i) => (
<div
key={i}
style={{
width: 12,
height: 2,
background: "#3b82f6",
borderRadius: 1,
}}
/>
))}
</div>
<span style={{ marginLeft: 6 }}>播放全部</span>
</div>
</div>
</div>
{/* 影片卡片列 */}
<div
style={{
display: "flex",
flexDirection: "row",
gap: CARD-GAP,
alignItems: "flex-start",
}}
>
{VIDEO-CARDS.map((card, i) => {
const startFrame = CARD-STARTS[i];
const cardSpring = spring({
frame: frame - startFrame,
fps,
config: { damping: 20, stiffness: 130 },
durationInFrames: 25,
});
const cardY = interpolate(cardSpring, [0, 1], [60, 0]);
const cardOpacity = interpolate(cardSpring, [0, 0.4], [0, 1], {
extrapolateRight: "clamp",
});
return (
<div
key={i}
style={{
width: CARD-WIDTH,
display: "flex",
flexDirection: "column",
transform: `translateY(${cardY}px)`,
opacity: cardOpacity,
}}
>
{/* 縮圖區 */}
<div
style={{
width: CARD-WIDTH,
height: CARD-THUMB-HEIGHT,
background: "#1a1a1a",
borderRadius: 8,
position: "relative",
overflow: "hidden",
flexShrink: 0,
}}
>
{/* 縮圖中央播放按鈕 */}
<div
style={{
position: "absolute",
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
width: 56,
height: 56,
borderRadius: "50%",
background: "rgba(255,255,255,0.18)",
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
<span
style={{
fontSize: 22,
color: "#ffffff",
marginLeft: 4,
fontFamily: "sans-serif",
}}
>
▶
</span>
</div>
{/* 右下角時長 */}
<div
style={{
position: "absolute",
bottom: 8,
right: 8,
background: "rgba(0,0,0,0.8)",
color: "#ffffff",
fontSize: 13,
fontFamily: "sans-serif",
fontWeight: 600,
padding: "2px 6px",
borderRadius: 4,
}}
>
{card.duration}
</div>
</div>
{/* 資訊區 */}
<div
style={{
height: CARD-INFO-HEIGHT,
padding: "12px 4px 0",
display: "flex",
flexDirection: "column",
gap: 4,
}}
>
{/* 標題 */}
<div
style={{
fontSize: 18,
fontWeight: 600,
color: "#ffffff",
fontFamily: "sans-serif",
lineHeight: 1.3,
overflow: "hidden",
display: "-webkit-box",
WebkitLineClamp: 2,
WebkitBoxOrient: "vertical",
}}
>
{card.title}
</div>
{/* 頻道名稱 */}
<div
style={{
fontSize: 14,
color: "#aaaaaa",
fontFamily: "sans-serif",
}}
>
{card.channel}
</div>
{/* 觀看次數 */}
<div
style={{
fontSize: 14,
color: "#aaaaaa",
fontFamily: "sans-serif",
}}
>
{card.views}
</div>
</div>
</div>
);
})}
</div>
{/* 底部進度條 */}
<div
style={{
position: "absolute",
bottom: 0,
left: 0,
width: "100%",
height: 3,
background: "#1a1a1a",
}}
>
<div
style={{
height: "100%",
width: `${progressWidth}%`,
background: "#3b82f6",
}}
/>
</div>
</AbsoluteFill>
);
};登入後查看完整程式碼