index.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563
  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. };
  21. export default {
  22. components: {
  23. Teleport,
  24. },
  25. data() {
  26. return {
  27. TextboxConfig:TextboxConfig,
  28. options:TextboxConfig.fontFamily,
  29. fontFamilyStyle:"",
  30. opacityValue: 1,
  31. clipPreview: {
  32. dataUrl: '',
  33. maxOffsetX: 0,
  34. maxOffsetY: 0,
  35. maxWidth: 0,
  36. maxHeight: 0,
  37. maxRadius: 0,
  38. },
  39. layerState: {
  40. fontSize: 16,
  41. lineHeight: 1.2,
  42. charSpacing: 0,
  43. fill: '#000000',
  44. textAlign: 'left',
  45. },
  46. shadowText:{
  47. x:0,
  48. y:0,
  49. vague:0,
  50. color:'#000',
  51. },
  52. clipSettings: {
  53. ...DEFAULT_CLIP_SETTINGS,
  54. },
  55. strokeObj: null // 用于引用当前描边图形
  56. }
  57. },
  58. props: {
  59. },
  60. computed: {
  61. editLayer(){
  62. let object = {}
  63. if(this.selected && this.selected.length === 1){
  64. object = this.selected[0]
  65. }
  66. // return reactive(object)
  67. return object
  68. },
  69. fontFamily(){
  70. let obj = {}
  71. this.TextboxConfig.fontFamily.map(item=>{
  72. obj[item.name] = item
  73. })
  74. return obj
  75. },
  76. shadow(){
  77. let shadowText = this.shadowText
  78. return `${shadowText.x}px ${shadowText.y}px ${shadowText.vague}px ${shadowText.color} `
  79. }
  80. },
  81. watch: {
  82. editLayer(){
  83. this.fontFamilyStyle = this.editLayer.fontFamily
  84. this.opacityValue = (this.editLayer && typeof this.editLayer.opacity === 'number')
  85. ? this.editLayer.opacity
  86. : 1
  87. this.layerState = {
  88. ...this.layerState,
  89. fontSize: this.editLayer?.fontSize ?? 16,
  90. lineHeight: this.editLayer?.lineHeight ?? 1,
  91. charSpacing: this.editLayer?.charSpacing ?? 0,
  92. fill: this.editLayer?.fill ?? '#000000',
  93. textAlign: this.editLayer?.textAlign ?? 'left',
  94. }
  95. switch (this.editLayer.type){
  96. case "textbox":
  97. if(this.editLayer.shadow){
  98. this.shadowText = {
  99. x:this.editLayer.shadow.offsetX || 0,
  100. y:this.editLayer.shadow.offsetY || 0,
  101. vague:this.editLayer.shadow.blur || 0,
  102. color:this.editLayer.shadow.color || '#000',
  103. }
  104. }else{
  105. this.shadowText = {
  106. x:0,
  107. y:0,
  108. vague:0,
  109. color:'#000'
  110. }
  111. }
  112. break;
  113. case "image":
  114. // 优先使用默认值
  115. let imageSettings = { ...DEFAULT_CLIP_SETTINGS };
  116. // 若已有 clipPath,合并其中的属性
  117. if (this.editLayer.clipPath) {
  118. imageSettings = {
  119. ...imageSettings,
  120. width: this.editLayer.clipPath.width || imageSettings.width,
  121. height: this.editLayer.clipPath.height || imageSettings.height,
  122. offsetX: this.editLayer.clipPath.left || imageSettings.offsetX,
  123. offsetY: this.editLayer.clipPath.top || imageSettings.offsetY,
  124. shape: this.editLayer.clipPath.type || imageSettings.shape,
  125. };
  126. }
  127. // 如果没有剪裁路径,则 shape 为 ""
  128. if (!this.editLayer.clipPath) {
  129. imageSettings.shape = '';
  130. }
  131. // 更新 clipSettings,用于 UI 显示
  132. this.clipSettings = imageSettings;
  133. // 同步描边图形(如果存在)
  134. if (this.editLayer.strokeObj) {
  135. this.fcanvas.add(markRaw(this.editLayer.strokeObj));
  136. }
  137. this.updateClipPreview && this.updateClipPreview()
  138. break;
  139. }
  140. },
  141. selected(){
  142. this.opacityValue = (this.editLayer && typeof this.editLayer.opacity === 'number')
  143. ? this.editLayer.opacity
  144. : 1
  145. this.layerState = {
  146. ...this.layerState,
  147. fontSize: this.editLayer?.fontSize ?? 16,
  148. lineHeight: this.editLayer?.lineHeight ?? 1,
  149. charSpacing: this.editLayer?.charSpacing ?? 0,
  150. fill: this.editLayer?.fill ?? '#000000',
  151. textAlign: this.editLayer?.textAlign ?? 'left',
  152. }
  153. }
  154. },
  155. created() {
  156. },
  157. mounted() {
  158. },
  159. methods:{
  160. judge(){
  161. return this.editLayer?.id
  162. },
  163. //翻转
  164. Flip(type){
  165. if(!this.judge()) return;
  166. switch (type){
  167. case 'X':
  168. this.editLayer.set({
  169. flipX:!this.editLayer.flipX,
  170. })
  171. break;
  172. case 'Y':
  173. this.editLayer.set({
  174. flipY:!this.editLayer.flipY,
  175. })
  176. break;
  177. }
  178. this.updateCanvasState()
  179. this.fcanvas.requestRenderAll()
  180. },
  181. //合并组
  182. setGroup(){
  183. const group = this.fcanvas.getActiveObject().toGroup();
  184. this.fcanvas.requestRenderAll();
  185. this.selected = [group]
  186. if(this.getLayers) this.getLayers()
  187. this.updateCanvasState()
  188. },
  189. //取消组
  190. unGroup(){
  191. if (!this.fcanvas.getActiveObject()) {
  192. return;
  193. }
  194. if (this.fcanvas.getActiveObject().type !== 'group') {
  195. return;
  196. }
  197. this.fcanvas.getActiveObject().toActiveSelection();
  198. this.selected = this.fcanvas.getActiveObject()._objects
  199. this.fcanvas.requestRenderAll();
  200. if(this.getLayers) this.getLayers()
  201. this.updateCanvasState()
  202. },
  203. //编辑文字
  204. editObj(data = {
  205. label:'',
  206. value:'',
  207. action:'set',
  208. }){
  209. if(!this.judge()) return;
  210. let {label,value,action } = data
  211. action = action || 'set'
  212. // 构造待设置的参数;仅当 label 为 fill 且当前无填充时兜底 #000
  213. let params = value
  214. if (label) {
  215. params = { [label]: value }
  216. if (label === 'fill' && !this.editLayer.fill) {
  217. params.fill = value || '#000'
  218. }
  219. }
  220. this.editLayer[action](params)
  221. if (Array.isArray(this.editLayer._objects)){
  222. this.editLayer._objects?.forEach(item=>{
  223. item[action](params)
  224. })
  225. }
  226. // 同步常用文本状态到 layerState,避免 UI 视图不同步
  227. if (['fontSize','lineHeight','charSpacing','fill','textAlign'].includes(label)) {
  228. this.layerState = {
  229. ...this.layerState,
  230. [label]: value,
  231. }
  232. }
  233. this.updateCanvasState()
  234. this.fcanvas.requestRenderAll()
  235. },
  236. // 通用设置图层属性,解决 markRaw 不响应问题
  237. setLayerAttr(name, val){
  238. if(!this.editLayer) return
  239. this.layerState = {
  240. ...this.layerState,
  241. [name]: val,
  242. }
  243. this.editLayer.set(name, val)
  244. if(Array.isArray(this.editLayer._objects)){
  245. this.editLayer._objects.forEach(o => o.set(name, val))
  246. }
  247. this.updateCanvasState()
  248. this.fcanvas && this.fcanvas.requestRenderAll()
  249. },
  250. // 设置透明度(解决 Vue3 markRaw 不响应)
  251. setOpacity(val){
  252. if(!this.editLayer) return
  253. const num = Number(val)
  254. this.opacityValue = num
  255. this.editLayer.set('opacity', num)
  256. if(Array.isArray(this.editLayer._objects)){
  257. this.editLayer._objects.forEach(o => o.set('opacity', num))
  258. }
  259. this.updateCanvasState()
  260. this.fcanvas && this.fcanvas.requestRenderAll()
  261. },
  262. async setFontFamily(val,item=null){
  263. console.log(val)
  264. console.log(item)
  265. console.log(this.fontFamily)
  266. if(!item && !this.judge()) return;
  267. let font = this.fontFamily[val]
  268. console.log(font)
  269. if(!font) return;
  270. await loadFont(font.name,font.src)
  271. let obj = item || this.editLayer
  272. if(typeof obj.set !== 'function') return;
  273. obj.set({'fontFamily': val});
  274. obj.setSelectionStyles({'fontFamily': val});
  275. this.fontFamilyStyle = val
  276. this.updateCanvasState()
  277. await this.$nextTick()
  278. this.fcanvas.requestRenderAll()
  279. async function loadFont(_fontName, _fontUrl) {
  280. return new Promise((resolve, reject) => {
  281. if (checkFont(_fontName)) {
  282. console.log('已有字体:', _fontName)
  283. return resolve(true)
  284. }
  285. let prefont = new FontFace(
  286. _fontName,
  287. 'url(' + _fontUrl + ')'
  288. );
  289. prefont.load().then(function (loaded_face) {
  290. document.fonts.add(loaded_face);
  291. // document.body.style.fontFamily = (_fontFamily ? _fontFamily + ',' : _fontFamily) + _fontName;
  292. console.log('字体加载成功', loaded_face, document.fonts)
  293. return resolve(true)
  294. }).catch(function (error) {
  295. console.log('字体加载失败', error)
  296. return reject(error)
  297. })
  298. })
  299. }
  300. function checkFont(name){
  301. let values = document.fonts.values();
  302. let isHave=false;
  303. let item = values.next();
  304. while(!item.done&&!isHave)
  305. {
  306. let fontFace=item.value;
  307. if(fontFace.family==name)
  308. {
  309. isHave=true;
  310. }
  311. item=values.next();
  312. }
  313. return isHave;
  314. }
  315. },
  316. remoteMethod(query){
  317. if (query !== '') {
  318. this.options = this.TextboxConfig.fontFamily.filter(item => {
  319. return item.localFile.toLowerCase()
  320. .indexOf(query.toLowerCase()) > -1;
  321. });
  322. } else {
  323. this.options = this.TextboxConfig.fontFamily
  324. }
  325. },
  326. async applyClipPath() {
  327. if (!this.judge()) return;
  328. const { shape, width, height, radius, offsetX, offsetY, rectRadius } = this.clipSettings;
  329. if (shape === '') {
  330. // 选择“无”时,直接清除剪裁
  331. this.clearClipPath();
  332. return;
  333. }
  334. try {
  335. // 创建纯剪裁区域(不带描边)
  336. let clipObj;
  337. if (shape === 'svg') {
  338. // 使用 fabric.js 加载远程 SVG
  339. clipObj = await new Promise((resolve, reject) => {
  340. fabric.loadSVGFromURL(
  341. this.clipSettings.svgUrl,
  342. (loadedObjects, options) => {
  343. console.log('=============');
  344. // 创建组合对象
  345. const svgGroup = fabric.util.groupSVGElements(loadedObjects, options);
  346. console.log(svgGroup);
  347. // 设置尺寸
  348. svgGroup.scaleToWidth(this.clipSettings.svgWidth);
  349. console.log(this.clipSettings.svgWidth);
  350. // svgGroup.scaleToHeight(this.clipSettings.svgHeight);
  351. svgGroup.set({
  352. absolutePositioned: true,
  353. left: offsetX,
  354. top: offsetY,
  355. })
  356. resolve(svgGroup);
  357. }
  358. )
  359. });
  360. }else if (shape === 'rect') {
  361. if (width <= 0 || height <= 0) throw new Error('尺寸必须大于0');
  362. clipObj = new fabric.Rect({
  363. width: width,
  364. height: height,
  365. left: offsetX,
  366. top: offsetY,
  367. rx: rectRadius,
  368. ry: rectRadius,
  369. absolutePositioned: true
  370. });
  371. } else if(shape === 'circle') {
  372. if (radius <= 0) throw new Error('半径必须大于0');
  373. clipObj = new fabric.Circle({
  374. radius: radius,
  375. left: offsetX,
  376. top: offsetY,
  377. absolutePositioned: true
  378. });
  379. }
  380. // 应用剪裁区域
  381. this.editLayer.set({ clipPath: clipObj });
  382. // 更新画布
  383. this.updateClipStroke()
  384. this.updateCanvasState();
  385. this.fcanvas.requestRenderAll();
  386. this.updateClipPreview && this.updateClipPreview(true)
  387. } catch (error) {
  388. console.error('剪裁区域创建失败:', error);
  389. this.$message.error('剪裁设置错误: ' + error.message);
  390. }
  391. },
  392. // 切换剪裁形状,自动填充默认值(位置跟随图层,尺寸为画布一半)
  393. onClipShapeChange(shape){
  394. if(!this.editLayer || this.editLayer.type !== 'image'){
  395. this.clipSettings.shape = shape
  396. return
  397. }
  398. if(!shape){
  399. this.clipSettings.shape = ''
  400. this.clearClipPath()
  401. this.updateClipPreview && this.updateClipPreview(true)
  402. return
  403. }
  404. const obj = this.editLayer
  405. const bounds = obj.getBoundingRect(true)
  406. const canvasW = this.fcanvas?.width || obj.canvas?.width || 800
  407. const canvasH = this.fcanvas?.height || obj.canvas?.height || 800
  408. const initWidth = Math.max(10, Math.round(canvasW / 2))
  409. const initHeight = Math.max(10, Math.round(canvasH / 2))
  410. const initRadius = Math.max(5, Math.round(Math.min(canvasW, canvasH) / 4))
  411. const initSettings = {
  412. shape,
  413. offsetX: Math.round(bounds.left),
  414. offsetY: Math.round(bounds.top),
  415. width: initWidth,
  416. height: initHeight,
  417. radius: initRadius,
  418. rectRadius: this.clipSettings.rectRadius || 0,
  419. }
  420. this.clipSettings = { ...this.clipSettings, ...initSettings }
  421. this.applyClipPath()
  422. },
  423. // 预览辅助,若用户保持旧样式可忽略 dataUrl
  424. updateClipPreview(force=false){
  425. if(!this.editLayer || this.editLayer.type !== 'image') return
  426. const obj = this.editLayer
  427. const bounds = obj.getBoundingRect(true)
  428. this.clipPreview = {
  429. ...this.clipPreview,
  430. maxOffsetX: Math.max(0, Math.floor(bounds.width)),
  431. maxOffsetY: Math.max(0, Math.floor(bounds.height)),
  432. maxWidth: Math.max(10, Math.floor(bounds.width)),
  433. maxHeight: Math.max(10, Math.floor(bounds.height)),
  434. maxRadius: Math.max(5, Math.floor(Math.min(bounds.width, bounds.height)/2)),
  435. }
  436. if(force || this.clipPreview.dataUrl === ''){
  437. try{
  438. // clone 可能是异步的(带 filters 时),使用回调形式避免 callback is not a function
  439. obj.clone((cloned)=>{
  440. if(!cloned) return
  441. const tmp = new fabric.Canvas(document.createElement('canvas'), { width: Math.min(200, bounds.width), height: Math.min(200, bounds.height) })
  442. cloned.scaleToWidth(tmp.width)
  443. tmp.add(cloned)
  444. tmp.renderAll()
  445. this.clipPreview.dataUrl = tmp.toDataURL({ format:'png', multiplier:1 })
  446. tmp.dispose()
  447. })
  448. }catch(e){
  449. console.warn('clip preview failed', e)
  450. }
  451. }
  452. },
  453. updateClipStroke() {
  454. if (!this.judge() || !this.clipSettings.showClipStroke) return;
  455. return;;
  456. const { shape, width, height, radius, offsetX, offsetY, strokeColor, strokeWidth, rectRadius } = this.clipSettings;
  457. // 移除当前对象已有的描边图形(如果存在)
  458. if (this.editLayer.strokeObj) {
  459. this.fcanvas.remove(this.editLayer.strokeObj);
  460. this.editLayer.strokeObj = null;
  461. }
  462. // 创建新的描边图形
  463. let strokeObj;
  464. if (shape === 'rect') {
  465. strokeObj = new fabric.Rect({
  466. width: width,
  467. height: height,
  468. left: offsetX,
  469. top: offsetY,
  470. rx: rectRadius,
  471. ry: rectRadius,
  472. stroke: strokeColor,
  473. sort:this.editLayer.sort,
  474. strokeWidth: strokeWidth,
  475. fill: null,
  476. selectable: false,
  477. evented: false
  478. });
  479. } else {
  480. strokeObj = new fabric.Circle({
  481. radius: radius,
  482. left: offsetX,
  483. top: offsetY,
  484. sort:this.editLayer.sort,
  485. stroke: strokeColor,
  486. strokeWidth: strokeWidth,
  487. fill: null,
  488. selectable: false,
  489. evented: false
  490. });
  491. }
  492. // 将描边图形绑定到当前对象
  493. this.editLayer.strokeObj = strokeObj;
  494. this.fcanvas.add(markRaw(strokeObj));
  495. this.fcanvas.requestRenderAll();
  496. },
  497. removeClipStroke() {
  498. if (this.clipStrokeObject) {
  499. this.fcanvas.remove(this.clipStrokeObject);
  500. this.clipStrokeObject = null;
  501. }
  502. },
  503. clearClipPath() {
  504. if (!this.judge()) return;
  505. // 清除剪裁路径
  506. this.editLayer.set({ clipPath: null });
  507. // 移除描边图形(如果存在)
  508. if (this.editLayer.strokeObj) {
  509. this.fcanvas.remove(this.editLayer.strokeObj);
  510. this.editLayer.strokeObj = null;
  511. }
  512. // 更新画布状态
  513. this.updateCanvasState();
  514. this.fcanvas.requestRenderAll();
  515. }
  516. }
  517. }