@@ -91,17 +91,40 @@ const getAxisPosition = (
9191const axisTouchesViewport = ( scaledSize : number , viewportSize : number ) =>
9292 scaledSize >= viewportSize - SCALE_EPSILON ;
9393
94+ const getMinimumScale = (
95+ baseDimensions : Size ,
96+ viewportWidth : number ,
97+ viewportHeight : number ,
98+ ) => {
99+ if (
100+ ! baseDimensions . width ||
101+ ! baseDimensions . height ||
102+ ! viewportWidth ||
103+ ! viewportHeight
104+ ) {
105+ return MIN_SCALE ;
106+ }
107+
108+ return Math . min (
109+ MIN_SCALE ,
110+ viewportWidth / baseDimensions . width ,
111+ viewportHeight / baseDimensions . height ,
112+ ) ;
113+ } ;
114+
94115const getFirstViewportEdgeScale = (
95116 baseDimensions : Size ,
96117 viewportWidth : number ,
97118 viewportHeight : number ,
98119) =>
99- Math . max (
100- MIN_SCALE ,
101- Math . min (
102- MAX_SCALE ,
103- viewportWidth / baseDimensions . width ,
104- viewportHeight / baseDimensions . height ,
120+ Math . min (
121+ MAX_SCALE ,
122+ Math . max (
123+ getMinimumScale ( baseDimensions , viewportWidth , viewportHeight ) ,
124+ Math . min (
125+ viewportWidth / baseDimensions . width ,
126+ viewportHeight / baseDimensions . height ,
127+ ) ,
105128 ) ,
106129 ) ;
107130
@@ -111,8 +134,15 @@ export const ZoomableImage = forwardRef<ZoomableImageRef, ZoomableImageProps>(
111134 const wheelAreaRef = useRef < HTMLDivElement > ( null ) ;
112135 const imageRef = useRef < HTMLImageElement > ( null ) ;
113136 const [ isOverflowing , setIsOverflowing ] = useState ( false ) ;
137+ const [ minScale , setMinScale ] = useState ( MIN_SCALE ) ;
114138 const rotationRef = useRef ( rotation ) ;
115139
140+ const setMinimumScale = useCallback ( ( scale : number ) => {
141+ setMinScale ( ( currentScale ) =>
142+ Math . abs ( currentScale - scale ) < SCALE_EPSILON ? currentScale : scale ,
143+ ) ;
144+ } , [ ] ) ;
145+
116146 useEffect ( ( ) => {
117147 rotationRef . current = rotation ;
118148 } , [ rotation ] ) ;
@@ -132,8 +162,10 @@ export const ZoomableImage = forwardRef<ZoomableImageRef, ZoomableImageProps>(
132162 const getBaseDimensions = useCallback ( ( ) : Size | null => {
133163 if ( ! imageRef . current ) return null ;
134164
135- const renderedWidth = imageRef . current . clientWidth ;
136- const renderedHeight = imageRef . current . clientHeight ;
165+ const renderedWidth =
166+ imageRef . current . naturalWidth || imageRef . current . clientWidth ;
167+ const renderedHeight =
168+ imageRef . current . naturalHeight || imageRef . current . clientHeight ;
137169
138170 if ( ! renderedWidth || ! renderedHeight ) return null ;
139171
@@ -186,30 +218,21 @@ export const ZoomableImage = forwardRef<ZoomableImageRef, ZoomableImageProps>(
186218 return ;
187219
188220 const wrapperRect = viewportElement . getBoundingClientRect ( ) ;
189- const img = imageRef . current ;
190-
191- const scale = 1 ;
192- const baseW = img . naturalWidth || img . clientWidth ;
193- const baseH = img . naturalHeight || img . clientHeight ;
194-
195- if ( ! baseW || ! baseH ) return ;
196-
197- const effectiveDims = getEffectiveDimensions ( baseW , baseH ) ;
198- const imgAspect = effectiveDims . width / effectiveDims . height ;
199- const viewAspect = wrapperRect . width / wrapperRect . height ;
221+ const baseDimensions = getBaseDimensions ( ) ;
200222
201- let renderedW , renderedH ;
202- if ( imgAspect > viewAspect ) {
203- renderedW = Math . min ( effectiveDims . width , wrapperRect . width ) ;
204- renderedH = renderedW / imgAspect ;
205- } else {
206- renderedH = Math . min ( effectiveDims . height , wrapperRect . height ) ;
207- renderedW = renderedH * imgAspect ;
208- }
223+ if ( ! baseDimensions ) return ;
209224
225+ const scale = getMinimumScale (
226+ baseDimensions ,
227+ wrapperRect . width ,
228+ wrapperRect . height ,
229+ ) ;
230+ const renderedW = baseDimensions . width * scale ;
231+ const renderedH = baseDimensions . height * scale ;
210232 const centerX = getCenteredAxisPosition ( wrapperRect . width , renderedW ) ;
211233 const centerY = getCenteredAxisPosition ( wrapperRect . height , renderedH ) ;
212234
235+ setMinimumScale ( scale ) ;
213236 transformRef . current . setTransform (
214237 centerX ,
215238 centerY ,
@@ -219,7 +242,7 @@ export const ZoomableImage = forwardRef<ZoomableImageRef, ZoomableImageProps>(
219242 ) ;
220243 setIsOverflowing ( false ) ;
221244 } ,
222- [ getEffectiveDimensions , getViewportElement ] ,
245+ [ getBaseDimensions , getViewportElement , setMinimumScale ] ,
223246 ) ;
224247
225248 useImperativeHandle ( ref , ( ) => ( {
@@ -239,10 +262,23 @@ export const ZoomableImage = forwardRef<ZoomableImageRef, ZoomableImageProps>(
239262
240263 const scale = transformRef . current . instance . transformState . scale ;
241264 const rect = viewportElement . getBoundingClientRect ( ) ;
265+ const baseDimensions = getBaseDimensions ( ) ;
266+
267+ if ( baseDimensions ) {
268+ setMinimumScale (
269+ getMinimumScale ( baseDimensions , rect . width , rect . height ) ,
270+ ) ;
271+ }
242272
243273 const overflow = getOverflowState ( scale , rect . width , rect . height ) ;
244274 setIsOverflowing ( overflow . width || overflow . height ) ;
245- } , [ rotation , getOverflowState , getViewportElement ] ) ;
275+ } , [
276+ rotation ,
277+ getBaseDimensions ,
278+ getOverflowState ,
279+ getViewportElement ,
280+ setMinimumScale ,
281+ ] ) ;
246282
247283 useEffect ( ( ) => {
248284 setIsOverflowing ( false ) ;
@@ -271,9 +307,20 @@ export const ZoomableImage = forwardRef<ZoomableImageRef, ZoomableImageProps>(
271307 wheelElement . getBoundingClientRect ( ) ;
272308
273309 const resizeObserver = new ResizeObserver ( ( ) => {
274- cachedViewportRect =
275- getViewportElement ( ) ?. getBoundingClientRect ( ) ??
276- wheelElement . getBoundingClientRect ( ) ;
310+ const viewportElement = getViewportElement ( ) ?? wheelElement ;
311+
312+ cachedViewportRect = viewportElement . getBoundingClientRect ( ) ;
313+
314+ const baseDimensions = getBaseDimensions ( ) ;
315+ if ( baseDimensions ) {
316+ setMinimumScale (
317+ getMinimumScale (
318+ baseDimensions ,
319+ cachedViewportRect . width ,
320+ cachedViewportRect . height ,
321+ ) ,
322+ ) ;
323+ }
277324 } ) ;
278325
279326 resizeObserver . observe ( wheelElement ) ;
@@ -306,13 +353,20 @@ export const ZoomableImage = forwardRef<ZoomableImageRef, ZoomableImageProps>(
306353
307354 const zoomChange = - e . deltaY * multiplier * factor ;
308355
356+ const baseDimensions = getBaseDimensions ( ) ;
357+ if ( ! baseDimensions ) return ;
358+
359+ const minimumScale = getMinimumScale (
360+ baseDimensions ,
361+ viewportRect . width ,
362+ viewportRect . height ,
363+ ) ;
309364 const currentScale = transformState . scale ;
310365 const desiredScale = Math . max (
311- MIN_SCALE ,
366+ minimumScale ,
312367 Math . min ( MAX_SCALE , currentScale + zoomChange ) ,
313368 ) ;
314- const baseDimensions = getBaseDimensions ( ) ;
315- if ( ! baseDimensions ) return ;
369+ setMinimumScale ( minimumScale ) ;
316370
317371 const currentDimensions = {
318372 width : baseDimensions . width * currentScale ,
@@ -354,7 +408,8 @@ export const ZoomableImage = forwardRef<ZoomableImageRef, ZoomableImageProps>(
354408 ) ;
355409
356410 const shouldRecenter =
357- newScale === MIN_SCALE || ( ! newOverflow . width && ! newOverflow . height ) ;
411+ newScale <= minimumScale + SCALE_EPSILON ||
412+ ( ! newOverflow . width && ! newOverflow . height ) ;
358413
359414 const centeredX = getCenteredAxisPosition (
360415 viewportRect . width ,
@@ -411,14 +466,15 @@ export const ZoomableImage = forwardRef<ZoomableImageRef, ZoomableImageProps>(
411466 getOverflowState ,
412467 getScaledDimensions ,
413468 getViewportElement ,
469+ setMinimumScale ,
414470 ] ) ;
415471
416472 return (
417473 < div ref = { wheelAreaRef } className = "h-full w-full" >
418474 < TransformWrapper
419475 ref = { transformRef }
420- initialScale = { MIN_SCALE }
421- minScale = { MIN_SCALE }
476+ initialScale = { minScale }
477+ minScale = { minScale }
422478 maxScale = { MAX_SCALE }
423479 centerOnInit
424480 limitToBounds = { ! isOverflowing }
@@ -461,12 +517,12 @@ export const ZoomableImage = forwardRef<ZoomableImageRef, ZoomableImageProps>(
461517 const positionY = ref . state . positionY ;
462518 const viewW = wrapper . clientWidth ;
463519 const viewH = wrapper . clientHeight ;
464- const imgW = imageRef . current . clientWidth ;
465- const imgH = imageRef . current . clientHeight ;
520+ const baseDimensions = getBaseDimensions ( ) ;
521+
522+ if ( ! baseDimensions ) return ;
466523
467- const effectiveDims = getEffectiveDimensions ( imgW , imgH ) ;
468- const scaledW = effectiveDims . width * scale ;
469- const scaledH = effectiveDims . height * scale ;
524+ const scaledW = baseDimensions . width * scale ;
525+ const scaledH = baseDimensions . height * scale ;
470526
471527 const finalX = getAxisPosition (
472528 positionX ,
@@ -512,8 +568,8 @@ export const ZoomableImage = forwardRef<ZoomableImageRef, ZoomableImageProps>(
512568 img . src = '/placeholder.svg' ;
513569 } }
514570 style = { {
515- maxWidth : '100vw ' ,
516- maxHeight : '100vh ' ,
571+ maxWidth : 'none ' ,
572+ maxHeight : 'none ' ,
517573 objectFit : 'contain' ,
518574 zIndex : 50 ,
519575 transform : `rotate(${ rotation } deg)` ,
0 commit comments