danini-the-panini/mittsu

View on GitHub
lib/mittsu/math/color.rb

Summary

Maintainability
A
3 hrs
Test Coverage
A
90%
require 'mittsu/math/vector3'

module Mittsu
  class Color < Vector3
    ELEMENTS = { r: 0, g: 1, b: 2 }
    DIMENSIONS = ELEMENTS.count

    def initialize(*args)
      super(0, 0, 0)
      case args.length
      when 3 then self.set_rgb(*args)
      when 1 then self.set(args.first)
      when 0 then self.set_rgb(1.0, 1.0, 1.0)
      else raise ArgumentError, "Arguments must be (r, g, b), (color), or none"
      end
    end

    alias :r :x
    alias :g :y
    alias :b :z

    alias :r= :x=
    alias :g= :y=
    alias :b= :z=

    def set(value)
      case value
      when Color
        self.copy(value)
      when Integer
        self.set_hex(value)
      when String
        self.set_style(value)
      else
        raise ArgumentError, "Arguments must be Color, Integer or String"
      end
      self
    end

    def set_hex(hex)
      hex = hex.floor
      self.r = (hex >> 16 & 255) / 255.0
      self.g = (hex >> 8 & 255) / 255.0
      self.b = (hex & 255) / 255.0
      self
    end

    def set_rgb(r, g, b)
      @elements = [r.to_f, g.to_f, b.to_f]
      self
    end

    def set_hsl(h, s, l)
      # h,s,l ranges are in 0.0 - 1.0
      if s.zero?
        self.r = self.g = self.b = l
      else
        p = l <= 0.5 ? l * (1.0 + s) : l + s - (l * s)
        q = (2.0 * l) - p
        self.r = hue2rgb(q, p, h + 1.0 / 3.0)
        self.g = hue2rgb(q, p, h)
        self.b = hue2rgb(q, p, h - 1.0 / 3.0)
      end
      self
    end

    def set_style(style)
      # rgb(255,0,0)
      if /^rgb\((\d+), ?(\d+), ?(\d+)\)$/i =~ style
        self.r = [255.0, $1.to_f].min / 255.0
        self.g = [255.0, $2.to_f].min / 255.0
        self.b = [255.0, $3.to_f].min / 255.0
        return self
      end
      # rgb(100%,0%,0%)
      if /^rgb\((\d+)\%, ?(\d+)\%, ?(\d+)\%\)$/i =~ style
        self.r = [100.0, $1.to_f].min / 100.0
        self.g = [100.0, $2.to_f].min / 100.0
        self.b = [100.0, $3.to_f].min / 100.0
        return self
      end
      # #ff0000
      if /^\#([0-9a-f]{6})$/i =~ style
        self.set_hex($1.hex)
        return self
      end
      # #f00
      if /^\#([0-9a-f])([0-9a-f])([0-9a-f])$/i =~ style
        self.set_hex(($1 + $1 + $2 + $2 + $3 + $3).hex)
        return self
      end
      # red
      if /^(\w+)$/i =~ style
        self.set_hex(Mittsu::ColorKeywords[style])
        return self
      end
    end

    def copy_gamma_to_linear(color, gamma_factor = 2.0)
      self.r = color.r ** gamma_factor
      self.g = color.g ** gamma_factor
      self.b = color.b ** gamma_factor
      self
    end

    def copy_linear_to_gamma(color, gamma_factor = 2.0)
      safe_inverse = (gamma_factor > 0) ? (1.0 / gamma_factor) : 1.0
      self.r = color.r ** safe_inverse
      self.g = color.g ** safe_inverse
      self.b = color.b ** safe_inverse
      self
    end

    def convert_gamma_to_linear
      rr, gg, bb = self.r, self.g, self.b
      self.r = rr * rr
      self.g = gg * gg
      self.b = bb * bb
      self
    end

    def convert_linear_to_gamma
      self.r = ::Math.sqrt(self.r)
      self.g = ::Math.sqrt(self.g)
      self.b = ::Math.sqrt(self.b)
      self
    end

    def hex
      (self.r * 255).to_i << 16 ^ (self.g * 255).to_i << 8 ^ (self.b * 255).to_i << 0
    end

    def hex_string
      ('000000' + self.hex.to_s(16))[-6..-1]
    end

    def hsl(target = nil)
      # h,s,l ranges are in 0.0 - 1.0
      hsl = target || { h: 0.0, s: 0.0, l: 0.0 }
      rr, gg, bb = self.r, self.g, self.b
      max = [r, g, b].max
      min = [r, g, b].min
      hue, saturation = nil, nil
      lightness = (min + max) / 2.0
      if min == max
        hue = 0.0
        saturation = 0.0
      else
        delta = max - min
        saturation = lightness <= 0.5 ? delta / (max + min) : delta / (2.0 - max - min)
        case max
        when rr then hue = (gg - bb) / delta + (gg < bb ? 6.0 : 0.0)
        when gg then hue = (bb - rr) / delta + 2.0
        when bb then hue = (rr - gg) / delta + 4.0
        end
        hue /= 6.0
      end
      hsl[:h] = hue
      hsl[:s] = saturation
      hsl[:l] = lightness
      hsl
    end

    def style
      "rgb(#{ (self.r * 255).to_i },#{ (self.g * 255).to_i },#{ (self.b * 255).to_i })"
    end

    def offset_hsl(h, s, l)
      hsl = self.hsl
      hsl[:h] += h
      hsl[:s] += s
      hsl[:l] += l
      self.set_hsl(hsl[:h], hsl[:s], hsl[:l])
      self
    end

    alias :add_colors :add_vectors

    private

    def hue2rgb(p, q, t)
      t += 1.0 if t < 0.0
      t -= 1.0 if t > 1.0
      return p + (q - p) * 6.0 * t if t < 1.0 / 6.0
      return q if t < 1.0 / 2.0
      return p + (q - p) * 6.0 * (2.0 / 3.0 - t) if t < 2.0 / 3.0
      p
    end

  end
end