// design2核心功能模块 // 注意:此代码在layui.use回调函数内部执行 // 全局变量已在主入口文件中定义,这里只需要初始化 function soonDesign2MainHeight() { var main = document.querySelector('.layui-fluid.main'); if (main && main.clientHeight > 40) return main.clientHeight; var tb = document.getElementById('portal-topbar'); return Math.max(480, window.innerHeight - (tb ? tb.offsetHeight : 0)); } function soonDesign2SidebarWidth() { var colRight = document.querySelector('body.soon-design-page .col-right'); if (colRight && colRight.offsetWidth > 0) return colRight.offsetWidth; return 320; } function soonDesign2ToolboxWidth() { var toolbox = document.querySelector('body.soon-design-page .container .left'); if (toolbox && toolbox.offsetWidth > 0) return toolbox.offsetWidth; return 50; } function soonDesign2CanvasWidth() { var canvasDiv = document.getElementById('canvas-div'); if (canvasDiv && canvasDiv.clientWidth > 0) return canvasDiv.clientWidth; var colLeft = document.querySelector('body.soon-design-page .col-left'); if (colLeft && colLeft.clientWidth > 0) { return Math.max(320, colLeft.clientWidth - soonDesign2ToolboxWidth()); } var docW = document.documentElement.clientWidth || window.innerWidth; return Math.max(320, docW - soonDesign2SidebarWidth() - soonDesign2ToolboxWidth()); } function layoutDesign2Shell() { var isWeb = !!(window.SOON_DEPLOY_CONFIG); var mainH = soonDesign2MainHeight(); var canvasW = soonDesign2CanvasWidth(); var canvasH = isWeb ? Math.max(400, mainH - 130) : (document.body.clientHeight - 100); $('#canvas1').attr('width', canvasW).attr('height', canvasH); $('#canvas2').attr('width', canvasW).attr('height', canvasH); if (!isWeb) { $('#object_attribute').css('height', '470px'); $('#obj_list').css('height', Math.max(120, mainH - 520)); $('.obj_list').css('height', Math.max(80, $('#obj_list').height() - 32)); } return { canvasW: canvasW, canvasH: canvasH }; } var mainEl = document.querySelector('.layui-fluid.main'); if (mainEl) mainEl.style.display = 'flex'; else $('.main').show(); var _design2Layout = layoutDesign2Shell(); // 初始化Canvas对象 // 优化性能:在创建 Fabric Canvas 之前,先设置 canvas context 的 willReadFrequently try { const canvas1El = document.getElementById('canvas1'); const canvas2El = document.getElementById('canvas2'); if (canvas1El) { canvas1El.getContext('2d', { willReadFrequently: true }); } if (canvas2El) { canvas2El.getContext('2d', { willReadFrequently: true }); } } catch (e) { // 如果浏览器不支持 willReadFrequently,忽略错误 } canvas1 = new fabric.Canvas('canvas1') canvas2 = new fabric.Canvas('canvas2') // 同步到 window 对象,确保在 eval() 加载 .jsc 文件时也能访问 window.canvas1 = canvas1; window.canvas2 = canvas2; // 撤销/重做过程中使用的标志位,防止并发操作和误记录历史 window._isApplyingHistory = false; fabric.Object.prototype.objectCaching = false ctx1 = canvas1.getSelectionContext() ctx2 = canvas2.getSelectionContext() is_bgi_add = false // 同步到 window 对象 window.ctx1 = ctx1; window.ctx2 = ctx2; window.is_bgi_add = is_bgi_add; canvas1.zoomToPoint(new fabric.Point(canvas1.width / 2, canvas1.height / 2), (function () { var divW = $('#canvas-div').width() || _design2Layout.canvasW || 800; return divW / 1200 > 1 ? 1 : divW / 1200; })()) canvas2.zoomToPoint(new fabric.Point(canvas2.width / 2, canvas2.height / 2), (function () { var divW = $('#canvas-div').width() || _design2Layout.canvasW || 800; return divW / 1200 > 1 ? 1 : divW / 1200; })()) zoom = (function () { var divW = $('#canvas-div').width() || _design2Layout.canvasW || 800; var z = divW / 1200 > 1 ? 1 : divW / 1200; return z > 0 ? z : 0.5; })() window.zoom = zoom; // 初始化标尺 function initRulers() { // dpi已在主入口文件中定义为全局变量 const mmToPx = dpi / 25.4; // 1mm = 11.811px (300 DPI) const rulerHeight = 20; const rulerWidth = 20; // 获取实际画布的像素尺寸(px) // design2.html 的实际画布尺寸是 1012px x 648px const actualCanvasWidth = canvas1.width || 1012; // 实际画布宽度(px) const actualCanvasHeight = canvas1.height || 648; // 实际画布高度(px) // 获取显示区域的尺寸(包括 margin) const canvasDiv = $('#canvas-div'); const canvasWrapper = $('.canvas-wrapper'); // 使用 canvas-wrapper 的宽度,如果没有则使用 canvas_div 的宽度 let canvasWidth = canvasWrapper.width(); let canvasHeight = canvasWrapper.height(); if (!canvasWidth || canvasWidth === 0) { const canvas1Width = $('#canvas1').width() || canvasDiv.width() - 20; canvasWidth = canvas1Width + 20; // 加上 margin-left: 20px } if (!canvasHeight || canvasHeight === 0) { const canvas1Height = $('#canvas1').height() || canvasDiv.height() - 20; canvasHeight = canvas1Height + 20; // 加上 margin-top: 20px } // 设置标尺尺寸(使用显示区域的尺寸) $('#ruler-h').attr('width', canvasWidth).attr('height', rulerHeight); $('#ruler-v').attr('width', rulerWidth).attr('height', canvasHeight); $('.ruler-horizontal').css('width', canvasWidth + 'px'); $('.ruler-vertical').css('height', canvasHeight + 'px'); // 绘制水平标尺 function drawHorizontalRuler() { const ctx = document.getElementById('ruler-h').getContext('2d', { willReadFrequently: true }); ctx.clearRect(0, 0, canvasWidth, rulerHeight); // 背景 ctx.fillStyle = '#2C2C2C'; ctx.fillRect(0, 0, canvasWidth, rulerHeight); // 边框 ctx.strokeStyle = '#1E1E1E'; ctx.lineWidth = 1; ctx.strokeRect(0, 0, canvasWidth, rulerHeight); ctx.beginPath(); ctx.moveTo(0, rulerHeight - 1); ctx.lineTo(canvasWidth, rulerHeight - 1); ctx.stroke(); // 刻度线 ctx.strokeStyle = '#888888'; ctx.fillStyle = '#CCCCCC'; ctx.font = '10px Arial'; ctx.textAlign = 'left'; ctx.textBaseline = 'top'; // 计算:实际画布尺寸(px)对应的物理尺寸(mm),然后根据显示区域的缩放比例计算 // 实际画布物理宽度 = actualCanvasWidth / dpi * 25.4 (mm) // 显示区域宽度 = actualCanvasWidth * zoom (px) // 所以:1mm 在显示区域中 = (actualCanvasWidth * zoom) / (actualCanvasWidth / dpi * 25.4) = zoom * dpi / 25.4 const pxPerMm = zoom * mmToPx; // 在显示区域中,1mm对应的像素数 const marginOffset = 20; // canvas 的 margin-left: 20px // 标尺0点应该在background_image的左上角位置 // background_image在画布坐标系中的left值,需要转换到标尺坐标系 // 画布从标尺的marginOffset位置开始,所以:标尺0点 = marginOffset + background_image.left (如果存在) let originX = marginOffset; var rulerSide = ($('#back_side').hasClass('ui-button-active')) ? canvas2 : canvas1; var anchorX = typeof soonDesign2GetFrameAnchor === 'function' ? soonDesign2GetFrameAnchor(rulerSide) : null; if (anchorX) { originX = marginOffset + anchorX.left; } else if (typeof background_image !== 'undefined' && background_image) { originX = marginOffset + background_image.left; } const startX = 0; const endX = canvasWidth; // 绘制主要刻度(每10mm) // 从0点向左绘制负值刻度 for (let mm = -10; originX + mm * pxPerMm >= startX; mm -= 10) { const x = originX + mm * pxPerMm; if (x < startX) break; ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, rulerHeight); ctx.stroke(); // 数字(负值) ctx.fillText(mm + 'mm', x + 2, 2); } // 从0点向右绘制正值刻度 for (let mm = 0; originX + mm * pxPerMm <= endX; mm += 10) { const x = originX + mm * pxPerMm; if (x > endX) break; ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, rulerHeight); ctx.stroke(); // 数字 ctx.fillText(mm + 'mm', x + 2, 2); } // 绘制次要刻度(每1mm) // 从0点向左绘制负值刻度 for (let mm = -1; originX + mm * pxPerMm >= startX; mm -= 1) { const x = originX + mm * pxPerMm; if (x < startX) break; if (mm % 10 === 0) continue; // 跳过主要刻度 ctx.beginPath(); ctx.moveTo(x, rulerHeight - 8); ctx.lineTo(x, rulerHeight); ctx.stroke(); } // 从0点向右绘制正值刻度 for (let mm = 0; originX + mm * pxPerMm <= endX; mm += 1) { const x = originX + mm * pxPerMm; if (x > endX) break; if (mm % 10 === 0) continue; // 跳过主要刻度 ctx.beginPath(); ctx.moveTo(x, rulerHeight - 8); ctx.lineTo(x, rulerHeight); ctx.stroke(); } } // 绘制垂直标尺 function drawVerticalRuler() { const ctx = document.getElementById('ruler-v').getContext('2d', { willReadFrequently: true }); ctx.clearRect(0, 0, rulerWidth, canvasHeight); // 背景 ctx.fillStyle = '#2C2C2C'; ctx.fillRect(0, 0, rulerWidth, canvasHeight); // 边框 ctx.strokeStyle = '#1E1E1E'; ctx.lineWidth = 1; ctx.strokeRect(0, 0, rulerWidth, canvasHeight); ctx.beginPath(); ctx.moveTo(rulerWidth - 1, 0); ctx.lineTo(rulerWidth - 1, canvasHeight); ctx.stroke(); // 刻度线 ctx.strokeStyle = '#888888'; ctx.fillStyle = '#CCCCCC'; ctx.font = '10px Arial'; ctx.textAlign = 'left'; ctx.textBaseline = 'top'; // 计算:实际画布尺寸(px)对应的物理尺寸(mm),然后根据显示区域的缩放比例计算 const pxPerMm = zoom * mmToPx; // 在显示区域中,1mm对应的像素数 const marginOffset = 20; // canvas 的 margin-top: 20px (但还要加上ruler的高度) const rulerHeightOffset = 20; // ruler 的高度 // 标尺0点应该在background_image的左上角位置 // background_image在画布坐标系中的top值,需要转换到标尺坐标系 // 画布从标尺的marginOffset + rulerHeightOffset位置开始,所以:标尺0点 = marginOffset + rulerHeightOffset + background_image.top (如果存在) let originY = marginOffset + rulerHeightOffset; var rulerSideY = ($('#back_side').hasClass('ui-button-active')) ? canvas2 : canvas1; var anchorY = typeof soonDesign2GetFrameAnchor === 'function' ? soonDesign2GetFrameAnchor(rulerSideY) : null; if (anchorY) { originY = marginOffset + rulerHeightOffset + anchorY.top; } else if (typeof background_image !== 'undefined' && background_image) { originY = marginOffset + rulerHeightOffset + background_image.top; } const startY = 0; const endY = canvasHeight; // 绘制主要刻度(每10mm) // 从0点向上绘制负值刻度 for (let mm = -10; originY + mm * pxPerMm >= startY; mm -= 10) { const y = originY + mm * pxPerMm; if (y < startY) break; ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(rulerWidth, y); ctx.stroke(); // 数字(旋转90度,负值) ctx.save(); ctx.translate(2, y + 8); ctx.rotate(-Math.PI / 2); ctx.fillText(mm + 'mm', 0, 0); ctx.restore(); } // 从0点向下绘制正值刻度 for (let mm = 0; originY + mm * pxPerMm <= endY; mm += 10) { const y = originY + mm * pxPerMm; if (y > endY) break; ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(rulerWidth, y); ctx.stroke(); // 数字(旋转90度) ctx.save(); ctx.translate(2, y + 8); ctx.rotate(-Math.PI / 2); ctx.fillText(mm + 'mm', 0, 0); ctx.restore(); } // 绘制次要刻度(每1mm) // 从0点向上绘制负值刻度 for (let mm = -1; originY + mm * pxPerMm >= startY; mm -= 1) { const y = originY + mm * pxPerMm; if (y < startY) break; if (mm % 10 === 0) continue; // 跳过主要刻度 ctx.beginPath(); ctx.moveTo(rulerWidth - 8, y); ctx.lineTo(rulerWidth, y); ctx.stroke(); } // 从0点向下绘制正值刻度 for (let mm = 0; originY + mm * pxPerMm <= endY; mm += 1) { const y = originY + mm * pxPerMm; if (y > endY) break; if (mm % 10 === 0) continue; // 跳过主要刻度 ctx.beginPath(); ctx.moveTo(rulerWidth - 8, y); ctx.lineTo(rulerWidth, y); ctx.stroke(); } } // 初始绘制 drawHorizontalRuler(); drawVerticalRuler(); // 监听窗口大小变化 $(window).on('resize', function () { setTimeout(function () { const canvasWrapper = $('.canvas-wrapper'); let newWidth = canvasWrapper.width(); let newHeight = canvasWrapper.height(); if (!newWidth || newWidth === 0) { const canvas1Width = $('#canvas1').width() || $('#canvas-div').width() - 20; newWidth = canvas1Width + 20; // 加上 margin-left: 20px } if (!newHeight || newHeight === 0) { const canvas1Height = $('#canvas1').height() || $('#canvas-div').height() - 20; newHeight = canvas1Height + 20; // 加上 margin-top: 20px } if (newWidth !== canvasWidth || newHeight !== canvasHeight) { canvasWidth = newWidth; canvasHeight = newHeight; $('#ruler-h').attr('width', newWidth); $('#ruler-v').attr('height', newHeight); $('.ruler-horizontal').css('width', newWidth + 'px'); $('.ruler-vertical').css('height', newHeight + 'px'); drawHorizontalRuler(); drawVerticalRuler(); } }, 100); }); canvas1.off('after:render'); canvas2.off('after:render'); canvas1.on('after:render', function () { drawHorizontalRuler(); drawVerticalRuler(); soonDesign2DrawCardMask(canvas1, ctx1, document.getElementById('source_front')); soonDesign2DrawTemplateOverlay(canvas1, ctx1, document.getElementById('source_front')); }); canvas2.on('after:render', function () { drawHorizontalRuler(); drawVerticalRuler(); soonDesign2DrawCardMask(canvas2, ctx2, document.getElementById('source_back')); soonDesign2DrawTemplateOverlay(canvas2, ctx2, document.getElementById('source_back')); }); // --- 辅助线功能 (参考 design1) --- // canvasWrapper 已在上面声明,直接使用 const canvasBox = $('#canvas-div'); // design2 使用 canvas-div 作为容器 // 确保容器定位 if (canvasWrapper.css('position') === 'static') canvasWrapper.css('position', 'relative'); // 定义临时拖拽线 DOM let $dragLine = null; // 通用拖拽开始函数 const startDragGuide = (type, e) => { e.preventDefault(); e.stopPropagation(); // 防止重复调用:如果正在拖拽,直接返回 if ($dragLine && $dragLine.length > 0) { return; } // 创建临时显示的线 $dragLine = $('
'); canvasWrapper.append($dragLine); if (type === 'horizontal') { // 从上往下拖 (水平线) $dragLine.css({ left: 0, width: '100%', height: '1px', top: e.offsetY }); } else { // 从左往右拖 (垂直线) $dragLine.css({ top: 0, height: '100%', width: '1px', left: e.offsetX }); } // 绑定 document 移动事件 $(document).on('mousemove.ruler', function (moveEvent) { let wrapperOffset = canvasWrapper.offset(); if (type === 'horizontal') { let relY = moveEvent.pageY - wrapperOffset.top; $dragLine.css('top', relY + 'px'); } else { let relX = moveEvent.pageX - wrapperOffset.left; $dragLine.css('left', relX + 'px'); } }); // 绑定松开事件 (创建真实的 Fabric 线) $(document).on('mouseup.ruler', function (upEvent) { $(document).off('mousemove.ruler mouseup.ruler'); if (!$dragLine) return; // 获取最终位置 let wrapperOffset = canvasWrapper.offset(); let finalX = upEvent.pageX - wrapperOffset.left; let finalY = upEvent.pageY - wrapperOffset.top; // 移除临时线 $dragLine.remove(); $dragLine = null; // 只有拖入画布区域才创建 if (finalX > rulerWidth && finalY > rulerHeight) { createFabricGuide(type, finalX, finalY); } }); }; // 绑定到标尺 DOM $('#ruler-h').off('mousedown').on('mousedown', (e) => startDragGuide('horizontal', e)); $('#ruler-v').off('mousedown').on('mousedown', (e) => startDragGuide('vertical', e)); // --- 在画布中创建辅助线 --- function createFabricGuide(type, domX, domY) { // domX 和 domY 是相对于 canvas-wrapper 的坐标 // 需要转换为相对于 canvas 的坐标,然后再转换为 Fabric 画布坐标 // 获取 canvas 相对于 canvas-wrapper 的位置 let canvas1El = $('#canvas1'); let canvasPosition = canvas1El.position(); let canvasMarginLeft = parseFloat(canvas1El.css('margin-left')) || 0; let canvasMarginTop = parseFloat(canvas1El.css('margin-top')) || 0; // 计算相对于 canvas 的坐标(减去 canvas 在 wrapper 中的位置和 margin) let relX = domX - canvasPosition.left - canvasMarginLeft; let relY = domY - canvasPosition.top - canvasMarginTop; // 转为 Fabric 坐标 (除以 zoom) let canvasX = relX / zoom; let canvasY = relY / zoom; // 使用与画布尺寸相关的长度,避免“整块区域都像辅助线” let lineExtent = Math.max(canvas1.width, canvas1.height) * 2; // 构造线对象 let linePoints = []; if (type === 'horizontal') { linePoints = [-lineExtent, canvasY, lineExtent, canvasY]; } else { linePoints = [canvasX, -lineExtent, canvasX, lineExtent]; } let guideLine = new fabric.Line(linePoints, { stroke: '#0066FF', strokeWidth: 1 / zoom, opacity: 0.9, shadow: { color: '#0066FF', blur: 6, offsetX: 0, offsetY: 0, affectStroke: true }, selectable: true, evented: true, hasControls: false, hasBorders: false, lockRotation: true, lockScalingX: true, lockScalingY: true, hoverCursor: type === 'horizontal' ? 'ns-resize' : 'ew-resize', // 只在靠近线条时触发命中,避免“大块区域都像辅助线” // 同时适当放宽 tolerance,保证易于点中拖拽 perPixelTargetFind: true, targetFindTolerance: 6, padding: 2, isGuideLine: true, ignoreSave: true }); // 限制移动方向(只约束方向,不把整条线拖到极远,避免“整片区域是辅助线”的感觉) guideLine.on('moving', function () { if (type === 'horizontal') { // 水平线:根据当前中心点的 Y,重算 x1/x2,使其横穿画布 const cy = this.getCenterPoint().y; this.set({ x1: -lineExtent, y1: cy, x2: lineExtent, y2: cy }); } else { // 垂直线:根据当前中心点的 X,重算 y1/y2,使其竖穿画布 const cx = this.getCenterPoint().x; this.set({ x1: cx, y1: -lineExtent, x2: cx, y2: lineExtent }); } }); // 确定应该添加到哪个 canvas(使用当前活动的 canvas) let targetCanvas = canvas; if (!targetCanvas || (targetCanvas !== canvas1 && targetCanvas !== canvas2)) { // 如果没有活动的 canvas,默认使用 canvas1 targetCanvas = canvas1; } targetCanvas.add(guideLine); targetCanvas.setActiveObject(guideLine); targetCanvas.renderAll(); } } function relayoutDesign2Canvas() { var layout = layoutDesign2Shell(); var divW = layout.canvasW; zoom = divW / 1200 > 1 ? 1 : divW / 1200; if (zoom <= 0) zoom = 0.5; window.zoom = zoom; canvas1.setWidth(layout.canvasW); canvas1.setHeight(layout.canvasH); canvas2.setWidth(layout.canvasW); canvas2.setHeight(layout.canvasH); canvas1.zoomToPoint(new fabric.Point(canvas1.width / 2, canvas1.height / 2), zoom); canvas2.zoomToPoint(new fabric.Point(canvas2.width / 2, canvas2.height / 2), zoom); canvas1.renderAll(); canvas2.renderAll(); initRulers(); if (typeof window.soonDesign2ScaleBgSources === 'function') { window.soonDesign2ScaleBgSources(); } } function soonDesign2ScaleBgSources() { var z = zoom || window.zoom || 0.5; var sf = document.getElementById('source_front'); var sb = document.getElementById('source_back'); if (sf && sf.naturalWidth > 0) { sf.style.width = (sf.naturalWidth * z) + 'px'; sf.style.height = (sf.naturalHeight * z) + 'px'; } if (sb && sb.naturalWidth > 0) { sb.style.width = (sb.naturalWidth * z) + 'px'; sb.style.height = (sb.naturalHeight * z) + 'px'; } } function soonDesign2BgOverlayMetrics(imgEl) { if (!imgEl || !imgEl.complete || imgEl.naturalWidth <= 0) return null; var z = zoom || window.zoom || 0.5; var w = parseFloat(imgEl.style.width); if (!w || w <= 0) w = imgEl.naturalWidth * z; var h = parseFloat(imgEl.style.height); if (!h || h <= 0) h = imgEl.naturalHeight * z; return { w: w, h: h }; } /** 卡面区域:与 background_image 对齐(1012×648),fallback 模板线框 */ function soonDesign2ResolveCardFrame(fabricCanvas, imgEl) { if (!fabricCanvas) return null; var metaList = (fabricCanvas === canvas2) ? (window.objs2 || objs2) : (window.objs1 || objs1); if (soonDesign2IsUserBgMeta(metaList)) { var anchor = soonDesign2GetFrameAnchor(fabricCanvas); var card = soonDesign2CardSize(); if (anchor) { return { left: anchor.left, top: anchor.top, width: card.w, height: card.h }; } } var bg = (fabricCanvas === canvas1) ? background_image1 : background_image2; if (bg && bg.width) { var sx = bg.scaleX == null ? 1 : bg.scaleX; var sy = bg.scaleY == null ? 1 : bg.scaleY; return { left: bg.left, top: bg.top, width: bg.width * sx, height: bg.height * sy }; } var cw = fabricCanvas.width || 1012; var ch = fabricCanvas.height || 648; if (imgEl) { var metrics = soonDesign2BgOverlayMetrics(imgEl); if (metrics) { return { left: (cw - metrics.w) / 2, top: (ch - metrics.h) / 2, width: metrics.w, height: metrics.h }; } } return { left: 0, top: 0, width: cw, height: ch }; } function soonDesign2CardSize() { return { w: 1012, h: 648 }; } function soonDesign2IsUserBgMeta(metaList) { return !!(metaList && metaList.length > 0 && metaList[0] && metaList[0].type === 1); } function soonDesign2SyncFrameAnchor(fabricCanvas, frame) { if (!frame) return; var pt = { left: frame.left, top: frame.top }; if (fabricCanvas === canvas2) { window.soonDesign2FrameAnchor2 = pt; } else { window.soonDesign2FrameAnchor1 = pt; } var active = window.canvas || canvas; if (!active || fabricCanvas === active) { window.soonDesign2FrameAnchor = pt; } } function soonDesign2GetFrameAnchor(fabricCanvas) { if (fabricCanvas === canvas2 && window.soonDesign2FrameAnchor2) { return window.soonDesign2FrameAnchor2; } if (window.soonDesign2FrameAnchor1) { return window.soonDesign2FrameAnchor1; } return window.soonDesign2FrameAnchor || null; } /** 用户设为背景的对象:坐标系锚点,属性面板不应再移动它 */ function soonDesign2ActiveIsAnchorBg() { var c = window.canvas || canvas; if (!c) return false; var ao = c.getActiveObject(); if (!ao) return false; var meta = (c === canvas2) ? (window.objs2 || objs2) : (window.objs1 || objs1); if (c === (window.canvas || canvas)) { meta = window.objs || objs || meta; } if (!soonDesign2IsUserBgMeta(meta)) return false; var bg = (c === canvas2) ? (window.background_image2 || background_image2) : (window.background_image1 || background_image1); return ao === bg; } function soonDesign2FrameOrigin(fabricCanvas) { var frame = soonDesign2ResolveCardFrame(fabricCanvas, null); if (frame) return frame; var card = soonDesign2CardSize(); var cw = (fabricCanvas && fabricCanvas.width) || card.w; var ch = (fabricCanvas && fabricCanvas.height) || card.h; return { left: (cw - card.w) / 2, top: (ch - card.h) / 2, width: card.w, height: card.h }; } /** 用户图铺满卡面框(1012×648),与蒙版/线框同源坐标 */ function soonDesign2SnapImageToCardFrame(img, fabricCanvas, frame) { if (!img || !img.width) return frame || soonDesign2FrameOrigin(fabricCanvas); frame = frame || soonDesign2FrameOrigin(fabricCanvas); var card = soonDesign2CardSize(); img.set({ originX: 'left', originY: 'top', scaleX: card.w / img.width, scaleY: card.h / img.height, left: frame.left, top: frame.top }); img.setCoords(); soonDesign2SyncFrameAnchor(fabricCanvas, frame); return frame; } function soonDesign2ApplyUserBgAsCanvas0(userImg, fabricCanvas, oldBg) { if (!userImg || !fabricCanvas) return; if (oldBg && oldBg !== userImg) { fabricCanvas.remove(oldBg); } fabricCanvas.sendToBack(userImg); if (fabricCanvas === canvas2) { background_image2 = userImg; window.background_image2 = userImg; } else { background_image1 = userImg; window.background_image1 = userImg; } var activeCanvas = window.canvas || canvas; if (fabricCanvas === activeCanvas) { background_image = userImg; window.background_image = userImg; } } function soonDesign2LockBgObject(bgObj) { if (!bgObj) return; bgObj.set({ lockMovementX: true, lockMovementY: true, lockRotation: true, lockScalingX: true, lockScalingY: true, lockSkewingX: true, lockSkewingY: true, selectable: false, evented: false, hoverCursor: 'default' }); } /** 打开旧文件:去掉残留模板层,用户背景独占 canvas[0] 并贴齐卡框 */ function soonDesign2NormalizeUserBg(fabricCanvas, metaList) { if (!soonDesign2IsUserBgMeta(metaList) || !fabricCanvas) return null; var all = fabricCanvas.getObjects().filter(function (o) { return !o.isGuideLine; }); if (!all.length) return null; var frame = soonDesign2FrameOrigin(fabricCanvas); var userBg = all[0]; if (all.length > 1) { fabricCanvas.remove(all[0]); userBg = fabricCanvas.getObjects().filter(function (o) { return !o.isGuideLine; })[0]; } if (!userBg) return null; soonDesign2SnapImageToCardFrame(userBg, fabricCanvas, frame); soonDesign2ApplyUserBgAsCanvas0(userBg, fabricCanvas, null); soonDesign2LockBgObject(userBg); return userBg; } window.soonDesign2CardSize = soonDesign2CardSize; window.soonDesign2IsUserBgMeta = soonDesign2IsUserBgMeta; window.soonDesign2SyncFrameAnchor = soonDesign2SyncFrameAnchor; window.soonDesign2GetFrameAnchor = soonDesign2GetFrameAnchor; window.soonDesign2ActiveIsAnchorBg = soonDesign2ActiveIsAnchorBg; window.soonDesign2FrameOrigin = soonDesign2FrameOrigin; window.soonDesign2SnapImageToCardFrame = soonDesign2SnapImageToCardFrame; window.soonDesign2ApplyUserBgAsCanvas0 = soonDesign2ApplyUserBgAsCanvas0; window.soonDesign2LockBgObject = soonDesign2LockBgObject; window.soonDesign2NormalizeUserBg = soonDesign2NormalizeUserBg; function soonDesign2DrawCardMask(fabricCanvas, ctx, imgEl) { if (window._soonSkipDesign2CardMask || !fabricCanvas || !ctx) return; var frame = soonDesign2ResolveCardFrame(fabricCanvas, imgEl); if (!frame) return; var left = frame.left; var top = frame.top; var cardW = frame.width; var cardH = frame.height; ctx.save(); ctx.beginPath(); ctx.rect(0, 0, fabricCanvas.width, fabricCanvas.height); ctx.moveTo(left + cardW, top); ctx.lineTo(left, top); ctx.lineTo(left, top + cardH); ctx.lineTo(left + cardW, top + cardH); ctx.closePath(); ctx.fillStyle = 'rgba(0, 0, 0, 0.9)'; ctx.fill('evenodd'); ctx.restore(); } function soonDesign2DrawTemplateOverlay(fabricCanvas, ctx, imgEl) { if (!window.is_bgi_add || !fabricCanvas || !ctx || !imgEl) return; var frame = soonDesign2ResolveCardFrame(fabricCanvas, imgEl); if (!frame) return; ctx.drawImage(imgEl, frame.left, frame.top, frame.width, frame.height); } function soonDesign2PrepareBgSources(done) { var sf = document.getElementById('source_front'); var sb = document.getElementById('source_back'); if (!sf || !sb) { is_bgi_add = true; window.is_bgi_add = true; if (typeof done === 'function') done(); return; } var ver = typeof bg_version !== 'undefined' ? bg_version : 1; var frontName = 'front_bg' + ver + '_2.png'; if (!(sf.complete && sf.naturalWidth > 0 && (sf.src || '').indexOf(frontName) !== -1)) { sf.src = soonAsset(frontName); } var pending = 2; function finishOne() { pending--; if (pending > 0) return; soonDesign2ScaleBgSources(); is_bgi_add = true; window.is_bgi_add = true; canvas1.requestRenderAll(); canvas2.requestRenderAll(); if (typeof done === 'function') done(); } function watch(img) { if (img.complete && img.naturalWidth > 0) { finishOne(); return; } img.onload = finishOne; img.onerror = finishOne; } watch(sf); watch(sb); } function soonDesign2StartBackground() { soonDesign2PrepareBgSources(addBackground); } function soonDesign2Kickoff() { if (window._soonDesign2KickoffDone) return; window._soonDesign2KickoffDone = true; if (window.SOON_DEPLOY_CONFIG && ($('#canvas-div').width() || 0) < 100) { relayoutDesign2Canvas(); } soonDesign2StartBackground(); } window.soonDesign2ScaleBgSources = soonDesign2ScaleBgSources; window.soonDesign2PrepareBgSources = soonDesign2PrepareBgSources; window.soonDesign2Kickoff = soonDesign2Kickoff; initRulers(); $('.canvas-container:eq(1)').hide() canvas = canvas1 window.canvas = canvas fabric.Object.prototype.set({ borderColor: '#9013FE', cornerColor: '#9013FE', //激活状态角落图标的填充颜色 cornerStrokeColor: '#9013FE', //激活状态角落图标的边框颜色 borderOpacityWhenMoving: 1, borderScaleFactor: 1, cornerSize: 6, cornerStyle: 'circle', //rect,circle centeredScaling: false, //角落放大缩小是否是以图形中心为放大原点 centeredRotation: true, //旋转按钮旋转是否是左上角为圆心旋转 transparentCorners: false, //激活状态角落的图标是否透明 rotatingPointOffset: 20, //旋转距旋转体的距离 padding: 0 }) canvas1.preserveObjectStacking = true canvas2.preserveObjectStacking = true // ========================================== // 对象选择和属性更新函数 // ========================================== // 将 updateControls 附加到 window 对象,确保在 eval() 加载 .jsc 文件时也能访问 window._soonSyncingFontControls = false; function soonFontSizeUiVal(fontSizePx) { var d = window.dpi || 300; var uiVal = parseFloat(fontSizePx) * 72 / d; if (uiVal % 0.5 !== 0) uiVal = Math.round(uiVal * 2) / 2; return uiVal; } function soonSyncActiveFontSizeInput(activeObj, currentCanvas) { if (!activeObj || !currentCanvas) return; var idx = getIndex(activeObj, currentCanvas); if (idx <= 0) return; var objData = (currentCanvas === canvas1 ? objs1 : objs2)[idx - 1]; if (!objData) return; var uiVal = soonFontSizeUiVal(activeObj.get('fontSize')); if (objData.type === 3) $("#text_size").val(uiVal); else if (objData.type === 4) $("#out_text_size").val(uiVal); else if (objData.type === 10) $("#count_size").val(uiVal); } window.updateControls = function () { const currentCanvas = window.canvas || canvas; const currentBackgroundImage = window.background_image || background_image; if (!currentCanvas || !currentCanvas.getActiveObject()) return; const activeObj = currentCanvas.getActiveObject(); window._soonSyncingFontControls = true; try { $("#text_r").val(activeObj.get("angle").toFixed(0)); $("#text_x").val((activeObj.get("left") - currentBackgroundImage.left).toFixed(0)); $("#text_y").val((activeObj.get("top") - currentBackgroundImage.top).toFixed(0)); $("#text_h").val((activeObj.get("scaleY") * activeObj.get("height")).toFixed(0)); $("#text_w").val((activeObj.get("scaleX") * activeObj.get("width")).toFixed(0)); soonSyncActiveFontSizeInput(activeObj, currentCanvas); if (activeObj.get("lockMovementX")) { $("#lock_img").attr("src", soonAsset("lock20.png")); } else { $("#lock_img").attr("src", soonAsset("unlock20.png")); } } finally { window._soonSyncingFontControls = false; } }; // 向后兼容 function updateControls() { return window.updateControls(); } function selectObject(target, index) { updateControls(); $("#base_control").show(); } function selectPic(target, index) { // 检查是否有选中对象,如果没有则直接返回 if (!canvas || !canvas.getActiveObject()) return; $("#pic_control").show(); $("#set_bg").show(); let activeObj = canvas.getActiveObject(); // 检查对象是否被锁定: // - 从画布点击时(_selectingFromList 为 false):不允许选中,被锁定的对象保持不可选状态 // - 从对象列表点击时(_selectingFromList 为 true):允许选中,但对象在画布上仍然保持锁定 if (!window._selectingFromList && isObjectLocked(activeObj)) { canvas.discardActiveObject(); canvas.renderAll(); return; } if (typeof pic_op !== 'undefined' && pic_op && typeof pic_op.setValue === 'function') { pic_op.setValue(100 - (activeObj.get("opacity") * 100)); } else { $("#pic_op").next().children(".layui-slider-bar").css("width", (1 - activeObj.get("opacity")) * 100 + "%"); $("#pic_op").next().children(".layui-slider-handle").css("left", (1 - activeObj.get("opacity")) * 100 + "%"); } $("#out_pic_control").hide(); $("#text_control").hide(); $("#circle_text_control").hide(); $("#out_text_control").hide(); $("#rect_control").hide(); $("#circle_control").hide(); $("#line_control").hide(); $("#counter_control").hide(); $("#qrcode_Controll").hide(); $("#barcode_controll").hide(); } function selectOutPic(target, index) { $("#out_pic_control").show(); if (typeof out_pic_op !== 'undefined' && out_pic_op.setValue) { out_pic_op.setValue(100 - (canvas.getActiveObject().get("opacity") * 100)); } else { $("#out_pic_op").next().children(".layui-slider-bar").css("width", (1 - canvas.getActiveObject().get("opacity")) * 100 + "%"); $("#out_pic_op").next().children(".layui-slider-handle").css("left", (1 - canvas.getActiveObject().get("opacity")) * 100 + "%"); } let idx = getIndex(canvas.getActiveObject(), canvas); $("#out_pic_field_name").val(objs[idx - 1].name); $("#pic_control").hide(); $("#set_bg").hide(); $("#text_control").hide(); $("#circle_text_control").hide(); $("#out_text_control").hide(); $("#rect_control").hide(); $("#circle_control").hide(); $("#line_control").hide(); $("#counter_control").hide(); $("#qrcode_Controll").hide(); $("#barcode_controll").hide(); } function selectText(target, index) { $("#text_control").show(); $("#text_color").val(canvas.getActiveObject().get("fill")); $("#text_back_color").val(canvas.getActiveObject().get("textBackgroundColor") == "" ? '#ffffff' : canvas.getActiveObject().get("textBackgroundColor")); let val = parseFloat(canvas.getActiveObject().get("fontSize")) * 72 / dpi if (val % 0.5 !== 0) val = Math.round(val * 2) / 2 $("#text_size").val(val); $("#text_text").val(canvas.getActiveObject().get("text")); $("#text_font_family").val(canvas.getActiveObject().get("fontFamily")); if (canvas.getActiveObject().get("underline") == true) { $("#text_underline").addClass("ui-radio-acitve") } else { $("#text_underline").removeClass("ui-radio-acitve") } if (canvas.getActiveObject().get("fontWeight") == "bold") { $("#text_bold").addClass("ui-radio-acitve") } else { $("#text_bold").removeClass("ui-radio-acitve") } if (canvas.getActiveObject().get("fontStyle") == "italic") { $("#text_italics").addClass("ui-radio-acitve") } else { $("#text_italics").removeClass("ui-radio-acitve") } if (canvas.getActiveObject().get("linethrough") == true) { $("#text_linethrough").addClass("ui-radio-acitve") } else { $("#text_linethrough").removeClass("ui-radio-acitve") } $("#circle_text_control").hide(); $("#pic_control").hide(); $("#set_bg").hide(); $("#out_pic_control").hide(); $("#out_text_control").hide(); $("#rect_control").hide(); $("#circle_control").hide(); $("#line_control").hide(); $("#counter_control").hide(); $("#qrcode_Controll").hide(); $("#barcode_controll").hide(); } function selectCircleText(target, index) { const activeObj = target || canvas.getActiveObject(); if (!activeObj) return; let idx = getIndex(activeObj); $("#circle_text_control").show(); $("#circle_text_color").val(activeObj.get("fill")); $("#circle_text_back_color").val(activeObj.get("textBackgroundColor") == "" ? '#ffffff' : activeObj.get("textBackgroundColor")); if (typeof circle_text_size !== 'undefined' && circle_text_size && typeof circle_text_size.setValue === 'function') { if (typeof circle_text_size_updating !== 'undefined') { circle_text_size_updating = true; } circle_text_size.setValue(activeObj.get("fontSize") - 10); if (typeof circle_text_size_updating !== 'undefined') { circle_text_size_updating = false; } } if (typeof circle_text_diameter !== 'undefined' && circle_text_diameter && typeof circle_text_diameter.setValue === 'function') { circle_text_diameter.setValue(activeObj.get("diameter") - 50); } $("#circle_text_center").prop("checked", objs[idx - 1].circleCenter ? 'checked' : ''); $("#circle_text_text").val(activeObj.get("text")); $("#circle_text_font_family").val(activeObj.get("fontFamily")); if (activeObj.get("underline") == true) { $("#circle_text_underline").addClass("ui-radio-acitve") } else { $("#circle_text_underline").removeClass("ui-radio-acitve") } if (activeObj.get("fontWeight") == "bold") { $("#circle_text_bold").addClass("ui-radio-acitve") } else { $("#circle_text_bold").removeClass("ui-radio-acitve") } if (activeObj.get("fontStyle") == "italic") { $("#circle_text_italics").addClass("ui-radio-acitve") } else { $("#circle_text_italics").removeClass("ui-radio-acitve") } if (activeObj.get("linethrough") == true) { $("#circle_text_linethrough").addClass("ui-radio-acitve") } else { $("#circle_text_linethrough").removeClass("ui-radio-acitve") } $("#pic_control").hide(); $("#set_bg").hide(); $("#rotate").hide(); $("#out_pic_control").hide(); $("#text_control").hide(); $("#out_text_control").hide(); $("#rect_control").hide(); $("#circle_control").hide(); $("#line_control").hide(); $("#counter_control").hide(); $("#qrcode_Controll").hide(); $("#barcode_controll").hide(); } function selectOutText(target, index) { const align = canvas.getActiveObject().get('textAlign') if (align === 'left') { $('#out_left_alignment').parent().addClass('tool1') } else if (align === 'right') { $('#out_right_alignment').parent().addClass('tool1') } else { $('#out_center_alignment').parent().addClass('tool1') } $("#out_text_control").show(); $("#out_text_color").val(canvas.getActiveObject().get("fill")); $("#out_text_text").val(canvas.getActiveObject().get("text")); let val = parseFloat(canvas.getActiveObject().get("fontSize")) * 72 / dpi if (val % 0.5 !== 0) val = Math.round(val * 2) / 2 $("#out_text_size").val(val); $("#out_text_back_color").val(canvas.getActiveObject().get("textBackgroundColor") == "" ? '#ffffff' : canvas.getActiveObject().get("textBackgroundColor")); let idx = getIndex(canvas.getActiveObject(), canvas); $("#out_text_field_name").val(objs[idx - 1].name); $("#out_text_font_family").val(canvas.getActiveObject().get("fontFamily")); $("#exceed").prop("checked", objs[idx - 1].exceed ? 'checked' : ''); $("#autoWrap").prop("checked", objs[idx - 1].autoWrap ? 'checked' : ''); $("#fixSize").prop("checked", objs[idx - 1].fixSize ? 'checked' : ''); if (canvas.getActiveObject().get("underline") == true) { $("#out_text_underline").addClass("ui-radio-acitve") } else { $("#out_text_underline").removeClass("ui-radio-acitve") } if (canvas.getActiveObject().get("fontWeight") == "bold") { $("#out_text_bold").addClass("ui-radio-acitve") } else { $("#out_text_bold").removeClass("ui-radio-acitve") } if (canvas.getActiveObject().get("fontStyle") == "italic") { $("#out_text_italics").addClass("ui-radio-acitve") } else { $("#out_text_italics").removeClass("ui-radio-acitve") } if (canvas.getActiveObject().get("linethrough") == true) { $("#out_text_linethrough").addClass("ui-radio-acitve") } else { $("#out_text_linethrough").removeClass("ui-radio-acitve") } $("#pic_control").hide(); $("#set_bg").hide(); $("#out_pic_control").hide(); $("#text_control").hide(); $("#circle_text_control").hide(); $("#rect_control").hide(); $("#circle_control").hide(); $("#line_control").hide(); $("#counter_control").hide(); $("#qrcode_Controll").hide(); $("#barcode_controll").hide(); } function selectRect(target, index) { $("#rect_control").show(); $("#rect_color").val(canvas.getActiveObject().get("fill") == "" ? "#ffffff" : canvas.getActiveObject().get("fill")); $("#rect_stroke").val(canvas.getActiveObject().get("stroke") == "" ? "#ffffff" : canvas.getActiveObject().get("stroke")); $("#rect_stroke_width").val(canvas.getActiveObject().get("strokeWidth")); $("#rect_radius").val(canvas.getActiveObject().get("rx")); if (typeof rect_op !== 'undefined' && rect_op.setValue) { rect_op.setValue(100 - (canvas.getActiveObject().get("opacity") * 100)); } else { $("#rect_op").next().children(".layui-slider-bar").css("width", (1 - canvas.getActiveObject().get("opacity")) * 100 + "%"); $("#rect_op").next().children(".layui-slider-handle").css("left", (1 - canvas.getActiveObject().get("opacity")) * 100 + "%"); } $("#pic_control").hide(); $("#set_bg").hide(); $("#out_pic_control").hide(); $("#text_control").hide(); $("#circle_text_control").hide(); $("#out_text_control").hide(); $("#circle_control").hide(); $("#line_control").hide(); $("#counter_control").hide(); $("#qrcode_Controll").hide(); $("#barcode_controll").hide(); } function selectCircle(target, index) { $("#circle_control").show(); $("#circle_color").val(canvas.getActiveObject().get("fill") == "" ? "#ffffff" : canvas.getActiveObject().get("fill")); $("#circle_stroke").val(canvas.getActiveObject().get("stroke") == "" ? "#ffffff" : canvas.getActiveObject().get("stroke")); $("#circle_stroke_width").val(canvas.getActiveObject().get("strokeWidth")); if (typeof circle_op !== 'undefined' && circle_op.setValue) { circle_op.setValue(100 - (canvas.getActiveObject().get("opacity") * 100)); } else { $("#circle_op").next().children(".layui-slider-bar").css("width", (1 - canvas.getActiveObject().get("opacity")) * 100 + "%"); $("#circle_op").next().children(".layui-slider-handle").css("left", (1 - canvas.getActiveObject().get("opacity")) * 100 + "%"); } $("#pic_control").hide(); $("#set_bg").hide(); $("#out_pic_control").hide(); $("#text_control").hide(); $("#circle_text_control").hide(); $("#out_text_control").hide(); $("#rect_control").hide(); $("#line_control").hide(); $("#counter_control").hide(); $("#qrcode_Controll").hide(); $("#barcode_controll").hide(); } function selectLine(target, index) { $("#line_control").show(); let idx = getIndex(canvas.getActiveObject(), canvas); $("#line_color").val(canvas.getActiveObject().get("stroke")); $("#line_dist").val(canvas.getActiveObject().get("strokeDashArray")[0]); $("#text_h").val(canvas.getActiveObject().get("strokeWidth")) $("#pic_control").hide(); $("#set_bg").hide(); $("#out_pic_control").hide(); $("#text_control").hide(); $("#circle_text_control").hide(); $("#out_text_control").hide(); $("#rect_control").hide(); $("#circle_control").hide(); $("#counter_control").hide(); $("#qrcode_Controll").hide(); $("#barcode_controll").hide(); } function selectQrCode(target, index) { $("#qrcode_Controll").show(); let idx = getIndex(canvas.getActiveObject(), canvas); $("#qrcode_field_name").val(objs[idx - 1].name); $("#qrcode_color").val(objs[idx - 1].color); $("#qrcode_val").val(objs[idx - 1].val); $("#line_control").hide(); $("#pic_control").hide(); $("#set_bg").hide(); $("#out_pic_control").hide(); $("#text_control").hide(); $("#circle_text_control").hide(); $("#out_text_control").hide(); $("#rect_control").hide(); $("#circle_control").hide(); $("#counter_control").hide(); $("#barcode_controll").hide(); } function selectBarCode(target, index) { $("#barcode_controll").show(); let idx = getIndex(canvas.getActiveObject(), canvas); $("#barcode_field_name").val(objs[idx - 1].name); $("#barcode_font_family").val(objs[idx - 1].font); let val = parseFloat(objs[idx - 1].fontSize) * 72 / dpi if (val % 0.5 !== 0) val = Math.round(val * 2) / 2 $("#barcode_size").val(val); $("#barcode_val").val(objs[idx - 1].val); $("#barcode_show").prop("checked", objs[idx - 1].show ? 'checked' : ''); $("#line_control").hide(); $("#pic_control").hide(); $("#set_bg").hide(); $("#out_pic_control").hide(); $("#text_control").hide(); $("#circle_text_control").hide(); $("#out_text_control").hide(); $("#rect_control").hide(); $("#circle_control").hide(); $("#counter_control").hide(); $("#qrcode_Controll").hide(); } function selectCounter(target, index) { $("#counter_control").show(); let idx = getIndex(canvas.getActiveObject(), canvas); $("#counter_start").val(objs[idx - 1].start); $("#counter_add").val(objs[idx - 1].add); $("#counter_color").val(canvas.getActiveObject().get("fill")); $("#count_font_family").val(canvas.getActiveObject().get("fontFamily")); let val = parseFloat(canvas.getActiveObject().get("fontSize")) * 72 / dpi if (val % 0.5 !== 0) val = Math.round(val * 2) / 2 $("#count_size").val(val); $("#line_control").hide(); $("#pic_control").hide(); $("#set_bg").hide(); $("#out_pic_control").hide(); $("#text_control").hide(); $("#circle_text_control").hide(); $("#out_text_control").hide(); $("#rect_control").hide(); $("#circle_control").hide(); $("#qrcode_Controll").hide(); $("#barcode_controll").hide(); } function handleObjectSelected(e) { // 确定当前活动的 canvas(从事件参数或全局变量) let currentCanvas = (e && e.target && (e.target.canvas === canvas1 || e.target.canvas === canvas2)) ? (e.target.canvas === canvas1 ? canvas1 : canvas2) : canvas; // 如果事件来自 canvas,更新全局 canvas 变量 if (currentCanvas === canvas1 || currentCanvas === canvas2) { canvas = currentCanvas; objs = (currentCanvas === canvas1) ? objs1 : objs2; window.canvas = canvas; window.objs = objs; } let select_obj = canvas.getActiveObject(); // 1. 未选中任何对象 if (!select_obj) { $("#base_control, #line_control, #pic_control, #out_pic_control, #text_control, #circle_text_control, #out_text_control, #rect_control, #circle_control, #counter_control, #qrcode_Controll, #barcode_controll").hide(); $("#component_type").text(language_str("bg")); selectObj(-1, true); // 传 true,只更新UI return; } // 2. 如果是辅助线,直接返回,不处理(在获取索引之前检查) if (select_obj.isGuideLine) { return; } // 3. 解析 objs 下标(用户设为背景时在 canvas[0],不能仅用 getIndex) let metaIdx = typeof soonDesign2ResolveObjMetaIndex === 'function' ? soonDesign2ResolveObjMetaIndex(select_obj, canvas) : (getIndex(select_obj, canvas) > 0 ? getIndex(select_obj, canvas) - 1 : -1); // 如果不是通过对象列表选中的,且对象被锁定,则取消选中 if (!window._selectingFromList && isObjectLocked(select_obj)) { canvas.discardActiveObject(); canvas.renderAll(); $("#base_control, #line_control, #pic_control, #out_pic_control, #text_control, #circle_text_control, #out_text_control, #rect_control, #circle_control, #counter_control, #qrcode_Controll, #barcode_controll").hide(); $("#component_type").text(language_str("bg")); selectObj(-1, true); return; } if (metaIdx >= 0) { if (objs[metaIdx] && typeof objs[metaIdx].type !== 'undefined') { switch (objs[metaIdx].type) { case 1: // 图片 $("#component_type").text(language_str("img")); selectObject(select_obj, metaIdx); selectPic(select_obj, metaIdx); break; case 2: // 合成图片 $("#component_type").text(language_str("simg")); selectObject(select_obj, metaIdx); selectOutPic(select_obj, metaIdx); break; case 3: // 文本 $("#component_type").text(language_str("text")); selectObject(select_obj, metaIdx); selectText(select_obj, metaIdx); break; case 4: // 合成文本 $("#component_type").text(language_str("stext")); selectObject(select_obj, metaIdx); selectOutText(select_obj, metaIdx); break; case 5: // 矩形 $("#component_type").text(language_str("rect")); selectObject(select_obj, metaIdx); selectRect(select_obj, metaIdx); break; case 6: // 圆形 $("#component_type").text(language_str("circ")); selectObject(select_obj, metaIdx); selectCircle(select_obj, metaIdx); break; case 7: // 直线 $("#component_type").text(language_str("line")); selectObject(select_obj, metaIdx); selectLine(select_obj, metaIdx); break; case 8: // 二维码 $("#component_type").text(language_str("qrc")); selectObject(select_obj, metaIdx); selectQrCode(select_obj, metaIdx); break; case 9: // 条形码 $("#component_type").text(language_str("barc")); selectObject(select_obj, metaIdx); selectBarCode(select_obj, metaIdx); break; case 10: // 计数器 $("#component_type").text(language_str("counter")); selectObject(select_obj, metaIdx); selectCounter(select_obj, metaIdx); break; case 11: // 圆形文本 $("#component_type").text(language_str("ctext")); selectObject(select_obj, metaIdx); selectCircleText(select_obj, metaIdx); break; } } selectObj(metaIdx, true); } else { // --- 模板背景(canvas[0] 且不在 objs 中)--- $("#base_control, #line_control, #pic_control, #out_pic_control, #text_control, #circle_text_control, #out_text_control, #rect_control, #circle_control, #counter_control, #qrcode_Controll, #barcode_controll").hide(); $("#component_type").text(language_str("bg")); selectObj(-1, true); } } function handleObjectMoving(e) { let obj = e.target; // 跳过辅助线 if (obj.isGuideLine) { updateControls(); return; } let idx = getIndex(obj, canvas); // 圆形文本特殊处理 // 安全检查:确保 objs[idx - 1] 存在且有 type 属性 if (idx > 0 && idx <= objs.length && objs[idx - 1] && typeof objs[idx - 1].type !== 'undefined' && objs[idx - 1].type === 11 && $("#circle_text_center").prop("checked")) { let pointer = { x: obj.left, y: obj.top }; let distance = getDisdance(pointer.x, pointer.y, canvas.width / 2, canvas.height / 2); let angleDeg = getDeg(pointer); let flipped = pointer.y > canvas.height / 2 ? true : false; if (typeof circle_text_diameter !== 'undefined' && circle_text_diameter.setValue) { circle_text_diameter.setValue(distance * 2 - 50); } obj.set("diameter", distance * 2); obj.set("angle", angleDeg); obj.set("flipped", flipped); } updateControls(); } // 辅助函数:检查对象是否被锁定 // 只有当 lockMovementX 为 true 时才认为对象被锁定 // selectable 为 false 可能是其他原因(如背景图),所以只检查 lockMovementX function isObjectLocked(obj) { if (!obj) return false; // 主要检查 lockMovementX,这是锁定的主要标志 return obj.get('lockMovementX') === true; } // 暴露到全局作用域,确保其他模块可以访问 window.isObjectLocked = isObjectLocked; // 统一的 toJSON 属性列表,不包含锁定相关属性(锁定状态不应被保存) // 包含所有需要保存的属性,特别是自定义对象的属性(如 CurvedText 的 diameter, flipped) const TO_JSON_PROPERTIES = [ "selectable", "hoverable", "hoverCursor", "text", "fontStyle", "fontWeight", "underline", "evented", "linethrough", "textBackgroundColor", "diameter", "flipped" ]; // 暴露到全局作用域 window.TO_JSON_PROPERTIES = TO_JSON_PROPERTIES; // 辅助函数:解锁所有对象(加载时使用) function unlockAllObjects(canvas) { if (!canvas) return; const objects = canvas.getObjects(); objects.forEach((obj, index) => { // 背景对象(索引0)保持锁定状态 if (index === 0) { obj.set({ lockMovementX: true, lockMovementY: true, lockRotation: true, lockScalingX: true, lockScalingY: true, lockSkewingX: true, lockSkewingY: true, selectable: false, evented: false }); } else { // 其他对象默认解锁 obj.set({ lockMovementX: false, lockMovementY: false, lockRotation: false, lockScalingX: false, lockScalingY: false, lockSkewingX: false, lockSkewingY: false, selectable: true, evented: true }); } }); } // 暴露到全局作用域 window.unlockAllObjects = unlockAllObjects; // 将 handleObjectSelected 附加到 window 对象,确保全局可访问 window.handleObjectSelected = handleObjectSelected; // 防止重复初始化事件监听器 var _canvasEventsInitialized = false; function initCanvasEvents() { // 如果已经初始化过,直接返回 if (_canvasEventsInitialized) { return; } _canvasEventsInitialized = true; [canvas1, canvas2].forEach(currentCanvas => { // 1. 鼠标移动 currentCanvas.on("mouse:move", (e) => { let offsetX = 0, offsetY = 0; if (currentCanvas === canvas2 && typeof background_image !== 'undefined') { offsetX = background_image.left; offsetY = background_image.top; } if (e.pointer) { $("#posText").text("X: " + (e.pointer.x - offsetX).toFixed(0) + " Y: " + (e.pointer.y - offsetY).toFixed(0)); } }); // 2. 鼠标移出 currentCanvas.on("mouse:out", () => { // 可以在这里隐藏标尺辅助线 }); // 3. 鼠标松开 (添加对象) currentCanvas.on("mouse:up", (e) => { if (addState === 0) return; const ptr = e.pointer; switch (addState) { case 1: addPic(ptr); break; case 2: addOutPic(ptr); break; case 3: addText(ptr); break; case 4: addOutText(ptr); break; case 5: addRect(ptr); break; case 6: addCircle(ptr); break; case 7: addLine(ptr); break; case 8: addQrCode(ptr); break; case 9: addBarCode(ptr); break; case 10: addCounter(ptr); break; case 11: addCircleText(ptr); break; } addState = 0; if (typeof background_image !== 'undefined') background_image.hoverCursor = "default"; currentCanvas.renderAll(); $("#addPic, #addOutPic, #addText, #addOutText, #addRect, #addCircle, #addLine, #addQrCode, #addBarCode, #addCounter, #addCircleText").css("background-color", ""); }); // 4. 选中处理 currentCanvas.on('mouse:down', (e) => { // 更新全局 canvas 变量 canvas = currentCanvas; objs = (currentCanvas === canvas1) ? objs1 : objs2; handleObjectSelected(e); }); // 阻止被锁定的对象被框选选中,同时彻底排除辅助线 currentCanvas.on('before:selection:created', (e) => { if (!e.targets || e.targets.length === 0) return; // 调试日志:原始选区 console.log('[Guide] before:selection:created raw targets', e.targets.map(t => ({ isGuideLine: !!t.isGuideLine, type: t.type, left: t.left, top: t.top }))); // 选区里彻底排除辅助线:不允许辅助线进入 ActiveSelection // 只保留未锁定的普通对象 e.targets = e.targets.filter(target => { if (target.isGuideLine) return false; // 完全移除辅助线 return !isObjectLocked(target); // 过滤掉被锁定的普通对象 }); // 调试日志:过滤后的选区 console.log('[Guide] before:selection:created filtered targets', e.targets.map(t => ({ isGuideLine: !!t.isGuideLine, type: t.type, left: t.left, top: t.top }))); }); // 添加 selection 事件以确保对象选中时正确显示属性面板 // 注意:真正排除辅助线的逻辑已经在 before:selection:created 里完成, // 这里不再对 ActiveSelection 做任何重建,避免 Fabric 内部递归导致栈溢出。 currentCanvas.on('selection:created', (e) => { canvas = currentCanvas; objs = (currentCanvas === canvas1) ? objs1 : objs2; handleObjectSelected(e); }); currentCanvas.on('selection:updated', (e) => { canvas = currentCanvas; objs = (currentCanvas === canvas1) ? objs1 : objs2; handleObjectSelected(e); }); currentCanvas.on('object:selected', (e) => { canvas = currentCanvas; objs = (currentCanvas === canvas1) ? objs1 : objs2; // 如果不是通过对象列表选中的,且对象被锁定,则取消选中 // 注意:只有真正被锁定的对象才会被取消选中,未锁定的对象应该正常选中 if (!window._selectingFromList && e.target && isObjectLocked(e.target)) { currentCanvas.discardActiveObject(); currentCanvas.renderAll(); return; } handleObjectSelected(e); // 更新对象列表以同步锁状态 if (typeof window.updateList === 'function') { window.updateList(); } }); // 5. 对象移动 currentCanvas.on("object:moving", (e) => { // 阻止背景图(索引0)被移动(如果已锁定) let movingObj = e.target; let idx = getIndex(movingObj, currentCanvas); if (idx === 0) { let bgObj = currentCanvas.getObjects()[0]; if (bgObj && bgObj.get('lockMovementX') === true) { e.e.preventDefault(); e.e.stopPropagation(); return false; } } handleObjectMoving(e); }); // 6. 变换事件 currentCanvas.on("object:rotating", updateControls); currentCanvas.on("object:scaling", updateControls); currentCanvas.on("object:rotated", () => { recordState(); }); currentCanvas.on('text:editing:entered', () => { _textEdit = true; }); currentCanvas.on('text:editing:exited', () => { _textEdit = false; }); currentCanvas.on("text:changed", (e) => { $("#text_text").val(e.target.get("text")); }); // 7. 对象修改后 currentCanvas.on('object:modified', function (event) { if (currentCanvas.getActiveObjects().length > 1) return; // 跳过辅助线 if (event.target.isGuideLine) { return; } let idx = getIndex(event.target, currentCanvas); if (idx <= 0) return; let objData = objs[idx - 1]; // 安全检查:确保 objData 存在且有 type 属性 if (!objData || typeof objData.type === 'undefined') { return; } let target = event.target; var modAction = event.action || (event.transform && event.transform.action); if ([3, 4, 10].includes(objData.type) && modAction === 'scale') { target.set({ fontSize: Math.round(target.fontSize * (target.scaleX || 1)), scaleX: 1, scaleY: 1, }); target._clearCache(); currentCanvas.renderAll(); } else if (objData.type == 5 && modAction === 'scale') { target.width *= target.scaleX || 1; target.height *= target.scaleY || 1; target.scaleX = 1; target.scaleY = 1; } recordState(); updateControls(); // 更新对象列表以同步锁状态 if (typeof window.updateList === 'function') { window.updateList(); } }); }); } // 将 updateList 附加到 window 对象,确保全局可访问 window.updateList = function updateList() { // 确保使用正确的 objs(优先使用 window.objs,因为它可能被撤回/重做更新) const currentObjs = window.objs || objs; const currentCanvas = window.canvas || canvas; let str = ""; let i = 0; // 定义行样式:Flex布局,两端对齐,相对定位防止溢出 const rowStyle = "display:flex; justify-content:space-between; align-items:center; height:32px; padding:0 5px; cursor:pointer; border-bottom:1px solid #444; color:#ccc; font-size:12px; position:relative;"; // 定义左侧内容样式 const leftStyle = "display:flex; align-items:center; overflow:hidden; white-space:nowrap; max-width:180px;"; // 定义序号样式:靠右,有左边框 const indexStyle = "min-width:24px; text-align:center; padding-left:5px; color:#666; border-left:1px solid #555;"; for (let item of currentObjs) { let displayIndex = i + 1; // 序号底层为1 // 图标逻辑 let iconName = "rect.svg"; switch (item.type) { case 1: iconName = "images.svg"; break; case 2: iconName = "images_add.svg"; break; case 3: iconName = "text.svg"; break; case 4: iconName = "text2.svg"; break; case 5: iconName = "rect.svg"; break; case 6: iconName = "round.svg"; break; case 7: iconName = "line.svg"; break; case 8: iconName = "qrcode.svg"; break; case 9: iconName = "barcode.svg"; break; case 10: iconName = "counter.svg"; break; case 11: iconName = "text3.svg"; break; } // 名称逻辑 let nameText = item.name ? item.name : (item.type === 3 ? "TEXT_" + displayIndex : (item.type === 1 ? "IMG_" + displayIndex : "OBJ_" + displayIndex)); // 检查锁定状态(通过lockMovementX判断,因为锁定时会设置所有lock属性) // 注意:索引0是背景图,objs数组是从下标0开始存第1个对象的,所以是 i+1 let isLocked = false; var canvasObj = typeof window.soonCanvasObjForListIndex === 'function' ? window.soonCanvasObjForListIndex(i) : currentCanvas.getObjects()[i + 1]; if (canvasObj) { isLocked = canvasObj.get('lockMovementX') === true; } // 锁定状态图标 const lockIcon = isLocked ? `
` :
`
`;
str = `