contour-deck 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.
Files changed (50) hide show
  1. package/README.md +107 -5
  2. package/dist/astro/Deck.astro +8 -1
  3. package/dist/astro/host.d.ts +7 -0
  4. package/dist/astro/host.js +58 -4
  5. package/dist/core/deck.js +16 -5
  6. package/dist/core/labels.js +1 -0
  7. package/dist/core/types.d.ts +12 -0
  8. package/dist/remote/clock.d.ts +35 -0
  9. package/dist/remote/clock.js +48 -0
  10. package/dist/remote/guest.d.ts +41 -0
  11. package/dist/remote/guest.js +229 -0
  12. package/dist/remote/index.d.ts +18 -0
  13. package/dist/remote/index.js +11 -0
  14. package/dist/remote/ink.d.ts +11 -0
  15. package/dist/remote/ink.js +74 -0
  16. package/dist/remote/labels.d.ts +76 -0
  17. package/dist/remote/labels.js +70 -0
  18. package/dist/remote/links.d.ts +2 -0
  19. package/dist/remote/links.js +12 -0
  20. package/dist/remote/overlay.d.ts +11 -0
  21. package/dist/remote/overlay.js +63 -0
  22. package/dist/remote/pairing.d.ts +40 -0
  23. package/dist/remote/pairing.js +97 -0
  24. package/dist/remote/panel.d.ts +40 -0
  25. package/dist/remote/panel.js +137 -0
  26. package/dist/remote/phone.d.ts +10 -0
  27. package/dist/remote/phone.js +408 -0
  28. package/dist/remote/presenter.d.ts +2 -0
  29. package/dist/remote/presenter.js +126 -0
  30. package/dist/remote/protocol.d.ts +106 -0
  31. package/dist/remote/protocol.js +123 -0
  32. package/dist/remote/qr.d.ts +8 -0
  33. package/dist/remote/qr.js +266 -0
  34. package/dist/remote/sensor.d.ts +9 -0
  35. package/dist/remote/sensor.js +50 -0
  36. package/dist/remote/session.d.ts +39 -0
  37. package/dist/remote/session.js +292 -0
  38. package/dist/remote/store.d.ts +9 -0
  39. package/dist/remote/store.js +27 -0
  40. package/dist/remote/style.d.ts +2 -0
  41. package/dist/remote/style.js +132 -0
  42. package/dist/remote/tilt.d.ts +78 -0
  43. package/dist/remote/tilt.js +148 -0
  44. package/dist/remote/transport.d.ts +36 -0
  45. package/dist/remote/transport.js +170 -0
  46. package/dist/remote/view.d.ts +48 -0
  47. package/dist/remote/view.js +119 -0
  48. package/docs/CONTRACT.md +14 -0
  49. package/docs/PROTOCOL.md +100 -0
  50. package/package.json +9 -2
@@ -0,0 +1,123 @@
1
+ /** The wire messages of docs/PROTOCOL.md, and the checks every incoming frame passes before it is used. */
2
+ /** Frames above this size are dropped by the relay (it closes the socket with 1009). */
3
+ export const MAX_FRAME = 2048;
4
+ /** An `ink` message carries at most this many points. */
5
+ export const MAX_POINTS = 64;
6
+ const COMMANDS = ["next", "prev", "slideNext", "slidePrev", "pageNext", "pagePrev", "overview", "open"];
7
+ const isFields = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
8
+ const text = (value, max) => typeof value === "string" && value.length > 0 && value.length <= max;
9
+ const finite = (value) => typeof value === "number" && Number.isFinite(value);
10
+ const unit = (value) => Math.min(1, Math.max(0, value));
11
+ /** A frame as an object with a string `t`, or `null`. */
12
+ export function decode(frame) {
13
+ if (typeof frame !== "string" || frame.length > MAX_FRAME)
14
+ return null;
15
+ try {
16
+ const value = JSON.parse(frame);
17
+ return isFields(value) && typeof value.t === "string" ? value : null;
18
+ }
19
+ catch {
20
+ return null;
21
+ }
22
+ }
23
+ /**
24
+ * A message a guest may send, with every field checked and every coordinate clamped to [0, 1];
25
+ * `null` for anything else. This is the only door for input from outside the host's own page.
26
+ */
27
+ export function guestMessage(value) {
28
+ switch (value.t) {
29
+ case "resume":
30
+ return text(value.token, 64) ? { t: "resume", token: value.token } : null;
31
+ case "cmd": {
32
+ if (typeof value.c !== "string" || !COMMANDS.includes(value.c))
33
+ return null;
34
+ const c = value.c;
35
+ if (c !== "open")
36
+ return { t: "cmd", c };
37
+ return text(value.at, 64) ? { t: "cmd", c, at: value.at } : null;
38
+ }
39
+ case "ptr":
40
+ return finite(value.x) && finite(value.y) && typeof value.on === "boolean"
41
+ ? { t: "ptr", x: unit(value.x), y: unit(value.y), on: value.on }
42
+ : null;
43
+ case "ink": {
44
+ const { s, pts } = value;
45
+ if (!finite(s) || !Array.isArray(pts) || pts.length < 2 || pts.length > MAX_POINTS * 2 || pts.length % 2)
46
+ return null;
47
+ if (!pts.every(finite))
48
+ return null;
49
+ return { t: "ink", s, pts: pts.map(unit), ...(value.end === true ? { end: true } : {}) };
50
+ }
51
+ case "ink-clear":
52
+ return { t: "ink-clear" };
53
+ case "ping":
54
+ return { t: "ping" };
55
+ default:
56
+ return null;
57
+ }
58
+ }
59
+ const number = (value, fallback = 0) => (finite(value) ? value : fallback);
60
+ /** A `state` message as the guest reads it; a field of the wrong type takes its neutral value. */
61
+ export function stateMessage(value) {
62
+ if (value.t !== "state" || typeof value.at !== "string")
63
+ return null;
64
+ return {
65
+ t: "state",
66
+ mode: value.mode === "overview" ? "overview" : "stage",
67
+ at: value.at.slice(0, 32),
68
+ step: number(value.step),
69
+ steps: number(value.steps),
70
+ elapsed: number(value.elapsed),
71
+ total: finite(value.total) ? value.total : null,
72
+ blockElapsed: number(value.blockElapsed),
73
+ blockMinutes: finite(value.blockMinutes) ? value.blockMinutes : null,
74
+ ...(finite(value.ratio) && value.ratio > 0 ? { ratio: value.ratio } : {}),
75
+ };
76
+ }
77
+ const CROCKFORD = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
78
+ const BASE64URL = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
79
+ function randomText(alphabet, bits, length) {
80
+ const bytes = crypto.getRandomValues(new Uint8Array(Math.ceil((length * bits) / 8)));
81
+ let out = "";
82
+ let buffer = 0;
83
+ let held = 0;
84
+ for (const byte of bytes) {
85
+ buffer = (buffer << 8) | byte;
86
+ held += 8;
87
+ while (held >= bits && out.length < length) {
88
+ held -= bits;
89
+ out += alphabet[(buffer >> held) & ((1 << bits) - 1)];
90
+ }
91
+ buffer &= (1 << held) - 1;
92
+ }
93
+ return out;
94
+ }
95
+ /**
96
+ * A host key: 26 characters of Crockford base32 from `crypto.getRandomValues` (the first carries 3 bits, 128 in all).
97
+ * It stays in the host's tab; the relay gives the host seat of a room only to the key whose hash is the room.
98
+ */
99
+ export const newKey = () => CROCKFORD[crypto.getRandomValues(new Uint8Array(1))[0] & 7] + randomText(CROCKFORD, 5, 25);
100
+ /** A room name, and the shape of a host key too. */
101
+ export const isRoom = (value) => typeof value === "string" && /^[0-9A-HJKMNP-TV-Z]{26}$/.test(value);
102
+ /** The room of a host key: the first 26 Crockford base32 characters of SHA-256 of the key (docs/PROTOCOL.md). */
103
+ export async function roomOf(key) {
104
+ if (!globalThis.crypto?.subtle)
105
+ throw new Error("contour-deck: the remote relay needs a secure context (https or localhost)");
106
+ const hash = new Uint8Array(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(key)));
107
+ let room = "";
108
+ for (let bit = 0; room.length < 26; bit += 5) {
109
+ const at = bit >> 3;
110
+ room += CROCKFORD[(((hash[at] << 8) | hash[at + 1]) >> (11 - (bit & 7))) & 31];
111
+ }
112
+ return room;
113
+ }
114
+ /** A resume token: 22 characters of base64url, 132 random bits. */
115
+ export const newToken = () => randomText(BASE64URL, 6, 22);
116
+ /** A fresh 4-digit pairing code, without modulo bias. */
117
+ export function newCode() {
118
+ const one = new Uint16Array(1);
119
+ do
120
+ crypto.getRandomValues(one);
121
+ while (one[0] >= 60000);
122
+ return String(one[0] % 10000).padStart(4, "0");
123
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * A small QR encoder for the pairing links: byte mode, error correction M, versions 1 to 12
3
+ * (up to 287 bytes of UTF-8). A host that already ships a QR library passes its own through the `qr` option.
4
+ */
5
+ /** The modules of the QR code of `text` (`true` is dark), `[row][column]`, or `null` when it does not fit. */
6
+ export declare function qrMatrix(text: string): boolean[][] | null;
7
+ /** The QR code of `text` as an SVG string, dark on white whatever the theme; `null` when the text is too long. */
8
+ export declare function qrSvg(text: string): string | null;
@@ -0,0 +1,266 @@
1
+ /**
2
+ * A small QR encoder for the pairing links: byte mode, error correction M, versions 1 to 12
3
+ * (up to 287 bytes of UTF-8). A host that already ships a QR library passes its own through the `qr` option.
4
+ */
5
+ /** Error-correction codewords per block and the number of blocks at level M, for versions 1 to 12. */
6
+ const BLOCKS = [[10, 1], [16, 1], [26, 1], [18, 2], [24, 2], [16, 4], [18, 4], [22, 4], [22, 5], [26, 5], [30, 5], [22, 8]];
7
+ function gfMul(x, y) {
8
+ let z = 0;
9
+ for (let i = 7; i >= 0; i--) {
10
+ z = (z << 1) ^ ((z >>> 7) * 0x11d);
11
+ z ^= ((y >>> i) & 1) * x;
12
+ }
13
+ return z;
14
+ }
15
+ /** Reed-Solomon remainder of `data` for `degree` error-correction codewords. */
16
+ function errorCorrection(data, degree) {
17
+ const divisor = new Array(degree).fill(0);
18
+ divisor[degree - 1] = 1;
19
+ let root = 1;
20
+ for (let i = 0; i < degree; i++) {
21
+ for (let j = 0; j < degree; j++) {
22
+ divisor[j] = gfMul(divisor[j], root);
23
+ if (j + 1 < degree)
24
+ divisor[j] ^= divisor[j + 1];
25
+ }
26
+ root = gfMul(root, 2);
27
+ }
28
+ const result = new Array(degree).fill(0);
29
+ for (const byte of data) {
30
+ const factor = byte ^ result.shift();
31
+ result.push(0);
32
+ divisor.forEach((coefficient, i) => (result[i] ^= gfMul(coefficient, factor)));
33
+ }
34
+ return result;
35
+ }
36
+ /** Codewords a version holds in all, data and error correction. */
37
+ function rawCodewords(version) {
38
+ let modules = (16 * version + 128) * version + 64;
39
+ if (version >= 2) {
40
+ const align = Math.floor(version / 7) + 2;
41
+ modules -= (25 * align - 10) * align - 55;
42
+ if (version >= 7)
43
+ modules -= 36;
44
+ }
45
+ return modules >> 3;
46
+ }
47
+ function alignmentCentres(version, size) {
48
+ if (version === 1)
49
+ return [];
50
+ const count = Math.floor(version / 7) + 2;
51
+ const step = Math.ceil((version * 4 + 4) / (count * 2 - 2)) * 2;
52
+ const centres = [6];
53
+ for (let at = size - 7; centres.length < count; at -= step)
54
+ centres.splice(1, 0, at);
55
+ return centres;
56
+ }
57
+ const MASKS = [
58
+ (x, y) => (x + y) % 2 === 0,
59
+ (_, y) => y % 2 === 0,
60
+ x => x % 3 === 0,
61
+ (x, y) => (x + y) % 3 === 0,
62
+ (x, y) => (Math.floor(x / 3) + Math.floor(y / 2)) % 2 === 0,
63
+ (x, y) => ((x * y) % 2) + ((x * y) % 3) === 0,
64
+ (x, y) => (((x * y) % 2) + ((x * y) % 3)) % 2 === 0,
65
+ (x, y) => (((x + y) % 2) + ((x * y) % 3)) % 2 === 0,
66
+ ];
67
+ /** The penalty score of the standard: long runs, 2×2 blocks, finder look-alikes, and dark balance. */
68
+ function penalty(modules) {
69
+ const size = modules.length;
70
+ let score = 0;
71
+ let dark = 0;
72
+ const lines = [];
73
+ for (let i = 0; i < size; i++) {
74
+ let row = "";
75
+ let column = "";
76
+ for (let j = 0; j < size; j++) {
77
+ row += modules[i][j] ? 1 : 0;
78
+ column += modules[j][i] ? 1 : 0;
79
+ if (modules[i][j])
80
+ dark++;
81
+ if (i && j && modules[i][j] === modules[i - 1][j] && modules[i][j] === modules[i][j - 1] && modules[i][j] === modules[i - 1][j - 1])
82
+ score += 3;
83
+ }
84
+ lines.push(row, column);
85
+ }
86
+ for (const line of lines) {
87
+ for (const run of line.match(/0{5,}|1{5,}/g) ?? [])
88
+ score += run.length - 2;
89
+ for (let at = 0; at <= size - 11; at++) {
90
+ const window = line.substr(at, 11);
91
+ if (window === "10111010000" || window === "00001011101")
92
+ score += 40;
93
+ }
94
+ }
95
+ return score + Math.floor(Math.abs((dark * 20) / (size * size) - 10)) * 10;
96
+ }
97
+ /** The modules of the QR code of `text` (`true` is dark), `[row][column]`, or `null` when it does not fit. */
98
+ export function qrMatrix(text) {
99
+ const bytes = Array.from(new TextEncoder().encode(text));
100
+ let version = 0;
101
+ let dataLength = 0;
102
+ for (let v = 1; v <= BLOCKS.length && !version; v++) {
103
+ const [ecLength, blocks] = BLOCKS[v - 1];
104
+ dataLength = rawCodewords(v) - ecLength * blocks;
105
+ if (4 + (v < 10 ? 8 : 16) + bytes.length * 8 <= dataLength * 8)
106
+ version = v;
107
+ }
108
+ if (!version)
109
+ return null;
110
+ const [ecLength, blockCount] = BLOCKS[version - 1];
111
+ const size = version * 4 + 17;
112
+ /* ---- data codewords: mode, length, bytes, terminator, padding ---- */
113
+ const bits = [];
114
+ const push = (value, length) => {
115
+ for (let i = length - 1; i >= 0; i--)
116
+ bits.push((value >>> i) & 1);
117
+ };
118
+ push(4, 4);
119
+ push(bytes.length, version < 10 ? 8 : 16);
120
+ for (const byte of bytes)
121
+ push(byte, 8);
122
+ push(0, Math.min(4, dataLength * 8 - bits.length));
123
+ push(0, (8 - (bits.length % 8)) % 8);
124
+ for (let pad = 0xec; bits.length < dataLength * 8; pad ^= 0xec ^ 0x11)
125
+ push(pad, 8);
126
+ const data = [];
127
+ for (let i = 0; i < bits.length; i += 8)
128
+ data.push(bits.slice(i, i + 8).reduce((byte, bit) => (byte << 1) | bit, 0));
129
+ /* ---- blocks with error correction, interleaved ---- */
130
+ const shortBlocks = blockCount - (rawCodewords(version) % blockCount);
131
+ const shortLength = Math.floor(rawCodewords(version) / blockCount) - ecLength;
132
+ const blocks = [];
133
+ for (let i = 0, at = 0; i < blockCount; i++) {
134
+ const part = data.slice(at, (at += shortLength + (i < shortBlocks ? 0 : 1)));
135
+ blocks.push({ data: part, ec: errorCorrection(part, ecLength) });
136
+ }
137
+ const codewords = [];
138
+ for (let i = 0; i <= shortLength; i++)
139
+ for (const block of blocks)
140
+ if (i < block.data.length)
141
+ codewords.push(block.data[i]);
142
+ for (let i = 0; i < ecLength; i++)
143
+ for (const block of blocks)
144
+ codewords.push(block.ec[i]);
145
+ /* ---- function patterns ---- */
146
+ const modules = Array.from({ length: size }, () => new Array(size).fill(false));
147
+ const reserved = Array.from({ length: size }, () => new Array(size).fill(false));
148
+ const set = (x, y, dark) => {
149
+ if (x < 0 || y < 0 || x >= size || y >= size)
150
+ return;
151
+ modules[y][x] = dark;
152
+ reserved[y][x] = true;
153
+ };
154
+ for (let i = 0; i < size; i++) {
155
+ set(6, i, i % 2 === 0);
156
+ set(i, 6, i % 2 === 0);
157
+ }
158
+ for (const [cx, cy] of [[3, 3], [size - 4, 3], [3, size - 4]]) {
159
+ for (let dy = -4; dy <= 4; dy++) {
160
+ for (let dx = -4; dx <= 4; dx++) {
161
+ const ring = Math.max(Math.abs(dx), Math.abs(dy));
162
+ set(cx + dx, cy + dy, ring !== 2 && ring !== 4);
163
+ }
164
+ }
165
+ }
166
+ const centres = alignmentCentres(version, size);
167
+ centres.forEach((cy, i) => centres.forEach((cx, j) => {
168
+ // The three corners with a finder pattern have no alignment pattern.
169
+ if ((i === 0 && j === 0) || (i === 0 && j === centres.length - 1) || (i === centres.length - 1 && j === 0))
170
+ return;
171
+ for (let dy = -2; dy <= 2; dy++)
172
+ for (let dx = -2; dx <= 2; dx++)
173
+ set(cx + dx, cy + dy, Math.max(Math.abs(dx), Math.abs(dy)) !== 1);
174
+ }));
175
+ const format = (mask) => {
176
+ // Level M is 00, so the five data bits are the mask alone.
177
+ let remainder = mask;
178
+ for (let i = 0; i < 10; i++)
179
+ remainder = (remainder << 1) ^ ((remainder >>> 9) * 0x537);
180
+ const word = ((mask << 10) | remainder) ^ 0x5412;
181
+ const bit = (i) => ((word >>> i) & 1) === 1;
182
+ for (let i = 0; i <= 5; i++)
183
+ set(8, i, bit(i));
184
+ set(8, 7, bit(6));
185
+ set(8, 8, bit(7));
186
+ set(7, 8, bit(8));
187
+ for (let i = 9; i < 15; i++)
188
+ set(14 - i, 8, bit(i));
189
+ for (let i = 0; i < 8; i++)
190
+ set(size - 1 - i, 8, bit(i));
191
+ for (let i = 8; i < 15; i++)
192
+ set(8, size - 15 + i, bit(i));
193
+ set(8, size - 8, true);
194
+ };
195
+ format(0);
196
+ if (version >= 7) {
197
+ let remainder = version;
198
+ for (let i = 0; i < 12; i++)
199
+ remainder = (remainder << 1) ^ ((remainder >>> 11) * 0x1f25);
200
+ const word = (version << 12) | remainder;
201
+ for (let i = 0; i < 18; i++) {
202
+ const dark = ((word >>> i) & 1) === 1;
203
+ const a = size - 11 + (i % 3);
204
+ const b = Math.floor(i / 3);
205
+ set(a, b, dark);
206
+ set(b, a, dark);
207
+ }
208
+ }
209
+ /* ---- data in the zigzag, then the mask with the lowest penalty ---- */
210
+ let index = 0;
211
+ for (let right = size - 1; right >= 1; right -= 2) {
212
+ if (right === 6)
213
+ right = 5;
214
+ for (let step = 0; step < size; step++) {
215
+ for (let j = 0; j < 2; j++) {
216
+ const x = right - j;
217
+ const y = ((right + 1) & 2) === 0 ? size - 1 - step : step;
218
+ if (reserved[y][x] || index >= codewords.length * 8)
219
+ continue;
220
+ modules[y][x] = ((codewords[index >>> 3] >>> (7 - (index & 7))) & 1) === 1;
221
+ index++;
222
+ }
223
+ }
224
+ }
225
+ const flip = (mask) => {
226
+ for (let y = 0; y < size; y++)
227
+ for (let x = 0; x < size; x++)
228
+ if (!reserved[y][x] && MASKS[mask](x, y))
229
+ modules[y][x] = !modules[y][x];
230
+ };
231
+ let best = 0;
232
+ let lowest = Infinity;
233
+ for (let mask = 0; mask < 8; mask++) {
234
+ flip(mask);
235
+ format(mask);
236
+ const score = penalty(modules);
237
+ if (score < lowest)
238
+ [best, lowest] = [mask, score];
239
+ flip(mask);
240
+ }
241
+ flip(best);
242
+ format(best);
243
+ return modules;
244
+ }
245
+ /** Quiet zone around the code, in modules. */
246
+ const QUIET = 4;
247
+ /** The QR code of `text` as an SVG string, dark on white whatever the theme; `null` when the text is too long. */
248
+ export function qrSvg(text) {
249
+ const modules = qrMatrix(text);
250
+ if (!modules)
251
+ return null;
252
+ const side = modules.length + QUIET * 2;
253
+ let path = "";
254
+ modules.forEach((row, y) => {
255
+ for (let x = 0; x < row.length; x++) {
256
+ if (!row[x])
257
+ continue;
258
+ let run = 1;
259
+ while (row[x + run])
260
+ run++;
261
+ path += `M${x + QUIET} ${y + QUIET}h${run}v1h-${run}z`;
262
+ x += run;
263
+ }
264
+ });
265
+ return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${side} ${side}" shape-rendering="crispEdges"><path fill="#fff" d="M0 0h${side}v${side}H0z"/><path d="${path}"/></svg>`;
266
+ }
@@ -0,0 +1,9 @@
1
+ import type { Aim } from "./tilt.js";
2
+ export type SensorResult = "ok" | "denied" | "unsupported";
3
+ export interface Sensor {
4
+ /** Asks for the sensor. Call it from a tap: iOS shows its permission prompt only then. */
5
+ start(): Promise<SensorResult>;
6
+ stop(): void;
7
+ }
8
+ /** Calls `onAim` with the direction the phone points at, and the time in seconds, for every reading. */
9
+ export declare function createSensor(onAim: (aim: Aim, t: number) => void): Sensor;
@@ -0,0 +1,50 @@
1
+ /** The orientation sensor of the phone: permission (iOS asks, and only on a tap), events, and the screen's turn. */
2
+ import { aimOf } from "./tilt.js";
3
+ /** How long to wait for the first reading before deciding there is no sensor (a desktop browser). */
4
+ const FIRST_READING = 1500;
5
+ function screenAngle() {
6
+ const legacy = window.orientation;
7
+ return screen.orientation?.angle ?? (Number(legacy) || 0);
8
+ }
9
+ /** Calls `onAim` with the direction the phone points at, and the time in seconds, for every reading. */
10
+ export function createSensor(onAim) {
11
+ let abort = null;
12
+ return {
13
+ async start() {
14
+ if (typeof DeviceOrientationEvent === "undefined")
15
+ return "unsupported";
16
+ const gate = DeviceOrientationEvent;
17
+ if (gate.requestPermission) {
18
+ try {
19
+ // Only a refusal ends it here; without a decision, the wait for a first reading below tells.
20
+ if ((await gate.requestPermission()) === "denied")
21
+ return "denied";
22
+ }
23
+ catch {
24
+ return "denied";
25
+ }
26
+ }
27
+ abort?.abort();
28
+ abort = new AbortController();
29
+ const { signal } = abort;
30
+ return new Promise(resolve => {
31
+ const timer = window.setTimeout(() => {
32
+ abort?.abort();
33
+ resolve("unsupported");
34
+ }, FIRST_READING);
35
+ addEventListener("deviceorientation", event => {
36
+ const { alpha, beta, gamma } = event;
37
+ if (alpha === null || beta === null || gamma === null)
38
+ return;
39
+ clearTimeout(timer);
40
+ resolve("ok");
41
+ onAim(aimOf({ alpha, beta, gamma }, screenAngle()), event.timeStamp / 1000);
42
+ }, { signal });
43
+ });
44
+ },
45
+ stop() {
46
+ abort?.abort();
47
+ abort = null;
48
+ },
49
+ };
50
+ }
@@ -0,0 +1,39 @@
1
+ /**
2
+ * The host side of the remote: one session per open deck. It owns the room, answers pairing,
3
+ * runs what the granted phone and the presenter window ask for, and tells them where the deck is.
4
+ */
5
+ import type { Deck } from "../core/index.js";
6
+ import type { RemoteLabels } from "./labels.js";
7
+ import type { QrEncoder } from "./panel.js";
8
+ export interface RemoteOptions {
9
+ /**
10
+ * Base URL of the relay, `wss://…` or `https://…` (see docs/PROTOCOL.md). Without it the remote stays
11
+ * on this computer: the presenter window and the notes link work, a phone cannot control the deck.
12
+ */
13
+ relay?: string;
14
+ /** Draws a QR code as an SVG string. Default: the built-in encoder, for links up to 287 bytes. */
15
+ qr?: QrEncoder;
16
+ /** Block kinds outside the talk, whose minutes are not planned time. Default `["appendix"]`, as `linear.skip` of the deck. */
17
+ skip?: string[];
18
+ labels?: Partial<RemoteLabels>;
19
+ /** Nonce for the injected `<style>`, for a page with a `style-src` policy. */
20
+ nonce?: string;
21
+ }
22
+ export interface RemoteHost {
23
+ /** The room of this session: the name of the relay room and of the BroadcastChannel. */
24
+ readonly room: string;
25
+ /** Whether the panel is showing. */
26
+ readonly open: boolean;
27
+ /** Shows or hides the panel; without an argument, the other state. */
28
+ toggle(open?: boolean): void;
29
+ /** Ends the session for good: control is taken back, the room closes, and the next session gets a new room. */
30
+ end(): void;
31
+ /** Closes the connections and removes the overlays. The room and the token stay in `sessionStorage`, so the next session in this tab continues. */
32
+ destroy(): void;
33
+ }
34
+ /**
35
+ * Starts the remote for an open deck, or returns the session it already has.
36
+ * `root` is the deck element given to `createDeck`. It resolves once the room is known: the room is
37
+ * the hash of the session's key, which takes `crypto.subtle`.
38
+ */
39
+ export declare function remoteHost(deck: Deck, root: HTMLElement, options?: RemoteOptions): Promise<RemoteHost>;