Remotion LabRemotion Lab
返回模板庫

社群通知中心

六大社群平台(IG、X、YouTube、GitHub、LinkedIn、TikTok)的通知卡片依序從右側滑入,各有平台色彩左邊框與入場光暈,全部顯示後紅色角標脈動提示。

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

const NOTIFS = [
  { platform: "IG",  color: "#e1306c", icon: "📸", title: "design.tw 對你的貼文按讚", desc: "你上傳的照片獲得了新的互動", time: "剛剛" },
  { platform: "X",   color: "#1d9bf0", icon: "✕",  title: "你的推文被轉推了 47 次", desc: "工程師 Kai 等人轉推了你的推文", time: "2分鐘前" },
  { platform: "YT",  color: "#ff0000", icon: "▶",  title: "你的影片達到 1,000 次觀看!", desc: "恭喜!你的最新影片表現亮眼", time: "5分鐘前" },
  { platform: "GH",  color: "#6e40c9", icon: "⑂",  title: "awesome-project 獲得新的 Star", desc: "你的儲存庫累計達到 500 ⭐", time: "12分鐘前" },
  { platform: "LI",  color: "#0a66c2", icon: "in", title: "你的文章有 234 次瀏覽", desc: "本週 LinkedIn 貼文表現超過 95% 的用戶", time: "1小時前" },
  { platform: "TK",  color: "#69c9d0", icon: "♪",  title: "你的影片登上推薦頁", desc: "TikTok 演算法推薦了你的最新作品", time: "2小時前" },
];

const CARD-WIDTH = 750;
const CARD-HEIGHT = 90;
const CARD-GAP = 14;
const CARDS-LEFT = (1920 - CARD-WIDTH) / 2;
const TOTAL-H = NOTIFS.length * CARD-HEIGHT + (NOTIFS.length - 1) * CARD-GAP;
const CARDS-TOP = (1080 - TOTAL-H) / 2;
const BADGE-START = 95;

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

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

  const badgeProgress = spring({
    frame: frame - BADGE-START,
    fps,
    config: { damping: 12, stiffness: 220 },
    durationInFrames: 14,
  });
  const badgeScale = interpolate(badgeProgress, [0, 1], [0, 1], {
    extrapolateRight: "clamp",
  });
  const badgePulse =
    frame >= BADGE-START
      ? interpolate(((frame - BADGE-START) % 24) / 24, [0, 0.5, 1], [1, 0.7, 1], {
          extrapolateRight: "clamp",
        })
      : 0;

  const cardProgressList = NOTIFS.map((_, i) => {
    const startFrame = i * 15 + 5;
    return spring({
      frame: frame - startFrame,
      fps,
      config: { damping: 22, stiffness: 160 },
      durationInFrames: 20,
    });
  });

  return (
    <AbsoluteFill style={{ background: "#0f0f0f", fontFamily: "sans-serif" }}>
      <div
        style={{
          position: "absolute",
          top: CARDS-TOP - 70,
          left: CARDS-LEFT,
          width: CARD-WIDTH,
          display: "flex",
          alignItems: "center",
          justifyContent: "space-between",
          opacity: headerOpacity,
          transform: `translateY(${headerY}px)`,
        }}
      >
        <span style={{ fontSize: 26, fontWeight: 700, color: "#ffffff" }}>
          通知中心
        </span>

        <div style={{ position: "relative" }}>
          <div
            style={{
              background: "rgba(255,255,255,0.08)",
              borderRadius: 20,
              padding: "6px 20px",
              fontSize: 14,
              color: "#aaaaaa",
            }}
          >
            今日通知
          </div>
          {frame >= BADGE-START && (
            <div
              style={{
                position: "absolute",
                top: -8,
                right: -8,
                width: 28,
                height: 28,
                borderRadius: "50%",
                background: "#ef4444",
                display: "flex",
                alignItems: "center",
                justifyContent: "center",
                fontSize: 13,
                fontWeight: 700,
                color: "#ffffff",
                transform: `scale(${badgeScale * badgePulse})`,
                boxShadow: "0 0 12px rgba(239,68,68,0.6)",
              }}
            >
              6
            </div>
          )}
        </div>
      </div>

      {NOTIFS.map((notif, i) => {
        const startFrame = i * 15 + 5;
        if (frame < startFrame) return null;

        const progress = cardProgressList[i];
        const slideX = interpolate(progress, [0, 1], [300, 0], {
          extrapolateRight: "clamp",
        });
        const opacity = interpolate(progress, [0, 0.25], [0, 1], {
          extrapolateRight: "clamp",
        });

        const cardY = CARDS-TOP + i * (CARD-HEIGHT + CARD-GAP);
        const glowOpacity = interpolate(progress, [0.6, 1], [0.6, 0], {
          extrapolateLeft: "clamp",
          extrapolateRight: "clamp",
        });

        return (
          <div
            key={i}
            style={{
              position: "absolute",
              left: CARDS-LEFT,
              top: cardY,
              width: CARD-WIDTH,
              height: CARD-HEIGHT,
              opacity,
              transform: `translateX(${slideX}px)`,
              display: "flex",
              alignItems: "center",
              gap: 18,
              background: "rgba(255,255,255,0.04)",
              borderRadius: 14,
              borderLeft: `4px solid ${notif.color}`,
              padding: "0 20px 0 18px",
              boxShadow: `0 0 30px ${notif.color}${Math.round(glowOpacity * 60).toString(16).padStart(2, "0")}`,
              overflow: "hidden",
            }}
          >
            <div
              style={{
                width: 46,
                height: 46,
                borderRadius: "50%",
                background: notif.color,
                flexShrink: 0,
                display: "flex",
                alignItems: "center",
                justifyContent: "center",
                fontSize: notif.icon.length <= 2 ? 18 : 20,
                fontWeight: 700,
                color: "#ffffff",
              }}
            >
              {notif.icon}
            </div>

            <div style={{ flex: 1, minWidth: 0 }}>
              <div
                style={{
                  fontSize: 16,
                  fontWeight: 700,
                  color: "#ffffff",
                  whiteSpace: "nowrap",
                  overflow: "hidden",
                  textOverflow: "ellipsis",
                }}
              >
                {notif.title}
              </div>
              <div
                style={{
                  fontSize: 13,
                  color: "#888888",
                  marginTop: 4,
                  whiteSpace: "nowrap",
                  overflow: "hidden",
                  textOverflow: "ellipsis",
                }}
              >
                {notif.desc}
              </div>
            </div>

            <div
              style={{
                flexShrink: 0,
                fontSize: 11,
                fontWeight: 700,
                color: notif.color,
                background: `${notif.color}22`,
                borderRadius: 6,
                padding: "3px 8px",
                letterSpacing: 1,
              }}
            >
              {notif.platform}
            </div>

            <div
              style={{
                flexShrink: 0,
                fontSize: 12,
                color: "#555555",
                minWidth: 70,
                textAlign: "right",
              }}
            >
              {notif.time}
            </div>
          </div>
        );
      })}
    </AbsoluteFill>
  );
};

登入後查看完整程式碼