feat: optimize drawing element and resizing in-group

This commit is contained in:
chenshenhai 2023-12-30 10:45:41 +08:00
parent d6f859f864
commit 09a9c8e7de
27 changed files with 314 additions and 121 deletions

View file

@ -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;

View file

@ -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',

View file

@ -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();
};

View file

@ -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';
};
};

View file

@ -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()
});
}

View file

@ -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';

View file

@ -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([]);
}

View file

@ -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;
}
});
});

View file

@ -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);
}

View file

@ -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;
});
}

View file

@ -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;
}
});
}

View file

@ -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();
}
}

View file

@ -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,

View file

@ -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
}

View file

@ -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;
}
});
}

View file

@ -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,

View file

@ -52,6 +52,7 @@ export class Renderer extends EventEmitter<RendererEventMap> implements BoardRen
calculator,
parentElementSize,
elementAssets: data.assets,
parentOpacity: 1,
...opts
});
}

View file

@ -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,

View file

@ -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 {

View 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;
}

View file

@ -40,4 +40,5 @@ export interface RendererDrawElementOptions extends RendererDrawOptions {
viewSizeInfo: ViewSizeInfo;
parentElementSize: ElementSize;
elementAssets?: ElementAssets;
parentOpacity: number;
}

View file

@ -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';

View file

@ -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',

View file

@ -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)

View file

@ -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;

View 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;
}

View file

@ -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;
}