☰
OpenPencil 画布渲染集成实战:useCanvas 全面解析与源码级指南
2026/10/9 10:11:06 网站建设 项目流程
  • 前端
  • 桌面应用
  • AI 应用
  • MCP 服务

【免费下载链接】open-pencil

AI-native design editor. Open-source Figma alternative.

项目地址:https://gitcode.com/gh_mirrors/op/open-pencil
点击查看免费下载

useCanvas()是 OpenPencil Vue SDK(@open-pencil/vue)中把编辑器与真实<canvas>元素连接起来的核心 composable:它负责加载 CanvasKit、创建与重建渲染 surface、调度渲染帧、处理尺寸变化与像素比(DPR)缩放、控制标尺可见性,并在渲染器就绪时触发回调。本文以 use-canvas 官方文档 为骨架,结合 packages/vue 包内的真实实现源码,从接入方式、全部选项、底层生命周期到生产级用法(多层画布、多窗格渲染、截图工作流)做完整讲解。读完你既能写出可运行的画布集成代码,也能理解 CanvasKit 渲染管线在 OpenPencil 中究竟如何运转。

useCanvas 的职责边界

按照官方文档的定位,useCanvas()负责"把编辑器连接到一个真实的<canvas>元素",其核心职责可以概括为六件事:

  • CanvasKit 初始化:异步加载 CanvasKit WASM 运行时;
  • surface 创建:为画布创建(并在需要时重建)Skia 渲染 surface;
  • 渲染调度:把编辑器的脏标记、版本号变化合并成逐帧渲染请求;
  • 尺寸变化处理:通过 ResizeObserver 观察画布尺寸,重建或调整 surface;
  • 标尺可见性控制:可选地强制开启或关闭画布标尺;
  • 渲染器就绪回调:surface 与字体加载完成后触发onReady。

同时文档明确了两条边界:useCanvas()面向渲染器(renderer-facing),实践中仅用于浏览器环境;它负责的是"实时画布管线",而不是应用层级的文件读写流程;它通常应与useCanvasInput()配对使用来完成交互处理。

如果你不需要直接控制 canvas 元素,可以改用 SDK 提供的[CanvasRoot](https://link.gitcode.com/i/3660a4eb0bfa6293b918bffebd4a0882)与[CanvasSurface](https://link.gitcode.com/i/fcf22a5f32bf2df246a8f39d782c4688)两个无头组件,由它们内部调用useCanvas()并注入画布上下文;useCanvas则是面向需要直接持有<canvas>引用的编辑器外壳组件的底层选项。

快速接入:最小可用示例

安装与前置条件

@open-pencil/vue位于@open-pencil/core之上,提供编辑器注入、画布集成、选择/面板/变量/i18n 等 composable,以及CanvasRoot、LayerTreeRoot等无头结构组件。按 packages/vue/README.md 的说明安装:

bun add @open-pencil/vue @open-pencil/core @open-pencil/scene-graph canvaskit-wasm

当前开发版要求 Vue^3.5.41,并需要canvaskit-wasm >=0.41.1作为 CanvasKit peer 依赖。

使用useCanvas前,画布组件必须位于provideEditor(editor)提供的编辑器上下文中,并通过useEditor()取出同一个编辑器实例:

import { ref } from 'vue' import { useCanvas, useEditor } from '@open-pencil/vue' const canvasRef = ref<HTMLCanvasElement | null>(null) const editor = useEditor() useCanvas(canvasRef, editor)

完整 SFC 示例

官方文档给出的基础示例是一个标准 Vue 单文件组件:模板里声明一个class="size-full"的<canvas>,脚本里用ref绑定元素,然后传入useCanvas并附带选项。

<script setup lang="ts"> import { ref } from 'vue' import { useCanvas, useEditor } from '@open-pencil/vue' const canvasRef = ref<HTMLCanvasElement | null>(null) const editor = useEditor() useCanvas(canvasRef, editor, { showRulers: true, onReady: () => { console.log('Renderer ready') }, }) </script> <template> <canvas ref="canvasRef" class="size-full" /> </template>

这里showRulers: true表示画布显示标尺,onReady会在渲染器完成初始化(surface 创建、字体加载、首帧渲染)之后被调用,适合用来收起加载遮罩或启动依赖渲染器的逻辑。

选项全解:完整签名与参数表

官方文档给出的类型签名如下:

interface UseCanvasOptions { showRulers?: boolean preserveDrawingBuffer?: boolean onReady?: () => void } function useCanvas( canvasRef: Ref<HTMLCanvasElement | null>, editor: Editor, options?: UseCanvasOptions, ): void

需要说明的是:文档中的类型是面向初学者的简化版本(返回类型写作void),而源码实现实际上会返回一组渲染与命中测试辅助函数(详见下文"返回值"一节)。同时,源码中 UseCanvasOptions 的完整定义 比文档更丰富,以下是结合源码注释整理出的全部选项:

选项类型说明
showRulersboolean强制开启/关闭该画布的标尺。缺省时 composable 回退到 viewport 与 URL 参数逻辑决定
preserveDrawingBufferboolean在呈现帧之后保留绘制缓冲区,适合截图或像素回读场景;可能随浏览器与 GPU 后端增加内存占用
onReady() => void渲染 surface 就绪后调用一次
layer'full' \| 'scene' \| 'overlays'选择该画布拥有哪个渲染层。'full'渲染完整画布,'scene'只渲染场景,'overlays'只渲染覆盖层
sceneRenderer'retained' \| 'tiled'为该 surface 启用实验性的分块(tiled)场景渲染器
shouldSuspendRender() => boolean返回true时挂起渲染(如正在准备文档/字体时),解除后自动补帧
getRenderState() => EditorState提供该画布渲染所依赖的视图状态,缺省为editor.state。多个画布可共享同一文档图、历史与事件总线,而各自使用独立的视图状态
getOverlayObstacles() => readonly Rect[]返回悬浮在该画布上方的 UI 屏幕矩形(以画布 CSS 像素计),每帧读取;贴边覆盖层(如 issue 边缘图钉)会避开这些区域
onPresented(versions) => void每次呈现后回调,参数为{ renderVersion, sceneVersion }
onPresentation(colorSpace) => void报告画布实际呈现的色域(含回退),无 surface 时为null
onViewportResize(width, height) => void画布 CSS 视口尺寸创建与缩放后回调

返回值:渲染与命中测试助手

从 use.ts 的实现 看,useCanvas实际返回:

{ render, // 标记 surface 脏并调度一帧(markDirty) renderNow, // 立即渲染一帧 hitTestSectionTitle, // 画布命中测试:章节标题 hitTestComponentLabel, // 画布命中测试:组件标签 hitTestFrameTitle, // 画布命中测试:Frame 标题 hitTestIssueMarker, // 画布命中测试:设计检查问题标记 }

这些命中测试函数以画布坐标(cx, cy)为入参、返回命中的SceneNode | null,正是上层useCanvasInput()完成选区、悬停、拖拽所需的能力来源。文档中 useCanvasInput 的示例 展示了这种配合方式:

const canvas = useCanvas(canvasRef, editor) useCanvasInput( canvasRef, editor, canvas.hitTestSectionTitle, canvas.hitTestComponentLabel, canvas.hitTestFrameTitle, )

典型实战场景

内嵌预览:关闭标尺

当画布被嵌入到产品预览、缩略图或协作跟随视图等场景时,通常不希望显示标尺:

useCanvas(canvasRef, editor, { showRulers: false, })

与onReady类似,showRulers是渲染期选项:每帧渲染时都会被读取(见 lifecycle.ts 中的 renderFromEditorState 调用),因此切换值后下一次渲染就会生效。

截图工作流:保留绘制缓冲区

浏览器在合成后默认会丢弃 WebGL 绘制缓冲区,导致toDataURL()/toBlob()或readPixels读到空白。开启preserveDrawingBuffer可让缓冲区在呈现帧后保留:

useCanvas(canvasRef, editor, { preserveDrawingBuffer: true, })

该选项在底层会被翻译成 WebGL 上下文属性:源码 gl-surface.ts 的 makeGLSurface 中,preserveDrawingBuffer为真时以{ preserveDrawingBuffer: 1 }调用ck.GetWebGLContext(canvas, glAttrs)。注意这可能会增加内存占用,仅对确实需要像素回读的画布开启。

生产级多画布组合:场景层 + 覆盖层

OpenPencil 编辑器本身在 EditorCanvas.vue 中同时使用两个useCanvas实例,分别渲染'scene'与'overlays'两个图层:

  • 场景画布(layer: 'scene'):承载场景渲染,关闭标尺,接入sceneRenderer(由运行时配置决定采用'retained'还是实验性的'tiled'),并在onPresented中把sceneVersion回传给文档准备控制器,用于确认场景已实际呈现;
  • 覆盖层画布(layer: 'overlays'):承载标尺、选区框、图钉等 UI 覆盖,showRulers由运行时配置、编辑器状态与预览状态共同决定(仅非预览态显示),并通过getOverlayObstacles上报悬浮 UI 的区域,让贴边覆盖层自动避让。

两个实例共用同一个getRenderState与shouldSuspendRender:当文档处于准备阶段(如字体重试之外的情形)时暂停渲染,避免在文档尚未就绪时绘制残缺帧。这一结构印证了文档"useCanvas是 live canvas 管线的负责人"的定位——它同时支撑了多窗格(pane)场景:getRenderState可返回每个窗格独立的视图状态,从而让多个画布共享文档与历史、各自持有 pan/zoom/页面/预览状态。

源码级原理:从 CanvasKit 加载到一帧渲染

1. CanvasKit 加载与初始化

useCanvas内部的useCanvasSurfaceLifecycle会启动 kit-loader.ts 的初始化流程,执行顺序是:

  1. 等待 canvas 元素出现(用whenever(canvasRef, ...)监听,CanvasSurface子组件挂载后会把元素交给根组件,因此只等挂载是不够的);
  2. 通过@open-pencil/core/canvaskit的getCanvasKit()异步加载 CanvasKit 实例;
  3. 等待一个requestAnimationFrame,确保浏览器完成当前帧布局;
  4. createSurface(canvas)创建 surface;
  5. loadFonts()加载编辑器字体;
  6. renderNow()渲染首帧;
  7. 调用onReady?.()。

任何一步之后若 composable 已销毁,都会提前返回,保证不会在卸载后继续触碰 DOM 或 WebGL 资源。

2. Surface 创建、尺寸与像素比

createCanvasSurfaceManager(lifecycle.ts)负责 surface 的完整生命周期。创建 surface 时首先执行sizeCanvas(gl-surface.ts):

  • 以window.devicePixelRatio(无浏览器环境时取 1)把 CSS 尺寸换算成物理像素,设置canvas.width/canvas.height;
  • 回调onViewportResize(width, height)(缺省则调用editor.setViewportSize)。

随后makeGLSurface依次执行ck.GetWebGLContext(canvas, glAttrs)→ck.MakeGrContext(handle)→ck.MakeOnScreenGLSurface(context, width, height, colorSpace),构造 Skia 的 WebGL 表面;surface 创建失败时会在 canvas 上打上data-surface-error="webgl"标记。渲染器封装为SkiaRenderer(来自@open-pencil/core/canvas),并通过editor.setCanvasKit(ck, renderer)注册到编辑器,同时设置tracksSceneSettlement(layer !== 'overlays'时跟踪场景结算)与tiledSceneEnabled(sceneRenderer === 'tiled'时启用分块渲染)。

3. 渲染调度:版本号驱动的帧循环

渲染循环实现在 render-loop.ts,核心思路是版本号对比 + 脏标记:

  • 每帧对比state.renderVersion、state.canvasVersion与state.selectedIds是否变化,任一变化或dirty为真就执行renderNow();
  • 订阅render:requested、repaint:requested(标脏)、viewport:changed(只调度)等编辑器事件;layer !== 'scene'时还订阅selection:changed;
  • shouldSuspendRender返回true时保持脏标记并重新调度,等条件解除后自动补上那一帧;
  • 同一编辑器的多个 surface 共享一个按编辑器实例缓存的requestAnimationFrame调度器(renderSchedulersWeakMap),避免每个画布各自发起 RAF 造成帧撕裂或浪费。

renderNow会把覆盖层障碍、渲染状态、文档图、文本编辑器、画布尺寸、标尺可见性、渲染层与"是否处于交互式编辑"一起交给state.renderer.renderFromEditorState(...)(lifecycle.ts),渲染完成后调用onPresented汇报renderVersion/sceneVersion。

4. 尺寸变化:ResizeObserver + RAF 合并

resize-observer.ts 用useResizeObserver监听画布,回调内先合并到requestAnimationFrame再执行resizeCanvas,避免一次帧内多次 resize 事件触发多次重建。resizeCanvas优先复用现有 GL 上下文直接replaceSurface并重渲染;如果 WebGL surface 重建失败,则回退到完整重建(createSurface(canvas, { reloadFonts: true }))并重新加载字体。

5. 宽色域(Display-P3)呈现与回退

surface 的色域不是简单的开关:CanvasKit 的 P3 屏幕表面要求浮点绘制缓冲区(Chromium 122+ 的drawingBufferStorage,WebKit/Firefox 无实现),否则会产生错误的颜色拷贝与混合模式。因此 color-space.ts 会按"文档色域为 display-p3 + 显示器支持 P3 + 硬件渲染器 + 浮点缓冲区可用"逐项探测,任一不满足就回退 sRGB;refreshPresentation还会监听graph:replaced与document:color-space-changed事件,在文档到达或色域切换后重建 surface,并通过onPresentation报告实际呈现色域。

6. 销毁与资源释放

整个生命周期挂在 Vue 的onScopeDispose上(lifecycle.ts):组件卸载时置destroyed标记、取消 ResizeObserver、暂停渲染循环、从编辑器移除并销毁SkiaRenderer、释放 GL 上下文。releaseGLContext还会用ck.deleteContext注销 CanvasKit 持有的 WebGL 上下文,必要时让一个 1×1 的"parking context"接管当前上下文,避免 CanvasKit 因残留 current context 而让画布及其 DOM 子树无法被 GC(gl-surface.ts)。

与相关 API 的配合方式

API配合点
useEditoruseCanvas(canvasRef, editor, ...)的第二参数;必须先有provideEditor上下文
useCanvasInput接收useCanvas返回的三个命中测试函数,负责选择、拖拽、缩放、旋转、平移、钢笔/绘制、文本编辑交互
useTextEdit画布文本编辑交互,与画布管线同层使用
CanvasRoot无头结构组件,内部调用useCanvas并通过provideCanvas注入上下文,适合"SDK 管结构、应用管布局样式"的场景
CanvasSurface渲染真正的<canvas>元素,挂载后把元素回填给CanvasRoot的canvasRef

使用注意事项

  • useCanvas()面向渲染器且实践上仅限浏览器使用(依赖window、requestAnimationFrame、WebGL、ResizeObserver),不要在 SSR/Node 环境直接调用;
  • 它负责的是实时画布渲染管线(surface、帧调度、resize、标尺、就绪回调),不负责应用层级的文件打开/保存等流程——那些属于编辑器与 IO 层;
  • 需要交互时,务必与useCanvasInput()配对,否则画布只能显示而无法响应选择、拖拽等操作;
  • 需要完整画布结构但不想自己管理 canvas 引用时,优先使用CanvasRoot+CanvasSurface;需要多窗格、多层画布或自定义布局时才直接用useCanvas(参考 EditorCanvas.vue 的生产用法);
  • 多个画布共享同一编辑器时,通过getRenderState各自提供独立的视图状态,即可实现"共享文档图与历史、独立视图与渲染层"。

相关文档导航

  • composable 总览:SDK Composables
  • 编辑器上下文:useEditor
  • 交互接线:useCanvasInput
  • 无头组件:CanvasRoot / CanvasSurface
  • SDK 概览与安装:packages/vue/README.md
  • 前端
  • 桌面应用
  • AI 应用
  • MCP 服务

【免费下载链接】open-pencil

AI-native design editor. Open-source Figma alternative.

项目地址:https://gitcode.com/gh_mirrors/op/open-pencil
点击查看免费下载
上一篇:k-skill 的 s2b-notice-search:S2B 学校市场公告查询的零依赖 CommonJS 工具包解析
下一篇:Describe your changes

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询