############################################################################
#
#	File:     color.icn
#
#	Subject:  Procedures dealing with colors
#
#	Author:   Gregg M. Townsend
#
#	Date:     April 1, 1997
#
############################################################################
#
#   This file is in the public domain.
#
############################################################################
#
#	These procedures deal with colors in various ways.
#
#	ScaleGamma(v, g)	scales a number with gamma correction.
#
#	Blend(k1, k2, ...)	generates a sequence of colors.
#
#	Contrast(win, k)	returns "white" or "black" contrasting with k.
#
#	Shade(win, k)		sets Fg(), with dithering on a bilevel screen.
#
#	RandomColor(W, p)	returns a randomly chosen color from a palette.
#
#	PaletteGrays(W, p)	returns the gray entries of a palette.
#
#	RGBKey(W, p, r, g, b)	returns the palette key closest to (r,g,b).
#
#	HSVKey(W, p, h, s, v)	returns the palette key closest to (h/s/v).
#
#	HSV(k)			returns the h/s/v interpretation of a color.
#
#	HSVValue(hsv)		returns the ColorValue() of an h/s/v string.
#
#	HLS(k)			returns the h:l:s interpretation of a color.
#
#	HLSValue(hls)		returns the ColorValue() of an h:l:s string.
#
############################################################################
#
#  ScaleGamma(v, g) nonlinearly scales the number v (between 0.0 and 1.0)
#  to an integer between 0 and 65535 using a gamma correction factor g.
#  the default value of g is 2.5.
#
#  Blend(color1, color2, color3,...) generates ColorValue(color1), then
#  some intermediate shades, then ColorValue(color2), then some more
#  intermediate shades, and so on, finally generating the color value of
#  the last argument.  An integer argument can be interpolated at any
#  point to set the number of steps (the default is four) from one color
#  to the next.
#
#  Contrast(win, colr) returns either "white" or "black", depending
#  on which provides the greater contrast with the specified color.
#
#  Shade(win, colr) sets the foreground for an area filling operation.
#  On a color screen, Shade() sets the foreground color and returns the
#  window.  On a bilevel monochrome screen, Shade() sets the foreground
#  to a magic-square dithering pattern approximating the luminance of the
#  color specified.  If the environment variable XSHADE is set to "gray"
#  (or "grey") then Shade simulates a multilevel grayscale monitor.
#  If it is set to any other value, Shade simulates a bilevel monitor.
#
#  RandomColor(win, palette) returns a randomly chosen color from the
#  given image palette, excluding the "extra" grays of the palette, if
#  any.  (Colors are selected from a small finite palette, rather than
#  from the entire color space, to avoid running out of colors if a
#  large number of random choices are desired.)  The default palette
#  for this procedure is "c6".
#
#  PaletteGrays([win,] palette) is like PaletteChars but it returns only
#  the characters corresponding to shades of gray.  The characters are
#  ordered from black to white, and in all palettes the shades of gray
#  are equally spaced.
#
#  RGBKey([win,] palette, r, g, b) returns a palette key given the
#  three color components as real number from 0.0 to 1.0.
#  HSVKey([win,] palette, h, s, v) returns a palette key given a
#  hue, saturation, and value as real numbers from 0.0 to 1.0.
#
#  HSV() and HSVValue() convert between Icon color strings and strings
#  containing slash-separated HSV values with maxima of "360/100/100".
#  HSV(k) returns the h/s/v interpretation of an Icon color specification;
#  HSVValue(hsv) translates an h/s/v value into an Icon r,g,b value.
#
#  HLS() and HLSValue() convert between Icon color strings and strings
#  containing colon-separated HLS values with maxima of "360:100:100".
#  HLS(k) returns the h:l:s interpretation of an Icon color specification;
#  HLSValue(hls) translates an h:l:s value into an Icon r,g,b value.
#
############################################################################
#
#  Requires:  Version 9 graphics
#
############################################################################


#   ScaleGamma(v, g) -- scale fraction to int with gamma correction.

procedure ScaleGamma(v, g)		#: scale with gamma correction
   /g := 2.5
   return integer(65535 * v ^ (1.0 / g))
end


#  Blend(color1, color2, ...) -- generate sequence of colors

procedure Blend(args[])			#: generate sequence of colors
   local win, n, s, a, i, f1, f2, r1, g1, b1, r2, g2, b2, r3, g3, b3
   static type

   initial type := proc("type", 0)	# protect attractive name

   n := 4
   if type(args[1]) == "window" then
      win := get(args)
   else
      win := &window

   while a := get(args) do
      if integer(a) >= 0 then
         n := integer(a)
      else {
         s := ColorValue(win, a) | fail
         s ? {
            r2 := tab(many(&digits));  move(1)
            g2 := tab(many(&digits));  move(1)
            b2 := tab(many(&digits))
            }
         if /r1 then
            suspend s
         else
            every i := 1 to n do {
               f2 := real(i) / real(n)
               f1 := 1.0 - f2
               r3 := integer(f1 * r1 + f2 * r2)
               g3 := integer(f1 * g1 + f2 * g2)
               b3 := integer(f1 * b1 + f2 * b2)
               suspend r3 || "," || g3 || "," || b3
               }
         r1 := r2
         g1 := g2
         b1 := b2
         }
end


#  Contrast(win, color) -- return "white" or "black" to maximize contrast

procedure Contrast(win, color)		#: choose contrasting color
   static l, type
   initial {
      l := ["white", "black"]
      type := proc("type", 0)	# protect attractive name
      }
       
   if type(win) == "window" then
      return l[1 + PaletteKey(win, "g2", color)]
   else
      return l[1 + PaletteKey("g2", win)]
end


#  Shade(win, color) -- approximate a shade with a pattern if bilevel screen

procedure Shade(win, color)		#: dither shade using pattern
   local r, g, b
   static dmat, env, type

   initial {
      env := ("" ~== map(getenv("XSHADE")))
      type := proc("type", 0)	# protect attractive name
      }

   if type(win) ~== "window" then {
      color := win
      win := &window
      }
   if WAttrib(win, "depth") ~== "1" & /env then {
      Fg(win, color) | fail
      return win
      }
   (ColorValue(win, color) | fail) ? {
      r := tab(many(&digits));  move(1)
      g := tab(many(&digits));  move(1)
      b := tab(many(&digits))
      }
   g := integer(0.30 * r + 0.59 * g + 0.11 * b)

   if \env == ("gray" | "grey") then {
      Fg(win, g || "," || g || "," || g)
      return win
      }

   /dmat := [
      "4,15,15,15,15",
      "4,15,15,13,15",
      "4,11,15,13,15",
      "4,10,15,13,15",
      "4,10,15,5,15",
      "4,10,7,5,15",
      "4,10,7,5,14",
      "4,10,7,5,10",
      "4,10,5,5,10",
      "4,10,5,5,2",
      "4,10,4,5,2",
      "4,10,0,5,2",
      "4,10,0,5,0",
      "4,8,0,5,0",
      "4,8,0,1,0",
      "4,8,0,0,0",
      "4,0,0,0,0",
      ]
   WAttrib(win, "fillstyle=textured")
   g := g / 3856 + 1
   Pattern(win, dmat[g])
   return win
end


#   RandomColor(win, palette) -- choose random color

procedure RandomColor(win, palette)	#: choose random color
   local s, n
   static type

   initial type := proc("type", 0)	# protect attractive name

   if type(win) ~== "window" then
      palette:= win			# window allowed but ignored
   /palette := "c6"

   s := PaletteChars(palette)
   palette ?
      if ="c" & any('23456') then {
         n := integer(move(1))
         s := s[1 +: n * n * n]
         }
   return PaletteColor(palette, ?s)

end


#  PaletteGrays(win, palette) -- return grayscale entries from palette.

procedure PaletteGrays(win, palette)	#: grayscale entries from palette
   static type

   initial type := proc("type", 0)	# protect attractive name
   if (type(win) ~== "window") then
      palette := win				# window not needed

   palette := string(palette) | runerr(103, palette)

   if palette ? ="g" then
      return PaletteChars(palette)

   return case palette of {
      "c1":	"0123456"
      "c2":	"kxw"
      "c3":	"@abMcdZ"
      "c4":	"0$%&L*+-g/?@}"
      "c5":	"\0}~\177\200\37\201\202\203\204>\205\206\207\210]_
                  \211\212\213\214|"
      "c6":	"\0\330\331\332\333\334+\335\336\337\340\341V\342\343\344\345_
                  \346\201\347\350\351\352\353\254\354\355\356\357\360\327"
      default:	fail
      }
end


#  RGBKey(win, palette, r, g, b) -- find key given real-valued color

procedure RGBKey(win, palette, r, g, b)	#: return palette key for color
   static type

   initial type := proc("type", 0)	# protect attractive name
   if type(win) ~== "window" then	# allow unused window argument
      win :=: palette :=: r :=: g :=: b
   r := integer(r * 65535.99)
   g := integer(g * 65535.99)
   b := integer(b * 65535.99)
   return PaletteKey(palette, r || "," || g || "," || b)
end


#  HSVKey(win, palette, h, s, v) -- find nearest key from h,s,v in [0.0,1.0]
#
#  HSV conversion based on Foley et al, 2/e, p.593

procedure HSVKey(win, palette, h, s, v)	#: nearest key from HSV specification
   local i, f, p, q, t, r, g, b
   static type

   initial type := proc("type", 0)	# protect attractive name
   if type(win) ~== "window" then	# allow unused window argument
      win :=: palette :=: h :=: s :=: v

   if s = 0.0 then			# achromatic case
      return RGBKey(palette, v, v, v)

   h *:= 6.0				# hue [0.0 - 6.0)
   if h >= 6.0 then
      h := 0.0

   i := integer(h)
   f := h - i
   p := v * (1.0 - s)
   q := v * (1.0 - f * s)
   t := v * (1.0 - (1.0 - f) * s)

   case i of {
      0: { r := v;  g := t;  b := p }	# red - yellow
      1: { r := q;  g := v;  b := p }	# yellow - green
      2: { r := p;  g := v;  b := t }	# green - cyan
      3: { r := p;  g := q;  b := v }	# cyan - blue
      4: { r := t;  g := p;  b := v }	# blue - magenta
      5: { r := v;  g := p;  b := q }	# magenta - red
      }

   return RGBKey(palette, r, g, b)
end


#  HSV(k) -- return h/s/v interpretation of color spec.
#
#  h is hue (0 <= h < 360)
#  s is saturation (0 <= s <= 100)
#  v is value (0 <= v <= 100)
#
#  based on Foley et al, 2/e, p.592

procedure HSV(k)			#: HSV interpretation of color
   local r, g, b, h, s, v, min, max, d

   (ColorValue(k) | fail) ? {
      r := tab(many(&digits)) / 65535.0
      move(1)
      g := tab(many(&digits)) / 65535.0
      move(1)
      b := tab(many(&digits)) / 65535.0
      }

   min := r;  min >:= g;  min >:= b		# minimum
   max := r;  max <:= g;  max <:= b		# maximum
   d := max - min				# difference

   v := max					# value is max of all values
   if max > 0 then
      s := d / max				# saturation is (max-min)/max
   else
      s := 0.0

   if s = 0 then
      h := 0.0					# use hue 0 if unsaturated
   else if g = max then
      h := 2 + (b - r) / d			# yellow through cyan
   else if b = max then
      h := 4 + (r - g) / d			# cyan through magenta
   else if g < b then
      h := 6 + (g - b) / d			# magenta through red
   else
      h := (g - b) / d				# red through yellow

   return integer(60 * h + 0.5) || "/" ||
      integer(100 * s + 0.5) || "/" || integer(100 * v + 0.5)
end


#  HSVValue(hsv) -- return ColorValue of h/s/v string
#
#  h is hue (0 <= h <= 360)
#  s is saturation (0 <= s <= 100)
#  v is value (0 <= v <= 100)
#
#  based on Foley et al, 2/e, p.593

procedure HSVValue(hsv)			#: color value of HSV specification
   local h, s, v, r, g, b, i, f, p, q, t

   hsv ? {
      h := tab(many(&digits)) / 360.0 | fail
      ="/" | fail
      s := tab(many(&digits)) / 100.0 | fail
      ="/" | fail
      v := tab(many(&digits)) / 100.0 | fail
      pos(0) | fail
      }
   if (h | s | v) > 1 then fail

   if s = 0.0 then {			# achromatic case
      v := integer(65535 * v + 0.499999)
      return v || "," || v || "," || v
      }

   h *:= 6.0				# hue [0.0 - 6.0)
   if h >= 6.0 then
      h := 0.0

   i := integer(h)
   f := h - i
   p := v * (1.0 - s)
   q := v * (1.0 - f * s)
   t := v * (1.0 - (1.0 - f) * s)

   case i of {
      0: { r := v;  g := t;  b := p }	# red - yellow
      1: { r := q;  g := v;  b := p }	# yellow - green
      2: { r := p;  g := v;  b := t }	# green - cyan
      3: { r := p;  g := q;  b := v }	# cyan - blue
      4: { r := t;  g := p;  b := v }	# blue - magenta
      5: { r := v;  g := p;  b := q }	# magenta - red
      }

   return integer(65535 * r + 0.499999) || "," ||
      integer(65535 * g + 0.499999) || "," || integer(65535 * b + 0.499999)
end


#  HLS(k) -- return h:l:s interpretation of color spec.
#
#  h is hue (0 <= h < 360)
#  l is lightness (0 <= l <= 100)
#  s is saturation (0 <= s <= 100)
#
#  based on Foley et al, 2/e, p.595

procedure HLS(k)			#: HLS interpretation of color
   local r, g, b, h, l, s, min, max, delta

   (ColorValue(k) | fail) ? {
      r := tab(many(&digits)) / 65535.0
      move(1)
      g := tab(many(&digits)) / 65535.0
      move(1)
      b := tab(many(&digits)) / 65535.0
      }

   min := r;  min >:= g;  min >:= b	# minimum
   max := r;  max <:= g;  max <:= b	# maximum
   delta := max - min			# difference

   l := (max + min) / 2			# lightness

   if max = min then 
      h := s := 0			# achromatic

   else {

      if l <= 0.5 then
         s := delta / (max + min)	# saturation
      else
         s := delta / (2 - max - min)

      if r = max then
         h := (g - b) / delta		# yellow through magenta
      else if g = max then
         h := 2 + (b - r) / delta	# cyan through yellow
      else # b = max
         h := 4 + (r - g) / delta	# magenta through cyan
      if h < 0 then
         h +:= 6			# ensure positive value
      }

   return integer(60 * h + 0.5) || ":" ||
      integer(100 * l + 0.5) || ":" || integer(100 * s + 0.5)
end


#  HLSValue(hls) -- return ColorValue of h:l:s string
#
#  h is hue (0 <= h <= 360)
#  l is lightness (0 <= l <= 100)
#  s is saturation (0 <= s <= 100)
#
#  based on Foley & Van Dam, 1/e, p.619

procedure HLSValue(hls)			#: color value of HLS specification
   local h, l, s, r, g, b, m1, m2

   hls ? {
      h := tab(many(&digits)) / 360.0 | fail
      =":" | fail
      l := tab(many(&digits)) / 100.0 | fail
      =":" | fail
      s := tab(many(&digits)) / 100.0 | fail
      pos(0) | fail
      }
   if (h | l | s) > 1 then fail

   if l <= 0.5 then
      m2 := l * (1 + s)
   else
      m2 := l + s - (l * s)
   m1 := 2 * l - m2

   if s = 0.0 then
      r := g := b := l			# achromatic
   else {
      r := hls_rgb_val(m1, m2, h + 0.3333333)
      g := hls_rgb_val(m1, m2, h)
      b := hls_rgb_val(m1, m2, h - 0.3333333)
      }

   return integer(65535 * r + 0.499999) || "," ||
      integer(65535 * g + 0.499999) || "," || integer(65535 * b + 0.499999)
end

procedure hls_rgb_val(n1, n2, hue)	# helper function for HLSValue
   hue *:= 6
   if hue >= 6 then
      hue -:= 6
   else if hue < 0 then
      hue +:= 6
   if (hue < 1) then
      return n1 + (n2 - n1) * hue
   else if (hue < 3) then
      return n2
   else if (hue < 4) then
      return n1 + (n2 - n1) * (4 - hue)
   else
      return n1
end