sk-wasm-surface
v0.1.2
Published
Platform-agnostic Skia/CanvasKit-Wasm surface renderer for React hosts
Maintainers
Readme
sk-wasm-surface
基于 CanvasKit-Wasm 的平台无关 Skia 表面渲染引擎,供 Electron 与 Capacitor 等宿主复用。
设计原则:本包负责「渲染 + 画布交互 + 画布状态」,即输入场景数据→绘制,以及平移/缩放/框选/拖拽/手绘/连线/撤销等交互。数据持久化、IPC/HTTP、快捷键配置、上层 React UI(toolbar/overlay)与 Redux 仍由宿主提供——这些通过 configureCanvasHost() 注入适配器接入,引擎不直接 import 任何 app 源码(由 scripts/check-boundaries.mjs 强制约束)。
包含什么
core/引擎核心(CanvasEngine、engine单例、RenderContext/ViewState类型)renderers/各类渲染器单例(node / connection / selection / mindmap-group / pinned …)drawers/各卡片/元素绘制器layers/、animation/、rich-text/、elements/、utils/绘制相关模块minimap/纯绘制小地图MinimapCanvas+ 坐标工具(不含与 store 绑定的CanvasKitMinimap)store.ts画布状态(Zustand,useCanvasStore)hooks/画布交互 hooks(平移/缩放/框选/拖拽use-canvas-interaction、连线、手绘、演示、撤销)canvas-undo.ts撤销快照host/宿主注入适配器(configureCanvasHost):交互层用到的serviceHandler/dataGen/getShortcutKeyByAction在此注入lib/从 app 抽出的纯逻辑/类型副本(flow-utils 子集、mindmap-layout、whiteboard-preview、focus-mode、env、interface 类型、枚举、shortcuts 枚举、space-preview-guard、highlight 图标 path)wasm/canvaskit.wasmCanvasKit 运行时(供宿主拷贝到 web 资源目录)MyCanvasKit.tsx完整画布 React 组件(原index.tsx),平台相关依赖经CanvasKitHostProvider注入各
*-toolbar.tsx/*-overlay.tsx画布浮层与工具条 UI
不包含(留在宿主 app 的「集成层」)
service/store(Redux)/lib/data 等的具体实现、各 slice、路由/主题/i18n 的具体接入。这些通过 configureCanvasHost()(交互层)与 <CanvasKitHostProvider>(组件层)注入。
开箱即用:直接渲染整块画布
import {
MyCanvasKit,
CanvasKitHostProvider,
configureCanvasHost,
} from "sk-wasm-surface";
import { useRouter } from "next/router"; // Electron;Capacitor 用等价 stub
import { useTheme } from "next-themes";
// 1) 交互层注入(启动时一次)
configureCanvasHost({
serviceHandler, dataGen: { spaceId, userId }, getShortcutKeyByAction,
});
// 2) 组件层注入(包裹 MyCanvasKit)
function CanvasHost() {
const router = useRouter();
const { resolvedTheme } = useTheme();
return (
<CanvasKitHostProvider
value={{
resolvedTheme,
i18n,
router, // Capacitor: { pathname, query, push } stub
getTargetCardFromState, // 从 Redux 取卡片展示数据
screenshotVideo, // 视频/网页截帧
DrawingStrokeToolbar, // app 手绘工具条
LoadingIndicator, // 可选,默认内置简易实现
actions: { updateWebsiteCard, updateImageGallery, updateWhiteboard },
}}
>
<MyCanvasKit /* 原有 props 不变 */ />
</CanvasKitHostProvider>
);
}移动端复用全部画布(渲染 + 交互 + UI),只需提供上面这些注入项;其中 router 用一个 { pathname, query, push } stub 即可,serviceHandler 接你已实现的移动端数据库。
注入宿主能力(必做)
交互层依赖三项宿主能力,应用启动时注入一次:
import { configureCanvasHost } from "sk-wasm-surface";
configureCanvasHost({
// 通用服务调度。交互层会调用:
// createDrawingStroke / updateDrawingStroke / deleteDrawingStroke / openFileInShell
// Electron 走 window.ipc;Capacitor 走你已实现的移动端数据库或 HTTP。
serviceHandler: (apiName, params) => myServiceHandler(apiName, params),
// 当前身份(新建笔画等需要)
dataGen: { spaceId: currentSpaceId, userId: currentUserId },
// 由 action 取当前生效快捷键(含用户自定义),配合 ShortcutActions 枚举
getShortcutKeyByAction: (action) => myShortcutMap[action],
});⚠️ store 必须单例:画布状态(
useCanvasStore)现在归本包所有。宿主务必从sk-wasm-surface引用useCanvasStore,不要再保留 app 内旧的store.ts副本,否则会出现两个互不同步的 store 实例。zustand/react设为 peerDependency 也是为避免重复实例。
构建
cd packages/canvaskit
npm install # 或在仓库根用 yarn(已配置 workspaces)
npm run build # tsup → dist/(ESM + d.ts)
npm run typecheck # tsc --noEmit
npm run check-boundaries # 校验无逃逸 import已验证:
tsc --noEmit通过、边界检查通过。
在 Capacitor 移动端使用
安装
sk-wasm-surface与 peer 依赖canvaskit-wasm(以及react,如使用 React 包装组件)。拷贝 wasm 到 web 资源目录。引擎用绝对路径
/canvaskit/canvaskit.wasm加载(见utils/canvaskit-loader.ts的CANVASKIT_BASE_PATH)。在构建脚本里把 wasm 拷到你的webDir/canvaskit/:cp node_modules/sk-wasm-surface/wasm/canvaskit.wasm <webDir>/canvaskit/canvaskit.wasm # 然后 npx cap copyCapacitor 的 WebView 把
webDir服务于capacitor://localhost(iOS)/https://localhost(Android),/canvaskit/canvaskit.wasm即可解析,无需改 loader。绘制:创建
<canvas>,用makeSurfaceForCanvas(canvasEl)拿到 Surface,构造RenderContext(注入getMindmap/getWhiteboardById/getEmbedCardData等数据回调,数据来自你已实现的移动端数据库),调用各 renderer 绘制。注意事项:
- WebGL2:老设备 WebView 若不支持,
MakeCanvasSurface可能返回 null,需软件渲染兜底。 - 触摸:交互层不在本包内,宿主接 Pointer/Touch 事件后更新数据再触发重绘。
- viewport:画布容器建议
touch-action: none并禁用 WebView 双指缩放/橡皮筋滚动。
- WebGL2:老设备 WebView 若不支持,
基本用法
import {
makeSurfaceForCanvas,
engine,
nodeRenderer,
type RenderContext,
} from "sk-wasm-surface";
const res = await makeSurfaceForCanvas(canvasEl);
if (!res) throw new Error("WebGL/CanvasKit surface 创建失败");
const { CanvasKit, surface } = res;
surface.requestAnimationFrame((skCanvas) => {
const ctx: RenderContext = {
CanvasKit, skCanvas, dpi: window.devicePixelRatio,
theme: "light", view: { translateX: 0, translateY: 0, scale: 1 },
canvasWidth: canvasEl.width, canvasHeight: canvasEl.height,
lodLevel: "high",
getWhiteboardById: (id) => myDb.getWhiteboardMeta(id), // 来自移动端数据库
// …其余数据回调
};
nodeRenderer.render(/* nodes */ [], ctx);
});让现有 Electron app 改为消费本包(迁移步骤)
这些步骤涉及删除 app 内重复文件与重写 import,建议在本地带构建运行(逐步替换、随时
tsc/nextron dev验证)。
- 仓库根已加
workspaces: ["packages/*"];执行一次yarn install让sk-wasm-surface链接。 - 在应用启动处调用
configureCanvasHost({ serviceHandler, dataGen, getShortcutKeyByAction })注入宿主能力。 - 把 app 中对渲染层 + 交互层 + store 的本地 import 改为包引用,例如
import { useCanvasStore } from "./store"→from "sk-wasm-surface";import { useCanvasInteraction } from "./hooks/use-canvas-interaction"→from "sk-wasm-surface";import { nodeRenderer } from "./renderers/node-renderer"→from "sk-wasm-surface"。 主要集中在renderer/components/canvaskit/index.tsx。 - 确认通过后,删除
renderer/components/canvaskit/下已迁入本包的目录与文件 (core/ drawers/ renderers/ layers/ animation/ rich-text/ elements/ utils/ minimap/ hooks/、types.ts、store.ts、canvas-undo.ts),仅保留集成层(index.tsx、各 toolbar/overlay)。 - store 单例:确保 app 内不再存在旧的
store.ts,所有useCanvasStore都来自包,避免双实例。 - 枚举/常量:本包内
lib/enums.ts(EPinStyle/EPinPosition)与lib/shortcuts.ts(ShortcutActions)是 app 端constants.tsx/shortcuts-list.tsx的副本,需保持同步(已在文件注释标注)。getShortcutKeyByAction的实现留在 app(读用户自定义配置),通过 host 注入。
CI 建议
在 lint/CI 中加入 npm run check-boundaries,防止渲染引擎再次与 app 的 store/service/UI 耦合。
性能
画布平移/缩放的性能优化(问题定位、各项措施、最终采用的「缩放手势期栅格缓存」架构、取舍与待办)见 ZOOM-PERFORMANCE.md。
