Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
406b2a9
DRAWING AN OMEZARR SLIDE
lanesawyer Apr 30, 2025
ce85bfc
Separate into it's own component
lanesawyer May 1, 2025
1f03edb
It works!
lanesawyer May 1, 2025
31a057f
Fix zoom
lanesawyer May 1, 2025
bc1b1c1
some cleanup
lanesawyer May 1, 2025
b545928
Merge branch 'main' into web-components-ome-zarr
lanesawyer May 15, 2025
73a0f96
Merge branch 'main' into web-components-ome-zarr
lanesawyer May 24, 2025
0d9d04b
Wooo got it loading again
lanesawyer May 24, 2025
e83c86f
DZI web component renders!
lanesawyer May 25, 2025
121be23
Pan and zoom work!
lanesawyer May 25, 2025
94ea2ed
fmt
lanesawyer May 25, 2025
7c832f8
render server with events!
lanesawyer May 25, 2025
cbd0613
Event pattern!
lanesawyer May 25, 2025
eae5a85
Improvements to base and DZI components
lanesawyer May 26, 2025
e9af445
Wooo OME-Zarr renders with BaseViewer
lanesawyer May 26, 2025
eda4435
fix a type
lanesawyer May 26, 2025
f09d803
component loggers
lanesawyer May 26, 2025
9d0f905
revert some react ome-zarr examples changes
lanesawyer May 26, 2025
83312e5
some more tweaks to undo changes for react side
lanesawyer May 26, 2025
2454cd9
another format fix
lanesawyer May 26, 2025
47e830b
clean up
lanesawyer May 26, 2025
fc2811b
Paired DZI viewers
lanesawyer May 27, 2025
11d9397
comments and clean up
lanesawyer May 27, 2025
be8d423
Merge branch 'main' into web-components
lanesawyer May 27, 2025
19fa9a3
Load data inside DZI component
lanesawyer May 28, 2025
f17207a
clean up loader
lanesawyer May 28, 2025
9d41855
a couple tweaks
lanesawyer May 29, 2025
87a04fc
Move camera into DZI. need to do camera sync now
lanesawyer May 29, 2025
5d5cf6b
Camera syncing web component!
lanesawyer May 29, 2025
d7d652a
Clean up
lanesawyer May 29, 2025
538b06c
Merge branch 'main' into web-components
lanesawyer May 29, 2025
084f6e2
Clean up oopsies
lanesawyer May 29, 2025
8ddc22a
synced svg overlay plugin. don't love the syncing logic though
lanesawyer May 30, 2025
e5ee48b
Merge branch 'main' into web-components
lanesawyer Jun 7, 2025
1e37a33
File rename
lanesawyer Jun 7, 2025
d571a8e
Moved OME-Zarr loading stuff into web component (starts way too zoome…
lanesawyer Jun 11, 2025
24aebbb
still loads, better viewport! more like the bkp-client function now
lanesawyer Jun 11, 2025
b787530
OME-Zarr web component render on first load of metadata
lanesawyer Jun 12, 2025
d059377
pass render server features by attribute
lanesawyer Jun 12, 2025
5895aa3
Remove unnecessary entry in package.json
lanesawyer Jun 12, 2025
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
21 changes: 21 additions & 0 deletions packages/core/src/camera.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { Box2D, Vec2, type box2D, type vec2 } from '@alleninstitute/vis-geometry';

/**
* Zooms relative to the current mouse position
*/
export function zoom(view: box2D, screenSize: vec2, zoomScale: number, mousePos: vec2): box2D {
const zoomPoint: vec2 = Vec2.add(view.minCorner, Vec2.mul(Vec2.div(mousePos, screenSize), Box2D.size(view)));
return Box2D.translate(
Box2D.scale(Box2D.translate(view, Vec2.scale(zoomPoint, -1)), [zoomScale, zoomScale]),
zoomPoint,
);
}

/**
* Pans by a pixel delta in screen space
*/
export function pan(view: box2D, screenSize: vec2, delta: vec2): box2D {
const relative = Vec2.div(Vec2.mul(delta, [-1, -1]), screenSize);
const offset = Vec2.mul(relative, Box2D.size(view));
return Box2D.translate(view, offset);
}
1 change: 1 addition & 0 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,4 @@ export type {
export { RenderServer } from './abstract/render-server';

export { Logger, logger } from './logger';
export { pan, zoom } from './camera';
1 change: 1 addition & 0 deletions packages/omezarr/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export {
type VoxelTile,
defaultDecoder,
getVisibleTiles,
makeZarrSettings,
} from './sliceview/loader';
export { buildTileRenderer, buildRGBTileRenderer } from './sliceview/tile-renderer';
export {
Expand Down
37 changes: 36 additions & 1 deletion packages/omezarr/src/sliceview/loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@ import {
type box2D,
type OrthogonalCartesianAxes,
type vec2,
type Interval,
PLANE_XY,
} from '@alleninstitute/vis-geometry';
import type { Chunk } from 'zarrita';
import type { ZarrRequest } from '../zarr/loading';
import { loadSlice, pickBestScale, planeSizeInVoxels, sizeInUnits } from '../zarr/loading';
import type { VoxelTileImage } from './slice-renderer';
import type { RenderSettings, RenderSettingsChannels, VoxelTileImage } from './slice-renderer';
import type { OmeZarrMetadata, OmeZarrShapedDataset } from '../zarr/types';

export type VoxelTile = {
Expand Down Expand Up @@ -123,3 +125,36 @@ export const defaultDecoder = (
return { shape, data: new Float32Array(buffer.data) };
});
};

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

export function makeZarrSettings(
omezarr: OmeZarrMetadata,
screenSize: vec2,
view: box2D,
plane: CartesianPlane,
orthoVal: number,
): RenderSettings {
const omezarrChannels = omezarr.colorChannels.reduce((acc, val, index) => {
acc[val.label ?? `${index}`] = {
rgb: val.rgb,
gamut: val.range,
index,
};
return acc;
}, {} as RenderSettingsChannels);

const fallbackChannels: RenderSettingsChannels = {
R: { rgb: [1.0, 0, 0], gamut: defaultInterval, index: 0 },
G: { rgb: [0, 1.0, 0], gamut: defaultInterval, index: 1 },
B: { rgb: [0, 0, 1.0], gamut: defaultInterval, index: 2 },
};

return {
camera: { screenSize, view },
plane,
orthoVal,
tileSize: 256,
channels: Object.keys(omezarrChannels).length > 0 ? omezarrChannels : fallbackChannels,
};
}
39 changes: 39 additions & 0 deletions packages/web-components/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
{
"name": "@alleninstitute/vis-web-components",
"version": "0.0.1",
"contributors": [
{
"name": "Lane Sawyer",
"email": "[email protected]"
}
],
"license": "BSD-3-Clause",
"source": "src/index.ts",
"main": "dist/main.js",
"module": "dist/module.js",
"types": "dist/types.d.ts",
"files": ["dist"],
"scripts": {
"typecheck": "tsc --noEmit",
"build": "parcel build --no-cache",
"dev": "parcel watch --port 1239",
"test": "vitest --watch",
"test:ci": "vitest run",
"coverage": "vitest run --coverage"
},
"repository": {
"type": "git",
"url": "https://github.com/AllenInstitute/vis.git"
},
"publishConfig": {
"registry": "https://npm.pkg.github.com/AllenInstitute"
},
"dependencies": {
"@alleninstitute/vis-core": "workspace:*",
"@alleninstitute/vis-geometry": "workspace:*",
"@alleninstitute/vis-omezarr": "workspace:*",
"regl": "2.1.1",
"@alleninstitute/vis-dzi": "workspace:*"
},
"packageManager": "[email protected]"
}
103 changes: 103 additions & 0 deletions packages/web-components/src/base-viewer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import { Logger, type RenderServer } from '@alleninstitute/vis-core';
import { RENDER_SERVER_READY, RENDER_SERVER_TAG_NAME } from './render-server-provider';

export const REQUEST_RENDER_SERVER = 'request-render-server';

/**
* Base viewer class that provides common functionality for all viewers.
* It's primary job is requesting the RenderServer and setting up the canvas (including width and height).
*
* Concrete implementations should extend this class and implement the `onServerReady` method to
* start the rendering process.
*/
export abstract class BaseViewer extends HTMLElement {
protected canvas = document.createElement('canvas');
protected renderServer: RenderServer | null = null;
// TODO: Change to warn once I'm done working on the viewer components
protected logger = new Logger(this.tagName, 'info');

constructor() {
super();

// make host a positioned block so shadow children size correctly
this.style.display = 'block';
this.style.position = 'relative';
this.logger.info(`Creating component`);
// build shadow DOM: canvas + plugin slot
const shadow = this.attachShadow({ mode: 'closed' });
// render surface
this.canvas.style.position = 'relative';
this.canvas.style.top = '0';
this.canvas.style.left = '0';
this.canvas.style.width = '100%';
this.canvas.style.height = '100%';
shadow.appendChild(this.canvas);
// plugin slot for overlays (SVG, annotations, controls, etc.)
const slot = document.createElement('slot');
slot.name = 'plugin';
shadow.appendChild(slot);
}

private renderServerReadyListener(e: Event) {
this.renderServer = (e as CustomEvent<RenderServer>).detail;
this.onServerReady();
}

static get observedAttributes() {
return ['width', 'height'];
}

connectedCallback() {
this.logger.info('Connected');

if (!customElements.get(RENDER_SERVER_TAG_NAME)) {
this.logger.error('Render Server Provider does not exist. Please make sure to include it in the DOM');
}

this.updateSize();

this.addEventListener(RENDER_SERVER_READY, this.renderServerReadyListener, { once: true });
this.dispatchEvent(
new CustomEvent(REQUEST_RENDER_SERVER, {
// Has to bubble so we can catch it in the RenderServerProvider
bubbles: true,
}),
);
}

disconnectedCallback() {
this.logger.info('Disconnected');

// Clean references (no need to remove event listener, it will be removed automatically due to `once`)
this.renderServer?.destroyClient(this.canvas);
this.renderServer = null;
this.canvas.remove();
}

attributeChangedCallback(name: string, oldValue: string | null, newValue: string | null) {
if (oldValue === newValue) {
return;
}

if (name === 'width' || name === 'height') {
this.updateSize();
}
}

protected updateSize() {
const w = this.getAttribute('width') || '100';
const h = this.getAttribute('height') || '100';
this.canvas.width = parseInt(w, 10);
this.canvas.height = parseInt(h, 10);
this.canvas.style.width = `${this.canvas.width}px`;
this.canvas.style.height = `${this.canvas.height}px`;
// size host element to match canvas
this.style.width = `${this.canvas.width}px`;
this.style.height = `${this.canvas.height}px`;
}

/**
* Used to set up the concrete implementation of a viewer once the RenderServer has been provided.
*/
protected abstract onServerReady(): void;
}
51 changes: 51 additions & 0 deletions packages/web-components/src/camera-sync.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import type { DziViewer } from './dzi';

/**
* CameraSync pairs sibling viewers by listening for one viewer's `camera-change` event
* and applying its camera settings to all other viewers under this element.
*
* TODO: Consider more than just the DZI and 2D use cases
*/
export class CameraSync extends HTMLElement {
connectedCallback() {
this.addEventListener('camera-change', this.handleCameraChange);
}

disconnectedCallback() {
this.removeEventListener('camera-change', this.handleCameraChange);
}

private handleCameraChange(event: Event) {
const custom = event as CustomEvent & {
detail: { view: any; screenSize?: [number, number]; __sync?: boolean };
};
// ignore events originating from sync to prevent loops
if (custom.detail.__sync) {
return;
}
custom.stopPropagation();
const camera = custom.detail;
const source = event.target as HTMLElement;
// Apply camera settings to all sibling DziViewers
this.querySelectorAll('dzi-viewer').forEach((v) => {
if (v !== source) {
// Apply settings without re-emitting core event
const target = v as DziViewer;
target.setRenderSettings?.({ camera }, false);
// dispatch sync camera-change for plugins
const syncDetail = { ...camera, __sync: true };
target.dispatchEvent(
new CustomEvent('camera-change', {
detail: syncDetail,
bubbles: true,
composed: true,
}),
);
}
});
}
}

if (!customElements.get('camera-sync')) {
customElements.define('camera-sync', CameraSync);
}
Loading