WORK 103
ゆずる
Three points. The one on the left tells the near one first, along a thin private line. A while later it speaks to everyone, in a ring that spreads across the whole ground. Sometimes the near one speaks first — a warm ring of its own — and the far one hears it early. Mostly the near one gives way. The far one lights when a ring reaches it: brightly if it is hearing this for the first time, faintly if it already knew. Below, its record runs left to right. The ground keeps what passed often: the private line, drawn again and again along the same path, becomes the brightest thing on it, and the public rings settle into a fine pattern around the speaker. The warm rings hardly stay on the ground at all. They stay in the record, as a bright warm mark followed by a faint one.
三つの点。左の点は、まず近い点にだけ、細い線で先に伝える。しばらくして、みんなに向けて話す。輪が画面じゅうに広がっていく。ときどき近い点が先に話してしまう——自分の暖色の輪で——すると遠い点は早く聞く。たいていは、近い点は道をゆずる。遠い点は輪が届いた瞬間に光る。初めて聞くなら明るく、もう知っていたなら、かすかに。下の帯は、遠い点の記録。地面は何度も通ったものを覚える。同じ道を何度も引かれた細い線が、いちばん明るいものになり、みんなへの輪は話し手のまわりに細かい模様として沈む。暖色の輪は、地面にはほとんど残らない。残るのは記録の方で、明るい暖色の印と、そのあとのかすかな印として。
// #103 ゆずる / Giving Way
// S tells N privately first. Later S speaks to everyone.
// Sometimes N speaks first (forestalls); mostly N gives way.
// F lights up when a ring reaches it: bright if it is the first time, dim if not.
export default function sketch(p) {
const W = 720;
const H = 720;
const FIELD_H = 620;
const STRIP_Y = 648;
const STRIP_H = 52;
const EP_LEN = 420;
const SPEED = 2.2;
const FORESTALL = 0.3;
const COOL = [120, 170, 255];
const WARM = [255, 160, 90];
const PRIV = [190, 170, 230];
let g; // persistent trace layer
let rings = [];
let ep = null;
let epIndex = -1;
let glowN = 0;
let glowF = 0;
let glowS = 0;
let glowFCol = COOL;
let stripX = 0;
const base = {
S: { x: 170, y: 300 },
N: { x: 300, y: 268 },
F: { x: 560, y: 360 },
};
function jit(pt) {
return { x: pt.x + p.random(-7, 7), y: pt.y + p.random(-7, 7) };
}
function newEpisode() {
epIndex++;
const forestall = p.random() < FORESTALL;
ep = {
t: 0,
S: jit(base.S),
N: jit(base.N),
F: jit(base.F),
forestall,
nSpeakAt: forestall ? 70 + Math.floor(p.random(0, 70)) : -1,
sSpeakAt: 180 + Math.floor(p.random(-15, 25)),
fHeard: false,
nHeardPublic: false,
};
}
function emit(origin, col, who) {
rings.push({ x: origin.x, y: origin.y, r: 0, col, who, hitF: false, hitN: false, ep: epIndex });
}
function markStrip(col, bright) {
const h = p.map(bright, 0, 1, 6, STRIP_H - 4);
p.push();
p.blendMode(p.BLEND);
p.stroke(col[0], col[1], col[2], 60 + bright * 195);
p.strokeWeight(2);
p.line(stripX, STRIP_Y + STRIP_H, stripX, STRIP_Y + STRIP_H - h);
p.pop();
}
p.setup = () => {
p.createCanvas(W, H);
p.randomSeed(103);
g = p.createGraphics(W, FIELD_H);
g.background(8, 9, 14);
p.background(8, 9, 14);
newEpisode();
};
p.draw = () => {
// slow fade of the trace layer
if (p.frameCount % 40 === 0) {
g.blendMode(g.BLEND);
g.noStroke();
g.fill(8, 9, 14, 6);
g.rect(0, 0, W, FIELD_H);
}
const e = ep;
// private line S -> N
if (e.t <= 40) {
const k = e.t / 40;
g.blendMode(g.ADD);
g.stroke(PRIV[0], PRIV[1], PRIV[2], 14);
g.strokeWeight(1);
g.line(e.S.x, e.S.y, p.lerp(e.S.x, e.N.x, k), p.lerp(e.S.y, e.N.y, k));
if (e.t === 40) glowN = 1;
}
if (e.t === 2) glowS = 0.5;
if (e.t === e.nSpeakAt) {
emit(e.N, WARM, 'N');
glowN = 1;
}
if (e.t === e.sSpeakAt) {
emit(e.S, COOL, 'S');
glowS = 1;
}
// rings
for (const r of rings) {
r.r += SPEED;
if (p.frameCount % 5 === 0) {
const a = p.map(r.r, 0, 650, 40, 3, true);
g.blendMode(g.ADD);
g.noFill();
g.stroke(r.col[0], r.col[1], r.col[2], a);
g.strokeWeight(1);
g.circle(r.x, r.y, r.r * 2);
}
if (r.ep === epIndex) {
if (!r.hitF && r.r >= p.dist(r.x, r.y, e.F.x, e.F.y)) {
r.hitF = true;
const bright = e.fHeard ? 0.28 : 1;
e.fHeard = true;
glowF = Math.max(glowF, bright);
glowFCol = r.col;
markStrip(r.col, bright);
}
if (r.who === 'S' && !r.hitN && r.r >= p.dist(r.x, r.y, e.N.x, e.N.y)) {
r.hitN = true;
glowN = Math.max(glowN, 0.3);
}
}
}
rings = rings.filter((r) => r.r < 900);
// compose
p.blendMode(p.BLEND);
p.image(g, 0, 0);
// live rings, a little brighter
const ctx = p.drawingContext;
ctx.save();
ctx.beginPath();
ctx.rect(0, 0, W, FIELD_H);
ctx.clip();
p.blendMode(p.ADD);
p.noFill();
for (const r of rings) {
const a = p.map(r.r, 0, 650, 90, 0, true);
p.stroke(r.col[0], r.col[1], r.col[2], a);
p.strokeWeight(1.2);
p.circle(r.x, r.y, r.r * 2);
}
// the three points
const dot = (pt, glow, col) => {
p.noStroke();
for (let i = 4; i >= 1; i--) {
p.fill(col[0], col[1], col[2], 18 * glow);
p.circle(pt.x, pt.y, 6 + i * 9 * glow);
}
p.fill(230, 232, 240, 120 + 135 * glow);
p.circle(pt.x, pt.y, 4);
};
dot(e.S, glowS, COOL);
dot(e.N, glowN, e.forestall && e.t >= e.nSpeakAt && e.nSpeakAt >= 0 ? WARM : PRIV);
dot(e.F, glowF, glowFCol);
ctx.restore();
glowS *= 0.97;
glowN *= 0.985;
glowF *= 0.975;
// strip: the far listener's record
p.blendMode(p.BLEND);
if (p.frameCount === 1) {
p.noStroke();
p.fill(8, 9, 14);
p.rect(0, FIELD_H, W, H - FIELD_H);
}
stripX = (p.frameCount * 0.12) % W;
p.noStroke();
p.fill(8, 9, 14);
p.rect(stripX + 1, STRIP_Y - 2, 14, STRIP_H + 4); // clear just ahead
p.stroke(60, 62, 75);
p.strokeWeight(1);
p.line(0, STRIP_Y + STRIP_H + 0.5, W, STRIP_Y + STRIP_H + 0.5);
e.t++;
if (e.t >= EP_LEN) newEpisode();
};
}