- 前端
- 桌面应用
- AI 应用
- MCP 服务
【免费下载链接】open-pencil
AI-native design editor. Open-source Figma alternative.
导读
useCanvasInput是 OpenPencil(AI-native 设计编辑器开源项目)Vue SDK 中负责把<canvas>元素的指针与鼠标事件接入编辑器输入系统的核心 composable。它把屏幕坐标换算成画布坐标、统一调度选中、拖拽、缩放、旋转、平移、绘图工具与文本/矢量编辑等全部画布交互,是任何自建编辑器外壳(editor shell)接入画布时必须掌握的低层 API。读完本文,你将理解useCanvasInput的完整签名、各参数的职责、内部交互状态机(DragState)、坐标换算原理以及它在真实编辑器外壳中的调用方式,能够据此在自己构建的 Vue 组件里正确接入 OpenPencil 画布。
本文基于仓库中该 API 的官方文档(packages/docs/fr/programmable/sdk/api/composables/use-canvas-input.md),并深入其实际源码 packages/vue/src/canvas/useCanvasInput.ts 及配套输入模块展开。
一、useCanvasInput 是什么
根据官方文档,useCanvasInput(options)把目标元素(通常是<canvas>)的事件绑定到编辑器的输入系统上,覆盖的能力清单包括:
- 指针的移动、按下与释放(mouvement, pression et relâchement du pointeur);
- 选中与框选(sélection et rectangle de sélection);
- 视图平移与缩放(déplacement de la vue et zoom);
- 对象拖拽(glissement des objets);
- 缩放与旋转(redimensionnement et rotation);
- 形状工具、钢笔(Plume)、文本与抓手工具(outils de formes, Plume, Texte et Main);
- 矢量编辑与文本编辑(édition vectorielle et textuelle)。
文档同时强调了两个关键设计:
- 坐标换算:该 composable 会把屏幕坐标转换为画布坐标,并在一次交互的整个生命周期内保持指针捕获(pointer capture);
- 定位:这是一个低层(bas niveau)API,主要面向“自己拥有画布、希望构建干净界面”的编辑器外壳组件,而非高层封装。
在 Vue SDK 中,useCanvasInput与useCanvas(负责 CanvasKit 渲染表面的创建与维护,见 use-canvas.md)和useTextEdit(负责文本输入,见 use-text-edit.md)三者通常搭配使用:useCanvas管渲染,useCanvasInput管交互,useTextEdit管画布内文本编辑。它在 SDK 公开入口 packages/vue/src/index.ts 中被导出。
二、函数签名与参数说明
虽然法语文档以useCanvasInput(options)概括,实际源码 packages/vue/src/canvas/useCanvasInput.ts 中的签名更为精确,是理解其行为的权威依据:
export function useCanvasInput( canvasRef: Ref<HTMLCanvasElement | null>, editor: Editor, hitTestSectionTitle: (cx: number, cy: number) => SceneNode | null, hitTestComponentLabel: (cx: number, cy: number) => SceneNode | null, hitTestFrameTitle: (cx: number, cy: number) => SceneNode | null, onCursorMove?: (cx: number, cy: number) => void, onActivate?: () => void, isEnabled: () => boolean = () => true )各参数含义如下:
| 参数 | 类型 | 说明 |
|---|---|---|
canvasRef | Ref<HTMLCanvasElement \| null> | 目标画布元素的引用,所有事件都绑定在此元素上 |
editor | Editor | 来自@open-pencil/core/editor的编辑器实例(通常由createEditor创建、useEditor从上下文取回) |
hitTestSectionTitle | (cx, cy) => SceneNode \| null | 命中测试:给定画布坐标,返回命中的 Section 标题节点 |
hitTestComponentLabel | (cx, cy) => SceneNode \| null | 命中测试:返回命中的组件标签节点 |
hitTestFrameTitle | (cx, cy) => SceneNode \| null | 命中测试:返回命中的 Frame 标题节点 |
onCursorMove | 可选回调 | 指针移动时回调,参数为画布坐标(cx, cy),可用于同步外部悬浮信息 |
onActivate | 可选回调 | 每次mousedown时触发,编辑器外壳用它做“激活面板”等处理 |
isEnabled | () => boolean | 交互开关(默认恒为true),为false时忽略输入;多面板场景下用于区分当前活动面板 |
返回对象包含供外壳组件消费的状态与操作(源码 useCanvasInput.ts):
drag:当前拖拽状态(Ref<DragState | null>),用于驱动覆盖层 UI;cursorOverride:Ref<string | null>,画布上的光标覆盖值;- 画布标签(Frame/Section 标题)编辑:
canvasLabelEdit、updateCanvasLabelEdit、commitCanvasLabelEdit、cancelCanvasLabelEdit; - Auto Layout 内边距编辑:
autoLayoutPaddingEdit、updateAutoLayoutPaddingEdit、commitAutoLayoutPaddingEdit、cancelAutoLayoutPaddingEdit; cleanupInteractions():取消进行中的交互、清空临时反馈并复位修饰键状态,常用于面板失活或组件卸载时。
注意:法语文档把参数概括为
options对象,德语版文档则给出位置参数形式的调用示例(见 packages/docs/de/programmable/sdk/api/composables/use-canvas-input.md),源码实现采用的是上面这组位置参数。
三、坐标换算:从屏幕坐标到画布坐标
文档明确提及“composable 把屏幕坐标转换成画布坐标”。这一逻辑集中在 packages/vue/src/shared/input/geometry.ts 的getPointerCoords中:
export function getPointerCoords(e: MouseEvent, canvas: HTMLCanvasElement | null, editor: Editor) { if (!canvas) return { sx: 0, sy: 0, cx: 0, cy: 0 } const rect = canvas.getBoundingClientRect() const sx = e.clientX - rect.left const sy = e.clientY - rect.top const { x: cx, y: cy } = editor.screenToCanvas(sx, sy) return { sx, sy, cx, cy } }换算分两步:
- 客户端坐标 → 画布像素坐标:用
getBoundingClientRect()减去画布左上角,得到sx/sy; - 画布像素坐标 → 画布逻辑坐标:通过
editor.screenToCanvas(sx, sy)得到cx/cy。screenToCanvas内部会考虑编辑器的平移(pan)与缩放(zoom),因此useCanvasInput处理的永远是逻辑坐标,缩放平移无需你手工处理。
此外,packages/vue/src/canvas/pointer/use.ts 的createCanvasPointer在换算基础上又封装了三类能力:canvasToLocal(画布坐标 → 某个节点局部坐标,供进入容器内部后的命中测试使用)、hitTestInScope(在当前“进入的容器”或当前页范围内做命中测试,支持深/浅两种模式)、isInsideContainerBounds(判断某点是否落在容器边界内)。这三者与三个标题命中测试函数一起组成了hitFns,供拖拽调度与光标更新共用。
四、统一拖拽状态机:DragState
useCanvasInput的核心是一个dragref,所有进行中的交互都归约为统一的拖拽状态联合类型DragState,定义于 packages/vue/src/shared/input/types.ts:
export type DragState = | DragDraw // 绘图工具拖拽(矩形、椭圆、线条……) | DragMove // 移动选中对象 | DragPan // 抓手/空格平移视图 | DragResize // 缩放 | DragMarquee // 框选 | DragRotate // 旋转 | DragPen // 钢笔路径绘制 | DragTextSelect // 文本选区 | DragEditNode // 矢量节点编辑 | DragEditHandle // 贝塞尔手柄编辑 | DragBendHandle // 弯曲手柄编辑 | DragGuide // 标尺/参考线拖拽以DragMove为例(types.ts),它记录起点、当前位置、已应用的位移增量(appliedDx/appliedDy)、是否已判定为拖拽(dragStarted)、所有对象的原始位置,以及duplicated(拖拽时按住修饰键复制)、autoLayoutParentId、brokeFromAutoLayout等自动布局相关信息。DragResize(types.ts)则保存origRect、原矢量网络、填充/描边几何、派生文本字形等快照,保证缩放预览可随时回滚。
onMouseMove(useCanvasInput.ts)是一个集中的分发器:根据drag.value.type把移动事件分别交给handlePanMove、guideInput.handleMove、handleRotateMove、handleMoveMove、applyResize、handlePenDragMove、handleNodeEditMove、handleBendHandleMove、handleDrawMove、handleMarqueeMove等实现。无拖拽时它仍会驱动钢笔悬浮(updatePenHover)、矢量节点悬浮(updateNodeEditHover)、参考线悬浮与光标更新,保证“仅悬停不拖拽”的场景也有即时反馈。
onMouseUp(useCanvasInput.ts)按类型提交结果:commitResizePreview提交缩放、editor.penCommit(true)提交钢笔路径、editor.commitRotation提交旋转、draw.commit()提交绘图、setMarquee(null)结束框选。这样所有交互都遵循“开始 → 预览 → 提交/取消”的同一生命周期。
五、指针捕获与事件绑定
文档强调“在交互期间保持指针捕获”。这一实现位于 useCanvasInput.ts:
function onPointerDown(e: PointerEvent) { if (e.pointerType !== 'mouse' || e.button !== 0) return canvasRef.value?.setPointerCapture(e.pointerId) }pointerdown时若为鼠标主键,立即调用setPointerCapture,保证即使指针移出画布,后续的mousemove/mouseup仍派发到画布,拖拽不中断;pointerup/pointercancel时释放捕获并分别走“提交”与“取消”路径。
事件绑定通过useEventListener完成(useCanvasInput.ts),包括画布上的pointerdown/pointerup/pointercancel/dblclick/mousedown/mousemove/mouseup/mouseleave,以及 window 级的keydown/keyup(追踪 Alt/Meta/Ctrl 修饰键)、blur(失焦时复位测量修饰键并取消交互)、捕获阶段的mouseup(防止指针离开画布后拖拽卡死)和捕获阶段的Escape(取消绘图/旋转)。
另外它还订阅了编辑器的若干事件(useCanvasInput.ts):rotation:preview-changed、tool:changed、selection:changed、page:changed、graph:replaced。当工具切换、选区变化或页面/图被替换时,进行中的绘图与旋转交互会被自动取消,避免状态错乱;组件卸载时(onScopeDispose)会统一停止监听并调用cancelPointerInteraction。
六、工具联动与修饰键语义
onMouseDown(useCanvasInput.ts)是交互入口:先触发onActivate,再依据isEnabled判断是否处理;随后提交可能存在的 Auto Layout 内边距编辑、聚焦画布、清空悬浮节点,并在参考线拖拽(标尺拖出/已有参考线)之后,把事件交给handleToolMouseDown统一派发。工具与节点类型的映射TOOL_TO_NODE(types.ts)说明绘图工具(FRAME/SECTION/RECTANGLE/ELLIPSE/LINE/POLYGON/STAR/TEXT)会创建对应类型的节点。
修饰键在交互中有明确语义,由updateModifier跟踪(useCanvasInput.ts),并在移动/释放分支中生效:
- Shift:缩放保持宽高比、旋转按角度约束、钢笔切线约束;
- Alt:测量模式开关(配合画布内测量)、参考线复制拖拽、节点编辑分支调整;
- Meta/Ctrl:测量模式升级为“deep”、拖拽复制、节点编辑分支的精确控制;
- 空格:临时切换到抓手平移(
setupPanZoom与useSpaceHeld配合实现)。
值得留意的是测量模式(measurement):canMeasure(useCanvasInput.ts)要求指针在画布内、无拖拽、处于 SELECT 工具且有选中对象、且不在文本/节点/钢笔编辑状态;此时按住 Alt 会以shallow/deep两种深度调用editor.setMeasurementMode,驱动画布内测量反馈与光标覆盖。
七、真实用法:EditorCanvas 外壳组件
该 API 的实际调用者正是主应用中的 src/components/EditorCanvas.vue。其调用方式(EditorCanvas.vue)是理解各参数实战含义的最佳示例:
const { cursorOverride, canvasLabelEdit, updateCanvasLabelEdit, commitCanvasLabelEdit, cancelCanvasLabelEdit, autoLayoutPaddingEdit, updateAutoLayoutPaddingEdit, commitAutoLayoutPaddingEdit, cancelAutoLayoutPaddingEdit, cleanupInteractions } = useCanvasInput( canvasRef, store, hitTestSectionTitle, hitTestComponentLabel, hitTestFrameTitle, updatePaneCursor, activatePane, () => isActivePane.value )要点:
- 三个命中测试函数(
hitTestSectionTitle等)来自编辑器外壳自身的场景查询逻辑,把“哪里是 Section 标题”这类自定义判定交给调用方; updatePaneCursor作为onCursorMove,把指针画布坐标同步给面板层;activatePane作为onActivate,保证点击画布时面板被激活;isEnabled返回() => isActivePane.value,实现多面板(多画布)共存时只有活动面板响应输入;- 外壳在面板失活(
watch(isActivePane))与组件卸载(onUnmounted)时调用cleanupInteractions()(EditorCanvas.vue),与useTextEdit(canvasRef, store, { isEnabled: () => isActivePane.value })和useCanvasDrop一起构成完整画布外壳。
返回的cursorOverride、autoLayoutPaddingEdit等还驱动着覆盖层:例如内边距编辑器的锚点坐标由paddingEditorAnchor计算(EditorCanvas.vue),再通过useCanvasVirtualReference定位浮层,实现拖拽内边距时实时显示数值编辑器的效果。
八、相关 API 与进一步阅读
useCanvasInput通常与以下 API 配套使用:
- useCanvas:负责 CanvasKit 加载、surface 创建/重建、尺寸观察、像素比调整与刷新请求,是渲染侧的对偶 composable;
- useTextEdit:负责 textarea 回退的文本输入、IME 组合、光标闪烁与样式快捷键,与
useCanvasInput的文本编辑入口互补; useEditor/provideEditor:从 Vue 注入上下文取得Editor实例(导出于 packages/vue/src/index.ts)。
若需更完整的接入方式,可参考 EditorCanvas.vue 与 SDK 文档目录 packages/docs/fr/programmable/sdk;底层坐标换算与命中测试实现在 packages/vue/src/shared/input/geometry.ts 与 packages/vue/src/canvas/pointer/use.ts。官方还提供了德语、西班牙语、意大利语、波兰语等多语言版本的同主题文档(如 packages/docs/de/programmable/sdk/api/composables/use-canvas-input.md),供交叉参考。
总结
useCanvasInput是 OpenPencil Vue SDK 中画布交互的总调度器:它以统一的DragState状态机覆盖移动、缩放、旋转、平移、框选、绘图、钢笔、参考线与矢量/文本编辑,通过screenToCanvas完成坐标换算,借助 Pointer Capture 与全局事件监听保证交互完整性,并通过回调与isEnabled机制适配多面板编辑器外壳。结合 EditorCanvas.vue 的实战用法,你可以快速在自己的 Vue 组件中搭建出与 OpenPencil 主应用一致的画布交互体验。
- 前端
- 桌面应用
- AI 应用
- MCP 服务
【免费下载链接】open-pencil
AI-native design editor. Open-source Figma alternative.
相关推荐
OpenPencil 的 useCanvasInput:把指针事件接入编辑器画布的 Vue Composable
OpenPencil 的 useCanvasInput:把指针事件接入编辑器画布的 Vue Composable 在 OpenPencil(开源 AI 原生设计
前端桌面应用AI 应用MCP 服务OpenPencil `useCanvasInput` 完全指南:为 Vue 画布组件接入选择、拖拽、缩放与工具交互
OpenPencil useCanvasInput 完全指南:为 Vue 画布组件接入选择、拖拽、缩放与工具交互 useCanvasInput 是 OpenPe
前端桌面应用AI 应用MCP 服务OpenPencil Vue SDK 的 useTextEdit 指南:画布文本编辑、IME 组合与 SceneGraph 同步全解析
OpenPencil Vue SDK 的 useTextEdit 指南:画布文本编辑、IME 组合与 SceneGraph 同步全解析 useTextEdit
前端桌面应用AI 应用MCP 服务
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考