@@ -17,6 +17,7 @@ const ZOOM_FACTOR = 0.001;
1717const LINE_HEIGHT_MULTIPLIER = 33 ;
1818const MAX_SCALE = 8 ;
1919const MIN_SCALE = 1 ;
20+ const SCALE_EPSILON = 0.0001 ;
2021
2122type Size = {
2223 width : number ;
@@ -87,6 +88,23 @@ const getAxisPosition = (
8788 : centeredPosition ;
8889} ;
8990
91+ const axisTouchesViewport = ( scaledSize : number , viewportSize : number ) =>
92+ scaledSize >= viewportSize - SCALE_EPSILON ;
93+
94+ const getFirstViewportEdgeScale = (
95+ baseDimensions : Size ,
96+ viewportWidth : number ,
97+ viewportHeight : number ,
98+ ) =>
99+ Math . max (
100+ MIN_SCALE ,
101+ Math . min (
102+ MAX_SCALE ,
103+ viewportWidth / baseDimensions . width ,
104+ viewportHeight / baseDimensions . height ,
105+ ) ,
106+ ) ;
107+
90108export const ZoomableImage = forwardRef < ZoomableImageRef , ZoomableImageProps > (
91109 ( { imagePath, alt, rotation, resetSignal } , ref ) => {
92110 const transformRef = useRef < ReactZoomPanPinchRef > ( null ) ;
@@ -111,26 +129,29 @@ export const ZoomableImage = forwardRef<ZoomableImageRef, ZoomableImageProps>(
111129 [ ] ,
112130 ) ;
113131
114- const getScaledDimensions = useCallback (
115- ( scale : number ) : Size | null => {
116- if ( ! imageRef . current ) return null ;
132+ const getBaseDimensions = useCallback ( ( ) : Size | null => {
133+ if ( ! imageRef . current ) return null ;
117134
118- const renderedWidth = imageRef . current . clientWidth ;
119- const renderedHeight = imageRef . current . clientHeight ;
135+ const renderedWidth = imageRef . current . clientWidth ;
136+ const renderedHeight = imageRef . current . clientHeight ;
120137
121- if ( ! renderedWidth || ! renderedHeight ) return null ;
138+ if ( ! renderedWidth || ! renderedHeight ) return null ;
122139
123- const effectiveDims = getEffectiveDimensions (
124- renderedWidth ,
125- renderedHeight ,
126- ) ;
140+ return getEffectiveDimensions ( renderedWidth , renderedHeight ) ;
141+ } , [ getEffectiveDimensions ] ) ;
142+
143+ const getScaledDimensions = useCallback (
144+ ( scale : number ) : Size | null => {
145+ const baseDimensions = getBaseDimensions ( ) ;
146+
147+ if ( ! baseDimensions ) return null ;
127148
128149 return {
129- width : effectiveDims . width * scale ,
130- height : effectiveDims . height * scale ,
150+ width : baseDimensions . width * scale ,
151+ height : baseDimensions . height * scale ,
131152 } ;
132153 } ,
133- [ getEffectiveDimensions ] ,
154+ [ getBaseDimensions ] ,
134155 ) ;
135156
136157 const getOverflowState = useCallback (
@@ -286,10 +307,42 @@ export const ZoomableImage = forwardRef<ZoomableImageRef, ZoomableImageProps>(
286307 const zoomChange = - e . deltaY * multiplier * factor ;
287308
288309 const currentScale = transformState . scale ;
289- const newScale = Math . max (
310+ const desiredScale = Math . max (
290311 MIN_SCALE ,
291312 Math . min ( MAX_SCALE , currentScale + zoomChange ) ,
292313 ) ;
314+ const baseDimensions = getBaseDimensions ( ) ;
315+ if ( ! baseDimensions ) return ;
316+
317+ const currentDimensions = {
318+ width : baseDimensions . width * currentScale ,
319+ height : baseDimensions . height * currentScale ,
320+ } ;
321+ const currentTouchesViewport = {
322+ width : axisTouchesViewport (
323+ currentDimensions . width ,
324+ viewportRect . width ,
325+ ) ,
326+ height : axisTouchesViewport (
327+ currentDimensions . height ,
328+ viewportRect . height ,
329+ ) ,
330+ } ;
331+ const isZoomingIn = desiredScale > currentScale ;
332+
333+ const shouldFitFirst =
334+ isZoomingIn &&
335+ ! currentTouchesViewport . width &&
336+ ! currentTouchesViewport . height ;
337+ const firstViewportEdgeScale = getFirstViewportEdgeScale (
338+ baseDimensions ,
339+ viewportRect . width ,
340+ viewportRect . height ,
341+ ) ;
342+ const newScale =
343+ shouldFitFirst && desiredScale > firstViewportEdgeScale
344+ ? firstViewportEdgeScale
345+ : desiredScale ;
293346
294347 const scaledDimensions = getScaledDimensions ( newScale ) ;
295348 if ( ! scaledDimensions ) return ;
@@ -318,19 +371,23 @@ export const ZoomableImage = forwardRef<ZoomableImageRef, ZoomableImageProps>(
318371 mouseViewportX - ( mouseViewportX - transformState . positionX ) * ratio ;
319372 const anchoredY =
320373 mouseViewportY - ( mouseViewportY - transformState . positionY ) * ratio ;
374+ const shouldAnchorX =
375+ isOverImage && currentTouchesViewport . width && newOverflow . width ;
376+ const shouldAnchorY =
377+ isOverImage && currentTouchesViewport . height && newOverflow . height ;
321378
322379 const targetX = shouldRecenter
323380 ? centeredX
324381 : getAxisPosition (
325- isOverImage ? anchoredX : centeredX ,
382+ shouldAnchorX ? anchoredX : centeredX ,
326383 viewportRect . width ,
327384 scaledDimensions . width ,
328385 newOverflow . width ,
329386 ) ;
330387 const targetY = shouldRecenter
331388 ? centeredY
332389 : getAxisPosition (
333- isOverImage ? anchoredY : centeredY ,
390+ shouldAnchorY ? anchoredY : centeredY ,
334391 viewportRect . height ,
335392 scaledDimensions . height ,
336393 newOverflow . height ,
@@ -349,7 +406,12 @@ export const ZoomableImage = forwardRef<ZoomableImageRef, ZoomableImageProps>(
349406 resizeObserver . disconnect ( ) ;
350407 wheelElement . removeEventListener ( 'wheel' , handleWheelInterceptor , true ) ;
351408 } ;
352- } , [ getOverflowState , getScaledDimensions , getViewportElement ] ) ;
409+ } , [
410+ getBaseDimensions ,
411+ getOverflowState ,
412+ getScaledDimensions ,
413+ getViewportElement ,
414+ ] ) ;
353415
354416 return (
355417 < div ref = { wheelAreaRef } className = "h-full w-full" >
0 commit comments