wang-canvas
v0.0.2
Published
Wang Canvas SDK - 通用 PC 与移动端 Canvas 白板 SDK
Maintainers
Readme
Wang Canvas SDK API
Wang Canvas 是一个原生 Canvas 白板 SDK,当前版本优先实现 Excalidraw 常用能力:无限画布、缩放/平移、选择/移动/缩放、矩形、圆角矩形、椭圆、菱形、线段/折线、箭头、自由绘制、文本自动换行、图片元素、精确橡皮、撤销/重做、复制/粘贴、层级调整、对齐吸附、复制/粘贴样式、元素锁定、统计信息、JSON/PNG 导出。
参考功能范围:Excalidraw README 中列出的 canvas whiteboard、hand-drawn style、rectangle/circle/diamond/arrow/line/free-draw/eraser、undo/redo、zoom and panning。
文件结构
sdk/
config/defaults.js 默认工具、视口、样式、编辑器配置
core/editor.js SDK 主类和公开 API
core/editorUtils.js 编辑器挂载、复制偏移、视口边界工具
core/events.js 事件订阅工具
core/history.js 撤销/重做历史栈
elements/factory.js 元素创建、标准化、草稿更新
elements/linear.js 线段/箭头点集、折线节点编辑工具
elements/freehand.js 自由书写点采样、简化、边界同步
elements/geometry.js 边界、缩放控制点、移动/缩放变换
elements/hitTest.js 命中检测
elements/image.js 图片元素标准化、加载缓存、文件读取
elements/eraser.js 精确橡皮擦与笔迹分段
elements/renderers.js Canvas 渲染
elements/selectionRenderer.js 单元素边界与多选公共变换框渲染
elements/style.js 元素样式字段提取
elements/text.js 文本测量与自动换行
elements/types.js 元素类型常量
core/stats.js 统计信息生成
interaction/constraints.js Shift/Alt 移动与缩放约束
interaction/controller.js Canvas 基础交互控制
interaction/controllerUtils.js 交互几何、光标与绘制校验工具
interaction/eraserSession.js 橡皮擦预览事务与连续路径采样
interaction/keyboard.js 键盘微移工具
interaction/snapping.js 元素边/中心对齐吸附
interaction/textEditor.js 文本内联编辑覆盖层
utils/ 通用工具sdk/index.js 是对外入口,预览项目在 preview/main.js 中通过该入口使用 SDK。
快速接入
import { TOOL, createWangCanvas } from "wang-canvas";
const editor = createWangCanvas(document.querySelector("#canvas"), {
defaultTool: TOOL.SELECT,
gridVisible: true,
defaultStyle: {
strokeColor: "#172033",
backgroundColor: "transparent",
fillStyle: "solid",
strokeStyle: "solid",
startArrowhead: "none",
endArrowhead: "triangle",
strokeWidth: 2,
roughness: 1,
opacity: 1,
roundness: 0,
fontSize: 24,
},
});容器元素需要有明确宽高,SDK 会在容器内创建并管理 <canvas>。
工具
import { TOOL } from "wang-canvas";| 工具 | 说明 | 快捷键 |
| --- | --- | --- |
| TOOL.SELECT | 选择、移动、缩放元素 | V |
| TOOL.HAND | 平移画布 | H / Space |
| TOOL.RECTANGLE | 矩形 | R |
| TOOL.ELLIPSE | 椭圆 | O |
| TOOL.DIAMOND | 菱形 | D |
| TOOL.LINE | 线段 | L |
| TOOL.ARROW | 箭头 | A |
| TOOL.DRAW | 自由书写,支持连续书写 | P |
| TOOL.TEXT | 文本 | T |
| TOOL.ERASER | 精确擦除自由书写笔迹,其他图形按元素删除 | E |
通用快捷键:Delete/Backspace 删除选择,Ctrl/Cmd + Z 撤销,Ctrl/Cmd + Y 或 Ctrl/Cmd + Shift + Z 重做,Ctrl/Cmd + C/V 复制粘贴,Ctrl/Cmd + D 快速复制,Ctrl/Cmd + A 全选,方向键按 1px 微移,Shift + 方向键 按 10px 微移。
层级快捷键:Ctrl/Cmd + ] 上移一层,Ctrl/Cmd + [ 下移一层,Ctrl/Cmd + Shift + ] 置顶,Ctrl/Cmd + Shift + [ 置底。
样式快捷键:Ctrl/Cmd + Alt + C 复制所选元素样式,Ctrl/Cmd + Alt + V 粘贴样式到当前选择。
选中单个元素后,顶部圆点可拖拽旋转;按住 Shift 旋转时会按 15 度吸附。多选时每个元素会保留独立的旋转边界,并额外显示一个公共虚线变换框;按住 Shift 框选会在原选择上追加元素。鼠标悬停到选区边框或缩放控制点时会显示对应方向的双箭头光标,并可直接拖拽缩放。移动时按住 Shift 锁定水平/垂直方向;缩放时按住 Shift 保持比例,按住 Alt 从中心缩放。绘制矩形、椭圆、菱形、线段、箭头时也支持 Shift 约束与 Alt 从中心绘制。
拖拽移动或缩放时,元素边缘/中心会自动吸附到其他元素的边缘/中心,并显示对齐辅助线。SDK 只提供复制、粘贴、层级、锁定、命中检测等能力 API;右键菜单、长按菜单、工具栏等各端交互入口由调用者自行实现,预览项目在 preview/App.vue 中演示了 PC 右键菜单。
线段和箭头支持 points 折线点集。选中单个线段/箭头后会显示节点控制点,拖拽节点可编辑折线;双击线段可插入中间点,按住 Alt 点击中间点可删除。调用方也可以通过公开 API 管理折线点。
自由书写工具会采集 pointer 合并事件以提升笔迹连续性,收笔时会简化点集并同步 x/y/width/height 边界。画笔收笔后默认保持 TOOL.DRAW,便于像 Excalidraw 一样连续书写;单击也会生成一个笔点。橡皮擦会对自由书写按笔迹片段精确擦除,并在需要时拆分为多个笔迹元素;按住指针时只灰显待擦元素,松开后才一次性提交场景和历史记录。
文本元素按固定 width 自动换行,双击文本会进入内联 textarea 编辑态,显示原生光标并支持拖选。
构造配置
createWangCanvas(target, {
readonly: false,
autoFocus: true,
keyboardShortcuts: true,
backgroundColor: "#f8fafc",
gridVisible: true,
gridSize: 32,
defaultTool: TOOL.SELECT,
switchToSelectAfterCreate: true,
alignmentSnapping: true,
snapThreshold: 6,
alignmentGuideColor: "#e03131",
selectionColor: "#2563eb",
confirmBeforeClear: true,
clearConfirmMessage: "确定要清空当前画布吗?",
eraser: {
precise: true,
size: 18,
pendingOpacity: 0.2,
},
viewportBounds: null,
scrollBoundsIndicatorColor: "rgba(37, 99, 235, 0.28)",
defaultStyle: {
strokeColor: "#172033",
backgroundColor: "transparent",
fillStyle: "solid",
strokeStyle: "solid",
startArrowhead: "none",
endArrowhead: "triangle",
strokeWidth: 2,
roughness: 1,
opacity: 1,
roundness: 0,
fontSize: 24,
fontFamily: "Segoe Print, Comic Sans MS, cursive",
},
zoom: {
min: 0.1,
max: 4,
step: 0.1,
},
historyLimit: 100,
});常用实例方法
工具与样式
editor.setTool(TOOL.ARROW);
editor.getTool();
editor.setStyle({
strokeColor: "#be123c",
backgroundColor: "#dbeafe",
fillStyle: "hachure",
strokeStyle: "dashed",
endArrowhead: "dot",
roundness: 16,
strokeWidth: 3,
});
editor.getStyle();元素管理
editor.insertElement("rectangle", {
x: 80,
y: 120,
width: 200,
height: 120,
angle: 0,
strokeColor: "#172033",
backgroundColor: "#dbeafe",
});
editor.addElement(element, { select: true });
editor.updateElement(elementId, { strokeColor: "#2563eb" });
editor.updateElements([elementId], { strokeWidth: 4 });
editor.removeElements([elementId], { confirmClear: true });
editor.clear();
editor.clear({ confirm: false });
const elements = editor.getElements();
editor.setElements(elements);图片元素由 SDK 提供能力入口,具体按钮、文件选择器、粘贴入口由调用方实现:
editor.insertImage({
src: "https://example.com/image.png",
x: 120,
y: 80,
width: 320,
height: 180,
crossOrigin: "anonymous",
});
await editor.insertImageFromFile(file, { x: 120, y: 80 }, { select: true });
await editor.insertImageFromClipboardData(event.clipboardData, { x: 120, y: 80 });层级、锁定、复制与样式
editor.duplicateSelection();
editor.duplicateElements(["element-id"], 32);
editor.bringForward();
editor.sendBackward();
editor.bringToFront(["element-id"]);
editor.sendToBack(["element-id"]);
editor.setElementsLocked(["element-id"], true);
editor.setElementsLocked(["element-id"], false);
editor.copySelectedStyle();
editor.copyElementStyle("element-id");
editor.pasteStyleToSelection();
editor.pasteStyleToElements(["element-id"]);clear() 默认会根据 confirmBeforeClear 弹出二次确认;需要跳过确认时可传 clear({ confirm: false })。通过键盘删除当前全部元素时,也会触发清空确认。
选择
editor.selectElements(["element-id"]);
editor.toggleSelection("element-id");
editor.getSelectedElements();
editor.getElementAtPoint({ x: 100, y: 100 });
editor.getElementAtPoint({ x: 100, y: 100 }, { coordinate: "screen", includeLocked: true });折线点编辑
editor.insertLinearPoint("line-id", { x: 180, y: 120 });
editor.updateLinearPoint("line-id", 1, { x: 200, y: 160 });
editor.removeLinearPoint("line-id", 1);线段和箭头元素的 points 字段使用相对元素起点的坐标;旧版只有 x/y/width/height 的线段数据会在加载时自动标准化为两个点。
历史
editor.undo();
editor.redo();视口
editor.setViewport({ x: 120, y: 80, zoom: 1.2 });
editor.getViewport();
editor.zoomBy(0.1);
editor.setZoomAt(1.5, { x: 400, y: 300 });
editor.zoomToFit({ padding: 90 });
const worldPoint = editor.screenToWorld({ x: 100, y: 100 });
const screenPoint = editor.worldToScreen({ x: 20, y: 20 });viewportBounds 可选配置 { minX, maxX, minY, maxY },配置后平移会被夹取到边界;缩放到最小/最大或平移触边时,画布会显示短暂边界提示。
统计
const stats = editor.getStats();
// { total, selected, locked, unlocked, byType, viewport }导入导出
const scene = editor.getScene();
const json = editor.exportToJSON();
editor.loadScene(scene);
editor.loadFromJSON(json);
const pngDataUrl = editor.exportToPNG({
padding: 32,
pixelRatio: 2,
backgroundColor: "#ffffff",
});exportToPNG() 返回 data URL,可直接赋给 <img src> 或用于下载。
事件
const offChange = editor.on("change", (scene) => {
renderElementCount(scene.elements.length);
});
editor.on("toolChange", (tool) => {});
editor.on("selectionChange", (selectedElements) => {});
editor.on("styleChange", (style) => {});
editor.on("viewportChange", (viewport) => {});
editor.on("statsChange", (stats) => {});
offChange();场景数据格式
{
type: "wang-canvas",
version: 1,
elements: [],
appState: {
viewport: { x: 0, y: 0, zoom: 1 },
style: {}
}
}元素核心字段:
{
id: "element_xxx",
type: "rectangle",
x: 80,
y: 120,
width: 200,
height: 120,
angle: 0,
strokeColor: "#172033",
backgroundColor: "transparent",
fillStyle: "solid",
strokeStyle: "solid",
startArrowhead: "none",
endArrowhead: "triangle",
strokeWidth: 2,
roughness: 1,
opacity: 1,
roundness: 0,
locked: false,
seed: 123,
version: 1,
createdAt: 1710000000000,
updatedAt: 1710000000000
}angle 使用弧度。自由书写元素额外包含 points: [{ x, y }],点坐标为世界坐标,触控笔环境下可能带有可选 pressure。线段/箭头元素也包含 points,但坐标相对元素起点。文本元素额外包含 text、fontSize、fontFamily。图片元素额外包含 src、alt、fileName、crossOrigin。
销毁
editor.destroy();销毁会移除事件监听、断开尺寸监听,并删除 SDK 创建的 canvas。
依赖说明
SDK 本身不引入第三方运行时依赖。当前项目已有 Vite 作为开发依赖,是否安装或运行由使用者自行决定。
