WORK 09
のびる
A single root reaches upward and forks. Neither branch turns back. The tips keep pushing — and where they pause, a faint dotted line hints at what comes next. Everything here is anticipation: the tree is not what grew, but what is still growing.
一本の根が上に伸びて、枝分かれする。どちらも引き返さない。先端はまだ先を見てる——止まった場所に、次の一歩がかすかに見える。ここにあるのは全部、予感。この木は育ったものじゃなくて、まだ育ってるもの。
export default function sketch(p) {
// nobiru — reaching that doesn't pull back
const BG = [252, 250, 245];
const WARM = [200, 130, 60]; // amber — the root, the origin
const COOL = [120, 150, 200]; // blue-silver — the tips, still becoming
const GHOST = [200, 190, 175]; // faint — anticipation of where it might go
const MAX_DEPTH = 8;
const GROW_SPEED = 0.35;
const BRANCH_INTERVAL = 110;
const SWAY_SPEED = 0.0006;
let branches = [];
let nextBranchFrame = 50;
p.setup = () => {
p.createCanvas(720, 720);
p.background(BG[0], BG[1], BG[2]);
const day = Math.floor(Date.now() / 86400000);
p.randomSeed(day * 317);
p.noiseSeed(day * 193);
// root — reaching upward from center
branches.push({
x0: 360,
y0: 560,
angle: -p.HALF_PI + (p.random() - 0.5) * 0.15,
targetLen: 70 + p.random(30),
currentLen: 0,
depth: 0,
children: [],
done: false,
swayOffset: p.random(p.TWO_PI),
});
};
p.draw = () => {
// no fade — branches persist
p.background(BG[0], BG[1], BG[2]);
const t = p.frameCount;
// grow all branches
for (const b of branches) {
if (!b.done) {
b.currentLen = Math.min(b.currentLen + GROW_SPEED, b.targetLen);
if (b.currentLen >= b.targetLen) {
b.done = true;
}
}
}
// periodically branch from a random tip
if (t >= nextBranchFrame) {
const tips = branches.filter(b => b.done && b.children.length === 0 && b.depth < MAX_DEPTH);
if (tips.length > 0) {
const parent = tips[Math.floor(p.random(tips.length))];
const tipAngle = sway(parent, t);
const tipX = parent.x0 + Math.cos(tipAngle) * parent.currentLen;
const tipY = parent.y0 + Math.sin(tipAngle) * parent.currentLen;
const spread = 0.2 + p.random(0.4);
const lean = (p.noise(tipX * 0.008, tipY * 0.008) - 0.5) * 0.5;
const depthFactor = Math.pow(0.82, parent.depth);
const childLen = (35 + p.random(30)) * depthFactor;
// both directions reach — neither is abandoned
for (let side = -1; side <= 1; side += 2) {
const a = tipAngle + lean + spread * 0.5 * side;
const lenScale = side === -1 ? 1.0 : (0.55 + p.random(0.35));
parent.children.push(branches.length);
branches.push({
x0: tipX,
y0: tipY,
angle: a,
targetLen: childLen * lenScale,
currentLen: 0,
depth: parent.depth + 1,
children: [],
done: false,
swayOffset: p.random(p.TWO_PI),
});
}
}
nextBranchFrame = t + BRANCH_INTERVAL + Math.floor(p.random(40));
}
// draw all branches
for (const b of branches) {
if (b.currentLen < 0.5) continue;
drawBranch(b, t);
}
// ghost anticipation on finished tips — where they might go next
const waitingTips = branches.filter(b => b.done && b.children.length === 0 && b.depth < MAX_DEPTH);
for (const tip of waitingTips) {
const tipAngle = sway(tip, t);
const tx = tip.x0 + Math.cos(tipAngle) * tip.currentLen;
const ty = tip.y0 + Math.sin(tipAngle) * tip.currentLen;
// a faint line hinting at the future
const ghostLen = 12 + 8 * Math.sin(t * 0.015 + tip.swayOffset);
const ghostAngle = tipAngle + Math.sin(t * 0.008 + tip.swayOffset * 1.7) * 0.15;
const gx = tx + Math.cos(ghostAngle) * ghostLen;
const gy = ty + Math.sin(ghostAngle) * ghostLen;
p.stroke(GHOST[0], GHOST[1], GHOST[2], 35);
p.strokeWeight(0.3);
// dotted ghost
const steps = 6;
for (let s = 1; s <= steps; s++) {
const frac = s / steps;
const px = p.lerp(tx, gx, frac);
const py = p.lerp(ty, gy, frac);
p.point(px, py);
}
}
// growing tips glow
const growing = branches.filter(b => !b.done);
for (const tip of growing) {
const tipAngle = sway(tip, t);
const ex = tip.x0 + Math.cos(tipAngle) * tip.currentLen;
const ey = tip.y0 + Math.sin(tipAngle) * tip.currentLen;
const pulse = 0.5 + 0.5 * Math.sin(t * 0.04 + tip.swayOffset);
p.noStroke();
const d = tip.depth / MAX_DEPTH;
const r = p.lerp(WARM[0], COOL[0], d);
const g = p.lerp(WARM[1], COOL[1], d);
const bl = p.lerp(WARM[2], COOL[2], d);
p.fill(r, g, bl, 12 + pulse * 18);
p.circle(ex, ey, 5 + pulse * 4);
}
};
function drawBranch(b, t) {
const d = b.depth / MAX_DEPTH;
const r = p.lerp(WARM[0], COOL[0], d);
const g = p.lerp(WARM[1], COOL[1], d);
const bl = p.lerp(WARM[2], COOL[2], d);
const thickness = p.lerp(3.0, 0.3, d);
const alpha = p.lerp(200, 55, d);
const angle = sway(b, t);
const ex = b.x0 + Math.cos(angle) * b.currentLen;
const ey = b.y0 + Math.sin(angle) * b.currentLen;
p.stroke(r, g, bl, alpha);
p.strokeWeight(thickness);
p.line(b.x0, b.y0, ex, ey);
// branch point — a tiny knot
if (b.children.length > 0) {
p.noStroke();
p.fill(r, g, bl, alpha * 0.6);
p.circle(ex, ey, thickness * 0.8);
}
}
function sway(branch, t) {
return branch.angle + Math.sin(t * SWAY_SPEED + branch.swayOffset) * 0.03 * (1 + branch.depth * 0.4);
}
}