Color: Improve CSS string parser and docs

This commit is contained in:
Jürg Lehni 2018-10-03 15:56:15 +02:00
parent e41ed5e723
commit da3a36230f
2 changed files with 89 additions and 39 deletions

View file

@ -56,9 +56,9 @@ var Color = Base.extend(new function() {
colorCache = {}, colorCache = {},
colorCtx; colorCtx;
// TODO: Implement hsv, etc. CSS parsing!
function fromCSS(string) { function fromCSS(string) {
var match = string.match(/^#(\w{1,2})(\w{1,2})(\w{1,2})$/), var match = string.match(/^#(\w{1,2})(\w{1,2})(\w{1,2})$/),
type = 'rgb',
components; components;
if (match) { if (match) {
// Hex // Hex
@ -68,12 +68,34 @@ var Color = Base.extend(new function() {
components[i] = parseInt(value.length == 1 components[i] = parseInt(value.length == 1
? value + value : value, 16) / 255; ? value + value : value, 16) / 255;
} }
} else if (match = string.match(/^rgba?\((.*)\)$/)) { } else if (match = string.match(/^(rgb|hsl)a?\((.*)\)$/)) {
// RGB / RGBA // RGB / RGBA or HSL / HSLA
components = match[1].split(','); type = match[1];
for (var i = 0, l = components.length; i < l; i++) { components = match[2].split(/[,\s]+/g);
var value = +components[i]; var isHSL = type === 'hsl';
components[i] = i < 3 ? value / 255 : value; for (var i = 0, l = Math.min(components.length, 4); i < l; i++) {
var component = components[i];
// Use `parseFloat()` instead of `+value` to parse '\d+%' to
// float for HSL:
var value = parseFloat(component);
if (isHSL) {
if (i === 0) {
// handle 'deg', 'turn', 'rad' 'grad':
var unit = component.match(/([a-z]*)$/)[1];
value *= ({
turn: 360,
rad: 180 / Math.PI,
grad: 0.9 // 360 / 400
}[unit] || 1);
} else if (i < 3) {
// Percentages to 0..1
value /= 100;
}
} else if (i < 3) {
// RGB color values to 0..1
value /= 255;
}
components[i] = value;
} }
} else if (window) { } else if (window) {
// Named // Named
@ -106,7 +128,7 @@ var Color = Base.extend(new function() {
// Web-workers can't resolve CSS color names, for now. // Web-workers can't resolve CSS color names, for now.
components = [0, 0, 0]; components = [0, 0, 0];
} }
return components; return [type, components];
} }
// For hsb-rgb conversion, used to lookup the right parameters in the // For hsb-rgb conversion, used to lookup the right parameters in the
@ -237,36 +259,40 @@ var Color = Base.extend(new function() {
// hsb and hsl. Handle this here separately, by testing for // hsb and hsl. Handle this here separately, by testing for
// overlaps and skipping conversion if the type is /hs[bl]/ // overlaps and skipping conversion if the type is /hs[bl]/
hasOverlap = /^(hue|saturation)$/.test(name), hasOverlap = /^(hue|saturation)$/.test(name),
// Produce value parser function for the given type / propeprty // Produce value parser function for the given type / property
// name combination. parser = componentParsers[type][index] = type === 'gradient'
parser = componentParsers[type][index] = name === 'gradient' ? name === 'gradient'
? function(value) { // gradient property of gradient color:
var current = this._components[0]; ? function(value) {
value = Gradient.read(Array.isArray(value) ? value var current = this._components[0];
: arguments, 0, { readNull: true }); value = Gradient.read(
if (current !== value) { Array.isArray(value)
if (current) ? value
current._removeOwner(this); : arguments, 0, { readNull: true }
if (value) );
value._addOwner(this); if (current !== value) {
if (current)
current._removeOwner(this);
if (value)
value._addOwner(this);
}
return value;
} }
return value; // all other (point) properties of gradient color:
} : function(/* value */) {
: type === 'gradient'
? function(/* value */) {
return Point.read(arguments, 0, { return Point.read(arguments, 0, {
readNull: name === 'highlight', readNull: name === 'highlight',
clone: true clone: true
}); });
} }
: function(value) { // Normal number component properties:
// NOTE: We don't clamp values here, they're only : function(value) {
// clamped once the actual CSS values are produced. // NOTE: We don't clamp values here, they're only
// Gotta love the fact that isNaN(null) is false, // clamped once the actual CSS values are produced.
// while isNaN(undefined) is true. // Gotta love the fact that isNaN(null) is false,
return value == null || isNaN(value) ? 0 : value; // while isNaN(undefined) is true.
}; return value == null || isNaN(value) ? 0 : +value;
};
this['get' + part] = function() { this['get' + part] = function() {
return this._type === type return this._type === type
|| hasOverlap && /^hs[bl]$/.test(this._type) || hasOverlap && /^hs[bl]$/.test(this._type)
@ -411,6 +437,25 @@ var Color = Base.extend(new function() {
* } * }
* }); * });
*/ */
/**
* Creates a Color object from a CSS string. All common CSS color string
* formats are supported:
* - Named colors (e.g. `'red'`, `'fuchsia'`, )
* - Hex strings (`'#ffff00'`, `'#ff0'`, )
* - RGB strings (`'rgb(255, 128, 0)'`, `'rgba(255, 128, 0, 0.5)'`, )
* - HSL strings (`'hsl(180deg, 20%, 50%)'`,
* `'hsla(3.14rad, 20%, 50%, 0.5)'`, )
*
* @name Color#initialize
* @param {String} color the color's CSS string representation
*
* @example {@paperscript}
* var circle = new Path.Circle({
* center: [80, 50],
* radius: 30,
* fillColor: new Color('rgba(255, 255, 0, 0.5)')
* });
*/
/** /**
* Creates a gradient Color object. * Creates a gradient Color object.
* *
@ -544,8 +589,9 @@ var Color = Base.extend(new function() {
if (values.length > length) if (values.length > length)
values = Base.slice(values, 0, length); values = Base.slice(values, 0, length);
} else if (argType === 'string') { } else if (argType === 'string') {
type = 'rgb'; var converted = fromCSS(arg);
components = fromCSS(arg); type = converted[0];
components = converted[1];
if (components.length === 4) { if (components.length === 4) {
alpha = components[3]; alpha = components[3];
components.length--; components.length--;

View file

@ -61,22 +61,26 @@ test('Creating Colors', function() {
'Color from name (red)'); 'Color from name (red)');
equals(new Color('#ff0000'), new Color(1, 0, 0), equals(new Color('#ff0000'), new Color(1, 0, 0),
'Color from hex code'); 'Color from hex string');
equals(new Color('rgb(255, 0, 0)'), new Color(1, 0, 0), equals(new Color('rgb(255, 0, 0)'), new Color(1, 0, 0),
'Color from RGB code'); 'Color from rgb() string');
equals(new Color('rgba(255, 0, 0, 0.5)'), new Color(1, 0, 0, 0.5), equals(new Color('rgba(255, 0, 0, 0.5)'), new Color(1, 0, 0, 0.5),
'Color from RGBA code'); 'Color from rgba() string');
equals(new Color('hsl(180deg, 20%, 40%)'),
new Color({ hue: 180, saturation: 0.2, lightness: 0.4 }),
'Color from hsl() string');
equals(new Color({ red: 1, green: 0, blue: 1}), equals(new Color({ red: 1, green: 0, blue: 1}),
new Color(1, 0, 1), 'Color from rgb object literal'); new Color(1, 0, 1), 'Color from RGB object literal');
equals(new Color({ gray: 0.2 }), equals(new Color({ gray: 0.2 }),
new Color(0.2), 'Color from gray object literal'); new Color(0.2), 'Color from gray object literal');
equals(new Color({ hue: 0, saturation: 1, brightness: 1}), equals(new Color({ hue: 0, saturation: 1, brightness: 1}),
new Color(1, 0, 0).convert('hsb'), 'Color from hsb object literal'); new Color(1, 0, 0).convert('hsb'), 'Color from HSB object literal');
equals(new Color([1, 0, 0]), new Color(1, 0, 0), equals(new Color([1, 0, 0]), new Color(1, 0, 0),
'RGB Color from array'); 'RGB Color from array');