Remotion LabRemotion Lab
返回模板庫

GitHub Repo 卡片

仿 GitHub 暗色模式的 Repository 分享卡片,包含倉庫名稱、描述、主題標籤、⭐ 星數與語言統計,帶有依序出現的入場動畫。

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

const USERNAME = "username";
const REPO-NAME = "awesome-project";
const DESCRIPTION = "一個超棒的開源專案,讓開發者更高效地完成工作。歡迎 Star ⭐";
const TOPICS = ["TypeScript", "React", "Animation", "Open Source"];
const STARS = 2400;
const FORKS = 342;
const WATCHERS = 1800;
const LICENSE = "MIT";
const LAST-UPDATE = "3 小時前";
const LANGUAGE = "TypeScript";
const LANG-PERCENT = "94.2%";

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

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

  // Header (logo + url): fade in, frames 8-22
  const headerOpacity = interpolate(frame, [8, 22], [0, 1], {
    extrapolateLeft: "clamp",
    extrapolateRight: "clamp",
  });

  // Repo name: slide from left, frames 18-32
  const repoNameProgress = spring({
    frame: frame - 18,
    fps,
    config: { damping: 22, stiffness: 160 },
    durationInFrames: 14,
  });
  const repoNameX = interpolate(repoNameProgress, [0, 1], [-40, 0]);
  const repoNameOpacity = interpolate(repoNameProgress, [0, 0.4], [0, 1], {
    extrapolateRight: "clamp",
  });

  // Description: fade in, frames 28-44
  const descOpacity = interpolate(frame, [28, 44], [0, 1], {
    extrapolateLeft: "clamp",
    extrapolateRight: "clamp",
  });

  // Topic tags: pop in one by one, frames 38-65 (stagger 5 frames)
  const tagScales = TOPICS.map((_, i) => {
    const p = spring({
      frame: frame - (38 + i * 5),
      fps,
      config: { damping: 16, stiffness: 220 },
      durationInFrames: 10,
    });
    return interpolate(p, [0, 1], [0, 1], { extrapolateRight: "clamp" });
  });

  // Stats count up: frames 55-90
  const statsRaw = interpolate(frame, [55, 90], [0, 1], {
    extrapolateLeft: "clamp",
    extrapolateRight: "clamp",
  });
  const currentStars = Math.round(statsRaw * STARS);
  const currentForks = Math.round(statsRaw * FORKS);
  const currentWatchers = Math.round(statsRaw * WATCHERS);

  // Bottom bar: fade in, frames 75-88
  const bottomOpacity = interpolate(frame, [75, 88], [0, 1], {
    extrapolateLeft: "clamp",
    extrapolateRight: "clamp",
  });

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

  return (
    <AbsoluteFill
      style={{
        background: "#0f0f0f",
        justifyContent: "center",
        alignItems: "center",
      }}
    >
      {/* Card */}
      <div
        style={{
          transform: `scale(${cardScale})`,
          opacity: cardOpacity,
          background: "#0d1117",
          border: "1px solid #30363d",
          borderRadius: 12,
          padding: "32px 40px",
          width: 860,
          boxSizing: "border-box",
          fontFamily: "sans-serif",
          boxShadow: "0 24px 64px rgba(0,0,0,0.7)",
        }}
      >
        {/* Header: octocat + github.com */}
        <div
          style={{
            display: "flex",
            alignItems: "center",
            gap: 10,
            marginBottom: 20,
            opacity: headerOpacity,
          }}
        >
          <div
            style={{
              width: 32,
              height: 32,
              borderRadius: "50%",
              background: "#f0f6fc",
              display: "flex",
              alignItems: "center",
              justifyContent: "center",
              fontSize: 18,
              flexShrink: 0,
            }}
          >

          </div>
          <span style={{ fontSize: 15, color: "#8b949e", fontWeight: 500 }}>
            github.com
          </span>
        </div>

        {/* Divider */}
        <div
          style={{
            height: 1,
            background: "#21262d",
            marginBottom: 24,
            opacity: headerOpacity,
          }}
        />

        {/* Repo name */}
        <div
          style={{
            transform: `translateX(${repoNameX}px)`,
            opacity: repoNameOpacity,
            marginBottom: 12,
          }}
        >
          <span style={{ fontSize: 26, fontWeight: 400, color: "#8b949e" }}>
            {USERNAME}
          </span>
          <span style={{ fontSize: 26, fontWeight: 400, color: "#8b949e" }}>
            {" / "}
          </span>
          <span style={{ fontSize: 28, fontWeight: 700, color: "#f0f6fc" }}>
            {REPO-NAME}
          </span>
        </div>

        {/* Description */}
        <div
          style={{
            fontSize: 16,
            color: "#8b949e",
            lineHeight: 1.6,
            marginBottom: 20,
            opacity: descOpacity,
          }}
        >
          {DESCRIPTION}
        </div>

        {/* Topic tags */}
        <div
          style={{
            display: "flex",
            flexWrap: "wrap",
            gap: 8,
            marginBottom: 24,
          }}
        >
          {TOPICS.map((topic, i) => (
            <div
              key={topic}
              style={{
                transform: `scale(${tagScales[i]})`,
                background: "transparent",
                border: "1px solid #30363d",
                borderRadius: 20,
                padding: "4px 14px",
                fontSize: 14,
                color: "#58a6ff",
                fontWeight: 500,
              }}
            >
              {topic}
            </div>
          ))}
        </div>

        {/* Divider */}
        <div
          style={{
            height: 1,
            background: "#21262d",
            marginBottom: 20,
            opacity: statsRaw > 0 ? 1 : 0,
          }}
        />

        {/* Stats row */}
        <div
          style={{
            display: "flex",
            gap: 28,
            marginBottom: 24,
            alignItems: "center",
          }}
        >
          {[
            { icon: "⭐", value: currentStars },
            { icon: "🍴", value: currentForks },
            { icon: "👁", value: currentWatchers },
          ].map((stat, idx) => (
            <div
              key={idx}
              style={{
                display: "flex",
                alignItems: "center",
                gap: 6,
                color: "#8b949e",
                fontSize: 15,
                opacity: statsRaw,
              }}
            >
              <span style={{ fontSize: 18 }}>{stat.icon}</span>
              <span style={{ fontWeight: 600, color: "#f0f6fc" }}>
                {formatCount(stat.value)}
              </span>
            </div>
          ))}
          <div
            style={{
              display: "flex",
              alignItems: "center",
              gap: 6,
              color: "#8b949e",
              fontSize: 15,
              opacity: statsRaw,
            }}
          >
            <span>📄 {LICENSE}</span>
          </div>
        </div>

        {/* Bottom bar */}
        <div
          style={{
            display: "flex",
            alignItems: "center",
            justifyContent: "space-between",
            opacity: bottomOpacity,
          }}
        >
          <span style={{ fontSize: 14, color: "#8b949e" }}>
            上次更新:{LAST-UPDATE}
          </span>
          <div
            style={{
              display: "flex",
              alignItems: "center",
              gap: 8,
            }}
          >
            <span
              style={{
                width: 12,
                height: 12,
                borderRadius: "50%",
                background: "#3178c6",
                display: "inline-block",
              }}
            />
            <span style={{ fontSize: 14, color: "#8b949e" }}>
              {LANGUAGE} {LANG-PERCENT}
            </span>
          </div>
        </div>
      </div>
    </AbsoluteFill>
  );
};

登入後查看完整程式碼