DEV Community

Cover image for Building Pixel Council: Orchestrating AI Agents as Animated Pixel Art Characters on a Spatial Canvas
Harish Kotra (he/him)
Harish Kotra (he/him)

Posted on

Building Pixel Council: Orchestrating AI Agents as Animated Pixel Art Characters on a Spatial Canvas

As multi-agent LLM workflows become increasingly popular, the way we interface with AI agents has remained stuck in 2D chat threads. Chat UI tabs struggle when you need to visualize multiple agents working concurrently, passing context back and forth, or running asynchronous background tasks.

Pixel Council flips this mental model on its head. It treats AI agents as spatial entities placed on an infinite 2D canvas, represented by custom-generated, animated pixel-art avatars. With custom Animated Spark Edges, agents pass data packets visually across connection lines, triggering downstream execution pipelines while maintaining a real-time Task & Flow Audit Log.

In this technical deep-dive, we'll examine how Pixel Council was built using React 18, @xyflow/react, PixiJS v8, DiceBear Pixel-Art, Zustand, and IndexedDB.


Architectural Overview

Pixel Council combines high-performance 2D WebGL canvas graphics with React's component state, custom SVG motion paths, and asynchronous streaming data flows.

                  +-----------------------------------+
                  |        React Flow Canvas          |
                  |  (Spatial Node Placement & Edges) |
                  +-----------------+-----------------+
                                    |
            +-----------------------+-----------------------+
            |                       |                       |
            v                       v                       v
+-----------------------+ +-----------------------+ +-----------------------+
|      Agent Node       | |   Animated Spark Edge | |      Output Node      |
| - PixiJS v8 Container | | - SVG animateMotion   | | - React Markdown      |
| - DiceBear Avatar     | | - Spark Particle Loop | | - Real-time Streaming |
| - Ticker Animation    | | - Manual/Auto Trigger | | - Code Highlight     |
+-----------+-----------+ +-----------+-----------+ +-----------------------+
            |                       |
            +-----------+-----------+
                        |
                        v
          +---------------------------+
          |    Sidebar & Audit Log    |
          | - Task & Data Flow Log    |
          | - Real-time Search/Filter |
          | - Inspector & Duplication |
          +-------------+-------------+
                        |
                        v
          +---------------------------+
          |   Zustand + IndexedDB     |
          | - Persistent State Engine |
          | - Agent Credentials       |
          | - Audit History Array     |
          +---------------------------+
Enter fullscreen mode Exit fullscreen mode

Core Technical Implementation Highlights

1. Custom Animated Spark Edges & Inter-Agent Pipeline Signals

Agents on the canvas pass context and output data to downstream agents or output notes using custom React Flow edge components. SVG animateMotion renders glowing trail particles (#F4A261 and #D95D39) when a data packet is dispatched.

// Excerpt from src/components/canvas/edges/AnimatedSparkEdge.tsx
import { BaseEdge, EdgeLabelRenderer, EdgeProps, getSmoothStepPath } from '@xyflow/react';
import { Zap } from 'lucide-react';
import { useStore } from '../../../store/useStore';

export default function AnimatedSparkEdge({
  id,
  sourceX,
  sourceY,
  targetX,
  targetY,
  sourcePosition,
  targetPosition,
  style = {},
  markerEnd,
  label,
  selected,
}: EdgeProps) {
  const [edgePath, labelX, labelY] = getSmoothStepPath({
    sourceX, sourceY, sourcePosition, targetX, targetY, targetPosition,
  });

  const { activeSignals, triggerSignal } = useStore();
  const isSignalActive = activeSignals ? activeSignals.some((s) => s.connectionId === id) : false;

  return (
    <>
      <BaseEdge
        id={id}
        path={edgePath}
        markerEnd={markerEnd}
        style={{
          ...style,
          stroke: isSignalActive ? '#F4A261' : selected ? '#D95D39' : '#A8C686',
          strokeWidth: isSignalActive ? 4 : selected ? 3.5 : 2.5,
        }}
      />

      {/* SVG Motion Particles for Data Sparks */}
      {isSignalActive && (
        <g>
          <circle r="7" fill="#F4A261" className="animate-pulse filter drop-shadow-[0_0_8px_#F4A261]">
            <animateMotion path={edgePath} dur="1.2s" repeatCount="indefinite" />
          </circle>
          <circle r="4" fill="#D95D39" opacity="0.8">
            <animateMotion path={edgePath} dur="1.2s" begin="0.3s" repeatCount="indefinite" />
          </circle>
        </g>
      )}

      <EdgeLabelRenderer>
        <div
          style={{ position: 'absolute', transform: `translate(-50%, -50%) translate(${labelX}px,${labelY}px)`, pointerEvents: 'all' }}
          className="nodrag nopan flex items-center gap-1.5 bg-[#FFFDF8] border border-[#D8D2C8] px-2 py-1 rounded-lg text-[10px] font-mono"
        >
          <button
            onClick={(e) => { e.stopPropagation(); triggerSignal(id); }}
            className="p-1 bg-[#F4A261]/10 text-[#F4A261] hover:bg-[#F4A261] hover:text-white rounded flex items-center gap-1 font-bold"
          >
            <Zap size={11} /> Pass Data
          </button>
        </div>
      </EdgeLabelRenderer>
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

2. High-Performance Pixel Art Rendering with PixiJS & DiceBear

Each agent node hosts an isolated, high-performance PixiJS v8 canvas application. To keep rendering lightweight while maintaining crisp pixel art optics, we combine DiceBear's seed-based generator with WebGL textures and PixiJS ticker loops.

// Excerpt from src/components/canvas/nodes/PixiCharacter.tsx
import { useEffect, useRef } from 'react';
import * as PIXI from 'pixi.js';
import { createAvatar } from '@dicebear/core';
import * as pixelArt from '@dicebear/pixel-art';

export default function PixiCharacter({ status, seed = 'default', size = 64 }: PixiCharacterProps) {
  const containerRef = useRef<HTMLDivElement>(null);

  useEffect(() => {
    const initPixi = async () => {
      const app = new PIXI.Application();
      await app.init({ width: size, height: size, backgroundAlpha: 0 });

      const avatar = createAvatar(pixelArt, { seed, size: 128 });
      const dataUri = await avatar.toDataUri();

      const texture = await PIXI.Assets.load(dataUri);
      const sprite = new PIXI.Sprite(texture);

      sprite.anchor.set(0.5);
      sprite.x = app.screen.width / 2;
      sprite.y = app.screen.height / 2;
      app.stage.addChild(sprite);

      // Smooth status micro-animation ticker loop
      app.ticker.add((ticker) => {
        const centerY = app.screen.height / 2;
        if (status === 'busy') {
          sprite.y = centerY + Math.sin(ticker.lastTime * 0.005) * 3;
          sprite.rotation = Math.sin(ticker.lastTime * 0.003) * 0.03;
        } else if (status === 'sleeping') {
          sprite.y = centerY + 3;
          sprite.alpha = 0.9;
        } else {
          sprite.y = centerY + Math.sin(ticker.lastTime * 0.002) * 1.5;
          sprite.rotation = 0;
        }
      });
    };

    initPixi();
  }, [seed, size, status]);

  return <div ref={containerRef} style={{ width: size, height: size, imageRendering: 'pixelated' }} />;
}
Enter fullscreen mode Exit fullscreen mode

3. Task & Data Flow Audit Log Engine

To make agent activities auditable, all task starts, completions, errors, and inter-agent data transfers are recorded into an append-only audit trail in Zustand and IndexedDB:

// Excerpt from store task logging in src/store/useStore.ts
addTaskLog: (log) => {
  const newEntry: TaskLogEntry = {
    id: `log-${Math.random().toString(36).substring(7)}`,
    timestamp: Date.now(),
    ...log,
  };
  setStore((state) => ({
    ...state,
    taskLogs: [newEntry, ...(state.taskLogs || [])].slice(0, 100),
  }));
  getStore().saveToStorage();
}
Enter fullscreen mode Exit fullscreen mode

4. Agent Duplication Logic

Users can duplicate any configured agent instantly across the UI (node badges, inspector sidebar, or modal dialog):

// Excerpt from src/store/useStore.ts
duplicateAgent: (id) => {
  const state = getStore();
  const sourceAgent = state.agents[id];
  if (!sourceAgent) return null;

  const newId = Math.random().toString(36).substring(7);
  const newAgent: Agent = {
    ...sourceAgent,
    id: newId,
    name: `${sourceAgent.name} (Copy)`,
    status: 'idle',
    memory: sourceAgent.memory ? [...sourceAgent.memory] : [],
    position: {
      x: sourceAgent.position.x + 40,
      y: sourceAgent.position.y + 40,
    },
  };

  setStore((s) => ({
    ...s,
    agents: { ...s.agents, [newId]: newAgent },
    selectedAgentId: newId,
  }));
  getStore().saveToStorage();
  return newId;
}
Enter fullscreen mode Exit fullscreen mode

Spatial agent workspaces bridge the gap between human intuition and autonomous AI execution. By pairing retro 8-bit visual aesthetics with animated spark connection pipelines, audit logs, and LLM streaming APIs, Pixel Council turns multi-agent orchestration into an intuitive delight.

Check out the project online or try building your own agent council today!

https://www.dailybuild.xyz/project/204-pixel-council

Top comments (0)