# Color functionalities file. See # - +Color+ # - +Palette+ # - +Gradient+ require 'geometry2D'; # for vector extension require 'interpolation' require 'attributable' require 'utils' require 'shape'; # for gradient # # Color class # # = Basics # Class Color derives from Vector, and consists in a 4D vector of (0.0..1.0) values, for red, blue, green, and opacity # = Utilities # Conversion from hsv and hsl color spaces available (see this link[http://en.wikipedia.org/wiki/HSV_color_space]) # = Future # - Must use this library[https://rubyforge.org/projects/color/], to avoid effort duplication # - Must add relative color operations as Nodebox wants to # - Must optimize 4D vector operations (as C extension ?) class Color < Vector # Color builder # # only allows to build 4D vector, with composants between 0.0 and 1.0 def initialize( *args ) # TODO : check args number super( *args ) end # return the red composant # Color[0.1,0.2,0.3,0.4].r => 0.1 def r return self[0] end # return the green composant # Color[0.1,0.2,0.3,0.4].r => 0.2 def g return self[1] end # return the blue composant # Color[0.1,0.2,0.3,0.4].r => 0.3 def b return self[2] end # return the opacity composant # Color[0.1,0.2,0.3,0.4].r => 0.4 def a return self[3] end # set the red composant # Color[0.1,0.2,0.3,0.4].r = 0.5 => Color[0.5,0.2,0.3,0.4] def r=(n) self[0]= n end # set the green composant # Color[0.1,0.2,0.3,0.4].g = 0.5 => Color[0.1,0.5,0.3,0.4] def g=(n) self[1] = n end # set the blue composant # Color[0.1,0.2,0.3,0.4].b = 0.5 => Color[0.1,0.2,0.5,0.4] def b=(n) self[2] = n end # set the opacity composant # Color[0.1,0.2,0.3,0.4].a = 0.5 => Color[0.1,0.2,0.3,0.5] def a=(n) self[3] = n end # return an array containing colors on 255 integer format # Color[0.0,1.0,0.0,1.0].format255 => [0,255,0,255] def format255() return self.map {|v| (v * 255.0).to_i} end # return a random color vector, with 1.0 opacity !! # Color.rand => Color[0.2345, 0.987623, 0.4123, 1.0] def Color.rand return Color[Kernel::rand,Kernel::rand,Kernel::rand,1.0] end # return a black color vector def Color.black(opacity=1.0) return Color[0.0, 0.0, 0.0, opacity] end # return a blue color vector def Color.blue(opacity=1.0) return Color[0.0, 0.0, 1.0, opacity] end # return a red color vector def Color.red(opacity=1.0) return Color[1.0, 0.0, 0.0, opacity] end # return a yellow color vector def Color.yellow(opacity=1.0) return Color[1.0, 1.0, 0.0, opacity] end # return a orange color vector def Color.orange(opacity=1.0) return Color[1.0, 0.5, 0.0, opacity] end # return a green color vector def Color.green(opacity=1.0) return Color[0.0, 1.0, 0.0, opacity] end # return a white color vector def Color.white(opacity=1.0) return Color[1.0, 1.0, 1.0, opacity] end # build a color vector from hsv parametrization (convert from hsv to rgb) h, s, v being between 0.0 and 1.0 # taken from wikipedia[http://en.wikipedia.org/wiki/HSV_color_space] def Color.hsv( h, s, v, a) if s == 0.0 return Color[v, v, v, a] end h *= 360.0 hi = (h/60.0).floor f = (h/60.0) - hi p = v * ( 1 - s ) q = v * ( 1 - f * s ) t = v * ( 1 - ( 1 - f ) * s ) if hi == 0 return Color[ v, t, p, a] end if hi == 1 return Color[ q, v, p, a] end if hi == 2 return Color[ p, v, t, a] end if hi == 3 return Color[ p, q, v, a] end if hi == 4 return Color[ t, p, v, a] end if hi == 5 return Color[ v, p, q, a] end end def Color.getHSLcomponent( tC, p, q ) #:nodoc: while tC < 0.0 tC = tC + 1.0 end while tC > 1.0 tC = tC - 1.0 end if tC < (1.0 / 6.0) tC = p + ( (q-p) * 6.0 * tC ) elsif tC >=(1.0 / 6.0) and tC < 0.5 tC = q elsif tC >= 0.5 and tC < (2.0 / 3.0) tC = p + ( (q-p) * 6.0 * ((2.0 / 3.0) - tC) ) else tC = p end return tC end # build a color vector from hsl parametrization (convert from hsl to rgb) h, s, l being between 0.0 and 1.0 # taken from [[http://en.wikipedia.org/wiki/HSV_color_space]] # h, s, l must be between 0.0 and 1.0 def Color.hsl( h, s, l, a) h *= 360.0 if l < 0.5 q = l * (1.0 + s) else q = l+ s - (l * s) end p = 2 * l - q hk = h / 360.0 tR = hk + 1.0 / 3.0 tG = hk tB = hk - 1.0 / 3.0 tR = self.getHSLcomponent( tR, p, q ) tG = self.getHSLcomponent( tG, p, q ) tB = self.getHSLcomponent( tB, p, q ) return Color[tR, tG, tB, a] end # get svg description of a color def svg values = self[0..2].map {|v| (255.0 * v).to_i } return "rgb(#{values.join(",")})" end end # class Palette # = Intro # Palette defines color palettes, as interpolation between color points. As such, use Interpolation module, so uses for the moment only linear interpolation. # But once built with interpolation, palette provides a continuous color "interval", and so is Samplable ! # = Use # palette = Palette[ :colorlist, [ Color.blue, 0.0, Color.orange, 0.5, Color.yellow, 1.0 ] ] # palette.rand( 10 ) # => return 10 random colors in palette # palette.color( 0.5 ) # => Color.orange class Palette include Attributable attribute :colorlist # compute color given float pourcentage. # Palette[ :colorlist, [ Color.black, 0.0, Color.white, 1.0 ] ].sample( 0.5 ) => Color[0.5,0.5,0.5,1.O] # "sample" method as defined in Samplable module def color(dindex) result = self.interpolate(dindex) return Color.elements(result[0..-1],false) end # return a new palette by reversing the current one def reverse() newcolorlist = [] self.colorlist.reverse.foreach do |index,color| newcolorlist += [color, (0.0..1.0).complement( index )] end return Palette[ :colorlist, newcolorlist ] end include Samplable include Interpolation def apply_sample( abs ) #:nodoc: # Trace("Palette#apply_sample abs #{abs}") return self.color( abs ) end # alias apply_sample color # alias apply_split ? => TODO alias samplelist colorlist alias colors samples end class Gradient < Palette #:nodoc: def defsvg() Kernel::raise("Gradient::defsvg must be redefined in subclasses") end end class LinearGradient < Gradient #:nodoc: def svgdef template = '%stops%' stoptemplate = '' stops = "\n" self.colorlist.foreach do |color, index| stops += stoptemplate.subreplace( {"%offset%" => index, "%color%" => color.svg, "%opacity%" => color.a} ) stops += "\n" end return template.subreplace( {"%stops%" => stops} ) end end class CircularGradient < Gradient #:nodoc: attribute :circle, nil, Circle def svgdef template = '%stops%' stoptemplate = '' stops = "\n" self.colorlist.foreach do |color, index| stops += stoptemplate.subreplace( {"%offset%" => index, "%color%" => color.svg, "%opacity%" => color.a} ) stops += "\n" end return template.subreplace( {"%stops%" => stops, "%cx%" => circle.center.x, "%cy%" => circle.center.y, "%r%" => circle.radius} ) end end