@palmerama/hd-canvas 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +412 -0
  3. package/dist/bridge/Canvas2DBridge.d.ts +37 -0
  4. package/dist/bridge/Canvas2DBridge.d.ts.map +1 -0
  5. package/dist/bridge/Canvas2DBridge.js +116 -0
  6. package/dist/bridge/Canvas2DBridge.js.map +1 -0
  7. package/dist/core/ColorBuffer.d.ts +41 -0
  8. package/dist/core/ColorBuffer.d.ts.map +1 -0
  9. package/dist/core/ColorBuffer.js +138 -0
  10. package/dist/core/ColorBuffer.js.map +1 -0
  11. package/dist/core/HDCanvas.d.ts +80 -0
  12. package/dist/core/HDCanvas.d.ts.map +1 -0
  13. package/dist/core/HDCanvas.js +104 -0
  14. package/dist/core/HDCanvas.js.map +1 -0
  15. package/dist/core/PaperSize.d.ts +40 -0
  16. package/dist/core/PaperSize.d.ts.map +1 -0
  17. package/dist/core/PaperSize.js +63 -0
  18. package/dist/core/PaperSize.js.map +1 -0
  19. package/dist/export/ExportPipeline.d.ts +94 -0
  20. package/dist/export/ExportPipeline.d.ts.map +1 -0
  21. package/dist/export/ExportPipeline.js +121 -0
  22. package/dist/export/ExportPipeline.js.map +1 -0
  23. package/dist/export/PNGExporter.d.ts +62 -0
  24. package/dist/export/PNGExporter.d.ts.map +1 -0
  25. package/dist/export/PNGExporter.js +146 -0
  26. package/dist/export/PNGExporter.js.map +1 -0
  27. package/dist/export/ToneMapper.d.ts +88 -0
  28. package/dist/export/ToneMapper.d.ts.map +1 -0
  29. package/dist/export/ToneMapper.js +175 -0
  30. package/dist/export/ToneMapper.js.map +1 -0
  31. package/dist/index.d.ts +16 -0
  32. package/dist/index.d.ts.map +1 -0
  33. package/dist/index.js +22 -0
  34. package/dist/index.js.map +1 -0
  35. package/dist/preview/FitStrategy.d.ts +27 -0
  36. package/dist/preview/FitStrategy.d.ts.map +1 -0
  37. package/dist/preview/FitStrategy.js +31 -0
  38. package/dist/preview/FitStrategy.js.map +1 -0
  39. package/dist/preview/PreviewRenderer.d.ts +71 -0
  40. package/dist/preview/PreviewRenderer.d.ts.map +1 -0
  41. package/dist/preview/PreviewRenderer.js +304 -0
  42. package/dist/preview/PreviewRenderer.js.map +1 -0
  43. package/package.json +47 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 palmerama
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,412 @@
1
+ # hd-canvas
2
+
3
+ A TypeScript library for generative art with **HDR color**, configurable **paper sizes**, and **print-quality export**.
4
+
5
+ Build artwork with unbounded float colors (values > 1.0 = super-bright HDR), preview it on screen with zoom/pan, and export print-ready PNGs at 300+ DPI with embedded resolution metadata.
6
+
7
+ ## Features
8
+
9
+ - **Float32/Float64 HDR color buffers** — unbounded RGBA values, no 8-bit clamping during creation
10
+ - **Paper size presets** — A0–A6, US Letter/Legal/Tabloid with DPI-aware pixel calculations
11
+ - **Tone mapping** — Reinhard, ACES filmic, clamp, or custom algorithms to compress HDR → LDR on export
12
+ - **Print-ready PNG export** — pHYs chunk injection for correct DPI metadata, 8-bit and 16-bit output
13
+ - **Canvas 2D bridge** — draw with the familiar `fillRect`, `arc`, `fillText` API, auto-tiled for large formats
14
+ - **Zoom/pan preview** — scroll-wheel zoom centered on cursor, click-drag pan, keyboard shortcuts, visible-region-only rendering
15
+ - **Blend modes** — normal (alpha composite), additive, multiply — layer effects in HDR space
16
+ - **Zero native dependencies** — pure JS PNG encoding via `fast-png`, runs in browser and Node.js
17
+
18
+ ## Install
19
+
20
+ ```bash
21
+ npm install hd-canvas
22
+ ```
23
+
24
+ ## Quick Start
25
+
26
+ ```typescript
27
+ import {
28
+ HDCanvas,
29
+ attachExportPipeline,
30
+ PreviewRenderer,
31
+ } from 'hd-canvas';
32
+
33
+ // 1. Create a canvas — A3 paper at 300 DPI
34
+ const canvas = new HDCanvas({ paperSize: 'A3', dpi: 300 });
35
+
36
+ // 2. Draw with HDR float colors
37
+ for (let y = 0; y < canvas.heightPx; y++) {
38
+ for (let x = 0; x < canvas.widthPx; x++) {
39
+ const intensity = Math.random() * 3.0; // HDR: values > 1.0
40
+ canvas.setPixel(x, y, intensity, intensity * 0.6, 0.1, 1.0);
41
+ }
42
+ }
43
+
44
+ // 3. Preview on screen (browser only)
45
+ const preview = new PreviewRenderer(canvas, {
46
+ container: document.getElementById('preview')!,
47
+ });
48
+
49
+ // 4. Export print-ready PNG
50
+ attachExportPipeline(canvas);
51
+ const blob = await canvas.export({ toneMap: 'aces', exposure: 0.5 });
52
+ ```
53
+
54
+ ## API Reference
55
+
56
+ ### Core
57
+
58
+ #### `HDCanvas`
59
+
60
+ The main class. Wraps a `ColorBuffer` with paper size and DPI configuration.
61
+
62
+ ```typescript
63
+ const canvas = new HDCanvas({
64
+ paperSize: 'A4', // or 'A3', 'letter', 'tabloid', etc.
65
+ dpi: 300, // default: 300
66
+ colorDepth: 32, // 32 (Float32) or 64 (Float64), default: 32
67
+ orientation: 'portrait', // 'portrait' or 'landscape', default: 'portrait'
68
+ });
69
+
70
+ canvas.widthPx; // pixel width (e.g., 2480 for A4 @ 300 DPI)
71
+ canvas.heightPx; // pixel height (e.g., 3508 for A4 @ 300 DPI)
72
+ canvas.dpi; // configured DPI
73
+ canvas.memoryBytes; // buffer memory usage in bytes
74
+ ```
75
+
76
+ #### Pixel Drawing
77
+
78
+ ```typescript
79
+ // Set a pixel (RGBA, unbounded floats)
80
+ canvas.setPixel(x, y, r, g, b, a);
81
+
82
+ // Read a pixel back
83
+ const [r, g, b, a] = canvas.getPixel(x, y);
84
+
85
+ // Blend onto existing content
86
+ canvas.blendPixel(x, y, r, g, b, a, 'normal'); // alpha composite
87
+ canvas.blendPixel(x, y, r, g, b, a, 'add'); // additive (glow effects)
88
+ canvas.blendPixel(x, y, r, g, b, a, 'multiply'); // multiply (shadows)
89
+
90
+ // Fill entire buffer
91
+ canvas.clear(0, 0, 0, 1); // solid black
92
+
93
+ // Region operations
94
+ const region = canvas.getRegion(x, y, width, height); // extract sub-buffer
95
+ canvas.putRegion(x, y, region); // paste sub-buffer
96
+ ```
97
+
98
+ #### `ColorBuffer`
99
+
100
+ The raw float pixel buffer. Used directly for advanced operations.
101
+
102
+ ```typescript
103
+ import { ColorBuffer } from 'hd-canvas';
104
+
105
+ const buf = new ColorBuffer(1920, 1080, 32); // width, height, depth
106
+ buf.setPixel(0, 0, 1.5, 0.3, 0.0, 1.0); // HDR orange
107
+ buf.data; // Float32Array — direct access for bulk operations
108
+ ```
109
+
110
+ #### Paper Sizes
111
+
112
+ ```typescript
113
+ import { PAPER_SIZES, sizeToPx, resolvePaperSize, estimateBufferBytes } from 'hd-canvas';
114
+
115
+ // All presets: A0–A6, letter, legal, tabloid
116
+ PAPER_SIZES.A4; // { widthMM: 210, heightMM: 297 }
117
+ PAPER_SIZES.letter; // { widthMM: 215.9, heightMM: 279.4 }
118
+
119
+ // Calculate pixel dimensions
120
+ sizeToPx({ widthMM: 210, heightMM: 297 }, 300);
121
+ // → { width: 2480, height: 3508 }
122
+
123
+ // Resolve with orientation
124
+ resolvePaperSize('A3', 'landscape');
125
+ // → { widthMM: 420, heightMM: 297 }
126
+
127
+ // Estimate memory before allocating
128
+ estimateBufferBytes('A0', 300, 32); // ~2.07 GB for Float32
129
+ estimateBufferBytes('A0', 300, 64); // ~4.14 GB for Float64
130
+ ```
131
+
132
+ ### Canvas 2D Bridge
133
+
134
+ Draw with the familiar Canvas 2D API — shapes, text, paths, gradients — then continue with HDR pixel operations on top.
135
+
136
+ ```typescript
137
+ // Draw convenience shapes with Canvas 2D
138
+ canvas.drawWith2D((ctx) => {
139
+ ctx.fillStyle = '#1a1a2e';
140
+ ctx.fillRect(0, 0, canvas.widthPx, canvas.heightPx);
141
+
142
+ ctx.strokeStyle = 'white';
143
+ ctx.lineWidth = 3;
144
+ ctx.beginPath();
145
+ ctx.arc(canvas.widthPx / 2, canvas.heightPx / 2, 200, 0, Math.PI * 2);
146
+ ctx.stroke();
147
+
148
+ ctx.font = '72px serif';
149
+ ctx.fillStyle = 'white';
150
+ ctx.fillText('Hello HD', 100, 400);
151
+ });
152
+
153
+ // Then layer HDR effects on top
154
+ canvas.blendPixel(x, y, 2.0, 0.5, 0.0, 0.8, 'add'); // HDR glow
155
+ ```
156
+
157
+ **Options:**
158
+
159
+ ```typescript
160
+ canvas.drawWith2D(callback, {
161
+ mode: 'blend', // 'overwrite' (default) or 'blend'
162
+ blendMode: 'normal', // 'normal', 'add', or 'multiply' (when mode is 'blend')
163
+ region: { x, y, width, height }, // draw into a sub-region only
164
+ });
165
+ ```
166
+
167
+ > **Note:** Canvas 2D is 8-bit, so this is for convenience shapes/text. For HDR drawing, use the pixel API directly. Large canvases (A0+) are automatically tiled at 4096px for browser compatibility.
168
+
169
+ ### Preview
170
+
171
+ Interactive zoom/pan preview for browser environments.
172
+
173
+ ```typescript
174
+ import { PreviewRenderer } from 'hd-canvas';
175
+
176
+ const preview = new PreviewRenderer(canvas, {
177
+ container: document.getElementById('preview')!,
178
+ });
179
+
180
+ preview.refresh(); // re-render after drawing changes
181
+ preview.destroy(); // clean up event listeners
182
+ ```
183
+
184
+ **Controls:**
185
+ - Scroll wheel: zoom (centered on cursor)
186
+ - Click + drag: pan
187
+ - `+` / `-` keys: zoom in/out
188
+ - `0` key: reset zoom
189
+ - Double-click: fit to view
190
+
191
+ ### Tone Mapping
192
+
193
+ Compress HDR float values to displayable/exportable range.
194
+
195
+ ```typescript
196
+ import { ToneMapper, reinhard, aces, clamp } from 'hd-canvas';
197
+
198
+ // Use standalone functions
199
+ reinhard(2.0); // → 0.667 (smooth compression)
200
+ aces(2.0); // → 0.928 (filmic look)
201
+ clamp(2.0); // → 1.0 (hard clip)
202
+
203
+ // Or the full pipeline
204
+ const mapper = new ToneMapper({
205
+ algorithm: 'aces', // 'reinhard', 'aces', 'clamp', or custom function
206
+ exposure: 1.0, // stops: multiply by 2^exposure before mapping
207
+ gamma: 2.2, // sRGB gamma correction (default: 2.2)
208
+ outputDepth: 8, // 8 → Uint8Array, 16 → Uint16Array
209
+ });
210
+
211
+ const ldrPixels = mapper.map(canvas.buffer); // Uint8Array RGBA
212
+ ```
213
+
214
+ **Custom tone mapping:**
215
+
216
+ ```typescript
217
+ const mapper = new ToneMapper({
218
+ algorithm: (v: number) => Math.sqrt(Math.min(1, v)), // square root compression
219
+ gamma: 1.0,
220
+ });
221
+ ```
222
+
223
+ ### Export
224
+
225
+ Print-ready PNG export with DPI metadata.
226
+
227
+ ```typescript
228
+ import { attachExportPipeline, exportBuffer, exportAndDownload } from 'hd-canvas';
229
+
230
+ // Option 1: Attach to HDCanvas (recommended)
231
+ attachExportPipeline(canvas);
232
+ const blob = await canvas.export({
233
+ toneMap: 'aces', // tone mapping algorithm
234
+ exposure: 0.5, // exposure adjustment (stops)
235
+ gamma: 2.2, // gamma correction
236
+ });
237
+
238
+ // Option 2: Export with progress tracking
239
+ attachExportPipeline(canvas, (percent) => {
240
+ console.log(`Export: ${percent}%`);
241
+ });
242
+
243
+ // Option 3: Standalone function (no HDCanvas needed)
244
+ const blob2 = exportBuffer(colorBuffer, {
245
+ dpi: 300,
246
+ toneMap: 'reinhard',
247
+ exposure: 0,
248
+ gamma: 2.2,
249
+ });
250
+
251
+ // Option 4: Export + browser download in one call
252
+ await exportAndDownload(canvas, { toneMap: 'aces' }, 'my-artwork.png');
253
+ ```
254
+
255
+ #### PNG Exporter (low-level)
256
+
257
+ ```typescript
258
+ import { PNGExporter, dpiToPixelsPerMeter } from 'hd-canvas';
259
+
260
+ const exporter = new PNGExporter();
261
+
262
+ // 8-bit export
263
+ const result = exporter.export(uint8Data, {
264
+ width: 2480,
265
+ height: 3508,
266
+ dpi: 300,
267
+ depth: 8,
268
+ });
269
+ // result.data: Uint8Array (raw PNG bytes)
270
+ // result.mimeType: 'image/png'
271
+ // result.filename: 'artwork-2480x3508-300dpi.png'
272
+
273
+ // 16-bit export for maximum quality
274
+ const result16 = exporter.export(uint16Data, {
275
+ width: 2480,
276
+ height: 3508,
277
+ dpi: 300,
278
+ depth: 16,
279
+ });
280
+
281
+ // DPI conversion utility
282
+ dpiToPixelsPerMeter(300); // → 11811
283
+ ```
284
+
285
+ ## Examples
286
+
287
+ ### Generative Flow Field
288
+
289
+ ```typescript
290
+ import { HDCanvas, attachExportPipeline } from 'hd-canvas';
291
+
292
+ const canvas = new HDCanvas({ paperSize: 'A3', dpi: 300 });
293
+ canvas.clear(0.02, 0.02, 0.05, 1.0); // near-black background
294
+
295
+ // Generate flow field with HDR highlights
296
+ for (let i = 0; i < 50000; i++) {
297
+ let x = Math.random() * canvas.widthPx;
298
+ let y = Math.random() * canvas.heightPx;
299
+
300
+ for (let step = 0; step < 100; step++) {
301
+ const angle = noise2D(x * 0.001, y * 0.001) * Math.PI * 4;
302
+ x += Math.cos(angle) * 2;
303
+ y += Math.sin(angle) * 2;
304
+
305
+ if (x < 0 || x >= canvas.widthPx || y < 0 || y >= canvas.heightPx) break;
306
+
307
+ // Additive blending creates natural HDR glow at intersections
308
+ canvas.blendPixel(
309
+ Math.floor(x), Math.floor(y),
310
+ 0.02, 0.015, 0.03, // subtle per-step contribution
311
+ 0.5, // semi-transparent
312
+ 'add' // accumulates beyond 1.0 = HDR
313
+ );
314
+ }
315
+ }
316
+
317
+ // ACES tone mapping compresses the HDR glow beautifully
318
+ attachExportPipeline(canvas);
319
+ const blob = await canvas.export({ toneMap: 'aces', exposure: 1.5 });
320
+ ```
321
+
322
+ ### Layered Composition
323
+
324
+ ```typescript
325
+ const canvas = new HDCanvas({ paperSize: 'letter', dpi: 300 });
326
+
327
+ // Layer 1: Canvas 2D background
328
+ canvas.drawWith2D((ctx) => {
329
+ const grad = ctx.createLinearGradient(0, 0, 0, canvas.heightPx);
330
+ grad.addColorStop(0, '#0a0a2e');
331
+ grad.addColorStop(1, '#1a0a3e');
332
+ ctx.fillStyle = grad;
333
+ ctx.fillRect(0, 0, canvas.widthPx, canvas.heightPx);
334
+ });
335
+
336
+ // Layer 2: HDR particle system (blend on top)
337
+ for (const particle of particles) {
338
+ canvas.blendPixel(
339
+ particle.x, particle.y,
340
+ particle.energy * 2.0, // HDR intensity
341
+ particle.energy * 0.8,
342
+ particle.energy * 0.3,
343
+ 0.6,
344
+ 'add'
345
+ );
346
+ }
347
+
348
+ // Layer 3: Canvas 2D text overlay (blend mode preserves HDR underneath)
349
+ canvas.drawWith2D((ctx) => {
350
+ ctx.font = 'bold 120px sans-serif';
351
+ ctx.fillStyle = 'rgba(255, 255, 255, 0.9)';
352
+ ctx.fillText('ENERGY', 100, canvas.heightPx / 2);
353
+ }, { mode: 'blend' });
354
+ ```
355
+
356
+ ### Custom Paper Size
357
+
358
+ ```typescript
359
+ const canvas = new HDCanvas({
360
+ paperSize: { widthMM: 300, heightMM: 300 }, // 30cm square
361
+ dpi: 600, // high quality
362
+ colorDepth: 64, // Float64 for maximum precision
363
+ });
364
+ ```
365
+
366
+ ## Paper Size Reference
367
+
368
+ | Size | Dimensions (mm) | Pixels @ 300 DPI | Memory (Float32) |
369
+ |------|-----------------|-------------------|-------------------|
370
+ | A6 | 105 × 148 | 1240 × 1748 | 8.3 MB |
371
+ | A5 | 148 × 210 | 1748 × 2480 | 16.6 MB |
372
+ | A4 | 210 × 297 | 2480 × 3508 | 33.3 MB |
373
+ | A3 | 297 × 420 | 3508 × 4961 | 66.6 MB |
374
+ | A2 | 420 × 594 | 4961 × 7016 | 133 MB |
375
+ | A1 | 594 × 841 | 7016 × 9933 | 267 MB |
376
+ | A0 | 841 × 1189 | 9933 × 14043 | 534 MB |
377
+ | Letter | 215.9 × 279.4 | 2550 × 3300 | 32.2 MB |
378
+ | Legal | 215.9 × 355.6 | 2550 × 4200 | 41.0 MB |
379
+ | Tabloid | 279.4 × 431.8 | 3300 × 5100 | 64.5 MB |
380
+
381
+ > Memory shown is for the pixel buffer only (4 × Float32 per pixel). Float64 doubles these values.
382
+
383
+ ## Architecture
384
+
385
+ ```
386
+ hd-canvas/
387
+ src/
388
+ core/
389
+ ColorBuffer.ts — Float32/Float64 RGBA pixel buffer
390
+ PaperSize.ts — Paper size registry + DPI calculations
391
+ HDCanvas.ts — Main class, wires everything together
392
+ preview/
393
+ PreviewRenderer.ts — Zoom/pan interactive preview
394
+ FitStrategy.ts — Contain/cover fitting math
395
+ bridge/
396
+ Canvas2DBridge.ts — Canvas 2D API → float buffer bridge
397
+ export/
398
+ ToneMapper.ts — HDR → LDR tone mapping algorithms
399
+ PNGExporter.ts — PNG encoding with DPI metadata
400
+ ExportPipeline.ts — Glue: tone map → encode → Blob
401
+ index.ts — Unified public API
402
+ ```
403
+
404
+ **Design principles:**
405
+ - **Dependency injection** — preview and export are pluggable, core has no DOM dependency
406
+ - **Interface segregation** — export pipeline codes against `IColorBuffer`, not the full `ColorBuffer`
407
+ - **Fail hard** — out-of-bounds pixels, invalid dimensions, and bad options throw immediately
408
+ - **One code path** — no duplicated logic, no silent fallbacks
409
+
410
+ ## License
411
+
412
+ MIT
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Canvas2DBridge — Draw with the familiar Canvas 2D API into an HDR float buffer
3
+ *
4
+ * Creates a temporary offscreen canvas at full resolution (or tiled for large
5
+ * formats), passes the 2D context to a user callback, reads back ImageData,
6
+ * and writes into the ColorBuffer as float values (0–255 → 0.0–1.0).
7
+ *
8
+ * This is a one-way bridge: Canvas 2D → float buffer. The 2D context is 8-bit,
9
+ * so this is for convenience shapes/text, not HDR input.
10
+ */
11
+ import { ColorBuffer, type BlendMode } from '../core/ColorBuffer.js';
12
+ export interface DrawWith2DOptions {
13
+ /** How to combine with existing buffer content. Default: 'overwrite' */
14
+ mode?: 'overwrite' | 'blend';
15
+ /** Blend mode when mode is 'blend'. Default: 'normal' */
16
+ blendMode?: BlendMode;
17
+ /** Region to draw into (default: full canvas). */
18
+ region?: {
19
+ x: number;
20
+ y: number;
21
+ width: number;
22
+ height: number;
23
+ };
24
+ }
25
+ /**
26
+ * Execute a Canvas 2D drawing callback and write the result into a ColorBuffer.
27
+ *
28
+ * For buffers larger than MAX_TILE_SIZE in either dimension, the drawing is
29
+ * automatically tiled: the callback is invoked once per tile with the context
30
+ * translated so the user draws in full-canvas coordinates.
31
+ */
32
+ export declare function drawWith2D(buffer: ColorBuffer, callback: (ctx: CanvasRenderingContext2D) => void, options?: DrawWith2DOptions): void;
33
+ /** Exported for testing */
34
+ export declare const _internals: {
35
+ MAX_TILE_SIZE: number;
36
+ };
37
+ //# sourceMappingURL=Canvas2DBridge.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Canvas2DBridge.d.ts","sourceRoot":"","sources":["../../src/bridge/Canvas2DBridge.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAE,WAAW,EAAE,KAAK,SAAS,EAAE,MAAM,wBAAwB,CAAC;AAErE,MAAM,WAAW,iBAAiB;IAChC,wEAAwE;IACxE,IAAI,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC;IAC7B,yDAAyD;IACzD,SAAS,CAAC,EAAE,SAAS,CAAC;IACtB,kDAAkD;IAClD,MAAM,CAAC,EAAE;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;CAClE;AASD;;;;;;GAMG;AACH,wBAAgB,UAAU,CACxB,MAAM,EAAE,WAAW,EACnB,QAAQ,EAAE,CAAC,GAAG,EAAE,wBAAwB,KAAK,IAAI,EACjD,OAAO,GAAE,iBAAsB,GAC9B,IAAI,CA8BN;AAgGD,2BAA2B;AAC3B,eAAO,MAAM,UAAU;;CAAoB,CAAC"}
@@ -0,0 +1,116 @@
1
+ /**
2
+ * Canvas2DBridge — Draw with the familiar Canvas 2D API into an HDR float buffer
3
+ *
4
+ * Creates a temporary offscreen canvas at full resolution (or tiled for large
5
+ * formats), passes the 2D context to a user callback, reads back ImageData,
6
+ * and writes into the ColorBuffer as float values (0–255 → 0.0–1.0).
7
+ *
8
+ * This is a one-way bridge: Canvas 2D → float buffer. The 2D context is 8-bit,
9
+ * so this is for convenience shapes/text, not HDR input.
10
+ */
11
+ /**
12
+ * Maximum tile dimension in pixels. Conservative to support all major browsers.
13
+ * Chrome: ~16384, Firefox: ~11180, Safari: ~4096.
14
+ * We use 4096 for maximum compatibility.
15
+ */
16
+ const MAX_TILE_SIZE = 4096;
17
+ /**
18
+ * Execute a Canvas 2D drawing callback and write the result into a ColorBuffer.
19
+ *
20
+ * For buffers larger than MAX_TILE_SIZE in either dimension, the drawing is
21
+ * automatically tiled: the callback is invoked once per tile with the context
22
+ * translated so the user draws in full-canvas coordinates.
23
+ */
24
+ export function drawWith2D(buffer, callback, options = {}) {
25
+ const mode = options.mode ?? 'overwrite';
26
+ const blendMode = options.blendMode ?? 'normal';
27
+ // Determine the target region
28
+ const region = options.region ?? { x: 0, y: 0, width: buffer.width, height: buffer.height };
29
+ validateRegion(region, buffer.width, buffer.height);
30
+ const { x: rx, y: ry, width: rw, height: rh } = region;
31
+ // Determine if we need tiling
32
+ if (rw <= MAX_TILE_SIZE && rh <= MAX_TILE_SIZE) {
33
+ // Single pass — no tiling needed
34
+ drawTile(buffer, callback, rx, ry, rw, rh, mode, blendMode);
35
+ }
36
+ else {
37
+ // Tiled rendering
38
+ for (let ty = 0; ty < rh; ty += MAX_TILE_SIZE) {
39
+ const tileH = Math.min(MAX_TILE_SIZE, rh - ty);
40
+ for (let tx = 0; tx < rw; tx += MAX_TILE_SIZE) {
41
+ const tileW = Math.min(MAX_TILE_SIZE, rw - tx);
42
+ drawTile(buffer, callback, rx + tx, ry + ty, tileW, tileH, mode, blendMode);
43
+ }
44
+ }
45
+ }
46
+ }
47
+ /**
48
+ * Draw a single tile: create an offscreen canvas, invoke the callback with
49
+ * a translated context, read back pixels, write into the buffer.
50
+ */
51
+ function drawTile(buffer, callback, tileX, tileY, tileW, tileH, mode, blendMode) {
52
+ // Create offscreen canvas for this tile
53
+ const offscreen = document.createElement('canvas');
54
+ offscreen.width = tileW;
55
+ offscreen.height = tileH;
56
+ const ctx = offscreen.getContext('2d');
57
+ if (!ctx) {
58
+ throw new Error('Failed to get 2D rendering context for offscreen canvas');
59
+ }
60
+ // Translate so the user draws in full-canvas coordinates
61
+ ctx.translate(-tileX, -tileY);
62
+ // Clip to the tile region (prevents drawing outside tile bounds)
63
+ ctx.beginPath();
64
+ ctx.rect(tileX, tileY, tileW, tileH);
65
+ ctx.clip();
66
+ // Let the user draw
67
+ callback(ctx);
68
+ // Read back pixels
69
+ const imageData = ctx.getImageData(0, 0, tileW, tileH);
70
+ const pixels = imageData.data; // Uint8ClampedArray, RGBA
71
+ // Write into the float buffer
72
+ const inv255 = 1 / 255;
73
+ if (mode === 'overwrite') {
74
+ for (let row = 0; row < tileH; row++) {
75
+ for (let col = 0; col < tileW; col++) {
76
+ const srcIdx = (row * tileW + col) * 4;
77
+ const r = pixels[srcIdx] * inv255;
78
+ const g = pixels[srcIdx + 1] * inv255;
79
+ const b = pixels[srcIdx + 2] * inv255;
80
+ const a = pixels[srcIdx + 3] * inv255;
81
+ buffer.setPixel(tileX + col, tileY + row, r, g, b, a);
82
+ }
83
+ }
84
+ }
85
+ else {
86
+ // Blend mode — alpha composite onto existing content
87
+ for (let row = 0; row < tileH; row++) {
88
+ for (let col = 0; col < tileW; col++) {
89
+ const srcIdx = (row * tileW + col) * 4;
90
+ const r = pixels[srcIdx] * inv255;
91
+ const g = pixels[srcIdx + 1] * inv255;
92
+ const b = pixels[srcIdx + 2] * inv255;
93
+ const a = pixels[srcIdx + 3] * inv255;
94
+ // Skip fully transparent pixels (common optimization)
95
+ if (a === 0)
96
+ continue;
97
+ buffer.blendPixel(tileX + col, tileY + row, r, g, b, a, blendMode);
98
+ }
99
+ }
100
+ }
101
+ // Clean up — remove references to allow GC
102
+ offscreen.width = 0;
103
+ offscreen.height = 0;
104
+ }
105
+ function validateRegion(region, bufferWidth, bufferHeight) {
106
+ const { x, y, width, height } = region;
107
+ if (x < 0 || y < 0 || width <= 0 || height <= 0) {
108
+ throw new RangeError(`Region must have non-negative origin and positive dimensions, got (${x},${y} ${width}×${height})`);
109
+ }
110
+ if (x + width > bufferWidth || y + height > bufferHeight) {
111
+ throw new RangeError(`Region (${x},${y} ${width}×${height}) exceeds buffer bounds ${bufferWidth}×${bufferHeight}`);
112
+ }
113
+ }
114
+ /** Exported for testing */
115
+ export const _internals = { MAX_TILE_SIZE };
116
+ //# sourceMappingURL=Canvas2DBridge.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Canvas2DBridge.js","sourceRoot":"","sources":["../../src/bridge/Canvas2DBridge.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,41 @@
1
+ /**
2
+ * ColorBuffer — Float32/Float64 RGBA pixel buffer
3
+ *
4
+ * The foundation of the HD Canvas framework. Stores pixel data as
5
+ * unbounded floats (0.0–1.0 is "standard" range, >1.0 is HDR).
6
+ * Row-major layout, 4 floats per pixel (R, G, B, A).
7
+ */
8
+ export type ColorDepth = 32 | 64;
9
+ export type BlendMode = 'normal' | 'add' | 'multiply';
10
+ export type RGBA = [r: number, g: number, b: number, a: number];
11
+ /** Minimal interface for reading pixel data — used by the export pipeline. */
12
+ export interface IColorBuffer {
13
+ readonly width: number;
14
+ readonly height: number;
15
+ readonly depth: ColorDepth;
16
+ readonly data: Float32Array | Float64Array;
17
+ }
18
+ export declare class ColorBuffer implements IColorBuffer {
19
+ readonly width: number;
20
+ readonly height: number;
21
+ readonly depth: ColorDepth;
22
+ readonly data: Float32Array | Float64Array;
23
+ constructor(width: number, height: number, depth?: ColorDepth);
24
+ /** Byte size of the underlying typed array */
25
+ get byteLength(): number;
26
+ private offset;
27
+ setPixel(x: number, y: number, r: number, g: number, b: number, a?: number): void;
28
+ getPixel(x: number, y: number): RGBA;
29
+ /**
30
+ * Blend a color onto the existing pixel using the specified blend mode.
31
+ * All modes use standard alpha compositing for the alpha channel.
32
+ */
33
+ blendPixel(x: number, y: number, r: number, g: number, b: number, a: number, mode?: BlendMode): void;
34
+ /** Fill the entire buffer with a single color (default: transparent black) */
35
+ clear(r?: number, g?: number, b?: number, a?: number): void;
36
+ /** Extract a rectangular region as a new ColorBuffer */
37
+ getRegion(x: number, y: number, w: number, h: number): ColorBuffer;
38
+ /** Write a ColorBuffer region into this buffer at the given position */
39
+ putRegion(x: number, y: number, region: ColorBuffer): void;
40
+ }
41
+ //# sourceMappingURL=ColorBuffer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ColorBuffer.d.ts","sourceRoot":"","sources":["../../src/core/ColorBuffer.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,MAAM,MAAM,UAAU,GAAG,EAAE,GAAG,EAAE,CAAC;AACjC,MAAM,MAAM,SAAS,GAAG,QAAQ,GAAG,KAAK,GAAG,UAAU,CAAC;AACtD,MAAM,MAAM,IAAI,GAAG,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,CAAC,CAAC;AAEhE,8EAA8E;AAC9E,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,KAAK,EAAE,UAAU,CAAC;IAC3B,QAAQ,CAAC,IAAI,EAAE,YAAY,GAAG,YAAY,CAAC;CAC5C;AAED,qBAAa,WAAY,YAAW,YAAY;IAC9C,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,KAAK,EAAE,UAAU,CAAC;IAC3B,QAAQ,CAAC,IAAI,EAAE,YAAY,GAAG,YAAY,CAAC;gBAE/B,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,GAAE,UAAe;IAgBjE,8CAA8C;IAC9C,IAAI,UAAU,IAAI,MAAM,CAEvB;IAED,OAAO,CAAC,MAAM;IASd,QAAQ,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,GAAE,MAAY,GAAG,IAAI;IAQtF,QAAQ,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,IAAI;IAUpC;;;OAGG;IACH,UAAU,CACR,CAAC,EAAE,MAAM,EACT,CAAC,EAAE,MAAM,EACT,CAAC,EAAE,MAAM,EACT,CAAC,EAAE,MAAM,EACT,CAAC,EAAE,MAAM,EACT,CAAC,EAAE,MAAM,EACT,IAAI,GAAE,SAAoB,GACzB,IAAI;IAgDP,8EAA8E;IAC9E,KAAK,CAAC,CAAC,GAAE,MAAU,EAAE,CAAC,GAAE,MAAU,EAAE,CAAC,GAAE,MAAU,EAAE,CAAC,GAAE,MAAU,GAAG,IAAI;IASvE,wDAAwD;IACxD,SAAS,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,WAAW;IAmBlE,wEAAwE;IACxE,SAAS,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,GAAG,IAAI;CAa3D"}