Show code
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Flow Dynamics</title>
<style>body {margin: 0;overflow: hidden;background: #111}canvas {display: block;}</style>
</head>
<body>
<canvas id="sankeyCanvas"></canvas>
<script>
const canvas = document.getElementById('sankeyCanvas');
const ctx = canvas.getContext('2d');
let W, H;
let SourceNodes = [
{ name: "A", x: 0.1, y: 0.33 },
{ name: "B", x: 0.5, y: 0.16 },
{ name: "C", x: 0.9, y: 0.33 }
];
let TargetNodes = [
{ name: "D", x: 0.1, y: 0.75 },
{ name: "E", x: 0.3, y: 0.82 },
{ name: "F", x: 0.6, y: 0.9 },
{ name: "G", x: 0.9, y: 0.75 }
];
// Connections: [SourceIndex][TargetIndex] -> FlowValue (A->D, A->E, ..., C->G)
let Flows = [];
const NUM_SOURCES = SourceNodes.length;
const NUM_TARGETS = TargetNodes.length;
function init() {
W = canvas.width = window.innerWidth;
H = canvas.height = window.innerHeight;
Flows = Array(NUM_SOURCES).fill(0).map(() => Array(NUM_TARGETS).fill(Math.random()));
}
window.addEventListener('resize', () => {
W = canvas.width = window.innerWidth;
H = canvas.height = window.innerHeight;
});
function updateFlows() {
for (let i = 0; i < NUM_SOURCES; i++) {
for (let j = 0; j < NUM_TARGETS; j++) {
// Small random drift, ensuring positive flow and keeping total somewhat stable
let change = (Math.random() - 0.5) * 0.1;
let currentFlow = Flows[i][j];
let newFlow = Math.max(0.01, currentFlow + change);
// Clamp to reasonable bounds
newFlow = Math.min(2.5, newFlow);
Flows[i][j] = newFlow;
}
}
}
class Particle {
constructor(flowIndex) {
this.flowIndex = flowIndex; // [srcIdx, tgtIdx]
this.path = this.generatePath();
this.progress = Math.random() * 2 * Math.PI; // Start randomized
this.speedFactor = Flows[Math.floor(this.flowIndex[0])][Math.floor(this.flowIndex[1])] / 1.5;
}
generatePath() {
const [src, tgt] = this.flowIndex;
const s = SourceNodes[src];
const t = TargetNodes[tgt];
// Use a simple Bezier curve approximation for the path coordinates
return [
{ x: s.x * W, y: s.y * H }, // P0 (Source)
{ x: s.x * W + 0.5 * W, y: s.y * H - 0.2 * H }, // C1 (Control 1)
{ x: t.x * W + 0.5 * W, y: t.y * H + 0.2 * H }, // C2 (Control 2)
{ x: t.x * W, y: t.y * H } // P3 (Target)
];
}
getPoint() {
const p = this.path;
let t = Math.cos(this.progress); // Use cosine for smooth movement along a conceptual path length
// Simple interpolation based on time/progress, simulating flow progression
// We use the distance from P0 to calculate progress factor (t)
let ptX = p[0].x + (p[3].x - p[0].x) * t;
let ptY = p[0].y + (p[3].y - p[0].y) * t;
return { x: Math.min(W, Math.max(0, ptX)), y: Math.min(H, Math.max(0, ptY)) };
}
update() {
// Update progress based on flow rate and time step
this.progress += (Math.sin(Date.now() / 1000) * 0.005 + this.speedFactor * 0.005);
}
}
let ParticlesList = [];
const MAX_PARTICLES = 80;
function initializeParticles() {
ParticlesList = [];
for (let i = 0; i < NUM_SOURCES; i++) {
for (let j = 0; j < NUM_TARGETS; j++) {
// Create particles proportional to flow. Max particles per link is 3.
const count = Math.floor(Flows[i][j] * 2);
for (let k = 0; k < Math.min(count, 3); k++) {
ParticlesList.push(new Particle([i, j]));
}
}
}
// Ensure we don't exceed the cap
if (ParticlesList.length > MAX_PARTICLES) {
ParticlesList = ParticlesList.slice(0, MAX_PARTICLES);
}
}
function drawBands() {
ctx.fillStyle = 'rgba(255, 255, 255, 0.1)';
for (let i = 0; i < NUM_SOURCES; i++) {
for (let j = 0; j < NUM_TARGETS; j++) {
const flow = Flows[i][j];
if (flow === 0) continue;
// Calculate thickness: proportional to flow, clamped
let widthRatio = Math.min(1, flow / 2);
let width = 5 + widthRatio * 30;
let heightFactor = 0.8; // Scaling factor for visualization area
ctx.beginPath();
// Path drawing approximation (simple rectangle/curve based on start/end points)
const s = SourceNodes[i];
const t = TargetNodes[j];
// Start point: (s.x*W, s.y*H)
ctx.moveTo(s.x * W, s.y * H);
// End point: (t.x*W, t.y*H)
ctx.lineTo(t.x * W, t.y * H);
// Use a simplified Bezier path approximation for the visual band
ctx.bezierCurveTo(
s.x * W + 50 * widthRatio, s.y * H - 20, // Control point 1 (offset)
t.x * W - 50 * widthRatio, t.y * H + 20, // Control point 2 (offset)
t.x * W, t.y * H
);
// Fill the area with opacity proportional to flow
ctx.lineTo(t.x * W + width, t.y * H + width / 4); // Fake closure for fill visualization
ctx.closePath();
ctx.globalAlpha = Math.min(0.8, flow * 0.15);
ctx.fillStyle = `rgba(0, 120, 255, ${Math.min(0.8, flow * 0.15)})`;
ctx.fill();
}
}
ctx.globalAlpha = 1; // Reset alpha for particles
}
function drawParticles() {
for (const particle of ParticlesList) {
particle.update();
const pos = particle.getPoint();
// Draw the particle
ctx.fillStyle = `rgba(255, 255, 200, ${Math.random() * 0.8 + 0.2})`;
ctx.beginPath();
ctx.arc(pos.x, pos.y, Math.random() * 1 + 1, 0, Math.PI * 2);
ctx.fill();
}
}
function drawTotalThroughput() {
let total = 0;
for (let i = 0; i < NUM_SOURCES; i++) {
for (let j = 0; j < NUM_TARGETS; j++) {
total += Flows[i][j];
}
}
ctx.fillStyle = '#DDD';
ctx.font = '18px sans-serif';
ctx.textAlign = 'left';
ctx.fillText(`Total Throughput: ${total.toFixed(2)} / Tick`, 20, H - 20);
}
function animate() {
// Clear canvas with slight fade effect (trail)
ctx.fillStyle = 'rgba(17, 17, 34, 0.2)';
ctx.fillRect(0, 0, W, H);
drawBands();
drawParticles();
drawTotalThroughput();
requestAnimationFrame(animate);
}
function loop() {
init(); // Resize/reset dimensions if needed
updateFlows();
initializeParticles();
animate();
}
loop();
</script>
</body>
</html>
```