lib/sass/value/color/space/utils.rb



# 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