canvas-drawing-editor 2.0.1 → 2.0.3
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.md
CHANGED
|
@@ -128,16 +128,93 @@ function App() {
|
|
|
128
128
|
| `show-import` | boolean | true | Show JSON load |
|
|
129
129
|
| `show-color` | boolean | true | Show color picker |
|
|
130
130
|
| `show-minimap` | boolean | true | Show navigation minimap |
|
|
131
|
+
| `initial-data` | string | - | Initial JSON data to render (see format below) |
|
|
132
|
+
|
|
133
|
+
### 📊 Initial Data
|
|
134
|
+
|
|
135
|
+
You can pass JSON data to initialize the canvas content:
|
|
136
|
+
|
|
137
|
+
```html
|
|
138
|
+
<canvas-drawing-editor
|
|
139
|
+
initial-data='{"objects":[{"id":"abc123","type":"RECTANGLE","x":100,"y":100,"width":200,"height":150,"color":"#3b82f6","lineWidth":2}]}'
|
|
140
|
+
></canvas-drawing-editor>
|
|
141
|
+
```
|
|
131
142
|
|
|
132
143
|
### 📡 Events
|
|
133
144
|
|
|
145
|
+
#### `editor-change` Event
|
|
146
|
+
|
|
147
|
+
Fires when canvas content changes. The `e.detail.objects` array contains all drawing objects.
|
|
148
|
+
|
|
134
149
|
```javascript
|
|
135
|
-
// Listen for canvas changes
|
|
136
150
|
document.addEventListener('editor-change', (e) => {
|
|
137
151
|
console.log('Objects:', e.detail.objects);
|
|
152
|
+
// Save to server or localStorage
|
|
153
|
+
localStorage.setItem('canvas-data', JSON.stringify({ objects: e.detail.objects }));
|
|
154
|
+
});
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
#### Object Types & Properties
|
|
158
|
+
|
|
159
|
+
Each object in `e.detail.objects` has the following base properties:
|
|
160
|
+
|
|
161
|
+
| Property | Type | Description |
|
|
162
|
+
|----------|------|-------------|
|
|
163
|
+
| `id` | string | Unique identifier |
|
|
164
|
+
| `type` | string | Object type: `RECTANGLE`, `CIRCLE`, `PATH`, `TEXT`, `IMAGE` |
|
|
165
|
+
| `x` | number | X coordinate |
|
|
166
|
+
| `y` | number | Y coordinate |
|
|
167
|
+
| `color` | string | Stroke/fill color (hex format, e.g., `#3b82f6`) |
|
|
168
|
+
| `lineWidth` | number | Line width in pixels |
|
|
169
|
+
|
|
170
|
+
**Rectangle** (`type: "RECTANGLE"`):
|
|
171
|
+
| Property | Type | Description |
|
|
172
|
+
|----------|------|-------------|
|
|
173
|
+
| `width` | number | Rectangle width |
|
|
174
|
+
| `height` | number | Rectangle height |
|
|
175
|
+
|
|
176
|
+
**Circle** (`type: "CIRCLE"`):
|
|
177
|
+
| Property | Type | Description |
|
|
178
|
+
|----------|------|-------------|
|
|
179
|
+
| `radius` | number | Circle radius |
|
|
180
|
+
|
|
181
|
+
**Path/Pencil** (`type: "PATH"`):
|
|
182
|
+
| Property | Type | Description |
|
|
183
|
+
|----------|------|-------------|
|
|
184
|
+
| `points` | Array<{x, y}> | Array of point coordinates |
|
|
185
|
+
|
|
186
|
+
**Text** (`type: "TEXT"`):
|
|
187
|
+
| Property | Type | Description |
|
|
188
|
+
|----------|------|-------------|
|
|
189
|
+
| `text` | string | Text content |
|
|
190
|
+
| `fontSize` | number | Font size in pixels |
|
|
191
|
+
|
|
192
|
+
**Image** (`type: "IMAGE"`):
|
|
193
|
+
| Property | Type | Description |
|
|
194
|
+
|----------|------|-------------|
|
|
195
|
+
| `width` | number | Image width |
|
|
196
|
+
| `height` | number | Image height |
|
|
197
|
+
| `dataUrl` | string | Base64 encoded image data |
|
|
198
|
+
|
|
199
|
+
#### Example: Saving and Loading
|
|
200
|
+
|
|
201
|
+
```javascript
|
|
202
|
+
// Save canvas content
|
|
203
|
+
document.addEventListener('editor-change', (e) => {
|
|
204
|
+
const data = JSON.stringify({ objects: e.detail.objects });
|
|
205
|
+
localStorage.setItem('my-canvas', data);
|
|
138
206
|
});
|
|
139
207
|
|
|
140
|
-
//
|
|
208
|
+
// Load canvas content
|
|
209
|
+
const savedData = localStorage.getItem('my-canvas');
|
|
210
|
+
if (savedData) {
|
|
211
|
+
document.querySelector('canvas-drawing-editor').setAttribute('initial-data', savedData);
|
|
212
|
+
}
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
#### `editor-close` Event
|
|
216
|
+
|
|
217
|
+
```javascript
|
|
141
218
|
document.addEventListener('editor-close', () => {
|
|
142
219
|
console.log('Editor closed');
|
|
143
220
|
});
|
|
@@ -282,16 +359,93 @@ function App() {
|
|
|
282
359
|
| `show-import` | boolean | true | 显示 JSON 加载 |
|
|
283
360
|
| `show-color` | boolean | true | 显示颜色选择器 |
|
|
284
361
|
| `show-minimap` | boolean | true | 显示导航小地图 |
|
|
362
|
+
| `initial-data` | string | - | 初始化 JSON 数据(格式见下方) |
|
|
363
|
+
|
|
364
|
+
### 📊 初始化数据
|
|
365
|
+
|
|
366
|
+
可以通过 `initial-data` 属性传入 JSON 数据来初始化画布内容:
|
|
367
|
+
|
|
368
|
+
```html
|
|
369
|
+
<canvas-drawing-editor
|
|
370
|
+
initial-data='{"objects":[{"id":"abc123","type":"RECTANGLE","x":100,"y":100,"width":200,"height":150,"color":"#3b82f6","lineWidth":2}]}'
|
|
371
|
+
></canvas-drawing-editor>
|
|
372
|
+
```
|
|
285
373
|
|
|
286
374
|
### 📡 事件监听
|
|
287
375
|
|
|
376
|
+
#### `editor-change` 事件
|
|
377
|
+
|
|
378
|
+
当画布内容变化时触发。`e.detail.objects` 数组包含所有绑图对象。
|
|
379
|
+
|
|
288
380
|
```javascript
|
|
289
|
-
// 监听画布内容变化
|
|
290
381
|
document.addEventListener('editor-change', (e) => {
|
|
291
382
|
console.log('对象列表:', e.detail.objects);
|
|
383
|
+
// 保存到服务器或 localStorage
|
|
384
|
+
localStorage.setItem('canvas-data', JSON.stringify({ objects: e.detail.objects }));
|
|
385
|
+
});
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
#### 对象类型和属性说明
|
|
389
|
+
|
|
390
|
+
`e.detail.objects` 中每个对象都有以下基础属性:
|
|
391
|
+
|
|
392
|
+
| 属性 | 类型 | 说明 |
|
|
393
|
+
|------|------|------|
|
|
394
|
+
| `id` | string | 唯一标识符 |
|
|
395
|
+
| `type` | string | 对象类型:`RECTANGLE`、`CIRCLE`、`PATH`、`TEXT`、`IMAGE` |
|
|
396
|
+
| `x` | number | X 坐标 |
|
|
397
|
+
| `y` | number | Y 坐标 |
|
|
398
|
+
| `color` | string | 描边/填充颜色(十六进制格式,如 `#3b82f6`) |
|
|
399
|
+
| `lineWidth` | number | 线条宽度(像素) |
|
|
400
|
+
|
|
401
|
+
**矩形** (`type: "RECTANGLE"`):
|
|
402
|
+
| 属性 | 类型 | 说明 |
|
|
403
|
+
|------|------|------|
|
|
404
|
+
| `width` | number | 矩形宽度 |
|
|
405
|
+
| `height` | number | 矩形高度 |
|
|
406
|
+
|
|
407
|
+
**圆形** (`type: "CIRCLE"`):
|
|
408
|
+
| 属性 | 类型 | 说明 |
|
|
409
|
+
|------|------|------|
|
|
410
|
+
| `radius` | number | 圆形半径 |
|
|
411
|
+
|
|
412
|
+
**画笔路径** (`type: "PATH"`):
|
|
413
|
+
| 属性 | 类型 | 说明 |
|
|
414
|
+
|------|------|------|
|
|
415
|
+
| `points` | Array<{x, y}> | 点坐标数组 |
|
|
416
|
+
|
|
417
|
+
**文本** (`type: "TEXT"`):
|
|
418
|
+
| 属性 | 类型 | 说明 |
|
|
419
|
+
|------|------|------|
|
|
420
|
+
| `text` | string | 文本内容 |
|
|
421
|
+
| `fontSize` | number | 字体大小(像素) |
|
|
422
|
+
|
|
423
|
+
**图片** (`type: "IMAGE"`):
|
|
424
|
+
| 属性 | 类型 | 说明 |
|
|
425
|
+
|------|------|------|
|
|
426
|
+
| `width` | number | 图片宽度 |
|
|
427
|
+
| `height` | number | 图片高度 |
|
|
428
|
+
| `dataUrl` | string | Base64 编码的图片数据 |
|
|
429
|
+
|
|
430
|
+
#### 示例:保存和加载画布
|
|
431
|
+
|
|
432
|
+
```javascript
|
|
433
|
+
// 保存画布内容
|
|
434
|
+
document.addEventListener('editor-change', (e) => {
|
|
435
|
+
const data = JSON.stringify({ objects: e.detail.objects });
|
|
436
|
+
localStorage.setItem('my-canvas', data);
|
|
292
437
|
});
|
|
293
438
|
|
|
294
|
-
//
|
|
439
|
+
// 加载画布内容
|
|
440
|
+
const savedData = localStorage.getItem('my-canvas');
|
|
441
|
+
if (savedData) {
|
|
442
|
+
document.querySelector('canvas-drawing-editor').setAttribute('initial-data', savedData);
|
|
443
|
+
}
|
|
444
|
+
```
|
|
445
|
+
|
|
446
|
+
#### `editor-close` 事件
|
|
447
|
+
|
|
448
|
+
```javascript
|
|
295
449
|
document.addEventListener('editor-close', () => {
|
|
296
450
|
console.log('编辑器已关闭');
|
|
297
451
|
});
|
|
@@ -30,12 +30,13 @@ class b extends HTMLElement {
|
|
|
30
30
|
"show-export",
|
|
31
31
|
"show-import",
|
|
32
32
|
"show-color",
|
|
33
|
-
"show-minimap"
|
|
33
|
+
"show-minimap",
|
|
34
|
+
"initial-data"
|
|
34
35
|
];
|
|
35
36
|
}
|
|
36
37
|
// 生命周期:连接到 DOM
|
|
37
38
|
connectedCallback() {
|
|
38
|
-
this.parseAttributes(), this.render(), this.setupEventListeners(), this.initCanvas();
|
|
39
|
+
this.parseAttributes(), this.render(), this.setupEventListeners(), this.initCanvas(), this.loadInitialData();
|
|
39
40
|
}
|
|
40
41
|
// 生命周期:从 DOM 断开
|
|
41
42
|
disconnectedCallback() {
|
|
@@ -43,7 +44,13 @@ class b extends HTMLElement {
|
|
|
43
44
|
}
|
|
44
45
|
// 生命周期:属性变化
|
|
45
46
|
attributeChangedCallback(t, i, s) {
|
|
46
|
-
i !== s
|
|
47
|
+
if (i !== s) {
|
|
48
|
+
if (t === "initial-data" && s && this.canvas) {
|
|
49
|
+
this.loadInitialData();
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
this.parseAttributes(), this.container && this.updateUI();
|
|
53
|
+
}
|
|
47
54
|
}
|
|
48
55
|
// 解析 HTML 属性
|
|
49
56
|
parseAttributes() {
|
|
@@ -66,6 +73,24 @@ class b extends HTMLElement {
|
|
|
66
73
|
generateId() {
|
|
67
74
|
return Math.random().toString(36).substr(2, 9);
|
|
68
75
|
}
|
|
76
|
+
// 加载初始数据
|
|
77
|
+
loadInitialData() {
|
|
78
|
+
const t = this.getAttribute("initial-data");
|
|
79
|
+
if (t)
|
|
80
|
+
try {
|
|
81
|
+
const i = JSON.parse(t);
|
|
82
|
+
i.objects && Array.isArray(i.objects) && (this.objects = i.objects, this.selectedId = null, this.objects.forEach((s) => {
|
|
83
|
+
if (s.type === "IMAGE" && s.dataUrl) {
|
|
84
|
+
const e = new Image();
|
|
85
|
+
e.onload = () => {
|
|
86
|
+
s.imageElement = e, this.renderCanvas(), this.renderMinimap();
|
|
87
|
+
}, e.src = s.dataUrl;
|
|
88
|
+
}
|
|
89
|
+
}), this.renderCanvas(), this.renderMinimap(), this.updateUI());
|
|
90
|
+
} catch (i) {
|
|
91
|
+
console.error("Failed to parse initial-data:", i);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
69
94
|
// 设置事件监听
|
|
70
95
|
setupEventListeners() {
|
|
71
96
|
window.addEventListener("resize", this.boundHandleResize), window.addEventListener("keydown", this.boundHandleKeyDown);
|
|
@@ -94,8 +119,8 @@ class b extends HTMLElement {
|
|
|
94
119
|
s = t.clientX, e = t.clientY;
|
|
95
120
|
else
|
|
96
121
|
return { x: 0, y: 0 };
|
|
97
|
-
const n = (s - i.left - this.panOffset.x) / this.scale,
|
|
98
|
-
return { x: n, y:
|
|
122
|
+
const n = (s - i.left - this.panOffset.x) / this.scale, o = (e - i.top - this.panOffset.y) / this.scale;
|
|
123
|
+
return { x: n, y: o };
|
|
99
124
|
}
|
|
100
125
|
// 获取屏幕坐标(不考虑缩放和平移)
|
|
101
126
|
getScreenPos(t) {
|
|
@@ -128,23 +153,23 @@ class b extends HTMLElement {
|
|
|
128
153
|
case "PATH": {
|
|
129
154
|
const i = t;
|
|
130
155
|
if (i.points.length === 0) return { x: 0, y: 0, width: 0, height: 0 };
|
|
131
|
-
const s = Math.min(...i.points.map((
|
|
132
|
-
return { x: s, y: n, width: e - s, height:
|
|
156
|
+
const s = Math.min(...i.points.map((a) => a.x)), e = Math.max(...i.points.map((a) => a.x)), n = Math.min(...i.points.map((a) => a.y)), o = Math.max(...i.points.map((a) => a.y));
|
|
157
|
+
return { x: s, y: n, width: e - s, height: o - n };
|
|
133
158
|
}
|
|
134
159
|
}
|
|
135
160
|
return { x: 0, y: 0, width: 0, height: 0 };
|
|
136
161
|
}
|
|
137
162
|
// 检查调整大小手柄
|
|
138
163
|
getResizeHandleAtPoint(t, i, s) {
|
|
139
|
-
const e = this.getObjectBounds(t), n = 8,
|
|
164
|
+
const e = this.getObjectBounds(t), n = 8, o = [
|
|
140
165
|
{ name: "nw", x: e.x, y: e.y },
|
|
141
166
|
{ name: "ne", x: e.x + e.width, y: e.y },
|
|
142
167
|
{ name: "sw", x: e.x, y: e.y + e.height },
|
|
143
168
|
{ name: "se", x: e.x + e.width, y: e.y + e.height }
|
|
144
169
|
];
|
|
145
|
-
for (const
|
|
146
|
-
if (Math.abs(i -
|
|
147
|
-
return
|
|
170
|
+
for (const a of o)
|
|
171
|
+
if (Math.abs(i - a.x) <= n && Math.abs(s - a.y) <= n)
|
|
172
|
+
return a.name;
|
|
148
173
|
return null;
|
|
149
174
|
}
|
|
150
175
|
// 碰撞检测
|
|
@@ -169,8 +194,8 @@ class b extends HTMLElement {
|
|
|
169
194
|
case "PATH": {
|
|
170
195
|
const e = t;
|
|
171
196
|
if (e.points.length === 0) return !1;
|
|
172
|
-
const n = Math.min(...e.points.map((r) => r.x)),
|
|
173
|
-
return i >= n && i <=
|
|
197
|
+
const n = Math.min(...e.points.map((r) => r.x)), o = Math.max(...e.points.map((r) => r.x)), a = Math.min(...e.points.map((r) => r.y)), l = Math.max(...e.points.map((r) => r.y));
|
|
198
|
+
return i >= n && i <= o && s >= a && s <= l;
|
|
174
199
|
}
|
|
175
200
|
}
|
|
176
201
|
return !1;
|
|
@@ -266,13 +291,13 @@ class b extends HTMLElement {
|
|
|
266
291
|
// 滚轮缩放
|
|
267
292
|
handleWheel(t) {
|
|
268
293
|
t.preventDefault();
|
|
269
|
-
const i = this.canvas.getBoundingClientRect(), s = t.clientX - i.left, e = t.clientY - i.top, n = t.deltaY > 0 ? 0.9 : 1.1,
|
|
270
|
-
this.zoomAtPoint(
|
|
294
|
+
const i = this.canvas.getBoundingClientRect(), s = t.clientX - i.left, e = t.clientY - i.top, n = t.deltaY > 0 ? 0.9 : 1.1, o = this.scale * n;
|
|
295
|
+
this.zoomAtPoint(o, s, e);
|
|
271
296
|
}
|
|
272
297
|
// 以指定点为中心缩放
|
|
273
298
|
zoomAtPoint(t, i, s) {
|
|
274
|
-
const e = Math.min(Math.max(t, 0.2), 5), n = (i - this.panOffset.x) / this.scale,
|
|
275
|
-
this.scale = e, this.panOffset = { x:
|
|
299
|
+
const e = Math.min(Math.max(t, 0.2), 5), n = (i - this.panOffset.x) / this.scale, o = (s - this.panOffset.y) / this.scale, a = i - n * e, l = s - o * e;
|
|
300
|
+
this.scale = e, this.panOffset = { x: a, y: l }, this.renderCanvas(), this.renderMinimap(), this.updateZoomDisplay();
|
|
276
301
|
}
|
|
277
302
|
// 放大
|
|
278
303
|
zoomIn() {
|
|
@@ -344,16 +369,16 @@ class b extends HTMLElement {
|
|
|
344
369
|
const { x: i, y: s } = this.getMousePos(t), e = this.getScreenPos(t);
|
|
345
370
|
if (this.dragStart = { x: i, y: s }, this.isDragging = !0, this.isTextInputVisible && this.tool !== "TEXT" && this.submitText(), this.tool === "SELECT") {
|
|
346
371
|
if (this.selectedId) {
|
|
347
|
-
const
|
|
348
|
-
if (
|
|
349
|
-
const
|
|
350
|
-
if (
|
|
351
|
-
this.saveHistory(), this.isResizing = !0, this.resizeHandle =
|
|
372
|
+
const o = this.objects.find((a) => a.id === this.selectedId);
|
|
373
|
+
if (o) {
|
|
374
|
+
const a = this.getResizeHandleAtPoint(o, i, s);
|
|
375
|
+
if (a) {
|
|
376
|
+
this.saveHistory(), this.isResizing = !0, this.resizeHandle = a, this.resizeStartBounds = this.getObjectBounds(o), this.resizeOriginalObject = JSON.parse(JSON.stringify(o));
|
|
352
377
|
return;
|
|
353
378
|
}
|
|
354
379
|
}
|
|
355
380
|
}
|
|
356
|
-
const n = [...this.objects].reverse().find((
|
|
381
|
+
const n = [...this.objects].reverse().find((o) => this.isHit(o, i, s));
|
|
357
382
|
n ? (this.selectedId = n.id, this.dragOffset = { x: i - n.x, y: s - n.y }, this.saveHistory(), this.updateUI()) : (this.selectedId = null, this.isPanning = !0, this.panStart = e, this.updateUI());
|
|
358
383
|
} else if (this.tool === "TEXT")
|
|
359
384
|
this.textInputPos = { x: i, y: s }, this.showTextInput(e.x, e.y), this.isDragging = !1;
|
|
@@ -367,8 +392,8 @@ class b extends HTMLElement {
|
|
|
367
392
|
// 画布鼠标移动
|
|
368
393
|
handleCanvasPointerMove(t) {
|
|
369
394
|
if (this.isPanning) {
|
|
370
|
-
const e = this.getScreenPos(t), n = e.x - this.panStart.x,
|
|
371
|
-
this.panOffset = { x: this.panOffset.x + n, y: this.panOffset.y +
|
|
395
|
+
const e = this.getScreenPos(t), n = e.x - this.panStart.x, o = e.y - this.panStart.y;
|
|
396
|
+
this.panOffset = { x: this.panOffset.x + n, y: this.panOffset.y + o }, this.panStart = e, this.renderCanvas(), this.renderMinimap();
|
|
372
397
|
return;
|
|
373
398
|
}
|
|
374
399
|
if (!this.isDragging || !this.dragStart) return;
|
|
@@ -376,27 +401,27 @@ class b extends HTMLElement {
|
|
|
376
401
|
if (this.isResizing && this.selectedId && this.resizeHandle && this.resizeStartBounds && this.resizeOriginalObject) {
|
|
377
402
|
const e = this.objects.find((d) => d.id === this.selectedId);
|
|
378
403
|
if (!e) return;
|
|
379
|
-
const n = i - this.dragStart.x,
|
|
380
|
-
let
|
|
381
|
-
switch (this.resizeHandle.includes("e") && (r = this.resizeStartBounds.width + n), this.resizeHandle.includes("w") && (
|
|
404
|
+
const n = i - this.dragStart.x, o = s - this.dragStart.y;
|
|
405
|
+
let a = this.resizeStartBounds.x, l = this.resizeStartBounds.y, r = this.resizeStartBounds.width, h = this.resizeStartBounds.height;
|
|
406
|
+
switch (this.resizeHandle.includes("e") && (r = this.resizeStartBounds.width + n), this.resizeHandle.includes("w") && (a = this.resizeStartBounds.x + n, r = this.resizeStartBounds.width - n), this.resizeHandle.includes("s") && (h = this.resizeStartBounds.height + o), this.resizeHandle.includes("n") && (l = this.resizeStartBounds.y + o, h = this.resizeStartBounds.height - o), r = Math.max(10, r), h = Math.max(10, h), e.type) {
|
|
382
407
|
case "RECTANGLE":
|
|
383
408
|
case "IMAGE":
|
|
384
|
-
e.x =
|
|
409
|
+
e.x = a, e.y = l, e.width = r, e.height = h;
|
|
385
410
|
break;
|
|
386
411
|
case "CIRCLE": {
|
|
387
412
|
const d = Math.max(r, h) / 2;
|
|
388
|
-
e.x =
|
|
413
|
+
e.x = a + d, e.y = l + d, e.radius = d;
|
|
389
414
|
break;
|
|
390
415
|
}
|
|
391
416
|
case "TEXT": {
|
|
392
417
|
const d = this.resizeOriginalObject, c = r / this.resizeStartBounds.width;
|
|
393
|
-
e.x =
|
|
418
|
+
e.x = a, e.y = l + h, e.fontSize = Math.max(8, Math.round(d.fontSize * c));
|
|
394
419
|
break;
|
|
395
420
|
}
|
|
396
421
|
case "PATH": {
|
|
397
422
|
const d = this.resizeOriginalObject, c = r / this.resizeStartBounds.width, p = h / this.resizeStartBounds.height;
|
|
398
423
|
e.points = d.points.map((u) => ({
|
|
399
|
-
x:
|
|
424
|
+
x: a + (u.x - this.resizeStartBounds.x) * c,
|
|
400
425
|
y: l + (u.y - this.resizeStartBounds.y) * p
|
|
401
426
|
}));
|
|
402
427
|
break;
|
|
@@ -409,8 +434,8 @@ class b extends HTMLElement {
|
|
|
409
434
|
const e = this.objects.find((n) => n.id === this.selectedId);
|
|
410
435
|
if (e) {
|
|
411
436
|
if (e.type === "PATH") {
|
|
412
|
-
const n = e,
|
|
413
|
-
n.points = n.points.map((l) => ({ x: l.x +
|
|
437
|
+
const n = e, o = i - this.dragStart.x, a = s - this.dragStart.y;
|
|
438
|
+
n.points = n.points.map((l) => ({ x: l.x + o, y: l.y + a })), this.dragStart = { x: i, y: s };
|
|
414
439
|
} else
|
|
415
440
|
e.x = i - this.dragOffset.x, e.y = s - this.dragOffset.y;
|
|
416
441
|
this.renderCanvas(), this.renderMinimap();
|
|
@@ -427,7 +452,7 @@ class b extends HTMLElement {
|
|
|
427
452
|
}
|
|
428
453
|
// 画布鼠标抬起
|
|
429
454
|
handleCanvasPointerUp() {
|
|
430
|
-
this.isDragging = !1, this.dragStart = null, this.isResizing = !1, this.resizeHandle = null, this.resizeStartBounds = null, this.resizeOriginalObject = null, this.isPanning = !1, this.currentObject && (this.objects.push(this.currentObject), this.currentObject = null, this.dispatchChangeEvent()), this.renderCanvas(), this.renderMinimap();
|
|
455
|
+
this.isDragging = !1, this.dragStart = null, this.isResizing = !1, this.resizeHandle = null, this.resizeStartBounds = null, this.resizeOriginalObject = null, this.isPanning = !1, this.currentObject && (this.objects.push(this.currentObject), this.currentObject = null, this.dispatchChangeEvent()), this.renderCanvas(), this.renderMinimap(), this.updateUI();
|
|
431
456
|
}
|
|
432
457
|
// 双击编辑文本
|
|
433
458
|
handleCanvasDoubleClick(t) {
|
|
@@ -436,8 +461,8 @@ class b extends HTMLElement {
|
|
|
436
461
|
if (e && e.type === "TEXT") {
|
|
437
462
|
const n = e;
|
|
438
463
|
this.editingTextId = n.id, this.textInputPos = { x: n.x, y: n.y };
|
|
439
|
-
const
|
|
440
|
-
this.showTextInput(
|
|
464
|
+
const o = n.x * this.scale + this.panOffset.x, a = n.y * this.scale + this.panOffset.y;
|
|
465
|
+
this.showTextInput(o, a, n.text), this.setTool("SELECT");
|
|
441
466
|
}
|
|
442
467
|
}
|
|
443
468
|
// 渲染画布
|
|
@@ -499,8 +524,8 @@ class b extends HTMLElement {
|
|
|
499
524
|
{ x: s.x + s.width, y: s.y },
|
|
500
525
|
{ x: s.x, y: s.y + s.height },
|
|
501
526
|
{ x: s.x + s.width, y: s.y + s.height }
|
|
502
|
-
].forEach((
|
|
503
|
-
t.beginPath(), t.rect(
|
|
527
|
+
].forEach((o) => {
|
|
528
|
+
t.beginPath(), t.rect(o.x - e / 2, o.y - e / 2, e, e), t.fill(), t.stroke();
|
|
504
529
|
}), t.strokeStyle = "#3b82f6", t.lineWidth = 1, t.setLineDash([5, 5]), t.strokeRect(s.x, s.y, s.width, s.height), t.setLineDash([]);
|
|
505
530
|
}
|
|
506
531
|
// 渲染小地图
|
|
@@ -508,8 +533,8 @@ class b extends HTMLElement {
|
|
|
508
533
|
if (!this.minimapCtx || !this.config.showMinimap) return;
|
|
509
534
|
const t = this.minimapCanvas, i = this.canvas;
|
|
510
535
|
this.minimapCtx.clearRect(0, 0, t.width, t.height);
|
|
511
|
-
const s = t.width / i.width, e = t.height / i.height, n = Math.min(s, e) * 0.92,
|
|
512
|
-
this.minimapCtx.fillStyle = "#ffffff", this.minimapCtx.fillRect(l, r,
|
|
536
|
+
const s = t.width / i.width, e = t.height / i.height, n = Math.min(s, e) * 0.92, o = i.width * n, a = i.height * n, l = (t.width - o) / 2, r = (t.height - a) / 2;
|
|
537
|
+
this.minimapCtx.fillStyle = "#ffffff", this.minimapCtx.fillRect(l, r, o, a), this.minimapCtx.save(), this.minimapCtx.translate(l, r), this.minimapCtx.scale(n, n), this.minimapCtx.translate(this.panOffset.x, this.panOffset.y), this.minimapCtx.scale(this.scale, this.scale), (this.currentObject ? [...this.objects, this.currentObject] : this.objects).forEach((d) => {
|
|
513
538
|
switch (this.minimapCtx.fillStyle = d.color, this.minimapCtx.strokeStyle = d.color, this.minimapCtx.lineWidth = Math.max(d.lineWidth, 1), this.minimapCtx.setLineDash([]), d.type) {
|
|
514
539
|
case "RECTANGLE": {
|
|
515
540
|
const c = d;
|
|
@@ -537,14 +562,14 @@ class b extends HTMLElement {
|
|
|
537
562
|
break;
|
|
538
563
|
}
|
|
539
564
|
}
|
|
540
|
-
}), this.minimapCtx.restore(), this.minimapCtx.strokeStyle = "#94a3b8", this.minimapCtx.lineWidth = 1, this.minimapCtx.strokeRect(l, r,
|
|
565
|
+
}), this.minimapCtx.restore(), this.minimapCtx.strokeStyle = "#94a3b8", this.minimapCtx.lineWidth = 1, this.minimapCtx.strokeRect(l, r, o, a);
|
|
541
566
|
}
|
|
542
567
|
// 小地图点击定位
|
|
543
568
|
handleMinimapClick(t) {
|
|
544
|
-
const i = this.minimapCanvas.getBoundingClientRect(), s = t.clientX - i.left, e = t.clientY - i.top, n = this.minimapCanvas.width / this.canvas.width,
|
|
569
|
+
const i = this.minimapCanvas.getBoundingClientRect(), s = t.clientX - i.left, e = t.clientY - i.top, n = this.minimapCanvas.width / this.canvas.width, o = this.minimapCanvas.height / this.canvas.height, a = Math.min(n, o) * 0.92, l = this.canvas.width * a, r = this.canvas.height * a, h = (this.minimapCanvas.width - l) / 2, d = (this.minimapCanvas.height - r) / 2, c = s - h, p = e - d, u = (c / a - this.panOffset.x) / this.scale, m = (p / a - this.panOffset.y) / this.scale, g = this.canvas.width / 2, x = this.canvas.height / 2;
|
|
545
570
|
this.panOffset = {
|
|
546
|
-
x:
|
|
547
|
-
y: x / this.scale -
|
|
571
|
+
x: g / this.scale - u,
|
|
572
|
+
y: x / this.scale - m
|
|
548
573
|
}, this.renderCanvas(), this.renderMinimap();
|
|
549
574
|
}
|
|
550
575
|
// 图片上传处理
|
|
@@ -554,11 +579,11 @@ class b extends HTMLElement {
|
|
|
554
579
|
const s = i.files[0], e = new FileReader();
|
|
555
580
|
e.onload = (n) => {
|
|
556
581
|
var l;
|
|
557
|
-
const
|
|
558
|
-
|
|
582
|
+
const o = (l = n.target) == null ? void 0 : l.result, a = new Image();
|
|
583
|
+
a.onload = () => {
|
|
559
584
|
this.saveHistory();
|
|
560
585
|
const r = 300;
|
|
561
|
-
let h =
|
|
586
|
+
let h = a.width, d = a.height;
|
|
562
587
|
if (h > r || d > r) {
|
|
563
588
|
const p = Math.min(r / h, r / d);
|
|
564
589
|
h *= p, d *= p;
|
|
@@ -572,19 +597,19 @@ class b extends HTMLElement {
|
|
|
572
597
|
height: d,
|
|
573
598
|
color: "#000000",
|
|
574
599
|
lineWidth: 1,
|
|
575
|
-
dataUrl:
|
|
576
|
-
imageElement:
|
|
600
|
+
dataUrl: o,
|
|
601
|
+
imageElement: a
|
|
577
602
|
};
|
|
578
603
|
this.objects.push(c), this.selectedId = c.id, this.setTool("SELECT"), this.renderCanvas(), this.renderMinimap(), this.updateUI(), this.dispatchChangeEvent();
|
|
579
|
-
},
|
|
604
|
+
}, a.src = o;
|
|
580
605
|
}, e.readAsDataURL(s), i.value = "";
|
|
581
606
|
}
|
|
582
607
|
// 保存 JSON
|
|
583
608
|
saveJson() {
|
|
584
609
|
const t = {
|
|
585
610
|
version: "1.0",
|
|
586
|
-
objects: this.objects.map((
|
|
587
|
-
const { imageElement:
|
|
611
|
+
objects: this.objects.map((o) => {
|
|
612
|
+
const { imageElement: a, ...l } = o;
|
|
588
613
|
return l;
|
|
589
614
|
})
|
|
590
615
|
}, i = JSON.stringify(t, null, 2), s = new Blob([i], { type: "application/json" }), e = URL.createObjectURL(s), n = document.createElement("a");
|
|
@@ -596,10 +621,10 @@ class b extends HTMLElement {
|
|
|
596
621
|
if (!i.files || i.files.length === 0) return;
|
|
597
622
|
const s = i.files[0], e = new FileReader();
|
|
598
623
|
e.onload = (n) => {
|
|
599
|
-
var
|
|
624
|
+
var o;
|
|
600
625
|
try {
|
|
601
|
-
const
|
|
602
|
-
|
|
626
|
+
const a = JSON.parse((o = n.target) == null ? void 0 : o.result);
|
|
627
|
+
a.objects && Array.isArray(a.objects) && (this.saveHistory(), this.objects = a.objects, this.selectedId = null, this.objects.forEach((l) => {
|
|
603
628
|
if (l.type === "IMAGE" && l.dataUrl) {
|
|
604
629
|
const r = new Image();
|
|
605
630
|
r.onload = () => {
|
|
@@ -607,8 +632,8 @@ class b extends HTMLElement {
|
|
|
607
632
|
}, r.src = l.dataUrl;
|
|
608
633
|
}
|
|
609
634
|
}), this.renderCanvas(), this.renderMinimap(), this.updateUI(), this.dispatchChangeEvent());
|
|
610
|
-
} catch (
|
|
611
|
-
console.error("Failed to load JSON:",
|
|
635
|
+
} catch (a) {
|
|
636
|
+
console.error("Failed to load JSON:", a);
|
|
612
637
|
}
|
|
613
638
|
}, e.readAsText(s), i.value = "";
|
|
614
639
|
}
|
|
@@ -628,7 +653,7 @@ class b extends HTMLElement {
|
|
|
628
653
|
if (this.selectedId) {
|
|
629
654
|
const e = this.objects.find((n) => n.id === this.selectedId);
|
|
630
655
|
if (e) {
|
|
631
|
-
const
|
|
656
|
+
const o = {
|
|
632
657
|
RECTANGLE: "矩形",
|
|
633
658
|
CIRCLE: "圆形",
|
|
634
659
|
PATH: "画笔",
|
|
@@ -636,15 +661,15 @@ class b extends HTMLElement {
|
|
|
636
661
|
IMAGE: "图片"
|
|
637
662
|
}[e.type] || e.type;
|
|
638
663
|
t.innerHTML = `
|
|
639
|
-
<span class="selection-label">已选择: ${
|
|
664
|
+
<span class="selection-label">已选择: ${o}</span>
|
|
640
665
|
<button class="delete-btn" title="删除">
|
|
641
666
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
642
667
|
<path d="M3 6h18M19 6v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6m3 0V4a2 2 0 012-2h4a2 2 0 012 2v2"/>
|
|
643
668
|
</svg>
|
|
644
669
|
</button>
|
|
645
670
|
`, t.classList.add("visible");
|
|
646
|
-
const
|
|
647
|
-
|
|
671
|
+
const a = t.querySelector(".delete-btn");
|
|
672
|
+
a && a.addEventListener("click", () => this.deleteSelected());
|
|
648
673
|
}
|
|
649
674
|
} else
|
|
650
675
|
t.classList.remove("visible"), t.innerHTML = "";
|
|
@@ -762,6 +787,7 @@ class b extends HTMLElement {
|
|
|
762
787
|
|
|
763
788
|
<!-- 文本输入 -->
|
|
764
789
|
<div class="text-input-container" style="display: none;">
|
|
790
|
+
<div class="text-input-hint">按 Enter 确认,Esc 取消</div>
|
|
765
791
|
<input type="text" class="text-input" placeholder="输入文本..." />
|
|
766
792
|
</div>
|
|
767
793
|
|
|
@@ -778,7 +804,10 @@ class b extends HTMLElement {
|
|
|
778
804
|
// 绑定事件
|
|
779
805
|
bindEvents() {
|
|
780
806
|
this.canvas.addEventListener("mousedown", (h) => this.handleCanvasPointerDown(h)), this.canvas.addEventListener("mousemove", (h) => this.handleCanvasPointerMove(h)), this.canvas.addEventListener("mouseup", () => this.handleCanvasPointerUp()), this.canvas.addEventListener("mouseleave", () => this.handleCanvasPointerUp()), this.canvas.addEventListener("dblclick", (h) => this.handleCanvasDoubleClick(h)), this.canvas.addEventListener("touchstart", (h) => this.handleCanvasPointerDown(h)), this.canvas.addEventListener("touchmove", (h) => this.handleCanvasPointerMove(h)), this.canvas.addEventListener("touchend", () => this.handleCanvasPointerUp()), this.canvas.addEventListener("wheel", this.boundHandleWheel, { passive: !1 }), this.shadow.querySelectorAll(".tool-btn[data-tool]").forEach((h) => {
|
|
781
|
-
h.addEventListener("
|
|
807
|
+
h.addEventListener("mousedown", (d) => {
|
|
808
|
+
d.preventDefault();
|
|
809
|
+
}), h.addEventListener("click", () => {
|
|
810
|
+
this.isTextInputVisible && this.submitText();
|
|
782
811
|
const d = h.getAttribute("data-tool");
|
|
783
812
|
this.setTool(d);
|
|
784
813
|
});
|
|
@@ -791,10 +820,10 @@ class b extends HTMLElement {
|
|
|
791
820
|
});
|
|
792
821
|
const s = this.shadow.querySelector(".image-input");
|
|
793
822
|
s && s.addEventListener("change", (h) => this.handleImageUpload(h));
|
|
794
|
-
const e = this.shadow.querySelector(".zoom-in-btn"), n = this.shadow.querySelector(".zoom-out-btn"),
|
|
795
|
-
e && e.addEventListener("click", () => this.zoomIn()), n && n.addEventListener("click", () => this.zoomOut()),
|
|
796
|
-
const
|
|
797
|
-
|
|
823
|
+
const e = this.shadow.querySelector(".zoom-in-btn"), n = this.shadow.querySelector(".zoom-out-btn"), o = this.shadow.querySelector(".zoom-text");
|
|
824
|
+
e && e.addEventListener("click", () => this.zoomIn()), n && n.addEventListener("click", () => this.zoomOut()), o && o.addEventListener("click", () => this.resetZoom());
|
|
825
|
+
const a = this.shadow.querySelector(".save-json-btn"), l = this.shadow.querySelector(".load-json-input"), r = this.shadow.querySelector(".export-png-btn");
|
|
826
|
+
a && a.addEventListener("click", () => this.saveJson()), l && l.addEventListener("change", (h) => this.loadJson(h)), r && r.addEventListener("click", () => this.exportPng()), this.minimapCanvas && this.minimapCanvas.addEventListener("click", (h) => this.handleMinimapClick(h)), this.textInput && (this.textInput.addEventListener("keydown", (h) => {
|
|
798
827
|
h.key === "Enter" ? (h.preventDefault(), this.submitText()) : h.key === "Escape" && this.hideTextInput();
|
|
799
828
|
}), this.textInput.addEventListener("blur", () => {
|
|
800
829
|
this.isTextInputVisible && this.submitText();
|
|
@@ -1105,6 +1134,19 @@ class b extends HTMLElement {
|
|
|
1105
1134
|
.text-input-container {
|
|
1106
1135
|
position: absolute;
|
|
1107
1136
|
z-index: 20;
|
|
1137
|
+
display: flex;
|
|
1138
|
+
flex-direction: column;
|
|
1139
|
+
align-items: flex-start;
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
.text-input-hint {
|
|
1143
|
+
background: rgba(0, 0, 0, 0.75);
|
|
1144
|
+
color: #fff;
|
|
1145
|
+
font-size: 12px;
|
|
1146
|
+
padding: 4px 8px;
|
|
1147
|
+
border-radius: 4px;
|
|
1148
|
+
margin-bottom: 4px;
|
|
1149
|
+
white-space: nowrap;
|
|
1108
1150
|
}
|
|
1109
1151
|
|
|
1110
1152
|
.text-input {
|