WORK 115
ふりわける
Small marks are set down like a diary, left to right, thirty to a row, six hundred in all. Each one carries a hidden number that drifts a little with every mark. When a mark is set down it is grey: nothing has happened after it yet. One row later it is filed for the first time, warm if the number has gone up since, cool if it has gone down. Three rows later it is filed again, by the same comparison, with a longer look. When the second filing disagrees with the first, the first colour stays underneath as a thin line. So the bottom row is always grey, the two rows above it have only their first colour, and everything higher has been filed twice. Before drawing I guessed that about three in ten marks would change their mind on the second filing. It was a little under that. What I had not thought about was which way they would change. In this run the number mostly sinks. Of the marks first filed warm, nine in ten were later filed cool; the other way round, one in fifteen. The warm rows near the bottom are warm only because nothing has come after them yet.
日記みたいに、小さな印を左から右へ置いていく。一行に三十、ぜんぶで六百。印はそれぞれ、見えない数を一つ持っていて、その数は印ごとに少しずつ揺れる。置かれたときの印は灰色。まだ、そのあとに何も起きていないから。一行あとで、一回目の振り分け。数が上がっていれば暖かい色、下がっていれば冷たい色。三行あとで、同じ比べ方で、もっと長く見て、もう一度振り分ける。二回目が一回目と食いちがったら、一回目の色は印の下に細い線で残る。だから、いちばん下の行はいつも灰色で、その上の二行は一回目の色だけ。それより上は、二回振り分けられている。描く前、二回目で色が変わる印は十に三つくらいだと思った。少し下だった。考えていなかったのは、どっち向きに変わるか。この一回では、数はだいたい沈んでいく。一回目に暖かい色だった印は、十に九が、あとで冷たい色に振り分け直された。逆向きは、十五に一つ。下の方の暖かい行が暖かいのは、そのあとにまだ何も来ていないから、それだけ。
作る前に書いた予想
一回目に暖かい色だった 124 個のうち 115 個が冷たい色に振り分け直された。冷たい色だった 386 個のうち、暖かい色に変わったのは 25 個。
// #115 ふりわける / Filed Later
// Small marks are set down like a diary, left to right, wrapping into rows:
// one every 4 frames, 30 to a row, 20 rows, 600 in all.
// Each mark carries a hidden value v (a random walk). When set down it is grey.
// 30 marks later (one row down) it is filed for the first time: warm if v has
// gone up since, cool if it has gone down; the bigger the change, the stronger.
// 90 marks later (three rows down) it is filed again by the same comparison.
// If the second filing disagrees, the first colour is left as a thin line below.
export const N = 600;
export const PER_ROW = 30;
export const EVERY = 4;
export const L1 = 30;
export const L2 = 90;
export const FADE = 14;
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;
};
}
function gauss(rnd) {
const u = Math.max(1e-9, rnd()), w = rnd();
return Math.sqrt(-2 * Math.log(u)) * Math.cos(2 * Math.PI * w);
}
// Pure model, so the same code can be measured outside the browser.
export function makeWorld(seed = 1027) {
const rnd = mulberry32(seed);
const v = [0];
for (let i = 1; i < N + L2; i++) v.push(v[i - 1] + gauss(rnd));
const marks = [];
for (let i = 0; i < N; i++) {
marks.push({
i,
ang: -0.9 + (rnd() - 0.5) * 0.5,
len: 9 + rnd() * 5,
jx: (rnd() - 0.5) * 3,
jy: (rnd() - 0.5) * 3,
});
}
// score in [-1, 1] for mark i judged with lag L (normalised by sqrt(L))
function score(i, L) {
const d = (v[i + L] - v[i]) / Math.sqrt(L);
return Math.max(-1, Math.min(1, d / 1.5));
}
// index of the newest mark that exists at frame t
function newest(t) { return Math.min(N - 1, Math.floor(t / EVERY)); }
// state of mark i at frame t
function state(i, t) {
const n = newest(t);
if (i > n) return null;
const s = { placed: true, f1: null, f2: null, a1: 0, a2: 0 };
// a later mark can only be compared against once it has been set down
if (n >= i + L1) { s.f1 = score(i, L1); s.a1 = Math.min(1, (t - (i + L1) * EVERY) / FADE); }
if (i + L2 < N && n >= i + L2) { s.f2 = score(i, L2); s.a2 = Math.min(1, (t - (i + L2) * EVERY) / FADE); }
return s;
}
return { marks, v, score, state, newest };
}
export default function sketch(p) {
const W = 720, H = 720;
const X0 = 66, DX = 20;
const Y0 = 70, DY = 29;
const GREY = [156, 152, 144];
const WARM = [204, 104, 52];
const COOL = [52, 92, 158];
let world;
let t = 0;
function colOf(sc) {
const k = Math.abs(sc);
const c = sc >= 0 ? WARM : COOL;
return [0, 1, 2].map((j) => GREY[j] + (c[j] - GREY[j]) * (0.25 + 0.75 * k));
}
function mix(a, b, u) { return [0, 1, 2].map((j) => a[j] + (b[j] - a[j]) * u); }
p.setup = () => {
p.createCanvas(W, H);
p.pixelDensity(1);
world = makeWorld();
};
p.draw = () => {
p.background(244, 240, 231);
p.strokeCap(p.ROUND);
// faint ruled lines, like a notebook
p.stroke(0, 0, 0, 12);
p.strokeWeight(1);
for (let r = 0; r < N / PER_ROW; r++) {
const y = Y0 + r * DY + 9;
p.line(X0 - 14, y, X0 + (PER_ROW - 1) * DX + 14, y);
}
for (const m of world.marks) {
const s = world.state(m.i, t);
if (!s) break;
const cx = X0 + (m.i % PER_ROW) * DX + m.jx;
const cy = Y0 + Math.floor(m.i / PER_ROW) * DY + m.jy;
let col = GREY;
if (s.f1 !== null) col = mix(GREY, colOf(s.f1), s.a1);
if (s.f2 !== null) {
const flipped = Math.sign(s.f2) !== Math.sign(s.f1);
if (flipped) {
// the first filing stays, thin, underneath
const c1 = colOf(s.f1);
p.stroke(c1[0], c1[1], c1[2], 200 * s.a2);
p.strokeWeight(1.3);
p.line(cx - 6, cy + 8, cx + 6, cy + 8);
}
col = mix(colOf(s.f1), colOf(s.f2), s.a2);
}
const dx = Math.cos(m.ang) * m.len / 2, dy = Math.sin(m.ang) * m.len / 2;
p.stroke(col[0], col[1], col[2]);
p.strokeWeight(3);
p.line(cx - dx, cy - dy, cx + dx, cy + dy);
}
t++;
};
}