Two Decision Paths
About This Sketch
Two decision paths visualized side by side — the direct route of expert pattern recognition versus the wandering route of deliberate analysis. Both reach the same destination. The difference is in the path taken and the time spent.
Algorithm
Two animated particles travel from START to GOAL on separate paths.
The expert particle (top half) traces a nearly direct route with minor variations.
The novice particle (bottom half) wanders, backtracks, and takes a longer winding route.
Both eventually arrive at the same destination. The visualization cycles automatically,
showing the expert path first, then the novice path, then resetting.
Pseudocode
SETUP:
Build expert path: 20 points in near-straight line with small sine jitter
Build novice path: 15 waypoints that wander and backtrack
PHASE CYCLE (loops):
Phase 0 (30 frames): Brief pause
Phase 1: Animate expert particle along direct path (fast, 0.025/frame)
Phase 2 (60 frames): Pause - expert has arrived
Phase 3: Animate novice particle along winding path (slow, 0.012/frame)
Phase 4 (90 frames): Both arrived, prepare to reset
DRAW each frame:
Clear background with theme colors
Draw labels and start/goal markers
Draw trail and moving dot for each active particle
Draw dashed dividing line between the two regions
Source Code
let nodes = [];
let expertPath = [];
let novicePath = [];
let expertProgress = 0;
let noviceProgress = 0;
let phase = 0;
let phaseTimer = 0;
let canvasW = 400;
let canvasH = 300;
let sketch = function(p) {
p.setup = function() {
let cnv = p.createCanvas(canvasW, canvasH);
cnv.parent('sketch-container');
p.frameRate(30);
buildPaths(p);
};
function buildPaths(p) {
let startX = 40;
let endX = 360;
let midY = canvasH / 2;
expertPath = [];
let steps = 20;
for (let i = 0; i <= steps; i++) {
let t = i / steps;
let x = p.lerp(startX, endX, t);
let jitter = (i === 0 || i === steps) ? 0 : p.sin(i * 1.7) * 8;
expertPath.push({ x: x, y: midY - 45 + jitter });
}
novicePath = [];
novicePath.push({ x: startX, y: midY + 45 });
novicePath.push({ x: 90, y: midY + 20 });
novicePath.push({ x: 130, y: midY + 65 });
novicePath.push({ x: 100, y: midY + 45 });
novicePath.push({ x: 150, y: midY + 30 });
novicePath.push({ x: 180, y: midY + 70 });
novicePath.push({ x: 160, y: midY + 50 });
novicePath.push({ x: 210, y: midY + 35 });
novicePath.push({ x: 240, y: midY + 65 });
novicePath.push({ x: 220, y: midY + 45 });
novicePath.push({ x: 270, y: midY + 30 });
novicePath.push({ x: 300, y: midY + 55 });
novicePath.push({ x: 310, y: midY + 40 });
novicePath.push({ x: 340, y: midY + 45 });
novicePath.push({ x: endX, y: midY + 45 });
}
function getPathPoint(path, t) {
let total = path.length - 1;
let idx = t * total;
let i = p.floor(idx);
let frac = idx - i;
if (i >= total) return path[total];
return {
x: p.lerp(path[i].x, path[i + 1].x, frac),
y: p.lerp(path[i].y, path[i + 1].y, frac)
};
}
p.draw = function() {
let colors = getThemeColors();
p.background(...colors.bg);
phaseTimer++;
if (phase === 0 && phaseTimer > 30) {
phase = 1; phaseTimer = 0; expertProgress = 0;
} else if (phase === 1) {
expertProgress += 0.025;
if (expertProgress >= 1) { expertProgress = 1; phase = 2; phaseTimer = 0; }
} else if (phase === 2 && phaseTimer > 60) {
phase = 3; phaseTimer = 0; noviceProgress = 0;
} else if (phase === 3) {
noviceProgress += 0.012;
if (noviceProgress >= 1) { noviceProgress = 1; phase = 4; phaseTimer = 0; }
} else if (phase === 4 && phaseTimer > 90) {
phase = 0; phaseTimer = 0; expertProgress = 0; noviceProgress = 0;
}
p.textFont('Georgia');
p.textSize(11);
p.textAlign(p.LEFT);
p.fill(...colors.text, 160);
p.noStroke();
p.text('Expert (fast thinking)', 40, 50);
p.text('Novice (slow deliberation)', 40, canvasH / 2 + 10);
let startX = 40, endX = 360;
p.fill(...colors.accent2, 180);
p.noStroke();
p.circle(startX, canvasH / 2 - 45, 10);
p.circle(endX, canvasH / 2 - 45, 10);
p.circle(startX, canvasH / 2 + 45, 10);
p.circle(endX, canvasH / 2 + 45, 10);
p.textAlign(p.CENTER);
p.textSize(10);
p.fill(...colors.accent2, 200);
p.text('START', startX, canvasH / 2 - 55);
p.text('GOAL', endX, canvasH / 2 - 55);
if (expertProgress > 0) {
let trailCount = p.floor(expertProgress * (expertPath.length - 1));
p.stroke(...colors.accent2, 200);
p.strokeWeight(2.5);
p.noFill();
p.beginShape();
for (let i = 0; i <= trailCount && i < expertPath.length; i++) {
p.vertex(expertPath[i].x, expertPath[i].y);
}
let ept2 = getPathPoint(expertPath, expertProgress);
p.vertex(ept2.x, ept2.y);
p.endShape();
p.noStroke();
p.fill(...colors.accent2);
p.circle(ept2.x, ept2.y, 12);
}
if (noviceProgress > 0) {
let trailCount = p.floor(noviceProgress * (novicePath.length - 1));
p.stroke(...colors.accent1, 180);
p.strokeWeight(2);
p.noFill();
p.beginShape();
for (let i = 0; i <= trailCount && i < novicePath.length; i++) {
p.vertex(novicePath[i].x, novicePath[i].y);
}
let npt2 = getPathPoint(novicePath, noviceProgress);
p.vertex(npt2.x, npt2.y);
p.endShape();
p.noStroke();
p.fill(...colors.accent1);
p.circle(npt2.x, npt2.y, 12);
}
p.stroke(...colors.accent3, 60);
p.strokeWeight(1);
p.drawingContext.setLineDash([4, 6]);
p.line(0, canvasH / 2, canvasW, canvasH / 2);
p.drawingContext.setLineDash([]);
};
};