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

Focal point + crop for featured images #20321

Open
strarsis opened this issue Feb 19, 2020 · 18 comments
Open

Focal point + crop for featured images #20321

strarsis opened this issue Feb 19, 2020 · 18 comments
Labels
[Feature] Media Anything that impacts the experience of managing media Needs Design Needs design efforts. [Type] Enhancement A suggestion for improvement.

Comments

@strarsis
Copy link
Contributor

strarsis commented Feb 19, 2020

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/

@brentswisher brentswisher added the [Type] Enhancement A suggestion for improvement. label Feb 20, 2020
@mtias mtias added the [Feature] Media Anything that impacts the experience of managing media label Feb 24, 2020
@cr0ybot
Copy link
Contributor

cr0ybot commented Mar 3, 2020

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
Copy link
Contributor

cr0ybot commented Mar 6, 2020

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 #17018 (comment)

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

@mundschenk-at
Copy link

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

I'd like to know that as well.

@ryanapsmith
Copy link

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:

  • the media object didn't have the width and height properties where expected, they're buried in the media_details property, and I opted not to use them since the FocalPointPicker doesn't really need them.
  • useEntityProp saved a lot of trouble with fetching and saving the meta value. It could also probably be used to restrict the rendering of the FocalPointPicker component to specific post types.
  • FocalPointPicker is barely usable in Safari (but that's for another issue)

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

@ryanapsmith
Copy link

$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
Copy link

NickGreen commented Mar 23, 2021

@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
Copy link

@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
Copy link

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.

@melchoyce melchoyce added the Needs Design Needs design efforts. label Mar 25, 2021
@strarsis
Copy link
Contributor Author

strarsis commented Mar 25, 2021

@ryanapsmith
Copy link

@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
Copy link
Contributor Author

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

@koraysels
Copy link

koraysels commented Dec 17, 2021

@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
Copy link

@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
Copy link

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
Copy link

koraysels commented Dec 17, 2021

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
Copy link
Contributor

cr0ybot commented Dec 17, 2021

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
Copy link

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
Copy link

tedw commented Apr 10, 2024

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
[Feature] Media Anything that impacts the experience of managing media Needs Design Needs design efforts. [Type] Enhancement A suggestion for improvement.
Projects
None yet
Development

No branches or pull requests