78 lines
3.2 KiB
TypeScript
78 lines
3.2 KiB
TypeScript
import type { Container, FederatedPointerEvent, FederatedWheelEvent, Graphics } from 'pixi.js';
|
||
import { clamp, drawGrid, zoomFactor } from '../../ui';
|
||
import type { Point } from '../framing';
|
||
|
||
/**
|
||
* What every surface that shows nodes shares with the graph editor: how far it zooms, how the grid
|
||
* is drawn and how the wheel zooms about the pointer. The graph editor and a plugin's node board
|
||
* look and move the same because they both come through here; the Tile tab's board borrows the
|
||
* panning and zooming.
|
||
*/
|
||
|
||
/** Spacing of the background grid – and, with snapping on, of the positions a drag can leave. */
|
||
export const GRID_STEP = 24;
|
||
export const ZOOM_LIMITS = { min: 0.25, max: 2.5 };
|
||
/** Breathing room around the framed nodes, so they do not touch the edge. */
|
||
export const FIT_MARGIN = 24;
|
||
/** Below this on-screen spacing the grid is noise rather than orientation, so it is dropped. */
|
||
const MIN_GRID_STEP = 8;
|
||
|
||
export interface GraphView {
|
||
/** Where the graph origin sits on screen. */
|
||
pan: Point;
|
||
/** Screen pixels per graph unit. */
|
||
zoom: number;
|
||
}
|
||
|
||
/**
|
||
* The view after zooming by `factor` about a screen point, which keeps the graph point under it.
|
||
* `limits` are the graph's own unless a surface with other content says otherwise – the tile
|
||
* board shows 48 px tiles, which one wants to come closer to than a node graph.
|
||
*/
|
||
export function zoomedAbout(
|
||
view: GraphView,
|
||
at: Point,
|
||
factor: number,
|
||
limits: { min: number; max: number } = ZOOM_LIMITS,
|
||
): GraphView {
|
||
const zoom = clamp(view.zoom * factor, limits.min, limits.max);
|
||
const graphX = (at.x - view.pan.x) / view.zoom;
|
||
const graphY = (at.y - view.pan.y) / view.zoom;
|
||
return { pan: { x: at.x - graphX * zoom, y: at.y - graphY * zoom }, zoom };
|
||
}
|
||
|
||
/**
|
||
* Drag with any button to pan, wheel to zoom about the pointer – all a board does with the pointer
|
||
* when its content takes none. `set` receives each new view and which gesture made it.
|
||
*/
|
||
export function panAndZoom(
|
||
surface: Container,
|
||
get: () => GraphView,
|
||
set: (view: GraphView, gesture: 'pan' | 'zoom') => void,
|
||
limits: { min: number; max: number } = ZOOM_LIMITS,
|
||
): void {
|
||
let panning: { start: Point; pan: Point } | null = null;
|
||
surface.on('pointerdown', (e: FederatedPointerEvent) => {
|
||
panning = { start: { x: e.global.x, y: e.global.y }, pan: { ...get().pan } };
|
||
});
|
||
surface.on('globalpointermove', (e: FederatedPointerEvent) => {
|
||
if (!panning) return;
|
||
const { start, pan } = panning;
|
||
set({ ...get(), pan: { x: pan.x + e.global.x - start.x, y: pan.y + e.global.y - start.y } }, 'pan');
|
||
});
|
||
surface.on('pointerup', () => (panning = null));
|
||
surface.on('pointerupoutside', () => (panning = null));
|
||
surface.on('wheel', (e: FederatedWheelEvent) => {
|
||
set(zoomedAbout(get(), surface.toLocal(e.global), zoomFactor(e.deltaY), limits), 'zoom');
|
||
e.preventDefault();
|
||
});
|
||
}
|
||
|
||
export function drawGraphGrid(g: Graphics, view: GraphView, w: number, h: number): void {
|
||
g.clear();
|
||
const step = GRID_STEP * view.zoom;
|
||
if (step < MIN_GRID_STEP) return;
|
||
// `pan` is where the graph origin sits on screen, but no axis lines: it marks nothing.
|
||
drawGrid(g, { originX: view.pan.x, originY: view.pan.y, step, w, h, alpha: 0.04 });
|
||
}
|