mirror of
https://github.com/haml/haml.git
synced 2022-11-09 12:33:31 -05:00
861 lines
28 KiB
Ruby
861 lines
28 KiB
Ruby
module Sass::Script
|
|
# Methods in this module are accessible from the SassScript context.
|
|
# For example, you can write
|
|
#
|
|
# $color = hsl(120deg, 100%, 50%)
|
|
#
|
|
# and it will call {Sass::Script::Functions#hsl}.
|
|
#
|
|
# The following functions are provided:
|
|
#
|
|
# ## RGB Functions
|
|
#
|
|
# \{#rgb}
|
|
# : Converts an `rgb(red, green, blue)` triplet into a color.
|
|
#
|
|
# \{#rgba}
|
|
# : Converts an `rgba(red, green, blue, alpha)` quadruplet into a color.
|
|
#
|
|
# \{#red}
|
|
# : Gets the red component of a color.
|
|
#
|
|
# \{#green}
|
|
# : Gets the green component of a color.
|
|
#
|
|
# \{#blue}
|
|
# : Gets the blue component of a color.
|
|
#
|
|
# \{#mix}
|
|
# : Mixes two colors together.
|
|
#
|
|
# ## HSL Functions
|
|
#
|
|
# \{#hsl}
|
|
# : Converts an `hsl(hue, saturation, lightness)` triplet into a color.
|
|
#
|
|
# \{#hsla}
|
|
# : Converts an `hsla(hue, saturation, lightness, alpha)` quadruplet into a color.
|
|
#
|
|
# \{#hue}
|
|
# : Gets the hue component of a color.
|
|
#
|
|
# \{#saturation}
|
|
# : Gets the saturation component of a color.
|
|
#
|
|
# \{#lightness}
|
|
# : Gets the lightness component of a color.
|
|
#
|
|
# \{#adjust_hue #adjust-hue}
|
|
# : Changes the hue of a color.
|
|
#
|
|
# \{#lighten}
|
|
# : Makes a color lighter.
|
|
#
|
|
# \{#darken}
|
|
# : Makes a color darker.
|
|
#
|
|
# \{#saturate}
|
|
# : Makes a color more saturated.
|
|
#
|
|
# \{#desaturate}
|
|
# : Makes a color less saturated.
|
|
#
|
|
# \{#grayscale}
|
|
# : Converts a color to grayscale.
|
|
#
|
|
# \{#complement}
|
|
# : Returns the complement of a color.
|
|
#
|
|
# ## Opacity Functions
|
|
#
|
|
# \{#alpha} / \{#opacity}
|
|
# : Gets the alpha component (opacity) of a color.
|
|
#
|
|
# \{#rgba}
|
|
# : Sets the alpha component of a color.
|
|
#
|
|
# \{#opacify} / \{#fade_in #fade-in}
|
|
# : Makes a color more opaque.
|
|
#
|
|
# \{#transparentize} / \{#fade_out #fade-out}
|
|
# : Makes a color more transparent.
|
|
#
|
|
# ## String Functions
|
|
#
|
|
# \{#unquote}
|
|
# : Removes the quotes from a string.
|
|
#
|
|
# \{#quote}
|
|
# : Adds quotes to a string.
|
|
#
|
|
# ## Number Functions
|
|
#
|
|
# \{#percentage}
|
|
# : Converts a unitless number to a percentage.
|
|
#
|
|
# \{#round}
|
|
# : Rounds a number to the nearest whole number.
|
|
#
|
|
# \{#ceil}
|
|
# : Rounds a number up to the nearest whole number.
|
|
#
|
|
# \{#floor}
|
|
# : Rounds a number down to the nearest whole number.
|
|
#
|
|
# \{#abs}
|
|
# : Returns the absolute value of a number.
|
|
#
|
|
# ## Introspection Functions
|
|
#
|
|
# \{#type_of}
|
|
# : Returns the type of a value.
|
|
#
|
|
# \{#unit}
|
|
# : Returns the units associated with a number.
|
|
#
|
|
# \{#unitless}
|
|
# : Returns whether a number has units or not.
|
|
#
|
|
# \{#comparable}
|
|
# : Returns whether two numbers can be added or compared.
|
|
#
|
|
# These functions are described in more detail below.
|
|
#
|
|
# ## Adding Custom Functions
|
|
#
|
|
# New Sass functions can be added by adding Ruby methods to this module.
|
|
# For example:
|
|
#
|
|
# module Sass::Script::Functions
|
|
# def reverse(string)
|
|
# assert_type string, :String
|
|
# Sass::Script::String.new(string.value.reverse)
|
|
# end
|
|
# end
|
|
#
|
|
# There are a few things to keep in mind when modifying this module.
|
|
# First of all, the arguments passed are {Sass::Script::Literal} objects.
|
|
# Literal objects are also expected to be returned.
|
|
# This means that Ruby values must be unwrapped and wrapped.
|
|
#
|
|
# Most Literal objects support the {Sass::Script::Literal#value value} accessor
|
|
# for getting their Ruby values.
|
|
# Color objects, though, must be accessed using {Sass::Script::Color#rgb rgb},
|
|
# {Sass::Script::Color#red red}, {Sass::Script::Color#blue green}, or {Sass::Script::Color#blue blue}.
|
|
#
|
|
# Second, making Ruby functions accessible from Sass introduces the temptation
|
|
# to do things like database access within stylesheets.
|
|
# This is generally a bad idea;
|
|
# since Sass files are by default only compiled once,
|
|
# dynamic code is not a great fit.
|
|
#
|
|
# If you really, really need to compile Sass on each request,
|
|
# first make sure you have adequate caching set up.
|
|
# Then you can use {Sass::Engine} to render the code,
|
|
# using the {file:SASS_REFERENCE.md#custom-option `options` parameter}
|
|
# to pass in data that {EvaluationContext#options can be accessed}
|
|
# from your Sass functions.
|
|
#
|
|
# Within one of the functions in this module,
|
|
# methods of {EvaluationContext} can be used.
|
|
#
|
|
# ### Caveats
|
|
#
|
|
# When creating new {Literal} objects within functions,
|
|
# be aware that it's not safe to call {Literal#to_s #to_s}
|
|
# (or other methods that use the string representation)
|
|
# on those objects without first setting {Node#options= the #options attribute}.
|
|
module Functions
|
|
# The context in which methods in {Script::Functions} are evaluated.
|
|
# That means that all instance methods of {EvaluationContext}
|
|
# are available to use in functions.
|
|
class EvaluationContext
|
|
# The options hash for the {Sass::Engine} that is processing the function call
|
|
#
|
|
# @return [{Symbol => Object}]
|
|
attr_reader :options
|
|
|
|
# @param options [{Symbol => Object}] See \{#options}
|
|
def initialize(options)
|
|
@options = options
|
|
|
|
# We need to include this individually in each instance
|
|
# because of an icky Ruby restriction
|
|
class << self; include Sass::Script::Functions; end
|
|
end
|
|
|
|
# Asserts that the type of a given SassScript value
|
|
# is the expected type (designated by a symbol).
|
|
# For example:
|
|
#
|
|
# assert_type value, :String
|
|
# assert_type value, :Number
|
|
#
|
|
# Valid types are `:Bool`, `:Color`, `:Number`, and `:String`.
|
|
# Note that `:String` will match both double-quoted strings
|
|
# and unquoted identifiers.
|
|
#
|
|
# @param value [Sass::Script::Literal] A SassScript value
|
|
# @param type [Symbol] The name of the type the value is expected to be
|
|
def assert_type(value, type)
|
|
return if value.is_a?(Sass::Script.const_get(type))
|
|
raise ArgumentError.new("#{value.inspect} is not a #{type.to_s.downcase}")
|
|
end
|
|
end
|
|
|
|
instance_methods.each { |m| undef_method m unless m.to_s =~ /^__/ }
|
|
|
|
|
|
# Creates a {Color} object from red, green, and blue values.
|
|
#
|
|
# @param red [Number]
|
|
# A number between 0 and 255 inclusive,
|
|
# or between 0% and 100% inclusive
|
|
# @param green [Number]
|
|
# A number between 0 and 255 inclusive,
|
|
# or between 0% and 100% inclusive
|
|
# @param blue [Number]
|
|
# A number between 0 and 255 inclusive,
|
|
# or between 0% and 100% inclusive
|
|
# @see #rgba
|
|
# @return [Color]
|
|
def rgb(red, green, blue)
|
|
assert_type red, :Number
|
|
assert_type green, :Number
|
|
assert_type blue, :Number
|
|
|
|
Color.new([red, green, blue].map do |c|
|
|
v = c.value
|
|
if c.numerator_units == ["%"] && c.denominator_units.empty?
|
|
next v * 255 / 100.0 if (0..100).include?(v)
|
|
raise ArgumentError.new("Color value #{c} must be between 0% and 100% inclusive")
|
|
else
|
|
next v if (0..255).include?(v)
|
|
raise ArgumentError.new("Color value #{v} must be between 0 and 255 inclusive")
|
|
end
|
|
end)
|
|
end
|
|
|
|
# @see #rgb
|
|
# @overload rgba(red, green, blue, alpha)
|
|
# Creates a {Color} object from red, green, and blue values,
|
|
# as well as an alpha channel indicating opacity.
|
|
#
|
|
# @param red [Number]
|
|
# A number between 0 and 255 inclusive
|
|
# @param green [Number]
|
|
# A number between 0 and 255 inclusive
|
|
# @param blue [Number]
|
|
# A number between 0 and 255 inclusive
|
|
# @param alpha [Number]
|
|
# A number between 0 and 1
|
|
# @return [Color]
|
|
#
|
|
# @overload rgba(color, alpha)
|
|
# Sets the opacity of a color.
|
|
#
|
|
# @example
|
|
# rgba(#102030, 0.5) => rgba(16, 32, 48, 0.5)
|
|
# rgba(blue, 0.2) => rgba(0, 0, 255, 0.2)
|
|
#
|
|
# @param color [Color]
|
|
# @param alpha [Number]
|
|
# A number between 0 and 1
|
|
# @return [Color]
|
|
def rgba(*args)
|
|
case args.size
|
|
when 2
|
|
color, alpha = args
|
|
|
|
assert_type color, :Color
|
|
assert_type alpha, :Number
|
|
|
|
unless (0..1).include?(alpha.value)
|
|
raise ArgumentError.new("Alpha channel #{alpha.value} must be between 0 and 1 inclusive")
|
|
end
|
|
|
|
color.with(:alpha => alpha.value)
|
|
when 4
|
|
red, green, blue, alpha = args
|
|
rgba(rgb(red, green, blue), alpha)
|
|
else
|
|
raise ArgumentError.new("wrong number of arguments (#{args.size} for 4)")
|
|
end
|
|
end
|
|
|
|
# Creates a {Color} object from hue, saturation, and lightness.
|
|
# 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
|
|
# @return [Color] The resulting color
|
|
# @see #hsla
|
|
# @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
|
|
# @see #hsl
|
|
# @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%") unless (0..100).include?(s)
|
|
raise ArgumentError.new("Lightness #{l} must be between 0% and 100%") unless (0..100).include?(l)
|
|
|
|
Color.new(:hue => h, :saturation => s, :lightness => l, :alpha => alpha.value)
|
|
end
|
|
|
|
# Returns the red component of a color.
|
|
#
|
|
# @param color [Color]
|
|
# @return [Number]
|
|
# @raise [ArgumentError] If `color` isn't a color
|
|
def red(color)
|
|
assert_type color, :Color
|
|
Sass::Script::Number.new(color.red)
|
|
end
|
|
|
|
# Returns the green component of a color.
|
|
#
|
|
# @param color [Color]
|
|
# @return [Number]
|
|
# @raise [ArgumentError] If `color` isn't a color
|
|
def green(color)
|
|
assert_type color, :Color
|
|
Sass::Script::Number.new(color.green)
|
|
end
|
|
|
|
# Returns the blue component of a color.
|
|
#
|
|
# @param color [Color]
|
|
# @return [Number]
|
|
# @raise [ArgumentError] If `color` isn't a color
|
|
def blue(color)
|
|
assert_type color, :Color
|
|
Sass::Script::Number.new(color.blue)
|
|
end
|
|
|
|
# Returns the hue component of a color.
|
|
#
|
|
# See [the CSS3 HSL specification](http://en.wikipedia.org/wiki/HSL_and_HSV#Conversion_from_RGB_to_HSL_or_HSV).
|
|
#
|
|
# Calculated from RGB where necessary via [this algorithm](http://en.wikipedia.org/wiki/HSL_and_HSV#Conversion_from_RGB_to_HSL_or_HSV).
|
|
#
|
|
# @param color [Color]
|
|
# @return [Number] between 0deg and 360deg
|
|
# @see #adjust_hue
|
|
# @raise [ArgumentError] if `color` isn't a color
|
|
def hue(color)
|
|
assert_type color, :Color
|
|
Sass::Script::Number.new(color.hue, ["deg"])
|
|
end
|
|
|
|
# Returns the saturation component of a color.
|
|
#
|
|
# See [the CSS3 HSL specification](http://en.wikipedia.org/wiki/HSL_and_HSV#Conversion_from_RGB_to_HSL_or_HSV).
|
|
#
|
|
# Calculated from RGB where necessary via [this algorithm](http://en.wikipedia.org/wiki/HSL_and_HSV#Conversion_from_RGB_to_HSL_or_HSV).
|
|
#
|
|
# @param color [Color]
|
|
# @return [Number] between 0% and 100%
|
|
# @see #saturate
|
|
# @see #desaturate
|
|
# @raise [ArgumentError] if `color` isn't a color
|
|
def saturation(color)
|
|
assert_type color, :Color
|
|
Sass::Script::Number.new(color.saturation, ["%"])
|
|
end
|
|
|
|
# Returns the hue component of a color.
|
|
#
|
|
# See [the CSS3 HSL specification](http://en.wikipedia.org/wiki/HSL_and_HSV#Conversion_from_RGB_to_HSL_or_HSV).
|
|
#
|
|
# Calculated from RGB where necessary via [this algorithm](http://en.wikipedia.org/wiki/HSL_and_HSV#Conversion_from_RGB_to_HSL_or_HSV).
|
|
#
|
|
# @param color [Color]
|
|
# @return [Number] between 0% and 100%
|
|
# @see #lighten
|
|
# @see #darken
|
|
# @raise [ArgumentError] if `color` isn't a color
|
|
def lightness(color)
|
|
assert_type color, :Color
|
|
Sass::Script::Number.new(color.lightness, ["%"])
|
|
end
|
|
|
|
# Returns the alpha component (opacity) of a color.
|
|
# This is 1 unless otherwise specified.
|
|
#
|
|
# This function also supports the proprietary Microsoft
|
|
# `alpha(opacity=20)` syntax.
|
|
#
|
|
# @overload def alpha(color)
|
|
# @param color [Color]
|
|
# @return [Number]
|
|
# @see #opacify
|
|
# @see #transparentize
|
|
# @raise [ArgumentError] If `color` isn't a color
|
|
def alpha(*args)
|
|
if args.all? do |a|
|
|
a.is_a?(Sass::Script::String) && a.type == :identifier &&
|
|
a.value =~ /^[a-zA-Z]+\s*=/
|
|
end
|
|
# Support the proprietary MS alpha() function
|
|
return Sass::Script::String.new("alpha(#{args.map {|a| a.to_s}.join(", ")})")
|
|
end
|
|
|
|
opacity(*args)
|
|
end
|
|
|
|
# Returns the alpha component (opacity) of a color.
|
|
# This is 1 unless otherwise specified.
|
|
#
|
|
# @param color [Color]
|
|
# @return [Number]
|
|
# @see #opacify
|
|
# @see #transparentize
|
|
# @raise [ArgumentError] If `color` isn't a color
|
|
def opacity(color)
|
|
assert_type color, :Color
|
|
Sass::Script::Number.new(color.alpha)
|
|
end
|
|
|
|
# Makes a color more opaque.
|
|
# Takes a color and an amount between 0 and 1,
|
|
# and returns a color with the opacity increased by that value.
|
|
#
|
|
# For example:
|
|
#
|
|
# opacify(rgba(0, 0, 0, 0.5), 0.1) => rgba(0, 0, 0, 0.6)
|
|
# opacify(rgba(0, 0, 17, 0.8), 0.2) => #001
|
|
#
|
|
# @param color [Color]
|
|
# @param amount [Number]
|
|
# @return [Color]
|
|
# @see #transparentize
|
|
# @raise [ArgumentError] If `color` isn't a color,
|
|
# or `number` isn't a number between 0 and 1
|
|
def opacify(color, amount)
|
|
adjust(color, amount, :alpha, 0..1, :+)
|
|
end
|
|
alias_method :fade_in, :opacify
|
|
|
|
# Makes a color more transparent.
|
|
# Takes a color and an amount between 0 and 1,
|
|
# and returns a color with the opacity decreased by that value.
|
|
#
|
|
# For example:
|
|
#
|
|
# transparentize(rgba(0, 0, 0, 0.5), 0.1) => rgba(0, 0, 0, 0.4)
|
|
# transparentize(rgba(0, 0, 0, 0.8), 0.2) => rgba(0, 0, 0, 0.6)
|
|
#
|
|
# @param color [Color]
|
|
# @param amount [Number]
|
|
# @return [Color]
|
|
# @see #opacify
|
|
# @raise [ArgumentError] If `color` isn't a color,
|
|
# or `number` isn't a number between 0 and 1
|
|
def transparentize(color, amount)
|
|
adjust(color, amount, :alpha, 0..1, :-)
|
|
end
|
|
alias_method :fade_out, :transparentize
|
|
|
|
# Makes a color lighter.
|
|
# Takes a color and an amount between 0% and 100%,
|
|
# and returns a color with the lightness increased by that value.
|
|
#
|
|
# For example:
|
|
#
|
|
# lighten(hsl(0, 0%, 0%), 30%) => hsl(0, 0, 30)
|
|
# lighten(#800, 20%) => #e00
|
|
#
|
|
# @param color [Color]
|
|
# @param amount [Number]
|
|
# @return [Color]
|
|
# @see #darken
|
|
# @raise [ArgumentError] If `color` isn't a color,
|
|
# or `number` isn't a number between 0% and 100%
|
|
def lighten(color, amount)
|
|
adjust(color, amount, :lightness, 0..100, :+, "%")
|
|
end
|
|
|
|
# Makes a color darker.
|
|
# Takes a color and an amount between 0% and 100%,
|
|
# and returns a color with the lightness decreased by that value.
|
|
#
|
|
# For example:
|
|
#
|
|
# darken(hsl(25, 100%, 80%), 30%) => hsl(25, 100%, 50%)
|
|
# darken(#800, 20%) => #200
|
|
#
|
|
# @param color [Color]
|
|
# @param amount [Number]
|
|
# @return [Color]
|
|
# @see #lighten
|
|
# @raise [ArgumentError] If `color` isn't a color,
|
|
# or `number` isn't a number between 0% and 100%
|
|
def darken(color, amount)
|
|
adjust(color, amount, :lightness, 0..100, :-, "%")
|
|
end
|
|
|
|
# Makes a color more saturated.
|
|
# Takes a color and an amount between 0% and 100%,
|
|
# and returns a color with the saturation increased by that value.
|
|
#
|
|
# For example:
|
|
#
|
|
# saturate(hsl(120, 30%, 90%), 20%) => hsl(120, 50%, 90%)
|
|
# saturate(#855, 20%) => #9e3f3f
|
|
#
|
|
# @param color [Color]
|
|
# @param amount [Number]
|
|
# @return [Color]
|
|
# @see #desaturate
|
|
# @raise [ArgumentError] If `color` isn't a color,
|
|
# or `number` isn't a number between 0% and 100%
|
|
def saturate(color, amount)
|
|
adjust(color, amount, :saturation, 0..100, :+, "%")
|
|
end
|
|
|
|
# Makes a color less saturated.
|
|
# Takes a color and an amount between 0% and 100%,
|
|
# and returns a color with the saturation decreased by that value.
|
|
#
|
|
# For example:
|
|
#
|
|
# desaturate(hsl(120, 30%, 90%), 20%) => hsl(120, 10%, 90%)
|
|
# desaturate(#855, 20%) => #726b6b
|
|
#
|
|
# @param color [Color]
|
|
# @param amount [Number]
|
|
# @return [Color]
|
|
# @see #saturate
|
|
# @raise [ArgumentError] If `color` isn't a color,
|
|
# or `number` isn't a number between 0% and 100%
|
|
def desaturate(color, amount)
|
|
adjust(color, amount, :saturation, 0..100, :-, "%")
|
|
end
|
|
|
|
# Changes the hue of a color while retaining the lightness and saturation.
|
|
# Takes a color and a number of degrees (usually between -360deg and 360deg),
|
|
# and returns a color with the hue rotated by that value.
|
|
#
|
|
# For example:
|
|
#
|
|
# adjust-hue(hsl(120, 30%, 90%), 60deg) => hsl(180, 30%, 90%)
|
|
# adjust-hue(hsl(120, 30%, 90%), 060deg) => hsl(60, 30%, 90%)
|
|
# adjust-hue(#811, 45deg) => #886a11
|
|
#
|
|
# @param color [Color]
|
|
# @param amount [Number]
|
|
# @return [Color]
|
|
# @raise [ArgumentError] If `color` isn't a color, or `number` isn't a number
|
|
def adjust_hue(color, degrees)
|
|
assert_type color, :Color
|
|
assert_type degrees, :Number
|
|
color.with(:hue => color.hue + degrees.value)
|
|
end
|
|
|
|
# Mixes together two colors.
|
|
# Specifically, takes the average of each of the RGB components,
|
|
# optionally weighted by the given percentage.
|
|
# The opacity of the colors is also considered when weighting the components.
|
|
#
|
|
# The weight specifies the amount of the first color that should be included
|
|
# in the returned color.
|
|
# The default, 50%, means that half the first color
|
|
# and half the second color should be used.
|
|
# 25% means that a quarter of the first color
|
|
# and three quarters of the second color should be used.
|
|
#
|
|
# For example:
|
|
#
|
|
# mix(#f00, #00f) => #7f007f
|
|
# mix(#f00, #00f, 25%) => #3f00bf
|
|
# mix(rgba(255, 0, 0, 0.5), #00f) => rgba(63, 0, 191, 0.75)
|
|
#
|
|
# @overload mix(color1, color2, weight = 50%)
|
|
# @param color1 [Color]
|
|
# @param color2 [Color]
|
|
# @param weight [Number] between 0% and 100%
|
|
# @return [Color]
|
|
# @raise [ArgumentError] if `color1` or `color2` aren't colors,
|
|
# or `weight` isn't a number between 0% and 100%
|
|
def mix(color1, color2, weight = Number.new(50))
|
|
assert_type color1, :Color
|
|
assert_type color2, :Color
|
|
assert_type weight, :Number
|
|
|
|
unless (0..100).include?(weight.value)
|
|
raise ArgumentError.new("Weight #{weight} must be between 0% and 100%")
|
|
end
|
|
|
|
# This algorithm factors in both the user-provided weight
|
|
# and the difference between the alpha values of the two colors
|
|
# to decide how to perform the weighted average of the two RGB values.
|
|
#
|
|
# It works by first normalizing both parameters to be within [-1, 1],
|
|
# where 1 indicates "only use color1", -1 indicates "only use color 0",
|
|
# and all values in between indicated a proportionately weighted average.
|
|
#
|
|
# Once we have the normalized variables w and a,
|
|
# we apply the formula (w + a)/(1 + w*a)
|
|
# to get the combined weight (in [-1, 1]) of color1.
|
|
# This formula has two especially nice properties:
|
|
#
|
|
# * When either w or a are -1 or 1, the combined weight is also that number
|
|
# (cases where w * a == -1 are undefined, and handled as a special case).
|
|
#
|
|
# * When a is 0, the combined weight is w, and vice versa
|
|
#
|
|
# Finally, the weight of color1 is renormalized to be within [0, 1]
|
|
# and the weight of color2 is given by 1 minus the weight of color1.
|
|
p = weight.value/100.0
|
|
w = p*2 - 1
|
|
a = color1.alpha - color2.alpha
|
|
|
|
w1 = (((w * a == -1) ? w : (w + a)/(1 + w*a)) + 1)/2.0
|
|
w2 = 1 - w1
|
|
|
|
rgb = color1.rgb.zip(color2.rgb).map {|v1, v2| v1*w1 + v2*w2}
|
|
alpha = color1.alpha*p + color2.alpha*(1-p)
|
|
Color.new(rgb + [alpha])
|
|
end
|
|
|
|
# Converts a color to grayscale.
|
|
# This is identical to `desaturate(color, 100%)`.
|
|
#
|
|
# @param color [Color]
|
|
# @return [Color]
|
|
# @raise [ArgumentError] if `color` isn't a color
|
|
# @see #desaturate
|
|
def grayscale(color)
|
|
desaturate color, Number.new(100)
|
|
end
|
|
|
|
# Returns the complement of a color.
|
|
# This is identical to `adjust-hue(color, 180deg)`.
|
|
#
|
|
# @param color [Color]
|
|
# @return [Color]
|
|
# @raise [ArgumentError] if `color` isn't a color
|
|
# @see #adjust_hue #adjust-hue
|
|
def complement(color)
|
|
adjust_hue color, Number.new(180)
|
|
end
|
|
|
|
# Removes quotes from a string if the string is quoted,
|
|
# or returns the same string if it's not.
|
|
#
|
|
# @param str [String]
|
|
# @return [String]
|
|
# @raise [ArgumentError] if `str` isn't a string
|
|
# @see #quote
|
|
# @example
|
|
# unquote("foo") => foo
|
|
# unquote(foo) => foo
|
|
def unquote(str)
|
|
assert_type str, :String
|
|
Sass::Script::String.new(str.value, :identifier)
|
|
end
|
|
|
|
# Add quotes to a string if the string isn't quoted,
|
|
# or returns the same string if it is.
|
|
#
|
|
# @param str [String]
|
|
# @return [String]
|
|
# @raise [ArgumentError] if `str` isn't a string
|
|
# @see #unquote
|
|
# @example
|
|
# quote("foo") => "foo"
|
|
# quote(foo) => "foo"
|
|
def quote(str)
|
|
assert_type str, :String
|
|
Sass::Script::String.new(str.value, :string)
|
|
end
|
|
|
|
# Inspects the type of the argument, returning it as an unquoted string.
|
|
# For example:
|
|
#
|
|
# type-of(100px) => number
|
|
# type-of(asdf) => string
|
|
# type-of("asdf") => string
|
|
# type-of(true) => bool
|
|
# type-of(#fff) => color
|
|
# type-of(blue) => color
|
|
#
|
|
# @param obj [Literal] The object to inspect
|
|
# @return [String] The unquoted string name of the literal's type
|
|
def type_of(obj)
|
|
Sass::Script::String.new(obj.class.name.gsub(/Sass::Script::/,'').downcase)
|
|
end
|
|
|
|
# Inspects the unit of the number, returning it as a quoted string.
|
|
# Complex units are sorted in alphabetical order by numerator and denominator.
|
|
# For example:
|
|
#
|
|
# unit(100) => ""
|
|
# unit(100px) => "px"
|
|
# unit(3em) => "em"
|
|
# unit(10px * 5em) => "em*px"
|
|
# unit(10px * 5em / 30cm / 1rem) => "em*px/cm*rem"
|
|
#
|
|
# @param number [Literal] The number to inspect
|
|
# @return [String] The unit(s) of the number
|
|
# @raise [ArgumentError] if `number` isn't a number
|
|
def unit(number)
|
|
assert_type number, :Number
|
|
Sass::Script::String.new(number.unit_str, :string)
|
|
end
|
|
|
|
# Inspects the unit of the number, returning a boolean indicating if it is unitless.
|
|
# For example:
|
|
#
|
|
# unitless(100) => true
|
|
# unitless(100px) => false
|
|
#
|
|
# @param number [Literal] The number to inspect
|
|
# @return [Bool] Whether or not the number is unitless
|
|
# @raise [ArgumentError] if `number` isn't a number
|
|
def unitless(number)
|
|
assert_type number, :Number
|
|
Sass::Script::Bool.new(number.unitless?)
|
|
end
|
|
|
|
# Returns true if two numbers are similar enough to be added, subtracted, or compared.
|
|
# For example:
|
|
#
|
|
# comparable(2px, 1px) => true
|
|
# comparable(100px, 3em) => false
|
|
# comparable(10cm, 3mm) => true
|
|
#
|
|
# @param number1 [Number]
|
|
# @param number2 [Number]
|
|
# @return [Bool] indicating if the numbers can be compared.
|
|
# @raise [ArgumentError] if `number1` or `number2` aren't numbers
|
|
def comparable(number1, number2)
|
|
assert_type number1, :Number
|
|
assert_type number2, :Number
|
|
Sass::Script::Bool.new(number1.comparable_to?(number2))
|
|
end
|
|
|
|
# Converts a decimal number to a percentage.
|
|
# For example:
|
|
#
|
|
# percentage(100px / 50px) => 200%
|
|
#
|
|
# @param value [Number] The decimal number to convert to a percentage
|
|
# @return [Number] The percentage
|
|
# @raise [ArgumentError] If `value` isn't a unitless number
|
|
def percentage(value)
|
|
unless value.is_a?(Sass::Script::Number) && value.unitless?
|
|
raise ArgumentError.new("#{value.inspect} is not a unitless number")
|
|
end
|
|
Sass::Script::Number.new(value.value * 100, ['%'])
|
|
end
|
|
|
|
# Rounds a number to the nearest whole number.
|
|
# For example:
|
|
#
|
|
# round(10.4px) => 10px
|
|
# round(10.6px) => 11px
|
|
#
|
|
# @param value [Number] The number
|
|
# @return [Number] The rounded number
|
|
# @raise [Sass::SyntaxError] if `value` isn't a number
|
|
def round(value)
|
|
numeric_transformation(value) {|n| n.round}
|
|
end
|
|
|
|
# Rounds a number up to the nearest whole number.
|
|
# For example:
|
|
#
|
|
# ciel(10.4px) => 11px
|
|
# ciel(10.6px) => 11px
|
|
#
|
|
# @param value [Number] The number
|
|
# @return [Number] The rounded number
|
|
# @raise [Sass::SyntaxError] if `value` isn't a number
|
|
def ceil(value)
|
|
numeric_transformation(value) {|n| n.ceil}
|
|
end
|
|
|
|
# Rounds down to the nearest whole number.
|
|
# For example:
|
|
#
|
|
# floor(10.4px) => 10px
|
|
# floor(10.6px) => 10px
|
|
#
|
|
# @param value [Number] The number
|
|
# @return [Number] The rounded number
|
|
# @raise [Sass::SyntaxError] if `value` isn't a number
|
|
def floor(value)
|
|
numeric_transformation(value) {|n| n.floor}
|
|
end
|
|
|
|
# Finds the absolute value of a number.
|
|
# For example:
|
|
#
|
|
# abs(10px) => 10px
|
|
# abs(-10px) => 10px
|
|
#
|
|
# @param value [Number] The number
|
|
# @return [Number] The absolute value
|
|
# @raise [Sass::SyntaxError] if `value` isn't a number
|
|
def abs(value)
|
|
numeric_transformation(value) {|n| n.abs}
|
|
end
|
|
|
|
private
|
|
|
|
# This method implements the pattern of transforming a numeric value into
|
|
# another numeric value with the same units.
|
|
# It yields a number to a block to perform the operation and return a number
|
|
def numeric_transformation(value)
|
|
assert_type value, :Number
|
|
Sass::Script::Number.new(yield(value.value), value.numerator_units, value.denominator_units)
|
|
end
|
|
|
|
def adjust(color, amount, attr, range, op, units = "")
|
|
assert_type color, :Color
|
|
assert_type amount, :Number
|
|
unless range.include?(amount.value)
|
|
raise ArgumentError.new("Amount #{amount} must be between #{range.first}#{units} and #{range.last}#{units}")
|
|
end
|
|
|
|
# TODO: is it worth restricting here,
|
|
# or should we do so in the Color constructor itself,
|
|
# and allow clipping in rgb() et al?
|
|
color.with(attr => Haml::Util.restrict(
|
|
color.send(attr).send(op, amount.value), range))
|
|
end
|
|
end
|
|
end
|