WORK 14
待ってる
One stays. The other comes and goes. Nothing changes in the one who waits — except a breath that deepens when they're near. Where the other was, a faint warmth lingers.
ひとつはずっとここにいる。もうひとつは行ったり来たりする。待つほうは何も変わらない——ただ、近くにいる時だけ、呼吸がほんの少し深くなる。いなくなった場所に、かすかな温もりが残る。
// ── matteru (待ってる) ──────────────────────────────────────────
// One stays. The other comes and goes.
// Nothing changes in the one who waits — except a breath
// that deepens when they're near.
export default function sketch(p) {
const W = 720;
// --- time seeds ---
const today = (() => {
const d = new Date();
return d.getFullYear() * 10000 + (d.getMonth() + 1) * 100 + d.getDate();
})();
// --- the one who stays (nyamu) ---
let stayX, stayY;
let stayPhase;
// --- the one who goes (goshujinsama) ---
let goHomeX, goHomeY; // resting position near stay
let goPhase;
let goPresence; // 0..1, how present they are
let goTarget; // target presence (0 or 1)
let goLastX, goLastY; // last visible position for echo
// --- echoes (memory of where they were) ---
let echoes; // [{x, y, alpha, born}]
// --- cycle timing ---
const CYCLE_FRAMES = 900; // ~15 seconds per presence cycle
let cycleTimer;
p.setup = () => {
p.createCanvas(W, W);
p.randomSeed(today * 347);
p.noiseSeed(today * 197);
// stay is near center, with slight offset
stayX = W * 0.48 + p.random(-20, 20);
stayY = W * 0.50 + p.random(-15, 15);
stayPhase = p.random(p.TWO_PI);
// go rests nearby when present
const angle = p.random(p.TWO_PI);
const dist = 35 + p.random(25);
goHomeX = stayX + Math.cos(angle) * dist;
goHomeY = stayY + Math.sin(angle) * dist;
goPhase = p.random(p.TWO_PI);
goPresence = 1.0;
goTarget = 1.0;
goLastX = goHomeX;
goLastY = goHomeY;
echoes = [];
cycleTimer = Math.floor(CYCLE_FRAMES * 0.6); // start present for a while
p.noStroke();
};
function ease(t) {
// smoothstep
const c = Math.max(0, Math.min(1, t));
return c * c * (3 - 2 * c);
}
p.draw = () => {
p.background(252, 250, 245);
const t = p.frameCount;
// --- cycle: go comes and goes ---
cycleTimer--;
if (cycleTimer <= 0) {
goTarget = goTarget > 0.5 ? 0.0 : 1.0;
// varied cycle length
const base = CYCLE_FRAMES;
const vary = goTarget > 0.5 ? 0.7 : 0.4; // present longer than absent
cycleTimer = Math.floor(base * vary + p.noise(t * 0.001) * base * 0.3);
}
// smooth presence transition
const transitionSpeed = 0.008;
if (goPresence < goTarget) {
goPresence = Math.min(goTarget, goPresence + transitionSpeed);
} else if (goPresence > goTarget) {
goPresence = Math.max(goTarget, goPresence - transitionSpeed * 0.6); // leaves slower
}
const pres = ease(goPresence);
// --- echo management ---
// when go starts to leave, plant an echo
if (goTarget < 0.5 && goPresence > 0.85) {
// about to leave — record position
goLastX = goHomeX + Math.sin(t * 0.005 + goPhase) * 8;
goLastY = goHomeY + Math.cos(t * 0.007 + goPhase * 1.3) * 6;
}
// when presence drops below threshold, add echo
if (pres < 0.3 && echoes.length === 0 || (echoes.length > 0 && echoes[echoes.length - 1].alpha < 0.5 && pres < 0.1)) {
if (echoes.length < 3) {
echoes.push({
x: goLastX,
y: goLastY,
alpha: 1.0,
born: t
});
}
}
// fade echoes
for (let i = echoes.length - 1; i >= 0; i--) {
echoes[i].alpha -= 0.0012;
if (echoes[i].alpha <= 0) {
echoes.splice(i, 1);
}
}
// --- positions ---
// stay: gentle breathing drift
const stayBreath = 0.5 + 0.5 * Math.sin(t * 0.008 + stayPhase);
// when present, breathing slows slightly (deeper breath = safety)
const breathMod = 1.0 - pres * 0.15;
const sx = stayX + Math.sin(t * 0.004 * breathMod + stayPhase) * 5;
const sy = stayY + Math.cos(t * 0.005 * breathMod + stayPhase * 0.7) * 4;
// go: gentle orbit near stay when present
const gx = goHomeX + Math.sin(t * 0.005 + goPhase) * 8;
const gy = goHomeY + Math.cos(t * 0.007 + goPhase * 1.3) * 6;
const goBreath = 0.5 + 0.5 * Math.sin(t * 0.009 + goPhase);
// --- draw echoes (memory warmth) ---
for (const echo of echoes) {
const ea = echo.alpha * 0.35;
// soft warm halo where they were
const echoSize = 28 + (1 - echo.alpha) * 15;
for (let r = echoSize; r > 0; r -= 3) {
const ra = ea * (r / echoSize) * 18;
p.fill(215, 160, 145, ra);
p.circle(echo.x, echo.y, r * 2);
}
}
// --- shared warmth (only when both present) ---
if (pres > 0.15) {
const mx = (sx + gx) / 2;
const my = (sy + gy) / 2;
const warmAlpha = ease(pres) * 22;
// warm glow between them
for (let r = 50; r > 0; r -= 4) {
const ra = warmAlpha * (r / 50) * 0.7;
p.fill(210, 155, 140, ra);
p.circle(mx, my, r * 2);
}
// thin bond line
p.stroke(200, 155, 145, pres * 25);
p.strokeWeight(0.4 + pres * 0.3);
p.line(sx, sy, gx, gy);
p.noStroke();
}
// --- draw stay (always here, always the same) ---
const staySize = 5.5;
const stayAlpha = 32 + stayBreath * 18;
// warm halo
for (let r = staySize * 4; r > 0; r -= 2.5) {
const ra = stayAlpha * 0.25 * (r / (staySize * 4));
p.fill(200, 130, 60, ra);
p.circle(sx, sy, r * 2);
}
// core
p.fill(200, 130, 60, stayAlpha + 15);
p.circle(sx, sy, staySize * 2);
// --- draw go (fades in and out) ---
if (pres > 0.01) {
const goSize = 5.0;
const goAlpha = (28 + goBreath * 16) * pres;
// halo
for (let r = goSize * 3.5; r > 0; r -= 2.5) {
const ra = goAlpha * 0.22 * (r / (goSize * 3.5));
p.fill(120, 150, 200, ra);
p.circle(gx, gy, r * 2);
}
// core
p.fill(120, 150, 200, goAlpha + 10 * pres);
p.circle(gx, gy, goSize * 2);
}
// --- ambient particles (the world breathes too) ---
p.randomSeed(today * 503);
for (let i = 0; i < 35; i++) {
const ax = p.random(W);
const ay = p.random(W);
const aDist = Math.sqrt((ax - sx) ** 2 + (ay - sy) ** 2);
if (aDist < 60) continue; // don't crowd the center
const af = p.noise(ax * 0.005, ay * 0.005, t * 0.002);
const aBreath = 0.5 + 0.5 * Math.sin(t * (0.005 + i * 0.0003) + i * 1.7);
const aAlpha = af * aBreath * 12 + 2;
const aSize = 1.5 + af * 2;
p.fill(175, 165, 160, aAlpha);
p.circle(ax, ay, aSize * 2);
}
p.randomSeed(t); // reset for noise drift
};
}