|
@@ -1,7 +1,6 @@
|
|
|
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";
|
|
@@ -60,8 +59,8 @@ const TEMPLATES: TemplateType[] = ["news", "knowledge", "opinion", "marketing",
|
|
|
const TemplateComposition: React.FC<RenderProps> = (props) => {
|
|
const TemplateComposition: React.FC<RenderProps> = (props) => {
|
|
|
const { fps } = useVideoConfig();
|
|
const { fps } = useVideoConfig();
|
|
|
const SceneComponent = SCENE_MAP[props.template];
|
|
const SceneComponent = SCENE_MAP[props.template];
|
|
|
- // github-trending: the cover lists every repo (name / language / today's
|
|
|
|
|
- // star gain / one-line description). Content scenes carry repo data in extension.
|
|
|
|
|
|
|
+ // github-trending: the cover aggregates the repos' languages into tag
|
|
|
|
|
+ // chips. Content scenes carry repo data in extension.
|
|
|
const coverRepos = props.scenes.filter(
|
|
const coverRepos = props.scenes.filter(
|
|
|
(s) => s.kind === "content" && ghExt(s)
|
|
(s) => s.kind === "content" && ghExt(s)
|
|
|
);
|
|
);
|
|
@@ -150,17 +149,20 @@ const TemplateComposition: React.FC<RenderProps> = (props) => {
|
|
|
|
|
|
|
|
// Faint, sparse code/data motifs on the github-trending cover background.
|
|
// Faint, sparse code/data motifs on the github-trending cover background.
|
|
|
// Low-opacity and edge-anchored so the cover stays clean — the motifs read
|
|
// 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 }) => {
|
|
|
|
|
|
|
+// 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.
|
|
|
|
|
+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: "#0f172a",
|
|
|
|
|
- opacity: 0.06,
|
|
|
|
|
|
|
+ color: onDark ? "#ffffff" : "#0f172a",
|
|
|
|
|
+ opacity: onDark ? 0.08 : 0.06,
|
|
|
letterSpacing: "0.02em",
|
|
letterSpacing: "0.02em",
|
|
|
...css,
|
|
...css,
|
|
|
});
|
|
});
|
|
|
|
|
+ const chartOpacity = onDark ? 0.16 : 0.08;
|
|
|
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>
|
|
@@ -168,11 +170,11 @@ const CoverDecor: React.FC<{ accent: string }> = ({ accent }) => {
|
|
|
<span style={tok({ bottom: 168, left: 54, fontSize: 28 })}>{"01 10 01"}</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>
|
|
<span style={tok({ bottom: 150, right: 58, fontSize: 26 })}>{"git push"}</span>
|
|
|
{/* Faint data line chart */}
|
|
{/* Faint data line chart */}
|
|
|
- <svg width="180" height="84" viewBox="0 0 180 84" style={{ position: "absolute", top: 64, right: 168, opacity: 0.08 }}>
|
|
|
|
|
|
|
+ <svg width="180" height="84" viewBox="0 0 180 84" style={{ position: "absolute", top: 64, right: 168, opacity: chartOpacity }}>
|
|
|
<polyline points="0,66 34,50 68,56 102,28 136,34 180,8" fill="none" stroke={accent} strokeWidth="3" strokeLinecap="round" strokeLinejoin="round" />
|
|
<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>
|
|
</svg>
|
|
|
{/* Faint data bar cluster */}
|
|
{/* Faint data bar cluster */}
|
|
|
- <div style={{ position: "absolute", bottom: 206, left: 80, display: "flex", alignItems: "flex-end", gap: 7, opacity: 0.08 }}>
|
|
|
|
|
|
|
+ <div style={{ position: "absolute", bottom: 206, left: 80, display: "flex", alignItems: "flex-end", gap: 7, opacity: chartOpacity }}>
|
|
|
<div style={{ width: 12, height: 32, background: accent, borderRadius: 3 }} />
|
|
<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: 58, background: accent, borderRadius: 3 }} />
|
|
|
<div style={{ width: 12, height: 24, background: accent, borderRadius: 3 }} />
|
|
<div style={{ width: 12, height: 24, background: accent, borderRadius: 3 }} />
|
|
@@ -198,21 +200,46 @@ const CoverScene: React.FC<{
|
|
|
const { width, height } = useVideoConfig();
|
|
const { width, height } = useVideoConfig();
|
|
|
const isPortrait = height > width;
|
|
const isPortrait = height > width;
|
|
|
|
|
|
|
|
- // 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.
|
|
|
|
|
|
|
+ // github-trending 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), and the trend summary line. Light gradient + CoverDecor is
|
|
|
|
|
+ // the no-image fallback (e.g. studio demo props).
|
|
|
if (isGithubTrending) {
|
|
if (isGithubTrending) {
|
|
|
- const topRepos = [...(coverRepos ?? [])]
|
|
|
|
|
- .sort((a, b) => (ghExt(b)!.repo.todayStars ?? 0) - (ghExt(a)!.repo.todayStars ?? 0))
|
|
|
|
|
- .slice(0, 6);
|
|
|
|
|
const primary = THEMES[template].primary;
|
|
const primary = THEMES[template].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);
|
|
|
|
|
|
|
|
return (
|
|
return (
|
|
|
<AbsoluteFill style={{
|
|
<AbsoluteFill style={{
|
|
|
background: "linear-gradient(135deg, #f8fafc 0%, #eef2f7 55%, #e2e8f0 100%)",
|
|
background: "linear-gradient(135deg, #f8fafc 0%, #eef2f7 55%, #e2e8f0 100%)",
|
|
|
}}>
|
|
}}>
|
|
|
- <CoverDecor accent={accent} />
|
|
|
|
|
|
|
+ {backgroundAsset ? (
|
|
|
|
|
+ <>
|
|
|
|
|
+ <Img
|
|
|
|
|
+ src={staticFile(backgroundAsset.filename)}
|
|
|
|
|
+ style={{ position: "absolute", width: "100%", height: "100%", objectFit: "cover" }}
|
|
|
|
|
+ />
|
|
|
|
|
+ <div style={{
|
|
|
|
|
+ position: "absolute", inset: 0,
|
|
|
|
|
+ background: "linear-gradient(to bottom, rgba(2,6,23,0.40), rgba(2,6,23,0.70))",
|
|
|
|
|
+ }} />
|
|
|
|
|
+ </>
|
|
|
|
|
+ ) : undefined}
|
|
|
|
|
+ {/* Code/data motifs over both backdrops (light gradient or scrimmed image) */}
|
|
|
|
|
+ <CoverDecor accent={accent} onDark={!!backgroundAsset} />
|
|
|
{/* Top accent line */}
|
|
{/* Top accent line */}
|
|
|
<div style={{
|
|
<div style={{
|
|
|
position: "absolute", top: 0, left: 0, width: "100%", height: 6,
|
|
position: "absolute", top: 0, left: 0, width: "100%", height: 6,
|
|
@@ -221,144 +248,75 @@ const CoverScene: React.FC<{
|
|
|
<div style={{
|
|
<div style={{
|
|
|
position: "absolute", inset: 0, display: "flex", flexDirection: "column",
|
|
position: "absolute", inset: 0, display: "flex", flexDirection: "column",
|
|
|
alignItems: "center",
|
|
alignItems: "center",
|
|
|
- justifyContent: isPortrait ? "center" : "flex-start",
|
|
|
|
|
- padding: isPortrait ? "72px 64px 130px" : "58px 80px 112px",
|
|
|
|
|
|
|
+ justifyContent: "center",
|
|
|
|
|
+ padding: isPortrait ? "72px 64px 150px" : "58px 80px 130px",
|
|
|
}}>
|
|
}}>
|
|
|
- {/* DARK title card — centered, near the top (contrast over light bg) */}
|
|
|
|
|
|
|
+ {/* DARK title card — centered, title + date + tags + summary */}
|
|
|
<div style={{
|
|
<div style={{
|
|
|
- display: "flex", flexDirection: "column", alignItems: "center", gap: 14,
|
|
|
|
|
- padding: isPortrait ? "38px 76px" : "32px 88px",
|
|
|
|
|
|
|
+ display: "flex", flexDirection: "column", alignItems: "center", gap: 20,
|
|
|
|
|
+ padding: isPortrait ? "64px 96px" : "56px 110px",
|
|
|
borderRadius: 26,
|
|
borderRadius: 26,
|
|
|
background: "linear-gradient(135deg, #0f172a 0%, #1e293b 100%)",
|
|
background: "linear-gradient(135deg, #0f172a 0%, #1e293b 100%)",
|
|
|
border: `1.5px solid ${primary}55`,
|
|
border: `1.5px solid ${primary}55`,
|
|
|
- boxShadow: "0 18px 44px rgba(15,23,42,0.22)",
|
|
|
|
|
- marginBottom: isPortrait ? 44 : 38,
|
|
|
|
|
- maxWidth: isPortrait ? "84%" : "74%",
|
|
|
|
|
|
|
+ boxShadow: "0 24px 64px rgba(0,0,0,0.55), 0 4px 16px rgba(0,0,0,0.4)",
|
|
|
|
|
+ maxWidth: isPortrait ? "90%" : "80%",
|
|
|
textAlign: "center",
|
|
textAlign: "center",
|
|
|
}}>
|
|
}}>
|
|
|
|
|
+ {/* Masthead title */}
|
|
|
<div style={{
|
|
<div style={{
|
|
|
- fontSize: isPortrait ? 72 : 66, fontWeight: 800, color: "#ffffff",
|
|
|
|
|
- fontFamily: "Noto Sans SC", lineHeight: 1.15, letterSpacing: "-0.02em",
|
|
|
|
|
|
|
+ fontSize: isPortrait ? 108 : 96, fontWeight: 800, color: "#ffffff",
|
|
|
|
|
+ fontFamily: "Noto Sans SC", lineHeight: 1.12, letterSpacing: "-0.02em",
|
|
|
}}>
|
|
}}>
|
|
|
{title}
|
|
{title}
|
|
|
</div>
|
|
</div>
|
|
|
- <div style={{
|
|
|
|
|
- display: "flex", alignItems: "center", gap: 14, flexWrap: "wrap",
|
|
|
|
|
- justifyContent: "center",
|
|
|
|
|
- }}>
|
|
|
|
|
- {subtitle && (
|
|
|
|
|
- <span style={{
|
|
|
|
|
- fontSize: isPortrait ? 30 : 26, fontWeight: 600, color: accent,
|
|
|
|
|
- fontFamily: "Noto Sans SC",
|
|
|
|
|
- }}>
|
|
|
|
|
- {subtitle}
|
|
|
|
|
- </span>
|
|
|
|
|
- )}
|
|
|
|
|
- {trendSummary && (
|
|
|
|
|
- <span style={{
|
|
|
|
|
- fontSize: isPortrait ? 28 : 24, fontWeight: 500, color: "#cbd5e1",
|
|
|
|
|
- fontFamily: "Noto Sans SC",
|
|
|
|
|
|
|
+ {/* Date */}
|
|
|
|
|
+ {subtitle && (
|
|
|
|
|
+ <div style={{
|
|
|
|
|
+ fontSize: isPortrait ? 36 : 32, fontWeight: 600, color: accent,
|
|
|
|
|
+ fontFamily: "Noto Sans SC",
|
|
|
|
|
+ }}>
|
|
|
|
|
+ {subtitle}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ )}
|
|
|
|
|
+ {/* Main tags — languages aggregated across today's repos */}
|
|
|
|
|
+ {topLangs.length > 0 && (
|
|
|
|
|
+ <div style={{
|
|
|
|
|
+ display: "flex", flexWrap: "wrap",
|
|
|
|
|
+ alignItems: "center", justifyContent: "center",
|
|
|
|
|
+ gap: 14, marginTop: 6,
|
|
|
|
|
+ }}>
|
|
|
|
|
+ {topLangs.map(([lang, stats]) => (
|
|
|
|
|
+ <span key={lang} style={{
|
|
|
|
|
+ display: "inline-flex", alignItems: "center", gap: 9,
|
|
|
|
|
+ fontSize: isPortrait ? 28 : 24, fontWeight: 500, color: "#ffffff",
|
|
|
|
|
+ fontFamily: "Noto Sans SC",
|
|
|
|
|
+ padding: "10px 24px", borderRadius: 999,
|
|
|
|
|
+ background: "rgba(255,255,255,0.08)",
|
|
|
|
|
+ border: `1px solid ${(stats.color || accent)}66`,
|
|
|
|
|
+ }}>
|
|
|
|
|
+ <ColorDot color={stats.color || accent} size={isPortrait ? 14 : 12} />
|
|
|
|
|
+ {lang}
|
|
|
|
|
+ </span>
|
|
|
|
|
+ ))}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ )}
|
|
|
|
|
+ {/* Trend summary */}
|
|
|
|
|
+ {trendSummary && (
|
|
|
|
|
+ <>
|
|
|
|
|
+ <div style={{
|
|
|
|
|
+ width: "56%", height: 1, marginTop: 10,
|
|
|
|
|
+ background: "rgba(255,255,255,0.16)",
|
|
|
|
|
+ }} />
|
|
|
|
|
+ <div style={{
|
|
|
|
|
+ fontSize: isPortrait ? 34 : 29, fontWeight: 500, color: "#cbd5e1",
|
|
|
|
|
+ fontFamily: "Noto Sans SC", lineHeight: 1.5,
|
|
|
|
|
+ maxWidth: "92%",
|
|
|
}}>
|
|
}}>
|
|
|
- · {trendSummary}
|
|
|
|
|
- </span>
|
|
|
|
|
- )}
|
|
|
|
|
- </div>
|
|
|
|
|
|
|
+ {trendSummary}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </>
|
|
|
|
|
+ )}
|
|
|
</div>
|
|
</div>
|
|
|
-
|
|
|
|
|
- {/* TOP 6 repos by today's star gain (3 per row → wraps to a grid) */}
|
|
|
|
|
- {topRepos.length > 0 && (
|
|
|
|
|
- <div style={{
|
|
|
|
|
- display: "flex", flexWrap: "wrap",
|
|
|
|
|
- gap: 24,
|
|
|
|
|
- width: "100%",
|
|
|
|
|
- maxWidth: isPortrait ? "90%" : "94%",
|
|
|
|
|
- }}>
|
|
|
|
|
- {topRepos.map((s, i) => {
|
|
|
|
|
- const g = ghExt(s)!;
|
|
|
|
|
- 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>
|
|
</div>
|
|
|
</AbsoluteFill>
|
|
</AbsoluteFill>
|
|
|
);
|
|
);
|