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] 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] 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] 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] 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] 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] 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 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