canvas-drawing-editor 1.0.1 → 2.0.0

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.
@@ -0,0 +1,1143 @@
1
+ const f = {
2
+ title: "Canvas Editor",
3
+ showPencil: !0,
4
+ showRectangle: !0,
5
+ showCircle: !0,
6
+ showText: !0,
7
+ showImage: !0,
8
+ showZoom: !0,
9
+ showDownload: !0,
10
+ showExport: !0,
11
+ showImport: !0,
12
+ showColor: !0,
13
+ showMinimap: !0
14
+ };
15
+ class b extends HTMLElement {
16
+ constructor() {
17
+ super(), this.config = { ...f }, this.objects = [], this.selectedId = null, this.tool = "SELECT", this.color = "#000000", this.lineWidth = 3, this.isDragging = !1, this.dragStart = null, this.currentObject = null, this.dragOffset = { x: 0, y: 0 }, this.isTextInputVisible = !1, this.textInputPos = { x: 0, y: 0 }, this.textInputScreenPos = { x: 0, y: 0 }, this.editingTextId = null, this.isResizing = !1, this.resizeHandle = null, this.resizeStartBounds = null, this.resizeOriginalObject = null, this.history = [], this.clipboard = null, this.scale = 1, this.panOffset = { x: 0, y: 0 }, this.isPanning = !1, this.panStart = { x: 0, y: 0 }, this.shadow = this.attachShadow({ mode: "open" }), this.boundHandleResize = this.handleResize.bind(this), this.boundHandleKeyDown = this.handleKeyDown.bind(this), this.boundHandleWheel = this.handleWheel.bind(this);
18
+ }
19
+ // 观察的属性
20
+ static get observedAttributes() {
21
+ return [
22
+ "title",
23
+ "show-pencil",
24
+ "show-rectangle",
25
+ "show-circle",
26
+ "show-text",
27
+ "show-image",
28
+ "show-zoom",
29
+ "show-download",
30
+ "show-export",
31
+ "show-import",
32
+ "show-color",
33
+ "show-minimap"
34
+ ];
35
+ }
36
+ // 生命周期:连接到 DOM
37
+ connectedCallback() {
38
+ this.parseAttributes(), this.render(), this.setupEventListeners(), this.initCanvas();
39
+ }
40
+ // 生命周期:从 DOM 断开
41
+ disconnectedCallback() {
42
+ this.removeEventListeners();
43
+ }
44
+ // 生命周期:属性变化
45
+ attributeChangedCallback(t, i, s) {
46
+ i !== s && (this.parseAttributes(), this.container && this.updateUI());
47
+ }
48
+ // 解析 HTML 属性
49
+ parseAttributes() {
50
+ this.config = {
51
+ title: this.getAttribute("title") || f.title,
52
+ showPencil: this.getAttribute("show-pencil") !== "false",
53
+ showRectangle: this.getAttribute("show-rectangle") !== "false",
54
+ showCircle: this.getAttribute("show-circle") !== "false",
55
+ showText: this.getAttribute("show-text") !== "false",
56
+ showImage: this.getAttribute("show-image") !== "false",
57
+ showZoom: this.getAttribute("show-zoom") !== "false",
58
+ showDownload: this.getAttribute("show-download") !== "false",
59
+ showExport: this.getAttribute("show-export") !== "false",
60
+ showImport: this.getAttribute("show-import") !== "false",
61
+ showColor: this.getAttribute("show-color") !== "false",
62
+ showMinimap: this.getAttribute("show-minimap") !== "false"
63
+ };
64
+ }
65
+ // 生成唯一 ID
66
+ generateId() {
67
+ return Math.random().toString(36).substr(2, 9);
68
+ }
69
+ // 设置事件监听
70
+ setupEventListeners() {
71
+ window.addEventListener("resize", this.boundHandleResize), window.addEventListener("keydown", this.boundHandleKeyDown);
72
+ }
73
+ // 移除事件监听
74
+ removeEventListeners() {
75
+ window.removeEventListener("resize", this.boundHandleResize), window.removeEventListener("keydown", this.boundHandleKeyDown), this.canvas && this.canvas.removeEventListener("wheel", this.boundHandleWheel);
76
+ }
77
+ // 窗口大小变化处理
78
+ handleResize() {
79
+ this.initCanvas();
80
+ }
81
+ // 初始化画布
82
+ initCanvas() {
83
+ !this.canvasContainer || !this.canvas || requestAnimationFrame(() => {
84
+ this.canvas.width = this.canvasContainer.clientWidth, this.canvas.height = this.canvasContainer.clientHeight, this.renderCanvas(), this.renderMinimap();
85
+ });
86
+ }
87
+ // 获取鼠标在画布上的位置(考虑缩放和平移)
88
+ getMousePos(t) {
89
+ const i = this.canvas.getBoundingClientRect();
90
+ let s, e;
91
+ if ("touches" in t && t.touches.length > 0)
92
+ s = t.touches[0].clientX, e = t.touches[0].clientY;
93
+ else if ("clientX" in t)
94
+ s = t.clientX, e = t.clientY;
95
+ else
96
+ return { x: 0, y: 0 };
97
+ const n = (s - i.left - this.panOffset.x) / this.scale, a = (e - i.top - this.panOffset.y) / this.scale;
98
+ return { x: n, y: a };
99
+ }
100
+ // 获取屏幕坐标(不考虑缩放和平移)
101
+ getScreenPos(t) {
102
+ const i = this.canvas.getBoundingClientRect();
103
+ let s, e;
104
+ if ("touches" in t && t.touches.length > 0)
105
+ s = t.touches[0].clientX, e = t.touches[0].clientY;
106
+ else if ("clientX" in t)
107
+ s = t.clientX, e = t.clientY;
108
+ else
109
+ return { x: 0, y: 0 };
110
+ return { x: s - i.left, y: e - i.top };
111
+ }
112
+ // 获取对象边界
113
+ getObjectBounds(t) {
114
+ switch (t.type) {
115
+ case "RECTANGLE":
116
+ case "IMAGE": {
117
+ const i = t;
118
+ return { x: i.x, y: i.y, width: i.width, height: i.height };
119
+ }
120
+ case "CIRCLE": {
121
+ const i = t;
122
+ return { x: i.x - i.radius, y: i.y - i.radius, width: i.radius * 2, height: i.radius * 2 };
123
+ }
124
+ case "TEXT": {
125
+ const i = t, s = i.text.length * i.fontSize * 0.6;
126
+ return { x: i.x, y: i.y - i.fontSize, width: s, height: i.fontSize };
127
+ }
128
+ case "PATH": {
129
+ const i = t;
130
+ if (i.points.length === 0) return { x: 0, y: 0, width: 0, height: 0 };
131
+ const s = Math.min(...i.points.map((o) => o.x)), e = Math.max(...i.points.map((o) => o.x)), n = Math.min(...i.points.map((o) => o.y)), a = Math.max(...i.points.map((o) => o.y));
132
+ return { x: s, y: n, width: e - s, height: a - n };
133
+ }
134
+ }
135
+ return { x: 0, y: 0, width: 0, height: 0 };
136
+ }
137
+ // 检查调整大小手柄
138
+ getResizeHandleAtPoint(t, i, s) {
139
+ const e = this.getObjectBounds(t), n = 8, a = [
140
+ { name: "nw", x: e.x, y: e.y },
141
+ { name: "ne", x: e.x + e.width, y: e.y },
142
+ { name: "sw", x: e.x, y: e.y + e.height },
143
+ { name: "se", x: e.x + e.width, y: e.y + e.height }
144
+ ];
145
+ for (const o of a)
146
+ if (Math.abs(i - o.x) <= n && Math.abs(s - o.y) <= n)
147
+ return o.name;
148
+ return null;
149
+ }
150
+ // 碰撞检测
151
+ isHit(t, i, s) {
152
+ switch (t.type) {
153
+ case "RECTANGLE": {
154
+ const e = t;
155
+ return i >= e.x && i <= e.x + e.width && s >= e.y && s <= e.y + e.height;
156
+ }
157
+ case "CIRCLE": {
158
+ const e = t;
159
+ return Math.sqrt(Math.pow(i - e.x, 2) + Math.pow(s - e.y, 2)) <= e.radius;
160
+ }
161
+ case "IMAGE": {
162
+ const e = t;
163
+ return i >= e.x && i <= e.x + e.width && s >= e.y && s <= e.y + e.height;
164
+ }
165
+ case "TEXT": {
166
+ const e = t;
167
+ return i >= e.x && i <= e.x + e.text.length * e.fontSize * 0.6 && s >= e.y - e.fontSize && s <= e.y;
168
+ }
169
+ case "PATH": {
170
+ const e = t;
171
+ if (e.points.length === 0) return !1;
172
+ const n = Math.min(...e.points.map((r) => r.x)), a = Math.max(...e.points.map((r) => r.x)), o = Math.min(...e.points.map((r) => r.y)), l = Math.max(...e.points.map((r) => r.y));
173
+ return i >= n && i <= a && s >= o && s <= l;
174
+ }
175
+ }
176
+ return !1;
177
+ }
178
+ // 保存历史
179
+ saveHistory() {
180
+ this.history.push(JSON.parse(JSON.stringify(this.objects)));
181
+ }
182
+ // 撤销
183
+ undo() {
184
+ if (this.history.length === 0) return;
185
+ const t = this.history.pop();
186
+ t && (this.objects = t, this.selectedId = null, this.renderCanvas(), this.renderMinimap(), this.dispatchChangeEvent());
187
+ }
188
+ // 删除选中对象
189
+ deleteSelected() {
190
+ this.selectedId && (this.saveHistory(), this.objects = this.objects.filter((t) => t.id !== this.selectedId), this.selectedId = null, this.renderCanvas(), this.renderMinimap(), this.updateUI(), this.dispatchChangeEvent());
191
+ }
192
+ // 复制选中对象
193
+ copySelected() {
194
+ if (this.selectedId) {
195
+ const t = this.objects.find((i) => i.id === this.selectedId);
196
+ t && (this.clipboard = JSON.parse(JSON.stringify(t)));
197
+ }
198
+ }
199
+ // 粘贴对象
200
+ pasteObject() {
201
+ if (this.clipboard) {
202
+ this.saveHistory();
203
+ const t = {
204
+ ...JSON.parse(JSON.stringify(this.clipboard)),
205
+ id: this.generateId(),
206
+ x: this.clipboard.x + 20,
207
+ y: this.clipboard.y + 20
208
+ };
209
+ t.type === "PATH" && t.points && (t.points = t.points.map((i) => ({
210
+ x: i.x + 20,
211
+ y: i.y + 20
212
+ }))), this.objects.push(t), this.selectedId = t.id, this.clipboard = t, this.renderCanvas(), this.renderMinimap(), this.updateUI(), this.dispatchChangeEvent();
213
+ }
214
+ }
215
+ // 派发变化事件
216
+ dispatchChangeEvent() {
217
+ this.dispatchEvent(new CustomEvent("editor-change", {
218
+ bubbles: !0,
219
+ composed: !0,
220
+ detail: { objects: this.objects }
221
+ }));
222
+ }
223
+ // 键盘事件处理
224
+ handleKeyDown(t) {
225
+ if (!this.isTextInputVisible) {
226
+ if ((t.ctrlKey || t.metaKey) && t.key === "z") {
227
+ t.preventDefault(), this.undo();
228
+ return;
229
+ }
230
+ if ((t.ctrlKey || t.metaKey) && t.key === "c") {
231
+ this.selectedId && (t.preventDefault(), this.copySelected());
232
+ return;
233
+ }
234
+ if ((t.ctrlKey || t.metaKey) && t.key === "v") {
235
+ this.clipboard && (t.preventDefault(), this.pasteObject());
236
+ return;
237
+ }
238
+ if ((t.key === "Delete" || t.key === "Backspace") && this.selectedId) {
239
+ t.preventDefault(), this.deleteSelected();
240
+ return;
241
+ }
242
+ if (!t.ctrlKey && !t.metaKey)
243
+ switch (t.key.toLowerCase()) {
244
+ case "v":
245
+ this.setTool("SELECT");
246
+ break;
247
+ case "p":
248
+ case "b":
249
+ this.setTool("PENCIL");
250
+ break;
251
+ case "r":
252
+ this.setTool("RECTANGLE");
253
+ break;
254
+ case "o":
255
+ this.setTool("CIRCLE");
256
+ break;
257
+ case "t":
258
+ this.setTool("TEXT");
259
+ break;
260
+ case "escape":
261
+ this.selectedId = null, this.hideTextInput(), this.renderCanvas(), this.updateUI();
262
+ break;
263
+ }
264
+ }
265
+ }
266
+ // 滚轮缩放
267
+ handleWheel(t) {
268
+ 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, a = this.scale * n;
270
+ this.zoomAtPoint(a, s, e);
271
+ }
272
+ // 以指定点为中心缩放
273
+ zoomAtPoint(t, i, s) {
274
+ const e = Math.min(Math.max(t, 0.2), 5), n = (i - this.panOffset.x) / this.scale, a = (s - this.panOffset.y) / this.scale, o = i - n * e, l = s - a * e;
275
+ this.scale = e, this.panOffset = { x: o, y: l }, this.renderCanvas(), this.renderMinimap(), this.updateZoomDisplay();
276
+ }
277
+ // 放大
278
+ zoomIn() {
279
+ const t = this.canvas.width / 2, i = this.canvas.height / 2;
280
+ this.zoomAtPoint(this.scale * 1.2, t, i);
281
+ }
282
+ // 缩小
283
+ zoomOut() {
284
+ const t = this.canvas.width / 2, i = this.canvas.height / 2;
285
+ this.zoomAtPoint(this.scale / 1.2, t, i);
286
+ }
287
+ // 重置缩放
288
+ resetZoom() {
289
+ this.scale = 1, this.panOffset = { x: 0, y: 0 }, this.renderCanvas(), this.renderMinimap(), this.updateZoomDisplay();
290
+ }
291
+ // 更新缩放显示
292
+ updateZoomDisplay() {
293
+ const t = this.shadow.querySelector(".zoom-text");
294
+ t && (t.textContent = `${Math.round(this.scale * 100)}%`);
295
+ }
296
+ // 设置工具
297
+ setTool(t) {
298
+ this.tool = t, this.updateToolButtons();
299
+ }
300
+ // 更新工具按钮状态
301
+ updateToolButtons() {
302
+ this.shadow.querySelectorAll(".tool-btn").forEach((i) => {
303
+ i.getAttribute("data-tool") === this.tool ? i.classList.add("active") : i.classList.remove("active");
304
+ });
305
+ }
306
+ // 隐藏文本输入
307
+ hideTextInput() {
308
+ this.isTextInputVisible = !1, this.textInputContainer && (this.textInputContainer.style.display = "none"), this.editingTextId = null;
309
+ }
310
+ // 显示文本输入
311
+ showTextInput(t, i, s = "") {
312
+ this.isTextInputVisible = !0, this.textInputScreenPos = { x: t, y: i }, this.textInputContainer && this.textInput && (this.textInputContainer.style.display = "block", this.textInputContainer.style.left = `${t}px`, this.textInputContainer.style.top = `${i - 30}px`, this.textInput.value = s, this.textInput.style.color = this.color, setTimeout(() => {
313
+ this.textInput.focus(), s && this.textInput.select();
314
+ }, 0));
315
+ }
316
+ // 提交文本
317
+ submitText() {
318
+ var i, s;
319
+ const t = (s = (i = this.textInput) == null ? void 0 : i.value) == null ? void 0 : s.trim();
320
+ if (t) {
321
+ if (this.editingTextId) {
322
+ const e = this.objects.find((n) => n.id === this.editingTextId);
323
+ e && e.text !== t && (this.saveHistory(), e.text = t), this.selectedId = this.editingTextId;
324
+ } else {
325
+ this.saveHistory();
326
+ const e = {
327
+ id: this.generateId(),
328
+ type: "TEXT",
329
+ x: this.textInputPos.x,
330
+ y: this.textInputPos.y,
331
+ text: t,
332
+ fontSize: 24,
333
+ color: this.color,
334
+ lineWidth: this.lineWidth
335
+ };
336
+ this.objects.push(e), this.selectedId = e.id;
337
+ }
338
+ this.dispatchChangeEvent();
339
+ }
340
+ this.hideTextInput(), this.setTool("SELECT"), this.renderCanvas(), this.renderMinimap(), this.updateUI();
341
+ }
342
+ // 画布鼠标按下
343
+ handleCanvasPointerDown(t) {
344
+ const { x: i, y: s } = this.getMousePos(t), e = this.getScreenPos(t);
345
+ if (this.dragStart = { x: i, y: s }, this.isDragging = !0, this.isTextInputVisible && this.tool !== "TEXT" && this.submitText(), this.tool === "SELECT") {
346
+ if (this.selectedId) {
347
+ const a = this.objects.find((o) => o.id === this.selectedId);
348
+ if (a) {
349
+ const o = this.getResizeHandleAtPoint(a, i, s);
350
+ if (o) {
351
+ this.saveHistory(), this.isResizing = !0, this.resizeHandle = o, this.resizeStartBounds = this.getObjectBounds(a), this.resizeOriginalObject = JSON.parse(JSON.stringify(a));
352
+ return;
353
+ }
354
+ }
355
+ }
356
+ const n = [...this.objects].reverse().find((a) => this.isHit(a, i, s));
357
+ 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
+ } else if (this.tool === "TEXT")
359
+ this.textInputPos = { x: i, y: s }, this.showTextInput(e.x, e.y), this.isDragging = !1;
360
+ else {
361
+ this.saveHistory();
362
+ const n = this.generateId();
363
+ this.tool === "RECTANGLE" ? this.currentObject = { id: n, type: "RECTANGLE", x: i, y: s, width: 0, height: 0, color: this.color, lineWidth: this.lineWidth } : this.tool === "CIRCLE" ? this.currentObject = { id: n, type: "CIRCLE", x: i, y: s, radius: 0, color: this.color, lineWidth: this.lineWidth } : this.tool === "PENCIL" && (this.currentObject = { id: n, type: "PATH", x: i, y: s, points: [{ x: i, y: s }], color: this.color, lineWidth: this.lineWidth });
364
+ }
365
+ this.renderCanvas();
366
+ }
367
+ // 画布鼠标移动
368
+ handleCanvasPointerMove(t) {
369
+ if (this.isPanning) {
370
+ const e = this.getScreenPos(t), n = e.x - this.panStart.x, a = e.y - this.panStart.y;
371
+ this.panOffset = { x: this.panOffset.x + n, y: this.panOffset.y + a }, this.panStart = e, this.renderCanvas(), this.renderMinimap();
372
+ return;
373
+ }
374
+ if (!this.isDragging || !this.dragStart) return;
375
+ const { x: i, y: s } = this.getMousePos(t);
376
+ if (this.isResizing && this.selectedId && this.resizeHandle && this.resizeStartBounds && this.resizeOriginalObject) {
377
+ const e = this.objects.find((d) => d.id === this.selectedId);
378
+ if (!e) return;
379
+ const n = i - this.dragStart.x, a = s - this.dragStart.y;
380
+ let o = this.resizeStartBounds.x, l = this.resizeStartBounds.y, r = this.resizeStartBounds.width, h = this.resizeStartBounds.height;
381
+ switch (this.resizeHandle.includes("e") && (r = this.resizeStartBounds.width + n), this.resizeHandle.includes("w") && (o = this.resizeStartBounds.x + n, r = this.resizeStartBounds.width - n), this.resizeHandle.includes("s") && (h = this.resizeStartBounds.height + a), this.resizeHandle.includes("n") && (l = this.resizeStartBounds.y + a, h = this.resizeStartBounds.height - a), r = Math.max(10, r), h = Math.max(10, h), e.type) {
382
+ case "RECTANGLE":
383
+ case "IMAGE":
384
+ e.x = o, e.y = l, e.width = r, e.height = h;
385
+ break;
386
+ case "CIRCLE": {
387
+ const d = Math.max(r, h) / 2;
388
+ e.x = o + d, e.y = l + d, e.radius = d;
389
+ break;
390
+ }
391
+ case "TEXT": {
392
+ const d = this.resizeOriginalObject, c = r / this.resizeStartBounds.width;
393
+ e.x = o, e.y = l + h, e.fontSize = Math.max(8, Math.round(d.fontSize * c));
394
+ break;
395
+ }
396
+ case "PATH": {
397
+ const d = this.resizeOriginalObject, c = r / this.resizeStartBounds.width, p = h / this.resizeStartBounds.height;
398
+ e.points = d.points.map((u) => ({
399
+ x: o + (u.x - this.resizeStartBounds.x) * c,
400
+ y: l + (u.y - this.resizeStartBounds.y) * p
401
+ }));
402
+ break;
403
+ }
404
+ }
405
+ this.renderCanvas(), this.renderMinimap();
406
+ return;
407
+ }
408
+ if (this.tool === "SELECT" && this.selectedId) {
409
+ const e = this.objects.find((n) => n.id === this.selectedId);
410
+ if (e) {
411
+ if (e.type === "PATH") {
412
+ const n = e, a = i - this.dragStart.x, o = s - this.dragStart.y;
413
+ n.points = n.points.map((l) => ({ x: l.x + a, y: l.y + o })), this.dragStart = { x: i, y: s };
414
+ } else
415
+ e.x = i - this.dragOffset.x, e.y = s - this.dragOffset.y;
416
+ this.renderCanvas(), this.renderMinimap();
417
+ }
418
+ } else if (this.currentObject) {
419
+ if (this.currentObject.type === "RECTANGLE")
420
+ this.currentObject.width = i - this.currentObject.x, this.currentObject.height = s - this.currentObject.y;
421
+ else if (this.currentObject.type === "CIRCLE") {
422
+ const e = Math.sqrt(Math.pow(i - this.currentObject.x, 2) + Math.pow(s - this.currentObject.y, 2));
423
+ this.currentObject.radius = e;
424
+ } else this.currentObject.type === "PATH" && this.currentObject.points.push({ x: i, y: s });
425
+ this.renderCanvas();
426
+ }
427
+ }
428
+ // 画布鼠标抬起
429
+ 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();
431
+ }
432
+ // 双击编辑文本
433
+ handleCanvasDoubleClick(t) {
434
+ t.preventDefault();
435
+ const { x: i, y: s } = this.getMousePos(t), e = [...this.objects].reverse().find((n) => this.isHit(n, i, s));
436
+ if (e && e.type === "TEXT") {
437
+ const n = e;
438
+ this.editingTextId = n.id, this.textInputPos = { x: n.x, y: n.y };
439
+ const a = n.x * this.scale + this.panOffset.x, o = n.y * this.scale + this.panOffset.y;
440
+ this.showTextInput(a, o, n.text), this.setTool("SELECT");
441
+ }
442
+ }
443
+ // 渲染画布
444
+ renderCanvas() {
445
+ if (this.ctx) {
446
+ if (this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height), this.ctx.fillStyle = "#ffffff", this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height), this.ctx.save(), this.ctx.translate(this.panOffset.x, this.panOffset.y), this.ctx.scale(this.scale, this.scale), this.objects.forEach((t) => this.drawObject(this.ctx, t, !1)), this.currentObject && this.drawObject(this.ctx, this.currentObject, !1), this.selectedId && this.tool === "SELECT") {
447
+ const t = this.objects.find((i) => i.id === this.selectedId);
448
+ t && this.drawSelectionHandles(this.ctx, t);
449
+ }
450
+ this.ctx.restore();
451
+ }
452
+ }
453
+ // 绘制单个对象
454
+ drawObject(t, i, s) {
455
+ switch (t.beginPath(), t.strokeStyle = i.color, t.lineWidth = i.lineWidth, t.fillStyle = i.color, !s && i.id === this.selectedId ? (t.shadowColor = "rgba(0, 100, 255, 0.5)", t.shadowBlur = 10) : t.shadowBlur = 0, i.type) {
456
+ case "RECTANGLE": {
457
+ const e = i;
458
+ t.strokeRect(e.x, e.y, e.width, e.height);
459
+ break;
460
+ }
461
+ case "CIRCLE": {
462
+ const e = i;
463
+ t.beginPath(), t.arc(e.x, e.y, e.radius, 0, 2 * Math.PI), t.stroke();
464
+ break;
465
+ }
466
+ case "PATH": {
467
+ const e = i;
468
+ if (e.points.length < 2) break;
469
+ t.beginPath(), t.lineCap = "round", t.lineJoin = "round", t.moveTo(e.points[0].x, e.points[0].y);
470
+ for (let n = 1; n < e.points.length; n++)
471
+ t.lineTo(e.points[n].x, e.points[n].y);
472
+ t.stroke();
473
+ break;
474
+ }
475
+ case "TEXT": {
476
+ const e = i;
477
+ t.font = `${e.fontSize}px sans-serif`, t.fillText(e.text, e.x, e.y);
478
+ break;
479
+ }
480
+ case "IMAGE": {
481
+ const e = i;
482
+ if (e.imageElement && e.imageElement.complete)
483
+ t.drawImage(e.imageElement, e.x, e.y, e.width, e.height);
484
+ else if (e.dataUrl) {
485
+ const n = new Image();
486
+ n.onload = () => {
487
+ e.imageElement = n, this.renderCanvas();
488
+ }, n.src = e.dataUrl;
489
+ }
490
+ break;
491
+ }
492
+ }
493
+ }
494
+ // 绘制选中手柄
495
+ drawSelectionHandles(t, i) {
496
+ const s = this.getObjectBounds(i), e = 8;
497
+ t.shadowBlur = 0, t.fillStyle = "#3b82f6", t.strokeStyle = "#ffffff", t.lineWidth = 2, [
498
+ { x: s.x, y: s.y },
499
+ { x: s.x + s.width, y: s.y },
500
+ { x: s.x, y: s.y + s.height },
501
+ { x: s.x + s.width, y: s.y + s.height }
502
+ ].forEach((a) => {
503
+ t.beginPath(), t.rect(a.x - e / 2, a.y - e / 2, e, e), t.fill(), t.stroke();
504
+ }), t.strokeStyle = "#3b82f6", t.lineWidth = 1, t.setLineDash([5, 5]), t.strokeRect(s.x, s.y, s.width, s.height), t.setLineDash([]);
505
+ }
506
+ // 渲染小地图
507
+ renderMinimap() {
508
+ if (!this.minimapCtx || !this.config.showMinimap) return;
509
+ const t = this.minimapCanvas, i = this.canvas;
510
+ 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, a = i.width * n, o = i.height * n, l = (t.width - a) / 2, r = (t.height - o) / 2;
512
+ this.minimapCtx.fillStyle = "#ffffff", this.minimapCtx.fillRect(l, r, a, o), 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
+ switch (this.minimapCtx.fillStyle = d.color, this.minimapCtx.strokeStyle = d.color, this.minimapCtx.lineWidth = d.lineWidth, this.minimapCtx.setLineDash([]), d.type) {
514
+ case "RECTANGLE": {
515
+ const c = d;
516
+ this.minimapCtx.fillRect(c.x, c.y, c.width, c.height);
517
+ break;
518
+ }
519
+ case "CIRCLE": {
520
+ const c = d;
521
+ this.minimapCtx.beginPath(), this.minimapCtx.arc(c.x, c.y, c.radius, 0, Math.PI * 2), this.minimapCtx.fill();
522
+ break;
523
+ }
524
+ case "PATH": {
525
+ const c = d;
526
+ c.points.length > 0 && (this.minimapCtx.beginPath(), this.minimapCtx.lineCap = "round", this.minimapCtx.lineJoin = "round", this.minimapCtx.moveTo(c.points[0].x, c.points[0].y), c.points.forEach((p) => this.minimapCtx.lineTo(p.x, p.y)), this.minimapCtx.stroke());
527
+ break;
528
+ }
529
+ case "TEXT": {
530
+ const c = d;
531
+ this.minimapCtx.font = `${c.fontSize}px sans-serif`, this.minimapCtx.fillText(c.text, c.x, c.y);
532
+ break;
533
+ }
534
+ case "IMAGE": {
535
+ const c = d;
536
+ c.imageElement && this.minimapCtx.drawImage(c.imageElement, c.x, c.y, c.width, c.height);
537
+ break;
538
+ }
539
+ }
540
+ }), this.minimapCtx.restore(), this.minimapCtx.strokeStyle = "#94a3b8", this.minimapCtx.lineWidth = 1, this.minimapCtx.strokeRect(l, r, a, o);
541
+ }
542
+ // 小地图点击定位
543
+ 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, a = this.minimapCanvas.height / this.canvas.height, o = Math.min(n, a) * 0.92, l = this.canvas.width * o, r = this.canvas.height * o, h = (this.minimapCanvas.width - l) / 2, d = (this.minimapCanvas.height - r) / 2, c = s - h, p = e - d, u = (c / o - this.panOffset.x) / this.scale, g = (p / o - this.panOffset.y) / this.scale, m = this.canvas.width / 2, x = this.canvas.height / 2;
545
+ this.panOffset = {
546
+ x: m / this.scale - u,
547
+ y: x / this.scale - g
548
+ }, this.renderCanvas(), this.renderMinimap();
549
+ }
550
+ // 图片上传处理
551
+ handleImageUpload(t) {
552
+ const i = t.target;
553
+ if (!i.files || i.files.length === 0) return;
554
+ const s = i.files[0], e = new FileReader();
555
+ e.onload = (n) => {
556
+ var l;
557
+ const a = (l = n.target) == null ? void 0 : l.result, o = new Image();
558
+ o.onload = () => {
559
+ this.saveHistory();
560
+ const r = 300;
561
+ let h = o.width, d = o.height;
562
+ if (h > r || d > r) {
563
+ const p = Math.min(r / h, r / d);
564
+ h *= p, d *= p;
565
+ }
566
+ const c = {
567
+ id: this.generateId(),
568
+ type: "IMAGE",
569
+ x: 100,
570
+ y: 100,
571
+ width: h,
572
+ height: d,
573
+ color: "#000000",
574
+ lineWidth: 1,
575
+ dataUrl: a,
576
+ imageElement: o
577
+ };
578
+ this.objects.push(c), this.selectedId = c.id, this.setTool("SELECT"), this.renderCanvas(), this.renderMinimap(), this.updateUI(), this.dispatchChangeEvent();
579
+ }, o.src = a;
580
+ }, e.readAsDataURL(s), i.value = "";
581
+ }
582
+ // 保存 JSON
583
+ saveJson() {
584
+ const t = {
585
+ version: "1.0",
586
+ objects: this.objects.map((a) => {
587
+ const { imageElement: o, ...l } = a;
588
+ return l;
589
+ })
590
+ }, i = JSON.stringify(t, null, 2), s = new Blob([i], { type: "application/json" }), e = URL.createObjectURL(s), n = document.createElement("a");
591
+ n.href = e, n.download = "canvas-project.json", n.click(), URL.revokeObjectURL(e);
592
+ }
593
+ // 加载 JSON
594
+ loadJson(t) {
595
+ const i = t.target;
596
+ if (!i.files || i.files.length === 0) return;
597
+ const s = i.files[0], e = new FileReader();
598
+ e.onload = (n) => {
599
+ var a;
600
+ try {
601
+ const o = JSON.parse((a = n.target) == null ? void 0 : a.result);
602
+ o.objects && Array.isArray(o.objects) && (this.saveHistory(), this.objects = o.objects, this.selectedId = null, this.objects.forEach((l) => {
603
+ if (l.type === "IMAGE" && l.dataUrl) {
604
+ const r = new Image();
605
+ r.onload = () => {
606
+ l.imageElement = r, this.renderCanvas(), this.renderMinimap();
607
+ }, r.src = l.dataUrl;
608
+ }
609
+ }), this.renderCanvas(), this.renderMinimap(), this.updateUI(), this.dispatchChangeEvent());
610
+ } catch (o) {
611
+ console.error("Failed to load JSON:", o);
612
+ }
613
+ }, e.readAsText(s), i.value = "";
614
+ }
615
+ // 导出 PNG
616
+ exportPng() {
617
+ const t = document.createElement("canvas");
618
+ t.width = this.canvas.width, t.height = this.canvas.height;
619
+ const i = t.getContext("2d");
620
+ i.fillStyle = "#ffffff", i.fillRect(0, 0, t.width, t.height), i.translate(this.panOffset.x, this.panOffset.y), i.scale(this.scale, this.scale), this.objects.forEach((n) => this.drawObject(i, n, !0));
621
+ const s = t.toDataURL("image/png"), e = document.createElement("a");
622
+ e.href = s, e.download = "canvas-export.png", e.click();
623
+ }
624
+ // 更新 UI
625
+ updateUI() {
626
+ const t = this.shadow.querySelector(".selection-info");
627
+ if (t)
628
+ if (this.selectedId) {
629
+ const s = this.objects.find((e) => e.id === this.selectedId);
630
+ if (s) {
631
+ const n = {
632
+ RECTANGLE: "矩形",
633
+ CIRCLE: "圆形",
634
+ PATH: "画笔",
635
+ TEXT: "文本",
636
+ IMAGE: "图片"
637
+ }[s.type] || s.type;
638
+ t.innerHTML = `
639
+ <span class="selection-label">已选择: ${n}</span>
640
+ <button class="delete-btn" title="删除">
641
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
642
+ <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
+ </svg>
644
+ </button>
645
+ `, t.classList.add("visible");
646
+ const a = t.querySelector(".delete-btn");
647
+ a && a.addEventListener("click", () => this.deleteSelected());
648
+ }
649
+ } else
650
+ t.classList.remove("visible"), t.innerHTML = "";
651
+ const i = this.shadow.querySelector(".undo-btn");
652
+ i && (i.disabled = this.history.length === 0);
653
+ }
654
+ // 渲染 DOM 结构
655
+ render() {
656
+ this.shadow.innerHTML = `
657
+ <style>${this.getStyles()}</style>
658
+ <div class="editor-container">
659
+ <!-- 左侧工具栏 -->
660
+ <div class="toolbar">
661
+ ${this.config.showPencil ? this.createToolButton("PENCIL", "pencil-icon", "画笔 (P)") : ""}
662
+ ${this.config.showRectangle ? this.createToolButton("RECTANGLE", "rect-icon", "矩形 (R)") : ""}
663
+ ${this.config.showCircle ? this.createToolButton("CIRCLE", "circle-icon", "圆形 (O)") : ""}
664
+ ${this.config.showText ? this.createToolButton("TEXT", "text-icon", "文本 (T)") : ""}
665
+ ${this.config.showImage ? `
666
+ <label class="tool-btn" title="插入图片">
667
+ <svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
668
+ <rect x="3" y="3" width="18" height="18" rx="2"/>
669
+ <circle cx="8.5" cy="8.5" r="1.5"/>
670
+ <path d="M21 15l-5-5L5 21"/>
671
+ </svg>
672
+ <input type="file" accept="image/*" class="hidden image-input" />
673
+ </label>
674
+ ` : ""}
675
+ <div class="divider"></div>
676
+ <button class="tool-btn undo-btn" title="撤销 (Ctrl+Z)" disabled>
677
+ <svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
678
+ <path d="M3 7v6h6M3 13a9 9 0 1 0 2.5-6.5L3 7"/>
679
+ </svg>
680
+ </button>
681
+ <div class="spacer"></div>
682
+ ${this.config.showColor ? `
683
+ <input type="color" class="color-picker" value="${this.color}" title="颜色" />
684
+ ` : ""}
685
+ </div>
686
+
687
+ <!-- 主区域 -->
688
+ <div class="main-area">
689
+ <!-- 顶部栏 -->
690
+ <div class="top-bar">
691
+ <div class="top-bar-left">
692
+ <h2 class="title">${this.config.title}</h2>
693
+ <div class="selection-info"></div>
694
+ </div>
695
+ <div class="top-bar-right">
696
+ ${this.config.showZoom ? `
697
+ <div class="zoom-controls">
698
+ <button class="zoom-btn zoom-out-btn" title="缩小">
699
+ <svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
700
+ <circle cx="11" cy="11" r="8"/><path d="M21 21l-4.35-4.35M8 11h6"/>
701
+ </svg>
702
+ </button>
703
+ <button class="zoom-text" title="重置缩放">100%</button>
704
+ <button class="zoom-btn zoom-in-btn" title="放大">
705
+ <svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
706
+ <circle cx="11" cy="11" r="8"/><path d="M21 21l-4.35-4.35M11 8v6M8 11h6"/>
707
+ </svg>
708
+ </button>
709
+ </div>
710
+ ` : ""}
711
+ ${this.config.showExport || this.config.showImport || this.config.showDownload ? `
712
+ <div class="file-controls">
713
+ ${this.config.showExport ? `
714
+ <button class="file-btn save-json-btn" title="保存项目 (JSON)">
715
+ <svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
716
+ <path d="M19 21H5a2 2 0 01-2-2V5a2 2 0 012-2h11l5 5v11a2 2 0 01-2 2z"/>
717
+ <polyline points="17 21 17 13 7 13 7 21"/><polyline points="7 3 7 8 15 8"/>
718
+ </svg>
719
+ </button>
720
+ ` : ""}
721
+ ${this.config.showImport ? `
722
+ <label class="file-btn" title="加载项目 (JSON)">
723
+ <svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
724
+ <path d="M22 19a2 2 0 01-2 2H4a2 2 0 01-2-2V5a2 2 0 012-2h5l2 3h9a2 2 0 012 2z"/>
725
+ </svg>
726
+ <input type="file" accept=".json" class="hidden load-json-input" />
727
+ </label>
728
+ ` : ""}
729
+ ${this.config.showDownload ? `
730
+ <button class="file-btn export-png-btn" title="导出 PNG">
731
+ <svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
732
+ <path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4M7 10l5 5 5-5M12 15V3"/>
733
+ </svg>
734
+ </button>
735
+ ` : ""}
736
+ </div>
737
+ ` : ""}
738
+ </div>
739
+ </div>
740
+
741
+ <!-- 画布容器 -->
742
+ <div class="canvas-container">
743
+ <canvas class="main-canvas"></canvas>
744
+
745
+ ${this.config.showMinimap ? `
746
+ <div class="minimap-wrapper">
747
+ <div class="minimap-header">
748
+ <svg class="minimap-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
749
+ <rect x="3" y="3" width="18" height="18" rx="2"/>
750
+ <path d="M3 9h18M9 21V9"/>
751
+ </svg>
752
+ <span>导航</span>
753
+ </div>
754
+ <canvas class="minimap-canvas" width="220" height="140"></canvas>
755
+ </div>
756
+ ` : ""}
757
+
758
+ <!-- 文本输入 -->
759
+ <div class="text-input-container" style="display: none;">
760
+ <input type="text" class="text-input" placeholder="输入文本..." />
761
+ </div>
762
+
763
+ <!-- 空画布提示 -->
764
+ <div class="empty-hint">
765
+ <h3>开始创作</h3>
766
+ <p>选择左侧的工具开始绘制</p>
767
+ </div>
768
+ </div>
769
+ </div>
770
+ </div>
771
+ `, this.container = this.shadow.querySelector(".editor-container"), this.toolbar = this.shadow.querySelector(".toolbar"), this.topBar = this.shadow.querySelector(".top-bar"), this.canvasContainer = this.shadow.querySelector(".canvas-container"), this.canvas = this.shadow.querySelector(".main-canvas"), this.ctx = this.canvas.getContext("2d"), this.config.showMinimap && (this.minimapCanvas = this.shadow.querySelector(".minimap-canvas"), this.minimapCtx = this.minimapCanvas.getContext("2d")), this.textInputContainer = this.shadow.querySelector(".text-input-container"), this.textInput = this.shadow.querySelector(".text-input"), this.bindEvents();
772
+ }
773
+ // 绑定事件
774
+ bindEvents() {
775
+ 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) => {
776
+ h.addEventListener("click", () => {
777
+ const d = h.getAttribute("data-tool");
778
+ this.setTool(d);
779
+ });
780
+ });
781
+ const t = this.shadow.querySelector(".undo-btn");
782
+ t && t.addEventListener("click", () => this.undo());
783
+ const i = this.shadow.querySelector(".color-picker");
784
+ i && i.addEventListener("input", (h) => {
785
+ this.color = h.target.value;
786
+ });
787
+ const s = this.shadow.querySelector(".image-input");
788
+ s && s.addEventListener("change", (h) => this.handleImageUpload(h));
789
+ const e = this.shadow.querySelector(".zoom-in-btn"), n = this.shadow.querySelector(".zoom-out-btn"), a = this.shadow.querySelector(".zoom-text");
790
+ e && e.addEventListener("click", () => this.zoomIn()), n && n.addEventListener("click", () => this.zoomOut()), a && a.addEventListener("click", () => this.resetZoom());
791
+ const o = this.shadow.querySelector(".save-json-btn"), l = this.shadow.querySelector(".load-json-input"), r = this.shadow.querySelector(".export-png-btn");
792
+ o && o.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) => {
793
+ h.key === "Enter" ? (h.preventDefault(), this.submitText()) : h.key === "Escape" && this.hideTextInput();
794
+ }), this.textInput.addEventListener("blur", () => {
795
+ this.isTextInputVisible && this.submitText();
796
+ }));
797
+ }
798
+ // 创建工具按钮 HTML
799
+ createToolButton(t, i, s) {
800
+ const e = {
801
+ "pencil-icon": '<path d="M17 3a2.85 2.85 0 114 4L7.5 20.5 2 22l1.5-5.5L17 3z"/>',
802
+ "rect-icon": '<rect x="3" y="3" width="18" height="18" rx="2"/>',
803
+ "circle-icon": '<circle cx="12" cy="12" r="10"/>',
804
+ "text-icon": '<path d="M4 7V4h16v3M9 20h6M12 4v16"/>'
805
+ };
806
+ return `
807
+ <button class="tool-btn ${this.tool === t ? "active" : ""}" data-tool="${t}" title="${s}">
808
+ <svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
809
+ ${e[i]}
810
+ </svg>
811
+ </button>
812
+ `;
813
+ }
814
+ // 获取样式
815
+ getStyles() {
816
+ return `
817
+ :host {
818
+ display: block;
819
+ width: 100%;
820
+ height: 100%;
821
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
822
+ }
823
+
824
+ * {
825
+ box-sizing: border-box;
826
+ margin: 0;
827
+ padding: 0;
828
+ }
829
+
830
+ .hidden {
831
+ display: none !important;
832
+ }
833
+
834
+ .editor-container {
835
+ display: flex;
836
+ width: 100%;
837
+ height: 100%;
838
+ background: #f1f5f9;
839
+ }
840
+
841
+ /* 工具栏 */
842
+ .toolbar {
843
+ width: 64px;
844
+ background: #ffffff;
845
+ border-right: 1px solid #e2e8f0;
846
+ display: flex;
847
+ flex-direction: column;
848
+ align-items: center;
849
+ padding: 12px 8px;
850
+ gap: 4px;
851
+ }
852
+
853
+ .tool-btn {
854
+ width: 44px;
855
+ height: 44px;
856
+ border: none;
857
+ background: transparent;
858
+ border-radius: 12px;
859
+ cursor: pointer;
860
+ display: flex;
861
+ align-items: center;
862
+ justify-content: center;
863
+ color: #64748b;
864
+ transition: all 0.2s;
865
+ }
866
+
867
+ .tool-btn:hover {
868
+ background: #f1f5f9;
869
+ color: #4f46e5;
870
+ }
871
+
872
+ .tool-btn.active {
873
+ background: #4f46e5;
874
+ color: #ffffff;
875
+ box-shadow: 0 4px 12px rgba(79, 70, 229, 0.3);
876
+ transform: scale(1.05);
877
+ }
878
+
879
+ .tool-btn:disabled {
880
+ color: #cbd5e1;
881
+ cursor: not-allowed;
882
+ }
883
+
884
+ .tool-btn:disabled:hover {
885
+ background: transparent;
886
+ color: #cbd5e1;
887
+ }
888
+
889
+ .icon {
890
+ width: 20px;
891
+ height: 20px;
892
+ }
893
+
894
+ .divider {
895
+ width: 32px;
896
+ height: 1px;
897
+ background: #e2e8f0;
898
+ margin: 8px 0;
899
+ }
900
+
901
+ .spacer {
902
+ flex: 1;
903
+ }
904
+
905
+ .color-picker {
906
+ width: 32px;
907
+ height: 32px;
908
+ border: 2px solid #e2e8f0;
909
+ border-radius: 50%;
910
+ cursor: pointer;
911
+ padding: 0;
912
+ overflow: hidden;
913
+ -webkit-appearance: none;
914
+ }
915
+
916
+ .color-picker::-webkit-color-swatch-wrapper {
917
+ padding: 0;
918
+ }
919
+
920
+ .color-picker::-webkit-color-swatch {
921
+ border: none;
922
+ border-radius: 50%;
923
+ }
924
+
925
+ /* 主区域 */
926
+ .main-area {
927
+ flex: 1;
928
+ display: flex;
929
+ flex-direction: column;
930
+ overflow: hidden;
931
+ }
932
+
933
+ /* 顶部栏 */
934
+ .top-bar {
935
+ height: 56px;
936
+ background: #ffffff;
937
+ border-bottom: 1px solid #e2e8f0;
938
+ display: flex;
939
+ align-items: center;
940
+ justify-content: space-between;
941
+ padding: 0 16px;
942
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
943
+ }
944
+
945
+ .top-bar-left, .top-bar-right {
946
+ display: flex;
947
+ align-items: center;
948
+ gap: 16px;
949
+ }
950
+
951
+ .title {
952
+ font-size: 16px;
953
+ font-weight: 600;
954
+ color: #334155;
955
+ }
956
+
957
+ .selection-info {
958
+ display: none;
959
+ align-items: center;
960
+ gap: 8px;
961
+ background: #eef2ff;
962
+ padding: 4px 12px;
963
+ border-radius: 20px;
964
+ border: 1px solid #c7d2fe;
965
+ }
966
+
967
+ .selection-info.visible {
968
+ display: flex;
969
+ }
970
+
971
+ .selection-label {
972
+ font-size: 12px;
973
+ font-weight: 600;
974
+ color: #4f46e5;
975
+ text-transform: uppercase;
976
+ }
977
+
978
+ .delete-btn {
979
+ background: none;
980
+ border: none;
981
+ color: #ef4444;
982
+ cursor: pointer;
983
+ padding: 4px;
984
+ display: flex;
985
+ align-items: center;
986
+ justify-content: center;
987
+ }
988
+
989
+ .delete-btn:hover {
990
+ color: #dc2626;
991
+ }
992
+
993
+ .zoom-controls, .file-controls {
994
+ display: flex;
995
+ align-items: center;
996
+ background: #f1f5f9;
997
+ border-radius: 8px;
998
+ padding: 4px;
999
+ }
1000
+
1001
+ .zoom-btn, .file-btn {
1002
+ width: 32px;
1003
+ height: 32px;
1004
+ border: none;
1005
+ background: transparent;
1006
+ border-radius: 6px;
1007
+ cursor: pointer;
1008
+ display: flex;
1009
+ align-items: center;
1010
+ justify-content: center;
1011
+ color: #475569;
1012
+ transition: all 0.2s;
1013
+ }
1014
+
1015
+ .zoom-btn:hover, .file-btn:hover {
1016
+ color: #4f46e5;
1017
+ }
1018
+
1019
+ .zoom-text {
1020
+ padding: 4px 8px;
1021
+ font-size: 12px;
1022
+ font-weight: 500;
1023
+ color: #475569;
1024
+ background: transparent;
1025
+ border: none;
1026
+ cursor: pointer;
1027
+ min-width: 50px;
1028
+ text-align: center;
1029
+ }
1030
+
1031
+ .zoom-text:hover {
1032
+ color: #4f46e5;
1033
+ }
1034
+
1035
+ /* 画布容器 */
1036
+ .canvas-container {
1037
+ flex: 1;
1038
+ position: relative;
1039
+ background: #f1f5f9;
1040
+ overflow: hidden;
1041
+ }
1042
+
1043
+ .main-canvas {
1044
+ position: absolute;
1045
+ inset: 0;
1046
+ display: block;
1047
+ cursor: crosshair;
1048
+ touch-action: none;
1049
+ }
1050
+
1051
+ /* 小地图 */
1052
+ .minimap-wrapper {
1053
+ position: absolute;
1054
+ top: 16px;
1055
+ right: 16px;
1056
+ z-index: 30;
1057
+ border-radius: 12px;
1058
+ overflow: hidden;
1059
+ box-shadow: 0 10px 25px rgba(0, 0, 0, 0.15);
1060
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
1061
+ padding: 2px;
1062
+ }
1063
+
1064
+ .minimap-wrapper > * {
1065
+ background: #ffffff;
1066
+ }
1067
+
1068
+ .minimap-header {
1069
+ display: flex;
1070
+ align-items: center;
1071
+ gap: 8px;
1072
+ padding: 6px 12px;
1073
+ background: linear-gradient(to right, #f8fafc, #f1f5f9);
1074
+ border-bottom: 1px solid #e2e8f0;
1075
+ font-size: 12px;
1076
+ font-weight: 600;
1077
+ color: #475569;
1078
+ border-radius: 10px 10px 0 0;
1079
+ }
1080
+
1081
+ .minimap-icon {
1082
+ width: 12px;
1083
+ height: 12px;
1084
+ color: #4f46e5;
1085
+ }
1086
+
1087
+ .minimap-canvas {
1088
+ cursor: pointer;
1089
+ background: #f8fafc;
1090
+ border-radius: 0 0 10px 10px;
1091
+ display: block;
1092
+ }
1093
+
1094
+ .minimap-canvas:hover {
1095
+ filter: brightness(1.05);
1096
+ }
1097
+
1098
+ /* 文本输入 */
1099
+ .text-input-container {
1100
+ position: absolute;
1101
+ z-index: 20;
1102
+ }
1103
+
1104
+ .text-input {
1105
+ padding: 8px 12px;
1106
+ border: 2px solid #4f46e5;
1107
+ border-radius: 8px;
1108
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
1109
+ outline: none;
1110
+ min-width: 200px;
1111
+ font-size: 16px;
1112
+ }
1113
+
1114
+ /* 空画布提示 */
1115
+ .empty-hint {
1116
+ position: absolute;
1117
+ inset: 0;
1118
+ display: flex;
1119
+ flex-direction: column;
1120
+ align-items: center;
1121
+ justify-content: center;
1122
+ pointer-events: none;
1123
+ opacity: 0.4;
1124
+ }
1125
+
1126
+ .empty-hint h3 {
1127
+ font-size: 24px;
1128
+ font-weight: 700;
1129
+ color: #94a3b8;
1130
+ margin-bottom: 8px;
1131
+ }
1132
+
1133
+ .empty-hint p {
1134
+ color: #94a3b8;
1135
+ }
1136
+ `;
1137
+ }
1138
+ }
1139
+ typeof window < "u" && !customElements.get("canvas-drawing-editor") && customElements.define("canvas-drawing-editor", b);
1140
+ export {
1141
+ b as CanvasDrawingEditor
1142
+ };
1143
+ //# sourceMappingURL=canvas-drawing-editor.es.js.map