index.js 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759
  1. import Teleport from '@/components/Teleport'
  2. import * as TextboxConfig from './module/TextboxConfig'
  3. import fabric from "../../js/fabric-adapter";
  4. import {markRaw,reactive} from "vue";
  5. // 将默认的 clipSettings 提取为常量
  6. const DEFAULT_CLIP_SETTINGS = {
  7. shape: '',
  8. width: 100,
  9. height: 100,
  10. radius: 50,
  11. offsetX: 50,
  12. offsetY: 50,
  13. showClipStroke: true,
  14. strokeColor: '#000000',
  15. strokeWidth: 1,
  16. rectRadius: 0,
  17. svgUrl: '',
  18. svgWidth: 100,
  19. svgHeight: 100,
  20. fillColor: '#FFFFFF', // 背景色
  21. fillOpacity: 0.5, // 背景透明度
  22. };
  23. export default {
  24. components: {
  25. Teleport,
  26. },
  27. data() {
  28. return {
  29. TextboxConfig:TextboxConfig,
  30. options:TextboxConfig.fontFamily,
  31. fontFamilyStyle:"",
  32. opacityValue: 1,
  33. clipPreview: {
  34. dataUrl: '',
  35. maxOffsetX: 0,
  36. maxOffsetY: 0,
  37. maxWidth: 0,
  38. maxHeight: 0,
  39. maxRadius: 0,
  40. },
  41. layerState: {
  42. fontSize: 16,
  43. lineHeight: 1.2,
  44. charSpacing: 0,
  45. fill: '#000000',
  46. textAlign: 'left',
  47. },
  48. shadowText:{
  49. x:0,
  50. y:0,
  51. vague:0,
  52. color:'#000',
  53. },
  54. clipSettings: {
  55. ...DEFAULT_CLIP_SETTINGS,
  56. },
  57. strokeObj: null, // 用于引用当前描边图形
  58. imageSize: {
  59. width: 100,
  60. height: 100,
  61. keepRatio: true,
  62. originalRatio: 1, // 原始宽高比
  63. lastAdjusted: 'width' // 最后调整的维度 ('width' 或 'height')
  64. },
  65. // 响应式的文字内容状态(用于解决markRaw后的响应式问题)
  66. reactiveTextContent: ''
  67. }
  68. },
  69. props: {
  70. },
  71. computed: {
  72. editLayer(){
  73. let object = {}
  74. if(this.selected && this.selected.length === 1){
  75. object = this.selected[0]
  76. }
  77. // return reactive(object)
  78. return object
  79. },
  80. fontFamily(){
  81. let obj = {}
  82. this.TextboxConfig.fontFamily.map(item=>{
  83. obj[item.name] = item
  84. })
  85. return obj
  86. },
  87. shadow(){
  88. let shadowText = this.shadowText
  89. return `${shadowText.x}px ${shadowText.y}px ${shadowText.vague}px ${shadowText.color} `
  90. }
  91. },
  92. watch: {
  93. editLayer(){
  94. // 自动加载文字对象的字体
  95. if (this.editLayer && (this.editLayer.type === 'textbox' || this.editLayer.type === 'text')) {
  96. const fontFamily = this.editLayer.fontFamily
  97. if (fontFamily && fontFamily !== 'Arial') {
  98. // 如果是自定义字体,自动加载
  99. this.setFontFamily(fontFamily, this.editLayer).catch(err => {
  100. console.warn('自动加载字体失败:', err)
  101. })
  102. }
  103. }
  104. this.fontFamilyStyle = this.editLayer.fontFamily
  105. this.opacityValue = (this.editLayer && typeof this.editLayer.opacity === 'number')
  106. ? this.editLayer.opacity
  107. : 1
  108. this.layerState = {
  109. ...this.layerState,
  110. text: this.editLayer?.text ?? '',
  111. fontSize: this.editLayer?.fontSize ?? 16,
  112. lineHeight: this.editLayer?.lineHeight ?? 1,
  113. charSpacing: this.editLayer?.charSpacing ?? 0,
  114. fill: this.editLayer?.fill ?? '#000000',
  115. textAlign: this.editLayer?.textAlign ?? 'left',
  116. }
  117. // 同步响应式文字内容状态
  118. this.reactiveTextContent = this.editLayer?.text ?? '';
  119. switch (this.editLayer.type){
  120. case "textbox":
  121. if(this.editLayer.shadow){
  122. this.shadowText = {
  123. x:this.editLayer.shadow.offsetX || 0,
  124. y:this.editLayer.shadow.offsetY || 0,
  125. vague:this.editLayer.shadow.blur || 0,
  126. color:this.editLayer.shadow.color || '#000',
  127. }
  128. }else{
  129. this.shadowText = {
  130. x:0,
  131. y:0,
  132. vague:0,
  133. color:'#000'
  134. }
  135. }
  136. break;
  137. case "image":
  138. // 计算原始宽高比(基于图片的原始尺寸)
  139. const originalWidth = this.editLayer.width || 100;
  140. const originalHeight = this.editLayer.height || 100;
  141. const originalRatio = originalWidth / originalHeight;
  142. // 更新图片尺寸显示
  143. this.imageSize = {
  144. width: Math.round(this.editLayer.getScaledWidth() || originalWidth),
  145. height: Math.round(this.editLayer.getScaledHeight() || originalHeight),
  146. keepRatio: true,
  147. originalRatio: originalRatio,
  148. lastAdjusted: 'width'
  149. };
  150. // 优先使用默认值
  151. let imageSettings = { ...DEFAULT_CLIP_SETTINGS };
  152. // 如果图片对象有保存的裁剪设置,优先使用
  153. if (this.editLayer.clipSettings) {
  154. imageSettings = { ...imageSettings, ...this.editLayer.clipSettings };
  155. }
  156. // 否则从 clipPath 恢复基本几何属性
  157. else if (this.editLayer.clipPath) {
  158. imageSettings = {
  159. ...imageSettings,
  160. width: this.editLayer.clipPath.width || imageSettings.width,
  161. height: this.editLayer.clipPath.height || imageSettings.height,
  162. offsetX: this.editLayer.clipPath.left || imageSettings.offsetX,
  163. offsetY: this.editLayer.clipPath.top || imageSettings.offsetY,
  164. shape: this.editLayer.clipPath.type || imageSettings.shape,
  165. };
  166. }
  167. // 如果没有剪裁路径,则 shape 为 ""
  168. if (!this.editLayer.clipPath) {
  169. imageSettings.shape = '';
  170. }
  171. // 更新 clipSettings,用于 UI 显示
  172. this.clipSettings = imageSettings;
  173. // 如果有裁剪设置,恢复背景对象
  174. if (imageSettings.shape && this.editLayer.clipSettings) {
  175. this.createClipBackground();
  176. }
  177. // 为图片对象添加尺寸变化监听
  178. if (this.editLayer.type === 'image') {
  179. // 移除之前可能的监听器
  180. this.editLayer.off('scaling', this.updateImageSizeFromCanvas);
  181. // 添加新的监听器
  182. this.editLayer.on('scaling', this.updateImageSizeFromCanvas);
  183. }
  184. // 同步描边图形(如果存在)
  185. if (this.editLayer.strokeObj) {
  186. this.fcanvas.add(markRaw(this.editLayer.strokeObj));
  187. }
  188. this.updateClipPreview && this.updateClipPreview()
  189. break;
  190. }
  191. },
  192. selected(){
  193. this.opacityValue = (this.editLayer && typeof this.editLayer.opacity === 'number')
  194. ? this.editLayer.opacity
  195. : 1
  196. this.layerState = {
  197. ...this.layerState,
  198. text: this.editLayer?.text ?? '',
  199. fontSize: this.editLayer?.fontSize ?? 16,
  200. lineHeight: this.editLayer?.lineHeight ?? 1,
  201. charSpacing: this.editLayer?.charSpacing ?? 0,
  202. fill: this.editLayer?.fill ?? '#000000',
  203. textAlign: this.editLayer?.textAlign ?? 'left',
  204. }
  205. // 同步响应式文字内容状态
  206. this.reactiveTextContent = this.editLayer?.text ?? '';
  207. // 更新图片尺寸显示
  208. if (this.editLayer && this.editLayer.type === 'image') {
  209. const originalWidth = this.editLayer.width || 100;
  210. const originalHeight = this.editLayer.height || 100;
  211. const originalRatio = originalWidth / originalHeight;
  212. this.imageSize = {
  213. width: Math.round(this.editLayer.getScaledWidth() || originalWidth),
  214. height: Math.round(this.editLayer.getScaledHeight() || originalHeight),
  215. keepRatio: this.imageSize.keepRatio, // 保持当前的保持比例设置
  216. originalRatio: originalRatio,
  217. lastAdjusted: this.imageSize.lastAdjusted || 'width'
  218. };
  219. }
  220. }
  221. },
  222. created() {
  223. },
  224. mounted() {
  225. },
  226. methods:{
  227. judge(){
  228. return this.editLayer?.id
  229. },
  230. //翻转
  231. Flip(type){
  232. if(!this.judge()) return;
  233. switch (type){
  234. case 'X':
  235. this.editLayer.set({
  236. flipX:!this.editLayer.flipX,
  237. })
  238. break;
  239. case 'Y':
  240. this.editLayer.set({
  241. flipY:!this.editLayer.flipY,
  242. })
  243. break;
  244. }
  245. this.updateCanvasState()
  246. this.fcanvas.requestRenderAll()
  247. },
  248. //合并组
  249. setGroup(){
  250. const group = this.fcanvas.getActiveObject().toGroup();
  251. this.fcanvas.requestRenderAll();
  252. this.selected = [group]
  253. if(this.getLayers) this.getLayers()
  254. this.updateCanvasState()
  255. },
  256. //取消组
  257. unGroup(){
  258. if (!this.fcanvas.getActiveObject()) {
  259. return;
  260. }
  261. if (this.fcanvas.getActiveObject().type !== 'group') {
  262. return;
  263. }
  264. this.fcanvas.getActiveObject().toActiveSelection();
  265. this.selected = this.fcanvas.getActiveObject()._objects
  266. this.fcanvas.requestRenderAll();
  267. if(this.getLayers) this.getLayers()
  268. this.updateCanvasState()
  269. },
  270. //编辑文字
  271. editObj(data = {
  272. label:'',
  273. value:'',
  274. action:'set',
  275. }){
  276. if(!this.judge()) return;
  277. let {label,value,action } = data
  278. action = action || 'set'
  279. // 构造待设置的参数;仅当 label 为 fill 且当前无填充时兜底 #000
  280. let params = value
  281. if (label) {
  282. params = { [label]: value }
  283. if (label === 'fill' && !this.editLayer.fill) {
  284. params.fill = value || '#000'
  285. }
  286. }
  287. this.editLayer[action](params)
  288. if (Array.isArray(this.editLayer._objects)){
  289. this.editLayer._objects?.forEach(item=>{
  290. item[action](params)
  291. })
  292. }
  293. // 同步常用文本状态到 layerState,避免 UI 视图不同步
  294. if (['fontSize','lineHeight','charSpacing','fill','textAlign'].includes(label)) {
  295. this.layerState = {
  296. ...this.layerState,
  297. [label]: value,
  298. }
  299. }
  300. this.updateCanvasState()
  301. this.fcanvas.requestRenderAll()
  302. },
  303. // 通用设置图层属性,解决 markRaw 不响应问题
  304. setLayerAttr(name, val){
  305. if(!this.editLayer) return
  306. this.layerState = {
  307. ...this.layerState,
  308. [name]: val,
  309. }
  310. this.editLayer.set(name, val)
  311. if(Array.isArray(this.editLayer._objects)){
  312. this.editLayer._objects.forEach(o => o.set(name, val))
  313. }
  314. this.updateCanvasState()
  315. this.fcanvas && this.fcanvas.requestRenderAll()
  316. },
  317. // 设置透明度(解决 Vue3 markRaw 不响应)
  318. setOpacity(val){
  319. if(!this.editLayer) return
  320. const num = Number(val)
  321. this.opacityValue = num
  322. this.editLayer.set('opacity', num)
  323. if(Array.isArray(this.editLayer._objects)){
  324. this.editLayer._objects.forEach(o => o.set('opacity', num))
  325. }
  326. this.updateCanvasState()
  327. this.fcanvas && this.fcanvas.requestRenderAll()
  328. },
  329. async setFontFamily(val,item=null){
  330. console.log(val)
  331. console.log(item)
  332. console.log(this.fontFamily)
  333. if(!item && !this.judge()) return;
  334. let font = this.fontFamily[val]
  335. console.log(font)
  336. if(!font) return;
  337. await loadFont(font.name,font.src)
  338. let obj = item || this.editLayer
  339. if(typeof obj.set !== 'function') return;
  340. obj.set({'fontFamily': val});
  341. obj.setSelectionStyles({'fontFamily': val});
  342. this.fontFamilyStyle = val
  343. this.updateCanvasState()
  344. await this.$nextTick()
  345. this.fcanvas.requestRenderAll()
  346. async function loadFont(_fontName, _fontUrl) {
  347. return new Promise((resolve, reject) => {
  348. if (checkFont(_fontName)) {
  349. console.log('已有字体:', _fontName)
  350. return resolve(true)
  351. }
  352. let prefont = new FontFace(
  353. _fontName,
  354. 'url(' + _fontUrl + ')'
  355. );
  356. prefont.load().then(function (loaded_face) {
  357. document.fonts.add(loaded_face);
  358. // document.body.style.fontFamily = (_fontFamily ? _fontFamily + ',' : _fontFamily) + _fontName;
  359. console.log('字体加载成功', loaded_face, document.fonts)
  360. return resolve(true)
  361. }).catch(function (error) {
  362. console.log('字体加载失败', error)
  363. return reject(error)
  364. })
  365. })
  366. }
  367. function checkFont(name){
  368. let values = document.fonts.values();
  369. let isHave=false;
  370. let item = values.next();
  371. while(!item.done&&!isHave)
  372. {
  373. let fontFace=item.value;
  374. if(fontFace.family==name)
  375. {
  376. isHave=true;
  377. }
  378. item=values.next();
  379. }
  380. return isHave;
  381. }
  382. },
  383. remoteMethod(query){
  384. if (query !== '') {
  385. this.options = this.TextboxConfig.fontFamily.filter(item => {
  386. return item.localFile.toLowerCase()
  387. .indexOf(query.toLowerCase()) > -1;
  388. });
  389. } else {
  390. this.options = this.TextboxConfig.fontFamily
  391. }
  392. },
  393. async applyClipPath() {
  394. if (!this.judge()) return;
  395. const { shape, width, height, radius, offsetX, offsetY, rectRadius } = this.clipSettings;
  396. if (shape === '') {
  397. // 选择“无”时,直接清除剪裁
  398. this.clearClipPath();
  399. return;
  400. }
  401. try {
  402. // 创建纯剪裁区域(不带描边)
  403. let clipObj;
  404. if (shape === 'svg') {
  405. // 使用 fabric.js 加载远程 SVG
  406. clipObj = await new Promise((resolve, reject) => {
  407. fabric.loadSVGFromURL(
  408. this.clipSettings.svgUrl,
  409. (loadedObjects, options) => {
  410. console.log('=============');
  411. // 创建组合对象
  412. const svgGroup = fabric.util.groupSVGElements(loadedObjects, options);
  413. console.log(svgGroup);
  414. // 设置尺寸
  415. svgGroup.scaleToWidth(this.clipSettings.svgWidth);
  416. console.log(this.clipSettings.svgWidth);
  417. // svgGroup.scaleToHeight(this.clipSettings.svgHeight);
  418. svgGroup.set({
  419. absolutePositioned: true,
  420. left: offsetX,
  421. top: offsetY,
  422. })
  423. resolve(svgGroup);
  424. }
  425. )
  426. });
  427. }else if (shape === 'rect') {
  428. if (width <= 0 || height <= 0) throw new Error('尺寸必须大于0');
  429. clipObj = new fabric.Rect({
  430. width: width,
  431. height: height,
  432. left: offsetX,
  433. top: offsetY,
  434. rx: rectRadius,
  435. ry: rectRadius,
  436. absolutePositioned: true
  437. });
  438. } else if(shape === 'circle') {
  439. if (radius <= 0) throw new Error('半径必须大于0');
  440. clipObj = new fabric.Circle({
  441. radius: radius,
  442. left: offsetX,
  443. top: offsetY,
  444. absolutePositioned: true
  445. });
  446. }
  447. // 应用剪裁区域
  448. this.editLayer.set({ clipPath: clipObj });
  449. // 保存裁剪设置到图片对象
  450. this.editLayer.clipSettings = { ...this.clipSettings };
  451. // 创建背景对象(方案二:独立背景对象)
  452. this.createClipBackground();
  453. // 更新画布
  454. this.updateClipStroke()
  455. this.updateCanvasState();
  456. this.fcanvas.requestRenderAll();
  457. this.updateClipPreview && this.updateClipPreview(true)
  458. } catch (error) {
  459. console.error('剪裁区域创建失败:', error);
  460. this.$message.error('剪裁设置错误: ' + error.message);
  461. }
  462. },
  463. // 创建裁剪背景对象(方案二:独立背景对象)
  464. createClipBackground() {
  465. if (!this.judge() || !this.clipSettings.shape) return;
  466. // 移除现有的背景对象
  467. if (this.editLayer.clipBackgroundObj) {
  468. this.fcanvas.remove(this.editLayer.clipBackgroundObj);
  469. this.editLayer.clipBackgroundObj = null;
  470. }
  471. const { shape, width, height, radius, offsetX, offsetY, rectRadius, fillColor, fillOpacity, strokeColor, strokeWidth } = this.clipSettings;
  472. let bgObj;
  473. if (shape === 'rect') {
  474. bgObj = new fabric.Rect({
  475. width: width,
  476. height: height,
  477. left: offsetX,
  478. top: offsetY,
  479. rx: rectRadius,
  480. ry: rectRadius,
  481. fill: fillColor,
  482. stroke: strokeColor,
  483. strokeWidth: strokeWidth,
  484. opacity: fillOpacity,
  485. selectable: false,
  486. evented: false,
  487. absolutePositioned: true
  488. });
  489. } else if (shape === 'circle') {
  490. bgObj = new fabric.Circle({
  491. radius: radius,
  492. left: offsetX,
  493. top: offsetY,
  494. fill: fillColor,
  495. stroke: strokeColor,
  496. strokeWidth: strokeWidth,
  497. opacity: fillOpacity,
  498. selectable: false,
  499. evented: false,
  500. absolutePositioned: true
  501. });
  502. }
  503. if (bgObj) {
  504. // 绑定到当前对象
  505. this.editLayer.clipBackgroundObj = bgObj;
  506. // 添加到画布
  507. this.fcanvas.add(markRaw(bgObj));
  508. // 确保层级正确:背景在最下面,图片在上面
  509. this.fcanvas.sendToBack(bgObj);
  510. this.fcanvas.bringToFront(this.editLayer);
  511. }
  512. },
  513. // 切换剪裁形状,自动填充默认值(位置跟随图层,尺寸为画布一半)
  514. onClipShapeChange(shape){
  515. if(!this.editLayer || this.editLayer.type !== 'image'){
  516. this.clipSettings.shape = shape
  517. return
  518. }
  519. if(!shape){
  520. this.clipSettings.shape = ''
  521. this.clearClipPath()
  522. this.updateClipPreview && this.updateClipPreview(true)
  523. return
  524. }
  525. const obj = this.editLayer
  526. const bounds = obj.getBoundingRect(true)
  527. const canvasW = this.fcanvas?.width || obj.canvas?.width || 800
  528. const canvasH = this.fcanvas?.height || obj.canvas?.height || 800
  529. const initWidth = Math.max(10, Math.round(canvasW / 2))
  530. const initHeight = initWidth // 高度和宽度保持一致
  531. const initRadius = Math.max(5, Math.round(Math.min(canvasW, canvasH) / 4))
  532. const initSettings = {
  533. shape,
  534. offsetX: Math.round(bounds.left),
  535. offsetY: Math.round(bounds.top),
  536. width: initWidth,
  537. height: initHeight,
  538. radius: initRadius,
  539. rectRadius: this.clipSettings.rectRadius || 0,
  540. }
  541. this.clipSettings = { ...this.clipSettings, ...initSettings }
  542. this.applyClipPath()
  543. },
  544. // 预览辅助,若用户保持旧样式可忽略 dataUrl
  545. updateClipPreview(force=false){
  546. if(!this.editLayer || this.editLayer.type !== 'image') return
  547. const obj = this.editLayer
  548. const bounds = obj.getBoundingRect(true)
  549. this.clipPreview = {
  550. ...this.clipPreview,
  551. maxOffsetX: Math.max(0, Math.floor(bounds.width)),
  552. maxOffsetY: Math.max(0, Math.floor(bounds.height)),
  553. maxWidth: Math.max(10, Math.floor(bounds.width)),
  554. maxHeight: Math.max(10, Math.floor(bounds.height)),
  555. maxRadius: Math.max(5, Math.floor(Math.min(bounds.width, bounds.height)/2)),
  556. }
  557. if(force || this.clipPreview.dataUrl === ''){
  558. try{
  559. // clone 可能是异步的(带 filters 时),使用回调形式避免 callback is not a function
  560. obj.clone((cloned)=>{
  561. if(!cloned) return
  562. const tmp = new fabric.Canvas(document.createElement('canvas'), { width: Math.min(200, bounds.width), height: Math.min(200, bounds.height) })
  563. cloned.scaleToWidth(tmp.width)
  564. tmp.add(cloned)
  565. tmp.renderAll()
  566. this.clipPreview.dataUrl = tmp.toDataURL({ format:'png', multiplier:1 })
  567. tmp.dispose()
  568. })
  569. }catch(e){
  570. console.warn('clip preview failed', e)
  571. }
  572. }
  573. },
  574. updateClipStroke() {
  575. if (!this.judge() || !this.clipSettings.showClipStroke) return;
  576. return;;
  577. const { shape, width, height, radius, offsetX, offsetY, strokeColor, strokeWidth, rectRadius } = this.clipSettings;
  578. // 移除当前对象已有的描边图形(如果存在)
  579. if (this.editLayer.strokeObj) {
  580. this.fcanvas.remove(this.editLayer.strokeObj);
  581. this.editLayer.strokeObj = null;
  582. }
  583. // 创建新的描边图形
  584. let strokeObj;
  585. if (shape === 'rect') {
  586. strokeObj = new fabric.Rect({
  587. width: width,
  588. height: height,
  589. left: offsetX,
  590. top: offsetY,
  591. rx: rectRadius,
  592. ry: rectRadius,
  593. stroke: strokeColor,
  594. sort:this.editLayer.sort,
  595. strokeWidth: strokeWidth,
  596. fill: null,
  597. selectable: false,
  598. evented: false
  599. });
  600. } else {
  601. strokeObj = new fabric.Circle({
  602. radius: radius,
  603. left: offsetX,
  604. top: offsetY,
  605. sort:this.editLayer.sort,
  606. stroke: strokeColor,
  607. strokeWidth: strokeWidth,
  608. fill: null,
  609. selectable: false,
  610. evented: false
  611. });
  612. }
  613. // 将描边图形绑定到当前对象
  614. this.editLayer.strokeObj = strokeObj;
  615. this.fcanvas.add(markRaw(strokeObj));
  616. this.fcanvas.requestRenderAll();
  617. },
  618. removeClipStroke() {
  619. if (this.clipStrokeObject) {
  620. this.fcanvas.remove(this.clipStrokeObject);
  621. this.clipStrokeObject = null;
  622. }
  623. },
  624. clearClipPath() {
  625. if (!this.judge()) return;
  626. // 清除剪裁路径
  627. this.editLayer.set({ clipPath: null });
  628. // 移除描边图形(如果存在)
  629. if (this.editLayer.strokeObj) {
  630. this.fcanvas.remove(this.editLayer.strokeObj);
  631. this.editLayer.strokeObj = null;
  632. }
  633. // 移除背景对象(如果存在)
  634. if (this.editLayer.clipBackgroundObj) {
  635. this.fcanvas.remove(this.editLayer.clipBackgroundObj);
  636. this.editLayer.clipBackgroundObj = null;
  637. }
  638. // 清除保存的裁剪设置
  639. delete this.editLayer.clipSettings;
  640. // 重置UI设置
  641. this.clipSettings = { ...DEFAULT_CLIP_SETTINGS };
  642. // 更新画布状态
  643. this.updateCanvasState();
  644. this.fcanvas.requestRenderAll();
  645. },
  646. // 更新图片尺寸
  647. updateImageSize(adjustedField) {
  648. if (!this.judge() || this.editLayer.type !== 'image') return;
  649. const { width, height, keepRatio } = this.imageSize;
  650. const originalWidth = this.editLayer.width;
  651. const originalHeight = this.editLayer.height;
  652. if (keepRatio) {
  653. // 保持宽高比的情况下,使用Fabric.js内置的scaleToWidth/scaleToHeight
  654. // 这些函数本身就会保持比例
  655. if (adjustedField === 'width') {
  656. this.editLayer.scaleToWidth(width);
  657. // 更新UI显示
  658. this.imageSize.height = Math.round(this.editLayer.getScaledHeight());
  659. } else {
  660. this.editLayer.scaleToHeight(height);
  661. // 更新UI显示
  662. this.imageSize.width = Math.round(this.editLayer.getScaledWidth());
  663. }
  664. } else {
  665. // 不保持比例的情况下,直接设置scaleX和scaleY
  666. const scaleX = width / originalWidth;
  667. const scaleY = height / originalHeight;
  668. this.editLayer.set({
  669. scaleX: scaleX,
  670. scaleY: scaleY
  671. });
  672. }
  673. // 重新计算边界和位置
  674. this.editLayer.setCoords();
  675. // 更新画布状态(撤销功能)
  676. this.updateCanvasState();
  677. this.fcanvas.requestRenderAll();
  678. // 更新剪裁预览(如果有剪裁)
  679. this.updateClipPreview && this.updateClipPreview(true);
  680. }
  681. }
  682. }