claude-exode-mcp 7.5.7 → 7.5.8

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 (173) hide show
  1. package/MCP_DOCUMENTATION.md +1 -0
  2. package/SKILL.md +10 -0
  3. package/examples/aurum-product-page.exode +3208 -0
  4. package/examples/bell-ring-icon.exode +1 -0
  5. package/examples/drift-kinetic-type.exode +770 -0
  6. package/examples/earbuds-product.exode +214 -0
  7. package/examples/energy-flow.exode +1736 -0
  8. package/examples/flux-hero.exode +223 -0
  9. package/examples/gooey-toggle.exode +214 -0
  10. package/examples/gravity-well.exode +3113 -0
  11. package/examples/heart-icon.exode +1 -0
  12. package/examples/hologram-temple.exode +8100 -0
  13. package/examples/jarful-app.exode +1 -0
  14. package/examples/kip-character-rig.exode +1 -0
  15. package/examples/low-tide-tilt.exode +2216 -0
  16. package/examples/manifest.json +352 -0
  17. package/examples/swipe-stack-carousel.exode +4048 -0
  18. package/examples/tokyo-neon-weather.exode +470 -0
  19. package/examples/typographic-collapse.exode +1578 -0
  20. package/examples/void-product-ad.exode +1269 -0
  21. package/examples/wire-globe-card.exode +580 -0
  22. package/index.d.ts +41 -1
  23. package/index.js +2553 -2407
  24. package/package.json +4 -2
  25. package/render-runtime/exode-runtime.js +20 -20
  26. package/skills/creative-motion/SKILL.md +10 -0
  27. package/skills/creative-motion/recipes/character-rig.js +104 -0
  28. package/skills/creative-motion/recipes/dashboard.js +120 -0
  29. package/skills/creative-motion/recipes/game-arcade.js +203 -0
  30. package/skills/creative-motion/recipes/hero-physics.js +65 -0
  31. package/skills/creative-motion/recipes/pricing.js +88 -0
  32. package/skills/creative-motion/recipes/title-sequence.js +62 -0
  33. package/skills/creative-motion/references/apps.md +37 -0
  34. package/skills/creative-motion/references/gotchas.md +80 -0
  35. package/skills/creative-motion/references/native-layers.md +60 -0
  36. package/skills/creative-motion/references/quality.md +54 -0
  37. package/src/__tests__/buildKit.test.ts +127 -0
  38. package/src/__tests__/effectKnobs.test.ts +162 -0
  39. package/src/__tests__/nativeLayers.test.ts +178 -0
  40. package/src/buildKit.d.ts +301 -0
  41. package/src/buildKit.d.ts.map +1 -0
  42. package/src/buildKit.js +559 -0
  43. package/src/buildKit.js.map +1 -0
  44. package/src/buildKit.ts +520 -0
  45. package/src/compiler.d.ts +4 -0
  46. package/src/compiler.d.ts.map +1 -1
  47. package/src/compiler.js +21 -45
  48. package/src/compiler.js.map +1 -1
  49. package/src/compiler.ts +22 -51
  50. package/src/critique.d.ts +8 -0
  51. package/src/critique.d.ts.map +1 -0
  52. package/src/critique.js +348 -0
  53. package/src/critique.js.map +1 -0
  54. package/src/critique.ts +239 -0
  55. package/src/effectKnobs.d.ts +72 -0
  56. package/src/effectKnobs.d.ts.map +1 -0
  57. package/src/effectKnobs.js +541 -0
  58. package/src/effectKnobs.js.map +1 -0
  59. package/src/effectKnobs.ts +445 -0
  60. package/src/effectOptimizer.d.ts +18 -0
  61. package/src/effectOptimizer.d.ts.map +1 -0
  62. package/src/effectOptimizer.js +125 -0
  63. package/src/effectOptimizer.js.map +1 -0
  64. package/src/effectOptimizer.ts +104 -0
  65. package/src/engine/layout.js +1 -1
  66. package/src/engine/layout.js.map +1 -1
  67. package/src/engine/layout.ts +1 -1
  68. package/src/examples.d.ts +30 -0
  69. package/src/examples.d.ts.map +1 -0
  70. package/src/examples.js +168 -0
  71. package/src/examples.js.map +1 -0
  72. package/src/examples.ts +151 -0
  73. package/src/handlers/artboardHandlers.d.ts.map +1 -1
  74. package/src/handlers/artboardHandlers.js +45 -10
  75. package/src/handlers/artboardHandlers.js.map +1 -1
  76. package/src/handlers/artboardHandlers.ts +43 -12
  77. package/src/handlers/buildHandlers.d.ts +2 -0
  78. package/src/handlers/buildHandlers.d.ts.map +1 -0
  79. package/src/handlers/buildHandlers.js +86 -0
  80. package/src/handlers/buildHandlers.js.map +1 -0
  81. package/src/handlers/buildHandlers.ts +76 -0
  82. package/src/handlers/compilerHandlers.d.ts.map +1 -1
  83. package/src/handlers/compilerHandlers.js +32 -2
  84. package/src/handlers/compilerHandlers.js.map +1 -1
  85. package/src/handlers/compilerHandlers.ts +31 -2
  86. package/src/handlers/exampleHandlers.d.ts +2 -0
  87. package/src/handlers/exampleHandlers.d.ts.map +1 -0
  88. package/src/handlers/exampleHandlers.js +81 -0
  89. package/src/handlers/exampleHandlers.js.map +1 -0
  90. package/src/handlers/exampleHandlers.ts +76 -0
  91. package/src/handlers/renderHandlers.d.ts.map +1 -1
  92. package/src/handlers/renderHandlers.js +100 -2
  93. package/src/handlers/renderHandlers.js.map +1 -1
  94. package/src/handlers/renderHandlers.ts +90 -2
  95. package/src/handlers/scaffoldHandlers.d.ts.map +1 -1
  96. package/src/handlers/scaffoldHandlers.js +3 -0
  97. package/src/handlers/scaffoldHandlers.js.map +1 -1
  98. package/src/handlers/scaffoldHandlers.ts +2 -0
  99. package/src/handlers/widgetHandlers.js +1 -1
  100. package/src/handlers/widgetHandlers.js.map +1 -1
  101. package/src/handlers/widgetHandlers.ts +1 -1
  102. package/src/lottie/convert.d.ts +16 -0
  103. package/src/lottie/convert.d.ts.map +1 -0
  104. package/src/lottie/convert.js +1225 -0
  105. package/src/lottie/convert.js.map +1 -0
  106. package/src/lottie/convert.ts +1134 -0
  107. package/src/lottie/evaluate.d.ts +48 -0
  108. package/src/lottie/evaluate.d.ts.map +1 -0
  109. package/src/lottie/evaluate.js +174 -0
  110. package/src/lottie/evaluate.js.map +1 -0
  111. package/src/lottie/evaluate.ts +184 -0
  112. package/src/nativeLayers.d.ts +76 -0
  113. package/src/nativeLayers.d.ts.map +1 -0
  114. package/src/nativeLayers.js +404 -0
  115. package/src/nativeLayers.js.map +1 -0
  116. package/src/nativeLayers.ts +339 -0
  117. package/src/render/motionBrief.d.ts.map +1 -1
  118. package/src/render/motionBrief.js +4 -0
  119. package/src/render/motionBrief.js.map +1 -1
  120. package/src/render/motionBrief.ts +4 -0
  121. package/src/render/videoRenderer.d.ts.map +1 -1
  122. package/src/render/videoRenderer.js +5 -2
  123. package/src/render/videoRenderer.js.map +1 -1
  124. package/src/render/videoRenderer.ts +5 -2
  125. package/src/rive/convert.d.ts +17 -0
  126. package/src/rive/convert.d.ts.map +1 -0
  127. package/src/rive/convert.js +1779 -0
  128. package/src/rive/convert.js.map +1 -0
  129. package/src/rive/convert.ts +1492 -0
  130. package/src/rive/font.d.ts +13 -0
  131. package/src/rive/font.d.ts.map +1 -0
  132. package/src/rive/font.js +154 -0
  133. package/src/rive/font.js.map +1 -0
  134. package/src/rive/font.ts +132 -0
  135. package/src/rive/layout.d.ts +20 -0
  136. package/src/rive/layout.d.ts.map +1 -0
  137. package/src/rive/layout.js +145 -0
  138. package/src/rive/layout.js.map +1 -0
  139. package/src/rive/layout.ts +140 -0
  140. package/src/rive/reader.d.ts +32 -0
  141. package/src/rive/reader.d.ts.map +1 -0
  142. package/src/rive/reader.js +172 -0
  143. package/src/rive/reader.js.map +1 -0
  144. package/src/rive/reader.ts +160 -0
  145. package/src/rive/schema.generated.d.ts +9 -0
  146. package/src/rive/schema.generated.d.ts.map +1 -0
  147. package/src/rive/schema.generated.js +13 -0
  148. package/src/rive/schema.generated.js.map +1 -0
  149. package/src/rive/schema.generated.ts +13 -0
  150. package/src/schema.d.ts +5 -0
  151. package/src/schema.d.ts.map +1 -1
  152. package/src/schema.js +5 -0
  153. package/src/schema.js.map +1 -1
  154. package/src/schema.ts +6 -0
  155. package/src/serverInstructions.d.ts +2 -0
  156. package/src/serverInstructions.d.ts.map +1 -0
  157. package/src/serverInstructions.js +22 -0
  158. package/src/serverInstructions.js.map +1 -0
  159. package/src/serverInstructions.ts +21 -0
  160. package/src/sseServer.d.ts +2 -0
  161. package/src/sseServer.d.ts.map +1 -0
  162. package/src/sseServer.js +165 -0
  163. package/src/sseServer.js.map +1 -0
  164. package/src/sseServer.ts +189 -0
  165. package/src/utils.d.ts.map +1 -1
  166. package/src/utils.js +23 -1
  167. package/src/utils.js.map +1 -1
  168. package/src/utils.ts +13 -1
  169. package/src/widgets.d.ts +26 -0
  170. package/src/widgets.d.ts.map +1 -1
  171. package/src/widgets.js +89 -45
  172. package/src/widgets.js.map +1 -1
  173. package/src/widgets.ts +88 -45
@@ -0,0 +1,214 @@
1
+ {
2
+ "id": "mg_earbuds",
3
+ "name": "Earbuds",
4
+ "width": 1440,
5
+ "height": 810,
6
+ "cornerRadius": 0,
7
+ "fit": "Layout",
8
+ "backgroundColor": "#0a0b0d",
9
+ "opacity": 1,
10
+ "x": -720,
11
+ "y": -405,
12
+ "responsive": {
13
+ "enabled": true,
14
+ "mode": "constraint",
15
+ "width": {
16
+ "type": "fixed"
17
+ },
18
+ "height": {
19
+ "type": "fixed"
20
+ },
21
+ "maintainAspectRatio": false,
22
+ "scaleObjects": false,
23
+ "breakpoints": [
24
+ {
25
+ "id": "bp_mobile",
26
+ "name": "Mobile",
27
+ "minWidth": 0,
28
+ "maxWidth": 599
29
+ },
30
+ {
31
+ "id": "bp_tablet",
32
+ "name": "Tablet",
33
+ "minWidth": 600,
34
+ "maxWidth": 1023
35
+ },
36
+ {
37
+ "id": "bp_desktop",
38
+ "name": "Desktop",
39
+ "minWidth": 1024
40
+ }
41
+ ]
42
+ },
43
+ "layoutProps": {
44
+ "mode": "linear",
45
+ "direction": "vertical",
46
+ "gap": 0,
47
+ "padding": {
48
+ "top": 0,
49
+ "right": 0,
50
+ "bottom": 0,
51
+ "left": 0
52
+ },
53
+ "alignItems": "stretch",
54
+ "layoutSizingHorizontal": "fixed",
55
+ "layoutSizingVertical": "fixed"
56
+ },
57
+ "seo": {
58
+ "title": "Earbuds — Exode Motion Graphics",
59
+ "description": "A glossy charging case whose lid swings open to lift two earbuds out on rings of sound. Click to open or close."
60
+ },
61
+ "stateMachine": {
62
+ "inputs": [
63
+ {
64
+ "id": "in_name",
65
+ "name": "name",
66
+ "value": {
67
+ "type": "Text",
68
+ "value": "Aura Buds Pro",
69
+ "defaultValue": "Aura Buds Pro"
70
+ }
71
+ },
72
+ {
73
+ "id": "in_tagline",
74
+ "name": "tagline",
75
+ "value": {
76
+ "type": "Text",
77
+ "value": "Silence the city. Hear every breath of the music. Up to 36 hours in a case that fits a coin pocket.",
78
+ "defaultValue": "Silence the city. Hear every breath of the music. Up to 36 hours in a case that fits a coin pocket."
79
+ }
80
+ },
81
+ {
82
+ "id": "in_cta",
83
+ "name": "cta",
84
+ "value": {
85
+ "type": "Text",
86
+ "value": "Pre-order · $249",
87
+ "defaultValue": "Pre-order · $249"
88
+ }
89
+ },
90
+ {
91
+ "id": "in_color",
92
+ "name": "color",
93
+ "value": {
94
+ "type": "Text",
95
+ "value": "#f1f0ec",
96
+ "defaultValue": "#f1f0ec"
97
+ }
98
+ },
99
+ {
100
+ "id": "in_speed",
101
+ "name": "speed",
102
+ "value": {
103
+ "type": "Number",
104
+ "value": 1,
105
+ "defaultValue": 1
106
+ }
107
+ },
108
+ {
109
+ "id": "in_accent",
110
+ "name": "accent",
111
+ "value": {
112
+ "type": "Text",
113
+ "value": "#7ce0c3",
114
+ "defaultValue": "#7ce0c3"
115
+ }
116
+ },
117
+ {
118
+ "id": "in_bg",
119
+ "name": "bg",
120
+ "value": {
121
+ "type": "Text",
122
+ "value": "#0a0b0d",
123
+ "defaultValue": "#0a0b0d"
124
+ }
125
+ },
126
+ {
127
+ "id": "in_text",
128
+ "name": "text",
129
+ "value": {
130
+ "type": "Text",
131
+ "value": "#f2f3f5",
132
+ "defaultValue": "#f2f3f5"
133
+ }
134
+ }
135
+ ],
136
+ "layers": [
137
+ {
138
+ "name": "Base",
139
+ "entryStateId": "s_idle",
140
+ "states": [
141
+ {
142
+ "id": "s_idle",
143
+ "name": "Idle",
144
+ "loop": true,
145
+ "transitions": []
146
+ }
147
+ ]
148
+ }
149
+ ],
150
+ "logicNodes": []
151
+ },
152
+ "animations": [],
153
+ "physics": {
154
+ "enabled": false,
155
+ "gravity": {
156
+ "x": 0,
157
+ "y": 9.8
158
+ }
159
+ },
160
+ "animationFlow": [],
161
+ "navigation": [],
162
+ "stateVariables": [],
163
+ "objects": [
164
+ {
165
+ "id": "stage",
166
+ "name": "Stage",
167
+ "type": "Group",
168
+ "transform": {
169
+ "x": 0,
170
+ "y": 0,
171
+ "rotation": 0,
172
+ "scale_x": 1,
173
+ "scale_y": 1,
174
+ "opacity": 1,
175
+ "skew_x": 0,
176
+ "skew_y": 0
177
+ },
178
+ "width": 1440,
179
+ "height": 810,
180
+ "geometry": {
181
+ "type": "Rectangle",
182
+ "width": 1440,
183
+ "height": 810,
184
+ "cornerRadius": 0
185
+ },
186
+ "cornerRadius": 0,
187
+ "children": [],
188
+ "clipContent": true,
189
+ "style": {
190
+ "fill": {
191
+ "type": "Solid",
192
+ "color": "#0a0b0d",
193
+ "opacity": 1
194
+ },
195
+ "effects": [
196
+ {
197
+ "id": "fx_earbuds",
198
+ "type": "CustomEffect",
199
+ "name": "Earbuds",
200
+ "isEnabled": true,
201
+ "params": {},
202
+ "code": "/* Shared by every motion-ui effect (prepended to each one).\n Rules for this file and the effects: the .bexode compiler minifies effect\n code, so use explicit semicolons, block comments only, and never write a\n minus followed by a negative value (it collapses to a decrement). */\n\n/* Read a live state-machine input, falling back to a default. */\nfunction inp(env, name, fallback) {\n const v = env.inputs ? env.inputs[name] : undefined;\n if (v === undefined || v === null || v === '') return fallback;\n if (typeof fallback === 'number') {\n const n = Number(v);\n return isFinite(n) ? n : fallback;\n }\n if (typeof fallback === 'boolean') return v === true || v === 1 || v === 'true';\n return v;\n}\n\n/* '#rgb' / '#rrggbb' to [r, g, b]; anything else falls back to white. */\nfunction rgbOf(hex) {\n let h = String(hex || '').trim().replace('#', '');\n if (h.length === 3) h = h[0] + h[0] + h[1] + h[1] + h[2] + h[2];\n if (!/^[0-9a-fA-F]{6}$/.test(h)) return [255, 255, 255];\n const n = parseInt(h, 16);\n return [(n >> 16) & 255, (n >> 8) & 255, n & 255];\n}\n\nfunction rgba(hex, a) {\n const c = rgbOf(hex);\n return 'rgba(' + c[0] + ',' + c[1] + ',' + c[2] + ',' + Math.max(0, Math.min(1, a)) + ')';\n}\n\nfunction mix(a, b, t) {\n const x = rgbOf(a), y = rgbOf(b);\n const r = Math.round(x[0] + (y[0] - x[0]) * t);\n const g = Math.round(x[1] + (y[1] - x[1]) * t);\n const bl = Math.round(x[2] + (y[2] - x[2]) * t);\n return '#' + ((1 << 24) + (r << 16) + (g << 8) + bl).toString(16).slice(1);\n}\n\n/* A soft round glow, rendered once per colour and blitted with drawImage\n (much cheaper than shadowBlur on every particle). */\nconst __glows = {};\nfunction glowSprite(hex) {\n if (__glows[hex]) return __glows[hex];\n const s = 64;\n const c = document.createElement('canvas');\n c.width = s; c.height = s;\n const g = c.getContext('2d');\n const gr = g.createRadialGradient(s / 2, s / 2, 0, s / 2, s / 2, s / 2);\n gr.addColorStop(0, rgba(hex, 1));\n gr.addColorStop(0.18, rgba(hex, 0.55));\n gr.addColorStop(0.45, rgba(hex, 0.14));\n gr.addColorStop(1, rgba(hex, 0));\n g.fillStyle = gr;\n g.fillRect(0, 0, s, s);\n __glows[hex] = c;\n return c;\n}\n\nfunction glow(ctx, hex, x, y, r, a) {\n if (r <= 0 || a <= 0) return;\n ctx.globalAlpha = Math.min(1, a);\n ctx.drawImage(glowSprite(hex), x - r, y - r, r * 2, r * 2);\n ctx.globalAlpha = 1;\n}\n\n/* Frame clock: seconds since start and a clamped delta. */\nfunction tick(S) {\n const now = performance.now();\n if (!S.t0) { S.t0 = now; S.last = now; }\n S.dt = Math.min(0.05, Math.max(0.001, (now - S.last) / 1000));\n S.last = now;\n S.age = (now - S.t0) / 1000;\n return S.dt;\n}\n\n/* Deterministic pseudo-random so layouts are stable across resizes. */\nfunction rng(seed) {\n let s = seed >>> 0 || 1;\n return function () {\n s = (s * 1664525 + 1013904223) >>> 0;\n return s / 4294967296;\n };\n}\n\nfunction ease(x) { return x < 0.5 ? 4 * x * x * x : 1 + (Math.pow(2 * x - 2, 3)) / 2; }\nfunction clamp(v, a, b) { return v < a ? a : v > b ? b : v; }\n\nconst SANS = 'Inter,\\u0020Helvetica,\\u0020Arial,\\u0020sans-serif';\nconst MONO = 'ui-monospace,\\u0020Menlo,\\u0020monospace';\n\nfunction roundRect(ctx, x, y, w, h, r) {\n r = Math.min(r, w / 2, h / 2);\n ctx.beginPath();\n ctx.moveTo(x + r, y);\n ctx.arcTo(x + w, y, x + w, y + h, r);\n ctx.arcTo(x + w, y + h, x, y + h, r);\n ctx.arcTo(x, y + h, x, y, r);\n ctx.arcTo(x, y, x + w, y, r);\n ctx.closePath();\n}\n\n/* Shared backdrop: flat colour, soft vignette and fine grain. */\nfunction backdrop(ctx, W, H, bg, tint, amt) {\n ctx.fillStyle = bg;\n ctx.fillRect(0, 0, W, H);\n if (tint && amt > 0) {\n const gr = ctx.createRadialGradient(W / 2, H * 0.45, 0, W / 2, H * 0.45, Math.max(W, H) * 0.7);\n gr.addColorStop(0, rgba(tint, 0.10 * amt));\n gr.addColorStop(1, rgba(tint, 0));\n ctx.fillStyle = gr;\n ctx.fillRect(0, 0, W, H);\n }\n const vg = ctx.createRadialGradient(W / 2, H / 2, Math.min(W, H) * 0.3, W / 2, H / 2, Math.max(W, H) * 0.8);\n vg.addColorStop(0, 'rgba(0,0,0,0)');\n vg.addColorStop(1, 'rgba(0,0,0,0.45)');\n ctx.fillStyle = vg;\n ctx.fillRect(0, 0, W, H);\n}\n\n/* Greedy word wrap with the current ctx.font. */\nfunction wrap(ctx, text, maxW) {\n const out = [];\n String(text).split('\\n').forEach(function (para) {\n const words = para.split('\\u0020');\n let cur = '';\n for (let i = 0; i < words.length; i++) {\n const next = cur ? cur + '\\u0020' + words[i] : words[i];\n if (cur && ctx.measureText(next).width > maxW) { out.push(cur); cur = words[i]; } else cur = next;\n }\n out.push(cur);\n });\n return out;\n}\n\n/* Largest font size (up to max) at which text wraps into maxLines within maxW. */\nfunction fitFont(ctx, text, weight, family, maxW, maxLines, max, min) {\n let fs = max;\n while (fs > min) {\n ctx.font = weight + '\\u0020' + fs + 'px\\u0020' + family;\n if (wrap(ctx, text, maxW).length <= maxLines) break;\n fs = fs * 0.92;\n }\n ctx.font = weight + '\\u0020' + fs + 'px\\u0020' + family;\n return fs;\n}\n\n/* Smoothed pointer that idles along a slow figure-eight when the cursor is away. */\nfunction follow(S, env, W, H, k) {\n const pt = env.pointer || {};\n const t = S.age || 0;\n const tx = pt.isHovered ? pt.localX : W / 2 + Math.sin(t * 0.37) * W * 0.3;\n const ty = pt.isHovered ? pt.localY : H / 2 + Math.sin(t * 0.74) * H * 0.22;\n if (S.px === undefined) { S.px = tx; S.py = ty; }\n S.px += (tx - S.px) * Math.min(1, S.dt * (k || 6));\n S.py += (ty - S.py) * Math.min(1, S.dt * (k || 6));\n S.inside = !!pt.isHovered;\n return pt;\n}\n\n/* Optional centred headline over a background effect. */\nfunction title(ctx, W, H, text, sub, ink, accent, bg) {\n if (!text) return;\n if (bg) {\n /* Soft scrim so the headline reads over busy motion. */\n const sc = ctx.createRadialGradient(W / 2, H / 2, 0, W / 2, H / 2, Math.min(W * 0.5, 560));\n sc.addColorStop(0, rgba(bg, 0.72));\n sc.addColorStop(1, rgba(bg, 0));\n ctx.fillStyle = sc;\n ctx.fillRect(0, 0, W, H);\n }\n ctx.textAlign = 'center';\n ctx.textBaseline = 'alphabetic';\n const maxW = Math.min(W * 0.86, 900);\n const fs = fitFont(ctx, text, 600, SANS, maxW, 2, clamp(W * 0.065, 26, 72), 16);\n const lines = wrap(ctx, text, maxW);\n const lh = fs * 1.08;\n const y0 = H / 2 - (lines.length * lh) / 2 + fs * 0.8 - (sub ? fs * 0.35 : 0);\n ctx.fillStyle = ink;\n lines.forEach(function (l, i) { ctx.fillText(l, W / 2, y0 + i * lh); });\n if (sub) {\n ctx.font = '500\\u0020' + clamp(fs * 0.24, 11, 15) + 'px\\u0020' + MONO;\n ctx.fillStyle = rgba(accent, 0.85);\n ctx.fillText(String(sub).toUpperCase(), W / 2, y0 + (lines.length - 1) * lh + fs * 0.9);\n }\n}\n\nconst PIECE = { n: '22', total: '57' };\n/* Shared by every motion-graphics piece (prepended after motion-ui/_kit.js).\n Same minifier rules as the kit: explicit semicolons, block comments only,\n never a minus followed by a negative value. */\n\nconst SERIF = 'Georgia,\\u0020serif';\n\n/* Value noise and fractal noise, deterministic. */\nconst NZP = (function () {\n const r = rng(1337);\n const p = [];\n for (let i = 0; i < 512; i++) p.push(Math.floor(r() * 256));\n return p;\n})();\nfunction hash2(x, y) { return NZP[(x & 255) + NZP[y & 255]] / 255; }\nfunction noise2(x, y) {\n const xi = Math.floor(x), yi = Math.floor(y);\n const xf = x - xi, yf = y - yi;\n const u = xf * xf * (3 - 2 * xf), v = yf * yf * (3 - 2 * yf);\n const a = hash2(xi, yi), b = hash2(xi + 1, yi), c = hash2(xi, yi + 1), d = hash2(xi + 1, yi + 1);\n return a + (b - a) * u + (c - a) * v + (a - b - c + d) * u * v;\n}\nfunction fbm(x, y) {\n return noise2(x, y) * 0.55 + noise2(x * 2.03 + 17.1, y * 2.03 + 3.7) * 0.3 + noise2(x * 4.1 + 5.3, y * 4.1 + 41.9) * 0.15;\n}\n\n/* Film grain: one noise tile, re-offset every frame. */\nlet GRAIN = null;\nfunction grain(ctx, W, H, a) {\n if (a <= 0) return;\n if (!GRAIN) {\n const c = document.createElement('canvas');\n c.width = 160; c.height = 160;\n const g = c.getContext('2d');\n const im = g.createImageData(160, 160);\n for (let i = 0; i < im.data.length; i += 4) {\n const v = Math.random() * 255;\n im.data[i] = v; im.data[i + 1] = v; im.data[i + 2] = v; im.data[i + 3] = 255;\n }\n g.putImageData(im, 0, 0);\n GRAIN = ctx.createPattern(c, 'repeat');\n }\n const ox = Math.floor(Math.random() * 160), oy = Math.floor(Math.random() * 160);\n ctx.save();\n ctx.globalAlpha = a;\n ctx.globalCompositeOperation = 'overlay';\n ctx.translate(ox, oy);\n ctx.fillStyle = GRAIN;\n ctx.fillRect(0 - ox, 0 - oy, W, H);\n ctx.restore();\n}\n\n/* Soft edge vignette. */\nfunction vignette(ctx, W, H, a) {\n const vg = ctx.createRadialGradient(W / 2, H / 2, Math.min(W, H) * 0.35, W / 2, H / 2, Math.max(W, H) * 0.75);\n vg.addColorStop(0, 'rgba(0,0,0,0)');\n vg.addColorStop(1, 'rgba(0,0,0,' + a + ')');\n ctx.fillStyle = vg;\n ctx.fillRect(0, 0, W, H);\n}\n\n/* Portfolio chrome: four mono captions and hairline registration marks. */\nfunction chrome(ctx, W, H, ink, name, caption) {\n const s = Math.min(W, H);\n const pad = clamp(s * 0.05, 14, 38);\n const fs = clamp(s * 0.017, 9, 11);\n ctx.save();\n ctx.font = '500\\u0020' + fs + 'px\\u0020' + MONO;\n ctx.textBaseline = 'middle';\n ctx.fillStyle = rgba(ink, 0.62);\n ctx.textAlign = 'left';\n ctx.fillText(String(name).toUpperCase(), pad, pad);\n ctx.textAlign = 'right';\n ctx.fillText(PIECE.n + '\\u0020/\\u0020' + PIECE.total, W - pad, pad);\n ctx.fillStyle = rgba(ink, 0.4);\n ctx.textAlign = 'left';\n if (W > 520) ctx.fillText(String(caption).toUpperCase(), pad, H - pad);\n ctx.textAlign = 'right';\n ctx.fillText('EXODE MOTION · 2026', W - pad, H - pad);\n ctx.strokeStyle = rgba(ink, 0.22);\n ctx.lineWidth = 1;\n const m = pad * 0.45, k = 7;\n ctx.beginPath();\n ctx.moveTo(m, m + k); ctx.lineTo(m, m); ctx.lineTo(m + k, m);\n ctx.moveTo(W - m - k, m); ctx.lineTo(W - m, m); ctx.lineTo(W - m, m + k);\n ctx.moveTo(m, H - m - k); ctx.lineTo(m, H - m); ctx.lineTo(m + k, H - m);\n ctx.moveTo(W - m - k, H - m); ctx.lineTo(W - m, H - m); ctx.lineTo(W - m, H - m - k);\n ctx.stroke();\n ctx.restore();\n}\n\n/* Spaced display text (letterSpacing where supported). */\nfunction spaced(ctx, text, x, y, em) {\n ctx.letterSpacing = em + 'em';\n ctx.fillText(text, x, y);\n ctx.letterSpacing = '0px';\n}\n\n/* Backing-store scale of the canvas, for offscreen buffers (max 2). */\nfunction dprOf(ctx) {\n const m = ctx.getTransform ? ctx.getTransform() : null;\n return m ? clamp(Math.sqrt(m.a * m.a + m.b * m.b), 1, 2) : 1;\n}\n\n/* One pointer press per click. */\nfunction pressed(S, pt) {\n const p = !!pt.isDown && !S.wasDown && !!pt.isHovered;\n S.wasDown = !!pt.isDown;\n return p;\n}\n\n/* ---------- Product launch helpers ---------- */\n\n/* Where the product sits: right of the copy on wide frames, above it on tall ones. */\nfunction productStage(W, H) {\n const land = W > H * 1.05;\n return land\n ? { land: true, cx: W * 0.67, cy: H * 0.52, size: Math.min(W * 0.5, H * 0.78) }\n : { land: false, cx: W / 2, cy: H * 0.32, size: Math.min(W * 0.88, H * 0.46) };\n}\n\n/* Studio sweep: spotlight behind the product and a soft floor. */\nfunction studio(ctx, W, H, bg, tint, st) {\n ctx.fillStyle = bg;\n ctx.fillRect(0, 0, W, H);\n const r = st.size * 1.1;\n const g = ctx.createRadialGradient(st.cx, st.cy, 0, st.cx, st.cy, r);\n g.addColorStop(0, rgba(tint, 0.22));\n g.addColorStop(0.5, rgba(tint, 0.06));\n g.addColorStop(1, rgba(tint, 0));\n ctx.fillStyle = g;\n ctx.fillRect(0, 0, W, H);\n const fy = st.cy + st.size * 0.42;\n const fl = ctx.createLinearGradient(0, fy, 0, H);\n fl.addColorStop(0, rgba(tint, 0.05));\n fl.addColorStop(1, rgba(tint, 0));\n ctx.fillStyle = fl;\n ctx.fillRect(0, fy, W, H - fy);\n}\n\n/* Soft contact shadow. */\nfunction floorShadow(ctx, x, y, w, a) {\n ctx.save();\n ctx.translate(x, y);\n ctx.scale(1, 0.16);\n const g = ctx.createRadialGradient(0, 0, 0, 0, 0, w);\n g.addColorStop(0, 'rgba(0,0,0,' + a + ')');\n g.addColorStop(1, 'rgba(0,0,0,0)');\n ctx.fillStyle = g;\n ctx.fillRect(0 - w, 0 - w, w * 2, w * 2);\n ctx.restore();\n}\n\n/* Launch copy block: eyebrow, name, tagline, three specs and a CTA pill.\n Staggers in with `appear` (seconds since start) and records CTA hover on S. */\nfunction productCopy(ctx, W, H, o, S, env) {\n const s = Math.min(W, H);\n const land = W > H * 1.05;\n const pad = clamp(s * 0.09, 20, 96);\n const cw = land ? Math.min(W * 0.34, 500) : W - pad * 2;\n /* Small frames (cards, thumbnails) drop the specs and shorten the tagline. */\n const compact = cw < 220 || H < 300;\n const fe = clamp(s * 0.016, 7, 12);\n const nameFs = fitFont(ctx, o.name, 600, SANS, cw, 2, clamp(s * 0.085, 16, 78), 12);\n const nl = wrap(ctx, o.name, cw);\n const ft = clamp(s * 0.024, 9, 18);\n ctx.font = '400\\u0020' + ft + 'px\\u0020' + SANS;\n const tl = wrap(ctx, o.tagline, cw).slice(0, compact ? 2 : 3);\n const fv = clamp(s * 0.034, 12, 26);\n const specs = compact ? [] : (o.specs || []).slice(0, 3);\n const ch = fe * 3.6;\n const total = fe * 2.6 + nl.length * nameFs * 1.02 + ft * 0.9 + tl.length * ft * 1.5 + (specs.length ? fv * 3.4 : 0) + ch + fe;\n let y = land ? H / 2 - total / 2 : H - pad - total;\n const x = pad;\n const reveal = function (i) {\n const k = ease(clamp(((o.appear || 9) - 0.15 - i * 0.12) / 0.7, 0, 1));\n ctx.globalAlpha = k;\n return (1 - k) * 18;\n };\n ctx.textAlign = 'left';\n ctx.textBaseline = 'alphabetic';\n\n let d = reveal(0);\n ctx.font = '600\\u0020' + fe + 'px\\u0020' + MONO;\n ctx.fillStyle = o.accent;\n spaced(ctx, String(o.eyebrow).toUpperCase(), x, y + fe + d, 0.22);\n y += fe * 2.6;\n\n d = reveal(1);\n ctx.font = '600\\u0020' + nameFs + 'px\\u0020' + SANS;\n ctx.fillStyle = o.ink;\n ctx.letterSpacing = '-0.025em';\n for (let i = 0; i < nl.length; i++) ctx.fillText(nl[i], x, y + nameFs * 0.86 + i * nameFs * 1.02 + d);\n ctx.letterSpacing = '0px';\n y += nl.length * nameFs * 1.02 + ft * 0.9;\n\n d = reveal(2);\n ctx.font = '400\\u0020' + ft + 'px\\u0020' + SANS;\n ctx.fillStyle = rgba(o.ink, 0.62);\n for (let i = 0; i < tl.length; i++) ctx.fillText(tl[i], x, y + ft + i * ft * 1.5 + d);\n y += tl.length * ft * 1.5;\n\n if (specs.length) {\n d = reveal(3);\n y += fv * 0.9;\n ctx.fillStyle = rgba(o.ink, 0.14);\n ctx.fillRect(x, y + d, cw, 1);\n const colw = cw / 3;\n for (let i = 0; i < specs.length; i++) {\n ctx.font = '600\\u0020' + fv + 'px\\u0020' + SANS;\n ctx.fillStyle = o.ink;\n ctx.fillText(String(specs[i][0]), x + i * colw, y + fv * 1.5 + d);\n ctx.font = '500\\u0020' + fe * 0.9 + 'px\\u0020' + MONO;\n ctx.fillStyle = rgba(o.ink, 0.45);\n ctx.fillText(String(specs[i][1]).toUpperCase(), x + i * colw, y + fv * 1.5 + fe * 1.7 + d);\n }\n y += fv * 2.5;\n }\n\n d = reveal(4);\n ctx.font = '600\\u0020' + fe * 1.2 + 'px\\u0020' + SANS;\n const label = String(o.cta);\n const bw = ctx.measureText(label).width + fe * 4;\n const by = y + d;\n const pt = env.pointer || {};\n const hot = !!pt.isHovered && pt.localX >= x && pt.localX <= x + bw && pt.localY >= by && pt.localY <= by + ch;\n S.ctaHot = hot;\n S.ctaK = (S.ctaK || 0) + ((hot ? 1 : 0) - (S.ctaK || 0)) * 0.2;\n ctx.fillStyle = mix(o.accent, '#ffffff', S.ctaK * 0.18);\n roundRect(ctx, x, by, bw, ch, ch / 2);\n ctx.fill();\n ctx.fillStyle = o.ctaInk;\n ctx.textBaseline = 'middle';\n ctx.fillText(label, x + fe * 2, by + ch / 2 + 1);\n ctx.font = '500\\u0020' + fe * 1.1 + 'px\\u0020' + SANS;\n ctx.fillStyle = rgba(o.ink, 0.7);\n if (cw > bw + 120) ctx.fillText('Learn more\\u0020\\u0020→', x + bw + fe * 2, by + ch / 2 + 1);\n ctx.textBaseline = 'alphabetic';\n ctx.globalAlpha = 1;\n}\n\n\n/* Earbuds: a glossy charging case whose lid swings open as you hover,\n lifting two earbuds out on a cushion of sound. Click to open or close.\n Inputs: name, tagline, cta, color, speed, accent, bg, text. */\nlet S = null;\n\nfunction bud(ctx, x, y, sz, ang, col, A, flip) {\n ctx.save();\n ctx.translate(x, y);\n ctx.rotate(ang);\n ctx.scale(flip ? -1 : 1, 1);\n /* Stem. */\n const sg = ctx.createLinearGradient(0 - sz * 0.12, 0, sz * 0.12, 0);\n sg.addColorStop(0, mix(col, '#000000', 0.25));\n sg.addColorStop(0.45, mix(col, '#ffffff', 0.5));\n sg.addColorStop(1, mix(col, '#000000', 0.35));\n ctx.fillStyle = sg;\n roundRect(ctx, 0 - sz * 0.11, 0, sz * 0.22, sz * 0.95, sz * 0.11);\n ctx.fill();\n ctx.fillStyle = rgba('#000000', 0.25);\n roundRect(ctx, 0 - sz * 0.07, sz * 0.82, sz * 0.14, sz * 0.08, sz * 0.04);\n ctx.fill();\n /* Head. */\n const hg = ctx.createRadialGradient(0 - sz * 0.12, 0 - sz * 0.12, sz * 0.02, 0, 0, sz * 0.42);\n hg.addColorStop(0, mix(col, '#ffffff', 0.85));\n hg.addColorStop(0.6, col);\n hg.addColorStop(1, mix(col, '#000000', 0.4));\n ctx.fillStyle = hg;\n ctx.beginPath();\n ctx.ellipse(sz * 0.06, 0 - sz * 0.02, sz * 0.36, sz * 0.32, 0.3, 0, Math.PI * 2);\n ctx.fill();\n /* Ear tip and mesh. */\n ctx.fillStyle = mix(col, '#7a7f88', 0.55);\n ctx.beginPath();\n ctx.ellipse(sz * 0.3, 0 - sz * 0.12, sz * 0.14, sz * 0.12, 0.6, 0, Math.PI * 2);\n ctx.fill();\n ctx.fillStyle = rgba('#000000', 0.55);\n ctx.beginPath();\n ctx.ellipse(0 - sz * 0.12, sz * 0.02, sz * 0.07, sz * 0.045, 0.3, 0, Math.PI * 2);\n ctx.fill();\n ctx.fillStyle = rgba(A, 0.9);\n ctx.beginPath();\n ctx.arc(0, sz * 0.62, sz * 0.025, 0, Math.PI * 2);\n ctx.fill();\n ctx.restore();\n}\n\nfunction render(ctx, env) {\n const W = env.width, H = env.height;\n if (!S) S = { open: 0, want: 1, auto: 0 };\n const dt = tick(S);\n const t = S.age;\n const speed = inp(env, 'speed', 1);\n const name = String(inp(env, 'name', 'Aura Buds Pro'));\n const tagline = String(inp(env, 'tagline', 'Silence the city. Hear every breath of the music. Up to 36 hours in a case that fits a coin pocket.'));\n const cta = String(inp(env, 'cta', 'Pre-order · $249'));\n const C = inp(env, 'color', '#f1f0ec');\n const A = inp(env, 'accent', '#7ce0c3');\n const bg = inp(env, 'bg', '#0a0b0d');\n const ink = inp(env, 'text', '#f2f3f5');\n const pt = follow(S, env, W, H, 4);\n const st = productStage(W, H);\n\n if (pressed(S, pt)) { if (S.ctaHot) { if (env.emit) env.emit('ctaClick'); } else { S.want = S.want ? 0 : 1; S.auto = 0; } }\n S.auto += dt * speed;\n if (S.auto > 5) { S.want = S.want ? 0 : 1; S.auto = 0; }\n S.open += (S.want - S.open) * Math.min(1, dt * 3.5 * speed);\n const op = S.open;\n\n ctx.save();\n ctx.translate(-W / 2, -H / 2);\n studio(ctx, W, H, bg, A, st);\n\n const sz = st.size;\n const tx = (S.px - st.cx) / W, ty = (S.py - st.cy) / H;\n const cx = st.cx + tx * sz * 0.05, cy = st.cy + sz * 0.12 + ty * sz * 0.03;\n const cw = sz * 0.62, chh = sz * 0.36, r = chh * 0.48;\n floorShadow(ctx, cx, cy + chh * 0.62, cw * 0.75, 0.55);\n\n /* Sound rings behind the buds. */\n if (op > 0.3) {\n ctx.globalCompositeOperation = 'lighter';\n for (let i = 0; i < 4; i++) {\n const u = ((t * 0.5 * speed + i / 4) % 1);\n ctx.strokeStyle = rgba(A, 0.35 * (1 - u) * (op - 0.3) / 0.7);\n ctx.lineWidth = 1.2;\n ctx.beginPath();\n ctx.ellipse(cx, cy - chh * 0.9, sz * (0.15 + u * 0.45), sz * (0.08 + u * 0.22), 0, 0, Math.PI * 2);\n ctx.stroke();\n }\n glow(ctx, A, cx, cy - chh * 0.8, sz * 0.35, 0.25 * op);\n ctx.globalCompositeOperation = 'source-over';\n }\n\n /* Case body (lower part). */\n const top = cy - chh / 2, seam = top + chh * 0.34;\n const lidH = seam - top;\n const bodyG = ctx.createLinearGradient(0, seam, 0, top + chh);\n bodyG.addColorStop(0, mix(C, '#ffffff', 0.4));\n bodyG.addColorStop(0.5, C);\n bodyG.addColorStop(1, mix(C, '#000000', 0.35));\n /* Cavity revealed when open. */\n if (op > 0.02) {\n ctx.fillStyle = mix(C, '#000000', 0.55);\n ctx.beginPath();\n ctx.ellipse(cx, seam, cw / 2 - 4, chh * 0.12 * op, 0, 0, Math.PI * 2);\n ctx.fill();\n }\n /* Open lid, tipped back behind the buds: outer shell with a dark inner rim. */\n if (Math.cos(op * 1.9) <= 0) {\n const uh = lidH * (0.5 + 0.7 * Math.sin(op * 1.9 - Math.PI / 2));\n const ly = seam - uh - lidH * 0.08;\n const og = ctx.createLinearGradient(0, ly, 0, seam);\n og.addColorStop(0, mix(C, '#ffffff', 0.5));\n og.addColorStop(1, mix(C, '#000000', 0.18));\n ctx.fillStyle = og;\n roundRect(ctx, cx - cw / 2, ly, cw, uh + lidH * 0.08, r * 0.7);\n ctx.fill();\n ctx.fillStyle = mix(C, '#000000', 0.5);\n roundRect(ctx, cx - cw / 2 + cw * 0.05, ly + uh * 0.55, cw * 0.9, uh * 0.45, r * 0.4);\n ctx.fill();\n }\n /* Buds rise out. */\n const lift = ease(clamp((op - 0.15) / 0.85, 0, 1));\n const bob = Math.sin(t * 1.6) * sz * 0.01 * lift;\n const bs = sz * 0.22;\n if (lift > 0.01) {\n ctx.save();\n ctx.beginPath();\n ctx.rect(0, 0, W, seam + 2);\n ctx.clip();\n bud(ctx, cx - cw * 0.18, seam + bs * 0.4 - lift * sz * 0.36 + bob, bs, -0.25 + lift * 0.1 + tx * 0.3, C, A, false);\n bud(ctx, cx + cw * 0.18, seam + bs * 0.4 - lift * sz * 0.36 - bob, bs, 0.25 - lift * 0.1 + tx * 0.3, C, A, true);\n ctx.restore();\n }\n ctx.save();\n ctx.beginPath();\n ctx.rect(0, seam, W, H);\n ctx.clip();\n ctx.fillStyle = bodyG;\n roundRect(ctx, cx - cw / 2, top, cw, chh, r);\n ctx.fill();\n ctx.restore();\n /* LED. */\n ctx.globalCompositeOperation = 'lighter';\n glow(ctx, A, cx, seam + chh * 0.28, 10, 0.5 + 0.5 * Math.sin(t * 3));\n ctx.globalCompositeOperation = 'source-over';\n ctx.fillStyle = mix(A, '#ffffff', 0.4);\n ctx.beginPath();\n ctx.arc(cx, seam + chh * 0.28, 2.2, 0, Math.PI * 2);\n ctx.fill();\n\n /* Lid: front face folds up and back around the hinge. */\n const ang = op * 1.9;\n const fh = lidH * Math.cos(ang);\n const lidG = ctx.createLinearGradient(0, seam - Math.abs(fh), 0, seam);\n if (fh > 0) {\n lidG.addColorStop(0, mix(C, '#ffffff', 0.7));\n lidG.addColorStop(1, mix(C, '#000000', 0.08));\n ctx.fillStyle = lidG;\n ctx.beginPath();\n ctx.moveTo(cx - cw / 2, seam);\n ctx.lineTo(cx - cw / 2, seam - fh + r * 0.6 * (fh / lidH));\n ctx.quadraticCurveTo(cx - cw / 2, seam - fh, cx - cw / 2 + r, seam - fh);\n ctx.lineTo(cx + cw / 2 - r, seam - fh);\n ctx.quadraticCurveTo(cx + cw / 2, seam - fh, cx + cw / 2, seam - fh + r * 0.6 * (fh / lidH));\n ctx.lineTo(cx + cw / 2, seam);\n ctx.closePath();\n ctx.fill();\n }\n ctx.strokeStyle = rgba('#000000', 0.25);\n ctx.lineWidth = 1;\n ctx.beginPath();\n ctx.moveTo(cx - cw / 2 + 2, seam);\n ctx.lineTo(cx + cw / 2 - 2, seam);\n ctx.stroke();\n /* Specular sweep. */\n const sw = ((t * 0.18 * speed) % 1.6) - 0.3;\n const sx = cx - cw / 2 + sw * cw;\n const spec = ctx.createLinearGradient(sx - cw * 0.1, 0, sx + cw * 0.1, 0);\n spec.addColorStop(0, 'rgba(255,255,255,0)');\n spec.addColorStop(0.5, 'rgba(255,255,255,0.35)');\n spec.addColorStop(1, 'rgba(255,255,255,0)');\n ctx.fillStyle = spec;\n ctx.save();\n roundRect(ctx, cx - cw / 2, top + (fh > 0 ? lidH - fh : lidH), cw, chh - (fh > 0 ? lidH - fh : lidH), r);\n ctx.clip();\n ctx.fillRect(sx - cw * 0.1, top, cw * 0.2, chh);\n ctx.restore();\n\n productCopy(ctx, W, H, {\n eyebrow: 'New · Adaptive audio', name: name, tagline: tagline,\n specs: [['36 h', 'Battery'], ['48 dB', 'Noise cancel'], ['4.9 g', 'Per bud']],\n cta: cta, accent: A, ink: ink, ctaInk: bg, appear: t,\n }, S, env);\n\n grain(ctx, W, H, 0.05);\n chrome(ctx, W, H, ink, 'Earbuds', 'Click to open or close');\n ctx.restore();\n}\n"
203
+ }
204
+ ]
205
+ },
206
+ "layoutProps": {
207
+ "layoutSizingHorizontal": "fill",
208
+ "layoutSizingVertical": "fill"
209
+ },
210
+ "zIndex": 1,
211
+ "interactions": []
212
+ }
213
+ ]
214
+ }