-
Notifications
You must be signed in to change notification settings - Fork 4.2k
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
Cover: Set custom color when applying initial background image #22564
Changes from all commits
2dab13e
e07f370
78732b2
fa4d59e
5bdefaf
bad740d
df515ae
60c3578
a705c48
a6a63db
5cb1c44
c49e4b2
54799c5
59faa2a
0794f03
e5a7e3a
cc9e0c0
2b04cd8
a74392d
431c6ef
8a0bbfa
cf51693
c407669
d62e498
291012b
ad809ac
756656e
1f6ffc7
91a7aa6
cec9121
ea75467
cdbdab3
53a15a5
73d87f8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,7 @@ | ||
/** | ||
* External dependencies | ||
*/ | ||
import { noop } from 'lodash'; | ||
import classnames from 'classnames'; | ||
import FastAverageColor from 'fast-average-color'; | ||
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. Hi @ItsJonQ, the cover block was already computing the color of an image using the FastAverageColor module. I think we should avoid using two different modules for the same task so I guess we should opt for one of the modules and update the code to use the chosen module. 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. @jorgefilipecosta Oh! I wasn't aware of that. Thank you for pointing that out. I'll make an update |
||
import tinycolor from 'tinycolor2'; | ||
|
@@ -19,6 +20,7 @@ import { | |
ResizableBox, | ||
ToggleControl, | ||
withNotices, | ||
useColorExtract, | ||
__experimentalBoxControl as BoxControl, | ||
} from '@wordpress/components'; | ||
import { compose, withInstanceId, useInstanceId } from '@wordpress/compose'; | ||
|
@@ -304,10 +306,25 @@ function CoverEdit( { | |
} | ||
} | ||
|
||
const backgroundColorValue = overlayColor.color || gradientValue; | ||
const hasBackground = !! ( url || overlayColor.color || gradientValue ); | ||
|
||
const showFocalPointPicker = | ||
isVideoBackground || ( isImageBackground && ! hasParallax ); | ||
|
||
/** | ||
* Custom hook used for setting the initial background color. | ||
* | ||
* If a background image is set, this hook extracts the primary color from | ||
* that image and sets it as a background color. | ||
*/ | ||
const { customColors } = useCoverColorExtract( { | ||
color: backgroundColorValue, | ||
isSelected, | ||
onChange: setOverlayColor, | ||
src: url, | ||
} ); | ||
|
||
const controls = ( | ||
<> | ||
<BlockControls> | ||
|
@@ -395,6 +412,7 @@ function CoverEdit( { | |
<PanelColorGradientSettings | ||
title={ __( 'Overlay' ) } | ||
initialOpen={ true } | ||
customColors={ customColors } | ||
settings={ [ | ||
{ | ||
colorValue: overlayColor.color, | ||
|
@@ -548,6 +566,53 @@ function CoverEdit( { | |
); | ||
} | ||
|
||
function useCoverColorExtract( { | ||
backgroundColor, | ||
onChange = noop, | ||
isSelected = false, | ||
src, | ||
} ) { | ||
const [ customExtractedColor, setCustomExtractedColor ] = useState( null ); | ||
const [ didSelect, setDidSelect ] = useState( false ); | ||
|
||
const updateCustomOverlayColor = ( value ) => { | ||
onChange( value ); | ||
setCustomExtractedColor( value ); | ||
}; | ||
|
||
const { extractColor } = useColorExtract( { | ||
color: backgroundColor, | ||
onChange: updateCustomOverlayColor, | ||
src, | ||
} ); | ||
|
||
useEffect( () => { | ||
// Extracts color to add to color palette. | ||
// Run when the block is first selected. | ||
if ( ! didSelect && isSelected ) { | ||
extractColor().then( ( [ value ] ) => { | ||
setCustomExtractedColor( value ); | ||
} ); | ||
setDidSelect( true ); | ||
} | ||
}, [ isSelected, didSelect ] ); | ||
|
||
let customColors = []; | ||
if ( customExtractedColor ) { | ||
customColors = [ | ||
{ | ||
name: __( 'Image dominant color' ), | ||
slug: __( 'image-dominant-color' ), | ||
Comment on lines
+604
to
+605
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. Some context or |
||
color: customExtractedColor, | ||
}, | ||
]; | ||
} | ||
|
||
return { | ||
customColors, | ||
}; | ||
} | ||
|
||
export default compose( [ | ||
withDispatch( ( dispatch ) => { | ||
const { toggleSelection } = dispatch( 'core/block-editor' ); | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,4 @@ | ||
export { useColorExtract } from './use-color-extract'; | ||
export { default as useControlledState } from './use-controlled-state'; | ||
export { default as useJumpStep } from './use-jump-step'; | ||
export { default as useUpdateEffect } from './use-update-effect'; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,131 @@ | ||
/** | ||
* External dependencies | ||
*/ | ||
import { noop } from 'lodash'; | ||
import FastAverageColor from 'fast-average-color'; | ||
|
||
/** | ||
* WordPress dependencies | ||
*/ | ||
import { useState, useEffect, useRef } from '@wordpress/element'; | ||
|
||
/** | ||
* Internal dependencies | ||
*/ | ||
|
||
/** | ||
* | ||
* @typedef UseColorExtractProps | ||
* | ||
* @property {string} color The initial color, used to track updates. | ||
* @property {string} onChange Callback when colors are extracted. | ||
* @property {Function} src The source of the image to extract colors from. | ||
*/ | ||
|
||
/** | ||
* | ||
* @typedef UseColorExtractHookProps | ||
* | ||
* @property {string} colors The color value extracted from an image source. | ||
* @property {Function} extractColor An async method (Promise) that extracts color values from the image source. | ||
*/ | ||
|
||
/** | ||
* Custom hook that extracts the primary color of an image. | ||
* | ||
* This component's extraction technique may not work depending on | ||
* CORS policy. | ||
* | ||
* @example | ||
* | ||
* ```js | ||
* useColorExtract({ | ||
* src: '/my-image.png', | ||
* onChange: imageColor => setState(imageColor) | ||
* }) | ||
* ``` | ||
* @param {UseColorExtractProps} props Props for the custom hook. | ||
* @return {UseColorExtractHookProps} Values and methods. | ||
*/ | ||
export function useColorExtract( { | ||
color: initialColor, | ||
onChange = noop, | ||
src, | ||
} ) { | ||
const [ color, setColor ] = useState(); | ||
const srcRef = useRef( src ); | ||
const initialColorRef = useRef( initialColor ); | ||
const imageNodeRef = useRef(); | ||
|
||
const getImageNode = () => { | ||
if ( ! imageNodeRef.current ) { | ||
imageNodeRef.current = document.createElement( 'img' ); | ||
imageNodeRef.current.crossOrigin = 'Anonymous'; | ||
} | ||
return imageNodeRef.current; | ||
}; | ||
|
||
const extractColor = () => { | ||
return new Promise( ( resolve, reject ) => { | ||
try { | ||
const imageNode = getImageNode(); | ||
|
||
imageNode.onload = async () => { | ||
const extractor = new FastAverageColor(); | ||
|
||
extractor.getColorAsync( imageNode, ( data ) => { | ||
setColor( data.hex ); | ||
resolve( [ data.hex, data ] ); | ||
} ); | ||
|
||
srcRef.current = src; | ||
}; | ||
// Load the image | ||
if ( src ) { | ||
imageNode.src = src; | ||
} | ||
} catch ( err ) { | ||
reject( err ); | ||
} | ||
} ); | ||
}; | ||
|
||
useEffect( () => { | ||
// Guard to quick return if the src does not change | ||
if ( srcRef.current === src ) { | ||
return; | ||
} | ||
/** | ||
* Checks to see if the src changes. | ||
* For Cover blocks, the initial src (on load) is undefined. | ||
* This tracks to see if an existing src changes to another. | ||
*/ | ||
const didSrcChange = | ||
// Initial src exists | ||
srcRef.current && | ||
// Next src exists | ||
src && | ||
// Next src does not match initial src (changed) | ||
srcRef.current !== src; | ||
|
||
// Guard to handle the initial load of an image (with potential pre-existing color) | ||
if ( initialColorRef.current && ! didSrcChange ) { | ||
return; | ||
} | ||
|
||
extractColor() | ||
.then( ( [ value, data ] ) => { | ||
const imageNode = getImageNode(); | ||
onChange( value, { data, node: imageNode } ); | ||
} ) | ||
.catch( ( err ) => { | ||
// eslint-disable-next-line no-console | ||
console.err( err ); | ||
} ); | ||
}, [ src ] ); | ||
|
||
return { | ||
color, | ||
extractColor, | ||
}; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
/** | ||
* Internal dependencies | ||
*/ | ||
/** | ||
* WordPress dependencies | ||
*/ | ||
import { useEffect } from '@wordpress/element'; | ||
import { useColorExtract } from '../hooks'; | ||
|
||
export default { title: 'Utils/Hooks/useColorExtract' }; | ||
|
||
const Example = () => { | ||
const src = | ||
'https://i0.wp.com/themes.svn.wordpress.org/twentytwenty/1.5/screenshot.png?w=1144&strip=all'; | ||
const { color, extractColor } = useColorExtract( { src } ); | ||
|
||
useEffect( () => { | ||
extractColor(); | ||
}, [] ); | ||
|
||
return ( | ||
<div> | ||
<h3>Extracted Color</h3> | ||
<p> | ||
<div | ||
style={ { width: 40, height: 40, backgroundColor: color } } | ||
/> | ||
</p> | ||
<h3>Image Source</h3> | ||
<img | ||
src={ src } | ||
alt="Twenty Twenty Theme Preview" | ||
style={ { width: 300, height: 'auto' } } | ||
/> | ||
</div> | ||
); | ||
}; | ||
|
||
export const _default = () => { | ||
return <Example />; | ||
}; |
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.
Depending on how often
PanelColorGradientSettingsInner
re-renders as a user interacts with the block, might be good to:Naturally, this supposes that references for
customColors
andcolors
are preserved when possible.