-
Notifications
You must be signed in to change notification settings - Fork 0
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
Changes from 5 commits
31b7d09
bdd3856
1163339
f55e099
65753c8
099c385
c17368c
03fd5b6
543a482
0547305
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
This file was deleted.
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> |
This file was deleted.
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} />; | ||
} |
This file was deleted.
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/', | ||
|
@@ -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, | ||
|
@@ -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. | ||
|
@@ -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) => ( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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> | ||
); | ||
} |
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) { | ||
|
@@ -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 = | ||
|
@@ -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); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this is out of place? This isn't disabling the |
||
} | ||
}; | ||
|
@@ -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) => { | ||
|
@@ -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} | ||
/> | ||
); | ||
} |
This file was deleted.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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....
There was a problem hiding this comment.
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