diff --git a/package-lock.json b/package-lock.json index f326d65e..c21e206a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -30,6 +30,7 @@ "vue": "^3.2.13", "vue-class-component": "^8.0.0-0", "vue-cropper": "^1.0.5", + "vue-draggable-plus": "^0.6.0", "vue-i18n": "^9.6.1", "vue-router": "^4.0.3", "vuedraggable": "^4.1.0", @@ -99,9 +100,9 @@ } }, "node_modules/@ant-design/icons-svg": { - "version": "4.2.1", - "resolved": "https://registry.npmmirror.com/@ant-design/icons-svg/-/icons-svg-4.2.1.tgz", - "integrity": "sha512-EB0iwlKDGpG93hW8f85CTJTs4SvMX7tt5ceupvhALp1IF44SeUFOMhKUOYqpsoYWQKAOuTRDMqn75rEaKDp0Xw==" + "version": "4.4.2", + "resolved": "https://registry.npmmirror.com/@ant-design/icons-svg/-/icons-svg-4.4.2.tgz", + "integrity": "sha512-vHbT+zJEVzllwP+CM+ul7reTEfBR0vgxFe7+lREAsAA7YGsYpboiq2sQNeQeRvh09GfQgs/GyFEvZpJ9cLXpXA==" }, "node_modules/@ant-design/icons-vue": { "version": "6.1.0", @@ -1764,9 +1765,9 @@ } }, "node_modules/@ctrl/tinycolor": { - "version": "3.4.1", - "resolved": "https://registry.npmmirror.com/@ctrl/tinycolor/-/tinycolor-3.4.1.tgz", - "integrity": "sha512-ej5oVy6lykXsvieQtqZxCOaLT+xD4+QNarq78cIYISHmZXshCvROLudpQN3lfL8G0NL7plMSSK+zlyvCaIJ4Iw==", + "version": "3.6.1", + "resolved": "https://registry.npmmirror.com/@ctrl/tinycolor/-/tinycolor-3.6.1.tgz", + "integrity": "sha512-SITSV6aIXsuVNV3f3O0f2n/cgyEDWoSqtZMYiAmcsYHydcKrOz3gUxB/iXd/Qf08+IZX4KpgNbvUdMBmWz+kcA==", "engines": { "node": ">=10" } @@ -2432,6 +2433,11 @@ "@types/node": "*" } }, + "node_modules/@types/sortablejs": { + "version": "1.15.8", + "resolved": "https://registry.npmmirror.com/@types/sortablejs/-/sortablejs-1.15.8.tgz", + "integrity": "sha512-b79830lW+RZfwaztgs1aVPgbasJ8e7AXtZYHTELNXZPsERt4ymJdjV4OccDbHQAvHrCcFpbF78jkm0R6h/pZVg==" + }, "node_modules/@types/stats.js": { "version": "0.17.3", "resolved": "https://registry.npmmirror.com/@types/stats.js/-/stats.js-0.17.3.tgz", @@ -3816,9 +3822,9 @@ } }, "node_modules/ant-design-vue": { - "version": "3.2.12", - "resolved": "https://registry.npmmirror.com/ant-design-vue/-/ant-design-vue-3.2.12.tgz", - "integrity": "sha512-CPsoWJ3t+sqq/EPINPXb4fC5/9iKkUdYOfK9M9kLKbXlRN3MAoVwWUbaFnUqc+ngtbEpn/d69hTF/Eh7MeWMhQ==", + "version": "3.2.20", + "resolved": "https://registry.npmmirror.com/ant-design-vue/-/ant-design-vue-3.2.20.tgz", + "integrity": "sha512-YWpMfGaGoRastIXEYfCoJiaRiDHk4chqtYhlKQM5GqPt6NfvrM1Vg2e60yHtjxlZjed91wCMm0rAmyUr7Hwzdg==", "dependencies": { "@ant-design/colors": "^6.0.0", "@ant-design/icons-vue": "^6.1.0", @@ -3841,6 +3847,10 @@ "engines": { "node": ">=12.22.0" }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/ant-design-vue" + }, "peerDependencies": { "vue": ">=3.2.0" } @@ -4672,9 +4682,9 @@ "dev": true }, "node_modules/compute-scroll-into-view": { - "version": "1.0.17", - "resolved": "https://registry.npmmirror.com/compute-scroll-into-view/-/compute-scroll-into-view-1.0.17.tgz", - "integrity": "sha512-j4dx+Fb0URmzbwwMUrhqWM2BEWHdFGx+qZ9qqASHRPqvTYdqvWnHg0H1hIbcyLnvgnoNAVMlwkepyqM3DaIFUg==" + "version": "1.0.20", + "resolved": "https://registry.npmmirror.com/compute-scroll-into-view/-/compute-scroll-into-view-1.0.20.tgz", + "integrity": "sha512-UCB0ioiyj8CRjtrvaceBLqqhZCVP+1B8+NWQhmdsm0VXOJtobBCf1dBQmebCCo34qZmUwZfIH2MZLqNHazrfjg==" }, "node_modules/concat-map": { "version": "0.0.1", @@ -5413,9 +5423,9 @@ } }, "node_modules/dom-align": { - "version": "1.12.3", - "resolved": "https://registry.npmmirror.com/dom-align/-/dom-align-1.12.3.tgz", - "integrity": "sha512-Gj9hZN3a07cbR6zviMUBOMPdWxYhbMI+x+WS0NAIu2zFZmbK8ys9R79g+iG9qLnlCwpFoaB+fKy8Pdv470GsPA==" + "version": "1.12.4", + "resolved": "https://registry.npmmirror.com/dom-align/-/dom-align-1.12.4.tgz", + "integrity": "sha512-R8LUSEay/68zE5c8/3BDxiTEvgb4xZTF0RKmAHfiEVN3klfIpXfi2/QCoiWPccVQ0J/ZGdz9OjzL4uJEP/MRAw==" }, "node_modules/dom-converter": { "version": "0.2.0", @@ -8518,9 +8528,9 @@ } }, "node_modules/nanopop": { - "version": "2.2.0", - "resolved": "https://registry.npmmirror.com/nanopop/-/nanopop-2.2.0.tgz", - "integrity": "sha512-E9JaHcxh3ere8/BEZHAcnuD10RluTSPyTToBvoFWS9/7DcCx6gyKjbn7M7Bx7E1veCxCuY1iO6h4+gdAf1j73Q==" + "version": "2.4.2", + "resolved": "https://registry.npmmirror.com/nanopop/-/nanopop-2.4.2.tgz", + "integrity": "sha512-NzOgmMQ+elxxHeIha+OG/Pv3Oc3p4RU2aBhwWwAqDpXrdTbtRylbRLQztLy8dMMwfl6pclznBdfUhccEn9ZIzw==" }, "node_modules/natural-compare": { "version": "1.4.0", @@ -10217,11 +10227,11 @@ } }, "node_modules/scroll-into-view-if-needed": { - "version": "2.2.29", - "resolved": "https://registry.npmmirror.com/scroll-into-view-if-needed/-/scroll-into-view-if-needed-2.2.29.tgz", - "integrity": "sha512-hxpAR6AN+Gh53AdAimHM6C8oTN1ppwVZITihix+WqalywBeFcQ6LdQP5ABNl26nX8GTEL7VT+b8lKpdqq65wXg==", + "version": "2.2.31", + "resolved": "https://registry.npmmirror.com/scroll-into-view-if-needed/-/scroll-into-view-if-needed-2.2.31.tgz", + "integrity": "sha512-dGCXy99wZQivjmjIqihaBQNjryrz5rueJY7eHfTdyWEiR4ttYpsajb14rn9s5d4DY4EcY6+4+U/maARBXJedkA==", "dependencies": { - "compute-scroll-into-view": "^1.0.17" + "compute-scroll-into-view": "^1.0.20" } }, "node_modules/select-hose": { @@ -11534,6 +11544,22 @@ "resolved": "https://registry.npmmirror.com/vue-cropper/-/vue-cropper-1.0.5.tgz", "integrity": "sha512-D4XXdqWmMWRLOIV9LIh7/mkH6OBOMQDFbRjwntkxmAtxOtwpC9U5ZZ6lSXw5F5cbd4g8znDjk6MuCwIL+fZSrA==" }, + "node_modules/vue-draggable-plus": { + "version": "0.6.0", + "resolved": "https://registry.npmmirror.com/vue-draggable-plus/-/vue-draggable-plus-0.6.0.tgz", + "integrity": "sha512-G5TSfHrt9tX9EjdG49InoFJbt2NYk0h3kgjgKxkFWr3ulIUays0oFObr5KZ8qzD4+QnhtALiRwIqY6qul4egqw==", + "dependencies": { + "@types/sortablejs": "^1.15.8" + }, + "peerDependencies": { + "@types/sortablejs": "^1.15.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } + }, "node_modules/vue-eslint-parser": { "version": "8.3.0", "resolved": "https://registry.npmmirror.com/vue-eslint-parser/-/vue-eslint-parser-8.3.0.tgz", @@ -12572,9 +12598,9 @@ } }, "@ant-design/icons-svg": { - "version": "4.2.1", - "resolved": "https://registry.npmmirror.com/@ant-design/icons-svg/-/icons-svg-4.2.1.tgz", - "integrity": "sha512-EB0iwlKDGpG93hW8f85CTJTs4SvMX7tt5ceupvhALp1IF44SeUFOMhKUOYqpsoYWQKAOuTRDMqn75rEaKDp0Xw==" + "version": "4.4.2", + "resolved": "https://registry.npmmirror.com/@ant-design/icons-svg/-/icons-svg-4.4.2.tgz", + "integrity": "sha512-vHbT+zJEVzllwP+CM+ul7reTEfBR0vgxFe7+lREAsAA7YGsYpboiq2sQNeQeRvh09GfQgs/GyFEvZpJ9cLXpXA==" }, "@ant-design/icons-vue": { "version": "6.1.0", @@ -13729,9 +13755,9 @@ } }, "@ctrl/tinycolor": { - "version": "3.4.1", - "resolved": "https://registry.npmmirror.com/@ctrl/tinycolor/-/tinycolor-3.4.1.tgz", - "integrity": "sha512-ej5oVy6lykXsvieQtqZxCOaLT+xD4+QNarq78cIYISHmZXshCvROLudpQN3lfL8G0NL7plMSSK+zlyvCaIJ4Iw==" + "version": "3.6.1", + "resolved": "https://registry.npmmirror.com/@ctrl/tinycolor/-/tinycolor-3.6.1.tgz", + "integrity": "sha512-SITSV6aIXsuVNV3f3O0f2n/cgyEDWoSqtZMYiAmcsYHydcKrOz3gUxB/iXd/Qf08+IZX4KpgNbvUdMBmWz+kcA==" }, "@element-plus/icons-vue": { "version": "2.1.0", @@ -14301,6 +14327,11 @@ "@types/node": "*" } }, + "@types/sortablejs": { + "version": "1.15.8", + "resolved": "https://registry.npmmirror.com/@types/sortablejs/-/sortablejs-1.15.8.tgz", + "integrity": "sha512-b79830lW+RZfwaztgs1aVPgbasJ8e7AXtZYHTELNXZPsERt4ymJdjV4OccDbHQAvHrCcFpbF78jkm0R6h/pZVg==" + }, "@types/stats.js": { "version": "0.17.3", "resolved": "https://registry.npmmirror.com/@types/stats.js/-/stats.js-0.17.3.tgz", @@ -15383,9 +15414,9 @@ } }, "ant-design-vue": { - "version": "3.2.12", - "resolved": "https://registry.npmmirror.com/ant-design-vue/-/ant-design-vue-3.2.12.tgz", - "integrity": "sha512-CPsoWJ3t+sqq/EPINPXb4fC5/9iKkUdYOfK9M9kLKbXlRN3MAoVwWUbaFnUqc+ngtbEpn/d69hTF/Eh7MeWMhQ==", + "version": "3.2.20", + "resolved": "https://registry.npmmirror.com/ant-design-vue/-/ant-design-vue-3.2.20.tgz", + "integrity": "sha512-YWpMfGaGoRastIXEYfCoJiaRiDHk4chqtYhlKQM5GqPt6NfvrM1Vg2e60yHtjxlZjed91wCMm0rAmyUr7Hwzdg==", "requires": { "@ant-design/colors": "^6.0.0", "@ant-design/icons-vue": "^6.1.0", @@ -16089,9 +16120,9 @@ } }, "compute-scroll-into-view": { - "version": "1.0.17", - "resolved": "https://registry.npmmirror.com/compute-scroll-into-view/-/compute-scroll-into-view-1.0.17.tgz", - "integrity": "sha512-j4dx+Fb0URmzbwwMUrhqWM2BEWHdFGx+qZ9qqASHRPqvTYdqvWnHg0H1hIbcyLnvgnoNAVMlwkepyqM3DaIFUg==" + "version": "1.0.20", + "resolved": "https://registry.npmmirror.com/compute-scroll-into-view/-/compute-scroll-into-view-1.0.20.tgz", + "integrity": "sha512-UCB0ioiyj8CRjtrvaceBLqqhZCVP+1B8+NWQhmdsm0VXOJtobBCf1dBQmebCCo34qZmUwZfIH2MZLqNHazrfjg==" }, "concat-map": { "version": "0.0.1", @@ -16660,9 +16691,9 @@ } }, "dom-align": { - "version": "1.12.3", - "resolved": "https://registry.npmmirror.com/dom-align/-/dom-align-1.12.3.tgz", - "integrity": "sha512-Gj9hZN3a07cbR6zviMUBOMPdWxYhbMI+x+WS0NAIu2zFZmbK8ys9R79g+iG9qLnlCwpFoaB+fKy8Pdv470GsPA==" + "version": "1.12.4", + "resolved": "https://registry.npmmirror.com/dom-align/-/dom-align-1.12.4.tgz", + "integrity": "sha512-R8LUSEay/68zE5c8/3BDxiTEvgb4xZTF0RKmAHfiEVN3klfIpXfi2/QCoiWPccVQ0J/ZGdz9OjzL4uJEP/MRAw==" }, "dom-converter": { "version": "0.2.0", @@ -19144,9 +19175,9 @@ "integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==" }, "nanopop": { - "version": "2.2.0", - "resolved": "https://registry.npmmirror.com/nanopop/-/nanopop-2.2.0.tgz", - "integrity": "sha512-E9JaHcxh3ere8/BEZHAcnuD10RluTSPyTToBvoFWS9/7DcCx6gyKjbn7M7Bx7E1veCxCuY1iO6h4+gdAf1j73Q==" + "version": "2.4.2", + "resolved": "https://registry.npmmirror.com/nanopop/-/nanopop-2.4.2.tgz", + "integrity": "sha512-NzOgmMQ+elxxHeIha+OG/Pv3Oc3p4RU2aBhwWwAqDpXrdTbtRylbRLQztLy8dMMwfl6pclznBdfUhccEn9ZIzw==" }, "natural-compare": { "version": "1.4.0", @@ -20424,11 +20455,11 @@ } }, "scroll-into-view-if-needed": { - "version": "2.2.29", - "resolved": "https://registry.npmmirror.com/scroll-into-view-if-needed/-/scroll-into-view-if-needed-2.2.29.tgz", - "integrity": "sha512-hxpAR6AN+Gh53AdAimHM6C8oTN1ppwVZITihix+WqalywBeFcQ6LdQP5ABNl26nX8GTEL7VT+b8lKpdqq65wXg==", + "version": "2.2.31", + "resolved": "https://registry.npmmirror.com/scroll-into-view-if-needed/-/scroll-into-view-if-needed-2.2.31.tgz", + "integrity": "sha512-dGCXy99wZQivjmjIqihaBQNjryrz5rueJY7eHfTdyWEiR4ttYpsajb14rn9s5d4DY4EcY6+4+U/maARBXJedkA==", "requires": { - "compute-scroll-into-view": "^1.0.17" + "compute-scroll-into-view": "^1.0.20" } }, "select-hose": { @@ -21496,6 +21527,14 @@ "resolved": "https://registry.npmmirror.com/vue-cropper/-/vue-cropper-1.0.5.tgz", "integrity": "sha512-D4XXdqWmMWRLOIV9LIh7/mkH6OBOMQDFbRjwntkxmAtxOtwpC9U5ZZ6lSXw5F5cbd4g8znDjk6MuCwIL+fZSrA==" }, + "vue-draggable-plus": { + "version": "0.6.0", + "resolved": "https://registry.npmmirror.com/vue-draggable-plus/-/vue-draggable-plus-0.6.0.tgz", + "integrity": "sha512-G5TSfHrt9tX9EjdG49InoFJbt2NYk0h3kgjgKxkFWr3ulIUays0oFObr5KZ8qzD4+QnhtALiRwIqY6qul4egqw==", + "requires": { + "@types/sortablejs": "^1.15.8" + } + }, "vue-eslint-parser": { "version": "8.3.0", "resolved": "https://registry.npmmirror.com/vue-eslint-parser/-/vue-eslint-parser-8.3.0.tgz", diff --git a/package.json b/package.json index 2aa88001..a8f4da6e 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "vue": "^3.2.13", "vue-class-component": "^8.0.0-0", "vue-cropper": "^1.0.5", + "vue-draggable-plus": "^0.6.0", "vue-i18n": "^9.6.1", "vue-router": "^4.0.3", "vuedraggable": "^4.1.0", diff --git a/src/assets/icons/CBrush.svg b/src/assets/icons/CBrush.svg new file mode 100644 index 00000000..2d1d0fd7 --- /dev/null +++ b/src/assets/icons/CBrush.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/CClose.svg b/src/assets/icons/CClose.svg new file mode 100644 index 00000000..feac3e97 --- /dev/null +++ b/src/assets/icons/CClose.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/CDelete.svg b/src/assets/icons/CDelete.svg new file mode 100644 index 00000000..cd8324c9 --- /dev/null +++ b/src/assets/icons/CDelete.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/CEllipse.svg b/src/assets/icons/CEllipse.svg new file mode 100644 index 00000000..153d50a9 --- /dev/null +++ b/src/assets/icons/CEllipse.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/CEraser.svg b/src/assets/icons/CEraser.svg new file mode 100644 index 00000000..34f086ad --- /dev/null +++ b/src/assets/icons/CEraser.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/CEye.svg b/src/assets/icons/CEye.svg new file mode 100644 index 00000000..5efc0b19 --- /dev/null +++ b/src/assets/icons/CEye.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/CFont.svg b/src/assets/icons/CFont.svg new file mode 100644 index 00000000..39c4c79a --- /dev/null +++ b/src/assets/icons/CFont.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/CFree.svg b/src/assets/icons/CFree.svg new file mode 100644 index 00000000..9f3661b2 --- /dev/null +++ b/src/assets/icons/CFree.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/CHand.svg b/src/assets/icons/CHand.svg new file mode 100644 index 00000000..b46815d7 --- /dev/null +++ b/src/assets/icons/CHand.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/CLasso.svg b/src/assets/icons/CLasso.svg new file mode 100644 index 00000000..544c2726 --- /dev/null +++ b/src/assets/icons/CLasso.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/CLassoArea.svg b/src/assets/icons/CLassoArea.svg new file mode 100644 index 00000000..999fd3cf --- /dev/null +++ b/src/assets/icons/CLassoArea.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/CLayout.svg b/src/assets/icons/CLayout.svg new file mode 100644 index 00000000..c0868e8f --- /dev/null +++ b/src/assets/icons/CLayout.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/CLiquefying.svg b/src/assets/icons/CLiquefying.svg new file mode 100644 index 00000000..0f3e8329 --- /dev/null +++ b/src/assets/icons/CLiquefying.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/CLock.svg b/src/assets/icons/CLock.svg new file mode 100644 index 00000000..94cc397b --- /dev/null +++ b/src/assets/icons/CLock.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/CMiniMap.svg b/src/assets/icons/CMiniMap.svg new file mode 100644 index 00000000..ac994d5b --- /dev/null +++ b/src/assets/icons/CMiniMap.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/CPaste.svg b/src/assets/icons/CPaste.svg new file mode 100644 index 00000000..b5dd7ebd --- /dev/null +++ b/src/assets/icons/CPaste.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/CPicture.svg b/src/assets/icons/CPicture.svg new file mode 100644 index 00000000..3bdc19dc --- /dev/null +++ b/src/assets/icons/CPicture.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/CRectangle.svg b/src/assets/icons/CRectangle.svg new file mode 100644 index 00000000..14dc26ae --- /dev/null +++ b/src/assets/icons/CRectangle.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/CRedo.svg b/src/assets/icons/CRedo.svg new file mode 100644 index 00000000..31558745 --- /dev/null +++ b/src/assets/icons/CRedo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/CSelect.svg b/src/assets/icons/CSelect.svg new file mode 100644 index 00000000..b6972b74 --- /dev/null +++ b/src/assets/icons/CSelect.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/CUnEye.svg b/src/assets/icons/CUnEye.svg new file mode 100644 index 00000000..f28c36dc --- /dev/null +++ b/src/assets/icons/CUnEye.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/CUnLock.svg b/src/assets/icons/CUnLock.svg new file mode 100644 index 00000000..bbc4130f --- /dev/null +++ b/src/assets/icons/CUnLock.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/CUndo.svg b/src/assets/icons/CUndo.svg new file mode 100644 index 00000000..291f9ff4 --- /dev/null +++ b/src/assets/icons/CUndo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/CUpload.svg b/src/assets/icons/CUpload.svg new file mode 100644 index 00000000..6d534def --- /dev/null +++ b/src/assets/icons/CUpload.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/CWave.svg b/src/assets/icons/CWave.svg new file mode 100644 index 00000000..f297038c --- /dev/null +++ b/src/assets/icons/CWave.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/CZoomIn.svg b/src/assets/icons/CZoomIn.svg new file mode 100644 index 00000000..5da5ef02 --- /dev/null +++ b/src/assets/icons/CZoomIn.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/CZoomOut.svg b/src/assets/icons/CZoomOut.svg new file mode 100644 index 00000000..6f9b7870 --- /dev/null +++ b/src/assets/icons/CZoomOut.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/images/homePage/defaultModel.png b/src/assets/images/homePage/defaultModel.png new file mode 100644 index 00000000..9863e532 Binary files /dev/null and b/src/assets/images/homePage/defaultModel.png differ diff --git a/src/assets/redGreenPic/clothing_base_image.png b/src/assets/redGreenPic/clothing_base_image.png new file mode 100644 index 00000000..74cf1c55 Binary files /dev/null and b/src/assets/redGreenPic/clothing_base_image.png differ diff --git a/src/assets/redGreenPic/clothing_mask_image.png b/src/assets/redGreenPic/clothing_mask_image.png new file mode 100644 index 00000000..4c9aebd6 Binary files /dev/null and b/src/assets/redGreenPic/clothing_mask_image.png differ diff --git a/src/assets/style/style.css b/src/assets/style/style.css index b10c4b81..9ba797be 100644 --- a/src/assets/style/style.css +++ b/src/assets/style/style.css @@ -707,7 +707,7 @@ tr > .ant-picker-cell-in-view.ant-picker-cell-range-hover-start:last-child::afte display: flex; align-items: center; margin-right: 5rem; - height: 6rem; + height: 5rem; } .generalModel_state .generalModel_state_item.smail > input { padding: 1rem 2rem !important; @@ -725,7 +725,7 @@ tr > .ant-picker-cell-in-view.ant-picker-cell-range-hover-start:last-child::afte align-items: center; } .generalModel_state .generalModel_state_item > input { - height: 6rem !important; + height: 5rem !important; padding: 1rem !important; border-radius: 1rem; box-sizing: border-box; diff --git a/src/assets/style/style.less b/src/assets/style/style.less index adb2017d..beb52164 100644 --- a/src/assets/style/style.less +++ b/src/assets/style/style.less @@ -780,7 +780,7 @@ tr > .ant-picker-cell-in-view.ant-picker-cell-range-hover-start:last-child::afte display: flex; align-items: center; margin-right: 5rem; - height: 6rem; + height: 5rem; &.smail{ >input{ padding: 1rem 2rem !important; @@ -797,7 +797,7 @@ tr > .ant-picker-cell-in-view.ant-picker-cell-range-hover-start:last-child::afte } } >input{ - height: 6rem !important; + height: 5rem !important; box-sizing: border-box; padding: 1rem !important; border-radius: 1rem; diff --git a/src/assets/texture/texture0.webp b/src/assets/texture/texture0.webp new file mode 100644 index 00000000..19fa7eb6 Binary files /dev/null and b/src/assets/texture/texture0.webp differ diff --git a/src/assets/texture/texture1.webp b/src/assets/texture/texture1.webp new file mode 100644 index 00000000..0c8da7bf Binary files /dev/null and b/src/assets/texture/texture1.webp differ diff --git a/src/assets/texture/texture10.webp b/src/assets/texture/texture10.webp new file mode 100644 index 00000000..a55d80de Binary files /dev/null and b/src/assets/texture/texture10.webp differ diff --git a/src/assets/texture/texture11.webp b/src/assets/texture/texture11.webp new file mode 100644 index 00000000..d5625ad7 Binary files /dev/null and b/src/assets/texture/texture11.webp differ diff --git a/src/assets/texture/texture12.webp b/src/assets/texture/texture12.webp new file mode 100644 index 00000000..3f77d452 Binary files /dev/null and b/src/assets/texture/texture12.webp differ diff --git a/src/assets/texture/texture13.webp b/src/assets/texture/texture13.webp new file mode 100644 index 00000000..7373346a Binary files /dev/null and b/src/assets/texture/texture13.webp differ diff --git a/src/assets/texture/texture14.webp b/src/assets/texture/texture14.webp new file mode 100644 index 00000000..430f95fe Binary files /dev/null and b/src/assets/texture/texture14.webp differ diff --git a/src/assets/texture/texture15.webp b/src/assets/texture/texture15.webp new file mode 100644 index 00000000..c5705063 Binary files /dev/null and b/src/assets/texture/texture15.webp differ diff --git a/src/assets/texture/texture16.webp b/src/assets/texture/texture16.webp new file mode 100644 index 00000000..b2aa625e Binary files /dev/null and b/src/assets/texture/texture16.webp differ diff --git a/src/assets/texture/texture17.webp b/src/assets/texture/texture17.webp new file mode 100644 index 00000000..0d8c44bf Binary files /dev/null and b/src/assets/texture/texture17.webp differ diff --git a/src/assets/texture/texture18.webp b/src/assets/texture/texture18.webp new file mode 100644 index 00000000..ddec1d1a Binary files /dev/null and b/src/assets/texture/texture18.webp differ diff --git a/src/assets/texture/texture19.webp b/src/assets/texture/texture19.webp new file mode 100644 index 00000000..6137cc47 Binary files /dev/null and b/src/assets/texture/texture19.webp differ diff --git a/src/assets/texture/texture2.webp b/src/assets/texture/texture2.webp new file mode 100644 index 00000000..3f77d452 Binary files /dev/null and b/src/assets/texture/texture2.webp differ diff --git a/src/assets/texture/texture20.webp b/src/assets/texture/texture20.webp new file mode 100644 index 00000000..bebd5ade Binary files /dev/null and b/src/assets/texture/texture20.webp differ diff --git a/src/assets/texture/texture3.webp b/src/assets/texture/texture3.webp new file mode 100644 index 00000000..403dceab Binary files /dev/null and b/src/assets/texture/texture3.webp differ diff --git a/src/assets/texture/texture4.webp b/src/assets/texture/texture4.webp new file mode 100644 index 00000000..2da9b66e Binary files /dev/null and b/src/assets/texture/texture4.webp differ diff --git a/src/assets/texture/texture5.webp b/src/assets/texture/texture5.webp new file mode 100644 index 00000000..3c3f645d Binary files /dev/null and b/src/assets/texture/texture5.webp differ diff --git a/src/assets/texture/texture6.webp b/src/assets/texture/texture6.webp new file mode 100644 index 00000000..d8160a80 Binary files /dev/null and b/src/assets/texture/texture6.webp differ diff --git a/src/assets/texture/texture7.webp b/src/assets/texture/texture7.webp new file mode 100644 index 00000000..895ceed9 Binary files /dev/null and b/src/assets/texture/texture7.webp differ diff --git a/src/assets/texture/texture8.webp b/src/assets/texture/texture8.webp new file mode 100644 index 00000000..e99a0b95 Binary files /dev/null and b/src/assets/texture/texture8.webp differ diff --git a/src/assets/texture/texture9.webp b/src/assets/texture/texture9.webp new file mode 100644 index 00000000..942955fd Binary files /dev/null and b/src/assets/texture/texture9.webp differ diff --git a/src/assets/vue.svg b/src/assets/vue.svg new file mode 100644 index 00000000..770e9d33 --- /dev/null +++ b/src/assets/vue.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/component/Canvas/CanvasEditor/commands/BackgroundCommands.js b/src/component/Canvas/CanvasEditor/commands/BackgroundCommands.js new file mode 100644 index 00000000..80f2525d --- /dev/null +++ b/src/component/Canvas/CanvasEditor/commands/BackgroundCommands.js @@ -0,0 +1,589 @@ +import { Command } from "./Command"; +//import { fabric } from "fabric-with-all"; + +/** + * 创建背景图层命令 + */ +export class CreateBackgroundLayerCommand extends Command { + constructor(options) { + super({ + name: "创建背景图层", + saveState: true, + }); + this.canvas = options.canvas; + this.layers = options.layers; + this.backgroundLayer = options.backgroundLayer; + this.canvasManager = options.canvasManager; + this.historyManager = options.historyManager; + + this.beforeLayers = [...this.layers.value]; // 备份原图层列表 + } + + execute() { + // 检查是否已经存在背景图层 + const existingBgLayer = this.layers.value.find( + (layer) => layer.isBackground + ); + if (existingBgLayer) { + console.warn("已存在背景层,不重复创建"); + return existingBgLayer.id; + } + + // 创建背景矩形对象 + const bgObject = this._createBackgroundObject(); + + // 将背景对象添加到图层中 + this.backgroundLayer.fabricObject = bgObject; + + // 添加图层到最底部 + this.layers.value.push(this.backgroundLayer); + + // 添加到画布 + this.canvas.add(bgObject); + + // 渲染画布 + this.canvas.renderAll(); + + return this.backgroundLayer.id; + } + + undo() { + // 从图层列表中删除背景图层 + const bgLayerIndex = this.layers.value.findIndex( + (layer) => layer.isBackground + ); + if (bgLayerIndex !== -1) { + this.layers.value.splice(bgLayerIndex, 1); + } + + // 从画布中移除背景对象 + if (this.backgroundLayer.fabricObject) { + this.canvas.remove(this.backgroundLayer.fabricObject); + } + + // 渲染画布 + this.canvas.renderAll(); + } + + /** + * 创建背景矩形对象 + * @returns {Object} fabric.js 矩形对象 + * @private + */ + _createBackgroundObject() { + // 计算画布尺寸 + const canvasWidth = this.canvas.width; + const canvasHeight = this.canvas.height; + + // 确保背景色为白色,如果没有设置或者是透明的话 + const backgroundColor = + this.backgroundLayer.backgroundColor && + this.backgroundLayer.backgroundColor !== "transparent" + ? this.backgroundLayer.backgroundColor + : "#ffffff"; + + const rect = new fabric.Rect({ + left: canvasWidth / 2, + top: canvasHeight / 2, + width: this.backgroundLayer.canvasWidth, + height: this.backgroundLayer.canvasHeight, + fill: backgroundColor, + selectable: false, + evented: false, + hoverCursor: "default", + id: `bg_object_${this.backgroundLayer.id}`, + layerId: this.backgroundLayer.id, + layerName: this.backgroundLayer.name, + originX: "center", + originY: "center", + isBackground: true, // 标记为背景对象 + }); + + return rect; + } + + getInfo() { + return { + name: this.name, + layerId: this.backgroundLayer.id, + layerName: this.backgroundLayer.name, + width: this.backgroundLayer.canvasWidth, + height: this.backgroundLayer.canvasHeight, + backgroundColor: this.backgroundLayer.backgroundColor, + }; + } +} + +/** + * 更新背景属性命令(如背景颜色) + */ +export class UpdateBackgroundCommand extends Command { + constructor(options) { + super({ + name: "更新背景属性", + saveState: true, + }); + this.canvas = options.canvas; + this.layers = options.layers; + this.backgroundColor = options.backgroundColor; + this.historyManager = options.historyManager; + + // 查找背景图层 + this.bgLayer = this.layers.value.find((layer) => layer.isBackground); + this.oldBackgroundColor = this.bgLayer + ? this.bgLayer.backgroundColor + : "#ffffff"; + } + + execute() { + if (!this.bgLayer) { + console.error("未找到背景图层"); + return false; + } + + // 更新背景图层属性 + this.bgLayer.backgroundColor = this.backgroundColor; + + // 更新背景对象属性 + if (this.bgLayer.fabricObject) { + this.bgLayer.fabricObject.set("fill", this.backgroundColor); + this.canvas.renderAll(); + } + + return true; + } + + undo() { + if (!this.bgLayer) { + return false; + } + + // 恢复背景图层属性 + this.bgLayer.backgroundColor = this.oldBackgroundColor; + + // 恢复背景对象属性 + if (this.bgLayer.fabricObject) { + this.bgLayer.fabricObject.set("fill", this.oldBackgroundColor); + this.canvas.renderAll(); + } + + return true; + } + + getInfo() { + return { + name: this.name, + layerId: this.bgLayer?.id, + oldColor: this.oldBackgroundColor, + newColor: this.backgroundColor, + }; + } +} + +/** + * 调整画布和背景大小命令 + */ +export class BackgroundSizeCommand extends Command { + constructor(options) { + super({ + name: "调整背景大小", + saveState: true, + }); + this.canvas = options.canvas; + this.layers = options.layers; + this.canvasManager = options.canvasManager; + this.newWidth = options.newWidth; + this.newHeight = options.newHeight; + this.historyManager = options.historyManager; + + // 记录原尺寸 + this.oldWidth = this.canvas.width; + this.oldHeight = this.canvas.height; + + // 查找背景图层 + this.bgLayer = this.layers.value.find((layer) => layer.isBackground); + } + + execute() { + // 调整画布大小 + this.canvas.setWidth(this.newWidth); + this.canvas.setHeight(this.newHeight); + + // 如果使用 CanvasManager,通知它画布大小变化 + if ( + this.canvasManager && + typeof this.canvasManager.updateCanvasSize === "function" + ) { + this.canvasManager.updateCanvasSize(this.newWidth, this.newHeight); + } + + // 调整背景对象大小 + if (this.bgLayer && this.bgLayer.fabricObject) { + // 保持原有的背景颜色,如果没有设置则使用白色 + const currentFill = + this.bgLayer.fabricObject.fill || + this.bgLayer.backgroundColor || + "#ffffff"; + + this.bgLayer.fabricObject.set({ + width: this.newWidth, + height: this.newHeight, + fill: currentFill, // 保持原有颜色 + }); + + // 更新图层记录的尺寸 + this.bgLayer.canvasWidth = this.newWidth; + this.bgLayer.canvasHeight = this.newHeight; + } + + // 渲染画布 + this.canvas.renderAll(); + + return true; + } + + undo() { + // 恢复画布大小 + this.canvas.setWidth(this.oldWidth); + this.canvas.setHeight(this.oldHeight); + + // 如果使用 CanvasManager,通知它画布大小恢复 + if ( + this.canvasManager && + typeof this.canvasManager.updateCanvasSize === "function" + ) { + this.canvasManager.updateCanvasSize(this.oldWidth, this.oldHeight); + } + + // 恢复背景对象大小 + if (this.bgLayer && this.bgLayer.fabricObject) { + this.bgLayer.fabricObject.set({ + width: this.oldWidth, + height: this.oldHeight, + }); + + // 恢复图层记录的尺寸 + this.bgLayer.canvasWidth = this.oldWidth; + this.bgLayer.canvasHeight = this.oldHeight; + } + + // 渲染画布 + this.canvas.renderAll(); + + return true; + } + + getInfo() { + return { + name: this.name, + oldWidth: this.oldWidth, + oldHeight: this.oldHeight, + newWidth: this.newWidth, + newHeight: this.newHeight, + }; + } +} + +/** + * 调整背景大小并等比缩放所有其他元素的命令 + */ +export class BackgroundSizeWithScaleCommand extends Command { + constructor(options) { + super({ + name: "调整背景大小并缩放元素", + saveState: true, + }); + this.canvas = options.canvas; + this.layers = options.layers; + this.canvasManager = options.canvasManager; + this.newWidth = options.newWidth; + this.newHeight = options.newHeight; + this.historyManager = options.historyManager; + + // 缩放策略:'uniform' | 'fill' | 'fit' | 'stretch' + this.scaleStrategy = options.scaleStrategy || "uniform"; + + // 记录原尺寸 + this.oldWidth = this.canvas.width; + this.oldHeight = this.canvas.height; + + // 查找背景图层 + this.bgLayer = this.layers.value.find((layer) => layer.isBackground); + + // 计算缩放比例 + const scaleXRatio = this.newWidth / this.oldWidth; + const scaleYRatio = this.newHeight / this.oldHeight; + + // 根据策略计算缩放比例和偏移 + this._calculateScaleAndOffset(scaleXRatio, scaleYRatio); + + // 存储所有非背景对象的原始状态 + this.objectStates = []; + this._saveOriginalStates(); + } + + /** + * 保存所有非背景对象的原始状态 + * @private + */ + _saveOriginalStates() { + this.canvas.getObjects().forEach((obj) => { + if (!obj.isBackground) { + // 检查对象是否已经有原始状态记录 + if (!obj._originalState) { + // 第一次记录原始状态 + obj._originalState = { + left: obj.left, + top: obj.top, + scaleX: obj.scaleX || 1, + scaleY: obj.scaleY || 1, + width: obj.width, + height: obj.height, + // 记录基准画布尺寸 + baseCanvasWidth: this.oldWidth, + baseCanvasHeight: this.oldHeight, + }; + } + + this.objectStates.push({ + obj: obj, + // 使用原始状态而不是当前状态 + left: obj._originalState.left, + top: obj._originalState.top, + scaleX: obj._originalState.scaleX, + scaleY: obj._originalState.scaleY, + width: obj._originalState.width, + height: obj._originalState.height, + }); + } + }); + } + + /** + * 根据缩放策略计算缩放比例和偏移量 + * @param {number} scaleXRatio X轴缩放比例 + * @param {number} scaleYRatio Y轴缩放比例 + * @private + */ + _calculateScaleAndOffset(scaleXRatio, scaleYRatio) { + switch (this.scaleStrategy) { + case "uniform": + // 统一缩放:使用平均值,保持相对比例的同时允许适度的形变 + this.uniformScale = Math.sqrt(scaleXRatio * scaleYRatio); + this.offsetX = (this.newWidth - this.oldWidth * this.uniformScale) / 2; + this.offsetY = + (this.newHeight - this.oldHeight * this.uniformScale) / 2; + break; + + case "fit": + // 适应模式:使用较小值,确保所有内容都在画布内,可能有留白 + this.uniformScale = Math.min(scaleXRatio, scaleYRatio); + this.offsetX = (this.newWidth - this.oldWidth * this.uniformScale) / 2; + this.offsetY = + (this.newHeight - this.oldHeight * this.uniformScale) / 2; + break; + + case "fill": + // 填充模式:使用较大值,填满画布,可能有部分内容被裁切 + this.uniformScale = Math.max(scaleXRatio, scaleYRatio); + this.offsetX = (this.newWidth - this.oldWidth * this.uniformScale) / 2; + this.offsetY = + (this.newHeight - this.oldHeight * this.uniformScale) / 2; + break; + + case "stretch": + // 拉伸模式:不保持宽高比,完全适应新尺寸 + this.scaleX = scaleXRatio; + this.scaleY = scaleYRatio; + this.offsetX = 0; + this.offsetY = 0; + break; + + default: + // 默认使用uniform模式 + this.uniformScale = Math.sqrt(scaleXRatio * scaleYRatio); + this.offsetX = (this.newWidth - this.oldWidth * this.uniformScale) / 2; + this.offsetY = + (this.newHeight - this.oldHeight * this.uniformScale) / 2; + } + } + + execute() { + // 调整画布大小 + this.canvas.setWidth(this.newWidth); + this.canvas.setHeight(this.newHeight); + + // 如果使用 CanvasManager,通知它画布大小变化 + if ( + this.canvasManager && + typeof this.canvasManager.updateCanvasSize === "function" + ) { + this.canvasManager.updateCanvasSize(this.newWidth, this.newHeight); + } + + // 调整背景对象大小和位置 + if (this.bgLayer && this.bgLayer.fabricObject) { + // 保持原有的背景颜色,如果没有设置则使用白色 + const currentFill = + this.bgLayer.fabricObject.fill || + this.bgLayer.backgroundColor || + "#ffffff"; + + this.bgLayer.fabricObject.set({ + width: this.newWidth, + height: this.newHeight, + left: this.newWidth / 2, + top: this.newHeight / 2, + fill: currentFill, // 保持原有颜色 + }); + + // 更新图层记录的尺寸 + this.bgLayer.canvasWidth = this.newWidth; + this.bgLayer.canvasHeight = this.newHeight; + } + + // 计算基于原始画布的缩放比例 + const baseScaleX = + this.newWidth / + this.objectStates[0]?.obj._originalState?.baseCanvasWidth || + this.newWidth / this.oldWidth; + const baseScaleY = + this.newHeight / + this.objectStates[0]?.obj._originalState?.baseCanvasHeight || + this.newHeight / this.oldHeight; + + // 根据策略缩放所有非背景对象 + this.objectStates.forEach((state) => { + const obj = state.obj; + + if (this.scaleStrategy === "stretch") { + // 拉伸模式:使用不同的X和Y缩放比例 + obj.set({ + left: state.left * baseScaleX, + top: state.top * baseScaleY, + scaleX: state.scaleX * baseScaleX, + scaleY: state.scaleY * baseScaleY, + }); + } else { + // 其他模式:计算基于原始状态的统一缩放比例 + const baseUniformScale = Math.sqrt(baseScaleX * baseScaleY); + const baseOffsetX = + (this.newWidth - + (obj._originalState?.baseCanvasWidth || this.oldWidth) * + baseUniformScale) / + 2; + const baseOffsetY = + (this.newHeight - + (obj._originalState?.baseCanvasHeight || this.oldHeight) * + baseUniformScale) / + 2; + + obj.set({ + left: state.left * baseUniformScale + baseOffsetX, + top: state.top * baseUniformScale + baseOffsetY, + scaleX: state.scaleX * baseUniformScale, + scaleY: state.scaleY * baseUniformScale, + }); + } + + obj.setCoords(); + }); + + // 渲染画布 + this.canvas.renderAll(); + + return true; + } + + undo() { + // 恢复画布大小 + this.canvas.setWidth(this.oldWidth); + this.canvas.setHeight(this.oldHeight); + + // 如果使用 CanvasManager,通知它画布大小恢复 + if ( + this.canvasManager && + typeof this.canvasManager.updateCanvasSize === "function" + ) { + this.canvasManager.updateCanvasSize(this.oldWidth, this.oldHeight); + } + + // 恢复背景对象大小和位置 + if (this.bgLayer && this.bgLayer.fabricObject) { + this.bgLayer.fabricObject.set({ + width: this.oldWidth, + height: this.oldHeight, + left: this.oldWidth / 2, + top: this.oldHeight / 2, + }); + + // 恢复图层记录的尺寸 + this.bgLayer.canvasWidth = this.oldWidth; + this.bgLayer.canvasHeight = this.oldHeight; + } + + // 恢复所有非背景对象的当前状态(而不是原始状态,因为可能有其他操作) + this.objectStates.forEach((state) => { + // 计算恢复到之前画布尺寸时的状态 + const obj = state.obj; + const originalState = obj._originalState; + + if (originalState) { + const baseScaleX = this.oldWidth / originalState.baseCanvasWidth; + const baseScaleY = this.oldHeight / originalState.baseCanvasHeight; + const baseUniformScale = Math.sqrt(baseScaleX * baseScaleY); + const baseOffsetX = + (this.oldWidth - originalState.baseCanvasWidth * baseUniformScale) / + 2; + const baseOffsetY = + (this.oldHeight - originalState.baseCanvasHeight * baseUniformScale) / + 2; + + obj.set({ + left: originalState.left * baseUniformScale + baseOffsetX, + top: originalState.top * baseUniformScale + baseOffsetY, + scaleX: originalState.scaleX * baseUniformScale, + scaleY: originalState.scaleY * baseUniformScale, + }); + } else { + // 降级到原来的逻辑 + obj.set({ + left: state.left, + top: state.top, + scaleX: state.scaleX, + scaleY: state.scaleY, + }); + } + + obj.setCoords(); + }); + + // 渲染画布 + this.canvas.renderAll(); + + return true; + } + + getInfo() { + const info = { + name: this.name, + oldWidth: this.oldWidth, + oldHeight: this.oldHeight, + newWidth: this.newWidth, + newHeight: this.newHeight, + scaleStrategy: this.scaleStrategy, + objectCount: this.objectStates.length, + }; + + if (this.scaleStrategy === "stretch") { + info.scaleX = this.scaleX; + info.scaleY = this.scaleY; + } else { + info.uniformScale = this.uniformScale; + info.offsetX = this.offsetX; + info.offsetY = this.offsetY; + } + + return info; + } +} diff --git a/src/component/Canvas/CanvasEditor/commands/BrushCommands.js b/src/component/Canvas/CanvasEditor/commands/BrushCommands.js new file mode 100644 index 00000000..cc1cdd27 --- /dev/null +++ b/src/component/Canvas/CanvasEditor/commands/BrushCommands.js @@ -0,0 +1,698 @@ +import { Command } from "./Command"; +import { BrushStore } from "../store/BrushStore"; + +/** + * 笔刷属性设置基类命令 + * 所有笔刷属性设置命令的基类 + */ +class BaseBrushCommand extends Command { + constructor(options = {}) { + super(options); + this.brushStore = options.brushStore || BrushStore; + } +} + +/** + * 笔刷大小设置命令 + */ +export class BrushSizeCommand extends BaseBrushCommand { + /** + * @param {Object} options 命令选项 + * @param {Number} options.size 要设置的笔刷大小 + * @param {Number} options.previousSize 之前的笔刷大小(可选) + * @param {Object} options.brushStore BrushStore实例(可选) + */ + constructor(options = {}) { + super({ + ...options, + name: `设置笔刷大小: ${options.size}`, + description: `将笔刷大小从 ${options.previousSize || "?"} 设为 ${ + options.size + }`, + }); + + this.size = options.size; + this.previousSize = options.previousSize || this.brushStore.state.size; + } + + execute() { + // 记录当前大小用于撤销 + if (this.previousSize === null) { + this.previousSize = this.brushStore.state.size; + } + + // 执行设置 + this.brushStore.setBrushSize(this.size); + return this.size; + } + + undo() { + this.brushStore.setBrushSize(this.previousSize); + return this.previousSize; + } +} + +/** + * 笔刷颜色设置命令 + */ +export class BrushColorCommand extends BaseBrushCommand { + /** + * @param {Object} options 命令选项 + * @param {String} options.color 要设置的颜色 + * @param {String} options.previousColor 之前的颜色(可选) + * @param {Object} options.brushStore BrushStore实例(可选) + */ + constructor(options = {}) { + super({ + ...options, + name: `设置笔刷颜色: ${options.color}`, + description: `将笔刷颜色从 ${options.previousColor || "?"} 设为 ${ + options.color + }`, + }); + + this.color = options.color; + this.previousColor = options.previousColor || this.brushStore.state.color; + } + + execute() { + // 记录当前颜色用于撤销 + if (this.previousColor === null) { + this.previousColor = this.brushStore.state.color; + } + + // 执行设置 + this.brushStore.setBrushColor(this.color); + return this.color; + } + + undo() { + this.brushStore.setBrushColor(this.previousColor); + return this.previousColor; + } +} + +/** + * 笔刷透明度设置命令 + */ +export class BrushOpacityCommand extends BaseBrushCommand { + /** + * @param {Object} options 命令选项 + * @param {Number} options.opacity 要设置的透明度 (0-1) + * @param {Number} options.previousOpacity 之前的透明度(可选) + * @param {Object} options.brushStore BrushStore实例(可选) + */ + constructor(options = {}) { + super({ + ...options, + name: `设置笔刷透明度: ${options.opacity}`, + description: `将笔刷透明度从 ${options.previousOpacity || "?"} 设为 ${ + options.opacity + }`, + }); + + this.opacity = options.opacity; + this.previousOpacity = + options.previousOpacity || this.brushStore.state.opacity; + } + + execute() { + // 记录当前透明度用于撤销 + if (this.previousOpacity === null) { + this.previousOpacity = this.brushStore.state.opacity; + } + + // 执行设置 + this.brushStore.setBrushOpacity(this.opacity); + return this.opacity; + } + + undo() { + this.brushStore.setBrushOpacity(this.previousOpacity); + return this.previousOpacity; + } +} + +/** + * 笔刷类型设置命令 + */ +export class BrushTypeCommand extends BaseBrushCommand { + /** + * @param {Object} options 命令选项 + * @param {String} options.brushType 要设置的笔刷类型 + * @param {String} options.previousType 之前的笔刷类型(可选) + * @param {Object} options.brushStore BrushStore实例(可选) + */ + constructor(options = {}) { + super({ + ...options, + name: `设置笔刷类型: ${options.brushType}`, + description: `将笔刷类型从 ${options.previousType || "?"} 设为 ${ + options.brushType + }`, + }); + + this.brushType = options.brushType; + this.previousType = options.previousType || this.brushStore.state.type; + this.brushManager = options.brushManager; + } + + execute() { + // 记录当前类型用于撤销 + if (this.previousType === null) { + this.previousType = this.brushStore.state.type; + } + + // 执行设置 + // this.brushStore.setBrushType(this.brushType); + this.brushManager.setBrushType(this.brushType); + return this.brushType; + } + + undo() { + // this.brushStore.setBrushType(this.previousType); + this.brushManager.setBrushType(this.previousType); + return this.previousType; + } +} + +/** + * 材质设置命令 + */ +export class TextureCommand extends BaseBrushCommand { + /** + * @param {Object} options 命令选项 + * @param {Boolean} options.enabled 是否启用材质 + * @param {String} options.path 材质路径 + * @param {Number} options.scale 材质缩放 + * @param {Object} options.previous 之前的材质设置(可选) + * @param {Object} options.brushStore BrushStore实例(可选) + */ + constructor(options = {}) { + super({ + ...options, + name: options.enabled ? "启用笔刷材质" : "禁用笔刷材质", + description: options.enabled + ? `启用材质: ${options.path || "[默认]"}` + : "禁用笔刷材质", + }); + + this.enabled = options.enabled; + this.path = options.path; + this.scale = options.scale; + + // 保存之前状态用于撤销 + this.previous = options.previous || { + enabled: this.brushStore.state.textureEnabled, + path: this.brushStore.state.texturePath, + scale: this.brushStore.state.textureScale, + }; + } + + execute() { + // 记录当前状态用于撤销 + if (!this.previous) { + this.previous = { + enabled: this.brushStore.state.textureEnabled, + path: this.brushStore.state.texturePath, + scale: this.brushStore.state.textureScale, + }; + } + + // 执行设置 + this.brushStore.setTextureEnabled(this.enabled); + + if (this.path) { + this.brushStore.setTexturePath(this.path); + } + + if (this.scale !== undefined) { + this.brushStore.setTextureScale(this.scale); + } + + return { + enabled: this.enabled, + path: this.path, + scale: this.scale, + }; + } + + undo() { + if (!this.previous) return null; + + this.brushStore.setTextureEnabled(this.previous.enabled); + this.brushStore.setTexturePath(this.previous.path); + this.brushStore.setTextureScale(this.previous.scale); + + return this.previous; + } +} + +/** + * 预设应用命令 + */ +export class BrushPresetCommand extends BaseBrushCommand { + /** + * @param {Object} options 命令选项 + * @param {Number|Object} options.preset 预设索引或预设对象 + * @param {Object} options.previousState 之前的状态(可选) + * @param {Object} options.brushStore BrushStore实例(可选) + */ + constructor(options = {}) { + const presetName = + typeof options.preset === "object" + ? options.preset.name + : `预设 ${options.preset}`; + + super({ + ...options, + name: `应用笔刷预设: ${presetName}`, + description: `应用预设: ${presetName}`, + }); + + this.preset = options.preset; + + // 保存之前状态用于撤销 + this.previousState = options.previousState || { + size: this.brushStore.state.size, + color: this.brushStore.state.color, + opacity: this.brushStore.state.opacity, + type: this.brushStore.state.type, + textureEnabled: this.brushStore.state.textureEnabled, + textureScale: this.brushStore.state.textureScale, + texturePath: this.brushStore.state.texturePath, + }; + } + + execute() { + // 记录当前状态用于撤销 + if (!this.previousState) { + this.previousState = { + size: this.brushStore.state.size, + color: this.brushStore.state.color, + opacity: this.brushStore.state.opacity, + type: this.brushStore.state.type, + textureEnabled: this.brushStore.state.textureEnabled, + textureScale: this.brushStore.state.textureScale, + texturePath: this.brushStore.state.texturePath, + }; + } + + // 应用预设 + if (typeof this.preset === "number") { + this.brushStore.applyPreset(this.preset); + } else if (typeof this.preset === "object") { + // 应用自定义预设对象 + if (this.preset.size !== undefined) + this.brushStore.setBrushSize(this.preset.size); + if (this.preset.color !== undefined) + this.brushStore.setBrushColor(this.preset.color); + if (this.preset.opacity !== undefined) + this.brushStore.setBrushOpacity(this.preset.opacity); + if (this.preset.type !== undefined) + this.brushStore.setBrushType(this.preset.type); + + if (this.preset.textureEnabled !== undefined) { + this.brushStore.setTextureEnabled(this.preset.textureEnabled); + } + + if (this.preset.texturePath !== undefined) { + this.brushStore.setTexturePath(this.preset.texturePath); + } + + if (this.preset.textureScale !== undefined) { + this.brushStore.setTextureScale(this.preset.textureScale); + } + } + + return true; + } + + undo() { + if (!this.previousState) return false; + + // 恢复之前的状态 + this.brushStore.setBrushSize(this.previousState.size); + this.brushStore.setBrushColor(this.previousState.color); + this.brushStore.setBrushOpacity(this.previousState.opacity); + this.brushStore.setBrushType(this.previousState.type); + this.brushStore.setTextureEnabled(this.previousState.textureEnabled); + this.brushStore.setTextureScale(this.previousState.textureScale); + this.brushStore.setTexturePath(this.previousState.texturePath); + + return true; + } +} + +/** + * 笔刷属性命令 + * 用于修改笔刷的任意属性,包括特殊属性 + */ +export class BrushPropertyCommand extends Command { + /** + * 构造函数 + * @param {Object} options 命令选项 + * @param {String} options.propertyId 属性ID + * @param {any} options.value 新的属性值 + * @param {Object} options.brushStore BrushStore实例 + */ + constructor(options) { + super({ + name: "笔刷属性更改", + description: `更改笔刷属性 ${options.propertyId}`, + options, + }); + + this.propertyId = options.propertyId; + this.newValue = options.value; + this.oldValue = null; + this.brushStore = options.brushStore || BrushStore; + } + + /** + * 执行命令 + * @returns {Boolean} 是否执行成功 + */ + execute() { + if (!this.brushStore) { + console.error("BrushStore不可用"); + return false; + } + + // 保存旧值用于撤销 + this.oldValue = this.brushStore.getPropertyValue(this.propertyId); + + // 更新属性值 + this.brushStore.updatePropertyValue(this.propertyId, this.newValue); + + return true; + } + + /** + * 撤销命令 + * @param {Object} context 命令上下文 + * @returns {Boolean} 是否撤销成功 + */ + undo() { + if (!this.brushStore || this.oldValue === null) { + return false; + } + + // 恢复旧值 + this.brushStore.updatePropertyValue(this.propertyId, this.oldValue); + + return true; + } +} + +/** + * 材质选择命令 + */ +export class TextureSelectionCommand extends BaseBrushCommand { + /** + * @param {Object} options 命令选项 + * @param {String} options.textureId 要设置的材质ID + * @param {String} options.previousTextureId 之前的材质ID(可选) + * @param {Object} options.brushManager 笔刷管理器实例 + */ + constructor(options = {}) { + super({ + ...options, + name: `切换纹理材质`, + description: `切换到材质: ${options.textureId}`, + }); + + this.textureId = options.textureId; + this.previousTextureId = options.previousTextureId; + this.brushManager = options.brushManager; + } + + execute() { + // 记录当前材质用于撤销 + const currentBrush = this.brushManager.activeBrush; + if (currentBrush && currentBrush.getCurrentTexture) { + const currentTexture = currentBrush.getCurrentTexture(); + this.previousTextureId = currentTexture ? currentTexture.id : null; + } + + // 确保当前是材质笔刷 + if (this.brushManager.getCurrentBrushType() !== "texture") { + this.brushManager.setBrushType("texture"); + } + + // 设置材质 + const activeBrush = this.brushManager.activeBrush; + if (activeBrush && activeBrush.setTextureById) { + activeBrush.setTextureById(this.textureId); + } + + return this.textureId; + } + + undo() { + if (!this.previousTextureId) return false; + + // 确保当前是材质笔刷 + if (this.brushManager.getCurrentBrushType() !== "texture") { + this.brushManager.setBrushType("texture"); + } + + // 恢复之前的材质 + const activeBrush = this.brushManager.activeBrush; + if (activeBrush && activeBrush.setTextureById) { + activeBrush.setTextureById(this.previousTextureId); + } + + return this.previousTextureId; + } +} + +/** + * 材质属性设置命令 + */ +export class TexturePropertyCommand extends BaseBrushCommand { + /** + * @param {Object} options 命令选项 + * @param {String} options.property 要设置的属性名称 (scale, rotation, offsetX, offsetY等) + * @param {any} options.value 要设置的属性值 + * @param {any} options.previousValue 之前的属性值(可选) + * @param {Object} options.brushManager 笔刷管理器实例 + */ + constructor(options = {}) { + super({ + ...options, + name: `设置材质属性: ${options.property}`, + description: `将材质${options.property}从 ${ + options.previousValue || "?" + } 设为 ${options.value}`, + }); + + this.property = options.property; + this.value = options.value; + this.previousValue = options.previousValue; + this.brushManager = options.brushManager; + } + + execute() { + // 确保当前是材质笔刷 + if (this.brushManager.getCurrentBrushType() !== "texture") { + this.brushManager.setBrushType("texture"); + } + + const activeBrush = this.brushManager.activeBrush; + if (!activeBrush || !activeBrush.setTextureProperty) { + return false; + } + + // 记录当前值用于撤销 + if (this.previousValue === undefined) { + this.previousValue = activeBrush.getTextureProperty(this.property); + } + + // 设置新值 + activeBrush.setTextureProperty(this.property, this.value); + return this.value; + } + + undo() { + if (this.previousValue === undefined) return false; + + // 确保当前是材质笔刷 + if (this.brushManager.getCurrentBrushType() !== "texture") { + this.brushManager.setBrushType("texture"); + } + + const activeBrush = this.brushManager.activeBrush; + if (activeBrush && activeBrush.setTextureProperty) { + activeBrush.setTextureProperty(this.property, this.previousValue); + } + + return this.previousValue; + } +} + +/** + * 材质预设应用命令 + */ +export class TexturePresetCommand extends BaseBrushCommand { + /** + * @param {Object} options 命令选项 + * @param {String|Object} options.preset 预设ID或预设对象 + * @param {Object} options.previousState 之前的材质状态(可选) + * @param {Object} options.brushManager 笔刷管理器实例 + */ + constructor(options = {}) { + const presetName = + typeof options.preset === "object" ? options.preset.name : options.preset; + + super({ + ...options, + name: `应用材质预设: ${presetName}`, + description: `应用材质预设: ${presetName}`, + }); + + this.preset = options.preset; + this.previousState = options.previousState; + this.brushManager = options.brushManager; + } + + execute() { + // 确保当前是材质笔刷 + if (this.brushManager.getCurrentBrushType() !== "texture") { + this.brushManager.setBrushType("texture"); + } + + const activeBrush = this.brushManager.activeBrush; + if (!activeBrush || !activeBrush.applyTexturePreset) { + return false; + } + + // 记录当前状态用于撤销 + if (!this.previousState) { + this.previousState = activeBrush.getCurrentTextureState(); + } + + // 应用预设 + activeBrush.applyTexturePreset(this.preset); + return true; + } + + undo() { + if (!this.previousState) return false; + + // 确保当前是材质笔刷 + if (this.brushManager.getCurrentBrushType() !== "texture") { + this.brushManager.setBrushType("texture"); + } + + const activeBrush = this.brushManager.activeBrush; + if (activeBrush && activeBrush.restoreTextureState) { + activeBrush.restoreTextureState(this.previousState); + } + + return true; + } +} + +/** + * 纹理上传命令 + */ +export class TextureUploadCommand extends BaseBrushCommand { + /** + * @param {Object} options 命令选项 + * @param {File} options.file 要上传的纹理文件 + * @param {String} options.name 纹理名称(可选) + * @param {String} options.category 纹理分类(可选) + * @param {Object} options.texturePresetManager 纹理预设管理器实例 + * @param {Object} options.brushManager 笔刷管理器实例 + */ + constructor(options = {}) { + super({ + ...options, + name: `上传纹理: ${options.name || options.file?.name || '未知'}`, + description: `上传自定义纹理文件`, + }); + + this.file = options.file; + this.name = options.name || options.file?.name?.replace(/\.[^/.]+$/, "") || "自定义纹理"; + this.category = options.category || "自定义材质"; + this.texturePresetManager = options.texturePresetManager; + this.brushManager = options.brushManager; + this.uploadedTextureId = null; + } + + async execute() { + if (!this.file || !this.texturePresetManager) { + throw new Error('缺少必要的文件或纹理预设管理器'); + } + + try { + // 创建文件 data URL + const dataUrl = await this._fileToDataUrl(this.file); + + // 添加到纹理预设管理器 + this.uploadedTextureId = this.texturePresetManager.addCustomTexture({ + name: this.name, + category: this.category, + file: this.file, + dataUrl: dataUrl, + preview: dataUrl, + }); + + // 如果是纹理笔刷,自动应用新上传的纹理 + if (this.brushManager) { + if (this.brushManager.getCurrentBrushType() !== "texture") { + this.brushManager.setBrushType("texture"); + } + + const activeBrush = this.brushManager.activeBrush; + if (activeBrush && activeBrush.updateProperty) { + activeBrush.updateProperty("textureSelector", this.uploadedTextureId); + } + } + + return { + textureId: this.uploadedTextureId, + dataUrl: dataUrl, + name: this.name + }; + } catch (error) { + console.error('纹理上传失败:', error); + throw new Error(`纹理上传失败: ${error.message}`); + } + } + + undo() { + if (!this.uploadedTextureId || !this.texturePresetManager) { + return false; + } + + // 从纹理预设管理器中移除上传的纹理 + try { + this.texturePresetManager.removeCustomTexture(this.uploadedTextureId); + return true; + } catch (error) { + console.error('撤销纹理上传失败:', error); + return false; + } + } + + /** + * 将文件转换为 data URL + * @private + * @param {File} file + * @returns {Promise} + */ + _fileToDataUrl(file) { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onload = (event) => { + resolve(event.target.result); + }; + reader.onerror = (error) => { + reject(error); + }; + reader.readAsDataURL(file); + }); + } +} diff --git a/src/component/Canvas/CanvasEditor/commands/Command.js b/src/component/Canvas/CanvasEditor/commands/Command.js new file mode 100644 index 00000000..9f5bed8d --- /dev/null +++ b/src/component/Canvas/CanvasEditor/commands/Command.js @@ -0,0 +1,296 @@ +/** + * 基础命令类 + * 所有命令都应该继承这个类 + */ +export class Command { + constructor(options = {}) { + this.name = options.name || "未命名命令"; + this.description = options.description || ""; + this.undoable = options.undoable !== false; // 默认可撤销 + this.timestamp = Date.now(); + } + + /** + * 执行命令 + * @returns {*} 执行结果,可以是Promise + */ + execute() { + throw new Error("子类必须实现execute方法"); + } + + /** + * 撤销命令 + * @returns {*} 撤销结果,可以是Promise + */ + undo() { + if (!this.undoable) { + throw new Error("此命令不支持撤销"); + } + throw new Error("可撤销命令必须实现undo方法"); + } + + /** + * 获取命令信息 + */ + getInfo() { + return { + name: this.name, + description: this.description, + undoable: this.undoable, + timestamp: this.timestamp, + }; + } +} + +/** + * 复合命令类 + * 用于批量执行多个命令,替代事务系统 + */ +export class CompositeCommand extends Command { + constructor(commands = [], options = {}) { + super({ + name: options.name || "复合命令", + description: options.description || "批量执行多个命令", + ...options, + }); + + this.commands = Array.isArray(commands) ? commands : []; + this.executedCommands = []; // 记录已执行的命令,用于撤销 + this.isExecuting = false; + } + + /** + * 添加子命令 + */ + addCommand(command) { + if (!command || typeof command.execute !== "function") { + throw new Error("无效的命令对象"); + } + this.commands.push(command); + return this; + } + + /** + * 批量添加子命令 + */ + addCommands(commands) { + if (Array.isArray(commands)) { + commands.forEach((cmd) => this.addCommand(cmd)); + } + return this; + } + + /** + * 执行所有子命令(串行执行) + */ + async execute() { + if (this.isExecuting) { + throw new Error("复合命令正在执行中"); + } + + this.isExecuting = true; + this.executedCommands = []; + + try { + const results = []; + + // 串行执行所有子命令 + for (const command of this.commands) { + try { + console.log(`📦 复合命令执行子命令: ${command.constructor.name}`); + + const result = command.execute(); + + // 如果是异步命令,等待完成 + const finalResult = this._isPromise(result) ? await result : result; + + results.push(finalResult); + this.executedCommands.push(command); + + console.log(`✅ 子命令执行成功: ${command.constructor.name}`); + } catch (error) { + console.error( + `❌ 子命令执行失败: ${command.constructor.name}`, + error + ); + + // 执行失败时,撤销已执行的命令 + await this._rollbackExecutedCommands(); + throw new Error(`复合命令执行失败:${error.message}`); + } + } + + this.isExecuting = false; + console.log(`✅ 复合命令执行完成,共执行 ${results.length} 个子命令`); + return results; + } catch (error) { + this.isExecuting = false; + throw error; + } + } + + /** + * 撤销所有已执行的子命令(逆序撤销) + */ + async undo() { + if (this.isExecuting) { + throw new Error("复合命令正在执行中,无法撤销"); + } + + if (this.executedCommands.length === 0) { + console.warn("没有已执行的子命令需要撤销"); + return true; + } + + console.log( + `↩️ 开始撤销复合命令,共 ${this.executedCommands.length} 个子命令` + ); + + try { + // 逆序撤销已执行的命令 + const commands = [...this.executedCommands].reverse(); + const results = []; + + for (const command of commands) { + if (typeof command.undo === "function") { + try { + console.log(`↩️ 撤销子命令: ${command.constructor.name}`); + + const result = command.undo(); + + // 如果是异步撤销,等待完成 + const finalResult = this._isPromise(result) ? await result : result; + + results.push(finalResult); + console.log(`✅ 子命令撤销成功: ${command.constructor.name}`); + } catch (error) { + console.error( + `❌ 子命令撤销失败: ${command.constructor.name}`, + error + ); + // 撤销失败不中断整个撤销过程,但要记录错误 + } + } else { + console.warn(`⚠️ 子命令不支持撤销: ${command.constructor.name}`); + } + } + + this.executedCommands = []; + console.log(`✅ 复合命令撤销完成`); + return results; + } catch (error) { + console.error("❌ 复合命令撤销过程中发生错误:", error); + throw error; + } + } + + /** + * 回滚已执行的命令(内部使用) + * @private + */ + async _rollbackExecutedCommands() { + console.log(`🔄 开始回滚已执行的 ${this.executedCommands.length} 个子命令`); + + const commands = [...this.executedCommands].reverse(); + + for (const command of commands) { + if (typeof command.undo === "function") { + try { + console.log(`🔄 回滚子命令: ${command.constructor.name}`); + const result = command.undo(); + if (this._isPromise(result)) { + await result; + } + console.log(`✅ 子命令回滚成功: ${command.constructor.name}`); + } catch (error) { + console.error( + `❌ 子命令回滚失败: ${command.constructor.name}`, + error + ); + // 回滚失败不中断整个回滚过程 + } + } + } + + this.executedCommands = []; + console.log(`✅ 回滚完成`); + } + + /** + * 检查返回值是否为Promise + * @private + */ + _isPromise(value) { + return ( + value && + typeof value === "object" && + typeof value.then === "function" && + typeof value.catch === "function" + ); + } + + /** + * 获取复合命令信息 + */ + getInfo() { + return { + ...super.getInfo(), + commandCount: this.commands.length, + executedCount: this.executedCommands.length, + isExecuting: this.isExecuting, + subCommands: this.commands.map((cmd) => + cmd.getInfo ? cmd.getInfo() : cmd.constructor.name + ), + }; + } +} + +/** + * 函数命令包装器 + * 将普通函数包装为命令对象 + */ +export class FunctionCommand extends Command { + constructor(executeFn, undoFn = null, options = {}) { + super({ + name: options.name || "函数命令", + undoable: typeof undoFn === "function", + ...options, + }); + + if (typeof executeFn !== "function") { + throw new Error("执行函数不能为空"); + } + + this.executeFn = executeFn; + this.undoFn = undoFn; + this.executeResult = null; // 保存执行结果用于撤销 + } + + async execute() { + const result = this.executeFn(); + this.executeResult = this._isPromise(result) ? await result : result; + return this.executeResult; + } + + async undo() { + if (!this.undoFn) { + throw new Error("此函数命令不支持撤销"); + } + + const result = this.undoFn(this.executeResult); + return this._isPromise(result) ? await result : result; + } + + /** + * 检查返回值是否为Promise + * @private + */ + _isPromise(value) { + return ( + value && + typeof value === "object" && + typeof value.then === "function" && + typeof value.catch === "function" + ); + } +} diff --git a/src/component/Canvas/CanvasEditor/commands/LassoCutoutCommand.js b/src/component/Canvas/CanvasEditor/commands/LassoCutoutCommand.js new file mode 100644 index 00000000..f6efffa5 --- /dev/null +++ b/src/component/Canvas/CanvasEditor/commands/LassoCutoutCommand.js @@ -0,0 +1,491 @@ +import { CompositeCommand } from "./Command.js"; +import { AddLayerCommand, CreateImageLayerCommand } from "./LayerCommands.js"; +import { ToolCommand } from "./ToolCommands.js"; +import { ClearSelectionCommand } from "./SelectionCommands.js"; +import { createLayer, LayerType, OperationType } from "../utils/layerHelper.js"; +//import { fabric } from "fabric-with-all"; + +/** + * 套索抠图命令 + * 实现将选区内容抠图到新图层的功能 + */ +export class LassoCutoutCommand extends CompositeCommand { + constructor(options = {}) { + super([], { + name: "套索抠图", + description: "将选区抠图到新图层", + }); + this.canvas = options.canvas; + this.layerManager = options.layerManager; + this.selectionManager = options.selectionManager; + this.toolManager = options.toolManager; + this.sourceLayerId = options.sourceLayerId; + this.newLayerName = options.newLayerName || "抠图"; + this.newLayerId = null; + this.cutoutImageUrl = null; + this.fabricImage = null; + this.executedCommands = []; + // 高清截图选项 + this.highResolutionEnabled = options.highResolutionEnabled !== false; // 默认启用 + this.baseResolutionScale = options.baseResolutionScale || 4; // 基础分辨率倍数,提高到4倍获得更清晰的图像 + } + + async execute() { + if (!this.canvas || !this.layerManager || !this.selectionManager) { + console.error("无法执行套索抠图:参数无效"); + return false; + } + + try { + this.executedCommands = []; + + // 获取选区 + const selectionObject = this.selectionManager.getSelectionObject(); + if (!selectionObject) { + console.error("无法执行套索抠图:当前没有选区"); + return false; + } + + // 确定源图层 + const sourceLayer = this.layerManager.getActiveLayer(); + if (!sourceLayer || sourceLayer.fabricObjects.length === 0) { + console.error("无法执行套索抠图:源图层无效"); + return false; + } + + // 获取选区边界信息用于后续定位 + const selectionBounds = selectionObject.getBoundingRect(true, true); + + // 执行在当前画布上的抠图操作 + this.cutoutImageUrl = await this._performCutout( + sourceLayer, + selectionObject + ); + if (!this.cutoutImageUrl) { + console.error("抠图失败"); + return false; + } + + // 创建fabric图像对象,传递选区边界信息 + this.fabricImage = await this._createFabricImage( + this.cutoutImageUrl, + selectionBounds + ); + if (!this.fabricImage) { + console.error("创建图像对象失败"); + return false; + } + + // 1. 创建图像图层命令 + const createImageLayerCmd = new CreateImageLayerCommand({ + layerManager: this.layerManager, + fabricImage: this.fabricImage, + toolManager: this.toolManager, + layerName: this.newLayerName, + }); + + // 执行创建图像图层命令 + const result = await createImageLayerCmd.execute(); + this.newLayerId = createImageLayerCmd.newLayerId; + this.executedCommands.push(createImageLayerCmd); + + // 2. 清除选区命令 + const clearSelectionCmd = new ClearSelectionCommand({ + canvas: this.canvas, + selectionManager: this.selectionManager, + }); + + // 执行清除选区命令 + await clearSelectionCmd.execute(); + this.executedCommands.push(clearSelectionCmd); + + console.log(`套索抠图完成,新图层ID: ${this.newLayerId}`); + return { + newLayerId: this.newLayerId, + cutoutImageUrl: this.cutoutImageUrl, + }; + } catch (error) { + console.error("套索抠图过程中出错:", error); + + // 如果已经创建了新图层,需要进行清理 + if (this.newLayerId) { + try { + await this.layerManager.removeLayer(this.newLayerId); + } catch (cleanupError) { + console.warn("清理新图层失败:", cleanupError); + } + } + + throw error; + } + } + + async undo() { + try { + // 逆序撤销所有已执行的命令 + for (let i = this.executedCommands.length - 1; i >= 0; i--) { + const command = this.executedCommands[i]; + if (command && typeof command.undo === "function") { + await command.undo(); + } + } + + this.executedCommands = []; + this.newLayerId = null; + this.cutoutImageUrl = null; + this.fabricImage = null; + + return true; + } catch (error) { + console.error("撤销套索抠图失败:", error); + return false; + } + } + + /** + * 在当前画布上执行抠图操作 + * @param {Object} sourceLayer 源图层 + * @param {Object} selectionObject 选区对象 + * @returns {String} 抠图结果的DataURL + * @private + */ + async _performCutout(sourceLayer, selectionObject) { + try { + console.log("=== 开始在当前画布执行抠图 ==="); + + // 获取选区边界 + const selectionBounds = selectionObject.getBoundingRect(true, true); + console.log( + `选区边界: left=${selectionBounds.left}, top=${selectionBounds.top}, width=${selectionBounds.width}, height=${selectionBounds.height}` + ); + + // 保存画布当前状态 + const originalActiveObject = this.canvas.getActiveObject(); + const originalSelection = this.canvas.selection; + + // 临时禁用画布选择 + this.canvas.selection = false; + this.canvas.discardActiveObject(); + + let tempGroup = null; + let originalObjects = []; + + try { + // 收集源图层中的可见对象 + const visibleObjects = sourceLayer.fabricObjects.filter( + (obj) => obj.visible + ); + console.log(`源图层可见对象数量: ${visibleObjects.length}`); + + if (visibleObjects.length === 0) { + throw new Error("源图层没有可见对象"); + } + + // 如果只有一个对象且已经是组,直接使用 + if (visibleObjects.length === 1 && visibleObjects[0].type === "group") { + tempGroup = visibleObjects[0]; + console.log("使用现有组对象"); + } else { + // 创建临时组 + console.log("创建临时组..."); + + // 记录原始对象的位置和状态,用于后续恢复 + originalObjects = visibleObjects.map((obj) => ({ + object: obj, + originalLeft: obj.left, + originalTop: obj.top, + originalAngle: obj.angle, + originalScaleX: obj.scaleX, + originalScaleY: obj.scaleY, + })); + + // 不需要从画布移除原对象,直接创建组 + // 克隆对象来创建组,避免影响原对象 + const clonedObjects = []; + for (const obj of visibleObjects) { + const cloned = await this._cloneObject(obj); + clonedObjects.push(cloned); + } + + // 创建组 + tempGroup = new fabric.Group(clonedObjects, { + selectable: false, + evented: false, + }); + + // 添加组到画布 + this.canvas.add(tempGroup); + } + + // 设置选区为裁剪路径 + const clipPath = await this._cloneObject(selectionObject); + clipPath.set({ + fill: "", + stroke: "", + absolutePositioned: true, + originX: "left", + originY: "top", + }); + + // 应用裁剪路径到组 + tempGroup.set({ + clipPath: clipPath, + }); + + this.canvas.renderAll(); + + // 计算渲染区域 + const renderBounds = { + left: selectionBounds.left, + top: selectionBounds.top, + width: selectionBounds.width, + height: selectionBounds.height, + }; + + // 设置高分辨率倍数,用于提高图像清晰度 + let highResolutionScale = 1; + + if (this.highResolutionEnabled) { + // 结合设备像素比和配置的基础倍数,确保在所有设备上都有最佳效果 + const devicePixelRatio = window.devicePixelRatio || 1; + // 使用更激进的缩放策略,确保高清晰度 + highResolutionScale = Math.max( + this.baseResolutionScale, + devicePixelRatio * 2 + ); + + console.log( + `设备像素比: ${devicePixelRatio}, 基础倍数: ${this.baseResolutionScale}, 最终放大倍数: ${highResolutionScale}` + ); + } else { + console.log("高分辨率渲染已禁用,使用1x倍数"); + } + + // 创建用于导出的高分辨率canvas + const exportCanvas = document.createElement("canvas"); + const exportCtx = exportCanvas.getContext("2d", { + alpha: true, + willReadFrequently: false, + colorSpace: "srgb", + }); + + // 设置canvas的实际像素尺寸(放大倍数) + const actualWidth = Math.round( + renderBounds.width * highResolutionScale + ); + const actualHeight = Math.round( + renderBounds.height * highResolutionScale + ); + + exportCanvas.width = actualWidth; + exportCanvas.height = actualHeight; + + // 设置canvas的显示尺寸(CSS尺寸,保持与选区一致) + exportCanvas.style.width = renderBounds.width + "px"; + exportCanvas.style.height = renderBounds.height + "px"; + + // 启用最高质量渲染设置 + exportCtx.imageSmoothingEnabled = true; + exportCtx.imageSmoothingQuality = "high"; + + // 设置文本渲染质量 + if (exportCtx.textRenderingOptimization) { + exportCtx.textRenderingOptimization = "optimizeQuality"; + } + + // 设置线条和图形的渲染质量 + exportCtx.lineCap = "round"; + exportCtx.lineJoin = "round"; + exportCtx.miterLimit = 10; + + // 设置画布背景为透明 + exportCtx.clearRect(0, 0, actualWidth, actualHeight); + + // 获取画布当前的变换矩阵 + const vpt = this.canvas.viewportTransform; + const zoom = this.canvas.getZoom(); + + // 保存当前变换状态 + exportCtx.save(); + + // 应用高分辨率缩放 + exportCtx.scale(highResolutionScale, highResolutionScale); + + // 应用偏移,只渲染选区部分 + exportCtx.translate(-renderBounds.left, -renderBounds.top); + + // 如果画布有缩放和平移,需要应用相应变换 + if (zoom !== 1 || vpt[4] !== 0 || vpt[5] !== 0) { + exportCtx.transform(vpt[0], vpt[1], vpt[2], vpt[3], vpt[4], vpt[5]); + } + + // 渲染被裁剪的组 + tempGroup.render(exportCtx); + + exportCtx.restore(); + + // 获取结果 - 使用最高质量设置 + const dataUrl = exportCanvas.toDataURL("image/png", 1.0); + + console.log( + `抠图完成,选区尺寸: ${renderBounds.width}x${renderBounds.height}, 实际渲染尺寸: ${actualWidth}x${actualHeight}, 放大倍数: ${highResolutionScale}x` + ); + + return dataUrl; + } finally { + // 清理和恢复 + if (tempGroup) { + // 移除裁剪路径 + tempGroup.set({ clipPath: null }); + + // 如果是我们创建的临时组,需要移除它 + if (originalObjects.length > 0) { + this.canvas.remove(tempGroup); + + // 恢复原始对象的状态(位置等信息保持不变) + originalObjects.forEach( + ({ + object, + originalLeft, + originalTop, + originalAngle, + originalScaleX, + originalScaleY, + }) => { + // 确保对象仍然在画布上且状态正确 + if (!this.canvas.getObjects().includes(object)) { + this.canvas.add(object); + } + + // 恢复原始变换状态(如果需要的话) + object.set({ + left: originalLeft, + top: originalTop, + angle: originalAngle, + scaleX: originalScaleX, + scaleY: originalScaleY, + }); + object.setCoords(); + } + ); + } + } + + // 恢复画布状态 + this.canvas.selection = originalSelection; + if (originalActiveObject) { + this.canvas.setActiveObject(originalActiveObject); + } + this.canvas.renderAll(); + } + } catch (error) { + console.error("在当前画布执行抠图失败:", error); + return null; + } + } + + /** + * 从DataURL创建fabric图像对象 + * @param {String} dataUrl 图像DataURL + * @param {Object} selectionBounds 选区边界信息 + * @returns {fabric.Image} fabric图像对象 + * @private + */ + async _createFabricImage(dataUrl, selectionBounds) { + return new Promise((resolve, reject) => { + fabric.Image.fromURL( + dataUrl, + (img) => { + if (!img) { + reject(new Error("无法从DataURL创建图像")); + return; + } + + // 计算画布中心位置 + const canvasCenter = this.canvas.getCenter(); + + // 如果有选区边界信息,使用选区的原始位置和尺寸 + let targetLeft = canvasCenter.left; + let targetTop = canvasCenter.top; + let targetWidth = img.width; + let targetHeight = img.height; + + if (selectionBounds) { + // 使用选区的原始位置 + targetLeft = selectionBounds.left + selectionBounds.width / 2; + targetTop = selectionBounds.top + selectionBounds.height / 2; + + // 确保图像显示尺寸与选区尺寸一致 + targetWidth = selectionBounds.width; + targetHeight = selectionBounds.height; + + console.log( + `设置图像位置: left=${targetLeft}, top=${targetTop}, 尺寸: ${targetWidth}x${targetHeight}` + ); + } + + // 计算缩放比例以匹配目标尺寸 + const scaleX = targetWidth / img.width; + const scaleY = targetHeight / img.height; + + // 设置图像属性 + img.set({ + left: targetLeft, + top: targetTop, + scaleX: scaleX, + scaleY: scaleY, + originX: "center", + originY: "center", + selectable: true, + evented: true, + hasControls: true, + hasBorders: true, + cornerStyle: "circle", + cornerColor: "#007aff", + cornerSize: 10, + transparentCorners: false, + borderColor: "#007aff", + borderScaleFactor: 2, + // 优化图像渲染质量 + objectCaching: false, // 禁用缓存以确保最佳质量 + statefullCache: true, + noScaleCache: false, + }); + + // 更新坐标 + img.setCoords(); + + resolve(img); + }, + { + crossOrigin: "anonymous", + // 确保图像以最高质量加载 + quality: 1.0, + } + ); + }); + } + + /** + * 克隆fabric对象 + * @param {Object} obj fabric对象 + * @returns {Object} 克隆的对象 + * @private + */ + async _cloneObject(obj) { + return new Promise((resolve, reject) => { + if (!obj) { + reject(new Error("对象无效,无法克隆")); + return; + } + + try { + obj.clone((cloned) => { + resolve(cloned); + }); + } catch (error) { + reject(error); + } + }); + } +} diff --git a/src/component/Canvas/CanvasEditor/commands/LayerCommands.js b/src/component/Canvas/CanvasEditor/commands/LayerCommands.js new file mode 100644 index 00000000..3a57ecd2 --- /dev/null +++ b/src/component/Canvas/CanvasEditor/commands/LayerCommands.js @@ -0,0 +1,2801 @@ +import { Command } from "./Command"; +import { createLayer, LayerType, OperationType } from "../utils/layerHelper"; +import { createStaticCanvas } from "../utils/canvasFactory"; +import { AddObjectToLayerCommand } from "./ObjectLayerCommands"; +import { ToolCommand } from "./ToolCommands"; + +/** + * 添加图层命令 + */ +export class AddLayerCommand extends Command { + constructor(options) { + super({ + name: "添加图层", + saveState: true, + }); + this.canvas = options.canvas; + this.layers = options.layers; + this.newLayer = options.newLayer; + this.activeLayerId = options.activeLayerId; + + this.insertIndex = options.insertIndex; + this.oldActiveLayerId = null; + this.beforeLayers = [...this.layers.value]; // 备份原图层列表 + } + + execute() { + // 保存当前活动图层ID + this.oldActiveLayerId = this.activeLayerId.value; + + // 执行添加图层操作 + if (this.insertIndex !== undefined && this.insertIndex !== null) { + this.layers.value.splice(this.insertIndex, 0, this.newLayer); + } else { + this.layers.value.push(this.newLayer); + } + + // 更新活动图层 + if (!this.newLayer.isBackground) { + this.activeLayerId.value = this.newLayer.id; + } + + return this.newLayer.id; + } + + undo() { + // 从图层列表删除该图层 + this.layers.value = this.beforeLayers; + + // 恢复原活动图层 + this.activeLayerId.value = this.oldActiveLayerId; + } + + getInfo() { + return { + name: this.name, + layerName: this.newLayer.name, + layerId: this.newLayer.id, + }; + } +} + +/** + * 粘贴并创建图层命令 - 更新为支持异步操作 + */ +export class PasteLayerCommand extends Command { + constructor(options) { + super({ + name: "粘贴图层", + saveState: true, + }); + this.canvas = options.canvas; + this.layers = options.layers; + this.activeLayerId = options.activeLayerId; + this.clipboardData = options.clipboardData; + this.layerManager = options.layerManager; + + // 新图层相关属性 + this.newLayer = null; + this.newLayerId = null; + this.insertIndex = null; + this.oldActiveLayerId = null; + this.createdObjects = []; + } + + async execute() { + if (!this.clipboardData) { + console.error("剪贴板中没有图层数据"); + return null; + } + + const data = this.clipboardData; + const fabric = window.fabric; + + if (!fabric) { + console.error("未找到fabric库"); + return null; + } + + // 生成新的图层ID + this.newLayerId = `layer_${Date.now()}_${Math.floor(Math.random() * 1000)}`; + + // 创建新图层 + this.newLayer = { + ...data, + id: this.newLayerId, + name: `${data.name} 副本`, + fabricObjects: [], + isCut: undefined, + serializedObjects: undefined, + }; + + // 保存当前活动图层ID + this.oldActiveLayerId = this.activeLayerId.value; + + // 计算插入位置 + this.insertIndex = this.layerManager._getInsertIndexAboveActiveLayer(); + + // 执行添加图层操作 + if (this.insertIndex !== undefined && this.insertIndex !== null) { + this.layers.value.splice(this.insertIndex, 0, this.newLayer); + } else { + this.layers.value.push(this.newLayer); + } + + // 更新活动图层 + if (!this.newLayer.isBackground) { + this.activeLayerId.value = this.newLayer.id; + } + + // 如果有序列化的对象,异步恢复它们 + if ( + data.serializedObjects && + Array.isArray(data.serializedObjects) && + data.serializedObjects.length > 0 + ) { + await this._restoreObjectsAsync(data); + } else { + this._onObjectsRestored(data); + } + + return this.newLayerId; + } + + /** + * 异步恢复序列化的对象 + * @param {Object} data 剪贴板数据 + * @private + */ + async _restoreObjectsAsync(data) { + const fabric = window.fabric; + + return new Promise((resolve, reject) => { + fabric.util.enlivenObjects(data.serializedObjects, (objects) => { + try { + objects.forEach((obj) => { + // 生成新的对象ID + const newObjId = `obj_${Date.now()}_${Math.floor( + Math.random() * 1000 + )}`; + obj.id = newObjId; + obj.layerId = this.newLayerId; + obj.layerName = this.newLayer.name; + + // 如果是复制操作,给对象添加偏移量 + if (!data.isCut) { + const offset = 10; + if (obj.left !== undefined) obj.left += offset; + if (obj.top !== undefined) obj.top += offset; + } + + // 添加到画布 + this.canvas.add(obj); + + // 添加到图层 + this.newLayer.fabricObjects.push(obj); + + // 记录创建的对象,用于撤销 + this.createdObjects.push(obj); + }); + + this._onObjectsRestored(data); + resolve(); + } catch (error) { + console.error("恢复对象时发生错误:", error); + reject(error); + } + }); + }); + } + + /** + * 对象恢复完成后的处理 + * @param {Object} data 剪贴板数据 + * @private + */ + _onObjectsRestored(data) { + // 更新对象交互性 + this.layerManager?.updateLayersObjectsInteractivity?.(); + + // 重新排列对象 + this.layerManager?._rearrangeObjects?.(); + + // 判断如果是剪切操作,粘贴完后需要删除剪贴板数据 + if (data.isCut && this.layerManager) { + this.layerManager.clipboardData = null; + console.log(`已粘贴图层:${this.newLayer.name}(剪切)`); + } else { + console.log(`已粘贴图层:${this.newLayer.name}(复制)`); + } + + // 重新渲染画布 + if (this.canvas) { + this.canvas.renderAll(); + } + } + + undo() { + if (!this.newLayer || !this.newLayerId) return; + + // 从图层列表删除该图层 + const index = this.layers.value.findIndex( + (layer) => layer.id === this.newLayerId + ); + if (index !== -1) { + this.layers.value.splice(index, 1); + } + + // 恢复原活动图层 + this.activeLayerId.value = this.oldActiveLayerId; + + // 从画布移除所有创建的对象 + this.createdObjects.forEach((obj) => { + this.canvas.remove(obj); + }); + + // 如果图层有其他fabric对象,也要移除 + if (this.newLayer.fabricObjects && this.newLayer.fabricObjects.length > 0) { + this.newLayer.fabricObjects.forEach((obj) => { + if (!this.createdObjects.includes(obj)) { + this.canvas.remove(obj); + } + }); + } + + // 如果是剪切操作的撤销,需要恢复剪贴板数据 + if (this.clipboardData && this.clipboardData.isCut && this.layerManager) { + this.layerManager.clipboardData = this.clipboardData; + } + + // 重新渲染画布 + if (this.canvas) { + this.canvas.renderAll(); + } + + // 更新对象交互性 + if ( + this.layerManager && + typeof this.layerManager.updateLayersObjectsInteractivity === "function" + ) { + this.layerManager.updateLayersObjectsInteractivity(); + } + } + + getInfo() { + return { + name: this.name, + layerName: this.newLayer?.name || "未知图层", + layerId: this.newLayerId, + objectCount: this.createdObjects.length, + }; + } +} + +/** + * 移除图层命令 + */ +export class RemoveLayerCommand extends Command { + constructor(options) { + super({ + name: "移除图层", + saveState: true, + }); + this.canvas = options.canvas; + this.layers = options.layers; + this.layerId = options.layerId; + this.activeLayerId = options.activeLayerId; + + // 查找要删除的图层 + this.layerIndex = this.layers.value.findIndex( + (layer) => layer.id === this.layerId + ); + this.removedLayer = this.layers.value[this.layerIndex]; + this.isActiveLayer = this.layerId === this.activeLayerId.value; + // this.beforeLayers = [...this.layers.value]; // 备份原图层列表 + } + + execute() { + if (this.layerIndex === -1 || !this.removedLayer) { + console.error(`图层 ${this.layerId} 不存在`); + return false; + } + + // 从画布中移除图层中的所有对象 + if ( + this.removedLayer.fabricObjects && + this.removedLayer.fabricObjects.length > 0 + ) { + this.removedLayer.fabricObjects.forEach((obj) => { + this.canvas.remove(obj); + }); + } + + // 如果是背景图层,移除特殊对象 + if (this.removedLayer.isBackground && this.removedLayer.fabricObject) { + this.canvas.remove(this.removedLayer.fabricObject); + } + + // 从图层列表中删除 + this.layers.value.splice(this.layerIndex, 1); + + // 如果删除的是当前活动图层,需要更新活动图层 + if (this.isActiveLayer) { + // 查找最近的非背景层作为新的活动图层 + const newActiveLayer = this.layers.value.find( + (layer) => !layer.isBackground + ); + if (newActiveLayer) { + this.activeLayerId.value = newActiveLayer.id; + } else { + this.activeLayerId.value = null; + } + } + + // 重新渲染画布 + if (this.canvas) { + this.canvas.renderAll(); + } + + return true; + } + + undo() { + // 恢复图层 + if (this.layerIndex !== -1 && this.removedLayer) { + this.layers.value.splice(this.layerIndex, 0, this.removedLayer); + + // 恢复图层中的所有对象到画布 + if ( + this.removedLayer.fabricObjects && + this.removedLayer.fabricObjects.length > 0 + ) { + this.removedLayer.fabricObjects.forEach((obj) => { + this.canvas.add(obj); + }); + } + + // 如果是背景图层,恢复特殊对象 + if (this.removedLayer.isBackground && this.removedLayer.fabricObject) { + this.canvas.add(this.removedLayer.fabricObject); + } + + // 如果删除的是当前活动图层,恢复活动图层 + if (this.isActiveLayer) { + this.activeLayerId.value = this.layerId; + } + + // 重新渲染画布 + if (this.canvas) { + this.canvas.renderAll(); + } + } + } + + getInfo() { + return { + name: this.name, + layerName: this.removedLayer?.name || "未知图层", + layerId: this.layerId, + }; + } +} + +/** + * 移动图层命令 + */ +export class MoveLayerCommand extends Command { + constructor(options) { + super({ + name: `移动图层 ${options.direction === "up" ? "上移" : "下移"}`, + saveState: false, + }); + this.canvas = options.canvas; + this.layers = options.layers; + this.layerId = options.layerId; + this.direction = options.direction; // "up" or "down" + + // 查找图层索引 + this.layerIndex = this.layers.value.findIndex( + (layer) => layer.id === this.layerId + ); + // this.beforeLayers = [...this.layers.value]; // 备份原图层列表 + } + + execute() { + if (this.layerIndex === -1) { + console.error(`图层 ${this.layerId} 不存在`); + return false; + } + + const layer = this.layers.value[this.layerIndex]; + let newIndex = this.layerIndex; + + // 计算新索引 + if (this.direction === "up") { + // 向上移动(在数组中向前移动) + if (this.layerIndex > 0) { + // 确保不会移动到背景层之前 + const prevLayer = this.layers.value[this.layerIndex - 1]; + if (!prevLayer.isBackground) { + newIndex = this.layerIndex - 1; + } + } + } else if (this.direction === "down") { + // 向下移动(在数组中向后移动) + if (this.layerIndex < this.layers.value.length - 1) { + // 确保不会移动背景层 + if (!layer.isBackground) { + newIndex = this.layerIndex + 1; + } + } + } + + // 如果位置有变化,执行移动 + if (newIndex !== this.layerIndex) { + // 移除原位置 + this.layers.value.splice(this.layerIndex, 1); + // 插入到新位置 + this.layers.value.splice(newIndex, 0, layer); + this.newIndex = newIndex; + + return true; + } + + return false; + } + + undo() { + if (this.layerIndex !== this.newIndex && this.newIndex !== undefined) { + // 获取图层 + const layer = this.layers.value[this.newIndex]; + + // 移除新位置 + this.layers.value.splice(this.newIndex, 1); + + // 插入到原位置 + this.layers.value.splice(this.layerIndex, 0, layer); + } + } + + getInfo() { + return { + name: this.name, + layerId: this.layerId, + direction: this.direction, + }; + } +} + +/** + * 切换图层可见性命令 + */ +export class ToggleLayerVisibilityCommand extends Command { + constructor(options) { + super({ + name: "切换图层可见性", + saveState: false, + }); + this.canvas = options.canvas; + this.layers = options.layers; + this.layerId = options.layerId; + + // 查找图层 + this.layer = this.layers.value.find((layer) => layer.id === this.layerId); + this.oldVisibility = this.layer ? this.layer.visible : null; + } + + execute() { + if (!this.layer) { + console.error(`图层 ${this.layerId} 不存在`); + return false; + } + + // 切换可见性 + this.layer.visible = !this.oldVisibility; + + // 更新画布上图层对象的可见性 + if (this.canvas) { + const layerObjects = this.canvas + .getObjects() + .filter((obj) => obj.layerId === this.layerId); + + layerObjects.forEach((obj) => { + obj.visible = this.layer.visible; + }); + + this.canvas.renderAll(); + } + + return true; + } + + undo() { + if (this.layer) { + // 恢复可见性 + this.layer.visible = this.oldVisibility; + + // 更新画布上图层对象的可见性 + if (this.canvas) { + const layerObjects = this.canvas + .getObjects() + .filter((obj) => obj.layerId === this.layerId); + + layerObjects.forEach((obj) => { + obj.visible = this.oldVisibility; + }); + + this.canvas.renderAll(); + } + } + } + + getInfo() { + return { + name: this.name, + layerName: this.layer?.name || "未知图层", + layerId: this.layerId, + newVisibility: this.layer?.visible, + }; + } +} + +/** + * 重命名图层命令 + */ +export class RenameLayerCommand extends Command { + constructor(options) { + super({ + name: "重命名图层", + saveState: false, + }); + this.canvas = options.canvas; + this.layers = options.layers; + this.layerId = options.layerId; + this.newName = options.newName; + + // 查找图层 + this.layer = this.layers.value.find((layer) => layer.id === this.layerId); + this.oldName = this.layer ? this.layer.name : null; + } + + execute() { + if (!this.layer) { + console.error(`图层 ${this.layerId} 不存在`); + return false; + } + + // 更新图层名称 + this.layer.name = this.newName; + + // 更新图层对象上的图层名称 + if (this.canvas) { + const layerObjects = this.canvas + .getObjects() + .filter((obj) => obj.layerId === this.layerId); + + layerObjects.forEach((obj) => { + obj.layerName = this.newName; + }); + } + + return true; + } + + undo() { + if (this.layer && this.oldName) { + // 恢复图层名称 + this.layer.name = this.oldName; + + // 恢复图层对象上的图层名称 + if (this.canvas) { + const layerObjects = this.canvas + .getObjects() + .filter((obj) => obj.layerId === this.layerId); + + layerObjects.forEach((obj) => { + obj.layerName = this.oldName; + }); + } + } + } + + getInfo() { + return { + name: this.name, + layerId: this.layerId, + oldName: this.oldName, + newName: this.newName, + }; + } +} + +/** + * 图层锁定/解锁命令 + */ +export class LayerLockCommand extends Command { + constructor(options) { + super({ + name: "图层锁定/解锁", + saveState: false, + }); + this.canvas = options.canvas; + this.layers = options.layers; + this.layerId = options.layerId; + + // 查找图层 + this.layer = this.layers.value.find((layer) => layer.id === this.layerId); + this.oldLocked = this.layer ? this.layer.locked : null; + } + + execute() { + if (!this.layer) { + console.error(`图层 ${this.layerId} 不存在`); + return false; + } + + // 切换锁定状态 + this.layer.locked = !this.oldLocked; + + // 更新画布上对象的可选择状态 + if (this.canvas) { + const layerObjects = this.canvas + .getObjects() + .filter((obj) => obj.layerId === this.layerId); + + layerObjects.forEach((obj) => { + obj.selectable = !this.layer.locked && this.layer.visible; + obj.evented = !this.layer.locked && this.layer.visible; + }); + + this.canvas.renderAll(); + } + + return true; + } + + undo() { + if (this.layer) { + // 恢复锁定状态 + this.layer.locked = this.oldLocked; + + // 更新画布上对象的可选择状态 + if (this.canvas) { + const layerObjects = this.canvas + .getObjects() + .filter((obj) => obj.layerId === this.layerId); + + layerObjects.forEach((obj) => { + obj.selectable = !this.oldLocked && this.layer.visible; + obj.evented = !this.oldLocked && this.layer.visible; + }); + + this.canvas.renderAll(); + } + } + } + + getInfo() { + return { + name: this.name, + layerName: this.layer?.name || "未知图层", + layerId: this.layerId, + newLocked: this.layer?.locked, + }; + } +} + +/** + * 设置图层不透明度命令 + */ +export class SetLayerOpacityCommand extends Command { + constructor(options) { + super({ + name: "设置图层不透明度", + saveState: false, + }); + this.canvas = options.canvas; + this.layers = options.layers; + this.layerId = options.layerId; + this.opacity = options.opacity; + + // 查找图层 + this.layer = this.layers.value.find((layer) => layer.id === this.layerId); + this.oldOpacity = this.layer ? this.layer.opacity : null; + } + + execute() { + if (!this.layer) { + console.error(`图层 ${this.layerId} 不存在`); + return false; + } + + // 设置图层不透明度 + this.layer.opacity = this.opacity; + + // 更新画布上对象的不透明度 + if (this.canvas) { + const layerObjects = this.canvas + .getObjects() + .filter((obj) => obj.layerId === this.layerId); + + layerObjects.forEach((obj) => { + obj.opacity = this.opacity; + }); + + this.canvas.renderAll(); + } + + return true; + } + + undo() { + if (this.layer && this.oldOpacity !== null) { + // 恢复图层不透明度 + this.layer.opacity = this.oldOpacity; + + // 更新画布上对象的不透明度 + if (this.canvas) { + const layerObjects = this.canvas + .getObjects() + .filter((obj) => obj.layerId === this.layerId); + + layerObjects.forEach((obj) => { + obj.opacity = this.oldOpacity; + }); + + this.canvas.renderAll(); + } + } + } + + getInfo() { + return { + name: this.name, + layerName: this.layer?.name || "未知图层", + layerId: this.layerId, + oldOpacity: this.oldOpacity, + newOpacity: this.opacity, + }; + } +} + +/** + * 设置图层混合模式命令 + */ +export class SetLayerBlendModeCommand extends Command { + constructor(options) { + super({ + name: "设置图层混合模式", + saveState: false, + }); + this.canvas = options.canvas; + this.layers = options.layers; + this.layerId = options.layerId; + this.blendMode = options.blendMode; + + // 查找图层 + this.layer = this.layers.value.find((layer) => layer.id === this.layerId); + this.oldBlendMode = this.layer ? this.layer.blendMode : null; + } + + execute() { + if (!this.layer) { + console.error(`图层 ${this.layerId} 不存在`); + return false; + } + + // 设置图层混合模式 + this.layer.blendMode = this.blendMode; + + // 更新画布上对象的混合模式 + if (this.canvas) { + const layerObjects = this.canvas + .getObjects() + .filter((obj) => obj.layerId === this.layerId); + + layerObjects.forEach((obj) => { + obj.globalCompositeOperation = this.blendMode; + }); + + this.canvas.renderAll(); + } + + return true; + } + + undo() { + if (this.layer && this.oldBlendMode) { + // 恢复图层混合模式 + this.layer.blendMode = this.oldBlendMode; + + // 更新画布上对象的混合模式 + if (this.canvas) { + const layerObjects = this.canvas + .getObjects() + .filter((obj) => obj.layerId === this.layerId); + + layerObjects.forEach((obj) => { + obj.globalCompositeOperation = this.oldBlendMode; + }); + + this.canvas.renderAll(); + } + } + } + + getInfo() { + return { + name: this.name, + layerName: this.layer?.name || "未知图层", + layerId: this.layerId, + oldBlendMode: this.oldBlendMode, + newBlendMode: this.blendMode, + }; + } +} + +/** + * 合并图层命令 + */ +export class MergeLayersCommand extends Command { + constructor(options) { + super({ + name: "合并图层", + saveState: true, + }); + this.canvas = options.canvas; + this.layers = options.layers; + this.layerIds = options.layerIds; + this.newName = options.newName; + this.activeLayerId = options.activeLayerId; // <--- 新增 + + // 备份原图层 + this.originalLayers = [...this.layers.value]; + // 新图层ID + this.newLayerId = `merged_layer_${Date.now()}_${Math.floor( + Math.random() * 1000 + )}`; + } + + execute() { + if ( + !this.layerIds || + !Array.isArray(this.layerIds) || + this.layerIds.length < 2 + ) { + console.error("合并图层至少需要两个图层"); + return null; + } + + // 查找所有要合并的图层 + const layersToMerge = this.layerIds + .map((id) => this.layers.value.find((layer) => layer.id === id)) + .filter(Boolean); + + if (layersToMerge.length < 2) { + console.error("找不到足够的图层进行合并"); + return null; + } + + // 检查是否包含背景图层 + if (layersToMerge.some((layer) => layer.isBackground)) { + console.error("不能合并背景图层"); + return null; + } + + // 查找最顶层图层的索引,用于插入合并后的图层 + const topLayerIndex = Math.min( + ...layersToMerge.map((layer) => this.layers.value.indexOf(layer)) + ); + + // 获取要保留的所有对象 + const allObjects = []; + layersToMerge.forEach((layer) => { + if (Array.isArray(layer.fabricObjects)) { + allObjects.push(...layer.fabricObjects); + } + }); + + // 创建新的合并图层 + const mergedLayer = createLayer({ + id: this.newLayerId, + name: this.newName || `合并图层`, + type: LayerType.BITMAP, + visible: true, + locked: false, + opacity: 1.0, + fabricObjects: allObjects, + }); + + // 更新对象与新图层的关联 + allObjects.forEach((obj) => { + obj.layerId = mergedLayer.id; + obj.layerName = mergedLayer.name; + }); + + // 移除原图层 + this.layers.value = this.layers.value.filter( + (layer) => !this.layerIds.includes(layer.id) + ); + + // 插入新图层 + this.layers.value.splice(topLayerIndex, 0, mergedLayer); + + // 更新当前活动图层 + this.activeLayerId.value = this.newLayerId; + + // 重新渲染画布 + if (this.canvas) { + this.canvas.renderAll(); + } + + return this.newLayerId; + } + + undo() { + // 恢复原始图层状态 + this.layers.value = [...this.originalLayers]; + + // 恢复活动图层 + if (this.activeLayerId) { + // 恢复到合并前的活动图层(取第一个合并前图层) + this.activeLayerId.value = this.layerIds[0]; + } + + // 重新渲染画布 + if (this.canvas) { + this.canvas.renderAll(); + } + } + + getInfo() { + return { + name: this.name, + layerIds: this.layerIds, + newLayerId: this.newLayerId, + newName: this.newName, + }; + } +} + +/** + * 图层组合命令 + */ +export class GroupLayersCommand extends Command { + constructor(options) { + super({ + name: "组合图层", + saveState: true, + }); + this.canvas = options.canvas; + this.layers = options.layers; + this.layerIds = options.layerIds; + this.groupName = options.groupName; + this.activeLayerId = options.activeLayerId; // <--- 新增 + + // 备份原图层 + this.originalLayers = [...this.layers.value]; + // 新组ID + this.groupId = `group_layer_${Date.now()}_${Math.floor( + Math.random() * 1000 + )}`; + } + + execute() { + if ( + !this.layerIds || + !Array.isArray(this.layerIds) || + this.layerIds.length < 2 + ) { + console.error("组合图层至少需要两个图层"); + return null; + } + + // 查找所有要组合的图层 + const layersToGroup = this.layerIds + .map((id) => this.layers.value.find((layer) => layer.id === id)) + .filter(Boolean); + + if (layersToGroup.length < 2) { + console.error("找不到足够的图层进行组合"); + return null; + } + + // 检查是否包含背景图层 + if (layersToGroup.some((layer) => layer.isBackground)) { + console.error("不能组合背景图层"); + return null; + } + + // 查找最顶层图层的索引,用于插入组图层 + const topLayerIndex = Math.min( + ...layersToGroup.map((layer) => this.layers.value.indexOf(layer)) + ); + + // 创建新的组图层 + const groupLayer = createLayer({ + id: this.groupId, + name: this.groupName || `图层组`, + type: LayerType.GROUP, + visible: true, + locked: false, + opacity: 1.0, + fabricObjects: [], + children: layersToGroup, + }); + + // 移除原图层 + this.layers.value = this.layers.value.filter( + (layer) => !this.layerIds.includes(layer.id) + ); + + // 插入新组图层 + this.layers.value.splice(topLayerIndex, 0, groupLayer); + + // 更新当前活动图层 + this.activeLayerId.value = this.groupId; + + return this.groupId; + } + + undo() { + // 恢复原始图层状态 + this.layers.value = [...this.originalLayers]; + // 恢复活动图层为原先第一个合并前层 + if (this.activeLayerId) { + this.activeLayerId.value = this.layerIds[0]; + } + } + + getInfo() { + return { + name: this.name, + layerIds: this.layerIds, + groupId: this.groupId, + groupName: this.groupName, + }; + } +} + +/** + * 解组图层命令 + */ +export class UngroupLayersCommand extends Command { + constructor(options) { + super({ + name: "解组图层", + saveState: true, + }); + this.canvas = options.canvas; + this.layers = options.layers; + this.groupId = options.groupId; + this.activeLayerId = options.activeLayerId; // <--- 新增 + + // 备份原图层 + this.originalLayers = [...this.layers.value]; + // 子图层ID列表 + this.childLayerIds = []; + } + + execute() { + // 查找组图层 + const groupIndex = this.layers.value.findIndex( + (layer) => layer.id === this.groupId + ); + + if (groupIndex === -1) { + console.error(`找不到组图层 ${this.groupId}`); + return null; + } + + const groupLayer = this.layers.value[groupIndex]; + + if (!groupLayer.children || groupLayer.children.length === 0) { + console.error(`组图层 ${this.groupId} 没有子图层`); + return null; + } + + // 收集子图层ID + this.childLayerIds = groupLayer.children.map((layer) => layer.id); + + // 将子图层添加到原位置 + this.layers.value.splice(groupIndex, 1, ...groupLayer.children); + + // 更新当前活动图层为第一个子图层 + if (this.childLayerIds.length > 0 && this.activeLayerId) { + this.activeLayerId.value = this.childLayerIds[0]; + } + + return this.childLayerIds; + } + + undo() { + // 恢复原始图层状态 + this.layers.value = [...this.originalLayers]; + // 恢复活动图层为原始组ID + if (this.activeLayerId) { + this.activeLayerId.value = this.groupId; + } + } + + getInfo() { + return { + name: this.name, + groupId: this.groupId, + childLayerIds: this.childLayerIds, + }; + } +} + +/** + * 合并图层内对象命令(重构版本) + * 将新的图像与图层内现有对象合并为一个高保真图像对象 + */ +export class MergeLayerObjectsCommand extends Command { + constructor(options) { + super({ + name: "合并图层内对象", + saveState: true, + }); + this.canvas = options.canvas; + this.layers = options.layers; + this.fabricImage = options.fabricImage; + this.activeLayer = options.activeLayer; + + // 备份原始对象,用于撤销 + if (this.activeLayer && Array.isArray(this.activeLayer.fabricObjects)) { + this.originalObjects = + this.canvas + ?.getObjects() + ?.filter((fItem) => fItem.layerId === this.activeLayer.id) || []; + } else { + this.originalObjects = []; + } + + // 新合并图像对象 + this.mergedImage = null; + this.newImageId = `merged_image_${Date.now()}_${Math.floor( + Math.random() * 1000 + )}`; + } + + async execute() { + if (!this.activeLayer || !this.canvas) { + console.error("图层或Canvas未初始化"); + return null; + } + + // 获取所有需要合并的对象(按正确的层级顺序) + const objectsToMerge = []; + + // 先添加图层中的现有对象(作为底层) + if (this.originalObjects.length > 0) { + objectsToMerge.push(...this.originalObjects); + } + + // 再添加新的图像对象(作为顶层) + if (this.fabricImage) { + objectsToMerge.push(this.fabricImage); + } + + if (objectsToMerge.length === 0) { + console.log("没有对象需要合并"); + return null; + } + + // 计算所有对象的合并边界 + const bounds = this._calculateMergedBounds(objectsToMerge); + if (!bounds) { + console.error("无法计算合并边界"); + return null; + } + + // 异步处理图像合并 + try { + const mergedImage = await this._createMergedImageAsync( + objectsToMerge, + bounds + ); + this._setupMergedImage(mergedImage, bounds); + this._replaceObjects(mergedImage); + + console.log("图像合并完成"); + return this.newImageId; + } catch (error) { + console.error("图像合并执行失败:", error); + return null; + } + } + + /** + * 异步创建合并图像 + * @param {Array} objectsToMerge 要合并的对象 + * @param {Object} bounds 边界信息 + * @returns {Promise} 合并后的图像 + * @private + */ + async _createMergedImageAsync(objectsToMerge, bounds) { + return new Promise((resolve, reject) => { + try { + // 创建高保真临时画布 + const tempCanvas = this._createHighQualityTempCanvas(bounds); + const tempFabricCanvas = createStaticCanvas(tempCanvas, { + width: bounds.width, + height: bounds.height, + }); + + // 设置高质量渲染选项 + tempFabricCanvas.enableRetinaScaling = true; + tempFabricCanvas.imageSmoothingEnabled = true; + + // 将所有对象添加到临时画布并调整位置 + this._addObjectsToTempCanvas(tempFabricCanvas, objectsToMerge, bounds); + + // 渲染临时画布 + tempFabricCanvas.renderAll(); + + // 生成高质量图像 + const dataUrl = tempFabricCanvas.toDataURL({ + format: "png", + quality: 1.0, + multiplier: 1, + }); + + // 创建新的合并图像 + fabric.Image.fromURL( + dataUrl, + (mergedImg) => { + try { + // 清理临时资源 + this._cleanupTempCanvas(tempFabricCanvas); + resolve(mergedImg); + } catch (error) { + console.error("设置合并图像时发生错误:", error); + this._cleanupTempCanvas(tempFabricCanvas); + reject(error); + } + }, + { + crossOrigin: "anonymous", + } + ); + } catch (error) { + console.error("合并过程中发生错误:", error); + reject(error); + } + }); + } + + /** + * 计算所有对象的合并边界 + * @param {Array} objects 要合并的对象数组 + * @returns {Object} 边界信息 + * @private + */ + _calculateMergedBounds(objects) { + if (!objects || objects.length === 0) return null; + + let minLeft = Infinity; + let minTop = Infinity; + let maxRight = -Infinity; + let maxBottom = -Infinity; + + objects.forEach((obj) => { + if (!obj) return; + + const bounds = obj.getBoundingRect(true, true); + minLeft = Math.min(minLeft, bounds.left); + minTop = Math.min(minTop, bounds.top); + maxRight = Math.max(maxRight, bounds.left + bounds.width); + maxBottom = Math.max(maxBottom, bounds.top + bounds.height); + }); + + const padding = 1; + + return { + left: minLeft - padding, + top: minTop - padding, + width: maxRight - minLeft + padding * 2, + height: maxBottom - minTop + padding * 2, + }; + } + + /** + * 创建高质量临时画布 + * @param {Object} bounds 边界信息 + * @returns {HTMLCanvasElement} 临时画布 + * @private + */ + _createHighQualityTempCanvas(bounds) { + const tempCanvas = document.createElement("canvas"); + tempCanvas.width = Math.ceil(bounds.width); + tempCanvas.height = Math.ceil(bounds.height); + tempCanvas.style.width = bounds.width + "px"; + tempCanvas.style.height = bounds.height + "px"; + + const ctx = tempCanvas.getContext("2d"); + ctx.imageSmoothingEnabled = true; + ctx.imageSmoothingQuality = "high"; + + return tempCanvas; + } + + /** + * 将对象添加到临时画布并调整位置 + * @param {fabric.Canvas} tempCanvas 临时画布 + * @param {Array} objects 对象数组 + * @param {Object} bounds 边界信息 + * @private + */ + _addObjectsToTempCanvas(tempCanvas, objects, bounds) { + objects.forEach((obj, index) => { + if (!obj) return; + + try { + const clonedObj = fabric.util.object.clone(obj); + const objBounds = obj.getBoundingRect(true, true); + const offsetX = objBounds.left - bounds.left; + const offsetY = objBounds.top - bounds.top; + + clonedObj.set({ + left: offsetX + (clonedObj.width * clonedObj.scaleX) / 2, + top: offsetY + (clonedObj.height * clonedObj.scaleY) / 2, + originX: "center", + originY: "center", + }); + + tempCanvas.add(clonedObj); + console.log( + `添加对象 ${index + 1}/${objects.length}: ${obj.type || "unknown"}` + ); + } catch (error) { + console.error(`添加对象到临时画布时发生错误:`, error); + } + }); + } + + /** + * 设置合并后的图像属性 + * @param {fabric.Image} mergedImg 合并后的图像 + * @param {Object} bounds 边界信息 + * @private + */ + _setupMergedImage(mergedImg, bounds) { + mergedImg.set({ + id: this.newImageId, + layerId: this.activeLayer.id, + layerName: this.activeLayer.name, + left: bounds.left + bounds.width / 2, + top: bounds.top + bounds.height / 2, + originX: "center", + originY: "center", + selectable: true, + evented: true, + }); + + this.mergedImage = mergedImg; + } + + /** + * 替换原有对象为合并后的图像 + * @param {fabric.Image} mergedImg 合并后的图像 + * @private + */ + _replaceObjects(mergedImg) { + if (!mergedImg || !this.canvas || !this.activeLayer) { + console.error("_replaceObjects: 缺少必要的参数"); + return; + } + + const wasRenderOnAddRemove = this.canvas.renderOnAddRemove; + this.canvas.renderOnAddRemove = false; + + try { + // 清空图层的对象列表 + this.activeLayer.fabricObjects = []; + + // 添加合并后的图像到画布和图层 + this.canvas.add(mergedImg); + this.activeLayer.fabricObjects.push(mergedImg); + + // 从画布中移除所有原始对象 + this.originalObjects.forEach((obj) => { + if (obj && this.canvas.getObjects().includes(obj)) { + this.canvas.remove(obj); + } + }); + + // 如果有新的图像对象,也要移除 + if ( + this.fabricImage && + this.canvas.getObjects().includes(this.fabricImage) + ) { + this.canvas.remove(this.fabricImage); + } + + console.log( + `成功替换图层 ${this.activeLayer.name} 中的 ${this.originalObjects.length} 个对象为合并图像` + ); + } catch (error) { + console.error("替换对象时发生错误:", error); + } finally { + this.canvas.renderOnAddRemove = wasRenderOnAddRemove; + this.canvas.renderAll(); // 同步渲染画布 + + // 更新缩略图 + if (this.canvas.thumbnailManager) { + setTimeout(() => { + this.canvas.thumbnailManager.generateLayerThumbnail( + this.activeLayer.id + ); + }, 100); + } + } + } + + undo() { + if (!this.activeLayer || !this.canvas) return; + + const wasRenderOnAddRemove = this.canvas.renderOnAddRemove; + this.canvas.renderOnAddRemove = false; + + try { + // 移除合并后的图像 + if (this.mergedImage) { + this.canvas.remove(this.mergedImage); + + const imageIndex = this.activeLayer.fabricObjects.findIndex( + (obj) => obj.id === this.newImageId + ); + if (imageIndex !== -1) { + this.activeLayer.fabricObjects.splice(imageIndex, 1); + } + } + + // 按原始顺序恢复对象到画布 + this.originalObjects.forEach((obj) => { + if (obj) { + this.canvas.add(obj); + } + }); + + // 恢复图层对象列表 + this.activeLayer.fabricObjects = [...this.originalObjects]; + console.log(`成功撤销图层 ${this.activeLayer.name} 的对象合并操作`); + } catch (error) { + console.error("撤销合并操作时发生错误:", error); + } finally { + this.canvas.renderOnAddRemove = wasRenderOnAddRemove; + this.canvas.renderAll(); + + // 更新缩略图 + if (this.canvas.thumbnailManager) { + setTimeout(() => { + this.canvas.thumbnailManager.generateLayerThumbnail( + this.activeLayer.id + ); + }, 100); + } + } + } + + /** + * 安全地清理临时画布资源 + * @param {fabric.Canvas} tempCanvas 临时画布 + * @private + */ + _cleanupTempCanvas(tempCanvas) { + if (!tempCanvas) return; + + try { + tempCanvas.clear(); + + const objects = tempCanvas.getObjects(); + objects.forEach((obj) => { + tempCanvas.remove(obj); + }); + + const canvasEl = tempCanvas.getElement(); + if (canvasEl && canvasEl.getContext) { + const ctx = canvasEl.getContext("2d"); + if (ctx) { + ctx.clearRect(0, 0, canvasEl.width, canvasEl.height); + } + } + + if (typeof tempCanvas.destroy === "function") { + tempCanvas.destroy(); + } + + if (canvasEl && canvasEl.parentNode) { + canvasEl.parentNode.removeChild(canvasEl); + } + + console.log("临时画布资源已安全清理"); + } catch (error) { + console.warn("清理临时画布时发生警告:", error); + } + } + + getInfo() { + return { + name: this.name, + layerId: this.activeLayer?.id, + layerName: this.activeLayer?.name || "未知图层", + originalObjectCount: this.originalObjects.length, + hasNewImage: !!this.fabricImage, + mergedImageId: this.newImageId, + }; + } +} + +/** + * 合并图层内对象成组的命令 + * 将新的图像与图层内现有对象合并为一个组对象 + * 支持向现有组添加对象,以及移除空组 + */ +export class LayerObjectsToGroupCommand extends Command { + constructor(options) { + super({ + name: "图层内对象合并为组", + saveState: true, + }); + this.canvas = options.canvas; + this.layers = options.layers; + this.fabricImage = options.fabricImage; + this.activeLayer = options.activeLayer; + + // 备份原始对象,用于撤销 + if (this.activeLayer && Array.isArray(this.activeLayer.fabricObjects)) { + this.originalObjects = + this.canvas + ?.getObjects() + ?.filter((fItem) => fItem.layerId === this.activeLayer.id) || []; + } else { + this.originalObjects = []; + } + + // 组对象相关 + this.existingGroup = null; // 现有的组对象 + this.groupObject = null; // 最终的组对象(可能是现有的或新创建的) + this.newGroupId = `group_${Date.now()}_${Math.floor(Math.random() * 1000)}`; + this.wasGroupCreated = false; // 是否创建了新组 + this.addedObjectsToGroup = []; // 添加到组中的新对象 + this.originalLayerObjects = [...(this.activeLayer.fabricObjects || [])]; // 备份图层原始对象列表 + } + + async execute() { + if (!this.activeLayer || !this.canvas) { + console.error("图层或Canvas未初始化"); + return null; + } + + // 查找图层中是否已有组对象 + this.existingGroup = this._findExistingGroup(); + + // 准备要添加的新对象 + const newObjectsToAdd = []; + if (this.fabricImage) { + newObjectsToAdd.push(this.fabricImage); + } + + if (newObjectsToAdd.length === 0) { + console.log("没有新对象需要添加到组"); + return this.existingGroup?.id || null; + } + + try { + // 暂时禁用画布自动渲染 + const wasRenderOnAddRemove = this.canvas.renderOnAddRemove; + this.canvas.renderOnAddRemove = false; + + if (this.existingGroup) { + // 如果已有组,将新对象添加到现有组中 + this.groupObject = this.existingGroup; + this._addObjectsToExistingGroup(newObjectsToAdd); + console.log(`✅ 将 ${newObjectsToAdd.length} 个对象添加到现有组中`); + } else { + // 如果没有组,创建新组 + this._createNewGroupWithAllObjects(newObjectsToAdd); + this.wasGroupCreated = true; + console.log( + `✅ 创建新组并添加 ${ + this.originalObjects.length + newObjectsToAdd.length + } 个对象` + ); + } + + // 恢复画布自动渲染并重新渲染 + this.canvas.renderOnAddRemove = wasRenderOnAddRemove; + this.canvas.renderAll(); + + // 选中组对象 + // this.canvas.setActiveObject(this.groupObject); + + // 更新缩略图 + this._updateThumbnail(); + + return this.groupObject.id; + } catch (error) { + console.error("执行组合操作时发生错误:", error); + return null; + } + } + + /** + * 查找图层中现有的组对象 + * @returns {fabric.Group|null} + * @private + */ + _findExistingGroup() { + if (!this.activeLayer.fabricObjects) return null; + + return ( + this.activeLayer.fabricObjects.find( + (obj) => obj && obj.type === "group" + ) || null + ); + } + + /** + * 将新对象添加到现有组中 + * @param {Array} newObjects 要添加的新对象 + * @private + */ + _addObjectsToExistingGroup(newObjects) { + // 先从画布移除新对象(避免重复添加) + newObjects.forEach((obj) => { + if (this.canvas.getObjects().includes(obj)) { + this.canvas.remove(obj); + } + }); + + // 使用 addWithUpdate 方法正确添加对象到组 + newObjects.forEach((obj) => { + this.existingGroup.addWithUpdate(obj); + }); + + // 记录添加的对象,用于撤销 + this.addedObjectsToGroup = newObjects; + + // 组对象引用不变,只是内容更新了 + this.groupObject = this.existingGroup; + } + + /** + * 创建包含所有对象的新组 + * @param {Array} newObjects 新添加的对象 + * @private + */ + _createNewGroupWithAllObjects(newObjects) { + const allObjects = [...this.originalObjects, ...newObjects]; + + // 从画布中移除所有要组合的对象 + allObjects.forEach((obj) => { + if (obj && this.canvas.getObjects().includes(obj)) { + this.canvas.remove(obj); + } + }); + + // 创建组对象 - Fabric.js 5 中创建组时会自动将对象从画布移除 + // 不需要手动从画布中移除对象 + this.groupObject = new fabric.Group(allObjects, { + id: this.newGroupId, + layerId: this.activeLayer.id, + layerName: this.activeLayer.name, + selectable: true, + evented: true, + }); + + // 添加组对象到画布 + this.canvas.add(this.groupObject); + + // 更新图层的对象列表 - 只包含组对象 + this.activeLayer.fabricObjects = [this.groupObject]; + + // 记录添加的对象 + this.addedObjectsToGroup = newObjects; + } + + undo() { + if (!this.activeLayer || !this.canvas || !this.groupObject) return; + + try { + // 暂时禁用画布自动渲染 + const wasRenderOnAddRemove = this.canvas.renderOnAddRemove; + this.canvas.renderOnAddRemove = false; + + if (this.wasGroupCreated) { + // 如果是新创建的组,完全撤销到原始状态 + this._undoNewGroupCreation(); + } else { + // 如果是向现有组添加对象,只移除新添加的对象 + this._undoAddToExistingGroup(); + } + + // 恢复画布自动渲染并重新渲染 + this.canvas.renderOnAddRemove = wasRenderOnAddRemove; + this.canvas.renderAll(); + + // 更新缩略图 + this._updateThumbnail(); + + console.log(`↩️ 成功撤销图层 ${this.activeLayer.name} 的对象组合操作`); + } catch (error) { + console.error("撤销组合操作时发生错误:", error); + } + } + + /** + * 撤销新组的创建 + * @private + */ + _undoNewGroupCreation() { + // 从画布中移除组对象 + if (this.canvas.getObjects().includes(this.groupObject)) { + this.canvas.remove(this.groupObject); + } + + // 解散组 - 使用 destroy 方法或直接处理 + if (typeof this.groupObject.destroy === "function") { + this.groupObject?.destroy?.(); + } + + // 将原始对象重新添加到画布 + // 注意:解散组后,对象会自动恢复到画布上,但需要重新设置属性 + this.originalObjects.forEach((obj) => { + if (obj && !this.canvas.getObjects().includes(obj)) { + // 恢复对象的原始属性 + obj.layerId = this.activeLayer.id; + obj.layerName = this.activeLayer.name; + this.canvas.add(obj); + } + }); + + // 恢复图层对象列表 + this.activeLayer.fabricObjects = [...this.originalObjects]; + } + + /** + * 撤销向现有组添加对象的操作 + * @private + */ + _undoAddToExistingGroup() { + // 使用 removeWithUpdate 方法从组中移除新添加的对象 + // 在 Fabric.js 5 中,removeWithUpdate 会自动将对象重新添加到画布 + this.addedObjectsToGroup.forEach((obj) => { + this.existingGroup.removeWithUpdate(obj); + + // 重新设置对象的图层关联 + obj.layerId = this.activeLayer.id; + obj.layerName = this.activeLayer.name; + + // 确保对象在图层对象列表中 + if (!this.activeLayer.fabricObjects.includes(obj)) { + this.activeLayer.fabricObjects.push(obj); + } + }); + + // 检查组是否变为空或只有一个对象 + const remainingObjects = this.existingGroup.getObjects(); + if (remainingObjects.length === 0) { + // 组为空,移除组 + this.canvas.remove(this.existingGroup); + const groupIndex = this.activeLayer.fabricObjects.indexOf( + this.existingGroup + ); + if (groupIndex !== -1) { + this.activeLayer.fabricObjects.splice(groupIndex, 1); + } + } else if (remainingObjects.length === 1) { + // 只剩一个对象,解散组 + const singleObj = remainingObjects[0]; + + // 从画布移除组 + this.canvas.remove(this.existingGroup); + + // 使用 removeWithUpdate 移除最后一个对象 + // 这会自动将对象添加回画布 + this.existingGroup.removeWithUpdate(singleObj); + + // 重新设置对象属性 + singleObj.layerId = this.activeLayer.id; + singleObj.layerName = this.activeLayer.name; + + // 更新图层对象列表 + const groupIndex = this.activeLayer.fabricObjects.indexOf( + this.existingGroup + ); + if (groupIndex !== -1) { + this.activeLayer.fabricObjects[groupIndex] = singleObj; + } + } + } + + /** + * 更新缩略图 + * @private + */ + _updateThumbnail() { + if (this.canvas.thumbnailManager) { + setTimeout(() => { + this.canvas.thumbnailManager.generateLayerThumbnail( + this.activeLayer.id + ); + }, 100); + } + } + + getInfo() { + return { + name: this.name, + layerId: this.activeLayer?.id, + layerName: this.activeLayer?.name || "未知图层", + originalObjectCount: this.originalObjects.length, + hasNewImage: !!this.fabricImage, + groupId: this.groupObject?.id, + wasGroupCreated: this.wasGroupCreated, + addedObjectsCount: this.addedObjectsToGroup.length, + }; + } +} + +/** + * 创建图片图层复合命令 + * 包含创建图层、添加对象到图层、切换工具等操作的复合命令 + */ +export class CreateImageLayerCommand extends Command { + constructor(options) { + super({ + name: "创建图片图层", + saveState: true, + }); + this.layerManager = options.layerManager; + this.fabricImage = options.fabricImage; + this.toolManager = options.toolManager; + this.layerName = options.layerName || null; + + // 存储执行过程中的结果 + this.newLayerId = null; + this.commands = []; + this.executedCommands = []; + } + + async execute() { + if (!this.layerManager || !this.fabricImage) { + throw new Error("图层管理器或图片对象无效"); + } + + try { + this.commands = []; + this.executedCommands = []; + + // 生成图层名称 + const fileName = + this.layerName || `图片 ${new Date().toLocaleTimeString()}`; + + // 1. 创建新图层命令 + const createLayerCmd = new AddLayerCommand({ + canvas: this.layerManager.canvas, + layers: this.layerManager.layers, + newLayer: createLayer({ + name: fileName, + type: LayerType.BITMAP, + visible: true, + locked: false, + opacity: 1.0, + fabricObjects: [], + }), + activeLayerId: this.layerManager.activeLayerId, + insertIndex: this.layerManager._getInsertIndexAboveActiveLayer(), + }); + + // 执行创建图层命令 + this.newLayerId = await createLayerCmd.execute(); + this.commands.push(createLayerCmd); + this.executedCommands.push(createLayerCmd); + + // 2. 添加图片对象到图层命令 + const addObjectCmd = new AddObjectToLayerCommand({ + canvas: this.layerManager.canvas, + layers: this.layerManager.layers, + layerId: this.newLayerId, + fabricObject: this.fabricImage, + }); + + // 执行添加对象命令 + await addObjectCmd.execute(); + this.commands.push(addObjectCmd); + this.executedCommands.push(addObjectCmd); + + // 3. 切换工具到选择模式命令 + if (this.toolManager) { + const toolCmd = new ToolCommand({ + toolManager: this.toolManager, + tool: OperationType.SELECT, + previousTool: this.toolManager.getCurrentTool(), + }); + + // 执行工具切换命令 + await toolCmd.execute(); + this.commands.push(toolCmd); + this.executedCommands.push(toolCmd); + } + + console.log(`✅ 创建图片图层完成: ${fileName}, ID: ${this.newLayerId}`); + return this.newLayerId; + } catch (error) { + console.error("创建图片图层失败:", error); + // 回滚已执行的命令 + await this._rollbackExecutedCommands(); + throw error; + } + } + + async undo() { + if (this.executedCommands.length === 0) { + console.warn("没有已执行的命令需要撤销"); + return true; + } + + console.log( + `↩️ 开始撤销创建图片图层操作,共 ${this.executedCommands.length} 个子命令` + ); + + try { + // 逆序撤销已执行的命令 + const commands = [...this.executedCommands].reverse(); + + for (const command of commands) { + if (typeof command.undo === "function") { + try { + console.log(`↩️ 撤销子命令: ${command.constructor.name}`); + const result = command.undo(); + if (this._isPromise(result)) { + await result; + } + console.log(`✅ 子命令撤销成功: ${command.constructor.name}`); + } catch (error) { + console.error( + `❌ 子命令撤销失败: ${command.constructor.name}`, + error + ); + } + } + } + + this.executedCommands = []; + console.log(`✅ 创建图片图层撤销完成`); + return true; + } catch (error) { + console.error("❌ 撤销创建图片图层过程中发生错误:", error); + throw error; + } + } + + /** + * 回滚已执行的命令(内部使用) + * @private + */ + async _rollbackExecutedCommands() { + console.log(`🔄 开始回滚已执行的 ${this.executedCommands.length} 个子命令`); + + const commands = [...this.executedCommands].reverse(); + + for (const command of commands) { + if (typeof command.undo === "function") { + try { + console.log(`🔄 回滚子命令: ${command.constructor.name}`); + const result = command.undo(); + if (this._isPromise(result)) { + await result; + } + console.log(`✅ 子命令回滚成功: ${command.constructor.name}`); + } catch (error) { + console.error( + `❌ 子命令回滚失败: ${command.constructor.name}`, + error + ); + } + } + } + + this.executedCommands = []; + console.log(`✅ 回滚完成`); + } + + /** + * 检查返回值是否为Promise + * @private + */ + _isPromise(value) { + return ( + value && + typeof value === "object" && + typeof value.then === "function" && + typeof value.catch === "function" + ); + } + + getInfo() { + return { + name: this.name, + layerName: this.layerName, + layerId: this.newLayerId, + commandCount: this.commands.length, + executedCount: this.executedCommands.length, + }; + } +} + +/** + * 图层重新排序命令 + */ +export class ReorderLayersCommand extends Command { + constructor(options) { + super({ + name: "图层重新排序", + saveState: false, + }); + this.layers = options.layers; + this.oldIndex = options.oldIndex; + this.newIndex = options.newIndex; + this.layerId = options.layerId; + + // 验证参数 + if (this.oldIndex === undefined || this.newIndex === undefined) { + throw new Error("缺少必要的索引参数"); + } + + if (this.oldIndex === this.newIndex) { + console.warn("新旧索引相同,无需排序"); + } + } + + execute() { + if ( + !this.layers || + !this.layers.value || + !Array.isArray(this.layers.value) + ) { + console.error("图层数组无效"); + return false; + } + + if (this.oldIndex < 0 || this.oldIndex >= this.layers.value.length) { + console.error("原始索引超出范围"); + return false; + } + + if (this.newIndex < 0 || this.newIndex >= this.layers.value.length) { + console.error("目标索引超出范围"); + return false; + } + + // 获取要移动的图层 + const layerToMove = this.layers.value[this.oldIndex]; + + if (!layerToMove) { + console.error("找不到要移动的图层"); + return false; + } + + // 验证图层ID是否匹配 + if (this.layerId && layerToMove.id !== this.layerId) { + console.error("图层ID不匹配"); + return false; + } + + // 执行重新排序 + this.layers.value.splice(this.oldIndex, 1); + this.layers.value.splice(this.newIndex, 0, layerToMove); + + console.log( + `✅ 图层 "${layerToMove.name}" 从索引 ${this.oldIndex} 移动到 ${this.newIndex}` + ); + return true; + } + + undo() { + if ( + !this.layers || + !this.layers.value || + !Array.isArray(this.layers.value) + ) { + console.error("图层数组无效"); + return false; + } + + if (this.newIndex < 0 || this.newIndex >= this.layers.value.length) { + console.error("当前索引超出范围"); + return false; + } + + // 获取当前在新位置的图层 + const layerToRestore = this.layers.value[this.newIndex]; + + if (!layerToRestore) { + console.error("找不到要恢复的图层"); + return false; + } + + // 验证图层ID是否匹配 + if (this.layerId && layerToRestore.id !== this.layerId) { + console.error("图层ID不匹配"); + return false; + } + + // 恢复原始排序 + this.layers.value.splice(this.newIndex, 1); + this.layers.value.splice(this.oldIndex, 0, layerToRestore); + + console.log( + `↩️ 图层 "${layerToRestore.name}" 恢复到原始位置 ${this.oldIndex}` + ); + return true; + } + + getInfo() { + return { + name: this.name, + layerId: this.layerId, + oldIndex: this.oldIndex, + newIndex: this.newIndex, + }; + } +} + +/** + * 子图层重新排序命令 + */ +export class ReorderChildLayersCommand extends Command { + constructor(options) { + super({ + name: "子图层重新排序", + saveState: false, + }); + this.layers = options.layers; + this.parentId = options.parentId; + this.oldIndex = options.oldIndex; + this.newIndex = options.newIndex; + this.layerId = options.layerId; + } + + execute() { + // 查找父图层 + const parentLayer = this.layers.value.find( + (layer) => layer.id === this.parentId + ); + + if (!parentLayer || !parentLayer.children) { + console.error("找不到父图层或父图层没有子图层"); + return false; + } + + // 执行重新排序 + const childToMove = parentLayer.children[this.oldIndex]; + parentLayer.children.splice(this.oldIndex, 1); + parentLayer.children.splice(this.newIndex, 0, childToMove); + + return true; + } + + undo() { + // 查找父图层 + const parentLayer = this.layers.value.find( + (layer) => layer.id === this.parentId + ); + + if (!parentLayer || !parentLayer.children) { + return false; + } + + // 恢复原始排序 + const childToRestore = parentLayer.children[this.newIndex]; + parentLayer.children.splice(this.newIndex, 1); + parentLayer.children.splice(this.oldIndex, 0, childToRestore); + + return true; + } + + getInfo() { + return { + name: this.name, + parentId: this.parentId, + layerId: this.layerId, + oldIndex: this.oldIndex, + newIndex: this.newIndex, + }; + } +} + +/** + * 更改固定图层图像命令 + * 用于更换固定图层(红绿图模式、模特图层等)的图像内容 + */ +export class ChangeFixedImageCommand extends Command { + constructor(options) { + super({ + name: "更改固定图层图像", + saveState: true, + }); + this.canvas = options.canvas; + this.layers = options.layers; + this.layerId = options.layerId; + this.newImageFile = options.newImageFile; + this.layerManager = options.layerManager; + + // 备份原始数据 + this.originalLayer = null; + this.originalObject = null; + this.newImageObject = null; + } + + async execute() { + if (!this.canvas || !this.layers || !this.layerId || !this.newImageFile) { + throw new Error("更改固定图层图像命令缺少必要参数"); + } + + // 查找目标固定图层 + this.originalLayer = this.layers.value.find( + (layer) => layer.id === this.layerId + ); + + if (!this.originalLayer) { + throw new Error(`找不到图层 ID: ${this.layerId}`); + } + + // 验证是否为固定图层 + if (!this.originalLayer.isFixed && !this.originalLayer.isBackground) { + throw new Error("只能更改固定图层或背景图层的图像"); + } + + // 备份原始对象 + if (this.originalLayer.fabricObject) { + this.originalObject = this.originalLayer.fabricObject; + } + + // 创建新的图像对象 + this.newImageObject = await this._createImageFromFile(this.newImageFile); + + // 设置新图像的属性 + this._setupNewImageProperties(); + + // 替换图层中的对象 + this._replaceLayerObject(); + + // 重新渲染画布 + this.canvas.renderAll(); + + console.log(`✅ 成功更改固定图层 "${this.originalLayer.name}" 的图像`); + return this.newImageObject.id; + } + + /** + * 从文件创建图像对象 + * @param {File} file 图像文件 + * @returns {Promise} 图像对象 + * @private + */ + async _createImageFromFile(file) { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onload = (e) => { + fabric.Image.fromURL( + e.target.result, + (img) => { + if (!img) { + reject(new Error("无法创建图像对象")); + return; + } + resolve(img); + }, + { + crossOrigin: "anonymous", + } + ); + }; + reader.onerror = () => reject(new Error("文件读取失败")); + reader.readAsDataURL(file); + }); + } + + /** + * 设置新图像的属性 + * @private + */ + _setupNewImageProperties() { + if (!this.newImageObject || !this.originalLayer) return; + + // 生成新的ID + const newId = `fixed_image_${Date.now()}_${Math.floor( + Math.random() * 1000 + )}`; + + // 基本属性设置 + this.newImageObject.set({ + id: newId, + layerId: this.originalLayer.id, + layerName: this.originalLayer.name, + selectable: false, // 固定图层通常不可选择 + evented: false, // 固定图层通常不响应事件 + visible: this.originalLayer.visible, + opacity: this.originalLayer.opacity || 1.0, + }); + + // 如果有原始对象,继承其位置和变换属性 + if (this.originalObject) { + this.newImageObject.set({ + left: this.originalObject.left, + top: this.originalObject.top, + scaleX: this.originalObject.scaleX, + scaleY: this.originalObject.scaleY, + angle: this.originalObject.angle, + flipX: this.originalObject.flipX, + flipY: this.originalObject.flipY, + originX: this.originalObject.originX || "center", + originY: this.originalObject.originY || "center", + }); + } else { + // 如果没有原始对象,设置默认居中位置 + const canvasCenter = this.canvas.getCenter(); + this.newImageObject.set({ + left: canvasCenter.left, + top: canvasCenter.top, + originX: "center", + originY: "center", + }); + } + + // 如果是背景图层,可能需要特殊处理 + if (this.originalLayer.isBackground) { + this.newImageObject.set({ + isBackground: true, + }); + } + } + + /** + * 替换图层中的对象 + * @private + */ + _replaceLayerObject() { + // 从画布移除原始对象 + if ( + this.originalObject && + this.canvas.getObjects().includes(this.originalObject) + ) { + this.canvas.remove(this.originalObject); + } + + // 添加新对象到画布 + this.canvas.add(this.newImageObject); + + // 更新图层引用 + this.originalLayer.fabricObject = this.newImageObject; + + // 如果图层有 fabricObjects 数组,也要更新 + if (this.originalLayer.fabricObjects) { + if (this.originalObject) { + const index = this.originalLayer.fabricObjects.indexOf( + this.originalObject + ); + if (index !== -1) { + this.originalLayer.fabricObjects[index] = this.newImageObject; + } else { + this.originalLayer.fabricObjects.push(this.newImageObject); + } + } else { + this.originalLayer.fabricObjects.push(this.newImageObject); + } + } + + // 确保图层顺序正确(固定图层通常在特定位置) + this._adjustObjectZIndex(); + } + + /** + * 调整对象的Z轴顺序 + * @private + */ + _adjustObjectZIndex() { + if (!this.newImageObject || !this.originalLayer) return; + + // 如果是背景图层,确保在最底层 + if (this.originalLayer.isBackground) { + this.newImageObject.sendToBack(); + } else if (this.originalLayer.isFixed) { + // 如果是固定图层,放在背景层之上,普通图层之下 + const backgroundObjects = this.canvas + .getObjects() + .filter((obj) => obj.isBackground); + if (backgroundObjects.length > 0) { + this.newImageObject.bringForward(); // 向前移动一层 + } + } + } + + async undo() { + if (!this.canvas || !this.originalLayer) { + console.error("无法撤销:缺少必要的状态信息"); + return false; + } + + try { + // 从画布移除新对象 + if ( + this.newImageObject && + this.canvas.getObjects().includes(this.newImageObject) + ) { + this.canvas.remove(this.newImageObject); + } + + // 恢复原始对象 + if (this.originalObject) { + this.canvas.add(this.originalObject); + this.originalLayer.fabricObject = this.originalObject; + + // 恢复 fabricObjects 数组 + if (this.originalLayer.fabricObjects) { + const newIndex = this.originalLayer.fabricObjects.indexOf( + this.newImageObject + ); + if (newIndex !== -1) { + this.originalLayer.fabricObjects[newIndex] = this.originalObject; + } + } + } else { + // 如果原来没有对象,清空图层引用 + this.originalLayer.fabricObject = null; + if (this.originalLayer.fabricObjects) { + this.originalLayer.fabricObjects = + this.originalLayer.fabricObjects.filter( + (obj) => obj !== this.newImageObject + ); + } + } + + // 重新渲染画布 + this.canvas.renderAll(); + + console.log( + `↩️ 成功撤销固定图层 "${this.originalLayer.name}" 的图像更改` + ); + return true; + } catch (error) { + console.error("撤销更改固定图层图像时发生错误:", error); + return false; + } + } + + getInfo() { + return { + name: this.name, + layerId: this.layerId, + layerName: this.originalLayer?.name || "未知图层", + fileName: this.newImageFile?.name || "未知文件", + hasOriginalObject: !!this.originalObject, + newImageId: this.newImageObject?.id, + }; + } +} + +/** + * 添加图像到图层命令 + * 如果指定图层ID则添加到指定图层,否则创建新图层 + */ +export class AddImageToLayerCommand extends Command { + constructor(options) { + super({ + name: "添加图像到图层", + saveState: true, + }); + this.canvas = options.canvas; + this.layers = options.layers; + this.activeLayerId = options.activeLayerId; + this.imageFile = options.imageFile; + this.targetLayerId = options.targetLayerId; // 可选:指定目标图层ID + this.layerManager = options.layerManager; + this.toolManager = options.toolManager; + + // 执行结果 + this.createdImageObject = null; + this.usedLayerId = null; + this.wasLayerCreated = false; + this.subCommands = []; + this.executedSubCommands = []; + } + + async execute() { + if (!this.canvas || !this.layers || !this.imageFile) { + throw new Error("添加图像到图层命令缺少必要参数"); + } + + try { + // 创建图像对象 + this.createdImageObject = await this._createImageFromFile(this.imageFile); + + // 确定目标图层 + const targetLayer = await this._determineTargetLayer(); + this.usedLayerId = targetLayer.id; + + // 添加图像到图层 + await this._addImageToLayer(targetLayer); + + // 切换到选择工具 + await this._switchToSelectTool(); + + console.log(`✅ 成功添加图像到图层 "${targetLayer.name}"`); + return { + layerId: this.usedLayerId, + imageId: this.createdImageObject.id, + wasLayerCreated: this.wasLayerCreated, + }; + } catch (error) { + console.error("添加图像到图层失败:", error); + // 回滚已执行的子命令 + await this._rollbackSubCommands(); + throw error; + } + } + + /** + * 从文件创建图像对象 + * @param {File} file 图像文件 + * @returns {Promise} 图像对象 + * @private + */ + async _createImageFromFile(file) { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onload = (e) => { + fabric.Image.fromURL( + e.target.result, + (img) => { + if (!img) { + reject(new Error("无法创建图像对象")); + return; + } + + // 设置图像基本属性 + const imageId = `image_${Date.now()}_${Math.floor( + Math.random() * 1000 + )}`; + + // 获取画布中心点 + const canvasCenter = this.canvas.getCenter(); + + img.set({ + id: imageId, + left: canvasCenter.left, + top: canvasCenter.top, + originX: "center", + originY: "center", + selectable: true, + evented: true, + }); + + resolve(img); + }, + { + crossOrigin: "anonymous", + } + ); + }; + reader.onerror = () => reject(new Error("文件读取失败")); + reader.readAsDataURL(file); + }); + } + + /** + * 确定目标图层 + * @returns {Promise} 目标图层对象 + * @private + */ + async _determineTargetLayer() { + // 如果指定了目标图层ID,查找该图层 + if (this.targetLayerId) { + const existingLayer = this.layers.value.find( + (layer) => layer.id === this.targetLayerId + ); + + if (existingLayer) { + // 验证图层是否可以添加对象 + if (existingLayer.isBackground) { + throw new Error("不能向背景图层添加对象"); + } + if (existingLayer.locked) { + throw new Error("不能向锁定图层添加对象"); + } + + console.log(`使用指定图层: ${existingLayer.name}`); + return existingLayer; + } else { + console.warn(`指定的图层ID不存在: ${this.targetLayerId},将创建新图层`); + } + } + + // 创建新图层 + return await this._createNewLayer(); + } + + /** + * 创建新图层 + * @returns {Promise} 新创建的图层对象 + * @private + */ + async _createNewLayer() { + // 生成图层名称 + const fileName = this.imageFile.name.replace(/\.[^/.]+$/, ""); // 移除文件扩展名 + const layerName = fileName || `图片 ${new Date().toLocaleTimeString()}`; + + // 创建图层对象 + const newLayer = createLayer({ + name: layerName, + type: LayerType.BITMAP, + visible: true, + locked: false, + opacity: 1.0, + fabricObjects: [], + }); + + // 执行添加图层命令 + const addLayerCmd = new AddLayerCommand({ + canvas: this.canvas, + layers: this.layers, + newLayer: newLayer, + activeLayerId: this.activeLayerId, + insertIndex: this.layerManager?._getInsertIndexAboveActiveLayer?.(), + }); + + const layerId = await addLayerCmd.execute(); + this.subCommands.push(addLayerCmd); + this.executedSubCommands.push(addLayerCmd); + this.wasLayerCreated = true; + + console.log(`创建新图层: ${layerName}, ID: ${layerId}`); + return newLayer; + } + + /** + * 添加图像到图层 + * @param {Object} targetLayer 目标图层 + * @private + */ + async _addImageToLayer(targetLayer) { + // 设置图像与图层的关联 + this.createdImageObject.set({ + layerId: targetLayer.id, + layerName: targetLayer.name, + }); + + // 执行添加对象到图层命令 + const addObjectCmd = new AddObjectToLayerCommand({ + canvas: this.canvas, + layers: this.layers, + layerId: targetLayer.id, + fabricObject: this.createdImageObject, + }); + + await addObjectCmd.execute(); + this.subCommands.push(addObjectCmd); + this.executedSubCommands.push(addObjectCmd); + + console.log(`图像已添加到图层: ${targetLayer.name}`); + } + + /** + * 切换到选择工具 + * @private + */ + async _switchToSelectTool() { + if (!this.toolManager) return; + + const toolCmd = new ToolCommand({ + toolManager: this.toolManager, + tool: OperationType.SELECT, + previousTool: this.toolManager.getCurrentTool(), + }); + + await toolCmd.execute(); + this.subCommands.push(toolCmd); + this.executedSubCommands.push(toolCmd); + + console.log("已切换到选择工具"); + } + + /** + * 回滚已执行的子命令 + * @private + */ + async _rollbackSubCommands() { + console.log(`🔄 开始回滚 ${this.executedSubCommands.length} 个子命令`); + + const commands = [...this.executedSubCommands].reverse(); + + for (const command of commands) { + if (typeof command.undo === "function") { + try { + console.log(`🔄 回滚子命令: ${command.constructor.name}`); + const result = command.undo(); + if (this._isPromise(result)) { + await result; + } + console.log(`✅ 子命令回滚成功: ${command.constructor.name}`); + } catch (error) { + console.error( + `❌ 子命令回滚失败: ${command.constructor.name}`, + error + ); + } + } + } + + this.executedSubCommands = []; + console.log(`✅ 回滚完成`); + } + + async undo() { + if (this.executedCommands.length === 0) { + console.warn("没有已执行的命令需要撤销"); + return true; + } + + console.log( + `↩️ 开始撤销创建图片图层操作,共 ${this.executedCommands.length} 个子命令` + ); + + try { + // 逆序撤销已执行的命令 + const commands = [...this.executedCommands].reverse(); + + for (const command of commands) { + if (typeof command.undo === "function") { + try { + console.log(`↩️ 撤销子命令: ${command.constructor.name}`); + const result = command.undo(); + if (this._isPromise(result)) { + await result; + } + console.log(`✅ 子命令撤销成功: ${command.constructor.name}`); + } catch (error) { + console.error( + `❌ 子命令撤销失败: ${command.constructor.name}`, + error + ); + } + } + } + + this.executedCommands = []; + console.log(`✅ 创建图片图层撤销完成`); + return true; + } catch (error) { + console.error("❌ 撤销创建图片图层过程中发生错误:", error); + throw error; + } + } + + /** + * 检查返回值是否为Promise + * @private + */ + _isPromise(value) { + return ( + value && + typeof value === "object" && + typeof value.then === "function" && + typeof value.catch === "function" + ); + } + + getInfo() { + return { + name: this.name, + layerName: this.layerName, + layerId: this.newLayerId, + commandCount: this.commands.length, + executedCount: this.executedCommands.length, + }; + } +} diff --git a/src/component/Canvas/CanvasEditor/commands/LiquifyCommands.js b/src/component/Canvas/CanvasEditor/commands/LiquifyCommands.js new file mode 100644 index 00000000..5c97d784 --- /dev/null +++ b/src/component/Canvas/CanvasEditor/commands/LiquifyCommands.js @@ -0,0 +1,578 @@ +import { Command, FunctionCommand } from "./Command"; + +/** + * 液化命令基类 + * 所有液化相关命令的基类 + */ +export class LiquifyCommand extends Command { + /** + * 创建液化命令 + * @param {Object} options 配置选项 + * @param {Object} options.canvas Fabric.js画布实例 + * @param {Object} options.layerManager 图层管理器实例 + * @param {Object} options.liquifyManager 液化管理器实例 + * @param {String} options.mode 液化模式 + * @param {Object} options.params 液化参数 + * @param {Object} options.targetObject 目标对象 + * @param {ImageData} options.originalData 原始图像数据 + * @param {ImageData} options.resultData 变形后图像数据 + */ + constructor(options) { + super({ + name: options.name || `液化操作: ${options.mode || "未知模式"}`, + description: + options.description || + `使用${options.mode || "未知模式"}模式进行液化操作`, + }); + + this.canvas = options.canvas; + this.layerManager = options.layerManager; + this.liquifyManager = options.liquifyManager; + this.mode = options.mode; + this.params = options.params || {}; + this.targetObject = options.targetObject; + this.targetLayerId = options.targetLayerId; + this.originalData = options.originalData; // 操作前的图像数据 + this.resultData = options.resultData; // 操作后的图像数据 + this.savedState = null; + } + + /** + * 执行液化操作 + * @returns {Promise} 执行结果 + */ + async execute() { + if (!this.canvas || !this.targetObject) { + throw new Error("液化命令缺少必要的画布或目标对象"); + } + + if (!this.resultData) { + // 如果没有预先计算的结果数据,现场执行变形 + this.resultData = await this.liquifyManager.applyLiquify( + this.targetObject, + this.mode, + this.params + ); + } + + // 保存执行前的状态 + this.savedState = await this._saveObjectState(); + + // 更新画布上的对象 + await this._updateObjectWithResult(); + + // 刷新Canvas + this.canvas.renderAll(); + + return this.resultData; + } + + /** + * 撤销液化操作 + * @returns {Promise} 撤销结果 + */ + async undo() { + if (!this.canvas || !this.targetObject || !this.savedState) { + throw new Error("无法撤销:缺少必要的状态信息"); + } + + // 恢复对象到原始状态 + await this._restoreObjectState(); + + // 刷新Canvas + this.canvas.renderAll(); + + return true; + } + + /** + * 保存对象状态 + * @private + */ + async _saveObjectState() { + if (!this.targetObject) return null; + + // 对于图像对象,我们需要保存src和元数据 + const state = { + src: this.targetObject.getSrc ? this.targetObject.getSrc() : null, + element: this.targetObject._element + ? this.targetObject._element.cloneNode(true) + : null, + filters: this.targetObject.filters ? [...this.targetObject.filters] : [], + originalData: this.originalData, + targetLayerId: this.targetLayerId, + }; + + return state; + } + + /** + * 恢复对象状态 + * @private + */ + async _restoreObjectState() { + if (!this.targetObject || !this.savedState) return false; + + // 获取当前图层对象 + const layer = this.layerManager.getLayerById(this.savedState.targetLayerId); + if (!layer) return false; + + // 恢复原始图像 + if (this.savedState.element && this.targetObject.setElement) { + this.targetObject.setElement(this.savedState.element); + + // 恢复滤镜 + if (this.savedState.filters) { + this.targetObject.filters = [...this.savedState.filters]; + this.targetObject.applyFilters(); + } + } + + return true; + } + + /** + * 使用变形结果更新对象 + * @private + */ + async _updateObjectWithResult() { + if (!this.targetObject || !this.resultData) return false; + + // 创建临时canvas来渲染结果数据 + const tempCanvas = document.createElement("canvas"); + tempCanvas.width = this.resultData.width; + tempCanvas.height = this.resultData.height; + const tempCtx = tempCanvas.getContext("2d"); + tempCtx.putImageData(this.resultData, 0, 0); + console.log("临时Canvas创建成功 _updateObjectWithResult", this.resultData); + // 更新Fabric图像 + await new Promise((resolve) => { + fabric.Image.fromURL(tempCanvas.toDataURL(), (img) => { + // 保留原对象的属性 + img.set({ + left: this.targetObject.left, + top: this.targetObject.top, + scaleX: this.targetObject.scaleX, + scaleY: this.targetObject.scaleY, + angle: this.targetObject.angle, + flipX: this.targetObject.flipX, + flipY: this.targetObject.flipY, + opacity: this.targetObject.opacity, + }); + + // 替换Canvas上的对象 + const index = this.canvas.getObjects().indexOf(this.targetObject); + if (index !== -1) { + this.canvas.remove(this.targetObject); + this.canvas.insertAt(img, index); + this.targetObject = img; + } + + // 确保图层引用更新 + const layer = this.layerManager.getLayerById(this.targetLayerId); + if (layer) { + if ( + layer.type === "background" && + layer.fabricObject === this.targetObject + ) { + layer.fabricObject = img; + } else if (layer.fabricObjects) { + const objIndex = layer.fabricObjects.indexOf(this.targetObject); + if (objIndex !== -1) { + layer.fabricObjects[objIndex] = img; + } + } + } + + resolve(); + }); + }); + + return true; + } +} + +/** + * 图层栅格化命令 + * 用于将复杂图层栅格化为单一图像,以便进行液化操作 + */ +export class RasterizeForLiquifyCommand extends Command { + /** + * 创建栅格化命令 + * @param {Object} options 配置选项 + * @param {Object} options.canvas Fabric.js画布实例 + * @param {Object} options.layerManager 图层管理器实例 + * @param {String} options.layerId 需要栅格化的图层ID + */ + constructor(options) { + super({ + name: options.name || "栅格化图层", + description: options.description || "将图层栅格化为单一图像以便液化操作", + }); + + this.canvas = options.canvas; + this.layerManager = options.layerManager; + this.layerId = options.layerId; + this.originalLayer = null; + this.rasterizedImageObj = null; + } + + /** + * 执行栅格化操作 + * @returns {Promise} 栅格化后的图像对象 + */ + async execute() { + if (!this.canvas || !this.layerManager || !this.layerId) { + throw new Error("栅格化命令缺少必要参数"); + } + + // 保存原始图层信息 + this.originalLayer = this.layerManager.getLayerById(this.layerId); + if (!this.originalLayer) { + throw new Error(`图层ID不存在: ${this.layerId}`); + } + + // 栅格化图层 + const rasterizedImage = await this.layerManager.rasterizeLayer( + this.layerId + ); + if (!rasterizedImage) { + throw new Error("栅格化图层失败"); + } + + this.rasterizedImageObj = rasterizedImage; + return rasterizedImage; + } + + /** + * 撤销栅格化操作 + * 注意:完整撤销栅格化是复杂的,这里提供近似还原 + * @returns {Promise} 撤销结果 + */ + async undo() { + if (!this.canvas || !this.layerManager || !this.originalLayer) { + throw new Error("无法撤销:缺少必要的状态信息"); + } + + // 恢复图层为原始状态是复杂的,这里可能需要与LayerManager协作 + // 这个实现可能需要根据实际的LayerManager功能来调整 + const restored = await this.layerManager.restoreLayerFromBackup( + this.layerId + ); + if (!restored) { + console.warn("无法完全还原栅格化前的图层状态"); + } + + return true; + } +} + +/** + * 液化工具初始化命令 + * 用于初始化液化工具的状态,不执行实际操作 + */ +export class InitLiquifyToolCommand extends Command { + constructor(options) { + super({ + name: "初始化液化工具", + description: "准备液化工具工作环境", + undoable: false, // 这个命令不需要撤销 + }); + + this.canvas = options.canvas; + this.layerManager = options.layerManager; + this.liquifyManager = options.liquifyManager; + this.toolManager = options.toolManager; + } + + /** + * 执行初始化 + */ + execute() { + if (this.liquifyManager) { + this.liquifyManager.initialize({ + canvas: this.canvas, + layerManager: this.layerManager, + }); + } + + // 通知各管理器进入液化模式 + this.toolManager?.notifyObservers("LIQUIFY"); + + return true; + } + + undo() { + // 不需要撤销初始化 + return true; + } +} + +/** + * 液化操作命令 - 针对单次变形操作 + * 用于实现可撤销的单次液化变形 + */ +export class LiquifyDeformCommand extends LiquifyCommand { + /** + * 创建液化变形命令 + * @param {Object} options 配置选项 + * @param {Number} options.x 变形中心X坐标 + * @param {Number} options.y 变形中心Y坐标 + * @param {ImageData} options.beforeData 变形前的图像数据 + * @param {ImageData} options.afterData 变形后的图像数据 + */ + constructor(options) { + super({ + ...options, + name: `液化变形: ${options.mode || "未知模式"}`, + description: `在(${options.x}, ${options.y})应用${ + options.mode || "未知模式" + }变形`, + }); + + this.x = options.x; + this.y = options.y; + this.beforeData = options.beforeData; + this.afterData = options.afterData; + } + + async execute() { + if (!this.afterData) { + // 如果没有预计算的结果,实时计算 + await this.liquifyManager.prepareForLiquify(this.targetObject); + this.afterData = await this.liquifyManager.applyLiquify( + this.targetObject, + this.mode, + this.params, + this.x, + this.y + ); + } + + // 保存当前状态 + this.savedState = await this._saveObjectState(); + + // 应用变形结果 + await this._updateObjectWithImageData(this.afterData); + + this.canvas.renderAll(); + return this.afterData; + } + + async undo() { + if (!this.beforeData) { + throw new Error("无法撤销:缺少变形前的数据"); + } + + // 恢复到变形前的状态 + await this._updateObjectWithImageData(this.beforeData); + + this.canvas.renderAll(); + return true; + } + + /** + * 使用图像数据更新对象 + * @param {ImageData} imageData 图像数据 + * @private + */ + async _updateObjectWithImageData(imageData) { + return new Promise((resolve) => { + // 创建临时canvas + const tempCanvas = document.createElement("canvas"); + tempCanvas.width = imageData.width; + tempCanvas.height = imageData.height; + const tempCtx = tempCanvas.getContext("2d"); + tempCtx.putImageData(imageData, 0, 0); + + // 从canvas创建新的fabric图像 + fabric.Image.fromURL(tempCanvas.toDataURL(), (img) => { + // 保留原对象的变换属性 + img.set({ + left: this.targetObject.left, + top: this.targetObject.top, + scaleX: this.targetObject.scaleX, + scaleY: this.targetObject.scaleY, + angle: this.targetObject.angle, + flipX: this.targetObject.flipX, + flipY: this.targetObject.flipY, + opacity: this.targetObject.opacity, + }); + + // 替换canvas上的对象 + const index = this.canvas.getObjects().indexOf(this.targetObject); + if (index !== -1) { + this.canvas.remove(this.targetObject); + this.canvas.insertAt(img, index); + this.targetObject = img; + + // 更新图层引用 + const layer = this.layerManager.getLayerById(this.targetLayerId); + if (layer) { + if ( + layer.type === "background" && + (layer.fabricObject === this.savedState?.originalObject || + layer.fabricObject === this.targetObject) + ) { + layer.fabricObject = img; + } else if (layer.fabricObjects) { + const objIndex = layer.fabricObjects.findIndex( + (obj) => + obj === this.savedState?.originalObject || + obj === this.targetObject + ); + if (objIndex !== -1) { + layer.fabricObjects[objIndex] = img; + } + } + } + } + + resolve(); + }); + }); + } +} + +/** + * 复合液化命令 - 用于组合多个液化操作 + * 支持一次撤销多个相关的液化变形 + */ +export class CompositeLiquifyCommand extends Command { + /** + * 创建复合液化命令 + * @param {Object} options 配置选项 + * @param {Array} options.commands 子命令列表 + * @param {String} options.name 命令名称 + */ + constructor(options) { + super({ + name: options.name || "液化操作组合", + description: + options.description || `包含${options.commands?.length || 0}个液化操作`, + }); + + this.commands = options.commands || []; + this.canvas = options.canvas; + this.layerManager = options.layerManager; + this.liquifyManager = options.liquifyManager; + } + + /** + * 添加子命令 + * @param {Command} command 要添加的命令 + */ + addCommand(command) { + this.commands.push(command); + } + + async execute() { + const results = []; + + for (const command of this.commands) { + try { + const result = await command.execute(); + results.push(result); + } catch (error) { + // 如果有命令失败,尝试回滚已执行的命令 + console.error("复合液化命令执行失败:", error); + await this.undo(); + throw error; + } + } + + return results; + } + + async undo() { + // 逆序撤销所有子命令 + const errors = []; + + for (let i = this.commands.length - 1; i >= 0; i--) { + try { + await this.commands[i].undo(); + } catch (error) { + console.error(`撤销子命令${i}失败:`, error); + errors.push(error); + } + } + + if (errors.length > 0) { + throw new Error(`复合命令撤销部分失败: ${errors.length}个错误`); + } + + return true; + } + + /** + * 检查命令是否可以执行 + * @returns {Boolean} 是否可执行 + */ + canExecute() { + return ( + this.commands.length > 0 && + this.commands.every((cmd) => (cmd.canExecute ? cmd.canExecute() : true)) + ); + } +} + +/** + * 液化重置命令 - 将图像恢复到原始状态 + */ +export class LiquifyResetCommand extends LiquifyCommand { + constructor(options) { + super({ + ...options, + name: "重置液化", + description: "将图像恢复到液化前的原始状态", + }); + } + + async execute() { + if (!this.liquifyManager || !this.targetObject) { + throw new Error("无法重置:缺少必要的管理器或目标对象"); + } + + // 保存当前状态 + this.savedState = await this._saveObjectState(); + + // 重置液化管理器 + const resetData = this.liquifyManager.reset(); + if (!resetData) { + throw new Error("重置失败:没有原始数据"); + } + + // 应用重置结果 + await this._updateObjectWithResult(); + + this.canvas.renderAll(); + return resetData; + } +} + +/** + * 辅助函数:创建液化重置命令 + * @param {Object} options 配置选项 + * @returns {LiquifyResetCommand} 重置命令实例 + */ +export function createLiquifyResetCommand(options) { + return new LiquifyResetCommand(options); +} + +/** + * 辅助函数:创建液化变形命令 + * @param {Object} options 配置选项 + * @returns {LiquifyDeformCommand} 变形命令实例 + */ +export function createLiquifyDeformCommand(options) { + return new LiquifyDeformCommand(options); +} + +/** + * 辅助函数:创建复合液化命令 + * @param {Object} options 配置选项 + * @returns {CompositeLiquifyCommand} 复合命令实例 + */ +export function createCompositeLiquifyCommand(options) { + return new CompositeLiquifyCommand(options); +} diff --git a/src/component/Canvas/CanvasEditor/commands/NonUndoableCommands.js b/src/component/Canvas/CanvasEditor/commands/NonUndoableCommands.js new file mode 100644 index 00000000..dd6c51c8 --- /dev/null +++ b/src/component/Canvas/CanvasEditor/commands/NonUndoableCommands.js @@ -0,0 +1,103 @@ +/** + * 查询类命令示例 - 不需要撤销 + */ +export class GetCanvasInfoCommand { + constructor(options) { + this.canvas = options.canvas; + this.layerManager = options.layerManager; + this.undoable = false; // 明确标记为不可撤销 + } + + execute() { + return { + width: this.canvas.getWidth(), + height: this.canvas.getHeight(), + zoom: this.canvas.getZoom(), + layers: this.layerManager?.getLayers()?.length || 0, + objects: this.canvas.getObjects().length, + }; + } + + // 查询类命令不需要实现 undo 方法 +} + +/** + * 导出类命令示例 - 不需要撤销 + */ +export class ExportCanvasCommand { + constructor(options) { + this.canvas = options.canvas; + this.format = options.format || "png"; + this.quality = options.quality || 1; + this.undoable = false; + } + + execute() { + return this.canvas.toDataURL(`image/${this.format}`, this.quality); + } +} + +/** + * 验证类命令示例 - 不需要撤销 + */ +export class ValidateCanvasCommand { + constructor(options) { + this.canvas = options.canvas; + this.undoable = false; + } + + execute() { + const objects = this.canvas.getObjects(); + const errors = []; + + objects.forEach((obj, index) => { + if (!obj.left || !obj.top) { + errors.push(`对象 ${index} 位置无效`); + } + if (obj.width <= 0 || obj.height <= 0) { + errors.push(`对象 ${index} 尺寸无效`); + } + }); + + return { + isValid: errors.length === 0, + errors, + objectCount: objects.length, + }; + } +} + +/** + * 系统清理命令示例 - 不可逆操作,不需要撤销 + */ +export class CleanupTempDataCommand { + constructor(options) { + this.canvas = options.canvas; + this.undoable = false; + } + + execute() { + // 清理临时数据 + const cleaned = []; + + // 移除无效对象 + const objects = this.canvas.getObjects(); + objects.forEach((obj, index) => { + if (obj._isTemp || obj._invalid) { + this.canvas.remove(obj); + cleaned.push(`临时对象 ${index}`); + } + }); + + // 清理缓存 + if (this.canvas._clearCache) { + this.canvas._clearCache(); + cleaned.push("画布缓存"); + } + + return { + cleaned, + count: cleaned.length, + }; + } +} diff --git a/src/component/Canvas/CanvasEditor/commands/ObjectLayerCommands.js b/src/component/Canvas/CanvasEditor/commands/ObjectLayerCommands.js new file mode 100644 index 00000000..916825f4 --- /dev/null +++ b/src/component/Canvas/CanvasEditor/commands/ObjectLayerCommands.js @@ -0,0 +1,942 @@ +import { OperationType } from "../utils/layerHelper"; +import { Command } from "./Command"; +import { generateId } from "../utils/helper"; + +/** + * 设置活动图层命令 + */ +export class SetActiveLayerCommand extends Command { + constructor(options) { + super({ + name: "设置活动图层", + saveState: false, + }); + this.layers = options.layers; + this.canvas = options.canvas; + this.activeLayerId = options.activeLayerId; + this.layerId = options.layerId; + this.oldActiveLayerId = this.activeLayerId.value; + this.layerManager = options.layerManager; + this.oldActiveObjects = []; + this.newLayer = null; + this.editorMode = options.editorMode; + } + + execute() { + this.newLayer = this.layers.value.find( + (layer) => layer.id === this.layerId + ); + + if (!this.newLayer) { + console.error(`图层 ${this.layerId} 不存在`); + return false; + } + + // 如果是背景层,不设置为活动图层 + if (this.newLayer.isBackground) { + console.warn("背景层不能设为活动图层"); + return false; + } + + // 如果图层已锁定,不设置为活动图层 + if (this.newLayer.locked) { + console.warn("锁定图层不能设为活动图层"); + return false; + } + + this.oldActiveObjects = this.canvas.getActiveObjects(); + + // 设置为活动图层 + this.activeLayerId.value = this.layerId; + + // 如果在选择模式下,取消所有选择 + if (this.editorMode === OperationType.SELECT && this.canvas) { + this.canvas.discardActiveObject(); + + // 设置为新的图层下的对象为激活,但需要确保对象存在于画布上 + if ( + this.newLayer.fabricObjects && + this.newLayer.fabricObjects.length > 0 + ) { + const canvasObjects = this.canvas.getObjects(); + const validObjects = this.newLayer.fabricObjects.filter( + (obj) => obj && canvasObjects.includes(obj) + ); + + if (validObjects.length > 0) { + if (validObjects.length === 1) { + // 只有一个对象时直接设置 + this.canvas.setActiveObject(validObjects[0]); + } else { + // 多个对象时创建活动选择组 + const activeSelection = new fabric.ActiveSelection(validObjects, { + canvas: this.canvas, + }); + this.canvas.setActiveObject(activeSelection); + } + } + } + this.canvas.renderAll(); + } + return true; + } + + undo() { + // 恢复原活动图层ID + this.activeLayerId.value = this.oldActiveLayerId; + + // 如果在选择模式下,恢复取消的所有选择 + if (this.editorMode === OperationType.SELECT && this.canvas) { + this.canvas.discardActiveObject(); + + // 修复:确保对象存在于画布上才激活 + if (this.oldActiveObjects && this.oldActiveObjects.length > 0) { + const canvasObjects = this.canvas.getObjects(); + const validObjects = this.oldActiveObjects.filter( + (obj) => obj && canvasObjects.includes(obj) + ); + + if (validObjects.length > 0) { + if (validObjects.length > 1) { + // 如果有多个对象,需要创建一个活动选择组 + const activeSelection = new fabric.ActiveSelection(validObjects, { + canvas: this.canvas, + }); + this.canvas.setActiveObject(activeSelection); + } else { + // 只有一个对象时直接设置 + this.canvas.setActiveObject(validObjects[0]); + } + } + } + + this.canvas.renderAll(); + } + } + + getInfo() { + return { + name: this.name, + layerId: this.layerId, + oldActiveLayerId: this.oldActiveLayerId, + }; + } +} + +/** + * 添加对象到图层命令 + */ +export class AddObjectToLayerCommand extends Command { + constructor(options) { + super({ + name: "添加对象到图层", + saveState: true, + }); + this.canvas = options.canvas; + this.layers = options.layers; + this.layerId = options.layerId; + this.fabricObject = options.fabricObject; + + // 保存对象原始状态和ID + this.objectId = + this.fabricObject.id || + `obj_${Date.now()}_${Math.floor(Math.random() * 1000)}`; + this.originalObjectState = this.fabricObject.toObject([ + "id", + "layerId", + "layerName", + ]); + } + + execute() { + // 查找目标图层 + const layer = this.layers.value.find((l) => l.id === this.layerId); + + if (!layer) { + console.error(`图层 ${this.layerId} 不存在`); + return null; + } + + // 如果是背景层,不允许添加对象 + if (layer.isBackground) { + console.warn("不能向背景层添加对象"); + return null; + } + + // 为对象生成唯一ID + this.fabricObject.id = this.objectId; + + // 设置对象与图层的关联 + this.fabricObject.layerId = this.layerId; + this.fabricObject.layerName = layer.name; + + // 设置对象可操作可选择 + this.fabricObject.selectable = true; + this.fabricObject.evented = true; + + // 将对象添加到画布 + this.canvas.add(this.fabricObject); + + // 将对象添加到图层的fabricObjects数组 + layer.fabricObjects = layer.fabricObjects || []; + layer.fabricObjects.push(this.fabricObject); + + this.canvas.discardActiveObject(); + + // 确保对象确实存在于画布上才激活 + const canvasObjects = this.canvas.getObjects(); + const validObjects = layer.fabricObjects.filter( + (obj) => obj && canvasObjects.includes(obj) + ); + + if (validObjects.length > 0) { + if (validObjects.length === 1) { + // 只有一个对象时直接设置 + this.canvas.setActiveObject(validObjects[0]); + } else { + // 多个对象时创建活动选择组 + const activeSelection = new fabric.ActiveSelection(validObjects, { + canvas: this.canvas, + }); + this.canvas.setActiveObject(activeSelection); + } + } + + // 更新画布 + this.canvas.renderAll(); + + return this.fabricObject; + } + + undo() { + // 查找图层 + const layer = this.layers.value.find((l) => l.id === this.layerId); + + if (!layer) { + return false; + } + + // 从图层的fabricObjects数组中移除对象 + if (layer.fabricObjects) { + layer.fabricObjects = layer.fabricObjects.filter( + (obj) => obj.id !== this.objectId + ); + } + // 从画布移除对象 + const object = this.canvas + .getObjects() + .find((obj) => obj.id === this.objectId); + if (object) { + // 先丢弃活动对象,避免控制点渲染错误 + this.canvas.discardActiveObject(); + this.canvas.remove(object); + // 更新画布 + this.canvas.renderAll(); + } + + return true; + } + + getInfo() { + return { + name: this.name, + layerId: this.layerId, + objectId: this.objectId, + }; + } +} + +/** + * 从图层中移除对象命令 + */ +export class RemoveObjectFromLayerCommand extends Command { + constructor(options) { + super({ + name: "从图层中移除对象", + saveState: true, + }); + this.canvas = options.canvas; + this.layers = options.layers; + this.objectId = options.objectId; + + // 查找对象和图层 + this.object = + typeof options.objectOrId === "object" + ? options.objectOrId + : this.canvas.getObjects().find((obj) => obj.id === this.objectId); + + if (this.object) { + this.layerId = this.object.layerId; + this.objectData = this.object.toObject(["id", "layerId", "layerName"]); + } + } + + execute() { + if (!this.object) { + console.error(`对象 ${this.objectId} 不存在`); + return false; + } + + if (!this.layerId) { + console.error(`对象 ${this.objectId} 未关联到任何图层`); + return false; + } + + // 查找图层 + const layer = this.layers.value.find((l) => l.id === this.layerId); + if (!layer) { + console.error(`图层 ${this.layerId} 不存在`); + return false; + } + + // 从画布移除对象 + this.canvas.remove(this.object); + + // 从图层的fabricObjects数组移除对象 + if (layer.fabricObjects) { + layer.fabricObjects = layer.fabricObjects.filter( + (obj) => obj.id !== this.objectId + ); + } + + // 更新画布 + this.canvas.renderAll(); + + return true; + } + + undo() { + if (!this.objectData || !this.layerId) { + return false; + } + + // 查找图层 + const layer = this.layers.value.find((l) => l.id === this.layerId); + if (!layer) { + return false; + } + + // 恢复对象到画布 + fabric.util.enlivenObjects([this.objectData], (objects) => { + const restoredObject = objects[0]; + + // 将对象添加到画布 + this.canvas.add(restoredObject); + + // 将对象添加回图层 + layer.fabricObjects = layer.fabricObjects || []; + layer.fabricObjects.push(restoredObject); + + // 更新画布 + this.canvas.renderAll(); + }); + + return true; + } + + getInfo() { + return { + name: this.name, + objectId: this.objectId, + layerId: this.layerId, + }; + } +} + +/** + * 更换固定图层图像命令 + * 专门用于更换固定图层(如背景图层)的图像 + */ +export class ChangeFixedImageCommand extends Command { + constructor(options = {}) { + super(); + this.canvas = options.canvas; + this.layerManager = options.layerManager; + this.imageUrl = options.imageUrl; + this.targetLayerType = options.targetLayerType || "background"; // 'background', 'fixed', etc. + this.position = options.position || { x: 0, y: 0 }; + this.scale = options.scale || { x: 1, y: 1 }; + this.preserveTransform = options.preserveTransform !== false; // 默认保留变换 + + // 用于回滚的状态 + this.previousImage = null; + this.previousTransform = null; + this.targetLayer = null; + this.isExecuted = false; + + // 错误处理 + this.maxRetries = options.maxRetries || 3; + this.retryCount = 0; + this.timeoutMs = options.timeoutMs || 10000; + } + + async execute() { + try { + this.validateInputs(); + + // 查找目标图层 + this.targetLayer = this.findTargetLayer(); + if (!this.targetLayer) { + throw new Error(`找不到目标图层类型: ${this.targetLayerType}`); + } + + // 保存当前状态用于回滚 + await this.saveCurrentState(); + + // 加载新图像 + const newImage = await this.loadImageWithRetry(); + + // 应用图像到图层 + await this.applyImageToLayer(newImage); + + this.isExecuted = true; + + // 触发成功事件 + this.emitEvent("image:changed", { + layerId: this.targetLayer.id, + newImageUrl: this.imageUrl, + command: this, + }); + + return { + success: true, + layerId: this.targetLayer.id, + imageUrl: this.imageUrl, + }; + } catch (error) { + console.error("ChangeFixedImageCommand执行失败:", error); + + // 如果已经执行了部分操作,尝试回滚 + if (this.isExecuted) { + try { + await this.undo(); + } catch (rollbackError) { + console.error("回滚失败:", rollbackError); + } + } + + throw error; + } + } + + async undo() { + if (!this.isExecuted || !this.targetLayer) { + throw new Error("命令未执行或目标图层不存在"); + } + + try { + if (this.previousImage) { + // 恢复之前的图像 + await this.restorePreviousImage(); + } else { + // 如果没有之前的图像,移除当前图像 + await this.removeCurrentImage(); + } + + this.isExecuted = false; + + // 触发撤销事件 + this.emitEvent("image:reverted", { + layerId: this.targetLayer.id, + command: this, + }); + + return { + success: true, + action: "reverted", + layerId: this.targetLayer.id, + }; + } catch (error) { + console.error("ChangeFixedImageCommand撤销失败:", error); + throw error; + } + } + + validateInputs() { + if (!this.canvas) throw new Error("Canvas实例是必需的"); + if (!this.layerManager) throw new Error("LayerManager实例是必需的"); + if (!this.imageUrl) throw new Error("图像URL是必需的"); + + // 验证URL格式 + try { + new URL(this.imageUrl); + } catch { + throw new Error("无效的图像URL格式"); + } + } + + findTargetLayer() { + const layers = this.layerManager.layers?.value || []; + + switch (this.targetLayerType) { + case "background": + return layers.find((layer) => layer.isBackground); + case "fixed": + return layers.find((layer) => layer.isFixed); + default: + return layers.find((layer) => layer.type === this.targetLayerType); + } + } + + async saveCurrentState() { + if (!this.targetLayer.fabricObject) return; + + const currentObj = this.targetLayer.fabricObject; + + // 保存当前图像URL(如果存在) + this.previousImage = { + url: currentObj.getSrc ? currentObj.getSrc() : null, + element: currentObj._element ? currentObj._element.cloneNode() : null, + }; + + // 保存变换状态 + this.previousTransform = { + left: currentObj.left, + top: currentObj.top, + scaleX: currentObj.scaleX, + scaleY: currentObj.scaleY, + angle: currentObj.angle, + flipX: currentObj.flipX, + flipY: currentObj.flipY, + opacity: currentObj.opacity, + }; + } + + async loadImageWithRetry() { + for (let attempt = 0; attempt <= this.maxRetries; attempt++) { + try { + return await this.loadImage(); + } catch (error) { + this.retryCount = attempt; + + if (attempt === this.maxRetries) { + throw new Error( + `图像加载失败,已重试${this.maxRetries}次: ${error.message}` + ); + } + + // 指数退避重试 + const delay = Math.pow(2, attempt) * 1000; + await new Promise((resolve) => setTimeout(resolve, delay)); + + console.warn( + `图像加载重试 ${attempt + 1}/${this.maxRetries}:`, + error.message + ); + } + } + } + + loadImage() { + return new Promise((resolve, reject) => { + const timeout = setTimeout(() => { + reject( + new Error(`图像加载超时 (${this.timeoutMs}ms): ${this.imageUrl}`) + ); + }, this.timeoutMs); + + fabric.Image.fromURL( + this.imageUrl, + (img) => { + clearTimeout(timeout); + + if (!img || !img.getElement()) { + reject(new Error("图像加载失败或无效")); + return; + } + + resolve(img); + }, + { + crossOrigin: "anonymous", + } + ); + }); + } + + async applyImageToLayer(newImage) { + const currentObj = this.targetLayer.fabricObject; + + // 设置基本属性 + newImage.set({ + id: currentObj?.id || generateId(), + layerId: this.targetLayer.id, + layerName: this.targetLayer.name, + isBackground: this.targetLayer.isBackground, + isFixed: this.targetLayer.isFixed, + }); + + // 应用位置和变换 + if (this.preserveTransform && this.previousTransform) { + newImage.set(this.previousTransform); + } else { + newImage.set({ + left: this.position.x, + top: this.position.y, + scaleX: this.scale.x, + scaleY: this.scale.y, + }); + } + + // 移除旧对象(如果存在) + if (currentObj) { + this.canvas.remove(currentObj); + } + + // 添加新图像 + this.canvas.add(newImage); + newImage.setCoords(); + + // 更新图层引用 + this.targetLayer.fabricObject = newImage; + + // 更新图层管理器 + this.layerManager.updateLayerObject(this.targetLayer.id, newImage); + + // 重新渲染画布 + this.canvas.renderAll(); + } + + async restorePreviousImage() { + if (!this.previousImage.url) return; + + const restoredImage = await this.loadImageFromUrl(this.previousImage.url); + + // 恢复之前的变换 + if (this.previousTransform) { + restoredImage.set(this.previousTransform); + } + + // 设置图层属性 + restoredImage.set({ + id: this.targetLayer.fabricObject?.id || generateId(), + layerId: this.targetLayer.id, + layerName: this.targetLayer.name, + isBackground: this.targetLayer.isBackground, + isFixed: this.targetLayer.isFixed, + }); + + // 替换当前对象 + if (this.targetLayer.fabricObject) { + this.canvas.remove(this.targetLayer.fabricObject); + } + + this.canvas.add(restoredImage); + restoredImage.setCoords(); + + // 更新引用 + this.targetLayer.fabricObject = restoredImage; + this.layerManager.updateLayerObject(this.targetLayer.id, restoredImage); + + this.canvas.renderAll(); + } + + async removeCurrentImage() { + if (this.targetLayer.fabricObject) { + this.canvas.remove(this.targetLayer.fabricObject); + this.targetLayer.fabricObject = null; + this.layerManager.updateLayerObject(this.targetLayer.id, null); + this.canvas.renderAll(); + } + } + + loadImageFromUrl(url) { + return new Promise((resolve, reject) => { + fabric.Image.fromURL( + url, + (img) => { + if (!img || !img.getElement()) { + reject(new Error("恢复图像加载失败")); + return; + } + resolve(img); + }, + { crossOrigin: "anonymous" } + ); + }); + } + + emitEvent(eventName, data) { + if (this.canvas && this.canvas.fire) { + this.canvas.fire(eventName, data); + } + } + + // 获取命令信息用于调试 + getCommandInfo() { + return { + type: "ChangeFixedImageCommand", + targetLayerType: this.targetLayerType, + imageUrl: this.imageUrl, + isExecuted: this.isExecuted, + retryCount: this.retryCount, + targetLayerId: this.targetLayer?.id, + preserveTransform: this.preserveTransform, + }; + } +} + +/** + * 向图层添加图像命令 + * 用于向指定图层添加新的图像对象 + */ +export class AddImageToLayerCommand extends Command { + constructor(options = {}) { + super(); + this.canvas = options.canvas; + this.layerManager = options.layerManager; + this.imageUrl = options.imageUrl; + this.layerId = options.layerId; + this.position = options.position || { x: 100, y: 100 }; + this.scale = options.scale || { x: 1, y: 1 }; + this.zIndex = options.zIndex || null; // 可选的层级控制 + + // 用于回滚的状态 + this.addedObject = null; + this.targetLayer = null; + this.isExecuted = false; + + // 错误处理 + this.maxRetries = options.maxRetries || 3; + this.retryCount = 0; + this.timeoutMs = options.timeoutMs || 10000; + } + + async execute() { + try { + this.validateInputs(); + + // 查找目标图层 + this.targetLayer = this.findTargetLayer(); + if (!this.targetLayer) { + throw new Error(`找不到目标图层: ${this.layerId}`); + } + + // 检查图层是否可编辑 + this.validateLayerEditability(); + + // 加载新图像 + const newImage = await this.loadImageWithRetry(); + + // 添加图像到图层 + await this.addImageToLayer(newImage); + + this.isExecuted = true; + + // 触发成功事件 + this.emitEvent("image:added", { + layerId: this.layerId, + objectId: this.addedObject.id, + imageUrl: this.imageUrl, + command: this, + }); + + return { + success: true, + layerId: this.layerId, + objectId: this.addedObject.id, + imageUrl: this.imageUrl, + }; + } catch (error) { + console.error("AddImageToLayerCommand执行失败:", error); + + // 如果已经添加了对象,尝试移除 + if (this.addedObject) { + try { + await this.undo(); + } catch (rollbackError) { + console.error("回滚失败:", rollbackError); + } + } + + throw error; + } + } + + async undo() { + if (!this.isExecuted || !this.addedObject) { + throw new Error("命令未执行或没有添加的对象"); + } + + try { + // 移除添加的对象 + this.canvas.remove(this.addedObject); + + // 从图层管理器中移除 + this.layerManager.removeObjectFromLayer( + this.addedObject.id, + this.layerId + ); + + this.isExecuted = false; + + // 触发撤销事件 + this.emitEvent("image:removed", { + layerId: this.layerId, + objectId: this.addedObject.id, + command: this, + }); + + // 重新渲染 + this.canvas.renderAll(); + + return { + success: true, + action: "removed", + layerId: this.layerId, + objectId: this.addedObject.id, + }; + } catch (error) { + console.error("AddImageToLayerCommand撤销失败:", error); + throw error; + } + } + + validateInputs() { + if (!this.canvas) throw new Error("Canvas实例是必需的"); + if (!this.layerManager) throw new Error("LayerManager实例是必需的"); + if (!this.imageUrl) throw new Error("图像URL是必需的"); + if (!this.layerId) throw new Error("图层ID是必需的"); + + // 验证URL格式 + try { + new URL(this.imageUrl); + } catch { + throw new Error("无效的图像URL格式"); + } + } + + findTargetLayer() { + const layers = this.layerManager.layers?.value || []; + return layers.find((layer) => layer.id === this.layerId); + } + + validateLayerEditability() { + if (this.targetLayer.locked) { + throw new Error("目标图层已锁定,无法添加对象"); + } + + if (!this.targetLayer.visible) { + console.warn("目标图层不可见,添加的对象可能不会显示"); + } + } + + async loadImageWithRetry() { + for (let attempt = 0; attempt <= this.maxRetries; attempt++) { + try { + return await this.loadImage(); + } catch (error) { + this.retryCount = attempt; + + if (attempt === this.maxRetries) { + throw new Error( + `图像加载失败,已重试${this.maxRetries}次: ${error.message}` + ); + } + + // 指数退避重试 + const delay = Math.pow(2, attempt) * 1000; + await new Promise((resolve) => setTimeout(resolve, delay)); + + console.warn( + `图像加载重试 ${attempt + 1}/${this.maxRetries}:`, + error.message + ); + } + } + } + + loadImage() { + return new Promise((resolve, reject) => { + const timeout = setTimeout(() => { + reject( + new Error(`图像加载超时 (${this.timeoutMs}ms): ${this.imageUrl}`) + ); + }, this.timeoutMs); + + fabric.Image.fromURL( + this.imageUrl, + (img) => { + clearTimeout(timeout); + + if (!img || !img.getElement()) { + reject(new Error("图像加载失败或无效")); + return; + } + + resolve(img); + }, + { + crossOrigin: "anonymous", + } + ); + }); + } + + async addImageToLayer(newImage) { + // 生成唯一ID + const objectId = generateId(); + + // 设置图像属性 + newImage.set({ + id: objectId, + layerId: this.layerId, + layerName: this.targetLayer.name, + left: this.position.x, + top: this.position.y, + scaleX: this.scale.x, + scaleY: this.scale.y, + selectable: true, + evented: true, + }); + + // 添加到画布 + this.canvas.add(newImage); + + // 设置层级 + if (this.zIndex !== null) { + this.setObjectZIndex(newImage, this.zIndex); + } + + newImage.setCoords(); + + // 保存引用用于回滚 + this.addedObject = newImage; + + // 添加到图层管理器 + this.layerManager.addObjectToLayer(newImage, this.layerId); + + // 重新渲染画布 + this.canvas.renderAll(); + } + + setObjectZIndex(object, zIndex) { + if (zIndex === "top") { + object.bringToFront(); + } else if (zIndex === "bottom") { + object.sendToBack(); + } else if (typeof zIndex === "number") { + object.moveTo(zIndex); + } + } + + emitEvent(eventName, data) { + if (this.canvas && this.canvas.fire) { + this.canvas.fire(eventName, data); + } + } + + // 获取命令信息用于调试 + getCommandInfo() { + return { + type: "AddImageToLayerCommand", + layerId: this.layerId, + imageUrl: this.imageUrl, + position: this.position, + scale: this.scale, + isExecuted: this.isExecuted, + retryCount: this.retryCount, + addedObjectId: this.addedObject?.id, + }; + } +} diff --git a/src/component/Canvas/CanvasEditor/commands/RedGreenCommands.js b/src/component/Canvas/CanvasEditor/commands/RedGreenCommands.js new file mode 100644 index 00000000..27fc9ef2 --- /dev/null +++ b/src/component/Canvas/CanvasEditor/commands/RedGreenCommands.js @@ -0,0 +1,379 @@ +import { OperationType } from "../utils/layerHelper.js"; +import { Command, CompositeCommand } from "./Command.js"; +//import { fabric } from "fabric-with-all"; + +/** + * 批量初始化红绿图模式命令 + * 将衣服底图添加到背景层、红绿图添加到固定图层、调整位置和大小,以及设置画布背景为白色等操作合并到一个命令中 + * 减少页面闪烁,一次性渲染完成 + */ +export class BatchInitializeRedGreenModeCommand extends Command { + constructor(options = {}) { + super({ + name: "批量初始化红绿图模式", + description: "一次性完成红绿图模式的所有初始化操作", + }); + + this.canvas = options.canvas; + this.layerManager = options.layerManager; + this.toolManager = options.toolManager; + this.clothingImageUrl = options.clothingImageUrl; + this.redGreenImageUrl = options.redGreenImageUrl; + this.onImageGenerated = options.onImageGenerated; + this.normalLayerOpacity = options.normalLayerOpacity || 0.4; + + // 存储原始状态以便撤销 + this.originalCanvasBackground = null; + this.originalBackgroundObject = null; + this.originalFixedObjects = null; + this.originalNormalObjects = null; + this.originalNormalOpacities = new Map(); + this.originalToolState = null; + + // 存储加载的图片对象 + this.clothingImage = null; + this.redGreenImage = null; + } + + async execute() { + try { + // 禁用画布渲染以避免闪烁 + this.canvas.renderOnAddRemove = false; + + // 1. 设置画布背景为白色 + this.originalCanvasBackground = this.canvas.backgroundColor; + this.canvas.setBackgroundColor('#ffffff', () => {}); + + // 2. 查找图层结构 + const layers = this.layerManager.layers?.value || []; + const backgroundLayer = layers.find((layer) => layer.isBackground); + const fixedLayer = layers.find((layer) => layer.isFixed); + const normalLayers = layers.filter( + (layer) => !layer.isBackground && !layer.isFixed + ); + + if (!backgroundLayer || !fixedLayer || normalLayers.length === 0) { + throw new Error("缺少必要的图层结构"); + } + + const normalLayer = normalLayers[0]; // 使用第一个普通图层 + + // 3. 保存原始状态 + this.originalBackgroundObject = backgroundLayer.fabricObject ? { + ...backgroundLayer.fabricObject.toObject(), + ref: backgroundLayer.fabricObject + } : null; + + this.originalFixedObjects = fixedLayer.fabricObject + ? [fixedLayer.fabricObject] + : []; + + this.originalNormalObjects = normalLayer.fabricObjects + ? [...normalLayer.fabricObjects] + : []; + + // 保存普通图层透明度 + normalLayers.forEach((layer) => { + this.originalNormalOpacities.set(layer.id, layer.opacity || 1); + if (layer.fabricObjects) { + layer.fabricObjects.forEach((obj) => { + this.originalNormalOpacities.set( + `${layer.id}_${obj.id || "unknown"}`, + obj.opacity || 1 + ); + }); + } + }); + + // 保存工具状态 + if (this.toolManager) { + this.originalToolState = { + currentTool: this.toolManager.getCurrentTool(), + isRedGreenMode: this.toolManager.isRedGreenMode, + }; + } + + // 4. 确保背景图层大小正确 + await this._setupBackgroundLayer(backgroundLayer); + + // 5. 并行加载两个图片 + const [clothingImg, redGreenImg] = await Promise.all([ + this._loadImage(this.clothingImageUrl), + this._loadImage(this.redGreenImageUrl) + ]); + + // 6. 设置衣服底图到固定图层 + await this._setupClothingImage(clothingImg, fixedLayer); + + // 7. 设置红绿图到普通图层,位置和大小与衣服底图一致 + await this._setupRedGreenImage(redGreenImg, normalLayer, this.clothingImage); + + // 8. 设置普通图层透明度 + this._setupNormalLayerOpacity(normalLayers); + + // 9. 配置工具管理器 + this._setupToolManager(); + + // 10. 重新启用渲染并执行一次性渲染 + this.canvas.renderOnAddRemove = true; + this.canvas.renderAll(); + + console.log("批量红绿图模式初始化完成", { + 衣服底图: this.clothingImageUrl, + 红绿图: this.redGreenImageUrl, + 普通图层透明度: `${Math.round(this.normalLayerOpacity * 100)}%`, + 画布背景: "白色", + }); + + return true; + } catch (error) { + // 恢复渲染 + this.canvas.renderOnAddRemove = true; + console.error("批量红绿图模式初始化失败:", error); + throw error; + } + } + + async undo() { + try { + // 禁用渲染 + this.canvas.renderOnAddRemove = false; + + // 1. 恢复画布背景 + if (this.originalCanvasBackground !== null) { + this.canvas.setBackgroundColor(this.originalCanvasBackground, () => {}); + } + + // 2. 恢复图层对象 + const layers = this.layerManager.layers?.value || []; + const backgroundLayer = layers.find((layer) => layer.isBackground); + const fixedLayer = layers.find((layer) => layer.isFixed); + const normalLayers = layers.filter( + (layer) => !layer.isBackground && !layer.isFixed + ); + + // 移除当前添加的对象 + if (this.clothingImage) { + this.canvas.remove(this.clothingImage); + } + if (this.redGreenImage) { + this.canvas.remove(this.redGreenImage); + } + + // 恢复背景图层 + if (backgroundLayer && this.originalBackgroundObject) { + if (this.originalBackgroundObject.ref) { + backgroundLayer.fabricObject = this.originalBackgroundObject.ref; + } + } + + // 恢复固定图层 + if (fixedLayer) { + fixedLayer.fabricObject = this.originalFixedObjects.length > 0 + ? this.originalFixedObjects[0] + : null; + + if (fixedLayer.fabricObject) { + this.canvas.add(fixedLayer.fabricObject); + } + } + + // 恢复普通图层 + if (normalLayers.length > 0) { + const normalLayer = normalLayers[0]; + normalLayer.fabricObjects = [...this.originalNormalObjects]; + this.originalNormalObjects.forEach((obj) => { + this.canvas.add(obj); + }); + } + + // 3. 恢复透明度 + normalLayers.forEach((layer) => { + if (this.originalNormalOpacities.has(layer.id)) { + layer.opacity = this.originalNormalOpacities.get(layer.id); + } + + if (layer.fabricObjects) { + layer.fabricObjects.forEach((obj) => { + const key = `${layer.id}_${obj.id || "unknown"}`; + if (this.originalNormalOpacities.has(key)) { + obj.opacity = this.originalNormalOpacities.get(key); + } + }); + } + }); + + // 4. 恢复工具状态 + if (this.toolManager && this.originalToolState) { + this.toolManager.isRedGreenMode = this.originalToolState.isRedGreenMode; + if (this.originalToolState.currentTool) { + this.toolManager.setTool(this.originalToolState.currentTool); + } + } + + // 5. 重新启用渲染 + this.canvas.renderOnAddRemove = true; + this.canvas.renderAll(); + + return true; + } catch (error) { + this.canvas.renderOnAddRemove = true; + console.error("撤销批量红绿图模式初始化失败:", error); + return false; + } + } + + /** + * 设置背景图层 + */ + async _setupBackgroundLayer(backgroundLayer) { + let backgroundObject = backgroundLayer.fabricObject; + + if (!backgroundObject) { + // 创建白色背景矩形 + backgroundObject = new fabric.Rect({ + left: 0, + top: 0, + width: this.canvas.width, + height: this.canvas.height, + fill: "#ffffff", + selectable: false, + evented: false, + isBackground: true, + layerId: backgroundLayer.id, + layerName: backgroundLayer.name, + }); + + this.canvas.add(backgroundObject); + this.canvas.sendToBack(backgroundObject); + backgroundLayer.fabricObject = backgroundObject; + } else { + // 更新现有背景对象大小 + backgroundObject.set({ + width: this.canvas.width, + height: this.canvas.height, + left: 0, + top: 0, + fill: "#ffffff", // 确保背景是白色 + }); + } + } + + /** + * 加载图片 + */ + async _loadImage(imageUrl) { + return new Promise((resolve, reject) => { + fabric.Image.fromURL( + imageUrl, + (img) => { + if (!img) { + reject(new Error(`无法加载图片: ${imageUrl}`)); + return; + } + resolve(img); + }, + { crossOrigin: "anonymous" } + ); + }); + } + + /** + * 设置衣服底图 + */ + async _setupClothingImage(img, fixedLayer) { + // 计算图片缩放,保持上下留边距 + const margin = 50; + const maxWidth = this.canvas.width - margin * 2; + const maxHeight = this.canvas.height - margin * 2; + const scale = Math.min(maxWidth / img.width, maxHeight / img.height); + + img.set({ + scaleX: scale, + scaleY: scale, + left: this.canvas.width / 2, + top: this.canvas.height / 2, + originX: "center", + originY: "center", + selectable: false, + evented: false, + layerId: fixedLayer.id, + layerName: fixedLayer.name, + }); + + // 清除固定图层原有内容 + if (fixedLayer.fabricObject) { + this.canvas.remove(fixedLayer.fabricObject); + } + + // 添加到画布和固定图层 + this.canvas.add(img); + fixedLayer.fabricObject = img; + this.clothingImage = img; + } + + /** + * 设置红绿图 + */ + async _setupRedGreenImage(img, normalLayer, clothingImage) { + if (!clothingImage) { + throw new Error("衣服底图未加载,无法设置红绿图位置"); + } + + // 使用与衣服底图完全相同的属性 + img.set({ + scaleX: clothingImage.scaleX, + scaleY: clothingImage.scaleY, + left: clothingImage.left, + top: clothingImage.top, + originX: clothingImage.originX, + originY: clothingImage.originY, + selectable: false, + evented: false, + layerId: normalLayer.id, + layerName: normalLayer.name, + }); + + // 清除普通图层原有内容 + if (normalLayer.fabricObjects) { + normalLayer.fabricObjects.forEach((obj) => { + this.canvas.remove(obj); + }); + } + + // 添加到画布和普通图层 + this.canvas.add(img); + normalLayer.fabricObjects = [img]; + this.redGreenImage = img; + } + + /** + * 设置普通图层透明度 + */ + _setupNormalLayerOpacity(normalLayers) { + normalLayers.forEach((layer) => { + // 设置图层透明度 + layer.opacity = this.normalLayerOpacity; + + // 更新图层中所有对象的透明度 + if (layer.fabricObjects) { + layer.fabricObjects.forEach((obj) => { + obj.opacity = this.normalLayerOpacity; + }); + } + }); + } + + /** + * 设置工具管理器 + */ + _setupToolManager() { + if (this.toolManager) { + // 设置红绿图模式 + this.toolManager.isRedGreenMode = true; + + // 切换到红色笔刷工具 + this.toolManager.setTool(OperationType.RED_BRUSH); + } + } +} diff --git a/src/component/Canvas/CanvasEditor/commands/SelectionCommands.js b/src/component/Canvas/CanvasEditor/commands/SelectionCommands.js new file mode 100644 index 00000000..de1c3f58 --- /dev/null +++ b/src/component/Canvas/CanvasEditor/commands/SelectionCommands.js @@ -0,0 +1,578 @@ +import { Command, CompositeCommand } from "./Command.js"; +//import { fabric } from "fabric-with-all"; +import { createLayer, LayerType } from "../utils/layerHelper.js"; + +/** + * 创建选区命令 + */ +export class CreateSelectionCommand extends Command { + constructor(options = {}) { + super({ + name: options.name || "创建选区", + description: "在画布上创建选区", + saveState: false, + }); + this.canvas = options.canvas; + this.selectionManager = options.selectionManager; + this.selectionObject = options.selectionObject; + this.selectionType = options.selectionType || "rectangle"; + } + + async execute() { + if (!this.canvas || !this.selectionManager || !this.selectionObject) { + console.error("无法创建选区:参数无效"); + return false; + } + + // 将选择对象添加到选区管理器 + this.selectionManager.setSelectionObject(this.selectionObject); + return true; + } + + async undo() { + if (!this.selectionManager) return false; + this.selectionManager.clearSelection(); + return true; + } +} + +/** + * 反转选区命令 + */ +export class InvertSelectionCommand extends Command { + constructor(options = {}) { + super({ + name: "反转选区", + description: "反转当前选区", + saveState: false, + }); + this.canvas = options.canvas; + this.selectionManager = options.selectionManager; + this.originalSelection = options.selectionManager + ? options.selectionManager.getSelectionPath() + : null; + } + + async execute() { + if (!this.canvas || !this.selectionManager) { + console.error("无法反转选区:参数无效"); + return false; + } + + // 保存原始选区 + if (!this.originalSelection) { + this.originalSelection = this.selectionManager.getSelectionPath(); + } + + // 反转选区 + const result = await this.selectionManager.invertSelection(); + return result; + } + + async undo() { + if (!this.selectionManager || !this.originalSelection) return false; + + // 恢复原始选区 + this.selectionManager.setSelectionFromPath(this.originalSelection); + return true; + } +} + +/** + * 添加到选区命令 + */ +export class AddToSelectionCommand extends Command { + constructor(options = {}) { + super({ + name: "添加到选区", + description: "将新的选区添加到现有选区", + saveState: false, + }); + this.canvas = options.canvas; + this.selectionManager = options.selectionManager; + this.newSelection = options.newSelection; + this.originalSelection = options.selectionManager + ? options.selectionManager.getSelectionPath() + : null; + } + + async execute() { + if (!this.canvas || !this.selectionManager || !this.newSelection) { + console.error("无法添加到选区:参数无效"); + return false; + } + + // 保存原始选区 + if (!this.originalSelection) { + this.originalSelection = this.selectionManager.getSelectionPath(); + } + + // 添加到选区 + const result = await this.selectionManager.addToSelection( + this.newSelection + ); + return result; + } + + async undo() { + if (!this.selectionManager || !this.originalSelection) return false; + + // 恢复原始选区 + this.selectionManager.setSelectionFromPath(this.originalSelection); + return true; + } +} + +/** + * 从选区中移除命令 + */ +export class RemoveFromSelectionCommand extends Command { + constructor(options = {}) { + super({ + name: "从选区中移除", + description: "从现有选区中移除指定区域", + saveState: false, + }); + this.canvas = options.canvas; + this.selectionManager = options.selectionManager; + this.removeSelection = options.removeSelection; + this.originalSelection = options.selectionManager + ? options.selectionManager.getSelectionPath() + : null; + } + + async execute() { + if (!this.canvas || !this.selectionManager || !this.removeSelection) { + console.error("无法从选区中移除:参数无效"); + return false; + } + + // 保存原始选区 + if (!this.originalSelection) { + this.originalSelection = this.selectionManager.getSelectionPath(); + } + + // 从选区中移除 + const result = await this.selectionManager.removeFromSelection( + this.removeSelection + ); + return result; + } + + async undo() { + if (!this.selectionManager || !this.originalSelection) return false; + + // 恢复原始选区 + this.selectionManager.setSelectionFromPath(this.originalSelection); + return true; + } +} + +/** + * 清除选区命令 + */ +export class ClearSelectionCommand extends Command { + constructor(options = {}) { + super({ + name: "清除选区", + description: "清除当前选区", + saveState: false, + }); + this.selectionManager = options.selectionManager; + this.originalSelection = options.selectionManager + ? options.selectionManager.getSelectionPath() + : null; + } + + async execute() { + if (!this.selectionManager) { + console.error("无法清除选区:参数无效"); + return false; + } + + // 保存原始选区 + if (!this.originalSelection) { + this.originalSelection = this.selectionManager.getSelectionPath(); + } + + // 清除选区 + this.selectionManager.clearSelection(); + return true; + } + + async undo() { + if (!this.selectionManager || !this.originalSelection) return false; + + // 恢复原始选区 + this.selectionManager.setSelectionFromPath(this.originalSelection); + return true; + } +} + +/** + * 羽化选区命令 + */ +export class FeatherSelectionCommand extends Command { + constructor(options = {}) { + super({ + name: "羽化选区", + description: "对当前选区应用羽化效果", + saveState: false, + }); + this.selectionManager = options.selectionManager; + this.featherAmount = options.featherAmount || 5; + this.originalSelection = options.selectionManager + ? options.selectionManager.getSelectionPath() + : null; + this.originalFeatherAmount = options.selectionManager + ? options.selectionManager.getFeatherAmount() + : 0; + } + + async execute() { + if (!this.selectionManager) { + console.error("无法羽化选区:参数无效"); + return false; + } + + // 保存原始选区和羽化值 + if (!this.originalSelection) { + this.originalSelection = this.selectionManager.getSelectionPath(); + this.originalFeatherAmount = this.selectionManager.getFeatherAmount(); + } + + // 应用羽化 + const result = await this.selectionManager.featherSelection( + this.featherAmount + ); + return result; + } + + async undo() { + if (!this.selectionManager || !this.originalSelection) return false; + + // 恢复原始选区和羽化值 + this.selectionManager.setSelectionFromPath(this.originalSelection); + this.selectionManager.setFeatherAmount(this.originalFeatherAmount); + return true; + } +} + +/** + * 填充选区命令 + */ +export class FillSelectionCommand extends Command { + constructor(options = {}) { + super({ + name: "填充选区", + description: "使用指定颜色填充当前选区", + saveState: false, + }); + this.canvas = options.canvas; + this.layerManager = options.layerManager; + this.selectionManager = options.selectionManager; + this.color = options.color || "#000000"; + this.targetLayerId = options.targetLayerId; + this.createdObjectIds = []; + } + + async execute() { + if (!this.canvas || !this.layerManager || !this.selectionManager) { + console.error("无法填充选区:参数无效"); + return false; + } + + // 获取选区路径 + const selectionPath = this.selectionManager.getSelectionObject(); + if (!selectionPath) { + console.error("无法填充选区:当前没有选区"); + return false; + } + + // 确定目标图层 + const layerId = this.targetLayerId || this.layerManager.getActiveLayerId(); + if (!layerId) { + console.error("无法填充选区:没有活动图层"); + return false; + } + + // 创建填充对象 + const fillObject = new fabric.Path(selectionPath.path, { + fill: this.color, + stroke: null, + opacity: 1, + id: `fill_${Date.now()}_${Math.floor(Math.random() * 1000)}`, + layerId: layerId, + selectable: false, + }); + + // 应用羽化效果(如果有) + const featherAmount = this.selectionManager.getFeatherAmount(); + if (featherAmount > 0) { + fillObject.shadow = new fabric.Shadow({ + color: this.color, + blur: featherAmount, + offsetX: 0, + offsetY: 0, + }); + } + + // 添加到图层 + this.layerManager.addObjectToLayer(layerId, fillObject); + this.createdObjectIds.push(fillObject.id); + + // 清空选区 + this.selectionManager.clearSelection(); + return true; + } + + async undo() { + if (!this.layerManager || this.createdObjectIds.length === 0) return false; + + // 移除创建的填充对象 + for (const id of this.createdObjectIds) { + const layerObj = this._findObjectInLayers(id); + if (layerObj) { + this.layerManager.removeObjectFromLayer(layerObj.layerId, id); + } + } + return true; + } + + _findObjectInLayers(objectId) { + if (!this.layerManager) return null; + + const layers = this.layerManager.layers.value; + for (const layer of layers) { + if (layer.fabricObjects) { + const obj = layer.fabricObjects.find((obj) => obj.id === objectId); + if (obj) return { object: obj, layerId: layer.id }; + } + } + return null; + } +} + +/** + * 复制选区内容到新图层命令 + */ +export class CopySelectionToNewLayerCommand extends CompositeCommand { + constructor(options = {}) { + super([], { + name: "复制选区到新图层", + description: "将选区中的内容复制到新图层", + }); + this.canvas = options.canvas; + this.layerManager = options.layerManager; + this.selectionManager = options.selectionManager; + this.sourceLayerId = options.sourceLayerId; + this.newLayerName = options.newLayerName || "选区复制"; + this.newLayerId = null; + this.copiedObjectIds = []; + } + + async execute() { + if (!this.canvas || !this.layerManager || !this.selectionManager) { + console.error("无法复制选区:参数无效"); + return false; + } + + try { + // 获取选区 + const selectionObject = this.selectionManager.getSelectionObject(); + if (!selectionObject) { + console.error("无法复制选区:当前没有选区"); + return false; + } + + // 确定源图层 + const sourceId = + this.sourceLayerId || this.layerManager.getActiveLayerId(); + const sourceLayer = this.layerManager.getLayerById(sourceId); + if (!sourceLayer || !sourceLayer.fabricObjects) { + console.error("无法复制选区:源图层无效或为空"); + return false; + } + + // 创建新图层 + this.newLayerId = await this.layerManager.createLayer( + this.newLayerName, + LayerType.EMPTY + ); + + // 获取选区内的对象 + const objectsToCopy = sourceLayer.fabricObjects.filter((obj) => { + return this.selectionManager.isObjectInSelection(obj); + }); + + if (objectsToCopy.length === 0) { + console.warn("选区内没有对象可复制"); + return true; // 仍然返回成功,因为已创建了新图层 + } + + // 复制对象到新图层 + for (const obj of objectsToCopy) { + // 克隆对象 + const clonedObj = await this._cloneObject(obj); + // 设置新的ID和图层ID + const newId = `copy_${Date.now()}_${Math.floor(Math.random() * 1000)}`; + clonedObj.id = newId; + clonedObj.layerId = this.newLayerId; + + // 添加到新图层 + await this.layerManager.addObjectToLayer(this.newLayerId, clonedObj); + this.copiedObjectIds.push(newId); + } + + // 设置新图层为活动图层 + this.layerManager.setActiveLayer(this.newLayerId); + + // 清空选区 + this.selectionManager.clearSelection(); + + return { + newLayerId: this.newLayerId, + copiedCount: this.copiedObjectIds.length, + }; + } catch (error) { + console.error("复制选区过程中出错:", error); + + // 如果已经创建了新图层,需要进行清理 + if (this.newLayerId) { + try { + await this.layerManager.removeLayer(this.newLayerId); + } catch (cleanupError) { + console.warn("清理新图层失败:", cleanupError); + } + } + + throw error; + } + } + + async _cloneObject(obj) { + return new Promise((resolve, reject) => { + if (!obj) { + reject(new Error("对象无效,无法克隆")); + return; + } + + try { + obj.clone((cloned) => { + resolve(cloned); + }); + } catch (error) { + reject(error); + } + }); + } +} + +/** + * 从选区中删除内容命令 + */ +export class ClearSelectionContentCommand extends Command { + constructor(options = {}) { + super({ + name: "清除选区内容", + description: "删除选区中的内容", + saveState: false, + }); + this.canvas = options.canvas; + this.layerManager = options.layerManager; + this.selectionManager = options.selectionManager; + this.targetLayerId = options.targetLayerId; + this.removedObjects = []; + } + + async execute() { + if (!this.canvas || !this.layerManager || !this.selectionManager) { + console.error("无法清除选区内容:参数无效"); + return false; + } + + // 获取选区 + const selectionObject = this.selectionManager.getSelectionObject(); + if (!selectionObject) { + console.error("无法清除选区内容:当前没有选区"); + return false; + } + + // 确定目标图层 + const layerId = this.targetLayerId || this.layerManager.getActiveLayerId(); + const layer = this.layerManager.getLayerById(layerId); + if (!layer || !layer.fabricObjects) { + console.error("无法清除选区内容:目标图层无效或为空"); + return false; + } + + // 找到选区内的对象 + const objectsToRemove = layer.fabricObjects.filter((obj) => { + return this.selectionManager.isObjectInSelection(obj); + }); + + if (objectsToRemove.length === 0) { + console.warn("选区内没有对象需要清除"); + return true; + } + + // 备份被删除的对象 + this.removedObjects = objectsToRemove.map((obj) => ({ + object: this._cloneObjectSync(obj), + layerId: layerId, + })); + + // 从图层中移除对象 + for (const obj of objectsToRemove) { + this.layerManager.removeObjectFromLayer(layerId, obj.id); + } + + return { removedCount: objectsToRemove.length }; + } + + async undo() { + if (!this.layerManager || this.removedObjects.length === 0) return false; + + // 恢复被删除的对象 + for (const item of this.removedObjects) { + if (item.object && item.layerId) { + const clonedObj = await this._cloneObject(item.object); + this.layerManager.addObjectToLayer(item.layerId, clonedObj); + } + } + return true; + } + + _cloneObjectSync(obj) { + // 这是一个简单的深拷贝,不适用于所有场景 + // 在实际应用中,应该使用fabric.js的clone方法 + if (!obj) return null; + return JSON.parse(JSON.stringify(obj)); + } + + async _cloneObject(obj) { + return new Promise((resolve, reject) => { + if (!obj) { + reject(new Error("对象无效,无法克隆")); + return; + } + + try { + if (typeof obj.clone === "function") { + obj.clone((cloned) => { + resolve(cloned); + }); + } else { + // 如果对象没有clone方法(可能是因为它已经是序列化后的对象) + // 在实际代码中需要适当处理这种情况 + resolve(Object.assign({}, obj)); + } + } catch (error) { + reject(error); + } + }); + } +} + +// 导入套索抠图命令 +export { LassoCutoutCommand } from "./LassoCutoutCommand.js"; diff --git a/src/component/Canvas/CanvasEditor/commands/StateCommands.js b/src/component/Canvas/CanvasEditor/commands/StateCommands.js new file mode 100644 index 00000000..10ebb15a --- /dev/null +++ b/src/component/Canvas/CanvasEditor/commands/StateCommands.js @@ -0,0 +1,134 @@ +import { Command } from "./Command"; + +/** + * 对象变换命令 + * 轻量级命令,只记录对象的变换属性变化(位置、缩放、旋转) + * 不保存整个对象或画布状态,只关注变换属性 + */ +export class TransformCommand extends Command { + constructor(options) { + super({ + name: options.name || "对象变换", + description: options.description || "移动、缩放或旋转对象", + saveState: false, // 自己管理状态,避免递归 + }); + + this.canvas = options.canvas; + this.objectId = options.objectId; + this.initialState = options.initialState || null; + this.finalState = options.finalState || null; + this.objectType = options.objectType || "object"; + } + + /** + * 执行命令 + * 如果是首次执行,记录初始和最终状态 + * 如果是重做,应用最终状态 + */ + execute() { + if (!this.finalState) { + console.warn("没有最终状态可应用"); + return false; + } + + // 查找目标对象 + const targetObject = this._findObject(this.objectId); + if (!targetObject) { + console.warn(`未找到ID为 ${this.objectId} 的对象`); + return false; + } + + // 应用最终变换状态 + this._applyTransform(targetObject, this.finalState); + + // 触发画布更新 + this.canvas.renderAll(); + + return true; + } + + /** + * 撤销命令 + * 应用初始状态 + */ + undo() { + if (!this.initialState) { + console.warn("没有初始状态可恢复"); + return false; + } + + // 查找目标对象 + const targetObject = this._findObject(this.objectId); + if (!targetObject) { + console.warn(`未找到ID为 ${this.objectId} 的对象`); + return false; + } + + // 应用初始变换状态 + this._applyTransform(targetObject, this.initialState); + + // 触发画布更新 + this.canvas.renderAll(); + + return true; + } + + /** + * 查找对象 + * @private + */ + _findObject(objectId) { + if (!this.canvas) return null; + return this.canvas.getObjects().find((obj) => obj.id === objectId); + } + + /** + * 应用变换状态到对象 + * @private + */ + _applyTransform(object, transformState) { + if (!object || !transformState) return; + + // 应用变换属性,只设置真正变化的值 + Object.entries(transformState).forEach(([key, value]) => { + object.set(key, value); + }); + + // 确保对象更新 + object.setCoords(); + } + + /** + * 获取命令信息 + */ + getInfo() { + return { + name: this.name, + description: this.description, + objectId: this.objectId, + objectType: this.objectType, + changedProps: this.finalState ? Object.keys(this.finalState) : [], + }; + } + + /** + * 捕获对象的变换状态 + * @static + */ + static captureTransformState(object) { + if (!object) return null; + + // 只捕获变换相关的属性 + return { + left: object.left, + top: object.top, + scaleX: object.scaleX, + scaleY: object.scaleY, + angle: object.angle, + flipX: object.flipX, + flipY: object.flipY, + skewX: object.skewX, + skewY: object.skewY, + }; + } +} diff --git a/src/component/Canvas/CanvasEditor/commands/TextCommands.js b/src/component/Canvas/CanvasEditor/commands/TextCommands.js new file mode 100644 index 00000000..b9604f3f --- /dev/null +++ b/src/component/Canvas/CanvasEditor/commands/TextCommands.js @@ -0,0 +1,304 @@ +import { Command } from "./Command"; + +/** + * 文本内容命令 + * 用于更改文本图层的文本内容 + */ +export class TextContentCommand extends Command { + constructor(options) { + super({ + name: "修改文本内容", + description: "修改文本图层的文本内容", + }); + this.canvas = options.canvas; + this.textObject = options.textObject; + this.newText = options.newText; + this.oldText = this.textObject.text; + } + + execute() { + this.textObject.set("text", this.newText); + this.canvas.renderAll(); + return true; + } + + undo() { + this.textObject.set("text", this.oldText); + this.canvas.renderAll(); + return true; + } +} + +/** + * 文本字体命令 + * 用于更改文本图层的字体 + */ +export class TextFontCommand extends Command { + constructor(options) { + super({ + name: "修改文本字体", + description: "修改文本图层的字体", + }); + this.canvas = options.canvas; + this.textObject = options.textObject; + this.newFont = options.newFont; + this.oldFont = this.textObject.fontFamily; + } + + execute() { + this.textObject.set("fontFamily", this.newFont); + this.canvas.renderAll(); + return true; + } + + undo() { + this.textObject.set("fontFamily", this.oldFont); + this.canvas.renderAll(); + return true; + } +} + +/** + * 文本尺寸命令 + * 用于更改文本图层的字体大小 + */ +export class TextSizeCommand extends Command { + constructor(options) { + super({ + name: "修改文本尺寸", + description: "修改文本图层的字体大小", + }); + this.canvas = options.canvas; + this.textObject = options.textObject; + this.newSize = options.newSize; + this.oldSize = this.textObject.fontSize; + } + + execute() { + this.textObject.set("fontSize", this.newSize); + this.canvas.renderAll(); + return true; + } + + undo() { + this.textObject.set("fontSize", this.oldSize); + this.canvas.renderAll(); + return true; + } +} + +/** + * 文本颜色命令 + * 用于更改文本图层的颜色 + */ +export class TextColorCommand extends Command { + constructor(options) { + super({ + name: "修改文本颜色", + description: "修改文本图层的颜色", + }); + this.canvas = options.canvas; + this.textObject = options.textObject; + this.newColor = options.newColor; + this.oldColor = this.textObject.fill; + } + + execute() { + this.textObject.set("fill", this.newColor); + this.canvas.renderAll(); + return true; + } + + undo() { + this.textObject.set("fill", this.oldColor); + this.canvas.renderAll(); + return true; + } +} + +/** + * 文本对齐方式命令 + * 用于更改文本图层的对齐方式 + */ +export class TextAlignCommand extends Command { + constructor(options) { + super({ + name: "修改文本对齐", + description: "修改文本图层的对齐方式", + }); + this.canvas = options.canvas; + this.textObject = options.textObject; + this.newAlign = options.newAlign; + this.oldAlign = this.textObject.textAlign; + } + + execute() { + this.textObject.set("textAlign", this.newAlign); + this.canvas.renderAll(); + return true; + } + + undo() { + this.textObject.set("textAlign", this.oldAlign); + this.canvas.renderAll(); + return true; + } +} + +/** + * 文本样式命令 + * 用于更改文本图层的样式(粗体、斜体、下划线等) + */ +export class TextStyleCommand extends Command { + constructor(options) { + super({ + name: "修改文本样式", + description: "修改文本图层的样式", + }); + this.canvas = options.canvas; + this.textObject = options.textObject; + this.property = options.property; // 'fontWeight', 'fontStyle', 'underline', 'linethrough', 'overline' + this.newValue = options.newValue; + this.oldValue = this.textObject[this.property]; + } + + execute() { + this.textObject.set(this.property, this.newValue); + this.canvas.renderAll(); + return true; + } + + undo() { + this.textObject.set(this.property, this.oldValue); + this.canvas.renderAll(); + return true; + } +} + +/** + * 文本间距命令 + * 用于更改文本图层的字符间距或行高 + */ +export class TextSpacingCommand extends Command { + constructor(options) { + super({ + name: "修改文本间距", + description: "修改文本图层的字符间距或行高", + }); + this.canvas = options.canvas; + this.textObject = options.textObject; + this.property = options.property; // 'charSpacing' 或 'lineHeight' + this.newValue = options.newValue; + this.oldValue = this.textObject[this.property]; + } + + execute() { + this.textObject.set(this.property, this.newValue); + this.canvas.renderAll(); + return true; + } + + undo() { + this.textObject.set(this.property, this.oldValue); + this.canvas.renderAll(); + return true; + } +} + +/** + * 文本背景颜色命令 + * 用于更改文本图层的背景颜色 + */ +export class TextBackgroundCommand extends Command { + constructor(options) { + super({ + name: "修改文本背景", + description: "修改文本图层的背景颜色", + }); + this.canvas = options.canvas; + this.textObject = options.textObject; + this.newColor = options.newColor; + this.oldColor = this.textObject.textBackgroundColor; + } + + execute() { + this.textObject.set("textBackgroundColor", this.newColor); + this.canvas.renderAll(); + return true; + } + + undo() { + this.textObject.set("textBackgroundColor", this.oldColor); + this.canvas.renderAll(); + return true; + } +} + +/** + * 文本透明度命令 + * 用于更改文本图层的透明度 + */ +export class TextOpacityCommand extends Command { + constructor(options) { + super({ + name: "修改文本透明度", + description: "修改文本图层的透明度", + }); + this.canvas = options.canvas; + this.textObject = options.textObject; + this.newOpacity = options.newOpacity; + this.oldOpacity = this.textObject.opacity; + } + + execute() { + this.textObject.set("opacity", this.newOpacity); + this.canvas.renderAll(); + return true; + } + + undo() { + this.textObject.set("opacity", this.oldOpacity); + this.canvas.renderAll(); + return true; + } +} + +/** + * 组合文本编辑命令 + * 用于一次性应用多个文本属性更改 + */ +export class CompositeTextCommand extends Command { + constructor(options) { + super({ + name: "组合文本编辑", + description: "组合多个文本编辑操作", + }); + this.canvas = options.canvas; + this.textObject = options.textObject; + this.changes = options.changes; // {property: newValue} 形式的对象 + this.oldValues = {}; + + // 保存所有属性的旧值 + for (const property in this.changes) { + if (this.textObject[property] !== undefined) { + this.oldValues[property] = this.textObject[property]; + } + } + } + + execute() { + for (const property in this.changes) { + this.textObject.set(property, this.changes[property]); + } + this.canvas.renderAll(); + return true; + } + + undo() { + for (const property in this.oldValues) { + this.textObject.set(property, this.oldValues[property]); + } + this.canvas.renderAll(); + return true; + } +} diff --git a/src/component/Canvas/CanvasEditor/commands/ToolCommands.js b/src/component/Canvas/CanvasEditor/commands/ToolCommands.js new file mode 100644 index 00000000..e39f8668 --- /dev/null +++ b/src/component/Canvas/CanvasEditor/commands/ToolCommands.js @@ -0,0 +1,54 @@ +import { Command } from "./Command"; + +/** + * 工具切换命令 + * 用于切换编辑器的工具模式(如绘画、选择、橡皮擦等) + */ +export class ToolCommand extends Command { + /** + * 创建一个工具切换命令 + * @param {Object} options 配置选项 + * @param {Object} options.toolManager 工具管理器实例 + * @param {String} options.tool 要设置的工具名称 + * @param {String} options.previousTool 先前的工具名称(可选,如果不提供会在执行时记录) + * @param {Boolean} options.saveState 是否保存画布状态(默认为false) + */ + constructor(options) { + super({ + ...options, + name: `切换工具: ${options.tool}`, + description: `将工具切换为 ${options.tool}`, + }); + + this.toolManager = options.toolManager; + this.tool = options.tool; + this.previousTool = options.previousTool || null; + } + + /** + * 执行工具切换 + * @returns {String} 设置的工具名称 + */ + execute() { + if (!this.toolManager) return null; + + // 记录当前工具(用于撤销) + if (!this.previousTool) { + this.previousTool = this.toolManager.getCurrentTool(); + } + + // 切换工具 + return this.toolManager.setTool(this.tool); + } + + /** + * 撤销工具切换 + * @returns {String} 恢复的工具名称 + */ + undo() { + if (!this.toolManager || !this.previousTool) return null; + + // 恢复到先前工具 + return this.toolManager.setTool(this.previousTool); + } +} diff --git a/src/component/Canvas/CanvasEditor/components/BrushControlPanel.vue b/src/component/Canvas/CanvasEditor/components/BrushControlPanel.vue new file mode 100644 index 00000000..5ae0ef61 --- /dev/null +++ b/src/component/Canvas/CanvasEditor/components/BrushControlPanel.vue @@ -0,0 +1,685 @@ + + + + + diff --git a/src/component/Canvas/CanvasEditor/components/BrushPanel.vue b/src/component/Canvas/CanvasEditor/components/BrushPanel.vue new file mode 100644 index 00000000..5cbbe5b2 --- /dev/null +++ b/src/component/Canvas/CanvasEditor/components/BrushPanel.vue @@ -0,0 +1,1596 @@ + + + + + diff --git a/src/component/Canvas/CanvasEditor/components/HeaderMenu.vue b/src/component/Canvas/CanvasEditor/components/HeaderMenu.vue new file mode 100644 index 00000000..de9b697b --- /dev/null +++ b/src/component/Canvas/CanvasEditor/components/HeaderMenu.vue @@ -0,0 +1,525 @@ + + + + + diff --git a/src/component/Canvas/CanvasEditor/components/KeyboardShortcutHelp.vue b/src/component/Canvas/CanvasEditor/components/KeyboardShortcutHelp.vue new file mode 100644 index 00000000..b8f09044 --- /dev/null +++ b/src/component/Canvas/CanvasEditor/components/KeyboardShortcutHelp.vue @@ -0,0 +1,506 @@ + + + + + diff --git a/src/component/Canvas/CanvasEditor/components/LayersPanel.vue b/src/component/Canvas/CanvasEditor/components/LayersPanel.vue new file mode 100644 index 00000000..98e6a8fb --- /dev/null +++ b/src/component/Canvas/CanvasEditor/components/LayersPanel.vue @@ -0,0 +1,1062 @@ + + + + + diff --git a/src/component/Canvas/CanvasEditor/components/LiquifyPanel.vue b/src/component/Canvas/CanvasEditor/components/LiquifyPanel.vue new file mode 100644 index 00000000..057ad9bf --- /dev/null +++ b/src/component/Canvas/CanvasEditor/components/LiquifyPanel.vue @@ -0,0 +1,1388 @@ + + + + + diff --git a/src/component/Canvas/CanvasEditor/components/MinimapPanel.vue b/src/component/Canvas/CanvasEditor/components/MinimapPanel.vue new file mode 100644 index 00000000..aff7dccd --- /dev/null +++ b/src/component/Canvas/CanvasEditor/components/MinimapPanel.vue @@ -0,0 +1,146 @@ + + + + + diff --git a/src/component/Canvas/CanvasEditor/components/SelectionPanel.vue b/src/component/Canvas/CanvasEditor/components/SelectionPanel.vue new file mode 100644 index 00000000..4ff22836 --- /dev/null +++ b/src/component/Canvas/CanvasEditor/components/SelectionPanel.vue @@ -0,0 +1,814 @@ + + + + + diff --git a/src/component/Canvas/CanvasEditor/components/TextEditorPanel.vue b/src/component/Canvas/CanvasEditor/components/TextEditorPanel.vue new file mode 100644 index 00000000..af15a51b --- /dev/null +++ b/src/component/Canvas/CanvasEditor/components/TextEditorPanel.vue @@ -0,0 +1,1104 @@ + + + + + + diff --git a/src/component/Canvas/CanvasEditor/components/ToolsSidebar.vue b/src/component/Canvas/CanvasEditor/components/ToolsSidebar.vue new file mode 100644 index 00000000..57e3a944 --- /dev/null +++ b/src/component/Canvas/CanvasEditor/components/ToolsSidebar.vue @@ -0,0 +1,411 @@ + + + + + diff --git a/src/component/Canvas/CanvasEditor/components/VerticalSlider.vue b/src/component/Canvas/CanvasEditor/components/VerticalSlider.vue new file mode 100644 index 00000000..088ae04e --- /dev/null +++ b/src/component/Canvas/CanvasEditor/components/VerticalSlider.vue @@ -0,0 +1,688 @@ + + + + + diff --git a/src/component/Canvas/CanvasEditor/config/canvasConfig.js b/src/component/Canvas/CanvasEditor/config/canvasConfig.js new file mode 100644 index 00000000..5d7595ed --- /dev/null +++ b/src/component/Canvas/CanvasEditor/config/canvasConfig.js @@ -0,0 +1,32 @@ +import { OperationType } from "../utils/layerHelper"; + +// 画布默认配置 +export const canvasConfig = { + width: 1024, // 画布宽度 + height: 1024, // 画布高度 + backgroundColor: "#ffffff", // 背景颜色 + objectCaching: true, // 是否启用对象缓存 + enableRetinaScaling: true, // 是否启用视网膜缩放 + brushWidth: 5, // 画笔宽度 + brushOpacity: 1, // 画笔透明度 + brushColor: "#000000", // 画笔颜色 + brushType: "pencil", // 画笔类型 + brushTypeList: [ + { name: "pencil", text: "铅笔" }, + // { name: "eraser", text: "橡皮擦" }, + { name: "brush", text: "画笔" }, + { name: "spray", text: "喷枪" }, + { name: "rectangle", text: "矩形" }, + { name: "circle", text: "圆形" }, + ], // 画笔类型列表 + layerWidth: 250, // 图层宽度 + + // History settings + maxHistorySteps: 50, // 最大历史记录步数 + onlyActiveLayerEditable: false, // 是否只有当前活动图层可编辑 + // 默认工具模式 + defaultTool: OperationType.DRAW || OperationType.SELECT, // 默认工具 TODO: 默认的地方可以陆续改成这个 + isCropBackground: true, // 是否要裁剪背景层以外的内容 +}; + +export default canvasConfig; diff --git a/src/component/Canvas/CanvasEditor/index.vue b/src/component/Canvas/CanvasEditor/index.vue new file mode 100644 index 00000000..4fb184b6 --- /dev/null +++ b/src/component/Canvas/CanvasEditor/index.vue @@ -0,0 +1,1021 @@ + + + + + diff --git a/src/component/Canvas/CanvasEditor/managers/CanvasManager.js b/src/component/Canvas/CanvasEditor/managers/CanvasManager.js new file mode 100644 index 00000000..3e8237a7 --- /dev/null +++ b/src/component/Canvas/CanvasEditor/managers/CanvasManager.js @@ -0,0 +1,1186 @@ +//import { fabric } from "fabric-with-all"; +import initAligningGuidelines, { + initCenteringGuidelines, +} from "../utils/helperLine"; +import { ThumbnailManager } from "./ThumbnailManager"; +import { ExportManager } from "./ExportManager"; +import { + isGroupLayer, + OperationType, + OperationTypes, +} from "../utils/layerHelper"; +import { AnimationManager } from "./animation/AnimationManager"; +import { createCanvas } from "../utils/canvasFactory"; +import { CanvasEventManager } from "./events/CanvasEventManager"; +import CanvasConfig from "../config/canvasConfig"; +import { RedGreenModeManager } from "./RedGreenModeManager"; +import { + ChangeFixedImageCommand, + AddImageToLayerCommand, +} from "../commands/ObjectLayerCommands"; + +export class CanvasManager { + constructor(canvasElement, options) { + this.canvasElement = canvasElement; + this.width = options.width || 1024; + this.height = options.height || 768; + this.backgroundColor = options.backgroundColor || "#ffffff"; + this.toolManager = options.toolManager || null; // 工具管理器引用 + this.currentZoom = options.currentZoom || { value: 100 }; + this.maskLayer = null; // 添加蒙层引用 + this.editorMode = CanvasConfig.defaultTool; // 默认编辑器模式 + this.layers = options.layers || null; // 图层引用 + this.canvasWidth = options.canvasWidth || this.width; // 画布宽度 + this.canvasHeight = options.canvasHeight || this.height; // 画布高度 + this.canvasColor = options.canvasColor || "#ffffff"; // 画布背景颜色 + this.enabledRedGreenMode = options.enabledRedGreenMode || false; // 是否启用红绿图模式 + // 初始化画布 + this.initializeCanvas(); + } + + initializeCanvas() { + console.log("fabric.version:", fabric.version); + this.canvas = createCanvas(this.canvasElement, { + width: this.width, + height: this.height, + preserveObjectStacking: true, + enableRetinaScaling: true, + stopContextMenu: true, + fireRightClick: true, + }); + + // 初始化动画管理器 + this.animationManager = new AnimationManager(this.canvas, { + currentZoom: this.currentZoom, + wheelThrottleTime: 15, // 降低滚轮事件节流时间,提高响应性 + defaultEase: "power2.lin", + defaultDuration: 0.3, // 缩短默认动画时间 + }); + + // 初始化缩略图管理器 + this.thumbnailManager = new ThumbnailManager(this.canvas, { + // 可以根据需求自定义选项 + layerThumbSize: { width: 32, height: 32 }, + elementThumbSize: { width: 32, height: 24 }, + layers: this.layers, + }); + + // 初始化红绿图模式管理器 + this.redGreenModeManager = new RedGreenModeManager({ + canvas: this.canvas, + layerManager: null, // 稍后设置 + toolManager: null, // 稍后设置 + commandManager: null, // 稍后设置 + }); + + // 设置画布辅助线 + initAligningGuidelines(this.canvas); + + // 设置画布中心线 + initCenteringGuidelines(this.canvas); + + // 初始化画布事件监听器 + this._initCanvasEvents(); + } + + /** + * 初始化画布事件监听器 + * 设置鼠标事件、键盘事件等 + * @private + */ + _initCanvasEvents() { + // 添加笔刷图像转换处理回调 + this.canvas.onBrushImageConverted = (fabricImage) => { + // 如果图层管理器存在,将图像合并到当前活动图层 + if (this.layerManager) { + // 获取当前活动图层 + const activeLayer = this.layerManager.getActiveLayer(); + + if (activeLayer) { + // 确保新图像具有正确的图层信息 + fabricImage.set({ + layerId: activeLayer.id, + layerName: activeLayer.name, + id: + fabricImage.id || + `brush_img_${Date.now()}_${Math.floor(Math.random() * 1000)}`, + }); + + // 执行高保真合并操作 + this.eventManager?.mergeLayerObjectsForPerformance?.({ + fabricImage, + activeLayer, + }); + + // 返回false表示不要自动添加到画布,因为我们已经通过图层管理器处理了 + return false; + } else { + console.warn("没有活动图层,使用默认行为添加图像"); + } + } + + // 返回true表示使用默认行为(直接添加到画布) + return true; + }; + } + + /** + * 设置编辑器模式 + * @param {string} mode 'draw'、'select'或'pan' + */ + toolChanged(mode) { + if (!OperationTypes.includes(mode)) { + console.warn(`不支持的编辑器模式: ${mode}`); + return; + } + + this.editorMode = mode; + + // 如果已创建事件管理器,更新它的编辑器模式 + if (this.eventManager) { + this.eventManager.setEditorMode(mode); + } + } + + setToolManager(toolManager) { + this.toolManager = toolManager || null; // 工具管理器引用 + + // 更新红绿图模式管理器的工具管理器引用 + if (this.redGreenModeManager) { + this.redGreenModeManager.toolManager = this.toolManager; + } + + // 如果已创建事件管理器,更新它的工具管理器引用 + if (this.eventManager) { + this.eventManager.toolManager = this.toolManager; + } + } + + setLayerManager(layerManager) { + this.layerManager = layerManager || null; // 图层管理器引用 + + // 初始化导出管理器(需要在图层管理器设置后初始化) + if (this.layerManager) { + this.exportManager = new ExportManager(this, this.layerManager); + } + + // 更新红绿图模式管理器的图层管理器引用 + if (this.redGreenModeManager) { + this.redGreenModeManager.layerManager = this.layerManager; + } + } + + /** + * 设置命令管理器 + * @param {Object} commandManager 命令管理器实例 + */ + setCommandManager(commandManager) { + this.commandManager = commandManager; + + // 更新红绿图模式管理器的命令管理器引用 + if (this.redGreenModeManager) { + this.redGreenModeManager.commandManager = this.commandManager; + } + } + + /** + * 设置液化管理器 + * @param {Object} liquifyManager 液化管理器实例 + */ + setLiquifyManager(liquifyManager) { + this.liquifyManager = liquifyManager; + } + + /** + * 设置选区管理器 + * @param {Object} selectionManager 选区管理器实例 + */ + setSelectionManager(selectionManager) { + this.selectionManager = selectionManager; + + // 如果已创建事件管理器,更新它的选区管理器引用 + if (this.eventManager) { + this.eventManager.selectionManager = this.selectionManager; + } + } + + // 设置红绿图模式管理器 + setRedGreenModeManager(redGreenModeManager) { + this.redGreenModeManager = redGreenModeManager; + } + + setupCanvasEvents(activeElementId, layerManager) { + // 创建画布事件管理器 + this.eventManager = new CanvasEventManager(this.canvas, { + toolManager: this.toolManager, + animationManager: this.animationManager, + thumbnailManager: this.thumbnailManager, + editorMode: this.editorMode, + activeElementId: activeElementId, + layerManager: layerManager, + layers: this.layers, + }); + + // 设置动画交互效果 + this.animationManager.setupInteractionAnimations(); + } + + setupLongPress(callback) { + if (this.eventManager) { + this.eventManager.setupLongPress(callback); + } + } + + updateSelectedElements(opt, activeElementId) { + if (this.eventManager) { + this.eventManager.updateSelectedElements(opt); + } else { + const selected = opt.selected[0]; + if (selected) { + activeElementId.value = selected.id; + } + } + } + + clearSelectedElements(activeElementId) { + if (this.eventManager) { + this.eventManager.clearSelectedElements(); + } else if (activeElementId) { + activeElementId.value = null; + } + } + + // 使用动画管理器的缩放方法 + animateZoom(point, targetZoom, options = {}) { + this.animationManager.animateZoom(point, targetZoom, options); + } + + // 应用缩放(为兼容性保留) + _applyZoom(point, zoom, skipUpdate = false) { + this.animationManager._applyZoom(point, zoom, skipUpdate); + } + + // 使用动画管理器的平移方法 + animatePan(targetPosition, options = {}) { + this.animationManager.animatePan(targetPosition, options); + } + + // 应用平移(为兼容性保留) + _applyPan(x, y) { + this.animationManager._applyPan(x, y); + } + + // 平移到指定元素 + panToElement(elementId) { + this.animationManager.panToElement(elementId); + } + + // 重置缩放并居中内容 + async resetZoom(animated = true) { + // 先重置缩放 + await this.animationManager.resetZoom(animated); + + // // 重置视图变换以确保元素位置正确 + // this._resetViewportTransform(); + + // // 居中所有画布元素,包括背景层和其他元素 + // this.centerAllObjects(); + + // 重新渲染画布使变更生效 + this.canvas.renderAll(); + } + + setCanvasSize(width, height) { + this.width = width; + this.height = height; + this.canvas.setWidth(width); + this.canvas.setHeight(height); + + // 重置视图变换以确保元素位置正确 + this._resetViewportTransform(); + + // 居中所有画布元素,包括背景层和其他元素 + this.centerAllObjects(); + + // 重新渲染画布使变更生效 + this.canvas.renderAll(); + } + + /** + * 重置视图变换,使元素回到原始位置 + * @private + */ + _resetViewportTransform() { + // 保存当前缩放值 + const currentZoom = this.canvas.getZoom(); + + // 重置视图变换,但保留缩放级别 + this.canvas.setViewportTransform([currentZoom, 0, 0, currentZoom, 0, 0]); + } + + /** + * 居中所有画布元素 + * 计算所有对象的边界框,然后将它们整体居中显示 + */ + centerAllObjects() { + if (!this.canvas) return; + + // 获取所有可见对象(不是背景元素的对象) + const allObjects = this.canvas.getObjects(); + if (allObjects.length === 0) return; + + const visibleObjects = allObjects.filter( + (obj) => obj.visible !== false && !obj.excludeFromExport + ); + + // 如果只有背景层或没有可见对象,只居中背景层 + if ( + visibleObjects.length === 0 || + (visibleObjects.length === 1 && visibleObjects[0].isBackground) + ) { + // 尝试居中背景层 + this.centerBackgroundLayer(this.width, this.height); + return; + } + + // 单独处理背景层 + const backgroundObject = visibleObjects.find((obj) => obj.isBackground); + const contentObjects = backgroundObject + ? visibleObjects.filter((obj) => obj !== backgroundObject) + : visibleObjects; + + // 如果只有背景层,居中背景层 + if (contentObjects.length === 0 && backgroundObject) { + this.centerBackgroundLayer(this.width, this.height); + return; + } + + // 计算内容对象的边界 + const bounds = this._calculateObjectsBounds(contentObjects); + + // 计算所有对象的中心点 + const objectsCenterX = bounds.left + bounds.width / 2; + const objectsCenterY = bounds.top + bounds.height / 2; + + // 计算画布中心点 + const canvasCenterX = this.width / 2; + const canvasCenterY = this.height / 2; + + // 计算需要移动的距离 + const deltaX = canvasCenterX - objectsCenterX; + const deltaY = canvasCenterY - objectsCenterY; + + // 移动所有对象,包括背景层 + visibleObjects.forEach((obj) => { + obj.set({ + left: obj.left + deltaX, + top: obj.top + deltaY, + }); + obj.setCoords(); // 更新对象的控制点坐标 + }); + + // 如果有背景层,更新蒙层位置 + if (backgroundObject && CanvasConfig.isCropBackground) { + this.updateMaskPosition(backgroundObject); + } + + // 重新渲染画布 + this.canvas.renderAll(); + } + + /** + * 计算多个对象的总边界框 + * @private + * @param {Array} objects 要计算边界的对象数组 + * @return {Object} 边界信息,包含left、top、width、height + */ + _calculateObjectsBounds(objects) { + if (!objects || objects.length === 0) return null; + + let minX = Infinity; + let minY = Infinity; + let maxX = -Infinity; + let maxY = -Infinity; + + objects.forEach((obj) => { + const bound = obj.getBoundingRect(); + minX = Math.min(minX, bound.left); + minY = Math.min(minY, bound.top); + maxX = Math.max(maxX, bound.left + bound.width); + maxY = Math.max(maxY, bound.top + bound.height); + }); + + return { + left: minX, + top: minY, + width: maxX - minX, + height: maxY - minY, + }; + } + + setCanvasColor(color) { + this.backgroundColor = color; + this.canvas.setBackgroundColor( + color, + this.canvas.renderAll.bind(this.canvas) + ); + } + + /** + * 居中背景层 + * @param {Object} backgroundLayerObject 背景层对象 + * @param {Number} canvasWidth 画布宽度 + * @param {Number} canvasHeight 画布高度 + */ + centerBackgroundLayer(canvasWidth, canvasHeight) { + const backgroundLayerObject = this.getBackgroundLayer(); + if (!backgroundLayerObject) return false; + + // const bgWidth = backgroundLayerObject.width * backgroundLayerObject.scaleX; + // const bgHeight = + // backgroundLayerObject.height * backgroundLayerObject.scaleY; + + // 计算居中位置 + const left = canvasWidth / 2; + const top = canvasHeight / 2; + + backgroundLayerObject.set({ + left: left, + top: top, + originX: "center", + originY: "center", + }); + + !CanvasConfig.isCropBackground && this.canvas.renderAll(); // 如果不需要裁剪背景层以外的内容,则渲染画布 + + // 如果需要裁剪背景层以外的内容,则更新蒙层位置 + // 创建或更新蒙层 + CanvasConfig.isCropBackground && + this.createOrUpdateMask(backgroundLayerObject); + return true; + } + + /** + * 创建或更新蒙层,用于裁剪不可见区域 + * @param {Object} backgroundLayerObject 背景层对象 + */ + createOrUpdateMask(backgroundLayerObject) { + if (!backgroundLayerObject) return; + + const bgWidth = backgroundLayerObject.width * backgroundLayerObject.scaleX; + const bgHeight = + backgroundLayerObject.height * backgroundLayerObject.scaleY; + const left = backgroundLayerObject.left; + const top = backgroundLayerObject.top; + + // 如果已经存在蒙层,则更新它 + if (this.maskLayer) { + this.canvas.remove(this.maskLayer); + } + + // 创建蒙层 - 使用透明矩形作为裁剪区域 + this.maskLayer = new fabric.Rect({ + width: bgWidth, + height: bgHeight, + left: left, + top: top, + fill: "transparent", + stroke: "#cccccc", + strokeWidth: 1, + strokeDashArray: [5, 5], + selectable: false, + evented: false, + hoverCursor: "default", + originX: "center", + originY: "center", + }); + + // 将蒙层添加到画布 + this.canvas.add(this.maskLayer); + + // 设置蒙层为最顶层 + this.maskLayer.bringToFront(); + + this.canvas.clipPath = new fabric.Rect({ + width: bgWidth, + height: bgHeight, + left: 0, + top: 0, + originX: backgroundLayerObject.originX || "left", + originY: backgroundLayerObject.originY || "top", + absolutePositioned: true, + }); + + // 设置蒙层位置 + this.canvas.clipPath.set({ + left: left, + top: top, + }); + + this.canvas.renderAll(); + } + getBackgroundLayer() { + if (!this.canvas) return null; + + const backgroundLayer = this.canvas.getObjects().find((obj) => { + return obj.isBackground; + }); + + if (backgroundLayer) return backgroundLayer; + + // 如果没有找到背景层,则根据图层ID查找 + const backgroundLayerId = this.layers.value.find((layer) => { + return layer.isBackground; + })?.id; + + const backgroundLayerByBgLayer = this.canvas.getObjects().find((obj) => { + return obj.isBackground || obj.id === backgroundLayerId; + }); + if (!backgroundLayerByBgLayer) { + console.warn( + "CanvasManager.js = >getBackgroundLayer 方法没有找到背景层" + ); + } + + return backgroundLayerByBgLayer; + } + /** + * 更新蒙层位置 + * @param {Object} backgroundLayerObject 背景层对象 + */ + updateMaskPosition(backgroundLayerObject) { + if (!backgroundLayerObject || !this.maskLayer || !this.canvas.clipPath) + return; + + const left = backgroundLayerObject.left; + const top = backgroundLayerObject.top; + + this.maskLayer.set({ + left: left, + top: top, + width: backgroundLayerObject.width * backgroundLayerObject.scaleX, + height: backgroundLayerObject.height * backgroundLayerObject.scaleY, + originX: backgroundLayerObject.originX || "left", + originY: backgroundLayerObject.originY || "top", + }); + + this.canvas.clipPath.set({ + left: left, + top: top, + width: backgroundLayerObject.width * backgroundLayerObject.scaleX, + height: backgroundLayerObject.height * backgroundLayerObject.scaleY, + }); + + this.canvas.renderAll(); + } + + /** + * 更新指定图层的缩略图 + * @param {String} layerId 图层ID + */ + updateLayerThumbnail(layerId) { + if (!this.thumbnailManager || !layerId || !this.layers) return; + + const layer = this.layers.value.find((l) => l.id === layerId); + if (layer) { + this.thumbnailManager.generateLayerThumbnail(layer); + } + } + + /** + * 更新指定元素图层的缩略图 + * @param {String} elementId 元素ID + * @param {Object} fabricObject fabric对象 + */ + updateElementThumbnail(elementId, fabricObject) { + if (this.eventManager) { + this.eventManager.updateElementThumbnail(elementId, fabricObject); + } else if ( + this.thumbnailManager && + elementId && + fabricObject && + this.layers + ) { + // 查找对应的图层(现在元素就是图层) + const layer = this.layers.value.find( + (l) => + l.id === elementId || + (l.fabricObject && l.fabricObject.id === elementId) + ); + + if (layer) { + // 生成图层缩略图 + this.thumbnailManager.generateLayerThumbnail(layer); + } + + // 同时也维护元素缩略图,以保持向后兼容性 + this.thumbnailManager.generateElementThumbnail( + { id: elementId, type: fabricObject.type }, + fabricObject + ); + } + } + + /** + * 更新所有图层和元素的缩略图 + */ + updateAllThumbnails() { + if (!this.thumbnailManager || !this.layers) return; + + this.thumbnailManager.generateAllLayerThumbnails(this.layers.value); + + // 为所有元素生成缩略图 + this.layers.value.forEach((layer) => { + // 如果是分组图层,处理子图层 + if (isGroupLayer(layer) && layer.children) { + layer.children.forEach((childLayerId) => { + const childLayer = this.layers.value.find( + (l) => l.id === childLayerId + ); + if (childLayer && childLayer.fabricObject) { + this.thumbnailManager.generateElementThumbnail( + { id: childLayer.id, type: childLayer.fabricObject.type }, + childLayer.fabricObject + ); + } + }); + } + // 如果是元素图层,则直接生成缩略图 + else if (layer.isElementLayer && layer.fabricObject) { + this.thumbnailManager.generateElementThumbnail( + { id: layer.id, type: layer.fabricObject.type }, + layer.fabricObject + ); + } + }); + } + + /** + * 导出图片 + * @param {Object} options 导出选项 + * @param {Boolean} options.isContainBg 是否包含背景图层 + * @param {Boolean} options.isContainFixed 是否包含固定图层 + * @param {String} options.layerId 导出具体图层ID + * @param {Array} options.layerIdArray 导出多个图层ID数组 + * @param {String} options.expPicType 导出图片类型 (png/jpg/svg) + * @returns {String} 导出的图片数据URL + */ + exportImage(options = {}) { + if (!this.exportManager) { + console.error("导出管理器未初始化,请确保已设置图层管理器"); + throw new Error("导出管理器未初始化"); + } + + try { + return this.exportManager.exportImage(options); + } catch (error) { + console.error("CanvasManager导出图片失败:", error); + throw error; + } + } + + dispose() { + // 释放导出管理器资源 + if (this.exportManager) { + this.exportManager = null; + } + + // 释放事件管理器资源 + if (this.eventManager) { + this.eventManager.dispose(); + this.eventManager = null; + } + + // 释放动画管理器资源 + if (this.animationManager) { + this.animationManager.dispose(); + this.animationManager = null; + } + + // 释放缩略图管理器资源 + if (this.thumbnailManager) { + this.thumbnailManager.dispose(); + this.thumbnailManager = null; + } + + if (this.canvas) { + this.canvas.dispose(); + } + } + + getJSON() { + // 简化图层数据,在loadJSON时要根据id恢复引用 + let tempLayers = this.layers ? this.layers.value : []; + + // 为所有fabric对象生成ID(如果没有的话) + const canvasObjects = this.canvas.getObjects(); + canvasObjects.forEach((obj) => { + if (!obj.id) { + obj.id = `obj_${Date.now()}_${Math.floor(Math.random() * 10000)}`; + } + }); + + // 创建对象ID映射表,用于快速查找 + const objectIdMap = new Map(); + canvasObjects.forEach((obj) => { + if (obj.id) { + objectIdMap.set(obj, obj.id); + } + }); + + tempLayers = tempLayers.map((layer) => { + const newLayer = { ...layer }; + + // 处理fabricObjects数组 + if (Array.isArray(layer.fabricObjects)) { + newLayer.fabricObjects = layer.fabricObjects + .map((item) => { + if (!item) return null; + + // 确保对象有ID + if (!item.id) { + item.id = `obj_${Date.now()}_${Math.floor( + Math.random() * 10000 + )}`; + } + + return { + id: item.id, + type: item.type || "object", // 保存类型信息用于调试 + }; + }) + .filter((item) => item !== null); + } else { + newLayer.fabricObjects = []; + } + + // 处理单个fabricObject + if (layer.fabricObject) { + if (!layer.fabricObject.id) { + layer.fabricObject.id = `obj_${Date.now()}_${Math.floor( + Math.random() * 10000 + )}`; + } + newLayer.fabricObject = { + id: layer.fabricObject.id, + type: layer.fabricObject.type || "object", + }; + } else { + newLayer.fabricObject = null; + } + + // 处理子图层 + if (Array.isArray(layer.children)) { + newLayer.children = layer.children.map((cItem) => { + const newChild = { ...cItem }; + + // 处理子图层的fabricObjects + if (Array.isArray(cItem.fabricObjects)) { + newChild.fabricObjects = cItem.fabricObjects + .map((item) => { + if (!item) return null; + + if (!item.id) { + item.id = `obj_${Date.now()}_${Math.floor( + Math.random() * 10000 + )}`; + } + + return { + id: item.id, + type: item.type || "object", + }; + }) + .filter((item) => item !== null); + } else { + newChild.fabricObjects = []; + } + + // 处理子图层的fabricObject + if (cItem.fabricObject) { + if (!cItem.fabricObject.id) { + cItem.fabricObject.id = `obj_${Date.now()}_${Math.floor( + Math.random() * 10000 + )}`; + } + newChild.fabricObject = { + id: cItem.fabricObject.id, + type: cItem.fabricObject.type || "object", + }; + } else { + newChild.fabricObject = null; + } + + return newChild; + }); + } else { + newLayer.children = []; + } + + return newLayer; + }); + + try { + return JSON.stringify({ + canvas: this.canvas.toJSON([ + "id", + "layerId", + "layerName", + "isBackground", + "isFixed", + "parentId", + "excludeFromExport", + ]), + layers: tempLayers, + version: "1.0", // 添加版本信息 + timestamp: new Date().toISOString(), // 添加时间戳 + canvasWidth: this.canvasWidth.value, + canvasHeight: this.canvasHeight.value, + canvasColor: this.canvasColor.value, + activeLayerId: this.canvas.activeLayerId.value, + }); + } catch (error) { + console.error("获取画布JSON失败:", error); + throw new Error("获取画布JSON失败"); + } + } + + loadJSON(json) { + console.log("加载画布JSON数据:", json); + + // 确保传入的json是字符串格式 + if (typeof json === "object") { + json = JSON.stringify(json); + } else if (typeof json !== "string") { + throw new Error("loadJSON方法需要传入字符串或对象格式的JSON数据"); + } + // 解析JSON字符串 + try { + const parsedJson = JSON.parse(json); + + return new Promise((resolve, reject) => { + const tempLayers = parsedJson?.layers || []; + const canvasData = parsedJson?.canvas; + + if (!tempLayers) { + reject(new Error("JSON数据中缺少layers字段")); + return; + } + + if (!canvasData) { + reject(new Error("JSON数据中缺少canvas字段")); + return; + } + + this.canvasWidth.value = parsedJson.canvasWidth || this.width; + this.canvasHeight.value = parsedJson.canvasHeight || this.height; + this.canvasColor.value = parsedJson.canvasColor || this.backgroundColor; + + console.log("是否检测到红绿图模式内容:", this.enabledRedGreenMode); + + // 重置视图变换以确保元素位置正确 + this._resetViewportTransform(); + + // 清除当前画布内容 + this.canvas.clear(); + + // 加载画布数据 + this.canvas.loadFromJSON(canvasData, () => { + this.backgroundColor = parsedJson.backgroundColor || "#ffffff"; + try { + // 重置画布数据 + this.setCanvasSize(this.canvas.width, this.canvas.height); + + // 创建对象ID映射表,用于快速查找 + const objectIdMap = new Map(); + const canvasObjects = this.canvas.getObjects(); + + canvasObjects.forEach((obj) => { + if (obj.id) { + objectIdMap.set(obj.id, obj); + } + }); + + // 辅助函数:根据ID查找对象 + const findObjectById = (id) => { + if (!id) return null; + return objectIdMap.get(id) || null; + }; + + // 恢复图层数据 + this.layers.value = tempLayers.map((layer) => { + const restoredLayer = { ...layer }; + + // 恢复fabricObjects数组 + if (Array.isArray(layer.fabricObjects)) { + restoredLayer.fabricObjects = layer.fabricObjects + .map((item) => { + if (!item || !item.id) return null; + return findObjectById(item.id); + }) + .filter((obj) => obj !== null); + } else { + restoredLayer.fabricObjects = []; + } + + // 恢复单个fabricObject + if (layer.fabricObject && layer.fabricObject.id) { + restoredLayer.fabricObject = findObjectById( + layer.fabricObject.id + ); + } else { + restoredLayer.fabricObject = null; + } + + // 恢复子图层 + if (Array.isArray(layer.children)) { + restoredLayer.children = layer.children.map((cItem) => { + const restoredChild = { ...cItem }; + + // 恢复子图层的fabricObjects + if (Array.isArray(cItem.fabricObjects)) { + restoredChild.fabricObjects = cItem.fabricObjects + .map((item) => { + if (!item || !item.id) return null; + return findObjectById(item.id); + }) + .filter((obj) => obj !== null); + } else { + restoredChild.fabricObjects = []; + } + + // 恢复子图层的fabricObject + if (cItem.fabricObject && cItem.fabricObject.id) { + restoredChild.fabricObject = findObjectById( + cItem.fabricObject.id + ); + } else { + restoredChild.fabricObject = null; + } + + return restoredChild; + }); + } else { + restoredLayer.children = []; + } + + return restoredLayer; + }); + + this.canvas.activeLayerId.value = + parsedJson?.activeLayerId || this.layers.value[0]?.id || null; + + // 如果检测到红绿图模式内容,进行缩放调整 + if (this.enabledRedGreenMode) { + this._rescaleRedGreenModeContent(); + } + + // 更新所有缩略图 + setTimeout(() => { + this.updateAllThumbnails(); + }, 100); + + console.log("画布JSON数据加载完成"); + resolve(); + } catch (error) { + console.error("恢复图层数据失败:", error); + reject(new Error("恢复图层数据失败: " + error.message)); + } + }); + }); + } catch (error) { + console.error("解析JSON失败:", error); + throw new Error("解析JSON失败,请检查输入格式: " + error.message); + } + } + + /** + * 缩放红绿图模式内容以适应当前画布大小 + * 确保衣服底图和红绿图永远在画布内可见 + * @private + */ + _rescaleRedGreenModeContent() { + if (!this.canvas) return; + + console.log("正在重新缩放红绿图内容..."); + + try { + // 获取固定图层和普通图层 + const fixedLayerObject = this._getFixedLayerObject(); + const normalLayerObjects = this._getNormalLayerObjects(); + + if (!fixedLayerObject) { + console.warn("找不到固定图层对象,无法进行红绿图内容缩放"); + return; + } + + // 计算边距(画布两侧各留出一定空间) + const margin = 50; + const maxWidth = this.canvas.width - margin * 2; + const maxHeight = this.canvas.height - margin * 2; + + // 计算原始尺寸 + const originalWidth = fixedLayerObject.width * fixedLayerObject.scaleX; + const originalHeight = fixedLayerObject.height * fixedLayerObject.scaleY; + + // 计算需要的缩放比例,确保图像完全适应画布 + const scaleX = maxWidth / originalWidth; + const scaleY = maxHeight / originalHeight; + const scale = Math.min(scaleX, scaleY); + + console.log( + `计算的缩放比例: ${scale},原始尺寸: ${originalWidth}x${originalHeight},目标尺寸: ${maxWidth}x${maxHeight}` + ); + + // 如果缩放比例接近1,不进行缩放 + if (Math.abs(scale - 1) < 0.05) { + console.log("缩放比例接近1,不进行缩放,仅居中内容"); + this.centerAllObjects(); + return; + } + + // 缩放固定图层(衣服底图) + this._rescaleObject(fixedLayerObject, scale); + + // 缩放所有普通图层对象(红绿图和其他内容) + normalLayerObjects.forEach((obj) => { + // 红绿图对象应与底图保持完全一致的位置和大小 + if (this._isLikelyRedGreenImage(obj, fixedLayerObject)) { + // 完全匹配底图的位置和大小 + obj.set({ + scaleX: fixedLayerObject.scaleX, + scaleY: fixedLayerObject.scaleY, + left: fixedLayerObject.left, + top: fixedLayerObject.top, + originX: fixedLayerObject.originX || "center", + originY: fixedLayerObject.originY || "center", + }); + } else { + // 其他普通对象进行等比例缩放 + this._rescaleObject(obj, scale); + } + }); + + // 重新居中所有内容 + this.centerAllObjects(); + + // 更新所有对象的坐标系统 + this.canvas.getObjects().forEach((obj) => { + obj.setCoords(); + }); + + // 渲染画布 + this.canvas.renderAll(); + + console.log("红绿图内容缩放完成"); + } catch (error) { + console.error("缩放红绿图内容时出错:", error); + } + } + + /** + * 缩放单个对象 + * @param {Object} obj fabric对象 + * @param {Number} scale 缩放比例 + * @private + */ + _rescaleObject(obj, scale) { + if (!obj) return; + + // 保存原始中心点 + const center = obj.getCenterPoint(); + + // 应用新的缩放 + obj.set({ + scaleX: obj.scaleX * scale, + scaleY: obj.scaleY * scale, + }); + + // 重新定位到原中心点 + obj.setPositionByOrigin(center, "center", "center"); + obj.setCoords(); + } + + /** + * 获取固定图层对象(衣服底图) + * @returns {Object|null} 固定图层对象或null + * @private + */ + _getFixedLayerObject() { + if (!this.layers || !this.layers.value) return null; + + // 查找固定图层 + const fixedLayer = this.layers.value.find((layer) => layer.isFixed); + if (!fixedLayer) return null; + + // 返回图层中的fabric对象 + return fixedLayer.fabricObject || null; + } + + /** + * 获取所有普通图层对象(包括红绿图) + * @returns {Array} 普通图层对象数组 + * @private + */ + _getNormalLayerObjects() { + if (!this.layers || !this.layers.value) return []; + + // 查找所有非背景、非固定的普通图层 + const normalLayers = this.layers.value.filter( + (layer) => !layer.isBackground && !layer.isFixed + ); + + // 收集所有普通图层中的对象 + const objects = []; + normalLayers.forEach((layer) => { + // 如果有单个对象属性 + if (layer.fabricObject) { + objects.push(layer.fabricObject); + } + + // 如果有对象数组 + if (Array.isArray(layer.fabricObjects)) { + layer.fabricObjects.forEach((obj) => { + if (obj) objects.push(obj); + }); + } + }); + + return objects; + } + + /** + * 判断对象是否可能是红绿图 + * 通过比较与衣服底图的大小、位置来判断 + * @param {Object} obj 要检查的对象 + * @param {Object} fixedLayerObject 固定图层对象(衣服底图) + * @returns {Boolean} 是否可能是红绿图 + * @private + */ + _isLikelyRedGreenImage(obj, fixedLayerObject) { + if (!obj || !fixedLayerObject) return false; + + // 检查对象是否为图像 + if (obj.type !== "image") return false; + + // 比较尺寸(允许5%的误差) + const sizeMatch = + Math.abs( + obj.width * obj.scaleX - + fixedLayerObject.width * fixedLayerObject.scaleX + ) < + fixedLayerObject.width * fixedLayerObject.scaleX * 0.05 && + Math.abs( + obj.height * obj.scaleY - + fixedLayerObject.height * fixedLayerObject.scaleY + ) < + fixedLayerObject.height * fixedLayerObject.scaleY * 0.05; + + // 比较位置(允许一定的偏差) + const positionMatch = + Math.abs(obj.left - fixedLayerObject.left) < 50 && + Math.abs(obj.top - fixedLayerObject.top) < 50; + + return sizeMatch && positionMatch; + } +} diff --git a/src/component/Canvas/CanvasEditor/managers/ExportManager.js b/src/component/Canvas/CanvasEditor/managers/ExportManager.js new file mode 100644 index 00000000..15866232 --- /dev/null +++ b/src/component/Canvas/CanvasEditor/managers/ExportManager.js @@ -0,0 +1,355 @@ +/** + * 图片导出管理器 + * 负责处理画布的图片导出功能,支持多种导出选项和图层过滤 + */ +export class ExportManager { + constructor(canvasManager, layerManager) { + this.canvasManager = canvasManager; + this.layerManager = layerManager; + this.canvas = canvasManager.canvas; + } + + /** + * 导出图片 + * @param {Object} options 导出选项 + * @param {Boolean} options.isContainBg 是否包含背景图层 + * @param {Boolean} options.isContainFixed 是否包含固定图层 + * @param {String} options.layerId 导出具体图层ID + * @param {Array} options.layerIdArray 导出多个图层ID数组 + * @param {String} options.expPicType 导出图片类型 (png/jpg/svg) + * @returns {String} 导出的图片数据URL + */ + exportImage(options = {}) { + const { + isContainBg = false, + isContainFixed = false, + layerId = "", + layerIdArray = [], + expPicType = "png" + } = options; + + try { + // 如果指定了具体图层ID,导出指定图层 + if (layerId) { + return this._exportSpecificLayer(layerId, expPicType); + } + + // 如果指定了多个图层ID,导出多个图层 + if (layerIdArray && layerIdArray.length > 0) { + return this._exportMultipleLayers(layerIdArray, expPicType, isContainBg, isContainFixed); + } + + // 默认导出所有可见图层 + return this._exportAllLayers(expPicType, isContainBg, isContainFixed); + + } catch (error) { + console.error("导出图片失败:", error); + throw new Error(`图片导出失败: ${error.message}`); + } + } + + /** + * 导出指定单个图层 + * @param {String} layerId 图层ID + * @param {String} expPicType 导出类型 + * @returns {String} 图片数据URL + * @private + */ + _exportSpecificLayer(layerId, expPicType) { + if (!this.layerManager) { + throw new Error("图层管理器未初始化"); + } + + const layer = this._getLayerById(layerId); + if (!layer) { + throw new Error(`未找到ID为 ${layerId} 的图层`); + } + + if (!layer.visible) { + console.warn(`图层 ${layer.name} 不可见,将导出空白图片`); + } + + // 创建临时画布 + const tempCanvas = this._createExportCanvas(); + const tempFabricCanvas = this._createTempFabricCanvas(tempCanvas); + + try { + // 只添加指定图层的对象 + this._addLayerObjectsToCanvas(tempFabricCanvas, layer); + + // 渲染并导出 + tempFabricCanvas.renderAll(); + return this._generateDataURL(tempCanvas, expPicType); + + } finally { + this._cleanupTempCanvas(tempFabricCanvas); + } + } + + /** + * 导出多个指定图层 + * @param {Array} layerIdArray 图层ID数组 + * @param {String} expPicType 导出类型 + * @param {Boolean} isContainBg 是否包含背景图层 + * @param {Boolean} isContainFixed 是否包含固定图层 + * @returns {String} 图片数据URL + * @private + */ + _exportMultipleLayers(layerIdArray, expPicType, isContainBg, isContainFixed) { + if (!this.layerManager) { + throw new Error("图层管理器未初始化"); + } + + // 创建临时画布 + const tempCanvas = this._createExportCanvas(); + const tempFabricCanvas = this._createTempFabricCanvas(tempCanvas); + + try { + // 按照图层顺序添加指定的图层 + const allLayers = this._getAllLayers(); + + allLayers.forEach(layer => { + if (!layerIdArray.includes(layer.id)) return; + + // 检查图层类型过滤条件 + if (!this._shouldIncludeLayer(layer, isContainBg, isContainFixed)) return; + + if (layer.visible) { + this._addLayerObjectsToCanvas(tempFabricCanvas, layer); + } + }); + + // 渲染并导出 + tempFabricCanvas.renderAll(); + return this._generateDataURL(tempCanvas, expPicType); + + } finally { + this._cleanupTempCanvas(tempFabricCanvas); + } + } + + /** + * 导出所有图层 + * @param {String} expPicType 导出类型 + * @param {Boolean} isContainBg 是否包含背景图层 + * @param {Boolean} isContainFixed 是否包含固定图层 + * @returns {String} 图片数据URL + * @private + */ + _exportAllLayers(expPicType, isContainBg, isContainFixed) { + // 创建临时画布 + const tempCanvas = this._createExportCanvas(); + const tempFabricCanvas = this._createTempFabricCanvas(tempCanvas); + + try { + // 获取所有图层并按顺序添加 + const allLayers = this._getAllLayers(); + + allLayers.forEach(layer => { + // 检查图层类型过滤条件 + if (!this._shouldIncludeLayer(layer, isContainBg, isContainFixed)) return; + + if (layer.visible) { + this._addLayerObjectsToCanvas(tempFabricCanvas, layer); + } + }); + + // 渲染并导出 + tempFabricCanvas.renderAll(); + return this._generateDataURL(tempCanvas, expPicType); + + } finally { + this._cleanupTempCanvas(tempFabricCanvas); + } + } + + /** + * 判断是否应该包含该图层 + * @param {Object} layer 图层对象 + * @param {Boolean} isContainBg 是否包含背景图层 + * @param {Boolean} isContainFixed 是否包含固定图层 + * @returns {Boolean} 是否包含 + * @private + */ + _shouldIncludeLayer(layer, isContainBg, isContainFixed) { + // 背景图层处理 + if (layer.type === 'background' || layer.isBackground) { + return isContainBg; + } + + // 固定图层处理 + if (layer.type === 'fixed' || layer.isFixed || layer.locked) { + return isContainFixed; + } + + // 其他图层默认包含 + return true; + } + + /** + * 创建导出用的临时画布 + * @returns {HTMLCanvasElement} 临时画布 + * @private + */ + _createExportCanvas() { + const tempCanvas = document.createElement("canvas"); + tempCanvas.width = this.canvas.width || 800; + tempCanvas.height = this.canvas.height || 600; + return tempCanvas; + } + + /** + * 创建临时Fabric画布 + * @param {HTMLCanvasElement} tempCanvas 临时画布元素 + * @returns {fabric.StaticCanvas} 临时Fabric画布 + * @private + */ + _createTempFabricCanvas(tempCanvas) { + const { fabric } = window; + + const tempFabricCanvas = new fabric.StaticCanvas(tempCanvas, { + width: this.canvas.width || 800, + height: this.canvas.height || 600, + backgroundColor: this.canvas.backgroundColor || 'transparent' + }); + + // 设置高质量渲染选项 + tempFabricCanvas.enableRetinaScaling = true; + tempFabricCanvas.imageSmoothingEnabled = true; + + return tempFabricCanvas; + } + + /** + * 将图层对象添加到临时画布 + * @param {fabric.StaticCanvas} tempCanvas 临时画布 + * @param {Object} layer 图层对象 + * @private + */ + _addLayerObjectsToCanvas(tempCanvas, layer) { + if (!layer) return; + + // 处理背景图层 + if (layer.type === 'background' && layer.fabricObject) { + this._cloneAndAddObject(tempCanvas, layer.fabricObject); + return; + } + + // 处理普通图层的对象 + if (layer.fabricObjects && Array.isArray(layer.fabricObjects)) { + layer.fabricObjects.forEach(obj => { + if (obj && obj.visible !== false) { + this._cloneAndAddObject(tempCanvas, obj); + } + }); + } + + // 处理单个fabricObject的情况 + if (layer.fabricObject && !layer.fabricObjects) { + this._cloneAndAddObject(tempCanvas, layer.fabricObject); + } + + // 处理分组图层的子图层 + if (layer.children && Array.isArray(layer.children)) { + layer.children.forEach(childLayerId => { + const childLayer = this._getLayerById(childLayerId); + if (childLayer && childLayer.visible) { + this._addLayerObjectsToCanvas(tempCanvas, childLayer); + } + }); + } + } + + /** + * 克隆并添加对象到临时画布 + * @param {fabric.StaticCanvas} tempCanvas 临时画布 + * @param {fabric.Object} obj Fabric对象 + * @private + */ + _cloneAndAddObject(tempCanvas, obj) { + if (!obj) return; + + try { + obj.clone((cloned) => { + if (cloned) { + // 确保克隆对象的属性正确 + cloned.set({ + selectable: false, + evented: false, + visible: true + }); + tempCanvas.add(cloned); + } + }, ['id', 'layerId', 'name']); // 保留自定义属性 + } catch (error) { + console.warn("克隆对象失败:", error); + } + } + + /** + * 生成数据URL + * @param {HTMLCanvasElement} canvas 画布元素 + * @param {String} expPicType 导出类型 + * @returns {String} 数据URL + * @private + */ + _generateDataURL(canvas, expPicType) { + const format = expPicType.toLowerCase(); + + switch (format) { + case 'jpg': + case 'jpeg': + return canvas.toDataURL('image/jpeg', 0.9); + case 'svg': + // SVG导出需要特殊处理,这里先返回PNG + console.warn("SVG导出暂未实现,返回PNG格式"); + return canvas.toDataURL('image/png', 1.0); + case 'png': + default: + return canvas.toDataURL('image/png', 1.0); + } + } + + /** + * 清理临时画布资源 + * @param {fabric.StaticCanvas} tempFabricCanvas 临时Fabric画布 + * @private + */ + _cleanupTempCanvas(tempFabricCanvas) { + if (tempFabricCanvas) { + try { + tempFabricCanvas.dispose(); + } catch (error) { + console.warn("清理临时画布失败:", error); + } + } + } + + /** + * 获取所有图层 + * @returns {Array} 图层数组 + * @private + */ + _getAllLayers() { + if (this.layerManager && this.layerManager.layers) { + return this.layerManager.layers.value || []; + } + return []; + } + + /** + * 根据ID获取图层 + * @param {String} layerId 图层ID + * @returns {Object|null} 图层对象 + * @private + */ + _getLayerById(layerId) { + if (this.layerManager && this.layerManager.getLayerById) { + return this.layerManager.getLayerById(layerId); + } + + // 备用方法:直接从图层数组中查找 + const allLayers = this._getAllLayers(); + return allLayers.find(layer => layer.id === layerId) || null; + } +} \ No newline at end of file diff --git a/src/component/Canvas/CanvasEditor/managers/LayerManager.js b/src/component/Canvas/CanvasEditor/managers/LayerManager.js new file mode 100644 index 00000000..55592129 --- /dev/null +++ b/src/component/Canvas/CanvasEditor/managers/LayerManager.js @@ -0,0 +1,2198 @@ +import { + AddLayerCommand, + PasteLayerCommand, + RemoveLayerCommand, + MoveLayerCommand, + ToggleLayerVisibilityCommand, + RenameLayerCommand, + LayerLockCommand, + SetLayerOpacityCommand, + SetLayerBlendModeCommand, + MergeLayersCommand, + GroupLayersCommand, + UngroupLayersCommand, + MergeLayerObjectsCommand, + LayerObjectsToGroupCommand, + ReorderLayersCommand, + ReorderChildLayersCommand, +} from "../commands/LayerCommands"; +import { + SetActiveLayerCommand, + AddObjectToLayerCommand, + RemoveObjectFromLayerCommand, +} from "../commands/ObjectLayerCommands"; +import { + LayerType, + BlendMode, + createLayer, + createBackgroundLayer, + createFixedLayer, + OperationType, + OperationTypes, +} from "../utils/layerHelper"; +import { + CreateBackgroundLayerCommand, + UpdateBackgroundCommand, + BackgroundSizeCommand, + BackgroundSizeWithScaleCommand, +} from "../commands/BackgroundCommands"; + +import CanvasConfig from "../config/canvasConfig"; + +/** + * 图层管理器 - 负责管理画布上的所有图层 + * 包含图层的创建、删除、修改、排序等操作 + * 现在统一使用命令管理器进行状态管理 + */ +export class LayerManager { + /** + * 创建图层管理器 + * @param {Object} options 配置选项 + * @param {Object} options.canvas fabric.js画布实例 + * @param {Object} options.layers 图层数组响应式引用 + * @param {Object} options.activeLayerId 当前激活图层ID响应式引用 + * @param {Object} options.commandManager 命令管理器 + * @param {Object} options.canvasManager 画布管理器 + * @param {String} options.editorMode 编辑器模式 + * @param {Number} options.canvasWidth 画布宽度 + * @param {Number} options.canvasHeight 画布高度 + * @param {String} options.backgroundColor 背景颜色 + */ + constructor(options) { + this.canvas = options.canvas; + this.layers = options.layers; + this.activeLayerId = options.activeLayerId; + this.commandManager = options.commandManager; + this.canvasManager = options.canvasManager || null; + + // 编辑器模式:draw(绘画)、select(选择)、pan(拖拽) + this.editorMode = options.editorMode || CanvasConfig.defaultTool; + + // 画布尺寸 + this.canvasWidth = options.canvasWidth || 800; + this.canvasHeight = options.canvasHeight || 600; + + // 默认背景颜色 + this.backgroundColor = options.backgroundColor || "#ffffff"; + + // 复制粘贴相关 + this.clipboardData = null; + + // 操作状态标志,用于控制状态保存 + this.isExecutingCommand = false; + // 是否正在执行操作 + this.operationInProgress = false; + + // 红绿图模式相关 + this.isRedGreenMode = false; + this.redGreenModeManager = null; + + // 初始化相关命令 + this.initCommandManager(); + } + + /** + * 初始化命令管理器 + * 现在直接使用命令类,不再需要注册命令 + */ + initCommandManager() { + // 命令注册逻辑已移除,现在直接使用命令类实例化和执行 + console.log("CommandManager 已初始化,使用直接命令调用模式"); + } + + /** + * 设置编辑器模式 + * @param {string} mode 'draw'、'select'或'pan' + */ + toolChanged(mode) { + if (!OperationTypes.includes(mode)) { + console.warn(`不支持的编辑器模式: ${mode}`); + return; + } + + this.editorMode = mode; + + // 更新所有对象的交互性 + this.updateLayersObjectsInteractivity(); + + console.log(`已切换到${mode}模式`); + } + + setToolManager(toolManager) { + this.toolManager = toolManager; + } + + /** + * 更新所有画布对象的交互性 + * 根据当前编辑模式和图层状态设置对象的交互属性 + * @private + */ + updateLayersObjectsInteractivity() { + if (!this.canvas) return; + + // 性能优化:使用requestAnimationFrame + requestAnimationFrame(() => { + // 暂停渲染以提高性能 + this.canvas.skipTargetFind = true; + // this.canvas.discardActiveObject(); + + // 暂停实时渲染和对象查找 + const wasRenderOnAddRemove = this.canvas.renderOnAddRemove; + this.canvas.renderOnAddRemove = false; + + // 应用图层交互规则 + this._applyInteractionRules(); + + // 恢复渲染设置 + this.canvas.renderOnAddRemove = wasRenderOnAddRemove; + this.canvas.skipTargetFind = false; + // this.canvas.renderAll(); + this.canvas.renderAll(); // 确保画布重新渲染 - 同步渲染 + }); + } + + // 私有方法:应用交互规则 + _applyInteractionRules() { + console.log("updateLayersObjectsInteractivity ===>", this.editorMode); + const objects = this.canvas.getObjects(); + const editorMode = this.editorMode || CanvasConfig.defaultTool; + const layers = this.layers?.value || []; + + // 创建缓存以避免重复查找 + const layerMap = {}; + layers.forEach((layer) => { + layerMap[layer.id] = layer; + }); + + // 获取当前活动图层ID + const currentActiveLayerId = this.activeLayerId?.value; + + // 批量更新对象 + objects.forEach((obj) => { + if (!obj.layerId) { + // 没有关联图层的对象使用默认设置 + obj.selectable = false; + obj.evented = false; + obj.erasable = false; // 未关联图层的对象不可擦除 + return; + } + + const layer = layerMap[obj.layerId]; + if (!layer) return; + + // 设置可见性 + obj.visible = layer.visible; + + // 判断对象是否在当前活动图层上 + const isInActiveLayer = obj.layerId === currentActiveLayerId; + + // 基于 fabric-with-erasing 库的 erasable 属性设置擦除权限 + // 只有活动图层、可见、非锁定、非背景、非固定图层的对象才可擦除 + obj.erasable = + isInActiveLayer && + layer.visible && + !layer.locked && + !layer.isBackground && + !layer.isFixed; + + // 图层状态决定交互性 + if (layer.isBackground || obj.isBackground || layer.isFixed) { + // 背景层永远不可选择和擦除 + obj.selectable = false; + obj.evented = false; + obj.erasable = false; + } else if (layer.locked) { + // 锁定图层不可交互和擦除 + obj.selectable = false; + obj.evented = false; + obj.erasable = false; + } else { + // 根据编辑模式设置交互性 + switch (editorMode) { + case OperationType.SELECT: + obj.selectable = true; + obj.evented = true; + break; + case OperationType.ERASER: + // 橡皮擦模式:利用 fabric-with-erasing 的内置机制 + // 只需要设置 erasable 属性,库会自动处理擦除逻辑 + obj.selectable = false; + obj.evented = true; // 需要设置为 true 以接收鼠标事件 + // erasable 已在上面根据图层状态设置 + break; + case OperationType.DRAW: + case OperationType.EYEDROPPER: + case OperationType.PAN: + case OperationType.WAVE: + case OperationType.LIQUIFY: + case OperationType.LASSO: + case OperationType.LASSO_RECTANGLE: + case OperationType.AREA_CUSTOM: + case OperationType.AREA_RECTANGLE: + obj.selectable = false; + obj.evented = false; + break; + default: + obj.selectable = false; + obj.evented = false; + } + + // 平移模式下,禁用多选和擦除 + if (editorMode === OperationType.PAN) { + obj.selectable = false; + obj.evented = false; + obj.erasable = false; + } + } + + // 应用图层视觉属性 + if (layer.opacity !== undefined) obj.opacity = layer.opacity; + if (layer.blendMode) obj.globalCompositeOperation = layer.blendMode; + }); + } + /** + * 创建新图层 + * @param {string} name 图层名称 + * @param {string} type 图层类型 + * @param {Object} options 额外选项 + * @returns {string} 新创建的图层ID + */ + createLayer(name = null, type = LayerType.EMPTY, options = {}) { + // 生成唯一ID + const layerId = `layer_${Date.now()}_${Math.floor(Math.random() * 1000)}`; + const layerIndex = this.layers.value.length; + + // 计算插入位置,如果没有指定insertIndex,则根据当前选中图层决定插入位置 + // 添加到图层列表 + let insertIndex = this._getInsertIndexAboveActiveLayer(); + if (options.insertIndex !== undefined) { + insertIndex = options.insertIndex; + } + + // 创建新图层 + const newLayer = createLayer({ + id: layerId, + name: name || `图层 ${layerIndex + 1}`, + type: type, + visible: true, + locked: false, + opacity: 1.0, + blendMode: BlendMode.NORMAL, + fabricObjects: [], + children: [], + ...options, + }); + + // 直接创建和执行命令 + const command = new AddLayerCommand({ + canvas: this.canvas, + layers: this.layers, + newLayer: newLayer, + activeLayerId: this.activeLayerId, + insertIndex: insertIndex, + }); + + // 计算普通图层数量(非背景、非固定) + const normalLayersCount = this.layers.value.filter( + (layer) => !layer.isBackground && !layer.isFixed + ).length; + + // 如果是第一个图层,或者普通图层数量小于等于3,设置为不可撤销 + if (this.layers.value.length === 1 || normalLayersCount <= 3) { + command.undoable = false; + } + + // 执行命令 + if (this.commandManager) { + this.commandManager.execute(command); + } else { + command.execute(); + } + + return layerId; + } + + /** + * 创建背景图层 + * @param {string} name 图层名称 + * @returns {string} 创建的背景层ID + */ + createBackgroundLayer(name = "背景") { + // 检查是否已有背景图层 + const hasBackgroundLayer = this.layers.value.some( + (layer) => layer.isBackground + ); + + if (hasBackgroundLayer) { + console.warn("已存在背景层,不再创建新的背景层"); + return null; + } + + // 创建背景图层 + const bgLayer = createBackgroundLayer({ + name: name, + canvasWidth: this.canvasWidth, + canvasHeight: this.canvasHeight, + backgroundColor: this.backgroundColor, + }); + + // 直接创建和执行命令 + const command = new CreateBackgroundLayerCommand({ + canvas: this.canvas, + layers: this.layers, + activeLayerId: this.activeLayerId, + canvasManager: this.canvasManager, + backgroundLayer: bgLayer, + }); + + // 背景图层设置为不可撤销 + command.undoable = false; + + // 执行命令 + if (this.commandManager) { + this.commandManager.execute(command); + } else { + command.execute(); + } + + // 返回创建的背景层ID + return bgLayer.id; + } + + /** + * 创建固定图层 - 位于背景图层之上,普通图层之下 + * @param {string} name 图层名称 + * @returns {string} 创建的固定图层ID + */ + createFixedLayer(name = "固定图层") { + // 检查是否已有固定图层 + const hasFixedLayer = this.layers.value.some((layer) => layer.isFixed); + + if (hasFixedLayer) { + console.warn("已存在固定图层,不再创建新的固定图层"); + return null; + } + + // 生成唯一ID + const layerId = `fixed_layer_${Date.now()}_${Math.floor( + Math.random() * 1000 + )}`; + + // 创建固定图层 + const fixedLayer = createFixedLayer({ + id: layerId, + name: name, + }); + + // 直接创建和执行命令 + const command = new AddLayerCommand({ + canvas: this.canvas, + layers: this.layers, + newLayer: fixedLayer, + activeLayerId: this.activeLayerId, + insertIndex: this._getFixedLayerInsertionIndex(), + }); + + // 固定图层设置为不可撤销 + command.undoable = false; + + // 执行命令 + if (this.commandManager) { + this.commandManager.execute(command); + } else { + command.execute(); + } + + return layerId; + } + + /** + * 获取应该插入固定图层的位置索引(在背景图层之上) + * @private + * @returns {Number} 插入索引 + */ + _getFixedLayerInsertionIndex() { + // 找到背景图层的位置 + const bgIndex = this.layers.value.findIndex((layer) => layer.isBackground); + if (bgIndex !== -1) { + return bgIndex; // 插入到背景图层之前(在数组中这意味着位于背景图层之上) + } + return this.layers.value.length; // 如果没有背景图层,则添加到最后 + } + + /** + * 创建普通图层 + * @param {String} name 图层名称 (默认: null,将生成自动名称) + * @param {String} type 图层类型 (默认: LayerType.EMPTY) + * @param {Object} options 附加选项 + * @returns {String} 新创建的图层ID + */ + createLayer(name = null, type = LayerType.EMPTY, options = {}) { + // 生成唯一ID + const layerId = `layer_${Date.now()}_${Math.floor(Math.random() * 1000)}`; + const layerIndex = this.layers.value.length; + + // 计算插入位置,如果没有指定insertIndex,则根据当前选中图层决定插入位置 + // 添加到图层列表 + let insertIndex = this._getInsertIndexAboveActiveLayer(); + if (options.insertIndex !== undefined) { + insertIndex = options.insertIndex; + } + + // 创建新图层 + const newLayer = createLayer({ + id: layerId, + name: name || `图层 ${layerIndex + 1}`, + type: type, + visible: true, + locked: false, + opacity: 1.0, + blendMode: BlendMode.NORMAL, + fabricObjects: [], + children: [], + ...options, + }); + + // 直接创建和执行命令 + const command = new AddLayerCommand({ + canvas: this.canvas, + layers: this.layers, + newLayer: newLayer, + activeLayerId: this.activeLayerId, + insertIndex: insertIndex, + }); + + // 如果是第一个图层,设置为不可撤销 + if (this.layers.value.length === 1) { + command.undoable = false; + } + + // 执行命令 + if (this.commandManager) { + this.commandManager.execute(command); + } else { + command.execute(); + } + + return layerId; + } + + /** + * 创建背景图层 + * @param {string} name 图层名称 + * @returns {string} 创建的背景层ID + */ + createBackgroundLayer(name = "背景") { + // 检查是否已有背景图层 + const hasBackgroundLayer = this.layers.value.some( + (layer) => layer.isBackground + ); + + if (hasBackgroundLayer) { + console.warn("已存在背景层,不再创建新的背景层"); + return null; + } + + // 创建背景图层 + const bgLayer = createBackgroundLayer({ + name: name, + canvasWidth: this.canvasWidth, + canvasHeight: this.canvasHeight, + backgroundColor: this.backgroundColor, + }); + + // 直接创建和执行命令 + const command = new CreateBackgroundLayerCommand({ + canvas: this.canvas, + layers: this.layers, + activeLayerId: this.activeLayerId, + canvasManager: this.canvasManager, + backgroundLayer: bgLayer, + }); + + // 背景图层设置为不可撤销 + command.undoable = false; + + // 执行命令 + if (this.commandManager) { + this.commandManager.execute(command); + } else { + command.execute(); + } + + // 返回创建的背景层ID + return bgLayer.id; + } + + /** + * 初始化图层,确保有背景层、固定图层和一个空白图层 + */ + initializeLayers() { + // 如果没有任何图层,创建背景层、固定图层和一个空白图层 + if (this.layers.value.length === 0) { + // 创建背景图层 + this.createBackgroundLayer(); + + // 创建固定图层,位于背景图层之上 + this.createFixedLayer(); + + // 创建一个空白图层(默认位于背景图层和固定图层之上) + this.createLayer("图层 1"); + } else { + // 检查是否已有背景层 + const hasBackgroundLayer = this.layers.value.some( + (layer) => layer.isBackground + ); + + if (!hasBackgroundLayer) { + this.createBackgroundLayer(); + } + + // 检查是否已有固定图层 + const hasFixedLayer = this.layers.value.some((layer) => layer.isFixed); + + if (!hasFixedLayer) { + this.createFixedLayer(); + } + + // 检查是否至少有一个普通图层(非背景、非固定) + const hasNormalLayer = this.layers.value.some( + (layer) => !layer.isBackground && !layer.isFixed + ); + + if (!hasNormalLayer) { + this.createLayer("图层 1"); + } + } + + // 排序图层 + this.sortLayers(); + + // 更新对象交互性 + this.updateLayersObjectsInteractivity(); + } + + /** + * 添加对象到图层 + * @param {Object} fabricObject fabric对象 + * @param {string} layerId 目标图层ID,如果不提供则使用当前活动图层 + * @returns {Object} 添加的对象 + */ + addObjectToLayer(fabricObject, layerId = null) { + const targetLayerId = layerId || this.activeLayerId.value; + + // 如果没有指定图层ID,也没有活动图层,则返回错误 + if (!targetLayerId) { + console.warn("没有指定目标图层ID且没有活动图层,无法添加对象"); + return null; + } + + // 验证目标图层是否存在 + const targetLayer = this.getLayerById(targetLayerId); + if (!targetLayer) { + console.error(`目标图层 ${targetLayerId} 不存在`); + return null; + } + + // 直接创建和执行命令 + const command = new AddObjectToLayerCommand({ + canvas: this.canvas, + layers: this.layers, + layerId: targetLayerId, + fabricObject: fabricObject, + }); + + // 执行命令 + if (this.commandManager) { + this.commandManager.execute(command); + } else { + command.execute(); + } + + return fabricObject; + } + + /** + * 向固定图层添加对象 + * @param {Object} fabricObject fabric对象 + * @returns {Object|null} 添加的对象或null(如果添加失败) + */ + addObjectToFixedLayer(fabricObject) { + // 查找固定图层 + const fixedLayer = this.layers.value.find((layer) => layer.isFixed); + + // 如果没有固定图层,则创建一个 + if (!fixedLayer) { + const fixedLayerId = this.createFixedLayer(); + return this.addObjectToLayer(fabricObject, fixedLayerId); + } + + // 添加对象到固定图层 + return this.addObjectToLayer(fabricObject, fixedLayer.id); + } + + /** + * 从图层中移除对象 + * @param {string|Object} objectOrId 要移除的对象或其ID + * @returns {boolean} 是否移除成功 + */ + removeObjectFromLayer(objectOrId) { + // 获取对象ID + const objectId = + typeof objectOrId === "string" ? objectOrId : objectOrId.id; + + if (!objectId) { + console.error("无效的对象ID"); + return false; + } + + // 直接创建和执行命令 + const command = new RemoveObjectFromLayerCommand({ + canvas: this.canvas, + layers: this.layers, + objectId: objectId, + objectOrId: objectOrId, + }); + + // 执行命令 + if (this.commandManager) { + return this.commandManager.execute(command); + } else { + return command.execute(); + } + } + + /** + * 设置活动图层 + * @param {string} layerId 图层ID + */ + setActiveLayer(layerId, options = {}) { + // 直接创建和执行命令 + const command = new SetActiveLayerCommand({ + layers: this.layers, + canvas: this.canvas, + layerManager: this, + activeLayerId: this.activeLayerId, + layerId: layerId, + editorMode: this.editorMode, + ...options, + }); + + // 执行命令 + if (this.commandManager) { + this.commandManager.execute(command); + } else { + command.execute(); + } + } + + /** + * 获取当前活动图层 + * @return {Object} layer 图层对象 + */ + getActiveLayer() { + // 查找当前活动图层 + const activeLayer = this.layers.value.find( + (layer) => layer.id === this.activeLayerId.value + ); + + if (activeLayer) { + return activeLayer; + } else { + console.warn("没有活动图层"); + return null; + } + } + + // 获取当前活动图层ID + getActiveLayerId() { + return this.activeLayerId.value; + } + + /** + * 根据ID获取图层 + * @param {string} layerId 图层ID + * @returns {Object|null} 图层对象或null + */ + getLayerById(layerId) { + if (!layerId || !this.layers.value) return null; + return this.layers.value.find((layer) => layer.id === layerId) || null; + } + + /** + * 获取当前图层对象的列表 + * @param {string} layerId 可选,指定图层ID,默认使用当前活动图层 + * @returns {Array} 图层中的对象列表 + */ + getLayerObjects(layerId = null) { + const targetLayerId = layerId || this.activeLayerId.value; + if (!targetLayerId) return []; + + const layer = this.getLayerById(targetLayerId); + if (!layer) return []; + + // 如果是背景图层且有单个对象 + if (layer.isBackground && layer.fabricObject) { + return [layer.fabricObject]; + } + + // 普通图层返回对象列表 + return Array.isArray(layer.fabricObjects) ? layer.fabricObjects : []; + } + + /** + * 移除图层 + * @param {string} layerId 图层ID + * @returns {boolean} 是否移除成功 + */ + removeLayer(layerId) { + // 查找要删除的图层 + const layer = this.layers.value.find((layer) => layer.id === layerId); + + // 如果是背景层或固定层,不允许删除 + if (layer && (layer.isBackground || layer.isFixed)) { + console.warn(layer.isBackground ? "背景层不可删除" : "固定层不可删除"); + return false; + } + + // 如果图层有子图层,提示确认 + if (layer && layer.children && layer.children.length > 0) { + console.warn("该图层包含子图层,删除将同时删除所有子图层"); + } + + // 直接创建和执行命令 + const command = new RemoveLayerCommand({ + canvas: this.canvas, + layers: this.layers, + layerId: layerId, + activeLayerId: this.activeLayerId, + }); + + // 执行命令 + if (this.commandManager) { + this.commandManager.execute(command); + } else { + command.execute(); + } + + return true; + } + + /** + * 移动图层位置 + * @param {string} layerId 图层ID + * @param {string} direction 移动方向,'up'或'down' + * @returns {boolean} 是否移动成功 + */ + moveLayer(layerId, direction) { + // 查找要移动的图层 + const layer = this.layers.value.find((layer) => layer.id === layerId); + + // 如果是背景层或固定层,不允许移动 + if (layer && (layer.isBackground || layer.isFixed)) { + console.warn(layer.isBackground ? "背景层不可移动" : "固定层不可移动"); + return false; + } + + // 直接创建和执行命令 + const command = new MoveLayerCommand({ + canvas: this.canvas, + layers: this.layers, + layerId: layerId, + direction: direction, + }); + + // 执行命令 + if (this.commandManager) { + this.commandManager.execute(command); + } else { + command.execute(); + } + + // 更新画布渲染顺序 + this._rearrangeObjects(); + + return true; + } + + /** + * 切换图层可见性 + * @param {string} layerId 图层ID + * @returns {boolean} 更新后的可见性状态 + */ + toggleLayerVisibility(layerId) { + // 直接创建和执行命令 + const command = new ToggleLayerVisibilityCommand({ + canvas: this.canvas, + layers: this.layers, + layerId: layerId, + }); + + // 执行命令 + if (this.commandManager) { + this.commandManager.execute(command); + } else { + command.execute(); + } + + // 更新对象交互性 + this.updateLayersObjectsInteractivity(); + + // 获取当前可见性 + const layer = this.layers.value.find((layer) => layer.id === layerId); + return layer ? layer.visible : false; + } + + /** + * 切换图层锁定状态 + * @param {string} layerId 图层ID + * @returns {boolean} 更新后的锁定状态 + */ + toggleLayerLock(layerId) { + // 直接创建和执行命令 + const command = new LayerLockCommand({ + canvas: this.canvas, + layers: this.layers, + layerId: layerId, + }); + + // 执行命令 + if (this.commandManager) { + this.commandManager.execute(command); + } else { + command.execute(); + } + + // 更新对象交互性 + this.updateLayersObjectsInteractivity(); + + // 获取当前锁定状态 + const layer = this.layers.value.find((layer) => layer.id === layerId); + return layer ? layer.locked : false; + } + + /** + * 设置图层不透明度 + * @param {string} layerId 图层ID + * @param {number} opacity 不透明度值 (0-1) + */ + setLayerOpacity(layerId, opacity) { + // 直接创建和执行命令 + const command = new SetLayerOpacityCommand({ + canvas: this.canvas, + layers: this.layers, + layerId: layerId, + opacity: opacity, + }); + + // 执行命令 + if (this.commandManager) { + this.commandManager.execute(command); + } else { + command.execute(); + } + + // 更新图层对象不透明度 + const layer = this.layers.value.find((layer) => layer.id === layerId); + if (layer && layer.fabricObjects) { + layer.fabricObjects.forEach((obj) => { + obj.opacity = opacity; + }); + this.canvas.renderAll(); + } + } + + /** + * 设置图层混合模式 + * @param {string} layerId 图层ID + * @param {string} blendMode 混合模式 + */ + setLayerBlendMode(layerId, blendMode) { + // 直接创建和执行命令 + const command = new SetLayerBlendModeCommand({ + canvas: this.canvas, + layers: this.layers, + layerId: layerId, + blendMode: blendMode, + }); + + // 执行命令 + if (this.commandManager) { + this.commandManager.execute(command); + } else { + command.execute(); + } + + // 更新图层对象混合模式 + const layer = this.layers.value.find((layer) => layer.id === layerId); + if (layer && layer.fabricObjects) { + layer.fabricObjects.forEach((obj) => { + obj.globalCompositeOperation = blendMode; + }); + this.canvas.renderAll(); + } + } + + /** + * 重命名图层 + * @param {string} layerId 图层ID + * @param {string} newName 新名称 + */ + renameLayer(layerId, newName) { + // 直接创建和执行命令 + const command = new RenameLayerCommand({ + canvas: this.canvas, + layers: this.layers, + layerId: layerId, + newName: newName, + }); + + // 执行命令 + if (this.commandManager) { + this.commandManager.execute(command); + } else { + command.execute(); + } + + // 更新图层对象上的图层名称 + const layer = this.layers.value.find((layer) => layer.id === layerId); + if (layer && layer.fabricObjects) { + layer.fabricObjects.forEach((obj) => { + obj.layerName = newName; + }); + } + } + + /** + * 合并多个图层 + * @param {Array} layerIds 要合并的图层ID数组 + * @param {string} newName 合并后的图层名称,可选 + * @returns {string} 合并后的新图层ID + */ + mergeLayers(layerIds, newName = null) { + // 检查参数 + if (!layerIds || !Array.isArray(layerIds) || layerIds.length < 2) { + console.error("合并图层至少需要两个图层ID"); + return null; + } + + // 直接创建和执行命令 + const command = new MergeLayersCommand({ + canvas: this.canvas, + layers: this.layers, + layerIds: layerIds, + newName: newName, + }); + + // 执行命令 + if (this.commandManager) { + this.commandManager.execute(command); + } else { + command.execute(); + } + + return command.newLayerId; + } + + /** + * 将多个图层组合为一个组 + * @param {Array} layerIds 要组合的图层ID数组 + * @param {string} groupName 组名称,可选 + * @returns {string} 新组图层ID + */ + groupLayers(layerIds, groupName = null) { + // 检查参数 + if (!layerIds || !Array.isArray(layerIds) || layerIds.length < 2) { + console.error("组合图层至少需要两个图层ID"); + return null; + } + + // 直接创建和执行命令 + const command = new GroupLayersCommand({ + canvas: this.canvas, + layers: this.layers, + layerIds: layerIds, + groupName: groupName, + }); + + // 执行命令 + if (this.commandManager) { + this.commandManager.execute(command); + } else { + command.execute(); + } + + return command.groupId; + } + + /** + * 解组一个组图层 + * @param {string} groupId 要解组的组图层ID + * @returns {Array} 解组后的图层ID数组 + */ + ungroupLayers(groupId) { + // 查找组图层 + const groupLayer = this.layers.value.find((l) => l.id === groupId); + if ( + !groupLayer || + !groupLayer.children || + groupLayer.children.length === 0 + ) { + console.error(`${groupId} 不是有效的组图层或不包含子图层`); + return null; + } + + // 直接创建和执行命令 + const command = new UngroupLayersCommand({ + canvas: this.canvas, + layers: this.layers, + groupId: groupId, + }); + + // 执行命令 + if (this.commandManager) { + this.commandManager.execute(command); + } else { + command.execute(); + } + + return command.childLayerIds; + } + + /** + * 调整画布和背景图层尺寸 + * @param {number} width 宽度 + * @param {number} height 高度 + */ + resizeCanvas(width, height, options = {}) { + // 直接创建和执行命令 + const command = new BackgroundSizeCommand({ + canvas: this.canvas, + layers: this.layers, + canvasManager: this.canvasManager, + newWidth: width, + newHeight: height, + isRedGreenMode: this.isInRedGreenMode(), // 传递红绿图模式状态 + }); + + command.undoable = !!options.undoable; // 设置为可撤销 + // 执行命令 + if (this.commandManager) { + this.commandManager.execute(command); + } else { + command.execute(); + } + + // 更新存储的尺寸 + this.canvasWidth = width; + this.canvasHeight = height; + } + + /** + * 调整背景大小并等比缩放所有其他元素 + * @param {number} width 宽度 + * @param {number} height 高度 + */ + resizeCanvasWithScale(width, height, options = {}) { + // 检查是否有除背景层外的其他元素 + const hasOtherElements = this.canvas + .getObjects() + .some((obj) => !obj.isBackground); + + if (hasOtherElements) { + // 有其他元素时使用带缩放的命令 + const command = new BackgroundSizeWithScaleCommand({ + canvas: this.canvas, + layers: this.layers, + canvasManager: this.canvasManager, + newWidth: width, + newHeight: height, + isRedGreenMode: this.isInRedGreenMode(), // 传递红绿图模式状态 + }); + + command.undoable = !!options.undoable; // 设置为可撤销 + // 执行命令 + if (this.commandManager) { + this.commandManager.execute(command); + } else { + command.execute(); + } + } else { + // 没有其他元素时使用普通的背景尺寸调整命令 + this.resizeCanvas(width, height, options); + } + + // 更新存储的尺寸 + this.canvasWidth = width; + this.canvasHeight = height; + } + + /** + * 排序图层,确保图层顺序: 普通图层 > 固定图层 > 背景图层 + */ + sortLayers() { + // 对图层进行排序:背景图层在最底层(数组最后),固定图层在中间 + this.layers.value.sort((a, b) => { + // 如果a是背景图层,它应该排在后面(最底层) + if (a.isBackground) return 1; + // 如果b是背景图层,它应该排在后面(最底层) + if (b.isBackground) return -1; + + // 如果a是固定图层而b不是固定图层,a应该排在后面(固定图层在普通图层下方) + if (a.isFixed && !b.isFixed) return 1; + // 如果b是固定图层而a不是固定图层,b应该排在后面(固定图层在普通图层下方) + if (b.isFixed && !a.isFixed) return -1; + + // 其他情况保持原有顺序 + return 0; + }); + + // 更新画布对象顺序 + this._rearrangeObjects(); + } + + /** + * 重新排列画布上的对象以匹配图层顺序 + * @private + */ + _rearrangeObjects() { + if (!this.canvas) return; + + // 获取画布上的所有对象 + const canvasObjects = [...this.canvas.getObjects()]; + + // 清空画布 + this.canvas.clear(); + + // 按图层顺序(从底到顶)重新添加对象 + // 注意:图层数组是从顶到底的顺序,需要反向遍历 + for (let i = this.layers.value.length - 1; i >= 0; i--) { + const layer = this.layers.value[i]; + + // 跳过不可见图层 + if (!layer.visible) continue; + + if (layer.isBackground) { + // 背景图层特殊处理 + if (layer.fabricObject) { + const bgObject = canvasObjects.find( + (obj) => obj.id === layer.fabricObject.id + ); + if (bgObject) { + this.canvas.add(bgObject); + } else if (layer.fabricObject) { + this.canvas.add(layer.fabricObject); + } + } + } else if ( + Array.isArray(layer.fabricObjects) && + layer.fabricObjects.length > 0 + ) { + // 普通图层,添加所有fabricObjects + layer.fabricObjects.forEach((obj) => { + const originalObj = canvasObjects.find((o) => o.id === obj.id); + if (originalObj) { + this.canvas.add(originalObj); + } else { + this.canvas.add(obj); + } + }); + } + } + + // 更新对象交互性 + this.updateLayersObjectsInteractivity(); + + // 渲染画布 + this.canvas.renderAll(); + } + + /** + * 同步画布对象到图层数据 + * 确保画布上的对象和图层数据一致 + */ + syncCanvasToLayers() { + if (!this.canvas) return; + + // 获取画布上的所有对象 + const canvasObjects = this.canvas.getObjects(); + + // 遍历所有图层 + this.layers.value.forEach((layer) => { + if (layer.isBackground) { + // 背景图层处理 + if (layer.fabricObject) { + const existsOnCanvas = canvasObjects.some( + (obj) => obj.id === layer.fabricObject.id + ); + if (!existsOnCanvas) { + this.canvas.add(layer.fabricObject); + } + } + } else if (Array.isArray(layer.fabricObjects)) { + // 更新图层中的对象列表 + const updatedObjects = []; + + // 处理已有对象 + layer.fabricObjects.forEach((obj) => { + const canvasObj = canvasObjects.find((cObj) => cObj.id === obj.id); + if (canvasObj) { + updatedObjects.push(canvasObj); + } else { + // 对象不在画布上,添加到画布 + this.canvas.add(obj); + updatedObjects.push(obj); + } + }); + + // 检查是否有新对象需要添加到图层 + canvasObjects.forEach((canvasObj) => { + if ( + canvasObj.layerId === layer.id && + !updatedObjects.some((obj) => obj.id === canvasObj.id) + ) { + updatedObjects.push(canvasObj); + } + }); + + // 更新图层的对象列表 + layer.fabricObjects = updatedObjects; + } + }); + + // 重新排列对象以匹配图层顺序 + this._rearrangeObjects(); + } + + /** + * 导出当前图层数据 + * @returns {Object} 图层数据对象 + */ + exportLayersData() { + // 深拷贝图层数据,避免修改原始数据 + const layersData = JSON.parse(JSON.stringify(this.layers.value)); + + // 移除无法序列化的属性 + layersData.forEach((layer) => { + if (layer.fabricObjects) { + // 序列化对象 + layer.serializedObjects = layer.fabricObjects + .map((obj) => { + if (typeof obj.toObject === "function") { + return obj.toObject(["id", "layerId", "layerName"]); + } + return null; + }) + .filter(Boolean); + + // 删除原始对象引用 + delete layer.fabricObjects; + } + + if (layer.fabricObject) { + layer.serializedBackgroundObject = + typeof layer.fabricObject.toObject === "function" + ? layer.fabricObject.toObject(["id", "layerId", "layerName"]) + : null; + + delete layer.fabricObject; + } + }); + + return { + layers: layersData, + activeLayerId: this.activeLayerId.value, + canvasWidth: this.canvasWidth, + canvasHeight: this.canvasHeight, + backgroundColor: this.backgroundColor, + editorMode: this.editorMode, + }; + } + + /** + * 导入图层数据 + * @param {Object} data 图层数据对象 + * @returns {boolean} 是否导入成功 + */ + importLayersData(data) { + if (!data || !data.layers || !Array.isArray(data.layers)) { + console.error("无效的图层数据"); + return false; + } + + const fabric = window.fabric; + if (!fabric) { + console.error("未找到fabric库"); + return false; + } + + // 清除画布 + this.canvas.clear(); + + // 清空图层列表 + this.layers.value = []; + + // 设置画布尺寸 + if (data.canvasWidth && data.canvasHeight) { + this.canvasWidth = data.canvasWidth; + this.canvasHeight = data.canvasHeight; + this.canvas.setWidth(data.canvasWidth); + this.canvas.setHeight(data.canvasHeight); + } + + // 设置背景颜色 + if (data.backgroundColor) { + this.backgroundColor = data.backgroundColor; + } + + // 设置编辑模式 + if (data.editorMode) { + this.editorMode = data.editorMode; + } + + // 导入图层 + const promises = data.layers.map((layerData) => { + return new Promise((resolve) => { + const newLayer = { + ...layerData, + fabricObjects: [], + children: layerData.children || [], + }; + + // 如果有序列化的对象,恢复它们 + if ( + layerData.serializedObjects && + Array.isArray(layerData.serializedObjects) + ) { + fabric.util.enlivenObjects(layerData.serializedObjects, (objects) => { + objects.forEach((obj) => { + // 关联到图层 + obj.layerId = newLayer.id; + obj.layerName = newLayer.name; + + // 添加到画布 + this.canvas.add(obj); + + // 添加到图层 + newLayer.fabricObjects.push(obj); + }); + + resolve(newLayer); + }); + } else if ( + layerData.isBackground && + layerData.serializedBackgroundObject + ) { + // 恢复背景对象 + fabric.util.enlivenObjects( + [layerData.serializedBackgroundObject], + ([bgObject]) => { + if (bgObject) { + bgObject.layerId = newLayer.id; + bgObject.layerName = newLayer.name; + this.canvas.add(bgObject); + newLayer.fabricObject = bgObject; + } + + resolve(newLayer); + } + ); + } else { + resolve(newLayer); + } + }); + }); + + // 等待所有图层加载完成 + Promise.all(promises).then((layers) => { + // 更新图层列表 + this.layers.value = layers; + + // 设置活动图层 + if (data.activeLayerId) { + const activeLayer = layers.find( + (layer) => layer.id === data.activeLayerId + ); + if (activeLayer && !activeLayer.isBackground && !activeLayer.locked) { + this.activeLayerId.value = data.activeLayerId; + } else { + // 查找第一个非背景、非锁定的图层 + const firstNormalLayer = layers.find( + (layer) => !layer.isBackground && !layer.locked + ); + if (firstNormalLayer) { + this.activeLayerId.value = firstNormalLayer.id; + } + } + } + + // 确保至少有一个背景图层和一个普通图层 + this.initializeLayers(); + + // 更新对象交互性 + this.updateLayersObjectsInteractivity(); + + // 重新排列对象 + this._rearrangeObjects(); + }); + + return true; + } + + /** + * 复制图层数据到剪贴板 + * @param {string} layerId 要复制的图层ID + * @returns {Object} 复制的图层数据 + */ + copyLayer(layerId) { + const layer = this.layers.value.find((l) => l.id === layerId); + if (!layer) { + console.error(`图层 ${layerId} 不存在`); + return null; + } + + // 不允许复制背景图层 + if (layer.isBackground) { + console.warn("不能复制背景图层"); + return null; + } + + // 序列化图层对象 + const layerCopy = JSON.parse(JSON.stringify(layer)); + + // 序列化fabricObjects数组 + if (layer.fabricObjects && layer.fabricObjects.length > 0) { + layerCopy.serializedObjects = layer.fabricObjects + .map((obj) => + typeof obj.toObject === "function" + ? obj.toObject(["id", "layerId", "layerName"]) + : null + ) + .filter(Boolean); + } + + // 存储到剪贴板 + this.clipboardData = layerCopy; + + console.log(`已复制图层:${layer.name}`); + + return this.clipboardData; + } + + /** + * 剪切图层数据到剪贴板 + * @param {string} layerId 要剪切的图层ID + * @returns {Object} 剪切的图层数据 + */ + cutLayer(layerId) { + const layer = this.layers.value.find((l) => l.id === layerId); + if (!layer) { + console.error(`图层 ${layerId} 不存在`); + return null; + } + + // 不允许剪切背景图层 + if (layer.isBackground) { + console.warn("不能剪切背景图层"); + return null; + } + + // 检查是否是唯一的普通图层 + const normalLayers = this.layers.value.filter((l) => !l.isBackground); + if (normalLayers.length === 1) { + console.warn("不能剪切唯一的普通图层"); + return null; + } + + // 序列化图层对象 + const layerCopy = JSON.parse(JSON.stringify(layer)); + + // 序列化fabricObjects数组 + if (layer.fabricObjects && layer.fabricObjects.length > 0) { + layerCopy.serializedObjects = layer.fabricObjects + .map((obj) => + typeof obj.toObject === "function" + ? obj.toObject(["id", "layerId", "layerName"]) + : null + ) + .filter(Boolean); + } + + // 存储到剪贴板 + this.clipboardData = layerCopy; + + // 记录是剪切操作,用于粘贴时的处理 + this.clipboardData.isCut = true; + + // 从画布中移除图层中的所有对象 + if (layer.fabricObjects && layer.fabricObjects.length > 0) { + layer.fabricObjects.forEach((obj) => { + this.canvas.remove(obj); + }); + } + + // 如果剪切的是当前活动图层,需要切换到其他图层 + if (this.activeLayerId.value === layerId) { + // 查找下一个可用的图层 + const nextLayer = this._findNextAvailableLayer(layerId); + if (nextLayer) { + this.setActiveLayer(nextLayer.id); + } + } + + // 直接创建和执行命令 + const command = new RemoveLayerCommand({ + canvas: this.canvas, + layers: this.layers, + layerId: layerId, + activeLayerId: this.activeLayerId, + }); + + // 执行命令 + if (this.commandManager) { + this.commandManager.execute(command); + } else { + command.execute(); + } + + // 更新对象交互性 + this.updateLayersObjectsInteractivity(); + + console.log(`已剪切图层:${layer.name}`); + + return this.clipboardData; + } + + /** + * 粘贴图层 + * @returns {string} 新创建的图层ID + */ + /** + * 粘贴图层 + * @returns {string} 新创建的图层ID + */ + pasteLayer() { + if (!this.clipboardData) { + console.error("剪贴板中没有图层数据"); + return null; + } + + // 使用命令管理器执行粘贴命令 + if (this.commandManager) { + return this.commandManager.executeByName("PasteLayer", { + canvas: this.canvas, + layers: this.layers, + activeLayerId: this.activeLayerId, + clipboardData: this.clipboardData, + layerManager: this, // 传递LayerManager实例 + }); + } else { + // 创建粘贴图层命令 + const command = new PasteLayerCommand({ + canvas: this.canvas, + layers: this.layers, + activeLayerId: this.activeLayerId, + clipboardData: this.clipboardData, + layerManager: this, // 传递LayerManager实例 + }); + + // 执行命令 + return command.execute(); + } + } + + /** + * 查找下一个可用的图层(非背景、非锁定) + * @param {string} excludeLayerId 要排除的图层ID + * @returns {Object|null} 下一个可用的图层 + * @private + */ + _findNextAvailableLayer(excludeLayerId) { + // 查找第一个非背景、非锁定的图层,排除指定的图层 + return ( + this.layers.value.find( + (layer) => + layer.id !== excludeLayerId && !layer.isBackground && !layer.locked + ) || null + ); + } + + /** + * 获取活动图层上方的插入索引 + * @returns {number} 插入索引 + * @private + */ + _getInsertIndexAboveActiveLayer() { + if (!this.activeLayerId.value) return 0; + + const activeLayerIndex = this.layers.value.findIndex( + (layer) => layer.id === this.activeLayerId.value + ); + + return activeLayerIndex !== -1 ? activeLayerIndex : 0; + } + + /** + * 保存画布状态 + * 现在统一通过命令管理器的智能状态保存 + */ + saveCanvasState() { + if (this.commandManager) { + // 使用智能状态保存,避免不必要的状态保存 + this.commandManager.saveStateIfNeeded({ + name: "图层状态更新", + stateType: "full", + }); + } + } + + /** + * 执行带状态保存的操作 + * 统一的状态管理入口 + */ + executeWithState(operation, name = "图层操作") { + if (this.commandManager) { + return this.commandManager.executeWithState(operation, { + name, + stateType: "full", + }); + } else { + // 降级处理 + return typeof operation === "function" ? operation() : operation; + } + } + + /** + * 批量执行图层操作 + */ + batchExecute(operations, name = "批量图层操作") { + if (this.commandManager) { + return this.commandManager.batch(operations, name); + } else { + // 降级处理 + const results = []; + for (const operation of operations) { + results.push(typeof operation === "function" ? operation() : operation); + } + return results; + } + } + + /** + * 清理画布,移除所有图层 + */ + clearCanvas() { + // 清空画布 + this.canvas.clear(); + + // 清空图层列表 + this.layers.value = []; + + // 重新初始化基本图层 + this.initializeLayers(); + + console.log("已清空画布"); + } + + /** + * 合并图层内的对象为单一图像 + * @param {string} layerId 图层ID,默认使用当前活动图层 + * @param {fabric.Image} newImage 要合并的新图像(可选) + * @returns {Promise} 合并后的图像ID + */ + async mergeLayerObjects(activeLayer, fabricImage = null) { + if (!activeLayer) { + console.error(`活动图层不存在`); + return null; + } + + // 直接创建和执行命令 + const command = new MergeLayerObjectsCommand({ + canvas: this.canvas, + layers: this.layers, + fabricImage, + activeLayer, + }); + + // 执行命令 + return command; + } + + /** + * 合并图层内对象成组的命令 + * 将新的图像与图层内现有对象合并为一个组对象 + * 注意:此命令与 MergeLayerObjectsCommand 类似,但它创建一个组而不是单个图像对象 + */ + async LayerObjectsToGroup(activeLayer, fabricImage = null) { + // 检查活动图层是否存在 + if (!activeLayer) { + console.error(`活动图层不存在`); + return null; + } + // 直接创建和执行命令 + const command = new LayerObjectsToGroupCommand({ + canvas: this.canvas, + layers: this.layers, + fabricImage, + activeLayer, + }); + + // 执行命令 + return command; + } + + /** + * 更新背景图层颜色 + * @param {string} backgroundColor 背景颜色 + */ + updateBackgroundColor(backgroundColor) { + // 查找背景图层 + const backgroundLayer = this.layers.value.find( + (layer) => layer.isBackground + ); + + if (!backgroundLayer) { + console.warn("没有找到背景图层"); + return; + } + + // 直接创建和执行命令 + const command = new UpdateBackgroundCommand({ + canvas: this.canvas, + layers: this.layers, + canvasManager: this.canvasManager, + backgroundColor: backgroundColor, + }); + + // 执行命令 + if (this.commandManager) { + this.commandManager.execute(command); + } else { + command.execute(); + } + + // 更新存储的背景颜色 + this.backgroundColor = backgroundColor; + } + + /** + * 获取图层缩略图 + * @param {string} layerId 图层ID + * @param {number} width 缩略图宽度 + * @param {number} height 缩略图高度 + * @returns {string} Base64编码的缩略图 + */ + getLayerThumbnail(layerId, width = 100, height = 100) { + const layer = this.getLayerById(layerId); + if (!layer) { + console.error(`图层 ${layerId} 不存在`); + return null; + } + + // 创建临时画布 + const tempCanvas = document.createElement("canvas"); + tempCanvas.width = width; + tempCanvas.height = height; + const ctx = tempCanvas.getContext("2d"); + + // 设置背景色 + ctx.fillStyle = "#ffffff"; + ctx.fillRect(0, 0, width, height); + + try { + if (layer.isBackground && layer.fabricObject) { + // 背景图层 + const bgObj = layer.fabricObject; + if (bgObj.toCanvasElement) { + const element = bgObj.toCanvasElement(); + ctx.drawImage(element, 0, 0, width, height); + } + } else if (layer.fabricObjects && layer.fabricObjects.length > 0) { + // 普通图层 + layer.fabricObjects.forEach((obj) => { + if (obj.toCanvasElement) { + const element = obj.toCanvasElement(); + const scaleX = width / this.canvasWidth; + const scaleY = height / this.canvasHeight; + + ctx.save(); + ctx.scale(scaleX, scaleY); + ctx.drawImage(element, obj.left || 0, obj.top || 0); + ctx.restore(); + } + }); + } + + return tempCanvas.toDataURL(); + } catch (error) { + console.error("生成图层缩略图失败:", error); + return null; + } + } + + /** + * 检查图层是否为空 + * @param {string} layerId 图层ID + * @returns {boolean} 是否为空图层 + */ + isLayerEmpty(layerId) { + const layer = this.getLayerById(layerId); + if (!layer) return true; + + if (layer.isBackground) { + return !layer.fabricObject; + } + + return !layer.fabricObjects || layer.fabricObjects.length === 0; + } + + /** + * 获取图层统计信息 + * @returns {Object} 图层统计信息 + */ + getLayerStats() { + const stats = { + totalLayers: this.layers.value.length, + backgroundLayers: 0, + normalLayers: 0, + lockedLayers: 0, + hiddenLayers: 0, + emptyLayers: 0, + totalObjects: 0, + }; + + this.layers.value.forEach((layer) => { + if (layer.isBackground) { + stats.backgroundLayers++; + if (layer.fabricObject) { + stats.totalObjects++; + } + } else { + stats.normalLayers++; + if (layer.fabricObjects) { + stats.totalObjects += layer.fabricObjects.length; + } + } + + if (layer.locked) stats.lockedLayers++; + if (!layer.visible) stats.hiddenLayers++; + if (this.isLayerEmpty(layer.id)) stats.emptyLayers++; + }); + + return stats; + } + + /** + * 清理空图层 + * @returns {Array} 被清理的图层ID数组 + */ + cleanupEmptyLayers() { + const emptyLayerIds = []; + + // 找出所有空的非背景图层 + this.layers.value.forEach((layer) => { + if (!layer.isBackground && this.isLayerEmpty(layer.id)) { + emptyLayerIds.push(layer.id); + } + }); + + // 删除空图层 + emptyLayerIds.forEach((layerId) => { + this.removeLayer(layerId); + }); + + if (emptyLayerIds.length > 0) { + console.log(`已清理 ${emptyLayerIds.length} 个空图层`); + } + + return emptyLayerIds; + } + + /** + * 销毁图层管理器 + * 清理所有引用和事件监听器 + */ + dispose() { + // 清空画布 + if (this.canvas) { + this.canvas.clear(); + } + + // 清空图层数据 + if (this.layers && this.layers.value) { + this.layers.value = []; + } + + // 清空剪贴板 + this.clipboardData = null; + + // 清除引用 + this.canvas = null; + this.layers = null; + this.activeLayerId = null; + this.commandManager = null; + this.canvasManager = null; + this.toolManager = null; + + console.log("LayerManager 已销毁"); + } + + /** + * 创建文本图层并添加文本对象 + * @param {Object} textObject Fabric文本对象 + * @param {Object} options 文本选项 + * @returns {Object} 创建的文本对象 + */ + createTextLayerWithObject(textObject, options = {}) { + if (!this.canvas || !textObject) return null; + + // 确保对象有ID + textObject.id = + textObject.id || `text_${Date.now()}_${Math.floor(Math.random() * 1000)}`; + + // 创建文本图层 + const layerName = options.name || "文本图层"; + const layerId = this.createLayer(layerName, LayerType.TEXT, { + layerProperties: { + text: options.text || textObject.text || "新文本", + fontFamily: options.fontFamily || textObject.fontFamily || "Arial", + fontSize: options.fontSize || textObject.fontSize || 24, + fontWeight: options.fontWeight || textObject.fontWeight || "normal", + fontStyle: options.fontStyle || textObject.fontStyle || "normal", + textAlign: options.textAlign || textObject.textAlign || "left", + underline: options.underline || textObject.underline || false, + linethrough: options.linethrough || textObject.linethrough || false, + overline: options.overline || textObject.overline || false, + fill: options.fill || textObject.fill || "#000000", + textBackgroundColor: + options.textBackgroundColor || + textObject.textBackgroundColor || + "transparent", + lineHeight: options.lineHeight || textObject.lineHeight || 1.16, + charSpacing: options.charSpacing || textObject.charSpacing || 0, + }, + }); + + // 把对象添加到新图层 + textObject.layerId = layerId; + textObject.layerName = layerName; + + // 添加到画布,如果还未添加 + const isOnCanvas = this.canvas + .getObjects() + .some((obj) => obj.id === textObject.id); + if (!isOnCanvas) { + this.canvas.add(textObject); + } + + // 更新图层中的对象列表 + const layer = this.getLayerById(layerId); + if (layer) { + layer.fabricObjects = layer.fabricObjects || []; + layer.fabricObjects.push(textObject); + } + + // 设置此图层为活动图层 + this.setActiveLayer(layerId); + + // 更新交互性 + this.updateLayersObjectsInteractivity(); + + return textObject; + } + + /** + * 根据fabric对象查找所属图层 + * @param {Object} fabricObject fabric对象 + * @returns {Object|null} 图层对象或null + */ + findLayerByObject(fabricObject) { + if (!fabricObject || !fabricObject.id) { + return null; + } + + // 遍历所有图层查找包含该对象的图层 + for (const layer of this.layers.value) { + // 检查背景图层 + if (layer.isBackground && layer.fabricObject) { + if (layer.fabricObject.id === fabricObject.id) { + return layer; + } + } + + // 检查普通图层 + if (layer.fabricObjects && Array.isArray(layer.fabricObjects)) { + const foundObject = layer.fabricObjects.find( + (obj) => obj.id === fabricObject.id + ); + if (foundObject) { + return layer; + } + } + } + + return null; + } + + /** + * 拖拽排序图层 + * @param {number} oldIndex 原索引 + * @param {number} newIndex 新索引 + * @param {string} layerId 图层ID + * @returns {boolean} 是否排序成功 + */ + reorderLayers(oldIndex, newIndex, layerId) { + // 检查索引有效性 + if ( + oldIndex < 0 || + newIndex < 0 || + oldIndex >= this.layers.value.length || + newIndex >= this.layers.value.length + ) { + console.warn("图层排序索引无效"); + return false; + } + + // 检查是否是同一位置 + if (oldIndex === newIndex) { + return true; + } + + // 获取要移动的图层 + const layer = this.layers.value[oldIndex]; + if (!layer || layer.id !== layerId) { + console.warn("图层ID与索引不匹配"); + return false; + } + + // 检查是否是背景层或固定层(不允许排序) + if (layer.isBackground || layer.isFixed) { + console.warn("背景层和固定层不能参与排序"); + return false; + } + + // 检查目标位置是否合法(不能移到背景层或固定层的位置) + const targetLayer = this.layers.value[newIndex]; + if (targetLayer && (targetLayer.isBackground || targetLayer.isFixed)) { + console.warn("不能移动到背景层或固定层的位置"); + return false; + } + + // 创建并执行拖拽排序命令 + const command = new ReorderLayersCommand({ + layers: this.layers, + oldIndex: oldIndex, + newIndex: newIndex, + layerId: layerId, + canvas: this.canvas, + }); + + // 执行命令 + if (this.commandManager) { + return this.commandManager.execute(command); + } else { + return command.execute(); + } + } + + /** + * 拖拽排序子图层 + * @param {string} parentId 父图层ID + * @param {number} oldIndex 原索引 + * @param {number} newIndex 新索引 + * @param {string} layerId 子图层ID + * @returns {boolean} 是否排序成功 + */ + reorderChildLayers(parentId, oldIndex, newIndex, layerId) { + // 检查父图层是否存在 + const parentLayer = this.getLayerById(parentId); + if (!parentLayer) { + console.warn(`父图层 ${parentId} 不存在`); + return false; + } + + // 获取所有子图层 + const childLayers = this.layers.value.filter( + (layer) => layer.parentId === parentId + ); + + // 检查索引有效性 + if ( + oldIndex < 0 || + newIndex < 0 || + oldIndex >= childLayers.length || + newIndex >= childLayers.length + ) { + console.warn("子图层排序索引无效"); + return false; + } + + // 检查是否是同一位置 + if (oldIndex === newIndex) { + return true; + } + + // 验证图层ID + const layer = childLayers[oldIndex]; + if (!layer || layer.id !== layerId) { + console.warn("子图层ID与索引不匹配"); + return false; + } + + // 创建并执行子图层拖拽排序命令 + const command = new ReorderChildLayersCommand({ + layers: this.layers, + parentId: parentId, + oldIndex: oldIndex, + newIndex: newIndex, + layerId: layerId, + canvas: this.canvas, + }); + + // 执行命令 + if (this.commandManager) { + return this.commandManager.execute(command); + } else { + return command.execute(); + } + } + + // 设置红绿图模式管理器 + setRedGreenModeManager(redGreenModeManager) { + this.redGreenModeManager = redGreenModeManager; + } + + // 启用红绿图模式 + enableRedGreenMode() { + this.isRedGreenMode = true; + console.log("图层管理器:红绿图模式已启用"); + } + + // 禁用红绿图模式 + disableRedGreenMode() { + this.isRedGreenMode = false; + console.log("图层管理器:红绿图模式已禁用"); + } + + // 检查是否为红绿图模式 + isInRedGreenMode() { + return this.isRedGreenMode; + } + + // 检查背景图层是否需要跟随画布大小变化(红绿图模式下) + shouldBackgroundFollowCanvasSize() { + return this.isRedGreenMode; + } + + // 在红绿图模式下创建图层 - 限制功能 + createLayerInRedGreenMode() { + console.warn("红绿图模式下不支持创建新图层"); + return null; + } + + // 在红绿图模式下移除图层 - 限制功能 + removeLayerInRedGreenMode(layerId) { + console.warn("红绿图模式下不支持删除图层"); + return false; + } +} diff --git a/src/component/Canvas/CanvasEditor/managers/RedGreenModeManager.js b/src/component/Canvas/CanvasEditor/managers/RedGreenModeManager.js new file mode 100644 index 00000000..eaa215b8 --- /dev/null +++ b/src/component/Canvas/CanvasEditor/managers/RedGreenModeManager.js @@ -0,0 +1,357 @@ +//import { fabric } from "fabric-with-all"; +import { createLayer, LayerType, OperationType } from "../utils/layerHelper.js"; +import { BatchInitializeRedGreenModeCommand } from "../commands/RedGreenCommands.js"; + +/** + * 红绿图模式管理器 + * 利用已有图层结构,不清除现有图层 + */ +export class RedGreenModeManager { + constructor(options = {}) { + this.canvas = options.canvas; + this.layerManager = options.layerManager; + this.toolManager = options.toolManager; + this.canvasManager = options.canvasManager; + this.commandManager = options.commandManager; + + // 红绿图模式状态 + this.isInitialized = false; + this.normalLayerOpacity = 0.4; // 默认40%透明度 (0-1) + + // 图片URL + this.clothingImageUrl = null; + this.redGreenImageUrl = null; + + // 回调函数 + this.onImageGenerated = null; + + console.log("RedGreenModeManager 已创建"); + } + + /** + * 初始化红绿图模式 + * @param {Object} options 配置选项 + * @param {String} options.clothingImageUrl 衣服底图URL + * @param {String} options.redGreenImageUrl 红绿图URL + * @param {Number} options.normalLayerOpacity 普通图层透明度 (0-1) + * @param {Function} options.onImageGenerated 图片生成回调 + * @param {Boolean} options.useBatchMode 是否使用批量模式 (默认true,减少闪烁) + * @returns {Promise} 是否初始化成功 + */ + async initialize(options = {}) { + try { + // 如果已经初始化,先清理状态 + if (this.isInitialized) { + console.log("红绿图模式已初始化,重新初始化..."); + } + + // 更新配置 + this.clothingImageUrl = options.clothingImageUrl; + this.redGreenImageUrl = options.redGreenImageUrl; + this.onImageGenerated = options.onImageGenerated; + + // 设置透明度 (支持0-100的百分比值或0-1的小数值) + if (typeof options.normalLayerOpacity === "number") { + if (options.normalLayerOpacity > 1) { + // 如果大于1,认为是百分比值(0-100) + this.normalLayerOpacity = + Math.max(0, Math.min(100, options.normalLayerOpacity)) / 100; + } else { + // 如果小于等于1,认为是小数值(0-1) + this.normalLayerOpacity = Math.max( + 0, + Math.min(1, options.normalLayerOpacity) + ); + } + } + + // 验证必需参数 + if (!this.clothingImageUrl || !this.redGreenImageUrl) { + throw new Error("缺少必需的图片URL参数"); + } + + // 使用批量模式或传统模式 + const useBatchMode = options.useBatchMode !== false; // 默认为true + + let initCommand; + + // 使用新的批量初始化命令,减少页面闪烁 + initCommand = new BatchInitializeRedGreenModeCommand({ + canvas: this.canvas, + layerManager: this.layerManager, + toolManager: this.toolManager, + clothingImageUrl: this.clothingImageUrl, + redGreenImageUrl: this.redGreenImageUrl, + normalLayerOpacity: this.normalLayerOpacity, + onImageGenerated: this.onImageGenerated, + }); + initCommand.undoable = false; // 不可撤销 + + // 执行命令 + if (this.commandManager) { + await this.commandManager.execute(initCommand); + } else { + await initCommand.execute(); + } + + this.registerRedGreenMouseUpEvent(); + // 标记为已初始化 + this.isInitialized = true; + + // 启用图层管理器的红绿图模式 + if ( + this.layerManager && + typeof this.layerManager.enableRedGreenMode === "function" + ) { + this.layerManager.enableRedGreenMode(); + } + + // 重置工具管理器状态 + // 默认红色笔刷 + if (this.toolManager) { + this.toolManager.isRedGreenMode = true; + } + + console.log("红绿图模式初始化成功", { + 衣服底图: this.clothingImageUrl, + 红绿图: this.redGreenImageUrl, + 普通图层透明度: `${Math.round(this.normalLayerOpacity * 100)}%`, + 批量模式: useBatchMode ? "已启用" : "已禁用", + 画布背景: "白色", + }); + + return true; + } catch (error) { + console.error("红绿图模式初始化失败:", error); + this.isInitialized = false; + throw error; + } + } + + // 注册鼠标抬起事件 + registerRedGreenMouseUpEvent() { + this.canvas.on("mouse:up", (event) => { + if (!this.isInitialized) { + console.warn("红绿图模式未初始化,无法处理鼠标事件"); + return; + } + + // 可以在这里添加更多逻辑,比如生成图片或更新状态 + if (this.onImageGenerated) { + const imageData = this.canvasManager.exportImage(); + console.log("生成红绿图图片数据:", imageData); + this.onImageGenerated(imageData); + } + }); + } + + /** + * 检查是否已初始化 + * @returns {boolean} 是否已初始化 + */ + isReady() { + return this.isInitialized; + } + + /** + * 更新普通图层透明度 + * @param {Number} opacity 透明度值 (0-100的百分比值或0-1的小数值) + * @returns {boolean} 是否更新成功 + */ + updateNormalLayerOpacity(opacity) { + if (!this.isInitialized) { + console.warn("红绿图模式未初始化,无法更新透明度"); + return false; + } + + try { + // 处理透明度值 + let normalizedOpacity; + if (opacity > 1) { + // 如果大于1,认为是百分比值(0-100) + normalizedOpacity = Math.max(0, Math.min(100, opacity)) / 100; + } else { + // 如果小于等于1,认为是小数值(0-1) + normalizedOpacity = Math.max(0, Math.min(1, opacity)); + } + + // 创建透明度更新命令 + const opacityCommand = new UpdateNormalLayerOpacityCommand({ + canvas: this.canvas, + layerManager: this.layerManager, + opacity: normalizedOpacity, + }); + + // 执行命令 + if (this.commandManager) { + this.commandManager.execute(opacityCommand); + } else { + opacityCommand.execute(); + } + + // 更新内部状态 + this.normalLayerOpacity = normalizedOpacity; + + console.log( + `普通图层透明度已更新为: ${Math.round(normalizedOpacity * 100)}%` + ); + return true; + } catch (error) { + console.error("更新普通图层透明度失败:", error); + return false; + } + } + + /** + * 获取当前普通图层透明度 + * @param {boolean} asPercentage 是否返回百分比值(0-100),默认返回小数值(0-1) + * @returns {Number} 透明度值 + */ + getNormalLayerOpacity(asPercentage = false) { + if (asPercentage) { + return Math.round(this.normalLayerOpacity * 100); + } + return this.normalLayerOpacity; + } + + /** + * 重新加载图片 + * @param {Object} options 配置选项 + * @param {String} options.clothingImageUrl 新的衣服底图URL (可选) + * @param {String} options.redGreenImageUrl 新的红绿图URL (可选) + * @returns {Promise} 是否重新加载成功 + */ + async reloadImages(options = {}) { + if (!this.isInitialized) { + console.warn("红绿图模式未初始化,无法重新加载图片"); + return false; + } + + try { + // 更新图片URL + if (options.clothingImageUrl) { + this.clothingImageUrl = options.clothingImageUrl; + } + if (options.redGreenImageUrl) { + this.redGreenImageUrl = options.redGreenImageUrl; + } + + // 重新初始化 + await this.initialize({ + clothingImageUrl: this.clothingImageUrl, + redGreenImageUrl: this.redGreenImageUrl, + normalLayerOpacity: this.normalLayerOpacity, + onImageGenerated: this.onImageGenerated, + }); + + console.log("图片重新加载成功"); + return true; + } catch (error) { + console.error("重新加载图片失败:", error); + return false; + } + } + + /** + * 获取当前状态信息 + * @returns {Object} 状态信息 + */ + getStatus() { + return { + isInitialized: this.isInitialized, + clothingImageUrl: this.clothingImageUrl, + redGreenImageUrl: this.redGreenImageUrl, + normalLayerOpacity: this.normalLayerOpacity, + normalLayerOpacityPercentage: Math.round(this.normalLayerOpacity * 100), + }; + } + + /** + * 获取图层信息 + * @returns {Object} 图层信息 + */ + getLayerInfo() { + if (!this.layerManager || !this.layerManager.layers) { + return null; + } + + const layers = this.layerManager.layers.value || []; + const backgroundLayer = layers.find((layer) => layer.isBackground); + const fixedLayer = layers.find((layer) => layer.isFixed); + const normalLayers = layers.filter( + (layer) => !layer.isBackground && !layer.isFixed + ); + + return { + backgroundLayer: + backgroundLayer && + Object.assign( + { + hasObject: !!backgroundLayer.fabricObject, + }, + backgroundLayer + ), + fixedLayer: + fixedLayer && + Object.assign( + { + hasObject: !!fixedLayer.fabricObject, + }, + fixedLayer + ), + normalLayers: normalLayers.map((layer) => ({ + id: layer.id, + name: layer.name, + visible: layer.visible, + opacity: layer.opacity, + objectCount: layer.fabricObjects ? layer.fabricObjects.length : 0, + })), + }; + } + + /** + * 清理红绿图模式 + * 注意:这不会删除图层,只是清理红绿图模式的特定内容 + */ + cleanup() { + try { + // 禁用图层管理器的红绿图模式 + if ( + this.layerManager && + typeof this.layerManager.disableRedGreenMode === "function" + ) { + this.layerManager.disableRedGreenMode(); + } + + // 重置工具管理器 + if (this.toolManager && this.toolManager.isRedGreenMode) { + this.toolManager.isRedGreenMode = false; + } + + // 重置状态 + this.isInitialized = false; + this.clothingImageUrl = null; + this.redGreenImageUrl = null; + this.onImageGenerated = null; + + console.log("红绿图模式已清理"); + } catch (error) { + console.error("清理红绿图模式失败:", error); + } + } + + /** + * 销毁管理器 + */ + dispose() { + this.cleanup(); + + // 清除引用 + this.canvas = null; + this.layerManager = null; + this.toolManager = null; + this.commandManager = null; + + console.log("RedGreenModeManager 已销毁"); + } +} diff --git a/src/component/Canvas/CanvasEditor/managers/ThumbnailManager.js b/src/component/Canvas/CanvasEditor/managers/ThumbnailManager.js new file mode 100644 index 00000000..434be013 --- /dev/null +++ b/src/component/Canvas/CanvasEditor/managers/ThumbnailManager.js @@ -0,0 +1,370 @@ +/** + * 缩略图管理器 - 负责生成和缓存图层和元素的预览缩略图 + */ +export class ThumbnailManager { + constructor(canvas, options = {}) { + this.canvas = canvas; + this.layers = options.layers || []; // 图层管理器 + this.layerThumbSize = options.layerThumbSize || { width: 48, height: 48 }; + this.elementThumbSize = options.elementThumbSize || { + width: 36, + height: 36, + }; + this.layerThumbnails = new Map(); // 图层缩略图缓存 + this.elementThumbnails = new Map(); // 元素缩略图缓存 + this.defaultThumbnail = + "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII="; // 1x1 透明图 + } + + /** + * 生成图层缩略图 + * @param {Object} layer 图层对象ID + */ + generateLayerThumbnail(layerId) { + // const layer = this?.layers.value?.find((layer) => layer.id === layerId); + // if (!layer) return; + // // 延迟执行,避免阻塞UI + // requestAnimationFrame(() => { + // this._generateLayerThumbnailNow(layer); + // }); + } + + /** + * 立即生成图层缩略图 + * @param {Object} layer 图层对象 + * @private + */ + _generateLayerThumbnailNow(layer) { + if ( + !layer || + !this.canvas || + !layer.fabricObjects || + !layer.fabricObjects?.length + ) + return; + + try { + let thumbnail = null; + + if (!layer.children?.length) { + // 如果是元素图层,直接生成元素缩略图 + thumbnail = this._generateThumbnailFromObjects( + layer.fabricObjects, + this.layerThumbSize.width, + this.layerThumbSize.height + ); + } else if (layer.type === "group" || layer.children?.length) { + const fabricObjects = layer.children.reduce((pre, next) => { + if (next.fabricObjects.length) { + pre.push(...next.fabricObjects); + } + return pre; + }, []); + // 如果是分组图层,合并所有子对象的缩略图 + thumbnail = this._generateThumbnailFromObjects( + fabricObjects, + this.layerThumbSize.width, + this.layerThumbSize.height + ); + } + + // 保存到缩略图缓存 + if (thumbnail) { + this.layerThumbnails.set(layer.id, thumbnail); + } else { + // 如果无法生成缩略图,使用默认缩略图 + this.layerThumbnails.set(layer.id, this.defaultThumbnail); + } + } catch (error) { + console.error("生成图层缩略图出错:", error); + } + } + + /** + * 生成元素缩略图 + * @param {Object} element 元素对象 + * @param {Object} fabricObject fabric对象 + */ + generateElementThumbnail(element, fabricObject) { + if (!element || !element.id || !fabricObject) return; + + // 延迟执行,避免阻塞UI + requestAnimationFrame(() => { + try { + const thumbnail = this._generateThumbnailFromObject( + fabricObject, + this.elementThumbSize.width, + this.elementThumbSize.height + ); + + if (thumbnail) { + this.elementThumbnails.set(element.id, thumbnail); + } + } catch (error) { + console.error("生成元素缩略图出错:", error); + } + }); + } + + /** + * 从fabric对象生成缩略图 + * @param {Object} obj fabric对象 + * @param {Number} width 缩略图宽度 + * @param {Number} height 缩略图高度 + * @returns {String} 缩略图数据URL + * @private + */ + _generateThumbnailFromObject(obj, width, height) { + if (!obj || !this.canvas) return null; + + // 保存对象状态 + const originalState = { + active: obj.active, + visible: obj.visible, + left: obj.left, + top: obj.top, + scaleX: obj.scaleX, + scaleY: obj.scaleY, + opacity: obj.opacity, + }; + + // 临时修改对象状态 + obj.set({ + active: false, + visible: true, + opacity: 1, + }); + + // 创建临时画布 + const tempCanvas = document.createElement("canvas"); + tempCanvas.width = width; + tempCanvas.height = height; + const tempCtx = tempCanvas.getContext("2d"); + + // 获取对象边界 + const bounds = obj.getBoundingRect(); + + // 绘制缩略图 + try { + // 清空画布 + tempCtx.clearRect(0, 0, width, height); + + // 计算缩放比例 + const scaleFactorX = width / bounds.width; + const scaleFactorY = height / bounds.height; + const scaleFactor = Math.min(scaleFactorX, scaleFactorY) * 0.8; // 保留一些边距 + + // 居中绘制 + const centerX = width / 2; + const centerY = height / 2; + + tempCtx.save(); + tempCtx.translate(centerX, centerY); + tempCtx.scale(scaleFactor, scaleFactor); + tempCtx.translate( + -bounds.left - bounds.width / 2, + -bounds.top - bounds.height / 2 + ); + + // 绘制对象 + obj.render(tempCtx); + + tempCtx.restore(); + + // 转换为数据URL + const dataUrl = tempCanvas.toDataURL("image/png"); + + // 恢复对象状态 + obj.set(originalState); + + return dataUrl; + } catch (error) { + console.error("绘制对象缩略图出错:", error); + + // 恢复对象状态 + obj.set(originalState); + + return null; + } + } + + /** + * 从多个fabric对象生成组合缩略图 + * @param {Array} objects fabric对象数组 + * @param {Number} width 缩略图宽度 + * @param {Number} height 缩略图高度 + * @returns {String} 缩略图数据URL + * @private + */ + _generateThumbnailFromObjects(objects, width, height) { + if (!objects || !objects.length || !this.canvas) return null; + + // 创建临时画布 + const tempCanvas = document.createElement("canvas"); + tempCanvas.width = width; + tempCanvas.height = height; + const tempCtx = tempCanvas.getContext("2d"); + + // 计算所有对象的总边界 + let minX = Infinity, + minY = Infinity, + maxX = -Infinity, + maxY = -Infinity; + + objects.forEach((obj) => { + if (!obj.visible) return; + + const bounds = obj.getBoundingRect(); + minX = Math.min(minX, bounds.left); + minY = Math.min(minY, bounds.top); + maxX = Math.max(maxX, bounds.left + bounds.width); + maxY = Math.max(maxY, bounds.top + bounds.height); + }); + + const groupWidth = maxX - minX; + const groupHeight = maxY - minY; + + // 如果没有有效对象,返回null + if (groupWidth <= 0 || groupHeight <= 0) return null; + + // 保存对象状态 + const originalStates = objects.map((obj) => ({ + obj, + state: { + active: obj.active, + visible: obj.visible, + opacity: obj.opacity, + }, + })); + + // 临时修改对象状态 + originalStates.forEach((item) => { + item.obj.set({ + active: false, + visible: true, + opacity: 1, + }); + }); + + // 绘制缩略图 + try { + // 清空画布 + tempCtx.clearRect(0, 0, width, height); + + // 计算缩放比例 + const scaleFactorX = width / groupWidth; + const scaleFactorY = height / groupHeight; + const scaleFactor = Math.min(scaleFactorX, scaleFactorY) * 0.8; // 保留一些边距 + + // 居中绘制 + const centerX = width / 2; + const centerY = height / 2; + + tempCtx.save(); + tempCtx.translate(centerX, centerY); + tempCtx.scale(scaleFactor, scaleFactor); + tempCtx.translate(-(minX + groupWidth / 2), -(minY + groupHeight / 2)); + + // 按顺序绘制所有对象 + objects.forEach((obj) => { + if (obj.visible) { + obj.render(tempCtx); + } + }); + + tempCtx.restore(); + + // 转换为数据URL + const dataUrl = tempCanvas.toDataURL("image/png"); + + // 恢复对象状态 + originalStates.forEach((item) => { + item.obj.set(item.state); + }); + + return dataUrl; + } catch (error) { + console.error("绘制组合缩略图出错:", error); + + // 恢复对象状态 + originalStates.forEach((item) => { + item.obj.set(item.state); + }); + + return null; + } + } + + /** + * 批量生成图层缩略图 + * @param {Array} layers 图层数组 + */ + generateAllLayerThumbnails(layers) { + if (!layers || !Array.isArray(layers)) return; + + // 使用requestAnimationFrame批量生成,避免阻塞主线程 + requestAnimationFrame(() => { + layers.forEach((layer) => { + if (layer && layer.id) { + this._generateLayerThumbnailNow(layer); + } + }); + }); + } + + /** + * 获取图层缩略图 + * @param {String} layerId 图层ID + * @returns {String|null} 缩略图URL或null + */ + getLayerThumbnail(layerId) { + if (!layerId) return null; + return this.layerThumbnails.get(layerId) || null; + } + + /** + * 获取元素缩略图 + * @param {String} elementId 元素ID + * @returns {String|null} 缩略图URL或null + */ + getElementThumbnail(elementId) { + if (!elementId) return null; + return this.elementThumbnails.get(elementId) || null; + } + + /** + * 清除图层缩略图 + * @param {String} layerId 图层ID + */ + clearLayerThumbnail(layerId) { + if (layerId && this.layerThumbnails.has(layerId)) { + this.layerThumbnails.delete(layerId); + } + } + + /** + * 清除元素缩略图 + * @param {String} elementId 元素ID + */ + clearElementThumbnail(elementId) { + if (elementId && this.elementThumbnails.has(elementId)) { + this.elementThumbnails.delete(elementId); + } + } + + /** + * 清除所有缩略图 + */ + clearAllThumbnails() { + this.layerThumbnails.clear(); + this.elementThumbnails.clear(); + } + + /** + * 释放资源 + */ + dispose() { + this.clearAllThumbnails(); + this.canvas = null; + } +} diff --git a/src/component/Canvas/CanvasEditor/managers/ToolManager.js b/src/component/Canvas/CanvasEditor/managers/ToolManager.js new file mode 100644 index 00000000..5e0f79df --- /dev/null +++ b/src/component/Canvas/CanvasEditor/managers/ToolManager.js @@ -0,0 +1,1192 @@ +import { BrushStore } from "../store/BrushStore"; +import { BrushManager } from "./brushes/brushManager"; +import { ToolCommand } from "../commands/ToolCommands"; +import { OperationType } from "../utils/layerHelper"; +import CanvasConfig from "../config/canvasConfig"; +//import { fabric } from "fabric-with-all"; +import { + InitLiquifyToolCommand, + RasterizeForLiquifyCommand, +} from "../commands/LiquifyCommands"; + +/** + * 工具管理器 + * 负责管理编辑器中的各种工具 + */ +export class ToolManager { + /** + * 创建工具管理器 + * @param {Object} options 配置选项 + * @param {Object} options.canvas fabric.js画布实例 + * @param {Object} options.commandManager 命令管理器实例 + * @param {Object} options.canvasManager 画布管理实例 + * @param {Object} options.layerManager 图层管理实例 + * @param {Object} options.brushManager 画笔管理器实例(可选,如果不提供会创建一个) + * @param {Object} options.activeTool 当前活动工具的响应式引用 + */ + constructor(options = {}) { + this.canvas = options.canvas; + this.commandManager = options.commandManager; + this.canvasManager = options.canvasManager; + this.layerManager = options.layerManager; + this.activeTool = options.activeTool || { + value: OperationType.SELECT, + }; + + // 红绿图模式状态 + this.isRedGreenMode = false; + this.redGreenModeManager = null; + + // 使用传入的brushManager或创建新的实例 + this.brushManager = + options.brushManager || + new BrushManager({ + canvas: this.canvas, + brushSize: options.brushSize, + layerManager: this.layerManager, // 传入图层管理器引用 + }); + + // 观察者列表 + this.observers = []; + + // 工具列表 - 与OperationType保持一致 + this.tools = { + // 基础工具 + [OperationType.SELECT]: { + name: "选择工具", + icon: "select", + cursor: "default", + shortcut: "V", + setup: this.setupSelectTool.bind(this), + allowedInRedGreen: false, + }, + [OperationType.DRAW]: { + name: "画笔工具", + icon: "brush", + cursor: "crosshair", + shortcut: "B", + setup: this.setupBrushTool.bind(this), + allowedInRedGreen: false, + }, + [OperationType.ERASER]: { + name: "橡皮擦", + icon: "eraser", + cursor: "crosshair", + shortcut: "E", + setup: this.setupEraserTool.bind(this), + allowedInRedGreen: true, // 红绿图模式允许橡皮擦 + }, + [OperationType.EYEDROPPER]: { + name: "吸色工具", + icon: "eyedropper", + cursor: "crosshair", + shortcut: "I", + setup: this.setupEyedropperTool.bind(this), + allowedInRedGreen: false, + }, + [OperationType.PAN]: { + name: "移动画布", + icon: "hand", + cursor: "grab", + shortcut: "H", + setup: this.setupHandTool.bind(this), + allowedInRedGreen: false, // 红绿图模式不允许PAN + }, + + // 套索工具 + [OperationType.LASSO]: { + name: "套索工具", + icon: "lasso", + cursor: "crosshair", + shortcut: "L", + setup: this.setupLassoTool.bind(this), + allowedInRedGreen: false, + }, + [OperationType.LASSO_RECTANGLE]: { + name: "矩形套索工具", + icon: "lasso", + cursor: "crosshair", + // shortcut: "L", + altKey: true, + setup: this.setupRectangleLassoTool.bind(this), + allowedInRedGreen: false, + }, + [OperationType.LASSO_ELLIPSE]: { + name: "椭圆形套索工具", + icon: "lasso", + cursor: "crosshair", + // shortcut: "L", + altKey: true, + setup: this.setupEllipseLassoTool.bind(this), + allowedInRedGreen: false, + }, + + // 选区工具 - 只需要矩形选区 + [OperationType.AREA_RECTANGLE]: { + name: "矩形选区工具", + icon: "area-rectangle", + cursor: "crosshair", + shortcut: "M", + altKey: true, + setup: this.setupRectangleAreaTool.bind(this), + allowedInRedGreen: false, + }, + + // 特效工具 + [OperationType.WAVE]: { + name: "波浪工具", + icon: "wave", + cursor: "crosshair", + shortcut: "W", + setup: this.setupWaveTool.bind(this), + allowedInRedGreen: false, + }, + [OperationType.LIQUIFY]: { + name: "液化工具", + icon: "liquify", + cursor: "crosshair", + shortcut: "J", + setup: this.setupLiquifyTool.bind(this), + allowedInRedGreen: false, // 红绿图模式不允许液化 + }, + [OperationType.TEXT]: { + name: "文本工具", + icon: "text", + cursor: "text", + shortcut: "T", + setup: this.setupTextTool.bind(this), + allowedInRedGreen: false, // 红绿图模式不允许文本 + }, + + // 红绿图模式专用工具 + [OperationType.RED_BRUSH]: { + name: "红色笔刷", + icon: "brush", + cursor: "crosshair", + shortcut: "R", + setup: this.setupRedBrushTool.bind(this), + allowedInRedGreen: true, + redGreenOnly: true, // 只在红绿图模式显示 + }, + [OperationType.GREEN_BRUSH]: { + name: "绿色笔刷", + icon: "brush", + cursor: "crosshair", + shortcut: "G", + setup: this.setupGreenBrushTool.bind(this), + allowedInRedGreen: true, + redGreenOnly: true, // 只在红绿图模式显示 + }, + }; + + // 记录先前的工具 + this.previousTool = null; + + // 初始化默认工具 + this.setTool(this.activeTool.value); + + // 初始化工具快捷键 + this.initKeyboardShortcuts(); + + // 设置文本编辑事件 + this.setupTextEditingEvents(); + + // 添加观察者 + // this.addObserver(this.brushManager); + this.addObserver(this.layerManager); + this.addObserver(this.canvasManager); + } + + /** + * 添加观察者 + * @param {Object} observer 观察者对象,必须实现toolChanged方法 + */ + addObserver(observer) { + if (typeof observer.toolChanged === "function") { + this.observers.push(observer); + } else { + console.warn("Observer must implement toolChanged method"); + } + } + + /** + * 移除观察者 + * @param {Object} observer 要移除的观察者对象 + */ + removeObserver(observer) { + this.observers = this.observers.filter((obs) => obs !== observer); + } + + /** + * 通知所有观察者工具已更改 + * @param {String} toolId 工具ID + */ + notifyObservers(toolId) { + this.observers.forEach((observer) => { + observer.toolChanged(toolId, this.tools[toolId]); + }); + } + + /** + * 初始化工具快捷键 + */ + initKeyboardShortcuts() { + // 可以在这里设置工具快捷键的全局监听 + // 如需要由外部统一管理键盘事件,则不需要此方法 + } + + /** + * 处理快捷键事件 + * @param {KeyboardEvent} event 键盘事件 + */ + handleKeyboardShortcut(event) { + const key = event.key.toUpperCase(); + const altKey = event.altKey; + const ctrlKey = event.ctrlKey; + const shiftKey = event.shiftKey; + + // 当处于输入状态时不触发快捷键 + if ( + event.target.tagName === "INPUT" || + event.target.tagName === "TEXTAREA" + ) { + return; + } + + // 在红绿图模式下,只允许特定快捷键 + if (this.isRedGreenMode) { + const allowedKeys = ["E", "R", "G"]; // 橡皮擦、红色笔刷、绿色笔刷 + if (!allowedKeys.includes(key)) { + return; // 忽略不允许的快捷键 + } + } + + // 查找匹配的工具 + for (const [toolId, tool] of Object.entries(this.tools)) { + if (tool.shortcut && tool.shortcut.toUpperCase() === key) { + // 检查可能的辅助键要求 + if ( + (tool.altKey && !altKey) || + (tool.ctrlKey && !ctrlKey) || + (tool.shiftKey && !shiftKey) + ) { + continue; + } + + // 在红绿图模式下检查工具可用性 + if ( + this.isRedGreenMode && + !this.isToolAvailableInRedGreenMode(toolId) + ) { + continue; + } + + // 切换到该工具 + this.setToolWithCommand(toolId); + event.preventDefault(); + return; + } + } + } + + /** + * 获取所有可用工具列表 + * @returns {Array} 工具列表 + */ + getTools() { + return Object.keys(this.tools).map((key) => { + return { + id: key, + ...this.tools[key], + }; + }); + } + + /** + * 获取当前工具 + * @returns {String} 当前工具ID + */ + getCurrentTool() { + return this.activeTool.value; + } + + /** + * 设置当前活动工具 + * @param {String} toolId 工具ID + */ + setTool(toolId) { + // 检查工具是否存在 + if (!this.tools[toolId]) { + console.error(`工具 '${toolId}' 不存在`); + return; + } + + // 在红绿图模式下检查工具可用性 + if (this.isRedGreenMode && !this.isToolAvailableInRedGreenMode(toolId)) { + console.warn(`工具 '${toolId}' 在红绿图模式下不可用`); + return; + } + + // 在普通模式下检查是否为红绿图专用工具 + if (!this.isRedGreenMode && this.isRedGreenOnlyTool(toolId)) { + console.warn(`工具 '${toolId}' 只能在红绿图模式下使用`); + return; + } + + // 保存先前的工具 + this.previousTool = this.activeTool.value; + + // 设置新工具 + this.activeTool.value = toolId; + + // 设置光标 + if (this.canvas) { + this.canvas.defaultCursor = this.tools[toolId].cursor; + } + + // 设置工具特定的状态 + const tool = this.tools[toolId]; + if (tool && typeof tool.setup === "function") { + tool.setup(); + } + + // 通知选区管理器工具已改变 + if (this.canvasManager && this.canvasManager.selectionManager) { + this.canvasManager.selectionManager.setCurrentTool(toolId); + } + + // 通知观察者 + this.notifyObservers(toolId); + + return this.activeTool.value; + } + + /** + * 通过命令模式设置工具 + * @param {String} toolId 工具ID + */ + setToolWithCommand(toolId, options = {}) { + if (!this.commandManager) { + this.setTool(toolId); + return; + } + + // 创建工具切换命令 + const command = new ToolCommand({ + toolManager: this, + tool: toolId, + previousTool: this.activeTool.value, + }); + + // 执行命令 + this.commandManager.execute(command, { ...options }); + } + + /** + * 恢复当前工具的选择状态 + * 在拖拽结束时调用,确保canvas.selection状态与当前工具一致 + */ + restoreSelectionState() { + if (!this.canvas) return; + + const currentTool = this.activeTool.value; + const tool = this.tools[currentTool]; + + // 根据当前工具设置selection状态 + if (currentTool === OperationType.SELECT) { + this.canvas.selection = true; + } else { + // 对于大多数工具,selection应该是false + this.canvas.selection = false; + } + + // 如有必要,可以调用当前工具的setup方法来全面恢复状态 + if (tool && typeof tool.setup === "function") { + tool.setup(); + } + } + + /** + * 切换回先前使用的工具 + */ + togglePreviousTool() { + if (this.previousTool && this.previousTool !== this.activeTool.value) { + this.setTool(this.previousTool); + } + } + + /** + * 设置选择工具 + */ + setupSelectTool() { + if (!this.canvas) return; + this.canvas.isDrawingMode = false; + this.canvas.selection = true; + } + + /** + * 设置画笔工具 + */ + setupBrushTool() { + if (!this.canvas) return; + + this.canvas.isDrawingMode = true; + this.canvas.selection = false; + + // 确保有笔刷管理器 + if (this.brushManager) { + // 检查画笔是否正在更新中 + if (this.brushManager.isUpdatingBrush) { + console.warn("画笔正在更新中,请稍候..."); + return; + } + + if (BrushStore) { + // 同步基本属性 + this.brushManager.setBrushSize(BrushStore.state.size); + this.brushManager.setBrushColor(BrushStore.state.color); + this.brushManager.setBrushOpacity(BrushStore.state.opacity); + + // 同步笔刷类型 - 修复方法名,使用正确的setBrushType方法 + this.brushManager.setBrushType(BrushStore.state.type); + + // 同步材质设置 + if (BrushStore.state.textureEnabled && BrushStore.state.texturePath) { + this.brushManager.setTexturePath(BrushStore.state.texturePath); + this.brushManager.setTextureScale(BrushStore.state.textureScale); + } + } + + // 更新应用到画布 + this.brushManager.updateBrush(); + } + } + + /** + * 设置橡皮擦工具 + */ + setupEraserTool() { + if (!this.canvas) return; + this.canvas.isDrawingMode = true; + this.canvas.selection = false; + + // 确保有笔刷管理器 + if (this.brushManager) { + this.brushManager.createEraser(); + } + } + + /** + * 设置吸色工具 + */ + setupEyedropperTool() { + if (!this.canvas || !this.brushManager) return; + + this.canvas.isDrawingMode = false; + this.canvas.selection = false; + + // 保存当前工具,以便吸色完成后还原 + const currentTool = this.activeTool.value; + + // 使用吸色工具 + this.brushManager.createEyedropper((color) => { + // 设置画笔颜色 + this.brushManager.setBrushColor(color); + + // 吸色完成后,恢复到之前的工具 + this.setTool(currentTool); + }); + } + + /** + * 设置移动画布工具 + */ + setupHandTool() { + if (!this.canvas) return; + + this.canvas.isDrawingMode = false; + this.canvas.selection = false; + + // 设置画布为可拖动状态 + this.canvas.defaultCursor = "grab"; + } + + /** + * 设置套索工具 + */ + setupLassoTool() { + if (!this.canvas) return; + + this.canvas.isDrawingMode = false; + this.canvas.selection = false; + + // 通知选区管理器切换到自由套索工具 + if (this.canvasManager && this.canvasManager.selectionManager) { + this.canvasManager.selectionManager.setCurrentTool(OperationType.LASSO); + } + } + + /** + * 设置矩形套索工具 + */ + setupRectangleLassoTool() { + if (!this.canvas) return; + + this.canvas.isDrawingMode = false; + this.canvas.selection = false; + + // 通知选区管理器切换到矩形套索工具 + if (this.canvasManager && this.canvasManager.selectionManager) { + this.canvasManager.selectionManager.setCurrentTool( + OperationType.LASSO_RECTANGLE + ); + } + } + + /** + * 设置椭圆形套索工具 + */ + setupEllipseLassoTool() { + if (!this.canvas) return; + + this.canvas.isDrawingMode = false; + this.canvas.selection = false; + + // 通知选区管理器切换到椭圆套索工具 + if (this.canvasManager && this.canvasManager.selectionManager) { + this.canvasManager.selectionManager.setCurrentTool( + OperationType.LASSO_ELLIPSE + ); + } + } + + /** + * 设置自由选区工具 + */ + setupCustomAreaTool() { + if (!this.canvas) return; + + this.canvas.isDrawingMode = false; + this.canvas.selection = false; + + // 通知选区管理器切换到椭圆套索工具 + if (this.canvasManager && this.canvasManager.selectionManager) { + this.canvasManager.selectionManager.setCurrentTool( + OperationType.AREA_CUSTOM + ); + } + } + + /** + * 设置矩形选区工具 + */ + setupRectangleAreaTool() { + if (!this.canvas) return; + + this.canvas.isDrawingMode = false; + this.canvas.selection = false; + + // // 设置矩形选区模式 + // // 这里需要具体的矩形选区工具实现 + console.log("矩形选区工具已激活"); + + if (this.canvasManager && this.canvasManager.selectionManager) { + this.canvasManager.selectionManager.setCurrentTool( + OperationType.AREA_RECTANGLE + ); + } + } + + /** + * 设置波浪工具 + */ + setupWaveTool() { + if (!this.canvas) return; + + this.canvas.isDrawingMode = false; + this.canvas.selection = false; + } + + /** + * 设置液化工具 + */ + setupLiquifyTool() { + if (!this.canvas || !this.layerManager) return; + + this.canvas.isDrawingMode = false; + this.canvas.selection = false; + + // 获取当前活动图层 + const activeLayerId = this.layerManager.getActiveLayerId(); + + // 准备液化面板显示的详情信息 + let panelDetail = { + activeLayerId: activeLayerId, + layerStatus: null, + canLiquify: false, + targetObject: null, + originalImageData: null, + }; + + // 如果有活动图层,检查其状态 + if (activeLayerId) { + const liquifyManager = this.canvasManager?.liquifyManager; + if (liquifyManager) { + // 检查图层状态 + const checkResult = liquifyManager.checkLayerForLiquify(activeLayerId); + panelDetail.layerStatus = checkResult; + + // 获取图层对象 + const layer = this.layerManager.getLayerById(activeLayerId); + + // 检查图层是否为空 + if (!checkResult.isEmpty) { + // 图层不为空,判断是否可直接液化或需要栅格化 + if (checkResult.valid) { + // 可以直接液化 (单个图像对象) + panelDetail.canLiquify = true; + + // 设置目标对象 + if (layer) { + if (layer.isBackground || layer.type === "background") { + panelDetail.targetObject = layer.fabricObject; + } else if ( + layer.fabricObjects && + layer.fabricObjects.length > 0 + ) { + panelDetail.targetObject = layer.fabricObjects[0]; + } + + // 准备液化环境,获取原始图像数据 + if (panelDetail.targetObject && liquifyManager) { + liquifyManager.initialize({ + canvas: this.canvas, + layerManager: this.layerManager, + }); + + // 异步获取原始图像数据 + liquifyManager + .prepareForLiquify(panelDetail.targetObject) + .then((result) => { + if (result && result.originalImageData) { + // 当获取到原始图像数据后触发更新 + const updatedDetail = { + ...panelDetail, + originalImageData: result.originalImageData, + }; + + // 重新触发液化面板显示事件,这次包含原始图像数据 + document.dispatchEvent( + new CustomEvent("showLiquifyPanel", { + detail: updatedDetail, + }) + ); + } + }) + .catch((err) => { + console.error("准备液化环境失败:", err); + }); + } + } + } else if (checkResult.needsRasterization) { + // 需要栅格化 (多个对象或组) + // 询问用户是否要栅格化 + if ( + confirm( + checkResult.isGroup + ? "组对象需要先栅格化才能进行液化操作,是否立即栅格化?" + : "当前图层含有多个对象,需要先栅格化才能进行液化操作,是否立即栅格化?" + ) + ) { + // 用户确认栅格化,执行栅格化操作 + this._rasterizeLayerForLiquify(activeLayerId); + return; // 栅格化后会重新调用液化功能,这里直接返回 + } + } + } + } + } + + // 总是触发液化面板显示事件,不论图层状态如何 + document.dispatchEvent( + new CustomEvent("showLiquifyPanel", { + detail: panelDetail, + }) + ); + } + + /** + * 检查并准备液化操作 + * @param {String} layerId 图层ID + * @private + */ + _checkAndPrepareForLiquify(layerId) { + // 确保存在液化管理器 + const liquifyManager = this.canvasManager?.liquifyManager; + if (!liquifyManager) { + console.error("液化管理器未初始化"); + return; + } + + // 检查图层是否适合液化 + const checkResult = liquifyManager.checkLayerForLiquify(layerId); + + if (checkResult.isEmpty) { + // 空图层 + alert("当前图层为空,无法进行液化操作"); + return; + } + + if (checkResult.isGroup) { + // 询问是否栅格化组 + if (confirm("组对象需要栅格化才能进行液化操作,是否立即栅格化?")) { + this._rasterizeLayerForLiquify(layerId); + } + return; + } + + if (checkResult.needsRasterization) { + // 询问是否栅格化图层 + if ( + confirm( + "当前图层含有多个对象,需要先栅格化才能进行液化操作,是否立即栅格化?" + ) + ) { + this._rasterizeLayerForLiquify(layerId); + } + return; + } + + // 如果图层可以直接液化(单个图像对象) + if (checkResult.valid) { + this._startLiquify(layerId); + } + } + + /** + * 栅格化图层用于液化操作 + * @param {String} layerId 图层ID + * @private + */ + async _rasterizeLayerForLiquify(layerId) { + if (!this.commandManager || !this.layerManager) return; + + try { + // 导入液化相关命令 + + // 创建栅格化命令 + const rasterizeCommand = new RasterizeForLiquifyCommand({ + canvas: this.canvas, + layerManager: this.layerManager, + layerId: layerId, + }); + + // 执行命令 + const result = await this.commandManager.execute(rasterizeCommand); + + if (result) { + // 栅格化成功,启动液化 + this._startLiquify(layerId); + } + } catch (error) { + console.error("栅格化图层失败:", error); + alert("栅格化失败,无法进行液化操作"); + } + } + + /** + * 开始液化操作 + * @param {String} layerId 图层ID + * @private + */ + async _startLiquify(layerId) { + // 获取图层信息 + const layer = this.layerManager.getLayerById(layerId); + if (!layer) { + console.error("图层不存在"); + return; + } + + // 检查图层是否为空 + let targetObject = null; + if (layer.isBackground) { + // 背景图层使用 fabricObject (单数) + if (!layer.fabricObject) { + console.error("背景图层为空"); + return; + } + targetObject = layer.fabricObject; + } else { + // 普通图层使用 fabricObjects (复数) + if (!layer.fabricObjects || layer.fabricObjects.length === 0) { + console.error("图层为空"); + return; + } + targetObject = layer.fabricObjects[0]; + } + + // 确保liquifyManager可用 + const liquifyManager = this.canvasManager?.liquifyManager; + if (!liquifyManager) { + console.error("液化管理器未初始化"); + return; + } + + try { + // 准备液化环境 + liquifyManager.initialize({ + canvas: this.canvas, + layerManager: this.layerManager, + }); + + // 准备液化操作,获取原始图像数据 + const prepareResult = await liquifyManager.prepareForLiquify( + targetObject + ); + + // 创建和初始化命令 + const initCommand = new InitLiquifyToolCommand({ + canvas: this.canvas, + layerManager: this.layerManager, + liquifyManager, + toolManager: this, + }); + + // 执行初始化命令 + await this.commandManager.execute(initCommand); + + // 触发液化面板显示事件 + document.dispatchEvent( + new CustomEvent("showLiquifyPanel", { + detail: { + targetObject, + targetLayerId: layerId, + originalImageData: prepareResult.originalImageData, + }, + }) + ); + } catch (error) { + console.error("启动液化工具失败:", error); + alert("启动液化工具失败:" + error.message); + } + } + + /** + * 触发文件上传操作 + */ + openFile() { + this.onFileSelected?.(); + } + + /** + * 设置文件选择回调 + * @param {Function} callback 回调函数 + */ + setFileUploadHandler(callback) { + this.onFileSelected = callback; + } + + /** + * 更新笔刷大小 + * @param {Number} size 笔刷大小 + */ + updateBrushSize(size) { + if (!this.canvas || !this.brushManager) return; + + // 更新BrushStore + BrushStore.setBrushSize(size); + + // 直接更新笔刷管理器 + this.brushManager.setBrushSize(size); + + // 更新应用到画布 + this.brushManager.updateBrush(); + } + + /** + * 创建文字对象并添加到画布 + * @param {Number} x 文本位置x坐标 + * @param {Number} y 文本位置y坐标 + * @param {Object} options 文本选项 + */ + createText(x, y, options = {}) { + if (!this.canvas || !this.layerManager) return null; + + // 默认文本属性 + const defaultOptions = { + text: "双击编辑文本", + fontFamily: "Arial", + fontSize: 24, + fontWeight: "normal", + fontStyle: "normal", + textAlign: "left", + fill: "#000000", + opacity: 1, + underline: false, + overline: false, + linethrough: false, + textBackgroundColor: "transparent", + lineHeight: 1.16, + charSpacing: 0, + }; + + // 合并默认选项和用户选项 + const textOptions = { ...defaultOptions, ...options, left: x, top: y }; + + // 创建文本对象 + const textObj = new fabric.IText(textOptions.text, { + ...textOptions, + originX: "center", + originY: "center", + id: options.id || this.generateId(), // 生成唯一ID + }); + + // 创建文本图层并通过LayerManager添加到画布 + this.layerManager.createTextLayerWithObject(textObj, textOptions); + this.canvas.renderAll(); + + return textObj; + } + + /** + * 生成唯一ID + * @returns {String} 唯一ID + */ + generateId() { + return "text_" + Date.now() + "_" + Math.floor(Math.random() * 1000); + } + + /** + * 更新笔刷颜色 + * @param {String} color 颜色值,如 "#ff0000" + */ + updateBrushColor(color) { + if (!this.canvas || !this.brushManager) return; + + // 更新BrushStore + BrushStore.setBrushColor(color); + + // 直接更新笔刷管理器 + this.brushManager.setBrushColor(color); + + // 更新应用到画布 + this.brushManager.updateBrush(); + } + + /** + * 更新笔刷透明度 + * @param {Number} opacity 透明度值,范围 0-1 + */ + updateBrushOpacity(opacity) { + if (!this.canvas || !this.brushManager) return; + + // 更新BrushStore + BrushStore.setBrushOpacity(opacity); + + // 直接更新笔刷管理器 + this.brushManager.setBrushOpacity(opacity); + + // 更新应用到画布 + this.brushManager.updateBrush(); + } + + /** + * 设置双击文本的事件监听 + * 当用户双击文本对象时,显示文本编辑弹窗 + */ + setupTextEditingEvents() { + if (!this.canvas) return; + + // 如果已有监听器,先移除以避免重复 + if (this._textEditHandler) { + this.canvas.off("mouse:dblclick", this._textEditHandler); + } + + // 创建双击事件处理函数 + this._textEditHandler = (e) => { + const target = e.target; + if ( + target && + (target.type === "text" || + target.type === "i-text" || + target.type === "textbox") + ) { + // 获取对应的图层 + const layer = this.layerManager.getLayerById(target.layerId); + if (layer) { + // 显示文本编辑面板 + this.showTextEditor(target, layer); + } + } + }; + + // 添加双击事件监听 + this.canvas.on("mouse:dblclick", this._textEditHandler); + } + + /** + * 显示文本编辑面板 + * @param {Object} textObject 文本对象 + * @param {Object} layer 图层对象 + */ + showTextEditor(textObject, layer) { + // 这个方法将在TextEditorPanel组件实现后调用 + console.log("显示文本编辑面板", textObject, layer); + // 将发出一个事件,让Vue组件捕获并显示编辑面板 + document.dispatchEvent( + new CustomEvent("showTextEditor", { + detail: { + textObject, + layer, + }, + }) + ); + } + + /** + * 清理资源 + */ + dispose() { + if (this.brushManager) { + this.brushManager.dispose(); + } + + // 移除文本编辑相关事件监听器 + if (this.canvas) { + this.canvas.off("mouse:dblclick", this._textEditHandler); + } + + this._textEditHandler = null; + + this.observers = []; // 清空观察者 + this.canvas = null; + this.commandManager = null; + this.onFileSelected = null; + } + + /** + * 设置文本工具 + */ + setupTextTool() { + if (!this.canvas) return; + + this.canvas.isDrawingMode = false; + this.canvas.selection = false; + + console.log("文本工具已激活"); + } + + /** + * 设置红色笔刷工具(红绿图模式专用) + */ + setupRedBrushTool() { + if (!this.canvas) return; + + this.canvas.isDrawingMode = true; + this.canvas.selection = false; + + // 确保有笔刷管理器 + if (this.brushManager) { + // 设置红色笔刷 + this.brushManager.setBrushColor("#FF0000"); // 纯红色 + this.brushManager.setBrushOpacity(1.0); // 完全不透明 + this.brushManager.setBrushType("pencil"); // 铅笔类型 + + // 更新笔刷大小(使用当前大小) + if (BrushStore && BrushStore.state.size) { + this.brushManager.setBrushSize(BrushStore.state.size); + } + + // 更新应用到画布 + this.brushManager.updateBrush(); + } + } + + /** + * 设置绿色笔刷工具(红绿图模式专用) + */ + setupGreenBrushTool() { + if (!this.canvas) return; + + this.canvas.isDrawingMode = true; + this.canvas.selection = false; + + // 确保有笔刷管理器 + if (this.brushManager) { + // 设置绿色笔刷 + this.brushManager.setBrushColor("#00FF00"); // 纯绿色 + this.brushManager.setBrushOpacity(1.0); // 完全不透明 + this.brushManager.setBrushType("pencil"); // 铅笔类型 + + // 更新笔刷大小(使用当前大小) + if (BrushStore && BrushStore.state.size) { + this.brushManager.setBrushSize(BrushStore.state.size); + } + + // 更新应用到画布 + this.brushManager.updateBrush(); + } + } + + /** + * 进入红绿图模式 + * @param {Object} redGreenModeManager 红绿图模式管理器实例 + */ + enterRedGreenMode(redGreenModeManager) { + this.isRedGreenMode = true; + this.redGreenModeManager = redGreenModeManager; + + // 切换到红色笔刷工具作为默认工具 + this.setTool(OperationType.RED_BRUSH); + + console.log("工具管理器已进入红绿图模式"); + } + + /** + * 退出红绿图模式 + */ + exitRedGreenMode() { + this.isRedGreenMode = false; + this.redGreenModeManager = null; + + // 切换回选择工具 + this.setTool(OperationType.SELECT); + + console.log("工具管理器已退出红绿图模式"); + } + + /** + * 检查工具是否在红绿图模式下可用 + * @param {String} toolId 工具ID + * @returns {Boolean} 是否可用 + */ + isToolAvailableInRedGreenMode(toolId) { + if (!this.isRedGreenMode) return true; + + const tool = this.tools[toolId]; + return tool && tool.allowedInRedGreen === true; + } + + /** + * 获取红绿图模式下可用的工具列表 + * @returns {Array} 工具列表 + */ + getRedGreenModeTools() { + return Object.keys(this.tools) + .filter((toolId) => this.tools[toolId].allowedInRedGreen === true) + .map((toolId) => ({ + id: toolId, + ...this.tools[toolId], + })); + } + + /** + * 检查是否为红绿图模式专用工具 + * @param {String} toolId 工具ID + * @returns {Boolean} 是否为红绿图模式专用工具 + */ + isRedGreenOnlyTool(toolId) { + const tool = this.tools[toolId]; + return tool && tool.redGreenOnly === true; + } +} diff --git a/src/component/Canvas/CanvasEditor/managers/animation/AnimationManager.js b/src/component/Canvas/CanvasEditor/managers/animation/AnimationManager.js new file mode 100644 index 00000000..0986dc40 --- /dev/null +++ b/src/component/Canvas/CanvasEditor/managers/animation/AnimationManager.js @@ -0,0 +1,856 @@ +import { gsap } from "gsap"; + +/** + * 画布动画管理器 + * 负责处理画布平移、缩放等动画效果 + */ +export class AnimationManager { + /** + * 创建动画管理器 + * @param {fabric.Canvas} canvas fabric.js画布实例 + * @param {Object} options 配置选项 + */ + constructor(canvas, options = {}) { + this.canvas = canvas; + this.currentZoom = options.currentZoom || { value: 100 }; + + // 动画相关属性 + this._zoomAnimation = null; + this._panAnimation = null; + this._lastWheelTime = 0; + this._lastWheelProcessTime = 0; // 上次处理wheel事件的时间 + this._wheelEvents = []; + + // 检测设备类型,Mac设备使用更短的节流时间确保响应性 + this._isMac = navigator.platform.toUpperCase().indexOf("MAC") >= 0; + this._wheelThrottleTime = this._isMac + ? options.wheelThrottleTime || 8 // Mac设备使用更短的节流时间 + : options.wheelThrottleTime || 30; + + this._accumulatedWheelDelta = 0; // 累积滚轮增量 + this._wheelAccumulationTimeout = null; // 滚轮累积超时 + + // Mac设备使用更短的累积时间窗口,确保及时响应 + this._wheelAccumulationTime = this._isMac ? 60 : 120; // 滚轮累积时间窗口(毫秒) + + // 添加新的状态跟踪变量 + this._wasPanning = false; // 是否有平移动画正在进行 + this._wasZooming = false; // 是否有缩放动画正在进行 + this._combinedAnimation = null; // 组合动画引用 + + // Mac特有的动画优化变量 - 使用最小防抖机制 + if (this._isMac) { + this._lastMacAnimationTime = 0; // 上次Mac动画时间 + this._macAnimationCooldown = 2; // 最小的动画冷却时间,确保最大响应性 + } + + // 初始化GSAP默认配置 + gsap.defaults({ + ease: options.defaultEase || (this._isMac ? "power2.out" : "power2.out"), // Mac使用简单高效的缓动 + duration: options.defaultDuration || (this._isMac ? 0.3 : 0.3), // Mac使用标准持续时间 + overwrite: "auto", // 自动覆盖同一对象上的动画 + }); + } + + /** + * 使用 GSAP 实现平滑缩放动画 + * @param {Object} point 缩放中心点 {x, y} + * @param {Number} targetZoom 目标缩放值 + * @param {Object} options 动画选项 + */ + animateZoom(point, targetZoom, options = {}) { + if (!this.canvas) return; + + // 限制缩放范围 + targetZoom = Math.min(Math.max(targetZoom, 0.1), 20); + + // 当前缩放值 + const currentZoom = this.canvas.getZoom(); + + // 如果变化太小,直接应用缩放 + if (Math.abs(targetZoom - currentZoom) < 0.01) { + this._applyZoom(point, targetZoom); + return; + } + + // 停止任何进行中的缩放动画 + if (this._zoomAnimation) { + // 不是直接 kill,而是获取当前进度值作为新的起点 + const currentProgress = this._zoomAnimation.progress(); + const currentZoomValue = this._zoomAnimation.targets()[0].value; + this._zoomAnimation.kill(); + this._zoomAnimation = null; + + // 从当前过渡中的值开始新动画,而不是从最初的值 + const zoomObj = { value: currentZoomValue }; + const currentVpt = [...this.canvas.viewportTransform]; + + // 计算过渡动画持续时间 - 根据当前值到目标值的距离比例 + const progressRatio = + Math.abs(targetZoom - currentZoomValue) / + Math.abs(targetZoom - currentZoom); + const duration = options.duration || 0.3 * progressRatio; + + // 计算缩放后目标位置需要的修正,保持缩放点不变 + const animOptions = { + value: targetZoom, + duration: duration, + ease: options.ease || "power2.out", + onUpdate: () => { + // 更新缩放值显示 + this.currentZoom.value = Math.round(zoomObj.value * 100); + + // 计算过渡中的变换矩阵 + const zoom = zoomObj.value; + const scale = zoom / currentZoomValue; + const currentScaleFactor = scale; + + // 应用变换 + const vpt = this.canvas.viewportTransform; + vpt[0] = currentVpt[0] * scale; + vpt[3] = currentVpt[3] * scale; + + // 应用平移修正以保持缩放点 + const adjustX = (1 - currentScaleFactor) * point.x; + const adjustY = (1 - currentScaleFactor) * point.y; + vpt[4] = currentVpt[4] * scale + adjustX; + vpt[5] = currentVpt[5] * scale + adjustY; + + this.canvas.renderAll(); + }, + onComplete: () => { + this._zoomAnimation = null; + + // 确保最终状态准确 + this._applyZoom(point, targetZoom, true); + }, + }; + + // 启动 GSAP 动画 + this._zoomAnimation = gsap.to(zoomObj, animOptions); + return; + } + + // 如果没有正在进行的动画,创建新的缩放动画 + const zoomObj = { value: currentZoom }; + const currentVpt = [...this.canvas.viewportTransform]; + + // 计算缩放后目标位置需要的修正,保持缩放点不变 + const scaleFactor = targetZoom / currentZoom; + const invertedScaleFactor = 1 / scaleFactor; + + // 这个数学公式确保缩放点在屏幕上的位置保持不变 + const dx = point.x - point.x * invertedScaleFactor; + const dy = point.y - point.y * invertedScaleFactor; + + // 创建动画配置 + const animOptions = { + value: targetZoom, + duration: options.duration || 0.3, + ease: options.ease || (this._isMac ? "expo.out" : "power2.out"), // Mac使用更平滑的缓动 + onUpdate: () => { + // 更新缩放值显示 + this.currentZoom.value = Math.round(zoomObj.value * 100); + + // 计算过渡中的变换矩阵 + const zoom = zoomObj.value; + const scale = zoom / currentZoom; + const currentScaleFactor = scale; + + // 应用变换 + const vpt = this.canvas.viewportTransform; + vpt[0] = currentVpt[0] * scale; + vpt[3] = currentVpt[3] * scale; + + // 应用平移修正以保持缩放点 + const adjustX = (1 - currentScaleFactor) * point.x; + const adjustY = (1 - currentScaleFactor) * point.y; + vpt[4] = currentVpt[4] * scale + adjustX; + vpt[5] = currentVpt[5] * scale + adjustY; + + this.canvas.renderAll(); + }, + onComplete: () => { + this._zoomAnimation = null; + + // 确保最终状态准确 + this._applyZoom(point, targetZoom, true); + }, + }; + + // 启动 GSAP 动画 + this._zoomAnimation = gsap.to(zoomObj, animOptions); + } + + /** + * 应用缩放(内部使用) + * @private + */ + _applyZoom(point, zoom, skipUpdate = false) { + if (!skipUpdate) { + this.currentZoom.value = Math.round(zoom * 100); + } + this.canvas.zoomToPoint(point, zoom); + } + + /** + * 使用 GSAP 实现平滑平移动画 + * @param {Object} targetPosition 目标位置 {x, y} + * @param {Object} options 动画选项 + */ + animatePan(targetPosition, options = {}) { + if (!this.canvas) return; + + // 停止任何进行中的平移动画 + if (this._panAnimation) { + this._panAnimation.kill(); + } + + const currentVpt = [...this.canvas.viewportTransform]; + const position = { + x: -currentVpt[4], + y: -currentVpt[5], + }; + + // 计算平移距离 + const dx = targetPosition.x - position.x; + const dy = targetPosition.y - position.y; + + // 如果距离太小,直接应用平移 + if (Math.abs(dx) < 1 && Math.abs(dy) < 1) { + this._applyPan(targetPosition.x, targetPosition.y); + return; + } + + // 创建动画配置 + const animOptions = { + x: targetPosition.x, + y: targetPosition.y, + duration: options.duration || 0.3, + ease: options.ease || (this._isMac ? "circ.out" : "power2.out"), // Mac使用更柔和的缓动 + onUpdate: () => { + this._applyPan(position.x, position.y); + }, + onComplete: () => { + this._panAnimation = null; + // 确保最终位置准确 + this._applyPan(targetPosition.x, targetPosition.y); + }, + }; + + // 启动 GSAP 动画 + this._panAnimation = gsap.to(position, animOptions); + } + + /** + * 应用平移(内部使用) + * @private + */ + _applyPan(x, y) { + if (!this.canvas) return; + + const vpt = this.canvas.viewportTransform; + vpt[4] = -x; + vpt[5] = -y; + + this.canvas.renderAll(); + } + + /** + * 使用动画平移到指定元素 + * @param {Object} elementId 元素ID + */ + panToElement(elementId) { + if (!this.canvas) return; + + const obj = this.canvas.getObjects().find((obj) => obj.id === elementId); + if (!obj) return; + + const zoom = this.canvas.getZoom(); + const center = obj.getCenterPoint(); + + // 计算目标中心位置 + const targetX = center.x * zoom - this.canvas.width / 2; + const targetY = center.y * zoom - this.canvas.height / 2; + + // 动画平移 + this.animatePan( + { x: targetX, y: targetY }, + { + duration: 0.6, + ease: this._isMac ? "back.out(0.3)" : "power3.out", // Mac使用轻微回弹效果 + } + ); + } + + /** + * 重置缩放(带平滑动画) + * @param {Boolean} animated 是否使用动画 + */ + async resetZoom(animated = true) { + return new Promise((resolve) => { + if (animated) { + // 停止任何进行中的动画 + if (this._zoomAnimation) { + this._zoomAnimation.kill(); + } + if (this._panAnimation) { + this._panAnimation.kill(); + } + + const center = { + x: this.canvas.width / 2, + y: this.canvas.height / 2, + }; + + // 获取当前变换矩阵 + const currentVpt = [...this.canvas.viewportTransform]; + const currentZoom = this.canvas.getZoom(); + + // 创建一个对象来动画整个视图变换 + const viewTransform = { + zoom: currentZoom, + panX: currentVpt[4], + panY: currentVpt[5], + }; + + // 使用GSAP同时动画缩放和平移 + gsap.to(viewTransform, { + zoom: 1, + panX: 0, + panY: 0, + duration: 0.5, + ease: this._isMac ? "back.out(0.2)" : "power3.out", // Mac使用轻微回弹效果 + onUpdate: () => { + // 更新缩放显示值 + this.currentZoom.value = Math.round(viewTransform.zoom * 100); + + // 应用新的变换 + const vpt = this.canvas.viewportTransform; + vpt[0] = viewTransform.zoom; + vpt[3] = viewTransform.zoom; + vpt[4] = viewTransform.panX; + vpt[5] = viewTransform.panY; + + this.canvas.renderAll(); + }, + onComplete: () => { + // 确保最终状态准确 + this.canvas.setViewportTransform([1, 0, 0, 1, 0, 0]); + this.currentZoom.value = 100; + this._zoomAnimation = null; + this._panAnimation = null; + resolve(); + }, + }); + } else { + this.canvas.setViewportTransform([1, 0, 0, 1, 0, 0]); + this.currentZoom.value = 100; + resolve(); + } + }); + } + + /** + * 处理鼠标滚轮缩放 + * @param {Object} opt 事件对象 + */ + handleMouseWheel(opt) { + const now = Date.now(); + let delta = opt.e.deltaY; + + // 记录事件用于计算速度和惯性 + this._wheelEvents.push({ + delta: delta, + point: { x: opt.e.offsetX, y: opt.e.offsetY }, + time: now, + hasPanAnimation: this._wasPanning, + hasZoomAnimation: this._wasZooming, + }); + + // 保留最近的事件记录 + if (this._wheelEvents.length > 10) { + this._wheelEvents.shift(); + } + + // 检查是否是第一个事件或者距离上次处理已经过了足够时间 + const isFirstEvent = !this._wheelAccumulationTimeout; + const timeSinceLastProcess = now - (this._lastWheelProcessTime || 0); + + if (isFirstEvent || timeSinceLastProcess > this._wheelAccumulationTime) { + // 立即处理第一个事件或长时间没有处理的事件,确保响应性 + this._processAccumulatedWheel(opt); + this._lastWheelProcessTime = now; + + // 清理之前的累积 + this._accumulatedWheelDelta = 0; + + // 如果有pending的timeout,清除它 + if (this._wheelAccumulationTimeout) { + clearTimeout(this._wheelAccumulationTimeout); + this._wheelAccumulationTimeout = null; + } + } else { + // 累积后续事件 + this._accumulatedWheelDelta += delta; + + // 如果正在累积中,清除之前的定时器 + if (this._wheelAccumulationTimeout) { + clearTimeout(this._wheelAccumulationTimeout); + } + + // 设置新的定时器,处理累积的事件 + this._wheelAccumulationTimeout = setTimeout(() => { + this._processAccumulatedWheel(opt); + this._lastWheelProcessTime = Date.now(); + + // 清理 + this._accumulatedWheelDelta = 0; + this._wheelAccumulationTimeout = null; + }, this._wheelThrottleTime); + } + + opt.e.preventDefault(); + opt.e.stopPropagation(); + } + + /** + * 处理累积的滚轮事件并应用缩放 + * @private + * @param {Object} lastOpt 最后一个滚轮事件 + */ + _processAccumulatedWheel(lastOpt) { + if (!this._wheelEvents.length) return; + + const now = Date.now(); + + // Mac设备的轻量防抖检查 - 进一步减少冷却时间,确保响应性 + if ( + this._isMac && + now - this._lastMacAnimationTime < this._macAnimationCooldown + ) { + // 如果距离上次动画时间太短,只延迟很短时间,不阻塞太久 + if (this._wheelAccumulationTimeout) { + clearTimeout(this._wheelAccumulationTimeout); + } + this._wheelAccumulationTimeout = setTimeout(() => { + this._processAccumulatedWheel(lastOpt); + }, Math.min(this._macAnimationCooldown, 3)); // 最多延迟3ms + return; + } + + const currentZoom = this.canvas.getZoom(); + + // 分析滚轮事件模式,计算平均增量、速度和加速度 + let sumDelta = 0; + let count = 0; + let earliestTime = now; + let latestTime = 0; + let point = { + x: lastOpt.e.offsetX, + y: lastOpt.e.offsetY, + }; + + // 判断是否在事件收集期间有平移或缩放动画 + let hadPanAnimation = false; + let hadZoomAnimation = false; + + // 计算平均增量和速度 + this._wheelEvents.forEach((event) => { + sumDelta += event.delta; + count++; + earliestTime = Math.min(earliestTime, event.time); + latestTime = Math.max(latestTime, event.time); + + // 使用最后记录的点作为缩放中心 + if (event.time > latestTime) { + point = event.point; + } + + // 检查是否有动画状态 + if (event.hasPanAnimation) hadPanAnimation = true; + if (event.hasZoomAnimation) hadZoomAnimation = true; + }); + + // 计算平均增量 + const avgDelta = sumDelta / count; + + // 计算滚动速度 - 基于事件频率和时间跨度 + const timeSpan = latestTime - earliestTime + 1; // 避免除以零 + const eventsPerSecond = (count / timeSpan) * 1000; + + // 速度系数: 速度越快,缩放越敏感 + let speedFactor = Math.min(3, Math.max(0.5, eventsPerSecond / 10)); + + // 计算缩放因子,应用速度系数 + // 针对Mac设备优化:Mac触控板的deltaY值通常较小,需要适度增加敏感度 + let zoomFactorBase = 0.999; + if (this._isMac) { + // Mac设备的触控板需要适度的敏感度,避免过度反应 + zoomFactorBase = 0.995; // 适度降低基数,增加缩放敏感度 + + // 检测是否为触控板滚动(小幅度、高频次的特征) + const avgAbsDelta = Math.abs(avgDelta); + if (avgAbsDelta < 50 && count > 2) { + // 触控板滚动,适度增加敏感度 + speedFactor *= 1.6; // 适度增加敏感度倍数 + zoomFactorBase = 0.993; // 进一步调整基数 + } + } + + const zoomFactor = zoomFactorBase ** (avgDelta * speedFactor); + let targetZoom = currentZoom * zoomFactor; + + // 限制缩放范围 + targetZoom = Math.min(Math.max(targetZoom, 0.1), 20); + + // 根据滚动速度和缩放幅度计算动画持续时间 + // 速度快时缩短动画时间,缩放幅度大时延长动画时间 + const zoomRatio = Math.abs(targetZoom - currentZoom) / currentZoom; + + let duration; + if (this._isMac) { + // Mac设备使用平衡的动画时间控制 + if (speedFactor > 2) { + // 快速操作:快速但平滑 + duration = Math.min( + 0.18, + Math.max(0.08, (zoomRatio * 0.3) / Math.sqrt(speedFactor)) + ); + } else if (speedFactor > 1.2) { + // 中等速度:标准响应 + duration = Math.min( + 0.25, + Math.max(0.1, (zoomRatio * 0.4) / Math.sqrt(speedFactor)) + ); + } else { + // 慢速精确操作:确保平滑 + duration = Math.min( + 0.3, + Math.max(0.12, (zoomRatio * 0.5) / Math.sqrt(speedFactor)) + ); + } + } else { + duration = Math.min( + 0.5, + Math.max(0.15, (zoomRatio * 0.8) / Math.sqrt(speedFactor)) + ); + } + + // 根据滚动速度选择不同的缓动效果 + let easeType; + if (this._isMac) { + // Mac设备使用更简单、性能更好的缓动函数 + // 避免复杂的指数和回弹效果,减少计算量 + if (speedFactor > 2) { + // 快速滚动:使用简单的缓出效果 + easeType = "power2.out"; + } else if (speedFactor > 1.2) { + // 中等速度:使用平滑的缓出 + easeType = "power1.out"; + } else { + // 慢速精确操作:使用线性过渡 + easeType = "power1.out"; + } + } else { + // 非Mac设备保持原有的缓动 + easeType = speedFactor > 1.5 ? "power1.out" : "power2.out"; + } + + // 根据是否有其他动画正在进行,选择合适的动画方法 + if (hadPanAnimation || this._wasPanning) { + // 如果有平移动画,使用组合动画以保持平滑过渡 + this.animateCombinedTransform(point, targetZoom, { + duration: duration, + ease: easeType, + }); + } else { + // 如果没有其他动画,使用标准缩放动画 + this.animateZoom(point, targetZoom, { + duration: duration, + ease: easeType, + }); + } + + // 更新Mac设备的最后动画时间 + if (this._isMac) { + this._lastMacAnimationTime = now; + } + + // 清理事件记录 + this._wheelEvents = []; + } + + /** + * 计算并应用拖动结束后的惯性效果 + * @param {Array} positions 拖动过程中记录的位置数组 + * @param {Boolean} isTouchDevice 是否是触摸设备 + */ + applyInertiaEffect(positions, isTouchDevice) { + if (!positions || positions.length <= 1) return; + + const lastPos = positions[positions.length - 1]; + const firstPos = positions[0]; + const deltaTime = lastPos.time - firstPos.time; + + if (deltaTime <= 0) return; + + // 计算速度向量 (像素/毫秒) + const velocityX = (lastPos.x - firstPos.x) / deltaTime; + const velocityY = (lastPos.y - firstPos.y) / deltaTime; + const speed = Math.sqrt(velocityX * velocityX + velocityY * velocityY); + + // 仅当速度足够大时应用惯性效果 + if (speed > 0.2) { + // 计算惯性距离,基于速度和衰减因子 + const decayFactor = 300; // 调整此值以改变惯性效果的强度 + const inertiaDistanceX = velocityX * decayFactor; + const inertiaDistanceY = velocityY * decayFactor; + + // 计算目标位置 + const vpt = this.canvas.viewportTransform; + const currentPos = { + x: -vpt[4], + y: -vpt[5], + }; + + const targetPos = { + x: currentPos.x - inertiaDistanceX, + y: currentPos.y - inertiaDistanceY, + }; + + // 应用惯性动画,速度越大,动画时间越长 + const animationDuration = Math.min(1.2, Math.max(0.6, speed * 2)); + + // 应用惯性动画 + this.animatePan(targetPos, { + duration: animationDuration, // 动态计算持续时间 + ease: this._isMac ? "quart.out" : "power3.out", // Mac使用更自然的减速效果 + }); + } + } + + /** + * 平滑过渡停止所有动画 + * 用于在需要中断当前动画时提供更自然的过渡,而不是硬性中断 + * @param {Object} options 过渡选项 + */ + smoothStopAnimations(options = {}) { + const duration = options.duration || 0.15; // 默认短暂过渡时间 + + // 处理缩放动画 + if (this._zoomAnimation) { + const zoomObj = this._zoomAnimation.targets()[0]; + const currentZoom = this.canvas.getZoom(); + + // 创建短暂的过渡动画到当前值 + gsap.to(zoomObj, { + value: currentZoom, + duration: duration, + ease: this._isMac ? "circ.out" : "power1.out", // Mac使用更平滑的缓动 + onUpdate: () => { + this.currentZoom.value = Math.round(zoomObj.value * 100); + this.canvas.renderAll(); + }, + onComplete: () => { + if (this._zoomAnimation) { + this._zoomAnimation.kill(); + this._zoomAnimation = null; + } + }, + }); + } + + // 处理平移动画 + if (this._panAnimation) { + const panObj = this._panAnimation.targets()[0]; + const vpt = this.canvas.viewportTransform; + const currentPos = { x: -vpt[4], y: -vpt[5] }; + + // 创建短暂的过渡动画到当前位置 + gsap.to(panObj, { + x: currentPos.x, + y: currentPos.y, + duration: duration, + ease: this._isMac ? "circ.out" : "power1.out", // Mac使用更平滑的缓动 + onUpdate: () => { + this._applyPan(panObj.x, panObj.y); + }, + onComplete: () => { + if (this._panAnimation) { + this._panAnimation.kill(); + this._panAnimation = null; + } + }, + }); + } + } + + /** + * 设置画布交互动画 + * 为对象交互添加流畅的动画效果 + */ + setupInteractionAnimations() { + if (!this.canvas) return; + + // 启用对象旋转的流畅动画 + this._setupRotationAnimation(); + } + + /** + * 设置旋转动画 + * @private + */ + _setupRotationAnimation() { + if (!fabric) return; + + // 保存原始旋转方法 + const originalRotate = fabric.Object.prototype.rotate; + const isMac = this._isMac; // 保存Mac检测结果 + + // 覆盖旋转方法以添加动画 + fabric.Object.prototype.rotate = function (angle) { + const currentAngle = this.angle || 0; + + if (Math.abs(angle - currentAngle) > 0.1) { + gsap.to(this, { + angle: angle, + duration: 0.3, + ease: isMac ? "back.out(0.3)" : "power2.out", // Mac使用轻微回弹 + onUpdate: () => { + this.canvas && this.canvas.renderAll(); + }, + }); + return this; + } + + // 如果角度差异很小,使用原始方法 + return originalRotate.call(this, angle); + }; + } + + /** + * 处理滚轮缩放,同时兼容正在进行的平移动画 + * @param {Object} point 缩放中心点 + * @param {Number} targetZoom 目标缩放值 + * @param {Object} options 动画选项 + */ + animateCombinedTransform(point, targetZoom, options = {}) { + if (!this.canvas) return; + + // 限制缩放范围 + targetZoom = Math.min(Math.max(targetZoom, 0.1), 20); + + // 当前状态 + const currentZoom = this.canvas.getZoom(); + const currentVpt = [...this.canvas.viewportTransform]; + const currentPos = { x: -currentVpt[4], y: -currentVpt[5] }; + + // 如果有正在进行的动画,先停止它们 + if (this._combinedAnimation) { + this._combinedAnimation.kill(); + this._combinedAnimation = null; + } + + if (this._zoomAnimation) { + this._zoomAnimation.kill(); + this._zoomAnimation = null; + } + + if (this._panAnimation) { + this._panAnimation.kill(); + this._panAnimation = null; + } + + // 创建一个统一的变换对象来动画 + const transform = { + zoom: currentZoom, + panX: currentVpt[4], + panY: currentVpt[5], + progress: 0, // 用于动画进度跟踪 + }; + + // 获取平移目标位置(如果有的话) + let panTarget = { x: currentPos.x, y: currentPos.y }; + if (this._wasPanning) { + // 如果之前有平移动画,尝试获取平移的目标位置 + const vpt = this.canvas.viewportTransform; + panTarget = { + x: currentPos.x, + y: currentPos.y, + }; + } + + // 计算新的变换矩阵,同时考虑平移和缩放 + const scaleFactor = targetZoom / currentZoom; + + // 创建动画 + this._combinedAnimation = gsap.to(transform, { + zoom: targetZoom, + progress: 1, + duration: options.duration || 0.3, + ease: options.ease || (this._isMac ? "expo.out" : "power2.out"), // Mac使用更平滑的缓动 + onUpdate: () => { + // 计算当前动画阶段的混合变换 + const currentScaleFactor = transform.zoom / currentZoom; + + // 应用缩放 + const vpt = this.canvas.viewportTransform; + vpt[0] = currentVpt[0] * (transform.zoom / currentZoom); + vpt[3] = currentVpt[3] * (transform.zoom / currentZoom); + + // 平滑混合平移和缩放调整 + const adjustX = (1 - currentScaleFactor) * point.x; + const adjustY = (1 - currentScaleFactor) * point.y; + + // 如果存在平移目标,进行插值 + if (this._wasPanning) { + const t = transform.progress; + const interpolatedX = currentPos.x * (1 - t) + panTarget.x * t; + const interpolatedY = currentPos.y * (1 - t) + panTarget.y * t; + + // 结合缩放和平移的调整 + vpt[4] = -interpolatedX * currentScaleFactor + adjustX; + vpt[5] = -interpolatedY * currentScaleFactor + adjustY; + } else { + // 只有缩放,保持中心点 + vpt[4] = currentVpt[4] * currentScaleFactor + adjustX; + vpt[5] = currentVpt[5] * currentScaleFactor + adjustY; + } + + // 更新缩放值显示 + this.currentZoom.value = Math.round(transform.zoom * 100); + this.canvas.renderAll(); + }, + onComplete: () => { + this._combinedAnimation = null; + this._zoomAnimation = null; + this._panAnimation = null; + this._wasPanning = false; + this._wasZooming = false; + + // 确保最终状态准确 + this._applyZoom(point, targetZoom, true); + }, + }); + } + + /** + * 清理资源 + */ + dispose() { + if (this._zoomAnimation) { + this._zoomAnimation.kill(); + this._zoomAnimation = null; + } + + if (this._panAnimation) { + this._panAnimation.kill(); + this._panAnimation = null; + } + + this._wheelEvents = []; + this.canvas = null; + this.currentZoom = null; + } +} diff --git a/src/component/Canvas/CanvasEditor/managers/brushes/BaseBrush.js b/src/component/Canvas/CanvasEditor/managers/brushes/BaseBrush.js new file mode 100644 index 00000000..921fa47d --- /dev/null +++ b/src/component/Canvas/CanvasEditor/managers/brushes/BaseBrush.js @@ -0,0 +1,234 @@ +/** + * 笔刷基类 + * 所有笔刷类型应继承此基类并实现必要的方法 + */ +export class BaseBrush { + /** + * 构造函数 + * @param {Object} canvas fabric画布实例 + * @param {Object} options 笔刷配置选项 + */ + constructor(canvas, options = {}) { + this.canvas = canvas; + this.options = options; + + // 基本属性 + this.id = options.id || this.constructor.name; + this.name = options.name || "未命名笔刷"; + this.description = options.description || ""; + this.icon = options.icon || null; + this.category = options.category || "默认"; + + // 笔刷实例 + this.brush = null; + } + + /** + * 创建笔刷实例(必须由子类实现) + * @returns {Object} fabric笔刷实例 + */ + create() { + throw new Error("必须由子类实现create方法"); + } + + /** + * 配置笔刷(必须由子类实现) + * @param {Object} brush fabric笔刷实例 + * @param {Object} options 配置选项 + */ + configure(brush, options) { + throw new Error("必须由子类实现configure方法"); + } + + /** + * 获取笔刷的元数据 + * @returns {Object} 笔刷元数据 + */ + getMetadata() { + return { + id: this.id, + name: this.name, + description: this.description, + icon: this.icon, + category: this.category, + }; + } + + /** + * 获取笔刷预览 + * @returns {String|null} 预览图URL或null + */ + getPreview() { + return null; + } + + /** + * 获取笔刷可配置属性 + * 这个方法返回一个对象数组,每个对象描述一个可配置属性 + * 每个属性对象包含: + * - id: 属性标识符 + * - name: 属性显示名称 + * - type: 属性类型(例如:'slider', 'color', 'checkbox', 'select') + * - defaultValue: 默认值 + * - min/max/step: 对于slider类型的限制值 + * - options: 对于select类型的选项 + * - description: 属性描述 + * - category: 属性分类 + * - order: 显示顺序(越小越靠前) + * - visibleWhen: 函数或对象,定义何时显示该属性 + * - dynamicOptions: 函数,返回动态的选项列表 + * @returns {Array} 可配置属性描述数组 + */ + getConfigurableProperties() { + // 返回基础属性,所有笔刷都有这些属性 + return [ + { + id: "size", + name: "笔刷大小", + type: "slider", + defaultValue: 5, + min: 0.5, + max: 100, + step: 0.5, + description: "笔刷的大小(像素)", + category: "基本", + order: 10, + }, + { + id: "color", + name: "笔刷颜色", + type: "color", + defaultValue: "#000000", + description: "笔刷的颜色", + category: "基本", + order: 20, + }, + { + id: "opacity", + name: "不透明度", + type: "slider", + defaultValue: 1, + min: 0.05, + max: 1, + step: 0.01, + description: "笔刷的不透明度", + category: "基本", + order: 30, + }, + ]; + } + + /** + * 合并特有属性与基本属性 + * 子类应该调用此方法来合并自身特有属性与基类提供的基本属性 + * @param {Array} specificProperties 特有属性数组 + * @returns {Array} 合并后的属性数组 + */ + mergeWithBaseProperties(specificProperties) { + const baseProperties = super.getConfigurableProperties(); + + // 过滤掉同名属性(子类优先) + const basePropsFiltered = baseProperties.filter( + (baseProp) => + !specificProperties.some( + (specificProp) => specificProp.id === baseProp.id + ) + ); + + return [...basePropsFiltered, ...specificProperties]; + } + + /** + * 更新笔刷属性 + * @param {String} propId 属性ID + * @param {any} value 属性值 + * @returns {Boolean} 是否更新成功 + */ + updateProperty(propId, value) { + // 基础实现,可以被子类覆盖以处理特殊属性 + if (propId === "size") { + if (this.brush) { + this.brush.width = value; + return true; + } + } else if (propId === "color") { + if (this.brush) { + this.brush.color = value; + return true; + } + } else if (propId === "opacity") { + if (this.brush) { + this.brush.opacity = value; + return true; + } + } + return false; + } + + /** + * 检查属性是否可见 + * @param {Object} property 属性对象 + * @param {Object} currentValues 当前所有属性的值 + * @returns {Boolean} 是否可见 + */ + isPropertyVisible(property, currentValues) { + // 如果没有visibleWhen条件,则始终显示 + if (!property.visibleWhen) { + return true; + } + + // 如果visibleWhen是函数,则调用函数判断 + if (typeof property.visibleWhen === "function") { + return property.visibleWhen(currentValues); + } + + // 如果visibleWhen是对象,检查条件是否满足 + if (typeof property.visibleWhen === "object") { + for (const [key, value] of Object.entries(property.visibleWhen)) { + if (currentValues[key] !== value) { + return false; + } + } + return true; + } + + return true; + } + + /** + * 获取动态选项 + * @param {Object} property 属性对象 + * @param {Object} currentValues 当前所有属性的值 + * @returns {Array} 选项数组 + */ + getDynamicOptions(property, currentValues) { + if ( + property.dynamicOptions && + typeof property.dynamicOptions === "function" + ) { + return property.dynamicOptions(currentValues); + } + return property.options || []; + } + + /** + * 生命周期方法:笔刷被选中 + */ + onSelected() { + // 可由子类覆盖 + } + + /** + * 生命周期方法:笔刷被取消选中 + */ + onDeselected() { + // 可由子类覆盖 + } + + /** + * 销毁笔刷实例并清理资源 + */ + destroy() { + this.brush = null; + } +} diff --git a/src/component/Canvas/CanvasEditor/managers/brushes/BrushRegistry.js b/src/component/Canvas/CanvasEditor/managers/brushes/BrushRegistry.js new file mode 100644 index 00000000..8e133f12 --- /dev/null +++ b/src/component/Canvas/CanvasEditor/managers/brushes/BrushRegistry.js @@ -0,0 +1,201 @@ +/** + * 笔刷注册表 + * 用于注册、获取和管理所有笔刷 + */ +export class BrushRegistry { + constructor() { + // 存储所有注册的笔刷类 + this.brushes = new Map(); + + // 按类别组织的笔刷 + this.brushesByCategory = new Map(); + + // 事件监听器 + this.listeners = { + register: [], + unregister: [], + }; + } + + /** + * 注册一个笔刷 + * @param {String} id 笔刷唯一标识 + * @param {Class} brushClass 笔刷类(需要继承BaseBrush) + * @param {Object} metadata 笔刷元数据(可选) + * @returns {Boolean} 是否注册成功 + */ + register(id, brushClass, metadata = {}) { + if (this.brushes.has(id)) { + console.warn(`笔刷 ${id} 已存在,请使用其他ID`); + return false; + } + + // 存储笔刷信息 + const brushInfo = { + id, + class: brushClass, + metadata: { + ...metadata, + id, + }, + }; + + this.brushes.set(id, brushInfo); + + // 添加到分类 + const category = metadata.category || "默认"; + if (!this.brushesByCategory.has(category)) { + this.brushesByCategory.set(category, []); + } + this.brushesByCategory.get(category).push(brushInfo); + + // 触发事件 + this._triggerEvent("register", brushInfo); + + return true; + } + + /** + * 取消注册笔刷 + * @param {String} id 笔刷ID + * @returns {Boolean} 是否成功 + */ + unregister(id) { + if (!this.brushes.has(id)) { + return false; + } + + const brushInfo = this.brushes.get(id); + this.brushes.delete(id); + + // 从分类中移除 + const category = brushInfo.metadata.category || "默认"; + if (this.brushesByCategory.has(category)) { + const brushes = this.brushesByCategory.get(category); + const index = brushes.findIndex((b) => b.id === id); + if (index !== -1) { + brushes.splice(index, 1); + } + + // 如果分类为空,删除该分类 + if (brushes.length === 0) { + this.brushesByCategory.delete(category); + } + } + + // 触发事件 + this._triggerEvent("unregister", brushInfo); + + return true; + } + + /** + * 获取笔刷信息 + * @param {String} id 笔刷ID + * @returns {Object|null} 笔刷信息或null + */ + getBrush(id) { + return this.brushes.get(id) || null; + } + + /** + * 获取所有笔刷 + * @returns {Array} 笔刷信息数组 + */ + getAllBrushes() { + return Array.from(this.brushes.values()); + } + + /** + * 获取指定分类的笔刷 + * @param {String} category 分类名称 + * @returns {Array} 笔刷信息数组 + */ + getBrushesByCategory(category) { + return this.brushesByCategory.get(category) || []; + } + + /** + * 获取所有分类 + * @returns {Array} 分类名称数组 + */ + getCategories() { + return Array.from(this.brushesByCategory.keys()); + } + + /** + * 创建一个笔刷实例 + * @param {String} id 笔刷ID + * @param {Object} canvas 画布实例 + * @param {Object} options 配置选项 + * @returns {Object|null} 笔刷实例或null + */ + createBrushInstance(id, canvas, options = {}) { + const brushInfo = this.getBrush(id); + if (!brushInfo) { + console.error(`笔刷 ${id} 不存在`); + return null; + } + + try { + // 创建笔刷实例 + return new brushInfo.class(canvas, { + ...options, + id: brushInfo.id, + ...brushInfo.metadata, + }); + } catch (error) { + console.error(`创建笔刷 ${id} 失败:`, error); + return null; + } + } + + /** + * 添加事件监听器 + * @param {String} event 事件名称 ('register'|'unregister') + * @param {Function} callback 回调函数 + */ + addEventListener(event, callback) { + if (this.listeners[event]) { + this.listeners[event].push(callback); + } + } + + /** + * 移除事件监听器 + * @param {String} event 事件名称 + * @param {Function} callback 回调函数 + */ + removeEventListener(event, callback) { + if (this.listeners[event]) { + const index = this.listeners[event].indexOf(callback); + if (index !== -1) { + this.listeners[event].splice(index, 1); + } + } + } + + /** + * 触发事件 + * @param {String} event 事件名称 + * @param {*} data 事件数据 + * @private + */ + _triggerEvent(event, data) { + if (this.listeners[event]) { + this.listeners[event].forEach((callback) => { + try { + callback(data); + } catch (error) { + console.error(`执行 ${event} 事件监听器出错:`, error); + } + }); + } + } +} + +// 导出单例实例 +export const brushRegistry = new BrushRegistry(); + +// 默认导出单例 +export default brushRegistry; diff --git a/src/component/Canvas/CanvasEditor/managers/brushes/TexturePresetManager.js b/src/component/Canvas/CanvasEditor/managers/brushes/TexturePresetManager.js new file mode 100644 index 00000000..afaaca21 --- /dev/null +++ b/src/component/Canvas/CanvasEditor/managers/brushes/TexturePresetManager.js @@ -0,0 +1,586 @@ +/** + * 材质预设管理器 + * 负责管理所有材质预设,包括内置预设和用户自定义预设 + */ +export class TexturePresetManager { + constructor() { + // 内置材质预设 + this.builtInTextures = []; + + // 用户自定义材质 + this.customTextures = []; + + // 材质分类 + this.categories = new Map(); + + // 材质缓存 + this.textureCache = new Map(); + + // 事件监听器 + this.listeners = { + textureAdded: [], + textureRemoved: [], + textureUpdated: [], + }; + + // 初始化内置材质 + this._initBuiltInTextures(); + } + + /** + * 初始化内置材质预设 + * @private + */ + _initBuiltInTextures() { + // 基于项目中的texture文件夹内容创建预设 + const textureList = [ + // 基础纹理 + { + id: "texture0", + name: "纸质纹理", + category: "基础纹理", + path: "/src/assets/texture/texture0.webp", + }, + { + id: "texture1", + name: "粗糙表面", + category: "基础纹理", + path: "/src/assets/texture/texture1.webp", + }, + { + id: "texture2", + name: "细腻纹理", + category: "基础纹理", + path: "/src/assets/texture/texture2.webp", + }, + { + id: "texture3", + name: "颗粒质感", + category: "基础纹理", + path: "/src/assets/texture/texture3.webp", + }, + { + id: "texture4", + name: "布料纹理", + category: "基础纹理", + path: "/src/assets/texture/texture4.webp", + }, + { + id: "texture5", + name: "木质纹理", + category: "自然纹理", + path: "/src/assets/texture/texture5.webp", + }, + { + id: "texture6", + name: "石材纹理", + category: "自然纹理", + path: "/src/assets/texture/texture6.webp", + }, + { + id: "texture7", + name: "金属质感", + category: "金属纹理", + path: "/src/assets/texture/texture7.webp", + }, + { + id: "texture8", + name: "皮革纹理", + category: "自然纹理", + path: "/src/assets/texture/texture8.webp", + }, + { + id: "texture9", + name: "水彩纸质", + category: "艺术纹理", + path: "/src/assets/texture/texture9.webp", + }, + { + id: "texture10", + name: "画布纹理", + category: "艺术纹理", + path: "/src/assets/texture/texture10.webp", + }, + { + id: "texture11", + name: "沙砾质感", + category: "自然纹理", + path: "/src/assets/texture/texture11.webp", + }, + { + id: "texture12", + name: "水波纹理", + category: "自然纹理", + path: "/src/assets/texture/texture12.webp", + }, + { + id: "texture13", + name: "云朵纹理", + category: "自然纹理", + path: "/src/assets/texture/texture13.webp", + }, + { + id: "texture14", + name: "火焰纹理", + category: "特效纹理", + path: "/src/assets/texture/texture14.webp", + }, + { + id: "texture15", + name: "烟雾效果", + category: "特效纹理", + path: "/src/assets/texture/texture15.webp", + }, + { + id: "texture16", + name: "星空纹理", + category: "特效纹理", + path: "/src/assets/texture/texture16.webp", + }, + { + id: "texture17", + name: "大理石纹", + category: "石材纹理", + path: "/src/assets/texture/texture17.webp", + }, + { + id: "texture18", + name: "花岗岩纹", + category: "石材纹理", + path: "/src/assets/texture/texture18.webp", + }, + { + id: "texture19", + name: "竹纹理", + category: "自然纹理", + path: "/src/assets/texture/texture19.webp", + }, + { + id: "texture20", + name: "抽象图案", + category: "艺术纹理", + path: "/src/assets/texture/texture20.webp", + }, + ]; + + // 添加内置材质 + textureList.forEach((texture) => { + this.builtInTextures.push({ + id: texture.id, + name: texture.name, + category: texture.category, + path: texture.path, + type: "builtin", + preview: texture.path, // 使用原图作为预览 + description: `内置${texture.category} - ${texture.name}`, + tags: [texture.category.replace("纹理", ""), "内置"], + created: new Date().toISOString(), + // 默认属性 + defaultSettings: { + scale: 1, + opacity: 1, + repeat: "repeat", + angle: 0, + }, + }); + + // 添加到分类 + if (!this.categories.has(texture.category)) { + this.categories.set(texture.category, []); + } + this.categories.get(texture.category).push(texture.id); + }); + } + + /** + * 获取所有材质(内置 + 自定义) + * @returns {Array} 材质数组 + */ + getAllTextures() { + return [...this.builtInTextures, ...this.customTextures]; + } + + /** + * 根据ID获取材质 + * @param {String} textureId 材质ID + * @returns {Object|null} 材质对象 + */ + getTextureById(textureId) { + return ( + this.getAllTextures().find((texture) => texture.id === textureId) || null + ); + } + + /** + * 根据分类获取材质 + * @param {String} category 分类名称 + * @returns {Array} 材质数组 + */ + getTexturesByCategory(category) { + return this.getAllTextures().filter( + (texture) => texture.category === category + ); + } + + /** + * 获取所有分类 + * @returns {Array} 分类名称数组 + */ + getCategories() { + const categories = new Set(); + this.getAllTextures().forEach((texture) => { + categories.add(texture.category); + }); + return Array.from(categories); + } + + /** + * 添加自定义材质 + * @param {Object} textureData 材质数据 + * @returns {String} 材质ID + */ + addCustomTexture(textureData) { + const textureId = + textureData.id || + `custom_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; + + const texture = { + id: textureId, + name: textureData.name || "自定义材质", + category: textureData.category || "自定义材质", + path: textureData.path || textureData.dataUrl, + type: "custom", + preview: textureData.preview || textureData.path || textureData.dataUrl, + description: textureData.description || "用户自定义材质", + tags: textureData.tags || ["自定义"], + created: new Date().toISOString(), + defaultSettings: { + scale: textureData.scale || 1, + opacity: textureData.opacity || 1, + repeat: textureData.repeat || "repeat", + angle: textureData.angle || 0, + ...textureData.defaultSettings, + }, + // 保存原始文件信息 + file: textureData.file || null, + dataUrl: textureData.dataUrl || null, + }; + + this.customTextures.push(texture); + + // 添加到分类 + if (!this.categories.has(texture.category)) { + this.categories.set(texture.category, []); + } + this.categories.get(texture.category).push(textureId); + + // 触发事件 + this._triggerEvent("textureAdded", texture); + + return textureId; + } + + /** + * 删除自定义材质 + * @param {String} textureId 材质ID + * @returns {Boolean} 是否删除成功 + */ + removeCustomTexture(textureId) { + const index = this.customTextures.findIndex( + (texture) => texture.id === textureId + ); + if (index === -1) { + return false; + } + + const texture = this.customTextures[index]; + + // 只能删除自定义材质 + if (texture.type !== "custom") { + console.warn("不能删除内置材质"); + return false; + } + + this.customTextures.splice(index, 1); + + // 从分类中移除 + if (this.categories.has(texture.category)) { + const categoryTextures = this.categories.get(texture.category); + const categoryIndex = categoryTextures.indexOf(textureId); + if (categoryIndex !== -1) { + categoryTextures.splice(categoryIndex, 1); + } + + // 如果分类为空且不是内置分类,删除分类 + if (categoryTextures.length === 0 && texture.category === "自定义材质") { + this.categories.delete(texture.category); + } + } + + // 清除缓存 + this.textureCache.delete(textureId); + + // 触发事件 + this._triggerEvent("textureRemoved", texture); + + return true; + } + + /** + * 更新材质信息 + * @param {String} textureId 材质ID + * @param {Object} updates 更新数据 + * @returns {Boolean} 是否更新成功 + */ + updateTexture(textureId, updates) { + const texture = this.getTextureById(textureId); + if (!texture || texture.type === "builtin") { + return false; + } + + // 更新材质属性 + Object.assign(texture, updates); + + // 触发事件 + this._triggerEvent("textureUpdated", texture); + + return true; + } + + /** + * 获取材质预览URL + * @param {Object} texture 材质对象 + * @returns {String} 预览URL + */ + getTexturePreviewUrl(texture) { + if (!texture) return null; + + // 如果有预览图,使用预览图 + if (texture.preview) { + return texture.preview; + } + + // 否则使用原图 + return texture.path || texture.dataUrl; + } + + /** + * 加载材质图像 + * @param {String} textureId 材质ID + * @returns {Promise} 图像对象 + */ + loadTextureImage(textureId) { + return new Promise((resolve, reject) => { + // 检查缓存 + if (this.textureCache.has(textureId)) { + resolve(this.textureCache.get(textureId)); + return; + } + + const texture = this.getTextureById(textureId); + if (!texture) { + reject(new Error(`材质 ${textureId} 不存在`)); + return; + } + + const img = new Image(); + img.crossOrigin = "anonymous"; + + img.onload = () => { + // 缓存图像 + this.textureCache.set(textureId, img); + resolve(img); + }; + + img.onerror = () => { + reject(new Error(`材质 ${textureId} 加载失败`)); + }; + + img.src = texture.path || texture.dataUrl; + }); + } + + /** + * 搜索材质 + * @param {String} query 搜索关键词 + * @returns {Array} 匹配的材质数组 + */ + searchTextures(query) { + if (!query) return this.getAllTextures(); + + const searchTerm = query.toLowerCase(); + return this.getAllTextures().filter((texture) => { + return ( + texture.name.toLowerCase().includes(searchTerm) || + texture.category.toLowerCase().includes(searchTerm) || + texture.description.toLowerCase().includes(searchTerm) || + texture.tags.some((tag) => tag.toLowerCase().includes(searchTerm)) + ); + }); + } + + /** + * 保存自定义材质到本地存储 + */ + saveCustomTexturesToStorage() { + try { + const customTexturesData = this.customTextures.map((texture) => ({ + ...texture, + // 不保存file对象到localStorage + file: null, + })); + + localStorage.setItem( + "canvasEditor_customTextures", + JSON.stringify(customTexturesData) + ); + } catch (error) { + console.error("保存自定义材质失败:", error); + } + } + + /** + * 从本地存储加载自定义材质 + */ + loadCustomTexturesFromStorage() { + try { + const stored = localStorage.getItem("canvasEditor_customTextures"); + if (stored) { + const customTexturesData = JSON.parse(stored); + this.customTextures = customTexturesData; + + // 重建分类索引 + this.customTextures.forEach((texture) => { + if (!this.categories.has(texture.category)) { + this.categories.set(texture.category, []); + } + if (!this.categories.get(texture.category).includes(texture.id)) { + this.categories.get(texture.category).push(texture.id); + } + }); + } + } catch (error) { + console.error("加载自定义材质失败:", error); + this.customTextures = []; + } + } + + /** + * 创建材质预设 + * @param {String} name 预设名称 + * @param {Object} settings 材质设置 + * @returns {String} 预设ID + */ + createTexturePreset(name, settings) { + const presetId = `preset_${Date.now()}_${Math.random() + .toString(36) + .substr(2, 9)}`; + + const preset = { + id: presetId, + name: name, + type: "preset", + category: "材质预设", + created: new Date().toISOString(), + settings: { + textureId: settings.textureId, + scale: settings.scale || 1, + opacity: settings.opacity || 1, + repeat: settings.repeat || "repeat", + angle: settings.angle || 0, + brushSize: settings.brushSize || 5, + brushOpacity: settings.brushOpacity || 1, + brushColor: settings.brushColor || "#000000", + }, + }; + + this.customTextures.push(preset); + this._triggerEvent("textureAdded", preset); + + return presetId; + } + + /** + * 应用材质预设 + * @param {String} presetId 预设ID + * @returns {Object|null} 预设设置 + */ + applyTexturePreset(presetId) { + const preset = this.getTextureById(presetId); + if (!preset || preset.type !== "preset") { + return null; + } + + return preset.settings; + } + + /** + * 添加事件监听器 + * @param {String} event 事件名称 + * @param {Function} callback 回调函数 + */ + addEventListener(event, callback) { + if (this.listeners[event]) { + this.listeners[event].push(callback); + } + } + + /** + * 移除事件监听器 + * @param {String} event 事件名称 + * @param {Function} callback 回调函数 + */ + removeEventListener(event, callback) { + if (this.listeners[event]) { + const index = this.listeners[event].indexOf(callback); + if (index !== -1) { + this.listeners[event].splice(index, 1); + } + } + } + + /** + * 触发事件 + * @param {String} event 事件名称 + * @param {*} data 事件数据 + * @private + */ + _triggerEvent(event, data) { + if (this.listeners[event]) { + this.listeners[event].forEach((callback) => { + try { + callback(data); + } catch (error) { + console.error(`执行 ${event} 事件监听器出错:`, error); + } + }); + } + } + + /** + * 清除所有缓存 + */ + clearCache() { + this.textureCache.clear(); + } + + /** + * 获取统计信息 + * @returns {Object} 统计信息 + */ + getStats() { + return { + builtInCount: this.builtInTextures.length, + customCount: this.customTextures.length, + totalCount: this.getAllTextures().length, + categoriesCount: this.getCategories().length, + cacheSize: this.textureCache.size, + }; + } +} + +// 创建单例实例 +const texturePresetManager = new TexturePresetManager(); + +// 导出单例 +export default texturePresetManager; diff --git a/src/component/Canvas/CanvasEditor/managers/brushes/brushManager.js b/src/component/Canvas/CanvasEditor/managers/brushes/brushManager.js new file mode 100644 index 00000000..c369089f --- /dev/null +++ b/src/component/Canvas/CanvasEditor/managers/brushes/brushManager.js @@ -0,0 +1,720 @@ +//import { fabric } from "fabric-with-all"; +import { BrushStore } from "../../store/BrushStore"; +import { brushRegistry } from "./BrushRegistry"; + +// 导入基础笔刷类型 +import { PencilBrush } from "./types/PencilBrush"; +import { TextureBrush } from "./types/TextureBrush"; + +// 导入集成的笔刷类型 +import { CrayonBrush } from "./types/CrayonBrush"; +import { FurBrush } from "./types/FurBrush"; +import { InkBrush } from "./types/InkBrush"; +import { LongfurBrush } from "./types/LongfurBrush"; +import { WritingBrush } from "./types/WritingBrush"; +import { MarkerBrush } from "./types/MarkerBrush"; +import { CustomPenBrush } from "./types/CustomPenBrush"; +import { RibbonBrush } from "./types/RibbonBrush"; +import { ShadedBrush } from "./types/ShadedBrush"; +import { SketchyBrush } from "./types/SketchyBrush"; +import { SpraypaintBrush } from "./types/SpraypaintBrush"; + +/** + * 笔刷管理器 + * 负责管理和切换不同的笔刷类型 + */ +export class BrushManager { + /** + * 构造函数 + * @param {Object} options 配置选项 + * @param {Object} options.canvas fabric.js画布实例 + * @param {Object} options.brushStore 笔刷数据存储(可选) + * @param {Object} options.layerManager 图层管理器实例(可选) + */ + constructor(options = {}) { + this.canvas = options.canvas; + this.brushStore = options.brushStore || BrushStore; + this.layerManager = options.layerManager; // 添加图层管理器引用 + + // 当前活动笔刷 + this.activeBrush = null; + this.activeBrushId = null; + + // 初始化笔刷注册 + this._registerDefaultBrushes(); + } + + /** + * 注册默认笔刷 + * @private + */ + _registerDefaultBrushes() { + // 注册铅笔笔刷 + brushRegistry.register("pencil", PencilBrush, { + name: "铅笔", + description: "基础铅笔工具,适合精细线条绘制", + category: "基础笔刷", + }); + + // 注册材质笔刷 + brushRegistry.register("texture", TextureBrush); + + // 注册集成的笔刷类型 + brushRegistry.register("crayon", CrayonBrush); + brushRegistry.register("fur", FurBrush); + brushRegistry.register("ink", InkBrush); + brushRegistry.register("longfur", LongfurBrush); + brushRegistry.register("writing", WritingBrush); + brushRegistry.register("marker", MarkerBrush); + brushRegistry.register("pen", CustomPenBrush); + brushRegistry.register("ribbon", RibbonBrush); + brushRegistry.register("shaded", ShadedBrush); + brushRegistry.register("sketchy", SketchyBrush); + brushRegistry.register("spraypaint", SpraypaintBrush); + + // 注册喷枪笔刷 + brushRegistry.register( + "spray", + class SprayBrush extends PencilBrush { + constructor(canvas, options = {}) { + super(canvas, { + id: "spray", + name: "喷枪", + description: "模拟喷枪效果,创建散点效果", + category: "基础笔刷", + ...options, + }); + } + + create() { + this.brush = new fabric.SprayBrush(this.canvas); + this.configure(this.brush, this.options); + return this.brush; + } + + configure(brush, options = {}) { + super.configure(brush, options); + + if (options.density !== undefined) { + brush.density = options.density; + } + + if (options.randomOpacity !== undefined) { + brush.randomOpacity = options.randomOpacity; + } + + if (options.dotWidth !== undefined) { + brush.dotWidth = options.dotWidth; + } + } + } + ); + // 注册橡皮擦笔刷 + brushRegistry.register( + "eraser", + class EraserBrush extends PencilBrush { + constructor(canvas, options = {}) { + super(canvas, { + id: "eraser", + name: "橡皮擦", + description: "擦除已绘制的内容", + category: "工具", + ...options, + }); + } + + create() { + // 直接使用 fabric-with-erasing 库提供的 EraserBrush + this.brush = new fabric.EraserBrush(this.canvas); + this.configure(this.brush, this.options); + + // 配置橡皮擦特有属性 + this.brush.inverted = this.options.inverted || false; // 是否反向擦除(恢复擦除) + + return this.brush; + } + + configure(brush, options = {}) { + super.configure(brush, options); + + // 橡皮擦特有配置 + if (options.inverted !== undefined) { + brush.inverted = options.inverted; + } + } + + /** + * 设置反向擦除模式 + * @param {Boolean} inverted 是否启用反向擦除(撤销擦除效果) + */ + setInverted(inverted) { + if (this.brush) { + this.brush.inverted = inverted; + } + } + + /** + * 获取橡皮擦配置属性 + * @returns {Array} 可配置属性数组 + */ + getConfigurableProperties() { + return [ + ...super.getConfigurableProperties(), + { + id: "inverted", + name: "反向擦除", + type: "boolean", + description: "启用时可以恢复已擦除的内容", + defaultValue: false, + min: null, + max: null, + }, + ]; + } + + /** + * 更新橡皮擦属性 + * @param {String} propId 属性ID + * @param {any} value 属性值 + */ + updateProperty(propId, value) { + if (propId === "inverted") { + this.setInverted(value); + } else { + super.updateProperty(propId, value); + } + } + } + ); + + // 注册水彩笔刷 + brushRegistry.register( + "watercolor", + class WatercolorBrush extends PencilBrush { + constructor(canvas, options = {}) { + super(canvas, { + id: "watercolor", + name: "水彩", + description: "模拟水彩效果,带有流动感和透明感", + category: "特效笔刷", + ...options, + }); + } + + create() { + // 创建一个自定义的PencilBrush来模拟水彩效果 + this.brush = new fabric.PencilBrush(this.canvas); + this.configure(this.brush, this.options); + + // 水彩效果特有的属性 + this.brush.globalCompositeOperation = "multiply"; + this.brush.shadow = new fabric.Shadow({ + color: this.options.color || "#000", + blur: 5, + offsetX: 0, + offsetY: 0, + }); + + return this.brush; + } + + configure(brush, options = {}) { + super.configure(brush, options); + + // 水彩笔刷特有的配置 + brush.opacity = Math.min(0.5, options.opacity || 0.3); // 默认透明度30% + } + } + ); + + // 注册粉笔笔刷 + brushRegistry.register( + "chalk", + class ChalkBrush extends PencilBrush { + constructor(canvas, options = {}) { + super(canvas, { + id: "chalk", + name: "粉笔", + description: "模拟粉笔效果,有颗粒感和不连续性", + category: "特效笔刷", + ...options, + }); + } + + create() { + this.brush = new fabric.PencilBrush(this.canvas); + this.configure(this.brush, this.options); + + // 自定义绘画方法来模拟粉笔效果 + const originalOnMouseMove = this.brush.onMouseMove; + this.brush.onMouseMove = function (pointer, options) { + // 随机调整坐标位置,增加粉笔质感 + const jitter = 2; + pointer.x += (Math.random() - 0.5) * jitter; + pointer.y += (Math.random() - 0.5) * jitter; + + // 调用原始方法 + originalOnMouseMove.call(this, pointer, options); + }; + + return this.brush; + } + + configure(brush, options = {}) { + super.configure(brush, options); + + // 粉笔特有的设置 + brush.strokeDashArray = [5, 5]; // 虚线效果 + } + } + ); + } + + /** + * 获取所有可用笔刷类型 + * @returns {Array} 笔刷类型数组,包含id、name和description + */ + getBrushTypes() { + // 从注册表获取所有笔刷信息 + const brushes = brushRegistry.getAllBrushes(); + + // 将笔刷信息转换为期望的格式 + return brushes.map((brushInfo) => ({ + id: brushInfo.id, + name: brushInfo.metadata.name || brushInfo.id, + description: brushInfo.metadata.description || "", + category: brushInfo.metadata.category || "默认", + icon: brushInfo.metadata.icon || null, + })); + } + + /** + * 初始化笔刷列表并更新BrushStore + */ + initializeBrushes() { + // 获取所有笔刷 + const allBrushes = this.getBrushTypes(); + + // 更新BrushStore中的可用笔刷列表 + this.brushStore.setAvailableBrushes(allBrushes); + + // 设置默认笔刷 + if (!this.activeBrushId && allBrushes.length > 0) { + this.setBrushType(allBrushes[0].id); + } + } + + /** + * 设置笔刷类型 + * @param {String} brushId 笔刷ID + * @returns {Object|null} 设置的笔刷实例 + */ + setBrushType(brushId) { + // 如果相同笔刷,不做处理 + if (this.activeBrushId === brushId) { + return this.activeBrush; + } + + // 销毁当前笔刷 + if (this.activeBrush) { + // 调用生命周期方法 + if (this.activeBrush.onDeselected) { + this.activeBrush.onDeselected(); + } + this.activeBrush.destroy(); + } + // 创建新笔刷实例 + try { + const brushInstance = brushRegistry.createBrushInstance( + brushId, + this.canvas, + { + color: brushId === "eraser" ? this.brushStore.state.color : undefined, + width: this.brushStore.state.size, + opacity: this.brushStore.state.opacity, + + // 材质笔刷特有配置 + textureEnabled: this.brushStore.state.textureEnabled, + texturePath: this.brushStore.state.texturePath, + textureScale: this.brushStore.state.textureScale, + } + ); + + if (brushInstance) { + // 创建笔刷 + const fabricBrush = brushInstance.create(); + + // 更新画布的当前笔刷 + if (fabricBrush) { + this.canvas.freeDrawingBrush = fabricBrush; + } + + // 更新当前笔刷引用 + this.activeBrush = brushInstance; + this.activeBrushId = brushId; + + // 调用生命周期方法 + if (this.activeBrush.onSelected) { + this.activeBrush.onSelected(); + } + + // 更新Store的笔刷类型 + this.brushStore.setBrushType(brushId); + + // 更新Store的当前笔刷实例,用于动态属性系统 + this.brushStore.setCurrentBrushInstance(brushInstance); + + return brushInstance; + } + } catch (error) { + console.error(`创建笔刷 ${brushId} 失败:`, error); + } + + return null; + } + + /** + * 设置笔刷颜色 + * @param {String} color 十六进制颜色值 + */ + setBrushColor(color) { + if (!this.canvas.freeDrawingBrush) return; + + // 更新笔刷颜色 + this.canvas.freeDrawingBrush.color = color; + + // 更新活动笔刷 + if (this.activeBrush) { + this.activeBrush.configure(this.canvas.freeDrawingBrush, { color }); + } + + // 更新Store + this.brushStore.setBrushColor(color); + } + + /** + * 设置笔刷大小 + * @param {Number} size 笔刷大小 + */ + setBrushSize(size) { + if (!this.canvas.freeDrawingBrush) return; + + // 限制大小范围 + const brushSize = Math.max(0.1, Math.min(100, size)); + + // 更新笔刷大小 + this.canvas.freeDrawingBrush.width = brushSize; + + // 更新活动笔刷 + if (this.activeBrush) { + this.activeBrush.configure(this.canvas.freeDrawingBrush, { + width: brushSize, + }); + } + + // 更新Store + this.brushStore.setBrushSize(brushSize); + + return brushSize; + } + + /** + * 增加笔刷大小 + * @param {Number} amount 增加量 + * @returns {Number} 新的笔刷大小 + */ + increaseBrushSize(amount = 1) { + const currentSize = this.brushStore.state.size; + return this.setBrushSize(currentSize + amount); + } + + /** + * 减少笔刷大小 + * @param {Number} amount 减少量 + * @returns {Number} 新的笔刷大小 + */ + decreaseBrushSize(amount = 1) { + const currentSize = this.brushStore.state.size; + return this.setBrushSize(currentSize - amount); + } + + /** + * 增加笔刷透明度 + * @param {Number} amount 增加量 + * @returns {Number} 新的笔刷大小 + */ + increaseBrushOpacity(amount = 0.01) { + const currentSize = this.brushStore.state.opacity; + return this.setBrushOpacity(currentSize + amount); + } + + /** + * 减少笔刷大小 + * @param {Number} amount 减少量 + * @returns {Number} 新的笔刷大小 + */ + decreaseBrushOpacity(amount = 0.01) { + const currentSize = this.brushStore.state.opacity; + return this.setBrushOpacity(currentSize - amount); + } + + /** + * 设置笔刷透明度 + * @param {Number} opacity 透明度 (0-1) + */ + setBrushOpacity(opacity) { + if (!this.canvas.freeDrawingBrush) return; + + // 限制透明度范围 + const brushOpacity = Math.max(0.05, Math.min(1, opacity)); + + // 更新笔刷透明度 + this.canvas.freeDrawingBrush.opacity = brushOpacity; + + // 更新活动笔刷 + if (this.activeBrush) { + this.activeBrush.configure(this.canvas.freeDrawingBrush, { + opacity: brushOpacity, + }); + } + + // 更新Store + this.brushStore.setBrushOpacity(brushOpacity); + + return brushOpacity; + } + + /** + * 设置材质缩放 + * @param {Number} scale 缩放比例 + */ + setTextureScale(scale) { + // 限制缩放范围 + const textureScale = Math.max(0.1, Math.min(10, scale)); + + // 更新活动笔刷 + if (this.activeBrush && this.activeBrush.setTextureScale) { + this.activeBrush.setTextureScale(textureScale); + } + + // 更新Store + this.brushStore.setTextureScale(textureScale); + + return textureScale; + } + + /** + * 增加材质缩放 + * @param {Number} amount 增加量 + */ + increaseTextureScale(amount = 0.1) { + const currentScale = this.brushStore.state.textureScale; + return this.setTextureScale(currentScale + amount); + } + + /** + * 减少材质缩放 + * @param {Number} amount 减少量 + */ + decreaseTextureScale(amount = 0.1) { + const currentScale = this.brushStore.state.textureScale; + return this.setTextureScale(currentScale - amount); + } + + /** + * 设置材质路径 + * @param {String} path 材质图片路径 + */ + setTexturePath(path) { + // 更新活动笔刷 + if (this.activeBrush && this.activeBrush.setTexturePath) { + this.activeBrush.setTexturePath(path); + } + + // 更新Store + this.brushStore.setTexturePath(path); + + return path; + } + + /** + * 启用/禁用材质 + * @param {Boolean} enabled 是否启用 + */ + setTextureEnabled(enabled) { + // 更新Store + this.brushStore.setTextureEnabled(enabled); + + // 如果启用材质,且当前不是材质笔刷,需要切换 + if (enabled && this.activeBrushId !== "texture") { + this.setBrushType("texture"); + } + + return enabled; + } + + /** + * 注册新笔刷 + * @param {String} id 笔刷ID + * @param {Class} brushClass 笔刷类 + * @param {Object} metadata 笔刷元数据 + * @returns {Boolean} 是否注册成功 + */ + registerBrush(id, brushClass, metadata = {}) { + const success = brushRegistry.register(id, brushClass, metadata); + + if (success) { + // 更新可用笔刷列表 + this.initializeBrushes(); + } + + return success; + } + + /** + * 获取当前笔刷类型 + * @returns {String} 当前笔刷类型ID + */ + getCurrentBrushType() { + return this.activeBrushId; + } + + /** + * 获取当前笔刷大小 + * @returns {Number} 当前笔刷大小 + */ + getBrushSize() { + return this.brushStore.state.size; + } + + /** + * 获取当前笔刷颜色 + * @returns {String} 当前笔刷颜色 + */ + getBrushColor() { + return this.brushStore.state.color; + } + + /** + * 获取当前笔刷透明度 + * @returns {Number} 当前笔刷透明度 + */ + getBrushOpacity() { + return this.brushStore.state.opacity; + } + + /** + * 获取材质缩放 + * @returns {Number} 材质缩放比例 + */ + getTextureScale() { + return this.brushStore.state.textureScale; + } + + /** + * 创建材质笔刷 + * 这个方法保留用于向下兼容 + * @deprecated 请使用setBrushType('texture')代替 + * @returns {Object} 材质笔刷实例 + */ + createTextureBrush() { + console.warn('createTextureBrush方法已废弃,请使用setBrushType("texture")'); + return this.setBrushType("texture"); + } + + /** + * 更新笔刷 + * 根据当前设置应用笔刷属性到画布 + */ + updateBrush() { + if (!this.canvas) return; + + // 如果有活动的笔刷实例,重新配置它 + if (this.activeBrush && this.canvas.freeDrawingBrush) { + this.activeBrush.configure(this.canvas.freeDrawingBrush, { + color: this.brushStore.state.color, + width: this.brushStore.state.size, + opacity: this.brushStore.state.opacity, + }); + } else { + // 如果没有活动笔刷,创建一个默认的 + this.setBrushType(this.activeBrushId || "pencil"); + } + + // 更新画布状态 + this?.canvas?.renderAll?.(); + + return this.canvas.freeDrawingBrush; + } + + /** + * 创建橡皮擦 + * @returns {Object} 橡皮擦笔刷 + */ + createEraser() { + return this.setBrushType("eraser"); + } + + /** + * 创建吸色工具 + * @param {Function} callback 选择颜色后的回调函数 + */ + createEyedropper(callback) { + // 保存当前状态 + const previousBrushId = this.activeBrushId; + + // 一次性事件处理程序 + const handleMouseDown = (event) => { + const pointer = this.canvas.getPointer(event.e); + const ctx = this.canvas.getContext(); + + // 获取点击位置的像素 + const imageData = ctx.getImageData(pointer.x, pointer.y, 1, 1).data; + + // 将RGB转换为十六进制颜色 + const color = `#${( + (1 << 24) + + (imageData[0] << 16) + + (imageData[1] << 8) + + imageData[2] + ) + .toString(16) + .slice(1)}`; + + // 调用回调函数 + if (typeof callback === "function") { + callback(color); + } + + // 恢复之前的笔刷 + this.setBrushType(previousBrushId); + + // 移除事件监听器 + this.canvas.off("mouse:down", handleMouseDown); + }; + + // 添加事件监听器 + this.canvas.on("mouse:down", handleMouseDown); + + // 设置吸色光标 + this.canvas.defaultCursor = "crosshair"; + + console.log("吸色工具已激活,点击画布选择颜色"); + } + + /** + * 销毁资源 + */ + dispose() { + // 销毁当前笔刷 + if (this.activeBrush) { + this.activeBrush.destroy(); + this.activeBrush = null; + } + + this.canvas = null; + } +} + +// 导出单例 +export default BrushManager; diff --git a/src/component/Canvas/CanvasEditor/managers/brushes/data/sprayBrushData.js b/src/component/Canvas/CanvasEditor/managers/brushes/data/sprayBrushData.js new file mode 100644 index 00000000..97420f4b --- /dev/null +++ b/src/component/Canvas/CanvasEditor/managers/brushes/data/sprayBrushData.js @@ -0,0 +1,4 @@ +export const sprayBrushDataUrl = + "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAT0AAAE/CAMAAAADjeSkAAAyGmlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4KPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS4zLWMwMTEgNjYuMTQ1NjYxLCAyMDEyLzAyLzA2LTE0OjU2OjI3ICAgICAgICAiPgogICA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPgogICAgICA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIgogICAgICAgICAgICB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iPgogICAgICAgICA8eG1wOkNyZWF0b3JUb29sPkFkb2JlIEZpcmV3b3JrcyBDUzYgKE1hY2ludG9zaCk8L3htcDpDcmVhdG9yVG9vbD4KICAgICAgICAgPHhtcDpDcmVhdGVEYXRlPjIwMjEtMDUtMjdUMTQ6NTg6NTVaPC94bXA6Q3JlYXRlRGF0ZT4KICAgICAgICAgPHhtcDpNb2RpZnlEYXRlPjIwMjEtMDUtMjdUMTQ6NTk6MDJaPC94bXA6TW9kaWZ5RGF0ZT4KICAgICAgPC9yZGY6RGVzY3JpcHRpb24+CiAgICAgIDxyZGY6RGVzY3JpcHRpb24gcmRmOmFib3V0PSIiCiAgICAgICAgICAgIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyI+CiAgICAgICAgIDxkYzpmb3JtYXQ+aW1hZ2UvcG5nPC9kYzpmb3JtYXQ+CiAgICAgIDwvcmRmOkRlc2NyaXB0aW9uPgogICA8L3JkZjpSREY+CjwveDp4bXBtZXRhPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgIAo8P3hwYWNrZXQgZW5kPSJ3Ij8+tc546gAAABx0RVh0U29mdHdhcmUAQWRvYmUgRmlyZXdvcmtzIENTNui8sowAAAAJcEhZcwAACxIAAAsSAdLdfvwAAADnUExURQAAAAAAAAAAAAAAAAAAAAAAAAAAAEdwTAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHxfYHoAAABNdFJOUwEGB0lKBAUAAgMICUgyCjULLw0MMQ8wDjQRMzgrOy4oEEIsEy02FBIVKh45JSkYNzoXJxYiPyYaHSAfGSEkPCMbRUNBHEtER0A+Rj1M2XzftQAAIABJREFUeNrkWtuO4zoOpARQpiTAEGwEQb8FGKC7sQ8HONiHfTj//19bJOVbkum59KR7ZjeYyc2yO64Uq4p0aPi0G233fqf3lCKt25Yl9KNHpg89hc8F8PiW3G7/3A/526L38x+bOr+EPvOE6DPO/Jufgu7vs72dZSlq+r/gHn3tbxLd8GdjFN090FrQIm+didCfit4tIF9fRk4u2l5eMW3Fge6X7/1DDpn+N7j3xnmuCww191v6Hne55fM111bHpj++cu/8zdz59oaz0h293Ep3t5luXq0e/kAK0mMPRm95By1U80dZ4bmL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"; + +export default sprayBrushDataUrl; diff --git a/src/component/Canvas/CanvasEditor/managers/brushes/types/CrayonBrush.js b/src/component/Canvas/CanvasEditor/managers/brushes/types/CrayonBrush.js new file mode 100644 index 00000000..892c6414 --- /dev/null +++ b/src/component/Canvas/CanvasEditor/managers/brushes/types/CrayonBrush.js @@ -0,0 +1,244 @@ +import { BaseBrush } from "../BaseBrush"; + +/** + * 蜡笔笔刷 + * 模拟蜡笔效果,具有颗粒感和纹理 + */ +export class CrayonBrush extends BaseBrush { + /** + * 构造函数 + * @param {Object} canvas fabric画布实例 + * @param {Object} options 配置选项 + */ + constructor(canvas, options = {}) { + super(canvas, { + id: "crayon", + name: "蜡笔", + description: "模拟蜡笔效果,具有颗粒感和纹理", + category: "特效笔刷", + icon: "crayon", + ...options, + }); + + // 蜡笔笔刷特有属性 + this._baseWidth = options._baseWidth || 15; + this._size = options._size || 0; + this._sep = options._sep || options._sep === 0 ? options._sep : 3; + this._inkAmount = options._inkAmount || 10; + this.randomness = options.randomness || 0.5; // 随机性 + this.texture = options.texture || "default"; // 纹理类型 + } + + /** + * 创建笔刷实例 + * @returns {Object} fabric笔刷实例 + */ + create() { + if (!this.canvas) { + throw new Error("画布实例不存在"); + } + + // 创建fabric原生蜡笔笔刷 + this.brush = new fabric.CrayonBrush(this.canvas); + + // 配置笔刷 + this.configure(this.brush, this.options); + + return this.brush; + } + + /** + * 配置笔刷 + * @param {Object} brush fabric笔刷实例 + * @param {Object} options 配置选项 + */ + configure(brush, options = {}) { + if (!brush) return; + + // 基础属性配置 + if (options.width !== undefined) { + brush.width = options.width; + // 更新笔刷相关属性 + this._baseWidth = options.width / 2; + this._size = options.width / 2 + this._baseWidth; + } + + if (options.color !== undefined) { + brush.color = options.color; + } + + if (options.opacity !== undefined) { + brush.opacity = options.opacity; + } + + // 蜡笔笔刷特有属性 + if (options._baseWidth !== undefined) { + brush._baseWidth = options._baseWidth; + this._baseWidth = options._baseWidth; + this._size = this.width / 2 + this._baseWidth; + } + + if (options._sep !== undefined) { + brush._sep = options._sep; + this._sep = options._sep; + } + + if (options._inkAmount !== undefined) { + brush._inkAmount = options._inkAmount; + this._inkAmount = options._inkAmount; + } + } + + /** + * 设置颗粒分离度 + * @param {Number} sep 分离度值 + */ + setSeparation(sep) { + this._sep = Math.max(0.5, Math.min(10, sep)); + + if (this.brush) { + this.brush._sep = this._sep; + } + + return this._sep; + } + + /** + * 设置墨量 + * @param {Number} amount 墨量值 + */ + setInkAmount(amount) { + this._inkAmount = Math.max(1, Math.min(50, amount)); + + if (this.brush) { + this.brush._inkAmount = this._inkAmount; + } + + return this._inkAmount; + } + + /** + * 设置随机性 + * @param {Number} value 随机性值(0-1) + */ + setRandomness(value) { + this.randomness = Math.max(0, Math.min(1, value)); + return this.randomness; + } + + /** + * 设置纹理类型 + * @param {String} type 纹理类型 + */ + setTexture(type) { + this.texture = type; + // 实际应用可能需要更多的实现逻辑 + return this.texture; + } + + /** + * 获取笔刷可配置属性 + * @returns {Array} 可配置属性描述数组 + * @override + */ + getConfigurableProperties() { + // 获取基础属性 + const baseProperties = super.getConfigurableProperties(); + + // 定义蜡笔笔刷特有属性 + const crayonProperties = [ + { + id: "separation", + name: "颗粒分离度", + type: "slider", + defaultValue: this._sep, + min: 0.5, + max: 10, + step: 0.5, + description: "控制蜡笔颗粒的分离程度", + category: "蜡笔设置", + order: 100, + }, + { + id: "inkAmount", + name: "墨量", + type: "slider", + defaultValue: this._inkAmount, + min: 1, + max: 50, + step: 1, + description: "控制蜡笔的颜料量", + category: "蜡笔设置", + order: 110, + }, + { + id: "randomness", + name: "随机性", + type: "slider", + defaultValue: this.randomness, + min: 0, + max: 1, + step: 0.05, + description: "控制蜡笔纹理的随机程度", + category: "蜡笔设置", + order: 120, + }, + { + id: "texture", + name: "纹理类型", + type: "select", + defaultValue: this.texture, + options: [ + { value: "default", label: "默认" }, + { value: "rough", label: "粗糙" }, + { value: "smooth", label: "平滑" }, + ], + description: "设置蜡笔的纹理类型", + category: "蜡笔设置", + order: 130, + }, + ]; + + // 合并并返回所有属性 + return [...baseProperties, ...crayonProperties]; + } + + /** + * 更新笔刷属性 + * @param {String} propId 属性ID + * @param {any} value 属性值 + * @returns {Boolean} 是否更新成功 + * @override + */ + updateProperty(propId, value) { + // 先检查基类能否处理此属性 + if (super.updateProperty(propId, value)) { + return true; + } + + // 处理蜡笔笔刷特有属性 + if (propId === "separation") { + this.setSeparation(value); + return true; + } else if (propId === "inkAmount") { + this.setInkAmount(value); + return true; + } else if (propId === "randomness") { + this.setRandomness(value); + return true; + } else if (propId === "texture") { + this.setTexture(value); + return true; + } + + return false; + } + + /** + * 获取预览图 + * @returns {String} 预览图URL + */ + getPreview() { + return "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMDAgMTAwIj48cmVjdCB4PSIxMCIgeT0iMTAiIHdpZHRoPSI4MCIgaGVpZ2h0PSI4MCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjMDAwIiBzdHJva2Utd2lkdGg9IjIiLz48cmVjdCB4PSIyMCIgeT0iMjAiIHdpZHRoPSI2MCIgaGVpZ2h0PSI2MCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjMDAwIiBzdHJva2Utd2lkdGg9IjIiLz48cmVjdCB4PSIzMCIgeT0iMzAiIHdpZHRoPSI0MCIgaGVpZ2h0PSI0MCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjMDAwIiBzdHJva2Utd2lkdGg9IjIiLz48L3N2Zz4="; + } +} diff --git a/src/component/Canvas/CanvasEditor/managers/brushes/types/CustomBrushExample.js b/src/component/Canvas/CanvasEditor/managers/brushes/types/CustomBrushExample.js new file mode 100644 index 00000000..4aaf4bcc --- /dev/null +++ b/src/component/Canvas/CanvasEditor/managers/brushes/types/CustomBrushExample.js @@ -0,0 +1,1349 @@ +import { BaseBrush } from "../BaseBrush"; +import { brushRegistry } from "../BrushRegistry"; + +/** + * 例子:霓虹笔刷 + * 创建带有发光效果的霓虹笔刷 + */ +export class NeonBrush extends BaseBrush { + /** + * 构造函数 + * @param {Object} canvas fabric画布实例 + * @param {Object} options 配置选项 + */ + constructor(canvas, options = {}) { + super(canvas, { + id: "neon", + name: "霓虹笔", + description: "带有发光效果的霓虹笔刷", + category: "特效笔刷", + icon: "neon", + ...options, + }); + + // 霓虹笔特有属性 + this.glowIntensity = options.glowIntensity || 10; + this.glowColor = options.glowColor || this.options.color || "#00ffff"; + } + + /** + * 创建笔刷实例 + * @returns {Object} fabric笔刷实例 + */ + create() { + if (!this.canvas) { + throw new Error("画布实例不存在"); + } + + // 使用基础的铅笔笔刷 + this.brush = new fabric.PencilBrush(this.canvas); + + // 配置笔刷 + this.configure(this.brush, this.options); + + return this.brush; + } + + /** + * 配置笔刷 + * @param {Object} brush fabric笔刷实例 + * @param {Object} options 配置选项 + */ + configure(brush, options = {}) { + if (!brush) return; + + // 设置基本属性 + if (options.color) { + brush.color = options.color; + // 更新发光颜色 + this.glowColor = options.glowColor || options.color; + } + + if (options.width !== undefined) { + brush.width = options.width; + } + + if (options.opacity !== undefined) { + brush.opacity = options.opacity; + } + + // 霓虹特有设置 - 发光效果 + const glowSize = options.glowIntensity || this.glowIntensity; + brush.shadow = new fabric.Shadow({ + color: this.glowColor, + blur: glowSize, + offsetX: 0, + offsetY: 0, + }); + + // 设置线条连接方式,使连接处更圆滑 + brush.strokeLineCap = "round"; + brush.strokeLineJoin = "round"; + + // 设置合成模式为"lighter",使重叠部分更亮 + brush.globalCompositeOperation = "lighter"; + } + + /** + * 设置发光强度 + * @param {Number} intensity 发光强度 + */ + setGlowIntensity(intensity) { + this.glowIntensity = Math.max(0, Math.min(30, intensity)); + + // 如果笔刷已创建,更新设置 + if (this.brush && this.brush.shadow) { + this.brush.shadow.blur = this.glowIntensity; + if (this.canvas) { + this.canvas.renderAll(); + } + } + + return this.glowIntensity; + } + + /** + * 设置发光颜色 + * @param {String} color 颜色值 + */ + setGlowColor(color) { + this.glowColor = color; + + // 如果笔刷已创建,更新设置 + if (this.brush && this.brush.shadow) { + this.brush.shadow.color = color; + if (this.canvas) { + this.canvas.renderAll(); + } + } + + return this.glowColor; + } + + /** + * 获取笔刷可配置属性 + * @returns {Array} 可配置属性描述数组 + * @override + */ + getConfigurableProperties() { + // 获取基础属性 + const baseProperties = super.getConfigurableProperties(); + + // 添加霓虹笔特有属性 + const neonProperties = [ + { + id: "glowIntensity", + name: "发光强度", + type: "slider", + defaultValue: 10, + min: 1, + max: 30, + step: 1, + description: "霓虹笔发光效果的强度", + category: "特效", + order: 100, + }, + { + id: "glowColor", + name: "发光颜色", + type: "color", + defaultValue: this.glowColor, + description: "发光效果的颜色", + category: "特效", + order: 110, + }, + ]; + + // 合并并返回所有属性 + return [...baseProperties, ...neonProperties]; + } + + /** + * 更新笔刷属性 + * @param {String} propId 属性ID + * @param {any} value 属性值 + * @returns {Boolean} 是否更新成功 + * @override + */ + updateProperty(propId, value) { + // 先检查基类能否处理此属性 + if (super.updateProperty(propId, value)) { + return true; + } + + // 处理霓虹笔特有属性 + if (propId === "glowIntensity") { + this.setGlowIntensity(value); + return true; + } else if (propId === "glowColor") { + this.setGlowColor(value); + return true; + } + + return false; + } +} + +/** + * 例子:彩虹笔刷 + * 创建随着绘制会变换颜色的彩虹效果笔刷 + */ +export class RainbowBrush extends BaseBrush { + /** + * 构造函数 + * @param {Object} canvas fabric画布实例 + * @param {Object} options 配置选项 + */ + constructor(canvas, options = {}) { + super(canvas, { + id: "rainbow", + name: "彩虹笔", + description: "绘制时会自动变换颜色的彩虹笔", + category: "特效笔刷", + icon: "rainbow", + ...options, + }); + + // 彩虹笔特有属性 + this.colorSpeed = options.colorSpeed || 10; // 颜色变化速度 + this.hue = 0; // 当前色相值 + this.colorUpdateInterval = null; + } + + /** + * 创建笔刷实例 + * @returns {Object} fabric笔刷实例 + */ + create() { + if (!this.canvas) { + throw new Error("画布实例不存在"); + } + + // 使用铅笔笔刷 + this.brush = new fabric.PencilBrush(this.canvas); + + // 配置基本属性 + this.configure(this.brush, this.options); + + // 覆盖onMouseDown方法,开始颜色循环 + const originalMouseDown = this.brush.onMouseDown; + this.brush.onMouseDown = (pointer, options) => { + // 开始颜色循环 + this.startColorCycle(); + + // 调用原始方法 + return originalMouseDown.call(this.brush, pointer, options); + }; + + // 覆盖onMouseUp方法,停止颜色循环 + const originalMouseUp = this.brush.onMouseUp; + this.brush.onMouseUp = (options) => { + // 停止颜色循环 + this.stopColorCycle(); + + // 调用原始方法 + return originalMouseUp.call(this.brush, options); + }; + + return this.brush; + } + + /** + * 配置笔刷 + * @param {Object} brush fabric笔刷实例 + * @param {Object} options 配置选项 + */ + configure(brush, options = {}) { + if (!brush) return; + + // 设置基本属性 + if (options.width !== undefined) { + brush.width = options.width; + } + + if (options.opacity !== undefined) { + brush.opacity = options.opacity; + } + + // 初始颜色 + brush.color = this.getCurrentColor(); + + // 彩虹笔特有设置 + brush.strokeLineCap = "round"; + brush.strokeLineJoin = "round"; + + // 设置笔刷颜色变化速度 + if (options.colorSpeed !== undefined) { + this.colorSpeed = options.colorSpeed; + } + } + + /** + * 开始颜色循环 + */ + startColorCycle() { + // 停止之前的循环 + this.stopColorCycle(); + + // 创建新的循环 + this.colorUpdateInterval = setInterval(() => { + // 更新色相值 + this.hue = (this.hue + this.colorSpeed) % 360; + + // 更新笔刷颜色 + if (this.brush) { + this.brush.color = this.getCurrentColor(); + } + }, 50); // 每50ms更新一次颜色 + } + + /** + * 停止颜色循环 + */ + stopColorCycle() { + if (this.colorUpdateInterval) { + clearInterval(this.colorUpdateInterval); + this.colorUpdateInterval = null; + } + } + + /** + * 获取当前HSL颜色 + * @returns {String} HSL颜色字符串 + */ + getCurrentColor() { + return `hsl(${this.hue}, 100%, 50%)`; + } + + /** + * 设置颜色变化速度 + * @param {Number} speed 变化速度 + */ + setColorSpeed(speed) { + this.colorSpeed = Math.max(1, Math.min(50, speed)); + return this.colorSpeed; + } + + /** + * 获取笔刷可配置属性 + * @returns {Array} 可配置属性描述数组 + * @override + */ + getConfigurableProperties() { + // 获取基础属性 + const baseProperties = super.getConfigurableProperties(); + + // 添加彩虹笔特有属性 + const rainbowProperties = [ + { + id: "colorSpeed", + name: "颜色变化速度", + type: "slider", + defaultValue: this.colorSpeed, + min: 1, + max: 50, + step: 1, + description: "彩虹颜色变化的速度", + category: "特效", + order: 100, + }, + ]; + + // 移除基础属性中的颜色选择器,因为彩虹笔不需要颜色选择 + const filteredBaseProps = baseProperties.filter( + (prop) => prop.id !== "color" + ); + + // 合并并返回所有属性 + return [...filteredBaseProps, ...rainbowProperties]; + } + + /** + * 更新笔刷属性 + * @param {String} propId 属性ID + * @param {any} value 属性值 + * @returns {Boolean} 是否更新成功 + * @override + */ + updateProperty(propId, value) { + // 先检查基类能否处理此属性 + if (super.updateProperty(propId, value)) { + return true; + } + + // 处理彩虹笔特有属性 + if (propId === "colorSpeed") { + this.setColorSpeed(value); + return true; + } + + return false; + } + + /** + * 笔刷被选中 + */ + onSelected() { + // 在笔刷被选中时可以做一些初始化工作 + } + + /** + * 笔刷被取消选中 + */ + onDeselected() { + // 停止所有循环 + this.stopColorCycle(); + } + + /** + * 销毁笔刷实例 + */ + destroy() { + this.stopColorCycle(); + super.destroy(); + } +} + +/** + * 自定义笔刷示例 + * 展示如何创建具有复杂特有属性的自定义笔刷 + */ +export class CustomBrushExample extends BaseBrush { + /** + * 构造函数 + * @param {Object} canvas fabric画布实例 + * @param {Object} options 配置选项 + */ + constructor(canvas, options = {}) { + super(canvas, { + id: "custom_example", + name: "特效笔刷", + description: "一个具有多种特效的自定义笔刷", + category: "效果笔刷", + icon: "magic", + ...options, + }); + + // 特有属性初始化 + this.effectType = options.effectType || "neon"; + this.patternType = options.patternType || "solid"; + this.particleCount = options.particleCount || 10; + this.particleSize = options.particleSize || 3; + this.dashPattern = options.dashPattern || [0, 0]; // 实线 + this.noiseAmount = options.noiseAmount || 0; + this.gradientEnabled = options.gradientEnabled || false; + this.gradientColors = options.gradientColors || [ + "#ff0000", + "#ffff00", + "#00ff00", + ]; + this.angle = options.angle || 0; + + // 材质相关属性 + this.textureEnabled = options.textureEnabled || false; + this.texturePath = options.texturePath || ""; + this.textureScale = options.textureScale || 1; + this.textureOpacity = options.textureOpacity || 1; + + // 动画相关 + this.animationEnabled = options.animationEnabled || false; + this.animationType = options.animationType || "none"; + this.animationSpeed = options.animationSpeed || 1; + + // 缓存的渐变和材质对象 + this._gradient = null; + this._texture = null; + } + + /** + * 创建笔刷实例 + * @returns {Object} fabric笔刷实例 + */ + create() { + if (!this.canvas) { + throw new Error("画布实例不存在"); + } + + // 根据效果类型选择合适的基础笔刷 + switch (this.effectType) { + case "neon": + this.brush = new fabric.PencilBrush(this.canvas); + break; + case "spray": + this.brush = new fabric.SprayBrush(this.canvas); + break; + case "pattern": + this.brush = new fabric.PatternBrush(this.canvas); + break; + case "custom": + // 创建完全自定义的笔刷 + this.brush = this._createCustomBrush(); + break; + default: + this.brush = new fabric.PencilBrush(this.canvas); + } + + // 配置笔刷 + this.configure(this.brush, this.options); + + return this.brush; + } + + /** + * 创建完全自定义的笔刷 + * @private + * @returns {Object} 自定义笔刷对象 + */ + _createCustomBrush() { + // 这里可以实现完全自定义的笔刷逻辑 + // 例如通过继承fabric.BaseBrush并覆盖其方法 + + // 示例:简单的自定义笔刷,这里只是概念演示 + const customBrush = new fabric.PencilBrush(this.canvas); + + // 覆盖绘制方法来实现自定义效果 + const originalOnMouseMove = customBrush.onMouseMove; + customBrush.onMouseMove = (pointer, options) => { + // 添加自定义效果,比如随机抖动 + if (this.noiseAmount > 0) { + pointer.x += (Math.random() - 0.5) * this.noiseAmount; + pointer.y += (Math.random() - 0.5) * this.noiseAmount; + } + + // 调用原始方法 + originalOnMouseMove.call(customBrush, pointer, options); + }; + + return customBrush; + } + + /** + * 配置笔刷 + * @param {Object} brush fabric笔刷实例 + * @param {Object} options 配置选项 + */ + configure(brush, options = {}) { + if (!brush) { + return; + } + + // 基础属性配置 + if (options.width !== undefined) { + brush.width = options.width; + } + + if (options.color !== undefined) { + brush.color = options.color; + } + + if (options.opacity !== undefined) { + brush.opacity = options.opacity; + } + + // 根据效果类型应用不同的配置 + switch (this.effectType) { + case "neon": + this._configureNeonEffect(brush); + break; + case "spray": + this._configureSprayEffect(brush); + break; + case "pattern": + this._configurePatternEffect(brush); + break; + case "custom": + this._configureCustomEffect(brush); + break; + } + + // 配置虚线效果 + if (this.patternType === "dashed" && Array.isArray(this.dashPattern)) { + brush.strokeDashArray = this.dashPattern; + } else { + brush.strokeDashArray = null; + } + + // 配置渐变 + if (this.gradientEnabled) { + this._configureGradient(brush); + } + + // 配置材质 + if (this.textureEnabled && this.texturePath) { + this._loadTextureImage(); + } + + // 配置动画 + if (this.animationEnabled) { + this._configureAnimation(brush); + } + } + + /** + * 配置霓虹灯效果 + * @private + * @param {Object} brush 笔刷实例 + */ + _configureNeonEffect(brush) { + // 霓虹灯效果 - 添加阴影效果 + brush.shadow = new fabric.Shadow({ + color: brush.color, + blur: 15, + offsetX: 0, + offsetY: 0, + }); + + // 增加笔刷的不透明度 + brush.opacity = Math.min(1, brush.opacity * 1.2); + } + + /** + * 配置喷枪效果 + * @private + * @param {Object} brush 笔刷实例 + */ + _configureSprayEffect(brush) { + if (brush.density !== undefined) { + brush.density = this.particleCount; + } + + if (brush.dotWidth !== undefined) { + brush.dotWidth = this.particleSize; + } + + if (brush.randomOpacity !== undefined) { + brush.randomOpacity = true; + } + } + + /** + * 配置图案效果 + * @private + * @param {Object} brush 笔刷实例 + */ + _configurePatternEffect(brush) { + // 如果有纹理,则使用纹理作为图案 + if (this._texture) { + brush.source = this._texture; + } else { + // 否则创建简单的图案 + const patternCanvas = document.createElement("canvas"); + patternCanvas.width = 20; + patternCanvas.height = 20; + const ctx = patternCanvas.getContext("2d"); + + ctx.fillStyle = brush.color; + + switch (this.patternType) { + case "dots": + // 绘制圆点 + ctx.beginPath(); + ctx.arc(10, 10, 5, 0, Math.PI * 2); + ctx.fill(); + break; + case "lines": + // 绘制线条 + ctx.lineWidth = 2; + ctx.beginPath(); + ctx.moveTo(0, 10); + ctx.lineTo(20, 10); + ctx.stroke(); + break; + default: + // 实心 + ctx.fillRect(0, 0, 20, 20); + } + + brush.source = patternCanvas; + } + } + + /** + * 配置自定义效果 + * @private + * @param {Object} brush 笔刷实例 + */ + _configureCustomEffect(brush) { + // 实现自定义效果... + brush.globalCompositeOperation = "source-over"; + } + + /** + * 配置渐变 + * @private + * @param {Object} brush 笔刷实例 + */ + _configureGradient(brush) { + // 创建线性渐变 + const gradient = new fabric.Gradient({ + type: "linear", + coords: { + x1: 0, + y1: 0, + x2: 100, + y2: 0, + }, + colorStops: this.gradientColors.map((color, i) => { + return { + offset: i / (this.gradientColors.length - 1), + color: color, + }; + }), + gradientAngle: this.angle, + }); + + // 存储渐变 + this._gradient = gradient; + + // 在这里无法直接将渐变应用到笔刷 + // 需要在实际绘制时使用... + } + + /** + * 配置动画 + * @private + * @param {Object} brush 笔刷实例 + */ + _configureAnimation(brush) { + // 动画需要在画布上持续更新 + // 这里只是示例,实际实现需要更复杂的处理 + if (!this._animationID) { + this._startAnimation(); + } + } + + /** + * 开始动画 + * @private + */ + _startAnimation() { + let phase = 0; + + const animate = () => { + phase += 0.05 * this.animationSpeed; + + // 执行动画效果 + switch (this.animationType) { + case "pulse": + // 脉冲效果 - 改变大小 + if (this.brush) { + const baseWidth = this.options.width || 5; + this.brush.width = baseWidth + Math.sin(phase) * baseWidth * 0.3; + } + break; + case "rainbow": + // 彩虹效果 - 改变颜色 + if (this.brush) { + const hue = (phase * 30) % 360; + this.brush.color = `hsl(${hue}, 100%, 50%)`; + } + break; + } + + // 请求下一帧 + this._animationID = requestAnimationFrame(animate); + }; + + this._animationID = requestAnimationFrame(animate); + } + + /** + * 停止动画 + * @private + */ + _stopAnimation() { + if (this._animationID) { + cancelAnimationFrame(this._animationID); + this._animationID = null; + } + } + + /** + * 加载材质图片 + * @private + * @returns {Promise} Promise对象 + */ + _loadTextureImage() { + return new Promise((resolve, reject) => { + if (!this.texturePath) { + this._texture = null; + reject(new Error("未指定材质路径")); + return; + } + + const img = new Image(); + img.crossOrigin = "anonymous"; + + img.onload = () => { + // 创建缩放后的图案 + const patternCanvas = document.createElement("canvas"); + const patternContext = patternCanvas.getContext("2d"); + + // 调整图案大小 + const scale = this.textureScale || 1; + const width = img.width * scale; + const height = img.height * scale; + + patternCanvas.width = width; + patternCanvas.height = height; + + // 应用透明度 + if (this.textureOpacity < 1) { + patternContext.globalAlpha = this.textureOpacity; + } + + // 绘制缩放后的图像 + patternContext.drawImage(img, 0, 0, width, height); + + this._texture = patternCanvas; + + // 如果当前是图案模式,重新配置笔刷 + if (this.effectType === "pattern" && this.brush) { + this.brush.source = patternCanvas; + } + + resolve(img); + }; + + img.onerror = () => { + this._texture = null; + reject(new Error(`加载材质图片失败: ${this.texturePath}`)); + }; + + img.src = this.texturePath; + }); + } + + /** + * 获取笔刷可配置属性 + * @returns {Array} 可配置属性描述数组 + * @override + */ + getConfigurableProperties() { + // 获取基础属性 + const baseProperties = super.getConfigurableProperties(); + + // 特效类型选择 + const effectTypeProperty = { + id: "effectType", + name: "特效类型", + type: "select", + defaultValue: this.effectType, + options: [ + { value: "neon", label: "霓虹灯" }, + { value: "spray", label: "喷枪" }, + { value: "pattern", label: "图案" }, + { value: "custom", label: "自定义" }, + ], + description: "选择笔刷特效类型", + category: "特效设置", + order: 100, + }; + + // 定义基于特效类型的动态属性 + const dynamicProperties = [ + // 喷枪效果特有属性 + { + id: "particleCount", + name: "粒子数量", + type: "slider", + defaultValue: this.particleCount, + min: 1, + max: 50, + step: 1, + description: "喷枪效果的粒子密度", + category: "特效设置", + order: 110, + visibleWhen: { effectType: "spray" }, + }, + { + id: "particleSize", + name: "粒子大小", + type: "slider", + defaultValue: this.particleSize, + min: 1, + max: 20, + step: 1, + description: "喷枪效果的粒子大小", + category: "特效设置", + order: 120, + visibleWhen: { effectType: "spray" }, + }, + + // 图案效果特有属性 + { + id: "patternType", + name: "图案类型", + type: "select", + defaultValue: this.patternType, + options: [ + { value: "solid", label: "实心" }, + { value: "dots", label: "圆点" }, + { value: "lines", label: "线条" }, + { value: "dashed", label: "虚线" }, + ], + description: "选择图案类型", + category: "特效设置", + order: 130, + visibleWhen: (values) => + values.effectType === "pattern" || values.patternType === "dashed", + }, + + // 虚线特有属性 + { + id: "dashPattern1", + name: "虚线长度", + type: "slider", + defaultValue: this.dashPattern[0] || 5, + min: 1, + max: 20, + step: 1, + description: "虚线段的长度", + category: "特效设置", + order: 140, + visibleWhen: { patternType: "dashed" }, + }, + { + id: "dashPattern2", + name: "虚线间隔", + type: "slider", + defaultValue: this.dashPattern[1] || 5, + min: 1, + max: 20, + step: 1, + description: "虚线段之间的间隔", + category: "特效设置", + order: 150, + visibleWhen: { patternType: "dashed" }, + }, + + // 自定义效果特有属性 + { + id: "noiseAmount", + name: "噪点强度", + type: "slider", + defaultValue: this.noiseAmount, + min: 0, + max: 20, + step: 0.5, + description: "添加随机噪点的强度", + category: "特效设置", + order: 160, + visibleWhen: { effectType: "custom" }, + }, + ]; + + // 渐变相关属性 + const gradientProperties = [ + { + id: "gradientEnabled", + name: "启用渐变", + type: "checkbox", + defaultValue: this.gradientEnabled, + description: "使用渐变色代替纯色", + category: "渐变设置", + order: 200, + }, + { + id: "gradientColor1", + name: "渐变颜色 1", + type: "color", + defaultValue: this.gradientColors[0] || "#ff0000", + description: "渐变的起始颜色", + category: "渐变设置", + order: 210, + visibleWhen: { gradientEnabled: true }, + }, + { + id: "gradientColor2", + name: "渐变颜色 2", + type: "color", + defaultValue: this.gradientColors[1] || "#ffff00", + description: "渐变的中间颜色", + category: "渐变设置", + order: 220, + visibleWhen: { gradientEnabled: true }, + }, + { + id: "gradientColor3", + name: "渐变颜色 3", + type: "color", + defaultValue: this.gradientColors[2] || "#00ff00", + description: "渐变的结束颜色", + category: "渐变设置", + order: 230, + visibleWhen: { gradientEnabled: true }, + }, + { + id: "angle", + name: "渐变角度", + type: "slider", + defaultValue: this.angle, + min: 0, + max: 360, + step: 5, + description: "渐变的方向角度", + category: "渐变设置", + order: 240, + visibleWhen: { gradientEnabled: true }, + }, + ]; + + // 材质相关属性 + const textureProperties = [ + { + id: "textureEnabled", + name: "启用材质", + type: "checkbox", + defaultValue: this.textureEnabled, + description: "使用图片材质", + category: "材质设置", + order: 300, + }, + { + id: "texturePath", + name: "材质图片", + type: "file", + defaultValue: this.texturePath, + description: "上传或选择要用作笔刷的图片材质", + category: "材质设置", + order: 310, + accept: "image/*", + visibleWhen: { textureEnabled: true }, + }, + { + id: "textureScale", + name: "材质缩放", + type: "slider", + defaultValue: this.textureScale, + min: 0.1, + max: 5.0, + step: 0.1, + description: "调整材质图片的大小", + category: "材质设置", + order: 320, + visibleWhen: { textureEnabled: true }, + }, + { + id: "textureOpacity", + name: "材质透明度", + type: "slider", + defaultValue: this.textureOpacity, + min: 0.1, + max: 1.0, + step: 0.05, + description: "调整材质图片的透明度", + category: "材质设置", + order: 330, + visibleWhen: { textureEnabled: true }, + }, + { + id: "presetTextures", + name: "预设材质", + type: "texture-grid", + defaultValue: "", + options: [ + { + value: "/textures/brush1.png", + preview: "/textures/brush1_thumb.png", + label: "纹理1", + }, + { + value: "/textures/brush2.png", + preview: "/textures/brush2_thumb.png", + label: "纹理2", + }, + ], + description: "选择预设材质图案", + category: "材质设置", + order: 340, + visibleWhen: { textureEnabled: true }, + // 动态选项加载(示例) + dynamicOptions: (values) => { + // 这里可以根据其他属性值动态加载不同的材质选项 + // 例如从服务器获取,或者根据effectType显示不同的材质集 + const defaultOptions = [ + { + value: "/textures/brush1.png", + preview: "/textures/brush1_thumb.png", + label: "纹理1", + }, + { + value: "/textures/brush2.png", + preview: "/textures/brush2_thumb.png", + label: "纹理2", + }, + ]; + + if (values.effectType === "neon") { + defaultOptions.push({ + value: "/textures/neon_glow.png", + preview: "/textures/neon_glow_thumb.png", + label: "霓虹光晕", + }); + } + + return defaultOptions; + }, + }, + ]; + + // 动画相关属性 + const animationProperties = [ + { + id: "animationEnabled", + name: "启用动画", + type: "checkbox", + defaultValue: this.animationEnabled, + description: "为笔刷添加动态动画效果", + category: "动画设置", + order: 400, + }, + { + id: "animationType", + name: "动画类型", + type: "select", + defaultValue: this.animationType, + options: [ + { value: "none", label: "无" }, + { value: "pulse", label: "脉冲" }, + { value: "rainbow", label: "彩虹" }, + ], + description: "选择动画效果类型", + category: "动画设置", + order: 410, + visibleWhen: { animationEnabled: true }, + }, + { + id: "animationSpeed", + name: "动画速度", + type: "slider", + defaultValue: this.animationSpeed, + min: 0.1, + max: 5, + step: 0.1, + description: "调整动画播放速度", + category: "动画设置", + order: 420, + visibleWhen: { animationEnabled: true }, + }, + ]; + + // 合并并返回所有属性 + return [ + ...baseProperties, + effectTypeProperty, + ...dynamicProperties, + ...gradientProperties, + ...textureProperties, + ...animationProperties, + ]; + } + + /** + * 更新笔刷属性 + * @param {String} propId 属性ID + * @param {any} value 属性值 + * @returns {Boolean} 是否更新成功 + * @override + */ + updateProperty(propId, value) { + // 先检查基类能否处理此属性 + if (super.updateProperty(propId, value)) { + return true; + } + + // 处理特效笔刷特有属性 + switch (propId) { + case "effectType": + this.effectType = value; + // 需要重新创建笔刷 + if (this.canvas) { + this.destroy(); + this.brush = this.create(); + } + return true; + + case "patternType": + this.patternType = value; + this.configure(this.brush, {}); + return true; + + case "particleCount": + this.particleCount = value; + if (this.effectType === "spray" && this.brush) { + this.brush.density = value; + } + return true; + + case "particleSize": + this.particleSize = value; + if (this.effectType === "spray" && this.brush) { + this.brush.dotWidth = value; + } + return true; + + case "dashPattern1": + case "dashPattern2": + // 更新虚线模式 + const idx = propId === "dashPattern1" ? 0 : 1; + this.dashPattern[idx] = value; + if (this.patternType === "dashed" && this.brush) { + this.brush.strokeDashArray = this.dashPattern; + } + return true; + + case "noiseAmount": + this.noiseAmount = value; + return true; + + case "gradientEnabled": + this.gradientEnabled = value; + if (value && this.brush) { + this._configureGradient(this.brush); + } + return true; + + case "gradientColor1": + case "gradientColor2": + case "gradientColor3": + const colorIdx = parseInt(propId.slice(-1)) - 1; + this.gradientColors[colorIdx] = value; + if (this.gradientEnabled && this.brush) { + this._configureGradient(this.brush); + } + return true; + + case "angle": + this.angle = value; + if (this.gradientEnabled && this.brush) { + this._configureGradient(this.brush); + } + return true; + + case "textureEnabled": + this.textureEnabled = value; + if (value && this.texturePath) { + this._loadTextureImage(); + } + return true; + + case "texturePath": + this.texturePath = value; + if (this.textureEnabled && value) { + this._loadTextureImage(); + } + return true; + + case "textureScale": + this.textureScale = value; + if (this.textureEnabled && this.texturePath) { + this._loadTextureImage(); + } + return true; + + case "textureOpacity": + this.textureOpacity = value; + if (this.textureEnabled && this.texturePath) { + this._loadTextureImage(); + } + return true; + + case "animationEnabled": + this.animationEnabled = value; + if (value) { + this._startAnimation(); + } else { + this._stopAnimation(); + } + return true; + + case "animationType": + this.animationType = value; + return true; + + case "animationSpeed": + this.animationSpeed = value; + return true; + } + + return false; + } + + /** + * 获取预览图 + * @returns {String} 预览图URL + */ + getPreview() { + // 根据效果类型返回不同的预览图 + switch (this.effectType) { + case "neon": + return "data:image/svg+xml;base64,..."; // 示例图 + case "spray": + return "data:image/svg+xml;base64,..."; // 示例图 + case "pattern": + if (this._texture) { + // 返回材质图 + return this._texture.toDataURL(); + } + return "data:image/svg+xml;base64,..."; // 示例图 + default: + return "data:image/svg+xml;base64,..."; // 示例图 + } + } + + /** + * 笔刷被选中时调用 + * @override + */ + onSelected() { + // 如果启用了动画,开始动画循环 + if (this.animationEnabled) { + this._startAnimation(); + } + } + + /** + * 笔刷被取消选中时调用 + * @override + */ + onDeselected() { + // 停止动画 + this._stopAnimation(); + } + + /** + * 销毁笔刷实例并清理资源 + * @override + */ + destroy() { + // 停止动画 + this._stopAnimation(); + + // 清理资源 + this._gradient = null; + this._texture = null; + + // 调用基类销毁方法 + super.destroy(); + } +} + +// 导出函数,用于注册这些自定义笔刷 +export function registerCustomBrushes() { + // 注册霓虹笔刷 + brushRegistry.register("neon", NeonBrush, { + name: "霓虹笔", + description: "带有发光效果的霓虹笔刷", + category: "特效笔刷", + }); + + // 注册彩虹笔刷 + brushRegistry.register("rainbow", RainbowBrush, { + name: "彩虹笔", + description: "绘制时会自动变换颜色的彩虹笔", + category: "特效笔刷", + }); + + console.log("已注册自定义笔刷"); +} diff --git a/src/component/Canvas/CanvasEditor/managers/brushes/types/CustomPenBrush.js b/src/component/Canvas/CanvasEditor/managers/brushes/types/CustomPenBrush.js new file mode 100644 index 00000000..a981653d --- /dev/null +++ b/src/component/Canvas/CanvasEditor/managers/brushes/types/CustomPenBrush.js @@ -0,0 +1,196 @@ +import { BaseBrush } from "../BaseBrush"; + +/** + * 钢笔笔刷 + * 模拟钢笔效果,具有变化的透明度 + */ +export class CustomPenBrush extends BaseBrush { + /** + * 构造函数 + * @param {Object} canvas fabric画布实例 + * @param {Object} options 配置选项 + */ + constructor(canvas, options = {}) { + super(canvas, { + id: "pen", + name: "钢笔", + description: "模拟钢笔效果,具有变化的透明度", + category: "基础笔刷", + icon: "pen", + ...options, + }); + + // 钢笔笔刷特有属性 + this._baseWidth = options._baseWidth || 15; + this._lineWidth = options._lineWidth || 2; + this.inkOpacityMin = options.inkOpacityMin || 0.2; + this.inkOpacityMax = options.inkOpacityMax || 0.6; + } + + /** + * 创建笔刷实例 + * @returns {Object} fabric笔刷实例 + */ + create() { + if (!this.canvas) { + throw new Error("画布实例不存在"); + } + + // 创建fabric原生钢笔笔刷 + this.brush = new fabric.PenBrush(this.canvas); + + // 配置笔刷 + this.configure(this.brush, this.options); + + return this.brush; + } + + /** + * 配置笔刷 + * @param {Object} brush fabric笔刷实例 + * @param {Object} options 配置选项 + */ + configure(brush, options = {}) { + if (!brush) return; + + // 基础属性配置 + if (options.width !== undefined) { + brush.width = options.width; + // 更新笔刷相关属性 + this._baseWidth = options.width / 2; + } + + if (options.color !== undefined) { + brush.color = options.color; + } + + if (options.opacity !== undefined) { + brush.opacity = options.opacity; + } + + // 钢笔笔刷特有属性 + if (options._baseWidth !== undefined) { + brush._baseWidth = options._baseWidth; + this._baseWidth = options._baseWidth; + } + + if (options._lineWidth !== undefined) { + brush._lineWidth = options._lineWidth; + this._lineWidth = options._lineWidth; + } + + // 确保线条连接设置正确 + brush.canvas.contextTop.lineJoin = "round"; + brush.canvas.contextTop.lineCap = "round"; + } + + /** + * 设置最小墨水透明度 + * @param {Number} opacity 透明度值(0-1) + */ + setInkOpacityMin(opacity) { + this.inkOpacityMin = Math.max(0.1, Math.min(0.5, opacity)); + return this.inkOpacityMin; + } + + /** + * 设置最大墨水透明度 + * @param {Number} opacity 透明度值(0-1) + */ + setInkOpacityMax(opacity) { + this.inkOpacityMax = Math.max(0.3, Math.min(1, opacity)); + return this.inkOpacityMax; + } + + /** + * 获取笔刷可配置属性 + * @returns {Array} 可配置属性描述数组 + * @override + */ + getConfigurableProperties() { + // 获取基础属性 + const baseProperties = super.getConfigurableProperties(); + + // 定义钢笔笔刷特有属性 + const penProperties = [ + { + id: "lineWidth", + name: "线条宽度", + type: "slider", + defaultValue: this._lineWidth, + min: 1, + max: 10, + step: 0.5, + description: "控制钢笔线条的宽度", + category: "钢笔设置", + order: 100, + }, + { + id: "inkOpacityMin", + name: "最小墨水透明度", + type: "slider", + defaultValue: this.inkOpacityMin, + min: 0.1, + max: 0.5, + step: 0.05, + description: "控制钢笔墨水最小透明度", + category: "钢笔设置", + order: 110, + }, + { + id: "inkOpacityMax", + name: "最大墨水透明度", + type: "slider", + defaultValue: this.inkOpacityMax, + min: 0.3, + max: 1, + step: 0.05, + description: "控制钢笔墨水最大透明度", + category: "钢笔设置", + order: 120, + }, + ]; + + // 合并并返回所有属性 + return [...baseProperties, ...penProperties]; + } + + /** + * 更新笔刷属性 + * @param {String} propId 属性ID + * @param {any} value 属性值 + * @returns {Boolean} 是否更新成功 + * @override + */ + updateProperty(propId, value) { + // 先检查基类能否处理此属性 + if (super.updateProperty(propId, value)) { + return true; + } + + // 处理钢笔笔刷特有属性 + if (propId === "lineWidth") { + this._lineWidth = value; + if (this.brush) { + this.brush._lineWidth = value; + } + return true; + } else if (propId === "inkOpacityMin") { + this.setInkOpacityMin(value); + return true; + } else if (propId === "inkOpacityMax") { + this.setInkOpacityMax(value); + return true; + } + + return false; + } + + /** + * 获取预览图 + * @returns {String} 预览图URL + */ + getPreview() { + return "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMDAgMTAwIj48cGF0aCBkPSJNMjAgMjBMODAgODBNMjAgODBMODAgMjAiIGZpbGw9Im5vbmUiIHN0cm9rZT0iIzAwMCIgc3Ryb2tlLXdpZHRoPSIzIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiLz48L3N2Zz4="; + } +} diff --git a/src/component/Canvas/CanvasEditor/managers/brushes/types/FurBrush.js b/src/component/Canvas/CanvasEditor/managers/brushes/types/FurBrush.js new file mode 100644 index 00000000..90f96c23 --- /dev/null +++ b/src/component/Canvas/CanvasEditor/managers/brushes/types/FurBrush.js @@ -0,0 +1,201 @@ +import { BaseBrush } from "../BaseBrush"; + +/** + * 毛发笔刷 + * 创建类似于毛发或草的效果 + */ +export class FurBrush extends BaseBrush { + /** + * 构造函数 + * @param {Object} canvas fabric画布实例 + * @param {Object} options 配置选项 + */ + constructor(canvas, options = {}) { + super(canvas, { + id: "fur", + name: "毛发笔刷", + description: "创建类似于毛发或草的纹理效果", + category: "特效笔刷", + icon: "fur", + ...options, + }); + + // 毛发笔刷特有属性 + this.furLength = options.furLength || 10; + this.furDensity = options.furDensity || 0.7; + this.furRandomness = options.furRandomness || 0.5; + } + + /** + * 创建笔刷实例 + * @returns {Object} fabric笔刷实例 + */ + create() { + if (!this.canvas) { + throw new Error("画布实例不存在"); + } + + // 创建fabric原生毛发笔刷 + this.brush = new fabric.FurBrush(this.canvas); + + // 配置笔刷 + this.configure(this.brush, this.options); + + return this.brush; + } + + /** + * 配置笔刷 + * @param {Object} brush fabric笔刷实例 + * @param {Object} options 配置选项 + */ + configure(brush, options = {}) { + if (!brush) return; + + // 基础属性配置 + if (options.width !== undefined) { + brush.width = options.width; + } + + if (options.color !== undefined) { + brush.color = options.color; + } + + if (options.opacity !== undefined) { + brush.opacity = options.opacity; + } + + // 这里可以添加对毛发笔刷特有属性的配置 + // 由于fabric.FurBrush的原始实现可能没有直接暴露这些属性, + // 我们可能需要在onMouseMove等事件中动态调整行为 + + // 存储特有属性,供后续使用 + if (options.furLength !== undefined) { + this.furLength = options.furLength; + } + + if (options.furDensity !== undefined) { + this.furDensity = options.furDensity; + } + + if (options.furRandomness !== undefined) { + this.furRandomness = options.furRandomness; + } + } + + /** + * 设置毛发长度 + * @param {Number} length 长度值 + */ + setFurLength(length) { + this.furLength = Math.max(1, Math.min(50, length)); + return this.furLength; + } + + /** + * 设置毛发密度 + * @param {Number} density 密度值(0-1) + */ + setFurDensity(density) { + this.furDensity = Math.max(0.1, Math.min(1, density)); + return this.furDensity; + } + + /** + * 设置毛发随机性 + * @param {Number} randomness 随机性值(0-1) + */ + setFurRandomness(randomness) { + this.furRandomness = Math.max(0, Math.min(1, randomness)); + return this.furRandomness; + } + + /** + * 获取笔刷可配置属性 + * @returns {Array} 可配置属性描述数组 + * @override + */ + getConfigurableProperties() { + // 获取基础属性 + const baseProperties = super.getConfigurableProperties(); + + // 定义毛发笔刷特有属性 + const furProperties = [ + { + id: "furLength", + name: "毛发长度", + type: "slider", + defaultValue: this.furLength, + min: 1, + max: 50, + step: 1, + description: "控制毛发的长度", + category: "毛发设置", + order: 100, + }, + { + id: "furDensity", + name: "毛发密度", + type: "slider", + defaultValue: this.furDensity, + min: 0.1, + max: 1, + step: 0.05, + description: "控制毛发的密度", + category: "毛发设置", + order: 110, + }, + { + id: "furRandomness", + name: "随机性", + type: "slider", + defaultValue: this.furRandomness, + min: 0, + max: 1, + step: 0.05, + description: "控制毛发的随机分布程度", + category: "毛发设置", + order: 120, + }, + ]; + + // 合并并返回所有属性 + return [...baseProperties, ...furProperties]; + } + + /** + * 更新笔刷属性 + * @param {String} propId 属性ID + * @param {any} value 属性值 + * @returns {Boolean} 是否更新成功 + * @override + */ + updateProperty(propId, value) { + // 先检查基类能否处理此属性 + if (super.updateProperty(propId, value)) { + return true; + } + + // 处理毛发笔刷特有属性 + if (propId === "furLength") { + this.setFurLength(value); + return true; + } else if (propId === "furDensity") { + this.setFurDensity(value); + return true; + } else if (propId === "furRandomness") { + this.setFurRandomness(value); + return true; + } + + return false; + } + + /** + * 获取预览图 + * @returns {String} 预览图URL + */ + getPreview() { + return "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMDAgMTAwIj48cGF0aCBkPSJNMTAgODBMNTAgMjBNMjAgODBMNjAgMjBNMzAgODBMNzAgMjBNNDAgODBMODAgMjBNNTAgODBMOTAgMjAiIHN0cm9rZT0iIzAwMCIgc3Ryb2tlLXdpZHRoPSIxIiBmaWxsPSJub25lIi8+PC9zdmc+"; + } +} diff --git a/src/component/Canvas/CanvasEditor/managers/brushes/types/InkBrush.js b/src/component/Canvas/CanvasEditor/managers/brushes/types/InkBrush.js new file mode 100644 index 00000000..b838804b --- /dev/null +++ b/src/component/Canvas/CanvasEditor/managers/brushes/types/InkBrush.js @@ -0,0 +1,267 @@ +import { BaseBrush } from "../BaseBrush"; + +/** + * 水墨笔刷 + * 模拟中国传统水墨画效果 + */ +export class InkBrush extends BaseBrush { + /** + * 构造函数 + * @param {Object} canvas fabric画布实例 + * @param {Object} options 配置选项 + */ + constructor(canvas, options = {}) { + super(canvas, { + id: "ink", + name: "水墨笔刷", + description: "模拟中国传统水墨画效果,墨色深浅不一", + category: "特效笔刷", + icon: "ink", + ...options, + }); + + // 水墨笔刷特有属性 + this._baseWidth = options._baseWidth || 15; + this._inkAmount = options._inkAmount || 7; + this._range = options._range || 10; + this.splashEnabled = + options.splashEnabled !== undefined ? options.splashEnabled : true; + this.splashSize = options.splashSize || 5; + this.splashDistance = options.splashDistance || 30; + } + + /** + * 创建笔刷实例 + * @returns {Object} fabric笔刷实例 + */ + create() { + if (!this.canvas) { + throw new Error("画布实例不存在"); + } + + // 创建fabric原生水墨笔刷 + this.brush = new fabric.InkBrush(this.canvas); + + // 配置笔刷 + this.configure(this.brush, this.options); + + return this.brush; + } + + /** + * 配置笔刷 + * @param {Object} brush fabric笔刷实例 + * @param {Object} options 配置选项 + */ + configure(brush, options = {}) { + if (!brush) return; + + // 基础属性配置 + if (options.width !== undefined) { + brush.width = options.width; + this._baseWidth = options.width / 2; + } + + if (options.color !== undefined) { + brush.color = options.color; + } + + if (options.opacity !== undefined) { + brush.opacity = options.opacity; + } + + // 水墨笔刷特有属性 + if (options._inkAmount !== undefined) { + brush._inkAmount = options._inkAmount; + this._inkAmount = options._inkAmount; + } + + if (options._range !== undefined) { + brush._range = options._range; + this._range = options._range; + } + + // 更新溅墨相关配置(这需要修改原始InkBrush的drawSplash方法) + if (this.splashEnabled !== undefined) { + // 由于原始InkBrush没有直接暴露这个配置,我们可能需要覆盖方法 + // 这里仅保存配置,实际逻辑需要在brush创建后处理 + } + } + + /** + * 设置墨量 + * @param {Number} amount 墨量值 + */ + setInkAmount(amount) { + this._inkAmount = Math.max(1, Math.min(20, amount)); + + if (this.brush) { + this.brush._inkAmount = this._inkAmount; + } + + return this._inkAmount; + } + + /** + * 设置笔触范围 + * @param {Number} range 范围值 + */ + setRange(range) { + this._range = Math.max(5, Math.min(50, range)); + + if (this.brush) { + this.brush._range = this._range; + } + + return this._range; + } + + /** + * 启用/禁用溅墨效果 + * @param {Boolean} enabled 是否启用 + */ + setSplashEnabled(enabled) { + this.splashEnabled = enabled; + + // 实际应用需要更多的逻辑来支持这个功能 + // 由于需要修改fabric.InkBrush的内部行为 + + return this.splashEnabled; + } + + /** + * 设置溅墨大小 + * @param {Number} size 大小值 + */ + setSplashSize(size) { + this.splashSize = Math.max(1, Math.min(20, size)); + return this.splashSize; + } + + /** + * 设置溅墨距离 + * @param {Number} distance 距离值 + */ + setSplashDistance(distance) { + this.splashDistance = Math.max(10, Math.min(100, distance)); + return this.splashDistance; + } + + /** + * 获取笔刷可配置属性 + * @returns {Array} 可配置属性描述数组 + * @override + */ + getConfigurableProperties() { + // 获取基础属性 + const baseProperties = super.getConfigurableProperties(); + + // 定义水墨笔刷特有属性 + const inkProperties = [ + { + id: "inkAmount", + name: "墨量", + type: "slider", + defaultValue: this._inkAmount, + min: 1, + max: 20, + step: 1, + description: "控制水墨的浓度", + category: "水墨设置", + order: 100, + }, + { + id: "range", + name: "笔触范围", + type: "slider", + defaultValue: this._range, + min: 5, + max: 50, + step: 1, + description: "控制水墨扩散的范围", + category: "水墨设置", + order: 110, + }, + { + id: "splashEnabled", + name: "溅墨效果", + type: "checkbox", + defaultValue: this.splashEnabled, + description: "是否启用溅墨效果", + category: "水墨设置", + order: 120, + }, + { + id: "splashSize", + name: "溅墨大小", + type: "slider", + defaultValue: this.splashSize, + min: 1, + max: 20, + step: 1, + description: "溅墨点的大小", + category: "水墨设置", + order: 130, + visibleWhen: { splashEnabled: true }, + }, + { + id: "splashDistance", + name: "溅墨距离", + type: "slider", + defaultValue: this.splashDistance, + min: 10, + max: 100, + step: 5, + description: "溅墨可扩散的最大距离", + category: "水墨设置", + order: 140, + visibleWhen: { splashEnabled: true }, + }, + ]; + + // 合并并返回所有属性 + return [...baseProperties, ...inkProperties]; + } + + /** + * 更新笔刷属性 + * @param {String} propId 属性ID + * @param {any} value 属性值 + * @returns {Boolean} 是否更新成功 + * @override + */ + updateProperty(propId, value) { + // 先检查基类能否处理此属性 + if (super.updateProperty(propId, value)) { + return true; + } + + // 处理水墨笔刷特有属性 + if (propId === "inkAmount") { + this.setInkAmount(value); + return true; + } else if (propId === "range") { + this.setRange(value); + return true; + } else if (propId === "splashEnabled") { + this.setSplashEnabled(value); + return true; + } else if (propId === "splashSize") { + this.setSplashSize(value); + return true; + } else if (propId === "splashDistance") { + this.setSplashDistance(value); + return true; + } + + return false; + } + + /** + * 获取预览图 + * @returns {String} 预览图URL + */ + getPreview() { + return "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMDAgMTAwIj48cGF0aCBkPSJNMjAgODBDNDAgNjAgNjAgNDAgODAgMjAiIHN0cm9rZT0iIzAwMCIgc3Ryb2tlLXdpZHRoPSI1IiBzdHJva2UtbGluZWNhcD0icm91bmQiIGZpbGw9Im5vbmUiLz48Y2lyY2xlIGN4PSI3MCIgY3k9IjMwIiByPSI1IiBmaWxsPSIjMDAwIi8+PGNpcmNsZSBjeD0iNTAiIGN5PSI1MCIgcj0iMyIgZmlsbD0iIzAwMCIvPjxjaXJjbGUgY3g9IjMwIiBjeT0iNzAiIHI9IjYiIGZpbGw9IiMwMDAiLz48L3N2Zz4="; + } +} diff --git a/src/component/Canvas/CanvasEditor/managers/brushes/types/LongfurBrush.js b/src/component/Canvas/CanvasEditor/managers/brushes/types/LongfurBrush.js new file mode 100644 index 00000000..10430969 --- /dev/null +++ b/src/component/Canvas/CanvasEditor/managers/brushes/types/LongfurBrush.js @@ -0,0 +1,305 @@ +import { BaseBrush } from "../BaseBrush"; + +/** + * 长毛发笔刷 + * 创建类似于长毛、毛皮、草或头发的效果 + */ +export class LongfurBrush extends BaseBrush { + /** + * 构造函数 + * @param {Object} canvas fabric画布实例 + * @param {Object} options 配置选项 + */ + constructor(canvas, options = {}) { + super(canvas, { + id: "longfur", + name: "长毛发", + description: "创建流动的长毛发效果,适合绘制动物毛皮、草或头发", + category: "特效笔刷", + icon: "longfur", + ...options, + }); + + // 长毛发笔刷特有属性 + this.furLength = options.furLength || 20; + this.furDensity = options.furDensity || 0.7; + this.furFlowFactor = options.furFlowFactor || 0.5; + this.furCurvature = options.furCurvature || 0.3; + this.randomizeDirection = + options.randomizeDirection !== undefined + ? options.randomizeDirection + : true; + } + + /** + * 创建笔刷实例 + * @returns {Object} fabric笔刷实例 + */ + create() { + if (!this.canvas) { + throw new Error("画布实例不存在"); + } + + // 创建fabric原生长毛发笔刷 + this.brush = new fabric.LongfurBrush(this.canvas); + + // 配置笔刷 + this.configure(this.brush, this.options); + + return this.brush; + } + + /** + * 配置笔刷 + * @param {Object} brush fabric笔刷实例 + * @param {Object} options 配置选项 + */ + configure(brush, options = {}) { + if (!brush) return; + + // 基础属性配置 + if (options.width !== undefined) { + brush.width = options.width; + } + + if (options.color !== undefined) { + brush.color = options.color; + } + + if (options.opacity !== undefined) { + brush.opacity = options.opacity; + } + + // 长毛发笔刷特有属性 + if (options.furLength !== undefined) { + this.furLength = options.furLength; + + // 如果原生笔刷支持此属性,则设置 + if (brush.furLength !== undefined) { + brush.furLength = this.furLength; + } + } + + if (options.furDensity !== undefined) { + this.furDensity = options.furDensity; + + // 如果原生笔刷支持此属性,则设置 + if (brush.furDensity !== undefined) { + brush.furDensity = this.furDensity; + } + } + + if (options.furFlowFactor !== undefined) { + this.furFlowFactor = options.furFlowFactor; + + // 如果原生笔刷支持此属性,则设置 + if (brush.furFlowFactor !== undefined) { + brush.furFlowFactor = this.furFlowFactor; + } + } + + if (options.furCurvature !== undefined) { + this.furCurvature = options.furCurvature; + + // 如果原生笔刷支持此属性,则设置 + if (brush.furCurvature !== undefined) { + brush.furCurvature = this.furCurvature; + } + } + + if (options.randomizeDirection !== undefined) { + this.randomizeDirection = options.randomizeDirection; + + // 如果原生笔刷支持此属性,则设置 + if (brush.randomizeDirection !== undefined) { + brush.randomizeDirection = this.randomizeDirection; + } + } + } + + /** + * 设置毛发长度 + * @param {Number} length 长度值 + */ + setFurLength(length) { + this.furLength = Math.max(5, Math.min(100, length)); + + if (this.brush && this.brush.furLength !== undefined) { + this.brush.furLength = this.furLength; + } + + return this.furLength; + } + + /** + * 设置毛发密度 + * @param {Number} density 密度值(0-1) + */ + setFurDensity(density) { + this.furDensity = Math.max(0.1, Math.min(1, density)); + + if (this.brush && this.brush.furDensity !== undefined) { + this.brush.furDensity = this.furDensity; + } + + return this.furDensity; + } + + /** + * 设置毛发流动系数 + * @param {Number} factor 流动系数(0-1) + */ + setFurFlowFactor(factor) { + this.furFlowFactor = Math.max(0, Math.min(1, factor)); + + if (this.brush && this.brush.furFlowFactor !== undefined) { + this.brush.furFlowFactor = this.furFlowFactor; + } + + return this.furFlowFactor; + } + + /** + * 设置毛发弯曲度 + * @param {Number} curvature 弯曲度(0-1) + */ + setFurCurvature(curvature) { + this.furCurvature = Math.max(0, Math.min(1, curvature)); + + if (this.brush && this.brush.furCurvature !== undefined) { + this.brush.furCurvature = this.furCurvature; + } + + return this.furCurvature; + } + + /** + * 设置是否随机化方向 + * @param {Boolean} randomize 是否随机化 + */ + setRandomizeDirection(randomize) { + this.randomizeDirection = randomize; + + if (this.brush && this.brush.randomizeDirection !== undefined) { + this.brush.randomizeDirection = this.randomizeDirection; + } + + return this.randomizeDirection; + } + + /** + * 获取笔刷可配置属性 + * @returns {Array} 可配置属性描述数组 + * @override + */ + getConfigurableProperties() { + // 获取基础属性 + const baseProperties = super.getConfigurableProperties(); + + // 定义长毛发笔刷特有属性 + const longfurProperties = [ + { + id: "furLength", + name: "毛发长度", + type: "slider", + defaultValue: this.furLength, + min: 5, + max: 100, + step: 1, + description: "控制毛发的长度", + category: "长毛发设置", + order: 100, + }, + { + id: "furDensity", + name: "毛发密度", + type: "slider", + defaultValue: this.furDensity, + min: 0.1, + max: 1, + step: 0.05, + description: "控制毛发的密度", + category: "长毛发设置", + order: 110, + }, + { + id: "furFlowFactor", + name: "流动系数", + type: "slider", + defaultValue: this.furFlowFactor, + min: 0, + max: 1, + step: 0.05, + description: "控制毛发的流动感", + category: "长毛发设置", + order: 120, + }, + { + id: "furCurvature", + name: "弯曲度", + type: "slider", + defaultValue: this.furCurvature, + min: 0, + max: 1, + step: 0.05, + description: "控制毛发的弯曲程度", + category: "长毛发设置", + order: 130, + }, + { + id: "randomizeDirection", + name: "随机方向", + type: "checkbox", + defaultValue: this.randomizeDirection, + description: "是否随机化毛发方向", + category: "长毛发设置", + order: 140, + }, + ]; + + // 合并并返回所有属性 + return [...baseProperties, ...longfurProperties]; + } + + /** + * 更新笔刷属性 + * @param {String} propId 属性ID + * @param {any} value 属性值 + * @returns {Boolean} 是否更新成功 + * @override + */ + updateProperty(propId, value) { + // 先检查基类能否处理此属性 + if (super.updateProperty(propId, value)) { + return true; + } + + // 处理长毛发笔刷特有属性 + if (propId === "furLength") { + this.setFurLength(value); + return true; + } else if (propId === "furDensity") { + this.setFurDensity(value); + return true; + } else if (propId === "furFlowFactor") { + this.setFurFlowFactor(value); + return true; + } else if (propId === "furCurvature") { + this.setFurCurvature(value); + return true; + } else if (propId === "randomizeDirection") { + this.setRandomizeDirection(value); + return true; + } + + return false; + } + + /** + * 获取预览图 + * @returns {String} 预览图URL + */ + getPreview() { + return "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMDAgMTAwIj48cGF0aCBkPSJNMjUgNTBDMjUgNTAgNTAgMTAgNTAgNTBDNTAgNTAgNTAgOTAgNzUgNTAiIHN0cm9rZT0iIzAwMCIgc3Ryb2tlLXdpZHRoPSIyIiBmaWxsPSJub25lIi8+PGxpbmUgeDE9IjMwIiB5MT0iNDUiIHgyPSIzMCIgeTI9IjEwIiBzdHJva2U9IiMwMDAiIHN0cm9rZS13aWR0aD0iMSIvPjxsaW5lIHgxPSI0MCIgeTE9IjQwIiB4Mj0iNDAiIHkyPSI1IiBzdHJva2U9IiMwMDAiIHN0cm9rZS13aWR0aD0iMSIvPjxsaW5lIHgxPSI1MCIgeTE9IjQwIiB4Mj0iNTAiIHkyPSIxMCIgc3Ryb2tlPSIjMDAwIiBzdHJva2Utd2lkdGg9IjEiLz48bGluZSB4MT0iNjAiIHkxPSI0MCIgeDI9IjYwIiB5Mj0iNSIgc3Ryb2tlPSIjMDAwIiBzdHJva2Utd2lkdGg9IjEiLz48bGluZSB4MT0iNzAiIHkxPSI0NSIgeDI9IjcwIiB5Mj0iMTAiIHN0cm9rZT0iIzAwMCIgc3Ryb2tlLXdpZHRoPSIxIi8+PC9zdmc+"; + } +} diff --git a/src/component/Canvas/CanvasEditor/managers/brushes/types/MarkerBrush.js b/src/component/Canvas/CanvasEditor/managers/brushes/types/MarkerBrush.js new file mode 100644 index 00000000..88c33b54 --- /dev/null +++ b/src/component/Canvas/CanvasEditor/managers/brushes/types/MarkerBrush.js @@ -0,0 +1,230 @@ +import { BaseBrush } from "../BaseBrush"; + +/** + * 马克笔笔刷 + * 模拟马克笔效果,具有半透明平头笔触 + */ +export class MarkerBrush extends BaseBrush { + /** + * 构造函数 + * @param {Object} canvas fabric画布实例 + * @param {Object} options 配置选项 + */ + constructor(canvas, options = {}) { + super(canvas, { + id: "marker", + name: "马克笔", + description: "模拟马克笔效果,具有半透明平头笔触", + category: "基础笔刷", + icon: "marker", + ...options, + }); + + // 马克笔特有属性 + this._baseWidth = options._baseWidth || 15; + this._lineWidth = options._lineWidth || 2; + this.capStyle = options.capStyle || "round"; // "round" 或 "square" + this.blendMode = options.blendMode || "multiply"; // 混合模式 + } + + /** + * 创建笔刷实例 + * @returns {Object} fabric笔刷实例 + */ + create() { + if (!this.canvas) { + throw new Error("画布实例不存在"); + } + + // 创建fabric原生马克笔笔刷 + this.brush = new fabric.MarkerBrush(this.canvas); + + // 配置笔刷 + this.configure(this.brush, this.options); + + return this.brush; + } + + /** + * 配置笔刷 + * @param {Object} brush fabric笔刷实例 + * @param {Object} options 配置选项 + */ + configure(brush, options = {}) { + if (!brush) return; + + // 基础属性配置 + if (options.width !== undefined) { + brush.width = options.width; + // 更新笔刷相关属性 + this._baseWidth = options.width; + } + + if (options.color !== undefined) { + brush.color = options.color; + } + + if (options.opacity !== undefined) { + // 马克笔的透明度默认不要太高 + brush.opacity = Math.min(0.8, options.opacity || 0.6); + } + + // 马克笔笔刷特有属性 + if (options._baseWidth !== undefined) { + brush._baseWidth = options._baseWidth; + this._baseWidth = options._baseWidth; + } + + if (options._lineWidth !== undefined) { + brush._lineWidth = options._lineWidth; + this._lineWidth = options._lineWidth; + } + + // 笔触样式设置 + brush.canvas.contextTop.lineJoin = "round"; + brush.canvas.contextTop.lineCap = this.capStyle || "round"; + + // 马克笔的混合模式设置 + if (this.blendMode === "multiply") { + brush.canvas.contextTop.globalCompositeOperation = "multiply"; + } else { + brush.canvas.contextTop.globalCompositeOperation = "source-over"; + } + } + + /** + * 设置笔触线宽 + * @param {Number} width 线宽值 + */ + setLineWidth(width) { + this._lineWidth = Math.max(1, Math.min(10, width)); + + if (this.brush) { + this.brush._lineWidth = this._lineWidth; + } + + return this._lineWidth; + } + + /** + * 设置笔头样式 + * @param {String} style 笔头样式 ('round' 或 'square') + */ + setCapStyle(style) { + if (style === "round" || style === "square") { + this.capStyle = style; + + if (this.brush && this.brush.canvas) { + this.brush.canvas.contextTop.lineCap = style; + } + } + + return this.capStyle; + } + + /** + * 设置混合模式 + * @param {String} mode 混合模式 ('multiply' 或 'normal') + */ + setBlendMode(mode) { + this.blendMode = mode; + + if (this.brush && this.brush.canvas) { + this.brush.canvas.contextTop.globalCompositeOperation = + mode === "multiply" ? "multiply" : "source-over"; + } + + return this.blendMode; + } + + /** + * 获取笔刷可配置属性 + * @returns {Array} 可配置属性描述数组 + * @override + */ + getConfigurableProperties() { + // 获取基础属性 + const baseProperties = super.getConfigurableProperties(); + + // 定义马克笔笔刷特有属性 + const markerProperties = [ + { + id: "lineWidth", + name: "笔触宽度", + type: "slider", + defaultValue: this._lineWidth, + min: 1, + max: 10, + step: 0.5, + description: "控制马克笔笔触的宽度", + category: "马克笔设置", + order: 100, + }, + { + id: "capStyle", + name: "笔头样式", + type: "select", + defaultValue: this.capStyle, + options: [ + { value: "round", label: "圆形" }, + { value: "square", label: "方形" }, + ], + description: "设置马克笔笔头的形状", + category: "马克笔设置", + order: 110, + }, + { + id: "blendMode", + name: "混合模式", + type: "select", + defaultValue: this.blendMode, + options: [ + { value: "multiply", label: "正片叠底" }, + { value: "normal", label: "正常" }, + ], + description: "设置马克笔的颜色混合方式", + category: "马克笔设置", + order: 120, + }, + ]; + + // 合并并返回所有属性 + return [...baseProperties, ...markerProperties]; + } + + /** + * 更新笔刷属性 + * @param {String} propId 属性ID + * @param {any} value 属性值 + * @returns {Boolean} 是否更新成功 + * @override + */ + updateProperty(propId, value) { + // 先检查基类能否处理此属性 + if (super.updateProperty(propId, value)) { + return true; + } + + // 处理马克笔笔刷特有属性 + if (propId === "lineWidth") { + this.setLineWidth(value); + return true; + } else if (propId === "capStyle") { + this.setCapStyle(value); + return true; + } else if (propId === "blendMode") { + this.setBlendMode(value); + return true; + } + + return false; + } + + /** + * 获取预览图 + * @returns {String} 预览图URL + */ + getPreview() { + return "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMDAgMTAwIj48cGF0aCBkPSJNMTAgNTBIOTAiIGZpbGw9Im5vbmUiIHN0cm9rZT0iIzAwMCIgc3Ryb2tlLXdpZHRoPSIyMCIgc3Ryb2tlLW9wYWNpdHk9IjAuNiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIi8+PC9zdmc+"; + } +} diff --git a/src/component/Canvas/CanvasEditor/managers/brushes/types/PencilBrush.js b/src/component/Canvas/CanvasEditor/managers/brushes/types/PencilBrush.js new file mode 100644 index 00000000..dc92af91 --- /dev/null +++ b/src/component/Canvas/CanvasEditor/managers/brushes/types/PencilBrush.js @@ -0,0 +1,318 @@ +import { BaseBrush } from "../BaseBrush"; + +/** + * 铅笔笔刷 + * fabric原生铅笔笔刷的包装类 + */ +export class PencilBrush extends BaseBrush { + /** + * 构造函数 + * @param {Object} canvas fabric画布实例 + * @param {Object} options 配置选项 + */ + constructor(canvas, options = {}) { + super(canvas, { + id: "pencil", + name: "铅笔", + description: "基础铅笔工具,适合精细线条绘制", + category: "基础笔刷", + icon: "pencil", + ...options, + }); + + // 铅笔笔刷特有属性 + this.decimate = options.decimate || 0.4; + this.strokeLineCap = options.strokeLineCap || "round"; + this.strokeLineJoin = options.strokeLineJoin || "round"; + } + + /** + * 创建笔刷实例 + * @returns {Object} fabric.PencilBrush实例 + */ + create() { + if (!this.canvas) { + throw new Error("画布实例不存在"); + } + + // 创建fabric原生铅笔笔刷 + this.brush = new fabric.PencilBrush(this.canvas); + + // 重写 _finalizeAndAddPath 方法,使其调用 convertToImg 而不是创建 Path 对象 + const originalFinalizeAndAddPath = this.brush._finalizeAndAddPath.bind( + this.brush + ); + const self = this; // 保存外部this引用 + + this.brush._finalizeAndAddPath = function () { + console.log("PencilBrush: _finalizeAndAddPath called"); + const ctx = this.canvas.contextTop; + ctx.closePath(); + + // 应用点简化 + if (this.decimate) { + this._points = this.decimatePoints(this._points, this.decimate); + } + + console.log( + "PencilBrush: points count =", + this._points ? this._points.length : 0 + ); + + // 检查是否有有效的路径数据 + if (!this._points || this._points.length < 2) { + // 如果点数不足,直接请求重新渲染 + console.log("PencilBrush: Not enough points, skipping"); + this.canvas.requestRenderAll(); + return; + } + + const pathData = this.convertPointsToSVGPath(this._points); + + const isEmpty = self._isEmptySVGPath(pathData); + console.log("PencilBrush: isEmpty =", isEmpty); + + if (isEmpty) { + // 如果路径为空,直接请求重新渲染 + console.log("PencilBrush: Path is empty, skipping"); + this.canvas.requestRenderAll(); + return; + } + + // 先触发事件,模拟原生行为 + const path = this.createPath(pathData); + this.canvas.fire("before:path:created", { path: path }); + + console.log("PencilBrush: Calling convertToImg"); + + // 调用 convertToImg 方法将绘制内容转换为图片 + if (typeof this.convertToImg === "function") { + this.convertToImg(); + console.log("PencilBrush: convertToImg called successfully"); + } else { + console.warn( + "convertToImg method not found, falling back to original behavior" + ); + // 如果没有convertToImg方法,回退到原始行为 + this.canvas.add(path); + this.canvas.fire("path:created", { path: path }); + this.canvas.clearContext(this.canvas.contextTop); + } + + // 重置阴影 + this._resetShadow(); + }; + + // 配置笔刷 + this.configure(this.brush, this.options); + + return this.brush; + } + + /** + * 检查 SVG 路径是否为空 + * @private + * @param {Array} pathData SVG 路径数据 + * @returns {Boolean} 是否为空路径 + */ + _isEmptySVGPath(pathData) { + if (!pathData || pathData.length === 0) { + return true; + } + + // 检查路径是否只包含移动命令或者是一个点 + let hasDrawing = false; + let moveCount = 0; + + for (let i = 0; i < pathData.length; i++) { + const command = pathData[i]; + if (command[0] === "M") { + moveCount++; + } else if ( + command[0] === "L" || + command[0] === "Q" || + command[0] === "C" + ) { + hasDrawing = true; + break; + } + } + + // 如果只有移动命令且超过1个,或者没有绘制命令,则认为是空路径 + return !hasDrawing || (moveCount > 0 && pathData.length <= moveCount); + } + + /** + * 配置笔刷 + * @param {Object} brush fabric.PencilBrush实例 + * @param {Object} options 配置选项 + */ + configure(brush, options = {}) { + if (!brush) { + return; + } + + // 基础属性配置 + if (options.width !== undefined) { + brush.width = options.width; + } + + if (options.color !== undefined) { + brush.color = options.color; + } + + if (options.opacity !== undefined) { + brush.opacity = options.opacity; + } + + // 特殊属性配置 + if (options.decimate !== undefined) { + brush.decimate = options.decimate; + this.decimate = options.decimate; + } + + if (options.strokeLineCap !== undefined) { + brush.strokeLineCap = options.strokeLineCap; + this.strokeLineCap = options.strokeLineCap; + } + + if (options.strokeLineJoin !== undefined) { + brush.strokeLineJoin = options.strokeLineJoin; + this.strokeLineJoin = options.strokeLineJoin; + } + } + + /** + * 获取笔刷可配置属性 + * @returns {Array} 可配置属性描述数组 + * @override + */ + getConfigurableProperties() { + // 获取基础属性 + const baseProperties = super.getConfigurableProperties(); + + // 定义铅笔笔刷特有属性 + const pencilProperties = [ + { + id: "decimate", + name: "精细度", + type: "slider", + defaultValue: this.decimate, + min: 0, + max: 1, + step: 0.1, + description: "控制笔触路径的简化程度,值越小路径越精细", + category: "铅笔设置", + order: 100, + }, + { + id: "strokeLineCap", + name: "线条端点", + type: "select", + defaultValue: this.strokeLineCap, + options: [ + { value: "round", label: "圆形" }, + { value: "butt", label: "平直" }, + { value: "square", label: "方形" }, + ], + description: "线条端点的形状", + category: "铅笔设置", + order: 110, + }, + { + id: "strokeLineJoin", + name: "线条连接", + type: "select", + defaultValue: this.strokeLineJoin, + options: [ + { value: "round", label: "圆角" }, + { value: "bevel", label: "斜角" }, + { value: "miter", label: "尖角" }, + ], + description: "线条拐角的连接方式", + category: "铅笔设置", + order: 120, + }, + { + id: "smoothingEnabled", + name: "启用平滑", + type: "checkbox", + defaultValue: false, + description: "是否对线条进行平滑处理", + category: "铅笔设置", + order: 130, + }, + { + id: "smoothingFactor", + name: "平滑程度", + type: "slider", + defaultValue: 0.5, + min: 0, + max: 1, + step: 0.05, + description: "线条平滑的强度", + category: "铅笔设置", + order: 140, + // 只有当smoothingEnabled为true时才显示 + visibleWhen: { smoothingEnabled: true }, + }, + ]; + + // 合并并返回所有属性 + return [...baseProperties, ...pencilProperties]; + } + + /** + * 更新笔刷属性 + * @param {String} propId 属性ID + * @param {any} value 属性值 + * @returns {Boolean} 是否更新成功 + * @override + */ + updateProperty(propId, value) { + // 先检查基类能否处理此属性 + if (super.updateProperty(propId, value)) { + return true; + } + + // 处理铅笔特有属性 + if (propId === "decimate") { + this.decimate = value; + if (this.brush) { + this.brush.decimate = value; + return true; + } + } else if (propId === "strokeLineCap") { + this.strokeLineCap = value; + if (this.brush) { + this.brush.strokeLineCap = value; + return true; + } + } else if (propId === "strokeLineJoin") { + this.strokeLineJoin = value; + if (this.brush) { + this.brush.strokeLineJoin = value; + return true; + } + } else if (propId === "smoothingEnabled") { + this.smoothingEnabled = value; + // 实现平滑逻辑... + return true; + } else if (propId === "smoothingFactor") { + this.smoothingFactor = value; + // 实现平滑度调整... + return true; + } + + return false; + } + + /** + * 获取预览图 + * @returns {String} 预览图URL + */ + getPreview() { + // 实际项目中可以返回一个实际的预览图URL + return "data:image/svg+xml;base64,..."; // 示例SVG + } +} diff --git a/src/component/Canvas/CanvasEditor/managers/brushes/types/RibbonBrush.js b/src/component/Canvas/CanvasEditor/managers/brushes/types/RibbonBrush.js new file mode 100644 index 00000000..d8a615d7 --- /dev/null +++ b/src/component/Canvas/CanvasEditor/managers/brushes/types/RibbonBrush.js @@ -0,0 +1,351 @@ +import { BaseBrush } from "../BaseBrush"; + +/** + * 丝带笔刷 + * 创建流畅的飘带状线条 + */ +export class RibbonBrush extends BaseBrush { + /** + * 构造函数 + * @param {Object} canvas fabric画布实例 + * @param {Object} options 配置选项 + */ + constructor(canvas, options = {}) { + super(canvas, { + id: "ribbon", + name: "飘带", + description: "创建流畅的飘带状线条,具有动态宽度变化和曲线美感", + category: "特效笔刷", + icon: "ribbon", + ...options, + }); + + // 丝带笔刷特有属性 + this.ribbonWidth = options.ribbonWidth || 20; + this.widthVariation = options.widthVariation || 0.5; + this.ribbonSmoothness = options.ribbonSmoothness || 0.7; + this.gradient = options.gradient !== undefined ? options.gradient : true; + this.gradientColors = options.gradientColors || ["#000000", "#555555"]; + } + + /** + * 创建笔刷实例 + * @returns {Object} fabric笔刷实例 + */ + create() { + if (!this.canvas) { + throw new Error("画布实例不存在"); + } + + // 创建fabric原生丝带笔刷 + this.brush = new fabric.RibbonBrush(this.canvas); + + // 配置笔刷 + this.configure(this.brush, this.options); + + return this.brush; + } + + /** + * 配置笔刷 + * @param {Object} brush fabric笔刷实例 + * @param {Object} options 配置选项 + */ + configure(brush, options = {}) { + if (!brush) return; + + // 基础属性配置 + if (options.width !== undefined) { + brush.width = options.width; + // 基于主宽度更新丝带宽度 + this.ribbonWidth = options.width * 2; + } + + if (options.color !== undefined) { + brush.color = options.color; + + // 如果启用渐变,更新渐变的第一个颜色 + if (this.gradient && this.gradientColors.length > 0) { + this.gradientColors[0] = options.color; + this.updateGradient(); + } + } + + if (options.opacity !== undefined) { + brush.opacity = options.opacity; + } + + // 丝带笔刷特有属性 + if (options.ribbonWidth !== undefined) { + this.ribbonWidth = options.ribbonWidth; + + // 如果原生笔刷支持此属性 + if (brush.ribbonWidth !== undefined) { + brush.ribbonWidth = this.ribbonWidth; + } + } + + if (options.widthVariation !== undefined) { + this.widthVariation = options.widthVariation; + + // 如果原生笔刷支持此属性 + if (brush.widthVariation !== undefined) { + brush.widthVariation = this.widthVariation; + } + } + + if (options.ribbonSmoothness !== undefined) { + this.ribbonSmoothness = options.ribbonSmoothness; + + // 如果原生笔刷支持此属性 + if (brush.ribbonSmoothness !== undefined) { + brush.ribbonSmoothness = this.ribbonSmoothness; + } + } + + if (options.gradient !== undefined) { + this.gradient = options.gradient; + this.updateGradient(); + } + + if (options.gradientColors !== undefined) { + this.gradientColors = options.gradientColors; + this.updateGradient(); + } + } + + /** + * 更新渐变设置 + * @private + */ + updateGradient() { + if (!this.brush || !this.canvas) return; + + if (this.gradient && this.gradientColors.length >= 2) { + // 创建渐变对象 + const ctx = this.canvas.contextTop; + const gradient = ctx.createLinearGradient(0, 0, this.ribbonWidth, 0); + + // 添加渐变色 + const colorCount = this.gradientColors.length; + this.gradientColors.forEach((color, index) => { + gradient.addColorStop(index / (colorCount - 1), color); + }); + + // 如果原生笔刷支持渐变 + if (typeof this.brush.setGradient === "function") { + this.brush.setGradient(gradient); + } else if (this.brush.gradient !== undefined) { + this.brush.gradient = gradient; + } + + // 如果原生笔刷支持渐变标志 + if (this.brush.useGradient !== undefined) { + this.brush.useGradient = true; + } + } else if (this.brush.useGradient !== undefined) { + // 禁用渐变 + this.brush.useGradient = false; + } + } + + /** + * 设置丝带宽度 + * @param {Number} width 宽度值 + */ + setRibbonWidth(width) { + this.ribbonWidth = Math.max(5, Math.min(100, width)); + + if (this.brush && this.brush.ribbonWidth !== undefined) { + this.brush.ribbonWidth = this.ribbonWidth; + } + + // 更新渐变(因为宽度变了) + if (this.gradient) { + this.updateGradient(); + } + + return this.ribbonWidth; + } + + /** + * 设置宽度变化率 + * @param {Number} variation 变化率(0-1) + */ + setWidthVariation(variation) { + this.widthVariation = Math.max(0, Math.min(1, variation)); + + if (this.brush && this.brush.widthVariation !== undefined) { + this.brush.widthVariation = this.widthVariation; + } + + return this.widthVariation; + } + + /** + * 设置丝带平滑度 + * @param {Number} smoothness 平滑度值(0-1) + */ + setRibbonSmoothness(smoothness) { + this.ribbonSmoothness = Math.max(0, Math.min(1, smoothness)); + + if (this.brush && this.brush.ribbonSmoothness !== undefined) { + this.brush.ribbonSmoothness = this.ribbonSmoothness; + } + + return this.ribbonSmoothness; + } + + /** + * 启用/禁用渐变效果 + * @param {Boolean} enabled 是否启用 + */ + setGradient(enabled) { + this.gradient = enabled; + this.updateGradient(); + return this.gradient; + } + + /** + * 设置渐变颜色 + * @param {Array} colors 颜色数组 + */ + setGradientColors(colors) { + if (Array.isArray(colors) && colors.length >= 2) { + this.gradientColors = colors; + this.updateGradient(); + } + return this.gradientColors; + } + + /** + * 获取笔刷可配置属性 + * @returns {Array} 可配置属性描述数组 + * @override + */ + getConfigurableProperties() { + // 获取基础属性 + const baseProperties = super.getConfigurableProperties(); + + // 定义丝带笔刷特有属性 + const ribbonProperties = [ + { + id: "ribbonWidth", + name: "飘带宽度", + type: "slider", + defaultValue: this.ribbonWidth, + min: 5, + max: 100, + step: 5, + description: "控制飘带的最大宽度", + category: "飘带设置", + order: 100, + }, + { + id: "widthVariation", + name: "宽度变化", + type: "slider", + defaultValue: this.widthVariation, + min: 0, + max: 1, + step: 0.05, + description: "控制飘带宽度的变化程度", + category: "飘带设置", + order: 110, + }, + { + id: "ribbonSmoothness", + name: "平滑度", + type: "slider", + defaultValue: this.ribbonSmoothness, + min: 0, + max: 1, + step: 0.05, + description: "控制飘带曲线的平滑程度", + category: "飘带设置", + order: 120, + }, + { + id: "gradient", + name: "启用渐变", + type: "checkbox", + defaultValue: this.gradient, + description: "是否启用渐变效果", + category: "飘带设置", + order: 130, + }, + { + id: "gradientColor1", + name: "渐变起始颜色", + type: "color", + defaultValue: this.gradientColors[0] || "#000000", + description: "设置渐变的起始颜色", + category: "飘带设置", + order: 140, + visibleWhen: { gradient: true }, + }, + { + id: "gradientColor2", + name: "渐变结束颜色", + type: "color", + defaultValue: this.gradientColors[1] || "#555555", + description: "设置渐变的结束颜色", + category: "飘带设置", + order: 150, + visibleWhen: { gradient: true }, + }, + ]; + + // 合并并返回所有属性 + return [...baseProperties, ...ribbonProperties]; + } + + /** + * 更新笔刷属性 + * @param {String} propId 属性ID + * @param {any} value 属性值 + * @returns {Boolean} 是否更新成功 + * @override + */ + updateProperty(propId, value) { + // 先检查基类能否处理此属性 + if (super.updateProperty(propId, value)) { + return true; + } + + // 处理丝带笔刷特有属性 + if (propId === "ribbonWidth") { + this.setRibbonWidth(value); + return true; + } else if (propId === "widthVariation") { + this.setWidthVariation(value); + return true; + } else if (propId === "ribbonSmoothness") { + this.setRibbonSmoothness(value); + return true; + } else if (propId === "gradient") { + this.setGradient(value); + return true; + } else if (propId === "gradientColor1") { + const colors = [...this.gradientColors]; + colors[0] = value; + this.setGradientColors(colors); + return true; + } else if (propId === "gradientColor2") { + const colors = [...this.gradientColors]; + colors[1] = value; + this.setGradientColors(colors); + return true; + } + + return false; + } + + /** + * 获取预览图 + * @returns {String} 预览图URL + */ + getPreview() { + return "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMDAgMTAwIj48ZGVmcz48bGluZWFyR3JhZGllbnQgaWQ9ImdyYWQiIHgxPSIwJSIgeTE9IjAlIiB4Mj0iMTAwJSIgeTI9IjAlIj48c3RvcCBvZmZzZXQ9IjAlIiBzdG9wLWNvbG9yPSIjMDAwIi8+PHN0b3Agb2Zmc2V0PSIxMDAlIiBzdG9wLWNvbG9yPSIjNTU1Ii8+PC9saW5lYXJHcmFkaWVudD48L2RlZnM+PHBhdGggZD0iTTIwIDUwQzMwIDMwIDUwIDMwIDYwIDUwQzcwIDcwIDgwIDcwIDkwIDUwIiBzdHJva2U9InVybCgjZ3JhZCkiIHN0cm9rZS13aWR0aD0iMTAiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgZmlsbD0ibm9uZSIvPjwvc3ZnPg=="; + } +} diff --git a/src/component/Canvas/CanvasEditor/managers/brushes/types/ShadedBrush.js b/src/component/Canvas/CanvasEditor/managers/brushes/types/ShadedBrush.js new file mode 100644 index 00000000..a12f4d2f --- /dev/null +++ b/src/component/Canvas/CanvasEditor/managers/brushes/types/ShadedBrush.js @@ -0,0 +1,362 @@ +import { BaseBrush } from "../BaseBrush"; + +/** + * 阴影笔刷 + * 创建带有阴影效果的绘制,有深浅变化 + */ +export class ShadedBrush extends BaseBrush { + /** + * 构造函数 + * @param {Object} canvas fabric画布实例 + * @param {Object} options 配置选项 + */ + constructor(canvas, options = {}) { + super(canvas, { + id: "shaded", + name: "阴影笔", + description: "创建带有阴影效果的绘制,适合素描和明暗表现", + category: "绘画笔刷", + icon: "shaded", + ...options, + }); + + // 阴影笔刷特有属性 + this.shadowColor = options.shadowColor || "#000000"; + this.shadowBlur = options.shadowBlur || 5; + this.shadowOffsetX = options.shadowOffsetX || 2; + this.shadowOffsetY = options.shadowOffsetY || 2; + this.blendMode = options.blendMode || "multiply"; + } + + /** + * 创建笔刷实例 + * @returns {Object} fabric笔刷实例 + */ + create() { + if (!this.canvas) { + throw new Error("画布实例不存在"); + } + + // 创建fabric原生阴影笔刷 + this.brush = new fabric.ShadedBrush(this.canvas); + + // 配置笔刷 + this.configure(this.brush, this.options); + + return this.brush; + } + + /** + * 配置笔刷 + * @param {Object} brush fabric笔刷实例 + * @param {Object} options 配置选项 + */ + configure(brush, options = {}) { + if (!brush) return; + + // 基础属性配置 + if (options.width !== undefined) { + brush.width = options.width; + } + + if (options.color !== undefined) { + brush.color = options.color; + } + + if (options.opacity !== undefined) { + brush.opacity = options.opacity; + } + + // 阴影笔刷特有属性 + if (options.shadowColor !== undefined) { + this.shadowColor = options.shadowColor; + + // 如果原生笔刷支持此属性,则设置 + if (brush.shadow) { + brush.shadow.color = this.shadowColor; + } else { + brush.shadow = new fabric.Shadow({ + color: this.shadowColor, + blur: this.shadowBlur, + offsetX: this.shadowOffsetX, + offsetY: this.shadowOffsetY, + }); + } + } + + if (options.shadowBlur !== undefined) { + this.shadowBlur = options.shadowBlur; + + // 如果原生笔刷支持此属性,则设置 + if (brush.shadow) { + brush.shadow.blur = this.shadowBlur; + } else { + brush.shadow = new fabric.Shadow({ + color: this.shadowColor, + blur: this.shadowBlur, + offsetX: this.shadowOffsetX, + offsetY: this.shadowOffsetY, + }); + } + } + + if (options.shadowOffsetX !== undefined) { + this.shadowOffsetX = options.shadowOffsetX; + + // 如果原生笔刷支持此属性,则设置 + if (brush.shadow) { + brush.shadow.offsetX = this.shadowOffsetX; + } else { + brush.shadow = new fabric.Shadow({ + color: this.shadowColor, + blur: this.shadowBlur, + offsetX: this.shadowOffsetX, + offsetY: this.shadowOffsetY, + }); + } + } + + if (options.shadowOffsetY !== undefined) { + this.shadowOffsetY = options.shadowOffsetY; + + // 如果原生笔刷支持此属性,则设置 + if (brush.shadow) { + brush.shadow.offsetY = this.shadowOffsetY; + } else { + brush.shadow = new fabric.Shadow({ + color: this.shadowColor, + blur: this.shadowBlur, + offsetX: this.shadowOffsetX, + offsetY: this.shadowOffsetY, + }); + } + } + + if (options.blendMode !== undefined) { + this.blendMode = options.blendMode; + + // 如果原生笔刷支持此属性,则设置 + if (brush.globalCompositeOperation !== undefined) { + brush.globalCompositeOperation = this.blendMode; + } + } + } + + /** + * 设置阴影颜色 + * @param {String} color 颜色值 + */ + setShadowColor(color) { + this.shadowColor = color; + + if (this.brush && this.brush.shadow) { + this.brush.shadow.color = this.shadowColor; + } + + return this.shadowColor; + } + + /** + * 设置阴影模糊值 + * @param {Number} blur 模糊值 + */ + setShadowBlur(blur) { + this.shadowBlur = Math.max(0, Math.min(50, blur)); + + if (this.brush && this.brush.shadow) { + this.brush.shadow.blur = this.shadowBlur; + } + + return this.shadowBlur; + } + + /** + * 设置阴影X偏移 + * @param {Number} offset X偏移值 + */ + setShadowOffsetX(offset) { + this.shadowOffsetX = Math.max(-20, Math.min(20, offset)); + + if (this.brush && this.brush.shadow) { + this.brush.shadow.offsetX = this.shadowOffsetX; + } + + return this.shadowOffsetX; + } + + /** + * 设置阴影Y偏移 + * @param {Number} offset Y偏移值 + */ + setShadowOffsetY(offset) { + this.shadowOffsetY = Math.max(-20, Math.min(20, offset)); + + if (this.brush && this.brush.shadow) { + this.brush.shadow.offsetY = this.shadowOffsetY; + } + + return this.shadowOffsetY; + } + + /** + * 设置混合模式 + * @param {String} mode 混合模式 + */ + setBlendMode(mode) { + const validModes = [ + "normal", + "multiply", + "screen", + "overlay", + "darken", + "lighten", + "color-dodge", + "color-burn", + "hard-light", + "soft-light", + "difference", + "exclusion", + "hue", + "saturation", + "color", + "luminosity", + ]; + + if (validModes.includes(mode)) { + this.blendMode = mode; + + if (this.brush && this.brush.globalCompositeOperation !== undefined) { + this.brush.globalCompositeOperation = this.blendMode; + } + } + + return this.blendMode; + } + + /** + * 获取笔刷可配置属性 + * @returns {Array} 可配置属性描述数组 + * @override + */ + getConfigurableProperties() { + // 获取基础属性 + const baseProperties = super.getConfigurableProperties(); + + // 定义阴影笔刷特有属性 + const shadedProperties = [ + { + id: "shadowColor", + name: "阴影颜色", + type: "color", + defaultValue: this.shadowColor, + description: "设置阴影的颜色", + category: "阴影设置", + order: 100, + }, + { + id: "shadowBlur", + name: "阴影模糊", + type: "slider", + defaultValue: this.shadowBlur, + min: 0, + max: 50, + step: 1, + description: "控制阴影的模糊程度", + category: "阴影设置", + order: 110, + }, + { + id: "shadowOffsetX", + name: "阴影X偏移", + type: "slider", + defaultValue: this.shadowOffsetX, + min: -20, + max: 20, + step: 1, + description: "控制阴影的水平偏移", + category: "阴影设置", + order: 120, + }, + { + id: "shadowOffsetY", + name: "阴影Y偏移", + type: "slider", + defaultValue: this.shadowOffsetY, + min: -20, + max: 20, + step: 1, + description: "控制阴影的垂直偏移", + category: "阴影设置", + order: 130, + }, + { + id: "blendMode", + name: "混合模式", + type: "select", + defaultValue: this.blendMode, + options: [ + { value: "normal", label: "正常" }, + { value: "multiply", label: "正片叠底" }, + { value: "screen", label: "滤色" }, + { value: "overlay", label: "叠加" }, + { value: "darken", label: "变暗" }, + { value: "lighten", label: "变亮" }, + { value: "color-dodge", label: "颜色减淡" }, + { value: "color-burn", label: "颜色加深" }, + { value: "hard-light", label: "强光" }, + { value: "soft-light", label: "柔光" }, + { value: "difference", label: "差值" }, + { value: "exclusion", label: "排除" }, + ], + description: "设置阴影的混合模式", + category: "阴影设置", + order: 140, + }, + ]; + + // 合并并返回所有属性 + return [...baseProperties, ...shadedProperties]; + } + + /** + * 更新笔刷属性 + * @param {String} propId 属性ID + * @param {any} value 属性值 + * @returns {Boolean} 是否更新成功 + * @override + */ + updateProperty(propId, value) { + // 先检查基类能否处理此属性 + if (super.updateProperty(propId, value)) { + return true; + } + + // 处理阴影笔刷特有属性 + if (propId === "shadowColor") { + this.setShadowColor(value); + return true; + } else if (propId === "shadowBlur") { + this.setShadowBlur(value); + return true; + } else if (propId === "shadowOffsetX") { + this.setShadowOffsetX(value); + return true; + } else if (propId === "shadowOffsetY") { + this.setShadowOffsetY(value); + return true; + } else if (propId === "blendMode") { + this.setBlendMode(value); + return true; + } + + return false; + } + + /** + * 获取预览图 + * @returns {String} 预览图URL + */ + getPreview() { + return "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMDAgMTAwIj48Y2lyY2xlIGN4PSI0MCIgY3k9IjQwIiByPSIyMCIgZmlsbD0iIzY2NiIvPjxjaXJjbGUgY3g9IjQ1IiBjeT0iNDUiIHI9IjIwIiBmaWxsPSIjMDAwIi8+PHBhdGggZD0iTTIwIDgwQzMwIDYwIDUwIDcwIDcwIDUwIiBzdHJva2U9IiMwMDAiIHN0cm9rZS13aWR0aD0iOCIgZmlsbD0ibm9uZSIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIi8+PHBhdGggZD0iTTIzIDgzQzMzIDYzIDUzIDczIDczIDUzIiBzdHJva2U9IiM2NjYiIHN0cm9rZS13aWR0aD0iOCIgZmlsbD0ibm9uZSIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIi8+PC9zdmc+"; + } +} diff --git a/src/component/Canvas/CanvasEditor/managers/brushes/types/SketchyBrush.js b/src/component/Canvas/CanvasEditor/managers/brushes/types/SketchyBrush.js new file mode 100644 index 00000000..f633132a --- /dev/null +++ b/src/component/Canvas/CanvasEditor/managers/brushes/types/SketchyBrush.js @@ -0,0 +1,371 @@ +import { BaseBrush } from "../BaseBrush"; + +/** + * 素描笔刷 + * 创建手绘素描效果,有不规则的线条和纹理 + */ +export class SketchyBrush extends BaseBrush { + /** + * 构造函数 + * @param {Object} canvas fabric画布实例 + * @param {Object} options 配置选项 + */ + constructor(canvas, options = {}) { + super(canvas, { + id: "sketchy", + name: "素描", + description: "创建手绘素描效果,有不规则的线条和纹理", + category: "绘画笔刷", + icon: "sketchy", + ...options, + }); + + // 素描笔刷特有属性 + this.roughness = options.roughness || 0.7; + this.bowing = options.bowing || 0.5; + this.stroke = options.stroke !== undefined ? options.stroke : true; + this.hachureAngle = options.hachureAngle || 60; + this.dashOffset = options.dashOffset || 0; + this.dashArray = options.dashArray || [6, 2]; + } + + /** + * 创建笔刷实例 + * @returns {Object} fabric笔刷实例 + */ + create() { + if (!this.canvas) { + throw new Error("画布实例不存在"); + } + + // 创建fabric原生素描笔刷 + this.brush = new fabric.SketchyBrush(this.canvas); + + // 配置笔刷 + this.configure(this.brush, this.options); + + return this.brush; + } + + /** + * 配置笔刷 + * @param {Object} brush fabric笔刷实例 + * @param {Object} options 配置选项 + */ + configure(brush, options = {}) { + if (!brush) return; + + // 基础属性配置 + if (options.width !== undefined) { + brush.width = options.width; + } + + if (options.color !== undefined) { + brush.color = options.color; + } + + if (options.opacity !== undefined) { + brush.opacity = options.opacity; + } + + // 素描笔刷特有属性 + if (options.roughness !== undefined) { + this.roughness = options.roughness; + + // 如果原生笔刷支持此属性,则设置 + if (brush.roughness !== undefined) { + brush.roughness = this.roughness; + } + } + + if (options.bowing !== undefined) { + this.bowing = options.bowing; + + // 如果原生笔刷支持此属性,则设置 + if (brush.bowing !== undefined) { + brush.bowing = this.bowing; + } + } + + if (options.stroke !== undefined) { + this.stroke = options.stroke; + + // 如果原生笔刷支持此属性,则设置 + if (brush.stroke !== undefined) { + brush.stroke = this.stroke; + } + } + + if (options.hachureAngle !== undefined) { + this.hachureAngle = options.hachureAngle; + + // 如果原生笔刷支持此属性,则设置 + if (brush.hachureAngle !== undefined) { + brush.hachureAngle = this.hachureAngle; + } + } + + if (options.dashOffset !== undefined) { + this.dashOffset = options.dashOffset; + + // 如果原生笔刷支持此属性,则设置 + if (brush.dashOffset !== undefined) { + brush.dashOffset = this.dashOffset; + } + } + + if (options.dashArray !== undefined) { + this.dashArray = options.dashArray; + + // 如果原生笔刷支持此属性,则设置 + if (brush.dashArray !== undefined) { + brush.dashArray = this.dashArray; + } + } + + // 为笔刷设置手绘效果 + const originalOnMouseMove = brush.onMouseMove; + brush.onMouseMove = function (pointer, options) { + // 添加微小随机偏移,模拟手绘效果 + const jitter = (this.width / 4) * this.roughness; + pointer.x += (Math.random() - 0.5) * jitter; + pointer.y += (Math.random() - 0.5) * jitter; + + // 调用原始方法 + if (originalOnMouseMove) { + originalOnMouseMove.call(this, pointer, options); + } + }; + } + + /** + * 设置粗糙度 + * @param {Number} value 粗糙度值(0-1) + */ + setRoughness(value) { + this.roughness = Math.max(0, Math.min(1, value)); + + if (this.brush && this.brush.roughness !== undefined) { + this.brush.roughness = this.roughness; + } + + return this.roughness; + } + + /** + * 设置弯曲度 + * @param {Number} value 弯曲度值(0-1) + */ + setBowing(value) { + this.bowing = Math.max(0, Math.min(1, value)); + + if (this.brush && this.brush.bowing !== undefined) { + this.brush.bowing = this.bowing; + } + + return this.bowing; + } + + /** + * 设置是否描边 + * @param {Boolean} value 是否描边 + */ + setStroke(value) { + this.stroke = value; + + if (this.brush && this.brush.stroke !== undefined) { + this.brush.stroke = this.stroke; + } + + return this.stroke; + } + + /** + * 设置素描线条角度 + * @param {Number} value 角度值(0-180) + */ + setHachureAngle(value) { + this.hachureAngle = Math.max(0, Math.min(180, value)); + + if (this.brush && this.brush.hachureAngle !== undefined) { + this.brush.hachureAngle = this.hachureAngle; + } + + return this.hachureAngle; + } + + /** + * 设置虚线偏移量 + * @param {Number} value 偏移量 + */ + setDashOffset(value) { + this.dashOffset = value; + + if (this.brush && this.brush.dashOffset !== undefined) { + this.brush.dashOffset = this.dashOffset; + } + + return this.dashOffset; + } + + /** + * 设置虚线数组 + * @param {Array} value 虚线数组[线长, 间隔] + */ + setDashArray(value) { + if (Array.isArray(value) && value.length >= 2) { + this.dashArray = value; + + if (this.brush && this.brush.dashArray !== undefined) { + this.brush.dashArray = this.dashArray; + } + } + + return this.dashArray; + } + + /** + * 获取笔刷可配置属性 + * @returns {Array} 可配置属性描述数组 + * @override + */ + getConfigurableProperties() { + // 获取基础属性 + const baseProperties = super.getConfigurableProperties(); + + // 定义素描笔刷特有属性 + const sketchyProperties = [ + { + id: "roughness", + name: "粗糙度", + type: "slider", + defaultValue: this.roughness, + min: 0, + max: 1, + step: 0.05, + description: "控制素描线条的粗糙程度", + category: "素描设置", + order: 100, + }, + { + id: "bowing", + name: "弯曲度", + type: "slider", + defaultValue: this.bowing, + min: 0, + max: 1, + step: 0.05, + description: "控制素描线条的弯曲程度", + category: "素描设置", + order: 110, + }, + { + id: "stroke", + name: "描边", + type: "checkbox", + defaultValue: this.stroke, + description: "是否使用描边", + category: "素描设置", + order: 120, + }, + { + id: "hachureAngle", + name: "线条角度", + type: "slider", + defaultValue: this.hachureAngle, + min: 0, + max: 180, + step: 5, + description: "控制素描线条的角度", + category: "素描设置", + order: 130, + }, + { + id: "dashOffset", + name: "虚线偏移", + type: "slider", + defaultValue: this.dashOffset, + min: 0, + max: 10, + step: 1, + description: "控制虚线的偏移量", + category: "素描设置", + order: 140, + }, + { + id: "dashArray", + name: "虚线模式", + type: "select", + defaultValue: JSON.stringify(this.dashArray), + options: [ + { value: JSON.stringify([0]), label: "实线" }, + { value: JSON.stringify([6, 2]), label: "短虚线" }, + { value: JSON.stringify([10, 5]), label: "长虚线" }, + { value: JSON.stringify([2, 2]), label: "点线" }, + { value: JSON.stringify([10, 5, 2, 5]), label: "点划线" }, + ], + description: "设置虚线的模式", + category: "素描设置", + order: 150, + parseValue: (value) => JSON.parse(value), + }, + ]; + + // 合并并返回所有属性 + return [...baseProperties, ...sketchyProperties]; + } + + /** + * 更新笔刷属性 + * @param {String} propId 属性ID + * @param {any} value 属性值 + * @returns {Boolean} 是否更新成功 + * @override + */ + updateProperty(propId, value) { + // 先检查基类能否处理此属性 + if (super.updateProperty(propId, value)) { + return true; + } + + // 处理素描笔刷特有属性 + if (propId === "roughness") { + this.setRoughness(value); + return true; + } else if (propId === "bowing") { + this.setBowing(value); + return true; + } else if (propId === "stroke") { + this.setStroke(value); + return true; + } else if (propId === "hachureAngle") { + this.setHachureAngle(value); + return true; + } else if (propId === "dashOffset") { + this.setDashOffset(value); + return true; + } else if (propId === "dashArray") { + let parsedValue = value; + if (typeof value === "string") { + try { + parsedValue = JSON.parse(value); + } catch (e) { + console.error("Invalid dashArray value:", e); + return false; + } + } + this.setDashArray(parsedValue); + return true; + } + + return false; + } + + /** + * 获取预览图 + * @returns {String} 预览图URL + */ + getPreview() { + return "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMDAgMTAwIj48cGF0aCBkPSJNMjMgMzBDMjUgMjggNTIgMzggNzUgMzciIHN0cm9rZT0iIzAwMCIgc3Ryb2tlLXdpZHRoPSIzIiBmaWxsPSJub25lIiBzdHJva2UtbGluZWNhcD0icm91bmQiLz48cGF0aCBkPSJNMjIgNDBDMjIgMzggNTkgNDYgNzYgNDMiIHN0cm9rZT0iIzAwMCIgc3Ryb2tlLXdpZHRoPSIyIiBmaWxsPSJub25lIiBzdHJva2UtbGluZWNhcD0icm91bmQiLz48cGF0aCBkPSJNMjAgNTBDMjIgNDggNTYgNTYgNzYgNTIiIHN0cm9rZT0iIzAwMCIgc3Ryb2tlLXdpZHRoPSIyIiBmaWxsPSJub25lIiBzdHJva2UtbGluZWNhcD0icm91bmQiLz48cGF0aCBkPSJNMjQgNjBDMjMgNTggNDYgNjQgNzUgNjQiIHN0cm9rZT0iIzAwMCIgc3Ryb2tlLXdpZHRoPSIyLjgiIGZpbGw9Im5vbmUiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIvPjxwYXRoIGQ9Ik0yNiA3MkMyNyA2OSA0OSA3NCA3NSA3MiIgc3Ryb2tlPSIjMDAwIiBzdHJva2Utd2lkdGg9IjIuNCIgZmlsbD0ibm9uZSIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIi8+PC9zdmc+"; + } +} diff --git a/src/component/Canvas/CanvasEditor/managers/brushes/types/SpraypaintBrush.js b/src/component/Canvas/CanvasEditor/managers/brushes/types/SpraypaintBrush.js new file mode 100644 index 00000000..4f0d107f --- /dev/null +++ b/src/component/Canvas/CanvasEditor/managers/brushes/types/SpraypaintBrush.js @@ -0,0 +1,316 @@ +import { BaseBrush } from "../BaseBrush"; + +/** + * 喷漆笔刷 + * 创建喷漆效果,点状分散的绘制风格 + */ +export class SpraypaintBrush extends BaseBrush { + /** + * 构造函数 + * @param {Object} canvas fabric画布实例 + * @param {Object} options 配置选项 + */ + constructor(canvas, options = {}) { + super(canvas, { + id: "spraypaint", + name: "喷漆笔刷", + description: "创建喷漆效果,点状分散的绘制风格", + category: "绘画笔刷", + icon: "spraypaint", + ...options, + }); + + // 喷漆笔刷特有属性 + this.density = options.density || 20; + this.sprayRadius = options.sprayRadius || 10; + this.randomOpacity = + options.randomOpacity !== undefined ? options.randomOpacity : true; + this.dotSize = options.dotSize || 1; + this.dotShape = options.dotShape || "circle"; + } + + /** + * 创建笔刷实例 + * @returns {Object} fabric笔刷实例 + */ + create() { + if (!this.canvas) { + throw new Error("画布实例不存在"); + } + + // 创建fabric原生喷漆笔刷 + this.brush = new fabric.SprayBrush(this.canvas); + + // 配置笔刷 + this.configure(this.brush, this.options); + + return this.brush; + } + + /** + * 配置笔刷 + * @param {Object} brush fabric笔刷实例 + * @param {Object} options 配置选项 + */ + configure(brush, options = {}) { + if (!brush) return; + + // 设置基本属性 + if (options.width !== undefined) { + brush.width = options.width; + } + + if (options.color !== undefined) { + brush.color = options.color; + } + + if (options.opacity !== undefined) { + brush.opacity = options.opacity; + } + + // 喷漆笔刷特有属性 + if (options.density !== undefined) { + this.density = options.density; + + // 如果原生笔刷支持此属性,则设置 + if (brush.density !== undefined) { + brush.density = this.density; + } + } + + if (options.sprayRadius !== undefined) { + this.sprayRadius = options.sprayRadius; + + // 如果原生笔刷支持此属性,则设置 + if (brush.sprayWidth !== undefined) { + brush.sprayWidth = this.sprayRadius; + } else if (brush.width !== undefined) { + brush.width = this.sprayRadius; + } + } + + if (options.randomOpacity !== undefined) { + this.randomOpacity = options.randomOpacity; + + // 如果原生笔刷支持此属性,则设置 + if (brush.randomOpacity !== undefined) { + brush.randomOpacity = this.randomOpacity; + } + } + + if (options.dotSize !== undefined) { + this.dotSize = options.dotSize; + + // 如果原生笔刷支持此属性,则设置 + if (brush.dotWidth !== undefined) { + brush.dotWidth = this.dotSize; + } + } + + if (options.dotShape !== undefined) { + this.dotShape = options.dotShape; + + // 如果原生笔刷支持此属性,则设置 + if (brush.dotShape !== undefined) { + brush.dotShape = this.dotShape; + } + } + } + + /** + * 设置喷漆密度 + * @param {Number} value 密度值 + */ + setDensity(value) { + this.density = Math.max(1, Math.min(100, value)); + + if (this.brush && this.brush.density !== undefined) { + this.brush.density = this.density; + } + + return this.density; + } + + /** + * 设置喷漆半径 + * @param {Number} value 半径值 + */ + setSprayRadius(value) { + this.sprayRadius = Math.max(1, value); + + if (this.brush) { + if (this.brush.sprayWidth !== undefined) { + this.brush.sprayWidth = this.sprayRadius; + } else if (this.brush.width !== undefined) { + this.brush.width = this.sprayRadius; + } + } + + return this.sprayRadius; + } + + /** + * 设置是否随机透明度 + * @param {Boolean} value 是否随机透明度 + */ + setRandomOpacity(value) { + this.randomOpacity = value; + + if (this.brush && this.brush.randomOpacity !== undefined) { + this.brush.randomOpacity = this.randomOpacity; + } + + return this.randomOpacity; + } + + /** + * 设置点大小 + * @param {Number} value 点大小 + */ + setDotSize(value) { + this.dotSize = Math.max(0.1, value); + + if (this.brush && this.brush.dotWidth !== undefined) { + this.brush.dotWidth = this.dotSize; + } + + return this.dotSize; + } + + /** + * 设置点形状 + * @param {String} value 点形状,如 'circle', 'square', 'diamond' + */ + setDotShape(value) { + const validShapes = ["circle", "square", "diamond", "random"]; + + if (validShapes.includes(value)) { + this.dotShape = value; + + if (this.brush && this.brush.dotShape !== undefined) { + this.brush.dotShape = this.dotShape; + } + } + + return this.dotShape; + } + + /** + * 获取笔刷可配置属性 + * @returns {Array} 可配置属性描述数组 + * @override + */ + getConfigurableProperties() { + // 获取基础属性 + const baseProperties = super.getConfigurableProperties(); + + // 定义喷漆笔刷特有属性 + const spraypaintProperties = [ + { + id: "density", + name: "喷漆密度", + type: "slider", + defaultValue: this.density, + min: 1, + max: 100, + step: 1, + description: "控制喷漆点的密度", + category: "喷漆设置", + order: 100, + }, + { + id: "sprayRadius", + name: "喷漆半径", + type: "slider", + defaultValue: this.sprayRadius, + min: 1, + max: 50, + step: 1, + description: "控制喷漆的覆盖半径", + category: "喷漆设置", + order: 110, + }, + { + id: "randomOpacity", + name: "随机透明度", + type: "checkbox", + defaultValue: this.randomOpacity, + description: "使喷漆点有随机透明度", + category: "喷漆设置", + order: 120, + }, + { + id: "dotSize", + name: "点大小", + type: "slider", + defaultValue: this.dotSize, + min: 0.1, + max: 10, + step: 0.1, + description: "控制喷漆点的大小", + category: "喷漆设置", + order: 130, + }, + { + id: "dotShape", + name: "点形状", + type: "select", + defaultValue: this.dotShape, + options: [ + { value: "circle", label: "圆形" }, + { value: "square", label: "方形" }, + { value: "diamond", label: "菱形" }, + { value: "random", label: "随机" }, + ], + description: "设置喷漆点的形状", + category: "喷漆设置", + order: 140, + }, + ]; + + // 合并并返回所有属性 + return [...baseProperties, ...spraypaintProperties]; + } + + /** + * 更新笔刷属性 + * @param {String} propId 属性ID + * @param {any} value 属性值 + * @returns {Boolean} 是否更新成功 + * @override + */ + updateProperty(propId, value) { + // 先检查基类能否处理此属性 + if (super.updateProperty(propId, value)) { + return true; + } + + // 处理喷漆笔刷特有属性 + if (propId === "density") { + this.setDensity(value); + return true; + } else if (propId === "sprayRadius") { + this.setSprayRadius(value); + return true; + } else if (propId === "randomOpacity") { + this.setRandomOpacity(value); + return true; + } else if (propId === "dotSize") { + this.setDotSize(value); + return true; + } else if (propId === "dotShape") { + this.setDotShape(value); + return true; + } + + return false; + } + + /** + * 获取预览图 + * @returns {String} 预览图URL + */ + getPreview() { + return "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMDAgMTAwIj48Y2lyY2xlIGN4PSI1NSIgY3k9IjUwIiByPSIyMCIgZmlsbD0icmdiYSgwLDAsMCwwLjEpIiBzdHJva2U9IiMwMDAiIHN0cm9rZS13aWR0aD0iMC41Ii8+PGNpcmNsZSBjeD0iNTAiIGN5PSI1MCIgcj0iMSIgZmlsbD0iIzAwMCIvPjxjaXJjbGUgY3g9IjU1IiBjeT0iNTUiIHI9IjAuOCIgZmlsbD0iIzAwMCIvPjxjaXJjbGUgY3g9IjYwIiBjeT0iNDUiIHI9IjEuMiIgZmlsbD0iIzAwMCIvPjxjaXJjbGUgY3g9IjQ1IiBjeT0iNTUiIHI9IjAuNyIgZmlsbD0iIzAwMCIvPjxjaXJjbGUgY3g9IjQ3IiBjeT0iNDgiIHI9IjAuOSIgZmlsbD0iIzAwMCIvPjxjaXJjbGUgY3g9IjU4IiBjeT0iNTMiIHI9IjEuMSIgZmlsbD0iIzAwMCIvPjxjaXJjbGUgY3g9IjYyIiBjeT0iNTYiIHI9IjAuNiIgZmlsbD0iIzAwMCIvPjxjaXJjbGUgY3g9IjUyIiBjeT0iNTgiIHI9IjAuOCIgZmlsbD0iIzAwMCIvPjxjaXJjbGUgY3g9IjU0IiBjeT0iNDMiIHI9IjEiIGZpbGw9IiMwMDAiLz48Y2lyY2xlIGN4PSI0OSIgY3k9IjQzIiByPSIwLjYiIGZpbGw9IiMwMDAiLz48Y2lyY2xlIGN4PSI0MyIgY3k9IjQ3IiByPSIwLjciIGZpbGw9IiMwMDAiLz48Y2lyY2xlIGN4PSI2NSIgY3k9IjQ4IiByPSIwLjkiIGZpbGw9IiMwMDAiLz48Y2lyY2xlIGN4PSI2MiIgY3k9IjQxIiByPSIwLjUiIGZpbGw9IiMwMDAiLz48Y2lyY2xlIGN4PSI0OSIgY3k9IjYxIiByPSIwLjgiIGZpbGw9IiMwMDAiLz48Y2lyY2xlIGN4PSI2NiIgY3k9IjUyIiByPSIwLjciIGZpbGw9IiMwMDAiLz48Y2lyY2xlIGN4PSI0MSIgY3k9IjUxIiByPSIwLjYiIGZpbGw9IiMwMDAiLz48Y2lyY2xlIGN4PSI2OCIgY3k9IjU3IiByPSIwLjQiIGZpbGw9IiMwMDAiLz48Y2lyY2xlIGN4PSI0NSIgY3k9IjQwIiByPSIwLjUiIGZpbGw9IiMwMDAiLz48Y2lyY2xlIGN4PSI1NyIgY3k9IjYxIiByPSIwLjciIGZpbGw9IiMwMDAiLz48L3N2Zz4="; + } +} diff --git a/src/component/Canvas/CanvasEditor/managers/brushes/types/TextureBrush.js b/src/component/Canvas/CanvasEditor/managers/brushes/types/TextureBrush.js new file mode 100644 index 00000000..0efa9627 --- /dev/null +++ b/src/component/Canvas/CanvasEditor/managers/brushes/types/TextureBrush.js @@ -0,0 +1,855 @@ +import { BaseBrush } from "../BaseBrush"; +//import { fabric } from "fabric-with-all"; +import texturePresetManager from "../TexturePresetManager"; + +/** + * 纹理笔刷 + * 使用图像纹理进行绘制的笔刷 + */ +export class TextureBrush extends BaseBrush { + /** + * 构造函数 + * @param {Object} canvas fabric画布实例 + * @param {Object} options 配置选项 + */ + constructor(canvas, options = {}) { + super(canvas, { + id: "texture", + name: "纹理笔刷", + description: "使用图像纹理进行绘制的笔刷", + category: "特效笔刷", + icon: "texture", + ...options, + }); + + // 纹理笔刷特有属性 + this.textureSource = options.textureSource || null; + this.textureRepeat = options.textureRepeat || "repeat"; + this.textureScale = options.textureScale || 1; + this.textureAngle = options.textureAngle || 0; + this.textureOpacity = + options.textureOpacity !== undefined ? options.textureOpacity : 1; + + // 预设材质相关 + this.selectedTextureId = options.selectedTextureId || null; + this.texturePresets = []; + + // 加载预设材质 + this._loadTexturePresets(); + + // 当前选中的材质索引 + this.currentTextureIndex = options.currentTextureIndex || 0; + + // 从预设管理器加载自定义材质 + texturePresetManager.loadCustomTexturesFromStorage(); + } + + /** + * 加载材质预设 + * @private + */ + _loadTexturePresets() { + // 从预设管理器获取所有材质 + this.texturePresets = texturePresetManager.getAllTextures(); + + // 如果没有选中的材质ID,使用第一个预设材质 + if (!this.selectedTextureId && this.texturePresets.length > 0) { + this.selectedTextureId = this.texturePresets[0].id; + this.currentTextureIndex = 0; + } + } + + /** + * 创建笔刷实例 + * @returns {Object} fabric笔刷实例 + */ + create() { + if (!this.canvas) { + throw new Error("画布实例不存在"); + } + + // 创建fabric原生纹理笔刷 + this.brush = new fabric.PatternBrush(this.canvas); + + // 配置笔刷 + this.configure(this.brush, this.options); + + // 如果有选中的材质,则设置纹理 + if (this.selectedTextureId) { + this.setTextureById(this.selectedTextureId); + } else if (this.textureSource) { + this.setTexture(this.textureSource); + } + + return this.brush; + } + + /** + * 配置笔刷 + * @param {Object} brush fabric笔刷实例 + * @param {Object} options 配置选项 + */ + configure(brush, options = {}) { + if (!brush) return; + + // 设置基本属性 + if (options.width !== undefined) { + brush.width = options.width; + } + + if (options.color !== undefined) { + brush.color = options.color; + } + + if (options.opacity !== undefined) { + brush.opacity = options.opacity; + } + + // 纹理笔刷特有属性 + if (options.textureRepeat !== undefined) { + this.textureRepeat = options.textureRepeat; + // 需要重新应用纹理以应用重复模式 + if (this.selectedTextureId) { + this.setTextureById(this.selectedTextureId); + } else if (this.textureSource) { + this.setTexture(this.textureSource); + } + } + + if (options.textureScale !== undefined) { + this.textureScale = options.textureScale; + // 需要重新应用纹理以应用缩放 + if (this.selectedTextureId) { + this.setTextureById(this.selectedTextureId); + } else if (this.textureSource) { + this.setTexture(this.textureSource); + } + } + + if (options.textureAngle !== undefined) { + this.textureAngle = options.textureAngle; + // 需要重新应用纹理以应用旋转角度 + if (this.selectedTextureId) { + this.setTextureById(this.selectedTextureId); + } else if (this.textureSource) { + this.setTexture(this.textureSource); + } + } + + if (options.textureOpacity !== undefined) { + this.textureOpacity = options.textureOpacity; + // 需要重新应用纹理以应用透明度 + if (this.selectedTextureId) { + this.setTextureById(this.selectedTextureId); + } else if (this.textureSource) { + this.setTexture(this.textureSource); + } + } + } + + /** + * 根据材质ID设置纹理 + * @param {String} textureId 材质ID + * @returns {Promise} 加载完成的Promise + */ + setTextureById(textureId) { + const texture = texturePresetManager.getTextureById(textureId); + if (!texture) { + return Promise.reject(new Error(`材质 ${textureId} 不存在`)); + } + + this.selectedTextureId = textureId; + + // 更新当前材质索引 + const allTextures = texturePresetManager.getAllTextures(); + this.currentTextureIndex = allTextures.findIndex((t) => t.id === textureId); + + return this.setTexture(texture.path); + } + + /** + * 设置纹理 + * @param {String|Object} source 纹理源(URL或Image对象) + * @returns {Promise} 加载完成的Promise + */ + setTexture(source) { + this.textureSource = source; + + if (!this.brush) { + return Promise.reject(new Error("笔刷实例不存在")); + } + + return new Promise((resolve, reject) => { + if (typeof source === "string") { + // 如果是URL,加载图像 + fabric.util.loadImage(source, (img) => { + if (!img) { + reject(new Error("纹理加载失败")); + return; + } + this._applyTextureToPatternBrush(img); + resolve(img); + }); + } else if ( + source instanceof Image || + source instanceof HTMLCanvasElement + ) { + // 如果已经是Image或Canvas对象,直接使用 + this._applyTextureToPatternBrush(source); + resolve(source); + } else { + reject(new Error("无效的纹理源")); + } + }); + } + + /** + * 将纹理应用到PatternBrush + * @param {Object} img 图像对象 + * @private + */ + _applyTextureToPatternBrush(img) { + if (!this.brush || !img) return; + + // 创建Canvas来处理纹理 + const canvasTexture = document.createElement("canvas"); + const ctx = canvasTexture.getContext("2d"); + + // 根据缩放设置Canvas大小 + const width = img.width * this.textureScale; + const height = img.height * this.textureScale; + canvasTexture.width = width; + canvasTexture.height = height; + + // 绘制前应用旋转 + if (this.textureAngle !== 0) { + ctx.save(); + ctx.translate(width / 2, height / 2); + ctx.rotate((this.textureAngle * Math.PI) / 180); + ctx.translate(-width / 2, -height / 2); + ctx.drawImage(img, 0, 0, width, height); + ctx.restore(); + } else { + ctx.drawImage(img, 0, 0, width, height); + } + + // 应用透明度 + if (this.textureOpacity < 1) { + ctx.globalAlpha = this.textureOpacity; + ctx.fillStyle = "#fff"; + ctx.fillRect(0, 0, width, height); + } + + // 创建Pattern对象 + const pattern = new fabric.Pattern({ + source: canvasTexture, + repeat: this.textureRepeat, + }); + + // 设置笔刷源纹理 + if (typeof this.brush.setSource === "function") { + this.brush.setSource(pattern); + } else if (typeof this.brush.source === "object") { + this.brush.source = pattern; + } else if (typeof this.brush.pattern === "object") { + this.brush.pattern = pattern; + } + } + + /** + * 设置纹理重复模式 + * @param {String} mode 重复模式:'repeat', 'repeat-x', 'repeat-y', 'no-repeat' + * @returns {String} 设置后的重复模式 + */ + setTextureRepeat(mode) { + const validModes = ["repeat", "repeat-x", "repeat-y", "no-repeat"]; + if (validModes.includes(mode)) { + this.textureRepeat = mode; + + // 重新应用纹理以更新重复模式 + if (this.selectedTextureId) { + this.setTextureById(this.selectedTextureId); + } else if (this.textureSource) { + this.setTexture(this.textureSource); + } + } + + return this.textureRepeat; + } + + /** + * 设置纹理缩放比例 + * @param {Number} scale 缩放比例 + * @returns {Number} 设置后的缩放比例 + */ + setTextureScale(scale) { + this.textureScale = Math.max(0.1, scale); + + // 重新应用纹理以更新缩放 + if (this.selectedTextureId) { + this.setTextureById(this.selectedTextureId); + } else if (this.textureSource) { + this.setTexture(this.textureSource); + } + + return this.textureScale; + } + + /** + * 设置纹理旋转角度 + * @param {Number} angle 旋转角度(度) + * @returns {Number} 设置后的旋转角度 + */ + setTextureAngle(angle) { + this.textureAngle = angle % 360; + + // 重新应用纹理以更新旋转角度 + if (this.selectedTextureId) { + this.setTextureById(this.selectedTextureId); + } else if (this.textureSource) { + this.setTexture(this.textureSource); + } + + return this.textureAngle; + } + + /** + * 设置纹理透明度 + * @param {Number} opacity 透明度 + * @returns {Number} 设置后的透明度 + */ + setTextureOpacity(opacity) { + this.textureOpacity = Math.min(1, Math.max(0, opacity)); + + // 重新应用纹理以更新透明度 + if (this.selectedTextureId) { + this.setTextureById(this.selectedTextureId); + } else if (this.textureSource) { + this.setTexture(this.textureSource); + } + + return this.textureOpacity; + } + + /** + * 切换到下一个预设材质 + * @returns {Promise} 切换完成的Promise + */ + nextTexture() { + const textures = texturePresetManager.getAllTextures(); + if (textures.length === 0) return Promise.resolve(); + + this.currentTextureIndex = (this.currentTextureIndex + 1) % textures.length; + const nextTexture = textures[this.currentTextureIndex]; + + return this.setTextureById(nextTexture.id); + } + + /** + * 切换到上一个预设材质 + * @returns {Promise} 切换完成的Promise + */ + previousTexture() { + const textures = texturePresetManager.getAllTextures(); + if (textures.length === 0) return Promise.resolve(); + + this.currentTextureIndex = + this.currentTextureIndex === 0 + ? textures.length - 1 + : this.currentTextureIndex - 1; + const prevTexture = textures[this.currentTextureIndex]; + + return this.setTextureById(prevTexture.id); + } + + /** + * 使用索引切换纹理 + * @param {Number} index 纹理索引 + */ + switchTexture(index) { + const textures = texturePresetManager.getAllTextures(); + if (index >= 0 && index < textures.length) { + this.currentTextureIndex = index; + const texture = textures[index]; + return this.setTextureById(texture.id); + } + return Promise.reject(new Error("无效的纹理索引")); + } + + /** + * 获取当前选中的材质信息 + * @returns {Object|null} 材质信息 + */ + getCurrentTexture() { + if (this.selectedTextureId) { + return texturePresetManager.getTextureById(this.selectedTextureId); + } + return null; + } + + /** + * 获取笔刷可配置属性 + * @returns {Array} 可配置属性描述数组 + * @override + */ + getConfigurableProperties() { + // 获取基础属性 + const baseProperties = super.getConfigurableProperties(); + + // 获取所有可用材质 + const allTextures = texturePresetManager.getAllTextures(); + const textureOptions = allTextures.map((texture, index) => ({ + value: texture.id, + label: texture.name, + preview: texturePresetManager.getTexturePreviewUrl(texture), + category: texture.category, + })); + + // 定义纹理笔刷特有属性 + const textureProperties = [ + { + id: "textureSelector", + name: "材质选择", + type: "texture-grid", + defaultValue: this.selectedTextureId, + options: textureOptions, + description: "选择要使用的纹理", + category: "纹理设置", + order: 100, + hidden: allTextures.length === 0, + }, + { + id: "textureRepeat", + name: "纹理重复模式", + type: "select", + defaultValue: this.textureRepeat, + options: [ + { value: "repeat", label: "双向重复" }, + { value: "repeat-x", label: "水平重复" }, + { value: "repeat-y", label: "垂直重复" }, + { value: "no-repeat", label: "不重复" }, + ], + description: "设置纹理的重复模式", + category: "纹理设置", + order: 110, + }, + { + id: "textureScale", + name: "纹理缩放", + type: "slider", + defaultValue: this.textureScale, + min: 0.1, + max: 5, + step: 0.1, + description: "调整纹理的缩放比例", + category: "纹理设置", + order: 120, + }, + { + id: "textureAngle", + name: "纹理旋转", + type: "slider", + defaultValue: this.textureAngle, + min: 0, + max: 360, + step: 5, + description: "调整纹理的旋转角度", + category: "纹理设置", + order: 130, + }, + { + id: "textureOpacity", + name: "纹理透明度", + type: "slider", + defaultValue: this.textureOpacity, + min: 0, + max: 1, + step: 0.05, + description: "调整纹理的透明度", + category: "纹理设置", + order: 140, + }, + { + id: "uploadTexture", + name: "上传纹理", + type: "button", + action: "uploadTexture", + description: "上传自定义纹理", + category: "纹理设置", + order: 150, + }, + { + id: "texturePreview", + name: "纹理预览", + type: "preview", + description: "当前纹理预览", + category: "纹理设置", + order: 160, + getValue: () => { + const currentTexture = this.getCurrentTexture(); + return currentTexture + ? texturePresetManager.getTexturePreviewUrl(currentTexture) + : null; + }, + }, + ]; + + // 合并并返回所有属性 + return [...baseProperties, ...textureProperties]; + } + + /** + * 更新笔刷属性 + * @param {String} propId 属性ID + * @param {any} value 属性值 + * @returns {Boolean} 是否更新成功 + * @override + */ + updateProperty(propId, value) { + // 先检查基类能否处理此属性 + if (super.updateProperty(propId, value)) { + return true; + } + + // 处理纹理笔刷特有属性 + if (propId === "textureSelector") { + this.setTextureById(value); + return true; + } else if (propId === "textureRepeat") { + this.setTextureRepeat(value); + return true; + } else if (propId === "textureScale") { + this.setTextureScale(value); + return true; + } else if (propId === "textureAngle") { + this.setTextureAngle(value); + return true; + } else if (propId === "textureOpacity") { + this.setTextureOpacity(value); + return true; + } else if (propId === "uploadTexture") { + // 触发上传纹理事件 + // 这里通常由外部处理,返回true表示属性被处理 + return true; + } + + return false; + } + + /** + * 添加自定义材质 + * @param {Object} textureData 材质数据 + * @returns {String} 材质ID + */ + addCustomTexture(textureData) { + const textureId = texturePresetManager.addCustomTexture(textureData); + + // 重新加载材质预设 + this._loadTexturePresets(); + + // 保存到本地存储 + texturePresetManager.saveCustomTexturesToStorage(); + + return textureId; + } + + /** + * 删除自定义材质 + * @param {String} textureId 材质ID + * @returns {Boolean} 是否删除成功 + */ + removeCustomTexture(textureId) { + const success = texturePresetManager.removeCustomTexture(textureId); + + if (success) { + // 如果删除的是当前选中的材质,切换到第一个可用材质 + if (this.selectedTextureId === textureId) { + const allTextures = texturePresetManager.getAllTextures(); + if (allTextures.length > 0) { + this.setTextureById(allTextures[0].id); + } else { + this.selectedTextureId = null; + this.currentTextureIndex = 0; + } + } + + // 重新加载材质预设 + this._loadTexturePresets(); + + // 保存到本地存储 + texturePresetManager.saveCustomTexturesToStorage(); + } + + return success; + } + + /** + * 获取预览图 + * @returns {String} 预览图URL + */ + getPreview() { + const currentTexture = this.getCurrentTexture(); + if (currentTexture) { + return texturePresetManager.getTexturePreviewUrl(currentTexture); + } + + // 返回默认纹理预览 + return "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMDAgMTAwIj48ZGVmcz48cGF0dGVybiBpZD0icGF0dGVybiIgcGF0dGVyblVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgd2lkdGg9IjEwIiBoZWlnaHQ9IjEwIj48cmVjdCB3aWR0aD0iNSIgaGVpZ2h0PSI1IiBmaWxsPSIjZGRkIi8+PHJlY3QgeD0iNSIgeT0iNSIgd2lkdGg9IjUiIGhlaWdodD0iNSIgZmlsbD0iI2RkZCIvPjwvcGF0dGVybj48L2RlZnM+PHJlY3Qgd2lkdGg9IjEwMCIgaGVpZ2h0PSIxMDAiIGZpbGw9InVybCgjcGF0dGVybikiLz48L3N2Zz4="; + } + + /** + * 笔刷被选中时调用 + * @override + */ + onSelected() { + // 重新加载材质预设(可能有新的自定义材质) + this._loadTexturePresets(); + } + + /** + * 销毁笔刷实例并清理资源 + * @override + */ + destroy() { + super.destroy(); + this.textureSource = null; + this.selectedTextureId = null; + this.texturePresets = []; + } + + /** + * 设置材质属性 + * @param {String} property 属性名称 + * @param {any} value 属性值 + * @returns {Boolean} 是否设置成功 + */ + setTextureProperty(property, value) { + switch (property) { + case "scale": + return this.setTextureScale(value); + case "opacity": + return this.setTextureOpacity(value); + case "repeat": + return this.setTextureRepeat(value); + case "angle": + return this.setTextureAngle(value); + default: + return false; + } + } + + /** + * 获取材质属性 + * @param {String} property 属性名称 + * @returns {any} 属性值 + */ + getTextureProperty(property) { + switch (property) { + case "scale": + return this.textureScale; + case "opacity": + return this.textureOpacity; + case "repeat": + return this.textureRepeat; + case "angle": + return this.textureAngle; + case "textureId": + return this.selectedTextureId; + default: + return undefined; + } + } + + /** + * 应用材质预设 + * @param {String|Object} preset 预设ID或预设对象 + * @returns {Boolean} 是否应用成功 + */ + applyTexturePreset(preset) { + let presetData = null; + + if (typeof preset === "string") { + // 如果是预设ID,从预设管理器获取 + presetData = texturePresetManager.applyTexturePreset(preset); + } else if (typeof preset === "object") { + // 如果是预设对象,直接使用 + presetData = preset; + } + + if (!presetData) { + console.warn("无效的材质预设:", preset); + return false; + } + + // 应用预设设置 + if (presetData.textureId) { + this.setTextureById(presetData.textureId); + } + + if (presetData.scale !== undefined) { + this.setTextureScale(presetData.scale); + } + + if (presetData.opacity !== undefined) { + this.setTextureOpacity(presetData.opacity); + } + + if (presetData.repeat !== undefined) { + this.setTextureRepeat(presetData.repeat); + } + + if (presetData.angle !== undefined) { + this.setTextureAngle(presetData.angle); + } + + // 如果预设包含笔刷属性,也一并应用 + if (presetData.brushSize !== undefined && this.brush) { + this.brush.width = presetData.brushSize; + } + + if (presetData.brushOpacity !== undefined && this.brush) { + this.brush.opacity = presetData.brushOpacity; + } + + if (presetData.brushColor !== undefined && this.brush) { + this.brush.color = presetData.brushColor; + } + + return true; + } + + /** + * 获取当前材质状态 + * @returns {Object} 当前材质状态 + */ + getCurrentTextureState() { + return { + textureId: this.selectedTextureId, + scale: this.textureScale, + opacity: this.textureOpacity, + repeat: this.textureRepeat, + angle: this.textureAngle, + // 包含笔刷状态 + brushSize: this.brush ? this.brush.width : this.options.width, + brushOpacity: this.brush ? this.brush.opacity : this.options.opacity, + brushColor: this.brush ? this.brush.color : this.options.color, + }; + } + + /** + * 恢复材质状态 + * @param {Object} state 要恢复的状态 + * @returns {Boolean} 是否恢复成功 + */ + restoreTextureState(state) { + if (!state) return false; + + try { + // 恢复材质属性 + if (state.textureId) { + this.setTextureById(state.textureId); + } + + if (state.scale !== undefined) { + this.setTextureScale(state.scale); + } + + if (state.opacity !== undefined) { + this.setTextureOpacity(state.opacity); + } + + if (state.repeat !== undefined) { + this.setTextureRepeat(state.repeat); + } + + if (state.angle !== undefined) { + this.setTextureAngle(state.angle); + } + + // 恢复笔刷属性 + if (this.brush) { + if (state.brushSize !== undefined) { + this.brush.width = state.brushSize; + } + + if (state.brushOpacity !== undefined) { + this.brush.opacity = state.brushOpacity; + } + + if (state.brushColor !== undefined) { + this.brush.color = state.brushColor; + } + } + + return true; + } catch (error) { + console.error("恢复材质状态失败:", error); + return false; + } + } + + /** + * 创建材质预设 + * @param {String} name 预设名称 + * @returns {String} 预设ID + */ + createTexturePreset(name) { + const currentState = this.getCurrentTextureState(); + return texturePresetManager.createTexturePreset(name, currentState); + } + + /** + * 获取可用的材质分类 + * @returns {Array} 分类数组 + */ + getTextureCategories() { + return texturePresetManager.getCategories(); + } + + /** + * 根据分类获取材质 + * @param {String} category 分类名称 + * @returns {Array} 材质数组 + */ + getTexturesByCategory(category) { + return texturePresetManager.getTexturesByCategory(category); + } + + /** + * 搜索材质 + * @param {String} query 搜索关键词 + * @returns {Array} 匹配的材质数组 + */ + searchTextures(query) { + return texturePresetManager.searchTextures(query); + } + + /** + * 预加载材质图像 + * @param {String} textureId 材质ID + * @returns {Promise} 图像对象 + */ + preloadTexture(textureId) { + return texturePresetManager.loadTextureImage(textureId); + } + + /** + * 批量预加载材质 + * @param {Array} textureIds 材质ID数组 + * @returns {Promise} 加载结果数组 + */ + preloadTextures(textureIds) { + const loadPromises = textureIds.map((id) => + this.preloadTexture(id).catch((error) => ({ id, error })) + ); + return Promise.all(loadPromises); + } + + /** + * 获取材质统计信息 + * @returns {Object} 统计信息 + */ + getTextureStats() { + return texturePresetManager.getStats(); + } +} diff --git a/src/component/Canvas/CanvasEditor/managers/brushes/types/WritingBrush.js b/src/component/Canvas/CanvasEditor/managers/brushes/types/WritingBrush.js new file mode 100644 index 00000000..3bd35a80 --- /dev/null +++ b/src/component/Canvas/CanvasEditor/managers/brushes/types/WritingBrush.js @@ -0,0 +1,266 @@ +import { BaseBrush } from "../BaseBrush"; + +/** + * 书法笔刷 + * 模拟中国传统书法效果,具有笔锋和墨色变化 + */ +export class WritingBrush extends BaseBrush { + /** + * 构造函数 + * @param {Object} canvas fabric画布实例 + * @param {Object} options 配置选项 + */ + constructor(canvas, options = {}) { + super(canvas, { + id: "writing", + name: "书法笔", + description: "模拟中国传统书法毛笔效果,具有笔锋和墨色变化", + category: "特效笔刷", + icon: "writing", + ...options, + }); + + // 书法笔刷特有属性 + this.brushPressure = options.brushPressure || 0.7; + this.inkAmount = options.inkAmount || 20; + this.brushTaperFactor = options.brushTaperFactor || 0.6; + this.enableInkDripping = + options.enableInkDripping !== undefined + ? options.enableInkDripping + : true; + } + + /** + * 创建笔刷实例 + * @returns {Object} fabric笔刷实例 + */ + create() { + if (!this.canvas) { + throw new Error("画布实例不存在"); + } + + // 创建fabric原生书法笔刷 + this.brush = new fabric.WritingBrush(this.canvas); + + // 配置笔刷 + this.configure(this.brush, this.options); + + return this.brush; + } + + /** + * 配置笔刷 + * @param {Object} brush fabric笔刷实例 + * @param {Object} options 配置选项 + */ + configure(brush, options = {}) { + if (!brush) return; + + // 基础属性配置 + if (options.width !== undefined) { + brush.width = options.width; + } + + if (options.color !== undefined) { + brush.color = options.color; + } + + if (options.opacity !== undefined) { + brush.opacity = options.opacity; + } + + // 书法笔刷特有属性 + if (options.brushPressure !== undefined) { + this.brushPressure = options.brushPressure; + + // 如果原生笔刷支持此属性,则设置 + if (brush.brushPressure !== undefined) { + brush.brushPressure = this.brushPressure; + } + } + + if (options.inkAmount !== undefined) { + this.inkAmount = options.inkAmount; + + // 如果原生笔刷支持此属性,则设置 + if (brush.inkAmount !== undefined) { + brush.inkAmount = this.inkAmount; + } + } + + if (options.brushTaperFactor !== undefined) { + this.brushTaperFactor = options.brushTaperFactor; + + // 如果原生笔刷支持此属性,则设置 + if (brush.brushTaperFactor !== undefined) { + brush.brushTaperFactor = this.brushTaperFactor; + } + } + + if (options.enableInkDripping !== undefined) { + this.enableInkDripping = options.enableInkDripping; + + // 如果原生笔刷支持此属性,则设置 + if (brush.enableInkDripping !== undefined) { + brush.enableInkDripping = this.enableInkDripping; + } + } + } + + /** + * 设置笔压感应 + * @param {Number} pressure 笔压值(0-1) + */ + setBrushPressure(pressure) { + this.brushPressure = Math.max(0.1, Math.min(1, pressure)); + + if (this.brush && this.brush.brushPressure !== undefined) { + this.brush.brushPressure = this.brushPressure; + } + + return this.brushPressure; + } + + /** + * 设置墨量 + * @param {Number} amount 墨量值 + */ + setInkAmount(amount) { + this.inkAmount = Math.max(1, Math.min(50, amount)); + + if (this.brush && this.brush.inkAmount !== undefined) { + this.brush.inkAmount = this.inkAmount; + } + + return this.inkAmount; + } + + /** + * 设置笔锋系数 + * @param {Number} factor 笔锋系数(0-1) + */ + setBrushTaperFactor(factor) { + this.brushTaperFactor = Math.max(0, Math.min(1, factor)); + + if (this.brush && this.brush.brushTaperFactor !== undefined) { + this.brush.brushTaperFactor = this.brushTaperFactor; + } + + return this.brushTaperFactor; + } + + /** + * 启用/禁用墨滴效果 + * @param {Boolean} enabled 是否启用 + */ + setInkDripping(enabled) { + this.enableInkDripping = enabled; + + if (this.brush && this.brush.enableInkDripping !== undefined) { + this.brush.enableInkDripping = this.enableInkDripping; + } + + return this.enableInkDripping; + } + + /** + * 获取笔刷可配置属性 + * @returns {Array} 可配置属性描述数组 + * @override + */ + getConfigurableProperties() { + // 获取基础属性 + const baseProperties = super.getConfigurableProperties(); + + // 定义书法笔刷特有属性 + const writingProperties = [ + { + id: "brushPressure", + name: "笔压感应", + type: "slider", + defaultValue: this.brushPressure, + min: 0.1, + max: 1, + step: 0.05, + description: "控制笔触的力度感应", + category: "书法设置", + order: 100, + }, + { + id: "inkAmount", + name: "墨量", + type: "slider", + defaultValue: this.inkAmount, + min: 1, + max: 50, + step: 1, + description: "控制笔触中的墨水量", + category: "书法设置", + order: 110, + }, + { + id: "brushTaperFactor", + name: "笔锋系数", + type: "slider", + defaultValue: this.brushTaperFactor, + min: 0, + max: 1, + step: 0.05, + description: "控制笔锋的尖锐程度", + category: "书法设置", + order: 120, + }, + { + id: "enableInkDripping", + name: "墨滴效果", + type: "checkbox", + defaultValue: this.enableInkDripping, + description: "是否启用墨滴效果", + category: "书法设置", + order: 130, + }, + ]; + + // 合并并返回所有属性 + return [...baseProperties, ...writingProperties]; + } + + /** + * 更新笔刷属性 + * @param {String} propId 属性ID + * @param {any} value 属性值 + * @returns {Boolean} 是否更新成功 + * @override + */ + updateProperty(propId, value) { + // 先检查基类能否处理此属性 + if (super.updateProperty(propId, value)) { + return true; + } + + // 处理书法笔刷特有属性 + if (propId === "brushPressure") { + this.setBrushPressure(value); + return true; + } else if (propId === "inkAmount") { + this.setInkAmount(value); + return true; + } else if (propId === "brushTaperFactor") { + this.setBrushTaperFactor(value); + return true; + } else if (propId === "enableInkDripping") { + this.setInkDripping(value); + return true; + } + + return false; + } + + /** + * 获取预览图 + * @returns {String} 预览图URL + */ + getPreview() { + return "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMDAgMTAwIj48cGF0aCBkPSJNMzAgMzBDNTAgMzAgNjAgNzAgODAgNzAiIHN0cm9rZT0iIzAwMCIgc3Ryb2tlLXdpZHRoPSIxMCIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIiBmaWxsPSJub25lIi8+PHBhdGggZD0iTTc1IDYwQzc4IDcwIDg1IDY1IDkwIDcwIiBzdHJva2U9IiMwMDAiIHN0cm9rZS13aWR0aD0iMyIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIiBmaWxsPSJub25lIi8+PC9zdmc+"; + } +} diff --git a/src/component/Canvas/CanvasEditor/managers/command/CommandManager.js b/src/component/Canvas/CanvasEditor/managers/command/CommandManager.js new file mode 100644 index 00000000..c2728277 --- /dev/null +++ b/src/component/Canvas/CanvasEditor/managers/command/CommandManager.js @@ -0,0 +1,433 @@ +import { CompositeCommand } from "../../commands/Command.js"; +import { PerformanceManager } from "./PerformanceManager.js"; + +/** + * 简化版命令管理器 + * 基于经典撤销/重做模式,支持命令队列 + * 使用复合命令替代事务处理 + */ +export class CommandManager { + constructor(options = {}) { + this.undoStack = []; + this.redoStack = []; + this.maxHistorySize = options.maxHistorySize || 50; + this.executing = false; + + // 命令执行队列 + this.commandQueue = []; + this.processing = false; + + // 可选的性能管理器 + this.performanceManager = options.performanceManager || null; + + // 状态变化回调 + this.onStateChange = null; + } + + // 兼容旧的executeCommand方法 + async executeCommand(command) { + return this.execute(command); + } + + /** + * 执行命令并添加到撤销栈 + */ + async execute(command) { + if (!command || typeof command.execute !== "function") { + throw new Error("无效的命令对象"); + } + + return this._executeDirectly(command); + } + + /** + * 直接执行命令(绕过事务检查) + * @private + */ + async _executeDirectly(command) { + // 返回Promise,等待命令执行完成 + return new Promise((resolve, reject) => { + // 将命令添加到队列 + this.commandQueue.push({ + type: "execute", + command, + resolve, + reject, + }); + + // 开始处理队列 + this._processQueue(); + }); + } + + /** + * 撤销最后一个命令 + */ + async undo() { + return new Promise((resolve, reject) => { + // 检查是否可以撤销 + if (this.undoStack.length === 0) { + console.warn("无法撤销:撤销栈为空"); + resolve(null); + return; + } + + // 将撤销操作添加到队列 + this.commandQueue.push({ + type: "undo", + resolve, + reject, + }); + + // 开始处理队列 + this._processQueue(); + }); + } + + /** + * 重做最后一个撤销的命令 + */ + async redo() { + return new Promise((resolve, reject) => { + // 检查是否可以重做 + if (this.redoStack.length === 0) { + console.warn("无法重做:重做栈为空"); + resolve(null); + return; + } + + // 将重做操作添加到队列 + this.commandQueue.push({ + type: "redo", + resolve, + reject, + }); + + // 开始处理队列 + this._processQueue(); + }); + } + + /** + * 处理命令队列 + * @private + */ + async _processQueue() { + // 如果正在处理或队列为空,直接返回 + if (this.processing || this.commandQueue.length === 0) { + return; + } + + this.processing = true; + + try { + while (this.commandQueue.length > 0) { + const task = this.commandQueue.shift(); + + try { + let result = null; + + switch (task.type) { + case "execute": + result = await this._executeCommandInternal(task.command); + break; + case "undo": + result = await this._undoInternal(); + break; + case "redo": + result = await this._redoInternal(); + break; + default: + throw new Error(`未知的任务类型: ${task.type}`); + } + + task.resolve(result); + } catch (error) { + task.reject(error); + } + } + } finally { + this.processing = false; + } + } + + /** + * 内部执行命令方法 + * @private + */ + async _executeCommandInternal(command) { + this.executing = true; + const startTime = performance.now(); + + try { + console.log(`🔄 执行命令: ${command.constructor.name}`); + + // 执行命令 + const result = await this._executeCommand(command); + + // 只有可撤销的命令才加入撤销栈 + if (command.undoable !== false) { + this.undoStack.push(command); + this.redoStack = []; // 清空重做栈 + + // 限制历史记录大小 + this._trimHistory(); + } + + // 记录性能 + const duration = performance.now() - startTime; + this._recordPerformance("execute", command.constructor.name, duration); + + // 通知状态变化 + this._notifyStateChange(); + + console.log(`✅ 命令执行成功: ${command.constructor.name}`); + return result; + } catch (error) { + console.error(`❌ 命令执行失败: ${command.constructor.name}`, error); + throw error; + } finally { + this.executing = false; + } + } + + /** + * 内部撤销方法 + * @private + */ + async _undoInternal() { + if (this.undoStack.length === 0) { + console.warn("无法撤销:撤销栈为空"); + return null; + } + + this.executing = true; + const startTime = performance.now(); + + try { + const command = this.undoStack.pop(); + console.log(`↩️ 撤销命令: ${command.constructor.name}`); + + const result = await this._undoCommand(command); + + this.redoStack.push(command); + + // 记录性能 + const duration = performance.now() - startTime; + this._recordPerformance("undo", command.constructor.name, duration); + + // 通知状态变化 + this._notifyStateChange(); + + console.log(`✅ 命令撤销成功: ${command.constructor.name}`); + return result; + } catch (error) { + console.error(`❌ 命令撤销失败`, error); + throw error; + } finally { + this.executing = false; + } + } + + /** + * 内部重做方法 + * @private + */ + async _redoInternal() { + if (this.redoStack.length === 0) { + console.warn("无法重做:重做栈为空"); + return null; + } + + this.executing = true; + const startTime = performance.now(); + + try { + const command = this.redoStack.pop(); + console.log(`↪️ 重做命令: ${command.constructor.name}`); + + const result = await this._executeCommand(command); + + this.undoStack.push(command); + + // 记录性能 + const duration = performance.now() - startTime; + this._recordPerformance("redo", command.constructor.name, duration); + + // 通知状态变化 + this._notifyStateChange(); + + console.log(`✅ 命令重做成功: ${command.constructor.name}`); + return result; + } catch (error) { + console.error(`❌ 命令重做失败`, error); + throw error; + } finally { + this.executing = false; + } + } + + /** + * 批量执行命令(使用 CompositeCommand) + * 推荐使用此方法替代原来的事务机制 + */ + async executeBatch(commands, batchName = "批量操作") { + if (!Array.isArray(commands) || commands.length === 0) { + throw new Error("命令数组不能为空"); + } + + const compositeCommand = new CompositeCommand(commands, { + name: batchName, + }); + + return this.execute(compositeCommand); + } + + /** + * 清空历史记录 + */ + clear() { + // 清空队列中的所有任务 + while (this.commandQueue.length > 0) { + const task = this.commandQueue.shift(); + task.reject(new Error("命令管理器已被清空")); + } + + this.undoStack = []; + this.redoStack = []; + this._notifyStateChange(); + console.log("📝 命令历史已清空"); + } + + /** + * 获取管理器状态 + */ + getState() { + return { + canUndo: this.undoStack.length > 0, + canRedo: this.redoStack.length > 0, + undoCount: this.undoStack.length, + redoCount: this.redoStack.length, + isExecuting: this.executing, + isProcessing: this.processing, + queueLength: this.commandQueue.length, + + lastCommand: + this.undoStack.length > 0 + ? this.undoStack[this.undoStack.length - 1].constructor.name + : null, + nextRedoCommand: + this.redoStack.length > 0 + ? this.redoStack[this.redoStack.length - 1].constructor.name + : null, + }; + } + + /** + * 获取命令历史信息 + */ + getHistory() { + return { + undoHistory: this.undoStack.map((cmd) => ({ + name: cmd.constructor.name, + info: cmd.getInfo ? cmd.getInfo() : {}, + timestamp: cmd.timestamp, + })), + redoHistory: this.redoStack.map((cmd) => ({ + name: cmd.constructor.name, + info: cmd.getInfo ? cmd.getInfo() : {}, + timestamp: cmd.timestamp, + })), + }; + } + + setChangeCallback(callback) { + if (typeof callback === "function") { + this.onStateChange = callback; + } else { + throw new Error("回调必须是一个函数"); + } + } + + /** + * 执行单个命令 + * @private + */ + async _executeCommand(command) { + const result = command.execute(); + return this._isPromise(result) ? await result : result; + } + + /** + * 撤销单个命令 + * @private + */ + async _undoCommand(command) { + if (typeof command.undo !== "function") { + throw new Error(`命令 ${command.constructor.name} 不支持撤销`); + } + + const result = command.undo(); + return this._isPromise(result) ? await result : result; + } + + /** + * 检查是否为Promise + * @private + */ + _isPromise(value) { + return ( + value && + typeof value === "object" && + typeof value.then === "function" && + typeof value.catch === "function" + ); + } + + /** + * 限制历史记录大小 + * @private + */ + _trimHistory() { + if (this.undoStack.length > this.maxHistorySize) { + this.undoStack.shift(); + } + } + + /** + * 记录性能数据 + * @private + */ + _recordPerformance(type, commandName, duration) { + if (this.performanceManager) { + if (type === "execute") { + this.performanceManager.recordExecution(commandName, duration); + } else if (type === "undo") { + this.performanceManager.recordUndo(commandName, duration); + } else if (type === "redo") { + this.performanceManager.recordRedo(commandName, duration); + } + } + } + + /** + * 通知状态变化 + * @private + */ + _notifyStateChange() { + if (this.onStateChange) { + try { + this.onStateChange(this.getState()); + } catch (error) { + console.error("状态变化回调执行失败:", error); + } + } + } +} + +/** + * 创建命令管理器实例的工厂函数 + */ +export function createCommandManager(options = {}) { + return new CommandManager(options); +} diff --git a/src/component/Canvas/CanvasEditor/managers/command/PerformanceManager.js b/src/component/Canvas/CanvasEditor/managers/command/PerformanceManager.js new file mode 100644 index 00000000..ef483b65 --- /dev/null +++ b/src/component/Canvas/CanvasEditor/managers/command/PerformanceManager.js @@ -0,0 +1,199 @@ +/** + * 简化版性能管理器 + * 提供基础的性能统计功能 + */ +export class PerformanceManager { + constructor() { + this.stats = { + totalExecutions: 0, + totalUndos: 0, + totalRedos: 0, + totalExecutionTime: 0, + totalUndoTime: 0, + totalRedoTime: 0, + commandStats: new Map(), // 每个命令的统计信息 + recentOperations: [], // 最近的操作记录 + }; + + this.maxRecentOperations = 100; + } + + /** + * 记录命令执行 + */ + recordExecution(commandName, duration) { + this.stats.totalExecutions++; + this.stats.totalExecutionTime += duration; + + this._updateCommandStats(commandName, "executions", duration); + this._addRecentOperation("execute", commandName, duration); + } + + /** + * 记录撤销操作 + */ + recordUndo(commandName, duration) { + this.stats.totalUndos++; + this.stats.totalUndoTime += duration; + + this._updateCommandStats(commandName, "undos", duration); + this._addRecentOperation("undo", commandName, duration); + } + + /** + * 记录重做操作 + */ + recordRedo(commandName, duration) { + this.stats.totalRedos++; + this.stats.totalRedoTime += duration; + + this._updateCommandStats(commandName, "redos", duration); + this._addRecentOperation("redo", commandName, duration); + } + + /** + * 获取统计信息 + */ + getStats() { + const avgExecutionTime = + this.stats.totalExecutions > 0 + ? this.stats.totalExecutionTime / this.stats.totalExecutions + : 0; + + const avgUndoTime = + this.stats.totalUndos > 0 + ? this.stats.totalUndoTime / this.stats.totalUndos + : 0; + + const avgRedoTime = + this.stats.totalRedos > 0 + ? this.stats.totalRedoTime / this.stats.totalRedos + : 0; + + return { + overview: { + totalExecutions: this.stats.totalExecutions, + totalUndos: this.stats.totalUndos, + totalRedos: this.stats.totalRedos, + avgExecutionTime: Number(avgExecutionTime.toFixed(2)), + avgUndoTime: Number(avgUndoTime.toFixed(2)), + avgRedoTime: Number(avgRedoTime.toFixed(2)), + }, + commandBreakdown: Array.from(this.stats.commandStats.entries()).map( + ([name, stats]) => ({ + commandName: name, + executions: stats.executions, + undos: stats.undos, + redos: stats.redos, + avgExecutionTime: + stats.executions > 0 + ? Number((stats.totalExecutionTime / stats.executions).toFixed(2)) + : 0, + avgUndoTime: + stats.undos > 0 + ? Number((stats.totalUndoTime / stats.undos).toFixed(2)) + : 0, + avgRedoTime: + stats.redos > 0 + ? Number((stats.totalRedoTime / stats.redos).toFixed(2)) + : 0, + }) + ), + recentOperations: this.stats.recentOperations.slice(-20), // 最近20个操作 + }; + } + + /** + * 获取慢命令报告 + */ + getSlowCommandsReport(threshold = 100) { + const slowCommands = []; + + for (const [name, stats] of this.stats.commandStats.entries()) { + const avgExecTime = + stats.executions > 0 ? stats.totalExecutionTime / stats.executions : 0; + const avgUndoTime = + stats.undos > 0 ? stats.totalUndoTime / stats.undos : 0; + + if (avgExecTime > threshold || avgUndoTime > threshold) { + slowCommands.push({ + commandName: name, + avgExecutionTime: Number(avgExecTime.toFixed(2)), + avgUndoTime: Number(avgUndoTime.toFixed(2)), + executions: stats.executions, + undos: stats.undos, + }); + } + } + + return slowCommands.sort( + (a, b) => + Math.max(b.avgExecutionTime, b.avgUndoTime) - + Math.max(a.avgExecutionTime, a.avgUndoTime) + ); + } + + /** + * 重置统计信息 + */ + reset() { + this.stats = { + totalExecutions: 0, + totalUndos: 0, + totalRedos: 0, + totalExecutionTime: 0, + totalUndoTime: 0, + totalRedoTime: 0, + commandStats: new Map(), + recentOperations: [], + }; + } + + /** + * 更新命令统计信息 + * @private + */ + _updateCommandStats(commandName, type, duration) { + if (!this.stats.commandStats.has(commandName)) { + this.stats.commandStats.set(commandName, { + executions: 0, + undos: 0, + redos: 0, + totalExecutionTime: 0, + totalUndoTime: 0, + totalRedoTime: 0, + }); + } + + const stats = this.stats.commandStats.get(commandName); + + if (type === "executions") { + stats.executions++; + stats.totalExecutionTime += duration; + } else if (type === "undos") { + stats.undos++; + stats.totalUndoTime += duration; + } else if (type === "redos") { + stats.redos++; + stats.totalRedoTime += duration; + } + } + + /** + * 添加最近操作记录 + * @private + */ + _addRecentOperation(type, commandName, duration) { + this.stats.recentOperations.push({ + type, + commandName, + duration: Number(duration.toFixed(2)), + timestamp: Date.now(), + }); + + // 限制记录数量 + if (this.stats.recentOperations.length > this.maxRecentOperations) { + this.stats.recentOperations.shift(); + } + } +} diff --git a/src/component/Canvas/CanvasEditor/managers/events/CanvasEventManager.js b/src/component/Canvas/CanvasEditor/managers/events/CanvasEventManager.js new file mode 100644 index 00000000..4fae1167 --- /dev/null +++ b/src/component/Canvas/CanvasEditor/managers/events/CanvasEventManager.js @@ -0,0 +1,909 @@ +import { TransformCommand } from "../../commands/StateCommands"; +import { generateId } from "../../utils/helper"; +import { OperationType, OperationTypes } from "../../utils/layerHelper"; + +export class CanvasEventManager { + constructor(canvas, options = {}) { + this.canvas = canvas; + this.toolManager = options.toolManager || null; + this.animationManager = options.animationManager; + this.thumbnailManager = options.thumbnailManager; + this.editorMode = options.editorMode || OperationType.SELECT; + this.activeElementId = options.activeElementId || { value: null }; + this.layerManager = options.layerManager || null; + this.layers = options.layers || null; + + // 事件处理的内部状态 - 优化设备检测 + this.deviceInfo = this._detectDeviceType(); + this.dragStartTime = 0; + this.lastMousePositions = []; + this.positionHistoryLimit = 5; // 追踪鼠标位置的历史记录,用于计算速度 + this.longPressTimer = null; + this.longPressThreshold = 500; + + // 初始化所有事件 + this.initEvents(); + } + + initEvents() { + this.setupZoomEvents(); + + // 优化三端设备的事件处理逻辑 + if (this.deviceInfo.isMobile || this.deviceInfo.isTablet) { + // 真正的移动设备和平板设备使用触摸事件 + this.setupTouchEvents(); + } else { + // PC 和 Mac 设备主要使用鼠标事件 + this.setupMouseEvents(); + } + + // Mac 设备需要额外的触摸手势支持(用于特殊场景) + if (this.deviceInfo.isMac && this.deviceInfo.hasTouchSupport) { + this.setupMacTouchGestures(); + } + + // 共享事件 + this.setupSelectionEvents(); + this.setupObjectEvents(); + this.setupDoubleClickEvents(); + + // this.setupHandlePathCreated(); + } + + setupZoomEvents() { + // 水平/垂直滚动相关状态 + this._scrollWheelEvents = []; + this._scrollAccumulatedDelta = { x: 0, y: 0 }; + this._scrollAccumulationTimeout = null; + this._scrollAccumulationTime = 100; // 降低滚轮累积时间窗口 + this._lastScrollTime = 0; // 跟踪上次滚动时间 + this._scrollThrottleDelay = 5; // 滚动节流延迟(毫秒) + + // 缩放处理 - 使用动画管理器,针对 Mac 设备优化 + this.canvas.on("mouse:wheel", (opt) => { + // Mac 设备双指滚动优化:确保滚动事件正确处理 + if (this.deviceInfo.isMac) { + // Mac设备的简化处理逻辑,减少不必要的动画中断 + // 让动画管理器自行处理冲突,避免过度干预 + } else { + // 非 Mac 设备的标准处理 + if ( + this.animationManager._panAnimation || + this.animationManager._zoomAnimation + ) { + this.animationManager._wasPanning = + !!this.animationManager._panAnimation; + this.animationManager._wasZooming = + !!this.animationManager._zoomAnimation; + this.animationManager.smoothStopAnimations({ duration: 0.1 }); + } + } + + // 按住 Ctrl 键时实现垂直滚动(Mac 下是 Cmd 键) + const isCtrlOrCmd = this.deviceInfo.isMac ? opt.e.metaKey : opt.e.ctrlKey; + if (isCtrlOrCmd) { + this.handleScrollWheel(opt, "vertical"); + opt.e.preventDefault(); + return; + } + + // 按住 Shift 键时实现水平滚动 + if (opt.e.shiftKey) { + this.handleScrollWheel(opt, "horizontal"); + opt.e.preventDefault(); + return; + } + + // 标准缩放行为 - 让 AnimationManager 处理平滑过渡 + // Mac 设备下的双指滚动将直接进入这里进行缩放 + this.animationManager.handleMouseWheel(opt); + }); + } + + /** + * 处理滚轮滚动事件 + * @param {Object} opt 滚轮事件对象 + * @param {String} direction 滚动方向: 'vertical' 或 'horizontal' + */ + handleScrollWheel(opt, direction) { + // 获取当前视图变换 + const vpt = this.canvas.viewportTransform.slice(0); // 创建副本避免直接修改 + const zoom = this.canvas.getZoom(); + + // 计算滚动量 - 根据方向决定是水平还是垂直滚动 + let deltaX = 0; + let deltaY = 0; + + // 设置滚动方向和距离 + if (direction === "horizontal") { + deltaX = opt.e.deltaY; // 水平滚动 + } else { + deltaY = opt.e.deltaY; // 垂直滚动 + } + + // 计算滚动因子,基于缩放级别和设备类型调整 + let scrollFactor = Math.max(0.4, Math.min(1, 1 / zoom)); + + // Mac 设备优化:触控板滚动通常比鼠标滚轮更敏感 + if (this.deviceInfo.isMac) { + const isMacTrackpadScroll = + Math.abs(opt.e.deltaY) < 100 && opt.e.deltaMode === 0; + if (isMacTrackpadScroll) { + // Mac 触控板滚动更细腻,需要调整滚动因子 + scrollFactor *= 0.8; // 降低滚动敏感度 + } + } + + // 直接应用滚动变化,不使用累积和计时器 + vpt[4] -= deltaX * scrollFactor; + vpt[5] -= deltaY * scrollFactor; + + // 直接设置新的视图变换,不使用动画 + this.canvas.setViewportTransform(vpt); + + // 请求重新渲染画布 + this.canvas.renderAll(); + } + + /** + * 处理累积的滚轮滚动事件并应用平移 + * @private + * @param {String} direction 滚动方向 + */ + _processAccumulatedScroll(direction) { + // 这个函数不再需要,但为了兼容性保留空实现 + // 所有滚动逻辑已经移到 handleScrollWheel 中直接处理 + return; + } + + /** + * 停止所有惯性动画 + * @param {boolean} smooth 是否平滑过渡,默认为 false(立即停止) + */ + stopInertiaAnimation(smooth = false) { + if (this.animationManager) { + if (this.animationManager._panAnimation && !smooth) { + this.animationManager._panAnimation.kill(); + this.animationManager._panAnimation = null; + } + if (this.animationManager._zoomAnimation && !smooth) { + this.animationManager._zoomAnimation.kill(); + this.animationManager._zoomAnimation = null; + } + } + } + + /** + * 设置鼠标事件处理 + */ + setupMouseEvents() { + // 鼠标按下事件 + this.canvas.on("mouse:down", (opt) => { + // 平滑停止任何正在进行的惯性动画 + this.stopInertiaAnimation(true); + + if ( + opt.e.altKey || + opt.e.which === 2 || + this.editorMode === OperationType.PAN + ) { + this.canvas.isDragging = true; + this.canvas.lastPosX = opt.e.clientX; + this.canvas.lastPosY = opt.e.clientY; + this.canvas.defaultCursor = "grabbing"; + + // 记录拖动开始时间和位置,用于计算速度 + this.dragStartTime = Date.now(); + this.lastMousePositions = []; // 重置位置历史 + + if (this.canvas.isDragging) { + this.canvas.selection = false; + this.canvas.renderAll(); + } + } + }); + + // 鼠标移动事件 + this.canvas.on("mouse:move", (opt) => { + if (!this.canvas.isDragging) return; + + const vpt = this.canvas.viewportTransform; + vpt[4] += opt.e.clientX - this.canvas.lastPosX; + vpt[5] += opt.e.clientY - this.canvas.lastPosY; + + // 记录鼠标位置和时间,用于计算惯性 + const now = Date.now(); + this.lastMousePositions.push({ + x: opt.e.clientX, + y: opt.e.clientY, + time: now, + }); + + // 保持历史记录在限定数量内 + if (this.lastMousePositions.length > this.positionHistoryLimit) { + this.lastMousePositions.shift(); + } + + this.canvas.renderAll(); + this.canvas.lastPosX = opt.e.clientX; + this.canvas.lastPosY = opt.e.clientY; + }); + + // 鼠标抬起事件 + this.canvas.on("mouse:up", (opt) => { + this.handleDragEnd(opt); + }); + } + + /** + * 设置触摸事件处理 + */ + setupTouchEvents() { + // 触摸开始事件 + this.canvas.on("touch:gesture", (opt) => { + // 平滑停止任何正在进行的惯性动画 + this.stopInertiaAnimation(true); + + if (opt.e.touches && opt.e.touches.length === 2) { + this.canvas.isDragging = true; + this.canvas.lastPosX = + (opt.e.touches[0].clientX + opt.e.touches[1].clientX) / 2; + this.canvas.lastPosY = + (opt.e.touches[0].clientY + opt.e.touches[1].clientY) / 2; + + // 重置触摸位置历史 + this.dragStartTime = Date.now(); + this.lastMousePositions = []; + + if (this.canvas.isDragging) { + this.canvas.selection = false; + this.canvas.renderAll(); + } + opt.e.preventDefault(); + } + }); + + // 单指触摸开始 - 处理拖动 + this.canvas.on("touch:drag", (opt) => { + // 平滑停止任何正在进行的惯性动画 + this.stopInertiaAnimation(true); + + if (this.editorMode === OperationType.PAN) { + this.canvas.isDragging = true; + this.canvas.lastPosX = opt.e.touches[0].clientX; + this.canvas.lastPosY = opt.e.touches[0].clientY; + + this.dragStartTime = Date.now(); + this.lastMousePositions = []; + + if (this.canvas.isDragging) { + this.canvas.selection = false; + this.canvas.renderAll(); + } + opt.e.preventDefault(); + } + }); + + // 触摸移动事件 + this.canvas.on("touch:gesture:update", (opt) => { + if (!this.canvas.isDragging) return; + + if (opt.e.touches && opt.e.touches.length === 2) { + const currentX = + (opt.e.touches[0].clientX + opt.e.touches[1].clientX) / 2; + const currentY = + (opt.e.touches[0].clientY + opt.e.touches[1].clientY) / 2; + + const vpt = this.canvas.viewportTransform; + vpt[4] += currentX - this.canvas.lastPosX; + vpt[5] += currentY - this.canvas.lastPosY; + + // 记录触摸位置和时间 + const now = Date.now(); + this.lastMousePositions.push({ + x: currentX, + y: currentY, + time: now, + }); + + // 保持历史记录在限定数量内 + if (this.lastMousePositions.length > this.positionHistoryLimit) { + this.lastMousePositions.shift(); + } + + this.canvas.renderAll(); + this.canvas.lastPosX = currentX; + this.canvas.lastPosY = currentY; + opt.e.preventDefault(); + } + }); + + // 单指拖动更新 + this.canvas.on("touch:drag:update", (opt) => { + if (!this.canvas.isDragging || this.editorMode !== OperationType.PAN) + return; + + const currentX = opt.e.touches[0].clientX; + const currentY = opt.e.touches[0].clientY; + + const vpt = this.canvas.viewportTransform; + vpt[4] += currentX - this.canvas.lastPosX; + vpt[5] += currentY - this.canvas.lastPosY; + + // 记录触摸位置和时间 + const now = Date.now(); + this.lastMousePositions.push({ + x: currentX, + y: currentY, + time: now, + }); + + if (this.lastMousePositions.length > this.positionHistoryLimit) { + this.lastMousePositions.shift(); + } + + this.canvas.renderAll(); + this.canvas.lastPosX = currentX; + this.canvas.lastPosY = currentY; + opt.e.preventDefault(); + }); + + // 触摸结束事件 + this.canvas.on("touch:gesture:end", (opt) => { + this.handleDragEnd(opt, true); + }); + + // 单指拖动结束 + this.canvas.on("touch:drag:end", (opt) => { + this.handleDragEnd(opt, true); + }); + } + + /** + * 处理拖动结束(鼠标抬起或触摸结束) + */ + handleDragEnd(opt, isTouch = false) { + if (this.canvas.isDragging) { + // 使用动画管理器处理惯性效果 + if (this.lastMousePositions.length > 1 && opt && opt.e) { + this.animationManager.applyInertiaEffect( + this.lastMousePositions, + isTouch + ); + } + } + + this.canvas.isDragging = false; + + if (this.toolManager) { + this.toolManager.restoreSelectionState(); // 恢复选择状态 + } + + this.canvas.renderAll(); + } + + setupSelectionEvents() { + // 监听对象选择事件 + this.canvas.on("selection:created", (opt) => this.updateSelectedLayer(opt)); + + this.canvas.on("selection:updated", (opt) => this.updateSelectedLayer(opt)); + + // this.canvas.on("selection:cleared", () => this.clearSelectedElements()); + } + + setupObjectEvents() { + // 监听对象变化事件,用于更新缩略图 + this.canvas.on("object:added", (e) => { + if (this.thumbnailManager && e.target && e.target.id) { + // 延迟更新以确保对象完全添加 + setTimeout(() => { + // 现在图层就是元素本身,直接更新元素的缩略图 + this.thumbnailManager.generateLayerThumbnail( + e.target.layerId, + e.target + ); + }, 300); + } + }); + + // 添加对象开始变换时的状态捕获 + this.canvas.on( + "object:moving", + this._captureInitialTransformState.bind(this) + ); + this.canvas.on( + "object:scaling", + this._captureInitialTransformState.bind(this) + ); + this.canvas.on( + "object:rotating", + this._captureInitialTransformState.bind(this) + ); + this.canvas.on( + "object:skewing", + this._captureInitialTransformState.bind(this) + ); + + this.canvas.on("object:modified", (e) => { + // 移除调试日志 + // console.log("object:modified", e); + + const activeObj = e.target || this.canvas.getActiveObject(); + + if (activeObj && this.layerManager?.commandManager) { + // 使用新的轻量级 TransformCommand 替代完整状态保存 + // 检查对象是否有初始变换状态记录 + if (activeObj._initialTransformState) { + // 创建并执行 TransformCommand,只记录变换属性的变化 + const transformCmd = new TransformCommand({ + canvas: this.canvas, + objectId: activeObj.id, + initialState: activeObj._initialTransformState, + finalState: TransformCommand.captureTransformState(activeObj), + objectType: activeObj.type, + name: `变换 ${activeObj.type || "对象"}`, + }); + + // 执行并将命令添加到历史栈 + this.layerManager.commandManager.execute(transformCmd, { + name: "对象修改", + }); + + // 清除临时状态记录 + delete activeObj._initialTransformState; + } + } + + if (this.thumbnailManager && e.target) { + if (e.target.id) { + this.updateLayerThumbnail(e.target.id, e.target); + + // 如果该元素是分组图层的一部分,也更新分组图层的缩略图 + if (e.target.parentId) { + this.updateLayerThumbnail(e.target.parentId); + } + } + } + }); + + this.canvas.on("object:removed", (e) => { + if (this.thumbnailManager && e.target) { + if (e.target.id) { + this.thumbnailManager.clearElementThumbnail(e.target.id); + + // 如果该元素是分组图层的一部分,也更新分组图层的缩略图 + if (e.target.parentId) { + setTimeout(() => this.updateLayerThumbnail(e.target.parentId), 50); + } + } + } + }); + + // // 鼠标抬起时,检查是否需要保存状态 + // this.canvas.on("mouse:up", (e) => { + // // 只在选择模式下处理对象变换的状态保存 + // if (this.editorMode !== OperationType.SELECT) { + // // 绘画、擦除等模式通过各自的命令管理状态,不需要在这里保存 + // return; + // } + + // const activeObj = this.canvas.getActiveObject(); + // if ( + // activeObj && + // activeObj._stateRecord && + // activeObj._stateRecord.isModifying + // ) { + // const original = activeObj._stateRecord.originalState; + + // // 检查是否是真正的变换操作(移动、缩放、旋转) + // const hasTransformChanged = + // original.left !== activeObj.left || + // original.top !== activeObj.top || + // original.scaleX !== activeObj.scaleX || + // original.scaleY !== activeObj.scaleY || + // original.angle !== activeObj.angle; + + // // 只有在对象发生变换且不是命令执行过程中时才保存状态 + // if (hasTransformChanged && this.layerManager) { + // // 立即保存状态,而不是延迟执行 + // this.layerManager.saveCanvasState(); + // delete activeObj._stateRecord; + // } else { + // // 清理状态记录,即使没有保存状态 + // delete activeObj._stateRecord; + // } + // } + // }); + } + + setupDoubleClickEvents() { + // 双击处理 + this.canvas.on("mouse:dblclick", (opt) => { + if (opt.target) { + // 双击对象的特殊处理 + } else { + // 双击空白处重置缩放 + if (this.animationManager) { + this.animationManager.resetZoom(true); + } + } + }); + } + + setupLongPress(callback) { + this.canvas.on("mouse:down", (opt) => { + if (!opt.target) return; + + this.longPressTimer = setTimeout(() => { + callback(opt); + }, this.longPressThreshold); + }); + + this.canvas.on("mouse:up", () => { + clearTimeout(this.longPressTimer); + }); + + this.canvas.on("mouse:move", () => { + clearTimeout(this.longPressTimer); + }); + } + + // 设置路径创建事件 + setupHandlePathCreated() { + // 在 CanvasEventManager 的构造函数或初始化方法中 + // this.canvas.on("path:created", this._handlePathCreated.bind(this)); + } + + _handlePathCreated(e) { + // // 获取新创建的路径对象 + // const path = e.path; + // // 设置路径的ID和其他属性 + // path.id = generateId(); // 生成唯一ID + // // 获取当前活动图层 + // const activeLayer = this.layerManager.getActiveLayer(); + // // 将路径对象绑定到当前活动图层 + // if (activeLayer) { + // // 设置路径的图层ID + // path.layerId = activeLayer.id; + // // 更新图层对象列表 + // if (!activeLayer.fabricObjects) activeLayer.fabricObjects = []; + // activeLayer.fabricObjects.push(path); + // // 更新图层缩略图 + // if (this.thumbnailManager) { + // this.thumbnailManager.generateLayerThumbnail(activeLayer.id); + // } + // } + } + + /** + * 合并图层中的对象为图像以提高性能 + * @param {Object} options 合并选项 + * @param {fabric.Image} options.fabricImage 新的图像对象 + * @param {Object} options.activeLayer 当前活动图层 + * @private + */ + async mergeLayerObjectsForPerformance({ fabricImage, activeLayer }) { + // 确保有命令管理器 + if (!this.layerManager || !this.layerManager.commandManager) { + console.warn("合并对象失败:没有命令管理器"); + return; + } + + // 确保有活动图层 + if (!activeLayer) { + console.warn("合并对象失败:没有活动图层"); + return; + } + + // 验证是否需要合并 + const hasExistingObjects = + Array.isArray(activeLayer.fabricObjects) && + activeLayer.fabricObjects.length > 0; + const hasNewImage = !!fabricImage; + + if (!hasExistingObjects && !hasNewImage) { + console.log("没有对象需要合并"); + return; + } + + // 如果只有一个新图像且图层为空,直接添加到图层 + if (hasNewImage && !hasExistingObjects) { + this.layerManager.addObjectToLayer(fabricImage, activeLayer.id); + return; + } + + // 执行高保真合并操作 + try { + console.log(`开始合并图层 ${activeLayer.name} 中的对象为组...`); + + const command = await this.layerManager.LayerObjectsToGroup( + activeLayer, + fabricImage + ); + + this.layerManager?.commandManager?.execute?.(command, { + name: `合并图层 ${activeLayer.name} 中的对象为组`, + }); + } catch (error) { + console.error("合并图层对象时发生错误:", error); + + // 降级处理:如果合并失败,至少保证新图像能添加到图层 + if (fabricImage && this.layerManager) { + console.log("执行降级处理:直接添加图像到图层"); + this.layerManager.addObjectToLayer(fabricImage, activeLayer.id); + } + } + } + + updateSelectedLayer(opt) { + const selected = opt.selected[0]; + if (selected) { + this.layerManager.activeLayerId.value = selected.layerId; + } + } + + // clearSelectedElements() { + // this.activeElementId.value = null; + // } + + // 更新图层缩略图 + updateLayerThumbnail(layerId) { + if (!this.thumbnailManager || !layerId || !this.layers) return; + + const layer = this.layers.value.find((l) => l.id === layerId); + if (layer) { + this.thumbnailManager.generateLayerThumbnail(layer); + } + } + + // 更新子元素组合缩略图 + updateLayerChidrenThumbnail(layerId, fabricObject) { + if (!this.thumbnailManager || !fabricObject || !this.layers) return; + + // 查找对应的图层(现在元素就是图层) + const layer = this.layers.value.find( + (l) => + l.id === elementId || + (l.fabricObjects && l.fabricObjects?.[0]?.id === layerId) + ); + + if (layer) { + // 生成图层缩略图 + this.thumbnailManager.generateLayerThumbnail(layer); + } + + // 同时也维护元素缩略图,以保持向后兼容性 + this.thumbnailManager.generateElementThumbnail( + { id: elementId, type: fabricObject.type }, + fabricObject + ); + } + + /** + * 设置编辑器模式 + * @param {string} mode 编辑器模式 + */ + setEditorMode(mode) { + if (!OperationTypes.includes(mode)) { + console.warn(`不支持的编辑器模式: ${mode}`); + return; + } + + // 切换工具时,立即停止任何惯性动画,但使用平滑过渡 + this.stopInertiaAnimation(true); + + this.editorMode = mode; + + // 如果切换到选择模式,还原鼠标指针 + if (mode === OperationType.SELECT) { + this.canvas.defaultCursor = "default"; + } else if (mode === OperationType.PAN) { + this.canvas.defaultCursor = "grab"; + } + } + + dispose() { + // 移除所有事件监听 + this.canvas.off(); + + // 清理 Mac 专用的原生事件监听器 + if (this.deviceInfo.isMac && this.canvas.upperCanvasEl) { + const upperCanvas = this.canvas.upperCanvasEl; + + // 移除手势事件监听器 + upperCanvas.removeEventListener("gesturestart", null); + upperCanvas.removeEventListener("gesturechange", null); + upperCanvas.removeEventListener("gestureend", null); + upperCanvas.removeEventListener("wheel", null); + } + + // 清除计时器 + if (this.longPressTimer) { + clearTimeout(this.longPressTimer); + this.longPressTimer = null; + } + + // 停止所有动画 + this.stopInertiaAnimation(); + } + + /** + * 捕获对象开始变换时的初始状态 + * @private + * @param {Object} e 事件对象 + */ + _captureInitialTransformState(e) { + const obj = e.target; + + // 只在首次触发变换事件时记录初始状态 + if (obj && !obj._initialTransformState && obj.id) { + // 捕获对象的初始变换状态 + obj._initialTransformState = TransformCommand.captureTransformState(obj); + + // 添加调试日志(可选) + // console.log(`捕获对象 ${obj.id} (${obj.type}) 的初始变换状态`); + } + } + + /** + * 精确检测设备类型,区分 PC、Mac、平板和移动设备 + * @private + * @returns {Object} 设备信息对象 + */ + _detectDeviceType() { + const userAgent = navigator.userAgent.toLowerCase(); + const platform = navigator.platform.toLowerCase(); + const hasTouchSupport = + "ontouchstart" in window || navigator.maxTouchPoints > 0; + + // 检测操作系统 + const isMac = /mac|darwin/.test(platform) || /macintosh/.test(userAgent); + const isWindows = /win/.test(platform); + const isLinux = /linux/.test(platform) && !/android/.test(userAgent); + + // 检测设备类型 + const isMobile = /mobile|phone|android.*mobile|iphone/.test(userAgent); + const isTablet = /tablet|ipad|android(?!.*mobile)/.test(userAgent); + const isDesktop = !isMobile && !isTablet; + + // 检测浏览器类型(用于特定优化) + const isSafari = /safari/.test(userAgent) && !/chrome/.test(userAgent); + const isChrome = /chrome/.test(userAgent); + const isFirefox = /firefox/.test(userAgent); + + return { + isMac, + isWindows, + isLinux, + isMobile, + isTablet, + isDesktop, + isSafari, + isChrome, + isFirefox, + hasTouchSupport, + // 判断是否应该使用触摸事件作为主要交互方式 + preferTouchEvents: (isMobile || isTablet) && !isDesktop, + // 判断是否需要特殊的 Mac 触控板处理 + needsMacTrackpadOptimization: isMac && isDesktop && hasTouchSupport, + }; + } + + /** + * 设置 Mac 专用的触摸手势处理 + * 主要用于处理触控板的多指手势,但不干扰双指滚动的缩放功能 + */ + setupMacTouchGestures() { + // Mac 触控板专用:三指拖拽进行画布平移 + let macGestureState = { + isThreeFingerDrag: false, + startX: 0, + startY: 0, + }; + + // 监听 Mac 专用的手势事件 + this.canvas.upperCanvasEl.addEventListener( + "gesturestart", + (e) => { + // 阻止浏览器默认的手势行为,但保留双指缩放 + if (e.scale !== 1) { + e.preventDefault(); + } + }, + { passive: false } + ); + + this.canvas.upperCanvasEl.addEventListener( + "gesturechange", + (e) => { + // 只处理三指以上的手势,保留双指缩放给 mouse:wheel 事件 + if (e.touches && e.touches.length >= 3) { + e.preventDefault(); + + if (!macGestureState.isThreeFingerDrag) { + macGestureState.isThreeFingerDrag = true; + macGestureState.startX = e.pageX; + macGestureState.startY = e.pageY; + this.canvas.isDragging = true; + this.canvas.lastPosX = e.pageX; + this.canvas.lastPosY = e.pageY; + this.stopInertiaAnimation(true); + } else { + // 执行三指拖拽平移 + const vpt = this.canvas.viewportTransform; + vpt[4] += e.pageX - this.canvas.lastPosX; + vpt[5] += e.pageY - this.canvas.lastPosY; + + this.canvas.renderAll(); + this.canvas.lastPosX = e.pageX; + this.canvas.lastPosY = e.pageY; + } + } + }, + { passive: false } + ); + + this.canvas.upperCanvasEl.addEventListener( + "gestureend", + (e) => { + if (macGestureState.isThreeFingerDrag) { + macGestureState.isThreeFingerDrag = false; + this.canvas.isDragging = false; + + if (this.toolManager) { + this.toolManager.restoreSelectionState(); + } + + this.canvas.renderAll(); + } + }, + { passive: false } + ); + + // 添加 Mac 专用的鼠标滚轮优化,确保双指滚动正常工作 + this.setupMacScrollOptimization(); + } + + /** + * Mac 滚轮优化:确保双指滚动正确触发缩放 + */ + setupMacScrollOptimization() { + if (!this.deviceInfo.isMac) return; + + // Mac 下的滚轮事件优化 + let macScrollState = { + lastWheelTime: 0, + wheelTimeout: null, + }; + + // 监听原生滚轮事件,确保 Mac 双指滚动正确处理 + this.canvas.upperCanvasEl.addEventListener( + "wheel", + (e) => { + const now = Date.now(); + + // Mac 双指滚动的特征:通常有较高的 deltaY 精度和连续性 + const isMacTrackpadScroll = + this.deviceInfo.isMac && + Math.abs(e.deltaY) < 100 && // 像素模式 + e.deltaMode === 0; // 像素模式 + + if (isMacTrackpadScroll) { + // 清除之前的超时 + if (macScrollState.wheelTimeout) { + clearTimeout(macScrollState.wheelTimeout); + } + + // 确保这个事件会被 Fabric.js 的 mouse:wheel 正确处理 + macScrollState.lastWheelTime = now; + + // 设置短暂延迟,防止与触摸事件冲突 + macScrollState.wheelTimeout = setTimeout(() => { + // 滚轮事件处理完成 + }, 16); // 约一帧的时间 + } + }, + { passive: true } + ); + } +} diff --git a/src/component/Canvas/CanvasEditor/managers/events/KeyboardManager.js b/src/component/Canvas/CanvasEditor/managers/events/KeyboardManager.js new file mode 100644 index 00000000..e906f1bd --- /dev/null +++ b/src/component/Canvas/CanvasEditor/managers/events/KeyboardManager.js @@ -0,0 +1,720 @@ +/** + * 键盘管理器 + * 负责处理编辑器中的键盘事件和快捷键 + * 支持PC、Mac和iPad三端适配 + */ +export class KeyboardManager { + /** + * 创建键盘管理器 + * @param {Object} options 配置选项 + * @param {Object} options.toolManager 工具管理器实例 + * @param {Object} options.commandManager 命令管理器实例 + * @param {Object} options.layerManager 图层管理器实例 + * @param {HTMLElement} options.container 容器元素,用于添加事件监听 + */ + constructor(options = {}) { + this.toolManager = options.toolManager; + this.commandManager = options.commandManager; + this.layerManager = options.layerManager; + this.container = options.container || document; + + // 检测平台类型 + this.platform = this.detectPlatform(); + this.isTouchDevice = this.detectTouchDevice(); + + // 快捷键的平台特定键名 + this.modifierKeys = { + ctrl: this.platform === "mac" ? "meta" : "ctrl", + cmdOrCtrl: this.platform === "mac" ? "meta" : "ctrl", + alt: "alt", + shift: "shift", + option: "alt", // Mac 特有,等同于 alt + cmd: "meta", // Mac 特有,等同于 Command + }; + + // 快捷键显示的平台特定符号 + this.keySymbols = { + ctrl: this.platform === "mac" ? "⌃" : "Ctrl", + meta: this.platform === "mac" ? "⌘" : "Win", + alt: this.platform === "mac" ? "⌥" : "Alt", + shift: this.platform === "mac" ? "⇧" : "Shift", + escape: "Esc", + space: "空格", + }; + + // 快捷键映射表 - 可通过配置进行扩展 + this.shortcuts = this.initShortcuts(); + + // 触摸相关状态 + this.touchState = { + pinchStartDistance: 0, + pinchStartBrushSize: 0, + touchStartX: 0, + touchStartY: 0, + isTwoFingerTouch: false, + }; + + // 临时工具状态 + this.tempToolState = { + active: false, + originalTool: null, + }; + + // 事件绑定 + this._handleKeyDown = this.handleKeyDown.bind(this); + this._handleKeyUp = this.handleKeyUp.bind(this); + this._handleTouchStart = this.handleTouchStart.bind(this); + this._handleTouchMove = this.handleTouchMove.bind(this); + this._handleTouchEnd = this.handleTouchEnd.bind(this); + + // 已注册的自定义事件处理程序 + this.customHandlers = {}; + } + + /** + * 检测当前平台 + * @returns {'mac'|'windows'|'ios'|'android'|'other'} 平台类型 + */ + detectPlatform() { + const userAgent = navigator.userAgent.toLowerCase(); + + if (userAgent.indexOf("mac") !== -1) return "mac"; + if (userAgent.indexOf("win") !== -1) return "windows"; + if (/(iphone|ipad|ipod)/.test(userAgent)) return "ios"; + if (userAgent.indexOf("android") !== -1) return "android"; + + return "other"; + } + + /** + * 检测是否为触摸设备 + * @returns {boolean} 是否为触摸设备 + */ + detectTouchDevice() { + return ( + "ontouchstart" in window || + navigator.maxTouchPoints > 0 || + navigator.msMaxTouchPoints > 0 + ); + } + + /** + * 初始化快捷键配置 + * @returns {Object} 快捷键配置 + */ + initShortcuts() { + const cmdOrCtrl = this.modifierKeys.cmdOrCtrl; + + // 基本快捷键映射,将在构建时根据平台类型自动调整 + return { + // 撤销/重做 + [`${cmdOrCtrl}+z`]: { action: "undo", description: "撤销" }, + [`${cmdOrCtrl}+shift+z`]: { action: "redo", description: "重做" }, + [`${cmdOrCtrl}+y`]: { action: "redo", description: "重做" }, + + // 复制/粘贴 + [`${cmdOrCtrl}+c`]: { action: "copy", description: "复制" }, + [`${cmdOrCtrl}+v`]: { action: "paste", description: "粘贴" }, + [`${cmdOrCtrl}+x`]: { action: "cut", description: "剪切" }, + + // 删除 + delete: { action: "delete", description: "删除" }, + backspace: { action: "delete", description: "删除" }, + + // 选择 + [`${cmdOrCtrl}+a`]: { action: "selectAll", description: "全选" }, + escape: { action: "clearSelection", description: "取消选择" }, + + // 保存 + [`${cmdOrCtrl}+s`]: { action: "save", description: "保存" }, + + // 工具切换 (这些会由工具管理器处理) + v: { action: "selectTool", param: "select", description: "选择工具" }, + b: { action: "selectTool", param: "draw", description: "画笔工具" }, + e: { action: "selectTool", param: "eraser", description: "橡皮擦" }, + i: { action: "selectTool", param: "eyedropper", description: "吸色工具" }, + h: { action: "selectTool", param: "pan", description: "移动画布" }, + l: { action: "selectTool", param: "lasso", description: "套索工具" }, + m: { + action: "selectTool", + param: "area_custom", + description: "自由选区工具", + }, + w: { action: "selectTool", param: "wave", description: "波浪工具" }, + j: { action: "selectTool", param: "liquify", description: "液化工具" }, + + // 数值调整 + "shift+[": { + action: "decreaseTextureScale", + description: "减小材质图片大小", + }, + "shift+]": { + action: "increaseTextureScale", + description: "增大材质图片大小", + }, + "[": { action: "decreaseBrushSize", param: 1, description: "减小画笔" }, + "]": { action: "increaseBrushSize", param: 1, description: "增大画笔" }, + + ",": { + action: "decreaseBrushOpacity", + param: 0.01, + description: "减小透明度", + }, + ".": { + action: "increaseBrushOpacity", + param: 0.01, + description: "增大透明度", + }, + + // 空格 - 临时切换到手型工具 + space: { + action: "toggleTempTool", + param: "pan", + description: "临时切换到手形工具", + }, + + // 图层操作 + [`${cmdOrCtrl}+shift+n`]: { action: "newLayer", description: "新建图层" }, + [`${cmdOrCtrl}+g`]: { action: "groupLayers", description: "组合图层" }, + [`${cmdOrCtrl}+o`]: { + action: "addImageToNewLayer", + description: "上传图片到新图层", + }, + [`${cmdOrCtrl}+shift+g`]: { + action: "ungroupLayers", + description: "取消组合", + }, + [`${cmdOrCtrl}+j`]: { action: "mergeLayers", description: "合并图层" }, + + // iPad特有的快捷键(当无法使用键盘时) + ...(this.platform === "ios" && { + two_finger_tap: { + action: "contextMenu", + description: "显示上下文菜单", + }, + three_finger_swipe_left: { action: "undo", description: "撤销" }, + three_finger_swipe_right: { action: "redo", description: "重做" }, + }), + }; + } + + /** + * 初始化并开始监听键盘事件 + */ + init() { + // 添加键盘事件监听 + this.container.addEventListener("keydown", this._handleKeyDown); + this.container.addEventListener("keyup", this._handleKeyUp); + + // 如果是触摸设备,添加触摸事件监听 + if (this.isTouchDevice) { + this.container.addEventListener("touchstart", this._handleTouchStart); + this.container.addEventListener("touchmove", this._handleTouchMove); + this.container.addEventListener("touchend", this._handleTouchEnd); + this.container.addEventListener("touchcancel", this._handleTouchEnd); + } + + console.log( + `键盘管理器已初始化,平台: ${this.platform}, 触摸设备: ${this.isTouchDevice}` + ); + } + + /** + * 处理键盘按下事件 + * @param {KeyboardEvent} event 键盘事件 + */ + handleKeyDown(event) { + // 如果当前焦点在输入框内,不处理大部分快捷键 + if (this.isInputActive() && !["Escape", "Tab"].includes(event.key)) { + return; + } + + // 构建快捷键标识符 + const shortcutKey = this.buildShortcutKey(event); + + // 查找并执行快捷键动作 + const shortcut = this.shortcuts[shortcutKey]; + if (shortcut) { + // 阻止默认行为,例如浏览器的保存对话框等 + if (shortcutKey.includes(`${this.modifierKeys.cmdOrCtrl}+`)) { + event.preventDefault(); + } + + this.executeAction(shortcut.action, shortcut.param, event); + return; + } + + // 工具快捷键处理 + if (this.toolManager && !event.ctrlKey && !event.metaKey && !event.altKey) { + this.toolManager.handleKeyboardShortcut(event); + } + } + + /** + * 处理键盘释放事件 + * @param {KeyboardEvent} event 键盘事件 + */ + handleKeyUp(event) { + // 当空格键释放时,如果是临时工具,切回原始工具 + if (event.key === " " && this.tempToolState.active) { + this.restoreTempTool(); + } + + // 调用自定义处理程序 + const key = event.key.toLowerCase(); + if ( + this.customHandlers[key] && + typeof this.customHandlers[key].onKeyUp === "function" + ) { + this.customHandlers[key].onKeyUp(event); + } + } + + /** + * 处理触摸开始事件 + * @param {TouchEvent} event 触摸事件 + */ + handleTouchStart(event) { + const touches = event.touches; + + // 存储初始状态以便后续计算 + if (touches.length === 2) { + // 双指触摸 - 可用于缩放或调整画笔大小 + this.touchState.isTwoFingerTouch = true; + this.touchState.pinchStartDistance = this.getDistanceBetweenTouches( + touches[0], + touches[1] + ); + + // 如果有画笔管理器,记录起始画笔大小 + if (this.toolManager && this.toolManager.brushManager) { + this.touchState.pinchStartBrushSize = + this.toolManager.brushManager.brushSize.value; + } + } else if (touches.length === 3) { + // 三指触摸 - 可用于撤销/重做 + this.touchState.touchStartX = touches[0].clientX; + } + } + + /** + * 处理触摸移动事件 + * @param {TouchEvent} event 触摸事件 + */ + handleTouchMove(event) { + const touches = event.touches; + + // 阻止默认行为(例如滚动) + if (touches.length >= 2) { + event.preventDefault(); + } + + // 双指缩放处理 - 调整画笔大小 + if (touches.length === 2 && this.touchState.isTwoFingerTouch) { + const currentDistance = this.getDistanceBetweenTouches( + touches[0], + touches[1] + ); + const scale = currentDistance / this.touchState.pinchStartDistance; + + // 调整画笔大小 + if (this.toolManager && this.toolManager.brushManager && scale !== 1) { + const newSize = this.touchState.pinchStartBrushSize * scale; + this.toolManager.brushManager.setBrushSize(newSize); + } + } + // 三指滑动处理 - 撤销/重做 + else if (touches.length === 3) { + const deltaX = touches[0].clientX - this.touchState.touchStartX; + + // 滑动超过50px认为是有效的手势 + if (Math.abs(deltaX) > 50) { + if (deltaX < 0) { + // 向左滑动 - 撤销 + this.executeAction("undo"); + } else { + // 向右滑动 - 重做 + this.executeAction("redo"); + } + + // 更新起始位置,防止连续触发 + this.touchState.touchStartX = touches[0].clientX; + } + } + } + + /** + * 处理触摸结束事件 + * @param {TouchEvent} event 触摸事件 + */ + handleTouchEnd(event) { + // 检测双指轻拍 (两个手指几乎同时按下,又几乎同时抬起) + if (this.touchState.isTwoFingerTouch && event.touches.length === 0) { + if (new Date().getTime() - this.touchState.touchStartTime < 300) { + // 双指轻拍 - 可以触发上下文菜单 + this.executeAction("contextMenu"); + } + } + + // 重置触摸状态 + this.touchState.isTwoFingerTouch = false; + } + + /** + * 计算两个触摸点之间的距离 + * @param {Touch} touch1 第一个触摸点 + * @param {Touch} touch2 第二个触摸点 + * @returns {number} 两点间距离 + */ + getDistanceBetweenTouches(touch1, touch2) { + const dx = touch1.clientX - touch2.clientX; + const dy = touch1.clientY - touch2.clientY; + return Math.sqrt(dx * dx + dy * dy); + } + + /** + * 执行快捷键对应的动作 + * @param {string} action 动作名称 + * @param {*} param 动作参数 + * @param {Event} event 原始事件 + */ + executeAction(action, param, event) { + switch (action) { + case "undo": + if (this.commandManager) { + this.commandManager.undo(); + } + break; + + case "redo": + if (this.commandManager) { + this.commandManager.redo(); + } + break; + + case "copy": + // 复制逻辑 + console.log("复制当前选中图层"); + this.layerManager.copyLayer(this.layerManager.activeLayerId.value); + break; + + case "paste": + // 粘贴逻辑 + console.log("粘贴"); + this.layerManager.pasteLayer(); + break; + + case "cut": + // 剪切逻辑 + console.log("剪切"); + this.layerManager.cutLayer(this.layerManager.activeLayerId.value); + break; + + case "delete": + // 删除逻辑 + console.log("删除"); + this.layerManager.removeLayer(this.layerManager.activeLayerId.value); + break; + + case "selectAll": + // 全选逻辑 + console.log("全选"); + // 这里需要实现全选逻辑 TODO: 是否在选择模式下才可以全选? + if (this.layerManager) { + this.layerManager.selectAll(); + } + break; + + case "clearSelection": + // 清除选择逻辑 + console.log("清除选择"); + // 这里需要实现清除选择逻辑 + if (this.layerManager) { + this.layerManager.clearSelection(); + } + break; + + case "save": + // 保存逻辑 + console.log("保存"); + break; + + case "selectTool": + // 选择工具 + if (this.toolManager && param) { + this.toolManager.setToolWithCommand(param); + } + break; + + case "increaseBrushSize": + // 增大画笔尺寸 + if (this.toolManager && this.toolManager.brushManager) { + const amount = param || 5; + this.toolManager.brushManager.increaseBrushSize(amount); + } + break; + + case "decreaseBrushSize": + // 减小画笔尺寸 + if (this.toolManager && this.toolManager.brushManager) { + const amount = param || 5; + this.toolManager.brushManager.decreaseBrushSize(amount); + } + break; + + case "increaseBrushOpacity": + // 增大画笔透明度 + if (this.toolManager && this.toolManager.brushManager) { + const amount = param || 0.01; + this.toolManager.brushManager.increaseBrushOpacity(amount); + } + break; + + case "decreaseTextureScale": + // 减小画笔材质图片大小 + if (this.toolManager && this.toolManager.brushManager) { + const amount = param || 5; + this.toolManager.brushManager.decreaseBrushSize(amount); + } + break; + + case "increaseTextureScale": + // 增大画笔材质图片大小 + if (this.toolManager && this.toolManager.brushManager) { + const amount = param || 0.01; + this.toolManager.brushManager.increaseTextureScale(amount); + } + break; + + case "decreaseBrushOpacity": + // 减小画笔透明度 + if (this.toolManager && this.toolManager.brushManager) { + const amount = param || 0.01; + this.toolManager.brushManager.decreaseBrushOpacity(amount); + } + break; + + case "toggleTempTool": + // 临时切换工具 + if (param && this.toolManager) { + this.setTempTool(param); + } + break; + + case "newLayer": + // 创建新图层 + if (this.layerManager) { + this.layerManager.createNewLayer(); + } + break; + + case "addImageToNewLayer": + this.toolManager?.openFile?.(); + break; + + case "groupLayers": + // 组合图层 + if (this.layerManager) { + this.layerManager.groupSelectedLayers(); + } + break; + + case "ungroupLayers": + // 解组图层 + if (this.layerManager) { + this.layerManager.ungroupSelectedLayer(); + } + break; + + case "mergeLayers": + // 合并图层 + if (this.layerManager) { + this.layerManager.mergeSelectedLayers(); + } + break; + + case "contextMenu": + // 上下文菜单(通常由右击或触控设备上的特定手势触发) + console.log("显示上下文菜单"); + // 这里需要实现显示上下文菜单的逻辑 + break; + + default: + // 调用自定义注册的动作处理 + if (this.customHandlers[action]) { + this.customHandlers[action].execute(param, event); + } + } + } + + /** + * 设置临时工具 + * @param {string} toolId 临时工具ID + */ + setTempTool(toolId) { + if (!this.toolManager || this.tempToolState.active) return; + + // 保存当前工具 + this.tempToolState.originalTool = this.toolManager.getCurrentTool(); + this.tempToolState.active = true; + + // 切换到临时工具 + this.toolManager.setTool(toolId); + } + + /** + * 恢复临时工具切换前的工具 + */ + restoreTempTool() { + if (!this.toolManager || !this.tempToolState.active) return; + + // 恢复到原始工具 + if (this.tempToolState.originalTool) { + this.toolManager.setTool(this.tempToolState.originalTool); + } + + // 重置状态 + this.tempToolState.active = false; + this.tempToolState.originalTool = null; + } + + /** + * 构建快捷键标识符 + * @param {KeyboardEvent} event 键盘事件 + * @returns {string} 快捷键标识符 + */ + buildShortcutKey(event) { + let shortcutKey = ""; + + // 统一处理Mac和PC的修饰键 + if ( + (this.platform === "mac" && event.metaKey) || + (this.platform !== "mac" && event.ctrlKey) + ) { + shortcutKey += `${this.modifierKeys.cmdOrCtrl}+`; + } else if (event.ctrlKey) { + shortcutKey += "ctrl+"; + } + + if (event.shiftKey) shortcutKey += "shift+"; + if (event.altKey) shortcutKey += "alt+"; + + const key = event.key.toLowerCase(); + + // 特殊键处理 + switch (key) { + case " ": + shortcutKey += "space"; + break; + case "arrowup": + shortcutKey += "up"; + break; + case "arrowdown": + shortcutKey += "down"; + break; + case "arrowleft": + shortcutKey += "left"; + break; + case "arrowright": + shortcutKey += "right"; + break; + default: + shortcutKey += key; + } + + return shortcutKey; + } + + /** + * 检查当前是否有输入框处于活动状态 + * @returns {boolean} 是否有输入框处于活动状态 + */ + isInputActive() { + const activeElement = document.activeElement; + const tagName = activeElement.tagName.toLowerCase(); + return ( + tagName === "input" || + tagName === "textarea" || + activeElement.getAttribute("contenteditable") === "true" + ); + } + + /** + * 获取所有可用的快捷键 + * @returns {Array} 快捷键列表 + */ + getShortcuts() { + return Object.entries(this.shortcuts).map(([key, value]) => ({ + key, + displayKey: this.formatShortcutForDisplay(key), + ...value, + })); + } + + /** + * 格式化快捷键以便显示 + * @param {string} shortcut 快捷键标识符 + * @returns {string} 格式化后的快捷键显示 + */ + formatShortcutForDisplay(shortcut) { + // 将快捷键格式化为适合当前平台显示的形式 + return shortcut + .split("+") + .map((key) => { + // 将键名转换为显示符号 + return this.keySymbols[key.toLowerCase()] || key.toUpperCase(); + }) + .join("+"); + } + + /** + * 注册自定义快捷键处理程序 + * @param {string} action 动作名称 + * @param {Object} handler 处理程序对象 + * @param {Function} handler.execute 执行函数 + * @param {Function} handler.onKeyUp 键释放处理函数(可选) + * @param {string} description 描述 + */ + registerCustomHandler(action, handler, description = "") { + if (!action || typeof handler.execute !== "function") { + console.error("无效的自定义处理程序"); + return; + } + + this.customHandlers[action] = handler; + + // 如果提供了快捷键,添加到快捷键映射 + if (handler.shortcut) { + this.shortcuts[handler.shortcut] = { + action, + description: description || handler.description || action, + }; + } + } + + /** + * 清理资源 + */ + dispose() { + // 移除事件监听 + this.container.removeEventListener("keydown", this._handleKeyDown); + this.container.removeEventListener("keyup", this._handleKeyUp); + + // 如果有触摸事件,也移除它们 + if (this.isTouchDevice) { + this.container.removeEventListener("touchstart", this._handleTouchStart); + this.container.removeEventListener("touchmove", this._handleTouchMove); + this.container.removeEventListener("touchend", this._handleTouchEnd); + this.container.removeEventListener("touchcancel", this._handleTouchEnd); + } + + // 清除引用 + this.toolManager = null; + this.commandManager = null; + this.layerManager = null; + this.container = null; + this.customHandlers = {}; + this.tempToolState = { active: false, originalTool: null }; + this.touchState = {}; + } +} diff --git a/src/component/Canvas/CanvasEditor/managers/liquify/EnhancedLiquifyManager.js b/src/component/Canvas/CanvasEditor/managers/liquify/EnhancedLiquifyManager.js new file mode 100644 index 00000000..7b34967f --- /dev/null +++ b/src/component/Canvas/CanvasEditor/managers/liquify/EnhancedLiquifyManager.js @@ -0,0 +1,702 @@ +/** + * 增强版液化管理器 + * 整合WebGL和CPU实现,智能选择最佳渲染方式 + */ +import { LiquifyWebGLManager } from "./LiquifyWebGLManager"; +import { LiquifyCPUManager } from "./LiquifyCPUManager"; + +export class EnhancedLiquifyManager { + /** + * 创建增强版液化管理器 + * @param {Object} options 配置选项 + */ + constructor(options = {}) { + this.config = { + // 性能阈值:图像超过此尺寸会尝试使用WebGL + webglSizeThreshold: options.webglSizeThreshold || 1000 * 1000, // 默认100万像素 + // 是否强制使用CPU模式 + forceCPU: options.forceCPU || false, + // 是否强制使用WebGL模式 + forceWebGL: options.forceWebGL || false, + // 网格大小 + gridSize: options.gridSize || 15, + // 最大变形强度 + maxStrength: options.maxStrength || 100, + // 平滑迭代次数 + smoothingIterations: options.smoothingIterations || 2, + // 网格弹性因子 + relaxFactor: options.relaxFactor || 0.25, + // WebGL网格精度 + meshResolution: options.meshResolution || 64, + }; + + // 性能监控 + this.performance = { + lastOperationTime: 0, + renderTimes: [], // 最近的渲染时间记录 + isPerformanceIssue: false, // 是否存在性能问题 + operationCount: 0, // 操作计数 + }; + + // 初始化标志 + this.initialized = false; + + // 当前参数 + this.params = { + size: 50, // 工具尺寸 + pressure: 0.5, // 压力大小 (0-1) + distortion: 0, // 失真程度 (0-1) + power: 0.5, // 动力/强度 (0-1) + }; + + // 液化工具模式 + this.modes = { + PUSH: "push", + CLOCKWISE: "clockwise", + COUNTERCLOCKWISE: "counterclockwise", + PINCH: "pinch", + EXPAND: "expand", + CRYSTAL: "crystal", + EDGE: "edge", + RECONSTRUCT: "reconstruct", + }; + + // 当前模式 + this.currentMode = this.modes.PUSH; + + // 图像数据和目标对象 + this.originalImageData = null; + this.currentImageData = null; + this.targetObject = null; + this.targetLayerId = null; + + // 创建渲染器实例 + this.webglRenderer = null; + this.cpuRenderer = null; + + // 当前激活的渲染器 + this.activeRenderer = null; + this.renderMode = "unknown"; // 'webgl', 'cpu', 'unknown' + + // 画布和管理器引用 + this.canvas = options.canvas || null; + this.layerManager = options.layerManager || null; + + // 渲染器状态 + this.isWebGLAvailable = LiquifyWebGLManager.isSupported(); + } + + /** + * 初始化液化管理器 + * @param {Object} options 配置选项 + * @returns {Boolean} 是否初始化成功 + */ + initialize(options = {}) { + if (options.canvas) this.canvas = options.canvas; + if (options.layerManager) this.layerManager = options.layerManager; + + if (!this.canvas || !this.layerManager) { + console.error("液化管理器初始化失败:缺少canvas或layerManager"); + return false; + } + + // 记录初始化时间,用于性能监控 + this.performance.lastInitTime = Date.now(); + + // 创建CPU渲染器 (始终创建作为备选) + this.cpuRenderer = new LiquifyCPUManager({ + gridSize: this.config.gridSize, + maxStrength: this.config.maxStrength, + smoothingIterations: this.config.smoothingIterations, + relaxFactor: this.config.relaxFactor, + }); + + // 检查是否应创建WebGL渲染器 + if (this.isWebGLAvailable && !this.config.forceCPU) { + this.webglRenderer = new LiquifyWebGLManager({ + gridSize: this.config.gridSize, + maxStrength: this.config.maxStrength, + meshResolution: this.config.meshResolution, + }); + } + + this.initialized = true; + return true; + } + + /** + * 为液化操作准备图像 + * @param {Object|String} target 目标对象或图层ID + * @returns {Promise} 准备结果 + */ + async prepareForLiquify(target) { + if (!this.initialized) { + throw new Error("液化管理器未初始化"); + } + + let targetObject, targetLayerId; + + // 处理传入的是图层ID的情况 + if (typeof target === "string") { + targetLayerId = target; + const layer = this.layerManager.getLayerById(targetLayerId); + + // 检查图层是否存在和是否有对象 + let hasObjects = false; + if (layer) { + if (layer.type === "background" && layer.fabricObject) { + hasObjects = true; + targetObject = layer.fabricObject; + } else if (layer.fabricObjects && layer.fabricObjects.length > 0) { + hasObjects = true; + targetObject = layer.fabricObjects[0]; + } + } + + if (!hasObjects) { + throw new Error("目标图层为空或不存在"); + } + } else if (typeof target === "object") { + // 传入的是对象 + targetObject = target; + const layer = this.layerManager.findLayerByObject(targetObject); + if (layer) { + targetLayerId = layer.id; + } else { + throw new Error("无法找到目标对象所属图层"); + } + } else { + throw new Error("无效的目标参数"); + } + + // 检查是否为图像对象 + if (!targetObject || targetObject.type !== "image") { + throw new Error("目标对象不是图像,无法进行液化操作"); + } + + // 保存目标对象引用 + this.targetObject = targetObject; + this.targetLayerId = targetLayerId; + + // 获取图像数据 + const imageData = await this._getImageData(targetObject); + if (!imageData) { + throw new Error("无法获取图像数据"); + } + + // 保存原始图像数据 + this.originalImageData = imageData; + this.currentImageData = this._cloneImageData(imageData); + + // 检查图像大小,选择适合的渲染器 + await this._selectRenderer(imageData); + + // 预热选定的渲染器 + await this._warmupRenderer(imageData); + + return { + targetObject: this.targetObject, + targetLayerId: this.targetLayerId, + imageData: this.currentImageData, + originalImageData: this.originalImageData, + renderMode: this.renderMode, + }; + } + + /** + * 根据图像大小和设备性能选择渲染器 + * @param {ImageData} imageData 图像数据 + * @private + */ + async _selectRenderer(imageData) { + // 计算图像大小 + const pixelCount = imageData.width * imageData.height; + + console.log( + `液化选择渲染器: 图像大小=${pixelCount}像素, WebGL可用=${this.isWebGLAvailable}` + ); + + // 默认使用CPU渲染器 + this.activeRenderer = this.cpuRenderer; + this.renderMode = "cpu"; + + // 如果配置强制使用WebGL + if (this.config.forceWebGL && this.isWebGLAvailable && this.webglRenderer) { + console.log("液化功能: 强制使用WebGL渲染模式"); + this.activeRenderer = this.webglRenderer; + this.renderMode = "webgl"; + return; + } + + // 如果配置强制使用CPU + if (this.config.forceCPU) { + console.log("液化功能: 强制使用CPU渲染模式"); + return; + } + + // 根据图像大小和WebGL可用性决定 + if ( + pixelCount > this.config.webglSizeThreshold / 2 && // 降低阈值,让更多尺寸的图像使用WebGL + this.isWebGLAvailable && + this.webglRenderer + ) { + // 切换到WebGL渲染器 + console.log("液化功能: 自动选择WebGL渲染模式(基于图像尺寸)"); + this.activeRenderer = this.webglRenderer; + this.renderMode = "webgl"; + } else { + console.log( + `液化功能: 使用CPU渲染模式${ + !this.isWebGLAvailable ? " (WebGL不可用)" : "" + }` + ); + } + } + + /** + * 预热渲染器 + * @param {ImageData} imageData 图像数据 + * @private + */ + async _warmupRenderer(imageData) { + // 创建图像元素 + const img = document.createElement("img"); + + // 将ImageData转换为URL + const canvas = document.createElement("canvas"); + canvas.width = imageData.width; + canvas.height = imageData.height; + const ctx = canvas.getContext("2d"); + ctx.putImageData(imageData, 0, 0); + + // 使用Promise等待图像加载 + await new Promise((resolve, reject) => { + img.onload = resolve; + img.onerror = reject; + img.src = canvas.toDataURL(); + }); + + // 初始化当前渲染器 + if (this.activeRenderer) { + if (this.renderMode === "webgl") { + this.activeRenderer.initialize(img); + } else { + this.activeRenderer.initialize(imageData); + } + } + } + + /** + * 设置液化模式 + * @param {String} mode 模式名称 + */ + setMode(mode) { + if (Object.values(this.modes).includes(mode)) { + this.currentMode = mode; + + // 同步更新当前渲染器 + if (this.activeRenderer) { + this.activeRenderer.setMode(mode); + } + + return true; + } + return false; + } + + /** + * 设置液化参数 + * @param {String} param 参数名称 + * @param {Number} value 参数值 + */ + setParam(param, value) { + if (param in this.params) { + this.params[param] = value; + + // 同步更新当前渲染器 + if (this.activeRenderer) { + this.activeRenderer.setParam(param, value); + } + + return true; + } + return false; + } + + /** + * 获取当前参数 + * @returns {Object} 当前参数对象 + */ + getParams() { + return { ...this.params }; + } + + /** + * 重置参数为默认值 + */ + resetParams() { + this.params = { + size: 50, + pressure: 0.5, + distortion: 0, + power: 0.5, + }; + + // 同步更新当前渲染器 + if (this.activeRenderer) { + this.activeRenderer.resetParams(); + } + } + + /** + * 应用液化变形 + * @param {Object} target 目标对象 + * @param {String} mode 液化模式 + * @param {Object} params 液化参数 + * @param {Number} x 操作中心点X坐标 (图像像素坐标) + * @param {Number} y 操作中心点Y坐标 (图像像素坐标) + * @returns {Promise} 处理后的图像数据 + */ + async applyLiquify(target, mode, params, x, y) { + // 性能追踪开始 + const startTime = performance.now(); + + // 如果首次调用,先准备环境 + if (!this.targetObject || this.targetObject !== target) { + await this.prepareForLiquify(target); + } + + // 更新模式和参数 + if (mode) this.setMode(mode); + if (params) { + for (const [key, value] of Object.entries(params)) { + this.setParam(key, value); + } + } + + // 验证坐标是否在图像范围内 + if (!this.originalImageData) { + console.error("缺少原始图像数据"); + return null; + } + + const imageWidth = this.originalImageData.width; + const imageHeight = this.originalImageData.height; + + // 坐标边界检查 + if (x < 0 || x >= imageWidth || y < 0 || y >= imageHeight) { + console.warn( + `液化坐标超出图像范围: (${x}, ${y}), 图像尺寸: ${imageWidth}x${imageHeight}` + ); + return null; + } + + console.log( + `应用液化变形: 模式=${mode}, 图像坐标=(${x}, ${y}), 图像尺寸=${imageWidth}x${imageHeight}` + ); + + // 检查并应用变形 + if (this.activeRenderer && typeof x === "number" && typeof y === "number") { + // 应用变形 + let result; + + if (this.renderMode === "webgl") { + // WebGL渲染器:传入图像像素坐标 + result = this.activeRenderer.applyDeformation(x, y); + } else { + // CPU渲染器:传入图像像素坐标 + result = this.activeRenderer.applyDeformation(x, y); + } + + // 更新当前图像数据 + if (result) { + this.currentImageData = result; + } + + // 性能追踪结束 + const endTime = performance.now(); + this._trackPerformance(endTime - startTime); + + return result; + } + + console.error("无法应用液化变形:渲染器未初始化或坐标无效"); + return null; + } + + /** + * 追踪性能数据 + * @param {Number} time 操作耗时(毫秒) + * @private + */ + _trackPerformance(time) { + this.performance.lastOperationTime = time; + this.performance.operationCount++; + + // 维护最近10次操作的耗时记录 + this.performance.renderTimes.push(time); + if (this.performance.renderTimes.length > 10) { + this.performance.renderTimes.shift(); + } + + // 计算平均耗时 + const avgTime = + this.performance.renderTimes.reduce((sum, t) => sum + t, 0) / + this.performance.renderTimes.length; + + // 检测性能问题 + this.performance.isPerformanceIssue = avgTime > 100; // 如果平均耗时超过100毫秒 + + // 输出性能信息(调试用) + if (this.performance.operationCount % 10 === 0) { + console.log( + `液化性能数据: 模式=${this.renderMode}, 平均耗时=${avgTime.toFixed( + 2 + )}ms, 图像尺寸=${this.originalImageData?.width}x${ + this.originalImageData?.height + }` + ); + } + + // 如果使用WebGL但性能差,可以考虑切换到优化的CPU实现 + if ( + this.renderMode === "webgl" && + this.performance.isPerformanceIssue && + this.performance.operationCount > 5 + ) { + console.warn("WebGL液化性能不佳,考虑切换到CPU模式"); + // 注意:这里不自动切换,因为可能会导致中途渲染结果不一致 + } + } + + /** + * 重置液化操作 + * @returns {ImageData} 重置后的图像数据 + */ + reset() { + if (!this.activeRenderer) return null; + + // 使用当前渲染器重置 + const result = this.activeRenderer.reset(); + + // 更新当前图像数据 + if (result) { + this.currentImageData = result; + } + + return result; + } + + /** + * 检查图层是否可以液化 + * @param {String} layerId 图层ID + * @returns {Object} 检查结果 + */ + checkLayerForLiquify(layerId) { + if (!this.layerManager) { + return { + valid: false, + message: "图层管理器未初始化", + needsRasterization: false, + isImage: false, + isEmpty: true, + isGroup: false, + }; + } + + // 获取图层 + const layer = this.layerManager.getLayerById(layerId); + if (!layer) { + return { + valid: false, + message: "图层不存在", + needsRasterization: false, + isImage: false, + isEmpty: true, + isGroup: false, + }; + } + + // 检查图层是否为空 + let objectsToCheck = []; + if (layer.isBackground || layer.type === "background") { + // 背景图层使用 fabricObject (单数) + if (layer.fabricObject) { + objectsToCheck = [layer.fabricObject]; + } + } else { + // 普通图层使用 fabricObjects (复数) + objectsToCheck = layer.fabricObjects || []; + } + + if (objectsToCheck.length === 0) { + return { + valid: false, + message: "图层为空,无法进行液化操作", + needsRasterization: false, + isImage: false, + isEmpty: true, + isGroup: false, + }; + } + + // 检查是否为单一图像 + const singleObject = objectsToCheck.length === 1; + const isImage = + singleObject && + (objectsToCheck[0].type === "image" || + objectsToCheck[0].type === "rasterized-layer"); + + // 检查是否为组 + const isGroup = objectsToCheck.some((obj) => obj.type === "group"); + + // 如果不是单一图像,需要栅格化 + const needsRasterization = !isImage || isGroup; + + return { + valid: isImage && !isGroup, + message: isImage ? "图层可以进行液化操作" : "需要先将图层栅格化", + needsRasterization: needsRasterization, + isImage: isImage, + isEmpty: false, + isGroup: isGroup, + }; + } + + /** + * 获取图像数据 + * @param {Object} fabricObject Fabric图像对象 + * @returns {Promise} 图像数据 + * @private + */ + async _getImageData(fabricObject) { + return new Promise((resolve, reject) => { + try { + // 创建临时canvas + const tempCanvas = document.createElement("canvas"); + tempCanvas.width = fabricObject.width * fabricObject.scaleX; + tempCanvas.height = fabricObject.height * fabricObject.scaleY; + const tempCtx = tempCanvas.getContext("2d"); + + // 如果对象有图像元素 + if (fabricObject._element) { + tempCtx.drawImage( + fabricObject._element, + 0, + 0, + tempCanvas.width, + tempCanvas.height + ); + } else if (fabricObject.getSrc) { + // 通过URL创建图像 + const img = new Image(); + img.onload = () => { + tempCtx.drawImage(img, 0, 0, tempCanvas.width, tempCanvas.height); + const imageData = tempCtx.getImageData( + 0, + 0, + tempCanvas.width, + tempCanvas.height + ); + resolve(imageData); + }; + img.onerror = reject; + img.src = fabricObject.getSrc(); + return; + } else { + reject(new Error("无法获取图像数据")); + return; + } + + // 获取图像数据 + const imageData = tempCtx.getImageData( + 0, + 0, + tempCanvas.width, + tempCanvas.height + ); + resolve(imageData); + } catch (error) { + reject(error); + } + }); + } + + /** + * 克隆图像数据 + * @param {ImageData} imageData 原始图像数据 + * @returns {ImageData} 克隆的图像数据 + * @private + */ + _cloneImageData(imageData) { + if (!imageData) return null; + + // 使用新的浏览器API直接复制 + if (typeof ImageData.prototype.constructor === "function") { + try { + return new ImageData( + new Uint8ClampedArray(imageData.data), + imageData.width, + imageData.height + ); + } catch (e) { + console.warn("使用备选方法克隆ImageData"); + } + } + + // 备选方法 + const canvas = document.createElement("canvas"); + const ctx = canvas.getContext("2d"); + + canvas.width = imageData.width; + canvas.height = imageData.height; + + ctx.putImageData(imageData, 0, 0); + + return ctx.getImageData(0, 0, imageData.width, imageData.height); + } + + /** + * 释放资源 + */ + dispose() { + // 释放渲染器资源 + if (this.webglRenderer) { + this.webglRenderer.dispose(); + this.webglRenderer = null; + } + + if (this.cpuRenderer) { + this.cpuRenderer.dispose(); + this.cpuRenderer = null; + } + + // 清除引用 + this.activeRenderer = null; + this.canvas = null; + this.layerManager = null; + this.targetObject = null; + this.originalImageData = null; + this.currentImageData = null; + this.initialized = false; + this.renderMode = "unknown"; + } + + /** + * 获取当前状态信息 + * @returns {Object} 状态信息 + */ + getStatus() { + return { + initialized: this.initialized, + renderMode: this.renderMode, + isWebGLAvailable: this.isWebGLAvailable, + currentMode: this.currentMode, + params: { ...this.params }, + performance: { ...this.performance }, + imageSize: this.originalImageData + ? `${this.originalImageData.width}x${this.originalImageData.height}` + : "N/A", + }; + } +} diff --git a/src/component/Canvas/CanvasEditor/managers/liquify/LiquifyCPUManager.js b/src/component/Canvas/CanvasEditor/managers/liquify/LiquifyCPUManager.js new file mode 100644 index 00000000..45a18071 --- /dev/null +++ b/src/component/Canvas/CanvasEditor/managers/liquify/LiquifyCPUManager.js @@ -0,0 +1,594 @@ +/** + * CPU版本的液化管理器 + * 修复版本 - 解决三角形网格失真问题 + */ +export class LiquifyCPUManager { + constructor(options = {}) { + this.config = { + gridSize: options.gridSize || 16, // 稍微增大网格提高性能 + maxStrength: options.maxStrength || 200, // 适度降低最大强度 + smoothingIterations: options.smoothingIterations || 1, // 增加平滑处理 + relaxFactor: options.relaxFactor || 0.1, // 适度松弛 + }; + + this.params = { + size: 80, // 增大默认尺寸 + pressure: 0.8, // 增大默认压力 + distortion: 0, + power: 0.8, // 增大默认动力 + }; + + this.modes = { + PUSH: "push", + CLOCKWISE: "clockwise", + COUNTERCLOCKWISE: "counterclockwise", + PINCH: "pinch", + EXPAND: "expand", + CRYSTAL: "crystal", + EDGE: "edge", + RECONSTRUCT: "reconstruct", + }; + + this.currentMode = this.modes.PUSH; + this.originalImageData = null; + this.currentImageData = null; + this.mesh = null; + this.initialized = false; + this.canvas = document.createElement("canvas"); + this.ctx = this.canvas.getContext("2d"); + this.deformHistory = []; + + // 性能优化相关 + this.lastUpdateTime = 0; + this.updateThrottle = 16; // 限制更新频率约60fps + this.isProcessing = false; + + // 鼠标位置跟踪(用于推拉模式) + this.lastMouseX = 0; + this.lastMouseY = 0; + this.mouseMovementX = 0; + this.mouseMovementY = 0; + this.isFirstApply = true; // 标记是否是首次应用 + } + + initialize(imageSource) { + try { + if (imageSource instanceof ImageData) { + this.originalImageData = new ImageData( + new Uint8ClampedArray(imageSource.data), + imageSource.width, + imageSource.height + ); + } else if (imageSource instanceof HTMLImageElement) { + this.canvas.width = imageSource.width; + this.canvas.height = imageSource.height; + this.ctx.drawImage(imageSource, 0, 0); + this.originalImageData = this.ctx.getImageData( + 0, + 0, + imageSource.width, + imageSource.height + ); + } else { + throw new Error("不支持的图像类型"); + } + + this.currentImageData = new ImageData( + new Uint8ClampedArray(this.originalImageData.data), + this.originalImageData.width, + this.originalImageData.height + ); + + this._initMesh( + this.originalImageData.width, + this.originalImageData.height + ); + this.initialized = true; + return true; + } catch (error) { + console.error("液化管理器初始化失败:", error); + return false; + } + } + + _initMesh(width, height) { + const gridSize = this.config.gridSize; + const cols = Math.ceil(width / gridSize); + const rows = Math.ceil(height / gridSize); + + this.mesh = { + cols, + rows, + gridSize, + width, + height, + originalPoints: [], + deformedPoints: [], + }; + + for (let y = 0; y <= rows; y++) { + for (let x = 0; x <= cols; x++) { + const point = { x: x * gridSize, y: y * gridSize }; + this.mesh.originalPoints.push({ ...point }); + this.mesh.deformedPoints.push({ ...point }); + } + } + } + + setMode(mode) { + if (Object.values(this.modes).includes(mode)) { + this.currentMode = mode; + return true; + } + return false; + } + + setParam(param, value) { + if (param in this.params) { + this.params[param] = value; + return true; + } + return false; + } + + getParams() { + return { ...this.params }; + } + + resetParams() { + this.params = { + size: 80, // 增大默认尺寸 + pressure: 0.8, // 增大默认压力 + distortion: 0, + power: 0.8, // 增大默认动力 + }; + } + + applyDeformation(x, y) { + // 计算鼠标移动方向 + if (!this.isFirstApply) { + this.mouseMovementX = x - this.lastMouseX; + this.mouseMovementY = y - this.lastMouseY; + } else { + // 首次应用时不计算移动,避免初始变形 + this.mouseMovementX = 0; + this.mouseMovementY = 0; + this.isFirstApply = false; + } + + this.lastMouseX = x; + this.lastMouseY = y; + + // 性能优化:限制更新频率 + const now = Date.now(); + if (now - this.lastUpdateTime < this.updateThrottle || this.isProcessing) { + return this.currentImageData; + } + + this.isProcessing = true; + this.lastUpdateTime = now; + + if (!this.initialized || !this.mesh) { + this.isProcessing = false; + return this.currentImageData; + } + + const { size, pressure, distortion, power } = this.params; + const mode = this.currentMode; + const radius = size * 1.2; // 稍微增大影响半径 + const strength = (pressure * power * this.config.maxStrength) / 20; // 调整基础强度 + + this._applyDeformation(x, y, radius, strength, mode, distortion); + + if (this.config.smoothingIterations > 0) { + this._smoothMesh(); + } + + const result = this._applyMeshToImage(); + this.isProcessing = false; + return result; + } + + _applyDeformation(x, y, radius, strength, mode, distortion) { + if (!this.mesh) return; + + const points = this.mesh.deformedPoints; + const originalPoints = this.mesh.originalPoints; + + for (let i = 0; i < points.length; i++) { + const point = points[i]; + const originalPoint = originalPoints[i]; + const dx = point.x - x; + const dy = point.y - y; + const distance = Math.sqrt(dx * dx + dy * dy); + + if (distance < radius && distance > 0) { + // 使用平方衰减函数 + const factor = Math.pow(1 - distance / radius, 2) * strength * 0.1; // 大幅降低基础系数 + + switch (mode) { + case this.modes.PUSH: { + // 推拉模式 - 真正的拖拽效果 + // 计算实际移动距离 + const movementLength = Math.sqrt( + this.mouseMovementX * this.mouseMovementX + + this.mouseMovementY * this.mouseMovementY + ); + + // 只有在有足够移动距离时才应用效果 + if (movementLength > 1.0) { + // 提高阈值,确保有明显移动 + // 归一化移动方向 + const moveX = this.mouseMovementX / movementLength; + const moveY = this.mouseMovementY / movementLength; + + // 计算衰减(距离中心越近,效果越强) + const radiusRatio = distance / radius; + const falloff = Math.pow(1 - radiusRatio, 2.0); // 使用更强的衰减 + + // 基于实际移动距离计算强度 + const { pressure, power } = this.params; + const moveStrength = pressure * power * movementLength * 0.3; // 降低移动强度系数 + + // 计算最终拖拽强度 + const dragStrength = moveStrength * falloff * factor; + + // 向鼠标移动方向拖拽 + const dragX = moveX * dragStrength; + const dragY = moveY * dragStrength; + + // 应用变形,但限制最大变形量 + const maxDeform = 2.0; // 限制单次最大变形量 + point.x += Math.max(-maxDeform, Math.min(maxDeform, dragX)); + point.y += Math.max(-maxDeform, Math.min(maxDeform, dragY)); + } + break; + } + case this.modes.CLOCKWISE: + case this.modes.COUNTERCLOCKWISE: { + // 旋转模式 - 保持原有效果 + const angle = Math.atan2(dy, dx); + const direction = mode === this.modes.CLOCKWISE ? 1 : -1; + const rotationAngle = angle + direction * factor; + const newX = x + Math.cos(rotationAngle) * distance; + const newY = y + Math.sin(rotationAngle) * distance; + + point.x += (newX - point.x) * 0.8; + point.y += (newY - point.y) * 0.8; + break; + } + case this.modes.PINCH: { + // 捏合模式 - 保持原有效果 + const pinchStrength = factor * 1.2; + point.x -= dx * pinchStrength; + point.y -= dy * pinchStrength; + break; + } + case this.modes.EXPAND: { + // 展开模式 - 参考捏合的反向操作 + const expandFactor = factor * 1.5; + point.x += dx * expandFactor; + point.y += dy * expandFactor; + break; + } + case this.modes.CRYSTAL: { + // 水晶模式 - 参考旋转算法创建多重波形 + const crystalAngle = Math.atan2(dy, dx); + const crystalRadius = distance / radius; + + // 确保有基础效果 + const baseDistortion = Math.max(distortion, 0.3); + + // 多重波形 - 类似旋转的角度调制 + const wave1 = Math.sin(crystalAngle * 8) * 0.6; + const wave2 = Math.cos(crystalAngle * 12) * 0.4; + const waveAngle = crystalAngle + (wave1 + wave2) * baseDistortion; + + // 径向调制 - 类似旋转的距离调制 + const radialMod = 1 + Math.sin(crystalRadius * Math.PI * 2) * 0.3; + const modDistance = distance * radialMod; + + const crystalX = x + Math.cos(waveAngle) * modDistance; + const crystalY = y + Math.sin(waveAngle) * modDistance; + + const crystalFactor = factor * baseDistortion; + point.x += (crystalX - point.x) * crystalFactor; + point.y += (crystalY - point.y) * crystalFactor; + break; + } + case this.modes.EDGE: { + // 边缘模式 - 参考旋转算法创建垂直波纹 + const edgeAngle = Math.atan2(dy, dx); + const edgeRadius = distance / radius; + + // 确保有基础效果 + const baseEdgeDistortion = Math.max(distortion, 0.5); + + // 边缘波纹 - 垂直于径向的调制 + const edgeWave = + Math.sin(edgeRadius * Math.PI * 4) * Math.cos(edgeAngle * 6); + const perpAngle = edgeAngle + Math.PI / 2; // 垂直角度 + + const edgeFactor = edgeWave * factor * baseEdgeDistortion; + const edgeOffsetX = Math.cos(perpAngle) * edgeFactor; + const edgeOffsetY = Math.sin(perpAngle) * edgeFactor; + + point.x += edgeOffsetX; + point.y += edgeOffsetY; + break; + } + case this.modes.RECONSTRUCT: { + // 重建模式 - 向原始位置恢复 + const restoreFactor = factor * 0.15; + point.x += (originalPoint.x - point.x) * restoreFactor; + point.y += (originalPoint.y - point.y) * restoreFactor; + break; + } + } + } + } + } + + // 优化衰减函数,使过渡更平滑 + _smoothFalloff(t) { + if (t >= 1) return 0; + // 使用更平滑的衰减曲线 + const smoothT = 1 - t; + return smoothT * smoothT * smoothT * (3 - 2 * smoothT); + } + + _smoothMesh() { + const { rows, cols } = this.mesh; + const points = this.mesh.deformedPoints; + const tempPoints = points.map((p) => ({ x: p.x, y: p.y })); + + for ( + let iteration = 0; + iteration < this.config.smoothingIterations; + iteration++ + ) { + for (let y = 1; y < rows; y++) { + for (let x = 1; x < cols; x++) { + const idx = y * (cols + 1) + x; + const left = points[y * (cols + 1) + (x - 1)]; + const right = points[y * (cols + 1) + (x + 1)]; + const top = points[(y - 1) * (cols + 1) + x]; + const bottom = points[(y + 1) * (cols + 1) + x]; + + const centerX = (left.x + right.x + top.x + bottom.x) / 4; + const centerY = (left.y + right.y + top.y + bottom.y) / 4; + + const relaxFactor = this.config.relaxFactor; + tempPoints[idx].x += (centerX - points[idx].x) * relaxFactor; + tempPoints[idx].y += (centerY - points[idx].y) * relaxFactor; + } + } + + for (let i = 0; i < points.length; i++) { + points[i].x = tempPoints[i].x; + points[i].y = tempPoints[i].y; + } + } + } + + _applyMeshToImage() { + if (!this.mesh || !this.originalImageData) { + return this.currentImageData; + } + + const width = this.originalImageData.width; + const height = this.originalImageData.height; + const result = new ImageData(width, height); + const srcData = this.originalImageData.data; + const dstData = result.data; + + // 性能优化:使用步长采样减少计算量 + const step = width > 1000 || height > 1000 ? 2 : 1; + + for (let y = 0; y < height; y += step) { + for (let x = 0; x < width; x += step) { + const srcPos = this._mapPointBack(x, y); + + if ( + srcPos.x >= 0 && + srcPos.x < width && + srcPos.y >= 0 && + srcPos.y < height + ) { + const color = this._bilinearInterpolate( + srcData, + width, + height, + srcPos.x, + srcPos.y + ); + + // 如果使用步长采样,需要填充相邻像素 + for (let dy = 0; dy < step && y + dy < height; dy++) { + for (let dx = 0; dx < step && x + dx < width; dx++) { + const dstIdx = ((y + dy) * width + (x + dx)) * 4; + dstData[dstIdx] = color[0]; + dstData[dstIdx + 1] = color[1]; + dstData[dstIdx + 2] = color[2]; + dstData[dstIdx + 3] = color[3]; + } + } + } + } + } + + this.currentImageData = result; + return result; + } + + // 添加异步处理方法用于大图像 + async applyDeformationAsync(x, y) { + return new Promise((resolve) => { + setTimeout(() => { + const result = this.applyDeformation(x, y); + resolve(result); + }, 0); + }); + } + + // 批量处理方法 + applyDeformationBatch(positions) { + if (!this.initialized || !this.mesh || positions.length === 0) { + return this.currentImageData; + } + + const { size, pressure, distortion, power } = this.params; + const mode = this.currentMode; + const radius = size * 1.0; + const strength = (pressure * power * this.config.maxStrength) / 60; + + // 批量应用所有变形 + positions.forEach((pos) => { + this._applyDeformation( + pos.x, + pos.y, + radius * 0.5, + strength * 0.3, + mode, + distortion + ); + }); + + if (this.config.smoothingIterations > 0) { + this._smoothMesh(); + } + + return this._applyMeshToImage(); + } + + _mapPointBack(x, y) { + const { cols, rows, gridSize } = this.mesh; + const gridX = x / gridSize; + const gridY = y / gridSize; + + const x1 = Math.floor(gridX); + const y1 = Math.floor(gridY); + const x2 = Math.min(x1 + 1, cols); + const y2 = Math.min(y1 + 1, rows); + + const fx = gridX - x1; + const fy = gridY - y1; + + // 获取四个网格点的变形和原始坐标 + const deformed = [ + this.mesh.deformedPoints[y1 * (cols + 1) + x1], + this.mesh.deformedPoints[y1 * (cols + 1) + x2], + this.mesh.deformedPoints[y2 * (cols + 1) + x1], + this.mesh.deformedPoints[y2 * (cols + 1) + x2], + ]; + + const original = [ + this.mesh.originalPoints[y1 * (cols + 1) + x1], + this.mesh.originalPoints[y1 * (cols + 1) + x2], + this.mesh.originalPoints[y2 * (cols + 1) + x1], + this.mesh.originalPoints[y2 * (cols + 1) + x2], + ]; + + // 双线性插值计算变形后的位置 + const deformedX = + (1 - fx) * (1 - fy) * deformed[0].x + + fx * (1 - fy) * deformed[1].x + + (1 - fx) * fy * deformed[2].x + + fx * fy * deformed[3].x; + const deformedY = + (1 - fx) * (1 - fy) * deformed[0].y + + fx * (1 - fy) * deformed[1].y + + (1 - fx) * fy * deformed[2].y + + fx * fy * deformed[3].y; + + // 计算原始网格位置 + const originalX = x1 * gridSize + fx * gridSize; + const originalY = y1 * gridSize + fy * gridSize; + + // 计算偏移量并应用反向映射 + const offsetX = deformedX - originalX; + const offsetY = deformedY - originalY; + + return { + x: x - offsetX, + y: y - offsetY, + }; + } + + _bilinearInterpolate(data, width, height, x, y) { + const x1 = Math.floor(x); + const y1 = Math.floor(y); + const x2 = Math.min(x1 + 1, width - 1); + const y2 = Math.min(y1 + 1, height - 1); + + const fx = x - x1; + const fy = y - y1; + + const getPixel = (px, py) => { + const idx = (py * width + px) * 4; + return [data[idx], data[idx + 1], data[idx + 2], data[idx + 3]]; + }; + + const p1 = getPixel(x1, y1); + const p2 = getPixel(x2, y1); + const p3 = getPixel(x1, y2); + const p4 = getPixel(x2, y2); + + return [ + Math.round( + (1 - fx) * (1 - fy) * p1[0] + + fx * (1 - fy) * p2[0] + + (1 - fx) * fy * p3[0] + + fx * fy * p4[0] + ), + Math.round( + (1 - fx) * (1 - fy) * p1[1] + + fx * (1 - fy) * p2[1] + + (1 - fx) * fy * p3[1] + + fx * fy * p4[1] + ), + Math.round( + (1 - fx) * (1 - fy) * p1[2] + + fx * (1 - fy) * p2[2] + + (1 - fx) * fy * p3[2] + + fx * fy * p4[2] + ), + Math.round( + (1 - fx) * (1 - fy) * p1[3] + + fx * (1 - fy) * p2[3] + + (1 - fx) * fy * p3[3] + + fx * fy * p4[3] + ), + ]; + } + + reset() { + if (!this.mesh || !this.originalImageData) return false; + + for (let i = 0; i < this.mesh.deformedPoints.length; i++) { + this.mesh.deformedPoints[i].x = this.mesh.originalPoints[i].x; + this.mesh.deformedPoints[i].y = this.mesh.originalPoints[i].y; + } + + this.currentImageData = new ImageData( + new Uint8ClampedArray(this.originalImageData.data), + this.originalImageData.width, + this.originalImageData.height + ); + + this.deformHistory = []; + return true; + } + + getCurrentImageData() { + return this.currentImageData; + } + + destroy() { + this.originalImageData = null; + this.currentImageData = null; + this.mesh = null; + this.deformHistory = []; + this.initialized = false; + } +} diff --git a/src/component/Canvas/CanvasEditor/managers/liquify/LiquifyManager.js b/src/component/Canvas/CanvasEditor/managers/liquify/LiquifyManager.js new file mode 100644 index 00000000..bf33ec37 --- /dev/null +++ b/src/component/Canvas/CanvasEditor/managers/liquify/LiquifyManager.js @@ -0,0 +1,191 @@ +/** + * 液化管理器 + * 负责管理液化操作的核心算法和变形处理 + * + * 此版本使用增强的液化算法,支持GPU加速和优化的CPU处理 + */ +import { EnhancedLiquifyManager } from "./EnhancedLiquifyManager"; + +export class LiquifyManager { + /** + * 创建液化管理器 + * @param {Object} options 配置选项 + */ + constructor(options = {}) { + // 将核心属性暴露给外部,保持API兼容性 + this.canvas = options.canvas || null; + this.layerManager = options.layerManager || null; + + // 配置参数 + this.config = { + gridSize: options.gridSize || 20, + maxStrength: options.maxStrength || 100, + defaultParams: { + size: 50, + pressure: 0.5, + distortion: 0, + power: 0.5, + }, + }; + + // 创建增强版液化管理器实例 + this.enhancedManager = new EnhancedLiquifyManager({ + // 配置选项 + gridSize: options.gridSize || 15, + maxStrength: options.maxStrength || 100, + smoothingIterations: options.smoothingIterations || 2, + relaxFactor: options.relaxFactor || 0.25, + meshResolution: options.meshResolution || 64, + // 根据环境选择合适的渲染模式 + forceCPU: true, // 默认不强制使用CPU + forceWebGL: false, // 优先使用WebGL模式 + webglSizeThreshold: options.webglSizeThreshold || 500 * 500, // 降低阈值以更倾向使用WebGL + layerManager: options.layerManager || null, + canvas: options.canvas || null, + }); + + // 初始化液化管理器 + this.initialize(); + } + + /** + * 初始化液化管理器 + * @param {Object} options 配置选项 + */ + initialize(options = {}) { + // 更新基础属性 + if (options.canvas) this.canvas = options.canvas; + if (options.layerManager) this.layerManager = options.layerManager; + + // 初始化增强液化管理器 + return this.enhancedManager.initialize({ + canvas: this.canvas, + layerManager: this.layerManager, + }); + } + + /** + * 为液化操作准备图像 + * @param {Object|String} target 目标对象或图层ID + * @returns {Promise} 准备结果 + */ + async prepareForLiquify(target) { + return this.enhancedManager.prepareForLiquify(target); + } + + /** + * 设置液化模式 + * @param {String} mode 液化模式 + */ + setMode(mode) { + return this.enhancedManager.setMode(mode); + } + + /** + * 设置液化参数 + * @param {String} param 参数名称 + * @param {Number} value 参数值 + */ + setParam(param, value) { + return this.enhancedManager.setParam(param, value); + } + + /** + * 获取当前参数 + * @returns {Object} 当前参数对象 + */ + getParams() { + return this.enhancedManager.getParams(); + } + + /** + * 重置参数为默认值 + */ + resetParams() { + return this.enhancedManager.resetParams(); + } + + /** + * 应用液化效果 + * @param {fabric.Object} targetObject 目标对象 + * @param {String} mode 液化模式 + * @param {Object} params 参数 + * @param {Number} x X坐标 + * @param {Number} y Y坐标 + * @returns {ImageData} 处理后的图像数据 + */ + async applyLiquify(targetObject, mode, params, x, y) { + if (!this.enhancedManager || !targetObject) { + console.error("液化管理器未正确初始化"); + return null; + } + + // 确保设置正确的模式和参数 + if (mode) { + this.enhancedManager.setMode(mode); + } + + if (params) { + Object.entries(params).forEach(([key, value]) => { + this.enhancedManager.setParam(key, value); + }); + } + + // 应用液化变形 + console.log(`应用液化变形, 模式=${mode}, 坐标=(${x}, ${y}), 参数=`, params); + try { + // 直接调用EnhancedLiquifyManager的applyLiquify方法 + const resultData = await this.enhancedManager.applyLiquify( + targetObject, + mode, + params, + x, + y + ); + + // 确保返回结果数据 + if (!resultData) { + console.warn("液化变形没有返回结果数据"); + } + + return resultData; + } catch (error) { + console.error("液化变形应用失败:", error); + return null; + } + } + + /** + * 重置液化操作 + * @returns {ImageData} 重置后的图像数据 + */ + reset() { + return this.enhancedManager.reset(); + } + + /** + * 检查图层是否可以液化 + * @param {String} layerId 图层ID + * @returns {Object} 检查结果 + */ + checkLayerForLiquify(layerId) { + return this.enhancedManager.checkLayerForLiquify(layerId); + } + + /** + * 获取当前状态信息 + * @returns {Object} 状态信息 + */ + getStatus() { + return this.enhancedManager.getStatus(); + } + + /** + * 释放资源 + */ + dispose() { + if (this.enhancedManager) { + this.enhancedManager.dispose(); + } + } +} diff --git a/src/component/Canvas/CanvasEditor/managers/liquify/LiquifyWebGLManager.js b/src/component/Canvas/CanvasEditor/managers/liquify/LiquifyWebGLManager.js new file mode 100644 index 00000000..67dcfafe --- /dev/null +++ b/src/component/Canvas/CanvasEditor/managers/liquify/LiquifyWebGLManager.js @@ -0,0 +1,878 @@ +/** + * WebGL加速的液化管理器 + * 使用WebGL技术进行加速液化变形处理 + */ +export class LiquifyWebGLManager { + /** + * 创建WebGL液化管理器 + * @param {Object} options 配置选项 + */ + constructor(options = {}) { + this.canvas = null; + this.gl = null; + this.program = null; + this.texture = null; + this.mesh = null; + this.initialized = false; + this.originalImageData = null; + this.currentImageData = null; + + // 变形配置 + this.config = { + gridSize: options.gridSize || 20, + maxStrength: options.maxStrength || 100, + textureSize: 0, + meshResolution: options.meshResolution || 64, + }; + + // 当前参数 + this.params = { + size: 80, // 增大默认尺寸 + pressure: 0.8, // 增大默认压力 + distortion: 0, + power: 0.8, // 增大默认动力 + }; + + // 鼠标位置跟踪(用于推拉模式) + this.lastMouseX = 0; + this.lastMouseY = 0; + this.mouseMovementX = 0; + this.mouseMovementY = 0; + this.isFirstApply = true; // 标记是否是首次应用 + + // 液化工具模式 + this.modes = { + PUSH: "push", + CLOCKWISE: "clockwise", + COUNTERCLOCKWISE: "counterclockwise", + PINCH: "pinch", + EXPAND: "expand", + CRYSTAL: "crystal", + EDGE: "edge", + RECONSTRUCT: "reconstruct", + }; + this.currentMode = this.modes.PUSH; + + // 变形点历史记录 + this.deformHistory = []; + + // WebGL着色器程序 + this.vertexShaderSource = ` + attribute vec2 a_position; + attribute vec2 a_texCoord; + + uniform mat3 u_matrix; + + varying vec2 v_texCoord; + + void main() { + gl_Position = vec4((u_matrix * vec3(a_position, 1)).xy, 0, 1); + v_texCoord = a_texCoord; + } + `; + + this.fragmentShaderSource = ` + precision mediump float; + + uniform sampler2D u_image; + uniform vec2 u_textureSize; + + varying vec2 v_texCoord; + + void main() { + vec2 onePixel = vec2(1.0, 1.0) / u_textureSize; + vec4 color = texture2D(u_image, v_texCoord); + + // 简单的边缘检查,保证边缘渲染正确 + if(v_texCoord.x < 0.0 || v_texCoord.x > 1.0 || + v_texCoord.y < 0.0 || v_texCoord.y > 1.0) { + gl_FragColor = vec4(0.0, 0.0, 0.0, 0.0); + } else { + gl_FragColor = color; + } + } + `; + + // 变形网格着色器程序 + this.deformVertexShaderSource = ` + attribute vec2 a_position; + attribute vec2 a_texCoord; + attribute vec2 a_deformation; + + varying vec2 v_texCoord; + + void main() { + vec2 position = a_position + a_deformation; + gl_Position = vec4(position * 2.0 - 1.0, 0, 1); + v_texCoord = a_texCoord; + } + `; + + this.deformFragmentShaderSource = ` + precision mediump float; + + uniform sampler2D u_image; + + varying vec2 v_texCoord; + + void main() { + vec4 color = texture2D(u_image, v_texCoord); + gl_FragColor = color; + } + `; + } + + /** + * 初始化WebGL环境 + * @param {HTMLImageElement} image 图像元素 + * @returns {Boolean} 是否初始化成功 + */ + initialize(image) { + // 创建WebGL Canvas + this.canvas = document.createElement("canvas"); + + // 设置canvas大小与图像相同 + this.canvas.width = image.width; + this.canvas.height = image.height; + + // 尝试获取WebGL上下文 + try { + this.gl = + this.canvas.getContext("webgl") || + this.canvas.getContext("experimental-webgl"); + } catch (e) { + console.error("WebGL初始化失败:", e); + return false; + } + + if (!this.gl) { + console.error("WebGL不可用"); + return false; + } + + // 设置视口 + this.gl.viewport(0, 0, this.canvas.width, this.canvas.height); + + // 编译着色器程序 + if (!this._createShaderProgram()) { + console.error("着色器程序创建失败"); + return false; + } + + // 创建纹理 + this.texture = this._createTexture(image); + if (!this.texture) { + console.error("纹理创建失败"); + return false; + } + + // 记录原始图像数据 + const tempCanvas = document.createElement("canvas"); + tempCanvas.width = image.width; + tempCanvas.height = image.height; + const tempCtx = tempCanvas.getContext("2d"); + tempCtx.drawImage(image, 0, 0); + this.originalImageData = tempCtx.getImageData( + 0, + 0, + image.width, + image.height + ); + this.currentImageData = new ImageData( + new Uint8ClampedArray(this.originalImageData.data), + this.originalImageData.width, + this.originalImageData.height + ); + + // 创建变形网格 + this._createDeformMesh(); + + this.config.textureSize = [image.width, image.height]; + this.initialized = true; + + return true; + } + + /** + * 创建着色器程序 + * @returns {Boolean} 是否创建成功 + * @private + */ + _createShaderProgram() { + // 创建标准渲染程序 + const vertexShader = this._compileShader( + this.vertexShaderSource, + this.gl.VERTEX_SHADER + ); + const fragmentShader = this._compileShader( + this.fragmentShaderSource, + this.gl.FRAGMENT_SHADER + ); + + if (!vertexShader || !fragmentShader) return false; + + // 创建程序 + this.program = this.gl.createProgram(); + this.gl.attachShader(this.program, vertexShader); + this.gl.attachShader(this.program, fragmentShader); + this.gl.linkProgram(this.program); + + if (!this.gl.getProgramParameter(this.program, this.gl.LINK_STATUS)) { + console.error( + "着色器程序链接失败:", + this.gl.getProgramInfoLog(this.program) + ); + return false; + } + + // 创建变形渲染程序 + const deformVertexShader = this._compileShader( + this.deformVertexShaderSource, + this.gl.VERTEX_SHADER + ); + const deformFragmentShader = this._compileShader( + this.deformFragmentShaderSource, + this.gl.FRAGMENT_SHADER + ); + + if (!deformVertexShader || !deformFragmentShader) return false; + + // 创建变形程序 + this.deformProgram = this.gl.createProgram(); + this.gl.attachShader(this.deformProgram, deformVertexShader); + this.gl.attachShader(this.deformProgram, deformFragmentShader); + this.gl.linkProgram(this.deformProgram); + + if (!this.gl.getProgramParameter(this.deformProgram, this.gl.LINK_STATUS)) { + console.error( + "变形着色器程序链接失败:", + this.gl.getProgramInfoLog(this.deformProgram) + ); + return false; + } + + return true; + } + + /** + * 编译着色器 + * @param {String} source 着色器源码 + * @param {Number} type 着色器类型 + * @returns {WebGLShader} 编译后的着色器 + * @private + */ + _compileShader(source, type) { + const shader = this.gl.createShader(type); + this.gl.shaderSource(shader, source); + this.gl.compileShader(shader); + + if (!this.gl.getShaderParameter(shader, this.gl.COMPILE_STATUS)) { + console.error( + "着色器编译失败:", + this.gl.getShaderInfoLog(shader), + "shader type:", + type === this.gl.VERTEX_SHADER ? "VERTEX_SHADER" : "FRAGMENT_SHADER", + "source:", + source + ); + this.gl.deleteShader(shader); + return null; + } + + return shader; + } + + /** + * 创建WebGL纹理 + * @param {HTMLImageElement} image 图像元素 + * @returns {WebGLTexture} WebGL纹理 + * @private + */ + _createTexture(image) { + const texture = this.gl.createTexture(); + this.gl.bindTexture(this.gl.TEXTURE_2D, texture); + + // 设置参数,使我们可以渲染任何尺寸的图像 + this.gl.texParameteri( + this.gl.TEXTURE_2D, + this.gl.TEXTURE_WRAP_S, + this.gl.CLAMP_TO_EDGE + ); + this.gl.texParameteri( + this.gl.TEXTURE_2D, + this.gl.TEXTURE_WRAP_T, + this.gl.CLAMP_TO_EDGE + ); + this.gl.texParameteri( + this.gl.TEXTURE_2D, + this.gl.TEXTURE_MIN_FILTER, + this.gl.LINEAR + ); + this.gl.texParameteri( + this.gl.TEXTURE_2D, + this.gl.TEXTURE_MAG_FILTER, + this.gl.LINEAR + ); + + // 上传图像到纹理 + try { + this.gl.texImage2D( + this.gl.TEXTURE_2D, + 0, + this.gl.RGBA, + this.gl.RGBA, + this.gl.UNSIGNED_BYTE, + image + ); + } catch (e) { + console.error("纹理上传失败:", e); + return null; + } + + return texture; + } + + /** + * 创建变形网格 + * @private + */ + _createDeformMesh() { + const { meshResolution } = this.config; + + // 创建网格顶点 + const vertices = []; + const texCoords = []; + const indices = []; + const deformations = []; + + // 创建顶点和纹理坐标 + for (let y = 0; y <= meshResolution; y++) { + for (let x = 0; x <= meshResolution; x++) { + const xPos = x / meshResolution; + const yPos = y / meshResolution; + + // 顶点位置 + vertices.push(xPos, yPos); + + // 纹理坐标 + texCoords.push(xPos, yPos); + + // 初始无变形 + deformations.push(0, 0); + } + } + + // 创建索引(三角形) + for (let y = 0; y < meshResolution; y++) { + for (let x = 0; x < meshResolution; x++) { + const i0 = y * (meshResolution + 1) + x; + const i1 = i0 + 1; + const i2 = i0 + meshResolution + 1; + const i3 = i2 + 1; + + // 三角形1 + indices.push(i0, i2, i1); + + // 三角形2 + indices.push(i1, i2, i3); + } + } + + this.mesh = { + vertices: new Float32Array(vertices), + texCoords: new Float32Array(texCoords), + indices: new Uint16Array(indices), + deformations: new Float32Array(deformations), + resolution: meshResolution, + }; + + // 创建顶点缓冲区 + this.vertexBuffer = this.gl.createBuffer(); + this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.vertexBuffer); + this.gl.bufferData( + this.gl.ARRAY_BUFFER, + this.mesh.vertices, + this.gl.STATIC_DRAW + ); + + // 创建纹理坐标缓冲区 + this.texCoordBuffer = this.gl.createBuffer(); + this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.texCoordBuffer); + this.gl.bufferData( + this.gl.ARRAY_BUFFER, + this.mesh.texCoords, + this.gl.STATIC_DRAW + ); + + // 创建变形缓冲区 + this.deformBuffer = this.gl.createBuffer(); + this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.deformBuffer); + this.gl.bufferData( + this.gl.ARRAY_BUFFER, + this.mesh.deformations, + this.gl.DYNAMIC_DRAW + ); + + // 创建索引缓冲区 + this.indexBuffer = this.gl.createBuffer(); + this.gl.bindBuffer(this.gl.ELEMENT_ARRAY_BUFFER, this.indexBuffer); + this.gl.bufferData( + this.gl.ELEMENT_ARRAY_BUFFER, + this.mesh.indices, + this.gl.STATIC_DRAW + ); + } + + /** + * 应用液化变形 + * @param {Number} x 变形中心X坐标 (图像像素坐标) + * @param {Number} y 变形中心Y坐标 (图像像素坐标) + */ + applyDeformation(x, y) { + if (!this.initialized || !this.mesh) return; + + // 计算鼠标移动方向 + if (!this.isFirstApply) { + this.mouseMovementX = x - this.lastMouseX; + this.mouseMovementY = y - this.lastMouseY; + } else { + // 首次应用时不计算移动,避免初始变形 + this.mouseMovementX = 0; + this.mouseMovementY = 0; + this.isFirstApply = false; + } + + this.lastMouseX = x; + this.lastMouseY = y; + + // 将图像像素坐标转换为纹理坐标 (0-1范围) + // 使用原始图像数据的尺寸进行归一化,而不是WebGL canvas的尺寸 + const imageWidth = this.originalImageData + ? this.originalImageData.width + : this.canvas.width; + const imageHeight = this.originalImageData + ? this.originalImageData.height + : this.canvas.height; + + const tx = x / imageWidth; + const ty = y / imageHeight; + + console.log( + `WebGL变形: 像素坐标(${x}, ${y}) -> 纹理坐标(${tx.toFixed( + 3 + )}, ${ty.toFixed(3)}), 图像尺寸(${imageWidth}x${imageHeight})` + ); + + // 获取当前参数 + const { size, pressure, distortion, power } = this.params; + const mode = this.currentMode; + + // 计算影响半径 (纹理坐标空间) + const radius = (size / 100) * 0.2; // 调整半径计算,使效果更自然 + const strength = (pressure * power * this.config.maxStrength) / 800; // 进一步降低基础强度 + + // 保存当前变形点,用于重建功能 + this.deformHistory.push({ + x: tx, + y: ty, + radius, + strength, + mode, + distortion, + }); + + // 对网格顶点应用变形 + const { resolution } = this.mesh; + const deformations = this.mesh.deformations; + + for (let i = 0; i <= resolution; i++) { + for (let j = 0; j <= resolution; j++) { + const idx = (i * (resolution + 1) + j) * 2; + + // 顶点在纹理空间中的位置 + const vx = j / resolution; + const vy = i / resolution; + + // 计算到变形中心的距离 + const dx = vx - tx; + const dy = vy - ty; + const distance = Math.sqrt(dx * dx + dy * dy); + + // 只影响半径内的点 + if (distance < radius) { + // 计算影响因子 + const factor = Math.pow(1 - distance / radius, 2) * strength; + + // 根据不同模式应用变形 + switch (mode) { + case this.modes.PUSH: + // 推拉模式 - 真正的拖拽效果 + // 计算鼠标移动距离(转换为纹理坐标空间) + const movementX = this.mouseMovementX / imageWidth; + const movementY = this.mouseMovementY / imageHeight; + const movementLength = Math.sqrt( + movementX * movementX + movementY * movementY + ); + + // 只有在有足够移动距离时才应用效果 + if (movementLength > 0.002) { + // 提高阈值,确保有明显移动 + // 归一化移动方向 + const moveX = movementX / movementLength; + const moveY = movementY / movementLength; + + // 计算衰减(距离中心越近,效果越强) + const radiusRatio = distance / radius; + const falloff = Math.pow(1 - radiusRatio, 2.0); // 使用更强的衰减 + + // 基于实际移动距离计算强度 + const moveStrength = pressure * power * movementLength * 0.5; // 降低移动强度系数 + + // 计算最终拖拽强度 + const dragStrength = moveStrength * falloff * factor; + + // 向鼠标移动方向拖拽 + const dragX = moveX * dragStrength; + const dragY = moveY * dragStrength; + + // 应用变形,但限制最大变形量 + const maxDeform = 0.01; // 限制单次最大变形量(纹理坐标空间) + deformations[idx] += Math.max( + -maxDeform, + Math.min(maxDeform, dragX) + ); + deformations[idx + 1] += Math.max( + -maxDeform, + Math.min(maxDeform, dragY) + ); + } + break; + + case this.modes.CLOCKWISE: + // 顺时针旋转 + const angle = Math.atan2(dy, dx) + factor; + const len = distance; + deformations[idx] += Math.cos(angle) * len - dx; + deformations[idx + 1] += Math.sin(angle) * len - dy; + break; + + case this.modes.COUNTERCLOCKWISE: + // 逆时针旋转 + const angle2 = Math.atan2(dy, dx) - factor; + const len2 = distance; + deformations[idx] += Math.cos(angle2) * len2 - dx; + deformations[idx + 1] += Math.sin(angle2) * len2 - dy; + break; + + case this.modes.PINCH: + // 捏合效果 - 向中心收缩 + deformations[idx] -= dx * factor; + deformations[idx + 1] -= dy * factor; + break; + + case this.modes.EXPAND: + // 展开效果 - 参考捏合算法的反向操作 + const expandFactor = factor * 1.5; + deformations[idx] += dx * expandFactor; + deformations[idx + 1] += dy * expandFactor; + break; + + case this.modes.CRYSTAL: + // 水晶效果 - 参考旋转算法创建多重角度变形 + const crystalAngle = Math.atan2(dy, dx); + const crystalRadius = distance / radius; + + // 确保有基础效果 + const baseDistortion = Math.max(distortion, 0.3); + + // 创建多重波形 - 类似旋转但加入波形调制 + const wave1 = Math.sin(crystalAngle * 8) * 0.6; + const wave2 = Math.cos(crystalAngle * 12) * 0.4; + const waveAngle = crystalAngle + (wave1 + wave2) * baseDistortion; + + // 径向扭曲 - 类似旋转的距离调制 + const radialMod = 1 + Math.sin(crystalRadius * Math.PI * 2) * 0.3; + const modDistance = distance * radialMod; + + const crystalX = Math.cos(waveAngle) * modDistance; + const crystalY = Math.sin(waveAngle) * modDistance; + + deformations[idx] += (crystalX - (tx + dx)) * factor; + deformations[idx + 1] += (crystalY - (ty + dy)) * factor; + break; + + case this.modes.EDGE: + // 边缘效果 - 参考旋转算法创建垂直于径向的波纹 + const edgeAngle = Math.atan2(dy, dx); + const edgeRadius = distance / radius; + + // 确保有基础效果 + const baseEdgeDistortion = Math.max(distortion, 0.5); + + // 创建边缘波纹 - 垂直于径向方向的调制 + const edgeWave = + Math.sin(edgeRadius * Math.PI * 4) * Math.cos(edgeAngle * 6); + const perpAngle = edgeAngle + Math.PI / 2; // 垂直角度 + + const edgeFactor = edgeWave * factor * baseEdgeDistortion; + const edgeX = Math.cos(perpAngle) * edgeFactor; + const edgeY = Math.sin(perpAngle) * edgeFactor; + + deformations[idx] += edgeX; + deformations[idx + 1] += edgeY; + break; + + case this.modes.RECONSTRUCT: + // 重建 - 向原始位置恢复 + deformations[idx] *= 0.9; + deformations[idx + 1] *= 0.9; + break; + } + } + } + } + + // 更新变形缓冲区 + this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.deformBuffer); + this.gl.bufferData( + this.gl.ARRAY_BUFFER, + deformations, + this.gl.DYNAMIC_DRAW + ); + + // 重新渲染 + this._render(); + + // 更新当前图像数据 + this.currentImageData = this._getImageData(); + + return this.currentImageData; + } + + /** + * 渲染变形后的图像 + * @private + */ + _render() { + if (!this.initialized) return; + + // 清除画布 + this.gl.clearColor(0, 0, 0, 0); + this.gl.clear(this.gl.COLOR_BUFFER_BIT); + + // 使用变形程序 + this.gl.useProgram(this.deformProgram); + + // 设置纹理 + this.gl.activeTexture(this.gl.TEXTURE0); + this.gl.bindTexture(this.gl.TEXTURE_2D, this.texture); + const u_image = this.gl.getUniformLocation(this.deformProgram, "u_image"); + this.gl.uniform1i(u_image, 0); + + // 设置顶点位置属性 + const a_position = this.gl.getAttribLocation( + this.deformProgram, + "a_position" + ); + this.gl.enableVertexAttribArray(a_position); + this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.vertexBuffer); + this.gl.vertexAttribPointer(a_position, 2, this.gl.FLOAT, false, 0, 0); + + // 设置纹理坐标属性 + const a_texCoord = this.gl.getAttribLocation( + this.deformProgram, + "a_texCoord" + ); + this.gl.enableVertexAttribArray(a_texCoord); + this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.texCoordBuffer); + this.gl.vertexAttribPointer(a_texCoord, 2, this.gl.FLOAT, false, 0, 0); + + // 设置变形属性 + const a_deformation = this.gl.getAttribLocation( + this.deformProgram, + "a_deformation" + ); + this.gl.enableVertexAttribArray(a_deformation); + this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.deformBuffer); + this.gl.vertexAttribPointer(a_deformation, 2, this.gl.FLOAT, false, 0, 0); + + // 绘制三角形 + this.gl.bindBuffer(this.gl.ELEMENT_ARRAY_BUFFER, this.indexBuffer); + this.gl.drawElements( + this.gl.TRIANGLES, + this.mesh.indices.length, + this.gl.UNSIGNED_SHORT, + 0 + ); + } + + /** + * 获取当前图像数据 + * @returns {ImageData} 当前图像数据 + * @private + */ + _getImageData() { + const width = this.canvas.width; + const height = this.canvas.height; + + // 读取WebGL画布像素 + const pixels = new Uint8Array(width * height * 4); + this.gl.readPixels( + 0, + 0, + width, + height, + this.gl.RGBA, + this.gl.UNSIGNED_BYTE, + pixels + ); + + // 直接创建ImageData,不进行翻转 + // WebGL和Canvas2D的坐标系不同,但这里我们保持WebGL的原始输出 + const imageData = new ImageData( + new Uint8ClampedArray(pixels), + width, + height + ); + + return imageData; + } + + /** + * 重置所有变形 + * @returns {ImageData} 重置后的图像数据 + */ + reset() { + if (!this.initialized) return null; + + // 清除变形历史 + this.deformHistory = []; + + // 重置所有变形 + const deformations = new Float32Array(this.mesh.deformations.length); + this.mesh.deformations = deformations; + + // 更新变形缓冲区 + this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.deformBuffer); + this.gl.bufferData( + this.gl.ARRAY_BUFFER, + deformations, + this.gl.DYNAMIC_DRAW + ); + + // 重新渲染 + this._render(); + + // 更新当前图像数据 + this.currentImageData = this._getImageData(); + + return this.currentImageData; + } + + /** + * 设置液化模式 + * @param {String} mode 液化模式 + */ + setMode(mode) { + if (Object.values(this.modes).includes(mode)) { + this.currentMode = mode; + return true; + } + return false; + } + + /** + * 设置液化参数 + * @param {String} param 参数名 + * @param {Number} value 参数值 + */ + setParam(param, value) { + if (param in this.params) { + this.params[param] = value; + return true; + } + return false; + } + + /** + * 获取当前参数 + * @returns {Object} 当前参数 + */ + getParams() { + return { ...this.params }; + } + + /** + * 重置参数为默认值 + */ + resetParams() { + this.params = { + size: 50, + pressure: 0.5, + distortion: 0, + power: 0.5, + }; + } + + /** + * 获取原始图像数据 + * @returns {ImageData} 原始图像数据 + */ + getOriginalImageData() { + return this.originalImageData; + } + + /** + * 获取当前图像数据 + * @returns {ImageData} 当前图像数据 + */ + getCurrentImageData() { + return this.currentImageData; + } + + /** + * 释放资源 + */ + dispose() { + if (!this.gl) return; + + // 删除缓冲区 + if (this.vertexBuffer) this.gl.deleteBuffer(this.vertexBuffer); + if (this.texCoordBuffer) this.gl.deleteBuffer(this.texCoordBuffer); + if (this.deformBuffer) this.gl.deleteBuffer(this.deformBuffer); + if (this.indexBuffer) this.gl.deleteBuffer(this.indexBuffer); + + // 删除纹理 + if (this.texture) this.gl.deleteTexture(this.texture); + + // 删除着色器程序 + if (this.program) this.gl.deleteProgram(this.program); + if (this.deformProgram) this.gl.deleteProgram(this.deformProgram); + + // 重置属性 + this.canvas = null; + this.gl = null; + this.program = null; + this.deformProgram = null; + this.texture = null; + this.mesh = null; + this.initialized = false; + this.deformHistory = []; + } + + /** + * 检查是否支持WebGL + * @returns {Boolean} 是否支持WebGL + */ + static isSupported() { + try { + const canvas = document.createElement("canvas"); + return !!( + window.WebGLRenderingContext && + (canvas.getContext("webgl") || canvas.getContext("experimental-webgl")) + ); + } catch (e) { + return false; + } + } +} diff --git a/src/component/Canvas/CanvasEditor/managers/minimap/MinimapManager.js b/src/component/Canvas/CanvasEditor/managers/minimap/MinimapManager.js new file mode 100644 index 00000000..6377150f --- /dev/null +++ b/src/component/Canvas/CanvasEditor/managers/minimap/MinimapManager.js @@ -0,0 +1,850 @@ +//import { fabric } from "fabric-with-all"; + +/** + * 小地图管理器类 + * 实现画布的小地图功能,展示当前视窗位置和内容概览 + */ +export class MinimapManager { + /** + * 构造函数 + * @param {fabric.Canvas} mainCanvas 主画布实例 + * @param {Object} options 配置选项 + */ + constructor(mainCanvas, options = {}) { + this.mainCanvas = mainCanvas; + this.minimapCanvas = null; + this.minimapCtx = null; + this.container = null; + this.minimapSize = options.size || { width: 200, height: 120 }; + this.visible = options.visible !== undefined ? options.visible : true; + this.isDragging = false; + this.lastRenderTime = 0; + this.renderInterval = options.renderInterval || 100; // 增加渲染间隔到100ms,降低频率 + this.initialized = false; + this.eventHandlers = {}; + + // 内容边界,用于确定小地图显示范围 + this.contentBounds = { + minX: 0, + minY: 0, + maxX: 0, + maxY: 0, + }; + + // 缓存上一次视口大小,用于减少抖动 + this.lastViewportSize = { width: 0, height: 0 }; + // 添加缓存标志,避免频繁重新计算 + this.contentBoundsDirty = true; + + // 预先绑定方法,避免上下文丢失 + this.render = this.render.bind(this); + this.handleMainCanvasChange = this.handleMainCanvasChange.bind(this); + this.handleMinimapMouseDown = this.handleMinimapMouseDown.bind(this); + this.handleMinimapMouseMove = this.handleMinimapMouseMove.bind(this); + this.handleMinimapMouseUp = this.handleMinimapMouseUp.bind(this); + this.calculateViewportRect = this.calculateViewportRect.bind(this); + this.calculateContentBounds = this.calculateContentBounds.bind(this); + this.moveViewport = this.moveViewport.bind(this); + + // 创建canvas元素 + this._createCanvas(); + } + + /** + * 创建小地图的canvas元素 + * @private + */ + _createCanvas() { + // 创建canvas元素 + this.minimapCanvas = document.createElement("canvas"); + this.minimapCanvas.width = this.minimapSize.width; + this.minimapCanvas.height = this.minimapSize.height; + this.minimapCanvas.style.width = "100%"; + this.minimapCanvas.style.height = "100%"; + this.minimapCanvas.style.display = this.visible ? "block" : "none"; + + // 获取绘图上下文 + this.minimapCtx = this.minimapCanvas.getContext("2d"); + } + + /** + * 将小地图挂载到指定的DOM容器中 + * @param {HTMLElement} containerElement 容器DOM元素 + * @returns {MinimapManager} 返回实例自身,支持链式调用 + */ + mount(containerElement) { + if (!containerElement) { + console.error("小地图挂载失败:未提供有效的容器元素"); + return this; + } + + // 保存容器引用 + this.container = containerElement; + + // 清空容器,防止重复挂载 + while (containerElement.firstChild) { + containerElement.removeChild(containerElement.firstChild); + } + + // 将canvas添加到容器 + containerElement.appendChild(this.minimapCanvas); + + // 初始化小地图 + if (!this.initialized) { + // 计算初始内容边界 + this.calculateContentBounds(); + + // 添加事件监听器 + this.addEventListeners(); + + // 首次渲染 + this.render(); + + this.initialized = true; + } + + return this; + } + + /** + * 添加事件监听器 + */ + addEventListeners() { + if (!this.mainCanvas || !this.minimapCanvas) return; + + // 监听主画布变化事件 + this.mainCanvas.on("after:render", this.handleMainCanvasChange); + // 仅在缩放时重新计算内容边界,避免频繁计算 + this.mainCanvas.on("zoom:change", () => { + this.contentBoundsDirty = true; + this.handleMainCanvasChange(); + }); + // 仅当对象添加、删除或修改时重新计算内容边界 + this.mainCanvas.on("object:added", () => { + this.contentBoundsDirty = true; + this.handleMainCanvasChange(); + }); + this.mainCanvas.on("object:removed", () => { + this.contentBoundsDirty = true; + this.handleMainCanvasChange(); + }); + this.mainCanvas.on("object:modified", () => { + this.contentBoundsDirty = true; + this.handleMainCanvasChange(); + }); + + // 移动、缩放、旋转操作时使用更强的节流,不重新计算内容边界 + this.mainCanvas.on("object:moving", this.handleMainCanvasChange); + this.mainCanvas.on("object:scaling", this.handleMainCanvasChange); + this.mainCanvas.on("object:rotating", this.handleMainCanvasChange); + + // 小地图交互事件 - 鼠标 + this.eventHandlers.mousedown = this.handleMinimapMouseDown; + this.eventHandlers.mousemove = this.handleMinimapMouseMove; + this.eventHandlers.mouseup = this.handleMinimapMouseUp; + // 移除mouseout事件处理,允许拖动操作持续到鼠标释放 + + this.minimapCanvas.addEventListener( + "mousedown", + this.eventHandlers.mousedown + ); + document.addEventListener("mousemove", this.eventHandlers.mousemove); + document.addEventListener("mouseup", this.eventHandlers.mouseup); + // 移除mouseout事件监听 + + // 小地图交互事件 - 触摸 + this.eventHandlers.touchstart = (e) => { + e.preventDefault(); + const touch = e.touches[0]; + this.handleMinimapMouseDown({ + clientX: touch.clientX, + clientY: touch.clientY, + preventDefault: () => {}, + }); + }; + + this.eventHandlers.touchmove = (e) => { + e.preventDefault(); + if (this.isDragging) { + const touch = e.touches[0]; + this.handleMinimapMouseMove({ + clientX: touch.clientX, + clientY: touch.clientY, + preventDefault: () => {}, + }); + } + }; + + this.eventHandlers.touchend = this.handleMinimapMouseUp; + + this.minimapCanvas.addEventListener( + "touchstart", + this.eventHandlers.touchstart + ); + document.addEventListener("touchmove", this.eventHandlers.touchmove, { + passive: false, + }); + document.addEventListener("touchend", this.eventHandlers.touchend); + } + + /** + * 移除事件监听器 + */ + removeEventListeners() { + if (!this.mainCanvas || !this.minimapCanvas) return; + + // 移除画布事件监听 + this.mainCanvas.off("after:render", this.handleMainCanvasChange); + this.mainCanvas.off("zoom:change", this.handleMainCanvasChange); + this.mainCanvas.off("object:added", this.handleMainCanvasChange); + this.mainCanvas.off("object:removed", this.handleMainCanvasChange); + this.mainCanvas.off("object:modified", this.handleMainCanvasChange); + this.mainCanvas.off("object:moving", this.handleMainCanvasChange); + this.mainCanvas.off("object:scaling", this.handleMainCanvasChange); + this.mainCanvas.off("object:rotating", this.handleMainCanvasChange); + + // 移除鼠标事件监听 + this.minimapCanvas.removeEventListener( + "mousedown", + this.eventHandlers.mousedown + ); + document.removeEventListener("mousemove", this.eventHandlers.mousemove); + document.removeEventListener("mouseup", this.eventHandlers.mouseup); + + // 移除触摸事件监听 + this.minimapCanvas.removeEventListener( + "touchstart", + this.eventHandlers.touchstart + ); + document.removeEventListener("touchmove", this.eventHandlers.touchmove); + document.removeEventListener("touchend", this.eventHandlers.touchend); + } + + /** + * 处理主画布变化事件 + * 使用节流限制渲染频率 + */ + handleMainCanvasChange() { + const now = Date.now(); + if (now - this.lastRenderTime > this.renderInterval) { + this.lastRenderTime = now; + + // 只在内容边界标记为脏时才重新计算 + if (this.contentBoundsDirty) { + this.calculateContentBounds(); + this.contentBoundsDirty = false; + } + + this.render(); + } + } + + /** + * 计算画布内容的边界范围 + * 包括所有可见对象和画布本身 + */ + calculateContentBounds() { + if (!this.mainCanvas) return; + + const objects = this.mainCanvas.getObjects(); + + // 初始化为画布尺寸 + let minX = 0; + let minY = 0; + let maxX = this.mainCanvas.getWidth(); + let maxY = this.mainCanvas.getHeight(); + + // 如果有对象,则计算所有对象的边界 + if (objects.length > 0) { + // 重置为极值 + minX = Infinity; + minY = Infinity; + maxX = -Infinity; + maxY = -Infinity; + + // 考虑所有可见对象的边界 + objects.forEach((obj) => { + if (!obj.visible) return; + + const rect = obj.getBoundingRect(true, true); + minX = Math.min(minX, rect.left); + minY = Math.min(minY, rect.top); + maxX = Math.max(maxX, rect.left + rect.width); + maxY = Math.max(maxY, rect.top + rect.height); + }); + + // 确保边界至少包含画布尺寸 + minX = Math.min(minX, 0); + minY = Math.min(minY, 0); + maxX = Math.max(maxX, this.mainCanvas.getWidth()); + maxY = Math.max(maxY, this.mainCanvas.getHeight()); + } + + // 添加边距 + const padding = + Math.max(this.mainCanvas.getWidth(), this.mainCanvas.getHeight()) * 0.1; + this.contentBounds = { + minX: minX - padding, + minY: minY - padding, + maxX: maxX + padding, + maxY: maxY + padding, + }; + } + + /** + * 处理小地图鼠标按下事件 + */ + handleMinimapMouseDown(e) { + if (!this.visible || !this.minimapCanvas) return; + + e.preventDefault(); + + const rect = this.minimapCanvas.getBoundingClientRect(); + const x = e.clientX - rect.left; + const y = e.clientY - rect.top; + + // 检查点击是否在视口矩形内 + const vpRect = this.calculateViewportRect(); + + // 在视口矩形内点击开始拖拽,否则直接跳转到点击位置 + if ( + x >= vpRect.x && + x <= vpRect.x + vpRect.width && + y >= vpRect.y && + y <= vpRect.y + vpRect.height + ) { + this.isDragging = true; + this.dragStart = { x, y }; + this.dragStartViewport = { ...vpRect }; + + // 缓存当前视口大小,确保拖动过程中大小不变 + this.lastViewportSize = { + width: vpRect.width, + height: vpRect.height, + }; + } else { + // 直接移动视口中心到点击位置 + this.moveViewport(x, y, true); + } + } + + /** + * 处理小地图鼠标移动事件 + */ + handleMinimapMouseMove(e) { + if (!this.isDragging || !this.visible) return; + + e.preventDefault(); + + const rect = this.minimapCanvas.getBoundingClientRect(); + const x = e.clientX - rect.left; + const y = e.clientY - rect.top; + + const deltaX = x - this.dragStart.x; + const deltaY = y - this.dragStart.y; + + // 更新拖拽起始位置 + this.dragStart = { x, y }; + + // 移动画布视口 + this.moveViewport( + this.dragStartViewport.x + deltaX, + this.dragStartViewport.y + deltaY, + false + ); + + // 更新拖拽起始视口位置 + this.dragStartViewport = this.calculateViewportRect(); + + // 立即渲染小地图,提升拖动流畅度 + this.render(); + } + + /** + * 处理小地图鼠标抬起事件 + */ + handleMinimapMouseUp() { + this.isDragging = false; + } + + /** + * 移动主画布视口到指定位置 + */ + moveViewport(x, y, isCentered) { + if (!this.mainCanvas) return; + + // 获取主画布的当前视图信息 + const vpt = this.mainCanvas.viewportTransform; + const zoom = this.mainCanvas.getZoom(); + + // 计算内容边界在小地图上的比例 + const contentWidth = this.contentBounds.maxX - this.contentBounds.minX; + const contentHeight = this.contentBounds.maxY - this.contentBounds.minY; + + const scaleX = this.minimapSize.width / contentWidth; + const scaleY = this.minimapSize.height / contentHeight; + + // 计算视口在小地图上的宽高 + let viewportWidth, viewportHeight; + if (this.isDragging && this.lastViewportSize.width > 0) { + viewportWidth = this.lastViewportSize.width; + viewportHeight = this.lastViewportSize.height; + } else { + viewportWidth = Math.round((this.mainCanvas.getWidth() / zoom) * scaleX); + viewportHeight = Math.round( + (this.mainCanvas.getHeight() / zoom) * scaleY + ); + } + + // 添加边界限制,确保视口不会超出小地图 + x = Math.max(0, Math.min(x, this.minimapSize.width - viewportWidth)); + y = Math.max(0, Math.min(y, this.minimapSize.height - viewportHeight)); + + // 将小地图坐标转换为主画布坐标 + let targetX = x / scaleX + this.contentBounds.minX; + let targetY = y / scaleY + this.contentBounds.minY; + + if (isCentered) { + // 如果是直接点击,则将点击位置设为视口中心 + targetX -= this.mainCanvas.getWidth() / zoom / 2; + targetY -= this.mainCanvas.getHeight() / zoom / 2; + } + + // 设置主画布的位置 + this.mainCanvas.setViewportTransform([ + vpt[0], + vpt[1], + vpt[2], + vpt[3], + -targetX * zoom, + -targetY * zoom, + ]); + + // 触发主画布重新渲染 + this.mainCanvas.renderAll(); + } + + /** + * 计算当前视口在小地图中的位置和大小 + */ + calculateViewportRect() { + if (!this.mainCanvas) return { x: 0, y: 0, width: 0, height: 0 }; + + // 获取主画布的视图变换信息 + const vpt = this.mainCanvas.viewportTransform; + const zoom = this.mainCanvas.getZoom(); + + // 计算内容边界在小地图上的比例 + const contentWidth = this.contentBounds.maxX - this.contentBounds.minX; + const contentHeight = this.contentBounds.maxY - this.contentBounds.minY; + + const scaleX = this.minimapSize.width / contentWidth; + const scaleY = this.minimapSize.height / contentHeight; + + // 计算当前视口区域相对于内容边界的位置 + const viewLeft = -vpt[4] / zoom - this.contentBounds.minX; + const viewTop = -vpt[5] / zoom - this.contentBounds.minY; + + // 转换为小地图上的坐标,使用取整减少精度误差 + const x = Math.round(viewLeft * scaleX); + const y = Math.round(viewTop * scaleY); + + // 如果正在拖动,则使用缓存的大小避免抖动 + let width, height; + if (this.isDragging && this.lastViewportSize.width > 0) { + width = this.lastViewportSize.width; + height = this.lastViewportSize.height; + } else { + width = Math.round((this.mainCanvas.getWidth() / zoom) * scaleX); + height = Math.round((this.mainCanvas.getHeight() / zoom) * scaleY); + + // 更新缓存的视口大小 + if (!this.isDragging) { + this.lastViewportSize = { width, height }; + } + } + + return { x, y, width, height }; + } + + /** + * 渲染小地图 + * 使用高性能的离屏渲染 + */ + render() { + if (!this.visible || !this.minimapCanvas || !this.mainCanvas) return; + + try { + // 清空小地图 + this.minimapCtx.clearRect( + 0, + 0, + this.minimapSize.width, + this.minimapSize.height + ); + + // 绘制小地图背景 + this.minimapCtx.fillStyle = this.mainCanvas.backgroundColor || "#f0f0f0"; + this.minimapCtx.fillRect( + 0, + 0, + this.minimapSize.width, + this.minimapSize.height + ); + + // 计算内容边界尺寸 + const contentWidth = this.contentBounds.maxX - this.contentBounds.minX; + const contentHeight = this.contentBounds.maxY - this.contentBounds.minY; + + // 检查是否有内容需要渲染 + const objects = this.mainCanvas.getObjects(); + if (objects.length === 0) { + // 如果没有对象,只需绘制视口框 + this.drawViewportBox(); + return; + } + + // 优化离屏渲染尺寸计算 + const maxSize = 1000; // 限制离屏canvas最大尺寸,提高性能 + let offscreenWidth = contentWidth; + let offscreenHeight = contentHeight; + let scale = 1; + + if (contentWidth > maxSize || contentHeight > maxSize) { + scale = Math.min(maxSize / contentWidth, maxSize / contentHeight); + offscreenWidth *= scale; + offscreenHeight *= scale; + } + + const offscreenCanvas = document.createElement("canvas"); + offscreenCanvas.width = offscreenWidth; + offscreenCanvas.height = offscreenHeight; + const offCtx = offscreenCanvas.getContext("2d"); + + // 创建临时fabric.Canvas用于渲染全内容 + const tempFabricCanvas = new fabric.StaticCanvas(); + tempFabricCanvas.setWidth(offscreenWidth); + tempFabricCanvas.setHeight(offscreenHeight); + tempFabricCanvas.backgroundColor = this.mainCanvas.backgroundColor; + + // 复制主画布对象到临时画布 + objects.forEach((obj) => { + if (!obj.visible) return; + + try { + // 使用浅克隆,避免深度克隆带来的性能开销 + const clonedObj = fabric.util.object.clone(obj); + + // 调整对象位置和大小,使其相对于内容边界并适应缩放 + clonedObj.set({ + left: (obj.left - this.contentBounds.minX) * scale, + top: (obj.top - this.contentBounds.minY) * scale, + scaleX: obj.scaleX * scale, + scaleY: obj.scaleY * scale, + // 禁用对象的交互属性,提高性能 + selectable: false, + evented: false, + hasControls: false, + hasBorders: false, + }); + + tempFabricCanvas.add(clonedObj); + } catch (err) { + console.warn("无法克隆对象到小地图", err); + } + }); + + // 渲染临时画布 + tempFabricCanvas.renderAll(); + + // 将临时画布内容绘制到离屏canvas + offCtx.drawImage(tempFabricCanvas.getElement(), 0, 0); + + // 将离屏canvas缩放绘制到小地图 + this.minimapCtx.drawImage( + offscreenCanvas, + 0, + 0, + offscreenWidth, + offscreenHeight, + 0, + 0, + this.minimapSize.width, + this.minimapSize.height + ); + + // 释放临时画布资源 + // tempFabricCanvas.dispose(); + + // 绘制视口框 + this.drawViewportBox(); + } catch (error) { + console.error("小地图渲染出错:", error); + } + } + + /** + * 绘制视口框,从render方法中分离出来提高代码清晰度 + */ + drawViewportBox() { + // 计算当前视口范围 + const vpRect = this.calculateViewportRect(); + + // 视口矩形边框 + this.minimapCtx.strokeStyle = "#ff3333"; + this.minimapCtx.lineWidth = 2; + this.minimapCtx.strokeRect(vpRect.x, vpRect.y, vpRect.width, vpRect.height); + + // 视口矩形半透明填充 + this.minimapCtx.fillStyle = "rgba(255, 0, 0, 0.1)"; + this.minimapCtx.fillRect(vpRect.x, vpRect.y, vpRect.width, vpRect.height); + } + + /** + * 设置小地图可见性 + */ + setVisibility(visible) { + this.visible = visible; + + // 更新canvas显示状态 + if (this.minimapCanvas) { + this.minimapCanvas.style.display = visible ? "block" : "none"; + } + + if (visible && this.initialized) { + this.contentBoundsDirty = true; // 标记需要重新计算内容边界 + this.calculateContentBounds(); + this.render(); + } + } + + /** + * 刷新小地图 + * 重新读取大画布数据并渲染 + */ + refresh() { + this.contentBoundsDirty = true; + this.calculateContentBounds(); + this.render(); + } + + /** + * 调整小地图大小 + * @param {Object} size 小地图尺寸,{width, height} + */ + resize(size) { + if (!size || !size.width || !size.height) return; + + this.minimapSize = { + width: size.width, + height: size.height, + }; + + if (this.minimapCanvas) { + this.minimapCanvas.width = size.width; + this.minimapCanvas.height = size.height; + this.refresh(); + } + } + + /** + * 清理资源,释放内存 + */ + dispose() { + this.removeEventListeners(); + + // 从DOM中移除canvas + if ( + this.container && + this.minimapCanvas && + this.minimapCanvas.parentNode === this.container + ) { + this.container.removeChild(this.minimapCanvas); + } + + this.mainCanvas = null; + this.minimapCanvas = null; + this.minimapCtx = null; + this.container = null; + this.initialized = false; + } + + /** + * 更新小地图 + * 使用更高效的渲染策略,减少不必要的重绘 + */ + update() { + if (!this.enabled || !this.minimapCanvas) return; + + // 使用节流来控制更新频率 + if (this._updateTimeout) { + clearTimeout(this._updateTimeout); + } + + this._updateTimeout = setTimeout(() => { + this._renderMinimap(); + }, 100); // 100ms 的节流,避免频繁渲染 + } + + /** + * 渲染小地图 + * 优化渲染性能,只在必要时重绘 + */ + _renderMinimap() { + if (!this.minimapCanvas || !this.canvas) return; + + const ctx = this.minimapCanvas.getContext("2d"); + const ratio = this.minimapCanvas.width / this.canvas.width; + + // 清除小地图 + ctx.clearRect(0, 0, this.minimapCanvas.width, this.minimapCanvas.height); + + // 使用缓存策略 + if (!this._minimapCache || this._shouldUpdateCache()) { + // 创建离屏画布作为缓存 + if (!this._offscreenCanvas) { + this._offscreenCanvas = document.createElement("canvas"); + this._offscreenCanvas.width = this.minimapCanvas.width; + this._offscreenCanvas.height = this.minimapCanvas.height; + } + + const offCtx = this._offscreenCanvas.getContext("2d"); + offCtx.clearRect( + 0, + 0, + this._offscreenCanvas.width, + this._offscreenCanvas.height + ); + + // 绘制图层内容到离屏画布 + this._renderLayersToMinimap(offCtx, ratio); + + // 保存渲染时间戳 + this._lastCacheUpdate = Date.now(); + this._minimapCache = true; + } + + // 将缓存的内容渲染到实际小地图画布 + if (this._offscreenCanvas) { + ctx.drawImage(this._offscreenCanvas, 0, 0); + } + + // 绘制可视区域指示器 + this._renderViewportIndicator(ctx, ratio); + } + + /** + * 检查是否应该更新小地图缓存 + */ + _shouldUpdateCache() { + // 如果没有缓存或缓存时间超过500ms,则更新 + return !this._lastCacheUpdate || Date.now() - this._lastCacheUpdate > 500; + } + + /** + * 渲染图层内容到小地图 + */ + _renderLayersToMinimap(ctx, ratio) { + // 获取画布上所有可见的图层 + const visibleLayers = []; + + // 安全地访问图层数据,避免 "forEach is not a function" 错误 + if (this.canvas && this.canvas.layers) { + // 检查 layers 是否是响应式对象 (有 value 属性) + const layersArray = + typeof this.canvas.layers.value !== "undefined" + ? this.canvas.layers.value + : Array.isArray(this.canvas.layers) + ? this.canvas.layers + : []; + + // 过滤出可见图层 + layersArray.forEach((layer) => { + if (layer.visible) { + visibleLayers.push(layer); + } + }); + } + + // 按照图层顺序渲染到小地图 + for (const layer of visibleLayers) { + let objectsToRender = []; + + // 根据图层类型获取要渲染的对象 + if (layer.type === "background" && layer.fabricObject) { + objectsToRender = [layer.fabricObject]; + } else if (layer.fabricObjects && Array.isArray(layer.fabricObjects)) { + objectsToRender = layer.fabricObjects; + } + + for (const fabricObj of objectsToRender) { + if (!fabricObj.visible) continue; + + // 根据对象类型渲染到小地图 + if (fabricObj.type === "image" && fabricObj._element) { + ctx.globalAlpha = fabricObj.opacity || 1; + const left = fabricObj.left * ratio; + const top = fabricObj.top * ratio; + const width = fabricObj.width * fabricObj.scaleX * ratio; + const height = fabricObj.height * fabricObj.scaleY * ratio; + + ctx.drawImage(fabricObj._element, left, top, width, height); + } else if ( + fabricObj.type === "path" || + fabricObj.type === "rect" || + fabricObj.type === "circle" + ) { + // 简单地用颜色块表示其他类型的对象 + ctx.fillStyle = fabricObj.fill || "#888"; + ctx.globalAlpha = fabricObj.opacity || 0.5; + + const left = fabricObj.left * ratio; + const top = fabricObj.top * ratio; + const width = + (fabricObj.width || 20) * (fabricObj.scaleX || 1) * ratio; + const height = + (fabricObj.height || 20) * (fabricObj.scaleY || 1) * ratio; + + ctx.fillRect(left, top, width, height); + } + } + } + + ctx.globalAlpha = 1; + } + + /** + * 渲染视口指示器 + */ + _renderViewportIndicator(ctx, ratio) { + if (!this.canvas) return; + + const vpt = this.canvas.viewportTransform; + if (!vpt) return; + + // 计算可视区域在小地图上的位置和大小 + const zoom = this.canvas.getZoom(); + const viewportWidth = this.canvas.width / zoom; + const viewportHeight = this.canvas.height / zoom; + + const x = (-vpt[4] / zoom) * ratio; + const y = (-vpt[5] / zoom) * ratio; + const width = viewportWidth * ratio; + const height = viewportHeight * ratio; + + // 绘制视口指示器 + ctx.strokeStyle = "#ff0000"; + ctx.lineWidth = 2; + ctx.strokeRect(x, y, width, height); + } + + /** + * 强制完全更新小地图 + */ + forceUpdate() { + this._minimapCache = false; + this.update(); + } +} + +export default MinimapManager; diff --git a/src/component/Canvas/CanvasEditor/managers/selection/SelectionManager.js b/src/component/Canvas/CanvasEditor/managers/selection/SelectionManager.js new file mode 100644 index 00000000..7fe2defb --- /dev/null +++ b/src/component/Canvas/CanvasEditor/managers/selection/SelectionManager.js @@ -0,0 +1,951 @@ +//import { fabric } from "fabric-with-all"; +import { generateId } from "../../utils/helper"; +import { OperationType } from "../../utils/layerHelper"; +import { + ClearSelectionCommand, + CreateSelectionCommand, +} from "../../commands/SelectionCommands"; + +/** + * 选区管理器 + * 负责管理画布上的选区操作 + */ +export class SelectionManager { + /** + * 创建选区管理器 + * @param {Object} options 配置选项 + * @param {Object} options.canvas fabric.js画布实例 + * @param {Object} options.commandManager 命令管理器实例 + * @param {Object} options.layerManager 图层管理实例 + */ + constructor(options = {}) { + this.canvas = options.canvas; + this.commandManager = options.commandManager; + this.layerManager = options.layerManager; + + // 选区状态 + this.isActive = false; + this.selectionType = OperationType.LASSO_RECTANGLE; // 使用常量而不是字符串 + this.selectionObject = null; // 当前选区对象 + this.selectionId = "selection_" + Date.now(); + this.featherAmount = 0; // 羽化值 + + // 选区样式配置 + this.selectionStyle = { + stroke: "#0096ff", + strokeWidth: 1, + strokeDashArray: [5, 5], + fill: "rgba(0, 150, 255, 0.1)", + selectable: false, + evented: false, + excludeFromExport: true, + hoverCursor: "default", + moveCursor: "default", + }; + + // 绘制状态 + this.drawingObject = null; + this.startPoint = null; + this.selectionPath = null; // 存储选区路径数据 + + // 自由选区相关状态 + this.drawingPoints = null; + this.currentPathString = null; + + // 不再直接绑定事件处理函数 + this._mouseDownHandler = null; + this._mouseMoveHandler = null; + this._mouseUpHandler = null; + this._keyDownHandler = null; + + // 选区相关的工具类型 + this.selectionTools = [ + OperationType.LASSO, + OperationType.LASSO_RECTANGLE, + OperationType.LASSO_ELLIPSE, + ]; + + // 当前工具 + this.currentTool = OperationType.SELECT; + + // 选区状态变化回调 + this.onSelectionChanged = null; + + // 不再自动初始化事件,改为手动控制 + // this.initEvents(); + } + + /** + * 设置当前工具 + * @param {String} toolId 工具ID + */ + setCurrentTool(toolId) { + this.currentTool = toolId; + + // 检查是否为选区工具 + const wasActive = this.isActive; + this.isActive = this.selectionTools.includes(toolId); + + // 如果从非选区工具切换到选区工具,初始化事件 + if (!wasActive && this.isActive) { + this.initEvents(); + } + // 如果从选区工具切换到非选区工具,清理事件和选区 + else if (wasActive && !this.isActive) { + this.cleanupEvents(); + this.clearSelection(); + } + + // 根据工具类型设置选区类型 + if (this.isActive) { + this.selectionType = toolId; + } + } + + /** + * 初始化选区相关事件 + */ + initEvents() { + if (!this.canvas || this._mouseDownHandler) return; // 避免重复初始化 + + // 保存实例引用,用于事件处理函数中 + const self = this; + + // 鼠标按下事件处理 + this._mouseDownHandler = (options) => { + // 如果选区功能未激活,不处理事件 + if (!this.isActive) return; + + // 如果点击的是已有对象且不是选区对象,则不处理 + if ( + options.target && + options.target.id !== this.selectionId && + options.target.selectable !== false && + options.target.type !== "selection" + ) { + return; + } + + // 阻止事件冒泡,避免与 CanvasEventManager 冲突 + options.e.stopPropagation(); + + // 根据选区类型执行不同的起始操作 + switch (this.selectionType) { + case OperationType.LASSO: + this.startFreeSelection(options); + break; + case OperationType.LASSO_ELLIPSE: + this.startEllipseSelection(options); + break; + case OperationType.LASSO_RECTANGLE: + this.startRectangleSelection(options); + break; + } + }; + + // 鼠标移动事件处理 + this._mouseMoveHandler = (options) => { + // 如果选区功能未激活或没有正在绘制的对象,不处理事件 + if (!this.isActive || !this.drawingObject) return; + + // 阻止事件冒泡 + options.e.stopPropagation(); + + // 根据选区类型执行不同的绘制操作 + switch (this.selectionType) { + case OperationType.LASSO_RECTANGLE: + this.drawRectangleSelection(options); + break; + case OperationType.LASSO_ELLIPSE: + this.drawEllipseSelection(options); + break; + case OperationType.LASSO: + this.drawFreeSelection(options); + break; + } + }; + + // 鼠标抬起事件处理 + this._mouseUpHandler = (options) => { + // 如果选区功能未激活或没有正在绘制的对象,不处理事件 + if (!this.isActive || !this.drawingObject) return; + + // 阻止事件冒泡 + if (options && options.e) { + options.e.stopPropagation(); + } + + // 根据选区类型执行不同的完成操作 + switch (this.selectionType) { + case OperationType.LASSO_RECTANGLE: + this.endRectangleSelection(); + break; + case OperationType.LASSO_ELLIPSE: + this.endEllipseSelection(); + break; + case OperationType.LASSO: + this.endFreeSelection(); + break; + } + + // 如果有命令管理器,使用命令模式记录选区创建 + if (this.commandManager && this.selectionObject) { + this.commandManager.execute( + new CreateSelectionCommand({ + canvas: this.canvas, + selectionManager: this, + selectionObject: this.selectionObject, + selectionType: this.selectionType, + }) + ); + } + }; + + // 键盘事件处理 + this._keyDownHandler = (event) => { + // 只在选区功能激活时处理键盘事件 + if (!this.isActive) return; + + if (event.key === "Escape") { + // ESC键取消当前选区操作 + if (this.drawingObject) { + this.canvas.remove(this.drawingObject); + this.drawingObject = null; + this.startPoint = null; + } + // 清除已有选区 + else if (this.selectionObject) { + if (this.commandManager) { + this.commandManager.execute( + new ClearSelectionCommand({ + selectionManager: this, + }) + ); + } else { + this.clearSelection(); + } + } + } + }; + + // 添加事件监听 + this.canvas.on("mouse:down", this._mouseDownHandler); + this.canvas.on("mouse:move", this._mouseMoveHandler); + this.canvas.on("mouse:up", this._mouseUpHandler); + + // 添加键盘事件监听 + document.addEventListener("keydown", this._keyDownHandler); + } + + /** + * 清理事件监听 + */ + cleanupEvents() { + if (!this.canvas) return; + + // 移除事件监听 + if (this._mouseDownHandler) { + this.canvas.off("mouse:down", this._mouseDownHandler); + this._mouseDownHandler = null; + } + if (this._mouseMoveHandler) { + this.canvas.off("mouse:move", this._mouseMoveHandler); + this._mouseMoveHandler = null; + } + if (this._mouseUpHandler) { + this.canvas.off("mouse:up", this._mouseUpHandler); + this._mouseUpHandler = null; + } + if (this._keyDownHandler) { + document.removeEventListener("keydown", this._keyDownHandler); + this._keyDownHandler = null; + } + } + + /** + * 获取选区对象 + * @returns {Object} 选区对象 + */ + getSelectionObject() { + return this.selectionObject; + } + + /** + * 获取选区路径 + * @returns {Array|String} 选区路径数据 + */ + getSelectionPath() { + return this.selectionPath; + } + + /** + * 获取羽化值 + * @returns {Number} 羽化值 + */ + getFeatherAmount() { + return this.featherAmount; + } + + /** + * 设置羽化值 + * @param {Number} amount 羽化值 + */ + setFeatherAmount(amount) { + this.featherAmount = amount; + return this.updateSelectionAppearance(); + } + + /** + * 设置选区对象 + * @param {Object} object 选区对象 + */ + setSelectionObject(object) { + // 如果已存在选区,先移除 + if (this.selectionObject) { + this.removeSelectionFromCanvas(); + } + + // 更新选区对象 + this.selectionObject = object; + this.selectionPath = object.path; + this.selectionId = object.id || generateId(); + + // 更新外观 + this.updateSelectionAppearance(); + + // 添加到画布(确保在顶层) + if (this.canvas && this.selectionObject) { + this.canvas.add(this.selectionObject); + this.canvas.bringToFront(this.selectionObject); + this.canvas.renderAll(); + } + + // 触发选区变化回调 + if ( + this.onSelectionChanged && + typeof this.onSelectionChanged === "function" + ) { + this.onSelectionChanged(); + } + + return true; + } + + /** + * 从路径数据设置选区 + * @param {Array|String} path 选区路径数据 + */ + setSelectionFromPath(path) { + if (!path) return false; + + // 创建选区对象 + const selectionObj = new fabric.Path(path, { + ...this.selectionStyle, + id: `selection_${Date.now()}`, + name: "selection", + }); + + // 设置选区 + return this.setSelectionObject(selectionObj); + } + + /** + * 更新选区外观 + */ + updateSelectionAppearance() { + if (!this.selectionObject) return false; + + // 应用基本样式 + Object.assign(this.selectionObject, this.selectionStyle); + + // 应用羽化效果 + if (this.featherAmount > 0) { + this.selectionObject.shadow = new fabric.Shadow({ + color: "rgba(0, 150, 255, 0.5)", + blur: this.featherAmount, + offsetX: 0, + offsetY: 0, + }); + } else { + this.selectionObject.shadow = null; + } + + // 更新画布 + this.canvas.renderAll(); + return true; + } + + /** + * 移除选区 + */ + removeSelectionFromCanvas() { + if (this.canvas && this.selectionObject) { + this.canvas.remove(this.selectionObject); + this.canvas.renderAll(); + } + } + + /** + * 清除选区 + */ + clearSelection() { + // 移除选区对象 + this.removeSelectionFromCanvas(); + + // 重置选区状态 + this.selectionObject = null; + this.selectionPath = null; + this.selectionId = null; + this.featherAmount = 0; + + // 触发选区变化回调 + if ( + this.onSelectionChanged && + typeof this.onSelectionChanged === "function" + ) { + this.onSelectionChanged(); + } + + return true; + } + + /** + * 反转选区 + */ + async invertSelection() { + if (!this.canvas || !this.selectionObject) return false; + + // 获取画布范围 + const canvasRect = new fabric.Rect({ + left: 0, + top: 0, + width: this.canvas.width, + height: this.canvas.height, + selectable: false, + }); + + // 创建反选路径 + let invertedPath; + try { + invertedPath = canvasRect.subtractPathFromRect(this.selectionObject.path); + } catch (error) { + console.error("无法反转选区:", error); + return false; + } + + // 设置新的选区 + const newSelection = new fabric.Path(invertedPath.path, { + ...this.selectionStyle, + id: `selection_${Date.now()}`, + name: "selection", + }); + + return this.setSelectionObject(newSelection); + } + + /** + * 添加到选区 + * @param {Object} newSelection 要添加的选区对象 + */ + async addToSelection(newSelection) { + if (!this.canvas) return false; + + // 如果当前没有选区,直接使用新选区 + if (!this.selectionObject) { + return this.setSelectionObject(newSelection); + } + + // 合并选区 + let combinedPath; + try { + combinedPath = this.selectionObject.union(newSelection); + } catch (error) { + console.error("无法添加到选区:", error); + return false; + } + + // 设置新的选区 + const combinedSelection = new fabric.Path(combinedPath.path, { + ...this.selectionStyle, + id: `selection_${Date.now()}`, + name: "selection", + }); + + return this.setSelectionObject(combinedSelection); + } + + /** + * 从选区中移除 + * @param {Object} removeSelection 要移除的选区对象 + */ + async removeFromSelection(removeSelection) { + if (!this.canvas || !this.selectionObject) return false; + + // 从当前选区中减去新选区 + let resultPath; + try { + resultPath = this.selectionObject.subtract(removeSelection); + } catch (error) { + console.error("无法从选区中移除:", error); + return false; + } + + // 设置新的选区 + const newSelection = new fabric.Path(resultPath.path, { + ...this.selectionStyle, + id: `selection_${Date.now()}`, + name: "selection", + }); + + return this.setSelectionObject(newSelection); + } + + /** + * 应用羽化效果 + * @param {Number} amount 羽化值 + */ + async featherSelection(amount) { + if (!this.selectionObject) return false; + + // 更新羽化值 + this.featherAmount = amount; + + // 更新选区外观 + return this.updateSelectionAppearance(); + } + + /** + * 检查对象是否在选区内 + * @param {Object} object 要检查的对象 + * @returns {Boolean} 是否在选区内 + */ + isObjectInSelection(object) { + if (!this.selectionObject || !object) return false; + + // 获取对象的边界框 + const bounds = object.getBoundingRect(); + const { left, top, width, height } = bounds; + + // 检查对象的中心点和四个角是否在选区内 + const centerX = left + width / 2; + const centerY = top + height / 2; + + // 检查中心点 + if (this.isPointInSelection(centerX, centerY)) return true; + + // 检查四个角 + if (this.isPointInSelection(left, top)) return true; + if (this.isPointInSelection(left + width, top)) return true; + if (this.isPointInSelection(left, top + height)) return true; + if (this.isPointInSelection(left + width, top + height)) return true; + + return false; + } + + /** + * 检查点是否在选区内 + * @param {Number} x X坐标 + * @param {Number} y Y坐标 + * @returns {Boolean} 是否在选区内 + */ + isPointInSelection(x, y) { + if (!this.selectionObject) return false; + + // 使用fabric.js的containsPoint方法判断点是否在选区内 + return this.selectionObject.containsPoint({ x, y }); + } + + /** + * 开始自由选区 + * @param {Object} options 事件对象 + */ + startFreeSelection(options) { + if (!this.canvas || !this.isActive) return; + + // 获取鼠标位置 + const pointer = this.canvas.getPointer(options.e); + this.startPoint = pointer; + + // 创建用于绘制轨迹的点数组 + this.drawingPoints = [pointer]; + + // 初始化SVG路径字符串 + this.currentPathString = `M ${pointer.x} ${pointer.y}`; + + // 创建临时路径对象用于实时显示 + this.drawingObject = new fabric.Path(this.currentPathString, { + stroke: this.selectionStyle.stroke, + strokeWidth: this.selectionStyle.strokeWidth, + strokeDashArray: this.selectionStyle.strokeDashArray, + fill: "transparent", + selectable: false, + evented: false, + strokeLineCap: "round", + strokeLineJoin: "round", + }); + + // 添加到画布 + this.canvas.add(this.drawingObject); + this.canvas.renderAll(); + } + + /** + * 绘制自由选区 + * @param {Object} options 事件对象 + */ + drawFreeSelection(options) { + if (!this.drawingObject || !this.drawingPoints || !this.isActive) return; + + // 获取鼠标位置 + const pointer = this.canvas.getPointer(options.e); + + // 添加新的点,但避免添加过于密集的点 + const lastPoint = this.drawingPoints[this.drawingPoints.length - 1]; + const distance = Math.sqrt( + Math.pow(pointer.x - lastPoint.x, 2) + + Math.pow(pointer.y - lastPoint.y, 2) + ); + + // 只有当距离大于2像素时才添加新点,避免路径过于复杂 + if (distance > 2) { + this.drawingPoints.push(pointer); + + // 更新路径字符串 + this.currentPathString += ` L ${pointer.x} ${pointer.y}`; + + // 移除旧的绘制对象 + this.canvas.remove(this.drawingObject); + + // 创建新的路径对象 + this.drawingObject = new fabric.Path(this.currentPathString, { + stroke: this.selectionStyle.stroke, + strokeWidth: this.selectionStyle.strokeWidth, + strokeDashArray: this.selectionStyle.strokeDashArray, + fill: "transparent", + selectable: false, + evented: false, + strokeLineCap: "round", + strokeLineJoin: "round", + }); + + // 重新添加到画布 + this.canvas.add(this.drawingObject); + this.canvas.renderAll(); + } + } + + /** + * 结束自由选区 + */ + endFreeSelection() { + if (!this.drawingObject || !this.drawingPoints || !this.isActive) return; + + // 检查是否有足够的点来形成选区 + if (this.drawingPoints.length < 3) { + // 点太少,清除绘制对象 + this.canvas.remove(this.drawingObject); + this.drawingObject = null; + this.drawingPoints = null; + this.startPoint = null; + this.currentPathString = null; + return; + } + + // 自动闭合路径 - 连接最后一点到第一点 + const firstPoint = this.drawingPoints[0]; + const lastPoint = this.drawingPoints[this.drawingPoints.length - 1]; + const closingDistance = Math.sqrt( + Math.pow(firstPoint.x - lastPoint.x, 2) + + Math.pow(firstPoint.y - lastPoint.y, 2) + ); + + // 如果首尾距离较大,自动添加闭合线段 + let finalPathString = this.currentPathString; + if (closingDistance > 10) { + finalPathString += ` L ${firstPoint.x} ${firstPoint.y}`; + } + finalPathString += " Z"; // 闭合路径 + + // 创建最终选区对象 + const selectionObj = new fabric.Path(finalPathString, { + ...this.selectionStyle, + id: `selection_${Date.now()}`, + name: "selection", + fill: this.selectionStyle.fill, // 恢复填充 + }); + + // 移除绘制中的临时对象 + this.canvas.remove(this.drawingObject); + + // 重置绘制状态 + this.drawingObject = null; + this.drawingPoints = null; + this.startPoint = null; + this.currentPathString = null; + + // 设置选区 + this.setSelectionObject(selectionObj); + } + + /** + * 开始矩形选区 + * @param {Object} options 事件对象 + */ + startRectangleSelection(options) { + if (!this.canvas || !this.isActive) return; + + // 获取鼠标位置 + const pointer = this.canvas.getPointer(options.e); + this.startPoint = pointer; + + // 创建矩形对象 + this.drawingObject = new fabric.Rect({ + left: pointer.x, + top: pointer.y, + width: 0, + height: 0, + ...this.selectionStyle, + fill: "transparent", // 在绘制过程中不显示填充 + }); + + // 添加到画布 + this.canvas.add(this.drawingObject); + this.canvas.renderAll(); + } + + /** + * 绘制矩形选区 + * @param {Object} options 事件对象 + */ + drawRectangleSelection(options) { + if (!this.drawingObject || !this.startPoint || !this.isActive) return; + + // 获取鼠标位置 + const pointer = this.canvas.getPointer(options.e); + + // 计算宽度和高度 + const width = Math.abs(pointer.x - this.startPoint.x); + const height = Math.abs(pointer.y - this.startPoint.y); + + // 确定左上角坐标 + const left = Math.min(this.startPoint.x, pointer.x); + const top = Math.min(this.startPoint.y, pointer.y); + + // 更新矩形 + this.drawingObject.set({ + left: left, + top: top, + width: width, + height: height, + }); + + this.canvas.renderAll(); + } + + /** + * 结束矩形选区 + */ + endRectangleSelection() { + if (!this.drawingObject || !this.startPoint || !this.isActive) return; + + // 将矩形转换为路径 + const left = this.drawingObject.left; + const top = this.drawingObject.top; + const width = this.drawingObject.width; + const height = this.drawingObject.height; + + // 如果矩形太小,忽略 + if (width < 5 || height < 5) { + this.canvas.remove(this.drawingObject); + this.drawingObject = null; + this.startPoint = null; + return; + } + + // 创建矩形路径字符串 + const pathString = `M ${left} ${top} L ${left + width} ${top} L ${ + left + width + } ${top + height} L ${left} ${top + height} Z`; + + // 创建最终选区对象 + const selectionObj = new fabric.Path(pathString, { + ...this.selectionStyle, + id: `selection_${Date.now()}`, + name: "selection", + fill: this.selectionStyle.fill, // 恢复填充 + }); + + // 移除绘制中的临时对象 + this.canvas.remove(this.drawingObject); + + // 重置绘制状态 + this.drawingObject = null; + this.startPoint = null; + + // 设置选区 + this.setSelectionObject(selectionObj); + } + + /** + * 开始椭圆选区 + * @param {Object} options 事件对象 + */ + startEllipseSelection(options) { + if (!this.canvas || !this.isActive) return; + + // 获取鼠标位置 + const pointer = this.canvas.getPointer(options.e); + this.startPoint = pointer; + + // 创建椭圆对象 + this.drawingObject = new fabric.Ellipse({ + left: pointer.x, + top: pointer.y, + rx: 0, + ry: 0, + ...this.selectionStyle, + fill: "transparent", // 在绘制过程中不显示填充 + // originX: "left", + // originY: "top", + originX: "center", + originY: "center", + }); + + // 添加到画布 + this.canvas.add(this.drawingObject); + this.canvas.renderAll(); + } + + /** + * 绘制椭圆选区 + * @param {Object} options 事件对象 + */ + drawEllipseSelection(options) { + if (!this.drawingObject || !this.startPoint || !this.isActive) return; + + // 获取鼠标位置 + const pointer = this.canvas.getPointer(options.e); + + // 计算半径 + const rx = Math.abs(pointer.x - this.startPoint.x) / 2; + const ry = Math.abs(pointer.y - this.startPoint.y) / 2; + + // 确定中心坐标 + const left = Math.min(this.startPoint.x, pointer.x); + const top = Math.min(this.startPoint.y, pointer.y); + + // 更新椭圆 + this.drawingObject.set({ + left: left, + top: top, + rx: rx, + ry: ry, + originX: "left", + originY: "top", + }); + + this.canvas.renderAll(); + } + + /** + * 结束椭圆选区 + */ + endEllipseSelection() { + if (!this.drawingObject || !this.startPoint || !this.isActive) return; + + // 获取椭圆参数 + const { left, top, rx, ry } = this.drawingObject; + + // 如果椭圆太小,忽略 + if (rx < 2 || ry < 2) { + this.canvas.remove(this.drawingObject); + this.drawingObject = null; + this.startPoint = null; + return; + } + + // 计算中心点 + const cx = left + rx; + const cy = top + ry; + + // 将椭圆转换为路径字符串 + const pathString = this.ellipseToSVGPath(cx, cy, rx, ry); + + // 创建最终选区对象 + const selectionObj = new fabric.Path(pathString, { + ...this.selectionStyle, + id: `selection_${Date.now()}`, + name: "selection", + fill: this.selectionStyle.fill, // 恢复填充 + }); + + // 移除绘制中的临时对象 + this.canvas.remove(this.drawingObject); + + // 重置绘制状态 + this.drawingObject = null; + this.startPoint = null; + + // 设置选区 + this.setSelectionObject(selectionObj); + } + + /** + * 将椭圆转换为SVG路径字符串 + * @param {Number} cx 中心点X坐标 + * @param {Number} cy 中心点Y坐标 + * @param {Number} rx X半径 + * @param {Number} ry Y半径 + * @returns {String} SVG路径字符串 + */ + ellipseToSVGPath(cx, cy, rx, ry) { + // 使用椭圆弧命令创建完整椭圆 + return `M ${cx - rx} ${cy} A ${rx} ${ry} 0 1 0 ${ + cx + rx + } ${cy} A ${rx} ${ry} 0 1 0 ${cx - rx} ${cy} Z`; + } + + /** + * 设置选区工具 + * @param {string} type 选区类型:OperationType.LASSO, OperationType.LASSO_RECTANGLE, OperationType.LASSO_ELLIPSE + */ + setSelectionType(type) { + this.selectionType = type; + + // 如果正在绘制,清除临时对象 + if (this.drawingObject) { + this.canvas.remove(this.drawingObject); + this.drawingObject = null; + this.startPoint = null; + } + } + + /** + * 设置选区工具的鼠标事件 + */ + setupSelectionEvents() { + // 选区事件现在通过 setCurrentTool 方法管理 + // 这个方法现在主要用于刷新或重置事件监听 + if (!this.canvas || !this.isActive) return; + + // 确保选区处于激活状态 + if (this.selectionTools.includes(this.currentTool)) { + this.isActive = true; + // 如果事件还没有初始化,初始化它们 + if (!this._mouseDownHandler) { + this.initEvents(); + } + } + } + + /** + * 清理资源 + */ + dispose() { + this.cleanupEvents(); + this.clearSelection(); + this.canvas = null; + this.commandManager = null; + this.layerManager = null; + } +} diff --git a/src/component/Canvas/CanvasEditor/store/BrushStore.js b/src/component/Canvas/CanvasEditor/store/BrushStore.js new file mode 100644 index 00000000..d3fd10ce --- /dev/null +++ b/src/component/Canvas/CanvasEditor/store/BrushStore.js @@ -0,0 +1,582 @@ +import { reactive, readonly } from "vue"; +import texturePresetManager from "../managers/brushes/TexturePresetManager"; + +/** + * 笔刷数据存储 + * 使用Vue 3的响应式API实现笔刷相关数据的全局状态管理 + */ +const state = reactive({ + // 笔刷基础属性 + size: 5, // 笔刷大小 + color: "#000000", // 笔刷颜色 + opacity: 1, // 笔刷透明度 + type: "pencil", // 当前笔刷类型 + + // 笔刷材质相关 + textureScale: 1, // 材质缩放 + textureEnabled: false, // 是否启用材质 + texturePath: "", // 材质图片路径 + textureOpacity: 1, // 材质透明度 + textureRepeat: "repeat", // 材质重复模式 + textureAngle: 0, // 材质旋转角度 + selectedTextureId: null, // 当前选中的材质ID + + // 可用笔刷类型列表 (由BrushManager初始化) + availableBrushes: [], + + // 自定义笔刷列表 + customBrushes: [], + + // 笔刷预设 + presets: [ + { name: "细线", size: 2, opacity: 1, color: "#000000", type: "pencil" }, + { name: "中粗", size: 5, opacity: 1, color: "#000000", type: "pencil" }, + { name: "粗线", size: 10, opacity: 1, color: "#000000", type: "pencil" }, + { name: "水彩", size: 15, opacity: 0.7, color: "#3366ff", type: "marker" }, + { name: "喷枪", size: 20, opacity: 0.5, color: "#ff6633", type: "spray" }, + ], + + // 材质预设 + texturePresets: [ + { + name: "默认纹理", + textureId: "preset_texture_0", + scale: 1, + opacity: 1, + repeat: "repeat", + angle: 0, + }, + { + name: "细纹理", + textureId: "preset_texture_1", + scale: 0.5, + opacity: 0.8, + repeat: "repeat", + angle: 0, + }, + { + name: "粗纹理", + textureId: "preset_texture_2", + scale: 2, + opacity: 1, + repeat: "repeat", + angle: 45, + }, + { + name: "水彩纹理", + textureId: "preset_texture_5", + scale: 1.5, + opacity: 0.6, + repeat: "no-repeat", + angle: 0, + }, + ], + + // 最近使用的颜色 + recentColors: ["#000000", "#ffffff", "#ff0000", "#00ff00", "#0000ff"], + + // 最近使用的材质 + recentTextures: [], + + // 当前笔刷可配置属性(由当前选中笔刷动态设置) + currentBrushProperties: [], + + // 当前笔刷实例的引用 + currentBrushInstance: null, + + // 笔刷属性值的映射,存储可由UI修改的属性值 + propertyValues: {}, +}); + +// Actions - 修改状态的函数 +const actions = { + setBrushSize(size) { + state.size = Math.max(0.5, Math.min(100, size)); + }, + + setBrushColor(color) { + state.color = color; + // 添加到最近使用的颜色 + if (!state.recentColors.includes(color)) { + state.recentColors.unshift(color); + if (state.recentColors.length > 10) { + state.recentColors.pop(); + } + } + }, + + setBrushOpacity(opacity) { + state.opacity = Math.max(0.05, Math.min(1, opacity)); + }, + + setBrushType(type) { + if (state.availableBrushes.some((brush) => brush.id === type)) { + state.type = type; + } + }, + + setTextureScale(scale) { + state.textureScale = Math.max(0.1, Math.min(10, scale)); + }, + + setTextureEnabled(enabled) { + state.textureEnabled = enabled; + }, + + setTexturePath(path) { + state.texturePath = path; + }, + + setAvailableBrushes(brushes) { + state.availableBrushes = brushes; + }, + + addCustomBrush(brush) { + if (!brush.id) { + brush.id = `custom_${Date.now()}`; + } + + state.customBrushes.push(brush); + return brush.id; + }, + + removeCustomBrush(brushId) { + const index = state.customBrushes.findIndex((b) => b.id === brushId); + if (index !== -1) { + state.customBrushes.splice(index, 1); + return true; + } + return false; + }, + + // 应用预设 + applyPreset(presetIndex) { + const preset = state.presets[presetIndex]; + if (preset) { + state.size = preset.size; + state.opacity = preset.opacity; + state.color = preset.color; + state.type = preset.type; + return true; + } + return false; + }, + + // 将当前设置保存为新预设 + saveCurrentAsPreset(name) { + const newPreset = { + name: name || `预设 ${state.presets.length + 1}`, + size: state.size, + opacity: state.opacity, + color: state.color, + type: state.type, + textureEnabled: state.textureEnabled, + textureScale: state.textureScale, + texturePath: state.texturePath, + }; + + state.presets.push(newPreset); + return state.presets.length - 1; // 返回新预设的索引 + }, + + /** + * 设置当前笔刷实例 + * @param {Object} brushInstance BaseBrush实例 + */ + setCurrentBrushInstance(brushInstance) { + state.currentBrushInstance = brushInstance; + + // 获取并设置当前笔刷的可配置属性 + if (brushInstance && brushInstance.getConfigurableProperties) { + const properties = brushInstance.getConfigurableProperties(); + state.currentBrushProperties = properties; + + // 初始化属性值 + properties.forEach((prop) => { + // 如果是基础属性,使用已有值 + if (prop.id === "size") { + state.propertyValues[prop.id] = state.size; + } else if (prop.id === "color") { + state.propertyValues[prop.id] = state.color; + } else if (prop.id === "opacity") { + state.propertyValues[prop.id] = state.opacity; + } else { + // 对于特殊属性,使用默认值 + state.propertyValues[prop.id] = prop.defaultValue; + } + }); + } else { + // 如果没有实例或方法,清空属性列表 + state.currentBrushProperties = []; + } + }, + + /** + * 更新笔刷属性值 + * @param {String} propId 属性ID + * @param {any} value 属性值 + */ + updatePropertyValue(propId, value) { + // 更新Store中的值 + state.propertyValues[propId] = value; + + // 同步更新基础属性 + if (propId === "size") { + state.size = value; + } else if (propId === "color") { + state.color = value; + } else if (propId === "opacity") { + state.opacity = value; + } + + // 如果有当前笔刷实例且有更新方法,则调用 + if ( + state.currentBrushInstance && + state.currentBrushInstance.updateProperty + ) { + state.currentBrushInstance.updateProperty(propId, value); + } + }, + + /** + * 获取属性值 + * @param {String} propId 属性ID + * @param {any} defaultValue 默认值 + * @returns {any} 属性值 + */ + getPropertyValue(propId, defaultValue) { + // 检查属性值是否存在 + if (state.propertyValues.hasOwnProperty(propId)) { + return state.propertyValues[propId]; + } + + // 对于基础属性,返回store中的值 + if (propId === "size") { + return state.size; + } else if (propId === "color") { + return state.color; + } else if (propId === "opacity") { + return state.opacity; + } + + // 否则返回默认值 + return defaultValue; + }, + + /** + * 按分类获取当前笔刷可配置属性 + * @returns {Object} 按分类分组的属性对象 + */ + getPropertiesByCategory() { + const result = {}; + + state.currentBrushProperties.forEach((prop) => { + const category = prop.category || "默认"; + if (!result[category]) { + result[category] = []; + } + result[category].push({ + ...prop, + value: this.getPropertyValue(prop.id, prop.defaultValue), + }); + }); + + // 按order排序每个分类中的属性 + Object.keys(result).forEach((category) => { + result[category].sort((a, b) => (a.order || 0) - (b.order || 0)); + }); + + return result; + }, + + /** + * 材质相关方法 + */ + setTextureOpacity(opacity) { + state.textureOpacity = Math.max(0, Math.min(1, opacity)); + }, + + setTextureRepeat(repeat) { + const validModes = ["repeat", "repeat-x", "repeat-y", "no-repeat"]; + if (validModes.includes(repeat)) { + state.textureRepeat = repeat; + } + }, + + setTextureAngle(angle) { + state.textureAngle = angle % 360; + }, + + setSelectedTextureId(textureId) { + state.selectedTextureId = textureId; + + // 添加到最近使用的材质 + if (textureId && !state.recentTextures.includes(textureId)) { + state.recentTextures.unshift(textureId); + if (state.recentTextures.length > 8) { + state.recentTextures.pop(); + } + } + }, + + /** + * 应用材质预设 + * @param {Number} presetIndex 预设索引 + * @returns {Boolean} 是否应用成功 + */ + applyTexturePreset(presetIndex) { + const preset = state.texturePresets[presetIndex]; + if (preset) { + state.selectedTextureId = preset.textureId; + state.textureScale = preset.scale; + state.textureOpacity = preset.opacity; + state.textureRepeat = preset.repeat; + state.textureAngle = preset.angle; + + // 添加到最近使用 + this.setSelectedTextureId(preset.textureId); + + return true; + } + return false; + }, + + /** + * 将当前材质设置保存为新预设 + * @param {String} name 预设名称 + * @returns {Number} 新预设的索引 + */ + saveCurrentTextureAsPreset(name) { + const newPreset = { + name: name || `材质预设 ${state.texturePresets.length + 1}`, + textureId: state.selectedTextureId, + scale: state.textureScale, + opacity: state.textureOpacity, + repeat: state.textureRepeat, + angle: state.textureAngle, + }; + + state.texturePresets.push(newPreset); + return state.texturePresets.length - 1; + }, + + /** + * 删除材质预设 + * @param {Number} presetIndex 预设索引 + * @returns {Boolean} 是否删除成功 + */ + removeTexturePreset(presetIndex) { + if (presetIndex >= 0 && presetIndex < state.texturePresets.length) { + state.texturePresets.splice(presetIndex, 1); + return true; + } + return false; + }, + + /** + * 获取所有可用材质(预设+自定义) + * @returns {Array} 材质列表 + */ + getAllTextures() { + return texturePresetManager.getAllTextures(); + }, + + /** + * 根据ID获取材质信息 + * @param {String} textureId 材质ID + * @returns {Object|null} 材质对象 + */ + getTextureById(textureId) { + return texturePresetManager.getTextureById(textureId); + }, + + /** + * 按分类获取材质 + * @param {String} category 分类名称 + * @returns {Array} 材质列表 + */ + getTexturesByCategory(category) { + return texturePresetManager.getTexturesByCategory(category); + }, + + /** + * 获取材质分类列表 + * @returns {Array} 分类名称数组 + */ + getTextureCategories() { + return texturePresetManager.getCategories(); + }, + + /** + * 搜索材质 + * @param {String} keyword 搜索关键词 + * @returns {Array} 匹配的材质列表 + */ + searchTextures(keyword) { + return texturePresetManager.searchTextures(keyword); + }, + + /** + * 添加自定义材质 + * @param {Object} textureData 材质数据 + * @returns {String} 材质ID + */ + addCustomTexture(textureData) { + const textureId = texturePresetManager.addCustomTexture(textureData); + + // 保存到本地存储 + texturePresetManager.saveCustomTexturesToStorage(); + + return textureId; + }, + + /** + * 删除自定义材质 + * @param {String} textureId 材质ID + * @returns {Boolean} 是否删除成功 + */ + removeCustomTexture(textureId) { + const success = texturePresetManager.removeCustomTexture(textureId); + + if (success) { + // 如果删除的是当前选中的材质,清空选择 + if (state.selectedTextureId === textureId) { + state.selectedTextureId = null; + } + + // 从最近使用中移除 + const recentIndex = state.recentTextures.indexOf(textureId); + if (recentIndex !== -1) { + state.recentTextures.splice(recentIndex, 1); + } + + // 保存到本地存储 + texturePresetManager.saveCustomTexturesToStorage(); + } + + return success; + }, + + /** + * 从文件上传自定义材质 + * @param {File} file 图片文件 + * @param {String} name 材质名称(可选) + * @returns {Promise} 材质ID + */ + uploadCustomTexture(file, name) { + return new Promise((resolve, reject) => { + // 验证文件 + if (!texturePresetManager.validateTextureFile(file)) { + reject(new Error("无效的材质文件")); + return; + } + + // 读取文件 + const reader = new FileReader(); + reader.onload = (e) => { + try { + const textureData = { + name: name || file.name.replace(/\.[^/.]+$/, ""), + path: e.target.result, + preview: e.target.result, + description: `用户上传的材质: ${file.name}`, + }; + + const textureId = this.addCustomTexture(textureData); + resolve(textureId); + } catch (error) { + reject(error); + } + }; + + reader.onerror = () => { + reject(new Error("文件读取失败")); + }; + + reader.readAsDataURL(file); + }); + }, + + /** + * 导出材质预设配置 + * @returns {String} JSON格式的配置 + */ + exportTexturePresets() { + const config = { + texturePresets: state.texturePresets, + customTextures: texturePresetManager.exportCustomTextures(), + }; + return JSON.stringify(config, null, 2); + }, + + /** + * 导入材质预设配置 + * @param {String} configJson JSON格式的配置 + * @returns {Boolean} 是否导入成功 + */ + importTexturePresets(configJson) { + try { + const config = JSON.parse(configJson); + + // 导入材质预设 + if (config.texturePresets && Array.isArray(config.texturePresets)) { + state.texturePresets = [ + ...state.texturePresets, + ...config.texturePresets, + ]; + } + + // 导入自定义材质 + if (config.customTextures) { + texturePresetManager.importCustomTextures(config.customTextures); + } + + return true; + } catch (error) { + console.error("导入材质预设失败:", error); + return false; + } + }, + + /** + * 初始化材质预设管理器 + */ + initializeTexturePresets() { + // 从本地存储加载自定义材质 + texturePresetManager.loadCustomTexturesFromStorage(); + + // 确保预设材质引用的是有效的材质ID + state.texturePresets.forEach((preset, index) => { + const texture = texturePresetManager.getTextureById(preset.textureId); + if (!texture) { + console.warn( + `材质预设 "${preset.name}" 引用的材质 ${preset.textureId} 不存在` + ); + // 可以选择使用默认材质替换或删除该预设 + if (texturePresetManager.getAllTextures().length > 0) { + preset.textureId = texturePresetManager.getAllTextures()[0].id; + } + } + }); + }, +}; + +// 暴露给组件使用的Store对象 +export const BrushStore = { + // 只读状态,防止直接修改 + state: readonly(state), + + // 可调用的Actions + ...actions, + + // 辅助方法 + getRGBAColor() { + // 解析十六进制颜色并添加透明度 + const hex = state.color.replace("#", ""); + const r = parseInt(hex.substring(0, 2), 16); + const g = parseInt(hex.substring(2, 4), 16); + const b = parseInt(hex.substring(4, 6), 16); + + return `rgba(${r}, ${g}, ${b}, ${state.opacity})`; + }, +}; diff --git a/src/component/Canvas/CanvasEditor/utils/canvasFactory.js b/src/component/Canvas/CanvasEditor/utils/canvasFactory.js new file mode 100644 index 00000000..c0a22d30 --- /dev/null +++ b/src/component/Canvas/CanvasEditor/utils/canvasFactory.js @@ -0,0 +1,31 @@ +//import { fabric } from "fabric-with-all"; +import { canvasConfig } from "../config/canvasConfig"; + +/** + * Factory for creating optimized fabric canvas instances + */ +export const createCanvas = (elementId, options = {}) => { + // Create the canvas instance + const canvas = new fabric.Canvas(elementId, { + enableRetinaScaling: canvasConfig.enableRetinaScaling, + renderOnAddRemove: false, + enableRetinaScaling: true, + preserveObjectStacking: true, // 保持对象堆叠顺序 + // skipOffscreen: true, // 跳过离屏渲染 + ...options, + }); + + return canvas; +}; + +/** + * Utility to create a static canvas (for improved performance when interaction is not needed) + */ +export const createStaticCanvas = (elementId, options = {}) => { + const canvas = new fabric.StaticCanvas(elementId, { + enableRetinaScaling: canvasConfig.enableRetinaScaling, + ...options, + }); + + return canvas; +}; diff --git a/src/component/Canvas/CanvasEditor/utils/helper.js b/src/component/Canvas/CanvasEditor/utils/helper.js new file mode 100644 index 00000000..22a60458 --- /dev/null +++ b/src/component/Canvas/CanvasEditor/utils/helper.js @@ -0,0 +1,458 @@ +export function deepCompare(obj1, obj2) { + const diff = {}; + + // 处理基础类型 + if (obj1 === obj2) { + return null; + } + + if ( + obj1 === null || + obj2 === null || + typeof obj1 !== "object" || + typeof obj2 !== "object" + ) { + return { _value: obj2, _oldValue: obj1 }; + } + + // 处理数组 + if (Array.isArray(obj1) && Array.isArray(obj2)) { + if (obj1.length !== obj2.length) { + return { _value: obj2, _oldValue: obj1 }; + } + + for (let i = 0; i < obj1.length; i++) { + const itemDiff = deepCompare(obj1[i], obj2[i]); + if (itemDiff !== null) { + diff[i] = itemDiff; + } + } + + return Object.keys(diff).length > 0 ? diff : null; + } + + // 处理对象 + const allKeys = new Set([...Object.keys(obj1), ...Object.keys(obj2)]); + + for (const key of allKeys) { + const val1 = obj1[key]; + const val2 = obj2[key]; + + if (!(key in obj1)) { + diff[key] = { _value: val2, _type: "added" }; + } else if (!(key in obj2)) { + diff[key] = { _value: undefined, _oldValue: val1, _type: "removed" }; + } else { + const itemDiff = deepCompare(val1, val2); + if (itemDiff !== null) { + diff[key] = itemDiff; + } + } + } + + return Object.keys(diff).length > 0 ? diff : null; +} + +/** + * 深度克隆对象 + * @param {any} obj 要克隆的对象 + * @returns {any} 克隆后的对象 + */ +export function deepClone(obj) { + if (obj === null || typeof obj !== "object") { + return obj; + } + + if (obj instanceof Date) { + return new Date(obj); + } + + if (obj instanceof RegExp) { + return new RegExp(obj); + } + + if (Array.isArray(obj)) { + return obj.map((item) => deepClone(item)); + } + + if (typeof obj === "object") { + const cloned = {}; + for (const key in obj) { + if (obj.hasOwnProperty(key)) { + cloned[key] = deepClone(obj[key]); + } + } + return cloned; + } + + return obj; +} + +/** + * 应用差异到基础对象 + * @param {Object} baseObj 基础对象 + * @param {Object} diff 差异对象 + * @returns {Object} 应用差异后的对象 + */ +export function applyDiff(baseObj, diff) { + if (!diff) { + return deepClone(baseObj); + } + + // 如果是直接值替换 + if (diff._value !== undefined) { + return diff._value; + } + + const result = deepClone(baseObj) || {}; + + for (const key in diff) { + const change = diff[key]; + + if (change._type === "added" || change._type === "removed") { + if (change._type === "removed") { + delete result[key]; + } else { + result[key] = change._value; + } + } else if (change._value !== undefined) { + result[key] = change._value; + } else { + // 递归应用嵌套差异 + result[key] = applyDiff(result[key], change); + } + } + + return result; +} + +/** + * 节流函数 + * @param {Function} func 要节流的函数 + * @param {number} wait 等待时间 + * @returns {Function} 节流后的函数 + */ +export function throttle(func, wait) { + let timeout; + let previous = 0; + + return function executedFunction(...args) { + const now = Date.now(); + const remaining = wait - (now - previous); + + if (remaining <= 0 || remaining > wait) { + if (timeout) { + clearTimeout(timeout); + timeout = null; + } + previous = now; + return func.apply(this, args); + } else if (!timeout) { + timeout = setTimeout(() => { + previous = Date.now(); + timeout = null; + func.apply(this, args); + }, remaining); + } + }; +} + +/** + * 防抖函数 + * @param {Function} func 要防抖的函数 + * @param {number} wait 等待时间 + * @param {boolean} immediate 是否立即执行 + * @returns {Function} 防抖后的函数 + */ +export function debounce(func, wait, immediate = false) { + let timeout; + + return function executedFunction(...args) { + const later = () => { + timeout = null; + if (!immediate) func.apply(this, args); + }; + + const callNow = immediate && !timeout; + clearTimeout(timeout); + timeout = setTimeout(later, wait); + + if (callNow) func.apply(this, args); + }; +} + +/** + * 生成唯一ID + * @param {string} prefix 前缀 + * @returns {string} 唯一ID + */ +export function generateId(prefix = "id") { + return `${prefix}_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; +} + +/** + * 格式化文件大小 + * @param {number} bytes 字节数 + * @returns {string} 格式化后的大小 + */ +export function formatFileSize(bytes) { + if (bytes === 0) return "0 Bytes"; + + const k = 1024; + const sizes = ["Bytes", "KB", "MB", "GB", "TB"]; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + + return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i]; +} + +/** + * 格式化时间差 + * @param {number} milliseconds 毫秒数 + * @returns {string} 格式化后的时间 + */ +export function formatDuration(milliseconds) { + if (milliseconds < 1000) { + return `${milliseconds.toFixed(2)}ms`; + } else if (milliseconds < 60000) { + return `${(milliseconds / 1000).toFixed(2)}s`; + } else { + const minutes = Math.floor(milliseconds / 60000); + const seconds = ((milliseconds % 60000) / 1000).toFixed(2); + return `${minutes}m ${seconds}s`; + } +} + +/** + * 检查是否是有效的命令对象 + * @param {*} command 命令对象 + * @returns {boolean} 是否有效 + */ +export function isValidCommand(command) { + return ( + command && + typeof command === "object" && + typeof command.execute === "function" + ); +} + +/** + * 检查是否是Promise + * @param {*} obj 对象 + * @returns {boolean} 是否是Promise + */ +export function isPromise(obj) { + return obj && typeof obj.then === "function"; +} + +/** + * 安全的JSON解析 + * @param {string} jsonString JSON字符串 + * @param {*} defaultValue 默认值 + * @returns {*} 解析结果 + */ +export function safeJSONParse(jsonString, defaultValue = null) { + try { + return JSON.parse(jsonString); + } catch (error) { + console.warn("JSON解析失败:", error); + return defaultValue; + } +} + +/** + * 安全的JSON序列化 + * @param {*} obj 要序列化的对象 + * @param {*} defaultValue 默认值 + * @returns {string} JSON字符串 + */ +export function safeJSONStringify(obj, defaultValue = "{}") { + try { + return JSON.stringify(obj); + } catch (error) { + console.warn("JSON序列化失败:", error); + return defaultValue; + } +} + +/** + * 计算对象深度 + * @param {*} obj 对象 + * @returns {number} 深度 + */ +export function getObjectDepth(obj) { + if (obj === null || typeof obj !== "object") { + return 0; + } + + let maxDepth = 0; + for (const key in obj) { + if (obj.hasOwnProperty(key)) { + const depth = getObjectDepth(obj[key]); + maxDepth = Math.max(maxDepth, depth); + } + } + + return maxDepth + 1; +} + +/** + * 计算对象大小(字节) + * @param {*} obj 对象 + * @returns {number} 大小(字节) + */ +export function getObjectSize(obj) { + const jsonString = safeJSONStringify(obj, "{}"); + return new Blob([jsonString]).size; +} + +/** + * 检查浏览器支持 + * @returns {Object} 支持信息 + */ +export function checkBrowserSupport() { + return { + WeakRef: typeof WeakRef !== "undefined", + FinalizationRegistry: typeof FinalizationRegistry !== "undefined", + PerformanceMemory: + typeof performance !== "undefined" && !!performance.memory, + RequestIdleCallback: typeof requestIdleCallback !== "undefined", + IntersectionObserver: typeof IntersectionObserver !== "undefined", + ResizeObserver: typeof ResizeObserver !== "undefined", + }; +} + +/** + * 延迟执行 + * @param {number} ms 延迟毫秒数 + * @returns {Promise} Promise对象 + */ +export function delay(ms) { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +/** + * 重试执行函数 + * @param {Function} fn 要执行的函数 + * @param {Object} options 重试选项 + * @returns {Promise} 执行结果 + */ +export async function retry(fn, options = {}) { + const { + retries = 3, + delay: delayMs = 1000, + backoff = 1.5, + shouldRetry = () => true, + } = options; + + let attempt = 0; + let currentDelay = delayMs; + + while (attempt <= retries) { + try { + return await fn(); + } catch (error) { + attempt++; + + if (attempt > retries || !shouldRetry(error)) { + throw error; + } + + await delay(currentDelay); + currentDelay *= backoff; + } + } +} + +/** + * 批处理执行 + * @param {Array} items 要处理的项目 + * @param {Function} processor 处理函数 + * @param {Object} options 批处理选项 + * @returns {Promise} 处理结果 + */ +export async function batchProcess(items, processor, options = {}) { + const { batchSize = 10, delay: delayMs = 0, onProgress = () => {} } = options; + + const results = []; + + for (let i = 0; i < items.length; i += batchSize) { + const batch = items.slice(i, i + batchSize); + const batchResults = await Promise.all( + batch.map((item) => processor(item)) + ); + + results.push(...batchResults); + + onProgress({ + completed: Math.min(i + batchSize, items.length), + total: items.length, + percentage: Math.min(((i + batchSize) / items.length) * 100, 100), + }); + + if (delayMs > 0 && i + batchSize < items.length) { + await delay(delayMs); + } + } + + return results; +} + +/** + * 创建可取消的Promise + * @param {Function} executor Promise执行器 + * @returns {Object} 包含promise和cancel方法的对象 + */ +export function createCancellablePromise(executor) { + let isCancelled = false; + let cancelCallback = null; + + const promise = new Promise((resolve, reject) => { + const wrappedResolve = (value) => { + if (!isCancelled) resolve(value); + }; + + const wrappedReject = (error) => { + if (!isCancelled) reject(error); + }; + + cancelCallback = () => { + isCancelled = true; + reject(new Error("Promise was cancelled")); + }; + + executor(wrappedResolve, wrappedReject); + }); + + return { + promise, + cancel: () => { + if (cancelCallback) { + cancelCallback(); + } + }, + isCancelled: () => isCancelled, + }; +} + +// 导出所有工具函数 +export default { + deepCompare, + deepClone, + applyDiff, + throttle, + debounce, + generateId, + formatFileSize, + formatDuration, + isValidCommand, + isPromise, + safeJSONParse, + safeJSONStringify, + getObjectDepth, + getObjectSize, + checkBrowserSupport, + delay, + retry, + batchProcess, + createCancellablePromise, +}; diff --git a/src/component/Canvas/CanvasEditor/utils/helperLine.js b/src/component/Canvas/CanvasEditor/utils/helperLine.js new file mode 100644 index 00000000..620f0bf1 --- /dev/null +++ b/src/component/Canvas/CanvasEditor/utils/helperLine.js @@ -0,0 +1,532 @@ +/** + * Should objects be aligned by a bounding box? + * [Bug] Scaled objects sometimes can not be aligned by edges + * + */ +function initAligningGuidelines(canvas) { + var ctx = canvas.getSelectionContext(), + aligningLineOffset = 5, + aligningLineMargin = 4, + aligningLineWidth = 1, + aligningLineColor = "rgb(0,255,0)", + viewportTransform, + zoom = 1; + + function drawVerticalLine(coords) { + drawLine( + coords.x + 0.5, + coords.y1 > coords.y2 ? coords.y2 : coords.y1, + coords.x + 0.5, + coords.y2 > coords.y1 ? coords.y2 : coords.y1 + ); + } + + function drawHorizontalLine(coords) { + drawLine( + coords.x1 > coords.x2 ? coords.x2 : coords.x1, + coords.y + 0.5, + coords.x2 > coords.x1 ? coords.x2 : coords.x1, + coords.y + 0.5 + ); + } + + function drawLine(x1, y1, x2, y2) { + ctx.save(); + ctx.lineWidth = aligningLineWidth; + ctx.strokeStyle = aligningLineColor; + ctx.beginPath(); + ctx.moveTo( + x1 * zoom + viewportTransform[4], + y1 * zoom + viewportTransform[5] + ); + ctx.lineTo( + x2 * zoom + viewportTransform[4], + y2 * zoom + viewportTransform[5] + ); + ctx.stroke(); + ctx.restore(); + } + + function isInRange(value1, value2) { + value1 = Math.round(value1); + value2 = Math.round(value2); + for ( + var i = value1 - aligningLineMargin, len = value1 + aligningLineMargin; + i <= len; + i++ + ) { + if (i === value2) { + return true; + } + } + return false; + } + + var verticalLines = [], + horizontalLines = []; + + canvas.on("mouse:down", function () { + viewportTransform = canvas.viewportTransform; + zoom = canvas.getZoom(); + }); + + canvas.on("object:moving", function (e) { + var activeObject = e.target, + canvasObjects = canvas.getObjects(), + activeObjectCenter = activeObject.getCenterPoint(), + activeObjectLeft = activeObjectCenter.x, + activeObjectTop = activeObjectCenter.y, + activeObjectBoundingRect = activeObject.getBoundingRect(), + activeObjectHeight = + activeObjectBoundingRect.height / viewportTransform[3], + activeObjectWidth = activeObjectBoundingRect.width / viewportTransform[0], + horizontalInTheRange = false, + verticalInTheRange = false, + transform = canvas._currentTransform; + + if (!transform) return; + + // It should be trivial to DRY this up by encapsulating (repeating) creation of x1, x2, y1, and y2 into functions, + // but we're not doing it here for perf. reasons -- as this a function that's invoked on every mouse move + + for (var i = canvasObjects.length; i--; ) { + if (canvasObjects[i] === activeObject) continue; + + var objectCenter = canvasObjects[i].getCenterPoint(), + objectLeft = objectCenter.x, + objectTop = objectCenter.y, + objectBoundingRect = canvasObjects[i].getBoundingRect(), + objectHeight = objectBoundingRect.height / viewportTransform[3], + objectWidth = objectBoundingRect.width / viewportTransform[0]; + + // snaps if the right side of the active object touches the left side of the object + if ( + isInRange( + activeObjectLeft + activeObjectWidth / 2, + objectLeft - objectWidth / 2 + ) + ) { + verticalInTheRange = true; + verticalLines.push({ + x: objectLeft - objectWidth / 2, + y1: + objectTop < activeObjectTop + ? objectTop - objectHeight / 2 - aligningLineOffset + : objectTop + objectHeight / 2 + aligningLineOffset, + y2: + activeObjectTop > objectTop + ? activeObjectTop + activeObjectHeight / 2 + aligningLineOffset + : activeObjectTop - activeObjectHeight / 2 - aligningLineOffset, + }); + + activeObject.setPositionByOrigin( + new fabric.Point( + objectLeft - objectWidth / 2 - activeObjectWidth / 2, + activeObjectTop + ), + "center", + "center" + ); + } + + // snaps if the left side of the active object touches the right side of the object + if ( + isInRange( + activeObjectLeft - activeObjectWidth / 2, + objectLeft + objectWidth / 2 + ) + ) { + verticalInTheRange = true; + verticalLines.push({ + x: objectLeft + objectWidth / 2, + y1: + objectTop < activeObjectTop + ? objectTop - objectHeight / 2 - aligningLineOffset + : objectTop + objectHeight / 2 + aligningLineOffset, + y2: + activeObjectTop > objectTop + ? activeObjectTop + activeObjectHeight / 2 + aligningLineOffset + : activeObjectTop - activeObjectHeight / 2 - aligningLineOffset, + }); + + activeObject.setPositionByOrigin( + new fabric.Point( + objectLeft + objectWidth / 2 + activeObjectWidth / 2, + activeObjectTop + ), + "center", + "center" + ); + } + + // snaps if the bottom of the object touches the top of the active object + if ( + isInRange( + objectTop + objectHeight / 2, + activeObjectTop - activeObjectHeight / 2 + ) + ) { + horizontalInTheRange = true; + horizontalLines.push({ + y: objectTop + objectHeight / 2, + x1: + objectLeft < activeObjectLeft + ? objectLeft - objectWidth / 2 - aligningLineOffset + : objectLeft + objectWidth / 2 + aligningLineOffset, + x2: + activeObjectLeft > objectLeft + ? activeObjectLeft + activeObjectWidth / 2 + aligningLineOffset + : activeObjectLeft - activeObjectWidth / 2 - aligningLineOffset, + }); + + activeObject.setPositionByOrigin( + new fabric.Point( + activeObjectLeft, + objectTop + objectHeight / 2 + activeObjectHeight / 2 + ), + "center", + "center" + ); + } + + // snaps if the top of the object touches the bottom of the active object + if ( + isInRange( + objectTop - objectHeight / 2, + activeObjectTop + activeObjectHeight / 2 + ) + ) { + horizontalInTheRange = true; + horizontalLines.push({ + y: objectTop - objectHeight / 2, + x1: + objectLeft < activeObjectLeft + ? objectLeft - objectWidth / 2 - aligningLineOffset + : objectLeft + objectWidth / 2 + aligningLineOffset, + x2: + activeObjectLeft > objectLeft + ? activeObjectLeft + activeObjectWidth / 2 + aligningLineOffset + : activeObjectLeft - activeObjectWidth / 2 - aligningLineOffset, + }); + + activeObject.setPositionByOrigin( + new fabric.Point( + activeObjectLeft, + objectTop - objectHeight / 2 - activeObjectHeight / 2 + ), + "center", + "center" + ); + } + + // snap by the horizontal center line + if (isInRange(objectLeft, activeObjectLeft)) { + verticalInTheRange = true; + verticalLines.push({ + x: objectLeft, + y1: + objectTop < activeObjectTop + ? objectTop - objectHeight / 2 - aligningLineOffset + : objectTop + objectHeight / 2 + aligningLineOffset, + y2: + activeObjectTop > objectTop + ? activeObjectTop + activeObjectHeight / 2 + aligningLineOffset + : activeObjectTop - activeObjectHeight / 2 - aligningLineOffset, + }); + activeObject.setPositionByOrigin( + new fabric.Point(objectLeft, activeObjectTop), + "center", + "center" + ); + } + + // snap by the left edge + if ( + isInRange( + objectLeft - objectWidth / 2, + activeObjectLeft - activeObjectWidth / 2 + ) + ) { + verticalInTheRange = true; + verticalLines.push({ + x: objectLeft - objectWidth / 2, + y1: + objectTop < activeObjectTop + ? objectTop - objectHeight / 2 - aligningLineOffset + : objectTop + objectHeight / 2 + aligningLineOffset, + y2: + activeObjectTop > objectTop + ? activeObjectTop + activeObjectHeight / 2 + aligningLineOffset + : activeObjectTop - activeObjectHeight / 2 - aligningLineOffset, + }); + activeObject.setPositionByOrigin( + new fabric.Point( + objectLeft - objectWidth / 2 + activeObjectWidth / 2, + activeObjectTop + ), + "center", + "center" + ); + } + + // snap by the right edge + if ( + isInRange( + objectLeft + objectWidth / 2, + activeObjectLeft + activeObjectWidth / 2 + ) + ) { + verticalInTheRange = true; + verticalLines.push({ + x: objectLeft + objectWidth / 2, + y1: + objectTop < activeObjectTop + ? objectTop - objectHeight / 2 - aligningLineOffset + : objectTop + objectHeight / 2 + aligningLineOffset, + y2: + activeObjectTop > objectTop + ? activeObjectTop + activeObjectHeight / 2 + aligningLineOffset + : activeObjectTop - activeObjectHeight / 2 - aligningLineOffset, + }); + activeObject.setPositionByOrigin( + new fabric.Point( + objectLeft + objectWidth / 2 - activeObjectWidth / 2, + activeObjectTop + ), + "center", + "center" + ); + } + + // snap by the vertical center line + if (isInRange(objectTop, activeObjectTop)) { + horizontalInTheRange = true; + horizontalLines.push({ + y: objectTop, + x1: + objectLeft < activeObjectLeft + ? objectLeft - objectWidth / 2 - aligningLineOffset + : objectLeft + objectWidth / 2 + aligningLineOffset, + x2: + activeObjectLeft > objectLeft + ? activeObjectLeft + activeObjectWidth / 2 + aligningLineOffset + : activeObjectLeft - activeObjectWidth / 2 - aligningLineOffset, + }); + activeObject.setPositionByOrigin( + new fabric.Point(activeObjectLeft, objectTop), + "center", + "center" + ); + } + + // snap by the top edge + if ( + isInRange( + objectTop - objectHeight / 2, + activeObjectTop - activeObjectHeight / 2 + ) + ) { + horizontalInTheRange = true; + horizontalLines.push({ + y: objectTop - objectHeight / 2, + x1: + objectLeft < activeObjectLeft + ? objectLeft - objectWidth / 2 - aligningLineOffset + : objectLeft + objectWidth / 2 + aligningLineOffset, + x2: + activeObjectLeft > objectLeft + ? activeObjectLeft + activeObjectWidth / 2 + aligningLineOffset + : activeObjectLeft - activeObjectWidth / 2 - aligningLineOffset, + }); + activeObject.setPositionByOrigin( + new fabric.Point( + activeObjectLeft, + objectTop - objectHeight / 2 + activeObjectHeight / 2 + ), + "center", + "center" + ); + } + + // snap by the bottom edge + if ( + isInRange( + objectTop + objectHeight / 2, + activeObjectTop + activeObjectHeight / 2 + ) + ) { + horizontalInTheRange = true; + horizontalLines.push({ + y: objectTop + objectHeight / 2, + x1: + objectLeft < activeObjectLeft + ? objectLeft - objectWidth / 2 - aligningLineOffset + : objectLeft + objectWidth / 2 + aligningLineOffset, + x2: + activeObjectLeft > objectLeft + ? activeObjectLeft + activeObjectWidth / 2 + aligningLineOffset + : activeObjectLeft - activeObjectWidth / 2 - aligningLineOffset, + }); + activeObject.setPositionByOrigin( + new fabric.Point( + activeObjectLeft, + objectTop + objectHeight / 2 - activeObjectHeight / 2 + ), + "center", + "center" + ); + } + } + + if (!horizontalInTheRange) { + horizontalLines.length = 0; + } + + if (!verticalInTheRange) { + verticalLines.length = 0; + } + }); + + canvas.on("before:render", function () { + if (canvas.contextTop) { + canvas.clearContext(canvas.contextTop); + } + }); + + canvas.on("after:render", function () { + for (var i = verticalLines.length; i--; ) { + drawVerticalLine(verticalLines[i]); + } + for (var i = horizontalLines.length; i--; ) { + drawHorizontalLine(horizontalLines[i]); + } + + verticalLines.length = horizontalLines.length = 0; + }); + + canvas.on("mouse:up", function () { + verticalLines.length = horizontalLines.length = 0; + canvas.renderAll(); + }); +} + +export default initAligningGuidelines; + +/** + * Augments canvas by assigning to `onObjectMove` and `onAfterRender`. + * This kind of sucks because other code using those methods will stop functioning. + * Need to fix it by replacing callbacks with pub/sub kind of subscription model. + * (or maybe use existing fabric.util.fire/observe (if it won't be too slow)) + */ + +export function initCenteringGuidelines(canvas) { + var canvasWidth = canvas.getWidth(), + canvasHeight = canvas.getHeight(), + canvasWidthCenter = canvasWidth / 2, + canvasHeightCenter = canvasHeight / 2, + canvasWidthCenterMap = {}, + canvasHeightCenterMap = {}, + centerLineMargin = 4, + centerLineColor = "rgba(255,0,241,0.5)", + centerLineWidth = 1, + ctx = canvas.getSelectionContext(), + viewportTransform; + + for ( + var i = canvasWidthCenter - centerLineMargin, + len = canvasWidthCenter + centerLineMargin; + i <= len; + i++ + ) { + canvasWidthCenterMap[Math.round(i)] = true; + } + for ( + var i = canvasHeightCenter - centerLineMargin, + len = canvasHeightCenter + centerLineMargin; + i <= len; + i++ + ) { + canvasHeightCenterMap[Math.round(i)] = true; + } + + function showVerticalCenterLine() { + showCenterLine( + canvasWidthCenter + 0.5, + 0, + canvasWidthCenter + 0.5, + canvasHeight + ); + } + + function showHorizontalCenterLine() { + showCenterLine( + 0, + canvasHeightCenter + 0.5, + canvasWidth, + canvasHeightCenter + 0.5 + ); + } + + function showCenterLine(x1, y1, x2, y2) { + ctx.save(); + ctx.strokeStyle = centerLineColor; + ctx.lineWidth = centerLineWidth; + ctx.beginPath(); + ctx.moveTo(x1 * viewportTransform[0], y1 * viewportTransform[3]); + ctx.lineTo(x2 * viewportTransform[0], y2 * viewportTransform[3]); + ctx.stroke(); + ctx.restore(); + } + + var afterRenderActions = [], + isInVerticalCenter, + isInHorizontalCenter; + + canvas.on("mouse:down", function () { + viewportTransform = canvas.viewportTransform; + }); + + canvas.on("object:moving", function (e) { + var object = e.target, + objectCenter = object.getCenterPoint(), + transform = canvas._currentTransform; + + if (!transform) return; + + (isInVerticalCenter = Math.round(objectCenter.x) in canvasWidthCenterMap), + (isInHorizontalCenter = + Math.round(objectCenter.y) in canvasHeightCenterMap); + + if (isInHorizontalCenter || isInVerticalCenter) { + object.setPositionByOrigin( + new fabric.Point( + isInVerticalCenter ? canvasWidthCenter : objectCenter.x, + isInHorizontalCenter ? canvasHeightCenter : objectCenter.y + ), + "center", + "center" + ); + } + }); + + canvas.on("before:render", function () { + if (canvas.contextTop) { + canvas.clearContext(canvas.contextTop); + } + }); + + canvas.on("after:render", function () { + if (isInVerticalCenter) { + showVerticalCenterLine(); + } + if (isInHorizontalCenter) { + showHorizontalCenterLine(); + } + }); + + canvas.on("mouse:up", function () { + // clear these values, to stop drawing guidelines once mouse is up + isInVerticalCenter = isInHorizontalCenter = null; + canvas.renderAll(); + }); +} diff --git a/src/component/Canvas/CanvasEditor/utils/imageHelper.js b/src/component/Canvas/CanvasEditor/utils/imageHelper.js new file mode 100644 index 00000000..509322fb --- /dev/null +++ b/src/component/Canvas/CanvasEditor/utils/imageHelper.js @@ -0,0 +1,1238 @@ +//import { fabric } from "fabric-with-all"; +import { LayerType, OperationType, createBitmapLayer } from "./layerHelper"; +// 导入新的复合命令 +import { CreateImageLayerCommand } from "../commands/LayerCommands"; +// 导入新的命令 +import { + ChangeFixedImageCommand, + AddImageToLayerCommand, +} from "../commands/LayerCommands"; + +/** + * 加载并处理图片 + * @param {string} imageSource - 图片URL或Base64字符串 + * @param {Object} options - 配置选项 + * @param {number} options.maxWidth - 最大宽度 + * @param {number} options.maxHeight - 最大高度 + * @param {boolean} options.centerOnCanvas - 是否居中图片 + * @param {function} options.onLoad - 加载完成回调 + * @returns {Promise} - 返回图片对象的Promise + */ +export function loadImage(imageSource, options = {}) { + return new Promise((resolve, reject) => { + fabric.Image.fromURL( + imageSource, + (fabricImage) => { + if (!fabricImage) { + reject(new Error("加载图片失败")); + return; + } + + // 计算缩放比例 + const imgWidth = fabricImage.width; + const imgHeight = fabricImage.height; + + // 应用缩放 + if (options.maxWidth && options.maxHeight) { + const scaleX = options.maxWidth / imgWidth; + const scaleY = options.maxHeight / imgHeight; + const scale = Math.min(scaleX, scaleY, 1); // 不超过原始大小 + + fabricImage.scale(scale); + } + + // 设置图片位置 - 默认居中 + if (options.centerOnCanvas !== false) { + fabricImage.set({ + left: (options.canvasWidth || 800) / 2, + top: (options.canvasHeight || 600) / 2, + originX: "center", + originY: "center", + selectable: true, + hasControls: true, + hasBorders: true, + }); + } + + // 执行加载完成回调 + if (typeof options.onLoad === "function") { + options.onLoad(fabricImage); + } + + resolve(fabricImage); + }, + { crossOrigin: "anonymous" } + ); + }); +} + +/** + * 创建图片图层 + * @param {Object} layerManager - 图层管理器 + * @param {Object} fabricImage - fabric图片对象 + * @param {Object} toolManager - 工具管理器 + * @param {string} layerName - 图层名称 (可选) + * @returns {Promise} 新图层ID + */ +export async function createImageLayer({ + layerManager, + fabricImage, + toolManager, + layerName = null, +} = {}) { + if (!layerManager || !fabricImage) { + console.error("图层管理器或图片对象无效"); + return null; + } + + try { + // 使用新的复合命令 + const createImageLayerCmd = new CreateImageLayerCommand({ + layerManager, + fabricImage, + toolManager, + layerName, + }); + + // 执行复合命令 + const newLayerId = await layerManager.commandManager.execute( + createImageLayerCmd + ); + + return newLayerId; + } catch (error) { + console.error("创建图片图层失败:", error); + throw error; + } +} + +/** + * 更改固定图层的图像 + * @param {Object} options - 配置选项 + * @param {Object} options.layerManager - 图层管理器 + * @param {string} options.fixedLayerId - 固定图层ID + * @param {Object} options.fabricImage - 新的图像对象 + * @returns {Promise} 是否成功更改 + */ +export async function changeFixedImage({ + layerManager, + fixedLayerId, + fabricImage, +} = {}) { + if (!layerManager || !fixedLayerId || !fabricImage) { + console.error("更改固定图层图像:参数无效"); + return false; + } + + try { + // 创建更改固定图层图像命令 + const changeFixedImageCmd = new ChangeFixedImageCommand({ + canvas: layerManager.canvas, + layers: layerManager.layers, + fixedLayerId, + newImage: fabricImage, + layerManager, + }); + + // 通过命令管理器执行 + const result = await layerManager.commandManager.execute( + changeFixedImageCmd + ); + + if (result) { + console.log(`✅ 成功更改固定图层 "${fixedLayerId}" 的图像`); + } + + return result; + } catch (error) { + console.error("更改固定图层图像失败:", error); + throw error; + } +} + +/** + * 添加图片到指定图层或创建新图层 + * @param {Object} options - 配置选项 + * @param {Object} options.layerManager - 图层管理器 + * @param {Object} options.toolManager - 工具管理器 + * @param {Object} options.fabricImage - 图像对象 + * @param {string} options.targetLayerId - 目标图层ID(可选,未指定则创建新图层) + * @param {string} options.layerName - 图层名称(用于新建图层) + * @returns {Promise} 图层ID + */ +export async function addImageToLayer({ + layerManager, + toolManager, + fabricImage, + targetLayerId = null, + layerName = null, +} = {}) { + if (!layerManager || !fabricImage) { + console.error("添加图片到图层:参数无效"); + return null; + } + + try { + // 创建添加图片到图层命令 + const addImageToLayerCmd = new AddImageToLayerCommand({ + canvas: layerManager.canvas, + layers: layerManager.layers, + layerManager, + toolManager, + fabricImage, + targetLayerId, + layerName, + activeLayerId: layerManager.activeLayerId, + }); + + // 通过命令管理器执行 + const resultLayerId = await layerManager.commandManager.execute( + addImageToLayerCmd + ); + + if (resultLayerId) { + if (targetLayerId) { + console.log(`✅ 成功添加图片到现有图层 "${targetLayerId}"`); + } else { + console.log(`✅ 成功创建新图层 "${resultLayerId}" 并添加图片`); + } + } + + return resultLayerId; + } catch (error) { + console.error("添加图片到图层失败:", error); + throw error; + } +} + +/** + * 从File对象加载图片并创建图层 + * @param {File} file - 文件对象 + * @param {Object} layerManager - 图层管理器 + * @param {Object} canvas - fabric.js画布实例 + * @param {Object} options - 配置选项 + * @returns {Promise} 新图层ID的Promise + */ +export function uploadImageAndCreateLayer( + { file, layerManager, canvas, toolManager }, + options = {} +) { + return new Promise((resolve, reject) => { + if (!file || !layerManager || !canvas) { + reject(new Error("参数无效")); + return; + } + + const reader = new FileReader(); + + reader.onload = async (e) => { + try { + // 查找背景图层以获取尺寸 + const bgLayer = layerManager.layers.value.find( + (layer) => layer.isBackground + ); + + // 设置最大宽高为背景图层的尺寸 + const maxWidth = bgLayer?.canvasWidth || canvas.width; + const maxHeight = bgLayer?.canvasHeight || canvas.height; + + // 加载并处理图片 + const fabricImage = await loadImage(e.target.result, { + maxWidth: maxWidth * 0.8, // 默认图片最大宽度为背景宽度的80% + maxHeight: maxHeight * 0.8, // 默认图片最大高度为背景高度的80% + canvasWidth: canvas.width, + canvasHeight: canvas.height, + ...options, + }); + + // 创建图片图层 + const layerId = await createImageLayer({ + layerManager, + fabricImage, + toolManager, + layerName: file.name, + }); + + resolve(layerId); + } catch (error) { + console.error("处理图片失败:", error); + reject(error); + } + }; + + reader.onerror = (error) => { + console.error("读取文件失败:", error); + reject(error); + }; + + reader.readAsDataURL(file); + }); +} + +/** + * 安全加载图片 + * 添加错误处理和重试机制 + * @param {string} imageSource - 图片URL或Base64字符串 + * @param {Object} options - 配置选项 + * @returns {Promise} - 返回图片对象的Promise + */ +export function safeLoadImage(imageSource, options = {}) { + return new Promise((resolve, reject) => { + let retries = options.retries || 1; + + const attemptLoad = (attempt = 0) => { + loadImage(imageSource, options) + .then(resolve) + .catch((error) => { + if (attempt < retries) { + console.warn( + `图片加载失败,正在重试 (${attempt + 1}/${retries})...` + ); + setTimeout(() => attemptLoad(attempt + 1), 500); + } else { + reject(error); + } + }); + }; + + attemptLoad(); + }); +} + +/** + * 从URL加载图片并更改固定图层 + * @param {Object} options - 配置选项 + * @param {string} options.imageUrl - 图片URL + * @param {Object} options.layerManager - 图层管理器 + * @param {string} options.fixedLayerId - 固定图层ID + * @param {Object} options.imageOptions - 图片加载选项 + * @returns {Promise} 是否成功 + */ +export function loadImageAndChangeFixedLayer({ + imageUrl, + layerManager, + fixedLayerId, + imageOptions = {}, +}) { + return new Promise((resolve, reject) => { + if (!imageUrl || !layerManager || !fixedLayerId) { + reject(new Error("参数无效")); + return; + } + + loadImage(imageUrl, imageOptions) + .then(async (fabricImage) => { + try { + const result = await changeFixedImage({ + layerManager, + fixedLayerId, + fabricImage, + }); + resolve(result); + } catch (error) { + console.error("更改固定图层失败:", error); + reject(error); + } + }) + .catch((error) => { + console.error("加载图片失败:", error); + reject(error); + }); + }); +} + +/** + * 从File对象更改固定图层图像 + * @param {Object} options - 配置选项 + * @param {File} options.file - 图像文件对象 + * @param {Object} options.layerManager - 图层管理器 + * @param {string} options.layerId - 固定图层ID + * @param {Object} options.imageOptions - 图片加载选项 + * @returns {Promise} 新图像对象ID的Promise + */ +export function uploadImageAndChangeFixedLayer({ + file, + layerManager, + layerId, + imageOptions = {}, +}) { + return new Promise((resolve, reject) => { + if (!file || !layerManager || !layerId) { + reject(new Error("参数无效:需要文件、图层管理器和图层ID")); + return; + } + + // 验证文件类型 + if (!file.type.startsWith("image/")) { + reject(new Error("无效的文件类型:必须是图像文件")); + return; + } + + const reader = new FileReader(); + + reader.onload = async (e) => { + try { + // 查找目标固定图层以获取尺寸信息 + const targetLayer = layerManager.layers.value.find( + (layer) => layer.id === layerId + ); + + if (!targetLayer) { + throw new Error(`找不到图层 ID: ${layerId}`); + } + + // 验证是否为固定图层 + if (!targetLayer.isFixed && !targetLayer.isBackground) { + throw new Error("只能更改固定图层或背景图层的图像"); + } + + // 查找背景图层以获取画布尺寸 + const bgLayer = layerManager.layers.value.find( + (layer) => layer.isBackground + ); + + const maxWidth = bgLayer?.canvasWidth || layerManager.canvas.width; + const maxHeight = bgLayer?.canvasHeight || layerManager.canvas.height; + + // 加载并处理图片 + const fabricImage = await loadImage(e.target.result, { + maxWidth: maxWidth, + maxHeight: maxHeight, + canvasWidth: layerManager.canvas.width, + canvasHeight: layerManager.canvas.height, + centerOnCanvas: true, + ...imageOptions, + }); + + // 创建更改固定图层图像命令 + const changeFixedImageCmd = new ChangeFixedImageCommand({ + canvas: layerManager.canvas, + layers: layerManager.layers, + layerId: layerId, + newImageFile: file, + layerManager: layerManager, + }); + + // 通过命令管理器执行 + const newImageId = await layerManager.commandManager.execute( + changeFixedImageCmd + ); + + if (newImageId) { + console.log( + `✅ 成功更改固定图层 "${targetLayer.name}" 的图像,新图像ID: ${newImageId}` + ); + resolve(newImageId); + } else { + throw new Error("更改固定图层图像失败"); + } + } catch (error) { + console.error("处理图片失败:", error); + reject(error); + } + }; + + reader.onerror = (error) => { + console.error("读取文件失败:", error); + reject(new Error("文件读取失败")); + }; + + reader.readAsDataURL(file); + }); +} + +/** + * 从File对象加载图片并添加到指定图层 (简化版) + * @param {Object} options - 配置选项 + * @param {File} options.file - 文件对象 + * @param {Object} options.layerManager - 图层管理器 + * @param {Object} options.toolManager - 工具管理器 + * @param {string} options.targetLayerId - 目标图层ID(可选) + * @param {Object} options.imageOptions - 图片加载选项 + * @returns {Promise} 返回 { layerId, imageId, wasLayerCreated } 的Promise + */ +export function uploadImageAndAddToLayer({ + file, + layerManager, + toolManager, + targetLayerId = null, + imageOptions = {}, +}) { + return new Promise((resolve, reject) => { + if (!file || !layerManager) { + reject(new Error("参数无效:需要文件和图层管理器")); + return; + } + + // 验证文件类型 + if (!file.type.startsWith("image/")) { + reject(new Error("无效的文件类型:必须是图像文件")); + return; + } + + // 创建添加图像到图层命令 + const addImageToLayerCmd = new AddImageToLayerCommand({ + canvas: layerManager.canvas, + layers: layerManager.layers, + activeLayerId: layerManager.activeLayerId, + imageFile: file, + targetLayerId: targetLayerId, + layerManager: layerManager, + toolManager: toolManager, + }); + + // 通过命令管理器执行 + layerManager.commandManager + .execute(addImageToLayerCmd) + .then((result) => { + if (result) { + console.log(`✅ 成功添加图像到图层,结果:`, result); + resolve(result); + } else { + throw new Error("添加图像到图层失败"); + } + }) + .catch((error) => { + console.error("添加图像到图层失败:", error); + reject(error); + }); + }); +} + +/** + * 从File对象加载图片并添加到指定图层 (简化版) + * @param {Object} options - 配置选项 + * @param {File} options.file - 文件对象 + * @param {Object} options.layerManager - 图层管理器 + * @param {Object} options.toolManager - 工具管理器 + * @param {string} options.targetLayerId - 目标图层ID(可选) + * @param {Object} options.imageOptions - 图片加载选项 + * @returns {Promise} 返回 { layerId, imageId, wasLayerCreated } 的Promise + */ +export function uploadImageAndAddToLayerSimple({ + file, + layerManager, + toolManager, + targetLayerId = null, + imageOptions = {}, +}) { + return new Promise((resolve, reject) => { + if (!file || !layerManager) { + reject(new Error("参数无效:需要文件和图层管理器")); + return; + } + + // 验证文件类型 + if (!file.type.startsWith("image/")) { + reject(new Error("无效的文件类型:必须是图像文件")); + return; + } + + // 创建添加图像到图层命令 + const addImageToLayerCmd = new AddImageToLayerCommand({ + canvas: layerManager.canvas, + layers: layerManager.layers, + activeLayerId: layerManager.activeLayerId, + imageFile: file, + targetLayerId: targetLayerId, + layerManager: layerManager, + toolManager: toolManager, + }); + + // 通过命令管理器执行 + layerManager.commandManager + .execute(addImageToLayerCmd) + .then((result) => { + if (result) { + console.log(`✅ 成功添加图像到图层,结果:`, result); + resolve(result); + } else { + throw new Error("添加图像到图层失败"); + } + }) + .catch((error) => { + console.error("添加图像到图层失败:", error); + reject(error); + }); + }); +} + +/** + * 批量上传图片并创建图层 + * @param {Object} options - 配置选项 + * @param {FileList|Array} options.files - 文件列表 + * @param {Object} options.layerManager - 图层管理器 + * @param {Object} options.canvas - fabric.js画布实例 + * @param {Object} options.toolManager - 工具管理器 + * @param {Object} options.imageOptions - 图片加载选项 + * @param {function} options.onProgress - 进度回调函数 + * @returns {Promise>} 新图层ID数组的Promise + */ +export async function batchUploadImagesAndCreateLayers({ + files, + layerManager, + canvas, + toolManager, + imageOptions = {}, + onProgress = null, +}) { + if (!files || files.length === 0) { + throw new Error("没有提供文件"); + } + + if (!layerManager || !canvas) { + throw new Error("缺少必要的参数:图层管理器或画布"); + } + + const results = []; + const errors = []; + + for (let i = 0; i < files.length; i++) { + const file = files[i]; + + try { + // 调用进度回调 + if (typeof onProgress === "function") { + onProgress({ + current: i + 1, + total: files.length, + fileName: file.name, + status: "processing", + }); + } + + // 验证文件类型 + if (!file.type.startsWith("image/")) { + console.warn(`跳过非图像文件: ${file.name}`); + continue; + } + + // 上传图片并创建图层 + const layerId = await uploadImageAndCreateLayer( + { file, layerManager, canvas, toolManager }, + imageOptions + ); + + results.push({ + fileName: file.name, + layerId: layerId, + success: true, + }); + + // 调用进度回调 + if (typeof onProgress === "function") { + onProgress({ + current: i + 1, + total: files.length, + fileName: file.name, + status: "success", + layerId: layerId, + }); + } + + console.log(`✅ 成功处理文件: ${file.name}, 图层ID: ${layerId}`); + } catch (error) { + console.error(`❌ 处理文件失败: ${file.name}`, error); + + errors.push({ + fileName: file.name, + error: error.message, + success: false, + }); + + // 调用进度回调 + if (typeof onProgress === "function") { + onProgress({ + current: i + 1, + total: files.length, + fileName: file.name, + status: "error", + error: error.message, + }); + } + } + } + + // 输出批量处理结果 + console.log(`📊 批量处理完成:`); + console.log(` ✅ 成功: ${results.length} 个文件`); + console.log(` ❌ 失败: ${errors.length} 个文件`); + + if (errors.length > 0) { + console.warn("失败的文件:", errors); + } + + return { + results: results, + errors: errors, + successCount: results.length, + errorCount: errors.length, + total: files.length, + }; +} + +/** + * 高级图像管理工具 + * 提供批量图像处理、缓存、预加载等高级功能 + */ +export class AdvancedImageManager { + constructor(canvasManager) { + this.canvasManager = canvasManager; + this.canvas = canvasManager.canvas; + this.layerManager = canvasManager.layerManager; + + // 图像缓存 + this.imageCache = new Map(); + this.preloadQueue = []; + this.maxCacheSize = 50; // 最大缓存数量 + + // 批量操作状态 + this.batchOperations = []; + this.isBatchMode = false; + + // 性能监控 + this.performanceMetrics = { + imageLoads: 0, + cacheHits: 0, + totalLoadTime: 0, + averageLoadTime: 0, + }; + } + + /** + * 预加载图像列表 + * @param {Array} imageUrls 要预加载的图像URL数组 + * @param {Object} options 选项 + */ + async preloadImages(imageUrls, options = {}) { + const { + concurrency = 3, // 并发数量 + timeout = 10000, + onProgress = null, + onError = null, + } = options; + + const loadPromises = []; + const results = []; + let completed = 0; + + // 分批并发加载 + for (let i = 0; i < imageUrls.length; i += concurrency) { + const batch = imageUrls.slice(i, i + concurrency); + + const batchPromises = batch.map(async (url, index) => { + try { + const startTime = performance.now(); + const image = await this.loadAndCacheImage(url, { timeout }); + const loadTime = performance.now() - startTime; + + // 更新性能指标 + this.updatePerformanceMetrics(loadTime); + + completed++; + onProgress?.({ completed, total: imageUrls.length, url }); + + return { success: true, url, image, loadTime }; + } catch (error) { + completed++; + onError?.({ url, error, completed, total: imageUrls.length }); + return { success: false, url, error: error.message }; + } + }); + + const batchResults = await Promise.all(batchPromises); + results.push(...batchResults); + } + + return { + results, + summary: { + total: imageUrls.length, + successful: results.filter((r) => r.success).length, + failed: results.filter((r) => !r.success).length, + cacheHitRate: + this.performanceMetrics.cacheHits / + this.performanceMetrics.imageLoads, + averageLoadTime: this.performanceMetrics.averageLoadTime, + }, + }; + } + + /** + * 加载并缓存图像 + * @param {String} url 图像URL + * @param {Object} options 选项 + */ + async loadAndCacheImage(url, options = {}) { + // 检查缓存 + if (this.imageCache.has(url)) { + this.performanceMetrics.cacheHits++; + return this.imageCache.get(url); + } + + // 加载新图像 + const image = await this.loadImage(url, options); + + // 添加到缓存 + this.addToCache(url, image); + + return image; + } + + /** + * 开始批量操作模式 + */ + startBatch() { + this.isBatchMode = true; + this.batchOperations = []; + } + + /** + * 批量更换多个固定图层的图像 + * @param {Array} operations 操作数组 [{layerType, imageUrl, options}, ...] + */ + async batchChangeFixedImages(operations) { + const operationResults = []; + + if (this.isBatchMode) { + // 如果在批量模式下,只收集操作 + this.batchOperations.push( + ...operations.map((op) => ({ + type: "changeFixed", + ...op, + })) + ); + return { queued: operations.length }; + } + + // 立即执行模式 + for (const operation of operations) { + try { + const result = await this.canvasManager.changeFixedImage( + operation.imageUrl, + { + targetLayerType: operation.layerType, + ...operation.options, + } + ); + operationResults.push({ success: true, ...result, operation }); + } catch (error) { + operationResults.push({ + success: false, + error: error.message, + operation, + }); + } + } + + return { + results: operationResults, + summary: this.getSummary(operationResults), + }; + } + + /** + * 批量向多个图层添加图像 + * @param {Array} operations 操作数组 [{layerId, imageUrl, position, options}, ...] + */ + async batchAddImagesToLayers(operations) { + const operationResults = []; + + if (this.isBatchMode) { + this.batchOperations.push( + ...operations.map((op) => ({ + type: "addToLayer", + ...op, + })) + ); + return { queued: operations.length }; + } + + // 并发执行以提高性能 + const concurrentOperations = operations.map(async (operation) => { + try { + const result = await this.canvasManager.addImageToLayer( + operation.imageUrl, + operation.layerId, + { + position: operation.position, + ...operation.options, + } + ); + return { success: true, ...result, operation }; + } catch (error) { + return { + success: false, + error: error.message, + operation, + }; + } + }); + + const concurrentResults = await Promise.all(concurrentOperations); + return { + results: concurrentResults, + summary: this.getSummary(concurrentResults), + }; + } + + /** + * 执行批量操作 + */ + async executeBatch() { + if (!this.isBatchMode || this.batchOperations.length === 0) { + return { message: "No batch operations to execute" }; + } + + const results = []; + const startTime = performance.now(); + + // 按类型分组操作以优化执行 + const groupedOps = this.groupOperationsByType(this.batchOperations); + + // 执行分组操作 + for (const [type, ops] of Object.entries(groupedOps)) { + try { + let typeResults; + + switch (type) { + case "changeFixed": + typeResults = await this.batchChangeFixedImages(ops); + break; + case "addToLayer": + typeResults = await this.batchAddImagesToLayers(ops); + break; + default: + console.warn(`Unknown operation type: ${type}`); + continue; + } + + if (typeResults.results) { + results.push(...typeResults.results); + } + } catch (error) { + console.error(`Batch execution failed for type ${type}:`, error); + // 继续执行其他类型的操作 + } + } + + const executionTime = performance.now() - startTime; + + // 清理批量状态 + this.isBatchMode = false; + this.batchOperations = []; + + return { + results, + summary: { + ...this.getSummary(results), + executionTime, + operationsPerSecond: results.length / (executionTime / 1000), + }, + }; + } + + /** + * 创建图像替换模板 + * @param {String} templateName 模板名称 + * @param {Array} operations 操作定义 + */ + createTemplate(templateName, operations) { + if (!this.templates) { + this.templates = new Map(); + } + + this.templates.set(templateName, { + name: templateName, + operations, + createdAt: new Date(), + usageCount: 0, + }); + } + + /** + * 应用模板 + * @param {String} templateName 模板名称 + * @param {Object} variables 变量替换映射 + */ + async applyTemplate(templateName, variables = {}) { + const template = this.templates?.get(templateName); + if (!template) { + throw new Error(`Template "${templateName}" not found`); + } + + // 替换模板中的变量 + const operations = this.replaceTemplateVariables( + template.operations, + variables + ); + + // 执行操作 + const result = await this.batchAddImagesToLayers(operations); + + // 更新使用计数 + template.usageCount++; + + return result; + } + + /** + * 智能图像优化 + * @param {String} imageUrl 图像URL + * @param {Object} targetSpecs 目标规格 {width, height, quality} + */ + async optimizeImage(imageUrl, targetSpecs) { + const image = await this.loadAndCacheImage(imageUrl); + + // 检查是否需要优化 + const currentSpecs = { + width: image.width, + height: image.height, + }; + + if (this.shouldOptimize(currentSpecs, targetSpecs)) { + return this.performImageOptimization(image, targetSpecs); + } + + return image; + } + + /** + * 清理缓存 + * @param {String} strategy 清理策略 'lru', 'size', 'all' + */ + clearCache(strategy = "lru") { + switch (strategy) { + case "all": + this.imageCache.clear(); + break; + case "size": + if (this.imageCache.size > this.maxCacheSize) { + const excess = this.imageCache.size - this.maxCacheSize; + const keys = Array.from(this.imageCache.keys()); + for (let i = 0; i < excess; i++) { + this.imageCache.delete(keys[i]); + } + } + break; + case "lru": + // 实现 LRU 清理逻辑 + this.implementLRUCleanup(); + break; + } + } + + // === 私有方法 === + + loadImage(url, options = {}) { + return new Promise((resolve, reject) => { + const timeout = setTimeout(() => { + reject(new Error(`Image load timeout: ${url}`)); + }, options.timeout || 10000); + + fabric.Image.fromURL( + url, + (img) => { + clearTimeout(timeout); + if (!img || !img.getElement()) { + reject(new Error("Invalid image")); + return; + } + resolve(img); + }, + { crossOrigin: "anonymous" } + ); + }); + } + + addToCache(url, image) { + // 检查缓存大小限制 + if (this.imageCache.size >= this.maxCacheSize) { + this.clearCache("size"); + } + + // 添加时间戳用于 LRU + const cacheEntry = { + image, + lastUsed: Date.now(), + usageCount: 1, + }; + + this.imageCache.set(url, cacheEntry); + } + + updatePerformanceMetrics(loadTime) { + this.performanceMetrics.imageLoads++; + this.performanceMetrics.totalLoadTime += loadTime; + this.performanceMetrics.averageLoadTime = + this.performanceMetrics.totalLoadTime / + this.performanceMetrics.imageLoads; + } + + getSummary(results) { + return { + total: results.length, + successful: results.filter((r) => r.success).length, + failed: results.filter((r) => !r.success).length, + successRate: results.filter((r) => r.success).length / results.length, + }; + } + + groupOperationsByType(operations) { + return operations.reduce((groups, op) => { + const type = op.type; + if (!groups[type]) groups[type] = []; + groups[type].push(op); + return groups; + }, {}); + } + + replaceTemplateVariables(operations, variables) { + return operations.map((op) => { + const newOp = { ...op }; + + // 替换字符串中的变量 {{variable}} + Object.keys(newOp).forEach((key) => { + if (typeof newOp[key] === "string") { + newOp[key] = newOp[key].replace( + /\{\{(\w+)\}\}/g, + (match, varName) => { + return variables[varName] || match; + } + ); + } + }); + + return newOp; + }); + } + + shouldOptimize(current, target) { + const sizeThreshold = 0.8; // 80% 的阈值 + return ( + current.width > target.width * (1 / sizeThreshold) || + current.height > target.height * (1 / sizeThreshold) + ); + } + + performImageOptimization(image, targetSpecs) { + // 实现图像优化逻辑 + // 这里可以集成图像压缩、尺寸调整等功能 + return image; // 简化实现 + } + + implementLRUCleanup() { + if (this.imageCache.size <= this.maxCacheSize) return; + + // 按最后使用时间排序,移除最久未使用的 + const entries = Array.from(this.imageCache.entries()).sort( + (a, b) => a[1].lastUsed - b[1].lastUsed + ); + + const toRemove = entries.slice(0, this.imageCache.size - this.maxCacheSize); + toRemove.forEach(([key]) => this.imageCache.delete(key)); + } + + // 获取性能报告 + getPerformanceReport() { + return { + ...this.performanceMetrics, + cacheSize: this.imageCache.size, + maxCacheSize: this.maxCacheSize, + cacheUtilization: this.imageCache.size / this.maxCacheSize, + recommendations: this.generatePerformanceRecommendations(), + }; + } + + generatePerformanceRecommendations() { + const recommendations = []; + + if ( + this.performanceMetrics.cacheHits / this.performanceMetrics.imageLoads < + 0.3 + ) { + recommendations.push("考虑增加缓存大小以提高缓存命中率"); + } + + if (this.performanceMetrics.averageLoadTime > 2000) { + recommendations.push("图像加载时间较长,考虑图像优化或CDN"); + } + + return recommendations; + } +} + +/** + * 图像工具集 + * 提供常用的图像处理和图层操作功能 + */ +export const ImageUtils = { + // 基础图像加载 + loadImage, + safeLoadImage, + + // 图层操作 + createImageLayer, + changeFixedImage, + addImageToLayer, + + // 文件上传处理 + uploadImageAndCreateLayer, + uploadImageAndChangeFixedLayer, + uploadImageAndAddToLayer, + uploadImageAndAddToLayerSimple, + batchUploadImagesAndCreateLayers, + + // URL图像处理 + loadImageAndChangeFixedLayer, + + /** + * 快速创建图像图层 (别名) + * @param {File} file - 图像文件 + * @param {Object} layerManager - 图层管理器 + * @param {Object} canvas - 画布实例 + * @param {Object} toolManager - 工具管理器 + * @returns {Promise} 图层ID + */ + quickCreateImageLayer: (file, layerManager, canvas, toolManager) => { + return uploadImageAndCreateLayer({ + file, + layerManager, + canvas, + toolManager, + }); + }, + + /** + * 快速更改固定图层图像 (别名) + * @param {File} file - 图像文件 + * @param {string} layerId - 图层ID + * @param {Object} layerManager - 图层管理器 + * @returns {Promise} 新图像ID + */ + quickChangeFixedImage: (file, layerId, layerManager) => { + return uploadImageAndChangeFixedLayer({ file, layerId, layerManager }); + }, + + /** + * 快速添加图像到图层 (别名) + * @param {File} file - 图像文件 + * @param {Object} layerManager - 图层管理器 + * @param {Object} toolManager - 工具管理器 + * @param {string} targetLayerId - 目标图层ID (可选) + * @returns {Promise} 执行结果 + */ + quickAddImageToLayer: ( + file, + layerManager, + toolManager, + targetLayerId = null + ) => { + return uploadImageAndAddToLayerSimple({ + file, + layerManager, + toolManager, + targetLayerId, + }); + }, +}; diff --git a/src/component/Canvas/CanvasEditor/utils/layerHelper.js b/src/component/Canvas/CanvasEditor/utils/layerHelper.js new file mode 100644 index 00000000..9d622c72 --- /dev/null +++ b/src/component/Canvas/CanvasEditor/utils/layerHelper.js @@ -0,0 +1,463 @@ +/** + * 图层类型枚举 + */ +export const LayerType = { + EMPTY: "empty", // 空图层 + BITMAP: "bitmap", // 位图图层 + VECTOR: "vector", // 矢量图层 + TEXT: "text", // 文字图层 + GROUP: "group", // 组图层 + ADJUSTMENT: "adjustment", // 调整图层 + SMART_OBJECT: "smartObject", // 智能对象 + SHAPE: "shape", // 形状图层 + VIDEO: "video", // 视频图层 (预留) + AUDIO: "audio", // 音频图层 (预留) + FIXED: "fixed", // 固定图层 - 位于背景图层之上,普通图层之下 +}; + +/** + * 画布操作模式枚举:draw(绘画)、select(选择)、pan(拖拽).... + */ +export const OperationType = { + // 编辑器模式 + DRAW: "draw", // 绘画模式 + ERASER: "eraser", // 橡皮擦模式 + SELECT: "select", // 选择模式 + PAN: "pan", // 拖拽模式 + EYEDROPPER: "eyedropper", // 吸色器模式 + + // 套索工具 + LASSO: "lasso", // 套索工具模式 - 自由套索模式 + LASSO_RECTANGLE: "lasso_rectangle", // 套索工具模式 - 矩形模式 + LASSO_ELLIPSE: "lasso_ellipse", // 套索工具模式 - 椭圆 + + // 创建临时选区工具模式 - 类似于临时图层 在这个区域的操作不会影响其他图层 + AREA_RECTANGLE: "area_rectangle", // 矩形选区模式 + + // 材质笔刷工具模式 + TEXTURE: "texture", // 选择材质笔刷工具模式 - // 选择材质笔刷后会切换到绘画模式 笔刷固定到材质笔刷 + + // 液化工具 + LIQUIFY: "liquify", // 液化工具模式 + + // 矢量工具 + // VECTOR: "vector", // 矢量工具模式 + // 矢量工具模式 - 自由绘制 + // VECTOR_FREE: "vector_free", + + TEXT: "text", // 文字工具模式 + + // 红绿图模式 + RED_GREEN: "red_green", // 红绿图模式 - 只有红色和绿色笔刷还有橡皮擦 不支持添加其他图片 特殊模式 + RED_BRUSH: "red_brush", // 红色笔刷 + GREEN_BRUSH: "green_brush", // 绿色笔刷 + + // SHAPE: "shape", // 形状模式 + // 可以根据需要添加更多工具 +}; + +// 所有操作模式类型列表 +export const OperationTypes = Object.values(OperationType); + +/** + * 混合模式枚举 + * 与 fabricjs 和 CSS3 的 globalCompositeOperation 对应 + */ +export const BlendMode = { + NORMAL: "source-over", // 正常模式 + MULTIPLY: "multiply", // 正片叠底 + SCREEN: "screen", // 滤色 + OVERLAY: "overlay", // 叠加 + DARKEN: "darken", // 变暗 + LIGHTEN: "lighten", // 变亮 + COLOR_DODGE: "color-dodge", // 颜色减淡 + COLOR_BURN: "color-burn", // 颜色加深 + HARD_LIGHT: "hard-light", // 强光 + SOFT_LIGHT: "soft-light", // 柔光 + DIFFERENCE: "difference", // 差值 + EXCLUSION: "exclusion", // 排除 + HUE: "hue", // 色相 + SATURATION: "saturation", // 饱和度 + COLOR: "color", // 颜色 + LUMINOSITY: "luminosity", // 明度 + DESTINATION_IN: "destination-in", // 目标内 + DESTINATION_OUT: "destination-out", // 目标外 +}; + +/** + * 判断图层是否为组图层 + * @param {Object} layer 要检查的图层 + * @returns {boolean} 是否为组图层 + */ +export function isGroupLayer(layer) { + if (!layer) return false; + return ( + layer.type === LayerType.GROUP || + (Array.isArray(layer.children) && layer.children.length > 0) + ); +} + +/** + * 从fabric对象创建图层 + * @param {Object} fabricObject fabric对象 + * @param {String} layerType 图层类型 + * @param {Object} options 其他选项 + * @returns {Object} 创建的图层对象 + */ +export function createLayerFromFabricObject( + fabricObject, + layerType = "bitmap", + options = {} +) { + if (!fabricObject) return null; + + // 确定图层类型 + let type = layerType; + if (fabricObject.type === "textbox" || fabricObject.type === "text") { + type = LayerType.TEXT; + } else if ( + fabricObject.type === "rect" || + fabricObject.type === "circle" || + fabricObject.type === "polygon" || + fabricObject.type === "polyline" + ) { + type = LayerType.SHAPE; + } else if (fabricObject.type === "path" || fabricObject.type === "line") { + type = LayerType.VECTOR; + } else if (fabricObject.type === "image") { + type = LayerType.BITMAP; + } + + // 创建基础图层 + let layer = createLayer({ + ...options, + type: type, + name: + options.name || + `${ + fabricObject.type.charAt(0).toUpperCase() + fabricObject.type.slice(1) + } 图层`, + parentId: options.parentId || null, + }); + + // 添加对象到图层 + if (Array.isArray(layer.fabricObjects)) { + layer.fabricObjects.push(fabricObject); + } else { + layer.fabricObjects = [fabricObject]; + } + + // 如果对象有自己的ID,将其与图层关联 + if (fabricObject.id) { + fabricObject.layerId = layer.id; + fabricObject.layerName = layer.name; + } + + return layer; +} + +/** + * 创建标准图层对象 + * @param {Object} options 图层选项 + * @returns {Object} 图层对象 + */ +export function createLayer(options = {}) { + const id = + options.id || `layer_${Date.now()}_${Math.floor(Math.random() * 1000)}`; + return { + id: id, + // 图层基本属性 + name: options.name || `图层 ${id.substring(id.lastIndexOf("_") + 1)}`, + type: options.type || LayerType.EMPTY, + visible: options.visible !== undefined ? options.visible : true, + locked: options.locked !== undefined ? options.locked : false, + opacity: options.opacity !== undefined ? options.opacity : 1.0, + blendMode: options.blendMode || BlendMode.NORMAL, + + // 确保不是背景图层 + isBackground: false, + + // Fabric.js 对象列表 + fabricObjects: options.fabricObjects || [], + + // 嵌套结构 - 适用于组图层 + children: options.children || [], + + // 剪切蒙版 + clippingMask: options.clippingMask || null, + + // 位置和大小信息(可选) + bounds: options.bounds || null, + + // 图层特定属性 + layerProperties: options.layerProperties || {}, + + // 元数据 - 可用于存储任意数据 + metadata: options.metadata || {}, + }; +} + +/** + * 创建背景图层 + * @param {Object} options 背景图层选项 + * @returns {Object} 背景图层对象 + */ +export function createBackgroundLayer(options = {}) { + const id = + options.id || `bg_layer_${Date.now()}_${Math.floor(Math.random() * 1000)}`; + return { + id: id, + // 图层基本属性 + name: options.name || "背景", + type: LayerType.BITMAP, + visible: true, + locked: true, // 背景图层默认锁定 + opacity: 1.0, // 背景图层始终不透明 + blendMode: BlendMode.NORMAL, // 背景图层始终使用正常混合模式 + + // 标记为背景图层 + isBackground: true, + + // 画布尺寸 + canvasWidth: options.canvasWidth || 800, + canvasHeight: options.canvasHeight || 600, + backgroundColor: options.backgroundColor || "#ffffff", + + // Fabric.js 背景对象 (单个矩形对象) + fabricObject: null, // 创建后设置 + // Fabric.js 对象列表 + fabricObjects: [], // 创建后设置 + + // 无子图层 + children: [], + + // 元数据 - 可用于存储任意数据 + metadata: options.metadata || {}, + }; +} + +/** + * 创建位图图层 + * @param {Object} options 图层选项 + * @returns {Object} 位图图层对象 + */ +export function createBitmapLayer(options = {}) { + const baseLayer = createLayer({ + ...options, + type: LayerType.BITMAP, + }); + + // 添加位图特定属性 + baseLayer.layerProperties = { + ...baseLayer.layerProperties, + filters: options.filters || [], // 滤镜数组 + imageUrl: options.imageUrl || null, // 图片URL + imageElement: options.imageElement || null, // 图片元素 + }; + + return baseLayer; +} + +/** + * 创建文本图层 + * @param {Object} options 图层选项 + * @returns {Object} 文本图层对象 + */ +export function createTextLayer(options = {}) { + const baseLayer = createLayer({ + ...options, + type: LayerType.TEXT, + }); + + // 添加文字特定属性 + baseLayer.layerProperties = { + ...baseLayer.layerProperties, + text: options.text || "新文本", + fontFamily: options.fontFamily || "Arial", + fontSize: options.fontSize || 24, + fontWeight: options.fontWeight || "normal", + fontStyle: options.fontStyle || "normal", + textAlign: options.textAlign || "left", + underline: options.underline || false, + overline: options.overline || false, + linethrough: options.linethrough || false, + textBackgroundColor: options.textBackgroundColor || "transparent", + lineHeight: options.lineHeight || 1.16, + charSpacing: options.charSpacing || 0, + }; + + return baseLayer; +} + +/** + * 创建矢量图层 + * @param {Object} options 图层选项 + * @returns {Object} 矢量图层对象 + */ +export function createVectorLayer(options = {}) { + const baseLayer = createLayer({ + ...options, + type: LayerType.VECTOR, + }); + + // 添加矢量特定属性 + baseLayer.layerProperties = { + ...baseLayer.layerProperties, + vectorType: options.vectorType || "path", // path, polygon, polyline等 + strokeWidth: options.strokeWidth !== undefined ? options.strokeWidth : 1, + strokeColor: options.strokeColor || "#000000", + fillColor: options.fillColor || "transparent", + fillRule: options.fillRule || "nonzero", + strokeLineCap: options.strokeLineCap || "butt", + strokeLineJoin: options.strokeLineJoin || "miter", + strokeDashArray: options.strokeDashArray || null, + strokeDashOffset: options.strokeDashOffset || 0, + }; + + return baseLayer; +} + +/** + * 创建形状图层 + * @param {Object} options 图层选项 + * @returns {Object} 形状图层对象 + */ +export function createShapeLayer(options = {}) { + const baseLayer = createLayer({ + ...options, + type: LayerType.SHAPE, + }); + + // 添加形状特定属性 + baseLayer.layerProperties = { + ...baseLayer.layerProperties, + shapeType: options.shapeType || "rect", // rect, circle, ellipse等 + strokeWidth: options.strokeWidth !== undefined ? options.strokeWidth : 1, + strokeColor: options.strokeColor || "#000000", + fillColor: options.fillColor || "#ffffff", + rx: options.rx || 0, // 矩形圆角 + ry: options.ry || 0, // 矩形圆角 + }; + + return baseLayer; +} + +/** + * 创建调整图层 + * @param {Object} options 图层选项 + * @returns {Object} 调整图层对象 + */ +export function createAdjustmentLayer(options = {}) { + const baseLayer = createLayer({ + ...options, + type: LayerType.ADJUSTMENT, + }); + + // 添加调整图层特定属性 + baseLayer.layerProperties = { + ...baseLayer.layerProperties, + adjustmentType: options.adjustmentType || "brightness", // brightness, contrast, hue, saturation等 + value: options.value !== undefined ? options.value : 0, + affectedLayerIds: options.affectedLayerIds || [], // 受影响的图层ID列表 + }; + + return baseLayer; +} + +/** + * 创建智能对象图层 + * @param {Object} options 图层选项 + * @returns {Object} 智能对象图层 + */ +export function createSmartObjectLayer(options = {}) { + const baseLayer = createLayer({ + ...options, + type: LayerType.SMART_OBJECT, + }); + + // 添加智能对象特定属性 + baseLayer.layerProperties = { + ...baseLayer.layerProperties, + sourceType: options.sourceType || "image", // image, vector, embedded等 + sourceUrl: options.sourceUrl || null, + sourceData: options.sourceData || null, + originalWidth: options.originalWidth || 0, + originalHeight: options.originalHeight || 0, + embedded: options.embedded !== undefined ? options.embedded : true, + }; + + return baseLayer; +} + +/** + * 创建固定图层 - 位于背景图层之上,普通图层之下 + * @param {Object} options 固定图层选项 + * @returns {Object} 固定图层对象 + */ +export function createFixedLayer(options = {}) { + const id = + options.id || + `fixed_layer_${Date.now()}_${Math.floor(Math.random() * 1000)}`; + return { + id: id, + // 图层基本属性 + name: options.name || "固定图层", + type: LayerType.FIXED, + visible: true, // 固定图层始终可见 + locked: true, // 固定图层默认锁定 + opacity: options.opacity !== undefined ? options.opacity : 1.0, + blendMode: options.blendMode || BlendMode.NORMAL, + + // 标记为固定图层 + isFixed: true, + isBackground: false, + + // Fabric.js 对象列表 + fabricObjects: options.fabricObjects || [], + + // 无子图层 + children: [], + + // 元数据 - 可用于存储任意数据 + metadata: options.metadata || {}, + }; +} + +/** + * 深拷贝图层对象 + * @param {Object} layer 要拷贝的图层 + * @returns {Object} 拷贝后的图层 + */ +export function cloneLayer(layer) { + if (!layer) return null; + + // 基本属性深拷贝 + const clonedLayer = { + ...JSON.parse(JSON.stringify(layer)), // 深拷贝基本属性 + fabricObjects: [], // 重置,后面处理 + }; + + // 复制 fabric 对象 (如果存在) + if (Array.isArray(layer.fabricObjects)) { + clonedLayer.fabricObjects = layer.fabricObjects.map((obj) => { + return obj && typeof obj.clone === "function" + ? obj.clone() + : JSON.parse(JSON.stringify(obj)); + }); + } + + // 复制背景对象 (如果存在) + if (layer.isBackground && layer.fabricObject) { + clonedLayer.fabricObject = + typeof layer.fabricObject.clone === "function" + ? layer.fabricObject.clone() + : JSON.parse(JSON.stringify(layer.fabricObject)); + } + + // 递归复制子图层 + if (Array.isArray(layer.children) && layer.children.length > 0) { + clonedLayer.children = layer.children.map((child) => cloneLayer(child)); + } + + return clonedLayer; +} diff --git a/src/component/Canvas/SvgIcon/index.vue b/src/component/Canvas/SvgIcon/index.vue new file mode 100644 index 00000000..009feea6 --- /dev/null +++ b/src/component/Canvas/SvgIcon/index.vue @@ -0,0 +1,49 @@ + + + + + diff --git a/src/component/Canvas/index.vue b/src/component/Canvas/index.vue index 7cdf4af3..f3609e0e 100644 --- a/src/component/Canvas/index.vue +++ b/src/component/Canvas/index.vue @@ -1,360 +1,83 @@ - - \ No newline at end of file + +.canvas-wrapper { + flex: 1; + position: relative; +} + +.canvas-wrapper-btns { + position: fixed; + top: 0; + right: 150px; + z-index: 1000; + display: flex; + gap: 20px; + & > div { + cursor: pointer; + padding: 10px 20px; + background-color: #f0f0f0; + border-radius: 5px; + transition: background-color 0.3s; + } +} + diff --git a/src/component/Canvas/index1.vue b/src/component/Canvas/index1.vue new file mode 100644 index 00000000..7cdf4af3 --- /dev/null +++ b/src/component/Canvas/index1.vue @@ -0,0 +1,360 @@ + + + \ No newline at end of file diff --git a/src/component/HomePage/Generate.vue b/src/component/HomePage/Generate.vue index e781ffa3..468e961f 100644 --- a/src/component/HomePage/Generate.vue +++ b/src/component/HomePage/Generate.vue @@ -141,18 +141,6 @@
-
{ @@ -301,6 +290,10 @@ export default defineComponent({ title:'Generate image in high quality low speed', label:'High', value:'high', + },{ + title:'Generate using Wanxiang', + label:'WX', + value:'wx', }, ], speedState:false, @@ -485,6 +478,12 @@ export default defineComponent({ if(this.type_.type2 == 'Sketchboard'){ maxImg = 20 } + let parent:any = this.$parent + if(parent.isUseGenerate){ + // parent.useGenerate.designType = 'collection' + this.$emit('setLibrary',data) + return + } data.jsContent1 = this.t('uploadFile.jsContent1',{maxImg:maxImg}) this.store.commit("addGenerateMaterialFils", data); // console.log(this.fileList); @@ -609,7 +608,7 @@ export default defineComponent({ seed:this.searchPictureSeed, userId:this?.userDetail?.userId, timeZone:Intl.DateTimeFormat().resolvedOptions().timeZone, - version:this.speedData.value,//为1就是Print + modelName:this.speedData.value,//为1就是Print isTestUser:this.driver__.driver?false:this.isTest, gender:this.workspace.sex, sloganBase64:base64, diff --git a/src/component/HomePage/Material.vue b/src/component/HomePage/Material.vue index d52d3a7a..148c482b 100644 --- a/src/component/HomePage/Material.vue +++ b/src/component/HomePage/Material.vue @@ -319,7 +319,7 @@ export default defineComponent({ this.setUseGenerate([]) let obj = { imgId : imgData.id, - imgUr: imgData.imgUrl, + imgUrl: imgData.imgUrl, level2Type:imgData.level2Type, categoryValue:imgData.categoryValue, category:imgData.category, diff --git a/src/component/HomePage/NewCollectionReview.vue b/src/component/HomePage/NewCollectionReview.vue index 62ae162d..0a78bc7a 100644 --- a/src/component/HomePage/NewCollectionReview.vue +++ b/src/component/HomePage/NewCollectionReview.vue @@ -50,14 +50,14 @@
Colorboard
-
-
-
-
-
{{color.tcx}}
-
{{color.name}}
-
-
+
+
+
+
+
{{color.tcx}}
+
{{color.name}}
+
+
@@ -311,16 +311,17 @@ export default defineComponent({ } .color_item{ display: inline-block; - vertical-align: top; + // vertical-align: top; // padding: 0 0.5rem; // margin-right: 1.6rem; margin-right: 4%; margin-bottom: 0.5rem; width: 21%; - &:nth-child(5n){ - margin-right: 0; - } + &:nth-child(4n+1){ + margin-right: 0; + } } + .color_content{ height:6.2rem; diff --git a/src/component/HomePage/PrintboardUpload.vue b/src/component/HomePage/PrintboardUpload.vue index e89fb9cd..14779e9d 100644 --- a/src/component/HomePage/PrintboardUpload.vue +++ b/src/component/HomePage/PrintboardUpload.vue @@ -1,5 +1,5 @@ - - \ No newline at end of file diff --git a/src/component/home/tools/cloudUploading/index.vue b/src/component/home/tools/cloudUploading/index.vue deleted file mode 100644 index a0c2296e..00000000 --- a/src/component/home/tools/cloudUploading/index.vue +++ /dev/null @@ -1,343 +0,0 @@ - - - \ No newline at end of file diff --git a/src/component/home/tools/deReconstruction/index.vue b/src/component/home/tools/deReconstruction/index.vue index 917b59d3..e3beea81 100644 --- a/src/component/home/tools/deReconstruction/index.vue +++ b/src/component/home/tools/deReconstruction/index.vue @@ -22,7 +22,8 @@
- + +
@@ -49,9 +50,11 @@ import { useI18n } from 'vue-i18n' import selectList from '@/component/DetailCopy/detailLeft/module/selectList.vue' import sketchCategory from "@/component/HomePage/sketchCategory.vue"; import canvasBox from "./canvas/index.vue"; +import canvasUpload from "@/component/Canvas/CanvasEditor/index.vue"; +// import defaultModel from '@/assets/images/homePage/defaultModel.png' export default defineComponent({ components:{ - selectList,sketchCategory,canvasBox + selectList,sketchCategory,canvasBox,canvasUpload }, props:{ }, @@ -64,6 +67,12 @@ export default defineComponent({ }), segmentationType:'product', generateImg:computed(()=>store.state.HomeStoreModule.deReconstruction) as any, + ceditorConfig:{ + width: 800, + height: 600, + backgroundColor: "#f8f8f8", + }, + defaultModel:'', }) const setIsShowMark:any = inject('setIsShowMark') const dataDom = reactive({ @@ -219,6 +228,7 @@ export default defineComponent({ } &.canvas{ flex: 1; + position: relative; } &.finished{ width: 58rem; diff --git a/src/component/home/tools/patternMaking3D/index.vue b/src/component/home/tools/patternMaking3D/index.vue index 20252101..f7bebbce 100644 --- a/src/component/home/tools/patternMaking3D/index.vue +++ b/src/component/home/tools/patternMaking3D/index.vue @@ -24,7 +24,6 @@
-
@@ -38,9 +37,7 @@
-
- -
+
@@ -57,7 +54,7 @@
- + @@ -81,17 +78,19 @@ + diff --git a/src/component/home/tools/patternMaking3D/three.vue b/src/component/home/tools/patternMaking3D/three.vue index 0b61c72f..623ab769 100644 --- a/src/component/home/tools/patternMaking3D/three.vue +++ b/src/component/home/tools/patternMaking3D/three.vue @@ -63,7 +63,7 @@ export default defineComponent({ }, props:{ }, - emits:[], + emits:['saveProject'], setup(props,{emit}) { const store = useStore(); const data = reactive({ @@ -265,7 +265,7 @@ export default defineComponent({ texture.anisotropy = 32; // 提高纹理清晰度 data.group?.traverse((child:any) => { if (child.isMesh) { - console.log(child.name) + // console.log(child.name) // 5. 创建新材质(根据需求选择材质类型) const textureWidth = texture.image.width; const textureHeight = texture.image.height; @@ -420,7 +420,7 @@ export default defineComponent({ const modeUrl = await getModelUrl(value) await setModel(modeUrl) let patternMaking3D = store.state.HomeStoreModule.patternMaking3D - if(patternMaking3D.printMinioUrl)await addMaterial({url:patternMaking3D.printMinioUrl}) + if(patternMaking3D.url)await addMaterial({url:patternMaking3D.url}) data.load.state = false } const changeRepeat = (e:any)=>{ @@ -437,6 +437,7 @@ export default defineComponent({ y:data.repeat.y, } store.commit('setPatternMaking3D',value) + emit('saveProject') },1000) } const setLock = ()=>{ diff --git a/src/component/home/tools/poseTransfer/index.vue b/src/component/home/tools/poseTransfer/index.vue index 643969d3..9a7ab3b9 100644 --- a/src/component/home/tools/poseTransfer/index.vue +++ b/src/component/home/tools/poseTransfer/index.vue @@ -64,8 +64,15 @@
-
+ +
+ +
+
+
{{ item.label }}
+
@@ -134,11 +141,11 @@ export default defineComponent({ selectImg:{}, token:getCookie('token'), upload:{ - projectId:store.state.Workspace.probjects.id + projectId:computed(()=>store.state.Workspace.probjects.id) }, waitList:[], likeList:computed(()=>store.state.HomeStoreModule.poseTransfer), - noLikeList:[ { "id": 128, "taskId": "df9cd154-6cf9-488a-8e64-426ef4a27e13-83", "productImage": null, "gifUrl": "https://www.minio-api.aida.com.hk/aida-users/83/pose_transform_gif/df9cd154-6cf9-488a-8e64-426ef4a27e13-83.gif?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=admin%2F20250530%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20250530T015548Z&X-Amz-Expires=86400&X-Amz-SignedHeaders=host&X-Amz-Signature=2a43ad4a389e8366207b6500506c17140e0387924a2adcb35188f2e59777e422", "videoUrl": "None", "firstFrameUrl": "https://www.minio-api.aida.com.hk/aida-users/83/pose_transform_first_img/df9cd154-6cf9-488a-8e64-426ef4a27e13-83.png?response-content-type=image%2Fpng&response-content-disposition=inline&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=admin%2F20250530%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20250530T015548Z&X-Amz-Expires=86400&X-Amz-SignedHeaders=host&X-Amz-Signature=e202d13874f0a28105602a268c802d4c99d2fbecd06a9460cc07e7c7ecbd0559", "isLiked": 0, "status": "Success", "collectionType": null, "url": "https://www.minio-api.aida.com.hk/aida-users/83/pose_transform_first_img/df9cd154-6cf9-488a-8e64-426ef4a27e13-83.png?response-content-type=image%2Fpng&response-content-disposition=inline&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=admin%2F20250530%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20250530T015548Z&X-Amz-Expires=86400&X-Amz-SignedHeaders=host&X-Amz-Signature=e202d13874f0a28105602a268c802d4c99d2fbecd06a9460cc07e7c7ecbd0559" } ], + noLikeList:[ ], isGenerate:false,//判断是否正在进行generate remGenerate:false, removeGenerate:false, @@ -146,6 +153,25 @@ export default defineComponent({ poseList:[], selectPose:null as any, }) + let speed = reactive({ + speedList:[ + { + title:'Generate high-quality content', + label:'High', + value:'', + },{ + title:'Generate using Wanxiang', + label:'WX', + value:'wx', + }, + ], + speedState:false, + speedData:{ + title:'Generate high-quality content', + label:'High', + value:'', + }, + }) const setIsShowMark:any = inject('setIsShowMark') const createProbject:any = inject('createProbject') const dataDom = reactive({ @@ -215,8 +241,9 @@ export default defineComponent({ poseId:data.selectPose, projectId:store.state.Workspace.probjects.id, productImage:data.selectImg.minioUrl, + modelName:speed.speedData.value, } - Https.axiosGet(Https.httpUrls.poseTransform,{params:value}).then((rv)=>{ + Https.axiosPost(Https.httpUrls.poseTransform,value).then((rv)=>{ data.noLikeList.unshift({taskId:rv}) setGenerate(rv) }).catch((res:any)=>{ @@ -233,7 +260,7 @@ export default defineComponent({ if(!data.isGenerate || !data.remGenerate)return if(!state)return state = false - Https.axiosGet(Https.httpUrls.poseTransformResult,{params:{taskId:list}}).then( + Https.axiosPost(Https.httpUrls.poseTransformResult,{taskId:list}).then( (rv) => { rv=[rv] state = true @@ -273,7 +300,7 @@ export default defineComponent({ data.isGenerate = false data.remGenerate = false }); - },1000) + },20000) } const removeGenerate = ()=>{ //取消操作 @@ -377,11 +404,13 @@ export default defineComponent({ value = { likeOrDislike:'like', transformedId:item.id, + projectId:store.state.Workspace.probjects.id, } }else{ value = { likeOrDislike:'dislike', transformedId:item.id, + projectId:store.state.Workspace.probjects.id, } } Https.axiosPost(Https.httpUrls.poselikeOrDisike, {},{params:value}).then( @@ -416,7 +445,19 @@ export default defineComponent({ onMounted(()=>{ // showViewVideo({url:'https://www.minio.aida.com.hk:12025/api/v1/download-shared-object/aHR0cHM6Ly93d3cubWluaW8uYWlkYS5jb20uaGs6MTIwMjQvYWlkYS11c2Vycy84OS9wb3NlX3RyYW5zZm9ybV92aWRlby8xMjMtODkubXA0P1gtQW16LUFsZ29yaXRobT1BV1M0LUhNQUMtU0hBMjU2JlgtQW16LUNyZWRlbnRpYWw9N0tOVDdNWlNLWkRXM1RVOEJZVlklMkYyMDI1MDQwOCUyRnVzLWVhc3QtMSUyRnMzJTJGYXdzNF9yZXF1ZXN0JlgtQW16LURhdGU9MjAyNTA0MDhUMDUxOTM1WiZYLUFtei1FeHBpcmVzPTQzMTk5JlgtQW16LVNlY3VyaXR5LVRva2VuPWV5SmhiR2NpT2lKSVV6VXhNaUlzSW5SNWNDSTZJa3BYVkNKOS5leUpoWTJObGMzTkxaWGtpT2lJM1MwNVVOMDFhVTB0YVJGY3pWRlU0UWxsV1dTSXNJbVY0Y0NJNk1UYzBOREV4T0RneE9Td2ljR0Z5Wlc1MElqb2lZV1J0YVc0aWZRLmY0Z3RoTU1BeC1GUnM3eGhWNFdjTUFCUW5lU19BVkIxUDlYbnJQbEFNWUFsVnJwY3RpYXgtU2cyY2FkZHZ0a0VCOU1NbWxGeUlIbU90aGhUWDlqN2lnJlgtQW16LVNpZ25lZEhlYWRlcnM9aG9zdCZ2ZXJzaW9uSWQ9bnVsbCZYLUFtei1TaWduYXR1cmU9Yjg5YmQ4ZDg5M2I4ZjBjYmYxZDI3NDFjZmY0NGRiZGNmYWM2NmU0ZGM2OGIwYzQzZDA2OGI4YjYzZjE5YjhhOA'}) }) + const openSpeed = ()=>{ + speed.speedState = !speed.speedState + if(speed.speedState){ + document.addEventListener('click',openSpeed) + }else{ + document.removeEventListener('click',openSpeed) + } + } + const setSpeed = (item:any)=>{ + speed.speedData = item + } return{ + ...toRefs(speed), ...toRefs(dataDom), ...toRefs(data), openSetData, @@ -433,6 +474,8 @@ export default defineComponent({ likeSetBtn, noLikeSetBtn, selectPose, + openSpeed, + setSpeed, } }, directives:{ @@ -478,6 +521,44 @@ export default defineComponent({ > .started_btn{ font-weight: 300; } + .generage_btn{ + width: 10rem; + position: relative; + } + .icon-xiala{ + margin-left: 1rem; + transition: all .3s; + cursor: pointer; + &.active{ + transform: rotate(180deg); + } + } + .content{ + position: absolute; + top: 100%; + width: 100%; + left: 0rem; + text-align: center; + border-radius: calc(1rem* 1.2); + overflow: hidden; + z-index: 3; + margin-top: .2rem; + >div{ + background: #cccccc; + line-height: 2; + font-size: 1.8rem; + cursor: pointer; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + &.active{ + background-color: #616161; + } + } + >div:hover{ + background: #999999; + } + } } } > .configuratioBox > .configuratio{ diff --git a/src/component/toolsPage/index.vue b/src/component/toolsPage/index.vue index 08e5ba21..39894012 100644 --- a/src/component/toolsPage/index.vue +++ b/src/component/toolsPage/index.vue @@ -16,7 +16,7 @@ - +
@@ -49,6 +49,19 @@ export default defineComponent({ const data = reactive({ openType:'' as any, isShowMark:false, + toolsList:{ + toProduct:'TO_PRODUCT_IMAGE', + relight:'RELIGHT', + poseTransfer:'POSE_TRANSFER', + deReconstruction:'DE_RECONSTRUCTION', + patternMaking3D:'THREE_D_PLATE_MAKING', + canvasUpload:'CANVAS', + } as any, + ceditorConfig:{ + width: 800, + height: 600, + backgroundColor: "#f8f8f8", + } }) const dataDom:any = reactive({ toProduct:null as any, @@ -69,7 +82,6 @@ export default defineComponent({ let settingGetHistory:any = inject('settingGetHistory') const open = (str:any)=>{ nextTick(()=>{ - console.log(dataDom[str],str,123123) if(dataDom[str]?.openSetData){ dataDom[str].openSetData() } @@ -90,11 +102,12 @@ export default defineComponent({ ); const createProbject = async ()=>{ return await new Promise((resolve, reject) => { - if(!route.query.tools)return + let tools = route.query.tools as any + if(!tools){reject(false)} let toolsData:any = openTypeList(t).tools.list.find((item:any)=>item.value == route.query.tools) let value = { name:toolsData.label, - process:route.query.tools, + process:data.toolsList[tools], } Https.axiosPost(Https.httpUrls.projectSaveOrUpdate,value).then((rv)=>{ if(rv){ diff --git a/src/lang/cn.ts b/src/lang/cn.ts index 86a22e6a..f03064da 100644 --- a/src/lang/cn.ts +++ b/src/lang/cn.ts @@ -615,7 +615,7 @@ export default { scaleImage:{ overlayOrNot:'是否覆盖当前图片', submitCanvas:'画布内容没有储存,是否继续', - cover:'是否覆盖编辑的内容', + cover:'是否要把生成的内容存为新的设计', }, account:{ personCentered:'个人中心', diff --git a/src/lang/en.ts b/src/lang/en.ts index bd0c49fd..d328fb80 100644 --- a/src/lang/en.ts +++ b/src/lang/en.ts @@ -615,7 +615,7 @@ export default { scaleImage:{ overlayOrNot:'Whether to overwrite the current picture', submitCanvas:'Canvas content is not saved, whether to continue', - cover:'Do you want to overwrite the edited content?', + cover:'Save the generated content as a new design?', }, account:{ personCentered:'Account', diff --git a/src/store/homeStore/homeStore.ts b/src/store/homeStore/homeStore.ts index 7b09de2c..338d6376 100644 --- a/src/store/homeStore/homeStore.ts +++ b/src/store/homeStore/homeStore.ts @@ -43,9 +43,9 @@ const HomeStoreModule : Module = { categoryShow:false }], patternMaking3D:{ - threeDsimpleId:null, - collectionElementId:null, + threeDSimpleId:null, printMinioUrl:null, + url:'', x:null, y:null, }, @@ -97,7 +97,6 @@ const HomeStoreModule : Module = { setPatternMaking3D(state,data){ for (const key in data) { if(data[key] == undefined)continue - console.log(data[key]) if(key == 'x' || key == 'y'){ state.patternMaking3D[key] = Number(data[key].toFixed(3)) }else{ @@ -170,9 +169,9 @@ const HomeStoreModule : Module = { state.poseTransfer = [] state.deReconstruction = null state.patternMaking3D = { - threeDsimpleId:null, - collectionElementId:null, + threeDSimpleId:null, printMinioUrl:null, + url:null, x:null, y:null, } diff --git a/src/store/uploadFile/uploadFile.ts b/src/store/uploadFile/uploadFile.ts index bdebd012..8bdb1bb0 100644 --- a/src/store/uploadFile/uploadFile.ts +++ b/src/store/uploadFile/uploadFile.ts @@ -351,12 +351,10 @@ const UploadFilesModule : Module = { state.allBoardData.moodTemplateId = state.moodTemplateId if(state.moodboard.length > 0)currentState = true }else if(str == 'printBoard'){ - console.log(state.printboard) state.allBoardData.printboardFiles = state.printboard if(state.printboard.length > 0)currentState = true }else if(str == 'sketchBoard'){ state.allBoardData.sketchboardFiles = state.sketchboard - console.log(state.allBoardData.sketchboardFiles) if(state.sketchboard.length > 0)currentState = true }else if(str == 'colorBoard'){ state.allBoardData.colorBoards = state.colorBoards diff --git a/src/store/workspace/workspace.ts b/src/store/workspace/workspace.ts index dd1b8853..b5a0251d 100644 --- a/src/store/workspace/workspace.ts +++ b/src/store/workspace/workspace.ts @@ -65,12 +65,14 @@ const Workspace : Module = { state.projectList = list }, setProbject(state,data){ + console.log(data) for (const key in data) { if(data[key] == undefined)continue state.probjects[key] = data[key] } }, createProbject(state){ + console.log(123) state.probjects = { name:'', id:'', diff --git a/src/tool/https.js b/src/tool/https.js index 4a5c60c4..b94538d9 100644 --- a/src/tool/https.js +++ b/src/tool/https.js @@ -234,7 +234,8 @@ export const Https = { editRelPublicClassificationIdList:`/api/classification/editRelPublicClassificationIdList`,//多选修改公共标签 //模块化 - llmStream:`/api/llm/stream`,//聊天 + llmStream:`/api/llm/streamNew`,//聊天 + // llmStream:`/api/llm/stream`,//聊天 chatCreateProject:`/api/llm/chatCreateProject`,//聊天创建项目 getChatHistory:`/api/llm/getChatHistory`,//获取聊天历史记录 llmUploadFile:`/api/llm/uploadFile`,//聊天上传文件 diff --git a/src/views/HomeMain.vue b/src/views/HomeMain.vue index 45af2aec..096438df 100644 --- a/src/views/HomeMain.vue +++ b/src/views/HomeMain.vue @@ -51,6 +51,10 @@ Setting
+
+ + Batch Generation +
Delete @@ -225,7 +229,7 @@
- + @@ -297,7 +301,8 @@ export default defineComponent({ selectHistoryIndex:-1, page:1, size:10, - } + }, + bathGenerationList:["poseTransfer","SERIES_DESIGN","toProduct","relight"] }) const historyData = reactive({ @@ -475,7 +480,22 @@ export default defineComponent({ if(childItem.process == 'SERIES_DESIGN' || childItem.process == "SINGLE_DESIGN"){ router.push(`/home?history=${childItem.id}`) }else{ - router.push(`/home/tools?tools=${childItem.process}&id=${childItem.id}`) + let processList = { + toProduct:'TO_PRODUCT_IMAGE', + relight:'RELIGHT', + poseTransfer:'POSE_TRANSFER', + deReconstruction:'DE_RECONSTRUCTION', + patternMaking3D:'THREE_D_PLATE_MAKING', + canvasUpload:'CANVAS', + } + let process = '' + for (const key in processList) { + if(processList[key] == childItem.process){ + process = key + break + } + } + router.push(`/home/tools?tools=${process}&id=${childItem.id}`) } // router.push(`/home?history=${childItem.id}`) } @@ -490,10 +510,11 @@ export default defineComponent({ if(homeMainData.historyData.isShowLoading && !homeMainData.historyData.isNoData)return homeMainData.historyData.isShowLoading = true let data = { - classificationIdList:[], page:homeMainData.historyData.page, size:homeMainData.historyData.size, - collectionName:homeMainData.historyData.searchCollectionName, + projectName:homeMainData.historyData.searchCollectionName, + asc:0, + process:'', // startDate:startDate, // endDate:endDate, // intersection:1, @@ -584,6 +605,11 @@ export default defineComponent({ const newProject = ()=>{ router.push('/home') } + const bathGeneration = (item)=>{ + homeMainData.openType = '' + homeMainData.historyData.selectHistoryIndex = -1 + router.push(`/home/cloud?type=creation&id=${item.id}&name=${item.name}`) + } return { store, userDetail, @@ -614,6 +640,7 @@ export default defineComponent({ setting, accomplishHistory, newProject, + bathGeneration, } }, data() { @@ -1073,6 +1100,7 @@ export default defineComponent({ } > span{ margin-left: .5rem; + white-space: nowrap; } &:hover{ background: #efeff1; diff --git a/src/views/HomeView/Works.vue b/src/views/HomeView/Works.vue index d3d33582..b98d889c 100644 --- a/src/views/HomeView/Works.vue +++ b/src/views/HomeView/Works.vue @@ -352,6 +352,7 @@ export default defineComponent({ .page_content{ scrollbar-width: none; border-radius: 0; + min-height: 100%; } } .modal_title_text{ @@ -386,7 +387,8 @@ export default defineComponent({ >img{ position: relative; left: 50%; - transform: translateX(-50%); + top: 50%; + transform: translate(-50%,-50%); } } &.active{ diff --git a/src/views/HomeView/library.vue b/src/views/HomeView/library.vue index 7eaa3d6d..0ec59bef 100644 --- a/src/views/HomeView/library.vue +++ b/src/views/HomeView/library.vue @@ -696,7 +696,7 @@ export default defineComponent({ let printModelList:any = ref([]) let speed = reactive({ speedList:[ - { + { title:'Picture quality is average, speed is fast', label:'Low Quality', value:'fast', @@ -705,6 +705,10 @@ export default defineComponent({ title:'Picture quality is high, speed is slow', label:'High Quality', value:'high', + },{ + title:'Generate using Wanxiang', + label:'WX', + value:'wx', }, ], speedState:false, @@ -1614,7 +1618,7 @@ export default defineComponent({ text:sloganText, seed:this.searchPictureSeed, timeZone:Intl.DateTimeFormat().resolvedOptions().timeZone, - version:this.speedData.value,//为1就是Print + modelName:this.speedData.value,//为1就是Print gender:selectCodeStr == 'Sketchboard'?this.sex:'', sloganBase64:base64, } diff --git a/vue.config.js b/vue.config.js index 1012f301..8c7fa812 100644 --- a/vue.config.js +++ b/vue.config.js @@ -4,6 +4,7 @@ const webpack = require('webpack') module.exports = defineConfig({ transpileDependencies: ['vuetify'], lintOnSave:false,//关闭语法检查 + productionSourceMap: false,//打包不生成map文件,减少文件大小 devServer: { // hot: true, // 热更新