mirror of
https://github.com/suitenumerique/meet.git
synced 2026-09-02 21:58:29 +00:00
fixup! ♻️(frontend) refactor screen share zoom pan with useMove and imperative DOM updates
This commit is contained in:
@@ -2,6 +2,7 @@ import { useCallback, useRef, useSyncExternalStore } from 'react'
|
||||
import { useMove } from 'react-aria'
|
||||
import type { MoveMoveEvent } from '@react-types/shared'
|
||||
import {
|
||||
FULL_PICTURE_RATIO,
|
||||
MIN_ZOOM,
|
||||
PAN_STEP,
|
||||
WHEEL_ZOOM_SPEED,
|
||||
@@ -14,6 +15,7 @@ import {
|
||||
getCursorFromZoomState,
|
||||
getCursorPercentsFromWheelEvent,
|
||||
getPanDeltaPercentsFromMove,
|
||||
getPictureRatio,
|
||||
getWheelPanOffset,
|
||||
getZoomTransform,
|
||||
} from '../utils/screenShareZoom'
|
||||
@@ -73,6 +75,22 @@ export const useScreenShareZoom = () => {
|
||||
el.style.transform = getZoomTransform(zoomRef.current, panRef.current)
|
||||
}, [])
|
||||
|
||||
// The video is letterboxed inside the surface by object-fit: contain, so the
|
||||
// pan bounds depend on how much of the surface the picture actually covers.
|
||||
// Read live rather than cached: both the tile and the shared resolution can
|
||||
// change at any time.
|
||||
const readPictureRatio = useCallback(() => {
|
||||
const surface = surfaceElRef.current
|
||||
const video = transformElRef.current?.querySelector('video')
|
||||
if (!surface || !video) return FULL_PICTURE_RATIO
|
||||
return getPictureRatio(
|
||||
surface.clientWidth,
|
||||
surface.clientHeight,
|
||||
video.videoWidth,
|
||||
video.videoHeight
|
||||
)
|
||||
}, [])
|
||||
|
||||
const applyCursor = useCallback(() => {
|
||||
const el = surfaceElRef.current
|
||||
if (!el) return
|
||||
@@ -85,21 +103,23 @@ export const useScreenShareZoom = () => {
|
||||
const zoomIn = useCallback(() => {
|
||||
const next = clampZoom(zoomRef.current + ZOOM_STEP)
|
||||
zoomRef.current = next
|
||||
panRef.current = clampPan(panRef.current, next)
|
||||
panRef.current = clampPan(panRef.current, next, readPictureRatio())
|
||||
applyTransform()
|
||||
applyCursor()
|
||||
flush()
|
||||
}, [applyTransform, applyCursor, flush])
|
||||
}, [applyTransform, applyCursor, flush, readPictureRatio])
|
||||
|
||||
const zoomOut = useCallback(() => {
|
||||
const next = clampZoom(zoomRef.current - ZOOM_STEP)
|
||||
zoomRef.current = next
|
||||
panRef.current =
|
||||
next <= MIN_ZOOM ? { x: 0, y: 0 } : clampPan(panRef.current, next)
|
||||
next <= MIN_ZOOM
|
||||
? { x: 0, y: 0 }
|
||||
: clampPan(panRef.current, next, readPictureRatio())
|
||||
applyTransform()
|
||||
applyCursor()
|
||||
flush()
|
||||
}, [applyTransform, applyCursor, flush])
|
||||
}, [applyTransform, applyCursor, flush, readPictureRatio])
|
||||
|
||||
const resetZoom = useCallback(() => {
|
||||
zoomRef.current = MIN_ZOOM
|
||||
@@ -136,6 +156,7 @@ export const useScreenShareZoom = () => {
|
||||
nextZoom: next,
|
||||
cursorXPercent,
|
||||
cursorYPercent,
|
||||
ratio: readPictureRatio(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -143,7 +164,7 @@ export const useScreenShareZoom = () => {
|
||||
applyCursor()
|
||||
flush()
|
||||
},
|
||||
[applyTransform, applyCursor, flush]
|
||||
[applyTransform, applyCursor, flush, readPictureRatio]
|
||||
)
|
||||
|
||||
// useMove handles mouse drag + touch pan. Keyboard arrows are not handled
|
||||
@@ -173,7 +194,8 @@ export const useScreenShareZoom = () => {
|
||||
x: panRef.current.x + deltaXPercent,
|
||||
y: panRef.current.y + deltaYPercent,
|
||||
},
|
||||
zoomRef.current
|
||||
zoomRef.current,
|
||||
readPictureRatio()
|
||||
)
|
||||
|
||||
applyTransform()
|
||||
@@ -195,12 +217,13 @@ export const useScreenShareZoom = () => {
|
||||
(dx: number, dy: number) => {
|
||||
panRef.current = clampPan(
|
||||
{ x: panRef.current.x + dx, y: panRef.current.y + dy },
|
||||
zoomRef.current
|
||||
zoomRef.current,
|
||||
readPictureRatio()
|
||||
)
|
||||
applyTransform()
|
||||
flush()
|
||||
},
|
||||
[applyTransform, flush]
|
||||
[applyTransform, flush, readPictureRatio]
|
||||
)
|
||||
|
||||
// Attached to the tile container (not the zoom surface) where keyboard
|
||||
|
||||
@@ -10,6 +10,14 @@ export interface PanOffset {
|
||||
y: number
|
||||
}
|
||||
|
||||
// Fraction of the surface each axis of the picture covers, in [0, 1].
|
||||
export interface PictureRatio {
|
||||
x: number
|
||||
y: number
|
||||
}
|
||||
|
||||
export const FULL_PICTURE_RATIO: PictureRatio = { x: 1, y: 1 }
|
||||
|
||||
export interface ZoomSnapshot {
|
||||
zoomLevel: number
|
||||
zoomPercentage: number
|
||||
@@ -24,15 +32,41 @@ export const clampZoom = (value: number) => {
|
||||
return Math.max(MIN_ZOOM, Math.min(MAX_ZOOM, value))
|
||||
}
|
||||
|
||||
// Restrict pan so the picture always covers the view.
|
||||
export const clampPan = (pan: PanOffset, zoom: number): PanOffset => {
|
||||
const maxPan = ((zoom - 1) / zoom) * PAN_CLAMP_HALF
|
||||
// Restrict pan so the picture always covers the view. Pan is a % of the
|
||||
// surface, in which object-fit: contain letterboxes the picture: its half
|
||||
// extent is `ratio * 50` against a view half extent of 50, and scaling by
|
||||
// `zoom` must keep `zoom * (ratio * 50 - |pan|) >= 50`. An axis whose picture
|
||||
// is still smaller than the view is pinned to 0, keeping the bars symmetric.
|
||||
export const clampPan = (
|
||||
pan: PanOffset,
|
||||
zoom: number,
|
||||
ratio: PictureRatio
|
||||
): PanOffset => {
|
||||
const maxPanX = Math.max(0, (ratio.x - 1 / zoom) * PAN_CLAMP_HALF)
|
||||
const maxPanY = Math.max(0, (ratio.y - 1 / zoom) * PAN_CLAMP_HALF)
|
||||
return {
|
||||
x: Math.max(-maxPan, Math.min(maxPan, pan.x)),
|
||||
y: Math.max(-maxPan, Math.min(maxPan, pan.y)),
|
||||
x: Math.max(-maxPanX, Math.min(maxPanX, pan.x)),
|
||||
y: Math.max(-maxPanY, Math.min(maxPanY, pan.y)),
|
||||
}
|
||||
}
|
||||
|
||||
// Per-axis fraction of the surface covered by an object-fit: contain picture.
|
||||
export const getPictureRatio = (
|
||||
surfaceWidth: number,
|
||||
surfaceHeight: number,
|
||||
videoWidth: number,
|
||||
videoHeight: number
|
||||
): PictureRatio => {
|
||||
if (!surfaceWidth || !surfaceHeight || !videoWidth || !videoHeight) {
|
||||
return FULL_PICTURE_RATIO
|
||||
}
|
||||
const surfaceRatio = surfaceWidth / surfaceHeight
|
||||
const videoRatio = videoWidth / videoHeight
|
||||
return surfaceRatio > videoRatio
|
||||
? { x: videoRatio / surfaceRatio, y: 1 }
|
||||
: { x: 1, y: surfaceRatio / videoRatio }
|
||||
}
|
||||
|
||||
export const buildZoomSnapshot = (
|
||||
zoom: number,
|
||||
pan: PanOffset,
|
||||
@@ -68,12 +102,14 @@ export const getWheelPanOffset = ({
|
||||
nextZoom,
|
||||
cursorXPercent,
|
||||
cursorYPercent,
|
||||
ratio,
|
||||
}: {
|
||||
pan: PanOffset
|
||||
prevZoom: number
|
||||
nextZoom: number
|
||||
cursorXPercent: number
|
||||
cursorYPercent: number
|
||||
ratio: PictureRatio
|
||||
}): PanOffset => {
|
||||
const panShift = 1 / nextZoom - 1 / prevZoom
|
||||
return clampPan(
|
||||
@@ -81,7 +117,8 @@ export const getWheelPanOffset = ({
|
||||
x: pan.x + cursorXPercent * panShift,
|
||||
y: pan.y + cursorYPercent * panShift,
|
||||
},
|
||||
nextZoom
|
||||
nextZoom,
|
||||
ratio
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user