mirror of
https://github.com/suitenumerique/meet.git
synced 2026-07-26 11:58:53 +00:00
✨(frontend) add useScreenShareZoom hook for zoom and pan state management
Manages zoom level, pan offset, wheel zoom, drag-to-pan and keyboard panBy.
This commit is contained in:
@@ -0,0 +1,227 @@
|
||||
import { useCallback, useRef, useState } from 'react'
|
||||
|
||||
const MIN_ZOOM = 1
|
||||
const MAX_ZOOM = 4
|
||||
const ZOOM_STEP = 0.1
|
||||
const WHEEL_ZOOM_SPEED = 0.002
|
||||
const WHEEL_ZOOM_FOCUS_BLEND = 0.3
|
||||
const PAN_STEP = 5
|
||||
const PAN_CLAMP_HALF = 50
|
||||
|
||||
interface PanOffset {
|
||||
x: number
|
||||
y: number
|
||||
}
|
||||
|
||||
export function useScreenShareZoom() {
|
||||
const [zoomLevel, setZoomLevel] = useState(MIN_ZOOM)
|
||||
const [panOffset, setPanOffset] = useState<PanOffset>({ x: 0, y: 0 })
|
||||
const [isDragging, setIsDragging] = useState(false)
|
||||
|
||||
// Ref for event handlers; state drives cursor re-renders during drag.
|
||||
const isDraggingRef = useRef(false)
|
||||
const dragStart = useRef<PanOffset>({ x: 0, y: 0 })
|
||||
const panStart = useRef<PanOffset>({ x: 0, y: 0 })
|
||||
|
||||
const isZoomed = zoomLevel > MIN_ZOOM
|
||||
const canZoomIn = zoomLevel < MAX_ZOOM
|
||||
const canZoomOut = zoomLevel > MIN_ZOOM
|
||||
|
||||
const clampPan = useCallback(
|
||||
(pan: PanOffset, zoom: number): PanOffset => {
|
||||
// Max pan in %, keeps content within the visible area at a given zoom level.
|
||||
const maxPan = ((zoom - 1) / zoom) * PAN_CLAMP_HALF
|
||||
return {
|
||||
x: Math.max(-maxPan, Math.min(maxPan, pan.x)),
|
||||
y: Math.max(-maxPan, Math.min(maxPan, pan.y)),
|
||||
}
|
||||
},
|
||||
[]
|
||||
)
|
||||
|
||||
const clampZoom = useCallback((value: number) => {
|
||||
return Math.max(MIN_ZOOM, Math.min(MAX_ZOOM, value))
|
||||
}, [])
|
||||
|
||||
const zoomIn = useCallback(() => {
|
||||
setZoomLevel((prev) => {
|
||||
const next = clampZoom(prev + ZOOM_STEP)
|
||||
setPanOffset((pan) => clampPan(pan, next))
|
||||
return next
|
||||
})
|
||||
}, [clampZoom, clampPan])
|
||||
|
||||
const zoomOut = useCallback(() => {
|
||||
setZoomLevel((prev) => {
|
||||
const next = clampZoom(prev - ZOOM_STEP)
|
||||
if (next <= MIN_ZOOM) {
|
||||
setPanOffset({ x: 0, y: 0 })
|
||||
return MIN_ZOOM
|
||||
}
|
||||
setPanOffset((pan) => clampPan(pan, next))
|
||||
return next
|
||||
})
|
||||
}, [clampZoom, clampPan])
|
||||
|
||||
const resetZoom = useCallback(() => {
|
||||
setZoomLevel(MIN_ZOOM)
|
||||
setPanOffset({ x: 0, y: 0 })
|
||||
}, [])
|
||||
|
||||
// Arrow key panning (keyboard a11y).
|
||||
const panBy = useCallback(
|
||||
(dx: number, dy: number) => {
|
||||
setPanOffset((pan) =>
|
||||
clampPan({ x: pan.x + dx, y: pan.y + dy }, zoomLevel)
|
||||
)
|
||||
},
|
||||
[zoomLevel, clampPan]
|
||||
)
|
||||
|
||||
const handleWheel = useCallback(
|
||||
(e: React.WheelEvent<HTMLDivElement>) => {
|
||||
// Match browser zoom gesture (Ctrl/Cmd + scroll).
|
||||
if (!e.ctrlKey && !e.metaKey) return
|
||||
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
|
||||
const delta = -e.deltaY * WHEEL_ZOOM_SPEED
|
||||
setZoomLevel((prev) => {
|
||||
const next = clampZoom(prev + delta)
|
||||
|
||||
if (next <= MIN_ZOOM) {
|
||||
setPanOffset({ x: 0, y: 0 })
|
||||
return MIN_ZOOM
|
||||
}
|
||||
|
||||
const rect = e.currentTarget.getBoundingClientRect()
|
||||
const cursorXPercent =
|
||||
((e.clientX - rect.left) / rect.width) * 100 - PAN_CLAMP_HALF
|
||||
const cursorYPercent =
|
||||
((e.clientY - rect.top) / rect.height) * 100 - PAN_CLAMP_HALF
|
||||
|
||||
setPanOffset((pan) => {
|
||||
const zoomRatio = next / prev
|
||||
return clampPan(
|
||||
{
|
||||
x:
|
||||
pan.x +
|
||||
(cursorXPercent - pan.x) *
|
||||
(1 - 1 / zoomRatio) *
|
||||
WHEEL_ZOOM_FOCUS_BLEND,
|
||||
y:
|
||||
pan.y +
|
||||
(cursorYPercent - pan.y) *
|
||||
(1 - 1 / zoomRatio) *
|
||||
WHEEL_ZOOM_FOCUS_BLEND,
|
||||
},
|
||||
next
|
||||
)
|
||||
})
|
||||
|
||||
return next
|
||||
})
|
||||
},
|
||||
[clampZoom, clampPan]
|
||||
)
|
||||
|
||||
const handlePanStart = useCallback(
|
||||
(e: React.MouseEvent<HTMLDivElement>) => {
|
||||
if (!isZoomed) return
|
||||
|
||||
e.preventDefault()
|
||||
isDraggingRef.current = true
|
||||
setIsDragging(true)
|
||||
dragStart.current = { x: e.clientX, y: e.clientY }
|
||||
panStart.current = { ...panOffset }
|
||||
},
|
||||
[isZoomed, panOffset]
|
||||
)
|
||||
|
||||
const handlePanMove = useCallback(
|
||||
(e: React.MouseEvent<HTMLDivElement>) => {
|
||||
if (!isDraggingRef.current) return
|
||||
|
||||
const rect = e.currentTarget.getBoundingClientRect()
|
||||
const deltaXPercent =
|
||||
((e.clientX - dragStart.current.x) / rect.width) * 100
|
||||
const deltaYPercent =
|
||||
((e.clientY - dragStart.current.y) / rect.height) * 100
|
||||
|
||||
setPanOffset(
|
||||
clampPan(
|
||||
{
|
||||
x: panStart.current.x + deltaXPercent,
|
||||
y: panStart.current.y + deltaYPercent,
|
||||
},
|
||||
zoomLevel
|
||||
)
|
||||
)
|
||||
},
|
||||
[zoomLevel, clampPan]
|
||||
)
|
||||
|
||||
const handlePanEnd = useCallback(() => {
|
||||
isDraggingRef.current = false
|
||||
setIsDragging(false)
|
||||
}, [])
|
||||
|
||||
const handleKeyDown = useCallback(
|
||||
(e: KeyboardEvent) => {
|
||||
if (!isZoomed && e.key !== '+' && e.key !== '=') return
|
||||
|
||||
switch (e.key) {
|
||||
case 'ArrowLeft':
|
||||
e.preventDefault()
|
||||
panBy(PAN_STEP, 0)
|
||||
break
|
||||
case 'ArrowRight':
|
||||
e.preventDefault()
|
||||
panBy(-PAN_STEP, 0)
|
||||
break
|
||||
case 'ArrowUp':
|
||||
e.preventDefault()
|
||||
panBy(0, PAN_STEP)
|
||||
break
|
||||
case 'ArrowDown':
|
||||
e.preventDefault()
|
||||
panBy(0, -PAN_STEP)
|
||||
break
|
||||
case '+':
|
||||
case '=':
|
||||
e.preventDefault()
|
||||
zoomIn()
|
||||
break
|
||||
case '-':
|
||||
e.preventDefault()
|
||||
zoomOut()
|
||||
break
|
||||
case '0':
|
||||
e.preventDefault()
|
||||
resetZoom()
|
||||
break
|
||||
}
|
||||
},
|
||||
[isZoomed, panBy, zoomIn, zoomOut, resetZoom]
|
||||
)
|
||||
|
||||
return {
|
||||
zoomLevel,
|
||||
zoomPercentage: Math.round(zoomLevel * 100),
|
||||
panOffset,
|
||||
isZoomed,
|
||||
isDragging,
|
||||
canZoomIn,
|
||||
canZoomOut,
|
||||
zoomIn,
|
||||
zoomOut,
|
||||
resetZoom,
|
||||
panBy,
|
||||
handleWheel,
|
||||
handlePanStart,
|
||||
handlePanMove,
|
||||
handlePanEnd,
|
||||
handleKeyDown,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user