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 { useMove } from 'react-aria'
|
||||||
import type { MoveMoveEvent } from '@react-types/shared'
|
import type { MoveMoveEvent } from '@react-types/shared'
|
||||||
import {
|
import {
|
||||||
|
FULL_PICTURE_RATIO,
|
||||||
MIN_ZOOM,
|
MIN_ZOOM,
|
||||||
PAN_STEP,
|
PAN_STEP,
|
||||||
WHEEL_ZOOM_SPEED,
|
WHEEL_ZOOM_SPEED,
|
||||||
@@ -14,6 +15,7 @@ import {
|
|||||||
getCursorFromZoomState,
|
getCursorFromZoomState,
|
||||||
getCursorPercentsFromWheelEvent,
|
getCursorPercentsFromWheelEvent,
|
||||||
getPanDeltaPercentsFromMove,
|
getPanDeltaPercentsFromMove,
|
||||||
|
getPictureRatio,
|
||||||
getWheelPanOffset,
|
getWheelPanOffset,
|
||||||
getZoomTransform,
|
getZoomTransform,
|
||||||
} from '../utils/screenShareZoom'
|
} from '../utils/screenShareZoom'
|
||||||
@@ -73,6 +75,22 @@ export const useScreenShareZoom = () => {
|
|||||||
el.style.transform = getZoomTransform(zoomRef.current, panRef.current)
|
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 applyCursor = useCallback(() => {
|
||||||
const el = surfaceElRef.current
|
const el = surfaceElRef.current
|
||||||
if (!el) return
|
if (!el) return
|
||||||
@@ -85,21 +103,23 @@ export const useScreenShareZoom = () => {
|
|||||||
const zoomIn = useCallback(() => {
|
const zoomIn = useCallback(() => {
|
||||||
const next = clampZoom(zoomRef.current + ZOOM_STEP)
|
const next = clampZoom(zoomRef.current + ZOOM_STEP)
|
||||||
zoomRef.current = next
|
zoomRef.current = next
|
||||||
panRef.current = clampPan(panRef.current, next)
|
panRef.current = clampPan(panRef.current, next, readPictureRatio())
|
||||||
applyTransform()
|
applyTransform()
|
||||||
applyCursor()
|
applyCursor()
|
||||||
flush()
|
flush()
|
||||||
}, [applyTransform, applyCursor, flush])
|
}, [applyTransform, applyCursor, flush, readPictureRatio])
|
||||||
|
|
||||||
const zoomOut = useCallback(() => {
|
const zoomOut = useCallback(() => {
|
||||||
const next = clampZoom(zoomRef.current - ZOOM_STEP)
|
const next = clampZoom(zoomRef.current - ZOOM_STEP)
|
||||||
zoomRef.current = next
|
zoomRef.current = next
|
||||||
panRef.current =
|
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()
|
applyTransform()
|
||||||
applyCursor()
|
applyCursor()
|
||||||
flush()
|
flush()
|
||||||
}, [applyTransform, applyCursor, flush])
|
}, [applyTransform, applyCursor, flush, readPictureRatio])
|
||||||
|
|
||||||
const resetZoom = useCallback(() => {
|
const resetZoom = useCallback(() => {
|
||||||
zoomRef.current = MIN_ZOOM
|
zoomRef.current = MIN_ZOOM
|
||||||
@@ -136,6 +156,7 @@ export const useScreenShareZoom = () => {
|
|||||||
nextZoom: next,
|
nextZoom: next,
|
||||||
cursorXPercent,
|
cursorXPercent,
|
||||||
cursorYPercent,
|
cursorYPercent,
|
||||||
|
ratio: readPictureRatio(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -143,7 +164,7 @@ export const useScreenShareZoom = () => {
|
|||||||
applyCursor()
|
applyCursor()
|
||||||
flush()
|
flush()
|
||||||
},
|
},
|
||||||
[applyTransform, applyCursor, flush]
|
[applyTransform, applyCursor, flush, readPictureRatio]
|
||||||
)
|
)
|
||||||
|
|
||||||
// useMove handles mouse drag + touch pan. Keyboard arrows are not handled
|
// useMove handles mouse drag + touch pan. Keyboard arrows are not handled
|
||||||
@@ -173,7 +194,8 @@ export const useScreenShareZoom = () => {
|
|||||||
x: panRef.current.x + deltaXPercent,
|
x: panRef.current.x + deltaXPercent,
|
||||||
y: panRef.current.y + deltaYPercent,
|
y: panRef.current.y + deltaYPercent,
|
||||||
},
|
},
|
||||||
zoomRef.current
|
zoomRef.current,
|
||||||
|
readPictureRatio()
|
||||||
)
|
)
|
||||||
|
|
||||||
applyTransform()
|
applyTransform()
|
||||||
@@ -195,12 +217,13 @@ export const useScreenShareZoom = () => {
|
|||||||
(dx: number, dy: number) => {
|
(dx: number, dy: number) => {
|
||||||
panRef.current = clampPan(
|
panRef.current = clampPan(
|
||||||
{ x: panRef.current.x + dx, y: panRef.current.y + dy },
|
{ x: panRef.current.x + dx, y: panRef.current.y + dy },
|
||||||
zoomRef.current
|
zoomRef.current,
|
||||||
|
readPictureRatio()
|
||||||
)
|
)
|
||||||
applyTransform()
|
applyTransform()
|
||||||
flush()
|
flush()
|
||||||
},
|
},
|
||||||
[applyTransform, flush]
|
[applyTransform, flush, readPictureRatio]
|
||||||
)
|
)
|
||||||
|
|
||||||
// Attached to the tile container (not the zoom surface) where keyboard
|
// Attached to the tile container (not the zoom surface) where keyboard
|
||||||
|
|||||||
@@ -10,6 +10,14 @@ export interface PanOffset {
|
|||||||
y: number
|
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 {
|
export interface ZoomSnapshot {
|
||||||
zoomLevel: number
|
zoomLevel: number
|
||||||
zoomPercentage: number
|
zoomPercentage: number
|
||||||
@@ -24,15 +32,41 @@ export const clampZoom = (value: number) => {
|
|||||||
return Math.max(MIN_ZOOM, Math.min(MAX_ZOOM, value))
|
return Math.max(MIN_ZOOM, Math.min(MAX_ZOOM, value))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Restrict pan so the picture always covers the view.
|
// Restrict pan so the picture always covers the view. Pan is a % of the
|
||||||
export const clampPan = (pan: PanOffset, zoom: number): PanOffset => {
|
// surface, in which object-fit: contain letterboxes the picture: its half
|
||||||
const maxPan = ((zoom - 1) / zoom) * PAN_CLAMP_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 {
|
return {
|
||||||
x: Math.max(-maxPan, Math.min(maxPan, pan.x)),
|
x: Math.max(-maxPanX, Math.min(maxPanX, pan.x)),
|
||||||
y: Math.max(-maxPan, Math.min(maxPan, pan.y)),
|
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 = (
|
export const buildZoomSnapshot = (
|
||||||
zoom: number,
|
zoom: number,
|
||||||
pan: PanOffset,
|
pan: PanOffset,
|
||||||
@@ -68,12 +102,14 @@ export const getWheelPanOffset = ({
|
|||||||
nextZoom,
|
nextZoom,
|
||||||
cursorXPercent,
|
cursorXPercent,
|
||||||
cursorYPercent,
|
cursorYPercent,
|
||||||
|
ratio,
|
||||||
}: {
|
}: {
|
||||||
pan: PanOffset
|
pan: PanOffset
|
||||||
prevZoom: number
|
prevZoom: number
|
||||||
nextZoom: number
|
nextZoom: number
|
||||||
cursorXPercent: number
|
cursorXPercent: number
|
||||||
cursorYPercent: number
|
cursorYPercent: number
|
||||||
|
ratio: PictureRatio
|
||||||
}): PanOffset => {
|
}): PanOffset => {
|
||||||
const panShift = 1 / nextZoom - 1 / prevZoom
|
const panShift = 1 / nextZoom - 1 / prevZoom
|
||||||
return clampPan(
|
return clampPan(
|
||||||
@@ -81,7 +117,8 @@ export const getWheelPanOffset = ({
|
|||||||
x: pan.x + cursorXPercent * panShift,
|
x: pan.x + cursorXPercent * panShift,
|
||||||
y: pan.y + cursorYPercent * panShift,
|
y: pan.y + cursorYPercent * panShift,
|
||||||
},
|
},
|
||||||
nextZoom
|
nextZoom,
|
||||||
|
ratio
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user