@machete-jhun/canvas-studio
v0.2.3
Published
一个基于 React 和 Konva 的画布编辑组件库,支持多种图形和媒体元素的添加与编辑,适用于构建复杂的图形应用。
Readme
Canvas Studio
基于 React + Konva 的高性能画布编辑与渲染引擎。采用 Context + Store 独立引擎架构,提供与 Figma/Sketch 类似的画布编辑体验。
安装
npm install canvas-studio
# 或
pnpm add canvas-studio核心架构 (Engine & Provider)
为了解决 React 渲染画布时的性能瓶颈,Canvas Studio 采用了 引擎模型分离 架构:
CanvasEditorEngine: 独立于 React 存在的纯状态控制器,负责调度画布的所有数据流(图形增删改、历史记录、选中状态等)。CanvasEditorProvider: 基于 React Context 注入引擎实例。useCanvasStudio及一系列 Hooks: 基于useSyncExternalStore,允许组件只订阅自己关心的数据切片,避免整个画布树的灾难性 Re-render。
快速使用
1. 顶层包裹 Provider
使用 <CanvasEditorProvider> 包裹你的编辑器视图。
import { CanvasEditorProvider, CanvasStudio } from 'canvas-studio'
function App() {
return (
// ignoredHistoryShapeTypes 用于声明哪些类型的图形【不进入撤销/重做栈】
// 例如背景板类型,不会因为用户的撤销操作而丢失当前状态
<CanvasEditorProvider ignoredHistoryShapeTypes={['BACK_SCREEN']}>
<EditorView />
</CanvasEditorProvider>
)
}2. 渲染画布与使用 Hooks 操作状态
在子组件中,渲染 <CanvasStudio /> 视图组件,并通过暴漏的 Hooks 获取和修改引擎数据。
import {
CanvasStudio,
useCanvasStudio,
useShapeList,
useSelectedShapeIds,
useHistoryState
} from 'canvas-studio'
function EditorView() {
// 获取引擎实例,用于执行操作(增删改、撤销重做等)
const engine = useCanvasStudio()
// 订阅响应式状态数据
const shapes = useShapeList()
const selectedIds = useSelectedShapeIds()
const { canUndo, canRedo } = useHistoryState()
// 添加图形
const handleAddRect = () => {
engine.addShapes([{
id: 'rect-1',
type: 'RECT',
x: 100, y: 100, width: 100, height: 100, fill: '#ff0000'
}])
}
// 获取底层 Konva 的 Transformer 实例(例如想要强制刷新包围盒尺寸)
const handleRefreshBox = () => {
const tr = engine.getTransformer()
tr?.forceUpdate()
}
return (
<div style={{ display: 'flex', flexDirection: 'column', height: '100vh' }}>
<div className="toolbar">
<button onClick={handleAddRect}>添加矩形</button>
<button onClick={engine.undo} disabled={!canUndo}>撤销</button>
<button onClick={engine.redo} disabled={!canRedo}>重做</button>
<button onClick={engine.fitToContent}>居中适应画面</button>
</div>
<div style={{ flex: 1 }}>
{/* 画布纯视觉容器 */}
<CanvasStudio
width={800}
height={600}
backgroundType="grid"
/>
</div>
</div>
)
}API 参考
<CanvasStudio /> Props
负责纯视觉与交互层面的容器组件配置:
| 属性 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| width | number | 800 | 画布初始宽度 |
| height | number | 600 | 画布初始高度 |
| className | string | - | 自定义容器类名 |
| renderMode | boolean | false | 开启后进入只读渲染模式:关闭辅助线、游标、选中态与变形控制框等所有交互 |
| backgroundType | 'grid' \| 'point' \| 'none' | 'grid' | 画布背景底纹样式 |
| hideRuler | boolean | false | 是否隐藏周边的坐标标尺 |
| disableRulerAuxiliary | boolean | false | 是否禁用从标尺拖拽生成辅助线的功能 |
| ppmm | number | 1 | 物理像素映射比:每毫米单位等于多少级像素 |
| auxiliaryLineColor | string | - | 辅助线的全局颜色 |
| auxiliaryLineDashType | 'solid' \| 'dash1' \| 'dash2' | - | 辅助线的全局虚线类型 |
| boundary | BoundaryRect | - | 约束矩形区域(内容坐标系)。设置后图形拖拽/缩放将限制在该区域内,舞台缩放不低于初始适配比例,舞台平移也会被夹拢使边界始终在屏幕可见范围内 |
| boundaryStyle | BoundaryStyle | - | 边界矩形的视觉样式,支持 stroke / strokeWidth / dash 自定义虚线外框 |
| boundarySvgMask | string[] | - | SVG 路径字符串数组,用于在边界区域上层叠加一层设备形状遮罩预览(如圆形屏幕轮廓) |
CanvasEditorEngine (引擎实例接口)
通过 const engine = useCanvasStudio() 获取,提供了丰富的底层命令:
视图控制
getStage(): 获取底层的Konva.Stage实例。getTransformer(): 获取底层的Konva.Transformer实例(常用于在外部修改图形尺寸后主动调用其forceUpdate()重算包围盒)。fitToContent(): 根据当前画布中的内容元素自动缩放、居中平移视图,使所有内容都展示在最佳视野内。
集合 / 元素管理
setShapes(shapes): 覆盖所有图形(会清空选中状态并记录一次历史)。addShapes(shapes): 增加新的图形。updateShape(shape)/updateShapes(shapes): 更新选定图形的数据。removeShapes(ids): 移除特定 ID 的图形。clearShapes(): 清空画布。duplicateShape(shape): 克隆图形到右下方错开的位置,并自动选中。
背景管理 (挂载在不同层,永远垫底)
同上提供了一套操作底层背景节点的方法:setBgShapes, addBgShapes, removeBgShapes, removeBgShape, clearBgShapes。
选中与状态
setSelectedShapeIds(ids): 覆盖设定当前选中的节点 ID 集合。addSelectedShapeId(id): 将某组节点设为激活。removeSelectedShapeId(id): 取消某个节点的激活。
历史栈
undo(): 撤销。redo(): 重做。
运行时数据字典
updateRuntimeVariables(variables, mode = 'merge' | 'replace'): 更新运行时动态注入在文本、状态机动画等上的变量依赖字典。该过程纯数据化向视图推流,防止由于数据更迭引起的 React 组件树销毁重叠。
Hooks 切片获取器 (Selectors)
| Hook | 返回值 | 作用描述 |
|------|--------|----------|
| useCanvasStudio() | CanvasEditorEngine | 获取执行命令的引擎单例。 |
| useShapeList() | MyShapeConfig[] | 响应式获取操作区的图形集合列表。 |
| useBgShapeList() | MyShapeConfig[] | 响应式获取背景区的图形集合列表。 |
| useSelectedShapeIds() | string[] | 响应式获取当前处于选中聚焦状态的节点 ID 数组。 |
| useRuntimeVariables() | Record<string, unknown> | 响应式获取当前的运行时动态变量字典。 |
| useHistoryState() | { canUndo: boolean, canRedo: boolean } | 响应式获取当前撤销重做按钮的状态。 |
.your-canvas-wrapper {
/* 画布容器整体背景,可使用颜色或 CSS gradient */
--background: linear-gradient(180deg, #0e0e44 0%, #3b2863 100%);
/* backgroundType="grid" 棋盘格模式 */
--grid-color: rgba(0, 0, 0, 0.08); /* 棋盘深色格颜色 */
--grid-bg-color: #ffffff; /* 棋盘浅色格(底色)颜色 */
/* backgroundType="point" 点阵模式 */
--point-color: #2f3542; /* 点的颜色 */
--point-bg-color: #ffffff; /* 点阵背景色 */
}所有变量均有内置的安全默认值,未注入时自动回退。示例:
import './inject.css' // 包含上述 CSS 变量定义
<div className="your-canvas-wrapper">
<CanvasStudio backgroundType="grid" />
</div>核心 Hooks 与 Engine 调度中心 (Core Pipeline)
不要使用常规状态变量,你需要的所有下发操作句柄全由 const engine = useCanvasStudio() 获取调度抛出:
engine.addShapes(shapes)/engine.removeShapes(ids): 压入与剔除正常渲染层的组件节点,动作入史。engine.updateShape(shape)/engine.updateShapes([shape]): 注入节点更替,属性深度重置。engine.setShapes(shapes): 一键替换当前渲染层的全部节点,动作入史。engine.addBgShapes(shapes)/engine.removeBgShape(id): 操作脱离标准渲染层的网格底层安全底色图形。engine.setSelectedShapeIds(ids): 直接下发控制指令将目标设定为框选焦点。engine.setBgShapes(shapes): 一键替换底部层的全部节点,动作入史。engine.undo()/engine.redo(): 后退与重做。底层自建隔离带跳过且清扫由ignoredHistoryShapeTypes圈定的免疫目标层。engine.fitToContent(): 一键扫描全局边界,将最大包含框按比例吸附拉放至视野正中。engine.updateRuntimeVariables(variables, mode): 向下派发非反应式变量对象池,只走 Konva 树进行局部字段的底层复写,无额外内存穿透影响。
对外的视图响应式 Hooks
useShapeList(): 获取主显示图层MyShapeConfig[]实例数据。useBgShapeList(): 获取底部层MyShapeConfig[]数据。useSelectedShapeIds(): 获取活跃选择目标的string[]集合。useActiveShape(): 快捷获取单目标下该选中的 Config。useHistoryState(): 返回操作栈上下界限:{ canUndo: boolean, canRedo: boolean }。useRuntimeVariables(): 获取渲染层当前的全局变量体。
🛠️ 导出路径结构说明
由于包体细分打包,你需要去往各个精确口读取能力:
- 主组件/类型层:
@machete-jhun/canvas-studio - 后台纯计算离屏导出域:
@machete-jhun/canvas-studio/render - 杂项工具域:
@machete-jhun/canvas-studio/utils - 无状态原子生成器:
@machete-jhun/canvas-studio/utils/shapeTools - 静态枚举与常量字面:
@machete-jhun/canvas-studio/constants
Boundary Constraint(边界约束系统)
通过向 <CanvasStudio /> 传入 boundary prop,可以激活一套完整的边界约束体系:
- 图形拖拽约束:所有可拖拽节点的
dragBoundFunc均被实时接管,图形超出边界时自动夹拢回区域内(节点尺寸大于边界时固定在边界起点)。 - Transformer 缩放约束:多选/单选的包围盒变换(
boundBoxFunc)同样限制在边界内,保证 resize 不会越界。 - 最小缩放保护:舞台缩放不会低于
boundary首次适配画布时的初始比例,防止边界缩到画布以外。 - 舞台拖拽夹拢:平移画布时,边界矩形的任意一侧都不会被拖出屏幕可见区域。
- BoundaryLayer 可视化:自动在底部渲染虚线边框;若传入
boundarySvgMask,则额外叠加 SVG 形状遮罩(适用于圆形屏幕等异形设备预览)。
相关类型
type BoundaryRect = {
x: number
y: number
width: number
height: number
}
type BoundaryStyle = {
stroke?: string // 虚线框颜色,默认 '#4096ff'
strokeWidth?: number // 线宽,默认 1
dash?: number[] // 虚线节奏,默认 [6, 3]
}使用示例
import { CanvasStudio } from 'canvas-studio'
function EditorView() {
const [boundary, setBoundary] = useState<BoundaryRect | undefined>()
return (
<CanvasStudio
width={800}
height={600}
boundary={boundary}
boundaryStyle={{ stroke: '#4096ff', strokeWidth: 1, dash: [6, 3] }}
// 圆形屏幕遮罩示例 (SVG 路径字符串,内容坐标系,边界适配后自动缩放)
boundarySvgMask={[
`<svg width="480" height="480" viewBox="0 0 480 480" xmlns="http://www.w3.org/2000/svg"> <circle cx="240" cy="240" r="240" fill="none" stroke="red" stroke-width="2"/> </svg>`,
]}
/>
)
}boundary 使用内容坐标系(与图形的 x / y / width / height 同一单位),引擎内部会自动将其转换为实时的 screen-space 后再参与夹拢计算,因此画布缩放和平移均不影响约束精度。
BackScreen(设备背景板)
MyBackScreenShape 是专为多屏硬件设备设计的特殊图形类型,用于在画布上标记一块物理屏幕的占位区域。
| 属性 | 类型 | 必填 | 说明 |
|------|------|------|------|
| screenId | string | ✓ | 设备唯一标识,对应硬件 chip ID |
| width | number | ✓ | 屏幕编码宽度(像素) |
| height | number | ✓ | 屏幕编码高度(像素) |
| fill | string | ✓ | 占位色(十六进制),用于编辑器预览 |
| screenShape | 'rect' \| 'circle' | - | 屏幕形状,默认矩形;圆形屏幕传 'circle' |
import { createBackScreen } from '@machete-jhun/canvas-studio/utils/shapeTools'
engine.addBgShapes([
createBackScreen({
screenId: 'A3F2C1',
width: 480,
height: 480,
fill: '#1a1a2e',
screenShape: 'circle', // 2.1 寸圆形屏
x: 100,
y: 100,
})
])screenShape 只影响编辑器内的视觉预览,不影响推流逻辑(设备硬件决定最终显示形状)。
License
MIT © Machete Jhun
