
OpenReel 形状图层升级指南Shape Groups 与 Merge Paths 的实现路线图【免费下载链接】openreel-videoOpenReel Video - Professional browser-based video editor. Open source CapCut alternative. 100% browser-based, no installation, no cloud uploads, no watermarks.项目地址: https://gitcode.com/GitHub_Trending/op/openreel-video本篇技术指南以 OpenReel开源浏览器端视频编辑器核心代码位于 packages/core中形状图层Shape Layer的组 布尔合并 新操作符实现计划为主线完整讲解如何在一个形状图层内部建立带独立变换与有序操作栈的 contents 树、用polygon-clipping实现 Union/Subtract/Intersect/Exclude 合并路径、新增 Offset Paths / Pucker Bloat / Twist 三个 AE 级操作符并贯通核心渲染器、关键帧引擎、Web UI 与 MCP Agent 工具。读完本文你将掌握该特性从数据模型、布尔算法、渲染分支到关键帧寻址与工具链的完整落地方案并能直接对照仓库中的真实实现如 motion-shape-contents.ts、motion-shape-boolean.ts进行二次开发。背景为什么一个形状不能只是一个形状在 OpenReel 中形状图层长期以来遵循一图层一形状模型types.ts 中MotionShapeLayer只持有一个shapeType、一个style与一个可选的pathData渲染器为每个图层只构建一条 ctx 路径并执行一次填充 一次描边见 motion-renderer.ts 附近。这带来四个现实痛点没有层内分组无法在一个图层内为多个子形状分别设置变换没有有序操作栈现有 modifier 按类型去重见 motion-shape-modifiers.ts无法表达 AE 中按顺序叠加操作符的语义没有路径布尔合并2D 矢量路径无法做 union/subtract掩码只能用ctx.clip/合成方式伪造加、减效果缺少三个标准 AE 操作符Offset Paths、Pucker Bloat、Twist。其直接后果是复合 Logo / 图标这类形状图层最典型的创作场景根本无法实现。该特性由 AE 对齐评审big rock #5提出对应的设计规格为 2026-07-02-shape-groups-merge-paths-design.md实现计划为 2026-07-02-shape-groups-merge-paths.md。截至当前仓库状态该计划的全部核心代码均已落地本文以计划文档为骨架、以真实源码为佐证展开。总体架构与全局约束计划的整体架构一句话概括在MotionShapeLayer上增加一个可选的contents树由纯函数辅助模块归一化用一个布尔模块包装polygon-clipping渲染器增加递归分支在合并时把变换烘焙进顶点关键帧使用扁平的contents.属性 ID 语法UI 新增 ShapeContentsSection 组件MCP 新增七个注册表工具。技术栈与约束如下TypeScript strict禁止any、不安全断言、非空断言Vitest RTL 测试polygon-clipping0.15.7已在 packages/core/package.json 安装不得重装Canvas2D 渲染遗留字节级兼容没有contents的形状图层必须走原有渲染/关键帧/工具路径既有测试套件是回归安全网依赖边界polygon-clipping只能在 motion-shape-boolean.ts 中 importcontents 树规则深度上限 8每个图层内 item id 唯一冲突即抛错组的opacity沿树向下相乘visible: false跳过渲染但保留数据合并语义union 全部子项合并subtract 第一个减去其余按文档顺序intersect 全体交集exclude 从左到右的对称差折叠。可见子几何少于 2 个时合并为直通no-op passthrough合并输出填充规则统一为evenoddUI 手势协议连续操作走updateMotionCompositionPreviewcommitMotionCompositionGesture一个手势一个 undo离散编辑一次提交类型检查各包内直接执行pnpm exec tsc --noEmit不要传--ignoreDeprecations测试保持全绿core motion 504、web motion 247、agent 409关键帧属性语法精确contents.{itemId}.transform.positionX|positionY|scaleX|scaleY|rotation|opacity、contents.{itemId}.pathData、contents.{itemId}.operator.{operatorId}.{param}。Task 1数据模型与 contents 辅助模块计划在 types.ts 中按设计规格 §Design.1 精确新增以下接口仓库已实现type MotionShapeMergeMode none | union | subtract | intersect | exclude; interface MotionShapeGroupTransform { readonly anchor: MotionVector2; // 组内局部 px readonly position: MotionVector2; // 父级局部 px 偏移 readonly scale: MotionVector2; // 1 100% readonly rotation: number; // 角度 readonly opacity: number; // 0..1沿树向下相乘 } interface MotionShapeGroupItem { readonly kind: group; readonly id: string; readonly name: string; readonly transform: MotionShapeGroupTransform; readonly items: readonly MotionShapeItem[]; readonly operators?: readonly MotionShapeOperator[]; // 有序允许重复 readonly mergeMode?: MotionShapeMergeMode; // 默认 none readonly style?: ShapeStyle; // 用于合并输出 / 被无样式子项继承 readonly visible?: boolean; // 默认 true } interface MotionShapePathItem { readonly kind: path; readonly id: string; readonly name: string; readonly shapeType: ShapeType; // 2D 子集path 使用 pathData readonly width: number; readonly height: number; readonly position: MotionVector2; // 子项中心相对父级的局部 px 偏移 readonly pathData?: string; readonly pathClosed?: boolean; readonly style?: ShapeStyle; // 覆盖项缺省 继承最近祖先组样式否则图层样式 readonly visible?: boolean; } type MotionShapeItem MotionShapeGroupItem | MotionShapePathItem; type MotionShapeOperator MotionShapeModifier; // 复用同一 union实例携带唯一 id同时在MotionShapeLayer上新增contents?: readonly MotionShapeItem[]见 types.ts。MotionShapeModifierunion 增加三个新成员MotionOffsetPathsModifier {id, type:offset-paths, enabled, amount:number, lineJoin:miter|round|bevel}、MotionPuckerBloatModifier {id, type:pucker-bloat, enabled, amount:number}范围 −100..100、MotionTwistModifier {id, type:twist, enabled, angle:number, center:MotionVector2}见 types.ts并同步扩展MOTION_SHAPE_MODIFIER_TYPES。纯函数辅助模块motion-shape-contents.ts无 React/store 依赖提供getMotionShapeContents(layer)当contents缺失时合成隐式根项{kind:path, id:__root, name: layer.name, shapeType, width, height, position:{x:0,y:0}, pathData, pathClosed, style: undefined}样式解析留给下游回退到图层样式hasExplicitShapeContents(layer)L30、findShapeItem(contents,id)递归查找L55、collectShapeItemIdsCRUDaddShapeItem(layer,parentGroupId|null,item)L134、updateShapeItem(layer,id,patch)判别式更新不允许改变kindL191、removeShapeItem递归剪除L237、moveShapeItem(layer,id,direction:up|down)仅在父级内移动L295materializeShapeContents(layer)把隐式项包装进名为 Group 1 的组默认变换 anchor/position 为 0、scale 为 1、rotation 为 0、opacity 为 1对应 UI 的 Group contents 操作L348工厂createShapeGroupItem(name)/createShapePathItem(init)带 id 生成与碰撞检查L308-L346。关键不变式插入时深度 8 或 id 重复即抛错assertNoDuplicateIds与MAX_SHAPE_CONTENTS_DEPTH 8在 L13-L29所有返回均为新对象输入永不被修改。测试≥12 个见 motion-shape-contents.test.ts覆盖遗留层合成、显式直通、根/嵌套组添加、更新不可变性原对象不动、改 kind 被拒、删除递归剪枝、上移/下移与边界 no-op、深度第 9 层抛错、重复 id 抛错、materialize 包装保留几何字段、嵌套查找命中/未命中。Task 2布尔合并引擎合并引擎集中在 motion-shape-boolean.ts这是仓库中唯一 importpolygon-clipping的文件。核心函数签名mergeMotionShapeRings( ringSets: ReadonlyArrayReadonlyArrayReadonlyArrayMotionVector2, mode: MotionShapeMergeMode, ): MotionVector2[][][]每个输入是一个子几何的环集合展平后的多边形可能含外环 洞的多环。实现细节L119-L148模式分发union →polygonClipping.unionsubtract →difference(head, ...rest)intersect →intersectionexclude → 从[head]开始对 rest 左折叠xor对称差none或非空输入 2 → 过滤后直通克隆坐标守卫任何非有限NaN/Infinity坐标直接抛出类型化错误MotionShapeBooleanError携带 mode见 L13-L22调用库前后均有 try/catch 包装坐标转换polygon-clipping使用Pair [number, number]元组toClippingPolygon/fromClippingMultiPolygon双向转换并在库返回未闭合环时补/去首尾点。贝塞尔展平flattenPathPointsToRing直线段直通带出/入控制柄outX/outY、inX/inY的立方段按samplesPerCurve默认 16与 CURVE_SAMPLES 一致均匀采样开放路径返回折线由调用方决定是否参与合并渲染器将开放路径排除在合并之外。测试≥10 个使用真实 polygon-clipping见 motion-shape-boolean.test.ts两重叠矩形 union → 单环 8 顶点断言角点坐标集合subtract → L 形全包含 subtract → 环 洞2 环不相交 intersect → 空相同图形 exclude → 空三输入 unionnone 直通单输入直通NaN 抛错立方圆近似环含 4×16 点且最大径向误差 半径 1%。另有 motion-shape-boolean.library-error.test.ts 通过 mock 库验证异常包装路径。Task 3三个新操作符与操作栈在 motion-shape-modifiers.ts 中实现均以顶点数组变换实现与既有 zig-zag 等操作符同一范式applyMotionOffsetPathsToPoints(points, amount, join)L1130仅闭合环开放输入原样返回顶点沿外法线位移相邻边法线平均并归一化miter limit 为 4超过后角点分裂为两点beveljoin round 用单个中点近似负 amount 内缩位移后若 amount ≠ 0用mergeMotionShapeRings([[ring]], union)做自并集清理并取最大面积环集applyMotionPuckerBloatToPoints(points, amount)L1252质心 顶点均值每个顶点按amount/100朝质心负值 pucker或远离质心正值 bloat插值applyMotionTwistToPoints(points, angle, center)L1278maxDist 顶点到 center 的最大距离每个顶点绕 center 旋转angleDeg * (1 - dist/maxDist)AE 语义越靠近中心扭得越厉害maxDist 为 0 时不变applyMotionShapeOperatorStack(ringSets, operators, evaluatedParams)L1314从左到右有序应用——zig-zag/round-corners/wiggle/offset/pucker/twist 逐环映射trim 逐环调用getTrimmedMotionPathPointsrepeater 通过getMotionRepeaterCopies将变换烘焙进顶点后扩增环集。createMotionShapeModifier新增三种类型的默认值offset amount 10、join miterpucker-bloat amount 0twist angle 0、center 取图层中心接受 center 参数并注册进MOTION_SHAPE_MODIFIER_TYPES、getMotionShapeModifierKeyframeProperty与evaluateMotionShapeModifiersAtTime的求值amount / amount / angle。三个新操作符同时作为图层级 modifier 类型注册去重规则不变因此遗留图层也可使用图层级管线buildMotionShapePolyline中按 wiggle 之后、offset → pucker-bloat → twist 的顺序接入。测试≥10 个覆盖方块 10 偏移后角点位于 ±(w/210)miter 容差内、−10 内缩、开放路径不变、pucker 100 朝质心坍缩50 处半径减半、bloat 对称、twist 90° 中心附近顶点比边缘顶点位移更大单调衰减断言、twist maxDist 0 no-op、栈顺序敏感方块上 offset→twist ≠ twist→offset、栈内 repeater 使环数×3、栈内 trim 缩短环。Task 4渲染器递归分支motion-renderer.ts 中renderShape按hasExplicitShapeContents(layer)分支无contents走原有单形状路径字节级兼容有则进入renderShapeContentsL1689。树遍历规则组 mergeMode nonectx.save→ translate(positionanchor 数学)/rotate/scale → opacity 累乘进运行中的 alpha → 递归渲染子项 →ctx.restore若组带几何类操作符则强制该子树走烘焙几何路径组 mergeMode ≠ none或含几何操作符先由collectShapeItemRings(item, time, layer, parentMatrix, parentOpacity)收集子几何把所有后代变换烘焙进顶点返回{rings, style, opacity}[]叶子条目布尔模块合并再施加操作栈最后以组有效样式 evenodd填充规则只填充 描边一次path 子项解析样式自身 → 最近祖先组 → 图层复用buildShapePath重构出的 item 输入形态绘制遗留调用点传图层字段行为必须保持逐字节一致开放路径pathClosed false跳过合并收集、单独渲染布尔失败MotionShapeBooleanError优雅回退为渲染未合并的子项。关键实现位于 collectShapeItemRings可见性为 false 直接返回空路径项把父矩阵 ×shapePathItemTranslationMatrix后逐点施加组项把父矩阵 ×shapeGroupTransformToMatrixL406rotation×scale 与 anchor 平移的 2×3 仿射复合multiplyShapeMatrices见 L379opacity 向下相乘样式向下继承。测试≥8 个见 motion-renderer-contents.test.ts用桩 ctx 记录方法调用遗留层 ctx 调用序列与改造前 golden 完全一致恰好一次 fill 一次 stroke、相同 transform 调用一个普通组 两个子项 → 两次 fillunion → 一次evenoddfill组 opacity 0.5 在叶子处把ctx.globalAlpha减半visible:false跳过嵌套组变换复合点烘焙 矩阵数学期望合并失败回退未合并子项开放路径不入合并但仍被描边。Task 5关键帧寻址contents.* 语法关键帧引擎在 motion-keyframes.ts 中扩展parseContentsPropertyId(propertyId)L836→{itemId, channel: {type:transform,field} | {type:pathData} | {type:operator, operatorId, param}} | null严格按全局约束的语法非法输入缺 itemId、未知字段、尾部垃圾、shape.width等非 contents 属性返回 nullresolveShapeContentsAtTime(layer, time)L1079在指定时刻解析出带关键帧的组变换通道、子项 pathData经morphMotionPathData形态渐变与操作符参数供 Task 4 的收集器消费getMotionLayerContentsPropertyDescriptors(layer)L1147按现有 shape-modifier 描述符函数模式返回{label, propertyId, currentValue}label 形如Circle 2 › Position X在既有描述符聚合点旁注册时间轴timeline即可免费消费遗留层返回空描述符、解析保持不变。数值通道复用标准插值引擎easing/贝塞尔/表达式不受影响——它们照旧作用于layer.keyframes中的 Keyframe 记录与今天shape.width一致。测试≥8 个见 motion-keyframes.test.ts覆盖三种通道形式 恶意输入拒绝如contents..transform.positionX、contents.abc.transform.zzz、contents.abc.operator.op-9、contents.abc.transform.positionX.extra均返回 nullpositionX 在 t0/1 的关键帧线性缓动下 t0.5 解析为中点rotation opacity 通道pathData 形态渐变解析出中间点数操作符参数twist angle解析双子项树为每个可动画通道枚举一个描述符遗留层无 contents返回空描述符。Task 6UI——ShapeContentsSectionWeb 端新增 ShapeContentsSection.tsx在 PropertiesPanel.tsx 中为形状图层挂载仅几行改动PropertiesPanel 约 4787 行树 UI 独立成组件是刻意为之。该组件接收选中图层 合成通过 PropertiesPanel 现有的 store 更新路径updateMotionLayer/upsertMotionComposition变更数据连续数字字段拖拽包进 preview/commit 手势协议离散编辑单次提交一个手势一个 undo。交互设计树行按深度缩进 12px组有 chevron 展开折叠局部 state、眼睛切换visible、双击改名Enter/blur 提交选区状态为组件局部。树下方按选中项渲染详情面板——组6 个变换数字字段 merge-modeselect aria-labelMerge mode 操作栈行类型标签、启用复选框、上/下/删除、参数数字字段路径项限制为 2D 类型的 shapeType select、width/height/position 字段、样式覆盖 Inherit 开关 既有 fill/stroke 颜色控件模式。工具栏按钮Add group、Add shape、Group contents仅在!hasExplicitShapeContents时可用并按选区设置禁用态。关键帧菱形不在本节范围内值可通过 MCP/时间轴打关键帧属 stretch。测试≥8 个 RTL见 ShapeContentsSection.test.tsx遗留层渲染 Group contents CTA点击 materialize 创建组断言 storeadd group/add shape 追加到 store改名单次提交merge-mode select 写入mergeMode添加操作符twist以默认值出现在 store重排按钮调用 move可见性眼睛切换每次离散编辑一个 undo 条目。Task 7MCP 工具在 packages/agent/src/registry.ts 注册 7 个 domain 为 motion 的工具与既有形状工具一致host.ts 无需改动工具走现有 composition-update host 方法工具关键参数返回add_motion_shape_groupcompositionId, layerId, parentGroupId?, name?, mergeMode?{groupId}add_motion_shape_to_groupcompositionId, layerId, parentGroupId?, shapeType, width?, height?, position?, pathData?, pathClosed?, style?, name?{itemId}update_motion_shape_item…, itemId, patch: {name?, visible?, transform?, mergeMode?, style?, width?, height?, position?, pathData?}kind 守卫transform/mergeMode 仅限组成功结果remove_motion_shape_item…, itemId成功结果move_motion_shape_item…, itemId, direction成功结果add_motion_shape_group_operator…, groupId, operatorType, params?有序追加允许重复{operatorId}update_motion_shape_group_operator/remove_motion_shape_group_operator拆为两个工具合计 8 个…, groupId, operatorId成功结果外加merge_motion_shape_layers {compositionId, layerIds[≥2], mode, name?}registry.ts把每个源图层的变换烘焙进其路径子项几何构建一个带 mergeMode 的分组图层删除源图层单一 undo 批次返回{layerId, groupId}文本/图像图层会被拒绝L27424 处的layer.type校验。校验错误沿用 registry 既有错误结果约定INVALID/NOT_FOUND 形态保持一致每个工具描述都附带一行contents.*关键帧语法说明add_motion_keyframe通过既有的属性解析接受新 ID。测试≥14 个见registry.shape-contents.test.ts每个工具 happy path、图层类型守卫、缺失 comp/layer/group/item 的 NOT_FOUND、kind 守卫违规、merge_motion_shape_layers2 个矩形层 → 1 个含 2 子项的组层 源层消失 几何偏移按源层位置烘焙、断言子项位置不同、非法 mode 拒绝、对contents.{id}.transform.rotation走既有add_motion_keyframe成功。Task 8门禁验证与执行顺序完整验证命令Task 8 gate逐条原文运行并汇报pnpm --filter openreel/core test:run src/motion # 504 通过 全部新增 cd apps/web pnpm exec vitest run src/motion # 247 全绿 pnpm --filter openreel/agent test:run # 409 # 三个包各执行一次均要求 0 错误 pnpm exec tsc --noEmit # 在 packages/core、apps/web、packages/agent 内分别运行执行波次controller 编排[T1 ∥ T2] → [T3 ∥ T5] → T4 → [T6 ∥ T7] → T8——T3 依赖 T2 的 union 做偏移清理T4 消费 T1T2T3T5T6/T7 触及互不相干的包。风险与设计取舍布尔输出是多边形贝塞尔在 CURVE_SAMPLES 密度下展平——可接受因为合并只喂给填充/描边采样密度与 polyline builder 现行输出一致已文档化不隐藏坐标系合并要求把组/项变换烘焙进顶点而非合并渲染则用 ctx 变换——collectShapeItemRings是唯一事实来源并以属性测试保证bake(render) ≡ ctx-transform(render)抽样几何上PropertiesPanel 体量4787 行的面板不改动核心树 UI 独立成ShapeContentsSection.tsx遗留字节级兼容contents undefined必须走原路径不动以渲染器 spy 既有 504 测试套件做回归polygon-clipping 健壮性公认成熟的 Martinez 实现调用前守卫 NaN/∞catch 后优雅失败渲染未合并子项不提供 console 错误面而不是让渲染器崩溃Live 视觉验收规格中的最终人工检查在画布上现场构建复合图标circle ∪ offset squares、star 被 subtract验证每组 repeater、动画 twist angle 与组位置并对比遗留单形状图层渲染前后截图不变。结语一条从数据结构到工具链的完整实现路径从 types.ts 的数据模型到 motion-shape-boolean.ts 的布尔内核再到 motion-renderer.ts 的递归渲染、motion-keyframes.ts 的contents.*关键帧寻址、ShapeContentsSection.tsx 的树形编辑 UI 与 registry.ts 的 MCP 工具Shape Groups Merge Paths 打通了一个图层内做复合形状的完整链路。该计划同时是仓库实现模式的范例纯函数核心 显式依赖边界 遗留兼容回归网 每个任务独立测试门禁。若你想在 OpenReel 上扩展形状能力例如 SVG 导入为组、路径子项的画布内钢笔编辑、多填充/描边以上各模块就是最直接的切入点——新增类型进 types.ts、纯逻辑进 motion-shape-*.ts、渲染进 renderShapeContents 分支、暴露面进 registry.ts照此模式即可平滑演进。【免费下载链接】openreel-videoOpenReel Video - Professional browser-based video editor. Open source CapCut alternative. 100% browser-based, no installation, no cloud uploads, no watermarks.项目地址: https://gitcode.com/GitHub_Trending/op/openreel-video创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考