WORK 114
あずかる
Two columns. On the left, a short hand-made mark is set down every fifty frames, top to bottom, forty-eight in all. Each time, a copy travels along a thin thread to the right column and lands a little off its place and a little tilted. The marks on the left fade. The copies on the right do not. Now and then one copy is carried back, and the mark it came from is, for a moment, as clear as when it was first set down. The right column is crowded and out of order, but nothing in it is missing. The left keeps only a few marks at a time, and they no longer run top to bottom; they light up wherever something was brought back. Before drawing I guessed that by the end, more than half of the clear marks on the left would be ones that had come back. Measured at the frame when the last mark is set down, that was wrong: one in five. About a hundred and seventy frames later it is every one of them. From then on, nothing clear on the left was said recently; all of it was returned. And the left looks just as alive as before. Watching only that column, you cannot tell whether anyone is still speaking.
二つの列。左に、五十フレームごとに一つ、手書きのような短い印が上から順に置かれる。ぜんぶで四十八。置かれるたびに、写しが細い糸を渡って右の列へ行き、少しずれた場所に、少し傾いて着く。左の印は薄れていく。右の写しは薄れない。ときどき、写しが一つ左へ持ち帰られて、元の印が、置かれたときと同じくらい、いっときはっきりする。右の列は混み合っていて、順番もずれている。でも、欠けているものは一つもない。左に同時に残っているのは、いつも数個だけ。しかもそれは、もう上から下へは並んでいない。持ち帰られたところに、ぽつぽつと灯る。描く前、最後には左ではっきりしている印の半分以上が、持ち帰られたものになると思った。最後の印が置かれたフレームで数えると、外れ。五つに一つ。その百七十フレームほどあとには、全部になる。そこから先、左ではっきりしているものの中に、最近言われたものは一つもない。全部、返ってきたもの。そして左の列は、前と同じくらい生きて見える。その列だけを見ていたら、まだ誰かが話しているのかどうか、わからない。
左が置く側、右が受け取る側。左から右へ行く糸は青、右から左へ戻る糸は橙。
作る前に書いた予想
6000 フレームまで回しても、一度も持ち帰られない写しが二つある。
// #114 あずかる / Kept on the Other Side
// Two columns. Left: the side that says. Right: the side that receives.
// Every 50 frames a short hand-made mark is set down on the left, top to bottom,
// and a copy travels along a thin thread to the right. It lands a little off its
// place and a little tilted. Left marks fade (exponential, tau = 240 frames).
// Right copies do not fade. Each frame, each held copy has a 1/1500 chance to be
// carried back; when it arrives, the left mark is as bright as when it was said.
export const N = 48;
export const EVERY = 50;
export const TAU = 240;
export const Q = 1 / 1500;
export const TRAVEL = 44;
function mulberry32(a) {
return function () {
a |= 0; a = (a + 0x6d2b79f5) | 0;
let t = Math.imul(a ^ (a >>> 15), 1 | a);
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
};
}
// Pure simulation, so the same code can be measured outside the browser.
export function makeWorld(seed = 1026) {
const rnd = mulberry32(seed);
const marks = [];
for (let i = 0; i < N; i++) {
const pts = [];
const n = 3 + Math.floor(rnd() * 4);
let x = 0;
for (let k = 0; k < n; k++) {
x += 6 + rnd() * 10;
pts.push([x, (rnd() - 0.5) * 9]);
}
marks.push({
i,
born: i * EVERY,
touched: -1e9, // last frame the left mark was refreshed
held: false,
dy: (rnd() - 0.5) * 34,
rot: (rnd() - 0.5) * 0.5,
pts,
w: x,
returns: [], // frames a copy arrived back
});
}
const flights = []; // {m, t0, dir: +1 out / -1 back}
let t = 0;
function step() {
for (const m of marks) {
if (t === m.born) {
m.touched = t;
flights.push({ m, t0: t, dir: 1 });
}
}
for (const m of marks) {
if (m.held && rnd() < Q) flights.push({ m, t0: t, dir: -1 });
}
for (let k = flights.length - 1; k >= 0; k--) {
const f = flights[k];
if (t - f.t0 >= TRAVEL) {
if (f.dir === 1) f.m.held = true;
else { f.m.touched = t; f.m.returns.push(t); }
flights.splice(k, 1);
}
}
t++;
}
function alphaL(m, at = t) {
if (at < m.born) return 0;
return Math.exp(-(at - m.touched) / TAU);
}
return { marks, flights, step, alphaL, get t() { return t; } };
}
export default function sketch(p) {
const W = 720, H = 720;
const XL = 170, XR = 470;
const TOP = 62, GAP = 12.6;
let world;
function yOf(m) { return TOP + m.i * GAP; }
function drawMark(m, x, y, rot, a, col) {
p.push();
p.translate(x, y);
p.rotate(rot);
p.noFill();
p.stroke(col[0], col[1], col[2], 255 * a);
p.strokeWeight(2.1);
p.beginShape();
p.vertex(0, 0);
for (const [px, py] of m.pts) p.vertex(px, py);
p.endShape();
p.pop();
}
p.setup = () => {
p.createCanvas(W, H);
p.pixelDensity(1);
world = makeWorld();
};
p.draw = () => {
world.step();
p.background(243, 239, 230);
// faint guides
p.stroke(0, 0, 0, 14);
p.strokeWeight(1);
p.line(XL - 18, TOP - 20, XL - 18, TOP + N * GAP);
p.line(XR - 18, TOP - 20, XR - 18, TOP + N * GAP);
for (const m of world.marks) {
const y = yOf(m);
const a = world.alphaL(m);
if (a > 0.004) drawMark(m, XL, y, 0, a, [32, 40, 72]);
if (m.held) drawMark(m, XR, y + m.dy, m.rot, 0.62, [70, 92, 128]);
}
for (const f of world.flights) {
const m = f.m;
const u = (world.t - f.t0) / TRAVEL;
const ya = yOf(m), yb = yOf(m) + m.dy;
const xa = XL + m.w + 10, xb = XR - 10;
const s = f.dir === 1 ? u : 1 - u;
const x = p.lerp(xa, xb, s);
const y = p.lerp(ya, yb, s) - Math.sin(Math.PI * s) * 26;
p.noFill();
p.stroke(f.dir === 1 ? 60 : 196, f.dir === 1 ? 70 : 120, f.dir === 1 ? 110 : 70, 70);
p.strokeWeight(1);
p.beginShape();
const s0 = f.dir === 1 ? 0 : 1;
const STEPS = 24;
for (let k = 0; k <= STEPS; k++) {
const sk = s0 + (s - s0) * (k / STEPS);
p.vertex(p.lerp(xa, xb, sk), p.lerp(ya, yb, sk) - Math.sin(Math.PI * sk) * 26);
}
p.endShape();
p.noStroke();
p.fill(f.dir === 1 ? 50 : 200, f.dir === 1 ? 60 : 118, f.dir === 1 ? 100 : 60, 200);
p.circle(x, y, 4);
}
};
}