diff --git a/packages/core/example/images/building-001.png b/packages/core/example/images/building-001.png new file mode 100644 index 0000000..4b87e78 Binary files /dev/null and b/packages/core/example/images/building-001.png differ diff --git a/packages/core/example/images/building-002.png b/packages/core/example/images/building-002.png new file mode 100644 index 0000000..c0699ff Binary files /dev/null and b/packages/core/example/images/building-002.png differ diff --git a/packages/core/example/images/building-003.png b/packages/core/example/images/building-003.png new file mode 100644 index 0000000..5391f28 Binary files /dev/null and b/packages/core/example/images/building-003.png differ diff --git a/packages/core/example/images/chart.png b/packages/core/example/images/chart.png new file mode 100644 index 0000000..67efe1f Binary files /dev/null and b/packages/core/example/images/chart.png differ diff --git a/packages/core/example/images/computer.png b/packages/core/example/images/computer.png new file mode 100644 index 0000000..01fde93 Binary files /dev/null and b/packages/core/example/images/computer.png differ diff --git a/packages/core/example/images/phone.png b/packages/core/example/images/phone.png new file mode 100644 index 0000000..4ffec7f Binary files /dev/null and b/packages/core/example/images/phone.png differ diff --git a/packages/core/example/lib/data-image.js b/packages/core/example/lib/data-image.js new file mode 100644 index 0000000..8310b2f --- /dev/null +++ b/packages/core/example/lib/data-image.js @@ -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; \ No newline at end of file diff --git a/packages/core/example/lib/data.js b/packages/core/example/lib/data-rect.js similarity index 100% rename from packages/core/example/lib/data.js rename to packages/core/example/lib/data-rect.js diff --git a/packages/core/example/main.js b/packages/core/example/main.js index 99cbca6..89f8803 100644 --- a/packages/core/example/main.js +++ b/packages/core/example/main.js @@ -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'; diff --git a/packages/core/src/lib/draw.ts b/packages/core/src/lib/draw.ts index f3809f2..a0f9e53 100644 --- a/packages/core/src/lib/draw.ts +++ b/packages/core/src/lib/draw.ts @@ -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(ctx: TypeContext, ele: TypeEleme }); } +function drawImage(ctx: TypeContext, ele: TypeElement) { + 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); diff --git a/packages/core/src/lib/loader-event.ts b/packages/core/src/lib/loader-event.ts new file mode 100644 index 0000000..918fb09 --- /dev/null +++ b/packages/core/src/lib/loader-event.ts @@ -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(key: T, callback: (p: TypeLoaderEventArgMap[T]) => void): void + off(key: T, callback: (p: TypeLoaderEventArgMap[T]) => void): void + trigger(key: T, p: TypeLoaderEventArgMap[T]): void +} + + +export class LoaderEvent implements TypeLoaderEvent { + + private _listeners: Map void)[]>; + + constructor() { + this._listeners = new Map(); + } + + on(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(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(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 (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; + } + +} \ No newline at end of file diff --git a/packages/core/src/lib/loader.ts b/packages/core/src/lib/loader.ts new file mode 100644 index 0000000..6c8d442 --- /dev/null +++ b/packages/core/src/lib/loader.ts @@ -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( + name: T, + callback: (arg: TypeLoaderEventArgMap[T] + ) => void) { + this._event.on(name, callback); + } + + off( + 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): 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 { + if (params.type === 'image') { + const image = await loadImage(params.source); + return image; + } + throw Error('Element\'s source is not support!') + } +} + + diff --git a/packages/core/src/lib/renderer.ts b/packages/core/src/lib/renderer.ts index f2aca96..0e4b159 100644 --- a/packages/core/src/lib/renderer.ts +++ b/packages/core/src/lib/renderer.ts @@ -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]); diff --git a/packages/types/src/lib/element.ts b/packages/types/src/lib/element.ts index b0ac00d..d3ce365 100644 --- a/packages/types/src/lib/element.ts +++ b/packages/types/src/lib/element.ts @@ -1,4 +1,4 @@ -import { TypePaintData } from './paint'; +// import { TypePaintData } from './paint'; type TypeElement = { 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, }; \ No newline at end of file