From 09a9c8e7de0fc6c74ab3133cc37fe41fac848db3 Mon Sep 17 00:00:00 2001 From: chenshenhai Date: Sat, 30 Dec 2023 10:45:41 +0800 Subject: [PATCH] feat: optimize drawing element and resizing in-group --- packages/board/src/lib/watcher.ts | 1 + packages/core/src/middleware/scroller/util.ts | 2 - .../core/src/middleware/selector/index.ts | 24 +++- packages/idraw/src/event.ts | 2 +- packages/idraw/src/idraw.ts | 5 +- packages/idraw/src/index.ts | 4 +- packages/renderer/src/draw/box.ts | 50 ++----- packages/renderer/src/draw/circle.ts | 40 ++++-- packages/renderer/src/draw/elements.ts | 8 +- packages/renderer/src/draw/group.ts | 11 +- packages/renderer/src/draw/html.ts | 8 +- packages/renderer/src/draw/image.ts | 10 +- packages/renderer/src/draw/path.ts | 3 +- packages/renderer/src/draw/rect.ts | 3 +- packages/renderer/src/draw/svg.ts | 8 +- packages/renderer/src/draw/text.ts | 3 +- packages/renderer/src/index.ts | 1 + packages/renderer/src/loader.ts | 4 +- packages/types/src/lib/element.ts | 5 +- packages/types/src/lib/modify.ts | 19 +++ packages/types/src/lib/renderer.ts | 1 + packages/util/src/index.ts | 3 +- packages/util/src/lib/config.ts | 19 +-- packages/util/src/lib/data.ts | 18 ++- packages/util/src/lib/handle-element.ts | 59 +++++---- packages/util/src/lib/resize-element.ts | 122 ++++++++++++++++++ packages/util/src/lib/view-box.ts | 2 +- 27 files changed, 314 insertions(+), 121 deletions(-) create mode 100644 packages/types/src/lib/modify.ts create mode 100644 packages/util/src/lib/resize-element.ts diff --git a/packages/board/src/lib/watcher.ts b/packages/board/src/lib/watcher.ts index 93dc8c5..4e58c8c 100644 --- a/packages/board/src/lib/watcher.ts +++ b/packages/board/src/lib/watcher.ts @@ -92,6 +92,7 @@ export class BoardWatcher extends EventEmitter { 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; diff --git a/packages/core/src/middleware/scroller/util.ts b/packages/core/src/middleware/scroller/util.ts index d4a5f4a..a72852a 100644 --- a/packages/core/src/middleware/scroller/util.ts +++ b/packages/core/src/middleware/scroller/util.ts @@ -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', diff --git a/packages/core/src/middleware/selector/index.ts b/packages/core/src/middleware/selector/index.ts index 1164302..69487db 100644 --- a/packages/core/src/middleware/selector/index.ts +++ b/packages/core/src/middleware/selector/index.ts @@ -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, { + 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= 0 && elem?.detail?.opacity <= 1) { + opacity = elem?.detail?.opacity; + } + return opacity; +} + export function drawBox( ctx: ViewContext2D, viewElem: Element, @@ -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, 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, opts: ctx.arcTo(x, y, x + w, y, radiusList[0]); ctx.closePath(); ctx.stroke(); - ctx.globalAlpha = 1; } ctx.setLineDash([]); } diff --git a/packages/renderer/src/draw/circle.ts b/packages/renderer/src/draw/circle.ts index 5629a88..22b46f0 100644 --- a/packages/renderer/src/draw/circle.ts +++ b/packages/renderer/src/draw/circle.ts @@ -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; } }); }); diff --git a/packages/renderer/src/draw/elements.ts b/packages/renderer/src/draw/elements.ts index bbf57f7..2a8f3c2 100644 --- a/packages/renderer/src/draw/elements.ts +++ b/packages/renderer/src/draw/elements.ts @@ -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); } diff --git a/packages/renderer/src/draw/group.ts b/packages/renderer/src/draw/group.ts index 071f023..a144bce 100644 --- a/packages/renderer/src/draw/group.ts +++ b/packages/renderer/src/draw/group.ts @@ -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, opts: RendererDrawElementOptions) { @@ -64,10 +64,11 @@ export function drawElement(ctx: ViewContext2D, elem: Element, 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; }); } diff --git a/packages/renderer/src/draw/html.ts b/packages/renderer/src/draw/html.ts index 17634fc..40ee874 100644 --- a/packages/renderer/src/draw/html.ts +++ b/packages/renderer/src/draw/html.ts @@ -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; } }); } diff --git a/packages/renderer/src/draw/image.ts b/packages/renderer/src/draw/image.ts index 3dd7ef5..78dc2d2 100644 --- a/packages/renderer/src/draw/image.ts +++ b/packages/renderer/src/draw/image.ts @@ -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(); } } diff --git a/packages/renderer/src/draw/path.ts b/packages/renderer/src/draw/path.ts index 74a6138..893b75a 100644 --- a/packages/renderer/src/draw/path.ts +++ b/packages/renderer/src/draw/path.ts @@ -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, diff --git a/packages/renderer/src/draw/rect.ts b/packages/renderer/src/draw/rect.ts index 741d609..b71c84f 100644 --- a/packages/renderer/src/draw/rect.ts +++ b/packages/renderer/src/draw/rect.ts @@ -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 } diff --git a/packages/renderer/src/draw/svg.ts b/packages/renderer/src/draw/svg.ts index b476f71..e84ecb2 100644 --- a/packages/renderer/src/draw/svg.ts +++ b/packages/renderer/src/draw/svg.ts @@ -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; } }); } diff --git a/packages/renderer/src/draw/text.ts b/packages/renderer/src/draw/text.ts index e9824fd..77d5bdf 100644 --- a/packages/renderer/src/draw/text.ts +++ b/packages/renderer/src/draw/text.ts @@ -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, diff --git a/packages/renderer/src/index.ts b/packages/renderer/src/index.ts index 2757500..67e3b42 100644 --- a/packages/renderer/src/index.ts +++ b/packages/renderer/src/index.ts @@ -52,6 +52,7 @@ export class Renderer extends EventEmitter implements BoardRen calculator, parentElementSize, elementAssets: data.assets, + parentOpacity: 1, ...opts }); } diff --git a/packages/renderer/src/loader.ts b/packages/renderer/src/loader.ts index 24424af..8adfb98 100644 --- a/packages/renderer/src/loader.ts +++ b/packages/renderer/src/loader.ts @@ -40,8 +40,8 @@ export class Loader extends EventEmitter 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, diff --git a/packages/types/src/lib/element.ts b/packages/types/src/lib/element.ts index 67ef830..cb9f625 100644 --- a/packages/types/src/lib/element.ts +++ b/packages/types/src/lib/element.ts @@ -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 = Record> extends ElementSize { diff --git a/packages/types/src/lib/modify.ts b/packages/types/src/lib/modify.ts new file mode 100644 index 0000000..6e43942 --- /dev/null +++ b/packages/types/src/lib/modify.ts @@ -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, '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 { + type: T; + data: ModifyDataMap[T]; + time: number; +} diff --git a/packages/types/src/lib/renderer.ts b/packages/types/src/lib/renderer.ts index eb9d56c..aa3e057 100644 --- a/packages/types/src/lib/renderer.ts +++ b/packages/types/src/lib/renderer.ts @@ -40,4 +40,5 @@ export interface RendererDrawElementOptions extends RendererDrawOptions { viewSizeInfo: ViewSizeInfo; parentElementSize: ElementSize; elementAssets?: ElementAssets; + parentOpacity: number; } diff --git a/packages/util/src/index.ts b/packages/util/src/index.ts index 71f5896..5d4d74b 100644 --- a/packages/util/src/index.ts +++ b/packages/util/src/index.ts @@ -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'; diff --git a/packages/util/src/lib/config.ts b/packages/util/src/lib/config.ts index 1740781..34ef63c 100644 --- a/packages/util/src/lib/config.ts +++ b/packages/util/src/lib/config.ts @@ -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', diff --git a/packages/util/src/lib/data.ts b/packages/util/src/lib/data.ts index e706f91..429c7e0 100644 --- a/packages/util/src/lib/data.ts +++ b/packages/util/src/lib/data.ts @@ -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(target: T): T { +export function deepClone(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(target: T): T { return _clone(target) as T; } +export function deepCloneElement(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) diff --git a/packages/util/src/lib/handle-element.ts b/packages/util/src/lib/handle-element.ts index 290d4b7..e9e2cc1 100644 --- a/packages/util/src/lib/handle-element.ts +++ b/packages/util/src/lib/handle-element.ts @@ -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( limitRatio?: boolean; } ): Element { - 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( detail = getDefaultElementGroupDetail(); } const elem: Element = { - ...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; diff --git a/packages/util/src/lib/resize-element.ts b/packages/util/src/lib/resize-element.ts new file mode 100644 index 0000000..961e8b8 --- /dev/null +++ b/packages/util/src/lib/resize-element.ts @@ -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, '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; +} diff --git a/packages/util/src/lib/view-box.ts b/packages/util/src/lib/view-box.ts index 4bc3a7d..1e2509d 100644 --- a/packages/util/src/lib/view-box.ts +++ b/packages/util/src/lib/view-box.ts @@ -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; }