From aeab9c79e6eafd753ed3ad8b2ef2cf4f1f721c88 Mon Sep 17 00:00:00 2001 From: Elodie MORIN Date: Wed, 9 Sep 2026 15:22:42 +0200 Subject: [PATCH] fix(annotator): zoom with mouse scroll should stay centered on mouse --- .../src/features/Annotator/Canvas/Window.tsx | 28 +++++++------------ frontend/src/features/Annotator/Zoom/Root.tsx | 23 +++++++-------- 2 files changed, 20 insertions(+), 31 deletions(-) diff --git a/frontend/src/features/Annotator/Canvas/Window.tsx b/frontend/src/features/Annotator/Canvas/Window.tsx index bcd32ab0f..c338345b3 100644 --- a/frontend/src/features/Annotator/Canvas/Window.tsx +++ b/frontend/src/features/Annotator/Canvas/Window.tsx @@ -10,7 +10,7 @@ import { useTempAnnotationsEvents, } from '@/features/Annotator/Annotation'; import { useWindowContainerWidth, useWindowHeight, useWindowWidth, Y_AXIS_WIDTH } from './window.hooks'; -import { useGetCoords, useGetFreqTime, useIsHoverCanvas, usePointer } from '@/features/Annotator/Pointer'; +import { useGetFreqTime, useIsHoverCanvas, usePointer } from '@/features/Annotator/Pointer'; import { Zoom } from '@/features/Annotator/Zoom'; import { useAudio } from '@/features/Audio'; import { useAnnotatorCanvasContext } from '@/features/Annotator/Canvas/context'; @@ -35,7 +35,6 @@ export const AnnotatorCanvasWindow: React.FC = () => { } = useAnnotatorCanvasContext() const { onStartTempAnnotation } = useTempAnnotationsEvents() const getFreqTime = useGetFreqTime() - const getCoords = useGetCoords() const { zoomLevel, zoomType, @@ -87,11 +86,11 @@ export const AnnotatorCanvasWindow: React.FC = () => { // Disable zoom if the user wants horizontal scroll if (event.shiftKey) return; - const origin = getCoords(event); + const origin = getFreqTime(event); if (!origin) return; if (event.deltaY < 0) zoomIn(origin) else if (event.deltaY > 0) zoomOut(origin) - }, [ zoomIn, zoomOut, getCoords ]) + }, [ zoomIn, zoomOut, getFreqTime ]) const seekAudio = useCallback((event: MouseEvent) => { seek(getFreqTime(event)?.time ?? 0) @@ -133,7 +132,7 @@ export const AnnotatorCanvasWindow: React.FC = () => { // Zoom update const isHoverCanvas = useIsHoverCanvas() - const onZoomUpdated: OnZoomInfoCallback = useCallback(({ previousLevel, level, origin }) => { + const onZoomUpdated: OnZoomInfoCallback = useCallback(({ level, origin }) => { const mainBounds = interactionCanvasRef?.current?.getBoundingClientRect() if (!window || !spectrogram || !mainBounds) return; @@ -143,23 +142,17 @@ export const AnnotatorCanvasWindow: React.FC = () => { // Compute new center (before resizing) let newCenter: number; if (origin) { - // x-coordinate has been given, center on it - newCenter = (origin.x - mainBounds.left) * level / previousLevel; - const coords = { - clientX: origin.x, - clientY: origin.y, - } - if (isHoverCanvas(coords)) { - const data = getFreqTime(coords); - if (data) pointer.setPosition(data) - } + newCenter = origin.time * newTimePxRatio } else { // If no x-coordinate: center on currentTime newCenter = oldTime.current * newTimePxRatio; } - window.scrollTo({ left: Math.floor(newCenter - containerWidth / 2) }) + const left = Math.floor(newCenter - containerWidth / 2) + setTimeout(() => requestAnimationFrame(() => { + windowCanvasRef?.current?.scrollTo({ left, behavior: 'instant' }) + })) refreshInteractionCanvas() - }, [ refreshInteractionCanvas, isHoverCanvas, pointer, getFreqTime, interactionCanvasRef, spectrogram, containerWidth ]) + }, [ refreshInteractionCanvas, isHoverCanvas, pointer, getFreqTime, interactionCanvasRef, spectrogram, containerWidth, windowCanvasRef ]) useEffect(() => { onZoomUpdatedSignal.add(onZoomUpdated) return () => { @@ -168,7 +161,6 @@ export const AnnotatorCanvasWindow: React.FC = () => { }, [ onZoomUpdatedSignal ]); useEffect(() => { onZoomUpdated({ - previousLevel: zoomLevel, level: zoomLevel, type: zoomType, }) diff --git a/frontend/src/features/Annotator/Zoom/Root.tsx b/frontend/src/features/Annotator/Zoom/Root.tsx index 1f1863819..04da2f311 100644 --- a/frontend/src/features/Annotator/Zoom/Root.tsx +++ b/frontend/src/features/Annotator/Zoom/Root.tsx @@ -1,13 +1,12 @@ import React, { createContext, type HTMLProps, useCallback, useContext, useMemo, useState } from 'react'; import { Signal } from 'signal-ts'; -import type { Point } from '@/service/type'; import type { CampaignAnalysisFragment, GetCampaignQuery } from '@/features/AnnotationCampaign'; +import type { TimeFreqPosition } from '@/features/Annotator/Pointer'; type ZoomInfo = { - previousLevel: number; level: number; type: 'preprocessed' | 'digital'; - origin?: Point; + origin?: TimeFreqPosition; } export type OnZoomInfoCallback = (zoomInfo: ZoomInfo) => void; @@ -17,11 +16,11 @@ type ZoomContext = { canZoomIn: boolean | 'digital'; zoomInLevel: number | null; - zoomIn: (origin?: Point) => void; + zoomIn: (origin?: TimeFreqPosition) => void; canZoomOut: boolean | 'digital'; zoomOutLevel: number | null; - zoomOut: (origin?: Point) => void; + zoomOut: (origin?: TimeFreqPosition) => void; resetZoom: () => void; @@ -100,35 +99,33 @@ export const ZoomRoot: React.FC = ({ children, campaign, analysis }) => { return null }, [ zoomLevel ]) - const zoomIn = useCallback((origin?: Point) => { + const zoomIn = useCallback((origin?: TimeFreqPosition) => { + console.debug('zoomIn', origin) if (zoomInLevel === null) return; signal.emit({ - previousLevel: zoomLevel, level: zoomInLevel, type: zoomInLevel > maxPreProcessedZoomLevel ? 'digital' : 'preprocessed', origin, }) setZoomLevel(zoomInLevel) - }, [ zoomLevel, zoomInLevel, signal, maxPreProcessedZoomLevel ]) - const zoomOut = useCallback((origin?: Point) => { + }, [ zoomInLevel, signal, maxPreProcessedZoomLevel ]) + const zoomOut = useCallback((origin?: TimeFreqPosition) => { if (zoomOutLevel === null) return; signal.emit({ - previousLevel: zoomLevel, level: zoomOutLevel, type: zoomOutLevel > maxPreProcessedZoomLevel ? 'digital' : 'preprocessed', origin, }) setZoomLevel(zoomOutLevel) - }, [ zoomLevel, zoomOutLevel, signal, maxPreProcessedZoomLevel ]) + }, [ zoomOutLevel, signal, maxPreProcessedZoomLevel ]) const resetZoom = useCallback(() => { signal.emit({ - previousLevel: zoomLevel, level: 1, type: 'preprocessed', }) setZoomLevel(1) - }, [ signal, zoomLevel ]) + }, [ signal ]) return