Skip to content

Adding VTK.wasm to a Project

There are two ways to get VTK.wasm into a web project:

  • HTML Script Tag — load a prebuilt bundle from a CDN, no build step required. Best for quick prototypes, demos, and embedding into existing pages.
  • Bundler — install the @kitware/vtk-wasm package and import it. Best for application development with a tool like Vite.

Either way you end up calling loadAsync; only how it reaches the page differs.

HTML Script Tag

Use VTK.wasm directly in an HTML file using a <script> tag without a build step.

The following examples rely on loading the vtk.umd.js bundle from a CDN. To focus on the initialization part, we've externalized the JS/WASM scene code since that part does not change.

Load WASM as a module

In this example we pre-load the WASM module, so we don't need to provide any URL when loading it.

html
<html>
  <head>
    <script
      src="/path/to/vtkWebAssembly.mjs"
      type="module"
    ></script>
    <script src="https://unpkg.com/@kitware/vtk-wasm/vtk-umd.js"></script>
    <script src="example.js"></script>
  </head>
  <body>
    <canvas id="vtk-wasm-window"></canvas>
    <script>
      vtkwasm.loadAsync().then((runtime) =>
        buildWASMScene(runtime.createStandaloneSession().vtk),
      );
    </script>
  </body>
</html>
js
function buildWASMScene(vtk, canvasSelector = "#vtk-wasm-window", titleText = "Sample VTK.wasm scene") {

  function createSharedTextProperty() {
    const textProperty = vtk.vtkTextProperty({fontSize: 22});
    return textProperty;
  }

  function createLookupTable(scalarRange) {
    const lut = vtk.vtkColorTransferFunction();
    lut.setColorSpaceToHSV();
    const colorSeries = vtk.vtkColorSeries({ colorScheme: 16 });
    const numColors = colorSeries.getNumberOfColors();
    const scalarDiff = (scalarRange[1] - scalarRange[0]) / numColors;
    for (let i = 0; i < numColors; i++) {
      const color = colorSeries.getColor(i);
      const t = scalarRange[0] + i * scalarDiff;
      lut.addRGBPoint(
        t,
        color[0] / 255,
        color[1] / 255,
        color[2] / 255,
      );
    }
    lut.build();
    return lut;
  }

  function createTitleTextActor(titleText, textProperty) {
    const textActor = vtk.vtkTextActor({ input: titleText, textProperty });
    const position = textActor.getPositionCoordinate();
    position.setCoordinateSystemToNormalizedViewport();
    return textActor;
  }

  // Create a VTK source. Output has a point data array named "Scalars" whose range is [0, PI].
  const shapes = vtk.vtkPartitionedDataSetCollectionSource({ numberOfShapes: 2 });
  const lut = createLookupTable([0.0, Math.PI]);

  const mapper = vtk.vtkCompositePolyDataMapper({ lookupTable: lut });
  mapper.setInputConnection(shapes.getOutputPort());
  const actor = vtk.vtkActor({ mapper, scale: [0.1, 0.1, 0.1] });
  actor.property.edgeVisibility = true;
  actor.property.edgeColor = [0.2, 0.2, 0.2];

  const textProperty = createSharedTextProperty();

  // Create an actor that displays the title.
  const titleTextActor = createTitleTextActor(titleText, textProperty);

  // Setup rendering part
  const renderer = vtk.vtkRenderer({ background: [0.384314, 0.364706, 0.352941] });
  renderer.addActor(actor);
  renderer.addActor(titleTextActor);
  renderer.resetCamera();

  // Create a RenderWindow and bind it to a canvas in the DOM
  const renderWindow = vtk.vtkRenderWindow({ canvasSelector });
  renderWindow.addRenderer(renderer);
  const interactor = vtk.vtkRenderWindowInteractor({
    canvasSelector,
    renderWindow,
  });
  interactor.interactorStyle.setCurrentStyleToTrackballCamera();

  // Create camera widget
  const cameraOrientation = vtk.vtkCameraOrientationWidget({ interactor, parentRenderer: renderer });
  cameraOrientation.enabled = true;

  // Display the scalar bar at the bottom with a horizontal orientation
  const scalarBarActor = vtk.vtkScalarBarActor({ 
    lookupTable: lut,
    title: "Scalars",
    titleTextProperty: textProperty,
    labelTextProperty: textProperty,
    annotationTextProperty: textProperty,
    unconstrainedFontSize: true,
  });
  const scalarBar = vtk.vtkScalarBarWidget({ scalarBarActor, interactor, defaultRenderer: renderer });
  const scalarBarRepresentation = scalarBar.getRepresentation();
  scalarBarRepresentation.setOrientation(0); // 1: vertical, 0: horizontal
  const lowerLeftPosition = scalarBarRepresentation.getPositionCoordinate();
  lowerLeftPosition.setValue([0.1, 0.05, 0.0]);
  scalarBar.enabled = true;

  // Trigger render and start interactor
  interactor.start();
}

Defer WASM loading

Since we didn't pre-load the WASM module here, we provide the URL where the WASM bundle can be found.

html
<html>
  <head>
    <script src="https://unpkg.com/@kitware/vtk-wasm/vtk-umd.js"></script>
    <script src="example.js"></script>
  </head>
  <body>
    <canvas id="vtk-wasm-window" tabindex="-1" onclick="focus()"></canvas>
    <script>
      vtkwasm.loadAsync({ url: "https://raw.githack.com/Kitware/vtk-wasm/dist/latest/vtk-wasm32-emscripten.tar.gz" })
      .then((runtime) => {
        const session = runtime.createStandaloneSession();
        buildWASMScene(session.vtk, "#vtk-wasm-window", "This scene passes the VTK.wasm bundle from GitLab registry to loadAsync()");
      });
    </script>
  </body>
</html>
js
function buildWASMScene(vtk, canvasSelector = "#vtk-wasm-window", titleText = "Sample VTK.wasm scene") {

  function createSharedTextProperty() {
    const textProperty = vtk.vtkTextProperty({fontSize: 22});
    return textProperty;
  }

  function createLookupTable(scalarRange) {
    const lut = vtk.vtkColorTransferFunction();
    lut.setColorSpaceToHSV();
    const colorSeries = vtk.vtkColorSeries({ colorScheme: 16 });
    const numColors = colorSeries.getNumberOfColors();
    const scalarDiff = (scalarRange[1] - scalarRange[0]) / numColors;
    for (let i = 0; i < numColors; i++) {
      const color = colorSeries.getColor(i);
      const t = scalarRange[0] + i * scalarDiff;
      lut.addRGBPoint(
        t,
        color[0] / 255,
        color[1] / 255,
        color[2] / 255,
      );
    }
    lut.build();
    return lut;
  }

  function createTitleTextActor(titleText, textProperty) {
    const textActor = vtk.vtkTextActor({ input: titleText, textProperty });
    const position = textActor.getPositionCoordinate();
    position.setCoordinateSystemToNormalizedViewport();
    return textActor;
  }

  // Create a VTK source. Output has a point data array named "Scalars" whose range is [0, PI].
  const shapes = vtk.vtkPartitionedDataSetCollectionSource({ numberOfShapes: 2 });
  const lut = createLookupTable([0.0, Math.PI]);

  const mapper = vtk.vtkCompositePolyDataMapper({ lookupTable: lut });
  mapper.setInputConnection(shapes.getOutputPort());
  const actor = vtk.vtkActor({ mapper, scale: [0.1, 0.1, 0.1] });
  actor.property.edgeVisibility = true;
  actor.property.edgeColor = [0.2, 0.2, 0.2];

  const textProperty = createSharedTextProperty();

  // Create an actor that displays the title.
  const titleTextActor = createTitleTextActor(titleText, textProperty);

  // Setup rendering part
  const renderer = vtk.vtkRenderer({ background: [0.384314, 0.364706, 0.352941] });
  renderer.addActor(actor);
  renderer.addActor(titleTextActor);
  renderer.resetCamera();

  // Create a RenderWindow and bind it to a canvas in the DOM
  const renderWindow = vtk.vtkRenderWindow({ canvasSelector });
  renderWindow.addRenderer(renderer);
  const interactor = vtk.vtkRenderWindowInteractor({
    canvasSelector,
    renderWindow,
  });
  interactor.interactorStyle.setCurrentStyleToTrackballCamera();

  // Create camera widget
  const cameraOrientation = vtk.vtkCameraOrientationWidget({ interactor, parentRenderer: renderer });
  cameraOrientation.enabled = true;

  // Display the scalar bar at the bottom with a horizontal orientation
  const scalarBarActor = vtk.vtkScalarBarActor({ 
    lookupTable: lut,
    title: "Scalars",
    titleTextProperty: textProperty,
    labelTextProperty: textProperty,
    annotationTextProperty: textProperty,
    unconstrainedFontSize: true,
  });
  const scalarBar = vtk.vtkScalarBarWidget({ scalarBarActor, interactor, defaultRenderer: renderer });
  const scalarBarRepresentation = scalarBar.getRepresentation();
  scalarBarRepresentation.setOrientation(0); // 1: vertical, 0: horizontal
  const lowerLeftPosition = scalarBarRepresentation.getPositionCoordinate();
  lowerLeftPosition.setValue([0.1, 0.05, 0.0]);
  scalarBar.enabled = true;

  // Trigger render and start interactor
  interactor.start();
}

Full Screen Viewer

Defer WASM loading with annotation

Here we tag the script to autoload WASM directly from the VTK repository's package registry; the VTK namespace is then reached by awaiting vtkwasm.ready. You can customize the wasm architecture and version by changing the data-url.

html
<html>
  <head>
    <script
      src="https://unpkg.com/@kitware/vtk-wasm/vtk-umd.js"
      id="vtk-wasm"
      data-url="https://raw.githack.com/Kitware/vtk-wasm/dist/latest/vtk-wasm32-emscripten.tar.gz"
    ></script>
    <script src="example.js"></script>
  </head>
  <body>
    <canvas id="vtk-wasm-window" tabindex="-1" onclick="focus()"></canvas>
    <script>
      vtkwasm.ready.then((vtk) => {
        buildWASMScene(vtk, "#vtk-wasm-window", "This scene points the data-url in script tag to the VTK.wasm bundle from GitLab registry");
      });
    </script>
  </body>
</html>
js
function buildWASMScene(vtk, canvasSelector = "#vtk-wasm-window", titleText = "Sample VTK.wasm scene") {

  function createSharedTextProperty() {
    const textProperty = vtk.vtkTextProperty({fontSize: 22});
    return textProperty;
  }

  function createLookupTable(scalarRange) {
    const lut = vtk.vtkColorTransferFunction();
    lut.setColorSpaceToHSV();
    const colorSeries = vtk.vtkColorSeries({ colorScheme: 16 });
    const numColors = colorSeries.getNumberOfColors();
    const scalarDiff = (scalarRange[1] - scalarRange[0]) / numColors;
    for (let i = 0; i < numColors; i++) {
      const color = colorSeries.getColor(i);
      const t = scalarRange[0] + i * scalarDiff;
      lut.addRGBPoint(
        t,
        color[0] / 255,
        color[1] / 255,
        color[2] / 255,
      );
    }
    lut.build();
    return lut;
  }

  function createTitleTextActor(titleText, textProperty) {
    const textActor = vtk.vtkTextActor({ input: titleText, textProperty });
    const position = textActor.getPositionCoordinate();
    position.setCoordinateSystemToNormalizedViewport();
    return textActor;
  }

  // Create a VTK source. Output has a point data array named "Scalars" whose range is [0, PI].
  const shapes = vtk.vtkPartitionedDataSetCollectionSource({ numberOfShapes: 2 });
  const lut = createLookupTable([0.0, Math.PI]);

  const mapper = vtk.vtkCompositePolyDataMapper({ lookupTable: lut });
  mapper.setInputConnection(shapes.getOutputPort());
  const actor = vtk.vtkActor({ mapper, scale: [0.1, 0.1, 0.1] });
  actor.property.edgeVisibility = true;
  actor.property.edgeColor = [0.2, 0.2, 0.2];

  const textProperty = createSharedTextProperty();

  // Create an actor that displays the title.
  const titleTextActor = createTitleTextActor(titleText, textProperty);

  // Setup rendering part
  const renderer = vtk.vtkRenderer({ background: [0.384314, 0.364706, 0.352941] });
  renderer.addActor(actor);
  renderer.addActor(titleTextActor);
  renderer.resetCamera();

  // Create a RenderWindow and bind it to a canvas in the DOM
  const renderWindow = vtk.vtkRenderWindow({ canvasSelector });
  renderWindow.addRenderer(renderer);
  const interactor = vtk.vtkRenderWindowInteractor({
    canvasSelector,
    renderWindow,
  });
  interactor.interactorStyle.setCurrentStyleToTrackballCamera();

  // Create camera widget
  const cameraOrientation = vtk.vtkCameraOrientationWidget({ interactor, parentRenderer: renderer });
  cameraOrientation.enabled = true;

  // Display the scalar bar at the bottom with a horizontal orientation
  const scalarBarActor = vtk.vtkScalarBarActor({ 
    lookupTable: lut,
    title: "Scalars",
    titleTextProperty: textProperty,
    labelTextProperty: textProperty,
    annotationTextProperty: textProperty,
    unconstrainedFontSize: true,
  });
  const scalarBar = vtk.vtkScalarBarWidget({ scalarBarActor, interactor, defaultRenderer: renderer });
  const scalarBarRepresentation = scalarBar.getRepresentation();
  scalarBarRepresentation.setOrientation(0); // 1: vertical, 0: horizontal
  const lowerLeftPosition = scalarBarRepresentation.getPositionCoordinate();
  lowerLeftPosition.setValue([0.1, 0.05, 0.0]);
  scalarBar.enabled = true;

  // Trigger render and start interactor
  interactor.start();
}

Full Screen Viewer

The data-config attribute on the annotation <script> accepts the same settings as the options object passed to loadAsync(...) — for example, add data-config='{"rendering": "webgpu"}' to switch the rendering backend. See Loading VTK.wasm for what each option does, or the loadAsync reference for the exact option types.

Bundler with TypeScript

@kitware/vtk-wasm ships hand-written types for the runtime and session API. See TypeScript types.

Project setup

The example below is a Vite + TypeScript app; the full code lives here. Three pieces wire the types together:

  • gen:types runs vtk-wasm gen-types --url <bundle> --out src/vtk-wasm.gen.d.ts against the same URL main.ts passes to loadAsync. Keeping one URL for both guarantees the declarations describe the binary that actually runs.
  • predev / prebuild call it, so the declarations are refreshed before every dev server start and every build, so that you don't accidentally use stale types.
  • tsconfig.json has "include": ["src"], which picks up the generated file automatically. build runs tsc --noEmit before vite build, so a scene that no longer matches the bundle fails the build instead of the browser showing a runtime type error.
json
{
  "name": "wave-app-ts",
  "private": true,
  "version": "1.0.0",
  "type": "module",
  "scripts": {
    "gen:types": "npx vtk-wasm gen-types --url https://raw.githack.com/Kitware/vtk-wasm/dist/latest/vtk-wasm32-emscripten.tar.gz --out src/vtk-wasm.gen.d.ts",
    "prepare:types": "npm run gen:types",
    "typecheck": "tsc --noEmit",
    "predev": "npm run prepare:types",
    "dev": "vite",
    "prebuild": "npm run prepare:types",
    "build": "npm run typecheck && vite build",
    "preview": "vite preview"
  },
  "dependencies": {
    "@kitware/vtk-wasm": "file:../../.."
  },
  "devDependencies": {
    "typescript": "^6.0.3",
    "vite": "^6.3.5"
  }
}
json
{
  "compilerOptions": {
    "target": "ESNext",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "strict": true,
    "skipLibCheck": true,
    "noEmit": true,
    "lib": ["ESNext", "DOM", "DOM.Iterable"],
    "types": []
  },
  "include": ["src"]
}
html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>VTK.wasm TypeScript wave example</title>
    <style>
      html,
      body {
        margin: 0;
        height: 100%;
        background: #05070f;
        color: #e8eef8;
        font: 14px/1.45 system-ui, -apple-system, "Segoe UI", sans-serif;
      }
      #app {
        position: relative;
        width: 100vw;
        height: 100vh;
      }
      #app > canvas {
        width: 100%;
        height: 100%;
        display: block;
      }
      #overlay {
        position: absolute;
        top: 20px;
        left: 20px;
        max-width: 22rem;
        padding: 14px 18px;
        border: 1px solid rgba(255, 255, 255, 0.14);
        border-radius: 10px;
        background: rgba(6, 12, 26, 0.55);
        backdrop-filter: blur(8px);
        pointer-events: none;
      }
      /* Kept in the layout (visibility, not display) so its measured area stays
         stable and the observer below cannot oscillate. */
      #overlay.hidden {
        visibility: hidden;
        opacity: 0;
      }
      #overlay h1 {
        margin: 0 0 6px;
        font-size: 15px;
        font-weight: 600;
        letter-spacing: 0.02em;
      }
      #overlay p {
        margin: 0;
        font-size: 12.5px;
        color: rgba(232, 238, 248, 0.7);
      }
      #stats {
        margin-top: 10px;
        font-variant-numeric: tabular-nums;
        font-size: 12.5px;
        color: #7fd8e8;
      }
    </style>
  </head>
  <body>
    <div id="app">
      <canvas tabindex="-1" onclick="focus()"></canvas>
      <div id="overlay">
        <h1>Procedural wave surface</h1>
        <p>
          VTK.wasm owns the mesh; JavaScript rewrites its point positions,
          normals and scalars every frame through zero-copy views onto the wasm
          heap. Drag to orbit, scroll to zoom.
        </p>
        <div id="stats">warming up…</div>
      </div>
    </div>
    <script>
      // On small viewports the overlay can swallow the scene. Hide it whenever
      // it would cover more than half of the canvas.
      (function () {
        const MAX_AREA_FRACTION = 0.5;
        const canvas = document.querySelector("#app > canvas");
        const overlay = document.querySelector("#overlay");

        function syncOverlay() {
          const canvasBox = canvas.getBoundingClientRect();
          const overlayBox = overlay.getBoundingClientRect();
          const canvasArea = canvasBox.width * canvasBox.height;
          const overlayArea = overlayBox.width * overlayBox.height;
          overlay.classList.toggle(
            "hidden",
            canvasArea > 0 && overlayArea / canvasArea > MAX_AREA_FRACTION
          );
        }

        const observer = new ResizeObserver(syncOverlay);
        observer.observe(canvas);
        observer.observe(overlay);
        syncOverlay();
      })();
    </script>
    <script type="module" src="/src/main.ts"></script>
  </body>
</html>
ts
import { loadAsync, vtkInteractorStyleSwitch } from "@kitware/vtk-wasm";

// This app loads the VTK.wasm bundle it was typed against. `session.vtk` is
// fully typed because `npm run gen:types` ran `vtk-wasm gen-types` against the
// same tarball and emitted `src/vtk-wasm.gen.d.ts`, a module-augmentation file
// that gives every `vtk.vtkXxx(...)` a precise signature. The generated types
// also tell you which calls are asynchronous: only methods VTK marks
// `maySuspend` return a Promise (and need `await`); every other method returns
// its value synchronously.
//
// The scene is a procedural water surface: VTK owns the mesh, JavaScript owns
// the vertex data. Every frame the point positions, normals and scalars are
// rewritten in place through zero-copy TypedArray views onto the wasm heap —
// no per-frame allocation, no copies, no pipeline re-execution.
const BUNDLE_URL = "https://raw.githack.com/Kitware/vtk-wasm/dist/latest/vtk-wasm32-emscripten.tar.gz";
const CANVAS_SELECTOR = "#app > canvas";

/** Quads per side of the surface: (RESOLUTION + 1)^2 points. */
const RESOLUTION = 200;
/** The surface spans [-EXTENT, EXTENT] in x and y. */
const EXTENT = 1;

/** Height range the color map is stretched over. */
const HEIGHT_RANGE = [-0.24, 0.24] as const;

/**
 * Wave field: two ripple sources orbiting the origin, a directional swell and
 * a little high-frequency chop. Evaluated per point per frame, together with
 * its analytic gradient — a heightfield's exact normal is
 * `normalize(-dh/dx, -dh/dy, 1)`, which is both cheaper and smoother than
 * re-running vtkPolyDataNormals on 40k points every frame.
 */
const RIPPLE_AMPLITUDE = 0.12;
const RIPPLE_WAVE_NUMBER = 9.0;
const RIPPLE_FREQUENCY = 3.0;
const RIPPLE_DECAY = 1.1;
const ORBIT_RADIUS = 0.55;
/** Normalizes `r e^(-Dr)` (peak at r = 1/D) to RIPPLE_AMPLITUDE. */
const RIPPLE_ENVELOPE_SCALE = RIPPLE_AMPLITUDE * RIPPLE_DECAY * Math.E;

/** Scratch tuple for `evaluateWave`, reused so the loop allocates nothing. */
const wave = { height: 0, slopeX: 0, slopeY: 0 };

function evaluateWave(x: number, y: number, time: number): void {
  let height = 0;
  let slopeX = 0;
  let slopeY = 0;

  // Two ripple sources drifting in opposite directions.
  for (let source = 0; source < 2; source++) {
    const angularSpeed = source === 0 ? 0.7 : -0.5;
    const phase = source === 0 ? 0 : Math.PI;
    const centerX = ORBIT_RADIUS * Math.cos(angularSpeed * time + phase);
    const centerY = ORBIT_RADIUS * Math.sin(angularSpeed * time + phase);

    const dx = x - centerX;
    const dy = y - centerY;
    const distance = Math.hypot(dx, dy);
    if (distance < 1e-5) {
      continue; // The gradient is undefined at the source; it contributes none.
    }

    const argument = RIPPLE_WAVE_NUMBER * distance - RIPPLE_FREQUENCY * time;
    // r e^(-Dr), scaled so its maximum is exactly RIPPLE_AMPLITUDE. Rising out
    // of zero at the source keeps the rings smooth instead of spiking there.
    const decay = Math.exp(-RIPPLE_DECAY * distance);
    const envelope = RIPPLE_ENVELOPE_SCALE * distance * decay;
    const sine = Math.sin(argument);
    const cosine = Math.cos(argument);

    height += envelope * sine;
    // d/dr [ E(r) sin(Kr - wt) ] = E'(r) sin + E(r) K cos, with
    // E'(r) = S e^(-Dr) (1 - Dr), and dr/dx = dx/r.
    const derivative =
      RIPPLE_ENVELOPE_SCALE * decay * (1 - RIPPLE_DECAY * distance);
    const radial =
      (derivative * sine + envelope * RIPPLE_WAVE_NUMBER * cosine) / distance;
    slopeX += radial * dx;
    slopeY += radial * dy;
  }

  // A slow directional swell.
  const swell = Math.sin(2.4 * x + 1.6 * y - 1.2 * time);
  const swellSlope = 0.07 * Math.cos(2.4 * x + 1.6 * y - 1.2 * time);
  height += 0.07 * swell;
  slopeX += 2.4 * swellSlope;
  slopeY += 1.6 * swellSlope;

  // Fine chop, to keep the specular highlights alive.
  const chopSlope = 0.035 * Math.cos(6.1 * x - 4.7 * y + 1.9 * time);
  height += 0.035 * Math.sin(6.1 * x - 4.7 * y + 1.9 * time);
  slopeX += 6.1 * chopSlope;
  slopeY += -4.7 * chopSlope;

  wave.height = height;
  wave.slopeX = slopeX;
  wave.slopeY = slopeY;
}

async function main(): Promise<void> {
  const runtime = await loadAsync({ url: BUNDLE_URL });
  const session = runtime.createStandaloneSession();
  const { vtk, typedArrayInterface } = session;

  // vtkPlaneSource is typed: the constructor only accepts real vtkPlaneSource
  // properties, and its methods are checked (note `update` takes a port index —
  // the surviving overload from VTK's serdes manifest). The plane is used for
  // its *topology* only: a regular grid of quads whose points this app then
  // takes ownership of.
  const plane = vtk.vtkPlaneSource({
    xResolution: RESOLUTION,
    yResolution: RESOLUTION,
    origin: [-EXTENT, -EXTENT, 0],
    point1: [EXTENT, -EXTENT, 0],
    point2: [-EXTENT, EXTENT, 0],
  });
  plane.update(0);

  const surface = plane.getOutput();
  const pointCount = (RESOLUTION + 1) * (RESOLUTION + 1);

  // Hand VTK three arrays this app allocated. `toVTKAoSArray` picks the VTK
  // array class from the TypedArray's own type (Float32Array ->
  // vtkTypeFloat32Array), copies the values onto the wasm heap and hands that
  // allocation to VTK, which frees it with the array. Because the class is
  // known statically, the views taken back out below are typed Float32Array
  // rather than the untyped TypedArray union.
  // Seeded at t = 0 so the polydata's bounds already cover the wave's full
  // height when the camera is reset below.
  const positions = new Float32Array(3 * pointCount);
  for (let j = 0, index = 0; j <= RESOLUTION; j++) {
    const y = -EXTENT + (2 * EXTENT * j) / RESOLUTION;
    for (let i = 0; i <= RESOLUTION; i++, index += 3) {
      const x = -EXTENT + (2 * EXTENT * i) / RESOLUTION;
      evaluateWave(x, y, 0);
      positions[index] = x;
      positions[index + 1] = y;
      positions[index + 2] = wave.height;
    }
  }
  const positionArray = typedArrayInterface.toVTKAoSArray(positions, 3, "Points");
  const normalArray = typedArrayInterface.toVTKAoSArray(
    new Float32Array(3 * pointCount),
    3,
    "Normals"
  );
  const heightArray = typedArrayInterface.toVTKAoSArray(
    new Float32Array(pointCount),
    1,
    "Height"
  );

  // Replacing the points' data array keeps the plane's quads and swaps in
  // memory this app can address directly.
  surface.points.setData(positionArray);
  surface.pointData.setNormals(normalArray);
  surface.pointData.setScalars(heightArray);

  // Deep water -> foam. Lab interpolation keeps the ramp perceptually even.
  const colorMap = vtk.vtkColorTransferFunction();
  colorMap.setColorSpaceToLab();
  colorMap.addRGBPoint(-0.24, 0.02, 0.09, 0.26);
  colorMap.addRGBPoint(-0.08, 0.05, 0.32, 0.56);
  colorMap.addRGBPoint(0.04, 0.11, 0.62, 0.7);
  colorMap.addRGBPoint(0.14, 0.53, 0.85, 0.72);
  colorMap.addRGBPoint(0.24, 0.97, 0.96, 0.86);

  // `setInputData` rather than `setInputConnection`: the pipeline has already
  // produced its geometry, and nothing downstream should re-execute when the
  // vertex data changes underneath it.
  const mapper = vtk.vtkPolyDataMapper();
  mapper.setInputData(surface);
  mapper.setLookupTable(colorMap);
  mapper.setScalarRange(HEIGHT_RANGE[0], HEIGHT_RANGE[1]);
  mapper.setScalarModeToUsePointData();
  mapper.scalarVisibilityOn();

  const property = vtk.vtkProperty();
  property.setInterpolationToPhong();
  property.setAmbient(0.15);
  property.setDiffuse(0.75);
  property.setSpecular(0.55);
  property.setSpecularPower(45);

  // Class-typed constructor properties and method parameters are nominal
  // `VtkRef<"...">` handles checked against the generated `$brands` ancestor
  // chain — the C++ is-a relationship — so a vtkPolyDataMapper is a valid
  // `mapper` and a vtkActor a valid `addActor` argument, no casts needed.
  const actor = vtk.vtkActor({ mapper, property });

  const renderer = vtk.vtkRenderer({
    background: [0.02, 0.03, 0.08],
    background2: [0.09, 0.13, 0.25],
    gradientBackground: true,
    twoSidedLighting: 1,
  });
  renderer.addActor(actor);

  // Two scene lights (world coordinates, `lightType` 3): a warm key from the
  // upper right and a cool fill from behind, so the crests read as water.
  renderer.addLight(
    vtk.vtkLight({
      lightType: 3,
      position: [2.2, -2.6, 3.2],
      focalPoint: [0, 0, 0],
      diffuseColor: [1.0, 0.95, 0.87],
      specularColor: [1.0, 0.98, 0.92],
      intensity: 1.0,
    })
  );
  renderer.addLight(
    vtk.vtkLight({
      lightType: 3,
      position: [-2.4, 2.2, 1.4],
      focalPoint: [0, 0, 0],
      diffuseColor: [0.42, 0.6, 0.95],
      specularColor: [0.5, 0.7, 1.0],
      intensity: 0.45,
    })
  );

  const camera = renderer.getActiveCamera();
  camera.setPosition(0, -2.9, 2.0);
  camera.setFocalPoint(0, 0, 0);
  camera.setViewUp(0, 0, 1);
  renderer.resetCamera();
  camera.zoom(1.25);

  // Construct the concrete WASM render-window/interactor classes directly:
  // unlike the generic vtkRenderWindow/vtkRenderWindowInteractor interfaces,
  // they carry the `canvasSelector` property that binds them to the canvas.
  const renderWindow = vtk.vtkWebAssemblyOpenGLRenderWindow({
    canvasSelector: CANVAS_SELECTOR,
  });
  renderWindow.addRenderer(renderer);

  const interactor = vtk.vtkWebAssemblyRenderWindowInteractor({
    renderWindow,
    canvasSelector: CANVAS_SELECTOR,
  });
  (interactor.getInteractorStyle() as vtkInteractorStyleSwitch).setCurrentStyleToTrackballCamera();
  interactor.start();

  const statsElement = document.querySelector<HTMLElement>("#stats");
  let frames = 0;
  let lastReport = performance.now();
  const startTime = performance.now();

  function animate(): void {
    const time = (performance.now() - startTime) / 1000;

    // Views alias the wasm heap and are invalidated whenever it grows, so take
    // fresh ones per frame instead of caching them across renders.
    const positionView = typedArrayInterface.toJSTypedArray(positionArray);
    const normalView = typedArrayInterface.toJSTypedArray(normalArray);
    const heightView = typedArrayInterface.toJSTypedArray(heightArray);

    for (let point = 0, index = 0; point < pointCount; point++, index += 3) {
      evaluateWave(positionView[index], positionView[index + 1], time);
      positionView[index + 2] = wave.height;
      heightView[point] = wave.height;

      const length = Math.hypot(wave.slopeX, wave.slopeY, 1);
      normalView[index] = -wave.slopeX / length;
      normalView[index + 1] = -wave.slopeY / length;
      normalView[index + 2] = 1 / length;
    }

    // Writes through a view are invisible to VTK until the array says so; the
    // mapper re-uploads its VBO because the points' MTime moved.
    positionArray.modified();
    normalArray.modified();
    heightArray.modified();
    renderWindow.render();

    frames++;
    const now = performance.now();
    if (statsElement && now - lastReport >= 500) {
      const fps = (frames * 1000) / (now - lastReport);
      statsElement.textContent =
        `${pointCount.toLocaleString()} points · ` +
        `${(RESOLUTION * RESOLUTION).toLocaleString()} quads · ` +
        `${fps.toFixed(0)} fps`;
      frames = 0;
      lastReport = now;
    }
    requestAnimationFrame(animate);
  }
  requestAnimationFrame(animate);
}

main().catch((err) => console.error(err));
bash
npm install
npm run build

Result

Full Screen Viewer

Bundler with pure Javascript

Modern web development relies on a package manager to bring in project dependencies. This section covers how published releases are used within a JavaScript project.

Project setup

In this simple example we use Vite with Vanilla JavaScript. The full code is available for reference here. Use a concrete version, or "latest", for the @kitware/vtk-wasm package. Here, the example uses a relative path to the vtk-wasm project root so the in-repo documentation stays relevant.

json
{
  "name": "modern-app",
  "private": true,
  "version": "1.0.0",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "preview": "vite preview"
  },
  "devDependencies": {
    "vite": "^6.3.5"
  },
  "dependencies": {
    "@kitware/vtk-wasm": "file:../../.."
  }
}
html
<!doctype html>
<html lang="en">

<head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Standalone VTK.wasm example</title>
</head>

<body>
    <div id="app">
        <canvas tabindex="-1" onclick="focus()"></canvas>
    </div>
    <script type="module" src="/src/main.js"></script>
</body>

</html>
js
import "./style.css";
import { loadAsync } from "@kitware/vtk-wasm";

const runtime = await loadAsync({
  url: "https://raw.githack.com/Kitware/vtk-wasm/dist/latest/vtk-wasm32-emscripten.tar.gz",
});

const session = runtime.createStandaloneSession();
const vtk = session.vtk;
const titleText = "This scene passes the VTK.wasm bundle from GitLab registry to loadAsync()";
const canvasSelector = "#app > canvas";

// Create a VTK source. Output has a point data array named "Scalars" whose range is [0, PI].
const shapes = vtk.vtkPartitionedDataSetCollectionSource({ numberOfShapes: 2 });
const lut = vtk.vtkColorTransferFunction();
lut.setColorSpaceToHSV();
const colorSeries = vtk.vtkColorSeries({ colorScheme: 16 });
const numColors = colorSeries.getNumberOfColors();
const scalarRange = [0.0, Math.PI];
const scalarDiff = (scalarRange[1] - scalarRange[0]) / numColors;
for (let i = 0; i < numColors; i++) {
  const color = colorSeries.getColor(i);
  const t = scalarRange[0] + i * scalarDiff;
  lut.addRGBPoint(
    t,
    color[0] / 255,
    color[1] / 255,
    color[2] / 255,
  );
}
lut.build();

const mapper = vtk.vtkCompositePolyDataMapper({ lookupTable: lut });
mapper.setInputConnection(shapes.getOutputPort());
const actor = vtk.vtkActor({ mapper, scale: [0.1, 0.1, 0.1] });
actor.property.edgeVisibility = true;
actor.property.edgeColor = [0.2, 0.2, 0.2];

// Create an actor that displays the title.
const textProperty = vtk.vtkTextProperty({ fontSize: 22 });
const titleTextActor = vtk.vtkTextActor({ input: titleText, textProperty });
const position = titleTextActor.getPositionCoordinate();
position.setCoordinateSystemToNormalizedViewport();

// Setup rendering part
const renderer = vtk.vtkRenderer({ background: [0.384314, 0.364706, 0.352941] });
renderer.addActor(actor);
renderer.addActor(titleTextActor);
renderer.resetCamera();

// Create a RenderWindow and bind it to a canvas in the DOM
const renderWindow = vtk.vtkRenderWindow({ canvasSelector });
renderWindow.addRenderer(renderer);
const interactor = vtk.vtkRenderWindowInteractor({
  canvasSelector,
  renderWindow,
});
interactor.interactorStyle.setCurrentStyleToTrackballCamera();

// Create camera widget
const cameraOrientation = vtk.vtkCameraOrientationWidget({ interactor, parentRenderer: renderer });
cameraOrientation.enabled = true;

// Display the scalar bar at the bottom with a horizontal orientation
const scalarBarActor = vtk.vtkScalarBarActor({
  lookupTable: lut,
  title: "Scalars",
  titleTextProperty: textProperty,
  labelTextProperty: textProperty,
  annotationTextProperty: textProperty,
  unconstrainedFontSize: true,
});
const scalarBar = vtk.vtkScalarBarWidget({ scalarBarActor, interactor, defaultRenderer: renderer });
const scalarBarRepresentation = scalarBar.getRepresentation();
scalarBarRepresentation.setOrientation(0); // 1: vertical, 0: horizontal
const lowerLeftPosition = scalarBarRepresentation.getPositionCoordinate();
lowerLeftPosition.setValue([0.1, 0.05, 0.0]);
scalarBar.enabled = true;

// Trigger render and start interactor
interactor.start();
css
:root {
  font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
  line-height: 1.5;
  font-weight: 400;

  color-scheme: light dark;
  color: rgba(255, 255, 255, 0.87);
  background-color: #242424;

  font-synthesis: none;
  text-rendering: optimizeLegibility;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

body {
  margin: 0;
  height: 100vh;
}

@media (prefers-color-scheme: light) {
  :root {
    color: #213547;
    background-color: #ffffff;
  }
}
bash
npm install
npm run build

Here, the VTK.wasm bundle is downloaded in the browser directly from the GitLab package registry. See the src/main.js file for the relevant code.

Result

Full Screen Viewer