Skip to content

Commit

Permalink
omezarr
Browse files Browse the repository at this point in the history
  • Loading branch information
TheMooseman committed Jan 8, 2025
1 parent f55e099 commit 65753c8
Show file tree
Hide file tree
Showing 6 changed files with 326 additions and 37 deletions.
6 changes: 3 additions & 3 deletions examples/src/app.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import React from 'react';
import { BrowserRouter, Route, Routes } from 'react-router';
import { Home } from './home';
import { OmezarrDemo } from './omezarr/omezarr';
import { DziViewerPair } from './dzi/double';
import { OmezarrDemo } from './omezarr/omezarr-demo';
import { DziDemo } from './dzi/dzi-demo';

export function App() {
return (
Expand All @@ -14,7 +14,7 @@ export function App() {
/>
<Route
path="/dzi"
element={<DziViewerPair />}
element={<DziDemo />}
/>
<Route
path="/omezarr"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ const images = [exampleA, exampleB];
* SVG overlays, etc may all be different!
*
*/
export function DziViewerPair() {
export function DziDemo() {
// the DZI renderer expects a "relative" camera - that means a box, from 0 to 1. 0 is the bottom or left of the image,
// and 1 is the top or right of the image, regardless of the aspect ratio of that image.
const [view, setView] = useState<box2D>(Box2D.create([0, 0], [1, 1]));
Expand Down
141 changes: 141 additions & 0 deletions examples/src/omezarr/omezarr-demo.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
import React, { useEffect, useMemo, useState } from 'react';
import { RenderServerProvider } from '~/common/react/render-server-provider';
import { SliceView } from './sliceview';
import { type OmeZarrDataset, loadOmeZarr, sizeInUnits } from '@alleninstitute/vis-omezarr';
import { OmezarrViewer } from './omezarr-viewer';
import { type RenderSettings } from '@alleninstitute/vis-omezarr';
import { Box2D, Vec2, type box2D, type Interval, type vec2 } from '@alleninstitute/vis-geometry';

const demo_versa = 'https://neuroglancer-vis-prototype.s3.amazonaws.com/VERSA/scratch/0500408166/';

const screenSize: vec2 = [500, 500];

const defaultInterval: Interval = { min: 0, max: 80 };

function makeZarrSettings(screenSize: vec2, view: box2D, planeIdx: number): RenderSettings {
return {
camera: { screenSize, view },
gamut: {
R: { gamut: defaultInterval, index: 0 },
G: { gamut: defaultInterval, index: 1 },
B: { gamut: defaultInterval, index: 2 },
},
plane: 'xy',
planeIndex: planeIdx,
tileSize: 256,
};
}

export function OmezarrDemo() {
const [omezarr, setOmezarr] = useState<OmeZarrDataset>();
const [view, setView] = useState(Box2D.create([0, 0], [1, 1]));
const [planeIndex, setPlaneIndex] = useState(0);
const [dragging, setDragging] = useState(false);

const settings: RenderSettings | undefined = useMemo(
() => (omezarr ? makeZarrSettings(screenSize, view, planeIndex) : undefined),
[omezarr, view, planeIndex]
);

useEffect(() => {
loadOmeZarr(demo_versa).then((v) => {
setOmezarr(v);
const size = sizeInUnits('xy', v.multiscales[0].axes, v.multiscales[0].datasets[0]);
if (size) {
console.log(size);
setView(Box2D.create([0, 0], [size[0], size[1]]));
}
});
}, []);

const zoom = (e: WheelEvent) => {
e.preventDefault();

const zoomScale = e.deltaY > 0 ? 1.1 : 0.9;

// translate mouse pos to data space
// offset divided by screen size gives us a percentage of the canvas where the mouse is
// multiply percentage by view size to make it data space
// add offset of the min corner so that the position takes into account any box offset
const zoomPoint: vec2 = Vec2.add(
view.minCorner,
Vec2.mul(Vec2.div([e.offsetX, e.offsetY], screenSize), Box2D.size(view))
);

// scale the box with our new zoom point as the center
const v = Box2D.translate(
Box2D.scale(Box2D.translate(view, Vec2.scale(zoomPoint, -1)), [zoomScale, zoomScale]),
zoomPoint
);

setView(v);
};

const pan = (e: React.MouseEvent<HTMLCanvasElement>) => {
if (dragging) {
const pos = Vec2.div([-e.movementX, -e.movementY], screenSize);
const scaledOffset = Vec2.mul(pos, Box2D.size(view));
const v = Box2D.translate(view, scaledOffset);
setView(v);
}
};

const handleMouseDown = () => {
setDragging(true);
};

const handleMouseUp = () => {
setDragging(false);
};

// you could put this on the mouse wheel, but for this demo we'll have buttons
const handlePlaneIndex = (next: 1 | -1) => {
setPlaneIndex((prev) => prev + next);
};

return omezarr && settings ? (
<RenderServerProvider>
<div>
<button onClick={() => handlePlaneIndex(-1)}>{'<-'}</button>
<button onClick={() => handlePlaneIndex(1)}>{'->'}</button>
</div>
<OmezarrViewer
omezarr={omezarr}
id="omezarr-viewer"
screenSize={screenSize}
settings={settings}
onWheel={zoom}
onMouseMove={pan}
onMouseDown={handleMouseDown}
onMouseUp={handleMouseUp}
onMouseLeave={handleMouseUp}
/>
</RenderServerProvider>
) : (
<h5>Unable to load OME-Zarr</h5>
);
}
/**
* HEY!!!
* this is an example React Component for rendering A single slice of an OMEZARR image in a react component
* This example is as bare-bones as possible! It is NOT the recommended way to do anything, its just trying to show
* one way of:
* 1. using our rendering utilities for OmeZarr data, specifically in a react component. Your needs for state-management,
* slicing logic, etc might all be different!
*
*/
function DataPlease() {
// load our canned data for now:
const [omezarr, setfile] = useState<OmeZarrDataset | undefined>(undefined);
useEffect(() => {
loadOmeZarr(demo_versa).then((dataset) => {
setfile(dataset);
console.log('loaded!');
});
}, []);
return (
<RenderServerProvider>
<SliceView omezarr={omezarr} />
</RenderServerProvider>
);
}
124 changes: 124 additions & 0 deletions examples/src/omezarr/omezarr-viewer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import React from 'react';
import {
buildAsyncOmezarrRenderer,
type ZarrDataset,
type VoxelTile,
defaultDecoder,
type RenderSettings,
type OmeZarrDataset,
} from '@alleninstitute/vis-omezarr';
import { type RenderFrameFn } from '@alleninstitute/vis-scatterbrain';
import { useContext, useEffect, useRef } from 'react';
import type { vec2, box2D } from '@alleninstitute/vis-geometry';
import { renderServerContext } from '~/common/react/render-server-provider';

interface OmezarrViewerProps {
omezarr: OmeZarrDataset;
id: string;
screenSize: vec2;
settings: RenderSettings;
onWheel?: (e: WheelEvent) => void;
onMouseDown?: (e: React.MouseEvent<HTMLCanvasElement>) => void;
onMouseUp?: (e: React.MouseEvent<HTMLCanvasElement>) => void;
onMouseMove?: (e: React.MouseEvent<HTMLCanvasElement>) => void;
onMouseLeave?: (e: React.MouseEvent<HTMLCanvasElement>) => void;
}

export type OmezarrViewerState = {
planeIndex: number;
view: box2D;
};

function compose(ctx: CanvasRenderingContext2D, image: ImageData) {
ctx.putImageData(image, 0, 0);
}

export function OmezarrViewer({
omezarr,
id,
settings,
onWheel,
onMouseDown,
onMouseUp,
onMouseMove,
onMouseLeave,
}: OmezarrViewerProps) {
const canvas = useRef<HTMLCanvasElement>(null);
const server = useContext(renderServerContext);
const renderer = useRef<ReturnType<typeof buildAsyncOmezarrRenderer>>();

// setup renderer and delete it when component goes away
useEffect(() => {
const c = canvas?.current;
if (server && server.regl) {
renderer.current = buildAsyncOmezarrRenderer(server.regl, defaultDecoder);
}
return () => {
if (c) {
server?.destroyClient(c);
}
};
}, [server]);

// render frames
useEffect(() => {
if (server && renderer.current && canvas.current && omezarr) {
const hey: RenderFrameFn<ZarrDataset, VoxelTile> = (target, cache, callback) => {
if (renderer.current) {
return renderer.current(omezarr, settings, callback, target, cache);
}
return null;
};

server.beginRendering(
hey,
(e) => {
switch (e.status) {
case 'begin':
server.regl?.clear({ framebuffer: e.target, color: [0, 0, 0, 0], depth: 1 });
break;
case 'progress':
// wanna see the tiles as they arrive?
e.server.copyToClient(compose);
break;
case 'finished': {
e.server.copyToClient(compose);
break;
}
default: {
break;
}
}
},
canvas.current
);
}
}, [server, renderer, canvas, omezarr, settings]);

// wheel event needs to be active for control + wheel zoom to work
useEffect(() => {
const c = canvas.current;
const handleWheel = (e: WheelEvent) => onWheel?.(e);
if (c) {
c.addEventListener('wheel', handleWheel, { passive: false });
}
return () => {
if (c) {
c.removeEventListener('wheel', handleWheel);
}
};
});

return (
<canvas
id={id}
ref={canvas}
width={settings.camera.screenSize[0]}
height={settings.camera.screenSize[1]}
onMouseDown={onMouseDown}
onMouseUp={onMouseUp}
onMouseMove={onMouseMove}
onMouseLeave={onMouseLeave}
/>
);
}
33 changes: 0 additions & 33 deletions examples/src/omezarr/omezarr.tsx

This file was deleted.

57 changes: 57 additions & 0 deletions examples/src/utils/use-why-did-you-update.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { useRef, useEffect } from 'react';

interface ChangesObj<P> {
[key: string]: {
from: P[keyof P];
to: P[keyof P];
};
}

/**
* NOT TO BE USED IN PRODUCTION
*
* This is a useful debugging and performance hook
* It will log what parts of props caused the functional component to update.
*
* See: https://usehooks.com/useWhyDidYouUpdate/
*
* @param {string} name - name will show up in console log
* @param {Object} props - entire props object of the component being tested
*/
export function useWhyDidYouUpdate<P extends object>(name: string, props: P) {
// Get a mutable ref object where we can store props ...
// ... for comparison next time this hook runs.
const previousProps = useRef<P>();

useEffect(() => {
if (previousProps.current) {
// Get all keys from previous and current props
const allKeys = Object.keys({ ...previousProps.current, ...props }) as Array<keyof P>;
// Use this object to keep track of changed props
const changesObj: ChangesObj<P> = {};
// Iterate through keys
allKeys.forEach((key) => {
// If previous is different from current
if (previousProps.current && key in previousProps.current && props && key in props) {
if (previousProps.current[key] !== props[key]) {
// Add to changesObj
const changeKey = key as string;
changesObj[changeKey] = {
from: previousProps.current[key],
to: props[key],
};
}
}
});

// If changesObj not empty then output to console
if (Object.keys(changesObj).length) {
// eslint-disable-next-line no-console
console.log('[why-did-you-update]', name, changesObj);
}
}

// Finally update previousProps with current props for next hook call
previousProps.current = props;
});
}

0 comments on commit 65753c8

Please sign in to comment.