Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: feat(color): Rewrite color in terms of css-color-4 #314

Draft
wants to merge 5 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5,928 changes: 5,195 additions & 733 deletions src/color.rs

Large diffs are not rendered by default.

316 changes: 158 additions & 158 deletions src/css-parsing-tests/color3.json

Large diffs are not rendered by default.

29,760 changes: 14,208 additions & 15,552 deletions src/css-parsing-tests/color3_hsl.json

Large diffs are not rendered by default.

1,210 changes: 605 additions & 605 deletions src/css-parsing-tests/color3_keywords.json

Large diffs are not rendered by default.

15,552 changes: 7,776 additions & 7,776 deletions src/css-parsing-tests/color4_hwb.json

Large diffs are not rendered by default.

55,200 changes: 50,336 additions & 4,864 deletions src/css-parsing-tests/color4_lab_lch_oklab_oklch.json

Large diffs are not rendered by default.

60 changes: 27 additions & 33 deletions src/css-parsing-tests/make_color3_hsl.py
Original file line number Diff line number Diff line change
@@ -1,35 +1,21 @@
import colorsys # It turns out Python already does HSL -> RGB!
import math
import re


def fix_rounding_error(x):
# We get rounding errors on these values.
# 127.5 sometimes rounds to 128, sometimes rounds to 127,
# so let's just brute force fix (haxX0r!1) them!
x = round(x)
if x == 127:
return 128
return x


def rgba_to_str(r, g, b, a=None):
if a is None:
a = 1.0

r = fix_rounding_error(r * 255)
g = fix_rounding_error(g * 255)
b = fix_rounding_error(b * 255)

if a == 1.0:
return 'rgb({:g}, {:g}, {:g})'.format(r, g, b)
else:
return 'rgba({:g}, {:g}, {:g}, {:g})'.format(r, g, b, a)


def trim(s): return s if not s.endswith('.0') else s[:-2]

# We sometimes get values of 127.499999 which
# may be 127.5000001 in the implementation. We'd like
# to exclude these values.
#
# 127.5 is the only case where the set of values used
# in the test causes rounding errors.
def has_rounding_error(hue, saturation, lightness):
rgb = colorsys.hls_to_rgb(
hue / 360.,
lightness / 1000.,
saturation / 1000.
)
return any(abs((x * 255) - 127.5) < 0.01 for x in rgb)

trim = lambda s: s if not s.endswith('.0') else s[:-2]
print('[')
print(',\n'.join(
function_format % tuple(
Expand All @@ -38,19 +24,27 @@ def trim(s): return s if not s.endswith('.0') else s[:-2]
hue,
trim(str(saturation / 10.)),
trim(str(lightness / 10.)),
alpha_format % round(alpha, 2) if alpha is not None else '',
rgba_to_str(*colorsys.hls_to_rgb(hue / 360.,
lightness / 1000., saturation / 1000.), alpha),
alpha_format % round(alpha, 2) if alpha is not None else ''
] + [
trim(str(min(255, round(component * 255.))))
for component in colorsys.hls_to_rgb(
hue / 360.,
lightness / 1000.,
saturation / 1000.
)
] + [
alpha if alpha is not None else 1.0
]
)
for function_format, alpha_format in [
('"%s(%s, %s%%, %s%%%s)", "%s"', ', %s'),
('"%s(%s %s%% %s%%%s)", "%s"', ' / %s')
('"%s(%s, %s%%, %s%%%s)", [%s, %s, %s, %s]', ', %s'),
('"%s(%s %s%% %s%%%s)", [%s, %s, %s, %s]', ' / %s')
]
for function_name in ["hsl", "hsla"]
for alpha in [None, 1.0, 0.25, 0.0]
for lightness in range(0, 1001, 125)
for saturation in range(0, 1001, 125)
for hue in range(0, 360, 30)
if not has_rounding_error(hue, saturation, lightness)
))
print(']')
10 changes: 1 addition & 9 deletions src/css-parsing-tests/make_color3_keywords.py
Original file line number Diff line number Diff line change
Expand Up @@ -172,17 +172,9 @@ def replace(s, i, r):
i %= len(s)
return s[:i] + r(s[i]) + s[i + 1:]


def rgba_to_str(r, g, b, a=1.0):
if a == 1.0:
return '"rgb({:g}, {:g}, {:g})"'.format(r, g, b)
else:
return '"rgba({:g}, {:g}, {:g}, {:g})"'.format(r, g, b, a)


print('[')
print(',\n'.join(
'"%s", %s' % (css, rgba_to_str(*rgba) if valid else 'null')
'"%s", %s' % (css, list(rgba) if valid else 'null')
for i, (keyword, rgba) in enumerate(all_keywords)
for css, valid, run in [
(keyword, True, True),
Expand Down
44 changes: 14 additions & 30 deletions src/css-parsing-tests/make_color4_hwb.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@
from decimal import Decimal


def fix_rounding_error(val):
return round(val * 100000) / 100000

# Based on https://github.com/web-platform-tests/wpt/blob/master/css/css-color/color-resolving-hwb.html


def hwb_to_rgb(hue, white, black):
"""
Based on https://github.com/web-platform-tests/wpt/blob/master/css/css-color/color-resolving-hwb.html
"""
if white + black >= 1:
gray = min(max(round(white / (white + black) * 255.0), 0.0), 255.0)
return (gray, gray, gray)
Expand All @@ -20,39 +23,20 @@ def hwb_to_rgb(hue, white, black):
return tuple([min(max(round((fix_rounding_error(x) * t + white) * 255.0), 0.0), 255.0) for x in rgb])


def fix_rounding_error(x):
# We get rounding errors on these values, so let's just
# brute force fix (haxX0r!1) them!
if x == 76 or x == 127 or x == 178:
return x + 1
return x


def rgba_to_str(r, g, b, a=1.0):
r = fix_rounding_error(r)
g = fix_rounding_error(g)
b = fix_rounding_error(b)

if a == 1.0:
return 'rgb({:g}, {:g}, {:g})'.format(r, g, b)
else:
return 'rgba({:g}, {:g}, {:g}, {:g})'.format(r, g, b, a)


items = []
for hue in [0, 30, 60, 90, 120, 180, 210, 240, 270, 300, 330, 360]:
for white in [0.0, 0.125, 0.25, 0.375, 0.5, 0.625, 0.75, 0.875, 1.0]:
for black in [0.0, 0.125, 0.25, 0.375, 0.5, 0.625, 0.75, 0.875, 1.0]:
(r, g, b) = hwb_to_rgb(hue, white, black)
items.append('"hwb({:g}deg {:g}% {:g}%)", "{}"'.format(
hue, white * 100, black * 100, rgba_to_str(r, g, b)))
items.append('"hwb({:g} {:g}% {:g}%)", "{}"'.format(
hue, white * 100, black * 100, rgba_to_str(r, g, b)))
items.append('"hwb({:g}deg {:g}% {:g}%)", [{:g}, {:g}, {:g}, 1.0]'.format(
hue, white * 100, black * 100, r, g, b))
items.append('"hwb({:g} {:g}% {:g}%)", [{:g}, {:g}, {:g}, 1.0]'.format(
hue, white * 100, black * 100, r, g, b))
for alpha in [0, 0.2, 1]:
items.append('"hwb({:g}deg {:g}% {:g}% / {:g})", "{}"'.format(
hue, white * 100, black * 100, alpha, rgba_to_str(r, g, b, alpha)))
items.append('"hwb({:g} {:g}% {:g}% / {:g})", "{}"'.format(
hue, white * 100, black * 100, alpha, rgba_to_str(r, g, b, alpha)))
items.append('"hwb({:g}deg {:g}% {:g}% / {:g})", [{:g}, {:g}, {:g}, {:g}]'.format(
hue, white * 100, black * 100, alpha, r, g, b, alpha))
items.append('"hwb({:g} {:g}% {:g}% / {:g})", [{:g}, {:g}, {:g}, {:g}]'.format(
hue, white * 100, black * 100, alpha, r, g, b, alpha))

print('[')
print(',\n'.join(items))
Expand Down
105 changes: 52 additions & 53 deletions src/css-parsing-tests/make_color4_lab_lch_oklab_oklch.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,18 @@
items = []


def lab(lightness, a, b, alpha=1):
return (max(0.0, lightness), a, b, alpha)
def cielab(lightness, a, b, alpha=1, clamp=False):
if clamp:
lightness = max(0.0, min(lightness, 100.0))

return (lightness, a * 125.0 / 100.0, b * 125.0 / 100.0, alpha)

def labp(lightness, a, b, alpha=1):
return (max(0.0, lightness), a * 125.0 / 100.0, b * 125.0 / 100.0, alpha)

def oklab(lightness, a, b, alpha=1, clamp=False):
if clamp:
lightness = max(0.0, min(lightness, 100.0))

def oklab(lightness, a, b, alpha=1):
return (max(0.0, lightness), a, b, alpha)


def oklabp(lightness, a, b, alpha=1):
return (max(0.0, lightness / 100.0), a * 0.4 / 100.0, b * 0.4 / 100.0, alpha)
return (lightness / 100.0, a * 0.4 / 100.0, b * 0.4 / 100.0, alpha)


def slab(name, lightness, a, b, alpha=1):
Expand All @@ -24,48 +22,44 @@ def slab(name, lightness, a, b, alpha=1):
return '{}({:g} {:g} {:g} / {:g})'.format(name, lightness, a, b, alpha)


def lab_like(name, f, fp):
for b in [0.0, 10.0, 110.0, -10.0]:
for a in [0.0, 10.0, 110.0, -10.0]:
for lightness in [0.0, 10.0, 110.0, -10.0]:
items.append('"{}({:g} {:g} {:g})", "{:s}"'.format(
name, lightness, a, b, slab(name, *f(lightness, a, b))))
items.append('"{}({:g}% {:g}% {:g}%)", "{:s}"'.format(
name, lightness, a, b, slab(name, *fp(lightness, a, b))))
for alpha in [0, 0.2, 1]:
items.append('"{}({:g} {:g} {:g} / {:g})", "{:s}"'.format(
name, lightness, a, b, alpha, slab(name, *f(lightness, a, b, alpha))))
items.append('"{}({:g}% {:g}% {:g}% / {:g})", "{:s}"'.format(
name, lightness, a, b, alpha, slab(name, *fp(lightness, a, b, alpha))))
def lab_like(name, lab):
percentages = [ 0.0, 10.0, 25.0, 33.33, 50.0, 66.67, 75.0, 90.0, 100.0, -10.0, 110.0 ]

for b in percentages:
for a in percentages:
for lightness in percentages:
items.append('"{}({:g}% {:g}% {:g}%)", [[{:#g}, {:#g}, {:#g}, {:#g}], "{:s}"]'.format(
name, lightness, a, b, *lab(lightness, a, b), slab(name, *lab(lightness, a, b, clamp=True))))

lab_like('lab', lab, labp)
lab_like('oklab', oklab, oklabp)
items.append('"{}({:g} {:g} {:g})", [[{:#g}, {:#g}, {:#g}, {:#g}], "{:s}"]'.format(
name, *lab(lightness, a, b)[:3], *lab(lightness, a, b), slab(name, *lab(lightness, a, b, clamp=True))))

for alpha in [0, 0.2, 1]:
items.append('"{}({:g}% {:g}% {:g}% / {:g})", [[{:#g}, {:#g}, {:#g}, {:#g}], "{:s}"]'.format(
name, lightness, a, b, alpha, *lab(lightness, a, b, alpha), slab(name, *lab(lightness, a, b, alpha, clamp=True))))

def calc_deg(deg):
while deg >= 360.0:
deg -= 360.0
while deg < 0.0:
deg += 360.0

return deg
items.append('"{}({:g} {:g} {:g} / {:g})", [[{:#g}, {:#g}, {:#g}, {:#g}], "{:s}"]'.format(
name, *lab(lightness, a, b, alpha), *lab(lightness, a, b, alpha), slab(name, *lab(lightness, a, b, alpha, clamp=True))))


def lch(lightness, c, h, alpha=1):
return (max(lightness, 0.0), max(0.0, c), calc_deg(h), alpha)
lab_like('lab', cielab)
lab_like('oklab', oklab)


def lchp(lightness, c, h, alpha=1):
return (max(lightness, 0.0), max(0.0, c * 150.0 / 100.0), calc_deg(h), alpha)
def cielch(lightness, c, h, alpha=1, clamp=False):
if clamp:
lightness = max(0.0, min(lightness, 100.0))
c = max(0.0, c)

return (lightness, c * 150.0 / 100.0, h, alpha)

def oklch(lightness, c, h, alpha=1):
return (max(lightness, 0.0), max(0.0, c), calc_deg(h), alpha)

def oklch(lightness, c, h, alpha=1, clamp=False):
if clamp:
lightness = max(0.0, min(lightness, 100.0))
c = max(0.0, c)

def oklchp(lightness, c, h, alpha=1):
return (max(lightness, 0.0) / 100.0, max(0.0, c * 0.4 / 100.0), calc_deg(h), alpha)
return (lightness / 100.0, c * 0.4 / 100.0, h, alpha)


def slch(name, lightness, c, h, alpha=1):
Expand All @@ -75,23 +69,28 @@ def slch(name, lightness, c, h, alpha=1):
return '{}({:g} {:g} {:g} / {:g})'.format(name, lightness, c, h, alpha)


def lch_like(name, f, fp):
def lch_like(name, lch):
percentages = [ 0.0, 10.0, 25.0, 33.33, 50.0, 66.67, 75.0, 90.0, 100.0, -10.0, 110.0 ]

for h in [0, 30, 60, 90, 120, 180, 210, 240, 270, 300, 330, 360, 380, 700, -20]:
for c in [0.0, 10.0, 110.0, -10.0]:
for lightness in [0.0, 10.0, 110.0, -10.0]:
items.append('"{}({:g} {:g} {:g})", "{:s}"'.format(
name, lightness, c, h, slch(name, *f(lightness, c, h))))
items.append('"{}({:g}% {:g}% {:g}deg)", "{:s}"'.format(
name, lightness, c, h, slch(name, *fp(lightness, c, h))))
for c in percentages:
for lightness in percentages:
items.append('"{}({:g}% {:g}% {:g}deg)", [[{:#g}, {:#g}, {:#g}, {:#g}], "{:s}"]'.format(
name, lightness, c, h, *lch(lightness, c, h), slch(name, *lch(lightness, c, h, clamp=True))))

items.append('"{}({:g} {:g} {:g})", [[{:#g}, {:#g}, {:#g}, {:#g}], "{:s}"]'.format(
name, *lch(lightness, c, h)[:3], *lch(lightness, c, h), slch(name, *lch(lightness, c, h, clamp=True))))

for alpha in [0, 0.2, 1]:
items.append('"{}({:g} {:g} {:g} / {:g})", "{:s}"'.format(
name, lightness, c, h, alpha, slch(name, *f(lightness, c, h, alpha))))
items.append('"{}({:g}% {:g}% {:g}deg / {:g})", "{:s}"'.format(
name, lightness, c, h, alpha, slch(name, *fp(lightness, c, h, alpha))))
items.append('"{}({:g}% {:g}% {:g}deg / {:g})", [[{:#g}, {:#g}, {:#g}, {:#g}], "{:s}"]'.format(
name, lightness, c, h, alpha, *lch(lightness, c, h, alpha), slch(name, *lch(lightness, c, h, alpha, clamp=True))))

items.append('"{}({:g} {:g} {:g} / {:g})", [[{:#g}, {:#g}, {:#g}, {:#g}], "{:s}"]'.format(
name, *lch(lightness, c, h, alpha), *lch(lightness, c, h, alpha), slch(name, *lch(lightness, c, h, alpha, clamp=True))))


lch_like('lch', lch, lchp)
lch_like('oklch', oklch, oklchp)
lch_like('lch', cielch)
lch_like('oklch', oklch)


print('[')
Expand Down
4 changes: 2 additions & 2 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,8 @@ fn parse_border_spacing(_context: &ParserContext, input: &mut Parser)
#![recursion_limit = "200"] // For color::parse_color_keyword

pub use crate::color::{
hsl_to_rgb, hwb_to_rgb, parse_color_keyword, AbsoluteColor, AngleOrNumber, Color,
ColorComponentParser, Lab, Lch, NumberOrPercentage, Oklab, Oklch, RGBA,
hsl_to_rgb, hwb_to_rgb, AlphaValue, CielabColor, Color, ColorComponentParser, CurrentColor,
DeprecatedColor, Hue, NamedColor, OklabColor, SrgbColor, SystemColor, RGBA,
};
pub use crate::cow_rc_str::CowRcStr;
pub use crate::from_bytes::{stylesheet_encoding, EncodingSupport};
Expand Down
Loading