Remotion LabRemotion Lab
返回模板庫

TikTok 影片貼文卡

模擬 TikTok 網頁版影片貼文卡的動畫元件,左側直式縮圖搭配右側資訊欄,包含追蹤按鈕彈出、說明文字逐行顯示、音樂跑馬燈與互動數據動態累加。

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

const DISPLAY-NAME = "Remotion 創作者";
const HANDLE = "@remotion-creator";
const DESCRIPTION = "這支影片展示了 Remotion 動畫的可能性 🎬 #remotion #創作者 #動畫";
const MUSIC-TEXT = "原創音樂 - 創作者名稱";

const LIKES-TARGET = 23400;
const COMMENTS-TARGET = 1200;
const SHARES-TARGET = 892;

function formatCount(n: number): string {
  if (n >= 10000) return (n / 1000).toFixed(1).replace(/\.0$/, "") + "K";
  if (n >= 1000) return (n / 1000).toFixed(1).replace(/\.0$/, "") + "K";
  return String(n);
}

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

  // Card scale + fade: frames 0-18
  const cardProgress = spring({
    frame,
    fps,
    config: { damping: 20, stiffness: 130 },
    durationInFrames: 18,
  });
  const cardScale = interpolate(cardProgress, [0, 1], [0.92, 1]);
  const cardOpacity = interpolate(cardProgress, [0, 0.3], [0, 1], {
    extrapolateRight: "clamp",
  });

  // Thumbnail fade: frames 8-22
  const thumbOpacity = interpolate(frame, [8, 22], [0, 1], {
    extrapolateLeft: "clamp",
    extrapolateRight: "clamp",
  });

  // Avatar + name slide from top-right: frames 18-32
  const headerProgress = spring({
    frame: frame - 18,
    fps,
    config: { damping: 22, stiffness: 160 },
    durationInFrames: 14,
  });
  const headerX = interpolate(headerProgress, [0, 1], [30, 0]);
  const headerY = interpolate(headerProgress, [0, 1], [-20, 0]);
  const headerOpacity = interpolate(headerProgress, [0, 0.4], [0, 1], {
    extrapolateRight: "clamp",
  });

  // Follow button pop: frames 28-40
  const followProgress = spring({
    frame: frame - 28,
    fps,
    config: { damping: 14, stiffness: 240 },
    durationInFrames: 12,
  });
  const followScale = interpolate(followProgress, [0, 1], [0, 1], {
    extrapolateRight: "clamp",
  });

  // Description line-by-line fade: frames 35-60
  const descLines = DESCRIPTION.split(" ");
  const descLine1 = descLines.slice(0, 5).join(" ");
  const descLine2 = descLines.slice(5).join(" ");

  const descOpacity1 = interpolate(frame, [35, 48], [0, 1], {
    extrapolateLeft: "clamp",
    extrapolateRight: "clamp",
  });
  const descOpacity2 = interpolate(frame, [46, 60], [0, 1], {
    extrapolateLeft: "clamp",
    extrapolateRight: "clamp",
  });
  const descY1 = interpolate(frame, [35, 48], [10, 0], {
    extrapolateLeft: "clamp",
    extrapolateRight: "clamp",
  });
  const descY2 = interpolate(frame, [46, 60], [10, 0], {
    extrapolateLeft: "clamp",
    extrapolateRight: "clamp",
  });

  // Stats count up staggered: frames 55-90
  const likesRaw = interpolate(frame, [55, 85], [0, 1], {
    extrapolateLeft: "clamp",
    extrapolateRight: "clamp",
  });
  const commentsRaw = interpolate(frame, [62, 88], [0, 1], {
    extrapolateLeft: "clamp",
    extrapolateRight: "clamp",
  });
  const sharesRaw = interpolate(frame, [68, 90], [0, 1], {
    extrapolateLeft: "clamp",
    extrapolateRight: "clamp",
  });

  const currentLikes = Math.round(likesRaw * LIKES-TARGET);
  const currentComments = Math.round(commentsRaw * COMMENTS-TARGET);
  const currentShares = Math.round(sharesRaw * SHARES-TARGET);

  // Music ticker: scrolls left after frame 60
  const tickerX = interpolate(frame, [60, 120], [0, -200], {
    extrapolateLeft: "clamp",
    extrapolateRight: "clamp",
  });
  const musicOpacity = interpolate(frame, [55, 65], [0, 1], {
    extrapolateLeft: "clamp",
    extrapolateRight: "clamp",
  });

  const statsOpacity = interpolate(frame, [55, 68], [0, 1], {
    extrapolateLeft: "clamp",
    extrapolateRight: "clamp",
  });

  return (
    <AbsoluteFill
      style={{
        background: "#0a0a0a",
        justifyContent: "center",
        alignItems: "center",
      }}
    >
      {/* Card */}
      <div
        style={{
          transform: `scale(${cardScale})`,
          opacity: cardOpacity,
          background: "#161823",
          borderRadius: 16,
          width: 780,
          height: 580,
          boxSizing: "border-box",
          fontFamily: "sans-serif",
          overflow: "hidden",
          display: "flex",
          flexDirection: "row",
          boxShadow: "0 28px 72px rgba(0,0,0,0.8)",
        }}
      >
        {/* Left: video thumbnail (9:16 proportion, ~300px wide) */}
        <div
          style={{
            width: 300,
            height: 580,
            flexShrink: 0,
            background: "linear-gradient(180deg, #1a0533 0%, #2d1b69 30%, #11998e 70%, #38ef7d 100%)",
            opacity: thumbOpacity,
            position: "relative",
            display: "flex",
            alignItems: "center",
            justifyContent: "center",
            overflow: "hidden",
          }}
        >
          {/* Subtle grid overlay */}
          <div
            style={{
              position: "absolute",
              inset: 0,
              backgroundImage:
                "repeating-linear-gradient(0deg, rgba(255,255,255,0.04) 0px, rgba(255,255,255,0.04) 1px, transparent 1px, transparent 32px), repeating-linear-gradient(90deg, rgba(255,255,255,0.04) 0px, rgba(255,255,255,0.04) 1px, transparent 1px, transparent 32px)",
            }}
          />
          {/* Play icon overlay */}
          <div
            style={{
              position: "relative",
              zIndex: 2,
              display: "flex",
              flexDirection: "column",
              alignItems: "center",
              gap: 8,
            }}
          >
            <div
              style={{
                width: 0,
                height: 0,
                borderTop: "20px solid transparent",
                borderBottom: "20px solid transparent",
                borderLeft: "34px solid rgba(255,255,255,0.7)",
                marginLeft: 8,
              }}
            />
          </div>
          {/* TikTok-style bottom gradient */}
          <div
            style={{
              position: "absolute",
              bottom: 0,
              left: 0,
              right: 0,
              height: 120,
              background: "linear-gradient(0deg, rgba(22,24,35,0.8) 0%, transparent 100%)",
            }}
          />
        </div>

        {/* Right: info panel */}
        <div
          style={{
            flex: 1,
            padding: "24px 20px 20px",
            display: "flex",
            flexDirection: "column",
            minWidth: 0,
          }}
        >
          {/* Avatar + name row */}
          <div
            style={{
              display: "flex",
              alignItems: "center",
              gap: 12,
              transform: `translate(${headerX}px, ${headerY}px)`,
              opacity: headerOpacity,
              marginBottom: 16,
            }}
          >
            {/* Avatar */}
            <div
              style={{
                width: 48,
                height: 48,
                borderRadius: "50%",
                background: "linear-gradient(135deg, #ff0050, #ff4081, #7c4dff)",
                border: "2px solid #fe2c55",
                flexShrink: 0,
                display: "flex",
                alignItems: "center",
                justifyContent: "center",
                fontSize: 18,
                fontWeight: 700,
                color: "#ffffff",
              }}
            >
              R
            </div>

            {/* Name + handle */}
            <div style={{ flex: 1, minWidth: 0 }}>
              <div
                style={{
                  fontSize: 15,
                  fontWeight: 700,
                  color: "#ffffff",
                  lineHeight: 1.2,
                  whiteSpace: "nowrap",
                  overflow: "hidden",
                  textOverflow: "ellipsis",
                }}
              >
                {DISPLAY-NAME}
              </div>
              <div
                style={{
                  fontSize: 13,
                  color: "#8a8b91",
                  marginTop: 1,
                }}
              >
                {HANDLE}
              </div>
            </div>

            {/* Follow button */}
            <div
              style={{
                transform: `scale(${followScale})`,
                border: "1.5px solid #fe2c55",
                borderRadius: 6,
                padding: "5px 14px",
                fontSize: 13,
                fontWeight: 700,
                color: "#fe2c55",
                flexShrink: 0,
                cursor: "pointer",
              }}
            >
              追蹤
            </div>
          </div>

          {/* Description */}
          <div style={{ marginBottom: 16, flex: 1 }}>
            <div
              style={{
                fontSize: 14,
                color: "#d1d1d1",
                lineHeight: 1.6,
                transform: `translateY(${descY1}px)`,
                opacity: descOpacity1,
                marginBottom: 2,
              }}
            >
              {descLine1}
            </div>
            <div
              style={{
                fontSize: 14,
                color: "#d1d1d1",
                lineHeight: 1.6,
                transform: `translateY(${descY2}px)`,
                opacity: descOpacity2,
              }}
            >
              {descLine2}
            </div>
          </div>

          {/* Music ticker */}
          <div
            style={{
              display: "flex",
              alignItems: "center",
              gap: 8,
              marginBottom: 20,
              opacity: musicOpacity,
              overflow: "hidden",
            }}
          >
            <span style={{ fontSize: 14, flexShrink: 0 }}>♫</span>
            <div style={{ overflow: "hidden", flex: 1 }}>
              <span
                style={{
                  fontSize: 13,
                  color: "#aaaaaa",
                  display: "inline-block",
                  transform: `translateX(${tickerX}px)`,
                  whiteSpace: "nowrap",
                }}
              >
                {MUSIC-TEXT} · {MUSIC-TEXT}
              </span>
            </div>
          </div>

          {/* Stats row */}
          <div
            style={{
              display: "flex",
              alignItems: "center",
              gap: 20,
              opacity: statsOpacity,
            }}
          >
            {[
              { icon: "♥", value: formatCount(currentLikes) },
              { icon: "💬", value: formatCount(currentComments) },
              { icon: "↗", value: formatCount(currentShares) },
              { icon: "🔖", value: "" },
            ].map(({ icon, value }, i) => (
              <div
                key={i}
                style={{
                  display: "flex",
                  flexDirection: "column",
                  alignItems: "center",
                  gap: 2,
                  cursor: "pointer",
                }}
              >
                <span style={{ fontSize: 22, color: "#ffffff" }}>{icon}</span>
                {value ? (
                  <span
                    style={{
                      fontSize: 12,
                      color: "#8a8b91",
                      fontWeight: 600,
                    }}
                  >
                    {value}
                  </span>
                ) : null}
              </div>
            ))}
          </div>
        </div>
      </div>
    </AbsoluteFill>
  );
};

登入後查看完整程式碼