-
Notifications
You must be signed in to change notification settings - Fork 4.3k
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 31 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'; | ||
|
@@ -297,7 +299,21 @@ function CoverEdit( { | |
}%`; | ||
} | ||
|
||
const hasBackground = !! ( url || overlayColor.color || gradientValue ); | ||
const backgroundColorValue = overlayColor.color || gradientValue; | ||
const hasBackground = !! ( url || backgroundColorValue ); | ||
|
||
/** | ||
* 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 = ( | ||
<> | ||
|
@@ -390,6 +406,7 @@ function CoverEdit( { | |
<PanelColorGradientSettings | ||
title={ __( 'Overlay' ) } | ||
initialOpen={ true } | ||
customColors={ customColors } | ||
settings={ [ | ||
{ | ||
colorValue: overlayColor.color, | ||
|
@@ -542,6 +559,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 |
---|---|---|
@@ -0,0 +1,211 @@ | ||
/** | ||
* Parts of this source were derived and modified from react-color, | ||
* released under the MIT license. | ||
* | ||
* https://github.com/lokesh/color-thief | ||
* | ||
* Copyright (c) 2015 Lokesh Dhakar | ||
* | ||
* Permission is hereby granted, free of charge, to any person obtaining a copy | ||
* of this software and associated documentation files (the "Software"), to deal | ||
* in the Software without restriction, including without limitation the rights | ||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||
* copies of the Software, and to permit persons to whom the Software is | ||
* furnished to do so, subject to the following conditions: | ||
* | ||
* The above copyright notice and this permission notice shall be included in all | ||
* copies or substantial portions of the Software. | ||
* | ||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | ||
* SOFTWARE. | ||
*/ | ||
|
||
/** | ||
* External dependencies | ||
*/ | ||
import quantize from 'quantize'; | ||
|
||
const { XMLHttpRequest } = window; | ||
|
||
/* | ||
* Color Thief v2.3.0 | ||
* by Lokesh Dhakar - http://www.lokeshdhakar.com | ||
* | ||
* Thanks | ||
* ------ | ||
* Nick Rabinowitz - For creating quantize.js. | ||
* John Schulz - For clean up and optimization. @JFSIII | ||
* Nathan Spady - For adding drag and drop support to the demo page. | ||
* | ||
* License | ||
* ------- | ||
* Copyright Lokesh Dhakar | ||
* Released under the MIT license | ||
* https://raw.githubusercontent.com/lokesh/color-thief/master/LICENSE | ||
* | ||
* @license | ||
*/ | ||
|
||
function createPixelArray( imgData, pixelCount, quality ) { | ||
const pixels = imgData; | ||
const pixelArray = []; | ||
|
||
for ( let i = 0, offset, r, g, b, a; i < pixelCount; i = i + quality ) { | ||
offset = i * 4; | ||
r = pixels[ offset + 0 ]; | ||
g = pixels[ offset + 1 ]; | ||
b = pixels[ offset + 2 ]; | ||
a = pixels[ offset + 3 ]; | ||
|
||
// If pixel is mostly opaque and not white | ||
if ( typeof a === 'undefined' || a >= 125 ) { | ||
if ( ! ( r > 250 && g > 250 && b > 250 ) ) { | ||
pixelArray.push( [ r, g, b ] ); | ||
} | ||
} | ||
} | ||
return pixelArray; | ||
} | ||
|
||
function validateOptions( options ) { | ||
let { colorCount, quality } = options; | ||
|
||
if ( | ||
typeof colorCount === 'undefined' || | ||
! Number.isInteger( colorCount ) | ||
) { | ||
colorCount = 10; | ||
} else if ( colorCount === 1 ) { | ||
throw new Error( | ||
'colorCount should be between 2 and 20. To get one color, call getColor() instead of getPalette()' | ||
); | ||
} else { | ||
colorCount = Math.max( colorCount, 2 ); | ||
colorCount = Math.min( colorCount, 20 ); | ||
} | ||
|
||
if ( typeof quality === 'undefined' || Number.isInteger( quality ) ) { | ||
quality = 10; | ||
} else if ( quality < 1 ) { | ||
quality = 10; | ||
} | ||
|
||
return { | ||
colorCount, | ||
quality, | ||
}; | ||
} | ||
|
||
/* | ||
CanvasImage Class | ||
Class that wraps the html image element and canvas. | ||
It also simplifies some of the canvas context manipulation | ||
with a set of helper functions. | ||
*/ | ||
|
||
const CanvasImage = function ( image ) { | ||
this.canvas = document.createElement( 'canvas' ); | ||
this.context = this.canvas.getContext( '2d' ); | ||
this.width = this.canvas.width = image.width; | ||
this.height = this.canvas.height = image.height; | ||
this.context.drawImage( image, 0, 0, this.width, this.height ); | ||
}; | ||
|
||
CanvasImage.prototype.getImageData = function () { | ||
return this.context.getImageData( 0, 0, this.width, this.height ); | ||
}; | ||
|
||
export const ColorThief = function () {}; | ||
|
||
/* | ||
* getColor(sourceImage[, quality]) | ||
* returns {r: num, g: num, b: num} | ||
* | ||
* Use the median cut algorithm provided by quantize.js to cluster similar | ||
* colors and return the base color from the largest cluster. | ||
* | ||
* Quality is an optional argument. It needs to be an integer. 1 is the highest quality settings. | ||
* 10 is the default. There is a trade-off between quality and speed. The bigger the number, the | ||
* faster a color will be returned but the greater the likelihood that it will not be the visually | ||
* most dominant color. | ||
* | ||
* */ | ||
ColorThief.prototype.getColor = function ( sourceImage, quality = 10 ) { | ||
const palette = this.getPalette( sourceImage, 5, quality ); | ||
/** | ||
* Custom update: | ||
* The palette may be null if the image is pure white. | ||
*/ | ||
const dominantColor = palette ? palette[ 0 ] : undefined; | ||
return dominantColor; | ||
}; | ||
|
||
/* | ||
* getPalette(sourceImage[, colorCount, quality]) | ||
* returns array[ {r: num, g: num, b: num}, {r: num, g: num, b: num}, ...] | ||
* | ||
* Use the median cut algorithm provided by quantize.js to cluster similar colors. | ||
* | ||
* colorCount determines the size of the palette; the number of colors returned. If not set, it | ||
* defaults to 10. | ||
* | ||
* BUGGY: Function does not always return the requested amount of colors. It can be +/- 2. | ||
* | ||
* quality is an optional argument. It needs to be an integer. 1 is the highest quality settings. | ||
* 10 is the default. There is a trade-off between quality and speed. The bigger the number, the | ||
* faster the palette generation but the greater the likelihood that colors will be missed. | ||
* | ||
* | ||
*/ | ||
ColorThief.prototype.getPalette = function ( | ||
sourceImage, | ||
colorCount, | ||
quality | ||
) { | ||
const options = validateOptions( { | ||
colorCount, | ||
quality, | ||
} ); | ||
|
||
// Create custom CanvasImage object | ||
const image = new CanvasImage( sourceImage ); | ||
const imageData = image.getImageData(); | ||
const pixelCount = image.width * image.height; | ||
|
||
const pixelArray = createPixelArray( | ||
imageData.data, | ||
pixelCount, | ||
options.quality | ||
); | ||
|
||
// Send array to quantize function which clusters values | ||
// using median cut algorithm | ||
const cmap = quantize( pixelArray, options.colorCount ); | ||
const palette = cmap ? cmap.palette() : null; | ||
|
||
return palette; | ||
}; | ||
|
||
ColorThief.prototype.getImageData = function ( imageUrl, callback ) { | ||
const xhr = new XMLHttpRequest(); | ||
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. Would it be possible to avoid a network request and process the content of file when it's uploaded? In some cases like drag & drop or when uploading with the media placeholder (the one in Gutenberg) we already use data of the image to show the placeholder. /cc @ellatrix 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. Tapping into 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. I tried to not touch this file 🙈 . I'm open to changing this implementation if it's a concern |
||
xhr.open( 'GET', imageUrl, true ); | ||
xhr.responseType = 'arraybuffer'; | ||
xhr.onload = function () { | ||
if ( this.status === 200 ) { | ||
const uInt8Array = new Uint8Array( this.response ); | ||
const binaryString = new Array( uInt8Array.length ); | ||
for ( let i = 0; i < uInt8Array.length; i++ ) { | ||
binaryString[ i ] = String.fromCharCode( uInt8Array[ i ] ); | ||
} | ||
const data = binaryString.join( '' ); | ||
const base64 = window.btoa( data ); | ||
callback( 'data:image/png;base64,' + base64 ); | ||
} | ||
}; | ||
xhr.send(); | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1,3 @@ | ||
export { useColorExtract } from './use-color-extract'; | ||
export { default as useControlledState } from './use-controlled-state'; | ||
export { default as useJumpStep } from './use-jump-step'; |
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.