@altnovatrail/canvas 0.0.0-stage → 0.1.0

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.
Files changed (75) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +59 -2
  3. package/lib/core/boxes.d.ts +54 -0
  4. package/lib/core/boxes.js +116 -0
  5. package/lib/core/clock.d.ts +25 -0
  6. package/lib/core/clock.js +39 -0
  7. package/lib/core/crop.d.ts +87 -0
  8. package/lib/core/crop.js +227 -0
  9. package/lib/core/eraser.d.ts +42 -0
  10. package/lib/core/eraser.js +127 -0
  11. package/lib/core/example.d.ts +66 -0
  12. package/lib/core/example.js +116 -0
  13. package/lib/core/fields.d.ts +29 -0
  14. package/lib/core/fields.js +78 -0
  15. package/lib/core/geometry.d.ts +38 -0
  16. package/lib/core/geometry.js +107 -0
  17. package/lib/core/grid.d.ts +30 -0
  18. package/lib/core/grid.js +47 -0
  19. package/lib/core/groups.d.ts +55 -0
  20. package/lib/core/groups.js +172 -0
  21. package/lib/core/hands.d.ts +120 -0
  22. package/lib/core/hands.js +273 -0
  23. package/lib/core/history.d.ts +42 -0
  24. package/lib/core/history.js +228 -0
  25. package/lib/core/index.d.ts +12 -0
  26. package/lib/core/index.js +15 -0
  27. package/lib/core/ink.d.ts +1 -0
  28. package/lib/core/ink.js +5 -0
  29. package/lib/core/manifest.d.ts +22 -0
  30. package/lib/core/manifest.js +50 -0
  31. package/lib/core/marks.d.ts +145 -0
  32. package/lib/core/marks.js +454 -0
  33. package/lib/core/outline.d.ts +22 -0
  34. package/lib/core/outline.js +50 -0
  35. package/lib/core/page.d.ts +62 -0
  36. package/lib/core/page.js +309 -0
  37. package/lib/core/png.d.ts +13 -0
  38. package/lib/core/png.js +231 -0
  39. package/lib/core/raster.d.ts +14 -0
  40. package/lib/core/raster.js +72 -0
  41. package/lib/core/recorder.d.ts +73 -0
  42. package/lib/core/recorder.js +127 -0
  43. package/lib/core/screen.d.ts +35 -0
  44. package/lib/core/screen.js +36 -0
  45. package/lib/core/sets.d.ts +10 -0
  46. package/lib/core/sets.js +31 -0
  47. package/lib/core/sheet.d.ts +9 -0
  48. package/lib/core/sheet.js +6 -0
  49. package/lib/core/submission.d.ts +41 -0
  50. package/lib/core/submission.js +63 -0
  51. package/lib/core/task.d.ts +214 -0
  52. package/lib/core/task.js +343 -0
  53. package/lib/core/widths.d.ts +57 -0
  54. package/lib/core/widths.js +218 -0
  55. package/lib/core/work.d.ts +77 -0
  56. package/lib/core/work.js +293 -0
  57. package/lib/element/attributes.d.ts +11 -0
  58. package/lib/element/attributes.js +22 -0
  59. package/lib/element/index.d.ts +219 -0
  60. package/lib/element/index.js +1898 -0
  61. package/lib/element/labels.d.ts +41 -0
  62. package/lib/element/labels.js +74 -0
  63. package/lib/element/parts.d.ts +63 -0
  64. package/lib/element/parts.js +191 -0
  65. package/lib/element/pictures.d.ts +45 -0
  66. package/lib/element/pictures.js +167 -0
  67. package/lib/element/probe.d.ts +33 -0
  68. package/lib/element/probe.js +18 -0
  69. package/lib/element/session.d.ts +3 -0
  70. package/lib/element/session.js +7 -0
  71. package/lib/element/statement.d.ts +35 -0
  72. package/lib/element/statement.js +116 -0
  73. package/lib/element/tools.d.ts +64 -0
  74. package/lib/element/tools.js +254 -0
  75. package/package.json +41 -4
@@ -0,0 +1,41 @@
1
+ /**
2
+ * What the element says: the row of buttons its own name — the canvas kind's, or a worked
3
+ * example's — the name of the group of tools and each button, a workbook's box its name, and the
4
+ * number of a part of a demo the element tells of.
5
+ */
6
+ export interface Labels {
7
+ readonly sheet: string;
8
+ readonly workbook: string;
9
+ /** The row of a lesson's worked example, which has no tools of writing (R42). */
10
+ readonly example: string;
11
+ /** The buttons of a worked example's demo: the next part, and the whole demo, shown or hidden. */
12
+ readonly next: string;
13
+ readonly demo: string;
14
+ readonly tool: string;
15
+ readonly pen: string;
16
+ readonly eraser: string;
17
+ readonly undo: string;
18
+ readonly clear: string;
19
+ /** The buttons that scroll the sheet back up and on down, with a single pointer (R33). */
20
+ readonly up: string;
21
+ readonly down: string;
22
+ /** The button that lets a finger write again, in pen mode (R29). */
23
+ readonly finger: string;
24
+ /** A box of a workbook's line, numbered when the line has more than one (R36). */
25
+ readonly box: string;
26
+ /** What a filled box's name adds (R37). */
27
+ readonly filled: string;
28
+ /** The number of a part of a demo the element tells of, of so many: «Part 2 of 4» (R42). */
29
+ readonly part: string;
30
+ readonly of: string;
31
+ }
32
+ /**
33
+ * The labels for a language tag, and the language they are in. The tag is looked up as RFC 4647,
34
+ * section 3.4, does it: in lower case, cut subtag by subtag from its end until the table has what
35
+ * is left, a single-letter subtag going with the subtag after it. English stands for any other
36
+ * tag, and for none.
37
+ */
38
+ export declare function labelsFor(tag: string | null): {
39
+ readonly lang: string;
40
+ readonly labels: Labels;
41
+ };
@@ -0,0 +1,74 @@
1
+ // The labels of the element's own buttons, of a workbook's boxes and of the parts of a worked
2
+ // example's demo, in the languages of its interface (R28, R29, R33, R36, R37, R42): English and
3
+ // Russian, as О-2 recommends, until app's T07 gives the shared scheme of translations of the packages
4
+ // of the interface. The language is the element's own `lang`, looked up as RFC 4647 does it. This
5
+ // module is no entry point of the package.
6
+ /** The labels in English, the code's own language: those of any language the table has not. */
7
+ const ENGLISH = Object.freeze({
8
+ sheet: "Sheet",
9
+ workbook: "Workbook",
10
+ example: "Worked example",
11
+ next: "Next",
12
+ demo: "Demo",
13
+ tool: "Tool",
14
+ pen: "Pen",
15
+ eraser: "Eraser",
16
+ undo: "Undo",
17
+ clear: "Clear",
18
+ up: "Up",
19
+ down: "Down",
20
+ finger: "Draw with finger",
21
+ box: "Box",
22
+ filled: "filled",
23
+ part: "Part",
24
+ of: "of",
25
+ });
26
+ /**
27
+ * The labels by language, each a language tag in lower case. A map, not an object: a tag such as
28
+ * "constructor" finds nothing of an object's prototype in it.
29
+ */
30
+ const LABELS = new Map([
31
+ ["en", ENGLISH],
32
+ [
33
+ "ru",
34
+ Object.freeze({
35
+ sheet: "Лист",
36
+ workbook: "Тетрадь",
37
+ example: "Пример",
38
+ next: "Дальше",
39
+ demo: "Показ",
40
+ tool: "Инструмент",
41
+ pen: "Перо",
42
+ eraser: "Ластик",
43
+ undo: "Отменить",
44
+ clear: "Очистить",
45
+ up: "Выше",
46
+ down: "Ниже",
47
+ finger: "Рисовать пальцем",
48
+ box: "Окошко",
49
+ filled: "заполнено",
50
+ part: "Часть",
51
+ of: "из",
52
+ }),
53
+ ],
54
+ ]);
55
+ /**
56
+ * The labels for a language tag, and the language they are in. The tag is looked up as RFC 4647,
57
+ * section 3.4, does it: in lower case, cut subtag by subtag from its end until the table has what
58
+ * is left, a single-letter subtag going with the subtag after it. English stands for any other
59
+ * tag, and for none.
60
+ */
61
+ export function labelsFor(tag) {
62
+ let range = (tag ?? "").toLowerCase();
63
+ while (range !== "") {
64
+ const labels = LABELS.get(range);
65
+ if (labels !== undefined)
66
+ return { lang: range, labels };
67
+ let cut = range.lastIndexOf("-");
68
+ // "x" of "en-x-school", or the letter of an extension, means nothing without what follows it.
69
+ if (cut >= 2 && range[cut - 2] === "-")
70
+ cut -= 2;
71
+ range = cut > 0 ? range.slice(0, cut) : "";
72
+ }
73
+ return { lang: "en", labels: ENGLISH };
74
+ }
@@ -0,0 +1,63 @@
1
+ import { type DemoState, type ExamplePart } from "../core/example.js";
2
+ import type { Pictures } from "./pictures.js";
3
+ /** The number of a part the region tells of, as the labels say it, and the labels' language. */
4
+ export interface Told {
5
+ /** The number of the part, of so many, in the labels' language: «Часть 2 из 4.». */
6
+ readonly number: string;
7
+ /** The language of the labels the number is in. */
8
+ readonly lang: string;
9
+ }
10
+ /** The list of a demo's open parts and the region that tells of them, as the element shows them. */
11
+ export declare class Parts {
12
+ #private;
13
+ /** The open parts, in the module's explanation language, or in the element's own without one. */
14
+ readonly list: HTMLOListElement;
15
+ /**
16
+ * The polite region that tells of the part just opened: empty from the start, and hidden from
17
+ * sight alone, for a region out of the tree of what a screen reader reads tells nothing.
18
+ */
19
+ readonly status: HTMLDivElement;
20
+ /**
21
+ * The pictures on areas of the sheet, for the layer of the author's demo: in sheet units, as the
22
+ * layer's view box counts, in the demo's order, so that a later part is drawn over an earlier.
23
+ */
24
+ readonly layer: SVGGElement;
25
+ /**
26
+ * Where the sheet scrolls to for a picture on an area just opened: a box of the area in the
27
+ * sheet's own flow, not on its sticky stage, whose pictures move only at the next frame. It is
28
+ * hidden, so it paints nothing and takes no pointer, but it has a box to scroll into sight.
29
+ */
30
+ readonly spot: HTMLDivElement;
31
+ /** The list and the region, by their IDs, whose pictures the element's pictures give files. */
32
+ constructor(ids: {
33
+ readonly status: string;
34
+ readonly parts: string;
35
+ }, pictures: Pictures);
36
+ /**
37
+ * The language the parts are in, a BCP 47 tag, or `undefined` for the element's own, which the
38
+ * list inherits. A new language changes the attribute alone.
39
+ */
40
+ set lang(lang: string | undefined);
41
+ /**
42
+ * The demo as a state has it: as many parts open as it says, from the first, items added or
43
+ * taken off at the list's end alone, and the list hidden or shown. Only what has changed is
44
+ * written: hidden, the parts keep their pictures.
45
+ */
46
+ show(demo: readonly ExamplePart[], { opened, hidden }: DemoState): void;
47
+ /** No part open, the list shown, and nothing told: a demo begins, or goes. */
48
+ clear(): void;
49
+ /**
50
+ * What the `n`th part, from 1, comes into sight by once it is open (R42): its item, or for a
51
+ * picture on an area of the sheet the spot, which it places on that area, in shares of the sheet,
52
+ * which keeps its proportions.
53
+ */
54
+ target(n: number): HTMLElement | undefined;
55
+ /**
56
+ * The region tells of a part just opened: its number, in the labels' language, and its words —
57
+ * a paragraph's text, a picture's description — in the parts' own. A picture that only decorates
58
+ * has its number alone.
59
+ */
60
+ tell(part: ExamplePart, told: Told): void;
61
+ /** The region is empty: what it told is no more, and the same words may be told again. */
62
+ quiet(): void;
63
+ }
@@ -0,0 +1,191 @@
1
+ // The parts of a worked example's demo the learner has opened (R42): a numbered list above the
2
+ // sheet, under the statement, in the element's own scrolling, and a polite region that tells a
3
+ // screen reader of the part just opened. A part opens at the list's end, and an item, once built,
4
+ // stays as it is while the demo is the same: hidden, the list keeps its open parts out of sight. A
5
+ // paragraph is its text; a picture beside the sheet is a frame of its size, as a statement's
6
+ // picture is, which shows the picture once the element's pictures have its file and what it shows
7
+ // in words till then. A picture on an area of the sheet is read by its words here, while the
8
+ // picture itself stands on the layer of the author's demo, stretched over its area, the later part
9
+ // over the earlier, and nothing stands there till its file is decoded (T61). This module is no
10
+ // entry point of the package.
11
+ import { onSheet } from "../core/example.js";
12
+ import { SHEET } from "../core/sheet.js";
13
+ import { placeOver, SVG, write } from "./attributes.js";
14
+ import { framed } from "./statement.js";
15
+ /** The list of a demo's open parts and the region that tells of them, as the element shows them. */
16
+ export class Parts {
17
+ /** The open parts, in the module's explanation language, or in the element's own without one. */
18
+ list = document.createElement("ol");
19
+ /**
20
+ * The polite region that tells of the part just opened: empty from the start, and hidden from
21
+ * sight alone, for a region out of the tree of what a screen reader reads tells nothing.
22
+ */
23
+ status = document.createElement("div");
24
+ /**
25
+ * The pictures on areas of the sheet, for the layer of the author's demo: in sheet units, as the
26
+ * layer's view box counts, in the demo's order, so that a later part is drawn over an earlier.
27
+ */
28
+ layer = document.createElementNS(SVG, "g");
29
+ /**
30
+ * Where the sheet scrolls to for a picture on an area just opened: a box of the area in the
31
+ * sheet's own flow, not on its sticky stage, whose pictures move only at the next frame. It is
32
+ * hidden, so it paints nothing and takes no pointer, but it has a box to scroll into sight.
33
+ */
34
+ spot = document.createElement("div");
35
+ #pictures;
36
+ /** What takes the picture of an item off, once the item leaves the list. */
37
+ #off = new WeakMap();
38
+ /** The area of the sheet of an item whose picture stands there. */
39
+ #areas = new WeakMap();
40
+ /** The list and the region, by their IDs, whose pictures the element's pictures give files. */
41
+ constructor(ids, pictures) {
42
+ this.#pictures = pictures;
43
+ this.list.className = "parts";
44
+ this.list.id = ids.parts;
45
+ this.status.className = "status";
46
+ this.status.id = ids.status;
47
+ this.status.setAttribute("role", "status");
48
+ this.spot.className = "spot";
49
+ }
50
+ /**
51
+ * The language the parts are in, a BCP 47 tag, or `undefined` for the element's own, which the
52
+ * list inherits. A new language changes the attribute alone.
53
+ */
54
+ set lang(lang) {
55
+ write(this.list, "lang", lang ?? null);
56
+ }
57
+ /**
58
+ * The demo as a state has it: as many parts open as it says, from the first, items added or
59
+ * taken off at the list's end alone, and the list hidden or shown. Only what has changed is
60
+ * written: hidden, the parts keep their pictures.
61
+ */
62
+ show(demo, { opened, hidden }) {
63
+ let last = this.list.lastElementChild;
64
+ while (last !== null && this.list.childElementCount > opened) {
65
+ this.#takeOff(last);
66
+ last.remove();
67
+ last = this.list.lastElementChild;
68
+ }
69
+ for (let n = this.list.childElementCount; n < opened; n++) {
70
+ const part = demo[n];
71
+ if (part !== undefined)
72
+ this.list.append(this.#itemOf(part));
73
+ }
74
+ if (this.list.hidden !== hidden)
75
+ this.list.hidden = hidden;
76
+ // An SVG element has no hidden attribute: the presentation attribute hides it.
77
+ write(this.layer, "display", hidden ? "none" : null);
78
+ }
79
+ /** No part open, the list shown, and nothing told: a demo begins, or goes. */
80
+ clear() {
81
+ for (const item of [...this.list.children])
82
+ this.#takeOff(item);
83
+ this.list.replaceChildren();
84
+ this.list.hidden = false;
85
+ write(this.layer, "display", null);
86
+ this.spot.removeAttribute("style");
87
+ this.quiet();
88
+ }
89
+ /**
90
+ * What the `n`th part, from 1, comes into sight by once it is open (R42): its item, or for a
91
+ * picture on an area of the sheet the spot, which it places on that area, in shares of the sheet,
92
+ * which keeps its proportions.
93
+ */
94
+ target(n) {
95
+ const item = this.list.children[n - 1];
96
+ const area = item === undefined ? undefined : this.#areas.get(item);
97
+ if (area !== undefined) {
98
+ const { left, top, width, height } = area;
99
+ const share = (value, of) => `${(100 * value) / of}%`;
100
+ Object.assign(this.spot.style, {
101
+ left: share(left, SHEET.width),
102
+ top: share(top, SHEET.height),
103
+ width: share(width, SHEET.width),
104
+ height: share(height, SHEET.height),
105
+ });
106
+ return this.spot;
107
+ }
108
+ return item instanceof HTMLElement ? item : undefined;
109
+ }
110
+ /**
111
+ * The region tells of a part just opened: its number, in the labels' language, and its words —
112
+ * a paragraph's text, a picture's description — in the parts' own. A picture that only decorates
113
+ * has its number alone.
114
+ */
115
+ tell(part, told) {
116
+ const number = document.createElement("span");
117
+ number.lang = told.lang;
118
+ number.textContent = told.number;
119
+ const words = "text" in part ? part.text : part.alt;
120
+ if (words === "") {
121
+ this.status.replaceChildren(number);
122
+ return;
123
+ }
124
+ const said = document.createElement("span");
125
+ // The parts' language, which the list has, or none: then the element's own, as the list's.
126
+ const lang = this.list.getAttribute("lang");
127
+ if (lang !== null)
128
+ said.lang = lang;
129
+ said.textContent = words;
130
+ this.status.replaceChildren(number, " ", said);
131
+ }
132
+ /** The region is empty: what it told is no more, and the same words may be told again. */
133
+ quiet() {
134
+ this.status.replaceChildren();
135
+ }
136
+ /** An item leaving the list takes its picture off, once. */
137
+ #takeOff(item) {
138
+ this.#off.get(item)?.();
139
+ this.#off.delete(item);
140
+ }
141
+ /**
142
+ * The item of an open part: a paragraph's text, a picture beside the sheet in its frame, or the
143
+ * words of a picture on an area of the sheet, which a screen reader reads as the picture, while
144
+ * the item stays an item of the list, and the picture itself on the sheet. One that only
145
+ * decorates leaves its item empty, and on the sheet it stands all the same.
146
+ */
147
+ #itemOf(part) {
148
+ const item = document.createElement("li");
149
+ if ("text" in part) {
150
+ // Text, never markup: a module is data (R02 spec).
151
+ item.textContent = part.text;
152
+ }
153
+ else if (!onSheet(part)) {
154
+ const { frame, off } = framed(this.#pictures, part);
155
+ this.#off.set(item, off);
156
+ item.append(frame);
157
+ }
158
+ else {
159
+ this.#off.set(item, this.#onTheSheet(part));
160
+ this.#areas.set(item, part.area);
161
+ if (part.alt !== "") {
162
+ const words = document.createElement("div");
163
+ words.setAttribute("role", "img");
164
+ words.setAttribute("aria-label", part.alt);
165
+ words.textContent = part.alt;
166
+ item.append(words);
167
+ }
168
+ }
169
+ return item;
170
+ }
171
+ /**
172
+ * A picture on its area of the sheet, at the end of the layer, stretched over the area: its
173
+ * corners at the area's (README of the format, §21; SVG 2). It shows its file's address once the
174
+ * element's pictures have it decoded, and nothing till then: its words are in its item. What it
175
+ * gives back takes the picture off the sheet.
176
+ */
177
+ #onTheSheet(part) {
178
+ const image = document.createElementNS(SVG, "image");
179
+ placeOver(image, part.area);
180
+ image.setAttribute("preserveAspectRatio", "none");
181
+ this.layer.append(image);
182
+ const off = this.#pictures.place(part.picture, {
183
+ show: (url) => image.setAttribute("href", url),
184
+ hide: () => image.removeAttribute("href"),
185
+ });
186
+ return () => {
187
+ off();
188
+ image.remove();
189
+ };
190
+ }
191
+ }
@@ -0,0 +1,45 @@
1
+ /**
2
+ * What gives the file of a picture of a task or a worked example: the consumer's, for the canvas
3
+ * goes to no network (R07; repositories 2.1 spec). app takes the files from what api serves, the
4
+ * samples page from its own build.
5
+ */
6
+ export type Media = (ref: string) => Promise<Blob>;
7
+ /** A picture on show, as what shows it has it. */
8
+ export interface PictureView {
9
+ /** Its file's address came, decoded: the picture shows it. */
10
+ show(url: string): void;
11
+ /** The address it showed went: the picture shows what it shows in words again. */
12
+ hide(): void;
13
+ }
14
+ /** The files of the pictures of an element, by their references, and their addresses. */
15
+ export declare class Pictures {
16
+ #private;
17
+ get media(): Media | undefined;
18
+ /**
19
+ * A new function serves the references that have no file and no load under way: those waiting
20
+ * for one, and those whose load failed. It stops no load the last one began.
21
+ */
22
+ set media(media: Media | undefined);
23
+ /**
24
+ * The references of the pictures the element shows, or may show: a new task's statement's, or a
25
+ * new worked example's statement's and demo's. Those no longer named go: their addresses at once,
26
+ * so their pictures show their words, and their files; a load under way goes on, and its file
27
+ * goes to the reference only if it is named again by then. A reference named before keeps what it
28
+ * has. On the page, every reference without a file asks for it: one that did not come, or did
29
+ * not decode, is asked for again with each new task or example.
30
+ */
31
+ name(refs: Iterable<string>): void;
32
+ /**
33
+ * A picture of a reference on show: it shows the file's address at once if the address is
34
+ * decoded, and otherwise once it is. What it gives back takes the picture off, and lets no
35
+ * address go: an address lives while its reference is named and the element is on the page.
36
+ */
37
+ place(ref: string, view: PictureView): () => void;
38
+ /** The element came to the page: the files it has get their addresses, and the rest load. */
39
+ connect(): void;
40
+ /**
41
+ * The element left the page. The addresses of its files go, unless it comes back at once, as an
42
+ * element moved in the page does; the files stay with it, for it may come back later.
43
+ */
44
+ disconnect(): void;
45
+ }
@@ -0,0 +1,167 @@
1
+ // The files of the pictures an element shows, by their references (R23, R42): a task's statement's,
2
+ // and a worked example's — its statement's and its demo's, beside the sheet and on areas of it.
3
+ // One reference is one file and one call of the consumer's media, however many pictures show it,
4
+ // and a file stays while what the element shows names its reference. On the page a file has one
5
+ // address, decoded once, before any picture shows it: a picture placed then shows it at once. What
6
+ // a picture is — a frame of the statement or of the list of parts, or a picture on the sheet — is
7
+ // its own: the pictures here are told when to show the address and when it has gone. This module is
8
+ // no entry point of the package.
9
+ /** The files of the pictures of an element, by their references, and their addresses. */
10
+ export class Pictures {
11
+ /** The references named, in the order they were named, and what the element has of each. */
12
+ #held = new Map();
13
+ /** The pictures on show, by reference: placed apart from the naming. */
14
+ #views = new Map();
15
+ /** The loads under way, by reference: a reference named again waits for the load it began. */
16
+ #loading = new Map();
17
+ #media;
18
+ #connected = false;
19
+ get media() {
20
+ return this.#media;
21
+ }
22
+ /**
23
+ * A new function serves the references that have no file and no load under way: those waiting
24
+ * for one, and those whose load failed. It stops no load the last one began.
25
+ */
26
+ set media(media) {
27
+ this.#media = media;
28
+ if (this.#connected)
29
+ this.#load();
30
+ }
31
+ /**
32
+ * The references of the pictures the element shows, or may show: a new task's statement's, or a
33
+ * new worked example's statement's and demo's. Those no longer named go: their addresses at once,
34
+ * so their pictures show their words, and their files; a load under way goes on, and its file
35
+ * goes to the reference only if it is named again by then. A reference named before keeps what it
36
+ * has. On the page, every reference without a file asks for it: one that did not come, or did
37
+ * not decode, is asked for again with each new task or example.
38
+ */
39
+ name(refs) {
40
+ const named = new Set(refs);
41
+ for (const [ref, held] of this.#held) {
42
+ if (named.has(ref))
43
+ continue;
44
+ this.#release(ref, held);
45
+ this.#held.delete(ref);
46
+ }
47
+ for (const ref of named) {
48
+ if (this.#held.has(ref))
49
+ continue;
50
+ this.#held.set(ref, { file: undefined, url: undefined, image: undefined, decoded: false });
51
+ }
52
+ if (this.#connected)
53
+ this.#load();
54
+ }
55
+ /**
56
+ * A picture of a reference on show: it shows the file's address at once if the address is
57
+ * decoded, and otherwise once it is. What it gives back takes the picture off, and lets no
58
+ * address go: an address lives while its reference is named and the element is on the page.
59
+ */
60
+ place(ref, view) {
61
+ let views = this.#views.get(ref);
62
+ if (views === undefined) {
63
+ views = new Set();
64
+ this.#views.set(ref, views);
65
+ }
66
+ views.add(view);
67
+ const held = this.#held.get(ref);
68
+ if (held?.decoded === true && held.url !== undefined)
69
+ view.show(held.url);
70
+ return () => {
71
+ const placed = this.#views.get(ref);
72
+ placed?.delete(view);
73
+ if (placed?.size === 0)
74
+ this.#views.delete(ref);
75
+ };
76
+ }
77
+ /** The element came to the page: the files it has get their addresses, and the rest load. */
78
+ connect() {
79
+ this.#connected = true;
80
+ for (const [ref, held] of this.#held)
81
+ this.#attach(ref, held);
82
+ this.#load();
83
+ }
84
+ /**
85
+ * The element left the page. The addresses of its files go, unless it comes back at once, as an
86
+ * element moved in the page does; the files stay with it, for it may come back later.
87
+ */
88
+ disconnect() {
89
+ this.#connected = false;
90
+ queueMicrotask(() => {
91
+ if (this.#connected)
92
+ return;
93
+ for (const [ref, held] of this.#held)
94
+ this.#release(ref, held);
95
+ });
96
+ }
97
+ /** Each reference without its file and without a load under way asks the consumer for it. */
98
+ #load() {
99
+ const media = this.#media;
100
+ if (media === undefined)
101
+ return;
102
+ for (const [ref, held] of this.#held) {
103
+ if (held.file !== undefined || this.#loading.has(ref))
104
+ continue;
105
+ // A function that throws fails the load as one whose promise is rejected does.
106
+ const load = Promise.resolve()
107
+ .then(() => media(ref))
108
+ .then((file) => {
109
+ if (!(file instanceof Blob))
110
+ throw new TypeError(`media gave ${typeof file}, not a Blob`);
111
+ // The file of a reference no longer named is not kept.
112
+ const named = this.#held.get(ref);
113
+ if (named === undefined)
114
+ return;
115
+ named.file = file;
116
+ this.#attach(ref, named);
117
+ })
118
+ .catch((reason) => {
119
+ console.warn(`the picture "${ref}" did not come: ${String(reason)}`);
120
+ })
121
+ .finally(() => this.#loading.delete(ref));
122
+ this.#loading.set(ref, load);
123
+ }
124
+ }
125
+ /**
126
+ * A file gets its address, if the element is on the page and the file has none, and an image
127
+ * decodes it: then the pictures of its reference show it.
128
+ */
129
+ #attach(ref, held) {
130
+ if (held.file === undefined || held.url !== undefined || !this.#connected)
131
+ return;
132
+ const url = URL.createObjectURL(held.file);
133
+ const image = new Image();
134
+ held.url = url;
135
+ held.image = image;
136
+ image.src = url;
137
+ image.decode().then(() => {
138
+ // The address may have gone, or its reference, while the file was decoded.
139
+ if (held.image !== image)
140
+ return;
141
+ held.decoded = true;
142
+ for (const view of this.#views.get(ref) ?? [])
143
+ view.show(url);
144
+ }, () => {
145
+ if (held.image !== image)
146
+ return;
147
+ // A file that is no picture: it goes, and a new media function may give another.
148
+ this.#release(ref, held);
149
+ held.file = undefined;
150
+ console.warn(`the picture "${ref}" is no picture a browser shows`);
151
+ });
152
+ }
153
+ /** A file lets its address go, and the pictures that showed it show their words again. */
154
+ #release(ref, held) {
155
+ if (held.url === undefined)
156
+ return;
157
+ URL.revokeObjectURL(held.url);
158
+ const shown = held.decoded;
159
+ held.url = undefined;
160
+ held.image = undefined;
161
+ held.decoded = false;
162
+ if (!shown)
163
+ return;
164
+ for (const view of this.#views.get(ref) ?? [])
165
+ view.hide();
166
+ }
167
+ }
@@ -0,0 +1,33 @@
1
+ import type { Box } from "../core/geometry.js";
2
+ import type { Role } from "../core/hands.js";
3
+ import type { Stroke } from "../core/ink.js";
4
+ import type { View } from "../core/screen.js";
5
+ /** An element's state, as its test reads it. */
6
+ export interface Probe {
7
+ /** The strokes on the sheet, in the order they were written: the work's, after any undo. */
8
+ readonly strokes: readonly Stroke[];
9
+ /** The stroke being written, or nothing between strokes. */
10
+ readonly live: Stroke | undefined;
11
+ /** The IDs of the strokes the eraser under way has come near, or none between presses. */
12
+ readonly pending: readonly number[];
13
+ /** How many outlines of strokes the element has computed so far. */
14
+ readonly outlines: number;
15
+ /** Where the sheet is on the screen, or nothing while the element is not laid out. */
16
+ readonly view: View | undefined;
17
+ /**
18
+ * The tutor's marks the sheet shows, in their order: each one's name, the strokes it holds on to
19
+ * and its frame as last drawn, in sheet units — null while none of its strokes is on the screen.
20
+ * A mark without a place is not among them (R26).
21
+ */
22
+ readonly marks: readonly {
23
+ readonly name: string;
24
+ readonly ids: readonly number[];
25
+ readonly frame: Box | null;
26
+ }[];
27
+ /** What each pointer down on the sheet does, by its `pointerId` (R29). */
28
+ readonly roles: ReadonlyMap<number, Role>;
29
+ }
30
+ /** Called by each element as it is built, with what reads its state. */
31
+ export declare function attachProbe(element: object, read: () => Probe): void;
32
+ /** The state of an element of this package. */
33
+ export declare function probe(element: object): Probe;
@@ -0,0 +1,18 @@
1
+ // What a test sees of an element beyond its interface: its strokes, the stroke under way, what the
2
+ // eraser is about to take, its view of the sheet, how many outlines it has drawn, the tutor's
3
+ // marks it shows and what each pointer down on it does (R29). The history of writing is the
4
+ // element's own property now (R27). This module is no entry point of the package, so the
5
+ // element's interface stays what the tasks of the sheet make it (R07, R12, R19, R22, R27, R29;
6
+ // RUN.md, T10, T14, T15, T33, T34, T35).
7
+ const probes = new WeakMap();
8
+ /** Called by each element as it is built, with what reads its state. */
9
+ export function attachProbe(element, read) {
10
+ probes.set(element, read);
11
+ }
12
+ /** The state of an element of this package. */
13
+ export function probe(element) {
14
+ const read = probes.get(element);
15
+ if (read === undefined)
16
+ throw new Error("not an element of this package");
17
+ return read();
18
+ }
@@ -0,0 +1,3 @@
1
+ import { PenMode } from "../core/hands.js";
2
+ /** The pen mode of every sheet of this page. */
3
+ export declare const PEN_MODE: PenMode;
@@ -0,0 +1,7 @@
1
+ // The page's pen mode (R29): one for every sheet of the page, from a pen's first touch on any of
2
+ // them until the learner asks for the finger, or the page is loaded again. The next task is a new
3
+ // element (R20), and the mode stays. It is a state of the interface in memory, not data: nothing is
4
+ // stored or sent. This module is no entry point of the package.
5
+ import { PenMode } from "../core/hands.js";
6
+ /** The pen mode of every sheet of this page. */
7
+ export const PEN_MODE = new PenMode();