From a2979cd5ba488b5a4efcdbacbe7c7a90a0d257b8 Mon Sep 17 00:00:00 2001 From: chenshenhai Date: Sun, 2 Jun 2024 12:10:26 +0800 Subject: [PATCH] feat: enhance element detail --- packages/board/src/lib/sharer.ts | 15 ++- .../core/src/middleware/selector/index.ts | 2 +- .../core/src/middleware/text-editor/index.ts | 31 ++++-- packages/renderer/src/draw/box.ts | 94 ++++++++++++++++--- packages/renderer/src/draw/global.ts | 11 +++ packages/renderer/src/draw/group.ts | 7 +- packages/renderer/src/draw/index.ts | 1 + packages/renderer/src/draw/layout.ts | 6 +- packages/renderer/src/draw/text.ts | 63 ++++++++++--- packages/renderer/src/index.ts | 6 +- packages/types/src/lib/config.ts | 2 +- packages/types/src/lib/data.ts | 8 +- packages/types/src/lib/element.ts | 9 ++ packages/types/src/lib/renderer.ts | 3 +- packages/types/src/lib/store.ts | 6 +- packages/util/src/index.ts | 1 + packages/util/src/lib/config.ts | 2 + packages/util/src/lib/text.ts | 5 + 18 files changed, 224 insertions(+), 48 deletions(-) create mode 100644 packages/renderer/src/draw/global.ts create mode 100644 packages/util/src/lib/text.ts diff --git a/packages/board/src/lib/sharer.ts b/packages/board/src/lib/sharer.ts index ae178f4..0644140 100644 --- a/packages/board/src/lib/sharer.ts +++ b/packages/board/src/lib/sharer.ts @@ -1,4 +1,4 @@ -import type { ActiveStore, StoreSharer, ViewScaleInfo, ViewSizeInfo } from '@idraw/types'; +import type { ActiveStore, Element, ElementDetailMap, RecursivePartial, StoreSharer, ViewScaleInfo, ViewSizeInfo } from '@idraw/types'; import { Store } from '@idraw/util'; const defaultActiveStorage: ActiveStore = { @@ -12,10 +12,11 @@ const defaultActiveStorage: ActiveStore = { offsetLeft: 0, offsetRight: 0, offsetTop: 0, - offsetBottom: 0 + offsetBottom: 0, + overrideElementMap: null }; -export class Sharer implements StoreSharer> { +export class Sharer implements StoreSharer> { #activeStore: Store; #sharedStore: Store<{ [string: string | number | symbol]: any; @@ -96,4 +97,12 @@ export class Sharer implements StoreSharer }; return sizeInfo; } + + getActiveOverrideElemenentMap(): Record>>> | null { + return this.#activeStore.get('overrideElementMap'); + } + + setActiveOverrideElemenentMap(map: Record>>> | null): void { + this.#activeStore.set('overrideElementMap', map); + } } diff --git a/packages/core/src/middleware/selector/index.ts b/packages/core/src/middleware/selector/index.ts index d0e3476..b15fbf8 100644 --- a/packages/core/src/middleware/selector/index.ts +++ b/packages/core/src/middleware/selector/index.ts @@ -705,7 +705,7 @@ export const MiddlewareSelector: BoardMiddleware & Re const defaultElementDetail = getDefaultElementDetailConfig(); export const MiddlewareTextEditor: BoardMiddleware = (opts) => { - const { eventHub, boardContent, viewer } = opts; + const { eventHub, boardContent, viewer, sharer } = opts; const canvas = boardContent.boardContext.canvas; // const textarea = document.createElement('textarea'); const textarea = document.createElement('div'); @@ -53,9 +53,26 @@ export const MiddlewareTextEditor: BoardMiddleware { + if (activeElem?.uuid) { + const map = sharer.getActiveOverrideElemenentMap(); + if (map) { + delete map[activeElem.uuid]; + } + sharer.setActiveOverrideElemenentMap(map); + viewer.drawFrame(); + } + mask.style.display = 'none'; activeElem = null; activePosition = []; @@ -157,11 +174,13 @@ export const MiddlewareTextEditor: BoardMiddleware { - ctx.globalAlpha = opacity; - drawBoxBackground(ctx, viewElem, { pattern, viewScaleInfo, viewSizeInfo }); - renderContent?.(); - drawBoxBorder(ctx, viewElem, { viewScaleInfo, viewSizeInfo }); - ctx.globalAlpha = parentOpacity; + const { clipPath, clipPathStrokeColor, clipPathStrokeWidth } = originElem.detail; + + const mainRender = () => { + ctx.globalAlpha = opacity; + drawBoxBackground(ctx, viewElem, { pattern, viewScaleInfo, viewSizeInfo }); + renderContent?.(); + drawBoxBorder(ctx, viewElem, { viewScaleInfo, viewSizeInfo }); + ctx.globalAlpha = parentOpacity; + }; + if (clipPath) { + drawClipPath(ctx, viewElem, { + originElem, + calcElemSize, + viewScaleInfo, + viewSizeInfo, + renderContent: () => { + mainRender(); + } + }); + + if (typeof clipPathStrokeWidth === 'number' && clipPathStrokeWidth > 0 && clipPathStrokeColor) { + drawClipPathStroke(ctx, viewElem, { + originElem, + calcElemSize, + viewScaleInfo, + viewSizeInfo, + parentOpacity + }); } - }); + } else { + mainRender(); + } } -// TODO function drawClipPath( ctx: ViewContext2D, viewElem: Element, @@ -75,6 +93,56 @@ function drawClipPath( const pathStr = generateSVGPath(clipPath.commands || []); const path2d = new Path2D(pathStr); ctx.clip(path2d); + + ctx.translate(0 - (internalX as number), 0 - (internalY as number)); + ctx.setTransform(1, 0, 0, 1, 0, 0); + + rotateElement(ctx, { ...viewElem }, () => { + renderContent?.(); + }); + + ctx.restore(); + } else { + renderContent?.(); + } +} + +function drawClipPathStroke( + ctx: ViewContext2D, + viewElem: Element, + opts: { + originElem?: Element; + calcElemSize?: ElementSize; + renderContent?: () => void; + viewScaleInfo: ViewScaleInfo; + viewSizeInfo: ViewSizeInfo; + parentOpacity: number; + } +) { + const { renderContent, originElem, calcElemSize, viewSizeInfo, parentOpacity } = opts; + const totalScale = viewSizeInfo.devicePixelRatio; + const { clipPath, clipPathStrokeColor, clipPathStrokeWidth } = originElem?.detail || {}; + if (clipPath && calcElemSize && clipPath.commands && typeof clipPathStrokeWidth === 'number' && clipPathStrokeWidth > 0 && clipPathStrokeColor) { + const { x, y, w, h } = calcElemSize; + const { originW, originH, originX, originY } = clipPath; + const scaleW = w / originW; + const scaleH = h / originH; + const viewOriginX = originX * scaleW; + const viewOriginY = originY * scaleH; + const internalX = x - viewOriginX; + const internalY = y - viewOriginY; + + ctx.save(); + ctx.globalAlpha = parentOpacity; + ctx.translate(internalX as number, internalY as number); + ctx.scale(totalScale * scaleW, totalScale * scaleH); + const pathStr = generateSVGPath(clipPath.commands || []); + const path2d = new Path2D(pathStr); + + ctx.strokeStyle = clipPathStrokeColor; + ctx.lineWidth = clipPathStrokeWidth; + ctx.stroke(path2d); + ctx.translate(0 - (internalX as number), 0 - (internalY as number)); ctx.setTransform(1, 0, 0, 1, 0, 0); diff --git a/packages/renderer/src/draw/global.ts b/packages/renderer/src/draw/global.ts new file mode 100644 index 0000000..c009ec8 --- /dev/null +++ b/packages/renderer/src/draw/global.ts @@ -0,0 +1,11 @@ +import type { RendererDrawElementOptions, ViewContext2D, ElementGlobalDetail } from '@idraw/types'; + +export function drawGlobalBackground(ctx: ViewContext2D, global: ElementGlobalDetail | undefined, opts: RendererDrawElementOptions) { + if (typeof global?.background === 'string') { + const { viewSizeInfo } = opts; + const { width, height } = viewSizeInfo; + ctx.globalAlpha = 1; + ctx.fillStyle = global.background; + ctx.fillRect(0, 0, width, height); + } +} diff --git a/packages/renderer/src/draw/group.ts b/packages/renderer/src/draw/group.ts index 718a13e..e9934c9 100644 --- a/packages/renderer/src/draw/group.ts +++ b/packages/renderer/src/draw/group.ts @@ -19,6 +19,11 @@ export function drawElement(ctx: ViewContext2D, elem: Element, opts return; } + const { overrideElementMap } = opts; + if (overrideElementMap?.[elem.uuid]?.operations?.invisible) { + return; + } + try { switch (elem.type) { case 'rect': { @@ -71,7 +76,7 @@ export function drawElement(ctx: ViewContext2D, elem: Element, opts export function drawGroup(ctx: ViewContext2D, elem: Element<'group'>, opts: RendererDrawElementOptions) { const { viewScaleInfo, viewSizeInfo, parentOpacity } = opts; - const { x, y, w, h, angle } = calcViewElementSize({ x: elem.x, y: elem.y, w: elem.w, h: elem.h, angle: elem.angle }, { viewScaleInfo, viewSizeInfo }) || elem; + const { x, y, w, h, angle } = calcViewElementSize({ x: elem.x, y: elem.y, w: elem.w, h: elem.h, angle: elem.angle }, { viewScaleInfo }) || elem; const viewElem = { ...elem, ...{ x, y, w, h, angle } }; rotateElement(ctx, { x, y, w, h, angle }, () => { ctx.globalAlpha = getOpacity(elem) * parentOpacity; diff --git a/packages/renderer/src/draw/index.ts b/packages/renderer/src/draw/index.ts index fd5228f..b7cd272 100644 --- a/packages/renderer/src/draw/index.ts +++ b/packages/renderer/src/draw/index.ts @@ -6,3 +6,4 @@ export { drawHTML } from './html'; export { drawText } from './text'; export { drawElementList } from './elements'; export { drawLayout } from './layout'; +export { drawGlobalBackground } from './global'; diff --git a/packages/renderer/src/draw/layout.ts b/packages/renderer/src/draw/layout.ts index f1636ac..80558e3 100644 --- a/packages/renderer/src/draw/layout.ts +++ b/packages/renderer/src/draw/layout.ts @@ -4,8 +4,8 @@ import { drawBoxShadow, drawBoxBackground, drawBoxBorder } from './box'; export function drawLayout(ctx: ViewContext2D, layout: DataLayout, opts: RendererDrawElementOptions, renderContent: (ctx: ViewContext2D) => void) { const { viewScaleInfo, viewSizeInfo, parentOpacity } = opts; - const elem: Element = { uuid: 'layout', type: 'group', ...layout }; - const { x, y, w, h } = calcViewElementSize(elem, { viewScaleInfo, viewSizeInfo }) || elem; + const elem: Element = { uuid: 'layout', type: 'group', ...layout } as Element; + const { x, y, w, h } = calcViewElementSize(elem, { viewScaleInfo }) || elem; const angle = 0; const viewElem: Element = { ...elem, ...{ x, y, w, h, angle } } as Element; ctx.globalAlpha = 1; @@ -20,7 +20,7 @@ export function drawLayout(ctx: ViewContext2D, layout: DataLayout, opts: Rendere if (layout.detail.overflow === 'hidden') { const { viewScaleInfo, viewSizeInfo } = opts; const elem: Element<'group'> = { uuid: 'layout', type: 'group', ...layout } as Element<'group'>; - const viewElemSize = calcViewElementSize(elem, { viewScaleInfo, viewSizeInfo }) || elem; + const viewElemSize = calcViewElementSize(elem, { viewScaleInfo }) || elem; const viewElem = { ...elem, ...viewElemSize }; const { x, y, w, h, radiusList } = calcViewBoxSize(viewElem, { viewScaleInfo, diff --git a/packages/renderer/src/draw/text.ts b/packages/renderer/src/draw/text.ts index 41c3d29..a0f1eed 100644 --- a/packages/renderer/src/draw/text.ts +++ b/packages/renderer/src/draw/text.ts @@ -1,10 +1,20 @@ import type { Element, RendererDrawElementOptions, ViewContext2D } from '@idraw/types'; -import { rotateElement, calcViewElementSize } from '@idraw/util'; +import { rotateElement, calcViewElementSize, enhanceFontFamliy } from '@idraw/util'; import { is, isColorStr, getDefaultElementDetailConfig } from '@idraw/util'; import { drawBox } from './box'; const detailConfig = getDefaultElementDetailConfig(); +// TODO +function isTextWidthWithinErrorRange(w0: number, w1: number, scale: number): boolean { + if (scale < 0.5) { + if (w0 < w1 && (w0 - w1) / w0 > -0.15) { + return true; + } + } + return w0 >= w1; +} + export function drawText(ctx: ViewContext2D, elem: Element<'text'>, opts: RendererDrawElementOptions) { const { viewScaleInfo, viewSizeInfo, parentOpacity } = opts; const { x, y, w, h, angle } = calcViewElementSize(elem, { viewScaleInfo }) || elem; @@ -24,6 +34,10 @@ export function drawText(ctx: ViewContext2D, elem: Element<'text'>, opts: Render const originFontSize = detail.fontSize || detailConfig.fontSize; const fontSize = originFontSize * viewScaleInfo.scale; + if (fontSize < 2) { + return; + } + const originLineHeight = detail.lineHeight || originFontSize; const lineHeight = originLineHeight * viewScaleInfo.scale; @@ -32,7 +46,7 @@ export function drawText(ctx: ViewContext2D, elem: Element<'text'>, opts: Render ctx.$setFont({ fontWeight: detail.fontWeight, fontSize: fontSize, - fontFamily: detail.fontFamily + fontFamily: enhanceFontFamliy(detail.fontFamily) }); let detailText = detail.text.replace(/\r\n/gi, '\n'); if (detail.textTransform === 'lowercase') { @@ -46,30 +60,49 @@ export function drawText(ctx: ViewContext2D, elem: Element<'text'>, opts: Render const lines: { text: string; width: number }[] = []; let lineNum = 0; - detailTextList.forEach((tempText: string, idx: number) => { + detailTextList.forEach((itemText: string, idx: number) => { if (detail.minInlineSize === 'maxContent') { lines.push({ - text: tempText, - width: ctx.$undoPixelRatio(ctx.measureText(tempText).width) + text: itemText, + width: ctx.$undoPixelRatio(ctx.measureText(itemText).width) }); } else { let lineText = ''; - if (tempText.length > 0) { - for (let i = 0; i < tempText.length; i++) { - if (ctx.measureText(lineText + (tempText[i] || '')).width <= ctx.$doPixelRatio(w)) { - lineText += tempText[i] || ''; + let splitStr = ''; + let tempStrList: string[] = itemText.split(splitStr); + if (detail.wordBreak === 'normal') { + const splitStr = ' '; + const wordList = itemText.split(splitStr); + tempStrList = []; + wordList.forEach((word: string, idx: number) => { + tempStrList.push(word); + if (idx < wordList.length - 1) { + tempStrList.push(splitStr); + } + }); + } + + if (tempStrList.length === 1 && detail.overflow === 'visible') { + lines.push({ + text: tempStrList[0], + width: ctx.$undoPixelRatio(ctx.measureText(tempStrList[0]).width) + }); + } else if (tempStrList.length > 0) { + for (let i = 0; i < tempStrList.length; i++) { + if (isTextWidthWithinErrorRange(ctx.$doPixelRatio(w), ctx.measureText(lineText + tempStrList[i]).width, viewScaleInfo.scale)) { + lineText += tempStrList[i] || ''; } else { lines.push({ text: lineText, width: ctx.$undoPixelRatio(ctx.measureText(lineText).width) }); - lineText = tempText[i] || ''; + lineText = tempStrList[i] || ''; lineNum++; } - if ((lineNum + 1) * fontHeight > h) { + if ((lineNum + 1) * fontHeight > h && detail.overflow === 'hidden') { break; } - if (tempText.length - 1 === i) { + if (tempStrList.length - 1 === i) { if ((lineNum + 1) * fontHeight <= h) { lines.push({ text: lineText, @@ -92,6 +125,10 @@ export function drawText(ctx: ViewContext2D, elem: Element<'text'>, opts: Render }); let startY = 0; + let eachLineStartY = 0; + if (fontHeight > fontSize) { + eachLineStartY = (fontHeight - fontSize) / 2; + } if (lines.length * fontHeight < h) { if (elem.detail.verticalAlign === 'top') { startY = 0; @@ -125,7 +162,7 @@ export function drawText(ctx: ViewContext2D, elem: Element<'text'>, opts: Render } else if (detail.textAlign === 'right') { _x = x + (w - line.width); } - ctx.fillText(line.text, _x, _y + fontHeight * i); + ctx.fillText(line.text, _x, _y + fontHeight * i + eachLineStartY); }); } diff --git a/packages/renderer/src/index.ts b/packages/renderer/src/index.ts index a7d8a7c..aeb28a1 100644 --- a/packages/renderer/src/index.ts +++ b/packages/renderer/src/index.ts @@ -1,6 +1,6 @@ import { EventEmitter } from '@idraw/util'; import type { DataLayout, LoadItemMap } from '@idraw/types'; -import { drawElementList, drawLayout } from './draw/index'; +import { drawElementList, drawLayout, drawGlobalBackground } from './draw/index'; import { Loader } from './loader'; import type { Data, BoardRenderer, RendererOptions, RendererEventMap, RendererDrawOptions } from '@idraw/types'; @@ -45,7 +45,7 @@ export class Renderer extends EventEmitter implements BoardRen drawData(data: Data, opts: RendererDrawOptions) { const loader = this.#loader; - const { calculator } = this.#opts; + const { calculator, sharer } = this.#opts; const viewContext = this.#opts.viewContext; viewContext.clearRect(0, 0, viewContext.canvas.width, viewContext.canvas.height); const parentElementSize = { @@ -69,8 +69,10 @@ export class Renderer extends EventEmitter implements BoardRen parentElementSize, elementAssets: data.assets, parentOpacity: 1, + overrideElementMap: sharer?.getActiveOverrideElemenentMap(), ...opts }; + drawGlobalBackground(viewContext, data.global, drawOpts); if (data.layout) { drawLayout(viewContext, data.layout as DataLayout, drawOpts, () => { drawElementList(viewContext, data, drawOpts); diff --git a/packages/types/src/lib/config.ts b/packages/types/src/lib/config.ts index 70a15ac..fb02734 100644 --- a/packages/types/src/lib/config.ts +++ b/packages/types/src/lib/config.ts @@ -1,5 +1,5 @@ import type { ElementBaseDetail, ElementTextDetail, ElementGroupDetail } from './element'; export type DefaultElementDetailConfig = Required> & - Required> & + Required> & Required>; diff --git a/packages/types/src/lib/data.ts b/packages/types/src/lib/data.ts index 8208bdf..73f7580 100644 --- a/packages/types/src/lib/data.ts +++ b/packages/types/src/lib/data.ts @@ -1,4 +1,4 @@ -import type { Element, ElementType, ElementAssets, ElementSize, ElementGroupDetail } from './element'; +import type { Element, ElementType, ElementAssets, ElementSize, ElementGroupDetail, ElementGlobalDetail } from './element'; export type DataLayout = Pick & { detail: Pick< @@ -16,11 +16,13 @@ export type DataLayout = Pick & { disabledBottomRight?: boolean; }; }; -export interface Data = Record> { + +export type Data = Record> = { elements: Element[]; assets?: ElementAssets; layout?: DataLayout; -} + global?: ElementGlobalDetail; +}; export type Matrix = [ number, diff --git a/packages/types/src/lib/element.ts b/packages/types/src/lib/element.ts index 33295e6..e698e85 100644 --- a/packages/types/src/lib/element.ts +++ b/packages/types/src/lib/element.ts @@ -82,6 +82,8 @@ export interface ElementBaseDetail { background?: string | LinearGradientColor | RadialGradientColor; opacity?: number; clipPath?: ElementClipPath; + clipPathStrokeWidth?: number; + clipPathStrokeColor?: string; } // interface ElementRectDetail extends ElementBaseDetail { @@ -106,6 +108,8 @@ export interface ElementTextDetail extends ElementBaseDetail { textShadowBlur?: number; minInlineSize?: 'maxContent' | 'auto'; textTransform?: 'none' | 'uppercase' | 'lowercase'; + wordBreak?: 'break-all' | 'normal'; // default: 'normal' + overflow?: 'hidden' | 'visible'; // default: 'hidden' } export interface ElementCircleDetail extends ElementBaseDetail { @@ -174,6 +178,10 @@ export interface ElementOperations { deepResize?: boolean; } +export interface ElementGlobalDetail { + background?: string; +} + export interface Element = Record> extends ElementSize { uuid: string; name?: string; @@ -181,6 +189,7 @@ export interface Element[]; diff --git a/packages/types/src/lib/renderer.ts b/packages/types/src/lib/renderer.ts index eea1cd5..134b0be 100644 --- a/packages/types/src/lib/renderer.ts +++ b/packages/types/src/lib/renderer.ts @@ -1,7 +1,7 @@ import type { ViewScaleInfo, ViewCalculator, ViewSizeInfo } from './view'; import type { Element, ElementSize, ElementAssets } from './element'; import type { LoaderEventMap, LoadElementType, LoadContent, LoadItemMap } from './loader'; -import type { UtilEventEmitter } from './util'; +import type { UtilEventEmitter, RecursivePartial } from './util'; import type { StoreSharer } from './store'; import { ViewContext2D } from '@idraw/types'; @@ -43,4 +43,5 @@ export interface RendererDrawElementOptions extends RendererDrawOptions { parentElementSize: ElementSize; elementAssets?: ElementAssets; parentOpacity: number; + overrideElementMap?: Record> | null; } diff --git a/packages/types/src/lib/store.ts b/packages/types/src/lib/store.ts index 46c90b1..53c702f 100644 --- a/packages/types/src/lib/store.ts +++ b/packages/types/src/lib/store.ts @@ -4,11 +4,13 @@ import { ViewScaleInfo, ViewSizeInfo } from './view'; +import { Element } from './element'; +import { RecursivePartial } from './util'; export type ActiveStore = ViewSizeInfo & ViewScaleInfo & { data: Data | null; - // selectedViewRectVertexes: ViewRectVertexes | null; + overrideElementMap: Record> | null; }; export interface StoreSharer = any> { @@ -23,4 +25,6 @@ export interface StoreSharer = any> { setActiveViewScaleInfo(viewScaleInfo: ViewScaleInfo): void; setActiveViewSizeInfo(size: ViewSizeInfo): void; getActiveViewSizeInfo(): ViewSizeInfo; + setActiveOverrideElemenentMap(map: Record> | null): void; + getActiveOverrideElemenentMap(): Record> | null; } diff --git a/packages/util/src/index.ts b/packages/util/src/index.ts index f29841a..da5795e 100644 --- a/packages/util/src/index.ts +++ b/packages/util/src/index.ts @@ -87,3 +87,4 @@ export { deepResizeGroupElement } from './lib/resize-element'; export { calcViewCenterContent, calcViewCenter } from './lib/view-content'; export { modifyElement, getModifiedElement } from './lib/modify'; // export { ModifyRecorder } from './lib/modify-recorder'; +export { enhanceFontFamliy } from './lib/text'; diff --git a/packages/util/src/lib/config.ts b/packages/util/src/lib/config.ts index cf2b386..0da1f7a 100644 --- a/packages/util/src/lib/config.ts +++ b/packages/util/src/lib/config.ts @@ -31,6 +31,8 @@ export function getDefaultElementDetailConfig(): DefaultElementDetailConfig { // lineHeight: 20, fontFamily: 'sans-serif', fontWeight: 400, + minInlineSize: 'auto', + wordBreak: 'break-all', overflow: 'hidden' }; return config; diff --git a/packages/util/src/lib/text.ts b/packages/util/src/lib/text.ts new file mode 100644 index 0000000..a4caf37 --- /dev/null +++ b/packages/util/src/lib/text.ts @@ -0,0 +1,5 @@ +const baseFontFamilyList = ['-apple-system', '"system-ui"', ' "Segoe UI"', ' Roboto', '"Helvetica Neue"', 'Arial', '"Noto Sans"', ' sans-serif']; + +export function enhanceFontFamliy(fontFamily?: string): string { + return [fontFamily, ...baseFontFamilyList].join(', '); +}