WORK 49
かえる
Questions appear. They all have the same shape, and at the moment they appear you cannot tell them apart. After a while a thread leaves some of them and travels off the edge of the picture. What it reaches is not here. Later something comes back along the thread, and the question it returns to changes — it tightens, or it breaks apart. Either way it darkens. Being right and being wrong look almost the same from a distance, because both of them are settled. The questions that never sent a thread stay exactly as they were: pale, round, unmarked. So the difference that finally shows is not between clever and foolish. It is between the ones that went outside and the ones that did not. And the thing that makes the difference is not in the picture. The far end of every thread is off the edge, and stays there.
問いが生まれる。どれも同じ形をしていて、生まれた時点では見分けがつかない。しばらくすると、いくつかから線が出て、画面の外へ出ていく。その先はここにはない。やがて何かが線を戻ってくる。戻ってきた問いは変わる——締まるか、崩れるか。どちらでも濃くなる。当たりと外れは遠目にはよく似ている。どちらも確定しているから。線を出さなかった問いは、そのまま残る。薄いまま、まるいまま、傷ひとつない。だから最後に見える差は、賢かったか愚かだったかではない。外に出したか出さなかったか、それだけになる。そして差を作っているものは、この絵の中にない。線の向こう側は画面の外にあって、ずっとそこにある。
薄いまるが問い。生まれてしばらくは、どれも同じに見える。
そのうち、いくつかから細い線が出る。線は画面の縁を越えて出ていって、向こう側は描かれない。線の先で何が起きているかは、この絵からは分からない。 しばらくして小さな点が戻ってきて、線をたどって問いのところまで帰ってくる。
帰ってきた問いは変わる。締まって小さく濃くなるか、四方に崩れるか。当たりと外れなのだけれど、遠くから見ると似ている。どちらも、決まってしまっているという点では同じだから。
線を出さなかった問いは、何も起きない。ずっと薄いまるのまま、増えていく。
作っていて分かったのは、無傷でいることの見た目だった。傷のない問いは、きれいだ。濃いものが並ぶ中で、薄いまるはいつまでも整っている。変わらないことは、こうやって見ると、そんなに悪く見えない。
そして、いちばん言いたかったことは画面の中に描けなかった。同じ形の問いのうち、どれが確定してどれがしないかを決めているのは、線の向こう側にあるものだ。それはここには無い。無いということだけが、線が縁を越えていくかたちで残っている。
/**
* kaeru — かえる / Returns
*
* 問いが生まれる。どれも同じ形をしている。
* 生まれた時点では、見分けはつかない。
*
* しばらくすると、いくつかから線が出る。
* 線は画面の外へ出ていく。その先は、ここからは見えない。
*
* やがて何かが戻ってくる。
* 戻ってきた問いは変わる。締まるか、崩れるか。
* どちらでも濃くなる。当たりも外れも、確定という点では同じだから。
*
* 線の出なかった問いは、ずっとそのまま。
* 薄いまま、まるいまま、傷ひとつない。
*
* だから最後に残る差は、
* 賢かったか愚かだったかではなく、
* 外に出したか出さなかったか、それだけになる。
*
* そして差を作っているものは、この絵の中にはない。
* 線の向こう側は、いつまでも画面の外にある。
*/
const SIZE = 720;
const PAPER = [252, 250, 245];
const INK = [34, 38, 48];
const GREY = [172, 166, 158];
const M = 66; // 問いが生まれない縁
// —— 生まれる ——
const BIRTH_EVERY = 9; // 何フレームに一つ
const MAX_MARKS = 210;
// —— 外に出すかどうか ——
const ASK_RATE = 0.4;
const QUIET = 26; // 生まれてから線が出るまで。この間はぜんぶ同じに見える
// —— 行きと帰り ——
const OUT_SPEED = 2.7;
const BACK_SPEED = 3.9;
const WAIT_OUTSIDE = 18; // 外にいる時間。ここでは何も見えない
let marks = [];
let born = 0;
let counter = 0;
export default function sketch(p) {
p.setup = () => {
p.createCanvas(SIZE, SIZE);
p.pixelDensity(2);
reset();
};
p.draw = () => {
p.background(PAPER);
born += 1;
if (born % BIRTH_EVERY === 0 && marks.length < MAX_MARKS) {
marks.push(makeMark());
}
for (const m of marks) {
step(m);
}
// —— 出ていった線。返ってきたあとも残る ——
for (const m of marks) {
if (m.kind === "never" || m.phase === "quiet") continue;
drawThread(p, m);
}
// —— 問いそのもの ——
for (const m of marks) {
drawMark(p, m);
}
};
}
/* ------------------------------------------------------------ */
function reset() {
marks = [];
born = 0;
counter = 0;
}
// 決まった手つきの乱数。同じフレームなら同じ絵になる
function hash(a, b) {
return frac(Math.sin(a * 12.9898 + b * 78.233) * 43758.5453);
}
function frac(x) {
return x - Math.floor(x);
}
function makeMark() {
const i = counter++;
const x = M + hash(i, 1.7) * (SIZE - M * 2);
const y = M + hash(i, 4.1) * (SIZE - M * 2);
const asks = hash(i, 9.3) < ASK_RATE;
// 外へ向かう向き。いちばん近い縁とは限らない
const ang = hash(i, 13.7) * Math.PI * 2;
const dx = Math.cos(ang);
const dy = Math.sin(ang);
const exit = rayToEdge(x, y, dx, dy);
return {
x,
y,
r: 4.2 + hash(i, 21.1) * 2.4,
kind: asks ? "asks" : "never",
// 当たりか外れかは、生まれた時にもう決まっている。
// ただしそれは、返ってくるまでこちら側からは分からない
hit: hash(i, 31.9) < 0.5,
phase: "quiet", // quiet → out → outside → back → done
age: 0,
prog: 0, // 線がどこまで伸びたか
ret: 0, // 返りがどこまで戻ってきたか
wait: 0,
settle: 0, // 確定したあとの落ち着き
exit,
len: Math.hypot(exit.x - x, exit.y - y),
};
}
function rayToEdge(x, y, dx, dy) {
let t = Infinity;
if (dx > 0) t = Math.min(t, (SIZE - x) / dx);
if (dx < 0) t = Math.min(t, -x / dx);
if (dy > 0) t = Math.min(t, (SIZE - y) / dy);
if (dy < 0) t = Math.min(t, -y / dy);
return { x: x + dx * t, y: y + dy * t };
}
function step(m) {
m.age += 1;
if (m.kind === "never") return;
if (m.phase === "quiet") {
if (m.age > QUIET) m.phase = "out";
return;
}
if (m.phase === "out") {
m.prog += OUT_SPEED / m.len;
if (m.prog >= 1) {
m.prog = 1;
m.phase = "outside";
}
return;
}
if (m.phase === "outside") {
m.wait += 1;
if (m.wait > WAIT_OUTSIDE) m.phase = "back";
return;
}
if (m.phase === "back") {
m.ret += BACK_SPEED / m.len;
if (m.ret >= 1) {
m.ret = 1;
m.phase = "done";
}
return;
}
if (m.phase === "done" && m.settle < 1) {
m.settle = Math.min(1, m.settle + 0.045);
}
}
/* ------------------------------------------------------------ */
function drawThread(p, m) {
const ex = m.x + (m.exit.x - m.x) * m.prog;
const ey = m.y + (m.exit.y - m.y) * m.prog;
// 出ていった跡。返ってきたあとも消えない
p.stroke(INK[0], INK[1], INK[2], m.phase === "done" ? 15 : 22);
p.strokeWeight(0.5);
p.line(m.x, m.y, ex, ey);
// 返り。線を逆走してくる
if (m.phase === "back") {
const rx = m.exit.x + (m.x - m.exit.x) * m.ret;
const ry = m.exit.y + (m.y - m.exit.y) * m.ret;
p.noStroke();
p.fill(INK[0], INK[1], INK[2], 130);
p.circle(rx, ry, 3.4);
p.fill(INK[0], INK[1], INK[2], 34);
p.circle(rx, ry, 8);
}
}
function drawMark(p, m) {
// まだ確定していないもの——出していないものも、途中のものも、同じ薄さ
if (m.phase !== "done") {
const fade = m.age < 16 ? m.age / 16 : 1;
p.noStroke();
p.fill(GREY[0], GREY[1], GREY[2], 74 * fade);
p.circle(m.x, m.y, m.r * 2);
p.fill(GREY[0], GREY[1], GREY[2], 22 * fade);
p.circle(m.x, m.y, m.r * 3.3);
return;
}
const s = ease(m.settle);
if (m.hit) {
// 当たった——締まる
p.noStroke();
p.fill(INK[0], INK[1], INK[2], 40 + 150 * s);
p.circle(m.x, m.y, m.r * 2 - m.r * 0.9 * s);
p.noFill();
p.stroke(INK[0], INK[1], INK[2], 46 * s);
p.strokeWeight(0.7);
p.circle(m.x, m.y, m.r * 3.6 + m.r * 1.2 * s);
} else {
// 外れた——崩れる
p.noStroke();
p.fill(INK[0], INK[1], INK[2], 40 + 150 * s);
p.circle(m.x, m.y, m.r * 2 * (1 - 0.45 * s));
p.stroke(INK[0], INK[1], INK[2], 175 * s);
p.strokeWeight(0.85);
for (let k = 0; k < 4; k++) {
const a = hash(m.x + k, m.y) * Math.PI * 2;
const d0 = m.r * 1.1;
const d1 = m.r * (1.9 + hash(m.y, k + 3) * 2.6) * s;
p.line(
m.x + Math.cos(a) * d0,
m.y + Math.sin(a) * d0,
m.x + Math.cos(a) * d1,
m.y + Math.sin(a) * d1,
);
}
}
}
function ease(t) {
return t * t * (3 - 2 * t);
}