napari-js 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.
Files changed (49) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +83 -0
  3. package/dist/cache/lru.d.ts +19 -0
  4. package/dist/camera/camera.d.ts +25 -0
  5. package/dist/camera/camera3d.d.ts +28 -0
  6. package/dist/camera/controls.d.ts +7 -0
  7. package/dist/camera/controls3d.d.ts +6 -0
  8. package/dist/color/checkerboard.d.ts +5 -0
  9. package/dist/color/colormap.d.ts +26 -0
  10. package/dist/color/display-pipeline.d.ts +22 -0
  11. package/dist/color/histogram.d.ts +14 -0
  12. package/dist/color/label-colormap.d.ts +7 -0
  13. package/dist/color/lut.d.ts +9 -0
  14. package/dist/engine/canvas.d.ts +20 -0
  15. package/dist/engine/device.d.ts +23 -0
  16. package/dist/engine/readback.d.ts +13 -0
  17. package/dist/engine/renderer.d.ts +42 -0
  18. package/dist/engine/viewport.d.ts +12 -0
  19. package/dist/index.d.ts +34 -0
  20. package/dist/io/pyramid.d.ts +41 -0
  21. package/dist/io/texture-source.d.ts +68 -0
  22. package/dist/layers/image-layer.d.ts +47 -0
  23. package/dist/layers/labels-layer.d.ts +32 -0
  24. package/dist/layers/layer.d.ts +32 -0
  25. package/dist/layers/points-layer.d.ts +59 -0
  26. package/dist/layers/volume-layer.d.ts +46 -0
  27. package/dist/math/mat4.d.ts +22 -0
  28. package/dist/napari-js.js +1986 -0
  29. package/dist/napari-js.js.map +1 -0
  30. package/dist/picking/pick.d.ts +6 -0
  31. package/dist/scene/dims.d.ts +20 -0
  32. package/dist/scene/events.d.ts +9 -0
  33. package/dist/scene/layer-list.d.ts +16 -0
  34. package/dist/scene/viewer-model.d.ts +20 -0
  35. package/dist/version.d.ts +1 -0
  36. package/dist/viewer.d.ts +76 -0
  37. package/dist/visuals/blend.d.ts +6 -0
  38. package/dist/visuals/format-plan.d.ts +19 -0
  39. package/dist/visuals/image-colormap-shader.d.ts +1 -0
  40. package/dist/visuals/image-visual.d.ts +45 -0
  41. package/dist/visuals/labels-shader.d.ts +1 -0
  42. package/dist/visuals/labels-visual.d.ts +23 -0
  43. package/dist/visuals/layer-visual.d.ts +23 -0
  44. package/dist/visuals/points-shader.d.ts +1 -0
  45. package/dist/visuals/points-visual.d.ts +22 -0
  46. package/dist/visuals/tiled-image-visual.d.ts +46 -0
  47. package/dist/visuals/volume-shader.d.ts +1 -0
  48. package/dist/visuals/volume-visual.d.ts +32 -0
  49. package/package.json +60 -0
@@ -0,0 +1,76 @@
1
+ import { ViewerModel } from './scene/viewer-model';
2
+ import { Camera } from './camera/camera';
3
+ import { LayerList } from './scene/layer-list';
4
+ import { ImageLayer, ImageLayerOptions } from './layers/image-layer';
5
+ import { PointsLayer, PointsLayerOptions } from './layers/points-layer';
6
+ import { LabelsLayer, LabelsLayerOptions } from './layers/labels-layer';
7
+ import { VolumeLayer, VolumeLayerOptions } from './layers/volume-layer';
8
+ import { ImageInput } from './io/texture-source';
9
+ import { Dims } from './scene/dims';
10
+ import { Camera3D } from './camera/camera3d';
11
+ import { PixelData } from './engine/readback';
12
+ import { Histogram } from './color/histogram';
13
+ export interface ViewerOptions {
14
+ canvas: HTMLCanvasElement;
15
+ /** Background clear color (RGBA 0..1). */
16
+ background?: GPUColor;
17
+ /** Attach pointer/wheel pan-zoom controls (default true). */
18
+ controls?: boolean;
19
+ }
20
+ /**
21
+ * The napari-js viewer: a headless {@link ViewerModel} (layers + camera) plus a WebGPU
22
+ * renderer. Construction kicks off async device acquisition — await {@link ready} before
23
+ * the first render. Adding layers / mutating display props schedules a coalesced redraw.
24
+ */
25
+ export declare class Viewer {
26
+ readonly ready: Promise<void>;
27
+ readonly model: ViewerModel;
28
+ private readonly canvas;
29
+ private readonly background;
30
+ private readonly useControls;
31
+ private ctx?;
32
+ private target?;
33
+ private renderer?;
34
+ private detachControls?;
35
+ private lastControlsNdisplay?;
36
+ private frameScheduled;
37
+ private firstImageFitted;
38
+ constructor(options: ViewerOptions);
39
+ get camera(): Camera;
40
+ get layers(): LayerList;
41
+ get dims(): Dims;
42
+ get camera3d(): Camera3D;
43
+ get device(): GPUDevice | undefined;
44
+ private init;
45
+ /** Attach the 2D pan/zoom or 3D orbit controls to match `dims.ndisplay`. */
46
+ private installControls;
47
+ private renderInputs;
48
+ /** Add an image layer. Accepts typed pixels or a decoded image (see {@link ImageInput}). */
49
+ addImage(input: ImageInput, opts?: ImageLayerOptions): ImageLayer;
50
+ /** Add a points (scatter) layer. Positions are `[x, y]` pairs in data coordinates. */
51
+ addPoints(positions: Float32Array | number[][], opts?: PointsLayerOptions): PointsLayer;
52
+ /** Add a labels (segmentation) layer from an 8-bit id image. */
53
+ addLabels(data: Uint8Array, width: number, height: number, opts?: LabelsLayerOptions): LabelsLayer;
54
+ /**
55
+ * Add a 3D volume layer (uint8 scalar field, x-fastest). Switches the viewer to 3D
56
+ * (`dims.ndisplay = 3`) and frames the orbit camera on the volume.
57
+ */
58
+ addVolume(data: Uint8Array, width: number, height: number, depth: number, opts?: VolumeLayerOptions): VolumeLayer;
59
+ /** Convert canvas client coordinates to data/world coordinates (for picking). */
60
+ canvasToWorld(clientX: number, clientY: number): [number, number];
61
+ private maybeFitFirst;
62
+ /** Request a coalesced redraw on the next animation frame. No-op until {@link ready}. */
63
+ requestRender(): void;
64
+ private renderFrame;
65
+ private allLayers;
66
+ /**
67
+ * Read back the composited displayed pixels as RGBA8 (top row first), by rendering the
68
+ * current scene into an offscreen texture at the canvas's device-pixel size.
69
+ */
70
+ readDisplayedPixels(): Promise<PixelData>;
71
+ /** Composite the displayed image to a PNG `Blob`. */
72
+ screenshot(): Promise<Blob>;
73
+ /** Luminance histogram (over `bins` bins) of the currently displayed composite. */
74
+ histogram(bins?: number): Promise<Histogram>;
75
+ dispose(): void;
76
+ }
@@ -0,0 +1,6 @@
1
+ import { BlendMode } from '../layers/layer';
2
+ /**
3
+ * Map a {@link BlendMode} to a WebGPU blend state for premultiplied-alpha output.
4
+ * `opaque` returns `undefined` (blending disabled).
5
+ */
6
+ export declare function blendStateFor(mode: BlendMode): GPUBlendState | undefined;
@@ -0,0 +1,19 @@
1
+ import { PixelDtype } from '../io/texture-source';
2
+ /** How a scalar/RGBA source maps onto a GPU texture (shared by single-image and tiled paths). */
3
+ export interface FormatPlan {
4
+ format: GPUTextureFormat;
5
+ bytesPerPixel: number;
6
+ /** Whether the texture can be linearly filtered (drives sampler + bind-group layout). */
7
+ filterable: boolean;
8
+ /** Factor applied to contrast limits so they match the shader's sample space. */
9
+ sampleScale: number;
10
+ isRgba: boolean;
11
+ }
12
+ /**
13
+ * Pick the texture plan for given channels/dtype. uint8 scalar → `r8unorm`; RGBA(uint8) →
14
+ * `rgba8unorm` (both normalized, so clim scales by 1/255); uint16/float32 scalar → `r32float`
15
+ * with native-unit windowing (clim scale 1), filterable only when `float32Filterable`.
16
+ */
17
+ export declare function formatPlanFor(channels: 1 | 4, dtype: PixelDtype, float32Filterable: boolean): FormatPlan;
18
+ /** Convert tile/image pixels to the upload representation for `format` (uint16 → float32). */
19
+ export declare function toUploadData(data: Uint8Array | Uint16Array | Float32Array, format: GPUTextureFormat): Uint8Array | Float32Array;
@@ -0,0 +1 @@
1
+ export declare const IMAGE_COLORMAP_SHADER = "\nstruct U {\n mvp : mat4x4<f32>,\n imageSize : vec2<f32>,\n origin : vec2<f32>, // data-space origin of this quad (0 for a full image; tile origin for tiles)\n params : vec4<f32>, // climLo, climHi, gamma, opacity (clim already normalized to sample space)\n flags : vec4<f32>, // isRgba, invert, 0, 0\n};\n\n@group(0) @binding(0) var<uniform> u : U;\n@group(0) @binding(1) var srcSamp : sampler;\n@group(0) @binding(2) var srcTex : texture_2d<f32>;\n@group(0) @binding(3) var lutSamp : sampler;\n@group(0) @binding(4) var lutTex : texture_2d<f32>;\n\nstruct VSOut {\n @builtin(position) position : vec4<f32>,\n @location(0) uv : vec2<f32>,\n};\n\n@vertex\nfn vs(@builtin(vertex_index) vi : u32) -> VSOut {\n var corners = array<vec2<f32>, 6>(\n vec2<f32>(0.0, 0.0), vec2<f32>(1.0, 0.0), vec2<f32>(0.0, 1.0),\n vec2<f32>(0.0, 1.0), vec2<f32>(1.0, 0.0), vec2<f32>(1.0, 1.0),\n );\n let c = corners[vi];\n var out : VSOut;\n out.position = u.mvp * vec4<f32>(u.origin + c * u.imageSize, 0.0, 1.0);\n out.uv = c;\n return out;\n}\n\n@fragment\nfn fs(in : VSOut) -> @location(0) vec4<f32> {\n let raw = textureSample(srcTex, srcSamp, in.uv);\n let climLo = u.params.x;\n let climHi = u.params.y;\n let gamma = u.params.z;\n let opacity = u.params.w;\n let denom = max(climHi - climLo, 1e-8);\n\n // Scalar path: window \u2192 invert \u2192 gamma \u2192 LUT.\n var t = clamp((raw.r - climLo) / denom, 0.0, 1.0);\n if (u.flags.y > 0.5) { t = 1.0 - t; }\n t = pow(t, gamma);\n let mapped = textureSample(lutTex, lutSamp, vec2<f32>(t, 0.5)).rgb;\n\n // RGB path: per-channel window \u2192 gamma.\n var direct = clamp((raw.rgb - vec3<f32>(climLo)) / vec3<f32>(denom), vec3<f32>(0.0), vec3<f32>(1.0));\n direct = pow(direct, vec3<f32>(gamma));\n\n let isRgba = u.flags.x > 0.5;\n let rgb = select(mapped, direct, isRgba);\n let a = select(opacity, raw.a * opacity, isRgba);\n return vec4<f32>(rgb * a, a);\n}\n";
@@ -0,0 +1,45 @@
1
+ import { ImageLayer } from '../layers/image-layer';
2
+ import { LayerVisual, RenderView } from './layer-visual';
3
+ /**
4
+ * Binds one {@link ImageLayer} to a WebGPU pipeline: uploads the source texture and colormap
5
+ * LUT, and draws the layer each frame with its current display uniforms (the napari
6
+ * `Vispy*Layer` wrapper analog). Supports uint8 (r8unorm/rgba8unorm) and uint16/float32
7
+ * (r32float, native-precision windowing). Uses an explicit bind-group layout so unfilterable
8
+ * float textures render correctly when `float32-filterable` is unavailable.
9
+ */
10
+ export declare class ImageVisual implements LayerVisual {
11
+ private readonly device;
12
+ private readonly format;
13
+ private readonly layer;
14
+ readonly ndisplay: 2 | 3;
15
+ private readonly module;
16
+ private readonly uniformBuffer;
17
+ private readonly scratch;
18
+ private readonly bindGroupLayout;
19
+ private readonly pipelineLayout;
20
+ private readonly plan;
21
+ private texture;
22
+ private lutTexture;
23
+ private srcSampler;
24
+ private lutSampler;
25
+ private pipeline;
26
+ private bindGroup;
27
+ private currentBlend;
28
+ private currentInterp;
29
+ private lutVersion;
30
+ constructor(device: GPUDevice, format: GPUTextureFormat, layer: ImageLayer, opts?: {
31
+ float32Filterable: boolean;
32
+ });
33
+ private buildBindGroupLayout;
34
+ private uploadTexture;
35
+ private createLutTexture;
36
+ private writeLut;
37
+ private createSrcSampler;
38
+ private buildPipeline;
39
+ private buildBindGroup;
40
+ /** Reconcile GPU state with the layer's current properties (cheap; called before draw). */
41
+ sync(): void;
42
+ /** Encode a draw of this layer for the current view. */
43
+ draw(pass: GPURenderPassEncoder, view: RenderView): void;
44
+ dispose(): void;
45
+ }
@@ -0,0 +1 @@
1
+ export declare const LABELS_SHADER = "\nstruct U {\n mvp : mat4x4<f32>,\n imageSize : vec2<f32>,\n origin : vec2<f32>,\n params : vec4<f32>, // selectedLabel, showSelectedOnly, opacity, lutSize\n};\n@group(0) @binding(0) var<uniform> u : U;\n@group(0) @binding(1) var samp : sampler;\n@group(0) @binding(2) var labelTex : texture_2d<f32>;\n@group(0) @binding(3) var lutSamp : sampler;\n@group(0) @binding(4) var lut : texture_2d<f32>;\n\nstruct VSOut {\n @builtin(position) position : vec4<f32>,\n @location(0) uv : vec2<f32>,\n};\n\n@vertex\nfn vs(@builtin(vertex_index) vi : u32) -> VSOut {\n var corners = array<vec2<f32>, 6>(\n vec2<f32>(0.0, 0.0), vec2<f32>(1.0, 0.0), vec2<f32>(0.0, 1.0),\n vec2<f32>(0.0, 1.0), vec2<f32>(1.0, 0.0), vec2<f32>(1.0, 1.0),\n );\n let c = corners[vi];\n var out : VSOut;\n out.position = u.mvp * vec4<f32>(u.origin + c * u.imageSize, 0.0, 1.0);\n out.uv = c;\n return out;\n}\n\n@fragment\nfn fs(in : VSOut) -> @location(0) vec4<f32> {\n let raw = textureSample(labelTex, samp, in.uv).r;\n let id = round(raw * 255.0);\n let lutSize = u.params.w;\n var rgba = textureSample(lut, lutSamp, vec2<f32>((id + 0.5) / lutSize, 0.5));\n if (u.params.y > 0.5 && abs(id - u.params.x) > 0.5) { rgba.a = 0.0; } // show-selected-only\n let a = rgba.a * u.params.z;\n return vec4<f32>(rgba.rgb * a, a);\n}\n";
@@ -0,0 +1,23 @@
1
+ import { LabelsLayer } from '../layers/labels-layer';
2
+ import { LayerVisual, RenderView } from './layer-visual';
3
+ /** Renders a {@link LabelsLayer}: nearest-sampled id texture + cyclic palette LUT. */
4
+ export declare class LabelsVisual implements LayerVisual {
5
+ private readonly device;
6
+ private readonly format;
7
+ private readonly layer;
8
+ readonly ndisplay: 2 | 3;
9
+ private readonly module;
10
+ private readonly uniformBuffer;
11
+ private readonly scratch;
12
+ private readonly texture;
13
+ private readonly lutTexture;
14
+ private readonly sampler;
15
+ private readonly bindGroup;
16
+ private pipeline;
17
+ private currentBlend;
18
+ constructor(device: GPUDevice, format: GPUTextureFormat, layer: LabelsLayer);
19
+ private buildPipeline;
20
+ sync(): void;
21
+ draw(pass: GPURenderPassEncoder, view: RenderView): void;
22
+ dispose(): void;
23
+ }
@@ -0,0 +1,23 @@
1
+ import { Camera } from '../camera/camera';
2
+ import { Camera3D } from '../camera/camera3d';
3
+ /** Per-frame view state handed to every visual. 2D visuals use `camera2d`; volume uses `camera3d`. */
4
+ export interface RenderView {
5
+ camera2d: Camera;
6
+ camera3d: Camera3D;
7
+ /** CSS-pixel projection size. */
8
+ vw: number;
9
+ vh: number;
10
+ /** Current z-slice (2D stacks). */
11
+ z: number;
12
+ ndisplay: 2 | 3;
13
+ }
14
+ /** A GPU renderer for one layer. The renderer draws only visuals whose `ndisplay` matches. */
15
+ export interface LayerVisual {
16
+ /** Display dimensionality this visual renders in (2 for image/points/labels, 3 for volume). */
17
+ readonly ndisplay: 2 | 3;
18
+ /** Reconcile GPU state with the layer's current display properties (cheap; pre-draw). */
19
+ sync(): void;
20
+ /** Encode draws for the given view. */
21
+ draw(pass: GPURenderPassEncoder, view: RenderView): void;
22
+ dispose(): void;
23
+ }
@@ -0,0 +1 @@
1
+ export declare const POINTS_SHADER = "\nstruct U {\n mvp : mat4x4<f32>,\n params : vec4<f32>, // symbolCode (0=disc,1=ring,2=square), opacity, 0, 0\n};\n@group(0) @binding(0) var<uniform> u : U;\n\nstruct VSOut {\n @builtin(position) position : vec4<f32>,\n @location(0) local : vec2<f32>,\n @location(1) face : vec4<f32>,\n @location(2) border : vec4<f32>,\n @location(3) borderFrac : f32,\n};\n\n@vertex\nfn vs(\n @builtin(vertex_index) vi : u32,\n @location(0) pos : vec2<f32>,\n @location(1) size : f32,\n @location(2) face : vec4<f32>,\n @location(3) border : vec4<f32>,\n @location(4) borderWidth : f32,\n) -> VSOut {\n var corners = array<vec2<f32>, 6>(\n vec2<f32>(-1.0, -1.0), vec2<f32>(1.0, -1.0), vec2<f32>(-1.0, 1.0),\n vec2<f32>(-1.0, 1.0), vec2<f32>(1.0, -1.0), vec2<f32>(1.0, 1.0),\n );\n let c = corners[vi];\n let world = pos + c * (size * 0.5);\n var out : VSOut;\n out.position = u.mvp * vec4<f32>(world, 0.0, 1.0);\n out.local = c;\n out.face = face;\n out.border = border;\n out.borderFrac = clamp(borderWidth / max(size, 1e-6), 0.0, 1.0);\n return out;\n}\n\n@fragment\nfn fs(in : VSOut) -> @location(0) vec4<f32> {\n let symbol = u.params.x;\n let opacity = u.params.y;\n var d : f32;\n if (symbol > 1.5) { d = max(abs(in.local.x), abs(in.local.y)); } // square\n else { d = length(in.local); } // disc / ring\n\n let aa = max(fwidth(d), 1e-5);\n let inside = 1.0 - smoothstep(1.0 - aa, 1.0, d);\n if (inside <= 0.0) { discard; }\n\n let borderEdge = 1.0 - in.borderFrac;\n let borderMix = smoothstep(borderEdge - aa, borderEdge, d);\n let rgb = mix(in.face.rgb, in.border.rgb, borderMix);\n var a = mix(in.face.a, in.border.a, borderMix);\n if (symbol > 0.5 && symbol < 1.5) { a = a * borderMix; } // ring: only the border ring shows\n a = a * inside * opacity;\n return vec4<f32>(rgb * a, a);\n}\n";
@@ -0,0 +1,22 @@
1
+ import { PointsLayer } from '../layers/points-layer';
2
+ import { LayerVisual, RenderView } from './layer-visual';
3
+ /** Renders a {@link PointsLayer} as instanced SDF markers. */
4
+ export declare class PointsVisual implements LayerVisual {
5
+ private readonly device;
6
+ private readonly format;
7
+ private readonly layer;
8
+ readonly ndisplay: 2 | 3;
9
+ private readonly module;
10
+ private readonly uniformBuffer;
11
+ private readonly scratch;
12
+ private instanceBuffer;
13
+ private pipeline;
14
+ private currentBlend;
15
+ private dataVersion;
16
+ constructor(device: GPUDevice, format: GPUTextureFormat, layer: PointsLayer);
17
+ private buildPipeline;
18
+ sync(): void;
19
+ private rebuildInstances;
20
+ draw(pass: GPURenderPassEncoder, view: RenderView): void;
21
+ dispose(): void;
22
+ }
@@ -0,0 +1,46 @@
1
+ import { ImageLayer } from '../layers/image-layer';
2
+ import { LayerVisual, RenderView } from './layer-visual';
3
+ /**
4
+ * Renders a pyramidal {@link TiledSource}: selects a pyramid level from zoom, draws the
5
+ * visible tiles (fetched lazily and cached on the GPU with LRU eviction), and underlays the
6
+ * coarsest level so pans/zooms never flash blank. Z-scrubbing fetches the new slice's tiles
7
+ * (cache keyed by z, so revisited slices are instant). Mirrors napari's tiled-image path.
8
+ */
9
+ export declare class TiledImageVisual implements LayerVisual {
10
+ private readonly device;
11
+ private readonly format;
12
+ private readonly layer;
13
+ private readonly opts;
14
+ readonly ndisplay: 2 | 3;
15
+ private readonly source;
16
+ private readonly plan;
17
+ private readonly module;
18
+ private readonly bindGroupLayout;
19
+ private readonly pipelineLayout;
20
+ private readonly lutTexture;
21
+ private readonly lutSampler;
22
+ private readonly scratch;
23
+ private readonly cache;
24
+ private readonly pending;
25
+ private srcSampler;
26
+ private pipeline;
27
+ private currentBlend;
28
+ private currentInterp;
29
+ private lutVersion;
30
+ private disposed;
31
+ constructor(device: GPUDevice, format: GPUTextureFormat, layer: ImageLayer, opts: {
32
+ float32Filterable: boolean;
33
+ onNeedsRedraw: () => void;
34
+ });
35
+ private buildBindGroupLayout;
36
+ private buildPipeline;
37
+ private createSrcSampler;
38
+ private writeLut;
39
+ private keyOf;
40
+ /** Return a ready tile, or kick an async fetch and return undefined. */
41
+ private ensureTile;
42
+ private drawTile;
43
+ sync(): void;
44
+ draw(pass: GPURenderPassEncoder, rv: RenderView): void;
45
+ dispose(): void;
46
+ }
@@ -0,0 +1 @@
1
+ export declare const VOLUME_SHADER = "\nstruct U {\n invMvp : mat4x4<f32>,\n params : vec4<f32>, // climLo, climHi (normalized 0..1), gamma, opacity\n params2 : vec4<f32>, // renderingCode (0=mip,1=translucent,2=iso), isoThreshold, steps, 0\n};\n@group(0) @binding(0) var<uniform> u : U;\n@group(0) @binding(1) var volSamp : sampler;\n@group(0) @binding(2) var volTex : texture_3d<f32>;\n@group(0) @binding(3) var lutSamp : sampler;\n@group(0) @binding(4) var lut : texture_2d<f32>;\n\nstruct VSOut {\n @builtin(position) position : vec4<f32>,\n @location(0) ndc : vec2<f32>,\n};\n\n@vertex\nfn vs(@builtin(vertex_index) vi : u32) -> VSOut {\n var corners = array<vec2<f32>, 6>(\n vec2<f32>(-1.0, -1.0), vec2<f32>(1.0, -1.0), vec2<f32>(-1.0, 1.0),\n vec2<f32>(-1.0, 1.0), vec2<f32>(1.0, -1.0), vec2<f32>(1.0, 1.0),\n );\n let c = corners[vi];\n var out : VSOut;\n out.position = vec4<f32>(c, 0.0, 1.0);\n out.ndc = c;\n return out;\n}\n\nfn unproject(ndc : vec2<f32>, z : f32) -> vec3<f32> {\n let p = u.invMvp * vec4<f32>(ndc, z, 1.0);\n return p.xyz / p.w;\n}\n\nfn sampleWindowed(pos : vec3<f32>) -> f32 {\n let s = textureSampleLevel(volTex, volSamp, pos, 0.0).r;\n let lo = u.params.x;\n let hi = u.params.y;\n let t = clamp((s - lo) / max(hi - lo, 1e-6), 0.0, 1.0);\n return pow(t, u.params.z);\n}\n\nfn lutColor(t : f32) -> vec3<f32> {\n return textureSampleLevel(lut, lutSamp, vec2<f32>(clamp(t, 0.0, 1.0), 0.5), 0.0).rgb;\n}\n\n@fragment\nfn fs(in : VSOut) -> @location(0) vec4<f32> {\n let ro = unproject(in.ndc, 0.0); // near point, volume space\n let rf = unproject(in.ndc, 1.0); // far point\n let rd = rf - ro;\n\n // Intersect ray with the unit box [0,1]^3 (parameter t along rd).\n let inv = 1.0 / rd;\n let t0 = (vec3<f32>(0.0) - ro) * inv;\n let t1 = (vec3<f32>(1.0) - ro) * inv;\n let tmin = min(t0, t1);\n let tmax = max(t0, t1);\n let tNear = max(max(max(tmin.x, tmin.y), tmin.z), 0.0);\n let tFar = min(min(tmax.x, tmax.y), 1.0 * min(tmax.z, 1.0e9));\n if (tFar <= tNear) { discard; }\n\n let steps = i32(u.params2.z);\n let dt = (tFar - tNear) / f32(steps);\n let mode = u.params2.x;\n let opacity = u.params.w;\n\n var maxT = 0.0;\n var col = vec3<f32>(0.0);\n var acc = 0.0;\n\n for (var i = 0; i < steps; i = i + 1) {\n let t = tNear + (f32(i) + 0.5) * dt;\n let pos = ro + rd * t;\n let w = sampleWindowed(pos);\n\n if (mode < 0.5) {\n // MIP\n maxT = max(maxT, w);\n } else if (mode < 1.5) {\n // Front-to-back translucent DVR\n let a = w * opacity;\n let c = lutColor(w);\n col = col + (1.0 - acc) * c * a;\n acc = acc + (1.0 - acc) * a;\n if (acc >= 0.99) { break; }\n } else {\n // Iso-surface: first crossing \u2192 gradient + lambert\n if (w >= u.params2.y) {\n let e = 1.0 / 128.0;\n let gx = sampleWindowed(pos + vec3<f32>(e, 0.0, 0.0)) - sampleWindowed(pos - vec3<f32>(e, 0.0, 0.0));\n let gy = sampleWindowed(pos + vec3<f32>(0.0, e, 0.0)) - sampleWindowed(pos - vec3<f32>(0.0, e, 0.0));\n let gz = sampleWindowed(pos + vec3<f32>(0.0, 0.0, e)) - sampleWindowed(pos - vec3<f32>(0.0, 0.0, e));\n let n = normalize(vec3<f32>(gx, gy, gz) + vec3<f32>(1e-5));\n let lightDir = normalize(vec3<f32>(0.5, 0.7, 1.0));\n let lambert = max(dot(n, lightDir), 0.0) * 0.8 + 0.2;\n col = lutColor(w) * lambert;\n acc = opacity;\n break;\n }\n }\n }\n\n if (mode < 0.5) {\n if (maxT <= 0.0) { discard; }\n col = lutColor(maxT);\n acc = opacity;\n }\n if (acc <= 0.0) { discard; }\n return vec4<f32>(col * acc, acc);\n}\n";
@@ -0,0 +1,32 @@
1
+ import { VolumeLayer } from '../layers/volume-layer';
2
+ import { LayerVisual, RenderView } from './layer-visual';
3
+ /**
4
+ * Renders a {@link VolumeLayer} by fragment raymarching a 3D texture (see volume-shader.ts).
5
+ * Builds `invMVP` (clip → volume [0,1]^3 space) each frame from the 3D camera and a model that
6
+ * centers the `[w,h,d]` box at the origin. NJ-5+ uploads uint8 volumes (r8unorm 3D).
7
+ */
8
+ export declare class VolumeVisual implements LayerVisual {
9
+ private readonly device;
10
+ private readonly format;
11
+ private readonly layer;
12
+ readonly ndisplay: 2 | 3;
13
+ private readonly module;
14
+ private readonly uniformBuffer;
15
+ private readonly scratch;
16
+ private readonly texture;
17
+ private readonly lutTexture;
18
+ private readonly volSampler;
19
+ private readonly lutSampler;
20
+ private readonly model;
21
+ private bindGroup;
22
+ private pipeline;
23
+ private currentBlend;
24
+ private lutVersion;
25
+ constructor(device: GPUDevice, format: GPUTextureFormat, layer: VolumeLayer);
26
+ private buildPipeline;
27
+ private buildBindGroup;
28
+ private writeLut;
29
+ sync(): void;
30
+ draw(pass: GPURenderPassEncoder, view: RenderView): void;
31
+ dispose(): void;
32
+ }
package/package.json ADDED
@@ -0,0 +1,60 @@
1
+ {
2
+ "name": "napari-js",
3
+ "version": "0.1.0",
4
+ "description": "WebGPU rendering engine porting napari's visualization model to the browser.",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "author": "Baha Elkassaby",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/belkassaby/napari-js.git"
11
+ },
12
+ "homepage": "https://github.com/belkassaby/napari-js#readme",
13
+ "bugs": {
14
+ "url": "https://github.com/belkassaby/napari-js/issues"
15
+ },
16
+ "sideEffects": false,
17
+ "keywords": [
18
+ "webgpu",
19
+ "napari",
20
+ "image",
21
+ "visualization",
22
+ "microscopy",
23
+ "rendering"
24
+ ],
25
+ "files": [
26
+ "dist"
27
+ ],
28
+ "main": "./dist/napari-js.js",
29
+ "module": "./dist/napari-js.js",
30
+ "types": "./dist/index.d.ts",
31
+ "exports": {
32
+ ".": {
33
+ "types": "./dist/index.d.ts",
34
+ "import": "./dist/napari-js.js"
35
+ }
36
+ },
37
+ "scripts": {
38
+ "dev": "vite",
39
+ "build": "vite build",
40
+ "preview": "vite preview",
41
+ "test": "vitest run",
42
+ "test:watch": "vitest",
43
+ "typecheck": "tsc --noEmit",
44
+ "lint": "eslint .",
45
+ "lint:fix": "eslint --fix .",
46
+ "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\" \"playground/**/*.ts\" \"*.{ts,json,md}\" \"docs/**/*.md\" \"index.html\"",
47
+ "format:check": "prettier --check \"src/**/*.ts\" \"test/**/*.ts\" \"playground/**/*.ts\" \"*.{ts,json,md}\" \"docs/**/*.md\" \"index.html\"",
48
+ "prepublishOnly": "npm run typecheck && npm run test && npm run build"
49
+ },
50
+ "devDependencies": {
51
+ "@webgpu/types": "^0.1.44",
52
+ "eslint": "^9.9.0",
53
+ "prettier": "^3.8.3",
54
+ "typescript": "^5.5.4",
55
+ "typescript-eslint": "^8.2.0",
56
+ "vite": "^5.4.0",
57
+ "vite-plugin-dts": "^4.2.1",
58
+ "vitest": "^2.0.5"
59
+ }
60
+ }