From 166c3e865db656ebdd5f2a494af7bf866a632319 Mon Sep 17 00:00:00 2001 From: chenshenhai Date: Sun, 12 Nov 2023 21:37:11 +0800 Subject: [PATCH] feat: improve image render --- packages/renderer/src/draw/circle.ts | 71 +++++++++++-------- packages/renderer/src/draw/image.ts | 2 +- packages/renderer/src/draw/text.ts | 14 ++-- packages/renderer/src/loader.ts | 53 +++++++++----- packages/types/src/lib/config.ts | 5 +- packages/types/src/lib/element.ts | 27 ++++--- packages/types/src/lib/renderer.ts | 2 +- packages/util/src/index.ts | 7 +- packages/util/src/lib/config.ts | 9 ++- packages/util/src/lib/element.ts | 102 ++++++++++++++++++++++++--- packages/util/src/lib/file.ts | 65 +++++++++++++++-- packages/util/src/lib/image.ts | 2 - 12 files changed, 268 insertions(+), 91 deletions(-) diff --git a/packages/renderer/src/draw/circle.ts b/packages/renderer/src/draw/circle.ts index 42432e50e..9ad40c1de 100644 --- a/packages/renderer/src/draw/circle.ts +++ b/packages/renderer/src/draw/circle.ts @@ -1,6 +1,7 @@ import type { Element, RendererDrawElementOptions, ViewContext2D } from '@idraw/types'; import { rotateElement } from '@idraw/util'; import { createColorStyle } from './color'; +import { drawBoxShadow } from './base'; export function drawCircle(ctx: ViewContext2D, elem: Element<'circle'>, opts: RendererDrawElementOptions) { const { detail, angle } = elem; @@ -8,41 +9,49 @@ export function drawCircle(ctx: ViewContext2D, elem: Element<'circle'>, opts: Re const { calculator, viewScaleInfo, viewSizeInfo } = 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); + const viewElem = { ...elem, ...{ x, y, w, h, angle } }; + rotateElement(ctx, { x, y, w, h, angle }, () => { - const a = w / 2; - const b = h / 2; - const centerX = x + a; - const centerY = y + b; + drawBoxShadow(ctx, viewElem, { + viewScaleInfo, + viewSizeInfo, + renderContent: () => { + const a = w / 2; + const b = h / 2; + 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 (elem?.detail?.opacity !== undefined && elem?.detail?.opacity >= 0) { + ctx.globalAlpha = elem.detail.opacity; + } else { + ctx.globalAlpha = 1; + } - // draw border - if (typeof borderWidth === 'number' && borderWidth > 0) { - const ba = borderWidth / 2 + a; - const bb = borderWidth / 2 + b; - ctx.beginPath(); - ctx.strokeStyle = borderColor; - ctx.lineWidth = borderWidth; - ctx.circle(centerX, centerY, ba, bb, 0, 0, 2 * Math.PI); - ctx.closePath(); - ctx.stroke(); - } + // draw border + if (typeof borderWidth === 'number' && borderWidth > 0) { + const ba = borderWidth / 2 + a; + const bb = borderWidth / 2 + b; + ctx.beginPath(); + ctx.strokeStyle = borderColor; + ctx.lineWidth = borderWidth; + ctx.circle(centerX, centerY, ba, bb, 0, 0, 2 * Math.PI); + ctx.closePath(); + ctx.stroke(); + } - // draw content - ctx.beginPath(); - const fillStyle = createColorStyle(ctx, background, { - viewElementSize: { x, y, w, h }, - viewScaleInfo, - opacity: ctx.globalAlpha + // draw content + ctx.beginPath(); + const fillStyle = createColorStyle(ctx, background, { + viewElementSize: { x, y, w, h }, + viewScaleInfo, + opacity: ctx.globalAlpha + }); + ctx.fillStyle = fillStyle; + ctx.circle(centerX, centerY, a, b, 0, 0, 2 * Math.PI); + ctx.closePath(); + ctx.fill(); + ctx.globalAlpha = 1; + } }); - ctx.fillStyle = fillStyle; - ctx.circle(centerX, centerY, a, b, 0, 0, 2 * Math.PI); - ctx.closePath(); - ctx.fill(); - ctx.globalAlpha = 1; }); } diff --git a/packages/renderer/src/draw/image.ts b/packages/renderer/src/draw/image.ts index 5531b11c0..3c7b44d68 100644 --- a/packages/renderer/src/draw/image.ts +++ b/packages/renderer/src/draw/image.ts @@ -2,7 +2,7 @@ import type { Element, RendererDrawElementOptions, ViewContext2D } from '@idraw/ import { rotateElement } from '@idraw/util'; export function drawImage(ctx: ViewContext2D, elem: Element<'image'>, opts: RendererDrawElementOptions) { - const content = opts.loader.getContent(elem.uuid); + const content = opts.loader.getContent(elem); const { calculator, viewScaleInfo, viewSizeInfo } = opts; const { x, y, w, h, angle } = calculator.elementSize(elem, viewScaleInfo, viewSizeInfo); rotateElement(ctx, { x, y, w, h, angle }, () => { diff --git a/packages/renderer/src/draw/text.ts b/packages/renderer/src/draw/text.ts index 4bbce272c..0966052ec 100644 --- a/packages/renderer/src/draw/text.ts +++ b/packages/renderer/src/draw/text.ts @@ -1,8 +1,10 @@ import type { Element, RendererDrawElementOptions, ViewContext2D } from '@idraw/types'; import { rotateElement } from '@idraw/util'; -import { is, isColorStr } from '@idraw/util'; +import { is, isColorStr, getDefaultElementDetailConfig } from '@idraw/util'; import { drawBox } from './base'; +const detailConfig = getDefaultElementDetailConfig(); + export function drawText(ctx: ViewContext2D, elem: Element<'text'>, opts: RendererDrawElementOptions) { const { calculator, viewScaleInfo, viewSizeInfo } = opts; const { x, y, w, h, angle } = calculator.elementSize(elem, viewScaleInfo, viewSizeInfo); @@ -15,17 +17,13 @@ export function drawText(ctx: ViewContext2D, elem: Element<'text'>, opts: Render viewSizeInfo, renderContent: () => { const detail: Element<'text'>['detail'] = { - ...{ - fontSize: 12, - fontFamily: 'sans-serif', - textAlign: 'center' - }, + ...detailConfig, ...elem.detail }; - const fontSize = detail.fontSize * viewScaleInfo.scale; + const fontSize = (detail.fontSize || detailConfig.fontSize) * viewScaleInfo.scale; const lineHeight = detail.lineHeight ? detail.lineHeight * viewScaleInfo.scale : fontSize; - ctx.fillStyle = elem.detail.color; + ctx.fillStyle = elem.detail.color || detailConfig.color; ctx.textBaseline = 'top'; ctx.$setFont({ fontWeight: detail.fontWeight, diff --git a/packages/renderer/src/loader.ts b/packages/renderer/src/loader.ts index 77e7b5d29..17a88041b 100644 --- a/packages/renderer/src/loader.ts +++ b/packages/renderer/src/loader.ts @@ -1,12 +1,30 @@ import type { RendererLoader, LoaderEventMap, LoadFunc, LoadContent, LoadItem, LoadElementType, Element, ElementAssets } from '@idraw/types'; -import { loadImage, loadHTML, loadSVG, EventEmitter, deepClone } from '@idraw/util'; +import { loadImage, loadHTML, loadSVG, EventEmitter, createAssetId, isAssetId, createUUID } from '@idraw/util'; interface LoadItemMap { - [uuid: string]: LoadItem; + [assetId: string]: LoadItem; } const supportElementTypes: LoadElementType[] = ['image', 'svg', 'html']; +const getAssetIdFromElement = (element: Element<'image' | 'svg' | 'html'>) => { + let source: string | null = null; + if (element.type === 'image') { + source = (element as Element<'image'>)?.detail?.src || null; + } else if (element.type === 'svg') { + source = (element as Element<'svg'>)?.detail?.svg || null; + } else if (element.type === 'html') { + source = (element as Element<'html'>)?.detail?.html || null; + } + if (typeof source === 'string' && source) { + if (isAssetId(source)) { + return source; + } + return createAssetId(source); + } + return createAssetId(`${createUUID()}-${element.uuid}-${createUUID()}-${createUUID()}`); +}; + export class Loader extends EventEmitter implements RendererLoader { private _loadFuncMap: Record> = {}; private _currentLoadItemMap: LoadItemMap = {}; @@ -76,37 +94,38 @@ export class Loader extends EventEmitter implements RendererLoad } private _emitLoad(item: LoadItem) { - const uuid = item.element.uuid; - const storageItem = this._storageLoadItemMap[uuid]; + const assetId = getAssetIdFromElement(item.element); + const storageItem = this._storageLoadItemMap[assetId]; if (storageItem) { if (storageItem.startTime < item.startTime) { - this._storageLoadItemMap[uuid] = item; + this._storageLoadItemMap[assetId] = item; this.trigger('load', { ...item, countTime: item.endTime - item.startTime }); } } else { - this._storageLoadItemMap[uuid] = item; + this._storageLoadItemMap[assetId] = item; this.trigger('load', { ...item, countTime: item.endTime - item.startTime }); } } private _emitError(item: LoadItem) { - const uuid = item.element.uuid; - const storageItem = this._storageLoadItemMap[uuid]; + const assetId = getAssetIdFromElement(item.element); + const storageItem = this._storageLoadItemMap[assetId]; if (storageItem) { if (storageItem.startTime < item.startTime) { - this._storageLoadItemMap[uuid] = item; + this._storageLoadItemMap[assetId] = item; this.trigger('error', { ...item, countTime: item.endTime - item.startTime }); } } else { - this._storageLoadItemMap[uuid] = item; + this._storageLoadItemMap[assetId] = item; this.trigger('error', { ...item, countTime: item.endTime - item.startTime }); } } private _loadResource(element: Element, assets: ElementAssets) { const item = this._createLoadItem(element); + const assetId = getAssetIdFromElement(element); - this._currentLoadItemMap[element.uuid] = item; + this._currentLoadItemMap[assetId] = item; const loadFunc = this._loadFuncMap[element.type]; if (typeof loadFunc === 'function') { item.startTime = Date.now(); @@ -128,7 +147,8 @@ export class Loader extends EventEmitter implements RendererLoad } private _isExistingErrorStorage(element: Element) { - const existItem = this._currentLoadItemMap?.[element?.uuid]; + const assetId = getAssetIdFromElement(element); + const existItem = this._currentLoadItemMap?.[assetId]; if (existItem && existItem.status === 'error' && existItem.source && existItem.source === this._getLoadElementSource(element)) { return true; } @@ -140,12 +160,13 @@ export class Loader extends EventEmitter implements RendererLoad return; } if (supportElementTypes.includes(element.type)) { - const elem = deepClone(element); - this._loadResource(elem, assets); + // const elem = deepClone(element); + this._loadResource(element, assets); } } - getContent(uuid: string): LoadContent | null { - return this._storageLoadItemMap?.[uuid]?.content || null; + getContent(element: Element): LoadContent | null { + const assetId = getAssetIdFromElement(element); + return this._storageLoadItemMap?.[assetId]?.content || null; } } diff --git a/packages/types/src/lib/config.ts b/packages/types/src/lib/config.ts index 352da87f7..575ba8dde 100644 --- a/packages/types/src/lib/config.ts +++ b/packages/types/src/lib/config.ts @@ -1,3 +1,4 @@ -import type { ElementBaseDetail } from './element'; +import type { ElementBaseDetail, ElementTextDetail } from './element'; -export type DefaultElementDetailConfig = Required>; +export type DefaultElementDetailConfig = Required> & + Required>; diff --git a/packages/types/src/lib/element.ts b/packages/types/src/lib/element.ts index d817ec5e0..8467e941c 100644 --- a/packages/types/src/lib/element.ts +++ b/packages/types/src/lib/element.ts @@ -17,7 +17,7 @@ export interface TransformMatrix { } export interface ElementAssetsItem { - type: 'svg' | 'image'; + type: 'svg' | 'image' | 'html'; value: string; } @@ -79,7 +79,6 @@ export interface ElementBaseDetail { shadowOffsetX?: number; shadowOffsetY?: number; shadowBlur?: number; - // color?: string; background?: string | LinearGradientColor | RadialGradientColor; opacity?: number; clipPath?: ElementClipPath; @@ -90,12 +89,12 @@ export interface ElementBaseDetail { // // background?: string; // } -interface ElementRectDetail extends ElementBaseDetail {} +export interface ElementRectDetail extends ElementBaseDetail {} -interface ElemenTextDetail extends ElementBaseDetail { +export interface ElementTextDetail extends ElementBaseDetail { text: string; - color: string; - fontSize: number; + color?: string; + fontSize?: number; lineHeight?: number; fontWeight?: 'bold' | string | number; fontFamily?: string; @@ -107,32 +106,32 @@ interface ElemenTextDetail extends ElementBaseDetail { textShadowBlur?: number; } -interface ElementCircleDetail extends ElementBaseDetail { +export interface ElementCircleDetail extends ElementBaseDetail { radius: number; background?: string; } -interface ElementHTMLDetail extends ElementBaseDetail { +export interface ElementHTMLDetail extends ElementBaseDetail { html: string; width?: number; height?: number; } -interface ElementImageDetail extends ElementBaseDetail { +export interface ElementImageDetail extends ElementBaseDetail { src: string; } -interface ElementSVGDetail extends ElementBaseDetail { +export interface ElementSVGDetail extends ElementBaseDetail { svg: string; } -interface ElementGroupDetail extends ElementBaseDetail { +export interface ElementGroupDetail extends ElementBaseDetail { children: Element[]; overflow?: 'hidden'; assets?: ElementAssets; } -interface ElementPathDetail extends ElementBaseDetail { +export interface ElementPathDetail extends ElementBaseDetail { // path: string; commands: SVGPathCommand[]; originX: number; @@ -145,10 +144,10 @@ interface ElementPathDetail extends ElementBaseDetail { strokeLineCap?: 'butt' | 'round' | 'square'; } -interface ElementDetailMap { +export interface ElementDetailMap { rect: ElementRectDetail; circle: ElementCircleDetail; - text: ElemenTextDetail; + text: ElementTextDetail; image: ElementImageDetail; html: ElementHTMLDetail; svg: ElementSVGDetail; diff --git a/packages/types/src/lib/renderer.ts b/packages/types/src/lib/renderer.ts index 64c51674d..1f67db0ae 100644 --- a/packages/types/src/lib/renderer.ts +++ b/packages/types/src/lib/renderer.ts @@ -21,7 +21,7 @@ export interface RendererEventMap { export interface RendererLoader extends UtilEventEmitter { // load(element: Element): void; load(element: Element, assets: ElementAssets): void; - getContent(uuid: string): LoadContent | null; + getContent(element: Element): LoadContent | null; } export interface RendererDrawOptions { diff --git a/packages/util/src/index.ts b/packages/util/src/index.ts index 4dbe4ca62..7938e72d6 100644 --- a/packages/util/src/index.ts +++ b/packages/util/src/index.ts @@ -1,5 +1,5 @@ export { delay, compose, throttle } from './lib/time'; -export { downloadImageFromCanvas } from './lib/file'; +export { downloadImageFromCanvas, parseFileToBase64, pickFile } 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'; @@ -34,7 +34,10 @@ export { findElementsFromList, updateElementInList, getGroupQueueFromList, - getElementSize + getElementSize, + mergeElementAsset, + filterElementAsset, + isResourceElement } from './lib/element'; export { checkRectIntersect } from './lib/rect'; export { diff --git a/packages/util/src/lib/config.ts b/packages/util/src/lib/config.ts index 46a1a6f43..8515c0a02 100644 --- a/packages/util/src/lib/config.ts +++ b/packages/util/src/lib/config.ts @@ -11,7 +11,14 @@ export function getDefaultElementDetailConfig(): DefaultElementDetailConfig { shadowOffsetX: 0, shadowOffsetY: 0, shadowBlur: 0, - opacity: 1 + opacity: 1, + color: '#000000', + textAlign: 'left', + verticalAlign: 'top', + fontSize: 16, + lineHeight: 20, + fontFamily: 'sans-serif', + fontWeight: 400 }; return config; } diff --git a/packages/util/src/lib/element.ts b/packages/util/src/lib/element.ts index 7ae5c1bcb..0386d5282 100644 --- a/packages/util/src/lib/element.ts +++ b/packages/util/src/lib/element.ts @@ -1,6 +1,18 @@ -import type { Data, Element, Elements, ElementType, ElementSize, ViewContextSize, ViewSizeInfo, RecursivePartial } from '@idraw/types'; +import type { + Data, + Element, + Elements, + ElementType, + ElementSize, + ViewContextSize, + ViewSizeInfo, + RecursivePartial, + ElementAssets, + ElementAssetsItem, + LoadElementType +} from '@idraw/types'; import { rotateElementVertexes } from './rotate'; -import { isAssetId } from './uuid'; +import { isAssetId, createAssetId } from './uuid'; import { istype } from './istype'; // // TODO need to be deprecated @@ -328,21 +340,19 @@ function mergeElement = Element>(ori return originElem; } -export function updateElementInList( - uuid: string, - updateContent: RecursivePartial>, - elements: Element[] -): Element[] { +export function updateElementInList(uuid: string, updateContent: RecursivePartial>, elements: Element[]): Element | null { + let targetElement: Element | null = null; for (let i = 0; i < elements.length; i++) { const elem = elements[i]; if (elem.uuid === uuid) { mergeElement(elem, updateContent); + targetElement = elem; break; } else if (elem.type === 'group') { - updateElementInList(uuid, updateContent, (elem as Element<'group'>)?.detail?.children || []); + targetElement = updateElementInList(uuid, updateContent, (elem as Element<'group'>)?.detail?.children || []); } } - return elements; + return targetElement; } export function getElementSize(elem: Element): ElementSize { @@ -350,3 +360,77 @@ export function getElementSize(elem: Element): ElementSize { const size: ElementSize = { x, y, w, h, angle }; return size; } + +export function mergeElementAsset>(element: T, assets: ElementAssets): T { + // const elem: T = { ...element, ...{ detail: { ...element.detail } } }; + const elem = element; + let assetId: string | null = null; + let assetItem: ElementAssetsItem | null = null; + if (elem.type === 'image') { + assetId = (elem as Element<'image'>).detail.src; + } else if (elem.type === 'svg') { + assetId = (elem as Element<'svg'>).detail.svg; + } else if (elem.type === 'html') { + assetId = (elem as Element<'html'>).detail.html; + } + + if (assetId && assetId?.startsWith('@assets/')) { + assetItem = assets[assetId]; + } + + if (assetItem?.type === elem.type && typeof assetItem?.value === 'string' && assetItem?.value) { + if (elem.type === 'image') { + (elem as Element<'image'>).detail.src = assetItem.value; + } else if (elem.type === 'svg') { + (elem as Element<'svg'>).detail.svg = assetItem.value; + } else if (elem.type === 'html') { + (elem as Element<'html'>).detail.html = assetItem.value; + } + } + return elem; +} + +export function filterElementAsset>( + element: T +): { + element: T; + assetId: string | null; + assetItem: ElementAssetsItem | null; +} { + let assetId: string | null = null; + let assetItem: ElementAssetsItem | null = null; + let resource: string | null = null; + + if (element.type === 'image') { + resource = (element as Element<'image'>).detail.src; + } else if (element.type === 'svg') { + resource = (element as Element<'svg'>).detail.svg; + } else if (element.type === 'html') { + resource = (element as Element<'html'>).detail.html; + } + + if (typeof resource === 'string' && !isAssetId(resource)) { + assetId = createAssetId(resource); + assetItem = { + type: element.type as LoadElementType, + value: resource + }; + if (element.type === 'image') { + (element as Element<'image'>).detail.src = assetId; + } else if (element.type === 'svg') { + (element as Element<'svg'>).detail.svg = assetId; + } else if (element.type === 'html') { + (element as Element<'html'>).detail.html = assetId; + } + } + + return { + element, + assetId, + assetItem + }; +} + +export function isResourceElement(elem: Element): boolean { + return ['image', 'svg', 'html'].includes(elem?.type); +} diff --git a/packages/util/src/lib/file.ts b/packages/util/src/lib/file.ts index 3b21d684d..81ea447ba 100644 --- a/packages/util/src/lib/file.ts +++ b/packages/util/src/lib/file.ts @@ -1,9 +1,6 @@ type ImageType = 'image/jpeg' | 'image/png'; -export function downloadImageFromCanvas( - canvas: HTMLCanvasElement, - opts: { filename: string; type: ImageType } -): void { +export function downloadImageFromCanvas(canvas: HTMLCanvasElement, opts: { filename: string; type: ImageType }): void { const { filename, type = 'image/jpeg' } = opts; const stream = canvas.toDataURL(type); const downloadLink = document.createElement('a'); @@ -13,3 +10,63 @@ export function downloadImageFromCanvas( downloadClickEvent.initEvent('click', true, false); downloadLink.dispatchEvent(downloadClickEvent); } + +export function pickFile(opts: { success: (data: { file: File }) => void; error?: (err: ErrorEvent) => void }) { + const { success, error } = opts; + let input: HTMLInputElement | null = document.createElement('input') as HTMLInputElement; + input.type = 'file'; + input.addEventListener('change', function () { + const file: File = (input as HTMLInputElement).files?.[0] as File; + success({ + file: file + }); + input = null; + }); + input.addEventListener('error', function (err) { + if (typeof error === 'function') { + error(err); + } + input = null; + }); + input.click(); +} + +export function parseFileToBase64(file: File): Promise { + return new Promise(function (resolve, reject) { + let reader: any = new FileReader(); + reader.addEventListener('load', function () { + resolve(reader.result); + reader = null; + }); + reader.addEventListener('error', function (err: Error) { + // reader.abort(); + reject(err); + reader = null; + }); + reader.addEventListener('abort', function () { + reject(new Error('abort')); + reader = null; + }); + reader.readAsDataURL(file); + }); +} + +export function parseFileToText(file: File): Promise { + return new Promise(function (resolve, reject) { + let reader: any = new FileReader(); + reader.addEventListener('load', function () { + resolve(reader.result); + reader = null; + }); + reader.addEventListener('error', function (err: Error) { + // reader.abort(); + reject(err); + reader = null; + }); + reader.addEventListener('abort', function () { + reject(new Error('abort')); + reader = null; + }); + reader.readAsText(file); + }); +} diff --git a/packages/util/src/lib/image.ts b/packages/util/src/lib/image.ts index eed958adb..7e940af16 100644 --- a/packages/util/src/lib/image.ts +++ b/packages/util/src/lib/image.ts @@ -1,5 +1,3 @@ -import { createOffscreenContext2D } from './canvas'; - export function compressImage(src: string, opts?: { radio?: number; type?: 'image/jpeg' | 'image/png' }): Promise { let radio = 0.5; const type = opts?.type || 'image/png';