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
|
@@ -0,0 +1,445 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lifts the tunable numbers out of CustomEffect code so they show up as parameters.
|
|
3
|
+
*
|
|
4
|
+
* The editor's Effects panel turns every top-level `var NAME = <number | colour | boolean>` into a
|
|
5
|
+
* control (slider, colour picker, toggle). Generated effects usually bury those values inside
|
|
6
|
+
* render()/update(): `ctx.lineWidth = 3.4`, `L.rgba(col, 0.75 * ax)`, `Math.sin(t * 2.4)`. This pass
|
|
7
|
+
* moves them to named top-level variables and puts the name where the number was, so the effect
|
|
8
|
+
* looks and behaves exactly the same until someone moves a slider.
|
|
9
|
+
*
|
|
10
|
+
* Only values with a clear meaning are lifted (nothing structural like indices, loop bounds or ids):
|
|
11
|
+
* - style assignments: x.lineWidth = 3.4 → WIDTH, x.globalAlpha → ALPHA, x.shadowBlur → BLUR,
|
|
12
|
+
* x.fillStyle / strokeStyle / shadowColor = '#5eeaff' → FILL / STROKE / SHADOW
|
|
13
|
+
* - time rates: t * 2.4, env.time * 0.35, dt * 2.6 → SPEED
|
|
14
|
+
* - colour opacity: a 0..1 factor multiplied inside an rgba()/hsla() call or a fill/stroke style
|
|
15
|
+
* expression → ALPHA
|
|
16
|
+
* Names come from the section comment above the code (`/* temple tank ripples *\/` → TEMPLE_TANK_RIPPLES_SPEED),
|
|
17
|
+
* else from the helper function's name. Running it twice changes nothing.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
export interface LiftedKnob { name: string; value: number | string; count: number }
|
|
21
|
+
export interface LiftResult { code: string; knobs: LiftedKnob[]; changed: boolean; reason?: string }
|
|
22
|
+
|
|
23
|
+
type Tok = { t: 'id' | 'num' | 'str' | 'punc' | 'com'; v: string; s: number; e: number; depth: number; nl: boolean };
|
|
24
|
+
|
|
25
|
+
const KNOB_MARK = '/* settings: each one is a parameter in the Effects panel */';
|
|
26
|
+
const STYLE_NUM: Record<string, string> = { lineWidth: 'WIDTH', globalAlpha: 'ALPHA', shadowBlur: 'BLUR' };
|
|
27
|
+
const STYLE_COLOR: Record<string, string> = { fillStyle: 'FILL', strokeStyle: 'STROKE', shadowColor: 'SHADOW' };
|
|
28
|
+
const COLOR_CALL = /^(rgba?|hsla?|withAlpha|alpha)$/i;
|
|
29
|
+
const STOP = new Set(['a', 'an', 'the', 'of', 'for', 'and', 'or', 'to', 'in', 'on', 'under', 'with', 'from', 'by', 'at', 'is', 'it', 'its', 'that', 'this', 'every', 'each', 'all', 'no', 'not']);
|
|
30
|
+
const NOTE = /^(no|not|if|when|while|only|skip|keep|else|otherwise|unless|note|todo|fixme|so|but|then|also|never|always)\b/i;
|
|
31
|
+
const snake = (n: string) => n.replace(/([a-z0-9])([A-Z])/g, '$1_$2').toUpperCase();
|
|
32
|
+
const isColor = (s: string) => /^(#[0-9a-f]{3,8}|rgba?\([^)]*\)|hsla?\([^)]*\))$/i.test(s.trim());
|
|
33
|
+
|
|
34
|
+
function tokenize(code: string): Tok[] {
|
|
35
|
+
const out: Tok[] = [];
|
|
36
|
+
let depth = 0, nl = true;
|
|
37
|
+
for (let i = 0; i < code.length;) {
|
|
38
|
+
const c = code[i]!, n = code[i + 1];
|
|
39
|
+
if (c === '\n') { nl = true; i++; continue; }
|
|
40
|
+
if (/\s/.test(c)) { i++; continue; }
|
|
41
|
+
const push = (t: Tok['t'], e: number) => { out.push({ t, v: code.slice(i, e), s: i, e, depth, nl }); nl = false; i = e; };
|
|
42
|
+
if (c === '/' && n === '/') { const e = code.indexOf('\n', i); push('com', e < 0 ? code.length : e); continue; }
|
|
43
|
+
if (c === '/' && n === '*') { const e = code.indexOf('*/', i + 2); push('com', e < 0 ? code.length : e + 2); continue; }
|
|
44
|
+
if (c === '"' || c === "'" || c === '`') {
|
|
45
|
+
let j = i + 1;
|
|
46
|
+
while (j < code.length && code[j] !== c) { if (code[j] === '\\') j++; j++; }
|
|
47
|
+
push('str', Math.min(code.length, j + 1)); continue;
|
|
48
|
+
}
|
|
49
|
+
if (/[0-9]/.test(c) || (c === '.' && /[0-9]/.test(n || ''))) {
|
|
50
|
+
const m = /^(?:0[xX][0-9a-fA-F]+|(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?)/.exec(code.slice(i))!;
|
|
51
|
+
push('num', i + m[0].length); continue;
|
|
52
|
+
}
|
|
53
|
+
if (/[A-Za-z_$]/.test(c)) { const m = /^[A-Za-z_$][\w$]*/.exec(code.slice(i))!; push('id', i + m[0].length); continue; }
|
|
54
|
+
/* regex literals are rare in effect code; a `/` is treated as division */
|
|
55
|
+
if (c === '}' || c === ')' || c === ']') depth = Math.max(0, depth - 1);
|
|
56
|
+
push('punc', i + 1);
|
|
57
|
+
if (c === '{' || c === '(' || c === '[') depth++;
|
|
58
|
+
}
|
|
59
|
+
return out;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** "white scan sweep: bloom and core" → WHITE_SCAN_SWEEP; "bloom under the native edges" → BLOOM */
|
|
63
|
+
function sectionName(comment: string): string {
|
|
64
|
+
let s = comment.replace(/^\/\/+|^\/\*+|\*+\/$/g, '').trim();
|
|
65
|
+
/* "no logic running: …", "if the user …" are notes about the next line, not section headings */
|
|
66
|
+
if (NOTE.test(s)) return '';
|
|
67
|
+
s = s.split(/[:(.,;—–]|\s-\s/)[0]!;
|
|
68
|
+
const words = s.toLowerCase().split(/[^a-z0-9]+/).filter(Boolean);
|
|
69
|
+
while (words.length && STOP.has(words[0]!)) words.shift();
|
|
70
|
+
const out: string[] = [];
|
|
71
|
+
for (const w of words) { if (STOP.has(w) || out.length === 3) break; out.push(w); }
|
|
72
|
+
return out.join('_').toUpperCase();
|
|
73
|
+
}
|
|
74
|
+
const fnName = (n: string) => (/^(render|update|animate|draw|main|tick|step)$/i.test(n) ? '' : snake(n));
|
|
75
|
+
|
|
76
|
+
const STATE_WORD: Record<string, string> = { sel: 'SELECTED', selected: 'SELECTED', hov: 'HOVER', hover: 'HOVER', hovered: 'HOVER', active: 'ACTIVE', on: 'ON', pressed: 'PRESSED', down: 'PRESSED', focus: 'FOCUS', disabled: 'DISABLED' };
|
|
77
|
+
/** The ternary condition ending at token q (`?`): its last identifier, `i === T.sel` → SELECTED. */
|
|
78
|
+
function conditionName(toks: Tok[], q: number): string {
|
|
79
|
+
const d = toks[q]!.depth;
|
|
80
|
+
let named = '';
|
|
81
|
+
for (let j = q - 1; j >= 0; j--) {
|
|
82
|
+
const t = toks[j]!;
|
|
83
|
+
if (t.depth < d || (t.depth === d && /^[;,{}(:?]$/.test(t.v)) || (t.depth === d && t.v === '=' && toks[j - 1]?.v !== '=' && toks[j + 1]?.v !== '=')) break;
|
|
84
|
+
if (t.t !== 'id') continue;
|
|
85
|
+
/* a state word anywhere in the condition wins (p === T.sel && g !== 1 → SELECTED) */
|
|
86
|
+
if (STATE_WORD[t.v.toLowerCase()]) return STATE_WORD[t.v.toLowerCase()]!;
|
|
87
|
+
if (!named && t.depth === d && t.v.length >= 3) named = snake(t.v);
|
|
88
|
+
}
|
|
89
|
+
return named;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** What the statement holding token k assigns to: `T.cam.yaw -= dt * 1.2` → YAW, `var damp = …` → DAMP. */
|
|
93
|
+
function assignTarget(toks: Tok[], k: number): string {
|
|
94
|
+
const d = toks[k]!.depth;
|
|
95
|
+
let a = k - 1;
|
|
96
|
+
for (; a >= 0; a--) {
|
|
97
|
+
const t = toks[a]!;
|
|
98
|
+
if (t.depth <= d && (t.v === ';' || t.v === '{' || t.v === '}')) break;
|
|
99
|
+
}
|
|
100
|
+
for (let j = a + 1; j < k; j++) {
|
|
101
|
+
const t = toks[j]!;
|
|
102
|
+
if (t.v !== '=' || t.depth > d) continue;
|
|
103
|
+
const p = toks[j - 1]?.v, n = toks[j + 1]?.v;
|
|
104
|
+
if (n === '=' || p === '=' || p === '!' || p === '<' || p === '>') continue;
|
|
105
|
+
const id = /^[+\-*/%]$/.test(p || '') ? toks[j - 2] : toks[j - 1];
|
|
106
|
+
return id && id.t === 'id' && id.v.length >= 3 && !STYLE_COLOR[id.v] && !STYLE_NUM[id.v] ? snake(id.v) : '';
|
|
107
|
+
}
|
|
108
|
+
return '';
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Effects that must stay as written: shared libraries, group mirrors, code with no entry point. */
|
|
112
|
+
export function liftable(code: string): string | null {
|
|
113
|
+
if (typeof code !== 'string' || !code.trim()) return 'empty';
|
|
114
|
+
if (!/function\s+(render|update)\s*\(/.test(code)) return 'no render/update';
|
|
115
|
+
/* the library creates or fills the registry; pieces only read globalThis.__exodeLibs[LIB_ID] */
|
|
116
|
+
if (/__exodeLibs\s*(?:=(?!=)|\[[^\]]+\]\s*=(?!=))/.test(code)) return 'shared library';
|
|
117
|
+
if (/\beval\s*\(|new\s+Function\s*\(/.test(code)) return 'uses eval';
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function liftEffectKnobs(code: string): LiftResult {
|
|
122
|
+
const skip = liftable(code);
|
|
123
|
+
if (skip) return { code, knobs: [], changed: false, reason: skip };
|
|
124
|
+
const toks = tokenize(code);
|
|
125
|
+
const used = new Set(toks.filter((t) => t.t === 'id').map((t) => t.v));
|
|
126
|
+
|
|
127
|
+
/* identifiers that hold time: env.time copies, plus dt/time/elapsed */
|
|
128
|
+
const timeIds = new Set(['time', 'elapsed', 'dt']);
|
|
129
|
+
for (const m of code.matchAll(/(?:^|[\s,(])([A-Za-z_$][\w$]*)\s*=\s*\(?\s*env\.time\b/g)) timeIds.add(m[1]!);
|
|
130
|
+
|
|
131
|
+
/* per-token context: enclosing top-level function, current section, colour-expression span */
|
|
132
|
+
type Hit = { tok: Tok; kind: string; prefix: string };
|
|
133
|
+
const hits: Hit[] = [];
|
|
134
|
+
let topFn = '', pendingFn = '', section = '';
|
|
135
|
+
let colorUntil = -1; // token index where the current fill/stroke style statement ends
|
|
136
|
+
const callStack: boolean[] = []; // for each open '(' : is it a colour call?
|
|
137
|
+
const isTimeOperand = (k: number, dir: -1 | 1): boolean => {
|
|
138
|
+
/* the operand on one side of `*` at token k: a time id, or member `.time` */
|
|
139
|
+
if (dir === -1) {
|
|
140
|
+
const a = toks[k - 1];
|
|
141
|
+
if (!a || a.t !== 'id') return false;
|
|
142
|
+
if (a.v === 'time' && toks[k - 2]?.v === '.') return true;
|
|
143
|
+
return timeIds.has(a.v) && toks[k - 2]?.v !== '.';
|
|
144
|
+
}
|
|
145
|
+
const a = toks[k + 1];
|
|
146
|
+
if (!a || a.t !== 'id') return false;
|
|
147
|
+
const after = toks[k + 2]?.v;
|
|
148
|
+
if (after === '.' && toks[k + 3]?.v === 'time') return toks[k + 4]?.v !== '.' && toks[k + 4]?.v !== '(';
|
|
149
|
+
return timeIds.has(a.v) && after !== '.' && after !== '(' && after !== '[';
|
|
150
|
+
};
|
|
151
|
+
const statementEnd = (k: number): number => {
|
|
152
|
+
const d = toks[k]!.depth;
|
|
153
|
+
for (let j = k + 1; j < toks.length; j++) {
|
|
154
|
+
const t = toks[j]!;
|
|
155
|
+
if (t.depth < d) return j;
|
|
156
|
+
if (t.depth === d && (t.v === ';' || t.v === ',' || (t.nl && t.t !== 'punc'))) return j;
|
|
157
|
+
}
|
|
158
|
+
return toks.length;
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
for (let k = 0; k < toks.length; k++) {
|
|
162
|
+
const tk = toks[k]!;
|
|
163
|
+
if (tk.depth === 0) {
|
|
164
|
+
if (tk.v === 'function' && toks[k + 1]?.t === 'id') pendingFn = toks[k + 1]!.v;
|
|
165
|
+
if (tk.v === '{' && pendingFn) { topFn = pendingFn; pendingFn = ''; section = ''; }
|
|
166
|
+
if (tk.v === '}') topFn = '';
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
if (tk.t === 'com') { if (tk.nl) { const n = sectionName(tk.v); if (n) section = n; } continue; }
|
|
170
|
+
if (tk.v === '(') callStack.push(toks[k - 1]?.t === 'id' && COLOR_CALL.test(toks[k - 1]!.v));
|
|
171
|
+
if (tk.v === ')') callStack.pop();
|
|
172
|
+
if (!topFn || /^(NK_|KNOB_|_)/.test(topFn)) continue;
|
|
173
|
+
const prefix = section || (tk.t === 'num' ? assignTarget(toks, k) : '') || fnName(topFn);
|
|
174
|
+
|
|
175
|
+
/* x.fillStyle = … : the statement is a colour expression */
|
|
176
|
+
if (tk.t === 'id' && (STYLE_COLOR[tk.v] || STYLE_NUM[tk.v]) && toks[k - 1]?.v === '.' && toks[k + 1]?.v === '=' && toks[k + 2]?.v !== '=') {
|
|
177
|
+
const rhs = toks[k + 2], end = statementEnd(k + 2);
|
|
178
|
+
const whole = rhs && end === k + 3;
|
|
179
|
+
if (whole && rhs!.t === 'num' && STYLE_NUM[tk.v] && Number(rhs!.v) !== 0 && !(tk.v === 'globalAlpha' && Number(rhs!.v) === 1)) hits.push({ tok: rhs!, kind: STYLE_NUM[tk.v]!, prefix });
|
|
180
|
+
else if (whole && rhs!.t === 'str' && STYLE_COLOR[tk.v] && isColor(rhs!.v.slice(1, -1))) hits.push({ tok: rhs!, kind: STYLE_COLOR[tk.v]!, prefix });
|
|
181
|
+
else if (STYLE_COLOR[tk.v]) colorUntil = end;
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
/* any other colour written as text: addColorStop(0, '#ffb048'), shade('rgba(255,90,31,0.4)') */
|
|
185
|
+
if (tk.t === 'str') {
|
|
186
|
+
const p1 = toks[k - 1]?.v, p2 = toks[k - 2]?.v, n1 = toks[k + 1]?.v, n2 = toks[k + 2]?.v;
|
|
187
|
+
const compared = (p1 === '=' && (p2 === '=' || p2 === '!')) || (n1 === '=' && n2 === '=') || (n1 === '!' && n2 === '=');
|
|
188
|
+
const objectKey = n1 === ':' && (p1 === '{' || p1 === ',');
|
|
189
|
+
/* `i === T.sel ? '#ffb048' : …` is the selected colour: name it after the condition */
|
|
190
|
+
const branch = p1 === '?' ? conditionName(toks, k - 1) : '';
|
|
191
|
+
if (tk.v[0] !== '`' && isColor(tk.v.slice(1, -1)) && !compared && !objectKey && p1 !== '[' && !hits.some((h) => h.tok === tk)) hits.push({ tok: tk, kind: 'COLOR', prefix: branch || prefix });
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
if (tk.t !== 'num') continue;
|
|
195
|
+
const v = Number(tk.v);
|
|
196
|
+
if (!Number.isFinite(v) || v === 0 || v === 1) continue;
|
|
197
|
+
const prevMul = toks[k - 1]?.v === '*', nextMul = toks[k + 1]?.v === '*';
|
|
198
|
+
if (!prevMul && !nextMul) continue;
|
|
199
|
+
/* t * 2.4, 2.4 * t, env.time * 0.35, dt * 2.6 */
|
|
200
|
+
if ((prevMul && isTimeOperand(k - 1, -1)) || (nextMul && isTimeOperand(k + 1, 1))) { hits.push({ tok: tk, kind: 'SPEED', prefix }); continue; }
|
|
201
|
+
/* 0.75 * ax inside rgba(…) or a fill/stroke style expression */
|
|
202
|
+
const inColor = k < colorUntil || callStack.some(Boolean);
|
|
203
|
+
if (inColor && v > 0 && v < 1 && toks[k - 1]?.v !== '.' && toks[k + 1]?.v !== '.') hits.push({ tok: tk, kind: 'ALPHA', prefix });
|
|
204
|
+
}
|
|
205
|
+
if (!hits.length) return { code, knobs: [], changed: false, reason: 'nothing to lift' };
|
|
206
|
+
|
|
207
|
+
/* names: SECTION_KIND; the same name with the same value shares one knob, a different value gets _2, _3 */
|
|
208
|
+
const knobs = new Map<string, LiftedKnob>();
|
|
209
|
+
const byNameValue = new Map<string, string>();
|
|
210
|
+
const names: string[] = [];
|
|
211
|
+
for (const h of hits) {
|
|
212
|
+
const value: number | string = h.tok.t === 'str' ? h.tok.v.slice(1, -1) : Number(h.tok.v);
|
|
213
|
+
const base = h.prefix ? `${h.prefix}_${h.kind}` : h.kind;
|
|
214
|
+
const key = `${base}=${value}`;
|
|
215
|
+
let name = byNameValue.get(key);
|
|
216
|
+
if (!name) {
|
|
217
|
+
name = base;
|
|
218
|
+
for (let n = 2; knobs.has(name) || used.has(name); n++) name = `${base}_${n}`;
|
|
219
|
+
knobs.set(name, { name, value, count: 0 });
|
|
220
|
+
byNameValue.set(key, name);
|
|
221
|
+
}
|
|
222
|
+
knobs.get(name)!.count++;
|
|
223
|
+
names.push(name);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/* replace from the end so offsets stay valid */
|
|
227
|
+
let out = code;
|
|
228
|
+
for (let i = hits.length - 1; i >= 0; i--) {
|
|
229
|
+
const { tok } = hits[i]!;
|
|
230
|
+
out = out.slice(0, tok.s) + names[i] + out.slice(tok.e);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/* declarations, grouped by section, placed before the first top-level function */
|
|
234
|
+
const groups = new Map<string, LiftedKnob[]>();
|
|
235
|
+
for (const kb of knobs.values()) {
|
|
236
|
+
const g = kb.name.replace(/_(WIDTH|ALPHA|BLUR|FILL|STROKE|SHADOW|SPEED|COLOR)(_\d+)?$/, '') || '';
|
|
237
|
+
(groups.get(g) || groups.set(g, []).get(g)!).push(kb);
|
|
238
|
+
}
|
|
239
|
+
const lit = (x: number | string) => (typeof x === 'number' ? String(x) : `'${x.replace(/['\\\n]/g, '')}'`);
|
|
240
|
+
const decl = [...groups.values()].map((g) => `var ${g.map((kb) => `${kb.name} = ${lit(kb.value)}`).join(', ')};`).join('\n');
|
|
241
|
+
const mark = out.indexOf(KNOB_MARK);
|
|
242
|
+
if (mark >= 0) {
|
|
243
|
+
const at = mark + KNOB_MARK.length;
|
|
244
|
+
out = `${out.slice(0, at)}\n${decl}${out.slice(at)}`;
|
|
245
|
+
} else {
|
|
246
|
+
const firstFn = toks.find((t) => t.depth === 0 && t.v === 'function');
|
|
247
|
+
/* the first function's own leading comment stays attached to it */
|
|
248
|
+
let at = firstFn ? firstFn.s : 0;
|
|
249
|
+
const fi = firstFn ? toks.indexOf(firstFn) : -1;
|
|
250
|
+
if (fi > 0 && toks[fi - 1]!.t === 'com' && toks[fi - 1]!.depth === 0 && fi > 1 && toks[fi - 1]!.nl) {
|
|
251
|
+
const prevCode = toks.slice(0, fi - 1).some((t) => t.t !== 'com');
|
|
252
|
+
if (prevCode) at = toks[fi - 1]!.s;
|
|
253
|
+
}
|
|
254
|
+
const lineStart = out.lastIndexOf('\n', at - 1) + 1;
|
|
255
|
+
out = `${out.slice(0, lineStart)}${KNOB_MARK}\n${decl}\n\n${out.slice(lineStart)}`;
|
|
256
|
+
}
|
|
257
|
+
return { code: out, knobs: [...knobs.values()], changed: true };
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
/** Shared multi-part app/game scripts (the apps-topic convention: `var GAME = { part: 'world' };` + one script
|
|
262
|
+
* copied into several layers). Lifting their numbers would mint the same knobs once per layer, as
|
|
263
|
+
* meaningless inputs (riverSpeed, rocksSpeed2, …), so these are left as written. */
|
|
264
|
+
export function isSharedPartScript(code: string): boolean {
|
|
265
|
+
return /^\s*var\s+[A-Za-z_$][\w$]*\s*=\s*\{\s*part\s*:/.test(code);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/** Lifts knobs in every CustomEffect of an artboard or multi-screen document, in place. */
|
|
269
|
+
export function liftKnobsInDoc(doc: any): { effects: number; changed: number; knobs: number; lines: string[] } {
|
|
270
|
+
const st = { effects: 0, changed: 0, knobs: 0, lines: [] as string[] };
|
|
271
|
+
const boards = Array.isArray(doc?.screens) && doc.screens.length ? [doc, ...doc.screens] : [doc];
|
|
272
|
+
const seen = new Set<any>();
|
|
273
|
+
for (const b of boards) {
|
|
274
|
+
for (const o of b?.objects || []) {
|
|
275
|
+
for (const e of o?.style?.effects || []) {
|
|
276
|
+
if (e?.type !== 'CustomEffect' || typeof e.code !== 'string' || e.groupMirror === true || seen.has(e) || isSharedPartScript(e.code)) continue;
|
|
277
|
+
seen.add(e);
|
|
278
|
+
st.effects++;
|
|
279
|
+
const r = liftEffectKnobs(e.code);
|
|
280
|
+
if (!r.changed) continue;
|
|
281
|
+
e.code = r.code;
|
|
282
|
+
st.changed++;
|
|
283
|
+
st.knobs += r.knobs.length;
|
|
284
|
+
st.lines.push(`${o.name || o.id}: ${r.knobs.map((k) => k.name).join(', ')}`);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
return st;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/* ── runtime inputs ─────────────────────────────────────────────────────────────────────────────
|
|
292
|
+
* The runtime's controls list state-machine inputs, not code. Every top-level setting the editor
|
|
293
|
+
* shows (var NAME = <number | colour | boolean>) is also declared as an input, and the effect reads
|
|
294
|
+
* it each frame: a value the host sets wins; while the input sits at its default the code's value
|
|
295
|
+
* is used, so editing the setting in the editor still works. Regenerated on every MCP write. */
|
|
296
|
+
|
|
297
|
+
const BIND_BEGIN = '/* runtime inputs: each setting above is also a state-machine input (runtime controls). A value the host sets wins; at its default the setting above is used. */';
|
|
298
|
+
const BIND_END = '/* /runtime inputs */';
|
|
299
|
+
const INTERNAL = new Set(['LIB_ID', 'VW', 'VH', 'W', 'H', 'TAU', 'PI', 'PIN', 'NK_PIN', 'DEG', 'RAD', 'EPS', 'name', 'params', 'properties', 'schema']);
|
|
300
|
+
|
|
301
|
+
export interface CodeSetting { name: string; value: number | string | boolean }
|
|
302
|
+
|
|
303
|
+
/** Top-level `var NAME = <number | colour string | boolean>` declarations: what the editor shows as parameters. */
|
|
304
|
+
export function topLevelSettings(code: string): CodeSetting[] {
|
|
305
|
+
const toks = tokenize(code);
|
|
306
|
+
const out: CodeSetting[] = [];
|
|
307
|
+
for (let k = 0; k < toks.length; k++) {
|
|
308
|
+
const t = toks[k]!;
|
|
309
|
+
if (t.depth !== 0 || !/^(var|let|const)$/.test(t.v) || t.t !== 'id') continue;
|
|
310
|
+
let j = k + 1;
|
|
311
|
+
for (;;) {
|
|
312
|
+
const id = toks[j], eq = toks[j + 1];
|
|
313
|
+
if (!id || id.t !== 'id' || eq?.v !== '=') break;
|
|
314
|
+
let p = j + 2, neg = false;
|
|
315
|
+
if (toks[p]?.v === '-' && toks[p + 1]?.t === 'num') { neg = true; p++; }
|
|
316
|
+
const lit = toks[p], after = toks[p + 1];
|
|
317
|
+
const ends = !after || after.v === ',' || after.v === ';' || (after.nl && after.depth === 0);
|
|
318
|
+
let value: number | string | boolean | null = null;
|
|
319
|
+
if (lit && ends) {
|
|
320
|
+
if (lit.t === 'num') value = (neg ? -1 : 1) * Number(lit.v);
|
|
321
|
+
else if (lit.t === 'str' && lit.v[0] !== '`' && isColor(lit.v.slice(1, -1))) value = lit.v.slice(1, -1);
|
|
322
|
+
else if (lit.t === 'id' && (lit.v === 'true' || lit.v === 'false')) value = lit.v === 'true';
|
|
323
|
+
}
|
|
324
|
+
if (value !== null && !INTERNAL.has(id.v) && !/^(_|NK_|KNOB_)/.test(id.v)) out.push({ name: id.v, value });
|
|
325
|
+
/* skip to the next declarator in this statement */
|
|
326
|
+
let q = j + 2;
|
|
327
|
+
for (; q < toks.length; q++) { const x = toks[q]!; if (x.depth === 0 && (x.v === ',' || x.v === ';')) break; if (q > j + 2 && x.depth === 0 && x.nl) { q = -1; break; } }
|
|
328
|
+
if (q < 0 || toks[q]?.v !== ',') break;
|
|
329
|
+
j = q + 1;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
return out;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const camel = (n: string) => n.toLowerCase().replace(/_([a-z0-9])/g, (_m, c) => c.toUpperCase());
|
|
336
|
+
const idCamel = (id: string) => { const s = String(id).split(/[^A-Za-z0-9]+/).filter(Boolean); if (s.length > 1 && s[0]!.length <= 2) s.shift(); return camel(s.join('_')); };
|
|
337
|
+
const lit = (v: number | string | boolean) => (typeof v === 'string' ? `'${v.replace(/['\\\n]/g, '')}'` : String(v));
|
|
338
|
+
|
|
339
|
+
function stripBinding(code: string): string {
|
|
340
|
+
const a = code.indexOf(BIND_BEGIN);
|
|
341
|
+
if (a >= 0) { const b = code.indexOf(BIND_END, a); if (b >= 0) code = code.slice(0, a).replace(/\n+$/, '\n') + code.slice(b + BIND_END.length).replace(/^\n+/, ''); }
|
|
342
|
+
return code.replace(/(function\s+(?:render|update)\s*\([^)]*\)\s*\{)\s*KNOB_apply\([^)]*\);/g, '$1');
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/** The input names an effect's binding block already uses, by setting. */
|
|
346
|
+
function boundNames(code: string): Map<string, string> {
|
|
347
|
+
const m = new Map<string, string>();
|
|
348
|
+
for (const x of code.matchAll(/\b([A-Za-z_$][\w$]*) = KNOB_in\(e, '([^']+)'/g)) m.set(x[1]!, x[2]!);
|
|
349
|
+
return m;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/** Writes the binding block for `binds` (setting → input name); with none, removes it. */
|
|
353
|
+
export function bindEffectCode(code: string, binds: Array<{ name: string; input: string; value: number | string | boolean }>): string {
|
|
354
|
+
let out = stripBinding(code);
|
|
355
|
+
if (!binds.length) return out;
|
|
356
|
+
const block = [
|
|
357
|
+
BIND_BEGIN,
|
|
358
|
+
'function KNOB_in(e, name, def, cur) {',
|
|
359
|
+
' var v = e && e.input ? e.input(name) : e && e.inputs ? e.inputs[name] : undefined;',
|
|
360
|
+
" if (v === undefined || v === null || v === '' || String(v) === String(def)) return cur;",
|
|
361
|
+
" if (typeof def === 'number') { v = Number(v); return isNaN(v) ? cur : v; }",
|
|
362
|
+
" if (typeof def === 'boolean') return v === true || v === 'true' || v === 1;",
|
|
363
|
+
' return String(v);',
|
|
364
|
+
'}',
|
|
365
|
+
`var KNOB_BASE = { ${binds.map((b) => `${b.name}: ${b.name}`).join(', ')} };`,
|
|
366
|
+
'function KNOB_apply(e) {',
|
|
367
|
+
...binds.map((b) => ` ${b.name} = KNOB_in(e, '${b.input}', ${lit(b.value)}, KNOB_BASE.${b.name});`),
|
|
368
|
+
'}',
|
|
369
|
+
BIND_END,
|
|
370
|
+
].join('\n');
|
|
371
|
+
out = out.replace(/\s*$/, '\n\n') + block + '\n';
|
|
372
|
+
/* every entry point applies the inputs first */
|
|
373
|
+
return out.replace(/(function\s+(render|update)\s*\(([^)]*)\)\s*\{)/g, (_m, head: string, fn: string, args: string) => {
|
|
374
|
+
const a = args.split(',').map((s) => s.trim());
|
|
375
|
+
const env = (fn === 'render' ? a[1] : a[0]) || 'env';
|
|
376
|
+
return `${head} KNOB_apply(${env});`;
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Declares a state-machine input for every effect setting on one artboard (or screen) and binds the
|
|
382
|
+
* code to it. Input names: the setting in camelCase (bloomWidth), prefixed with the layer id when two
|
|
383
|
+
* effects would share a name (camMotionYawSpeed). Defaults follow the code's current values.
|
|
384
|
+
*/
|
|
385
|
+
export function bindKnobInputs(board: any): { effects: number; inputs: number; lines: string[] } {
|
|
386
|
+
const st = { effects: 0, inputs: 0, lines: [] as string[] };
|
|
387
|
+
if (!board || !Array.isArray(board.objects)) return st;
|
|
388
|
+
const sm = board.stateMachine || (board.stateMachine = { inputs: [], layers: [], logicNodes: [] });
|
|
389
|
+
if (!Array.isArray(sm.inputs)) sm.inputs = [];
|
|
390
|
+
/* names our blocks already own; every other existing input name is taken */
|
|
391
|
+
const ours = new Map<string, string>(); // input name -> owner key
|
|
392
|
+
const owned: Array<{ o: any; e: any; prev: Map<string, string> }> = [];
|
|
393
|
+
for (const o of board.objects) for (const e of o?.style?.effects || []) {
|
|
394
|
+
if (e?.type !== 'CustomEffect' || typeof e.code !== 'string' || e.groupMirror === true || isSharedPartScript(e.code)) continue;
|
|
395
|
+
const prev = boundNames(e.code);
|
|
396
|
+
for (const [n, inp] of prev) ours.set(inp, `${o.id}/${n}`);
|
|
397
|
+
owned.push({ o, e, prev });
|
|
398
|
+
}
|
|
399
|
+
const taken = new Set<string>(sm.inputs.map((i: any) => i?.name).filter((n: any) => typeof n === 'string' && !ours.has(n)));
|
|
400
|
+
const claimed = new Map<string, string>(); // input name -> owner key, this pass
|
|
401
|
+
for (const { o, e, prev } of owned) {
|
|
402
|
+
if (liftable(e.code)) { if (prev.size) e.code = stripBinding(e.code); continue; }
|
|
403
|
+
const settings = topLevelSettings(stripBinding(e.code));
|
|
404
|
+
const binds: Array<{ name: string; input: string; value: number | string | boolean }> = [];
|
|
405
|
+
for (const s of settings) {
|
|
406
|
+
const owner = `${o.id}/${s.name}`;
|
|
407
|
+
let input = prev.get(s.name) || camel(s.name);
|
|
408
|
+
if (taken.has(input) || (claimed.has(input) && claimed.get(input) !== owner)) input = idCamel(o.id) + s.name.charAt(0) + camel(s.name).slice(1);
|
|
409
|
+
for (let n = 2; taken.has(input) || (claimed.has(input) && claimed.get(input) !== owner); n++) input = `${idCamel(o.id)}${s.name.charAt(0)}${camel(s.name).slice(1)}${n}`;
|
|
410
|
+
claimed.set(input, owner);
|
|
411
|
+
binds.push({ name: s.name, input, value: s.value });
|
|
412
|
+
}
|
|
413
|
+
const next = bindEffectCode(e.code, binds);
|
|
414
|
+
if (next !== e.code) e.code = next;
|
|
415
|
+
if (!binds.length) continue;
|
|
416
|
+
st.effects++;
|
|
417
|
+
for (const b of binds) {
|
|
418
|
+
const type = typeof b.value === 'number' ? 'Number' : typeof b.value === 'boolean' ? 'Boolean' : 'Color';
|
|
419
|
+
const value: any = { type, value: b.value, defaultValue: b.value };
|
|
420
|
+
if (type === 'Number') {
|
|
421
|
+
const v = b.value as number, a = Math.abs(v);
|
|
422
|
+
value.min = v < 0 ? v * 3 : 0;
|
|
423
|
+
value.max = v === 0 ? 1 : a * 3;
|
|
424
|
+
value.step = Number.isInteger(v) && a >= 1 ? 1 : Math.pow(10, Math.floor(Math.log10(a || 1)) - 1);
|
|
425
|
+
}
|
|
426
|
+
const i = sm.inputs.findIndex((x: any) => x?.name === b.input);
|
|
427
|
+
if (i >= 0) sm.inputs[i] = { ...sm.inputs[i], value: { ...(sm.inputs[i].value || {}), ...value } };
|
|
428
|
+
else sm.inputs.push({ id: b.input, name: b.input, value });
|
|
429
|
+
st.inputs++;
|
|
430
|
+
}
|
|
431
|
+
st.lines.push(`${o.name || o.id}: ${binds.map((b) => b.input).join(', ')}`);
|
|
432
|
+
}
|
|
433
|
+
/* inputs a removed setting used to own */
|
|
434
|
+
const live = new Set(claimed.keys());
|
|
435
|
+
sm.inputs = sm.inputs.filter((i: any) => !(ours.has(i?.name) && !live.has(i.name)));
|
|
436
|
+
return st;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/** bindKnobInputs on an artboard or every screen of a multi-screen document. */
|
|
440
|
+
export function bindKnobsInDoc(doc: any) {
|
|
441
|
+
const boards = Array.isArray(doc?.screens) && doc.screens.length ? [doc, ...doc.screens] : [doc];
|
|
442
|
+
const all = { effects: 0, inputs: 0, lines: [] as string[] };
|
|
443
|
+
for (const b of boards) { const r = bindKnobInputs(b); all.effects += r.effects; all.inputs += r.inputs; all.lines.push(...r.lines); }
|
|
444
|
+
return all;
|
|
445
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export interface OptimizeResult {
|
|
2
|
+
code: string;
|
|
3
|
+
before: number;
|
|
4
|
+
after: number;
|
|
5
|
+
changed: boolean;
|
|
6
|
+
reason?: string;
|
|
7
|
+
}
|
|
8
|
+
/** Runs passes until the code stops shrinking: each pass can expose more dead code. */
|
|
9
|
+
export declare function optimizeEffectCode(code: string): OptimizeResult;
|
|
10
|
+
/** Optimises every CustomEffect in an artboard or multi-screen project, in place. */
|
|
11
|
+
export declare function optimizeEffectsInDoc(doc: any): {
|
|
12
|
+
effects: number;
|
|
13
|
+
changed: number;
|
|
14
|
+
before: number;
|
|
15
|
+
after: number;
|
|
16
|
+
notes: string[];
|
|
17
|
+
};
|
|
18
|
+
//# sourceMappingURL=effectOptimizer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"effectOptimizer.d.ts","sourceRoot":"","sources":["effectOptimizer.ts"],"names":[],"mappings":"AAeA,MAAM,WAAW,cAAc;IAAG,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,OAAO,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE;AAiBlH,uFAAuF;AACvF,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,cAAc,CAU/D;AA0CD,qFAAqF;AACrF,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,GAAG,GAAG;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,EAAE,CAAA;CAAE,CAiBnI"}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lossless size optimisation for CustomEffect code.
|
|
3
|
+
*
|
|
4
|
+
* Apps and games often put the same big script on several layers, each with a small constant
|
|
5
|
+
* prefix that says what that layer does, e.g. `var GAME = { part: "world", screen: "level" };`.
|
|
6
|
+
* Each layer only ever reaches part of the script. This module:
|
|
7
|
+
* 1. reads the leading `var NAME = { ... };` constant (JSON-like) if there is one,
|
|
8
|
+
* 2. substitutes its primitive fields as compile-time constants (NAME.part -> "world"),
|
|
9
|
+
* 3. tree-shakes from the real entry points (render, update, animate) and folds dead branches,
|
|
10
|
+
* 4. keeps whitespace and names, so the code stays readable and editable in the editor.
|
|
11
|
+
* Behaviour and output are unchanged: only code that can never run is removed. If anything looks
|
|
12
|
+
* risky (no entry point, eval/new Function, the result fails to compile) the code is left as is.
|
|
13
|
+
*/
|
|
14
|
+
import { transformSync } from 'esbuild';
|
|
15
|
+
const ENTRY_POINTS = ['render', 'update', 'animate', 'nativeBake'];
|
|
16
|
+
function constantPrefix(code) {
|
|
17
|
+
const m = /^\s*var\s+([A-Za-z_$][\w$]*)\s*=\s*(\{[^\n]*\})\s*;?\s*\n/.exec(code);
|
|
18
|
+
if (!m)
|
|
19
|
+
return null;
|
|
20
|
+
try {
|
|
21
|
+
const value = JSON.parse(m[2]);
|
|
22
|
+
return value && typeof value === 'object' && !Array.isArray(value) ? { name: m[1], value, line: m[0] } : null;
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
function definedEntries(code) {
|
|
29
|
+
return ENTRY_POINTS.filter((n) => new RegExp(`(^|\\n)\\s*(export\\s+)?function\\s+${n}\\s*\\(`).test(code));
|
|
30
|
+
}
|
|
31
|
+
/** Runs passes until the code stops shrinking: each pass can expose more dead code. */
|
|
32
|
+
export function optimizeEffectCode(code) {
|
|
33
|
+
const first = optimizeOnce(code);
|
|
34
|
+
if (!first.changed)
|
|
35
|
+
return first;
|
|
36
|
+
let cur = first;
|
|
37
|
+
for (let i = 0; i < 4; i++) {
|
|
38
|
+
const next = optimizeOnce(cur.code);
|
|
39
|
+
if (!next.changed)
|
|
40
|
+
break;
|
|
41
|
+
cur = { ...next, before: first.before };
|
|
42
|
+
}
|
|
43
|
+
return { ...cur, before: first.before };
|
|
44
|
+
}
|
|
45
|
+
function optimizeOnce(code) {
|
|
46
|
+
const before = code.length;
|
|
47
|
+
const keep = (reason) => ({ code, before, after: before, changed: false, reason });
|
|
48
|
+
if (/\beval\s*\(|new\s+Function\s*\(/.test(code))
|
|
49
|
+
return keep('uses eval/new Function');
|
|
50
|
+
const entries = definedEntries(code);
|
|
51
|
+
if (!entries.length)
|
|
52
|
+
return keep('no render/update/animate entry point');
|
|
53
|
+
const prefix = constantPrefix(code);
|
|
54
|
+
const define = {};
|
|
55
|
+
if (prefix) {
|
|
56
|
+
for (const [k, v] of Object.entries(prefix.value)) {
|
|
57
|
+
if (['string', 'number', 'boolean'].includes(typeof v) && /^[A-Za-z_$][\w$]*$/.test(k))
|
|
58
|
+
define[`${prefix.name}.${k}`] = JSON.stringify(v);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/* entry points must survive; strip `export` so they are plain declarations, then re-export them */
|
|
62
|
+
/* the constant line is lifted out so esbuild treats NAME as a global it may substitute; it goes back on top afterwards */
|
|
63
|
+
/* nothing to fold (no primitive constant like GAME.part): leave the code as written, comments and all */
|
|
64
|
+
if (!Object.keys(define).length)
|
|
65
|
+
return keep('no constant prefix to fold');
|
|
66
|
+
const body = prefix ? code.slice(prefix.line.length) : code;
|
|
67
|
+
const src = body.replace(/(^|\n)(\s*)export\s+(const|let|var|function)\s/g, '$1$2$3 ') + `\nexport { ${entries.join(', ')} };\n`;
|
|
68
|
+
let out;
|
|
69
|
+
try {
|
|
70
|
+
out = transformSync(src, {
|
|
71
|
+
loader: 'js', format: 'esm', treeShaking: true, minifySyntax: true, minifyWhitespace: false, minifyIdentifiers: false,
|
|
72
|
+
define, target: 'es2019', legalComments: 'none',
|
|
73
|
+
}).code;
|
|
74
|
+
}
|
|
75
|
+
catch (e) {
|
|
76
|
+
return keep(`transform failed: ${e.message}`);
|
|
77
|
+
}
|
|
78
|
+
out = out.replace(/\nexport\s*\{[^}]*\};?\s*$/, '\n');
|
|
79
|
+
if (prefix)
|
|
80
|
+
out = prefix.line.replace(/\s*$/, '\n') + out;
|
|
81
|
+
/* keep a marker comment that the editor relies on */
|
|
82
|
+
if (code.includes('EXODE_KEEP_LIVE') && !out.includes('EXODE_KEEP_LIVE'))
|
|
83
|
+
out = '/* EXODE_KEEP_LIVE */\n' + out;
|
|
84
|
+
/* minifier-safety for the .bexode compiler: never leave a minus followed by a spaced minus */
|
|
85
|
+
if (/-\s+-/.test(out))
|
|
86
|
+
return keep('output would contain a minus followed by a spaced minus');
|
|
87
|
+
try {
|
|
88
|
+
const fn = new Function(`${out}\nreturn { ${entries.map((n) => `${n}: typeof ${n} === 'function'`).join(', ')} };`)();
|
|
89
|
+
if (!entries.every((n) => fn[n]))
|
|
90
|
+
return keep('entry point lost');
|
|
91
|
+
}
|
|
92
|
+
catch (e) {
|
|
93
|
+
return keep(`result does not compile: ${e.message}`);
|
|
94
|
+
}
|
|
95
|
+
if (out.length >= before)
|
|
96
|
+
return keep('no smaller');
|
|
97
|
+
return { code: out, before, after: out.length, changed: true };
|
|
98
|
+
}
|
|
99
|
+
/** Optimises every CustomEffect in an artboard or multi-screen project, in place. */
|
|
100
|
+
export function optimizeEffectsInDoc(doc) {
|
|
101
|
+
const stats = { effects: 0, changed: 0, before: 0, after: 0, notes: [] };
|
|
102
|
+
const boards = Array.isArray(doc?.screens) && doc.screens.length ? [doc, ...doc.screens] : [doc];
|
|
103
|
+
const seen = new Set();
|
|
104
|
+
for (const b of boards) {
|
|
105
|
+
for (const o of b?.objects || []) {
|
|
106
|
+
for (const e of o?.style?.effects || []) {
|
|
107
|
+
if (e?.type !== 'CustomEffect' || typeof e.code !== 'string' || seen.has(e))
|
|
108
|
+
continue;
|
|
109
|
+
seen.add(e);
|
|
110
|
+
stats.effects++;
|
|
111
|
+
const r = optimizeEffectCode(e.code);
|
|
112
|
+
stats.before += r.before;
|
|
113
|
+
stats.after += r.after;
|
|
114
|
+
if (r.changed) {
|
|
115
|
+
e.code = r.code;
|
|
116
|
+
stats.changed++;
|
|
117
|
+
}
|
|
118
|
+
else if (r.reason && stats.notes.length < 8)
|
|
119
|
+
stats.notes.push(`${o.name || o.id}: kept as is (${r.reason})`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return stats;
|
|
124
|
+
}
|
|
125
|
+
//# sourceMappingURL=effectOptimizer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"effectOptimizer.js","sourceRoot":"","sources":["effectOptimizer.ts"],"names":[],"mappings":"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"}
|