mirror of
https://github.com/idrawjs/idraw
synced 2026-05-24 10:08:34 +00:00
feat: add image loader
This commit is contained in:
parent
b7dda1acfc
commit
890afc3687
14 changed files with 335 additions and 10 deletions
BIN
packages/core/example/images/building-001.png
Normal file
BIN
packages/core/example/images/building-001.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 17 KiB |
BIN
packages/core/example/images/building-002.png
Normal file
BIN
packages/core/example/images/building-002.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
BIN
packages/core/example/images/building-003.png
Normal file
BIN
packages/core/example/images/building-003.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 21 KiB |
BIN
packages/core/example/images/chart.png
Normal file
BIN
packages/core/example/images/chart.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 222 KiB |
BIN
packages/core/example/images/computer.png
Normal file
BIN
packages/core/example/images/computer.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 142 KiB |
BIN
packages/core/example/images/phone.png
Normal file
BIN
packages/core/example/images/phone.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 124 KiB |
24
packages/core/example/lib/data-image.js
Normal file
24
packages/core/example/lib/data-image.js
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
|
||||
const data = {
|
||||
// bgColor: '#ffffff',
|
||||
elements: [
|
||||
{
|
||||
uuid: '"e93bb349-aa7c-dab6-fa7f-7f2cf809e283',
|
||||
name: 'image-001',
|
||||
x: 10,
|
||||
y: 10,
|
||||
w: 200,
|
||||
h: 100,
|
||||
type: 'image',
|
||||
// angle: 30,
|
||||
// angle: 0,
|
||||
desc: {
|
||||
src: './images/computer.png'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
|
||||
export default data;
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
import data from './lib/data.js';
|
||||
// import data from './lib/data-rect.js';
|
||||
import data from './lib/data-image.js';
|
||||
import { doScale } from './lib/scale.js';
|
||||
import { doScroll } from './lib/scroll.js';
|
||||
import { doElemens } from './lib/element.js';
|
||||
|
|
|
|||
|
|
@ -24,6 +24,9 @@ export function drawContext(ctx: TypeContext, data: TypeData, config: TypeHelper
|
|||
case 'rect': {
|
||||
drawRect<'rect'>(ctx, ele as TypeElement<'rect'>);
|
||||
}
|
||||
case 'image': {
|
||||
drawImage<'image'>(ctx, ele as TypeElement<'image'>);
|
||||
}
|
||||
default: {
|
||||
// nothing
|
||||
}
|
||||
|
|
@ -42,6 +45,14 @@ function drawRect<T extends keyof TypeElemDesc>(ctx: TypeContext, ele: TypeEleme
|
|||
});
|
||||
}
|
||||
|
||||
function drawImage<T extends keyof TypeElemDesc>(ctx: TypeContext, ele: TypeElement<T>) {
|
||||
const desc = ele.desc as TypeElemDesc['rect'];
|
||||
rotateElement(ctx, ele, () => {
|
||||
ctx.setFillStyle(desc.color);
|
||||
ctx.fillRect(ele.x, ele.y, ele.w, ele.h);
|
||||
});
|
||||
}
|
||||
|
||||
function drawBgColor(ctx: TypeContext, color: string) {
|
||||
const size = ctx.getSize();
|
||||
ctx.setFillStyle(color);
|
||||
|
|
|
|||
81
packages/core/src/lib/loader-event.ts
Normal file
81
packages/core/src/lib/loader-event.ts
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
|
||||
|
||||
export type TypeLoadData = {
|
||||
[uuid: string]: {
|
||||
type: 'image' | 'svg',
|
||||
status: 'null' | 'loaded' | 'fail',
|
||||
content: null | HTMLImageElement | HTMLCanvasElement,
|
||||
source: string,
|
||||
error?: any,
|
||||
}
|
||||
}
|
||||
|
||||
export type TypeLoaderEventArgMap = {
|
||||
'complete': undefined;
|
||||
'load': TypeLoadData[string];
|
||||
'error': TypeLoadData[string];
|
||||
}
|
||||
|
||||
export interface TypeLoaderEvent {
|
||||
on<T extends keyof TypeLoaderEventArgMap >(key: T, callback: (p: TypeLoaderEventArgMap[T]) => void): void
|
||||
off<T extends keyof TypeLoaderEventArgMap >(key: T, callback: (p: TypeLoaderEventArgMap[T]) => void): void
|
||||
trigger<T extends keyof TypeLoaderEventArgMap >(key: T, p: TypeLoaderEventArgMap[T]): void
|
||||
}
|
||||
|
||||
|
||||
export class LoaderEvent implements TypeLoaderEvent {
|
||||
|
||||
private _listeners: Map<string, ((p: any) => void)[]>;
|
||||
|
||||
constructor() {
|
||||
this._listeners = new Map();
|
||||
}
|
||||
|
||||
on<T extends keyof TypeLoaderEventArgMap >(eventKey: T, callback: (p: TypeLoaderEventArgMap[T]) => void) {
|
||||
if (this._listeners.has(eventKey)) {
|
||||
const callbacks = this._listeners.get(eventKey);
|
||||
callbacks?.push(callback);
|
||||
this._listeners.set(eventKey, callbacks || []);
|
||||
} else {
|
||||
this._listeners.set(eventKey, [callback]);
|
||||
}
|
||||
}
|
||||
|
||||
off<T extends keyof TypeLoaderEventArgMap >(eventKey: T, callback: (p: TypeLoaderEventArgMap[T]) => void) {
|
||||
if (this._listeners.has(eventKey)) {
|
||||
const callbacks = this._listeners.get(eventKey);
|
||||
if (Array.isArray(callbacks)) {
|
||||
for (let i = 0; i < callbacks?.length; i++) {
|
||||
if (callbacks[i] === callback) {
|
||||
callbacks.splice(i, 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
this._listeners.set(eventKey, callbacks || []);
|
||||
}
|
||||
}
|
||||
|
||||
trigger<T extends keyof TypeLoaderEventArgMap >(eventKey: T, arg: TypeLoaderEventArgMap[T]) {
|
||||
const callbacks = this._listeners.get(eventKey);
|
||||
if (Array.isArray(callbacks)) {
|
||||
callbacks.forEach((cb) => {
|
||||
cb(arg);
|
||||
});
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
has<T extends keyof TypeLoaderEventArgMap> (name: string) {
|
||||
if (this._listeners.has(name)) {
|
||||
const list: ((p: TypeLoaderEventArgMap[T]) => void)[] | undefined = this._listeners.get(name);
|
||||
if (Array.isArray(list) && list.length > 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
182
packages/core/src/lib/loader.ts
Normal file
182
packages/core/src/lib/loader.ts
Normal file
|
|
@ -0,0 +1,182 @@
|
|||
import { TypeData, TypeElement, TypeElemDesc } from '@idraw/types';
|
||||
import util from '@idraw/util';
|
||||
import { LoaderEvent, TypeLoadData, TypeLoaderEventArgMap } from './loader-event';
|
||||
|
||||
const { loadImage } = util.loader;
|
||||
|
||||
type Options = {
|
||||
maxParallelNum: number
|
||||
}
|
||||
|
||||
enum LoaderStatus {
|
||||
FREE = 'free',
|
||||
LOADING = 'loading',
|
||||
COMPLETE = 'complete',
|
||||
}
|
||||
|
||||
export default class Loader {
|
||||
|
||||
private _opts: Options;
|
||||
private _event: LoaderEvent;
|
||||
private _loadData: TypeLoadData = {};
|
||||
private _uuidQueue: string[] = [];
|
||||
private _status: LoaderStatus = LoaderStatus.FREE
|
||||
|
||||
constructor(opts: Options) {
|
||||
this._opts = opts;
|
||||
this._event = new LoaderEvent();
|
||||
}
|
||||
|
||||
load(data: TypeData): void {
|
||||
const [uuidQueue, loadData] = this._resetLoadData(data);
|
||||
this._uuidQueue = uuidQueue;
|
||||
this._loadData = loadData;
|
||||
if (this._status === LoaderStatus.FREE) {
|
||||
this._loadTask();
|
||||
}
|
||||
}
|
||||
|
||||
on<T extends keyof TypeLoaderEventArgMap>(
|
||||
name: T,
|
||||
callback: (arg: TypeLoaderEventArgMap[T]
|
||||
) => void) {
|
||||
this._event.on(name, callback);
|
||||
}
|
||||
|
||||
off<T extends keyof TypeLoaderEventArgMap>(
|
||||
name: T,
|
||||
callback: (arg: TypeLoaderEventArgMap[T]
|
||||
) => void) {
|
||||
this._event.off(name, callback);
|
||||
}
|
||||
|
||||
isComplete() {
|
||||
return this._status === LoaderStatus.COMPLETE;
|
||||
}
|
||||
|
||||
private _resetLoadData(data: TypeData): [string[], TypeLoadData] {
|
||||
const loadData: TypeLoadData = this._loadData;
|
||||
const uuidQueue: string[] = [];
|
||||
|
||||
// add new load-data
|
||||
for (let i = data.elements.length - 1; i >= 0; i --) {
|
||||
const elem = data.elements[i];
|
||||
if (['image', 'svg'].includes(elem.type) && !loadData[elem.uuid]) {
|
||||
loadData[elem.uuid] = this._createEmptyLoadItem(elem);
|
||||
uuidQueue.push(elem.uuid);
|
||||
}
|
||||
}
|
||||
|
||||
// clear unuse load-data
|
||||
const uuids = Object.keys(loadData);
|
||||
data.elements.forEach((elem) => {
|
||||
if (uuids.includes(elem.uuid) !== true) {
|
||||
delete loadData[elem.uuid];
|
||||
}
|
||||
});
|
||||
return [uuidQueue, loadData];
|
||||
}
|
||||
|
||||
private _createEmptyLoadItem(elem: TypeElement<keyof TypeElemDesc>): TypeLoadData[string] {
|
||||
let source = '';
|
||||
let type: TypeLoadData[string]['type'] = 'image';
|
||||
if (elem.type === 'image') {
|
||||
const _elem = elem as TypeElement<'image'>;
|
||||
source = _elem.desc.src || '';
|
||||
} else if (elem.type === 'svg') {
|
||||
const _elem = elem as TypeElement<'svg'>;
|
||||
source = _elem.desc.svg || '';
|
||||
}
|
||||
return {
|
||||
type: type,
|
||||
status: 'null',
|
||||
content: null,
|
||||
source,
|
||||
}
|
||||
}
|
||||
|
||||
private _loadTask() {
|
||||
if (this._status === LoaderStatus.LOADING) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._uuidQueue.length === 0) {
|
||||
this._status = LoaderStatus.COMPLETE;
|
||||
this._event.trigger('complete', undefined);
|
||||
return;
|
||||
}
|
||||
|
||||
const { maxParallelNum } = this._opts;
|
||||
const uuids = this._uuidQueue.splice(0, maxParallelNum);
|
||||
const uuidMap: {[uuid: string]: number} = {};
|
||||
|
||||
uuids.forEach((url, i) => {
|
||||
uuidMap[url] = i;
|
||||
});
|
||||
const loadUUIDList: string[] = [];
|
||||
const _loadAction = () => {
|
||||
|
||||
if (loadUUIDList.length >= maxParallelNum) {
|
||||
return false;
|
||||
}
|
||||
if (uuids.length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
for (let i = loadUUIDList.length; i < maxParallelNum; i++) {
|
||||
const uuid = uuids.shift();
|
||||
if (uuid === undefined) {
|
||||
break
|
||||
}
|
||||
loadUUIDList.push(uuid);
|
||||
|
||||
this._loadElementSource(this._loadData[uuid]).then((image) => {
|
||||
loadUUIDList.splice(loadUUIDList.indexOf(uuid), 1);
|
||||
const status = _loadAction();
|
||||
this._loadData[uuid].status = 'loaded';
|
||||
this._loadData[uuid].content = image;
|
||||
if (loadUUIDList.length === 0 && uuids.length === 0 && status === true) {
|
||||
this._status = LoaderStatus.FREE;
|
||||
this._loadTask();
|
||||
}
|
||||
this._event.trigger('load', {
|
||||
type: this._loadData[uuid].type,
|
||||
status: this._loadData[uuid].status,
|
||||
content: this._loadData[uuid].content,
|
||||
source: this._loadData[uuid].source,
|
||||
});
|
||||
}).catch((err) => {
|
||||
loadUUIDList.splice(loadUUIDList.indexOf(uuid), 1);
|
||||
const status = _loadAction();
|
||||
this._loadData[uuid].status = 'fail';
|
||||
this._loadData[uuid].error = err;
|
||||
if (loadUUIDList.length === 0 && uuids.length === 0 && status === true) {
|
||||
this._status = LoaderStatus.FREE;
|
||||
this._loadTask();
|
||||
}
|
||||
this._event.trigger('error', {
|
||||
type: this._loadData[uuid].type,
|
||||
status: this._loadData[uuid].status,
|
||||
content: this._loadData[uuid].content,
|
||||
source: this._loadData[uuid].source,
|
||||
})
|
||||
})
|
||||
|
||||
}
|
||||
return false;
|
||||
}
|
||||
_loadAction();
|
||||
}
|
||||
|
||||
private async _loadElementSource(
|
||||
params: TypeLoadData[string]
|
||||
): Promise<HTMLImageElement> {
|
||||
if (params.type === 'image') {
|
||||
const image = await loadImage(params.source);
|
||||
return image;
|
||||
}
|
||||
throw Error('Element\'s source is not support!')
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -2,6 +2,7 @@ import { TypeData, TypeHelperConfig, } from '@idraw/types';
|
|||
import util from '@idraw/util';
|
||||
import Board from '@idraw/board';
|
||||
import { drawContext } from './draw';
|
||||
import Loader from './loader';
|
||||
|
||||
const { requestAnimationFrame } = window;
|
||||
const { deepClone } = util.data;
|
||||
|
|
@ -17,28 +18,46 @@ export class Renderer {
|
|||
private _queue: QueueItem[] = [];
|
||||
private _board: Board;
|
||||
private _status: DrawStatus = DrawStatus.FREE;
|
||||
private _loader: Loader;
|
||||
|
||||
constructor(board: Board) {
|
||||
this._board = board;
|
||||
this._loader = new Loader({ maxParallelNum: 6 });
|
||||
// TODO
|
||||
this._loader.on('load', (res) => {
|
||||
console.log('load: ', res);
|
||||
});
|
||||
this._loader.on('error', (res) => {
|
||||
console.log('error: ', res);
|
||||
});
|
||||
this._loader.on('complete', (res) => {
|
||||
console.log('complete: ', res);
|
||||
})
|
||||
}
|
||||
|
||||
render(data: TypeData, helper: TypeHelperConfig): void {
|
||||
const _data: QueueItem = deepClone({ data, helper }) as QueueItem;
|
||||
this._queue.push(_data);
|
||||
if (this._status === DrawStatus.FREE) {
|
||||
if (this._status !== DrawStatus.DRAWING) {
|
||||
this._status = DrawStatus.DRAWING;
|
||||
this._drawFrame();
|
||||
this._loader.load(data);
|
||||
}
|
||||
}
|
||||
|
||||
private _drawFrame() {
|
||||
requestAnimationFrame(() => {
|
||||
const item: QueueItem | undefined = this._queue.shift();
|
||||
if (item) {
|
||||
let item: QueueItem | undefined = this._queue[0];
|
||||
if (this._queue.length > 1) {
|
||||
item = this._queue.shift();
|
||||
}
|
||||
if (this._loader.isComplete() !== true) {
|
||||
this._drawFrame();
|
||||
} else if (item) {
|
||||
drawContext(this._board.getContext(), item.data, item.helper);
|
||||
this._board.draw();
|
||||
this._drawFrame();
|
||||
this._retainQueueOneItem();
|
||||
this._drawFrame();
|
||||
} else {
|
||||
this._status = DrawStatus.FREE
|
||||
}
|
||||
|
|
@ -46,7 +65,7 @@ export class Renderer {
|
|||
}
|
||||
|
||||
private _retainQueueOneItem() {
|
||||
if (this._queue.length === 0) {
|
||||
if (this._queue.length <= 1) {
|
||||
return;
|
||||
}
|
||||
const lastOne = deepClone(this._queue[this._queue.length - 1]);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { TypePaintData } from './paint';
|
||||
// import { TypePaintData } from './paint';
|
||||
|
||||
type TypeElement<T extends keyof TypeElemDesc> = {
|
||||
name?: string;
|
||||
|
|
@ -17,7 +17,8 @@ type TypeElemDesc = {
|
|||
rect: TypeElemDescRect,
|
||||
circle: TypeElemDescCircle,
|
||||
image: TypeElemDescImage,
|
||||
paint: TypeElemDescPaint
|
||||
svg: TypeElemDescSVG,
|
||||
// paint: TypeElemDescPaint,
|
||||
}
|
||||
|
||||
type TypeElemDescRect = {
|
||||
|
|
@ -36,15 +37,21 @@ type TypeElemDescCircle = {
|
|||
}
|
||||
|
||||
type TypeElemDescImage = {
|
||||
src: number;
|
||||
src: string;
|
||||
}
|
||||
|
||||
type TypeElemDescPaint = TypePaintData
|
||||
type TypeElemDescSVG = {
|
||||
svg: string;
|
||||
}
|
||||
|
||||
// type TypeElemDescPaint = TypePaintData
|
||||
|
||||
export {
|
||||
TypeElemDescText,
|
||||
TypeElemDescRect,
|
||||
TypeElemDescCircle,
|
||||
TypeElemDescImage,
|
||||
TypeElemDescSVG,
|
||||
TypeElemDesc,
|
||||
TypeElement,
|
||||
};
|
||||
Loading…
Reference in a new issue