xiaodao-painter 0.1.8 → 0.1.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.ZH.md +362 -0
- package/README.md +259 -85
- package/package.json +17 -3
package/README.ZH.md
ADDED
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
# 小刀画板工具 (Xiaodao Painter)
|
|
2
|
+
|
|
3
|
+
**中文** | [English](./README.md) | [演示](https://painter.xdz.me)
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/xiaodao-painter)
|
|
6
|
+
[](LICENSE)
|
|
7
|
+
[](https://www.npmjs.com/package/xiaodao-painter)
|
|
8
|
+
[](https://vuejs.org/)
|
|
9
|
+
[](https://www.typescriptlang.org/)
|
|
10
|
+
[](https://vitejs.dev/)
|
|
11
|
+
|
|
12
|
+
一个自包含、可嵌入的 Vue 3 SVG 画板 / 绘图组件:把功能完整的画布(自由铅笔、基础图形、文本、选择与变换、平移缩放、撤销重做、内部剪贴板、颜色系统、明/暗主题、内置中英文国际化)打包进单个组件,并通过一个 `v-model` 驱动。
|
|
13
|
+
|
|
14
|
+

|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## 功能特性
|
|
19
|
+
|
|
20
|
+
### 绘制与编辑
|
|
21
|
+
|
|
22
|
+
- **9 种工具**:选择、平移、铅笔、直线、矩形、圆、三角形、五角星、文本(另含独立的缩放工具)。
|
|
23
|
+
- **选择与变换**:单击选择;空白拖动框选;拖动已选图形移动;8 个手柄缩放(Shift = 对称 / 中心扩展);图层顺序(上移 / 下移)。
|
|
24
|
+
- **平移与缩放**:拖动平移画布;离散缩放档位(10%–1000%)以光标为锚点;触屏支持双指捏合缩放。
|
|
25
|
+
- **撤销 / 重做**:完整的笔画级撤销/重做栈。
|
|
26
|
+
- **内部剪贴板**:剪切、复制、粘贴(粘贴时偏移 +20px)。
|
|
27
|
+
- **颜色系统**:描边(前景)、填充(背景)、文本颜色三个色槽;可对选中图形逐个应用颜色,画布背景支持纯色或透明网格。
|
|
28
|
+
- **文本工具**:单击放置可编辑文本框;双击已有文本进行编辑。
|
|
29
|
+
|
|
30
|
+
### 交互
|
|
31
|
+
|
|
32
|
+
- **指针与触屏**:统一的 `useDrawing` 交互层同时处理鼠标与触摸路径,包括双指捏合缩放、拖动绘制/选择。
|
|
33
|
+
- **缩放手柄**:图形 8 个、直线 2 个端点,拖动时实时预览。
|
|
34
|
+
- **修饰键**:`Ctrl` = 锁定 1:1 比例(闭合图形);`Shift` = 中心扩展 / 直线 45° 吸附。
|
|
35
|
+
- **快捷键**:Delete、Esc、Ctrl/⌘+Z / Ctrl/⌘+Y / Ctrl/⌘+X / Ctrl/⌘+C / Ctrl/� (V。
|
|
36
|
+
- **SVG 渲染**:每一条笔画都是真实的 SVG 元素(`<path>`、`<line>`、`<ellipse>`、`<rect>`、`<polygon>`、`foreignObject`),输出与分辨率无关、易于导出。
|
|
37
|
+
|
|
38
|
+
### 视觉与主题
|
|
39
|
+
|
|
40
|
+
- **明 / 暗主题**:由 CSS 变量(`--wb-*`)驱动,切换主题只是换一个 class。
|
|
41
|
+
- **国际化**:内置中文(`zh-CN`)与英文(`en-US`),工具栏文案与缩放指示均走 `t()` 字典。
|
|
42
|
+
- **双向绑定**:通过 `v-model` 读写整份绘图数据。
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## 安装
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
# npm
|
|
50
|
+
npm install xiaodao-painter
|
|
51
|
+
# pnpm
|
|
52
|
+
pnpm add xiaodao-painter
|
|
53
|
+
# yarn
|
|
54
|
+
yarn add xiaodao-painter
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### 对等依赖
|
|
58
|
+
|
|
59
|
+
- `vue` `^3.0.0`
|
|
60
|
+
|
|
61
|
+
需要引入一次样式文件:
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
import 'xiaodao-painter/style.css'
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
## 快速开始
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
# 克隆仓库
|
|
73
|
+
git clone https://github.com/xiaodaozhi/xiaodao-painter.git
|
|
74
|
+
cd xiaodao-painter
|
|
75
|
+
|
|
76
|
+
# 安装依赖
|
|
77
|
+
npm install
|
|
78
|
+
|
|
79
|
+
# 启动开发服务器(演示页)
|
|
80
|
+
npm run dev
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
在浏览器打开 `http://localhost:5173` 查看演示应用。
|
|
84
|
+
|
|
85
|
+
要构建独立演示页,运行 `npm run build:demo` 并打开 `dist-demo/index.html`。
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## 基础用法
|
|
90
|
+
|
|
91
|
+
```vue
|
|
92
|
+
<template>
|
|
93
|
+
<Painter
|
|
94
|
+
v-model="data"
|
|
95
|
+
theme="light"
|
|
96
|
+
locale="zh-CN"
|
|
97
|
+
/>
|
|
98
|
+
</template>
|
|
99
|
+
|
|
100
|
+
<script setup lang="ts">
|
|
101
|
+
import { ref, watch } from 'vue'
|
|
102
|
+
import { Painter } from 'xiaodao-painter'
|
|
103
|
+
import type { PainterData } from 'xiaodao-painter'
|
|
104
|
+
import 'xiaodao-painter/style.css'
|
|
105
|
+
|
|
106
|
+
const data = ref<PainterData>({
|
|
107
|
+
strokes: [],
|
|
108
|
+
canvasWidth: 800,
|
|
109
|
+
canvasHeight: 600,
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
watch(data, (val) => {
|
|
113
|
+
console.log('绘图数据变化:', val)
|
|
114
|
+
}, { deep: true })
|
|
115
|
+
</script>
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### 指定尺寸与暗色主题
|
|
119
|
+
|
|
120
|
+
```html
|
|
121
|
+
<Painter
|
|
122
|
+
v-model="data"
|
|
123
|
+
theme="dark"
|
|
124
|
+
locale="en-US"
|
|
125
|
+
:width="1200"
|
|
126
|
+
:height="800"
|
|
127
|
+
/>
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### 自定义画布背景
|
|
131
|
+
|
|
132
|
+
```html
|
|
133
|
+
<Painter
|
|
134
|
+
v-model="data"
|
|
135
|
+
:modelValue="{
|
|
136
|
+
strokes: [],
|
|
137
|
+
canvasWidth: 800,
|
|
138
|
+
canvasHeight: 600,
|
|
139
|
+
canvasBackgroundColor: '#f5f5f5',
|
|
140
|
+
}"
|
|
141
|
+
/>
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
---
|
|
145
|
+
|
|
146
|
+
## Props(属性)
|
|
147
|
+
|
|
148
|
+
| 属性 | 类型 | 默认值 | 说明 |
|
|
149
|
+
|------|------|---------|------|
|
|
150
|
+
| `modelValue` (v-model) | `PainterData` | `{ strokes: [], canvasWidth: 800, canvasHeight: 600 }` | 双向绑定的绘图数据,支持 `canvasBackgroundColor`。 |
|
|
151
|
+
| `theme` | `'light' \| 'dark'` | `'light'` | 界面主题。 |
|
|
152
|
+
| `locale` | `'zh-CN' \| 'en-US'` | `'zh-CN'` | 界面语言,传入其他值回退到 `zh-CN`。 |
|
|
153
|
+
| `width` | `string \| number` | `'100%'` | 组件宽度(CSS 值或 px 数值)。 |
|
|
154
|
+
| `height` | `string \| number` | `'100%'` | 组件高度(CSS 值或 px 数值)。 |
|
|
155
|
+
|
|
156
|
+
---
|
|
157
|
+
|
|
158
|
+
## 数据模型
|
|
159
|
+
|
|
160
|
+
整份绘图由一个可序列化的 `PainterData` 对象描述,便于持久化、撤销重做与协作。
|
|
161
|
+
|
|
162
|
+
```ts
|
|
163
|
+
interface PainterData {
|
|
164
|
+
strokes: Stroke[]
|
|
165
|
+
canvasWidth: number
|
|
166
|
+
canvasHeight: number
|
|
167
|
+
canvasBackgroundColor?: string // 任意 CSS 颜色或 'transparent'
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
type ToolType =
|
|
171
|
+
| 'select' | 'pan' | 'zoom'
|
|
172
|
+
| 'pencil' | 'line' | 'circle' | 'rect' | 'triangle' | 'star'
|
|
173
|
+
| 'text'
|
|
174
|
+
|
|
175
|
+
interface Point { x: number; y: number }
|
|
176
|
+
|
|
177
|
+
interface Stroke {
|
|
178
|
+
id: string // crypto.randomUUID()
|
|
179
|
+
type: ToolType
|
|
180
|
+
x: number // 包围盒左上角(提交时归一化)
|
|
181
|
+
y: number
|
|
182
|
+
width: number // 包围盒宽(绘制中可能为负)
|
|
183
|
+
height: number
|
|
184
|
+
points: Point[] // 铅笔路径点;非铅笔图形为空
|
|
185
|
+
strokeColor: string
|
|
186
|
+
fillColor: string // 铅笔与直线为 'transparent' / 'none'
|
|
187
|
+
strokeWidth: number
|
|
188
|
+
// 仅文本图形
|
|
189
|
+
text?: string
|
|
190
|
+
fontSize?: number
|
|
191
|
+
textAlign?: 'left' | 'center' | 'right'
|
|
192
|
+
textColor?: string
|
|
193
|
+
textAutoWidth?: boolean
|
|
194
|
+
}
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
坐标统一保存在**画布(SVG 用户)坐标系**中,与缩放和平移无关。组件在提交时会把负宽高的图形归一化,保证存储的包围盒始终为正。
|
|
198
|
+
|
|
199
|
+
### 类型导出
|
|
200
|
+
|
|
201
|
+
```ts
|
|
202
|
+
import type {
|
|
203
|
+
PainterData, // 主数据接口
|
|
204
|
+
Stroke, // 单条笔画 / 图形 / 文本
|
|
205
|
+
Point, // { x: number; y: number }
|
|
206
|
+
ToolType, // 所有工具标识的联合类型
|
|
207
|
+
CanvasConfig, // 画布尺寸 / 背景配置
|
|
208
|
+
} from 'xiaodao-painter'
|
|
209
|
+
|
|
210
|
+
import {
|
|
211
|
+
DEFAULT_CANVAS_WIDTH,
|
|
212
|
+
DEFAULT_CANVAS_HEIGHT,
|
|
213
|
+
STROKE_WIDTH,
|
|
214
|
+
DEFAULT_FONT_SIZE,
|
|
215
|
+
HIT_THRESHOLD,
|
|
216
|
+
} from 'xiaodao-painter'
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
---
|
|
220
|
+
|
|
221
|
+
## 架构
|
|
222
|
+
|
|
223
|
+
```src/
|
|
224
|
+
├── index.ts # 公开入口:导出 Painter 与类型/常量
|
|
225
|
+
├── App.vue # 演示根组件(设置页面标题、挂载组件)
|
|
226
|
+
├── style.css # 全局主题 CSS 变量(明/暗)
|
|
227
|
+
└── components/painter/
|
|
228
|
+
├── Painter.vue # 公开组件:props、v-model 同步、主题/语言注入
|
|
229
|
+
├── Whiteboard.vue # SVG 画布、指针/触摸处理、缩放 UI、文本编辑
|
|
230
|
+
├── Toolbar.vue # 工具按钮、颜色指示、画布尺寸弹窗
|
|
231
|
+
├── ColorPalette.vue # 前景/填充/文本色槽的颜色选择器
|
|
232
|
+
├── SelectionOverlay.vue # 包围框 + 8 个缩放手柄(直线仅 2 个)
|
|
233
|
+
├── StrokeRenderer.vue # 把单个 Stroke 渲染为 SVG 元素
|
|
234
|
+
├── composables/
|
|
235
|
+
│ ├── useDrawing.ts # 交互引擎(绘制/选择/移动/缩放/平移/文本)
|
|
236
|
+
│ └── useI18n.ts # locale、theme、根元素的 provide/inject
|
|
237
|
+
├── stores/
|
|
238
|
+
│ ├── canvas.ts # 中心仓库:笔画、颜色、平移缩放、撤销、剪贴板
|
|
239
|
+
│ └── tools.ts # 当前工具状态
|
|
240
|
+
├── utils/
|
|
241
|
+
│ ├── geometry.ts # 命中测试、星/三角形顶点、包围盒
|
|
242
|
+
│ ├── svg.ts # Stroke → SVG 元素转换
|
|
243
|
+
│ └── i18n.ts # zh-CN / en-US 字典 + t()
|
|
244
|
+
└── types/index.ts # TypeScript 类型与默认常量
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
### 设计原则
|
|
248
|
+
|
|
249
|
+
- **自包含**:单个 Vue 3 组件,除 Vue 外零运行时依赖,无需全局注册,无需 CSS 框架。
|
|
250
|
+
- **状态可序列化**:整份绘图是单个普通对象(`PainterData`),便于保存、加载与同步。
|
|
251
|
+
- **基于 SVG 渲染**:每条笔画都是真实 SVG 节点(`<path>`、`<line>`、`<ellipse>`、`<rect>`、`<polygon>`、`foreignObject`),输出与分辨率无关、易主题化、易导出。
|
|
252
|
+
- **可预测的双向绑定**:父组件拥有数据,组件回显并在变更时通过 `v-model` 上报;store 内的 `syncing` 标志与组件内的 `suppressing` 标志共同避免回环。
|
|
253
|
+
- **轻量交互**:手动处理指针与触摸事件并显式计算坐标变换(不使用 `getScreenCTM`),规避 CSS transform 下 iOS Safari 的不一致行为。
|
|
254
|
+
- **纯函数工具**:命中测试、SVG 转换、i18n 都放在 `utils/` 中作为无框架依赖的纯函数,易于复用与单测。
|
|
255
|
+
- **组合式 API**:业务逻辑抽离到 `composables/`(`useDrawing`、`useI18n`)与轻量 store 层(`canvas.ts`、`tools.ts`),保持单一职责。
|
|
256
|
+
|
|
257
|
+
> **单实例说明**:`canvas.ts` 与 `tools.ts` 是模块级响应式单例,同一页面多个 `<Painter>` 会共享状态。若要支持多实例,应将 store 改为基于 `provide`/`inject` 的组件级 store。
|
|
258
|
+
|
|
259
|
+
---
|
|
260
|
+
|
|
261
|
+
## 快捷键
|
|
262
|
+
|
|
263
|
+
| 按键 | 动作 |
|
|
264
|
+
|------|------|
|
|
265
|
+
| `Delete` / `Backspace` | 删除选中图形 |
|
|
266
|
+
| `Escape` | 取消选择 / 取消文本编辑 |
|
|
267
|
+
| `Ctrl/⌘ + Z` | 撤销 |
|
|
268
|
+
| `Ctrl/⌘ + Y` 或 `Ctrl/⌘ + Shift + Z` | 重做 |
|
|
269
|
+
| `Ctrl/⌘ + X` | 剪切 |
|
|
270
|
+
| `Ctrl/⌘ + C` | 复制 |
|
|
271
|
+
| `Ctrl/⌘ + V` | 粘贴(偏移 +20px) |
|
|
272
|
+
|
|
273
|
+
绘制或缩放过程中,`Ctrl` 锁定 1:1,`Shift` 对闭合图形为中心扩展、对直线为 45° 吸附。
|
|
274
|
+
|
|
275
|
+
---
|
|
276
|
+
|
|
277
|
+
## 主题与国际化
|
|
278
|
+
|
|
279
|
+
### 内置主题
|
|
280
|
+
|
|
281
|
+
通过 `theme` 属性提供两套主题,颜色由 `style.css` 中的 CSS 变量(`--wb-*`)驱动,自动适配明/暗。切换主题时若当前前景色为默认值,会自动在 `#000000` 与 `#ffffff` 之间翻转。
|
|
282
|
+
|
|
283
|
+
可在任意外层元素上覆盖 CSS 变量实现深度定制:
|
|
284
|
+
|
|
285
|
+
```html
|
|
286
|
+
<div style="--wb-bg: #1a1a2e; --wb-text: #e0e0e0;">
|
|
287
|
+
<Painter v-model="data" theme="dark" />
|
|
288
|
+
</div>
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
### 国际化
|
|
292
|
+
|
|
293
|
+
内置 `zh-CN` 与 `en-US` 文案。通过 `locale` 切换,未知取值回退到 `zh-CN`。字典位于 `src/components/painter/utils/i18n.ts`,可方便扩展更多语言。
|
|
294
|
+
|
|
295
|
+
---
|
|
296
|
+
|
|
297
|
+
## 开发
|
|
298
|
+
|
|
299
|
+
```bash
|
|
300
|
+
npm run dev # 启动 Vite 开发服务器(演示页)
|
|
301
|
+
npm run build # 类型检查(vue-tsc)并构建库产物
|
|
302
|
+
npm run build:demo # 构建独立演示页到 ./dist-demo
|
|
303
|
+
npm run preview # 预览库的生产构建
|
|
304
|
+
npm run lint:check # 运行 ESLint
|
|
305
|
+
npm run typecheck # 仅类型检查
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
## 构建
|
|
309
|
+
|
|
310
|
+
库构建通过 `vite.config.ts`(lib 模式)。`npm run build` 产出:
|
|
311
|
+
|
|
312
|
+
| 文件 | 说明 |
|
|
313
|
+
|------|------|
|
|
314
|
+
| `dist/xiaodao-painter.es.js` | ES 模块(供打包工具使用) |
|
|
315
|
+
| `dist/xiaodao-painter.umd.js` | UMD 包(供直接 `<script>` 引入) |
|
|
316
|
+
| `dist/xiaodao-painter.css` | 提取的样式表 |
|
|
317
|
+
| `dist/types/` | TypeScript 类型声明 |
|
|
318
|
+
|
|
319
|
+
`npm run build:demo` 通过 `vite.demo.config.ts` 构建独立演示站点到 `dist-demo/`(按 `dist` 同等规则 git 忽略)。
|
|
320
|
+
|
|
321
|
+
---
|
|
322
|
+
|
|
323
|
+
## 路线图
|
|
324
|
+
|
|
325
|
+
### 近期
|
|
326
|
+
|
|
327
|
+
- [x] 铅笔、图形、文本绘制与选择变换
|
|
328
|
+
- [x] 平移缩放、撤销重做、内部剪贴板
|
|
329
|
+
- [x] 颜色系统、明/暗主题、国际化
|
|
330
|
+
- [ ] 导出 PNG / SVG / JSON
|
|
331
|
+
- [ ] 图形级对齐吸附与参考线
|
|
332
|
+
|
|
333
|
+
### 中期
|
|
334
|
+
|
|
335
|
+
- [ ] 多页面 / 多图层支持
|
|
336
|
+
- [ ] 图片嵌入
|
|
337
|
+
- [ ] 云端保存 / 加载集成
|
|
338
|
+
|
|
339
|
+
### 长期
|
|
340
|
+
|
|
341
|
+
- [ ] 协同编辑
|
|
342
|
+
- [ ] 自定义图形渲染插件系统
|
|
343
|
+
|
|
344
|
+
---
|
|
345
|
+
|
|
346
|
+
## 技术栈
|
|
347
|
+
|
|
348
|
+
| 层 | 技术 | 版本 |
|
|
349
|
+
|----|------|------|
|
|
350
|
+
| 框架 | Vue 3(Composition API + `<script setup>`) | ^3.5 |
|
|
351
|
+
| 构建 | Vite | ^8.0 |
|
|
352
|
+
| 语言 | TypeScript(strict) | ~6.0 |
|
|
353
|
+
| 渲染 | SVG 2D(DOM) | - |
|
|
354
|
+
| 类型检查 | vue-tsc | ^3.3 |
|
|
355
|
+
| 包管理器 | npm / pnpm | - |
|
|
356
|
+
| CSS | Scoped CSS + CSS 自定义属性 | - |
|
|
357
|
+
|
|
358
|
+
---
|
|
359
|
+
|
|
360
|
+
## 许可证
|
|
361
|
+
|
|
362
|
+
本项目基于 MIT 许可证发布,详见 [LICENSE](LICENSE) 文件。
|
package/README.md
CHANGED
|
@@ -1,49 +1,107 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Xiaodao Painter
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[中文](./README.ZH.md) | **English** | [Demo](https://painter.xdz.me)
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
[](https://www.npmjs.com/package/xiaodao-painter)
|
|
6
|
+
[](LICENSE)
|
|
7
|
+
[](https://www.npmjs.com/package/xiaodao-painter)
|
|
8
|
+
[](https://vuejs.org/)
|
|
9
|
+
[](https://www.typescriptlang.org/)
|
|
10
|
+
[](https://vitejs.dev/)
|
|
11
|
+
|
|
12
|
+
A self-contained, embeddable **SVG drawing component** for Vue 3: a full-featured drawing surface (freehand pencil, shape primitives, text, selection & transform, pan & zoom, undo/redo, an internal clipboard, a color system, light/dark themes, and built-in i18n) packed into a single component, driven by one `v-model`.
|
|
6
13
|
|
|
7
14
|

|
|
8
15
|
|
|
16
|
+
---
|
|
17
|
+
|
|
9
18
|
## Features
|
|
10
19
|
|
|
11
|
-
|
|
12
|
-
- **Selection & manipulation** — click to select, box-select, drag-move, 8-handle resize (Shift = symmetric), layer reorder (bring to front / send to back)
|
|
13
|
-
- **Pan & zoom** — grab-drag panning, discrete zoom steps (10% – 1000%) anchored at cursor position
|
|
14
|
-
- **Undo / redo** — full stroke-level undo/redo stacks
|
|
15
|
-
- **Internal clipboard** — cut, copy, paste (within the whiteboard, offset +20 px)
|
|
16
|
-
- **Color system** — stroke (foreground) and fill (background) color slots, per-stroke color application to selections, canvas background color with transparent grid support
|
|
17
|
-
- **Themes** — light and dark, with auto-switching foreground color on theme change
|
|
18
|
-
- **Internationalization** — Chinese (zh-CN) and English (en-US) built in
|
|
19
|
-
- **Keyboard shortcuts** — Delete, Escape, Ctrl+Z / Ctrl+Y / Ctrl+X / Ctrl+C / Ctrl+V
|
|
20
|
-
- **Modifier keys** — Ctrl = constrain 1:1 ratio, Shift = center-out expansion / 45° line snap
|
|
21
|
-
- **Two-way binding** — parent reads/writes full drawing data via `v-model`
|
|
20
|
+
### Drawing & Editing
|
|
22
21
|
|
|
23
|
-
|
|
22
|
+
- **9 tools**: Select, Pan, Pencil, Line, Rectangle, Circle, Triangle, Star, Text (plus a dedicated Zoom tool).
|
|
23
|
+
- **Selection & transform**: Click to select, box-select on empty space, drag to move, 8-handle resize (Shift = center-out / symmetric), layer reorder (bring to front / send to back).
|
|
24
|
+
- **Pan & zoom**: Grab-drag panning, discrete zoom steps (10%–1000%) anchored at the cursor; pinch zoom on touch devices.
|
|
25
|
+
- **Undo / redo**: Full stroke-level undo/redo stacks.
|
|
26
|
+
- **Internal clipboard**: Cut, copy, paste within the canvas (paste offset +20 px).
|
|
27
|
+
- **Color system**: Stroke (foreground), fill (background), and text-color slots; per-selection color application and canvas background (solid or transparent with grid).
|
|
28
|
+
- **Text tool**: Click to place an editable text box; double-click existing text to edit.
|
|
29
|
+
|
|
30
|
+
### Interaction
|
|
31
|
+
|
|
32
|
+
- **Pointer & touch**: A single interaction layer (`useDrawing`) unifies mouse and touch paths, including pinch-to-zoom and drag-to-draw/select.
|
|
33
|
+
- **Resize handles**: 8 handles for shapes, 2 endpoints for lines; live preview while dragging.
|
|
34
|
+
- **Modifier keys**: `Ctrl` = constrain 1:1 ratio (shapes), `Shift` = center-out expansion / 45° line snap.
|
|
35
|
+
- **Keyboard shortcuts**: Delete, Escape, Ctrl/⌘+Z / Ctrl/⌘+Y / Ctrl/⌘+X / Ctrl/⌘+C / Ctrl/⌘+V.
|
|
36
|
+
- **SVG rendering**: Every stroke is a real SVG element (`<path>`, `<line>`, `<ellipse>`, `<rect>`, `<polygon>`, `foreignObject`), so the output is resolution-independent and easy to export.
|
|
37
|
+
|
|
38
|
+
### Visual & Theming
|
|
24
39
|
|
|
25
|
-
|
|
40
|
+
- **Light / Dark themes**: Full color palette driven by CSS custom properties (`--wb-*`), switching swaps a single class.
|
|
41
|
+
- **Internationalization**: Chinese (`zh-CN`) and English (`en-US`) built in; toolbar labels and zoom indicator route through an `t()` dictionary.
|
|
42
|
+
- **Two-way binding**: Read and write the entire drawing via `v-model`.
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## Installation
|
|
26
47
|
|
|
27
48
|
```bash
|
|
49
|
+
# npm
|
|
28
50
|
npm install xiaodao-painter
|
|
51
|
+
# pnpm
|
|
52
|
+
pnpm add xiaodao-painter
|
|
53
|
+
# yarn
|
|
54
|
+
yarn add xiaodao-painter
|
|
29
55
|
```
|
|
30
56
|
|
|
31
|
-
|
|
57
|
+
### Peer Dependencies
|
|
58
|
+
|
|
59
|
+
- `vue` `^3.0.0`
|
|
60
|
+
|
|
61
|
+
Import the stylesheet once:
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
import 'xiaodao-painter/style.css'
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
## Quick Start
|
|
32
70
|
|
|
33
71
|
```bash
|
|
34
|
-
|
|
35
|
-
|
|
72
|
+
# Clone the repository
|
|
73
|
+
git clone https://github.com/xiaodaozhi/xiaodao-painter.git
|
|
74
|
+
cd xiaodao-painter
|
|
75
|
+
|
|
76
|
+
# Install dependencies
|
|
77
|
+
npm install
|
|
78
|
+
|
|
79
|
+
# Start the dev server (demo page)
|
|
80
|
+
npm run dev
|
|
36
81
|
```
|
|
37
82
|
|
|
38
|
-
|
|
83
|
+
Navigate to `http://localhost:5173` to see the demo application.
|
|
84
|
+
|
|
85
|
+
To build a standalone demo page, run `npm run build:demo` and open `dist-demo/index.html`.
|
|
86
|
+
|
|
87
|
+
---
|
|
39
88
|
|
|
40
|
-
|
|
89
|
+
## Basic Usage
|
|
41
90
|
|
|
42
91
|
```vue
|
|
92
|
+
<template>
|
|
93
|
+
<Painter
|
|
94
|
+
v-model="data"
|
|
95
|
+
theme="light"
|
|
96
|
+
locale="zh-CN"
|
|
97
|
+
/>
|
|
98
|
+
</template>
|
|
99
|
+
|
|
43
100
|
<script setup lang="ts">
|
|
44
101
|
import { ref, watch } from 'vue'
|
|
45
102
|
import { Painter } from 'xiaodao-painter'
|
|
46
103
|
import type { PainterData } from 'xiaodao-painter'
|
|
104
|
+
import 'xiaodao-painter/style.css'
|
|
47
105
|
|
|
48
106
|
const data = ref<PainterData>({
|
|
49
107
|
strokes: [],
|
|
@@ -52,32 +110,24 @@ const data = ref<PainterData>({
|
|
|
52
110
|
})
|
|
53
111
|
|
|
54
112
|
watch(data, (val) => {
|
|
55
|
-
console.log('Drawing
|
|
113
|
+
console.log('Drawing changed:', val)
|
|
56
114
|
}, { deep: true })
|
|
57
115
|
</script>
|
|
58
|
-
|
|
59
|
-
<template>
|
|
60
|
-
<Painter
|
|
61
|
-
v-model="data"
|
|
62
|
-
theme="light"
|
|
63
|
-
locale="zh-CN"
|
|
64
|
-
/>
|
|
65
|
-
</template>
|
|
66
116
|
```
|
|
67
117
|
|
|
68
|
-
With explicit dimensions and dark theme
|
|
118
|
+
### With explicit dimensions and dark theme
|
|
69
119
|
|
|
70
120
|
```html
|
|
71
121
|
<Painter
|
|
72
122
|
v-model="data"
|
|
73
123
|
theme="dark"
|
|
74
124
|
locale="en-US"
|
|
75
|
-
width="1200"
|
|
76
|
-
height="800"
|
|
125
|
+
:width="1200"
|
|
126
|
+
:height="800"
|
|
77
127
|
/>
|
|
78
128
|
```
|
|
79
129
|
|
|
80
|
-
With a custom canvas background
|
|
130
|
+
### With a custom canvas background
|
|
81
131
|
|
|
82
132
|
```html
|
|
83
133
|
<Painter
|
|
@@ -91,44 +141,51 @@ With a custom canvas background color:
|
|
|
91
141
|
/>
|
|
92
142
|
```
|
|
93
143
|
|
|
144
|
+
---
|
|
145
|
+
|
|
94
146
|
## Props
|
|
95
147
|
|
|
96
148
|
| Prop | Type | Default | Description |
|
|
97
|
-
|
|
98
|
-
| `modelValue` | `PainterData` | `{ strokes: [], canvasWidth: 800, canvasHeight: 600 }` | Two-way bound drawing data
|
|
99
|
-
| `theme` | `'light' \| 'dark'` | `'light'` | UI theme |
|
|
100
|
-
| `locale` | `
|
|
101
|
-
| `width` | `string \| number` | `'100%'` | Component width (CSS value or px number) |
|
|
102
|
-
| `height` | `string \| number` | `'100%'` | Component height (CSS value or px number) |
|
|
149
|
+
|------|------|---------|-------------|
|
|
150
|
+
| `modelValue` (v-model) | `PainterData` | `{ strokes: [], canvasWidth: 800, canvasHeight: 600 }` | Two-way bound drawing data. Supports `canvasBackgroundColor`. |
|
|
151
|
+
| `theme` | `'light' \| 'dark'` | `'light'` | UI theme. |
|
|
152
|
+
| `locale` | `'zh-CN' \| 'en-US'` | `'zh-CN'` | UI language. Any other value falls back to `zh-CN`. |
|
|
153
|
+
| `width` | `string \| number` | `'100%'` | Component width (CSS value or px number). |
|
|
154
|
+
| `height` | `string \| number` | `'100%'` | Component height (CSS value or px number). |
|
|
155
|
+
|
|
156
|
+
---
|
|
103
157
|
|
|
104
158
|
## Data Model
|
|
105
159
|
|
|
160
|
+
The whole drawing is described by a single serializable `PainterData` object, which makes persistence, undo/redo, and collaboration straightforward.
|
|
161
|
+
|
|
106
162
|
```ts
|
|
107
163
|
interface PainterData {
|
|
108
164
|
strokes: Stroke[]
|
|
109
165
|
canvasWidth: number
|
|
110
166
|
canvasHeight: number
|
|
111
|
-
canvasBackgroundColor?: string
|
|
167
|
+
canvasBackgroundColor?: string // any CSS color or 'transparent'
|
|
112
168
|
}
|
|
113
169
|
|
|
114
|
-
type ToolType =
|
|
170
|
+
type ToolType =
|
|
171
|
+
| 'select' | 'pan' | 'zoom'
|
|
172
|
+
| 'pencil' | 'line' | 'circle' | 'rect' | 'triangle' | 'star'
|
|
173
|
+
| 'text'
|
|
115
174
|
|
|
116
|
-
interface Point {
|
|
117
|
-
x: number
|
|
118
|
-
y: number
|
|
119
|
-
}
|
|
175
|
+
interface Point { x: number; y: number }
|
|
120
176
|
|
|
121
177
|
interface Stroke {
|
|
122
|
-
id: string
|
|
178
|
+
id: string // crypto.randomUUID()
|
|
123
179
|
type: ToolType
|
|
124
|
-
x: number
|
|
180
|
+
x: number // bounding-box top-left (normalized on commit)
|
|
125
181
|
y: number
|
|
126
|
-
width: number
|
|
182
|
+
width: number // bounding-box width (may be negative while drawing)
|
|
127
183
|
height: number
|
|
128
|
-
points: Point[]
|
|
184
|
+
points: Point[] // pencil path points; empty for non-pencil
|
|
129
185
|
strokeColor: string
|
|
130
|
-
fillColor: string
|
|
186
|
+
fillColor: string // 'transparent' / 'none' for pencil & line
|
|
131
187
|
strokeWidth: number
|
|
188
|
+
// text-only fields
|
|
132
189
|
text?: string
|
|
133
190
|
fontSize?: number
|
|
134
191
|
textAlign?: 'left' | 'center' | 'right'
|
|
@@ -137,52 +194,169 @@ interface Stroke {
|
|
|
137
194
|
}
|
|
138
195
|
```
|
|
139
196
|
|
|
197
|
+
Coordinates are stored in **canvas (SVG user) coordinate space**, independent of zoom and pan. The component normalizes negative-width/height shapes on commit so the stored bounding box is always positive.
|
|
198
|
+
|
|
199
|
+
### Type Exports
|
|
200
|
+
|
|
201
|
+
```ts
|
|
202
|
+
import type {
|
|
203
|
+
PainterData, // main data interface
|
|
204
|
+
Stroke, // a single shape / text / pencil entry
|
|
205
|
+
Point, // { x: number; y: number }
|
|
206
|
+
ToolType, // union of all tool identifiers
|
|
207
|
+
CanvasConfig, // canvas size / background config
|
|
208
|
+
} from 'xiaodao-painter'
|
|
209
|
+
|
|
210
|
+
import {
|
|
211
|
+
DEFAULT_CANVAS_WIDTH,
|
|
212
|
+
DEFAULT_CANVAS_HEIGHT,
|
|
213
|
+
STROKE_WIDTH,
|
|
214
|
+
DEFAULT_FONT_SIZE,
|
|
215
|
+
HIT_THRESHOLD,
|
|
216
|
+
} from 'xiaodao-painter'
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
---
|
|
220
|
+
|
|
221
|
+
## Architecture
|
|
222
|
+
|
|
223
|
+
```src/
|
|
224
|
+
├── index.ts # Public entry: exports Painter + types/constants
|
|
225
|
+
├── App.vue # Demo root (sets page title, mounts the component)
|
|
226
|
+
├── style.css # Global theme CSS variables (light/dark)
|
|
227
|
+
└── components/painter/
|
|
228
|
+
├── Painter.vue # Public component: props, v-model sync, theme/locale provide
|
|
229
|
+
├── Whiteboard.vue # SVG canvas, pointer/touch handling, zoom UI, text editing
|
|
230
|
+
├── Toolbar.vue # Tool buttons, color indicators, canvas-size popover
|
|
231
|
+
├── ColorPalette.vue # Color picker for foreground/fill/text slots
|
|
232
|
+
├── SelectionOverlay.vue # Bounding box + 8 resize handles (line gets 2 handles)
|
|
233
|
+
├── StrokeRenderer.vue # Renders one Stroke as an SVG element
|
|
234
|
+
├── composables/
|
|
235
|
+
│ ├── useDrawing.ts # Interaction engine (draw/select/move/resize/pan/zoom/text)
|
|
236
|
+
│ └── useI18n.ts # provide/inject for locale, theme, root element
|
|
237
|
+
├── stores/
|
|
238
|
+
│ ├── canvas.ts # Central store: strokes, colors, pan/zoom, undo, clipboard
|
|
239
|
+
│ └── tools.ts # Active tool state
|
|
240
|
+
├── utils/
|
|
241
|
+
│ ├── geometry.ts # Hit testing, star/triangle vertices, bounding box
|
|
242
|
+
│ ├── svg.ts # Stroke → SVG element conversion
|
|
243
|
+
│ └── i18n.ts # zh-CN / en-US dictionaries + t()
|
|
244
|
+
└── types/index.ts # TypeScript types and default constants
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
### Design Principles
|
|
248
|
+
|
|
249
|
+
- **Self-contained**: A single Vue 3 component with zero runtime dependencies beyond Vue; no global registration, no CSS framework.
|
|
250
|
+
- **Serializable state**: The whole drawing is one plain `PainterData` object, so it can be saved, loaded, and synced trivially.
|
|
251
|
+
- **SVG-based rendering**: Every stroke is a real SVG node (`<path>`, `<line>`, `<ellipse>`, `<rect>`, `<polygon>`, `foreignObject`). Output is resolution-independent, easily themeable, and export-friendly.
|
|
252
|
+
- **Predictable two-way binding**: The parent owns the data; the component reflects it and emits changes back through `v-model`. A `syncing` flag in the store plus a `suppressing` flag in the component prevents feedback loops.
|
|
253
|
+
- **Framework-light interaction**: Pointer + touch are handled manually with an explicit coordinate transform (not `getScreenCTM`), avoiding unreliable behavior under CSS transforms on iOS Safari.
|
|
254
|
+
- **Pure helpers**: Hit-testing, SVG conversion, and i18n live in `utils/` as framework-free functions, easy to reuse or unit-test.
|
|
255
|
+
- **Composition API**: Business logic is extracted into `composables/` (`useDrawing`, `useI18n`) and a small store layer (`canvas.ts`, `tools.ts`), keeping single-responsibility modules.
|
|
256
|
+
|
|
257
|
+
> **Single-instance note:** `canvas.ts` and `tools.ts` are module-level reactive singletons. Multiple `<Painter>` components on one page currently share state. To support multiple instances, promote the stores to a `provide`/`inject` scoped store per component.
|
|
258
|
+
|
|
259
|
+
---
|
|
260
|
+
|
|
140
261
|
## Keyboard Shortcuts
|
|
141
262
|
|
|
142
263
|
| Key | Action |
|
|
143
|
-
|
|
264
|
+
|-----|--------|
|
|
144
265
|
| `Delete` / `Backspace` | Delete selected strokes |
|
|
145
|
-
| `Escape` | Clear selection |
|
|
146
|
-
| `Ctrl + Z` | Undo |
|
|
147
|
-
| `Ctrl + Y`
|
|
148
|
-
| `Ctrl + X` | Cut |
|
|
149
|
-
| `Ctrl + C` | Copy |
|
|
150
|
-
| `Ctrl + V` | Paste (offset +20 px) |
|
|
266
|
+
| `Escape` | Clear selection / cancel text edit |
|
|
267
|
+
| `Ctrl/⌘ + Z` | Undo |
|
|
268
|
+
| `Ctrl/⌘ + Y` or `Ctrl/⌘ + Shift + Z` | Redo |
|
|
269
|
+
| `Ctrl/⌘ + X` | Cut |
|
|
270
|
+
| `Ctrl/⌘ + C` | Copy |
|
|
271
|
+
| `Ctrl/⌘ + V` | Paste (offset +20 px) |
|
|
151
272
|
|
|
152
|
-
|
|
273
|
+
While drawing or resizing, `Ctrl` constrains to 1:1, `Shift` gives center-out expansion (for closed shapes) or 45° line snapping (for lines).
|
|
153
274
|
|
|
275
|
+
---
|
|
276
|
+
|
|
277
|
+
## Theming & Internationalization
|
|
278
|
+
|
|
279
|
+
### Built-in Themes
|
|
280
|
+
|
|
281
|
+
The component ships two themes via the `theme` prop. Colors are driven by CSS custom properties defined in `style.css` (`--wb-*`), so the component adapts to light/dark automatically. Switching theme also flips the default foreground color (`#000000` ⇄ `#ffffff`) when the current color is the theme default.
|
|
282
|
+
|
|
283
|
+
Override CSS variables on any wrapper element for full customization:
|
|
284
|
+
|
|
285
|
+
```html
|
|
286
|
+
<div style="--wb-bg: #1a1a2e; --wb-text: #e0e0e0;">
|
|
287
|
+
<Painter v-model="data" theme="dark" />
|
|
288
|
+
</div>
|
|
154
289
|
```
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
SelectionOverlay.vue # Selection bounding box + resize handles
|
|
162
|
-
StrokeRenderer.vue # Renders a single stroke as SVG
|
|
163
|
-
composables/
|
|
164
|
-
useDrawing.ts # Mouse interaction engine (draw, select, move, resize)
|
|
165
|
-
useI18n.ts # Locale and theme provide/inject
|
|
166
|
-
stores/
|
|
167
|
-
canvas.ts # Main store (strokes, colors, pan/zoom, undo, clipboard)
|
|
168
|
-
tools.ts # Active tool state
|
|
169
|
-
utils/
|
|
170
|
-
geometry.ts # Hit testing, star/triangle vertices, bounding box
|
|
171
|
-
svg.ts # Stroke-to-SVG element conversion
|
|
172
|
-
i18n.ts # zh-CN / en-US dictionaries
|
|
173
|
-
types/
|
|
174
|
-
index.ts # TypeScript types and default constants
|
|
175
|
-
style.css # CSS custom properties (themes)
|
|
176
|
-
```
|
|
290
|
+
|
|
291
|
+
### Internationalization
|
|
292
|
+
|
|
293
|
+
UI text is provided for `zh-CN` and `en-US`. Pass `locale` to switch; unknown values fall back to `zh-CN`. The dictionary lives in `src/components/painter/utils/i18n.ts` and can be extended with additional locales.
|
|
294
|
+
|
|
295
|
+
---
|
|
177
296
|
|
|
178
297
|
## Development
|
|
179
298
|
|
|
180
299
|
```bash
|
|
181
|
-
npm run dev
|
|
182
|
-
npm run build
|
|
183
|
-
npm run
|
|
300
|
+
npm run dev # Start the Vite dev server (demo page)
|
|
301
|
+
npm run build # Type-check (vue-tsc) and build the library bundle
|
|
302
|
+
npm run build:demo # Build the standalone demo page into ./dist-demo
|
|
303
|
+
npm run preview # Preview the library production build
|
|
304
|
+
npm run lint:check # Run ESLint
|
|
305
|
+
npm run typecheck # Type-check only
|
|
184
306
|
```
|
|
185
307
|
|
|
308
|
+
## Building
|
|
309
|
+
|
|
310
|
+
The library build uses `vite.config.ts` (lib mode). `npm run build` produces:
|
|
311
|
+
|
|
312
|
+
| File | Description |
|
|
313
|
+
|------|-------------|
|
|
314
|
+
| `dist/xiaodao-painter.es.js` | ES module (for bundlers) |
|
|
315
|
+
| `dist/xiaodao-painter.umd.js` | UMD bundle (for direct `<script>` usage) |
|
|
316
|
+
| `dist/xiaodao-painter.css` | Extracted stylesheet |
|
|
317
|
+
| `dist/types/` | TypeScript declaration files |
|
|
318
|
+
|
|
319
|
+
`npm run build:demo` builds a standalone demo site via `vite.demo.config.ts` into `dist-demo/` (git-ignored, same as `dist`).
|
|
320
|
+
|
|
321
|
+
---
|
|
322
|
+
|
|
323
|
+
## Roadmap
|
|
324
|
+
|
|
325
|
+
### Near-term
|
|
326
|
+
|
|
327
|
+
- [x] Pencil, shape, text drawing with selection & transform
|
|
328
|
+
- [x] Pan & zoom, undo/redo, internal clipboard
|
|
329
|
+
- [x] Color system, light/dark themes, i18n
|
|
330
|
+
- [ ] Export to PNG / SVG / JSON
|
|
331
|
+
- [ ] Shape-level snapping & alignment guides
|
|
332
|
+
|
|
333
|
+
### Mid-term
|
|
334
|
+
|
|
335
|
+
- [ ] Multi-page / multi-layer support
|
|
336
|
+
- [ ] Image embedding
|
|
337
|
+
- [ ] Cloud save / load integration
|
|
338
|
+
|
|
339
|
+
### Long-term
|
|
340
|
+
|
|
341
|
+
- [ ] Collaborative editing
|
|
342
|
+
- [ ] Plugin system for custom shape renderers
|
|
343
|
+
|
|
344
|
+
---
|
|
345
|
+
|
|
346
|
+
## Tech Stack
|
|
347
|
+
|
|
348
|
+
| Layer | Technology | Version |
|
|
349
|
+
|-------|-----------|---------|
|
|
350
|
+
| Framework | Vue 3 (Composition API + `<script setup>`) | ^3.5 |
|
|
351
|
+
| Build | Vite | ^8.0 |
|
|
352
|
+
| Language | TypeScript (strict) | ~6.0 |
|
|
353
|
+
| Rendering | SVG 2D (DOM) | - |
|
|
354
|
+
| Type Checker | vue-tsc | ^3.3 |
|
|
355
|
+
| Package Manager | npm / pnpm | - |
|
|
356
|
+
| CSS | Scoped CSS + CSS Custom Properties | - |
|
|
357
|
+
|
|
358
|
+
---
|
|
359
|
+
|
|
186
360
|
## License
|
|
187
361
|
|
|
188
|
-
MIT
|
|
362
|
+
This project is licensed under the MIT License: see the [LICENSE](LICENSE) file for details.
|
package/package.json
CHANGED
|
@@ -1,15 +1,29 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "xiaodao-painter",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.9",
|
|
4
4
|
"description": "A Vue 3 component for drawing on the canvas",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"homepage": "https://xiaodaozhi.com/xiaodao-painter.html",
|
|
8
8
|
"keywords": [
|
|
9
9
|
"vue",
|
|
10
|
+
"vue3",
|
|
11
|
+
"vue-component",
|
|
12
|
+
"whiteboard",
|
|
10
13
|
"canvas",
|
|
11
|
-
"
|
|
12
|
-
"
|
|
14
|
+
"draw",
|
|
15
|
+
"drawing-board",
|
|
16
|
+
"paint",
|
|
17
|
+
"sketch",
|
|
18
|
+
"annotation",
|
|
19
|
+
"手写板",
|
|
20
|
+
"白板",
|
|
21
|
+
"画布",
|
|
22
|
+
"画板",
|
|
23
|
+
"绘图",
|
|
24
|
+
"手绘",
|
|
25
|
+
"批注",
|
|
26
|
+
"电子白板"
|
|
13
27
|
],
|
|
14
28
|
"files": [
|
|
15
29
|
"dist"
|