@@ -19,8 +19,11 @@ let curStats = {envs: 0, viewers: 0};
1919let backgroundSharp = null ;
2020let backgroundSmooth = null ;
2121
22- // animate each batch of updates for 12 seconds
23- const animationDuration = 30000 ;
22+ // Default animation duration - will be adjusted based on batch timing
23+ const defaultAnimationDuration = 30000 ;
24+
25+ // Sprite cache: Maps "user-envID" to sprite state
26+ const spriteCache = new Map ( ) ;
2427
2528const container = new PIXI . Container ( ) ;
2629// scale and center container initially
@@ -49,13 +52,27 @@ function smoothstep(min, max, value) {
4952
5053let userFilter = new RegExp ( "" ) ;
5154let activeSprites = [ ] ;
55+
56+ // Helper function to calculate exponential moving average
57+ function updateMovingAverage ( currentAvg , newValue , alpha = 0.3 ) {
58+ if ( currentAvg === null ) return newValue ;
59+ return alpha * newValue + ( 1 - alpha ) * currentAvg ;
60+ }
61+
62+ // Helper function to get cache key
63+ function getCacheKey ( meta ) {
64+ const envID = meta . env_id !== undefined ? meta . env_id : "default" ;
65+ return `${ meta . user } -${ envID } ` ;
66+ }
67+
5268function setUserFilter ( value ) {
5369 userFilter = new RegExp ( value ) ;
5470 activeSprites . forEach ( obj => {
5571 container . removeChild ( obj . subContainer ) ; // Remove sprite from the scene
5672 obj . subContainer . destroy ( { children : true } ) ; // Optional: frees up memory used by the sprite
5773 } ) ;
58- activeSprites = [ ]
74+ activeSprites = [ ] ;
75+ spriteCache . clear ( ) ; // Clear the cache as well
5976}
6077
6178app . view . addEventListener ( 'wheel' , ( e ) => {
@@ -258,7 +275,7 @@ PIXI.Assets.load([
258275 const path = data [ "coords" ] ;
259276 const meta = data [ "metadata" ] ;
260277 console . log ( meta ) ;
261- if ( Date . now ( ) - lastFrameTime < 2 * animationDuration ) {
278+ if ( Date . now ( ) - lastFrameTime < 2 * defaultAnimationDuration ) {
262279 startAnimationForPath ( path , meta ) ;
263280 }
264281 }
@@ -279,8 +296,6 @@ PIXI.Assets.load([
279296 // Refresh WebSocket connection every 2 minutes (120000 milliseconds)
280297 setInterval ( refreshWS , 120000 ) ;
281298
282-
283-
284299 let baseTextureChar = new PIXI . BaseTexture ( "assets/characters_transparent.png" , {
285300 scaleMode : PIXI . SCALE_MODES . NEAREST ,
286301 } ) ;
@@ -304,51 +319,106 @@ PIXI.Assets.load([
304319
305320 // Check if meta is defined and has a 'user' key
306321 if ( meta && meta . user !== undefined && typeof ( meta . user ) === "string" ) {
307- // Create a text label
308322 const envID = meta . env_id !== undefined ? `-${ meta . env_id } ` : "" ;
309323 const extraInfo = meta . extra !== undefined ? ` ${ meta . extra } ` : "" ;
310324 const color = ( meta . color && CSS . supports ( 'color' , meta . color ) ) ? meta . color : "0x000000" ;
311325
312326 const labelText = meta . user + envID + extraInfo ;
313327 if ( userFilter . exec ( labelText ) !== null ) {
328+ const cacheKey = getCacheKey ( meta ) ;
329+ const currentTime = Date . now ( ) ;
330+
314331 let spriteIdx = 0 ;
315332 if ( meta . sprite_id !== undefined ) {
316333 let parsed = parseInt ( meta . sprite_id , 10 ) ;
317334 if ( ! isNaN ( parsed ) && parsed > 0 && parsed < 50 ) {
318335 spriteIdx = parsed ;
319336 }
320337 }
321- const sprite = new PIXI . Sprite ( textureCharsDown [ spriteIdx ] ) ;
322- //sprite.x = charOffset * 40;
323- sprite . anchor . set ( 0.5 ) ;
324- //sprite.scale.set(0.5); // Adjust scale as needed
325- const subContainer = new PIXI . Container ( ) ;
326-
327- subContainer . addChild ( sprite ) ;
328- const label = new PIXI . Text (
329- labelText ,
330- {
331- fontFamily : 'Arial' ,
332- fontSize : 14 ,
333- fill : color ,
334- align : 'center' ,
335- } ) ;
336- label . x = sprite . x + sprite . width * 0.5 ; // Position the label next to the sprite
337- label . y -= sprite . height ; // Adjust the label position as needed
338- subContainer . addChild ( label ) ;
339- container . addChild ( subContainer ) ;
340-
341- activeSprites . push ( { subContainer, sprite, spriteIdx, path, startTime : null } ) ;
338+
339+ // Check if sprite already exists in cache
340+ if ( spriteCache . has ( cacheKey ) ) {
341+ const cached = spriteCache . get ( cacheKey ) ;
342+
343+ // Calculate time since last update
344+ const timeSinceLastUpdate = currentTime - cached . lastUpdateTime ;
345+
346+ // Update moving average of batch timing
347+ cached . batchTimingAvg = updateMovingAverage ( cached . batchTimingAvg , timeSinceLastUpdate ) ;
348+
349+ // Update the path and reset animation with dynamic duration
350+ cached . path = path ;
351+ cached . startTime = null ; // Reset to restart animation
352+ cached . lastUpdateTime = currentTime ;
353+ cached . animationDuration = Math . max ( 1000 , cached . batchTimingAvg * 0.95 ) ; // 95% of avg to finish slightly early
354+
355+ // Update sprite index if it changed, preserving current direction
356+ if ( cached . spriteIdx !== spriteIdx ) {
357+ cached . spriteIdx = spriteIdx ;
358+ // Update texture with new sprite ID but maintain direction
359+ const directionTextures = {
360+ 'down' : textureCharsDown ,
361+ 'up' : textureCharsUp ,
362+ 'left' : textureCharsLeft ,
363+ 'right' : textureCharsRight
364+ } ;
365+ cached . sprite . texture = directionTextures [ cached . currentDirection ] [ spriteIdx ] ;
366+ }
367+
368+ // Update label text and color if changed
369+ if ( cached . label ) {
370+ cached . label . text = labelText ;
371+ cached . label . style . fill = color ;
372+ }
373+ } else {
374+ // Create new sprite
375+ const sprite = new PIXI . Sprite ( textureCharsDown [ spriteIdx ] ) ;
376+ sprite . anchor . set ( 0.5 ) ;
377+ const subContainer = new PIXI . Container ( ) ;
378+
379+ subContainer . addChild ( sprite ) ;
380+ const label = new PIXI . Text (
381+ labelText ,
382+ {
383+ fontFamily : 'Arial' ,
384+ fontSize : 14 ,
385+ fill : color ,
386+ align : 'center' ,
387+ } ) ;
388+ label . x = sprite . x + sprite . width * 0.5 ;
389+ label . y -= sprite . height ;
390+ subContainer . addChild ( label ) ;
391+ container . addChild ( subContainer ) ;
392+
393+ // Initialize sprite in cache
394+ const newCachedSprite = {
395+ subContainer,
396+ sprite,
397+ label,
398+ spriteIdx,
399+ path,
400+ startTime : null ,
401+ lastUpdateTime : currentTime ,
402+ batchTimingAvg : null , // Will be set on second update
403+ animationDuration : defaultAnimationDuration ,
404+ currentDirection : 'down'
405+ } ;
406+
407+ spriteCache . set ( cacheKey , newCachedSprite ) ;
408+ activeSprites . push ( newCachedSprite ) ;
409+ }
342410 }
343411 }
344412
345413 }
346414
347415 function animate ( time ) {
416+ const currentTime = Date . now ( ) ;
417+
348418 activeSprites . forEach ( obj => {
349419 if ( ! obj . startTime ) obj . startTime = time ;
350420 const timeDelta = time - obj . startTime ;
351- const progress = Math . min ( timeDelta / animationDuration , 1 ) ;
421+ const progress = Math . min ( timeDelta / obj . animationDuration , 1 ) ;
352422
353423 // Calculate the current position
354424 const currentIndex = Math . floor ( progress * ( obj . path . length - 1 ) ) ;
@@ -365,33 +435,51 @@ PIXI.Assets.load([
365435 const dx = nextPoint [ 0 ] - currentPoint [ 0 ] ;
366436 const dy = nextPoint [ 1 ] - currentPoint [ 1 ] ;
367437
368- // Determine which direction is dominant
369- if ( Math . abs ( dx ) > Math . abs ( dy ) ) {
370- // Horizontal movement is dominant
371- if ( dx > 0 ) {
372- obj . sprite . texture = textureCharsRight [ obj . spriteIdx ] ;
373- } else {
374- obj . sprite . texture = textureCharsLeft [ obj . spriteIdx ] ;
375- }
376- } else {
377- // Vertical movement is dominant
378- if ( dy > 0 ) {
379- obj . sprite . texture = textureCharsDown [ obj . spriteIdx ] ;
438+ // Only update direction if there's actual movement
439+ if ( dx !== 0 || dy !== 0 ) {
440+ // Determine which direction is dominant
441+ let newDirection = obj . currentDirection ;
442+ if ( Math . abs ( dx ) > Math . abs ( dy ) ) {
443+ // Horizontal movement is dominant
444+ if ( dx > 0 ) {
445+ obj . sprite . texture = textureCharsRight [ obj . spriteIdx ] ;
446+ newDirection = 'right' ;
447+ } else {
448+ obj . sprite . texture = textureCharsLeft [ obj . spriteIdx ] ;
449+ newDirection = 'left' ;
450+ }
380451 } else {
381- obj . sprite . texture = textureCharsUp [ obj . spriteIdx ] ;
452+ // Vertical movement is dominant
453+ if ( dy > 0 ) {
454+ obj . sprite . texture = textureCharsDown [ obj . spriteIdx ] ;
455+ newDirection = 'down' ;
456+ } else {
457+ obj . sprite . texture = textureCharsUp [ obj . spriteIdx ] ;
458+ newDirection = 'up' ;
459+ }
382460 }
461+ obj . currentDirection = newDirection ;
383462 }
384463 }
464+ } ) ;
385465
386- if ( progress >= 1 ) {
387- container . removeChild ( obj . subContainer ) ; // Remove sprite from the scene
388- obj . subContainer . destroy ( { children : true } ) ; // Optional: frees up memory used by the sprite
389- }
466+ // Clean up stale sprites
467+ const staleThreshold = 120000 ; // 2 minutes
468+ const keysToRemove = [ ] ;
390469
470+ spriteCache . forEach ( ( cached , key ) => {
471+ if ( currentTime - cached . lastUpdateTime > staleThreshold ) {
472+ container . removeChild ( cached . subContainer ) ;
473+ cached . subContainer . destroy ( { children : true } ) ;
474+ keysToRemove . push ( key ) ;
475+ }
391476 } ) ;
392477
393- // Remove sprites that have completed their animation
394- activeSprites = activeSprites . filter ( obj => ( time - obj . startTime ) < animationDuration ) ;
478+ keysToRemove . forEach ( key => spriteCache . delete ( key ) ) ;
479+
480+ // Update activeSprites to match spriteCache
481+ activeSprites = Array . from ( spriteCache . values ( ) ) ;
482+
395483 lastFrameTime = Date . now ( ) ;
396484 requestAnimationFrame ( animate ) ;
397485 }
0 commit comments