blob: f73de75eecbfdf27708084e09c79673703aa0dd6 [file] [log] [blame]
/*
* Copyright (C) 2014 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 fromString(cssString)
{
var type;
var openingParenthesisIndex = cssString.indexOf("(");
var typeString = cssString.substring(0, openingParenthesisIndex);
if (typeString.indexOf(WI.Gradient.Types.Linear) !== -1)
type = WI.Gradient.Types.Linear;
else if (typeString.indexOf(WI.Gradient.Types.Radial) !== -1)
type = WI.Gradient.Types.Radial;
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;
var gradient;
if (type === WI.Gradient.Types.Linear)
gradient = WI.LinearGradient.fromComponents(components);
else
gradient = WI.RadialGradient.fromComponents(components);
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
copy()
{
// Implemented by subclasses.
}
toString()
{
// Implemented by subclasses.
}
};
WI.Gradient.Types = {
Linear: "linear-gradient",
Radial: "radial-gradient"
};
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.LinearGradient.AngleUnits.DEG};
if (components[0].length === 1 && !WI.Color.fromString(components[0][0])) {
let match = components[0][0].match(/([-\d\.]+)(\w+)/);
if (!match || !Object.values(WI.LinearGradient.AngleUnits).includes(match[2]))
return null;
angle.value = parseFloat(match[1]);
angle.units = match[2];
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;
}
var stops = WI.Gradient.stopsWithComponents(components);
if (!stops)
return null;
return new WI.LinearGradient(angle, stops);
}
// Public
set angleValue(value) { this._angle.value = value; }
get angleValue()
{
return this._angle.value.maxDecimals(2);
}
set angleUnits(units)
{
if (units === this._angle.units)
return;
this._angle.value = this._angleValueForUnits(units);
this._angle.units = units;
}
get angleUnits() { return this._angle.units; }
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 + ")";
}
// Private
_angleValueForUnits(units)
{
if (units === this._angle.units)
return this._angle.value;
let deg = 0;
switch (this._angle.units) {
case WI.LinearGradient.AngleUnits.DEG:
deg = this._angle.value;
break;
case WI.LinearGradient.AngleUnits.RAD:
deg = this._angle.value * 180 / Math.PI;
break;
case WI.LinearGradient.AngleUnits.GRAD:
deg = this._angle.value / 400 * 360;
break;
case WI.LinearGradient.AngleUnits.TURN:
deg = this._angle.value * 360;
break;
default:
WI.reportInternalError(`Unknown angle units "${this._angle.units}"`);
return 0;
}
let value = 0;
switch (units) {
case WI.LinearGradient.AngleUnits.DEG:
value = deg;
break;
case WI.LinearGradient.AngleUnits.RAD:
value = deg * Math.PI / 180;
break;
case WI.LinearGradient.AngleUnits.GRAD:
value = deg / 360 * 400;
break;
case WI.LinearGradient.AngleUnits.TURN:
value = deg / 360;
break;
}
return value;
}
};
WI.LinearGradient.AngleUnits = {
DEG: "deg",
RAD: "rad",
GRAD: "grad",
TURN: "turn",
};
WI.RadialGradient = class RadialGradient extends WI.Gradient
{
constructor(sizing, stops)
{
super(WI.Gradient.Types.Radial, stops);
this.sizing = sizing;
}
// Static
static fromComponents(components)
{
var sizing = !WI.Color.fromString(components[0].join(" ")) ? components.shift().join(" ") : "";
var stops = WI.Gradient.stopsWithComponents(components);
if (!stops)
return null;
return new WI.RadialGradient(sizing, stops);
}
// Public
copy()
{
return new WI.RadialGradient(this.sizing, this.stops.concat());
}
toString()
{
var str = this.sizing;
if (str !== "")
str += ", ";
str += this.stringFromStops(this.stops);
return (this.repeats ? "repeating-" : "") + this.type + "(" + str + ")";
}
};