11import { type RefObject , useEffect , useLayoutEffect , useRef , useState } from 'react' ;
22
33import { isZoomExcludedTarget } from '../../helpers/canvas-positioning' ;
4- import { isZoomKey , nextZoom } from '../../helpers/zoom' ;
4+ import { isZoomKey , maxZoom , minZoom , nextZoom } from '../../helpers/zoom' ;
55
66interface UseCanvasZoomOptions {
77 canvasRef : RefObject < HTMLElement | null > ;
@@ -13,6 +13,8 @@ export function useCanvasZoom({ canvasRef, setZoom, zoom }: UseCanvasZoomOptions
1313 const pendingZoomAnchor = useRef < ZoomAnchor | null > ( null ) ;
1414 const [ isZoomKeyDown , setIsZoomKeyDown ] = useState ( false ) ;
1515 const [ isAltDown , setIsAltDown ] = useState ( false ) ;
16+ const zoomRef = useRef ( zoom ) ;
17+ zoomRef . current = zoom ;
1618
1719 useEffect ( ( ) => {
1820 const root = document . documentElement ;
@@ -30,26 +32,82 @@ export function useCanvasZoom({ canvasRef, setZoom, zoom }: UseCanvasZoomOptions
3032 event . preventDefault ( ) ;
3133 event . stopPropagation ( ) ;
3234 event . stopImmediatePropagation ( ) ;
33- changeZoom ( event . altKey || isAltDown ? - 1 : 1 , { x : event . clientX , y : event . clientY } ) ;
35+ changeZoom ( event . altKey || isAltDown ? - 1 : 1 , {
36+ x : event . clientX ,
37+ y : event . clientY
38+ } ) ;
3439 }
3540
3641 document . addEventListener ( 'click' , handleDocumentClick , true ) ;
3742 return ( ) => document . removeEventListener ( 'click' , handleDocumentClick , true ) ;
38- } , [ isAltDown , isZoomKeyDown , zoom ] ) ;
43+ } , [ isAltDown , isZoomKeyDown ] ) ;
44+
45+ useEffect ( ( ) => {
46+ const canvas = canvasRef . current ;
47+ if ( ! canvas ) return undefined ;
48+
49+ function handleWheel ( event : WheelEvent ) {
50+ const { ctrlKey, metaKey } = event ;
51+ if ( ! ctrlKey && ! metaKey ) return ;
52+ if ( isZoomExcludedTarget ( event . target ) ) return ;
53+
54+ event . preventDefault ( ) ;
55+
56+ const { current } = zoomRef ;
57+ const anchor = { x : event . clientX , y : event . clientY } ;
58+ const delta = - event . deltaY ;
59+
60+ if ( ctrlKey ) {
61+ // Pinch / Ctrl+scroll — continuous
62+ const next = Math . round ( Math . min ( maxZoom , Math . max ( minZoom , current + delta * 0.5 ) ) ) ;
63+ if ( next !== current ) {
64+ setZoomAnchor ( current , anchor ) ;
65+ setZoom ( next ) ;
66+ }
67+ } else {
68+ // Cmd+scroll — step zoom
69+ changeZoom ( delta > 0 ? 1 : - 1 , anchor ) ;
70+ }
71+ }
72+
73+ canvas . addEventListener ( 'wheel' , handleWheel , { passive : false } ) ;
74+ return ( ) => canvas . removeEventListener ( 'wheel' , handleWheel ) ;
75+ } , [ canvasRef ] ) ;
3976
4077 useEffect ( ( ) => {
4178 function handleZoomEvent ( event : Event ) {
4279 const direction = Number ( ( event as CustomEvent < number > ) . detail ) ;
4380 if ( direction === - 1 || direction === 1 ) changeZoom ( direction ) ;
4481 }
82+
4583 function handleKeyDown ( event : KeyboardEvent ) {
4684 setIsAltDown ( event . altKey ) ;
4785 if ( isZoomKey ( event ) ) setIsZoomKeyDown ( true ) ;
86+
87+ const isMod = event . metaKey || event . ctrlKey ;
88+ if ( ! isMod ) return ;
89+
90+ if ( event . key === '=' || event . key === '+' ) {
91+ event . preventDefault ( ) ;
92+ changeZoom ( 1 ) ;
93+ } else if ( event . key === '-' ) {
94+ event . preventDefault ( ) ;
95+ changeZoom ( - 1 ) ;
96+ } else if ( event . key === '0' ) {
97+ event . preventDefault ( ) ;
98+ const { current } = zoomRef ;
99+ if ( current !== 100 ) {
100+ setZoomAnchor ( current ) ;
101+ setZoom ( 100 ) ;
102+ }
103+ }
48104 }
105+
49106 function handleKeyUp ( event : KeyboardEvent ) {
50107 setIsAltDown ( event . altKey ) ;
51108 if ( isZoomKey ( event ) ) setIsZoomKeyDown ( false ) ;
52109 }
110+
53111 function handleBlur ( ) {
54112 setIsZoomKeyDown ( false ) ;
55113 setIsAltDown ( false ) ;
@@ -66,7 +124,7 @@ export function useCanvasZoom({ canvasRef, setZoom, zoom }: UseCanvasZoomOptions
66124 window . removeEventListener ( 'keyup' , handleKeyUp ) ;
67125 window . removeEventListener ( 'blur' , handleBlur ) ;
68126 } ;
69- } , [ zoom ] ) ;
127+ } , [ ] ) ;
70128
71129 useLayoutEffect ( ( ) => {
72130 const anchor = pendingZoomAnchor . current ;
@@ -87,27 +145,31 @@ export function useCanvasZoom({ canvasRef, setZoom, zoom }: UseCanvasZoomOptions
87145 pendingZoomAnchor . current = null ;
88146 } , [ canvasRef , zoom ] ) ;
89147
90- function changeZoom ( direction : number , anchorPoint ?: { x : number ; y : number } ) {
91- const next = nextZoom ( zoom , direction ) ;
92- if ( next === zoom ) return ;
148+ function setZoomAnchor ( currentZoom : number , anchorPoint ?: { x : number ; y : number } ) {
93149 const canvas = canvasRef . current ;
94150 const content = canvas ?. firstElementChild ;
151+ if ( ! canvas || ! content ) return ;
152+
153+ const canvasRect = canvas . getBoundingClientRect ( ) ;
154+ const contentRect = content . getBoundingClientRect ( ) ;
155+ const scale = currentZoom / 100 ;
156+ const anchorX = anchorPoint ?. x ?? canvasRect . left + canvasRect . width / 2 ;
157+ const anchorY = anchorPoint ?. y ?? canvasRect . top + canvasRect . height / 2 ;
158+
159+ pendingZoomAnchor . current = {
160+ localX : ( anchorX - contentRect . left ) / scale ,
161+ localY : ( anchorY - contentRect . top ) / scale ,
162+ scale,
163+ scrollLeft : canvas . scrollLeft ,
164+ scrollTop : canvas . scrollTop
165+ } ;
166+ }
95167
96- if ( canvas && content ) {
97- const canvasRect = canvas . getBoundingClientRect ( ) ;
98- const contentRect = content . getBoundingClientRect ( ) ;
99- const scale = zoom / 100 ;
100- const anchorX = anchorPoint ?. x ?? canvasRect . left + canvasRect . width / 2 ;
101- const anchorY = anchorPoint ?. y ?? canvasRect . top + canvasRect . height / 2 ;
102- pendingZoomAnchor . current = {
103- localX : ( anchorX - contentRect . left ) / scale ,
104- localY : ( anchorY - contentRect . top ) / scale ,
105- scale,
106- scrollLeft : canvas . scrollLeft ,
107- scrollTop : canvas . scrollTop
108- } ;
109- }
110-
168+ function changeZoom ( direction : number , anchorPoint ?: { x : number ; y : number } ) {
169+ const { current } = zoomRef ;
170+ const next = nextZoom ( current , direction ) ;
171+ if ( next === current ) return ;
172+ setZoomAnchor ( current , anchorPoint ) ;
111173 setZoom ( next ) ;
112174 }
113175
0 commit comments