feat(library): read the wall's time by the year and the month, and mint only the frames near the eye

This commit is contained in:
2026-10-13 09:54:47 +07:00
parent 5d9b4c3058
commit ddbde7d426
4 changed files with 244 additions and 99 deletions
+114 -57
View File
@@ -5,8 +5,13 @@
// node holds is a card (the wall is drawn whole — no window over it, so no card
// arriving late and no spacer standing in for one), every card sits under the band
// of its own day, the last frame is reachable by scrolling with nothing left under
// it, and the rail down the wall's right edge — the shelf's own time — reads the
// day the eye is in and takes the wall to the day it is pressed for.
// it, and the rail down the wall's right edge — the shelf's own time, by the year
// and the month and nothing finer — scrolls under the reader's hand, reads the month
// the eye is in and takes the wall to the month or the year it is pressed for.
//
// The wall is also the one screen that reads the whole shelf, so what it mints is
// watched here too: a roll of years is a rail of months, and a rail whose every entry
// is in the box it is read through.
//
// It is drawn whole for a reason this check keeps an eye on: the wall used to be a
// window over the shelf, and a window measured off the window before it cost a
@@ -31,8 +36,8 @@ const APP_PORT = Number(process.env.APP_PORT ?? 5033);
const SAMPLE_PORT = Number(process.env.SAMPLE_PORT ?? 5034);
const FRAMES = Number(process.env.FRAMES ?? 300);
const FOLDER = 'WallRoll';
// The second roll: a shelf of one month, which the rail reads by the day rather
// than by the month — the other half of what the rail is (`rail` in Library.tsx),
// The second roll: a shelf of a single month, which the rail reads with one head and
// one month under it — the other half of what the rail is (`rail` in Library.tsx),
// and the shape a folder per event has. Its frames are two days of twenty.
const SMALL_FOLDER = 'OneDayRoll';
const SMALL_EACH = 20;
@@ -44,7 +49,11 @@ const SMALL_DAYS = 2;
// half of what this check is for.
const names = Array.from({ length: FRAMES }, (_, i) => `frame-${String(i + 1).padStart(4, '0')}.jpg`);
const DAY_MS = 86400000;
const DAY0 = Date.UTC(2026, 0, 1);
// The roll is spread over years, a shooting day every forty days or so: a shelf of
// months is what the rail is for, and a rail that holds three of them is a rail that
// never has to be scrolled — the years are what make it longer than the wall is tall.
const DAY_SPREAD = 40;
const DAY0 = Date.UTC(2018, 0, 1);
// The days the roll was shot on, and they are not equal: a roll is an afternoon
// of three frames and a wedding of four hundred, and the wall's own model of its
// rows is the thing that has to survive that. A shelf of equal days is a shelf a
@@ -73,7 +82,7 @@ const dayIndex = (n) => {
}
return 0;
};
const dayOfName = (name) => DAY0 + dayIndex(Number((name.match(/\d+/) ?? ['1'])[0]) - 1) * DAY_MS;
const dayOfName = (name) => DAY0 + dayIndex(Number((name.match(/\d+/) ?? ['1'])[0]) - 1) * DAY_SPREAD * DAY_MS;
// The second roll's own names and days: `one-0001` onwards, twenty to a day, over
// the fourth and fifth of March 2026 — one month, two days, which is the shelf the
@@ -100,7 +109,9 @@ const keyOf = (ts) => {
const d = new Date(ts);
return `${monthOf(ts)}-${String(d.getDate()).padStart(2, '0')}`;
};
const yearOf = (ts) => String(new Date(ts).getFullYear());
const MONTHS = [...new Set(names.map((name) => monthOf(dayOf(name))))];
const YEARS = [...new Set(names.map((name) => yearOf(dayOf(name))))];
const SMALL_DAY_KEYS = [...new Set(smallNames.map((name) => keyOf(dayOf(name))))];
// The days the big roll is cut into, which is what the wall draws bands for.
const BIG_DAYS = (() => {
@@ -359,7 +370,7 @@ step('every header names the day of the cards under it', days1.bad.length === 0,
// What the rail is read for: the entries it holds, the entry the eye is in, and where
// a press on one takes the wall. The day at the fold is read off the sections
// themselves — the first one crossing the wall's top band, which is the rule the rail
// itself reads by — and the entry the wall says it is in is read off the accent.
// itself reads by — and the month the wall says it is in is read off the accent.
const railOf = () =>
page.evaluate(() => {
const grid = document.querySelector('.adm-grid');
@@ -377,16 +388,23 @@ const railOf = () =>
}
const on = document.querySelector('.lib-rail-item.on');
return {
kinds: items.map((b) => b.getAttribute('data-kind') ?? ''),
keys: items.map((b) => (b.getAttribute('data-key') ?? '').replace('lib-rail-', '')),
// The day a press on the entry sends the wall to, which the entry carries.
ats: items.map((b) => b.getAttribute('data-at') ?? ''),
labels: items.map((b) => b.textContent.trim()),
onKind: on?.getAttribute('data-kind') ?? null,
on: on ? (on.getAttribute('data-key') ?? '').replace('lib-rail-', '') : null,
fold,
scroll: Math.round(rail?.scrollTop ?? 0),
tops: items.map((b) => b.offsetTop),
heights: items.map((b) => b.offsetHeight),
box: rail?.clientHeight ?? 0,
whole: rail?.scrollHeight ?? 0,
};
});
const monthsOf = (rail) => rail.keys.filter((_, i) => rail.kinds[i] === 'month');
const yearsOf = (rail) => rail.keys.filter((_, i) => rail.kinds[i] === 'year');
const stops = [0.15, 0.35, 0.55, 0.75, 0.95];
const seenMonths = new Set();
let scrolledBad = [];
@@ -401,13 +419,13 @@ for (const at of stops) {
const audit = await dayAudit();
scrolledBad = scrolledBad.concat(audit.bad);
const rail = await railOf();
// The entry the rail says the eye is in has to own the day at the fold — the month
// its key begins with — and the entry has to be inside the rail's own box: a rail
// read for the whole shelf and standing on its own top is a rail that says nothing.
if (!rail.fold || !rail.on || rail.fold.slice(0, 7) !== rail.on.slice(0, 7)) {
railBad.push({ at, fold: rail.fold, on: rail.on });
// The month the rail says the eye is in has to own the day at the fold, and the
// entry has to be inside the rail's own box: a rail read for the whole shelf and
// standing on its own top is a rail that says nothing.
if (!rail.fold || !rail.on || rail.onKind !== 'month' || rail.fold.slice(0, 7) !== rail.on.slice(0, 7)) {
railBad.push({ at, fold: rail.fold, on: rail.on, kind: rail.onKind });
} else {
seenMonths.add(rail.on.slice(0, 7));
seenMonths.add(rail.on);
const i = rail.keys.indexOf(rail.on);
const y = (rail.tops[i] ?? 0) - rail.scroll;
if (y < -2 || y + (rail.heights[i] ?? 0) > rail.box + 2) railBad.push({ at, on: rail.on, y, box: rail.box });
@@ -419,33 +437,72 @@ step(
JSON.stringify(scrolledBad.slice(0, 2))
);
const rail1 = await railOf();
const railMonths = [...new Set(rail1.keys.map((k) => k.slice(0, 7)))].sort();
step('the rail holds the months of the shelf', railMonths.join() === [...MONTHS].sort().join(), `${rail1.keys.join(' ')}`);
const months1 = monthsOf(rail1);
// The rail carries the shelf's own order — the reader's sort, months down the wall as
// the wall holds them — so what is compared here is the set of months, not the order
// twice over.
step(
'the rail holds the shelf by the year and the month, and by nothing finer',
[...months1].sort().join() === [...MONTHS].sort().join() &&
[...yearsOf(rail1)].sort().join() === [...YEARS].sort().join() &&
rail1.kinds.every((k) => k === 'year' || k === 'month'),
`${yearsOf(rail1).length} years, ${months1.length} months of ${MONTHS.length}`
);
step(
'and the entry it reads follows the eye down the shelf',
railBad.length === 0 && seenMonths.size === MONTHS.length,
railBad.length === 0 && seenMonths.size >= 3,
`${seenMonths.size} of ${MONTHS.length} months read, ${JSON.stringify(railBad.slice(0, 2))}`
);
// And the press: the month's own first day is where the wall has to stand, its header
// just under the wall's top edge, and the entry pressed is the one read afterwards.
const jumped = rail1.keys[1];
await page.click(`[data-key="lib-rail-${jumped}"]`);
await page.waitForTimeout(700);
const landed = await page.evaluate((key) => {
const grid = document.querySelector('.adm-grid');
const section = grid.querySelector(`[data-key="lib-day-${key}"]`);
const box = grid.getBoundingClientRect();
const on = document.querySelector('.lib-rail-item.on');
return {
top: section ? Math.round(section.getBoundingClientRect().top - box.top) : null,
on: on ? (on.getAttribute('data-key') ?? '').replace('lib-rail-', '') : null,
};
}, jumped);
step(
'a press on a month takes the wall to that day',
landed.top !== null && Math.abs(landed.top - 6) <= 6 && landed.on === jumped,
`header at ${landed.top}, entry ${landed.on}`
'the rail is longer than the wall is tall, and scrolls under the reader',
rail1.whole > rail1.box,
`${rail1.whole}px of rail in ${rail1.box}px of box`
);
// And the press: the shelf's own first day of the month is where the wall has to
// stand, its header just under the wall's top edge, the entry pressed is the month
// read afterwards, and the rail brings that entry into its own box.
const press = async (key) => {
await page.click(`[data-key="lib-rail-${key}"]`);
await page.waitForTimeout(700);
return page.evaluate((k) => {
const grid = document.querySelector('.adm-grid');
const rail = document.querySelector('.lib-rail');
const button = document.querySelector(`[data-key="lib-rail-${k}"]`);
const at = button?.getAttribute('data-at') ?? null;
const section = at ? grid.querySelector(`[data-key="lib-day-${at}"]`) : null;
const on = document.querySelector('.lib-rail-item.on');
const y = button ? button.offsetTop - rail.scrollTop : null;
return {
at,
top: section ? Math.round(section.getBoundingClientRect().top - grid.getBoundingClientRect().top) : null,
cards: section ? section.querySelectorAll('.adm-card').length : 0,
on: on ? (on.getAttribute('data-key') ?? '').replace('lib-rail-', '') : null,
onKind: on?.getAttribute('data-kind') ?? null,
seen: y !== null && y >= -2 && y + (button?.offsetHeight ?? 0) <= rail.clientHeight + 2,
};
}, key);
};
const jumped = months1[Math.floor(months1.length / 2)];
const landed = await press(jumped);
step(
'a press on a month stands the wall on that month, and the rail on its entry',
landed.top !== null &&
Math.abs(landed.top - 6) <= 6 &&
landed.on === jumped &&
landed.onKind === 'month' &&
landed.seen,
`header at ${landed.top} of ${landed.at}, entry ${landed.on}, in rail ${landed.seen}`
);
// The year is a head and not a month of the shelf: a press on it stands the wall on
// the first month the shelf holds of that year, and the eye then reads that month.
const yearRow = yearsOf(rail1)[1];
const overYear = await press(yearRow);
step(
'a press on a year stands the wall on the first month it holds',
overYear.top !== null && Math.abs(overYear.top - 6) <= 6 && overYear.at?.slice(0, 4) === yearRow && overYear.on === overYear.at?.slice(0, 7),
`year ${yearRow}: header at ${overYear.top} of ${overYear.at}, entry ${overYear.on}`
);
// The shelf walked to its own end, a screen at a time — a jump straight to the bottom
@@ -483,6 +540,21 @@ const walked = await page.evaluate(async () => {
step('the shelf is walked to its own end', walked.reached, `stopped at ${walked.top} of ${walked.room}`);
step('and stands on nothing there', walked.gap !== null && walked.gap <= 16, `${walked.gap}px of nothing under the last card`);
// What the wall mints, read at the far end of the shelf: the tiles are the frames of
// the days around the eye, and a shelf walked end to end is not a shelf of tiles held
// end to end. A frame the wall holds no tile for draws nothing at all — a folder off
// the disk is a file only this page can open — so the count of blob-backed images is
// the count of tiles the wall is holding.
const held = await page.evaluate(() => ({
cards: document.querySelectorAll('.adm-grid .adm-card').length,
tiles: document.querySelectorAll('.adm-grid img[src^="blob:"]').length,
}));
step(
'the wall mints the tiles around the eye and not the whole shelf',
held.tiles > 0 && held.tiles < held.cards / 2,
`${held.tiles} tiles held of ${held.cards} cards`
);
// A wall resized under its own eye: the class of input that cost the screen once, when
// the wall measured itself off itself. It re-columns, the rail re-reads, and the reader
// is still standing in the library after it.
@@ -518,32 +590,17 @@ await page.waitForFunction(
await page.click('[data-key="lib-view-grid"]');
await page.waitForTimeout(1500);
const rail3 = await railOf();
const smallMonth = SMALL_DAY_KEYS[0].slice(0, 7);
step(
'a shelf of one month is read by the day',
[...rail3.keys].sort().join() === [...SMALL_DAY_KEYS].sort().join() && rail3.keys.length === SMALL_DAY_KEYS.length,
'a shelf of one month is still read by the year and the month',
rail3.kinds.join() === 'year,month' && yearsOf(rail3).length === 1 && monthsOf(rail3).join() === smallMonth,
`${rail3.labels.join(' ')} / ${rail3.keys.join(' ')}`
);
const plainDay = SMALL_DAY_KEYS[0];
await page.click(`[data-key="lib-rail-${plainDay}"]`);
await page.waitForTimeout(700);
const small = await page.evaluate((key) => {
const grid = document.querySelector('.adm-grid');
const section = grid.querySelector(`[data-key="lib-day-${key}"]`);
const box = grid.getBoundingClientRect();
const on = document.querySelector('.lib-rail-item.on');
return {
top: section ? Math.round(section.getBoundingClientRect().top - box.top) : null,
cards: section ? section.querySelectorAll('.adm-card').length : 0,
on: on?.textContent.trim() ?? null,
};
}, plainDay);
const overMonth = await press(smallMonth);
step(
'a press on a day takes the wall to it, whole',
small.top !== null &&
Math.abs(small.top - 6) <= 6 &&
small.cards === SMALL_EACH &&
small.on === String(Number(plainDay.slice(8, 10))),
`header at ${small.top}, ${small.cards} cards, entry ${small.on}`
'and a press on it stands the wall on that month, whole',
overMonth.top !== null && Math.abs(overMonth.top - 6) <= 6 && overMonth.cards === SMALL_EACH && overMonth.on === smallMonth,
`header at ${overMonth.top}, ${overMonth.cards} cards, entry ${overMonth.on}`
);
await page.screenshot({ path: process.env.SHOT ?? '/tmp/wall-check.png' }).catch(() => {});