mirror of
https://github.com/idrawjs/idraw
synced 2026-05-24 01:58:27 +00:00
feat: optimize drawing element and resizing in-group
This commit is contained in:
parent
d6f859f864
commit
09a9c8e7de
27 changed files with 314 additions and 121 deletions
|
|
@ -92,6 +92,7 @@ export class BoardWatcher extends EventEmitter<BoardWatcherEventMap> {
|
|||
return;
|
||||
}
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
const deltaX = e.deltaX > 0 || e.deltaX < 0 ? e.deltaX : 0;
|
||||
const deltaY = e.deltaY > 0 || e.deltaY < 0 ? e.deltaY : 0;
|
||||
|
||||
|
|
|
|||
|
|
@ -220,7 +220,6 @@ function drawScrollerInfo(helperContext: ViewContext2D, opts: { viewScaleInfo: V
|
|||
ctx.fillRect(0, height - wrapper.lineSize, width, wrapper.lineSize);
|
||||
}
|
||||
|
||||
// ctx.globalAlpha = 1;
|
||||
// x-thumb
|
||||
drawScrollerThumb(ctx, {
|
||||
axis: 'X',
|
||||
|
|
@ -236,7 +235,6 @@ function drawScrollerInfo(helperContext: ViewContext2D, opts: { viewScaleInfo: V
|
|||
ctx.fillRect(width - wrapper.lineSize, 0, wrapper.lineSize, height);
|
||||
}
|
||||
|
||||
// ctx.globalAlpha = 1;
|
||||
// y-thumb
|
||||
drawScrollerThumb(ctx, {
|
||||
axis: 'Y',
|
||||
|
|
|
|||
|
|
@ -6,7 +6,8 @@ import {
|
|||
rotatePointInGroup,
|
||||
getGroupQueueFromList,
|
||||
findElementsFromList,
|
||||
findElementsFromListByPositions
|
||||
findElementsFromListByPositions,
|
||||
deepResizeGroupElement
|
||||
} from '@idraw/util';
|
||||
import type { ViewRectVertexes, CoreEvent, ElementPosition } from '@idraw/types';
|
||||
import type {
|
||||
|
|
@ -393,8 +394,19 @@ export const MiddlewareSelector: BoardMiddleware<DeepSelectorSharedStorage, Core
|
|||
const resizedElemSize = resizeElement(elems[0], { scale, start: resizeStart, end: resizeEnd, resizeType, sharer });
|
||||
elems[0].x = resizedElemSize.x;
|
||||
elems[0].y = resizedElemSize.y;
|
||||
elems[0].w = resizedElemSize.w;
|
||||
elems[0].h = resizedElemSize.h;
|
||||
if (elems[0].type === 'group' && elems[0].operations?.deepResize === true) {
|
||||
// TODO
|
||||
// elems[0].w = resizedElemSize.w;
|
||||
// elems[0].h = resizedElemSize.h;
|
||||
deepResizeGroupElement(elems[0] as Element<'group'>, {
|
||||
w: resizedElemSize.w,
|
||||
h: resizedElemSize.h
|
||||
});
|
||||
} else {
|
||||
elems[0].w = resizedElemSize.w;
|
||||
elems[0].h = resizedElemSize.h;
|
||||
}
|
||||
|
||||
updateSelectedElementList([elems[0]]);
|
||||
viewer.drawFrame();
|
||||
}
|
||||
|
|
@ -476,7 +488,11 @@ export const MiddlewareSelector: BoardMiddleware<DeepSelectorSharedStorage, Core
|
|||
}
|
||||
|
||||
if (data && (['drag', 'drag-list', 'drag-list-end', 'resize'] as ActionType[]).includes(actionType)) {
|
||||
eventHub.trigger('change', { data });
|
||||
let type = 'drag-element';
|
||||
if (type === 'resize') {
|
||||
type = 'resize-element';
|
||||
}
|
||||
eventHub.trigger('change', { data, type });
|
||||
}
|
||||
viewer.drawFrame();
|
||||
};
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ export interface IDrawEventKeys {
|
|||
export type IDrawEvent = CoreEvent & {
|
||||
change: {
|
||||
data: Data;
|
||||
type: 'update-element' | 'delete-element' | 'move-element' | 'add-element' | 'set-data' | 'other';
|
||||
type: 'update-element' | 'delete-element' | 'move-element' | 'add-element' | 'drag-element' | 'resize-element' | 'set-data' | 'other';
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -248,7 +248,10 @@ export class iDraw {
|
|||
devicePixelRatio,
|
||||
data,
|
||||
viewScaleInfo: { scale: 1, offsetLeft: -outputSize.x, offsetTop: -outputSize.y, offsetBottom: 0, offsetRight: 0 },
|
||||
viewSizeInfo,
|
||||
viewSizeInfo: {
|
||||
...viewSizeInfo,
|
||||
...{ devicePixelRatio }
|
||||
},
|
||||
loadItemMap: this.#core.getLoadItemMap()
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -111,7 +111,9 @@ export {
|
|||
moveElementPosition,
|
||||
insertElementToListByPosition,
|
||||
deleteElementInListByPosition,
|
||||
deleteElementInList
|
||||
deleteElementInList,
|
||||
deepResizeGroupElement,
|
||||
deepCloneElement
|
||||
} from '@idraw/util';
|
||||
export { iDraw } from './idraw';
|
||||
export type { IDrawEvent, IDrawEventKeys } from './event';
|
||||
|
|
|
|||
|
|
@ -4,6 +4,14 @@ import { createColorStyle } from './color';
|
|||
|
||||
const defaultElemConfig = getDefaultElementDetailConfig();
|
||||
|
||||
export function getOpacity(elem: Element): number {
|
||||
let opacity = 1;
|
||||
if (elem?.detail?.opacity !== undefined && elem?.detail?.opacity >= 0 && elem?.detail?.opacity <= 1) {
|
||||
opacity = elem?.detail?.opacity;
|
||||
}
|
||||
return opacity;
|
||||
}
|
||||
|
||||
export function drawBox(
|
||||
ctx: ViewContext2D,
|
||||
viewElem: Element<ElementType>,
|
||||
|
|
@ -14,9 +22,12 @@ export function drawBox(
|
|||
renderContent: () => void;
|
||||
viewScaleInfo: ViewScaleInfo;
|
||||
viewSizeInfo: ViewSizeInfo;
|
||||
parentOpacity: number;
|
||||
}
|
||||
): void {
|
||||
const { pattern, renderContent, originElem, calcElemSize, viewScaleInfo, viewSizeInfo } = opts || {};
|
||||
const { parentOpacity } = opts;
|
||||
const opacity = getOpacity(originElem) * parentOpacity;
|
||||
|
||||
drawClipPath(ctx, viewElem, {
|
||||
originElem,
|
||||
|
|
@ -24,17 +35,11 @@ export function drawBox(
|
|||
viewScaleInfo,
|
||||
viewSizeInfo,
|
||||
renderContent: () => {
|
||||
if (viewElem?.detail?.opacity !== undefined && viewElem?.detail?.opacity >= 0) {
|
||||
ctx.globalAlpha = viewElem.detail.opacity;
|
||||
} else {
|
||||
ctx.globalAlpha = 1;
|
||||
}
|
||||
ctx.globalAlpha = opacity;
|
||||
drawBoxBackground(ctx, viewElem, { pattern, viewScaleInfo, viewSizeInfo });
|
||||
renderContent?.();
|
||||
drawBoxBorder(ctx, viewElem, { viewScaleInfo, viewSizeInfo });
|
||||
// TODO
|
||||
// drawBoxBackground(ctx, viewElem, { pattern, viewScaleInfo, viewSizeInfo });
|
||||
ctx.globalAlpha = 1;
|
||||
ctx.globalAlpha = parentOpacity;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -89,23 +94,12 @@ function drawBoxBackground(
|
|||
): void {
|
||||
const { pattern, viewScaleInfo, viewSizeInfo } = opts;
|
||||
const transform: TransformAction[] = [];
|
||||
let { borderRadius } = viewElem.detail;
|
||||
const { borderWidth } = viewElem.detail;
|
||||
if (typeof borderWidth !== 'number') {
|
||||
// TODO: If borderWidth is an array, borderRadius will not take effect and will become 0.
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
borderRadius = 0;
|
||||
}
|
||||
if (viewElem.detail.background || pattern) {
|
||||
const { x, y, w, h, radiusList } = calcViewBoxSize(viewElem, {
|
||||
viewScaleInfo,
|
||||
viewSizeInfo
|
||||
});
|
||||
|
||||
// r = Math.min(r, w / 2, h / 2);
|
||||
// if (w < r * 2 || h < r * 2) {
|
||||
// r = 0;
|
||||
// }
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x + radiusList[0], y);
|
||||
ctx.arcTo(x + w, y, x + w, y + h, radiusList[1]);
|
||||
|
|
@ -150,18 +144,6 @@ function drawBoxBackground(
|
|||
|
||||
if (transform && transform.length > 0) {
|
||||
ctx.setTransform(1, 0, 0, 1, 0, 0);
|
||||
// for (let i = transform?.length - 1; i > 0; i--) {
|
||||
// const action = transform[i];
|
||||
// if (action.method === 'translate') {
|
||||
// const args = action.args.map((num) => -num);
|
||||
// ctx.translate(...(args as [number, number]));
|
||||
// } else if (action.method === 'rotate') {
|
||||
// const args = action.args.map((num) => -num);
|
||||
// // ctx.rotate(...(args as [number]));
|
||||
// } else if (action.method === 'scale') {
|
||||
// ctx.setTransform(1, 0, 0, 1, 0, 0);
|
||||
// }
|
||||
// }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -173,11 +155,6 @@ function drawBoxBorder(ctx: ViewContext2D, viewElem: Element<ElementType>, opts:
|
|||
if (!isColorStr(viewElem.detail.borderColor)) {
|
||||
return;
|
||||
}
|
||||
if (viewElem?.detail?.opacity !== undefined && viewElem?.detail?.opacity >= 0) {
|
||||
ctx.globalAlpha = viewElem.detail.opacity;
|
||||
} else {
|
||||
ctx.globalAlpha = 1;
|
||||
}
|
||||
const { viewScaleInfo } = opts;
|
||||
const { scale } = viewScaleInfo;
|
||||
let borderColor = defaultElemConfig.borderColor;
|
||||
|
|
@ -315,7 +292,6 @@ function drawBoxBorder(ctx: ViewContext2D, viewElem: Element<ElementType>, opts:
|
|||
ctx.arcTo(x, y, x + w, y, radiusList[0]);
|
||||
ctx.closePath();
|
||||
ctx.stroke();
|
||||
ctx.globalAlpha = 1;
|
||||
}
|
||||
ctx.setLineDash([]);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,18 @@
|
|||
import type { Element, RendererDrawElementOptions, ViewContext2D } from '@idraw/types';
|
||||
import { rotateElement } from '@idraw/util';
|
||||
import { createColorStyle } from './color';
|
||||
import { drawBoxShadow } from './box';
|
||||
import { drawBoxShadow, getOpacity } from './box';
|
||||
|
||||
export function drawCircle(ctx: ViewContext2D, elem: Element<'circle'>, opts: RendererDrawElementOptions) {
|
||||
const { detail, angle } = elem;
|
||||
const { background = '#000000', borderColor = '#000000', borderWidth = 0 } = detail;
|
||||
const { calculator, viewScaleInfo, viewSizeInfo } = opts;
|
||||
const { background = '#000000', borderColor = '#000000', boxSizing, borderWidth = 0 } = detail;
|
||||
let bw: number = 0;
|
||||
if (typeof borderWidth === 'number' && borderWidth > 0) {
|
||||
bw = borderWidth as number;
|
||||
} else if (Array.isArray(borderWidth) && typeof borderWidth[0] === 'number' && borderWidth[0] > 0) {
|
||||
bw = borderWidth[0] as number;
|
||||
}
|
||||
const { calculator, viewScaleInfo, viewSizeInfo, parentOpacity } = opts;
|
||||
// const { scale, offsetTop, offsetBottom, offsetLeft, offsetRight } = viewScaleInfo;
|
||||
const { x, y, w, h } = calculator?.elementSize({ x: elem.x, y: elem.y, w: elem.w, h: elem.h }, viewScaleInfo, viewSizeInfo) || elem;
|
||||
const viewElem = { ...elem, ...{ x, y, w, h, angle } };
|
||||
|
|
@ -16,17 +22,29 @@ export function drawCircle(ctx: ViewContext2D, elem: Element<'circle'>, opts: Re
|
|||
viewScaleInfo,
|
||||
viewSizeInfo,
|
||||
renderContent: () => {
|
||||
const a = w / 2;
|
||||
const b = h / 2;
|
||||
let a = w / 2;
|
||||
let b = h / 2;
|
||||
// 'content-box'
|
||||
const centerX = x + a;
|
||||
const centerY = y + b;
|
||||
|
||||
if (elem?.detail?.opacity !== undefined && elem?.detail?.opacity >= 0) {
|
||||
ctx.globalAlpha = elem.detail.opacity;
|
||||
} else {
|
||||
ctx.globalAlpha = 1;
|
||||
if (bw > 0) {
|
||||
if (boxSizing === 'border-box') {
|
||||
a = a - bw;
|
||||
b = b - bw;
|
||||
} else if (boxSizing === 'center-line') {
|
||||
a = a - bw / 2;
|
||||
b = b - bw / 2;
|
||||
} else {
|
||||
// 'border-box'
|
||||
a = a - bw;
|
||||
b = b - bw;
|
||||
}
|
||||
}
|
||||
|
||||
const opacity = getOpacity(viewElem) * parentOpacity;
|
||||
|
||||
ctx.globalAlpha = opacity;
|
||||
|
||||
// draw border
|
||||
if (typeof borderWidth === 'number' && borderWidth > 0) {
|
||||
const ba = borderWidth / 2 + a;
|
||||
|
|
@ -50,7 +68,7 @@ export function drawCircle(ctx: ViewContext2D, elem: Element<'circle'>, opts: Re
|
|||
ctx.circle(centerX, centerY, a, b, 0, 0, 2 * Math.PI);
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
ctx.globalAlpha = 1;
|
||||
ctx.globalAlpha = parentOpacity;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ const defaultDetail = getDefaultElementDetailConfig();
|
|||
|
||||
export function drawElementList(ctx: ViewContext2D, data: Data, opts: RendererDrawElementOptions) {
|
||||
const { elements = [] } = data;
|
||||
const { parentOpacity } = opts;
|
||||
for (let i = 0; i < elements.length; i++) {
|
||||
const element = elements[i];
|
||||
const elem = {
|
||||
|
|
@ -24,7 +25,12 @@ export function drawElementList(ctx: ViewContext2D, data: Data, opts: RendererDr
|
|||
}
|
||||
|
||||
try {
|
||||
drawElement(ctx, elem, opts);
|
||||
drawElement(ctx, elem, {
|
||||
...opts,
|
||||
...{
|
||||
parentOpacity
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import { drawImage } from './image';
|
|||
import { drawText } from './text';
|
||||
import { drawSVG } from './svg';
|
||||
import { drawHTML } from './html';
|
||||
import { drawBox, drawBoxShadow } from './box';
|
||||
import { drawBox, drawBoxShadow, getOpacity } from './box';
|
||||
import { drawPath } from './path';
|
||||
|
||||
export function drawElement(ctx: ViewContext2D, elem: Element<ElementType>, opts: RendererDrawElementOptions) {
|
||||
|
|
@ -64,10 +64,11 @@ export function drawElement(ctx: ViewContext2D, elem: Element<ElementType>, opts
|
|||
}
|
||||
|
||||
export function drawGroup(ctx: ViewContext2D, elem: Element<'group'>, opts: RendererDrawElementOptions) {
|
||||
const { calculator, viewScaleInfo, viewSizeInfo } = opts;
|
||||
const { calculator, viewScaleInfo, viewSizeInfo, parentOpacity } = opts;
|
||||
const { x, y, w, h, angle } = calculator?.elementSize({ x: elem.x, y: elem.y, w: elem.w, h: elem.h, angle: elem.angle }, viewScaleInfo, viewSizeInfo) || elem;
|
||||
const viewElem = { ...elem, ...{ x, y, w, h, angle } };
|
||||
rotateElement(ctx, { x, y, w, h, angle }, () => {
|
||||
ctx.globalAlpha = getOpacity(elem) * parentOpacity;
|
||||
drawBoxShadow(ctx, viewElem, {
|
||||
viewScaleInfo,
|
||||
viewSizeInfo,
|
||||
|
|
@ -77,6 +78,7 @@ export function drawGroup(ctx: ViewContext2D, elem: Element<'group'>, opts: Rend
|
|||
calcElemSize: { x, y, w, h, angle },
|
||||
viewScaleInfo,
|
||||
viewSizeInfo,
|
||||
parentOpacity,
|
||||
renderContent: () => {
|
||||
const { x, y, w, h, radiusList } = calcViewBoxSize(viewElem, {
|
||||
viewScaleInfo,
|
||||
|
|
@ -84,6 +86,7 @@ export function drawGroup(ctx: ViewContext2D, elem: Element<'group'>, opts: Rend
|
|||
});
|
||||
if (elem.detail.overflow === 'hidden') {
|
||||
ctx.save();
|
||||
|
||||
ctx.fillStyle = 'transparent';
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x + radiusList[0], y);
|
||||
|
|
@ -123,7 +126,7 @@ export function drawGroup(ctx: ViewContext2D, elem: Element<'group'>, opts: Rend
|
|||
}
|
||||
|
||||
try {
|
||||
drawElement(ctx, child, { ...opts });
|
||||
drawElement(ctx, child, { ...opts, ...{ parentOpacity: parentOpacity * getOpacity(elem) } });
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
|
|
@ -131,12 +134,12 @@ export function drawGroup(ctx: ViewContext2D, elem: Element<'group'>, opts: Rend
|
|||
}
|
||||
|
||||
if (elem.detail.overflow === 'hidden') {
|
||||
ctx.globalAlpha = 1;
|
||||
ctx.restore();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
ctx.globalAlpha = parentOpacity;
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,19 +1,19 @@
|
|||
import type { Element, RendererDrawElementOptions, ViewContext2D } from '@idraw/types';
|
||||
import { rotateElement } from '@idraw/util';
|
||||
import { getOpacity } from './box';
|
||||
|
||||
export function drawHTML(ctx: ViewContext2D, elem: Element<'html'>, opts: RendererDrawElementOptions) {
|
||||
const content = opts.loader.getContent(elem);
|
||||
const { calculator, viewScaleInfo, viewSizeInfo } = opts;
|
||||
const { calculator, viewScaleInfo, viewSizeInfo, parentOpacity } = opts;
|
||||
const { x, y, w, h, angle } = calculator?.elementSize(elem, viewScaleInfo, viewSizeInfo) || elem;
|
||||
rotateElement(ctx, { x, y, w, h, angle }, () => {
|
||||
if (!content) {
|
||||
opts.loader.load(elem as Element<'html'>, opts.elementAssets || {});
|
||||
}
|
||||
if (elem.type === 'html' && content) {
|
||||
const { opacity } = elem.detail;
|
||||
ctx.globalAlpha = opacity ? opacity : 1;
|
||||
ctx.globalAlpha = getOpacity(elem) * parentOpacity;
|
||||
ctx.drawImage(content, x, y, w, h);
|
||||
ctx.globalAlpha = 1;
|
||||
ctx.globalAlpha = parentOpacity;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
import type { Element, RendererDrawElementOptions, ViewContext2D } from '@idraw/types';
|
||||
import { rotateElement, calcViewBoxSize } from '@idraw/util';
|
||||
import { drawBox, drawBoxShadow } from './box';
|
||||
import { drawBox, drawBoxShadow, getOpacity } from './box';
|
||||
|
||||
export function drawImage(ctx: ViewContext2D, elem: Element<'image'>, opts: RendererDrawElementOptions) {
|
||||
const content = opts.loader.getContent(elem);
|
||||
const { calculator, viewScaleInfo, viewSizeInfo } = opts;
|
||||
const { calculator, viewScaleInfo, viewSizeInfo, parentOpacity } = opts;
|
||||
const { x, y, w, h, angle } = calculator?.elementSize(elem, viewScaleInfo, viewSizeInfo) || elem;
|
||||
|
||||
const viewElem = { ...elem, ...{ x, y, w, h, angle } };
|
||||
|
|
@ -18,13 +18,13 @@ export function drawImage(ctx: ViewContext2D, elem: Element<'image'>, opts: Rend
|
|||
calcElemSize: { x, y, w, h, angle },
|
||||
viewScaleInfo,
|
||||
viewSizeInfo,
|
||||
parentOpacity,
|
||||
renderContent: () => {
|
||||
if (!content) {
|
||||
opts.loader.load(elem as Element<'image'>, opts.elementAssets || {});
|
||||
}
|
||||
if (elem.type === 'image' && content) {
|
||||
const { opacity } = elem.detail;
|
||||
ctx.globalAlpha = opacity ? opacity : 1;
|
||||
ctx.globalAlpha = getOpacity(elem) * parentOpacity;
|
||||
const { x, y, w, h, radiusList } = calcViewBoxSize(viewElem, {
|
||||
viewScaleInfo,
|
||||
viewSizeInfo
|
||||
|
|
@ -42,7 +42,7 @@ export function drawImage(ctx: ViewContext2D, elem: Element<'image'>, opts: Rend
|
|||
ctx.fill();
|
||||
ctx.clip();
|
||||
ctx.drawImage(content, x, y, w, h);
|
||||
ctx.globalAlpha = 1;
|
||||
ctx.globalAlpha = parentOpacity;
|
||||
ctx.restore();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { drawBox, drawBoxShadow } from './box';
|
|||
export function drawPath(ctx: ViewContext2D, elem: Element<'path'>, opts: RendererDrawElementOptions) {
|
||||
const { detail } = elem;
|
||||
const { originX, originY, originW, originH } = detail;
|
||||
const { calculator, viewScaleInfo, viewSizeInfo } = opts;
|
||||
const { calculator, viewScaleInfo, viewSizeInfo, parentOpacity } = opts;
|
||||
const { x, y, w, h, angle } = calculator?.elementSize(elem, viewScaleInfo, viewSizeInfo) || elem;
|
||||
const scaleW = w / originW;
|
||||
const scaleH = h / originH;
|
||||
|
|
@ -23,6 +23,7 @@ export function drawPath(ctx: ViewContext2D, elem: Element<'path'>, opts: Render
|
|||
calcElemSize: { x, y, w, h, angle },
|
||||
viewScaleInfo,
|
||||
viewSizeInfo,
|
||||
parentOpacity,
|
||||
renderContent: () => {
|
||||
drawBoxShadow(ctx, viewElem, {
|
||||
viewScaleInfo,
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { rotateElement } from '@idraw/util';
|
|||
import { drawBox, drawBoxShadow } from './box';
|
||||
|
||||
export function drawRect(ctx: ViewContext2D, elem: Element<'rect'>, opts: RendererDrawElementOptions) {
|
||||
const { calculator, viewScaleInfo, viewSizeInfo } = opts;
|
||||
const { calculator, viewScaleInfo, viewSizeInfo, parentOpacity } = opts;
|
||||
const { x, y, w, h, angle } = calculator?.elementSize(elem, viewScaleInfo, viewSizeInfo) || elem;
|
||||
|
||||
const viewElem = { ...elem, ...{ x, y, w, h, angle } };
|
||||
|
|
@ -17,6 +17,7 @@ export function drawRect(ctx: ViewContext2D, elem: Element<'rect'>, opts: Render
|
|||
calcElemSize: { x, y, w, h, angle },
|
||||
viewScaleInfo,
|
||||
viewSizeInfo,
|
||||
parentOpacity,
|
||||
renderContent: () => {
|
||||
// empty
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,19 +1,19 @@
|
|||
import type { Element, RendererDrawElementOptions, ViewContext2D } from '@idraw/types';
|
||||
import { rotateElement } from '@idraw/util';
|
||||
import { getOpacity } from './box';
|
||||
|
||||
export function drawSVG(ctx: ViewContext2D, elem: Element<'svg'>, opts: RendererDrawElementOptions) {
|
||||
const content = opts.loader.getContent(elem);
|
||||
const { calculator, viewScaleInfo, viewSizeInfo } = opts;
|
||||
const { calculator, viewScaleInfo, viewSizeInfo, parentOpacity } = opts;
|
||||
const { x, y, w, h, angle } = calculator?.elementSize(elem, viewScaleInfo, viewSizeInfo) || elem;
|
||||
rotateElement(ctx, { x, y, w, h, angle }, () => {
|
||||
if (!content) {
|
||||
opts.loader.load(elem as Element<'svg'>, opts.elementAssets || {});
|
||||
}
|
||||
if (elem.type === 'svg' && content) {
|
||||
const { opacity } = elem.detail;
|
||||
ctx.globalAlpha = opacity ? opacity : 1;
|
||||
ctx.globalAlpha = getOpacity(elem) * parentOpacity;
|
||||
ctx.drawImage(content, x, y, w, h);
|
||||
ctx.globalAlpha = 1;
|
||||
ctx.globalAlpha = parentOpacity;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import { drawBox } from './box';
|
|||
const detailConfig = getDefaultElementDetailConfig();
|
||||
|
||||
export function drawText(ctx: ViewContext2D, elem: Element<'text'>, opts: RendererDrawElementOptions) {
|
||||
const { calculator, viewScaleInfo, viewSizeInfo } = opts;
|
||||
const { calculator, viewScaleInfo, viewSizeInfo, parentOpacity } = opts;
|
||||
const { x, y, w, h, angle } = calculator?.elementSize(elem, viewScaleInfo, viewSizeInfo) || elem;
|
||||
const viewElem = { ...elem, ...{ x, y, w, h, angle } };
|
||||
rotateElement(ctx, { x, y, w, h, angle }, () => {
|
||||
|
|
@ -15,6 +15,7 @@ export function drawText(ctx: ViewContext2D, elem: Element<'text'>, opts: Render
|
|||
calcElemSize: { x, y, w, h, angle },
|
||||
viewScaleInfo,
|
||||
viewSizeInfo,
|
||||
parentOpacity,
|
||||
renderContent: () => {
|
||||
const detail: Element<'text'>['detail'] = {
|
||||
...detailConfig,
|
||||
|
|
|
|||
|
|
@ -52,6 +52,7 @@ export class Renderer extends EventEmitter<RendererEventMap> implements BoardRen
|
|||
calculator,
|
||||
parentElementSize,
|
||||
elementAssets: data.assets,
|
||||
parentOpacity: 1,
|
||||
...opts
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,8 +40,8 @@ export class Loader extends EventEmitter<LoaderEventMap> implements RendererLoad
|
|||
this.#registerLoadFunc<'html'>('html', async (elem: Element<'html'>, assets: ElementAssets) => {
|
||||
const html = assets[elem.detail.html]?.value || elem.detail.html;
|
||||
const content = await loadHTML(html, {
|
||||
width: elem.detail.width || elem.w,
|
||||
height: elem.detail.height || elem.h
|
||||
width: elem.detail.originW || elem.w,
|
||||
height: elem.detail.originH || elem.h
|
||||
});
|
||||
return {
|
||||
uuid: elem.uuid,
|
||||
|
|
|
|||
|
|
@ -113,8 +113,8 @@ export interface ElementCircleDetail extends ElementBaseDetail {
|
|||
|
||||
export interface ElementHTMLDetail extends ElementBaseDetail {
|
||||
html: string;
|
||||
width?: number;
|
||||
height?: number;
|
||||
originW?: number;
|
||||
originH?: number;
|
||||
}
|
||||
|
||||
export interface ElementImageDetail extends ElementBaseDetail {
|
||||
|
|
@ -165,6 +165,7 @@ export interface ElementOperations {
|
|||
disableRotate?: boolean;
|
||||
limitRatio?: boolean;
|
||||
lastModified?: number;
|
||||
deepResize?: boolean;
|
||||
}
|
||||
|
||||
export interface Element<T extends ElementType = ElementType, E extends Record<string, any> = Record<string, any>> extends ElementSize {
|
||||
|
|
|
|||
19
packages/types/src/lib/modify.ts
Normal file
19
packages/types/src/lib/modify.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import type { Element, ElementPosition } from './element';
|
||||
import type { RecursivePartial } from './util';
|
||||
|
||||
export type ModifyType = 'update-element' | 'add-element' | 'delete-element' | 'move-element';
|
||||
|
||||
export type ModifyElement = Omit<RecursivePartial<Element>, 'uuid'> & { uuid: string };
|
||||
|
||||
export interface ModifyDataMap {
|
||||
'update-element': { position: ElementPosition; modifyElement: ModifyElement };
|
||||
'add-element': { position: ElementPosition; element: Element };
|
||||
'delete-element': { position: ElementPosition; element: Element };
|
||||
'move-element': { from: ElementPosition; to: ElementPosition };
|
||||
}
|
||||
|
||||
export interface ModifyItem<T extends ModifyType> {
|
||||
type: T;
|
||||
data: ModifyDataMap[T];
|
||||
time: number;
|
||||
}
|
||||
|
|
@ -40,4 +40,5 @@ export interface RendererDrawElementOptions extends RendererDrawOptions {
|
|||
viewSizeInfo: ViewSizeInfo;
|
||||
parentElementSize: ElementSize;
|
||||
elementAssets?: ElementAssets;
|
||||
parentOpacity: number;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ export { delay, compose, throttle } from './lib/time';
|
|||
export { downloadImageFromCanvas, parseFileToBase64, pickFile, parseFileToText, downloadFileFromText } from './lib/file';
|
||||
export { toColorHexStr, toColorHexNum, isColorStr, colorNameToHex, colorToCSS, colorToLinearGradientCSS, mergeHexColorAlpha } from './lib/color';
|
||||
export { createUUID, isAssetId, createAssetId } from './lib/uuid';
|
||||
export { deepClone, sortDataAsserts } from './lib/data';
|
||||
export { deepClone, sortDataAsserts, deepCloneElement } from './lib/data';
|
||||
export { istype } from './lib/istype';
|
||||
export { loadImage, loadSVG, loadHTML } from './lib/load';
|
||||
export { is } from './lib/is';
|
||||
|
|
@ -73,3 +73,4 @@ export {
|
|||
moveElementPosition,
|
||||
updateElementInList
|
||||
} from './lib/handle-element';
|
||||
export { deepResizeGroupElement } from './lib/resize-element';
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import type {
|
||||
ViewScaleInfo,
|
||||
// ViewScaleInfo,
|
||||
DefaultElementDetailConfig,
|
||||
ElementSize,
|
||||
ElementRectDetail,
|
||||
ElementCircleDetail,
|
||||
ElementTextDetail,
|
||||
|
|
@ -9,6 +10,8 @@ import type {
|
|||
ElementGroupDetail
|
||||
} from '@idraw/types';
|
||||
|
||||
export const defaultText = 'Text Element';
|
||||
|
||||
export function getDefaultElementDetailConfig(): DefaultElementDetailConfig {
|
||||
const config: DefaultElementDetailConfig = {
|
||||
boxSizing: 'border-box',
|
||||
|
|
@ -40,7 +43,7 @@ export function getDefaultElementRectDetail(): ElementRectDetail {
|
|||
return detail;
|
||||
}
|
||||
|
||||
export function getDefaultElementCircleDetail(opts: { radius: number }): ElementCircleDetail {
|
||||
export function getDefaultElementCircleDetail(): ElementCircleDetail {
|
||||
const detail: ElementCircleDetail = {
|
||||
background: '#D9D9D9',
|
||||
radius: 0
|
||||
|
|
@ -48,16 +51,16 @@ export function getDefaultElementCircleDetail(opts: { radius: number }): Element
|
|||
return detail;
|
||||
}
|
||||
|
||||
export function getDefaultElementTextDetail(opts?: { viewScaleInfo: ViewScaleInfo }): ElementTextDetail {
|
||||
export function getDefaultElementTextDetail(elementSize: ElementSize): ElementTextDetail {
|
||||
const detailConfig = getDefaultElementDetailConfig();
|
||||
const scale = opts?.viewScaleInfo?.scale || 1;
|
||||
// const scale = opts?.viewScaleInfo?.scale || 1;
|
||||
const detail: ElementTextDetail = {
|
||||
text: 'Text Element',
|
||||
text: defaultText,
|
||||
color: detailConfig.color,
|
||||
fontFamily: detailConfig.fontFamily,
|
||||
fontWeight: detailConfig.fontWeight,
|
||||
lineHeight: detailConfig.fontSize * scale,
|
||||
fontSize: detailConfig.fontSize * scale,
|
||||
lineHeight: elementSize.w / defaultText.length,
|
||||
fontSize: elementSize.w / defaultText.length,
|
||||
textAlign: 'center',
|
||||
verticalAlign: 'middle'
|
||||
};
|
||||
|
|
@ -78,7 +81,7 @@ export function getDefaultElementImageDetail(): ElementImageDetail {
|
|||
return detail;
|
||||
}
|
||||
|
||||
export function getDefaultElementGroupDetail(opts?: { viewScaleInfo: ViewScaleInfo }): ElementGroupDetail {
|
||||
export function getDefaultElementGroupDetail(): ElementGroupDetail {
|
||||
const detail: ElementGroupDetail = {
|
||||
children: [],
|
||||
background: '#D9D9D9',
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import type { Data, ElementAssets, Elements, ElementType, Element } from '@idraw/types';
|
||||
import { createAssetId } from './uuid';
|
||||
import { createAssetId, createUUID } from './uuid';
|
||||
|
||||
export function deepClone<T extends any = any>(target: T): T {
|
||||
export function deepClone<T = any>(target: T): T {
|
||||
function _clone(t: T) {
|
||||
const type = is(t);
|
||||
if (['Null', 'Number', 'String', 'Boolean', 'Undefined'].indexOf(type) >= 0) {
|
||||
|
|
@ -28,6 +28,20 @@ export function deepClone<T extends any = any>(target: T): T {
|
|||
return _clone(target) as T;
|
||||
}
|
||||
|
||||
export function deepCloneElement<T extends Element = Element>(element: T): T {
|
||||
const elem = deepClone(element);
|
||||
const _resetUUID = (e: Element) => {
|
||||
e.uuid = createUUID();
|
||||
if (e.type === 'group' && (e as Element<'group'>).detail.children) {
|
||||
(e as Element<'group'>).detail.children.forEach((child) => {
|
||||
_resetUUID(child);
|
||||
});
|
||||
}
|
||||
};
|
||||
_resetUUID(elem);
|
||||
return elem;
|
||||
}
|
||||
|
||||
function is(target: any): string {
|
||||
return Object.prototype.toString
|
||||
.call(target)
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
import type { RecursivePartial, Element, Elements, ElementPosition, ElementSize, ElementType, ViewScaleInfo, ViewSizeInfo } from '@idraw/types';
|
||||
import { createUUID } from './uuid';
|
||||
import {
|
||||
getDefaultElementDetailConfig,
|
||||
defaultText,
|
||||
getDefaultElementRectDetail,
|
||||
getDefaultElementCircleDetail,
|
||||
getDefaultElementTextDetail,
|
||||
|
|
@ -12,10 +12,11 @@ import {
|
|||
} from './config';
|
||||
import { istype } from './istype';
|
||||
import { findElementFromListByPosition, getElementPositionFromList } from './element';
|
||||
import { deepResizeGroupElement } from './resize-element';
|
||||
|
||||
const defaultViewWidth = 200;
|
||||
const defaultViewHeight = 200;
|
||||
const defaultDetail = getDefaultElementDetailConfig();
|
||||
// const defaultDetail = getDefaultElementDetailConfig();
|
||||
|
||||
function createElementSize(type: ElementType, opts?: { viewScaleInfo: ViewScaleInfo; viewSizeInfo: ViewSizeInfo }): ElementSize {
|
||||
let x = 0;
|
||||
|
|
@ -27,27 +28,24 @@ function createElementSize(type: ElementType, opts?: { viewScaleInfo: ViewScaleI
|
|||
const { viewScaleInfo, viewSizeInfo } = opts;
|
||||
const { scale, offsetLeft, offsetTop } = viewScaleInfo;
|
||||
const { width, height } = viewSizeInfo;
|
||||
if (type === 'text') {
|
||||
const textDetail = getDefaultElementTextDetail();
|
||||
w = defaultDetail.fontSize * scale * textDetail.text.length;
|
||||
h = defaultDetail.fontSize * scale * 2;
|
||||
const limitViewWidth = width / 4;
|
||||
const limitViewHeight = height / 4;
|
||||
if (defaultViewWidth >= limitViewWidth) {
|
||||
w = limitViewWidth / scale;
|
||||
} else {
|
||||
const limitViewWidth = width / 4;
|
||||
const limitViewHeight = height / 4;
|
||||
if (defaultViewWidth >= limitViewWidth) {
|
||||
w = limitViewWidth / scale;
|
||||
} else {
|
||||
w = defaultViewWidth / scale;
|
||||
}
|
||||
w = defaultViewWidth / scale;
|
||||
}
|
||||
|
||||
if (defaultViewHeight >= limitViewHeight) {
|
||||
h = limitViewHeight / scale;
|
||||
} else {
|
||||
h = defaultViewHeight / scale;
|
||||
}
|
||||
if (['circle', 'svg', 'image'].includes(type)) {
|
||||
w = h = Math.max(w, h);
|
||||
}
|
||||
if (defaultViewHeight >= limitViewHeight) {
|
||||
h = limitViewHeight / scale;
|
||||
} else {
|
||||
h = defaultViewHeight / scale;
|
||||
}
|
||||
if (['circle', 'svg', 'image'].includes(type)) {
|
||||
w = h = Math.max(w, h);
|
||||
} else if (type === 'text') {
|
||||
const fontSize = w / defaultText.length;
|
||||
h = fontSize * 2;
|
||||
}
|
||||
|
||||
x = (0 - offsetLeft + width / 2 - (w * scale) / 2) / scale;
|
||||
|
|
@ -73,16 +71,14 @@ export function createElement<T extends ElementType>(
|
|||
limitRatio?: boolean;
|
||||
}
|
||||
): Element<T> {
|
||||
const elemSize = createElementSize(type, opts);
|
||||
const elementSize = createElementSize(type, opts);
|
||||
let detail = {};
|
||||
if (type === 'rect') {
|
||||
detail = getDefaultElementRectDetail();
|
||||
} else if (type === 'circle') {
|
||||
detail = getDefaultElementCircleDetail({
|
||||
radius: elemSize.w
|
||||
});
|
||||
detail = getDefaultElementCircleDetail();
|
||||
} else if (type === 'text') {
|
||||
detail = getDefaultElementTextDetail(opts);
|
||||
detail = getDefaultElementTextDetail(elementSize);
|
||||
} else if (type === 'svg') {
|
||||
detail = getDefaultElementSVGDetail();
|
||||
} else if (type === 'image') {
|
||||
|
|
@ -91,7 +87,7 @@ export function createElement<T extends ElementType>(
|
|||
detail = getDefaultElementGroupDetail();
|
||||
}
|
||||
const elem: Element<T> = {
|
||||
...elemSize,
|
||||
...elementSize,
|
||||
...baseElem,
|
||||
uuid: createUUID(),
|
||||
type,
|
||||
|
|
@ -262,6 +258,15 @@ export function updateElementInList(uuid: string, updateContent: RecursivePartia
|
|||
for (let i = 0; i < elements.length; i++) {
|
||||
const elem = elements[i];
|
||||
if (elem.uuid === uuid) {
|
||||
if (elem.type === 'group' && elem.operations?.deepResize === true) {
|
||||
if ((updateContent.w && updateContent.w > 0) || (updateContent.h && updateContent.h > 0)) {
|
||||
deepResizeGroupElement(elem as Element<'group'>, {
|
||||
w: updateContent.w,
|
||||
h: updateContent.h
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
mergeElement(elem, updateContent);
|
||||
targetElement = elem;
|
||||
break;
|
||||
|
|
|
|||
122
packages/util/src/lib/resize-element.ts
Normal file
122
packages/util/src/lib/resize-element.ts
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
import type { Element, ElementSize } from '@idraw/types';
|
||||
import { formatNumber } from './number';
|
||||
|
||||
const doNum = (n: number) => {
|
||||
return formatNumber(n, { decimalPlaces: 4 });
|
||||
};
|
||||
|
||||
interface ResizeOptions {
|
||||
xRatio: number;
|
||||
yRatio: number;
|
||||
minRatio: number;
|
||||
maxRatio: number;
|
||||
}
|
||||
|
||||
function resizeElementBaseDetail(elem: Element, opts: ResizeOptions) {
|
||||
const { detail } = elem;
|
||||
const { xRatio, yRatio, maxRatio } = opts;
|
||||
const middleRatio = (xRatio + yRatio) / 2;
|
||||
const { borderWidth, borderRadius, borderDash, shadowOffsetX, shadowOffsetY, shadowBlur } = detail;
|
||||
if (typeof borderWidth === 'number') {
|
||||
detail.borderWidth = doNum(borderWidth * middleRatio);
|
||||
} else if (Array.isArray(detail.borderWidth)) {
|
||||
const bw = borderWidth as [number, number, number, number];
|
||||
// [top, right, bottom, left]
|
||||
detail.borderWidth = [doNum(bw[0] * yRatio), doNum(bw[1] * xRatio), doNum(bw[2] * yRatio), doNum(bw[3] * xRatio)];
|
||||
}
|
||||
|
||||
if (typeof borderRadius === 'number') {
|
||||
detail.borderRadius = doNum(borderRadius * middleRatio);
|
||||
} else if (Array.isArray(detail.borderRadius)) {
|
||||
const br = borderRadius as [number, number, number, number];
|
||||
// [top-left, top-right, bottom-left, bottom-right]
|
||||
detail.borderRadius = [br[0] * xRatio, br[1] * xRatio, br[2] * yRatio, br[3] * yRatio];
|
||||
}
|
||||
|
||||
if (Array.isArray(borderDash)) {
|
||||
borderDash.forEach((dash: number, i) => {
|
||||
(detail.borderDash as number[])[i] = doNum(dash * maxRatio);
|
||||
});
|
||||
}
|
||||
|
||||
if (typeof shadowOffsetX === 'number') {
|
||||
detail.shadowOffsetX = doNum(shadowOffsetX * maxRatio);
|
||||
}
|
||||
if (typeof shadowOffsetY === 'number') {
|
||||
detail.shadowOffsetX = doNum(shadowOffsetY * maxRatio);
|
||||
}
|
||||
if (typeof shadowBlur === 'number') {
|
||||
detail.shadowOffsetX = doNum(shadowBlur * maxRatio);
|
||||
}
|
||||
}
|
||||
|
||||
function resizeElementBase(elem: Element, opts: ResizeOptions) {
|
||||
const { xRatio, yRatio } = opts;
|
||||
const { x, y, w, h } = elem;
|
||||
elem.x = doNum(x * xRatio);
|
||||
elem.y = doNum(y * yRatio);
|
||||
elem.w = doNum(w * xRatio);
|
||||
elem.h = doNum(h * yRatio);
|
||||
resizeElementBaseDetail(elem, opts);
|
||||
}
|
||||
|
||||
function resizeTextElementDetail(elem: Element<'text'>, opts: ResizeOptions) {
|
||||
const { minRatio, maxRatio } = opts;
|
||||
const { fontSize, lineHeight } = elem.detail;
|
||||
const ratio = (minRatio + maxRatio) / 2;
|
||||
|
||||
if (fontSize && fontSize > 0) {
|
||||
elem.detail.fontSize = doNum(fontSize * ratio);
|
||||
}
|
||||
if (lineHeight && lineHeight > 0) {
|
||||
elem.detail.lineHeight = doNum(lineHeight * ratio);
|
||||
}
|
||||
}
|
||||
|
||||
function resizeElement(elem: Element, opts: ResizeOptions) {
|
||||
const { type } = elem;
|
||||
// base and rect
|
||||
resizeElementBase(elem, opts);
|
||||
|
||||
if (type === 'circle') {
|
||||
// TODO
|
||||
} else if (type === 'text') {
|
||||
resizeTextElementDetail(elem as Element<'text'>, opts);
|
||||
} else if (type === 'image') {
|
||||
// TODO
|
||||
} else if (type === 'svg') {
|
||||
// TODO
|
||||
} else if (type === 'html') {
|
||||
// TODO
|
||||
} else if (type === 'path') {
|
||||
// TODO
|
||||
} else if (type === 'group' && Array.isArray((elem as Element<'group'>).detail.children)) {
|
||||
(elem as Element<'group'>).detail.children.forEach((child) => {
|
||||
resizeElement(child, opts);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function deepResizeGroupElement(elem: Element<'group'>, size: Pick<Partial<ElementSize>, 'w' | 'h'>): Element<'group'> {
|
||||
const resizeW: number = size.w && size.w > 0 ? size.w : elem.w;
|
||||
const resizeH: number = size.h && size.h > 0 ? size.h : elem.h;
|
||||
const xRatio = resizeW / elem.w;
|
||||
const yRatio = resizeH / elem.h;
|
||||
if (xRatio === yRatio && xRatio === 1) {
|
||||
return elem;
|
||||
}
|
||||
|
||||
const minRatio = Math.min(xRatio, yRatio);
|
||||
const maxRatio = Math.max(xRatio, yRatio);
|
||||
|
||||
elem.w = resizeW;
|
||||
elem.h = resizeH;
|
||||
const opts = { xRatio, yRatio, minRatio, maxRatio };
|
||||
if (elem.type === 'group' && Array.isArray(elem.detail.children)) {
|
||||
elem.detail.children.forEach((child) => {
|
||||
resizeElement(child, opts);
|
||||
});
|
||||
}
|
||||
resizeElementBaseDetail(elem, opts);
|
||||
return elem;
|
||||
}
|
||||
|
|
@ -8,7 +8,7 @@ export function calcViewBoxSize(viewElem: Element, opts: { viewScaleInfo: ViewSc
|
|||
let { borderRadius } = viewElem.detail;
|
||||
const { boxSizing = defaultElemConfig.boxSizing, borderWidth } = viewElem.detail;
|
||||
|
||||
if (typeof borderWidth !== 'number') {
|
||||
if (Array.isArray(borderWidth)) {
|
||||
// TODO: If borderWidth is an array, borderRadius will not take effect and will become 0.
|
||||
borderRadius = 0;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue