Skip to content

Orthogonal Views of Zebrafish Embryo

Orthogonal XY, XZ and YZ slices plus a 3D volume, all on one canvas.

Data: Zebrahub multi-view · public.czbiohub.org

ts
import {
  ChannelProps,
  Color,
  Idetik,
  ImageLayer,
  OmeZarrImageSource,
  OrbitControls,
  OrthographicCamera,
  PanZoomControls,
  PerspectiveCamera,
  SliceOrientation,
  VolumeLayer,
  createPlaybackPolicy,
  Viewport,
} from "idetik";
import { addDimensionSlider } from "../lil_gui_utils";
import { vec3 } from "gl-matrix";

import GUI from "lil-gui";

const url =
  "https://public.czbiohub.org/royerlab/zebrahub/imaging/multi-view/ZMNS001.ome.zarr/";

const source = await OmeZarrImageSource.fromHttp({ url });

const baseLod = 0;
const dims = source.getDimensions();
const axisRange = (axis: "x" | "y" | "z"): readonly [number, number] => {
  const d = dims[axis]!.lods[baseLod];
  return [d.translation, d.translation + d.size * d.scale] as const;
};
const [xMin, xMax] = axisRange("x");
const [yMin, yMax] = axisRange("y");
const [zMin, zMax] = axisRange("z");
const step = (axis: "x" | "y" | "z") => dims[axis]!.lods[baseLod].scale;

const timeCount = dims.t?.lods[baseLod].size ?? 1;
const center = vec3.fromValues(
  (xMin + xMax) / 2,
  (yMin + yMax) / 2,
  (zMin + zMax) / 2
);

const currentTime = { t: Math.floor((timeCount - 1) / 2) };

const channelProps: ChannelProps[] = [
  { color: Color.CYAN, contrastLimits: [0, 1200] },
  { color: Color.MAGENTA, contrastLimits: [0, 400] },
];

const sliceCoords = {
  get t() {
    return currentTime.t;
  },
  x: center[0],
  y: center[1],
  z: center[2],
  c: [0, 1],
};

function createSliceLayer(orientation: SliceOrientation) {
  return new ImageLayer({
    source,
    sliceCoords,
    policy: createPlaybackPolicy(),
    channelProps,
    orientation,
  });
}

function createSliceViewport(
  id: string,
  orientation: SliceOrientation,
  [uMin, uMax]: readonly [number, number],
  [vMin, vMax]: readonly [number, number]
) {
  const uPad = 0.2 * (uMax - uMin);
  const vPad = 0.2 * (vMax - vMin);
  const camera = new OrthographicCamera({
    left: uMin - uPad,
    right: uMax + uPad,
    top: vMin - vPad,
    bottom: vMax + vPad,
    orientation,
  });

  return new Viewport({
    id,
    domElement: document.querySelector<HTMLDivElement>(`#viewport-${id}`)!,
    camera,
    cameraControls: new PanZoomControls(camera),
    layers: [createSliceLayer(orientation)],
  });
}

const volumeLod = Math.min(2, dims.x!.lods.length - 1);
const volumeLayer = new VolumeLayer({
  source,
  sliceCoords: {
    get t() {
      return currentTime.t;
    },
    c: [0, 1],
  },
  policy: createPlaybackPolicy({ lod: { min: volumeLod, max: volumeLod } }),
  channelProps,
});
volumeLayer.opacityMultiplier = 0.01;

const xRange = [xMin, xMax] as const;
const yRange = [yMin, yMax] as const;
const zRange = [zMin, zMax] as const;

const camera3D = new PerspectiveCamera({ near: 1.0 });

new Idetik({
  canvas: document.querySelector<HTMLCanvasElement>("#canvas")!,
  viewports: [
    createSliceViewport("slice-xy", "XY", xRange, yRange),
    createSliceViewport("slice-xz", "XZ", xRange, zRange),
    createSliceViewport("slice-yz", "YZ", yRange, zRange),
    new Viewport({
      id: "3d",
      domElement: document.querySelector<HTMLDivElement>("#viewport-3d")!,
      camera: camera3D,
      cameraControls: new OrbitControls(camera3D, {
        radius: Math.hypot(xMax - xMin, yMax - yMin, zMax - zMin),
        yaw: 0.4,
        pitch: 0.1,
        target: center,
      }),
      layers: [
        createSliceLayer("XY"),
        createSliceLayer("XZ"),
        createSliceLayer("YZ"),
        volumeLayer,
      ],
    }),
  ],
}).start();

const gui = new GUI({ width: 300 }).close();

addDimensionSlider({
  gui,
  sliceCoords: currentTime,
  dimensionName: "t",
  minValue: 0,
  maxValue: timeCount - 1,
  stepValue: 1,
  playback: {
    maxRateHz: 30,
    stride: 1,
  },
});

addDimensionSlider({
  gui,
  sliceCoords,
  dimensionName: "x",
  minValue: xMin,
  maxValue: xMax,
  stepValue: step("x"),
});

addDimensionSlider({
  gui,
  sliceCoords,
  dimensionName: "y",
  minValue: yMin,
  maxValue: yMax,
  stepValue: step("y"),
});

addDimensionSlider({
  gui,
  sliceCoords,
  dimensionName: "z",
  minValue: zMin,
  maxValue: zMax,
  stepValue: step("z"),
});

Released under the MIT License. © 2026-present Chan Zuckerberg Biohub, Inc.