scenic-prism-playwright 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/LICENSE ADDED
@@ -0,0 +1,133 @@
1
+ # PolyForm Noncommercial License 1.0.0
2
+
3
+ <https://polyformproject.org/licenses/noncommercial/1.0.0>
4
+
5
+ Required Notice: Copyright James Porter (https://scenic-prism.pages.dev/license)
6
+
7
+ ## Acceptance
8
+
9
+ In order to get any license under these terms, you must agree
10
+ to them as both strict obligations and conditions to all
11
+ your licenses.
12
+
13
+ ## Copyright License
14
+
15
+ The licensor grants you a copyright license for the
16
+ software to do everything you might do with the software
17
+ that would otherwise infringe the licensor's copyright
18
+ in it for any permitted purpose. However, you may
19
+ only distribute the software according to [Distribution
20
+ License](#distribution-license) and make changes or new works
21
+ based on the software according to [Changes and New Works
22
+ License](#changes-and-new-works-license).
23
+
24
+ ## Distribution License
25
+
26
+ The licensor grants you an additional copyright license
27
+ to distribute copies of the software. Your license
28
+ to distribute covers distributing the software with
29
+ changes and new works permitted by [Changes and New Works
30
+ License](#changes-and-new-works-license).
31
+
32
+ ## Notices
33
+
34
+ You must ensure that anyone who gets a copy of any part of
35
+ the software from you also gets a copy of these terms or the
36
+ URL for them above, as well as copies of any plain-text lines
37
+ beginning with `Required Notice:` that the licensor provided
38
+ with the software. For example:
39
+
40
+ > Required Notice: Copyright Yoyodyne, Inc. (http://example.com)
41
+
42
+ ## Changes and New Works License
43
+
44
+ The licensor grants you an additional copyright license to
45
+ make changes and new works based on the software for any
46
+ permitted purpose.
47
+
48
+ ## Patent License
49
+
50
+ The licensor grants you a patent license for the software that
51
+ covers patent claims the licensor can license, or becomes able
52
+ to license, that you would infringe by using the software.
53
+
54
+ ## Noncommercial Purposes
55
+
56
+ Any noncommercial purpose is a permitted purpose.
57
+
58
+ ## Personal Uses
59
+
60
+ Personal use for research, experiment, and testing for
61
+ the benefit of public knowledge, personal study, private
62
+ entertainment, hobby projects, amateur pursuits, or religious
63
+ observance, without any anticipated commercial application,
64
+ is use for a permitted purpose.
65
+
66
+ ## Noncommercial Organizations
67
+
68
+ Use by any charitable organization, educational institution,
69
+ public research organization, public safety or health
70
+ organization, environmental protection organization,
71
+ or government institution is use for a permitted purpose
72
+ regardless of the source of funding or obligations resulting
73
+ from the funding.
74
+
75
+ ## Fair Use
76
+
77
+ You may have "fair use" rights for the software under the
78
+ law. These terms do not limit them.
79
+
80
+ ## No Other Rights
81
+
82
+ These terms do not allow you to sublicense or transfer any of
83
+ your licenses to anyone else, or prevent the licensor from
84
+ granting licenses to anyone else. These terms do not imply
85
+ any other licenses.
86
+
87
+ ## Patent Defense
88
+
89
+ If you make any written claim that the software infringes or
90
+ contributes to infringement of any patent, your patent license
91
+ for the software granted under these terms ends immediately. If
92
+ your company makes such a claim, your patent license ends
93
+ immediately for work on behalf of your company.
94
+
95
+ ## Violations
96
+
97
+ The first time you are notified in writing that you have
98
+ violated any of these terms, or done anything with the software
99
+ not covered by your licenses, your licenses can nonetheless
100
+ continue if you come into full compliance with these terms,
101
+ and take practical steps to correct past violations, within
102
+ 32 days of receiving notice. Otherwise, all your licenses
103
+ end immediately.
104
+
105
+ ## No Liability
106
+
107
+ **_As far as the law allows, the software comes as is, without
108
+ any warranty or condition, and the licensor will not be liable
109
+ to you for any damages arising out of these terms or the use
110
+ or nature of the software, under any kind of legal claim._**
111
+
112
+ ## Definitions
113
+
114
+ The **licensor** is the individual or entity offering these
115
+ terms, and the **software** is the software the licensor makes
116
+ available under these terms.
117
+
118
+ **You** refers to the individual or entity agreeing to these
119
+ terms.
120
+
121
+ **Your company** is any legal entity, sole proprietorship,
122
+ or other kind of organization that you work for, plus all
123
+ organizations that have control over, are under the control of,
124
+ or are under common control with that organization. **Control**
125
+ means ownership of substantially all the assets of an entity,
126
+ or the power to direct its management and policies by vote,
127
+ contract, or otherwise. Control can be direct or indirect.
128
+
129
+ **Your licenses** are all the licenses granted to you for the
130
+ software under these terms.
131
+
132
+ **Use** means anything you do with the software requiring one
133
+ of your licenses.
package/README.md ADDED
@@ -0,0 +1,240 @@
1
+ # scenic-prism-playwright
2
+
3
+ [`scenic-prism`](https://www.npmjs.com/package/scenic-prism) scenes, path-traced
4
+ to image files from Node.
5
+
6
+ ```ts
7
+ import { backgrounds, materials, plane, sphere } from 'scenic-prism-fluent';
8
+ import { renderToFile } from 'scenic-prism-playwright';
9
+
10
+ const SCENE = {
11
+ scene: sphere(1).paint(materials.chrome).union(plane([0, 1, 0], -1)),
12
+ background: backgrounds.dusk,
13
+ };
14
+
15
+ await renderToFile(SCENE, 'renders/chrome.png', { size: 1200, maxFrames: 900, seed: 7 });
16
+ ```
17
+
18
+ One call opens a headless Chromium, traces the scene until the accumulation
19
+ reaches its sample budget, writes the file and shuts the browser down again —
20
+ from a script, in CI, on a machine with no screen.
21
+
22
+ The headless page uses your installed copy of `scenic-prism` and calls its
23
+ `render()` function, just as a browser canvas does.
24
+
25
+ ## Install
26
+
27
+ ```sh
28
+ npm install scenic-prism-playwright scenic-prism-fluent playwright
29
+ # or: pnpm add scenic-prism-playwright scenic-prism-fluent playwright
30
+ npx playwright install chromium
31
+ ```
32
+
33
+ The two large dependencies are **peers**, and both are bounded from below only:
34
+
35
+ | Peer | Range | Why |
36
+ | --- | --- | --- |
37
+ | `scenic-prism-fluent` | `>=` this package's version | It carries `scenic-prism`, and the copy it resolves is the one that gets served to the page. |
38
+ | `playwright` | `>=1.40` | Its browsers are your project's to install and version. |
39
+
40
+ It installs one thing of its own — `sharp` — and only uses it for `margin`,
41
+ which is drawn onto the finished image after the browser has closed.
42
+
43
+ Run TypeScript scripts with `tsx render.ts` or
44
+ `node --experimental-strip-types render.ts`. JavaScript scripts run directly
45
+ with Node. The package ships ESM with TypeScript declarations.
46
+
47
+ ## `renderToFile(spec, output, options?)`
48
+
49
+ Traces one scene and writes it. The encoding comes from the file's extension —
50
+ `.png`, `.jpg`, `.jpeg` or `.webp` — unless `format` says otherwise, and any
51
+ directories in the path are created. Returns `{ path, format, width, height,
52
+ frames, bytes, renderer, accelerated, colorSpace }` — `renderer` and
53
+ `accelerated` being what WebGPU said drew it, and whether that was hardware.
54
+
55
+ `renderToBuffer(spec, options?)` is the same render handed back as
56
+ `{ data: Buffer, … }`, for something that is going to upload it rather than
57
+ keep it. `format` defaults to `png` there, there being no filename to read.
58
+
59
+ | Option | | |
60
+ | --- | --- | --- |
61
+ | `size` | `number` | Square backing resolution in pixels (default 1024). |
62
+ | `width` `height` | `number` | The same, when the image is not square. |
63
+ | `maxFrames` | `number` | Samples per pixel to accumulate (default 1200). |
64
+ | `bounces` | `number` | Light-bounce budget (default 6); glass and metal reward 8–12. |
65
+ | `tone` | `'aces' \| 'reinhard' \| 'linear'` | The curve that brings the traced light into display range (default `'aces'`). |
66
+ | `exposure` | `number` | Exposure in stops, before that curve: `-1` is half as bright, `+2` four times (default 0). |
67
+ | `alpha` | `boolean` | Save a cut-out rather than a background: rays that leave the scene write no coverage (default false). Wants a format with an alpha channel — `png` or `webp`. |
68
+ | `seed` | `number` | Fix the sample sequence. Without one, two runs differ in their noise. |
69
+ | `format` | `'png' \| 'jpeg' \| 'webp'` | Overrides the extension. |
70
+ | `quality` | `number` | Encoder quality in (0, 1], for `jpeg` and `webp`. |
71
+ | `margin` | `{ size, colour? }` | A border of flat colour around the image: `size` whole pixels on each side, in the `[r, g, b]` triple used everywhere else (0..1, white by default). |
72
+ | `timeout` | `number` | Milliseconds before the render is abandoned (default 300000). |
73
+ | `onProgress` | `(frames, total) => void` | After every accumulated frame, in your process. |
74
+ | `gpu` | `boolean \| 'auto'` | Render on the machine's GPU (default `'auto'`: hardware where there is any, software where there is not). |
75
+ | `launch` | `LaunchOptions` | Passed to `chromium.launch()`, on top of this package's own arguments. |
76
+ | `browser` | `Browser` | A Playwright browser to draw in, instead of launching one. |
77
+ | `libraryRoot` | `string` | The `scenic-prism` package root to serve. Resolved for you by default. |
78
+
79
+ ### `margin`
80
+
81
+ A margin is added **after** the render, not before it. The page is served and
82
+ the canvas is sized exactly as they would be without one, so a scene traced at
83
+ `size: 1200` holds the same 1200² of image whether it is mounted or not — it
84
+ simply lands in a larger file. Widening the canvas instead would have widened
85
+ the field of view with it, which is a different picture rather than a mounted
86
+ one.
87
+
88
+ ```ts
89
+ await renderToFile(SCENE, 'renders/plate.png', {
90
+ size: 1200,
91
+ margin: { size: 64, colour: [0.96, 0.95, 0.92] }, // 1328² of file, 1200² of scene
92
+ });
93
+ ```
94
+
95
+ `size` is a whole number of pixels greater than zero and `colour` is the same
96
+ `[r, g, b]` triple as everywhere else, defaulting to white — but unlike a colour
97
+ in a scene it is never traced and never tonemapped, so it is the colour of the
98
+ file: `[0, 0, 0]` is black and `[0.5, 0.5, 0.5]` is the middle of the range the
99
+ file can hold. Components outside 0..1 have nowhere to go and are refused. The
100
+ border is opaque whatever the image is, so an `alpha` render comes back as a
101
+ cut-out standing on that colour. `width` and `height` on the result count it.
102
+
103
+ A margin means the image is encoded twice — once by the canvas, once when the
104
+ border is drawn — so on a `jpeg` or a `webp` it costs one further generation of
105
+ the encoder, at whatever `quality` the render was saved at.
106
+
107
+ Everything the scene itself can do is unchanged — this package adds no
108
+ vocabulary and re-exports none. Build scenes with `scenic-prism-fluent` (or with
109
+ `scenic-prism` directly) and hand them over; either dialect is accepted, and a
110
+ fluent `camera(...)` is normalised on the way in.
111
+
112
+ ## `openStudio(options?)`
113
+
114
+ Launching the browser is most of the cost of the first image and all of the cost
115
+ of the rest, so a script writing more than one should open a studio and keep it:
116
+
117
+ ```ts
118
+ import { openStudio } from 'scenic-prism-playwright';
119
+
120
+ const studio = await openStudio();
121
+ try {
122
+ for (const [index, scene] of SCENES.entries()) {
123
+ await studio.renderToFile(scene, `renders/plate-${index}.png`, { size: 900, seed: 3 });
124
+ }
125
+ } finally {
126
+ await studio.close();
127
+ }
128
+ ```
129
+
130
+ The studio takes the browser options (`gpu`, `launch`, `browser`, `libraryRoot`,
131
+ `timeout`) and exposes the same `renderToFile` / `renderToBuffer`, each on a page
132
+ of its own — so each render gets its own GPU device and gives it back. Given
133
+ an existing `browser`, the studio never launches or closes anything, which is
134
+ what makes it usable from inside a Playwright test.
135
+
136
+ ## What it actually does
137
+
138
+ It launches Chromium with WebGPU switched on, and hands a page your copy of
139
+ `scenic-prism` as ES modules, resolved off disk from the fluent package you
140
+ installed. The page imports it and calls `render()`. There is no HTTP server
141
+ and no temporary directory — the requests, for a `.localhost` origin that
142
+ counts as a secure context and so gets WebGPU, are intercepted by Playwright
143
+ and answered from memory.
144
+
145
+ Reading the image back takes one extra step. A WebGPU canvas does not keep what was drawn into it once that has been shown, so
146
+ the page renders with the core's `retain` option and reads the finished image
147
+ with `handle.readPixels()` — the same tonemap, drawn into a texture and copied
148
+ back — then encodes those pixels through a 2D canvas.
149
+
150
+ ## GPU or software
151
+
152
+ Path tracing is the same work wherever it runs, and a graphics card does it
153
+ between ten and a hundred times faster than a CPU pretending to be one. So the
154
+ browser is asked for **the GPU by default**, and is given permission to fall
155
+ back to SwiftShader's software WebGPU adapter — which is what happens, by itself,
156
+ inside the build container that has no GPU at all. Nothing needs configuring
157
+ either way:
158
+
159
+ ```ts
160
+ const image = await renderToFile(SCENE, 'renders/plate.png', { size: 1600 });
161
+ image.renderer; // 'apple metal-3' — the adapter's vendor and architecture
162
+ image.accelerated; // true
163
+ ```
164
+
165
+ Asking for the GPU means asking Playwright for the **full Chromium build** as
166
+ well (`channel: 'chromium'`), because its default headless binary is the
167
+ *headless shell*, which has no GPU stack in it. On Linux, Vulkan is switched on
168
+ too, since that is what Chromium's WebGPU runs on there. `npx playwright install
169
+ chromium` fetches both, and a launch that cannot find the full one quietly tries
170
+ the ordinary binary and then software, so this never becomes a script that fails
171
+ instead of rendering.
172
+
173
+ `gpu` is the opt-out, and the opt-in-and-insist:
174
+
175
+ | `gpu` | |
176
+ | --- | --- |
177
+ | `'auto'` *(default)* | Hardware where the machine has some, SwiftShader where it does not. |
178
+ | `false` | SwiftShader everywhere. **The reproducible one**: a GPU and a software adapter round differently, so only this setting makes the same scene and `seed` give the same bytes on every machine. |
179
+ | `true` | Hardware or nothing — the render fails rather than quietly taking twenty minutes over what should have taken one. |
180
+
181
+ ```ts
182
+ // A plate that is going to be committed, and diffed, and re-rendered by
183
+ // somebody else's machine.
184
+ await renderToFile(SCENE, 'renders/plate.png', { gpu: false, seed: 7 });
185
+ ```
186
+
187
+ Software path tracing is minutes, not seconds, for a large image at a high
188
+ sample count, so a script that opts out should raise `timeout` and report
189
+ progress:
190
+
191
+ ```ts
192
+ await renderToFile(SCENE, 'renders/poster.png', {
193
+ gpu: false,
194
+ size: 1600,
195
+ maxFrames: 2000,
196
+ timeout: 30 * 60 * 1000,
197
+ onProgress: (frames, total) => process.stdout.write(`\r${frames}/${total}`),
198
+ });
199
+ ```
200
+
201
+ Anything finer than that is a Chromium switch, and `launch` goes straight to
202
+ Playwright. The last occurrence of a switch wins, so the defaults can be
203
+ overridden one at a time:
204
+
205
+ ```ts
206
+ await renderToFile(SCENE, 'renders/plate.png', {
207
+ launch: { channel: 'chrome', args: ['--use-webgpu-adapter=swiftshader'] },
208
+ });
209
+ ```
210
+
211
+ `DEFAULT_BROWSER_ARGS`, `GPU_BROWSER_ARGS`, `SOFTWARE_BROWSER_ARGS`,
212
+ `DEFAULT_TIMEOUT` and `launchPlan(gpu, launch?)` are exported, for a script that
213
+ wants to say what it is changing — or to print what it would have launched.
214
+
215
+ ## Documentation
216
+
217
+ - [`/scenic-prism-playwright`](https://scenic-prism.pages.dev/scenic-prism-playwright)
218
+ — this package, with four worked examples
219
+ - [`scenic-prism-fluent`](https://www.npmjs.com/package/scenic-prism-fluent) —
220
+ the chainable API these examples are written in
221
+ - [`scenic-prism-react`](https://www.npmjs.com/package/scenic-prism-react) — the
222
+ same renderer, on a canvas in a component
223
+
224
+ ## Licence
225
+
226
+ [PolyForm Noncommercial 1.0.0](https://polyformproject.org/licenses/noncommercial/1.0.0).
227
+
228
+ Free for any **noncommercial** purpose — personal projects, study, experiments,
229
+ hobby and amateur work, teaching, and use by charities, schools, universities,
230
+ public research bodies, public health and safety organisations and government,
231
+ whoever is funding them. Change it, build on it and redistribute it on those
232
+ same terms, passing on the licence and its `Required Notice:` line with any copy.
233
+
234
+ **Commercial use needs a separate licence.** If you want to use `scenic-prism`
235
+ in or around a commercial product, ask via
236
+ [the repository](https://github.com/jamesporter/Scenic-Draft).
237
+
238
+ The terms in full are in the `LICENSE` file shipped inside this package, and
239
+ explained in plain language at
240
+ [scenic-prism.pages.dev/license](https://scenic-prism.pages.dev/license).
@@ -0,0 +1,31 @@
1
+ /**
2
+ * The image half of the package: what a file extension means, and how the
3
+ * canvas's data URL becomes bytes on disk. Nothing here touches a browser, so
4
+ * it is all pure and directly testable.
5
+ */
6
+ /** The encodings a browser canvas can produce, and this package can save. */
7
+ export type ImageFormat = 'png' | 'jpeg' | 'webp';
8
+ /** The MIME type `canvas.toDataURL()` wants for each of them. */
9
+ export declare const MIME_TYPES: Readonly<Record<ImageFormat, string>>;
10
+ /**
11
+ * The format an output path asks for, by its extension. Callers can always
12
+ * override it with `format`, which is what this falls back to being for a path
13
+ * whose extension says nothing.
14
+ */
15
+ export declare function formatForPath(path: string): ImageFormat;
16
+ /**
17
+ * Quality is meaningful to the lossy encoders only. PNG ignores it, so passing
18
+ * one alongside a `.png` is a mistake worth naming rather than silently
19
+ * dropping — a caller who set it believed it was doing something.
20
+ */
21
+ export declare function checkQuality(format: ImageFormat, quality: number | undefined): void;
22
+ /**
23
+ * The bytes behind a `data:` URL. `canvas.toDataURL()` is the only producer
24
+ * this ever sees, so the payload is always base64 — but a canvas asked for a
25
+ * format it cannot encode quietly returns a PNG instead, and the URL's own
26
+ * MIME type is the only place that shows. It is checked by the caller.
27
+ */
28
+ export declare function decodeDataUrl(dataUrl: string): {
29
+ mimeType: string;
30
+ bytes: Buffer;
31
+ };
package/dist/image.js ADDED
@@ -0,0 +1,68 @@
1
+ /**
2
+ * The image half of the package: what a file extension means, and how the
3
+ * canvas's data URL becomes bytes on disk. Nothing here touches a browser, so
4
+ * it is all pure and directly testable.
5
+ */
6
+ /** The MIME type `canvas.toDataURL()` wants for each of them. */
7
+ export const MIME_TYPES = {
8
+ png: 'image/png',
9
+ jpeg: 'image/jpeg',
10
+ webp: 'image/webp',
11
+ };
12
+ /**
13
+ * File extensions, against the format they mean. `.jpg` and `.jpeg` are the
14
+ * same encoding; there is deliberately no default for anything else, because
15
+ * writing PNG bytes into a `.gif` a caller asked for is worse than saying so.
16
+ */
17
+ const EXTENSIONS = {
18
+ '.png': 'png',
19
+ '.jpg': 'jpeg',
20
+ '.jpeg': 'jpeg',
21
+ '.webp': 'webp',
22
+ };
23
+ /**
24
+ * The format an output path asks for, by its extension. Callers can always
25
+ * override it with `format`, which is what this falls back to being for a path
26
+ * whose extension says nothing.
27
+ */
28
+ export function formatForPath(path) {
29
+ const match = /\.[^./\\]+$/.exec(path);
30
+ const extension = match?.[0].toLowerCase() ?? '';
31
+ const format = EXTENSIONS[extension];
32
+ if (format)
33
+ return format;
34
+ throw new Error(`scenic-prism-playwright: cannot tell the image format of '${path}'. ` +
35
+ `Use one of ${Object.keys(EXTENSIONS).join(', ')}, or pass an explicit \`format\`.`);
36
+ }
37
+ /**
38
+ * Quality is meaningful to the lossy encoders only. PNG ignores it, so passing
39
+ * one alongside a `.png` is a mistake worth naming rather than silently
40
+ * dropping — a caller who set it believed it was doing something.
41
+ */
42
+ export function checkQuality(format, quality) {
43
+ if (quality === undefined)
44
+ return;
45
+ if (format === 'png') {
46
+ throw new Error('scenic-prism-playwright: `quality` applies to jpeg and webp only — png is lossless.');
47
+ }
48
+ if (!(quality > 0 && quality <= 1)) {
49
+ throw new Error(`scenic-prism-playwright: \`quality\` must be in (0, 1], got ${quality}.`);
50
+ }
51
+ }
52
+ /**
53
+ * The bytes behind a `data:` URL. `canvas.toDataURL()` is the only producer
54
+ * this ever sees, so the payload is always base64 — but a canvas asked for a
55
+ * format it cannot encode quietly returns a PNG instead, and the URL's own
56
+ * MIME type is the only place that shows. It is checked by the caller.
57
+ */
58
+ export function decodeDataUrl(dataUrl) {
59
+ const comma = dataUrl.indexOf(',');
60
+ const header = comma === -1 ? '' : dataUrl.slice(0, comma);
61
+ if (!header.startsWith('data:') || !header.endsWith(';base64')) {
62
+ throw new Error('scenic-prism-playwright: the canvas did not return a base64 data URL.');
63
+ }
64
+ return {
65
+ mimeType: header.slice('data:'.length, -';base64'.length),
66
+ bytes: Buffer.from(dataUrl.slice(comma + 1), 'base64'),
67
+ };
68
+ }
@@ -0,0 +1,53 @@
1
+ /**
2
+ * scenic-prism-playwright — a `scenic-prism` scene, path-traced to an image
3
+ * file from Node.
4
+ *
5
+ * ```ts
6
+ * import { backgrounds, materials, sphere } from 'scenic-prism-fluent';
7
+ * import { renderToFile } from 'scenic-prism-playwright';
8
+ *
9
+ * await renderToFile(
10
+ * { scene: sphere(1).paint(materials.chrome), background: backgrounds.dusk },
11
+ * 'renders/chrome.png',
12
+ * { size: 1200, maxFrames: 900, seed: 7 },
13
+ * );
14
+ * ```
15
+ *
16
+ * Run it with anything that runs TypeScript — `tsx render.ts`, `node
17
+ * --experimental-strip-types`, or plain JavaScript. One call launches a
18
+ * headless Chromium, traces until the accumulation finishes, saves the file and
19
+ * shuts the browser down again.
20
+ *
21
+ * Four things are worth knowing before reading further.
22
+ *
23
+ * **It is the library's own renderer.** There is no second path tracer here and
24
+ * no shader of this package's own: the installed copy of `scenic-prism` is
25
+ * served to the page as ES modules and its `render()` does the work, so a saved
26
+ * image and the same scene on a web page come out of the same code.
27
+ *
28
+ * **The two big dependencies are peers.** `scenic-prism-fluent` — which carries
29
+ * `scenic-prism` — and `playwright` are the caller's to install and the
30
+ * caller's to version. The scenes handed in are built from the very copy of the
31
+ * library this then renders, and the browsers Playwright manages stay under the
32
+ * project's own control. The one thing it installs for itself is `sharp`, which
33
+ * is what draws a `margin` around a finished image — the browser has been shut
34
+ * by then, so there is nothing else left to draw it with.
35
+ *
36
+ * **It uses the GPU when the machine has one.** Path tracing in software is
37
+ * minutes where hardware is seconds, so a render asks for the graphics card and
38
+ * falls back to SwiftShader on the build container that has none. Pass
39
+ * `gpu: false` to rasterise in software everywhere, which is what makes the
40
+ * same scene and `seed` give the same pixels on every machine.
41
+ *
42
+ * **One browser, many images.** {@link renderToFile} and
43
+ * {@link renderToBuffer} open and close a browser around a single render.
44
+ * A script writing a folder of plates should {@link openStudio} instead and
45
+ * close it at the end — the launch is most of the cost of the first image.
46
+ */
47
+ export { renderToBuffer, renderToFile } from './render.js';
48
+ export type { HeadlessOptions } from './render.js';
49
+ export { DEFAULT_BROWSER_ARGS, DEFAULT_TIMEOUT, GPU_BROWSER_ARGS, SOFTWARE_BROWSER_ARGS, launchPlan, openStudio, } from './studio.js';
50
+ export type { AdapterReport, GpuPreference, RenderOptions, RenderedImage, RenderedImageData, SavedImage, Studio, StudioOptions, TraceOptions, } from './studio.js';
51
+ export { MIME_TYPES } from './image.js';
52
+ export type { ImageFormat } from './image.js';
53
+ export type { MarginOptions } from './margin.js';
package/dist/index.js ADDED
@@ -0,0 +1,49 @@
1
+ /**
2
+ * scenic-prism-playwright — a `scenic-prism` scene, path-traced to an image
3
+ * file from Node.
4
+ *
5
+ * ```ts
6
+ * import { backgrounds, materials, sphere } from 'scenic-prism-fluent';
7
+ * import { renderToFile } from 'scenic-prism-playwright';
8
+ *
9
+ * await renderToFile(
10
+ * { scene: sphere(1).paint(materials.chrome), background: backgrounds.dusk },
11
+ * 'renders/chrome.png',
12
+ * { size: 1200, maxFrames: 900, seed: 7 },
13
+ * );
14
+ * ```
15
+ *
16
+ * Run it with anything that runs TypeScript — `tsx render.ts`, `node
17
+ * --experimental-strip-types`, or plain JavaScript. One call launches a
18
+ * headless Chromium, traces until the accumulation finishes, saves the file and
19
+ * shuts the browser down again.
20
+ *
21
+ * Four things are worth knowing before reading further.
22
+ *
23
+ * **It is the library's own renderer.** There is no second path tracer here and
24
+ * no shader of this package's own: the installed copy of `scenic-prism` is
25
+ * served to the page as ES modules and its `render()` does the work, so a saved
26
+ * image and the same scene on a web page come out of the same code.
27
+ *
28
+ * **The two big dependencies are peers.** `scenic-prism-fluent` — which carries
29
+ * `scenic-prism` — and `playwright` are the caller's to install and the
30
+ * caller's to version. The scenes handed in are built from the very copy of the
31
+ * library this then renders, and the browsers Playwright manages stay under the
32
+ * project's own control. The one thing it installs for itself is `sharp`, which
33
+ * is what draws a `margin` around a finished image — the browser has been shut
34
+ * by then, so there is nothing else left to draw it with.
35
+ *
36
+ * **It uses the GPU when the machine has one.** Path tracing in software is
37
+ * minutes where hardware is seconds, so a render asks for the graphics card and
38
+ * falls back to SwiftShader on the build container that has none. Pass
39
+ * `gpu: false` to rasterise in software everywhere, which is what makes the
40
+ * same scene and `seed` give the same pixels on every machine.
41
+ *
42
+ * **One browser, many images.** {@link renderToFile} and
43
+ * {@link renderToBuffer} open and close a browser around a single render.
44
+ * A script writing a folder of plates should {@link openStudio} instead and
45
+ * close it at the end — the launch is most of the cost of the first image.
46
+ */
47
+ export { renderToBuffer, renderToFile } from "./render.js";
48
+ export { DEFAULT_BROWSER_ARGS, DEFAULT_TIMEOUT, GPU_BROWSER_ARGS, SOFTWARE_BROWSER_ARGS, launchPlan, openStudio, } from "./studio.js";
49
+ export { MIME_TYPES } from "./image.js";
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Walk `node_modules` upwards from a directory, the way Node's own resolver
3
+ * does, and return the root of the first `name` found. Deliberately not
4
+ * `require.resolve`: this package and the ones it looks for are ESM with an
5
+ * `exports` map that has no `require` condition, so CommonJS resolution cannot
6
+ * see them, and `import.meta.resolve` answers with an entry module rather than
7
+ * with the package root and its manifest — which is what has to be served.
8
+ */
9
+ export declare function findPackageRoot(from: string, name: string): string | null;
10
+ /**
11
+ * Where the installed `scenic-prism` lives. Looked for beside the fluent
12
+ * package first — that is the copy the caller's scenes are built with, and the
13
+ * one whose version the fluent package pins — and only then beside this one,
14
+ * for an install flat enough to hoist it.
15
+ */
16
+ export declare function resolveLibraryRoot(): string;
17
+ /**
18
+ * The module a browser should import for a package root, as a path relative to
19
+ * it. Read off the manifest the way a bundler would: the `import` condition of
20
+ * the main export first, then the legacy fields.
21
+ */
22
+ export declare function browserEntry(root: string): string;
23
+ /** Content types for the handful of things a package's `dist` holds. */
24
+ export declare function contentType(path: string): string;
25
+ /**
26
+ * Resolve a request path against the served root, refusing anything that
27
+ * climbs out of it. Nothing here is reachable from outside the browser this
28
+ * package launched, but a scene is data and data comes from somewhere, so the
29
+ * one place a path could be influenced is still checked.
30
+ */
31
+ export declare function resolveServedFile(root: string, requestPath: string): string | null;