Skip to content

Instantly share code, notes, and snippets.

@ZoeLeee

ZoeLeee/video Secret

Created April 13, 2022 07:57
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ZoeLeee/f4c58be0765fab64bc6821e15c82a6db to your computer and use it in GitHub Desktop.
Save ZoeLeee/f4c58be0765fab64bc6821e15c82a6db to your computer and use it in GitHub Desktop.
VideoGUi
export class Video extends Control {
private _workingCanvas: Nullable<ICanvas> = null;
private _domImage: HTMLVideoElement;
private _imageWidth: number;
private _imageHeight: number;
private _loaded = false;
private _stretch = Image2.STRETCH_FILL;
private _source: Nullable<string>;
private _autoScale = false;
private _sourceLeft = 0;
private _sourceTop = 0;
private _sourceWidth = 0;
private _sourceHeight = 0;
private _svgAttributesComputationCompleted = false;
private _isSVG = false;
private _cellWidth = 0;
private _cellHeight = 0;
private _cellId = -1;
private _keepSize = true;
private _detectPointerOnOpaqueOnly: boolean;
private _imageDataCache: {
data: Uint8ClampedArray | null;
key: string;
} = { data: null, key: "" };
private listener: Observer<Scene>;
/**
* Observable notified when the content is loaded
*/
public onImageLoadedObservable = new Observable<Video>();
/**
* Observable notified when _sourceLeft, _sourceTop, _sourceWidth and _sourceHeight are computed
*/
public onSVGAttributesComputedObservable = new Observable<Video>();
/**
* Gets a boolean indicating that the content is loaded
*/
public get isLoaded(): boolean {
return this._loaded;
}
/**
* Gets or sets a boolean indicating if pointers should only be validated on pixels with alpha > 0.
* Beware using this as this will consume more memory as the image has to be stored twice
*/
@serialize()
public get detectPointerOnOpaqueOnly(): boolean {
return this._detectPointerOnOpaqueOnly;
}
public set detectPointerOnOpaqueOnly(value: boolean) {
if (this._detectPointerOnOpaqueOnly === value) {
return;
}
this._detectPointerOnOpaqueOnly = value;
}
/**
* Gets or sets the left coordinate in the source image
*/
@serialize()
public get sourceLeft(): number {
return this._sourceLeft;
}
public set sourceLeft(value: number) {
if (this._sourceLeft === value) {
return;
}
this._sourceLeft = value;
this._markAsDirty();
}
/**
* Gets or sets the top coordinate in the source image
*/
@serialize()
public get sourceTop(): number {
return this._sourceTop;
}
public set sourceTop(value: number) {
if (this._sourceTop === value) {
return;
}
this._sourceTop = value;
this._markAsDirty();
}
/**
* Gets or sets the width to capture in the source image
*/
@serialize()
public get sourceWidth(): number {
return this._sourceWidth;
}
public set sourceWidth(value: number) {
if (this._sourceWidth === value) {
return;
}
this._sourceWidth = value;
this._markAsDirty();
}
/**
* Gets or sets the height to capture in the source image
*/
@serialize()
public get sourceHeight(): number {
return this._sourceHeight;
}
public set sourceHeight(value: number) {
if (this._sourceHeight === value) {
return;
}
this._sourceHeight = value;
this._markAsDirty();
}
/**
* Gets the image width
*/
public get imageWidth(): number {
return this._imageWidth;
}
/**
* Gets the image height
*/
public get imageHeight(): number {
return this._imageHeight;
}
/** Indicates if the format of the image is SVG */
public get isSVG(): boolean {
return this._isSVG;
}
/** Gets the status of the SVG attributes computation (sourceLeft, sourceTop, sourceWidth, sourceHeight) */
public get svgAttributesComputationCompleted(): boolean {
return this._svgAttributesComputationCompleted;
}
/**
* Gets or sets a boolean indicating if the image can force its container to adapt its size
* @see https://doc.babylonjs.com/how_to/gui#image
*/
@serialize()
public get autoScale(): boolean {
return this._autoScale;
}
public set autoScale(value: boolean) {
if (this._autoScale === value) {
return;
}
this._autoScale = value;
if (value && this._loaded) {
this.synchronizeSizeWithContent();
}
}
/** Gets or sets the stretching mode used by the image */
@serialize()
public get stretch(): number {
return this._stretch;
}
public set stretch(value: number) {
if (this._stretch === value) {
return;
}
this._stretch = value;
this._markAsDirty();
}
/**
* Gets or sets the internal DOM image used to render the control
*/
public set domImage(value: HTMLVideoElement) {
this._domImage = value;
this._loaded = false;
this._imageDataCache.data = null;
if (this._domImage.width) {
this._onImageLoaded();
} else {
this._domImage.onload = () => {
this._onImageLoaded();
};
}
}
public get domImage(): HTMLVideoElement {
return this._domImage;
}
private _onImageLoaded(): void {
this._imageDataCache.data = null;
this._imageWidth = this._domImage.videoWidth;
this._imageHeight = this._domImage.videoHeight;
this._loaded = true;
if (this._autoScale) {
this.synchronizeSizeWithContent();
}
this.onImageLoadedObservable.notifyObservers(this);
this._markAsDirty();
}
private _getVideo(
src: string | string[] | HTMLVideoElement
): HTMLVideoElement {
if ((<any>src).isNative) {
return <HTMLVideoElement>src;
}
if (src instanceof HTMLVideoElement) {
Tools.SetCorsBehavior(src.currentSrc, src);
return src;
}
const video: HTMLVideoElement = document.createElement("video");
if (typeof src === "string") {
Tools.SetCorsBehavior(src, video);
video.src = src;
} else {
Tools.SetCorsBehavior(src[0], video);
src.forEach((url) => {
const source = document.createElement("source");
source.src = url;
video.appendChild(source);
});
}
this.onDisposeObservable.addOnce(() => {
removeSource(video);
});
return video;
}
/**
* Gets the image source url
*/
@serialize()
public get source() {
return this._source;
}
/**
* Gets or sets image source url
*/
public set source(value: Nullable<string>) {
if (this._source === value) {
return;
}
this._loaded = false;
this._source = value;
this._imageDataCache.data = null;
if (value) {
this._domImage = this._getVideo(value);
this._domImage.crossOrigin = "anonymous";
this._domImage.width = 400;
this._domImage.height = 400;
this._domImage.muted = true;
this._domImage.autoplay = true;
this._domImage.loop = true;
this._domImage.play();
this._domImage.addEventListener("play", () => {
this._onImageLoaded();
});
this._domImage.addEventListener("seeked", () => {
this._onImageLoaded();
});
this._domImage.addEventListener("canPlay", () => {
this._onImageLoaded();
});
this._domImage.addEventListener("loadedData", () => {
this._onImageLoaded();
});
}
}
/**
* Gets or sets the cell width to use when animation sheet is enabled
* @see https://doc.babylonjs.com/how_to/gui#image
*/
@serialize()
get cellWidth(): number {
return this._cellWidth;
}
set cellWidth(value: number) {
if (this._cellWidth === value) {
return;
}
this._cellWidth = value;
this._markAsDirty();
}
/**
* Gets or sets the cell height to use when animation sheet is enabled
* @see https://doc.babylonjs.com/how_to/gui#image
*/
@serialize()
get cellHeight(): number {
return this._cellHeight;
}
set cellHeight(value: number) {
if (this._cellHeight === value) {
return;
}
this._cellHeight = value;
this._markAsDirty();
}
/**
* Gets or sets the cell id to use (this will turn on the animation sheet mode)
* @see https://doc.babylonjs.com/how_to/gui#image
*/
@serialize()
get cellId(): number {
return this._cellId;
}
set cellId(value: number) {
if (this._cellId === value) {
return;
}
this._cellId = value;
this._markAsDirty();
}
/**
* Creates a new Image
* @param name defines the control name
* @param url defines the image url
*/
constructor(
public name?: string,
url: Nullable<string> = null,
width?: string | number,
height?: string | number
) {
super(name);
this.width = width;
this.height = height;
this.source = url;
this.listener = AppStore.MainScene.onAfterRenderObservable.add(() => {
if (this.isVisible) this._onImageLoaded();
});
}
/**
* Tests if a given coordinates belong to the current control
* @param x defines x coordinate to test
* @param y defines y coordinate to test
* @returns true if the coordinates are inside the control
*/
public contains(x: number, y: number): boolean {
if (!super.contains(x, y)) {
return false;
}
if (!this._detectPointerOnOpaqueOnly || !this._workingCanvas) {
return true;
}
const width = this._currentMeasure.width | 0;
const height = this._currentMeasure.height | 0;
const key = width + "_" + height;
let imageData = this._imageDataCache.data;
if (!imageData || this._imageDataCache.key !== key) {
const canvas = this._workingCanvas;
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const context = canvas.getContext("2d")!;
this._imageDataCache.data = imageData = context.getImageData(
0,
0,
width,
height
).data;
this._imageDataCache.key = key;
}
x = (x - this._currentMeasure.left) | 0;
y = (y - this._currentMeasure.top) | 0;
const pickedPixel = imageData[(x + y * width) * 4 + 3];
return pickedPixel > 0;
}
protected _getTypeName(): string {
return "Image";
}
/** Force the control to synchronize with its content */
public synchronizeSizeWithContent() {
if (!this._loaded) {
return;
}
this.width = this._domImage.width + "px";
this.height = this._domImage.height + "px";
}
protected _processMeasures(
parentMeasure: Measure,
context: ICanvasRenderingContext
): void {
if (this._loaded) {
switch (this._stretch) {
case Image2.STRETCH_NONE:
break;
case Image2.STRETCH_FILL:
break;
case Image2.STRETCH_UNIFORM:
break;
case Image2.STRETCH_NINE_PATCH:
break;
case Image2.STRETCH_EXTEND:
if (this._autoScale) {
this.synchronizeSizeWithContent();
}
if (this.parent && this.parent.parent) {
// Will update root size if root is not the top root
this.parent.adaptWidthToChildren = true;
this.parent.adaptHeightToChildren = true;
}
break;
}
}
super._processMeasures(parentMeasure, context);
}
private _prepareWorkingCanvasForOpaqueDetection() {
if (!this._detectPointerOnOpaqueOnly) {
return;
}
const width = this._currentMeasure.width;
const height = this._currentMeasure.height;
if (!this._workingCanvas) {
const engine =
this._host?.getScene()?.getEngine() || EngineStore.LastCreatedEngine;
if (!engine) {
throw new Error("Invalid engine. Unable to create a canvas.");
}
this._workingCanvas = engine.createCanvas(width, height);
}
const canvas = this._workingCanvas;
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const context = canvas.getContext("2d")!;
context.clearRect(0, 0, width, height);
}
private _drawImage(
context: ICanvasRenderingContext,
sx: number,
sy: number,
sw: number,
sh: number,
tx: number,
ty: number,
tw: number,
th: number
) {
context.drawImage(this._domImage, sx, sy, sw, sh, tx, ty, tw, th);
if (!this._detectPointerOnOpaqueOnly) {
return;
}
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const canvas = this._workingCanvas!;
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
context = canvas.getContext("2d")!;
context.drawImage(
this._domImage,
sx,
sy,
sw,
sh,
tx - this._currentMeasure.left,
ty - this._currentMeasure.top,
tw,
th
);
}
public _draw(context: ICanvasRenderingContext): void {
context.save();
if (this.shadowBlur || this.shadowOffsetX || this.shadowOffsetY) {
context.shadowColor = this.shadowColor;
context.shadowBlur = this.shadowBlur;
context.shadowOffsetX = this.shadowOffsetX;
context.shadowOffsetY = this.shadowOffsetY;
}
let x, y, width, height;
if (this.cellId === -1) {
x = this._sourceLeft;
y = this._sourceTop;
width = this._sourceWidth ? this._sourceWidth : this._imageWidth;
height = this._sourceHeight ? this._sourceHeight : this._imageHeight;
} else {
const rowCount = this._domImage.videoWidth / this.cellWidth;
const column = (this.cellId / rowCount) >> 0;
const row = this.cellId % rowCount;
x = this.cellWidth * row;
y = this.cellHeight * column;
width = this.cellWidth;
height = this.cellHeight;
}
this._prepareWorkingCanvasForOpaqueDetection();
this._applyStates(context);
if (this._loaded) {
this._drawImage(
context,
x,
y,
width,
height,
this._currentMeasure.left,
this._currentMeasure.top,
this._currentMeasure.width,
this._keepSize
? (this._currentMeasure.width * height) / width
: this._currentMeasure.height
);
}
context.restore();
}
public dispose() {
super.dispose();
this.onImageLoadedObservable.clear();
this.onSVGAttributesComputedObservable.clear();
if (this.listener)
AppStore.Scene.onAfterRenderObservable.remove(this.listener);
removeSource(this._domImage);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment