lazy-widgets
Version:
Typescript retained mode GUI for the HTML canvas API
166 lines • 6.1 kB
JavaScript
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