-
-
Save ZoeLeee/f4c58be0765fab64bc6821e15c82a6db to your computer and use it in GitHub Desktop.
VideoGUi
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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