import { Observable , ReplaySubject } from 'rxjs' ; import { vec2 , vec4 } from 'gl-matrix' ; import { clamp2 , parseVec2 , parseVec4 } from './gl-matrix-extensions' ; import { assert , log , logIf , LogLevel } from './auxiliaries' ; import { GLclampf2 , GLsizei2 , tuple2 , tuple4 } from './tuples' ; import { Color } from './color' ; import { Context } from './context' ; import { Controller } from './controller' ; import { EyeGazeDataStreams } from './eyegazedatastream' ; import { EyeGazeEventProvider } from './eyegazeeventprovider' ; import { MouseEventProvider } from './mouseeventprovider' ; import { PointerEventProvider } from './pointereventprovider' ; import { Renderer } from './renderer' ; import { Resizable } from './resizable' ; import { TouchEventProvider } from './toucheventprovider' ; import { Wizard } from './wizard' ; /**
 * A canvas is associated to a single canvas element (DOM) and integrates or delegates event handling as well as
 * control over the rendering loop and the attached renderer respectively. Furthermore, the canvas can bind a single
 * renderer (non owning) and sets up communication between controller, renderer, and navigation. The controller invokes
 * the renderer's update, frame, and swap methods. The navigation manipulates the renderer's camera. The renderer can
 * invalidate itself which causes a controller update.
 *
 * Note: Since only the multi-frame number is used by the renderer and the controller, the canvas provides getter,
 * setter, and change callback setter. Debug-frame and frame number are managed exclusively by the controller.
 *
 * Note: the canvas should hold any properties that are required to be passed onto a newly bound renderer (in the case
 * multiple renderer are used with a canvas). The clear color is one example of such a property.
 */ export class Canvas extends Resizable { /**
     * Default color that is used if none is set via data attributes.
     */ protected static readonly DEFAULT_CLEAR_COLOR : Color = new Color ( [ 0.203 , 0.227 , 0.250 , 1.0 ] ) ; /**
     * Default frame precision, e.g., accumulation format when multi-frame rendering is used.
     */ protected static readonly DEFAULT_FRAME_PRECISION : Wizard . Precision = Wizard . Precision . auto ; /**
     * Default multi-frame number used if none is set via data attributes.
     */ protected static readonly DEFAULT_MULTI_FRAME_NUMBER = 0 ; see context protected _context : Context ; see controller protected _controller : Controller ; see renderer protected _renderer : Renderer | undefined ; see clearColor protected _clearColor : Color ; see framePrecision * This property can be observed, e.g., `aCanvas.framePrecisionObservable.subscribe()`.
      protected _framePrecision : Wizard . Precision ; protected _framePrecisionSubject = new ReplaySubject < Wizard . Precision > ( 1 ) ; see size * This property can be observed, e.g., `aCanvas.sizeObservable.subscribe()`.
     * Zero-initialization prevents drawing on an invalid canvas size, i.e., a canvas size [1, 1] is technically valid
     * for rendering, which might lead to rendering on an [1, 1] canvas before the first 'real' size is set (e.g., via
     * resize event), resulting in visually unpleasant initial frames in some (slow) applications.
      protected _size : GLsizei2 = [ 0 , 0 ] ; protected _sizeSubject = new ReplaySubject < GLsizei2 > ( 1 ) ; see frameScale * This property can be observed, `aCanvas.frameScaleObservable.subscribe()`.
      protected _frameScale : GLclampf2 ; protected _frameScaleSubject = new ReplaySubject < GLclampf2 > ( 1 ) ; see frameSize * This property can be observed, `aCanvas.frameSizeObservable.subscribe()`.
      protected _frameSize : GLsizei2 ; protected _frameSizeSubject = new ReplaySubject < GLsizei2 > ( 1 ) ; /**
     * Flag used to determine whether frame size or frame scale is the dominant configuration.
     */ protected _favorSizeOverScale : boolean ; see element protected _element : HTMLCanvasElement ; see mouseEventProvider protected _mouseEventProvider : MouseEventProvider ; see touchEventProvider protected _touchEventProvider : TouchEventProvider ; see pointerEventProvider protected _pointerEventProvider : PointerEventProvider ; see eyeGazeEventProvider protected _eyeGazeEventProvider : EyeGazeEventProvider ; protected _lostContextExtension : WEBGL_lose_context | undefined ; param element param attributes see { Context . DEFAULT_ATTRIBUTES } constructor ( element , attributes ? ) { super ( ) ; this . _element = element instanceof HTMLCanvasElement ? element : document . getElementById ( element ) as HTMLCanvasElement ; this . _element . addEventListener ( 'webglcontextcreationerror' , ( e ) => { console . log ( e . statusMessage || 'Unknown error' ) ; } , false ) ; this . observe ( this . _element ) ; this . _mouseEventProvider = new MouseEventProvider ( this . _element , 200 ) ; this . _touchEventProvider = new TouchEventProvider ( this . _element , 200 ) ; this . _pointerEventProvider = new PointerEventProvider ( this . _element , 200 ) ; /**
         * Disable default handling of touch events by the browser.
         * Touch events are handled using PointerEventProvider.
         */ this . _element . style . touchAction = 'none' ; const dataset = this . _element . dataset ; this . _context = Context . request ( this . _element , attributes ) ; this . configureController ( dataset ) ; this . configureSizeAndScale ( dataset ) ; this . configureContextLostAndRestore ( ) ; this . configureContextLostAndRestoreEmulation ( ) ; let dataClearColor ; if ( dataset . clearColor ) { dataClearColor = parseVec4 ( dataset . clearColor ) ; logIf ( dataClearColor === undefined , LogLevel . Warning , `data-clear-color could not be parsed, given '${ dataset . clearColor }'` ) ; } this . _clearColor = dataClearColor ? new Color ( tuple4 < GLclampf > ( dataClearColor ) ) : Canvas . DEFAULT_CLEAR_COLOR ; let dataFramePrecision = dataset . accumulationFormat ? dataset . accumulationFormat as Wizard . Precision : Canvas . DEFAULT_FRAME_PRECISION ; if ( ! ( dataFramePrecision in Wizard . Precision ) ) { dataFramePrecision = Canvas . DEFAULT_FRAME_PRECISION ; log ( LogLevel . Warning , `unknown frame precision '${ dataset . accumulationFormat }' changed to '${ dataFramePrecision }'` ) ; } this . _framePrecision = dataFramePrecision ; this . framePrecisionNext ( ) ; } param dataset protected configureController ( dataset ) { this . _controller = new Controller ( ) ; this . _controller . block ( ) ; let dataMFNum ; if ( dataset . multiFrameNumber ) { dataMFNum = parseInt ( dataset . multiFrameNumber , 10 ) ; logIf ( isNaN ( dataMFNum ) , LogLevel . Warning , `data-multi-frame-number could not be parsed, given '${ dataset . multiFrameNumber }'` ) ; } let dataDFNum ; if ( dataset . debugFrameNumber ) { dataDFNum = parseInt ( dataset . debugFrameNumber , 10 ) ; logIf ( isNaN ( dataDFNum ) , LogLevel . Warning , `data-debug-frame-number could not be parsed, given '${ dataset . debugFrameNumber }'` ) ; } this . _controller . multiFrameNumber = dataMFNum ? dataMFNum : Canvas . DEFAULT_MULTI_FRAME_NUMBER ; this . _controller . debugFrameNumber = dataDFNum ? dataDFNum : 0 ; const mfNumChanged = dataMFNum ? dataMFNum !== this . _controller . multiFrameNumber : false ; logIf ( mfNumChanged , LogLevel . Warning , `data-multi-frame-number changed to ` + `${ this . _controller . multiFrameNumber }, given '${ dataset . multiFrameNumber }'` ) ; const dfNumChanged = dataDFNum ? dataDFNum !== this . _controller . debugFrameNumber : false ; logIf ( dfNumChanged , LogLevel . Warning , `data-debug-frame-number changed to ` + `${ this . _controller . debugFrameNumber }, given '${ dataset . debugFrameNumber }'` ) ; } param dataset protected configureSizeAndScale ( dataset ) { let dataFrameScale ; if ( dataset . frameScale ) { dataFrameScale = parseVec2 ( dataset . frameScale ) ; logIf ( dataset . frameScale !== undefined && dataFrameScale === undefined , LogLevel . Warning , `data-frame-scale could not be parsed, given '${ dataset . frameScale }'` ) ; } this . _frameScale = dataFrameScale ? tuple2 < GLfloat > ( dataFrameScale ) : [ 1.0 , 1.0 ] ; let dataFrameSize ; if ( dataset . frameSize ) { dataFrameSize = parseVec2 ( dataset . frameSize ) ; logIf ( dataset . frameSize !== undefined && dataFrameSize === undefined , LogLevel . Warning , `data-frame-size could not be parsed, given '${ dataset . frameSize }'` ) ; } this . _favorSizeOverScale = dataFrameSize !== undefined ; this . _frameSize = dataFrameSize ? tuple2 < GLsizei > ( dataFrameSize ) : [ this . _size [ 0 ] , this . _size [ 1 ] ] ; this . onResize ( ) ; } /**
     * Register 'webglcontextlost' and 'webglcontextrestored' to handle lost and restoration
     * of WebGL contexts.
     */ protected configureContextLostAndRestore ( ) { this . _element . addEventListener ( 'webglcontextlost' , ( event ) => { event . preventDefault ( ) ; this . onContextLost ( ) ; } , false ) ; this . _element . addEventListener ( 'webglcontextrestored' , ( ) => { this . onContextRestore ( ) ; } , false ) ; } /**
     * Obtain the WEBGL_lose_context extension, store it with this canvas instance and use it
     * for emulation of the context lost and restore feature.
     */ protected configureContextLostAndRestoreEmulation ( ) { this . _lostContextExtension = this . _context . gl . getExtension ( 'WEBGL_lose_context' ) ; } /**
     * Handle a WebGL context lost event.
     * This is for both natural and emulated lost contexts.
     */ protected onContextLost ( ) { log ( LogLevel . Warning , 'WebGL Context lost. Discarding renderer...' ) ; this . _controller . cancel ( ) ; this . _controller . block ( ) ; if ( this . _renderer ) { this . _renderer . discard ( ) ; } } /**
     * Handle a WebGL context restore event.
     * This is for both natural and emulated restored contexts.
     */ protected onContextRestore ( ) { log ( LogLevel . Warning , 'WebGL Context restored. Reinitializing renderer...' ) ; const renderer = this . _renderer ; this . unbind ( ) ; this . bind ( renderer ) ; this . _controller . unblock ( ) ; const formerVisibility = this . _element . style . visibility ; this . _element . style . visibility = 'hidden' ; this . _element . offsetHeight ; this . _element . style . visibility = formerVisibility ; } /**
     * Convenience function that triggers the canvas size retrieval. The native width and height of the canvas dom
     * element is cached (in pixel).
     */ protected retrieveSize ( ) { const size = Resizable . elementSize ( this . _element ) ; if ( size === undefined ) { this . _size = [ 0 , 0 ] ; return ; } this . _size = [ size [ 0 ] , size [ 1 ] ] ; this . sizeNext ( ) ; } /**
     * Resize is invoked by the resizable mixin. It retrieves the canvas size and promotes it to the renderer.
     */ protected onResize ( ) { this . retrieveSize ( ) ; if ( this . _size [ 0 ] === 0 || this . _size [ 1 ] === 0 ) { log ( LogLevel . Debug , `canvas width or height is invalid, resize discarded and controller blocked` ) ; this . _controller . block ( ) ; return ; } /**
         * Set canvas rendering size to pixel size of the canvas. This assures a 1 to 1 mapping of native pixels to
         * fragments and thus should prevent upscaling.
         */ this . _element . width = this . _size [ 0 ] ; this . _element . height = this . _size [ 1 ] ; if ( this . _renderer ) { this . _controller . block ( ) ; } if ( this . _favorSizeOverScale ) { this . frameSize = this . _frameSize ; } else { this . frameScale = this . _frameScale ; } if ( this . _renderer ) { this . _controller . unblock ( ) ; this . _renderer . swap ( ) ; } } /**
     * Utility for communicating this._framePrecision changes to its associated subject.
     */ protected framePrecisionNext ( ) { this . _framePrecisionSubject . next ( this . _framePrecision ) ; } /**
     * Utility for communicating this._size changes to its associated subject.
     */ protected sizeNext ( ) { this . _sizeSubject . next ( this . _size ) ; } /**
     * Utility for communicating this._frameScale changes to its associated subject.
     */ protected frameScaleNext ( ) { this . _frameScaleSubject . next ( this . _frameScale ) ; } /**
     * Utility for communicating this._frameSize changes to its associated subject.
     */ protected frameSizeNext ( ) { this . _frameSizeSubject . next ( this . _frameSize ) ; } param renderer protected bind ( renderer ) { if ( this . _renderer === renderer ) { return ; } this . unbind ( ) ; if ( renderer === undefined ) { return ; } assert ( this . _controller . blocked , `expected controller to be blocked` ) ; this . _renderer = renderer ; /**
         * Note: a renderer that is to be bound to a canvas is expected to be not initialized. For it, initializable
         * throws on re-initialization. Similarly to the frame callback for the controller, the controller's update
         * method is assigned to the pipelines invalidation event.
         */ this . _renderer . initialize ( this . context , ( force ) => this . _controller . update ( force ) , { pointerEventProvider : this . _pointerEventProvider , mouseEventProvider : this . _mouseEventProvider , eyeGazeEventProvider : this . _eyeGazeEventProvider , } ) ; this . _renderer . frameSize = this . _frameSize ; this . _renderer . clearColor = this . _clearColor . rgba ; this . _renderer . framePrecision = this . _framePrecision ; this . _renderer . debugTexture = - 1 ; /**
         * Note: again, no asserts required since controller and renderer already take care of that.
         * Assign the renderer's update, frame, and swap method to the controller's frame and swap event callback.
         * The assignments trigger immediate update and subsequently updates on invalidation.
         */ this . _controller . controllable = this . _renderer ; this . _controller . unblock ( ) ; } /**
     * Unbinds the current renderer from the canvas as well as the controller and navigation, and uninitializes the
     * renderer.
     */ protected unbind ( ) { if ( this . _renderer === undefined ) { return ; } this . _controller . block ( ) ; /**
         * Since canvas is not the owner of the renderer it should not dispose it. However, the canvas manages the
         * initialization of bound pipelines.
         */ this . _controller . controllable = undefined ; this . _renderer = undefined ; } /**
     * Uninitializes and deletes the controller as well as all other properties.
     */ dispose ( ) { super . dispose ( ) ; if ( this . _renderer ) { this . _renderer . uninitialize ( ) ; this . unbind ( ) ; } } /**
     * Allows for explicit trigger of onResize, e.g., in case resize event-handling is managed explicitly ...
     */ resize ( ) { this . onResize ( ) ; } returns get controller ( ) : Controller { return this . _controller ; } returns get renderer ( ) : Renderer | undefined { return this . _renderer ; } param renderer set renderer ( renderer ) { this . bind ( renderer ) ; } returns get frameScale ( ) : GLclampf2 { return this . _frameScale ; } param frameScale returns set frameScale ( frameScale ) { if ( ! isFinite ( frameScale [ 0 ] ) || ! isFinite ( frameScale [ 1 ] ) ) { log ( LogLevel . Warning , `expected finite frame size, non-finite values ignored, given [${ frameScale }]` ) ; return ; } logIf ( frameScale [ 0 ] < 0.0 || frameScale [ 0 ] > 2.0 , LogLevel . Info , `frame width scale clamped to [0.0,2.0], given ${ frameScale [ 0 ] }` ) ; logIf ( frameScale [ 1 ] < 0.0 || frameScale [ 1 ] > 2.0 , LogLevel . Info , `frame height scale clamped to [0.0,2.0], given ${ frameScale [ 0 ] }` ) ; const scale = vec2 . create ( ) ; clamp2 ( scale , frameScale , [ 0.0 , 0.0 ] , [ 2.0 , 2.0 ] ) ; const size = vec2 . create ( ) ; vec2 . mul ( size , this . _size , scale ) ; vec2 . max ( size , [ 1 , 1 ] , size ) ; vec2 . round ( size , size ) ; vec2 . div ( scale , size , this . _size ) ; logIf ( ! vec2 . exactEquals ( scale , frameScale ) , 2 , `frame scale was adjusted to ${ scale . toString ( ) }, given ${ frameScale . toString ( ) }` ) ; this . _frameScale = tuple2 < GLclampf > ( scale ) ; this . _frameSize = tuple2 < GLsizei > ( size ) ; this . _favorSizeOverScale = false ; this . frameScaleNext ( ) ; this . frameSizeNext ( ) ; if ( this . _renderer ) { this . _renderer . frameSize = this . _frameSize ; } } /**
     * Observable that can be used to subscribe to frame scale changes.
     */ get frameScale$ ( ) : Observable < GLclampf2 > { return this . _frameScaleSubject . asObservable ( ) ; } returns get frameSize ( ) : GLsizei2 { return this . _frameSize ; } param frameSize returns set frameSize ( frameSize ) { if ( ! isFinite ( frameSize [ 0 ] ) || ! isFinite ( frameSize [ 1 ] ) ) { log ( LogLevel . Warning , `expected finite frame size, non-finite values ignored, given [${ frameSize }]` ) ; return ; } logIf ( frameSize [ 0 ] < 1 || frameSize [ 0 ] > this . _size [ 0 ] , LogLevel . Info , `frame width scale clamped to [1,${ this . _size [ 0 ] }], given ${ frameSize [ 0 ] }` ) ; logIf ( frameSize [ 1 ] < 1 || frameSize [ 1 ] > this . _size [ 1 ] , LogLevel . Info , `frame height scale clamped to [1, ${ this . _size [ 1 ] }], given ${ frameSize [ 1 ] }` ) ; const size = vec2 . create ( ) ; clamp2 ( size , frameSize , [ 1.0 , 1.0 ] , this . _size ) ; vec2 . round ( size , size ) ; logIf ( ! vec2 . exactEquals ( size , frameSize ) , LogLevel . Warning , `frame size was adjusted to ${ size . toString ( ) }, given ${ frameSize . toString ( ) }` ) ; const scale = vec2 . create ( ) ; vec2 . div ( scale , size , this . _size ) ; this . _frameScale = tuple2 < GLclampf > ( scale ) ; this . _frameSize = tuple2 < GLsizei > ( size ) ; this . _favorSizeOverScale = ! vec2 . exactEquals ( this . _frameSize , this . _size ) ; this . frameScaleNext ( ) ; this . frameSizeNext ( ) ; if ( this . _renderer ) { this . _renderer . frameSize = this . _frameSize ; } } /**
     * Observable that can be used to subscribe to frame size changes.
     */ get frameSize$ ( ) : Observable < GLsizei2 > { return this . _frameSizeSubject . asObservable ( ) ; } returns get clearColor ( ) : Color { return this . _clearColor ; } param clearColor set clearColor ( clearColor ) { this . _clearColor = clearColor ; if ( this . _renderer ) { this . _renderer . clearColor = this . _clearColor . rgba ; } } returns get framePrecision ( ) : Wizard . Precision { return this . _framePrecision ; } param precision set framePrecision ( precision ) { this . _framePrecision = precision ; if ( this . _renderer ) { this . _renderer . framePrecision = this . _framePrecision ; this . _framePrecision = this . _renderer . framePrecision ; } this . framePrecisionNext ( ) ; } /**
     * Observable that can be used to subscribe to frame precision changes.
     */ get framePrecision$ ( ) : Observable < string > { return this . _framePrecisionSubject . asObservable ( ) ; } /**
     * Provides access to the WebGL context (leaky abstraction).
     */ get context ( ) : Context { return this . _context ; } returns get backend ( ) : string { return this . _context . backendString as string ; } returns get size ( ) : GLsizei2 { return this . _size ; } /**
     * Observable that can be used to subscribe to canvas size changes.
     */ get size$ ( ) : Observable < GLsizei2 > { return this . _sizeSubject . asObservable ( ) ; } returns get width ( ) : GLsizei { return this . _size [ 0 ] ; } returns get height ( ) : GLsizei { return this . _size [ 1 ] ; } /**
     * Canvas element within the HTML5 document.
     */ get element ( ) : HTMLElement { return this . _element ; } /**
     * Canvas mouse event provider referring to the canvas element.
     */ get mouseEventProvider ( ) : MouseEventProvider { return this . _mouseEventProvider ; } /**
     * Canvas touch event provider referring to the canvas element.
     */ get touchEventProvider ( ) : TouchEventProvider { return this . _touchEventProvider ; } /**
     * Emulate a WebGL context loss.
     * This functionality requires to have configureContextLostAndRestoreEmulation() called before.
     * This function is usually called within the constructor of this canvas.
     */ public testLoseContext ( ) { if ( this . _lostContextExtension === undefined ) { return ; } this . _lostContextExtension . loseContext ( ) ; } /**
     * Emulate a WebGL context restore.
     * This functionality requires to have configureContextLostAndRestoreEmulation() called before.
     * This function is usually called within the constructor of this canvas.
     */ public testRestoreContext ( ) { if ( this . _lostContextExtension === undefined ) { return ; } this . _lostContextExtension . restoreContext ( ) ; } /**
     * Eye gaze event provider referring to the canvas element.
     */ get eyeGazeEventProvider ( ) : EyeGazeEventProvider { return this . _eyeGazeEventProvider ; } /**
     * Activates the eye gaze event provider referring to the canvas element.
     */ public activateEyeGazeEventProvider ( eyeGazeDataStreams , serverAddress ) { this . _eyeGazeEventProvider = new EyeGazeEventProvider ( eyeGazeDataStreams , serverAddress ) ; } }