canvas-drawing-editor 2.6.0 → 2.6.2

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
@@ -576,6 +576,38 @@ editor.tweenAnimate(objectId, { x: 400 }, {
576
576
  editor.stopAllAnimations();
577
577
  ```
578
578
 
579
+ ### 🖼️ 图片导出 API
580
+
581
+ 通过 `getImageData()` 方法可以获取画布图片数据(base64 或 Blob),无需触发下载:
582
+
583
+ ```javascript
584
+ const editor = document.querySelector('canvas-drawing-editor');
585
+
586
+ // 获取 base64 格式(默认)
587
+ const dataURL = await editor.getImageData();
588
+ console.log(dataURL); // data:image/png;base64,...
589
+
590
+ // 获取 Blob 格式(适合上传到服务器)
591
+ const blob = await editor.getImageData({
592
+ type: 'blob',
593
+ format: 'png', // 'png' | 'jpeg' | 'webp'
594
+ quality: 0.92, // jpeg/webp 质量 (0-1)
595
+ background: '#ffffff' // 背景色
596
+ });
597
+
598
+ // 上传到服务器示例
599
+ const formData = new FormData();
600
+ formData.append('image', blob, 'canvas.png');
601
+ await fetch('/api/upload', { method: 'POST', body: formData });
602
+ ```
603
+
604
+ | 参数 | 类型 | 默认值 | 说明 |
605
+ |------|------|--------|------|
606
+ | `format` | string | 'png' | 图片格式:'png', 'jpeg', 'webp' |
607
+ | `quality` | number | 0.92 | 图片质量(仅 jpeg/webp 有效,0-1) |
608
+ | `type` | string | 'dataURL' | 返回类型:'dataURL' 或 'blob' |
609
+ | `background` | string | '#ffffff' | 背景颜色 |
610
+
579
611
  ### 🛠️ 开发
580
612
 
581
613
  ```bash
@@ -1127,6 +1159,38 @@ editor.tweenAnimate(objectId, { x: 400 }, {
1127
1159
  editor.stopAllAnimations();
1128
1160
  ```
1129
1161
 
1162
+ ### 🖼️ Image Export API
1163
+
1164
+ Use `getImageData()` method to get canvas image data (base64 or Blob) without triggering download:
1165
+
1166
+ ```javascript
1167
+ const editor = document.querySelector('canvas-drawing-editor');
1168
+
1169
+ // Get base64 format (default)
1170
+ const dataURL = await editor.getImageData();
1171
+ console.log(dataURL); // data:image/png;base64,...
1172
+
1173
+ // Get Blob format (suitable for server upload)
1174
+ const blob = await editor.getImageData({
1175
+ type: 'blob',
1176
+ format: 'png', // 'png' | 'jpeg' | 'webp'
1177
+ quality: 0.92, // jpeg/webp quality (0-1)
1178
+ background: '#ffffff' // Background color
1179
+ });
1180
+
1181
+ // Upload to server example
1182
+ const formData = new FormData();
1183
+ formData.append('image', blob, 'canvas.png');
1184
+ await fetch('/api/upload', { method: 'POST', body: formData });
1185
+ ```
1186
+
1187
+ | Parameter | Type | Default | Description |
1188
+ |-----------|------|---------|-------------|
1189
+ | `format` | string | 'png' | Image format: 'png', 'jpeg', 'webp' |
1190
+ | `quality` | number | 0.92 | Image quality (jpeg/webp only, 0-1) |
1191
+ | `type` | string | 'dataURL' | Return type: 'dataURL' or 'blob' |
1192
+ | `background` | string | '#ffffff' | Background color |
1193
+
1130
1194
  ### 🛠️ Development
1131
1195
 
1132
1196
  ```bash
@@ -1240,6 +1240,12 @@ class H extends HTMLElement {
1240
1240
  clearSelection() {
1241
1241
  this.selectedIds.clear(), this.transformOrigin = null;
1242
1242
  }
1243
+ // 更新对象颜色(支持 GROUP 对象的子对象颜色更新)
1244
+ updateObjectColor(t, i) {
1245
+ t.color = i, t.type === "GROUP" && t.children.forEach((s) => {
1246
+ s.color = i, "fillColor" in s && s.fillColor && (s.fillColor = i), s.type !== "TEXT" && s.type !== "RICH_TEXT" && (s.color = i, "fillColor" in s && (s.fillColor = i));
1247
+ }), "fillColor" in t && t.fillColor && (t.fillColor = i);
1248
+ }
1243
1249
  // 派发变化事件
1244
1250
  dispatchChangeEvent() {
1245
1251
  this.dispatchEvent(new CustomEvent("editor-change", {
@@ -1563,7 +1569,7 @@ class H extends HTMLElement {
1563
1569
  else if (this.tool === "RICH_TEXT")
1564
1570
  this.showRichTextEditor(i, e), this.isDragging = !1;
1565
1571
  else {
1566
- this.saveHistory();
1572
+ this.saveHistory(), this.hideEmptyHint();
1567
1573
  const o = this.generateId();
1568
1574
  if (this.tool === "RECTANGLE")
1569
1575
  this.currentObject = { id: o, type: "RECTANGLE", x: i, y: e, width: 0, height: 0, color: this.color, lineWidth: this.lineWidth, lineStyle: this.lineStyle, fillMode: this.fillMode };
@@ -1604,7 +1610,7 @@ class H extends HTMLElement {
1604
1610
  this.finishSmoothCurve();
1605
1611
  return;
1606
1612
  }
1607
- this.smoothCurvePoints.push({ x: t, y: i }), this.isSmoothCurveDrawing = !0, this.renderCanvas();
1613
+ this.smoothCurvePoints.push({ x: t, y: i }), this.isSmoothCurveDrawing = !0, this.hideEmptyHint(), this.renderCanvas();
1608
1614
  }
1609
1615
  // 完成平滑曲线绘制
1610
1616
  finishSmoothCurve() {
@@ -1652,7 +1658,7 @@ class H extends HTMLElement {
1652
1658
  y: i,
1653
1659
  type: "smooth"
1654
1660
  };
1655
- this.bezierPoints.push(s), this.isBezierDrawing = !0, this.bezierDraggingPoint = this.bezierPoints.length - 1, this.bezierDraggingHandle = "cp2", this.renderCanvas();
1661
+ this.bezierPoints.push(s), this.isBezierDrawing = !0, this.bezierDraggingPoint = this.bezierPoints.length - 1, this.bezierDraggingHandle = "cp2", this.hideEmptyHint(), this.renderCanvas();
1656
1662
  }
1657
1663
  // 完成贝塞尔曲线路径
1658
1664
  finishBezierPath(t) {
@@ -1838,9 +1844,9 @@ class H extends HTMLElement {
1838
1844
  m.fontSize = Math.max(8, Math.round(b.fontSize * Math.min(d, u)));
1839
1845
  else if (m.type === "RICH_TEXT") {
1840
1846
  const k = b;
1841
- m.fontSize = Math.max(8, Math.round(k.fontSize * Math.min(d, u))), m.segments = k.segments.map((z) => ({
1842
- ...z,
1843
- fontSize: z.fontSize ? Math.max(8, Math.round(z.fontSize * Math.min(d, u))) : void 0
1847
+ m.fontSize = Math.max(8, Math.round(k.fontSize * Math.min(d, u))), m.segments = k.segments.map((E) => ({
1848
+ ...E,
1849
+ fontSize: E.fontSize ? Math.max(8, Math.round(E.fontSize * Math.min(d, u))) : void 0
1844
1850
  }));
1845
1851
  } else m.type === "LINE" || m.type === "ARROW" ? (m.x2 = b.x2 * d, m.y2 = b.y2 * u) : m.type === "PATH" && (m.points = b.points.map((k) => ({
1846
1852
  x: k.x * d,
@@ -1972,12 +1978,38 @@ class H extends HTMLElement {
1972
1978
  })
1973
1979
  };
1974
1980
  }
1975
- // 导出画布为 PNG 图片
1981
+ // 导出画布为 PNG 图片(下载文件)
1976
1982
  exportPNG(t = "canvas-export.png") {
1977
1983
  if (!this.canvas) return;
1978
1984
  const i = document.createElement("a");
1979
1985
  i.download = t, i.href = this.canvas.toDataURL("image/png"), i.click();
1980
1986
  }
1987
+ // 获取画布图片数据(返回 base64 或 Blob)
1988
+ getImageData(t = {}) {
1989
+ return new Promise((i, e) => {
1990
+ if (!this.canvas) {
1991
+ e(new Error("Canvas not initialized"));
1992
+ return;
1993
+ }
1994
+ const {
1995
+ format: s = "png",
1996
+ quality: o = 0.92,
1997
+ type: n = "dataURL",
1998
+ background: r = "#ffffff"
1999
+ } = t, h = document.createElement("canvas");
2000
+ h.width = this.canvas.width, h.height = this.canvas.height;
2001
+ const a = h.getContext("2d");
2002
+ a.fillStyle = r, a.fillRect(0, 0, h.width, h.height), a.translate(this.panOffset.x, this.panOffset.y), a.scale(this.scale, this.scale), this.objects.forEach((c) => this.drawObject(a, c));
2003
+ const l = `image/${s}`;
2004
+ n === "blob" ? h.toBlob(
2005
+ (c) => {
2006
+ c ? i(c) : e(new Error("Failed to create blob"));
2007
+ },
2008
+ l,
2009
+ o
2010
+ ) : i(h.toDataURL(l, o));
2011
+ });
2012
+ }
1981
2013
  // ========== Tween动画系统方法 ==========
1982
2014
  // 创建动画 (使用 tweenAnimate 避免与 HTMLElement.animate 冲突)
1983
2015
  tweenAnimate(t, i, e = {}) {
@@ -2805,7 +2837,7 @@ class H extends HTMLElement {
2805
2837
  }
2806
2838
  // 更新 UI
2807
2839
  updateUI() {
2808
- var n, r, h, a;
2840
+ var o, n, r, h;
2809
2841
  const t = this.shadow.querySelector(".selection-info");
2810
2842
  if (t)
2811
2843
  if (this.selectedIds.size > 0) {
@@ -2817,48 +2849,48 @@ class H extends HTMLElement {
2817
2849
  </svg>
2818
2850
  </button>
2819
2851
  `, t.classList.add("visible");
2820
- const l = t.querySelector(".delete-btn");
2821
- l && l.addEventListener("click", () => this.deleteSelected());
2852
+ const a = t.querySelector(".delete-btn");
2853
+ a && a.addEventListener("click", () => this.deleteSelected());
2822
2854
  } else if (this.selectedId) {
2823
- const l = this.objects.find((c) => c.id === this.selectedId);
2824
- if (l) {
2825
- const p = {
2855
+ const a = this.objects.find((l) => l.id === this.selectedId);
2856
+ if (a) {
2857
+ const c = {
2826
2858
  RECTANGLE: "矩形",
2827
2859
  CIRCLE: "圆形",
2828
2860
  PATH: "画笔",
2829
2861
  TEXT: "文本",
2830
2862
  IMAGE: "图片"
2831
- }[l.type] || l.type;
2863
+ }[a.type] || a.type;
2832
2864
  t.innerHTML = `
2833
- <span class="selection-label">${this.t("selected")}: ${p}</span>
2865
+ <span class="selection-label">${this.t("selected")}: ${c}</span>
2834
2866
  <button class="delete-btn" title="${this.t("delete")}">
2835
2867
  <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
2836
2868
  <path d="M3 6h18M19 6v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6m3 0V4a2 2 0 012-2h4a2 2 0 012 2v2"/>
2837
2869
  </svg>
2838
2870
  </button>
2839
2871
  `, t.classList.add("visible");
2840
- const d = t.querySelector(".delete-btn");
2841
- d && d.addEventListener("click", () => this.deleteSelected());
2872
+ const p = t.querySelector(".delete-btn");
2873
+ p && p.addEventListener("click", () => this.deleteSelected());
2842
2874
  }
2843
2875
  } else
2844
2876
  t.classList.remove("visible"), t.innerHTML = "";
2845
2877
  const i = this.shadow.querySelector(".filter-controls");
2846
2878
  if (i) {
2847
- const c = this.getSelectedObjects().find((p) => p.type === "IMAGE");
2848
- if (c) {
2879
+ const l = this.getSelectedObjects().find((c) => c.type === "IMAGE");
2880
+ if (l) {
2849
2881
  i.style.display = "flex";
2850
- const p = i.querySelector(".filter-brightness"), d = i.querySelector(".filter-contrast");
2851
- if (p) {
2852
- const u = ((n = c.filters) == null ? void 0 : n.brightness) ?? 100;
2853
- p.value = String(u);
2854
- const g = (r = p.parentElement) == null ? void 0 : r.querySelector(".filter-value");
2855
- g && (g.textContent = `${u}%`);
2882
+ const c = i.querySelector(".filter-brightness"), p = i.querySelector(".filter-contrast");
2883
+ if (c) {
2884
+ const d = ((o = l.filters) == null ? void 0 : o.brightness) ?? 100;
2885
+ c.value = String(d);
2886
+ const u = (n = c.parentElement) == null ? void 0 : n.querySelector(".filter-value");
2887
+ u && (u.textContent = `${d}%`);
2856
2888
  }
2857
- if (d) {
2858
- const u = ((h = c.filters) == null ? void 0 : h.contrast) ?? 100;
2859
- d.value = String(u);
2860
- const g = (a = d.parentElement) == null ? void 0 : a.querySelector(".filter-value");
2861
- g && (g.textContent = `${u}%`);
2889
+ if (p) {
2890
+ const d = ((r = l.filters) == null ? void 0 : r.contrast) ?? 100;
2891
+ p.value = String(d);
2892
+ const u = (h = p.parentElement) == null ? void 0 : h.querySelector(".filter-value");
2893
+ u && (u.textContent = `${d}%`);
2862
2894
  }
2863
2895
  } else
2864
2896
  i.style.display = "none";
@@ -2866,9 +2898,20 @@ class H extends HTMLElement {
2866
2898
  const e = this.shadow.querySelector(".undo-btn");
2867
2899
  e && (e.disabled = this.history.length === 0);
2868
2900
  const s = this.shadow.querySelector(".redo-btn");
2869
- s && (s.disabled = this.redoHistory.length === 0);
2870
- const o = this.shadow.querySelector(".empty-hint");
2871
- o && (o.style.display = this.objects.length === 0 ? "flex" : "none");
2901
+ s && (s.disabled = this.redoHistory.length === 0), this.updateEmptyHint();
2902
+ }
2903
+ // 更新空画布提示显示状态
2904
+ updateEmptyHint() {
2905
+ const t = this.shadow.querySelector(".empty-hint");
2906
+ if (t) {
2907
+ const i = this.objects.length > 0 || this.currentObject !== null;
2908
+ t.style.display = i ? "none" : "flex";
2909
+ }
2910
+ }
2911
+ // 立即隐藏空画布提示(用于开始绘制时)
2912
+ hideEmptyHint() {
2913
+ const t = this.shadow.querySelector(".empty-hint");
2914
+ t && (t.style.display = "none");
2872
2915
  }
2873
2916
  // ========== 热区功能相关方法 ==========
2874
2917
  // 绑定热区相关事件
@@ -3588,10 +3631,10 @@ class H extends HTMLElement {
3588
3631
  const w = S.target.value;
3589
3632
  if (this.color = w, this.selectedId) {
3590
3633
  const v = this.objects.find((C) => C.id === this.selectedId);
3591
- v && (this.saveHistory(), v.color = w, this.renderCanvas(), this.dispatchChangeEvent());
3634
+ v && (this.saveHistory(), this.updateObjectColor(v, w), this.renderCanvas(), this.dispatchChangeEvent());
3592
3635
  } else this.selectedIds.size > 0 && (this.saveHistory(), this.selectedIds.forEach((v) => {
3593
3636
  const C = this.objects.find((x) => x.id === v);
3594
- C && (C.color = w);
3637
+ C && this.updateObjectColor(C, w);
3595
3638
  }), this.renderCanvas(), this.dispatchChangeEvent());
3596
3639
  };
3597
3640
  g.addEventListener("input", y), g.addEventListener("change", y);
@@ -3601,15 +3644,15 @@ class H extends HTMLElement {
3601
3644
  });
3602
3645
  const b = this.shadow.querySelector(".zoom-in-btn"), m = this.shadow.querySelector(".zoom-out-btn"), k = this.shadow.querySelector(".zoom-text");
3603
3646
  b && b.addEventListener("click", () => this.zoomIn()), m && m.addEventListener("click", () => this.zoomOut()), k && k.addEventListener("click", () => this.resetZoom());
3604
- const z = this.shadow.querySelector(".save-json-btn"), O = this.shadow.querySelector(".load-json-input"), R = this.shadow.querySelector(".export-png-btn");
3605
- z && z.addEventListener("click", () => this.saveJson()), O && O.addEventListener("change", (y) => this.loadJson(y)), R && R.addEventListener("click", () => this.exportPng());
3606
- const L = this.shadow.querySelector(".clear-canvas-btn"), E = this.shadow.querySelector(".clear-confirm-popup"), A = this.shadow.querySelector(".clear-confirm-yes"), $ = this.shadow.querySelector(".clear-confirm-no");
3607
- L && E && L.addEventListener("click", () => {
3608
- E.classList.toggle("show");
3609
- }), A && E && A.addEventListener("click", () => {
3610
- this.clearCanvas(), E.classList.remove("show");
3611
- }), $ && E && $.addEventListener("click", () => {
3612
- E.classList.remove("show");
3647
+ const E = this.shadow.querySelector(".save-json-btn"), O = this.shadow.querySelector(".load-json-input"), R = this.shadow.querySelector(".export-png-btn");
3648
+ E && E.addEventListener("click", () => this.saveJson()), O && O.addEventListener("change", (y) => this.loadJson(y)), R && R.addEventListener("click", () => this.exportPng());
3649
+ const L = this.shadow.querySelector(".clear-canvas-btn"), z = this.shadow.querySelector(".clear-confirm-popup"), A = this.shadow.querySelector(".clear-confirm-yes"), $ = this.shadow.querySelector(".clear-confirm-no");
3650
+ L && z && L.addEventListener("click", () => {
3651
+ z.classList.toggle("show");
3652
+ }), A && z && A.addEventListener("click", () => {
3653
+ this.clearCanvas(), z.classList.remove("show");
3654
+ }), $ && z && $.addEventListener("click", () => {
3655
+ z.classList.remove("show");
3613
3656
  }), this.textInput && (this.textInput.addEventListener("keydown", (y) => {
3614
3657
  y.key === "Enter" ? (y.preventDefault(), this.submitText()) : y.key === "Escape" && this.hideTextInput();
3615
3658
  }), this.textInput.addEventListener("blur", () => {
@@ -3849,7 +3892,7 @@ class H extends HTMLElement {
3849
3892
  // 添加形状到画布
3850
3893
  addShapeToCanvas(t) {
3851
3894
  const i = (this.canvas.width / 2 - this.panOffset.x) / this.scale, e = (this.canvas.height / 2 - this.panOffset.y) / this.scale, s = t.width || 100, o = t.height || 100;
3852
- this.saveHistory();
3895
+ this.saveHistory(), this.hideEmptyHint();
3853
3896
  let n;
3854
3897
  const r = t.fillMode === "stroke" ? t.strokeColor || t.fillColor || "#000000" : t.fillColor || t.strokeColor || "#000000";
3855
3898
  switch (t.type) {