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.
- package/MCP_DOCUMENTATION.md +1 -0
- package/SKILL.md +10 -0
- package/examples/aurum-product-page.exode +3208 -0
- package/examples/bell-ring-icon.exode +1 -0
- package/examples/drift-kinetic-type.exode +770 -0
- package/examples/earbuds-product.exode +214 -0
- package/examples/energy-flow.exode +1736 -0
- package/examples/flux-hero.exode +223 -0
- package/examples/gooey-toggle.exode +214 -0
- package/examples/gravity-well.exode +3113 -0
- package/examples/heart-icon.exode +1 -0
- package/examples/hologram-temple.exode +8100 -0
- package/examples/jarful-app.exode +1 -0
- package/examples/kip-character-rig.exode +1 -0
- package/examples/low-tide-tilt.exode +2216 -0
- package/examples/manifest.json +352 -0
- package/examples/swipe-stack-carousel.exode +4048 -0
- package/examples/tokyo-neon-weather.exode +470 -0
- package/examples/typographic-collapse.exode +1578 -0
- package/examples/void-product-ad.exode +1269 -0
- package/examples/wire-globe-card.exode +580 -0
- package/index.d.ts +41 -1
- package/index.js +2553 -2407
- package/package.json +4 -2
- package/render-runtime/exode-runtime.js +20 -20
- package/skills/creative-motion/SKILL.md +10 -0
- package/skills/creative-motion/recipes/character-rig.js +104 -0
- package/skills/creative-motion/recipes/dashboard.js +120 -0
- package/skills/creative-motion/recipes/game-arcade.js +203 -0
- package/skills/creative-motion/recipes/hero-physics.js +65 -0
- package/skills/creative-motion/recipes/pricing.js +88 -0
- package/skills/creative-motion/recipes/title-sequence.js +62 -0
- package/skills/creative-motion/references/apps.md +37 -0
- package/skills/creative-motion/references/gotchas.md +80 -0
- package/skills/creative-motion/references/native-layers.md +60 -0
- package/skills/creative-motion/references/quality.md +54 -0
- package/src/__tests__/buildKit.test.ts +127 -0
- package/src/__tests__/effectKnobs.test.ts +162 -0
- package/src/__tests__/nativeLayers.test.ts +178 -0
- package/src/buildKit.d.ts +301 -0
- package/src/buildKit.d.ts.map +1 -0
- package/src/buildKit.js +559 -0
- package/src/buildKit.js.map +1 -0
- package/src/buildKit.ts +520 -0
- package/src/compiler.d.ts +4 -0
- package/src/compiler.d.ts.map +1 -1
- package/src/compiler.js +21 -45
- package/src/compiler.js.map +1 -1
- package/src/compiler.ts +22 -51
- package/src/critique.d.ts +8 -0
- package/src/critique.d.ts.map +1 -0
- package/src/critique.js +348 -0
- package/src/critique.js.map +1 -0
- package/src/critique.ts +239 -0
- package/src/effectKnobs.d.ts +72 -0
- package/src/effectKnobs.d.ts.map +1 -0
- package/src/effectKnobs.js +541 -0
- package/src/effectKnobs.js.map +1 -0
- package/src/effectKnobs.ts +445 -0
- package/src/effectOptimizer.d.ts +18 -0
- package/src/effectOptimizer.d.ts.map +1 -0
- package/src/effectOptimizer.js +125 -0
- package/src/effectOptimizer.js.map +1 -0
- package/src/effectOptimizer.ts +104 -0
- package/src/engine/layout.js +1 -1
- package/src/engine/layout.js.map +1 -1
- package/src/engine/layout.ts +1 -1
- package/src/examples.d.ts +30 -0
- package/src/examples.d.ts.map +1 -0
- package/src/examples.js +168 -0
- package/src/examples.js.map +1 -0
- package/src/examples.ts +151 -0
- package/src/handlers/artboardHandlers.d.ts.map +1 -1
- package/src/handlers/artboardHandlers.js +45 -10
- package/src/handlers/artboardHandlers.js.map +1 -1
- package/src/handlers/artboardHandlers.ts +43 -12
- package/src/handlers/buildHandlers.d.ts +2 -0
- package/src/handlers/buildHandlers.d.ts.map +1 -0
- package/src/handlers/buildHandlers.js +86 -0
- package/src/handlers/buildHandlers.js.map +1 -0
- package/src/handlers/buildHandlers.ts +76 -0
- package/src/handlers/compilerHandlers.d.ts.map +1 -1
- package/src/handlers/compilerHandlers.js +32 -2
- package/src/handlers/compilerHandlers.js.map +1 -1
- package/src/handlers/compilerHandlers.ts +31 -2
- package/src/handlers/exampleHandlers.d.ts +2 -0
- package/src/handlers/exampleHandlers.d.ts.map +1 -0
- package/src/handlers/exampleHandlers.js +81 -0
- package/src/handlers/exampleHandlers.js.map +1 -0
- package/src/handlers/exampleHandlers.ts +76 -0
- package/src/handlers/renderHandlers.d.ts.map +1 -1
- package/src/handlers/renderHandlers.js +100 -2
- package/src/handlers/renderHandlers.js.map +1 -1
- package/src/handlers/renderHandlers.ts +90 -2
- package/src/handlers/scaffoldHandlers.d.ts.map +1 -1
- package/src/handlers/scaffoldHandlers.js +3 -0
- package/src/handlers/scaffoldHandlers.js.map +1 -1
- package/src/handlers/scaffoldHandlers.ts +2 -0
- package/src/handlers/widgetHandlers.js +1 -1
- package/src/handlers/widgetHandlers.js.map +1 -1
- package/src/handlers/widgetHandlers.ts +1 -1
- package/src/lottie/convert.d.ts +16 -0
- package/src/lottie/convert.d.ts.map +1 -0
- package/src/lottie/convert.js +1225 -0
- package/src/lottie/convert.js.map +1 -0
- package/src/lottie/convert.ts +1134 -0
- package/src/lottie/evaluate.d.ts +48 -0
- package/src/lottie/evaluate.d.ts.map +1 -0
- package/src/lottie/evaluate.js +174 -0
- package/src/lottie/evaluate.js.map +1 -0
- package/src/lottie/evaluate.ts +184 -0
- package/src/nativeLayers.d.ts +76 -0
- package/src/nativeLayers.d.ts.map +1 -0
- package/src/nativeLayers.js +404 -0
- package/src/nativeLayers.js.map +1 -0
- package/src/nativeLayers.ts +339 -0
- package/src/render/motionBrief.d.ts.map +1 -1
- package/src/render/motionBrief.js +4 -0
- package/src/render/motionBrief.js.map +1 -1
- package/src/render/motionBrief.ts +4 -0
- package/src/render/videoRenderer.d.ts.map +1 -1
- package/src/render/videoRenderer.js +5 -2
- package/src/render/videoRenderer.js.map +1 -1
- package/src/render/videoRenderer.ts +5 -2
- package/src/rive/convert.d.ts +17 -0
- package/src/rive/convert.d.ts.map +1 -0
- package/src/rive/convert.js +1779 -0
- package/src/rive/convert.js.map +1 -0
- package/src/rive/convert.ts +1492 -0
- package/src/rive/font.d.ts +13 -0
- package/src/rive/font.d.ts.map +1 -0
- package/src/rive/font.js +154 -0
- package/src/rive/font.js.map +1 -0
- package/src/rive/font.ts +132 -0
- package/src/rive/layout.d.ts +20 -0
- package/src/rive/layout.d.ts.map +1 -0
- package/src/rive/layout.js +145 -0
- package/src/rive/layout.js.map +1 -0
- package/src/rive/layout.ts +140 -0
- package/src/rive/reader.d.ts +32 -0
- package/src/rive/reader.d.ts.map +1 -0
- package/src/rive/reader.js +172 -0
- package/src/rive/reader.js.map +1 -0
- package/src/rive/reader.ts +160 -0
- package/src/rive/schema.generated.d.ts +9 -0
- package/src/rive/schema.generated.d.ts.map +1 -0
- package/src/rive/schema.generated.js +13 -0
- package/src/rive/schema.generated.js.map +1 -0
- package/src/rive/schema.generated.ts +13 -0
- package/src/schema.d.ts +5 -0
- package/src/schema.d.ts.map +1 -1
- package/src/schema.js +5 -0
- package/src/schema.js.map +1 -1
- package/src/schema.ts +6 -0
- package/src/serverInstructions.d.ts +2 -0
- package/src/serverInstructions.d.ts.map +1 -0
- package/src/serverInstructions.js +22 -0
- package/src/serverInstructions.js.map +1 -0
- package/src/serverInstructions.ts +21 -0
- package/src/sseServer.d.ts +2 -0
- package/src/sseServer.d.ts.map +1 -0
- package/src/sseServer.js +165 -0
- package/src/sseServer.js.map +1 -0
- package/src/sseServer.ts +189 -0
- package/src/utils.d.ts.map +1 -1
- package/src/utils.js +23 -1
- package/src/utils.js.map +1 -1
- package/src/utils.ts +13 -1
- package/src/widgets.d.ts +26 -0
- package/src/widgets.d.ts.map +1 -1
- package/src/widgets.js +89 -45
- package/src/widgets.js.map +1 -1
- package/src/widgets.ts +88 -45
package/src/buildKit.ts
ADDED
|
@@ -0,0 +1,520 @@
|
|
|
1
|
+
// The build kit behind build_from_script: a small, opinionated builder for
|
|
2
|
+
// whole .exode documents, so an agent can write one script with loops, layout
|
|
3
|
+
// maths, palettes and staggers instead of placing objects one tool call at a
|
|
4
|
+
// time. Formats match the editor and the runtime (see the examples library).
|
|
5
|
+
// Scripts run in a vm context that only sees `exode` (this kit) and `log`.
|
|
6
|
+
import vm from 'vm';
|
|
7
|
+
|
|
8
|
+
type Frame = [number, any, string?];
|
|
9
|
+
type Fill = string | { type: string;[k: string]: any } | null | undefined;
|
|
10
|
+
|
|
11
|
+
export const EASE = {
|
|
12
|
+
out: 'easeOutCubic', outQuart: 'easeOutQuart', outExpo: 'easeOutExpo', outBack: 'easeOutBack',
|
|
13
|
+
inOut: 'easeInOutCubic', sine: 'easeInOutSine', in: 'easeInCubic', inQuad: 'easeInQuad', linear: 'linear',
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
/** Curated palettes: one neutral ramp plus a restrained accent each. */
|
|
17
|
+
export const PALETTES: Record<string, Record<string, string>> = {
|
|
18
|
+
obsidian: { bg: '#0B0A0F', surface: '#15131B', raised: '#1E1B26', line: '#2A2633', ink: '#F2EFEA', muted: '#8D8796', faint: '#5A5563', accent: '#FF8A5C', accent2: '#FFC59E', onAccent: '#140D09' },
|
|
19
|
+
midnight: { bg: '#060A12', surface: '#0E1522', raised: '#16202F', line: '#223044', ink: '#EAF1FA', muted: '#8193AA', faint: '#4A5A70', accent: '#5CE1E6', accent2: '#8B9CFF', onAccent: '#04161A' },
|
|
20
|
+
graphite: { bg: '#0D0E10', surface: '#16181B', raised: '#1F2226', line: '#2C3036', ink: '#F4F5F6', muted: '#8E949C', faint: '#596069', accent: '#C6F432', accent2: '#7DE2A8', onAccent: '#121A00' },
|
|
21
|
+
aurora: { bg: '#09070F', surface: '#130F1E', raised: '#1C162C', line: '#2B2340', ink: '#F3EEFF', muted: '#9A8FB5', faint: '#5E5577', accent: '#8F7CFF', accent2: '#3EE0C3', onAccent: '#0C0820' },
|
|
22
|
+
paper: { bg: '#F4F1EA', surface: '#FBF9F4', raised: '#FFFFFF', line: '#DDD6C8', ink: '#141210', muted: '#6E675C', faint: '#A39B8D', accent: '#E8432E', accent2: '#1F4ED8', onAccent: '#FFFFFF' },
|
|
23
|
+
bone: { bg: '#EFEBE3', surface: '#F7F4EE', raised: '#FFFFFF', line: '#D5CEC1', ink: '#1C1B18', muted: '#6B675E', faint: '#A19B8F', accent: '#3F5A3C', accent2: '#B8862F', onAccent: '#F7F4EE' },
|
|
24
|
+
porcelain: { bg: '#F5F5F7', surface: '#FFFFFF', raised: '#FFFFFF', line: '#E3E3E8', ink: '#1D1D1F', muted: '#86868B', faint: '#B8B8BE', accent: '#0071E3', accent2: '#BF4800', onAccent: '#FFFFFF' },
|
|
25
|
+
swiss: { bg: '#FFFFFF', surface: '#F3F3F1', raised: '#FFFFFF', line: '#1A1A1A', ink: '#111111', muted: '#6B6B6B', faint: '#B5B5B5', accent: '#E30613', accent2: '#111111', onAccent: '#FFFFFF' },
|
|
26
|
+
clay: { bg: '#F1E7DD', surface: '#F8F1EA', raised: '#FFFFFF', line: '#DCCBBB', ink: '#2A1D16', muted: '#8A7466', faint: '#B9A595', accent: '#C2562C', accent2: '#6F8F7A', onAccent: '#FFF7F0' },
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
// ── colour helpers ───────────────────────────────────────────────────────
|
|
30
|
+
const hex = (c: string) => { let h = c.replace('#', ''); if (h.length === 3) h = h.split('').map((x) => x + x).join(''); return [0, 2, 4].map((i) => parseInt(h.slice(i, i + 2), 16)); };
|
|
31
|
+
const toHex = (rgb: number[]) => '#' + rgb.map((v) => Math.max(0, Math.min(255, Math.round(v))).toString(16).padStart(2, '0')).join('').toUpperCase();
|
|
32
|
+
export const mix = (a: string, b: string, t: number) => { const A = hex(a), B = hex(b); return toHex(A.map((v, i) => v + (B[i]! - v) * t)); };
|
|
33
|
+
export const alpha = (c: string, a: number) => { const [r, g, b] = hex(c); return `rgba(${r}, ${g}, ${b}, ${Math.max(0, Math.min(1, a))})`; };
|
|
34
|
+
|
|
35
|
+
/** Seeded random (mulberry32) so builds are deterministic. */
|
|
36
|
+
export function rng(seed = 1) {
|
|
37
|
+
let s = seed >>> 0;
|
|
38
|
+
const next = () => { s = (s + 0x6D2B79F5) >>> 0; let t = s; t = Math.imul(t ^ (t >>> 15), t | 1); t ^= t + Math.imul(t ^ (t >>> 7), t | 61); return ((t ^ (t >>> 14)) >>> 0) / 4294967296; };
|
|
39
|
+
return Object.assign(next, { range: (a: number, b: number) => a + (b - a) * next(), pick: <T>(arr: T[]) => arr[Math.floor(next() * arr.length)]! });
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Modular type scale: typeScale(16, 1.333) → { xs, sm, base, md, lg, xl, xxl, display }. */
|
|
43
|
+
export function typeScale(base = 16, ratio = 1.333) {
|
|
44
|
+
const s = (n: number) => Math.round(base * Math.pow(ratio, n));
|
|
45
|
+
return { xs: s(-2), sm: s(-1), base, md: s(1), lg: s(2), xl: s(3), xxl: s(4), display: s(6) };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function linear(stops: string[] | { offset: number; color: string; opacity?: number }[], angle = 90) {
|
|
49
|
+
const a = ((angle - 90) * Math.PI) / 180;
|
|
50
|
+
const dx = Math.cos(a) / 2, dy = Math.sin(a) / 2;
|
|
51
|
+
return { type: 'LinearGradient', opacity: 1, start: [0.5 - dx, 0.5 - dy], end: [0.5 + dx, 0.5 + dy], stops: normStops(stops) };
|
|
52
|
+
}
|
|
53
|
+
export function radial(stops: string[] | { offset: number; color: string; opacity?: number }[], center: [number, number] = [0.5, 0.5], radius = 0.5) {
|
|
54
|
+
/* radius is a fraction of max(w, h): 0.5 reaches a centred ellipse's edge, so a glow fades out before the shape clips it */
|
|
55
|
+
return { type: 'RadialGradient', opacity: 1, center, radius, stops: normStops(stops) };
|
|
56
|
+
}
|
|
57
|
+
/** Gradient stops must be hex + a separate opacity: the runtime turns any non-hex colour (rgba()) into white. */
|
|
58
|
+
const splitColour = (c: string): { color: string; opacity: number } => {
|
|
59
|
+
const m = String(c).match(/rgba?\(([^)]+)\)/i);
|
|
60
|
+
if (!m) return { color: c, opacity: 1 };
|
|
61
|
+
const [r, g, b, a] = m[1]!.split(',').map((v) => parseFloat(v));
|
|
62
|
+
return { color: toHex([r!, g!, b!]), opacity: a === undefined || isNaN(a) ? 1 : a };
|
|
63
|
+
};
|
|
64
|
+
const normStops = (stops: any[]) => stops.map((s, i) => {
|
|
65
|
+
const offset = stops.length === 1 ? 0 : i / (stops.length - 1);
|
|
66
|
+
if (typeof s === 'string') return { offset, ...splitColour(s) };
|
|
67
|
+
const c = splitColour(s.color); return { offset, ...s, color: c.color, opacity: (s.opacity ?? 1) * c.opacity };
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
const TRANSFORM = new Set(['x', 'y', 'rotation', 'scale_x', 'scale_y', 'opacity', 'skew_x', 'skew_y']);
|
|
71
|
+
const propPath = (p: string) => TRANSFORM.has(p) ? `transform.${p}` : p === 'fill' ? 'style.fill.color' : p === 'text' ? 'geometry.text' : p;
|
|
72
|
+
const slug = (s: string) => s.toLowerCase().replace(/[^a-z0-9]+/g, '_').replace(/^_|_$/g, '') || 'obj';
|
|
73
|
+
const nz = (v: number) => (v === 0 ? 0.001 : v);
|
|
74
|
+
|
|
75
|
+
/** Width of the longest line, from per-character advance classes (close to Inter / Helvetica metrics). */
|
|
76
|
+
export function estimateTextWidth(str: string, size: number, letterSpacing = 0, weight = 400) {
|
|
77
|
+
const adv = (ch: string) => /[il.,:;|!'`]/.test(ch) ? 0.27 : /[fjrtI]/.test(ch) ? 0.36 : ch === ' ' ? 0.28
|
|
78
|
+
: /[mwMW@%]/.test(ch) ? 0.88 : /[A-Z]/.test(ch) ? 0.68 : /[0-9$€£]/.test(ch) ? 0.6 : 0.55;
|
|
79
|
+
const bold = weight >= 700 ? 1.08 : weight >= 600 ? 1.05 : 1;
|
|
80
|
+
const lineW = (l: string) => [...l].reduce((n, ch) => n + adv(ch), 0) * size * bold + Math.max(0, l.length - 1) * letterSpacing;
|
|
81
|
+
return Math.ceil(Math.max(...String(str).split('\n').map(lineW)) + 4);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export class ArtboardBuilder {
|
|
85
|
+
doc: any;
|
|
86
|
+
ease: string;
|
|
87
|
+
private ids = new Set<string>();
|
|
88
|
+
private n = 0;
|
|
89
|
+
warnings: string[] = [];
|
|
90
|
+
|
|
91
|
+
constructor(o: { name?: string; width?: number; height?: number; background?: string; palette?: string; ease?: string } = {}) {
|
|
92
|
+
const pal = PALETTES[o.palette || ''];
|
|
93
|
+
this.ease = o.ease || EASE.out;
|
|
94
|
+
this.doc = {
|
|
95
|
+
id: `ab_${slug(o.name || 'artboard')}`, name: o.name || 'Artboard', width: o.width ?? 1440, height: o.height ?? 900,
|
|
96
|
+
cornerRadius: 0, fit: 'Contain', backgroundColor: o.background || pal?.bg || '#0B0A0F', opacity: 1,
|
|
97
|
+
objects: [], animations: [], navigation: [], animationFlow: [], stateVariables: [],
|
|
98
|
+
stateMachine: { inputs: [], layers: [], logicNodes: [] },
|
|
99
|
+
physics: { enabled: false, gravity: { x: 0, y: 9.8 } },
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
private uid(base: string) {
|
|
104
|
+
let id = slug(base); if (!this.ids.has(id)) { this.ids.add(id); return id; }
|
|
105
|
+
let k = 2; while (this.ids.has(`${id}_${k}`)) k++; id = `${id}_${k}`; this.ids.add(id); return id;
|
|
106
|
+
}
|
|
107
|
+
private ref(t: any): string { return typeof t === 'string' ? t : t?.id; }
|
|
108
|
+
get(idOrName: string) { return this.doc.objects.find((o: any) => o.id === idOrName || o.name === idOrName); }
|
|
109
|
+
|
|
110
|
+
private base(type: string, o: any, w: number, h: number) {
|
|
111
|
+
this.n++;
|
|
112
|
+
const name = o.name || `${type} ${this.n}`;
|
|
113
|
+
const x = o.x ?? (o.left !== undefined ? o.left + w / 2 : o.right !== undefined ? o.right - w / 2 : 0);
|
|
114
|
+
const y = o.y ?? (o.top !== undefined ? o.top + h / 2 : o.bottom !== undefined ? o.bottom - h / 2 : 0);
|
|
115
|
+
const sc = o.scale ?? 1;
|
|
116
|
+
const obj: any = {
|
|
117
|
+
id: o.id ? this.uid(o.id) : this.uid(name), name, type, isVisible: o.visible !== false, isLocked: false,
|
|
118
|
+
transform: { x, y, rotation: o.rotation ?? 0, scale_x: nz(o.scaleX ?? sc), scale_y: nz(o.scaleY ?? sc), opacity: Math.max(0, Math.min(1, o.opacity ?? 1)), skew_x: 0, skew_y: 0 },
|
|
119
|
+
children: [], interactions: [],
|
|
120
|
+
};
|
|
121
|
+
const parent = o.parent ? this.ref(o.parent) : undefined;
|
|
122
|
+
if (parent) obj.parentId = parent;
|
|
123
|
+
return obj;
|
|
124
|
+
}
|
|
125
|
+
private add(obj: any) {
|
|
126
|
+
obj.zIndex = this.doc.objects.length;
|
|
127
|
+
this.doc.objects.push(obj);
|
|
128
|
+
if (obj.parentId) {
|
|
129
|
+
const p = this.get(obj.parentId);
|
|
130
|
+
if (p) p.children.push(obj.id); else this.warnings.push(`${obj.name}: parent ${obj.parentId} not found`);
|
|
131
|
+
}
|
|
132
|
+
return obj;
|
|
133
|
+
}
|
|
134
|
+
private style(o: any) {
|
|
135
|
+
const f: Fill = o.fill;
|
|
136
|
+
const fill = !f ? { type: 'None', color: 'transparent', opacity: 0 }
|
|
137
|
+
: typeof f === 'string' ? { type: 'Solid', color: f, opacity: Math.max(0, Math.min(1, o.fillOpacity ?? 1)) } : f;
|
|
138
|
+
const s: any = { fill, stroke: o.stroke ? { color: o.stroke, width: o.strokeWidth ?? 1, opacity: 1, isEnabled: true } : { color: 'transparent', width: 0 } };
|
|
139
|
+
if (o.shadow) s.shadow = { color: '#000000', blur: 24, offsetX: 0, offsetY: 12, spread: -8, opacity: 40, isEnabled: true, ...(o.shadow === true ? {} : o.shadow) };
|
|
140
|
+
if (o.blendMode) s.blendMode = o.blendMode;
|
|
141
|
+
return s;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
rect(o: any = {}) {
|
|
145
|
+
const w = o.w ?? 100, h = o.h ?? 100, r = o.r ?? 0;
|
|
146
|
+
return this.add({ ...this.base('Shape', o, w, h), width: w, height: h, geometry: { type: 'Rectangle', width: w, height: h, cornerRadius: r, corner_radius: [r, r, r, r] }, style: this.style(o), ...(o.clip ? { clipContent: true } : {}) });
|
|
147
|
+
}
|
|
148
|
+
ellipse(o: any = {}) {
|
|
149
|
+
let w = o.w ?? o.size ?? 100, h = o.h ?? o.size ?? 100;
|
|
150
|
+
/* a radial fill's radius follows max(w, h), so on an oval the short sides cut the glow into a hard edge:
|
|
151
|
+
draw a circle and squash it with scale_y instead (soft all round) */
|
|
152
|
+
if (o.fill && typeof o.fill === 'object' && o.fill.type === 'RadialGradient' && w !== h) {
|
|
153
|
+
const d = Math.max(w, h); o = { ...o, scaleX: (o.scaleX ?? o.scale ?? 1) * (w / d), scaleY: (o.scaleY ?? o.scale ?? 1) * (h / d) }; w = h = d;
|
|
154
|
+
}
|
|
155
|
+
return this.add({ ...this.base('Shape', o, w, h), width: w, height: h, geometry: { type: 'Ellipse', width: w, height: h }, style: this.style(o) });
|
|
156
|
+
}
|
|
157
|
+
/** Path data centred on (0,0) = the object's position (author it around its own bbox centre). */
|
|
158
|
+
path(data: string, o: any = {}) {
|
|
159
|
+
const w = o.w ?? 100, h = o.h ?? 100;
|
|
160
|
+
return this.add({ ...this.base('Shape', o, w, h), width: w, height: h, geometry: { type: 'Path', data, width: w, height: h, baseWidth: w, baseHeight: h }, style: this.style({ ...o, fill: o.fill ?? null }) });
|
|
161
|
+
}
|
|
162
|
+
text(str: string, o: any = {}) {
|
|
163
|
+
const size = o.size ?? 16, lh = o.lineHeight ?? (size >= 40 ? 1.05 : 1.4), lines = String(str).split('\n').length;
|
|
164
|
+
const ls = o.letterSpacing ?? (size >= 40 ? -size * 0.02 : 0);
|
|
165
|
+
const weight = o.weight ?? 400;
|
|
166
|
+
const need = estimateTextWidth(str, size, ls, weight);
|
|
167
|
+
/* A text box wraps at its width, and the editor measures real glyphs (tracking included), so a box sized
|
|
168
|
+
to an estimate can break onto two lines in the editor while the player shows one. Single lines get a
|
|
169
|
+
box well wider than the text: it never wraps, and left/centre/right alignment anchor to the same box
|
|
170
|
+
edge in both renderers. (Point text is avoided: the editor centres it on x, the player doesn't.)
|
|
171
|
+
Paragraphs (longer than the given w, or wrap: true) keep their width and wrap. */
|
|
172
|
+
const paragraph = o.wrap === true || (o.wrap !== false && o.w !== undefined && need > o.w);
|
|
173
|
+
const w = paragraph ? (o.w ?? need) : Math.max(o.w ?? 0, Math.ceil(need * 1.3 + 16));
|
|
174
|
+
const wrapped = String(str).split('\n').reduce((n, l) => n + Math.max(1, Math.ceil(estimateTextWidth(l, size, ls, weight) / w)), 0);
|
|
175
|
+
const h = o.h ?? Math.ceil(size * lh * (paragraph ? Math.max(lines, wrapped) : lines) + 4);
|
|
176
|
+
const font = String(o.font ?? 'Inter').split(',')[0]!.replace(/["']/g, '').trim();
|
|
177
|
+
return this.add({
|
|
178
|
+
...this.base('Shape', { ...o, name: o.name || String(str).split('\n')[0]!.slice(0, 32) }, w, h),
|
|
179
|
+
geometry: { type: 'Text', text: str, fontSize: size, fontFamily: font, fontWeight: weight, textAlign: o.align ?? 'left', textVerticalAlign: 'middle', letterSpacing: ls, lineHeight: lh, width: w, height: h },
|
|
180
|
+
style: { fill: { type: 'Solid', color: o.color ?? '#FFFFFF', opacity: 1 } },
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
image(src: string, o: any = {}) {
|
|
184
|
+
const w = o.w ?? 200, h = o.h ?? 200;
|
|
185
|
+
return this.add({ ...this.base('Shape', o, w, h), width: w, height: h, geometry: { type: 'Image', src, width: w, height: h }, style: { fill: { type: 'None', color: 'transparent', opacity: 0 }, stroke: { color: 'transparent', width: 0 } } });
|
|
186
|
+
}
|
|
187
|
+
svg(content: string, o: any = {}) {
|
|
188
|
+
const w = o.w ?? 24, h = o.h ?? 24;
|
|
189
|
+
return this.add({ ...this.base('Shape', o, w, h), width: w, height: h, geometry: { type: 'SVG', svgContent: content, width: w, height: h }, style: { fill: { type: 'None', color: 'transparent', opacity: 0 }, stroke: { color: 'transparent', width: 0 } } });
|
|
190
|
+
}
|
|
191
|
+
/** A Group: hugs its children; child x/y are relative to it. */
|
|
192
|
+
group(o: any = {}) {
|
|
193
|
+
const w = o.w ?? 10, h = o.h ?? 10;
|
|
194
|
+
return this.add({ ...this.base('Group', o, w, h), width: w, height: h, clipContent: !!o.clip, geometry: { type: 'Rectangle', width: w, height: h, cornerRadius: o.r ?? 0 }, style: { fill: { type: 'None', color: 'transparent', opacity: 0 }, stroke: { color: 'transparent', width: 0 } } });
|
|
195
|
+
}
|
|
196
|
+
/** Invisible hit area (fill opacity 0.001). Add it last so it sits on top. */
|
|
197
|
+
hit(o: any = {}) { return this.rect({ ...o, name: o.name || 'Hit area', fill: '#000000', fillOpacity: 0.001 }); }
|
|
198
|
+
|
|
199
|
+
// ── inputs, animation, state machine ────────────────────────────────────
|
|
200
|
+
input(name: string, type: 'Number' | 'Boolean' | 'Text' | 'Color' | 'Trigger', value?: any) {
|
|
201
|
+
const existing = this.doc.stateMachine.inputs.find((i: any) => i.name === name);
|
|
202
|
+
if (existing) return existing.id;
|
|
203
|
+
const id = `in_${slug(name)}`;
|
|
204
|
+
const v = type === 'Trigger' ? false : value ?? (type === 'Number' ? 0 : type === 'Boolean' ? false : '');
|
|
205
|
+
this.doc.stateMachine.inputs.push({ id, name, value: { type, value: v, defaultValue: v } });
|
|
206
|
+
return id;
|
|
207
|
+
}
|
|
208
|
+
/** frames: [[time, value, easing?], …]. Easing applies to the segment that STARTS at that frame. */
|
|
209
|
+
track(target: any, prop: string, frames: Frame[]): any {
|
|
210
|
+
const p = propPath(prop);
|
|
211
|
+
if (prop === 'scale') return [this.track(target, 'scale_x', frames), this.track(target, 'scale_y', frames)] as any;
|
|
212
|
+
const isOpacity = /opacity$/.test(p);
|
|
213
|
+
return {
|
|
214
|
+
id: `t_${++this.n}`, object_id: this.ref(target), property: p,
|
|
215
|
+
keyframes: frames.map(([t, v, e]) => ({ id: `k_${++this.n}`, time: t, value: isOpacity && typeof v === 'number' ? Math.max(0, Math.min(1, v)) : v, easing: e || this.ease })),
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
anim(name: string, tracks: any[], o: { duration?: number | undefined; loop?: boolean } = {}) {
|
|
219
|
+
const flat = tracks.flat(3).filter(Boolean);
|
|
220
|
+
const end = Math.max(0.01, ...flat.flatMap((t: any) => t.keyframes.map((k: any) => k.time)));
|
|
221
|
+
const id = this.uid(`anim_${name}`);
|
|
222
|
+
this.doc.animations.push({ id, name, duration: o.duration ?? end, loop: !!o.loop, tracks: flat });
|
|
223
|
+
return id;
|
|
224
|
+
}
|
|
225
|
+
/** layer('Name', [{ name, anim, loop, on: { inputName: 'Target state name' } }]) — first state is the entry. */
|
|
226
|
+
layer(name: string, states: { name: string; anim?: string; loop?: boolean; on?: Record<string, string | { to: string; op?: string; value?: any }> }[]) {
|
|
227
|
+
const sid = (s: string) => `s_${slug(name)}_${slug(s)}`;
|
|
228
|
+
const inputId = (n: string) => this.doc.stateMachine.inputs.find((i: any) => i.name === n || i.id === n)?.id;
|
|
229
|
+
const built = states.map((s) => ({
|
|
230
|
+
id: sid(s.name), name: s.name, animationId: s.anim, loop: !!s.loop,
|
|
231
|
+
transitions: Object.entries(s.on || {}).map(([inp, to]) => {
|
|
232
|
+
const t = typeof to === 'string' ? { to } : to;
|
|
233
|
+
const id = inputId(inp);
|
|
234
|
+
if (!id) this.warnings.push(`layer ${name}: input "${inp}" not declared (call input() first)`);
|
|
235
|
+
return { id: `tr_${++this.n}`, targetStateId: sid(t.to), duration: 0, conditions: [{ inputId: id || inp, inputName: inp, op: t.op || 'IsTrue', value: t.value ?? true }] };
|
|
236
|
+
}),
|
|
237
|
+
}));
|
|
238
|
+
this.doc.stateMachine.layers.push({ id: this.uid(`layer_${name}`), name, entryStateId: built[0]!.id, states: built });
|
|
239
|
+
return built.map((s) => s.id);
|
|
240
|
+
}
|
|
241
|
+
/** One-shot layer that plays on load. */
|
|
242
|
+
intro(tracks: any[], name = 'Intro') { const a = this.anim(name, tracks); this.layer(name, [{ name, anim: a }]); return a; }
|
|
243
|
+
/** Looping layer that plays forever. Make the last frame equal the first. */
|
|
244
|
+
ambient(tracks: any[], name = 'Ambient', duration?: number) { const a = this.anim(name, tracks, { loop: true, duration }); this.layer(name, [{ name, anim: a, loop: true }]); return a; }
|
|
245
|
+
|
|
246
|
+
/** Staggered entrance tracks: each target fades in and travels `dy` px into place. */
|
|
247
|
+
reveal(targets: any[], o: { dy?: number; dx?: number; duration?: number; step?: number; delay?: number; ease?: string; scaleFrom?: number } = {}) {
|
|
248
|
+
const dur = o.duration ?? 0.7, step = o.step ?? 0.06, delay = o.delay ?? 0, e = o.ease ?? this.ease, dy = o.dy ?? 24, dx = o.dx ?? 0;
|
|
249
|
+
return targets.flatMap((t, i) => {
|
|
250
|
+
const obj = this.get(this.ref(t)); if (!obj) return [];
|
|
251
|
+
const t0 = delay + i * step, tr = obj.transform;
|
|
252
|
+
const out = [this.track(obj, 'opacity', [[0, 0, 'linear'], [t0, 0, e], [t0 + dur * 0.8, tr.opacity]])];
|
|
253
|
+
if (dy) out.push(this.track(obj, 'y', [[0, tr.y + dy, 'linear'], [t0, tr.y + dy, e], [t0 + dur, tr.y]]));
|
|
254
|
+
if (dx) out.push(this.track(obj, 'x', [[0, tr.x + dx, 'linear'], [t0, tr.x + dx, e], [t0 + dur, tr.x]]));
|
|
255
|
+
if (o.scaleFrom !== undefined) out.push(...(this.track(obj, 'scale', [[0, o.scaleFrom, 'linear'], [t0, o.scaleFrom, e], [t0 + dur, tr.scale_x]]) as any));
|
|
256
|
+
return out;
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
/** Same frames on every target, offset by `step` seconds each. */
|
|
260
|
+
stagger(targets: any[], prop: string, frames: Frame[], step = 0.06) {
|
|
261
|
+
return targets.map((t, i) => this.track(t, prop, frames.map(([tm, v, e]) => [tm + i * step, v, e] as Frame)));
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Hover + press feel on one object, in its own layer (that layer owns the
|
|
266
|
+
* target's y, scale and shadow, so don't animate those elsewhere: put
|
|
267
|
+
* entrances on a parent group). Interactions go on `hit` or the target.
|
|
268
|
+
*/
|
|
269
|
+
interactive(target: any, o: { lift?: number; scale?: number; press?: number; duration?: number; shadowBlur?: [number, number]; hit?: any; onTap?: string } = {}) {
|
|
270
|
+
const obj = this.get(this.ref(target)); if (!obj) { this.warnings.push(`interactive: ${this.ref(target)} not found`); return; }
|
|
271
|
+
const key = obj.name, lift = o.lift ?? 3, s = o.scale ?? 1.02, ps = s * (o.press ?? 0.97), d = o.duration ?? 0.22;
|
|
272
|
+
const y0 = obj.transform.y, s0 = obj.transform.scale_x;
|
|
273
|
+
const enter = this.input(`${key} hover`, 'Trigger'), leave = this.input(`${key} leave`, 'Trigger'), press = this.input(`${key} press`, 'Trigger');
|
|
274
|
+
const pose = (from: [number, number], to: [number, number], blur?: [number, number]) => [
|
|
275
|
+
this.track(obj, 'y', [[0, from[0]], [d, to[0]]]), ...(this.track(obj, 'scale', [[0, from[1]], [d, to[1]]]) as any),
|
|
276
|
+
...(blur ? [this.track(obj, 'style.shadow.blur', [[0, blur[0]], [d, blur[1]]])] : []),
|
|
277
|
+
];
|
|
278
|
+
const sb = o.shadowBlur;
|
|
279
|
+
const rest = this.anim(`${key} rest`, pose([y0, s0], [y0, s0]).map((t: any) => ({ ...t, keyframes: [t.keyframes[0]] })), { duration: 0.01 });
|
|
280
|
+
const inn = this.anim(`${key} hover in`, pose([y0, s0], [y0 - lift, s], sb && [sb[0], sb[1]]));
|
|
281
|
+
const out = this.anim(`${key} hover out`, pose([y0 - lift, s], [y0, s0], sb && [sb[1], sb[0]]));
|
|
282
|
+
const pr = this.anim(`${key} press`, [this.track(obj, 'y', [[0, y0 - lift], [0.26, y0 - lift]]), ...(this.track(obj, 'scale', [[0, s, EASE.out], [0.08, ps, EASE.outBack], [0.26, s]]) as any)]);
|
|
283
|
+
const n = (x: string) => x;
|
|
284
|
+
this.layer(`${key} interaction`, [
|
|
285
|
+
{ name: 'Rest', anim: rest, on: { [`${key} hover`]: 'Hover', [`${key} press`]: 'Press A' } },
|
|
286
|
+
{ name: 'Hover', anim: inn, on: { [`${key} leave`]: 'Leave', [`${key} press`]: 'Press A' } },
|
|
287
|
+
{ name: 'Leave', anim: out, on: { [`${key} hover`]: 'Hover', [`${key} press`]: 'Press A' } },
|
|
288
|
+
{ name: 'Press A', anim: pr, on: { [`${key} press`]: 'Press B', [`${key} leave`]: 'Leave' } },
|
|
289
|
+
{ name: 'Press B', anim: pr, on: { [`${key} press`]: 'Press A', [`${key} leave`]: 'Leave' } },
|
|
290
|
+
].map((st) => ({ ...st, name: n(st.name) })));
|
|
291
|
+
const host = o.hit ? this.get(this.ref(o.hit)) : obj;
|
|
292
|
+
const ix = (event: string, input: string) => ({ id: `int_${++this.n}`, event, trigger: event, triggers: [event], action: 'fireTrigger', targetInputId: input });
|
|
293
|
+
host.interactions.push(ix('onPointerEnter', enter), ix('onPointerLeave', leave), ix('onPointerDown', press));
|
|
294
|
+
if (o.onTap) host.interactions.push({ ...ix('onClick', this.input(o.onTap, 'Trigger')) });
|
|
295
|
+
return { enter, leave, press };
|
|
296
|
+
}
|
|
297
|
+
/** Fire a trigger input when `target` is clicked. */
|
|
298
|
+
onTap(target: any, triggerName: string) {
|
|
299
|
+
const obj = this.get(this.ref(target)); const id = this.input(triggerName, 'Trigger');
|
|
300
|
+
obj.interactions.push({ id: `int_${++this.n}`, event: 'onClick', trigger: 'onClick', triggers: ['onClick', 'Click'], action: 'fireTrigger', targetInputId: id });
|
|
301
|
+
return id;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// ── ready-made pieces (the hard-to-get-right parts, done right) ─────────
|
|
305
|
+
/** Soft light: a circle with a radial fade to 0 (scaleY makes it an oval without a hard edge). */
|
|
306
|
+
glow(o: { name?: string; x: number; y: number; size: number; color: string; strength?: number; scaleY?: number; parent?: any; blendMode?: string }) {
|
|
307
|
+
return this.ellipse({ name: o.name || 'Glow', x: o.x, y: o.y, size: o.size, parent: o.parent, scaleY: o.scaleY ?? 1, blendMode: o.blendMode,
|
|
308
|
+
fill: radial([{ offset: 0, color: o.color, opacity: o.strength ?? 0.35 }, { offset: 0.45, color: o.color, opacity: (o.strength ?? 0.35) * 0.35 }, { offset: 1, color: o.color, opacity: 0 }], [0.5, 0.5], 0.5) });
|
|
309
|
+
}
|
|
310
|
+
/** Looping track with a phase offset: f(u) for u in 0..1 over `duration`, started at `phase` (0..1). */
|
|
311
|
+
loopTrack(target: any, prop: string, f: (u: number) => any, duration: number, phase = 0, samples = [0, 0.15, 0.5, 0.85, 1]) {
|
|
312
|
+
const pts = samples.map((u) => ({ t: ((u - phase + 1) % 1) * duration, v: f(u), u }));
|
|
313
|
+
const wrapAt = (1 - phase) % 1 * duration;
|
|
314
|
+
const keys: Frame[] = [];
|
|
315
|
+
pts.filter((p) => p.u < 1).sort((a, b) => a.t - b.t).forEach((p) => keys.push([p.t, p.v, 'linear']));
|
|
316
|
+
if (phase > 0) { keys.push([Math.max(0, wrapAt - 0.001), f(1), 'linear']); keys.push([wrapAt, f(0), 'linear']); }
|
|
317
|
+
keys.sort((a, b) => a[0] - b[0]);
|
|
318
|
+
if (keys[0]![0] > 0) keys.unshift([0, f(phase), 'linear']);
|
|
319
|
+
keys.push([duration, f(phase), 'linear']);
|
|
320
|
+
const uniq = keys.filter((k, i) => i === 0 || k[0] > keys[i - 1]![0]);
|
|
321
|
+
return this.track(target, prop, uniq);
|
|
322
|
+
}
|
|
323
|
+
/** Drifting particles (embers, dust, bubbles, stars) as native dots in a group, with their looping tracks.
|
|
324
|
+
* Pass the returned tracks to ambient(). rise < 0 sinks; spread sways sideways. */
|
|
325
|
+
particles(o: { name?: string; count?: number; x: number; y: number; w: number; h: number; color: string; size?: [number, number]; rise?: number; spread?: number; opacity?: number; duration?: number; seed?: number }) {
|
|
326
|
+
const r = rng(o.seed ?? 3), n = o.count ?? 24, D = o.duration ?? 6, rise = o.rise ?? 120, sway = o.spread ?? 16, a = o.opacity ?? 0.8;
|
|
327
|
+
const g = this.group({ name: o.name || 'Particles', x: o.x, y: o.y, w: o.w, h: o.h });
|
|
328
|
+
const tracks: any[] = [];
|
|
329
|
+
for (let i = 0; i < n; i++) {
|
|
330
|
+
const sz = r.range(...(o.size ?? [2, 5])), px = r.range(-o.w / 2, o.w / 2), py = r.range(-o.h / 2, o.h / 2), ph = r(), peak = a * r.range(0.4, 1);
|
|
331
|
+
const dot = this.ellipse({ name: `${o.name || 'Particle'} ${i + 1}`, parent: g, x: px, y: py, size: sz, fill: o.color });
|
|
332
|
+
tracks.push(this.loopTrack(dot, 'y', (u) => py + rise / 2 - rise * u, D, ph));
|
|
333
|
+
tracks.push(this.loopTrack(dot, 'opacity', (u) => (u < 0.15 ? peak * (u / 0.15) : u > 0.85 ? peak * ((1 - u) / 0.15) : peak), D, ph));
|
|
334
|
+
if (sway) tracks.push(this.loopTrack(dot, 'x', (u) => px + Math.sin(u * Math.PI * 2) * sway, D, ph, [0, 0.25, 0.5, 0.75, 1]));
|
|
335
|
+
}
|
|
336
|
+
return { group: g, tracks };
|
|
337
|
+
}
|
|
338
|
+
/** Count a Text up to its value with proper formatting ("12,480", "$48.2k", "92%").
|
|
339
|
+
* Digit-leading strings interpolate as bare numbers (parseFloat): each step is prefixed with a
|
|
340
|
+
* zero-width space so the engine swaps formatted text instead. Returns the track for intro(). */
|
|
341
|
+
countUp(target: any, to: number, o: { from?: number; delay?: number; duration?: number; steps?: number; prefix?: string; suffix?: string; decimals?: number; ease?: (u: number) => number } = {}) {
|
|
342
|
+
const obj = this.get(this.ref(target));
|
|
343
|
+
const from = o.from ?? 0, delay = o.delay ?? 0, dur = o.duration ?? 1.4, n = o.steps ?? 28, dec = o.decimals ?? 0;
|
|
344
|
+
const ease = o.ease ?? ((u: number) => 1 - Math.pow(1 - u, 3));
|
|
345
|
+
const fmt = (v: number) => `${o.prefix ?? ''}${v.toLocaleString('en-US', { minimumFractionDigits: dec, maximumFractionDigits: dec })}${o.suffix ?? ''}`;
|
|
346
|
+
const ZW = '\u200B';
|
|
347
|
+
const keys: Frame[] = [[0, ZW + fmt(from), 'linear']];
|
|
348
|
+
for (let i = 1; i <= n; i++) { const u = i / n; keys.push([delay + dur * u, i === n ? fmt(to) : ZW + fmt(from + (to - from) * ease(u)), 'linear']); }
|
|
349
|
+
if (delay > 0) keys.splice(1, 0, [delay, ZW + fmt(from), 'linear']);
|
|
350
|
+
obj.geometry.text = fmt(to);
|
|
351
|
+
return this.track(obj, 'text', keys.filter((k, i, a) => i === 0 || k[0] > a[i - 1]![0]));
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/** A complete button: pill + label as one unit (the label rides along), with hover and press built in.
|
|
355
|
+
* variant 'primary' (filled), 'secondary' (outlined) or 'ghost' (text only). */
|
|
356
|
+
button(label: string, o: { name?: string; x?: number; y?: number; left?: number; top?: number; parent?: any; w?: number; h?: number; variant?: 'primary' | 'secondary' | 'ghost'; fill?: string; color?: string; line?: string; size?: number; font?: string; radius?: number; glow?: boolean; onTap?: string }) {
|
|
357
|
+
const size = o.size ?? 15, h = o.h ?? Math.round(size * 3.2), w = o.w ?? Math.round(estimateTextWidth(label, size) + size * 3);
|
|
358
|
+
const v = o.variant ?? 'primary';
|
|
359
|
+
const pill = this.rect({ name: o.name || `${label} button`, x: o.x, y: o.y, left: o.left, top: o.top, parent: o.parent, w, h, r: o.radius ?? h / 2,
|
|
360
|
+
fill: v === 'primary' ? (o.fill ?? '#FFFFFF') : v === 'secondary' ? (o.fill ?? '#FFFFFF') : '#000000', fillOpacity: v === 'primary' ? 1 : v === 'secondary' ? 0.04 : 0.001,
|
|
361
|
+
stroke: v === 'secondary' ? (o.line ?? 'rgba(255,255,255,0.18)') : undefined, strokeWidth: 1,
|
|
362
|
+
shadow: v === 'primary' && o.glow ? { color: (o.fill ?? '#FFFFFF').slice(0, 7), blur: 18, offsetY: 8, opacity: 35 } : undefined });
|
|
363
|
+
this.text(label, { name: `${label} label`, parent: pill, x: 0, y: 0, size, weight: 600, font: o.font, align: 'center', w, color: o.color ?? (v === 'primary' ? '#111111' : '#FFFFFF') });
|
|
364
|
+
this.interactive(pill, { lift: 2, scale: 1.03, press: 0.96, ...(o.glow && v === 'primary' ? { shadowBlur: [18, 30] as [number, number] } : {}), ...(o.onTap ? { onTap: o.onTap } : {}) });
|
|
365
|
+
return pill;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// ── characters ────────────────────────────────────────────────────────
|
|
369
|
+
/** An FK bone. Shapes given parent: bone follow it rigidly (x/y relative to the bone). */
|
|
370
|
+
bone(o: { name: string; x?: number; y?: number; length?: number; rotation?: number; parent?: any; id?: string }) {
|
|
371
|
+
return this.add({
|
|
372
|
+
...this.base('Bone', o, 0, 0), width: 0, height: 0,
|
|
373
|
+
geometry: { type: 'Bone', length: o.length ?? 40, isTip: false },
|
|
374
|
+
style: { stroke: { color: '#EC4899', width: 2, opacity: 1, isEnabled: true }, fill: { type: 'None', opacity: 0 } },
|
|
375
|
+
options: { type: 'Bone', children: [], connected: false, mode: 'fk' },
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
/**
|
|
379
|
+
* A character's action layer, built the way that works at runtime:
|
|
380
|
+
* - every action animates the SAME set of tracks (missing ones are held at rest), so no pose leaks;
|
|
381
|
+
* - one-shots always end on the rest pose; loops are welcome (loop: true);
|
|
382
|
+
* - each action gets a trigger (its name, lowercased) and alternating a/b states so it replays on every fire.
|
|
383
|
+
* actions: { Wave: { duration: 2, loop?: false, tracks: [[obj, 'rotation', frames], …] }, … }
|
|
384
|
+
* Keep breathing/blinking in their own layers on objects the actions never touch.
|
|
385
|
+
*/
|
|
386
|
+
actions(layerName: string, actions: Record<string, { duration: number; loop?: boolean; tracks: [any, string, Frame[]][] }>) {
|
|
387
|
+
const restOf = (id: string, prop: string) => { const o = this.get(id); const k = prop.replace(/^transform\./, ''); return o?.transform?.[k] ?? (k.startsWith('scale') ? 1 : 0); };
|
|
388
|
+
const keyOf = (t: any, prop: string) => `${this.ref(t)}|${prop === 'scale' ? 'scale' : prop}`;
|
|
389
|
+
const union = new Map<string, [string, string]>();
|
|
390
|
+
for (const a of Object.values(actions)) for (const [t, prop] of a.tracks) {
|
|
391
|
+
for (const p of prop === 'scale' ? ['scale_x', 'scale_y'] : [prop]) union.set(`${this.ref(t)}|${p}`, [this.ref(t), p]);
|
|
392
|
+
}
|
|
393
|
+
const build = (name: string, a: { duration: number; loop?: boolean; tracks: [any, string, Frame[]][] } | null) => {
|
|
394
|
+
const given = new Map<string, Frame[]>();
|
|
395
|
+
for (const [t, prop, frames] of a?.tracks || []) for (const p of prop === 'scale' ? ['scale_x', 'scale_y'] : [prop]) given.set(`${this.ref(t)}|${p}`, frames);
|
|
396
|
+
const dur = a ? a.duration : 0.1;
|
|
397
|
+
const tracks: any[] = [];
|
|
398
|
+
for (const [k, [id, p]] of union) {
|
|
399
|
+
const rest = restOf(id, p);
|
|
400
|
+
let f = given.get(k);
|
|
401
|
+
if (!f) f = [[0, rest], [dur, rest]];
|
|
402
|
+
else if (a && !a.loop) { const last = f[f.length - 1]!; if (last[1] !== rest) f = [...f, [Math.max(last[0] + 0.15, dur), rest]]; }
|
|
403
|
+
tracks.push(this.track(id, p, f));
|
|
404
|
+
}
|
|
405
|
+
return this.anim(name, tracks, { duration: Math.max(dur, ...tracks.flatMap((t: any) => t.keyframes.map((k: any) => k.time))), loop: !!a?.loop });
|
|
406
|
+
};
|
|
407
|
+
const rest = build(`${layerName} rest`, null);
|
|
408
|
+
const names = Object.keys(actions);
|
|
409
|
+
const trig = (n: string) => n.toLowerCase().replace(/[^a-z0-9]+/g, '');
|
|
410
|
+
names.forEach((n) => this.input(trig(n), 'Trigger'));
|
|
411
|
+
const anims = Object.fromEntries(names.map((n) => [n, build(n, actions[n]!)]));
|
|
412
|
+
const go = (except?: string, self?: string) => Object.fromEntries(names.map((n) => [trig(n), n === except ? self! : `${n} a`]));
|
|
413
|
+
const states: any[] = [{ name: 'Rest', anim: rest, on: go() }];
|
|
414
|
+
for (const n of names) {
|
|
415
|
+
states.push({ name: `${n} a`, anim: anims[n], loop: !!actions[n]!.loop, on: go(n, `${n} b`) });
|
|
416
|
+
states.push({ name: `${n} b`, anim: anims[n], loop: !!actions[n]!.loop, on: go(n, `${n} a`) });
|
|
417
|
+
}
|
|
418
|
+
this.layer(layerName, states);
|
|
419
|
+
return Object.fromEntries(names.map((n) => [n, trig(n)]));
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// ── physics ─────────────────────────────────────────────────────────────
|
|
423
|
+
/** World physics. gravity in m/s² (9.8 = earth). effects: { squash: { amount }, impact: { color, size, minSpeed }, shake: { amount } }. */
|
|
424
|
+
physics(o: { gravity?: [number, number]; effects?: any; timeScale?: number } = {}) {
|
|
425
|
+
const [gx, gy] = o.gravity ?? [0, 9.8];
|
|
426
|
+
this.doc.physics = { enabled: true, gravity: { x: gx, y: gy }, wind: { x: 0, y: 0 }, timeScale: o.timeScale ?? 1, collideWithBounds: true, joints: [], ...(o.effects ? { effects: o.effects } : {}) };
|
|
427
|
+
}
|
|
428
|
+
/** Static floor and side walls just outside the artboard (and a ceiling if asked). inset raises the floor. */
|
|
429
|
+
walls(o: { floor?: boolean; sides?: boolean; ceiling?: boolean; inset?: number } = {}) {
|
|
430
|
+
const W = this.doc.width, H = this.doc.height, t = 60, inset = o.inset ?? 0, out: any[] = [];
|
|
431
|
+
/* explicit walls replace the artboard bounds, so bodies can start above the top edge and fall in */
|
|
432
|
+
if (this.doc.physics) this.doc.physics.collideWithBounds = !!o.ceiling;
|
|
433
|
+
const wall = (name: string, x: number, y: number, w: number, h: number) => { const r = this.rect({ name, x, y, w, h, fill: this.doc.backgroundColor, fillOpacity: 0 }); this.body(r, { type: 'Static', friction: 0.8, restitution: 0.1 }); out.push(r); };
|
|
434
|
+
if (o.floor !== false) wall('Floor', W / 2, H + t / 2 - inset, W * 3, t);
|
|
435
|
+
if (o.sides !== false) { wall('Wall left', -t / 2, H / 2, t, H * 3); wall('Wall right', W + t / 2, H / 2, t, H * 3); }
|
|
436
|
+
if (o.ceiling) wall('Ceiling', W / 2, -t / 2, W * 3, t);
|
|
437
|
+
return out;
|
|
438
|
+
}
|
|
439
|
+
/** Make an object a physics body. Weighty defaults: low bounce, a little air friction. */
|
|
440
|
+
body(target: any, o: { type?: 'Dynamic' | 'Static' | 'Kinematic'; density?: number; restitution?: number; friction?: number; air?: number; mass?: number; draggable?: boolean; sensor?: boolean } = {}) {
|
|
441
|
+
const obj = this.get(this.ref(target));
|
|
442
|
+
obj.physics = { enabled: true, bodyType: o.type ?? 'Dynamic', mass: o.mass ?? 1, density: o.density ?? 0.0016, friction: o.friction ?? 0.6, restitution: o.restitution ?? 0.2, frictionAir: o.air ?? 0.015, isSensor: !!o.sensor };
|
|
443
|
+
if (o.draggable) obj.behaviors = [...(obj.behaviors || []), { id: `drag_${obj.id}`, type: 'Draggable', enabled: true, sensors: [], settings: {}, startTime: 0, endTime: 100 }];
|
|
444
|
+
return obj;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// ── code ────────────────────────────────────────────────────────────────
|
|
448
|
+
/** Attach a CustomEffect: function render(ctx, env) and/or update(env, dt). */
|
|
449
|
+
effect(target: any, code: string, o: { name?: string; params?: any } = {}) {
|
|
450
|
+
const obj = this.get(this.ref(target));
|
|
451
|
+
obj.style = obj.style || {};
|
|
452
|
+
obj.style.effects = [...(obj.style.effects || []), { id: `fx_${++this.n}`, type: 'CustomEffect', name: o.name || `${obj.name} effect`, isEnabled: true, params: o.params || {}, code }];
|
|
453
|
+
return obj;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
done() {
|
|
457
|
+
/* With scene physics on, the runtime makes EVERY Shape without an explicit physics block a dynamic body,
|
|
458
|
+
so text, buttons and backgrounds would fall. Only objects given body() take part. */
|
|
459
|
+
if (this.doc.physics?.enabled) for (const o of this.doc.objects) if (!o.physics) o.physics = { enabled: false };
|
|
460
|
+
return this.doc;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
export function createKit() {
|
|
465
|
+
return {
|
|
466
|
+
artboard: (o?: any) => new ArtboardBuilder(o),
|
|
467
|
+
palette: (name: string) => { const p = PALETTES[name]; if (!p) throw new Error(`Unknown palette "${name}". Palettes: ${Object.keys(PALETTES).join(', ')}`); return { ...p }; },
|
|
468
|
+
palettes: Object.keys(PALETTES),
|
|
469
|
+
typeScale, ease: EASE, rng, mix, alpha, linear, radial, measure: estimateTextWidth,
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
export const KIT_REFERENCE = `exode kit (global \`exode\`), return the builder or its doc at the end:
|
|
474
|
+
const d = exode.artboard({ name, width: 1440, height: 900, palette: 'obsidian', ease: exode.ease.out });
|
|
475
|
+
const P = exode.palette('obsidian'); // bg surface raised line ink muted faint accent accent2 onAccent
|
|
476
|
+
palettes: ${Object.keys(PALETTES).join(', ')} (or pass background: '#hex' and your own colours)
|
|
477
|
+
const T = exode.typeScale(16, 1.333); // xs sm base md lg xl xxl display
|
|
478
|
+
exode.linear(['#a','#b'], angleDeg) / exode.radial(['#a', exode.alpha('#a', 0)], [cx,cy], r=0.5) → fill (glows: fade the last stop to alpha 0 and keep r ≤ 0.5 on an ellipse); exode.mix(a,b,t), exode.alpha(c,a), exode.rng(seed)
|
|
479
|
+
Objects (x,y = centre; or left/top/right/bottom edges; parent: obj → x,y relative to parent):
|
|
480
|
+
d.rect({ name, x, y, w, h, r, fill, fillOpacity, stroke, strokeWidth, shadow: true|{blur,offsetY,opacity}, clip, opacity, rotation, scale })
|
|
481
|
+
d.ellipse({ name, x, y, w, h | size, fill }) · d.path(data, { name, x, y, w, h, stroke, strokeWidth, fill })
|
|
482
|
+
d.text(str, { name, x|left, y|top, size, weight, font: 'Inter', color, align: 'left'|'center'|'right', w, lineHeight, letterSpacing, wrap })
|
|
483
|
+
(single lines get a roomy box so they never wrap in the player or the editor; text longer than w wraps as a paragraph; wrap: true/false forces it)
|
|
484
|
+
d.group({ name, x, y, w, h, clip }) · d.image(src, { w, h }) · d.svg(markup, { w, h }) · d.hit({ x, y, w, h })
|
|
485
|
+
Motion (frames = [[time, value, easing?]]; easing sits on the frame where a segment starts):
|
|
486
|
+
d.track(obj, 'y'|'x'|'opacity'|'scale'|'rotation'|'fill'|'text'|'style.shadow.blur'|…, frames)
|
|
487
|
+
d.reveal([objs], { dy: 24, duration: 0.7, step: 0.06, delay, scaleFrom }) → tracks · d.stagger([objs], prop, frames, step)
|
|
488
|
+
d.intro(tracks) (plays once on load) · d.ambient(tracks, name, duration) (loops; last frame = first)
|
|
489
|
+
d.anim(name, tracks, { loop }) · d.input(name, 'Number'|'Boolean'|'Text'|'Color'|'Trigger', value)
|
|
490
|
+
d.layer(name, [{ name: 'Idle', anim, loop, on: { inputName: 'Other state' } }, …])
|
|
491
|
+
d.interactive(obj, { lift: 3, scale: 1.02, press: 0.97, shadowBlur: [16, 28], hit, onTap: 'triggerName' }) → hover/leave/press layer
|
|
492
|
+
d.onTap(obj, 'triggerName')
|
|
493
|
+
Ready-made (use these, they get the details right):
|
|
494
|
+
d.button('Get started', { left, y, variant: 'primary'|'secondary'|'ghost', fill, color, glow, onTap }) → pill + label as one unit, hover + press built in
|
|
495
|
+
d.glow({ x, y, size, color, strength: 0.35, scaleY }) → soft light, no hard edge
|
|
496
|
+
const p = d.particles({ x, y, w, h, color, count: 24, size: [2,5], rise: 120, spread: 16, duration: 6 }); d.ambient(p.tracks, 'Embers')
|
|
497
|
+
d.loopTrack(obj, prop, u => value, duration, phase) → seamless loop with a phase offset
|
|
498
|
+
d.countUp(textObj, 48210, { prefix: '$', suffix: 'k', decimals: 1, delay, duration }) → formatted count-up track (never mangled into a bare number)
|
|
499
|
+
line draw-in: d.track(path, 'style.stroke.trimEnd', [[0, 0.002], [1.2, 1]])
|
|
500
|
+
Characters: const b = d.bone({ name, x, y, length, rotation, parent }); shapes take parent: b (rigid)
|
|
501
|
+
d.actions('Action', { Wave: { duration: 2, tracks: [[armR, 'rotation', frames], …] }, Dance: { duration: 3, loop: true, tracks } })
|
|
502
|
+
→ full-coverage poses, one-shots end at rest, trigger per action ('wave'), replayable; buttons: d.button('Wave', { onTap: 'wave' })
|
|
503
|
+
Physics: d.physics({ gravity: [0, 9.8], effects: { squash: { amount: 0.5 }, impact: { color, size, minSpeed: 500 }, shake: { amount: 0.4 } } })
|
|
504
|
+
d.walls({ floor, sides, ceiling, inset }) · d.body(obj, { type: 'Dynamic', density, restitution: 0.2, friction, air: 0.015, draggable })
|
|
505
|
+
Code: d.effect(obj, 'function render(ctx, env){…}', { name, params }) (follow the effect-code topic rules)
|
|
506
|
+
Rules: one owner per animated property (an object's interactive() layer owns its y/scale: put entrances on a parent group);
|
|
507
|
+
opacity ≤ 1; name every layer; reuse one Text per role instead of stacking hidden ones.`;
|
|
508
|
+
|
|
509
|
+
/** Run an agent's build script in an isolated context. */
|
|
510
|
+
export function runBuildScript(script: string, opts: { timeoutMs?: number } = {}) {
|
|
511
|
+
const logs: string[] = [];
|
|
512
|
+
const kit = createKit();
|
|
513
|
+
const context = vm.createContext({ exode: kit, log: (...a: any[]) => { if (logs.length < 200) logs.push(a.map((x) => typeof x === 'string' ? x : JSON.stringify(x)).join(' ')); }, Math, JSON, Number, String, Array, Object }, { codeGeneration: { strings: false, wasm: false } });
|
|
514
|
+
const wrapped = `(function(){\n${script}\n})()`;
|
|
515
|
+
const result = new vm.Script(wrapped, { filename: 'build.js' }).runInContext(context, { timeout: opts.timeoutMs ?? 5000 });
|
|
516
|
+
const doc = result instanceof ArtboardBuilder ? result.done() : result && typeof result === 'object' && typeof result.done === 'function' ? result.done() : result;
|
|
517
|
+
if (!doc || typeof doc !== 'object' || !Array.isArray(doc.objects)) throw new Error('The script must end with `return d` (the builder) or return an artboard object with objects[].');
|
|
518
|
+
const warnings = result?.warnings || [];
|
|
519
|
+
return { doc: JSON.parse(JSON.stringify(doc)), logs, warnings };
|
|
520
|
+
}
|
package/src/compiler.d.ts
CHANGED
|
@@ -222,6 +222,10 @@ export declare function compileCssToExode(cssString: string): {
|
|
|
222
222
|
artboard: any;
|
|
223
223
|
validation: any;
|
|
224
224
|
};
|
|
225
|
+
export declare function compileRiveToExode(rivBytes: Uint8Array, artboardName?: string): {
|
|
226
|
+
artboard: any;
|
|
227
|
+
validation: any;
|
|
228
|
+
};
|
|
225
229
|
export declare function compileLottieToExode(lottieJson: any): {
|
|
226
230
|
artboard: any;
|
|
227
231
|
validation: any;
|
package/src/compiler.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"compiler.d.ts","sourceRoot":"","sources":["compiler.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,WAAW,EAAE,cAAc,EAAE,gBAAgB,EAAE,cAAc,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"compiler.d.ts","sourceRoot":"","sources":["compiler.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,WAAW,EAAE,cAAc,EAAE,gBAAgB,EAAE,cAAc,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAS1I,MAAM,WAAW,OAAO;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;IACX,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,OAAO,CAAC;CACjB;AAoBD,wBAAgB,WAAW,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAEnD;AAED,wBAAgB,YAAY;;;;;;;;;;;;IAc3B;AAID,MAAM,WAAW,QAAQ;IACvB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,aAAa,EAAE,MAAM,CAAC;IACtB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,eAAO,MAAM,UAAU,EAAE,QAAQ,EAWhC,CAAC;AAEF,wBAAgB,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,QAAQ,CAEjD;AAID,MAAM,MAAM,UAAU,GAAG,UAAU,GAAG,YAAY,GAAG,aAAa,GAAG,YAAY,GAAG,YAAY,GAAG,UAAU,CAAC;AAC9G,MAAM,MAAM,aAAa,GAAG,MAAM,GAAG,SAAS,GAAG,WAAW,GAAG,OAAO,GAAG,QAAQ,CAAC;AAClF,MAAM,MAAM,YAAY,GAAG,WAAW,GAAG,OAAO,GAAG,UAAU,GAAG,OAAO,CAAC;AACxE,MAAM,MAAM,OAAO,GAAG,OAAO,GAAG,gBAAgB,GAAG,gBAAgB,GAAG,cAAc,GAAG,YAAY,CAAC;AACpG,MAAM,MAAM,eAAe,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,MAAM,GAAG,WAAW,GAAG,SAAS,GAAG,SAAS,CAAC;AAEvG,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,QAAQ,GAAG,MAAM,GAAG,OAAO,CAAC;IACxF,SAAS,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IACxD,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAC3B,aAAa,EAAE,MAAM,CAAC;IAAC,aAAa,EAAE,MAAM,CAAC;IAC7C,cAAc,EAAE,MAAM,GAAG,OAAO,GAAG,MAAM,CAAC;CAC3C;AAED,wBAAgB,aAAa,CAAC,IAAI,EAAE,UAAU,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,YAAY,CAelF;AAID,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,eAAe,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAmB1G;AAGD,MAAM,WAAW,QAAQ;IACvB,IAAI,EAAE,GAAG,CAAC;IACV,UAAU,CAAC,EAAE;QAAE,KAAK,EAAE,eAAe,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE,CAAC;CACtD;AAED,wBAAgB,OAAO,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,GAAG,QAAQ,CAe1F;AAKD,wBAAgB,GAAG,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAE1C;AAID,MAAM,MAAM,WAAW,GAAG,aAAa,GAAG,aAAa,GAAG,eAAe,GAAG,QAAQ,GAAG,UAAU,GAAG,UAAU,GAAG,gBAAgB,GAAG,gBAAgB,GAAG,YAAY,GAAG,iBAAiB,GAAG,oBAAoB,CAAC;AAE/M,MAAM,WAAW,cAAc;IAC7B,YAAY,EAAE,WAAW,EAAE,CAAC;IAC5B,eAAe,EAAE,cAAc,EAAE,CAAC;IAClC,UAAU,EAAE,cAAc,EAAE,CAAC;IAC7B,WAAW,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,GAAG,CAAA;KAAE,EAAE,CAAC;IACxD,cAAc,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,aAAa,CAAA;KAAE,EAAE,CAAC;IAChE,WAAW,CAAC,EAAE,UAAU,EAAE,CAAC;IAC3B,OAAO,CAAC,EAAE,aAAa,CAAC,SAAS,CAAC,CAAC;IACnC,mBAAmB,EAAE;QACnB,WAAW,EAAE;YAAE,UAAU,EAAE,MAAM,CAAC;YAAC,MAAM,EAAE,GAAG,EAAE,CAAA;SAAE,EAAE,CAAC;QACrD,gBAAgB,EAAE;YAAE,QAAQ,EAAE,MAAM,CAAC;YAAC,WAAW,EAAE,gBAAgB,CAAA;SAAE,EAAE,CAAC;KACzE,CAAC;IACF,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACjC;AA22BD,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,GAAG,EAAE,MAAM,CAAC;IACZ,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,UAAU,CAAC;IACpB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,WAAW,CAAC,EAAE,SAAS,GAAG,QAAQ,GAAG,SAAS,GAAG,MAAM,CAAC;IACxD,OAAO,CAAC,EAAE,WAAW,CAAC;IACtB,aAAa,CAAC,EAAE,aAAa,CAAC;IAC9B,YAAY,CAAC,EAAE,YAAY,CAAC;IAC5B,eAAe,CAAC,EAAE,eAAe,CAAC;CACnC;AAED,wBAAgB,WAAW,CAAC,IAAI,EAAE,cAAc,GAAG;IAAE,QAAQ,EAAE,aAAa,CAAC;IAAC,UAAU,EAAE,GAAG,CAAA;CAAE,CAgW9F;AAiDD,wBAAgB,aAAa,IAAI,OAAO,CAGvC;AAED,wBAAgB,WAAW,IAAI,MAAM,EAAE,CAEtC;AAED,wBAAgB,YAAY,IAAI,MAAM,EAAE,CAcvC;AAID,wBAAgB,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAiI9B;AAmCD,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,MAAM,GAAG;IAAE,QAAQ,EAAE,GAAG,CAAC;IAAC,UAAU,EAAE,GAAG,CAAA;CAAE,CA4DvF;AAID,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,UAAU,EAAE,YAAY,CAAC,EAAE,MAAM,GAAG;IAAE,QAAQ,EAAE,GAAG,CAAC;IAAC,UAAU,EAAE,GAAG,CAAA;CAAE,CAOlH;AAKD,wBAAgB,oBAAoB,CAAC,UAAU,EAAE,GAAG,GAAG;IAAE,QAAQ,EAAE,GAAG,CAAC;IAAC,UAAU,EAAE,GAAG,CAAA;CAAE,CAQxF"}
|