Parcourir la source

github-trending: 重设计封面与进度条章节目录

- 封面:深色标题卡居中靠上 + top6 仓库卡片网格(横屏 3×2 / 竖屏 2×3),每卡显示 名称/语言/今日 +N 涨星/highlights;浅底 + 稀疏代码·数据装饰
- 进度条上方新增 ChapterToc 章节目录,当前章节高亮(仅 github-trending)
- 附 test/fixtures/github-trending-cover.json 测试夹具

Co-Authored-By: Claude <noreply@anthropic.com>
lkatzey il y a 5 jours
Parent
commit
c38cf617ba
2 fichiers modifiés avec 418 ajouts et 57 suppressions
  1. 273 57
      packages/templates/src/Root.tsx
  2. 145 0
      test/fixtures/github-trending-cover.json

+ 273 - 57
packages/templates/src/Root.tsx

@@ -1,12 +1,13 @@
 import React, { useEffect, useState } from "react";
-import { Composition, Sequence, AbsoluteFill, Audio, staticFile, Img, useCurrentFrame, interpolate, continueRender, delayRender } from "remotion";
+import { Composition, Sequence, AbsoluteFill, Audio, staticFile, Img, useCurrentFrame, useVideoConfig, interpolate, continueRender, delayRender } from "remotion";
 import type { TemplateType, AspectRatio } from "@pipeline/shared";
 import type { WordTimestamp, GithubSceneData } from "@pipeline/shared";
+import { formatCount } from "@pipeline/shared";
 import NewsScene from "./news/index";
 import KnowledgeScene from "./knowledge/index";
 import OpinionScene from "./opinion/index";
 import MarketingScene from "./marketing/index";
-import GithubTrendingScene from "./github-trending/index";
+import GithubTrendingScene, { ColorDot } from "./github-trending/index";
 import { THEMES } from "./base/theme/colors";
 
 export interface RemotionScene {
@@ -76,6 +77,11 @@ const TEMPLATES: TemplateType[] = ["news", "knowledge", "opinion", "marketing",
 const TemplateComposition: React.FC<RemotionProps> = (props) => {
   const SceneComponent = SCENE_MAP[props.template];
   const totalFrames = props.scenes.at(-1)?.endFrame ?? 90;
+  // github-trending: the cover lists every repo (name / language / today's
+  // star gain / one-line description). Content scenes each carry scene.github.
+  const coverRepos = props.scenes.filter(
+    (s) => s.sceneType === "content" && s.github
+  );
 
   return (
     <AbsoluteFill style={{ backgroundColor: "#000" }}>
@@ -99,7 +105,7 @@ const TemplateComposition: React.FC<RemotionProps> = (props) => {
                 subtitle={props.subtitle}
                 keyframes={scene.keyframes}
                 backgroundAsset={scene.backgroundAsset}
-                coverTags={scene.coverTags}
+                coverRepos={coverRepos}
                 trendSummary={scene.trendSummary}
                 totalFrames={totalFrames}
               />
@@ -141,96 +147,222 @@ const TemplateComposition: React.FC<RemotionProps> = (props) => {
         );
       })}
       <GlobalProgressBar totalFrames={totalFrames} color={THEMES[props.template].primary} />
+      {props.template === "github-trending" && <ChapterToc scenes={props.scenes} />}
     </AbsoluteFill>
   );
 };
 
 // --- Cover Scene ---
 
+// Faint, sparse code/data motifs on the github-trending cover background.
+// Low-opacity and edge-anchored so the cover stays clean — the motifs read
+// only as subtle texture, never competing with the cards.
+const CoverDecor: React.FC<{ accent: string }> = ({ accent }) => {
+  const tok = (css: React.CSSProperties): React.CSSProperties => ({
+    position: "absolute",
+    fontFamily: "monospace",
+    fontWeight: 700,
+    color: "#0f172a",
+    opacity: 0.06,
+    letterSpacing: "0.02em",
+    ...css,
+  });
+  return (
+    <div style={{ position: "absolute", inset: 0, overflow: "hidden", pointerEvents: "none" }}>
+      <span style={tok({ top: 44, left: 52, fontSize: 56 })}>{"</>"}</span>
+      <span style={tok({ top: 58, right: 60, fontSize: 48 })}>{"{ }"}</span>
+      <span style={tok({ bottom: 168, left: 54, fontSize: 28 })}>{"01 10 01"}</span>
+      <span style={tok({ bottom: 150, right: 58, fontSize: 26 })}>{"git push"}</span>
+      {/* Faint data line chart */}
+      <svg width="180" height="84" viewBox="0 0 180 84" style={{ position: "absolute", top: 64, right: 168, opacity: 0.08 }}>
+        <polyline points="0,66 34,50 68,56 102,28 136,34 180,8" fill="none" stroke={accent} strokeWidth="3" strokeLinecap="round" strokeLinejoin="round" />
+      </svg>
+      {/* Faint data bar cluster */}
+      <div style={{ position: "absolute", bottom: 206, left: 80, display: "flex", alignItems: "flex-end", gap: 7, opacity: 0.08 }}>
+        <div style={{ width: 12, height: 32, background: accent, borderRadius: 3 }} />
+        <div style={{ width: 12, height: 58, background: accent, borderRadius: 3 }} />
+        <div style={{ width: 12, height: 24, background: accent, borderRadius: 3 }} />
+        <div style={{ width: 12, height: 76, background: accent, borderRadius: 3 }} />
+        <div style={{ width: 12, height: 46, background: accent, borderRadius: 3 }} />
+      </div>
+    </div>
+  );
+};
+
 const CoverScene: React.FC<{
   template: TemplateType;
   title: string;
   subtitle?: string;
   keyframes: Array<{ type: string; content: string }>;
   backgroundAsset?: { id: string; filename: string };
-  coverTags?: string[];
+  coverRepos?: RemotionScene[];
   trendSummary?: string;
   totalFrames: number;
-}> = ({ template, title, subtitle, keyframes, backgroundAsset, coverTags, trendSummary }) => {
+}> = ({ template, title, subtitle, keyframes, backgroundAsset, coverRepos, trendSummary }) => {
   const accent = THEMES[template].primaryLight;
   const isGithubTrending = template === "github-trending";
+  const { width, height } = useVideoConfig();
+  const isPortrait = height > width;
 
-  // github-trending cover doubles as the opening narration screen — light
-  // background, large masthead typography, no dark image overlay. It also
-  // shows the day's theme tags (coverTags) and a one-line trend summary
-  // (trendSummary), both LLM-produced.
+  // github-trending cover: a DARK title card (contrast over a light bg with
+  // faint code/data motifs) centered near the top, then the TOP 3 repos by
+  // today's star gain as large rounded "floating" cards — horizontal row in
+  // landscape, vertical stack in portrait. Doubles as the opening narration.
   if (isGithubTrending) {
-    const hasTags = !!(coverTags && coverTags.length > 0);
+    const topRepos = [...(coverRepos ?? [])]
+      .sort((a, b) => (b.github!.repo.todayStars ?? 0) - (a.github!.repo.todayStars ?? 0))
+      .slice(0, 6);
+    const primary = THEMES[template].primary;
+
     return (
       <AbsoluteFill style={{
-        background: "linear-gradient(135deg, #f8fafc 0%, #e2e8f0 60%, #cbd5e1 100%)",
+        background: "linear-gradient(135deg, #f8fafc 0%, #eef2f7 55%, #e2e8f0 100%)",
       }}>
-        {/* Top accent bar in template primary */}
+        <CoverDecor accent={accent} />
+        {/* Top accent line */}
         <div style={{
-          position: "absolute", top: 0, left: 0, width: "100%", height: 8,
-          background: `linear-gradient(90deg, ${THEMES[template].primary}, ${accent})`,
+          position: "absolute", top: 0, left: 0, width: "100%", height: 6,
+          background: `linear-gradient(90deg, ${primary}, ${accent})`,
         }} />
         <div style={{
-          position: "absolute", inset: 0, display: "flex",
-          flexDirection: "column", justifyContent: "center", alignItems: "center",
-          padding: 80, textAlign: "center",
+          position: "absolute", inset: 0, display: "flex", flexDirection: "column",
+          alignItems: "center",
+          justifyContent: isPortrait ? "center" : "flex-start",
+          padding: isPortrait ? "72px 64px 130px" : "58px 80px 112px",
         }}>
+          {/* DARK title card — centered, near the top (contrast over light bg) */}
           <div style={{
-            fontSize: 104, fontWeight: 800, color: "#0f172a",
-            fontFamily: "Noto Sans SC", lineHeight: 1.15,
-            marginBottom: 28, letterSpacing: "-0.02em",
+            display: "flex", flexDirection: "column", alignItems: "center", gap: 14,
+            padding: isPortrait ? "38px 76px" : "32px 88px",
+            borderRadius: 26,
+            background: "linear-gradient(135deg, #0f172a 0%, #1e293b 100%)",
+            border: `1.5px solid ${primary}55`,
+            boxShadow: "0 18px 44px rgba(15,23,42,0.22)",
+            marginBottom: isPortrait ? 44 : 38,
+            maxWidth: isPortrait ? "84%" : "74%",
+            textAlign: "center",
           }}>
-            {title}
-          </div>
-          {subtitle && (
             <div style={{
-              fontSize: 50, fontWeight: 600, color: "#475569",
-              fontFamily: "Noto Sans SC",
-              padding: "12px 36px",
-              borderRadius: 16,
-              background: "rgba(255,255,255,0.7)",
-              border: `2px solid ${accent}`,
-              boxShadow: "0 8px 24px rgba(15,23,42,0.08)",
-              marginBottom: hasTags || trendSummary ? 44 : 0,
+              fontSize: isPortrait ? 72 : 66, fontWeight: 800, color: "#ffffff",
+              fontFamily: "Noto Sans SC", lineHeight: 1.15, letterSpacing: "-0.02em",
             }}>
-              {subtitle}
+              {title}
             </div>
-          )}
-          {hasTags && (
             <div style={{
-              display: "flex", flexWrap: "wrap", justifyContent: "center",
-              gap: 18, maxWidth: "82%", marginBottom: trendSummary ? 36 : 0,
+              display: "flex", alignItems: "center", gap: 14, flexWrap: "wrap",
+              justifyContent: "center",
             }}>
-              {coverTags!.map((tag, i) => (
-                <div key={i} style={{
-                  display: "inline-flex", alignItems: "center", gap: 12,
-                  padding: "12px 26px", borderRadius: 12,
-                  background: "rgba(255,255,255,0.78)",
-                  border: `1.5px solid ${accent}66`,
-                  color: "#1e293b",
-                  fontFamily: "Noto Sans SC", fontSize: 34, fontWeight: 600,
-                  boxShadow: "0 2px 10px rgba(15,23,42,0.06)",
+              {subtitle && (
+                <span style={{
+                  fontSize: isPortrait ? 30 : 26, fontWeight: 600, color: accent,
+                  fontFamily: "Noto Sans SC",
                 }}>
-                  <span style={{
-                    display: "inline-block", width: 11, height: 11,
-                    borderRadius: "50%", background: accent, flexShrink: 0,
-                  }} />
-                  {tag}
-                </div>
-              ))}
+                  {subtitle}
+                </span>
+              )}
+              {trendSummary && (
+                <span style={{
+                  fontSize: isPortrait ? 28 : 24, fontWeight: 500, color: "#cbd5e1",
+                  fontFamily: "Noto Sans SC",
+                }}>
+                  · {trendSummary}
+                </span>
+              )}
             </div>
-          )}
-          {trendSummary && (
+          </div>
+
+          {/* TOP 6 repos by today's star gain (3 per row → wraps to a grid) */}
+          {topRepos.length > 0 && (
             <div style={{
-              fontSize: 40, fontWeight: 500, color: "#475569",
-              fontFamily: "Noto Sans SC", maxWidth: "70%", lineHeight: 1.5,
+              display: "flex", flexWrap: "wrap",
+              gap: 24,
+              width: "100%",
+              maxWidth: isPortrait ? "90%" : "94%",
             }}>
-              {trendSummary}
+              {topRepos.map((s, i) => {
+                const g = s.github!;
+                const repo = g.repo;
+                const language = repo.language || "";
+                const languageColor = repo.languageColor || accent;
+                const todayLabel = repo.todayStars != null ? `+${formatCount(repo.todayStars)}` : "";
+                return (
+                  <div key={s.id} style={{
+                    flex: isPortrait ? "0 0 calc((100% - 24px) / 2)" : "0 0 calc((100% - 48px) / 3)",
+                    minWidth: 0,
+                    display: "flex", flexDirection: "column",
+                    padding: isPortrait ? "28px 30px" : "30px",
+                    borderRadius: 22,
+                    background: "#ffffff",
+                    border: `1px solid ${primary}22`,
+                    boxShadow: "0 16px 38px rgba(15,23,42,0.12), 0 2px 8px rgba(15,23,42,0.06)",
+                  }}>
+                    {/* Rank badge + today's gain (the selection signal) */}
+                    <div style={{
+                      display: "flex", alignItems: "center", justifyContent: "space-between",
+                      marginBottom: 18,
+                    }}>
+                      <div style={{
+                        display: "inline-flex", alignItems: "center", justifyContent: "center",
+                        width: isPortrait ? 56 : 52, height: isPortrait ? 56 : 52,
+                        borderRadius: "50%",
+                        background: `linear-gradient(135deg, ${accent}, ${primary})`,
+                        color: "#fff", fontWeight: 800, fontFamily: "Noto Sans SC",
+                        fontSize: isPortrait ? 30 : 26,
+                        boxShadow: "0 4px 12px rgba(34,197,94,0.35)",
+                      }}>
+                        {i + 1}
+                      </div>
+                      {todayLabel && (
+                        <span style={{
+                          fontSize: isPortrait ? 56 : 50, fontWeight: 800, color: primary,
+                          fontFamily: "Noto Sans SC", lineHeight: 1,
+                        }}>{todayLabel}</span>
+                      )}
+                    </div>
+                    {/* Repo full name */}
+                    <div style={{
+                      fontSize: isPortrait ? 40 : 34, fontWeight: 800, color: "#0f172a",
+                      fontFamily: "Noto Sans SC", lineHeight: 1.2, marginBottom: 12,
+                      wordBreak: "break-word",
+                    }}>
+                      {repo.fullName}
+                    </div>
+                    {/* Language + total stars */}
+                    <div style={{
+                      display: "flex", alignItems: "center", gap: 14, flexWrap: "wrap",
+                      marginBottom: 14, fontFamily: "Noto Sans SC",
+                    }}>
+                      {language && (
+                        <span style={{
+                          display: "inline-flex", alignItems: "center", gap: 8,
+                          fontSize: isPortrait ? 24 : 21, fontWeight: 600, color: "#334155",
+                          padding: "5px 14px", borderRadius: 999,
+                          background: `${languageColor}1a`, border: `1px solid ${languageColor}55`,
+                        }}>
+                          <ColorDot color={languageColor} size={isPortrait ? 14 : 12} />
+                          {language}
+                        </span>
+                      )}
+                      {repo.stars != null && (
+                        <span style={{
+                          fontSize: isPortrait ? 23 : 20, color: "#64748b", fontWeight: 500,
+                        }}>
+                          {formatCount(repo.stars)} stars
+                        </span>
+                      )}
+                    </div>
+                    {/* One-line description (highlights) */}
+                    <div style={{
+                      fontSize: isPortrait ? 27 : 23, lineHeight: 1.4, color: "#475569",
+                      fontFamily: "Noto Sans SC",
+                      display: "-webkit-box", WebkitLineClamp: 2, WebkitBoxOrient: "vertical",
+                      overflow: "hidden",
+                    }}>
+                      {g.highlights}
+                    </div>
+                  </div>
+                );
+              })}
             </div>
           )}
         </div>
@@ -370,6 +502,90 @@ const GlobalProgressBar: React.FC<{
   );
 };
 
+const ChapterToc: React.FC<{ scenes: RemotionScene[] }> = ({ scenes }) => {
+  // github-trending only: a chapter table of contents pinned just above the
+  // global progress bar. Lists every repo's short name; the chapter whose
+  // [startFrame, endFrame) contains the current frame is highlighted. A faint
+  // dark gradient strip behind the row keeps labels legible over light
+  // backgrounds (e.g. the cover) and visually groups the TOC with the bar.
+  const frame = useCurrentFrame();
+  const { width, height } = useVideoConfig();
+  const isPortrait = height > width;
+  const accent = THEMES["github-trending"].primary;
+
+  const chapters = scenes.filter((s) => s.sceneType === "content" && s.github);
+  const active = chapters.findIndex(
+    (c) => frame >= c.startFrame && frame < c.endFrame
+  );
+
+  return (
+    <>
+      <div
+        style={{
+          position: "absolute",
+          bottom: 0,
+          left: 0,
+          width: "100%",
+          height: 72,
+          background: "linear-gradient(to top, rgba(0,0,0,0.42), transparent)",
+          pointerEvents: "none",
+        }}
+      />
+      <div
+        style={{
+          position: "absolute",
+          bottom: 12,
+          left: 0,
+          width: "100%",
+          display: "flex",
+          justifyContent: "center",
+          alignItems: "center",
+          gap: isPortrait ? 16 : 12,
+          padding: "0 32px",
+          fontFamily: "Noto Sans SC",
+          pointerEvents: "none",
+        }}
+      >
+        {chapters.map((c, i) => {
+          const isActive = i === active;
+          const repo = c.github!.repo;
+          const label = repo.name || repo.fullName || c.displayText || "";
+          return (
+            <React.Fragment key={c.id}>
+              {i > 0 && (
+                <span
+                  style={{
+                    color: "rgba(255,255,255,0.3)",
+                    fontSize: isPortrait ? 22 : 18,
+                    flexShrink: 0,
+                  }}
+                >
+                  ·
+                </span>
+              )}
+              <span
+                style={{
+                  flex: "1 1 0",
+                  minWidth: 0,
+                  fontSize: isPortrait ? 26 : 22,
+                  fontWeight: isActive ? 700 : 500,
+                  color: isActive ? accent : "rgba(255,255,255,0.6)",
+                  whiteSpace: "nowrap",
+                  overflow: "hidden",
+                  textOverflow: "ellipsis",
+                  textAlign: "center",
+                }}
+              >
+                {label}
+              </span>
+            </React.Fragment>
+          );
+        })}
+      </div>
+    </>
+  );
+};
+
 const defaultProps: RemotionProps = {
   scenes: [
     {

+ 145 - 0
test/fixtures/github-trending-cover.json

@@ -0,0 +1,145 @@
+{
+  "title": "GitHub 每日热榜",
+  "summary": "今日精选四个值得关注的开源项目",
+  "trendSummary": "AI Agent 与代码智能工具持续升温",
+  "scenes": [
+    {
+      "id": "scene-1",
+      "narration": "codebase-memory-mcp 是一个高性能代码智能服务,把整个代码库索引成知识图谱。",
+      "displayText": "DeusData/codebase-memory-mcp",
+      "duration": 5,
+      "github": {
+        "repo": {
+          "owner": "DeusData",
+          "name": "codebase-memory-mcp",
+          "fullName": "DeusData/codebase-memory-mcp",
+          "language": "C",
+          "languageColor": "#555555",
+          "stars": 8590,
+          "forks": 653,
+          "license": "MIT",
+          "todayStars": 142
+        },
+        "highlights": "代码知识图谱 + MCP 协议",
+        "intro": "把代码库索引成持久化知识图谱的高性能 MCP 服务,支持跨文件语义检索与引用追踪,适合大型工程的代码智能场景。",
+        "review": "适合构建代码助手与 IDE 智能后端"
+      }
+    },
+    {
+      "id": "scene-2",
+      "narration": "React 是 Facebook 出品的声明式 UI 库,生态最大。",
+      "displayText": "facebook/react",
+      "duration": 5,
+      "github": {
+        "repo": {
+          "owner": "facebook",
+          "name": "react",
+          "fullName": "facebook/react",
+          "language": "JavaScript",
+          "languageColor": "#f1e05a",
+          "stars": 232000,
+          "forks": 47600,
+          "license": "MIT",
+          "todayStars": 95
+        },
+        "highlights": "声明式 UI + 虚拟 DOM,生态最大",
+        "intro": "Facebook 出品的声明式组件化 UI 库,状态驱动视图更新,虚拟 DOM 优化渲染,适合复杂交互的 Web 应用。",
+        "review": "中大型 Web 应用的默认选择"
+      }
+    },
+    {
+      "id": "scene-3",
+      "narration": "Ollama 让本地一键运行大模型变得简单。",
+      "displayText": "ollama/ollama",
+      "duration": 5,
+      "github": {
+        "repo": {
+          "owner": "ollama",
+          "name": "ollama",
+          "fullName": "ollama/ollama",
+          "language": "Go",
+          "languageColor": "#00ADD8",
+          "stars": 142000,
+          "forks": 11300,
+          "license": "MIT",
+          "todayStars": 310
+        },
+        "highlights": "本地一键运行开源大模型",
+        "intro": "用 Go 实现的本地大模型运行时,一条命令拉取并运行 Llama、Qwen 等开源模型,提供 REST API,便于集成到本地工具链。",
+        "review": "想在本地跑大模型的开发者首选"
+      }
+    },
+    {
+      "id": "scene-4",
+      "narration": "Next.js 是 Vercel 出品的 React 全栈应用框架。",
+      "displayText": "vercel/next.js",
+      "duration": 5,
+      "github": {
+        "repo": {
+          "owner": "vercel",
+          "name": "next.js",
+          "fullName": "vercel/next.js",
+          "language": "TypeScript",
+          "languageColor": "#3178c6",
+          "stars": 128000,
+          "forks": 27200,
+          "license": "MIT",
+          "todayStars": 76
+        },
+        "highlights": "React 全栈框架,SSR/SSG/RSC",
+        "intro": "Vercel 出品的 React 全栈应用框架,内置服务端渲染、静态生成与 React Server Components,配套文件路由与边缘部署。",
+        "review": "追求生产效率的全栈团队"
+      }
+    },
+    {
+      "id": "scene-5",
+      "narration": "Rust 是系统级编程语言,靠所有权机制在编译期保证内存安全。",
+      "displayText": "rust-lang/rust",
+      "duration": 5,
+      "github": {
+        "repo": {
+          "owner": "rust-lang",
+          "name": "rust",
+          "fullName": "rust-lang/rust",
+          "language": "Rust",
+          "languageColor": "#dea584",
+          "stars": 102000,
+          "forks": 13300,
+          "license": "Apache-2.0",
+          "todayStars": 120
+        },
+        "highlights": "内存安全 + 零成本抽象的系统语言",
+        "intro": "Rust 是一门系统级编程语言,凭借所有权机制在编译期保证内存与线程安全,无需垃圾回收,适合操作系统、WebAssembly 与高性能服务端。",
+        "review": "追求性能与安全的底层系统开发"
+      }
+    },
+    {
+      "id": "scene-6",
+      "narration": "Tailwind CSS 是原子化实用类 CSS 框架。",
+      "displayText": "tailwindlabs/tailwindcss",
+      "duration": 5,
+      "github": {
+        "repo": {
+          "owner": "tailwindlabs",
+          "name": "tailwindcss",
+          "fullName": "tailwindlabs/tailwindcss",
+          "language": "TypeScript",
+          "languageColor": "#3178c6",
+          "stars": 84000,
+          "forks": 4100,
+          "license": "MIT",
+          "todayStars": 58
+        },
+        "highlights": "原子化实用类 CSS 框架",
+        "intro": "Tailwind CSS 是原子化 CSS 框架,通过组合实用类直接在标记中构建设计系统,产出体积小且一致的 UI。",
+        "review": "偏好实用优先工作流的前端团队"
+      }
+    }
+  ],
+  "outro": {
+    "text": "今天的 GitHub 热榜就到这里",
+    "narration": "感谢观看,我们明天见。",
+    "cta": "点赞 | 关注"
+  },
+  "globalStyle": { "tone": "calm", "pace": "normal" }
+}