@tileflow/capture 0.1.0-alpha.29 → 0.1.0-alpha.30

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/README.md CHANGED
@@ -106,6 +106,66 @@ The equivalent CLI command is `tileflow setup capture`. These settings control b
106
106
  not the network access required by a map's remote resources. Tileflow never deletes the shared
107
107
  per-user browser cache.
108
108
 
109
+ ## Render a timed shot
110
+
111
+ A [capture shot](https://github.com/tileflow/tileflow-sdk/blob/main/packages/core/docs/capture-shots.md)
112
+ is a committed camera move with keyframes and an optional theme blend. This module assumes a
113
+ `dusk-flyover` shot in `tileflow.config.ts` and writes its WebM video and receipt:
114
+
115
+ <!-- docs:check -->
116
+
117
+ ```ts
118
+ import {mkdir, writeFile} from 'node:fs/promises';
119
+ import {captureTileflowShot, serializeTileflowShotReceipt} from '@tileflow/capture';
120
+
121
+ const shot = await captureTileflowShot({shot: 'dusk-flyover', allowBrowserInstall: true});
122
+ if (!shot.video) throw new Error('The shot produced no video.');
123
+
124
+ await mkdir('.tileflow/captures/shots', {recursive: true});
125
+ await writeFile('.tileflow/captures/shots/dusk-flyover.webm', shot.video);
126
+ await writeFile(
127
+ '.tileflow/captures/shots/dusk-flyover.receipt.json',
128
+ serializeTileflowShotReceipt(shot.receipt),
129
+ );
130
+ console.log(shot.frameCount, shot.codec, shot.framesSha256);
131
+ ```
132
+
133
+ Every frame moves the camera with MapLibre `jumpTo`, moves the blend position, and waits for
134
+ MapLibre `idle` and two animation frames before its screenshot, exactly as a scene does. A slow
135
+ machine or network makes the render take longer; the video keeps its frame rate and shows every
136
+ frame. `timeoutMs` bounds page setup and each frame separately.
137
+
138
+ The pinned browser encodes the captured PNGs with WebCodecs: VP9, or VP8 where VP9 is unavailable.
139
+ A browser that supports neither fails before the first frame with `VIDEO_ENCODER_UNAVAILABLE`.
140
+ Use `format: 'frames'` to receive the PNG frames through `onFrame` instead of a video; frames are
141
+ delivered in order and are not kept in memory:
142
+
143
+ <!-- docs:check -->
144
+
145
+ ```ts
146
+ import {mkdir, writeFile} from 'node:fs/promises';
147
+ import {createTileflowCaptureSession} from '@tileflow/capture';
148
+
149
+ await mkdir('frames', {recursive: true});
150
+ const session = createTileflowCaptureSession({allowBrowserInstall: true});
151
+ try {
152
+ const shot = await session.captureShot('dusk-flyover', {
153
+ format: 'frames',
154
+ onFrame: (frame) =>
155
+ writeFile(`frames/frame-${String(frame.index).padStart(4, '0')}.png`, frame.png),
156
+ });
157
+ console.log(shot.frameCount, shot.framesSha256);
158
+ } finally {
159
+ await session.close();
160
+ }
161
+ ```
162
+
163
+ `captureShots` and `captureAllShots` render several shots from one compiled snapshot. A shot
164
+ receipt records the shot, theme and blend styles, timing, dimensions, renderer, data identity, and
165
+ `frames.sha256`, a digest of every frame's PNG SHA-256. Frames are reproducible under the same
166
+ conditions as scene captures; encoder bytes may differ between runs, so `output.sha256` identifies
167
+ the written video while `frames.sha256` identifies its pixels.
168
+
109
169
  ## Capture the running application
110
170
 
111
171
  Application capture uses the app's normal loopback server; it does not start a server or discover a
@@ -195,7 +255,8 @@ validation, browser startup, resource loading, MapLibre readiness, and screensho
195
255
  Resource diagnostics contain safe classification and origin/status information, not credentials,
196
256
  query strings, response bodies, raw browser stacks, or DOM contents.
197
257
 
198
- Receipts use schema version 4; parsing rejects any other version. Import
258
+ Receipts use schema version 4; parsing rejects any other version. Shot receipts use their own
259
+ schema version 1. Import
199
260
  `@tileflow/capture/receipt` for receipt-only tooling without loading the capture runtime. World selectors resolve once per capture session to
200
261
  an immutable release. Capture retains the complete resolved TileJSON, including its zoom range,
201
262
  bounds, attribution and vector-layer metadata. Above the data's maximum zoom, MapLibre enlarges
@@ -6,6 +6,7 @@ import {
6
6
  serializeCanonicalJson,
7
7
  tileflowCaptureSceneLimits,
8
8
  tileflowCaptureSceneNameSchema,
9
+ tileflowCaptureShotLimits,
9
10
  tileflowPortableIdSchema,
10
11
  tileflowThemeNameSchema
11
12
  } from "@tileflow/core";
@@ -231,6 +232,219 @@ function validateTileflowCaptureReceipt(value) {
231
232
  assertCanonicalReceiptByteLimit(normalized);
232
233
  return normalized;
233
234
  }
235
+ var tileflowShotReceiptSchemaVersion = 1;
236
+ var tileflowShotReceiptLimits = Object.freeze({ maximumBytes: 64 * 1024 });
237
+ function createTileflowShotReceipt(input) {
238
+ return validateTileflowShotReceipt({
239
+ schemaVersion: tileflowShotReceiptSchemaVersion,
240
+ kind: "shot",
241
+ shot: { name: input.shot, map: input.map, theme: input.theme, sha256: input.shotSha256 },
242
+ style: { sha256: input.styleSha256 },
243
+ ...input.blend ? { blend: { themes: input.blend } } : {},
244
+ timing: { fps: input.fps, frameCount: input.frameCount, durationMs: input.durationMs },
245
+ image: {
246
+ cssWidth: input.width,
247
+ cssHeight: input.height,
248
+ physicalWidth: input.width * input.dpr,
249
+ physicalHeight: input.height * input.dpr,
250
+ dpr: input.dpr
251
+ },
252
+ frames: { sha256: input.framesSha256 },
253
+ output: input.output,
254
+ renderer: input.renderer,
255
+ platform: { os: process.platform, architecture: process.arch },
256
+ data: normalizeCaptureDataInput(input.data),
257
+ verification: { data: "rendered", style: "rendered" },
258
+ networkDependent: input.networkDependent
259
+ });
260
+ }
261
+ function serializeTileflowShotReceipt(receipt) {
262
+ const serialized = `${serializeCanonicalJson(validateTileflowShotReceipt(receipt))}
263
+ `;
264
+ if (new TextEncoder().encode(serialized).byteLength > tileflowShotReceiptLimits.maximumBytes) {
265
+ throw invalidReceipt("The shot receipt exceeds the supported byte limit.");
266
+ }
267
+ return serialized;
268
+ }
269
+ function parseTileflowShotReceipt(source) {
270
+ const byteLength = typeof source === "string" ? new TextEncoder().encode(source).byteLength : source.byteLength;
271
+ if (byteLength > tileflowShotReceiptLimits.maximumBytes) {
272
+ throw invalidReceipt("The shot receipt exceeds the supported byte limit.");
273
+ }
274
+ let value;
275
+ let text;
276
+ try {
277
+ text = typeof source === "string" ? source : new TextDecoder("utf-8", { fatal: true }).decode(source);
278
+ value = JSON.parse(text);
279
+ } catch (error) {
280
+ throw invalidReceipt("The shot receipt is not valid UTF-8 JSON.", error);
281
+ }
282
+ const receipt = validateTileflowShotReceipt(value);
283
+ if (text.trim() !== serializeCanonicalJson(value)) {
284
+ throw invalidReceipt("The shot receipt is not canonical JSON.");
285
+ }
286
+ return receipt;
287
+ }
288
+ function validateTileflowShotReceipt(value) {
289
+ assertPlainReceiptData(value, "receipt", {
290
+ allowArrays: true,
291
+ ancestors: /* @__PURE__ */ new WeakSet(),
292
+ nodes: 0
293
+ });
294
+ const receipt = requireRecord(value, "receipt");
295
+ if (receipt.schemaVersion !== tileflowShotReceiptSchemaVersion || receipt.kind !== "shot") {
296
+ throw invalidReceipt("The shot receipt schema version is unsupported.");
297
+ }
298
+ requireExactKeys(receipt, [
299
+ "schemaVersion",
300
+ "kind",
301
+ "shot",
302
+ "style",
303
+ ...receipt.blend === void 0 ? [] : ["blend"],
304
+ "timing",
305
+ "image",
306
+ "frames",
307
+ "output",
308
+ "renderer",
309
+ "platform",
310
+ "data",
311
+ "verification",
312
+ "networkDependent"
313
+ ]);
314
+ const shot = requireRecord(receipt.shot, "shot");
315
+ requireExactKeys(shot, ["name", "map", "theme", "sha256"]);
316
+ const style = requireRecord(receipt.style, "style");
317
+ requireExactKeys(style, ["sha256"]);
318
+ const blend = receipt.blend === void 0 ? void 0 : validateShotBlend(receipt.blend);
319
+ const timing = requireRecord(receipt.timing, "timing");
320
+ requireExactKeys(timing, ["fps", "frameCount", "durationMs"]);
321
+ const fps = timing.fps;
322
+ if (fps !== 24 && fps !== 25 && fps !== 30 && fps !== 50 && fps !== 60) {
323
+ throw invalidReceipt("The shot receipt has an invalid timing.fps.");
324
+ }
325
+ const frameCount = requirePositiveInteger(timing.frameCount, "timing.frameCount");
326
+ const durationMs = requirePositiveInteger(timing.durationMs, "timing.durationMs");
327
+ if (durationMs > tileflowCaptureShotLimits.durationMs.maximum || frameCount > tileflowCaptureShotLimits.maximumFrames || frameCount !== Math.floor(durationMs * fps / 1e3 + 1e-9) + 1) {
328
+ throw invalidReceipt("The shot receipt timing is inconsistent.");
329
+ }
330
+ const image = requireRecord(receipt.image, "image");
331
+ requireExactKeys(image, ["cssWidth", "cssHeight", "physicalWidth", "physicalHeight", "dpr"]);
332
+ const cssWidth = requirePositiveInteger(image.cssWidth, "image.cssWidth");
333
+ const cssHeight = requirePositiveInteger(image.cssHeight, "image.cssHeight");
334
+ const physicalWidth = requirePositiveInteger(image.physicalWidth, "image.physicalWidth");
335
+ const physicalHeight = requirePositiveInteger(image.physicalHeight, "image.physicalHeight");
336
+ if (image.dpr !== 1 && image.dpr !== 2) {
337
+ throw invalidReceipt("The shot receipt has an invalid image DPR.");
338
+ }
339
+ if (cssWidth * image.dpr !== physicalWidth || cssHeight * image.dpr !== physicalHeight) {
340
+ throw invalidReceipt("The shot receipt image dimensions are inconsistent.");
341
+ }
342
+ if (physicalWidth > tileflowCaptureSceneLimits.viewport.maximum || physicalHeight > tileflowCaptureSceneLimits.viewport.maximum || physicalWidth * physicalHeight > tileflowCaptureSceneLimits.maximumPhysicalPixels) {
343
+ throw invalidReceipt("The shot receipt image exceeds the supported pixel limit.");
344
+ }
345
+ const frames = requireRecord(receipt.frames, "frames");
346
+ requireExactKeys(frames, ["sha256"]);
347
+ const framesSha256 = requireHash(frames.sha256, "frames.sha256");
348
+ const output = requireRecord(receipt.output, "output");
349
+ if (output.kind === "webm") {
350
+ requireExactKeys(output, ["kind", "codec", "sha256"]);
351
+ if (output.codec !== "vp8" && output.codec !== "vp9") {
352
+ throw invalidReceipt("The shot receipt has an invalid output.codec.");
353
+ }
354
+ } else if (output.kind === "frames") {
355
+ requireExactKeys(output, ["kind", "sha256"]);
356
+ if (output.sha256 !== framesSha256) {
357
+ throw invalidReceipt("The shot receipt frame output does not match its frames.");
358
+ }
359
+ } else {
360
+ throw invalidReceipt("The shot receipt has an invalid output.kind.");
361
+ }
362
+ const renderer = requireRecord(receipt.renderer, "renderer");
363
+ requireExactKeys(renderer, [
364
+ "tileflow",
365
+ "maplibre",
366
+ "playwright",
367
+ "chromiumRevision",
368
+ "chromiumVersion"
369
+ ]);
370
+ const platform = requireRecord(receipt.platform, "platform");
371
+ requireExactKeys(platform, ["os", "architecture"]);
372
+ const verification = requireRecord(receipt.verification, "verification");
373
+ requireExactKeys(verification, ["data", "style"]);
374
+ if (verification.data !== "rendered" || verification.style !== "rendered") {
375
+ throw invalidReceipt("The shot receipt verification must record rendered style and data.");
376
+ }
377
+ if (typeof receipt.networkDependent !== "boolean") {
378
+ throw invalidReceipt("The shot receipt has an invalid network dependency flag.");
379
+ }
380
+ const normalized = {
381
+ schemaVersion: tileflowShotReceiptSchemaVersion,
382
+ kind: "shot",
383
+ shot: {
384
+ name: requireSceneName(shot.name),
385
+ map: requirePortableIdentifier(shot.map, "shot.map"),
386
+ theme: requireShotTheme(shot.theme, "shot.theme"),
387
+ sha256: requireHash(shot.sha256, "shot.sha256")
388
+ },
389
+ style: { sha256: requireHash(style.sha256, "style.sha256") },
390
+ ...blend ? { blend } : {},
391
+ timing: { fps, frameCount, durationMs },
392
+ image: { cssWidth, cssHeight, physicalWidth, physicalHeight, dpr: image.dpr },
393
+ frames: { sha256: framesSha256 },
394
+ output: output.kind === "webm" ? {
395
+ kind: "webm",
396
+ codec: output.codec,
397
+ sha256: requireHash(output.sha256, "output.sha256")
398
+ } : { kind: "frames", sha256: framesSha256 },
399
+ renderer: {
400
+ tileflow: requireBoundedString(renderer.tileflow, "renderer.tileflow"),
401
+ maplibre: requireBoundedString(renderer.maplibre, "renderer.maplibre"),
402
+ playwright: requireBoundedString(renderer.playwright, "renderer.playwright"),
403
+ chromiumRevision: requireBoundedString(
404
+ renderer.chromiumRevision,
405
+ "renderer.chromiumRevision"
406
+ ),
407
+ chromiumVersion: requireBoundedString(renderer.chromiumVersion, "renderer.chromiumVersion")
408
+ },
409
+ platform: {
410
+ os: requireBoundedString(platform.os, "platform.os"),
411
+ architecture: requireBoundedString(platform.architecture, "platform.architecture")
412
+ },
413
+ data: validateDataIdentity(receipt.data),
414
+ verification: { data: "rendered", style: "rendered" },
415
+ networkDependent: receipt.networkDependent
416
+ };
417
+ const serialized = `${serializeCanonicalJson(normalized)}
418
+ `;
419
+ if (new TextEncoder().encode(serialized).byteLength > tileflowShotReceiptLimits.maximumBytes) {
420
+ throw invalidReceipt("The shot receipt exceeds the supported byte limit.");
421
+ }
422
+ return normalized;
423
+ }
424
+ function requireShotTheme(value, field) {
425
+ const result = tileflowThemeNameSchema.safeParse(value);
426
+ if (!result.success) throw invalidReceipt(`The shot receipt has an invalid ${field}.`);
427
+ return result.data;
428
+ }
429
+ function validateShotBlend(value) {
430
+ const blend = requireRecord(value, "blend");
431
+ requireExactKeys(blend, ["themes"]);
432
+ if (!Array.isArray(blend.themes) || blend.themes.length < tileflowCaptureShotLimits.blendThemes.minimum || blend.themes.length > tileflowCaptureShotLimits.blendThemes.maximum) {
433
+ throw invalidReceipt("The shot receipt has an invalid blend.themes list.");
434
+ }
435
+ const themes = blend.themes.map((entry, index) => {
436
+ const theme = requireRecord(entry, `blend.themes.${index}`);
437
+ requireExactKeys(theme, ["name", "styleSha256"]);
438
+ return {
439
+ name: requireShotTheme(theme.name, `blend.themes.${index}.name`),
440
+ styleSha256: requireHash(theme.styleSha256, `blend.themes.${index}.styleSha256`)
441
+ };
442
+ });
443
+ if (new Set(themes.map((theme) => theme.name)).size !== themes.length) {
444
+ throw invalidReceipt("The shot receipt blends a theme more than once.");
445
+ }
446
+ return { themes };
447
+ }
234
448
  function validateDataIdentity(value) {
235
449
  const data = requireRecord(value, "data");
236
450
  if (data.kind === "tileflow-world") {
@@ -390,12 +604,17 @@ function assertCanonicalReceiptByteLimit(receipt) {
390
604
  }
391
605
  }
392
606
  function assertPlainReceiptData(value, path = "receipt", state = {
607
+ allowArrays: false,
393
608
  ancestors: /* @__PURE__ */ new WeakSet(),
394
609
  nodes: 0
395
610
  }, depth = 0) {
396
611
  if (value === null || value === void 0 || typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
397
612
  return;
398
613
  }
614
+ if (typeof value === "object" && Array.isArray(value) && state.allowArrays) {
615
+ assertPlainReceiptArray(value, path, state, depth);
616
+ return;
617
+ }
399
618
  if (typeof value !== "object" || Array.isArray(value)) {
400
619
  throw invalidReceipt(`The baseline receipt has an executable or non-plain ${path} value.`);
401
620
  }
@@ -434,6 +653,35 @@ function assertPlainReceiptData(value, path = "receipt", state = {
434
653
  state.ancestors.delete(value);
435
654
  }
436
655
  }
656
+ function assertPlainReceiptArray(value, path, state, depth) {
657
+ if (nodeUtilTypes.isProxy(value)) {
658
+ throw invalidReceipt(`The baseline receipt has an executable proxy at ${path}.`);
659
+ }
660
+ if (Object.getPrototypeOf(value) !== Array.prototype) {
661
+ throw invalidReceipt(`The baseline receipt has a non-plain ${path} array.`);
662
+ }
663
+ if (depth > 32 || ++state.nodes > 4096 || value.length > 4096) {
664
+ throw invalidReceipt("The baseline receipt exceeds the supported structural limit.");
665
+ }
666
+ if (state.ancestors.has(value)) {
667
+ throw invalidReceipt("The baseline receipt must not contain cyclic data.");
668
+ }
669
+ if (Reflect.ownKeys(value).length !== value.length + 1) {
670
+ throw invalidReceipt(`The baseline receipt has a sparse or extended ${path} array.`);
671
+ }
672
+ state.ancestors.add(value);
673
+ try {
674
+ for (let index = 0; index < value.length; index += 1) {
675
+ const descriptor = Object.getOwnPropertyDescriptor(value, index);
676
+ if (!descriptor || !("value" in descriptor) || !descriptor.enumerable) {
677
+ throw invalidReceipt("The baseline receipt contains an accessor or non-enumerable field.");
678
+ }
679
+ assertPlainReceiptData(descriptor.value, `${path}.${index}`, state, depth + 1);
680
+ }
681
+ } finally {
682
+ state.ancestors.delete(value);
683
+ }
684
+ }
437
685
  function requireRecord(value, field) {
438
686
  if (!value || typeof value !== "object" || Array.isArray(value)) {
439
687
  throw invalidReceipt(`The baseline receipt has an invalid ${field} object.`);
@@ -544,5 +792,11 @@ export {
544
792
  createTileflowCaptureReceipt,
545
793
  serializeTileflowCaptureReceipt,
546
794
  parseTileflowCaptureReceipt,
547
- validateTileflowCaptureReceipt
795
+ validateTileflowCaptureReceipt,
796
+ tileflowShotReceiptSchemaVersion,
797
+ tileflowShotReceiptLimits,
798
+ createTileflowShotReceipt,
799
+ serializeTileflowShotReceipt,
800
+ parseTileflowShotReceipt,
801
+ validateTileflowShotReceipt
548
802
  };
package/dist/index.d.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  import { Browser } from 'playwright';
2
- import { T as TileflowCaptureRendererIdentity, a as TileflowCaptureReceipt, b as TileflowCaptureDataInput, c as TileflowCaptureWorldIdentity } from './receipt-DhFOATRO.js';
3
- export { C as CreateTileflowCaptureReceiptInput, d as TileflowCaptureDataBindings, e as TileflowCaptureDataCapabilities, f as TileflowCaptureDataIdentity, g as TileflowCaptureDataSemantics, h as TileflowCaptureDataSource, i as TileflowCaptureVectorIdentity, j as TileflowCaptureVerification, k as createTileflowCaptureReceipt, l as createTileflowCaptureRendererIdentity, p as parseTileflowCaptureReceipt, s as serializeTileflowCaptureReceipt, t as tileflowCaptureReceiptLimits, m as tileflowCaptureReceiptSchemaVersion, n as tileflowCaptureRuntime, v as validateTileflowCaptureReceipt } from './receipt-DhFOATRO.js';
4
- import { TileflowCaptureScene, MapLibreStyle, NormalizedTileflowCaptureScene } from '@tileflow/core';
5
- import { TileflowIconResolutionOptions, TileflowBuildArtifacts, TileflowBuildAsset } from '@tileflow/dev/artifacts';
2
+ import { T as TileflowCaptureRendererIdentity, a as TileflowShotReceiptV1, b as TileflowCaptureReceipt, c as TileflowCaptureDataInput, d as TileflowCaptureWorldIdentity } from './receipt-CfZkzyXd.js';
3
+ export { C as CreateTileflowCaptureReceiptInput, e as CreateTileflowShotReceiptInput, f as TileflowCaptureDataBindings, g as TileflowCaptureDataCapabilities, h as TileflowCaptureDataIdentity, i as TileflowCaptureDataSemantics, j as TileflowCaptureDataSource, k as TileflowCaptureVectorIdentity, l as TileflowCaptureVerification, m as createTileflowCaptureReceipt, n as createTileflowCaptureRendererIdentity, o as createTileflowShotReceipt, p as parseTileflowCaptureReceipt, q as parseTileflowShotReceipt, s as serializeTileflowCaptureReceipt, r as serializeTileflowShotReceipt, t as tileflowCaptureReceiptLimits, u as tileflowCaptureReceiptSchemaVersion, v as tileflowCaptureRuntime, w as tileflowShotReceiptLimits, x as tileflowShotReceiptSchemaVersion, y as validateTileflowCaptureReceipt, z as validateTileflowShotReceipt } from './receipt-CfZkzyXd.js';
4
+ import { NormalizedTileflowCaptureShot, TileflowCaptureScene, MapLibreStyle, NormalizedTileflowCaptureScene } from '@tileflow/core';
5
+ import { TileflowBuildAsset, TileflowIconResolutionOptions, TileflowBuildArtifacts } from '@tileflow/dev/artifacts';
6
6
 
7
7
  type TileflowBrowserInstallProgress = 'installing' | 'installed';
8
8
  type TileflowBrowserLaunchOptions = {
@@ -24,6 +24,49 @@ declare class TileflowCaptureBrowserManager {
24
24
  close(): Promise<void>;
25
25
  }
26
26
 
27
+ declare const tileflowSyntheticAssetOrigin = "https://tileflow.local.invalid";
28
+ type TileflowCaptureAsset = TileflowBuildAsset & {
29
+ url?: string;
30
+ };
31
+
32
+ type TileflowShotVideoCodec = 'vp8' | 'vp9';
33
+
34
+ type TileflowShotFormat = 'frames' | 'webm';
35
+ /** One rendered frame, delivered in order while a shot renders. */
36
+ type TileflowShotFrame = {
37
+ shot: string;
38
+ index: number;
39
+ png: Uint8Array;
40
+ sha256: string;
41
+ };
42
+ type TileflowShotCapture = {
43
+ shot: string;
44
+ map: string;
45
+ theme: string;
46
+ format: TileflowShotFormat;
47
+ /** WebM bytes for the `webm` format. Frames reach `onFrame` and are not retained. */
48
+ video?: Uint8Array;
49
+ codec?: TileflowShotVideoCodec;
50
+ /** SHA-256 of the video, or of the frame set (`framesSha256`) for the `frames` format. */
51
+ sha256: string;
52
+ framesSha256: string;
53
+ frameSha256s: string[];
54
+ shotSha256: string;
55
+ styleSha256: string;
56
+ fps: NormalizedTileflowCaptureShot['fps'];
57
+ frameCount: number;
58
+ durationMs: number;
59
+ width: number;
60
+ height: number;
61
+ dpr: 1 | 2;
62
+ networkDependent: boolean;
63
+ renderer: TileflowCaptureRendererIdentity;
64
+ receipt: TileflowShotReceiptV1;
65
+ warnings: string[];
66
+ };
67
+ /** SHA-256 of each frame's PNG SHA-256 in lowercase hex, one per line, in frame order. */
68
+ declare function tileflowShotFramesSha256(frameSha256s: readonly string[]): Promise<string>;
69
+
27
70
  declare const tileflowCaptureResultSchemaVersion: 1;
28
71
  type TileflowCaptureOptions = {
29
72
  allowBrowserInstall?: boolean;
@@ -68,18 +111,39 @@ type TileflowCaptureResult = {
68
111
  schemaVersion: 1;
69
112
  captures: TileflowCapture[];
70
113
  };
114
+ type TileflowShotCaptureOptions = {
115
+ /** `webm` (the default) encodes a VP9 or VP8 video; `frames` delivers only PNG frames. */
116
+ format?: TileflowShotFormat;
117
+ /** Receives each frame's PNG in order while the shot renders. */
118
+ onFrame?: (frame: TileflowShotFrame) => Promise<void> | void;
119
+ signal?: AbortSignal;
120
+ };
121
+ type TileflowShotCaptureResult = {
122
+ schemaVersion: 1;
123
+ shots: TileflowShotCapture[];
124
+ };
125
+ type CaptureTileflowShotOptions = CreateTileflowCaptureSessionOptions & Omit<TileflowShotCaptureOptions, 'signal'> & {
126
+ shot: string;
127
+ };
71
128
  type TileflowCaptureSession = {
72
129
  capture(scenes: string[], signal?: AbortSignal): Promise<TileflowCaptureResult>;
73
130
  captureAll(signal?: AbortSignal): Promise<TileflowCaptureResult>;
74
131
  captureDefinitions(scenes: Record<string, TileflowCaptureScene>, signal?: AbortSignal): Promise<TileflowCaptureResult>;
75
132
  captureArtifacts(artifacts: TileflowBuildArtifacts, requestedScenes: string[], signal?: AbortSignal): Promise<TileflowCaptureResult>;
133
+ /** Renders one committed shot. */
134
+ captureShot(shot: string, options?: TileflowShotCaptureOptions): Promise<TileflowShotCapture>;
135
+ /** Renders committed shots in name order from one compiled artifact snapshot. */
136
+ captureShots(shots: string[], options?: TileflowShotCaptureOptions): Promise<TileflowShotCaptureResult>;
137
+ captureAllShots(options?: TileflowShotCaptureOptions): Promise<TileflowShotCaptureResult>;
76
138
  close(): Promise<void>;
77
139
  };
78
140
  declare function captureTileflowScenes(options: TileflowCaptureOptions): Promise<TileflowCaptureResult>;
141
+ /** Renders one committed shot with a temporary session. */
142
+ declare function captureTileflowShot(options: CaptureTileflowShotOptions): Promise<TileflowShotCapture>;
79
143
  declare function createTileflowCaptureSession(options?: CreateTileflowCaptureSessionOptions): TileflowCaptureSession;
80
144
 
81
- type TileflowCaptureErrorCode = 'ABORTED' | 'APPLICATION_ORIGIN_REQUIRED' | 'APPLICATION_ORIGIN_INVALID' | 'APPLICATION_NAVIGATION_FAILED' | 'APPLICATION_TARGET_NOT_FOUND' | 'APPLICATION_TARGET_AMBIGUOUS' | 'APPLICATION_ERROR' | 'BASELINE_INVALID' | 'BROWSER_INSTALL_FAILED' | 'BROWSER_MISSING' | 'BROWSER_START_FAILED' | 'CAPTURE_TIMEOUT' | 'INVALID_PNG' | 'MAP_LOAD_FAILED' | 'RENDER_FAILED' | 'RESOURCE_FAILED' | 'SCENE_NOT_FOUND' | 'SCREENSHOT_FAILED' | 'STYLE_INVALID' | 'SYNTHETIC_ASSET_NOT_FOUND' | 'WORLD_RESOLUTION_FAILED';
82
- type TileflowCapturePhase = 'style-validation' | 'browser-start' | 'resource-load' | 'map-load' | 'map-idle' | 'screenshot';
145
+ type TileflowCaptureErrorCode = 'ABORTED' | 'APPLICATION_ORIGIN_REQUIRED' | 'APPLICATION_ORIGIN_INVALID' | 'APPLICATION_NAVIGATION_FAILED' | 'APPLICATION_TARGET_NOT_FOUND' | 'APPLICATION_TARGET_AMBIGUOUS' | 'APPLICATION_ERROR' | 'BASELINE_INVALID' | 'BROWSER_INSTALL_FAILED' | 'BROWSER_MISSING' | 'BROWSER_START_FAILED' | 'CAPTURE_TIMEOUT' | 'INVALID_PNG' | 'MAP_LOAD_FAILED' | 'RENDER_FAILED' | 'RESOURCE_FAILED' | 'SCENE_NOT_FOUND' | 'SCREENSHOT_FAILED' | 'SHOT_NOT_FOUND' | 'STYLE_INVALID' | 'SYNTHETIC_ASSET_NOT_FOUND' | 'VIDEO_ENCODE_FAILED' | 'VIDEO_ENCODER_UNAVAILABLE' | 'WORLD_RESOLUTION_FAILED';
146
+ type TileflowCapturePhase = 'style-validation' | 'browser-start' | 'resource-load' | 'map-load' | 'theme-blend' | 'map-idle' | 'screenshot' | 'video-encode';
83
147
  type TileflowCaptureDiagnostic = {
84
148
  message: string;
85
149
  path: string;
@@ -106,11 +170,6 @@ declare class TileflowCaptureError extends Error {
106
170
  }
107
171
  declare function captureErrorMessage(error: unknown, fallback: string): string;
108
172
 
109
- declare const tileflowSyntheticAssetOrigin = "https://tileflow.local.invalid";
110
- type TileflowCaptureAsset = TileflowBuildAsset & {
111
- url?: string;
112
- };
113
-
114
173
  declare const tileflowWorldCurrentTileJsonUrl: "https://api.tileflow.dev/tiles/world/tiles.json";
115
174
  type FetchTileJson = (input: string, init: {
116
175
  cache: 'no-store';
@@ -365,4 +424,4 @@ declare const tileflowVisualReviewLimits: Readonly<{
365
424
  maximumAggregatePngBytes: number;
366
425
  }>;
367
426
 
368
- export { type CreateTileflowCaptureSessionOptions, type PreparedTileflowCaptureStyle, type TileflowBrowserInstallProgress, type TileflowBrowserLaunchOptions, type TileflowCapture, TileflowCaptureBrowserManager, TileflowCaptureDataInput, type TileflowCaptureDiagnostic, TileflowCaptureError, type TileflowCaptureErrorCode, type TileflowCaptureErrorDetails, type TileflowCaptureOptions, type TileflowCapturePhase, TileflowCaptureReceipt, TileflowCaptureRendererIdentity, type TileflowCaptureResolvedWorldV1, type TileflowCaptureResourceDiagnostic, type TileflowCaptureResourceKind, type TileflowCaptureResult, type TileflowCaptureSession, TileflowCaptureWorldIdentity, TileflowCaptureWorldSession, type TileflowVisualAppearanceDelta, type TileflowVisualAppearanceProfile, type TileflowVisualAppearanceStatistics, type TileflowVisualBaseline, type TileflowVisualComparison, type TileflowVisualComparisonDocument, type TileflowVisualComparisonStatus, type TileflowVisualImageIdentity, type TileflowVisualPaletteColor, type TileflowVisualPixelMetric, type TileflowVisualReferenceAnalysis, type TileflowVisualReferenceAnalysisDocument, type TileflowVisualReferenceAnalysisOptions, type TileflowVisualReferenceAppearance, type TileflowVisualRegion, type TileflowVisualReviewAppearance, type TileflowVisualReviewCapture, type TileflowVisualReviewComparison, type TileflowVisualReviewDefinition, type TileflowVisualReviewDocument, TileflowVisualReviewError, type TileflowVisualReviewFrameIdentity, type TileflowVisualReviewOptions, type TileflowVisualReviewSide, type TileflowVisualReviewStatus, type TileflowVisualRuntimeIdentity, type TileflowVisualSceneIdentity, analyzeTileflowCaptureReference, captureErrorMessage, captureTileflowScenes, compareTileflowCaptureToBaseline, compareTileflowCapturesForReview, createTileflowCaptureBrowserEnvironment, createTileflowCaptureSession, createTileflowVisualComparisonDocument, createTileflowVisualReferenceAnalysisDocument, createTileflowVisualReviewDocument, launchTileflowCaptureBrowser, resolveTileflowApplicationUrl, resolveTileflowCaptureWorldTileJson, serializeTileflowVisualComparison, setupTileflowCaptureBrowser, tileflowCaptureResultSchemaVersion, tileflowSyntheticAssetOrigin, tileflowVisualArtifactLimits, tileflowVisualComparisonSchemaVersion, tileflowVisualEdgeThreshold, tileflowVisualPerceptualThreshold, tileflowVisualReviewLimits, tileflowVisualReviewSchemaVersion, tileflowWorldCurrentTileJsonUrl, validateTileflowVisualReferencePng };
427
+ export { type CaptureTileflowShotOptions, type CreateTileflowCaptureSessionOptions, type PreparedTileflowCaptureStyle, type TileflowBrowserInstallProgress, type TileflowBrowserLaunchOptions, type TileflowCapture, TileflowCaptureBrowserManager, TileflowCaptureDataInput, type TileflowCaptureDiagnostic, TileflowCaptureError, type TileflowCaptureErrorCode, type TileflowCaptureErrorDetails, type TileflowCaptureOptions, type TileflowCapturePhase, TileflowCaptureReceipt, TileflowCaptureRendererIdentity, type TileflowCaptureResolvedWorldV1, type TileflowCaptureResourceDiagnostic, type TileflowCaptureResourceKind, type TileflowCaptureResult, type TileflowCaptureSession, TileflowCaptureWorldIdentity, TileflowCaptureWorldSession, type TileflowShotCapture, type TileflowShotCaptureOptions, type TileflowShotCaptureResult, type TileflowShotFormat, type TileflowShotFrame, TileflowShotReceiptV1, type TileflowShotVideoCodec, type TileflowVisualAppearanceDelta, type TileflowVisualAppearanceProfile, type TileflowVisualAppearanceStatistics, type TileflowVisualBaseline, type TileflowVisualComparison, type TileflowVisualComparisonDocument, type TileflowVisualComparisonStatus, type TileflowVisualImageIdentity, type TileflowVisualPaletteColor, type TileflowVisualPixelMetric, type TileflowVisualReferenceAnalysis, type TileflowVisualReferenceAnalysisDocument, type TileflowVisualReferenceAnalysisOptions, type TileflowVisualReferenceAppearance, type TileflowVisualRegion, type TileflowVisualReviewAppearance, type TileflowVisualReviewCapture, type TileflowVisualReviewComparison, type TileflowVisualReviewDefinition, type TileflowVisualReviewDocument, TileflowVisualReviewError, type TileflowVisualReviewFrameIdentity, type TileflowVisualReviewOptions, type TileflowVisualReviewSide, type TileflowVisualReviewStatus, type TileflowVisualRuntimeIdentity, type TileflowVisualSceneIdentity, analyzeTileflowCaptureReference, captureErrorMessage, captureTileflowScenes, captureTileflowShot, compareTileflowCaptureToBaseline, compareTileflowCapturesForReview, createTileflowCaptureBrowserEnvironment, createTileflowCaptureSession, createTileflowVisualComparisonDocument, createTileflowVisualReferenceAnalysisDocument, createTileflowVisualReviewDocument, launchTileflowCaptureBrowser, resolveTileflowApplicationUrl, resolveTileflowCaptureWorldTileJson, serializeTileflowVisualComparison, setupTileflowCaptureBrowser, tileflowCaptureResultSchemaVersion, tileflowShotFramesSha256, tileflowSyntheticAssetOrigin, tileflowVisualArtifactLimits, tileflowVisualComparisonSchemaVersion, tileflowVisualEdgeThreshold, tileflowVisualPerceptualThreshold, tileflowVisualReviewLimits, tileflowVisualReviewSchemaVersion, tileflowWorldCurrentTileJsonUrl, validateTileflowVisualReferencePng };