This commit is contained in:
@@ -0,0 +1,37 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Sass
|
||||
module Value
|
||||
# Sass's argument list type.
|
||||
#
|
||||
# An argument list comes from a rest argument. It's distinct from a normal {List} in that it may contain a keyword
|
||||
# map as well as the positional arguments.
|
||||
#
|
||||
# @see https://sass-lang.com/documentation/js-api/classes/sassargumentlist/
|
||||
class ArgumentList < List
|
||||
# @param contents [Array<Value>]
|
||||
# @param keywords [Hash<Symbol, Value>]
|
||||
# @param separator [::String]
|
||||
def initialize(contents = [], keywords = {}, separator = ',')
|
||||
super(contents, separator:)
|
||||
|
||||
@id = 0
|
||||
@keywords_accessed = false
|
||||
@keywords = keywords.freeze
|
||||
end
|
||||
|
||||
# @return [Hash<Symbol, Value>]
|
||||
def keywords
|
||||
@keywords_accessed = true
|
||||
@keywords
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def initialize_dup(orig)
|
||||
@id = 0
|
||||
super
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,52 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Sass
|
||||
module Value
|
||||
# Sass's boolean type.
|
||||
#
|
||||
# @see https://sass-lang.com/documentation/js-api/classes/sassboolean/
|
||||
class Boolean
|
||||
include Value
|
||||
|
||||
# @param value [::Boolean]
|
||||
def initialize(value)
|
||||
@value = value
|
||||
end
|
||||
|
||||
# @return [::Boolean]
|
||||
attr_reader :value
|
||||
|
||||
# @return [Boolean]
|
||||
def !
|
||||
value ? Boolean::FALSE : Boolean::TRUE
|
||||
end
|
||||
|
||||
# @return [::Boolean]
|
||||
def ==(other)
|
||||
other.is_a?(Sass::Value::Boolean) && other.value == value
|
||||
end
|
||||
|
||||
# @return [Integer]
|
||||
def hash
|
||||
@hash ||= value.hash
|
||||
end
|
||||
|
||||
alias to_bool value
|
||||
|
||||
# @return [Boolean]
|
||||
def assert_boolean(_name = nil)
|
||||
self
|
||||
end
|
||||
|
||||
# Sass's true value.
|
||||
TRUE = Boolean.new(true)
|
||||
|
||||
# Sass's false value.
|
||||
FALSE = Boolean.new(false)
|
||||
|
||||
def self.new(value)
|
||||
value ? Boolean::TRUE : Boolean::FALSE
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,90 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Sass
|
||||
module Value
|
||||
# Sass's calculation type.
|
||||
#
|
||||
# @see https://sass-lang.com/documentation/js-api/classes/sasscalculation/
|
||||
class Calculation
|
||||
include Value
|
||||
include CalculationValue
|
||||
|
||||
class << self
|
||||
private :new
|
||||
|
||||
# @param argument [CalculationValue]
|
||||
# @return [Calculation]
|
||||
def calc(argument)
|
||||
new('calc', [argument])
|
||||
end
|
||||
|
||||
# @param arguments [Array<CalculationValue>]
|
||||
# @return [Calculation]
|
||||
def min(arguments)
|
||||
new('min', arguments)
|
||||
end
|
||||
|
||||
# @param arguments [Array<CalculationValue>]
|
||||
# @return [Calculation]
|
||||
def max(arguments)
|
||||
new('max', arguments)
|
||||
end
|
||||
|
||||
# @param min [CalculationValue]
|
||||
# @param value [CalculationValue]
|
||||
# @param max [CalculationValue]
|
||||
# @return [Calculation]
|
||||
def clamp(min, value = nil, max = nil)
|
||||
if (value.nil? && !valid_clamp_arg?(min)) ||
|
||||
(max.nil? && [min, value].none? { |x| x && valid_clamp_arg?(x) })
|
||||
raise Sass::ScriptError, 'Argument must be an unquoted SassString.'
|
||||
end
|
||||
|
||||
new('clamp', [min, value, max].compact)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def valid_clamp_arg?(value)
|
||||
value.is_a?(Sass::Value::String) && !value.quoted?
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def initialize(name, arguments)
|
||||
arguments.each do |value|
|
||||
assert_calculation_value(value)
|
||||
end
|
||||
|
||||
@name = name.freeze
|
||||
@arguments = arguments.freeze
|
||||
end
|
||||
|
||||
public
|
||||
|
||||
# @return [::String]
|
||||
attr_reader :name
|
||||
|
||||
# @return [Array<CalculationValue>]
|
||||
attr_reader :arguments
|
||||
|
||||
# @return [Calculation]
|
||||
def assert_calculation(_name = nil)
|
||||
self
|
||||
end
|
||||
|
||||
# @return [::Boolean]
|
||||
def ==(other)
|
||||
other.is_a?(Sass::Value::Calculation) &&
|
||||
other.name == name &&
|
||||
other.arguments == arguments
|
||||
end
|
||||
|
||||
# @return [Integer]
|
||||
def hash
|
||||
@hash ||= [name, *arguments].hash
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,628 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require_relative 'color/channel'
|
||||
require_relative 'color/conversions'
|
||||
require_relative 'color/gamut_map_method'
|
||||
require_relative 'color/interpolation_method'
|
||||
require_relative 'color/space'
|
||||
|
||||
module Sass
|
||||
module Value
|
||||
# Sass's color type.
|
||||
#
|
||||
# No matter what representation was originally used to create this color, all of its channels are accessible.
|
||||
#
|
||||
# @see https://sass-lang.com/documentation/js-api/classes/sasscolor/
|
||||
class Color
|
||||
include Value
|
||||
|
||||
# @param red [Numeric]
|
||||
# @param green [Numeric]
|
||||
# @param blue [Numeric]
|
||||
# @param hue [Numeric]
|
||||
# @param saturation [Numeric]
|
||||
# @param lightness [Numeric]
|
||||
# @param whiteness [Numeric]
|
||||
# @param blackness [Numeric]
|
||||
# @param a [Numeric]
|
||||
# @param b [Numeric]
|
||||
# @param chroma [Numeric]
|
||||
# @param x [Numeric]
|
||||
# @param y [Numeric]
|
||||
# @param z [Numeric]
|
||||
# @param alpha [Numeric]
|
||||
# @param space [::String]
|
||||
# @overload initialize(red: nil, green: nil, blue: nil, alpha: nil, space: 'rgb')
|
||||
# @overload initialize(hue: nil, saturation: nil, lightness: nil, alpha: nil, space: 'hsl')
|
||||
# @overload initialize(hue: nil, whiteness: nil, blackness: nil, alpha: nil, space: 'hwb')
|
||||
# @overload initialize(lightness: nil, a: nil, b: nil, alpha: nil, space: 'lab')
|
||||
# @overload initialize(lightness: nil, a: nil, b: nil, alpha: nil, space: 'oklab')
|
||||
# @overload initialize(lightness: nil, chroma: nil, hue: nil, alpha: nil, space: 'lch')
|
||||
# @overload initialize(lightness: nil, chroma: nil, hue: nil, alpha: nil, space: 'oklch')
|
||||
# @overload initialize(red: nil, green: nil, blue: nil, alpha: nil, space: 'a98-rgb')
|
||||
# @overload initialize(red: nil, green: nil, blue: nil, alpha: nil, space: 'display-p3')
|
||||
# @overload initialize(red: nil, green: nil, blue: nil, alpha: nil, space: 'prophoto-rgb')
|
||||
# @overload initialize(red: nil, green: nil, blue: nil, alpha: nil, space: 'rec2020')
|
||||
# @overload initialize(red: nil, green: nil, blue: nil, alpha: nil, space: 'srgb')
|
||||
# @overload initialize(red: nil, green: nil, blue: nil, alpha: nil, space: 'srgb-linear')
|
||||
# @overload initialize(x: nil, y: nil, z: nil, alpha: nil, space: 'xyz')
|
||||
# @overload initialize(x: nil, y: nil, z: nil, alpha: nil, space: 'xyz-d50')
|
||||
# @overload initialize(x: nil, y: nil, z: nil, alpha: nil, space: 'xyz-d65')
|
||||
def initialize(**options)
|
||||
unless options.key?(:space)
|
||||
options[:space] = case options
|
||||
in {red: _, green: _, blue: _}
|
||||
'rgb'
|
||||
in {hue: _, saturation: _, lightness: _}
|
||||
'hsl'
|
||||
in {hue: _, whiteness: _, blackness: _}
|
||||
'hwb'
|
||||
else
|
||||
raise Sass::ScriptError.new('No color space found', 'space')
|
||||
end
|
||||
end
|
||||
|
||||
space = Space.from_name(options[:space])
|
||||
|
||||
keys = _assert_options(space, options)
|
||||
|
||||
_initialize_for_space_internal(space,
|
||||
options[keys[0]],
|
||||
options[keys[1]],
|
||||
options[keys[2]],
|
||||
options.fetch(:alpha, 1))
|
||||
end
|
||||
|
||||
# @return [::String]
|
||||
def space
|
||||
_space.name
|
||||
end
|
||||
|
||||
# @param space [::String]
|
||||
# @return [Color]
|
||||
def to_space(space)
|
||||
_to_space(Space.from_name(space))
|
||||
end
|
||||
|
||||
# @param space [::String]
|
||||
# @return [::Boolean]
|
||||
def in_gamut?(space = nil)
|
||||
return to_space(space)._in_gamut? unless space.nil?
|
||||
|
||||
_in_gamut?
|
||||
end
|
||||
|
||||
# @param method [::String]
|
||||
# @param space [::String]
|
||||
# @return [Color]
|
||||
def to_gamut(method:, space: nil)
|
||||
return to_space(space).to_gamut(method:)._to_space(_space) unless space.nil?
|
||||
|
||||
_to_gamut(GamutMapMethod.from_name(method, 'method'))
|
||||
end
|
||||
|
||||
# @return [Array<Numeric, nil>]
|
||||
def channels_or_nil
|
||||
[channel0_or_nil, channel1_or_nil, channel2_or_nil].freeze
|
||||
end
|
||||
|
||||
# @return [Array<Numeric>]
|
||||
def channels
|
||||
[channel0, channel1, channel2].freeze
|
||||
end
|
||||
|
||||
# @param channel [::String]
|
||||
# @param space [::String]
|
||||
# @return [Numeric]
|
||||
def channel(channel, space: nil)
|
||||
return to_space(space).channel(channel) unless space.nil?
|
||||
|
||||
channels = _space.channels
|
||||
return channel0 if channel == channels[0].name
|
||||
return channel1 if channel == channels[1].name
|
||||
return channel2 if channel == channels[2].name
|
||||
return alpha if channel == 'alpha'
|
||||
|
||||
raise Sass::ScriptError.new("Color #{self} doesn't have a channel named \"#{channel}\".", channel)
|
||||
end
|
||||
|
||||
# @param channel [::String]
|
||||
# @return [::Boolean]
|
||||
def channel_missing?(channel)
|
||||
channels = _space.channels
|
||||
return channel0_missing? if channel == channels[0].name
|
||||
return channel1_missing? if channel == channels[1].name
|
||||
return channel2_missing? if channel == channels[2].name
|
||||
return alpha_missing? if channel == 'alpha'
|
||||
|
||||
raise Sass::ScriptError.new("Color #{self} doesn't have a channel named \"#{channel}\".", channel)
|
||||
end
|
||||
|
||||
# @param channel [::String]
|
||||
# @param space [::String]
|
||||
# @return [::Boolean]
|
||||
def channel_powerless?(channel, space: nil)
|
||||
return to_space(space).channel_powerless?(channel) unless space.nil?
|
||||
|
||||
channels = _space.channels
|
||||
return channel0_powerless? if channel == channels[0].name
|
||||
return channel1_powerless? if channel == channels[1].name
|
||||
return channel2_powerless? if channel == channels[2].name
|
||||
return false if channel == 'alpha'
|
||||
|
||||
raise Sass::ScriptError.new("Color #{self} doesn't have a channel named \"#{channel}\".", channel)
|
||||
end
|
||||
|
||||
# @param other [Color]
|
||||
# @param method [::String]
|
||||
# @param weight [Numeric]
|
||||
# @return [Color]
|
||||
def interpolate(other, method: nil, weight: nil)
|
||||
interpolation_method = if !method.nil?
|
||||
InterpolationMethod.new(_space, HueInterpolationMethod.from_name(method))
|
||||
elsif !_space.polar?
|
||||
InterpolationMethod.new(_space)
|
||||
else
|
||||
InterpolationMethod.new(_space, :shorter)
|
||||
end
|
||||
_interpolate(other, interpolation_method, weight:)
|
||||
end
|
||||
|
||||
# @param red [Numeric]
|
||||
# @param green [Numeric]
|
||||
# @param blue [Numeric]
|
||||
# @param hue [Numeric]
|
||||
# @param saturation [Numeric]
|
||||
# @param lightness [Numeric]
|
||||
# @param whiteness [Numeric]
|
||||
# @param blackness [Numeric]
|
||||
# @param a [Numeric]
|
||||
# @param b [Numeric]
|
||||
# @param chroma [Numeric]
|
||||
# @param x [Numeric]
|
||||
# @param y [Numeric]
|
||||
# @param z [Numeric]
|
||||
# @param alpha [Numeric]
|
||||
# @param space [::String]
|
||||
# @overload change(red: nil, green: nil, blue: nil, alpha: nil, space: 'rgb')
|
||||
# @overload change(hue: nil, saturation: nil, lightness: nil, alpha: nil, space: 'hsl')
|
||||
# @overload change(hue: nil, whiteness: nil, blackness: nil, alpha: nil, space: 'hwb')
|
||||
# @overload change(lightness: nil, a: nil, b: nil, alpha: nil, space: 'lab')
|
||||
# @overload change(lightness: nil, a: nil, b: nil, alpha: nil, space: 'oklab')
|
||||
# @overload change(lightness: nil, chroma: nil, hue: nil, alpha: nil, space: 'lch')
|
||||
# @overload change(lightness: nil, chroma: nil, hue: nil, alpha: nil, space: 'oklch')
|
||||
# @overload change(red: nil, green: nil, blue: nil, alpha: nil, space: 'a98-rgb')
|
||||
# @overload change(red: nil, green: nil, blue: nil, alpha: nil, space: 'display-p3')
|
||||
# @overload change(red: nil, green: nil, blue: nil, alpha: nil, space: 'prophoto-rgb')
|
||||
# @overload change(red: nil, green: nil, blue: nil, alpha: nil, space: 'rec2020')
|
||||
# @overload change(red: nil, green: nil, blue: nil, alpha: nil, space: 'srgb')
|
||||
# @overload change(red: nil, green: nil, blue: nil, alpha: nil, space: 'srgb-linear')
|
||||
# @overload change(x: nil, y: nil, z: nil, alpha: nil, space: 'xyz')
|
||||
# @overload change(x: nil, y: nil, z: nil, alpha: nil, space: 'xyz-d50')
|
||||
# @overload change(x: nil, y: nil, z: nil, alpha: nil, space: 'xyz-d65')
|
||||
# @return [Color]
|
||||
def change(**options)
|
||||
space_set_explictly = !options[:space].nil?
|
||||
space = space_set_explictly ? Space.from_name(options[:space]) : _space
|
||||
|
||||
if legacy? && !space_set_explictly
|
||||
case options
|
||||
in {whiteness: _} | {blackness: _}
|
||||
space = Space::HWB
|
||||
in {saturation: _} | {lightness: _}
|
||||
space = Space::HSL
|
||||
in {hue: _}
|
||||
space = if _space == Space::HWB
|
||||
Space::HWB
|
||||
else
|
||||
Space::HSL
|
||||
end
|
||||
in {red: _} | {blue: _} | {green: _}
|
||||
space = Space::RGB
|
||||
else
|
||||
end
|
||||
|
||||
if space != _space
|
||||
# deprecated
|
||||
end
|
||||
end
|
||||
|
||||
keys = _assert_options(space, options)
|
||||
|
||||
color = _to_space(space)
|
||||
|
||||
changed_color = if space_set_explictly
|
||||
Color.send(:for_space_internal,
|
||||
space,
|
||||
options.fetch(keys[0], color.channel0_or_nil),
|
||||
options.fetch(keys[1], color.channel1_or_nil),
|
||||
options.fetch(keys[2], color.channel2_or_nil),
|
||||
options.fetch(:alpha, color.alpha_or_nil))
|
||||
else
|
||||
changed_channel0_or_nil = options[keys[0]]
|
||||
changed_channel1_or_nil = options[keys[1]]
|
||||
changed_channel2_or_nil = options[keys[2]]
|
||||
changed_alpha_or_nil = options[:alpha]
|
||||
Color.send(:for_space_internal,
|
||||
space,
|
||||
changed_channel0_or_nil.nil? ? color.channel0_or_nil : changed_channel0_or_nil,
|
||||
changed_channel1_or_nil.nil? ? color.channel1_or_nil : changed_channel1_or_nil,
|
||||
changed_channel2_or_nil.nil? ? color.channel2_or_nil : changed_channel2_or_nil,
|
||||
changed_alpha_or_nil.nil? ? color.alpha_or_nil : changed_alpha_or_nil)
|
||||
end
|
||||
|
||||
changed_color._to_space(_space)
|
||||
end
|
||||
|
||||
# @return [Numeric]
|
||||
def alpha
|
||||
@alpha_or_nil.nil? ? 0 : @alpha_or_nil
|
||||
end
|
||||
|
||||
# @return [::Boolean]
|
||||
def legacy?
|
||||
_space.legacy?
|
||||
end
|
||||
|
||||
# @deprecated
|
||||
# @return [Numeric]
|
||||
def red
|
||||
_to_space(Space::RGB).channel('red').round
|
||||
end
|
||||
|
||||
# @deprecated
|
||||
# @return [Numeric]
|
||||
def green
|
||||
_to_space(Space::RGB).channel('green').round
|
||||
end
|
||||
|
||||
# @deprecated
|
||||
# @return [Numeric]
|
||||
def blue
|
||||
_to_space(Space::RGB).channel('blue').round
|
||||
end
|
||||
|
||||
# @deprecated
|
||||
# @return [Numeric]
|
||||
def hue
|
||||
_to_space(Space::HSL).channel('hue')
|
||||
end
|
||||
|
||||
# @deprecated
|
||||
# @return [Numeric]
|
||||
def saturation
|
||||
_to_space(Space::HSL).channel('saturation')
|
||||
end
|
||||
|
||||
# @deprecated
|
||||
# @return [Numeric]
|
||||
def lightness
|
||||
_to_space(Space::HSL).channel('lightness')
|
||||
end
|
||||
|
||||
# @deprecated
|
||||
# @return [Numeric]
|
||||
def whiteness
|
||||
_to_space(Space::HWB).channel('whiteness')
|
||||
end
|
||||
|
||||
# @deprecated
|
||||
# @return [Numeric]
|
||||
def blackness
|
||||
_to_space(Space::HWB).channel('blackness')
|
||||
end
|
||||
|
||||
# @return [::Boolean]
|
||||
def ==(other)
|
||||
return false unless other.is_a?(Sass::Value::Color)
|
||||
|
||||
if legacy?
|
||||
return false unless other.legacy?
|
||||
return false unless FuzzyMath.equals_nilable(other.alpha_or_nil, alpha_or_nil)
|
||||
|
||||
if _space == other._space
|
||||
FuzzyMath.equals_nilable(other.channel0_or_nil, channel0_or_nil) &&
|
||||
FuzzyMath.equals_nilable(other.channel1_or_nil, channel1_or_nil) &&
|
||||
FuzzyMath.equals_nilable(other.channel2_or_nil, channel2_or_nil)
|
||||
else
|
||||
_to_space(Space::RGB) == other._to_space(Space::RGB)
|
||||
end
|
||||
else
|
||||
other._space == _space &&
|
||||
FuzzyMath.equals_nilable(other.channel0_or_nil, channel0_or_nil) &&
|
||||
FuzzyMath.equals_nilable(other.channel1_or_nil, channel1_or_nil) &&
|
||||
FuzzyMath.equals_nilable(other.channel2_or_nil, channel2_or_nil) &&
|
||||
FuzzyMath.equals_nilable(other.alpha_or_nil, alpha_or_nil)
|
||||
end
|
||||
end
|
||||
|
||||
# @return [Integer]
|
||||
def hash
|
||||
@hash ||= [
|
||||
_space.name,
|
||||
FuzzyMath._hash(channel0_or_nil),
|
||||
FuzzyMath._hash(channel1_or_nil),
|
||||
FuzzyMath._hash(channel2_or_nil),
|
||||
FuzzyMath._hash(alpha_or_nil)
|
||||
].hash
|
||||
end
|
||||
|
||||
# @return [Color]
|
||||
def assert_color(_name = nil)
|
||||
self
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
attr_reader :channel0_or_nil, :channel1_or_nil, :channel2_or_nil, :alpha_or_nil
|
||||
|
||||
def channel0
|
||||
@channel0_or_nil.nil? ? 0 : @channel0_or_nil
|
||||
end
|
||||
|
||||
def channel0_missing?
|
||||
@channel0_or_nil.nil?
|
||||
end
|
||||
|
||||
def channel0_powerless?
|
||||
case _space
|
||||
when Space::HSL
|
||||
FuzzyMath.equals(channel1, 0)
|
||||
when Space::HWB
|
||||
FuzzyMath.greater_than_or_equals(channel1 + channel2, 100)
|
||||
else
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
def channel1
|
||||
@channel1_or_nil.nil? ? 0 : @channel1_or_nil
|
||||
end
|
||||
|
||||
def channel1_missing?
|
||||
@channel1_or_nil.nil?
|
||||
end
|
||||
|
||||
def channel1_powerless?
|
||||
false
|
||||
end
|
||||
|
||||
def channel2
|
||||
@channel2_or_nil.nil? ? 0 : @channel2_or_nil
|
||||
end
|
||||
|
||||
def channel2_missing?
|
||||
@channel2_or_nil.nil?
|
||||
end
|
||||
|
||||
def channel2_powerless?
|
||||
case _space
|
||||
when Space::LCH, Space::OKLCH
|
||||
FuzzyMath.equals(channel1, 0)
|
||||
else
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
def alpha_missing?
|
||||
@alpha_or_nil.nil?
|
||||
end
|
||||
|
||||
def _space
|
||||
@space
|
||||
end
|
||||
|
||||
def _to_space(space)
|
||||
return self if _space == space
|
||||
|
||||
_space.convert(space, channel0_or_nil, channel1_or_nil, channel2_or_nil, alpha)
|
||||
end
|
||||
|
||||
def _in_gamut?
|
||||
return true unless _space.bounded?
|
||||
|
||||
_is_channel_in_gamut(channel0, _space.channels[0]) &&
|
||||
_is_channel_in_gamut(channel1, _space.channels[1]) &&
|
||||
_is_channel_in_gamut(channel2, _space.channels[2])
|
||||
end
|
||||
|
||||
def _to_gamut(method)
|
||||
_in_gamut? ? self : method.map(self)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def _assert_options(space, options)
|
||||
keys = space.channels.map do |channel|
|
||||
channel.name.to_sym
|
||||
end << :alpha << :space
|
||||
options.each_key do |key|
|
||||
unless keys.include?(key)
|
||||
raise Sass::ScriptError.new("`#{key}` is not a valid channel in `#{space.name}`.", key)
|
||||
end
|
||||
end
|
||||
keys
|
||||
end
|
||||
|
||||
def _initialize_for_space_internal(space, channel0, channel1, channel2, alpha = 1)
|
||||
case space
|
||||
when Space::HSL
|
||||
_initialize_for_space(
|
||||
space,
|
||||
_normalize_hue(channel0, invert: !channel1.nil? && FuzzyMath.less_than(channel1, 0)),
|
||||
channel1&.abs,
|
||||
channel2,
|
||||
alpha
|
||||
)
|
||||
when Space::HWB
|
||||
_initialize_for_space(space, _normalize_hue(channel0, invert: false), channel1, channel2, alpha)
|
||||
when Space::LCH, Space::OKLCH
|
||||
_initialize_for_space(
|
||||
space,
|
||||
channel0,
|
||||
channel1&.abs,
|
||||
_normalize_hue(channel2, invert: !channel1.nil? && FuzzyMath.less_than(channel1, 0)),
|
||||
alpha
|
||||
)
|
||||
else
|
||||
_initialize_for_space(space, channel0, channel1, channel2, alpha)
|
||||
end
|
||||
end
|
||||
|
||||
def _initialize_for_space(space, channel0_or_nil, channel1_or_nil, channel2_or_nil, alpha)
|
||||
@space = space
|
||||
@channel0_or_nil = channel0_or_nil
|
||||
@channel1_or_nil = channel1_or_nil
|
||||
@channel2_or_nil = channel2_or_nil
|
||||
@alpha_or_nil = alpha
|
||||
|
||||
FuzzyMath.assert_between(@alpha_or_nil, 0, 1, 'alpha') unless @alpha_or_nil.nil?
|
||||
end
|
||||
|
||||
def _normalize_hue(hue, invert:)
|
||||
return hue if hue.nil?
|
||||
|
||||
((hue % 360) + 360 + (invert ? 180 : 0)) % 360
|
||||
end
|
||||
|
||||
def _is_channel_in_gamut(value, channel)
|
||||
case channel
|
||||
when LinearChannel
|
||||
FuzzyMath.less_than_or_equals(value, channel.max) && FuzzyMath.greater_than_or_equals(value, channel.min)
|
||||
else
|
||||
true
|
||||
end
|
||||
end
|
||||
|
||||
def _interpolate(other, method, weight: nil)
|
||||
weight = 0.5 if weight.nil?
|
||||
if weight.negative? || weight > 1
|
||||
raise Sass::ScriptError.new("Expected #{wieght} to be within 0 and 1.", 'weight')
|
||||
end
|
||||
|
||||
return other if FuzzyMath.equals(weight, 0)
|
||||
return self if FuzzyMath.equals(weight, 1)
|
||||
|
||||
color1 = _to_space(method.space)
|
||||
color2 = other._to_space(method.space)
|
||||
|
||||
c1_missing0 = _analogous_channel_missing?(self, color1, 0)
|
||||
c1_missing1 = _analogous_channel_missing?(self, color1, 1)
|
||||
c1_missing2 = _analogous_channel_missing?(self, color1, 2)
|
||||
c2_missing0 = _analogous_channel_missing?(other, color2, 0)
|
||||
c2_missing1 = _analogous_channel_missing?(other, color2, 1)
|
||||
c2_missing2 = _analogous_channel_missing?(other, color2, 2)
|
||||
c1_channel0 = (c1_missing0 ? color2 : color1).channel0
|
||||
c1_channel1 = (c1_missing1 ? color2 : color1).channel1
|
||||
c1_channel2 = (c1_missing2 ? color2 : color1).channel2
|
||||
c2_channel0 = (c2_missing0 ? color1 : color2).channel0
|
||||
c2_channel1 = (c2_missing1 ? color1 : color2).channel1
|
||||
c2_channel2 = (c2_missing2 ? color1 : color2).channel2
|
||||
c1_alpha = alpha_or_nil.nil? ? other.alpha : alpha_or_nil
|
||||
c2_alpha = other.alpha_or_nil.nil? ? alpha : other.alpha_or_nil
|
||||
|
||||
c1_multiplier = (alpha_or_nil.nil? ? 1 : alpha_or_nil) * weight
|
||||
c2_multiplier = (other.alpha_or_nil.nil? ? 1 : other.alpha_or_nil) * (1 - weight)
|
||||
mixed_alpha = alpha_missing? && other.alpha_missing? ? nil : (c1_alpha * weight) + (c2_alpha * (1 - weight))
|
||||
mixed0 = if c1_missing0 && c2_missing0
|
||||
nil
|
||||
else
|
||||
((c1_channel0 * c1_multiplier) + (c2_channel0 * c2_multiplier)) /
|
||||
(mixed_alpha.nil? ? 1 : mixed_alpha)
|
||||
end
|
||||
mixed1 = if c1_missing1 && c2_missing1
|
||||
nil
|
||||
else
|
||||
((c1_channel1 * c1_multiplier) + (c2_channel1 * c2_multiplier)) /
|
||||
(mixed_alpha.nil? ? 1 : mixed_alpha)
|
||||
end
|
||||
mixed2 = if c1_missing2 && c2_missing2
|
||||
nil
|
||||
else
|
||||
((c1_channel2 * c1_multiplier) + (c2_channel2 * c2_multiplier)) /
|
||||
(mixed_alpha.nil? ? 1 : mixed_alpha)
|
||||
end
|
||||
|
||||
case method.space
|
||||
when Space::HSL, Space::HWB
|
||||
Color.send(:for_space_internal,
|
||||
method.space,
|
||||
c1_missing0 && c2_missing0 ? nil : _interpolate_hues(c1_channel0, c2_channel0, method.hue, weight),
|
||||
mixed1,
|
||||
mixed2,
|
||||
mixed_alpha)
|
||||
when Space::LCH, Space::OKLCH
|
||||
Color.send(:for_space_internal,
|
||||
method.space,
|
||||
mixed0,
|
||||
mixed1,
|
||||
c1_missing2 && c2_missing2 ? nil : _interpolate_hues(c1_channel2, c2_channel2, method.hue, weight),
|
||||
mixed_alpha)
|
||||
else
|
||||
Color.send(:_for_space,
|
||||
method.space, mixed0, mixed1, mixed2, mixed_alpha)
|
||||
end._to_space(_space)
|
||||
end
|
||||
|
||||
def _analogous_channel_missing?(original, output, output_channel_index)
|
||||
return true if output.channels_or_nil[output_channel_index].nil?
|
||||
|
||||
return false if original.equal?(output)
|
||||
|
||||
output_channel = output._space.channels[output_channel_index]
|
||||
original_channel = original._space.channels.find do |channel|
|
||||
output_channel.analogous?(channel)
|
||||
end
|
||||
|
||||
return false if original_channel.nil?
|
||||
|
||||
original.channel_missing?(original_channel.name)
|
||||
end
|
||||
|
||||
def _interpolate_hues(hue1, hue2, method, weight)
|
||||
case method
|
||||
when :shorter
|
||||
diff = hue2 - hue1
|
||||
if diff > 180
|
||||
hue1 += 360
|
||||
elsif diff < -180
|
||||
hue2 += 360
|
||||
end
|
||||
when :longer
|
||||
diff = hue2 - hue1
|
||||
if diff.positive? && diff < 180
|
||||
hue2 += 360
|
||||
elsif diff > -180 && diff <= 0
|
||||
hue1 += 360
|
||||
end
|
||||
when :increasing
|
||||
hue2 += 360 if hue2 < hue1
|
||||
when :decreasing
|
||||
hue1 += 360 if hue1 < hue2
|
||||
end
|
||||
|
||||
(hue1 * weight) + (hue2 * (1 - weight))
|
||||
end
|
||||
|
||||
class << self
|
||||
private
|
||||
|
||||
def for_space(space, channel0_or_nil, channel1_or_nil, channel2_or_nil, alpha)
|
||||
_for_space(Space.from_name(space), channel0_or_nil, channel1_or_nil, channel2_or_nil, alpha)
|
||||
end
|
||||
|
||||
def for_space_internal(space, channel0, channel1, channel2, alpha)
|
||||
o = allocate
|
||||
o.send(:_initialize_for_space_internal, space, channel0, channel1, channel2, alpha)
|
||||
o
|
||||
end
|
||||
|
||||
def _for_space(space, channel0_or_nil, channel1_or_nil, channel2_or_nil, alpha)
|
||||
o = allocate
|
||||
o.send(:_initialize_for_space, space, channel0_or_nil, channel1_or_nil, channel2_or_nil, alpha)
|
||||
o
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,79 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Sass
|
||||
module Value
|
||||
class Color
|
||||
# @see https://github.com/sass/dart-sass/blob/main/lib/src/value/color/channel.dart
|
||||
class ColorChannel
|
||||
# @return [::String]
|
||||
attr_reader :name
|
||||
|
||||
# @return [::Boolean]
|
||||
def polar_angle?
|
||||
@polar_angle
|
||||
end
|
||||
|
||||
# @return [::String, nil]
|
||||
attr_reader :associated_unit
|
||||
|
||||
# @param name [::String]
|
||||
# @param polar_angle [::Boolean]
|
||||
# @param associated_unit [::String]
|
||||
def initialize(name, polar_angle:, associated_unit: nil)
|
||||
@name = name
|
||||
@polar_angle = polar_angle
|
||||
@associated_unit = associated_unit
|
||||
end
|
||||
|
||||
# @return [::Boolean]
|
||||
def analogous?(other)
|
||||
case [name, other.name]
|
||||
in ['red' | 'x', 'red' | 'x'] |
|
||||
['green' | 'y', 'green' | 'y'] |
|
||||
['blue' | 'z', 'blue' | 'z'] |
|
||||
['chroma' | 'saturation', 'chroma' | 'saturation'] |
|
||||
['lightness', 'lightness'] |
|
||||
['hue', 'hue']
|
||||
true
|
||||
else
|
||||
false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private_constant :ColorChannel
|
||||
|
||||
# @see https://github.com/sass/dart-sass/blob/main/lib/src/value/color/channel.dart
|
||||
class LinearChannel < ColorChannel
|
||||
# @return [Numeric]
|
||||
attr_reader :min, :max
|
||||
|
||||
# @return [::Boolean]
|
||||
attr_reader :requires_percent, :lower_clamped, :upper_clamped
|
||||
|
||||
# @param name [::String]
|
||||
# @param min [Numeric]
|
||||
# @param max [Numeric]
|
||||
# @param requires_percent [::Boolean]
|
||||
# @param lower_clamped [::Boolean]
|
||||
# @param upper_clamped [::Boolean]
|
||||
# @param conventionally_percent [::Boolean]
|
||||
def initialize(name, min, max, requires_percent: false, lower_clamped: false, upper_clamped: false,
|
||||
conventionally_percent: nil)
|
||||
super(name,
|
||||
polar_angle: false,
|
||||
associated_unit: if conventionally_percent.nil? ? (min.zero? && max == 100) : conventionally_percent
|
||||
'%'
|
||||
end)
|
||||
@min = min
|
||||
@max = max
|
||||
@requires_percent = requires_percent
|
||||
@lower_clamped = lower_clamped
|
||||
@upper_clamped = upper_clamped
|
||||
end
|
||||
end
|
||||
|
||||
private_constant :LinearChannel
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,473 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Sass
|
||||
module Value
|
||||
class Color
|
||||
# @see https:#www.w3.org/TR/css-color-4/#color-conversion-code.
|
||||
# @see https://github.com/sass/dart-sass/blob/main/lib/src/value/color/conversions.dart
|
||||
module Conversions
|
||||
# The D50 white point.
|
||||
#
|
||||
# Definition from https://www.w3.org/TR/css-color-4/#color-conversion-code.
|
||||
D50 = [0.3457 / 0.3585, 1.00000, (1.0 - 0.3457 - 0.3585) / 0.3585].freeze
|
||||
|
||||
# Matrix values from https://www.w3.org/TR/css-color-4/#color-conversion-code.
|
||||
|
||||
# The transformation matrix for converting LMS colors to OKLab.
|
||||
#
|
||||
# Note that this can't be directly multiplied with [XYZ_D65_TO_LMS]; see Color
|
||||
# Level 4 spec for details on how to convert between XYZ and OKLab.
|
||||
LMS_TO_OKLAB = [
|
||||
0.21045426830931400, 0.79361777470230540, -0.00407204301161930,
|
||||
1.97799853243116840, -2.42859224204858000, 0.45059370961741100,
|
||||
0.02590404246554780, 0.78277171245752960, -0.80867575492307740
|
||||
].freeze
|
||||
|
||||
# The transformation matrix for converting OKLab colors to LMS.
|
||||
#
|
||||
# Note that this can't be directly multiplied with [LMS_TO_XYZ_D65]; see Color
|
||||
# Level 4 spec for details on how to convert between XYZ and OKLab.
|
||||
OKLAB_TO_LMS = [
|
||||
1.00000000000000020, 0.39633777737617490, 0.21580375730991360,
|
||||
0.99999999999999980, -0.10556134581565854, -0.06385417282581334,
|
||||
0.99999999999999990, -0.08948417752981180, -1.29148554801940940
|
||||
].freeze
|
||||
|
||||
# The following matrices were precomputed using
|
||||
# https://gist.github.com/nex3/3d7ecfef467b22e02e7a666db1b8a316.
|
||||
|
||||
# The transformation matrix for converting linear-light srgb colors to
|
||||
# linear-light display-p3.
|
||||
LINEAR_SRGB_TO_LINEAR_DISPLAY_P3 = [
|
||||
0.82246196871436230, 0.17753803128563775, 0.00000000000000000,
|
||||
0.03319419885096161, 0.96680580114903840, 0.00000000000000000,
|
||||
0.01708263072112003, 0.07239744066396346, 0.91051992861491650
|
||||
].freeze
|
||||
|
||||
# The transformation matrix for converting linear-light display-p3 colors to
|
||||
# linear-light srgb.
|
||||
LINEAR_DISPLAY_P3_TO_LINEAR_SRGB = [
|
||||
1.22494017628055980, -0.22494017628055996, 0.00000000000000000,
|
||||
-0.04205695470968816, 1.04205695470968800, 0.00000000000000000,
|
||||
-0.01963755459033443, -0.07863604555063188, 1.09827360014096630
|
||||
].freeze
|
||||
|
||||
# The transformation matrix for converting linear-light srgb colors to
|
||||
# linear-light a98-rgb.
|
||||
LINEAR_SRGB_TO_LINEAR_A98_RGB = [
|
||||
0.71512560685562470, 0.28487439314437535, 0.00000000000000000,
|
||||
0.00000000000000000, 1.00000000000000000, 0.00000000000000000,
|
||||
0.00000000000000000, 0.04116194845011846, 0.95883805154988160
|
||||
].freeze
|
||||
|
||||
# The transformation matrix for converting linear-light a98-rgb colors to
|
||||
# linear-light srgb.
|
||||
LINEAR_A98_RGB_TO_LINEAR_SRGB = [
|
||||
1.39835574396077830, -0.39835574396077830, 0.00000000000000000,
|
||||
0.00000000000000000, 1.00000000000000000, 0.00000000000000000,
|
||||
0.00000000000000000, -0.04292898929447326, 1.04292898929447330
|
||||
].freeze
|
||||
|
||||
# The transformation matrix for converting linear-light srgb colors to
|
||||
# linear-light rec2020.
|
||||
LINEAR_SRGB_TO_LINEAR_REC2020 = [
|
||||
0.62740389593469900, 0.32928303837788370, 0.04331306568741722,
|
||||
0.06909728935823208, 0.91954039507545870, 0.01136231556630917,
|
||||
0.01639143887515027, 0.08801330787722575, 0.89559525324762400
|
||||
].freeze
|
||||
|
||||
# The transformation matrix for converting linear-light rec2020 colors to
|
||||
# linear-light srgb.
|
||||
LINEAR_REC2020_TO_LINEAR_SRGB = [
|
||||
1.66049100210843450, -0.58764113878854950, -0.07284986331988487,
|
||||
-0.12455047452159074, 1.13289989712596030, -0.00834942260436947,
|
||||
-0.01815076335490530, -0.10057889800800737, 1.11872966136291270
|
||||
].freeze
|
||||
|
||||
# The transformation matrix for converting linear-light srgb colors to xyz.
|
||||
LINEAR_SRGB_TO_XYZ_D65 = [
|
||||
0.41239079926595950, 0.35758433938387796, 0.18048078840183430,
|
||||
0.21263900587151036, 0.71516867876775590, 0.07219231536073371,
|
||||
0.01933081871559185, 0.11919477979462598, 0.95053215224966060
|
||||
].freeze
|
||||
|
||||
# The transformation matrix for converting xyz colors to linear-light srgb.
|
||||
XYZ_D65_TO_LINEAR_SRGB = [
|
||||
3.24096994190452130, -1.53738317757009350, -0.49861076029300330,
|
||||
-0.96924363628087980, 1.87596750150772060, 0.04155505740717561,
|
||||
0.05563007969699360, -0.20397695888897657, 1.05697151424287860
|
||||
].freeze
|
||||
|
||||
# The transformation matrix for converting linear-light srgb colors to lms.
|
||||
LINEAR_SRGB_TO_LMS = [
|
||||
0.41222146947076300, 0.53633253726173480, 0.05144599326750220,
|
||||
0.21190349581782520, 0.68069955064523420, 0.10739695353694050,
|
||||
0.08830245919005641, 0.28171883913612150, 0.62997870167382210
|
||||
].freeze
|
||||
|
||||
# The transformation matrix for converting lms colors to linear-light srgb.
|
||||
LMS_TO_LINEAR_SRGB = [
|
||||
4.07674163607595800, -3.30771153925806200, 0.23096990318210417,
|
||||
-1.26843797328503200, 2.60975734928768900, -0.34131937600265710,
|
||||
-0.00419607613867551, -0.70341861793593630, 1.70761469407461200
|
||||
].freeze
|
||||
|
||||
# The transformation matrix for converting linear-light srgb colors to
|
||||
# linear-light prophoto-rgb.
|
||||
LINEAR_SRGB_TO_LINEAR_PROPHOTO_RGB = [
|
||||
0.52927697762261160, 0.33015450197849283, 0.14056852039889556,
|
||||
0.09836585954044917, 0.87347071290696180, 0.02816342755258900,
|
||||
0.01687534092138684, 0.11765941425612084, 0.86546524482249230
|
||||
].freeze
|
||||
|
||||
# The transformation matrix for converting linear-light prophoto-rgb colors to
|
||||
# linear-light srgb.
|
||||
LINEAR_PROPHOTO_RGB_TO_LINEAR_SRGB = [
|
||||
2.03438084951699600, -0.72763578993413420, -0.30674505958286180,
|
||||
-0.22882573163305037, 1.23174254119010480, -0.00291680955705449,
|
||||
-0.00855882878391742, -0.15326670213803720, 1.16182553092195470
|
||||
].freeze
|
||||
|
||||
# The transformation matrix for converting linear-light srgb colors to
|
||||
# xyz-d50.
|
||||
LINEAR_SRGB_TO_XYZ_D50 = [
|
||||
0.43606574687426936, 0.38515150959015960, 0.14307841996513868,
|
||||
0.22249317711056518, 0.71688701309448240, 0.06061980979495235,
|
||||
0.01392392146316939, 0.09708132423141015, 0.71409935681588070
|
||||
].freeze
|
||||
|
||||
# The transformation matrix for converting xyz-d50 colors to linear-light
|
||||
# srgb.
|
||||
XYZ_D50_TO_LINEAR_SRGB = [
|
||||
3.13413585290011780, -1.61738599801804200, -0.49066221791109754,
|
||||
-0.97879547655577770, 1.91625437739598840, 0.03344287339036693,
|
||||
0.07195539255794733, -0.22897675981518200, 1.40538603511311820
|
||||
].freeze
|
||||
|
||||
# The transformation matrix for converting linear-light display-p3 colors to
|
||||
# linear-light a98-rgb.
|
||||
LINEAR_DISPLAY_P3_TO_LINEAR_A98_RGB = [
|
||||
0.86400513747404840, 0.13599486252595164, 0.00000000000000000,
|
||||
-0.04205695470968816, 1.04205695470968800, 0.00000000000000000,
|
||||
-0.02056038078232985, -0.03250613804550798, 1.05306651882783790
|
||||
].freeze
|
||||
|
||||
# The transformation matrix for converting linear-light a98-rgb colors to
|
||||
# linear-light display-p3.
|
||||
LINEAR_A98_RGB_TO_LINEAR_DISPLAY_P3 = [
|
||||
1.15009441814101840, -0.15009441814101834, 0.00000000000000000,
|
||||
0.04641729862941844, 0.95358270137058150, 0.00000000000000000,
|
||||
0.02388759479083904, 0.02650477632633013, 0.94960762888283080
|
||||
].freeze
|
||||
|
||||
# The transformation matrix for converting linear-light display-p3 colors to
|
||||
# linear-light rec2020.
|
||||
LINEAR_DISPLAY_P3_TO_LINEAR_REC2020 = [
|
||||
0.75383303436172180, 0.19859736905261630, 0.04756959658566187,
|
||||
0.04574384896535833, 0.94177721981169350, 0.01247893122294812,
|
||||
-0.00121034035451832, 0.01760171730108989, 0.98360862305342840
|
||||
].freeze
|
||||
|
||||
# The transformation matrix for converting linear-light rec2020 colors to
|
||||
# linear-light display-p3.
|
||||
LINEAR_REC2020_TO_LINEAR_DISPLAY_P3 = [
|
||||
1.34357825258433200, -0.28217967052613570, -0.06139858205819628,
|
||||
-0.06529745278911953, 1.07578791584857460, -0.01049046305945495,
|
||||
0.00282178726170095, -0.01959849452449406, 1.01677670726279310
|
||||
].freeze
|
||||
|
||||
# The transformation matrix for converting linear-light display-p3 colors to
|
||||
# xyz.
|
||||
LINEAR_DISPLAY_P3_TO_XYZ_D65 = [
|
||||
0.48657094864821626, 0.26566769316909294, 0.19821728523436250,
|
||||
0.22897456406974884, 0.69173852183650620, 0.07928691409374500,
|
||||
0.00000000000000000, 0.04511338185890257, 1.04394436890097570
|
||||
].freeze
|
||||
|
||||
# The transformation matrix for converting xyz colors to linear-light
|
||||
# display-p3.
|
||||
XYZ_D65_TO_LINEAR_DISPLAY_P3 = [
|
||||
2.49349691194142450, -0.93138361791912360, -0.40271078445071684,
|
||||
-0.82948896956157490, 1.76266406031834680, 0.02362468584194359,
|
||||
0.03584583024378433, -0.07617238926804170, 0.95688452400768730
|
||||
].freeze
|
||||
|
||||
# The transformation matrix for converting linear-light display-p3 colors to
|
||||
# lms.
|
||||
LINEAR_DISPLAY_P3_TO_LMS = [
|
||||
0.48137985274995443, 0.46211837101131803, 0.05650177623872756,
|
||||
0.22883194181124472, 0.65321681938356760, 0.11795123880518774,
|
||||
0.08394575232299319, 0.22416527097756642, 0.69188897669944040
|
||||
].freeze
|
||||
|
||||
# The transformation matrix for converting lms colors to linear-light
|
||||
# display-p3.
|
||||
LMS_TO_LINEAR_DISPLAY_P3 = [
|
||||
3.12776897136187370, -2.25713576259163860, 0.12936679122976494,
|
||||
-1.09100901843779790, 2.41333171030692250, -0.32232269186912466,
|
||||
-0.02601080193857045, -0.50804133170416700, 1.53405213364273730
|
||||
].freeze
|
||||
|
||||
# The transformation matrix for converting linear-light display-p3 colors to
|
||||
# linear-light prophoto-rgb.
|
||||
LINEAR_DISPLAY_P3_TO_LINEAR_PROPHOTO_RGB = [
|
||||
0.63168691934035890, 0.21393038569465722, 0.15438269496498390,
|
||||
0.08320371426648458, 0.88586513676302430, 0.03093114897049121,
|
||||
-0.00127273456473881, 0.05075510433665735, 0.95051763022808140
|
||||
].freeze
|
||||
|
||||
# The transformation matrix for converting linear-light prophoto-rgb colors to
|
||||
# linear-light display-p3.
|
||||
LINEAR_PROPHOTO_RGB_TO_LINEAR_DISPLAY_P3 = [
|
||||
1.63257560870691790, -0.37977161848259840, -0.25280399022431950,
|
||||
-0.15370040233755072, 1.16670254724250140, -0.01300214490495082,
|
||||
0.01039319529676572, -0.06280731264959440, 1.05241411735282870
|
||||
].freeze
|
||||
|
||||
# The transformation matrix for converting linear-light display-p3 colors to
|
||||
# xyz-d50.
|
||||
LINEAR_DISPLAY_P3_TO_XYZ_D50 = [
|
||||
0.51514644296811600, 0.29200998206385770, 0.15713925139759397,
|
||||
0.24120032212525520, 0.69222254113138180, 0.06657713674336294,
|
||||
-0.00105013914714014, 0.04187827018907460, 0.78427647146852570
|
||||
].freeze
|
||||
|
||||
# The transformation matrix for converting xyz-d50 colors to linear-light
|
||||
# display-p3.
|
||||
XYZ_D50_TO_LINEAR_DISPLAY_P3 = [
|
||||
2.40393412185549730, -0.99003044249559310, -0.39761363181465614,
|
||||
-0.84227001614546880, 1.79895801610670820, 0.01604562477090472,
|
||||
0.04819381686413303, -0.09738519815446048, 1.27367136933212730
|
||||
].freeze
|
||||
|
||||
# The transformation matrix for converting linear-light a98-rgb colors to
|
||||
# linear-light rec2020.
|
||||
LINEAR_A98_RGB_TO_LINEAR_REC2020 = [
|
||||
0.87733384166365680, 0.07749370651571998, 0.04517245182062317,
|
||||
0.09662259146620378, 0.89152732024418050, 0.01185008828961569,
|
||||
0.02292106270284839, 0.04303668501067932, 0.93404225228647230
|
||||
].freeze
|
||||
|
||||
# The transformation matrix for converting linear-light rec2020 colors to
|
||||
# linear-light a98-rgb.
|
||||
LINEAR_REC2020_TO_LINEAR_A98_RGB = [
|
||||
1.15197839471591630, -0.09750305530240860, -0.05447533941350766,
|
||||
-0.12455047452159074, 1.13289989712596030, -0.00834942260436947,
|
||||
-0.02253038278105590, -0.04980650742838876, 1.07233689020944460
|
||||
].freeze
|
||||
|
||||
# The transformation matrix for converting linear-light a98-rgb colors to xyz.
|
||||
LINEAR_A98_RGB_TO_XYZ_D65 = [
|
||||
0.57666904291013080, 0.18555823790654627, 0.18822864623499472,
|
||||
0.29734497525053616, 0.62736356625546600, 0.07529145849399789,
|
||||
0.02703136138641237, 0.07068885253582714, 0.99133753683763890
|
||||
].freeze
|
||||
|
||||
# The transformation matrix for converting xyz colors to linear-light a98-rgb.
|
||||
XYZ_D65_TO_LINEAR_A98_RGB = [
|
||||
2.04158790381074600, -0.56500697427885960, -0.34473135077832950,
|
||||
-0.96924363628087980, 1.87596750150772060, 0.04155505740717561,
|
||||
0.01344428063203102, -0.11836239223101823, 1.01517499439120540
|
||||
].freeze
|
||||
|
||||
# The transformation matrix for converting linear-light a98-rgb colors to lms.
|
||||
LINEAR_A98_RGB_TO_LMS = [
|
||||
0.57643225961839410, 0.36991322261987963, 0.05365451776172635,
|
||||
0.29631647054222465, 0.59167613325218850, 0.11200739620558686,
|
||||
0.12347825101427760, 0.21949869837199862, 0.65702305061372380
|
||||
].freeze
|
||||
|
||||
# The transformation matrix for converting lms colors to linear-light a98-rgb.
|
||||
LMS_TO_LINEAR_A98_RGB = [
|
||||
2.55403683861155660, -1.62197618068286990, 0.06793934207131327,
|
||||
-1.26843797328503200, 2.60975734928768900, -0.34131937600265710,
|
||||
-0.05623473593749381, -0.56704183956690610, 1.62327657550439990
|
||||
].freeze
|
||||
|
||||
# The transformation matrix for converting linear-light a98-rgb colors to
|
||||
# linear-light prophoto-rgb.
|
||||
LINEAR_A98_RGB_TO_LINEAR_PROPHOTO_RGB = [
|
||||
0.74011750180477920, 0.11327951328898105, 0.14660298490623970,
|
||||
0.13755046469802620, 0.83307708026948400, 0.02937245503248977,
|
||||
0.02359772990871766, 0.07378347703906656, 0.90261879305221580
|
||||
].freeze
|
||||
|
||||
# The transformation matrix for converting linear-light prophoto-rgb colors to
|
||||
# linear-light a98-rgb.
|
||||
LINEAR_PROPHOTO_RGB_TO_LINEAR_A98_RGB = [
|
||||
1.38965124815152000, -0.16945907691487766, -0.22019217123664242,
|
||||
-0.22882573163305037, 1.23174254119010480, -0.00291680955705449,
|
||||
-0.01762544368426068, -0.09625702306122665, 1.11388246674548740
|
||||
].freeze
|
||||
|
||||
# The transformation matrix for converting linear-light a98-rgb colors to
|
||||
# xyz-d50.
|
||||
LINEAR_A98_RGB_TO_XYZ_D50 = [
|
||||
0.60977504188618140, 0.20530000261929401, 0.14922063192409227,
|
||||
0.31112461220464155, 0.62565323083468560, 0.06322215696067286,
|
||||
0.01947059555648168, 0.06087908649415867, 0.74475492045981980
|
||||
].freeze
|
||||
|
||||
# The transformation matrix for converting xyz-d50 colors to linear-light
|
||||
# a98-rgb.
|
||||
XYZ_D50_TO_LINEAR_A98_RGB = [
|
||||
1.96246703637688060, -0.61074234048150730, -0.34135809808271540,
|
||||
-0.97879547655577770, 1.91625437739598840, 0.03344287339036693,
|
||||
0.02870443944957101, -0.14067486633170680, 1.34891418141379370
|
||||
].freeze
|
||||
|
||||
# The transformation matrix for converting linear-light rec2020 colors to xyz.
|
||||
LINEAR_REC2020_TO_XYZ_D65 = [
|
||||
0.63695804830129130, 0.14461690358620838, 0.16888097516417205,
|
||||
0.26270021201126703, 0.67799807151887100, 0.05930171646986194,
|
||||
0.00000000000000000, 0.02807269304908750, 1.06098505771079090
|
||||
].freeze
|
||||
|
||||
# The transformation matrix for converting xyz colors to linear-light rec2020.
|
||||
XYZ_D65_TO_LINEAR_REC2020 = [
|
||||
1.71665118797126760, -0.35567078377639240, -0.25336628137365980,
|
||||
-0.66668435183248900, 1.61648123663493900, 0.01576854581391113,
|
||||
0.01763985744531091, -0.04277061325780865, 0.94210312123547400
|
||||
].freeze
|
||||
|
||||
# The transformation matrix for converting linear-light rec2020 colors to lms.
|
||||
LINEAR_REC2020_TO_LMS = [
|
||||
0.61675578486544440, 0.36019840122646335, 0.02304581390809228,
|
||||
0.26513305939263670, 0.63583937206784910, 0.09902756853951408,
|
||||
0.10010262952034828, 0.20390652261661452, 0.69599084786303720
|
||||
].freeze
|
||||
|
||||
# The transformation matrix for converting lms colors to linear-light rec2020.
|
||||
LMS_TO_LINEAR_REC2020 = [
|
||||
2.13990673043465130, -1.24638949376061800, 0.10648276332596668,
|
||||
-0.88473583575776740, 2.16323093836120070, -0.27849510260343340,
|
||||
-0.04857374640044396, -0.45450314971409640, 1.50307689611454040
|
||||
].freeze
|
||||
|
||||
# The transformation matrix for converting linear-light rec2020 colors to
|
||||
# linear-light prophoto-rgb.
|
||||
LINEAR_REC2020_TO_LINEAR_PROPHOTO_RGB = [
|
||||
0.83518733312972350, 0.04886884858605698, 0.11594381828421951,
|
||||
0.05403324519953363, 0.92891840856920440, 0.01704834623126199,
|
||||
-0.00234203897072539, 0.03633215316169465, 0.96600988580903070
|
||||
].freeze
|
||||
|
||||
# The transformation matrix for converting linear-light prophoto-rgb colors to
|
||||
# linear-light rec2020.
|
||||
LINEAR_PROPHOTO_RGB_TO_LINEAR_REC2020 = [
|
||||
1.20065932951740800, -0.05756805370122346, -0.14309127581618444,
|
||||
-0.06994154955888504, 1.08061789759721400, -0.01067634803832895,
|
||||
0.00554147334294746, -0.04078219298657951, 1.03524071964363200
|
||||
].freeze
|
||||
|
||||
# The transformation matrix for converting linear-light rec2020 colors to
|
||||
# xyz-d50.
|
||||
LINEAR_REC2020_TO_XYZ_D50 = [
|
||||
0.67351546318827600, 0.16569726370390453, 0.12508294953738705,
|
||||
0.27905900514112060, 0.67531800574910980, 0.04562298910976962,
|
||||
-0.00193242713400438, 0.02997782679282923, 0.79705920285163550
|
||||
].freeze
|
||||
|
||||
# The transformation matrix for converting xyz-d50 colors to linear-light
|
||||
# rec2020.
|
||||
XYZ_D50_TO_LINEAR_REC2020 = [
|
||||
1.64718490467176600, -0.39368189813164710, -0.23595963848828266,
|
||||
-0.68266410741738180, 1.64771461274440760, 0.01281708338512084,
|
||||
0.02966887665275675, -0.06292589642970030, 1.25355782018657710
|
||||
].freeze
|
||||
|
||||
# The transformation matrix for converting xyz colors to lms.
|
||||
XYZ_D65_TO_LMS = [
|
||||
0.81902243799670300, 0.36190626005289034, -0.12887378152098788,
|
||||
0.03298365393238846, 0.92928686158634330, 0.03614466635064235,
|
||||
0.04817718935962420, 0.26423953175273080, 0.63354782846943080
|
||||
].freeze
|
||||
|
||||
# The transformation matrix for converting lms colors to xyz.
|
||||
LMS_TO_XYZ_D65 = [
|
||||
1.22687987584592430, -0.55781499446021710, 0.28139104566596460,
|
||||
-0.04057574521480084, 1.11228680328031730, -0.07171105806551635,
|
||||
-0.07637293667466007, -0.42149333240224324, 1.58692401983678180
|
||||
].freeze
|
||||
|
||||
# The transformation matrix for converting xyz colors to linear-light
|
||||
# prophoto-rgb.
|
||||
XYZ_D65_TO_LINEAR_PROPHOTO_RGB = [
|
||||
1.40319046337749790, -0.22301514479051668, -0.10160668507413790,
|
||||
-0.52623840216330720, 1.48163196292346440, 0.01701879027252688,
|
||||
-0.01120226528622150, 0.01824640347962099, 0.91124722749150480
|
||||
].freeze
|
||||
|
||||
# The transformation matrix for converting linear-light prophoto-rgb colors to
|
||||
# xyz.
|
||||
LINEAR_PROPHOTO_RGB_TO_XYZ_D65 = [
|
||||
0.75559074229692100, 0.11271984265940525, 0.08214534209534540,
|
||||
0.26832184357857190, 0.71511525666179120, 0.01656289975963685,
|
||||
0.00391597276242580, -0.01293344283684181, 1.09807522083429450
|
||||
].freeze
|
||||
|
||||
# The transformation matrix for converting xyz colors to xyz-d50.
|
||||
XYZ_D65_TO_XYZ_D50 = [
|
||||
1.04792979254499660, 0.02294687060160952, -0.05019226628920519,
|
||||
0.02962780877005567, 0.99043442675388000, -0.01707379906341879,
|
||||
-0.00924304064620452, 0.01505519149029816, 0.75187428142813700
|
||||
].freeze
|
||||
|
||||
# The transformation matrix for converting xyz-d50 colors to xyz.
|
||||
XYZ_D50_TO_XYZ_D65 = [
|
||||
0.95547342148807520, -0.02309845494876452, 0.06325924320057065,
|
||||
-0.02836970933386358, 1.00999539808130410, 0.02104144119191730,
|
||||
0.01231401486448199, -0.02050764929889898, 1.33036592624212400
|
||||
].freeze
|
||||
|
||||
# The transformation matrix for converting lms colors to linear-light
|
||||
# prophoto-rgb.
|
||||
LMS_TO_LINEAR_PROPHOTO_RGB = [
|
||||
1.73835514811572070, -0.98795094275144580, 0.24959579463572504,
|
||||
-0.70704940153292660, 1.93437004444013820, -0.22732064290721150,
|
||||
-0.08407882206239634, -0.35754060521141334, 1.44161942727380970
|
||||
].freeze
|
||||
|
||||
# The transformation matrix for converting linear-light prophoto-rgb colors to
|
||||
# lms.
|
||||
LINEAR_PROPHOTO_RGB_TO_LMS = [
|
||||
0.71544846056555340, 0.35279155007721186, -0.06824001064276530,
|
||||
0.27441164900156710, 0.66779764984123670, 0.05779070115719616,
|
||||
0.10978443261622942, 0.18619829115002018, 0.70401727623375040
|
||||
].freeze
|
||||
|
||||
# The transformation matrix for converting lms colors to xyz-d50.
|
||||
LMS_TO_XYZ_D50 = [
|
||||
1.28858621817270600, -0.53787174449737450, 0.21358120275423640,
|
||||
-0.00253387643187372, 1.09231679887191650, -0.08978292244004273,
|
||||
-0.06937382305734124, -0.29500839894431263, 1.18948682451211420
|
||||
].freeze
|
||||
|
||||
# The transformation matrix for converting xyz-d50 colors to lms.
|
||||
XYZ_D50_TO_LMS = [
|
||||
0.77070004204311720, 0.34924840261939616, -0.11202351884164681,
|
||||
0.00559649248368848, 0.93707234011367690, 0.06972568836252771,
|
||||
0.04633714262191069, 0.25277531574310524, 0.85145807674679600
|
||||
].freeze
|
||||
|
||||
# The transformation matrix for converting linear-light prophoto-rgb colors to
|
||||
# xyz-d50.
|
||||
LINEAR_PROPHOTO_RGB_TO_XYZ_D50 = [
|
||||
0.79776664490064230, 0.13518129740053308, 0.03134773412839220,
|
||||
0.28807482881940130, 0.71183523424187300, 0.00008993693872564,
|
||||
0.00000000000000000, 0.00000000000000000, 0.82510460251046020
|
||||
].freeze
|
||||
|
||||
# The transformation matrix for converting xyz-d50 colors to linear-light
|
||||
# prophoto-rgb.
|
||||
XYZ_D50_TO_LINEAR_PROPHOTO_RGB = [
|
||||
1.34578688164715830, -0.25557208737979464, -0.05110186497554526,
|
||||
-0.54463070512490190, 1.50824774284514680, 0.02052744743642139,
|
||||
0.00000000000000000, 0.00000000000000000, 1.21196754563894520
|
||||
].freeze
|
||||
end
|
||||
|
||||
private_constant :Conversions
|
||||
end
|
||||
end
|
||||
end
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Sass
|
||||
module Value
|
||||
class Color
|
||||
# @see https://github.com/sass/dart-sass/blob/main/lib/src/value/color/gamut_map_method.dart
|
||||
module GamutMapMethod
|
||||
# @return [::String]
|
||||
attr_reader :name
|
||||
|
||||
# @param name [::String]
|
||||
def initialize(name)
|
||||
@name = name
|
||||
end
|
||||
|
||||
class << self
|
||||
# @param name [::String]
|
||||
# @param argument_name [::String]
|
||||
# @return [GamutMapMethod]
|
||||
def from_name(name, argument_name = nil)
|
||||
case name
|
||||
when 'clip'
|
||||
CLIP
|
||||
when 'local-minde'
|
||||
LOCAL_MINDE
|
||||
else
|
||||
raise Sass::ScriptError.new("Unknown gamut map method \"#{name}\".", argument_name)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# @param color [Color]
|
||||
# @return [Color]
|
||||
def map(color)
|
||||
raise NotImplementedError, "[BUG] gamut map method #{name} doesn't implement map."
|
||||
end
|
||||
end
|
||||
|
||||
private_constant :GamutMapMethod
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
require_relative 'gamut_map_method/clip'
|
||||
require_relative 'gamut_map_method/local_minde'
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Sass
|
||||
module Value
|
||||
class Color
|
||||
module GamutMapMethod
|
||||
# @see https://github.com/sass/dart-sass/blob/main/lib/src/value/color/gamut_map_method/local_minde.dart
|
||||
class Clip
|
||||
include GamutMapMethod
|
||||
|
||||
def initialize
|
||||
super('clip')
|
||||
end
|
||||
|
||||
def map(color)
|
||||
space = color.send(:_space)
|
||||
Color.send(:for_space_internal,
|
||||
space,
|
||||
_clamp_channel(color.send(:channel0_or_nil), space.channels[0]),
|
||||
_clamp_channel(color.send(:channel1_or_nil), space.channels[1]),
|
||||
_clamp_channel(color.send(:channel2_or_nil), space.channels[2]),
|
||||
color.send(:alpha_or_nil))
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def _clamp_channel(value, channel)
|
||||
return nil if value.nil?
|
||||
|
||||
case channel
|
||||
when LinearChannel
|
||||
FuzzyMath._clamp_like_css(value, channel.min, channel.max)
|
||||
else
|
||||
value
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private_constant :Clip
|
||||
|
||||
CLIP = Clip.new
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
+94
@@ -0,0 +1,94 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Sass
|
||||
module Value
|
||||
class Color
|
||||
module GamutMapMethod
|
||||
# @see https://github.com/sass/dart-sass/blob/main/lib/src/value/color/gamut_map_method/local_minde.dart
|
||||
class LocalMinde
|
||||
include GamutMapMethod
|
||||
|
||||
# A constant from the gamut-mapping algorithm.
|
||||
JND = 0.02
|
||||
|
||||
private_constant :JND
|
||||
|
||||
# A constant from the gamut-mapping algorithm.
|
||||
EPSILON = 0.0001
|
||||
|
||||
private_constant :EPSILON
|
||||
|
||||
def initialize
|
||||
super('local-minde')
|
||||
end
|
||||
|
||||
def map(color)
|
||||
original_oklch = color.send(:_to_space, Space::OKLCH)
|
||||
lightness = original_oklch.send(:channel0_or_nil)
|
||||
hue = original_oklch.send(:channel2_or_nil)
|
||||
alpha = original_oklch.send(:alpha_or_nil)
|
||||
|
||||
if FuzzyMath.greater_than_or_equals(lightness.nil? ? 0 : lightness, 1)
|
||||
if color.legacy?
|
||||
return Color.send(:_for_space,
|
||||
Space::RGB, 255, 255, 255, color.send(:alpha_or_nil))
|
||||
.send(:_to_space, color.send(:_space))
|
||||
else
|
||||
return Color.send(:for_space_internal,
|
||||
color.send(:_space), 1, 1, 1, color.send(:alpha_or_nil))
|
||||
end
|
||||
elsif FuzzyMath.less_than_or_equals(lightness.nil? ? 0 : lightness, 0)
|
||||
return Color.send(:_for_space,
|
||||
Space::RGB, 0, 0, 0, color.send(:alpha_or_nil))
|
||||
.send(:_to_space, color.send(:_space))
|
||||
end
|
||||
|
||||
clipped = color.send(:_to_gamut, CLIP)
|
||||
return clipped if _delta_eok(clipped, color) < JND
|
||||
|
||||
min = 0.0
|
||||
max = original_oklch.send(:channel1)
|
||||
min_in_gamut = true
|
||||
while max - min > EPSILON
|
||||
chroma = (min + max) / 2
|
||||
|
||||
current = Space::OKLCH.convert(color.send(:_space), lightness, chroma, hue, alpha)
|
||||
|
||||
if min_in_gamut && current.in_gamut?
|
||||
min = chroma
|
||||
next
|
||||
end
|
||||
|
||||
clipped = current.send(:_to_gamut, CLIP)
|
||||
e = _delta_eok(clipped, current)
|
||||
|
||||
if e < JND
|
||||
return clipped if JND - e < EPSILON
|
||||
|
||||
min_in_gamut = false
|
||||
min = chroma
|
||||
else
|
||||
max = chroma
|
||||
end
|
||||
end
|
||||
clipped
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def _delta_eok(color1, color2)
|
||||
lab1 = color1.send(:_to_space, Space::OKLAB)
|
||||
lab2 = color2.send(:_to_space, Space::OKLAB)
|
||||
Math.sqrt(((lab1.send(:channel0) - lab2.send(:channel0))**2) +
|
||||
((lab1.send(:channel1) - lab2.send(:channel1))**2) +
|
||||
((lab1.send(:channel2) - lab2.send(:channel2))**2))
|
||||
end
|
||||
end
|
||||
|
||||
private_constant :LocalMinde
|
||||
|
||||
LOCAL_MINDE = LocalMinde.new
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Sass
|
||||
module Value
|
||||
class Color
|
||||
# @see https://github.com/sass/dart-sass/blob/main/lib/src/value/color/interpolation_method.dart
|
||||
class InterpolationMethod
|
||||
# @return [Space]
|
||||
attr_reader :space
|
||||
|
||||
# @return [Symbol, nil]
|
||||
attr_reader :hue
|
||||
|
||||
# @param space [Space]
|
||||
# @param hue [Symbol]
|
||||
def initialize(space, hue = nil)
|
||||
@space = space
|
||||
@hue = if space.polar?
|
||||
hue.nil? ? :shorter : hue
|
||||
end
|
||||
|
||||
return unless !space.polar? && !hue.nil?
|
||||
|
||||
raise Sass::ScriptError,
|
||||
"Hue interpolation method may not be set for rectangular color space #{space.name}."
|
||||
end
|
||||
end
|
||||
|
||||
private_constant :InterpolationMethod
|
||||
|
||||
# @see https://github.com/sass/dart-sass/blob/main/lib/src/value/color/interpolation_method.dart
|
||||
module HueInterpolationMethod
|
||||
class << self
|
||||
# @param name [::String]
|
||||
# @param argument_name [::String]
|
||||
# @return [Symbol]
|
||||
def from_name(name, argument_name = nil)
|
||||
case name
|
||||
when 'decreasing', 'increasing', 'longer', 'shorter'
|
||||
name.to_sym
|
||||
else
|
||||
raise Sass::ScriptError.new("Unknown hue interpolation method \"#{name}\".", argument_name)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private_constant :HueInterpolationMethod
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,198 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Sass
|
||||
module Value
|
||||
class Color
|
||||
# @see https://github.com/sass/dart-sass/blob/main/lib/src/value/color/space.dart
|
||||
module Space
|
||||
# @return [::String]
|
||||
attr_reader :name
|
||||
|
||||
# @return [Array<ColorChannel>]
|
||||
attr_reader :channels
|
||||
|
||||
# @return [::Boolean]
|
||||
def bounded?
|
||||
raise NotImplementedError
|
||||
end
|
||||
|
||||
# @return [::Boolean]
|
||||
def legacy?
|
||||
false
|
||||
end
|
||||
|
||||
# @return [::Boolean]
|
||||
def polar?
|
||||
false
|
||||
end
|
||||
|
||||
# @param name [::String]
|
||||
# @param channels [Array<ColorChannel>]
|
||||
def initialize(name, channels)
|
||||
@name = name
|
||||
@channels = channels
|
||||
end
|
||||
|
||||
class << self
|
||||
# @param name [::String]
|
||||
# @param argument_name [::String]
|
||||
# @return [Space]
|
||||
def from_name(name, argument_name = nil)
|
||||
case name.downcase
|
||||
when 'rgb'
|
||||
RGB
|
||||
when 'hwb'
|
||||
HWB
|
||||
when 'hsl'
|
||||
HSL
|
||||
when 'srgb'
|
||||
SRGB
|
||||
when 'srgb-linear'
|
||||
SRGB_LINEAR
|
||||
when 'display-p3'
|
||||
DISPLAY_P3
|
||||
when 'a98-rgb'
|
||||
A98_RGB
|
||||
when 'prophoto-rgb'
|
||||
PROPHOTO_RGB
|
||||
when 'rec2020'
|
||||
REC2020
|
||||
when 'xyz', 'xyz-d65'
|
||||
XYZ_D65
|
||||
when 'xyz-d50'
|
||||
XYZ_D50
|
||||
when 'lab'
|
||||
LAB
|
||||
when 'lch'
|
||||
LCH
|
||||
when 'oklab'
|
||||
OKLAB
|
||||
when 'oklch'
|
||||
OKLCH
|
||||
else
|
||||
raise Sass::ScriptError.new("Unknown color space \"#{name}\".", argument_name)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# @param dest [Space]
|
||||
# @param channel0 [Numeric]
|
||||
# @param channel1 [Numeric]
|
||||
# @param channel2 [Numeric]
|
||||
# @param alpha [Numeric]
|
||||
# @return [Color]
|
||||
def convert(dest, channel0, channel1, channel2, alpha)
|
||||
convert_linear(dest, channel0, channel1, channel2, alpha)
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def convert_linear(dest, red, green, blue, alpha,
|
||||
missing_lightness: false,
|
||||
missing_chroma: false,
|
||||
missing_hue: false,
|
||||
missing_a: false,
|
||||
missing_b: false)
|
||||
linear_dest = case dest
|
||||
when HSL, HWB
|
||||
SRGB
|
||||
when LAB, LCH
|
||||
XYZ_D50
|
||||
when OKLAB, OKLCH
|
||||
LMS
|
||||
else
|
||||
dest
|
||||
end
|
||||
if linear_dest == self
|
||||
transformed_red = red
|
||||
transformed_green = green
|
||||
transformed_blue = blue
|
||||
else
|
||||
linear_red = to_linear(red.nil? ? 0 : red)
|
||||
linear_green = to_linear(green.nil? ? 0 : green)
|
||||
linear_blue = to_linear(blue.nil? ? 0 : blue)
|
||||
matrix = transformation_matrix(linear_dest)
|
||||
|
||||
# (matrix * [linear_red, linear_green, linear_blue]).map(linear_dest.from_linear)
|
||||
transformed_red = linear_dest.from_linear((matrix[0] * linear_red) +
|
||||
(matrix[1] * linear_green) +
|
||||
(matrix[2] * linear_blue))
|
||||
transformed_green = linear_dest.from_linear((matrix[3] * linear_red) +
|
||||
(matrix[4] * linear_green) +
|
||||
(matrix[5] * linear_blue))
|
||||
transformed_blue = linear_dest.from_linear((matrix[6] * linear_red) +
|
||||
(matrix[7] * linear_green) +
|
||||
(matrix[8] * linear_blue))
|
||||
end
|
||||
|
||||
case dest
|
||||
when HSL, HWB
|
||||
SRGB.convert(dest, transformed_red, transformed_green, transformed_blue, alpha,
|
||||
missing_lightness:,
|
||||
missing_chroma:,
|
||||
missing_hue:)
|
||||
when LAB, LCH
|
||||
XYZ_D50.convert(dest, transformed_red, transformed_green, transformed_blue, alpha,
|
||||
missing_lightness:,
|
||||
missing_chroma:,
|
||||
missing_hue:,
|
||||
missing_a:,
|
||||
missing_b:)
|
||||
when OKLAB, OKLCH
|
||||
LMS.convert(dest, transformed_red, transformed_green, transformed_blue, alpha,
|
||||
missing_lightness:,
|
||||
missing_chroma:,
|
||||
missing_hue:,
|
||||
missing_a:,
|
||||
missing_b:)
|
||||
else
|
||||
Color.send(:_for_space,
|
||||
dest,
|
||||
red.nil? ? nil : transformed_red,
|
||||
green.nil? ? nil : transformed_green,
|
||||
blue.nil? ? nil : transformed_blue,
|
||||
alpha)
|
||||
end
|
||||
end
|
||||
|
||||
# @param channel [Numeric]
|
||||
# @return [Numeric]
|
||||
def to_linear(channel)
|
||||
raise NotImplementedError, "[BUG] Color space #{name} doesn't support linear conversions."
|
||||
end
|
||||
|
||||
# @param channel [Numeric]
|
||||
# @return [Numeric]
|
||||
def from_linear(channel)
|
||||
raise NotImplementedError, "[BUG] Color space #{name} doesn't support linear conversions."
|
||||
end
|
||||
|
||||
# @param dest [Space]
|
||||
# @return [Array<Numeric>]
|
||||
def transformation_matrix(dest)
|
||||
raise NotImplementedError, "[BUG] Color space conversion from #{name} to #{dest.name} not implemented."
|
||||
end
|
||||
end
|
||||
|
||||
private_constant :Space
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
require_relative 'space/utils'
|
||||
require_relative 'space/a98_rgb'
|
||||
require_relative 'space/display_p3'
|
||||
require_relative 'space/hsl'
|
||||
require_relative 'space/hwb'
|
||||
require_relative 'space/lab'
|
||||
require_relative 'space/lch'
|
||||
require_relative 'space/lms'
|
||||
require_relative 'space/oklab'
|
||||
require_relative 'space/oklch'
|
||||
require_relative 'space/prophoto_rgb'
|
||||
require_relative 'space/rec2020'
|
||||
require_relative 'space/rgb'
|
||||
require_relative 'space/srgb'
|
||||
require_relative 'space/srgb_linear'
|
||||
require_relative 'space/xyz_d50'
|
||||
require_relative 'space/xyz_d65'
|
||||
@@ -0,0 +1,57 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Sass
|
||||
module Value
|
||||
class Color
|
||||
module Space
|
||||
# @see https://github.com/sass/dart-sass/blob/main/lib/src/value/color/space/a98_rgb.dart
|
||||
class A98Rgb
|
||||
include Space
|
||||
|
||||
def bounded?
|
||||
true
|
||||
end
|
||||
|
||||
def initialize
|
||||
super('a98-rgb', Utils::RGB_CHANNELS)
|
||||
end
|
||||
|
||||
def to_linear(channel)
|
||||
(channel <=> 0) * (channel.abs**(563 / 256.0))
|
||||
end
|
||||
|
||||
def from_linear(channel)
|
||||
(channel <=> 0) * (channel.abs**(256 / 563.0))
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def transformation_matrix(dest)
|
||||
case dest
|
||||
when DISPLAY_P3
|
||||
Conversions::LINEAR_A98_RGB_TO_LINEAR_DISPLAY_P3
|
||||
when LMS
|
||||
Conversions::LINEAR_A98_RGB_TO_LMS
|
||||
when PROPHOTO_RGB
|
||||
Conversions::LINEAR_A98_RGB_TO_LINEAR_PROPHOTO_RGB
|
||||
when REC2020
|
||||
Conversions::LINEAR_A98_RGB_TO_LINEAR_REC2020
|
||||
when RGB, SRGB, SRGB_LINEAR
|
||||
Conversions::LINEAR_A98_RGB_TO_LINEAR_SRGB
|
||||
when XYZ_D50
|
||||
Conversions::LINEAR_A98_RGB_TO_XYZ_D50
|
||||
when XYZ_D65
|
||||
Conversions::LINEAR_A98_RGB_TO_XYZ_D65
|
||||
else
|
||||
super
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private_constant :A98Rgb
|
||||
|
||||
A98_RGB = A98Rgb.new
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Sass
|
||||
module Value
|
||||
class Color
|
||||
module Space
|
||||
# @see https://github.com/sass/dart-sass/blob/main/lib/src/value/color/space/display_p3.dart
|
||||
class DisplayP3
|
||||
include Space
|
||||
|
||||
def bounded?
|
||||
true
|
||||
end
|
||||
|
||||
def initialize
|
||||
super('display-p3', Utils::RGB_CHANNELS)
|
||||
end
|
||||
|
||||
def to_linear(channel)
|
||||
Utils.srgb_and_display_p3_to_linear(channel)
|
||||
end
|
||||
|
||||
def from_linear(channel)
|
||||
Utils.srgb_and_display_p3_from_linear(channel)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def transformation_matrix(dest)
|
||||
case dest
|
||||
when A98_RGB
|
||||
Conversions::LINEAR_DISPLAY_P3_TO_LINEAR_A98_RGB
|
||||
when LMS
|
||||
Conversions::LINEAR_DISPLAY_P3_TO_LMS
|
||||
when PROPHOTO_RGB
|
||||
Conversions::LINEAR_DISPLAY_P3_TO_LINEAR_PROPHOTO_RGB
|
||||
when REC2020
|
||||
Conversions::LINEAR_DISPLAY_P3_TO_LINEAR_REC2020
|
||||
when RGB, SRGB, SRGB_LINEAR
|
||||
Conversions::LINEAR_DISPLAY_P3_TO_LINEAR_SRGB
|
||||
when XYZ_D50
|
||||
Conversions::LINEAR_DISPLAY_P3_TO_XYZ_D50
|
||||
when XYZ_D65
|
||||
Conversions::LINEAR_DISPLAY_P3_TO_XYZ_D65
|
||||
else
|
||||
super
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private_constant :DisplayP3
|
||||
|
||||
DISPLAY_P3 = DisplayP3.new
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,65 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Sass
|
||||
module Value
|
||||
class Color
|
||||
module Space
|
||||
# @see https://github.com/sass/dart-sass/blob/main/lib/src/value/color/space/hsl.dart
|
||||
class Hsl
|
||||
include Space
|
||||
|
||||
def bounded?
|
||||
true
|
||||
end
|
||||
|
||||
def legacy?
|
||||
true
|
||||
end
|
||||
|
||||
def polar?
|
||||
true
|
||||
end
|
||||
|
||||
def initialize
|
||||
super('hsl', [
|
||||
Utils::HUE_CHANNEL,
|
||||
LinearChannel.new('saturation', 0, 100, requires_percent: true, lower_clamped: true).freeze,
|
||||
LinearChannel.new('lightness', 0, 100, requires_percent: true).freeze
|
||||
].freeze)
|
||||
end
|
||||
|
||||
def convert(dest, hue, saturation, lightness, alpha)
|
||||
missing_lightness = lightness.nil?
|
||||
missing_chroma = saturation.nil?
|
||||
missing_hue = hue.nil?
|
||||
|
||||
hue = ((hue.nil? ? 0 : hue) % 360) / 30.0
|
||||
saturation = (saturation.nil? ? 0 : saturation) / 100.0
|
||||
lightness = (lightness.nil? ? 0 : lightness) / 100.0
|
||||
|
||||
a = saturation * [lightness, 1 - lightness].min
|
||||
f = lambda do |n|
|
||||
k = (n + hue) % 12
|
||||
lightness - (a * [-1, [k - 3, 9 - k, 1].min].max)
|
||||
end
|
||||
|
||||
SRGB.convert(
|
||||
dest,
|
||||
f.call(0),
|
||||
f.call(8),
|
||||
f.call(4),
|
||||
alpha,
|
||||
missing_lightness:,
|
||||
missing_chroma:,
|
||||
missing_hue:
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
private_constant :Hsl
|
||||
|
||||
HSL = Hsl.new
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,70 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Sass
|
||||
module Value
|
||||
class Color
|
||||
module Space
|
||||
# @see https://github.com/sass/dart-sass/blob/main/lib/src/value/color/space/hwb.dart
|
||||
class Hwb
|
||||
include Space
|
||||
|
||||
def bounded?
|
||||
true
|
||||
end
|
||||
|
||||
def legacy?
|
||||
true
|
||||
end
|
||||
|
||||
def polar?
|
||||
true
|
||||
end
|
||||
|
||||
def initialize
|
||||
super('hwb', [
|
||||
Utils::HUE_CHANNEL,
|
||||
LinearChannel.new('whiteness', 0, 100, requires_percent: true).freeze,
|
||||
LinearChannel.new('blackness', 0, 100, requires_percent: true).freeze
|
||||
].freeze)
|
||||
end
|
||||
|
||||
def convert(dest, hue, whiteness, blackness, alpha)
|
||||
missing_hue = hue.nil?
|
||||
|
||||
hue = ((hue.nil? ? 0 : hue) % 360) / 30.0
|
||||
whiteness = (whiteness.nil? ? 0 : whiteness) / 100.0
|
||||
blackness = (blackness.nil? ? 0 : blackness) / 100.0
|
||||
|
||||
sum = whiteness + blackness
|
||||
if sum > 1
|
||||
gray = whiteness / sum
|
||||
SRGB.convert(dest,
|
||||
gray,
|
||||
gray,
|
||||
gray,
|
||||
alpha,
|
||||
missing_hue:)
|
||||
else
|
||||
f = lambda do |n|
|
||||
k = (n + hue) % 12
|
||||
0.5 - ([-1, [k - 3, 9 - k, 1].min].max / 2.0)
|
||||
end
|
||||
|
||||
factor = 1 - sum
|
||||
SRGB.convert(dest,
|
||||
(f.call(0) * factor) + whiteness,
|
||||
(f.call(8) * factor) + whiteness,
|
||||
(f.call(4) * factor) + whiteness,
|
||||
alpha,
|
||||
missing_hue:)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private_constant :Hwb
|
||||
|
||||
HWB = Hwb.new
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,77 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Sass
|
||||
module Value
|
||||
class Color
|
||||
module Space
|
||||
# @see https://github.com/sass/dart-sass/blob/main/lib/src/value/color/space/lab.dart
|
||||
class Lab
|
||||
include Space
|
||||
|
||||
def bounded?
|
||||
false
|
||||
end
|
||||
|
||||
def initialize
|
||||
super('lab', [
|
||||
LinearChannel.new('lightness', 0, 100, lower_clamped: true, upper_clamped: true).freeze,
|
||||
LinearChannel.new('a', -125, 125).freeze,
|
||||
LinearChannel.new('b', -125, 125).freeze
|
||||
].freeze)
|
||||
end
|
||||
|
||||
def convert(dest, lightness, a, b, alpha, # rubocop:disable Naming/MethodParameterName
|
||||
missing_chroma: false, missing_hue: false)
|
||||
case dest
|
||||
when LAB
|
||||
powerless_ab = lightness.nil? || FuzzyMath.equals(lightness, 0)
|
||||
Color.send(
|
||||
:_for_space,
|
||||
dest,
|
||||
lightness,
|
||||
a.nil? || powerless_ab ? nil : a,
|
||||
b.nil? || powerless_ab ? nil : b,
|
||||
alpha
|
||||
)
|
||||
when LCH
|
||||
Utils.lab_to_lch(dest, lightness, a, b, alpha)
|
||||
else
|
||||
missing_lightness = lightness.nil?
|
||||
lightness = 0 if missing_lightness
|
||||
|
||||
f1 = (lightness + 16) / 116.0
|
||||
|
||||
XYZ_D50.convert(
|
||||
dest,
|
||||
_convert_f_to_x_or_z(((a.nil? ? 0 : a) / 500.0) + f1) * Conversions::D50[0],
|
||||
(if lightness > Utils::LAB_KAPPA * Utils::LAB_EPSILON
|
||||
(((lightness + 16) / 116.0)**3)
|
||||
else
|
||||
lightness / Utils::LAB_KAPPA
|
||||
end) * Conversions::D50[1],
|
||||
_convert_f_to_x_or_z(f1 - ((b.nil? ? 0 : b) / 200.0)) * Conversions::D50[2],
|
||||
alpha,
|
||||
missing_lightness:,
|
||||
missing_chroma:,
|
||||
missing_hue:,
|
||||
missing_a: a.nil?,
|
||||
missing_b: b.nil?
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def _convert_f_to_x_or_z(component)
|
||||
cubed = component**3
|
||||
cubed > Utils::LAB_EPSILON ? cubed : ((116 * component) - 16) / Utils::LAB_KAPPA
|
||||
end
|
||||
end
|
||||
|
||||
private_constant :Lab
|
||||
|
||||
LAB = Lab.new
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,53 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Sass
|
||||
module Value
|
||||
class Color
|
||||
module Space
|
||||
# @see https://github.com/sass/dart-sass/blob/main/lib/src/value/color/space/lch.dart
|
||||
class Lch
|
||||
include Space
|
||||
|
||||
def bounded?
|
||||
false
|
||||
end
|
||||
|
||||
def polar?
|
||||
true
|
||||
end
|
||||
|
||||
def initialize
|
||||
super('lch', [
|
||||
LinearChannel.new('lightness', 0, 100, lower_clamped: true, upper_clamped: true).freeze,
|
||||
LinearChannel.new('chroma', 0, 150, lower_clamped: true).freeze,
|
||||
Utils::HUE_CHANNEL
|
||||
].freeze)
|
||||
end
|
||||
|
||||
def convert(dest, lightness, chroma, hue, alpha)
|
||||
missing_chroma = chroma.nil?
|
||||
missing_hue = hue.nil?
|
||||
|
||||
chroma = 0 if missing_chroma
|
||||
hue = 0 if missing_hue
|
||||
|
||||
hue_radians = hue * Math::PI / 180
|
||||
LAB.convert(
|
||||
dest,
|
||||
lightness,
|
||||
chroma * Math.cos(hue_radians),
|
||||
chroma * Math.sin(hue_radians),
|
||||
alpha,
|
||||
missing_chroma:,
|
||||
missing_hue:
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
private_constant :Lch
|
||||
|
||||
LCH = Lch.new
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,129 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Sass
|
||||
module Value
|
||||
class Color
|
||||
module Space
|
||||
# @see https://github.com/sass/dart-sass/blob/main/lib/src/value/color/space/lms.dart
|
||||
class Lms
|
||||
include Space
|
||||
|
||||
def bounded?
|
||||
false
|
||||
end
|
||||
|
||||
def initialize
|
||||
super('lms', [
|
||||
LinearChannel.new('long', 0, 1).freeze,
|
||||
LinearChannel.new('medium', 0, 1).freeze,
|
||||
LinearChannel.new('short', 0, 1).freeze
|
||||
].freeze)
|
||||
end
|
||||
|
||||
def convert(dest, long, medium, short, alpha,
|
||||
missing_lightness: false,
|
||||
missing_chroma: false,
|
||||
missing_hue: false,
|
||||
missing_a: false,
|
||||
missing_b: false)
|
||||
case dest
|
||||
when OKLAB
|
||||
long_scaled = Math.cbrt(long.nil? ? 0 : long)
|
||||
medium_scaled = Math.cbrt(medium.nil? ? 0 : medium)
|
||||
short_scaled = Math.cbrt(short.nil? ? 0 : short)
|
||||
|
||||
Color.send(
|
||||
:_for_space,
|
||||
dest,
|
||||
unless missing_lightness
|
||||
(Conversions::LMS_TO_OKLAB[0] * long_scaled) +
|
||||
(Conversions::LMS_TO_OKLAB[1] * medium_scaled) +
|
||||
(Conversions::LMS_TO_OKLAB[2] * short_scaled)
|
||||
end,
|
||||
unless missing_a
|
||||
(Conversions::LMS_TO_OKLAB[3] * long_scaled) +
|
||||
(Conversions::LMS_TO_OKLAB[4] * medium_scaled) +
|
||||
(Conversions::LMS_TO_OKLAB[5] * short_scaled)
|
||||
end,
|
||||
unless missing_b
|
||||
(Conversions::LMS_TO_OKLAB[6] * long_scaled) +
|
||||
(Conversions::LMS_TO_OKLAB[7] * medium_scaled) +
|
||||
(Conversions::LMS_TO_OKLAB[8] * short_scaled)
|
||||
end,
|
||||
alpha
|
||||
)
|
||||
when OKLCH
|
||||
long_scaled = Math.cbrt(long.nil? ? 0 : long)
|
||||
medium_scaled = Math.cbrt(medium.nil? ? 0 : medium)
|
||||
short_scaled = Math.cbrt(short.nil? ? 0 : short)
|
||||
|
||||
Utils.lab_to_lch(
|
||||
dest,
|
||||
unless missing_lightness
|
||||
(Conversions::LMS_TO_OKLAB[0] * long_scaled) +
|
||||
(Conversions::LMS_TO_OKLAB[1] * medium_scaled) +
|
||||
(Conversions::LMS_TO_OKLAB[2] * short_scaled)
|
||||
end,
|
||||
unless missing_a
|
||||
(Conversions::LMS_TO_OKLAB[3] * long_scaled) +
|
||||
(Conversions::LMS_TO_OKLAB[4] * medium_scaled) +
|
||||
(Conversions::LMS_TO_OKLAB[5] * short_scaled)
|
||||
end,
|
||||
unless missing_b
|
||||
(Conversions::LMS_TO_OKLAB[6] * long_scaled) +
|
||||
(Conversions::LMS_TO_OKLAB[7] * medium_scaled) +
|
||||
(Conversions::LMS_TO_OKLAB[8] * short_scaled)
|
||||
end,
|
||||
alpha
|
||||
)
|
||||
else
|
||||
convert_linear(dest, long, medium, short, alpha,
|
||||
missing_lightness:,
|
||||
missing_chroma:,
|
||||
missing_hue:,
|
||||
missing_a:,
|
||||
missing_b:)
|
||||
end
|
||||
end
|
||||
|
||||
def to_linear(channel)
|
||||
channel
|
||||
end
|
||||
|
||||
def from_linear(channel)
|
||||
channel
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def transformation_matrix(dest)
|
||||
case dest
|
||||
when A98_RGB
|
||||
Conversions::LMS_TO_LINEAR_A98_RGB
|
||||
when DISPLAY_P3
|
||||
Conversions::LMS_TO_LINEAR_DISPLAY_P3
|
||||
when PROPHOTO_RGB
|
||||
Conversions::LMS_TO_LINEAR_PROPHOTO_RGB
|
||||
when REC2020
|
||||
Conversions::LMS_TO_LINEAR_REC2020
|
||||
when RGB, SRGB, SRGB_LINEAR
|
||||
Conversions::LMS_TO_LINEAR_SRGB
|
||||
when XYZ_D50
|
||||
Conversions::LMS_TO_XYZ_D50
|
||||
when XYZ_D65
|
||||
Conversions::LMS_TO_XYZ_D65
|
||||
else
|
||||
super
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private_constant :Lms
|
||||
|
||||
LMS = Lms.new
|
||||
|
||||
private_constant :LMS
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,66 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Sass
|
||||
module Value
|
||||
class Color
|
||||
module Space
|
||||
# @see https://github.com/sass/dart-sass/blob/main/lib/src/value/color/space/oklab.dart
|
||||
class Oklab
|
||||
include Space
|
||||
|
||||
def bounded?
|
||||
false
|
||||
end
|
||||
|
||||
def initialize
|
||||
super('oklab', [
|
||||
LinearChannel.new('lightness', 0, 1,
|
||||
conventionally_percent: true, lower_clamped: true, upper_clamped: true).freeze,
|
||||
LinearChannel.new('a', -0.4, 0.4).freeze,
|
||||
LinearChannel.new('b', -0.4, 0.4).freeze
|
||||
].freeze)
|
||||
end
|
||||
|
||||
def convert(dest, lightness, a, b, alpha, # rubocop:disable Naming/MethodParameterName
|
||||
missing_chroma: false, missing_hue: false)
|
||||
case dest
|
||||
when OKLCH
|
||||
Utils.lab_to_lch(dest, lightness, a, b, alpha)
|
||||
else
|
||||
missing_lightness = lightness.nil?
|
||||
missing_a = a.nil?
|
||||
missing_b = b.nil?
|
||||
|
||||
lightness = 0 if missing_lightness
|
||||
a = 0 if missing_a
|
||||
b = 0 if missing_b
|
||||
|
||||
LMS.convert(
|
||||
dest,
|
||||
((Conversions::OKLAB_TO_LMS[0] * lightness) +
|
||||
(Conversions::OKLAB_TO_LMS[1] * a) +
|
||||
(Conversions::OKLAB_TO_LMS[2] * b))**3,
|
||||
((Conversions::OKLAB_TO_LMS[3] * lightness) +
|
||||
(Conversions::OKLAB_TO_LMS[4] * a) +
|
||||
(Conversions::OKLAB_TO_LMS[5] * b))**3,
|
||||
((Conversions::OKLAB_TO_LMS[6] * lightness) +
|
||||
(Conversions::OKLAB_TO_LMS[7] * a) +
|
||||
(Conversions::OKLAB_TO_LMS[8] * b))**3,
|
||||
alpha,
|
||||
missing_lightness:,
|
||||
missing_chroma:,
|
||||
missing_hue:,
|
||||
missing_a:,
|
||||
missing_b:
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private_constant :Oklab
|
||||
|
||||
OKLAB = Oklab.new
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,54 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Sass
|
||||
module Value
|
||||
class Color
|
||||
module Space
|
||||
# @see https://github.com/sass/dart-sass/blob/main/lib/src/value/color/space/oklch.dart
|
||||
class Oklch
|
||||
include Space
|
||||
|
||||
def bounded?
|
||||
false
|
||||
end
|
||||
|
||||
def polar?
|
||||
true
|
||||
end
|
||||
|
||||
def initialize
|
||||
super('oklch', [
|
||||
LinearChannel.new('lightness', 0, 1,
|
||||
conventionally_percent: true, lower_clamped: true, upper_clamped: true).freeze,
|
||||
LinearChannel.new('chroma', 0, 0.4, lower_clamped: true).freeze,
|
||||
Utils::HUE_CHANNEL
|
||||
].freeze)
|
||||
end
|
||||
|
||||
def convert(dest, lightness, chroma, hue, alpha)
|
||||
missing_chroma = chroma.nil?
|
||||
missing_hue = hue.nil?
|
||||
|
||||
chroma = 0 if missing_chroma
|
||||
hue = 0 if missing_hue
|
||||
|
||||
hue_radians = hue * Math::PI / 180
|
||||
OKLAB.convert(
|
||||
dest,
|
||||
lightness,
|
||||
chroma * Math.cos(hue_radians),
|
||||
chroma * Math.sin(hue_radians),
|
||||
alpha,
|
||||
missing_chroma:,
|
||||
missing_hue:
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
private_constant :Oklch
|
||||
|
||||
OKLCH = Oklch.new
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Sass
|
||||
module Value
|
||||
class Color
|
||||
module Space
|
||||
# @see https://github.com/sass/dart-sass/blob/main/lib/src/value/color/space/prophoto_rgb.dart
|
||||
class ProphotoRgb
|
||||
include Space
|
||||
|
||||
def bounded?
|
||||
true
|
||||
end
|
||||
|
||||
def initialize
|
||||
super('prophoto-rgb', Utils::RGB_CHANNELS)
|
||||
end
|
||||
|
||||
def to_linear(channel)
|
||||
abs = channel.abs
|
||||
abs <= 16 / 512.0 ? channel / 16.0 : (channel <=> 0) * (abs**1.8)
|
||||
end
|
||||
|
||||
def from_linear(channel)
|
||||
abs = channel.abs
|
||||
abs >= 1 / 512.0 ? (channel <=> 0) * (abs**(1 / 1.8)) : 16 * channel
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def transformation_matrix(dest)
|
||||
case dest
|
||||
when A98_RGB
|
||||
Conversions::LINEAR_PROPHOTO_RGB_TO_LINEAR_A98_RGB
|
||||
when DISPLAY_P3
|
||||
Conversions::LINEAR_PROPHOTO_RGB_TO_LINEAR_DISPLAY_P3
|
||||
when LMS
|
||||
Conversions::LINEAR_PROPHOTO_RGB_TO_LMS
|
||||
when REC2020
|
||||
Conversions::LINEAR_PROPHOTO_RGB_TO_LINEAR_REC2020
|
||||
when RGB, SRGB, SRGB_LINEAR
|
||||
Conversions::LINEAR_PROPHOTO_RGB_TO_LINEAR_SRGB
|
||||
when XYZ_D50
|
||||
Conversions::LINEAR_PROPHOTO_RGB_TO_XYZ_D50
|
||||
when XYZ_D65
|
||||
Conversions::LINEAR_PROPHOTO_RGB_TO_XYZ_D65
|
||||
else
|
||||
super
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private_constant :ProphotoRgb
|
||||
|
||||
PROPHOTO_RGB = ProphotoRgb.new
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,69 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Sass
|
||||
module Value
|
||||
class Color
|
||||
module Space
|
||||
# @see https://github.com/sass/dart-sass/blob/main/lib/src/value/color/space/rec2020.dart
|
||||
class Rec2020
|
||||
include Space
|
||||
|
||||
# A constant used in the rec2020 gamma encoding/decoding functions.
|
||||
ALPHA = 1.09929682680944
|
||||
|
||||
private_constant :ALPHA
|
||||
|
||||
# A constant used in the rec2020 gamma encoding/decoding functions.
|
||||
BETA = 0.018053968510807
|
||||
|
||||
private_constant :BETA
|
||||
|
||||
def bounded?
|
||||
true
|
||||
end
|
||||
|
||||
def initialize
|
||||
super('rec2020', Utils::RGB_CHANNELS)
|
||||
end
|
||||
|
||||
def to_linear(channel)
|
||||
abs = channel.abs
|
||||
abs < BETA * 4.5 ? channel / 4.5 : (channel <=> 0) * (((abs + ALPHA - 1) / ALPHA)**(1 / 0.45))
|
||||
end
|
||||
|
||||
def from_linear(channel)
|
||||
abs = channel.abs
|
||||
abs > BETA ? (channel <=> 0) * ((ALPHA * (abs**0.45)) - (ALPHA - 1)) : 4.5 * channel
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def transformation_matrix(dest)
|
||||
case dest
|
||||
when A98_RGB
|
||||
Conversions::LINEAR_REC2020_TO_LINEAR_A98_RGB
|
||||
when DISPLAY_P3
|
||||
Conversions::LINEAR_REC2020_TO_LINEAR_DISPLAY_P3
|
||||
when LMS
|
||||
Conversions::LINEAR_REC2020_TO_LMS
|
||||
when PROPHOTO_RGB
|
||||
Conversions::LINEAR_REC2020_TO_LINEAR_PROPHOTO_RGB
|
||||
when RGB, SRGB, SRGB_LINEAR
|
||||
Conversions::LINEAR_REC2020_TO_LINEAR_SRGB
|
||||
when XYZ_D50
|
||||
Conversions::LINEAR_REC2020_TO_XYZ_D50
|
||||
when XYZ_D65
|
||||
Conversions::LINEAR_REC2020_TO_XYZ_D65
|
||||
else
|
||||
super
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private_constant :Rec2020
|
||||
|
||||
REC2020 = Rec2020.new
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,52 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Sass
|
||||
module Value
|
||||
class Color
|
||||
module Space
|
||||
# @see https://github.com/sass/dart-sass/blob/main/lib/src/value/color/space/rgb.dart
|
||||
class Rgb
|
||||
include Space
|
||||
|
||||
def bounded?
|
||||
true
|
||||
end
|
||||
|
||||
def legacy?
|
||||
true
|
||||
end
|
||||
|
||||
def initialize
|
||||
super('rgb', [
|
||||
LinearChannel.new('red', 0, 255, lower_clamped: true, upper_clamped: true).freeze,
|
||||
LinearChannel.new('green', 0, 255, lower_clamped: true, upper_clamped: true).freeze,
|
||||
LinearChannel.new('blue', 0, 255, lower_clamped: true, upper_clamped: true).freeze
|
||||
].freeze)
|
||||
end
|
||||
|
||||
def convert(dest, red, green, blue, alpha)
|
||||
SRGB.convert(
|
||||
dest,
|
||||
red.nil? ? nil : red / 255.0,
|
||||
green.nil? ? nil : green / 255.0,
|
||||
blue.nil? ? nil : blue / 255.0,
|
||||
alpha
|
||||
)
|
||||
end
|
||||
|
||||
def to_linear(channel)
|
||||
Utils.srgb_and_display_p3_to_linear(channel / 255.0)
|
||||
end
|
||||
|
||||
def from_linear(channel)
|
||||
Utils.srgb_and_display_p3_from_linear(channel) * 255
|
||||
end
|
||||
end
|
||||
|
||||
private_constant :Rgb
|
||||
|
||||
RGB = Rgb.new
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,140 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Sass
|
||||
module Value
|
||||
class Color
|
||||
module Space
|
||||
# @see https://github.com/sass/dart-sass/blob/main/lib/src/value/color/space/srgb.dart
|
||||
class Srgb
|
||||
include Space
|
||||
|
||||
def bounded?
|
||||
true
|
||||
end
|
||||
|
||||
def initialize
|
||||
super('srgb', Utils::RGB_CHANNELS)
|
||||
end
|
||||
|
||||
def convert(dest, red, green, blue, alpha,
|
||||
missing_lightness: false,
|
||||
missing_chroma: false,
|
||||
missing_hue: false)
|
||||
case dest
|
||||
when HSL, HWB
|
||||
red = 0 if red.nil?
|
||||
green = 0 if green.nil?
|
||||
blue = 0 if blue.nil?
|
||||
|
||||
min, max = [red, green, blue].minmax
|
||||
delta = max - min
|
||||
|
||||
hue = if max == min
|
||||
0.0
|
||||
elsif max == red
|
||||
(60.0 * (green - blue) / delta) + 360
|
||||
elsif max == green
|
||||
(60.0 * (blue - red) / delta) + 120
|
||||
else # max == blue
|
||||
(60.0 * (red - green) / delta) + 240
|
||||
end
|
||||
|
||||
if dest == HSL
|
||||
lightness = (min + max) / 2.0
|
||||
|
||||
saturation = if [0, 1].include?(lightness)
|
||||
0.0
|
||||
else
|
||||
100.0 * (max - lightness) / [lightness, 1 - lightness].min
|
||||
end
|
||||
if saturation.negative?
|
||||
hue += 180
|
||||
saturation = saturation.abs
|
||||
end
|
||||
|
||||
Color.send(
|
||||
:for_space_internal,
|
||||
dest,
|
||||
missing_hue || FuzzyMath.equals(saturation, 0) ? nil : hue % 360,
|
||||
missing_chroma ? nil : saturation,
|
||||
missing_lightness ? nil : lightness * 100,
|
||||
alpha
|
||||
)
|
||||
else
|
||||
whiteness = min * 100
|
||||
blackness = 100 - (max * 100)
|
||||
|
||||
Color.send(
|
||||
:for_space_internal,
|
||||
dest,
|
||||
missing_hue || FuzzyMath.greater_than_or_equals(whiteness + blackness, 100) ? nil : hue % 360,
|
||||
whiteness,
|
||||
blackness,
|
||||
alpha
|
||||
)
|
||||
end
|
||||
when RGB
|
||||
Color.send(
|
||||
:_for_space,
|
||||
dest,
|
||||
red.nil? ? nil : red * 255,
|
||||
green.nil? ? nil : green * 255,
|
||||
blue.nil? ? nil : blue * 255,
|
||||
alpha
|
||||
)
|
||||
when SRGB_LINEAR
|
||||
Color.send(
|
||||
:_for_space,
|
||||
dest,
|
||||
red.nil? ? nil : to_linear(red),
|
||||
green.nil? ? nil : to_linear(green),
|
||||
blue.nil? ? nil : to_linear(blue),
|
||||
alpha
|
||||
)
|
||||
else
|
||||
convert_linear(dest, red, green, blue, alpha,
|
||||
missing_lightness:,
|
||||
missing_chroma:,
|
||||
missing_hue:)
|
||||
end
|
||||
end
|
||||
|
||||
def to_linear(channel)
|
||||
Utils.srgb_and_display_p3_to_linear(channel)
|
||||
end
|
||||
|
||||
def from_linear(channel)
|
||||
Utils.srgb_and_display_p3_from_linear(channel)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def transformation_matrix(dest)
|
||||
case dest
|
||||
when A98_RGB
|
||||
Conversions::LINEAR_SRGB_TO_LINEAR_A98_RGB
|
||||
when DISPLAY_P3
|
||||
Conversions::LINEAR_SRGB_TO_LINEAR_DISPLAY_P3
|
||||
when LMS
|
||||
Conversions::LINEAR_SRGB_TO_LMS
|
||||
when PROPHOTO_RGB
|
||||
Conversions::LINEAR_SRGB_TO_LINEAR_PROPHOTO_RGB
|
||||
when REC2020
|
||||
Conversions::LINEAR_SRGB_TO_LINEAR_REC2020
|
||||
when XYZ_D50
|
||||
Conversions::LINEAR_SRGB_TO_XYZ_D50
|
||||
when XYZ_D65
|
||||
Conversions::LINEAR_SRGB_TO_XYZ_D65
|
||||
else
|
||||
super
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private_constant :Srgb
|
||||
|
||||
SRGB = Srgb.new
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Sass
|
||||
module Value
|
||||
class Color
|
||||
module Space
|
||||
# @see https://github.com/sass/dart-sass/blob/main/lib/src/value/color/space/srgb_linear.dart
|
||||
class SrgbLinear
|
||||
include Space
|
||||
|
||||
def bounded?
|
||||
true
|
||||
end
|
||||
|
||||
def initialize
|
||||
super('srgb-linear', Utils::RGB_CHANNELS)
|
||||
end
|
||||
|
||||
def convert(dest, red, green, blue, alpha)
|
||||
case dest
|
||||
when HSL, HWB, RGB, SRGB
|
||||
SRGB.convert(
|
||||
dest,
|
||||
red.nil? ? nil : Utils.srgb_and_display_p3_from_linear(red),
|
||||
green.nil? ? nil : Utils.srgb_and_display_p3_from_linear(green),
|
||||
blue.nil? ? nil : Utils.srgb_and_display_p3_from_linear(blue),
|
||||
alpha
|
||||
)
|
||||
else
|
||||
super
|
||||
end
|
||||
end
|
||||
|
||||
def to_linear(channel)
|
||||
channel
|
||||
end
|
||||
|
||||
def from_linear(channel)
|
||||
channel
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def transformation_matrix(dest)
|
||||
case dest
|
||||
when A98_RGB
|
||||
Conversions::LINEAR_SRGB_TO_LINEAR_A98_RGB
|
||||
when DISPLAY_P3
|
||||
Conversions::LINEAR_SRGB_TO_LINEAR_DISPLAY_P3
|
||||
when PROPHOTO_RGB
|
||||
Conversions::LINEAR_SRGB_TO_LINEAR_PROPHOTO_RGB
|
||||
when REC2020
|
||||
Conversions::LINEAR_SRGB_TO_LINEAR_REC2020
|
||||
when XYZ_D65
|
||||
Conversions::LINEAR_SRGB_TO_XYZ_D65
|
||||
when XYZ_D50
|
||||
Conversions::LINEAR_SRGB_TO_XYZ_D50
|
||||
when LMS
|
||||
Conversions::LINEAR_SRGB_TO_LMS
|
||||
else
|
||||
super
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private_constant :SrgbLinear
|
||||
|
||||
SRGB_LINEAR = SrgbLinear.new
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,86 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Sass
|
||||
module Value
|
||||
class Color
|
||||
module Space
|
||||
# @see https://github.com/sass/dart-sass/blob/main/lib/src/value/color/space/utils.dart
|
||||
module Utils
|
||||
module_function
|
||||
|
||||
# A constant used to convert Lab to/from XYZ.
|
||||
LAB_KAPPA = Rational(24_389, 27) # 29^3/3^3
|
||||
|
||||
# A constant used to convert Lab to/from XYZ.
|
||||
LAB_EPSILON = Rational(216, 24_389) # 6^3/29^3
|
||||
|
||||
# The hue channel shared across all polar color spaces.
|
||||
HUE_CHANNEL = ColorChannel.new('hue', polar_angle: true, associated_unit: 'deg').freeze
|
||||
|
||||
# The color channels shared across all RGB color spaces (except the legacy RGB space).
|
||||
RGB_CHANNELS = [
|
||||
LinearChannel.new('red', 0, 1).freeze,
|
||||
LinearChannel.new('green', 0, 1).freeze,
|
||||
LinearChannel.new('blue', 0, 1).freeze
|
||||
].freeze
|
||||
|
||||
# The color channels shared across both XYZ color spaces.
|
||||
XYZ_CHANNELS = [
|
||||
LinearChannel.new('x', 0, 1).freeze,
|
||||
LinearChannel.new('y', 0, 1).freeze,
|
||||
LinearChannel.new('z', 0, 1).freeze
|
||||
].freeze
|
||||
|
||||
# The algorithm for converting a single `srgb` or `display-p3` channel to
|
||||
# linear-light form.
|
||||
# @param [Numeric]
|
||||
# @return [Numeric]
|
||||
def srgb_and_display_p3_to_linear(channel)
|
||||
abs = channel.abs
|
||||
abs <= 0.04045 ? channel / 12.92 : (channel <=> 0) * (((abs + 0.055) / 1.055)**2.4)
|
||||
end
|
||||
|
||||
# The algorithm for converting a single `srgb` or `display-p3` channel to
|
||||
# gamma-corrected form.
|
||||
# @param [Numeric]
|
||||
# @return [Numeric]
|
||||
def srgb_and_display_p3_from_linear(channel)
|
||||
abs = channel.abs
|
||||
abs <= 0.0031308 ? channel * 12.92 : (channel <=> 0) * ((1.055 * (abs**(1 / 2.4))) - 0.055)
|
||||
end
|
||||
|
||||
# Converts a Lab or OKLab color to LCH or OKLCH, respectively.
|
||||
#
|
||||
# The [missing_chroma] and [missing_hue] arguments indicate whether this came
|
||||
# from a color that was missing its chroma or hue channels, respectively.
|
||||
# @param dest [Space]
|
||||
# @param lightness [Numeric]
|
||||
# @param a [Numeric]
|
||||
# @param b [Numeric]
|
||||
# @param alpha [Numeric]
|
||||
# @return [Color]
|
||||
def lab_to_lch(dest, lightness, a, b, alpha, # rubocop:disable Naming/MethodParameterName
|
||||
missing_chroma: false, missing_hue: false)
|
||||
chroma = Math.sqrt(((a.nil? ? 0 : a)**2) + ((b.nil? ? 0 : b)**2))
|
||||
hue = if missing_hue || FuzzyMath.equals(chroma, 0)
|
||||
nil
|
||||
else
|
||||
Math.atan2(b.nil? ? 0 : b, a.nil? ? 0 : a) * 180 / Math::PI
|
||||
end
|
||||
|
||||
Color.send(
|
||||
:for_space_internal,
|
||||
dest,
|
||||
lightness,
|
||||
missing_chroma ? nil : chroma,
|
||||
hue.nil? || hue >= 0 ? hue : hue + 360,
|
||||
alpha
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
private_constant :Utils
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,100 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Sass
|
||||
module Value
|
||||
class Color
|
||||
module Space
|
||||
# @see https://github.com/sass/dart-sass/blob/main/lib/src/value/color/space/xyz_d50.dart
|
||||
class XyzD50
|
||||
include Space
|
||||
|
||||
def bounded?
|
||||
false
|
||||
end
|
||||
|
||||
def initialize
|
||||
super('xyz-d50', Utils::XYZ_CHANNELS)
|
||||
end
|
||||
|
||||
def convert(dest, x, y, z, alpha, # rubocop:disable Naming/MethodParameterName
|
||||
missing_lightness: false,
|
||||
missing_chroma: false,
|
||||
missing_hue: false,
|
||||
missing_a: false,
|
||||
missing_b: false)
|
||||
case dest
|
||||
when LAB, LCH
|
||||
f0 = _convert_component_to_lab_f((x.nil? ? 0 : x) / Conversions::D50[0])
|
||||
f1 = _convert_component_to_lab_f((y.nil? ? 0 : y) / Conversions::D50[1])
|
||||
f2 = _convert_component_to_lab_f((z.nil? ? 0 : z) / Conversions::D50[2])
|
||||
lightness = missing_lightness ? nil : (116 * f1) - 16
|
||||
a = 500 * (f0 - f1)
|
||||
b = 200 * (f1 - f2)
|
||||
|
||||
if dest == LAB
|
||||
Color.send(:_for_space,
|
||||
dest,
|
||||
lightness,
|
||||
missing_a ? nil : a,
|
||||
missing_b ? nil : b,
|
||||
alpha)
|
||||
else
|
||||
Utils.lab_to_lch(dest, lightness, a, b, alpha, missing_chroma:, missing_hue:)
|
||||
end
|
||||
else
|
||||
convert_linear(dest, x, y, z, alpha,
|
||||
missing_lightness:,
|
||||
missing_chroma:,
|
||||
missing_hue:,
|
||||
missing_a:,
|
||||
missing_b:)
|
||||
end
|
||||
end
|
||||
|
||||
def to_linear(channel)
|
||||
channel
|
||||
end
|
||||
|
||||
def from_linear(channel)
|
||||
channel
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def _convert_component_to_lab_f(component)
|
||||
if component > Utils::LAB_EPSILON
|
||||
Math.cbrt(component)
|
||||
else
|
||||
((Utils::LAB_KAPPA * component) + 16) / 116.0
|
||||
end
|
||||
end
|
||||
|
||||
def transformation_matrix(dest)
|
||||
case dest
|
||||
when A98_RGB
|
||||
Conversions::XYZ_D50_TO_LINEAR_A98_RGB
|
||||
when DISPLAY_P3
|
||||
Conversions::XYZ_D50_TO_LINEAR_DISPLAY_P3
|
||||
when LMS
|
||||
Conversions::XYZ_D50_TO_LMS
|
||||
when PROPHOTO_RGB
|
||||
Conversions::XYZ_D50_TO_LINEAR_PROPHOTO_RGB
|
||||
when REC2020
|
||||
Conversions::XYZ_D50_TO_LINEAR_REC2020
|
||||
when RGB, SRGB, SRGB_LINEAR
|
||||
Conversions::XYZ_D50_TO_LINEAR_SRGB
|
||||
when XYZ_D65
|
||||
Conversions::XYZ_D50_TO_XYZ_D65
|
||||
else
|
||||
super
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private_constant :XyzD50
|
||||
|
||||
XYZ_D50 = XyzD50.new
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,57 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Sass
|
||||
module Value
|
||||
class Color
|
||||
module Space
|
||||
# @see https://github.com/sass/dart-sass/blob/main/lib/src/value/color/space/xyz_d65.dart
|
||||
class XyzD65
|
||||
include Space
|
||||
|
||||
def bounded?
|
||||
false
|
||||
end
|
||||
|
||||
def initialize
|
||||
super('xyz', Utils::XYZ_CHANNELS)
|
||||
end
|
||||
|
||||
def to_linear(channel)
|
||||
channel
|
||||
end
|
||||
|
||||
def from_linear(channel)
|
||||
channel
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def transformation_matrix(dest)
|
||||
case dest
|
||||
when A98_RGB
|
||||
Conversions::XYZ_D65_TO_LINEAR_A98_RGB
|
||||
when DISPLAY_P3
|
||||
Conversions::XYZ_D65_TO_LINEAR_DISPLAY_P3
|
||||
when LMS
|
||||
Conversions::XYZ_D65_TO_LMS
|
||||
when PROPHOTO_RGB
|
||||
Conversions::XYZ_D65_TO_LINEAR_PROPHOTO_RGB
|
||||
when REC2020
|
||||
Conversions::XYZ_D65_TO_LINEAR_REC2020
|
||||
when RGB, SRGB, SRGB_LINEAR
|
||||
Conversions::XYZ_D65_TO_LINEAR_SRGB
|
||||
when XYZ_D50
|
||||
Conversions::XYZ_D65_TO_XYZ_D50
|
||||
else
|
||||
super
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private_constant :XyzD65
|
||||
|
||||
XYZ_D65 = XyzD65.new
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,49 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Sass
|
||||
module Value
|
||||
# Sass's function type.
|
||||
#
|
||||
# @see https://sass-lang.com/documentation/js-api/classes/sassfunction/
|
||||
class Function
|
||||
include Value
|
||||
|
||||
# @param signature [::String]
|
||||
# @param callback [Proc]
|
||||
def initialize(signature, &callback)
|
||||
raise Sass::ScriptError, 'no block given' unless signature.nil? || callback
|
||||
|
||||
@signature = signature.freeze
|
||||
@callback = callback.freeze
|
||||
end
|
||||
|
||||
# @return [Integer, nil]
|
||||
protected attr_reader :id
|
||||
|
||||
# @return [::String, nil]
|
||||
attr_reader :signature
|
||||
|
||||
# @return [Proc, nil]
|
||||
attr_reader :callback
|
||||
|
||||
# @return [::Boolean]
|
||||
def ==(other)
|
||||
if id.nil?
|
||||
other.equal?(self)
|
||||
else
|
||||
other.is_a?(Sass::Value::Function) && other.id == id
|
||||
end
|
||||
end
|
||||
|
||||
# @return [Integer]
|
||||
def hash
|
||||
@hash ||= id.nil? ? signature.hash : id.hash
|
||||
end
|
||||
|
||||
# @return [Function]
|
||||
def assert_function(_name = nil)
|
||||
self
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,84 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Sass
|
||||
module Value
|
||||
# Sass's {FuzzyMath} module.
|
||||
module FuzzyMath
|
||||
PRECISION = 10
|
||||
|
||||
EPSILON = 10**(-PRECISION - 1)
|
||||
|
||||
INVERSE_EPSILON = 10**(PRECISION + 1)
|
||||
|
||||
module_function
|
||||
|
||||
def equals(number1, number2)
|
||||
return true if number1 == number2
|
||||
|
||||
(number1 - number2).abs <= EPSILON &&
|
||||
(number1 * INVERSE_EPSILON).round ==
|
||||
(number2 * INVERSE_EPSILON).round
|
||||
end
|
||||
|
||||
def equals_nilable(number1, number2)
|
||||
return true if number1 == number2
|
||||
return false if number1.nil? || number2.nil?
|
||||
|
||||
(number1 - number2).abs <= EPSILON &&
|
||||
(number1 * INVERSE_EPSILON).round ==
|
||||
(number2 * INVERSE_EPSILON).round
|
||||
end
|
||||
|
||||
def less_than(number1, number2)
|
||||
number1 < number2 && !equals(number1, number2)
|
||||
end
|
||||
|
||||
def less_than_or_equals(number1, number2)
|
||||
number1 < number2 || equals(number1, number2)
|
||||
end
|
||||
|
||||
def greater_than(number1, number2)
|
||||
number1 > number2 && !equals(number1, number2)
|
||||
end
|
||||
|
||||
def greater_than_or_equals(number1, number2)
|
||||
number1 > number2 || equals(number1, number2)
|
||||
end
|
||||
|
||||
def integer?(number)
|
||||
return false unless number.finite?
|
||||
|
||||
equals(number, number.round)
|
||||
end
|
||||
|
||||
def to_i(number)
|
||||
integer?(number) ? number.round : nil
|
||||
end
|
||||
|
||||
def between(number, min, max)
|
||||
return min if equals(number, min)
|
||||
return max if equals(number, max)
|
||||
return number if number > min && number < max
|
||||
|
||||
nil
|
||||
end
|
||||
|
||||
def assert_between(number, min, max, name)
|
||||
result = between(number, min, max)
|
||||
return result unless result.nil?
|
||||
|
||||
raise Sass::ScriptError.new("#{number} must be between #{min} and #{max}.", name)
|
||||
end
|
||||
|
||||
def _clamp_like_css(number, lower_bound, upper_bound)
|
||||
number.to_f.nan? ? lower_bound : number.clamp(lower_bound, upper_bound)
|
||||
end
|
||||
|
||||
def _hash(number)
|
||||
number&.finite? ? (number * INVERSE_EPSILON).round : number
|
||||
end
|
||||
end
|
||||
|
||||
private_constant :FuzzyMath
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,79 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Sass
|
||||
module Value
|
||||
# Sass's list type.
|
||||
#
|
||||
# @see https://sass-lang.com/documentation/js-api/classes/sasslist/
|
||||
class List
|
||||
include Value
|
||||
|
||||
# @param contents [Array<Value>]
|
||||
# @param separator [::String]
|
||||
# @param bracketed [::Boolean]
|
||||
def initialize(contents = [], separator: ',', bracketed: false)
|
||||
if separator.nil? && contents.length > 1
|
||||
raise Sass::ScriptError, 'A list with more than one element must have an explicit separator'
|
||||
end
|
||||
|
||||
@contents = contents.freeze
|
||||
@separator = separator.freeze
|
||||
@bracketed = bracketed.freeze
|
||||
end
|
||||
|
||||
# @return [::String, nil]
|
||||
attr_reader :separator
|
||||
|
||||
# @return [::Boolean]
|
||||
def bracketed?
|
||||
@bracketed
|
||||
end
|
||||
|
||||
# @return [::Boolean]
|
||||
def ==(other)
|
||||
(other.is_a?(Sass::Value::List) &&
|
||||
other.to_a == to_a &&
|
||||
other.separator == separator &&
|
||||
other.bracketed? == bracketed?) ||
|
||||
(to_a.empty? && other.is_a?(Sass::Value::Map) && other.to_a.empty?)
|
||||
end
|
||||
|
||||
# @param index [Numeric]
|
||||
# @return [Value]
|
||||
def at(index)
|
||||
index = index.floor
|
||||
index = to_a.length + index if index.negative?
|
||||
return nil if index.negative? || index >= to_a.length
|
||||
|
||||
to_a[index]
|
||||
end
|
||||
|
||||
# @return [Integer]
|
||||
def hash
|
||||
@hash ||= contents.hash
|
||||
end
|
||||
|
||||
# @return [Array<Value>]
|
||||
def to_a
|
||||
@contents
|
||||
end
|
||||
|
||||
# @return [Map, nil]
|
||||
def to_map
|
||||
to_a.empty? ? Sass::Value::Map.new({}) : nil
|
||||
end
|
||||
|
||||
# @return [Map]
|
||||
# @raise [ScriptError]
|
||||
def assert_map(name = nil)
|
||||
to_a.empty? ? Sass::Value::Map.new({}) : super.assert_map(name)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def to_a_length
|
||||
to_a.length
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,71 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Sass
|
||||
module Value
|
||||
# Sass's map type.
|
||||
#
|
||||
# @see https://sass-lang.com/documentation/js-api/classes/sassmap/
|
||||
class Map
|
||||
include Value
|
||||
|
||||
# @param contents [Hash<Value, Value>]
|
||||
def initialize(contents = {})
|
||||
@contents = contents.freeze
|
||||
end
|
||||
|
||||
# @return [Hash<Value, Value>]
|
||||
attr_reader :contents
|
||||
|
||||
# @return [::String, nil]
|
||||
def separator
|
||||
contents.empty? ? nil : ','
|
||||
end
|
||||
|
||||
# @return [::Boolean]
|
||||
def ==(other)
|
||||
(other.is_a?(Sass::Value::Map) && other.contents == contents) ||
|
||||
(contents.empty? && other.is_a?(Sass::Value::List) && other.to_a.empty?)
|
||||
end
|
||||
|
||||
# @param index [Numeric, Value]
|
||||
# @return [List<(Value, Value)>, Value]
|
||||
def at(index)
|
||||
if index.is_a?(Numeric)
|
||||
index = index.floor
|
||||
index = to_a_length + index if index.negative?
|
||||
return nil if index.negative? || index >= to_a_length
|
||||
|
||||
Sass::Value::List.new(contents.to_a[index], separator: ' ')
|
||||
else
|
||||
contents[index]
|
||||
end
|
||||
end
|
||||
|
||||
# @return [Integer]
|
||||
def hash
|
||||
@hash ||= contents.hash
|
||||
end
|
||||
|
||||
# @return [Array<List<(Value, Value)>>]
|
||||
def to_a
|
||||
contents.map { |key, value| Sass::Value::List.new([key, value], separator: ' ') }
|
||||
end
|
||||
|
||||
# @return [Map]
|
||||
def to_map
|
||||
self
|
||||
end
|
||||
|
||||
# @return [Map]
|
||||
def assert_map(_name = nil)
|
||||
self
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def to_a_length
|
||||
contents.length
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,34 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Sass
|
||||
module Value
|
||||
# Sass's mixin type.
|
||||
#
|
||||
# @see https://sass-lang.com/documentation/js-api/classes/sassmixin/
|
||||
class Mixin
|
||||
include Value
|
||||
|
||||
class << self
|
||||
private :new
|
||||
end
|
||||
|
||||
# @return [Integer]
|
||||
protected attr_reader :id
|
||||
|
||||
# @return [::Boolean]
|
||||
def ==(other)
|
||||
other.is_a?(Sass::Value::Mixin) && other.id == id
|
||||
end
|
||||
|
||||
# @return [Integer]
|
||||
def hash
|
||||
@hash ||= id.hash
|
||||
end
|
||||
|
||||
# @return [Mixin]
|
||||
def assert_mixin(_name = nil)
|
||||
self
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,48 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Sass
|
||||
module Value
|
||||
# Sass's null type.
|
||||
#
|
||||
# @see https://sass-lang.com/documentation/js-api/variables/sassnull/
|
||||
class Null
|
||||
include Value
|
||||
|
||||
def initialize
|
||||
@value = nil
|
||||
end
|
||||
|
||||
# @return [nil]
|
||||
attr_reader :value
|
||||
|
||||
# @return [Boolean]
|
||||
def !
|
||||
Boolean::TRUE
|
||||
end
|
||||
|
||||
# @return [::Boolean]
|
||||
def ==(other)
|
||||
other.is_a?(Sass::Value::Null)
|
||||
end
|
||||
|
||||
# @return [Integer]
|
||||
def hash
|
||||
@hash ||= value.hash
|
||||
end
|
||||
|
||||
# @return [::Boolean]
|
||||
def to_bool
|
||||
false
|
||||
end
|
||||
|
||||
alias to_nil value
|
||||
|
||||
# Sass's null value.
|
||||
NULL = Null.new
|
||||
|
||||
def self.new
|
||||
NULL
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,366 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require_relative 'number/unit'
|
||||
|
||||
module Sass
|
||||
module Value
|
||||
# Sass's number type.
|
||||
#
|
||||
# @see https://sass-lang.com/documentation/js-api/classes/sassnumber/
|
||||
class Number
|
||||
include Value
|
||||
include CalculationValue
|
||||
|
||||
# @param value [Numeric]
|
||||
# @param unit [::String, Hash]
|
||||
# @option unit [Array<::String>] :numerator_units
|
||||
# @option unit [Array<::String>] :denominator_units
|
||||
def initialize(value, unit = nil)
|
||||
case unit
|
||||
when nil
|
||||
numerator_units = []
|
||||
denominator_units = []
|
||||
when ::String
|
||||
numerator_units = [unit]
|
||||
denominator_units = []
|
||||
when ::Hash
|
||||
numerator_units = unit.fetch(:numerator_units, [])
|
||||
unless numerator_units.is_a?(Array)
|
||||
raise Sass::ScriptError, "invalid numerator_units #{numerator_units.inspect}"
|
||||
end
|
||||
|
||||
denominator_units = unit.fetch(:denominator_units, [])
|
||||
unless denominator_units.is_a?(Array)
|
||||
raise Sass::ScriptError, "invalid denominator_units #{denominator_units.inspect}"
|
||||
end
|
||||
else
|
||||
raise Sass::ScriptError, "invalid unit #{unit.inspect}"
|
||||
end
|
||||
|
||||
unless denominator_units.empty? && numerator_units.empty?
|
||||
value = value.dup
|
||||
numerator_units = numerator_units.dup
|
||||
new_denominator_units = []
|
||||
|
||||
denominator_units.each do |denominator_unit|
|
||||
index = numerator_units.find_index do |numerator_unit|
|
||||
factor = Unit.conversion_factor(denominator_unit, numerator_unit)
|
||||
if factor.nil?
|
||||
false
|
||||
else
|
||||
value *= factor
|
||||
true
|
||||
end
|
||||
end
|
||||
if index.nil?
|
||||
new_denominator_units.push(denominator_unit)
|
||||
else
|
||||
numerator_units.delete_at(index)
|
||||
end
|
||||
end
|
||||
|
||||
denominator_units = new_denominator_units
|
||||
end
|
||||
|
||||
@value = value.freeze
|
||||
@numerator_units = numerator_units.each(&:freeze).freeze
|
||||
@denominator_units = denominator_units.each(&:freeze).freeze
|
||||
end
|
||||
|
||||
# @return [Numeric]
|
||||
attr_reader :value
|
||||
|
||||
# @return [Array<::String>]
|
||||
attr_reader :numerator_units, :denominator_units
|
||||
|
||||
# @return [::Boolean]
|
||||
def ==(other)
|
||||
return false unless other.is_a?(Sass::Value::Number)
|
||||
|
||||
return false if numerator_units.length != other.numerator_units.length ||
|
||||
denominator_units.length != other.denominator_units.length
|
||||
|
||||
return FuzzyMath.equals(value, other.value) if unitless?
|
||||
|
||||
if Unit.canonicalize_units(numerator_units) != Unit.canonicalize_units(other.numerator_units) &&
|
||||
Unit.canonicalize_units(denominator_units) != Unit.canonicalize_units(other.denominator_units)
|
||||
return false
|
||||
end
|
||||
|
||||
FuzzyMath.equals(
|
||||
(value *
|
||||
Unit.canonical_multiplier(numerator_units) /
|
||||
Unit.canonical_multiplier(denominator_units)),
|
||||
(other.value *
|
||||
Unit.canonical_multiplier(other.numerator_units) /
|
||||
Unit.canonical_multiplier(other.denominator_units))
|
||||
)
|
||||
end
|
||||
|
||||
# @return [Integer]
|
||||
def hash
|
||||
@hash ||= FuzzyMath._hash(canonical_units_value).hash
|
||||
end
|
||||
|
||||
# @return [::Boolean]
|
||||
def unitless?
|
||||
numerator_units.empty? && denominator_units.empty?
|
||||
end
|
||||
|
||||
# @return [Number]
|
||||
# @raise [ScriptError]
|
||||
def assert_unitless(name = nil)
|
||||
raise Sass::ScriptError.new("Expected #{self} to have no units", name) unless unitless?
|
||||
|
||||
self
|
||||
end
|
||||
|
||||
# @return [::Boolean]
|
||||
def units?
|
||||
!unitless?
|
||||
end
|
||||
|
||||
# @param unit [::String]
|
||||
# @return [::Boolean]
|
||||
def unit?(unit)
|
||||
single_unit? && numerator_units.first == unit
|
||||
end
|
||||
|
||||
# @param unit [::String]
|
||||
# @return [Number]
|
||||
# @raise [ScriptError]
|
||||
def assert_unit(unit, name = nil)
|
||||
raise Sass::ScriptError.new("Expected #{self} to have unit #{unit.inspect}", name) unless unit?(unit)
|
||||
|
||||
self
|
||||
end
|
||||
|
||||
# @return [::Boolean]
|
||||
def integer?
|
||||
FuzzyMath.integer?(value)
|
||||
end
|
||||
|
||||
# @return [Integer]
|
||||
# @raise [ScriptError]
|
||||
def assert_integer(name = nil)
|
||||
raise Sass::ScriptError.new("#{self} is not an integer", name) unless integer?
|
||||
|
||||
to_i
|
||||
end
|
||||
|
||||
# @return [Integer]
|
||||
def to_i
|
||||
FuzzyMath.to_i(value)
|
||||
end
|
||||
|
||||
# @param min [Numeric]
|
||||
# @param max [Numeric]
|
||||
# @return [Numeric]
|
||||
# @raise [ScriptError]
|
||||
def assert_between(min, max, name = nil)
|
||||
FuzzyMath.assert_between(value, min, max, name)
|
||||
end
|
||||
|
||||
# @param unit [::String]
|
||||
# @return [::Boolean]
|
||||
def compatible_with_unit?(unit)
|
||||
single_unit? && !Unit.conversion_factor(numerator_units.first, unit).nil?
|
||||
end
|
||||
|
||||
# @param new_numerator_units [Array<::String>]
|
||||
# @param new_denominator_units [Array<::String>]
|
||||
# @return [Number]
|
||||
def convert(new_numerator_units, new_denominator_units, name = nil)
|
||||
Number.new(convert_value(new_numerator_units, new_denominator_units, name), {
|
||||
numerator_units: new_numerator_units,
|
||||
denominator_units: new_denominator_units
|
||||
})
|
||||
end
|
||||
|
||||
# @param new_numerator_units [Array<::String>]
|
||||
# @param new_denominator_units [Array<::String>]
|
||||
# @return [Numeric]
|
||||
def convert_value(new_numerator_units, new_denominator_units, name = nil)
|
||||
coerce_or_convert_value(new_numerator_units, new_denominator_units,
|
||||
coerce_unitless: false,
|
||||
name:)
|
||||
end
|
||||
|
||||
# @param other [Number]
|
||||
# @return [Number]
|
||||
def convert_to_match(other, name = nil, other_name = nil)
|
||||
Number.new(convert_value_to_match(other, name, other_name), {
|
||||
numerator_units: other.numerator_units,
|
||||
denominator_units: other.denominator_units
|
||||
})
|
||||
end
|
||||
|
||||
# @param other [Number]
|
||||
# @return [Numeric]
|
||||
def convert_value_to_match(other, name = nil, other_name = nil)
|
||||
coerce_or_convert_value(other.numerator_units, other.denominator_units,
|
||||
coerce_unitless: false,
|
||||
name:,
|
||||
other:,
|
||||
other_name:)
|
||||
end
|
||||
|
||||
# @param new_numerator_units [Array<::String>]
|
||||
# @param new_denominator_units [Array<::String>]
|
||||
# @return [Number]
|
||||
def coerce(new_numerator_units, new_denominator_units, name = nil)
|
||||
Number.new(coerce_value(new_numerator_units, new_denominator_units, name), {
|
||||
numerator_units: new_numerator_units,
|
||||
denominator_units: new_denominator_units
|
||||
})
|
||||
end
|
||||
|
||||
# @param new_numerator_units [Array<::String>]
|
||||
# @param new_denominator_units [Array<::String>]
|
||||
# @return [Numeric]
|
||||
def coerce_value(new_numerator_units, new_denominator_units, name = nil)
|
||||
coerce_or_convert_value(new_numerator_units, new_denominator_units,
|
||||
coerce_unitless: true,
|
||||
name:)
|
||||
end
|
||||
|
||||
# @param unit [::String]
|
||||
# @return [Numeric]
|
||||
def coerce_value_to_unit(unit, name = nil)
|
||||
coerce_value([unit], [], name)
|
||||
end
|
||||
|
||||
# @param other [Number]
|
||||
# @return [Number]
|
||||
def coerce_to_match(other, name = nil, other_name = nil)
|
||||
Number.new(coerce_value_to_match(other, name, other_name), {
|
||||
numerator_units: other.numerator_units,
|
||||
denominator_units: other.denominator_units
|
||||
})
|
||||
end
|
||||
|
||||
# @param other [Number]
|
||||
# @return [Numeric]
|
||||
def coerce_value_to_match(other, name = nil, other_name = nil)
|
||||
coerce_or_convert_value(other.numerator_units, other.denominator_units,
|
||||
coerce_unitless: true,
|
||||
name:,
|
||||
other:,
|
||||
other_name:)
|
||||
end
|
||||
|
||||
# @return [Number]
|
||||
def assert_number(_name = nil)
|
||||
self
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def single_unit?
|
||||
numerator_units.length == 1 && denominator_units.empty?
|
||||
end
|
||||
|
||||
def canonical_units_value
|
||||
if unitless?
|
||||
value
|
||||
elsif single_unit?
|
||||
value * Unit.canonical_multiplier_for_unit(numerator_units.first)
|
||||
else
|
||||
value * Unit.canonical_multiplier(numerator_units) / Unit.canonical_multiplier(denominator_units)
|
||||
end
|
||||
end
|
||||
|
||||
def coerce_or_convert_value(new_numerator_units, new_denominator_units,
|
||||
coerce_unitless:,
|
||||
name: nil,
|
||||
other: nil,
|
||||
other_name: nil)
|
||||
if other && (other.numerator_units != new_denominator_units && other.denominator_units != new_denominator_units)
|
||||
raise Sass::ScriptError, "Expect #{other} to have units #{unit_string(new_numerator_units,
|
||||
new_denominator_units).inspect}"
|
||||
end
|
||||
|
||||
return value if numerator_units == new_numerator_units && denominator_units == new_denominator_units
|
||||
|
||||
return value if numerator_units == new_numerator_units && denominator_units == new_denominator_units
|
||||
|
||||
other_unitless = new_numerator_units.empty? && new_denominator_units.empty?
|
||||
|
||||
return value if coerce_unitless && (unitless? || other_unitless)
|
||||
|
||||
compatibility_error = lambda {
|
||||
unless other.nil?
|
||||
message = "#{self} and"
|
||||
message << " $#{other_name}:" unless other_name.nil?
|
||||
message << " #{other} have incompatible units"
|
||||
message << " (one has units and the other doesn't)" if unitless? || other_unitless
|
||||
return Sass::ScriptError.new(message, name)
|
||||
end
|
||||
|
||||
return Sass::ScriptError.new("Expected #{self} to have no units", name) unless other_unitless
|
||||
|
||||
if new_numerator_units.length == 1 && new_denominator_units.empty?
|
||||
type = Unit::TYPES_BY_UNIT[new_numerator_units.first]
|
||||
return Sass::ScriptError.new(
|
||||
"Expected #{self} to have a #{type} unit (#{Unit::UNITS_BY_TYPE[type].join(', ')})", name
|
||||
)
|
||||
end
|
||||
|
||||
unit_length = new_numerator_units.length + new_denominator_units.length
|
||||
units = unit_string(new_numerator_units, new_denominator_units)
|
||||
Sass::ScriptError.new("Expected #{self} to have unit#{unit_length > 1 ? 's' : ''} #{units}", name)
|
||||
}
|
||||
|
||||
result = value
|
||||
|
||||
old_numerator_units = numerator_units.dup
|
||||
new_numerator_units.each do |new_numerator_unit|
|
||||
index = old_numerator_units.find_index do |old_numerator_unit|
|
||||
factor = Unit.conversion_factor(new_numerator_unit, old_numerator_unit)
|
||||
if factor.nil?
|
||||
false
|
||||
else
|
||||
result *= factor
|
||||
true
|
||||
end
|
||||
end
|
||||
raise compatibility_error.call if index.nil?
|
||||
|
||||
old_numerator_units.delete_at(index)
|
||||
end
|
||||
|
||||
old_denominator_units = denominator_units.dup
|
||||
new_denominator_units.each do |new_denominator_unit|
|
||||
index = old_denominator_units.find_index do |old_denominator_unit|
|
||||
factor = Unit.conversion_factor(new_denominator_unit, old_denominator_unit)
|
||||
if factor.nil?
|
||||
false
|
||||
else
|
||||
result /= factor
|
||||
true
|
||||
end
|
||||
end
|
||||
raise compatibility_error.call if index.nil?
|
||||
|
||||
old_denominator_units.delete_at(index)
|
||||
end
|
||||
|
||||
raise compatibility_error.call unless old_numerator_units.empty? && old_denominator_units.empty?
|
||||
|
||||
result
|
||||
end
|
||||
|
||||
def unit_string(numerator_units, denominator_units)
|
||||
if numerator_units.empty?
|
||||
return 'no units' if denominator_units.empty?
|
||||
|
||||
return denominator_units.length == 1 ? "#{denominator_units.first}^-1" : "(#{denominator_units.join('*')})^-1"
|
||||
end
|
||||
|
||||
return numerator_units.join('*') if denominator_units.empty?
|
||||
|
||||
"#{numerator_units.join('*')}/#{denominator_units.join('*')}"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,187 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Sass
|
||||
module Value
|
||||
class Number
|
||||
# The {Unit} module.
|
||||
module Unit
|
||||
CONVERSIONS = {
|
||||
# Length
|
||||
'in' => {
|
||||
'in' => Rational(1),
|
||||
'cm' => Rational(1, 2.54),
|
||||
'pc' => Rational(1, 6),
|
||||
'mm' => Rational(1, 25.4),
|
||||
'q' => Rational(1, 101.6),
|
||||
'pt' => Rational(1, 72),
|
||||
'px' => Rational(1, 96)
|
||||
},
|
||||
'cm' => {
|
||||
'in' => Rational(2.54),
|
||||
'cm' => Rational(1),
|
||||
'pc' => Rational(2.54, 6),
|
||||
'mm' => Rational(1, 10),
|
||||
'q' => Rational(1, 40),
|
||||
'pt' => Rational(2.54, 72),
|
||||
'px' => Rational(2.54, 96)
|
||||
},
|
||||
'pc' => {
|
||||
'in' => Rational(6),
|
||||
'cm' => Rational(6, 2.54),
|
||||
'pc' => Rational(1),
|
||||
'mm' => Rational(6, 25.4),
|
||||
'q' => Rational(6, 101.6),
|
||||
'pt' => Rational(1, 12),
|
||||
'px' => Rational(1, 16)
|
||||
},
|
||||
'mm' => {
|
||||
'in' => Rational(25.4),
|
||||
'cm' => Rational(10),
|
||||
'pc' => Rational(25.4, 6),
|
||||
'mm' => Rational(1),
|
||||
'q' => Rational(1, 4),
|
||||
'pt' => Rational(25.4, 72),
|
||||
'px' => Rational(25.4, 96)
|
||||
},
|
||||
'q' => {
|
||||
'in' => Rational(101.6),
|
||||
'cm' => Rational(40),
|
||||
'pc' => Rational(101.6, 6),
|
||||
'mm' => Rational(4),
|
||||
'q' => Rational(1),
|
||||
'pt' => Rational(101.6, 72),
|
||||
'px' => Rational(101.6, 96)
|
||||
},
|
||||
'pt' => {
|
||||
'in' => Rational(72),
|
||||
'cm' => Rational(72, 2.54),
|
||||
'pc' => Rational(12),
|
||||
'mm' => Rational(72, 25.4),
|
||||
'q' => Rational(72, 101.6),
|
||||
'pt' => Rational(1),
|
||||
'px' => Rational(3, 4)
|
||||
},
|
||||
'px' => {
|
||||
'in' => Rational(96),
|
||||
'cm' => Rational(96, 2.54),
|
||||
'pc' => Rational(16),
|
||||
'mm' => Rational(96, 25.4),
|
||||
'q' => Rational(96, 101.6),
|
||||
'pt' => Rational(4, 3),
|
||||
'px' => Rational(1)
|
||||
},
|
||||
|
||||
# Rotation
|
||||
'deg' => {
|
||||
'deg' => Rational(1),
|
||||
'grad' => Rational(9, 10),
|
||||
'rad' => Rational(180, Math::PI),
|
||||
'turn' => Rational(360)
|
||||
},
|
||||
'grad' => {
|
||||
'deg' => Rational(10, 9),
|
||||
'grad' => Rational(1),
|
||||
'rad' => Rational(200, Math::PI),
|
||||
'turn' => Rational(400)
|
||||
},
|
||||
'rad' => {
|
||||
'deg' => Rational(Math::PI, 180),
|
||||
'grad' => Rational(Math::PI, 200),
|
||||
'rad' => Rational(1),
|
||||
'turn' => Rational(Math::PI * 2)
|
||||
},
|
||||
'turn' => {
|
||||
'deg' => Rational(1, 360),
|
||||
'grad' => Rational(1, 400),
|
||||
'rad' => Rational(1, Math::PI * 2),
|
||||
'turn' => Rational(1)
|
||||
},
|
||||
|
||||
# Time
|
||||
's' => {
|
||||
's' => Rational(1),
|
||||
'ms' => Rational(1, 1000)
|
||||
},
|
||||
'ms' => {
|
||||
's' => Rational(1000),
|
||||
'ms' => Rational(1)
|
||||
},
|
||||
|
||||
# Frequency
|
||||
'Hz' => {
|
||||
'Hz' => Rational(1),
|
||||
'kHz' => Rational(1000)
|
||||
},
|
||||
'kHz' => {
|
||||
'Hz' => Rational(1, 1000),
|
||||
'kHz' => Rational(1)
|
||||
},
|
||||
|
||||
# Pixel density
|
||||
'dpi' => {
|
||||
'dpi' => Rational(1),
|
||||
'dpcm' => Rational(2.54),
|
||||
'dppx' => Rational(96)
|
||||
},
|
||||
'dpcm' => {
|
||||
'dpi' => Rational(1, 2.54),
|
||||
'dpcm' => Rational(1),
|
||||
'dppx' => Rational(96, 2.54)
|
||||
},
|
||||
'dppx' => {
|
||||
'dpi' => Rational(1, 96),
|
||||
'dpcm' => Rational(2.54, 96),
|
||||
'dppx' => Rational(1)
|
||||
}
|
||||
}.freeze
|
||||
|
||||
UNITS_BY_TYPE = {
|
||||
time: %w[s ms],
|
||||
frequency: %w[Hz kHz],
|
||||
'pixel density': %w[dpi dpcm dppx]
|
||||
}.freeze
|
||||
|
||||
TYPES_BY_UNIT = UNITS_BY_TYPE.each_with_object({}) do |(key, values), hash|
|
||||
values.each do |value|
|
||||
hash[value] = key
|
||||
end
|
||||
end
|
||||
|
||||
module_function
|
||||
|
||||
def conversion_factor(unit1, unit2)
|
||||
return 1 if unit1 == unit2
|
||||
|
||||
CONVERSIONS.dig(unit1, unit2)
|
||||
end
|
||||
|
||||
def canonicalize_units(units)
|
||||
return units if units.empty?
|
||||
|
||||
if units.length == 1
|
||||
type = TYPES_BY_UNIT[units.first]
|
||||
return type.nil? ? units : [UNITS_BY_TYPE[type].first]
|
||||
end
|
||||
|
||||
units.map do |unit|
|
||||
type = TYPES_BY_UNIT[unit]
|
||||
type.nil? ? units : [UNITS_BY_TYPE[type].first]
|
||||
end.sort
|
||||
end
|
||||
|
||||
def canonical_multiplier(units)
|
||||
units.reduce(1) do |multiplier, unit|
|
||||
multiplier * canonical_multiplier_for_unit(unit)
|
||||
end
|
||||
end
|
||||
|
||||
def canonical_multiplier_for_unit(unit)
|
||||
inner_map = CONVERSIONS[unit]
|
||||
inner_map.nil? ? 1 : 1 / inner_map.values.first
|
||||
end
|
||||
end
|
||||
|
||||
private_constant :Unit
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,61 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Sass
|
||||
module Value
|
||||
# Sass's string type.
|
||||
#
|
||||
# @see https://sass-lang.com/documentation/js-api/classes/sassstring/
|
||||
class String
|
||||
include Value
|
||||
include CalculationValue
|
||||
|
||||
# @param text [::String]
|
||||
# @param quoted [::Boolean]
|
||||
def initialize(text = '', quoted: true)
|
||||
@text = text.freeze
|
||||
@quoted = quoted
|
||||
end
|
||||
|
||||
# @return [::String]
|
||||
attr_reader :text
|
||||
|
||||
# @return [::Boolean]
|
||||
def quoted?
|
||||
@quoted
|
||||
end
|
||||
|
||||
# @return [::Boolean]
|
||||
def ==(other)
|
||||
other.is_a?(Sass::Value::String) && other.text == text
|
||||
end
|
||||
|
||||
# @return [Integer]
|
||||
def hash
|
||||
@hash ||= text.hash
|
||||
end
|
||||
|
||||
# @return [String]
|
||||
def assert_string(_name = nil)
|
||||
self
|
||||
end
|
||||
|
||||
# @param sass_index [Number]
|
||||
# @return [Integer]
|
||||
def sass_index_to_string_index(sass_index, name = nil)
|
||||
index = sass_index.assert_number(name).assert_integer(name)
|
||||
raise Sass::ScriptError.new('String index may not be 0', name) if index.zero?
|
||||
|
||||
if index.abs > text.length
|
||||
raise Sass::ScriptError.new("Invalid index #{sass_index} for a string with #{text.length} characters", name)
|
||||
end
|
||||
|
||||
index.negative? ? text.length + index : index - 1
|
||||
end
|
||||
|
||||
# @return [::String]
|
||||
def to_s
|
||||
@quoted ? Serializer.serialize_quoted_string(@text) : Serializer.serialize_unquoted_string(@text)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user