diff --git a/.env.dev_build b/.env.dev_build index da2a9a20..9de2530a 100644 --- a/.env.dev_build +++ b/.env.dev_build @@ -1,4 +1,4 @@ -VITE_USER_NODE_ENV = 'production' +VITE_USER_NODE_ENV = 'development' # VITE_APP_BASE_URL = 'https://aida.com.hk/test' # VITE_APP_BASE_URL = 'http://18.167.251.121:10088' # VITE_APP_BASE_URL = 'https://api.aida.com.hk' diff --git a/.env.test b/.env.test index ebe37860..cc8ff50b 100644 --- a/.env.test +++ b/.env.test @@ -1,12 +1,6 @@ -<<<<<<< HEAD VITE_USER_NODE_ENV = 'development' VITE_APP_BASE_URL = 'https://test.api.aida.com.hk' # VITE_APP_BASE_URL = 'https://api.aida.com.hk' -======= -NODE_ENV = 'development' -# VUE_APP_BASE_URL = 'https://api.aida.com.hk' -VUE_APP_BASE_URL = 'https://test.api.aida.com.hk' ->>>>>>> 5d8304ce3ece21dd3200ffffb0c76e3ef55dd213 # VITE_APP_BASE_URL = 'http://18.167.251.121:10086' # VITE_APP_BASE_URL = 'http://192.168.1.9:5567' diff --git a/.gitignore b/.gitignore index 118a6b88..61bc8e64 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,4 @@ dist.rar *.sln *.sw? .eslintrc-auto-import.json +components.d.ts diff --git a/components.d.ts b/components.d.ts deleted file mode 100644 index a74ec2fa..00000000 --- a/components.d.ts +++ /dev/null @@ -1,31 +0,0 @@ -/* eslint-disable */ -// @ts-nocheck -// Generated by unplugin-vue-components -// Read more: https://github.com/vuejs/core/pull/3399 -// biome-ignore lint: disable -export {} - -/* prettier-ignore */ -declare module 'vue' { - export interface GlobalComponents { - ABadge: typeof import('ant-design-vue/es')['Badge'] - ACheckbox: typeof import('ant-design-vue/es')['Checkbox'] - ADrawer: typeof import('ant-design-vue/es')['Drawer'] - AImage: typeof import('ant-design-vue/es')['Image'] - AInputNumber: typeof import('ant-design-vue/es')['InputNumber'] - AModal: typeof import('ant-design-vue/es')['Modal'] - APagination: typeof import('ant-design-vue/es')['Pagination'] - APopover: typeof import('ant-design-vue/es')['Popover'] - ARangePicker: typeof import('ant-design-vue/es')['RangePicker'] - ASelect: typeof import('ant-design-vue/es')['Select'] - ASelectOption: typeof import('ant-design-vue/es')['SelectOption'] - ASlider: typeof import('ant-design-vue/es')['Slider'] - ASpin: typeof import('ant-design-vue/es')['Spin'] - ASwitch: typeof import('ant-design-vue/es')['Switch'] - ATable: typeof import('ant-design-vue/es')['Table'] - AUpload: typeof import('ant-design-vue/es')['Upload'] - ElCascader: typeof import('element-plus/es')['ElCascader'] - RouterLink: typeof import('vue-router')['RouterLink'] - RouterView: typeof import('vue-router')['RouterView'] - } -} diff --git a/dist.7z b/dist.7z index 704ed151..471bee9f 100644 Binary files a/dist.7z and b/dist.7z differ diff --git a/public/image/brush/PencilBrush-2.jpg b/public/image/brush/PencilBrush-2.jpg deleted file mode 100644 index 4ae64cd3..00000000 Binary files a/public/image/brush/PencilBrush-2.jpg and /dev/null differ diff --git a/public/image/brush/CrayonBrush.jpg b/public/image/brush/crayon.jpg similarity index 100% rename from public/image/brush/CrayonBrush.jpg rename to public/image/brush/crayon.jpg diff --git a/public/image/brush/fur.jpg b/public/image/brush/fur.jpg new file mode 100644 index 00000000..6c4563c9 Binary files /dev/null and b/public/image/brush/fur.jpg differ diff --git a/public/image/brush/InkBrush.jpg b/public/image/brush/ink.jpg similarity index 100% rename from public/image/brush/InkBrush.jpg rename to public/image/brush/ink.jpg diff --git a/public/image/brush/LongfurBrush.jpg b/public/image/brush/longFur.jpg similarity index 100% rename from public/image/brush/LongfurBrush.jpg rename to public/image/brush/longFur.jpg diff --git a/public/image/brush/MarkerBrush.jpg b/public/image/brush/marker.jpg similarity index 100% rename from public/image/brush/MarkerBrush.jpg rename to public/image/brush/marker.jpg diff --git a/public/image/brush/pen.jpg b/public/image/brush/pen.jpg new file mode 100644 index 00000000..450979ad Binary files /dev/null and b/public/image/brush/pen.jpg differ diff --git a/public/image/brush/PencilBrush.jpg b/public/image/brush/pencil.jpg similarity index 100% rename from public/image/brush/PencilBrush.jpg rename to public/image/brush/pencil.jpg diff --git a/public/image/brush/RibbonBrush.jpg b/public/image/brush/ribbon.jpg similarity index 100% rename from public/image/brush/RibbonBrush.jpg rename to public/image/brush/ribbon.jpg diff --git a/public/image/brush/shaded.jpg b/public/image/brush/shaded.jpg new file mode 100644 index 00000000..a60d35c0 Binary files /dev/null and b/public/image/brush/shaded.jpg differ diff --git a/public/image/brush/spray.jpg b/public/image/brush/spray.jpg new file mode 100644 index 00000000..a4ecd7e5 Binary files /dev/null and b/public/image/brush/spray.jpg differ diff --git a/public/image/brush/imgBrush.webp b/public/image/brush/texture.jpg similarity index 100% rename from public/image/brush/imgBrush.webp rename to public/image/brush/texture.jpg diff --git a/public/image/brush/WritingBrush.jpg b/public/image/brush/writing.jpg similarity index 100% rename from public/image/brush/WritingBrush.jpg rename to public/image/brush/writing.jpg diff --git a/src/assets/iconfont2/iconfont.css b/src/assets/iconfont2/iconfont.css index 1aba4c42..32375ef9 100644 --- a/src/assets/iconfont2/iconfont.css +++ b/src/assets/iconfont2/iconfont.css @@ -1,18 +1,22 @@ @font-face { font-family: "iconfont"; /* Project id 4292253 */ - src: url('iconfont.woff2?t=1727415711578') format('woff2'), - url('iconfont.woff?t=1727415711578') format('woff'), - url('iconfont.ttf?t=1727415711578') format('truetype'); + src: url('iconfont.woff2?t=1759888699816') format('woff2'), + url('iconfont.woff?t=1759888699816') format('woff'), + url('iconfont.ttf?t=1759888699816') format('truetype'); } .iconfont { font-family: "iconfont" !important; - font-size: 1.8rem; + font-size: 1.6rem; font-style: normal; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } +.icon-clothes:before { + content: "\e8d1"; +} + .icon-caizhi:before { content: "\e647"; } diff --git a/src/assets/iconfont2/iconfont.json b/src/assets/iconfont2/iconfont.json index 578a73da..21b485b7 100644 --- a/src/assets/iconfont2/iconfont.json +++ b/src/assets/iconfont2/iconfont.json @@ -1,170 +1,219 @@ { - "id": "", - "name": "", + "id": "4292253", + "name": "aida", "font_family": "iconfont", "css_prefix_text": "icon-", "description": "", "glyphs": [ { - "icon_id": "124968799", - "name": "外套_长款外套1@1x", - "font_class": "a-waitao_changkuanwaitao11x", - "unicode": "e66c", - "unicode_decimal": 58988 + "icon_id": "20183053", + "name": "clothes", + "font_class": "clothes", + "unicode": "e8d1", + "unicode_decimal": 59601 }, { - "icon_id": "125198319", - "name": "撤销 返回 撤回 上一步", - "font_class": "fanchehui", - "unicode": "e626", - "unicode_decimal": 58918 + "icon_id": "15739173", + "name": "材质", + "font_class": "caizhi", + "unicode": "e647", + "unicode_decimal": 58951 }, { - "icon_id": "125198320", - "name": "撤销 返回 撤回 上一步", - "font_class": "chehui", - "unicode": "e609", - "unicode_decimal": 58889 + "icon_id": "35023469", + "name": "IC-液化", + "font_class": "IC-yehua", + "unicode": "e61b", + "unicode_decimal": 58907 }, { - "icon_id": "125524062", - "name": "语言", - "font_class": "yuyan", - "unicode": "e85f", - "unicode_decimal": 59487 + "icon_id": "12096844", + "name": "上一层", + "font_class": "shangyiceng", + "unicode": "e751", + "unicode_decimal": 59217 }, { - "icon_id": "126177191", - "name": "标签", - "font_class": "biaoqian", - "unicode": "e603", - "unicode_decimal": 58883 + "icon_id": "16531912", + "name": "上一层", + "font_class": "shangyiceng1", + "unicode": "e604", + "unicode_decimal": 58884 }, { - "icon_id": "126459101", - "name": "并集", - "font_class": "bingji", - "unicode": "e620", - "unicode_decimal": 58912 + "icon_id": "24253227", + "name": "下一层", + "font_class": "xiayiceng", + "unicode": "e68a", + "unicode_decimal": 59018 }, { - "icon_id": "126459102", - "name": "并集", - "font_class": "bingji1", - "unicode": "e668", - "unicode_decimal": 58984 + "icon_id": "24253230", + "name": "上一层", + "font_class": "shangyiceng2", + "unicode": "e68b", + "unicode_decimal": 59019 }, { - "icon_id": "126901286", - "name": "点位", - "font_class": "dianwei", - "unicode": "e685", - "unicode_decimal": 59013 - }, - { - "icon_id": "130743908", - "name": "编辑", - "font_class": "bianji", - "unicode": "e600", - "unicode_decimal": 58880 - }, - { - "icon_id": "130743909", - "name": "圆形", - "font_class": "circle", - "unicode": "e64f", - "unicode_decimal": 58959 - }, - { - "icon_id": "130743910", - "name": "三角形", - "font_class": "sanjiaoxing", - "unicode": "e615", - "unicode_decimal": 58901 - }, - { - "icon_id": "130743911", - "name": "图层", - "font_class": "tuceng", - "unicode": "e632", - "unicode_decimal": 58930 - }, - { - "icon_id": "130743912", - "name": "平移", - "font_class": "move", - "unicode": "e616", - "unicode_decimal": 58902 - }, - { - "icon_id": "130743913", - "name": "橡皮", - "font_class": "xiangpi_huaban1", - "unicode": "e67b", - "unicode_decimal": 59003 - }, - { - "icon_id": "130743914", - "name": "tx-fill-椭圆形", - "font_class": "tx-fill-tuoyuanxing", - "unicode": "e64c", - "unicode_decimal": 58956 - }, - { - "icon_id": "130743915", - "name": "直线", - "font_class": "zhixian", - "unicode": "e602", - "unicode_decimal": 58882 - }, - { - "icon_id": "130743916", - "name": "线", - "font_class": "xian", - "unicode": "ec5f", - "unicode_decimal": 60511 - }, - { - "icon_id": "130743917", - "name": "正方形", - "font_class": "checkbox-full", - "unicode": "ea6f", - "unicode_decimal": 60015 - }, - { - "icon_id": "130743918", - "name": "图层", - "font_class": "tuceng1", - "unicode": "e62d", - "unicode_decimal": 58925 - }, - { - "icon_id": "130751283", + "icon_id": "3663275", "name": "审批", "font_class": "shenpi", "unicode": "e6a1", "unicode_decimal": 59041 }, { - "icon_id": "130751284", + "icon_id": "7638976", "name": "用户", "font_class": "yonghu", "unicode": "e617", "unicode_decimal": 58903 }, { - "icon_id": "130751285", + "icon_id": "9775414", "name": "使用次数", "font_class": "usetime", "unicode": "e601", "unicode_decimal": 58881 }, { - "icon_id": "130751286", + "icon_id": "16843615", "name": "下拉", "font_class": "xiala", "unicode": "e634", "unicode_decimal": 58932 + }, + { + "icon_id": "1264", + "name": "编辑", + "font_class": "bianji", + "unicode": "e600", + "unicode_decimal": 58880 + }, + { + "icon_id": "755612", + "name": "圆形", + "font_class": "circle", + "unicode": "e64f", + "unicode_decimal": 58959 + }, + { + "icon_id": "3101162", + "name": "三角形", + "font_class": "sanjiaoxing", + "unicode": "e615", + "unicode_decimal": 58901 + }, + { + "icon_id": "6774075", + "name": "图层", + "font_class": "tuceng", + "unicode": "e632", + "unicode_decimal": 58930 + }, + { + "icon_id": "10905244", + "name": "平移", + "font_class": "move", + "unicode": "e616", + "unicode_decimal": 58902 + }, + { + "icon_id": "14421659", + "name": "橡皮", + "font_class": "xiangpi_huaban1", + "unicode": "e67b", + "unicode_decimal": 59003 + }, + { + "icon_id": "14718690", + "name": "tx-fill-椭圆形", + "font_class": "tx-fill-tuoyuanxing", + "unicode": "e64c", + "unicode_decimal": 58956 + }, + { + "icon_id": "17521049", + "name": "直线", + "font_class": "zhixian", + "unicode": "e602", + "unicode_decimal": 58882 + }, + { + "icon_id": "17581689", + "name": "线", + "font_class": "xian", + "unicode": "ec5f", + "unicode_decimal": 60511 + }, + { + "icon_id": "18175800", + "name": "正方形", + "font_class": "checkbox-full", + "unicode": "ea6f", + "unicode_decimal": 60015 + }, + { + "icon_id": "26998795", + "name": "图层", + "font_class": "tuceng1", + "unicode": "e62d", + "unicode_decimal": 58925 + }, + { + "icon_id": "31762941", + "name": "点位", + "font_class": "dianwei", + "unicode": "e685", + "unicode_decimal": 59013 + }, + { + "icon_id": "8722601", + "name": "并集", + "font_class": "bingji", + "unicode": "e620", + "unicode_decimal": 58912 + }, + { + "icon_id": "15192904", + "name": "并集", + "font_class": "bingji1", + "unicode": "e668", + "unicode_decimal": 58984 + }, + { + "icon_id": "17863630", + "name": "标签", + "font_class": "biaoqian", + "unicode": "e603", + "unicode_decimal": 58883 + }, + { + "icon_id": "16399020", + "name": "语言", + "font_class": "yuyan", + "unicode": "e85f", + "unicode_decimal": 59487 + }, + { + "icon_id": "4240742", + "name": "撤销 返回 撤回 上一步", + "font_class": "fanchehui", + "unicode": "e626", + "unicode_decimal": 58918 + }, + { + "icon_id": "6126117", + "name": "撤销 返回 撤回 上一步", + "font_class": "chehui", + "unicode": "e609", + "unicode_decimal": 58889 + }, + { + "icon_id": "33174601", + "name": "外套_长款外套1@1x", + "font_class": "a-waitao_changkuanwaitao11x", + "unicode": "e66c", + "unicode_decimal": 58988 } ] } diff --git a/src/assets/iconfont2/iconfont.ttf b/src/assets/iconfont2/iconfont.ttf index 905838aa..d708a6c9 100644 Binary files a/src/assets/iconfont2/iconfont.ttf and b/src/assets/iconfont2/iconfont.ttf differ diff --git a/src/assets/iconfont2/iconfont.woff b/src/assets/iconfont2/iconfont.woff index a1c35fd0..2f9fcd35 100644 Binary files a/src/assets/iconfont2/iconfont.woff and b/src/assets/iconfont2/iconfont.woff differ diff --git a/src/assets/iconfont2/iconfont.woff2 b/src/assets/iconfont2/iconfont.woff2 index 3fc507c2..e48d0fb0 100644 Binary files a/src/assets/iconfont2/iconfont.woff2 and b/src/assets/iconfont2/iconfont.woff2 differ diff --git a/src/assets/icons/CClothes.svg b/src/assets/icons/CClothes.svg new file mode 100644 index 00000000..bcbd739a --- /dev/null +++ b/src/assets/icons/CClothes.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/CHelp.svg b/src/assets/icons/CHelp.svg new file mode 100644 index 00000000..f8b7726b --- /dev/null +++ b/src/assets/icons/CHelp.svg @@ -0,0 +1,17 @@ + + + + + + + + diff --git a/src/assets/icons/CLibrary.svg b/src/assets/icons/CLibrary.svg new file mode 100644 index 00000000..ce0f94c5 --- /dev/null +++ b/src/assets/icons/CLibrary.svg @@ -0,0 +1,2 @@ + + diff --git a/src/assets/icons/CSelect.svg b/src/assets/icons/CSelect.svg index 717e534e..c118de07 100644 --- a/src/assets/icons/CSelect.svg +++ b/src/assets/icons/CSelect.svg @@ -1 +1,19 @@ - \ No newline at end of file + + + + + + + + diff --git a/src/assets/icons/DetailCompare.svg b/src/assets/icons/DetailCompare.svg new file mode 100644 index 00000000..aeceaf6b --- /dev/null +++ b/src/assets/icons/DetailCompare.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/icons/Folder.svg b/src/assets/icons/Folder.svg new file mode 100644 index 00000000..abfd060d --- /dev/null +++ b/src/assets/icons/Folder.svg @@ -0,0 +1,22 @@ + + + + + + + + diff --git a/src/assets/icons/editFrontBack.svg b/src/assets/icons/editFrontBack.svg new file mode 100644 index 00000000..6977c448 --- /dev/null +++ b/src/assets/icons/editFrontBack.svg @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/images/icon/library.png b/src/assets/images/icon/library.png new file mode 100644 index 00000000..60b8e7bf Binary files /dev/null and b/src/assets/images/icon/library.png differ diff --git a/src/assets/images/icon/selected.png b/src/assets/images/icon/selected.png new file mode 100644 index 00000000..149bc06a Binary files /dev/null and b/src/assets/images/icon/selected.png differ diff --git a/src/assets/images/icon/upload_black.png b/src/assets/images/icon/upload_black.png new file mode 100644 index 00000000..635249c7 Binary files /dev/null and b/src/assets/images/icon/upload_black.png differ diff --git a/src/assets/images/icon/upload_gray.png b/src/assets/images/icon/upload_gray.png new file mode 100644 index 00000000..4e109e29 Binary files /dev/null and b/src/assets/images/icon/upload_gray.png differ diff --git a/src/assets/style/style.css b/src/assets/style/style.css index 0ebbe619..96ae3887 100644 --- a/src/assets/style/style.css +++ b/src/assets/style/style.css @@ -62,6 +62,9 @@ li { height: 100%; margin: 0 auto; } +.ant-dropdown-menu { + border-radius: 1rem; +} .button_second { width: 14rem; text-align: center; @@ -237,7 +240,7 @@ li { color: #fff; background-color: #000; text-align: center; - font-weight: 600; + font-weight: 500; border: 2px solid #000; cursor: pointer; box-sizing: border-box; @@ -2106,7 +2109,7 @@ textarea:focus { .generalMenu_printModel_upload .input_border .input_box_btnBox .upload_item .upload_file_item, .generate .input_border .input_box_btnBox .upload_item .upload_file_item { position: absolute; - left: 0; + left: 1rem; top: 50%; transform: translateY(-50%); height: 4.7rem; @@ -2207,7 +2210,7 @@ textarea:focus { cursor: pointer; transition: all 0.3s; position: relative; - width: 4rem; + width: 3rem; display: flex; min-height: 3rem; justify-content: center; diff --git a/src/assets/style/style.less b/src/assets/style/style.less index db55a3e7..8e0a7922 100644 --- a/src/assets/style/style.less +++ b/src/assets/style/style.less @@ -63,6 +63,9 @@ input:focus{ height: 100%; margin: 0 auto; } +.ant-dropdown-menu{ + border-radius: 1rem; +} .button_second{ width: 14rem; text-align: center; @@ -241,7 +244,7 @@ input:focus{ color: #fff; background-color: #000; text-align: center; - font-weight: 600; + font-weight: 500; border: 2px solid #000; cursor: pointer; box-sizing: border-box; @@ -2105,7 +2108,7 @@ textarea:focus{ width: 4.7rem; .upload_file_item{ position: absolute; - left: 0; + left: 1rem; top: 50%; transform: translateY(-50%); height: 4.7rem; @@ -2150,7 +2153,7 @@ textarea:focus{ cursor: pointer; transition: all .3s; position: relative; - width: 4rem; + width: 3rem; align-items: center; justify-content: center; diff --git a/src/assets/work/1.PNG b/src/assets/work/1.PNG deleted file mode 100644 index c91dfdf7..00000000 Binary files a/src/assets/work/1.PNG and /dev/null differ diff --git a/src/assets/work/2.PNG b/src/assets/work/2.PNG deleted file mode 100644 index 81540426..00000000 Binary files a/src/assets/work/2.PNG and /dev/null differ diff --git a/src/assets/work/3.PNG b/src/assets/work/3.PNG deleted file mode 100644 index f877ece1..00000000 Binary files a/src/assets/work/3.PNG and /dev/null differ diff --git a/src/assets/work/5.PNG b/src/assets/work/5.PNG deleted file mode 100644 index f0d87430..00000000 Binary files a/src/assets/work/5.PNG and /dev/null differ diff --git a/src/assets/work/6.PNG b/src/assets/work/6.PNG deleted file mode 100644 index 9ccafd2a..00000000 Binary files a/src/assets/work/6.PNG and /dev/null differ diff --git a/src/assets/work/IMG_0001.PNG b/src/assets/work/IMG_0001.PNG deleted file mode 100644 index 139e4ba3..00000000 Binary files a/src/assets/work/IMG_0001.PNG and /dev/null differ diff --git a/src/assets/work/IMG_0008.PNG b/src/assets/work/IMG_0008.PNG deleted file mode 100644 index b999a075..00000000 Binary files a/src/assets/work/IMG_0008.PNG and /dev/null differ diff --git a/src/assets/work/group_hide.PNG b/src/assets/work/group_hide.PNG deleted file mode 100644 index 8ea506bb..00000000 Binary files a/src/assets/work/group_hide.PNG and /dev/null differ diff --git a/src/assets/work/group_show.PNG b/src/assets/work/group_show.PNG deleted file mode 100644 index 3d1e2d45..00000000 Binary files a/src/assets/work/group_show.PNG and /dev/null differ diff --git a/src/assets/work/图片1.png b/src/assets/work/图片1.png deleted file mode 100644 index 9f7d2b27..00000000 Binary files a/src/assets/work/图片1.png and /dev/null differ diff --git a/src/assets/work/图片2.png b/src/assets/work/图片2.png deleted file mode 100644 index 015e64f3..00000000 Binary files a/src/assets/work/图片2.png and /dev/null differ diff --git a/src/assets/work/图片4.png b/src/assets/work/图片4.png deleted file mode 100644 index afab67c2..00000000 Binary files a/src/assets/work/图片4.png and /dev/null differ diff --git a/src/assets/work/图片5.png b/src/assets/work/图片5.png deleted file mode 100644 index a43e8b87..00000000 Binary files a/src/assets/work/图片5.png and /dev/null differ diff --git a/src/assets/work/图片6.png b/src/assets/work/图片6.png deleted file mode 100644 index dea147d3..00000000 Binary files a/src/assets/work/图片6.png and /dev/null differ diff --git a/src/assets/work/图片7.png b/src/assets/work/图片7.png deleted file mode 100644 index 50276080..00000000 Binary files a/src/assets/work/图片7.png and /dev/null differ diff --git a/src/assets/work/图片8.png b/src/assets/work/图片8.png deleted file mode 100644 index 54914c87..00000000 Binary files a/src/assets/work/图片8.png and /dev/null differ diff --git a/src/assets/work/图片9.png b/src/assets/work/图片9.png deleted file mode 100644 index 6755cd89..00000000 Binary files a/src/assets/work/图片9.png and /dev/null differ diff --git a/src/component/Account/frontPage/bindPage.vue b/src/component/Account/frontPage/bindPage.vue index 7ec33f51..825781f4 100644 --- a/src/component/Account/frontPage/bindPage.vue +++ b/src/component/Account/frontPage/bindPage.vue @@ -111,28 +111,46 @@ export default defineComponent({ .catch((res) => {accountHomeData.loadingShow = false}); } const ungroupWeiXinModel = ()=>{ - Https.axiosGet(Https.httpUrls.unbindWeChat,).then((rv)=>{ - message.success(t('frontPage.jsContent1')); - let value = { - accountExtendList:{ - WeChat:undefined, - Google:accountHomeData.userDetail.accountExtendList?.Google - } + Modal.confirm({ + title: t('frontPage.UnbindTip'), + okText: t('Yes'), + cancelText: t('No'), + mask:false, + centered:true, + onOk() { + Https.axiosGet(Https.httpUrls.unbindWeChat,).then((rv)=>{ + message.success(t('frontPage.jsContent1')); + let value = { + accountExtendList:{ + WeChat:undefined, + Google:accountHomeData.userDetail.accountExtendList?.Google + } + } + store.commit("upUserDetail", value) + }) } - store.commit("upUserDetail", value) - }) + }); } const ungroupGoogleModel = ()=>{ - Https.axiosGet(Https.httpUrls.unbindGoogle,).then((rv)=>{ - let value = { - accountExtendList:{ - WeChat:accountHomeData.userDetail.accountExtendList?.WeChat, - Google:undefined, - } + Modal.confirm({ + title: t('frontPage.UnbindTip'), + okText: t('Yes'), + cancelText: t('No'), + mask:false, + centered:true, + onOk() { + Https.axiosGet(Https.httpUrls.unbindGoogle,).then((rv)=>{ + let value = { + accountExtendList:{ + WeChat:accountHomeData.userDetail.accountExtendList?.WeChat, + Google:undefined, + } + } + store.commit("upUserDetail", value) + message.success(t('frontPage.jsContent1')); + }) } - store.commit("upUserDetail", value) - message.success(t('frontPage.jsContent1')); - }) + }); } const modifyEmail = ()=>{ bindPageDom.bindEmail.init('Modify') diff --git a/src/component/Account/frontPage/mylnformation.vue b/src/component/Account/frontPage/mylnformation.vue index 80175852..72da23b4 100644 --- a/src/component/Account/frontPage/mylnformation.vue +++ b/src/component/Account/frontPage/mylnformation.vue @@ -117,13 +117,12 @@ export default defineComponent({ let value = { country:accountHomeData.Country, title:accountHomeData.selectSex, - userName:accountHomeData.editUserName, surname:accountHomeData.surname, givenName:accountHomeData.givenName, } store.commit('upUserDetail',value) accountHomeData.loadingShow = false - message.success(t('exportModel.jsContent7')) + message.success(t('account.jsContent13')) }).catch((err:any)=>{ accountHomeData.loadingShow = false }) diff --git a/src/component/Administrator/SE/designDetailList/index.vue b/src/component/Administrator/SE/designDetailList/index.vue index 89ac71fd..cb046d96 100644 --- a/src/component/Administrator/SE/designDetailList/index.vue +++ b/src/component/Administrator/SE/designDetailList/index.vue @@ -1,12 +1,11 @@ \ No newline at end of file diff --git a/src/component/Administrator/TestClickData.vue b/src/component/Administrator/TestClickData.vue index f5bf6b81..ea76f449 100644 --- a/src/component/Administrator/TestClickData.vue +++ b/src/component/Administrator/TestClickData.vue @@ -1,12 +1,11 @@ \ No newline at end of file diff --git a/src/component/Administrator/organization/organization.vue b/src/component/Administrator/organization/organization.vue index f3fb8d79..0488bed0 100644 --- a/src/component/Administrator/organization/organization.vue +++ b/src/component/Administrator/organization/organization.vue @@ -1,228 +1,305 @@ \ No newline at end of file diff --git a/src/component/Canvas/CanvasEditor/commands/CutSelectionToNewLayerCommand.js b/src/component/Canvas/CanvasEditor/commands/CutSelectionToNewLayerCommand.js index ad5addc5..62de515a 100644 --- a/src/component/Canvas/CanvasEditor/commands/CutSelectionToNewLayerCommand.js +++ b/src/component/Canvas/CanvasEditor/commands/CutSelectionToNewLayerCommand.js @@ -10,6 +10,8 @@ import { fabric } from "fabric-with-all"; import { generateId } from "../utils/helper.js"; import { ClearSelectionCommand } from "./LassoCutoutCommand.js"; import { ClearSelectionContentCommand } from "./ClearSelectionContentCommand.js"; +import i18n from "@/lang/index.ts"; +const { t } = i18n.global; /** * 剪切选区到新图层命令 @@ -36,7 +38,7 @@ export class CutSelectionToNewLayerCommand extends CompositeCommand { this.baseResolutionScale = options.baseResolutionScale || 2; // 基础分辨率倍数 this.groupId = options.groupId || generateId("lasso-copy-group-"); - this.groupName = options.groupName || `选区组`; + this.groupName = options.groupName || t(`Canvas.ConstituencyGroup`); this.groupLayer = null; // 新增:保存组图层的引用 this.originalLayersLength = 0; // 新增:保存原始图层数量 @@ -179,7 +181,7 @@ export class CutSelectionToNewLayerCommand extends CompositeCommand { // 创建新的组图层 this.groupLayer = createLayer({ id: this.groupId, - name: this.groupName || `选区组`, + name: this.groupName || t(`Canvas.ConstituencyGroup`), type: LayerType.GROUP, visible: true, locked: false, diff --git a/src/component/Canvas/CanvasEditor/commands/LassoCutoutCommand.bak.js b/src/component/Canvas/CanvasEditor/commands/LassoCutoutCommand.bak.js index 8897a6bd..5c4a50b2 100644 --- a/src/component/Canvas/CanvasEditor/commands/LassoCutoutCommand.bak.js +++ b/src/component/Canvas/CanvasEditor/commands/LassoCutoutCommand.bak.js @@ -11,6 +11,10 @@ import { } from "./LayerCommands.js"; import { fabric } from "fabric-with-all"; import { generateId } from "../utils/helper.js"; +import i18n from "@/lang/index.ts"; +const { t } = i18n.global; + + /** * 套索抠图命令 @@ -37,7 +41,7 @@ export class LassoCutoutCommand extends CompositeCommand { this.baseResolutionScale = options.baseResolutionScale || 2; // 基础分辨率倍数 this.groupId = options.groupId || generateId("lasso-group-"); - this.groupName = options.groupName || `选区组`; + this.groupName = options.groupName || t(`Canvas.ConstituencyGroup`); this.clippingMaskId = generateId("clipping-mask-"); @@ -238,7 +242,7 @@ export class LassoCutoutCommand extends CompositeCommand { // 创建新的组图层 this.groupLayer = createLayer({ id: this.groupId, - name: this.groupName || `选区组`, + name: this.groupName || t(`Canvas.ConstituencyGroup`), type: LayerType.GROUP, visible: true, locked: false, diff --git a/src/component/Canvas/CanvasEditor/commands/LassoCutoutCommand.js b/src/component/Canvas/CanvasEditor/commands/LassoCutoutCommand.js index 06a3ade1..a3ad1eaf 100644 --- a/src/component/Canvas/CanvasEditor/commands/LassoCutoutCommand.js +++ b/src/component/Canvas/CanvasEditor/commands/LassoCutoutCommand.js @@ -13,6 +13,9 @@ import { import { fabric } from "fabric-with-all"; import { generateId } from "../utils/helper.js"; import { ToolCommand } from "./ToolCommands.js"; +import i18n from "@/lang/index.ts"; +const { t } = i18n.global; + /** * 套索抠图命令 @@ -39,7 +42,7 @@ export class LassoCutoutCommand extends CompositeCommand { this.baseResolutionScale = options.baseResolutionScale || 2; // 基础分辨率倍数 this.groupId = options.groupId || generateId("lasso-group-"); - this.groupName = options.groupName || `选区组`; + this.groupName = options.groupName || t(`Canvas.ConstituencyGroup`); this.clippingMaskId = generateId("clipping-mask-"); @@ -220,16 +223,26 @@ export class LassoCutoutCommand extends CompositeCommand { await clearSelectionCmd.execute(); this.executedCommands.push(clearSelectionCmd); - const topLayerIndex = this.layerManager.layers.value.findIndex( - (layer) => layer.id === this.originalLayer.id - ); + const layers = this.layerManager.layers.value; + var topLayerIndex = 0; + layers.forEach((layer, index) => { + if (layer.id === this.originalLayer.id) { + topLayerIndex = index; + }else if (layer.children.length > 0) { + layer.children.forEach((childLayer) => { + if (childLayer.id === this.originalLayer.id) { + topLayerIndex = index; + } + }); + } + }); // const selectLayer = this.layerManager.layers.value[topLayerIndex]; // 创建新的组图层 this.groupLayer = createLayer({ id: this.groupId, - name: this.groupName || `选区组`, + name: this.groupName || t(`Canvas.ConstituencyGroup`), type: LayerType.GROUP, visible: true, locked: false, @@ -244,7 +257,7 @@ export class LassoCutoutCommand extends CompositeCommand { // }); const selectLayer = createLayer({ - name: `选区空图层`, + name: t(`Canvas.ConstituencyEmptyLayer`), type: LayerType.EMPTY, visible: true, locked: false, @@ -284,6 +297,7 @@ export class LassoCutoutCommand extends CompositeCommand { this.groupLayer.children.push(selectLayer); // 插入新组图层 + console.log("新增套索添加index", topLayerIndex); this.layerManager.layers.value.splice(topLayerIndex, 0, this.groupLayer); this.layerManager.activeLayerId.value = selectLayer.id; // 设置新组图层为活动图层 diff --git a/src/component/Canvas/CanvasEditor/commands/LayerCommands.js b/src/component/Canvas/CanvasEditor/commands/LayerCommands.js index 97f551be..1dffba96 100644 --- a/src/component/Canvas/CanvasEditor/commands/LayerCommands.js +++ b/src/component/Canvas/CanvasEditor/commands/LayerCommands.js @@ -116,7 +116,7 @@ export class AddLayerCommand extends Command { parentLayer.children.splice(insertIndex, 0, newLayer); console.log( - `新图层已插入到子图层位置: ${insertIndex} (父图层: ${parentLayer.name})` + `新图层已插入到子图层位置: ${insertIndex} (父图层: ${parentLayer.name})` ); } else { // 当前激活图层是一级图层 @@ -2569,7 +2569,6 @@ export class CreateImageLayerCommand extends Command { this.fabricImage = options.fabricImage; this.toolManager = options.toolManager; this.layerName = options.layerName || null; - this.imageId = generateId("image_"); // 存储执行过程中的结果 @@ -2617,6 +2616,18 @@ export class CreateImageLayerCommand extends Command { this.commands.push(createLayerCmd); this.executedCommands.push(createLayerCmd); + // 新加功能-选区套索内添加图片自动居中 + const { parent } = findLayerRecursively(this.layerManager.layers.value, this.newLayerId); + if(parent && parent.selectObject){ + let {top,left,width,height} = parent.selectObject; + const ltop = top + height / 2; + const lleft = left + width / 2; + this.fabricImage.set({ + top: ltop, + left: lleft, + }) + } + // 2. 添加图片对象到图层命令 const addObjectCmd = new AddObjectToLayerCommand({ canvas: this.layerManager.canvas, @@ -4197,6 +4208,7 @@ export class RemoveChildLayerCommand extends Command { this.isActiveLayer = this.layerId === this.activeLayerId.value; this.originalObjects = this.canvas.getObjects().filter((obj) => { + obj.parentId = this.parentId; return obj.layerId === this.layerId; }); } diff --git a/src/component/Canvas/CanvasEditor/commands/TextCommands.js b/src/component/Canvas/CanvasEditor/commands/TextCommands.js index f4d52013..24c47c62 100644 --- a/src/component/Canvas/CanvasEditor/commands/TextCommands.js +++ b/src/component/Canvas/CanvasEditor/commands/TextCommands.js @@ -1,6 +1,8 @@ import { generateId, optimizeCanvasRendering } from "../utils/helper"; import { createLayer, LayerType, OperationType } from "../utils/layerHelper"; import { Command } from "./Command"; +import i18n from "@/lang/index.ts"; +const { t } = i18n.global; /** * 文本内容命令 @@ -329,7 +331,7 @@ export class CreateTextCommand extends Command { // 默认文本属性 this.defaultOptions = { - text: "双击编辑文本", + text: t('Canvas.DoubleClickText'), fontFamily: "Arial", fontSize: 24, fontWeight: "normal", diff --git a/src/component/Canvas/CanvasEditor/components/BrushControlPanel.vue b/src/component/Canvas/CanvasEditor/components/BrushControlPanel.vue index 3d08bb67..6ef70614 100644 --- a/src/component/Canvas/CanvasEditor/components/BrushControlPanel.vue +++ b/src/component/Canvas/CanvasEditor/components/BrushControlPanel.vue @@ -362,6 +362,11 @@ watch( setBrushSize(newSize); } ); +watch(()=>isVisible.value, (newVisible) => { + if (newVisible) { + setBrushSize(brushSize.value); + } +}) // 监听brushOpacity的变化,更新到BrushStore watch( diff --git a/src/component/Canvas/CanvasEditor/components/BrushPanel.vue b/src/component/Canvas/CanvasEditor/components/BrushPanel.vue index 0688d390..b625358b 100644 --- a/src/component/Canvas/CanvasEditor/components/BrushPanel.vue +++ b/src/component/Canvas/CanvasEditor/components/BrushPanel.vue @@ -14,7 +14,8 @@ @click="setBrushTypeWithCommand(brush.id)" :class="['brush-type-item', { active: brushStore.state.type === brush.id }]" > -
+ + {{ brush.name }} @@ -189,7 +190,7 @@
- {{ $t("上传的纹理") }} + {{ $t("Canvas.UploadedTexture") }}
@@ -200,7 +201,7 @@
+
- {{ $t("上传纹理") }} + {{ $t("Canvas.UploadTexture") }}
×
@@ -534,6 +535,9 @@ import { TextureUploadCommand, } from "../commands/BrushCommands"; import { debounce } from "lodash-es"; +import { useI18n } from "vue-i18n"; +const { t } = useI18n(); + // 从工具管理器获取可用笔刷类型 const toolManager = inject("toolManager"); @@ -872,7 +876,7 @@ function applyPresetWithCommand(presetIndex) { // 保存当前设置为预设 function saveCurrentAsPreset() { // 简单实现,可以后续优化为弹窗输入名称 - const name = prompt("请输入预设名称:", `预设 ${BrushStore.state.presets.length + 1}`); + const name = prompt(t('Canvas.presetNamePrompt'), `${t('Canvas.preset')} ${BrushStore.state.presets.length + 1}`); if (name) { const presetIndex = BrushStore.saveCurrentAsPreset(name); // 应用新创建的预设(可选) @@ -886,6 +890,7 @@ onMounted(() => { const availableBrushes = toolManager.brushManager .getBrushTypes() ?.filter((brush) => brush.id !== "eraser"); + console.log(availableBrushes) BrushStore.setAvailableBrushes(availableBrushes); } }); @@ -1178,6 +1183,8 @@ const brushStore = BrushStore; margin-bottom: 8px; border-radius: 4px; background-color: rgba(0, 0, 0, 0.02); + object-fit: contain; + background-color: #fff; } /* 保持笔刷预览内容样式一致 */ diff --git a/src/component/Canvas/CanvasEditor/components/KeyboardShortcutHelp.vue b/src/component/Canvas/CanvasEditor/components/KeyboardShortcutHelp.vue index edba6ecc..e1d19484 100644 --- a/src/component/Canvas/CanvasEditor/components/KeyboardShortcutHelp.vue +++ b/src/component/Canvas/CanvasEditor/components/KeyboardShortcutHelp.vue @@ -136,7 +136,7 @@ function convertShortcuts(managerShortcuts) { action: actionDisplay, windows: shortcut.key.replace(/cmdOrCtrl\+/g, "Ctrl+"), mac: shortcut.key.replace(/cmdOrCtrl\+/g, "⌘+"), - touch: shortcut.touch || "触控界面点击对应工具", + touch: shortcut.touch || t('Canvas.touchTools'), displayKey: shortcut.displayKey, }); } @@ -326,8 +326,8 @@ function getShortcutsByCategory(category) { iPad iOS Android - 其他 - (触控设备) + {{ $t('Canvas.other') }} + ({{ $t('Canvas.touchDevice') }})
@@ -421,14 +421,14 @@ function getShortcutsByCategory(category) {
-->
-

触控设备提示

+

{{ $t('Canvas.touchDevicePrompts') }}

diff --git a/src/component/Canvas/CanvasEditor/components/LiquifyPanel.vue b/src/component/Canvas/CanvasEditor/components/LiquifyPanel.vue index c8946485..081bf615 100644 --- a/src/component/Canvas/CanvasEditor/components/LiquifyPanel.vue +++ b/src/component/Canvas/CanvasEditor/components/LiquifyPanel.vue @@ -258,6 +258,23 @@ const setClosePanel = ()=>{ closePanel.value = !closePanel.value } +// 工具管理器和画布管理器 +const toolManager = inject("toolManager"); +const canvasManager = inject("canvasManager"); + +watch(size, (newSize, oldSize) => { + setBrushIndicatorSize(newSize) + +}) +// 设置笔刷指示器大小 +function setBrushIndicatorSize(size) { + // 如果工具管理器存在,立即应用此更改 + console.log(`=========== ${size}`,toolManager); + if (toolManager) { + toolManager.updateBrushIndicatorSize(size); + } +} + // 监听当前工具变化 - 参考 SelectionPanel 的实现方式 watch( () => props.activeTool, @@ -269,10 +286,11 @@ watch( // 如果面板未显示且有合适的目标对象,则显示面板 if (!visible.value) { visible.value = true; - closePanel.value = true + closePanel.value = true // 检查是否有可液化的对象 checkAndShowPanel(); } + setBrushIndicatorSize(size.value) } else { visible.value = false; // 切换到其他工具时隐藏面板 // 切换到其他工具,隐藏液化面板 @@ -444,16 +462,16 @@ function showPanel(event) { if (detail.layerStatus && detail.layerStatus.message) { console.log("液化操作提示:", detail.layerStatus.message); Modal.error({ - title: "错误提示", + title: t('Canvas.ErrorMessage'), content: detail.layerStatus.message, - okText: "确定", + okText: t('Canvas.confirm'), centered: true, }); } else { Modal.error({ - title: "错误提示", - content: "未选择有效图像或图层不适合液化操作", - okText: "确定", + title: t('Canvas.ErrorMessage'), + content: t('Canvas.LiquidationError'), + okText: t('Canvas.confirm'), centered: true, }); console.log("未选择有效图像或图层不适合液化操作"); @@ -1634,24 +1652,26 @@ function stopPressTimer() { color: #333; border: 1px solid rgba(0, 0, 0, 0.05); padding-bottom: 12px; - &.active{ - transform: translateY(100%); - > .btn{ - > i{ - transform: rotate(90deg); + &.active{ + transform: translateY(100%); + > .btn{ + > i{ + transform: rotate(90deg); + } } } -} > .btn{ - width: 100%; - text-align: center; - cursor: pointer; - - > i{ - font-size: 1.4rem; - display: block; - transform: rotate(270deg); - } + width: 100%; + text-align: center; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + + > i{ + font-size: 1.4rem; + transform: rotate(270deg); + } } } diff --git a/src/component/Canvas/CanvasEditor/components/SelectionPanel.vue b/src/component/Canvas/CanvasEditor/components/SelectionPanel.vue index 31bd74b6..2c2d0232 100644 --- a/src/component/Canvas/CanvasEditor/components/SelectionPanel.vue +++ b/src/component/Canvas/CanvasEditor/components/SelectionPanel.vue @@ -145,10 +145,10 @@
@@ -159,7 +159,7 @@
-

{{ $t("选择填充颜色") }}

+

{{ $t("Canvas.SelectFillColor") }}

@@ -168,10 +168,10 @@
diff --git a/src/component/Canvas/CanvasEditor/components/ToolsSidebar.vue b/src/component/Canvas/CanvasEditor/components/ToolsSidebar.vue index 2198665a..f944678a 100644 --- a/src/component/Canvas/CanvasEditor/components/ToolsSidebar.vue +++ b/src/component/Canvas/CanvasEditor/components/ToolsSidebar.vue @@ -16,8 +16,9 @@ const emit = defineEmits([ "zoom-out", "toggle-red-green-mode", "undo-redo-status-changed", + "trigger-library" ]); -const {t} = useI18n() +const {t,locale} = useI18n() const props = defineProps({ activeTool: String, minimapEnabled: { @@ -151,6 +152,13 @@ const normalToolsList = ref([ icon: { name: "CUpload", size: "26" }, class: "upload-btn", }, + { + id: "library", + title: t("LibraryPage.library"), + action: triggerLibrary, + icon: { name: "CLibrary", size: "26" }, + class: "library-btn", + }, { id: "addText", title: t("Canvas.AddText"), @@ -158,6 +166,17 @@ const normalToolsList = ref([ icon: { name: "CFont", size: "20" }, class: "text-btn", }, + { + id: "help", + title: t("Canvas.help"), + action: () => openTutorial(), + icon: { name: "CHelp", size: "30" }, + class: "text-btn", + style: { + 'position': 'absolute', + 'bottom': '0', + }, + }, ]); // 红绿图模式工具列表 @@ -228,10 +247,22 @@ function triggerImageUpload() { emit("trigger-image-upload"); } +function triggerLibrary() { + emit("trigger-library"); +} + function addText() { emit("add-text"); } +function openTutorial() { + if(locale.value == 'ENGLISH'){ + window.open('https://aida-user-manual.super.site/specific-scenarios/freely-sketching-in-canvas', '_blank'); + }else{ + window.open('https://aida-user-manual-chinese.super.site/%e4%bd%bf%e7%94%a8%e7%94%bb%e5%b8%83%e8%bf%9b%e8%a1%8c%e7%bc%96%e8%be%91 ', '_blank'); + } +} + function undo() { if (!canUndo.value) return; undoFun(); @@ -327,19 +358,25 @@ const handleToolClick = (tool) => { @change="fillColorChange" style="width: 0; height: 0; opacity: 0" /> +
+ - - - + + + + +
+
@@ -347,15 +384,25 @@ const handleToolClick = (tool) => { .tools-sidebar { display: flex; flex-direction: column; - gap: 1.0rem; padding: 1.5rem 1.0rem; border-right: .1rem solid #e0e0e0; background-color: #ffffff; user-select: none; min-width: 5.8rem; height: 100%; + padding-top: .5rem; + padding-bottom: 4rem; + /* overflow-y: auto; */ + /* overflow-x: hidden; */ +} +.tools-list{ + display: flex; + flex-direction: column; + gap: 0.5rem; + flex: 1; + overflow-y: auto; + overflow-x: hidden; } - .red-green-mode { background-color: #fff4f4; } diff --git a/src/component/Canvas/CanvasEditor/index.vue b/src/component/Canvas/CanvasEditor/index.vue index aa90942e..d46799e6 100644 --- a/src/component/Canvas/CanvasEditor/index.vue +++ b/src/component/Canvas/CanvasEditor/index.vue @@ -21,6 +21,9 @@ import { RedGreenModeManager } from "./managers/RedGreenModeManager"; import texturePresetManager from "./managers/brushes/TexturePresetManager"; import { BrushStore } from "./store/BrushStore"; import cuowuImg from '@/assets/images/homePage/cuowu.svg' +import { Https } from "@/tool/https"; +import SelectImages from '@/component/common/SelectImages.vue' +import { UrlToFile } from '@/tool/util' // import { MinimapManager } from "./managers/minimap/MinimapManager"; @@ -52,6 +55,7 @@ const emit = defineEmits([ "trigger-red-green-mouseup", // 红绿图模式鼠标抬起事件 "changeCanvas", // 画布变更事件 "canvasInit", // 画布初始化事件 + "trigger-library", // 触发打开Library选择图片事件 ]); const props = defineProps({ @@ -101,6 +105,14 @@ const props = defineProps({ type: Boolean, default: false, // 是否显示固定图层 }, + isGeneral: { // 从generalMiniCanvas来的 + type: Boolean, + default:false + }, + isEdit: { // 从design点击喜欢过的图片,再点击顶部的编辑图标 + type: Boolean, + default: false + } }); // 引用和状态 @@ -569,7 +581,6 @@ function zoomOut() { x: canvasManager.canvas.width / 2, y: canvasManager.canvas.height / 2, }; - canvasManager.animateZoom(centerPoint, newZoom); } @@ -601,7 +612,7 @@ async function addLayer() { await layerManager.createLayer(t("Canvas.EmptyLayer")); } async function addTopLayer() { - await layerManager.createLayer("空图层", LayerType.EMPTY, { + await layerManager.createLayer(t("Canvas.EmptyLayer"), LayerType.EMPTY, { insertTop: true, }); } @@ -667,12 +678,20 @@ function deleteFun(){ function removeLayer(layerId) { // Check if this is the last layer - prevent deletion - if (layers.value.length <= 2) { - console.warn( + var isChild = false; + var parentLength = 0; + layers.value.forEach((layer) => { + if(layer.children.some(v => v.id == layerId)){ + isChild = true; + parentLength = layer.children.length; + } + }) + if(isChild && parentLength == 1 || layers.value.length <= 3){ + console.warn( "Cannot delete the last layer. At least one layer must remain." ); return; - } + } if (canvasManager && canvasManager.canvas) { const layerToRemove = layers.value.find((l) => l.id === layerId); @@ -727,6 +746,21 @@ function handleImageUpload(event) { }); } +const selectImages = ref(null); +const handleImageSelect = (data) => { + UrlToFile(data.url,data.name).then((file)=>{ + handleImageUpload({ target: { files: [file] } }) + }) +} +function triggerLibrary() { + // console.log('CanvasEditor', '打开收藏') + if (props.isGeneral || props.isEdit) { + selectImages.value.init() + } else { + emit("trigger-library"); + } +} + function handleAddText() { if (toolManager && canvasManager && canvasManager.canvas) { // 在画布中央创建文本 @@ -828,6 +862,7 @@ const changeCanvas = async (command) => { setTimeout(async ()=>{ const imageData = await canvasManager.exportImage({ restoreOpacityInRedGreen: true, // 恢复红绿图模式下的透明度 + isCropByBg:true, }); emit("trigger-red-green-mouseup", imageData); },100) @@ -1066,10 +1101,14 @@ defineExpose({ @zoom-in="zoomIn" @zoom-out="zoomOut" @undo-redo-status-changed="changeCanvas" + @trigger-library="triggerLibrary" > + - diff --git a/src/component/Canvas/CanvasEditor/managers/BrushIndicator.js b/src/component/Canvas/CanvasEditor/managers/BrushIndicator.js index dac4c65b..11383dad 100644 --- a/src/component/Canvas/CanvasEditor/managers/BrushIndicator.js +++ b/src/component/Canvas/CanvasEditor/managers/BrushIndicator.js @@ -1,4 +1,5 @@ import { fabric } from "fabric-with-all"; +import { OperationType } from "../utils/layerHelper"; /** * 笔刷指示器 @@ -104,9 +105,8 @@ export class BrushIndicator { this.canvas.freeDrawingBrush && this.canvas.freeDrawingBrush.type === "eraser"; - if (!isBrushMode && !isEraserMode) { - return; - } + const isLiquifyMode = this.canvas.toolId === OperationType.LIQUIFY;// 检查是否在液化模式 + if ([isBrushMode, isEraserMode, isLiquifyMode].every(v => !v)) return; let hasChanges = false; @@ -391,6 +391,9 @@ export class BrushIndicator { this._mouseEnterHandler = (e) => { if (this._shouldShowIndicator()) { this.show(e.e); + const currentVpt = this.canvas.viewportTransform; + this.staticCanvas.setViewportTransform([...currentVpt]); + this.staticCanvas.renderAll(); } }; @@ -471,9 +474,12 @@ export class BrushIndicator { * @returns {Boolean} 是否显示 */ _shouldShowIndicator() { - // 检查画布是否在绘图模式 - if (!this.canvas.isDrawingMode) return false; - + const isDrawingMode = this.canvas.isDrawingMode;// 检查画布是否在绘图模式 + const isLiquifyMode = this.canvas.toolId === OperationType.LIQUIFY;// 检查是否在液化模式 + // console.log(`笔刷指示器\n绘图模式:${isDrawingMode}\n液化模式:${isLiquifyMode}`) + // 检查画布是否在绘图模式OR液化模式 + if ([isDrawingMode, isLiquifyMode].every(v => !v)) return false; + // 检查是否有笔刷 if (!this.canvas.freeDrawingBrush) return false; diff --git a/src/component/Canvas/CanvasEditor/managers/ExportManager.js b/src/component/Canvas/CanvasEditor/managers/ExportManager.js index 0b474d05..14bca2bd 100644 --- a/src/component/Canvas/CanvasEditor/managers/ExportManager.js +++ b/src/component/Canvas/CanvasEditor/managers/ExportManager.js @@ -137,7 +137,9 @@ export class ExportManager { return await this._exportWithCanvasSize( objectsToExport, expPicType, - restoreOpacityInRedGreen + restoreOpacityInRedGreen, + isCropByBg, // 是否使用背景大小裁剪 + isEnhanceImg, // 是否是增强图片 ); } @@ -224,7 +226,7 @@ export class ExportManager { const objectsToExport = this._collectObjectsByLayerOrder( null, // 导出所有图层 isContainBg, - isContainFixed + isContainFixed, ); if (objectsToExport.length === 0) { @@ -549,7 +551,7 @@ export class ExportManager { return await this._exportWithCanvasSize( objectsToExport, expPicType, - restoreOpacityInRedGreen + restoreOpacityInRedGreen, ); } diff --git a/src/component/Canvas/CanvasEditor/managers/ToolManager.js b/src/component/Canvas/CanvasEditor/managers/ToolManager.js index 07ec3046..0a4d0f25 100644 --- a/src/component/Canvas/CanvasEditor/managers/ToolManager.js +++ b/src/component/Canvas/CanvasEditor/managers/ToolManager.js @@ -373,6 +373,8 @@ export class ToolManager { // 设置工具特定的状态 const tool = this.tools[toolId]; if (tool && typeof tool.setup === "function") { + console.log(`画布切换工具:${tool.name}(${toolId})`) + this.canvas.toolId = toolId; tool.setup(); } @@ -450,6 +452,7 @@ export class ToolManager { if (!this.canvas) return; this.canvas.isDrawingMode = false; this.canvas.selection = true; + } /** @@ -750,6 +753,7 @@ export class ToolManager { detail: panelDetail, }) ); + this._enableBrushIndicator(); } /** @@ -759,14 +763,14 @@ export class ToolManager { * @private */ _showRasterizeConfirmModal(isGroup, layerId) { - const title = "栅格化图层"; - const content = "需要先栅格化才能进行液化操作,是否立即栅格化?"; + const title = this.t("Canvas.RasterizedLayer"); + const content = this.t("Canvas.rasterizeImmediately"); Modal.confirm({ title, content, - okText: "确定栅格化", - cancelText: "取消", + okText: this.t("Canvas.ConfirmRasterization"), + cancelText: this.t("Canvas.close"), centered: true, icon: h("span", { style: "color: #faad14;" }, "⚠️"), onOk: () => { @@ -800,8 +804,8 @@ export class ToolManager { if (!this.commandManager || !this.layerManager) return; // 显示加载Modal const loadingModal = Modal.info({ - title: "正在栅格化", - content: "正在栅格化图层,请稍候...", + title: this.t('Canvas.beingRasterized'), + content: this.t('Canvas.waitRasterizing'), okButtonProps: { style: { display: "none" } }, centered: true, closable: false, @@ -823,16 +827,16 @@ export class ToolManager { if (result) { // 栅格化成功,启动液化 - message.success("图层已成功栅格化,可以进行液化操作"); + message.success(this.t('Canvas.successRasterizing')); this._startLiquify(result); this.setTool(OperationType.LIQUIFY); // 切换到液化工具 } else { // 栅格化失败 Modal.error({ - title: "栅格化失败", - content: "栅格化失败,无法进行液化操作", - okText: "确定", + title: this.t('Canvas.gridingFailed'), + content: this.t('Canvas.gridingFailedNoOperation'), + okText: this.t('Canvas.ok'), centered: true, }); } @@ -840,9 +844,9 @@ export class ToolManager { console.error("栅格化图层失败:", error); Modal.error({ - title: "栅格化错误", - content: `栅格化失败:${error.message}`, - okText: "确定", + title: this.t('Canvas.gridingError'), + content: `${this.t('Canvas.gridingFailed')}:${error.message}`, + okText: this.t('Canvas.ok'), centered: true, }); } finally { @@ -866,9 +870,9 @@ export class ToolManager { ); if (!layer) { Modal.error({ - title: "图层错误", - content: "图层不存在", - okText: "确定", + title: this.t('Canvas.LayerError'), + content: this.t('Canvas.LayerDoesNotExist'), + okText: this.t('Canvas.ok'), centered: true, }); return; @@ -880,9 +884,9 @@ export class ToolManager { // 背景图层使用 fabricObject (单数) if (!layer.fabricObject) { Modal.warning({ - title: "背景图层为空", - content: "背景图层为空,无法进行液化操作", - okText: "确定", + title: this.t('Canvas.backgroundEmpty'), + content: this.t('Canvas.backgroundEmptyNoLiquidation'), + okText: this.t('Canvas.ok'), centered: true, }); return; @@ -892,9 +896,9 @@ export class ToolManager { // 普通图层使用 fabricObjects (复数) if (!layer.fabricObjects || layer.fabricObjects.length === 0) { Modal.warning({ - title: "图层为空", - content: "图层为空,无法进行液化操作", - okText: "确定", + title: this.t('Canvas.layerEmpty'), + content: this.t('Canvas.layerEmptyNoLiquidation'), + okText: this.t('Canvas.ok'), centered: true, }); return; @@ -906,9 +910,9 @@ export class ToolManager { const liquifyManager = this.canvasManager?.liquifyManager; if (!liquifyManager) { Modal.error({ - title: "液化管理器错误", - content: "液化管理器未初始化", - okText: "确定", + title: this.t('Canvas.liqueficationManagerError'), + content: this.t('Canvas.liqueficationManagerErrorInitialized'), + okText: this.t('Canvas.ok'), centered: true, }); return; @@ -917,8 +921,8 @@ export class ToolManager { try { // 显示准备中的Modal const preparingModal = Modal.info({ - title: "准备液化环境", - content: "正在准备液化环境,请稍候...", + title: this.t('Canvas.liquefactionEnvironment'), + content: this.t('Canvas.liquefactionEnvironmentLoading'), okButtonProps: { style: { display: "none" } }, centered: true, closable: false, @@ -963,9 +967,9 @@ export class ToolManager { } catch (error) { console.error("启动液化工具失败:", error); Modal.error({ - title: "液化工具启动失败", - content: `启动液化工具失败:${error.message}`, - okText: "确定", + title: this.t('Canvas.LiqueficationFailed'), + content: `${this.t('Canvas.LiqueficationFailed')}:${error.message}`, + okText: this.t('Canvas.ok'), centered: true, }); } @@ -1041,7 +1045,7 @@ export class ToolManager { _createTextDirect(x, y, options = {}) { // 默认文本属性 const defaultOptions = { - text: "双击编辑文本", + text: this.t('Canvas.DoubleClickText'), fontFamily: "Arial", fontSize: 24, fontWeight: "normal", @@ -1188,6 +1192,7 @@ export class ToolManager { if (this.brushIndicator) { this.brushIndicator.dispose(); this.brushIndicator = null; + console.log("笔刷指示器已清理"); } // 移除文本编辑相关事件监听器 @@ -1381,6 +1386,7 @@ export class ToolManager { if (!this.brushIndicator) return; this.brushIndicator.updateSize(size); + console.log(`笔刷指示器大小已更新为: ${size}`); } /** @@ -1391,6 +1397,7 @@ export class ToolManager { if (!this.brushIndicator) return; this.brushIndicator.updateColor(color); + console.log(`笔刷指示器颜色已更新为: ${color}`); } /** @@ -1465,6 +1472,7 @@ export class ToolManager { OperationType.ERASER, OperationType.RED_BRUSH, OperationType.GREEN_BRUSH, + OperationType.LIQUIFY, ]; return brushTools.includes(currentTool); diff --git a/src/component/Canvas/CanvasEditor/managers/animation/AnimationManager.js b/src/component/Canvas/CanvasEditor/managers/animation/AnimationManager.js index b8ed055d..c9250732 100644 --- a/src/component/Canvas/CanvasEditor/managers/animation/AnimationManager.js +++ b/src/component/Canvas/CanvasEditor/managers/animation/AnimationManager.js @@ -173,7 +173,7 @@ export class AnimationManager { this._zoomAnimation = null; // 确保最终状态准确 - this._applyZoom(point, targetZoom, true); + // this._applyZoom(point, targetZoom, true); }, }; diff --git a/src/component/Canvas/CanvasEditor/managers/brushes/brushManager.js b/src/component/Canvas/CanvasEditor/managers/brushes/brushManager.js index 051659ab..15ae7463 100644 --- a/src/component/Canvas/CanvasEditor/managers/brushes/brushManager.js +++ b/src/component/Canvas/CanvasEditor/managers/brushes/brushManager.js @@ -65,6 +65,7 @@ export class BrushManager { description: "基础铅笔工具,适合精细线条绘制", t:this.t, category: this.t('Canvas.BasicBrushes'), + imgUrl:'./image/brush/pencil.jpg', }); // 注册材质笔刷 @@ -73,6 +74,7 @@ export class BrushManager { description: "使用纹理图片作为笔刷,支持缩放和透明度", t:this.t, category: this.t('Canvas.BasicBrushes'), + imgUrl:'./image/brush/texture.jpg', }); // 注册集成的笔刷类型 @@ -81,54 +83,63 @@ export class BrushManager { description: "使用纹理图片作为笔刷,支持缩放和透明度", t:this.t, category: this.t('Canvas.BasicBrushes'), + imgUrl:'./image/brush/crayon.jpg', }); brushRegistry.register("fur", FurBrush, { name: this.t("Canvas.Fur"), description: "使用纹理图片作为笔刷,支持缩放和透明度", t:this.t, category: this.t('Canvas.BasicBrushes'), + imgUrl:'./image/brush/fur.jpg', }); brushRegistry.register("ink", InkBrush, { name: this.t("Canvas.Ink"), description: "墨水笔刷,适合书写和绘图", t:this.t, category: this.t('Canvas.BasicBrushes'), + imgUrl:'./image/brush/ink.jpg', }); brushRegistry.register("", LongfurBrush, { name: this.t("Canvas.Longfur"), description: "长毛发笔刷,适合绘制动物毛皮、草或头发", t:this.t, category: this.t('Canvas.BasicBrushes'), + imgUrl:'./image/brush/longFur.jpg', }); brushRegistry.register("writing", WritingBrush, { name: this.t("Canvas.Writing"), description: "书法笔刷,模拟中国传统书法毛笔效果,具有笔锋和墨色变化", t:this.t, category: this.t('Canvas.BasicBrushes'), + imgUrl:'./image/brush/writing.jpg', }); brushRegistry.register("marker", MarkerBrush, { name: this.t("Canvas.Marker"), description: "马克笔笔刷,适合粗线条和填充", t:this.t, category: this.t('Canvas.BasicBrushes'), + imgUrl:'./image/brush/marker.jpg', }); brushRegistry.register("pen", CustomPenBrush, { name: this.t("Canvas.Pen"), description: "自定义钢笔笔刷,适合书写和绘图", t:this.t, category: this.t('Canvas.BasicBrushes'), + imgUrl:'./image/brush/pen.jpg', }); brushRegistry.register("ribbon", RibbonBrush, { name: this.t("Canvas.Ribbon"), description: "丝带笔刷,适合创建流动的丝带效果", t:this.t, category: this.t('Canvas.BasicBrushes'), + imgUrl:'./image/brush/ribbon.jpg', }); brushRegistry.register("shaded", ShadedBrush, { name: this.t("Canvas.Shaded"), description: "阴影笔刷,适合创建渐变和阴影效果", t:this.t, category: this.t('Canvas.BasicBrushes'), + imgUrl:'./image/brush/shaded.jpg', }); brushRegistry.register("spray", SprayBrush, { @@ -136,6 +147,7 @@ export class BrushManager { description: "模拟喷枪效果,创建散点效果", t:this.t, category: this.t('Canvas.BasicBrushes'), + imgUrl:'./image/brush/spray.jpg', }); // brushRegistry.register("sketchy", SketchyBrush); @@ -365,6 +377,7 @@ export class BrushManager { description: brushInfo.metadata.description || "", category: brushInfo.metadata.category || "默认", icon: brushInfo.metadata.icon || null, + imgUrl: brushInfo.metadata.imgUrl || null, })); } diff --git a/src/component/Canvas/CanvasEditor/managers/brushes/types/TextureBrush.js b/src/component/Canvas/CanvasEditor/managers/brushes/types/TextureBrush.js index ccdcece5..71ce6f17 100644 --- a/src/component/Canvas/CanvasEditor/managers/brushes/types/TextureBrush.js +++ b/src/component/Canvas/CanvasEditor/managers/brushes/types/TextureBrush.js @@ -1,6 +1,8 @@ import { BaseBrush } from "../BaseBrush"; import { fabric } from "fabric-with-all"; import texturePresetManager from "../TexturePresetManager"; +import i18n from "@/lang/index.ts"; +const {t} = i18n.global; /** * 纹理笔刷 @@ -468,12 +470,12 @@ export class TextureBrush extends BaseBrush { const textureProperties = [ { id: "textureSelector", - name: "材质选择", + name: t('Canvas.TextureSelector'), type: "texture-grid", defaultValue: this.selectedTextureId, options: textureOptions, - description: "选择要使用的纹理", - category: "纹理设置", + description: t('Canvas.selectTexture'), + category: t('Canvas.TextureSettings'), order: 100, hidden: allTextures.length === 0, }, diff --git a/src/component/Canvas/CanvasEditor/managers/events/CanvasEventManager.js b/src/component/Canvas/CanvasEditor/managers/events/CanvasEventManager.js index 8306e125..e906d467 100644 --- a/src/component/Canvas/CanvasEditor/managers/events/CanvasEventManager.js +++ b/src/component/Canvas/CanvasEditor/managers/events/CanvasEventManager.js @@ -706,7 +706,8 @@ export class CanvasEventManager { if (e.target.id) { // 如果该元素是分组图层的一部分,也更新分组图层的缩略图 if (e.target.parentId) { - setTimeout(() => this.updateLayerThumbnail(e.target.parentId), 50); + // setTimeout(() => this.updateLayerThumbnail(e.target.parentId), 50); + this.thumbnailManager.generateLayerThumbnail(e.target.parentId); } } } diff --git a/src/component/Canvas/CanvasEditor/managers/liquify/EnhancedLiquifyManager.js b/src/component/Canvas/CanvasEditor/managers/liquify/EnhancedLiquifyManager.js index 13a6015e..34e902dd 100644 --- a/src/component/Canvas/CanvasEditor/managers/liquify/EnhancedLiquifyManager.js +++ b/src/component/Canvas/CanvasEditor/managers/liquify/EnhancedLiquifyManager.js @@ -5,6 +5,8 @@ import { LiquifyWebGLManager } from "./LiquifyWebGLManager"; import { LiquifyCPUManager } from "./LiquifyCPUManager"; import { findLayerRecursively, LayerType } from "../../utils/layerHelper"; +import i18n from "@/lang/index.ts"; +const {t} = i18n.global; export class EnhancedLiquifyManager { /** @@ -20,13 +22,13 @@ export class EnhancedLiquifyManager { // 是否强制使用WebGL模式 forceWebGL: options.forceWebGL || false, // 网格大小 - gridSize: options.gridSize || 15, + gridSize: options.gridSize || 8, // 最大变形强度 - maxStrength: options.maxStrength || 100, + maxStrength: options.maxStrength || 200, // 平滑迭代次数 - smoothingIterations: options.smoothingIterations || 2, + smoothingIterations: options.smoothingIterations || 1, // 网格弹性因子 - relaxFactor: options.relaxFactor || 0.25, + relaxFactor: options.relaxFactor || 0.05, // WebGL网格精度 meshResolution: options.meshResolution || 64, }; @@ -599,7 +601,7 @@ export class EnhancedLiquifyManager { if (objectsToCheck.length === 0) { return { valid: false, - message: "图层为空,无法进行液化操作", + message: t('Canvas.layerEmptyNoLiquidation'), needsRasterization: false, isImage: false, isEmpty: true, diff --git a/src/component/Canvas/CanvasEditor/managers/liquify/LiquifyCPUManager.js b/src/component/Canvas/CanvasEditor/managers/liquify/LiquifyCPUManager.js index f7455e10..cb91dcae 100644 --- a/src/component/Canvas/CanvasEditor/managers/liquify/LiquifyCPUManager.js +++ b/src/component/Canvas/CanvasEditor/managers/liquify/LiquifyCPUManager.js @@ -3,1313 +3,1451 @@ * 修复版本 - 解决三角形网格失真问题,优化持续按压效果 */ 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.initialMouseX = 0; // 初始点击位置X - this.initialMouseY = 0; // 初始点击位置Y - this.currentMouseX = 0; // 当前鼠标位置X - this.currentMouseY = 0; // 当前鼠标位置Y - this.lastMouseX = 0; - this.lastMouseY = 0; - this.mouseMovementX = 0; - this.mouseMovementY = 0; - this.isFirstApply = true; // 标记是否是首次应用 - this.isDragging = false; // 标记是否正在拖拽 - this.dragDistance = 0; // 拖拽距离 - this.dragAngle = 0; // 拖拽角度 - - // 新增:持续按压相关状态 - this.pressStartTime = 0; // 按压开始时间 - this.pressDuration = 0; // 按压持续时间 - this.accumulatedRotation = 0; // 累积旋转角度(用于顺时针/逆时针) - this.accumulatedScale = 0; // 累积缩放量(用于捏合/展开) - this.lastApplyTime = 0; // 上次应用时间 - this.continuousApplyInterval = 50; // 持续应用间隔(毫秒) - this.isHolding = false; // 是否正在持续按压 - } - - 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, // 增大默认动力 - }; - } - - /** - * 开始液化操作(记录初始点) - * @param {Number} x 初始X坐标 - * @param {Number} y 初始Y坐标 - */ - startDeformation(x, y) { - this.initialMouseX = x; - this.initialMouseY = y; - this.currentMouseX = x; - this.currentMouseY = y; - this.lastMouseX = x; - this.lastMouseY = y; - this.isDragging = true; - this.isFirstApply = true; - this.dragDistance = 0; - this.dragAngle = 0; - - // 新增:初始化持续按压状态 - this.pressStartTime = Date.now(); - this.pressDuration = 0; - this.accumulatedRotation = 0; - this.accumulatedScale = 0; - this.lastApplyTime = this.pressStartTime; - this.isHolding = true; - - // 启动持续效果定时器(对于所有模式都支持持续按压) - this.startContinuousEffect(); - - console.log(`开始液化操作,初始点: (${x}, ${y})`); - } - - /** - * 结束液化操作 - */ - endDeformation() { - this.isDragging = false; - this.isFirstApply = true; - this.dragDistance = 0; - this.dragAngle = 0; - - // 新增:重置持续按压状态 - this.isHolding = false; - this.pressStartTime = 0; - this.pressDuration = 0; - this.accumulatedRotation = 0; - this.accumulatedScale = 0; - this.lastApplyTime = 0; - - // 停止持续效果定时器 - this.stopContinuousEffect(); - - console.log("结束液化操作"); - } - - // 新增:启动持续效果 - startContinuousEffect() { - this.stopContinuousEffect(); // 先停止已有的定时器 - - this.continuousTimer = setInterval(() => { - if (this.isHolding && this.initialized) { - // 更新持续时间 - this.pressDuration = Date.now() - this.pressStartTime; - - // 所有模式都支持持续效果 - this.applyContinuousDeformation(); - } - }, this.continuousApplyInterval); - } - - // 新增:停止持续效果 - stopContinuousEffect() { - if (this.continuousTimer) { - clearInterval(this.continuousTimer); - this.continuousTimer = null; - } - } - - /** - * 稳定的旋转衰减函数 - 确保内圈快外圈慢,保持纹理连续性 - * @param {number} t 归一化距离 (0-1) - * @returns {number} 衰减因子 (0-1) - */ - _stableRotationFalloff(t) { - if (t >= 1.0) return 0; - if (t <= 0) return 1; - - // 使用反向二次函数:内圈(t=0)时值为1,外圈(t=1)时值为0 - // 这确保了内圈旋转最快,外圈旋转最慢 - const inverseFalloff = 1 - t; - - // 使用平滑的二次衰减,确保内圈效果强,外圈效果弱 - const quadraticFalloff = inverseFalloff * inverseFalloff; - - // 添加轻微的线性分量,确保过渡平滑 - const linearFalloff = inverseFalloff; - - // 混合二次和线性衰减,70%二次衰减 + 30%线性衰减 - return quadraticFalloff * 0.7 + linearFalloff * 0.3; - } - - /** - * 基于test-liquify-enhanced.html的旋转算法 - 像素级实现 - * @param {number} centerX 旋转中心X坐标 - * @param {number} centerY 旋转中心Y坐标 - * @param {number} radius 影响半径 - * @param {number} strength 强度 - * @param {boolean} isClockwise 是否顺时针旋转 - */ - _applyEnhancedRotationDeformation(centerX, centerY, radius, strength, isClockwise) { - if (!this.currentImageData) return; - - const data = this.currentImageData.data; - const width = this.currentImageData.width; - const height = this.currentImageData.height; - const tempData = new Uint8ClampedArray(data); - - // 计算旋转角度 - 基于test-liquify-enhanced.html的算法 - const { pressure, power } = this.params; - const timeFactor = Math.min(this.pressDuration / 1000, 5.0); - const baseRotationSpeed = 0.02; // 使用与测试文件相同的速度 - const rotationAngle = - (isClockwise ? 1 : -1) * baseRotationSpeed * pressure * power * (1.0 + timeFactor * 0.5); - - // 累积旋转角度 - 关键:这确保了持续旋转效果 - this.accumulatedRotation += rotationAngle; - - const processRadius = Math.min(radius, Math.min(width, height) / 2); - const minX = Math.max(0, Math.floor(centerX - processRadius)); - const maxX = Math.min(width, Math.ceil(centerX + processRadius)); - const minY = Math.max(0, Math.floor(centerY - processRadius)); - const maxY = Math.min(height, Math.ceil(centerY + processRadius)); - - // 遍历影响区域内的每个像素 - for (let y = minY; y < maxY; y++) { - for (let x = minX; x < maxX; x++) { - const dx = x - centerX; - const dy = y - centerY; - const distance = Math.sqrt(dx * dx + dy * dy); - - if (distance < processRadius && distance > 0.1) { - // 距离衰减:内圈快,外圈慢 - 与测试文件算法一致 - const normalizedDistance = distance / processRadius; - const falloff = Math.pow(1 - normalizedDistance, 2); // 二次衰减 - - // 计算旋转后的源位置 - 关键算法 - const angle = Math.atan2(dy, dx); - const newAngle = angle + this.accumulatedRotation * falloff; - - const sourceX = centerX + Math.cos(newAngle) * distance; - const sourceY = centerY + Math.sin(newAngle) * distance; - - // 双线性插值采样 - 确保像素连续性 - const color = this._bilinearSample(tempData, width, height, sourceX, sourceY); - - if (color) { - const targetIdx = (y * width + x) * 4; - data[targetIdx] = color[0]; - data[targetIdx + 1] = color[1]; - data[targetIdx + 2] = color[2]; - data[targetIdx + 3] = color[3]; - } - } - } - } - - return true; - } - - /** - * 基于test-liquify-enhanced.html的捏合/展开算法 - * @param {number} centerX 中心X坐标 - * @param {number} centerY 中心Y坐标 - * @param {number} radius 影响半径 - * @param {number} strength 强度 - * @param {boolean} isPinch 是否为捏合模式 - */ - _applyEnhancedPinchDeformation(centerX, centerY, radius, strength, isPinch) { - if (!this.currentImageData) return; - - const data = this.currentImageData.data; - const width = this.currentImageData.width; - const height = this.currentImageData.height; - const tempData = new Uint8ClampedArray(data); - - // 计算时间相关的缩放因子 - 基于test-liquify-enhanced.html - const timeFactor = Math.min(this.pressDuration / 1000, 3.0); - const baseScaleFactor = isPinch ? -0.01 : 0.01; - const scaleFactor = baseScaleFactor * (1.0 + timeFactor * 0.5); - - this.accumulatedScale += scaleFactor; - - const processRadius = Math.min(radius, Math.min(width, height) / 2); - const minX = Math.max(0, Math.floor(centerX - processRadius)); - const maxX = Math.min(width, Math.ceil(centerX + processRadius)); - const minY = Math.max(0, Math.floor(centerY - processRadius)); - const maxY = Math.min(height, Math.ceil(centerY + processRadius)); - - for (let y = minY; y < maxY; y++) { - for (let x = minX; x < maxX; x++) { - const dx = x - centerX; - const dy = y - centerY; - const distance = Math.sqrt(dx * dx + dy * dy); - - if (distance < processRadius && distance > 0.1) { - const normalizedDistance = distance / processRadius; - const falloff = 1 - normalizedDistance * normalizedDistance; - - // 计算缩放后的位置 - const scale = 1 + this.accumulatedScale * falloff; - const sourceX = centerX + dx * scale; - const sourceY = centerY + dy * scale; - - // 双线性插值采样 - const color = this._bilinearSample(tempData, width, height, sourceX, sourceY); - - if (color) { - const targetIdx = (y * width + x) * 4; - data[targetIdx] = color[0]; - data[targetIdx + 1] = color[1]; - data[targetIdx + 2] = color[2]; - data[targetIdx + 3] = color[3]; - } - } - } - } - - return true; - } - - /** - * 基于test-liquify-enhanced.html的推拉算法 - * @param {number} centerX 中心X坐标 - * @param {number} centerY 中心Y坐标 - * @param {number} radius 影响半径 - * @param {number} strength 强度 - */ - _applyEnhancedPushDeformation(centerX, centerY, radius, strength) { - if (!this.currentImageData) return; - - const data = this.currentImageData.data; - const width = this.currentImageData.width; - const height = this.currentImageData.height; - const tempData = new Uint8ClampedArray(data); - - // 计算推拉方向 - const deltaX = this.currentMouseX - this.initialMouseX; - const deltaY = this.currentMouseY - this.initialMouseY; - const dragLength = Math.sqrt(deltaX * deltaX + deltaY * deltaY); - - const processRadius = Math.min(radius, Math.min(width, height) / 2); - const minX = Math.max(0, Math.floor(centerX - processRadius)); - const maxX = Math.min(width, Math.ceil(centerX + processRadius)); - const minY = Math.max(0, Math.floor(centerY - processRadius)); - const maxY = Math.min(height, Math.ceil(centerY + processRadius)); - - if (dragLength === 0) { - // 如果没有拖拽,在持续按压时执行基础的外推效果 - if (this.isHolding) { - const timeFactor = Math.min(this.pressDuration / 1000, 2.0); - const pushStrength = strength * timeFactor * 0.3; - - for (let y = minY; y < maxY; y++) { - for (let x = minX; x < maxX; x++) { - const dx = x - centerX; - const dy = y - centerY; - const distance = Math.sqrt(dx * dx + dy * dy); - - if (distance < processRadius && distance > 0.1) { - const normalizedDistance = distance / processRadius; - const falloff = 1 - normalizedDistance * normalizedDistance; - const factor = falloff * pushStrength; - - // 径向外推效果 - const pushX = (dx / distance) * factor; - const pushY = (dy / distance) * factor; - - const sourceX = x - pushX; - const sourceY = y - pushY; - - const color = this._bilinearSample(tempData, width, height, sourceX, sourceY); - - if (color) { - const targetIdx = (y * width + x) * 4; - data[targetIdx] = color[0]; - data[targetIdx + 1] = color[1]; - data[targetIdx + 2] = color[2]; - data[targetIdx + 3] = color[3]; - } - } - } - } - } - return true; - } - - // 有拖拽时的推拉效果 - const dirX = deltaX / dragLength; - const dirY = deltaY / dragLength; - - for (let y = minY; y < maxY; y++) { - for (let x = minX; x < maxX; x++) { - const dx = x - centerX; - const dy = y - centerY; - const distance = Math.sqrt(dx * dx + dy * dy); - - if (distance < processRadius && distance > 0.1) { - const normalizedDistance = distance / processRadius; - const falloff = 1 - normalizedDistance * normalizedDistance; - const factor = falloff * strength; - - const offsetX = dirX * factor * Math.min(dragLength * 0.3, 15); - const offsetY = dirY * factor * Math.min(dragLength * 0.3, 15); - - const sourceX = x - offsetX; - const sourceY = y - offsetY; - - const color = this._bilinearSample(tempData, width, height, sourceX, sourceY); - - if (color) { - const targetIdx = (y * width + x) * 4; - data[targetIdx] = color[0]; - data[targetIdx + 1] = color[1]; - data[targetIdx + 2] = color[2]; - data[targetIdx + 3] = color[3]; - } - } - } - } - - return true; - } - - /** - * 优化的持续变形效果处理 - 使用增强算法 - */ - applyContinuousDeformation() { - if (!this.isHolding || !this.initialized || !this.currentImageData) return; - - const { size, pressure, power } = this.params; - const mode = this.currentMode; - const radius = size; - const x = this.initialMouseX; - const y = this.initialMouseY; - const strength = pressure * power; - - // 根据模式使用相应的增强算法 - switch (mode) { - case this.modes.CLOCKWISE: - this._applyEnhancedRotationDeformation(x, y, radius, strength, true); - break; - - case this.modes.COUNTERCLOCKWISE: - this._applyEnhancedRotationDeformation(x, y, radius, strength, false); - break; - - case this.modes.PINCH: - this._applyEnhancedPinchDeformation(x, y, radius, strength, true); - break; - - case this.modes.EXPAND: - this._applyEnhancedPinchDeformation(x, y, radius, strength, false); - break; - - case this.modes.PUSH: - this._applyEnhancedPushDeformation(x, y, radius, strength); - break; - - default: { - // 对于其他模式,使用原有的网格算法 - if (!this.mesh) return; - - const baseStrength = (pressure * power * this.config.maxStrength) / 100; - const timeFactor = Math.min(this.pressDuration / 1000, 4.0); - const finalStrength = baseStrength * (1.0 + timeFactor * 0.5); - - this._applyDeformation(x, y, radius, finalStrength, mode, this.params.distortion); - - if (this.config.smoothingIterations > 0) { - this._lightSmoothing(); - } - - return this._applyMeshToImage(); - } - } - - // 对于像素算法,直接返回当前图像数据 - return this.currentImageData; - } - - /** - * 应用液化变形 - 主要入口,集成增强算法 - */ - // applyDeformation(x, y) { - // if (!this.initialized || !this.originalImageData) { - // console.warn("液化管理器未初始化或缺少必要数据"); - // return this.currentImageData; - // } - - // // 更新鼠标位置 - // this.currentMouseX = x; - // this.currentMouseY = y; - - // // 计算拖拽参数 - // const deltaX = this.currentMouseX - this.initialMouseX; - // const deltaY = this.currentMouseY - this.initialMouseY; - // this.dragDistance = Math.sqrt(deltaX * deltaX + deltaY * deltaY); - // this.dragAngle = Math.atan2(deltaY, deltaX); - - // // 获取当前参数 - // const { size, pressure, power } = this.params; - // const mode = this.currentMode; - // const radius = size; - // const strength = pressure * power; - - // // 根据模式选择算法 - // const pixelModes = [ - // this.modes.CLOCKWISE, - // this.modes.COUNTERCLOCKWISE, - // this.modes.PINCH, - // this.modes.EXPAND, - // this.modes.PUSH, - // ]; - - // if (pixelModes.includes(mode)) { - // // 使用增强的像素算法 - // switch (mode) { - // case this.modes.CLOCKWISE: - // this._applyEnhancedRotationDeformation(x, y, radius, strength, false); - // break; - // case this.modes.COUNTERCLOCKWISE: - // this._applyEnhancedRotationDeformation(x, y, radius, strength, true); - // break; - // case this.modes.PINCH: - // this._applyEnhancedPinchDeformation(x, y, radius, strength, true); - // break; - // case this.modes.EXPAND: - // this._applyEnhancedPinchDeformation(x, y, radius, strength, false); - // break; - // case this.modes.PUSH: - // this._applyEnhancedPushDeformation(x, y, radius, strength); - // break; - // } - - // // 更新最后应用时间 - // this.lastApplyTime = Date.now(); - // this.isFirstApply = false; - - // return this.currentImageData; - // } else { - // // 使用原有的网格算法处理其他模式 - // if (!this.mesh) { - // console.warn("网格未初始化"); - // return this.currentImageData; - // } - - // const finalStrength = (strength * this.config.maxStrength) / 100; - - // // 应用变形 - // this._applyDeformation( - // x, - // y, - // radius, - // finalStrength, - // mode, - // this.params.distortion, - // ); - - // // 平滑处理 - // if (this.config.smoothingIterations > 0) { - // this._smoothMesh(); - // } - - // // 更新图像数据 - // const result = this._applyMeshToImage(); - - // // 更新最后应用时间 - // this.lastApplyTime = Date.now(); - // this.isFirstApply = false; - - // return result; - // } - // } - - /** - * 双线性插值采样 - 用于像素级算法 - * @param {Uint8ClampedArray} data 图像数据 - * @param {number} width 图像宽度 - * @param {number} height 图像高度 - * @param {number} x X坐标 - * @param {number} y Y坐标 - * @returns {Array|null} RGBA颜色值数组或null - */ - _bilinearSample(data, width, height, x, y) { - if (x < 0 || x >= width - 1 || y < 0 || y >= height - 1) { - return null; - } - - const x1 = Math.floor(x); - const y1 = Math.floor(y); - const x2 = x1 + 1; - const y2 = y1 + 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] - ), - ]; - } - - /** - * 应用变形到网格 - 原有的网格算法(用于其他模式) - */ - _applyDeformation(x, y, radius, strength, mode, distortion) { - if (!this.mesh) return; - - const points = this.mesh.deformedPoints; - const originalPoints = this.mesh.originalPoints; - - // 性能优化:只计算影响范围内的网格点 - const affectedPoints = this._getAffectedPoints(x, y, radius); - - for (const pointInfo of affectedPoints) { - const { index: i, point, originalPoint, distance } = pointInfo; - - if (distance > 0) { - // 使用优化的衰减函数 - const normalizedDistance = distance / radius; - const factor = this._optimizedFalloff(normalizedDistance) * strength; - - switch (mode) { - case this.modes.CRYSTAL: { - // 水晶模式 - const dx = point.x - x; - const dy = point.y - y; - const crystalAngle = Math.atan2(dy, dx); - const crystalRadius = normalizedDistance; - - const baseDistortion = Math.max(distortion, 0.3); - const timeFactor = Math.min(this.pressDuration / 1000, 2.0); - const timeEnhancedDistortion = baseDistortion * (1.0 + timeFactor * 0.3); - - const wave1 = Math.sin(crystalAngle * 8 + this.pressDuration * 0.005) * 0.6; - const wave2 = Math.cos(crystalAngle * 12 + this.pressDuration * 0.003) * 0.4; - const waveAngle = crystalAngle + (wave1 + wave2) * timeEnhancedDistortion; - - const radialMod = - 1 + Math.sin(crystalRadius * Math.PI * 2 + this.pressDuration * 0.002) * 0.3; - const modDistance = distance * radialMod; - - const crystalX = x + Math.cos(waveAngle) * modDistance; - const crystalY = y + Math.sin(waveAngle) * modDistance; - - const crystalFactor = factor * timeEnhancedDistortion * 0.7; - point.x += (crystalX - point.x) * crystalFactor; - point.y += (crystalY - point.y) * crystalFactor; - break; - } - - case this.modes.EDGE: { - // 边缘模式 - const dx = point.x - x; - const dy = point.y - y; - const edgeAngle = Math.atan2(dy, dx); - const edgeRadius = normalizedDistance; - - const baseEdgeDistortion = Math.max(distortion, 0.5); - const timeFactor = Math.min(this.pressDuration / 1000, 2.5); - const timeEnhancedDistortion = baseEdgeDistortion * (1.0 + timeFactor * 0.4); - - const edgeWave = - Math.sin(edgeRadius * Math.PI * 4 + this.pressDuration * 0.004) * - Math.cos(edgeAngle * 6 + this.pressDuration * 0.002); - const perpAngle = edgeAngle + Math.PI / 2; - - const edgeFactor = edgeWave * factor * timeEnhancedDistortion * 0.5; - 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.2; - point.x += (originalPoint.x - point.x) * restoreFactor; - point.y += (originalPoint.y - point.y) * restoreFactor; - break; - } - } - } - } - } - - /** - * 获取受影响的网格点(范围优化) - */ - _getAffectedPoints(centerX, centerY, radius) { - const { cols, rows, gridSize } = this.mesh; - const points = this.mesh.deformedPoints; - const originalPoints = this.mesh.originalPoints; - const affectedPoints = []; - - // 计算影响范围的网格边界 - const minGridX = Math.max(0, Math.floor((centerX - radius) / gridSize)); - const maxGridX = Math.min(cols, Math.ceil((centerX + radius) / gridSize)); - const minGridY = Math.max(0, Math.floor((centerY - radius) / gridSize)); - const maxGridY = Math.min(rows, Math.ceil((centerY + radius) / gridSize)); - - // 只遍历影响范围内的网格点 - for (let gridY = minGridY; gridY <= maxGridY; gridY++) { - for (let gridX = minGridX; gridX <= maxGridX; gridX++) { - const index = gridY * (cols + 1) + gridX; - if (index < points.length) { - const point = points[index]; - const originalPoint = originalPoints[index]; - const dx = point.x - centerX; - const dy = point.y - centerY; - const distance = Math.sqrt(dx * dx + dy * dy); - - // 只包含在影响半径内的点 - if (distance <= radius) { - affectedPoints.push({ - index, - point, - originalPoint, - distance, - dx, - dy, - }); - } - } - } - } - - return affectedPoints; - } - - _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; - } - } - } - - /** - * 专门为旋转模式优化的网格平滑 - */ - _lightSmoothing() { - const { rows, cols } = this.mesh; - const points = this.mesh.deformedPoints; - const tempPoints = points.map((p) => ({ x: p.x, y: p.y })); - - // 只进行一次轻微平滑 - 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 lightRelaxFactor = this.config.relaxFactor * 0.3; - tempPoints[idx].x += (centerX - points[idx].x) * lightRelaxFactor; - tempPoints[idx].y += (centerY - points[idx].y) * lightRelaxFactor; - } - } - - for (let i = 0; i < points.length; i++) { - points[i].x = tempPoints[i].x; - points[i].y = tempPoints[i].y; - } - } - - /** - * 使用更优化的衰减函数 - * @param {number} t 归一化距离 (0-1) - * @returns {number} 衰减因子 (0-1) - */ - _optimizedFalloff(t) { - if (t >= 1.0) return 0; - - // 对于旋转模式,使用专门的衰减函数 - if ( - this.currentMode === this.modes.CLOCKWISE || - this.currentMode === this.modes.COUNTERCLOCKWISE - ) { - return this._stableRotationFalloff(t); // 修复函数名 - } - - // 其他模式使用原来的衰减函数 - const smoothT = 1 - t; - - // 多项式衰减 + 指数衰减的组合 - const polynomial = smoothT * smoothT * (3 - 2 * smoothT); // 平滑阶梯函数 - const exponential = Math.exp(-t * 2); // 指数衰减 - - // 组合两种衰减方式,在不同区域有不同特性 - const weight = Math.cos(t * Math.PI * 0.5); // 权重函数 - - return polynomial * weight + exponential * (1 - weight); - } - - _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; - } - - // 对于批量处理,模拟连续的拖拽操作 - if (positions.length > 0) { - // 使用第一个位置作为初始点 - this.startDeformation(positions[0].x, positions[0].y); - - // 逐个应用每个位置的变形 - positions.forEach((pos, index) => { - if (index === 0) return; // 跳过第一个,因为已经作为初始点 - - // 更新当前位置并应用变形 - this.currentMouseX = pos.x; - this.currentMouseY = pos.y; - - // 重新计算拖拽参数 - const deltaX = this.currentMouseX - this.initialMouseX; - const deltaY = this.currentMouseY - this.initialMouseY; - this.dragDistance = Math.sqrt(deltaX * deltaX + deltaY * deltaY); - this.dragAngle = Math.atan2(deltaY, deltaX); - - const { size, pressure, distortion, power } = this.params; - const mode = this.currentMode; - const radius = size * 0.8; - - // 根据推拉模式和拖拽距离动态调整强度 - let strength; - if (mode === this.modes.PUSH) { - const baseStrength = (pressure * power * this.config.maxStrength) / 100; - const distanceFactor = Math.min(this.dragDistance / radius, 2.0); - strength = baseStrength * distanceFactor * 0.3; // 批量处理时降低强度 - } else { - strength = (pressure * power * this.config.maxStrength) / 100; - } - - this._applyDeformation(pos.x, pos.y, radius, strength, mode, distortion); - }); - - // 结束拖拽操作 - this.endDeformation(); - } - - 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; - - // 限制偏移量,防止过度扭曲 - const maxOffset = gridSize * 0.5; - const limitedOffsetX = Math.max(-maxOffset, Math.min(maxOffset, offsetX)); - const limitedOffsetY = Math.max(-maxOffset, Math.min(maxOffset, offsetY)); - - return { - x: Math.max(0, Math.min(this.mesh.width - 1, x - limitedOffsetX)), - y: Math.max(0, Math.min(this.mesh.height - 1, y - limitedOffsetY)), - }; - } - - _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.initialMouseX = 0; - this.initialMouseY = 0; - this.currentMouseX = 0; - this.currentMouseY = 0; - this.lastMouseX = 0; - this.lastMouseY = 0; - this.mouseMovementX = 0; - this.mouseMovementY = 0; - this.isFirstApply = true; - this.isDragging = false; - this.dragDistance = 0; - this.dragAngle = 0; - - // 新增:重置持续按压状态 - this.isHolding = false; - this.pressStartTime = 0; - this.pressDuration = 0; - this.accumulatedRotation = 0; - this.accumulatedScale = 0; - this.lastApplyTime = 0; - - this.deformHistory = []; - return true; - } - - // 新增:获取持续按压状态信息 - getHoldingInfo() { - return { - isHolding: this.isHolding, - pressDuration: this.pressDuration, - accumulatedRotation: this.accumulatedRotation, - accumulatedScale: this.accumulatedScale, - pressStartTime: this.pressStartTime, - }; - } - - /** - * 应用液化变形 - 主要的公共接口方法 - * @param {Number} x X坐标 - * @param {Number} y Y坐标 - * @returns {ImageData} 变形后的图像数据 - */ - applyDeformation(x, y) { - if (!this.initialized || !this.mesh || !this.originalImageData) { - console.warn("液化管理器未初始化或缺少必要数据"); - return this.currentImageData; - } - - // 更新鼠标位置 - this.currentMouseX = x; - this.currentMouseY = y; - - // 计算拖拽参数 - const deltaX = this.currentMouseX - this.initialMouseX; - const deltaY = this.currentMouseY - this.initialMouseY; - this.dragDistance = Math.sqrt(deltaX * deltaX + deltaY * deltaY); - this.dragAngle = Math.atan2(deltaY, deltaX); - - // 获取当前参数 - const { size, pressure, power } = this.params; - const mode = this.currentMode; - const radius = size; - const strength = pressure * power; - - // 根据模式选择算法 - const pixelModes = [ - this.modes.CLOCKWISE, - this.modes.COUNTERCLOCKWISE, - this.modes.PINCH, - this.modes.EXPAND, - this.modes.PUSH, - ]; - - if (pixelModes.includes(mode)) { - // 使用增强的像素算法 - switch (mode) { - case this.modes.CLOCKWISE: - this._applyEnhancedRotationDeformation(x, y, radius, strength, false); - break; - case this.modes.COUNTERCLOCKWISE: - this._applyEnhancedRotationDeformation(x, y, radius, strength, true); - break; - case this.modes.PINCH: - this._applyEnhancedPinchDeformation(x, y, radius, strength, true); - break; - case this.modes.EXPAND: - this._applyEnhancedPinchDeformation(x, y, radius, strength, false); - break; - case this.modes.PUSH: - this._applyEnhancedPushDeformation(x, y, radius, strength); - break; - } - - // 更新最后应用时间 - this.lastApplyTime = Date.now(); - this.isFirstApply = false; - - return this.currentImageData; - } else { - // 使用原有的网格算法处理其他模式 - if (!this.mesh) { - console.warn("网格未初始化"); - return this.currentImageData; - } - - const finalStrength = (strength * this.config.maxStrength) / 100; - - // 应用变形 - this._applyDeformation(x, y, radius, finalStrength, mode, this.params.distortion); - - // 平滑处理 - if (this.config.smoothingIterations > 0) { - this._smoothMesh(); - } - - // 更新图像数据 - const result = this._applyMeshToImage(); - - // 更新最后应用时间 - this.lastApplyTime = Date.now(); - this.isFirstApply = false; - - return result; - } - } - - getCurrentImageData() { - return this.currentImageData; - } - - destroy() { - // 停止持续效果定时器 - this.stopContinuousEffect(); - - this.originalImageData = null; - this.currentImageData = null; - this.mesh = null; - this.deformHistory = []; - this.initialized = false; - - // 清理拖拽状态 - this.initialMouseX = 0; - this.initialMouseY = 0; - this.currentMouseX = 0; - this.currentMouseY = 0; - this.lastMouseX = 0; - this.lastMouseY = 0; - this.mouseMovementX = 0; - this.mouseMovementY = 0; - this.isFirstApply = true; - this.isDragging = false; - this.dragDistance = 0; - this.dragAngle = 0; - - // 新增:清理持续按压状态 - this.isHolding = false; - this.pressStartTime = 0; - this.pressDuration = 0; - this.accumulatedRotation = 0; - this.accumulatedScale = 0; - this.lastApplyTime = 0; - } - - /** - * 释放资源 - 别名方法,与其他管理器保持一致 - */ - dispose() { - this.destroy(); - } + constructor(options = {}) { + this.config = { + gridSize: 8, // 稍微增大网格提高性能 + maxStrength: 200, // 适度降低最大强度 + smoothingIterations: 1, // 增加平滑处理 + relaxFactor: 0.05, // 适度松弛 + sharpenAmount: 0.3, // 添加锐化强度参数 + ...options, + }; + console.log("CPU版本的液化管理器config",this.config); + + this.params = { + size: 60, // 增大默认尺寸 + pressure: 0.6, // 增大默认压力 + distortion: 0, + power: 0.7, // 增大默认动力 + }; + + 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.initialMouseX = 0; // 初始点击位置X + this.initialMouseY = 0; // 初始点击位置Y + this.currentMouseX = 0; // 当前鼠标位置X + this.currentMouseY = 0; // 当前鼠标位置Y + this.lastMouseX = 0; + this.lastMouseY = 0; + this.mouseMovementX = 0; + this.mouseMovementY = 0; + this.isFirstApply = true; // 标记是否是首次应用 + this.isDragging = false; // 标记是否正在拖拽 + this.dragDistance = 0; // 拖拽距离 + this.dragAngle = 0; // 拖拽角度 + + // 新增:持续按压相关状态 + this.pressStartTime = 0; // 按压开始时间 + this.pressDuration = 0; // 按压持续时间 + this.accumulatedRotation = 0; // 累积旋转角度(用于顺时针/逆时针) + this.accumulatedScale = 0; // 累积缩放量(用于捏合/展开) + this.lastApplyTime = 0; // 上次应用时间 + this.continuousApplyInterval = 50; // 持续应用间隔(毫秒) + this.isHolding = false; // 是否正在持续按压 + } + + 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 === 'sharpness') { + this.config.sharpenAmount = Math.max(0, Math.min(1, value)); + return true; + } + if (param in this.params) { + this.params[param] = value; + return true; + } + return false; + } + + getParams() { + return { ...this.params, sharpness: this.config.sharpenAmount, }; + } + // 添加清晰度控制方法 + setSharpness(amount) { + this.config.sharpenAmount = Math.max(0, Math.min(1, amount)); + return this; + } + resetParams() { + this.params = { + size: 60, // 增大默认尺寸 + pressure: 0.6, // 增大默认压力 + distortion: 0, + power: 0.7, // 增大默认动力 + }; + } + + /** + * 开始液化操作(记录初始点) + * @param {Number} x 初始X坐标 + * @param {Number} y 初始Y坐标 + */ + startDeformation(x, y) { + this.initialMouseX = x; + this.initialMouseY = y; + this.currentMouseX = x; + this.currentMouseY = y; + this.lastMouseX = x; + this.lastMouseY = y; + this.isDragging = true; + this.isFirstApply = true; + this.dragDistance = 0; + this.dragAngle = 0; + + // 新增:初始化持续按压状态 + this.pressStartTime = Date.now(); + this.pressDuration = 0; + this.accumulatedRotation = 0; + this.accumulatedScale = 0; + this.lastApplyTime = this.pressStartTime; + this.isHolding = true; + + // 启动持续效果定时器(对于所有模式都支持持续按压) + this.startContinuousEffect(); + + console.log(`开始液化操作,初始点: (${x}, ${y})`); + } + + /** + * 结束液化操作 + */ + endDeformation() { + this.isDragging = false; + this.isFirstApply = true; + this.dragDistance = 0; + this.dragAngle = 0; + + // 新增:重置持续按压状态 + this.isHolding = false; + this.pressStartTime = 0; + this.pressDuration = 0; + this.accumulatedRotation = 0; + this.accumulatedScale = 0; + this.lastApplyTime = 0; + + // 停止持续效果定时器 + this.stopContinuousEffect(); + + console.log("结束液化操作"); + } + + // 新增:启动持续效果 + startContinuousEffect() { + this.stopContinuousEffect(); // 先停止已有的定时器 + + this.continuousTimer = setInterval(() => { + if (this.isHolding && this.initialized) { + // 更新持续时间 + this.pressDuration = Date.now() - this.pressStartTime; + + // 所有模式都支持持续效果 + this.applyContinuousDeformation(); + } + }, this.continuousApplyInterval); + } + + // 新增:停止持续效果 + stopContinuousEffect() { + if (this.continuousTimer) { + clearInterval(this.continuousTimer); + this.continuousTimer = null; + } + } + + /** + * 稳定的旋转衰减函数 - 确保内圈快外圈慢,保持纹理连续性 + * @param {number} t 归一化距离 (0-1) + * @returns {number} 衰减因子 (0-1) + */ + _stableRotationFalloff(t) { + if (t >= 1.0) return 0; + if (t <= 0) return 1; + + // 使用反向二次函数:内圈(t=0)时值为1,外圈(t=1)时值为0 + // 这确保了内圈旋转最快,外圈旋转最慢 + const inverseFalloff = 1 - t; + + // 使用平滑的二次衰减,确保内圈效果强,外圈效果弱 + const quadraticFalloff = inverseFalloff * inverseFalloff; + + // 添加轻微的线性分量,确保过渡平滑 + const linearFalloff = inverseFalloff; + + // 混合二次和线性衰减,70%二次衰减 + 30%线性衰减 + return quadraticFalloff * 0.7 + linearFalloff * 0.3; + } + + /** + * 基于test-liquify-enhanced.html的旋转算法 - 像素级实现 + * @param {number} centerX 旋转中心X坐标 + * @param {number} centerY 旋转中心Y坐标 + * @param {number} radius 影响半径 + * @param {number} strength 强度 + * @param {boolean} isClockwise 是否顺时针旋转 + */ + _applyEnhancedRotationDeformation(centerX, centerY, radius, strength, isClockwise) { + if (!this.currentImageData) return; + + const data = this.currentImageData.data; + const width = this.currentImageData.width; + const height = this.currentImageData.height; + const tempData = new Uint8ClampedArray(data); + + // 计算旋转角度 - 基于test-liquify-enhanced.html的算法 + const { pressure, power } = this.params; + const timeFactor = Math.min(this.pressDuration / 1000, 5.0); + const baseRotationSpeed = 0.02; // 使用与测试文件相同的速度 + const rotationAngle = + (isClockwise ? 1 : -1) * baseRotationSpeed * pressure * power * (1.0 + timeFactor * 0.5); + + // 累积旋转角度 - 关键:这确保了持续旋转效果 + this.accumulatedRotation += rotationAngle; + + const processRadius = Math.min(radius, Math.min(width, height) / 2); + const minX = Math.max(0, Math.floor(centerX - processRadius)); + const maxX = Math.min(width, Math.ceil(centerX + processRadius)); + const minY = Math.max(0, Math.floor(centerY - processRadius)); + const maxY = Math.min(height, Math.ceil(centerY + processRadius)); + + // 遍历影响区域内的每个像素 + for (let y = minY; y < maxY; y++) { + for (let x = minX; x < maxX; x++) { + const dx = x - centerX; + const dy = y - centerY; + const distance = Math.sqrt(dx * dx + dy * dy); + + if (distance < processRadius && distance > 0.1) { + // 距离衰减:内圈快,外圈慢 - 与测试文件算法一致 + const normalizedDistance = distance / processRadius; + const falloff = Math.pow(1 - normalizedDistance, 2); // 二次衰减 + + // 计算旋转后的源位置 - 关键算法 + const angle = Math.atan2(dy, dx); + const newAngle = angle + this.accumulatedRotation * falloff; + + const sourceX = centerX + Math.cos(newAngle) * distance; + const sourceY = centerY + Math.sin(newAngle) * distance; + + // 双线性插值采样 - 确保像素连续性 + const color = this._bilinearSample(tempData, width, height, sourceX, sourceY); + + if (color) { + const targetIdx = (y * width + x) * 4; + data[targetIdx] = color[0]; + data[targetIdx + 1] = color[1]; + data[targetIdx + 2] = color[2]; + data[targetIdx + 3] = color[3]; + } + } + } + } + + return true; + } + + /** + * 基于test-liquify-enhanced.html的捏合/展开算法 + * @param {number} centerX 中心X坐标 + * @param {number} centerY 中心Y坐标 + * @param {number} radius 影响半径 + * @param {number} strength 强度 + * @param {boolean} isPinch 是否为捏合模式 + */ + _applyEnhancedPinchDeformation(centerX, centerY, radius, strength, isPinch) { + if (!this.currentImageData) return; + + const data = this.currentImageData.data; + const width = this.currentImageData.width; + const height = this.currentImageData.height; + const tempData = new Uint8ClampedArray(data); + + // 计算时间相关的缩放因子 - 基于test-liquify-enhanced.html + const timeFactor = Math.min(this.pressDuration / 1000, 3.0); + const baseScaleFactor = isPinch ? -0.01 : 0.01; + const scaleFactor = baseScaleFactor * (1.0 + timeFactor * 0.5); + + this.accumulatedScale += scaleFactor; + + const processRadius = Math.min(radius, Math.min(width, height) / 2); + const minX = Math.max(0, Math.floor(centerX - processRadius)); + const maxX = Math.min(width, Math.ceil(centerX + processRadius)); + const minY = Math.max(0, Math.floor(centerY - processRadius)); + const maxY = Math.min(height, Math.ceil(centerY + processRadius)); + + for (let y = minY; y < maxY; y++) { + for (let x = minX; x < maxX; x++) { + const dx = x - centerX; + const dy = y - centerY; + const distance = Math.sqrt(dx * dx + dy * dy); + + if (distance < processRadius && distance > 0.1) { + const normalizedDistance = distance / processRadius; + const falloff = 1 - normalizedDistance * normalizedDistance; + + // 计算缩放后的位置 + const scale = 1 + this.accumulatedScale * falloff; + const sourceX = centerX + dx * scale; + const sourceY = centerY + dy * scale; + + // 双线性插值采样 + const color = this._bilinearSample(tempData, width, height, sourceX, sourceY); + + if (color) { + const targetIdx = (y * width + x) * 4; + data[targetIdx] = color[0]; + data[targetIdx + 1] = color[1]; + data[targetIdx + 2] = color[2]; + data[targetIdx + 3] = color[3]; + } + } + } + } + + return true; + } + + /** + * 基于test-liquify-enhanced.html的推拉算法 + * @param {number} centerX 中心X坐标 + * @param {number} centerY 中心Y坐标 + * @param {number} radius 影响半径 + * @param {number} strength 强度 + */ + _applyEnhancedPushDeformation(centerX, centerY, radius, strength) { + if (!this.currentImageData) return; + + const data = this.currentImageData.data; + const width = this.currentImageData.width; + const height = this.currentImageData.height; + const tempData = new Uint8ClampedArray(data); + + // 计算推拉方向 + const deltaX = this.currentMouseX - this.initialMouseX; + const deltaY = this.currentMouseY - this.initialMouseY; + const dragLength = Math.sqrt(deltaX * deltaX + deltaY * deltaY); + + const processRadius = Math.min(radius, Math.min(width, height) / 2); + const minX = Math.max(0, Math.floor(centerX - processRadius)); + const maxX = Math.min(width, Math.ceil(centerX + processRadius)); + const minY = Math.max(0, Math.floor(centerY - processRadius)); + const maxY = Math.min(height, Math.ceil(centerY + processRadius)); + + if (dragLength === 0) { + // 如果没有拖拽,在持续按压时执行基础的外推效果 + if (this.isHolding) { + const timeFactor = Math.min(this.pressDuration / 1000, 2.0); + const pushStrength = strength * timeFactor * 0.3; + + for (let y = minY; y < maxY; y++) { + for (let x = minX; x < maxX; x++) { + const dx = x - centerX; + const dy = y - centerY; + const distance = Math.sqrt(dx * dx + dy * dy); + + if (distance < processRadius && distance > 0.1) { + const normalizedDistance = distance / processRadius; + const falloff = 1 - normalizedDistance * normalizedDistance; + const factor = falloff * pushStrength; + + // 径向外推效果 + const pushX = (dx / distance) * factor; + const pushY = (dy / distance) * factor; + + const sourceX = x - pushX; + const sourceY = y - pushY; + + const color = this._bilinearSample(tempData, width, height, sourceX, sourceY); + + if (color) { + const targetIdx = (y * width + x) * 4; + data[targetIdx] = color[0]; + data[targetIdx + 1] = color[1]; + data[targetIdx + 2] = color[2]; + data[targetIdx + 3] = color[3]; + } + } + } + } + } + return true; + } + + // 有拖拽时的推拉效果 + const dirX = deltaX / dragLength; + const dirY = deltaY / dragLength; + + for (let y = minY; y < maxY; y++) { + for (let x = minX; x < maxX; x++) { + const dx = x - centerX; + const dy = y - centerY; + const distance = Math.sqrt(dx * dx + dy * dy); + + if (distance < processRadius && distance > 0.1) { + const normalizedDistance = distance / processRadius; + const falloff = 1 - normalizedDistance * normalizedDistance; + const factor = falloff * strength; + + const offsetX = dirX * factor * Math.min(dragLength * 0.3, 15); + const offsetY = dirY * factor * Math.min(dragLength * 0.3, 15); + + const sourceX = x - offsetX; + const sourceY = y - offsetY; + + const color = this._bilinearSample(tempData, width, height, sourceX, sourceY); + + if (color) { + const targetIdx = (y * width + x) * 4; + data[targetIdx] = color[0]; + data[targetIdx + 1] = color[1]; + data[targetIdx + 2] = color[2]; + data[targetIdx + 3] = color[3]; + } + } + } + } + + return true; + } + + /** + * 优化的持续变形效果处理 - 使用增强算法 + */ + applyContinuousDeformation() { + if (!this.isHolding || !this.initialized || !this.currentImageData) return; + + const { size, pressure, power } = this.params; + const mode = this.currentMode; + const radius = size; + const x = this.initialMouseX; + const y = this.initialMouseY; + const strength = pressure * power; + + // 根据模式使用相应的增强算法 + switch (mode) { + case this.modes.CLOCKWISE: + this._applyEnhancedRotationDeformation(x, y, radius, strength, true); + break; + + case this.modes.COUNTERCLOCKWISE: + this._applyEnhancedRotationDeformation(x, y, radius, strength, false); + break; + + case this.modes.PINCH: + this._applyEnhancedPinchDeformation(x, y, radius, strength, true); + break; + + case this.modes.EXPAND: + this._applyEnhancedPinchDeformation(x, y, radius, strength, false); + break; + + case this.modes.PUSH: + this._applyEnhancedPushDeformation(x, y, radius, strength); + break; + + default: { + // 对于其他模式,使用原有的网格算法 + if (!this.mesh) return; + + const baseStrength = (pressure * power * this.config.maxStrength) / 100; + const timeFactor = Math.min(this.pressDuration / 1000, 4.0); + const finalStrength = baseStrength * (1.0 + timeFactor * 0.5); + + this._applyDeformation(x, y, radius, finalStrength, mode, this.params.distortion); + + if (this.config.smoothingIterations > 0) { + this._lightSmoothing(); + } + + return this._applyMeshToImage(); + } + } + + // 对于像素算法,直接返回当前图像数据 + return this.currentImageData; + } + + /** + * 应用液化变形 - 主要入口,集成增强算法 + */ + // applyDeformation(x, y) { + // if (!this.initialized || !this.originalImageData) { + // console.warn("液化管理器未初始化或缺少必要数据"); + // return this.currentImageData; + // } + + // // 更新鼠标位置 + // this.currentMouseX = x; + // this.currentMouseY = y; + + // // 计算拖拽参数 + // const deltaX = this.currentMouseX - this.initialMouseX; + // const deltaY = this.currentMouseY - this.initialMouseY; + // this.dragDistance = Math.sqrt(deltaX * deltaX + deltaY * deltaY); + // this.dragAngle = Math.atan2(deltaY, deltaX); + + // // 获取当前参数 + // const { size, pressure, power } = this.params; + // const mode = this.currentMode; + // const radius = size; + // const strength = pressure * power; + + // // 根据模式选择算法 + // const pixelModes = [ + // this.modes.CLOCKWISE, + // this.modes.COUNTERCLOCKWISE, + // this.modes.PINCH, + // this.modes.EXPAND, + // this.modes.PUSH, + // ]; + + // if (pixelModes.includes(mode)) { + // // 使用增强的像素算法 + // switch (mode) { + // case this.modes.CLOCKWISE: + // this._applyEnhancedRotationDeformation(x, y, radius, strength, false); + // break; + // case this.modes.COUNTERCLOCKWISE: + // this._applyEnhancedRotationDeformation(x, y, radius, strength, true); + // break; + // case this.modes.PINCH: + // this._applyEnhancedPinchDeformation(x, y, radius, strength, true); + // break; + // case this.modes.EXPAND: + // this._applyEnhancedPinchDeformation(x, y, radius, strength, false); + // break; + // case this.modes.PUSH: + // this._applyEnhancedPushDeformation(x, y, radius, strength); + // break; + // } + + // // 更新最后应用时间 + // this.lastApplyTime = Date.now(); + // this.isFirstApply = false; + + // return this.currentImageData; + // } else { + // // 使用原有的网格算法处理其他模式 + // if (!this.mesh) { + // console.warn("网格未初始化"); + // return this.currentImageData; + // } + + // const finalStrength = (strength * this.config.maxStrength) / 100; + + // // 应用变形 + // this._applyDeformation( + // x, + // y, + // radius, + // finalStrength, + // mode, + // this.params.distortion, + // ); + + // // 平滑处理 + // if (this.config.smoothingIterations > 0) { + // this._smoothMesh(); + // } + + // // 更新图像数据 + // const result = this._applyMeshToImage(); + + // // 更新最后应用时间 + // this.lastApplyTime = Date.now(); + // this.isFirstApply = false; + + // return result; + // } + // } + + /** + * 双线性插值采样 - 用于像素级算法 + * @param {Uint8ClampedArray} data 图像数据 + * @param {number} width 图像宽度 + * @param {number} height 图像高度 + * @param {number} x X坐标 + * @param {number} y Y坐标 + * @returns {Array|null} RGBA颜色值数组或null + */ + _bilinearSample(data, width, height, x, y) { + return this._bicubicInterpolate(data, width, height, x, y); + } + /** + * 双三次插值实现 - 确保正确处理Alpha通道 + * @param {Uint8ClampedArray} data 图像数据 + * @param {number} width 图像宽度 + * @param {number} height 图像高度 + * @param {number} x X坐标 + * @param {number} y Y坐标 + * @returns {Array|null} RGBA颜色值数组或null + */ + _bicubicInterpolate(data, width, height, x, y) { + // 获取周围16个像素点 + const x1 = Math.floor(x) - 1; + const y1 = Math.floor(y) - 1; + + // 创建16个采样点的颜色数组 + const pixels = []; + for (let ky = 0; ky < 4; ky++) { + for (let kx = 0; kx < 4; kx++) { + const px = Math.max(0, Math.min(width - 1, x1 + kx)); + const py = Math.max(0, Math.min(height - 1, y1 + ky)); + const idx = (py * width + px) * 4; + pixels[ky * 4 + kx] = [data[idx], data[idx + 1], data[idx + 2], data[idx + 3]]; + } + } + + // 计算小数部分 + const fx = x - (x1 + 1); + const fy = y - (y1 + 1); + + // 计算行插值 + const row0 = this._cubicInterpolateRow(pixels[0], pixels[1], pixels[2], pixels[3], fx); + const row1 = this._cubicInterpolateRow(pixels[4], pixels[5], pixels[6], pixels[7], fx); + const row2 = this._cubicInterpolateRow(pixels[8], pixels[9], pixels[10], pixels[11], fx); + const row3 = this._cubicInterpolateRow(pixels[12], pixels[13], pixels[14], pixels[15], fx); + + // 计算最终结果 + return this._cubicInterpolateRow(row0, row1, row2, row3, fy); + } + // 三次插值辅助方法 - 单行插值 + _cubicInterpolateRow(p0, p1, p2, p3, t) { + // 使用三次多项式插值公式 + const a = [0, 0, 0, 0]; + const b = [0, 0, 0, 0]; + const c = [0, 0, 0, 0]; + + // 为每个通道计算插值系数 + for (let i = 0; i < 4; i++) { + a[i] = -0.5 * p0[i] + 1.5 * p1[i] - 1.5 * p2[i] + 0.5 * p3[i]; + b[i] = p0[i] - 2.5 * p1[i] + 2 * p2[i] - 0.5 * p3[i]; + c[i] = -0.5 * p0[i] + 0.5 * p2[i]; + } + + // 应用三次多项式 + const t2 = t * t; + const t3 = t * t2; + + return [ + Math.round(a[0] * t3 + b[0] * t2 + c[0] * t + p1[0]), + Math.round(a[1] * t3 + b[1] * t2 + c[1] * t + p1[1]), + Math.round(a[2] * t3 + b[2] * t2 + c[2] * t + p1[2]), + Math.round(a[3] * t3 + b[3] * t2 + c[3] * t + p1[3]) // 确保Alpha通道也被正确插值 + ]; + } + + /** + * 应用变形到网格 - 原有的网格算法(用于其他模式) + */ + _applyDeformation(x, y, radius, strength, mode, distortion) { + if (!this.mesh) return; + + const points = this.mesh.deformedPoints; + const originalPoints = this.mesh.originalPoints; + + // 性能优化:只计算影响范围内的网格点 + const affectedPoints = this._getAffectedPoints(x, y, radius); + + for (const pointInfo of affectedPoints) { + const { index: i, point, originalPoint, distance } = pointInfo; + + if (distance > 0) { + // 使用优化的衰减函数 + const normalizedDistance = distance / radius; + const factor = this._optimizedFalloff(normalizedDistance) * strength; + + switch (mode) { + case this.modes.CRYSTAL: { + // 水晶模式 + const dx = point.x - x; + const dy = point.y - y; + const crystalAngle = Math.atan2(dy, dx); + const crystalRadius = normalizedDistance; + + const baseDistortion = Math.max(distortion, 0.3); + const timeFactor = Math.min(this.pressDuration / 1000, 2.0); + const timeEnhancedDistortion = baseDistortion * (1.0 + timeFactor * 0.3); + + const wave1 = Math.sin(crystalAngle * 8 + this.pressDuration * 0.005) * 0.6; + const wave2 = Math.cos(crystalAngle * 12 + this.pressDuration * 0.003) * 0.4; + const waveAngle = crystalAngle + (wave1 + wave2) * timeEnhancedDistortion; + + const radialMod = + 1 + Math.sin(crystalRadius * Math.PI * 2 + this.pressDuration * 0.002) * 0.3; + const modDistance = distance * radialMod; + + const crystalX = x + Math.cos(waveAngle) * modDistance; + const crystalY = y + Math.sin(waveAngle) * modDistance; + + const crystalFactor = factor * timeEnhancedDistortion * 0.7; + point.x += (crystalX - point.x) * crystalFactor; + point.y += (crystalY - point.y) * crystalFactor; + break; + } + + case this.modes.EDGE: { + // 边缘模式 + const dx = point.x - x; + const dy = point.y - y; + const edgeAngle = Math.atan2(dy, dx); + const edgeRadius = normalizedDistance; + + const baseEdgeDistortion = Math.max(distortion, 0.5); + const timeFactor = Math.min(this.pressDuration / 1000, 2.5); + const timeEnhancedDistortion = baseEdgeDistortion * (1.0 + timeFactor * 0.4); + + const edgeWave = + Math.sin(edgeRadius * Math.PI * 4 + this.pressDuration * 0.004) * + Math.cos(edgeAngle * 6 + this.pressDuration * 0.002); + const perpAngle = edgeAngle + Math.PI / 2; + + const edgeFactor = edgeWave * factor * timeEnhancedDistortion * 0.5; + 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.2; + point.x += (originalPoint.x - point.x) * restoreFactor; + point.y += (originalPoint.y - point.y) * restoreFactor; + break; + } + } + } + } + } + + /** + * 获取受影响的网格点(范围优化) + */ + _getAffectedPoints(centerX, centerY, radius) { + const { cols, rows, gridSize } = this.mesh; + const points = this.mesh.deformedPoints; + const originalPoints = this.mesh.originalPoints; + const affectedPoints = []; + + // 计算影响范围的网格边界 + const minGridX = Math.max(0, Math.floor((centerX - radius) / gridSize)); + const maxGridX = Math.min(cols, Math.ceil((centerX + radius) / gridSize)); + const minGridY = Math.max(0, Math.floor((centerY - radius) / gridSize)); + const maxGridY = Math.min(rows, Math.ceil((centerY + radius) / gridSize)); + + // 只遍历影响范围内的网格点 + for (let gridY = minGridY; gridY <= maxGridY; gridY++) { + for (let gridX = minGridX; gridX <= maxGridX; gridX++) { + const index = gridY * (cols + 1) + gridX; + if (index < points.length) { + const point = points[index]; + const originalPoint = originalPoints[index]; + const dx = point.x - centerX; + const dy = point.y - centerY; + const distance = Math.sqrt(dx * dx + dy * dy); + + // 只包含在影响半径内的点 + if (distance <= radius) { + affectedPoints.push({ + index, + point, + originalPoint, + distance, + dx, + dy, + }); + } + } + } + } + + return affectedPoints; + } + + _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; + } + } + } + + /** + * 专门为旋转模式优化的网格平滑 + */ + _lightSmoothing() { + const { rows, cols } = this.mesh; + const points = this.mesh.deformedPoints; + const tempPoints = points.map((p) => ({ x: p.x, y: p.y })); + + // 只进行一次轻微平滑 + 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 lightRelaxFactor = this.config.relaxFactor * 0.3; + tempPoints[idx].x += (centerX - points[idx].x) * lightRelaxFactor; + tempPoints[idx].y += (centerY - points[idx].y) * lightRelaxFactor; + } + } + + for (let i = 0; i < points.length; i++) { + points[i].x = tempPoints[i].x; + points[i].y = tempPoints[i].y; + } + } + + /** + * 使用更优化的衰减函数 + * @param {number} t 归一化距离 (0-1) + * @returns {number} 衰减因子 (0-1) + */ + _optimizedFalloff(t) { + if (t >= 1.0) return 0; + + // 对于旋转模式,使用专门的衰减函数 + if ( + this.currentMode === this.modes.CLOCKWISE || + this.currentMode === this.modes.COUNTERCLOCKWISE + ) { + return this._stableRotationFalloff(t); // 修复函数名 + } + + // 其他模式使用原来的衰减函数 + const smoothT = 1 - t; + + // 多项式衰减 + 指数衰减的组合 + const polynomial = smoothT * smoothT * (3 - 2 * smoothT); // 平滑阶梯函数 + const exponential = Math.exp(-t * 2); // 指数衰减 + + // 组合两种衰减方式,在不同区域有不同特性 + const weight = Math.cos(t * Math.PI * 0.5); // 权重函数 + + return polynomial * weight + exponential * (1 - weight); + } + + _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; + + // 移除步长采样,始终使用1:1采样 + for (let y = 0; y < height; y++) { + for (let x = 0; x < width; x++) { + const srcPos = this._mapPointBack(x, y); + const dstIdx = (y * width + x) * 4; + + if (srcPos.x >= 0 && srcPos.x < width && srcPos.y >= 0 && srcPos.y < height) { + // 使用双三次插值获取颜色 + const color = this._bicubicInterpolate(srcData, width, height, srcPos.x, srcPos.y); + dstData[dstIdx] = color[0]; + dstData[dstIdx + 1] = color[1]; + dstData[dstIdx + 2] = color[2]; + dstData[dstIdx + 3] = color[3]; // 确保Alpha通道值被正确设置 + } else { + // 对于边界外的点,使用最近的有效像素或保持原Alpha通道 + // 这里我们确保Alpha通道不为0,防止出现透明区域 + const nearestX = Math.max(0, Math.min(width - 1, Math.round(srcPos.x))); + const nearestY = Math.max(0, Math.min(height - 1, Math.round(srcPos.y))); + const nearestIdx = (nearestY * width + nearestX) * 4; + + // 复制最近像素的颜色,但保持Alpha通道为不透明 + dstData[dstIdx] = srcData[nearestIdx]; + dstData[dstIdx + 1] = srcData[nearestIdx + 1]; + dstData[dstIdx + 2] = srcData[nearestIdx + 2]; + dstData[dstIdx + 3] = 255; // 强制设置为完全不透明 + } + } + } + + this.currentImageData = result; + // 添加锐化处理 + if (this.config.sharpenAmount > 0) { + this.currentImageData = this._sharpenImage(this.currentImageData, this.config.sharpenAmount); + } + 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; + } + + // 对于批量处理,模拟连续的拖拽操作 + if (positions.length > 0) { + // 使用第一个位置作为初始点 + this.startDeformation(positions[0].x, positions[0].y); + + // 逐个应用每个位置的变形 + positions.forEach((pos, index) => { + if (index === 0) return; // 跳过第一个,因为已经作为初始点 + + // 更新当前位置并应用变形 + this.currentMouseX = pos.x; + this.currentMouseY = pos.y; + + // 重新计算拖拽参数 + const deltaX = this.currentMouseX - this.initialMouseX; + const deltaY = this.currentMouseY - this.initialMouseY; + this.dragDistance = Math.sqrt(deltaX * deltaX + deltaY * deltaY); + this.dragAngle = Math.atan2(deltaY, deltaX); + + const { size, pressure, distortion, power } = this.params; + const mode = this.currentMode; + const radius = size * 0.8; + + // 根据推拉模式和拖拽距离动态调整强度 + let strength; + if (mode === this.modes.PUSH) { + const baseStrength = (pressure * power * this.config.maxStrength) / 100; + const distanceFactor = Math.min(this.dragDistance / radius, 2.0); + strength = baseStrength * distanceFactor * 0.3; // 批量处理时降低强度 + } else { + strength = (pressure * power * this.config.maxStrength) / 100; + } + + this._applyDeformation(pos.x, pos.y, radius, strength, mode, distortion); + }); + + // 结束拖拽操作 + this.endDeformation(); + } + + 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 isNearEdge = this._isNearEdge(originalX, originalY); + const edgeProtectionFactor = isNearEdge ? 0.2 : 1.0; // 边缘区域减少变形量 + + // 计算偏移量并应用反向映射 + const offsetX = (deformedX - originalX) * edgeProtectionFactor; + const offsetY = (deformedY - originalY) * edgeProtectionFactor; + + return { + x: Math.max(0, Math.min(this.mesh.width - 1, x - offsetX)), + y: Math.max(0, Math.min(this.mesh.height - 1, y - offsetY)), + }; + } + // 边缘检测辅助方法 + _isNearEdge(x, y, threshold = 10) { + if (!this.originalImageData) return false; + + const data = this.originalImageData.data; + const width = this.originalImageData.width; + const height = this.originalImageData.height; + + // 检查像素是否在边缘 + if (x <= 0 || x >= width - 1 || y <= 0 || y >= height - 1) return true; + + // 简单的Sobel边缘检测 + const getPixelBrightness = (px, py) => { + const idx = (py * width + px) * 4; + return (data[idx] + data[idx + 1] + data[idx + 2]) / 3; + }; + + const kernelX = [ + [-1, 0, 1], + [-2, 0, 2], + [-1, 0, 1] + ]; + + const kernelY = [ + [-1, -2, -1], + [0, 0, 0], + [1, 2, 1] + ]; + + let gradientX = 0; + let gradientY = 0; + + for (let ky = -1; ky <= 1; ky++) { + for (let kx = -1; kx <= 1; kx++) { + const px = Math.min(Math.max(0, x + kx), width - 1); + const py = Math.min(Math.max(0, y + ky), height - 1); + const brightness = getPixelBrightness(px, py); + gradientX += brightness * kernelX[ky + 1][kx + 1]; + gradientY += brightness * kernelY[ky + 1][kx + 1]; + } + } + + const gradientMagnitude = Math.sqrt(gradientX * gradientX + gradientY * gradientY); + return gradientMagnitude > threshold; + } + // 图像锐化方法 + _sharpenImage(imageData, amount = 0.5) { + if (!imageData) return imageData; + + const data = new Uint8ClampedArray(imageData.data); + const width = imageData.width; + const height = imageData.height; + const result = new ImageData(width, height); + const dstData = result.data; + + // 锐化核 - 中心为5,周围为-1 + const kernel = [ + [0, -1, 0], + [-1, 5, -1], + [0, -1, 0] + ]; + + for (let y = 0; y < height; y++) { + for (let x = 0; x < width; x++) { + // 边缘像素不处理 + if (x === 0 || x === width - 1 || y === 0 || y === height - 1) { + const idx = (y * width + x) * 4; + for (let c = 0; c < 4; c++) { + dstData[idx + c] = data[idx + c]; + } + continue; + } + + const sharpened = [0, 0, 0, 0]; + + // 应用锐化核 + for (let ky = -1; ky <= 1; ky++) { + for (let kx = -1; kx <= 1; kx++) { + const px = x + kx; + const py = y + ky; + const idx = (py * width + px) * 4; + const weight = kernel[ky + 1][kx + 1]; + + for (let c = 0; c < 3; c++) { // 只锐化RGB通道 + sharpened[c] += data[idx + c] * weight; + } + sharpened[3] = data[idx + 3]; // 保持Alpha通道不变 + } + } + + // 应用锐化强度并裁剪值范围 + const idx = (y * width + x) * 4; + for (let c = 0; c < 3; c++) { + const original = data[idx + c]; + const diff = sharpened[c] - original; + dstData[idx + c] = Math.max(0, Math.min(255, original + diff * amount)); + } + dstData[idx + 3] = sharpened[3]; + } + } + + return result; + } + _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.initialMouseX = 0; + this.initialMouseY = 0; + this.currentMouseX = 0; + this.currentMouseY = 0; + this.lastMouseX = 0; + this.lastMouseY = 0; + this.mouseMovementX = 0; + this.mouseMovementY = 0; + this.isFirstApply = true; + this.isDragging = false; + this.dragDistance = 0; + this.dragAngle = 0; + + // 新增:重置持续按压状态 + this.isHolding = false; + this.pressStartTime = 0; + this.pressDuration = 0; + this.accumulatedRotation = 0; + this.accumulatedScale = 0; + this.lastApplyTime = 0; + + this.deformHistory = []; + return true; + } + + // 新增:获取持续按压状态信息 + getHoldingInfo() { + return { + isHolding: this.isHolding, + pressDuration: this.pressDuration, + accumulatedRotation: this.accumulatedRotation, + accumulatedScale: this.accumulatedScale, + pressStartTime: this.pressStartTime, + }; + } + + /** + * 应用液化变形 - 主要的公共接口方法 + * @param {Number} x X坐标 + * @param {Number} y Y坐标 + * @returns {ImageData} 变形后的图像数据 + */ + applyDeformation(x, y) { + if (!this.initialized || !this.mesh || !this.originalImageData) { + console.warn("液化管理器未初始化或缺少必要数据"); + return this.currentImageData; + } + + // 更新鼠标位置 + this.currentMouseX = x; + this.currentMouseY = y; + + // 计算拖拽参数 + const deltaX = this.currentMouseX - this.initialMouseX; + const deltaY = this.currentMouseY - this.initialMouseY; + this.dragDistance = Math.sqrt(deltaX * deltaX + deltaY * deltaY); + this.dragAngle = Math.atan2(deltaY, deltaX); + + // 获取当前参数 + const { size, pressure, power } = this.params; + const mode = this.currentMode; + const radius = size; + const strength = pressure * power; + + // 根据模式选择算法 + const pixelModes = [ + this.modes.CLOCKWISE, + this.modes.COUNTERCLOCKWISE, + this.modes.PINCH, + this.modes.EXPAND, + this.modes.PUSH, + ]; + + if (pixelModes.includes(mode)) { + // 使用增强的像素算法 + switch (mode) { + case this.modes.CLOCKWISE: + this._applyEnhancedRotationDeformation(x, y, radius, strength, false); + break; + case this.modes.COUNTERCLOCKWISE: + this._applyEnhancedRotationDeformation(x, y, radius, strength, true); + break; + case this.modes.PINCH: + this._applyEnhancedPinchDeformation(x, y, radius, strength, true); + break; + case this.modes.EXPAND: + this._applyEnhancedPinchDeformation(x, y, radius, strength, false); + break; + case this.modes.PUSH: + this._applyEnhancedPushDeformation(x, y, radius, strength); + break; + } + + // 更新最后应用时间 + this.lastApplyTime = Date.now(); + this.isFirstApply = false; + + return this.currentImageData; + } else { + // 使用原有的网格算法处理其他模式 + if (!this.mesh) { + console.warn("网格未初始化"); + return this.currentImageData; + } + + const finalStrength = (strength * this.config.maxStrength) / 100; + + // 应用变形 + this._applyDeformation(x, y, radius, finalStrength, mode, this.params.distortion); + + // 有条件地应用平滑处理,仅在特定模式下应用 + const smoothingModes = [this.modes.CRYSTAL, this.modes.EDGE]; + if (smoothingModes.includes(mode) && this.config.smoothingIterations > 0) { + this._smoothMesh(); + } + + // 更新图像数据 + const result = this._applyMeshToImage(); + + // 更新最后应用时间 + this.lastApplyTime = Date.now(); + this.isFirstApply = false; + + return result; + } + } + + getCurrentImageData() { + return this.currentImageData; + } + + destroy() { + // 停止持续效果定时器 + this.stopContinuousEffect(); + + this.originalImageData = null; + this.currentImageData = null; + this.mesh = null; + this.deformHistory = []; + this.initialized = false; + + // 清理拖拽状态 + this.initialMouseX = 0; + this.initialMouseY = 0; + this.currentMouseX = 0; + this.currentMouseY = 0; + this.lastMouseX = 0; + this.lastMouseY = 0; + this.mouseMovementX = 0; + this.mouseMovementY = 0; + this.isFirstApply = true; + this.isDragging = false; + this.dragDistance = 0; + this.dragAngle = 0; + + // 新增:清理持续按压状态 + this.isHolding = false; + this.pressStartTime = 0; + this.pressDuration = 0; + this.accumulatedRotation = 0; + this.accumulatedScale = 0; + this.lastApplyTime = 0; + } + + /** + * 释放资源 - 别名方法,与其他管理器保持一致 + */ + dispose() { + this.destroy(); + } } diff --git a/src/component/Canvas/CanvasEditor/managers/liquify/LiquifyManager.js b/src/component/Canvas/CanvasEditor/managers/liquify/LiquifyManager.js index 5475501b..ce033261 100644 --- a/src/component/Canvas/CanvasEditor/managers/liquify/LiquifyManager.js +++ b/src/component/Canvas/CanvasEditor/managers/liquify/LiquifyManager.js @@ -31,10 +31,10 @@ export class LiquifyManager { // 创建增强版液化管理器实例 this.enhancedManager = new EnhancedLiquifyManager({ // 配置选项 - gridSize: options.gridSize || 15, - maxStrength: options.maxStrength || 100, - smoothingIterations: options.smoothingIterations || 2, - relaxFactor: options.relaxFactor || 0.25, + gridSize: options.gridSize || 8, + maxStrength: options.maxStrength || 200, + smoothingIterations: options.smoothingIterations || 1, + relaxFactor: options.relaxFactor || 0.05, meshResolution: options.meshResolution || 64, // 根据环境选择合适的渲染模式 forceCPU: true, // 默认不强制使用CPU diff --git a/src/component/Canvas/ExistsImageList/ToolButton.vue b/src/component/Canvas/ExistsImageList/ToolButton.vue index df816c72..5c51ff9a 100644 --- a/src/component/Canvas/ExistsImageList/ToolButton.vue +++ b/src/component/Canvas/ExistsImageList/ToolButton.vue @@ -21,6 +21,10 @@ const props = defineProps({ type: Boolean, default: true, }, + tipBody: { + type: Boolean, + default: false, + }, }); const emit = defineEmits(["click"]); @@ -47,10 +51,45 @@ const handleClick = () => { if (isDisabled.value) return; emit("click", props.tool); }; + +const tipId = "tooltip-" + Math.random().toString(36).substring(2); +const el = ref(null); +// 鼠标移入获取位置信息 +const handleMouseEnter = (e) => { + const tooltip = el.value; + const rect = tooltip.getBoundingClientRect(); + const left = rect.left + rect.width; + const top = rect.top + rect.height / 2; + const tip = document.getElementById(tipId); + tip.style.position = 'fixed'; + tip.style.left = `${left}px`; + tip.style.top = `${top}px`; + tip.style.display = 'block'; +} +// 鼠标移出隐藏提示 +const handleMouseLeave = () => { + const tip = document.getElementById(tipId); + tip.style.display = 'none'; +} +onMounted(() => { + if(props.tipBody){ + el.value.addEventListener('mouseenter', handleMouseEnter); + el.value.addEventListener('mouseleave', handleMouseLeave); + } +}) + +onUnmounted(() => { + if(props.tipBody && el.value){ + el.value.removeEventListener('mouseenter', handleMouseEnter); + el.value.removeEventListener('mouseleave', handleMouseLeave); + } +}) + @@ -82,6 +124,7 @@ const handleClick = () => { font-size: 1.6rem; color: #333; transition: all 0.2s ease; + flex-shrink: 0; } .tool-btn:hover { @@ -115,7 +158,7 @@ const handleClick = () => { margin-left: .8rem; white-space: nowrap; font-size: 1.2rem; - z-index: 10; + z-index: 9999; } .tool-tooltip:before { diff --git a/src/component/Canvas/ExistsImageList/index.vue b/src/component/Canvas/ExistsImageList/index.vue index 5730cbfe..d1aaab6c 100644 --- a/src/component/Canvas/ExistsImageList/index.vue +++ b/src/component/Canvas/ExistsImageList/index.vue @@ -4,7 +4,7 @@
@@ -56,6 +56,14 @@ {{ item.name || "未命名" }}
+ + @@ -70,6 +78,7 @@
{{ $t("Canvas.general") }} {{ filteredImages.length }} {{ $t("Canvas.PicturesInTotal") }}
+ @@ -103,6 +112,7 @@ const emits = defineEmits(["select"]); // 响应式数据 const showPanel = ref(false); const selectedCategory = ref(t("Canvas.all")); +const selectList = ref([]) // 计算属性:获取所有分类 const categories = computed(() => { @@ -152,8 +162,12 @@ const filteredImages = computed(() => { // 处理图片点击 const handleImageClick = (item) => { - emits("select", item); - showPanel.value = false; + // 已选中,取消选中 + if(selectList.value.includes(item.url)){ + selectList.value = selectList.value.filter(url => url !== item.url) + }else{ + selectList.value.push(item.url) + } }; // 处理图片加载错误 @@ -162,6 +176,13 @@ const handleImageError = (event) => { "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAwIiBoZWlnaHQ9IjIwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cmVjdCB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiBmaWxsPSIjZjVmNWY1Ii8+PHRleHQgeD0iNTAlIiB5PSI1MCUiIGZvbnQtZmFtaWx5PSJBcmlhbCwgc2Fucy1zZXJpZiIgZm9udC1zaXplPSIxNCIgZmlsbD0iIzk5OSIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZHk9Ii4zZW0iPuWbvueJh+WKoOi9veWksei0pe+8jOivt+ajgOafpeWbvueJh+i3r+W+hDwvdGV4dD48L3N2Zz4="; event.target.alt = "图片加载失败"; }; + +//提交选中的T图片 +const confirm = ()=>{ + emits("select", selectList.value); + selectList.value = [] + showPanel.value = false; +} \ No newline at end of file diff --git a/src/component/Detail/detailLeft/models.vue b/src/component/Detail/detailLeft/models.vue index 403240bc..c239fa94 100644 --- a/src/component/Detail/detailLeft/models.vue +++ b/src/component/Detail/detailLeft/models.vue @@ -100,11 +100,12 @@ export default defineComponent({ padding: 1rem 0; text-align: center; border-radius: .5rem; - // border: 1px dashed #202020; - border: 1px dashed transparent; + border: 1px solid #000; + // border: 1px dashed transparent; + border-radius: 1.5rem; position: relative; background: linear-gradient(#fff, #fff) padding-box,repeating-linear-gradient(-45deg,#fff 0,#fff 0.3em, #000 0,#000 0.6em); - margin-bottom: 3rem; + margin-bottom: 1.4rem; > img{ width: 100%; height: 100%; diff --git a/src/component/Detail/detailLeft/module/currentList.vue b/src/component/Detail/detailLeft/module/currentList.vue index 36094527..95143250 100644 --- a/src/component/Detail/detailLeft/module/currentList.vue +++ b/src/component/Detail/detailLeft/module/currentList.vue @@ -107,9 +107,9 @@ export default defineComponent({ align-content: flex-start; &::-webkit-scrollbar{display: none;} > .content_img_item{ + width: calc((50% - 1rem)); > .content_img_item_block{ - width: calc((34rem - 2rem) / 2); - height: calc((34rem - 2rem) / 2); + aspect-ratio: 1/1; position: relative; margin-bottom: 2rem; @@ -121,16 +121,6 @@ export default defineComponent({ } } } - > .material_content_list_loding{ - width: 100%; - height: calc((34rem - 2rem) / 2); - } - > .upload_item{ - width: calc((34rem - 2rem) / 2); - height: calc((34rem - 2rem) / 2); - align-items: center; - justify-content: center; - } } } diff --git a/src/component/Detail/detailLeft/module/libraryList.vue b/src/component/Detail/detailLeft/module/libraryList.vue index 5b40f497..bd3459c0 100644 --- a/src/component/Detail/detailLeft/module/libraryList.vue +++ b/src/component/Detail/detailLeft/module/libraryList.vue @@ -6,7 +6,7 @@ v-model:value="designType" :options="(designTypeList)" @change="handleChange" - style="width:100%" + style="width:100%; font-size: 1.4rem;" size="large" :fieldNames="{ label: 'name', value: 'value' }" > @@ -23,7 +23,7 @@ v-model:value="mannequinData.system" :options="systemList" @change="handleChange" - style="width:100%" + style="width:100%; font-size: 1.4rem;" size="large" :fieldNames="{ label: 'name', value: 'value' }" > @@ -41,7 +41,7 @@ v-model:value="mannequinData.style" :options="mannequinStyle" @change="handleChange" - style="width:100%" + style="width:100%; font-size: 1.4rem;" size="large" :fieldNames="{ label: 'name', value: 'value' }" > @@ -58,7 +58,7 @@ v-model:value="mannequinData.ageGroup" :options="ageGroupList" @change="handleChange" - style="width:100%" + style="width:100%; font-size: 1.4rem;" size="large" :fieldNames="{ label: 'name', value: 'value' }" > @@ -75,7 +75,7 @@ v-model:value="mannequinData.sex" :options="sexList" @change="handleChange" - style="width:100%" + style="width:100%; font-size: 1.4rem;" size="large" :fieldNames="{ label: 'name', value: 'value' }" > @@ -333,6 +333,9 @@ export default defineComponent({ justify-content: center; cursor: pointer; } + > .search_input{ + font-size: 1.4rem; + } } > .generalModel_state_item:last-child{ // margin-top: 2rem; @@ -347,11 +350,11 @@ export default defineComponent({ margin-top: 1rem; justify-content: space-between; align-content: flex-start; - &::-webkit-scrollbar{display: none;} + // &::-webkit-scrollbar{display: none;} > .content_img_item{ + width: calc((50% - 1rem)); > .content_img_item_block{ - width: calc((34rem - 2rem) / 2); - height: calc((34rem - 2rem) / 2); + aspect-ratio: 1/1; position: relative; margin-bottom: 2rem; cursor: pointer; @@ -364,7 +367,7 @@ export default defineComponent({ } > .material_content_list_loding{ width: 100%; - height: calc((34rem - 2rem) / 2); + aspect-ratio: 1/1; overflow: hidden; > img{ width: 100%; diff --git a/src/component/Detail/detailLeft/module/selectList.vue b/src/component/Detail/detailLeft/module/selectList.vue index 0bf32560..489df33f 100644 --- a/src/component/Detail/detailLeft/module/selectList.vue +++ b/src/component/Detail/detailLeft/module/selectList.vue @@ -1,6 +1,6 @@