Remotion LabRemotion Lab
返回模板庫

直播留言板

模擬 YouTube / Twitch 直播留言流,8 則留言依序從底部彈入,舊留言自動往上推,每則留言含彩色頭像、使用者名稱、留言文字與愛心按鈕。

社群簡約橫式
提示詞(可直接修改內容)
import {
  AbsoluteFill,
  interpolate,
  spring,
  useCurrentFrame,
  useVideoConfig,
} from "remotion";
import React from "react";

const COMMENTS = [
  { name: "工程師 Kai", color: "#3b82f6", text: "這個動畫超讚!🔥", avatar: "linear-gradient(135deg,#667eea,#764ba2)", liked: false },
  { name: "設計師 Mei", color: "#ec4899", text: "請問用什麼工具做的?", avatar: "linear-gradient(135deg,#f093fb,#f5576c)", liked: true },
  { name: "前端新手", color: "#10b981", text: "第一次看到用 React 做影片,太神奇了", avatar: "linear-gradient(135deg,#43e97b,#38f9d7)", liked: false },
  { name: "UI Designer", color: "#f59e0b", text: "顏色搭配很好看 👍", avatar: "linear-gradient(135deg,#fa709a,#fee140)", liked: true },
  { name: "開源愛好者", color: "#8b5cf6", text: "Open source 嗎?", avatar: "linear-gradient(135deg,#a18cd1,#fbc2eb)", liked: false },
  { name: "React 粉", color: "#06b6d4", text: "React + 影片 = 完美組合!", avatar: "linear-gradient(135deg,#4facfe,#00f2fe)", liked: true },
  { name: "創作者 Tim", color: "#ef4444", text: "馬上去 star 這個 repo ⭐", avatar: "linear-gradient(135deg,#ff9a9e,#fecfef)", liked: false },
  { name: "訂閱者", color: "#84cc16", text: "已訂閱!期待更多教學影片 🎬", avatar: "linear-gradient(135deg,#a1c4fd,#c2e9fb)", liked: true },
];

const CONTAINER-WIDTH = 800;
const CONTAINER-LEFT = (1920 - CONTAINER-WIDTH) / 2;
const COMMENT-HEIGHT = 72;
const COMMENT-GAP = 10;
const MAX-VISIBLE = 6;
const TOTAL-ITEM-H = COMMENT-HEIGHT + COMMENT-GAP;
const STACK-BOTTOM = 700;

export const CommentWall: React.FC = () => {
  const frame = useCurrentFrame();
  const { fps } = useVideoConfig();

  const visibleCount = COMMENTS.reduce((acc, _, i) => {
    const startFrame = i * 15 + 5;
    return frame >= startFrame ? acc + 1 : acc;
  }, 0);

  const progressList = COMMENTS.map((_, i) => {
    const startFrame = i * 15 + 5;
    return spring({
      frame: frame - startFrame,
      fps,
      config: { damping: 20, stiffness: 180 },
      durationInFrames: 18,
    });
  });

  const headerOpacity = interpolate(frame, [0, 25], [0, 1], {
    extrapolateLeft: "clamp",
    extrapolateRight: "clamp",
  });

  const overflow = Math.max(0, visibleCount - MAX-VISIBLE);

  return (
    <AbsoluteFill style={{ background: "#0f0f0f", fontFamily: "sans-serif" }}>
      <div
        style={{
          position: "absolute",
          top: 60,
          left: CONTAINER-LEFT,
          width: CONTAINER-WIDTH,
          display: "flex",
          alignItems: "center",
          justifyContent: "space-between",
          opacity: headerOpacity,
        }}
      >
        <div style={{ display: "flex", alignItems: "center", gap: 10 }}>
          <div
            style={{
              width: 10,
              height: 10,
              borderRadius: "50%",
              background: "#ff0000",
            }}
          />
          <span style={{ fontSize: 20, fontWeight: 700, color: "#ffffff" }}>
            直播留言板
          </span>
        </div>
        <div
          style={{
            background: "#1a1a1a",
            borderRadius: 20,
            padding: "4px 16px",
            fontSize: 14,
            color: "#aaaaaa",
          }}
        >
          {visibleCount} 則留言
        </div>
      </div>

      <div
        style={{
          position: "absolute",
          left: CONTAINER-LEFT,
          width: CONTAINER-WIDTH,
          bottom: 1080 - STACK-BOTTOM,
          top: 140,
          overflow: "hidden",
        }}
      >
        {COMMENTS.map((comment, i) => {
          const startFrame = i * 15 + 5;
          if (frame < startFrame) return null;

          const progress = progressList[i];
          const stackPos = i - overflow;
          const targetY = (MAX-VISIBLE - 1 - stackPos) * TOTAL-ITEM-H;

          const slideOffset = interpolate(progress, [0, 1], [COMMENT-HEIGHT + 20, 0], {
            extrapolateRight: "clamp",
          });
          const opacity = interpolate(progress, [0, 0.3], [0, 1], {
            extrapolateRight: "clamp",
          });

          const isAboveFold = stackPos < 0;
          const finalOpacity = isAboveFold ? 0 : opacity;

          return (
            <div
              key={i}
              style={{
                position: "absolute",
                left: 0,
                top: targetY + slideOffset,
                width: CONTAINER-WIDTH,
                height: COMMENT-HEIGHT,
                opacity: finalOpacity,
                display: "flex",
                alignItems: "center",
                gap: 14,
                background: "rgba(255,255,255,0.04)",
                borderRadius: 12,
                padding: "0 16px",
                border: "1px solid rgba(255,255,255,0.06)",
              }}
            >
              <div
                style={{
                  width: 40,
                  height: 40,
                  borderRadius: "50%",
                  background: comment.avatar,
                  flexShrink: 0,
                  display: "flex",
                  alignItems: "center",
                  justifyContent: "center",
                  fontSize: 16,
                  fontWeight: 700,
                  color: "rgba(255,255,255,0.9)",
                }}
              >
                {comment.name[0]}
              </div>

              <div style={{ flex: 1, minWidth: 0 }}>
                <span
                  style={{
                    fontSize: 15,
                    fontWeight: 700,
                    color: comment.color,
                    marginRight: 8,
                  }}
                >
                  {comment.name}
                </span>
                <span style={{ fontSize: 15, color: "#e0e0e0" }}>
                  {comment.text}
                </span>
              </div>

              <div
                style={{
                  fontSize: 18,
                  color: comment.liked ? "#ef4444" : "rgba(255,255,255,0.3)",
                  flexShrink: 0,
                }}
              >
                {comment.liked ? "♥" : "♡"}
              </div>
            </div>
          );
        })}
      </div>

      <div
        style={{
          position: "absolute",
          left: CONTAINER-LEFT,
          width: CONTAINER-WIDTH,
          bottom: 1080 - STACK-BOTTOM,
          height: 60,
          background: "linear-gradient(to top, #0f0f0f 0%, transparent 100%)",
          pointerEvents: "none",
        }}
      />

      <div
        style={{
          position: "absolute",
          left: CONTAINER-LEFT,
          width: CONTAINER-WIDTH,
          top: 140,
          height: 60,
          background: "linear-gradient(to bottom, #0f0f0f 0%, transparent 100%)",
          pointerEvents: "none",
        }}
      />
    </AbsoluteFill>
  );
};

登入後查看完整程式碼