@@ -30,17 +30,17 @@ export const IPFSUploader = ({
3030 const DEST_W = 1200 ;
3131 const DEST_H = 800 ;
3232
33- const minCoverZoom = useMemo ( ( ) => {
34- if ( ! imgDims ) return 1 ;
35- const cover = Math . max ( DEST_W / imgDims . w , DEST_H / imgDims . h ) ;
36- return cover ;
37- } , [ imgDims ] ) ;
33+ // Absolute zoom scales relative to the image's natural size
34+ // containZoom: entire image fits within the crop (furthest edges touch)
35+ // coverZoom: crop fully filled by the image (closest edges touch)
36+ const [ coverZoom , setCoverZoom ] = useState < number > ( 1 ) ;
3837
3938 const zoomBounds = useMemo ( ( ) => {
40- const min = minCoverZoom * 0.5 ; // allow zooming out to show black bars
41- const max = minCoverZoom * 4 ;
39+ const min = 0.75 ; // normalized contain
40+ // Give headroom above cover to overcome rounding/clamping and allow precise edge contact
41+ const max = Math . max ( coverZoom * 3 , min + 0.01 ) ;
4242 return { min, max } ;
43- } , [ minCoverZoom ] ) ;
43+ } , [ coverZoom ] ) ;
4444
4545 // container width not needed with react-easy-crop
4646
@@ -152,9 +152,7 @@ export const IPFSUploader = ({
152152 const w = img . naturalWidth || img . width ;
153153 const h = img . naturalHeight || img . height ;
154154 setImgDims ( { w, h } ) ;
155- // default zoom to cover
156- const cover = Math . max ( DEST_W / w , DEST_H / h ) ;
157- setZoom ( cover ) ;
155+ // Default zoom will be set on media load using the actual container size
158156 setCrop ( { x : 0 , y : 0 } ) ;
159157 setCropOpen ( true ) ;
160158 } catch {
@@ -202,14 +200,36 @@ export const IPFSUploader = ({
202200 aspect = { 3 / 2 }
203201 crop = { crop }
204202 zoom = { zoom }
205- minZoom = { minCoverZoom }
206- maxZoom = { minCoverZoom * 5 }
203+ minZoom = { zoomBounds . min }
204+ maxZoom = { zoomBounds . max }
207205 zoomWithScroll
208206 restrictPosition = { false }
209207 showGrid = { false }
210208 onCropChange = { setCrop }
211209 onZoomChange = { setZoom }
212210 onCropComplete = { ( _ : Area , areaPixels : Area ) => setCroppedAreaPixels ( areaPixels ) }
211+ onMediaLoaded = { ( { naturalWidth, naturalHeight } ) => {
212+ const rect = cropRef . current ?. getBoundingClientRect ( ) ;
213+ if ( rect && naturalWidth && naturalHeight ) {
214+ // Compute absolute scales relative to natural size. Account for 3:2 crop area.
215+ const containerW = rect . width ;
216+ const containerH = rect . height ;
217+ const containAbs = Math . min ( containerW / naturalWidth , containerH / naturalHeight ) ;
218+ const coverAbs = Math . max ( containerW / naturalWidth , containerH / naturalHeight ) ;
219+ const coverFactor = coverAbs / containAbs ; // normalized units
220+ setCoverZoom ( coverFactor ) ;
221+ // Default view = cover and centered
222+ setZoom ( coverFactor ) ;
223+ // Center programmatically so the nearest edges are symmetric
224+ // crop in react-easy-crop is percentage based; {x:0,y:0} is centered
225+ // Ensure centered positioning at initial load
226+ setCrop ( { x : 0 , y : 0 } ) ;
227+ } else {
228+ setCoverZoom ( 1 ) ;
229+ setZoom ( 1 ) ;
230+ setCrop ( { x : 0 , y : 0 } ) ;
231+ }
232+ } }
213233 style = { { containerStyle : { width : "100%" , height : "100%" } } }
214234 />
215235 </ div >
@@ -232,6 +252,7 @@ export const IPFSUploader = ({
232252 setImgDims ( null ) ;
233253 setCrop ( { x : 0 , y : 0 } ) ;
234254 setZoom ( 1 ) ;
255+ setCoverZoom ( 1 ) ;
235256 onSelected ?.( null ) ;
236257 setCropOpen ( false ) ;
237258 } }
0 commit comments