@waits/cadence 0.2.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/MOTION.md +70 -0
- package/README.md +109 -0
- package/bin/cli.mjs +21 -0
- package/package.json +59 -0
- package/prompts/art/compose.ts +18 -0
- package/prompts/art/fantasy.ts +15 -0
- package/prompts/art/landmarks.ts +49 -0
- package/prompts/art/style.ts +20 -0
- package/public/backgrounds/barton-springs.png +0 -0
- package/public/backgrounds/capitol.png +0 -0
- package/public/backgrounds/congress.png +0 -0
- package/public/backgrounds/enchanted-rock.png +0 -0
- package/public/backgrounds/hamilton-pool.png +0 -0
- package/public/backgrounds/mount-bonnell.png +0 -0
- package/public/backgrounds/pennybacker.png +0 -0
- package/public/backgrounds/ut-tower.png +0 -0
- package/scripts/_pkg.ts +28 -0
- package/scripts/audit.ts +69 -0
- package/scripts/changes.ts +70 -0
- package/scripts/check-motion.ts +37 -0
- package/scripts/cli.ts +79 -0
- package/scripts/generate-art.ts +72 -0
- package/scripts/guide.ts +114 -0
- package/scripts/make.ts +76 -0
- package/scripts/redesign.ts +83 -0
- package/scripts/render.ts +62 -0
- package/scripts/smoke.ts +43 -0
- package/scripts/sync-skill.ts +43 -0
- package/scripts/theme.ts +97 -0
- package/src/Root.tsx +51 -0
- package/src/adapters/index.ts +72 -0
- package/src/adapters/parse.ts +65 -0
- package/src/adapters/types.ts +24 -0
- package/src/brand/fonts.ts +74 -0
- package/src/brand/tokens.ts +31 -0
- package/src/brand/tone.ts +25 -0
- package/src/code/highlight.ts +90 -0
- package/src/components/Background.tsx +75 -0
- package/src/components/Changelog.tsx +17 -0
- package/src/components/ChangelogScene.tsx +117 -0
- package/src/components/CodeWindow.tsx +106 -0
- package/src/components/ContactSheet.tsx +44 -0
- package/src/components/Headline.tsx +73 -0
- package/src/components/MotionReel.tsx +108 -0
- package/src/components/panels/DataTable.tsx +34 -0
- package/src/components/panels/Diagram.tsx +67 -0
- package/src/components/panels/Feed.tsx +46 -0
- package/src/components/panels/Fork.tsx +86 -0
- package/src/components/panels/PanelCard.tsx +44 -0
- package/src/components/panels/Proof.tsx +64 -0
- package/src/components/panels/Stat.tsx +28 -0
- package/src/components/panels/Status.tsx +41 -0
- package/src/components/panels/StreamResume.tsx +48 -0
- package/src/components/panels/UploadProgress.tsx +42 -0
- package/src/components/panels/index.tsx +34 -0
- package/src/content/clarinet-3.18.beats.ts +58 -0
- package/src/content/gradient-demo.beats.ts +29 -0
- package/src/content/index-decoded.beats.ts +93 -0
- package/src/content/mainnet-launch.beats.ts +70 -0
- package/src/content/panels.beats.ts +40 -0
- package/src/content/sdk-6.5-mempool.beats.ts +67 -0
- package/src/content/streams-launch.beats.ts +143 -0
- package/src/content/streams.beats.ts +46 -0
- package/src/index.ts +4 -0
- package/src/motion/names.ts +29 -0
- package/src/motion/presets.ts +67 -0
- package/src/motion/useMotion.ts +63 -0
- package/src/prepare.ts +33 -0
- package/src/schema/beats.ts +161 -0
- package/src/templates/changelog-reel.ts +28 -0
- package/src/templates/feature-launch.ts +26 -0
- package/src/templates/index.ts +13 -0
- package/src/templates/milestone.ts +25 -0
- package/src/templates/parts.ts +32 -0
- package/src/templates/types.ts +31 -0
- package/src/theme/default.ts +39 -0
- package/src/theme/derive.ts +119 -0
- package/src/theme/index.ts +33 -0
- package/src/theme/library.ts +60 -0
- package/src/theme/slate.ts +39 -0
- package/src/theme/types.ts +67 -0
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { ChangelogInput } from "../schema/beats";
|
|
2
|
+
|
|
3
|
+
/** Showcase fixture: one beat per panel kind, used to eyeball the registry. */
|
|
4
|
+
const video: ChangelogInput = {
|
|
5
|
+
format: "16x9",
|
|
6
|
+
beats: [
|
|
7
|
+
{
|
|
8
|
+
id: "upload",
|
|
9
|
+
durationInFrames: 150,
|
|
10
|
+
background: { src: "mountains.jpg" },
|
|
11
|
+
eyebrow: "new in sdk 6.0",
|
|
12
|
+
headline: "Pause and resume backfills.",
|
|
13
|
+
code: { filename: "backfill.ts", lang: "ts", source: `const ctrl = new UploadControl();\n\nawait sl.datasets.export("sbtc", file, {\n control: ctrl,\n});` },
|
|
14
|
+
panel: { kind: "upload-progress", file: "datasets/sbtc.parquet", sizeMB: 210, parts: 14 },
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
id: "query",
|
|
18
|
+
durationInFrames: 150,
|
|
19
|
+
background: { src: "mountains.jpg" },
|
|
20
|
+
headline: "Query any dataset.",
|
|
21
|
+
code: { filename: "query.ts", lang: "ts", source: `const { rows } = await sl.datasets.query("sbtc", {\n where: { event: "transfer" },\n limit: 4,\n});` },
|
|
22
|
+
panel: { kind: "data-table", title: "sbtc · transfers", columns: ["block", "amount", "to"], rows: [["951475", "1,200.00", "SP2J6…"], ["951474", "48.50", "SP3K9…"], ["951472", "9,000.00", "SPF8M…"], ["951470", "150.00", "SP1Y4…"]] },
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
id: "status",
|
|
26
|
+
durationInFrames: 150,
|
|
27
|
+
background: { src: "mountains.jpg" },
|
|
28
|
+
headline: "Watch every service.",
|
|
29
|
+
panel: { kind: "status", title: "status", services: [{ name: "api", state: "ok" }, { name: "indexer", state: "ok", detail: "block 951475" }, { name: "l2-decoder", state: "syncing", detail: "−2 blocks" }, { name: "database", state: "ok" }] },
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
id: "pipeline",
|
|
33
|
+
durationInFrames: 150,
|
|
34
|
+
background: { src: "mountains.jpg" },
|
|
35
|
+
headline: "Decoded once. Query forever.",
|
|
36
|
+
panel: { kind: "diagram", nodes: [{ id: "node", label: "Stacks node", type: "default" }, { id: "idx", label: "Indexer", type: "data" }, { id: "api", label: "Index API", type: "api" }], edges: [{ from: "node", to: "idx", label: "events" }, { from: "idx", to: "api", label: "decoded" }], note: "decoded once — query forever" },
|
|
37
|
+
},
|
|
38
|
+
],
|
|
39
|
+
};
|
|
40
|
+
export default video;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { ChangelogInput } from "../schema/beats";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* T0.1 brain-spike output. Sourced entirely by reading the Secondlayer repo
|
|
5
|
+
* GENERICALLY (no baked-in knowledge):
|
|
6
|
+
* - latest release: git tag `@secondlayer/sdk@6.5.0`
|
|
7
|
+
* - what changed: commits 6.4.0..6.5.0 → the Index mempool feature (+contractId filter)
|
|
8
|
+
* - honest API: packages/sdk/src/index-api/client.ts types + packages/sdk/README.md
|
|
9
|
+
* Honesty ladder caught: the real class is `SecondLayer` (capital L), and the
|
|
10
|
+
* real call is `sl.index.mempool.list({ contractId, limit })` returning `{ mempool }`.
|
|
11
|
+
*/
|
|
12
|
+
const BG = "backgrounds/mount-bonnell.png";
|
|
13
|
+
|
|
14
|
+
const video: ChangelogInput = {
|
|
15
|
+
format: "16x9",
|
|
16
|
+
beats: [
|
|
17
|
+
{
|
|
18
|
+
id: "opener",
|
|
19
|
+
durationInFrames: 150,
|
|
20
|
+
background: { src: BG },
|
|
21
|
+
eyebrow: "new in sdk 6.5",
|
|
22
|
+
headline: "The mempool, indexed.",
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
id: "pending",
|
|
26
|
+
durationInFrames: 235,
|
|
27
|
+
background: { src: BG },
|
|
28
|
+
eyebrow: "index.mempool",
|
|
29
|
+
headline: "Query the pending set.",
|
|
30
|
+
code: {
|
|
31
|
+
filename: "mempool.ts",
|
|
32
|
+
lang: "ts",
|
|
33
|
+
source: `import { SecondLayer } from "@secondlayer/sdk";
|
|
34
|
+
|
|
35
|
+
const sl = new SecondLayer();
|
|
36
|
+
|
|
37
|
+
const { mempool } = await sl.index.mempool.list({
|
|
38
|
+
contractId: "SP….amm-pool-v2",
|
|
39
|
+
limit: 50,
|
|
40
|
+
});`,
|
|
41
|
+
},
|
|
42
|
+
panel: {
|
|
43
|
+
kind: "data-table",
|
|
44
|
+
title: "index.mempool",
|
|
45
|
+
columns: ["tx_id", "function", "sender"],
|
|
46
|
+
rows: [
|
|
47
|
+
["0x8f2a…", "swap-x-for-y", "SP2J6…WVEF"],
|
|
48
|
+
["0x3c91…", "add-liquidity", "SP3K9…X1A0"],
|
|
49
|
+
["0x7e0d…", "swap-x-for-y", "SPF8M…7QQC"],
|
|
50
|
+
["0x1a44…", "transfer", "SP1Y4…NZ2D"],
|
|
51
|
+
],
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
id: "cta",
|
|
56
|
+
durationInFrames: 160,
|
|
57
|
+
background: { src: BG },
|
|
58
|
+
headline: "Start building.",
|
|
59
|
+
layout: "center",
|
|
60
|
+
code: { filename: "terminal", lang: "bash", source: `bun add @secondlayer/sdk` },
|
|
61
|
+
badge: "v6.5",
|
|
62
|
+
caption: "index · streams · datasets · subgraphs",
|
|
63
|
+
},
|
|
64
|
+
],
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
export default video;
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import type { ChangelogInput } from "../schema/beats";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Streams showcase — the Stacks event firehose. Subscribe → provable → resumable
|
|
5
|
+
* → reorg-safe → install. New panel kinds (proof / cursor-resume / reorg) designed
|
|
6
|
+
* with the design skills; real @secondlayer/sdk/streams API.
|
|
7
|
+
*/
|
|
8
|
+
const BG = "backgrounds/congress.png";
|
|
9
|
+
|
|
10
|
+
const video: ChangelogInput = {
|
|
11
|
+
format: "16x9",
|
|
12
|
+
beats: [
|
|
13
|
+
{
|
|
14
|
+
id: "opener",
|
|
15
|
+
durationInFrames: 150,
|
|
16
|
+
background: { src: BG },
|
|
17
|
+
eyebrow: "new in streams",
|
|
18
|
+
headline: "The Stacks event firehose.",
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
id: "subscribe",
|
|
22
|
+
durationInFrames: 235,
|
|
23
|
+
background: { src: BG },
|
|
24
|
+
eyebrow: "real-time",
|
|
25
|
+
headline: "Subscribe to every block.",
|
|
26
|
+
code: {
|
|
27
|
+
filename: "stream.ts",
|
|
28
|
+
lang: "ts",
|
|
29
|
+
source: `import { createStreamsClient } from "@secondlayer/sdk/streams";
|
|
30
|
+
|
|
31
|
+
const streams = createStreamsClient();
|
|
32
|
+
|
|
33
|
+
for await (const event of streams.events.consume({
|
|
34
|
+
types: ["ft_transfer"],
|
|
35
|
+
mode: "tail",
|
|
36
|
+
})) {
|
|
37
|
+
ledger.append(event);
|
|
38
|
+
}`,
|
|
39
|
+
},
|
|
40
|
+
panel: {
|
|
41
|
+
kind: "feed",
|
|
42
|
+
title: "streams.consume",
|
|
43
|
+
subtitle: "tail",
|
|
44
|
+
rows: [
|
|
45
|
+
{ badge: "sBTC", label: "SP2J6…WVEF", value: "1,200.00" },
|
|
46
|
+
{ badge: "USDA", label: "SP3K9…X1A0", value: "48.50" },
|
|
47
|
+
{ badge: "ALEX", label: "SPF8M…7QQC", value: "9,000.00" },
|
|
48
|
+
{ badge: "sBTC", label: "SP1Y4…NZ2D", value: "150.00" },
|
|
49
|
+
],
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
id: "proof",
|
|
54
|
+
durationInFrames: 235,
|
|
55
|
+
background: { src: BG },
|
|
56
|
+
eyebrow: "signed",
|
|
57
|
+
headline: "Every event, provable.",
|
|
58
|
+
code: {
|
|
59
|
+
filename: "verify.ts",
|
|
60
|
+
lang: "ts",
|
|
61
|
+
source: `const streams = createStreamsClient({
|
|
62
|
+
verify: true,
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// every response carries an ed25519 signature,
|
|
66
|
+
// checked against the server's public key.`,
|
|
67
|
+
},
|
|
68
|
+
panel: {
|
|
69
|
+
kind: "proof",
|
|
70
|
+
eventLine: "ft_transfer · 1,200 sBTC",
|
|
71
|
+
cursor: "951475:3",
|
|
72
|
+
signature: "3a9f8c217b14e0d5f29a4c6b8e1d70a3c8a17b142e9d",
|
|
73
|
+
keyId: "f3a9…2b1c",
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
id: "resume",
|
|
78
|
+
durationInFrames: 235,
|
|
79
|
+
background: { src: BG },
|
|
80
|
+
eyebrow: "resumable",
|
|
81
|
+
headline: "Resume from anywhere.",
|
|
82
|
+
code: {
|
|
83
|
+
filename: "resume.ts",
|
|
84
|
+
lang: "ts",
|
|
85
|
+
source: `for await (const event of streams.events.consume({
|
|
86
|
+
fromCursor: "951475:3",
|
|
87
|
+
mode: "tail",
|
|
88
|
+
})) {
|
|
89
|
+
process(event);
|
|
90
|
+
}`,
|
|
91
|
+
},
|
|
92
|
+
panel: {
|
|
93
|
+
kind: "stream-resume",
|
|
94
|
+
fromCursor: "951475:3",
|
|
95
|
+
rows: [
|
|
96
|
+
{ cursor: "951475:4", label: "print · swap-x-for-y" },
|
|
97
|
+
{ cursor: "951476:0", label: "ft_transfer" },
|
|
98
|
+
{ cursor: "951476:1", label: "nft_transfer" },
|
|
99
|
+
{ cursor: "951477:0", label: "print · mint" },
|
|
100
|
+
],
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
id: "reorg",
|
|
105
|
+
durationInFrames: 235,
|
|
106
|
+
background: { src: BG },
|
|
107
|
+
eyebrow: "reorg-safe",
|
|
108
|
+
headline: "Forks, handled.",
|
|
109
|
+
code: {
|
|
110
|
+
filename: "reorg.ts",
|
|
111
|
+
lang: "ts",
|
|
112
|
+
source: `streams.events.replay({
|
|
113
|
+
from: "genesis",
|
|
114
|
+
onReorg: (reorg, { cursor }) => {
|
|
115
|
+
rewind(cursor);
|
|
116
|
+
},
|
|
117
|
+
});`,
|
|
118
|
+
},
|
|
119
|
+
panel: {
|
|
120
|
+
kind: "fork",
|
|
121
|
+
blocks: [
|
|
122
|
+
{ height: 951472, hash: "a3f9", state: "canonical" },
|
|
123
|
+
{ height: 951473, hash: "b1c4", state: "canonical" },
|
|
124
|
+
{ height: 951474, hash: "7e2d", state: "orphaned" },
|
|
125
|
+
{ height: 951474, hash: "c8a1", state: "new" },
|
|
126
|
+
],
|
|
127
|
+
rewindTo: "951472:0",
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
id: "cta",
|
|
132
|
+
durationInFrames: 160,
|
|
133
|
+
background: { src: BG },
|
|
134
|
+
headline: "Stream it now.",
|
|
135
|
+
layout: "center",
|
|
136
|
+
code: { filename: "terminal", lang: "bash", source: `bun add @secondlayer/sdk` },
|
|
137
|
+
badge: "streams",
|
|
138
|
+
caption: "subscribe · verify · resume · reorg-safe",
|
|
139
|
+
},
|
|
140
|
+
],
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
export default video;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { ChangelogInput } from "../schema/beats";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The Sprint-0 reference scene, re-expressed as pure data. Renders identically
|
|
5
|
+
* through the changelog engine — `bun run render src/content/streams.beats.ts`.
|
|
6
|
+
*/
|
|
7
|
+
const video: ChangelogInput = {
|
|
8
|
+
format: "16x9",
|
|
9
|
+
beats: [
|
|
10
|
+
{
|
|
11
|
+
id: "stream",
|
|
12
|
+
durationInFrames: 210,
|
|
13
|
+
background: { src: "mountains.jpg" },
|
|
14
|
+
eyebrow: "new in sdk 5.5",
|
|
15
|
+
headline: "Stream every Stacks event.",
|
|
16
|
+
code: {
|
|
17
|
+
filename: "stream.ts",
|
|
18
|
+
lang: "ts",
|
|
19
|
+
source: `import { Secondlayer } from "@secondlayer/sdk";
|
|
20
|
+
|
|
21
|
+
const sl = new Secondlayer({
|
|
22
|
+
apiKey: process.env.SL_API_KEY,
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const { events } = await sl.index.events({
|
|
26
|
+
eventType: "ft_transfer",
|
|
27
|
+
limit: 50,
|
|
28
|
+
});`,
|
|
29
|
+
},
|
|
30
|
+
panel: {
|
|
31
|
+
kind: "feed",
|
|
32
|
+
title: "index.events",
|
|
33
|
+
subtitle: "ft_transfer",
|
|
34
|
+
rows: [
|
|
35
|
+
{ badge: "sBTC", label: "SP2J6…WVEF", value: "1,200.00" },
|
|
36
|
+
{ badge: "USDA", label: "SP3K9…X1A0", value: "48.50" },
|
|
37
|
+
{ badge: "ALEX", label: "SPF8M…7QQC", value: "9,000.00" },
|
|
38
|
+
{ badge: "sBTC", label: "SP1Y4…NZ2D", value: "150.00" },
|
|
39
|
+
{ badge: "WELSH", label: "SPGR2…0KME", value: "73.25" },
|
|
40
|
+
],
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
],
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export default video;
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Frozen vocabulary of motion preset names. Single source of truth shared by
|
|
3
|
+
* the presets implementation (Sprint A), the beats schema (Sprint B), and
|
|
4
|
+
* MOTION.md. Adding a transition means adding it here first.
|
|
5
|
+
*/
|
|
6
|
+
export const ENTER_PRESETS = [
|
|
7
|
+
"rise", // translateY up + fade — headlines, cards
|
|
8
|
+
"settle", // scale 1.03→1 + fade — windows/panels arriving
|
|
9
|
+
"bloom", // fade + de-blur — backgrounds
|
|
10
|
+
"type", // character-by-character typewriter — code/terminal
|
|
11
|
+
"stagger", // sequential rise of children — list rows
|
|
12
|
+
"draw", // SVG stroke reveal — diagrams, the NEW badge circle
|
|
13
|
+
"count", // numeric tween 0→value — metrics
|
|
14
|
+
] as const;
|
|
15
|
+
|
|
16
|
+
export const EXIT_PRESETS = [
|
|
17
|
+
"sink", // translateY up + fade out
|
|
18
|
+
"dissolve", // fade out
|
|
19
|
+
"lift", // scale 1→1.02 + fade out
|
|
20
|
+
"cut", // instant
|
|
21
|
+
] as const;
|
|
22
|
+
|
|
23
|
+
export type EnterPreset = (typeof ENTER_PRESETS)[number];
|
|
24
|
+
export type ExitPreset = (typeof EXIT_PRESETS)[number];
|
|
25
|
+
|
|
26
|
+
export const isEnterPreset = (s: string): s is EnterPreset =>
|
|
27
|
+
(ENTER_PRESETS as readonly string[]).includes(s);
|
|
28
|
+
export const isExitPreset = (s: string): s is ExitPreset =>
|
|
29
|
+
(EXIT_PRESETS as readonly string[]).includes(s);
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { EnterPreset, ExitPreset } from "./names";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Pure preset functions: given a normalized progress `p` (0→1, already eased by
|
|
5
|
+
* the caller) they return a style fragment. Keeping them pure (no hooks) makes
|
|
6
|
+
* the vocabulary testable and lets `useMotion` own all timing/easing.
|
|
7
|
+
*
|
|
8
|
+
* Entrances use the caller's `smooth` easing (pure ease-out). `type`/`draw`/
|
|
9
|
+
* `count` carry their value internally (typewriter chars, stroke offset, tween)
|
|
10
|
+
* so as container presets they only gate visibility — see the helpers below.
|
|
11
|
+
*/
|
|
12
|
+
export type StyleFrag = { opacity: number; transform: string; filter?: string };
|
|
13
|
+
|
|
14
|
+
export function enterStyle(
|
|
15
|
+
name: EnterPreset,
|
|
16
|
+
p: number,
|
|
17
|
+
opts: { distance?: number; reduced?: boolean } = {}
|
|
18
|
+
): StyleFrag {
|
|
19
|
+
const reduced = opts.reduced ?? false;
|
|
20
|
+
const d = opts.distance ?? 24;
|
|
21
|
+
switch (name) {
|
|
22
|
+
case "rise":
|
|
23
|
+
return { opacity: p, transform: reduced ? "none" : `translateY(${(1 - p) * d}px)` };
|
|
24
|
+
case "settle":
|
|
25
|
+
return { opacity: p, transform: reduced ? "none" : `scale(${(1.03 - 0.03 * p).toFixed(4)})` };
|
|
26
|
+
case "bloom":
|
|
27
|
+
return { opacity: p, transform: "none", filter: reduced ? undefined : `blur(${((1 - p) * 8).toFixed(2)}px)` };
|
|
28
|
+
case "stagger":
|
|
29
|
+
return { opacity: p, transform: reduced ? "none" : `translateY(${(1 - p) * 16}px)` };
|
|
30
|
+
case "type":
|
|
31
|
+
case "draw":
|
|
32
|
+
case "count":
|
|
33
|
+
return { opacity: p > 0 ? 1 : 0, transform: "none" };
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function exitStyle(
|
|
38
|
+
name: ExitPreset,
|
|
39
|
+
p: number,
|
|
40
|
+
opts: { reduced?: boolean } = {}
|
|
41
|
+
): StyleFrag {
|
|
42
|
+
const reduced = opts.reduced ?? false;
|
|
43
|
+
switch (name) {
|
|
44
|
+
case "sink":
|
|
45
|
+
return { opacity: 1 - p, transform: reduced ? "none" : `translateY(${-16 * p}px)` };
|
|
46
|
+
case "lift":
|
|
47
|
+
return { opacity: 1 - p, transform: reduced ? "none" : `scale(${(1 + 0.02 * p).toFixed(4)})` };
|
|
48
|
+
case "dissolve":
|
|
49
|
+
return { opacity: 1 - p, transform: "none" };
|
|
50
|
+
case "cut":
|
|
51
|
+
return { opacity: p < 1 ? 1 : 0, transform: "none" };
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ─── Helpers for the value-carrying presets ──────────────────────────────────
|
|
56
|
+
|
|
57
|
+
/** `type`: how many characters of `total` are revealed at eased progress `p`. */
|
|
58
|
+
export const typewriterChars = (p: number, total: number) => Math.round(p * total);
|
|
59
|
+
|
|
60
|
+
/** `draw`: stroke-dashoffset for an SVG path of length `len` at progress `p`. */
|
|
61
|
+
export const drawDashoffset = (p: number, len: number) => (1 - p) * len;
|
|
62
|
+
|
|
63
|
+
/** `count`: tween a metric from `from`→`to` at eased progress `p`. */
|
|
64
|
+
export const countValue = (p: number, to: number, from = 0) => from + (to - from) * p;
|
|
65
|
+
|
|
66
|
+
/** `stagger`: per-child delay (in frames) for index `i`. */
|
|
67
|
+
export const staggerDelay = (i: number, step = 4) => i * step;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { interpolate, useCurrentFrame, useVideoConfig } from "remotion";
|
|
2
|
+
import type { CSSProperties } from "react";
|
|
3
|
+
import { EASE } from "../brand/tokens";
|
|
4
|
+
import type { EnterPreset, ExitPreset } from "./names";
|
|
5
|
+
import { enterStyle, exitStyle } from "./presets";
|
|
6
|
+
|
|
7
|
+
/** Declarative motion for one element. Mirrored as zod in `src/schema/beats.ts`. */
|
|
8
|
+
export type MotionSpec = {
|
|
9
|
+
enter?: EnterPreset;
|
|
10
|
+
exit?: ExitPreset;
|
|
11
|
+
easing?: "smooth" | "snappy";
|
|
12
|
+
/** frames to wait before entering */
|
|
13
|
+
delay?: number;
|
|
14
|
+
/** frames the entrance takes (default ~0.55s) */
|
|
15
|
+
durationInFrames?: number;
|
|
16
|
+
/** px travel for rise (default 24) */
|
|
17
|
+
distance?: number;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Resolves a MotionSpec to a style for the current frame, composing the entrance
|
|
22
|
+
* (from `delay`) with an exit ramp in the sequence's final ~0.4s. `smooth` is the
|
|
23
|
+
* default entrance easing; `snappy` (slight settle) is opt-in for state pops.
|
|
24
|
+
*/
|
|
25
|
+
export const useMotion = (spec: MotionSpec = {}, opts: { reduced?: boolean } = {}): CSSProperties => {
|
|
26
|
+
const frame = useCurrentFrame();
|
|
27
|
+
const { fps, durationInFrames } = useVideoConfig();
|
|
28
|
+
const reduced = opts.reduced ?? false;
|
|
29
|
+
|
|
30
|
+
const easing = EASE[spec.easing ?? "smooth"];
|
|
31
|
+
const delay = spec.delay ?? 0;
|
|
32
|
+
const enterDur = spec.durationInFrames ?? Math.round(fps * 0.55);
|
|
33
|
+
|
|
34
|
+
const ep = interpolate(frame, [delay, delay + enterDur], [0, 1], {
|
|
35
|
+
extrapolateLeft: "clamp",
|
|
36
|
+
extrapolateRight: "clamp",
|
|
37
|
+
easing,
|
|
38
|
+
});
|
|
39
|
+
const enterFrag = enterStyle(spec.enter ?? "rise", ep, { distance: spec.distance, reduced });
|
|
40
|
+
|
|
41
|
+
let opacity = enterFrag.opacity;
|
|
42
|
+
const transforms: string[] = [];
|
|
43
|
+
if (enterFrag.transform !== "none") transforms.push(enterFrag.transform);
|
|
44
|
+
|
|
45
|
+
if (spec.exit) {
|
|
46
|
+
const exitDur = Math.round(fps * 0.4);
|
|
47
|
+
const xStart = durationInFrames - exitDur;
|
|
48
|
+
const xp = interpolate(frame, [xStart, durationInFrames], [0, 1], {
|
|
49
|
+
extrapolateLeft: "clamp",
|
|
50
|
+
extrapolateRight: "clamp",
|
|
51
|
+
easing: EASE.smooth,
|
|
52
|
+
});
|
|
53
|
+
const exitFrag = exitStyle(spec.exit, xp, { reduced });
|
|
54
|
+
opacity *= exitFrag.opacity;
|
|
55
|
+
if (exitFrag.transform !== "none") transforms.push(exitFrag.transform);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
opacity,
|
|
60
|
+
transform: transforms.length ? transforms.join(" ") : undefined,
|
|
61
|
+
filter: enterFrag.filter,
|
|
62
|
+
};
|
|
63
|
+
};
|
package/src/prepare.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { changelogSchema, DIMENSIONS, type ChangelogVideo } from "./schema/beats";
|
|
2
|
+
import { tokenize } from "./code/highlight";
|
|
3
|
+
import { waitForFonts } from "./brand/fonts";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Pre-render preparation, shared by Root's `calculateMetadata` and the render
|
|
7
|
+
* script: validate against the schema, pre-tokenize code via shiki (off the hot
|
|
8
|
+
* path), compute duration + dimensions from the format, and load fonts.
|
|
9
|
+
*/
|
|
10
|
+
export async function prepareChangelog(raw: unknown): Promise<{
|
|
11
|
+
durationInFrames: number;
|
|
12
|
+
width: number;
|
|
13
|
+
height: number;
|
|
14
|
+
props: ChangelogVideo;
|
|
15
|
+
}> {
|
|
16
|
+
const video = changelogSchema.parse(raw);
|
|
17
|
+
|
|
18
|
+
const beats = await Promise.all(
|
|
19
|
+
video.beats.map(async (beat) => {
|
|
20
|
+
if (beat.code && !beat.code.tokens) {
|
|
21
|
+
const tokens = await tokenize(beat.code.source, beat.code.lang);
|
|
22
|
+
return { ...beat, code: { ...beat.code, tokens } };
|
|
23
|
+
}
|
|
24
|
+
return beat;
|
|
25
|
+
})
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
const durationInFrames = beats.reduce((n, b) => n + b.durationInFrames, 0);
|
|
29
|
+
const { width, height } = DIMENSIONS[video.format];
|
|
30
|
+
await waitForFonts();
|
|
31
|
+
|
|
32
|
+
return { durationInFrames, width, height, props: { ...video, beats } };
|
|
33
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { ENTER_PRESETS, EXIT_PRESETS } from "../motion/names";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The data contract for a changelog video. Authored as a `.ts` module (for types
|
|
6
|
+
* + comments) but **100% JSON-serializable** — every motion/panel/background is a
|
|
7
|
+
* string key resolved at render time via registries. `scripts/render.ts` parses
|
|
8
|
+
* a beats module with this schema, serializes to JSON, and feeds it as Remotion
|
|
9
|
+
* input props.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const enterEnum = z.enum(ENTER_PRESETS);
|
|
13
|
+
const exitEnum = z.enum(EXIT_PRESETS);
|
|
14
|
+
|
|
15
|
+
export const motionSchema = z
|
|
16
|
+
.object({
|
|
17
|
+
enter: enterEnum.optional(),
|
|
18
|
+
exit: exitEnum.optional(),
|
|
19
|
+
easing: z.enum(["smooth", "snappy"]).optional(),
|
|
20
|
+
delay: z.number().optional(),
|
|
21
|
+
durationInFrames: z.number().optional(),
|
|
22
|
+
distance: z.number().optional(),
|
|
23
|
+
})
|
|
24
|
+
.strict();
|
|
25
|
+
|
|
26
|
+
export const formatSchema = z.enum(["16x9", "1x1", "9x16"]);
|
|
27
|
+
|
|
28
|
+
const codeTokenSchema = z.object({ content: z.string(), color: z.string() });
|
|
29
|
+
|
|
30
|
+
export const codeSchema = z
|
|
31
|
+
.object({
|
|
32
|
+
filename: z.string(),
|
|
33
|
+
lang: z.enum(["ts", "tsx", "bash", "json"]),
|
|
34
|
+
source: z.string(),
|
|
35
|
+
theme: z.literal("light").default("light"),
|
|
36
|
+
motion: motionSchema.optional(),
|
|
37
|
+
/** Filled by calculateMetadata via shiki — do not author by hand. */
|
|
38
|
+
tokens: z.array(z.array(codeTokenSchema)).optional(),
|
|
39
|
+
})
|
|
40
|
+
.strict();
|
|
41
|
+
|
|
42
|
+
export const panelSchema = z.discriminatedUnion("kind", [
|
|
43
|
+
z.object({
|
|
44
|
+
kind: z.literal("feed"),
|
|
45
|
+
title: z.string().default("feed"),
|
|
46
|
+
subtitle: z.string().optional(),
|
|
47
|
+
status: z.string().default("live…"),
|
|
48
|
+
rows: z.array(z.object({ badge: z.string(), label: z.string(), value: z.string() })),
|
|
49
|
+
motion: motionSchema.optional(),
|
|
50
|
+
}),
|
|
51
|
+
z.object({
|
|
52
|
+
kind: z.literal("upload-progress"),
|
|
53
|
+
file: z.string(),
|
|
54
|
+
sizeMB: z.number(),
|
|
55
|
+
parts: z.number(),
|
|
56
|
+
motion: motionSchema.optional(),
|
|
57
|
+
}),
|
|
58
|
+
z.object({
|
|
59
|
+
kind: z.literal("data-table"),
|
|
60
|
+
title: z.string().optional(),
|
|
61
|
+
columns: z.array(z.string()),
|
|
62
|
+
rows: z.array(z.array(z.string())),
|
|
63
|
+
motion: motionSchema.optional(),
|
|
64
|
+
}),
|
|
65
|
+
z.object({
|
|
66
|
+
kind: z.literal("status"),
|
|
67
|
+
title: z.string().default("status"),
|
|
68
|
+
services: z.array(z.object({ name: z.string(), state: z.enum(["ok", "syncing", "error", "idle"]), detail: z.string().optional() })),
|
|
69
|
+
motion: motionSchema.optional(),
|
|
70
|
+
}),
|
|
71
|
+
z.object({
|
|
72
|
+
kind: z.literal("proof"),
|
|
73
|
+
eventLine: z.string(),
|
|
74
|
+
cursor: z.string(),
|
|
75
|
+
signature: z.string(),
|
|
76
|
+
keyId: z.string(),
|
|
77
|
+
motion: motionSchema.optional(),
|
|
78
|
+
}),
|
|
79
|
+
z.object({
|
|
80
|
+
kind: z.literal("stream-resume"),
|
|
81
|
+
fromCursor: z.string(),
|
|
82
|
+
rows: z.array(z.object({ cursor: z.string(), label: z.string() })),
|
|
83
|
+
motion: motionSchema.optional(),
|
|
84
|
+
}),
|
|
85
|
+
z.object({
|
|
86
|
+
kind: z.literal("fork"),
|
|
87
|
+
blocks: z.array(z.object({ height: z.number(), hash: z.string(), state: z.enum(["canonical", "orphaned", "new"]) })),
|
|
88
|
+
rewindTo: z.string(),
|
|
89
|
+
motion: motionSchema.optional(),
|
|
90
|
+
}),
|
|
91
|
+
z.object({
|
|
92
|
+
kind: z.literal("stat"),
|
|
93
|
+
value: z.string(),
|
|
94
|
+
label: z.string(),
|
|
95
|
+
sub: z.string().optional(),
|
|
96
|
+
motion: motionSchema.optional(),
|
|
97
|
+
}),
|
|
98
|
+
z.object({
|
|
99
|
+
kind: z.literal("diagram"),
|
|
100
|
+
nodes: z.array(z.object({ id: z.string(), label: z.string(), type: z.enum(["default", "data", "api"]).default("default") })),
|
|
101
|
+
edges: z.array(z.object({ from: z.string(), to: z.string(), label: z.string().optional() })),
|
|
102
|
+
note: z.string().optional(),
|
|
103
|
+
motion: motionSchema.optional(),
|
|
104
|
+
}),
|
|
105
|
+
]);
|
|
106
|
+
|
|
107
|
+
export const beatSchema = z
|
|
108
|
+
.object({
|
|
109
|
+
id: z.string(),
|
|
110
|
+
durationInFrames: z.number().int().positive(),
|
|
111
|
+
/** Omit for the default procedural, theme-colored backdrop (the "shapes" pack). */
|
|
112
|
+
background: z
|
|
113
|
+
.object({
|
|
114
|
+
/** A painting/image in public/ (the optional painterly style pack). */
|
|
115
|
+
src: z.string().optional(),
|
|
116
|
+
treatment: z.enum(["kenburns", "static"]).default("kenburns"),
|
|
117
|
+
/** AI-free packs: a [from, to] gradient or a solid color. */
|
|
118
|
+
gradient: z.tuple([z.string(), z.string()]).optional(),
|
|
119
|
+
angle: z.number().default(160),
|
|
120
|
+
solid: z.string().optional(),
|
|
121
|
+
/** Procedural theme-colored backdrop (no asset, no API key) — the default. */
|
|
122
|
+
shapes: z.boolean().optional(),
|
|
123
|
+
})
|
|
124
|
+
.refine((b) => b.src || b.gradient || b.solid || b.shapes, "background needs src, gradient, solid, or shapes")
|
|
125
|
+
.optional(),
|
|
126
|
+
eyebrow: z.string().optional(),
|
|
127
|
+
headline: z.string(),
|
|
128
|
+
headlineMotion: motionSchema.optional(),
|
|
129
|
+
/** Optional sub-line pinned bottom-center — e.g. an install closer's tagline. */
|
|
130
|
+
caption: z.string().optional(),
|
|
131
|
+
/** Optional gold version pill shown with the caption — e.g. "v1.0". */
|
|
132
|
+
badge: z.string().optional(),
|
|
133
|
+
layout: z.enum(["split", "center"]).default("split"),
|
|
134
|
+
code: codeSchema.optional(),
|
|
135
|
+
panel: panelSchema.optional(),
|
|
136
|
+
})
|
|
137
|
+
.strict();
|
|
138
|
+
|
|
139
|
+
export const changelogSchema = z
|
|
140
|
+
.object({
|
|
141
|
+
format: formatSchema.default("16x9"),
|
|
142
|
+
beats: z.array(beatSchema).min(1),
|
|
143
|
+
/** Reserved — silent render for now. */
|
|
144
|
+
audio: z.object({ music: z.string().optional(), vo: z.string().optional() }).optional(),
|
|
145
|
+
})
|
|
146
|
+
.strict();
|
|
147
|
+
|
|
148
|
+
export type MotionSpecData = z.infer<typeof motionSchema>;
|
|
149
|
+
export type Format = z.infer<typeof formatSchema>;
|
|
150
|
+
export type CodeSpec = z.infer<typeof codeSchema>;
|
|
151
|
+
export type PanelSpec = z.infer<typeof panelSchema>;
|
|
152
|
+
export type Beat = z.infer<typeof beatSchema>;
|
|
153
|
+
export type ChangelogVideo = z.infer<typeof changelogSchema>;
|
|
154
|
+
/** Authoring type (defaults optional) — used by `content/*.beats.ts`. */
|
|
155
|
+
export type ChangelogInput = z.input<typeof changelogSchema>;
|
|
156
|
+
|
|
157
|
+
export const DIMENSIONS: Record<Format, { width: number; height: number }> = {
|
|
158
|
+
"16x9": { width: 1920, height: 1080 },
|
|
159
|
+
"1x1": { width: 1080, height: 1080 },
|
|
160
|
+
"9x16": { width: 1080, height: 1920 },
|
|
161
|
+
};
|