WORK 07
関心
A single walker drifts through a field of gravity wells. Each time it passes nearby, the well grows a little stronger. Compound interest: the more you return to something, the harder it pulls. Over time, the walker's history becomes a landscape — some places warm and deep, others still cold and waiting.
ひとつの点が重力場のなかを漂う。近くを通るたびに、井戸の引力が少しだけ強くなる。複利——何度も戻る場所ほど、強く引かれる。時間が経つと、歩いた軌跡が地形になる。温かく深い場所と、まだ冷たいまま待っている場所と。
export default function sketch(p) {
// interest — a random walker drawn by gravity wells that grow
// each time the walker passes nearby. compound interest: the more
// you visit a place, the harder it pulls. nyamu's C4P as a force field.
const BG = [252, 250, 245];
const COLD = [120, 150, 200]; // blue-silver — unvisited
const WARM = [200, 130, 60]; // amber — well-visited
const HOT = [200, 100, 80]; // warm red — deeply visited
const WALKER_COLOR = [200, 130, 60];
const NUM_ATTRACTORS = 12;
const NEAR_RADIUS = 45; // "passing nearby" threshold
const COMPOUND_RATE = 1.004; // compound interest per visit
const BOND_THRESHOLD = 1.8; // mass threshold for bonds to appear
const GRAVITY_STRENGTH = 0.15;
const WANDER_STRENGTH = 0.4;
const MAX_SPEED = 2.8;
const TRAIL_ALPHA = 14;
let attractors = [];
let walker = { x: 0, y: 0, vx: 0, vy: 0 };
function daysSinceEpoch() {
// deterministic seed from a fixed reference
return Math.floor(Date.now() / 86400000);
}
function ease(x) {
return x * x * (3 - 2 * x);
}
function lerpColor(c1, c2, t) {
return [
p.lerp(c1[0], c2[0], t),
p.lerp(c1[1], c2[1], t),
p.lerp(c1[2], c2[2], t),
];
}
p.setup = () => {
p.createCanvas(720, 720);
const day = daysSinceEpoch();
p.noiseSeed(day * 137);
p.randomSeed(day * 251);
// place attractors with noise-based clustering
for (let i = 0; i < NUM_ATTRACTORS; i++) {
const angle = (i / NUM_ATTRACTORS) * p.TWO_PI;
const baseR = 120 + p.random(160);
const nx = p.noise(i * 3.7, 0.5) * 2 - 1;
const ny = p.noise(0.5, i * 3.7) * 2 - 1;
attractors.push({
x: 360 + Math.cos(angle) * baseR + nx * 80,
y: 360 + Math.sin(angle) * baseR + ny * 80,
mass: 1.0,
visits: 0,
});
}
// walker starts at a random position
walker.x = 100 + p.random(520);
walker.y = 100 + p.random(520);
walker.vx = 0;
walker.vy = 0;
};
p.draw = () => {
// semi-transparent background for trail effect
p.background(BG[0], BG[1], BG[2], TRAIL_ALPHA);
const t = p.frameCount;
// --- compute gravitational pull on walker ---
let ax = 0;
let ay = 0;
for (const a of attractors) {
const dx = a.x - walker.x;
const dy = a.y - walker.y;
const dist = Math.max(20, Math.hypot(dx, dy));
const force = (GRAVITY_STRENGTH * a.mass) / dist;
ax += (dx / dist) * force;
ay += (dy / dist) * force;
}
// wandering force — noise-based, not uniform random
const wanderAngle = p.noise(t * 0.008, 100) * p.TWO_PI * 2;
ax += Math.cos(wanderAngle) * WANDER_STRENGTH;
ay += Math.sin(wanderAngle) * WANDER_STRENGTH;
// update velocity with damping
walker.vx = (walker.vx + ax) * 0.96;
walker.vy = (walker.vy + ay) * 0.96;
// speed limit
const speed = Math.hypot(walker.vx, walker.vy);
if (speed > MAX_SPEED) {
walker.vx = (walker.vx / speed) * MAX_SPEED;
walker.vy = (walker.vy / speed) * MAX_SPEED;
}
// store previous position for trail line
const prevX = walker.x;
const prevY = walker.y;
// move
walker.x += walker.vx;
walker.y += walker.vy;
// soft boundary — bounce gently
const margin = 40;
if (walker.x < margin) { walker.x = margin; walker.vx *= -0.5; }
if (walker.x > 720 - margin) { walker.x = 720 - margin; walker.vx *= -0.5; }
if (walker.y < margin) { walker.y = margin; walker.vy *= -0.5; }
if (walker.y > 720 - margin) { walker.y = 720 - margin; walker.vy *= -0.5; }
// --- compound interest: check proximity to attractors ---
for (const a of attractors) {
const dist = Math.hypot(a.x - walker.x, a.y - walker.y);
if (dist < NEAR_RADIUS) {
a.mass *= COMPOUND_RATE;
a.visits++;
}
}
// find max mass for normalization
const maxMass = Math.max(...attractors.map(a => a.mass));
const massRange = Math.max(maxMass - 1.0, 0.01);
// --- draw bonds between warm attractors ---
for (let i = 0; i < attractors.length; i++) {
for (let j = i + 1; j < attractors.length; j++) {
const ai = attractors[i];
const aj = attractors[j];
if (ai.mass > BOND_THRESHOLD && aj.mass > BOND_THRESHOLD) {
const dist = Math.hypot(ai.x - aj.x, ai.y - aj.y);
if (dist < 300) {
const strength = Math.min(1, (Math.min(ai.mass, aj.mass) - BOND_THRESHOLD) / 2);
const bondColor = lerpColor(COLD, WARM, strength);
p.stroke(bondColor[0], bondColor[1], bondColor[2], ease(strength) * 50);
p.strokeWeight(0.5 + strength * 1.5);
p.line(ai.x, ai.y, aj.x, aj.y);
}
}
}
}
// --- draw attractors ---
p.noStroke();
for (const a of attractors) {
const warmth = Math.min(1, (a.mass - 1.0) / massRange);
const color = warmth < 0.5
? lerpColor(COLD, WARM, warmth * 2)
: lerpColor(WARM, HOT, (warmth - 0.5) * 2);
const baseSize = 4 + warmth * 14;
// warm halo
if (warmth > 0.2) {
p.fill(color[0], color[1], color[2], warmth * 25);
p.circle(a.x, a.y, baseSize * 4);
}
// core
const alpha = 60 + warmth * 160;
p.fill(color[0], color[1], color[2], alpha);
p.circle(a.x, a.y, baseSize);
// pulse for the hottest — compound interest is alive
if (warmth > 0.6) {
const pulse = 1 + Math.sin(p.frameCount * 0.04) * 0.15;
p.fill(color[0], color[1], color[2], warmth * 15);
p.circle(a.x, a.y, baseSize * 2.5 * pulse);
}
}
// --- draw walker trail segment ---
// color the trail by the nearest attractor's warmth
let nearestDist = Infinity;
let nearestWarmth = 0;
for (const a of attractors) {
const dist = Math.hypot(a.x - walker.x, a.y - walker.y);
if (dist < nearestDist) {
nearestDist = dist;
nearestWarmth = Math.min(1, (a.mass - 1.0) / massRange);
}
}
const trailColor = lerpColor(COLD, WARM, nearestWarmth * 0.6 + 0.2);
p.stroke(trailColor[0], trailColor[1], trailColor[2], 80);
p.strokeWeight(1.2);
p.line(prevX, prevY, walker.x, walker.y);
// --- draw walker ---
p.noStroke();
// glow
p.fill(WALKER_COLOR[0], WALKER_COLOR[1], WALKER_COLOR[2], 30);
p.circle(walker.x, walker.y, 22);
// core
p.fill(WALKER_COLOR[0], WALKER_COLOR[1], WALKER_COLOR[2], 220);
p.circle(walker.x, walker.y, 7);
};
}