gitreel 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +78 -0
- package/assets/music/drive.wav +0 -0
- package/assets/sfx/pop.wav +0 -0
- package/assets/sfx/sting.wav +0 -0
- package/assets/sfx/success.wav +0 -0
- package/assets/sfx/thud.wav +0 -0
- package/assets/sfx/tick.wav +0 -0
- package/assets/sfx/whoosh.wav +0 -0
- package/dist/cli/bundling.d.ts +3 -0
- package/dist/cli/bundling.js +76 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +145 -0
- package/dist/cli/narrate.d.ts +1 -0
- package/dist/cli/narrate.js +41 -0
- package/dist/cli/workspace.d.ts +7 -0
- package/dist/cli/workspace.js +56 -0
- package/dist/engine/code/CodeBlock.d.ts +16 -0
- package/dist/engine/code/CodeBlock.js +41 -0
- package/dist/engine/code/highlight.d.ts +6 -0
- package/dist/engine/code/highlight.js +30 -0
- package/dist/engine/episode-definition.d.ts +8 -0
- package/dist/engine/episode-definition.js +1 -0
- package/dist/engine/index.d.ts +16 -0
- package/dist/engine/index.js +15 -0
- package/dist/engine/scenes/BigText.d.ts +7 -0
- package/dist/engine/scenes/BigText.js +18 -0
- package/dist/engine/scenes/CalloutOverlay.d.ts +11 -0
- package/dist/engine/scenes/CalloutOverlay.js +34 -0
- package/dist/engine/scenes/CodeScene.d.ts +33 -0
- package/dist/engine/scenes/CodeScene.js +41 -0
- package/dist/engine/scenes/DiagramScene.d.ts +31 -0
- package/dist/engine/scenes/DiagramScene.js +147 -0
- package/dist/engine/scenes/DiffMorph.d.ts +12 -0
- package/dist/engine/scenes/DiffMorph.js +69 -0
- package/dist/engine/scenes/FileTreeScene.d.ts +23 -0
- package/dist/engine/scenes/FileTreeScene.js +59 -0
- package/dist/engine/scenes/MontageBeat.d.ts +13 -0
- package/dist/engine/scenes/MontageBeat.js +24 -0
- package/dist/engine/scenes/SceneShell.d.ts +19 -0
- package/dist/engine/scenes/SceneShell.js +96 -0
- package/dist/engine/scenes/TerminalScene.d.ts +10 -0
- package/dist/engine/scenes/TerminalScene.js +32 -0
- package/dist/engine/scenes/TitleCard.d.ts +10 -0
- package/dist/engine/scenes/TitleCard.js +34 -0
- package/dist/engine/scenes/VerdictCard.d.ts +13 -0
- package/dist/engine/scenes/VerdictCard.js +44 -0
- package/dist/engine/theme.d.ts +20 -0
- package/dist/engine/theme.js +27 -0
- package/dist/engine/timeline.d.ts +25 -0
- package/dist/engine/timeline.js +27 -0
- package/package.json +47 -0
- package/skills/gitreel/SKILL.md +99 -0
- package/templates/episode/episode.tsx +32 -0
- package/templates/episode/narration.json +8 -0
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { Audio, Sequence, spring, staticFile, useCurrentFrame, useVideoConfig } from "remotion";
|
|
3
|
+
import { FPS, severityColor, severityLabel, theme } from "../theme";
|
|
4
|
+
export const CalloutOverlay = ({ finding }) => {
|
|
5
|
+
const frame = useCurrentFrame();
|
|
6
|
+
const { fps } = useVideoConfig();
|
|
7
|
+
const startFrame = Math.round(finding.atSecond * FPS);
|
|
8
|
+
const slide = spring({ frame: frame - startFrame, fps, config: { damping: 15, stiffness: 170, mass: 0.6 } });
|
|
9
|
+
if (frame < startFrame)
|
|
10
|
+
return null;
|
|
11
|
+
const color = severityColor[finding.severity];
|
|
12
|
+
return (_jsxs(_Fragment, { children: [_jsx(Sequence, { from: startFrame, name: "finding-sting", children: _jsx(Audio, { src: staticFile("sfx/sting.wav"), volume: 0.5 }) }), _jsxs("div", { style: {
|
|
13
|
+
position: "absolute",
|
|
14
|
+
right: 72,
|
|
15
|
+
bottom: 64,
|
|
16
|
+
maxWidth: 760,
|
|
17
|
+
backgroundColor: theme.bgPanel,
|
|
18
|
+
border: `2px solid ${color}`,
|
|
19
|
+
borderLeft: `12px solid ${color}`,
|
|
20
|
+
borderRadius: 16,
|
|
21
|
+
padding: "26px 32px",
|
|
22
|
+
boxShadow: `0 24px 70px rgba(0,0,0,0.6), 0 0 40px ${color}22`,
|
|
23
|
+
transform: `translateY(${(1 - slide) * 120}px) rotate(${(1 - slide) * 2}deg)`,
|
|
24
|
+
opacity: slide,
|
|
25
|
+
}, children: [_jsxs("div", { style: {
|
|
26
|
+
fontFamily: theme.fontDisplay,
|
|
27
|
+
fontWeight: 800,
|
|
28
|
+
fontSize: 24,
|
|
29
|
+
letterSpacing: 3,
|
|
30
|
+
color,
|
|
31
|
+
marginBottom: 10,
|
|
32
|
+
}, children: [severityIcon(finding.severity), " ", severityLabel[finding.severity]] }), _jsx("div", { style: { fontFamily: theme.fontDisplay, fontWeight: 600, fontSize: 32, color: theme.text }, children: finding.title }), finding.detail && (_jsx("div", { style: { fontFamily: theme.fontDisplay, fontSize: 26, color: theme.textDim, marginTop: 10 }, children: finding.detail }))] })] }));
|
|
33
|
+
};
|
|
34
|
+
const severityIcon = (severity) => severity === "blocking" ? "🛑" : severity === "warn" ? "⚠️" : "🔍";
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { type LineRange } from "../code/CodeBlock";
|
|
3
|
+
import type { CodeLang } from "../code/highlight";
|
|
4
|
+
import { type Finding } from "./CalloutOverlay";
|
|
5
|
+
export type SpotlightBeat = {
|
|
6
|
+
readonly atSecond: number;
|
|
7
|
+
readonly lines: readonly LineRange[];
|
|
8
|
+
readonly label?: string;
|
|
9
|
+
};
|
|
10
|
+
export type TreeLine = {
|
|
11
|
+
readonly text: string;
|
|
12
|
+
readonly depth: number;
|
|
13
|
+
readonly highlight?: boolean;
|
|
14
|
+
readonly note?: string;
|
|
15
|
+
};
|
|
16
|
+
export declare const CodeScene: React.FC<{
|
|
17
|
+
kicker?: string;
|
|
18
|
+
title?: string;
|
|
19
|
+
file?: string;
|
|
20
|
+
badge?: string;
|
|
21
|
+
badgeColor?: string;
|
|
22
|
+
contextTag?: boolean;
|
|
23
|
+
code: string;
|
|
24
|
+
lang?: CodeLang;
|
|
25
|
+
fontSize?: number;
|
|
26
|
+
startLineNumber?: number;
|
|
27
|
+
showLineNumbers?: boolean;
|
|
28
|
+
spotlights?: readonly SpotlightBeat[];
|
|
29
|
+
finding?: Finding;
|
|
30
|
+
highlightColor?: string;
|
|
31
|
+
tree?: readonly TreeLine[];
|
|
32
|
+
treeLabel?: string;
|
|
33
|
+
}>;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useCurrentFrame } from "remotion";
|
|
3
|
+
import { CodeBlock } from "../code/CodeBlock";
|
|
4
|
+
import { FPS, theme } from "../theme";
|
|
5
|
+
import { CalloutOverlay } from "./CalloutOverlay";
|
|
6
|
+
import { FileChip, Panel, SceneShell } from "./SceneShell";
|
|
7
|
+
const TreePanel = ({ label, lines }) => (_jsxs("div", { style: {
|
|
8
|
+
backgroundColor: theme.bgPanel,
|
|
9
|
+
border: `1px solid ${theme.stroke}`,
|
|
10
|
+
borderRadius: 16,
|
|
11
|
+
padding: "26px 30px",
|
|
12
|
+
flexShrink: 0,
|
|
13
|
+
}, children: [_jsx("div", { style: {
|
|
14
|
+
fontFamily: theme.fontDisplay,
|
|
15
|
+
fontWeight: 600,
|
|
16
|
+
fontSize: 21,
|
|
17
|
+
letterSpacing: 2,
|
|
18
|
+
textTransform: "uppercase",
|
|
19
|
+
color: theme.textDim,
|
|
20
|
+
marginBottom: 16,
|
|
21
|
+
}, children: label }), lines.map((line, i) => (_jsxs("div", { style: {
|
|
22
|
+
fontFamily: theme.fontMono,
|
|
23
|
+
fontSize: 23,
|
|
24
|
+
lineHeight: 1.7,
|
|
25
|
+
paddingLeft: line.depth * 26,
|
|
26
|
+
color: line.highlight ? theme.accent2 : theme.textDim,
|
|
27
|
+
fontWeight: line.highlight ? 700 : 400,
|
|
28
|
+
whiteSpace: "nowrap",
|
|
29
|
+
}, children: [line.depth > 0 && _jsx("span", { style: { opacity: 0.5 }, children: "\u2514\u2500 " }), line.text, line.highlight && _jsx("span", { children: " \u2190" }), line.note && (_jsx("span", { style: { fontFamily: theme.fontDisplay, fontSize: 19, color: theme.amber, marginLeft: 12 }, children: line.note }))] }, i)))] }));
|
|
30
|
+
export const CodeScene = ({ kicker, title, file, badge, badgeColor, contextTag, code, lang, fontSize, startLineNumber, showLineNumbers, spotlights = [], finding, highlightColor, tree, treeLabel = "where this lives", }) => {
|
|
31
|
+
const frame = useCurrentFrame();
|
|
32
|
+
const second = frame / FPS;
|
|
33
|
+
const active = [...spotlights].reverse().find((beat) => second >= beat.atSecond);
|
|
34
|
+
return (_jsxs(SceneShell, { kicker: kicker, title: title, contextTag: contextTag, center: true, children: [_jsxs("div", { style: { display: "flex", gap: 36, alignItems: "center", maxWidth: "100%" }, children: [tree && _jsx(TreePanel, { label: treeLabel, lines: tree }), _jsxs(Panel, { children: [file && _jsx(FileChip, { path: file, badge: badge, badgeColor: badgeColor }), _jsx(CodeBlock, { code: code, lang: lang, fontSize: fontSize, startLineNumber: startLineNumber, showLineNumbers: showLineNumbers, spotlight: active?.lines ?? [], highlightColor: highlightColor }), active?.label && (_jsxs("div", { style: {
|
|
35
|
+
marginTop: 24,
|
|
36
|
+
fontFamily: theme.fontDisplay,
|
|
37
|
+
fontWeight: 600,
|
|
38
|
+
fontSize: 28,
|
|
39
|
+
color: theme.accent2,
|
|
40
|
+
}, children: ["\u2191 ", active.label] }))] })] }), finding && _jsx(CalloutOverlay, { finding: finding })] }));
|
|
41
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
export type DiagramNode = {
|
|
3
|
+
readonly id: string;
|
|
4
|
+
readonly label: string;
|
|
5
|
+
readonly sub?: string;
|
|
6
|
+
readonly icon?: string;
|
|
7
|
+
readonly x: number;
|
|
8
|
+
readonly y: number;
|
|
9
|
+
readonly w?: number;
|
|
10
|
+
readonly h?: number;
|
|
11
|
+
readonly kind?: "existing" | "new" | "focus";
|
|
12
|
+
readonly badge?: string;
|
|
13
|
+
readonly atSecond?: number;
|
|
14
|
+
};
|
|
15
|
+
export type ArrowSide = "top" | "bottom" | "left" | "right";
|
|
16
|
+
export type DiagramArrow = {
|
|
17
|
+
readonly from: string;
|
|
18
|
+
readonly to: string;
|
|
19
|
+
readonly label?: string;
|
|
20
|
+
readonly atSecond?: number;
|
|
21
|
+
readonly kind?: "existing" | "new";
|
|
22
|
+
readonly fromSide?: ArrowSide;
|
|
23
|
+
readonly toSide?: ArrowSide;
|
|
24
|
+
};
|
|
25
|
+
export declare const DiagramScene: React.FC<{
|
|
26
|
+
kicker?: string;
|
|
27
|
+
title?: string;
|
|
28
|
+
nodes: readonly DiagramNode[];
|
|
29
|
+
arrows: readonly DiagramArrow[];
|
|
30
|
+
legend?: boolean;
|
|
31
|
+
}>;
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { interpolate, spring, useCurrentFrame, useVideoConfig } from "remotion";
|
|
3
|
+
import { FPS, theme } from "../theme";
|
|
4
|
+
import { SceneShell } from "./SceneShell";
|
|
5
|
+
const DEFAULT_W = 400;
|
|
6
|
+
const DEFAULT_H = 150;
|
|
7
|
+
export const DiagramScene = ({ kicker, title, nodes, arrows, legend = true }) => {
|
|
8
|
+
const frame = useCurrentFrame();
|
|
9
|
+
const { fps } = useVideoConfig();
|
|
10
|
+
const byId = new Map(nodes.map((node) => [node.id, node]));
|
|
11
|
+
return (_jsx(SceneShell, { kicker: kicker, title: title, children: _jsxs("div", { style: { position: "relative", flex: 1 }, children: [_jsxs("svg", { style: { position: "absolute", inset: 0, width: "100%", height: "100%", overflow: "visible" }, children: [_jsx("defs", { children: _jsxs("filter", { id: "arrow-glow", x: "-50%", y: "-50%", width: "200%", height: "200%", children: [_jsx("feGaussianBlur", { stdDeviation: "7", result: "blur" }), _jsxs("feMerge", { children: [_jsx("feMergeNode", { in: "blur" }), _jsx("feMergeNode", { in: "SourceGraphic" })] })] }) }), arrows.map((arrow, i) => {
|
|
12
|
+
const from = byId.get(arrow.from);
|
|
13
|
+
const to = byId.get(arrow.to);
|
|
14
|
+
if (!from || !to)
|
|
15
|
+
return null;
|
|
16
|
+
const fromSide = arrow.fromSide ?? autoSide(from, to);
|
|
17
|
+
const toSide = arrow.toSide ?? autoSide(to, from);
|
|
18
|
+
const start = sideAnchor(from, fromSide);
|
|
19
|
+
const end = sideAnchor(to, toSide);
|
|
20
|
+
const reach = Math.min(170, Math.hypot(end.x - start.x, end.y - start.y) * 0.45);
|
|
21
|
+
const c1 = offsetAlongNormal(start, fromSide, reach);
|
|
22
|
+
const c2 = offsetAlongNormal(end, toSide, reach);
|
|
23
|
+
const path = `M ${start.x} ${start.y} C ${c1.x} ${c1.y} ${c2.x} ${c2.y} ${end.x} ${end.y}`;
|
|
24
|
+
const length = approximateCubicLength(start, c1, c2, end);
|
|
25
|
+
const startFrame = (arrow.atSecond ?? 0) * FPS;
|
|
26
|
+
const drawn = interpolate(frame, [startFrame, startFrame + 18], [0, 1], {
|
|
27
|
+
extrapolateLeft: "clamp",
|
|
28
|
+
extrapolateRight: "clamp",
|
|
29
|
+
});
|
|
30
|
+
if (drawn === 0)
|
|
31
|
+
return null;
|
|
32
|
+
const dim = arrow.kind === "existing";
|
|
33
|
+
const color = dim ? "#4a5268" : theme.accent2;
|
|
34
|
+
const tip = pointOnCubic(start, c1, c2, end, Math.min(1, drawn));
|
|
35
|
+
const beforeTip = pointOnCubic(start, c1, c2, end, Math.max(0, drawn - 0.04));
|
|
36
|
+
const angle = Math.atan2(tip.y - beforeTip.y, tip.x - beforeTip.x) * (180 / Math.PI);
|
|
37
|
+
const mid = pointOnCubic(start, c1, c2, end, 0.5);
|
|
38
|
+
return (_jsxs("g", { children: [_jsx("path", { d: path, fill: "none", stroke: color, strokeWidth: dim ? 4 : 6, strokeLinecap: "round", strokeDasharray: length, strokeDashoffset: length * (1 - drawn), filter: dim ? undefined : "url(#arrow-glow)", opacity: dim ? 0.8 : 1 }), _jsx("g", { transform: `translate(${tip.x}, ${tip.y}) rotate(${angle})`, children: _jsx("polygon", { points: "0,0 -22,-10 -22,10", fill: color, filter: dim ? undefined : "url(#arrow-glow)" }) }), arrow.label && drawn > 0.7 && (_jsx("text", { x: mid.x, y: mid.y - 16, fill: theme.textDim, fontFamily: theme.fontMono, fontSize: 24, textAnchor: "middle", children: arrow.label }))] }, i));
|
|
39
|
+
})] }), nodes.map((node) => {
|
|
40
|
+
const startFrame = (node.atSecond ?? 0) * FPS;
|
|
41
|
+
const pop = spring({ frame: frame - startFrame, fps, config: { damping: 14, stiffness: 160, mass: 0.6 } });
|
|
42
|
+
const isNew = node.kind === "new" || node.kind === "focus";
|
|
43
|
+
const borderColor = node.kind === "focus" ? theme.accent : isNew ? theme.green : "#39415a";
|
|
44
|
+
const w = node.w ?? DEFAULT_W;
|
|
45
|
+
const h = node.h ?? DEFAULT_H;
|
|
46
|
+
return (_jsxs("div", { style: {
|
|
47
|
+
position: "absolute",
|
|
48
|
+
left: node.x - w / 2,
|
|
49
|
+
top: node.y - h / 2,
|
|
50
|
+
width: w,
|
|
51
|
+
height: h,
|
|
52
|
+
background: isNew
|
|
53
|
+
? `linear-gradient(160deg, ${theme.bgPanel} 0%, #1a1430 100%)`
|
|
54
|
+
: `linear-gradient(160deg, ${theme.bgPanel} 0%, #141927 100%)`,
|
|
55
|
+
border: `3px solid ${borderColor}`,
|
|
56
|
+
borderRadius: 20,
|
|
57
|
+
display: "flex",
|
|
58
|
+
alignItems: "center",
|
|
59
|
+
justifyContent: "center",
|
|
60
|
+
gap: 20,
|
|
61
|
+
transform: `scale(${pop})`,
|
|
62
|
+
opacity: pop * (isNew ? 1 : 0.78),
|
|
63
|
+
boxShadow: isNew ? `0 0 60px ${borderColor}40` : "0 16px 40px rgba(0,0,0,0.4)",
|
|
64
|
+
}, children: [node.icon && _jsx("span", { style: { fontSize: 46 }, children: node.icon }), _jsxs("div", { style: { display: "flex", flexDirection: "column", gap: 6, alignItems: "flex-start" }, children: [_jsx("div", { style: {
|
|
65
|
+
fontFamily: theme.fontMono,
|
|
66
|
+
fontSize: 33,
|
|
67
|
+
fontWeight: 700,
|
|
68
|
+
color: isNew ? theme.text : "#aab2c5",
|
|
69
|
+
}, children: node.label }), node.sub && (_jsx("div", { style: { fontFamily: theme.fontDisplay, fontSize: 23, color: theme.textDim }, children: node.sub }))] }), (node.kind === "new" || (node.kind === "focus" && node.badge)) && (_jsx("div", { style: {
|
|
70
|
+
position: "absolute",
|
|
71
|
+
top: -18,
|
|
72
|
+
right: -16,
|
|
73
|
+
fontFamily: theme.fontDisplay,
|
|
74
|
+
fontWeight: 800,
|
|
75
|
+
fontSize: 19,
|
|
76
|
+
letterSpacing: 2,
|
|
77
|
+
color: theme.bg,
|
|
78
|
+
backgroundColor: node.kind === "focus" ? theme.accent : theme.green,
|
|
79
|
+
borderRadius: 999,
|
|
80
|
+
padding: "6px 16px",
|
|
81
|
+
boxShadow: `0 0 30px ${node.kind === "focus" ? theme.accent : theme.green}66`,
|
|
82
|
+
}, children: node.kind === "focus" ? node.badge : "NEW" }))] }, node.id));
|
|
83
|
+
}), legend && (_jsxs("div", { style: {
|
|
84
|
+
position: "absolute",
|
|
85
|
+
bottom: 0,
|
|
86
|
+
right: 0,
|
|
87
|
+
display: "flex",
|
|
88
|
+
gap: 32,
|
|
89
|
+
alignItems: "center",
|
|
90
|
+
fontFamily: theme.fontDisplay,
|
|
91
|
+
fontSize: 24,
|
|
92
|
+
color: theme.textDim,
|
|
93
|
+
backgroundColor: `${theme.bgPanel}cc`,
|
|
94
|
+
border: `1px solid ${theme.stroke}`,
|
|
95
|
+
borderRadius: 12,
|
|
96
|
+
padding: "12px 24px",
|
|
97
|
+
}, children: [_jsxs("span", { style: { display: "flex", alignItems: "center", gap: 12 }, children: [_jsx("span", { style: { width: 22, height: 22, borderRadius: 6, border: "3px solid #39415a", opacity: 0.78 } }), "already existed"] }), _jsxs("span", { style: { display: "flex", alignItems: "center", gap: 12 }, children: [_jsx("span", { style: { width: 22, height: 22, borderRadius: 6, border: `3px solid ${theme.green}`, boxShadow: `0 0 14px ${theme.green}66` } }), "this PR"] })] }))] }) }));
|
|
98
|
+
};
|
|
99
|
+
function autoSide(node, towards) {
|
|
100
|
+
const dx = towards.x - node.x;
|
|
101
|
+
const dy = towards.y - node.y;
|
|
102
|
+
if (Math.abs(dx) * ((node.h ?? DEFAULT_H) / 2) > Math.abs(dy) * ((node.w ?? DEFAULT_W) / 2)) {
|
|
103
|
+
return dx > 0 ? "right" : "left";
|
|
104
|
+
}
|
|
105
|
+
return dy > 0 ? "bottom" : "top";
|
|
106
|
+
}
|
|
107
|
+
function sideAnchor(node, side) {
|
|
108
|
+
const w = (node.w ?? DEFAULT_W) / 2;
|
|
109
|
+
const h = (node.h ?? DEFAULT_H) / 2;
|
|
110
|
+
if (side === "left")
|
|
111
|
+
return { x: node.x - w - 8, y: node.y };
|
|
112
|
+
if (side === "right")
|
|
113
|
+
return { x: node.x + w + 8, y: node.y };
|
|
114
|
+
if (side === "top")
|
|
115
|
+
return { x: node.x, y: node.y - h - 8 };
|
|
116
|
+
return { x: node.x, y: node.y + h + 8 };
|
|
117
|
+
}
|
|
118
|
+
function offsetAlongNormal(point, side, distance) {
|
|
119
|
+
if (side === "left")
|
|
120
|
+
return { x: point.x - distance, y: point.y };
|
|
121
|
+
if (side === "right")
|
|
122
|
+
return { x: point.x + distance, y: point.y };
|
|
123
|
+
if (side === "top")
|
|
124
|
+
return { x: point.x, y: point.y - distance };
|
|
125
|
+
return { x: point.x, y: point.y + distance };
|
|
126
|
+
}
|
|
127
|
+
function pointOnCubic(start, c1, c2, end, t) {
|
|
128
|
+
const inverse = 1 - t;
|
|
129
|
+
const a = inverse * inverse * inverse;
|
|
130
|
+
const b = 3 * inverse * inverse * t;
|
|
131
|
+
const c = 3 * inverse * t * t;
|
|
132
|
+
const d = t * t * t;
|
|
133
|
+
return {
|
|
134
|
+
x: a * start.x + b * c1.x + c * c2.x + d * end.x,
|
|
135
|
+
y: a * start.y + b * c1.y + c * c2.y + d * end.y,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
function approximateCubicLength(start, c1, c2, end) {
|
|
139
|
+
let length = 0;
|
|
140
|
+
let previous = start;
|
|
141
|
+
for (let i = 1; i <= 20; i++) {
|
|
142
|
+
const point = pointOnCubic(start, c1, c2, end, i / 20);
|
|
143
|
+
length += Math.hypot(point.x - previous.x, point.y - previous.y);
|
|
144
|
+
previous = point;
|
|
145
|
+
}
|
|
146
|
+
return length;
|
|
147
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { type CodeLang } from "../code/highlight";
|
|
3
|
+
export declare const DiffMorph: React.FC<{
|
|
4
|
+
kicker?: string;
|
|
5
|
+
title?: string;
|
|
6
|
+
file?: string;
|
|
7
|
+
before: string;
|
|
8
|
+
after: string;
|
|
9
|
+
lang?: CodeLang;
|
|
10
|
+
fontSize?: number;
|
|
11
|
+
morphAtSecond: number;
|
|
12
|
+
}>;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { diffLines } from "diff";
|
|
3
|
+
import { interpolate, useCurrentFrame } from "remotion";
|
|
4
|
+
import { CodeLine } from "../code/CodeBlock";
|
|
5
|
+
import { useTokenLines } from "../code/highlight";
|
|
6
|
+
import { FPS, theme } from "../theme";
|
|
7
|
+
import { FileChip, Panel, SceneShell } from "./SceneShell";
|
|
8
|
+
function computeRows(before, after) {
|
|
9
|
+
const rows = [];
|
|
10
|
+
let beforeLine = 0;
|
|
11
|
+
let afterLine = 0;
|
|
12
|
+
for (const part of diffLines(before.replace(/\n$/, "") + "\n", after.replace(/\n$/, "") + "\n")) {
|
|
13
|
+
const count = part.count ?? 0;
|
|
14
|
+
for (let i = 0; i < count; i++) {
|
|
15
|
+
if (part.added) {
|
|
16
|
+
rows.push({ kind: "add", afterLine: afterLine++ });
|
|
17
|
+
}
|
|
18
|
+
else if (part.removed) {
|
|
19
|
+
rows.push({ kind: "del", beforeLine: beforeLine++ });
|
|
20
|
+
}
|
|
21
|
+
else {
|
|
22
|
+
rows.push({ kind: "same", beforeLine: beforeLine++, afterLine: afterLine++ });
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return rows;
|
|
27
|
+
}
|
|
28
|
+
export const DiffMorph = ({ kicker, title, file, before, after, lang = "typescript", fontSize = 30, morphAtSecond }) => {
|
|
29
|
+
const frame = useCurrentFrame();
|
|
30
|
+
const beforeTokens = useTokenLines(before.replace(/\n$/, ""), lang);
|
|
31
|
+
const afterTokens = useTokenLines(after.replace(/\n$/, ""), lang);
|
|
32
|
+
if (!beforeTokens || !afterTokens)
|
|
33
|
+
return null;
|
|
34
|
+
const rows = computeRows(before, after);
|
|
35
|
+
const lineHeight = fontSize * 1.55;
|
|
36
|
+
const morphStart = morphAtSecond * FPS;
|
|
37
|
+
const morph = interpolate(frame, [morphStart, morphStart + 22], [0, 1], {
|
|
38
|
+
extrapolateLeft: "clamp",
|
|
39
|
+
extrapolateRight: "clamp",
|
|
40
|
+
});
|
|
41
|
+
const flashFade = interpolate(frame, [morphStart + 22, morphStart + 60], [1, 0], {
|
|
42
|
+
extrapolateLeft: "clamp",
|
|
43
|
+
extrapolateRight: "clamp",
|
|
44
|
+
});
|
|
45
|
+
return (_jsx(SceneShell, { kicker: kicker, title: title, center: true, children: _jsxs(Panel, { children: [file && (_jsx(FileChip, { path: file, badge: morph > 0.5 ? "after" : "before", badgeColor: morph > 0.5 ? theme.green : theme.red })), _jsx("div", { style: { fontFamily: theme.fontMono, fontSize, lineHeight: `${lineHeight}px` }, children: rows.map((row, i) => {
|
|
46
|
+
const tokens = row.kind === "del" ? beforeTokens[row.beforeLine] : afterTokens[row.afterLine];
|
|
47
|
+
const height = row.kind === "same" ? lineHeight : row.kind === "del" ? lineHeight * (1 - morph) : lineHeight * morph;
|
|
48
|
+
const opacity = row.kind === "same" ? 1 : row.kind === "del" ? 1 - morph : morph;
|
|
49
|
+
const flash = row.kind === "add" && morph > 0
|
|
50
|
+
? `${theme.green}${alphaHex(0.16 * flashFade + 0.06 * morph)}`
|
|
51
|
+
: row.kind === "del"
|
|
52
|
+
? `${theme.red}${alphaHex(0.14 * (1 - morph) + 0.04)}`
|
|
53
|
+
: "transparent";
|
|
54
|
+
return (_jsxs("div", { style: {
|
|
55
|
+
height,
|
|
56
|
+
opacity,
|
|
57
|
+
overflow: "hidden",
|
|
58
|
+
backgroundColor: flash,
|
|
59
|
+
borderRadius: 4,
|
|
60
|
+
paddingLeft: 12,
|
|
61
|
+
display: "flex",
|
|
62
|
+
alignItems: "center",
|
|
63
|
+
}, children: [_jsx("span", { style: { width: fontSize * 1.2, color: rowGlyphColor(row.kind), flexShrink: 0 }, children: row.kind === "add" ? "+" : row.kind === "del" ? "-" : " " }), tokens && _jsx(CodeLine, { tokens: tokens })] }, i));
|
|
64
|
+
}) })] }) }));
|
|
65
|
+
};
|
|
66
|
+
const rowGlyphColor = (kind) => kind === "add" ? theme.green : kind === "del" ? theme.red : "transparent";
|
|
67
|
+
const alphaHex = (alpha) => Math.round(Math.max(0, Math.min(1, alpha)) * 255)
|
|
68
|
+
.toString(16)
|
|
69
|
+
.padStart(2, "0");
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
export type FileEntry = {
|
|
3
|
+
readonly name: string;
|
|
4
|
+
readonly status: "added" | "modified";
|
|
5
|
+
readonly note?: string;
|
|
6
|
+
};
|
|
7
|
+
export type FileGroup = {
|
|
8
|
+
readonly label: string;
|
|
9
|
+
readonly icon: string;
|
|
10
|
+
readonly files: readonly FileEntry[];
|
|
11
|
+
readonly more?: number;
|
|
12
|
+
};
|
|
13
|
+
export type GroupSpotlight = {
|
|
14
|
+
readonly atSecond: number;
|
|
15
|
+
readonly groupIndex: number | null;
|
|
16
|
+
};
|
|
17
|
+
export declare const FileTreeScene: React.FC<{
|
|
18
|
+
kicker?: string;
|
|
19
|
+
title?: string;
|
|
20
|
+
groups: readonly FileGroup[];
|
|
21
|
+
spotlights?: readonly GroupSpotlight[];
|
|
22
|
+
columns?: number;
|
|
23
|
+
}>;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { spring, useCurrentFrame, useVideoConfig } from "remotion";
|
|
3
|
+
import { FPS, theme } from "../theme";
|
|
4
|
+
import { SceneShell } from "./SceneShell";
|
|
5
|
+
export const FileTreeScene = ({ kicker, title, groups, spotlights = [], columns = 3 }) => {
|
|
6
|
+
const frame = useCurrentFrame();
|
|
7
|
+
const { fps } = useVideoConfig();
|
|
8
|
+
const second = frame / FPS;
|
|
9
|
+
const active = [...spotlights].reverse().find((beat) => second >= beat.atSecond);
|
|
10
|
+
const activeIndex = active?.groupIndex ?? null;
|
|
11
|
+
return (_jsx(SceneShell, { kicker: kicker, title: title, center: true, children: _jsx("div", { style: {
|
|
12
|
+
display: "grid",
|
|
13
|
+
gridTemplateColumns: `repeat(${columns}, 1fr)`,
|
|
14
|
+
gap: 28,
|
|
15
|
+
width: "100%",
|
|
16
|
+
alignItems: "start",
|
|
17
|
+
}, children: groups.map((group, groupIndex) => {
|
|
18
|
+
const pop = spring({ frame: frame - 6 - groupIndex * 6, fps, config: { damping: 15, stiffness: 160, mass: 0.6 } });
|
|
19
|
+
const lit = activeIndex === null || activeIndex === groupIndex;
|
|
20
|
+
return (_jsxs("div", { style: {
|
|
21
|
+
backgroundColor: theme.bgPanel,
|
|
22
|
+
border: `2px solid ${lit && activeIndex !== null ? theme.accent : theme.stroke}`,
|
|
23
|
+
borderRadius: 18,
|
|
24
|
+
padding: "24px 28px",
|
|
25
|
+
opacity: pop * (lit ? 1 : 0.3),
|
|
26
|
+
transform: `translateY(${(1 - pop) * 40}px) scale(${lit && activeIndex !== null ? 1.02 : 1})`,
|
|
27
|
+
boxShadow: lit && activeIndex !== null ? `0 0 50px ${theme.accent}2a` : "none",
|
|
28
|
+
}, children: [_jsxs("div", { style: { display: "flex", alignItems: "center", gap: 14, marginBottom: 18 }, children: [_jsx("span", { style: { fontSize: 30 }, children: group.icon }), _jsx("span", { style: { fontFamily: theme.fontMono, fontWeight: 700, fontSize: 27, color: theme.accent2 }, children: group.label })] }), group.files.map((file, fileIndex) => {
|
|
29
|
+
const fileAt = 10 + groupIndex * 6 + fileIndex * 3;
|
|
30
|
+
const filePop = spring({ frame: frame - fileAt, fps, config: { damping: 18, stiffness: 200, mass: 0.4 } });
|
|
31
|
+
const color = file.status === "added" ? theme.green : theme.amber;
|
|
32
|
+
return (_jsxs("div", { style: {
|
|
33
|
+
display: "flex",
|
|
34
|
+
alignItems: "center",
|
|
35
|
+
gap: 12,
|
|
36
|
+
padding: "5px 0",
|
|
37
|
+
opacity: filePop,
|
|
38
|
+
transform: `translateX(${(1 - filePop) * 20}px)`,
|
|
39
|
+
}, children: [_jsx("span", { style: {
|
|
40
|
+
fontFamily: theme.fontMono,
|
|
41
|
+
fontWeight: 800,
|
|
42
|
+
fontSize: 21,
|
|
43
|
+
color,
|
|
44
|
+
width: 26,
|
|
45
|
+
textAlign: "center",
|
|
46
|
+
backgroundColor: `${color}1a`,
|
|
47
|
+
borderRadius: 6,
|
|
48
|
+
flexShrink: 0,
|
|
49
|
+
}, children: file.status === "added" ? "A" : "M" }), _jsx("span", { style: {
|
|
50
|
+
fontFamily: theme.fontMono,
|
|
51
|
+
fontSize: 22.5,
|
|
52
|
+
color: theme.text,
|
|
53
|
+
whiteSpace: "nowrap",
|
|
54
|
+
overflow: "hidden",
|
|
55
|
+
textOverflow: "ellipsis",
|
|
56
|
+
}, children: file.name }), file.note && (_jsx("span", { style: { fontFamily: theme.fontDisplay, fontSize: 19, color: theme.textDim, marginLeft: "auto", flexShrink: 0 }, children: file.note }))] }, fileIndex));
|
|
57
|
+
}), group.more !== undefined && group.more > 0 && (_jsxs("div", { style: { fontFamily: theme.fontDisplay, fontSize: 21, color: theme.textDim, marginTop: 10 }, children: ["+ ", group.more, " more"] }))] }, groupIndex));
|
|
58
|
+
}) }) }));
|
|
59
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
export type MontageItem = {
|
|
3
|
+
readonly icon: string;
|
|
4
|
+
readonly label: string;
|
|
5
|
+
readonly sub?: string;
|
|
6
|
+
readonly atSecond?: number;
|
|
7
|
+
};
|
|
8
|
+
export declare const MontageBeat: React.FC<{
|
|
9
|
+
kicker?: string;
|
|
10
|
+
title?: string;
|
|
11
|
+
items: readonly MontageItem[];
|
|
12
|
+
staggerFrames?: number;
|
|
13
|
+
}>;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import React from "react";
|
|
3
|
+
import { Audio, Sequence, spring, staticFile, useCurrentFrame, useVideoConfig } from "remotion";
|
|
4
|
+
import { theme } from "../theme";
|
|
5
|
+
import { SceneShell } from "./SceneShell";
|
|
6
|
+
export const MontageBeat = ({ kicker, title, items, staggerFrames = 9 }) => {
|
|
7
|
+
const frame = useCurrentFrame();
|
|
8
|
+
const { fps } = useVideoConfig();
|
|
9
|
+
return (_jsx(SceneShell, { kicker: kicker, title: title, center: true, children: _jsx("div", { style: { display: "flex", flexDirection: "column", gap: 26, width: 1200 }, children: items.map((item, i) => {
|
|
10
|
+
const at = item.atSecond !== undefined ? Math.round(item.atSecond * 30) : 8 + i * staggerFrames;
|
|
11
|
+
const pop = spring({ frame: frame - at, fps, config: { damping: 13, stiffness: 190, mass: 0.5 } });
|
|
12
|
+
return (_jsxs(React.Fragment, { children: [frame >= at && (_jsx(Sequence, { from: at, name: `tick-${i}`, children: _jsx(Audio, { src: staticFile("sfx/tick.wav"), volume: 0.35 }) })), _jsxs("div", { style: {
|
|
13
|
+
display: "flex",
|
|
14
|
+
alignItems: "center",
|
|
15
|
+
gap: 28,
|
|
16
|
+
backgroundColor: theme.bgPanel,
|
|
17
|
+
border: `1px solid ${theme.stroke}`,
|
|
18
|
+
borderRadius: 16,
|
|
19
|
+
padding: "22px 34px",
|
|
20
|
+
transform: `translateX(${(1 - pop) * 200}px) scale(${0.9 + pop * 0.1})`,
|
|
21
|
+
opacity: pop,
|
|
22
|
+
}, children: [_jsx("span", { style: { fontSize: 44 }, children: item.icon }), _jsx("span", { style: { fontFamily: theme.fontDisplay, fontWeight: 700, fontSize: 36, color: theme.text }, children: item.label }), item.sub && (_jsx("span", { style: { fontFamily: theme.fontMono, fontSize: 25, color: theme.textDim, marginLeft: "auto" }, children: item.sub }))] })] }, i));
|
|
23
|
+
}) }) }));
|
|
24
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
export declare const SceneShell: React.FC<{
|
|
3
|
+
kicker?: string;
|
|
4
|
+
title?: string;
|
|
5
|
+
contextTag?: boolean;
|
|
6
|
+
center?: boolean;
|
|
7
|
+
children: React.ReactNode;
|
|
8
|
+
}>;
|
|
9
|
+
export declare const ContextTag: React.FC;
|
|
10
|
+
export declare const Panel: React.FC<{
|
|
11
|
+
children: React.ReactNode;
|
|
12
|
+
padding?: number;
|
|
13
|
+
appearAtSecond?: number;
|
|
14
|
+
}>;
|
|
15
|
+
export declare const FileChip: React.FC<{
|
|
16
|
+
path: string;
|
|
17
|
+
badge?: string;
|
|
18
|
+
badgeColor?: string;
|
|
19
|
+
}>;
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { AbsoluteFill, interpolate, spring, useCurrentFrame, useVideoConfig } from "remotion";
|
|
3
|
+
import { theme } from "../theme";
|
|
4
|
+
export const SceneShell = ({ kicker, title, contextTag, center, children }) => {
|
|
5
|
+
const frame = useCurrentFrame();
|
|
6
|
+
const { fps } = useVideoConfig();
|
|
7
|
+
const slide = spring({ frame, fps, config: { damping: 200, stiffness: 120 } });
|
|
8
|
+
return (_jsxs(AbsoluteFill, { style: { backgroundColor: theme.bg, overflow: "hidden" }, children: [_jsx(Backdrop, {}), _jsxs(AbsoluteFill, { style: { padding: "64px 96px", display: "flex", flexDirection: "column" }, children: [(kicker || title || contextTag) && (_jsxs("div", { style: {
|
|
9
|
+
marginBottom: 36,
|
|
10
|
+
opacity: slide,
|
|
11
|
+
transform: `translateY(${(1 - slide) * -30}px)`,
|
|
12
|
+
display: "flex",
|
|
13
|
+
alignItems: "baseline",
|
|
14
|
+
gap: 24,
|
|
15
|
+
}, children: [_jsxs("div", { children: [kicker && (_jsx("div", { style: {
|
|
16
|
+
fontFamily: theme.fontDisplay,
|
|
17
|
+
fontWeight: 600,
|
|
18
|
+
fontSize: 26,
|
|
19
|
+
letterSpacing: 4,
|
|
20
|
+
textTransform: "uppercase",
|
|
21
|
+
color: theme.accent2,
|
|
22
|
+
marginBottom: 6,
|
|
23
|
+
}, children: kicker })), title && (_jsx("div", { style: { fontFamily: theme.fontDisplay, fontWeight: 700, fontSize: 52, color: theme.text }, children: title }))] }), contextTag && _jsx(ContextTag, {})] })), _jsx("div", { style: {
|
|
24
|
+
flex: 1,
|
|
25
|
+
display: "flex",
|
|
26
|
+
flexDirection: "column",
|
|
27
|
+
justifyContent: center ? "center" : "flex-start",
|
|
28
|
+
alignItems: center ? "center" : "stretch",
|
|
29
|
+
minHeight: 0,
|
|
30
|
+
}, children: children })] })] }));
|
|
31
|
+
};
|
|
32
|
+
export const ContextTag = () => (_jsx("span", { style: {
|
|
33
|
+
fontFamily: theme.fontDisplay,
|
|
34
|
+
fontWeight: 600,
|
|
35
|
+
fontSize: 22,
|
|
36
|
+
color: theme.textDim,
|
|
37
|
+
border: `2px solid ${theme.stroke}`,
|
|
38
|
+
borderRadius: 999,
|
|
39
|
+
padding: "6px 18px",
|
|
40
|
+
whiteSpace: "nowrap",
|
|
41
|
+
}, children: "\u23EA existing code \u2014 not part of this PR" }));
|
|
42
|
+
const Backdrop = () => {
|
|
43
|
+
const frame = useCurrentFrame();
|
|
44
|
+
const drift = interpolate(frame, [0, 600], [0, 80]);
|
|
45
|
+
return (_jsxs(AbsoluteFill, { children: [_jsx("div", { style: {
|
|
46
|
+
position: "absolute",
|
|
47
|
+
width: 1300,
|
|
48
|
+
height: 1300,
|
|
49
|
+
borderRadius: "50%",
|
|
50
|
+
background: `radial-gradient(circle, ${theme.accent}14 0%, transparent 65%)`,
|
|
51
|
+
top: -500 + drift * 0.4,
|
|
52
|
+
left: -300,
|
|
53
|
+
} }), _jsx("div", { style: {
|
|
54
|
+
position: "absolute",
|
|
55
|
+
width: 1100,
|
|
56
|
+
height: 1100,
|
|
57
|
+
borderRadius: "50%",
|
|
58
|
+
background: `radial-gradient(circle, ${theme.accent2}10 0%, transparent 65%)`,
|
|
59
|
+
bottom: -450,
|
|
60
|
+
right: -250 + drift * 0.3,
|
|
61
|
+
} })] }));
|
|
62
|
+
};
|
|
63
|
+
export const Panel = ({ children, padding = 36, }) => {
|
|
64
|
+
const frame = useCurrentFrame();
|
|
65
|
+
const { fps } = useVideoConfig();
|
|
66
|
+
const pop = spring({ frame, fps, config: { damping: 16, stiffness: 140, mass: 0.7 } });
|
|
67
|
+
return (_jsx("div", { style: {
|
|
68
|
+
backgroundColor: theme.bgCode,
|
|
69
|
+
border: `1px solid ${theme.stroke}`,
|
|
70
|
+
borderRadius: 18,
|
|
71
|
+
padding,
|
|
72
|
+
boxShadow: "0 30px 80px rgba(0,0,0,0.55)",
|
|
73
|
+
transform: `scale(${0.94 + pop * 0.06})`,
|
|
74
|
+
opacity: pop,
|
|
75
|
+
maxWidth: "100%",
|
|
76
|
+
}, children: children }));
|
|
77
|
+
};
|
|
78
|
+
export const FileChip = ({ path, badge, badgeColor = theme.green, }) => (_jsxs("div", { style: { display: "flex", alignItems: "center", gap: 14, marginBottom: 20 }, children: [_jsx("span", { style: {
|
|
79
|
+
fontFamily: theme.fontMono,
|
|
80
|
+
fontSize: 24,
|
|
81
|
+
color: theme.accent2,
|
|
82
|
+
backgroundColor: `${theme.accent2}14`,
|
|
83
|
+
border: `1px solid ${theme.accent2}33`,
|
|
84
|
+
borderRadius: 8,
|
|
85
|
+
padding: "6px 16px",
|
|
86
|
+
}, children: path }), badge && (_jsx("span", { style: {
|
|
87
|
+
fontFamily: theme.fontDisplay,
|
|
88
|
+
fontWeight: 700,
|
|
89
|
+
fontSize: 20,
|
|
90
|
+
letterSpacing: 2,
|
|
91
|
+
color: badgeColor,
|
|
92
|
+
backgroundColor: `${badgeColor}1a`,
|
|
93
|
+
borderRadius: 8,
|
|
94
|
+
padding: "6px 14px",
|
|
95
|
+
textTransform: "uppercase",
|
|
96
|
+
}, children: badge }))] }));
|