Files

285 lines
10 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 画个标 · 核心: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; }
};
})();