11import Base from '../../../src/core/Base.mjs' ;
22
33const
4- PRIMARY = '#3E63DD' ,
5- SECONDARY = '#8BA6FF' ,
6- HIGHLIGHT = '#40C4FF' ,
7- NODE_COUNT = 150 ,
8- STRIDE = 7 ; // x, y, vx, vy, radius, layer, parentId
4+ PRIMARY = '#3E63DD' ,
5+ SECONDARY = '#8BA6FF' ,
6+ HIGHLIGHT = '#40C4FF' ,
7+ NODE_COUNT = 150 ,
8+ NODE_STRIDE = 7 , // x, y, vx, vy, radius, layer, parentId
9+ AGENT_COUNT = 20 ,
10+ AGENT_STRIDE = 6 ; // x, y, vx, vy, targetIdx, state (0=seek, 1=scan)
911
1012/**
1113 * @summary SharedWorker renderer for the Portal Home "Neural Swarm" background.
1214 *
1315 * Handles the physics simulation and rendering loop for the 2.5D network visualization.
1416 * Uses a Zero-Allocation strategy (pre-allocated buffers) for high performance.
1517 *
16- * **Buffer Layout (Float32Array):**
18+ * **Node Buffer Layout (Float32Array):**
1719 * [x, y, vx, vy, radius, layer, parentId, ...]
1820 *
21+ * **Agent Buffer Layout (Float32Array):**
22+ * [x, y, vx, vy, targetIdx, state, ...]
23+ *
1924 * @class Portal.canvas.HomeCanvas
2025 * @extends Neo.core.Base
2126 * @singleton
@@ -49,6 +54,11 @@ class HomeCanvas extends Base {
4954 singleton : true
5055 }
5156
57+ /**
58+ * Pre-allocated buffer for agent data.
59+ * @member {Float32Array|null} agentBuffer=null
60+ */
61+ agentBuffer = null
5262 /**
5363 * @member {String|null} canvasId=null
5464 */
@@ -88,12 +98,72 @@ class HomeCanvas extends Base {
8898 */
8999 clearGraph ( ) {
90100 let me = this ;
91- me . context = null ;
92- me . canvasId = null ;
93- me . canvasSize = null ;
94- me . nodeBuffer = null ;
95- me . isPaused = false ;
96- me . gradients = { } ;
101+ me . context = null ;
102+ me . canvasId = null ;
103+ me . canvasSize = null ;
104+ me . nodeBuffer = null ;
105+ me . agentBuffer = null ;
106+ me . isPaused = false ;
107+ me . gradients = { } ;
108+ }
109+
110+ /**
111+ * Draws the autonomous agents (Seeker Drones).
112+ * @param {CanvasRenderingContext2D } ctx
113+ */
114+ drawAgents ( ctx ) {
115+ let me = this ;
116+
117+ if ( ! me . agentBuffer ) return ;
118+
119+ const
120+ buffer = me . agentBuffer ,
121+ count = AGENT_COUNT ;
122+
123+ ctx . strokeStyle = HIGHLIGHT ;
124+ ctx . fillStyle = '#FFFFFF' ;
125+ ctx . lineCap = 'round' ;
126+
127+ for ( let i = 0 ; i < count ; i ++ ) {
128+ let idx = i * AGENT_STRIDE ,
129+ x = buffer [ idx ] ,
130+ y = buffer [ idx + 1 ] ,
131+ vx = buffer [ idx + 2 ] ,
132+ vy = buffer [ idx + 3 ] ,
133+ state = buffer [ idx + 5 ] ;
134+
135+ // 1. Draw Trail (Motion Blur)
136+ // Length depends on speed
137+ let speed = Math . sqrt ( vx * vx + vy * vy ) ;
138+
139+ if ( speed > 0.1 ) {
140+ ctx . beginPath ( ) ;
141+ ctx . lineWidth = 2 ;
142+ ctx . globalAlpha = 0.6 ;
143+ // Trail extends opposite to velocity
144+ ctx . moveTo ( x , y ) ;
145+ ctx . lineTo ( x - vx * 4 , y - vy * 4 ) ;
146+ ctx . stroke ( ) ;
147+ }
148+
149+ // 2. Draw Head
150+ ctx . beginPath ( ) ;
151+ ctx . globalAlpha = state === 1 ? 1 : 0.8 ; // Brighten when scanning
152+ let radius = state === 1 ? 3 : 2 ;
153+ ctx . arc ( x , y , radius , 0 , Math . PI * 2 ) ;
154+ ctx . fill ( ) ;
155+
156+ // 3. Scan Effect (Ring)
157+ if ( state === 1 ) {
158+ ctx . beginPath ( ) ;
159+ ctx . lineWidth = 1 ;
160+ ctx . globalAlpha = 0.3 ;
161+ ctx . arc ( x , y , 8 + Math . sin ( me . time * 10 ) * 2 , 0 , Math . PI * 2 ) ;
162+ ctx . stroke ( ) ;
163+ }
164+ }
165+
166+ ctx . globalAlpha = 1 ;
97167 }
98168
99169 /**
@@ -142,13 +212,13 @@ class HomeCanvas extends Base {
142212 // 1. Draw Connections (behind nodes)
143213 // Optimization: Double loop, but limited by distance check.
144214 for ( let i = 0 ; i < count ; i ++ ) {
145- let idx = i * STRIDE ,
215+ let idx = i * NODE_STRIDE ,
146216 l1 = buffer [ idx + 5 ] ,
147217 pid1 = buffer [ idx + 6 ] ,
148218 p1 = getPos ( idx , l1 ) ;
149219
150220 for ( let j = i + 1 ; j < count ; j ++ ) {
151- let idx2 = j * STRIDE ,
221+ let idx2 = j * NODE_STRIDE ,
152222 l2 = buffer [ idx2 + 5 ] ,
153223 pid2 = buffer [ idx2 + 6 ] ;
154224
@@ -197,7 +267,7 @@ class HomeCanvas extends Base {
197267
198268 // 2. Draw Nodes
199269 for ( let i = 0 ; i < count ; i ++ ) {
200- let idx = i * STRIDE ,
270+ let idx = i * NODE_STRIDE ,
201271 radius = buffer [ idx + 4 ] ,
202272 layer = buffer [ idx + 5 ] ,
203273 parentId = buffer [ idx + 6 ] ,
@@ -235,6 +305,32 @@ class HomeCanvas extends Base {
235305 ctx . globalAlpha = 1 ;
236306 }
237307
308+ /**
309+ * Initializes the autonomous agents.
310+ * @param {Number } width
311+ * @param {Number } height
312+ */
313+ initAgents ( width , height ) {
314+ let me = this ;
315+
316+ if ( ! me . agentBuffer ) {
317+ me . agentBuffer = new Float32Array ( AGENT_COUNT * AGENT_STRIDE ) ;
318+ }
319+
320+ const buffer = me . agentBuffer ;
321+
322+ for ( let i = 0 ; i < AGENT_COUNT ; i ++ ) {
323+ let idx = i * AGENT_STRIDE ;
324+
325+ buffer [ idx ] = Math . random ( ) * width ; // x
326+ buffer [ idx + 1 ] = Math . random ( ) * height ; // y
327+ buffer [ idx + 2 ] = ( Math . random ( ) - 0.5 ) * 4 ; // vx (Fast!)
328+ buffer [ idx + 3 ] = ( Math . random ( ) - 0.5 ) * 4 ; // vy
329+ buffer [ idx + 4 ] = - 1 ; // targetIdx (none)
330+ buffer [ idx + 5 ] = 0 ; // state (moving)
331+ }
332+ }
333+
238334 /**
239335 * Initializes the canvas context.
240336 * @param {Object } opts
@@ -271,7 +367,7 @@ class HomeCanvas extends Base {
271367 let me = this ;
272368
273369 if ( ! me . nodeBuffer ) {
274- me . nodeBuffer = new Float32Array ( NODE_COUNT * STRIDE ) ;
370+ me . nodeBuffer = new Float32Array ( NODE_COUNT * NODE_STRIDE ) ;
275371 }
276372
277373 const
@@ -286,7 +382,7 @@ class HomeCanvas extends Base {
286382 let parentIndices = [ ] ;
287383
288384 for ( let i = 0 ; i < NODE_COUNT ; i ++ ) {
289- let idx = i * STRIDE ,
385+ let idx = i * NODE_STRIDE ,
290386 isParent = i < parentCount ;
291387
292388 if ( isParent ) parentIndices . push ( i ) ;
@@ -320,15 +416,15 @@ class HomeCanvas extends Base {
320416
321417 // 2. Assign Children to nearest Parent
322418 for ( let i = parentCount ; i < NODE_COUNT ; i ++ ) {
323- let idx = i * STRIDE ,
419+ let idx = i * NODE_STRIDE ,
324420 x = buffer [ idx ] ,
325421 y = buffer [ idx + 1 ] ,
326422 bestDist = Infinity ,
327423 bestParent = - 1 ;
328424
329425 // Find nearest parent
330426 for ( let pid of parentIndices ) {
331- let pIdx = pid * STRIDE ,
427+ let pIdx = pid * NODE_STRIDE ,
332428 px = buffer [ pIdx ] ,
333429 py = buffer [ pIdx + 1 ] ,
334430 dx = x - px ,
@@ -390,9 +486,15 @@ class HomeCanvas extends Base {
390486 if ( ! me . nodeBuffer ) {
391487 me . initNodes ( width , height ) ;
392488 }
489+
490+ // Auto-init agents if missing
491+ if ( ! me . agentBuffer ) {
492+ me . initAgents ( width , height ) ;
493+ }
393494
394- // Physics Step
495+ // Physics Steps
395496 me . updatePhysics ( width , height ) ;
497+ me . updateAgents ( width , height ) ;
396498
397499 ctx . clearRect ( 0 , 0 , width , height ) ;
398500
@@ -404,10 +506,113 @@ class HomeCanvas extends Base {
404506
405507 // Draw Network
406508 me . drawNetwork ( ctx , width , height ) ;
509+
510+ // Draw Agents
511+ me . drawAgents ( ctx ) ;
407512
408513 setTimeout ( me . renderLoop , 1000 / 60 )
409514 }
410515
516+ /**
517+ * Updates agent positions (Boids + Seek).
518+ * @param {Number } width
519+ * @param {Number } height
520+ */
521+ updateAgents ( width , height ) {
522+ let me = this ;
523+
524+ if ( ! me . agentBuffer || ! me . nodeBuffer ) return ;
525+
526+ const
527+ agents = me . agentBuffer ,
528+ nodes = me . nodeBuffer ,
529+ count = AGENT_COUNT ,
530+ mx = me . mouse . x ,
531+ my = me . mouse . y ;
532+
533+ for ( let i = 0 ; i < count ; i ++ ) {
534+ let idx = i * AGENT_STRIDE ,
535+ targetIdx = agents [ idx + 4 ] ,
536+ state = agents [ idx + 5 ] ;
537+
538+ // --- Behavior 1: Pick a Target ---
539+ if ( targetIdx === - 1 || Math . random ( ) < 0.005 ) { // 0.5% chance to retarget randomly
540+ // Pick a random Cluster Center (Parent)
541+ const parentCount = Math . floor ( NODE_COUNT * 0.1 ) ;
542+ agents [ idx + 4 ] = Math . floor ( Math . random ( ) * parentCount ) ;
543+ targetIdx = agents [ idx + 4 ] ;
544+ agents [ idx + 5 ] = 0 ; // Set to moving
545+ }
546+
547+ // --- Behavior 2: Seek Target ---
548+ if ( targetIdx !== - 1 && state === 0 ) {
549+ let nIdx = targetIdx * NODE_STRIDE ,
550+ tx = nodes [ nIdx ] ,
551+ ty = nodes [ nIdx + 1 ] ,
552+ dx = tx - agents [ idx ] ,
553+ dy = ty - agents [ idx + 1 ] ,
554+ dist = Math . sqrt ( dx * dx + dy * dy ) ;
555+
556+ if ( dist < 10 ) {
557+ // Arrived! Scan.
558+ agents [ idx + 5 ] = 1 ; // Scan state
559+ agents [ idx + 2 ] *= 0.1 ; // Slow down
560+ agents [ idx + 3 ] *= 0.1 ;
561+ } else {
562+ // Steer towards target
563+ let force = 0.05 ; // Steering force
564+ agents [ idx + 2 ] += ( dx / dist ) * force ;
565+ agents [ idx + 3 ] += ( dy / dist ) * force ;
566+ }
567+ } else if ( state === 1 ) {
568+ // Scanning (hovering)
569+ // Randomly leave after a while
570+ if ( Math . random ( ) < 0.02 ) {
571+ agents [ idx + 4 ] = - 1 ; // Reset target
572+ agents [ idx + 5 ] = 0 ; // Move
573+ // Boost speed
574+ agents [ idx + 2 ] += ( Math . random ( ) - 0.5 ) * 4 ;
575+ agents [ idx + 3 ] += ( Math . random ( ) - 0.5 ) * 4 ;
576+ }
577+ }
578+
579+ // --- Behavior 3: Mouse Repulsion (High Priority) ---
580+ if ( mx !== - 1000 ) {
581+ let dx = agents [ idx ] - mx ,
582+ dy = agents [ idx + 1 ] - my ,
583+ distSq = dx * dx + dy * dy ;
584+
585+ if ( distSq < 10000 ) { // 100px radius
586+ let dist = Math . sqrt ( distSq ) ,
587+ force = ( 100 - dist ) / 100 ;
588+
589+ // Strong push
590+ agents [ idx + 2 ] += ( dx / dist ) * force * 1.5 ;
591+ agents [ idx + 3 ] += ( dy / dist ) * force * 1.5 ;
592+ agents [ idx + 5 ] = 0 ; // Stop scanning if scared
593+ }
594+ }
595+
596+ // --- Physics Update ---
597+ // Speed Limit
598+ let speed = Math . sqrt ( agents [ idx + 2 ] ** 2 + agents [ idx + 3 ] ** 2 ) ;
599+ if ( speed > 4 ) {
600+ agents [ idx + 2 ] *= 4 / speed ;
601+ agents [ idx + 3 ] *= 4 / speed ;
602+ }
603+
604+ // Move
605+ agents [ idx ] += agents [ idx + 2 ] ;
606+ agents [ idx + 1 ] += agents [ idx + 3 ] ;
607+
608+ // Wrap around
609+ if ( agents [ idx ] < 0 ) agents [ idx ] = width ;
610+ if ( agents [ idx ] > width ) agents [ idx ] = 0 ;
611+ if ( agents [ idx + 1 ] < 0 ) agents [ idx + 1 ] = height ;
612+ if ( agents [ idx + 1 ] > height ) agents [ idx + 1 ] = 0 ;
613+ }
614+ }
615+
411616 /**
412617 * @param {Object } data
413618 * @param {Boolean } [data.leave]
@@ -442,13 +647,13 @@ class HomeCanvas extends Base {
442647 my = me . mouse . y ;
443648
444649 for ( let i = 0 ; i < NODE_COUNT ; i ++ ) {
445- let idx = i * STRIDE ,
650+ let idx = i * NODE_STRIDE ,
446651 parentId = buffer [ idx + 6 ] ,
447652 isParent = parentId === - 1 ;
448653
449654 // 1. Cluster Cohesion (Children stick to Parent)
450655 if ( ! isParent ) {
451- let pIdx = parentId * STRIDE ,
656+ let pIdx = parentId * NODE_STRIDE ,
452657 px = buffer [ pIdx ] ,
453658 py = buffer [ pIdx + 1 ] ,
454659 dx = px - buffer [ idx ] ,
@@ -533,11 +738,14 @@ class HomeCanvas extends Base {
533738 if ( me . context ) {
534739 me . context . canvas . width = size . width ;
535740 me . context . canvas . height = size . height ;
536- // Re-init on significant resize? For now just re-init nodes to fix layout
537- me . initNodes ( size . width , size . height ) ;
741+ // Re-distribute nodes on significant resize to fill space?
742+ // For now, let them drift naturally or re-init if 0
743+ if ( ! me . nodeBuffer ) {
744+ me . initNodes ( size . width , size . height ) ;
745+ }
538746 me . updateResources ( size . width , size . height ) ;
539747 }
540748 }
541749}
542750
543- export default Neo . setupClass ( HomeCanvas ) ;
751+ export default Neo . setupClass ( HomeCanvas ) ;
0 commit comments