feat: improve image render

This commit is contained in:
chenshenhai 2023-11-12 21:37:11 +08:00
parent 14b8436ed3
commit 166c3e865d
12 changed files with 270 additions and 93 deletions

View file

@ -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;
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 content
ctx.beginPath();
const fillStyle = createColorStyle(ctx, background, {
viewElementSize: { x, y, w, h },
drawBoxShadow(ctx, viewElem, {
viewScaleInfo,
opacity: ctx.globalAlpha
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;
}
// 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
});
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;
});
}

View file

@ -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 }, () => {

View file

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

View file

@ -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<LoaderEventMap> implements RendererLoader {
private _loadFuncMap: Record<LoadElementType | string, LoadFunc<LoadElementType, LoadContent>> = {};
private _currentLoadItemMap: LoadItemMap = {};
@ -76,37 +94,38 @@ export class Loader extends EventEmitter<LoaderEventMap> 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<LoadElementType>, 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<LoaderEventMap> implements RendererLoad
}
private _isExistingErrorStorage(element: Element<LoadElementType>) {
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<LoaderEventMap> 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<LoadElementType>): LoadContent | null {
const assetId = getAssetIdFromElement(element);
return this._storageLoadItemMap?.[assetId]?.content || null;
}
}

View file

@ -1,3 +1,4 @@
import type { ElementBaseDetail } from './element';
import type { ElementBaseDetail, ElementTextDetail } from './element';
export type DefaultElementDetailConfig = Required<Omit<ElementBaseDetail, 'clipPath' | 'background'>>;
export type DefaultElementDetailConfig = Required<Omit<ElementBaseDetail, 'clipPath' | 'background'>> &
Required<Pick<ElementTextDetail, 'color' | 'textAlign' | 'verticalAlign' | 'fontSize' | 'fontFamily' | 'fontWeight' | 'lineHeight'>>;

View file

@ -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<ElementType>[];
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;

View file

@ -21,7 +21,7 @@ export interface RendererEventMap {
export interface RendererLoader extends UtilEventEmitter<LoaderEventMap> {
// load(element: Element<LoadElementType>): void;
load(element: Element<LoadElementType>, assets: ElementAssets): void;
getContent(uuid: string): LoadContent | null;
getContent(element: Element<LoadElementType>): LoadContent | null;
}
export interface RendererDrawOptions {

View file

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

View file

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

View file

@ -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<T extends Element<ElementType> = Element<ElementType>>(ori
return originElem;
}
export function updateElementInList(
uuid: string,
updateContent: RecursivePartial<Element<ElementType>>,
elements: Element<ElementType>[]
): Element<ElementType>[] {
export function updateElementInList(uuid: string, updateContent: RecursivePartial<Element<ElementType>>, 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<T extends Element<LoadElementType>>(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<T extends Element<LoadElementType>>(
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);
}

View file

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

View file

@ -1,5 +1,3 @@
import { createOffscreenContext2D } from './canvas';
export function compressImage(src: string, opts?: { radio?: number; type?: 'image/jpeg' | 'image/png' }): Promise<string> {
let radio = 0.5;
const type = opts?.type || 'image/png';