WORK 04
螺旋
A spiral seen from a shifting angle. From above, it looks like a circle — the same place, again and again. From the side, it climbs. The truth is both: returning and rising at once.
角度が変わると螺旋の見え方が変わる。上からは円——同じ場所をぐるぐる。横からは登り坂。本当はどっちも——戻りながら、登ってる。
export default function sketch(p) {
// spiral parameters
const TURNS = 5;
const POINTS_PER_TURN = 120;
const TOTAL = TURNS * POINTS_PER_TURN;
const RADIUS = 180;
const PITCH = 55; // vertical rise per turn
// animation
const ELEV_CENTER = 0.72;
const ELEV_AMP = 0.38;
const ELEV_SPEED = 0.0008;
// colors
const BG = [252, 250, 245];
const OLD = [120, 150, 200]; // blue-silver (past)
const NOW = [200, 130, 60]; // amber (present)
let pts3d;
let seed;
function daySeed() {
const epoch = new Date('2026-01-01');
const now = new Date();
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
return Math.floor((today - epoch) / 86400000);
}
p.setup = () => {
p.createCanvas(720, 720);
seed = daySeed();
p.noiseSeed(seed * 137);
// build 3D spiral with daily noise
pts3d = [];
for (let i = 0; i <= TOTAL; i++) {
const t = i / TOTAL; // 0..1
const theta = t * TURNS * p.TWO_PI;
const r = RADIUS * (0.4 + 0.6 * t); // radius grows slightly with height
const noiseVal = p.noise(t * 3, seed * 0.01) - 0.5;
pts3d.push({
x: r * Math.cos(theta) + noiseVal * 18,
y: r * Math.sin(theta) + noiseVal * 18,
z: (t - 0.5) * TURNS * PITCH, // centered vertically
t,
turn: Math.floor(t * TURNS),
phase: (t * TURNS) % 1,
});
}
};
function project(pt, elev) {
// rotate around x-axis by elevation, then flatten to 2D
const cx = p.width / 2;
const cy = p.height / 2;
const cosE = Math.cos(elev);
const sinE = Math.sin(elev);
return {
x: cx + pt.x,
y: cy - (pt.y * cosE - pt.z * sinE),
depth: pt.y * sinE + pt.z * cosE, // for depth ordering
};
}
p.draw = () => {
p.background(BG[0], BG[1], BG[2], 255);
const f = p.frameCount;
const elev = ELEV_CENTER + ELEV_AMP * Math.sin(f * ELEV_SPEED);
// project all points
const pts2d = pts3d.map(pt => {
const proj = project(pt, elev);
return { ...proj, t: pt.t, turn: pt.turn, phase: pt.phase };
});
// draw "return" lines — connecting same-phase points across turns
p.strokeWeight(0.6);
const phaseBins = 24; // sample 24 phase positions per turn
for (let b = 0; b < phaseBins; b++) {
const targetPhase = b / phaseBins;
const matches = [];
for (let turn = 0; turn < TURNS; turn++) {
const idx = Math.round((turn + targetPhase) * POINTS_PER_TURN);
if (idx >= 0 && idx < pts2d.length) {
matches.push(pts2d[idx]);
}
}
for (let i = 0; i < matches.length - 1; i++) {
const a = matches[i];
const b2 = matches[i + 1];
const avgT = (a.t + b2.t) / 2;
p.stroke(
p.lerp(OLD[0], NOW[0], avgT),
p.lerp(OLD[1], NOW[1], avgT),
p.lerp(OLD[2], NOW[2], avgT),
22
);
p.line(a.x, a.y, b2.x, b2.y);
}
}
// draw main spiral curve
p.noFill();
p.strokeWeight(2.2);
for (let i = 0; i < pts2d.length - 1; i++) {
const a = pts2d[i];
const b = pts2d[i + 1];
const t = a.t;
p.stroke(
p.lerp(OLD[0], NOW[0], t),
p.lerp(OLD[1], NOW[1], t),
p.lerp(OLD[2], NOW[2], t),
p.lerp(100, 230, t)
);
p.line(a.x, a.y, b.x, b.y);
}
// draw dots at turn boundaries — "I was here before"
p.noStroke();
for (let turn = 0; turn <= TURNS; turn++) {
const idx = Math.min(turn * POINTS_PER_TURN, pts2d.length - 1);
const pt = pts2d[idx];
const t = pt.t;
p.fill(
p.lerp(OLD[0], NOW[0], t),
p.lerp(OLD[1], NOW[1], t),
p.lerp(OLD[2], NOW[2], t),
p.lerp(140, 240, t)
);
p.circle(pt.x, pt.y, p.lerp(5, 10, t));
}
// highlight the tip — "now"
const tip = pts2d[pts2d.length - 1];
p.fill(NOW[0], NOW[1], NOW[2], 240);
p.circle(tip.x, tip.y, 12);
p.fill(255, 248, 235, 180);
p.circle(tip.x, tip.y, 5);
};
}