/** * 画个标 · 核心:SVG 渲染引擎 * 职责:将 store 中的元素渲染到画布 SVG,绘制选中框/手柄,支持缩放显示 */ const HGB_Renderer = (function () { 'use strict'; let svg, defs, contentLayer, overlayLayer; let zoom = 1; // 当前缩放 const SVG_NS = 'http://www.w3.org/2000/svg'; const MIN_ZOOM = 0.2, MAX_ZOOM = 4; /* ---------- 初始化 ---------- */ function init() { svg = document.getElementById('logo-canvas'); // 清空并重建分层 svg.innerHTML = ''; defs = svgCreate('defs'); svg.appendChild(defs); contentLayer = svgCreate('g'); contentLayer.setAttribute('class', 'hgb-content'); svg.appendChild(contentLayer); overlayLayer = svgCreate('g'); overlayLayer.setAttribute('class', 'hgb-overlay'); overlayLayer.setAttribute('pointer-events', 'none'); svg.appendChild(overlayLayer); // 背景 const bgRect = svgCreate('rect'); bgRect.setAttribute('x', 0); bgRect.setAttribute('y', 0); bgRect.setAttribute('width', HGB_Store.CANVAS_W); bgRect.setAttribute('height', HGB_Store.CANVAS_H); bgRect.setAttribute('fill', HGB_Store.get().bg); bgRect.setAttribute('data-hgb-bg', '1'); contentLayer.appendChild(bgRect); } function svgCreate(tag) { return document.createElementNS(SVG_NS, tag); } /** * 生成圆弧路径 d(环形文字用) * arc = { cx, cy, r, start, sweep }:圆心 / 半径 / 起始角度(度,0=右) / 扫过角度(度,顺时针) */ function arcPathD(arc) { const a1 = arc.start * Math.PI / 180; const a2 = (arc.start + arc.sweep) * Math.PI / 180; const x1 = arc.cx + arc.r * Math.cos(a1); const y1 = arc.cy + arc.r * Math.sin(a1); const x2 = arc.cx + arc.r * Math.cos(a2); const y2 = arc.cy + arc.r * Math.sin(a2); const large = Math.abs(arc.sweep) > 180 ? 1 : 0; const sweepFlag = arc.sweep < 0 ? 0 : 1; // sweep-flag=0 逆时针(y向下坐标系中负角走下半弧) return 'M ' + x1.toFixed(2) + ' ' + y1.toFixed(2) + ' A ' + arc.r + ' ' + arc.r + ' 0 ' + large + ' ' + sweepFlag + ' ' + x2.toFixed(2) + ' ' + y2.toFixed(2); } /* ---------- 主渲染 ---------- */ function render() { const s = HGB_Store.get(); init(); // 简单起见全量重绘 // 背景色同步到 input const bgInput = document.getElementById('canvas-bg'); if (bgInput && bgInput.value !== s.bg) bgInput.value = s.bg; // 绘制元素(按数组顺序,后画的在上层) for (const el of s.elements) { if (!el.visible) continue; contentLayer.appendChild(renderElement(el)); } // 选中框与手柄(overlay 层) const selIds = s.selectedIds || (s.selectedId ? [s.selectedId] : []); for (const sid of selIds) { const el = s.elements.find(e => e.id === sid); if (!el || !el.visible) continue; drawSelection(el, sid === s.selectedId); } } /** 渲染单个元素为 SVG 节点 */ function renderElement(el) { let node; if (el.type === 'icon') { node = svgCreate('g'); node.innerHTML = el.svg; // 应用图标颜色 node.querySelectorAll('path,circle,rect,polygon,polyline,line,ellipse').forEach(p => { if (el.fill && el.fill !== 'none') p.setAttribute('fill', el.fill); else p.setAttribute('fill', 'none'); if (el.stroke && el.stroke !== 'none') { p.setAttribute('stroke', el.stroke); p.setAttribute('stroke-width', el.strokeWidth); p.setAttribute('stroke-linecap', 'round'); p.setAttribute('stroke-linejoin', 'round'); } else { p.setAttribute('stroke', 'none'); } }); } else if (el.type === 'text') { node = svgCreate('text'); node.setAttribute('text-anchor', 'middle'); node.setAttribute('dominant-baseline', 'central'); node.setAttribute('font-family', el.fontFamily || 'sans-serif'); node.setAttribute('font-size', el.fontSize || 40); node.setAttribute('font-weight', el.fontWeight || 400); if (el.letterSpacing) node.setAttribute('letter-spacing', el.letterSpacing); node.setAttribute('fill', el.fill || '#000'); if (el.stroke && el.stroke !== 'none') { node.setAttribute('stroke', el.stroke); node.setAttribute('stroke-width', el.strokeWidth); } else { node.setAttribute('stroke', 'none'); } if (el.arc) { // 环形文字:沿圆弧路径排列(品牌名环绕 mark 的排版能力) const pathId = 'arc_' + el.id; const p = svgCreate('path'); p.setAttribute('id', pathId); p.setAttribute('d', arcPathD(el.arc)); p.setAttribute('fill', 'none'); p.setAttribute('stroke', 'none'); defs.appendChild(p); const tp = svgCreate('textPath'); tp.setAttribute('href', '#' + pathId); // 居中锚定:text-anchor=middle 配合 startOffset=50%,文字中心落在弧线中点 tp.setAttribute('startOffset', '50%'); tp.textContent = el.text || ''; node.appendChild(tp); node.removeAttribute('x'); node.removeAttribute('y'); } else { node.setAttribute('x', el.x + el.w / 2); node.setAttribute('y', el.y + el.h / 2); node.textContent = el.text || ''; } } else { // 预留:其他类型(图片等) node = svgCreate('g'); } // 通用变换:位置 + 旋转 const cx = el.x + el.w / 2; const cy = el.y + el.h / 2; if (el.rotation) { node.setAttribute('transform', `translate(${cx} ${cy}) rotate(${el.rotation}) translate(${-cx} ${-cy})`); } node.setAttribute('opacity', el.opacity != null ? el.opacity : 1); // 元素定位 node.setAttribute('data-id', el.id); node.setAttribute('data-type', el.type); if (el.type === 'icon') { node.setAttribute('data-icon', el.iconId || ''); } node.classList.add('hgb-elem'); const s = HGB_Store.get(); const selIds = s.selectedIds || (s.selectedId ? [s.selectedId] : []); if (selIds.includes(el.id)) node.classList.add('selected'); if (el.locked) node.classList.add('locked'); // 命中测试基于数据层(store),无需 DOM 命中框 node._el = el; return node; } /* ---------- 选中框与手柄 ---------- */ function drawSelection(el, withHandles) { const box = svgCreate('rect'); box.setAttribute('class', 'hgb-selection' + (withHandles ? '' : ' multi')); box.setAttribute('x', el.x); box.setAttribute('y', el.y); box.setAttribute('width', el.w); box.setAttribute('height', el.h); overlayLayer.appendChild(box); if (!withHandles) return; // 多选元素仅显示虚线框 // 8 个缩放手柄 + 1 个旋转手柄 const handles = [ { cx: el.x, cy: el.y, cursor: 'nwse-resize', action: 'nw' }, { cx: el.x + el.w / 2, cy: el.y, cursor: 'ns-resize', action: 'n' }, { cx: el.x + el.w, cy: el.y, cursor: 'nesw-resize', action: 'ne' }, { cx: el.x + el.w, cy: el.y + el.h / 2, cursor: 'ew-resize', action: 'e' }, { cx: el.x + el.w, cy: el.y + el.h, cursor: 'nwse-resize', action: 'se' }, { cx: el.x + el.w / 2, cy: el.y + el.h, cursor: 'ns-resize', action: 's' }, { cx: el.x, cy: el.y + el.h, cursor: 'nesw-resize', action: 'sw' }, { cx: el.x, cy: el.y + el.h / 2, cursor: 'ew-resize', action: 'w' } ]; for (const h of handles) { const node = svgCreate('rect'); node.setAttribute('class', 'hgb-handle'); node.setAttribute('x', h.cx - 5); node.setAttribute('y', h.cy - 5); node.setAttribute('width', 10); node.setAttribute('height', 10); node.setAttribute('rx', 2); node.setAttribute('style', 'cursor:' + h.cursor); node.setAttribute('data-handle', h.action); node.setAttribute('pointer-events', 'all'); overlayLayer.appendChild(node); } // 旋转手柄 const rx = el.x + el.w / 2; const ry = el.y - 28; const line = svgCreate('line'); line.setAttribute('class', 'hgb-rotate-line'); line.setAttribute('x1', rx); line.setAttribute('y1', el.y); line.setAttribute('x2', rx); line.setAttribute('y2', ry); overlayLayer.appendChild(line); const rot = svgCreate('circle'); rot.setAttribute('class', 'hgb-handle rotate'); rot.setAttribute('cx', rx); rot.setAttribute('cy', ry); rot.setAttribute('r', 7); rot.setAttribute('data-handle', 'rotate'); rot.setAttribute('pointer-events', 'all'); overlayLayer.appendChild(rot); } /* ---------- 缩放与显示 ---------- */ function setZoom(z) { zoom = Math.min(MAX_ZOOM, Math.max(MIN_ZOOM, z)); applyZoom(); return zoom; } function getZoom() { return zoom; } function zoomIn() { return setZoom(zoom * 1.2); } function zoomOut() { return setZoom(zoom / 1.2); } function fitToStage() { const stage = document.getElementById('canvas-stage'); const size = Math.min(stage.clientWidth - 48, stage.clientHeight - 48); zoom = Math.min(MAX_ZOOM, Math.max(MIN_ZOOM, size / HGB_Store.CANVAS_W)); applyZoom(); return zoom; } function applyZoom() { svg.style.width = (HGB_Store.CANVAS_W * zoom) + 'px'; svg.style.height = (HGB_Store.CANVAS_H * zoom) + 'px'; const lv = document.getElementById('zoom-level'); if (lv) lv.textContent = Math.round(zoom * 100) + '%'; } /* ---------- 坐标转换(client -> svg user units) ---------- */ function toSvgPoint(clientX, clientY) { const pt = svg.createSVGPoint(); pt.x = clientX; pt.y = clientY; return pt.matrixTransform(svg.getScreenCTM().inverse()); } /** 命中测试:返回最上层可交互元素 */ function hitTest(clientX, clientY) { const s = HGB_Store.get(); const p = toSvgPoint(clientX, clientY); // 从顶层往下找 for (let i = s.elements.length - 1; i >= 0; i--) { const el = s.elements[i]; if (!el.visible || el.locked) continue; if (p.x >= el.x && p.x <= el.x + el.w && p.y >= el.y && p.y <= el.y + el.h) { return el; } } return null; } /* ---------- 画布状态 ---------- */ function setCanvasBg(color) { const bgRect = svg.querySelector('[data-hgb-bg]'); if (bgRect) bgRect.setAttribute('fill', color); } return { init, render, renderElement, setZoom, getZoom, zoomIn, zoomOut, fitToStage, toSvgPoint, hitTest, setCanvasBg, svgCreate, get svg() { return svg; } }; })();