feat: 初始化画个标仓库 - AI驱动设计系统(BYOK模式) - 224源/371504图标+25海报源+130技能

This commit is contained in:
sgp
2026-10-03 21:44:18 +08:00
commit 9e222877ea
497 changed files with 43611 additions and 0 deletions
+284
View File
@@ -0,0 +1,284 @@
/**
* 画个标 · 核心: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; }
};
})();