UNPKG

@meta2d/core

Version:

@meta2d/core: Powerful, Beautiful, Simple, Open - Web-Based 2D At Its Best .

1,307 lines 340 kB
import { KeydownType } from '../options'; import { addLineAnchor, calcIconRect, calcTextRect, calcWorldAnchors, calcWorldRects, LockState, nearestAnchor, PenType, pushPenAnchor, removePenAnchor, renderPen, scalePen, translateLine, deleteTempAnchor, connectLine, disconnectLine, getAnchor, calcAnchorDock, calcMoveDock, calcTextLines, setNodeAnimate, setLineAnimate, calcPenRect, setChildrenActive, getParent, setHover, randomId, getPensLock, getToAnchor, getFromAnchor, calcPadding, getPensDisableRotate, getPensDisableResize, needCalcTextRectProps, calcResizeDock, needPatchFlagsPenRectProps, needCalcIconRectProps, isDomShapes, renderPenRaw, needSetPenProps, getAllChildren, calcInView, isShowChild, getTextColor, clearLifeCycle, rotatePen, calcTextAutoWidth, getGradientAnimatePath, CanvasLayer, ctxFlip, ctxRotate, setGlobalAlpha, drawImage, setElemPosition, getAllFollowers, calcChildrenInitRect, needImgCanvasPatchFlagsProps, } from '../pen'; import { calcRotate, distance, getDistance, hitPoint, PointType, PrevNextType, rotatePoint, samePoint, scalePoint, translatePoint, TwoWay, } from '../point'; import { calcCenter, calcRightBottom, calcRelativePoint, getRect, getRectOfPoints, pointInRect, pointInSimpleRect, rectInRect, rectToPoints, resizeRect, translateRect, pointInPolygon } from '../rect'; import { EditType, globalStore, } from '../store'; import { deepClone, fileToBase64, uploadFile, formatPadding, rgba, s8, toNumber, } from '../utils'; import { inheritanceProps, defaultCursors, defaultDrawLineFns, HotkeyType, HoverType, MouseRight, rotatedCursors, } from '../data'; import { createOffscreen } from './offscreen'; import { curve, mind, getLineLength, getLineRect, pointInLine, simplify, smoothLine, lineSegment, getLineR, lineInRect, } from '../diagrams'; import { polyline, translatePolylineAnchor } from '../diagrams/line/polyline'; import { Tooltip } from '../tooltip'; import { Scroll } from '../scroll'; import { CanvasImage } from './canvasImage'; import { MagnifierCanvas } from './magnifierCanvas'; import { lockedError } from '../utils/error'; import { Dialog } from '../dialog'; import { setter } from '../utils/object'; import { isNumber } from '../utils/tool'; import { Title } from '../title'; import { CanvasTemplate } from './canvasTemplate'; import { getLinePoints } from '../diagrams/line'; import { Popconfirm } from '../popconfirm'; import { le5leTheme, themeKeys } from '../theme'; export const movingSuffix = '-moving'; export class Canvas { parent; parentElement; store; canvas = document.createElement('canvas'); offscreen = createOffscreen(); width; height; externalElements = document.createElement('div'); clientRect; canvasRect; activeRect; initActiveRect; dragRect; lastRotate = 0; sizeCPs; activeInitPos; hoverType = HoverType.None; resizeIndex = 0; mouseDown; hotkeyType; mouseRight; addCaches; touchCenter; initTouchDis; initScale; touchScaling; touchMoving; startTouches; lastTouchY; startDistance; startCenter; currentCenter; lastOffsetX = 0; lastOffsetY = 0; drawingLineName; drawLineFns = [...defaultDrawLineFns]; drawingLine; pencil; pencilLine; movingPens; patchFlagsLines = new Set(); dock; prevAnchor; nextAnchor; lastMouseTime = 0; hoverTimer = 0; fitTimer = 0; // 即将取消活动状态的画笔,用于Ctrl选中/取消选中画笔 willInactivePen; patchFlags = false; lastRender = 0; touchStart = 0; touchStartTimer; timer; lastTapTime = 0; lastAnimateRender = 0; animateRendering = false; renderTimer; initPens; pointSize = 8; pasteOffset = true; opening = false; maxZindex = 5; canMoveLine = false; //moveConnectedLine=false randomIdObj; //记录拖拽前后id变化 keyOptions; isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent); /** * @deprecated 改用 beforeAddPens */ beforeAddPen; beforeAddPens; beforeAddAnchor; beforeRemovePens; beforeRemoveAnchor; customResizeDock; customMoveDock; inputParent = document.createElement('div'); // input = document.createElement('textarea'); inputDiv = document.createElement('div'); // inputRight = document.createElement('div'); dropdown = document.createElement('ul'); tooltip; popconfirm; title; mousePos = { x: 0, y: 0 }; scroll; movingAnchor; // 正在移动中的瞄点 canvasTemplate; canvasImage; canvasImageBottom; magnifierCanvas; dialog; autoPolylineFlag = false; //标记open不自动计算 stopPropagation = (e) => { e.stopPropagation(); }; constructor(parent, parentElement, store) { this.parent = parent; this.parentElement = parentElement; this.store = store; this.canvasTemplate = new CanvasTemplate(parentElement, store); this.canvasTemplate.canvas.style.zIndex = '1'; this.canvasImageBottom = new CanvasImage(parentElement, store, true); this.canvasImageBottom.canvas.style.zIndex = '2'; parentElement.appendChild(this.canvas); this.canvas.style.position = 'absolute'; this.canvas.style.backgroundRepeat = 'no-repeat'; this.canvas.style.backgroundSize = '100% 100%'; this.canvas.style.zIndex = '3'; this.canvasImage = new CanvasImage(parentElement, store); this.canvasImage.canvas.style.zIndex = '4'; this.magnifierCanvas = new MagnifierCanvas(this, parentElement, store); this.magnifierCanvas.canvas.style.zIndex = '5'; this.externalElements.style.position = 'absolute'; this.externalElements.style.left = '0'; this.externalElements.style.top = '0'; this.externalElements.style.outline = 'none'; this.externalElements.style.background = 'transparent'; this.externalElements.style.zIndex = '5'; parentElement.style.position = 'relative'; parentElement.style['-webkit-tap-highlight-color'] = 'transparent'; parentElement.appendChild(this.externalElements); this.createInput(); this.tooltip = new Tooltip(parentElement, store); this.tooltip.box.onmouseleave = (e) => { this.patchFlags = true; this.store.lastHover && (this.store.lastHover.calculative.hover = false); let hover = this.store.data.pens.find((item) => item.calculative.hover === true); setHover(hover, false); }; this.popconfirm = new Popconfirm(parentElement, store); this.dialog = new Dialog(parentElement, store); this.title = new Title(parentElement); if (this.store.options.scroll) { this.scroll = new Scroll(this); } this.store.dpiRatio = globalThis.devicePixelRatio || 1; if (this.store.dpiRatio < 1) { this.store.dpiRatio = 1; } else if (this.store.dpiRatio > 1 && this.store.dpiRatio < 1.5) { this.store.dpiRatio = 1.5; } this.clientRect = this.externalElements.getBoundingClientRect(); this.listen(); window?.addEventListener('resize', this.onResize); window?.addEventListener('scroll', this.onScroll); window?.addEventListener('message', this.onMessage); } curve = curve; polyline = polyline; mind = mind; line = lineSegment; listen() { // ios this.externalElements.addEventListener('gesturestart', this.onGesturestart); this.externalElements.ondragover = (e) => e.preventDefault(); this.externalElements.ondrop = this.ondrop; this.externalElements.oncontextmenu = (e) => e.preventDefault(); this.store.options.interval = 50; if (this.store.options.parentTouch) { this.parentElement.ontouchstart = this.ontouchstart; this.parentElement.ontouchmove = this.ontouchmove; this.parentElement.ontouchend = this.ontouchend; } else { this.externalElements.ontouchstart = this.ontouchstart; this.externalElements.ontouchmove = this.ontouchmove; this.externalElements.ontouchend = this.ontouchend; } this.externalElements.onmousedown = (e) => { if (this.isMobile) { return; } this.onMouseDown({ x: e.offsetX, y: e.offsetY, clientX: e.clientX, clientY: e.clientY, pageX: e.pageX, pageY: e.pageY, ctrlKey: e.ctrlKey || e.metaKey, shiftKey: e.shiftKey, altKey: e.altKey, buttons: e.buttons, }); }; this.externalElements.onmousemove = (e) => { if (this.isMobile) { return; } if (e.target !== this.externalElements) { return; } this.onMouseMove({ x: e.offsetX, y: e.offsetY, clientX: e.clientX, clientY: e.clientY, pageX: e.pageX, pageY: e.pageY, ctrlKey: e.ctrlKey || e.metaKey, shiftKey: e.shiftKey, altKey: e.altKey, buttons: e.buttons, }); }; this.externalElements.onmouseup = (e) => { if (this.isMobile) { return; } this.onMouseUp({ x: e.offsetX, y: e.offsetY, clientX: e.clientX, clientY: e.clientY, pageX: e.pageX, pageY: e.pageY, ctrlKey: e.ctrlKey || e.metaKey, shiftKey: e.shiftKey, altKey: e.altKey, buttons: e.buttons, button: e.button, }); }; this.externalElements.onmouseleave = (e) => { //离开画布取消所有选中 this.store.data.pens.forEach((pen) => { if (pen.calculative.hover) { pen.calculative.hover = false; } }); if (this.store.hover) { this.store.hover.calculative.hover = false; this.store.hover = undefined; } this.render(); if (e.toElement !== this.tooltip.box && e.toElement !== this.tooltip.arrowUp && e.toElement !== this.tooltip.arrowDown) { this.tooltip.hide(); this.store.lastHover = undefined; } }; this.externalElements.ondblclick = (e) => { if (this.isMobile) { return; } this.ondblclick(e); }; this.externalElements.tabIndex = 0; this.externalElements.onblur = () => { this.mouseDown = undefined; }; this.externalElements.onwheel = this.onwheel; document.addEventListener('copy', this.onCopy); document.addEventListener('cut', this.onCut); document.addEventListener('paste', this.onPaste); switch (this.store.options.keydown) { case KeydownType.Document: document.addEventListener('keydown', this.onkeydown); document.addEventListener('keyup', this.onkeyup); break; case KeydownType.Canvas: this.externalElements.addEventListener('keydown', this.onkeydown); this.externalElements.addEventListener('keyup', this.onkeyup); break; } } onCopy = (event) => { if (this.store.options.disableClipboard) { return; } if (event.target !== this.externalElements && event.target !== document.body && event.target.offsetParent !== this.externalElements) { return; } this.copy(); }; onCut = (event) => { if (this.store.options.disableClipboard) { return; } if (event.target !== this.externalElements && event.target !== document.body && event.target.offsetParent !== this.externalElements) { return; } this.cut(); }; onPaste = async (event) => { if (this.store.data.locked || this.store.options.disableClipboard) { return; } if (event.target !== this.externalElements && event.target !== document.body && event.target.offsetParent !== this.externalElements) { return; } // 是否粘贴图片 let hasImages; if (navigator.clipboard && event.clipboardData) { const items = event.clipboardData.items; if (items) { for (let i = 0; i < items.length; i++) { if (items[i].type.indexOf('image') !== -1 && items[i].getAsFile()) { hasImages = true; break; } } } } if (hasImages) { const items = event.clipboardData.items; if (items) { for (let i = 0; i < items.length; i++) { if (items[i].type.indexOf('image') !== -1 && items[i].getAsFile()) { const { x, y } = this.mousePos; const blob = items[i].getAsFile(); let name = items[i].type.slice(6) === 'gif' ? 'gif' : 'image'; if (blob !== null) { const isGif = name === 'gif'; const pen = await this.fileToPen(blob, isGif); pen.height = (pen.height / pen.width) * 100, pen.width = 100; pen.x = x - 50 / 2, pen.y = y - (pen.height / pen.width) * 50, pen.externElement = isGif, this.addPens([pen]); this.active([pen]); this.copy([pen]); // let base64_str: any; // const reader = new FileReader(); // reader.onload = (e) => { // base64_str = e.target.result; // const image = new Image(); // image.src = base64_str; // image.onload = () => { // const { width, height } = image; // const pen = { // name, // x: x - 50 / 2, // y: y - (height / width) * 50, // externElement: name === 'gif', // width: 100, // height: (height / width) * 100, // image: base64_str as any, // }; // this.addPens([pen]); // this.active([pen]); // this.copy([pen]); // }; // }; // reader.readAsDataURL(blob); } } } } } else { this.paste(); } }; onMessage = (e) => { if (typeof e.data !== 'string' || !e.data || e.data.startsWith('setImmediate') || e.data.startsWith('webpackHotUpdate') // 处理vue2 webpack4 热更新消息冲突问题 ) { return; } let data = JSON.parse(e.data); if (typeof data === 'object') { if (data.name === 'onload') { this.dialog.iframe.contentWindow.postMessage(JSON.stringify({ name: 'dialog', data: this.dialog.data }), '*'); } if (data.name === 'closeDialog') { this.dialog.hide(); } this.parent.doMessageEvent(data.name, JSON.stringify(data.data)); } else { this.parent.doMessageEvent(data); } }; onwheel = (e) => { //输入模式不允许滚动 if (this.inputDiv.contentEditable === 'true') { return; } //画线过程中不允许缩放 if (this.drawingLine) { return; } if (this.pencil) { return; } if (this.store.hover) { if (this.store.hover.onWheel) { this.store.hover.onWheel(this.store.hover, e); return; } } if (this.store.data.disableScale || this.store.options.disableScale) { return; } e.preventDefault(); e.stopPropagation(); //移动画笔过程中不允许缩放 if (this.mouseDown && (this.hoverType === HoverType.Node || this.hoverType === HoverType.Line)) return; if (this.store.data.locked === LockState.Disable) return; if (this.store.data.locked === LockState.DisableScale) return; if (this.store.data.locked === LockState.DisableMoveScale) return; // e.ctrlKey: false - 平移; true - 缩放。老windows触摸板不支持 if (!e.ctrlKey && Math.abs(e.wheelDelta) < 100 && e.deltaY.toString().indexOf('.') === -1) { if (this.store.options.scroll && !e.metaKey && this.scroll) { this.scroll.wheel(e.deltaY < 0); return; } const scale = this.store.data.scale || 1; this.translate(-e.deltaX / scale, -e.deltaY / scale); return; } if (Math.abs(e.wheelDelta) > 100) { //鼠标滚轮滚动 scroll模式下是上下滚动而不是缩放 ctrl可以控制缩放 if (this.store.options.scroll && this.scroll && !this.store.options.scrollButScale && !(e.ctrlKey || e.metaKey)) { this.scroll.wheel(e.deltaY < 0); return; } } //禁止触摸屏双指缩放操作 if (this.store.options.disableTouchPadScale) { return; } let scaleOff = 0.015; if (this.store.options.scaleOff) { scaleOff = this.store.options.scaleOff; if (e.deltaY > 0) { scaleOff = -this.store.options.scaleOff; } } else { let isMac = /mac os /i.test(navigator.userAgent); if (isMac) { if (!e.ctrlKey) { scaleOff *= e.wheelDeltaY / 240; } else if (e.deltaY > 0) { scaleOff *= -1; } } else { let offset = 0.2; if (e.deltaY.toString().indexOf('.') !== -1) { offset = 0.01; } if (e.deltaY > 0) { scaleOff = -offset; } else { scaleOff = offset; } } } let { offsetX: x, offsetY: y } = e; // if (this.parent.map && e.target === this.parent.map?.box) { // //放大镜缩放 // const width = this.store.data.width || this.store.options.width; // const height = this.store.data.height || this.store.options.height; // if (width && height) { // //大屏 // x = // (x / this.parent.map.boxWidth) * width * this.store.data.scale + // this.store.data.origin.x; // y = // (y / this.parent.map.boxHeight) * height * this.store.data.scale + // this.store.data.origin.y; // const rect = this.canvas.getBoundingClientRect(); // x = x + rect.left; // y = y + rect.top; // } else { // const rect = this.parent.getRect(); // x = // (x / this.parent.map.boxWidth) * rect.width + // rect.x + // this.store.data.x; // y = // (y / this.parent.map.boxHeight) * rect.height + // rect.y + // this.store.data.y; // } // } this.scale(this.store.data.scale + scaleOff, { x, y }); this.externalElements.focus(); // 聚焦 }; onkeydown = (e) => { if (this.store.data.locked >= LockState.DisableEdit && e.target.tagName !== 'INPUT' && e.target.tagName !== 'TEXTAREA' && !e.target.dataset.meta2dIgnore) { this.store.active.forEach((pen) => { pen.onKeyDown?.(pen, e.key); }); } if (this.store.data.locked >= LockState.DisableEdit || e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA' || e.target.dataset.meta2dIgnore) { return; } if (this.store.options.unavailableKeys.includes(e.key)) { return; } if (!this.keyOptions) { this.keyOptions = {}; } this.keyOptions.altKey = e.altKey; this.keyOptions.shiftKey = e.shiftKey; this.keyOptions.ctrlKey = e.ctrlKey; this.keyOptions.metaKey = e.metaKey; this.keyOptions.F = false; if (e.key === 'F' || e.key === 'f') { this.keyOptions.F = true; } let x = 10; let y = 10; let vRect = null; if (this.store.options.strictScope) { const width = this.store.data.width || this.store.options.width; const height = this.store.data.height || this.store.options.height; if (width && height) { vRect = { x: this.store.data.origin.x, y: this.store.data.origin.y, width: width * this.store.data.scale, height: height * this.store.data.scale, }; } } switch (e.key) { case ' ': this.hotkeyType = HotkeyType.Translate; break; case 'Control': if (this.drawingLine) { this.drawingLine.calculative.drawlineH = !this.drawingLine.calculative.drawlineH; } else if (!this.hotkeyType) { this.patchFlags = true; this.hotkeyType = HotkeyType.Select; } break; case 'Meta': break; case 'Shift': if (this.store.active.length === 1 && this.store.active[0].type && this.store.activeAnchor) { this.toggleAnchorHand(); } else if (!this.hotkeyType) { this.patchFlags = true; if (!this.store.options.resizeMode) { this.hotkeyType = HotkeyType.Resize; } } break; case 'Alt': if (!e.ctrlKey && !e.shiftKey && this.drawingLine) { const to = getToAnchor(this.drawingLine); if (to !== this.drawingLine.calculative.activeAnchor) { deleteTempAnchor(this.drawingLine); this.drawingLine.calculative.worldAnchors.push(to); } else { this.drawingLine.calculative.worldAnchors.push({ x: to.x, y: to.y, }); } const index = this.drawLineFns.indexOf(this.drawingLineName); this.drawingLineName = this.drawLineFns[(index + 1) % this.drawLineFns.length]; this.drawingLine.lineName = this.drawingLineName; this.drawline(); this.patchFlags = true; } e.preventDefault(); break; case 'a': case 'A': if (e.ctrlKey || e.metaKey) { // TODO: ctrl + A 会选中 visible == false 的元素 this.active(this.store.data.pens.filter((pen) => !pen.parentId && pen.locked !== LockState.Disable)); e.preventDefault(); } else { this.toggleAnchorMode(); } break; case 'Delete': case 'Backspace': if (this.canvasImage.fitFlag && this.canvasImage.activeFit) { this.deleteFit(); break; } !this.store.data.locked && this.delete(); break; case 'ArrowLeft': if (this.movingAnchor) { this.translateAnchor(-1, 0); break; } x = -1; if (e.shiftKey) { x = -5; } if (e.ctrlKey || e.metaKey) { x = -10; } x = x * this.store.data.scale; if (this.store.activeAnchor && this.store.active && this.store.active.length === 1 && this.store.active[0].type) { this.moveLineAnchor({ x: this.store.activeAnchor.x + x, y: this.store.activeAnchor.y }, {}); break; } if (vRect && this.activeRect.x + x < vRect.x) { x = vRect.x - this.activeRect.x; } this.translatePens(this.store.active, x, 0); break; case 'ArrowUp': if (this.movingAnchor) { this.translateAnchor(0, -1); break; } y = -1; if (e.shiftKey) { y = -5; } if (e.ctrlKey || e.metaKey) { y = -10; } y = y * this.store.data.scale; if (vRect && this.activeRect.y + y < vRect.y) { y = vRect.y - this.activeRect.y; } if (this.store.activeAnchor && this.store.active && this.store.active.length === 1 && this.store.active[0].type) { this.moveLineAnchor({ x: this.store.activeAnchor.x, y: this.store.activeAnchor.y + y }, {}); break; } this.translatePens(this.store.active, 0, y); break; case 'ArrowRight': if (this.movingAnchor) { this.translateAnchor(1, 0); break; } x = 1; if (e.shiftKey) { x = 5; } if (e.ctrlKey || e.metaKey) { x = 10; } x = x * this.store.data.scale; if (this.store.activeAnchor && this.store.active && this.store.active.length === 1 && this.store.active[0].type) { this.moveLineAnchor({ x: this.store.activeAnchor.x + x, y: this.store.activeAnchor.y }, {}); break; } if (vRect && this.activeRect.x + this.activeRect.width + x > vRect.x + vRect.width) { x = vRect.x + vRect.width - (this.activeRect.x + this.activeRect.width); } this.translatePens(this.store.active, x, 0); break; case 'ArrowDown': if (this.movingAnchor) { this.translateAnchor(0, 1); break; } y = 1; if (e.shiftKey) { y = 5; } if (e.ctrlKey || e.metaKey) { y = 10; } y = y * this.store.data.scale; if (vRect && this.activeRect.y + this.activeRect.height + y > vRect.y + vRect.height) { y = vRect.y + vRect.height - (this.activeRect.y + this.activeRect.height); } if (this.store.activeAnchor && this.store.active && this.store.active.length === 1 && this.store.active[0].type) { this.moveLineAnchor({ x: this.store.activeAnchor.x, y: this.store.activeAnchor.y + y }, {}); break; } this.translatePens(this.store.active, 0, y); break; case 'd': case 'D': if (!this.store.active[0]?.locked) { this.removeAnchorHand(); } break; case 'h': case 'H': if (!this.store.active[0]?.locked) { this.addAnchorHand(); } break; case 'm': case 'M': this.toggleMagnifier(); break; case 'g': case 'G': //组合/解组 if (e.ctrlKey || e.metaKey) { if (e.shiftKey) { this.parent.uncombine(); } else { if (this.store.active.length > 1) { this.parent.combine(this.store.active); } } e.preventDefault(); break; } // 进入移动瞄点状态 if (this.hoverType === HoverType.NodeAnchor) { this.movingAnchor = this.store.hoverAnchor; this.externalElements.style.cursor = 'move'; } break; case 's': case 'S': // 分割线 if (!this.store.data.locked && this.hoverType === HoverType.LineAnchor && this.store.hover === this.store.active[0]) { this.splitLine(this.store.active[0], this.store.hoverAnchor); } // 保存 (e.ctrlKey || e.metaKey) && this.store.emitter.emit('save', { event: e }); break; case 'c': case 'C': if ((e.ctrlKey || e.metaKey) && this.store.options.disableClipboard) { this.copy(); } break; case 'x': case 'X': if ((e.ctrlKey || e.metaKey) && this.store.options.disableClipboard) { this.cut(); } break; case '√': //MAC OPTION + V case 'v': case 'V': if (!e.ctrlKey && !e.metaKey) { if (this.pencil) { this.stopPencil(); } if (this.drawingLineName) { this.finishDrawline(); this.drawingLineName = ''; } else { this.drawingLineName = this.store.options.drawingLineName; } } if (!this.store.data.locked && (e.ctrlKey || e.metaKey) && (this.store.options.disableClipboard || (!this.store.options.disableClipboard && e.altKey)) //alt按下,paste事件无效 ) { this.paste(); } break; case 'b': case 'B': if (this.drawingLineName) { this.finishDrawline(); this.drawingLineName = ''; } if (this.pencil) { this.stopPencil(); } else { this.drawingPencil(); } break; case 'y': case 'Y': if (e.ctrlKey || e.metaKey) { this.redo(); } break; case 'z': case 'Z': if (e.ctrlKey || e.metaKey) { if (e.shiftKey) { this.redo(); } else { this.undo(); } } else if (e.shiftKey) { this.redo(); } break; case 'Enter': if (this.drawingLineName) { this.finishDrawline(true); if (this.store.active[0].anchors[0].connectTo) { this.drawingLineName = ''; } else { this.drawingLineName = this.store.options.drawingLineName; } } if (this.store.active) { this.store.active.forEach((pen) => { if (pen.type) { pen.close = !pen.close; if (pen.close) { getLinePoints(pen); } this.store.path2dMap.set(pen, globalStore.path2dDraws.line(pen)); getLineLength(pen); } else { //图元进入编辑模式 pen.calculative.focus = true; } }); this.render(); } break; case 'Escape': if (this.drawingLineName) { this.finishDrawline(); } this.drawingLineName = undefined; this.stopPencil(); if (this.store.active) { this.store.active.forEach((pen) => { if (pen.type) { } else { //图元退出编辑模式 pen.calculative.focus = false; } }); } if (this.movingPens) { this.getAllByPens(this.movingPens).forEach((pen) => { this.store.pens[pen.id] = undefined; }); this.movingPens = undefined; this.mouseDown = undefined; this.clearDock(); this.store.active?.forEach((pen) => { this.updateLines(pen); }); this.calcActiveRect(); this.patchFlags = true; } this.hotkeyType = HotkeyType.None; this.movingAnchor = undefined; if (this.magnifierCanvas.magnifier) { this.magnifierCanvas.magnifier = false; this.patchFlags = true; } break; case 'E': case 'e': this.store.options.disableAnchor = !this.store.options.disableAnchor; this.store.emitter.emit('disableAnchor', this.store.options.disableAnchor); break; case '=': if (e.ctrlKey || e.metaKey) { this.scale(this.store.data.scale + 0.1); e.preventDefault(); e.stopPropagation(); } break; case '-': if (e.ctrlKey || e.metaKey) { this.scale(this.store.data.scale - 0.1); e.preventDefault(); e.stopPropagation(); } break; case 'l': case 'L': this.canMoveLine = true; break; case '[': //下一层 this.parent.down(); break; case ']': //上一层 this.parent.up(); break; case '{': // 置底 this.parent.bottom(); break; case '}': //置顶 this.parent.top(); break; case 'F': case 'f': if (!this.store.data.locked && (e.ctrlKey || e.metaKey) && !this.store.options.disableClipboard) { //粘贴到被复制图元上一层 this.paste(); } this.setFollowers(); break; } this.render(false); }; /** * 分割连线的锚点,变成两条线 * @param line 连线 * @param anchor 锚点,连线的某个锚点,引用相同 */ splitLine(line, anchor) { const worldAnchors = line.calculative.worldAnchors; const index = worldAnchors.findIndex((a) => a === anchor); if ([-1, 0, worldAnchors.length - 1].includes(index)) { // 没找到,起终点不处理 return; } const initLine = deepClone(line, true); const newLine = deepClone(line, true); const id = s8(); newLine.id = id; newLine.calculative.canvas = this; newLine.calculative.active = false; newLine.calculative.hover = false; // index 作为公共点 const preAnchors = deepClone(worldAnchors.slice(0, index + 1)); const laterAnchors = deepClone(worldAnchors.slice(index)).map((a) => { a.penId = id; return a; }); line.calculative.worldAnchors = preAnchors; newLine.calculative.worldAnchors = laterAnchors; this.initLineRect(line); this.initLineRect(newLine); this.store.data.pens.push(newLine); this.store.pens[id] = newLine; this.pushHistory({ type: EditType.Add, pens: [deepClone(newLine, true)], step: 2, }); this.pushHistory({ type: EditType.Update, initPens: [initLine], pens: [deepClone(line, true)], step: 2, }); } translateAnchor(x, y) { this.movingAnchor.x += x; this.movingAnchor.y += y; // 点不在范围内,移动到范围内 const penId = this.movingAnchor.penId; if (penId) { const pen = this.store.pens[penId]; const rect = pen.calculative.worldRect; if (this.movingAnchor.x < rect.x) { this.movingAnchor.x = rect.x; } else if (this.movingAnchor.x > rect.ex) { this.movingAnchor.x = rect.ex; } if (this.movingAnchor.y < rect.y) { this.movingAnchor.y = rect.y; } else if (this.movingAnchor.y > rect.ey) { this.movingAnchor.y = rect.ey; } const anchor = calcRelativePoint(this.movingAnchor, rect); // 更改 pen 的 anchors 属性 const index = pen.anchors.findIndex((anchor) => anchor.id === this.movingAnchor.id); pen.anchors[index] = anchor; this.patchFlags = true; } } onkeyup = (e) => { switch (e.key) { case 'l': case 'L': this.canMoveLine = false; break; // case 'Alt': // if (this.drawingLine) { // this.store.options.autoAnchor = !this.store.options.autoAnchor; // } // break; } if (this.hotkeyType) { this.render(); } if (this.hotkeyType < HotkeyType.AddAnchor) { this.hotkeyType = HotkeyType.None; } }; async fileToPen(file, isGif) { let url = ''; if (this.store.options.uploadFn) { url = await this.store.options.uploadFn(file); } else if (this.store.options.uploadUrl) { url = await uploadFile(file, this.store.options.uploadUrl, this.store.options.uploadParams, this.store.options.uploadHeaders); } else { url = await fileToBase64(file); } return new Promise((resolve, reject) => { const img = new Image(); img.onload = () => { globalStore.htmlElements[url] = img; resolve({ width: img.width, height: img.height, name: isGif ? 'gif' : 'image', image: url, }); }; img.onerror = (e) => { reject(e); }; img.crossOrigin = this.store.options.crossOrigin || 'anonymous'; img.src = url; }); } ondrop = async (event) => { if (this.store.data.locked) { console.warn('canvas is locked, can not drop'); return; } // Fix bug: 在 firefox 上拖拽图片会打开新页 event.preventDefault(); event.stopPropagation(); const json = event.dataTransfer.getData('Meta2d') || event.dataTransfer.getData('Text'); let obj = null; try { if (json) { obj = JSON.parse(json); } } catch (e) { } if (!obj) { const { files } = event.dataTransfer; if (files.length && files[0].type.match('image.*') && !(this.addCaches && this.addCaches.length)) { // 必须是图片类型 const isGif = files[0].type === 'image/gif'; obj = await this.fileToPen(files[0], isGif); } else if (this.addCaches && this.addCaches.length) { obj = this.addCaches; this.addCaches = []; } else { this.store.emitter.emit('drop', undefined); return; } } obj = Array.isArray(obj) ? obj : [obj]; if (obj[0] && obj[0].draggable !== false) { const pt = { x: event.offsetX, y: event.offsetY }; this.calibrateMouse(pt); await this.dropPens(obj, pt); this.addCaches = []; // 拖拽新增图元判断是否是在容器上 this.getContainerHover(pt); this.mousePos.x = pt.x; this.mousePos.y = pt.y; this.store.emitter.emit('mouseup', { x: pt.x, y: pt.y, pen: this.store.hoverContainer, }); } this.store.emitter.emit('drop', obj || json); }; async dropPens(pens, e) { this.randomIdObj = {}; if (this.parent.store.options.textPresetStyle) { if (pens.length === 1 && !pens[0].temPreset && pens[0].name === 'text') { Object.assign(pens[0], this.parent.store.options.textPresetStyle); } } for (const pen of pens) { // 只修改 树根处的 祖先节点, randomCombineId 会递归更改子节点 !pen.parentId && this.randomCombineId(pen, pens); } if (Object.keys(this.randomIdObj).length !== 0) { const keys = Object.keys(this.randomIdObj).join('|'); const regex = new RegExp(`(${keys})`, "g"); for (const pen of pens) { if (pen.type) { pen.anchors[0].connectTo = this.randomIdObj[pen.anchors[0].connectTo]; pen.anchors[pen.anchors.length - 1].connectTo = this.randomIdObj[pen.anchors[pen.anchors.length - 1].connectTo]; } else { pen.connectedLines?.forEach((item) => { item.lineAnchor = this.randomIdObj[item.lineAnchor]; item.lineId = this.randomIdObj[item.lineId]; }); } //动画、事件和状态 if (pen.animations?.length) { const str = JSON.stringify(pen.animations).replace(regex, match => this.randomIdObj[match]); pen.animations = JSON.parse(str); } if (pen.triggers?.length) { const str = JSON.stringify(pen.triggers).replace(regex, match => this.randomIdObj[match]); pen.triggers = JSON.parse(str); } if (pen.events?.length) { const str = JSON.stringify(pen.events).replace(regex, match => this.randomIdObj[match]); pen.events = JSON.parse(str); } } } for (const pen of pens) { // TODO: randomCombineId 会更改 id, 此处应该不存在空 id if (!pen.id) { pen.id = s8(); } !pen.calculative && (pen.calculative = { canvas: this }); this.store.pens[pen.id] = pen; } // // 计算区域 // for (const pen of pens) { // // 组合节点才需要提前计算 // Array.isArray(pen.children) && pen.children.length > 0 && this.updatePenRect(pen); // } let num = 0; let lastH = 0; let lastW = 0; for (const pen of pens) { if (!pen.parentId) { pen.width *= this.store.data.scale; pen.height *= this.store.data.scale; pen.x = e.x - pen.width / 2 + lastW; pen.y = e.y - pen.height / 2 + lastH; if (pen.tags && pen.tags.includes('meta3d')) { pen.x = this.store.data.origin.x; pen.y = this.store.data.origin.y; } if (pen.dataset) { if (num % 2 === 0) { lastW = pen.width - 40 * this.store.data.scale; } else { lastW = 0; } num++; if (num % 2 === 0) { lastH += pen.height + 10 * this.store.data.scale; } delete pen.dataset; } if (pen.temOffsetX) { pen.x += pen.temOffsetX * this.store.data.scale; delete pen.temOffsetX; } if (pen.temOffsetY) { pen.y += pen.temOffsetY * this.store.data.scale; delete pen.temOffsetY; } } } //大屏区域 const width = this.store.data.width || this.store.options.width; const height = this.store.data.height || this.store.options.height; if (width && height) { let rect = { x: this.store.data.origin.x, y: this.store.data.origin.y, width: width * this.store.data.scale, height: height * this.store.data.scale, }; let flag = true; for (const pen of pens) { if (!pen.parentId) { let points = [ { x: pen.x, y: pen.y }, { x: pen.x + pen.width, y: pen.y }, { x: pen.x, y: pen.y + pen.height }, { x: pen.x + pen.width, y: pen.y + pen.height }, { x: pen.x + pen.width / 2, y: pen.y + pen.height / 2 }, ]; if ((pen.x === rect.x && pen.y === rect.y && pen.width === rect.width && pen.height === rect.height) || points.some((point) => pointInRect(point, rect))) { flag = false; //严格范围模式下对齐大屏边界 if (this.store.options.strictScope) { if (pen.x < rect.x) { pen.x = rect.x; } if (pen.y < rect.y) { pen.y = rect.y; } if (pen.x + pen.width > rect.x + rect.width) { pen.x = rect.x + rect.width - pen.width; } if (pen.y + pen.height > rect.y + rect.height) { pen.y = rect.y + rect.height - pen.height; } } break; } } } if (flag) { console.info('画笔在大屏范围外'); return; } } await this.addPens(pens, true); this.active(pens.filter((pen) => !pen.parentId)); this.render(); this.externalElements.focus(); // 聚焦 } randomCombineId(pen, pens, parentId) { let beforeIds = null; if (pen.type) { if (pen.anchors[0].connectTo || pen.anchors[pen.anchors.length - 1].connectTo) { beforeIds = [ pen.id, pen.anchors[0].id, pen.anchors[pen.a