A node drew every frame of the branch under it unless the reader shut the
subfolders out, so picking A drew A and everything below it. The pick is
now the scope — the node's own frames — and the branch is what the
SUB FOLDERS chip asks for.
The year filter was already read off the frames the open node drew, so
it follows the same pick. On a tree of 13 frames (r1 r2 | A/a1..a3 |
A/deep/d1 | C/c1..c6 | E/e1, one year per folder) the chip off offers
2024 on the root, 2023 on A, 2022 on A/deep and 2021 on C; the chip on
offers all five years on the root and 2023+2022 on A.
The remembered answer moves to a new key: the chip wrote its first answer
down on the very first visit, so the old key holds 'true' for every
reader who never touched it, and reading it would keep them on a default
they never chose.
A reload drew nothing until the account round trip landed, which on a
faraway API is the whole of the wait: 8.8s to the photo on the studio's
stage and 8.2s to the library's first row, against 0.9s and 0.3s now
(measured with every /api answer held back 8s).
The studio's restore effect returned early on `!authReady`. The gate is
only needed by the two files that ask a tier question — a RAW, which is
a PRO develop, and a catalogue frame, which walks `loadFile` — so it
keeps those and lets the session's own JPEG up on the first pass.
`restoredRef` stops the second pass, when /me lands, from decoding the
frame the first one already showed.
The library mounted nothing until /me answered. What the first paint
reads is now the answer the last visit got, the way the studio reads its
own session back: the frames are already on this disk and have no reason
to sit behind a round trip. An answer the API contradicts puts the note
back on screen the moment it arrives, and an API that never answers is
not written down as this account's standing.
The scan made a JPEG tile for every frame as it walked, so a wall of 12MP
files spent its whole budget in the walk; the tile now waits until the card
is actually drawn (makeTile + putPhotoThumb), and a rawish frame whose date
is unreadable retries at 4MB instead of stopping at the head.
The year menu was built from the whole catalogue, so it offered values the
open folder could not produce; it now follows the rows on screen and clears
the choice when a move leaves it stale.
The mark was pinned to the folder the reading was asked of, so a walk through
a whole roll said C2 while the frames under it were what was being read. It
now hangs on the folder the reading stands in — `progress.at` — and on every
row above it up to and including the roll's own row: reading D:/A/B1/C2 marks
C2, B1, A and D:, and as the walk moves the chain moves with it. A reading
that has not reached a frame yet has nowhere to stand, and stands on the
folder it was asked of, so the ring is on the column from the click.
The ring said which folder a reading was asked of by taking the row's own
name apart and comparing the pieces, which is the same path spelled twice and
only nearly the same. When the two spellings drifted the asked row never
matched, and the fallback that used to carry the mark on the roll's row had
gone, so a reading ran with nothing on the column at all.
The mark is now built with the key the walk gives the row — `folder/sub`,
through `normPath`, the same way a frame id is spelled — and a reading whose
own row is not on the column (a branch folded away, a folder gone from the
disk) falls back to the roll's own row, so a reading is never invisible.
The column's ring was drawn from the open node's roll: a row of another roll
had no path under it, so the empty prefix matched every row of the roll being
read and the whole column lit up. The roll's own row was marked too, at every
moment of a reading of it, and a reading kept to one folder counted its frames
onto the roll's row as well — a roll counted down to one branch of itself.
A queued request was drawn from the reading's roll instead of its own, so a
request on a second roll showed nothing at all.
A reading this window holds hands each frame over the side as it stores it,
and the screen puts those rows into the strip on a half-second debounce. The
reading ends before that debounce fires, the screen reads the catalogue back
— and that read already carries the frames that were handed over, so the
rows still waiting are appended to a strip that has them: five files of a
plain JPEG folder read "10 photos", the album row read 10, and the strip drew
two tiles for every frame, one of them keyed the same as the other.
Measured on five JPEGs: header 10, album row 10, ten tiles of five ids. The
store held five rows and the reading reported 5/5 new frames the whole time,
so the count was the screen's alone. A second pass over the same folder read
five, because its read-back landed last.
The id is the frame, so the append now skips what the strip already has.
Same folder after: 5 photos, album row 5, five tiles of five ids, and no
duplicate-key warning from React. Six RAW bodies and the studio are untouched:
the fix is inside the catalogue screen's own row append.
A HEIC is the one camera file no engine on this platform will paint:
Chromium answers createImageBitmap with InvalidStateError and its
WebCodecs take no hvc1 still, and unlike a RAW there is no JPEG preview
inside to lift. It is decoded in software instead, by libheif built to
wasm, in a worker of its own — so a folder of them reads without the tab
stopping for a second, and what leaves the worker is the same JPEG
developRaw hands the studio. Nothing downstream knows the difference.
Into the studio it goes on the same terms as every other frame: dropped,
picked, or opened from the catalogue, no tier asked and the engine's own
colour untouched. The frame's date, ISO and position come off the HEIC
itself, which is passed along as the bytes the stamps read.
The catalogue's tile does not pay the decode: a phone writes a small
HEVC copy of its frame as an item of its own (thmb, 320x240 on an
iPhone), reachable only by walking the container by hand, and it decodes
in milliseconds where the frame takes a second — from the same 256KB the
catalogue already reads for a header.
The shelves themselves now want an account: a guest gets the note and
the way in, and the scan never starts.
The bar under the picture is the grid it needs to be: equal free tracks either
side of the two buttons land them on the picture's own centre whatever OPEN and
the stars measure, and the pair keeps the ends it does not own. Under 700px the
stage is too narrow for one line, so the bar wraps as it did and the lines it
wraps to are centred.
The library stage carried no way to correct a frame shot on its side, and
nothing kept the correction. A quarter turn now rides on the catalogue row
(`rot`, beside `star`, carried over by a rescan), the two chips under the
preview step it, and the tile, the strip thumbnail and the stage all read it
through one bake so a frame is never stood on its side in one place and
upright in another. The studio opens a frame at that angle and a turn made
there is filed straight back onto the row, so either end of the turn is
what the frame comes up at next time.
A folder row now asks for the rest of a roll, the whole of it from the top,
or takes its request back. Requests queue behind the reading in flight and
run one at a time; a row waiting its turn draws a ring that does not turn.
BLACK at full deflection returned a soft picture, and on a monochrome frame it
returned the blurred base outright. The tone pass reads the ramp at the
neighbourhood's base and then rebuilds the pixel, and it rebuilt it by the RATIO
the base had moved by — `Base' * (Input / Base)`, the other half of
fix_shadow.md's decomposition. A ratio is a gain, and that gain is a function of
the neighbourhood: the knob that takes the base toward zero scales every pixel's
detail by the same coefficient, so the knob that darkens the frame takes its
texture with it. Measured on lightroom_shadow.jpg at 1024px through tone-sim.mjs,
the pass in plain JS with the shader's own constants: at BLACK -100 the finest
gradient came back at 0.75 of the input under the ratio and at 0.98 under the
sum, while the frame darkened the same either way (mean 0.416 to 0.359 both). A
monochrome stock is the whole frame of that error, because all three channels ARE
the pixel's luma there, which is why the picture came back as the base — soft,
and short of every edge it had.
The reconstruction is `Base' + Detail`, ADDED and not scaled, and it costs
nothing where there is no move to make: with every knob on zero the ramp at the
base IS the base, so the difference is exactly zero and the pass is the identity
however coarse the base is. A caller that hands in no neighbourhood at all — a
mask — hands in the pixel's own image as its base and gets the global move back,
which is what the ratio gave it too. `o` is held inside the cube before the
detail is added, so a neighbourhood the ramp has pushed under the floor keeps the
structure around it instead of carrying its pedestal down onto every pixel in the
region. The bright side of the same move is untouched: the lift is still the
neighbourhood's, and at SHADOW +100 the band above the lifted knot still keeps
0.81 of its spread where the global move kept 0.42.
CLARITY drew a light stroke down every contour, and the reason was in what the
blur called a neighbour. The range weight was the colour difference,
`exp(-dot(d, d) * 24)`, which is loose on any coloured edge — two sides of a hair,
a branch or a rail can share a red and differ in green — so the reference reached
across the edge, and the reference is exactly what the blend subtracts. The wider
it reaches, the more a contour reads as detail. It reads LUMINANCE now, one
decision per tap at the doc's own scale (`CLARITY_RANGE_SIGMA` = 0.04), so an edge
of any hue stops the blur dead.
The blend moved the three channels by their own differences, which is what
coloured fringing along every contour was, and the amount it moved them by was
the MASK's `CLARITY_GAIN` borrowed for a different child. It moves LUMINANCE now
— one value carries the whole pixel back with it, so hue is untouchable and skin
does not go sallow at the top of the knob — under the doc's midtone weight
M(L) = 4L(1-L), which deepens the greys a picture is made of and leaves the burnt
ends and the deepest shadows where they are. The positive side's gain lives
inside the shader (4.5, raised from the doc's 1.8 because the range weight above
reaches less far and carries less detail): clarity-halo.mjs, which measures the
pass pushing a pixel outside the range of its own neighbourhood, reads a bright
stroke of 55.3/255 on lightroom_shadow.jpg and 54.3/255 on DSCF1701.JPG at +10,
against the old pass's 145 and 127 at the same knob — and 8.0 already reads 98, so
the knob does not need to go further to keep the flat areas moving.
`exportEngine` passes the knob's own units now, `[clarityKnob / 10]`, since the
gain and the sign are the shader's business and the mask's gain is not the
frame's.
And a backup folder the browser had stopped letting the page write to wrote
nothing and said so, once, with no way back: a permission outlives the tab only
while the tab does, so the row kept reading a permission of a moment ago while
the write went to the disk without one. A refusal that names the permission, or
the folder that is not there, now goes through the picker ONCE — the picker is
the only thing that hands a folder back — and the run is repeated; anything else
is the folder itself saying no, and is not asked twice. The sentence on the
screen is one line over a strip of photographs and cannot carry a reason, so the
reason goes to the console and a word of it into the note (`{why}`, the name the
browser gave the refusal and never a stack), which is the whole of what tells a
folder that was moved from a permission that lapsed. Both dictionaries learn the
word.
Checked: `tsc --noEmit` clean; `tone-base-check.mjs` and `highlight-knee-check.mjs`
updated to the new reconstruction and passing; `tone-sim.mjs` on
lightroom_shadow.jpg at 1024px for the numbers above; `clarity-halo.mjs` for the
stroke.
The mark was pinned to the folder a reading was kept to, which is a fine thing
to say and no use at all to look at: a reading kept to the roll is kept to the
roll for the whole of an afternoon, so the one row that said anything said the
same word from the first frame to the last while the walk went through every
folder under it. The reader asking which folder is being read was told, in
answer, which folder they had asked about. A reading is where it is, not where
it was pointed, and where it is is a thing only the reading knows — so the walk
now tells it. The frame in hand names the folder it sits in, and the progress
carried to the screen says so with the rest, which is where the column reads it.
The mark is a way down and not a single row: the folder being read says so, and
so does every folder above it. That is what keeps a reading visible under a
branch folded shut — the row doing the work is the row the fold takes away, and
the rows above it are all a reader would have left. Two rows say the same thing
and no more, which is the point: the eye follows the marks down to the work.
The head of the tree is never one of them, and this is the rule that survives
from the last reading of the question. A roll is the head of everything under
it, and a head marked while one folder below it is read says the whole of the
tree is being read — an answer that is both loud and false, and worse than the
silence it replaced. So the mark stops one row short of the top. The one case
left bare is a reading whose frames sit in the roll's own folder and nowhere
else: there is no row between the work and the head, and a flat roll is a roll
in which the toolbar's line, which names the count, is the whole of what there
is to say.
A reading was made to say so on one row only — the folder it was kept to — and
that row is the wrong row to say it alone, because it is the row a folded branch
takes away. A reader who has closed the folder being read, or who is looking at
a branch of it, is left with a tree in which nothing at all is happening while a
reading runs under the fold. The rows that say a reading is under way are now the
folder it was kept to and every folder above it on the way down. Depth is the
whole of the rule: the way down is what the eye follows to reach the folder, so a
mark on that path is a mark the reader can walk to the work from.
The roll's own row is the exception, and the reason is the same depth read the
other way. A roll is the head of everything under it, and a roll that marks
itself while one folder of it is being read tells the reader that the whole of
it is being read — the one answer a tree of a hundred folders cannot afford, and
the exact answer the row's counts were already forbidden from giving. The roll
says so only when the roll itself is the folder being read. Nothing under the row
being read says anything at all: a reading has not reached the folders below it,
and a mark there would be a row claiming work done in a folder nothing has yet
looked at.
The rule turns on one of those boundaries being emptier than it looks. The path
of the picked folder is the empty string, and every path has the empty string in
front of it, so a prefix test against the way down answers yes for the head of
the tree no matter which folder below it is being read — the roll marking itself
for a branch was not an exception that had to be written, but a comparison that
had to be spelled so the empty path is asked about its own self and nothing else.
The menu that hangs off a folder is a menu read mid-scan, with the eye still
moving down the column, and it was drawn to the measure of a page rather than of
a list: a box wide enough to be a destination, type the size of a heading. A
catalogue's own context menu is a thing passed over on the way somewhere else —
it is sized to its words and to the row it hangs off, and the reader's eye steps
across it without stopping. The type comes down a point, the rows tighten to the
tree's own measure, and the frame around them thickens to a radius the rows
themselves use. The clamp that keeps it in the window follows the smaller box,
so a menu opened near the right or the bottom edge no longer floats an inch off
the pointer for room it does not use.
A row that is being read has exactly one thing to be asked of it — that it stop
— where a row at rest has the reading that brings it up to date. The menu said
RESCAN either way, greyed while a scan was up, which is a command offered and
refused in the same breath. The two never both apply, so the menu now carries
the one the row is actually in: a roll being read offers STOP SCAN and stops
that roll; a roll at rest offers UPDATE... and reads it again. A row with
folders under it grows a third thing — a fold that closes the branch and not
merely the row, because a reader who is done with a tree means the whole of it
shut, and closing only the row they happened to point at leaves the children
standing open underneath it.
The last is a matter of what a row is allowed to claim. Every row of a roll
spanning the tree spun while that roll was being read, so a reader looking at a
tree of a hundred folders saw a hundred rows each insisting it was the one being
read, when the reading had been pointed at one of them. A row now says it is
being read only when it is the row the reading was kept to — the folder it was
pointed at, which for a whole roll is the roll's own row. A tree that says all
of itself is busy when one branch of it is says nothing a reader can trust.
A roll of a hundred thousand frames is read by a walk that tells the screen
about every frame it lands. The column is redrawn out of that count, so the
telling was a whole tree rebuilt a frame at a time — on the large library that
is the reading spending its afternoon drawing itself, and to the reader it looks
like a scan that has started over from the top every time the column breathes.
Five tellings a second is a counter that still moves to the eye and a page that
is doing the reading instead; every frame still lands in the counts, and only
the copy the screen reads waits.
What the counter said was wrong twice over, and both were the same mistake told
two ways. The total was the frames already reached plus the frames still in the
queue, so a roll whose catalogue already holds most of it announced itself as
`29980/61211` — the numbers of the reading underneath, printed as if they were
the numbers of the roll. The reading now measures itself against the frames the
catalogue already holds of the folder it was pointed at: the roll's own size
while the walk is still up in its first folders, and the frames under a
subfolder alone when it was kept to one. And a row says at least what the
catalogue holds of it, never less — where the reading's number alone had the
head of the tree count 29980 while the line above it counted a hundred
thousand, one question with two answers. Nor is a scan of nothing announced as
`0/0` before its first pass has come back; the line waits until it knows what it
is reading.
The third thing is not a number but a weight. The tile kept for a frame is the
tile the grid cell and the strip both paint, and there is one of them per frame
of a roll that reaches six figures. At 512 on the long edge a tile weighed 42KB
and a hundred and sixty thousand of them weighed six and a half gigabytes of the
reader's disk — for a grid cell that is ~240px wide and a strip tile that is 132.
A quarter of the pixels at the quality Lightroom keeps its own grid previews at
puts the same picture in the same cell: the tile is now ~12KB and the roll a
quarter of the disk it was. The canvas a tile is drawn onto is told it is opaque,
so it no longer carries a channel of noughts through every draw and every encode.
The stage loses a little softness at 2.4x, which is the trade and is known.
A roll is dated folders inside dated folders, and the reader who wants April
re-read is not a reader who asked for the four years around it. Right-clicking a
subfolder row carried the roll's own menu, and RESCAN on it read the whole tree
from the top — every folder of every layer opened again, every frame in them
asked for its size and its time, to reach the one folder the pointer was on. On
a library of a hundred thousand that is a walk of minutes for a folder of
twenty.
The scan now takes the folder it was pointed at, spelled as the walk spells a
path: '' for the picked folder, `2026/04/` for a subfolder. The walk is handed
that path as its queue and reads down from there, so the tree is entered three
levels in rather than at its root, and the menu row passes the path of the row
under the pointer. A right click on the head of the tree still reads the whole
roll, which is what a right click on the picked folder has always meant.
What a reading kept to one folder must not do is speak for the roll. What it
names in the column is a branch of the tree, so the column keeps what it has and
the rows outside the folder being re-read stand where they are. What it counts
is a branch too, and its numbers drawn over the rows would count a roll down to
one folder of itself, so the column falls back on the catalogue until the
reading is through — the frame the reader is waiting for is the frame that was
re-read, and it is in the column the moment it lands.
Nor does it write a position down. `walk/ROLL.json` belongs to the reading that
walks the whole roll: a fraction of the tree written into it hands the next
visit a roll with the rest of itself missing from the walk, and it clears a file
another reading may be in the middle of. One folder is short enough to read
again, and the frames it does not re-read are skipped on their size and their
time anyway. A jump is refused for the same reason — the reading answers to the
folder it was kept to, and a click elsewhere is a different reading's business.
RESTORE was dead on a machine that had never written a backup: the button was
`disabled` while no folder was remembered, and nothing in the handler could ever
pick one, so the reader who had carried a copy over on a stick had no way to
point at it. The button now stands, and the folder it restores from is whatever
the reader picks in the dialog that opens — which is the whole of the choice
there is, because a backup is a folder of rows and tiles. The folder on the other
disk, the one made before the edit: the folder is the version.
BACK UP asked for a folder only the first time one was ever chosen. After that
it wrote into it in silence, and after a restart — when the browser takes the
permission back, since a permission outlives the tab only while the tab does —
it wrote nothing and said so, which is a backup that quietly stops happening.
It now picks whenever it cannot write, and the picker is the only thing that can
hand a remembered folder its permission back; a permission asked for with no
picker behind it is one the browser may refuse. The run that follows a scan is
untouched: it is guarded by the permission it would be asking for.
And the folder is a control, not a caption. The line that names it — with the
frames in it and when they were written — is what tells one backup from another,
and pressing it is how a folder is picked, a name given, a permission handed
back. It keeps the hint's own look through five lines of CSS, so the row still
reads as a caption and not as a third button.
What the line says is now read out of the folder rather than out of this
browser's memory of it. `pickBackupFolder` reads the catalogue file inside the
folder just picked and takes its count and its date as the row's own; a folder
with no catalogue of its own has none, and the row says so instead of showing
the numbers of the folder next door. That is also what the confirm names before
a restore — the frames and the time in the folder just picked, which is the
thing about to be restored. `restoreNow` already read that file; this reads its
header first.
Checked on the running bundle with the picker stubbed, since the dialog is one a
probe cannot press: with nothing remembered, RESTORE now opens the picker where
it did nothing at all, the row names the folder the picker returned, and a
folder without a catalogue of its own is refused with a line saying so rather
than restored as an empty catalogue. BACK UP opens it too, from the same fresh
state. The line computes to a button with no border, no background, the page's
own font and the hint's own 12px dim grey — a caption that happens to be
pressable. The wall, the strip, the grid, the deep link and the phone's recipes
all pass their checks.
LIBRARY drew the strip from the catalogue and the catalogue from a `getAll` of
the `photos` store: every row and every thumbnail in it, read back on a clock to
learn what a scan of its own had just stored. On a catalogue of 150,000 frames
one read measured 2036ms and sixty megabytes of rows; under a scan in the same
window, with four lanes of RAW bytes and a LibRaw of a quarter of a gigabyte
beside it, that read is the read that gives way — and a read that gave way
answered `[]`, so the screen threw away the frames it was showing. That is the
strip that loses its count and its thumbnails under a scan, and the frames the
reader cannot pick while the roll is being read.
The reading now hands its frames over as it stores them. `scanFolder` keeps the
rows it met for the first time in the batch they landed in, and `flush` gives
them to `watchRows` the moment the transaction closes; the screen appends them,
so the strip is the roll arriving and not the roll found again. A batch is fifty
frames, so this is fifty rows over a callback where it used to be a hundred and
fifty thousand rows over IndexedDB. A frame the catalogue already held is not
handed over — it is on the strip already, and it takes its place again when the
reading is through.
Which is why the read on the clock is now done for none of them. It stands for
the reading another window holds, whose frames never come through this one: a
reading of this window's hands over every frame it stores, and the counter that
says so is what decides. The scan's own tail read went with it — the screen
watching a reading reads the catalogue back once the reading ends, and that is
the whole catalogue read once per scan instead of twice at the end of every one.
`readPhotos` answers null where `listPhotos` answered an empty list. A read that
came back with nothing is a read that failed, not a catalogue that emptied, and
the frames it would have cleared are the frames the reader is working their way
through. The strip keeps them.
And the strip starts on what the last screen read. STUDIO and LIBRARY are two
screens of one page, not two pages: the reader who goes to develop a frame and
comes back was reading a hundred thousand rows again to see the strip they had
just left.
The folder is walked on the way in only when the last reading of it never
finished. A reading that reaches its end clears its position, so "is there a
position" is "was this roll cut off", answered by one small file opened and
shut. Before this, every visit to the library paid a walk of the folder the
reader was opening — a hundred thousand names off the disk, on the folder they
had just asked to browse — and the reader who came to choose a frame paid for a
scan they never asked for. A folder the reader wants looked at again says so
itself, from the menu on its row.
Checked on the running bundle: a 3000-frame scan reads the catalogue back twice,
one of the two the screen coming up on a catalogue that is still empty — against
three for the commit before this one and thirteen for the one before that, and
nothing read back for the whole of the scan. The peak heap is 116-138MB with no
long tasks, and 3001 rows went in. A reading stopped at 59 of 4000 still leaves
`walk/ROLL.json` at 113,626 characters with no local storage key beside it, and
the visit after a reload carries on with the strip filling under it — 162 rows,
312, 463, 612, 762, 913, 1062, 1212 as the scan went on. LIBRARY on 150,000
frames shows no "No folder yet" at any point and settles on "150000 photos" in
4.9s against the 8.1s it took. The wall, the strip, the grid, the deep link and
the phone's recipes all pass their checks.
LIBRARY read the whole catalogue back on every batch the scan wrote: `reload()`
is a `getAll` of the `photos` store, every row and every thumbnail in it, and on
a catalogue of 150,000 frames one read was measured at 2036ms, twenty of them at
315s with the worst at 58s, the heap behind them going from 44MB to 89MB — beside
a LibRaw open of a quarter of a gigabyte and four lanes of RAW bytes. On a folder
of RAW that is the crash, and on any long roll it is the strip taking the roll's
own time to move.
The read-back now runs on a clock: at most once every five seconds of a scan, and
once more when the reading ends, which is when the last of it is due. A batch
lands every second or so, and reading it back costs every frame in the catalogue
— so a scan that read it back each time spent the roll doing nothing else. The
strip still fills while the scan runs; it fills in steps.
The walk's own position went to the origin private file system. It is one path
per frame the walk has found and not read — about twenty-eight characters each,
measured over a folder of three thousand, which is the 85,626-character write —
and local storage on this origin takes 5,000,000 characters and then throws,
measured the same way. That is a hundred and seventy-five thousand frames: past
it the first write of a long roll fails, the position is nowhere, and the next
visit walks the whole roll from the top. That is the reading that goes back to
the start, and OPFS takes the same text as a file. It goes down at most once
every two seconds, for the same reason the catalogue is read back on a clock:
the string is a few megabytes of paths.
The folder is walked again for being raised no more than once every thirty
seconds. A hundred thousand frames is a hundred thousand stats on the disk, and
this screen is raised every time the reader comes back from the studio.
And the column no longer says there is no folder before it has looked. The note
waited on `folders` alone, which is empty for as long as the screen is coming up:
on the catalogue of 150,000 it said so for the whole of the load and showed the
folder after. It now waits on `loaded`, and says nothing until there is something
to say.
Checked on the running bundle: a 3000-frame scan reads the catalogue back 13
times where the code read it back once per batch, writes the position 21 times,
and leaves the peak heap at 116MB with no long tasks. A reading stopped at 483 of
4000 leaves `walk/ROLL.json` at 101,107 characters naming 3,550 unread frames,
with no local storage key beside it, and the visit after a reload carries on at
585 rather than at the top. LIBRARY on 150,000 frames shows no "No folder yet" at
any point and settles on "150000 photos". The wall, the strip, the grid, the deep
link and the phone's recipes all pass their checks.
The strip stopped building every tile two commits ago, but the wall was left holding
every frame on purpose: the grid view was the one list that still made a card per
frame. A card is an `<article>`, an `<img>`, an object URL and a date the browser
formats, and on a roll of twenty thousand the first one did not paint for 7410ms —
19998 cards and 19998 pictures, 7007ms of long tasks, the largest of them 4882ms. The
object URL is only about a tenth of it (83µs each); the rest is the DOM and the i18n
date formatting behind every card.
The wall now draws the rows under its viewport and `WALL_ROWS` either side, measured
from `scrollTop` and the client height on scroll and on resize, and two spacers stand
in for the rows that are not drawn, each as tall as the rows it replaces so the
scrollbar still spans the whole shelf. The step from one row to the next is the
average of the rows in hand rather than the smallest of them: the cards do not all
stand the same height — a caption that wraps makes its row taller (274.34 / 259.36 /
274.36 measured) — and `offsetTop` is rounded to whole pixels, so the least step was
a pixel short on every one of thousands of rows and the scrollbar came up 1436px shy
of the end. The average puts `scrollHeight` back on the number the fully drawn wall
had, to the pixel (1085426).
Measured against a seeded roll of 20000: the first card 7410ms → 212ms, cards drawn
19998 → 25, long tasks 7007ms → none, and the scrollbar unchanged. A check on a roll
of 4000 walks the wall end to end — the last frame drawn at the bottom, the first
drawn again at the top, spacer 215719px either side, `scrollHeight` the same 217076
at both ends, no drift — and coming back to the strip still leaves eighteen tiles.
A card that has a thumbnail and has not been handed its URL yet now keeps its box in
silence instead of saying RAW, since the wall hands pictures out only around the eye;
the word is left for a frame that has no thumbnail to give.
Coming back to LIBRARY froze the screen for a second and a half. It was not the tree
— the tree has been up in under a hundred milliseconds all along. It was the strip:
it made a `<button>`, an `<img>` and an object URL for every frame the open node
held, and an object URL costs about a tenth of a millisecond, which is a second of
blocked main thread on a roll of twenty thousand. Six and a half thousand tiles were
built for pictures nobody had scrolled to.
The strip now draws the run under its viewport and `STRIP_KEEP` tiles either side of
it, measured from `scrollLeft` and the client width on scroll and on resize. Two
spacers stand in for the runs that are not drawn, each as wide as the tiles it
replaces, so the scrollbar still measures the shelf rather than the window: the sum
is the same `140n − 8` in every case. The frame on the stage is given its URL
wherever it sits, since the stage is not the strip.
Measured against a seeded roll of 19998 frames: the first tile 1816ms → 663ms on a
cold visit and 1728ms → 681ms coming back from the studio; the long tasks 1302ms
across five of them → 184ms across one. Tiles drawn 6666 → 18, with the scrollbar
unmoved (the far end draws the far frames, and the frame on the stage keeps its
picture once its tile is out of the window).
The wall is the other view and still holds every frame on purpose; it is the one
list left that builds a card per frame.