Skip to main content

· 交互与动画

交互层:控制面板

ICEControlPanelManager 负责管理选中与变换工具(纯逻辑组件,无外观):

  • 两类面板默认 disable;用户 mousedown 命中可变换组件时,mouseDownHandler 根据组件类型启用对应面板:
    • component.isLine(线条类)→ 启用 LineControlPanel
    • 其它 → 启用 TransformControlPanel
  • 面板本身与手柄是 toolNodes,复用同一套组件模型(ICEControlPanel extends ICEGroup,手柄继承 ICECircle)。
  • interactive/transformable 等开关决定组件是否可交互、可变换。

拖拽与变换

  • 拖拽:mouseDown 命中后注册 mousemove/mouseupmouseMoveEvtHandlermoveGlobalPosition(见 03 嵌套坐标 里全局位移需抵消父层变换)。
  • 旋转手柄 RotateControlAFTER_MOVE 时用父组件 absoluteOrigin 计算旋转角,回写父组件 rotate
  • 键盘:keyboardEvtHandler 支持方向键步进移动(ArrowUp/Down/Left/Right)、Delete 删除组件。

连接线(Visio 风格)

ICELinkSlotManager 负责连线逻辑:

  • 5 个共享插槽ICELinkSlot,位置 T/R/B/L/C = 上/右/下/左/中心)在 ICE.init 时创建、所有可连接组件复用,不随组件增删。
  • 连线交互:拖动连线端点(ICELinkHook)→ HOOK_MOUSEMOVE 阶段用包围盒相交检测getMaxBoundingBox().isIntersect())找碰撞的 linkable 组件与插槽 → HOOK_MOUSEUP 建立或断开 links 关系。
  • 连线关系记录在 ICEPolyLine.state.links{ [端点位置]: { id, position } })。

动画

AnimationManager 订阅 ICE_FRAME_EVENT,对 animationMap 里的组件做补间:

动画配置挂在 props.animations,键可以是属性名或点路径'transform.rotate' / 'style.globalAlpha'), 取值有两种形态:

形态写法说明
单段{ from, to, duration, easing? }在两个值之间补间
关键帧{ keyframes: [{ offset, value, easing? }], duration }offset 为 0~1 时间占比,缺省按顺序均分、超出会被夹紧、乱序自动排序;easing 写在段起始帧上,只作用于「该帧 → 下一帧」这一段(未写则回落到动画级 easing);时间轴之外的取值保持首/末帧值,不外推
  • 取值可为数值等长的数字数组transform.scale / transform.translate / transform.skew 等按分量逐元素补间)。两端长度不一致或含非数字会被拒绝,并只 console.warn 一次 (不再像早期实现那样写出 NaN 破坏矩阵)。
  • 通用配置:durationdelay(延迟期内保持起始值,可做多属性错峰/多组件序列)、easingloopiterationCountroundduration / easing 可用主题 motion token 的语义名 ('normal' / 'out' / 'spring' …)。
  • 缓动分两层:EasingProgress(归一化进度函数,t: [0,1] → 进度,纯函数、不读时钟)是唯一实现; Easing(from, to, duration, startTime) 值语义,内部读 Date.now())是它的适配层,保持历史签名。 弹簧类(spring / springSoft / springSnappy,欠阻尼谐振子解析解)自带过冲,进度会短暂 > 1
  • 结束判定按已流逝时间elapsed >= duration),而不是「值是否越过 to」——否则弹簧过冲的第一帧 就会被误判成结束。到点后精确落到终点值,不留浮点残差。
  • duration 非正数/缺失时立即落到终点并结束(旧实现会算出 NaN / Infinity,当 from === to 时 比较恒为 false,动画永不结束、每帧空转 setState)。
  • 动画期间把组件 state.interactive 临时置 false保存并恢复原值,不覆盖用户显式设置的 false), 避免交互干扰属性计算;组件销毁时自动从 animationMap 摘除。
  • 默认不取整(避免 0→1 的透明度/角度/缩放被压掉),需要整数步进时显式 round: true(数组逐元素取整)。
  • fps(可选):次要动画降频,如 { duration: 900, fps: 30 }。采样按时间、跳过帧不改变曲线,终点仍精确落值。
  • 取值类型interpolators.ts 统一判定与求值):
    • 数值 / 等长数字数组(矩阵与位移);
    • 颜色#rgb / #rgba / #rrggbb / #rrggbbaa / rgb() / rgba(),两端都必须是颜色, 插值在 sRGB 数值空间做(与 CSS transition 一致),输出统一为 rgb() / rgba()
    • 带单位数字串'12px' / '1.5em'单位必须一致)。
  • 自定义缓动easing 可以直接给函数 (t) => number(只对这条动画生效), 或先 ICE.registerEasing(name, fn) 再按名字用(内置缓动不可覆盖,重名抛 ICE_ANIM_EASING_NAME_CONFLICT)。
  • 生命周期回调onStart / onUpdate / onRepeat / onComplete,回调收到 { component, key, value, progress, iteration, animation };回调抛异常只记一条 ICE_ANIM_CALLBACK_ERROR 诊断并忽略,不会打断帧循环
  • 往返方向direction: 'normal' | 'reverse' | 'alternate'——reverse 从 to 走到 from; alternateloop / iterationCount 组合即 yoyo(奇数轮反向)。
  • 运行时 APIcomponent.setAnimation(key, cfg) / removeAnimation(key) —— 免"必须在构造时声明 animations", 内部做写时复制(没声明过的组件继承的是冻结的共享默认对象,直接写会抛 object is not extensible); ice.animationManager.replay(component)(重播)与 isAnimating(component)(查询)。
  • 时间轴(编排)ice.animationManager.timeline() —— add(component, cfg, { at })at 为绝对毫秒或 '+=N')、 stagger(components, cfg, { each, at })(错峰)、play()/pause()/resume()/stop()/restart()duration / isPlaying() / finished(Promise)。它是调度器而非新的求值器play()at 折算成 delay 写进 props.animations,推进仍由 AnimationManager 完成,因此与缓动/关键帧/量化/缓存复用/空闲停帧完全兼容。 play() 每次都从播放(会重置运行时状态),restart() 即"点击重播";暂停/继续走全局 pause/resume
  • 减少动态效果:系统开着 prefers-reduced-motion: reduce(或应用层 ice.setReducedMotion(true))时, 动画不播放过程、直接落终态,并记一条 ICE_ANIM_REDUCED_MOTION 诊断(见 getDiagnostics())。
  • 空闲停帧:没有脏、没有动画在推进时 FrameManager停掉 rAF(省电);置脏、animationManager.add()resume() 都会自动唤醒。应用层自己按帧做计算时用 ice.setContinuousFrames(true) 保持常驻。
  • 未知缓动名回退 linear 并只提示一次;非法配置的告警不会逐帧刷屏。