summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorEevee (Alex Munroe) <eevee.git@veekun.com>2013-08-27 14:07:03 -0700
committerEevee (Alex Munroe) <eevee.git@veekun.com>2013-08-27 14:07:03 -0700
commit67182c52db1b7f44881651624388c086f964a8d5 (patch)
tree3c6c6a6ceb79d0895c694b20e5b226c5d27be21c
parenta3eef3ec84543e17849a522daedacdf29cc4f1d7 (diff)
downloadpyscss-67182c52db1b7f44881651624388c086f964a8d5.tar.gz
Fix change_color and scale_color. Fixes #161, #164.
-rw-r--r--scss/functions/core.py130
-rw-r--r--scss/tests/functions/test_core.py2
-rw-r--r--scss/types.py59
3 files changed, 172 insertions, 19 deletions
diff --git a/scss/functions/core.py b/scss/functions/core.py
index 488c528..44ba7ab 100644
--- a/scss/functions/core.py
+++ b/scss/functions/core.py
@@ -3,6 +3,7 @@ Ruby implementation.
"""
from __future__ import absolute_import
+from __future__ import division
import colorsys
import logging
@@ -12,7 +13,7 @@ import operator
from six.moves import xrange
from scss.functions.library import FunctionLibrary
-from scss.types import Boolean, Color, List, Number, String, Map
+from scss.types import Boolean, Color, List, Number, String, Map, expect_type
log = logging.getLogger(__name__)
@@ -50,6 +51,40 @@ def _apply_percentage(n, relto=1):
raise TypeError("Expected unitless number or percentage, got %r" % (n,))
+def _interpret_percentage(n, relto=1.):
+ expect_type(n, Number, unit='%')
+
+ if n.is_unitless:
+ ret = n.value / relto
+ else:
+ ret = n.value / 100.
+
+ if ret < 0:
+ return 0.
+ elif ret > 1:
+ return 1.
+ else:
+ return ret
+
+
+def _interpret_rgb_args(r, g, b):
+ """Given arguments from Sass-land representing red, green, and blue
+ channels, return plain Python numbers appropriate for passing to `Color`
+ constructors.
+ """
+ ret = []
+ for channel in (r, g, b):
+ expect_type(channel, Number, unit='%')
+ if channel.is_simple_unit('%'):
+ value = channel.value / 100.
+ else:
+ value = channel.value / 255.
+
+ ret.append(_constrain(value, 0, 1))
+
+ return ret
+
+
def _color_type(color, a, type):
color = Color(color).value
a = Number(a).value if a is not None else color[3]
@@ -61,13 +96,12 @@ def _color_type(color, a, type):
@register('rgba', 4)
def rgba(r, g, b, a, type='rgba'):
- channels = []
- for ch in (r, g, b):
- channels.append(_constrain(_apply_percentage(ch, relto=255), 0, 255))
+ r = _interpret_percentage(r, relto=255.)
+ g = _interpret_percentage(g, relto=255.)
+ b = _interpret_percentage(b, relto=255.)
+ a = _interpret_percentage(a, relto=1.)
- channels.append(_constrain(_apply_percentage(a), 0, 1))
- channels.append(type)
- return Color(channels)
+ return Color.from_rgb(r, g, b, a)
@register('rgb', 3)
@@ -389,18 +423,90 @@ def _asc_color(op, color, saturation=None, lightness=None, red=None, green=None,
@register('adjust-color')
-def adjust_color(color, saturation=None, lightness=None, red=None, green=None, blue=None, alpha=None):
+def adjust_color(color, red=None, green=None, blue=None, hue=None, saturation=None, lightness=None, alpha=None):
return _asc_color(operator.__add__, color, saturation, lightness, red, green, blue, alpha)
+def _scale_channel(channel, scaleby):
+ if scaleby is None:
+ return channel
+
+ expect_type(scaleby, Number)
+ if not scaleby.is_simple_unit('%'):
+ raise ValueError("Expected percentage, got %r" % (scaleby,))
+
+ factor = scaleby.value / 100
+ if factor > 0:
+ # Add x% of the remaining range, up to 1
+ return channel + (1 - channel) * factor
+ else:
+ # Subtract x% of the existing channel. We add here because the factor
+ # is already negative
+ return channel * (1 + factor)
+
+
@register('scale-color')
-def scale_color(color, saturation=None, lightness=None, red=None, green=None, blue=None, alpha=None):
- return _asc_color(operator.__mul__, color, saturation, lightness, red, green, blue, alpha)
+def scale_color(color, red=None, green=None, blue=None, saturation=None, lightness=None, alpha=None):
+ do_rgb = red or green or blue
+ do_hsl = saturation or lightness
+ if do_rgb and do_hsl:
+ raise ValueError("Can't scale both RGB and HSL channels at the same time")
+
+ scaled_alpha = _scale_channel(color.alpha, alpha)
+
+ if do_rgb:
+ channels = [
+ _scale_channel(channel, scaleby)
+ for channel, scaleby in zip(color.rgba, (red, green, blue))
+ ]
+
+ return Color.from_rgb(*channels, alpha=scaled_alpha)
+
+ else:
+ channels = [
+ _scale_channel(channel, scaleby)
+ for channel, scaleby in zip(color.hsl, (None, saturation, lightness))
+ ]
+
+ return Color.from_hsl(*channels, alpha=scaled_alpha)
@register('change-color')
-def change_color(color, saturation=None, lightness=None, red=None, green=None, blue=None, alpha=None):
- return _asc_color(None, color, saturation, lightness, red, green, blue, alpha)
+def change_color(color, red=None, green=None, blue=None, hue=None, saturation=None, lightness=None, alpha=None):
+ do_rgb = red or green or blue
+ do_hsl = hue or saturation or lightness
+ if do_rgb and do_hsl:
+ raise ValueError("Can't change both RGB and HSL channels at the same time")
+
+ if alpha is None:
+ alpha = color.alpha
+ else:
+ alpha = alpha.value
+
+ if do_rgb:
+ channels = list(color.rgba[:3])
+ if red is not None:
+ channels[0] = _interpret_percentage(red, relto=255.)
+ if green is not None:
+ channels[1] = _interpret_percentage(green, relto=255.)
+ if blue is not None:
+ channels[2] = _interpret_percentage(blue, relto=255.)
+
+ return Color.from_rgb(*channels, alpha=alpha)
+
+ else:
+ channels = list(color.hsl)
+ if hue is not None:
+ expect_type(hue, Number, unit=None)
+ channels[0] = _constrain(hue / 360., 0, 1)
+ # Ruby sass treats plain numbers for saturation and lightness as though
+ # they were percentages, just without the %
+ if saturation is not None:
+ channels[1] = _interpret_percentage(saturation, relto=100.)
+ if lightness is not None:
+ channels[2] = _interpret_percentage(lightness, relto=100.)
+
+ return Color.from_hsl(*channels, alpha=alpha)
# ------------------------------------------------------------------------------
diff --git a/scss/tests/functions/test_core.py b/scss/tests/functions/test_core.py
index 0988bdf..d6207f1 100644
--- a/scss/tests/functions/test_core.py
+++ b/scss/tests/functions/test_core.py
@@ -198,7 +198,6 @@ def test_adjust_color(calc):
assert calc('adjust-color(hsl(25, 100%, 80%), $lightness: -30%, $alpha: -0.4)') == calc('hsla(25, 100%, 50%, 0.6)')
-@xfail(reason="outright broken")
def test_scale_color(calc):
# Examples from the Ruby docs
assert calc('scale-color(hsl(120, 70, 80), $lightness: 50%)') == calc('hsl(120, 70, 90)')
@@ -206,7 +205,6 @@ def test_scale_color(calc):
assert calc('scale-color(hsl(200, 70, 80), $saturation: -90%, $alpha: -30%)') == calc('hsla(200, 7, 80, 0.7)')
-@xfail(reason="outright broken")
def test_change_color(calc):
# Examples from the Ruby docs
assert calc('change-color(#102030, $blue: 5)') == calc('#102005')
diff --git a/scss/types.py b/scss/types.py
index 1a8c654..308a007 100644
--- a/scss/types.py
+++ b/scss/types.py
@@ -2,6 +2,7 @@ from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
+import colorsys
import operator
import six
@@ -666,6 +667,11 @@ class Color(Value):
return self
@classmethod
+ def from_hsl(cls, hue, saturation, lightness, alpha=1.0):
+ r, g, b = colorsys.hls_to_rgb(hue, lightness, saturation)
+ return cls.from_rgb(r, g, b, alpha)
+
+ @classmethod
def from_hex(cls, hex_string):
if not hex_string.startswith('#'):
raise ValueError("Expected #abcdef, got %r" % (hex_string,))
@@ -705,9 +711,25 @@ class Color(Value):
@property
def rgb(self):
+ # TODO: deprecate, relies on internals
return tuple(self.value[:3])
@property
+ def rgba(self):
+ return (
+ self.value[0] / 255,
+ self.value[1] / 255,
+ self.value[2] / 255,
+ self.value[3],
+ )
+
+ @property
+ def hsl(self):
+ rgba = self.rgba
+ h, l, s = colorsys.rgb_to_hls(*rgba[:3])
+ return h, s, l
+
+ @property
def alpha(self):
return self.value[3]
@@ -730,11 +752,11 @@ class Color(Value):
if not isinstance(other, Color):
return Boolean(False)
- # Round to the nearest 5 digits for comparisons; corresponds roughly to
- # 16 bits per channel, the most that generally matters. Otherwise
- # float errors make equality fail for HSL colors.
- left = tuple(round(n, 5) for n in self.value)
- right = tuple(round(n, 5) for n in other.value)
+ # Scale channels to 255 and round to integers; this allows only 8-bit
+ # color, but Ruby sass makes the same assumption, and otherwise it's
+ # easy to get lots of float errors for HSL colors.
+ left = tuple(round(n) for n in self.value)
+ right = tuple(round(n) for n in other.value)
return Boolean(left == right)
def __add__(self, other):
@@ -954,3 +976,30 @@ class Map(Value):
def render(self, compress=False):
raise TypeError("maps cannot be rendered as CSS")
+
+
+def expect_type(value, types, unit=any):
+ if not isinstance(value, types):
+ if isinstance(types, type):
+ types = (type,)
+ sass_type_names = list(set(t.sass_type_name for t in types))
+ sass_type_names.sort()
+
+ # Join with commas in English fashion
+ if len(sass_type_names) == 1:
+ sass_type = sass_type_names[0]
+ elif len(sass_type_names) == 2:
+ sass_type = u' or '.join(sass_type_names)
+ else:
+ sass_type = u', '.join(sass_type_names[:-1])
+ sass_type += u', or ' + sass_type_names[-1]
+
+ raise TypeError("Expected %s, got %r" % (sass_type, value))
+
+ if unit is not any and isinstance(value, Number):
+ if unit is None and not value.is_unitless:
+ raise ValueError("Expected unitless number, got %r" % (value,))
+
+ elif unit == '%' and not (
+ value.is_unitless or value.is_simple_unit('%')):
+ raise ValueError("Expected unitless number or percentage, got %r" % (value,))