Skip to content

Commit 2ec5ff7

Browse files
committed
feat: Implement Autonomous Agent Drones (Boids) for Neural Swarm (#8669)
1 parent 50230c8 commit 2ec5ff7

1 file changed

Lines changed: 233 additions & 25 deletions

File tree

apps/portal/canvas/HomeCanvas.mjs

Lines changed: 233 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,26 @@
11
import Base from '../../../src/core/Base.mjs';
22

33
const
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

Comments
 (0)