147 lines
7.4 KiB
JavaScript
147 lines
7.4 KiB
JavaScript
// The wall's own model of its rows (src/ui/dateGroups.ts, `bandRows` /
|
|
// `rowPhoto` / `photoRow`), checked where it can go wrong without a browser.
|
|
//
|
|
// The wall draws the rows around the reader and a spacer stands in for the rest,
|
|
// so a model of the rows that is out of step with the shelf drawn is a reader
|
|
// stopping at a screen of nothing with frames below them never drawn — which is
|
|
// what a flat `count / cols` count of rows is the moment the shelf is cut into
|
|
// days: every day spends a row on its band, and a day whose frames do not come
|
|
// out even over the columns leaves the rest of its last row empty.
|
|
//
|
|
// What has to hold is that the rows tile the shelf: one window per row, in
|
|
// order, meeting at both ends and never leaving a frame between two of them, and
|
|
// that a frame stands in the row the browser will draw it in.
|
|
//
|
|
// node scripts/wall-slot-check.mjs
|
|
import assert from 'node:assert/strict';
|
|
import { readFileSync } from 'node:fs';
|
|
import ts from 'typescript';
|
|
|
|
const src = readFileSync(new URL('../src/ui/dateGroups.ts', import.meta.url), 'utf8');
|
|
const js = ts.transpileModule(src, {
|
|
compilerOptions: { module: ts.ModuleKind.ESNext, target: ts.ScriptTarget.ES2022 },
|
|
}).outputText;
|
|
const { bandRows, dayAt, rowPhoto, photoRow, rowAt, rowTop, wallLayout } = await import(
|
|
`data:text/javascript,${encodeURIComponent(js)}`
|
|
);
|
|
|
|
// The frames per day, cycling — and with them the shelves the check stands on: a
|
|
// shelf of equal days, a shelf of single-frame days, one day of everything, and
|
|
// the rolls in between, which is what a camera roll is and where a flat count of
|
|
// rows stops being right.
|
|
const SHELVES = [
|
|
[300, [10]],
|
|
[200, [1]],
|
|
[120, [120]],
|
|
[300, [1, 40, 2, 3, 60, 1, 8, 120, 4, 2, 25, 7]],
|
|
[1, [1]],
|
|
[7, [2, 3]],
|
|
[0, []],
|
|
];
|
|
const COLUMNS = [1, 2, 3, 6, 7, 13];
|
|
// The two steps a shelf is stacked in, in the two shapes a measurement of them can
|
|
// come out: a band of words a good deal shorter than a row of pictures, which is
|
|
// the shelf on screen, and the two equal, which is what the model looked like when
|
|
// it was one step and has to keep working.
|
|
const STEPS = [
|
|
{ band: 44, row: 208, gap: 12 },
|
|
{ band: 208, row: 208, gap: 12 },
|
|
];
|
|
|
|
let checked = 0;
|
|
for (const [count, pattern] of SHELVES) {
|
|
// The first frame of every day, off the pattern — the same list the wall cuts
|
|
// its own `dayStarts` into.
|
|
const days = [];
|
|
for (let i = 0, day = 0; i < count; day++) {
|
|
days.push(i);
|
|
i += pattern[day % pattern.length];
|
|
}
|
|
assert.equal(days[0] ?? 0, 0);
|
|
|
|
for (const cols of COLUMNS) {
|
|
const order = bandRows(days, count, cols);
|
|
const rows = order[days.length];
|
|
// One band per day, and its frames under it in rows of `cols`: the rows the
|
|
// shelf is drawn in, which is what the model has to come to.
|
|
let drawn = 0;
|
|
for (let k = 0; k < days.length; k++) {
|
|
drawn += 1 + Math.ceil(((days[k + 1] ?? count) - days[k]) / cols);
|
|
}
|
|
assert.equal(rows, drawn, `${count} frames in ${days.length} days at ${cols} columns: ${rows} rows modelled, ${drawn} drawn`);
|
|
|
|
// The rows tile the shelf: row `r` draws from `rowPhoto(r)` and the next row
|
|
// takes it up from there, the first row starts at the first frame, and the
|
|
// row past the last answers with the end of the shelf.
|
|
let from = rowPhoto(days, count, cols, order, 0);
|
|
assert.equal(from, 0, `${count}/${cols}: the shelf starts at its first frame`);
|
|
for (let r = 1; r <= rows; r++) {
|
|
const to = rowPhoto(days, count, cols, order, r);
|
|
assert.ok(to >= from, `${count}/${cols}: row ${r} answers ${to}, behind ${from}`);
|
|
assert.ok(to <= count, `${count}/${cols}: row ${r} answers ${to} of ${count}`);
|
|
if (r === rows) assert.equal(to, count, `${count}/${cols}: the last row reaches the end`);
|
|
from = to;
|
|
}
|
|
|
|
// And a frame's own row is the row it is drawn in: that row's window starts
|
|
// at or above it, and the row after starts below it.
|
|
for (let i = 0; i < count; i++) {
|
|
const row = photoRow(days, cols, order, i);
|
|
assert.ok(row >= 1 && row < rows, `${count}/${cols}: frame ${i} is drawn in row ${row} of ${rows}`);
|
|
assert.ok(rowPhoto(days, count, cols, order, row) <= i, `${count}/${cols}: frame ${i} stands past the start of row ${row}`);
|
|
assert.ok(rowPhoto(days, count, cols, order, row + 1) > i, `${count}/${cols}: frame ${i} stands past the end of row ${row}`);
|
|
checked++;
|
|
}
|
|
|
|
// The shelf's own height and the two spacers that stand in for the days not
|
|
// drawn, in the two steps it is actually stacked in. What has to hold is that
|
|
// the shelf comes out the same height whichever window is over it: a height
|
|
// that moves with the window is the reader walking down to the end of the
|
|
// scroll and stopping a screen short of the frames it was standing in for.
|
|
for (const steps of STEPS) {
|
|
const at = `${count} frames in ${days.length} days at ${cols} columns, band ${steps.band}/row ${steps.row}`;
|
|
const layout = wallLayout(days, count, cols, steps);
|
|
const heights = days.map(
|
|
(start, k) => steps.band + Math.ceil(((days[k + 1] ?? count) - start) / cols) * steps.row
|
|
);
|
|
const drawnHeight = heights.reduce((sum, h) => sum + h, 0);
|
|
assert.equal(layout.height, days.length ? drawnHeight + (days.length - 1) * steps.gap : 0, `${at}: the shelf is ${layout.height} tall`);
|
|
assert.equal(layout.rows, rows, `${at}: ${layout.rows} rows of the two steps, ${rows} drawn`);
|
|
if (!days.length) continue;
|
|
assert.equal(layout.ends[days.length - 1], layout.height, `${at}: the last day ends at the shelf's own height`);
|
|
|
|
// A row stands below the row before it, and the top it stands at reads back
|
|
// as the row it is — which is what puts a scroll where a frame is.
|
|
let last = -1;
|
|
for (let r = 0; r < layout.rows; r++) {
|
|
const top = rowTop(layout, r);
|
|
assert.ok(top > last, `${at}: row ${r} stands at ${top}, behind ${last}`);
|
|
last = top;
|
|
assert.equal(rowAt(layout, top), r, `${at}: row ${r} at ${top} reads back as ${rowAt(layout, top)}`);
|
|
}
|
|
assert.equal(rowTop(layout, layout.rows - 1) + steps.row, layout.height, `${at}: the last row reaches the shelf's own height`);
|
|
assert.equal(dayAt(days, count - 1), days.length - 1, `${at}: the last frame is filed under the last day`);
|
|
|
|
// And every window of days lays out the same shelf: the spacer over it, the
|
|
// days drawn, the spacer under it and the grid's own gaps add up to one
|
|
// height, whichever days are drawn.
|
|
for (let a = 0; a < days.length; a++) {
|
|
for (let b = a; b < days.length; b++) {
|
|
// Laid out the way the DOM lays it: a spacer where the wall draws one
|
|
// (a height of nought is no spacer), the days drawn, the grid's gap
|
|
// between one item and the next.
|
|
const above = a ? Math.max(0, layout.tops[a] - steps.gap) : 0;
|
|
const below = Math.max(0, layout.height - layout.ends[b] - steps.gap);
|
|
const items = b - a + 1 + (above > 0 ? 1 : 0) + (below > 0 ? 1 : 0);
|
|
let sum = 0;
|
|
for (let k = a; k <= b; k++) sum += heights[k];
|
|
const total = above + sum + below + (items - 1) * steps.gap;
|
|
assert.equal(total, layout.height, `${at}: days ${a}..${b} stand ${total} tall, the shelf ${layout.height}`);
|
|
}
|
|
}
|
|
checked++;
|
|
}
|
|
}
|
|
}
|
|
console.log(`ok ${checked} frames placed on ${SHELVES.length} shelves`);
|