Browse Source

Merge pull request #501 from motiz88/hsl-color

hsl() and hsla() color parsing
v1.x
Juriy Zaytsev 10 years ago
parent
commit
745cfdc6ad
  1. 368
      src/color.cc
  2. 50
      test/canvas.test.js
  3. 22
      test/public/tests.js

368
src/color.cc

@ -7,31 +7,233 @@
#include "color.h"
#include <stdlib.h>
#include <cmath>
#include <limits>
/*
* Consume whitespace.
* Parse integer value
*/
#define WHITESPACE \
while (' ' == *str) ++str;
template <typename parsed_t>
static bool
parse_integer(const char** pStr, parsed_t *pParsed) {
parsed_t& c = *pParsed;
const char*& str = *pStr;
int8_t sign=1;
c = 0;
if (*str == '-') {
sign=-1;
++str;
}
else if (*str == '+')
++str;
if (*str >= '0' && *str <= '9') {
do {
c *= 10;
c += *str++ - '0';
} while (*str >= '0' && *str <= '9');
} else {
return false;
}
if (sign<0)
c=-c;
return true;
}
/*
* Parse CSS <number> value
* Adapted from http://crackprogramming.blogspot.co.il/2012/10/implement-atof.html
*/
template <typename parsed_t>
static bool
parse_css_number(const char** pStr, parsed_t *pParsed) {
parsed_t &parsed = *pParsed;
const char*& str = *pStr;
const char* startStr = str;
if (!str || !*str)
return false;
parsed_t integerPart = 0;
parsed_t fractionPart = 0;
int divisorForFraction = 1;
int sign = 1;
int exponent = 0;
int digits = 0;
bool inFraction = false;
if (*str == '-') {
++str;
sign = -1;
}
else if (*str == '+')
++str;
while (*str != '\0') {
if (*str >= '0' && *str <= '9') {
if (digits>=std::numeric_limits<parsed_t>::digits10) {
if (!inFraction)
return false;
}
else {
++digits;
if (inFraction) {
fractionPart = fractionPart*10 + (*str - '0');
divisorForFraction *= 10;
}
else {
integerPart = integerPart*10 + (*str - '0');
}
}
}
else if (*str == '.') {
if (inFraction)
break;
else
inFraction = true;
}
else if (*str == 'e') {
++str;
if (!parse_integer(&str, &exponent))
return false;
break;
}
else
break;
++str;
}
if (str != startStr) {
parsed = sign * (integerPart + fractionPart/divisorForFraction);
for (;exponent>0;--exponent)
parsed *= 10;
for (;exponent<0;++exponent)
parsed /= 10;
return true;
}
return false;
}
/*
* Clip value to the range [minValue, maxValue]
*/
template <typename T>
static T
clip(T value, T minValue, T maxValue) {
if (value > maxValue)
value = maxValue;
if (value < minValue)
value = minValue;
return value;
}
/*
* Wrap value to the range [0, limit]
*/
template <typename T>
static T
wrap_float(T value, T limit) {
return fmod(fmod(value, limit) + limit, limit);
}
/*
* Wrap value to the range [0, limit] - currently-unused integer version of wrap_float
*/
// template <typename T>
// static T wrap_int(T value, T limit) {
// return (value % limit + limit) % limit;
// }
/*
* Parse color channel value
*/
static bool
parse_rgb_channel(const char** pStr, uint8_t *pChannel) {
int channel;
if (parse_integer(pStr, &channel)) {
*pChannel = clip(channel, 0, 255);
return true;
}
return false;
}
/*
* Parse a value in degrees
*/
static bool
parse_degrees(const char** pStr, float *pDegrees) {
float degrees;
if (parse_css_number(pStr, &degrees)) {
*pDegrees = wrap_float(degrees, 360.0f);
return true;
}
return false;
}
/*
* Parse and clip a percentage value. Returns a float in the range [0, 1].
*/
static bool
parse_clipped_percentage(const char** pStr, float *pFraction) {
float percentage;
bool result = parse_css_number(pStr,&percentage);
const char*& str = *pStr;
if (result) {
if (*str == '%') {
++str;
*pFraction = clip(percentage, 0.0f, 100.0f) / 100.0f;
return result;
}
}
return false;
}
/*
* Macros to help with parsing inside rgba_from_*_string
*/
#define WHITESPACE \
while (' ' == *str) ++str;
#define WHITESPACE_OR_COMMA \
while (' ' == *str || ',' == *str) ++str;
#define CHANNEL(NAME) \
c = 0; \
if (*str >= '0' && *str <= '9') { \
do { \
c *= 10; \
c += *str++ - '0'; \
} while (*str >= '0' && *str <= '9'); \
} else { \
return 0; \
} \
if (c > 255) c = 255; \
NAME = c; \
while (' ' == *str || ',' == *str) str++;
if (!parse_rgb_channel(&str, &NAME)) \
return 0; \
#define HUE(NAME) \
if (!parse_degrees(&str, &NAME)) \
return 0;
#define SATURATION(NAME) \
if (!parse_clipped_percentage(&str, &NAME)) \
return 0;
#define LIGHTNESS(NAME) SATURATION(NAME)
#define ALPHA(NAME) \
if (*str >= '1' && *str <= '9') { \
NAME = 1; \
} else { \
if ('0' == *str) ++str; \
if ('.' == *str) { \
++str; \
float n = .1f; \
while (*str >= '0' && *str <= '9') { \
NAME += (*str++ - '0') * n; \
n *= .1f; \
} \
} \
} \
do {} while (0) // require trailing semicolon
/*
* Named colors.
@ -161,6 +363,7 @@ static struct named_color {
, { "plum", 0xDDA0DDFF }
, { "powderblue", 0xB0E0E6FF }
, { "purple", 0x800080FF }
, { "rebeccapurple", 0x663399FF } // Source: CSS Color Level 4 draft
, { "red", 0xFF0000FF }
, { "rosybrown", 0xBC8F8FFF }
, { "royalblue", 0x4169E1FF }
@ -275,6 +478,64 @@ rgba_from_rgba(uint8_t r, uint8_t g, uint8_t b, uint8_t a) {
| a;
}
/*
* Helper function used in rgba_from_hsla().
* Based on http://dev.w3.org/csswg/css-color-4/#hsl-to-rgb
*/
static float
hue_to_rgb(float t1, float t2, float hue) {
if (hue < 0)
hue += 6;
if (hue >= 6)
hue -= 6;
if (hue < 1)
return (t2 - t1) * hue + t1;
else if (hue < 3)
return t2;
else if (hue < 4)
return (t2 - t1) * (4 - hue) + t1;
else
return t1;
}
/*
* Return rgba from (h,s,l,a).
* Expects h values in the range [0, 360), and s, l, a in the range [0, 1].
* Adapted from http://dev.w3.org/csswg/css-color-4/#hsl-to-rgb
*/
static inline int32_t
rgba_from_hsla(float h_deg, float s, float l, float a) {
uint8_t r, g, b;
float h = (6 * h_deg) / 360.0f, m1, m2;
if (l<=0.5)
m2=l*(s+1);
else
m2=l+s-l*s;
m1 = l*2 - m2;
// Scale and round the RGB components
r = (uint8_t)floor(hue_to_rgb(m1, m2, h + 2) * 255 + 0.5);
g = (uint8_t)floor(hue_to_rgb(m1, m2, h ) * 255 + 0.5);
b = (uint8_t)floor(hue_to_rgb(m1, m2, h - 2) * 255 + 0.5);
return rgba_from_rgba(r, g, b, (uint8_t) (a * 255));
}
/*
* Return rgba from (h,s,l).
* Expects h values in the range [0, 360), and s, l in the range [0, 1].
*/
static inline int32_t
rgba_from_hsl(float h_deg, float s, float l) {
return rgba_from_hsla(h_deg, s, l, 1.0);
}
/*
* Return rgba from (r,g,b).
*/
@ -320,10 +581,12 @@ rgba_from_rgb_string(const char *str, short *ok) {
str += 4;
WHITESPACE;
uint8_t r = 0, g = 0, b = 0;
int c;
CHANNEL(r);
WHITESPACE_OR_COMMA;
CHANNEL(g);
WHITESPACE_OR_COMMA;
CHANNEL(b);
WHITESPACE;
return *ok = 1, rgba_from_rgb(r, g, b);
}
return *ok = 0;
@ -339,29 +602,68 @@ rgba_from_rgba_string(const char *str, short *ok) {
str += 5;
WHITESPACE;
uint8_t r = 0, g = 0, b = 0;
int c;
float a = 0;
CHANNEL(r);
WHITESPACE_OR_COMMA;
CHANNEL(g);
WHITESPACE_OR_COMMA;
CHANNEL(b);
if (*str >= '1' && *str <= '9') {
a = 1;
} else {
if ('0' == *str) ++str;
if ('.' == *str) {
++str;
float n = .1f;
while (*str >= '0' && *str <= '9') {
a += (*str++ - '0') * n;
n *= .1f;
}
}
}
WHITESPACE_OR_COMMA;
ALPHA(a);
WHITESPACE;
return *ok = 1, rgba_from_rgba(r, g, b, (int) (a * 255));
}
return *ok = 0;
}
/*
* Return rgb from "hsla()"
*/
static int32_t
rgba_from_hsla_string(const char *str, short *ok) {
if (str == strstr(str, "hsla(")) {
str += 5;
WHITESPACE;
float h_deg = 0;
float s = 0, l = 0;
float a = 0;
HUE(h_deg);
WHITESPACE_OR_COMMA;
SATURATION(s);
WHITESPACE_OR_COMMA;
LIGHTNESS(l);
WHITESPACE_OR_COMMA;
ALPHA(a);
WHITESPACE;
return *ok = 1, rgba_from_hsla(h_deg, s, l, a);
}
return *ok = 0;
}
/*
* Return rgb from "hsl()"
*/
static int32_t
rgba_from_hsl_string(const char *str, short *ok) {
if (str == strstr(str, "hsl(")) {
str += 4;
WHITESPACE;
float h_deg = 0;
float s = 0, l = 0;
HUE(h_deg);
WHITESPACE_OR_COMMA;
SATURATION(s);
WHITESPACE_OR_COMMA;
LIGHTNESS(l);
WHITESPACE;
return *ok = 1, rgba_from_hsl(h_deg, s, l);
}
return *ok = 0;
}
/*
* Return rgb from:
*
@ -401,6 +703,8 @@ rgba_from_name_string(const char *str, short *ok) {
* - #RRGGBB
* - rgb(r,g,b)
* - rgba(r,g,b,a)
* - hsl(h,s,l)
* - hsla(h,s,l,a)
* - name
*
*/
@ -413,6 +717,10 @@ rgba_from_string(const char *str, short *ok) {
return rgba_from_rgba_string(str, ok);
if (str == strstr(str, "rgb"))
return rgba_from_rgb_string(str, ok);
if (str == strstr(str, "hsla"))
return rgba_from_hsla_string(str, ok);
if (str == strstr(str, "hsl"))
return rgba_from_hsl_string(str, ok);
return rgba_from_name_string(str, ok);
}

50
test/canvas.test.js

@ -158,6 +158,56 @@ module.exports = {
ctx.fillStyle = 'rgba(0, 0, 0, 42.42)';
assert.equal('#000000', ctx.fillStyle);
// hsl / hsla tests
ctx.fillStyle = 'hsl(0, 0%, 0%)';
assert.equal('#000000', ctx.fillStyle);
ctx.fillStyle = 'hsl(3600, -10%, -10%)';
assert.equal('#000000', ctx.fillStyle);
ctx.fillStyle = 'hsl(10, 100%, 42%)';
assert.equal('#d62400', ctx.fillStyle);
ctx.fillStyle = 'hsl(370, 120%, 42%)';
assert.equal('#d62400', ctx.fillStyle);
ctx.fillStyle = 'hsl(0, 100%, 100%)';
assert.equal('#ffffff', ctx.fillStyle);
ctx.fillStyle = 'hsl(0, 150%, 150%)';
assert.equal('#ffffff', ctx.fillStyle);
ctx.fillStyle = 'hsl(237, 76%, 25%)';
assert.equal('#0f1470', ctx.fillStyle);
ctx.fillStyle = 'hsl(240, 73%, 25%)';
assert.equal('#11116e', ctx.fillStyle);
ctx.fillStyle = 'hsl(262, 32%, 42%)';
assert.equal('#62498d', ctx.fillStyle);
ctx.fillStyle = 'hsla(0, 0%, 0%, 1)';
assert.equal('#000000', ctx.fillStyle);
ctx.fillStyle = 'hsla(0, 100%, 100%, 1)';
assert.equal('#ffffff', ctx.fillStyle);
ctx.fillStyle = 'hsla(120, 25%, 75%, 0.5)';
assert.equal('rgba(175, 207, 175, 0.50)', ctx.fillStyle);
ctx.fillStyle = 'hsla(240, 75%, 25%, 0.75)';
assert.equal('rgba(16, 16, 112, 0.75)', ctx.fillStyle);
ctx.fillStyle = 'hsla(172.0, 33.00000e0%, 42%, 1)';
assert.equal('#488e85', ctx.fillStyle);
ctx.fillStyle = 'hsl(124.5, 76.1%, 47.6%)';
assert.equal('#1dd62b', ctx.fillStyle);
ctx.fillStyle = 'hsl(1.24e2, 760e-1%, 4.7e1%)';
assert.equal('#1dd329', ctx.fillStyle);
},
'test Canvas#type': function(){

22
test/public/tests.js

@ -1916,3 +1916,25 @@ tests['lineDashOffset'] = function(ctx, done){
for (var i=0; i<10; i++)
line(90 + i/5, "red");
}
tests['fillStyle=\'hsl(...)\''] = function(ctx){
for (i=0;i<6;i++){
for (j=0;j<6;j++){
ctx.fillStyle = 'hsl(' + (360-60*i) + ',' +
(100-16.66*j) + '%,' + (50+(i+j)*(50/12)) + '%)';
ctx.fillRect(j*25,i*25,25,25);
}
}
};
tests['fillStyle=\'hsla(...)\''] = function(ctx){
for (i=0;i<6;i++){
for (j=0;j<6;j++){
ctx.fillStyle = 'hsla(' + (360-60*i) + ',' +
(100-16.66*j) + '%,50%,' + (1-0.16*j) + ')';
console.log((100-16.66*j));
ctx.fillRect(j*25,i*25,25,25);
}
}
};

Loading…
Cancel
Save