| /* |
| * Copyright (C) 2019 Apple Inc. All rights reserved. |
| * |
| * Redistribution and use in source and binary forms, with or without |
| * modification, are permitted provided that the following conditions |
| * are met: |
| * 1. Redistributions of source code must retain the above copyright |
| * notice, this list of conditions and the following disclaimer. |
| * 2. Redistributions in binary form must reproduce the above copyright |
| * notice, this list of conditions and the following disclaimer in the |
| * documentation and/or other materials provided with the distribution. |
| * |
| * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' |
| * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, |
| * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
| * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS |
| * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR |
| * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF |
| * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS |
| * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN |
| * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) |
| * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF |
| * THE POSSIBILITY OF SUCH DAMAGE. |
| */ |
| |
| WI.ColorSquare = class ColorSquare |
| { |
| constructor(delegate, dimension) |
| { |
| this._delegate = delegate; |
| |
| this._hue = 0; |
| this._x = 0; |
| this._y = 0; |
| this._gamut = null; |
| this._crosshairPosition = null; |
| |
| this._element = document.createElement("div"); |
| this._element.className = "color-square"; |
| this._element.tabIndex = 0; |
| |
| let saturationGradientElement = this._element.appendChild(document.createElement("div")); |
| saturationGradientElement.className = "saturation-gradient fill"; |
| |
| let lightnessGradientElement = this._element.appendChild(document.createElement("div")); |
| lightnessGradientElement.className = "lightness-gradient fill"; |
| |
| this._srgbLabelElement = null; |
| this._svgElement = null; |
| this._polylineElement = null; |
| |
| this._element.addEventListener("mousedown", this); |
| this._element.addEventListener("keydown", this._handleKeyDown.bind(this)); |
| |
| this._crosshairElement = this._element.appendChild(document.createElement("div")); |
| this._crosshairElement.className = "crosshair"; |
| |
| this.dimension = dimension; |
| } |
| |
| // Public |
| |
| get element() { return this._element; } |
| |
| set dimension(dimension) |
| { |
| console.assert(!isNaN(dimension)); |
| if (dimension === this._dimension) |
| return; |
| |
| this._dimension = dimension; |
| this._element.style.width = this.element.style.height = `${this._dimension}px`; |
| this._updateBaseColor(); |
| } |
| |
| get hue() |
| { |
| return this._hue; |
| } |
| |
| set hue(hue) |
| { |
| this._hue = hue; |
| this._updateBaseColor(); |
| } |
| |
| get tintedColor() |
| { |
| if (this._crosshairPosition) { |
| if (this._gamut === WI.Color.Gamut.DisplayP3) { |
| let rgb = WI.Color.hsv2rgb(this._hue, this._saturation, this._brightness); |
| rgb = rgb.map(((x) => Math.roundTo(x, 0.001))); |
| return new WI.Color(WI.Color.Format.ColorFunction, rgb, this._gamut); |
| } |
| |
| let hsl = WI.Color.hsv2hsl(this._hue, this._saturation, this._brightness); |
| return new WI.Color(WI.Color.Format.HSL, hsl); |
| } |
| |
| return new WI.Color(WI.Color.Format.HSLA, [0, 0, 0, 0]); |
| } |
| |
| set tintedColor(tintedColor) |
| { |
| console.assert(tintedColor instanceof WI.Color); |
| |
| this._gamut = tintedColor.gamut; |
| |
| let [hue, saturation, value] = WI.Color.rgb2hsv(...tintedColor.normalizedRGB); |
| let x = saturation / 100 * this._dimension; |
| let y = (1 - (value / 100)) * this._dimension; |
| |
| if (this._gamut === WI.Color.Gamut.DisplayP3) |
| this._drawSRGBOutline(); |
| |
| this._setCrosshairPosition(new WI.Point(x, y)); |
| this._updateBaseColor(); |
| } |
| |
| // Protected |
| |
| handleEvent(event) |
| { |
| switch (event.type) { |
| case "mousedown": |
| this._handleMousedown(event); |
| break; |
| case "mousemove": |
| this._handleMousemove(event); |
| break; |
| case "mouseup": |
| this._handleMouseup(event); |
| break; |
| } |
| } |
| |
| // Private |
| |
| get _saturation() |
| { |
| let saturation = this._x / this._dimension; |
| return Number.constrain(saturation, 0, 1) * 100; |
| } |
| |
| get _brightness() |
| { |
| let brightness = 1 - (this._y / this._dimension); |
| return Number.constrain(brightness, 0, 1) * 100; |
| } |
| |
| _handleMousedown(event) |
| { |
| if (event.button !== 0 || event.ctrlKey) |
| return; |
| |
| window.addEventListener("mousemove", this, true); |
| window.addEventListener("mouseup", this, true); |
| this._updateColorForMouseEvent(event); |
| |
| // Prevent text selection. |
| event.stop(); |
| this._element.focus(); |
| } |
| |
| _handleMousemove(event) |
| { |
| this._updateColorForMouseEvent(event); |
| } |
| |
| _handleMouseup(event) |
| { |
| window.removeEventListener("mousemove", this, true); |
| window.removeEventListener("mouseup", this, true); |
| } |
| |
| _handleKeyDown(event) |
| { |
| let dx = 0; |
| let dy = 0; |
| let step = event.shiftKey ? 10 : 1; |
| |
| switch (event.keyIdentifier) { |
| case "Right": |
| dx += step; |
| break; |
| case "Left": |
| dx -= step; |
| break; |
| case "Down": |
| dy += step; |
| break; |
| case "Up": |
| dy -= step; |
| break; |
| } |
| |
| if (dx || dy) { |
| event.preventDefault(); |
| this._setCrosshairPosition(new WI.Point(this._x + dx, this._y + dy)); |
| |
| if (this._delegate && this._delegate.colorSquareColorDidChange) |
| this._delegate.colorSquareColorDidChange(this); |
| } |
| } |
| |
| _updateColorForMouseEvent(event) |
| { |
| let point = window.webkitConvertPointFromPageToNode(this._element, new WebKitPoint(event.pageX, event.pageY)); |
| this._setCrosshairPosition(point); |
| |
| if (this._delegate && this._delegate.colorSquareColorDidChange) |
| this._delegate.colorSquareColorDidChange(this); |
| } |
| |
| _setCrosshairPosition(point) |
| { |
| this._crosshairPosition = point; |
| this._x = Number.constrain(Math.round(point.x), 0, this._dimension); |
| this._y = Number.constrain(Math.round(point.y), 0, this._dimension); |
| this._crosshairElement.style.setProperty("transform", `translate(${this._x}px, ${this._y}px)`); |
| |
| this._updateCrosshairBackground(); |
| } |
| |
| _updateBaseColor() |
| { |
| if (this._gamut === WI.Color.Gamut.DisplayP3) { |
| let [r, g, b] = WI.Color.hsl2rgb(this._hue, 100, 50); |
| this._element.style.backgroundColor = `color(display-p3 ${r / 255} ${g / 255} ${b / 255})`; |
| } else |
| this._element.style.backgroundColor = `hsl(${this._hue}, 100%, 50%)`; |
| |
| this._updateCrosshairBackground(); |
| |
| if (this._gamut === WI.Color.Gamut.DisplayP3) |
| this._drawSRGBOutline(); |
| } |
| |
| _updateCrosshairBackground() |
| { |
| this._crosshairElement.style.backgroundColor = this.tintedColor.toString(); |
| } |
| |
| _drawSRGBOutline() |
| { |
| if (!this._svgElement) { |
| this._srgbLabelElement = this._element.appendChild(document.createElement("span")); |
| this._srgbLabelElement.className = "srgb-label"; |
| this._srgbLabelElement.textContent = WI.unlocalizedString("sRGB"); |
| this._srgbLabelElement.title = WI.UIString("Edge of sRGB color space", "Label for a guide within the color picker"); |
| |
| const svgNamespace = "http://www.w3.org/2000/svg"; |
| this._svgElement = this._element.appendChild(document.createElementNS(svgNamespace, "svg")); |
| this._svgElement.classList.add("svg-root"); |
| |
| this._polylineElement = this._svgElement.appendChild(document.createElementNS(svgNamespace, "polyline")); |
| this._polylineElement.classList.add("srgb-edge"); |
| } |
| |
| let points = []; |
| let step = 1 / window.devicePixelRatio; |
| let x = 0; |
| for (let y = 0; y < this._dimension; y += step) { |
| let value = 100 - ((y / this._dimension) * 100); |
| |
| // Optimization: instead of starting from x = 0, we can benefit from the fact that the next point |
| // always has x >= of the current x. This minimizes processing time over 100 times. |
| for (; x < this._dimension; x += step) { |
| let saturation = x / this._dimension * 100; |
| let rgb = WI.Color.hsv2rgb(this._hue, saturation, value); |
| let srgb = WI.Color.displayP3toSRGB(rgb[0], rgb[1], rgb[2]); |
| if (srgb.some((value) => value < 0 || value > 1)) { |
| // The point is outside of sRGB. |
| points.push({x, y}); |
| break; |
| } |
| } |
| } |
| |
| if (points.lastValue.y < this._dimension * 0.95) { |
| // For `color(display-p3 0 0 1)`, the line is almost horizontal. |
| // Position the label directly under the line. |
| points.push({x: this._dimension, y: points.lastValue.y}); |
| this._srgbLabelElement.style.removeProperty("bottom"); |
| this._srgbLabelElement.style.top = `${points.lastValue.y}px`; |
| } else { |
| this._srgbLabelElement.style.removeProperty("top"); |
| this._srgbLabelElement.style.bottom = "0px"; |
| } |
| |
| this._srgbLabelElement.style.right = `${this._dimension - points.lastValue.x}px`; |
| |
| this._polylineElement.points.clear(); |
| for (let {x, y} of points) { |
| let svgPoint = this._svgElement.createSVGPoint(); |
| svgPoint.x = x; |
| svgPoint.y = y; |
| this._polylineElement.points.appendItem(svgPoint); |
| } |
| } |
| }; |