@itk-viewer/element
Version:
Web Component for multi-dimensional viewer
191 lines (160 loc) • 4.76 kB
text/typescript
import { LitElement, PropertyValues, html } from 'lit';
import { customElement, property } from 'lit/decorators.js';
import { ReadonlyQuat, ReadonlyVec3, quat, vec3 } from 'gl-matrix';
import { createArcballCamera, ArcballCamera } from '@itk-viewer/arcball';
import { Camera, Pose } from '@itk-viewer/viewer/camera.js';
import { SelectorController } from 'xstate-lit';
const PAN_SPEED = 1;
const ZOOM_SPEED = 0.001;
const bindCamera = (
camera: ArcballCamera,
viewport: HTMLElement,
onUpdate: (
center: ReadonlyVec3,
rotation: ReadonlyQuat,
distance: number,
) => unknown,
) => {
let width = viewport.clientWidth;
let height = viewport.clientHeight;
const updateView = () => {
onUpdate(camera.center, camera.rotation, camera.distance);
};
const resizeObserver = new ResizeObserver((entries) => {
for (const entry of entries) {
width = entry.contentRect.width;
height = entry.contentRect.height;
}
});
resizeObserver.observe(viewport);
let rotate = false;
let pan = false;
let scale = false;
const onPointerDown = (e: PointerEvent) => {
if (e.button === 0) {
rotate = true;
} else if (e.button === 1) {
scale = true;
} else if (e.button === 2) {
pan = true;
}
};
viewport.addEventListener('pointerdown', onPointerDown);
const onPointerUp = (e: PointerEvent) => {
if (e.button === 0) {
rotate = false;
} else if (e.button === 1) {
scale = false;
} else if (e.button === 2) {
pan = false;
}
};
window.addEventListener('pointerup', onPointerUp);
let prevPointerX = 0;
let prevPointerY = 0;
const onPointerMove = (e: PointerEvent) => {
const pointerX = e.offsetX;
const pointerY = e.offsetY;
if (rotate) {
camera.rotate(
[pointerX / width - 0.5, pointerY / height - 0.5],
[prevPointerX / width - 0.5, prevPointerY / height - 0.5],
);
}
if (pan) {
camera.pan([
(PAN_SPEED * (pointerX - prevPointerX)) / width,
(PAN_SPEED * (pointerY - prevPointerY)) / height,
]);
}
if (scale) {
const d = pointerY - prevPointerY;
if (d) camera.distance *= Math.exp(d / height);
}
prevPointerX = pointerX;
prevPointerY = pointerY;
if (!rotate && !pan && !scale) return;
updateView();
};
viewport.addEventListener('pointermove', onPointerMove);
const onWheel = (e: WheelEvent) => {
camera.zoom(ZOOM_SPEED * camera.distance * e.deltaY);
updateView();
};
viewport.addEventListener('wheel', onWheel, { passive: false });
const preventDefault = (e: Event) => e.preventDefault();
viewport.addEventListener('contextmenu', preventDefault);
const unBind = () => {
resizeObserver.disconnect();
viewport.removeEventListener('pointerdown', onPointerDown);
window.removeEventListener('pointerup', onPointerUp);
viewport.removeEventListener('pointermove', onPointerMove);
viewport.removeEventListener('wheel', onWheel);
viewport.removeEventListener('contextmenu', preventDefault);
};
return unBind;
};
('itk-camera')
export class ItkCamera extends LitElement {
({ attribute: false })
actor: Camera | undefined;
oldPose: Pose | undefined;
pose: SelectorController<Camera, Pose> | undefined;
cameraController: ArcballCamera;
unBind: (() => unknown) | undefined;
constructor() {
super();
this.cameraController = createArcballCamera(
[0, 0, -1],
[0, 0, 0],
[0, 1, 0],
);
}
connectedCallback(): void {
super.connectedCallback();
this.unBind = bindCamera(
this.cameraController,
this,
(center, rotation, distance) => {
if (!this.actor) return;
this.actor.send({
type: 'setPose',
pose: {
center,
rotation,
distance,
},
});
},
);
}
disconnectedCallback(): void {
super.disconnectedCallback();
this.unBind?.();
}
willUpdate(changedProperties: PropertyValues<this>) {
if (changedProperties.has('actor') && this.actor) {
this.pose = new SelectorController(
this,
this.actor,
(state) => state?.context.pose,
);
}
if (this.pose?.value !== this.oldPose) {
this.oldPose = this.pose?.value;
if (this.pose?.value) {
vec3.copy(this.cameraController.center, this.pose.value.center);
quat.copy(this.cameraController.rotation, this.pose.value.rotation);
this.cameraController.distance = this.pose.value.distance;
}
}
}
render() {
return html`<slot></slot>`;
}
}
declare global {
interface HTMLElementTagNameMap {
'itk-camera': ItkCamera;
}
}