feat: 初始化画个标仓库 - AI驱动设计系统(BYOK模式) - 224源/371504图标+25海报源+130技能
This commit is contained in:
+280
@@ -0,0 +1,280 @@
|
||||
/**
|
||||
* 画个标 · UI:画布编辑器交互
|
||||
* 职责:拖拽移动、缩放手柄、旋转手柄、双击编辑文字、快捷键
|
||||
*/
|
||||
const HGB_Editor = (function () {
|
||||
'use strict';
|
||||
|
||||
let svg, stage;
|
||||
let drag = null; // 当前拖拽状态
|
||||
let clipboard = []; // 复制/粘贴剪贴板
|
||||
const MIN_SIZE = 16;
|
||||
|
||||
/* ---------- 初始化 ---------- */
|
||||
function init() {
|
||||
svg = HGB_Renderer.svg;
|
||||
stage = document.getElementById('canvas-stage');
|
||||
|
||||
// 画布空白处点击取消选中
|
||||
svg.addEventListener('mousedown', onCanvasMouseDown);
|
||||
window.addEventListener('mousemove', onMouseMove);
|
||||
window.addEventListener('mouseup', onMouseUp);
|
||||
|
||||
// 双击编辑文字
|
||||
svg.addEventListener('dblclick', onDblClick);
|
||||
|
||||
// 快捷键
|
||||
document.addEventListener('keydown', onKeyDown);
|
||||
|
||||
// 阻止 SVG 内元素默认拖拽(避免浏览器拖拽行为)
|
||||
svg.addEventListener('dragstart', (e) => e.preventDefault());
|
||||
}
|
||||
|
||||
/* ---------- 鼠标事件 ---------- */
|
||||
function onCanvasMouseDown(e) {
|
||||
const target = e.target;
|
||||
const t = target.tagName;
|
||||
|
||||
// 1) 手柄缩放/旋转
|
||||
const handle = target.getAttribute && target.getAttribute('data-handle');
|
||||
if (target.classList && target.classList.contains('hgb-handle')) {
|
||||
e.preventDefault();
|
||||
const el = HGB_Store.selected();
|
||||
if (!el) return;
|
||||
const start = HGB_Renderer.toSvgPoint(e.clientX, e.clientY);
|
||||
// 旋转起始角:以元素中心为原点
|
||||
const center = { x: el.x + el.w / 2, y: el.y + el.h / 2 };
|
||||
const startAngle = Math.atan2(start.y - center.y, start.x - center.x) * 180 / Math.PI;
|
||||
drag = {
|
||||
type: 'resize',
|
||||
handle,
|
||||
elId: el.id,
|
||||
startX: start.x, startY: start.y,
|
||||
startAngle: startAngle - (el.rotation || 0),
|
||||
orig: { ...el },
|
||||
moved: false
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
// 2) 元素命中
|
||||
const el = HGB_Renderer.hitTest(e.clientX, e.clientY);
|
||||
if (el) {
|
||||
e.preventDefault();
|
||||
// 已选中组内元素 → 保持当前多选状态,整体拖动;未选中 → 选中(Shift 追加)
|
||||
if (!HGB_Store.get().selectedIds.includes(el.id)) {
|
||||
HGB_Store.select(el.id, e.shiftKey);
|
||||
}
|
||||
const start = HGB_Renderer.toSvgPoint(e.clientX, e.clientY);
|
||||
drag = {
|
||||
type: 'move',
|
||||
elId: el.id,
|
||||
startX: start.x, startY: start.y,
|
||||
orig: { ...el },
|
||||
moved: false
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
// 3) 空白处:取消选中
|
||||
HGB_Store.select(null);
|
||||
}
|
||||
|
||||
function onMouseMove(e) {
|
||||
if (!drag) return;
|
||||
const p = HGB_Renderer.toSvgPoint(e.clientX, e.clientY);
|
||||
const dx = p.x - drag.startX;
|
||||
const dy = p.y - drag.startY;
|
||||
if (!drag.moved && Math.abs(dx) + Math.abs(dy) > 2) drag.moved = true;
|
||||
|
||||
const el = HGB_Store.get().elements.find(x => x.id === drag.elId);
|
||||
if (!el) return;
|
||||
|
||||
if (drag.type === 'move') {
|
||||
// 以主元素原始位置为基准计算位移,整体移动全部选中元素(moveSelection 内部 emit change → refreshAll)
|
||||
const nX = Math.round(drag.orig.x + dx);
|
||||
const nY = Math.round(drag.orig.y + dy);
|
||||
const boundX = Math.max(-drag.orig.w * 0.5, Math.min(HGB_Store.CANVAS_W - drag.orig.w * 0.5, nX));
|
||||
const boundY = Math.max(-drag.orig.h * 0.5, Math.min(HGB_Store.CANVAS_H - drag.orig.h * 0.5, nY));
|
||||
HGB_Store.moveSelection(boundX - drag.orig.x, boundY - drag.orig.y);
|
||||
} else if (drag.type === 'resize') {
|
||||
if (drag.handle === 'rotate') {
|
||||
rotateElement(el, drag, p);
|
||||
} else {
|
||||
resizeElement(el, drag, p, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function onMouseUp() {
|
||||
if (!drag) return;
|
||||
// 拖拽/缩放完成后记录历史
|
||||
if (drag.moved) {
|
||||
HGB_Store.pushHistory();
|
||||
}
|
||||
drag = null;
|
||||
HGB_Renderer.render();
|
||||
HGB_UI.refreshAll();
|
||||
}
|
||||
|
||||
/* ---------- 缩放逻辑 ---------- */
|
||||
function resizeElement(el, drag, p, ev) {
|
||||
const o = drag.orig;
|
||||
let x = o.x, y = o.y, w = o.w, h = o.h;
|
||||
|
||||
const isE = drag.handle.includes('e');
|
||||
const isW = drag.handle.includes('w');
|
||||
const isS = drag.handle.includes('s');
|
||||
const isN = drag.handle.includes('n');
|
||||
|
||||
if (isE) w = Math.max(MIN_SIZE, o.w + (p.x - (o.x + o.w)));
|
||||
if (isS) h = Math.max(MIN_SIZE, o.h + (p.y - (o.y + o.h)));
|
||||
if (isW) {
|
||||
const nw = Math.max(MIN_SIZE, o.w + (o.x - p.x));
|
||||
x = o.x + o.w - nw; w = nw;
|
||||
}
|
||||
if (isN) {
|
||||
const nh = Math.max(MIN_SIZE, o.h + (o.y - p.y));
|
||||
y = o.y + o.h - nh; h = nh;
|
||||
}
|
||||
|
||||
// 等比缩放(按住 Shift)
|
||||
if (ev && ev.shiftKey) {
|
||||
const ratio = o.w / o.h;
|
||||
if (w / h > ratio) h = w / ratio; else w = h * ratio;
|
||||
// 重新计算锚点
|
||||
if (isW) x = o.x + o.w - w;
|
||||
if (isN) y = o.y + o.h - h;
|
||||
}
|
||||
|
||||
el.x = Math.round(x); el.y = Math.round(y);
|
||||
el.w = Math.round(w); el.h = Math.round(h);
|
||||
HGB_Renderer.render();
|
||||
HGB_UI.refreshProps();
|
||||
}
|
||||
|
||||
/* ---------- 旋转逻辑 ---------- */
|
||||
function rotateElement(el, drag, p) {
|
||||
const center = { x: drag.orig.x + drag.orig.w / 2, y: drag.orig.y + drag.orig.h / 2 };
|
||||
const startAngle = drag.startAngle;
|
||||
const angle = Math.atan2(p.y - center.y, p.x - center.x) * 180 / Math.PI;
|
||||
el.rotation = Math.round((angle - startAngle) * 10) / 10;
|
||||
HGB_Renderer.render();
|
||||
HGB_UI.refreshProps();
|
||||
}
|
||||
|
||||
/* ---------- 双击编辑文字 ---------- */
|
||||
function onDblClick(e) {
|
||||
const el = HGB_Renderer.hitTest(e.clientX, e.clientY);
|
||||
if (!el || el.type !== 'text' || el.locked) return;
|
||||
HGB_Store.select(el.id);
|
||||
const val = prompt('编辑文字内容:', el.text || '');
|
||||
if (val !== null) {
|
||||
HGB_Store.updateElement(el.id, { text: val });
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------- 快捷键 ---------- */
|
||||
function onKeyDown(e) {
|
||||
const tag = (document.activeElement && document.activeElement.tagName) || '';
|
||||
if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT') {
|
||||
// 输入框内不触发全局快捷键(Ctrl+Z 撤销文字除外)
|
||||
if (e.ctrlKey || e.metaKey) {
|
||||
if (e.key === 'z' || e.key === 'Z') { e.preventDefault(); }
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const ctrl = e.ctrlKey || e.metaKey;
|
||||
if (ctrl && (e.key === 'z' || e.key === 'Z')) {
|
||||
e.preventDefault();
|
||||
e.shiftKey ? HGB_Store.redo() : HGB_Store.undo();
|
||||
HGB_UI.refreshAll();
|
||||
return;
|
||||
}
|
||||
if (ctrl && (e.key === 'y' || e.key === 'Y')) {
|
||||
e.preventDefault();
|
||||
HGB_Store.redo();
|
||||
HGB_UI.refreshAll();
|
||||
return;
|
||||
}
|
||||
if (ctrl && (e.key === 'd' || e.key === 'D')) {
|
||||
e.preventDefault();
|
||||
HGB_Store.duplicateSelection();
|
||||
HGB_UI.refreshAll();
|
||||
return;
|
||||
}
|
||||
if (ctrl && (e.key === 'c' || e.key === 'C')) {
|
||||
e.preventDefault();
|
||||
clipboard = HGB_Store.selectedElements().map(el => JSON.parse(JSON.stringify(el)));
|
||||
HGB_UI.toast(`📋 已复制 ${clipboard.length} 个元素`);
|
||||
return;
|
||||
}
|
||||
if (ctrl && (e.key === 'v' || e.key === 'V')) {
|
||||
e.preventDefault();
|
||||
if (clipboard.length) { HGB_Store.pasteElements(clipboard); HGB_UI.refreshAll(); }
|
||||
return;
|
||||
}
|
||||
if (e.key === 'Delete' || e.key === 'Backspace') {
|
||||
HGB_Store.removeSelection();
|
||||
HGB_UI.refreshAll();
|
||||
return;
|
||||
}
|
||||
if (e.key === 'Escape') {
|
||||
HGB_Store.select(null);
|
||||
HGB_UI.refreshAll();
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------- 添加到画布 ---------- */
|
||||
function addText() {
|
||||
const el = {
|
||||
id: HGB_Store.uid(),
|
||||
type: 'text',
|
||||
name: '文字',
|
||||
text: 'EDUCATION',
|
||||
x: 200, y: 260, w: 200, h: 60,
|
||||
rotation: 0,
|
||||
fontSize: 36,
|
||||
fontWeight: 700,
|
||||
fontFamily: 'Arial',
|
||||
letterSpacing: 2,
|
||||
fill: '#1B4F72',
|
||||
stroke: 'none',
|
||||
strokeWidth: 0,
|
||||
opacity: 1,
|
||||
visible: true, locked: false
|
||||
};
|
||||
HGB_Store.addElement(el);
|
||||
HGB_UI.refreshAll();
|
||||
return el;
|
||||
}
|
||||
|
||||
function addIcon(iconId, name) {
|
||||
const el = {
|
||||
id: HGB_Store.uid(),
|
||||
type: 'icon',
|
||||
name: name || iconId,
|
||||
iconId: iconId,
|
||||
svg: HGB_Store.iconSvg(iconId),
|
||||
x: 220, y: 170, w: 160, h: 160,
|
||||
rotation: 0,
|
||||
fill: 'none',
|
||||
stroke: '#1B4F72',
|
||||
strokeWidth: 2,
|
||||
opacity: 1,
|
||||
visible: true, locked: false
|
||||
};
|
||||
HGB_Store.addElement(el);
|
||||
HGB_UI.refreshAll();
|
||||
return el;
|
||||
}
|
||||
|
||||
/* ---------- 导出 ---------- */
|
||||
function exportPngFlow() {
|
||||
// 打开导出对话框
|
||||
HGB_UI.openExportModal('png');
|
||||
}
|
||||
|
||||
return { init, addText, addIcon, exportPngFlow };
|
||||
})();
|
||||
Reference in New Issue
Block a user