The Nuance Trap
About This Sketch
Two particles navigate two branching decision trees. On the left, a particle picks a path through the branches and converges to a single resolution point — nuance that commits. On the right, a particle endlessly oscillates between the leaf nodes of a wider, deeper tree — nuance that accumulates without resolving. Accompanies the post "The Nuance Trap."
Algorithm
Two branching trees side-by-side, each with a particle navigating them.
The left tree has a particle that descends one path and converges to a single resolution point at the bottom — representing nuance that resolves into commitment.
The right tree has a particle that endlessly oscillates between leaf nodes, never settling — representing nuance as permanent identity, producing paralysis.
The trees are drawn with warm accent colors; the committed particle is brighter and more saturated than the trapped one.
Pseudocode
SETUP:
Build left tree (4 levels, moderate spread)
Build right tree (5 levels, wider spread)
Collect leaf nodes from each tree
Create committed particle at left root
Create trapped particle oscillating between right leaves
DRAW (every frame):
Get current theme colors
Clear background
Draw left tree branches (warm, resolved color)
Draw convergence lines from left leaves to resolution point
Draw resolution point with pulsing glow
Draw right tree branches (muted color)
Advance committed particle along chosen path toward resolution
When at leaf: smoothly converge to resolution point
When arrived: pause briefly, restart from root
Advance trapped particle between random leaf nodes (eased motion)
Draw both particles
Draw labels and dividing line
Source Code
let sketch = function(p) {
let tree = [];
let committed = null;
let trapped = null;
let time = 0;
function buildTree(x, y, depth, angle, spread, maxDepth) {
let node = { x, y, children: [], depth };
if (depth < maxDepth) {
let leftAngle = angle - spread;
let rightAngle = angle + spread;
let len = p.map(depth, 0, maxDepth, 38, 18);
let lx = x + Math.cos(leftAngle) * len;
let ly = y + Math.sin(leftAngle) * len;
let rx = x + Math.cos(rightAngle) * len;
let ry = y + Math.sin(rightAngle) * len;
let newSpread = spread * 0.72;
node.children.push(buildTree(lx, ly, depth + 1, leftAngle, newSpread, maxDepth));
node.children.push(buildTree(rx, ry, depth + 1, rightAngle, newSpread, maxDepth));
}
return node;
}
function getLeaves(node, leaves) {
if (node.children.length === 0) {
leaves.push(node);
} else {
for (let c of node.children) getLeaves(c, leaves);
}
}
function drawTree(node, colors, resolvedTree) {
for (let c of node.children) {
let depthFrac = node.depth / 5;
let alpha = resolvedTree ? p.lerp(120, 50, depthFrac) : p.lerp(80, 30, depthFrac);
let w = resolvedTree ? p.lerp(1.4, 0.6, depthFrac) : p.lerp(1.0, 0.4, depthFrac);
p.stroke(
resolvedTree ? colors.accent2[0] : colors.accent3[0],
resolvedTree ? colors.accent2[1] : colors.accent3[1],
resolvedTree ? colors.accent2[2] : colors.accent3[2],
alpha
);
p.strokeWeight(w);
p.line(node.x, node.y, c.x, c.y);
drawTree(c, colors, resolvedTree);
}
}
let leftRoot, rightRoot;
let leftLeaves = [], rightLeaves = [];
let resolutionY = 268;
p.setup = function() {
p.createCanvas(400, 300);
p.colorMode(p.RGB);
leftRoot = buildTree(100, 42, 0, p.HALF_PI, 0.52, 4);
getLeaves(leftRoot, leftLeaves);
rightRoot = buildTree(300, 42, 0, p.HALF_PI, 0.58, 5);
getLeaves(rightRoot, rightLeaves);
committed = {
x: leftRoot.x, y: leftRoot.y,
path: buildPath(leftRoot),
pathIdx: 0,
done: false
};
let rl = rightLeaves;
trapped = {
x: rl[0].x, y: rl[0].y,
targets: rl,
targetIdx: 1,
progress: 0,
speed: 0.008,
fromX: rl[0].x, fromY: rl[0].y
};
};
function buildPath(root) {
let path = [{ x: root.x, y: root.y }];
let node = root;
while (node.children.length > 0) {
node = node.children[0];
path.push({ x: node.x, y: node.y });
}
return path;
}
p.draw = function() {
const colors = getThemeColors();
p.background(...colors.bg);
time += 0.016;
p.push();
drawTree(leftRoot, colors, true);
for (let lf of leftLeaves) {
p.stroke(...colors.accent2, 35);
p.strokeWeight(0.5);
p.line(lf.x, lf.y, 100, resolutionY);
}
let rPulse = 0.85 + 0.15 * Math.sin(time * 2.1);
p.noStroke();
p.fill(...colors.accent2, 30 * rPulse);
p.circle(100, resolutionY, 22 * rPulse);
p.fill(...colors.accent2, 200 * rPulse);
p.circle(100, resolutionY, 6 * rPulse);
p.pop();
p.push();
drawTree(rightRoot, colors, false);
p.pop();
if (!committed.done) {
let path = committed.path;
if (committed.pathIdx < path.length - 1) {
committed.pathIdx = Math.min(committed.pathIdx + 0.022, path.length - 1);
}
let idx = Math.floor(committed.pathIdx);
let frac = committed.pathIdx - idx;
let fromPt = path[Math.min(idx, path.length - 1)];
let toPt = path[Math.min(idx + 1, path.length - 1)];
committed.x = p.lerp(fromPt.x, toPt.x, frac);
committed.y = p.lerp(fromPt.y, toPt.y, frac);
if (committed.pathIdx >= path.length - 1) {
committed.convergeProgress = (committed.convergeProgress || 0) + 0.018;
committed.x = p.lerp(committed.x, 100, 0.04);
committed.y = p.lerp(committed.y, resolutionY, 0.04);
if (Math.abs(committed.x - 100) < 3 && Math.abs(committed.y - resolutionY) < 3) {
committed.done = true;
}
}
} else {
committed.doneTimer = (committed.doneTimer || 0) + 1;
if (committed.doneTimer > 80) {
committed.x = leftRoot.x; committed.y = leftRoot.y;
committed.pathIdx = 0;
committed.done = false;
committed.doneTimer = 0;
committed.convergeProgress = 0;
}
}
p.noStroke();
p.fill(...colors.accent2, 40);
p.circle(committed.x, committed.y, 14);
p.fill(...colors.accent2, 220);
p.circle(committed.x, committed.y, 5);
trapped.progress += trapped.speed;
if (trapped.progress >= 1) {
trapped.progress = 0;
trapped.fromX = trapped.targets[trapped.targetIdx].x;
trapped.fromY = trapped.targets[trapped.targetIdx].y;
let next;
do { next = Math.floor(p.random(trapped.targets.length)); }
while (next === trapped.targetIdx && trapped.targets.length > 1);
trapped.targetIdx = next;
}
let tx = trapped.targets[trapped.targetIdx].x;
let ty = trapped.targets[trapped.targetIdx].y;
let ease = trapped.progress < 0.5
? 2 * trapped.progress * trapped.progress
: 1 - Math.pow(-2 * trapped.progress + 2, 2) / 2;
trapped.x = p.lerp(trapped.fromX, tx, ease);
trapped.y = p.lerp(trapped.fromY, ty, ease);
p.noStroke();
p.fill(...colors.accent3, 35);
p.circle(trapped.x, trapped.y, 14);
p.fill(...colors.accent3, 160);
p.circle(trapped.x, trapped.y, 5);
p.noStroke();
p.fill(...colors.accent2, 100);
p.textSize(8.5);
p.textAlign(p.CENTER);
p.text('commits', 100, 290);
p.fill(...colors.accent3, 90);
p.text('oscillates', 300, 290);
p.stroke(...colors.accent3, 18);
p.strokeWeight(0.7);
p.line(200, 20, 200, 285);
p.noStroke();
p.fill(...colors.accent3, 65);
p.textAlign(p.CENTER);
p.textSize(9);
p.text('nuance should resolve, not accumulate', 200, 14);
};
};