-
Notifications
You must be signed in to change notification settings - Fork 4.8k
Image Cropper: Add interactive grid lines state #77683
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
andrewserong
wants to merge
2
commits into
trunk
from
update/image-cropper-interactive-grid-lines-state
Closed
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -40,6 +40,9 @@ import './cropper.scss'; | |
| /** Threshold for comparing normalized crop rect values. */ | ||
| const CROP_RECT_EPSILON = 1e-6; | ||
|
|
||
| /** How long to wait after the last interaction before fading the grid out. */ | ||
| const GRID_FADE_DELAY_MS = 200; | ||
|
|
||
| // Largest rect of the given pixel aspect ratio that fits inside the visual | ||
| // bounds, centered in [0,1] × [0,1] normalized space. Returns a full-frame | ||
| // rect (1×1) if `aspectRatio` is unset or non-positive. | ||
|
|
@@ -81,8 +84,14 @@ export interface CropperProps { | |
| controller: UseCropperStateReturn; | ||
| /** Stencil component for the crop area. Defaults to RectangleStencil. */ | ||
| stencil?: React.ComponentType< StencilProps >; | ||
| /** Show the rule-of-thirds grid overlay. */ | ||
| showGrid?: boolean; | ||
| /** | ||
| * Controls the rule-of-thirds grid overlay. | ||
| * - `false` (default): grid is never shown. | ||
| * - `true`: grid is always shown. | ||
| * - `'interactive'`: grid fades in when any cropper value changes and fades | ||
| * out automatically after the user stops interacting. | ||
| */ | ||
| showGrid?: boolean | 'interactive'; | ||
| /** Show the dimming overlay outside the crop area. */ | ||
| showDimming?: boolean; | ||
| /** Minimum zoom level. */ | ||
|
|
@@ -132,7 +141,7 @@ export interface CropperProps { | |
| * @param root0.src Image source URL. | ||
| * @param root0.controller The full state/setter object from `useCropperState`. | ||
| * @param root0.stencil Custom stencil component. | ||
| * @param root0.showGrid Show rule-of-thirds grid overlay. | ||
| * @param root0.showGrid Grid overlay mode: false | true | 'interactive'. | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Also like this flexibility. |
||
| * @param root0.showDimming Show dimming overlay outside crop. | ||
| * @param root0.minZoom Minimum zoom level. | ||
| * @param root0.maxZoom Maximum zoom level. | ||
|
|
@@ -273,6 +282,48 @@ function CropperInner( | |
| return getCropBounds( state, elementSize, visualSize, canvasSize ); | ||
| }, [ state, elementSize, visualSize, canvasSize ] ); | ||
|
|
||
| // Interactive grid: visible while the user is interacting, fades out | ||
| // after GRID_FADE_DELAY_MS of inactivity. | ||
| const [ gridVisible, setGridVisible ] = useState( false ); | ||
| const gridFadeTimerRef = useRef< ReturnType< typeof setTimeout > >(); | ||
| const gridInteractionReadyRef = useRef( false ); | ||
|
|
||
| // Defer readiness until after image load and its dependent effects settle, | ||
| // so automatic initialisation changes don't trigger the interactive grid. | ||
| useEffect( () => { | ||
| gridInteractionReadyRef.current = false; | ||
| if ( ! state.image ) { | ||
| return; | ||
| } | ||
| const id = setTimeout( () => { | ||
| gridInteractionReadyRef.current = true; | ||
| }, 0 ); | ||
| return () => clearTimeout( id ); | ||
| }, [ state.image ] ); | ||
|
|
||
| // Show the grid at the start of a canvas gesture (pan, zoom, rotate, | ||
| // stencil resize) and schedule the fade-out when the gesture ends. | ||
| // These are defined before useInteraction() so they can be passed as | ||
| // its options — they also forward to the external onGestureStart/End props. | ||
| const handleGestureStart = useCallback( () => { | ||
| if ( showGrid === 'interactive' && gridInteractionReadyRef.current ) { | ||
| clearTimeout( gridFadeTimerRef.current ); | ||
| setGridVisible( true ); | ||
| } | ||
| onGestureStart?.(); | ||
| }, [ showGrid, onGestureStart ] ); | ||
|
|
||
| const handleGestureEnd = useCallback( () => { | ||
| if ( showGrid === 'interactive' ) { | ||
| clearTimeout( gridFadeTimerRef.current ); | ||
| gridFadeTimerRef.current = setTimeout( | ||
| () => setGridVisible( false ), | ||
| GRID_FADE_DELAY_MS | ||
| ); | ||
| } | ||
| onGestureEnd?.(); | ||
| }, [ showGrid, onGestureEnd ] ); | ||
|
|
||
| // Use the interaction hook for mouse, touch, and keyboard events. | ||
| const { handlers, onWheelNative, isDragging, isZooming } = useInteraction( | ||
| state, | ||
|
|
@@ -282,8 +333,8 @@ function CropperInner( | |
| { | ||
| minZoom, | ||
| maxZoom, | ||
| onGestureStart, | ||
| onGestureEnd, | ||
| onGestureStart: handleGestureStart, | ||
| onGestureEnd: handleGestureEnd, | ||
| } | ||
| ); | ||
|
|
||
|
|
@@ -351,6 +402,22 @@ function CropperInner( | |
| }; | ||
| }, [] ); | ||
|
|
||
| // Register the grid handlers with the controller so that external controls | ||
| // (e.g. the fine-rotation and zoom sliders) can trigger the grid via | ||
| // controller.notifyInteractionStart/End without needing to know about | ||
| // the Cropper's internal grid state. | ||
| useEffect( () => { | ||
| controller.registerInteractionListener( { | ||
| onStart: handleGestureStart, | ||
| onEnd: handleGestureEnd, | ||
| } ); | ||
| return () => controller.registerInteractionListener( null ); | ||
| }, [ controller, handleGestureStart, handleGestureEnd ] ); | ||
|
|
||
| useEffect( () => { | ||
| return () => clearTimeout( gridFadeTimerRef.current ); | ||
| }, [] ); | ||
|
Comment on lines
+417
to
+419
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this required? It's cleaned up in the |
||
|
|
||
| /** | ||
| * Handle Escape on a resize handle — return focus to the canvas so | ||
| * arrow keys pan the image rather than resize. | ||
|
|
@@ -365,12 +432,12 @@ function CropperInner( | |
| const handleResizeEnd = useCallback( () => { | ||
| setSettling( true ); | ||
| settleCrop(); | ||
| onGestureEnd?.(); | ||
| handleGestureEnd(); | ||
| clearTimeout( settleTimerRef.current ); | ||
| settleTimerRef.current = setTimeout( () => { | ||
| setSettling( false ); | ||
| }, 200 ); | ||
| }, [ settleCrop, onGestureEnd ] ); | ||
| }, [ settleCrop, handleGestureEnd ] ); | ||
|
|
||
| const imageTransition = | ||
| settling || isZooming ? 'transform 150ms linear' : undefined; | ||
|
|
@@ -465,7 +532,7 @@ function CropperInner( | |
| containerSize={ canvasSize } | ||
| imageSize={ visualSize } | ||
| onCropChange={ handleCropChange } | ||
| onResizeStart={ onGestureStart } | ||
| onResizeStart={ handleGestureStart } | ||
| onResizeEnd={ handleResizeEnd } | ||
| onEscape={ handleEscape } | ||
| aspectRatio={ aspectRatio } | ||
|
|
@@ -475,11 +542,14 @@ function CropperInner( | |
| /> | ||
|
|
||
| { /* Rule-of-thirds grid */ } | ||
| { showGrid && ( | ||
| { ( showGrid === true || showGrid === 'interactive' ) && ( | ||
| <GridOverlay | ||
| cropRect={ state.cropRect } | ||
| containerSize={ canvasSize } | ||
| imageSize={ visualSize } | ||
| opacity={ | ||
| showGrid === 'interactive' && ! gridVisible ? 0 : 1 | ||
| } | ||
| /> | ||
| ) } | ||
|
|
||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like