feat: add image loader

This commit is contained in:
chenshenhai 2021-05-31 23:38:32 +08:00
parent b7dda1acfc
commit 890afc3687
14 changed files with 335 additions and 10 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 222 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 142 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

View 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;

View file

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

View file

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

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

View 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!')
}
}

View file

@ -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]);

View file

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