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((
|
|
1842
|
-
...
|
|
1843
|
-
fontSize:
|
|
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
|
|
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
|
|
2821
|
-
|
|
2852
|
+
const a = t.querySelector(".delete-btn");
|
|
2853
|
+
a && a.addEventListener("click", () => this.deleteSelected());
|
|
2822
2854
|
} else if (this.selectedId) {
|
|
2823
|
-
const
|
|
2824
|
-
if (
|
|
2825
|
-
const
|
|
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
|
-
}[
|
|
2863
|
+
}[a.type] || a.type;
|
|
2832
2864
|
t.innerHTML = `
|
|
2833
|
-
<span class="selection-label">${this.t("selected")}: ${
|
|
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
|
|
2841
|
-
|
|
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
|
|
2848
|
-
if (
|
|
2879
|
+
const l = this.getSelectedObjects().find((c) => c.type === "IMAGE");
|
|
2880
|
+
if (l) {
|
|
2849
2881
|
i.style.display = "flex";
|
|
2850
|
-
const
|
|
2851
|
-
if (
|
|
2852
|
-
const
|
|
2853
|
-
|
|
2854
|
-
const
|
|
2855
|
-
|
|
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 (
|
|
2858
|
-
const
|
|
2859
|
-
|
|
2860
|
-
const
|
|
2861
|
-
|
|
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
|
-
|
|
2871
|
-
|
|
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
|
|
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
|
|
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
|
|
3605
|
-
|
|
3606
|
-
const L = this.shadow.querySelector(".clear-canvas-btn"),
|
|
3607
|
-
L &&
|
|
3608
|
-
|
|
3609
|
-
}), A &&
|
|
3610
|
-
this.clearCanvas(),
|
|
3611
|
-
}), $ &&
|
|
3612
|
-
|
|
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) {
|