@mavware/bug-surveillance 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.
package/src/index.d.ts ADDED
@@ -0,0 +1,685 @@
1
+ // Type declarations for @mavware/bug-surveillance.
2
+ //
3
+ // Hand-written rather than generated, so the shapes that matter, such as a
4
+ // stored night, a closed track and the report payload, are named and precise
5
+ // instead of widened to `any`. `npm run typecheck` in this package compiles
6
+ // these declarations together with a type test that exercises the whole public
7
+ // API, which catches an inconsistent or unusable signature but cannot prove the
8
+ // declarations match the runtime: if you change a signature in src, change it
9
+ // here in the same commit.
10
+ //
11
+ // This is a browser library: it references DOM types, so a consumer's tsconfig
12
+ // needs "lib": ["ES2022", "DOM"].
13
+
14
+ // ---------------------------------------------------------------------------
15
+ // Shared shapes
16
+ // ---------------------------------------------------------------------------
17
+
18
+ /** A frame's pixels. Real `ImageData` satisfies this, and so does a plain object in tests. */
19
+ export interface ImageLike {
20
+ width: number;
21
+ height: number;
22
+ data: Uint8ClampedArray;
23
+ }
24
+
25
+ /** One sample on a track: milliseconds since the night began, then x and y in full-frame pixels. */
26
+ export type TrackPoint = [offsetMs: number, x: number, y: number];
27
+
28
+ /** Which side of the frame a point sits against, or the middle of it. */
29
+ export type Edge = 'left' | 'right' | 'top' | 'bottom';
30
+ export type EdgeOrInterior = Edge | 'interior';
31
+
32
+ /** Where a night stands. A night is `active` until it is ended or discarded. */
33
+ export type NightStatus = 'active' | 'completed' | 'aborted';
34
+
35
+ // ---------------------------------------------------------------------------
36
+ // Camera
37
+ // ---------------------------------------------------------------------------
38
+
39
+ /**
40
+ * Owns the camera stream and two canvases: a full-resolution one for reference
41
+ * frames and crops, and a downscaled one for per-frame processing.
42
+ */
43
+ export declare class Camera {
44
+ constructor(videoElement: HTMLVideoElement, processingWidth?: number);
45
+
46
+ video: HTMLVideoElement;
47
+ processingWidth: number;
48
+ stream: MediaStream | null;
49
+ fullCanvas: HTMLCanvasElement;
50
+ procCanvas: HTMLCanvasElement;
51
+
52
+ /** Set once `start()` has resolved. */
53
+ frameWidth: number;
54
+ frameHeight: number;
55
+ /** Full-frame pixels per processing pixel. */
56
+ scale: number;
57
+
58
+ /** Opens the stream. Rejects if the user refuses the camera prompt. */
59
+ start(): Promise<void>;
60
+ stop(): void;
61
+
62
+ grabProcessedFrame(): ImageData;
63
+ captureReferenceJpeg(): Promise<Blob | null>;
64
+
65
+ /** A small crop as raw base64, with no `data:` prefix, so it can ride inside JSON. */
66
+ captureCropBase64(centerX: number, centerY: number, size?: number): string;
67
+ }
68
+
69
+ // ---------------------------------------------------------------------------
70
+ // Brightness and calibration
71
+ // ---------------------------------------------------------------------------
72
+
73
+ /** Mean luminance at or below which a scene is refused outright. */
74
+ export declare const BRIGHTNESS_BLOCK: number;
75
+ /** Mean luminance below which a scene is watched, but warned about. */
76
+ export declare const BRIGHTNESS_WARN: number;
77
+
78
+ export interface Calibration {
79
+ meanLuminance: number;
80
+ tooDark: boolean;
81
+ dim: boolean;
82
+ /** The motion threshold this camera needs in this light. */
83
+ diffThreshold: number;
84
+ }
85
+
86
+ /** Watches an empty room for a few seconds to measure its light and its noise floor. */
87
+ export declare function calibrate(
88
+ camera: Pick<Camera, 'grabProcessedFrame'>,
89
+ durationMs?: number,
90
+ sampleIntervalMs?: number,
91
+ ): Promise<Calibration>;
92
+
93
+ export declare function toGrayscale(imageData: ImageLike): Float32Array;
94
+
95
+ // ---------------------------------------------------------------------------
96
+ // Detection
97
+ // ---------------------------------------------------------------------------
98
+
99
+ export interface DetectorParams {
100
+ processFps: number;
101
+ procWidth: number;
102
+ /** How fast the background model absorbs what it sees. */
103
+ bgAlpha: number;
104
+ diffThreshold: number;
105
+ minArea: number;
106
+ maxArea: number;
107
+ /** Moving pixels a whole frame may hold before it is dropped as a person or a pet. */
108
+ maxChangedArea: number;
109
+ maxAspectRatio: number;
110
+ darkerThanBackground: boolean;
111
+ darkMargin: number;
112
+ }
113
+
114
+ export declare const DEFAULT_PARAMS: DetectorParams;
115
+
116
+ export interface DetectedBlob {
117
+ cx: number;
118
+ cy: number;
119
+ area: number;
120
+ box: { x: number; y: number; width: number; height: number };
121
+ }
122
+
123
+ /**
124
+ * Frame-differencing blob detector: keeps a running-average background,
125
+ * thresholds the difference, and extracts roach-sized connected components.
126
+ */
127
+ export declare class Detector {
128
+ constructor(params?: Partial<DetectorParams>);
129
+
130
+ params: DetectorParams;
131
+ background: Float32Array | null;
132
+
133
+ /**
134
+ * True while the last frame was dropped for holding something far larger
135
+ * than a bug. Read it to tell the user why nothing is being reported.
136
+ */
137
+ largeMotion: boolean;
138
+
139
+ /** Blobs in processing-canvas coordinates. The first frame only seeds the background. */
140
+ detect(imageData: ImageLike): DetectedBlob[];
141
+ reset(): void;
142
+ }
143
+
144
+ // ---------------------------------------------------------------------------
145
+ // Tracking
146
+ // ---------------------------------------------------------------------------
147
+
148
+ export interface TrackerParams {
149
+ /** Processing pixels a blob may move between frames and still be the same bug. */
150
+ maxMatchDistance: number;
151
+ confirmAfterHits: number;
152
+ closeAfterMisses: number;
153
+ minPoints: number;
154
+ /** Discards jitter that never went anywhere. */
155
+ minDisplacement: number;
156
+ maxPointsPerTrack: number;
157
+ maxTrackDurationMs: number;
158
+ }
159
+
160
+ export declare const TRACKER_DEFAULTS: TrackerParams;
161
+
162
+ /** A track that has ended, handed to the sink. Field names match the wire format. */
163
+ export interface ClosedTrack {
164
+ client_track_id: string;
165
+ start_offset_ms: number;
166
+ end_offset_ms: number;
167
+ points: TrackPoint[];
168
+ start_crop: string | null;
169
+ end_crop: string | null;
170
+ }
171
+
172
+ /** A track still being followed. */
173
+ export interface ActiveTrack {
174
+ id: string;
175
+ points: TrackPoint[];
176
+ lastX: number;
177
+ lastY: number;
178
+ hits: number;
179
+ misses: number;
180
+ startCrop: string | null;
181
+ endCrop: string | null;
182
+ }
183
+
184
+ /**
185
+ * Associates per-frame blobs into tracks by nearest neighbour, and hands closed
186
+ * tracks, scaled to full-frame pixels, to `onTrackClosed`.
187
+ */
188
+ export declare class Tracker {
189
+ constructor(options: {
190
+ /** Full-frame pixels per processing pixel, from `Camera.scale`. */
191
+ scale: number;
192
+ sessionStartTime: number;
193
+ captureCrop: (x: number, y: number) => string | null;
194
+ onTrackClosed: (track: ClosedTrack) => void;
195
+ params?: Partial<TrackerParams>;
196
+ });
197
+
198
+ params: TrackerParams;
199
+ candidates: ActiveTrack[];
200
+ active: ActiveTrack[];
201
+ closedCount: number;
202
+
203
+ update(blobs: DetectedBlob[], now?: number): void;
204
+
205
+ /** Closes everything still open, for the end of a night. */
206
+ flush(): void;
207
+ }
208
+
209
+ // ---------------------------------------------------------------------------
210
+ // Wake lock
211
+ // ---------------------------------------------------------------------------
212
+
213
+ /**
214
+ * Holds the screen awake for the night, re-acquiring the lock whenever the page
215
+ * becomes visible again. `onUnsupported` fires when the browser refuses.
216
+ */
217
+ export declare class WakeLock {
218
+ constructor(onUnsupported?: () => void);
219
+
220
+ acquire(): Promise<void>;
221
+ release(): Promise<void>;
222
+ }
223
+
224
+ // ---------------------------------------------------------------------------
225
+ // What a capture page says and shows
226
+ // ---------------------------------------------------------------------------
227
+
228
+ export declare const TOO_DARK_MESSAGE: string;
229
+ export declare const DIM_MESSAGE: string;
230
+ export declare const PREFLIGHT_MESSAGE: string;
231
+ export declare const CAMERA_CHECK_MESSAGE: string;
232
+ export declare const WATCHING_MESSAGE: string;
233
+ export declare const LARGE_MOTION_MESSAGE: string;
234
+
235
+ /** Seconds the room is left alone before anything is measured. */
236
+ export declare const LEAVE_ROOM_SECONDS: number;
237
+
238
+ export declare function countdownMessage(secondsLeft: number): string;
239
+
240
+ export declare function cameraCheckLabel(
241
+ previewing: boolean,
242
+ messages?: { stop?: string; check?: string },
243
+ ): string;
244
+
245
+ /** Names the actual OS setting to change, chosen from the user agent. */
246
+ export declare function wakeLockMessage(userAgent?: string): string;
247
+
248
+ /** Whether a calibrated scene can be watched, and what to say about it. */
249
+ export declare function calibrationOutcome(
250
+ calibration: Pick<Calibration, 'tooDark' | 'dim'>,
251
+ messages?: { tooDark?: string; dim?: string },
252
+ ): { blocked: boolean; banner: string | null };
253
+
254
+ /** The status line while a night runs, given the detector's `largeMotion`. */
255
+ export declare function watchingState(
256
+ largeMotion: boolean,
257
+ messages?: { watching?: string; largeMotion?: string },
258
+ ): string;
259
+
260
+ export interface OverlayBox {
261
+ x: number;
262
+ y: number;
263
+ width: number;
264
+ height: number;
265
+ }
266
+
267
+ /** Detection boxes scaled from the processing canvas up to an on-screen overlay. */
268
+ export declare function overlayBoxes(
269
+ blobs: DetectedBlob[],
270
+ canvas: { canvasWidth: number; canvasHeight: number; procWidth: number; procHeight: number },
271
+ ): OverlayBox[];
272
+
273
+ /** Milliseconds as HH:MM:SS, for a clock that may run all night. */
274
+ export declare function formatClock(ms: number): string;
275
+
276
+ // ---------------------------------------------------------------------------
277
+ // Analytics
278
+ // ---------------------------------------------------------------------------
279
+
280
+ export declare const EDGE_BINS: number;
281
+
282
+ /** A stretch of one frame edge that several bugs crossed. */
283
+ export interface Zone {
284
+ edge: Edge;
285
+ from: number;
286
+ to: number;
287
+ center: [x: number, y: number];
288
+ count: number;
289
+ }
290
+
291
+ export declare function classifyEdge(
292
+ point: [x: number, y: number],
293
+ width: number,
294
+ height: number,
295
+ ): EdgeOrInterior;
296
+
297
+ export declare function clusterEdgePoints(
298
+ points: Array<[x: number, y: number]>,
299
+ width: number,
300
+ height: number,
301
+ ): Zone[];
302
+
303
+ /** Runs of adjacent non-empty bins, as `[fromBin, toBin, count]`. */
304
+ export declare function mergeAdjacentBins(bins: number[]): Array<[number, number, number]>;
305
+
306
+ export declare function trackEdges(
307
+ points: TrackPoint[],
308
+ width: number,
309
+ height: number,
310
+ ): { entryEdge: EdgeOrInterior | null; exitEdge: EdgeOrInterior | null };
311
+
312
+ /** The summary stored on a night. Keys are snake_case to match the server's. */
313
+ export interface NightAnalytics {
314
+ track_count: number;
315
+ total_points: number;
316
+ duration_ms: number;
317
+ entry_zones: Zone[];
318
+ exit_zones: Zone[];
319
+ }
320
+
321
+ /** Anything with points and a dismissal can be summarised. Dismissed tracks never count. */
322
+ export interface AnalysableTrack {
323
+ points: TrackPoint[];
324
+ pointCount?: number;
325
+ dismissedAt?: number | null;
326
+ }
327
+
328
+ export declare function computeNightAnalytics(input: {
329
+ tracks: AnalysableTrack[];
330
+ startedAt?: number | null;
331
+ endedAt?: number | null;
332
+ frameWidth?: number;
333
+ frameHeight?: number;
334
+ }): NightAnalytics;
335
+
336
+ // ---------------------------------------------------------------------------
337
+ // Keeping a night in the browser
338
+ // ---------------------------------------------------------------------------
339
+
340
+ export interface StoredNight {
341
+ id: string;
342
+ name: string;
343
+ status: NightStatus;
344
+ startedAt: number;
345
+ endedAt: number | null;
346
+ lastHeartbeatAt: number;
347
+ frameWidth: number;
348
+ frameHeight: number;
349
+ settings: Record<string, unknown>;
350
+ analytics: NightAnalytics | null;
351
+ createdAt: number;
352
+ /** Set once the night has been copied somewhere durable, such as an account. */
353
+ claimedSessionId: number | string | null;
354
+ claimedAt: number | null;
355
+ }
356
+
357
+ export interface StoredTrack {
358
+ nightId: string;
359
+ clientTrackId: string;
360
+ startOffsetMs: number;
361
+ endOffsetMs: number;
362
+ pointCount: number;
363
+ points: TrackPoint[];
364
+ entryEdge: EdgeOrInterior | null;
365
+ exitEdge: EdgeOrInterior | null;
366
+ /** Raw base64 JPEG, no `data:` prefix. */
367
+ startCrop: string | null;
368
+ endCrop: string | null;
369
+ dismissedAt: number | null;
370
+ }
371
+
372
+ export interface StoredBlob {
373
+ key: string;
374
+ nightId: string;
375
+ bytes: ArrayBuffer;
376
+ type: string;
377
+ }
378
+
379
+ /** What both stores answer. Every method resolves; unknown keys give null. */
380
+ export interface NightStore {
381
+ /** True when nothing written here survives the tab, so the page can warn. */
382
+ volatile: boolean;
383
+
384
+ putNight(night: StoredNight): Promise<void>;
385
+ getNight(id: string): Promise<StoredNight | null>;
386
+ patchNight(id: string, patch: Partial<StoredNight>): Promise<StoredNight | null>;
387
+ /** Newest first. */
388
+ listNights(): Promise<StoredNight[]>;
389
+ /** Removes the night with its tracks and its blobs. */
390
+ deleteNight(id: string): Promise<void>;
391
+
392
+ putTrack(track: StoredTrack): Promise<void>;
393
+ listTracks(nightId: string): Promise<StoredTrack[]>;
394
+ patchTrack(
395
+ nightId: string,
396
+ clientTrackId: string,
397
+ patch: Partial<StoredTrack>,
398
+ ): Promise<StoredTrack | null>;
399
+
400
+ putBlob(blob: StoredBlob): Promise<void>;
401
+ getBlob(key: string): Promise<StoredBlob | null>;
402
+ }
403
+
404
+ export declare const STORE_NAME: string;
405
+ export declare const STORE_VERSION: number;
406
+
407
+ /** Builds the schema. Exported so a spec can run it against a fake factory. */
408
+ export declare function upgradeSchema(db: IDBDatabase): void;
409
+
410
+ /**
411
+ * Opens the IndexedDB store, or falls back to an in-memory one when the browser
412
+ * has no IndexedDB or refuses to open it. The fallback is marked volatile.
413
+ */
414
+ export declare function openNightStore(options?: {
415
+ indexedDB?: IDBFactory | null;
416
+ }): Promise<NightStore>;
417
+
418
+ export declare class IndexedDbNightStore implements NightStore {
419
+ constructor(db: IDBDatabase);
420
+ volatile: boolean;
421
+ putNight(night: StoredNight): Promise<void>;
422
+ getNight(id: string): Promise<StoredNight | null>;
423
+ patchNight(id: string, patch: Partial<StoredNight>): Promise<StoredNight | null>;
424
+ listNights(): Promise<StoredNight[]>;
425
+ deleteNight(id: string): Promise<void>;
426
+ putTrack(track: StoredTrack): Promise<void>;
427
+ listTracks(nightId: string): Promise<StoredTrack[]>;
428
+ patchTrack(nightId: string, clientTrackId: string, patch: Partial<StoredTrack>): Promise<StoredTrack | null>;
429
+ putBlob(blob: StoredBlob): Promise<void>;
430
+ getBlob(key: string): Promise<StoredBlob | null>;
431
+ }
432
+
433
+ /** The runtime fallback, and the double to test against. */
434
+ export declare class InMemoryNightStore implements NightStore {
435
+ volatile: boolean;
436
+ putNight(night: StoredNight): Promise<void>;
437
+ getNight(id: string): Promise<StoredNight | null>;
438
+ patchNight(id: string, patch: Partial<StoredNight>): Promise<StoredNight | null>;
439
+ listNights(): Promise<StoredNight[]>;
440
+ deleteNight(id: string): Promise<void>;
441
+ putTrack(track: StoredTrack): Promise<void>;
442
+ listTracks(nightId: string): Promise<StoredTrack[]>;
443
+ patchTrack(nightId: string, clientTrackId: string, patch: Partial<StoredTrack>): Promise<StoredTrack | null>;
444
+ putBlob(blob: StoredBlob): Promise<void>;
445
+ getBlob(key: string): Promise<StoredBlob | null>;
446
+ }
447
+
448
+ // ---------------------------------------------------------------------------
449
+ // Sinks
450
+ // ---------------------------------------------------------------------------
451
+
452
+ export interface NightSinkStatus {
453
+ queueDepth?: number;
454
+ lastError?: string;
455
+ /** Set by a sink that uploads, when the session behind it expired. */
456
+ authLost?: boolean;
457
+ }
458
+
459
+ export interface NightSinkResult {
460
+ ok: boolean;
461
+ status: number;
462
+ reportUrl: string | null;
463
+ }
464
+
465
+ /**
466
+ * Where a night goes. Implement this to upload nights instead of keeping them
467
+ * locally; a capture page calls nothing else, so the two are interchangeable.
468
+ */
469
+ export interface NightSink {
470
+ storeReference(input: {
471
+ blob: Blob;
472
+ frameWidth: number;
473
+ frameHeight: number;
474
+ settings: Record<string, unknown>;
475
+ }): Promise<void>;
476
+
477
+ start(): void;
478
+ stop(): void;
479
+ enqueue(track: ClosedTrack): void;
480
+ flush(options?: { keepalive?: boolean }): Promise<void>;
481
+ end(input: { endedAtOffsetMs: number; aborted: boolean }): Promise<NightSinkResult>;
482
+ }
483
+
484
+ /** How often the local sink records that the night is still running. */
485
+ export declare const HEARTBEAT_INTERVAL_MS: number;
486
+
487
+ /** The sink that keeps a night in this browser and never touches the network. */
488
+ export declare class LocalNightSink implements NightSink {
489
+ constructor(options: {
490
+ store: NightStore;
491
+ /** A URL containing `LOCAL_ID_PLACEHOLDER`, swapped for the night's id. */
492
+ reportUrlTemplate: string;
493
+ onStatus?: (status: NightSinkStatus) => void;
494
+ });
495
+
496
+ /** The night being written, once `storeReference` has run. */
497
+ nightId: string | null;
498
+
499
+ storeReference(input: {
500
+ blob: Blob;
501
+ frameWidth: number;
502
+ frameHeight: number;
503
+ settings: Record<string, unknown>;
504
+ }): Promise<void>;
505
+ start(): void;
506
+ stop(): void;
507
+ enqueue(track: ClosedTrack): void;
508
+ flush(options?: { keepalive?: boolean }): Promise<void>;
509
+ end(input: { endedAtOffsetMs: number; aborted: boolean }): Promise<NightSinkResult>;
510
+ }
511
+
512
+ // ---------------------------------------------------------------------------
513
+ // A night's shape, naming and report
514
+ // ---------------------------------------------------------------------------
515
+
516
+ /** A night runs until this hour of the following morning. */
517
+ export declare const NIGHT_BOUNDARY_HOUR: number;
518
+
519
+ /** The uuid a report URL is generated with, for the page to swap out. */
520
+ export declare const LOCAL_ID_PLACEHOLDER: string;
521
+
522
+ export declare const LOCAL_STORAGE_NOTICE: string;
523
+ export declare const VOLATILE_STORE_MESSAGE: string;
524
+ export declare const MISSING_NIGHT_MESSAGE: string;
525
+
526
+ /** The evening a moment belongs to, even past midnight. */
527
+ export declare function nightDateFor(ms: number): Date;
528
+
529
+ /** For example, "Night of Sep 8". */
530
+ export declare function nightName(startedAt: number): string;
531
+
532
+ export declare function buildLocalNight(input: {
533
+ id: string;
534
+ startedAt: number;
535
+ frameWidth: number;
536
+ frameHeight: number;
537
+ settings: Record<string, unknown>;
538
+ }): StoredNight;
539
+
540
+ export declare function referenceBlobKey(nightId: string): string;
541
+
542
+ export declare function reportUrlFor(template: string, nightId: string): string;
543
+
544
+ export declare function localTrackFromClosed(
545
+ track: ClosedTrack,
546
+ nightId: string,
547
+ frameWidth: number,
548
+ frameHeight: number,
549
+ ): StoredTrack;
550
+
551
+ export declare function nightAnalytics(night: StoredNight, tracks: StoredTrack[]): NightAnalytics;
552
+
553
+ export interface ReportTrack {
554
+ id: string;
555
+ startOffsetMs: number;
556
+ endOffsetMs: number;
557
+ points: TrackPoint[];
558
+ entryEdge: EdgeOrInterior | null;
559
+ exitEdge: EdgeOrInterior | null;
560
+ }
561
+
562
+ /** What `Replay` draws. Dismissed tracks are already filtered out. */
563
+ export interface ReportPayload {
564
+ frameWidth: number;
565
+ frameHeight: number;
566
+ analytics: NightAnalytics | null;
567
+ tracks: ReportTrack[];
568
+ }
569
+
570
+ export declare function buildLocalReportPayload(
571
+ night: StoredNight,
572
+ tracks: StoredTrack[],
573
+ ): ReportPayload;
574
+
575
+ /** One row of the sightings table. Dismissed tracks are included, and marked. */
576
+ export interface SightingRow {
577
+ clientTrackId: string;
578
+ time: string;
579
+ durationSeconds: number;
580
+ entered: string;
581
+ exited: string;
582
+ /** A `data:` URL ready for an `img`, or null when no crop was kept. */
583
+ startCropSrc: string | null;
584
+ endCropSrc: string | null;
585
+ dismissed: boolean;
586
+ }
587
+
588
+ export declare function sightingRows(night: StoredNight, tracks: StoredTrack[]): SightingRow[];
589
+
590
+ export declare function statTiles(analytics: NightAnalytics | null | undefined): {
591
+ trackCount: number;
592
+ topEntry: string;
593
+ topExit: string;
594
+ };
595
+
596
+ export declare function reportHeader(night: StoredNight): {
597
+ title: string;
598
+ range: string;
599
+ discarded: boolean;
600
+ };
601
+
602
+ export interface NightRow {
603
+ id: string;
604
+ name: string;
605
+ started: string;
606
+ status: string;
607
+ sightings: number;
608
+ reportUrl: string;
609
+ claimed: boolean;
610
+ }
611
+
612
+ export declare function nightRows(
613
+ nights: StoredNight[],
614
+ options: { reportUrlTemplate: string },
615
+ ): NightRow[];
616
+
617
+ /**
618
+ * A night still active when a page loads is one whose tab died overnight.
619
+ * Returns the patch that closes it at its last heartbeat, or null if it is
620
+ * already finished.
621
+ */
622
+ export declare function finalizeInterruptedNight(
623
+ night: StoredNight,
624
+ tracks: StoredTrack[],
625
+ ): { status: 'completed'; endedAt: number; analytics: NightAnalytics } | null;
626
+
627
+ // ---------------------------------------------------------------------------
628
+ // Replay
629
+ // ---------------------------------------------------------------------------
630
+
631
+ /**
632
+ * Draws trails and the animated replay over the reference photo. Coordinates
633
+ * are full-frame pixels; the canvas is sized to the frame and scaled by CSS.
634
+ */
635
+ export declare class Replay {
636
+ constructor(options: {
637
+ canvas: HTMLCanvasElement;
638
+ data: ReportPayload;
639
+ referenceImage?: HTMLImageElement | null;
640
+ });
641
+
642
+ data: ReportPayload;
643
+ duration: number;
644
+ /** Null while the whole night is shown at once, rather than a moment in it. */
645
+ playheadMs: number | null;
646
+ playing: boolean;
647
+ speed: number;
648
+ showTrails: boolean;
649
+ highlightedTrackId: string | number | null;
650
+ /** Called on every animation frame with progress from zero to one. */
651
+ onFrame?: (fraction: number) => void;
652
+
653
+ draw(): void;
654
+ play(): void;
655
+ pause(): void;
656
+ stopReplay(): void;
657
+ seek(fraction: number): void;
658
+ trackColor(index: number, alpha?: number): string;
659
+ }
660
+
661
+ /**
662
+ * Wires the replay controls inside `root`. It reads these attributes:
663
+ * `data-report="canvas"`, `"play"`, `"speed"`, `"scrub"`, `"clock"`, `"trails"`,
664
+ * and treats a click on any `[data-track-id]` as a request to highlight it.
665
+ */
666
+ export declare function mountReportControls(
667
+ root: Element,
668
+ options: {
669
+ loadData: () => Promise<{
670
+ data: ReportPayload;
671
+ referenceImage: HTMLImageElement | null;
672
+ }>;
673
+ },
674
+ ): {
675
+ /** Rebuilds from a fresh payload, after a track is dismissed or restored. */
676
+ rebuild: () => Promise<void>;
677
+ /** Resolves once the first build has finished. */
678
+ ready: Promise<void>;
679
+ };
680
+
681
+ /** Keeps a uuid a string and a numeric id a number, so `===` matches the payload. */
682
+ export declare function trackIdFrom(raw: string): string | number;
683
+
684
+ /** Resolves to null rather than rejecting when the image cannot be loaded. */
685
+ export declare function loadImage(url: string): Promise<HTMLImageElement | null>;