UNPKG

lazy-widgets

Version:

Typescript retained mode GUI for the HTML canvas API

166 lines 6.1 kB
import { AsyncMedia } from './AsyncMedia.js'; import { createBackingCanvas } from './BackingCanvas.js'; import { BackingMediaEventType } from './BackingMediaEventType.js'; import { urlToBackingMediaSource } from './BackingMediaSource.js'; import { BackingMediaSourceType } from './BackingMediaSourceType.js'; import { BackingMediaWrapper } from './BackingMediaWrapper.js'; import { incrementUint31 } from './incrementUint31.js'; export class EffectMedia extends AsyncMedia { constructor(backingMedia, options) { var _a, _b; super(); this.backingMedia = backingMedia; this._currentFrame = null; this.ctx = null; this.waiting = false; this.dirty = true; this.lastPHashBeforeRemove = 0; this._presentationHash = 0; this.onWrapperEvent = (ev) => { switch (ev) { case BackingMediaEventType.Resized: this.dispatchEvent(ev); break; case BackingMediaEventType.Dirty: case BackingMediaEventType.Loaded: this.dirty = true; this.evaluateEffect(); break; } }; this.tint = (_a = options === null || options === void 0 ? void 0 : options.tint) !== null && _a !== void 0 ? _a : 0xFFFFFF; this.resolution = (_b = options === null || options === void 0 ? void 0 : options.resolution) !== null && _b !== void 0 ? _b : 1; this.wrapper = new BackingMediaWrapper(backingMedia); } addEventListener(listener) { super.addEventListener(listener); if (this.listeners.size == 1) { if (this.lastPHashBeforeRemove !== this.wrapper.presentationHash) { this.dirty = true; } this.wrapper.addEventListener(this.onWrapperEvent); if (this.wrapper.loaded) { this.evaluateEffect(); } } } removeEventListener(listener) { const removed = super.removeEventListener(listener); if (removed && this.listeners.size == 0) { this.lastPHashBeforeRemove = this.wrapper.presentationHash; this.wrapper.removeEventListener(this.onWrapperEvent); } return removed; } static fromURL(url, options) { const [source, type] = urlToBackingMediaSource(url); if (type === BackingMediaSourceType.HTMLVideoElement) { const video = source; video.muted = true; video.loop = true; video.play(); } return new EffectMedia(source, options); } get width() { return this.wrapper.width; } get height() { return this.wrapper.height; } getScratchCtx(width, height) { if (this.ctx) { const canvas = this.ctx.canvas; if (canvas.width !== width) { canvas.width = width; } if (canvas.height !== height) { canvas.height = height; } this.ctx.globalCompositeOperation = 'source-over'; } else { const canvas = createBackingCanvas(width, height); const ctx = canvas.getContext('2d'); if (!ctx) { throw new Error('Could not create 2D offscreen context'); } this.ctx = ctx; } return this.ctx; } setCurrentFrame(currentFrame) { this._presentationHash = incrementUint31(this._presentationHash); const firstTime = this._currentFrame === null; this._currentFrame = currentFrame; if (firstTime) { this.dispatchEvent(BackingMediaEventType.Loaded); } this.dispatchEvent(BackingMediaEventType.Dirty); } handleBitmapPromise(promise) { this.waiting = true; promise.then((bitmap) => { this.setCurrentFrame(bitmap); }).catch((err) => { console.error(err); }).finally(() => { this.waiting = false; }); } evaluateEffect() { if (this.waiting || !this.dirty) { return; } this.dirty = false; // special case if there is no tint; just convert image to bitmap if (this.tint === 0xFFFFFF) { const fastSource = this.wrapper.fastCanvasImageSource; if (fastSource) { this.setCurrentFrame(fastSource); this.waiting = false; } else { const source = this.wrapper.canvasImageSource; if (source) { this.handleBitmapPromise(createImageBitmap(source)); } else { this.waiting = false; } } return; } if (!this.wrapper.loaded) { this.waiting = false; return; } const source = this.wrapper.canvasImageSource; if (!source) { this.waiting = false; return; } // get context with target size const width = Math.max(1, Math.round(this.wrapper.width * this.resolution)); const height = Math.max(1, Math.round(this.wrapper.height * this.resolution)); const ctx = this.getScratchCtx(width, height); // tint image // source: https://stackoverflow.com/a/44558286 ctx.drawImage(source, 0, 0, width, height); ctx.fillStyle = `#${this.tint.toString(16).padStart(6, '0')}`; ctx.globalCompositeOperation = 'multiply'; ctx.fillRect(0, 0, width, height); ctx.globalCompositeOperation = 'destination-in'; ctx.drawImage(source, 0, 0, width, height); // convert to bitmap this.handleBitmapPromise(createImageBitmap(ctx.canvas, 0, 0, width, height)); } get currentFrame() { this.evaluateEffect(); return this._currentFrame; } get presentationHash() { return this._presentationHash; } } //# sourceMappingURL=EffectMedia.js.map