@proteus-vue/render-backend 0.3.0-beta.11 → 0.3.0-beta.23

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.
@@ -0,0 +1,3017 @@
1
+ // ../router/src/app-stack.ts
2
+ function paramKey(params) {
3
+ if (!params) return "";
4
+ return Object.keys(params).sort().filter((k) => params[k] !== void 0).map((k) => `${k}=${String(params[k])}`).join("&");
5
+ }
6
+ function createAppStack(opts) {
7
+ const screens = opts.screens;
8
+ const policy = {
9
+ nodeBudget: null,
10
+ keepWindow: 3,
11
+ defaultScreenNodes: 64,
12
+ ...opts.policy
13
+ };
14
+ const stack = [];
15
+ const commands = [];
16
+ const handlers = [];
17
+ let seq = 0;
18
+ let freezeCount = 0;
19
+ let rebuildCount = 0;
20
+ let overBudget = false;
21
+ let activeNodeCount = 0;
22
+ let frozenPrefix = 0;
23
+ const emit = (e) => {
24
+ for (const h of handlers) h(e);
25
+ };
26
+ function specOf2(name) {
27
+ const s = screens[name];
28
+ if (!s) {
29
+ throw new Error(`[app-stack] \u672A\u6CE8\u518C\u7684\u5C4F "${name}"\uFF08\u53EF\u7528\uFF1A${Object.keys(screens).join(", ") || "\uFF08\u7A7A\u6CE8\u518C\u8868\uFF09"}\uFF09`);
30
+ }
31
+ return s;
32
+ }
33
+ function makeRecord(name, params, transition) {
34
+ const spec = specOf2(name);
35
+ return {
36
+ screenId: `${name}#${++seq}`,
37
+ name,
38
+ path: spec.path,
39
+ params: params ?? {},
40
+ transition: transition ?? spec.transition,
41
+ state: "hidden",
42
+ // 调用方按角色置可见性(仅栈顶 mounted)
43
+ needsRebuild: false,
44
+ budgetNodes: spec.budgetNodes ?? policy.defaultScreenNodes
45
+ };
46
+ }
47
+ function pushMount(rec, rebuild) {
48
+ commands.push({ op: "mount", screenId: rec.screenId, name: rec.name, path: rec.path, params: rec.params, rebuild });
49
+ }
50
+ function activeNodes() {
51
+ return activeNodeCount;
52
+ }
53
+ function applyMemoryPolicy() {
54
+ overBudget = false;
55
+ const budget = policy.nodeBudget;
56
+ if (budget == null) return;
57
+ if (activeNodeCount <= budget) return;
58
+ const protectFrom = Math.max(0, stack.length - policy.keepWindow);
59
+ while (activeNodeCount > budget) {
60
+ let frozenOne = false;
61
+ for (let i = frozenPrefix; i < protectFrom; i++) {
62
+ const r = stack[i];
63
+ if (r.state !== "hidden") continue;
64
+ r.state = "frozen";
65
+ r.needsRebuild = true;
66
+ freezeCount++;
67
+ commands.push({ op: "unmount", screenId: r.screenId, reason: "freeze" });
68
+ activeNodeCount -= r.budgetNodes;
69
+ while (frozenPrefix < stack.length && stack[frozenPrefix].state === "frozen") frozenPrefix++;
70
+ emit({ type: "freeze", screenId: r.screenId, name: r.name, depth: stack.length });
71
+ frozenOne = true;
72
+ break;
73
+ }
74
+ if (!frozenOne) break;
75
+ }
76
+ overBudget = activeNodeCount > budget;
77
+ if (overBudget) emit({ type: "over-budget", activeNodes: activeNodeCount, nodeBudget: budget });
78
+ }
79
+ function activateTop() {
80
+ const top = stack[stack.length - 1];
81
+ if (!top || top.state === "mounted") return;
82
+ const rebuild = top.state === "frozen";
83
+ top.state = "mounted";
84
+ if (rebuild) {
85
+ rebuildCount++;
86
+ activeNodeCount += top.budgetNodes;
87
+ pushMount(top, true);
88
+ emit({ type: "restore", screenId: top.screenId, name: top.name, depth: stack.length });
89
+ }
90
+ commands.push({ op: "enter", screenId: top.screenId, transition: top.transition });
91
+ }
92
+ function deactivate(oldTop, transition) {
93
+ if (!oldTop || oldTop.state !== "mounted") return;
94
+ oldTop.state = "hidden";
95
+ commands.push({ op: "exit", screenId: oldTop.screenId, transition });
96
+ }
97
+ function suspend(opts2) {
98
+ const top = stack[stack.length - 1] ?? null;
99
+ deactivate(top, opts2?.transition);
100
+ }
101
+ function resume() {
102
+ if (stack.length === 0) return;
103
+ activateTop();
104
+ }
105
+ function releaseTrees() {
106
+ let released = 0;
107
+ for (const r of stack) {
108
+ if (r.state === "frozen") continue;
109
+ if (r.state === "mounted") r.state = "hidden";
110
+ activeNodeCount -= r.budgetNodes;
111
+ r.state = "frozen";
112
+ r.needsRebuild = true;
113
+ freezeCount++;
114
+ commands.push({ op: "unmount", screenId: r.screenId, reason: "freeze" });
115
+ released++;
116
+ }
117
+ while (frozenPrefix < stack.length && stack[frozenPrefix].state === "frozen") frozenPrefix++;
118
+ if (released > 0) emit({ type: "release", screens: released, depth: stack.length });
119
+ return released;
120
+ }
121
+ function frames() {
122
+ return stack.map((r) => {
123
+ const f = { name: r.name };
124
+ if (Object.keys(r.params).length > 0) f.params = { ...r.params };
125
+ if (r.transition) f.transition = r.transition;
126
+ return f;
127
+ });
128
+ }
129
+ function push(name, params, o) {
130
+ const oldTop = stack[stack.length - 1] ?? null;
131
+ const rec = makeRecord(name, params, o?.transition);
132
+ const t = rec.transition;
133
+ deactivate(oldTop, t);
134
+ pushMount(rec, false);
135
+ rec.state = "mounted";
136
+ stack.push(rec);
137
+ activeNodeCount += rec.budgetNodes;
138
+ commands.push({ op: "enter", screenId: rec.screenId, transition: t });
139
+ applyMemoryPolicy();
140
+ emit({ type: "push", name, depth: stack.length });
141
+ }
142
+ function popToIndex(targetIdx, exitTransition) {
143
+ if (stack.length - 1 <= targetIdx) return null;
144
+ const removed = [];
145
+ while (stack.length - 1 > targetIdx) removed.push(stack.pop());
146
+ commands.push({ op: "exit", screenId: removed[0].screenId, transition: exitTransition });
147
+ for (const r of removed) {
148
+ if (r.state !== "frozen") activeNodeCount -= r.budgetNodes;
149
+ r.state = "destroyed";
150
+ commands.push({ op: "unmount", screenId: r.screenId, reason: "pop" });
151
+ }
152
+ if (frozenPrefix > stack.length) frozenPrefix = stack.length;
153
+ activateTop();
154
+ return removed[0];
155
+ }
156
+ function pop(delta = 1) {
157
+ const target = Math.max(0, stack.length - 1 - Math.max(1, delta));
158
+ const oldTop = stack[stack.length - 1] ?? null;
159
+ const removed = popToIndex(target, oldTop?.transition);
160
+ if (!removed) return;
161
+ applyMemoryPolicy();
162
+ emit({ type: "pop", name: removed.name, depth: stack.length });
163
+ }
164
+ function replace(name, params, o) {
165
+ const oldTop = stack[stack.length - 1] ?? null;
166
+ if (oldTop) {
167
+ commands.push({ op: "exit", screenId: oldTop.screenId, transition: o?.transition ?? oldTop.transition });
168
+ commands.push({ op: "unmount", screenId: oldTop.screenId, reason: "pop" });
169
+ stack.pop();
170
+ if (oldTop.state !== "frozen") activeNodeCount -= oldTop.budgetNodes;
171
+ oldTop.state = "destroyed";
172
+ if (frozenPrefix > stack.length) frozenPrefix = stack.length;
173
+ }
174
+ const rec = makeRecord(name, params, o?.transition);
175
+ pushMount(rec, false);
176
+ rec.state = "mounted";
177
+ stack.push(rec);
178
+ activeNodeCount += rec.budgetNodes;
179
+ commands.push({ op: "enter", screenId: rec.screenId, transition: rec.transition });
180
+ applyMemoryPolicy();
181
+ emit({ type: "replace", name, depth: stack.length });
182
+ }
183
+ function removeByName(name) {
184
+ const removedIdx = [];
185
+ for (let i = stack.length - 1; i >= 0; i--) {
186
+ if (stack[i].name === name) removedIdx.push(i);
187
+ }
188
+ if (removedIdx.length === 0) return 0;
189
+ const topIdx = stack.length - 1;
190
+ const removedMounted = removedIdx.some((i) => stack[i].state === "mounted");
191
+ const removedWasTop = removedIdx.includes(topIdx);
192
+ for (const i of removedIdx) {
193
+ const r = stack[i];
194
+ if (r.state !== "frozen") activeNodeCount -= r.budgetNodes;
195
+ r.state = "destroyed";
196
+ commands.push({ op: "unmount", screenId: r.screenId, reason: "pop" });
197
+ }
198
+ for (const i of removedIdx) stack.splice(i, 1);
199
+ if (frozenPrefix > stack.length) frozenPrefix = stack.length;
200
+ if (removedMounted && removedWasTop && stack.length > 0) {
201
+ const top = stack[stack.length - 1];
202
+ if (top.state !== "mounted") {
203
+ const rebuild = top.state === "frozen";
204
+ top.state = "mounted";
205
+ if (rebuild) {
206
+ rebuildCount++;
207
+ activeNodeCount += top.budgetNodes;
208
+ pushMount(top, true);
209
+ emit({ type: "restore", screenId: top.screenId, name: top.name, depth: stack.length });
210
+ }
211
+ commands.push({ op: "enter", screenId: top.screenId, transition: top.transition });
212
+ }
213
+ }
214
+ emit({ type: "pop", name, depth: stack.length });
215
+ return removedIdx.length;
216
+ }
217
+ function moveToTop(name) {
218
+ const idx = stack.findIndex((r) => r.name === name);
219
+ if (idx < 0) {
220
+ throw new Error(
221
+ `[app-stack] moveToTop("${name}")\uFF1A\u6808\u4E2D\u4E0D\u5B58\u5728\u8BE5\u5C4F\uFF08\u5F53\u524D\u6808\uFF1A${stack.map((r) => r.name).join(" \u2192 ") || "\u7A7A"}\uFF09`
222
+ );
223
+ }
224
+ const target = stack[idx];
225
+ if (idx === stack.length - 1) return;
226
+ const oldTop = stack[stack.length - 1];
227
+ deactivate(oldTop, target.transition);
228
+ stack.splice(idx, 1);
229
+ stack.push(target);
230
+ if (target.state !== "mounted") {
231
+ const rebuild = target.state === "frozen";
232
+ target.state = "mounted";
233
+ if (rebuild) {
234
+ rebuildCount++;
235
+ activeNodeCount += target.budgetNodes;
236
+ pushMount(target, true);
237
+ emit({ type: "restore", screenId: target.screenId, name: target.name, depth: stack.length });
238
+ }
239
+ }
240
+ commands.push({ op: "enter", screenId: target.screenId, transition: target.transition });
241
+ emit({ type: "push", name, depth: stack.length });
242
+ }
243
+ function popTo(name) {
244
+ let idx = -1;
245
+ for (let i = stack.length - 1; i >= 0; i--) {
246
+ if (stack[i].name === name) {
247
+ idx = i;
248
+ break;
249
+ }
250
+ }
251
+ if (idx < 0) throw new Error(`[app-stack] popTo("${name}")\uFF1A\u6808\u4E2D\u4E0D\u5B58\u5728\u8BE5\u5C4F\uFF08\u5F53\u524D\u6808\uFF1A${stack.map((r) => r.name).join(" \u2192 ") || "\u7A7A"}\uFF09`);
252
+ const oldTop = stack[stack.length - 1] ?? null;
253
+ const removed = popToIndex(idx, oldTop?.transition);
254
+ if (!removed) return;
255
+ applyMemoryPolicy();
256
+ emit({ type: "pop", name: removed.name, depth: stack.length });
257
+ }
258
+ function popToRoot() {
259
+ const oldTop = stack[stack.length - 1] ?? null;
260
+ if (stack.length <= 1) return;
261
+ const removed = popToIndex(0, oldTop?.transition);
262
+ if (!removed) return;
263
+ applyMemoryPolicy();
264
+ emit({ type: "pop", name: removed.name, depth: stack.length });
265
+ }
266
+ function resetTo(name, params) {
267
+ while (stack.length) {
268
+ const r = stack.pop();
269
+ if (r.state !== "frozen") activeNodeCount -= r.budgetNodes;
270
+ r.state = "destroyed";
271
+ commands.push({ op: "unmount", screenId: r.screenId, reason: "reset" });
272
+ }
273
+ frozenPrefix = 0;
274
+ const rec = makeRecord(name, params);
275
+ pushMount(rec, false);
276
+ rec.state = "mounted";
277
+ stack.push(rec);
278
+ activeNodeCount += rec.budgetNodes;
279
+ commands.push({ op: "enter", screenId: rec.screenId, transition: rec.transition });
280
+ applyMemoryPolicy();
281
+ emit({ type: "reset", name, depth: stack.length });
282
+ }
283
+ function navigate(frames2) {
284
+ for (const f of frames2) specOf2(f.name);
285
+ if (frames2.length === 0) {
286
+ if (stack.length > 0) {
287
+ resetEmpty();
288
+ }
289
+ return;
290
+ }
291
+ if (frames2.length === stack.length && frames2.every((f, i) => stack[i].name === f.name && paramKey(stack[i].params) === paramKey(f.params))) {
292
+ return;
293
+ }
294
+ let common = 0;
295
+ while (common < stack.length && common < frames2.length && stack[common].name === frames2[common].name && paramKey(stack[common].params) === paramKey(frames2[common].params)) {
296
+ common++;
297
+ }
298
+ const oldTop = stack[stack.length - 1] ?? null;
299
+ let popRemoved = null;
300
+ let pushCount = 0;
301
+ if (stack.length > common) {
302
+ const removed = [];
303
+ while (stack.length > common) removed.push(stack.pop());
304
+ const t = frames2[common]?.transition ?? removed[0].transition;
305
+ commands.push({ op: "exit", screenId: removed[0].screenId, transition: t });
306
+ for (const r of removed) {
307
+ if (r.state !== "frozen") activeNodeCount -= r.budgetNodes;
308
+ r.state = "destroyed";
309
+ commands.push({ op: "unmount", screenId: r.screenId, reason: "pop" });
310
+ }
311
+ popRemoved = removed[0];
312
+ if (frozenPrefix > stack.length) frozenPrefix = stack.length;
313
+ }
314
+ if (oldTop && oldTop.state === "mounted" && stack[stack.length - 1] !== oldTop) {
315
+ deactivate(oldTop, frames2[common]?.transition ?? oldTop.transition);
316
+ }
317
+ for (let i = common; i < frames2.length; i++) {
318
+ const rec = makeRecord(frames2[i].name, frames2[i].params, frames2[i].transition);
319
+ pushMount(rec, false);
320
+ stack.push(rec);
321
+ activeNodeCount += rec.budgetNodes;
322
+ pushCount++;
323
+ }
324
+ activateTop();
325
+ applyMemoryPolicy();
326
+ if (popRemoved) emit({ type: "pop", name: popRemoved.name, depth: stack.length });
327
+ if (pushCount > 0) {
328
+ const top = stack[stack.length - 1];
329
+ if (top) emit({ type: "push", name: top.name, depth: stack.length });
330
+ }
331
+ }
332
+ function resetEmpty() {
333
+ while (stack.length) {
334
+ const r = stack.pop();
335
+ if (r.state !== "frozen") activeNodeCount -= r.budgetNodes;
336
+ r.state = "destroyed";
337
+ commands.push({ op: "unmount", screenId: r.screenId, reason: "reset" });
338
+ }
339
+ frozenPrefix = 0;
340
+ emit({ type: "reset", name: "", depth: 0 });
341
+ }
342
+ return {
343
+ push,
344
+ pop,
345
+ replace,
346
+ popTo,
347
+ removeByName,
348
+ moveToTop,
349
+ popToRoot,
350
+ tab: resetTo,
351
+ reset: resetTo,
352
+ navigate,
353
+ markRebuilt(screenId) {
354
+ const r = stack.find((s) => s.screenId === screenId);
355
+ if (r) r.needsRebuild = false;
356
+ },
357
+ drainCommands() {
358
+ const out = commands.slice();
359
+ commands.length = 0;
360
+ return out;
361
+ },
362
+ suspend,
363
+ resume,
364
+ releaseTrees,
365
+ frames,
366
+ current() {
367
+ return stack[stack.length - 1] ?? null;
368
+ },
369
+ get stack() {
370
+ return stack.slice();
371
+ },
372
+ get depth() {
373
+ return stack.length;
374
+ },
375
+ stats() {
376
+ let mounted = 0;
377
+ let hidden = 0;
378
+ let frozen = 0;
379
+ for (const r of stack) {
380
+ if (r.state === "mounted") mounted++;
381
+ else if (r.state === "hidden") hidden++;
382
+ else frozen++;
383
+ }
384
+ return {
385
+ depth: stack.length,
386
+ mounted,
387
+ hidden,
388
+ frozen,
389
+ activeNodes: activeNodes(),
390
+ nodeBudget: policy.nodeBudget,
391
+ overBudget,
392
+ freezeCount,
393
+ rebuildCount
394
+ };
395
+ },
396
+ on(handler) {
397
+ handlers.push(handler);
398
+ return () => {
399
+ const i = handlers.indexOf(handler);
400
+ if (i >= 0) handlers.splice(i, 1);
401
+ };
402
+ }
403
+ };
404
+ }
405
+
406
+ // ../router/src/app-adapter.ts
407
+ function normPath(p) {
408
+ return p.replace(/^\/+/, "").replace(/\/+$/, "");
409
+ }
410
+ function parseRouterUrl(url) {
411
+ const [rawPath, rawQuery = ""] = url.split("?");
412
+ const params = {};
413
+ if (rawQuery) {
414
+ for (const pair of rawQuery.split("&")) {
415
+ if (!pair) continue;
416
+ const eq = pair.indexOf("=");
417
+ const k = eq < 0 ? pair : pair.slice(0, eq);
418
+ const v = eq < 0 ? "" : pair.slice(eq + 1);
419
+ params[decodeURIComponent(k)] = decodeURIComponent(v);
420
+ }
421
+ }
422
+ return { path: rawPath ?? "", params };
423
+ }
424
+ function createAppNavigationAdapter(opts) {
425
+ const { stack, pump, screens } = opts;
426
+ const byPath = /* @__PURE__ */ new Map();
427
+ for (const spec of Object.values(screens)) byPath.set(normPath(spec.path), spec.name);
428
+ if (byPath.size === 0) {
429
+ throw new Error("[app-adapter] \u5C4F\u6CE8\u518C\u8868\u4E3A\u7A7A\u2014\u2014App \u7AEF\u5BFC\u822A\u9700\u8981 screens\uFF08\u4E0E createAppStack \u540C\u6E90\uFF09");
430
+ }
431
+ const resolveName = (urlPath) => {
432
+ const name = byPath.get(normPath(urlPath));
433
+ if (!name) {
434
+ throw new Error(
435
+ `[app-adapter] \u672A\u77E5\u8DEF\u5F84 "${urlPath}"\uFF08\u5C4F\u6CE8\u518C\u8868\u65E0\u6B64\u5C4F\uFF09\u2014\u2014\u68C0\u67E5\u8DEF\u7531\u8868\u4E0E\u5C4F\u6CE8\u518C\u8868\u540C\u6E90\uFF1A\u53EF\u7528\u8DEF\u5F84 ${[...byPath.keys()].slice(0, 8).join(", ")}${byPath.size > 8 ? " \u2026" : ""}`
436
+ );
437
+ }
438
+ return name;
439
+ };
440
+ let chain = Promise.resolve();
441
+ const fire = () => {
442
+ chain = chain.then(() => pump()).catch((e) => {
443
+ console.error("[app-adapter] \u6CF5\u5931\u8D25\uFF1A", e);
444
+ });
445
+ return chain;
446
+ };
447
+ const flush = () => chain;
448
+ const doUrl = (url) => {
449
+ const { path, params } = parseRouterUrl(url);
450
+ return { name: resolveName(path), params };
451
+ };
452
+ return {
453
+ isMP: false,
454
+ getCurrentPages() {
455
+ return stack.stack.map((rec) => ({
456
+ route: rec.path,
457
+ query: Object.fromEntries(Object.entries(rec.params).map(([k, v]) => [k, String(v ?? "")]))
458
+ }));
459
+ },
460
+ async navigateTo({ url }) {
461
+ const { name, params } = doUrl(url);
462
+ stack.push(name, params);
463
+ await fire();
464
+ },
465
+ async redirectTo({ url }) {
466
+ const { name, params } = doUrl(url);
467
+ stack.replace(name, params);
468
+ await fire();
469
+ },
470
+ async reLaunch({ url }) {
471
+ const { name, params } = doUrl(url);
472
+ stack.reset(name, params);
473
+ await fire();
474
+ },
475
+ async switchTab({ url }) {
476
+ const { name, params } = doUrl(url);
477
+ stack.tab(name, params);
478
+ await fire();
479
+ },
480
+ navigateBack({ delta }) {
481
+ stack.pop(delta);
482
+ void fire();
483
+ },
484
+ // ★★★(2026-10-02 · 启示 3)**App 专属栈原语**——Web/MP 受平台栈语义限制,App 虚拟栈完整支持
485
+ // ("App 端可解除 Skyline 硬限制"的又一兑现:按名回退/移除/提顶都不需要平台配合)
486
+ popTo(name) {
487
+ stack.popTo(name);
488
+ void fire();
489
+ },
490
+ removeByName(name) {
491
+ const n = stack.removeByName(name);
492
+ void fire();
493
+ return n;
494
+ },
495
+ moveToTop(name) {
496
+ stack.moveToTop(name);
497
+ void fire();
498
+ },
499
+ flush
500
+ };
501
+ }
502
+ function screensFromRoutes(routes) {
503
+ const out = {};
504
+ for (const r of routes) {
505
+ if (!r.name) {
506
+ throw new Error(`[app-adapter] \u8DEF\u7531\u8868\u6761\u76EE\u7F3A name\uFF08path="${r.path}"\uFF09\u2014\u2014\u68C0\u67E5 gen-routes \u4EA7\u7269`);
507
+ }
508
+ const spec = { name: r.name, path: r.path };
509
+ const t = r.meta?.transition;
510
+ if (typeof t === "string") spec.transition = t;
511
+ const b = r.meta?.budgetNodes;
512
+ if (typeof b === "number" && b > 0) spec.budgetNodes = b;
513
+ const ka = r.meta?.branch?.keepAlive;
514
+ if (ka === "none" || ka === "active" || ka === "all") spec.keepAlive = ka;
515
+ out[r.name] = spec;
516
+ }
517
+ return out;
518
+ }
519
+
520
+ // ../contracts/src/layers.ts
521
+ var LAYER_PRIMITIVES = ["layer-content", "layer-navigation", "layer-mask", "layer-popout"];
522
+
523
+ // ../contracts/src/mount-layers.ts
524
+ var MOUNT_LAYERS = ["global", "page", "overlay"];
525
+ var MOUNT_LAYER_ORDER = {
526
+ global: 0,
527
+ page: 1,
528
+ overlay: 2
529
+ };
530
+ var MOUNT_LAYER_NODE_OFFSET = {
531
+ global: 1,
532
+ page: 2,
533
+ overlay: 3
534
+ };
535
+ function mountLayerContainerPlans() {
536
+ return [...MOUNT_LAYERS].sort((a, b) => MOUNT_LAYER_ORDER[a] - MOUNT_LAYER_ORDER[b]).map((layer) => ({
537
+ layer,
538
+ nodeOffset: MOUNT_LAYER_NODE_OFFSET[layer],
539
+ order: MOUNT_LAYER_ORDER[layer],
540
+ frame: "fullscreen"
541
+ }));
542
+ }
543
+
544
+ // ../contracts/src/scroll.ts
545
+ var DOCK_REQUIRES_LAYER = LAYER_PRIMITIVES.includes("layer-navigation");
546
+
547
+ // src/screen-executor.ts
548
+ function createScreenExecutor(opts) {
549
+ const { host, anim, plan, contentOf, onScreenMounted, onEvent } = opts;
550
+ const nodes = /* @__PURE__ */ new Map();
551
+ const stats = {
552
+ commands: 0,
553
+ transitions: 0,
554
+ forward: 0,
555
+ back: 0,
556
+ skippedNone: 0,
557
+ destroyed: { pop: 0, reset: 0, freeze: 0 },
558
+ visibility: { shown: 0, hidden: 0 },
559
+ mounts: { first: 0, rebuild: 0 },
560
+ errors: []
561
+ };
562
+ let txn = { exiting: null, mounted: /* @__PURE__ */ new Map() };
563
+ const emit = (type, screenId, detail) => {
564
+ onEvent?.({ type, screenId, detail });
565
+ };
566
+ async function runCommand(cmd) {
567
+ stats.commands++;
568
+ switch (cmd.op) {
569
+ case "mount": {
570
+ const rebuild = cmd.rebuild === true;
571
+ const name = cmd.name ?? cmd.screenId;
572
+ const path = cmd.path ?? "";
573
+ const content = contentOf?.({ name, path });
574
+ const node = await host.mountScreen({
575
+ screenId: cmd.screenId,
576
+ name,
577
+ path,
578
+ params: cmd.params ?? null,
579
+ rebuild,
580
+ ...content ? { content } : {},
581
+ // ★GP3-c:三层容器计划(由**契约**算,宿主只消费——偏移量而非绝对 id,见类型注释)
582
+ layerContainers: mountLayerContainerPlans()
583
+ });
584
+ nodes.set(cmd.screenId, node);
585
+ txn.mounted.set(cmd.screenId, rebuild);
586
+ if (rebuild) stats.mounts.rebuild++;
587
+ else stats.mounts.first++;
588
+ emit("mount", cmd.screenId, { rebuild, node, contentNodes: content?.nodes.length ?? 0 });
589
+ onScreenMounted?.(cmd.screenId, rebuild);
590
+ return;
591
+ }
592
+ case "exit": {
593
+ txn.exiting = { screenId: cmd.screenId, transition: cmd.transition, destroy: null };
594
+ emit("exit", cmd.screenId, { transition: cmd.transition });
595
+ return;
596
+ }
597
+ case "unmount": {
598
+ const reason = cmd.reason ?? "pop";
599
+ if (txn.exiting && txn.exiting.screenId === cmd.screenId) {
600
+ txn.exiting.destroy = reason;
601
+ emit("unmount-deferred", cmd.screenId, { reason });
602
+ return;
603
+ }
604
+ await host.destroyScreen(cmd.screenId, reason, nodes.get(cmd.screenId));
605
+ nodes.delete(cmd.screenId);
606
+ stats.destroyed[reason]++;
607
+ emit("destroy", cmd.screenId, { reason });
608
+ return;
609
+ }
610
+ case "enter": {
611
+ const incomingNode = nodes.get(cmd.screenId);
612
+ if (incomingNode === void 0) {
613
+ stats.errors.push(`enter(${cmd.screenId})\uFF1A\u5C4F\u5B50\u6811\u4E0D\u5B58\u5728\uFF08mount \u672A\u6267\u884C\uFF1F\uFF09`);
614
+ emit("error", cmd.screenId, "enter-without-subtree");
615
+ return;
616
+ }
617
+ const exiting = txn.exiting;
618
+ const outgoingNode = exiting ? nodes.get(exiting.screenId) : void 0;
619
+ const mountedHere = txn.mounted.get(cmd.screenId);
620
+ const direction = mountedHere === false ? "forward" : "back";
621
+ const transition = cmd.transition;
622
+ const batch = plan(transition, { incoming: incomingNode, outgoing: outgoingNode }, { direction });
623
+ const normalized = typeof transition === "string" ? transition : "none";
624
+ await host.setScreenVisible(cmd.screenId, true, incomingNode);
625
+ stats.visibility.shown++;
626
+ emit("visible", cmd.screenId, true);
627
+ const hasAnims = batch.incoming.anims.length > 0 || batch.outgoing.anims.length > 0;
628
+ if (hasAnims) {
629
+ await anim.playRouteTransition(batch, { direction, transition: normalized });
630
+ stats.transitions++;
631
+ if (direction === "forward") stats.forward++;
632
+ else stats.back++;
633
+ emit("transition", cmd.screenId, { direction, transition: normalized, anims: batch.incoming.anims.length + batch.outgoing.anims.length });
634
+ } else {
635
+ stats.skippedNone++;
636
+ emit("transition-skipped", cmd.screenId, { transition: normalized });
637
+ }
638
+ if (exiting) {
639
+ if (exiting.destroy !== null) {
640
+ await host.destroyScreen(exiting.screenId, exiting.destroy, outgoingNode);
641
+ nodes.delete(exiting.screenId);
642
+ stats.destroyed[exiting.destroy]++;
643
+ emit("destroy", exiting.screenId, { reason: exiting.destroy, deferred: true });
644
+ } else if (outgoingNode !== void 0) {
645
+ await host.setScreenVisible(exiting.screenId, false, outgoingNode);
646
+ stats.visibility.hidden++;
647
+ emit("visible", exiting.screenId, false);
648
+ }
649
+ }
650
+ txn = { exiting: null, mounted: /* @__PURE__ */ new Map() };
651
+ return;
652
+ }
653
+ }
654
+ }
655
+ let queue = Promise.resolve();
656
+ return {
657
+ applyCommands(commands) {
658
+ queue = queue.then(async () => {
659
+ for (const c of commands) await runCommand(c);
660
+ });
661
+ return queue;
662
+ },
663
+ stats: () => JSON.parse(JSON.stringify(stats)),
664
+ subtreeNode: (screenId) => nodes.get(screenId)
665
+ };
666
+ }
667
+
668
+ // src/screen-executor-host.ts
669
+ var SCREEN_ANIM_DONE_KEY = "__proteusHostScreenAnimDone";
670
+ var instSeq = 0;
671
+ function call(channel, method, args) {
672
+ const raw = channel(method, JSON.stringify(args ?? null));
673
+ let parsed;
674
+ try {
675
+ parsed = JSON.parse(raw);
676
+ } catch (e) {
677
+ throw new Error(`[screen-host] ${method} \u56DE\u6267\u975E JSON\uFF1A${String(raw).slice(0, 120)}`);
678
+ }
679
+ if (parsed && parsed.ok === false) {
680
+ throw new Error(`[screen-host] ${method} \u5931\u8D25${parsed.missing ? "\uFF08\u672A\u5B9E\u73B0\uFF09" : ""}\uFF1A${parsed.reason ?? "\u65E0\u539F\u56E0"}`);
681
+ }
682
+ return parsed && typeof parsed === "object" && "data" in parsed ? parsed.data : parsed;
683
+ }
684
+ function createHostScreenPorts(opts) {
685
+ const channel = opts.invoke;
686
+ const pending = /* @__PURE__ */ new Map();
687
+ let tokenSeq = 0;
688
+ let hookInstalled = false;
689
+ const INSTANCE_ID = `s${++instSeq}-`;
690
+ const resolveOne = (token, payload) => {
691
+ const r = pending.get(token);
692
+ if (!r) return false;
693
+ pending.delete(token);
694
+ r(payload);
695
+ return true;
696
+ };
697
+ if (opts.installAnimDoneHook !== false) {
698
+ const g = globalThis;
699
+ const REGISTRY_KEY = `${SCREEN_ANIM_DONE_KEY}__registry`;
700
+ let registry = g[REGISTRY_KEY];
701
+ if (!Array.isArray(registry)) {
702
+ registry = [];
703
+ g[REGISTRY_KEY] = registry;
704
+ }
705
+ const handler = (token, resultJson) => {
706
+ if (typeof token !== "string") return "bad-token";
707
+ const r = registry;
708
+ for (let i = 0; i < r.length; i++) {
709
+ if (r[i](token, resultJson)) return "ok";
710
+ }
711
+ return "unknown-token";
712
+ };
713
+ registry.push((token, payload) => resolveOne(token, payload));
714
+ try {
715
+ g[SCREEN_ANIM_DONE_KEY] = handler;
716
+ hookInstalled = true;
717
+ } catch {
718
+ try {
719
+ Object.defineProperty(g, SCREEN_ANIM_DONE_KEY, { value: handler, writable: true, configurable: true });
720
+ hookInstalled = true;
721
+ } catch {
722
+ }
723
+ }
724
+ }
725
+ const tree = {
726
+ mountScreen(screen) {
727
+ const d = call(channel, "screen.mount", {
728
+ screenId: screen.screenId,
729
+ name: screen.name,
730
+ path: screen.path,
731
+ params: screen.params,
732
+ rebuild: screen.rebuild,
733
+ // ★★★GP3-c:**三层容器计划**随 mount 下发(宿主照此建树——偏移量,宿主加到自己分配的根 id 上)。
734
+ // 执行器已按契约算好(见 screen-executor.ts);本层只做**透传**(不再重复计算——
735
+ // 本仓纪律:同一件事两份实现 = 修一份等于没修)。
736
+ ...screen.layerContainers ? { layerContainers: screen.layerContainers } : {},
737
+ // ★★★阶段 1(2026-10-04 · App 三端对齐 B1+B2):**屏内容**(真实页面渲染产物)透传。
738
+ // 执行器按 `contentOf` 提供者解析后放入 mountScreen 入参;本层只做转发(与上方同款纪律)。
739
+ ...screen.content ? { content: screen.content } : {}
740
+ });
741
+ const rootNodeId = Number(d?.rootNodeId ?? 0);
742
+ if (!rootNodeId) {
743
+ throw new Error(`[screen-host] screen.mount \u672A\u8FD4\u56DE rootNodeId\uFF08${JSON.stringify(d)}\uFF09\u2014\u2014\u5BBF\u4E3B\u5B9E\u73B0\u4E0D\u5B8C\u6574`);
744
+ }
745
+ return rootNodeId;
746
+ },
747
+ setScreenVisible(screenId, visible, rootNodeId) {
748
+ call(channel, "screen.visible", { screenId, visible, rootNodeId });
749
+ },
750
+ destroyScreen(screenId, reason, rootNodeId) {
751
+ call(channel, "screen.destroy", { screenId, reason, rootNodeId });
752
+ }
753
+ };
754
+ const anim = {
755
+ playRouteTransition(plan, ctx) {
756
+ const anims = [...plan.incoming.anims, ...plan.outgoing.anims];
757
+ if (anims.length === 0) return;
758
+ const token = `${INSTANCE_ID}screen-anim-${++tokenSeq}`;
759
+ const d = call(channel, "screen.anim", {
760
+ anims,
761
+ durationMs: plan.durationMs,
762
+ direction: ctx.direction,
763
+ transition: ctx.transition,
764
+ token
765
+ });
766
+ if (d?.immediate === true) return;
767
+ return new Promise((resolve) => {
768
+ pending.set(token, () => resolve());
769
+ });
770
+ }
771
+ };
772
+ return {
773
+ tree,
774
+ anim,
775
+ shared: {
776
+ async rect(screenId, nodeId) {
777
+ const d = call(channel, "screen.rect", { screenId, nodeId });
778
+ if (!d || typeof d.x !== "number") {
779
+ throw new Error(`[screen-host] screen.rect \u672A\u8FD4\u56DE\u51E0\u4F55\uFF08${JSON.stringify(d)}\uFF09`);
780
+ }
781
+ return { x: d.x, y: d.y ?? 0, w: d.width ?? 0, h: d.height ?? 0 };
782
+ },
783
+ fly(opts2) {
784
+ const token = `screen-shared-${++tokenSeq}`;
785
+ const d = call(channel, "screen.shared", { ...opts2, token });
786
+ if (!d || (d.started ?? 0) < 1) {
787
+ return Promise.reject(new Error(`[screen-host] screen.shared \u672A\u542F\u52A8\uFF08${JSON.stringify(d)}\uFF09`));
788
+ }
789
+ const geom = { fromRect: d.fromRect, toRect: d.toRect };
790
+ return new Promise((resolve) => {
791
+ pending.set(token, (v) => {
792
+ let extra = {};
793
+ if (typeof v === "string") {
794
+ try {
795
+ const o = JSON.parse(v);
796
+ if (o && typeof o === "object") extra = o;
797
+ } catch {
798
+ }
799
+ } else if (v && typeof v === "object") {
800
+ extra = v;
801
+ }
802
+ resolve({ ...geom, ...extra });
803
+ });
804
+ });
805
+ }
806
+ },
807
+ get pendingAnimations() {
808
+ return pending.size;
809
+ },
810
+ get animDoneHookInstalled() {
811
+ return hookInstalled;
812
+ }
813
+ };
814
+ }
815
+
816
+ // ../animation/dist/index.js
817
+ var AnimKind = {
818
+ TRANSLATE_X: 0,
819
+ TRANSLATE_Y: 1,
820
+ SCALE: 2,
821
+ ROTATE: 3,
822
+ OPACITY: 4,
823
+ // ★★颜色(2026-10-01):用户面**一个** `color` 声明,内核面**四个标量通道**(kind 5..8)。
824
+ // 分解的理由见内核 `anim.rs::AnimKind` 头注(求值机器全是标量的 ⇒ 零改动复用)。
825
+ // ★对外**不暴露**通道号(`ANIM_KIND_ID` 里也没有它们)——那是编译内部的事。
826
+ COLOR_R: 5,
827
+ COLOR_G: 6,
828
+ COLOR_B: 7,
829
+ COLOR_A: 8,
830
+ // ★★文字色(2026-10-01):与底色**同一条数学、不同的样式槽**(`text_color` vs `bg`)。
831
+ // 分成两组编号(而不是复用 5..8)的唯一原因:内核 `write()` 必须知道**往哪个槽写**——
832
+ // 用同一批编号无法区分(会在"同时动底色与文字色"时互相覆盖)。
833
+ TEXT_COLOR_R: 9,
834
+ TEXT_COLOR_G: 10,
835
+ TEXT_COLOR_B: 11,
836
+ TEXT_COLOR_A: 12,
837
+ // ★★3D 旋转(2026-10-01 · B 批):与 `rotate`(Z 轴)并列的两个轴。
838
+ // 走 tick 路径(不进平台零参与——iOS/Android 的 3D 插值语义不同,跨端一致优先;
839
+ // 见内核 is_composited 注释)。
840
+ ROTATE_X: 13,
841
+ ROTATE_Y: 14,
842
+ // ★★裁剪形状参数(2026-10-01 · C1):最多 16 条标量通道(与颜色同源的分解法)。
843
+ // 含义按节点声明的**形状类型**解释(见 `ClipShape`);类型静态、参数可动画(CSS 同规)。
844
+ CLIP0: 15,
845
+ CLIP1: 16,
846
+ CLIP2: 17,
847
+ CLIP3: 18,
848
+ CLIP4: 19,
849
+ CLIP5: 20,
850
+ CLIP6: 21,
851
+ CLIP7: 22,
852
+ CLIP8: 23,
853
+ CLIP9: 24,
854
+ CLIP10: 25,
855
+ CLIP11: 26,
856
+ CLIP12: 27,
857
+ CLIP13: 28,
858
+ CLIP14: 29,
859
+ CLIP15: 30,
860
+ /** ★★SVG 描边进度(2026-10-01 · C2):0..1 = 沿路径弧长画到哪("手写字"动效) */
861
+ STROKE_PROGRESS: 31,
862
+ /** ★★渐变混合因子(2026-10-01 · 渐变 v2):0..1 = A 态(fillGradient)→ B 态(fillGradientTo) */
863
+ GRADIENT_MIX: 32,
864
+ /** ★★路径变形因子(2026-10-01 · 路径变形 v1):0..1 = A 态(svgPath)→ B 态(svgPathTo) */
865
+ PATH_MORPH: 33,
866
+ /** ★★发光强度(2026-10-01 · glow v1):0..1 乘子(1 = 按声明全额发光) */
867
+ GLOW_INTENSITY: 34,
868
+ /** ★★遮罩进度(2026-10-01 · mask v1):0..1 = 软边揭示的进度(0=全隐 / 1=全显) */
869
+ MASK_PROGRESS: 35,
870
+ /** ★★倾斜 X(2026-10-01 · skew v1;度) */
871
+ SKEW_X: 36,
872
+ /** ★★倾斜 Y(2026-10-01 · skew v1;度) */
873
+ SKEW_Y: 37
874
+ };
875
+ var ANIM_KIND_ID = {
876
+ translateX: AnimKind.TRANSLATE_X,
877
+ translateY: AnimKind.TRANSLATE_Y,
878
+ scale: AnimKind.SCALE,
879
+ rotate: AnimKind.ROTATE,
880
+ opacity: AnimKind.OPACITY,
881
+ // ★`color` 的编号是**名义值**:编译期会把它展开成 4 条通道指令(见 `compileOne`)。
882
+ // 这里给 COLOR_R 是为了让 `ANIM_KIND_ID` 保持"每个名字都有编号"的完备形状——
883
+ // 直接消费它会少写 3 个通道,故 `compileOne` 有专门的 `color` 分支(不读这个值)。
884
+ color: AnimKind.COLOR_R,
885
+ // ★`textColor` 同理(名义值 = 文字色 R 通道 9;编译期展开成 4 条)
886
+ textColor: AnimKind.TEXT_COLOR_R,
887
+ // ★3D 旋转(单通道——无数值展开)
888
+ rotateX: AnimKind.ROTATE_X,
889
+ rotateY: AnimKind.ROTATE_Y,
890
+ // ★`clip` 的编号是**名义值**(= 参数槽 0 的 15):编译期按参数个数展开成 N 条参数通道
891
+ // (见 `isClipDecl` 分支——它不读这个值)。给名义值是为了 `ANIM_KIND_ID` 的完备形状。
892
+ clip: AnimKind.CLIP0,
893
+ // ★描边进度是**单通道**(31——与内核同号)
894
+ strokeProgress: AnimKind.STROKE_PROGRESS,
895
+ // ★渐变混合是**单通道**(32——与内核同号)
896
+ gradientMix: AnimKind.GRADIENT_MIX,
897
+ // ★路径变形是**单通道**(33——与内核同号)
898
+ pathMorph: AnimKind.PATH_MORPH,
899
+ // ★发光强度是**单通道**(34——与内核同号)
900
+ glowIntensity: AnimKind.GLOW_INTENSITY,
901
+ // ★遮罩进度是**单通道**(35——与内核同号)
902
+ maskProgress: AnimKind.MASK_PROGRESS,
903
+ // ★倾斜是**两条单通道**(36/37——与内核同号;X/Y 各一条:可独立动或一起动)
904
+ skewX: AnimKind.SKEW_X,
905
+ skewY: AnimKind.SKEW_Y
906
+ };
907
+ var Curve = {
908
+ LINEAR: 0,
909
+ EASE_OUT: 1,
910
+ EASE_IN: 2,
911
+ EASE_IN_OUT: 3,
912
+ /** 阻尼振荡近似(查表);**与真弹簧不同**——需要物理语义时用 `spring` */
913
+ SPRING_APPROX: 4
914
+ };
915
+ var CURVE_ID = {
916
+ linear: Curve.LINEAR,
917
+ easeOut: Curve.EASE_OUT,
918
+ easeIn: Curve.EASE_IN,
919
+ easeInOut: Curve.EASE_IN_OUT,
920
+ springApprox: Curve.SPRING_APPROX
921
+ };
922
+ function parseColorToChannels(css) {
923
+ if (typeof css !== "string") {
924
+ throw new Error(`\u989C\u8272\u5FC5\u987B\u662F\u5B57\u7B26\u4E32\uFF08\u6536\u5230 ${typeof css}\uFF09\u2014\u2014\u7528 '#RRGGBB' \u5F62\u6001`);
925
+ }
926
+ const s = css.trim();
927
+ if (!s.startsWith("#")) {
928
+ throw new Error(`\u989C\u8272 "${s}" \u4E0D\u652F\u6301\uFF1A\u53EA\u63A5\u53D7 #RGB / #RRGGBB / #RRGGBBAA \u5341\u516D\u8FDB\u5236\u5F62\u6001`);
929
+ }
930
+ const hex = s.slice(1);
931
+ if (!/^[0-9a-fA-F]+$/.test(hex)) {
932
+ throw new Error(`\u989C\u8272 "${s}" \u4E0D\u662F\u5408\u6CD5\u5341\u516D\u8FDB\u5236\uFF08\u5E94\u4E3A #RGB / #RRGGBB / #RRGGBBAA\uFF09`);
933
+ }
934
+ switch (hex.length) {
935
+ case 3: {
936
+ return {
937
+ r: parseInt(hex[0] + hex[0], 16),
938
+ g: parseInt(hex[1] + hex[1], 16),
939
+ b: parseInt(hex[2] + hex[2], 16),
940
+ a: 255
941
+ };
942
+ }
943
+ case 6:
944
+ return {
945
+ r: parseInt(hex.slice(0, 2), 16),
946
+ g: parseInt(hex.slice(2, 4), 16),
947
+ b: parseInt(hex.slice(4, 6), 16),
948
+ a: 255
949
+ };
950
+ case 8:
951
+ return {
952
+ r: parseInt(hex.slice(0, 2), 16),
953
+ g: parseInt(hex.slice(2, 4), 16),
954
+ b: parseInt(hex.slice(4, 6), 16),
955
+ a: parseInt(hex.slice(6, 8), 16)
956
+ };
957
+ default:
958
+ throw new Error(
959
+ `\u989C\u8272 "${s}" \u4F4D\u6570\u975E\u6CD5\uFF08${hex.length} \u4F4D\uFF09\u2014\u2014\u53EA\u63A5\u53D7 #RGB\uFF083\uFF09/ #RRGGBB\uFF086\uFF09/ #RRGGBBAA\uFF088\uFF09`
960
+ );
961
+ }
962
+ }
963
+ var COMPOSITED_KINDS = [
964
+ "translateX",
965
+ "translateY",
966
+ "scale",
967
+ "rotate",
968
+ "opacity"
969
+ ];
970
+ var TICK_ONLY_KINDS = [
971
+ "rotateX",
972
+ "rotateY",
973
+ // ★★C1(2026-10-01):裁剪形状是绘制期约束(canvas.clipPath / CALayer.mask)——
974
+ // Android RenderNode 无"可动画裁剪形状"属性 ⇒ 两端统一 tick 路径。
975
+ "clip",
976
+ // ★★C2(2026-10-01):SVG 描边进度同样非合成(改占位层/重画路径)⇒ tick 路径。
977
+ "strokeProgress",
978
+ // ★★渐变 v2(2026-10-01):色标混合是 paint 状态(改 shader/渐变层),且 lerp 只在
979
+ // 内核一处 ⇒ 两端一致走 tick(与颜色同类决策)。
980
+ "gradientMix",
981
+ // ★★路径变形 v1(2026-10-01):改的是**几何**(宿主每帧重建平台 path),且 lerp 只在
982
+ // 内核一处(宿主只翻译变形后的段)⇒ 必走 tick(与描边同族但更重)。
983
+ "pathMorph",
984
+ // ★★发光强度(glow v1):改的是 paint 状态(分层描边 alpha)——非合成,走 tick。
985
+ "glowIntensity",
986
+ // ★★遮罩进度(mask v1):改的是**合成状态**(layer.mask / saveLayer+DST_IN)——非合成,走 tick。
987
+ "maskProgress",
988
+ // ★★倾斜(skew v1):两端都不是"一等属性"(Android 无 setSkewX / iOS 无倾斜属性)
989
+ // ⇒ 平台插值器无从谈起 ⇒ 统一 tick(与 rotateX/Y 同一推理)。
990
+ "skewX",
991
+ "skewY"
992
+ ];
993
+ function isTickOnly(kind) {
994
+ return TICK_ONLY_KINDS.includes(kind);
995
+ }
996
+ function isComposited(kind) {
997
+ return COMPOSITED_KINDS.includes(kind);
998
+ }
999
+ function checkCurveBezier(d, i, issues) {
1000
+ const cb = d.curveBezier;
1001
+ if (cb === void 0) return;
1002
+ if (!Array.isArray(cb) || cb.length !== 4) {
1003
+ issues.push({
1004
+ index: i,
1005
+ code: "invalid-range",
1006
+ message: `\`curveBezier\` \u9700\u8981 4 \u4E2A\u63A7\u5236\u70B9 [x1,y1,x2,y2]\uFF0C\u6536\u5230 ${JSON.stringify(cb)}`,
1007
+ hint: "\u4F8B\uFF1AcurveBezier: [0.34, 1.56, 0.64, 1]\uFF08\u56DE\u5F39\uFF09\u6216 [0.2, 0, 0, 1]"
1008
+ });
1009
+ return;
1010
+ }
1011
+ const bad = cb.some((v) => typeof v !== "number" || !Number.isFinite(v));
1012
+ if (bad) {
1013
+ issues.push({ index: i, code: "invalid-range", message: `\`curveBezier\` \u542B\u975E\u6709\u9650\u6570\uFF1A${JSON.stringify(cb)}`, hint: "\u7ED9\u5177\u4F53\u6570\u503C" });
1014
+ return;
1015
+ }
1016
+ if (cb[0] < 0 || cb[0] > 1 || cb[2] < 0 || cb[2] > 1) {
1017
+ issues.push({
1018
+ index: i,
1019
+ code: "invalid-range",
1020
+ message: `\`curveBezier\` \u7684 x1/x2 \u5FC5\u987B\u5728 [0,1]\uFF08\u65F6\u95F4\u8F74\u5355\u8C03\u2014\u2014\u5426\u5219\u7ED9\u5B9A\u8FDB\u5EA6\u6C42\u503C\u4E0D\u552F\u4E00\uFF09\uFF1Ax1=${cb[0]}, x2=${cb[2]}`,
1021
+ hint: "y1/y2 \u53EF\u4EE5\u4EFB\u610F\uFF08> 1 \u6216 < 0 \u662F\u56DE\u5F39/\u9884\u671F\u6548\u679C\uFF09\uFF1B\u53EA\u6709 x \u53D7\u9650\uFF08CSS cubic-bezier \u540C\u89C4\uFF09"
1022
+ });
1023
+ }
1024
+ if (d.curve !== void 0) {
1025
+ issues.push({
1026
+ index: i,
1027
+ code: "conflicting-easing",
1028
+ message: "`curveBezier` \u4E0E `curve` \u5E76\u5B58 \u2014\u2014 \u4E24\u5904\u90FD\u63CF\u8FF0\u7F13\u52A8\uFF0C\u5FC5\u987B\u552F\u4E00",
1029
+ hint: "\u5220\u6389 `curve`\uFF08\u5C01\u95ED\u96C6\u5FEB\u6377\u540D\uFF09\uFF0C\u53EA\u7559 `curveBezier`"
1030
+ });
1031
+ }
1032
+ if (d.spring !== void 0) {
1033
+ issues.push({
1034
+ index: i,
1035
+ code: "conflicting-easing",
1036
+ message: "`curveBezier` \u4E0E `spring` \u5E76\u5B58 \u2014\u2014 \u6C42\u503C\u6A21\u5F0F\u5FC5\u987B\u552F\u4E00",
1037
+ hint: "\u4E8C\u9009\u4E00\uFF1A\u8981\u7269\u7406\u624B\u611F\u7528 `spring`\uFF0C\u8981\u786E\u5B9A\u66F2\u7EBF\u7528 `curveBezier`"
1038
+ });
1039
+ }
1040
+ if (d.keyframes !== void 0) {
1041
+ issues.push({
1042
+ index: i,
1043
+ code: "conflicting-easing",
1044
+ message: "`curveBezier` \u4E0E `keyframes` \u5E76\u5B58 \u2014\u2014 \u6BB5\u7EA7\u66F2\u7EBF\u76EE\u524D\u53EA\u7528\u5C01\u95ED\u96C6\uFF08\u8BDA\u5B9E\u8FB9\u754C\uFF09",
1045
+ hint: "\u5220\u6389 `curveBezier`\uFF08\u591A\u6BB5\u5E8F\u5217\u91CC\u6BCF\u6BB5\u7528 `curve` \u5C01\u95ED\u96C6\uFF09\uFF0C\u6216\u6539\u7528\u5355\u6BB5 + `curveBezier`"
1046
+ });
1047
+ }
1048
+ }
1049
+ function validateAnimations(decls) {
1050
+ const issues = [];
1051
+ if (decls.length === 0) {
1052
+ issues.push({
1053
+ index: -1,
1054
+ code: "empty",
1055
+ message: "\u52A8\u753B\u58F0\u660E\u4E3A\u7A7A",
1056
+ hint: '\u81F3\u5C11\u7ED9\u4E00\u6761\u58F0\u660E\uFF1B\u82E5\u672C\u610F\u662F"\u505C\u6389\u52A8\u753B"\uFF0C\u8BF7\u8C03 stopAnimations \u800C\u4E0D\u662F\u7F16\u8BD1\u7A7A\u6279\u6B21'
1057
+ });
1058
+ return issues;
1059
+ }
1060
+ decls.forEach((d, i) => {
1061
+ if (d.kind === "color" || d.kind === "textColor") {
1062
+ if (d.from === void 0) {
1063
+ issues.push({
1064
+ index: i,
1065
+ code: "invalid-range",
1066
+ message: '`color` \u52A8\u753B\u7684 `from` \u662F**\u5FC5\u586B**\uFF08\u5185\u6838\u6CA1\u6709"\u7F3A\u7701 = \u8282\u70B9\u5F53\u524D\u5E95\u8272"\u8BED\u4E49\uFF09',
1067
+ hint: "\u663E\u5F0F\u7ED9\u8D77\u70B9\uFF0C\u5982 from: '#2f6fed'\uFF08\u901A\u5E38\u53D6\u8BE5\u8282\u70B9\u7684 backgroundColor\uFF09"
1068
+ });
1069
+ }
1070
+ for (const [key, val] of [
1071
+ ["from", d.from],
1072
+ ["to", d.to]
1073
+ ]) {
1074
+ if (val === void 0) continue;
1075
+ try {
1076
+ parseColorToChannels(val);
1077
+ } catch (e) {
1078
+ issues.push({
1079
+ index: i,
1080
+ code: "invalid-range",
1081
+ message: `\`color\` \u7684 \`${key}\` \u975E\u6CD5\uFF1A${e.message}`,
1082
+ hint: "\u7528\u5341\u516D\u8FDB\u5236\uFF1A'#RGB' / '#RRGGBB' / '#RRGGBBAA'\uFF08CSS4 \u5E8F\uFF0C\u6700\u540E\u4E24\u4F4D\u662F alpha\uFF09"
1083
+ });
1084
+ }
1085
+ }
1086
+ if (d.keyframes) {
1087
+ const kf = d.keyframes;
1088
+ if (kf.length === 0) {
1089
+ issues.push({
1090
+ index: i,
1091
+ code: "empty",
1092
+ message: "`keyframes` \u4E3A\u7A7A\u6570\u7EC4\uFF08\u5E8F\u5217\u81F3\u5C11\u8981\u4E00\u6BB5\uFF1B\u5355\u6BB5\u8BF7\u76F4\u63A5\u7528 `curve`\uFF09",
1093
+ hint: "\u53BB\u6389 `keyframes` \u7528\u5355\u6BB5\u58F0\u660E\uFF0C\u6216\u8865\u4E0A\u81F3\u5C11\u4E00\u6BB5 `{ to, durationMs }`"
1094
+ });
1095
+ }
1096
+ const sum = kf.reduce((acc, seg) => acc + (Number.isFinite(seg.durationMs) ? seg.durationMs : 0), 0);
1097
+ kf.forEach((seg, j) => {
1098
+ try {
1099
+ parseColorToChannels(seg.to);
1100
+ } catch (e) {
1101
+ issues.push({
1102
+ index: i,
1103
+ code: "invalid-range",
1104
+ message: `\u989C\u8272\u5E8F\u5217\u7B2C ${j} \u6BB5 \`to\` \u975E\u6CD5\uFF1A${e.message}`,
1105
+ hint: "\u6BCF\u6BB5\u7EC8\u70B9\u90FD\u8981\u662F\u989C\u8272\uFF1A'#RGB' / '#RRGGBB' / '#RRGGBBAA'"
1106
+ });
1107
+ }
1108
+ if (!Number.isFinite(seg.durationMs) || seg.durationMs < 0) {
1109
+ issues.push({
1110
+ index: i,
1111
+ code: "invalid-range",
1112
+ message: `\u989C\u8272\u5E8F\u5217\u7B2C ${j} \u6BB5 \`durationMs\` \u975E\u6CD5\uFF1A${seg.durationMs}`,
1113
+ hint: "\u7ED9\u975E\u8D1F\u6BEB\u79D2\u6570\uFF080 = \u8BE5\u6BB5\u77AC\u53D8\uFF0C\u5408\u6CD5\u4F46\u901A\u5E38\u4E0D\u662F\u672C\u610F\uFF09"
1114
+ });
1115
+ }
1116
+ if (seg.curve !== void 0 && !(seg.curve in CURVE_ID)) {
1117
+ issues.push({
1118
+ index: i,
1119
+ code: "invalid-range",
1120
+ message: `\u989C\u8272\u5E8F\u5217\u7B2C ${j} \u6BB5\u66F2\u7EBF\u672A\u77E5\uFF1A${seg.curve}`,
1121
+ hint: "\u7528 Curve \u5C01\u95ED\u96C6\u91CC\u7684\u540D\u5B57"
1122
+ });
1123
+ }
1124
+ });
1125
+ if (sum <= 0) {
1126
+ issues.push({
1127
+ index: i,
1128
+ code: "invalid-range",
1129
+ message: "\u989C\u8272\u5E8F\u5217\u603B\u65F6\u957F\u4E3A 0 \u2014\u2014 \u52A8\u753B\u4F1A**\u77AC\u95F4\u8DF3\u5230\u672B\u6BB5\u7EC8\u70B9**",
1130
+ hint: '\u81F3\u5C11\u7ED9\u4E00\u6BB5\u6B63\u65F6\u957F\uFF1B\u96F6\u65F6\u957F\u5E8F\u5217\u5728\u771F\u673A\u4E0A\u770B\u8D77\u6765\u5C31\u662F"\u6CA1\u505A\u52A8\u753B"'
1131
+ });
1132
+ }
1133
+ if (d.spring !== void 0) {
1134
+ issues.push({
1135
+ index: i,
1136
+ code: "conflicting-easing",
1137
+ message: "`keyframes` \u4E0E `spring` \u5E76\u5B58 \u2014\u2014 \u6C42\u503C\u6A21\u5F0F\u5FC5\u987B\u552F\u4E00",
1138
+ hint: "\u5E8F\u5217\u91CC\u8981\u5F39\u6027\u624B\u611F\uFF0C\u628A\u67D0\u4E00\u6BB5\u7528\u66F2\u7EBF\u8FD1\u4F3C\uFF08\u5982 springApprox\uFF09\uFF0C\u6216\u6574\u6761\u6539\u7528 spring"
1139
+ });
1140
+ }
1141
+ if (d.curve !== void 0) {
1142
+ issues.push({
1143
+ index: i,
1144
+ code: "conflicting-easing",
1145
+ message: "`keyframes` \u4E0E `curve` \u5E76\u5B58 \u2014\u2014 \u6BB5\u5185\u66F2\u7EBF\u7531\u6BCF\u6BB5\u81EA\u5DF1\u7684 `curve` \u51B3\u5B9A",
1146
+ hint: "\u5220\u6389\u5916\u5C42\u7684 `curve`\uFF08\u5B83\u53EA\u5BF9\u5355\u6BB5\u6A21\u5F0F\u6709\u610F\u4E49\uFF09"
1147
+ });
1148
+ }
1149
+ const last = kf[kf.length - 1];
1150
+ if (last && last.to.toLowerCase() !== d.to.toLowerCase()) {
1151
+ issues.push({
1152
+ index: i,
1153
+ code: "invalid-range",
1154
+ message: `\u672B\u6BB5 \`to\`(${last.to}) \u4E0E\u58F0\u660E \`to\`(${d.to}) \u4E0D\u4E00\u81F4 \u2014\u2014 \u4E24\u5904\u90FD\u63CF\u8FF0"\u7EC8\u70B9"`,
1155
+ hint: "\u8BA9\u4E8C\u8005\u76F8\u7B49\uFF08\u7F16\u8BD1\u5668\u4EE5\u58F0\u660E `to` \u4E3A\u51C6\u505A\u7AEF\u70B9\u9489\u6B7B\uFF0C\u4E0D\u4E00\u81F4\u4F1A\u8BA9\u7EC8\u503C\u4E0E\u4F60\u5199\u7684\u4E0D\u7B26\uFF09"
1156
+ });
1157
+ }
1158
+ }
1159
+ if (d.curve !== void 0 && d.spring !== void 0) {
1160
+ issues.push({
1161
+ index: i,
1162
+ code: "conflicting-easing",
1163
+ message: "\u540C\u65F6\u58F0\u660E\u4E86 `curve` \u4E0E `spring` \u2014\u2014 \u6C42\u503C\u6A21\u5F0F\u5FC5\u987B\u552F\u4E00",
1164
+ hint: "\u4E8C\u9009\u4E00\uFF1A\u8981\u7269\u7406\u624B\u611F\u7528 `spring`\uFF0C\u8981\u786E\u5B9A\u66F2\u7EBF\u7528 `curve`"
1165
+ });
1166
+ }
1167
+ if (d.spring) {
1168
+ const s = d.spring;
1169
+ const bad = !Number.isFinite(s.stiffness) || s.stiffness <= 0 || !Number.isFinite(s.damping) || s.damping < 0 || s.mass !== void 0 && (!Number.isFinite(s.mass) || s.mass <= 0);
1170
+ if (bad) {
1171
+ issues.push({
1172
+ index: i,
1173
+ code: "invalid-spring",
1174
+ message: `\u5F39\u7C27\u53C2\u6570\u975E\u6CD5\uFF1A${JSON.stringify(s)}\uFF08\u8981\u6C42 stiffness>0 \xB7 damping\u22650 \xB7 mass>0\uFF09`,
1175
+ hint: "\u7528\u9884\u8BBE\uFF08presets.easing.snappy / smooth\uFF09\u907F\u514D\u624B\u8C03\uFF1B\u5185\u6838\u6709 10s \u5B89\u5168\u4E0A\u9650\u515C\u5E95\u4F46\u90A3\u662F\u515C\u5E95\u4E0D\u662F\u8BBE\u8BA1"
1176
+ });
1177
+ }
1178
+ }
1179
+ if (d.durationMs !== void 0 && (!Number.isFinite(d.durationMs) || d.durationMs < 0)) {
1180
+ issues.push({ index: i, code: "invalid-range", message: `\`durationMs\` \u975E\u6CD5\uFF1A${d.durationMs}`, hint: "\u7ED9\u975E\u8D1F\u6BEB\u79D2\u6570" });
1181
+ }
1182
+ if (d.delayMs !== void 0 && (!Number.isFinite(d.delayMs) || d.delayMs < 0)) {
1183
+ issues.push({ index: i, code: "invalid-range", message: `\`delayMs\` \u975E\u6CD5\uFF1A${d.delayMs}`, hint: "\u7ED9\u975E\u8D1F\u6BEB\u79D2\u6570" });
1184
+ }
1185
+ if (d.scroll) {
1186
+ const { from, to } = d.scroll;
1187
+ if (!Number.isFinite(from) || !Number.isFinite(to)) {
1188
+ issues.push({
1189
+ index: i,
1190
+ code: "invalid-range",
1191
+ message: `\u6EDA\u52A8\u7A97\u53E3\u542B\u975E\u6709\u9650\u503C\uFF1A${JSON.stringify(d.scroll)}`,
1192
+ hint: "\u7A97\u53E3\u4E24\u7AEF\u90FD\u8981\u662F\u5177\u4F53\u7684\u6EDA\u52A8\u4F4D\u7F6E\uFF08px\uFF09"
1193
+ });
1194
+ } else if (to <= from) {
1195
+ issues.push({
1196
+ index: i,
1197
+ code: "invalid-range",
1198
+ message: `\u6EDA\u52A8\u7A97\u53E3\u9000\u5316\uFF08to=${to} <= from=${from}\uFF09\u2014\u2014\u8FDB\u5EA6\u5C06\u6052\u4E3A 1\uFF0C\u52A8\u753B\u4E00\u5F00\u59CB\u5C31\u505C\u5728\u7EC8\u70B9`,
1199
+ hint: "\u68C0\u67E5 from/to \u662F\u5426\u5199\u53CD\uFF1B\u7A97\u53E3\u8DE8\u5EA6\u5E94\u4E3A\u6B63\u6570\uFF08\u5982 from: 0, to: 120\uFF09"
1200
+ });
1201
+ }
1202
+ }
1203
+ checkCurveBezier(d, i, issues);
1204
+ {
1205
+ const dd = d;
1206
+ if (dd.repeat !== void 0) {
1207
+ const badNum = typeof dd.repeat === "number" && (!Number.isFinite(dd.repeat) || dd.repeat < 1);
1208
+ if (dd.repeat !== "infinite" && (typeof dd.repeat !== "number" || badNum)) {
1209
+ issues.push({
1210
+ index: i,
1211
+ code: "invalid-range",
1212
+ message: `\`repeat\` \u975E\u6CD5\uFF1A${JSON.stringify(dd.repeat)}\uFF08\u5E94 \u2265 1 \u7684\u6570\u5B57\uFF0C\u6216 'infinite'\uFF09`,
1213
+ hint: "\u4F8B\uFF1Arepeat: 3 \xB7 repeat: 'infinite'\uFF08\u547C\u5438\u706F\u7684\u5E95\u8272\u5FAA\u73AF\uFF09"
1214
+ });
1215
+ }
1216
+ if ((dd.repeat === "infinite" || typeof dd.repeat === "number" && dd.repeat > 1) && dd.scroll) {
1217
+ issues.push({
1218
+ index: i,
1219
+ code: "conflicting-easing",
1220
+ message: '`repeat` \u4E0E `scroll` \u5E76\u5B58 \u2014\u2014 \u6EDA\u52A8\u9A71\u52A8\u7684\u8FDB\u5EA6\u6765\u81EA\u4F4D\u7F6E\uFF0C\u6CA1\u6709"\u8F6E"\u7684\u6982\u5FF5',
1221
+ hint: "\u53BB\u6389 `repeat`"
1222
+ });
1223
+ }
1224
+ }
1225
+ if (dd.direction !== void 0 && dd.direction !== "normal" && dd.direction !== "alternate") {
1226
+ issues.push({
1227
+ index: i,
1228
+ code: "invalid-range",
1229
+ message: `\`direction\` \u975E\u6CD5\uFF1A${JSON.stringify(dd.direction)}`,
1230
+ hint: "yoyo \u5F80\u590D\u7528 direction: 'alternate'"
1231
+ });
1232
+ }
1233
+ }
1234
+ return;
1235
+ }
1236
+ if (d.kind === "clip" && d.scroll) {
1237
+ const { from: sfrom, to: sto } = d.scroll;
1238
+ if (!Number.isFinite(sfrom) || !Number.isFinite(sto)) {
1239
+ issues.push({ index: i, code: "invalid-range", message: "\u6EDA\u52A8\u7A97\u53E3\u542B\u975E\u6709\u9650\u6570", hint: "from/to \u90FD\u5E94\u662F\u6709\u9650 px" });
1240
+ } else if (sto <= sfrom) {
1241
+ issues.push({
1242
+ index: i,
1243
+ code: "invalid-range",
1244
+ message: `\u6EDA\u52A8\u7A97\u53E3\u9000\u5316\uFF08to=${sto} <= from=${sfrom}\uFF09\u2014\u2014\u8FDB\u5EA6\u5C06\u6052\u4E3A 1`,
1245
+ hint: "\u68C0\u67E5 from/to \u662F\u5426\u5199\u53CD\uFF1B\u7A97\u53E3\u8DE8\u5EA6\u5E94\u4E3A\u6B63\u6570"
1246
+ });
1247
+ }
1248
+ }
1249
+ if (d.kind === "clip") {
1250
+ const cd = d;
1251
+ const n = cd.to?.length ?? 0;
1252
+ if (!Array.isArray(cd.to) || n === 0) {
1253
+ issues.push({
1254
+ index: i,
1255
+ code: "empty",
1256
+ message: "`clip` \u58F0\u660E\u7684 `to` \u4E3A\u7A7A\u2014\u2014\u81F3\u5C11\u7ED9\u4E00\u4E2A\u53C2\u6570",
1257
+ hint: "inset 4 \u4E2A\uFF08top/right/bottom/left\uFF09\xB7 circle 3 \u4E2A\uFF08cx/cy/r\uFF09\xB7 polygon \u5076\u6570\u4E2A\uFF08\u226416\uFF0C\u6700\u591A 8 \u70B9\uFF09"
1258
+ });
1259
+ } else if (n > 16) {
1260
+ issues.push({
1261
+ index: i,
1262
+ code: "invalid-range",
1263
+ message: `\`clip\` \u53C2\u6570\u8FC7\u591A\uFF1A${n} \u4E2A\uFF08\u6700\u591A 16 = 8 \u4E2A\u9876\u70B9\uFF09\u2014\u2014\u5185\u6838\u53EA\u6709 16 \u4E2A\u53C2\u6570\u69FD`,
1264
+ hint: "polygon \u6700\u591A 8 \u4E2A\u9876\u70B9\uFF0816 \u4E2A\u6570\uFF09\uFF1B\u66F4\u590D\u6742\u7684\u5F62\u72B6\u8BF7\u62C6\u6210\u591A\u4E2A\u8282\u70B9\u6216\u8D70\u9003\u751F\u53E3\u767B\u8BB0"
1265
+ });
1266
+ }
1267
+ if (!Array.isArray(cd.from) || cd.from.length < n) {
1268
+ issues.push({
1269
+ index: i,
1270
+ code: "invalid-range",
1271
+ message: `\`clip\` \u7684 \`from\` \u53C2\u6570\u4E0D\u8DB3\uFF1A\u9700\u8981 ${n} \u4E2A\uFF08\u4E0E \`to\` \u5BF9\u9F50\uFF09\uFF0C\u6536\u5230 ${cd.from?.length ?? 0} \u4E2A`,
1272
+ hint: '\u5185\u6838\u6CA1\u6709"\u7F3A\u7701 = \u5F53\u524D\u503C"\u8BED\u4E49\uFF08\u4E0E\u989C\u8272\u540C\u4E00\u6761\u7EAA\u5F8B\uFF09\u2014\u2014\u8D77\u70B9\u5FC5\u987B\u663E\u5F0F\u7ED9\u51FA'
1273
+ });
1274
+ }
1275
+ const badNum = (arr) => Array.isArray(arr) && arr.some((v) => typeof v !== "number" || !Number.isFinite(v));
1276
+ if (badNum(cd.from) || badNum(cd.to)) {
1277
+ issues.push({
1278
+ index: i,
1279
+ code: "invalid-range",
1280
+ message: "`clip` \u53C2\u6570\u542B\u975E\u6709\u9650\u6570\uFF08NaN / Infinity\uFF09",
1281
+ hint: "\u53C2\u6570\u662F\u76D2\u5206\u6570\uFF080..1 \u5E38\u89C1\uFF0C\u53EF\u8D1F = \u5916\u6269\uFF09\u2014\u2014\u7ED9\u5177\u4F53\u6570\u503C"
1282
+ });
1283
+ }
1284
+ if (cd.keyframes) {
1285
+ for (const [j, seg] of cd.keyframes.entries()) {
1286
+ if (!Array.isArray(seg.to) || seg.to.length < n) {
1287
+ issues.push({
1288
+ index: i,
1289
+ code: "invalid-range",
1290
+ message: `\`clip\` \u5E8F\u5217\u7B2C ${j} \u6BB5\u53C2\u6570\u4E0D\u8DB3\uFF1A\u9700\u8981 ${n} \u4E2A\uFF0C\u6536\u5230 ${seg.to?.length ?? 0} \u4E2A`,
1291
+ hint: "\u6BCF\u6BB5 `to` \u90FD\u662F\u5B8C\u6574\u7684\u53C2\u6570\u6570\u7EC4\uFF08\u4E0E\u58F0\u660E `to` \u5BF9\u9F50\uFF09"
1292
+ });
1293
+ }
1294
+ }
1295
+ }
1296
+ checkCurveBezier(d, i, issues);
1297
+ if (d.curve !== void 0 && d.spring !== void 0) {
1298
+ issues.push({
1299
+ index: i,
1300
+ code: "conflicting-easing",
1301
+ message: "`clip` \u7684 `curve` \u4E0E `spring` \u5E76\u5B58 \u2014\u2014 \u6C42\u503C\u6A21\u5F0F\u5FC5\u987B\u552F\u4E00",
1302
+ hint: "\u4E8C\u9009\u4E00\uFF1A\u8981\u7269\u7406\u624B\u611F\u7528 `spring`\uFF0C\u8981\u786E\u5B9A\u66F2\u7EBF\u7528 `curve`"
1303
+ });
1304
+ }
1305
+ if (cd.keyframes && d.spring !== void 0) {
1306
+ issues.push({
1307
+ index: i,
1308
+ code: "conflicting-easing",
1309
+ message: "`clip` \u7684 `keyframes` \u4E0E `spring` \u5E76\u5B58 \u2014\u2014 \u6C42\u503C\u6A21\u5F0F\u5FC5\u987B\u552F\u4E00",
1310
+ hint: "\u4E8C\u9009\u4E00\uFF08\u4E0E\u6807\u91CF/\u989C\u8272\u540C\u89C4\u5219\uFF09"
1311
+ });
1312
+ }
1313
+ return;
1314
+ }
1315
+ checkCurveBezier(d, i, issues);
1316
+ {
1317
+ const dd = d;
1318
+ if (dd.repeat !== void 0) {
1319
+ const badNum = typeof dd.repeat === "number" && (!Number.isFinite(dd.repeat) || dd.repeat < 1);
1320
+ if (dd.repeat !== "infinite" && (typeof dd.repeat !== "number" || badNum)) {
1321
+ issues.push({
1322
+ index: i,
1323
+ code: "invalid-range",
1324
+ message: `\`repeat\` \u975E\u6CD5\uFF1A${JSON.stringify(dd.repeat)}\uFF08\u5E94 \u2265 1 \u7684\u6570\u5B57\uFF0C\u6216 'infinite'\uFF09`,
1325
+ hint: "\u4F8B\uFF1Arepeat: 3\uFF08\u64AD\u4E09\u904D\uFF09\xB7 repeat: 'infinite'\uFF08\u65E0\u9650\uFF09"
1326
+ });
1327
+ }
1328
+ if ((dd.repeat === "infinite" || typeof dd.repeat === "number" && dd.repeat > 1) && dd.scroll) {
1329
+ issues.push({
1330
+ index: i,
1331
+ code: "conflicting-easing",
1332
+ message: '`repeat` \u4E0E `scroll` \u5E76\u5B58 \u2014\u2014 \u6EDA\u52A8\u9A71\u52A8\u7684\u8FDB\u5EA6\u6765\u81EA\u4F4D\u7F6E\uFF0C\u6CA1\u6709"\u8F6E"\u7684\u6982\u5FF5',
1333
+ hint: "\u53BB\u6389 `repeat`\uFF08\u6EDA\u52A8\u8054\u52A8\u5929\u7136\u968F\u4F4D\u7F6E\u5F80\u590D\uFF09"
1334
+ });
1335
+ }
1336
+ }
1337
+ if (dd.direction !== void 0 && dd.direction !== "normal" && dd.direction !== "alternate") {
1338
+ issues.push({
1339
+ index: i,
1340
+ code: "invalid-range",
1341
+ message: `\`direction\` \u975E\u6CD5\uFF1A${JSON.stringify(dd.direction)}\uFF08\u5E94 'normal' / 'alternate'\uFF09`,
1342
+ hint: "yoyo \u5F80\u590D\u7528 direction: 'alternate'"
1343
+ });
1344
+ }
1345
+ }
1346
+ const dup = decls.slice(0, i).find((p) => p.kind === d.kind);
1347
+ if (dup) {
1348
+ issues.push({
1349
+ index: i,
1350
+ code: "duplicate-kind",
1351
+ message: `\u540C\u4E00\u6279\u6B21\u91CC \`${d.kind}\` \u51FA\u73B0\u4E86\u591A\u6B21\uFF08\u5185\u6838\u5BF9\u540C (\u8282\u70B9,\u5C5E\u6027) \u662F**\u66FF\u6362**\u8BED\u4E49 \u21D2 \u540E\u8005\u4F1A\u9759\u9ED8\u66FF\u6362\u524D\u8005\uFF09`,
1352
+ hint: "\u591A\u6BB5\u5E8F\u5217\u8BF7\u7528 `keyframes`\uFF08\u4E00\u6761\u52A8\u753B\u5185\u5206\u6BB5\uFF0C\u5185\u6838 AnimMode::Keyframes\uFF09\uFF1B\u6216\u62C6\u6210\u4E24\u6B21\u8C03\u7528\uFF1B\u6216\u6539\u7528\u4E0D\u540C\u5C5E\u6027\u7EC4\u5408\uFF08\u5982 scale + opacity \u540C\u65F6\u8FDB\u884C\uFF09"
1353
+ });
1354
+ }
1355
+ if (d.keyframes) {
1356
+ const kf = d.keyframes;
1357
+ if (kf.length === 0) {
1358
+ issues.push({
1359
+ index: i,
1360
+ code: "empty",
1361
+ message: "`keyframes` \u4E3A\u7A7A\u6570\u7EC4\uFF08\u5E8F\u5217\u81F3\u5C11\u8981\u4E00\u6BB5\uFF1B\u5355\u6BB5\u8BF7\u76F4\u63A5\u7528 `curve`\uFF09",
1362
+ hint: "\u53BB\u6389 `keyframes` \u7528\u5355\u6BB5\u58F0\u660E\uFF0C\u6216\u8865\u4E0A\u81F3\u5C11\u4E00\u6BB5 `{ to, durationMs }`"
1363
+ });
1364
+ }
1365
+ const sum = kf.reduce((acc, s) => acc + (Number.isFinite(s.durationMs) ? s.durationMs : 0), 0);
1366
+ kf.forEach((s, j) => {
1367
+ if (!Number.isFinite(s.to)) {
1368
+ issues.push({ index: i, code: "invalid-range", message: `\u5E8F\u5217\u7B2C ${j} \u6BB5 \`to\` \u975E\u6709\u9650\u6570`, hint: "\u7ED9\u5177\u4F53\u6570\u503C" });
1369
+ }
1370
+ if (!Number.isFinite(s.durationMs) || s.durationMs < 0) {
1371
+ issues.push({
1372
+ index: i,
1373
+ code: "invalid-range",
1374
+ message: `\u5E8F\u5217\u7B2C ${j} \u6BB5 \`durationMs\` \u975E\u6CD5\uFF1A${s.durationMs}`,
1375
+ hint: "\u7ED9\u975E\u8D1F\u6BEB\u79D2\u6570\uFF080 = \u8BE5\u6BB5\u77AC\u53D8\uFF0C\u5408\u6CD5\u4F46\u901A\u5E38\u4E0D\u662F\u672C\u610F\uFF09"
1376
+ });
1377
+ }
1378
+ if (s.curve !== void 0 && !(s.curve in CURVE_ID)) {
1379
+ issues.push({ index: i, code: "invalid-range", message: `\u5E8F\u5217\u7B2C ${j} \u6BB5\u66F2\u7EBF\u672A\u77E5\uFF1A${s.curve}`, hint: "\u7528 Curve \u5C01\u95ED\u96C6\u91CC\u7684\u540D\u5B57" });
1380
+ }
1381
+ });
1382
+ if (sum <= 0) {
1383
+ issues.push({
1384
+ index: i,
1385
+ code: "invalid-range",
1386
+ message: "\u5E8F\u5217\u603B\u65F6\u957F\u4E3A 0 \u2014\u2014 \u52A8\u753B\u4F1A**\u77AC\u95F4\u8DF3\u5230\u672B\u6BB5\u7EC8\u70B9**",
1387
+ hint: '\u81F3\u5C11\u7ED9\u4E00\u6BB5\u6B63\u65F6\u957F\uFF1B\u96F6\u65F6\u957F\u5E8F\u5217\u5728\u771F\u673A\u4E0A\u770B\u8D77\u6765\u5C31\u662F"\u6CA1\u505A\u52A8\u753B"'
1388
+ });
1389
+ }
1390
+ if (d.spring !== void 0) {
1391
+ issues.push({
1392
+ index: i,
1393
+ code: "conflicting-easing",
1394
+ message: "`keyframes` \u4E0E `spring` \u5E76\u5B58 \u2014\u2014 \u6C42\u503C\u6A21\u5F0F\u5FC5\u987B\u552F\u4E00",
1395
+ hint: "\u5E8F\u5217\u91CC\u8981\u5F39\u6027\u624B\u611F\uFF0C\u628A\u67D0\u4E00\u6BB5\u7528\u66F2\u7EBF\u8FD1\u4F3C\uFF08\u5982 springApprox\uFF09\uFF0C\u6216\u6574\u6761\u6539\u7528 spring"
1396
+ });
1397
+ }
1398
+ if (d.curve !== void 0) {
1399
+ issues.push({
1400
+ index: i,
1401
+ code: "conflicting-easing",
1402
+ message: "`keyframes` \u4E0E `curve` \u5E76\u5B58 \u2014\u2014 \u6BB5\u5185\u66F2\u7EBF\u7531\u6BCF\u6BB5\u81EA\u5DF1\u7684 `curve` \u51B3\u5B9A",
1403
+ hint: "\u5220\u6389\u5916\u5C42\u7684 `curve`\uFF08\u5B83\u53EA\u5BF9\u5355\u6BB5\u6A21\u5F0F\u6709\u610F\u4E49\uFF09"
1404
+ });
1405
+ }
1406
+ const last = kf[kf.length - 1];
1407
+ if (last && Number.isFinite(last.to) && Number.isFinite(d.to) && last.to !== d.to) {
1408
+ issues.push({
1409
+ index: i,
1410
+ code: "invalid-range",
1411
+ message: `\u672B\u6BB5 \`to\`(${last.to}) \u4E0E\u58F0\u660E \`to\`(${d.to}) \u4E0D\u4E00\u81F4 \u2014\u2014 \u4E24\u5904\u90FD\u63CF\u8FF0"\u7EC8\u70B9"`,
1412
+ hint: "\u8BA9\u4E8C\u8005\u76F8\u7B49\uFF08\u7F16\u8BD1\u5668\u4EE5\u58F0\u660E `to` \u4E3A\u51C6\u505A\u7AEF\u70B9\u9489\u6B7B\uFF0C\u4E0D\u4E00\u81F4\u4F1A\u8BA9\u7EC8\u503C\u4E0E\u4F60\u5199\u7684\u4E0D\u7B26\uFF09"
1413
+ });
1414
+ }
1415
+ }
1416
+ if (!isComposited(d.kind) && !isTickOnly(d.kind)) {
1417
+ issues.push({
1418
+ index: i,
1419
+ code: "non-composited",
1420
+ message: `\`${d.kind}\` \u4E0D\u662F\u5408\u6210\u5C5E\u6027 \u2014\u2014 \u4F1A\u89E6\u53D1\u5E73\u53F0\u5E03\u5C40\u94FE\u8DEF\uFF08requestLayout / \u5E03\u5C40\u91CD\u7B97\uFF09\uFF0C\u5E73\u53F0\u6E32\u67D3\u7EBF\u7A0B\u7684\u5F02\u6B65\u7EA2\u5229**\u5B8C\u5168\u5931\u6548**`,
1421
+ hint: '\u6539\u7528\u5408\u6210\u5C5E\u6027\uFF1A\u4F4D\u79FB\u7528 translateX/Y\u3001\u7F29\u653E\u7528 scale\u3001\u65CB\u8F6C\u7528 rotate\u3001\u900F\u660E\u5EA6\u7528 opacity\uFF1B\u5982\u679C\u8981\u52A8\u7684\u662F"\u5E03\u5C40\u8BA9\u4F4D"\uFF0C\u8BF7\u8D70 FLIP\uFF08presets.list.shift\uFF09'
1422
+ });
1423
+ }
1424
+ if (!Number.isFinite(d.to)) {
1425
+ issues.push({ index: i, code: "invalid-range", message: "`to` \u4E0D\u662F\u6709\u9650\u6570", hint: "\u7ED9\u4E00\u4E2A\u5177\u4F53\u7684\u6570\u503C\u76EE\u6807" });
1426
+ }
1427
+ if (d.from !== void 0 && !Number.isFinite(d.from)) {
1428
+ issues.push({ index: i, code: "invalid-range", message: "`from` \u4E0D\u662F\u6709\u9650\u6570", hint: "\u7701\u7565 `from` \u8BA9\u5185\u6838\u53D6\u8282\u70B9\u5F53\u524D\u503C" });
1429
+ }
1430
+ const isSpring = d.spring !== void 0;
1431
+ if (isSpring && d.curve !== void 0) {
1432
+ issues.push({
1433
+ index: i,
1434
+ code: "conflicting-easing",
1435
+ message: "\u540C\u65F6\u58F0\u660E\u4E86 `curve` \u4E0E `spring` \u2014\u2014 \u6C42\u503C\u6A21\u5F0F\u5FC5\u987B\u552F\u4E00",
1436
+ hint: "\u4E8C\u9009\u4E00\uFF1A\u8981\u7269\u7406\u624B\u611F\u7528 `spring`\uFF0C\u8981\u786E\u5B9A\u66F2\u7EBF\u7528 `curve`"
1437
+ });
1438
+ }
1439
+ if (d.durationMs !== void 0 && (!Number.isFinite(d.durationMs) || d.durationMs < 0)) {
1440
+ issues.push({ index: i, code: "invalid-range", message: `\`durationMs\` \u975E\u6CD5\uFF1A${d.durationMs}`, hint: "\u7ED9\u975E\u8D1F\u6BEB\u79D2\u6570" });
1441
+ }
1442
+ if (d.delayMs !== void 0 && (!Number.isFinite(d.delayMs) || d.delayMs < 0)) {
1443
+ issues.push({ index: i, code: "invalid-range", message: `\`delayMs\` \u975E\u6CD5\uFF1A${d.delayMs}`, hint: "\u7ED9\u975E\u8D1F\u6BEB\u79D2\u6570" });
1444
+ }
1445
+ if (isSpring) {
1446
+ const s = d.spring;
1447
+ const bad = !Number.isFinite(s.stiffness) || s.stiffness <= 0 || !Number.isFinite(s.damping) || s.damping < 0 || s.mass !== void 0 && (!Number.isFinite(s.mass) || s.mass <= 0);
1448
+ if (bad) {
1449
+ issues.push({
1450
+ index: i,
1451
+ code: "invalid-spring",
1452
+ message: `\u5F39\u7C27\u53C2\u6570\u975E\u6CD5\uFF1A${JSON.stringify(s)}\uFF08\u8981\u6C42 stiffness>0 \xB7 damping\u22650 \xB7 mass>0\uFF09`,
1453
+ hint: "\u7528\u9884\u8BBE\uFF08presets.easing.snappy / smooth\uFF09\u907F\u514D\u624B\u8C03\uFF1B\u5185\u6838\u6709 10s \u5B89\u5168\u4E0A\u9650\u515C\u5E95\u4F46\u90A3\u662F\u515C\u5E95\u4E0D\u662F\u8BBE\u8BA1"
1454
+ });
1455
+ }
1456
+ }
1457
+ if (d.scroll) {
1458
+ const { from, to } = d.scroll;
1459
+ if (!Number.isFinite(from) || !Number.isFinite(to)) {
1460
+ issues.push({
1461
+ index: i,
1462
+ code: "invalid-range",
1463
+ message: `\u6EDA\u52A8\u7A97\u53E3\u542B\u975E\u6709\u9650\u503C\uFF1A${JSON.stringify(d.scroll)}`,
1464
+ hint: "\u7A97\u53E3\u4E24\u7AEF\u90FD\u8981\u662F\u5177\u4F53\u7684\u6EDA\u52A8\u4F4D\u7F6E\uFF08px\uFF09"
1465
+ });
1466
+ } else if (to <= from) {
1467
+ issues.push({
1468
+ index: i,
1469
+ code: "invalid-range",
1470
+ message: `\u6EDA\u52A8\u7A97\u53E3\u9000\u5316\uFF08to=${to} <= from=${from}\uFF09\u2014\u2014\u8FDB\u5EA6\u5C06\u6052\u4E3A 1\uFF0C\u52A8\u753B\u4E00\u5F00\u59CB\u5C31\u505C\u5728\u7EC8\u70B9`,
1471
+ hint: "\u68C0\u67E5 from/to \u662F\u5426\u5199\u53CD\uFF1B\u7A97\u53E3\u8DE8\u5EA6\u5E94\u4E3A\u6B63\u6570\uFF08\u5982 from: 0, to: 120\uFF09"
1472
+ });
1473
+ }
1474
+ }
1475
+ if (d.scroll && d.spring) {
1476
+ issues.push({
1477
+ index: i,
1478
+ code: "conflicting-easing",
1479
+ message: "\u6EDA\u52A8\u9A71\u52A8\u4E0E\u5F39\u7C27\u7269\u7406\u5E76\u5B58 \u2014\u2014 \u5F39\u7C27\u7684\u8FDB\u5EA6\u53C2\u6570\u5728\u6EDA\u52A8\u9A71\u52A8\u4E0B\u65E0\u610F\u4E49\uFF08\u5185\u6838\u6309\u4F4D\u7F6E\u6362\u7B97\u8FDB\u5EA6\u540E\u8D70\u66F2\u7EBF\u6C42\u503C\uFF09",
1480
+ hint: "\u6EDA\u52A8\u8054\u52A8\u8BF7\u7528 `curve`\uFF08\u5982 easeOut / linear\uFF09\uFF1B\u5F39\u7C27\u7559\u7ED9\u65F6\u95F4\u9A71\u52A8\u7684\u52A8\u753B"
1481
+ });
1482
+ }
1483
+ });
1484
+ return issues;
1485
+ }
1486
+ function compileAnimations(decls, targets, opts) {
1487
+ const issues = validateAnimations(decls);
1488
+ if (issues.length > 0) {
1489
+ const detail = issues.map((i) => `[${i.code}] #${i.index} ${i.message} \u2192 ${i.hint}`).join("; ");
1490
+ throw new Error(
1491
+ `Morpheus \u52A8\u753B\u58F0\u660E\u6821\u9A8C\u5931\u8D25\uFF1A${detail}
1492
+ \u21D2 \u82E5\u5C01\u95ED\u96C6\u786E\u5B9E\u8868\u8FBE\u4E0D\u4E86\uFF0C\u8D70**\u663E\u5F0F\u9003\u751F\u53E3**\uFF08\`escapes.register({kind, detail, reason, behaviorRisk})\`\uFF09\u2014\u2014\u53EF\u7528\u4F46\u5FC5\u987B\u767B\u8BB0\u5E76\u63A5\u53D7 degraded \u98CE\u9669\uFF08Morpheus \xA74.2\uFF09\uFF0C\u4E0D\u8981\u7ED5\u8FC7\u6846\u67B6\u3002`
1493
+ );
1494
+ }
1495
+ const anims = decls.flatMap((d) => compileOne(d, targets));
1496
+ opts?.escapes?.noteDeclarative(anims.length);
1497
+ const kindNames = new Set(decls.map((d) => d.kind));
1498
+ const composited = [...kindNames].every((k) => isComposited(k));
1499
+ return {
1500
+ anims,
1501
+ composited,
1502
+ // ★颜色两类(color / textColor)都是 paint-only 非合成 ⇒ 如实列进清单
1503
+ nonComposited: composited ? [] : [...kindNames].filter((k) => !isComposited(k))
1504
+ };
1505
+ }
1506
+ function isColorDecl(d) {
1507
+ return d.kind === "color" || d.kind === "textColor";
1508
+ }
1509
+ function isClipDecl(d) {
1510
+ return d.kind === "clip";
1511
+ }
1512
+ function compileOne(d, targets) {
1513
+ const easing2 = resolveEasing(d);
1514
+ if (isClipDecl(d)) {
1515
+ const n = Math.min(16, d.to.length);
1516
+ if (n === 0) {
1517
+ throw new Error("\u88C1\u526A\u58F0\u660E `to` \u4E3A\u7A7A\u2014\u2014\u81F3\u5C11\u7ED9\u4E00\u4E2A\u53C2\u6570\uFF08inset\u22654 / circle\u22653 / polygon \u5076\u6570\u4E2A\uFF09");
1518
+ }
1519
+ const from = d.from;
1520
+ if (from.length < n) {
1521
+ throw new Error(
1522
+ `\u88C1\u526A\u58F0\u660E \`from\` \u53C2\u6570\u4E0D\u8DB3\uFF1A\u9700\u8981 ${n} \u4E2A\uFF08\u4E0E \`to\` \u5BF9\u9F50\uFF09\uFF0C\u6536\u5230 ${from.length} \u4E2A\uFF08\u5185\u6838\u6CA1\u6709"\u7F3A\u7701 = \u5F53\u524D\u503C"\u8BED\u4E49\u2014\u2014\u8D77\u70B9\u5FC5\u987B\u663E\u5F0F\u7ED9\u51FA\uFF09`
1523
+ );
1524
+ }
1525
+ const kf = d.keyframes?.map((seg) => ({ seg, to: seg.to.length >= n ? seg.to : seg.to }));
1526
+ const mkClip = (slot) => ({
1527
+ nodeId: targets.nodeId,
1528
+ // ★通道编号 = 15 + 槽位(契约:CLIP0 = 15,见 types.ts AnimKind)
1529
+ kind: 15 + slot,
1530
+ curve: easing2.curve,
1531
+ clipSlot: slot,
1532
+ ...d.curveBezier ? { curveBezier: d.curveBezier } : {},
1533
+ from: from[slot] ?? 0,
1534
+ to: d.to[slot] ?? 0,
1535
+ durMs: easing2.durMs,
1536
+ delayMs: d.delayMs ?? 0,
1537
+ // ★★滚动驱动(长卷探索补):与标量/颜色同字段同语义(内核 `start_scroll` 支持任意 kind)
1538
+ drive: d.drive === "progress" ? 1 : 0,
1539
+ ...d.scroll ? { scrollFrom: d.scroll.from, scrollTo: d.scroll.to } : {},
1540
+ takeover: d.takeover !== false,
1541
+ ...d.spring ? { spring: { stiffness: d.spring.stiffness, damping: d.spring.damping, mass: d.spring.mass ?? 1 } } : {},
1542
+ ...kf ? {
1543
+ keyframes: kf.map(({ seg }) => ({
1544
+ to: seg.to[slot] ?? 0,
1545
+ durMs: seg.durationMs,
1546
+ curve: CURVE_ID[seg.curve ?? "easeOut"]
1547
+ }))
1548
+ } : {},
1549
+ ...d.repeat !== void 0 ? {
1550
+ repeat: d.repeat === "infinite" ? -1 : d.repeat,
1551
+ ...d.direction === "alternate" ? { alternate: true } : {}
1552
+ } : {}
1553
+ });
1554
+ const out = [];
1555
+ for (let slot = 0; slot < n; slot++) out.push(mkClip(slot));
1556
+ return out;
1557
+ }
1558
+ if (isColorDecl(d)) {
1559
+ const from = parseColorToChannels(d.from);
1560
+ const to = parseColorToChannels(d.to);
1561
+ const base = d.kind === "color" ? [AnimKind.COLOR_R, AnimKind.COLOR_G, AnimKind.COLOR_B, AnimKind.COLOR_A] : [AnimKind.TEXT_COLOR_R, AnimKind.TEXT_COLOR_G, AnimKind.TEXT_COLOR_B, AnimKind.TEXT_COLOR_A];
1562
+ const kf = d.keyframes?.map((seg) => {
1563
+ const ch = parseColorToChannels(seg.to);
1564
+ return { ch, durMs: seg.durationMs, curve: CURVE_ID[seg.curve ?? "easeOut"] };
1565
+ });
1566
+ const mc = d;
1567
+ const mk = (kind, idx) => {
1568
+ const f = [from.r, from.g, from.b, from.a][idx];
1569
+ const t = [to.r, to.g, to.b, to.a][idx];
1570
+ return {
1571
+ nodeId: targets.nodeId,
1572
+ kind,
1573
+ curve: easing2.curve,
1574
+ // ★自定义贝塞尔:**展开到四条通道**(同一控制点 ⇒ 同一条曲线——颜色不能分通道变缓动)
1575
+ ...mc.curveBezier ? { curveBezier: mc.curveBezier } : {},
1576
+ from: f,
1577
+ to: t,
1578
+ durMs: easing2.durMs,
1579
+ delayMs: d.delayMs ?? 0,
1580
+ drive: d.drive === "progress" ? 1 : 0,
1581
+ takeover: d.takeover !== false,
1582
+ ...d.spring ? { spring: { stiffness: d.spring.stiffness, damping: d.spring.damping, mass: d.spring.mass ?? 1 } } : {},
1583
+ ...kf ? {
1584
+ keyframes: kf.map(({ ch, durMs, curve }) => ({
1585
+ to: [ch.r, ch.g, ch.b, ch.a][idx],
1586
+ durMs,
1587
+ curve
1588
+ }))
1589
+ } : {},
1590
+ ...d.scroll ? { scrollFrom: d.scroll.from, scrollTo: d.scroll.to } : {},
1591
+ // ★A2 循环:四条通道**都要带**(少一条通道不循环 = 颜色分叉——经典静默缺陷)
1592
+ ...d.repeat !== void 0 ? {
1593
+ repeat: d.repeat === "infinite" ? -1 : d.repeat,
1594
+ ...d.direction === "alternate" ? { alternate: true } : {}
1595
+ } : {}
1596
+ };
1597
+ };
1598
+ return [mk(base[0], 0), mk(base[1], 1), mk(base[2], 2), mk(base[3], 3)];
1599
+ }
1600
+ return [
1601
+ {
1602
+ nodeId: targets.nodeId,
1603
+ kind: ANIM_KIND_ID[d.kind],
1604
+ curve: easing2.curve,
1605
+ // ★自定义贝塞尔(给了它内核求值优先于 `curve` id——见内核 `Anim::curve_at`)
1606
+ ...d.curveBezier ? { curveBezier: d.curveBezier } : {},
1607
+ from: d.from ?? 0,
1608
+ to: d.to,
1609
+ durMs: easing2.durMs,
1610
+ delayMs: d.delayMs ?? 0,
1611
+ drive: d.drive === "progress" ? 1 : 0,
1612
+ takeover: d.takeover !== false,
1613
+ ...d.spring ? { spring: { stiffness: d.spring.stiffness, damping: d.spring.damping, mass: d.spring.mass ?? 1 } } : {},
1614
+ // ★MA6:序列(每段曲线在编译期落定;durMs = 各段之和 ⇒ 内核用它做时间→进度换算)
1615
+ ...d.keyframes ? {
1616
+ keyframes: d.keyframes.map((s) => ({
1617
+ to: s.to,
1618
+ durMs: s.durationMs,
1619
+ curve: CURVE_ID[s.curve ?? "easeOut"]
1620
+ }))
1621
+ } : {},
1622
+ // ★MA5:滚动窗口(内核据 scrollTo > scrollFrom 判定为滚动驱动)
1623
+ ...d.scroll ? { scrollFrom: d.scroll.from, scrollTo: d.scroll.to } : {},
1624
+ // ★A2 循环(`'infinite'` → -1 哨兵;`direction` 只在有 repeat 时下发——不污染线格式)
1625
+ ...d.repeat !== void 0 ? {
1626
+ repeat: d.repeat === "infinite" ? -1 : d.repeat,
1627
+ ...d.direction === "alternate" ? { alternate: true } : {}
1628
+ } : {}
1629
+ }
1630
+ ];
1631
+ }
1632
+ function resolveEasing(d) {
1633
+ if (d.keyframes) {
1634
+ const sum = d.keyframes.reduce((acc, s) => acc + s.durationMs, 0);
1635
+ return { curve: CURVE_ID[d.curve ?? "easeOut"], durMs: d.durationMs ?? sum };
1636
+ }
1637
+ if (d.spring) {
1638
+ return { curve: CURVE_ID.easeOut, durMs: d.durationMs ?? 1e3 };
1639
+ }
1640
+ return { curve: CURVE_ID[d.curve ?? "easeOut"], durMs: d.durationMs ?? 300 };
1641
+ }
1642
+ var easing = {
1643
+ /** 快速、微回弹(对齐 iOS `.snappy`) */
1644
+ snappy: { stiffness: 320, damping: 30, mass: 1 },
1645
+ /** 顺滑、几乎无回弹(对齐 iOS `.smooth`) */
1646
+ smooth: { stiffness: 180, damping: 26, mass: 1 }
1647
+ };
1648
+ var FONT_5X7 = {
1649
+ A: [".###.", "#...#", "#...#", "#####", "#...#", "#...#", "#...#"],
1650
+ B: ["####.", "#...#", "#...#", "####.", "#...#", "#...#", "####."],
1651
+ C: [".###.", "#...#", "#....", "#....", "#....", "#...#", ".###."],
1652
+ D: ["####.", "#...#", "#...#", "#...#", "#...#", "#...#", "####."],
1653
+ E: ["#####", "#....", "#....", "####.", "#....", "#....", "#####"],
1654
+ F: ["#####", "#....", "#....", "####.", "#....", "#....", "#...."],
1655
+ G: [".###.", "#...#", "#....", "#.###", "#...#", "#...#", ".###."],
1656
+ H: ["#...#", "#...#", "#...#", "#####", "#...#", "#...#", "#...#"],
1657
+ I: ["#####", "..#..", "..#..", "..#..", "..#..", "..#..", "#####"],
1658
+ J: ["..###", "...#.", "...#.", "...#.", "...#.", "#..#.", ".##.."],
1659
+ K: ["#...#", "#..#.", "#.#..", "##...", "#.#..", "#..#.", "#...#"],
1660
+ L: ["#....", "#....", "#....", "#....", "#....", "#....", "#####"],
1661
+ M: ["#...#", "##.##", "#.#.#", "#.#.#", "#...#", "#...#", "#...#"],
1662
+ N: ["#...#", "##..#", "#.#.#", "#..##", "#...#", "#...#", "#...#"],
1663
+ O: [".###.", "#...#", "#...#", "#...#", "#...#", "#...#", ".###."],
1664
+ P: ["####.", "#...#", "#...#", "####.", "#....", "#....", "#...."],
1665
+ Q: [".###.", "#...#", "#...#", "#...#", "#.#.#", "#..#.", ".##.#"],
1666
+ R: ["####.", "#...#", "#...#", "####.", "#.#..", "#..#.", "#...#"],
1667
+ S: [".####", "#....", "#....", ".###.", "....#", "....#", "####."],
1668
+ T: ["#####", "..#..", "..#..", "..#..", "..#..", "..#..", "..#.."],
1669
+ U: ["#...#", "#...#", "#...#", "#...#", "#...#", "#...#", ".###."],
1670
+ V: ["#...#", "#...#", "#...#", "#...#", "#...#", ".#.#.", "..#.."],
1671
+ W: ["#...#", "#...#", "#...#", "#.#.#", "#.#.#", "##.##", "#...#"],
1672
+ X: ["#...#", "#...#", ".#.#.", "..#..", ".#.#.", "#...#", "#...#"],
1673
+ Y: ["#...#", "#...#", ".#.#.", "..#..", "..#..", "..#..", "..#.."],
1674
+ Z: ["#####", "....#", "...#.", "..#..", ".#...", "#....", "#####"],
1675
+ "0": [".###.", "#...#", "#..##", "#.#.#", "##..#", "#...#", ".###."],
1676
+ "1": ["..#..", ".##..", "..#..", "..#..", "..#..", "..#..", ".###."],
1677
+ "2": [".###.", "#...#", "....#", "...#.", "..#..", ".#...", "#####"],
1678
+ "3": ["#####", "...#.", "..#..", "...#.", "....#", "#...#", ".###."],
1679
+ "4": ["...#.", "..##.", ".#.#.", "#..#.", "#####", "...#.", "...#."],
1680
+ "5": ["#####", "#....", "####.", "....#", "....#", "#...#", ".###."],
1681
+ "6": ["..##.", ".#...", "#....", "####.", "#...#", "#...#", ".###."],
1682
+ "7": ["#####", "....#", "...#.", "..#..", ".#...", ".#...", ".#..."],
1683
+ "8": [".###.", "#...#", "#...#", ".###.", "#...#", "#...#", ".###."],
1684
+ "9": [".###.", "#...#", "#...#", ".####", "....#", "...#.", ".##.."],
1685
+ " ": [".....", ".....", ".....", ".....", ".....", ".....", "....."],
1686
+ "-": [".....", ".....", ".....", "#####", ".....", ".....", "....."],
1687
+ "!": ["..#..", "..#..", "..#..", "..#..", "..#..", ".....", "..#.."],
1688
+ "?": [".###.", "#...#", "....#", "...#.", "..#..", ".....", "..#.."]
1689
+ };
1690
+ var GLYPH_ADVANCE = 6;
1691
+ var GLYPH_HEIGHT = 7;
1692
+ var LINE_GAP = 1;
1693
+ function textBitmap(text) {
1694
+ const up = text.toUpperCase();
1695
+ const lines = up.split("\n");
1696
+ const lineWidth = (s) => (Math.max(s.length, 1) - 1) * GLYPH_ADVANCE + 5;
1697
+ const widths = lines.map(lineWidth);
1698
+ const width = Math.max(...widths, 5);
1699
+ const height = lines.length * GLYPH_HEIGHT + (lines.length - 1) * LINE_GAP;
1700
+ const lit = [];
1701
+ lines.forEach((line, li) => {
1702
+ const xBase = Math.floor((width - widths[li]) / 2);
1703
+ const yBase = li * (GLYPH_HEIGHT + LINE_GAP);
1704
+ for (let ci = 0; ci < line.length; ci++) {
1705
+ const ch = line[ci];
1706
+ const glyph = FONT_5X7[ch] ?? FONT_5X7[" "];
1707
+ const x0 = xBase + ci * GLYPH_ADVANCE;
1708
+ for (let y = 0; y < GLYPH_HEIGHT; y++) {
1709
+ const row = glyph[y] ?? ".....";
1710
+ for (let x = 0; x < 5; x++) {
1711
+ if (row[x] === "#") lit.push({ x: x0 + x, y: yBase + y });
1712
+ }
1713
+ }
1714
+ }
1715
+ });
1716
+ return { width, height, lit, text: up };
1717
+ }
1718
+ function staggerRanks(n, cols, order = "index") {
1719
+ const ranks = new Array(n);
1720
+ if (n === 0) return ranks;
1721
+ const c = Math.max(1, cols);
1722
+ const rowOf = (i) => Math.floor(i / c);
1723
+ const colOf = (i) => i % c;
1724
+ switch (order) {
1725
+ case "index":
1726
+ for (let i = 0; i < n; i++) ranks[i] = i;
1727
+ return ranks;
1728
+ case "diagonal": {
1729
+ const keyed = Array.from({ length: n }, (_, i) => ({ i, key: rowOf(i) + colOf(i) }));
1730
+ keyed.sort((a, b) => a.key - b.key || a.i - b.i);
1731
+ keyed.forEach((e, k) => ranks[e.i] = k);
1732
+ return ranks;
1733
+ }
1734
+ case "serpentine": {
1735
+ const rows = Math.ceil(n / c);
1736
+ let k = 0;
1737
+ for (let r = 0; r < rows; r++) {
1738
+ const lo = r * c;
1739
+ const hi = Math.min(lo + c, n);
1740
+ if (r % 2 === 0) {
1741
+ for (let i = lo; i < hi; i++) ranks[i] = k++;
1742
+ } else {
1743
+ for (let i = hi - 1; i >= lo; i--) ranks[i] = k++;
1744
+ }
1745
+ }
1746
+ return ranks;
1747
+ }
1748
+ case "radialOut":
1749
+ case "radialIn": {
1750
+ const rows = Math.ceil(n / c);
1751
+ const cx = (c - 1) / 2;
1752
+ const cy = (rows - 1) / 2;
1753
+ const keyed = Array.from({ length: n }, (_, i) => ({
1754
+ i,
1755
+ key: Math.hypot(colOf(i) - cx, rowOf(i) - cy)
1756
+ }));
1757
+ keyed.sort(
1758
+ (a, b) => order === "radialOut" ? a.key - b.key || a.i - b.i : b.key - a.key || a.i - b.i
1759
+ );
1760
+ keyed.forEach((e, k) => ranks[e.i] = k);
1761
+ return ranks;
1762
+ }
1763
+ case "alternate": {
1764
+ const half = Math.ceil(n / 2);
1765
+ let even = 0;
1766
+ let odd = half;
1767
+ for (let i = 0; i < n; i++) ranks[i] = i % 2 === 0 ? even++ : odd++;
1768
+ return ranks;
1769
+ }
1770
+ }
1771
+ }
1772
+ function compileChoreography(spec) {
1773
+ const { ids, canvas, make } = spec;
1774
+ const n = ids.length;
1775
+ const c = Math.max(1, canvas.cols);
1776
+ const ranks = staggerRanks(n, c, spec.order ?? "index");
1777
+ const staggerMs = spec.staggerMs ?? 0;
1778
+ const out = [];
1779
+ for (let i = 0; i < n; i++) {
1780
+ const nth = ranks[i] ?? i;
1781
+ const delayMs = nth * staggerMs;
1782
+ const ctx = {
1783
+ i,
1784
+ n,
1785
+ row: Math.floor(i / c),
1786
+ col: i % c,
1787
+ nth,
1788
+ delayMs,
1789
+ at: canvas.centers?.[i] ?? { x: 0, y: 0 }
1790
+ };
1791
+ const decls = make(ctx).map((d) => ({ ...d, delayMs: (d.delayMs ?? 0) + delayMs }));
1792
+ try {
1793
+ out.push(...compileAnimations(decls, { nodeId: ids[i] }).anims);
1794
+ } catch (e) {
1795
+ throw new Error(`\u7F16\u6392\u7B2C ${i} \u7247\uFF08nodeId=${ids[i]}\uFF09\u7F16\u8BD1\u5931\u8D25\uFF1A${e.message}`);
1796
+ }
1797
+ }
1798
+ return out;
1799
+ }
1800
+ function hash01(seed) {
1801
+ const s = Math.sin(seed * 127.1 + 311.7) * 43758.5453;
1802
+ return s - Math.floor(s);
1803
+ }
1804
+ function viewCenter(view) {
1805
+ return { x: view.width / 2, y: view.height / 2 };
1806
+ }
1807
+ function needCenters(canvas, who) {
1808
+ const cs = canvas.centers;
1809
+ if (!cs || cs.length === 0) {
1810
+ throw new Error(
1811
+ `${who} \u9700\u8981 \`canvas.centers\`\uFF08\u6BCF\u7247\u5F53\u524D\u4E2D\u5FC3\uFF09\u2014\u2014\u4F4D\u79FB\u91CF = \u76EE\u6807 \u2212 \u5F53\u524D\uFF0C\u5F15\u64CE\u4E0D\u731C\u5F53\u524D\u5728\u54EA\u3002\u8BF7\u5148\u4ECE\u5185\u6838\u51E0\u4F55\u8BFB\u56DE\uFF08\u5BBF\u4E3B \`rects()\`\uFF09\u518D\u4F20\u5165\u3002`
1812
+ );
1813
+ }
1814
+ return cs;
1815
+ }
1816
+ var specOf = (scene) => (make) => ({
1817
+ ids: scene.ids,
1818
+ canvas: scene.canvas,
1819
+ order: scene.order,
1820
+ staggerMs: scene.staggerMs,
1821
+ make
1822
+ });
1823
+ var choreograph = {
1824
+ /**
1825
+ * **波浪**:每片从 `from` 偏移处弹回布局位(spring 物理),随相位错峰扫过。
1826
+ * 方向由 `dir` 决定(`down` = 从上方向下扫;`up` = 从下方向上托)。
1827
+ */
1828
+ wave(scene) {
1829
+ const mag = scene.magnitude ?? 260;
1830
+ const from = scene.dir === "up" ? mag : -mag;
1831
+ const spring = scene.spring ?? easing.smooth;
1832
+ const fadeMs = scene.fadeMs ?? 220;
1833
+ return compileChoreography(
1834
+ specOf(scene)(() => [
1835
+ { kind: "translateY", from, to: 0, spring },
1836
+ { kind: "opacity", from: 0, to: 1, curve: "easeOut", durationMs: fadeMs }
1837
+ ])
1838
+ );
1839
+ },
1840
+ /**
1841
+ * **涟漪**:从中心向外逐圈脉冲——scale(放大回弹)+ opacity(压暗回暖),
1842
+ * `radialOut` 相位天然形成"一圈圈推出去"的节奏。
1843
+ */
1844
+ ripple(scene) {
1845
+ const peak = scene.peak ?? 1.35;
1846
+ const dip = scene.dip ?? 0.5;
1847
+ const half = (scene.durationMs ?? 760) / 2;
1848
+ return compileChoreography(
1849
+ specOf(scene)(() => [
1850
+ {
1851
+ kind: "scale",
1852
+ from: 1,
1853
+ to: 1,
1854
+ keyframes: [
1855
+ { to: peak, durationMs: half, curve: "easeOut" },
1856
+ { to: 1, durationMs: half, curve: "easeInOut" }
1857
+ ]
1858
+ },
1859
+ {
1860
+ kind: "opacity",
1861
+ from: 1,
1862
+ to: 1,
1863
+ keyframes: [
1864
+ { to: dip, durationMs: half, curve: "easeOut" },
1865
+ { to: 1, durationMs: half, curve: "easeInOut" }
1866
+ ]
1867
+ }
1868
+ ])
1869
+ );
1870
+ },
1871
+ /**
1872
+ * **漩涡**:每片沿渐开线收向视口中心(`turns` 圈),同时旋转 + 缩小。
1873
+ * 位移量 = 目标 − **当前**(centers 由调用方从内核几何读回——重排之后也能算对)。
1874
+ */
1875
+ spiral(scene) {
1876
+ const turns = scene.turns ?? 3;
1877
+ const rMin = scene.rMin ?? 0.05;
1878
+ const rMax = scene.rMax ?? 0.9;
1879
+ const toScale = scene.toScale ?? 0.35;
1880
+ const durationMs = scene.durationMs ?? 900;
1881
+ const curve = scene.curve ?? "easeInOut";
1882
+ const view = scene.canvas.view;
1883
+ const centers = needCenters(scene.canvas, "choreograph.spiral");
1884
+ return compileChoreography(
1885
+ specOf(scene)((c) => {
1886
+ const center = viewCenter(view);
1887
+ const rad0 = Math.min(view.width, view.height) / 2;
1888
+ const ang = c.i / Math.max(1, c.n) * Math.PI * 2 * turns;
1889
+ const rad = rad0 * (rMin + (rMax - rMin) * (c.i / Math.max(1, c.n)));
1890
+ const tx = center.x + Math.cos(ang) * rad - centers[c.i].x;
1891
+ const ty = center.y + Math.sin(ang) * rad - centers[c.i].y;
1892
+ return [
1893
+ { kind: "translateX", from: 0, to: tx, curve, durationMs },
1894
+ { kind: "translateY", from: 0, to: ty, curve, durationMs },
1895
+ {
1896
+ kind: "rotate",
1897
+ from: 0,
1898
+ to: (c.i % 2 === 0 ? 1 : -1) * (180 + c.i / Math.max(1, c.n) * 540),
1899
+ curve,
1900
+ durationMs
1901
+ },
1902
+ { kind: "scale", from: 1, to: toScale, curve, durationMs }
1903
+ ];
1904
+ })
1905
+ );
1906
+ },
1907
+ /**
1908
+ * **归位**:一切变换回到"布局位"(translate/rotate → 0、scale → 1),按相位错峰。
1909
+ * 幕间清理用它——把上一幕留下的姿态收干净。
1910
+ *
1911
+ * ★起点来自 `canvas.attitudes`(**必须提供**):内核的 `from` 是必填量,缺省落 0——
1912
+ * 若不给当前姿态,"归位"会**瞬移归零**(不是收回去)。传 `attitudes: []`(空数组)
1913
+ * 等价于"已在基线"(此时本预设等于无操作,调用方可跳过)。
1914
+ */
1915
+ settle(scene) {
1916
+ const durationMs = scene.durationMs ?? 700;
1917
+ const curve = scene.curve ?? "easeInOut";
1918
+ const attitudes = scene.canvas.attitudes;
1919
+ if (!attitudes) {
1920
+ throw new Error(
1921
+ "choreograph.settle \u9700\u8981 `canvas.attitudes`\uFF08\u6BCF\u7247\u5F53\u524D\u59FF\u6001\uFF09\u2014\u2014\u5185\u6838\u7684 `from` \u662F\u5FC5\u586B\u91CF\uFF0C\u4E0D\u7ED9\u5F53\u524D\u59FF\u6001\u4F1A**\u77AC\u79FB\u5F52\u96F6**\u800C\u4E0D\u662F\u6536\u56DE\u3002\u8BF7\u4F20\u5165\u4E0A\u4E00\u5E55\u7684\u7EC8\u6001\uFF08\u8BB0\u8D26\u6216\u5B9E\u8BFB\uFF1B\u89C1 `ChoreoAttitude` \u6CE8\u91CA\uFF09\u3002"
1922
+ );
1923
+ }
1924
+ const withOpacity = scene.opacity ?? true;
1925
+ return compileChoreography(
1926
+ specOf(scene)((c) => {
1927
+ const a = attitudes[c.i] ?? {};
1928
+ const decls = [
1929
+ { kind: "translateX", from: a.tx ?? 0, to: 0, curve, durationMs },
1930
+ { kind: "translateY", from: a.ty ?? 0, to: 0, curve, durationMs },
1931
+ { kind: "rotate", from: a.rotate ?? 0, to: 0, curve, durationMs },
1932
+ { kind: "scale", from: a.scale ?? 1, to: 1, curve, durationMs }
1933
+ ];
1934
+ if (withOpacity) {
1935
+ decls.push({ kind: "opacity", from: a.opacity ?? 1, to: 1, curve, durationMs });
1936
+ }
1937
+ return decls;
1938
+ })
1939
+ );
1940
+ },
1941
+ /**
1942
+ * **多米诺**:每片"翻过去再弹回"(rotate 用 keyframes 两段;奇偶反向),
1943
+ * `serpentine` 相位形成掠过感。
1944
+ */
1945
+ domino(scene) {
1946
+ const tilt = scene.tilt ?? 26;
1947
+ const fallMs = scene.fallMs ?? 150;
1948
+ const riseMs = scene.riseMs ?? 430;
1949
+ return compileChoreography(
1950
+ specOf(scene)((c) => {
1951
+ const dir = c.i % 2 === 0 ? 1 : -1;
1952
+ return [
1953
+ {
1954
+ kind: "rotate",
1955
+ from: 0,
1956
+ to: 0,
1957
+ keyframes: [
1958
+ { to: dir * tilt, durationMs: fallMs, curve: "easeIn" },
1959
+ { to: 0, durationMs: riseMs, curve: "springApprox" }
1960
+ ]
1961
+ },
1962
+ {
1963
+ kind: "scale",
1964
+ from: 1,
1965
+ to: 1,
1966
+ keyframes: [
1967
+ { to: 0.82, durationMs: fallMs, curve: "easeIn" },
1968
+ { to: 1, durationMs: riseMs, curve: "springApprox" }
1969
+ ]
1970
+ }
1971
+ ];
1972
+ })
1973
+ );
1974
+ },
1975
+ /**
1976
+ * ★★**聚字**:把元素聚成点阵文字(`bitmap-font.ts` 的 5×7 字形;支持 `\n` 多行)。
1977
+ *
1978
+ * 分工:前 `lit.length` 片各就一个**亮像素**(放大显示);其余成为**星尘**
1979
+ * (确定性散布在文字四周的椭圆环上——同输入同画面)。
1980
+ * 元素数 < 亮像素数时按序取前几片(文字会缺笔画——由调用方保证规模,
1981
+ * 判据端有"亮像素数 ≤ 元素数"的检查)。
1982
+ *
1983
+ * ★**点距与点大小**(真机截图两轮打磨的结果):
1984
+ * · 点距 `gap = min(宽比/矩阵宽, 高比/矩阵高)`——**受限于较紧的一边**;
1985
+ * 单行 9 字(53 列)时点距只有 ~6.6px(整屏才 390px)⇒ 想要"字大"要么少字、要么**分两行**;
1986
+ * · 点尺寸给了 `elemPx`(元素原始边长)时**自动**按 `fill×gap` 推导(默认 80% 填充度)——
1987
+ * 手填 `pixelScale` 时点会随点距变化忽大忽小(点距 6.6 时 0.42 几乎粘连、
1988
+ * 点距 20 时又变成一盘散沙——两轮真机截图都抓到了)。
1989
+ */
1990
+ text(scene) {
1991
+ const bm = textBitmap(scene.text);
1992
+ if (bm.lit.length === 0) throw new Error(`choreograph.text\uFF1A\u6587\u672C "${scene.text}" \u6CA1\u6709\u4EFB\u4F55\u4EAE\u50CF\u7D20\uFF08\u5B57\u7B26\u5168\u5728\u5B57\u5E93\u5916\uFF1F\uFF09`);
1993
+ const view = scene.canvas.view;
1994
+ const centers = needCenters(scene.canvas, "choreograph.text");
1995
+ const widthRatio = scene.widthRatio ?? 0.9;
1996
+ const heightRatio = scene.heightRatio ?? 0.5;
1997
+ const durationMs = scene.durationMs ?? 900;
1998
+ const curve = scene.curve ?? "easeInOut";
1999
+ const gap = Math.min(
2000
+ view.width * widthRatio / Math.max(1, bm.width),
2001
+ view.height * heightRatio / Math.max(1, bm.height)
2002
+ );
2003
+ const fill = scene.fill ?? 0.8;
2004
+ const pixelScale = scene.pixelScale ?? (scene.elemPx && scene.elemPx > 0 ? Math.max(0.12, Math.min(1, fill * gap / scene.elemPx)) : 0.42);
2005
+ const x0 = (view.width - (bm.width - 1) * gap) / 2;
2006
+ const y0 = (view.height - (bm.height - 1) * gap) / 2;
2007
+ const center = viewCenter(view);
2008
+ const lit = bm.lit.length;
2009
+ return compileChoreography(
2010
+ specOf(scene)((c) => {
2011
+ let target;
2012
+ let scale;
2013
+ let opacity;
2014
+ if (c.i < lit) {
2015
+ const p = bm.lit[c.i];
2016
+ target = { x: x0 + p.x * gap, y: y0 + p.y * gap };
2017
+ scale = pixelScale;
2018
+ opacity = 1;
2019
+ } else {
2020
+ const h1 = hash01(c.i);
2021
+ const h2 = hash01(c.i + 977);
2022
+ const h3 = hash01(c.i + 1543);
2023
+ const ang = h1 * Math.PI * 2;
2024
+ const rx = view.width * (0.36 + 0.06 * h2);
2025
+ const ry = view.height * (0.26 + 0.05 * h3);
2026
+ target = { x: center.x + Math.cos(ang) * rx, y: center.y + Math.sin(ang) * ry };
2027
+ scale = Math.max(0.14, pixelScale * (0.42 + 0.3 * h2));
2028
+ opacity = 0.14 + 0.26 * h3;
2029
+ }
2030
+ return [
2031
+ { kind: "translateX", from: 0, to: target.x - centers[c.i].x, curve, durationMs },
2032
+ { kind: "translateY", from: 0, to: target.y - centers[c.i].y, curve, durationMs },
2033
+ { kind: "scale", from: 1, to: scale, curve, durationMs },
2034
+ { kind: "opacity", from: 1, to: opacity, curve, durationMs }
2035
+ ];
2036
+ })
2037
+ );
2038
+ },
2039
+ /**
2040
+ * **汇聚**:每片沿"屏心 → 自己"的径向外推 `spread` 倍后飞回(开场"星尘凝聚";
2041
+ * `spread` ≥ 2 时起点已在屏外)。spring 物理。
2042
+ */
2043
+ gather(scene) {
2044
+ const spread = scene.spread ?? 2.6;
2045
+ const spring = scene.spring ?? easing.smooth;
2046
+ const fadeMs = scene.fadeMs ?? 260;
2047
+ const centers = needCenters(scene.canvas, "choreograph.gather");
2048
+ const center = viewCenter(scene.canvas.view);
2049
+ return compileChoreography(
2050
+ specOf(scene)((c) => [
2051
+ { kind: "translateX", from: (centers[c.i].x - center.x) * spread, to: 0, spring },
2052
+ { kind: "translateY", from: (centers[c.i].y - center.y) * spread, to: 0, spring },
2053
+ { kind: "opacity", from: 0, to: 1, curve: "easeOut", durationMs: fadeMs }
2054
+ ])
2055
+ );
2056
+ },
2057
+ /**
2058
+ * **风暴**:五属性并发(位移抖动 + 大幅旋转 + 收放 + 呼吸),`alternate` 相位两班倒。
2059
+ * 这是单帧负载最重的一幕(800×5 = 4000 条指令并发)——压力上限的代表。
2060
+ */
2061
+ storm(scene) {
2062
+ const durationMs = scene.durationMs ?? 900;
2063
+ const spread = scene.spread ?? 90;
2064
+ const shrink = scene.shrink ?? 0.45;
2065
+ return compileChoreography(
2066
+ specOf(scene)((c) => {
2067
+ const h1 = hash01(c.i * 1.7);
2068
+ const h2 = hash01(c.i * 2.3 + 11);
2069
+ const h3 = hash01(c.i * 3.1 + 29);
2070
+ return [
2071
+ { kind: "translateX", from: 0, to: (h1 - 0.5) * spread, curve: "easeInOut", durationMs },
2072
+ { kind: "translateY", from: 0, to: (h2 - 0.5) * spread, curve: "easeInOut", durationMs },
2073
+ {
2074
+ kind: "rotate",
2075
+ from: 0,
2076
+ to: (c.i % 2 === 0 ? 1 : -1) * (360 + h3 * 360),
2077
+ curve: "easeInOut",
2078
+ durationMs
2079
+ },
2080
+ { kind: "scale", from: 1, to: shrink + h1 * 0.3, curve: "easeInOut", durationMs },
2081
+ { kind: "opacity", from: 1, to: 0.72, curve: "easeInOut", durationMs }
2082
+ ];
2083
+ })
2084
+ );
2085
+ },
2086
+ /* ────────────────────────── 颜色编排(2026-10-01 · 灯光秀抽出) ──────────────────────────
2087
+ *
2088
+ * 【为什么这批是**通用预设**(而不是节目单私有的循环)】它们全部走 `compileChoreography`
2089
+ * 的 make 回调——与 wave/ripple 同一形态、同一编译链(同一份校验/线格式)。
2090
+ * 灯光秀是第一个消费面,但"颜色 + 相位编排"是通用需求(霓虹标题/状态灯/节奏带),
2091
+ * 收敛到本包 = 一份实现(纪律 #22:第 N 份手写相位循环 = 下一个静默缺陷)。
2092
+ * ★颜色声明按通道展开(一次声明 → 4 条标量指令)由 `compileAnimations` 保证。 */
2093
+ /**
2094
+ * **闪烁**:`from →(峰值闪)→ to` 一次往返,附 scale 脉冲("灯亮起"的物理感)。
2095
+ *
2096
+ * 相位错峰由 `order × staggerMs` 决定——`radialOut` = 从中心向外逐个点亮(点火),
2097
+ * `serpentine` = 蛇形扫过(斜扫)。
2098
+ */
2099
+ flash(scene) {
2100
+ const peak = scene.peak ?? "#ffffff";
2101
+ const flashMs = scene.flashMs ?? 160;
2102
+ const fallMs = scene.fallMs ?? 620;
2103
+ const pulse = scene.pulse ?? 1.18;
2104
+ const dur = flashMs + fallMs;
2105
+ return compileChoreography(
2106
+ specOf(scene)(() => {
2107
+ const decls = [
2108
+ {
2109
+ kind: "color",
2110
+ from: scene.from,
2111
+ to: scene.to,
2112
+ durationMs: dur,
2113
+ keyframes: [
2114
+ { to: peak, durationMs: flashMs, curve: "easeOut" },
2115
+ { to: scene.to, durationMs: fallMs, curve: "easeInOut" }
2116
+ ]
2117
+ }
2118
+ ];
2119
+ if (pulse !== 1) {
2120
+ decls.push({
2121
+ kind: "scale",
2122
+ from: 1,
2123
+ to: 1,
2124
+ durationMs: dur,
2125
+ keyframes: [
2126
+ { to: pulse, durationMs: flashMs, curve: "easeOut" },
2127
+ { to: 1, durationMs: fallMs, curve: "easeInOut" }
2128
+ ]
2129
+ });
2130
+ }
2131
+ return decls;
2132
+ })
2133
+ );
2134
+ },
2135
+ /**
2136
+ * **色环**:每片走同一个多段色序列(`from → colors… → to`),相位错峰形成流动色带。
2137
+ *
2138
+ * `staggerMs = 0` 时全场同步("呼吸":`colors = [亮, 暗, 亮, 暗]`);
2139
+ * 错峰时是流动的彩虹(灯光秀 rainbow 幕)。
2140
+ */
2141
+ cycle(scene) {
2142
+ const segMs = scene.segMs ?? 240;
2143
+ return compileChoreography(
2144
+ specOf(scene)(() => [
2145
+ {
2146
+ kind: "color",
2147
+ from: scene.from,
2148
+ to: scene.to,
2149
+ durationMs: segMs * scene.colors.length,
2150
+ keyframes: scene.colors.map((c) => ({ to: c, durationMs: segMs, curve: "easeInOut" }))
2151
+ }
2152
+ ])
2153
+ );
2154
+ },
2155
+ /**
2156
+ * **极光带**:整行同色、行间错峰(行内 0 相位)——横向光带自上而下流过。
2157
+ *
2158
+ * 与 `cycle` 的差别:相位**按行**(同一行的灯严格同拍)而不是按片的 `order` 名次——
2159
+ * 行内错峰会打散"光带"的横线形态。行色缺省是 青→蓝→紫 三角波(冷色调)。
2160
+ */
2161
+ aurora(scene) {
2162
+ const rowMs = scene.rowMs ?? 40;
2163
+ const hold = scene.holdMs ?? 900;
2164
+ const rows = Math.max(1, scene.rows);
2165
+ const colorAt = scene.colorAt ?? defaultAuroraColor;
2166
+ return compileChoreography({
2167
+ ids: scene.ids,
2168
+ canvas: scene.canvas,
2169
+ order: "index",
2170
+ staggerMs: 0,
2171
+ make: (c) => {
2172
+ const d = c.row * rowMs;
2173
+ const rowColor = colorAt(c.row, rows);
2174
+ return [
2175
+ {
2176
+ kind: "color",
2177
+ from: scene.from,
2178
+ to: scene.from,
2179
+ durationMs: hold * 2,
2180
+ delayMs: d,
2181
+ keyframes: [
2182
+ { to: rowColor, durationMs: hold, curve: "easeInOut" },
2183
+ { to: scene.from, durationMs: hold, curve: "easeInOut" }
2184
+ ]
2185
+ }
2186
+ ];
2187
+ }
2188
+ });
2189
+ },
2190
+ /**
2191
+ * **染色**:纯颜色补间(`from → to`),相位错峰——"全场变某色"的一句话。
2192
+ * 聚字/归位幕常与位置预设**同批**使用(各自独立声明,颜色与位置互不干扰)。
2193
+ */
2194
+ paint(scene) {
2195
+ const durationMs = scene.durationMs ?? 700;
2196
+ const curve = scene.curve ?? "easeInOut";
2197
+ return compileChoreography(
2198
+ specOf(scene)(() => [{ kind: "color", from: scene.from, to: scene.to, durationMs, curve }])
2199
+ );
2200
+ },
2201
+ /* ────────────────────────── 灯阵模式(2026-10-01 · 灯光秀重构抽出) ──────────────────────────
2202
+ *
2203
+ * 【与上面"颜色编排"的本质差别】那批是"每片同一种色变化"(靠 `order` 相位错峰);
2204
+ * 这批是**灯阵语义**:每颗灯的亮/灭/色由它的**位置**(行/列/角度)决定,灯**一颗不动**——
2205
+ * 整场演出只是"亮灯和灭灯"(真实灯光秀与 LED 矩阵屏的工作方式,用户语义修正的落点)。
2206
+ * ★所有预设只产出**颜色指令**(kind 5..8),不产出任何位移/缩放/旋转/透明度。
2207
+ * ★仍走 `compileChoreography` 同一编译链(同一份校验/线格式)。 */
2208
+ /**
2209
+ * ★★**灯阵点字**("亮灯成字"——LED 矩阵屏的经典节目):
2210
+ * 把 `text`(5×7 点阵)渲染到灯阵上——**亮的灯** = `on` 色,**灭的灯** = `off` 色(暗盘)。
2211
+ * 灯位保持不变,全靠亮灭组成文字。
2212
+ *
2213
+ * 布局:点阵水平居中;`\n` 多行纵向堆叠。每行最多 `floor((cols+1)/6)` 个字(5 列字宽 + 1 列间隔)。
2214
+ * `blinks` > 0 时亮灯先闪烁(灭→亮交替)再定格——"霓虹招牌"的观感。
2215
+ */
2216
+ matrixText(scene) {
2217
+ const on = scene.on ?? "#ffffff";
2218
+ const resolveMs = scene.resolveMs ?? 420;
2219
+ const bm = textBitmap(scene.text);
2220
+ const cols = Math.max(1, scene.canvas.cols);
2221
+ const rows = Math.max(1, Math.ceil(scene.ids.length / cols));
2222
+ const x0 = Math.floor((cols - bm.width) / 2);
2223
+ const y0 = Math.floor((rows - bm.height) / 2);
2224
+ const cells = /* @__PURE__ */ new Set();
2225
+ for (const pt of bm.lit) cells.add(pt.y * 4096 + pt.x);
2226
+ const litOf = (i) => {
2227
+ const gx = i % cols - x0;
2228
+ const gy = Math.floor(i / cols) - y0;
2229
+ return gx >= 0 && gy >= 0 && gx < bm.width && gy < bm.height && cells.has(gy * 4096 + gx);
2230
+ };
2231
+ const cur = (i) => scene.currentOf?.(i) ?? scene.from ?? scene.off;
2232
+ return compileChoreography({
2233
+ ids: scene.ids,
2234
+ canvas: scene.canvas,
2235
+ // ★相位错峰由 order × staggerMs 推出(compileChoreography 自动叠加 delayMs)——
2236
+ // 波形扫过:灯一颗颗(按对角名次)解析成字,而不是整屏同时跳变
2237
+ order: scene.order ?? "diagonal",
2238
+ staggerMs: scene.staggerMs ?? 6,
2239
+ make: (c) => [
2240
+ {
2241
+ kind: "color",
2242
+ from: cur(c.i),
2243
+ to: litOf(c.i) ? on : scene.off,
2244
+ durationMs: resolveMs,
2245
+ curve: "easeInOut"
2246
+ }
2247
+ ]
2248
+ });
2249
+ },
2250
+ /**
2251
+ * ★★**灯阵图案帧**(纯计算,不产出指令):给定文本与网格,返回**每颗灯的目标色**数组——
2252
+ * 亮盘 = `on`、灭盘 = `off`。调用方用它算下一幕的 `currentOf`(逐灯起点色):
2253
+ * `matrixFrame('800', cols, n, {on, off})` 的终态 = 下一幕(如 `matrixText('LIG\nHTS')`)
2254
+ * `currentOf` 的输入 ⇒ **串幕零跳变**(灯从"它此刻真实的颜色"出发)。
2255
+ */
2256
+ matrixFrame(text, cols, n, colors) {
2257
+ const bm = textBitmap(text);
2258
+ const cc = Math.max(1, cols);
2259
+ const rows = Math.max(1, Math.ceil(n / cc));
2260
+ const x0 = Math.floor((cc - bm.width) / 2);
2261
+ const y0 = Math.floor((rows - bm.height) / 2);
2262
+ const cells = /* @__PURE__ */ new Set();
2263
+ for (const pt of bm.lit) cells.add(pt.y * 4096 + pt.x);
2264
+ const out = [];
2265
+ for (let i = 0; i < n; i++) {
2266
+ const gx = i % cc - x0;
2267
+ const gy = Math.floor(i / cc) - y0;
2268
+ const lit = gx >= 0 && gy >= 0 && gx < bm.width && gy < bm.height && cells.has(gy * 4096 + gx);
2269
+ out.push(lit ? colors.on : colors.off);
2270
+ }
2271
+ return out;
2272
+ },
2273
+ /**
2274
+ * **跑马灯**(边框追逐——灯会实景最常见的节目):灯阵**外圈**的灯按顺时针次序
2275
+ * 逐颗点亮(亮白 → 回落),内部灯保持灭。相位名次 = 沿边框的序号。
2276
+ */
2277
+ perimeterChase(scene) {
2278
+ const on = scene.on ?? "#ffffff";
2279
+ const staggerMs = scene.staggerMs ?? 16;
2280
+ const holdMs = scene.holdMs ?? 260;
2281
+ const from = scene.from ?? scene.off;
2282
+ const cols = Math.max(1, scene.canvas.cols);
2283
+ const rows = Math.max(1, Math.ceil(scene.ids.length / cols));
2284
+ const rankOf = (i) => {
2285
+ const r = Math.floor(i / cols);
2286
+ const c = i % cols;
2287
+ if (r === 0) return c;
2288
+ if (c === cols - 1) return cols - 1 + r;
2289
+ if (r === rows - 1) return cols - 1 + rows - 1 + (cols - 1 - c);
2290
+ if (c === 0) return cols - 1 + rows - 1 + cols - 1 + (rows - 1 - r);
2291
+ return -1;
2292
+ };
2293
+ return compileChoreography(
2294
+ specOf(scene)((c) => {
2295
+ const rank = rankOf(c.i);
2296
+ if (rank < 0) {
2297
+ return [{ kind: "color", from, to: scene.off, durationMs: staggerMs, curve: "linear" }];
2298
+ }
2299
+ return [
2300
+ {
2301
+ kind: "color",
2302
+ from,
2303
+ to: scene.off,
2304
+ durationMs: rank * staggerMs + holdMs,
2305
+ // 等到自己那一棒:先灭着(或保持起点色),轮到时亮白,再回落
2306
+ keyframes: [
2307
+ { to: scene.off, durationMs: Math.max(1, rank * staggerMs), curve: "linear" },
2308
+ { to: on, durationMs: holdMs, curve: "easeOut" },
2309
+ { to: scene.off, durationMs: 1, curve: "linear" }
2310
+ ]
2311
+ }
2312
+ ];
2313
+ })
2314
+ );
2315
+ },
2316
+ /**
2317
+ * **光扇**(旋转光束——灯会实景的另一招牌):一束光绕屏心**旋转扫过**,
2318
+ * 被扫到的灯亮白、扫过即灭(角度由灯在阵中的位置推出)。
2319
+ *
2320
+ * 每颗灯的关键帧 = **按"该灯被光束照到的时间区间"归并**(通常 2–4 段,不是 steps 段)——
2321
+ * 800 颗灯的总指令仍是每灯 4 条颜色通道(四通道展开)。`steps` × `stepMs` = 转一整圈的时长。
2322
+ */
2323
+ fan(scene) {
2324
+ const on = scene.on ?? "#ffffff";
2325
+ const steps = Math.max(3, scene.steps ?? 12);
2326
+ const widthDeg = scene.widthDeg ?? 60;
2327
+ const stepMs = scene.stepMs ?? 150;
2328
+ const from = scene.from ?? scene.off;
2329
+ const cols = Math.max(1, scene.canvas.cols);
2330
+ const rows = Math.max(1, Math.ceil(scene.ids.length / cols));
2331
+ const angOf = (i) => {
2332
+ const r = Math.floor(i / cols);
2333
+ const c = i % cols;
2334
+ const x = c + 0.5 - cols / 2;
2335
+ const y = r + 0.5 - rows / 2;
2336
+ const a = Math.atan2(y, x) * 180 / Math.PI;
2337
+ return (a + 360) % 360;
2338
+ };
2339
+ const diff = (a, b) => {
2340
+ const d = Math.abs(a - b) % 360;
2341
+ return d > 180 ? 360 - d : d;
2342
+ };
2343
+ return compileChoreography(
2344
+ specOf(scene)((c) => {
2345
+ const ang = angOf(c.i);
2346
+ const status = [];
2347
+ for (let k2 = 0; k2 < steps; k2++) {
2348
+ const beam = k2 * 360 / steps;
2349
+ status.push(diff(ang, beam) <= widthDeg / 2);
2350
+ }
2351
+ const kf = [];
2352
+ let k = 0;
2353
+ while (k < steps) {
2354
+ const s0 = status[k];
2355
+ let j = k;
2356
+ while (j < steps && status[j] === s0) j++;
2357
+ kf.push({ to: s0 ? on : scene.off, durationMs: (j - k) * stepMs, curve: "linear" });
2358
+ k = j;
2359
+ }
2360
+ if (kf.length === 0 || kf[kf.length - 1].to !== scene.off) {
2361
+ kf.push({ to: scene.off, durationMs: stepMs, curve: "linear" });
2362
+ }
2363
+ const dur = kf.reduce((acc, x) => acc + x.durationMs, 0);
2364
+ return [{ kind: "color", from, to: scene.off, durationMs: dur, keyframes: kf }];
2365
+ })
2366
+ );
2367
+ },
2368
+ /**
2369
+ * **棋盘翻转**(矩阵屏经典):亮灭按 (行+列) 的奇偶交替成棋盘格,整体**翻转 `flips` 次**
2370
+ * 后收为全灭。
2371
+ */
2372
+ checker(scene) {
2373
+ const on = scene.on ?? "#ffffff";
2374
+ const flips = Math.max(1, scene.flips ?? 4);
2375
+ const segMs = scene.segMs ?? 240;
2376
+ const from = scene.from ?? scene.off;
2377
+ const cols = Math.max(1, scene.canvas.cols);
2378
+ return compileChoreography(
2379
+ specOf(scene)((c) => {
2380
+ const kf = [];
2381
+ for (let k = 0; k < flips; k++) {
2382
+ const onPhase = (c.row + c.col + k) % 2 === 0;
2383
+ kf.push({ to: onPhase ? on : scene.off, durationMs: segMs, curve: "linear" });
2384
+ }
2385
+ kf.push({ to: scene.off, durationMs: segMs, curve: "linear" });
2386
+ const dur = kf.reduce((acc, x) => acc + x.durationMs, 0);
2387
+ return [{ kind: "color", from, to: scene.off, durationMs: dur, keyframes: kf }];
2388
+ })
2389
+ );
2390
+ },
2391
+ /**
2392
+ * **星火**(确定性闪烁):每颗灯按确定性伪随机走 `segments` 段霓虹色,
2393
+ * 最后收灭——"满屏灯在闪"的压力形态(800 灯 × 多段 keyframes)。
2394
+ */
2395
+ sparkle(scene) {
2396
+ const palette = scene.palette.length > 0 ? scene.palette : ["#ffffff"];
2397
+ const segments = Math.max(2, scene.segments ?? 6);
2398
+ const segMs = scene.segMs ?? 200;
2399
+ const from = scene.from ?? scene.off;
2400
+ return compileChoreography(
2401
+ specOf(scene)((c) => {
2402
+ const kf = [];
2403
+ for (let k = 0; k < segments; k++) {
2404
+ const pick = palette[Math.floor(hash01(c.i * 31.7 + k * 7.3) * palette.length) % palette.length];
2405
+ kf.push({ to: pick, durationMs: segMs, curve: "linear" });
2406
+ }
2407
+ kf.push({ to: scene.off, durationMs: segMs, curve: "linear" });
2408
+ const dur = kf.reduce((acc, x) => acc + x.durationMs, 0);
2409
+ return [{ kind: "color", from, to: scene.off, durationMs: dur, keyframes: kf }];
2410
+ })
2411
+ );
2412
+ },
2413
+ /**
2414
+ * **扫描光幕**:一条"暗带"先自上而下压过(灯逐行暗下),紧接着"亮带"再自下而上
2415
+ * 点亮(灯逐行亮起)——真实舞台常见的"灯光窗帘"。每行整行同拍、行间错峰 `rowMs`。
2416
+ */
2417
+ rowSweep(scene) {
2418
+ const rowMs = scene.rowMs ?? 30;
2419
+ const holdMs = scene.holdMs ?? 240;
2420
+ const cols = Math.max(1, scene.canvas.cols);
2421
+ return compileChoreography(
2422
+ specOf(scene)((c) => [
2423
+ {
2424
+ kind: "color",
2425
+ from: scene.from,
2426
+ to: scene.from,
2427
+ durationMs: c.row * rowMs + holdMs,
2428
+ delayMs: 1,
2429
+ keyframes: [
2430
+ { to: scene.dim, durationMs: Math.max(1, c.row * rowMs), curve: "linear" },
2431
+ { to: scene.from, durationMs: holdMs, curve: "easeOut" }
2432
+ ]
2433
+ }
2434
+ ])
2435
+ );
2436
+ }
2437
+ };
2438
+ function defaultAuroraColor(row, rows) {
2439
+ const t = row / Math.max(1, rows - 1);
2440
+ const tri = t < 0.5 ? t * 2 : (1 - t) * 2;
2441
+ const a = [34, 211, 238];
2442
+ const b = [59, 130, 246];
2443
+ const c = [168, 85, 247];
2444
+ const mix = (x, y, u) => {
2445
+ const f = (p, q) => Math.round(p + (q - p) * u).toString(16).padStart(2, "0");
2446
+ return `#${f(x[0], y[0])}${f(x[1], y[1])}${f(x[2], y[2])}`;
2447
+ };
2448
+ return tri < 0.5 ? mix(a, b, tri * 2) : mix(b, c, (tri - 0.5) * 2);
2449
+ }
2450
+ var DUR = 300;
2451
+ var route = {
2452
+ /**
2453
+ * **半屏弹窗**(从底部滑入)—— 对齐 `wx://bottom-sheet`
2454
+ *
2455
+ * ★为什么"只动进场页":弹窗场景下旧页面**保持不动**(背景被遮罩压暗即可)——
2456
+ * 动它反而会让用户误以为页面在跳。这与微信 `wx://bottom-sheet` 的行为一致。
2457
+ */
2458
+ bottomSheet(opts = {}) {
2459
+ const dist = opts.distance ?? 400;
2460
+ const dur = opts.durationMs ?? DUR;
2461
+ const enter = opts.spring ? [{ kind: "translateY", from: dist, to: 0, spring: easing.smooth }] : [{ kind: "translateY", from: dist, to: 0, curve: "easeOut", durationMs: dur }];
2462
+ return { name: "bottomSheet", wxRouteType: "wx://bottom-sheet", enter, exit: [], opaque: false, durationMs: dur };
2463
+ },
2464
+ /**
2465
+ * **全屏向上推入**(新页从下方推入,旧页被推出)—— 对齐 `wx://upwards`
2466
+ */
2467
+ slideUp(opts = {}) {
2468
+ const dist = opts.distance ?? 800;
2469
+ const dur = opts.durationMs ?? DUR;
2470
+ const parallax = opts.exitParallax ?? 0.3;
2471
+ return {
2472
+ name: "slideUp",
2473
+ wxRouteType: "wx://upwards",
2474
+ enter: [{ kind: "translateY", from: dist, to: 0, curve: "easeOut", durationMs: dur }],
2475
+ // 旧页向反方向让位(视差)+ 轻微淡出——这是"推入"的层次感来源
2476
+ exit: [
2477
+ { kind: "translateY", from: 0, to: -dist * parallax, curve: "easeOut", durationMs: dur },
2478
+ { kind: "opacity", from: 1, to: 1 - parallax, curve: "easeOut", durationMs: dur }
2479
+ ],
2480
+ opaque: true,
2481
+ durationMs: dur
2482
+ };
2483
+ },
2484
+ /**
2485
+ * **下滑关闭**(dismiss:当前页向下滑出,露出下层页)—— 对齐统一枚举的 `slideDown`
2486
+ *
2487
+ * 【与 `slideUp` 的方向关系】`slideUp` 是"推入"(新页从下往上),`slideDown` 是"弹出/关闭"
2488
+ * (当前页从上往下)——两者是**相反**的位移方向,对应统一枚举 `RouteTransition` 的两个成员
2489
+ * (Web 侧 `WEB_TRANSITION_MAP.slideDown = 'slide-down'`、MP 侧 `routeType: 'slideDown'`)。
2490
+ *
2491
+ * ★注意 `enter`/`exit` 的语义:本预设描述的是**被关闭页(exit)下滑**;
2492
+ * `enter` 留空(下层页本就静止——动它会让"关闭"看起来像"又推了一页")。
2493
+ */
2494
+ slideDown(opts = {}) {
2495
+ const dist = opts.distance ?? 800;
2496
+ const dur = opts.durationMs ?? 300;
2497
+ return {
2498
+ name: "slideDown",
2499
+ wxRouteType: null,
2500
+ // ★诚实边界:微信 routeType 里没有"下滑关闭"这个预设(dismiss 由导航栈语义表达)
2501
+ // ★★`role: 'dismiss'`(退场型)——`exit` 描述的是**被关闭页**的下滑动作(pop 语义)。
2502
+ // 没有它时 back 推导会把 `exit` 反向绑给"返回目标页"⇒ 视觉变成"下层页升上来"(方向反了)。
2503
+ role: "dismiss",
2504
+ enter: [],
2505
+ exit: [
2506
+ { kind: "translateY", from: 0, to: dist, curve: "easeIn", durationMs: dur },
2507
+ { kind: "opacity", from: 1, to: 0.8, curve: "easeIn", durationMs: dur }
2508
+ ],
2509
+ opaque: false,
2510
+ // 下滑时下层页可见
2511
+ durationMs: dur
2512
+ };
2513
+ },
2514
+ /**
2515
+ * **缩放下沉**(新页从底部放大进入,旧页下沉)—— 对齐 `wx://zoom`
2516
+ */
2517
+ zoom(opts = {}) {
2518
+ const dur = opts.durationMs ?? DUR;
2519
+ const sc = opts.fromScale ?? 0.92;
2520
+ const offY = opts.fromOffsetY ?? 60;
2521
+ return {
2522
+ name: "zoom",
2523
+ wxRouteType: "wx://zoom",
2524
+ enter: [
2525
+ { kind: "scale", from: sc, to: 1, curve: "easeOut", durationMs: dur },
2526
+ { kind: "translateY", from: offY, to: 0, curve: "easeOut", durationMs: dur },
2527
+ { kind: "opacity", from: 0.6, to: 1, curve: "easeOut", durationMs: dur }
2528
+ ],
2529
+ // 旧页"下沉"(缩小 + 变暗)——视觉上像被压到下面
2530
+ exit: [
2531
+ { kind: "scale", from: 1, to: 0.96, curve: "easeOut", durationMs: dur },
2532
+ { kind: "opacity", from: 1, to: 0.7, curve: "easeOut", durationMs: dur }
2533
+ ],
2534
+ opaque: true,
2535
+ durationMs: dur
2536
+ };
2537
+ },
2538
+ /**
2539
+ * **iOS 风格模态**(从底部滑入 + 轻微缩放)—— 对齐 `wx://cupertino-modal`
2540
+ *
2541
+ * 与 `bottomSheet` 的差别:模态是**全屏**(距离 = 屏幕高),且**带阻尼感**(弹簧)。
2542
+ */
2543
+ cupertinoModal(opts = {}) {
2544
+ const dist = opts.distance ?? 800;
2545
+ return {
2546
+ name: "cupertinoModal",
2547
+ wxRouteType: "wx://cupertino-modal",
2548
+ enter: [{ kind: "translateY", from: dist, to: 0, spring: easing.smooth }],
2549
+ exit: [{ kind: "scale", from: 1, to: 0.92, curve: "easeInOut", durationMs: 250 }],
2550
+ opaque: true,
2551
+ durationMs: 350
2552
+ };
2553
+ }
2554
+ };
2555
+ var list = {
2556
+ /**
2557
+ * ★★**列表项增删让位**(Morpheus §5 的招牌能力)
2558
+ *
2559
+ * 【为什么这一条最值钱】传统 FLIP 要**前后各读一次几何**(跨边界查询,VDOM 框架里很贵);
2560
+ * 而本仓几何本来就在 Rust 内核 ⇒ 两次快照都是内部读,**零跨边界、零 JS**。
2561
+ * 配合 0.08ms 全量重排,"列表增删时其他项平滑让位"几乎白送。
2562
+ *
2563
+ * 用法(一句话):
2564
+ * ```ts
2565
+ * const spec = presets.list.shift() // 默认 300ms easeOut、无交错
2566
+ * engine.flipCapture() // 变更前记快照
2567
+ * applyListMutation() // 增删数据 → 布局变
2568
+ * engine.flipStart(spec) // 启动补间
2569
+ * ```
2570
+ */
2571
+ shift(opts = {}) {
2572
+ return {
2573
+ name: "listShift",
2574
+ durationMs: opts.durationMs ?? 300,
2575
+ curve: opts.curve ?? "easeOut",
2576
+ staggerMs: opts.staggerMs ?? 0
2577
+ };
2578
+ }
2579
+ };
2580
+ var element = {
2581
+ /** **淡入**(透明度 0 → 1;可加轻微上移,避免"平淡地出现") */
2582
+ fadeIn(opts = {}) {
2583
+ const dur = opts.durationMs ?? 240;
2584
+ const rise = opts.risePx ?? 0;
2585
+ const decls = [
2586
+ { kind: "opacity", from: 0, to: 1, curve: "easeOut", durationMs: dur, delayMs: opts.delayMs }
2587
+ ];
2588
+ if (rise > 0) {
2589
+ decls.push({ kind: "translateY", from: rise, to: 0, curve: "easeOut", durationMs: dur, delayMs: opts.delayMs });
2590
+ }
2591
+ return { name: "fadeIn", decls, durationMs: dur };
2592
+ },
2593
+ /**
2594
+ * ★★**翻牌 / 3D 旋入**(2026-10-01 · B 批)——`rotateX` / `rotateY` + 透视。
2595
+ *
2596
+ * `axis: 'x' | 'y'` 决定绕哪个轴翻;`from` 是起始角度(度,正 = 朝观察者翻起/右缘向内)。
2597
+ * 常配 `perspective`(在**节点样式**上声明:`perspective: 1200`——CSS 语义)。
2598
+ *
2599
+ * ★**为什么走 tick 路径(诚实标注)**:两端平台插值器的 3D 语义不同(iOS 完整 4×4
2600
+ * 矩阵 vs Android rotationX/Y + Camera)⇒ 统一走内核逐帧求值(跨端一致优先,与 color 同源)。
2601
+ * 实测余量充足(120Hz p95 2ms / 预算 8.3ms)。
2602
+ */
2603
+ flipIn(opts = {}) {
2604
+ const axis = opts.axis ?? "y";
2605
+ const from = opts.fromDeg ?? -90;
2606
+ const dur = opts.durationMs ?? 420;
2607
+ const decls = [
2608
+ {
2609
+ kind: axis === "x" ? "rotateX" : "rotateY",
2610
+ from,
2611
+ to: 0,
2612
+ curve: opts.curve ?? "easeOut",
2613
+ durationMs: dur,
2614
+ delayMs: opts.delayMs
2615
+ },
2616
+ // ★翻入常配淡入(纯旋转在无背面的元素上观感单调)——与 3D 同批下发(不同属性不冲突)
2617
+ { kind: "opacity", from: 0.6, to: 1, curve: "easeOut", durationMs: dur, delayMs: opts.delayMs }
2618
+ ];
2619
+ return { name: `flipIn-${axis}`, decls, durationMs: dur };
2620
+ },
2621
+ /**
2622
+ * ★★**3D 翻面**(`rotateY` 0→180 或 `rotateX`)——卡片翻到"背面"。
2623
+ *
2624
+ * `flips: 1` 翻一次停住;与 `repeat`/`direction` 组合可做"持续翻转"
2625
+ * (`{ kind: 'rotateY', repeat: 'infinite', direction: 'alternate' }` 一句话 = 来回翻转)。
2626
+ */
2627
+ flip3D(opts = {}) {
2628
+ const axis = opts.axis ?? "y";
2629
+ const to = opts.toDeg ?? 180;
2630
+ const dur = opts.durationMs ?? 500;
2631
+ return {
2632
+ name: `flip3D-${axis}`,
2633
+ decls: [
2634
+ {
2635
+ kind: axis === "x" ? "rotateX" : "rotateY",
2636
+ from: 0,
2637
+ to,
2638
+ curve: opts.curve ?? "easeInOut",
2639
+ durationMs: dur,
2640
+ ...opts.repeat !== void 0 ? { repeat: opts.repeat } : {},
2641
+ ...opts.direction !== void 0 ? { direction: opts.direction } : {}
2642
+ }
2643
+ ],
2644
+ durationMs: dur
2645
+ };
2646
+ },
2647
+ /**
2648
+ * **按压反馈**(松手弹回:`from` 是按下时的缩放,弹回 1)
2649
+ *
2650
+ * ★**诚实边界(引擎能力决定形态)**:内核对同 (节点,属性) 是**替换**语义 ⇒
2651
+ * "按下 → 弹回"**两段序列**在一个批次里表达不了(第二条会替换第一条)。
2652
+ * ⇒ 本预设只做**弹回段**;按下段由调用方在 `pressStart` 时单独启动
2653
+ * (或直接用 `spring` 从按下值弹回——这也是最常用的用法)。
2654
+ * ★序列编排(sequence)是已知缺口,见 README「未做」一节。
2655
+ */
2656
+ pressRelease(opts = {}) {
2657
+ const sc = opts.fromScale ?? 0.96;
2658
+ return {
2659
+ name: "pressRelease",
2660
+ decls: [{ kind: "scale", from: sc, to: 1, spring: easing.snappy }],
2661
+ durationMs: 400
2662
+ };
2663
+ },
2664
+ /**
2665
+ * ★★**按压反馈(两段序列)**——按下后回弹,**一条动画**表达完整序列(MA6 起)
2666
+ *
2667
+ * 【这条预设是"序列编排"的存在理由】此前只能做"弹回段"(`pressRelease`),
2668
+ * 因为内核对同 (节点,属性) 是替换语义、两条 scale 会互相覆盖;
2669
+ * MA6 的 `keyframes` 让"下压 → 回弹"收敛在**一条**动画里,且提交平台路径时仍是**一条**
2670
+ * `CAKeyframeAnimation`(不额外增加提交次数)。
2671
+ */
2672
+ press(opts = {}) {
2673
+ const sc = opts.fromScale ?? 0.94;
2674
+ const downMs = opts.downMs ?? 90;
2675
+ const upMs = opts.upMs ?? 260;
2676
+ return {
2677
+ name: "pressSequence",
2678
+ decls: [
2679
+ {
2680
+ kind: "scale",
2681
+ from: 1,
2682
+ to: 1,
2683
+ keyframes: [
2684
+ { to: sc, durationMs: downMs, curve: opts.downCurve ?? "easeOut" },
2685
+ { to: 1, durationMs: upMs, curve: opts.upCurve ?? "springApprox" }
2686
+ ]
2687
+ }
2688
+ ],
2689
+ durationMs: downMs + upMs
2690
+ };
2691
+ },
2692
+ /**
2693
+ * **抖动(错误提示)**——水平往复三段,末段回到起点
2694
+ *
2695
+ * ★为什么"末段必须回 0":抖动是**扰动**,不是位移;末段不回 0 会让元素永久偏移
2696
+ * (这是"看起来对、实际错位"的典型)。预设已保证末段 `to: 0`。
2697
+ */
2698
+ shake(opts = {}) {
2699
+ const amp = opts.amplitude ?? 10;
2700
+ const dur = opts.durationMs ?? 360;
2701
+ const seg = dur / 3;
2702
+ return {
2703
+ name: "shake",
2704
+ decls: [
2705
+ {
2706
+ kind: "translateX",
2707
+ from: 0,
2708
+ to: 0,
2709
+ keyframes: [
2710
+ { to: -amp, durationMs: seg, curve: "easeOut" },
2711
+ { to: amp, durationMs: seg, curve: "easeInOut" },
2712
+ { to: 0, durationMs: seg, curve: "easeOut" }
2713
+ ]
2714
+ }
2715
+ ],
2716
+ durationMs: dur
2717
+ };
2718
+ },
2719
+ /**
2720
+ * ★★**共享元素**(跨元素/跨页面飞行)——Morpheus §6.1 清单最后一项
2721
+ *
2722
+ * 【与 `sharedElementFlyIn` 的本质差别】那个是"**在落点上**做缩放+淡入"(不需要源几何);
2723
+ * 本条是**真·共享元素**:从**源矩形**飞到目标位置再归位——需要"源"的稳态几何。
2724
+ *
2725
+ * 【源有两种,覆盖两种场景】
2726
+ * - `fromNodeId`:**同树节点**(同页面内的共享元素,如列表项 → 扩展卡);
2727
+ * - `fromRect`:**系统坐标矩形**(跨页面/跨稳态,如"上一页的缩略图位置"——
2728
+ * 该几何由调用方注入:静态布局(tab/宫格/固定 header)可在**编译期**算出、随指令一起下发,
2729
+ * 只有真正运行期才知道的才由宿主上报)。
2730
+ *
2731
+ * 【几何数学在内核,本预设只是"声明"】编译产物带 `shared: {sourceRect | sourceNodeId}`,
2732
+ * 由宿主交 `proteus_layout_shared_element` 处理(中心差 + 宽度比 + 缓动都在内核)。
2733
+ *
2734
+ * ★诚实边界(见 README「未做」):内核只有**等比** scale ⇒ 以宽度比为准,
2735
+ * 源/目标宽高比不一致时高度按目标比例推出。
2736
+ */
2737
+ sharedElement(opts = {}) {
2738
+ if (!opts.fromRect && opts.fromNodeId === void 0) {
2739
+ throw new Error("sharedElement \u9700\u8981\u6E90\uFF1A\u7ED9 fromRect\uFF08\u7CFB\u7EDF\u5750\u6807\uFF09\u6216 fromNodeId\uFF08\u540C\u6811\u8282\u70B9\uFF09");
2740
+ }
2741
+ if (opts.fromRect && opts.fromNodeId !== void 0) {
2742
+ throw new Error("sharedElement \u7684\u6E90\u53EA\u80FD\u7ED9\u4E00\u4E2A\uFF1AfromRect \u4E0E fromNodeId \u4E8C\u9009\u4E00");
2743
+ }
2744
+ return {
2745
+ name: "sharedElement",
2746
+ durationMs: opts.durationMs ?? 400,
2747
+ fadeIn: opts.fadeIn !== false,
2748
+ ...opts.fromRect ? { fromRect: opts.fromRect } : { fromNodeId: opts.fromNodeId }
2749
+ };
2750
+ },
2751
+ /** **共享元素飞入**(在同树落点上做缩放+淡入——不需要源几何的简化形态) */
2752
+ sharedElementFlyIn(opts = {}) {
2753
+ const dur = opts.durationMs ?? 400;
2754
+ const sc = opts.fromScale ?? 0.4;
2755
+ return {
2756
+ name: "sharedElementFlyIn",
2757
+ decls: [
2758
+ { kind: "scale", from: sc, to: 1, spring: easing.smooth },
2759
+ { kind: "opacity", from: 0, to: 1, curve: "easeOut", durationMs: dur }
2760
+ ],
2761
+ durationMs: dur
2762
+ };
2763
+ }
2764
+ };
2765
+ var scroll = {
2766
+ /**
2767
+ * **吸顶**(滚过 `pinAt` 后头部固定:用反向位移抵消继续滚动)
2768
+ *
2769
+ * ★实现说明:本引擎只写**合成属性**(translate/opacity)⇒ 吸顶表达为
2770
+ * `translateY: 0 → -(scrollSpan)` 的窗口动画,配合布局让位实现"钉住"观感;
2771
+ * 真·改变定位(position: sticky)属布局属性,不在本引擎属性面上(编译期会拦)。
2772
+ */
2773
+ sticky(opts = {}) {
2774
+ const pinAt = opts.pinAt ?? 80;
2775
+ const span = opts.span ?? 120;
2776
+ return {
2777
+ name: "scrollSticky",
2778
+ decls: [{ kind: "translateY", from: 0, to: -span, curve: "linear", scroll: { from: pinAt, to: pinAt + span } }],
2779
+ window: { from: pinAt, to: pinAt + span }
2780
+ };
2781
+ },
2782
+ /**
2783
+ * **视差**(背景层随滚动反向慢移;`factor` 0..1 = 慢移比例)
2784
+ *
2785
+ * `factor=0.4` ⇒ 滚过 100px 时背景只上移 40px(相对前景的"景深感")。
2786
+ */
2787
+ parallax(opts = {}) {
2788
+ const factor = opts.factor ?? 0.4;
2789
+ const from = opts.from ?? 0;
2790
+ const to = opts.to ?? 400;
2791
+ return {
2792
+ name: "scrollParallax",
2793
+ decls: [
2794
+ {
2795
+ kind: "translateY",
2796
+ from: 0,
2797
+ to: -(to - from) * factor,
2798
+ curve: "linear",
2799
+ scroll: { from, to }
2800
+ }
2801
+ ],
2802
+ window: { from, to }
2803
+ };
2804
+ },
2805
+ /**
2806
+ * **渐显**(滚入 `from..to` 区间内透明度 0 → 1;可叠加轻微上移)
2807
+ *
2808
+ * ★`from/to` 通常取"元素进入视口"的滚动位置区间(由布局计算给出,预设不猜)。
2809
+ */
2810
+ fadeIn(opts) {
2811
+ const rise = opts.risePx ?? 0;
2812
+ const decls = [
2813
+ { kind: "opacity", from: 0, to: 1, curve: "easeOut", scroll: { from: opts.from, to: opts.to } }
2814
+ ];
2815
+ if (rise > 0) {
2816
+ decls.push({ kind: "translateY", from: rise, to: 0, curve: "easeOut", scroll: { from: opts.from, to: opts.to } });
2817
+ }
2818
+ return { name: "scrollFadeIn", decls, window: { from: opts.from, to: opts.to } };
2819
+ }
2820
+ };
2821
+ var presets = { route, list, element, easing, scroll, choreograph };
2822
+ var APP_TRANSITION_MAP = {
2823
+ // 推入(新页从下往上)—— 与 Web `slide-up` / MP `routeType: 'slideUp'` 同语义
2824
+ slideUp: presets.route.slideUp(),
2825
+ // 下滑关闭(当前页往下滑出,露出下层)—— 与 Web `slide-down` / MP `slideDown` 同语义
2826
+ slideDown: presets.route.slideDown(),
2827
+ // 半屏(弹窗从底部滑入,下层不动)—— 与 Web `halfscreen` / MP `halfScreen` 同语义
2828
+ halfScreen: presets.route.bottomSheet(),
2829
+ // 缩放下沉(新页放大进入 + 旧页下沉)—— 与 Web `scale` / MP `scaleDown` 同语义
2830
+ scaleDown: presets.route.zoom(),
2831
+ // 无转场(瞬切)
2832
+ none: {
2833
+ name: "none",
2834
+ wxRouteType: null,
2835
+ enter: [],
2836
+ exit: [],
2837
+ opaque: true,
2838
+ durationMs: 0
2839
+ }
2840
+ };
2841
+ function appTransition(transition, opts) {
2842
+ const spec = APP_TRANSITION_MAP[transition];
2843
+ if (!spec) return APP_TRANSITION_MAP.none;
2844
+ if (!opts || opts.distance === void 0 && opts.durationMs === void 0) return spec;
2845
+ const p = opts.distance;
2846
+ const d = opts.durationMs;
2847
+ switch (transition) {
2848
+ case "slideUp":
2849
+ return presets.route.slideUp({ distance: p, durationMs: d });
2850
+ case "slideDown":
2851
+ return presets.route.slideDown({ distance: p, durationMs: d });
2852
+ case "halfScreen":
2853
+ return presets.route.bottomSheet({ distance: p, durationMs: d });
2854
+ case "scaleDown":
2855
+ return presets.route.zoom({ durationMs: d, fromOffsetY: p });
2856
+ default:
2857
+ return spec;
2858
+ }
2859
+ }
2860
+ function reverseDecls(decls) {
2861
+ return decls.map((d) => {
2862
+ if (isColorDecl(d)) {
2863
+ const { from: from2, ...rest2 } = d;
2864
+ return { ...rest2, from: d.to, to: from2 };
2865
+ }
2866
+ if (isClipDecl(d)) {
2867
+ const { from: from2, ...rest2 } = d;
2868
+ return { ...rest2, from: d.to, to: from2 };
2869
+ }
2870
+ const { from, ...rest } = d;
2871
+ return { ...rest, from: d.to, to: from ?? 0 };
2872
+ });
2873
+ }
2874
+ var EMPTY_BATCH = { anims: [], composited: true, nonComposited: [] };
2875
+ function routeTransitionBatches(transition, targets, opts) {
2876
+ const spec = appTransition(transition, opts);
2877
+ const direction = opts?.direction ?? "forward";
2878
+ const role = spec.role ?? "push";
2879
+ const effective = role === "dismiss" ? { enter: spec.enter, exit: spec.exit, durationMs: spec.durationMs, opaque: spec.opaque } : direction === "back" ? { enter: reverseDecls(spec.exit), exit: reverseDecls(spec.enter), durationMs: spec.durationMs, opaque: spec.opaque } : { enter: spec.enter, exit: spec.exit, durationMs: spec.durationMs, opaque: spec.opaque };
2880
+ const name = Object.keys(APP_TRANSITION_MAP).includes(transition) ? transition : "none";
2881
+ const incoming = targets.incoming !== void 0 && effective.enter.length > 0 ? compileAnimations(effective.enter, { nodeId: targets.incoming }) : EMPTY_BATCH;
2882
+ const outgoing = targets.outgoing !== void 0 && effective.exit.length > 0 ? compileAnimations(effective.exit, { nodeId: targets.outgoing }) : EMPTY_BATCH;
2883
+ return { incoming, outgoing, durationMs: spec.durationMs, opaque: spec.opaque, direction, transition: name, role };
2884
+ }
2885
+ var ESCAPE_KINDS = [
2886
+ "custom-easing",
2887
+ "external-driver",
2888
+ "layout-property",
2889
+ "cross-property-timeline",
2890
+ "platform-mixing",
2891
+ "other"
2892
+ ];
2893
+ var ESCAPE_RATIO_TARGET = 0.05;
2894
+ var EscapeRegistry = class {
2895
+ constructor() {
2896
+ this.records = [];
2897
+ this.declarativeCount = 0;
2898
+ }
2899
+ /** 登记一条逃生口(**三要素缺失当场抛错**——"没有理由的逃生口"是设计泄漏,不许静默通过) */
2900
+ register(r) {
2901
+ if (!r || typeof r !== "object") throw new Error("\u9003\u751F\u53E3\u767B\u8BB0\u9700\u8981\u5BF9\u8C61\uFF08{kind, detail, reason, behaviorRisk}\uFF09");
2902
+ if (!ESCAPE_KINDS.includes(r.kind)) {
2903
+ throw new Error(`\u672A\u77E5\u9003\u751F\u53E3\u7C7B\u522B \`${r.kind}\`\uFF08\u5408\u6CD5\u503C\uFF1A${ESCAPE_KINDS.join(" / ")}\uFF09`);
2904
+ }
2905
+ for (const f of ["detail", "reason", "behaviorRisk"]) {
2906
+ const v = r[f];
2907
+ if (typeof v !== "string" || v.trim().length === 0) {
2908
+ throw new Error(
2909
+ `\u9003\u751F\u53E3\u767B\u8BB0\u7F3A\u5C11 \`${f}\`\uFF08${r.kind}\uFF09\u2014\u2014` + (f === "behaviorRisk" ? '\xA74.2 \u8981\u6C42\u5199\u660E"\u884C\u4E3A\u53EF\u80FD\u4E0D\u4E00\u81F4"\u7684\u5177\u4F53\u5F62\u6001\uFF08degraded \u7684\u5B9A\u4E49\uFF09' : f === "reason" ? '\u6CA1\u6709\u7406\u7531\u7684\u9003\u751F\u53E3\u662F\u8BBE\u8BA1\u6CC4\u6F0F\uFF0C\u5FC5\u987B\u5199\u6E05"\u4E3A\u4EC0\u4E48\u5C01\u95ED\u96C6\u4E0D\u591F"' : '\u5FC5\u987B\u5199\u6E05"\u5177\u4F53\u5728\u505A\u4EC0\u4E48"')
2910
+ );
2911
+ }
2912
+ }
2913
+ this.records.push({ ...r });
2914
+ }
2915
+ /** 记录声明式使用量(由 `compileAnimations` 在**注入本注册表**时调用) */
2916
+ noteDeclarative(n) {
2917
+ if (Number.isFinite(n) && n > 0) this.declarativeCount += n;
2918
+ }
2919
+ /** 全部登记(只读视图) */
2920
+ list() {
2921
+ return this.records;
2922
+ }
2923
+ /** ★聚合(degraded 单列;**全部类别都出现**,零值也列出——不给人留"未归类"的想象空间) */
2924
+ summary() {
2925
+ const byKind = Object.fromEntries(ESCAPE_KINDS.map((k) => [k, 0]));
2926
+ for (const r of this.records) byKind[r.kind] += 1;
2927
+ const total = this.records.length;
2928
+ const denom = this.declarativeCount + total;
2929
+ return {
2930
+ total,
2931
+ declaratives: this.declarativeCount,
2932
+ byKind,
2933
+ // §4.2:degraded 是**最高危险度**,单列(不与其它类别混)——这里就是那条单列表
2934
+ degraded: this.records.slice(),
2935
+ ratio: denom > 0 ? total / denom : 0
2936
+ };
2937
+ }
2938
+ /** ★可读报告(degraded **单独一节**,不与类别汇总混排) */
2939
+ format() {
2940
+ const s = this.summary();
2941
+ const lines = [];
2942
+ lines.push("\u2550\u2550\u2550 Morpheus \u9003\u751F\u53E3\u62A5\u544A \u2550\u2550\u2550");
2943
+ lines.push(
2944
+ `\u58F0\u660E\u5F0F ${s.declaratives} \u6761 \xB7 \u9003\u751F\u53E3 ${s.total} \u6761 \xB7 \u7387 ${s.total === 0 ? "0%" : `${(s.ratio * 100).toFixed(1)}%`}\uFF08\u76EE\u6807 < ${(ESCAPE_RATIO_TARGET * 100).toFixed(0)}%\uFF09`
2945
+ );
2946
+ lines.push("");
2947
+ if (s.total === 0) {
2948
+ lines.push("\u65E0\u9003\u751F\u53E3\u767B\u8BB0\uFF08\u5168\u90E8\u8D70\u58F0\u660E\u5F0F\u8DEF\u5F84\uFF09\u3002");
2949
+ } else {
2950
+ lines.push("\u2605 degraded\uFF08\u884C\u4E3A\u53EF\u80FD\u4E0E\u58F0\u660E\u5F0F\u8DEF\u5F84\u4E0D\u4E00\u81F4\u2014\u2014\xA74.2 \u6700\u9AD8\u5371\u9669\u5EA6\uFF0C\u5355\u5217\uFF09:");
2951
+ for (const r of s.degraded) {
2952
+ lines.push(` \xB7 [${r.kind}] ${r.detail}`);
2953
+ lines.push(` \u7406\u7531\uFF1A${r.reason}`);
2954
+ lines.push(` \u98CE\u9669\uFF1A${r.behaviorRisk}`);
2955
+ if (r.site) lines.push(` \u51FA\u5904\uFF1A${r.site}`);
2956
+ }
2957
+ lines.push("");
2958
+ }
2959
+ lines.push('\u6309\u7C7B\u522B\u6C47\u603B\uFF08\u542B\u96F6\u503C\u2014\u2014"\u672A\u5F52\u7C7B"\u4E0D\u5B58\u5728\uFF0C\u53EA\u6709 other \u515C\u5E95\uFF09:');
2960
+ for (const k of ESCAPE_KINDS) lines.push(` \xB7 ${k}: ${s.byKind[k]}`);
2961
+ if (s.ratio > ESCAPE_RATIO_TARGET) {
2962
+ lines.push("");
2963
+ lines.push(`\u26A0 \u9003\u751F\u53E3\u7387 ${(s.ratio * 100).toFixed(1)}% \u8D85\u8FC7\u76EE\u6807 ${(ESCAPE_RATIO_TARGET * 100).toFixed(0)}%\u2014\u2014\u4F18\u5148\u770B\u80FD\u5426\u628A\u9AD8\u9891\u7C7B\u522B\u8865\u6210\u9884\u8BBE\uFF08\u9884\u8BBE\u4F18\u5148\u4E8E\u9003\u751F\u53E3\uFF09`);
2964
+ }
2965
+ return lines.join("\n");
2966
+ }
2967
+ /** 清空(**测试隔离用**——跨用例共享状态必须可归零,本仓纪律) */
2968
+ reset() {
2969
+ this.records = [];
2970
+ this.declarativeCount = 0;
2971
+ }
2972
+ };
2973
+ var escapes = new EscapeRegistry();
2974
+
2975
+ // src/app-navigation.ts
2976
+ function createAppNavigation(opts) {
2977
+ if (!opts.screens && !opts.routes) {
2978
+ throw new Error("[app-navigation] \u9700\u8981 screens \u6216 routes \u4E4B\u4E00\uFF08\u5C4F\u6CE8\u518C\u8868\u7684\u6765\u6E90\u2014\u2014\u4E0D\u731C\uFF09");
2979
+ }
2980
+ const screens = opts.screens ?? screensFromRoutes(opts.routes);
2981
+ if (Object.keys(screens).length === 0) {
2982
+ throw new Error("[app-navigation] \u5C4F\u6CE8\u518C\u8868\u4E3A\u7A7A\u2014\u2014\u68C0\u67E5 routes/screens \u662F\u5426\u4E3A\u7A7A");
2983
+ }
2984
+ const stack = createAppStack({ screens, ...opts.policy ? { policy: opts.policy } : {} });
2985
+ const ports = createHostScreenPorts({ invoke: opts.invoke });
2986
+ const executor = createScreenExecutor({
2987
+ host: ports.tree,
2988
+ anim: ports.anim,
2989
+ ...opts.contentOf ? { contentOf: opts.contentOf } : {},
2990
+ plan: (t, targets, o) => routeTransitionBatches(t, targets, o ?? {}),
2991
+ onScreenMounted: opts.onScreenMounted ?? ((id) => stack.markRebuilt(id)),
2992
+ ...opts.onEvent ? { onEvent: opts.onEvent } : {}
2993
+ });
2994
+ const autoPump = opts.autoPump !== false;
2995
+ const adapter = createAppNavigationAdapter({
2996
+ stack,
2997
+ screens,
2998
+ pump: autoPump ? () => executor.applyCommands(stack.drainCommands()) : async () => {
2999
+ }
3000
+ });
3001
+ return {
3002
+ stack,
3003
+ executor,
3004
+ adapter,
3005
+ flush: () => adapter.flush(),
3006
+ hostStats: () => {
3007
+ try {
3008
+ return JSON.parse(opts.invoke("screen.stats", "null"));
3009
+ } catch (e) {
3010
+ return { error: String(e) };
3011
+ }
3012
+ }
3013
+ };
3014
+ }
3015
+ export {
3016
+ createAppNavigation
3017
+ };