@quario/landing 0.1.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +65 -9
- package/README.md +34 -20
- package/lib/index.d.ts +26 -9
- package/lib/landing.js +2 -2
- package/lib/sheet.js +387 -144
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,15 +1,73 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @quario/landing
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
## 0.2.1
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
5
|
+
### Patch Changes
|
|
7
6
|
|
|
8
|
-
|
|
7
|
+
- Updated dependencies
|
|
8
|
+
- @quario/layout@0.6.0
|
|
9
9
|
|
|
10
|
-
##
|
|
10
|
+
## 0.2.0
|
|
11
11
|
|
|
12
|
-
###
|
|
12
|
+
### Minor Changes
|
|
13
|
+
|
|
14
|
+
- **The sheet carries the extent; only the reach carries elements.** `sheet()`
|
|
15
|
+
gave every page of a display list a sized canvas, so a scale change
|
|
16
|
+
invalidated one box per page and the browser laid all of them out again — a
|
|
17
|
+
cost linear in the length of the document, around 10 microseconds a page, so
|
|
18
|
+
a five-thousand-page report paid roughly 50 ms on every zoom step. The sheet
|
|
19
|
+
element is now given its own height and width inline, from the paper the
|
|
20
|
+
list carries, and paints the page silhouettes as a repeating gradient at the
|
|
21
|
+
page pitch; the pages in the reach are positioned absolutely at the offset
|
|
22
|
+
`topOf()` reports, and a page leaving the reach is removed rather than
|
|
23
|
+
emptied. Sizing is now O(reach).
|
|
24
|
+
|
|
25
|
+
The extent a reader scrolls through is unchanged, and is still whole and
|
|
26
|
+
synchronous — more so, since it no longer waits on the browser laying out one
|
|
27
|
+
box per page to discover it.
|
|
28
|
+
|
|
29
|
+
- **`pageAt(i)` answers `null` for a page outside the reach**, which has no
|
|
30
|
+
element and is not given one by being asked for. `count()` still answers the
|
|
31
|
+
document's length rather than the sheet's child count.
|
|
32
|
+
- **`sheet()` places its own pages.** It writes `position: relative` on the
|
|
33
|
+
sheet element and `position: absolute` on every page canvas, alongside the
|
|
34
|
+
offsets and sizes it was already writing. A caller's own stylesheet no longer
|
|
35
|
+
stacks the pages, and must not: everything that places a page is decided in
|
|
36
|
+
one place now, and what a surface's CSS keeps is the look of one.
|
|
37
|
+
- **`sheet().at(clientX, clientY)`** — which page a client point is over, and
|
|
38
|
+
where it lands in that page's own points, or `null` for a point on no page:
|
|
39
|
+
the gap between two pages, above the first, below the last, or left or right
|
|
40
|
+
of the sheet. The inverse of `topOf()`, and answered by the same pitch the
|
|
41
|
+
sheet places its pages at — one division, and one read of the sheet's own
|
|
42
|
+
box rather than one per page. It answers for any page of the
|
|
43
|
+
list, with an element or without one — the reach decides what is painted,
|
|
44
|
+
not what is there — so a surface asking it during a fast scroll gets the
|
|
45
|
+
page the reader is looking at rather than nothing until the reach catches
|
|
46
|
+
up.
|
|
47
|
+
|
|
48
|
+
### Patch Changes
|
|
49
|
+
|
|
50
|
+
- **Finding the reach no longer walks the document.** Every page of a report is
|
|
51
|
+
drawn on the same paper, so the pages down a sheet sit one pitch apart and the
|
|
52
|
+
two ends of the reach are a division rather than a pass over every page. A
|
|
53
|
+
scroll frame spent 24 microseconds choosing the reach at five thousand pages
|
|
54
|
+
and now spends one, whatever the document's length. `topOf(i)` answers where
|
|
55
|
+
page `i` sits, computed from that pitch.
|
|
56
|
+
- **Every published README says where the documentation is.** Each package now
|
|
57
|
+
carries a Documentation section pointing at the reference, at the report schema
|
|
58
|
+
that normatively specifies what a report may declare, and at the package's own
|
|
59
|
+
API. The paragraphs that used to end on an unstated contract — the event
|
|
60
|
+
stream's field semantics, the style vocabulary, page columns, the Content
|
|
61
|
+
Security Policy a fragment with images needs, the formula mangling, and each
|
|
62
|
+
target's own contract — link the page that states it. Every link is an absolute
|
|
63
|
+
URL, so it resolves from the npm package page as readily as from an installed
|
|
64
|
+
copy.
|
|
65
|
+
- Updated dependencies
|
|
66
|
+
- @quario/layout@0.5.0
|
|
67
|
+
|
|
68
|
+
## 0.1.0
|
|
69
|
+
|
|
70
|
+
### Minor Changes
|
|
13
71
|
|
|
14
72
|
- **First release.** `options()` is the option check two surface elements run
|
|
15
73
|
per update cycle — first-failure-wins, re-validate-everything, and a commit
|
|
@@ -19,12 +77,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
19
77
|
counter that asks for a fresh one. `failures()` is which failure a panel is
|
|
20
78
|
showing and whether the reader put it away, with the announce policy taken
|
|
21
79
|
as an argument rather than settled.
|
|
22
|
-
|
|
23
80
|
- **`failures().reopen()`** — the dismissal lapses without announcing
|
|
24
81
|
anything. A dismissal is about a subject, so when that changes underneath
|
|
25
82
|
it, a standing failure is worth drawing again. Distinct from `clear()`,
|
|
26
83
|
which is a render landing and takes the failure with it.
|
|
27
|
-
|
|
28
84
|
- **`sheet()`** — the paged sheet both surfaces paint on: one canvas per page
|
|
29
85
|
of a display list, and the reach among them carrying pixels. Every page
|
|
30
86
|
keeps its CSS size, so the extent a reader scrolls through is whole whatever
|
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@ render boundary across their lifetime, and both draw a panel when something fail
|
|
|
6
6
|
that which are easy to get subtly wrong — and invisible when they are — live here once.
|
|
7
7
|
|
|
8
8
|
You almost certainly do not install this package. The two surfaces depend on it and run it for
|
|
9
|
-
you
|
|
9
|
+
you. It holds no report vocabulary and renders nothing.
|
|
10
10
|
|
|
11
11
|
## Install
|
|
12
12
|
|
|
@@ -15,8 +15,8 @@ npm install @quario/landing
|
|
|
15
15
|
```
|
|
16
16
|
|
|
17
17
|
One runtime dependency, `@quario/layout`, which the paged sheet paints through. It implements
|
|
18
|
-
Lit's `ReactiveController` protocol, which is structural, so
|
|
19
|
-
time and
|
|
18
|
+
Lit's `ReactiveController` protocol, which is structural, so this package imports nothing from
|
|
19
|
+
`lit` at run time and adds none of it to your tree.
|
|
20
20
|
|
|
21
21
|
ESM-only, Node 22+, and browser-ready through a standards-based bundler. CSP-safe: no
|
|
22
22
|
string-to-code paths anywhere.
|
|
@@ -24,25 +24,39 @@ string-to-code paths anywhere.
|
|
|
24
24
|
## What is here
|
|
25
25
|
|
|
26
26
|
**`options()`** — the option check across one update cycle. A host writes several properties at
|
|
27
|
-
once and gets one render, so the checks have to agree that
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
27
|
+
once and gets one render, so the checks have to agree that:
|
|
28
|
+
|
|
29
|
+
- one bad property does not block a good write to another,
|
|
30
|
+
- a property still broken keeps the element failing, rather than an unrelated write clearing it,
|
|
31
|
+
- writing one property does not clobber what another committed.
|
|
32
|
+
|
|
33
|
+
The checks themselves are each surface's own. Only that agreement is here.
|
|
34
|
+
|
|
35
|
+
**`Landing`** — the render boundary: whether a render has ever painted the sheet, whether the
|
|
36
|
+
element abandoned work mid-flight, and the counter that asks for a fresh one. A disconnected
|
|
37
|
+
element abandons work rather than hiding it, and comes back owing a render.
|
|
38
|
+
|
|
39
|
+
**`sheet()`** — the paged sheet both surfaces paint on: the extent of a display list, and the
|
|
40
|
+
reach within it carrying elements and pixels. The sheet element takes its own height and width
|
|
41
|
+
from the list's paper and paints the page silhouettes itself, so the extent a reader scrolls
|
|
42
|
+
through is whole whatever is standing. Only the pages in the reach have a canvas, positioned
|
|
43
|
+
absolutely at the offset `topOf()` reports. A list has one paper, so the pages are a pitch apart:
|
|
44
|
+
a position is a multiple of it and the reach's two ends are a division by it. `sheet()` takes a
|
|
45
|
+
`scale()` thunk and the two elements, so scale policy stays with each surface, and so does the
|
|
46
|
+
look of a page — the paper, the shadow, the margins. Everything that places a page is `sheet()`'s
|
|
47
|
+
and needs no stylesheet from the caller.
|
|
48
|
+
|
|
49
|
+
**`failures()`** — which failure a panel carries and whether the reader dismissed it. Whether
|
|
50
|
+
a repeat announces again is a policy the caller passes in, never a rule settled here. It is
|
|
43
51
|
downstream of what re-runs the render, which is the one thing the two surfaces do not share.
|
|
44
52
|
|
|
45
53
|
## What is not here
|
|
46
54
|
|
|
47
55
|
Each surface's own property checks, its panel markup, and its public API. The two elements differ
|
|
48
|
-
in their vocabulary and in their policy
|
|
56
|
+
in their vocabulary and in their policy. Only the machinery underneath is one thing.
|
|
57
|
+
|
|
58
|
+
## Documentation
|
|
59
|
+
|
|
60
|
+
[The quario documentation](https://getquario.com/docs/) is the reference.
|
|
61
|
+
The [report schema](https://getquario.com/docs/reference/report-schema/) is the normative
|
|
62
|
+
specification of what a report may declare.
|
package/lib/index.d.ts
CHANGED
|
@@ -82,14 +82,19 @@ export class Landing implements ReactiveController {
|
|
|
82
82
|
|
|
83
83
|
/** The gutter around the sheet, and the gap between pages, in CSS pixels at
|
|
84
84
|
* any scale: both are chrome, so neither scales with the page. A surface reads
|
|
85
|
-
* them rather than restating them — the reach
|
|
85
|
+
* them rather than restating them — the reach is measured in them, and a
|
|
86
86
|
* sheet whose CSS disagreed would paint the wrong pages. */
|
|
87
87
|
export const GUTTER: number;
|
|
88
88
|
export const GAP: number;
|
|
89
89
|
|
|
90
90
|
/**
|
|
91
|
-
* A paged sheet on `scroll`, painting into `sheetEl`:
|
|
92
|
-
*
|
|
91
|
+
* A paged sheet on `scroll`, painting into `sheetEl`: the extent of a display
|
|
92
|
+
* list, and the reach within it carrying elements and pixels.
|
|
93
|
+
*
|
|
94
|
+
* `sheetEl` is given the extent inline — `position: relative`, its height, its
|
|
95
|
+
* width and the page silhouettes — and each canvas of the reach its own
|
|
96
|
+
* `position: absolute` and offset within it, so a caller owes it no stylesheet
|
|
97
|
+
* (ADR 0065). What a surface's CSS keeps is the look of a page.
|
|
93
98
|
*
|
|
94
99
|
* `scale` is asked afresh every time — CSS pixels per point — so scale policy
|
|
95
100
|
* stays with the surface, and the elements are the surface's too, because
|
|
@@ -101,19 +106,31 @@ export function sheet(
|
|
|
101
106
|
scale: () => number,
|
|
102
107
|
pageClass?: string,
|
|
103
108
|
): {
|
|
104
|
-
/**
|
|
105
|
-
* page the browser will not draw is blank, not a failure. */
|
|
109
|
+
/** The extent whole at once, with the reach's pages painted. Never rejects:
|
|
110
|
+
* a page the browser will not draw is blank, not a failure. */
|
|
106
111
|
swap(list: unknown, fonts: unknown): Promise<void>;
|
|
107
|
-
/** Give
|
|
112
|
+
/** Give the sheet its extent and the reach its sizes at the current scale. */
|
|
108
113
|
sizeAll(): void;
|
|
109
114
|
/** Repaint the reach, releasing what it has left behind. */
|
|
110
115
|
repaint(): Promise<void>;
|
|
111
116
|
/** One reach pass a frame, however many scrolls or resizes ask. */
|
|
112
117
|
follow(): void;
|
|
113
|
-
/** Where
|
|
114
|
-
|
|
118
|
+
/** Where page `i` starts down the sheet, in CSS pixels — the very offset the
|
|
119
|
+
* sheet places that page at, so an overlay can agree with it. `0` before a
|
|
120
|
+
* list arrives. */
|
|
121
|
+
topOf(i: number): number;
|
|
115
122
|
/** The page width the list carries, or `null` before one arrives. */
|
|
116
123
|
width(): number | null;
|
|
117
|
-
|
|
124
|
+
/** The canvas standing for page `i`, or `null` for a page outside the reach
|
|
125
|
+
* — which has no element, and is not minted one by being asked for. */
|
|
126
|
+
pageAt(i: number): HTMLCanvasElement | null;
|
|
127
|
+
/** Which page a client point is over, and where it lands in that page's own
|
|
128
|
+
* points — the inverse of `topOf()`. Answers for any page of the list, with
|
|
129
|
+
* an element or without one, since the list carries a page's geometry
|
|
130
|
+
* whether or not the reach has minted a canvas for it. `null` for a point on
|
|
131
|
+
* no page: the gap between two, above the first, below the last, or left or
|
|
132
|
+
* right of the sheet. */
|
|
133
|
+
at(clientX: number, clientY: number): { index: number; x: number; y: number } | null;
|
|
134
|
+
/** The document's length, which is not the sheet's child count. */
|
|
118
135
|
count(): number;
|
|
119
136
|
};
|
package/lib/landing.js
CHANGED
|
@@ -63,7 +63,7 @@ export class Landing {
|
|
|
63
63
|
*
|
|
64
64
|
* Called by Lit through the controller protocol, never by name here, which
|
|
65
65
|
* is why the dead-code scan cannot see the caller. */
|
|
66
|
-
// fallow-ignore-next-line unused-class-member
|
|
66
|
+
// fallow-ignore-next-line unused-class-member -- Lit calls it through the controller protocol, so no caller is in the graph
|
|
67
67
|
hostConnected() {
|
|
68
68
|
this.wake();
|
|
69
69
|
}
|
|
@@ -72,7 +72,7 @@ export class Landing {
|
|
|
72
72
|
* only the work in flight stops. Each surface's own teardown — an observer,
|
|
73
73
|
* a key binding — stays in its `disconnectedCallback`. Called by Lit, as
|
|
74
74
|
* `hostConnected` is. */
|
|
75
|
-
// fallow-ignore-next-line unused-class-member
|
|
75
|
+
// fallow-ignore-next-line unused-class-member -- Lit calls it through the controller protocol, so no caller is in the graph
|
|
76
76
|
hostDisconnected() {
|
|
77
77
|
this.#task.abort();
|
|
78
78
|
}
|
package/lib/sheet.js
CHANGED
|
@@ -1,12 +1,24 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The paged sheet both surfaces paint on:
|
|
3
|
-
*
|
|
2
|
+
* The paged sheet both surfaces paint on: the extent of a display list, and
|
|
3
|
+
* the [reach](../../../CONTEXT.md#reach) within it carrying elements and
|
|
4
|
+
* pixels.
|
|
4
5
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
6
|
+
* The sheet carries its own extent — its height and width are written here
|
|
7
|
+
* from the list's [paper](../../../CONTEXT.md#paper), and a page canvas is
|
|
8
|
+
* positioned absolutely at the offset `topOf()` reports. Only the pages in the
|
|
9
|
+
* reach have an element at all; a page leaving it is removed, which takes its
|
|
10
|
+
* backing store with it. So the extent a reader scrolls through is whole and
|
|
11
|
+
* synchronous whatever is painted, without the browser having to lay out one
|
|
12
|
+
* box per page to discover it (ADR 0065). ADR 0043 says what the reach is
|
|
13
|
+
* worth: painting the whole sheet asked a thousand-page report for gigabytes,
|
|
14
|
+
* and past what the browser would grant the pages came up blank.
|
|
15
|
+
*
|
|
16
|
+
* **The page silhouettes are a paint, not elements.** A page outside the reach
|
|
17
|
+
* has nothing to carry white, so the sheet's own background is a repeating
|
|
18
|
+
* gradient at the page pitch — exact rather than approximate, because a list
|
|
19
|
+
* has one paper and the pitch is the same one the pages are placed at. One
|
|
20
|
+
* style write per scale change, and it rasters even with the main thread
|
|
21
|
+
* wedged.
|
|
10
22
|
*
|
|
11
23
|
* **A page the browser will not paint is blank, not a failure.** `start`
|
|
12
24
|
* swallows and nothing here rejects: the report laid out and the list reached
|
|
@@ -16,7 +28,8 @@
|
|
|
16
28
|
* it draws, so a canvas whose paint is still in flight is *retired* rather
|
|
17
29
|
* than painted over: it comes off the sheet, its pixels go back, and a fresh
|
|
18
30
|
* one stands in its place, so the superseded paint draws into an element
|
|
19
|
-
* nobody is looking at (ADR 0046).
|
|
31
|
+
* nobody is looking at (ADR 0046). A page that leaves the reach mid-paint is
|
|
32
|
+
* dropped for the same reason, with nothing standing in.
|
|
20
33
|
*
|
|
21
34
|
* What this does **not** own is scale policy. It takes a `scale()` thunk and
|
|
22
35
|
* asks it afresh every time: the viewer answers a zoom percentage, the editor
|
|
@@ -28,16 +41,27 @@ import { paint } from "@quario/layout";
|
|
|
28
41
|
|
|
29
42
|
/** The gutter around the sheet, and the gap between pages, in CSS pixels at
|
|
30
43
|
* any scale: both are chrome, so neither scales with the page. Here rather
|
|
31
|
-
* than in each surface because the reach
|
|
32
|
-
* geometry disagreed with its own reach would paint the wrong pages. */
|
|
44
|
+
* than in each surface because the reach is measured in them, and a sheet
|
|
45
|
+
* whose geometry disagreed with its own reach would paint the wrong pages. */
|
|
33
46
|
export const GUTTER = 28;
|
|
34
47
|
export const GAP = 16;
|
|
35
48
|
|
|
49
|
+
/** The paper a page is drawn on, and what the sheet paints where a page has
|
|
50
|
+
* no element. Both surfaces' page rules carry the same white; this is the
|
|
51
|
+
* copy that shows through a canvas with no backing store, and the one a page
|
|
52
|
+
* outside the reach is made of. */
|
|
53
|
+
const PAPER = "#fff";
|
|
54
|
+
|
|
36
55
|
/**
|
|
37
56
|
* A paged sheet on `scroll`, painting into `sheetEl`.
|
|
38
57
|
*
|
|
39
58
|
* @param {HTMLElement} scroll The scroll container the reader looks through.
|
|
40
|
-
* @param {HTMLElement} sheetEl What the page canvases are children of.
|
|
59
|
+
* @param {HTMLElement} sheetEl What the page canvases are children of. Made a
|
|
60
|
+
* containing block here, as its pages are made absolute here: a page's
|
|
61
|
+
* `top` is meaningless without those two, so they are one decision with it
|
|
62
|
+
* rather than an obligation on a caller who would have to be told, and
|
|
63
|
+
* pinned, and could still forget. What a surface's stylesheet keeps is the
|
|
64
|
+
* look — the paper, the shadow, the margins.
|
|
41
65
|
* @param {() => number} scale CSS pixels per point, asked afresh each time.
|
|
42
66
|
* @param {string} [pageClass] What to name a page element. Each surface's
|
|
43
67
|
* `q*-` names are its own documented CSS surface, so the class is the
|
|
@@ -48,12 +72,16 @@ export let sheet = (scroll, sheetEl, scale, pageClass = "qv-page") => {
|
|
|
48
72
|
let list = null;
|
|
49
73
|
/** @type {any} */
|
|
50
74
|
let fonts;
|
|
51
|
-
/**
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
|
|
56
|
-
|
|
75
|
+
/**
|
|
76
|
+
* The page canvases the reach has, by their index in the list. Sparse, and
|
|
77
|
+
* its own map rather than `sheetEl.children`: a sheet may hold more than
|
|
78
|
+
* pages — the editor hangs its box overlay in the same element — and
|
|
79
|
+
* indexing the children would then read an overlay as a page, and could not
|
|
80
|
+
* say which page a canvas stood for anyway.
|
|
81
|
+
*
|
|
82
|
+
* @type {Map<number, HTMLCanvasElement>}
|
|
83
|
+
*/
|
|
84
|
+
let pages = new Map();
|
|
57
85
|
|
|
58
86
|
/**
|
|
59
87
|
* What each backed page is carrying: the scale it was painted at, so a page
|
|
@@ -64,55 +92,143 @@ export let sheet = (scroll, sheetEl, scale, pageClass = "qv-page") => {
|
|
|
64
92
|
* and the pages it replaced take their entries with them — which is why the
|
|
65
93
|
* map is weak: nothing has to remember to forget them.
|
|
66
94
|
*
|
|
67
|
-
*
|
|
95
|
+
* `done` is the third: whether that paint has finished. A promise's settled
|
|
96
|
+
* state is not synchronously observable and it is the one question the
|
|
97
|
+
* retirement rule turns on — a canvas whose entry is `done` has nothing left
|
|
98
|
+
* that could draw into it and can be re-sized in place; one still painting
|
|
99
|
+
* must be retired instead. It rides the entry rather than a set beside it,
|
|
100
|
+
* so it is per *paint* for free: `start` mints a fresh entry, and a page
|
|
101
|
+
* settled at one scale cannot count as settled the moment it begins painting
|
|
102
|
+
* at the next.
|
|
103
|
+
*
|
|
104
|
+
* @typedef {{ scale: number, painted: Promise<void>, done: boolean }} Backing
|
|
105
|
+
* @type {WeakMap<HTMLCanvasElement, Backing>}
|
|
68
106
|
*/
|
|
69
107
|
let backed = new WeakMap();
|
|
70
108
|
|
|
71
109
|
/**
|
|
72
|
-
* The
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
* @type {WeakSet<HTMLCanvasElement>}
|
|
110
|
+
* The list stacked at one scale, in CSS pixels: the
|
|
111
|
+
* [paper](../../../CONTEXT.md#paper) every page is drawn on, the pitch from
|
|
112
|
+
* one page's top to the next's, and how tall the sheet has to be to hold them
|
|
113
|
+
* all. There is no width beside those: the sheet is one paper wide, which is
|
|
114
|
+
* the claim `extend` writes rather than a second number to keep in step.
|
|
115
|
+
*
|
|
116
|
+
* The **pitch** is the paper's height plus the gap — the step down the sheet,
|
|
117
|
+
* and the period of the silhouette gradient.
|
|
118
|
+
*
|
|
119
|
+
* @typedef {{ px: number, paper: { width: number, height: number },
|
|
120
|
+
* pitch: number, height: number }} Stacked
|
|
84
121
|
*/
|
|
85
|
-
let settled = new WeakSet();
|
|
86
122
|
|
|
87
123
|
/**
|
|
88
|
-
* The
|
|
89
|
-
*
|
|
90
|
-
*
|
|
124
|
+
* The stacking at the applied scale, computed once and held until the scale
|
|
125
|
+
* moves or a list arrives. **The one reading of the list's paper there is** —
|
|
126
|
+
* the reach divides by it, the pages are positioned from it, `topOf()`
|
|
127
|
+
* answers it and the extent is written from it, so none of those can drift
|
|
128
|
+
* from the others.
|
|
91
129
|
*
|
|
92
|
-
* @type {
|
|
130
|
+
* @type {Stacked | null}
|
|
131
|
+
*/
|
|
132
|
+
let stacking = null;
|
|
133
|
+
|
|
134
|
+
/** The stacking the sheet element's style already says, so an `extend` that
|
|
135
|
+
* would write what is there writes nothing.
|
|
136
|
+
* @type {Stacked | null} */
|
|
137
|
+
let extended = null;
|
|
138
|
+
|
|
139
|
+
/** The stacking itself, at `px` CSS pixels per point: every page a gap below
|
|
140
|
+
* the one before it, from the sheet's own top. Read off the list's own paper,
|
|
141
|
+
* which is one width and one height for every page of it (ADR 0069), so a
|
|
142
|
+
* page's offset is a multiple of the pitch rather than a sum walked up from
|
|
143
|
+
* the top — and the extent, the silhouettes and the placement all come off
|
|
144
|
+
* the same two numbers instead of agreeing by hand.
|
|
145
|
+
*
|
|
146
|
+
* @type {(px: number) => Stacked}
|
|
147
|
+
*/
|
|
148
|
+
let stack = (px) => {
|
|
149
|
+
let paper = { width: list.width * px, height: list.height * px };
|
|
150
|
+
let pitch = paper.height + GAP;
|
|
151
|
+
// The trailing gap belongs to no page: the extent ends at the last page's
|
|
152
|
+
// own bottom, which is where the silhouette gradient's last band ends too.
|
|
153
|
+
return { px, paper, pitch, height: Math.max(0, list.pages.length * pitch - GAP) };
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
/** @type {() => Stacked} */
|
|
157
|
+
let measure = () => {
|
|
158
|
+
let px = scale();
|
|
159
|
+
return stacking?.px === px ? stacking : (stacking = stack(px));
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* The page silhouettes, as one background over the sheet's own height. This
|
|
164
|
+
* paints what a page element used to, for the pages that no longer have one
|
|
165
|
+
* (ADR 0065).
|
|
166
|
+
*
|
|
167
|
+
* A band of paper and a gap of nothing, repeated at the pitch — which is the
|
|
168
|
+
* stacking's own pitch, not a second answer to it. This used to presume the
|
|
169
|
+
* pitch it painted, reading the first page's height as every page's because
|
|
170
|
+
* a gradient can only have one; the list states its paper now, so the
|
|
171
|
+
* gradient asks for what the pages are placed by (ADR 0069). No guard for a
|
|
172
|
+
* pageless list either: the band flow opens the body's first page whether or
|
|
173
|
+
* not anything renders on it, and a gradient over a sheet no pages tall
|
|
174
|
+
* paints nothing regardless.
|
|
175
|
+
*
|
|
176
|
+
* @type {(at: Stacked) => string}
|
|
177
|
+
*/
|
|
178
|
+
let silhouettes = (at) =>
|
|
179
|
+
`repeating-linear-gradient(${PAPER} 0 ${at.paper.height}px, ` +
|
|
180
|
+
`transparent ${at.paper.height}px ${at.pitch}px)`;
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Give the sheet the extent of the whole list, and the silhouettes of the
|
|
184
|
+
* pages it is not showing. Three style writes, whatever the page count: this
|
|
185
|
+
* is what makes the extent whole without an element per page, and callers
|
|
186
|
+
* run it before writing scroll offsets back, because the extent those are
|
|
187
|
+
* clamped against is this one.
|
|
93
188
|
*/
|
|
94
|
-
let
|
|
189
|
+
let extend = () => {
|
|
190
|
+
let at = measure();
|
|
191
|
+
// Reference, not value: `measure` hands back the very same object until
|
|
192
|
+
// the scale moves, so an unchanged stacking costs one compare rather than
|
|
193
|
+
// three style writes — and a background write invalidates the paint of an
|
|
194
|
+
// extent that can be hundreds of thousands of pixels tall. The editor's
|
|
195
|
+
// scale never moves, so every swap after the first takes this branch.
|
|
196
|
+
if (at === extended) return;
|
|
197
|
+
extended = at;
|
|
198
|
+
sheetEl.style.position = "relative";
|
|
199
|
+
// One paper wide, and as tall as the pages stacked: the sheet has no width
|
|
200
|
+
// of its own, so this is where that is said rather than a field carrying it.
|
|
201
|
+
sheetEl.style.width = at.paper.width + "px";
|
|
202
|
+
sheetEl.style.height = at.height + "px";
|
|
203
|
+
sheetEl.style.background = silhouettes(at);
|
|
204
|
+
};
|
|
95
205
|
|
|
96
206
|
/**
|
|
97
|
-
* Give a page's canvas its size on
|
|
98
|
-
* `i`
|
|
99
|
-
* `sizeAll` writes it over the
|
|
100
|
-
* retired between the two cannot arrive sizeless
|
|
101
|
-
* reader is scrolling through.
|
|
207
|
+
* Give a page's canvas its size and its place on the sheet. The one answer
|
|
208
|
+
* to where page `i` sits at the applied scale: `pageOf` builds a canvas with
|
|
209
|
+
* it and `sizeAll` writes it over the reach after a scale change, so a page
|
|
210
|
+
* retired between the two cannot arrive sizeless or off its own offset.
|
|
102
211
|
*
|
|
103
|
-
* @type {(canvas: HTMLElement, i: number
|
|
212
|
+
* @type {(canvas: HTMLElement, i: number) => void}
|
|
104
213
|
*/
|
|
105
|
-
let
|
|
106
|
-
let
|
|
107
|
-
canvas.style.
|
|
108
|
-
|
|
214
|
+
let place = (canvas, i) => {
|
|
215
|
+
let at = measure();
|
|
216
|
+
canvas.style.position = "absolute";
|
|
217
|
+
// `left` written rather than left to the static position, which would be
|
|
218
|
+
// the sheet's content edge and so would move with a padding neither
|
|
219
|
+
// surface sets today. `at()` below measures from the padding edge, so the
|
|
220
|
+
// two agree by construction instead of by that absence.
|
|
221
|
+
canvas.style.left = "0px";
|
|
222
|
+
canvas.style.top = i * at.pitch + "px";
|
|
223
|
+
canvas.style.width = at.paper.width + "px";
|
|
224
|
+
canvas.style.height = at.paper.height + "px";
|
|
109
225
|
};
|
|
110
226
|
|
|
111
227
|
/**
|
|
112
|
-
* One page of the list as a canvas: sized
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
* element in every respect but identity.
|
|
228
|
+
* One page of the list as a canvas: sized, placed, carrying no pixels yet,
|
|
229
|
+
* and named for a screen reader. Every canvas on the sheet is built here —
|
|
230
|
+
* a page entering the reach and a retirement alike — so a replacement is the
|
|
231
|
+
* same element in every respect but identity.
|
|
116
232
|
*
|
|
117
233
|
* @type {(i: number) => HTMLCanvasElement}
|
|
118
234
|
*/
|
|
@@ -125,17 +241,94 @@ export let sheet = (scroll, sheetEl, scale, pageClass = "qv-page") => {
|
|
|
125
241
|
canvas.height = 0;
|
|
126
242
|
canvas.setAttribute("role", "img");
|
|
127
243
|
canvas.setAttribute("aria-label", "Page " + (i + 1) + " of " + list.pages.length);
|
|
128
|
-
|
|
244
|
+
place(canvas, i);
|
|
129
245
|
return canvas;
|
|
130
246
|
};
|
|
131
247
|
|
|
248
|
+
/**
|
|
249
|
+
* The canvas standing for page `i` right now, or `null` for a page outside
|
|
250
|
+
* the reach — which has no element, and is not minted one by being asked
|
|
251
|
+
* for: that would make a read a mutation, and would have `keep()`
|
|
252
|
+
* materialise every page it releases. Read through here and never held: a
|
|
253
|
+
* retired or released page is gone, so a canvas taken before an await may be
|
|
254
|
+
* off the sheet by the time it is used.
|
|
255
|
+
*
|
|
256
|
+
* @type {(i: number) => HTMLCanvasElement | null}
|
|
257
|
+
*/
|
|
258
|
+
let pageAt = (i) => pages.get(i) ?? null;
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Which page a client point is over, and where it lands in that page's own
|
|
262
|
+
* points — or `null` for a point on no page at all.
|
|
263
|
+
*
|
|
264
|
+
* **The inverse of `topOf()`, and here because `topOf()` is here.** The
|
|
265
|
+
* stacking is one arithmetic read two ways: `topOf()` answers where a page
|
|
266
|
+
* starts, this answers which page a distance is inside. A caller computing
|
|
267
|
+
* it from `topOf()` would be a second place that knows how pages stack,
|
|
268
|
+
* which is the drift ADR 0065 bought out by making the offset a fact rather
|
|
269
|
+
* than a presumption.
|
|
270
|
+
*
|
|
271
|
+
* **One division, and one layout read.** The pages step by a fixed pitch, so
|
|
272
|
+
* the page a distance falls in is that distance over the pitch — the same
|
|
273
|
+
* arithmetic `inReach()` bounds the reach with, in the other direction, and
|
|
274
|
+
* flat in the length of the document. The sheet's own box is read afresh
|
|
275
|
+
* each call, because a point is a client coordinate and nothing else can
|
|
276
|
+
* turn it into a sheet one. The editor's hit test walked every page to find
|
|
277
|
+
* the handful with an element and read a rect off the one it landed in; this
|
|
278
|
+
* replaces that walk, not that read.
|
|
279
|
+
*
|
|
280
|
+
* **It answers for a page that has no element.** The reach decides what is
|
|
281
|
+
* painted, not what is there: a point over a page the reach has not minted
|
|
282
|
+
* still lands on that page, because the stacking is the list's geometry and
|
|
283
|
+
* not the elements'. So a fast scroll or a resize cannot make a point over a
|
|
284
|
+
* page the reader is looking at answer "nowhere" for the frame before the
|
|
285
|
+
* reach catches up.
|
|
286
|
+
*
|
|
287
|
+
* Null in four places, all of them genuinely no page: the gap between two
|
|
288
|
+
* pages, above the first, below the last, and left or right of the sheet.
|
|
289
|
+
*
|
|
290
|
+
* @type {(clientX: number, clientY: number) => { index: number, x: number, y: number } | null}
|
|
291
|
+
*/
|
|
292
|
+
let at = (clientX, clientY) => {
|
|
293
|
+
if (!list?.pages.length) return null;
|
|
294
|
+
let stacked = measure();
|
|
295
|
+
let box = sheetEl.getBoundingClientRect();
|
|
296
|
+
// The padding box, not the border box the rect describes: a page is placed
|
|
297
|
+
// `left: 0` inside this element and resolves against its padding edge, as
|
|
298
|
+
// does an overlay a surface hangs there with `inset: 0`. `clientLeft` and
|
|
299
|
+
// `clientTop` are that difference — the border — and are read off the very
|
|
300
|
+
// element whose rect was just read, so they cost no second flush.
|
|
301
|
+
let x = clientX - box.left - sheetEl.clientLeft;
|
|
302
|
+
let y = clientY - box.top - sheetEl.clientTop;
|
|
303
|
+
// The page whose pitch band `y` falls in. Negative above the first page,
|
|
304
|
+
// past the end below the last: each runs off its own end of the list.
|
|
305
|
+
let i = Math.floor(y / stacked.pitch);
|
|
306
|
+
return i < 0 || i >= list.pages.length ? null : onPage(i, x, y, stacked);
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
/** Page `i`'s own answer for a sheet-space point already known to be in its
|
|
310
|
+
* pitch band: the point in that page's points, or `null` past its paper —
|
|
311
|
+
* the gap under it, or the sheet's end — or beside it. The paper's own edges
|
|
312
|
+
* count as inside, which is what the rect test this replaced said with `<=`.
|
|
313
|
+
*
|
|
314
|
+
* Answers the point rather than a yes, so one page's origin is read in one
|
|
315
|
+
* place instead of being tested here and subtracted in the caller.
|
|
316
|
+
*
|
|
317
|
+
* @type {(i: number, x: number, y: number, stacked: Stacked) => { index: number, x: number, y: number } | null} */
|
|
318
|
+
let onPage = (i, x, y, stacked) => {
|
|
319
|
+
let top = i * stacked.pitch;
|
|
320
|
+
if (y > top + stacked.paper.height) return null;
|
|
321
|
+
if (x < 0 || x > stacked.paper.width) return null;
|
|
322
|
+
return { index: i, x: x / stacked.px, y: (y - top) / stacked.px };
|
|
323
|
+
};
|
|
324
|
+
|
|
132
325
|
/**
|
|
133
326
|
* Take page `i`'s canvas off the sheet and stand a fresh one in its place,
|
|
134
327
|
* answering with the replacement. A paint still in flight holds the old
|
|
135
328
|
* canvas's context and draws into something nobody is looking at, which is
|
|
136
329
|
* how a superseded paint is stopped here — by construction, rather than by
|
|
137
330
|
* a check the painter would have to make above its own draw (ADR 0046).
|
|
138
|
-
* The retired canvas takes its
|
|
331
|
+
* The retired canvas takes its entry in `backed` with it, being weak.
|
|
139
332
|
*
|
|
140
333
|
* Its pixels go back at once, by the same 0 × 0 idiom `release` uses: the
|
|
141
334
|
* store is what ADR 0043 rations, and leaving it to be collected whenever
|
|
@@ -147,33 +340,42 @@ export let sheet = (scroll, sheetEl, scale, pageClass = "qv-page") => {
|
|
|
147
340
|
* @type {(i: number) => HTMLCanvasElement}
|
|
148
341
|
*/
|
|
149
342
|
let retire = (i) => {
|
|
150
|
-
let old = pageAt(i);
|
|
343
|
+
let old = /** @type {HTMLCanvasElement} */ (pageAt(i));
|
|
151
344
|
let fresh = pageOf(i);
|
|
152
345
|
old.replaceWith(fresh);
|
|
153
|
-
pages
|
|
346
|
+
pages.set(i, fresh);
|
|
154
347
|
old.width = 0;
|
|
155
348
|
old.height = 0;
|
|
156
349
|
return fresh;
|
|
157
350
|
};
|
|
158
351
|
|
|
159
352
|
/**
|
|
160
|
-
*
|
|
161
|
-
*
|
|
162
|
-
*
|
|
163
|
-
*
|
|
164
|
-
*
|
|
165
|
-
*
|
|
166
|
-
*
|
|
353
|
+
* Let page `i` go. It leaves the sheet entirely — the element and its
|
|
354
|
+
* backing store together, since a detached canvas takes its store with it —
|
|
355
|
+
* and the extent does not move, because the extent is the sheet's own.
|
|
356
|
+
*
|
|
357
|
+
* Removal is what makes this safe whether the page's paint has settled or
|
|
358
|
+
* not: a paint still in flight is left drawing into a detached element, the
|
|
359
|
+
* same outcome retirement arranges, so there is nothing here to stand in
|
|
360
|
+
* its place for. Sized to 0 × 0 rather than left to be collected when that
|
|
361
|
+
* paint lets go of the context, which is the timing ADR 0043 refuses.
|
|
167
362
|
*
|
|
168
363
|
* @type {(i: number) => void}
|
|
169
364
|
*/
|
|
170
365
|
let release = (i) => {
|
|
171
366
|
let canvas = pageAt(i);
|
|
172
|
-
if (!
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
canvas
|
|
176
|
-
|
|
367
|
+
if (!canvas) return;
|
|
368
|
+
pages.delete(i);
|
|
369
|
+
canvas.remove();
|
|
370
|
+
// Only where there is a store to give back: assigning a canvas's width
|
|
371
|
+
// resets its bitmap whatever the value, so an untouched page would pay for
|
|
372
|
+
// a reset it has nothing to reset. `backed` is weak and the canvas is now
|
|
373
|
+
// unreachable, so its entry needs no forgetting — the same reason `retire`
|
|
374
|
+
// does not forget one either.
|
|
375
|
+
if (canvas.width) {
|
|
376
|
+
canvas.width = 0;
|
|
377
|
+
canvas.height = 0;
|
|
378
|
+
}
|
|
177
379
|
};
|
|
178
380
|
|
|
179
381
|
/** Device pixels per point at the applied scale: what a page is
|
|
@@ -187,7 +389,7 @@ export let sheet = (scroll, sheetEl, scale, pageClass = "qv-page") => {
|
|
|
187
389
|
* at it, so no caller can reach a live canvas with a second paint by
|
|
188
390
|
* forgetting to ask first (ADR 0046). A paint that failed is forgotten, so
|
|
189
391
|
* the next pass over the reach tries again instead of counting the page as
|
|
190
|
-
* painted; one that settles
|
|
392
|
+
* painted; one that settles marks its own entry `done`.
|
|
191
393
|
*
|
|
192
394
|
* **The paint this answers with never rejects**, which is the whole of why
|
|
193
395
|
* `swap` does not either. A page that could not be drawn is not a render
|
|
@@ -206,23 +408,33 @@ export let sheet = (scroll, sheetEl, scale, pageClass = "qv-page") => {
|
|
|
206
408
|
* @type {(i: number) => Promise<void>}
|
|
207
409
|
*/
|
|
208
410
|
let start = (i) => {
|
|
209
|
-
let canvas = pageAt(i);
|
|
210
|
-
|
|
211
|
-
|
|
411
|
+
let canvas = /** @type {HTMLCanvasElement} */ (pageAt(i));
|
|
412
|
+
let carrying = backed.get(canvas);
|
|
413
|
+
if (carrying && !carrying.done) canvas = retire(i);
|
|
212
414
|
let scale = deviceScale();
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
415
|
+
// The store is the paper in device pixels, rounded to a whole one. Every
|
|
416
|
+
// page of the list is the same size, so this is the list's paper and not
|
|
417
|
+
// the page's — a page carries no geometry to ask (ADR 0069).
|
|
418
|
+
canvas.width = Math.round(list.width * scale);
|
|
419
|
+
canvas.height = Math.round(list.height * scale);
|
|
216
420
|
let ctx = /** @type {CanvasRenderingContext2D} */ (canvas.getContext("2d"));
|
|
217
|
-
|
|
421
|
+
// The handlers mark the entry they belong to rather than the canvas: a
|
|
422
|
+
// second `start` on the same canvas replaces the entry, so the superseded
|
|
423
|
+
// paint marks one nobody reads instead of declaring the new paint settled.
|
|
424
|
+
// Assigned after the promise and read only from a handler, which cannot
|
|
425
|
+
// run before this turn ends.
|
|
426
|
+
/** @type {Backing} */
|
|
427
|
+
let entry;
|
|
428
|
+
let painted = paint(ctx, list.pages[i], { scale, fonts }).then(
|
|
218
429
|
() => {
|
|
219
|
-
|
|
430
|
+
entry.done = true;
|
|
220
431
|
},
|
|
221
432
|
() => {
|
|
222
433
|
backed.delete(canvas);
|
|
223
434
|
},
|
|
224
435
|
);
|
|
225
|
-
|
|
436
|
+
entry = { scale, painted, done: false };
|
|
437
|
+
backed.set(canvas, entry);
|
|
226
438
|
return painted;
|
|
227
439
|
};
|
|
228
440
|
|
|
@@ -235,12 +447,16 @@ export let sheet = (scroll, sheetEl, scale, pageClass = "qv-page") => {
|
|
|
235
447
|
*
|
|
236
448
|
* Takes the index, not the canvas: the page it paints may be retired out
|
|
237
449
|
* from under a caller, so a caller that handed one in would be left holding
|
|
238
|
-
* an element that is no longer on the sheet.
|
|
450
|
+
* an element that is no longer on the sheet. A page released between the
|
|
451
|
+
* pass that chose it and the paint that follows has nothing to paint onto,
|
|
452
|
+
* and is skipped.
|
|
239
453
|
*
|
|
240
454
|
* @type {(i: number) => Promise<void>}
|
|
241
455
|
*/
|
|
242
456
|
let paintPage = (i) => {
|
|
243
|
-
let
|
|
457
|
+
let canvas = pageAt(i);
|
|
458
|
+
if (!canvas) return Promise.resolve();
|
|
459
|
+
let carrying = backed.get(canvas);
|
|
244
460
|
if (carrying && carrying.scale === deviceScale()) return carrying.painted;
|
|
245
461
|
return start(i);
|
|
246
462
|
};
|
|
@@ -255,54 +471,65 @@ export let sheet = (scroll, sheetEl, scale, pageClass = "qv-page") => {
|
|
|
255
471
|
*
|
|
256
472
|
* It guards repaint against repaint, and nothing else. A repaint chooses
|
|
257
473
|
* its pages once and holds that list across its awaits, so a scroll pass
|
|
258
|
-
* releasing a page mid-repaint is one this loop
|
|
259
|
-
* store that leaves behind is bounded by the reach
|
|
260
|
-
* next pass over it.
|
|
474
|
+
* releasing a page mid-repaint is one this loop skips when it gets there;
|
|
475
|
+
* the store that leaves behind is bounded by the reach.
|
|
261
476
|
*/
|
|
262
477
|
let epoch = 0;
|
|
263
478
|
|
|
264
479
|
/**
|
|
265
|
-
* Give
|
|
266
|
-
*
|
|
267
|
-
*
|
|
480
|
+
* Give the sheet its extent and the reach its sizes, at the current scale.
|
|
481
|
+
* Callers run this before writing the scroll offsets back: the extent those
|
|
482
|
+
* offsets are clamped against is this one, and the reach below is read from
|
|
483
|
+
* the offsets once they are in. O(reach), not O(pages) — which is what ADR
|
|
484
|
+
* 0065 is for.
|
|
268
485
|
*/
|
|
269
486
|
let sizeAll = () => {
|
|
270
487
|
if (!list) return;
|
|
271
|
-
|
|
272
|
-
for (let i
|
|
488
|
+
extend();
|
|
489
|
+
for (let [i, canvas] of pages) place(canvas, i);
|
|
273
490
|
};
|
|
274
491
|
|
|
275
492
|
/**
|
|
276
493
|
* The indices of the pages in the reach — the ones the viewport shows, and
|
|
277
|
-
* everything within one viewport height above or below.
|
|
278
|
-
*
|
|
279
|
-
*
|
|
280
|
-
*
|
|
281
|
-
*
|
|
282
|
-
*
|
|
283
|
-
*
|
|
494
|
+
* everything within one viewport height above or below. Read off the pitch
|
|
495
|
+
* `measure` holds, never a layout read, so it costs nothing to ask
|
|
496
|
+
* mid-scroll. `clientHeight` is read afresh each time, so a pane that
|
|
497
|
+
* changed size changes the reach with it.
|
|
498
|
+
*
|
|
499
|
+
* **Two divisions rather than a search, because the pages are a pitch
|
|
500
|
+
* apart.** Page `i` starts at `i * pitch` and ends a paper below that, so
|
|
501
|
+
* both bounds of the band invert straight into an index — no walk over the
|
|
502
|
+
* pages, and no search among them either. This was a bisection first (ADR
|
|
503
|
+
* 0065's amendment); what made it arithmetic is the list stating its paper
|
|
504
|
+
* once, so the stacking is a pitch rather than an array of offsets to look
|
|
505
|
+
* through (ADR 0069).
|
|
284
506
|
*
|
|
285
507
|
* @returns {number[]}
|
|
286
508
|
*/
|
|
287
509
|
let inReach = () => {
|
|
288
510
|
let view = scroll.clientHeight;
|
|
289
|
-
let
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
511
|
+
let at = measure();
|
|
512
|
+
// Sheet-space: the sheet starts a gutter down what the reader scrolls, so
|
|
513
|
+
// the band is measured from the first page's own top rather than the scroll
|
|
514
|
+
// container's. Converted once, here.
|
|
515
|
+
let eye = scroll.scrollTop - GUTTER;
|
|
516
|
+
// The near bound is the first page whose foot the band has reached — its
|
|
517
|
+
// foot being a paper below its own top — and the far one the first page
|
|
518
|
+
// past the band's head. Each can only run off its own end of the list: a
|
|
519
|
+
// band above the sheet gives a negative near bound, one below it a far
|
|
520
|
+
// bound past the last page.
|
|
521
|
+
let first = Math.max(0, Math.ceil((eye - view - at.paper.height) / at.pitch));
|
|
522
|
+
let after = Math.min(list.pages.length, Math.floor((eye + 2 * view) / at.pitch) + 1);
|
|
293
523
|
let found = [];
|
|
294
|
-
for (let
|
|
295
|
-
let height = each.height * px;
|
|
296
|
-
if (y + height >= top && y <= bottom) found.push(i);
|
|
297
|
-
y += height + GAP;
|
|
298
|
-
}
|
|
524
|
+
for (let i = first; i < after; i++) found.push(i);
|
|
299
525
|
return found;
|
|
300
526
|
};
|
|
301
527
|
|
|
302
528
|
/**
|
|
303
|
-
*
|
|
304
|
-
* of the ones
|
|
305
|
-
*
|
|
529
|
+
* Bring the sheet's elements to the reach — minting the pages that entered
|
|
530
|
+
* it, letting go of the ones that left — and answer with the indices in the
|
|
531
|
+
* order they are painted in. The single definition of which pages carry
|
|
532
|
+
* pixels, and now of which carry elements: the repaint below and the scroll
|
|
306
533
|
* pass both go through here, and neither holds a page the other released.
|
|
307
534
|
*
|
|
308
535
|
* Indices rather than elements, for the reason `paintPage` takes one: a
|
|
@@ -311,16 +538,37 @@ export let sheet = (scroll, sheetEl, scale, pageClass = "qv-page") => {
|
|
|
311
538
|
* @type {() => number[]}
|
|
312
539
|
*/
|
|
313
540
|
let keep = () => {
|
|
314
|
-
let
|
|
315
|
-
let
|
|
316
|
-
// `
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
}
|
|
541
|
+
let kept = inReach();
|
|
542
|
+
let wanted = new Set(kept);
|
|
543
|
+
// A `Map` iterator tolerates the entry it is visiting being deleted, which
|
|
544
|
+
// is the only one `release` takes, so this walks the live map.
|
|
545
|
+
for (let i of pages.keys()) if (!wanted.has(i)) release(i);
|
|
546
|
+
mint(kept);
|
|
321
547
|
return kept;
|
|
322
548
|
};
|
|
323
549
|
|
|
550
|
+
/**
|
|
551
|
+
* Give every page of the reach that has none an element. Ascending, each
|
|
552
|
+
* fresh page inserted after the one before it, so the sheet's page elements
|
|
553
|
+
* stay in the list's own order and stay one run of children — an overlay the
|
|
554
|
+
* caller hung in the same element keeps its place after them.
|
|
555
|
+
*
|
|
556
|
+
* @type {(kept: number[]) => void}
|
|
557
|
+
*/
|
|
558
|
+
let mint = (kept) => {
|
|
559
|
+
let previous = null;
|
|
560
|
+
for (let i of kept) {
|
|
561
|
+
let canvas = pageAt(i);
|
|
562
|
+
if (!canvas) {
|
|
563
|
+
canvas = pageOf(i);
|
|
564
|
+
pages.set(i, canvas);
|
|
565
|
+
if (previous) previous.after(canvas);
|
|
566
|
+
else sheetEl.prepend(canvas);
|
|
567
|
+
}
|
|
568
|
+
previous = canvas;
|
|
569
|
+
}
|
|
570
|
+
};
|
|
571
|
+
|
|
324
572
|
// Paint the reach in order, yielding between pages and giving way to any
|
|
325
573
|
// repaint that started since. Sizing is the caller's, and comes first.
|
|
326
574
|
let repaint = async () => {
|
|
@@ -353,54 +601,49 @@ export let sheet = (scroll, sheetEl, scale, pageClass = "qv-page") => {
|
|
|
353
601
|
scroll.addEventListener("scroll", follow);
|
|
354
602
|
|
|
355
603
|
return {
|
|
356
|
-
/** Put a list on the sheet:
|
|
357
|
-
*
|
|
604
|
+
/** Put a list on the sheet: the extent whole at once, and the reach among
|
|
605
|
+
* its pages given elements and painted. Settles behind the reach — never
|
|
606
|
+
* rejects. */
|
|
358
607
|
/** @type {(next: any, faces: any) => Promise<void>} */
|
|
359
608
|
async swap(next, faces) {
|
|
360
609
|
fonts = faces;
|
|
361
|
-
for (let el of pages) el.remove();
|
|
610
|
+
for (let el of pages.values()) el.remove();
|
|
611
|
+
pages = new Map();
|
|
362
612
|
list = next;
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
//
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
sheetEl.prepend(batch);
|
|
613
|
+
stacking = null;
|
|
614
|
+
extended = null;
|
|
615
|
+
// Before the first page is built, so the extent is whole from the moment
|
|
616
|
+
// the list arrives rather than from the moment the reach has painted.
|
|
617
|
+
extend();
|
|
369
618
|
await repaint();
|
|
370
619
|
},
|
|
371
|
-
/** Give
|
|
372
|
-
* before writing scroll offsets back: the extent those
|
|
373
|
-
* against is this one. */
|
|
620
|
+
/** Give the sheet its extent and the reach its sizes at the current scale.
|
|
621
|
+
* Callers run this before writing scroll offsets back: the extent those
|
|
622
|
+
* are clamped against is this one. */
|
|
374
623
|
sizeAll,
|
|
375
624
|
/** Repaint the reach, releasing what it has left behind. */
|
|
376
625
|
repaint,
|
|
377
626
|
/** One reach pass a frame, however many scrolls or resizes ask. */
|
|
378
627
|
follow,
|
|
379
|
-
/** Where
|
|
380
|
-
* positioned against
|
|
628
|
+
/** Where page `i` starts down the sheet, in CSS pixels: what an overlay
|
|
629
|
+
* positioned against that page has to agree with. `0` before a list
|
|
630
|
+
* arrives, there being no sheet to sit on.
|
|
381
631
|
*
|
|
382
|
-
*
|
|
383
|
-
*
|
|
384
|
-
*
|
|
385
|
-
*
|
|
386
|
-
* against
|
|
387
|
-
|
|
388
|
-
let out = [];
|
|
389
|
-
// Sheet-space, from the sheet's own top — not the scroll-space walk the
|
|
390
|
-
// reach uses, which starts a gutter down. An overlay hangs inside the
|
|
391
|
-
// sheet, so this is the origin it is positioned against.
|
|
392
|
-
let y = 0;
|
|
393
|
-
for (let page of list ? list.pages : []) {
|
|
394
|
-
out.push(y);
|
|
395
|
-
y += page.height * scale() + GAP;
|
|
396
|
-
}
|
|
397
|
-
return out;
|
|
398
|
-
},
|
|
632
|
+
* The very offset the sheet places its own page at — so the agreement an
|
|
633
|
+
* overlay needs is a fact rather than the presumption it used to be (ADR
|
|
634
|
+
* 0065). One page rather than an array of every page's, because the
|
|
635
|
+
* stacking is a pitch and there is no array: a caller asks for the page it
|
|
636
|
+
* is placing something against (ADR 0069). */
|
|
637
|
+
topOf: (/** @type {number} */ i) => (list ? i * measure().pitch : 0),
|
|
399
638
|
/** The page width the list carries, or `null` before one arrives — what a
|
|
400
639
|
* fit is measured against. */
|
|
401
640
|
width: () => (list ? list.width : null),
|
|
402
|
-
/** @type {(i: number) => HTMLCanvasElement} */
|
|
641
|
+
/** @type {(i: number) => HTMLCanvasElement | null} */
|
|
403
642
|
pageAt,
|
|
643
|
+
/** Which page a client point is over, and where in that page's own points
|
|
644
|
+
* — the inverse of `topOf()`, answering for any page of the list whether or
|
|
645
|
+
* not the reach has given it an element. */
|
|
646
|
+
at,
|
|
404
647
|
count: () => (list ? list.pages.length : 0),
|
|
405
648
|
};
|
|
406
649
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@quario/landing",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "What quario's two surface elements share — the option check, the render boundary and the failure state — in the makings, not yet released",
|
|
5
5
|
"homepage": "https://getquario.com",
|
|
6
6
|
"license": "SEE LICENSE IN LICENSE",
|
|
@@ -36,13 +36,13 @@
|
|
|
36
36
|
"test:browser": "node test/browser/setup.js"
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
39
|
-
"@quario/layout": "^0.
|
|
39
|
+
"@quario/layout": "^0.6.0"
|
|
40
40
|
},
|
|
41
41
|
"devDependencies": {
|
|
42
42
|
"@arethetypeswrong/cli": "^0.18.3",
|
|
43
43
|
"@size-limit/preset-small-lib": "^13.0.3",
|
|
44
44
|
"lit": "^3.3.3",
|
|
45
|
-
"playwright": "^1.
|
|
45
|
+
"playwright": "^1.63.0",
|
|
46
46
|
"size-limit": "^13.0.3",
|
|
47
47
|
"typescript": "^7.0.2"
|
|
48
48
|
},
|