WordPress / gutenberg

The Block Editor project for WordPress and beyond. Plugin is available from the official repository.
https://wordpress.org/gutenberg/
Other
10.23k stars 4.08k forks source link

Focal point + crop for featured images #20321

Open strarsis opened 4 years ago

strarsis commented 4 years ago

Is your feature request related to a problem? Please describe. It would be nice if the Featured Image field in Gutenberg editor also supports focal point + crop like in e.g. the cover block.

Describe the solution you'd like Allow/offer focal point + crop controls for featured image fields.

Describe alternatives you've considered Using a plugin for this, but this will introduce completely different, "classic" UI.

Edit: Similar plugin that does it for all media though: https://wordpress.org/plugins/wp-smartcrop/

cr0ybot commented 4 years ago

I had need of this feature and did a quick and dirty plugin that filters the PostFeaturedImage component. Posting here in case anyone else needs something similar.

First, I registered the featured_image_focal_point post meta:

/**
 * Register post meta for featured image focal point
 */
function featured_image_focal_point_post_meta() {
    register_post_meta( '', 'featured_image_focal_point', array(
        'type' => 'object',
        'description' => 'Focal point of the featured image',
        'single' => true,
        'show_in_rest' => array(
            'schema' => array(
                'type'       => 'object',
                'properties' => array(
                    'x' => array(
                        'type' => 'number',
                    ),
                    'y'  => array(
                        'type' => 'number',
                    ),
                ),
            ),
        ),
    ) );
}
add_action( 'init', 'featured_image_focal_point_post_meta' );

Then I used wp.hooks.addFilter to filter the PostFeaturedImage component to append the FocalPointPicker below. Ideally, I'd love for the focal point picker to replace the standard featured image, but, like I said, quick and dirty.

/**
 * EDITOR: Featured Image with Focal Point
 */

const { FocalPointPicker } = wp.components;
const { compose } = wp.compose;
const { withDispatch, withSelect } = wp.data;
const { Fragment } = wp.element;
const { addFilter } = wp.hooks;
const { __ } = wp.i18n;

function wrapPostFeaturedImage( PostFeaturedImage ) {
    return compose(
        applyWithSelect,
        applyWithDispatch,
    )( ( props ) => {
        const {
            media,
            featuredImageFocalPoint,
            setFeaturedImageFocalPoint,
        } = props;

        if ( media && media.source_url ) {
            const url = media.source_url;
            const { width, height } = media;

            return (
                <Fragment>
                    <PostFeaturedImage { ...props } />
                    <FocalPointPicker
                        label={ __( 'Focal point picker' ) }
                        url={ url }
                        dimensions={ { width, height } }
                        value={ featuredImageFocalPoint }
                        onChange={ ( newFocalPoint ) =>
                            setFeaturedImageFocalPoint( newFocalPoint )
                        }
                    />
                </Fragment>
            );
        }

        return (
            <PostFeaturedImage { ...props } />
        );
    } );
}

const applyWithSelect = withSelect( ( select ) => {
    const { getEditedPostAttribute } = select( 'core/editor' );
    const featuredImageFocalPoint = getEditedPostAttribute( 'meta' )[ 'featured_image_focal_point' ];

    return {
        featuredImageFocalPoint,
    };
} );

const applyWithDispatch = withDispatch( ( dispatch ) => {
    const { editPost } = dispatch( 'core/editor' );

    return {
        setFeaturedImageFocalPoint( focalPoint ) {
            editPost( { meta: { featured_image_focal_point: focalPoint } } );
        },
    };
} );

addFilter(
    'editor.PostFeaturedImage',
    'centralex/wrap-post-featured-image',
    wrapPostFeaturedImage
);

EDIT: Using compose with addFilter got a bit hairy, let me know if I should have done that bit differently.

If this were to be a built-in feature, the theme would need to know how to use the focal point meta values, and it would perhaps be misleading to show the focal point picker by default if the theme doesn't reflect the setting. Perhaps this feature could be behind an add_theme_support flag?

For me personally, in the future I'd rather just use the existing Cover Image block, but it would need to be aware of the post's featured image, and Gutenberg's template features aren't quite to the point where I feel I can utilize them yet. Also, the Cover Image block uses background-image, but I prefer to use an actual img tag to take advantage of srcset. I output the featured image as an absolutely-positioned img tag with object-fit: cover and then use object-position to set the focal point:

$image = get_post_thumbnail_id();
$focal_point = get_post_meta( get_the_ID(), 'featured_image_focal_point', true );

$focal_point_style = ( $focal_point && $focal_point['x'] && $focal_point['y'] ) ? sprintf('object-position: %d%% %d%%', $focal_point['x']*100, $focal_point['y']*100) : '';

echo wp_get_attachment_image( $image, 'banner-xl', false, array( 'style' => $focal_point_style ) );
cr0ybot commented 4 years ago

Fun fact for anyone attempting to use the code I posted above with a custom post type: your custom post type MUST have 'custom-fields' in its supports array when the post type is registered.

See https://github.com/WordPress/gutenberg/issues/17018#issuecomment-521483868

Not sure how to modify the code to not run on unsupported post types...

mundschenk-at commented 4 years ago

Not sure how to modify the code to not run on unsupported post types...

I'd like to know that as well.

ryanapsmith commented 3 years ago

did this a little (but ultimately not so) differently with HigherOrderComponents and it seems to work just as well.

const { __ } = wp.i18n;
const { addFilter } = wp.hooks;
const { Fragment } = wp.element;
const { createHigherOrderComponent } = wp.compose;
const { FocalPointPicker } = wp.components;
const { useEntityProp } = wp.coreData;

/**
 * Add Focal Point Picker to Featured Image on posts.
 *
 * @param {function} PostFeaturedImage Featured Image component.
 *
 * @return {function} PostFeaturedImage Modified Featured Image component.
 */
const wrapPostFeaturedImage = createHigherOrderComponent(
  (PostFeaturedImage) => {
    return (props) => {
      const { media } = props;

      const [meta, setMeta] = useEntityProp('postType', 'post', 'meta');

      const setFeaturedImageMeta = (val) => {
        setMeta(
          Object.assign({}, meta, {
            featured_image_focal_point: val,
          })
        );
      };

      if (media && media.source_url) {
        const url = media.source_url;

        return (
          <Fragment>
            <PostFeaturedImage {...props} />
            <FocalPointPicker
              label={__('Focal point picker')}
              url={url}
              value={meta.featured_image_focal_point}
              onChange={(newFocalPoint) => setFeaturedImageMeta(newFocalPoint)}
            />
          </Fragment>
        );
      }

      return <PostFeaturedImage {...props} />;
    };
  },
  'wrapPostFeaturedImage'
);

addFilter(
  'editor.PostFeaturedImage',
  'abc/featured-image-control',
  wrapPostFeaturedImage
);

couple things I noted:

Highly recommend making the FocalPointPicker component a part of the Featured Image, with the add_theme_support flag suggestion @cr0ybot mentioned.

ryanapsmith commented 3 years ago
$focal_point_style = ( $focal_point && $focal_point['x'] && $focal_point['y'] ) ? sprintf('object-position: %d%% %d%%', $focal_point['x']*100, $focal_point['y']*100) : '';

"0" is a valid value for x and y, but returns false in the statement above, btw, which would prevent any style from being added to the wp_get_attachment_image call. Maybe isset() would work better here.

NickGreen commented 3 years ago

@cr0ybot or @ryanapsmith I'm interested in getting some of your code working, to see if I can push this along at all.

Edit: I've done some research into how this code might run in a Gutenberg context, and it's a little more clear now, but if you happen to have a plugin where this code is in context, it would still be very helpful!

ryanapsmith commented 3 years ago

@cr0ybot or @ryanapsmith I'm interested in getting some of your code working, to see if I can push this along at all.

Edit: I've done some research into how this code might run in a Gutenberg context, and it's a little more clear now, but if you happen to have a plugin where this code is in context, it would still be very helpful!

@NickGreen you got it right, you have to enqueue the JS file that contains the snippet I provided.

/**
  * Register the JavaScript for the admin area.
  *
  * @since    1.0.0
  */
  public function my_enqueue_scripts() {
    wp_enqueue_script( 'admin-blocks', 'admin-blocks.js', array(
        'jquery',
        'wp-dom-ready',
        'wp-i18n',
        'wp-hooks',
        'wp-element',
        'wp-block-editor',
        'wp-compose',
        'wp-components',
        'wp-core-data',
        'wp-plugins',
        'wp-edit-post',
    ), '1.0.0', false );
  }

add_action('admin_enqueue_scripts', 'my_enqueue_scripts');

admin-blocks.js would be the file that has the snippet in it, and the dependency array in wp_enqueue_script function call just makes sure the stuff you need is available.

Insert this php into your theme's functions.php file (easiest route), or generate a plugin and stick this in there, then activate the plugin (best way). Never generated a plugin before? Couple tools out there, like WP CLI (https://developer.wordpress.org/cli/commands/scaffold/plugin/) or the boilerplate generator found at https://wppb.me

Next time you load the editor, the focal point picker should show up in the sidebar for any post type with featured image support.

NickGreen commented 3 years ago

This is a really interesting topic, and something that deserves thought into the best way that it would actually work in practice.

First of all, the cover image focal point is a set of x and y coordinates that determine the offset of the image when loaded into a container. It does not re-crop the image. If this same approach were used for the featured image, then you're relying on the theme developer to output the featured image in the various locations using those coordinates.

This technique also isn't as performant as cropping would be; you're loading a larger image into a smaller container, and moving it around. This kind of defeats the whole purpose of having thumbnails of various sizes which are used in the appropriate context (Consider an archive page which could have tens or hundreds of featured image thumbnails. Would you want to load the full sized image in all of those cases?).

Consider how this 3rd party plugin does it: https://wecodepixels.com/shop/theia-smart-thumbnails-for-wordpress/

This is how, as a user, I would assume a focal point picker would work for a featured image. Similar to cropping an image from the Gutenberg image block, I would assume that setting a focal point would create a new version of the image, and re-crop all of the thumbnails based on the new focal point. This would not require theme developers to do anything to support it, since displaying various image sizes in various contexts is already standard practice.

strarsis commented 3 years ago

@NickGreen Also the WordPress image sizes support a crop offset: https://developer.wordpress.org/reference/functions/add_image_size/#parameters:~:text=x_crop_position%20accepts%3A%20'left'%2C%20'center'%2C%20or%20'right'.,y_crop_position%20accepts%3A%20'top'%2C%20'center'%2C%20or%20'bottom'.

ryanapsmith commented 3 years ago

@NickGreen I'd personally prefer a lossless approach. Selecting a focal point allows the theme developer greater control over aspect ratios, with the content editor only having to worry about placement within an image container, while the developer dictates final sizing across different breakpoints with the container. Cropping would produce undesirable effects there (like trying to cram a square crop into a hero region with a 16:9 aspect ratio). The focal point picker suffices here – for image manipulation like cropping, that's more appropriate in the body of a post than a region predefined in a theme's template.

strarsis commented 3 years ago

@ryanapsmith: Good point - but wouldn't <picture alleviate this issue? It allows art direction.

koraysels commented 2 years ago

@NickGreen

no this is just cropping, that is not preferable., we need focal point for responsive images that use object-fill.. so cropping is out of the question

koraysels commented 2 years ago

@cr0ybot I try to load the snippet in the admin but it seems I cannot load in React code.. It results in a Syntax error image

koraysels commented 2 years ago

had to rewrite it like this... probably am doing something wrong...

const {__} = wp.i18n;
const {addFilter} = wp.hooks;
const {Fragment} = wp.element;
const {createHigherOrderComponent} = wp.compose;
const {FocalPointPicker} = wp.components;
const useEntityProp = wp.coreData.useEntityProp;
const el = wp.element.createElement;

/**
 * Add Focal Point Picker to Featured Image on posts.
 *
 * @param {function} PostFeaturedImage Featured Image component.
 *
 * @return {function} PostFeaturedImage Modified Featured Image component.
 */
const wrapPostFeaturedImage = createHigherOrderComponent(
    (PostFeaturedImage) => {
        return (props) => {
            const {media} = props;

            const [meta, setMeta] = useEntityProp('postType', 'project', 'meta');

            const setFeaturedImageMeta = (val) => {
                if (meta)
                    setMeta(
                        Object.assign({}, meta, {
                            featured_image_focal_point: val,
                        })
                    );
                else {
                    setMeta({
                        featured_image_focal_point: val,
                    });
                }
            };

            if (media && media.source_url) {
                const url = media.source_url;

                return el(
                    wp.element.Fragment,
                    {},
                    'Prepend above',
                    el(
                        PostFeaturedImage,
                        props
                    ),
                    el(
                        wp.components.FocalPointPicker,
                        {
                            label: __('Focal point picker'),
                            value: meta?.featured_image_focal_point,
                            url: url,
                            onChange: (newFocalPoint) => setFeaturedImageMeta(newFocalPoint)
                        }
                    )
                )
            }

            return el(
                PostFeaturedImage,
                props
            )
        };
    },
    'wrapPostFeaturedImage'
);

wp.hooks.addFilter(
    'editor.PostFeaturedImage',
    'koraysels/wrap-post-featured-image',
    wrapPostFeaturedImage
);
koraysels commented 2 years ago

If you want it to be displayed in wp-graphql i wrote this:

add_action('graphql_register_types', function () {
    register_graphql_object_type('FocalPoint', [
        'description' => __("FocalPoint of image", 'your-textdomain'),
        'fields' => [
            'x' => [
                'type' => 'Number',
                'description' => __('x focal point', 'your-textdomain'),
            ],
            'y' => [
                'type' => 'Number',
                'description' => __('y focal point', 'your-textdomain'),
            ]
        ],
    ]);
});

add_action('graphql_register_types', function () {
    register_graphql_field('NodeWithFeaturedImage', 'featuredImageFocalPoint', [
        'type' => 'FocalPoint',
        'description' => __('Focal Point of the featured Image', 'your-textdomain'),
        'resolve' => function (\WPGraphQL\Model\Post $post, $args, $context, $info) {
            return get_post_meta($post->ID, 'featured_image_focal_point', true);
        }
    ]);
});
cr0ybot commented 2 years ago

had to rewrite it like this... probably am doing something wrong...

You would need to be set up with a build step to use JSX syntax.

Also, sorry for being absent from this thread but @ryanapsmith did a great job condensing/simplifying the implementation via createHigherOrderComponent and newer hooks like useEntityProp.

Maybe I'll revisit this next time I have a need for it, though with the new Post Featured Image block (#19875) maybe this is not quite as relevant anymore, especially with the push towards full site editing instead of coded themes.

MadtownLems commented 2 years ago

This is such a good idea. Setting the focal point would ideally be capable on all images in the Media Library. Most themes register a variety of different sizes, with different aspect ratios, and many plugins register their own as well. Displaying these images with a reasonable crop would be such a benefit to sites.

There have been various plugins attempting this over the years, but most seem abandoned or add way too much additional complexity beyond simply setting a focal point.

tedw commented 4 months ago

FWIW I like the way https://wordpress.org/plugins/better-image-sizes/ implements this (h/t @kubiqsk). The admin UI is really nice, providing previews of how the cropped image will look at different aspect ratios. It also includes face detection, which in my experience is the primary reason for wanting to set the focal point in the first place.

screenshot-3