lazy-widgets
Version:
Typescript retained mode GUI for the HTML canvas API
240 lines • 10 kB
JavaScript
import { Msg } from '../core/Strings.js';
import { BackingMediaEventType } from './BackingMediaEventType.js';
import { getBackingMediaSourceType } from './BackingMediaSource.js';
import { BackingMediaSourceType } from './BackingMediaSourceType.js';
import { incrementUint31 } from './incrementUint31.js';
import { Notifier } from './Notifier.js';
// TODO move to a unified media source system, instead of using helper wrappers.
// in the future, media won't even be in the main thread, so we can't use
// BackingMediaSource directly in widgets
export class BackingMediaWrapper extends Notifier {
constructor(source) {
super();
this.source = source;
this.rvf = 0;
this._presentationHash = 0;
this.videoLoaded = false;
this.dispatchResized = () => {
this.dispatchEvent(BackingMediaEventType.Resized);
};
this.dispatchDirty = () => {
this._presentationHash = incrementUint31(this._presentationHash);
this.dispatchEvent(BackingMediaEventType.Dirty);
};
this.dispatchLoaded = () => {
this.dispatchEvent(BackingMediaEventType.Loaded);
};
this.onImageLoaded = () => {
this.dispatchResized();
this.dispatchLoaded();
this.dispatchDirty();
};
this.onReadyStateChanged = () => {
const video = this.source;
if (this.videoLoaded) {
if (video.readyState < 2) {
this.videoLoaded = false;
}
}
else if (video.readyState >= 2) {
this.videoLoaded = true;
this.dispatchLoaded();
}
};
this.dispatchDirtyAndRVF = () => {
this.dispatchDirty();
this.rvf = this.source.requestVideoFrameCallback(this.dispatchDirtyAndRVF);
};
this.redirectEvents = (event) => {
this.dispatchEvent(event);
};
this.sourceType = getBackingMediaSourceType(source);
}
addSourceListeners() {
switch (this.sourceType) {
case BackingMediaSourceType.HTMLImageElement:
// XXX need to assume that the image changed, because the source of
// the image is not guaranteed to remain the same
this._presentationHash = incrementUint31(this._presentationHash);
// falls through
case BackingMediaSourceType.SVGImageElement:
this.source.addEventListener('load', this.onImageLoaded);
break;
case BackingMediaSourceType.HTMLVideoElement:
{
const video = this.source;
video.addEventListener('resize', this.dispatchResized);
video.addEventListener('loadeddata', this.onReadyStateChanged);
video.addEventListener('loadedmetadata', this.onReadyStateChanged);
video.addEventListener('canplay', this.onReadyStateChanged);
video.addEventListener('canplaythrough', this.onReadyStateChanged);
video.addEventListener('waiting', this.onReadyStateChanged);
this.onReadyStateChanged();
if ('requestVideoFrameCallback' in video) {
console.warn(Msg.VIDEO_API_AVAILABLE);
this.rvf = video.requestVideoFrameCallback(this.dispatchDirtyAndRVF);
}
else {
video.addEventListener('timeupdate', this.dispatchDirty);
}
}
break;
case BackingMediaSourceType.AsyncMedia:
this.source.addEventListener(this.redirectEvents);
break;
}
}
removeSourceListeners() {
switch (this.sourceType) {
case BackingMediaSourceType.HTMLImageElement:
case BackingMediaSourceType.SVGImageElement:
this.source.removeEventListener('load', this.onImageLoaded);
break;
case BackingMediaSourceType.HTMLVideoElement:
{
const video = this.source;
video.removeEventListener('resize', this.dispatchResized);
video.removeEventListener('loadeddata', this.onReadyStateChanged);
video.removeEventListener('loadedmetadata', this.onReadyStateChanged);
video.removeEventListener('canplay', this.onReadyStateChanged);
video.removeEventListener('canplaythrough', this.onReadyStateChanged);
video.removeEventListener('waiting', this.onReadyStateChanged);
if ('requestVideoFrameCallback' in video) {
video.cancelVideoFrameCallback(this.rvf);
}
else {
video.removeEventListener('timeupdate', this.dispatchDirty);
}
}
break;
case BackingMediaSourceType.AsyncMedia:
this.source.removeEventListener(this.redirectEvents);
break;
}
}
addEventListener(listener) {
super.addEventListener(listener);
if (this.listeners.size == 1) {
this.addSourceListeners();
}
}
removeEventListener(listener) {
const removed = super.removeEventListener(listener);
if (removed && this.listeners.size == 0) {
this.removeSourceListeners();
}
return removed;
}
get width() {
switch (this.sourceType) {
case BackingMediaSourceType.HTMLImageElement: {
const source = this.source;
let width = source.naturalWidth;
// HACK firefox has a naturalWidth of 0 for some SVGs. note
// that images will likely have a bad aspect ratio
if (width === 0 && source.complete) {
width = 150;
}
return width;
}
case BackingMediaSourceType.HTMLVideoElement:
return this.source.videoWidth;
case BackingMediaSourceType.SVGImageElement: {
const baseVal = this.source.width.baseVal;
if (baseVal.unitType === SVGLength.SVG_LENGTHTYPE_PX) {
return baseVal.value;
}
else {
baseVal.convertToSpecifiedUnits(SVGLength.SVG_LENGTHTYPE_PX);
return baseVal.valueInSpecifiedUnits;
}
}
case BackingMediaSourceType.VideoFrame:
return this.source.codedWidth;
default:
return this.source.width;
}
}
get height() {
switch (this.sourceType) {
case BackingMediaSourceType.HTMLImageElement: {
const source = this.source;
let height = source.naturalHeight;
// HACK firefox has a naturalHeight of 0 for some SVGs. note
// that images will likely have a bad aspect ratio
if (height === 0 && source.complete) {
height = 150;
}
return height;
}
case BackingMediaSourceType.HTMLVideoElement:
return this.source.videoHeight;
case BackingMediaSourceType.SVGImageElement: {
const baseVal = this.source.height.baseVal;
if (baseVal.unitType === SVGLength.SVG_LENGTHTYPE_PX) {
return baseVal.value;
}
else {
baseVal.convertToSpecifiedUnits(SVGLength.SVG_LENGTHTYPE_PX);
return baseVal.valueInSpecifiedUnits;
}
}
case BackingMediaSourceType.VideoFrame:
return this.source.codedHeight;
default:
return this.source.height;
}
}
get loaded() {
switch (this.sourceType) {
case BackingMediaSourceType.HTMLImageElement:
return this.source.complete;
case BackingMediaSourceType.HTMLVideoElement:
return this.source.readyState >= 2;
case BackingMediaSourceType.SVGImageElement: {
const baseVal = this.source.height.baseVal;
if (baseVal.unitType === SVGLength.SVG_LENGTHTYPE_PX) {
return baseVal.value;
}
else {
baseVal.convertToSpecifiedUnits(SVGLength.SVG_LENGTHTYPE_PX);
return baseVal.valueInSpecifiedUnits;
}
}
case BackingMediaSourceType.AsyncMedia:
return !!this.source.currentFrame;
default:
// assume anything else is already loaded, including SVGElement,
// because not all browsers implement the load event properly
return true;
}
}
get canvasImageSource() {
if (this.sourceType === BackingMediaSourceType.AsyncMedia) {
return this.source.currentFrame;
}
else {
return this.source;
}
}
get fastCanvasImageSource() {
switch (this.sourceType) {
case BackingMediaSourceType.AsyncMedia:
return this.source.currentFrame;
case BackingMediaSourceType.VideoFrame:
case BackingMediaSourceType.ImageBitmap:
return this.source;
default:
return null;
}
}
get presentationHash() {
if (this.sourceType === BackingMediaSourceType.AsyncMedia) {
return this.source.presentationHash;
}
else {
return this._presentationHash;
}
}
}
//# sourceMappingURL=BackingMediaWrapper.js.map