@awayjs/stage
Version:
Stage for AwayJS
751 lines (587 loc) • 19.4 kB
text/typescript
import {
EventDispatcher,
Rectangle,
IAssetClass,
IAbstractionPool,
IAbstractionClass,
CSS,
Point,
ColorTransform,
IAsset,
UUID,
IAbstraction
} from '@awayjs/core';
import { ContextMode } from './base/ContextMode';
import { ContextGLMipFilter } from './base/ContextGLMipFilter';
import { ContextGLTextureFilter } from './base/ContextGLTextureFilter';
import { ContextGLWrapMode } from './base/ContextGLWrapMode';
import { ContextGLProfile } from './base/ContextGLProfile';
import { IContextGL } from './base/IContextGL';
import { IVertexBuffer } from './base/IVertexBuffer';
import { StageEvent } from './events/StageEvent';
import { ImageBase } from './image/ImageBase';
import { ImageSampler } from './image/ImageSampler';
import { _Stage_ImageBase } from './image/ImageBase';
import { ProgramData } from './image/ProgramData';
import { ProgramDataPool } from './image/ProgramDataPool';
import { StageManager } from './managers/StageManager';
import { ContextWebGL } from './webgl/ContextWebGL';
import { ContextGLClearMask } from './base/ContextGLClearMask';
import { Image2D } from './image/Image2D';
import { UnloadService } from './managers/UnloadManager';
import { ImageUtils } from './utils/ImageUtils';
import { TouchPoint } from './base/TouchPoint';
import { FilterManager } from './managers/FilterManager';
import { BUFFER_FORMATS_MAP } from './utils/BufferFormat';
const TMP_POINT = { x: 0, y: 0 };
interface ITargetConf {
target: ImageBase;
mipSelector: number;
surfaceSelector: number;
depthStencil: boolean;
msaa: boolean;
}
/**
* Stage provides a proxy class to handle the creation and attachment of the Context
* (and in turn the back buffer) it uses. Stage should never be created directly,
* but requested through StageManager.
*
* @see away.managers.StageManager
*
*/
export class Stage extends EventDispatcher implements IAbstractionPool {
private static _abstractionClassPool: Record<string, IAbstractionClass> = {};
private _programData: Array<ProgramData> = new Array<ProgramData>();
private _programDataPool: ProgramDataPool;
private _context: IContextGL;
private _container: HTMLCanvasElement;
private _width: number;
private _height: number;
private _x: number = 0;
private _y: number = 0;
public _screenX: number;
public _screenY: number;
public _touchPoints: Array<TouchPoint> = new Array<TouchPoint>();
//private static _frameEventDriver:Shape = new Shape(); // TODO: add frame driver/request animation frame
private _stageIndex: number = -1;
private _usesSoftwareRendering: boolean;
private _profile: ContextGLProfile;
private _stageManager: StageManager;
private _antiAlias: number = 4;
private readonly savedTargetStack: Array<ITargetConf> = [];
private readonly _activeTargetConf: ITargetConf = {
mipSelector: 0,
surfaceSelector: 0,
target: null,
depthStencil: true,
msaa: true
};
private _color: number;
private _backBufferDirty: boolean;
private _sizeDirty: boolean;
private _lastScissorBox: Rectangle;
private _frameEndCallbackOnce: Array<() => void> = [];
private _initialised: boolean = false;
public globalDisableMipmap: boolean = false;
public globalDisableSmooth: boolean = false;
public numUsedStreams: number = 0;
public numUsedTextures: number = 0;
public readonly filterManager: FilterManager;
public set pixelRatio(v: number) {
this._context.pixelRatio = v;
}
public get pixelRatio(): number {
return this._context.pixelRatio;
}
public get glVersion(): number {
return this._context.glVersion;
}
public get screenX(): number {
return this._screenX;
}
public get screenY(): number {
return this._screenY;
}
public get touchPoints(): Array<TouchPoint> {
return this._touchPoints;
}
public readonly id: number;
constructor(
container: HTMLCanvasElement, stageIndex: number,
stageManager: StageManager, forceSoftware: boolean = false,
profile: ContextGLProfile = ContextGLProfile.BASELINE) {
super();
this.id = UUID.Next();
this._programDataPool = new ProgramDataPool(this);
this._container = container;
if (this._container) {
this._container.addEventListener('webglcontextlost', (event) => this.onContextLost(event));
this._container.addEventListener('webglcontextrestored', (event) => this.onContextRestored(event));
}
this._stageIndex = stageIndex;
this._stageManager = stageManager;
CSS.setElementX(this._container, 0);
CSS.setElementY(this._container, 0);
this._width = this._container.clientWidth;
this._height = this._container.clientHeight;
this.visible = true;
this.filterManager = new FilterManager(this);
}
// for avoid using a Dispather, because it has big overhead
public requiestFrameEnd(func: () => void) {
if (this._frameEndCallbackOnce.indexOf(func) > -1 || typeof func !== 'function') {
return;
}
this._frameEndCallbackOnce.push(func);
}
/**
* @description Should be executed AFTER rendering process
*/
public present() {
this._context.present();
UnloadService.executeAll();
for (const call of this._frameEndCallbackOnce) {
call();
}
this._frameEndCallbackOnce.length = 0;
}
public getProgramData(vertexString: string, fragmentString: string): ProgramData {
return this._programDataPool.getItem(vertexString, fragmentString);
}
/**
* Safe current target config to stack
*/
public pushRenderTargetConfig() {
this.savedTargetStack.push(Object.assign({}, this._activeTargetConf));
}
/**
* Pop render target config and apply it, used for deep caching
*/
public popRenderTarget() {
if (this.savedTargetStack.length === 0)
return;
const {
target,
depthStencil,
surfaceSelector,
mipSelector,
msaa
} = this.savedTargetStack.pop();
this.setRenderTarget(
target,
depthStencil,
surfaceSelector,
mipSelector,
!msaa
);
}
public setRenderTarget(
target: ImageBase & {antialiasQuality?: number},
enableDepthAndStencil: boolean = false,
surfaceSelector: number = 0,
mipmapSelector: number = 0,
disableMSAA = false
): void {
const conf = this._activeTargetConf;
if (conf.target === target
&& conf.surfaceSelector === surfaceSelector
&& conf.mipSelector === mipmapSelector
&& conf.depthStencil === enableDepthAndStencil
&& conf.msaa !== disableMSAA
) {
return;
}
conf.msaa = !disableMSAA;
conf.target = target;
conf.surfaceSelector = surfaceSelector;
conf.mipSelector = mipmapSelector;
conf.depthStencil = enableDepthAndStencil;
if (target) {
const targetStageElement = target.getAbstraction<_Stage_ImageBase>(this);
const antiallias = typeof target.antialiasQuality === 'number' // for SceneImage2D MSAA
? target.antialiasQuality
: this._antiAlias;
this._context.setRenderToTexture(
targetStageElement.getTexture(),
enableDepthAndStencil,
disableMSAA ? 0 : antiallias,
surfaceSelector,
mipmapSelector);
if (mipmapSelector != 0 && this._context.glVersion != 1) { //hack to stop auto generated mipmaps
targetStageElement._invalidMipmaps = false;
targetStageElement._mipmap = true;
}
} else {
this._context.setRenderToBackBuffer();
this.configureBackBuffer(this._width, this._height, this._antiAlias, conf.depthStencil);
}
}
public copyPixels(
source: Image2D, target: Image2D,
rect: Rectangle, destPoint: Point,
alphaBitmapData: Image2D = null, alphaPoint: Point = null,
mergeAlpha: boolean = false): void {
// migrated to filter manager
this.filterManager.copyPixels(source, target, rect, destPoint, mergeAlpha);
}
public threshold(
source: Image2D, target: Image2D,
rect: Rectangle, destPoint: Point,
operation: string, threshold: number,
color: number, mask: number, copySource: boolean): void {
// migrated to filter manager
this.filterManager.threshold(source, target, rect, destPoint, operation, threshold, color, mask, copySource);
}
public colorTransform(source: Image2D, target: Image2D, rect: Rectangle, colorTransform: ColorTransform): void {
// migrated to filter manager
this.filterManager.colorTransform(source, target, rect, colorTransform);
}
public requestAbstraction(asset: IAsset): IAbstraction {
return new Stage._abstractionClassPool[asset.assetType];
}
public storeAbstraction(abstraction: IAbstraction): void {
}
/**
*
* @param imageObjectClass
*/
public static registerAbstraction(abstractionClass: IAbstractionClass, assetClass: IAssetClass): void {
Stage._abstractionClassPool[assetClass.assetType] = abstractionClass;
}
/**
* Requests a Context object to attach to the managed gl canvas.
*/
public requestContext(
forceSoftware: boolean = false,
profile: ContextGLProfile = ContextGLProfile.BASELINE,
_mode: ContextMode = ContextMode.AUTO,
alpha: boolean = false
): void {
// If forcing software, we can be certain that the
// returned Context will be running software mode.
// If not, we can't be sure and should stick to the
// old value (will likely be same if re-requesting.)
if (this._usesSoftwareRendering != null)
this._usesSoftwareRendering = forceSoftware;
this._profile = profile;
try {
this._context = new ContextWebGL(this._container, alpha);
} catch (e) {
this.dispatchEvent(new StageEvent(StageEvent.STAGE_ERROR, this));
}
if (this._context)
this._callback(this._context);
}
/**
* The width of the gl canvas
*/
public get width(): number {
this._sizeDirty = false;
return this._width;
}
public set width(val: number) {
if (this._width == val)
return;
this._container.style.width = val + 'px';
this._container.width = val * this.pixelRatio;
this._width = val;
this._backBufferDirty = true;
if (this._width && this._height)
this._invalidateSize();
}
/**
* The height of the gl canvas
*/
public get height(): number {
this._sizeDirty = false;
return this._height;
}
public set height(val: number) {
if (this._height == val)
return;
this._container.style.height = val + 'px';
this._container.height = val * this.pixelRatio;
this._height = val;
this._backBufferDirty = true;
if (this._width && this._height)
this._invalidateSize();
}
/**
* The x position of the gl canvas
*/
public get x(): number {
return this._x;
}
public set x(val: number) {
if (this._x == val)
return;
CSS.setElementX(this._container, val);
this._x = val;
}
/**
* The y position of the gl canvas
*/
public get y(): number {
return this._y;
}
public set y(val: number) {
if (this._y == val)
return;
CSS.setElementY(this._container, val);
this._y = val;
}
public set visible(val: boolean) {
CSS.setElementVisibility(this._container, val);
}
public get visible(): boolean {
return CSS.getElementVisibility(this._container);
}
public set container(value: HTMLElement) {
this._container = <HTMLCanvasElement>value;
if (this._context) {
this._context.container = this._container;
}
}
public get container(): HTMLElement {
return this._container;
}
/**
* The Context object associated with the given stage object.
*/
public get context(): IContextGL {
return this._context;
}
private _invalidateSize(): void {
if (this._sizeDirty)
return;
this._sizeDirty = true;
this.dispatchEvent(new StageEvent(StageEvent.INVALIDATE_SIZE, this));
}
public get profile(): ContextGLProfile {
return this._profile;
}
/**
* Disposes the Stage object, freeing the Context attached to the Stage.
*/
public dispose(): void {
/*
for (var id in this._abstractionPool){
if(this._abstractionPool[id].clear)
this._abstractionPool[id].clear();
}*/
this._stageManager.iRemoveStage(this);
this.freeContext();
this._stageManager = null;
this._stageIndex = -1;
UnloadService.clearAll();
}
/**
* Configures the back buffer associated with the Stage object.
* @param backBufferWidth The width of the backbuffer.
* @param backBufferHeight The height of the backbuffer.
* @param antiAlias The amount of anti-aliasing to use.
* @param enableDepthAndStencil Indicates whether the back buffer contains a depth and stencil buffer.
*/
public configureBackBuffer(
backBufferWidth: number, backBufferHeight: number, antiAlias: number, enableDepthAndStencil: boolean): void {
this.width = backBufferWidth;
this.height = backBufferHeight;
this._antiAlias = antiAlias;
this._activeTargetConf.depthStencil = enableDepthAndStencil;
if (this._context)
this._context.configureBackBuffer(backBufferWidth, backBufferHeight, antiAlias, enableDepthAndStencil);
}
/*
* Indicates whether the depth and stencil buffer is used
*/
public get enableDepthAndStencil(): boolean {
return this._activeTargetConf.depthStencil;
}
public set enableDepthAndStencil(enableDepthAndStencil: boolean) {
if (this._activeTargetConf.depthStencil == enableDepthAndStencil)
return;
this._activeTargetConf.depthStencil = enableDepthAndStencil;
this._backBufferDirty = true;
}
public get renderSurfaceSelector(): number {
return this._activeTargetConf.surfaceSelector;
}
/*
* Clear and reset the back buffer when using a shared context
*/
public clear(
red: number = 0, green: number = 0, blue: number = 0, alpha: number = 1,
depth: number = 1, stencil: number = 0, mask: ContextGLClearMask = ContextGLClearMask.ALL): void {
if (!this._context)
return;
if (this._backBufferDirty && this._activeTargetConf.target == null) {
this._backBufferDirty = false;
this.configureBackBuffer(
this._width,
this._height,
this._antiAlias,
this._activeTargetConf.depthStencil);
}
this._context.clear(red, green, blue, alpha, depth, stencil, mask);
}
/**
* The index of the Stage which is managed by this instance of StageProxy.
*/
public get stageIndex(): number {
return this._stageIndex;
}
/**
* Indicates whether the Stage managed by this proxy is running in software mode.
* Remember to wait for the CONTEXT_CREATED event before checking this property,
* as only then will it be guaranteed to be accurate.
*/
public get usesSoftwareRendering(): boolean {
return this._usesSoftwareRendering;
}
/**
* The antiAliasing of the Stage.
*/
public get antiAlias(): number {
return this._antiAlias;
}
public set antiAlias(antiAlias: number) {
if (this._antiAlias == antiAlias)
return;
this._antiAlias = antiAlias;
this._backBufferDirty = true;
}
/**
* The background color of the Stage.
*/
public get color(): number {
return this._color;
}
public set color(color: number) {
this._color = color;
}
public registerProgram(programData: ProgramData): void {
let i: number = 0;
while (this._programData[i] != null)
i++;
this._programData[i] = programData;
programData.id = i;
}
public unRegisterProgram(programData: ProgramData): void {
this._programData[programData.id] = null;
programData.id = -1;
}
public interactionHandler(event: MouseEvent | TouchEvent) {
const screenX: number = ((<MouseEvent> event).clientX != null) ? (<MouseEvent> event).clientX : (<TouchEvent> event).changedTouches[0].clientX;
const screenY: number = ((<MouseEvent> event).clientY != null) ? (<MouseEvent> event).clientY : (<TouchEvent> event).changedTouches[0].clientY;
let point = this.mapWindowToStage(screenX, screenY, TMP_POINT);
this._screenX = point.x;
this._screenY = point.y;
this._touchPoints.length = 0;
if ((<TouchEvent> event).touches) {
for (let i: number = 0; i < (<TouchEvent> event).touches.length; i++) {
const touch = (<TouchEvent> event).touches[i];
point = this.mapWindowToStage(touch.clientX, touch.clientY, TMP_POINT);
this._touchPoints.push(new TouchPoint(point.x, point.y, touch.identifier));
}
}
}
public mapWindowToStage(x: number, y: number, out: {x: number, y: number} = { x: 0, y: 0 }) {
const container = <HTMLCanvasElement> this.container;
// IE 11 fix
const rect = (!container.parentElement)
? { x: 0, y: 0, width: 0, height: 0 }
: container.getBoundingClientRect();
// workground when pixelRatio !== self.devicePixelRatio
out.x = (x - rect.x) * container.width / (rect.width * this.pixelRatio);
out.y = (y - rect.y) * container.height / (rect.height * this.pixelRatio);
return out;
}
/**
* Frees the Context associated with this StageProxy.
*/
private freeContext(): void {
if (this._context) {
this._context.dispose();
this.dispatchEvent(new StageEvent(StageEvent.CONTEXT_DISPOSED, this));
}
this._context = null;
this._initialised = false;
}
private onContextLost(_event): void {
}
private onContextRestored(_event): void {
}
public recoverFromDisposal(): boolean {
if (!this._context)
return false;
//away.Debug.throwPIR( 'StageProxy' , 'recoverFromDisposal' , '' );
/*
if (this._iContext.driverInfo == "Disposed")
{
this._iContext = null;
this.dispatchEvent(new StageEvent(StageEvent.CONTEXT_DISPOSED));
return false;
}
*/
return true;
}
private _callback(context: IContextGL): void {
const gl = (<ContextWebGL>context)._gl;
ImageUtils.MAX_SIZE = gl.getParameter(gl.MAX_TEXTURE_SIZE);
this._context = context;
this._container = this._context.container;
// Only configure back buffer if width and height have been set,
// which they may not have been if View.render() has yet to be
// invoked for the first time.
if (this._width && this._height) {
this._context.configureBackBuffer(
this._width,
this._height,
this._antiAlias,
this._activeTargetConf.depthStencil);
}
// Dispatch the appropriate event depending on whether context was
// created for the first time or recreated after a device loss.
this.dispatchEvent(
new StageEvent(this._initialised ? StageEvent.CONTEXT_RECREATED : StageEvent.CONTEXT_CREATED, this));
this._initialised = true;
}
public setVertexBuffer(
index: number, buffer: IVertexBuffer,
size: number, dimensions: number,
offset: number, unsigned: boolean = false): void {
this._context.setVertexBufferAt(
index, buffer, offset, BUFFER_FORMATS_MAP[unsigned ? size + 4 : size][dimensions - 1]);
}
public setScissor(rectangle: Rectangle): void {
if (this._backBufferDirty && this._activeTargetConf.target == null) {
this._backBufferDirty = false;
this.configureBackBuffer(this._width, this._height, this._antiAlias, this._activeTargetConf.depthStencil);
}
if (!this._lastScissorBox && !rectangle) {
return;
}
if (rectangle && this._lastScissorBox && this._lastScissorBox.equals(rectangle)) {
return;
}
if (!rectangle) {
this._lastScissorBox = null;
} else {
if (!this._lastScissorBox) {
this._lastScissorBox = new Rectangle();
}
this._lastScissorBox.copyFrom(rectangle);
}
this._context.setScissorRectangle(rectangle);
}
public setSamplerAt(index: number, sampler: ImageSampler): void {
const wrap = sampler.repeat ? ContextGLWrapMode.REPEAT : ContextGLWrapMode.CLAMP;
const filter = (sampler.smooth && !this.globalDisableSmooth)
? ContextGLTextureFilter.LINEAR
: ContextGLTextureFilter.NEAREST;
const mipfilter = (sampler.mipmap && !this.globalDisableMipmap)
? ContextGLMipFilter.MIPLINEAR
: ContextGLMipFilter.MIPNONE;
this._context.setSamplerStateAt(index, wrap, filter, mipfilter);
}
}