blob: 34abe83c4703756e271863c5d4f2f1545fd122ab [file] [log] [blame]
module Sass::Script::Value
# A SassScript object representing a CSS color.
#
# A color may be represented internally as RGBA, HSLA, or both.
# It's originally represented as whatever its input is;
# if it's created with RGB values, it's represented as RGBA,
# and if it's created with HSL values, it's represented as HSLA.
# Once a property is accessed that requires the other representation --
# for example, \{#red} for an HSL color --
# that component is calculated and cached.
#
# The alpha channel of a color is independent of its RGB or HSL representation.
# It's always stored, as 1 if nothing else is specified.
# If only the alpha channel is modified using \{#with},
# the cached RGB and HSL values are retained.
class Color < Base
# @private
#
# Convert a ruby integer to a rgba components
# @param color [Integer]
# @return [Array<Integer>] Array of 4 numbers representing r,g,b and alpha
def self.int_to_rgba(color)
rgba = (0..3).map {|n| color >> (n << 3) & 0xff}.reverse
rgba[-1] = rgba[-1] / 255.0
rgba
end
ALTERNATE_COLOR_NAMES = Sass::Util.map_vals(
{
'aqua' => 0x00FFFFFF,
'darkgrey' => 0xA9A9A9FF,
'darkslategrey' => 0x2F4F4FFF,
'dimgrey' => 0x696969FF,
'fuchsia' => 0xFF00FFFF,
'grey' => 0x808080FF,
'lightgrey' => 0xD3D3D3FF,
'lightslategrey' => 0x778899FF,
'slategrey' => 0x708090FF,
}, &method(:int_to_rgba))
# A hash from color names to `[red, green, blue]` value arrays.
COLOR_NAMES = Sass::Util.map_vals(
{
'aliceblue' => 0xF0F8FFFF,
'antiquewhite' => 0xFAEBD7FF,
'aquamarine' => 0x7FFFD4FF,
'azure' => 0xF0FFFFFF,
'beige' => 0xF5F5DCFF,
'bisque' => 0xFFE4C4FF,
'black' => 0x000000FF,
'blanchedalmond' => 0xFFEBCDFF,
'blue' => 0x0000FFFF,
'blueviolet' => 0x8A2BE2FF,
'brown' => 0xA52A2AFF,
'burlywood' => 0xDEB887FF,
'cadetblue' => 0x5F9EA0FF,
'chartreuse' => 0x7FFF00FF,
'chocolate' => 0xD2691EFF,
'coral' => 0xFF7F50FF,
'cornflowerblue' => 0x6495EDFF,
'cornsilk' => 0xFFF8DCFF,
'crimson' => 0xDC143CFF,
'cyan' => 0x00FFFFFF,
'darkblue' => 0x00008BFF,
'darkcyan' => 0x008B8BFF,
'darkgoldenrod' => 0xB8860BFF,
'darkgray' => 0xA9A9A9FF,
'darkgreen' => 0x006400FF,
'darkkhaki' => 0xBDB76BFF,
'darkmagenta' => 0x8B008BFF,
'darkolivegreen' => 0x556B2FFF,
'darkorange' => 0xFF8C00FF,
'darkorchid' => 0x9932CCFF,
'darkred' => 0x8B0000FF,
'darksalmon' => 0xE9967AFF,
'darkseagreen' => 0x8FBC8FFF,
'darkslateblue' => 0x483D8BFF,
'darkslategray' => 0x2F4F4FFF,
'darkturquoise' => 0x00CED1FF,
'darkviolet' => 0x9400D3FF,
'deeppink' => 0xFF1493FF,
'deepskyblue' => 0x00BFFFFF,
'dimgray' => 0x696969FF,
'dodgerblue' => 0x1E90FFFF,
'firebrick' => 0xB22222FF,
'floralwhite' => 0xFFFAF0FF,
'forestgreen' => 0x228B22FF,
'gainsboro' => 0xDCDCDCFF,
'ghostwhite' => 0xF8F8FFFF,
'gold' => 0xFFD700FF,
'goldenrod' => 0xDAA520FF,
'gray' => 0x808080FF,
'green' => 0x008000FF,
'greenyellow' => 0xADFF2FFF,
'honeydew' => 0xF0FFF0FF,
'hotpink' => 0xFF69B4FF,
'indianred' => 0xCD5C5CFF,
'indigo' => 0x4B0082FF,
'ivory' => 0xFFFFF0FF,
'khaki' => 0xF0E68CFF,
'lavender' => 0xE6E6FAFF,
'lavenderblush' => 0xFFF0F5FF,
'lawngreen' => 0x7CFC00FF,
'lemonchiffon' => 0xFFFACDFF,
'lightblue' => 0xADD8E6FF,
'lightcoral' => 0xF08080FF,
'lightcyan' => 0xE0FFFFFF,
'lightgoldenrodyellow' => 0xFAFAD2FF,
'lightgreen' => 0x90EE90FF,
'lightgray' => 0xD3D3D3FF,
'lightpink' => 0xFFB6C1FF,
'lightsalmon' => 0xFFA07AFF,
'lightseagreen' => 0x20B2AAFF,
'lightskyblue' => 0x87CEFAFF,
'lightslategray' => 0x778899FF,
'lightsteelblue' => 0xB0C4DEFF,
'lightyellow' => 0xFFFFE0FF,
'lime' => 0x00FF00FF,
'limegreen' => 0x32CD32FF,
'linen' => 0xFAF0E6FF,
'magenta' => 0xFF00FFFF,
'maroon' => 0x800000FF,
'mediumaquamarine' => 0x66CDAAFF,
'mediumblue' => 0x0000CDFF,
'mediumorchid' => 0xBA55D3FF,
'mediumpurple' => 0x9370DBFF,
'mediumseagreen' => 0x3CB371FF,
'mediumslateblue' => 0x7B68EEFF,
'mediumspringgreen' => 0x00FA9AFF,
'mediumturquoise' => 0x48D1CCFF,
'mediumvioletred' => 0xC71585FF,
'midnightblue' => 0x191970FF,
'mintcream' => 0xF5FFFAFF,
'mistyrose' => 0xFFE4E1FF,
'moccasin' => 0xFFE4B5FF,
'navajowhite' => 0xFFDEADFF,
'navy' => 0x000080FF,
'oldlace' => 0xFDF5E6FF,
'olive' => 0x808000FF,
'olivedrab' => 0x6B8E23FF,
'orange' => 0xFFA500FF,
'orangered' => 0xFF4500FF,
'orchid' => 0xDA70D6FF,
'palegoldenrod' => 0xEEE8AAFF,
'palegreen' => 0x98FB98FF,
'paleturquoise' => 0xAFEEEEFF,
'palevioletred' => 0xDB7093FF,
'papayawhip' => 0xFFEFD5FF,
'peachpuff' => 0xFFDAB9FF,
'peru' => 0xCD853FFF,
'pink' => 0xFFC0CBFF,
'plum' => 0xDDA0DDFF,
'powderblue' => 0xB0E0E6FF,
'purple' => 0x800080FF,
'red' => 0xFF0000FF,
'rebeccapurple' => 0x663399FF,
'rosybrown' => 0xBC8F8FFF,
'royalblue' => 0x4169E1FF,
'saddlebrown' => 0x8B4513FF,
'salmon' => 0xFA8072FF,
'sandybrown' => 0xF4A460FF,
'seagreen' => 0x2E8B57FF,
'seashell' => 0xFFF5EEFF,
'sienna' => 0xA0522DFF,
'silver' => 0xC0C0C0FF,
'skyblue' => 0x87CEEBFF,
'slateblue' => 0x6A5ACDFF,
'slategray' => 0x708090FF,
'snow' => 0xFFFAFAFF,
'springgreen' => 0x00FF7FFF,
'steelblue' => 0x4682B4FF,
'tan' => 0xD2B48CFF,
'teal' => 0x008080FF,
'thistle' => 0xD8BFD8FF,
'tomato' => 0xFF6347FF,
'transparent' => 0x00000000,
'turquoise' => 0x40E0D0FF,
'violet' => 0xEE82EEFF,
'wheat' => 0xF5DEB3FF,
'white' => 0xFFFFFFFF,
'whitesmoke' => 0xF5F5F5FF,
'yellow' => 0xFFFF00FF,
'yellowgreen' => 0x9ACD32FF
}, &method(:int_to_rgba))
# A hash from `[red, green, blue, alpha]` value arrays to color names.
COLOR_NAMES_REVERSE = COLOR_NAMES.invert.freeze
# We add the alternate color names after inverting because
# different ruby implementations and versions vary on the ordering of the result of invert.
COLOR_NAMES.update(ALTERNATE_COLOR_NAMES).freeze
# The user's original representation of the color.
#
# @return [String]
attr_reader :representation
# Constructs an RGB or HSL color object,
# optionally with an alpha channel.
#
# RGB values are clipped within 0 and 255.
# Saturation and lightness values are clipped within 0 and 100.
# The alpha value is clipped within 0 and 1.
#
# @raise [Sass::SyntaxError] if any color value isn't in the specified range
#
# @overload initialize(attrs)
# The attributes are specified as a hash. This hash must contain either
# `:hue`, `:saturation`, and `:lightness` keys, or `:red`, `:green`, and
# `:blue` keys. It cannot contain both HSL and RGB keys. It may also
# optionally contain an `:alpha` key, and a `:representation` key
# indicating the original representation of the color that the user wrote
# in their stylesheet.
#
# @param attrs [{Symbol => Numeric}] A hash of color attributes to values
# @raise [ArgumentError] if not enough attributes are specified,
# or both RGB and HSL attributes are specified
#
# @overload initialize(rgba, [representation])
# The attributes are specified as an array.
# This overload only supports RGB or RGBA colors.
#
# @param rgba [Array<Numeric>] A three- or four-element array
# of the red, green, blue, and optionally alpha values (respectively)
# of the color
# @param representation [String] The original representation of the color
# that the user wrote in their stylesheet.
# @raise [ArgumentError] if not enough attributes are specified
def initialize(attrs, representation = nil, allow_both_rgb_and_hsl = false)
super(nil)
if attrs.is_a?(Array)
unless (3..4).include?(attrs.size)
raise ArgumentError.new("Color.new(array) expects a three- or four-element array")
end
red, green, blue = attrs[0...3].map {|c| Sass::Util.round(c)}
@attrs = {:red => red, :green => green, :blue => blue}
@attrs[:alpha] = attrs[3] ? attrs[3].to_f : 1
@representation = representation
else
attrs = attrs.reject {|_k, v| v.nil?}
hsl = [:hue, :saturation, :lightness] & attrs.keys
rgb = [:red, :green, :blue] & attrs.keys
if !allow_both_rgb_and_hsl && !hsl.empty? && !rgb.empty?
raise ArgumentError.new("Color.new(hash) may not have both HSL and RGB keys specified")
elsif hsl.empty? && rgb.empty?
raise ArgumentError.new("Color.new(hash) must have either HSL or RGB keys specified")
elsif !hsl.empty? && hsl.size != 3
raise ArgumentError.new("Color.new(hash) must have all three HSL values specified")
elsif !rgb.empty? && rgb.size != 3
raise ArgumentError.new("Color.new(hash) must have all three RGB values specified")
end
@attrs = attrs
@attrs[:hue] %= 360 if @attrs[:hue]
@attrs[:alpha] ||= 1
@representation = @attrs.delete(:representation)
end
[:red, :green, :blue].each do |k|
next if @attrs[k].nil?
@attrs[k] = Sass::Util.restrict(Sass::Util.round(@attrs[k]), 0..255)
end
[:saturation, :lightness].each do |k|
next if @attrs[k].nil?
@attrs[k] = Sass::Util.restrict(@attrs[k], 0..100)
end
@attrs[:alpha] = Sass::Util.restrict(@attrs[:alpha], 0..1)
end
# Create a new color from a valid CSS hex string.
#
# The leading hash is optional.
#
# @return [Color]
def self.from_hex(hex_string, alpha = nil)
unless hex_string =~ /^#?([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/i ||
hex_string =~ /^#?([0-9a-f])([0-9a-f])([0-9a-f])$/i
raise ArgumentError.new("#{hex_string.inspect} is not a valid hex color.")
end
red = $1.ljust(2, $1).to_i(16)
green = $2.ljust(2, $2).to_i(16)
blue = $3.ljust(2, $3).to_i(16)
hex_string = "##{hex_string}" unless hex_string[0] == ?#
attrs = {:red => red, :green => green, :blue => blue, :representation => hex_string}
attrs[:alpha] = alpha if alpha
new(attrs)
end
# The red component of the color.
#
# @return [Integer]
def red
hsl_to_rgb!
@attrs[:red]
end
# The green component of the color.
#
# @return [Integer]
def green
hsl_to_rgb!
@attrs[:green]
end
# The blue component of the color.
#
# @return [Integer]
def blue
hsl_to_rgb!
@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.
#
# @return [Integer]
def alpha
@attrs[:alpha].to_f
end
# Returns whether this color object is translucent;
# that is, whether the alpha channel is non-1.
#
# @return [Boolean]
def alpha?
alpha < 1
end
# Returns the red, green, and blue components of the color.
#
# @return [Array<Integer>] A frozen three-element array of the red, green, and blue
# values (respectively) of the color
def rgb
[red, green, blue].freeze
end
# Returns the red, green, blue, and alpha components of the color.
#
# @return [Array<Integer>] A frozen four-element array of the red, green,
# blue, and alpha values (respectively) of the color
def rgba
[red, green, blue, alpha].freeze
end
# Returns the hue, saturation, and lightness components of the color.
#
# @return [Array<Integer>] A frozen three-element array of the
# hue, saturation, and lightness values (respectively) of the color
def hsl
[hue, saturation, lightness].freeze
end
# Returns the hue, saturation, lightness, and alpha components of the color.
#
# @return [Array<Integer>] A frozen four-element array of the hue,
# saturation, lightness, and alpha values (respectively) of the color
def hsla
[hue, saturation, lightness, alpha].freeze
end
# The SassScript `==` operation.
# **Note that this returns a {Sass::Script::Value::Bool} object,
# not a Ruby boolean**.
#
# @param other [Value] The right-hand side of the operator
# @return [Bool] True if this value is the same as the other,
# false otherwise
def eq(other)
Sass::Script::Value::Bool.new(
other.is_a?(Color) && rgb == other.rgb && alpha == other.alpha)
end
def hash
[rgb, alpha].hash
end
# Returns a copy of this color with one or more channels changed.
# RGB or HSL colors may be changed, but not both at once.
#
# For example:
#
# Color.new([10, 20, 30]).with(:blue => 40)
# #=> rgb(10, 40, 30)
# Color.new([126, 126, 126]).with(:red => 0, :green => 255)
# #=> rgb(0, 255, 126)
# Color.new([255, 0, 127]).with(:saturation => 60)
# #=> rgb(204, 51, 127)
# Color.new([1, 2, 3]).with(:alpha => 0.4)
# #=> rgba(1, 2, 3, 0.4)
#
# @param attrs [{Symbol => Numeric}]
# A map of channel names (`:red`, `:green`, `:blue`,
# `:hue`, `:saturation`, `:lightness`, or `:alpha`) to values
# @return [Color] The new Color object
# @raise [ArgumentError] if both RGB and HSL keys are specified
def with(attrs)
attrs = attrs.reject {|_k, v| v.nil?}
hsl = !([:hue, :saturation, :lightness] & attrs.keys).empty?
rgb = !([:red, :green, :blue] & attrs.keys).empty?
if hsl && rgb
raise ArgumentError.new("Cannot specify HSL and RGB values for a color at the same time")
end
if hsl
[:hue, :saturation, :lightness].each {|k| attrs[k] ||= send(k)}
elsif rgb
[:red, :green, :blue].each {|k| attrs[k] ||= send(k)}
else
# If we're just changing the alpha channel,
# keep all the HSL/RGB stuff we've calculated
attrs = @attrs.merge(attrs)
end
attrs[:alpha] ||= alpha
Color.new(attrs, nil, :allow_both_rgb_and_hsl)
end
# The SassScript `+` operation.
# Its functionality depends on the type of its argument:
#
# {Number}
# : Adds the number to each of the RGB color channels.
#
# {Color}
# : Adds each of the RGB color channels together.
#
# {Value}
# : See {Value::Base#plus}.
#
# @param other [Value] The right-hand side of the operator
# @return [Color] The resulting color
# @raise [Sass::SyntaxError] if `other` is a number with units
def plus(other)
if other.is_a?(Sass::Script::Value::Number) || other.is_a?(Sass::Script::Value::Color)
piecewise(other, :+)
else
super
end
end
# The SassScript `-` operation.
# Its functionality depends on the type of its argument:
#
# {Number}
# : Subtracts the number from each of the RGB color channels.
#
# {Color}
# : Subtracts each of the other color's RGB color channels from this color's.
#
# {Value}
# : See {Value::Base#minus}.
#
# @param other [Value] The right-hand side of the operator
# @return [Color] The resulting color
# @raise [Sass::SyntaxError] if `other` is a number with units
def minus(other)
if other.is_a?(Sass::Script::Value::Number) || other.is_a?(Sass::Script::Value::Color)
piecewise(other, :-)
else
super
end
end
# The SassScript `*` operation.
# Its functionality depends on the type of its argument:
#
# {Number}
# : Multiplies the number by each of the RGB color channels.
#
# {Color}
# : Multiplies each of the RGB color channels together.
#
# @param other [Number, Color] The right-hand side of the operator
# @return [Color] The resulting color
# @raise [Sass::SyntaxError] if `other` is a number with units
def times(other)
if other.is_a?(Sass::Script::Value::Number) || other.is_a?(Sass::Script::Value::Color)
piecewise(other, :*)
else
raise NoMethodError.new(nil, :times)
end
end
# The SassScript `/` operation.
# Its functionality depends on the type of its argument:
#
# {Number}
# : Divides each of the RGB color channels by the number.
#
# {Color}
# : Divides each of this color's RGB color channels by the other color's.
#
# {Value}
# : See {Value::Base#div}.
#
# @param other [Value] The right-hand side of the operator
# @return [Color] The resulting color
# @raise [Sass::SyntaxError] if `other` is a number with units
def div(other)
if other.is_a?(Sass::Script::Value::Number) ||
other.is_a?(Sass::Script::Value::Color)
piecewise(other, :/)
else
super
end
end
# The SassScript `%` operation.
# Its functionality depends on the type of its argument:
#
# {Number}
# : Takes each of the RGB color channels module the number.
#
# {Color}
# : Takes each of this color's RGB color channels modulo the other color's.
#
# @param other [Number, Color] The right-hand side of the operator
# @return [Color] The resulting color
# @raise [Sass::SyntaxError] if `other` is a number with units
def mod(other)
if other.is_a?(Sass::Script::Value::Number) ||
other.is_a?(Sass::Script::Value::Color)
piecewise(other, :%)
else
raise NoMethodError.new(nil, :mod)
end
end
# Returns a string representation of the color.
# This is usually the color's hex value,
# but if the color has a name that's used instead.
#
# @return [String] The string representation
def to_s(opts = {})
return smallest if options[:style] == :compressed
return representation if representation
# IE10 doesn't properly support the color name "transparent", so we emit
# generated transparent colors as rgba(0, 0, 0, 0) in favor of that. See
# #1782.
return rgba_str if Number.basically_equal?(alpha, 0)
return name if name
alpha? ? rgba_str : hex_str
end
alias_method :to_sass, :to_s
# Returns a string representation of the color.
#
# @return [String] The hex value
def inspect
alpha? ? rgba_str : hex_str
end
# Returns the color's name, if it has one.
#
# @return [String, nil]
def name
COLOR_NAMES_REVERSE[rgba]
end
private
def smallest
small_explicit_str = alpha? ? rgba_str : hex_str.gsub(/^#(.)\1(.)\2(.)\3$/, '#\1\2\3')
[representation, COLOR_NAMES_REVERSE[rgba], small_explicit_str].
compact.min_by {|str| str.size}
end
def rgba_str
split = options[:style] == :compressed ? ',' : ', '
"rgba(#{rgb.join(split)}#{split}#{Number.round(alpha)})"
end
def hex_str
red, green, blue = rgb.map {|num| num.to_s(16).rjust(2, '0')}
"##{red}#{green}#{blue}"
end
def operation_name(operation)
case operation
when :+
"add"
when :-
"subtract"
when :*
"multiply"
when :/
"divide"
when :%
"modulo"
end
end
def piecewise(other, operation)
other_num = other.is_a? Number
if other_num && !other.unitless?
raise Sass::SyntaxError.new(
"Cannot #{operation_name(operation)} a number with units (#{other}) to a color (#{self})."
)
end
result = []
(0...3).each do |i|
res = rgb[i].to_f.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
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| Sass::Util.round(c * 0xff)}
end
def hue_to_rgb(m1, m2, h)
h += 1 if h < 0
h -= 1 if h > 1
return m1 + (m2 - m1) * h * 6 if h * 6 < 1
return m2 if h * 2 < 1
return m1 + (m2 - m1) * (2.0 / 3 - h) * 6 if h * 3 < 2
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