/ proof-of-concept / phase-perception-2
Sign in
proof-of-concept / phase-perception-2
phase-perception-2 / src / hooks / useZoomPan.ts
115 lines · 4.1 KBCodeBlameHistory
7886a79Waveforms: wheel zoom, drag pan, double-click reset, shared view marked on the spectrogramsJeremy Magland 1import { useEffect, useRef, type RefObject } from 'react'
3export type View = { start: number; span: number }
5/** Keep the view inside [0, duration] with a span of at least minSpan. */
6export function clampView(v: View, duration: number, minSpan: number): View {
7 const span = Math.min(duration, Math.max(minSpan, v.span))
8 const start = Math.min(Math.max(0, v.start), Math.max(0, duration - span))
9 return { start, span }
12/**
e9f92d1Time-varying random phase maps by decimated subband synthesis; drag-to-zoom, scrollbar pan and zoom buttons for the waveformsJeremy Magland 13 * Zoom interactions on a canvas whose plot area spans horizontally from
14 * `left` to `width - right`: drag across a range to zoom into it (the range is
15 * shown in the overlay element while dragging), wheel to zoom about the cursor
16 * or, with horizontal scrolling, to pan, and double-click to reset. The
17 * listeners are registered once and read the latest view through a ref.
20 ref: RefObject<HTMLCanvasElement | null>,
21 overlayRef: RefObject<HTMLDivElement | null>,
22 view: View,
23 duration: number,
24 minSpan: number,
25 margins: { left: number; right: number },
26 onView: (v: View) => void,
27): void {
29 viewRef.current = view
30 const onViewRef = useRef(onView)
31 onViewRef.current = onView
34 useEffect(() => {
35 const el = ref.current
36 if (!el) return
37 const plotWidth = () => el.clientWidth - margins.left - margins.right
e9f92d1Time-varying random phase maps by decimated subband synthesis; drag-to-zoom, scrollbar pan and zoom buttons for the waveformsJeremy Magland 38 const plotX = (clientX: number) => clientX - el.getBoundingClientRect().left - margins.left
40 const v = viewRef.current
43 const emit = (v: View) => onViewRef.current(clampView(v, duration, minSpan))
45 const ov = overlayRef.current
46 const d = drag.current
47 if (!ov) return
48 if (!d) {
49 ov.style.display = 'none'
50 return
51 }
52 const a = Math.max(0, Math.min(d.x0, d.x1))
53 const b = Math.min(plotWidth(), Math.max(d.x0, d.x1))
54 ov.style.display = 'block'
55 ov.style.left = `${margins.left + a}px`
56 ov.style.width = `${Math.max(1, b - a)}px`
57 }
59 const onWheel = (e: WheelEvent) => {
60 e.preventDefault()
61 const v = viewRef.current
63 emit({ start: v.start + (e.deltaX / plotWidth()) * v.span, span: v.span })
64 return
65 }
7886a79Waveforms: wheel zoom, drag pan, double-click reset, shared view marked on the spectrogramsJeremy Magland 66 const factor = Math.exp(e.deltaY * 0.0015)
67 const t = timeAt(e.clientX)
68 const span = v.span * factor
69 emit({ start: t - (t - v.start) * (span / v.span), span })
70 }
71 const onDown = (e: PointerEvent) => {
72 if (e.button !== 0) return
74 drag.current = { x0: x, x1: x }
78 const onMove = (e: PointerEvent) => {
79 const d = drag.current
80 if (!d) return
82 showSelection()
84 const onUp = (e: PointerEvent) => {
86 if (!d) return
7886a79Waveforms: wheel zoom, drag pan, double-click reset, shared view marked on the spectrogramsJeremy Magland 89 if (el.hasPointerCapture(e.pointerId)) el.releasePointerCapture(e.pointerId)
91 const b = Math.max(d.x0, d.x1)
92 if (b - a < 4) return
93 const v = viewRef.current
94 const pw = plotWidth()
95 emit({ start: v.start + (a / pw) * v.span, span: ((b - a) / pw) * v.span })
97 const onDouble = () => emit({ start: 0, span: duration })
99 el.style.touchAction = 'none'
100 el.addEventListener('wheel', onWheel, { passive: false })
101 el.addEventListener('pointerdown', onDown)
102 el.addEventListener('pointermove', onMove)
103 el.addEventListener('pointerup', onUp)
104 el.addEventListener('pointercancel', onUp)
105 el.addEventListener('dblclick', onDouble)
106 return () => {
107 el.removeEventListener('wheel', onWheel)
108 el.removeEventListener('pointerdown', onDown)
109 el.removeEventListener('pointermove', onMove)
110 el.removeEventListener('pointerup', onUp)
111 el.removeEventListener('pointercancel', onUp)
112 el.removeEventListener('dblclick', onDouble)
113 }
↑↓ move↵openescclose