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

Cover: Set custom color when applying initial background image #22564

Closed
wants to merge 34 commits into from
Closed
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
2dab13e
Add useColorExtract hook. Set up color extract with background image …
May 22, 2020
e07f370
Fix example for useColorExtract
May 22, 2020
78732b2
Fix background color extraction to change on replace media
May 23, 2020
fa4d59e
Extract core functionality from color-thief for color extraction
May 23, 2020
5bdefaf
Fix quantize dependency source
May 23, 2020
bad740d
Fix color extract on initial image setting
Jun 1, 2020
df515ae
Merge branch 'master' into try/cover-background-image-auto-color
Jun 2, 2020
60c3578
Add "Pick dominant" button in Overlay InspectorControl
Jun 2, 2020
a705c48
Merge branch 'master' into try/cover-background-image-auto-color
Jun 3, 2020
a6a63db
Add custom dominant color to Solid color palette
Jun 3, 2020
5cb1c44
Merge branch 'master' into try/cover-background-image-auto-color
Jun 4, 2020
c49e4b2
Update Image dominant color tooltip label
Jun 4, 2020
54799c5
Merge branch 'master' into try/cover-background-image-auto-color
Jun 8, 2020
59faa2a
Merge branch 'master' into try/cover-background-image-auto-color
Jun 10, 2020
0794f03
Merge branch 'master' into try/cover-background-image-auto-color
Jun 11, 2020
e5a7e3a
Merge branch 'master' into try/cover-background-image-auto-color
Jun 16, 2020
cc9e0c0
Merge branch 'master' into try/cover-background-image-auto-color
Jun 17, 2020
2b04cd8
Merge branch 'master' into try/cover-background-image-auto-color
Jun 19, 2020
a74392d
Merge branch 'master' into try/cover-background-image-auto-color
Jun 22, 2020
431c6ef
Merge branch 'master' into try/cover-background-image-auto-color
Jun 23, 2020
8a0bbfa
Merge branch 'master' into try/cover-background-image-auto-color
Jun 23, 2020
cf51693
Merge branch 'master' into try/cover-background-image-auto-color
Jun 26, 2020
c407669
Merge branch 'master' into try/cover-background-image-auto-color
Jun 26, 2020
d62e498
Merge branch 'master' into try/cover-background-image-auto-color
Jun 29, 2020
291012b
Fix test from merge issue
Jun 29, 2020
ad809ac
Merge branch 'master' into try/cover-background-image-auto-color
Jun 30, 2020
756656e
Fix E2E tests. Adjust try/catch + only loading if src in extractColor…
Jun 30, 2020
1f6ffc7
Wrap getImageNode() in try/catch to handle CORS error
Jun 30, 2020
91a7aa6
Merge branch 'master' into try/cover-background-image-auto-color
Jul 7, 2020
cec9121
Fix extract function to run only for Editor (not preview)
Jul 7, 2020
ea75467
Remove export of useControlledState
Jul 7, 2020
cdbdab3
Merge branch 'master' into try/cover-background-image-auto-color
Sep 14, 2020
53a15a5
useExtractColor: Replace ColorThief with FastAverageColor module (alr…
Sep 14, 2020
73d87f8
Adjust fast-average-color package to match the one used in block-library
Sep 14, 2020
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
6 changes: 6 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

30 changes: 29 additions & 1 deletion packages/block-library/src/cover/edit.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
ResizableBox,
ToggleControl,
withNotices,
useColorExtract,
} from '@wordpress/components';
import { compose, withInstanceId, useInstanceId } from '@wordpress/compose';
import {
Expand Down Expand Up @@ -293,7 +294,25 @@ 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 { extractColor } = useColorExtract( {
color: backgroundColorValue,
onChange: ( nextColor ) => setOverlayColor( nextColor ),
src: url,
} );

const handleOnExtractColor = async () => {
const [ value ] = await extractColor();
setOverlayColor( value );
};

const controls = (
<>
Expand Down Expand Up @@ -391,6 +410,15 @@ function CoverEdit( {
},
] }
>
<div className="block-library-cover__dominant-color-button-wrapper">
<Button
isSecondary
isSmall
onClick={ handleOnExtractColor }
>
{ __( 'Pick dominant' ) }
</Button>
</div>
{ !! url && (
<RangeControl
label={ __( 'Background opacity' ) }
Expand Down
5 changes: 5 additions & 0 deletions packages/block-library/src/cover/editor.scss
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,11 @@
width: 100%;
}

.block-library-cover__dominant-color-button-wrapper {
margin-bottom: $grid-unit-30;
margin-top: $grid-unit-20 * -1;
}

.block-library-cover__reset-button {
margin-left: auto;
}
Expand Down
1 change: 1 addition & 0 deletions packages/components/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
"lodash": "^4.17.15",
"memize": "^1.1.0",
"moment": "^2.22.1",
"quantize": "^1.0.2",
"re-resizable": "^6.0.0",
"react-dates": "^17.1.1",
"react-spring": "^8.0.20",
Expand Down
3 changes: 3 additions & 0 deletions packages/components/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -126,3 +126,6 @@ export {
} from './higher-order/with-focus-return';
export { default as withNotices } from './higher-order/with-notices';
export { default as withSpokenMessages } from './higher-order/with-spoken-messages';

// Utilities
export { useColorExtract, useControlledState } from './utils/hooks';
211 changes: 211 additions & 0 deletions packages/components/src/utils/hooks/extract-color.js
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();
Copy link
Member

Choose a reason for hiding this comment

The 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

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tapping into getMedia could be good.

Copy link
Author

Choose a reason for hiding this comment

The 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();
};
2 changes: 2 additions & 0 deletions packages/components/src/utils/hooks/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { useColorExtract } from './use-color-extract';
export { useControlledState } from './use-controlled-state';
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { noop } from 'lodash';
/**
* Internal dependencies
*/
import { useControlledState } from '../hooks';
import { useControlledState } from '../use-controlled-state';

describe( 'hooks', () => {
const getInput = () => screen.getByTestId( 'input' );
Expand Down
Loading