1
0
Fork 0
mirror of https://github.com/haml/haml.git synced 2022-11-09 12:33:31 -05:00

Merge branch 'rgba'

Closes gh-21
This commit is contained in:
Nathan Weizenbaum 2009-11-11 21:20:18 -08:00
commit 33b9a21c57
7 changed files with 368 additions and 25 deletions

View file

@ -7,10 +7,19 @@
### Colors
Sass now supports functions that return the values of the
Sass now supports colors with alpha channels,
constructed via the {Sass::Script::Functions#rgba rgba}
and {Sass::Script::Functions#hsla hsla} functions.
Alpha channels are unaffected by color arithmetic.
However, the {Sass::Script::Functions#opacify opacify}
and {Sass::Script::Functions#transparentize transparentize} functions
allow colors to be made more and less opaque, respectively.
Sass now also supports functions that return the values of the
{Sass::Script::Functions#red red},
{Sass::Script::Functions#blue blue},
and {Sass::Script::Functions#green green}
{Sass::Script::Functions#green green},
and {Sass::Script::Functions#alpha alpha}
components of colors.
### Variable Names

View file

@ -692,7 +692,7 @@ available in that context.
SassScript supports four data types:
* numbers (e.g. `1.2`, `13`, `10px`)
* strings of text (e.g. `"foo"`, `"bar"`)
* colors (e.g. `blue`, `#04a3f9`)
* colors (e.g. `blue`, `#04a3f9`, `rgba(255, 0, 0, 0.5)`)
* booleans (e.g. `true`, `false`)
Any text that doesn't fit into one of those types
@ -715,6 +715,8 @@ is compiled to:
### Operations
#### Number Operations
SassScript supports the standard arithmetic operations on numbers
(`+`, `-`, `*`, `/`, `%`),
and will automatically convert between units if it can:
@ -734,6 +736,8 @@ and equality operators
(`==`, `!=`)
are supported for all types.
#### Color Operations
All arithmetic operations are supported for color values,
where they work piecewise.
This means that the operation is performed
@ -762,6 +766,40 @@ and is compiled to:
p {
color: #020406; }
Note that colors with an alpha channel
(those created with the {Sass::Script::Functions#rgba rgba}
or {Sass::Script::Functions#hsla hsla} functions)
must have the same alpha value in order for color arithmetic
to be done with them.
The arithmetic doesn't affect the alpha value.
For example:
p
color = rgba(255, 0, 0, 0.75) + rgba(0, 255, 0, 0.75)
is compiled to:
p {
color: rgba(255, 255, 0, 0.75)
The alpha channel of a color can be adjusted using the
{Sass::Script::Functions#opacify opacify} and
{Sass::Script::Functions#transparentize transparentize} functions.
For example:
!translucent-red = rgba(255, 0, 0, 0.5)
p
color = opacify(!translucent-red, 80%)
background-color = transparentize(!translucent-red, 50%)
is compiled to:
p {
color: rgba(255, 0, 0, 0.9)
background-color: rgba(255, 0, 0, 0.25) }
#### String Operations
The `+` operation can be used to concatenate strings:
p

View file

@ -27,6 +27,30 @@ module Sass::Script
# A hash from [red, green, blue] value arrays to color names.
HTML4_COLORS_REVERSE = map_hash(HTML4_COLORS) {|k, v| [v, k]}
# Constructs an RGB or RGBA color object.
# The RGB values must be between 0 and 255,
# and the alpha value is generally expected to be between 0 and 1.
# However, the alpha value can be greater than 1
# in order to allow it to be used for color multiplication.
#
# @param rgba [Array<Numeric>] A three-element array of the red, green, blue,
# and optionally alpha values (respectively) of the color
# @raise [Sass::SyntaxError] if any color value isn't between 0 and 255,
# or the alpha value is negative
def initialize(rgba)
@red, @green, @blue = rgba[0...3].map {|c| c.to_i}
@alpha = rgba[3] ? rgba[3].to_f : 1
super(nil)
unless rgb.all? {|c| (0..255).include?(c)}
raise Sass::SyntaxError.new("Color values must be between 0 and 255")
end
unless (0..1).include?(alpha)
raise Sass::SyntaxError.new("Color opacity value must between 0 and 1")
end
end
# The red component of the color.
#
# @return [Fixnum]
@ -42,16 +66,18 @@ module Sass::Script
# @return [Fixnum]
attr_reader :blue
# @param rgb [Array<Fixnum>] A three-element array of the red, green, and blue values (respectively)
# of the color
# @raise [Sass::SyntaxError] if any color value isn't between 0 and 255
def initialize(rgb)
rgb = rgb.map {|c| c.to_i}
raise Sass::SyntaxError.new("Color values must be between 0 and 255") if rgb.any? {|c| c < 0 || c > 255}
@red = rgb[0]
@green = rgb[1]
@blue = rgb[2]
super(nil)
# The alpha channel (opacity) of the color.
# This is 1 unless otherwise defined.
#
# @return [Fixnum]
attr_accessor :alpha
# Returns whether this color object is translucent;
# that is, whether the alpha channel is non-1.
#
# @return [Boolean]
def alpha?
alpha < 1
end
# @deprecated This will be removed in version 2.6.
@ -81,7 +107,8 @@ END
# @return [Bool] True if this literal is the same as the other,
# false otherwise
def eq(other)
Sass::Script::Bool.new(other.is_a?(Color) && rgb == other.rgb)
Sass::Script::Bool.new(
other.is_a?(Color) && rgb == other.rgb && alpha == other.alpha)
end
# The SassScript `+` operation.
@ -205,6 +232,7 @@ END
#
# @return [String] The string representation
def to_s
return "rgba(#{rgb.join(', ')}, #{alpha % 1 == 0.0 ? alpha.to_i : alpha})" if alpha?
return HTML4_COLORS_REVERSE[rgb] if HTML4_COLORS_REVERSE[rgb]
red, green, blue = rgb.map { |num| num.to_s(16).rjust(2, '0') }
"##{red}#{green}#{blue}"
@ -224,6 +252,12 @@ END
res = rgb[i].send(operation, other_num ? other.value : other.rgb[i])
result[i] = [ [res, 255].min, 0 ].max
end
if !other_num && other.alpha != alpha
raise Sass::SyntaxError.new("Alpha channels must be equal: #{self} #{operation} #{other}")
end
result[3] = alpha
Color.new(result)
end
end

View file

@ -36,11 +36,12 @@ module Sass
# @raise [Sass::SyntaxError] if the function call raises an ArgumentError
def perform(environment)
args = self.args.map {|a| a.perform(environment)}
unless Haml::Util.has?(:public_instance_method, Functions, name) && name !~ /^__/
ruby_name = name.gsub('-', '_')
unless Haml::Util.has?(:public_instance_method, Functions, ruby_name) && ruby_name !~ /^__/
return Script::String.new("#{name}(#{args.map {|a| a.perform(environment)}.join(', ')})")
end
return Functions::EvaluationContext.new(environment.options).send(name, *args)
return Functions::EvaluationContext.new(environment.options).send(ruby_name, *args)
rescue ArgumentError => e
raise e unless e.backtrace.any? {|t| t =~ /:in `(block in )?(#{name}|perform)'$/}
raise Sass::SyntaxError.new("#{e.message} for `#{name}'")

View file

@ -11,11 +11,14 @@ module Sass::Script
# \{#hsl}
# : Converts an `hsl(hue, saturation, lightness)` triplet into a color.
#
# \{#hsla}
# : Converts an `hsla(hue, saturation, lightness, alpha)` quadruplet into a color.
#
# \{#rgb}
# : Converts an `rgb(red, green, blue)` triplet into a color.
#
# \{#percentage}
# : Converts a unitless number to a percentage.
# \{#rgba}
# : Converts an `rgb(red, green, blue, alpha)` triplet into a color.
#
# \{#red}
# : Gets the red component of a color.
@ -26,6 +29,18 @@ module Sass::Script
# \{#blue}
# : Gets the blue component of a color.
#
# \{#alpha} / \{#opacity}
# : Gets the alpha component (opacity) of a color.
#
# \{#opacify} / \{#fade_in #fade-in}
# : Makes a color more opaque.
#
# \{#transparentize} / \{#fade_out #fade-out}
# : Makes a color more transparent.
#
# \{#percentage}
# : Converts a unitless number to a percentage.
#
# \{#round}
# : Rounds a number to the nearest whole number.
#
@ -117,15 +132,36 @@ module Sass::Script
# @param blue
# A number between 0 and 255 inclusive
def rgb(red, green, blue)
rgba(red, green, blue, Number.new(1))
end
# Creates a {Color} object from red, green, and blue values,
# as well as an alpha channel indicating opacity.
#
# @param red
# A number between 0 and 255 inclusive
# @param green
# A number between 0 and 255 inclusive
# @param blue
# A number between 0 and 255 inclusive
# @param alpha
# A number between 0 and 1
def rgba(red, green, blue, alpha)
assert_type red, :Number
assert_type green, :Number
assert_type blue, :Number
assert_type alpha, :Number
[red.value, green.value, blue.value].each do |v|
next unless v < 0 || v > 255
next if (0..255).include?(v)
raise ArgumentError.new("Color value #{v} must be between 0 and 255 inclusive")
end
Color.new([red.value, green.value, blue.value])
unless (0..1).include?(alpha.value)
raise ArgumentError.new("Alpha channel #{alpha.value} must be between 0 and 1 inclusive")
end
Color.new([red.value, green.value, blue.value, alpha.value])
end
# Creates a {Color} object from hue, saturation, and lightness.
@ -140,16 +176,39 @@ module Sass::Script
# @return [Color] The resulting color
# @raise [ArgumentError] if `saturation` or `lightness` are out of bounds
def hsl(hue, saturation, lightness)
hsla(hue, saturation, lightness, Number.new(1))
end
# Creates a {Color} object from hue, saturation, and lightness,
# as well as an alpha channel indicating opacity.
# Uses the algorithm from the [CSS3 spec](http://www.w3.org/TR/css3-color/#hsl-color).
#
# @param hue [Number] The hue of the color.
# Should be between 0 and 360 degrees, inclusive
# @param saturation [Number] The saturation of the color.
# Must be between `0%` and `100%`, inclusive
# @param lightness [Number] The lightness of the color.
# Must be between `0%` and `100%`, inclusive
# @param alpha [Number] The opacity of the color.
# Must be between 0 and 1, inclusive
# @return [Color] The resulting color
# @raise [ArgumentError] if `saturation`, `lightness`, or `alpha` are out of bounds
def hsla(hue, saturation, lightness, alpha)
assert_type hue, :Number
assert_type saturation, :Number
assert_type lightness, :Number
assert_type alpha, :Number
unless (0..1).include?(alpha.value)
raise ArgumentError.new("Alpha channel #{alpha.value} must be between 0 and 1")
end
original_s = saturation
original_l = lightness
# This algorithm is from http://www.w3.org/TR/css3-color#hsl-color
h, s, l = [hue, saturation, lightness].map { |a| a.value }
raise ArgumentError.new("Saturation #{s} must be between 0% and 100%") if s < 0 || s > 100
raise ArgumentError.new("Lightness #{l} must be between 0% and 100%") if l < 0 || l > 100
raise ArgumentError.new("Saturation #{s} must be between 0% and 100%") unless (0..100).include?(s)
raise ArgumentError.new("Lightness #{l} must be between 0% and 100%") unless (0..100).include?(l)
h = (h % 360) / 360.0
s /= 100.0
@ -157,9 +216,11 @@ module Sass::Script
m2 = l <= 0.5 ? l * (s + 1) : l + s - l * s
m1 = l * 2 - m2
Color.new([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 })
Color.new(
[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 } +
[alpha.value])
end
# Returns the red component of a color.
@ -192,6 +253,68 @@ module Sass::Script
Sass::Script::Number.new(color.blue)
end
# Returns the alpha component (opacity) of a color.
# This is 1 unless otherwise specified.
#
# @param color [Color]
# @return [Number]
# @raise [ArgumentError] If `color` isn't a color
def alpha(color)
assert_type color, :Color
Sass::Script::Number.new(color.alpha)
end
alias_method :opacity, :alpha
# Makes a color more opaque.
# Takes a color and an amount between `0%` and `100%`
# and returns a color that's that much closer to opaque.
#
# For example, `50%` will make the color twice as opaque:
#
# opacify(rgba(0, 0, 0, 0.5), 50%) => rgba(0, 0, 0, 0.75)
# opacify(rgba(0, 0, 0, 0.8), 50%) => rgba(0, 0, 0, 0.9)
# opacify(rgba(0, 0, 0, 0.2), 50%) => rgba(0, 0, 0, 0.8)
#
# Specifically, `opacify(color, n%)` will make the color
# `n%` closer to fully opaque.
def opacify(color, amount)
assert_type color, :Color
assert_type amount, :Number
unless (0..100).include?(amount.value)
raise ArgumentError.new("Amount #{amount} must be between 0% and 100%")
end
color = color.dup
color.alpha += (1 - color.alpha) * (amount.value / 100.0)
color
end
alias_method :fade_in, :opacify
# Makes a color more transparent.
# Takes a color and an amount between `0%` and `100%`
# and returns a color that's that much closer to transparent.
#
# For example, `50%` will make the color twice as transparent:
#
# opacify(rgba(0, 0, 0, 0.5), 50%) => rgba(0, 0, 0, 0.25)
# opacify(rgba(0, 0, 0, 0.8), 50%) => rgba(0, 0, 0, 0.4)
# opacify(rgba(0, 0, 0, 0.2), 50%) => rgba(0, 0, 0, 0.1)
#
# Specifically, `transparentize(color, n%)` will make the color
# `n%` closer to fully transparent.
def transparentize(color, amount)
assert_type color, :Color
assert_type amount, :Number
unless (0..100).include?(amount.value)
raise ArgumentError.new("Amount #{amount} must be between 0% and 100%")
end
color = color.dup
color.alpha *= 1 - (amount.value / 100.0)
color
end
alias_method :fade_out, :transparentize
# Converts a decimal number to a percentage.
# For example:
#

View file

@ -27,6 +27,26 @@ class SassFunctionTest < Test::Unit::TestCase
assert_error_message("\"foo\" is not a number for `hsl'", "hsl(10, 10, \"foo\")");
end
def test_hsla
assert_equal "rgba(51, 204, 204, 0.4)", evaluate("hsla(180, 60%, 50%, 0.4)")
assert_equal "#33cccc", evaluate("hsla(180, 60%, 50%, 1)")
assert_equal "rgba(51, 204, 204, 0)", evaluate("hsla(180, 60%, 50%, 0)")
end
def test_hsla_checks_bounds
assert_error_message("Saturation -114 must be between 0% and 100% for `hsla'", "hsla(10, -114, 12, 1)");
assert_error_message("Lightness 256 must be between 0% and 100% for `hsla'", "hsla(10, 10, 256%, 0)");
assert_error_message("Alpha channel -0.1 must be between 0 and 1 for `hsla'", "hsla(10, 10, 10, -0.1)");
assert_error_message("Alpha channel 1.1 must be between 0 and 1 for `hsla'", "hsla(10, 10, 10, 1.1)");
end
def test_hsla_checks_types
assert_error_message("\"foo\" is not a number for `hsla'", "hsla(\"foo\", 10, 12, 0.3)");
assert_error_message("\"foo\" is not a number for `hsla'", "hsla(10, \"foo\", 12, 0)");
assert_error_message("\"foo\" is not a number for `hsla'", "hsla(10, 10, \"foo\", 1)");
assert_error_message("\"foo\" is not a number for `hsla'", "hsla(10, 10, 10, \"foo\")");
end
def test_percentage
assert_equal("50%", evaluate("percentage(.5)"))
assert_equal("100%", evaluate("percentage(1)"))
@ -95,6 +115,36 @@ class SassFunctionTest < Test::Unit::TestCase
assert_error_message("\"foo\" is not a number for `rgb'", "rgb(10, 10, \"foo\")");
end
def test_rgba
assert_equal("rgba(18, 52, 86, 0.5)", evaluate("rgba(18, 52, 86, 0.5)"))
assert_equal("#beaded", evaluate("rgba(190, 173, 237, 1)"))
assert_equal("rgba(0, 255, 127, 0)", evaluate("rgba(0, 255, 127, 0)"))
end
def test_rgb_tests_bounds
assert_error_message("Color value 256 must be between 0 and 255 inclusive for `rgba'",
"rgba(256, 1, 1, 0.3)")
assert_error_message("Color value 256 must be between 0 and 255 inclusive for `rgba'",
"rgba(1, 256, 1, 0.3)")
assert_error_message("Color value 256 must be between 0 and 255 inclusive for `rgba'",
"rgba(1, 1, 256, 0.3)")
assert_error_message("Color value 256 must be between 0 and 255 inclusive for `rgba'",
"rgba(1, 256, 257, 0.3)")
assert_error_message("Color value -1 must be between 0 and 255 inclusive for `rgba'",
"rgba(-1, 1, 1, 0.3)")
assert_error_message("Alpha channel -0.2 must be between 0 and 1 inclusive for `rgba'",
"rgba(1, 1, 1, -0.2)")
assert_error_message("Alpha channel 1.2 must be between 0 and 1 inclusive for `rgba'",
"rgba(1, 1, 1, 1.2)")
end
def test_rgba_tests_types
assert_error_message("\"foo\" is not a number for `rgba'", "rgba(\"foo\", 10, 12, 0.2)");
assert_error_message("\"foo\" is not a number for `rgba'", "rgba(10, \"foo\", 12, 0.1)");
assert_error_message("\"foo\" is not a number for `rgba'", "rgba(10, 10, \"foo\", 0)");
assert_error_message("\"foo\" is not a number for `rgba'", "rgba(10, 10, 10, \"foo\")");
end
def test_red
assert_equal("18", evaluate("red(#123456)"))
end
@ -119,6 +169,56 @@ class SassFunctionTest < Test::Unit::TestCase
assert_error_message("12 is not a color for `blue'", "blue(12)")
end
def test_alpha
assert_equal("1", evaluate("alpha(#123456)"))
assert_equal("0.34", evaluate("alpha(rgba(0, 1, 2, 0.34))"))
assert_equal("0", evaluate("alpha(hsla(0, 1, 2, 0))"))
end
def test_alpha_exception
assert_error_message("12 is not a color for `alpha'", "alpha(12)")
end
def test_opacify
assert_equal("rgba(0, 0, 0, 0.75)", evaluate("opacify(rgba(0, 0, 0, 0.5), 50%)"))
assert_equal("rgba(0, 0, 0, 0.8)", evaluate("opacify(rgba(0, 0, 0, 0.2), 75)"))
assert_equal("rgba(0, 0, 0, 0.28)", evaluate("fade-in(rgba(0, 0, 0, 0.2), 10px)"))
assert_equal("black", evaluate("fade_in(rgba(0, 0, 0, 0.2), 100%)"))
assert_equal("rgba(0, 0, 0, 0.2)", evaluate("opacify(rgba(0, 0, 0, 0.2), 0%)"))
end
def test_opacify_tests_bounds
assert_error_message("Amount -3012% must be between 0% and 100% for `opacify'",
"opacify(rgba(0, 0, 0, 0.2), -3012%)")
assert_error_message("Amount 101 must be between 0% and 100% for `opacify'",
"opacify(rgba(0, 0, 0, 0.2), 101)")
end
def test_opacify_tests_types
assert_error_message("\"foo\" is not a color for `opacify'", "opacify(\"foo\", 10%)")
assert_error_message("\"foo\" is not a number for `opacify'", "opacify(#fff, \"foo\")")
end
def test_transparentize
assert_equal("rgba(0, 0, 0, 0.25)", evaluate("transparentize(rgba(0, 0, 0, 0.5), 50%)"))
assert_equal("rgba(0, 0, 0, 0.05)", evaluate("transparentize(rgba(0, 0, 0, 0.2), 75)"))
assert_equal("rgba(0, 0, 0, 0.18)", evaluate("fade-out(rgba(0, 0, 0, 0.2), 10px)"))
assert_equal("rgba(0, 0, 0, 0)", evaluate("fade_out(rgba(0, 0, 0, 0.2), 100%)"))
assert_equal("rgba(0, 0, 0, 0.2)", evaluate("transparentize(rgba(0, 0, 0, 0.2), 0%)"))
end
def test_transparentize_tests_bounds
assert_error_message("Amount -3012% must be between 0% and 100% for `transparentize'",
"transparentize(rgba(0, 0, 0, 0.2), -3012%)")
assert_error_message("Amount 101 must be between 0% and 100% for `transparentize'",
"transparentize(rgba(0, 0, 0, 0.2), 101)")
end
def test_transparentize_tests_types
assert_error_message("\"foo\" is not a color for `transparentize'", "transparentize(\"foo\", 10%)")
assert_error_message("\"foo\" is not a number for `transparentize'", "transparentize(#fff, \"foo\")")
end
private
def evaluate(value)

View file

@ -10,6 +10,11 @@ class SassScriptTest < Test::Unit::TestCase
assert_raise(Sass::SyntaxError, "Color values must be between 0 and 255") {Color.new([256, 2, 3])}
end
def test_color_checks_rgba_input
assert_raise(Sass::SyntaxError, "Alpha channel must be between 0 and 1") {Color.new([1, 2, 3, 1.1])}
assert_raise(Sass::SyntaxError, "Alpha channel must be between 0 and 1") {Color.new([1, 2, 3, -0.1])}
end
def test_string_escapes
assert_equal '"', resolve("\"\\\"\"")
assert_equal "\\", resolve("\"\\\\\"")
@ -22,6 +27,39 @@ class SassScriptTest < Test::Unit::TestCase
assert_equal "#fffffe", resolve("white - #000001")
end
def test_rgba_color_literals
assert_equal Sass::Script::Color.new([1, 2, 3, 0.75]), eval("rgba(1, 2, 3, 0.75)")
assert_equal "rgba(1, 2, 3, 0.75)", resolve("rgba(1, 2, 3, 0.75)")
assert_equal Sass::Script::Color.new([1, 2, 3, 0]), eval("rgba(1, 2, 3, 0)")
assert_equal "rgba(1, 2, 3, 0)", resolve("rgba(1, 2, 3, 0)")
assert_equal Sass::Script::Color.new([1, 2, 3]), eval("rgba(1, 2, 3, 1)")
assert_equal Sass::Script::Color.new([1, 2, 3, 1]), eval("rgba(1, 2, 3, 1)")
assert_equal "#010203", resolve("rgba(1, 2, 3, 1)")
assert_equal "white", resolve("rgba(255, 255, 255, 1)")
end
def test_rgba_color_math
assert_equal "rgba(50, 50, 100, 0.35)", resolve("rgba(1, 1, 2, 0.35) * rgba(50, 50, 50, 0.35)")
assert_equal "rgba(52, 52, 52, 0.25)", resolve("rgba(2, 2, 2, 0.25) + rgba(50, 50, 50, 0.25)")
assert_raise(Sass::SyntaxError, "Alpha channels must be equal: rgba(1, 2, 3, 0.15) + rgba(50, 50, 50, 0.75)") do
resolve("rgba(1, 2, 3, 0.15) + rgba(50, 50, 50, 0.75)")
end
assert_raise(Sass::SyntaxError, "Alpha channels must be equal: #123456 * rgba(50, 50, 50, 0.75)") do
resolve("#123456 * rgba(50, 50, 50, 0.75)")
end
assert_raise(Sass::SyntaxError, "Alpha channels must be equal: #123456 / #123456") do
resolve("rgba(50, 50, 50, 0.75) / #123456")
end
end
def test_rgba_number_math
assert_equal "rgba(49, 49, 49, 0.75)", resolve("rgba(50, 50, 50, 0.75) - 1")
assert_equal "rgba(100, 100, 100, 0.75)", resolve("rgba(50, 50, 50, 0.75) * 2")
end
def test_implicit_strings
silence_warnings do
assert_equal Sass::Script::String.new("foo"), eval("foo")