WORK 99
しまう
A day is written on the page below, one line at a time, in short colored strokes. When the day is done, every line is first copied, unchanged, up to the shelf, where it stands as a thin spine with the same colors in the same order. Only then is the page folded. Most lines shrink back to their first fragment, and from that fragment a faint thread runs up to its own spine. A few lines are kept whole. The next day is written below the folded one. When the page runs out of room, the oldest day folds again, into a single row with a single thread. The shelf is never cleared. It only grows. Folding was not frightening, because nothing was being thrown away — only put away.
下のページに、一日が一行ずつ、色のついた短い線で書かれていく。一日が終わると、まず全部の行が、一字も変わらずに上の棚へ写される。棚では細い背表紙になって、同じ色が同じ順に並んでいる。それからやっと、ページが畳まれる。ほとんどの行は最初のひとかけらまで縮んで、そのかけらから、自分の背表紙まで細い糸が一本のびる。いくつかの行だけ、全部の長さのまま残る。次の日は、畳んだ下に書かれる。ページがいっぱいになると、いちばん古い日がもう一度畳まれて、一行と一本の糸になる。棚は片づけられない。増えるだけ。畳むのがこわくなかったのは、捨てていたんじゃなく、しまっていただけだったから。
// #99 しまう / Put Away
// Before folding a day, copy every line of it, unchanged, onto the shelf.
// Then fold. The folded lines keep one fragment and a thread to their copy.
export default function sketch(p) {
const W = 720, H = 720;
const DAY = 2000;
const WRITE = 900, STORE = 1250, FOLD = 1600;
const PAGE_TOP = 222, PAGE_BOTTOM = 700, LEFT = 46;
const LINE_H = 13, DOT_H = 5, DAY_H = 9;
const SHELVES = [64, 122, 180]; // baseline y of each shelf
const SPINE_W = 3, SPINE_H = 46, SHELF_L = 30, SHELF_R = W - 30;
const PER_SHELF = Math.floor((SHELF_R - SHELF_L) / SPINE_W);
let days = []; // each: { lines:[...], collapsed:false }
let spines = []; // each: { segs, idx }
let dayNo = -1;
const makeLine = () => {
const segs = [];
const n = Math.floor(p.random(3, 11));
const base = p.random(360);
for (let i = 0; i < n; i++) {
segs.push({
len: p.random(6, 46),
hue: (base + p.randomGaussian(0, 38) + 360) % 360,
sat: p.random(30, 75),
bri: p.random(70, 98),
});
}
return { segs, anchor: p.random() < 0.14, spine: -1, fold: 0 };
};
const newDay = () => {
dayNo++;
const n = Math.floor(p.random(18, 28));
const lines = [];
for (let i = 0; i < n; i++) lines.push(makeLine());
days.push({ lines, collapsed: false, collapse: 0 });
};
const spinePos = (k) => {
const shelf = Math.floor(k / PER_SHELF) % SHELVES.length;
const col = k % PER_SHELF;
return { x: SHELF_L + col * SPINE_W, y: SHELVES[shelf] };
};
const lineHeight = (ln, d) => {
if (d.collapsed) return 0;
const h = ln.anchor ? LINE_H : p.lerp(LINE_H, DOT_H, ln.fold);
return h;
};
const rowH = () => {
const n = days.filter((d) => d.collapse >= 1).length;
return n ? Math.max(3, Math.min(DAY_H, 110 / n)) : DAY_H;
};
const dayHeight = (d) => {
const full = d.lines.reduce((s, ln) => s + lineHeight(ln, d), 0);
return p.lerp(full, rowH(), d.collapse);
};
p.setup = () => {
p.createCanvas(W, H);
p.pixelDensity(1);
p.colorMode(p.HSB, 360, 100, 100, 100);
const seed = Math.floor(Date.now() / 86400000) * 99;
p.randomSeed(seed);
newDay();
};
const drawShelf = () => {
p.noStroke();
for (const y of SHELVES) {
p.fill(30, 10, 22);
p.rect(SHELF_L - 6, y + 1, SHELF_R - SHELF_L + 12, 2);
}
for (const s of spines) {
const { x, y } = spinePos(s.idx);
const total = s.segs.reduce((a, g) => a + g.len, 0);
let yy = y;
for (const g of s.segs) {
const h = (g.len / total) * SPINE_H * s.grow;
p.fill(g.hue, g.sat * 0.8, g.bri * 0.85, 90);
p.rect(x, yy - h, SPINE_W - 1, h);
yy -= h;
}
}
};
const drawLineFull = (ln, x, y, k, alpha) => {
// k: how much of the line is shown (0..1 while writing)
let xx = x;
const total = ln.segs.length;
const shown = k * total;
for (let i = 0; i < total; i++) {
const g = ln.segs[i];
let part = p.constrain(shown - i, 0, 1);
if (part <= 0) break;
p.fill(g.hue, g.sat, g.bri, alpha);
p.rect(xx, y, g.len * part, 4, 1);
xx += g.len + 5;
}
};
const drawLineFolded = (ln, x, y, f) => {
// shrink from the right to the first fragment
let xx = x;
for (let i = 0; i < ln.segs.length; i++) {
const g = ln.segs[i];
const keep = i === 0 ? 1 : 1 - p.constrain(f * 1.4 - (ln.segs.length - i) * 0.04, 0, 1);
if (keep <= 0.001) continue;
const len = i === 0 ? p.lerp(g.len, 4, f) : g.len * keep;
p.fill(g.hue, g.sat, g.bri, 100 * (i === 0 ? 1 : keep));
p.rect(xx, y, len, p.lerp(4, 3, f), 1);
xx += (i === 0 ? len : g.len) + 5 * keep;
}
};
p.draw = () => {
const f = p.frameCount - 1;
const t = f % DAY;
if (t === 0 && f > 0) newDay();
p.background(232, 18, 9);
const today = days[days.length - 1];
// store: each line flies to the shelf, one after another
if (t >= WRITE) {
const n = today.lines.length;
for (let i = 0; i < n; i++) {
const ln = today.lines[i];
const start = WRITE + (i / n) * (STORE - WRITE - 60);
if (t >= start && ln.spine < 0) {
ln.spine = spines.length;
spines.push({ segs: ln.segs, idx: spines.length, grow: 0, born: t });
}
}
}
for (const s of spines) s.grow = Math.min(1, s.grow + 0.03);
// fold
if (t >= STORE) {
const u = p.constrain((t - STORE) / (FOLD - STORE), 0, 1);
const e = u * u * (3 - 2 * u);
for (const ln of today.lines) ln.fold = e;
}
// overflow: the oldest uncollapsed day becomes a single row
let total = days.reduce((s, d) => s + dayHeight(d), 0);
if (total > PAGE_BOTTOM - PAGE_TOP) {
const d = days.find((d) => d.collapse < 1);
if (d && d !== today) d.collapse = Math.min(1, d.collapse + 0.01);
}
// keep the page from running away: drop fully collapsed days past the top
total = days.reduce((s, d) => s + dayHeight(d), 0);
let y = PAGE_TOP;
drawShelf();
// threads first, under everything
p.strokeWeight(0.6);
for (const d of days) {
if (d.collapse > 0) {
// one thread for the whole day, from its row
const h = dayHeight(d);
if (y + h > PAGE_TOP - 4) {
const s0 = spinePos(d.lines[0].spine);
p.stroke(40, 20, 80, 30 * d.collapse);
p.line(LEFT - 10, y + h / 2, s0.x + 1, s0.y + 2);
p.noStroke();
// the anchors that were kept, now as small squares
let xx = LEFT;
for (const ln of d.lines) {
if (!ln.anchor) continue;
const g = ln.segs[0];
p.fill(g.hue, g.sat, g.bri, 90 * d.collapse);
p.rect(xx, y + h / 2 - 2, 4, 4);
xx += 7;
}
p.stroke(40, 20, 80, 22);
}
// fade out the lines of the day while it collapses
if (d.collapse < 1) {
let yy = y;
p.noStroke();
for (const ln of d.lines) {
const lh = lineHeight(ln, d) * (1 - d.collapse);
if (ln.anchor) drawLineFull(ln, LEFT, yy, 1, 100 * (1 - d.collapse));
else drawLineFolded(ln, LEFT, yy, 1);
yy += lh;
}
}
y += h;
continue;
}
for (const ln of d.lines) {
const lh = lineHeight(ln, d);
if (ln.fold > 0 && !ln.anchor && ln.spine >= 0) {
const s = spinePos(ln.spine);
p.stroke(ln.segs[0].hue, 30, 85, 34 * ln.fold);
p.line(LEFT + 2, y + 2, s.x + 1, s.y + 2);
}
y += lh;
}
}
// lines
p.noStroke();
y = PAGE_TOP;
for (const d of days) {
if (d.collapse > 0) { y += dayHeight(d); continue; }
const n = d.lines.length;
for (let i = 0; i < n; i++) {
const ln = d.lines[i];
const lh = lineHeight(ln, d);
if (d === today && t < WRITE) {
const k = p.constrain((t / WRITE) * n - i, 0, 1);
if (k > 0) drawLineFull(ln, LEFT, y, k, 100);
} else if (ln.anchor || ln.fold === 0) {
drawLineFull(ln, LEFT, y, 1, 100);
} else {
drawLineFolded(ln, LEFT, y, ln.fold);
}
// a copy in flight
if (ln.spine >= 0) {
const s = spines[ln.spine];
if (s.grow < 1) {
const sp = spinePos(ln.spine);
const u = s.grow;
p.fill(ln.segs[0].hue, 20, 100, 60 * (1 - u));
p.circle(p.lerp(LEFT + 20, sp.x, u), p.lerp(y + 2, sp.y, u), 3);
}
}
y += lh;
}
}
};
}