diff --git a/lib/sass/script/color.rb b/lib/sass/script/color.rb index 7b73f109..617bc849 100644 --- a/lib/sass/script/color.rb +++ b/lib/sass/script/color.rb @@ -80,6 +80,7 @@ module Sass::Script end @attrs = attrs + @attrs[:hue] %= 360 if @attrs[:hue] @attrs[:alpha] ||= 1 end @@ -122,6 +123,30 @@ module Sass::Script @attrs[:blue] end + # The hue component of the color. + # + # @return [Numeric] + def hue + rgb_to_hsl! + @attrs[:hue] + end + + # The saturation component of the color. + # + # @return [Numeric] + def saturation + rgb_to_hsl! + @attrs[:saturation] + end + + # The lightness component of the color. + # + # @return [Numeric] + def lightness + rgb_to_hsl! + @attrs[:lightness] + end + # The alpha channel (opacity) of the color. # This is 1 unless otherwise defined. # @@ -316,23 +341,6 @@ END private - def hsl_to_rgb! - return if @attrs[:red] && @attrs[:blue] && @attrs[:green] - - h = (@attrs[:hue] % 360) / 360.0 - s = @attrs[:saturation] / 100.0 - l = @attrs[:lightness] / 100.0 - - # Algorithm from the CSS3 spec: http://www.w3.org/TR/css3-color/#hsl-color. - m2 = l <= 0.5 ? l * (s + 1) : l + s - l * s - m1 = l * 2 - m2 - @attrs[:red], @attrs[:green], @attrs[:blue] = [ - hue_to_rgb(m1, m2, h + 1.0/3), - hue_to_rgb(m1, m2, h), - hue_to_rgb(m1, m2, h - 1.0/3) - ].map {|c| (c * 0xff).round} - end - def piecewise(other, operation) other_num = other.is_a? Number if other_num && !other.unitless? @@ -352,6 +360,23 @@ END with(:red => result[0], :green => result[1], :blue => result[2]) end + def hsl_to_rgb! + return if @attrs[:red] && @attrs[:blue] && @attrs[:green] + + h = @attrs[:hue] / 360.0 + s = @attrs[:saturation] / 100.0 + l = @attrs[:lightness] / 100.0 + + # Algorithm from the CSS3 spec: http://www.w3.org/TR/css3-color/#hsl-color. + m2 = l <= 0.5 ? l * (s + 1) : l + s - l * s + m1 = l * 2 - m2 + @attrs[:red], @attrs[:green], @attrs[:blue] = [ + hue_to_rgb(m1, m2, h + 1.0/3), + hue_to_rgb(m1, m2, h), + hue_to_rgb(m1, m2, h - 1.0/3) + ].map {|c| (c * 0xff).round} + end + def hue_to_rgb(m1, m2, h) h += 1 if h < 0 h -= 1 if h > 1 @@ -360,5 +385,38 @@ END return m1 + (m2 - m1) * (2.0/3 - h) * 6 if h * 3 < 2 return m1 end + + def rgb_to_hsl! + return if @attrs[:hue] && @attrs[:saturation] && @attrs[:lightness] + r, g, b = [:red, :green, :blue].map {|k| @attrs[k] / 255.0} + + # Algorithm from http://en.wikipedia.org/wiki/HSL_and_HSV#Conversion_from_RGB_to_HSL_or_HSV + max = [r, g, b].max + min = [r, g, b].min + d = max - min + + h = + case max + when min; 0 + when r; 60 * (g-b)/d + when g; 60 * (b-r)/d + 120 + when b; 60 * (r-g)/d + 240 + end + + l = (max + min)/2.0 + + s = + if max == min + 0 + elsif l < 0.5 + d/(2*l) + else + d/(2 - 2*l) + end + + @attrs[:hue] = h % 360 + @attrs[:saturation] = s * 100 + @attrs[:lightness] = l * 100 + end end end diff --git a/test/sass/functions_test.rb b/test/sass/functions_test.rb index 200cf8ae..05e4041c 100644 --- a/test/sass/functions_test.rb +++ b/test/sass/functions_test.rb @@ -9,10 +9,27 @@ class SassFunctionTest < Test::Unit::TestCase File.read(File.dirname(__FILE__) + "/data/hsl-rgb.txt").split("\n\n").each do |chunk| hsls, rgbs = chunk.strip.split("====") hsls.strip.split("\n").zip(rgbs.strip.split("\n")) do |hsl, rgb| - method = "test_hsl: #{hsl} = #{rgb}" - define_method(method) do + hsl_method = "test_hsl: #{hsl} = #{rgb}" + define_method(hsl_method) do assert_equal(evaluate(rgb), evaluate(hsl)) end + + rgb_to_hsl_method = "test_rgb_to_hsl: #{rgb} = #{hsl}" + define_method(rgb_to_hsl_method) do + rgb_color = perform(rgb) + hsl_color = perform(hsl) + + white = hsl_color.lightness == 100 + black = hsl_color.lightness == 0 + grayscale = white || black || hsl_color.saturation == 0 + + assert_in_delta(hsl_color.hue, rgb_color.hue, 0.0001, + "Hues should be equal") unless grayscale + assert_in_delta(hsl_color.saturation, rgb_color.saturation, 0.0001, + "Saturations should be equal") unless white || black + assert_in_delta(hsl_color.lightness, rgb_color.lightness, 0.0001, + "Lightnesses should be equal") + end end end @@ -243,6 +260,10 @@ class SassFunctionTest < Test::Unit::TestCase Sass::Script::Parser.parse(value, 0, 0).perform(Sass::Environment.new).to_s end + def perform(value) + Sass::Script::Parser.parse(value, 0, 0).perform(Sass::Environment.new) + end + def assert_error_message(message, value) evaluate(value) flunk("Error message expected but not raised: #{message}")