|
@@ -1,6 +1,7 @@
|
|
|
import React, { useEffect, useState } from "react";
|
|
import React, { useEffect, useState } from "react";
|
|
|
import { Composition, Sequence, AbsoluteFill, Audio, staticFile, Img, useCurrentFrame, useVideoConfig, interpolate, continueRender, delayRender } from "remotion";
|
|
import { Composition, Sequence, AbsoluteFill, Audio, staticFile, Img, useCurrentFrame, useVideoConfig, interpolate, continueRender, delayRender } from "remotion";
|
|
|
import type { TemplateType, AspectRatio, RenderScene, RenderProps } from "@pipeline/shared";
|
|
import type { TemplateType, AspectRatio, RenderScene, RenderProps } from "@pipeline/shared";
|
|
|
|
|
+import { formatCount } from "@pipeline/shared";
|
|
|
import NewsScene from "./news/index";
|
|
import NewsScene from "./news/index";
|
|
|
import KnowledgeScene from "./knowledge/index";
|
|
import KnowledgeScene from "./knowledge/index";
|
|
|
import OpinionScene from "./opinion/index";
|
|
import OpinionScene from "./opinion/index";
|
|
@@ -9,6 +10,7 @@ import GithubTrendingScene, { ColorDot } from "./github-trending/index";
|
|
|
import GithubWeeklyScene from "./github-weekly/index";
|
|
import GithubWeeklyScene from "./github-weekly/index";
|
|
|
import GithubDailyPickScene from "./github-daily-pick/index";
|
|
import GithubDailyPickScene from "./github-daily-pick/index";
|
|
|
import { THEMES } from "./base/theme/colors";
|
|
import { THEMES } from "./base/theme/colors";
|
|
|
|
|
+import { Watermark } from "./base/components/watermark";
|
|
|
|
|
|
|
|
// Re-export the render-time contract so legacy imports (`import { RemotionScene
|
|
// Re-export the render-time contract so legacy imports (`import { RemotionScene
|
|
|
// } from "../Root"`) keep resolving during the migration. New code should import
|
|
// } from "../Root"`) keep resolving during the migration. New code should import
|
|
@@ -134,8 +136,11 @@ const TemplateComposition: React.FC<RenderProps> = (props) => {
|
|
|
>
|
|
>
|
|
|
{sceneAudio}
|
|
{sceneAudio}
|
|
|
<OutroScene
|
|
<OutroScene
|
|
|
|
|
+ template={props.template}
|
|
|
text={scene.captionOrigin ?? ""}
|
|
text={scene.captionOrigin ?? ""}
|
|
|
cta={scene.title}
|
|
cta={scene.title}
|
|
|
|
|
+ channelName={props.channelName}
|
|
|
|
|
+ outroRepos={coverRepos}
|
|
|
totalFrames={totalFrames}
|
|
totalFrames={totalFrames}
|
|
|
/>
|
|
/>
|
|
|
</Sequence>
|
|
</Sequence>
|
|
@@ -172,22 +177,22 @@ const TemplateComposition: React.FC<RenderProps> = (props) => {
|
|
|
|
|
|
|
|
// --- Cover Scene ---
|
|
// --- 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 title card. Colors adapt
|
|
|
|
|
-// to the backdrop: near-black on the light gradient, white on the dark
|
|
|
|
|
-// scrim over the background image.
|
|
|
|
|
|
|
+// Faint, sparse code/data motifs on the github board covers (and the outro —
|
|
|
|
|
+// the video closes the way it opened). Edge-anchored so the masthead stays
|
|
|
|
|
+// clean: the motifs read as subtle texture, never competing with the
|
|
|
|
|
+// headline. Colors adapt to the backdrop: near-black on the paper covers,
|
|
|
|
|
+// white on the dark stage.
|
|
|
const CoverDecor: React.FC<{ accent: string; onDark?: boolean }> = ({ accent, onDark }) => {
|
|
const CoverDecor: React.FC<{ accent: string; onDark?: boolean }> = ({ accent, onDark }) => {
|
|
|
const tok = (css: React.CSSProperties): React.CSSProperties => ({
|
|
const tok = (css: React.CSSProperties): React.CSSProperties => ({
|
|
|
position: "absolute",
|
|
position: "absolute",
|
|
|
fontFamily: "monospace",
|
|
fontFamily: "monospace",
|
|
|
fontWeight: 700,
|
|
fontWeight: 700,
|
|
|
color: onDark ? "#ffffff" : "#0f172a",
|
|
color: onDark ? "#ffffff" : "#0f172a",
|
|
|
- opacity: onDark ? 0.08 : 0.06,
|
|
|
|
|
|
|
+ opacity: onDark ? 0.08 : 0.1,
|
|
|
letterSpacing: "0.02em",
|
|
letterSpacing: "0.02em",
|
|
|
...css,
|
|
...css,
|
|
|
});
|
|
});
|
|
|
- const chartOpacity = onDark ? 0.16 : 0.08;
|
|
|
|
|
|
|
+ const chartOpacity = onDark ? 0.16 : 0.15;
|
|
|
return (
|
|
return (
|
|
|
<div style={{ position: "absolute", inset: 0, overflow: "hidden", pointerEvents: "none" }}>
|
|
<div style={{ position: "absolute", inset: 0, overflow: "hidden", pointerEvents: "none" }}>
|
|
|
<span style={tok({ top: 44, left: 52, fontSize: 56 })}>{"</>"}</span>
|
|
<span style={tok({ top: 44, left: 52, fontSize: 56 })}>{"</>"}</span>
|
|
@@ -221,7 +226,6 @@ const CoverScene: React.FC<{
|
|
|
totalFrames: number;
|
|
totalFrames: number;
|
|
|
}> = ({ template, title, subtitle, cardList, backgroundAsset, coverRepos, trendSummary }) => {
|
|
}> = ({ template, title, subtitle, cardList, backgroundAsset, coverRepos, trendSummary }) => {
|
|
|
const accent = THEMES[template].primaryLight;
|
|
const accent = THEMES[template].primaryLight;
|
|
|
- const isGithubTrending = template === "github-trending" || template === "github-daily-pick";
|
|
|
|
|
const isGithubWeekly = template === "github-weekly" || template === "github-weekly-recap";
|
|
const isGithubWeekly = template === "github-weekly" || template === "github-weekly-recap";
|
|
|
const { width, height } = useVideoConfig();
|
|
const { width, height } = useVideoConfig();
|
|
|
const isPortrait = height > width;
|
|
const isPortrait = height > width;
|
|
@@ -261,6 +265,8 @@ const CoverScene: React.FC<{
|
|
|
`,
|
|
`,
|
|
|
backgroundSize: "96px 96px",
|
|
backgroundSize: "96px 96px",
|
|
|
}} />
|
|
}} />
|
|
|
|
|
+ {/* Code/data motifs — the cover's tech texture */}
|
|
|
|
|
+ <CoverDecor accent={primary} />
|
|
|
{/* Editorial double rule, top and bottom */}
|
|
{/* Editorial double rule, top and bottom */}
|
|
|
<div style={{ position: "absolute", top: 0, left: 0, width: "100%", height: 8, background: primary }} />
|
|
<div style={{ position: "absolute", top: 0, left: 0, width: "100%", height: 8, background: primary }} />
|
|
|
<div style={{ position: "absolute", top: 14, left: 0, width: "100%", height: 1, background: `${primary}66` }} />
|
|
<div style={{ position: "absolute", top: 14, left: 0, width: "100%", height: 1, background: `${primary}66` }} />
|
|
@@ -290,11 +296,12 @@ const CoverScene: React.FC<{
|
|
|
}}>
|
|
}}>
|
|
|
{title}
|
|
{title}
|
|
|
</div>
|
|
</div>
|
|
|
- {/* Week range */}
|
|
|
|
|
|
|
+ {/* Week range — the issue date, set large so the week reads at a
|
|
|
|
|
+ glance (journal issue-line convention). */}
|
|
|
{subtitle && (
|
|
{subtitle && (
|
|
|
<div style={{
|
|
<div style={{
|
|
|
- fontSize: isPortrait ? 38 : 34, fontWeight: 600, color: "#57534e",
|
|
|
|
|
- fontFamily: "Noto Sans SC", letterSpacing: "0.08em",
|
|
|
|
|
|
|
+ fontSize: isPortrait ? 52 : 46, fontWeight: 700, color: "#1e293b",
|
|
|
|
|
+ fontFamily: "Noto Sans SC", letterSpacing: "0.06em",
|
|
|
marginTop: 26,
|
|
marginTop: 26,
|
|
|
}}>
|
|
}}>
|
|
|
{subtitle}
|
|
{subtitle}
|
|
@@ -343,14 +350,12 @@ const CoverScene: React.FC<{
|
|
|
);
|
|
);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // github-trending / github-daily-pick cover: the renderer-provided
|
|
|
|
|
- // background image (default.png fallback — no template-specific bg exists
|
|
|
|
|
- // yet) under a dark scrim, with a DARK title card fully centered on top — no
|
|
|
|
|
- // repo list. Shows only the masthead title, the date, the day's main
|
|
|
|
|
- // language tags (aggregated across today's repos / the featured repo), and
|
|
|
|
|
- // the trend summary line. Light gradient + CoverDecor is the no-image
|
|
|
|
|
- // fallback (e.g. studio demo props).
|
|
|
|
|
- if (isGithubTrending) {
|
|
|
|
|
|
|
+ // github-daily-pick cover: the renderer-provided background image
|
|
|
|
|
+ // (default.png fallback) under a dark scrim, with a DARK title card fully
|
|
|
|
|
+ // centered on top — the deep-dive board keeps the dark stage. Shows only the
|
|
|
|
|
+ // masthead title, the date, the featured repo's language tags, and the trend
|
|
|
|
|
+ // summary line. Light gradient + CoverDecor is the no-image fallback.
|
|
|
|
|
+ if (template === "github-daily-pick") {
|
|
|
const primary = THEMES[template].primary;
|
|
const primary = THEMES[template].primary;
|
|
|
// Aggregate languages across today's repos → the cover's "main tags".
|
|
// Aggregate languages across today's repos → the cover's "main tags".
|
|
|
// Count-weighted, top repo's languageColor wins per language.
|
|
// Count-weighted, top repo's languageColor wins per language.
|
|
@@ -413,11 +418,11 @@ const CoverScene: React.FC<{
|
|
|
}}>
|
|
}}>
|
|
|
{title}
|
|
{title}
|
|
|
</div>
|
|
</div>
|
|
|
- {/* Date */}
|
|
|
|
|
|
|
+ {/* Date — the issue line, set large so the day reads at a glance */}
|
|
|
{subtitle && (
|
|
{subtitle && (
|
|
|
<div style={{
|
|
<div style={{
|
|
|
- fontSize: isPortrait ? 36 : 32, fontWeight: 600, color: accent,
|
|
|
|
|
- fontFamily: "Noto Sans SC",
|
|
|
|
|
|
|
+ fontSize: isPortrait ? 52 : 46, fontWeight: 700, color: accent,
|
|
|
|
|
+ fontFamily: "Noto Sans SC", letterSpacing: "0.04em",
|
|
|
}}>
|
|
}}>
|
|
|
{subtitle}
|
|
{subtitle}
|
|
|
</div>
|
|
</div>
|
|
@@ -466,6 +471,168 @@ const CoverScene: React.FC<{
|
|
|
);
|
|
);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ // github-trending cover: the LIGHT daily board — paper masthead matching the
|
|
|
|
|
+ // (now light) content scenes: editorial rules top/bottom, masthead title +
|
|
|
|
|
+ // date + language tags + trend summary typeset on paper. No background image
|
|
|
|
|
+ // (the paper IS the identity, same choice as github-weekly).
|
|
|
|
|
+ if (template === "github-trending") {
|
|
|
|
|
+ const theme = THEMES[template];
|
|
|
|
|
+ const primary = theme.primary;
|
|
|
|
|
+ // Aggregate languages across today's repos → the cover's "main tags".
|
|
|
|
|
+ // Count-weighted, top repo's languageColor wins per language.
|
|
|
|
|
+ const langStats = new Map<string, { count: number; color?: string }>();
|
|
|
|
|
+ for (const s of coverRepos ?? []) {
|
|
|
|
|
+ const repo = ghExt(s)!.repo;
|
|
|
|
|
+ if (!repo.language) continue;
|
|
|
|
|
+ const stats = langStats.get(repo.language) ?? { count: 0, color: repo.languageColor };
|
|
|
|
|
+ stats.count += 1;
|
|
|
|
|
+ langStats.set(repo.language, stats);
|
|
|
|
|
+ }
|
|
|
|
|
+ const topLangs = [...langStats.entries()]
|
|
|
|
|
+ .sort((a, b) => b[1].count - a[1].count)
|
|
|
|
|
+ .slice(0, 6);
|
|
|
|
|
+ // Report identity signals: how many repos make up today's briefing and
|
|
|
|
|
+ // their combined star base — the two numbers a data report leads with.
|
|
|
|
|
+ const repos = (coverRepos ?? []).map((s) => ghExt(s)!.repo);
|
|
|
|
|
+ const totalStars = repos.reduce((sum, r) => sum + (r.stars ?? 0), 0);
|
|
|
|
|
+
|
|
|
|
|
+ return (
|
|
|
|
|
+ <AbsoluteFill style={{
|
|
|
|
|
+ background: `linear-gradient(150deg, ${theme.bg} 0%, ${theme.bgLight} 60%, #dbe4ee 100%)`,
|
|
|
|
|
+ }}>
|
|
|
|
|
+ {/* Fine ledger ruling — denser than the weekly's journal grid */}
|
|
|
|
|
+ <div style={{
|
|
|
|
|
+ position: "absolute", inset: 0,
|
|
|
|
|
+ backgroundImage: `
|
|
|
|
|
+ linear-gradient(rgba(30,41,59,0.045) 1px, transparent 1px),
|
|
|
|
|
+ linear-gradient(90deg, rgba(30,41,59,0.045) 1px, transparent 1px)
|
|
|
|
|
+ `,
|
|
|
|
|
+ backgroundSize: "48px 48px",
|
|
|
|
|
+ }} />
|
|
|
|
|
+ {/* Code/data motifs — the cover's tech texture */}
|
|
|
|
|
+ <CoverDecor accent={primary} />
|
|
|
|
|
+ {/* Report rules: heavy primary band top, thin accent rule bottom —
|
|
|
|
|
+ NOT the weekly's double editorial rules */}
|
|
|
|
|
+ <div style={{ position: "absolute", top: 0, left: 0, width: "100%", height: 10, background: primary }} />
|
|
|
|
|
+ <div style={{ position: "absolute", bottom: 0, left: 0, width: "100%", height: 5, background: `${theme.accent}` }} />
|
|
|
|
|
+
|
|
|
|
|
+ {/* LEFT-ALIGNED report header — the daily board's identity vs the
|
|
|
|
|
+ weekly magazine's centered masthead */}
|
|
|
|
|
+ <div style={{
|
|
|
|
|
+ position: "absolute", inset: 0, display: "flex", flexDirection: "column",
|
|
|
|
|
+ justifyContent: "center",
|
|
|
|
|
+ alignItems: "flex-start",
|
|
|
|
|
+ padding: isPortrait ? "72px 64px 150px" : "58px 96px 130px",
|
|
|
|
|
+ maxWidth: "100%",
|
|
|
|
|
+ }}>
|
|
|
|
|
+ {/* Doc-type kicker — "DAILY BRIEFING", not the weekly's "WEEKLY ISSUE" */}
|
|
|
|
|
+ <div style={{
|
|
|
|
|
+ display: "inline-flex", alignItems: "baseline", gap: 18,
|
|
|
|
|
+ marginBottom: 24,
|
|
|
|
|
+ }}>
|
|
|
|
|
+ <span style={{
|
|
|
|
|
+ fontSize: isPortrait ? 26 : 22, fontWeight: 700,
|
|
|
|
|
+ color: "#ffffff", background: primary,
|
|
|
|
|
+ padding: "8px 20px", letterSpacing: "0.3em",
|
|
|
|
|
+ fontFamily: "Noto Sans SC", textTransform: "uppercase",
|
|
|
|
|
+ }}>
|
|
|
|
|
+ Daily Briefing
|
|
|
|
|
+ </span>
|
|
|
|
|
+ {subtitle && (
|
|
|
|
|
+ <span style={{
|
|
|
|
|
+ fontSize: isPortrait ? 40 : 36, fontWeight: 600,
|
|
|
|
|
+ color: "#334155", fontFamily: "Noto Sans SC",
|
|
|
|
|
+ letterSpacing: "0.04em",
|
|
|
|
|
+ }}>
|
|
|
|
|
+ {subtitle}
|
|
|
|
|
+ </span>
|
|
|
|
|
+ )}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ {/* Masthead title — flush left with a heavy primary sidebar rule,
|
|
|
|
|
+ no underline (the weekly underlines its centered masthead) */}
|
|
|
|
|
+ <div style={{
|
|
|
|
|
+ fontSize: isPortrait ? 108 : 98, fontWeight: 800, color: "#1e293b",
|
|
|
|
|
+ fontFamily: "Noto Sans SC", lineHeight: 1.12,
|
|
|
|
|
+ letterSpacing: "-0.01em",
|
|
|
|
|
+ borderLeft: `10px solid ${primary}`,
|
|
|
|
|
+ paddingLeft: isPortrait ? 36 : 42,
|
|
|
|
|
+ }}>
|
|
|
|
|
+ {title}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ {/* Trend summary — set as the report's abstract line */}
|
|
|
|
|
+ {trendSummary && (
|
|
|
|
|
+ <div style={{
|
|
|
|
|
+ fontSize: isPortrait ? 34 : 28, fontWeight: 500, color: "#475569",
|
|
|
|
|
+ fontFamily: "Noto Sans SC", lineHeight: 1.5,
|
|
|
|
|
+ maxWidth: isPortrait ? "92%" : "72%",
|
|
|
|
|
+ marginTop: 26,
|
|
|
|
|
+ borderLeft: `3px solid ${primary}55`,
|
|
|
|
|
+ paddingLeft: isPortrait ? 22 : 24,
|
|
|
|
|
+ }}>
|
|
|
|
|
+ {trendSummary}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ )}
|
|
|
|
|
+ {/* Data strip — repo count + aggregate stars, then language tags.
|
|
|
|
|
+ Numbers-first row a report leads with; chips are SQUARE-cornered
|
|
|
|
|
+ with a top data bar (report legend), unlike the weekly's round
|
|
|
|
|
+ paper pills. */}
|
|
|
|
|
+ <div style={{
|
|
|
|
|
+ display: "flex", flexWrap: "wrap",
|
|
|
|
|
+ alignItems: "stretch",
|
|
|
|
|
+ gap: 12, marginTop: 44,
|
|
|
|
|
+ }}>
|
|
|
|
|
+ <span style={{
|
|
|
|
|
+ display: "inline-flex", alignItems: "baseline", gap: 8,
|
|
|
|
|
+ padding: "12px 22px",
|
|
|
|
|
+ background: "#ffffff",
|
|
|
|
|
+ border: `1px solid ${primary}44`,
|
|
|
|
|
+ borderTop: `3px solid ${primary}`,
|
|
|
|
|
+ fontFamily: "Noto Sans SC",
|
|
|
|
|
+ }}>
|
|
|
|
|
+ <span style={{
|
|
|
|
|
+ fontSize: isPortrait ? 38 : 32, fontWeight: 800, color: primary,
|
|
|
|
|
+ }}>{repos.length}</span>
|
|
|
|
|
+ <span style={{
|
|
|
|
|
+ fontSize: isPortrait ? 26 : 22, fontWeight: 500, color: "#64748b",
|
|
|
|
|
+ }}>个项目</span>
|
|
|
|
|
+ </span>
|
|
|
|
|
+ {totalStars > 0 && (
|
|
|
|
|
+ <span style={{
|
|
|
|
|
+ display: "inline-flex", alignItems: "baseline", gap: 8,
|
|
|
|
|
+ padding: "12px 22px",
|
|
|
|
|
+ background: "#ffffff",
|
|
|
|
|
+ border: `1px solid ${primary}44`,
|
|
|
|
|
+ borderTop: `3px solid ${primary}`,
|
|
|
|
|
+ fontFamily: "Noto Sans SC",
|
|
|
|
|
+ }}>
|
|
|
|
|
+ <span style={{
|
|
|
|
|
+ fontSize: isPortrait ? 38 : 32, fontWeight: 800, color: primary,
|
|
|
|
|
+ }}>{formatCount(totalStars)}</span>
|
|
|
|
|
+ <span style={{
|
|
|
|
|
+ fontSize: isPortrait ? 26 : 22, fontWeight: 500, color: "#64748b",
|
|
|
|
|
+ }}>总 stars</span>
|
|
|
|
|
+ </span>
|
|
|
|
|
+ )}
|
|
|
|
|
+ {topLangs.map(([lang, stats]) => (
|
|
|
|
|
+ <span key={lang} style={{
|
|
|
|
|
+ display: "inline-flex", alignItems: "center", gap: 8,
|
|
|
|
|
+ fontSize: isPortrait ? 26 : 22, fontWeight: 500, color: "#334155",
|
|
|
|
|
+ fontFamily: "Noto Sans SC",
|
|
|
|
|
+ padding: "12px 20px",
|
|
|
|
|
+ background: "rgba(255,255,255,0.75)",
|
|
|
|
|
+ border: `1px solid ${(stats.color || primary)}44`,
|
|
|
|
|
+ borderTop: `3px solid ${(stats.color || primary)}`,
|
|
|
|
|
+ }}>
|
|
|
|
|
+ <ColorDot color={stats.color || primary} size={isPortrait ? 13 : 11} />
|
|
|
|
|
+ {lang}
|
|
|
|
|
+ </span>
|
|
|
|
|
+ ))}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </AbsoluteFill>
|
|
|
|
|
+ );
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
return (
|
|
return (
|
|
|
<AbsoluteFill style={{ backgroundColor: "#0f172a" }}>
|
|
<AbsoluteFill style={{ backgroundColor: "#0f172a" }}>
|
|
|
{backgroundAsset && (
|
|
{backgroundAsset && (
|
|
@@ -529,40 +696,165 @@ const CoverScene: React.FC<{
|
|
|
|
|
|
|
|
// --- Outro Scene ---
|
|
// --- Outro Scene ---
|
|
|
|
|
|
|
|
|
|
+/** The github outro's recap chips — repo short names (board boards) or act
|
|
|
|
|
+ * titles (daily-pick), one chip per content scene. */
|
|
|
|
|
+const OutroChips: React.FC<{
|
|
|
|
|
+ entries: string[];
|
|
|
|
|
+ accent: string;
|
|
|
|
|
+ onLight?: boolean;
|
|
|
|
|
+ isPortrait: boolean;
|
|
|
|
|
+}> = ({ entries, accent, onLight, isPortrait }) => {
|
|
|
|
|
+ const frame = useCurrentFrame();
|
|
|
|
|
+ const shown = Math.min(
|
|
|
|
|
+ entries.length,
|
|
|
|
|
+ Math.floor(frame / 4) + 1
|
|
|
|
|
+ );
|
|
|
|
|
+ return (
|
|
|
|
|
+ <div style={{
|
|
|
|
|
+ display: "flex", flexWrap: "wrap",
|
|
|
|
|
+ alignItems: "center", justifyContent: "center",
|
|
|
|
|
+ gap: isPortrait ? 16 : 12,
|
|
|
|
|
+ maxWidth: isPortrait ? "92%" : "80%",
|
|
|
|
|
+ }}>
|
|
|
|
|
+ {entries.slice(0, shown).map((label, i) => (
|
|
|
|
|
+ <span key={i} style={{
|
|
|
|
|
+ display: "inline-flex", alignItems: "center",
|
|
|
|
|
+ fontSize: isPortrait ? 30 : 26, fontWeight: 500,
|
|
|
|
|
+ fontFamily: "Noto Sans SC",
|
|
|
|
|
+ color: onLight ? "#334155" : "#e2e8f0",
|
|
|
|
|
+ padding: "12px 26px", borderRadius: 999,
|
|
|
|
|
+ background: onLight ? "rgba(255,255,255,0.8)" : "rgba(255,255,255,0.06)",
|
|
|
|
|
+ border: `1px solid ${accent}55`,
|
|
|
|
|
+ }}>
|
|
|
|
|
+ {label}
|
|
|
|
|
+ </span>
|
|
|
|
|
+ ))}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ );
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
const OutroScene: React.FC<{
|
|
const OutroScene: React.FC<{
|
|
|
|
|
+ template: TemplateType;
|
|
|
text: string;
|
|
text: string;
|
|
|
cta?: string;
|
|
cta?: string;
|
|
|
|
|
+ channelName?: string;
|
|
|
|
|
+ /** Content scenes — the outro's recap chips list their repo/act labels. */
|
|
|
|
|
+ outroRepos?: RenderScene[];
|
|
|
totalFrames: number;
|
|
totalFrames: number;
|
|
|
-}> = ({ text, cta }) => {
|
|
|
|
|
|
|
+}> = ({ template, text, cta, channelName, outroRepos }) => {
|
|
|
const frame = useCurrentFrame();
|
|
const frame = useCurrentFrame();
|
|
|
- const opacity = interpolate(frame, [0, 15], [0, 1], { extrapolateRight: "clamp" });
|
|
|
|
|
|
|
+ const { width, height } = useVideoConfig();
|
|
|
|
|
+ const isPortrait = height > width;
|
|
|
|
|
+ const theme = THEMES[template];
|
|
|
|
|
+ const isGithub = template.startsWith("github");
|
|
|
|
|
+ const onLight =
|
|
|
|
|
+ template === "github-trending" ||
|
|
|
|
|
+ template === "github-weekly" ||
|
|
|
|
|
+ template === "github-weekly-recap";
|
|
|
|
|
+ const accent = theme.primary;
|
|
|
|
|
+
|
|
|
|
|
+ // Staggered entrance: the closing line rises first, chips pop in one by one,
|
|
|
|
|
+ // the CTA lands last.
|
|
|
|
|
+ const rise = (delay: number) =>
|
|
|
|
|
+ interpolate(frame, [delay, delay + 14], [36, 0], {
|
|
|
|
|
+ extrapolateLeft: "clamp", extrapolateRight: "clamp",
|
|
|
|
|
+ });
|
|
|
|
|
+ const fade = (delay: number) =>
|
|
|
|
|
+ interpolate(frame, [delay, delay + 14], [0, 1], {
|
|
|
|
|
+ extrapolateLeft: "clamp", extrapolateRight: "clamp",
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // Recap entries: repo short names for the board boards, act titles for the
|
|
|
|
|
+ // daily deep-dive (same labeling rule as ChapterToc).
|
|
|
|
|
+ const entries = (outroRepos ?? [])
|
|
|
|
|
+ .map((s) => {
|
|
|
|
|
+ const ext = ghExt(s);
|
|
|
|
|
+ if (!ext) return "";
|
|
|
|
|
+ return template === "github-daily-pick"
|
|
|
|
|
+ ? (s.title || ext.repo.name || "")
|
|
|
|
|
+ : (ext.repo.name || ext.repo.fullName || s.title || "");
|
|
|
|
|
+ })
|
|
|
|
|
+ .filter(Boolean);
|
|
|
|
|
|
|
|
return (
|
|
return (
|
|
|
- <AbsoluteFill style={{ opacity, backgroundColor: "#0f172a" }}>
|
|
|
|
|
|
|
+ <AbsoluteFill style={{
|
|
|
|
|
+ backgroundColor: onLight ? theme.bg : "#0f172a",
|
|
|
|
|
+ background: onLight
|
|
|
|
|
+ ? `linear-gradient(150deg, ${theme.bg} 0%, ${theme.bgLight} 55%, ${theme.bgLight} 100%)`
|
|
|
|
|
+ : `linear-gradient(150deg, #0f172a 0%, ${theme.bgLight} 100%)`,
|
|
|
|
|
+ }}>
|
|
|
|
|
+ {/* Close the way we opened — the cover's tech texture bookends the video */}
|
|
|
|
|
+ <CoverDecor accent={accent} onDark={!onLight} />
|
|
|
|
|
+ {/* Editorial rules matching the paper boards' covers */}
|
|
|
|
|
+ {onLight ? (
|
|
|
|
|
+ <>
|
|
|
|
|
+ <div style={{ position: "absolute", top: 0, left: 0, width: "100%", height: 8, background: accent }} />
|
|
|
|
|
+ <div style={{ position: "absolute", top: 14, left: 0, width: "100%", height: 1, background: `${accent}66` }} />
|
|
|
|
|
+ <div style={{ position: "absolute", bottom: 0, left: 0, width: "100%", height: 6, background: theme.accent }} />
|
|
|
|
|
+ <div style={{ position: "absolute", bottom: 12, left: 0, width: "100%", height: 1, background: `${theme.accent}55` }} />
|
|
|
|
|
+ </>
|
|
|
|
|
+ ) : (
|
|
|
|
|
+ <div style={{
|
|
|
|
|
+ position: "absolute", top: 0, left: 0, width: "100%", height: 6,
|
|
|
|
|
+ background: `linear-gradient(90deg, ${accent}, ${theme.primaryLight})`,
|
|
|
|
|
+ }} />
|
|
|
|
|
+ )}
|
|
|
|
|
+
|
|
|
<div style={{
|
|
<div style={{
|
|
|
position: "absolute", inset: 0, display: "flex",
|
|
position: "absolute", inset: 0, display: "flex",
|
|
|
flexDirection: "column", justifyContent: "center", alignItems: "center",
|
|
flexDirection: "column", justifyContent: "center", alignItems: "center",
|
|
|
- padding: 80, textAlign: "center",
|
|
|
|
|
|
|
+ padding: isPortrait ? "100px 64px 170px" : "80px",
|
|
|
|
|
+ textAlign: "center",
|
|
|
}}>
|
|
}}>
|
|
|
|
|
+ {/* Kicker — the show's masthead, echoes the cover */}
|
|
|
|
|
+ {isGithub && (
|
|
|
|
|
+ <div style={{
|
|
|
|
|
+ fontSize: isPortrait ? 28 : 24, fontWeight: 600,
|
|
|
|
|
+ color: onLight ? accent : theme.primaryLight,
|
|
|
|
|
+ fontFamily: "Noto Sans SC", letterSpacing: "0.42em",
|
|
|
|
|
+ textTransform: "uppercase", marginBottom: 22,
|
|
|
|
|
+ opacity: fade(0), transform: `translateY(${rise(0)}px)`,
|
|
|
|
|
+ }}>
|
|
|
|
|
+ {template === "github-trending" ? "GitHub Daily" : template === "github-weekly" ? "GitHub Weekly" : "GitHub Pick"}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ )}
|
|
|
|
|
+ {/* Closing line */}
|
|
|
<div style={{
|
|
<div style={{
|
|
|
- fontSize: 48, fontWeight: 700, color: "white",
|
|
|
|
|
- fontFamily: "Noto Sans SC", lineHeight: 1.4, marginBottom: 40,
|
|
|
|
|
- maxWidth: "80%",
|
|
|
|
|
|
|
+ fontSize: isPortrait ? 56 : 50, fontWeight: 700,
|
|
|
|
|
+ color: onLight ? "#1e293b" : "white",
|
|
|
|
|
+ fontFamily: "Noto Sans SC", lineHeight: 1.4,
|
|
|
|
|
+ maxWidth: "84%", marginBottom: isGithub ? 44 : 40,
|
|
|
|
|
+ opacity: fade(4), transform: `translateY(${rise(4)}px)`,
|
|
|
}}>
|
|
}}>
|
|
|
{text}
|
|
{text}
|
|
|
</div>
|
|
</div>
|
|
|
|
|
+ {/* Today/this week's projects — the recap chip strip */}
|
|
|
|
|
+ {entries.length > 0 && (
|
|
|
|
|
+ <OutroChips
|
|
|
|
|
+ entries={entries}
|
|
|
|
|
+ accent={accent}
|
|
|
|
|
+ onLight={onLight}
|
|
|
|
|
+ isPortrait={isPortrait}
|
|
|
|
|
+ />
|
|
|
|
|
+ )}
|
|
|
|
|
+ {/* CTA — the landing action, last in */}
|
|
|
{cta && (
|
|
{cta && (
|
|
|
<div style={{
|
|
<div style={{
|
|
|
- fontSize: 28, fontWeight: 600, color: "#fbbf24",
|
|
|
|
|
|
|
+ marginTop: 52,
|
|
|
|
|
+ fontSize: isPortrait ? 32 : 28, fontWeight: 600,
|
|
|
|
|
+ color: onLight ? "#ffffff" : "#0f172a",
|
|
|
fontFamily: "Noto Sans SC",
|
|
fontFamily: "Noto Sans SC",
|
|
|
- padding: "16px 48px",
|
|
|
|
|
- borderRadius: 12,
|
|
|
|
|
- border: "2px solid #fbbf24",
|
|
|
|
|
|
|
+ padding: "16px 52px",
|
|
|
|
|
+ borderRadius: 999,
|
|
|
|
|
+ background: onLight ? accent : theme.primaryLight,
|
|
|
|
|
+ boxShadow: onLight ? "0 8px 24px rgba(0,0,0,0.12)" : "0 8px 24px rgba(0,0,0,0.4)",
|
|
|
|
|
+ opacity: fade(16), transform: `translateY(${rise(16)}px)`,
|
|
|
}}>
|
|
}}>
|
|
|
{cta}
|
|
{cta}
|
|
|
</div>
|
|
</div>
|
|
|
)}
|
|
)}
|
|
|
</div>
|
|
</div>
|
|
|
|
|
+ <Watermark text={channelName} light={onLight} />
|
|
|
</AbsoluteFill>
|
|
</AbsoluteFill>
|
|
|
);
|
|
);
|
|
|
};
|
|
};
|
|
@@ -606,13 +898,13 @@ const ChapterToc: React.FC<{
|
|
|
// global progress bar. Lists every repo's short name; the chapter whose
|
|
// global progress bar. Lists every repo's short name; the chapter whose
|
|
|
// [startFrame, endFrame) contains the current frame is highlighted. A faint
|
|
// [startFrame, endFrame) contains the current frame is highlighted. A faint
|
|
|
// gradient strip behind the row keeps labels legible — dark strip + white
|
|
// gradient strip behind the row keeps labels legible — dark strip + white
|
|
|
- // text for github-trending, light strip + slate text for github-weekly's
|
|
|
|
|
- // paper theme.
|
|
|
|
|
|
|
+ // text for github-daily-pick (the dark deep-dive board), light strip +
|
|
|
|
|
+ // slate text for the paper boards (github-trending / github-weekly / recap).
|
|
|
const frame = useCurrentFrame();
|
|
const frame = useCurrentFrame();
|
|
|
const { width, height } = useVideoConfig();
|
|
const { width, height } = useVideoConfig();
|
|
|
const isPortrait = height > width;
|
|
const isPortrait = height > width;
|
|
|
const theme = THEMES[template];
|
|
const theme = THEMES[template];
|
|
|
- const onLight = template === "github-weekly" || template === "github-weekly-recap";
|
|
|
|
|
|
|
+ const onLight = template !== "github-daily-pick";
|
|
|
const accent = theme.primary;
|
|
const accent = theme.primary;
|
|
|
|
|
|
|
|
const chapters = layout.filter((l) => l.scene.kind === "content" && ghExt(l.scene));
|
|
const chapters = layout.filter((l) => l.scene.kind === "content" && ghExt(l.scene));
|