mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-03-18 06:08:22 +03:00
Merge pull request #31 from ibs0d/codex/add-shared-zoom-helper-module
Add camera zoom utility module for grid cards
This commit is contained in:
commit
8d6be93d25
162
web/src/utils/cameraZoom.ts
Normal file
162
web/src/utils/cameraZoom.ts
Normal file
@ -0,0 +1,162 @@
|
||||
export const CAMERA_ZOOM_MIN_SCALE = 1;
|
||||
export const CAMERA_ZOOM_MAX_SCALE = 8;
|
||||
// Tuning constant for discrete shift+wheel zoom on grid cards.
|
||||
export const CAMERA_ZOOM_SHIFT_WHEEL_STEP = 0.1;
|
||||
|
||||
export type CameraZoomRuntimeTransform = {
|
||||
scale: number;
|
||||
positionX: number;
|
||||
positionY: number;
|
||||
};
|
||||
|
||||
export type CameraZoomDimensions = {
|
||||
viewportWidth: number;
|
||||
viewportHeight: number;
|
||||
contentWidth: number;
|
||||
contentHeight: number;
|
||||
};
|
||||
|
||||
export type CameraZoomPersistedState = {
|
||||
/**
|
||||
* Scale normalized to a [0, 1] range between min and max zoom.
|
||||
*/
|
||||
normalizedScale: number;
|
||||
/**
|
||||
* Relative content x-coordinate (0..1) that should map to the viewport center.
|
||||
*/
|
||||
focusX: number;
|
||||
/**
|
||||
* Relative content y-coordinate (0..1) that should map to the viewport center.
|
||||
*/
|
||||
focusY: number;
|
||||
};
|
||||
|
||||
export function clamp(value: number, min: number, max: number): number {
|
||||
return Math.min(max, Math.max(min, value));
|
||||
}
|
||||
|
||||
export function clampScale(
|
||||
scale: number,
|
||||
minScale: number = CAMERA_ZOOM_MIN_SCALE,
|
||||
maxScale: number = CAMERA_ZOOM_MAX_SCALE,
|
||||
): number {
|
||||
return clamp(scale, minScale, maxScale);
|
||||
}
|
||||
|
||||
export function normalizeScale(
|
||||
scale: number,
|
||||
minScale: number = CAMERA_ZOOM_MIN_SCALE,
|
||||
maxScale: number = CAMERA_ZOOM_MAX_SCALE,
|
||||
): number {
|
||||
if (maxScale <= minScale) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return clamp(
|
||||
(clampScale(scale, minScale, maxScale) - minScale) / (maxScale - minScale),
|
||||
0,
|
||||
1,
|
||||
);
|
||||
}
|
||||
|
||||
export function denormalizeScale(
|
||||
normalizedScale: number,
|
||||
minScale: number = CAMERA_ZOOM_MIN_SCALE,
|
||||
maxScale: number = CAMERA_ZOOM_MAX_SCALE,
|
||||
): number {
|
||||
if (maxScale <= minScale) {
|
||||
return minScale;
|
||||
}
|
||||
|
||||
const normalized = clamp(normalizedScale, 0, 1);
|
||||
return minScale + normalized * (maxScale - minScale);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates a new pan position to keep the content under the cursor fixed
|
||||
* while changing scale.
|
||||
*/
|
||||
export function getCursorRelativeZoomTransform(
|
||||
current: CameraZoomRuntimeTransform,
|
||||
targetScale: number,
|
||||
cursorX: number,
|
||||
cursorY: number,
|
||||
minScale: number = CAMERA_ZOOM_MIN_SCALE,
|
||||
maxScale: number = CAMERA_ZOOM_MAX_SCALE,
|
||||
): CameraZoomRuntimeTransform {
|
||||
const nextScale = clampScale(targetScale, minScale, maxScale);
|
||||
const safeCurrentScale =
|
||||
Number.isFinite(current.scale) && current.scale > 0
|
||||
? current.scale
|
||||
: CAMERA_ZOOM_MIN_SCALE;
|
||||
const contentX = (cursorX - current.positionX) / safeCurrentScale;
|
||||
const contentY = (cursorY - current.positionY) / safeCurrentScale;
|
||||
|
||||
return {
|
||||
scale: nextScale,
|
||||
positionX: cursorX - contentX * nextScale,
|
||||
positionY: cursorY - contentY * nextScale,
|
||||
};
|
||||
}
|
||||
|
||||
export function toPersistedCameraZoomState(
|
||||
runtime: CameraZoomRuntimeTransform,
|
||||
dimensions: CameraZoomDimensions,
|
||||
minScale: number = CAMERA_ZOOM_MIN_SCALE,
|
||||
maxScale: number = CAMERA_ZOOM_MAX_SCALE,
|
||||
): CameraZoomPersistedState {
|
||||
const safeContentWidth = dimensions.contentWidth || 1;
|
||||
const safeContentHeight = dimensions.contentHeight || 1;
|
||||
const safeScale =
|
||||
Number.isFinite(runtime.scale) && runtime.scale > 0
|
||||
? runtime.scale
|
||||
: CAMERA_ZOOM_MIN_SCALE;
|
||||
const centerContentX =
|
||||
(dimensions.viewportWidth / 2 - runtime.positionX) / safeScale;
|
||||
const centerContentY =
|
||||
(dimensions.viewportHeight / 2 - runtime.positionY) / safeScale;
|
||||
|
||||
return {
|
||||
normalizedScale: normalizeScale(runtime.scale, minScale, maxScale),
|
||||
focusX: clamp(centerContentX / safeContentWidth, 0, 1),
|
||||
focusY: clamp(centerContentY / safeContentHeight, 0, 1),
|
||||
};
|
||||
}
|
||||
|
||||
export function fromPersistedCameraZoomState(
|
||||
persisted: CameraZoomPersistedState,
|
||||
dimensions: CameraZoomDimensions,
|
||||
minScale: number = CAMERA_ZOOM_MIN_SCALE,
|
||||
maxScale: number = CAMERA_ZOOM_MAX_SCALE,
|
||||
): CameraZoomRuntimeTransform {
|
||||
const scale = denormalizeScale(persisted.normalizedScale, minScale, maxScale);
|
||||
const safeContentWidth = dimensions.contentWidth || 1;
|
||||
const safeContentHeight = dimensions.contentHeight || 1;
|
||||
const contentX = clamp(persisted.focusX, 0, 1) * safeContentWidth;
|
||||
const contentY = clamp(persisted.focusY, 0, 1) * safeContentHeight;
|
||||
|
||||
return {
|
||||
scale,
|
||||
positionX: dimensions.viewportWidth / 2 - contentX * scale,
|
||||
positionY: dimensions.viewportHeight / 2 - contentY * scale,
|
||||
};
|
||||
}
|
||||
|
||||
export function getNextScaleFromWheelDelta(
|
||||
currentScale: number,
|
||||
wheelDeltaY: number,
|
||||
step: number = CAMERA_ZOOM_SHIFT_WHEEL_STEP,
|
||||
minScale: number = CAMERA_ZOOM_MIN_SCALE,
|
||||
maxScale: number = CAMERA_ZOOM_MAX_SCALE,
|
||||
): number {
|
||||
if (wheelDeltaY === 0) {
|
||||
return clampScale(currentScale, minScale, maxScale);
|
||||
}
|
||||
|
||||
const direction = wheelDeltaY > 0 ? -1 : 1;
|
||||
return clampScale(currentScale + direction * step, minScale, maxScale);
|
||||
}
|
||||
|
||||
export function getCameraZoomStorageKey(cameraName: string): string {
|
||||
return `live:grid-card:zoom:${cameraName}`;
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user