Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: React Examples Components (DT-7024) #49

Merged
merged 10 commits into from
Jan 30, 2025
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 0 additions & 17 deletions examples/dzi.html

This file was deleted.

12 changes: 5 additions & 7 deletions examples/index.html
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
<!doctype html>
<html>
<body>
EXAMPLES
<br />
<ul>
<li><a href="/dzi">Deep Zoom Image</a><br /></li>
<li><a href="/omezarr">OMEZARR</a><br /></li>
<li><a href="/layers">Layers</a><br /></li>
</ul>
<div id="app"></div>
<script
type="module"
src="/src/index.tsx"
></script>
</body>
</html>
17 changes: 0 additions & 17 deletions examples/omezarr.html

This file was deleted.

5 changes: 3 additions & 2 deletions examples/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,10 @@
"vite": "^5.3.5"
},
"dependencies": {
"@alleninstitute/vis-geometry": "workspace:*",
"@alleninstitute/vis-scatterbrain": "workspace:*",
"@alleninstitute/vis-dzi": "workspace:*",
"@alleninstitute/vis-geometry": "workspace:*",
"@alleninstitute/vis-omezarr": "workspace:*",
"@alleninstitute/vis-scatterbrain": "workspace:*",
"@czi-sds/components": "^20.0.1",
"@emotion/css": "^11.11.2",
"@emotion/react": "^11.11.4",
Expand All @@ -62,6 +62,7 @@
"lodash": "^4.17.21",
"react": "^18.3.0",
"react-dom": "^18.3.0",
"react-router": "^7.0.2",
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

are a handful of static, independant pages worth pulling in this dependency?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suppose it does let us delete the kinda gross repetetative {demo-name.html} files....

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lets keep it, seems fine

"regl": "^2.1.0",
"zarrita": "0.4.0-next.14"
}
Expand Down
75 changes: 22 additions & 53 deletions examples/src/app.tsx
Original file line number Diff line number Diff line change
@@ -1,58 +1,27 @@
import React from 'react';
import { SliceViewLayer } from './ui/slice-ui';
import type { Demo } from './layers';
import { AnnotationGrid } from './ui/annotation-grid';
import { ContactSheetUI } from './ui/contact-sheet';
import { ScatterplotUI } from './ui/scatterplot-ui';
import { Button } from '@czi-sds/components';
import { BrowserRouter, Route, Routes } from 'react-router';
import { Home } from './home';
import { OmezarrDemo } from './omezarr/omezarr-demo';
import { DziDemo } from './dzi/dzi-demo';

export function AppUi(props: { demo: Demo }) {
const { demo } = props;
export function App() {
return (
<div>
<Button
onClick={() => {
demo.requestSnapshot(3000);
}}
>
{'📸'}
</Button>
<label>{`Layer ${demo.selectedLayer}`}</label>
<Button
onClick={() => {
demo.selectLayer(demo.selectedLayer - 1);
}}
>
{'<-'}
</Button>
<Button
onClick={() => {
demo.selectLayer(demo.selectedLayer + 1);
}}
>
{'->'}
</Button>
<LayerUi demo={demo} />
</div>
<BrowserRouter>
<Routes>
<Route
index
element={<Home />}
/>
<Route
path="/dzi"
element={<DziDemo />}
/>
<Route
path="/omezarr"
element={<OmezarrDemo />}
/>
<Route path="/layers" />
</Routes>
</BrowserRouter>
);
}
function LayerUi(props: { demo: Demo }) {
const { demo } = props;
const layer = demo.layers[demo.selectedLayer];
if (layer) {
switch (layer.type) {
case 'annotationGrid':
return <AnnotationGrid demo={demo} />;
case 'volumeGrid':
return <ContactSheetUI demo={demo} />;
case 'volumeSlice':
return <SliceViewLayer demo={demo} />;
case 'scatterplot':
case 'scatterplotGrid':
return <ScatterplotUI demo={demo} />;
default:
return null;
}
}
return <SliceViewLayer demo={props.demo} />;
}
6 changes: 0 additions & 6 deletions examples/src/dzi/app.tsx

This file was deleted.

60 changes: 33 additions & 27 deletions examples/src/dzi/double.tsx → examples/src/dzi/dzi-demo.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { useContext, useEffect, useMemo, useRef, useState } from 'react';
import { useEffect, useMemo, useRef, useState } from 'react';
import { RenderServerProvider } from '../common/react/render-server-provider';
import React from 'react';
import { DziView } from './dziView';
import type { DziImage, DziRenderSettings } from '@alleninstitute/vis-dzi';
import { Box2D, Vec2, type box2D } from '@alleninstitute/vis-geometry';
import { Box2D, Vec2, type box2D, type vec2 } from '@alleninstitute/vis-geometry';
import { DziViewer } from './dzi-viewer';

const example: DziImage = {
// We know the sizes and formats ahead of time for these examples,
// if you'd like to see how to get this data from an endpoint with a dzi file check out use-dzi-image.ts
const exampleA: DziImage = {
format: 'jpeg',
imagesUrl:
'https://idk-etl-prod-download-bucket.s3.amazonaws.com/idf-23-10-pathology-images/pat_images_HPW332DMO29NC92JPWA/H20.33.029-A12-I6-primary/H20.33.029-A12-I6-primary_files/',
Expand All @@ -16,7 +18,8 @@ const example: DziImage = {
},
tileSize: 512,
};
const exampleDzi: DziImage = {

const exampleB: DziImage = {
imagesUrl: 'https://openseadragon.github.io/example-images/highsmith/highsmith_files/',
format: 'jpg',
overlap: 2,
Expand All @@ -26,12 +29,11 @@ const exampleDzi: DziImage = {
},
tileSize: 256,
};
const exampleSettings: DziRenderSettings = {
camera: {
screenSize: [500, 500],
view: Box2D.create([0, 0], [1, 1]),
},
};

const screenSize: vec2 = [500, 500];

const images = [exampleA, exampleB];

/**
* HEY!!!
* this is an example React Component for rendering two DZI images which share a camera.
Expand All @@ -42,40 +44,44 @@ const exampleSettings: DziRenderSettings = {
* SVG overlays, etc may all be different!
*
*/
export function TwoClientsPOC() {
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]));
const zoom = (e: React.WheelEvent<HTMLCanvasElement>) => {
const zoom = (e: WheelEvent) => {
e.preventDefault();
const scale = e.deltaY > 0 ? 1.1 : 0.9;
const m = Box2D.midpoint(view);
const v = Box2D.translate(Box2D.scale(Box2D.translate(view, Vec2.scale(m, -1)), [scale, scale]), m);
setView(v);
};
const overlay = useRef<HTMLImageElement>(new Image());

const camera: DziRenderSettings['camera'] = useMemo(() => ({ screenSize, view }), [view]);

useEffect(() => {
overlay.current.onload = () => {
console.log('loaded svg!');
};
overlay.current.src =
'https://idk-etl-prod-download-bucket.s3.amazonaws.com/idf-22-07-pathology-image-move/pat_images_JGCXWER774NLNWX2NNR/7179-A6-I6-MTG-classified/annotation.svg';
}, []);

return (
<RenderServerProvider>
<DziView
id="left"
svgOverlay={overlay.current}
dzi={example}
camera={{ ...exampleSettings.camera, view }}
wheel={zoom}
/>
<DziView
id="right"
dzi={exampleDzi}
svgOverlay={overlay.current}
camera={{ ...exampleSettings.camera, view }}
wheel={zoom}
/>
<div style={{ display: 'flex', flexDirection: 'row' }}>
{images.map((v) => (
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you add some text to the page encouraging users to scroll to get the demos to appear? Alternatively, we figure out why it takes interaction to make these show up properly.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In bkp we solve that problem with an "onComplete" callback with a hook, but lets wait to look into this deeper in another ticket

<div style={{ width: screenSize[0], height: screenSize[1] }}>
<DziViewer
id={v.imagesUrl}
dzi={v}
camera={camera}
svgOverlay={overlay.current}
onWheel={zoom}
/>
</div>
))}
</div>
</RenderServerProvider>
);
}
60 changes: 38 additions & 22 deletions examples/src/dzi/dziView.tsx → examples/src/dzi/dzi-viewer.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,25 @@
import { useContext, useEffect, useRef, useState } from 'react';
import { useContext, useEffect, useRef } from 'react';
import {
buildDziRenderer,
type DziImage,
type DziRenderSettings,
type DziTile,
type GpuProps as CachedPixels,
buildAsyncDziRenderer,
} from '@alleninstitute/vis-dzi';
import React from 'react';
import { buildAsyncRenderer, type RenderFrameFn } from '@alleninstitute/vis-scatterbrain';
import { isEqual } from 'lodash';
import { renderServerContext } from '../common/react/render-server-provider';
import { Vec2, type vec2 } from '@alleninstitute/vis-geometry';
import { renderServerContext } from '~/common/react/render-server-provider';
import React from 'react';

type Props = {
id: string;
dzi: DziImage;
svgOverlay: HTMLImageElement;
wheel: (e: React.WheelEvent<HTMLCanvasElement>) => void;
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;
} & DziRenderSettings;

function buildCompositor(svg: HTMLImageElement, settings: DziRenderSettings) {
Expand All @@ -36,20 +38,11 @@ function buildCompositor(svg: HTMLImageElement, settings: DziRenderSettings) {
};
}

export function DziView(props: Props) {
const { svgOverlay, camera, dzi, wheel, id } = props;
export function DziViewer(props: Props) {
const { svgOverlay, camera, dzi, onWheel, id, onMouseDown, onMouseUp, onMouseMove, onMouseLeave } = props;
const server = useContext(renderServerContext);
const cnvs = useRef<HTMLCanvasElement>(null);

// this is a demo, so rather than work hard to have a referentially stable camera,
// we just memoize it like so to prevent over-rendering
const [cam, setCam] = useState(camera);
useEffect(() => {
if (!isEqual(cam, camera)) {
setCam(camera);
}
}, [camera]);

// the renderer needs WebGL for us to create it, and WebGL needs a canvas to exist, and that canvas needs to be the same canvas forever
// hence the awkwardness of refs + an effect to initialize the whole hting
const renderer =
Expand All @@ -63,6 +56,7 @@ export function DziView(props: Props) {
}
return () => {
if (cnvs.current) {
// eslint-disable-next-line react-hooks/exhaustive-deps
server?.destroyClient(cnvs.current);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is out of place? This isn't disabling the useEffect dependency lint issues. We also still need to actually integrate a linter soon 😅

}
};
Expand All @@ -73,11 +67,11 @@ export function DziView(props: Props) {
const renderMyData: RenderFrameFn<DziImage, DziTile> = (target, cache, callback) => {
if (renderer.current) {
// erase the frame before we start drawing on it
return renderer.current(dzi, { camera: cam }, callback, target, cache);
return renderer.current(dzi, { camera }, callback, target, cache);
}
return null;
};
const compose = buildCompositor(svgOverlay, { camera: cam });
const compose = buildCompositor(svgOverlay, { camera });
server.beginRendering(
renderMyData,
(e) => {
Expand All @@ -91,20 +85,42 @@ export function DziView(props: Props) {
break;
case 'finished': {
e.server.copyToClient(compose);
break;
}
default:
break;
}
},
cnvs.current
);
}
}, [server, renderer.current, cnvs.current, cam]);
}, [server, svgOverlay, dzi, camera]);

// we have to add the listener this way because onWheel is a passive listener by default
// that means we can't preventDefault to stop scrolling
useEffect(() => {
const handleWheel = (e: WheelEvent) => onWheel?.(e);
const canvas = cnvs;
if (canvas?.current) {
canvas.current.addEventListener('wheel', handleWheel, { passive: false });
}
return () => {
if (canvas?.current) {
canvas.current.removeEventListener('wheel', handleWheel);
}
};
}, [onWheel]);

return (
<canvas
id={id}
ref={cnvs}
onWheel={wheel}
width={camera.screenSize[0]}
froyo-np marked this conversation as resolved.
Show resolved Hide resolved
height={camera.screenSize[1]}
></canvas>
onMouseDown={onMouseDown}
onMouseUp={onMouseUp}
onMouseMove={onMouseMove}
onMouseLeave={onMouseLeave}
/>
);
}
5 changes: 0 additions & 5 deletions examples/src/dzi/dzi.ts

This file was deleted.

Loading
Loading