Skip to content

packages/core/src/features/textures/lib/presentation.ts

Read as Markdown

This is the source snapshot used to build these API details. View this revision on GitHub.

Back to reference

1 import type { ReactionDiffusionProps } from './reaction-diffusion.js';
2 import type { ProceduralTextureProps } from './procedural-texture.js';
3 import { rasterizeField } from "./wasm/memory.js";
4 /**
5  * Borrowed sources are valid only during the synchronous renderer callback.
6  *
7  * @see {@link TextureRenderer}
8  */
9 export interface TextureRenderFrame {
10   /** Current offscreen texture raster available to the presentation callback. See {@link TextureRenderFrame}. */
11   readonly source: OffscreenCanvas;
12   /** White with texture intensity as alpha, allocated lazily on first access. */
13   readonly mask: OffscreenCanvas;
14   /**
15    * Horizontal extent in the units of the containing geometry or surface. See
16    * {@link TextureRenderFrame}.
17    */
18   readonly width: number;
19   /**
20    * Vertical extent in the units of the containing geometry or surface. See
21    * {@link TextureRenderFrame}.
22    */
23   readonly height: number;
24   /** Width of the underlying texture raster in pixels. See {@link TextureRenderFrame}. */
25   readonly textureWidth: number;
26   /** Height of the underlying texture raster in pixels. See {@link TextureRenderFrame}. */
27   readonly textureHeight: number;
28 }
29 /**
30  * Custom presentation callback receiving an offscreen context and the current texture raster
31  * frame.
32  *
33  * @param context - Offscreen drawing context to paint into.
34  * @param texture - Current texture surface and frame metadata. See {@link TextureRenderFrame}.
35  *
36  * @see {@link TextureRenderFrame}
37  * @see {@link ReactionDiffusionProps}
38  * @see {@link ProceduralTextureProps}
39  */
40 export type TextureRenderer = (
41   context: OffscreenCanvasRenderingContext2D,
42   texture: Readonly<TextureRenderFrame>,
43 ) => void;
44 
45 export class TextureRaster {
46   readonly surface: OffscreenCanvas;
47   readonly context: OffscreenCanvasRenderingContext2D;
48   private maskRaster?: () => ImageData;
49   private maskSurface?: OffscreenCanvas;
50   private maskContext?: OffscreenCanvasRenderingContext2D;
51   private maskDirty = true;
52   constructor(
53     readonly width: number,
54     readonly height: number,
55   ) {
56     this.surface = new OffscreenCanvas(width, height);
57     const context = this.surface.getContext("2d");
58     if (!context) {
59       this.surface.width = this.surface.height = 0;
60       throw new Error("Textures require offscreen Canvas 2D.");
61     }
62     this.context = context;
63   }
64   update(field: Float32Array, lut: Uint8ClampedArray, gain = 1) {
65     const raster = rasterizeField(field, lut, gain);
66     this.maskRaster = raster.mask;
67     this.context.putImageData(raster.image, 0, 0);
68     this.maskDirty = true;
69   }
70   private getMask(): OffscreenCanvas {
71     if (!this.maskSurface) {
72       this.maskSurface = new OffscreenCanvas(this.width, this.height);
73       this.maskContext = this.maskSurface.getContext("2d") ?? undefined;
74       if (!this.maskContext)
75         throw new Error("Textures require offscreen Canvas 2D.");
76     }
77     if (this.maskDirty) {
78       if (!this.maskRaster)
79         throw new Error("Texture mask requires a live WASM raster.");
80       this.maskContext!.putImageData(this.maskRaster(), 0, 0);
81       this.maskDirty = false;
82     }
83     return this.maskSurface;
84   }
85   draw(
86     ctx: OffscreenCanvasRenderingContext2D,
87     width: number,
88     height: number,
89     backingWidth: number,
90     backingHeight: number,
91     render?: TextureRenderer,
92   ) {
93     ctx.save();
94     try {
95       ctx.resetTransform();
96       ctx.clearRect(0, 0, backingWidth, backingHeight);
97       if (width <= 0 || height <= 0) return;
98       ctx.scale(backingWidth / width, backingHeight / height);
99       if (render) {
100         const raster = this;
101         const result: unknown = render(
102           ctx,
103           Object.freeze({
104             source: this.surface,
105             get mask() {
106               return raster.getMask();
107             },
108             width,
109             height,
110             textureWidth: this.width,
111             textureHeight: this.height,
112           }),
113         );
114         if (result && typeof (result as { then?: unknown }).then === "function")
115           throw new TypeError("Texture render callbacks must be synchronous.");
116       } else ctx.drawImage(this.surface, 0, 0, width, height);
117     } finally {
118       ctx.restore();
119     }
120   }
121   dispose() {
122     this.surface.width = this.surface.height = 0;
123     if (this.maskSurface) this.maskSurface.width = this.maskSurface.height = 0;
124     this.maskSurface = undefined;
125     this.maskContext = undefined;
126 
127     this.maskRaster = undefined;
128   }
129 }
130 

Documentation built with @pibbl/core 0.0.2, revision 272a94a. ALPHA — NOT FOR PRODUCTION USE.