Skip to content
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

[Experiment] Add freeform image cropper component #63335

Draft
wants to merge 34 commits into
base: trunk
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
1247190
Add freeform image cropper component
kevin940726 Jul 10, 2024
9b6907a
Try rotating 90deg
kevin940726 Jul 16, 2024
3c917e1
Fix rotation
kevin940726 Jul 17, 2024
7066571
Add flip
kevin940726 Jul 17, 2024
a32efe4
Add comments for math
kevin940726 Jul 17, 2024
85e8b52
Set basic max-width and max-height for resizing
kevin940726 Jul 18, 2024
56dd4d0
Refactor story
kevin940726 Jul 18, 2024
c30fb87
Refactor state
kevin940726 Jul 18, 2024
ff9ecb6
Add some comments
kevin940726 Jul 18, 2024
f076ada
Better move algo
kevin940726 Jul 27, 2024
51ab644
Smooth animation
kevin940726 Jul 29, 2024
dcbfc49
Fix aspect ratio and generated image
kevin940726 Jul 29, 2024
37cca6d
Fix flip (scale)
kevin940726 Jul 29, 2024
9f3518b
Fix pinching animation
kevin940726 Jul 29, 2024
3ad8c12
Rewrite algo
kevin940726 Jul 29, 2024
75a0dca
motionize resizable-box
kevin940726 Jul 29, 2024
b699f8d
Fix resizing and scaling from cursor position
kevin940726 Jul 31, 2024
d0d9963
Fix rotation from the cropper center
kevin940726 Jul 31, 2024
797ec46
Fix resizing container
kevin940726 Jul 31, 2024
2fd0c69
Rename rotate action
kevin940726 Aug 6, 2024
b85b4e5
Fix animation in iframe
kevin940726 Aug 7, 2024
afba46f
Integrate into the image block behind an experimental flag
kevin940726 Aug 8, 2024
c9be286
Add aspect ratio support and remove xy state for cropper
kevin940726 Aug 8, 2024
33a7511
Convert to doc comments for vscode tooltips
ajlende Aug 1, 2024
1bbec8b
Add doc comments for actions
ajlende Aug 1, 2024
7ae9d82
Add back missing lock and unlock aspect ration actions
ajlende Aug 12, 2024
48ec1de
Add return type for better type checking
ajlende Aug 12, 2024
d9c6cc8
Remove MOVE_WINDOW action
ajlende Aug 12, 2024
6fc6e25
Refactor cropper state
ajlende Aug 12, 2024
9c3753b
Refactor absScale
ajlende Aug 12, 2024
53085ef
Simplify flip
ajlende Aug 12, 2024
f0baf8e
Add aspect ratio dropdown
kevin940726 Aug 9, 2024
7a1afba
Default to inline the image
kevin940726 Aug 16, 2024
15b4f30
Use ID as a namespace to increase specificity
kevin940726 Aug 16, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions lib/experimental/editor-settings.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ function gutenberg_enable_experiments() {
if ( $gutenberg_experiments && array_key_exists( 'gutenberg-block-bindings-ui', $gutenberg_experiments ) ) {
wp_add_inline_script( 'wp-block-editor', 'window.__experimentalBlockBindingsUI = true', 'before' );
}
if ( $gutenberg_experiments && array_key_exists( 'gutenberg-image-cropper', $gutenberg_experiments ) ) {
wp_add_inline_script( 'wp-block-editor', 'window.__experimentalImageCropper = true', 'before' );
}
}

add_action( 'admin_init', 'gutenberg_enable_experiments' );
Expand Down
12 changes: 12 additions & 0 deletions lib/experiments-page.php
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,18 @@ function gutenberg_initialize_experiments_settings() {
)
);

add_settings_field(
'gutenberg-image-cropper',
__( 'Redesigned image cropper', 'gutenberg' ),
'gutenberg_display_experiment_field',
'gutenberg-experiments',
'gutenberg_experiments_section',
array(
'label' => __( 'Enable a redesigned version of the image cropper in the block editor.', 'gutenberg' ),
'id' => 'gutenberg-image-cropper',
)
);

register_setting(
'gutenberg-experiments',
'gutenberg-experiments'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
/**
* WordPress dependencies
*/
import { check, aspectRatio as aspectRatioIcon } from '@wordpress/icons';
import { DropdownMenu, MenuGroup, MenuItem } from '@wordpress/components';
import { __ } from '@wordpress/i18n';

/**
* Internal dependencies
*/
import { useSettings } from '../../use-settings';
import { useImageCropper } from './context';

function AspectRatioGroup( { aspectRatios, label, onClick, value } ) {
return (
<MenuGroup label={ label }>
{ aspectRatios.map( ( { name, slug, ratio } ) => (
<MenuItem
key={ slug }
onClick={ () => {
onClick( ratio );
} }
role="menuitemradio"
isSelected={ ratio.toFixed( 4 ) === value.toFixed( 4 ) }
icon={ ratio === value ? check : undefined }
>
{ name }
</MenuItem>
) ) }
</MenuGroup>
);
}

export function ratioToNumber( str ) {
// TODO: support two-value aspect ratio?
// https://css-tricks.com/almanac/properties/a/aspect-ratio/#aa-it-can-take-two-values
const [ a, b, ...rest ] = str.split( '/' ).map( Number );
if (
a <= 0 ||
b <= 0 ||
Number.isNaN( a ) ||
Number.isNaN( b ) ||
rest.length
) {
return NaN;
}
return b ? a / b : a;
}

function presetRatioAsNumber( { ratio, ...rest } ) {
return {
ratio: ratioToNumber( ratio ),
...rest,
};
}

export default function AspectRatioDropdown( { toggleProps } ) {
const {
state: { image, cropper, isAspectRatioLocked },
dispatch,
} = useImageCropper();
const defaultAspect = image.width / image.height;
const aspectRatio = cropper.width / cropper.height;

const [ defaultRatios, themeRatios, showDefaultRatios ] = useSettings(
'dimensions.aspectRatios.default',
'dimensions.aspectRatios.theme',
'dimensions.defaultAspectRatios'
);

return (
<DropdownMenu
icon={ aspectRatioIcon }
label={ __( 'Aspect Ratio' ) }
popoverProps={ { placement: 'bottom-start' } }
toggleProps={ toggleProps }
>
{ ( { onClose } ) => (
<>
<AspectRatioGroup
onClick={ ( newAspect ) => {
if ( newAspect === 0 ) {
dispatch( { type: 'UNLOCK_ASPECT_RATIO' } );
} else {
dispatch( {
type: 'LOCK_ASPECT_RATIO',
aspectRatio: newAspect,
} );
}
onClose();
} }
value={ isAspectRatioLocked ? aspectRatio : 0 }
aspectRatios={ [
// All ratios should be mirrored in AspectRatioTool in @wordpress/block-editor.
{
slug: 'free',
name: __( 'Free' ),
ratio: 0,
},
{
slug: 'original',
name: __( 'Original' ),
ratio: defaultAspect,
},
...( showDefaultRatios
? defaultRatios
.map( presetRatioAsNumber )
.filter( ( { ratio } ) => ratio === 1 )
: [] ),
] }
/>
{ themeRatios?.length > 0 && (
<AspectRatioGroup
label={ __( 'Theme' ) }
onClick={ ( newAspect ) => {
dispatch( {
type: 'LOCK_ASPECT_RATIO',
aspectRatio: newAspect,
} );
onClose();
} }
value={ aspectRatio }
aspectRatios={ themeRatios }
/>
) }
{ showDefaultRatios && (
<AspectRatioGroup
label={ __( 'Landscape' ) }
onClick={ ( newAspect ) => {
dispatch( {
type: 'LOCK_ASPECT_RATIO',
aspectRatio: newAspect,
} );
onClose();
} }
value={ aspectRatio }
aspectRatios={ defaultRatios
.map( presetRatioAsNumber )
.filter( ( { ratio } ) => ratio > 1 ) }
/>
) }
{ showDefaultRatios && (
<AspectRatioGroup
label={ __( 'Portrait' ) }
onClick={ ( newAspect ) => {
dispatch( {
type: 'LOCK_ASPECT_RATIO',
aspectRatio: newAspect,
} );
onClose();
} }
value={ aspectRatio }
aspectRatios={ defaultRatios
.map( presetRatioAsNumber )
.filter( ( { ratio } ) => ratio < 1 ) }
/>
) }
</>
) }
</DropdownMenu>
);
}
14 changes: 14 additions & 0 deletions packages/block-editor/src/components/image-editor/v2/context.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/**
* WordPress dependencies
*/
import { privateApis as componentsPrivateApis } from '@wordpress/components';
import { useContext } from '@wordpress/element';

/**
* Internal dependencies
*/
import { unlock } from '../../../lock-unlock';

const { ImageCropperContext } = unlock( componentsPrivateApis );

export const useImageCropper = () => useContext( ImageCropperContext );
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/**
* WordPress dependencies
*/
import { ToolbarButton } from '@wordpress/components';
import { __ } from '@wordpress/i18n';

/**
* Internal dependencies
*/
import { useImageCropper } from './context';

export default function FormControls( { onCrop, onCancel } ) {
const { state, getImageBlob, dispatch } = useImageCropper();

async function apply() {
const blob = await getImageBlob( state );
onCrop?.( blob, state );
}

function cancel() {
dispatch( { type: 'RESET' } );
onCancel?.();
}

return (
<>
<ToolbarButton onClick={ apply }>{ __( 'Apply' ) }</ToolbarButton>
<ToolbarButton onClick={ cancel }>{ __( 'Cancel' ) }</ToolbarButton>
</>
);
}
47 changes: 47 additions & 0 deletions packages/block-editor/src/components/image-editor/v2/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/**
* WordPress dependencies
*/
import {
ToolbarGroup,
ToolbarItem,
privateApis as componentsPrivateApis,
} from '@wordpress/components';

/**
* Internal dependencies
*/
import AspectRatioDropdown from './aspect-ratio-dropdown';
import BlockControls from '../../block-controls';
import RotationButton from './rotation-button';
import FormControls from './form-controls';
import { unlock } from '../../../lock-unlock';

const { ImageCropper } = unlock( componentsPrivateApis );

export default function ImageEditor( {
src,
width,
height,
onCrop,
onCancel,
} ) {
return (
<ImageCropper.Provider src={ src } width={ width } height={ height }>
<ImageCropper />

<BlockControls>
<ToolbarGroup>
<ToolbarItem>
{ ( toggleProps ) => (
<AspectRatioDropdown toggleProps={ toggleProps } />
) }
</ToolbarItem>
<RotationButton />
</ToolbarGroup>
<ToolbarGroup>
<FormControls onCrop={ onCrop } onCancel={ onCancel } />
</ToolbarGroup>
</BlockControls>
</ImageCropper.Provider>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/**
* WordPress dependencies
*/

import { ToolbarButton } from '@wordpress/components';
import { __ } from '@wordpress/i18n';
import { rotateRight as rotateRightIcon } from '@wordpress/icons';

/**
* Internal dependencies
*/
import { useImageCropper } from './context';

export default function RotationButton() {
const { dispatch } = useImageCropper();

return (
<ToolbarButton
icon={ rotateRightIcon }
label={ __( 'Rotate' ) }
onClick={ () => {
dispatch( { type: 'ROTATE_90_DEG' } );
} }
/>
);
}
2 changes: 2 additions & 0 deletions packages/block-editor/src/private-apis.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ import { PrivatePublishDateTimePicker } from './components/publish-date-time-pic
import useSpacingSizes from './components/spacing-sizes-control/hooks/use-spacing-sizes';
import useBlockDisplayTitle from './components/block-title/use-block-display-title';
import TabbedSidebar from './components/tabbed-sidebar';
import ImageEditor from './components/image-editor/v2';

/**
* Private @wordpress/block-editor APIs.
Expand Down Expand Up @@ -92,4 +93,5 @@ lock( privateApis, {
useBlockDisplayTitle,
__unstableBlockStyleVariationOverridesWithConfig,
setBackgroundStyleDefaults,
ImageEditor,
} );
58 changes: 42 additions & 16 deletions packages/block-library/src/image/image.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,11 @@ import { useToolsPanelDropdownMenuProps } from '../utils/hooks';
import { MIN_SIZE, ALLOWED_MEDIA_TYPES } from './constants';
import { evalAspectRatio } from './utils';

const { DimensionsTool, ResolutionTool } = unlock( blockEditorPrivateApis );
const {
DimensionsTool,
ResolutionTool,
ImageEditor: ImageEditorV2,
} = unlock( blockEditorPrivateApis );

const scaleOptions = [
{
Expand Down Expand Up @@ -870,21 +874,43 @@ export default function Image( {
if ( canEditImage && isEditingImage ) {
img = (
<ImageWrapper href={ href }>
<ImageEditor
id={ id }
url={ url }
width={ numericWidth }
height={ numericHeight }
naturalHeight={ naturalHeight }
naturalWidth={ naturalWidth }
onSaveImage={ ( imageAttributes ) =>
setAttributes( imageAttributes )
}
onFinishEditing={ () => {
setIsEditingImage( false );
} }
borderProps={ isRounded ? undefined : borderProps }
/>
{ window.__experimentalImageCropper ? (
<ImageEditorV2
src={ url }
width={ numericWidth }
height={ numericHeight }
onCrop={ ( imageBlob ) => {
getSettings().mediaUpload( {
filesList: [ imageBlob ],
onFileChange: ( [ media ] ) => {
onSelectImage( media );
},
allowedTypes: ALLOWED_MEDIA_TYPES,
onError: onUploadError,
} );
setIsEditingImage( false );
} }
onCancel={ () => {
setIsEditingImage( false );
} }
/>
) : (
<ImageEditor
id={ id }
url={ url }
width={ numericWidth }
height={ numericHeight }
naturalHeight={ naturalHeight }
naturalWidth={ naturalWidth }
onSaveImage={ ( imageAttributes ) =>
setAttributes( imageAttributes )
}
onFinishEditing={ () => {
setIsEditingImage( false );
} }
borderProps={ isRounded ? undefined : borderProps }
/>
) }
</ImageWrapper>
);
} else if ( ! isResizable || parentLayoutType === 'grid' ) {
Expand Down
Loading
Loading