@@ -110,7 +110,6 @@ export const machine = createMachine<ImageCropperSchema>({
110110 } ) ) ,
111111 zoom : bindable < number > ( ( ) => ( {
112112 defaultValue : prop ( "defaultZoom" ) ,
113- value : prop ( "zoom" ) ,
114113 onChange ( zoom ) {
115114 prop ( "onZoomChange" ) ?.( { zoom } )
116115 } ,
@@ -184,6 +183,18 @@ export const machine = createMachine<ImageCropperSchema>({
184183 isImageReady : ( { context } ) => isVisibleRect ( context . get ( "naturalSize" ) ) ,
185184 } ,
186185
186+ watch ( { track, context, prop, send } ) {
187+ track ( [ ( ) => prop ( "zoom" ) ] , ( ) => {
188+ const propZoom = prop ( "zoom" )
189+ if ( propZoom === undefined ) return
190+
191+ const currentZoom = context . get ( "zoom" )
192+ if ( propZoom === currentZoom ) return
193+
194+ send ( { type : "SET_ZOOM" , zoom : propZoom , src : "prop" } )
195+ } )
196+ } ,
197+
187198 states : {
188199 idle : {
189200 entry : [ "checkImageStatus" ] ,
@@ -515,110 +526,101 @@ export const machine = createMachine<ImageCropperSchema>({
515526 updateZoom ( { context, event, prop } ) {
516527 let { delta, point, zoom : targetZoom , scale, panDelta } = event
517528
529+ const crop = context . get ( "crop" )
530+ const currentZoom = context . get ( "zoom" )
531+ const currentOffset = context . get ( "offset" )
532+ const rotation = context . get ( "rotation" )
533+ const viewportRect = context . get ( "viewportRect" )
534+ const naturalSize = context . get ( "naturalSize" )
535+ const fixedCropArea = prop ( "fixedCropArea" )
536+
518537 // If no point is specified, zoom based on the center of the crop area
519538 if ( ! point ) {
520- const crop = context . get ( "crop" )
521539 point = getCenterPoint ( crop )
522540 }
523541
524542 const step = Math . abs ( prop ( "zoomStep" ) )
525543 const sensitivity = Math . max ( 0 , prop ( "zoomSensitivity" ) )
526544 const [ minZoom , maxZoom ] = [ prop ( "minZoom" ) , prop ( "maxZoom" ) ]
527- const currentZoom = context . get ( "zoom" )
528- const viewportRect = context . get ( "viewportRect" )
529545
530- let nextZoom
531-
532- if ( typeof targetZoom === "number" ) {
533- nextZoom = clampValue ( targetZoom , minZoom , maxZoom )
534- } else if ( event . trigger === "touch" && typeof scale === "number" ) {
535- const minScale = 0.5
536- const maxScale = 2
537- const clampedScale = clampValue ( scale , minScale , maxScale )
538- const smoothing = sensitivity > 0 ? Math . pow ( clampedScale , sensitivity ) : clampedScale
539- nextZoom = clampValue ( currentZoom * smoothing , minZoom , maxZoom )
540- } else if ( typeof delta === "number" ) {
541- const direction = Math . sign ( delta ) < 0 ? 1 : - 1
542- nextZoom = clampValue ( currentZoom + step * direction , minZoom , maxZoom )
543- } else {
544- return
545- }
546+ const calculateNextZoom = ( ) : number | null => {
547+ if ( typeof targetZoom === "number" ) {
548+ return clampValue ( targetZoom , minZoom , maxZoom )
549+ }
546550
547- // Only pan if there's a pan delta from pinch movement
548- if ( nextZoom === currentZoom && panDelta ) {
549- const currentOffset = context . get ( "offset" )
550- const rotation = context . get ( "rotation" )
551+ if ( event . trigger === "touch" && typeof scale === "number" ) {
552+ const minScale = 0.5
553+ const maxScale = 2
554+ const clampedScale = clampValue ( scale , minScale , maxScale )
555+ const smoothing = sensitivity > 0 ? Math . pow ( clampedScale , sensitivity ) : clampedScale
556+ return clampValue ( currentZoom * smoothing , minZoom , maxZoom )
557+ }
558+
559+ if ( typeof delta === "number" ) {
560+ const direction = Math . sign ( delta ) < 0 ? 1 : - 1
561+ return clampValue ( currentZoom + step * direction , minZoom , maxZoom )
562+ }
563+
564+ return null
565+ }
551566
552- const nextOffset = clampOffset ( {
553- zoom : currentZoom ,
567+ const applyClampedOffset = ( zoom : number , offset : Point ) : Point => {
568+ return clampOffset ( {
569+ zoom,
554570 rotation,
555571 viewportSize : viewportRect ,
556- offset : addPoints ( currentOffset , panDelta ) ,
557- fixedCropArea : prop ( "fixedCropArea" ) ,
558- crop : context . get ( "crop" ) ,
559- naturalSize : context . get ( "naturalSize" ) ,
572+ offset,
573+ fixedCropArea,
574+ crop,
575+ naturalSize,
560576 } )
577+ }
561578
579+ const nextZoom = calculateNextZoom ( )
580+ if ( nextZoom === null ) return
581+
582+ // Handle pan-only update
583+ if ( nextZoom === currentZoom && panDelta ) {
584+ const nextOffset = applyClampedOffset ( currentZoom , addPoints ( currentOffset , panDelta ) )
562585 context . set ( "offset" , nextOffset )
563586 return
564587 }
565588
566589 if ( nextZoom === currentZoom ) return
567590
568- const { width : vpW , height : vpH } = viewportRect
591+ const { width : viewportWidth , height : viewportHeight } = viewportRect
569592 const { x : centerX , y : centerY } = getViewportCenter ( viewportRect )
570593
571- const currentOffset = context . get ( "offset" )
572-
573- const ratio = nextZoom / currentZoom
574- let nextOffset = {
575- x : ( 1 - ratio ) * ( point . x - centerX ) + ratio * currentOffset . x ,
576- y : ( 1 - ratio ) * ( point . y - centerY ) + ratio * currentOffset . y ,
594+ const zoomRatio = nextZoom / currentZoom
595+ let nextOffset : Point = {
596+ x : ( 1 - zoomRatio ) * ( point . x - centerX ) + zoomRatio * currentOffset . x ,
597+ y : ( 1 - zoomRatio ) * ( point . y - centerY ) + zoomRatio * currentOffset . y ,
577598 }
578599
579600 // Apply pan delta from pinch movement if provided
580601 if ( panDelta ) {
581- nextOffset = addPoints ( nextOffset , panDelta )
582-
583- const rotation = context . get ( "rotation" )
584- nextOffset = clampOffset ( {
585- zoom : nextZoom ,
586- rotation,
587- viewportSize : viewportRect ,
588- offset : nextOffset ,
589- fixedCropArea : prop ( "fixedCropArea" ) ,
590- crop : context . get ( "crop" ) ,
591- naturalSize : context . get ( "naturalSize" ) ,
592- } )
602+ nextOffset = applyClampedOffset ( nextZoom , addPoints ( nextOffset , panDelta ) )
593603 } else if ( nextZoom < currentZoom ) {
594- if ( prop ( "fixedCropArea" ) ) {
595- const rotation = context . get ( "rotation" )
596- nextOffset = clampOffset ( {
597- zoom : nextZoom ,
598- rotation,
599- viewportSize : viewportRect ,
600- offset : nextOffset ,
601- fixedCropArea : true ,
602- crop : context . get ( "crop" ) ,
603- naturalSize : context . get ( "naturalSize" ) ,
604- } )
604+ // Handle zoom out - clamp offset to keep image within bounds
605+ if ( fixedCropArea ) {
606+ nextOffset = applyClampedOffset ( nextZoom , nextOffset )
605607 } else {
606- const imgSize = context . get ( "naturalSize" )
607- const { width : scaledW , height : scaledH } = scaleSize ( imgSize , nextZoom )
608+ // Manual clamping for non-fixed crop area
609+ const { width : scaledImageWidth , height : scaledImageHeight } = scaleSize ( naturalSize , nextZoom )
608610
609- if ( scaledW <= vpW ) {
611+ if ( scaledImageWidth <= viewportWidth ) {
610612 nextOffset . x = 0
611613 } else {
612- const minX = vpW - centerX - scaledW / 2
613- const maxX = scaledW / 2 - centerX
614+ const minX = viewportWidth - centerX - scaledImageWidth / 2
615+ const maxX = scaledImageWidth / 2 - centerX
614616 nextOffset . x = Math . max ( minX , Math . min ( maxX , nextOffset . x ) )
615617 }
616618
617- if ( scaledH <= vpH ) {
619+ if ( scaledImageHeight <= viewportHeight ) {
618620 nextOffset . y = 0
619621 } else {
620- const minY = vpH - centerY - scaledH / 2
621- const maxY = scaledH / 2 - centerY
622+ const minY = viewportHeight - centerY - scaledImageHeight / 2
623+ const maxY = scaledImageHeight / 2 - centerY
622624 nextOffset . y = Math . max ( minY , Math . min ( maxY , nextOffset . y ) )
623625 }
624626 }
0 commit comments