WORK 33
となり
The same values, reseated. Nothing added, nothing removed; mean, median and spread never move. Only who sits beside whom. And it reads as noise, then as a landscape, then as a rising trend. The meaning was never in the material. It was in the order — and in whoever set it.
同じ値たちが、席を替えるだけ。何も足されず、何も取られず、平均も中央値もばらつきも一度も動かない。変わるのは、どれがどれの隣にいるかだけ。それが、何もないようにも、風景にも、右肩上がりにも見える。意味は材料の中になかった。並びの中に、そして並べた者の側にあった。
三つの並びを行き来する。ほどけた並び、どの大きさにも起伏がある並び、大きなうねりがひとつだけの並び。点は同じ点で、値も同じ値。埋まっている面積さえ変わらない。持っているものを測るかぎり、この絵は最初から最後まで何も起きていない。
最後の並びは、右肩上がりに見える。何かが増えたわけではない。大きい値がうしろに集まっただけで、それを増加と読むのはこちら側の癖だ。
// となり (Tonari) — 同じものたちが、誰の隣にいるかだけで
//
// 点は増えも減りもしない。値の集合はずっと同じ。
// 変わるのは、どれがどれの隣にいるか、それだけ。
//
// 並びがほどけているとき、そこには何もないように見える。
// 材料が足りないようにも見える。でも足りている。
// ぜんぶ、最初から、そこにある。
//
// 平均も、中央値も、ばらつきも、この絵のあいだ一度も動かない。
// 動いているのは順番だけ。
const W = 720;
const H = 720;
const N = 240; // 点の数。最後まで変わらない
const MARGIN_X = 64;
const BASE_Y = H * 0.84; // 地面。値はここから上に立つ
const AMP = H * 0.70; // いちばん大きい値がこの高さになる
const HOLD = 200; // ひとつの並びに留まるフレーム数
const MOVE = 140; // 並び替えにかかるフレーム数
const PAPER = [250, 248, 243];
const INK = [46, 44, 41];
const WARM = [196, 118, 74];
const COOL = [92, 118, 146];
export default function sketch(p) {
let vals = []; // 各点の値。一度決めたら二度と変えない
let orders = []; // 並べ方(各点がどの席に座るか)
let slotFrom = [];
let slotTo = [];
let phase = 0; // 0: 留まる / 1: 並び替え中
let clock = 0;
let step = 0;
p.setup = () => {
p.createCanvas(W, H);
p.noiseSeed(dailySeed());
p.randomSeed(dailySeed());
p.pixelDensity(2);
// 値をつくる。この集合はこの先ずっと変わらない
for (let i = 0; i < N; i++) {
vals.push(Math.exp(p.randomGaussian(0, 0.62)));
}
normalize(vals);
// 三つの並べ方。どれも同じ値の集合を、違う順で置くだけ
orders = [
slotsFrom(whiteSeq(N)), // ほどけている
slotsFrom(pinkSeq(N)), // どの大きさにも起伏がある
slotsFrom(brownSeq(N)), // 大きなうねりがひとつ
];
slotFrom = orders[0].slice();
slotTo = orders[0].slice();
};
p.draw = () => {
p.background(PAPER[0], PAPER[1], PAPER[2]);
clock++;
if (phase === 0 && clock > HOLD) {
step = (step + 1) % orders.length;
slotFrom = slotTo.slice();
slotTo = orders[step].slice();
phase = 1;
clock = 0;
} else if (phase === 1 && clock > MOVE) {
phase = 0;
clock = 0;
}
const t = phase === 1 ? ease(p.constrain(clock / MOVE, 0, 1)) : 1;
// 席替えの途中がいちばん薄い。落ち着くとかたちが立ち上がる
const settle = phase === 1 ? 1 - Math.sin(t * Math.PI) * 0.8 : 1;
// 各点の現在位置。値は持ったまま、席だけ移る
const pts = [];
for (let i = 0; i < N; i++) {
const s = slotFrom[i] + (slotTo[i] - slotFrom[i]) * t;
pts.push({
x: p.map(s, 0, N - 1, MARGIN_X, W - MARGIN_X),
y: BASE_Y - vals[i] * AMP,
v: vals[i],
});
}
const bySlot = pts.slice().sort((a, b) => a.x - b.x);
// 埋まっているところ。並べ替えても、この面積は一度も変わらない
p.noStroke();
p.fill(INK[0], INK[1], INK[2], 20 + settle * 26);
p.beginShape();
p.vertex(bySlot[0].x, BASE_Y);
for (const q of bySlot) p.vertex(q.x, q.y);
p.vertex(bySlot[bySlot.length - 1].x, BASE_Y);
p.endShape(p.CLOSE);
// 稜線。まっすぐ結ぶ——なめらかにすると、隣と隣のあいだに
// 誰もいないものを描いてしまう。この絵は隣の関係そのものが主題だから、繋ぎは直線でいい
p.noFill();
p.stroke(INK[0], INK[1], INK[2], 30 + settle * 122);
p.strokeWeight(0.7 + settle * 0.6);
p.beginShape();
for (const q of bySlot) p.vertex(q.x, q.y);
p.endShape();
// 地面。値のゼロ位置。動かないもの
p.stroke(INK[0], INK[1], INK[2], 34);
p.strokeWeight(0.6);
p.line(MARGIN_X, BASE_Y, W - MARGIN_X, BASE_Y);
// 点そのもの。色は値だけで決まる——だから並びが変わっても、絵の色の総量は変わらない
p.noStroke();
for (const q of pts) {
const w = p.constrain(q.v, 0, 1);
p.fill(
COOL[0] + (WARM[0] - COOL[0]) * w,
COOL[1] + (WARM[1] - COOL[1]) * w,
COOL[2] + (WARM[2] - COOL[2]) * w,
96 + settle * 88
);
p.circle(q.x, q.y, 2.0 + q.v * 3.4);
}
};
// ---- 並べ方をつくる ----
// src の形をなぞる席順を返す。値の集合はまったく変えない——
// 「値が小さい方から r 番目の点」を「src が小さい方から r 番目の席」へ置くだけ
function slotsFrom(src) {
const seatByRank = src
.map((v, i) => [v, i])
.sort((a, b) => a[0] - b[0])
.map((e) => e[1]);
const ptByRank = vals
.map((v, i) => [v, i])
.sort((a, b) => a[0] - b[0])
.map((e) => e[1]);
const slot = new Array(vals.length);
for (let r = 0; r < vals.length; r++) slot[ptByRank[r]] = seatByRank[r];
return slot;
}
function whiteSeq(n) {
const out = [];
for (let i = 0; i < n; i++) out.push(p.randomGaussian());
return out;
}
// Voss-McCartney。octave ごとに更新間隔が倍になる白色を重ねると 1/f になる
function pinkSeq(n) {
const OCT = 8;
const held = [];
for (let k = 0; k < OCT; k++) held.push(p.randomGaussian());
const out = [];
for (let i = 0; i < n; i++) {
for (let k = 0; k < OCT; k++) {
if (i % (1 << k) === 0) held[k] = p.randomGaussian();
}
let s = 0;
for (let k = 0; k < OCT; k++) s += held[k];
out.push(s);
}
return out;
}
// 白色の累積和。相関が強すぎて、大きなうねりがひとつだけ残る
function brownSeq(n) {
const out = [];
let acc = 0;
for (let i = 0; i < n; i++) {
acc += p.randomGaussian();
out.push(acc);
}
return out;
}
// 0..1 に収める。切り落とさない——枠から出た値があると、
// 「何も取られていない」という前提のほうが嘘になる
function normalize(arr) {
let lo = Infinity;
let hi = -Infinity;
for (const v of arr) {
if (v < lo) lo = v;
if (v > hi) hi = v;
}
for (let i = 0; i < arr.length; i++) arr[i] = (arr[i] - lo) / (hi - lo);
}
function ease(u) {
return u < 0.5 ? 4 * u * u * u : 1 - Math.pow(-2 * u + 2, 3) / 2;
}
function dailySeed() {
const d = new Date();
return (d.getFullYear() * 10000 + (d.getMonth() + 1) * 100 + d.getDate()) % 100000;
}
}