presentation/components/stage.component.ts
presentation/components/stage.component.ts is a file in Bane's Lab Site. 131 lines of code and 30 definitions.
import {
MOUSE_POINTER,
PANEL_DRAGGING_CLASS,
PANEL_MAX_SCALE,
PANEL_POINTER_PAIR,
PANEL_REVEAL_MARGIN_PX,
PANEL_SCALE_PROPERTY,
PANEL_WHEEL_ZOOM_RATE,
PANEL_X_PROPERTY,
PANEL_Y_PROPERTY,
} from "#configuration/constants/panel.constants";
import {
ORIGIN_VIEW,
boundedView,
centerOf,
contentExtentOf,
distanceBetween,
extentOf,
fittedView,
midpointOf,
zoomedView,
} from "#core/analyzers/panel.analyzer";
import type { PanelMode, PanelView, PanelPoint as Point } from "#types/panel.types";
import { PIXEL } from "#configuration/constants/element.constants";
import { declareStyle } from "#core/registries/style.registry";
import { observeArming } from "#core/observers/panel.observer";
const PRIMARY_BUTTON = 0;
export class PanelState {
public view: PanelView = ORIGIN_VIEW;
private readonly element: HTMLElement;
private readonly stage: HTMLElement;
private readonly zoom: Element;
private readonly mode: PanelMode;
private readonly pointers = new Map<number, Point>();
private pinchDistance = 0;
private readonly armed: () => boolean;
public constructor(element: HTMLElement, stage: HTMLElement, mode: PanelMode) {
this.element = element;
this.stage = stage;
this.zoom = stage.firstElementChild ?? stage;
this.mode = mode;
this.armed = observeArming(element);
}
public apply(next: PanelView): void {
this.view = boundedView(next, extentOf(this.element), contentExtentOf(this.zoom, this.view.scale));
declareStyle(this.stage, PANEL_X_PROPERTY, String(this.view.x) + PIXEL);
declareStyle(this.stage, PANEL_Y_PROPERTY, String(this.view.y) + PIXEL);
declareStyle(this.stage, PANEL_SCALE_PROPERTY, String(this.view.scale));
}
public reset(): void {
if (this.mode === "origin") {
this.apply(ORIGIN_VIEW);
return;
}
const ceiling = this.mode === "fill" ? PANEL_MAX_SCALE : 1;
this.apply(fittedView(extentOf(this.element), contentExtentOf(this.zoom, this.view.scale), ceiling));
}
public zoomBy(factor: number, anchor: Point): void {
this.apply(zoomedView(this.view, factor, anchor));
}
public zoomAtCenter(factor: number): void {
this.zoomBy(factor, centerOf(extentOf(this.element)));
}
public reveal(top: number): void {
this.apply({ ...this.view, y: PANEL_REVEAL_MARGIN_PX - top * this.view.scale });
}
public press(event: PointerEvent): void {
if (event.pointerType === MOUSE_POINTER && (event.button !== PRIMARY_BUTTON || !this.armed())) {
return;
}
this.pointers.set(event.pointerId, this.local(event));
this.element.setPointerCapture(event.pointerId);
this.element.classList.add(PANEL_DRAGGING_CLASS);
this.pinchDistance = this.pairDistance();
event.preventDefault();
}
public move(event: PointerEvent): void {
const previous = this.pointers.get(event.pointerId);
if (previous === undefined) {
return;
}
const current = this.local(event);
this.pointers.set(event.pointerId, current);
event.preventDefault();
if (this.pointers.size >= PANEL_POINTER_PAIR) {
this.pinch();
return;
}
this.apply({ ...this.view, x: this.view.x + current.x - previous.x, y: this.view.y + current.y - previous.y });
}
public release(event: PointerEvent): void {
this.pointers.delete(event.pointerId);
this.pinchDistance = this.pairDistance();
if (this.pointers.size === 0) {
this.element.classList.remove(PANEL_DRAGGING_CLASS);
}
}
public wheel(event: WheelEvent): void {
if (!this.armed()) {
return;
}
if (event.ctrlKey || event.metaKey) {
this.zoomBy(Math.exp(-event.deltaY * PANEL_WHEEL_ZOOM_RATE), this.local(event));
event.preventDefault();
return;
}
this.apply({ ...this.view, x: this.view.x - event.deltaX, y: this.view.y - event.deltaY });
event.preventDefault();
}
private local(event: MouseEvent): Point {
const rect = this.element.getBoundingClientRect();
return { x: event.clientX - rect.left, y: event.clientY - rect.top };
}
private pair(): [Point, Point] | null {
const [first, second] = [...this.pointers.values()];
return first !== undefined && second !== undefined ? [first, second] : null;
}
private pairDistance(): number {
const pair = this.pair();
return pair === null ? 0 : distanceBetween(pair[0], pair[1]);
}
private pinch(): void {
const pair = this.pair();
if (pair === null || this.pinchDistance === 0) {
return;
}
const now = distanceBetween(pair[0], pair[1]);
this.zoomBy(now / this.pinchDistance, midpointOf(pair[0], pair[1]));
this.pinchDistance = now;
}
}