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.
- package/AGENTS.md +37 -0
- package/LICENSE +21 -0
- package/README.md +492 -2
- package/SECURITY.md +40 -0
- package/THIRD_PARTY_NOTICES.md +48 -0
- package/blocks/bar-chart.html +63 -0
- package/blocks/browser-window.html +52 -0
- package/blocks/callout.html +57 -0
- package/blocks/captions.html +120 -0
- package/blocks/checklist.html +32 -0
- package/blocks/code-window.html +126 -0
- package/blocks/compare.html +33 -0
- package/blocks/cursor.html +157 -0
- package/blocks/end-card.html +28 -0
- package/blocks/gradient-bg.html +48 -0
- package/blocks/grid-bg.html +18 -0
- package/blocks/highlight.html +13 -0
- package/blocks/line-chart.html +36 -0
- package/blocks/logo-reveal.html +33 -0
- package/blocks/lottie.html +34 -0
- package/blocks/lower-third.html +24 -0
- package/blocks/phone.html +18 -0
- package/blocks/quote.html +19 -0
- package/blocks/stat.html +24 -0
- package/blocks/steps.html +40 -0
- package/blocks/title-card.html +22 -0
- package/blocks/word-pop.html +40 -0
- package/examples/bench/index.html +24 -0
- package/examples/data-story/data.json +9 -0
- package/examples/data-story/index.html +361 -0
- package/examples/data-story/other.json +46 -0
- package/examples/generative/index.html +60 -0
- package/examples/made-by-agents/README.md +5 -0
- package/examples/made-by-agents/photosynthesis/blocks/checklist.html +32 -0
- package/examples/made-by-agents/photosynthesis/blocks/end-card.html +25 -0
- package/examples/made-by-agents/photosynthesis/blocks/gradient-bg.html +48 -0
- package/examples/made-by-agents/photosynthesis/blocks/steps.html +40 -0
- package/examples/made-by-agents/photosynthesis/blocks/title-card.html +22 -0
- package/examples/made-by-agents/photosynthesis/index.html +217 -0
- package/examples/personalized/index.html +28 -0
- package/llms.txt +34 -0
- package/package.json +80 -4
- package/skills/givemotion/SKILL.md +152 -0
- package/skills/givemotion/references/api.md +40 -0
- package/skills/givemotion/references/camera.md +72 -0
- package/skills/givemotion/references/charts.md +138 -0
- package/skills/givemotion/references/check-codes.md +94 -0
- package/skills/givemotion/references/data-templates.md +61 -0
- package/skills/givemotion/references/diagrams.md +75 -0
- package/skills/givemotion/references/genres/code-explainer.md +15 -0
- package/skills/givemotion/references/genres/data-story.md +21 -0
- package/skills/givemotion/references/genres/explainer.md +20 -0
- package/skills/givemotion/references/genres/hand-drawn.md +18 -0
- package/skills/givemotion/references/genres/logo-sting.md +20 -0
- package/skills/givemotion/references/genres/promo.md +18 -0
- package/skills/givemotion/references/genres/ui-demo.md +21 -0
- package/skills/givemotion/references/genres/vertical.md +19 -0
- package/skills/givemotion/references/genres/year-recap.md +18 -0
- package/skills/givemotion/references/javascript.md +72 -0
- package/skills/givemotion/references/media-audio.md +57 -0
- package/skills/givemotion/references/motion-design.md +183 -0
- package/skills/givemotion/references/output.md +64 -0
- package/skills/givemotion/references/review.md +60 -0
- package/skills/givemotion/references/styles.md +133 -0
- package/skills/givemotion/references/techniques/backgrounds-and-ambient.md +85 -0
- package/skills/givemotion/references/techniques/camera-moves.md +88 -0
- package/skills/givemotion/references/techniques/captions-vertical.md +89 -0
- package/skills/givemotion/references/techniques/code-on-screen.md +125 -0
- package/skills/givemotion/references/techniques/hand-drawn.md +87 -0
- package/skills/givemotion/references/techniques/impacts-and-energy.md +106 -0
- package/skills/givemotion/references/techniques/kinetic-type.md +92 -0
- package/skills/givemotion/references/techniques/light-and-depth.md +91 -0
- package/skills/givemotion/references/techniques/logo-reveal.md +94 -0
- package/skills/givemotion/references/techniques/numbers-and-charts.md +92 -0
- package/skills/givemotion/references/techniques/paths-and-maps.md +99 -0
- package/skills/givemotion/references/techniques/scene-transitions.md +85 -0
- package/skills/givemotion/references/techniques/shapes-and-motion-graphics.md +92 -0
- package/skills/givemotion/references/techniques/text-reveals.md +94 -0
- package/skills/givemotion/references/techniques/ui-demo.md +110 -0
- package/skills/givemotion/references/timeline.md +85 -0
- package/src/analyze.js +1485 -0
- package/src/api.js +36 -0
- package/src/args.js +33 -0
- package/src/audio-cli.js +58 -0
- package/src/bench.js +153 -0
- package/src/browser.js +381 -0
- package/src/bundle.js +79 -0
- package/src/cdp.js +94 -0
- package/src/check.js +1818 -0
- package/src/cli.js +611 -0
- package/src/dev.js +26 -0
- package/src/ffmpeg.js +330 -0
- package/src/parts.js +380 -0
- package/src/player/gm-player.js +175 -0
- package/src/render.js +772 -0
- package/src/runtime/audio.js +306 -0
- package/src/runtime/chart.js +665 -0
- package/src/runtime/fonts/BebasNeue-OFL.txt +93 -0
- package/src/runtime/fonts/Caveat-OFL.txt +93 -0
- package/src/runtime/fonts/Inter-OFL.txt +93 -0
- package/src/runtime/fonts/JetBrainsMono-OFL.txt +93 -0
- package/src/runtime/fonts/Nunito-OFL.txt +93 -0
- package/src/runtime/fonts/PlayfairDisplay-OFL.txt +93 -0
- package/src/runtime/fonts/SpaceGrotesk-OFL.txt +93 -0
- package/src/runtime/fonts/bebas-neue-latin-400-normal.woff2 +0 -0
- package/src/runtime/fonts/bebas-neue-latin-ext-400-normal.woff2 +0 -0
- package/src/runtime/fonts/caveat-latin-ext-wght-normal.woff2 +0 -0
- package/src/runtime/fonts/caveat-latin-wght-normal.woff2 +0 -0
- package/src/runtime/fonts/inter-latin-ext-wght-italic.woff2 +0 -0
- package/src/runtime/fonts/inter-latin-ext-wght-normal.woff2 +0 -0
- package/src/runtime/fonts/inter-latin-wght-italic.woff2 +0 -0
- package/src/runtime/fonts/inter-latin-wght-normal.woff2 +0 -0
- package/src/runtime/fonts/jetbrains-mono-latin-ext-wght-italic.woff2 +0 -0
- package/src/runtime/fonts/jetbrains-mono-latin-ext-wght-normal.woff2 +0 -0
- package/src/runtime/fonts/jetbrains-mono-latin-wght-italic.woff2 +0 -0
- package/src/runtime/fonts/jetbrains-mono-latin-wght-normal.woff2 +0 -0
- package/src/runtime/fonts/nunito-latin-ext-wght-normal.woff2 +0 -0
- package/src/runtime/fonts/nunito-latin-wght-normal.woff2 +0 -0
- package/src/runtime/fonts/playfair-display-latin-ext-wght-italic.woff2 +0 -0
- package/src/runtime/fonts/playfair-display-latin-ext-wght-normal.woff2 +0 -0
- package/src/runtime/fonts/playfair-display-latin-wght-italic.woff2 +0 -0
- package/src/runtime/fonts/playfair-display-latin-wght-normal.woff2 +0 -0
- package/src/runtime/fonts/space-grotesk-latin-ext-wght-normal.woff2 +0 -0
- package/src/runtime/fonts/space-grotesk-latin-wght-normal.woff2 +0 -0
- package/src/runtime/motion.js +2674 -0
- package/src/runtime/preset.css +97 -0
- package/src/runtime/runtime.js +2141 -0
- package/src/runtime/sketch.js +1249 -0
- package/src/server.js +402 -0
- package/src/studio/index.html +238 -0
- package/src/types/api.d.ts +354 -0
- package/templates/starter/index.html +70 -0
|
@@ -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, '<')}</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>
|