| /* |
| * Copyright (C) 2014, 2021 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.Gradient = class Gradient |
| { |
| constructor(type, stops) |
| { |
| this.type = type; |
| this.stops = stops; |
| } |
| |
| // Static |
| |
| static angleFromString(string) |
| { |
| let match = string.match(/([-\d\.]+)(\w+)/); |
| if (!match || !Object.values(WI.Gradient.AngleUnits).includes(match[2])) |
| return null; |
| |
| return {value: parseFloat(match[1]), units: match[2]}; |
| } |
| |
| static fromString(cssString) |
| { |
| var type; |
| var openingParenthesisIndex = cssString.indexOf("("); |
| var typeString = cssString.substring(0, openingParenthesisIndex); |
| if (typeString.includes(WI.Gradient.Types.Linear)) |
| type = WI.Gradient.Types.Linear; |
| else if (typeString.includes(WI.Gradient.Types.Radial)) |
| type = WI.Gradient.Types.Radial; |
| else if (typeString.includes(WI.Gradient.Types.Conic)) |
| type = WI.Gradient.Types.Conic; |
| else |
| return null; |
| |
| var components = []; |
| var currentParams = []; |
| var currentParam = ""; |
| var openParentheses = 0; |
| var ch = openingParenthesisIndex + 1; |
| var c = null; |
| while (c = cssString[ch]) { |
| if (c === "(") |
| openParentheses++; |
| if (c === ")") |
| openParentheses--; |
| |
| var isComma = c === ","; |
| var isSpace = /\s/.test(c); |
| |
| if (openParentheses === 0) { |
| if (isSpace) { |
| if (currentParam !== "") |
| currentParams.push(currentParam); |
| currentParam = ""; |
| } else if (isComma) { |
| currentParams.push(currentParam); |
| components.push(currentParams); |
| currentParams = []; |
| currentParam = ""; |
| } |
| } |
| |
| if (openParentheses === -1) { |
| currentParams.push(currentParam); |
| components.push(currentParams); |
| break; |
| } |
| |
| if (openParentheses > 0 || (!isComma && !isSpace)) |
| currentParam += c; |
| |
| ch++; |
| } |
| |
| if (openParentheses !== -1) |
| return null; |
| |
| let gradient = null; |
| switch (type) { |
| case WI.Gradient.Types.Linear: |
| gradient = WI.LinearGradient.fromComponents(components); |
| break; |
| |
| case WI.Gradient.Types.Radial: |
| gradient = WI.RadialGradient.fromComponents(components); |
| break; |
| |
| case WI.Gradient.Types.Conic: |
| gradient = WI.ConicGradient.fromComponents(components); |
| break; |
| } |
| |
| if (gradient) |
| gradient.repeats = typeString.startsWith("repeating"); |
| |
| return gradient; |
| } |
| |
| static stopsWithComponents(components) |
| { |
| // FIXME: handle lengths. |
| var stops = components.map(function(component) { |
| while (component.length) { |
| var color = WI.Color.fromString(component.shift()); |
| if (!color) |
| continue; |
| |
| var stop = {color}; |
| if (component.length && component[0].substr(-1) === "%") |
| stop.offset = parseFloat(component.shift()) / 100; |
| return stop; |
| } |
| }); |
| |
| if (!stops.length) |
| return null; |
| |
| for (var i = 0, count = stops.length; i < count; ++i) { |
| var stop = stops[i]; |
| |
| // If one of the stops failed to parse, then this is not a valid |
| // set of components for a gradient. So the whole thing is invalid. |
| if (!stop) |
| return null; |
| |
| if (!stop.offset) |
| stop.offset = i / (count - 1); |
| } |
| |
| return stops; |
| } |
| |
| // Public |
| |
| stringFromStops(stops) |
| { |
| var count = stops.length - 1; |
| return stops.map(function(stop, index) { |
| var str = stop.color; |
| if (stop.offset !== index / count) |
| str += " " + Math.round(stop.offset * 10_000) / 100 + "%"; |
| return str; |
| }).join(", "); |
| } |
| |
| // Public |
| |
| get angleValue() |
| { |
| return this._angle.value.maxDecimals(2); |
| } |
| |
| set angleValue(value) |
| { |
| this._angle.value = value; |
| } |
| |
| get angleUnits() |
| { |
| return this._angle.units; |
| } |
| |
| set angleUnits(units) |
| { |
| if (units === this._angle.units) |
| return; |
| |
| this._angle.value = this._angleValueForUnits(units); |
| this._angle.units = units; |
| } |
| |
| copy() |
| { |
| // Implemented by subclasses. |
| } |
| |
| toString() |
| { |
| // Implemented by subclasses. |
| } |
| |
| // Private |
| |
| _angleValueForUnits(units) |
| { |
| if (units === this._angle.units) |
| return this._angle.value; |
| |
| let deg = 0; |
| |
| switch (this._angle.units) { |
| case WI.Gradient.AngleUnits.DEG: |
| deg = this._angle.value; |
| break; |
| |
| case WI.Gradient.AngleUnits.RAD: |
| deg = this._angle.value * 180 / Math.PI; |
| break; |
| |
| case WI.Gradient.AngleUnits.GRAD: |
| deg = this._angle.value / 400 * 360; |
| break; |
| |
| case WI.Gradient.AngleUnits.TURN: |
| deg = this._angle.value * 360; |
| break; |
| } |
| |
| switch (units) { |
| case WI.Gradient.AngleUnits.DEG: |
| return deg; |
| |
| case WI.Gradient.AngleUnits.RAD: |
| return deg * Math.PI / 180; |
| |
| case WI.Gradient.AngleUnits.GRAD: |
| return deg / 360 * 400; |
| |
| case WI.Gradient.AngleUnits.TURN: |
| return deg / 360; |
| } |
| |
| return 0; |
| } |
| }; |
| |
| WI.Gradient.Types = { |
| Linear: "linear-gradient", |
| Radial: "radial-gradient", |
| Conic: "conic-gradient", |
| }; |
| |
| WI.Gradient.AngleUnits = { |
| DEG: "deg", |
| RAD: "rad", |
| GRAD: "grad", |
| TURN: "turn", |
| }; |
| |
| WI.LinearGradient = class LinearGradient extends WI.Gradient |
| { |
| constructor(angle, stops) |
| { |
| super(WI.Gradient.Types.Linear, stops); |
| this._angle = angle; |
| } |
| |
| // Static |
| |
| static fromComponents(components) |
| { |
| let angle = {value: 180, units: WI.Gradient.AngleUnits.DEG}; |
| |
| if (components[0].length === 1 && !WI.Color.fromString(components[0][0])) { |
| angle = WI.Gradient.angleFromString(components[0][0]); |
| |
| if (!angle) |
| return null; |
| |
| components.shift(); |
| } else if (components[0][0] === "to") { |
| components[0].shift(); |
| switch (components[0].sort().join(" ")) { |
| case "top": |
| angle.value = 0; |
| break; |
| case "right top": |
| angle.value = 45; |
| break; |
| case "right": |
| angle.value = 90; |
| break; |
| case "bottom right": |
| angle.value = 135; |
| break; |
| case "bottom": |
| angle.value = 180; |
| break; |
| case "bottom left": |
| angle.value = 225; |
| break; |
| case "left": |
| angle.value = 270; |
| break; |
| case "left top": |
| angle.value = 315; |
| break; |
| default: |
| return null; |
| } |
| |
| components.shift(); |
| } else if (components[0].length !== 1 && !WI.Color.fromString(components[0][0])) { |
| // If the first component is not a color, then we're dealing with a |
| // legacy linear gradient format that we don't support. |
| return null; |
| } |
| |
| let stops = WI.Gradient.stopsWithComponents(components); |
| if (!stops) |
| return null; |
| |
| return new WI.LinearGradient(angle, stops); |
| } |
| |
| copy() |
| { |
| return new WI.LinearGradient(this._angle, this.stops.concat()); |
| } |
| |
| toString() |
| { |
| let str = ""; |
| |
| let deg = this._angleValueForUnits(WI.LinearGradient.AngleUnits.DEG); |
| if (deg === 0) |
| str += "to top"; |
| else if (deg === 45) |
| str += "to top right"; |
| else if (deg === 90) |
| str += "to right"; |
| else if (deg === 135) |
| str += "to bottom right"; |
| else if (deg === 225) |
| str += "to bottom left"; |
| else if (deg === 270) |
| str += "to left"; |
| else if (deg === 315) |
| str += "to top left"; |
| else if (deg !== 180) |
| str += this.angleValue + this.angleUnits; |
| |
| if (str) |
| str += ", "; |
| |
| str += this.stringFromStops(this.stops); |
| |
| return (this.repeats ? "repeating-" : "") + this.type + "(" + str + ")"; |
| } |
| }; |
| |
| WI.RadialGradient = class RadialGradient extends WI.Gradient |
| { |
| constructor(sizing, stops) |
| { |
| super(WI.Gradient.Types.Radial, stops); |
| this.sizing = sizing; |
| } |
| |
| // Static |
| |
| static fromComponents(components) |
| { |
| let sizing = !WI.Color.fromString(components[0].join(" ")) ? components.shift().join(" ") : ""; |
| |
| let stops = WI.Gradient.stopsWithComponents(components); |
| if (!stops) |
| return null; |
| |
| return new WI.RadialGradient(sizing, stops); |
| } |
| |
| // Public |
| |
| get angleValue() |
| { |
| return 0; |
| } |
| |
| set angleValue(value) |
| { |
| console.assert(false, "CSS radial gradients do not have an angle"); |
| } |
| |
| get angleUnits() |
| { |
| return ""; |
| } |
| |
| set angleUnits(units) |
| { |
| console.assert(false, "CSS radial gradients do not have an angle"); |
| } |
| |
| copy() |
| { |
| return new WI.RadialGradient(this.sizing, this.stops.concat()); |
| } |
| |
| toString() |
| { |
| let str = this.sizing; |
| |
| if (str) |
| str += ", "; |
| |
| str += this.stringFromStops(this.stops); |
| |
| return (this.repeats ? "repeating-" : "") + this.type + "(" + str + ")"; |
| } |
| }; |
| |
| WI.ConicGradient = class ConicGradient extends WI.Gradient |
| { |
| constructor(angle, position, stops) |
| { |
| super(WI.Gradient.Types.Conic, stops); |
| |
| this._angle = angle; |
| this._position = position; |
| } |
| |
| // Static |
| |
| static fromComponents(components) |
| { |
| let angle = {value: 0, units: WI.Gradient.AngleUnits.DEG}; |
| let position = null; |
| let hasCustomAngleOrPosition = false; |
| |
| if (components[0][0] == "from") { |
| components[0].shift(); |
| angle = WI.Gradient.angleFromString(components[0][0]); |
| if (!angle) |
| return null; |
| components[0].shift(); |
| hasCustomAngleOrPosition = true; |
| } |
| if (components[0][0] == "at") { |
| components[0].shift(); |
| // FIXME: <https://webkit.org/b/234643> (Web Inspector: allow editing positions in gradient editor) |
| if (components[0].length <= 0) |
| return null; |
| position = components[0].join(" "); |
| hasCustomAngleOrPosition = true; |
| } |
| if (hasCustomAngleOrPosition) |
| components.shift(); |
| |
| let stops = WI.Gradient.stopsWithComponents(components); |
| if (!stops) |
| return null; |
| |
| return new WI.ConicGradient(angle, position, stops); |
| } |
| |
| // Public |
| |
| copy() |
| { |
| return new WI.ConicGradient(this._angle, this._position, this.stops.concat()); |
| } |
| |
| toString() |
| { |
| let str = ""; |
| |
| if (this._angle.value) |
| str += `from ${this._angle.value}${this._angle.units}`; |
| |
| if (this._position) { |
| if (str) |
| str += " "; |
| str += `at ${this._position}`; |
| } |
| |
| if (str) |
| str += ", "; |
| |
| str += this.stringFromStops(this.stops); |
| |
| return (this.repeats ? "repeating-" : "") + this.type + "(" + str + ")"; |
| } |
| }; |