ice-render 2.2.0 → 2.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/dist/index.cjs +2 -2
  2. package/dist/index.mjs +2 -2
  3. package/dist/index.umd.js +2 -2
  4. package/dist/types/FrameManager.d.mts +16 -1
  5. package/dist/types/FrameManager.d.ts +16 -1
  6. package/dist/types/ICE.d.mts +93 -0
  7. package/dist/types/ICE.d.ts +93 -0
  8. package/dist/types/animation/AnimationManager.d.mts +117 -16
  9. package/dist/types/animation/AnimationManager.d.ts +117 -16
  10. package/dist/types/animation/AnimationTimeline.d.mts +77 -0
  11. package/dist/types/animation/AnimationTimeline.d.ts +77 -0
  12. package/dist/types/animation/easing-registry.d.mts +37 -0
  13. package/dist/types/animation/easing-registry.d.ts +37 -0
  14. package/dist/types/animation/interpolators.d.mts +34 -0
  15. package/dist/types/animation/interpolators.d.ts +34 -0
  16. package/dist/types/animation/validate-animations.d.mts +55 -0
  17. package/dist/types/animation/validate-animations.d.ts +55 -0
  18. package/dist/types/event/DOMEventDispatcher.d.mts +10 -0
  19. package/dist/types/event/DOMEventDispatcher.d.ts +10 -0
  20. package/dist/types/export/SvgExporter.d.mts +9 -2
  21. package/dist/types/export/SvgExporter.d.ts +9 -2
  22. package/dist/types/export/compose-layers.d.mts +30 -0
  23. package/dist/types/export/compose-layers.d.ts +30 -0
  24. package/dist/types/graphic/ICEComponent.d.mts +37 -1
  25. package/dist/types/graphic/ICEComponent.d.ts +37 -1
  26. package/dist/types/graphic/container/ICEGroup.d.mts +3 -1
  27. package/dist/types/graphic/container/ICEGroup.d.ts +3 -1
  28. package/dist/types/graphic/link/ICEPolyLine.d.mts +9 -1
  29. package/dist/types/graphic/link/ICEPolyLine.d.ts +9 -1
  30. package/dist/types/graphic/shape/ICEEllipse.d.mts +3 -1
  31. package/dist/types/graphic/shape/ICEEllipse.d.ts +3 -1
  32. package/dist/types/graphic/shape/ICEIsogon.d.mts +3 -1
  33. package/dist/types/graphic/shape/ICEIsogon.d.ts +3 -1
  34. package/dist/types/graphic/shape/ICERose.d.mts +3 -1
  35. package/dist/types/graphic/shape/ICERose.d.ts +3 -1
  36. package/dist/types/graphic/shape/ICEStar.d.mts +3 -1
  37. package/dist/types/graphic/shape/ICEStar.d.ts +3 -1
  38. package/dist/types/graphic/text/ICEText.d.mts +6 -0
  39. package/dist/types/graphic/text/ICEText.d.ts +6 -0
  40. package/dist/types/index.d.mts +9 -0
  41. package/dist/types/index.d.ts +9 -0
  42. package/dist/types/util/data-util.d.mts +10 -0
  43. package/dist/types/util/data-util.d.ts +10 -0
  44. package/dist/types/util/errors.d.mts +4 -0
  45. package/dist/types/util/errors.d.ts +4 -0
  46. package/package.json +4 -2
@@ -0,0 +1,77 @@
1
+ /**
2
+ * 动画时间轴:把「谁、在什么时刻、动什么」组织成一条可播放/暂停/重播的轨道集合。
3
+ *
4
+ * 为什么要它:`props.animations` 是**每条属性各自为政**的(各自 duration / easing / 自己的 startTime),
5
+ * 表达"卡片 A 入场 → 200ms 后卡片 B 跟上 → 一起停住"这类编排时,应用只能手算 delay —— 这正是
6
+ * Agent / 低代码最不擅长的部分(算时间)。
7
+ *
8
+ * 设计取向(与 18 §3.1 的"不引入新时间模型"一致):时间轴**不是新的求值器**,它只是调度器 ——
9
+ * `play()` 时把每条轨道折算成 `delay` 写进组件的 `props.animations`,剩下的推进仍由 `AnimationManager`
10
+ * 按帧/按时间完成。因此它与既有的缓动、关键帧、量化、缓存复用、空闲停帧完全兼容。
11
+ *
12
+ * ```js
13
+ * const timeline = ice.animationManager.timeline();
14
+ * timeline
15
+ * .add(cardA, { 'transform.translate': { from: [-40, 0], to: [0, 0], duration: 400 } }, { at: 0 })
16
+ * .add(cardB, { 'transform.translate': { from: [-40, 0], to: [0, 0], duration: 400 } }, { at: '+=120' })
17
+ * .stagger(rows, { 'style.globalAlpha': { from: 0, to: 1, duration: 240 } }, { each: 60, at: 300 });
18
+ * timeline.play();
19
+ * // 点击重播:
20
+ * timeline.restart();
21
+ * ```
22
+ */
23
+ import AnimationManager from './AnimationManager.mjs';
24
+ export type TimelineOptions = {
25
+ /** 起始时刻:绝对毫秒,或 `'+=N'`(相对**上一条轨道**的时间) */
26
+ at?: number | string;
27
+ /** `stagger` 用:每条之间的间隔毫秒 */
28
+ each?: number;
29
+ };
30
+ export default class AnimationTimeline {
31
+ private manager;
32
+ private tracks;
33
+ private cursor;
34
+ private playing;
35
+ private paused;
36
+ private pending;
37
+ private finishedPromise;
38
+ private resolveFinished;
39
+ constructor(manager: AnimationManager);
40
+ /** 时间轴总时长(最后一条轨道的起始 + 它自己的时长;用于排版/展示)。 */
41
+ get duration(): number;
42
+ isPlaying(): boolean;
43
+ /** 全部轨道跑完时 resolve(`play()` 之前拿到的是同一个 Promise)。 */
44
+ get finished(): Promise<void>;
45
+ /**
46
+ * 加一条轨道:把 `config`(`{属性路径: 动画配置}`)合并进组件的 `props.animations`,
47
+ * 并记录这条轨道的起始时刻。
48
+ */
49
+ add(component: any, config: Record<string, any>, options?: TimelineOptions): this;
50
+ /**
51
+ * 错峰:同一份配置按 `each` 毫秒依次加到一组组件上("卡片依次滑入")。
52
+ * 返回 this 以便链式继续 `add`。
53
+ */
54
+ stagger(components: any[], config: Record<string, any>, options?: TimelineOptions): this;
55
+ /**
56
+ * 播放:把每条轨道注册进 `AnimationManager`(按 `at` 折算 delay)并唤醒帧循环。
57
+ * 重复调用是幂等的(已在播放则什么都不做)。
58
+ */
59
+ play(): this;
60
+ /** 暂停整条时间轴(等价于 `AnimationManager.pause()`)。 */
61
+ pause(): this;
62
+ /** 继续(从暂停处接上)。 */
63
+ resume(): this;
64
+ /**
65
+ * 停止:把本时间轴注册的动画从管理器里摘掉(还原 onComplete 包装),组件停在当前值。
66
+ */
67
+ stop(): this;
68
+ /**
69
+ * 重播:重置每条动画的运行时状态(startTime / finished / 轮次 / 降频节拍)后重新播放。
70
+ * 这是"点击重播"的直接入口。
71
+ */
72
+ restart(): this;
73
+ /** `'+=N'` → 游标 + N;数字 → 原值;非法 → 当前游标。 */
74
+ private __resolveAt;
75
+ private __onTrackKeyDone;
76
+ private __resolveIfDone;
77
+ }
@@ -0,0 +1,77 @@
1
+ /**
2
+ * 动画时间轴:把「谁、在什么时刻、动什么」组织成一条可播放/暂停/重播的轨道集合。
3
+ *
4
+ * 为什么要它:`props.animations` 是**每条属性各自为政**的(各自 duration / easing / 自己的 startTime),
5
+ * 表达"卡片 A 入场 → 200ms 后卡片 B 跟上 → 一起停住"这类编排时,应用只能手算 delay —— 这正是
6
+ * Agent / 低代码最不擅长的部分(算时间)。
7
+ *
8
+ * 设计取向(与 18 §3.1 的"不引入新时间模型"一致):时间轴**不是新的求值器**,它只是调度器 ——
9
+ * `play()` 时把每条轨道折算成 `delay` 写进组件的 `props.animations`,剩下的推进仍由 `AnimationManager`
10
+ * 按帧/按时间完成。因此它与既有的缓动、关键帧、量化、缓存复用、空闲停帧完全兼容。
11
+ *
12
+ * ```js
13
+ * const timeline = ice.animationManager.timeline();
14
+ * timeline
15
+ * .add(cardA, { 'transform.translate': { from: [-40, 0], to: [0, 0], duration: 400 } }, { at: 0 })
16
+ * .add(cardB, { 'transform.translate': { from: [-40, 0], to: [0, 0], duration: 400 } }, { at: '+=120' })
17
+ * .stagger(rows, { 'style.globalAlpha': { from: 0, to: 1, duration: 240 } }, { each: 60, at: 300 });
18
+ * timeline.play();
19
+ * // 点击重播:
20
+ * timeline.restart();
21
+ * ```
22
+ */
23
+ import AnimationManager from './AnimationManager';
24
+ export type TimelineOptions = {
25
+ /** 起始时刻:绝对毫秒,或 `'+=N'`(相对**上一条轨道**的时间) */
26
+ at?: number | string;
27
+ /** `stagger` 用:每条之间的间隔毫秒 */
28
+ each?: number;
29
+ };
30
+ export default class AnimationTimeline {
31
+ private manager;
32
+ private tracks;
33
+ private cursor;
34
+ private playing;
35
+ private paused;
36
+ private pending;
37
+ private finishedPromise;
38
+ private resolveFinished;
39
+ constructor(manager: AnimationManager);
40
+ /** 时间轴总时长(最后一条轨道的起始 + 它自己的时长;用于排版/展示)。 */
41
+ get duration(): number;
42
+ isPlaying(): boolean;
43
+ /** 全部轨道跑完时 resolve(`play()` 之前拿到的是同一个 Promise)。 */
44
+ get finished(): Promise<void>;
45
+ /**
46
+ * 加一条轨道:把 `config`(`{属性路径: 动画配置}`)合并进组件的 `props.animations`,
47
+ * 并记录这条轨道的起始时刻。
48
+ */
49
+ add(component: any, config: Record<string, any>, options?: TimelineOptions): this;
50
+ /**
51
+ * 错峰:同一份配置按 `each` 毫秒依次加到一组组件上("卡片依次滑入")。
52
+ * 返回 this 以便链式继续 `add`。
53
+ */
54
+ stagger(components: any[], config: Record<string, any>, options?: TimelineOptions): this;
55
+ /**
56
+ * 播放:把每条轨道注册进 `AnimationManager`(按 `at` 折算 delay)并唤醒帧循环。
57
+ * 重复调用是幂等的(已在播放则什么都不做)。
58
+ */
59
+ play(): this;
60
+ /** 暂停整条时间轴(等价于 `AnimationManager.pause()`)。 */
61
+ pause(): this;
62
+ /** 继续(从暂停处接上)。 */
63
+ resume(): this;
64
+ /**
65
+ * 停止:把本时间轴注册的动画从管理器里摘掉(还原 onComplete 包装),组件停在当前值。
66
+ */
67
+ stop(): this;
68
+ /**
69
+ * 重播:重置每条动画的运行时状态(startTime / finished / 轮次 / 降频节拍)后重新播放。
70
+ * 这是"点击重播"的直接入口。
71
+ */
72
+ restart(): this;
73
+ /** `'+=N'` → 游标 + N;数字 → 原值;非法 → 当前游标。 */
74
+ private __resolveAt;
75
+ private __onTrackKeyDone;
76
+ private __resolveIfDone;
77
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * 缓动注册表:内置缓动 + **应用层自定义缓动**。
3
+ *
4
+ * 为什么要开放这一层(用户目标②「应用层有机会自定义动画」):
5
+ * 内置只有十几个 easing,产品想表达自己的手感(品牌回弹、机械式阶跃、物理曲线)时,
6
+ * 最自然的方式是**给一个函数**,而不是"只能从枚举里挑"。
7
+ *
8
+ * 用法:
9
+ * ```js
10
+ * ICE.registerEasing('brandSpring', (t) => 1 - Math.pow(1 - t, 3) * Math.cos(t * 6));
11
+ * new ICERect({ animations: { left: { from: 0, to: 100, duration: 400, easing: 'brandSpring' } } });
12
+ * // 也可以直接传函数(只对这一条动画生效,不用注册):
13
+ * animations: { left: { from: 0, to: 100, duration: 400, easing: (t) => t * t } }
14
+ * ```
15
+ *
16
+ * 注册的**不覆盖内置**(内置是引擎契约的一部分,被偷偷替换会让同一份文档在不同应用里表现不同);
17
+ * 重名注册会明确抛错(`ICE_ANIM_EASING_NAME_CONFLICT`)。
18
+ */
19
+ import { EasingProgress } from './Easing.mjs';
20
+ /**
21
+ * 注册一个自定义缓动。
22
+ *
23
+ * @param name 缓动名(非空字符串;不能与**内置**缓动重名,也不能重复注册同一个名字)
24
+ * @param fn 归一化进度函数 `(t) => number`
25
+ */
26
+ export declare function registerEasing(name: string, fn: EasingProgress): void;
27
+ /** 注销自定义缓动(内置缓动不可注销);返回是否真的移除了。 */
28
+ export declare function unregisterEasing(name: string): boolean;
29
+ /**
30
+ * 解析缓动:**函数**原样返回(临时自定义)、字符串查内置 → 自定义;没有就返回 null
31
+ * (调用方决定是回退 linear 还是报错)。
32
+ */
33
+ export declare function resolveEasing(nameOrFn: any): EasingProgress | null;
34
+ /** 当前可用的缓动名(内置 + 自定义),用于校验提示与文档。 */
35
+ export declare function easingNames(): string[];
36
+ /** 只列出应用层注册的缓动名(调试 / 测试用)。 */
37
+ export declare function customEasingNames(): string[];
@@ -0,0 +1,37 @@
1
+ /**
2
+ * 缓动注册表:内置缓动 + **应用层自定义缓动**。
3
+ *
4
+ * 为什么要开放这一层(用户目标②「应用层有机会自定义动画」):
5
+ * 内置只有十几个 easing,产品想表达自己的手感(品牌回弹、机械式阶跃、物理曲线)时,
6
+ * 最自然的方式是**给一个函数**,而不是"只能从枚举里挑"。
7
+ *
8
+ * 用法:
9
+ * ```js
10
+ * ICE.registerEasing('brandSpring', (t) => 1 - Math.pow(1 - t, 3) * Math.cos(t * 6));
11
+ * new ICERect({ animations: { left: { from: 0, to: 100, duration: 400, easing: 'brandSpring' } } });
12
+ * // 也可以直接传函数(只对这一条动画生效,不用注册):
13
+ * animations: { left: { from: 0, to: 100, duration: 400, easing: (t) => t * t } }
14
+ * ```
15
+ *
16
+ * 注册的**不覆盖内置**(内置是引擎契约的一部分,被偷偷替换会让同一份文档在不同应用里表现不同);
17
+ * 重名注册会明确抛错(`ICE_ANIM_EASING_NAME_CONFLICT`)。
18
+ */
19
+ import { EasingProgress } from './Easing';
20
+ /**
21
+ * 注册一个自定义缓动。
22
+ *
23
+ * @param name 缓动名(非空字符串;不能与**内置**缓动重名,也不能重复注册同一个名字)
24
+ * @param fn 归一化进度函数 `(t) => number`
25
+ */
26
+ export declare function registerEasing(name: string, fn: EasingProgress): void;
27
+ /** 注销自定义缓动(内置缓动不可注销);返回是否真的移除了。 */
28
+ export declare function unregisterEasing(name: string): boolean;
29
+ /**
30
+ * 解析缓动:**函数**原样返回(临时自定义)、字符串查内置 → 自定义;没有就返回 null
31
+ * (调用方决定是回退 linear 还是报错)。
32
+ */
33
+ export declare function resolveEasing(nameOrFn: any): EasingProgress | null;
34
+ /** 当前可用的缓动名(内置 + 自定义),用于校验提示与文档。 */
35
+ export declare function easingNames(): string[];
36
+ /** 只列出应用层注册的缓动名(调试 / 测试用)。 */
37
+ export declare function customEasingNames(): string[];
@@ -0,0 +1,34 @@
1
+ /**
2
+ * 可插值取值的**唯一判定与求值入口**:数值 / 等长数字数组 / **颜色** / 带单位数字串。
3
+ *
4
+ * 为什么要有颜色:`fillStyle` 从红渐变到蓝是界面里最常见的动效之一,而 canvas 只会画字符串。
5
+ * 早先引擎一律拒绝非数值取值(避免写出 NaN 破坏矩阵),代价就是"颜色动画做不了"。
6
+ * 这里把"哪些类型可插值、怎么插值"收敛到一个模块:校验器(`validateAnimations`)与
7
+ * 运行时(`AnimationManager`)都用它,避免"校验通过但跑起来算错"。
8
+ *
9
+ * 支持的类型:
10
+ * - `number`:线性插值;
11
+ * - `number[]`(等长):逐元素线性插值(矩阵/位移/缩放都走这条);
12
+ * - 颜色字符串:`#rgb` / `#rgba` / `#rrggbb` / `#rrggbbaa` / `rgb()` / `rgba()`(两端必须都是颜色);
13
+ * - 带单位数字串:`'12px'` / `'1.5em'` 等(**单位必须一致**,否则视为不可插值)。
14
+ *
15
+ * 注意:插值在 **sRGB 数值空间**做(与 CSS `transition` 的默认行为一致),不做 OKLab 之类感知均匀空间。
16
+ */
17
+ export type AnimationValueKind = 'number' | 'array' | 'color' | 'length' | null;
18
+ /** 解析颜色字符串为 `[r, g, b, a]`(a ∈ [0,1]);不是颜色返回 null。 */
19
+ export declare function parseColor(value: any): [number, number, number, number] | null;
20
+ /** 把 `[r,g,b,a]` 写回 `rgba(...)`(alpha 为 1 时给 `rgb(...)`,与输入形态无关,输出统一)。 */
21
+ export declare function formatColor(rgba: [number, number, number, number]): string;
22
+ /** 解析带单位数字串(`'12px'` / `'1.5em'`)→ `{ number, unit }`;不是就返回 null。 */
23
+ export declare function parseNumberWithUnit(value: any): {
24
+ number: number;
25
+ unit: string;
26
+ } | null;
27
+ /** 取值种类(`null` = 不可插值)。 */
28
+ export declare function classifyValue(value: any): AnimationValueKind;
29
+ /** 两个取值能否互相插值(同型;数组等长;带单位数字串单位一致)。 */
30
+ export declare function isInterpolatable(from: any, to: any): boolean;
31
+ /**
32
+ * 按进度 `p` 求值。不可插值时返回 `from`(调用方负责在校验阶段拦住非法配置)。
33
+ */
34
+ export declare function interpolateValue(from: any, to: any, p: number): any;
@@ -0,0 +1,34 @@
1
+ /**
2
+ * 可插值取值的**唯一判定与求值入口**:数值 / 等长数字数组 / **颜色** / 带单位数字串。
3
+ *
4
+ * 为什么要有颜色:`fillStyle` 从红渐变到蓝是界面里最常见的动效之一,而 canvas 只会画字符串。
5
+ * 早先引擎一律拒绝非数值取值(避免写出 NaN 破坏矩阵),代价就是"颜色动画做不了"。
6
+ * 这里把"哪些类型可插值、怎么插值"收敛到一个模块:校验器(`validateAnimations`)与
7
+ * 运行时(`AnimationManager`)都用它,避免"校验通过但跑起来算错"。
8
+ *
9
+ * 支持的类型:
10
+ * - `number`:线性插值;
11
+ * - `number[]`(等长):逐元素线性插值(矩阵/位移/缩放都走这条);
12
+ * - 颜色字符串:`#rgb` / `#rgba` / `#rrggbb` / `#rrggbbaa` / `rgb()` / `rgba()`(两端必须都是颜色);
13
+ * - 带单位数字串:`'12px'` / `'1.5em'` 等(**单位必须一致**,否则视为不可插值)。
14
+ *
15
+ * 注意:插值在 **sRGB 数值空间**做(与 CSS `transition` 的默认行为一致),不做 OKLab 之类感知均匀空间。
16
+ */
17
+ export type AnimationValueKind = 'number' | 'array' | 'color' | 'length' | null;
18
+ /** 解析颜色字符串为 `[r, g, b, a]`(a ∈ [0,1]);不是颜色返回 null。 */
19
+ export declare function parseColor(value: any): [number, number, number, number] | null;
20
+ /** 把 `[r,g,b,a]` 写回 `rgba(...)`(alpha 为 1 时给 `rgb(...)`,与输入形态无关,输出统一)。 */
21
+ export declare function formatColor(rgba: [number, number, number, number]): string;
22
+ /** 解析带单位数字串(`'12px'` / `'1.5em'`)→ `{ number, unit }`;不是就返回 null。 */
23
+ export declare function parseNumberWithUnit(value: any): {
24
+ number: number;
25
+ unit: string;
26
+ } | null;
27
+ /** 取值种类(`null` = 不可插值)。 */
28
+ export declare function classifyValue(value: any): AnimationValueKind;
29
+ /** 两个取值能否互相插值(同型;数组等长;带单位数字串单位一致)。 */
30
+ export declare function isInterpolatable(from: any, to: any): boolean;
31
+ /**
32
+ * 按进度 `p` 求值。不可插值时返回 `from`(调用方负责在校验阶段拦住非法配置)。
33
+ */
34
+ export declare function interpolateValue(from: any, to: any, p: number): any;
@@ -0,0 +1,55 @@
1
+ import { AnimationValueKind } from './interpolators.mjs';
2
+ export declare const ICE_ANIMATION_DIAGNOSTIC_CODES: {
3
+ /** 键不是合法的属性路径(空串 / 非字符串)。 */
4
+ readonly KEY_INVALID: "ICE_ANIM_KEY_INVALID";
5
+ /** `duration` 缺失 / 非正数 / 超上限 / 不是已注册的 motion token。 */
6
+ readonly DURATION_INVALID: "ICE_ANIM_DURATION_INVALID";
7
+ /** `delay` 不是非负有限数。 */
8
+ readonly DELAY_INVALID: "ICE_ANIM_DELAY_INVALID";
9
+ /** `iterationCount` 不是 ≥1 的整数。 */
10
+ readonly ITERATION_INVALID: "ICE_ANIM_ITERATION_INVALID";
11
+ /** 缓动名既不是内置缓动,也不是主题 motion.easing 里的语义名(运行时会回退 linear)。 */
12
+ readonly EASING_UNKNOWN: "ICE_ANIM_EASING_UNKNOWN";
13
+ /** `from` / `to` 不是可插值的同型数值(数值或等长数字数组)。 */
14
+ readonly VALUE_NOT_INTERPOLATABLE: "ICE_ANIM_VALUE_NOT_INTERPOLATABLE";
15
+ /** `keyframes` 形状非法(少于 2 帧 / offset 非有限数 / 各帧取值不同型或不等长)。 */
16
+ readonly KEYFRAMES_INVALID: "ICE_ANIM_KEYFRAMES_INVALID";
17
+ /** 无限循环(`loop: true`):Agent 生成的界面里要谨慎,建议给 `iterationCount` 或可中断入口。 */
18
+ readonly INFINITE_LOOP: "ICE_ANIM_INFINITE_LOOP";
19
+ /** 该属性会改变派生参数(尺寸 / 点集 / 文本量测)→ 每帧都要重量测,性能上不划算。 */
20
+ readonly KEY_AFFECTS_MEASUREMENT: "ICE_ANIM_KEY_AFFECTS_MEASUREMENT";
21
+ /** 运行期告知:用户开启了「减少动态效果」,这条动画被折叠成终态(未播放过程)。 */
22
+ readonly REDUCED_MOTION: "ICE_ANIM_REDUCED_MOTION";
23
+ /** `direction` 不是 `normal` / `reverse` / `alternate`。 */
24
+ readonly DIRECTION_INVALID: "ICE_ANIM_DIRECTION_INVALID";
25
+ /** 生命周期回调(onStart/onUpdate/onRepeat/onComplete)抛了异常(已忽略,不打断动画)。 */
26
+ readonly CALLBACK_ERROR: "ICE_ANIM_CALLBACK_ERROR";
27
+ };
28
+ export type ICEAnimationDiagnosticCode = (typeof ICE_ANIMATION_DIAGNOSTIC_CODES)[keyof typeof ICE_ANIMATION_DIAGNOSTIC_CODES];
29
+ export interface ICEAnimationDiagnostic {
30
+ severity: 'error' | 'warning';
31
+ code: ICEAnimationDiagnosticCode;
32
+ message: string;
33
+ /** 出问题的位置:属性动画键(如 `transform.translate`),便于 Agent 精确改一处。 */
34
+ path: string;
35
+ }
36
+ export type ValidateAnimationsOptions = {
37
+ /** 主题 motion token(缺省用内置默认主题)——`duration: 'normal'` / `easing: 'out'` 这类语义名靠它解析。 */
38
+ motionTokens?: {
39
+ duration?: Record<string, any>;
40
+ easing?: Record<string, any>;
41
+ } | null;
42
+ /** 判断某个属性键是否"动画安全"(不影响派生参数);给了就会对不安全的键出性能警告。 */
43
+ isSafeKey?: ((path: string) => boolean) | null;
44
+ /** 单条动画的时长上限(ms),默认 60s;Agent 生成的长动画多半是写错了。 */
45
+ maxDuration?: number;
46
+ };
47
+ /**
48
+ * 取值种类:`number` / `array`(等长数字数组)/ `color` / `length`(带单位数字串)/ `null`(不可插值)。
49
+ * 判定与求值都转发到 `interpolators.ts`(运行时同源,避免"校验通过但跑起来算错")。
50
+ */
51
+ export declare function classifyAnimationValue(value: any): AnimationValueKind;
52
+ /** 两个取值能否互相插值(同型;数组等长;颜色互插;带单位数字串单位一致)。 */
53
+ export declare function isInterpolatablePair(from: any, to: any): boolean;
54
+ /** 校验一份 `animations` 配置(键 → 单段/关键帧配置),返回按 `path` 排序的诊断列表(无问题 = 空数组)。 */
55
+ export declare function validateAnimations(animations: any, options?: ValidateAnimationsOptions): ICEAnimationDiagnostic[];
@@ -0,0 +1,55 @@
1
+ import { AnimationValueKind } from './interpolators';
2
+ export declare const ICE_ANIMATION_DIAGNOSTIC_CODES: {
3
+ /** 键不是合法的属性路径(空串 / 非字符串)。 */
4
+ readonly KEY_INVALID: "ICE_ANIM_KEY_INVALID";
5
+ /** `duration` 缺失 / 非正数 / 超上限 / 不是已注册的 motion token。 */
6
+ readonly DURATION_INVALID: "ICE_ANIM_DURATION_INVALID";
7
+ /** `delay` 不是非负有限数。 */
8
+ readonly DELAY_INVALID: "ICE_ANIM_DELAY_INVALID";
9
+ /** `iterationCount` 不是 ≥1 的整数。 */
10
+ readonly ITERATION_INVALID: "ICE_ANIM_ITERATION_INVALID";
11
+ /** 缓动名既不是内置缓动,也不是主题 motion.easing 里的语义名(运行时会回退 linear)。 */
12
+ readonly EASING_UNKNOWN: "ICE_ANIM_EASING_UNKNOWN";
13
+ /** `from` / `to` 不是可插值的同型数值(数值或等长数字数组)。 */
14
+ readonly VALUE_NOT_INTERPOLATABLE: "ICE_ANIM_VALUE_NOT_INTERPOLATABLE";
15
+ /** `keyframes` 形状非法(少于 2 帧 / offset 非有限数 / 各帧取值不同型或不等长)。 */
16
+ readonly KEYFRAMES_INVALID: "ICE_ANIM_KEYFRAMES_INVALID";
17
+ /** 无限循环(`loop: true`):Agent 生成的界面里要谨慎,建议给 `iterationCount` 或可中断入口。 */
18
+ readonly INFINITE_LOOP: "ICE_ANIM_INFINITE_LOOP";
19
+ /** 该属性会改变派生参数(尺寸 / 点集 / 文本量测)→ 每帧都要重量测,性能上不划算。 */
20
+ readonly KEY_AFFECTS_MEASUREMENT: "ICE_ANIM_KEY_AFFECTS_MEASUREMENT";
21
+ /** 运行期告知:用户开启了「减少动态效果」,这条动画被折叠成终态(未播放过程)。 */
22
+ readonly REDUCED_MOTION: "ICE_ANIM_REDUCED_MOTION";
23
+ /** `direction` 不是 `normal` / `reverse` / `alternate`。 */
24
+ readonly DIRECTION_INVALID: "ICE_ANIM_DIRECTION_INVALID";
25
+ /** 生命周期回调(onStart/onUpdate/onRepeat/onComplete)抛了异常(已忽略,不打断动画)。 */
26
+ readonly CALLBACK_ERROR: "ICE_ANIM_CALLBACK_ERROR";
27
+ };
28
+ export type ICEAnimationDiagnosticCode = (typeof ICE_ANIMATION_DIAGNOSTIC_CODES)[keyof typeof ICE_ANIMATION_DIAGNOSTIC_CODES];
29
+ export interface ICEAnimationDiagnostic {
30
+ severity: 'error' | 'warning';
31
+ code: ICEAnimationDiagnosticCode;
32
+ message: string;
33
+ /** 出问题的位置:属性动画键(如 `transform.translate`),便于 Agent 精确改一处。 */
34
+ path: string;
35
+ }
36
+ export type ValidateAnimationsOptions = {
37
+ /** 主题 motion token(缺省用内置默认主题)——`duration: 'normal'` / `easing: 'out'` 这类语义名靠它解析。 */
38
+ motionTokens?: {
39
+ duration?: Record<string, any>;
40
+ easing?: Record<string, any>;
41
+ } | null;
42
+ /** 判断某个属性键是否"动画安全"(不影响派生参数);给了就会对不安全的键出性能警告。 */
43
+ isSafeKey?: ((path: string) => boolean) | null;
44
+ /** 单条动画的时长上限(ms),默认 60s;Agent 生成的长动画多半是写错了。 */
45
+ maxDuration?: number;
46
+ };
47
+ /**
48
+ * 取值种类:`number` / `array`(等长数字数组)/ `color` / `length`(带单位数字串)/ `null`(不可插值)。
49
+ * 判定与求值都转发到 `interpolators.ts`(运行时同源,避免"校验通过但跑起来算错")。
50
+ */
51
+ export declare function classifyAnimationValue(value: any): AnimationValueKind;
52
+ /** 两个取值能否互相插值(同型;数组等长;颜色互插;带单位数字串单位一致)。 */
53
+ export declare function isInterpolatablePair(from: any, to: any): boolean;
54
+ /** 校验一份 `animations` 配置(键 → 单段/关键帧配置),返回按 `path` 排序的诊断列表(无问题 = 空数组)。 */
55
+ export declare function validateAnimations(animations: any, options?: ValidateAnimationsOptions): ICEAnimationDiagnostic[];
@@ -45,6 +45,8 @@ declare class DOMEventDispatcher {
45
45
  * 组件先收到,总线后收到(与既有语义一致:总线始终会收到一次)。
46
46
  */
47
47
  private __dispatch;
48
+ /** 只派发给指定组件(不触发总线)——"拖拽归属"补派抬起事件用。 */
49
+ private __dispatchToComponentOnly;
48
50
  /**
49
51
  * 取 canvas 矩形用于坐标换算。
50
52
  *
@@ -58,6 +60,14 @@ declare class DOMEventDispatcher {
58
60
  * 相对每帧渲染可忽略(见 AGENTS「性能相关铁律」)。
59
61
  */
60
62
  private __resolveCanvasRect;
63
+ /**
64
+ * 事件目标是否属于「别的 canvas」(同页另一个 ICE 实例的绘制面)。
65
+ *
66
+ * - 目标是本实例的 canvas、或本实例 canvas 内的元素 → 不是"外来"事件;
67
+ * - 目标不是 canvas(body / window / 工具栏按钮等)→ 不是"外来"事件(键盘、合成事件照旧);
68
+ * - 目标正是**另一个 canvas 元素** → 外来事件,本实例应忽略。
69
+ */
70
+ private __isForeignCanvasTarget;
61
71
  set stopped(flag: boolean);
62
72
  get stopped(): boolean;
63
73
  /**
@@ -45,6 +45,8 @@ declare class DOMEventDispatcher {
45
45
  * 组件先收到,总线后收到(与既有语义一致:总线始终会收到一次)。
46
46
  */
47
47
  private __dispatch;
48
+ /** 只派发给指定组件(不触发总线)——"拖拽归属"补派抬起事件用。 */
49
+ private __dispatchToComponentOnly;
48
50
  /**
49
51
  * 取 canvas 矩形用于坐标换算。
50
52
  *
@@ -58,6 +60,14 @@ declare class DOMEventDispatcher {
58
60
  * 相对每帧渲染可忽略(见 AGENTS「性能相关铁律」)。
59
61
  */
60
62
  private __resolveCanvasRect;
63
+ /**
64
+ * 事件目标是否属于「别的 canvas」(同页另一个 ICE 实例的绘制面)。
65
+ *
66
+ * - 目标是本实例的 canvas、或本实例 canvas 内的元素 → 不是"外来"事件;
67
+ * - 目标不是 canvas(body / window / 工具栏按钮等)→ 不是"外来"事件(键盘、合成事件照旧);
68
+ * - 目标正是**另一个 canvas 元素** → 外来事件,本实例应忽略。
69
+ */
70
+ private __isForeignCanvasTarget;
61
71
  set stopped(flag: boolean);
62
72
  get stopped(): boolean;
63
73
  /**
@@ -50,5 +50,12 @@ export type SvgExportResult = {
50
50
  *
51
51
  * @param target ICE 实例(导出整幅画布)或任意组件(导出它的子树)
52
52
  */
53
- export declare function exportSvg(target: any, options?: SvgExportOptions): string;
54
- export declare function exportSvgResult(target: any, options?: SvgExportOptions): SvgExportResult;
53
+ /**
54
+ * 导出场景为 SVG 字符串。
55
+ *
56
+ * `target` 可以是 **ICE 实例**、**任意组件**,也可以是**这两个的数组**(多层合成):
57
+ * 分层渲染(静态层 + 动画层)导出时按数组顺序叠加,各层的世界坐标共用同一套 `worldToView`。
58
+ * 单层(传单个 target)的输出与历史版本逐字节一致。
59
+ */
60
+ export declare function exportSvg(target: any | any[], options?: SvgExportOptions): string;
61
+ export declare function exportSvgResult(target: any | any[], options?: SvgExportOptions): SvgExportResult;
@@ -50,5 +50,12 @@ export type SvgExportResult = {
50
50
  *
51
51
  * @param target ICE 实例(导出整幅画布)或任意组件(导出它的子树)
52
52
  */
53
- export declare function exportSvg(target: any, options?: SvgExportOptions): string;
54
- export declare function exportSvgResult(target: any, options?: SvgExportOptions): SvgExportResult;
53
+ /**
54
+ * 导出场景为 SVG 字符串。
55
+ *
56
+ * `target` 可以是 **ICE 实例**、**任意组件**,也可以是**这两个的数组**(多层合成):
57
+ * 分层渲染(静态层 + 动画层)导出时按数组顺序叠加,各层的世界坐标共用同一套 `worldToView`。
58
+ * 单层(传单个 target)的输出与历史版本逐字节一致。
59
+ */
60
+ export declare function exportSvg(target: any | any[], options?: SvgExportOptions): string;
61
+ export declare function exportSvgResult(target: any | any[], options?: SvgExportOptions): SvgExportResult;
@@ -0,0 +1,30 @@
1
+ export type ComposeLayersOptions = {
2
+ /** 输出类型(`toDataURL` 的 MIME),默认 `image/png` */
3
+ type?: string;
4
+ /** 有损格式的质量 0~1(`image/jpeg` / `image/webp` 用) */
5
+ quality?: number;
6
+ /** 背景色(如 `'#ffffff'`)。缺省不铺背景 → 保留透明度(分层图通常需要透明背景) */
7
+ background?: string | null;
8
+ /** 输出尺寸(缺省 = 各层 canvas 中最大的那一份,单位:**设备像素**) */
9
+ width?: number;
10
+ height?: number;
11
+ };
12
+ /**
13
+ * 把多层 canvas 叠到一张离屏画布上(按数组顺序,第一层在最下面)。
14
+ *
15
+ * @returns `{ canvas, ctx, width, height }`;运行时没有离屏画布能力时抛 `ICE_UNSUPPORTED_RUNTIME`。
16
+ */
17
+ export declare function composeLayersToCanvas(layers: any[], options?: ComposeLayersOptions): {
18
+ canvas: any;
19
+ ctx: any;
20
+ width: number;
21
+ height: number;
22
+ };
23
+ /**
24
+ * 多层合成为一张图(DataURL)。
25
+ *
26
+ * ```js
27
+ * const png = composeLayersDataURL([staticIce, animIce], { background: '#fff' });
28
+ * ```
29
+ */
30
+ export declare function composeLayersDataURL(layers: any[], options?: ComposeLayersOptions): string;
@@ -0,0 +1,30 @@
1
+ export type ComposeLayersOptions = {
2
+ /** 输出类型(`toDataURL` 的 MIME),默认 `image/png` */
3
+ type?: string;
4
+ /** 有损格式的质量 0~1(`image/jpeg` / `image/webp` 用) */
5
+ quality?: number;
6
+ /** 背景色(如 `'#ffffff'`)。缺省不铺背景 → 保留透明度(分层图通常需要透明背景) */
7
+ background?: string | null;
8
+ /** 输出尺寸(缺省 = 各层 canvas 中最大的那一份,单位:**设备像素**) */
9
+ width?: number;
10
+ height?: number;
11
+ };
12
+ /**
13
+ * 把多层 canvas 叠到一张离屏画布上(按数组顺序,第一层在最下面)。
14
+ *
15
+ * @returns `{ canvas, ctx, width, height }`;运行时没有离屏画布能力时抛 `ICE_UNSUPPORTED_RUNTIME`。
16
+ */
17
+ export declare function composeLayersToCanvas(layers: any[], options?: ComposeLayersOptions): {
18
+ canvas: any;
19
+ ctx: any;
20
+ width: number;
21
+ height: number;
22
+ };
23
+ /**
24
+ * 多层合成为一张图(DataURL)。
25
+ *
26
+ * ```js
27
+ * const png = composeLayersDataURL([staticIce, animIce], { background: '#fff' });
28
+ * ```
29
+ */
30
+ export declare function composeLayersDataURL(layers: any[], options?: ComposeLayersOptions): string;
@@ -408,7 +408,43 @@ declare abstract class ICEComponent extends ICEEventTarget {
408
408
  *
409
409
  * @param newState
410
410
  */
411
- setState(newState: any): void;
411
+ setState(newState: any, options?: {
412
+ paramsDirty?: boolean;
413
+ }): void;
414
+ /**
415
+ * 「动画安全键」白名单:写这些 state 键**不会改变派生参数**(尺寸 / 点集 / 文本量测),
416
+ * 因此动画/高频写值可以跳过 `paramsDirty` —— 省一次重量测,并且保住离屏位图的纯平移复用。
417
+ *
418
+ * 判定方向很重要:**未声明的键一律当作"影响派生参数"**(保守)。这样漏判只会少一点优化,
419
+ * 不会让尺寸/点集停在旧值上;第三方组件不声明就等于维持旧行为(每帧都置脏)。
420
+ * 子类覆盖时应把基类的键并入(也允许更窄,例如文本把字号/字间距/行高那类量测相关 style 键排除在外)。
421
+ */
422
+ static readonly ANIMATION_SAFE_KEYS: readonly string[];
423
+ /**
424
+ * 运行时挂上/改写一条属性动画(免"必须在构造时声明 `animations`")。
425
+ *
426
+ * 关键细节:**写时复制**。没在构造时传 `animations` 的组件,`props.animations` 继承的是
427
+ * `DEFAULT_PROPS.animations`(**冻结的共享默认对象**)—— 直接 `props.animations[key] = cfg`
428
+ * 在严格模式下会抛 "Cannot add property …: object is not extensible"。这里先把默认对象复制成
429
+ * 实例自己的、可扩展的对象再写,并顺手把组件纳入 `AnimationManager`(否则动画不会推进)。
430
+ *
431
+ * ```js
432
+ * card.setAnimation('style.globalAlpha', { from: 0, to: 1, duration: 300 });
433
+ * ```
434
+ */
435
+ setAnimation(key: string, config: any): this;
436
+ /** 运行时摘掉一条属性动画(组件没在动画里就什么都不做)。 */
437
+ removeAnimation(key: string): this;
438
+ /** 该 state 键路径是否在「动画安全键」白名单里(`transform` 这类前缀按 `transform.xxx` 匹配)。 */
439
+ isAnimationSafeKey(path: string): boolean;
440
+ /**
441
+ * 按**类**查询某个 state 键路径是否"动画安全"(不需要实例)。
442
+ *
443
+ * 给下游用(DSL / Agent 侧校验):它们手里只有类型(`ICEText` / `ICERect`…),
444
+ * 没有也不该造实例 —— 但需要给出"这个属性动画会每帧重量测"这类性能提示。
445
+ * 判定口径与实例方法完全一致(只查静态白名单)。
446
+ */
447
+ static isAnimationSafeKeyFor(ctor: any, path: string): boolean;
412
448
  /**
413
449
  * `setState` 的**前置**钩子:必须在 `merge(this.state, newState)` 之前调用(要对比新旧值)。
414
450
  *
@@ -408,7 +408,43 @@ declare abstract class ICEComponent extends ICEEventTarget {
408
408
  *
409
409
  * @param newState
410
410
  */
411
- setState(newState: any): void;
411
+ setState(newState: any, options?: {
412
+ paramsDirty?: boolean;
413
+ }): void;
414
+ /**
415
+ * 「动画安全键」白名单:写这些 state 键**不会改变派生参数**(尺寸 / 点集 / 文本量测),
416
+ * 因此动画/高频写值可以跳过 `paramsDirty` —— 省一次重量测,并且保住离屏位图的纯平移复用。
417
+ *
418
+ * 判定方向很重要:**未声明的键一律当作"影响派生参数"**(保守)。这样漏判只会少一点优化,
419
+ * 不会让尺寸/点集停在旧值上;第三方组件不声明就等于维持旧行为(每帧都置脏)。
420
+ * 子类覆盖时应把基类的键并入(也允许更窄,例如文本把字号/字间距/行高那类量测相关 style 键排除在外)。
421
+ */
422
+ static readonly ANIMATION_SAFE_KEYS: readonly string[];
423
+ /**
424
+ * 运行时挂上/改写一条属性动画(免"必须在构造时声明 `animations`")。
425
+ *
426
+ * 关键细节:**写时复制**。没在构造时传 `animations` 的组件,`props.animations` 继承的是
427
+ * `DEFAULT_PROPS.animations`(**冻结的共享默认对象**)—— 直接 `props.animations[key] = cfg`
428
+ * 在严格模式下会抛 "Cannot add property …: object is not extensible"。这里先把默认对象复制成
429
+ * 实例自己的、可扩展的对象再写,并顺手把组件纳入 `AnimationManager`(否则动画不会推进)。
430
+ *
431
+ * ```js
432
+ * card.setAnimation('style.globalAlpha', { from: 0, to: 1, duration: 300 });
433
+ * ```
434
+ */
435
+ setAnimation(key: string, config: any): this;
436
+ /** 运行时摘掉一条属性动画(组件没在动画里就什么都不做)。 */
437
+ removeAnimation(key: string): this;
438
+ /** 该 state 键路径是否在「动画安全键」白名单里(`transform` 这类前缀按 `transform.xxx` 匹配)。 */
439
+ isAnimationSafeKey(path: string): boolean;
440
+ /**
441
+ * 按**类**查询某个 state 键路径是否"动画安全"(不需要实例)。
442
+ *
443
+ * 给下游用(DSL / Agent 侧校验):它们手里只有类型(`ICEText` / `ICERect`…),
444
+ * 没有也不该造实例 —— 但需要给出"这个属性动画会每帧重量测"这类性能提示。
445
+ * 判定口径与实例方法完全一致(只查静态白名单)。
446
+ */
447
+ static isAnimationSafeKeyFor(ctor: any, path: string): boolean;
412
448
  /**
413
449
  * `setState` 的**前置**钩子:必须在 `merge(this.state, newState)` 之前调用(要对比新旧值)。
414
450
  *