blob: 0eb0014255e33fe6838adb6e9237da4d329728eb [file] [log] [blame]
// Copyright (C) 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/**
* @fileoverview Color picker used by <input type='color' />
*/
function initializeColorPicker() {
if (global.params.selectedColor === undefined) {
global.params.selectedColor = DefaultColor;
}
const colorPicker = new ColorPicker(new Color(global.params.selectedColor));
main.append(colorPicker);
const width = colorPicker.offsetWidth;
const height = colorPicker.offsetHeight;
resizeWindow(width, height);
}
/**
* @param {!Object} args
* @return {?string} An error message, or null if the argument has no errors.
*/
function validateColorPickerArguments(args) {
if (args.shouldShowColorSuggestionPicker)
return 'Should be showing the color suggestion picker.'
if (!args.selectedColor)
return 'No selectedColor.';
return null;
}
/**
* Supported color channels.
* @enum {number}
*/
const ColorChannel = {
UNDEFINED: 0,
HEX: 1,
R: 2,
G: 3,
B: 4,
H: 5,
S: 6,
L: 7,
};
/**
* Supported color formats.
* @enum {number}
*/
const ColorFormat = {
UNDEFINED: 0,
HEX: 1,
RGB: 2,
HSL: 3,
};
/**
* Color: Helper class to get color values in different color formats.
*/
class Color {
/**
* @param {string|!ColorFormat} colorStringOrFormat
* @param {...number} colorValues ignored if colorStringOrFormat is a string
*/
constructor(colorStringOrFormat, ...colorValues) {
if (typeof colorStringOrFormat === 'string') {
colorStringOrFormat = colorStringOrFormat.toLowerCase();
if (colorStringOrFormat.startsWith('#')) {
this.hexValue_ = colorStringOrFormat.substr(1);
} else if (colorStringOrFormat.startsWith('rgb')) {
// Ex. 'rgb(255, 255, 255)' => [255,255,255]
colorStringOrFormat = colorStringOrFormat.replace(/\s+/g, '');
[this.rValue_, this.gValue_, this.bValue_] =
colorStringOrFormat.substring(4, colorStringOrFormat.length - 1)
.split(',').map(Number);
} else if (colorStringOrFormat.startsWith('hsl')) {
colorStringOrFormat = colorStringOrFormat.replace(/%|\s+/g, '');
[this.hValue_, this.sValue_, this.lValue_] =
colorStringOrFormat.substring(4, colorStringOrFormat.length - 1)
.split(',').map(Number);
}
} else {
switch(colorStringOrFormat) {
case ColorFormat.HEX:
this.hexValue_ = colorValues[0].toLowerCase();
break;
case ColorFormat.RGB:
[this.rValue_, this.gValue_, this.bValue_] = colorValues.map(Number);
break;
case ColorFormat.HSL:
[this.hValue_, this.sValue_, this.lValue_] = colorValues.map(Number);
break;
}
}
}
/**
* @param {!Color} other
*/
equals(other) {
return (this.hexValue === other.hexValue);
}
/**
* @returns {string}
*/
get hexValue() {
this.computeHexValue_();
return this.hexValue_;
}
computeHexValue_() {
if (this.hexValue_ !== undefined) {
// Already computed.
} else if (this.rValue_ !== undefined) {
this.hexValue_ =
Color.rgbToHex(this.rValue_, this.gValue_, this.bValue_);
} else if (this.hValue_ !== undefined) {
this.hexValue_ =
Color.hslToHex(this.hValue_, this.sValue_, this.lValue_);
}
}
asHex() {
return '#' + this.hexValue;
}
/**
* @returns {number} between 0 and 255
*/
get rValue() {
this.computeRGBValues_();
return this.rValue_;
}
/**
* @returns {number} between 0 and 255
*/
get gValue() {
this.computeRGBValues_();
return this.gValue_;
}
/**
* @returns {number} between 0 and 255
*/
get bValue() {
this.computeRGBValues_();
return this.bValue_;
}
computeRGBValues_() {
if (this.rValue_ !== undefined) {
// Already computed.
} else if (this.hexValue_ !== undefined) {
[this.rValue_, this.gValue_, this.bValue_] =
Color.hexToRGB(this.hexValue_);
} else if (this.hValue_ !== undefined) {
[this.rValue_, this.gValue_, this.bValue_] =
Color.hslToRGB(this.hValue_, this.sValue_, this.lValue_);
}
}
rgbValues() {
return [this.rValue, this.gValue, this.bValue];
}
asRGB() {
return 'rgb(' + this.rgbValues().join() + ')';
}
/**
* @returns {number} between 0 and 359
*/
get hValue() {
this.computeHSLValues_();
return this.hValue_;
}
/**
* @returns {number} between 0 and 100
*/
get sValue() {
this.computeHSLValues_();
return this.sValue_;
}
/**
* @returns {number} between 0 and 100
*/
get lValue() {
this.computeHSLValues_();
return this.lValue_;
}
computeHSLValues_() {
if (this.hValue_ !== undefined) {
// Already computed.
} else if (this.rValue_ !== undefined) {
[this.hValue_, this.sValue_, this.lValue_] =
Color.rgbToHSL(this.rValue_, this.gValue_, this.bValue_);
} else if (this.hexValue_ !== undefined) {
[this.hValue_, this.sValue_, this.lValue_] =
Color.hexToHSL(this.hexValue_);
}
}
hslValues() {
return [this.hValue, this.sValue, this.lValue];
}
asHSL() {
return 'hsl(' + this.hValue + ',' + this.sValue + '%,' + this.lValue + '%)';
}
/**
* @param {string} hexValue
* @returns {number[]}
*/
static hexToRGB(hexValue) {
// Ex. 'ffffff' => '[255,255,255]'
const colorValue = parseInt(hexValue, 16);
return [(colorValue >> 16) & 255, (colorValue >> 8) & 255, colorValue & 255];
}
/**
* @param {...number} rgbValues
* @returns {string}
*/
static rgbToHex(...rgbValues) {
// Ex. '[255,255,255]' => 'ffffff'
return rgbValues.reduce((cumulativeHexValue, rgbValue) => {
let hexValue = Number(rgbValue).toString(16);
if(hexValue.length == 1) {
hexValue = '0' + hexValue;
}
return (cumulativeHexValue + hexValue);
}, '');
}
/**
* The algorithm has been written based on the mathematical formula found at:
* https://en.wikipedia.org/wiki/HSL_and_HSV#HSL_to_RGB.
* @param {...number} hslValues
* @returns {number[]}
*/
static hslToRGB(...hslValues) {
let [hValue, sValue, lValue] = hslValues;
hValue /= 60;
sValue /= 100;
lValue /= 100;
let rValue = lValue;
let gValue = lValue;
let bValue = lValue;
let match = 0;
if (sValue !== 0) {
const chroma = (1 - Math.abs(2 * lValue - 1)) * sValue;
const x = chroma * (1 - Math.abs(hValue % 2 - 1));
match = lValue - chroma / 2;
if ((0 <= hValue) && (hValue <= 1)) {
rValue = chroma;
gValue = x;
bValue = 0;
} else if ((1 < hValue) && (hValue <= 2)) {
rValue = x;
gValue = chroma;
bValue = 0;
} else if ((2 < hValue) && (hValue <= 3)) {
rValue = 0;
gValue = chroma;
bValue = x;
} else if ((3 < hValue) && (hValue <= 4)) {
rValue = 0;
gValue = x;
bValue = chroma;
} else if ((4 < hValue) && (hValue <= 5)) {
rValue = x;
gValue = 0;
bValue = chroma;
} else {
// (5 < hValue) && (hValue < 6)
rValue = chroma;
gValue = 0;
bValue = x;
}
}
rValue = Math.round((rValue + match) * 255);
gValue = Math.round((gValue + match) * 255);
bValue = Math.round((bValue + match) * 255);
return [rValue, gValue, bValue];
}
/**
* The algorithm has been written based on the mathematical formula found at:
* https://en.wikipedia.org/wiki/HSL_and_HSV#From_RGB.
* @param {...number} rgbValues
* @returns {number[]}
*/
static rgbToHSL(...rgbValues) {
const [rValue, gValue, bValue] = rgbValues.map((value) => value / 255);
const max = Math.max(rValue, gValue, bValue);
const min = Math.min(rValue, gValue, bValue);
let hValue = 0;
let sValue = 0;
let lValue = (max + min) / 2;
if (max !== min) {
const diff = max - min;
if (max === rValue) {
hValue = ((gValue - bValue) / diff);
} else if (max === gValue) {
hValue = ((bValue - rValue) / diff) + 2;
} else {
// max === bValue
hValue = ((rValue - gValue) / diff) + 4;
}
hValue = Math.round(hValue * 60);
if (hValue < 0) {
hValue += 360;
}
sValue = Math.round((diff / (1 - Math.abs(2 * lValue - 1))) * 100);
}
lValue = Math.round(lValue * 100);
return [hValue, sValue, lValue];
}
/**
* @param {...number} rgbValues
* @returns {string}
*/
static hslToHex(...hslValues) {
return Color.rgbToHex(...Color.hslToRGB(...hslValues));
}
/**
* @param {string} hexValue
* @returns {...number}
*/
static hexToHSL(hexValue) {
return Color.rgbToHSL(...Color.hexToRGB(hexValue));
}
/**
* @param {number[]} colorTripleA RGB or HSL values
* @param {number[]} colorTripleB RGB or HSL values
* Both color triples must be of the same color format.
*/
static distance(colorTripleA, colorTripleB) {
return Math.sqrt(Math.pow(colorTripleA[0] - colorTripleB[0], 2)
+ Math.pow(colorTripleA[1] - colorTripleB[1], 2)
+ Math.pow(colorTripleA[2] - colorTripleB[2], 2));
}
}
/**
* Point: Helper class to get/set coordinates for two dimensional points.
*/
class Point {
/**
* @param {number} x
* @param {number} y
*/
constructor(x, y) {
this.set(x, y);
}
/**
* @param {number} x
* @param {number} y
*/
set(x, y) {
this.x = x;
this.y = y;
}
get x() {
return this.x_;
}
/**
* @param {number} x
*/
set x(x) {
this.x_ = x;
}
get y() {
return this.y_;
}
/**
* @param {number} y
*/
set y(y) {
this.y_ = y;
}
}
/**
* ColorPicker: Custom element providing a color picker implementation.
* A color picker is comprised of three main parts: a visual color
* picker to allow visual selection of colors, a manual color
* picker to allow numeric selection of colors, and submission
* controls to save/discard new color selections.
*/
class ColorPicker extends HTMLElement {
/**
* @param {!Color} initialColor
*/
constructor(initialColor) {
super();
this.selectedColor_ = initialColor;
this.visualColorPicker_ = new VisualColorPicker(initialColor);
this.manualColorPicker_ = new ManualColorPicker(initialColor);
this.submissionControls_ =
new SubmissionControls(this.onSubmitButtonClick_,
this.onCancelButtonClick_);
this.append(this.visualColorPicker_,
this.manualColorPicker_,
this.submissionControls_);
this.visualColorPicker_.addEventListener('visual-color-picker-initialized',
this.initializeListeners_);
}
initializeListeners_ = () => {
this.manualColorPicker_
.addEventListener('manual-color-change', this.onManualColorChange_);
this.addEventListener('visual-color-change', this.onVisualColorChange_);
}
get selectedColor() {
return this.selectedColor_;
}
/**
* @param {!Color} newColor
*/
set selectedColor(newColor) {
this.selectedColor_ = newColor;
}
/**
* @param {!Event} event
*/
onManualColorChange_ = (event) => {
const newColor = event.detail.color;
if (!this.selectedColor.equals(newColor)) {
this.selectedColor = newColor;
// There may not be an exact match for newColor in the HueSlider or
// ColorWell, in which case we will display the closest match. When this
// happens though, we want the manually chosen values to remain the
// selected values (as they were explicitly specified by the user).
// Therefore, we need to prevent them from getting overwritten when
// onVisualColorChange_ runs. We do this by setting the
// processingManualColorChange_ flag here and checking for it inside
// onVisualColorChange_. If the flag is set, the manual color values
// will not be updated with the color shown in the visual color picker.
this.processingManualColorChange_ = true;
this.visualColorPicker_.color = newColor;
this.processingManualColorChange_ = false;
}
}
/**
* @param {!Event} event
*/
onVisualColorChange_ = (event) => {
const newColor = event.detail.color;
if (!this.selectedColor.equals(newColor)) {
if (!this.processingManualColorChange_) {
this.selectedColor = newColor;
this.manualColorPicker_.color = newColor;
} else {
// We are making a visual color change in response to a manual color
// change. So we do not overwrite the manually specified values and do
// not change the selected color.
}
}
}
onSubmitButtonClick_ = () => {
const selectedValue = this.selectedColor_.asHex();
window.setTimeout(function() {
window.pagePopupController.setValueAndClosePopup(0, selectedValue);
}, 100);
}
onCancelButtonClick_ = () => {
window.pagePopupController.closePopup();
}
}
window.customElements.define('color-picker', ColorPicker);
/**
* VisualColorPicker: Provides functionality to see the selected color and
* select a different color visually.
*/
class VisualColorPicker extends HTMLElement {
/**
* @param {!Color} initialColor
*/
constructor(initialColor) {
super();
let visualColorPickerStrip = document.createElement('span');
visualColorPickerStrip.setAttribute('id', 'visual-color-picker-strip');
this.eyeDropper_ = new EyeDropper();
this.colorViewer_ = new ColorViewer(initialColor);
this.hueSlider_ = new HueSlider(initialColor);
visualColorPickerStrip.append(this.eyeDropper_,
this.colorViewer_,
this.hueSlider_);
this.append(visualColorPickerStrip);
this.colorWell_ = new ColorWell(initialColor);
this.prepend(this.colorWell_);
this.colorWell_.addEventListener('color-well-initialized', () => {
this.initializeListeners_();
});
this.hueSlider_.addEventListener('hue-slider-initialized', () => {
this.initializeListeners_();
});
}
initializeListeners_ = () => {
if (this.colorWell_.initialized && this.hueSlider_.initialized) {
this.addEventListener('hue-slider-update', this.onHueSliderUpdate_);
this.addEventListener('visual-color-change', this.onVisualColorChange_);
this.colorWell_
.addEventListener('mousedown', this.onColorWellMouseDown_);
this.hueSlider_
.addEventListener('mousedown', this.onHueSliderMouseDown_);
this.colorWell_
.addEventListener('mousedown', (event) => event.preventDefault());
this.hueSlider_
.addEventListener('mousedown', (event) => event.preventDefault());
document.documentElement
.addEventListener('mousemove', this.onMouseMove_);
document.documentElement.addEventListener('mouseup', this.onMouseUp_);
this.dispatchEvent(new CustomEvent('visual-color-picker-initialized'));
}
}
onHueSliderUpdate_ = () => {
this.colorWell_.fillColor = this.hueSlider_.color;
}
/**
* @param {!Event} event
*/
onVisualColorChange_ = (event) => {
this.colorViewer_.color = event.detail.color;
}
/**
* @param {!Event} event
*/
onColorWellMouseDown_ = (event) => {
this.colorWell_.mouseDown(new Point(event.clientX, event.clientY));
}
/**
* @param {!Event} event
*/
onMouseMove_ = (event) => {
var point = new Point(event.clientX, event.clientY);
this.colorWell_.mouseMove(point);
this.hueSlider_.mouseMove(point);
}
onMouseUp_ = () => {
this.colorWell_.mouseUp();
this.hueSlider_.mouseUp();
}
/**
* @param {!Event} event
*/
onHueSliderMouseDown_ = (event) => {
this.hueSlider_.mouseDown(new Point(event.clientX, event.clientY));
}
/**
* @param {!Color} newColor
*/
set color(newColor) {
this.hueSlider_.color = newColor;
this.colorWell_.selectedColor = newColor;
}
}
window.customElements.define('visual-color-picker', VisualColorPicker);
/**
* EyeDropper: Allows color selection from content outside the color picker.
* (This is currently just a placeholder for a future
* implementation.)
* TODO(http://crbug.com/992297): Implement eye dropper
*/
class EyeDropper extends HTMLElement { }
window.customElements.define('eye-dropper', EyeDropper);
/**
* ColorViewer: Provides a view of the selected color.
*/
class ColorViewer extends HTMLElement {
/**
* @param {!Color} initialColor
*/
constructor(initialColor) {
super();
this.color = initialColor;
}
get color() {
return this.color_;
}
/**
* @param {!Color} color
*/
set color(color) {
if (this.color_ === undefined || !this.color_.equals(color)) {
this.color_ = color;
this.style.backgroundColor = color.asRGB();
}
}
}
window.customElements.define('color-viewer', ColorViewer);
/**
* ColorSelectionArea: Base class for ColorWell and HueSlider that encapsulates
* a ColorPalette and a ColorSelectionRing.
*/
class ColorSelectionArea extends HTMLElement {
constructor() {
super();
this.colorPalette_ = new ColorPalette();
this.colorSelectionRing_ = new ColorSelectionRing(this.colorPalette_);
this.append(this.colorPalette_, this.colorSelectionRing_);
this.initialized_ = false;
}
get initialized() {
return this.initialized_;
}
/**
* @param {!Point} point
*/
mouseDown(point) {
this.colorSelectionRing_.drag = true;
this.moveColorSelectionRingTo_(point);
}
/**
* @param {!Point} point
*/
mouseMove(point) {
if (this.colorSelectionRing_.drag) {
this.moveColorSelectionRingTo_(point);
}
}
mouseUp() {
this.colorSelectionRing_.drag = false;
}
}
window.customElements.define('color-selection-area', ColorSelectionArea);
/**
* ColorPalette: Displays a range of colors.
*/
class ColorPalette extends HTMLCanvasElement {
constructor() {
super();
this.gradients_ = [];
}
/**
* @param {...CanvasGradient} gradients
*/
initialize(...gradients) {
this.width = this.offsetWidth;
this.height = this.offsetHeight;
this.renderingContext.rect(0, 0, this.width, this.height);
this.gradients_.push(...gradients);
this.fillColor = new Color('hsl(0, 100%, 50%)');
}
get hslImageData() {
if (this.pendingColorChange_) {
const rgbaImageData = this.renderingContext
.getImageData(0, 0, this.width, this.height).data;
this.hslImageData_ = rgbaImageData
.reduce((hslArray, {}, currentIndex, rgbaArray) => {
if ((currentIndex % 4) === 0) {
hslArray.push(...Color.rgbToHSL(rgbaArray[currentIndex],
rgbaArray[currentIndex + 1],
rgbaArray[currentIndex + 2]));
}
return hslArray;
}, []);
this.pendingColorChange_ = false;
}
if (this.pendingHueChange_) {
const hValueToSet = this.fillColor.hValue;
this.hslImageData_.forEach(({}, currentIndex, hslArray) => {
if ((currentIndex % 3) === 0) {
hslArray[currentIndex] = hValueToSet;
}
});
this.pendingHueChange_ = false;
}
return this.hslImageData_;
}
/**
* @param {!Point} point
*/
colorAtPoint(point) {
const hslImageDataAtPoint =
this.hslImageDataAtPoint_(point.x - this.left, point.y - this.top);
return new Color(ColorFormat.HSL, hslImageDataAtPoint[0],
hslImageDataAtPoint[1], hslImageDataAtPoint[2]);
}
/**
* @param {number} x
* @param {number} y
*/
hslImageDataAtPoint_(x, y) {
const offset = Math.round(y * this.width + x) * 3;
return this.hslImageData.slice(offset, offset + 3);
}
get renderingContext() {
return this.getContext('2d');
}
get fillColor() {
return this.fillColor_;
}
/**
* @param {!Color} color
*/
set fillColor(color) {
this.fillColor_ = color;
this.fillColorAndGradients_();
this.pendingColorChange_ = true;
}
/**
* @param {!Color} color
*/
fillHue(color) {
this.fillColor_ = new Color(ColorFormat.HSL, color.hValue,
this.fillColor_.sValue, this.fillColor_.lValue);
this.fillColorAndGradients_();
this.pendingHueChange_ = true;
}
fillColorAndGradients_() {
this.fillWithStyle_(this.fillColor_.asRGB());
this.gradients_.forEach((gradient) => this.fillWithStyle_(gradient));
}
/**
* @param {string|!CanvasGradient} fillStyle
*/
fillWithStyle_(fillStyle) {
let colorPaletteCtx = this.renderingContext;
colorPaletteCtx.fillStyle = fillStyle;
colorPaletteCtx.fill();
}
/**
* @param {!Point} point
*/
nearestPointOnColorPalette(point) {
if (!this.isXCoordinateOnColorPalette_(point)) {
if (point.x >= this.right) {
point.x = this.right - 1;
} else if (point.x < this.left) {
point.x = this.left;
}
}
if (!this.isYCoordinateOnColorPalette_(point)) {
if (point.y >= this.bottom) {
point.y = this.bottom - 1;
} else if (point.y < this.top) {
point.y = this.top;
}
}
return point;
}
/**
* @param {!Point} point
*/
isXCoordinateOnColorPalette_(point) {
return (point.x >= this.left) && (point.x < this.right);
}
/**
* @param {!Point} point
*/
isYCoordinateOnColorPalette_(point) {
return (point.y >= this.top) && (point.y < this.bottom);
}
get left() {
return this.getBoundingClientRect().left;
}
get right() {
return this.getBoundingClientRect().right;
}
get top() {
return this.getBoundingClientRect().top;
}
get bottom() {
return this.getBoundingClientRect().bottom;
}
}
window.customElements.define('color-palette',
ColorPalette,
{ extends: 'canvas' });
/**
* ColorSelectionRing: Provides movement and color selection functionality to
* pick colors from a given ColorPalette.
*/
class ColorSelectionRing extends HTMLElement {
/**
* @param {!ColorPalette} backingColorPalette
*/
constructor(backingColorPalette) {
super();
this.backingColorPalette_ = backingColorPalette;
this.position_ = new Point(0, 0);
this.drag_ = false;
}
initialize() {
this.set(this.backingColorPalette_.left, this.backingColorPalette_.top);
}
/**
* @param {!Point} newPosition
*/
moveTo(newPosition) {
this.set(newPosition.x, newPosition.y);
}
/**
* @param {number} x
* @param {number} y
*/
set(x, y) {
if ((x !== this.position_.x) || (y !== this.position_.y)) {
this.position_.x = x;
this.position_.y = y;
this.onPositionChange_();
}
}
/**
* @param {number} x
*/
setX(x) {
if (x !== this.position_.x) {
this.position_.x = x;
this.onPositionChange_();
}
}
/**
* @param {number} shiftFactor
*/
shiftX(shiftFactor) {
this.setX(this.position_.x + shiftFactor);
}
onPositionChange_() {
this.setElementPosition_();
this.updateColor();
}
setElementPosition_() {
if (this.height > this.backingColorPalette_.height) {
this.style.top = this.top
- (this.height - this.backingColorPalette_.height) / 2
- this.backingColorPalette_.top + 'px';
} else {
this.style.top = this.top - this.radius
- this.backingColorPalette_.top + 'px';
}
if (this.width > this.backingColorPalette_.width) {
this.style.left = this.left
- (this.width - this.backingColorPalette_.width) / 2
- this.backingColorPalette_.left + 'px';
} else {
this.style.left = this.left - this.radius
- this.backingColorPalette_.left + 'px';
}
}
updateColor() {
this.color = this.backingColorPalette_.colorAtPoint(this.position_);
this.dispatchEvent(new CustomEvent('color-selection-ring-update'));
}
get color() {
return this.color_;
}
/**
* @param {!Color} color
*/
set color(color) {
if (this.color_ === undefined || !this.color_.equals(color)) {
this.color_ = color;
this.style.backgroundColor = color.asRGB();
}
}
get drag() {
return this.drag_;
}
/**
* @param {boolean} drag
*/
set drag(drag) {
this.drag_ = drag;
}
get radius() {
return this.width / 2;
}
get width() {
return this.getBoundingClientRect().width;
}
get height() {
return this.getBoundingClientRect().height;
}
get left() {
return this.position_.x;
}
get top() {
return this.position_.y;
}
}
window.customElements.define('color-selection-ring', ColorSelectionRing);
/**
* ColorWell: Allows selection from a range of colors, between black and white,
* that have the same hue value.
*/
class ColorWell extends ColorSelectionArea {
/**
* @param {!Color} initialColor
*/
constructor(initialColor) {
super();
this.fillColor_ = new Color(ColorFormat.HSL, initialColor.hValue, 100, 50);
this.selectedColor_ = initialColor;
this.resizeObserver_ = new ResizeObserver(() => {
let whiteGradient = this.colorPalette_.renderingContext
.createLinearGradient(0, 0, this.colorPalette_.offsetWidth, 0);
whiteGradient.addColorStop(0.01, 'hsla(0, 0%, 100%, 1)');
whiteGradient.addColorStop(0.99, 'hsla(0, 0%, 100%, 0)');
let blackGradient = this.colorPalette_.renderingContext
.createLinearGradient(0, this.colorPalette_.offsetHeight, 0, 0);
blackGradient.addColorStop(0.01, 'hsla(0, 0%, 0%, 1)');
blackGradient.addColorStop(0.99, 'hsla(0, 0%, 0%, 0)');
this.colorPalette_.initialize(whiteGradient, blackGradient);
this.colorPalette_.fillHue(this.fillColor_);
this.colorSelectionRing_.initialize();
this.colorSelectionRing_.addEventListener('color-selection-ring-update',
this.onColorSelectionRingUpdate_);
this.moveColorSelectionRingTo_(this.selectedColor_);
this.resizeObserver_.disconnect();
this.resizeObserver_ = null;
this.initialized_ = true;
this.dispatchEvent(new CustomEvent('color-well-initialized'));
});
this.resizeObserver_.observe(this);
}
/**
* @param {!Point|!Color} newPositionOrColor
*/
moveColorSelectionRingTo_(newPositionOrColor) {
if (newPositionOrColor instanceof Point) {
const point =
this.colorPalette_.nearestPointOnColorPalette(newPositionOrColor);
this.colorSelectionRing_.moveTo(point);
} else {
const closestHSLValueIndex = this.colorPalette_.hslImageData
.reduce((closestSoFar, {}, index, array) => {
if ((index % 3) === 0) {
const currentHSLValueDistance = Color.distance([array[index],
array[index + 1], array[index + 2]],
newPositionOrColor.hslValues());
const closestHSLValueDistance =
Color.distance([array[closestSoFar], array[closestSoFar + 1],
array[closestSoFar + 2]], newPositionOrColor.hslValues());
if (currentHSLValueDistance < closestHSLValueDistance) {
return index;
}
}
return closestSoFar;
}, 0);
const offsetX = (closestHSLValueIndex / 3) % this.colorPalette_.width;
const offsetY =
Math.floor((closestHSLValueIndex / 3) / this.colorPalette_.width);
this.colorSelectionRing_.set(this.colorPalette_.left + offsetX,
this.colorPalette_.top + offsetY);
}
}
get selectedColor() {
return this.selectedColor_;
}
/**
* @param {!Color} newColor
*/
set selectedColor(newColor) {
if (!this.selectedColor_.equals(newColor)) {
this.selectedColor_ = newColor;
this.moveColorSelectionRingTo_(newColor);
}
}
get fillColor() {
return this.fillColor_;
}
/**
* @param {!Color} color
*/
set fillColor(color) {
if (!this.fillColor_.equals(color)) {
this.fillColor_ = color;
this.colorPalette_.fillHue(color);
this.colorSelectionRing_.updateColor();
}
}
onColorSelectionRingUpdate_ = () => {
this.selectedColor_ = this.colorSelectionRing_.color;
this.dispatchEvent(new CustomEvent('visual-color-change', {
bubbles: true,
detail: {
color: this.selectedColor
}
}));
}
}
window.customElements.define('color-well', ColorWell);
/**
* HueSlider: Allows selection from a range of colors with distinct hue values.
*/
class HueSlider extends ColorSelectionArea {
/**
* @param {!Color} initialColor
*/
constructor(initialColor) {
super();
this.color_ = new Color(ColorFormat.HSL, initialColor.hValue, 100, 50);
this.resizeObserver_ = new ResizeObserver(() => {
let hueSliderPaletteGradient = this.colorPalette_.renderingContext
.createLinearGradient(0, 0, this.colorPalette_.offsetWidth, 0);
hueSliderPaletteGradient.addColorStop(0.01, 'hsl(0, 100%, 50%)');
hueSliderPaletteGradient.addColorStop(0.17, 'hsl(300, 100%, 50%)');
hueSliderPaletteGradient.addColorStop(0.33, 'hsl(240, 100%, 50%)');
hueSliderPaletteGradient.addColorStop(0.5, 'hsl(180, 100%, 50%)');
hueSliderPaletteGradient.addColorStop(0.67, 'hsl(120, 100%, 50%)');
hueSliderPaletteGradient.addColorStop(0.83, 'hsl(60, 100%, 50%)');
hueSliderPaletteGradient.addColorStop(0.99, 'hsl(0, 100%, 50%)');
this.colorPalette_.initialize(hueSliderPaletteGradient);
this.colorSelectionRing_.initialize();
this.colorSelectionRing_.addEventListener('color-selection-ring-update',
this.onColorSelectionRingUpdate_);
this.moveColorSelectionRingTo_(this.color_);
this.resizeObserver_.disconnect();
this.resizeObserver_ = null;
this.initialized_ = true;
this.dispatchEvent(new CustomEvent('hue-slider-initialized'));
});
this.resizeObserver_.observe(this);
}
/**
* @param {!Point|!Color} newPositionOrColor
*/
moveColorSelectionRingTo_(newPositionOrColor) {
if (newPositionOrColor instanceof Point) {
const point =
this.colorPalette_.nearestPointOnColorPalette(newPositionOrColor);
this.colorSelectionRing_.shiftX(point.x - this.colorSelectionRing_.left);
} else {
const targetHValue = newPositionOrColor.hValue;
if (targetHValue !== this.colorSelectionRing_.color.hValue) {
const closestHValueIndex = this.colorPalette_.hslImageData
.reduce((closestHValueIndexSoFar, currentHValue, index, array) => {
if ((index % 3 === 0) &&
(Math.abs(currentHValue - targetHValue) <
Math.abs(array[closestHValueIndexSoFar] - targetHValue))) {
return index;
}
return closestHValueIndexSoFar;
}, 0);
const offsetX = (closestHValueIndex / 3) % this.colorPalette_.width;
this.colorSelectionRing_.setX(this.colorPalette_.left + offsetX);
}
}
}
get color() {
return this.color_;
}
/**
* @param {!Color} newColor
*/
set color(newColor) {
if (this.color_.hValue !== newColor.hValue) {
this.color_ = new Color(ColorFormat.HSL, newColor.hValue, 100, 50);
this.moveColorSelectionRingTo_(this.color_);
}
}
onColorSelectionRingUpdate_ = () => {
this.color_ = this.colorSelectionRing_.color;
this.dispatchEvent(new CustomEvent('hue-slider-update', {
bubbles: true
}));
}
}
window.customElements.define('hue-slider', HueSlider);
/**
* ManualColorPicker: Provides functionality to change the selected color by
* manipulating its numeric values.
*/
class ManualColorPicker extends HTMLElement {
/**
* @param {!Color} initialColor
*/
constructor(initialColor) {
super();
this.hexValueContainer_ = new ColorValueContainer(ColorChannel.HEX,
initialColor);
this.rgbValueContainer_ = new ColorValueContainer(ColorFormat.RGB,
initialColor);
this.hslValueContainer_ = new ColorValueContainer(ColorFormat.HSL,
initialColor);
this.colorValueContainers_ = [
this.hexValueContainer_,
this.rgbValueContainer_,
this.hslValueContainer_,
];
this.currentColorFormat_ = ColorFormat.RGB;
this.adjustValueContainerVisibility_();
this.formatToggler_ = new FormatToggler(this.currentColorFormat_);
this.append(...this.colorValueContainers_, this.formatToggler_);
this.formatToggler_
.addEventListener('format-change', this.onFormatChange_);
this.addEventListener('manual-color-change', this.onManualColorChange_);
}
adjustValueContainerVisibility_() {
this.colorValueContainers_.forEach((colorValueContainer) => {
if (colorValueContainer.colorFormat === this.currentColorFormat_) {
colorValueContainer.show();
} else {
colorValueContainer.hide();
}
});
}
/**
* @param {!Event} event
*/
onFormatChange_ = (event) => {
this.currentColorFormat_ = event.detail.colorFormat;
this.adjustValueContainerVisibility_();
}
/**
* @param {!Event} event
*/
onManualColorChange_ = (event) => {
this.color = event.detail.color;
}
/**
* @param {!Color} newColor
*/
set color(newColor) {
this.colorValueContainers_.forEach((colorValueContainer) =>
colorValueContainer.color = newColor);
}
}
window.customElements.define('manual-color-picker', ManualColorPicker);
/**
* ColorValueContainer: Maintains a set of channel values that make up a given
* color format, and tracks value changes.
*/
class ColorValueContainer extends HTMLElement {
/**
* @param {!ColorFormat} colorFormat
* @param {!Color} initialColor
*/
constructor(colorFormat, initialColor) {
super();
this.colorFormat_ = colorFormat;
this.channelValueContainers_ = [];
if (this.colorFormat_ === ColorFormat.HEX) {
const hexValueContainer = new ChannelValueContainer(ColorChannel.HEX,
initialColor);
this.channelValueContainers_.push(hexValueContainer);
} else if (this.colorFormat_ === ColorFormat.RGB) {
const rValueContainer = new ChannelValueContainer(ColorChannel.R,
initialColor);
const gValueContainer = new ChannelValueContainer(ColorChannel.G,
initialColor);
const bValueContainer = new ChannelValueContainer(ColorChannel.B,
initialColor);
this.channelValueContainers_.push(rValueContainer,
gValueContainer,
bValueContainer);
} else if (this.colorFormat_ === ColorFormat.HSL) {
const hValueContainer = new ChannelValueContainer(ColorChannel.H,
initialColor);
const sValueContainer = new ChannelValueContainer(ColorChannel.S,
initialColor);
const lValueContainer = new ChannelValueContainer(ColorChannel.L,
initialColor);
this.channelValueContainers_.push(hValueContainer,
sValueContainer,
lValueContainer);
}
this.append(...this.channelValueContainers_);
this.channelValueContainers_.forEach((channelValueContainer) =>
channelValueContainer.addEventListener('input',
this.onChannelValueChange_));
}
get colorFormat() {
return this.colorFormat_;
}
get color() {
return new Color(this.colorFormat_,
...this.channelValueContainers_.map((channelValueContainer) =>
channelValueContainer.channelValue));
}
/**
* @param {!Color} color
*/
set color(color) {
this.channelValueContainers_.forEach((channelValueContainer) =>
channelValueContainer.setValue(color));
}
show() {
return this.classList.remove('hidden-color-value-container');
}
hide() {
return this.classList.add('hidden-color-value-container');
}
onChannelValueChange_ = () => {
this.dispatchEvent(new CustomEvent('manual-color-change', {
bubbles: true,
detail: {
color: this.color
}
}));
}
}
window.customElements.define('color-value-container', ColorValueContainer);
/**
* ChannelValueContainer: Maintains and displays the numeric value
* for a given color channel.
*/
class ChannelValueContainer extends HTMLInputElement {
/**
* @param {!ColorChannel} colorChannel
* @param {!Color} initialColor
*/
constructor(colorChannel, initialColor) {
super();
this.setAttribute('type', 'text');
this.colorChannel_ = colorChannel;
switch(colorChannel) {
case ColorChannel.HEX:
this.setAttribute('id', 'hexValueContainer');
this.setAttribute('maxlength', '7');
break;
case ColorChannel.R:
this.setAttribute('id', 'rValueContainer');
this.setAttribute('maxlength', '3');
break;
case ColorChannel.G:
this.setAttribute('id', 'gValueContainer');
this.setAttribute('maxlength', '3');
break;
case ColorChannel.B:
this.setAttribute('id', 'bValueContainer');
this.setAttribute('maxlength', '3');
break;
case ColorChannel.H:
this.setAttribute('id', 'hValueContainer');
this.setAttribute('maxlength', '3');
break;
case ColorChannel.S:
// up to 3 digits plus '%'
this.setAttribute('id', 'sValueContainer');
this.setAttribute('maxlength', '4');
break;
case ColorChannel.L:
// up to 3 digits plus '%'
this.setAttribute('id', 'lValueContainer');
this.setAttribute('maxlength', '4');
break;
}
this.setValue(initialColor);
this.addEventListener('input', this.onValueChange_);
}
get channelValue() {
return this.channelValue_;
}
/**
* @param {!Color} color
*/
setValue(color) {
switch(this.colorChannel_) {
case ColorChannel.HEX:
if (this.channelValue_ !== color.hexValue) {
this.channelValue_ = color.hexValue;
this.value = '#' + this.channelValue_;
}
break;
case ColorChannel.R:
if (this.channelValue_ !== color.rValue) {
this.channelValue_ = color.rValue;
this.value = this.channelValue_;
}
break;
case ColorChannel.G:
if (this.channelValue_ !== color.gValue) {
this.channelValue_ = color.gValue;
this.value = this.channelValue_;
}
break;
case ColorChannel.B:
if (this.channelValue_ !== color.bValue) {
this.channelValue_ = color.bValue;
this.value = this.channelValue_;
}
break;
case ColorChannel.H:
if (this.channelValue_ !== color.hValue) {
this.channelValue_ = color.hValue;
this.value = this.channelValue_;
}
break;
case ColorChannel.S:
if (this.channelValue_ !== color.sValue) {
this.channelValue_ = color.sValue;
this.value = this.channelValue_ + '%';
}
break;
case ColorChannel.L:
if (this.channelValue_ !== color.lValue) {
this.channelValue_ = color.lValue;
this.value = this.channelValue_ + '%';
}
break;
}
}
onValueChange_ = () => {
// Set this.channelValue_ based on the element's new value.
let value = this.value;
if (value) {
switch(this.colorChannel_) {
case ColorChannel.HEX:
if (value.startsWith('#')) {
value = value.substr(1).toLowerCase();
if (value.match(/^[0-9a-f]+$/)) {
// Ex. 'ffffff' => this.channelValue_ == 'ffffff'
// Ex. 'ff' => this.channelValue_ == '0000ff'
this.channelValue_ = ('000000' + value).slice(-6);
}
}
break;
case ColorChannel.R:
case ColorChannel.G:
case ColorChannel.B:
if (value.match(/^\d+$/) && (0 <= value) && (value <= 255)) {
this.channelValue_ = Number(value);
}
break;
case ColorChannel.H:
if (value.match(/^\d+$/) && (0 <= value) && (value < 360)) {
this.channelValue_ = Number(value);
}
break;
case ColorChannel.S:
case ColorChannel.L:
if (value.endsWith('%')) {
value = value.substring(0, value.length - 1);
if (value.match(/^\d+$/) && (0 <= value) && (value <= 100)) {
this.channelValue_ = Number(value);
}
}
break;
}
}
}
}
window.customElements.define('channel-value-container',
ChannelValueContainer,
{ extends: 'input' });
/**
* FormatToggler: Button that powers switching between different color formats.
*/
class FormatToggler extends HTMLElement {
/**
* @param {!ColorFormat} initialColorFormat
*/
constructor(initialColorFormat) {
super();
this.currentColorFormat_ = initialColorFormat;
this.hexFormatLabel_ = new FormatLabel(ColorFormat.HEX);
this.rgbFormatLabel_ = new FormatLabel(ColorFormat.RGB);
this.hslFormatLabel_ = new FormatLabel(ColorFormat.HSL);
this.colorFormatLabels_ = [
this.hexFormatLabel_,
this.rgbFormatLabel_,
this.hslFormatLabel_,
];
this.adjustFormatLabelVisibility_();
this.upDownIcon_ = document.createElement('span');
this.upDownIcon_.innerHTML =
'<svg width="6" height="8" viewBox="0 0 6 8" fill="none" ' +
'xmlns="http://www.w3.org/2000/svg"><path d="M1.18359 ' +
'3.18359L0.617188 2.61719L3 0.234375L5.38281 2.61719L4.81641 ' +
'3.18359L3 1.36719L1.18359 3.18359ZM4.81641 4.81641L5.38281 ' +
'5.38281L3 7.76562L0.617188 5.38281L1.18359 4.81641L3 ' +
'6.63281L4.81641 4.81641Z" fill="black"/></svg>';
this.append(...this.colorFormatLabels_, this.upDownIcon_);
this.addEventListener('click', this.onClick_);
this.addEventListener('mousedown', (event) => event.preventDefault());
}
adjustFormatLabelVisibility_() {
this.colorFormatLabels_.forEach((colorFormatLabel) => {
if (colorFormatLabel.colorFormat === this.currentColorFormat_) {
colorFormatLabel.show();
} else {
colorFormatLabel.hide();
}
});
}
onClick_ = () => {
if (this.currentColorFormat_ == ColorFormat.HEX) {
this.currentColorFormat_ = ColorFormat.RGB;
} else if (this.currentColorFormat_ == ColorFormat.RGB) {
this.currentColorFormat_ = ColorFormat.HSL;
} else if (this.currentColorFormat_ == ColorFormat.HSL) {
this.currentColorFormat_ = ColorFormat.HEX;
}
this.adjustFormatLabelVisibility_();
this.dispatchEvent(new CustomEvent('format-change', {
detail: {
colorFormat: this.currentColorFormat_
}
}));
}
}
window.customElements.define('format-toggler', FormatToggler);
/**
* FormatLabel: Label for a given color format.
*/
class FormatLabel extends HTMLElement {
/**
* @param {!ColorFormat} colorFormat
*/
constructor(colorFormat) {
super();
this.colorFormat_ = colorFormat;
if (colorFormat === ColorFormat.HEX) {
this.hexChannelLabel_ = new ChannelLabel(ColorChannel.HEX);
this.append(this.hexChannelLabel_);
} else if (colorFormat === ColorFormat.RGB) {
this.rChannelLabel_ = new ChannelLabel(ColorChannel.R);
this.gChannelLabel_ = new ChannelLabel(ColorChannel.G);
this.bChannelLabel_ = new ChannelLabel(ColorChannel.B);
this.append(this.rChannelLabel_,
this.gChannelLabel_,
this.bChannelLabel_);
} else if (colorFormat === ColorFormat.HSL) {
this.hChannelLabel_ = new ChannelLabel(ColorChannel.H);
this.sChannelLabel_ = new ChannelLabel(ColorChannel.S);
this.lChannelLabel_ = new ChannelLabel(ColorChannel.L);
this.append(this.hChannelLabel_,
this.sChannelLabel_,
this.lChannelLabel_);
}
}
get colorFormat() {
return this.colorFormat_;
}
show() {
return this.classList.remove('hidden-format-label');
}
hide() {
return this.classList.add('hidden-format-label');
}
}
window.customElements.define('format-label', FormatLabel);
/**
* ChannelLabel: Label for a color channel, to be used within a FormatLabel.
*/
class ChannelLabel extends HTMLElement {
/**
* @param {!ColorChannel} colorChannel
*/
constructor(colorChannel) {
super();
if (colorChannel === ColorChannel.HEX) {
this.textContent = 'HEX';
} else if (colorChannel === ColorChannel.R) {
this.textContent = 'R';
} else if (colorChannel === ColorChannel.G) {
this.textContent = 'G';
} else if (colorChannel === ColorChannel.B) {
this.textContent = 'B';
} else if (colorChannel === ColorChannel.H) {
this.textContent = 'H';
} else if (colorChannel === ColorChannel.S) {
this.textContent = 'S';
} else if (colorChannel === ColorChannel.L) {
this.textContent = 'L';
}
}
}
window.customElements.define('channel-label', ChannelLabel);
/**
* SubmissionControls: Provides functionality to submit or discard a change.
*/
class SubmissionControls extends HTMLElement {
/**
* @param {function} submitCallback executed if the submit button is clicked
* @param {function} cancelCallback executed if the cancel button is clicked
*/
constructor(submitCallback, cancelCallback) {
super();
const padding = document.createElement('span');
padding.setAttribute('id', 'submission-controls-padding');
this.append(padding);
this.submitButton_ = new SubmissionButton(submitCallback,
'<svg width="14" height="10" viewBox="0 0 14 10" fill="none" ' +
'xmlns="http://www.w3.org/2000/svg"><path d="M13.3516 ' +
'1.35156L5 9.71094L0.648438 5.35156L1.35156 4.64844L5 ' +
'8.28906L12.6484 0.648438L13.3516 1.35156Z" fill="black"/></svg>'
);
this.cancelButton_ = new SubmissionButton(cancelCallback,
'<svg width="14" height="14" viewBox="0 0 14 14" fill="none" ' +
'xmlns="http://www.w3.org/2000/svg"><path d="M7.71094 7L13.1016 ' +
'12.3984L12.3984 13.1016L7 7.71094L1.60156 13.1016L0.898438 ' +
'12.3984L6.28906 7L0.898438 1.60156L1.60156 0.898438L7 ' +
'6.28906L12.3984 0.898438L13.1016 1.60156L7.71094 7Z" ' +
'fill="black"/></svg>'
);
this.append(this.submitButton_, this.cancelButton_);
}
}
window.customElements.define('submission-controls', SubmissionControls);
/**
* SubmissionButton: Button with a custom look that can be clicked for
* a submission action.
*/
class SubmissionButton extends HTMLElement {
/**
* @param {function} clickCallback executed when the button is clicked
* @param {string} htmlString custom look for the button
*/
constructor(clickCallback, htmlString) {
super();
this.setAttribute('tabIndex', '0');
this.innerHTML = htmlString;
this.addEventListener('click', clickCallback);
}
}
window.customElements.define('submission-button', SubmissionButton);