import Random from '../../../../shared/lib/vendor/random' ; import * as createjs from '@createjs/easeljs' ; import * as d3 from 'd3-random' ; const { customElement , property , observe } = Polymer . decorators ; const fooMap = new WeakMap ( ) ; type DIRECTION = 'up' | 'down' | 'left' | 'right' ; customElement polymer @ customElement ( 'atom-tronlines' ) export default class AtomTronlinesElement extends Polymer . Element { static BLOCK_SIZE = 50 ; static WARNING_THRESHOLD = 500 ; /**
     * The width of the canvas.
     */ @ property ( { type : Number } ) width = 450 ; /**
     * The height of the canvas.
     */ @ property ( { type : Number } ) height = 300 ; @ property ( { type : Boolean , computed : '_computeInvertDimensions(direction)' } ) _invertDimensions : boolean ; /**
     * The solid background color of the canvas.
     */ @ property ( { type : String , observer : AtomTronlinesElement . prototype . _backgroundColorChanged } ) backgroundColor = '#051113' ; /**
     * The direction of travel for the nodes.
     * Can be one of "up", "down", "left", or "right".
     */ @ property ( { type : String , reflectToAttribute : true } ) direction : DIRECTION = 'up' ; /**
     * The width and height of each node, in pixels.
     */ @ property ( { type : Number } ) nodeSize = 2 ; /**
     * Nodes created per second.
     */ @ property ( { type : Number , observer : AtomTronlinesElement . prototype . _creationRateChanged } ) creationRate = 20 ; /**
     * Expected distance traveled per frame, in pixels.
     * This is the "mu" value of the normal distribution.
     */ @ property ( { type : Number } ) speed = 1.5 ; /**
     * Variance in speed per node.
     * This is the "sigma" of the normal distribution.
     */ @ property ( { type : Number } ) speedRandomness = 0.25 ; /**
     * Expected distance tail length, in pixels.
     * This is the "mu" value of the normal distribution.
     */ @ property ( { type : Number } ) tailLength = 200 ; /**
     * Variance in tail length per node.
     * This is the "sigma" of the normal distribution.
     */ @ property ( { type : Number } ) tailLengthRandomness = 5 ; /**
     * The opacity of each node at the start of its path.
     */ @ property ( { type : Number } ) opacityStart = 0.5 ; /**
     * The opacity of each node at the end of its path.
     */ @ property ( { type : Number } ) opacityEnd = 0.2 ; /**
     * The color of the head of each node.
     */ @ property ( { type : String } ) nodeColor = '#abd3e9' ; /**
     * The starting color of the tail of each node.
     */ @ property ( { type : String } ) tailStartColor = '#abd3e9' ; /**
     * The ending color of the tail of each node.
     */ @ property ( { type : String } ) tailEndColor = '#12383c' ; @ property ( { type : Boolean , reflectToAttribute : true } ) debug = false ; /**
     * An array containing all nodes currently being drawn to the stage.
     */ _allocatedNodes = new Set ( ) ; /**
     * An array containing all nodes currently unallocated.
     */ _freeNodes = new Set ( ) ; @ property ( { type : Object , computed : '_computeRandomSpeedFunc(speed, speedRandomness)' } ) _getRandomSpeed : Function ; @ property ( { type : Object , computed : '_computeRandomTailLengthFunc(tailLength, tailLengthRandomness)' } ) _getRandomTailLength : Function ; private bgFillCommand : any ; private bgRectCommand : any ; private stage : createjs . Stage ; private _creationInterval ? : number ; static getRandomUniform ( min = 0 , max = 1 ) { return Random . real ( min , max , true ) ( Random . engines . browserCrypto ) ; } ready ( ) { super . ready ( ) ; let frameCounter = 0 ; let warnedLeak = false ; const stage = new createjs . Stage ( this . $ . canvas ) ; const bg = new createjs . Shape ( ) ; this . bgFillCommand = bg . graphics . beginFill ( this . backgroundColor ) . command ; this . bgRectCommand = bg . graphics . drawRect ( 0 , 0 , this . width , this . height ) . command ; stage . addChild ( bg ) ; this . stage = stage ; const handleFrame = ( ) => { if ( ( window as any ) . __SCREENSHOT_TESTING__ ) { this . clear ( ) ; stage . update ( ) ; return ; } this . advanceSimulation ( ) ; if ( this . debug ) { const totalNodes = this . _allocatedNodes . size + this . _freeNodes . size ; this . $ . debugInfo . textContent = `${ this . _allocatedNodes . size }/${ totalNodes }` ; } frameCounter ++ ; if ( frameCounter > 60 ) { frameCounter = 0 ; if ( this . _allocatedNodes . size > AtomTronlinesElement . WARNING_THRESHOLD ) { if ( ! warnedLeak ) { console . warn ( 'More than %d nodes are active, this is probably a leak!' , AtomTronlinesElement . WARNING_THRESHOLD , this ) ; warnedLeak = true ; } } else { warnedLeak = false ; } } stage . update ( ) ; requestAnimationFrame ( handleFrame ) ; } ; handleFrame ( ) ; setInterval ( ( ) => { this . _sweepExcessFreeNodes ( ) ; } , 10000 ) ; } /**
     * Advances the simulation by one tick.
     * In most cases, this means one frame in a 60fps simulation.
     */ advanceSimulation ( ) { const opacityRange = Math . abs ( this . opacityStart - this . opacityEnd ) ; const tickTime = Date . now ( ) ; const TIME_PER_TICK_IDEAL = 1000 / 60 ; Array . from ( this . _allocatedNodes ) . forEach ( ( node ) => { const metadata = fooMap . get ( node ) ; let percent = 1 ; if ( metadata . lastTickTime ) { percent = ( tickTime - metadata . lastTickTime ) / TIME_PER_TICK_IDEAL ; } node . y -= metadata . speed * percent ; const journeyPercentage = 1 - ( node . y / ( this . _invertDimensions ? this . width : this . height ) ) ; node . alpha = this . opacityStart - ( opacityRange * journeyPercentage ) ; metadata . lastTickTime = tickTime ; if ( node . alpha <= 0 || ( node . y + metadata . tailLength ) <= 0 ) { this . _freeNode ( node ) ; } } ) ; } param deep clear ( deep ? ) { this . _freeAllNodes ( ) ; if ( deep ) { this . _freeNodes = new Set ( ) ; } } _creationRateChanged ( newVal ) { if ( this . _creationInterval ) { clearInterval ( this . _creationInterval ) ; } this . _creationInterval = window . setInterval ( ( ) => { if ( this . _freeNodes . size <= 0 ) { this . _createBlockOfFreeNodes ( AtomTronlinesElement . BLOCK_SIZE ) ; } const node = this . _freeNodes . values ( ) . next ( ) . value ; this . _allocateNode ( node ) ; } , ( 1000 / newVal ) ) ; } param blockSize _createBlockOfFreeNodes ( blockSize ) { for ( let i = 0 ; i < blockSize ; i ++ ) { this . _freeNodes . add ( this . _createNode ( ) ) ; } } returns _createNode ( ) { const shape = new createjs . Shape ( ) ; const maxTailLength = this . tailLength + this . tailLengthRandomness ; const tailMidColor = ( createjs as any ) . Graphics . getRGB ( parseInt ( this . tailEndColor . slice ( 1 ) , 16 ) , 0.5 ) ; const tailEndColor = ( createjs as any ) . Graphics . getRGB ( parseInt ( this . tailEndColor . slice ( 1 ) , 16 ) , 0 ) ; const metadata = { tailGradientCommand : shape . graphics . beginLinearGradientFill ( [ this . tailStartColor , tailMidColor , tailEndColor ] , [ 0 , 0.5 , 1 ] , 0 , 0 , 0 , maxTailLength ) . command , tailRectCommand : shape . graphics . drawRect ( 0 , 0 , this . nodeSize , 0 ) . command , tailLength : 0 , speed : 0 , lastTickTime : null } ; fooMap . set ( shape , metadata ) ; shape . graphics . beginFill ( this . nodeColor ) . drawRect ( 0 , 0 , this . nodeSize , this . nodeSize ) ; shape . cache ( 0 , 0 , this . nodeSize , maxTailLength ) ; return shape ; } param node _allocateNode ( node ) { const tailLength = this . _getRandomTailLength ( ) ; const metadata = fooMap . get ( node ) ; metadata . tailGradientCommand . style . props . ratios [ 0 ] = Math . min ( this . nodeSize / this . tailLength , 1 ) ; metadata . tailGradientCommand . style . props . y1 = tailLength ; metadata . tailRectCommand . h = tailLength ; metadata . tailLength = tailLength ; metadata . speed = this . _getRandomSpeed ( ) ; metadata . lastTickTime = null ; node . updateCache ( ) ; node . alpha = this . opacityStart ; node . y = this . _invertDimensions ? this . width : this . height ; node . x = AtomTronlinesElement . getRandomUniform ( 0 , this . _invertDimensions ? this . height : this . width ) ; this . stage . addChild ( node ) ; this . _freeNodes . delete ( node ) ; this . _allocatedNodes . add ( node ) ; } /**
     * Removes all nodes from the stage, returning them to the pool.
     */ _freeAllNodes ( ) { this . _allocatedNodes . forEach ( node => { this . _freeNode ( node ) ; } ) ; } param node _freeNode ( node ) { this . stage . removeChild ( node ) ; this . _allocatedNodes . delete ( node ) ; this . _freeNodes . add ( node ) ; } /**
     * Removes excess free nodes.
     * Excess free nodes are caused by tabbing away from the page,
     * or after lowering the node creation rate.
     */ _sweepExcessFreeNodes ( ) { if ( this . _freeNodes . size > AtomTronlinesElement . BLOCK_SIZE * 2 ) { const freeNodesToKeep = Array . from ( this . _freeNodes ) . slice ( 0 , AtomTronlinesElement . BLOCK_SIZE ) ; this . _freeNodes = new Set ( freeNodesToKeep ) ; } } _computeRandomSpeedFunc ( speed , speedRandomness ) { return d3 . randomNormal . source ( AtomTronlinesElement . getRandomUniform ) ( speed , speedRandomness ) ; } _computeRandomTailLengthFunc ( tailLength , tailLengthRandomness ) { return d3 . randomNormal . source ( AtomTronlinesElement . getRandomUniform ) ( tailLength , tailLengthRandomness ) ; } @ observe ( 'width' , 'height' , 'direction' ) _resizeCanvas ( width , height , direction ) { this . style . width = `${ width }px` ; this . style . height = `${ height }px` ; if ( direction === 'left' || direction === 'right' ) { const temp = width ; width = height ; height = temp ; } ( this . $ . canvas as HTMLCanvasElement ) . width = width ; ( this . $ . canvas as HTMLCanvasElement ) . height = height ; if ( this . bgRectCommand ) { this . bgRectCommand . w = width ; this . bgRectCommand . h = height ; } } _computeInvertDimensions ( direction ) { return direction === 'left' || direction === 'right' ; } _backgroundColorChanged ( newValue ) { if ( ! this . bgFillCommand ) { return ; } this . bgFillCommand . style = newValue ; } }