import { Scene } from "../Scene" ; import { Game } from "../Game" ; import { Direction } from "../geom/Direction" ; import { AnimationArgs , SceneNodeAnimation } from "./SceneNodeAnimation" ; import { AffineTransform } from "../graphics/AffineTransform" ; import { Polygon2 } from "../graphics/Polygon2" ; import { Vector2 } from "../graphics/Vector2" ; /**
 * Hints which are returned to the scene after drawing the scene graph. These hints can suggest further actions after
 * drawing like requesting continuous drawing because of running animations.
 */ export enum PostDrawHints { /** As long as this hint is present the scene must be continuously redrawn to keep animations running. */ CONTINUE_DRAWING = 1 , /**
     * When this flag is set then at least one node has the showBounds flag set to true. The root node already
     * handles this flag by drawing the bounds when this hint is present.
     */ DRAW_BOUNDS = 2 } param T export interface SceneNodeArgs { /** Optional initial scene node ID. */ id ? : string | null ; /** Optional initial horizontal position of the scene node. Defaults to 0. */ x ? : number ; /** Optional initial vertical position of the scene node. Defaults to 0. */ y ? : number ; /** Optional initial width of the scene node. Defaults to 0. */ width ? : number ; /** Optional initial height of the scene node. Defaults to 0. */ height ? : number ; /**
     * Optional initial node anchor. Defaults to CENTER which means the X/Y coordinates of the node define where
     * the center of this scene node is displayed relative to the parent node.
     */ anchor ? : Direction ; /**
     * Optional initial child node anchor. Defaults to CENTER which means the child nodes are positioned relative to
     * the center of this scene node.
     */ childAnchor ? : Direction ; /**
     * Optional initial node opacity. Must be 0 or higher. 0.0 means fully transparent. Defaults to 1.0 which means
     * fully opaque. Can be larger than 1.0 to compensate transparency of its parent.
     */ opacity ? : number ; /** Optional initial layer (0-31) to put the node onto. Defaults to 0. */ layer ? : number ; /** Optional initial showBounds flag. Set to true to show bounds around the node for debugging purposes. */ showBounds ? : boolean ; } /**
 * Base scene node. Is used as base class for more specialized scene nodes but can also be used standalone as parent
 * node for other nodes (similar to a DIV element in HTML for example).
 *
 * TODO Implement scene invalidation properly.
 */ export class SceneNode < T extends Game = Game > { /** The parent node. Null if none. */ private parent : SceneNode < T > | null = null ; /** The next sibling node. Null if none. */ private nextSibling : SceneNode < T > | null = null ; /** The previous sibling node. Null if none. */ private previousSibling : SceneNode < T > | null = null ; /** The first child node. Null if none. */ private firstChild : SceneNode < T > | null = null ; /** The last child node. Null if none. */ private lastChild : SceneNode < T > | null = null ; /** The scene this node is connected to. Null if none. */ protected scene : Scene < T , unknown > | null = null ; /** The ID of the node. Null if none. */ private id : string | null ; /** The horizontal position relative to parent node. */ private x : number ; /** The vertical position relative to parent node. */ private y : number ; /** The node width. */ private width : number ; /** The node height. */ private height : number ; /**
     * The anchor defining the origin of this scene node. When set to TOP_LEFT for example then the X/Y coordinates of
     * this node define where to display the upper left corner of it. When set to CENTER then the node is centered at
     * its X/Y coordinates.
     */ private anchor : Direction ; /**
     * The anchor of the local coordinate system. When set to CENTER for example then the X/Y coordinates or child
     * nodes are relative to the center of this node.
     */ private childAnchor : Direction ; /**
     * The bounds polygon. This is updated on demand and automatically invalidated when node size changes. Node
     * has to call [[invalidateBounds]] manually when something else influences the bounds.
     */ private boundsPolygon : Polygon2 = new Polygon2 ( ) ; /**
     * The transformation matrix of this node. This transformation is applied to the node before moving the node to
     * its position (X/Y coordinates). So in simple cases this transformation is not needed at all and its up to you
     * if you want to use the coordinates and/or the transformation matrix.
     */ private readonly transformation = new AffineTransform ( ) ; /**
     * The transformation matrix combining the nodes transformation with all the parent transformations. This is
     * calculated on-the-fly when a scene node is updated.
     */ private readonly sceneTransformation = new AffineTransform ( ) ; /** Array with currently active animations. Animations are automatically removed from the array when finished.*/ private readonly animations : SceneNodeAnimation < T > [ ] = [ ] ; /**
     * The current opacity of the node. 0.0 means fully transparent and 1.0 means fully opaque. Can be larger
     * than 1.0 to compensate transparency of its parent.
     */ private opacity : number ; /** Set to true to show bounds. Useful for debugging. */ private showBounds : boolean ; /**
     * The layer this node is drawn on. Internal representation is stored in `2^n` while setter and getter works
     * with `n` instead. This is because the layering system internally works with fast bit masks.
     */ private layer : number ; /**
     * Creates a new scene node with the given initial settings.
     */ public constructor ( { id = null , x = 0 , y = 0 , width = 0 , height = 0 , anchor = Direction . CENTER , childAnchor = Direction . CENTER , opacity = 1 , showBounds = false , layer = 0 } = { } ) { this . id = id ; this . x = x ; this . y = y ; this . width = width ; this . height = height ; this . opacity = opacity ; this . anchor = anchor ; this . childAnchor = childAnchor ; this . showBounds = showBounds ; this . layer = 1 << layer ; } return public getId ( ) { return this . id ; } param id public setId ( id ) { this . id = id ; return this ; } return public getX ( ) { return this . x ; } param x public setX ( x ) { if ( x !== this . x ) { this . x = x ; this . invalidate ( ) ; } return this ; } return public getY ( ) { return this . y ; } param y public setY ( y ) { if ( y !== this . y ) { this . y = y ; this . invalidate ( ) ; } return this ; } param x param y public moveBy ( x , y ) { if ( x !== 0 || y !== 0 ) { this . x += x ; this . y += y ; this . invalidate ( ) ; } return this ; } param x param y public moveTo ( x , y ) { if ( x !== this . x || y !== this . y ) { this . x = x ; this . y = y ; this . invalidate ( ) ; } return this ; } return public getWidth ( ) { return this . width ; } param width public setWidth ( width ) { if ( width !== this . width ) { this . width = width ; this . invalidate ( ) ; this . invalidateBounds ( ) ; } return this ; } return public getHeight ( ) { return this . height ; } param height public setHeight ( height ) { if ( height !== this . height ) { this . height = height ; this . invalidate ( ) ; this . invalidateBounds ( ) ; } return this ; } param width param height public resizeTo ( width , height ) { if ( width !== this . width || height !== this . height ) { this . width = width ; this . height = height ; this . invalidate ( ) ; this . invalidateBounds ( ) ; } return this ; } return public getOpacity ( ) { return this . opacity ; } pram public setOpacity ( opacity ) { opacity = Math . max ( 0 , opacity ) ; if ( opacity !== this . opacity ) { this . opacity = opacity ; this . invalidate ( ) ; } return this ; } return public getEffectiveOpacity ( ) { if ( this . opacity === Infinity ) { return 1 ; } return Math . max ( 0 , Math . min ( 1 , ( this . parent ?. getEffectiveOpacity ( ) ?? 1 ) * this . opacity ) ) ; } return public getAnchor ( ) { return this . anchor ; } param anchor public setAnchor ( anchor ) { if ( anchor !== this . anchor ) { this . anchor = anchor ; this . invalidate ( ) ; } return this ; } return public getChildAnchor ( ) { return this . childAnchor ; } param childAnchor public setChildAnchor ( childAnchor ) { if ( childAnchor !== this . childAnchor ) { this . childAnchor = childAnchor ; this . invalidate ( ) ; } return this ; } return public getTransformation ( ) { return this . transformation ; } param transformer public transform ( transformer ) { transformer ( this . transformation ) ; return this . invalidate ( ) ; } return public getScene ( ) { return this . scene ; } param scene private setScene ( scene ) { if ( scene !== this . scene ) { if ( this . scene ) { this . deactivate ( ) ; } this . scene = scene ; if ( scene ) { this . activate ( ) ; } this . forEachChild ( node => node . setScene ( scene ) ) ; } } /**
     * Called when node is added to scene. Can be overwritten to connect event handlers for example.
     */ protected activate ( ) { } /**
     * Called when node is removed from scene. Can be overwritten to disconnect event handlers for example.
     */ protected deactivate ( ) { } return public getParent ( ) { return this . parent ; } return public getNextSibling ( ) { return this . nextSibling ; } return public getPreviousSibling ( ) { return this . previousSibling ; } return public getFirstChild ( ) { return this . firstChild ; } return public getLastChild ( ) { return this . lastChild ; } return public hasChildNodes ( ) { return this . firstChild != null ; } param node public appendChild ( node ) { if ( node === this ) { throw new Error ( "Node can not be appended to itself" ) ; } const oldParent = node . parent ; if ( oldParent ) { oldParent . removeChild ( node ) ; } node . previousSibling = this . lastChild ; const oldLastChild = this . lastChild ; if ( oldLastChild ) { oldLastChild . nextSibling = node ; } this . lastChild = node ; if ( ! this . firstChild ) { this . firstChild = node ; } node . parent = this ; node . setScene ( this . scene ) ; node . invalidate ( ) ; this . invalidate ( ) ; return this ; } param node public prependChild ( node ) { if ( this . firstChild != null ) { return this . insertBefore ( node , this . firstChild ) ; } else { return this . appendChild ( node ) ; } } param child public removeChild ( node ) { if ( node . parent !== this ) { throw new Error ( "Node must be a child node" ) ; } const next = node . nextSibling ; const prev = node . previousSibling ; if ( next ) { next . previousSibling = prev ; } if ( prev ) { prev . nextSibling = next ; } if ( node === this . firstChild ) { this . firstChild = next ; } if ( node === this . lastChild ) { this . lastChild = prev ; } node . parent = null ; node . nextSibling = null ; node . previousSibling = null ; node . setScene ( null ) ; node . invalidate ( ) ; this . invalidate ( ) ; return this ; } /**
     * Removes this node from the scene. The node is then a detached node ready to be added to the scene (or some
     * other scene) again.
     */ public remove ( ) { if ( this . parent ) { this . parent . removeChild ( this ) ; } return this ; } /**
     * Removes all child nodes.
     */ public clear ( ) { while ( this . lastChild ) { this . removeChild ( this . lastChild ) ; } return this ; } param newNode param refNode public insertBefore ( newNode , refNode ) { if ( newNode === this ) { throw new Error ( "Node can not be inserted into itself" ) ; } if ( refNode . parent !== this ) { throw new Error ( "Reference node must be a child node" ) ; } const oldParent = newNode . parent ; if ( oldParent ) { oldParent . removeChild ( newNode ) ; } const oldPrevious = refNode . previousSibling ; if ( ! oldPrevious ) { this . firstChild = newNode ; } else { oldPrevious . nextSibling = newNode ; } refNode . previousSibling = newNode ; newNode . previousSibling = oldPrevious ; newNode . nextSibling = refNode ; newNode . parent = this ; newNode . setScene ( this . scene ) ; return this . invalidate ( ) ; } param oldNode param newNode public replaceChild ( oldNode , newNode ) { if ( newNode === this ) { throw new Error ( "newNode must not be the parent node" ) ; } if ( oldNode . parent !== this ) { throw new Error ( "oldNode must be a child node" ) ; } if ( newNode === oldNode ) { return this ; } const next = oldNode . nextSibling ; this . removeChild ( oldNode ) ; if ( next ) { this . insertBefore ( newNode , next ) ; } else { this . appendChild ( newNode ) ; } return this ; } param node public replace ( node ) { if ( this . parent ) { this . parent . replaceChild ( this , node ) ; } return this ; } param node public appendTo ( node ) { node . appendChild ( this ) ; return this ; } param node public prependTo ( node ) { node . prependChild ( this ) ; return this ; } param callback param thisArg public forEachChild ( callback , thisArg = this ) { let index = 0 ; let node = this . firstChild ; while ( node ) { const next = node . nextSibling ; callback . call ( thisArg , node , index ++ ) ; node = next ; } return this ; } param callback param thisArg public forEachDescendant ( callback , thisArg = this ) { let node = this . firstChild ; while ( node != null && node !== this ) { let next = node . firstChild ; if ( next == null ) { next = node . nextSibling ; } if ( next == null ) { next = node . parent ?. nextSibling ?? null ; } callback . call ( thisArg , node ) ; node = next ; } return this ; } param callback return public findChild ( callback , thisArg = this ) { let index = 0 ; let node = this . firstChild ; while ( node ) { const next = node . nextSibling ; if ( callback . call ( thisArg , node , index ++ ) ) { return node ; } node = next ; } return null ; } param callback return public findDescendant ( callback , thisArg = this ) { let node = this . firstChild ; while ( node != null && node !== this ) { let next = node . firstChild ; if ( next == null ) { next = node . nextSibling ; } if ( next == null ) { next = node . parent ?. nextSibling ?? null ; } if ( callback . call ( thisArg , node ) ) { return node ; } node = next ; } return null ; } param callback param thisArg return public someChildren ( callback , thisArg = this ) { let index = 0 ; let node = this . firstChild ; while ( node ) { const next = node . nextSibling ; if ( callback . call ( thisArg , node , index ++ ) ) { return true ; } node = next ; } return false ; } return public getChildren ( ) { const children = [ ] ; let node = this . firstChild ; while ( node ) { children . push ( node ) ; node = node . nextSibling ; } return children ; } param id return public getDescendantById ( id ) { return this . findDescendant ( node => node . getId ( ) === id ) ; } /**
     * Invalidates the bounds of the node. AUtomatically called when node size is changed. Must be called manually
     * when some other aspect of the node which may influence the bounds is changed.
     */ public invalidateBounds ( ) { this . boundsPolygon . clear ( ) ; return this ; } param bounds protected updateBoundsPolygon ( bounds ) { bounds . addVertex ( new Vector2 ( 0 , 0 ) ) ; bounds . addVertex ( new Vector2 ( this . width , 0 ) ) ; bounds . addVertex ( new Vector2 ( this . width , this . height ) ) ; bounds . addVertex ( new Vector2 ( 0 , this . height ) ) ; } return public getBoundsPolygon ( ) { if ( ! this . boundsPolygon . hasVertices ( ) ) { this . updateBoundsPolygon ( this . boundsPolygon ) ; } return this . boundsPolygon ; } /**
     * Marks this node, all parent nodes and the scene as invalid to trigger a scene revalidation. This must be
     * called every time when some aspect of the node is changed which requires a redraw of the scene node.
     *
     * TODO Not yet implemented, currently the scene is constantly redrawn.
     */ public invalidate ( ) { return this ; } param animationArgs public animate ( animationArgs ) { this . animations . push ( new SceneNodeAnimation ( this , animationArgs ) ) ; return this ; } /**
     * Finishes all currently running animations. This calls all animator functions with their last animation index
     * (1.0) and then removes the animations.
     */ public finishAnimations ( ) { for ( const animation of this . animations ) { animation . finish ( ) ; } return this . forEachChild ( child => child . finishAnimations ( ) ) ; } return public hasAnimations ( ) { return this . animations . length > 0 || this . someChildren ( child => child . hasAnimations ( ) ) ; } param showBounds public setShowBounds ( showBounds ) { if ( showBounds !== this . showBounds ) { this . showBounds = showBounds ; this . invalidate ( ) ; } return this ; } return public isShowBounds ( ) { return this . showBounds ; } return public getLayer ( ) { return Math . log2 ( this . layer ) ; } param layer public setLayer ( layer ) { if ( layer < 0 || layer > 31 ) { throw new Error ( `Valid layer range is 0-31 but was ${ layer }` ) ; } layer = 1 << layer ; if ( layer !== this . layer ) { this . layer = layer ; this . invalidate ( ) ; } return this ; } /**
     * Updates the animations and removes finished animations.
     */ private updateAnimations ( dt ) { const animations = this . animations ; let numAnimations = animations . length ; let i = 0 ; while ( i < numAnimations ) { if ( animations [ i ] . update ( dt ) ) { animations . splice ( i , 1 ) ; numAnimations -- ; } else { i ++ ; } } } param dt return protected updateAll ( dt ) { const postUpdate = this . update ( dt ) ; this . updateAnimations ( dt ) ; const parent = this . parent ; if ( parent != null ) { this . sceneTransformation . setMatrix ( parent . sceneTransformation ) ; this . sceneTransformation . translate ( ( Direction . getX ( parent . childAnchor ) + 1 ) / 2 * parent . width , ( Direction . getY ( parent . childAnchor ) + 1 ) / 2 * parent . height ) ; } else { this . sceneTransformation . reset ( ) ; } this . sceneTransformation . translate ( this . x , this . y ) ; this . sceneTransformation . mul ( this . transformation ) ; this . sceneTransformation . translate ( - ( Direction . getX ( this . anchor ) + 1 ) / 2 * this . width , - ( Direction . getY ( this . anchor ) + 1 ) / 2 * this . height ) ; const layers = this . updateChildren ( dt ) | this . layer ; if ( postUpdate != null ) { postUpdate ( ) ; } return layers ; } param dt return protected updateChildren ( dt ) { let layers = 0 ; this . forEachChild ( child => { layers |= child . updateAll ( dt ) ; } ) ; return layers ; } param dt return protected update ( dt ) { } param ctx protected drawBounds ( ctx ) { if ( this . showBounds ) { const bounds = this . getBoundsPolygon ( ) ; ctx . save ( ) ; this . sceneTransformation . setCanvasTransform ( ctx ) ; ctx . beginPath ( ) ; bounds . draw ( ctx ) ; ctx . clip ( ) ; ctx . save ( ) ; ctx . lineWidth = 2 ; ctx . strokeStyle = "red" ; ctx . stroke ( ) ; ctx . setLineDash ( [ 4 , 4 ] ) ; ctx . strokeStyle = "white" ; ctx . lineDashOffset = Math . round ( Date . now ( ) / 100 ) % 8 ; ctx . stroke ( ) ; ctx . restore ( ) ; ctx . restore ( ) ; } return this . forEachChild ( child => child . drawBounds ( ctx ) ) ; } param ctx param layer param width param height return protected drawAll ( ctx , layer , width , height ) { ctx . save ( ) ; ctx . globalAlpha *= this . getEffectiveOpacity ( ) ; ctx . translate ( this . x , this . y ) ; this . transformation . transformCanvas ( ctx ) ; ctx . translate ( - ( Direction . getX ( this . anchor ) + 1 ) / 2 * this . width , - ( Direction . getY ( this . anchor ) + 1 ) / 2 * this . height ) ; const postDraw = layer === this . layer ? this . draw ( ctx , width , height ) : null ; ctx . save ( ) ; ctx . translate ( ( Direction . getX ( this . childAnchor ) + 1 ) / 2 * this . width , ( Direction . getY ( this . childAnchor ) + 1 ) / 2 * this . height ) ; let flags = this . drawChildren ( ctx , layer , width , height ) ; ctx . restore ( ) ; if ( postDraw != null ) { if ( postDraw === true ) { flags |= PostDrawHints . CONTINUE_DRAWING ; } else if ( postDraw !== false ) { postDraw ( ) ; } } ctx . restore ( ) ; return this . showBounds ? flags | PostDrawHints . DRAW_BOUNDS | PostDrawHints . CONTINUE_DRAWING : flags ; } param ctx param layer param width param height return protected drawChildren ( ctx , layer , width , height ) { let flags = 0 ; this . forEachChild ( child => { flags |= child . drawAll ( ctx , layer , width , height ) ; } ) ; return flags ; } param ctx param width param height return protected draw ( ctx , width , height ) { } }