givemotion 0.0.0-stage → 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.
Files changed (132) hide show
  1. package/AGENTS.md +37 -0
  2. package/LICENSE +21 -0
  3. package/README.md +492 -2
  4. package/SECURITY.md +40 -0
  5. package/THIRD_PARTY_NOTICES.md +48 -0
  6. package/blocks/bar-chart.html +63 -0
  7. package/blocks/browser-window.html +52 -0
  8. package/blocks/callout.html +57 -0
  9. package/blocks/captions.html +120 -0
  10. package/blocks/checklist.html +32 -0
  11. package/blocks/code-window.html +126 -0
  12. package/blocks/compare.html +33 -0
  13. package/blocks/cursor.html +157 -0
  14. package/blocks/end-card.html +28 -0
  15. package/blocks/gradient-bg.html +48 -0
  16. package/blocks/grid-bg.html +18 -0
  17. package/blocks/highlight.html +13 -0
  18. package/blocks/line-chart.html +36 -0
  19. package/blocks/logo-reveal.html +33 -0
  20. package/blocks/lottie.html +34 -0
  21. package/blocks/lower-third.html +24 -0
  22. package/blocks/phone.html +18 -0
  23. package/blocks/quote.html +19 -0
  24. package/blocks/stat.html +24 -0
  25. package/blocks/steps.html +40 -0
  26. package/blocks/title-card.html +22 -0
  27. package/blocks/word-pop.html +40 -0
  28. package/examples/bench/index.html +24 -0
  29. package/examples/data-story/data.json +9 -0
  30. package/examples/data-story/index.html +361 -0
  31. package/examples/data-story/other.json +46 -0
  32. package/examples/generative/index.html +60 -0
  33. package/examples/made-by-agents/README.md +5 -0
  34. package/examples/made-by-agents/photosynthesis/blocks/checklist.html +32 -0
  35. package/examples/made-by-agents/photosynthesis/blocks/end-card.html +25 -0
  36. package/examples/made-by-agents/photosynthesis/blocks/gradient-bg.html +48 -0
  37. package/examples/made-by-agents/photosynthesis/blocks/steps.html +40 -0
  38. package/examples/made-by-agents/photosynthesis/blocks/title-card.html +22 -0
  39. package/examples/made-by-agents/photosynthesis/index.html +217 -0
  40. package/examples/personalized/index.html +28 -0
  41. package/llms.txt +34 -0
  42. package/package.json +80 -4
  43. package/skills/givemotion/SKILL.md +152 -0
  44. package/skills/givemotion/references/api.md +40 -0
  45. package/skills/givemotion/references/camera.md +72 -0
  46. package/skills/givemotion/references/charts.md +138 -0
  47. package/skills/givemotion/references/check-codes.md +94 -0
  48. package/skills/givemotion/references/data-templates.md +61 -0
  49. package/skills/givemotion/references/diagrams.md +75 -0
  50. package/skills/givemotion/references/genres/code-explainer.md +15 -0
  51. package/skills/givemotion/references/genres/data-story.md +21 -0
  52. package/skills/givemotion/references/genres/explainer.md +20 -0
  53. package/skills/givemotion/references/genres/hand-drawn.md +18 -0
  54. package/skills/givemotion/references/genres/logo-sting.md +20 -0
  55. package/skills/givemotion/references/genres/promo.md +18 -0
  56. package/skills/givemotion/references/genres/ui-demo.md +21 -0
  57. package/skills/givemotion/references/genres/vertical.md +19 -0
  58. package/skills/givemotion/references/genres/year-recap.md +18 -0
  59. package/skills/givemotion/references/javascript.md +72 -0
  60. package/skills/givemotion/references/media-audio.md +57 -0
  61. package/skills/givemotion/references/motion-design.md +183 -0
  62. package/skills/givemotion/references/output.md +64 -0
  63. package/skills/givemotion/references/review.md +60 -0
  64. package/skills/givemotion/references/styles.md +133 -0
  65. package/skills/givemotion/references/techniques/backgrounds-and-ambient.md +85 -0
  66. package/skills/givemotion/references/techniques/camera-moves.md +88 -0
  67. package/skills/givemotion/references/techniques/captions-vertical.md +89 -0
  68. package/skills/givemotion/references/techniques/code-on-screen.md +125 -0
  69. package/skills/givemotion/references/techniques/hand-drawn.md +87 -0
  70. package/skills/givemotion/references/techniques/impacts-and-energy.md +106 -0
  71. package/skills/givemotion/references/techniques/kinetic-type.md +92 -0
  72. package/skills/givemotion/references/techniques/light-and-depth.md +91 -0
  73. package/skills/givemotion/references/techniques/logo-reveal.md +94 -0
  74. package/skills/givemotion/references/techniques/numbers-and-charts.md +92 -0
  75. package/skills/givemotion/references/techniques/paths-and-maps.md +99 -0
  76. package/skills/givemotion/references/techniques/scene-transitions.md +85 -0
  77. package/skills/givemotion/references/techniques/shapes-and-motion-graphics.md +92 -0
  78. package/skills/givemotion/references/techniques/text-reveals.md +94 -0
  79. package/skills/givemotion/references/techniques/ui-demo.md +110 -0
  80. package/skills/givemotion/references/timeline.md +85 -0
  81. package/src/analyze.js +1485 -0
  82. package/src/api.js +36 -0
  83. package/src/args.js +33 -0
  84. package/src/audio-cli.js +58 -0
  85. package/src/bench.js +153 -0
  86. package/src/browser.js +381 -0
  87. package/src/bundle.js +79 -0
  88. package/src/cdp.js +94 -0
  89. package/src/check.js +1818 -0
  90. package/src/cli.js +611 -0
  91. package/src/dev.js +26 -0
  92. package/src/ffmpeg.js +330 -0
  93. package/src/parts.js +380 -0
  94. package/src/player/gm-player.js +175 -0
  95. package/src/render.js +772 -0
  96. package/src/runtime/audio.js +306 -0
  97. package/src/runtime/chart.js +665 -0
  98. package/src/runtime/fonts/BebasNeue-OFL.txt +93 -0
  99. package/src/runtime/fonts/Caveat-OFL.txt +93 -0
  100. package/src/runtime/fonts/Inter-OFL.txt +93 -0
  101. package/src/runtime/fonts/JetBrainsMono-OFL.txt +93 -0
  102. package/src/runtime/fonts/Nunito-OFL.txt +93 -0
  103. package/src/runtime/fonts/PlayfairDisplay-OFL.txt +93 -0
  104. package/src/runtime/fonts/SpaceGrotesk-OFL.txt +93 -0
  105. package/src/runtime/fonts/bebas-neue-latin-400-normal.woff2 +0 -0
  106. package/src/runtime/fonts/bebas-neue-latin-ext-400-normal.woff2 +0 -0
  107. package/src/runtime/fonts/caveat-latin-ext-wght-normal.woff2 +0 -0
  108. package/src/runtime/fonts/caveat-latin-wght-normal.woff2 +0 -0
  109. package/src/runtime/fonts/inter-latin-ext-wght-italic.woff2 +0 -0
  110. package/src/runtime/fonts/inter-latin-ext-wght-normal.woff2 +0 -0
  111. package/src/runtime/fonts/inter-latin-wght-italic.woff2 +0 -0
  112. package/src/runtime/fonts/inter-latin-wght-normal.woff2 +0 -0
  113. package/src/runtime/fonts/jetbrains-mono-latin-ext-wght-italic.woff2 +0 -0
  114. package/src/runtime/fonts/jetbrains-mono-latin-ext-wght-normal.woff2 +0 -0
  115. package/src/runtime/fonts/jetbrains-mono-latin-wght-italic.woff2 +0 -0
  116. package/src/runtime/fonts/jetbrains-mono-latin-wght-normal.woff2 +0 -0
  117. package/src/runtime/fonts/nunito-latin-ext-wght-normal.woff2 +0 -0
  118. package/src/runtime/fonts/nunito-latin-wght-normal.woff2 +0 -0
  119. package/src/runtime/fonts/playfair-display-latin-ext-wght-italic.woff2 +0 -0
  120. package/src/runtime/fonts/playfair-display-latin-ext-wght-normal.woff2 +0 -0
  121. package/src/runtime/fonts/playfair-display-latin-wght-italic.woff2 +0 -0
  122. package/src/runtime/fonts/playfair-display-latin-wght-normal.woff2 +0 -0
  123. package/src/runtime/fonts/space-grotesk-latin-ext-wght-normal.woff2 +0 -0
  124. package/src/runtime/fonts/space-grotesk-latin-wght-normal.woff2 +0 -0
  125. package/src/runtime/motion.js +2674 -0
  126. package/src/runtime/preset.css +97 -0
  127. package/src/runtime/runtime.js +2141 -0
  128. package/src/runtime/sketch.js +1249 -0
  129. package/src/server.js +402 -0
  130. package/src/studio/index.html +238 -0
  131. package/src/types/api.d.ts +354 -0
  132. package/templates/starter/index.html +70 -0
@@ -0,0 +1,9 @@
1
+ {
2
+ "title": "How we grew this year",
3
+ "weeks": ["Jan", "Mar", "May", "Jul", "Sep", "Nov"],
4
+ "users": [1200, 1850, 1700, 2600, 3400, 4720],
5
+ "growth": "3.9×",
6
+ "quarters": ["Q1", "Q2", "Q3", "Q4"],
7
+ "revenue": [180, 240, 310, 455],
8
+ "nps": 72
9
+ }
@@ -0,0 +1,361 @@
1
+ <!doctype html>
2
+ <html data-width="1920" data-height="1080" data-fps="30">
3
+ <head>
4
+ <style>
5
+ :root {
6
+ --bg: #090d1c;
7
+ --ink: #f3f5fb;
8
+ --muted: rgba(214, 222, 255, .62);
9
+ --teal: #4fe3c1;
10
+ --violet: #8f7bff;
11
+ --amber: #ffb84d;
12
+ }
13
+ body { background: var(--bg); color: var(--ink); font-family: Inter, system-ui, sans-serif; margin: 0; overflow: hidden; }
14
+
15
+ /* ---------- living background (always on screen) ---------- */
16
+ .bg { position: absolute; inset: 0; overflow: hidden; }
17
+ .blob { position: absolute; border-radius: 50%; filter: blur(110px); opacity: .55; }
18
+ .blob.b1 { width: 900px; height: 900px; left: -200px; top: -300px; background: #1d3a7a; animation: drift1 15s ease-in-out both; }
19
+ .blob.b2 { width: 800px; height: 800px; right: -250px; bottom: -350px; background: #3b1f78; animation: drift2 15s ease-in-out both; }
20
+ .blob.b3 { width: 500px; height: 500px; left: 45%; top: 55%; background: #0f4d5a; opacity: .45; animation: drift3 15s ease-in-out both; }
21
+ @keyframes drift1 { to { translate: 380px 220px; } }
22
+ @keyframes drift2 { to { translate: -420px -160px; } }
23
+ @keyframes drift3 { from { translate: -300px 0; } to { translate: 260px -380px; } }
24
+ .grid {
25
+ position: absolute; inset: -60px;
26
+ background-image: linear-gradient(rgba(255,255,255,.045) 1px, transparent 1px), linear-gradient(90deg, rgba(255,255,255,.045) 1px, transparent 1px);
27
+ background-size: 80px 80px;
28
+ mask-image: radial-gradient(ellipse at center, #000 30%, transparent 75%);
29
+ animation: gridmove 15s linear both;
30
+ }
31
+ @keyframes gridmove { to { translate: -80px -40px; } }
32
+
33
+ .eyebrow { font-size: 30px; font-weight: 600; letter-spacing: .18em; text-transform: uppercase; color: var(--muted); }
34
+
35
+ /* ---------- 1. title ---------- */
36
+ #intro { display: grid; place-items: center; }
37
+ #intro .spark { position: absolute; left: 0; top: 0; width: 1920px; height: 1080px; overflow: visible; }
38
+ #intro .spark path { fill: none; stroke: var(--teal); stroke-width: 6; stroke-linecap: round; stroke-linejoin: round; opacity: .35; }
39
+ #intro .spark circle { fill: var(--teal); }
40
+ #intro .center { position: relative; text-align: center; display: flex; flex-direction: column; align-items: center; gap: 34px; }
41
+ #intro h1 { font-family: 'Inter Display', Inter, sans-serif; font-size: 156px; font-weight: 800; letter-spacing: -.045em; line-height: .98; margin: 0; max-width: 1500px; text-wrap: balance; }
42
+ #intro h1 .gm-word:last-child { color: var(--teal); }
43
+ #intro .range { display: flex; align-items: center; gap: 22px; font-size: 40px; font-weight: 600; color: var(--muted); }
44
+ #intro .range i { display: block; width: 120px; height: 3px; background: linear-gradient(90deg, var(--teal), var(--violet)); border-radius: 3px; transform-origin: left; }
45
+
46
+ /* ---------- 2. users line chart ---------- */
47
+ #users .hud { position: absolute; left: 120px; top: 84px; z-index: 2; }
48
+ #users .readout { font-family: 'Inter Display', Inter, sans-serif; font-size: 132px; font-weight: 800; letter-spacing: -.04em; line-height: 1; margin-top: 10px; font-variant-numeric: tabular-nums; }
49
+ #users .readout small { font-size: 40px; font-weight: 600; color: var(--muted); letter-spacing: 0; margin-left: 18px; }
50
+ #users .growth { position: absolute; right: 120px; top: 84px; text-align: right; z-index: 2; }
51
+ #users .growth b { display: block; font-family: 'Inter Display', Inter, sans-serif; font-size: 190px; font-weight: 900; letter-spacing: -.05em; line-height: .9;
52
+ background: linear-gradient(180deg, #fff 20%, var(--teal)); -webkit-background-clip: text; background-clip: text; color: transparent; }
53
+ #users .growth span { font-size: 34px; font-weight: 600; color: var(--muted); }
54
+ #world { position: absolute; left: 0; top: 0; width: 1920px; height: 1080px; }
55
+ #chart { position: absolute; left: 0; top: 0; width: 1920px; height: 1080px; overflow: visible; }
56
+ #chart .gl { stroke: rgba(255,255,255,.09); stroke-width: 2; }
57
+ #chart .yl { fill: rgba(214,222,255,.5); font-size: 26px; font-weight: 500; }
58
+ #chart .xl { fill: rgba(214,222,255,.75); font-size: 30px; font-weight: 600; text-anchor: middle; }
59
+ #chart .line { fill: none; stroke: var(--teal); stroke-width: 8; stroke-linecap: round; stroke-linejoin: round; filter: drop-shadow(0 0 18px rgba(79,227,193,.55)); }
60
+ #chart .dot { fill: var(--bg); stroke: var(--teal); stroke-width: 6; }
61
+ #chart .ring { fill: none; stroke: var(--teal); stroke-width: 4; opacity: 0; }
62
+ #tip { position: absolute; left: 0; top: 0; width: 34px; height: 34px; margin: -17px 0 0 -17px; border-radius: 50%; background: #fff;
63
+ box-shadow: 0 0 0 10px rgba(79,227,193,.35), 0 0 50px 10px rgba(79,227,193,.6); }
64
+
65
+ /* ---------- 3. revenue bars ---------- */
66
+ #revenue .hud { position: absolute; left: 120px; top: 84px; }
67
+ #revenue .stage { position: absolute; inset: 0; }
68
+ #revenue h2 small { font-size: 40px; font-weight: 600; color: var(--muted); letter-spacing: 0; margin-left: 18px; }
69
+ #revenue h2 { font-family: 'Inter Display', Inter, sans-serif; font-size: 96px; font-weight: 800; letter-spacing: -.035em; margin: 10px 0 0; line-height: 1; }
70
+ #bars { position: absolute; left: 0; top: 0; width: 1920px; height: 1080px; }
71
+ .bar { position: absolute; bottom: 0; border-radius: 22px 22px 6px 6px; transform-origin: 50% 100%;
72
+ background: linear-gradient(180deg, color-mix(in oklab, var(--violet), white 22%), color-mix(in oklab, var(--violet), #090d1c 35%)); }
73
+ .bar.dim { background: linear-gradient(180deg, rgba(143,123,255,.55), rgba(143,123,255,.18)); }
74
+ .bar.hl { box-shadow: 0 0 80px rgba(143,123,255,.55); }
75
+ .bval { position: absolute; text-align: center; font-family: 'Inter Display', Inter, sans-serif; font-size: 54px; font-weight: 800; letter-spacing: -.02em; font-variant-numeric: tabular-nums; }
76
+ .blbl { position: absolute; text-align: center; font-size: 34px; font-weight: 600; color: var(--muted); }
77
+ .baseline { position: absolute; height: 3px; background: rgba(255,255,255,.22); transform-origin: left; }
78
+ #trend { position: absolute; left: 0; top: 0; width: 1920px; height: 1080px; overflow: visible; pointer-events: none; }
79
+ #trend path { fill: none; stroke: var(--amber); stroke-width: 6; stroke-linecap: round; stroke-linejoin: round; }
80
+ .delta { position: absolute; z-index: 2; padding: 14px 30px; border-radius: 999px; background: var(--amber); color: #1b1203; font-size: 46px; font-weight: 800; letter-spacing: -.01em; white-space: nowrap; }
81
+
82
+ /* ---------- 4. NPS gauge ---------- */
83
+ #nps .gauge { position: absolute; left: 50%; top: 170px; width: 1100px; height: 640px; translate: -50% 0; overflow: visible; }
84
+ #nps .track { fill: none; stroke: rgba(255,255,255,.09); stroke-width: 64; stroke-linecap: round; }
85
+ #nps .fill { fill: none; stroke: url(#npsgrad); stroke-width: 64; stroke-linecap: round; }
86
+ #nps .tick { stroke: rgba(255,255,255,.25); stroke-width: 3; }
87
+ #nps .tl { fill: rgba(214,222,255,.55); font-size: 30px; font-weight: 600; text-anchor: middle; }
88
+ #knob { fill: #fff; filter: drop-shadow(0 0 18px rgba(255,255,255,.7)); }
89
+ #nps .readout { position: absolute; left: 0; right: 0; top: 470px; text-align: center; }
90
+ #nps .readout b { display: block; font-family: 'Inter Display', Inter, sans-serif; font-size: 230px; font-weight: 900; letter-spacing: -.05em; line-height: .9; font-variant-numeric: tabular-nums; }
91
+ #nps .readout span { display: block; margin-top: 18px; }
92
+ </style>
93
+ </head>
94
+ <body>
95
+
96
+ <div class="bg">
97
+ <div class="blob b1"></div><div class="blob b2"></div><div class="blob b3"></div>
98
+ <div class="grid"></div>
99
+ </div>
100
+
101
+ <main data-sequence>
102
+ <!-- 1. Title -->
103
+ <section id="intro" class="scene" data-duration="3.3" data-camera="push 1.07">
104
+ <svg class="spark" viewBox="0 0 1920 1080"><path id="sparkPath" pathLength="1"/><circle id="sparkDot" r="12"/></svg>
105
+ <div class="center">
106
+ <div class="range"><span id="rangeFrom"></span><i></i><span id="rangeTo"></span></div>
107
+ <h1 data-split="words" data-enter="mask-up 0.9 0.12" data-stagger="0.07">{{data.title}}</h1>
108
+ </div>
109
+ </section>
110
+
111
+ <!-- 2. Users -->
112
+ <section id="users" class="scene" data-duration="6" data-transition="zoom-through 0.7">
113
+ <div id="world">
114
+ <svg id="chart" viewBox="0 0 1920 1080"></svg>
115
+ <div id="tip"></div>
116
+ </div>
117
+ <div class="hud">
118
+ <div class="eyebrow">{{data.usersLabel|Monthly users}}</div>
119
+ <div class="readout"><span id="readNum">0</span><small id="readLbl"></small></div>
120
+ </div>
121
+ <div class="growth"><b>{{data.growth}}</b><span>{{data.growthLabel|growth}}</span></div>
122
+ </section>
123
+
124
+ <!-- 3. Revenue -->
125
+ <section id="revenue" class="scene" data-duration="4.4" data-transition="whip-left 0.5">
126
+ <div class="stage" data-camera="push 1.06, float 8">
127
+ <div id="bars"></div>
128
+ <svg id="trend" viewBox="0 0 1920 1080"><path id="trendPath" pathLength="1"/><path id="trendHead" d="M-16,-14 L8,0 L-16,14"/></svg>
129
+ </div>
130
+ <div class="hud">
131
+ <div class="eyebrow">{{data.revenueLabel|Quarterly revenue}}</div>
132
+ <h2><span id="revTotal"></span><small>{{data.totalLabel|total}}</small></h2>
133
+ </div>
134
+ </section>
135
+
136
+ <!-- 4. NPS -->
137
+ <section id="nps" class="scene" data-duration="3.9" data-transition="push-up 0.6" data-camera="push 1.06">
138
+ <svg class="gauge" viewBox="0 0 1100 640">
139
+ <defs><linearGradient id="npsgrad" x1="0" x2="1" y1="0" y2="0"><stop offset="0" stop-color="#ff6b6b"/><stop offset=".5" stop-color="#ffb84d"/><stop offset="1" stop-color="#4fe3c1"/></linearGradient></defs>
140
+ <path class="track" id="npsTrack"/>
141
+ <path class="fill" id="npsFill" pathLength="1"/>
142
+ <g id="npsTicks"></g>
143
+ <circle id="knob" r="24"/>
144
+ </svg>
145
+ <div class="readout">
146
+ <b><span data-count="0 {{data.nps}}" data-count-duration="1.6" data-count-delay="0.5" data-count-ease="out-cubic"></span></b>
147
+ <span class="eyebrow">{{data.npsLabel|Net Promoter Score}}</span>
148
+ </div>
149
+ </section>
150
+ </main>
151
+
152
+ <script>
153
+ const D = gm.data || {};
154
+ const users = D.users || [], months = D.weeks || [], quarters = D.quarters || [], revenue = D.revenue || [];
155
+ const fmt = (v) => Math.round(v).toLocaleString('en-US');
156
+ const compact = (v) => v >= 1000 ? (v / 1000).toFixed(v % 1000 === 0 ? 0 : 1).replace(/\.0$/, '') + 'k' : String(Math.round(v));
157
+ const NS = 'http://www.w3.org/2000/svg';
158
+ const el = (tag, attrs, parent) => { const e = document.createElementNS(NS, tag); for (const k in attrs) e.setAttribute(k, attrs[k]); parent && parent.appendChild(e); return e; };
159
+ function smooth(pts) {
160
+ let d = `M${pts[0][0]},${pts[0][1]}`;
161
+ for (let i = 0; i < pts.length - 1; i++) {
162
+ const p0 = pts[i - 1] || pts[i], p1 = pts[i], p2 = pts[i + 1], p3 = pts[i + 2] || p2;
163
+ d += ` C${p1[0] + (p2[0] - p0[0]) / 6},${p1[1] + (p2[1] - p0[1]) / 6} ${p2[0] - (p3[0] - p1[0]) / 6},${p2[1] - (p3[1] - p1[1]) / 6} ${p2[0]},${p2[1]}`;
164
+ }
165
+ return d;
166
+ }
167
+ function niceStep(max, ticks) {
168
+ const raw = max / ticks, mag = Math.pow(10, Math.floor(Math.log10(raw)));
169
+ const n = raw / mag;
170
+ return (n <= 1 ? 1 : n <= 2 ? 2 : n <= 2.5 ? 2.5 : n <= 5 ? 5 : 10) * mag;
171
+ }
172
+
173
+ /* ---------------- 1. Title ---------------- */
174
+ document.getElementById('rangeFrom').textContent = months[0] || '';
175
+ document.getElementById('rangeTo').textContent = months[months.length - 1] || '';
176
+ {
177
+ const n = users.length, max = Math.max(...users), min = Math.min(...users);
178
+ const pts = users.map((v, i) => [-40 + i * 2000 / Math.max(1, n - 1), 960 - (v - min) / Math.max(1, max - min) * 260]);
179
+ document.getElementById('sparkPath').setAttribute('d', smooth(pts));
180
+ }
181
+ // (gm.path.travel does not move SVG elements, so the dot is placed by hand)
182
+ const sparkD = document.getElementById('sparkPath').getAttribute('d'), sparkDot = document.getElementById('sparkDot');
183
+ gm.onFrame('#intro', (t) => {
184
+ const p = gm.interpolate(t, [0.2, 2.6], [0, 1], { easing: 'in-out-sine' });
185
+ document.getElementById('sparkPath').style.strokeDasharray = p + ' 2';
186
+ const q = gm.path.point(sparkD, p);
187
+ sparkDot.setAttribute('cx', q.x); sparkDot.setAttribute('cy', q.y);
188
+ });
189
+ gm.timeline('#intro')
190
+ .from('.range span, .range i', { autoAlpha: 0, y: 30, stagger: 0.08, duration: 0.7, ease: 'expo.out' }, 0)
191
+ .from('.range i', { scaleX: 0, duration: 0.8, ease: 'expo.out' }, 0.25);
192
+
193
+ /* ---------------- 2. Users line chart ---------------- */
194
+ const CH = { x0: 260, x1: 1680, y0: 420, y1: 900 }; // plot box inside the world
195
+ const uMax = Math.max(...users), step = niceStep(uMax, 5), yTop = Math.ceil(uMax * 1.05 / step) * step;
196
+ const ux = (i) => CH.x0 + i * (CH.x1 - CH.x0) / Math.max(1, users.length - 1);
197
+ const uy = (v) => CH.y1 - v / yTop * (CH.y1 - CH.y0);
198
+ const chart = document.getElementById('chart');
199
+ const gGrid = el('g', {}, chart);
200
+ for (let v = 0; v <= yTop + 1e-9; v += step) {
201
+ el('line', { class: 'gl', x1: CH.x0 - 40, x2: CH.x1 + 40, y1: uy(v), y2: uy(v) }, gGrid);
202
+ el('text', { class: 'yl', x: CH.x0 - 70, y: uy(v) + 9, 'text-anchor': 'end' }, gGrid).textContent = compact(v);
203
+ }
204
+ const upts = users.map((v, i) => [ux(i), uy(v)]);
205
+ const ud = smooth(upts);
206
+ const defs = el('defs', {}, chart);
207
+ defs.innerHTML = `<linearGradient id="area" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#4fe3c1" stop-opacity=".32"/><stop offset="1" stop-color="#4fe3c1" stop-opacity="0"/></linearGradient>
208
+ <clipPath id="areaClip"><rect id="areaRect" x="0" y="0" width="0" height="1080"/></clipPath>`;
209
+ el('path', { d: `${ud} L${CH.x1},${CH.y1} L${CH.x0},${CH.y1} Z`, fill: 'url(#area)', 'clip-path': 'url(#areaClip)' }, chart);
210
+ const line = el('path', { class: 'line', id: 'uline', d: ud, pathLength: 1 }, chart);
211
+ const ring = el('circle', { class: 'ring', id: 'lastRing', cx: upts[upts.length - 1][0], cy: upts[upts.length - 1][1], r: 26 }, chart);
212
+ users.forEach((v, i) => {
213
+ el('circle', { class: 'dot', id: 'ud' + i, cx: upts[i][0], cy: upts[i][1], r: 13 }, chart);
214
+ el('text', { class: 'xl', id: 'ux' + i, x: upts[i][0], y: CH.y1 + 62 }, chart).textContent = months[i] || '';
215
+ });
216
+
217
+ const T0 = 0.7, T1 = 3.5; // line draws from T0 to T1 (scene time)
218
+ const trip = gm.path.travel('#tip', '#uline', { stops: [[T0, 0], [T1, 1]], ease: 'in-out-sine' });
219
+ // when does the tip pass each point? (sampled once, deterministic)
220
+ const passTime = upts.map(([x]) => {
221
+ // NOTE: trip.progress(t) ignores the ease when called at load time, so use the same curve via gm.interpolate
222
+ for (let t = T0; t <= T1; t += 1 / 120) if (gm.path.point(ud, gm.interpolate(t, [T0, T1], [0, 1], { easing: 'in-out-sine' })).x >= x - 0.5) return t;
223
+ return T1;
224
+ });
225
+ const readNum = document.getElementById('readNum'), readLbl = document.getElementById('readLbl');
226
+ gm.onFrame('#users', (t) => {
227
+ const p = trip.progress(t);
228
+ line.style.strokeDasharray = p + ' 2';
229
+ const x = gm.path.point(ud, p).x;
230
+ document.getElementById('areaRect').setAttribute('width', x);
231
+ // live readout = value under the tip
232
+ const f = Math.max(0, Math.min(users.length - 1, (x - CH.x0) / ((CH.x1 - CH.x0) / Math.max(1, users.length - 1))));
233
+ const i = Math.floor(f), frac = f - i;
234
+ const v = i >= users.length - 1 ? users[users.length - 1] : users[i] + (users[i + 1] - users[i]) * frac;
235
+ readNum.textContent = fmt(v);
236
+ readLbl.textContent = months[Math.round(f)] || '';
237
+ });
238
+
239
+ const tl2 = gm.timeline('#users')
240
+ .from('.hud .eyebrow', { autoAlpha: 0, x: -40, duration: 0.6, ease: 'expo.out' }, 0.35)
241
+ .from('.readout', { autoAlpha: 0, y: 40, duration: 0.7, ease: 'expo.out' }, 0.45)
242
+ .from('#chart .gl', { scaleX: 0, transformOrigin: '0% 50%', stagger: 0.06, duration: 0.9, ease: 'expo.out' }, 0.2)
243
+ .from('#chart .yl', { autoAlpha: 0, x: -20, stagger: 0.06, duration: 0.5 }, T1 - 0.2)
244
+ .from('#tip', { scale: 0, duration: 0.5, ease: 'back.out(3)' }, T0 - 0.3);
245
+ users.forEach((_, i) => {
246
+ tl2.from('#ud' + i, { scale: 0, duration: 0.5, ease: 'back.out(4)' }, passTime[i] - 0.05)
247
+ .from('#ux' + i, { autoAlpha: 0, y: 24, duration: 0.5, ease: 'expo.out' }, T1 - 0.3 + i * 0.05);
248
+ });
249
+ tl2.to('#tip', { scale: 0, duration: 0.3, ease: 'power2.in' }, T1 + 0.05)
250
+ .fromTo('#lastRing', { scale: 0.5, autoAlpha: 1 }, { scale: 3, autoAlpha: 0, duration: 1.1, ease: 'expo.out', immediateRender: false }, T1 + 0.05)
251
+ .fromTo('.readout', { scale: 1 }, { keyframes: [{ scale: 1.12, duration: 0.15 }, { scale: 1, duration: 0.5, ease: 'back.out(3)' }], transformOrigin: '0% 50%' }, T1)
252
+ .from('.growth b', { scale: 2.6, autoAlpha: 0, rotate: -8, duration: 0.55, ease: 'expo.in' }, T1 + 0.2)
253
+ .from('.growth span', { autoAlpha: 0, y: 20, duration: 0.5, ease: 'expo.out' }, T1 + 0.45);
254
+
255
+ gm.camera('#world')
256
+ .shot(0, { follow: '#tip', zoom: 1.9, offset: [120, -60] })
257
+ .shot(T1 - 0.6, { fit: { x: 0, y: 0, w: 1920, h: 1080 }, duration: 1.2, ease: 'in-out-cubic' })
258
+ .shot(T1 + 0.6, { fit: { x: 0, y: 0, w: 1920, h: 1080 }, zoom: 1.05, offset: [60, 40], duration: 0.9, ease: 'in-out-sine' })
259
+ .shake(T1 + 0.72, { duration: 0.35, amount: 12 });
260
+
261
+ /* ---------------- 3. Revenue bars ---------------- */
262
+ const BX = { x0: 200, x1: 1720, base: 930, h: 430 };
263
+ const bars = document.getElementById('bars');
264
+ const rN = revenue.length, rMax = Math.max(...revenue);
265
+ const gap = Math.min(70, (BX.x1 - BX.x0) / rN * 0.3);
266
+ const bw = Math.min(260, ((BX.x1 - BX.x0) - gap * (rN - 1)) / rN);
267
+ const totalW = bw * rN + gap * (rN - 1), bx0 = (BX.x0 + BX.x1) / 2 - totalW / 2;
268
+ const hlIdx = revenue.indexOf(rMax);
269
+ const pre = D.revenuePrefix != null ? D.revenuePrefix : '', suf = D.revenueSuffix != null ? D.revenueSuffix : '';
270
+ const base = document.createElement('div');
271
+ base.className = 'baseline';
272
+ Object.assign(base.style, { left: bx0 - 40 + 'px', width: totalW + 80 + 'px', top: BX.base + 'px' });
273
+ bars.appendChild(base);
274
+ const tops = [];
275
+ revenue.forEach((v, i) => {
276
+ const h = v / rMax * BX.h, x = bx0 + i * (bw + gap);
277
+ tops.push([x + bw / 2, BX.base - h]);
278
+ const b = document.createElement('div');
279
+ b.className = 'bar ' + (i === hlIdx ? 'hl' : 'dim');
280
+ b.id = 'bar' + i;
281
+ Object.assign(b.style, { left: x + 'px', width: bw + 'px', height: h + 'px', top: BX.base - h + 'px', bottom: 'auto' });
282
+ bars.appendChild(b);
283
+ const val = document.createElement('div');
284
+ val.className = 'bval'; val.id = 'bv' + i;
285
+ Object.assign(val.style, { left: x - 40 + 'px', width: bw + 80 + 'px', top: BX.base - h - 78 + 'px' });
286
+ val.innerHTML = `<span data-count="0 ${v}" data-count-duration="1" data-count-delay="${0.55 + i * 0.14}" data-count-prefix="${pre}" data-count-suffix="${suf}"></span>`;
287
+ bars.appendChild(val);
288
+ const lbl = document.createElement('div');
289
+ lbl.className = 'blbl'; lbl.id = 'bl' + i;
290
+ Object.assign(lbl.style, { left: x + 'px', width: bw + 'px', top: BX.base + 22 + 'px' });
291
+ lbl.textContent = quarters[i] || '';
292
+ bars.appendChild(lbl);
293
+ });
294
+ // dashed trend from first bar top to last bar top, then the % change badge
295
+ const first = tops[0], last = tops[tops.length - 1];
296
+ const lift = 120;
297
+ document.getElementById('trendPath').setAttribute('d', `M${first[0]},${first[1] - lift} Q${(first[0] + last[0]) / 2},${Math.min(first[1], last[1]) - lift - 160} ${last[0]},${last[1] - lift}`);
298
+ const tp = document.getElementById('trendPath').getAttribute('d');
299
+ const head = gm.path.point(tp, 1), mid = gm.path.point(tp, 0.5);
300
+ document.getElementById('trendHead').setAttribute('transform', `translate(${head.x},${head.y}) rotate(${head.angle})`);
301
+ const total = revenue.reduce((a, b) => a + b, 0);
302
+ const rt = document.getElementById('revTotal');
303
+ rt.setAttribute('data-count', '0 ' + total); rt.setAttribute('data-count-duration', '1.8'); rt.setAttribute('data-count-delay', '0.4');
304
+ rt.setAttribute('data-count-prefix', pre); rt.setAttribute('data-count-suffix', suf);
305
+ const pct = revenue[0] ? Math.round((revenue[rN - 1] / revenue[0] - 1) * 100) : 0;
306
+ const delta = document.createElement('div');
307
+ delta.className = 'delta'; delta.id = 'delta';
308
+ delta.textContent = (pct >= 0 ? '+' : '') + pct + '%';
309
+ bars.appendChild(delta);
310
+ Object.assign(delta.style, { left: mid.x + 'px', top: mid.y + 'px', translate: '-50% -50%' });
311
+
312
+ const tl3 = gm.timeline('#revenue')
313
+ .from('.hud .eyebrow', { autoAlpha: 0, x: -40, duration: 0.6, ease: 'expo.out' }, 0.3)
314
+ .from('h2', { autoAlpha: 0, y: 40, duration: 0.7, ease: 'expo.out' }, 0.4)
315
+ .from('.baseline', { scaleX: 0, duration: 0.8, ease: 'expo.out' }, 0.25);
316
+ revenue.forEach((_, i) => {
317
+ const at = 0.45 + i * 0.14;
318
+ tl3.from('#bar' + i, { scaleY: 0, duration: 0.9, ease: 'back.out(1.6)' }, at)
319
+ .from('#bv' + i, { autoAlpha: 0, y: 40, duration: 0.6, ease: 'expo.out' }, at + 0.1)
320
+ .from('#bl' + i, { autoAlpha: 0, y: 20, duration: 0.5, ease: 'expo.out' }, at + 0.05);
321
+ });
322
+ const tEnd = 0.45 + rN * 0.14 + 0.7;
323
+ tl3.from('#trendPath', { draw: 0, duration: 0.8, ease: 'power2.inOut' }, tEnd)
324
+ .from('#trendHead', { autoAlpha: 0, duration: 0.15 }, tEnd + 0.7)
325
+ .from('#delta', { scale: 0, rotate: -12, duration: 0.6, ease: 'back.out(2.5)' }, tEnd + 0.3)
326
+ .to('#bar' + hlIdx, { keyframes: [{ scaleY: 1.04, duration: 0.15 }, { scaleY: 1, duration: 0.4, ease: 'back.out(3)' }] }, tEnd + 0.6);
327
+
328
+ /* ---------------- 4. NPS gauge ---------------- */
329
+ const G = { cx: 550, cy: 560, r: 440 };
330
+ const npsVal = Math.max(-100, Math.min(100, Number(D.nps) || 0));
331
+ const ang = (v) => Math.PI - (v + 100) / 200 * Math.PI; // -100 -> left (180deg), 100 -> right (0deg)
332
+ const pt = (v, r) => [G.cx + Math.cos(ang(v)) * r, G.cy - Math.sin(ang(v)) * r];
333
+ const arc = (a, b) => { const [x0, y0] = pt(a, G.r), [x1, y1] = pt(b, G.r); return `M${x0},${y0} A${G.r},${G.r} 0 0 1 ${x1},${y1}`; };
334
+ document.getElementById('npsTrack').setAttribute('d', arc(-100, 100));
335
+ document.getElementById('npsFill').setAttribute('d', arc(-100, npsVal));
336
+ const ticks = document.getElementById('npsTicks');
337
+ [-50, 0, 50].forEach((v) => {
338
+ const [a, b] = pt(v, G.r - 52), [c, d] = pt(v, G.r - 76);
339
+ el('line', { class: 'tick', x1: a, y1: b, x2: c, y2: d }, ticks);
340
+ const [lx, ly] = pt(v, G.r - 112);
341
+ el('text', { class: 'tl', x: lx, y: ly + 10 }, ticks).textContent = v;
342
+ });
343
+ [-100, 100].forEach((v) => { const [x, y] = pt(v, G.r); el('text', { class: 'tl', x, y: y + 80 }, ticks).textContent = v; });
344
+ const fillD = document.getElementById('npsFill').getAttribute('d'), knob = document.getElementById('knob');
345
+ gm.onFrame('#nps', (t) => {
346
+ const q = gm.path.point(fillD, gm.interpolate(t, [0.5, 2.1], [0, 1], { easing: 'out-cubic' }));
347
+ knob.setAttribute('cx', q.x); knob.setAttribute('cy', q.y);
348
+ });
349
+ gm.timeline('#nps')
350
+ .from('#npsTrack', { draw: 0, duration: 0.9, ease: 'power2.inOut' }, 0.15)
351
+ .from('#npsTicks > *', { autoAlpha: 0, stagger: 0.04, duration: 0.4 }, 0.5)
352
+ .from('#npsFill', { draw: 0, duration: 1.6, ease: 'out-cubic' }, 0.5)
353
+ .from('#knob', { scale: 0, duration: 0.4, ease: 'back.out(3)' }, 0.35)
354
+ .from('.readout b', { scale: 0.6, autoAlpha: 0, duration: 0.7, ease: 'back.out(2)' }, 0.4)
355
+ .from('.readout .eyebrow', { autoAlpha: 0, y: 20, duration: 0.6, ease: 'expo.out' }, 0.9)
356
+ .fromTo('.readout b', { scale: 1 }, { keyframes: [{ scale: 1.1, duration: 0.12 }, { scale: 1, duration: 0.5, ease: 'back.out(3)' }] }, 2.1);
357
+ { const [kx, ky] = pt(npsVal, G.r); // where the knob lands, in page px (gauge is centered, top 170px)
358
+ gm.burst('#nps', { at: 2.1, kind: 'sparks', x: (960 - 550 + kx) / 19.2 + '%', y: (170 + ky) / 10.8 + '%', count: 36, colors: ['#4fe3c1', '#ffb84d', '#ffffff'] }); }
359
+ </script>
360
+ </body>
361
+ </html>
@@ -0,0 +1,46 @@
1
+ {
2
+ "title": "Our second year, in numbers",
3
+ "weeks": [
4
+ "W1",
5
+ "W2",
6
+ "W3",
7
+ "W4",
8
+ "W5",
9
+ "W6",
10
+ "W7",
11
+ "W8",
12
+ "W9",
13
+ "W10"
14
+ ],
15
+ "users": [
16
+ 8200,
17
+ 12900,
18
+ 11400,
19
+ 19800,
20
+ 27500,
21
+ 24100,
22
+ 41000,
23
+ 58300,
24
+ 71200,
25
+ 96400
26
+ ],
27
+ "growth": "11.8×",
28
+ "quarters": [
29
+ "Q1 '25",
30
+ "Q2 '25",
31
+ "Q3 '25",
32
+ "Q4 '25",
33
+ "Q1 '26"
34
+ ],
35
+ "revenue": [
36
+ 1240,
37
+ 980,
38
+ 1610,
39
+ 2275,
40
+ 2930
41
+ ],
42
+ "nps": -12,
43
+ "usersLabel": "Weekly active users",
44
+ "revenuePrefix": "$",
45
+ "revenueSuffix": "k"
46
+ }
@@ -0,0 +1,60 @@
1
+ <!doctype html>
2
+ <!-- Generative art with canvas, drawn as a pure function of time (seekable, parallel-safe).
3
+ Render: npx givemotion render examples/generative/index.html -o out/generative.mp4 -->
4
+ <html data-width="1920" data-height="1080" data-fps="30" data-duration="10">
5
+ <head>
6
+ <meta charset="utf-8">
7
+ <style>
8
+ body { background: #05060a; color: #fff; font-family: Inter, system-ui, sans-serif; }
9
+ canvas { position: absolute; inset: 0; width: 100%; height: 100%; }
10
+ .title { position: absolute; left: 120px; bottom: 120px; }
11
+ .title h1 { margin: 0; font-size: 120px; font-weight: 800; letter-spacing: -.045em; line-height: .95; }
12
+ .title p { margin: 18px 0 0; font-size: 38px; color: #ffffffb0; }
13
+ </style>
14
+ </head>
15
+ <body>
16
+ <canvas id="c" width="1920" height="1080"></canvas>
17
+ <div class="title" data-start="0.2" data-exit="fade 0.8">
18
+ <h1 data-split="chars" data-enter="blur 0.7" data-stagger="0.035">Flow fields</h1>
19
+ <p data-enter="fade-up 0.8 0.8">Every frame is a function of time.</p>
20
+ </div>
21
+ <script>
22
+ const c = document.getElementById('c'), g = c.getContext('2d');
23
+ const W = c.width, H = c.height, N = 1400, TRAIL = 14;
24
+ const seeds = Array.from({ length: N }, () => [Math.random(), Math.random(), Math.random()]);
25
+ // Position of particle i at time t: integrates nothing, so any frame can be drawn directly.
26
+ function pos(i, t) {
27
+ const [a, b, s] = seeds[i];
28
+ const sp = 0.035 + s * 0.05;
29
+ const u = (a + t * sp) % 1;
30
+ const x = u * (W + 200) - 100;
31
+ const y0 = b * H;
32
+ const y = y0 + Math.sin(u * 6.0 + b * 9 + t * 0.6) * 90 * (0.4 + s) + (gm.noise(u * 4 + i * 0.01, b * 10) - 0.5) * 260;
33
+ return [x, y, u];
34
+ }
35
+ const hue = (b) => 200 + b * 120;
36
+ gm.onFrame((t) => {
37
+ g.globalCompositeOperation = 'source-over';
38
+ g.fillStyle = '#05060a'; g.fillRect(0, 0, W, H);
39
+ g.globalCompositeOperation = 'lighter';
40
+ g.lineCap = 'round';
41
+ // already visible on frame 0 (the thumbnail), full strength by 1.2 s, fading out over the last second
42
+ const fade = Math.min(1, 0.45 + t / 2.2) * Math.min(1, (gm.duration - t) / 1.0);
43
+ for (let i = 0; i < N; i++) {
44
+ g.beginPath();
45
+ let prev = null;
46
+ for (let k = TRAIL; k >= 0; k--) {
47
+ const p = pos(i, t - k * 0.035);
48
+ if (prev && Math.abs(p[0] - prev[0]) > 300) { prev = null; }
49
+ if (!prev) g.moveTo(p[0], p[1]); else g.lineTo(p[0], p[1]);
50
+ prev = p;
51
+ }
52
+ const b = seeds[i][1];
53
+ g.strokeStyle = `hsla(${hue(b)}, 90%, 62%, ${0.16 * fade})`;
54
+ g.lineWidth = 1.2 + seeds[i][2] * 2.2;
55
+ g.stroke();
56
+ }
57
+ });
58
+ </script>
59
+ </body>
60
+ </html>
@@ -0,0 +1,5 @@
1
+ # Made by AI agents, from the docs alone
2
+
3
+ A fresh AI agent (that had never seen givemotion) was given only `skills/givemotion/SKILL.md`, the block files and the examples, plus a one-paragraph brief. It wrote this composition, used `check` and `snapshot` to fix its own mistakes, and rendered the video. Its feedback was used to improve the tool and the docs.
4
+
5
+ - `photosynthesis/` — "How plants make food", a 30 s kids explainer with hand-built SVG illustrations.
@@ -0,0 +1,32 @@
1
+ <!-- block:
2
+ description: A list whose items appear one by one and get a check mark. Put <li> items between the tags.
3
+ params: title, every (seconds between items, default 0.6), first (seconds before the first item, default 0.4), accent (color)
4
+ usage: <gm-include src="blocks/checklist.html" title="Launch plan" every="0.7" data-duration="5"><li>Write the script</li><li>Record voiceover</li><li>Render</li></gm-include>
5
+ -->
6
+ <div class="gmb-check" style="--accent:{{accent|#4ade80}}" data-every="{{every|0.6}}" data-first="{{first|0.4}}">
7
+ <div class="gmb-check__title" data-enter="fade-up 0.6">{{title}}</div>
8
+ <ul class="gmb-check__list">{{content}}</ul>
9
+ </div>
10
+ <script>
11
+ (() => {
12
+ const root = document.currentScript.previousElementSibling;
13
+ const every = parseFloat(root.dataset.every) || 0.6, start = parseFloat(root.dataset.first) || 0;
14
+ [...root.querySelectorAll('li')].forEach((li, i) => {
15
+ const d = start + i * every;
16
+ li.style.setProperty('--d', d + 's');
17
+ li.insertAdjacentHTML('afterbegin', '<span class="gmb-check__box"><svg viewBox="0 0 24 24"><path d="M5 12.5l4.2 4.2L19 7" pathLength="1"/></svg></span>');
18
+ });
19
+ })();
20
+ </script>
21
+ <style>
22
+ .gmb-check{display:flex;flex-direction:column;gap:calc(var(--px)*30)}
23
+ .gmb-check__title{font-size:calc(var(--px)*56);font-weight:800;letter-spacing:-.02em}
24
+ .gmb-check__title:empty{display:none}
25
+ .gmb-check__list{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:calc(var(--px)*22)}
26
+ .gmb-check__list li{display:flex;align-items:center;gap:calc(var(--px)*24);font-size:calc(var(--px)*46);font-weight:500;animation:fade-left .55s var(--ease-out-expo) var(--d) both}
27
+ .gmb-check__box{flex:none;width:calc(var(--px)*52);height:calc(var(--px)*52);border-radius:calc(var(--px)*14);border:calc(var(--px)*3) solid rgba(255,255,255,.3);display:grid;place-items:center;position:relative;
28
+ animation:gmb-check-fill .35s var(--ease-out-cubic) calc(var(--d) + .35s) both}
29
+ .gmb-check__box svg{width:72%;height:72%;fill:none;stroke:#0b1210;stroke-width:3.2;stroke-linecap:round;stroke-linejoin:round}
30
+ .gmb-check__box path{stroke-dasharray:1 1;animation:draw .4s var(--ease-out-cubic) calc(var(--d) + .45s) both}
31
+ @keyframes gmb-check-fill{from{background:transparent;border-color:rgba(255,255,255,.3);scale:.85}to{background:var(--accent);border-color:var(--accent);scale:1}}
32
+ </style>
@@ -0,0 +1,25 @@
1
+ <!-- block:
2
+ description: Closing card: optional logo, headline, subtitle and a call-to-action button that pops in.
3
+ params: title, subtitle, cta (button text), url (small text under the button), logo (image path), accent (color)
4
+ usage: <gm-include src="blocks/end-card.html" title="Try it today" cta="Get started" url="example.com" data-duration="4"></gm-include>
5
+ -->
6
+ <div class="gmb-end" style="--accent:{{accent|#7c9cff}}">
7
+ <img class="gmb-end__logo" src="{{logo}}" alt="" data-enter="scale 0.7">
8
+ <h2 class="gmb-end__h" data-split="words" data-enter="rise 0.8 0.15" data-stagger="0.06">{{title|Thanks for watching}}</h2>
9
+ <p class="gmb-end__sub" data-enter="fade-up 0.6 0.5">{{subtitle}}</p>
10
+ <div class="gmb-end__cta" data-enter="pop 0.6 0.8">{{cta}}</div>
11
+ <div class="gmb-end__url" data-enter="fade 0.6 1.1">{{url}}</div>
12
+ </div>
13
+ <script>
14
+ (() => { const r = document.currentScript.previousElementSibling; const i = r.querySelector('img'); if (!i.getAttribute('src')) i.remove(); })();
15
+ </script>
16
+ <style>
17
+ .gmb-end{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;gap:calc(var(--px)*28);padding:0 calc(var(--px)*160)}
18
+ .gmb-end__logo{height:calc(var(--px)*110);width:auto;margin-bottom:calc(var(--px)*10)}
19
+ .gmb-end__h{margin:0;font-size:calc(var(--px)*112);font-weight:800;letter-spacing:-.035em;line-height:1.04;max-width:18ch;text-wrap:balance}
20
+ .gmb-end__sub{margin:0;font-size:calc(var(--px)*42);opacity:.72;max-width:32ch;text-wrap:balance}
21
+ .gmb-end__cta{margin-top:calc(var(--px)*18);padding:calc(var(--px)*24) calc(var(--px)*54);border-radius:99px;background:var(--accent);color:#0b0d12;font-size:calc(var(--px)*42);font-weight:800;
22
+ box-shadow:0 calc(var(--px)*16) calc(var(--px)*50) color-mix(in oklab,var(--accent),transparent 55%)}
23
+ .gmb-end__url{font-size:calc(var(--px)*32);opacity:.6;letter-spacing:.02em}
24
+ .gmb-end__sub:empty,.gmb-end__cta:empty,.gmb-end__url:empty{display:none}
25
+ </style>
@@ -0,0 +1,48 @@
1
+ <!-- block:
2
+ description: Soft animated mesh-gradient background (slowly drifting color blobs with fine grain). Put it first inside a scene.
3
+ params: c1, c2, c3 (colors), base (background color), speed (1 = default), grain (0-1, default 0.06)
4
+ usage: <gm-include src="blocks/gradient-bg.html" c1="#6d5dfc" c2="#00c2ff" c3="#ff4ecd"></gm-include>
5
+ -->
6
+ <div class="gmb-grad" data-c="{{c1|#6d5dfc}},{{c2|#00b3ff}},{{c3|#ff4ecd}}" data-base="{{base|#07080d}}" data-speed="{{speed|1}}" style="--grain:{{grain|0.06}}">
7
+ <canvas class="gmb-grad__c" width="240" height="135"></canvas><div class="gmb-grad__grain"></div>
8
+ </div>
9
+ <script>
10
+ (() => {
11
+ // Drawn small and scaled up: soft gradients look identical, and it renders ~5x faster than big blurred layers.
12
+ const root = document.currentScript.previousElementSibling;
13
+ const cv = root.querySelector('canvas'), g = cv.getContext('2d');
14
+ const [c1, c2, c3] = root.dataset.c.split(',');
15
+ const base = root.dataset.base, sp = parseFloat(root.dataset.speed) || 1;
16
+ const W = cv.width, H = cv.height;
17
+ const blobs = [
18
+ { c: c1, x: 0.18, y: 0.12, ax: 0.22, ay: 0.16, p: 14, r: 0.62 },
19
+ { c: c2, x: 0.82, y: 0.42, ax: -0.24, ay: 0.14, p: 17, r: 0.6 },
20
+ { c: c3, x: 0.45, y: 0.95, ax: 0.16, ay: -0.22, p: 20, r: 0.66 },
21
+ ];
22
+ const hex = (c, a) => { const h = c.trim().replace('#', ''); const n = parseInt(h.length === 3 ? [...h].map((x) => x + x).join('') : h.slice(0, 6), 16); return `rgba(${(n >> 16) & 255},${(n >> 8) & 255},${n & 255},${a})`; };
23
+ gm.onFrame(root, (t, f) => {
24
+ t = f.time; // drift on global time so it continues smoothly across scenes
25
+ g.globalCompositeOperation = 'source-over';
26
+ g.fillStyle = base; g.fillRect(0, 0, W, H);
27
+ g.globalCompositeOperation = 'screen';
28
+ for (const b of blobs) {
29
+ const k = (1 - Math.cos((2 * Math.PI * t * sp) / b.p)) / 2; // smooth back-and-forth drift
30
+ const x = (b.x + b.ax * k) * W, y = (b.y + b.ay * k) * H, r = b.r * W;
31
+ const gr = g.createRadialGradient(x, y, 0, x, y, r);
32
+ gr.addColorStop(0, hex(b.c, 0.72)); gr.addColorStop(0.45, hex(b.c, 0.26)); gr.addColorStop(1, hex(b.c, 0));
33
+ g.fillStyle = gr; g.fillRect(0, 0, W, H);
34
+ }
35
+ });
36
+ // Static film grain tile
37
+ const n = document.createElement('canvas'); n.width = n.height = 160;
38
+ const ng = n.getContext('2d'), id = ng.createImageData(160, 160);
39
+ for (let i = 0; i < id.data.length; i += 4) { const v = Math.random() * 255; id.data[i] = id.data[i + 1] = id.data[i + 2] = v; id.data[i + 3] = 255; }
40
+ ng.putImageData(id, 0, 0);
41
+ root.querySelector('.gmb-grad__grain').style.backgroundImage = `url(${n.toDataURL()})`;
42
+ })();
43
+ </script>
44
+ <style>
45
+ .gmb-grad{position:absolute;inset:0;overflow:hidden;z-index:0}
46
+ .gmb-grad__c{position:absolute;inset:0;width:100%;height:100%;display:block}
47
+ .gmb-grad__grain{position:absolute;inset:0;opacity:var(--grain);background-size:calc(var(--px)*160)}
48
+ </style>
@@ -0,0 +1,40 @@
1
+ <!-- block:
2
+ description: Horizontal process steps. A progress line fills and each step lights up in turn.
3
+ params: steps (labels separated by |), every (seconds per step, default 0.9), first (delay, default 0.4), accent (color)
4
+ usage: <gm-include src="blocks/steps.html" steps="Trigger|Fetch data|Ask AI|Send email" data-duration="5"></gm-include>
5
+ -->
6
+ <div class="gmb-steps" style="--accent:{{accent|#7c9cff}}" data-steps="{{steps|Plan|Build|Ship}}" data-every="{{every|0.9}}" data-first="{{first|0.4}}">
7
+ <div class="gmb-steps__track"><div class="gmb-steps__fill"></div></div>
8
+ <div class="gmb-steps__row"></div>
9
+ </div>
10
+ <script>
11
+ (() => {
12
+ const root = document.currentScript.previousElementSibling;
13
+ const names = root.dataset.steps.split('|').map((s) => s.trim()).filter(Boolean);
14
+ const every = parseFloat(root.dataset.every) || 0.9, first = parseFloat(root.dataset.first) || 0;
15
+ const row = root.querySelector('.gmb-steps__row');
16
+ names.forEach((n, i) => {
17
+ const el = document.createElement('div');
18
+ el.className = 'gmb-steps__step';
19
+ el.style.setProperty('--d', (first + i * every) + 's');
20
+ el.innerHTML = `<div class="gmb-steps__dot">${i + 1}</div><div class="gmb-steps__name">${n.replace(/</g, '&lt;')}</div>`;
21
+ row.appendChild(el);
22
+ });
23
+ root.style.setProperty('--n', names.length);
24
+ const fill = root.querySelector('.gmb-steps__fill');
25
+ fill.style.animationDuration = (every * (names.length - 1)) + 's';
26
+ fill.style.animationDelay = first + 's';
27
+ })();
28
+ </script>
29
+ <style>
30
+ .gmb-steps{position:relative;width:calc(var(--px)*1500);max-width:calc(var(--stage-w,1920px) - var(--px)*192)}
31
+ .gmb-steps__track{position:absolute;left:calc(100% / var(--n) / 2);right:calc(100% / var(--n) / 2);top:calc(var(--px)*44);height:calc(var(--px)*6);border-radius:99px;background:rgba(255,255,255,.14);overflow:hidden}
32
+ .gmb-steps__fill{height:100%;background:var(--accent);transform-origin:left;animation:gmb-steps-fill 2s linear both}
33
+ .gmb-steps__row{position:relative;display:grid;grid-template-columns:repeat(var(--n),1fr)}
34
+ .gmb-steps__step{display:flex;flex-direction:column;align-items:center;gap:calc(var(--px)*22);text-align:center}
35
+ .gmb-steps__dot{width:calc(var(--px)*94);height:calc(var(--px)*94);border-radius:50%;display:grid;place-items:center;font-size:calc(var(--px)*38);font-weight:800;
36
+ background:#151923;border:calc(var(--px)*4) solid rgba(255,255,255,.18);color:rgba(255,255,255,.6);animation:gmb-steps-on .45s var(--ease-out-back) var(--d) both}
37
+ .gmb-steps__name{font-size:calc(var(--px)*36);font-weight:600;max-width:12ch;animation:fade-up .5s var(--ease-out-expo) var(--d) both}
38
+ @keyframes gmb-steps-fill{from{scale:0 1}}
39
+ @keyframes gmb-steps-on{from{scale:.8}to{background:var(--accent);border-color:var(--accent);color:#0b0d12;scale:1;box-shadow:0 0 0 calc(var(--px)*10) color-mix(in oklab,var(--accent),transparent 80%)}}
40
+ </style>