packages/core/src/features/textures/lib/presentation.ts
This is the source snapshot used to build these API details. View this revision on GitHub.
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 version
Section titled “Documentation version”Documentation built with @pibbl/core 0.0.2, revision 2dccb19. ALPHA — NOT FOR PRODUCTION USE.