############################################################################
#
#       File:     glib.icn
#
#	Subject:  Procedures for graphics
#
#	Author:   Stephen B. Wampler
#
#	Date:     May 2, 2001
#
############################################################################
#
#   This file is in the public domain.
#
############################################################################
#
#	Version:  1.0
#
############################################################################
#
#
#   Comments: This package is the collection of routines
#      developed to facilitate traditional 2D graphics.
#      It is incomplete, but still provides
#      a reasonable amount of support.  There is some
#      support for 3D graphics here, but that is not so
#      well developed.  People are encouraged to improve
#      these routines and add new routines.
#
#      All routines use list-based subscripting.  This allows
#      programs to describe points as lists OR records.
#
#      In the turtle graphics code, the use gives angles in
#      degrees.
#
############################################################################
#
#	Requires:  Version 9 graphics, co-expressions
#
############################################################################

record point(x,y)

############################################################################
# Clipping algorithms...
#
global DO_CLIPPING


#  Set the state of clipping: "on" or "off"
#
procedure set_clip(state)
   if map(state) == "on" then
      DO_CLIPPING := "yes"
   else
      DO_CLIPPING := &null
end

# Either clip a line or leave it alone
#
procedure Clip_Line(line,box)
   if \DO_CLIPPING then
      return LB_line_clip(line, box)
   return line
end

# Note: Liang-Barsky algorithms (or variants) are used.  If you
#   have fast FP hardware, they are faster than Cohen-Sutherland
#   (and *much* slower if you *don't*!).  Anyway, they're more
#   fun to code and easier to extend to 3-D.

#
# LB_line_clip -- takes a 2-D line (two points) and returns it clipped to
#    a box (normally the viewport).
procedure LB_line_clip(line, box)
   local nline, u, dx, dy

			# initialize important parametric values
   dx := line[2][1] - line[1][1]
   dy := line[2][2] - line[2][2]
   u := [0.0, 1.0]

			# do the clipping
   if clipcheck(-dx, line[1][1] - box[1][1], u) &
      clipcheck( dx, box[2][1] - line[1][1], u) &
      clipcheck(-dy, line[1][2] - box[1][2], u) &
      clipcheck( dy, box[2][2] - line[1][1], u) then {
			# return a modified copy of original line
      nline := copy(line)
      nline[1] := copy(line[1])
      nline[2] := copy(line[2])

      if u[2] < 1.0 then {
         nline[2][1] := line[1][1] + (u[2]*dx)
         nline[2][2] := line[1][2] + (u[2]*dy)
         }
      if u[1] < 1.0 then {
         nline[1][1] := line[1][1] + (u[1]*dx)
         nline[1][2] := line[1][2] + (u[1]*dy)
         }
      return nline
      }
			# no need to clip
   fail
end
   
procedure clipcheck(p,q,u)
   local r

   if p < 0.0 then {
      r := real(q)/p
      if r > u[2] then fail
      else if r > u[1] then u[1] := r
      }
   else if p > 0.0 then {
      r := real(q)/p
      if r > u[1] then fail
      else if r > u[2] then u[2] := r
      }
   else if q >= 0.0 then return

end

#
# Clip a line to a convex polygon (2-D)
# 
procedure Convex_clip(poly, line[])
	# Cyrus-Beck line clipping against a convex polygon
	#   (assumes poly is a convex polygon!)
   local D, nc, E, cline
   local n, p		# point normal of polygon edge
   local c, p1		# point slope of line
   local t_in, t_out	# current endpoints
   local t, i

   c := make_vector(line[1],line[2])
   p1 := line[1]
   t_in := 0
   t_out := 1

   every i := 2 to *poly+1 do {	# for each edge
      p := poly[i-1]
      if i > *poly then
         n := normal_line(poly[i-1],poly[1])
      else
         n := normal_line(poly[i-1],poly[i])
      D := dot(n,p)
      
      if (nc := dot(n,c)) = 0 then {	# parallel to edge
         if not inside_line(p1,p,n) then {fail}
                                    else next
        
         } 

      t := (D - dot(n,p1))/nc

      if nc > 0 then 			# entering polygon
         t_in <:= t
      else				# exiting polygon
         t_out >:= t

      if t_in >= t_out then {fail}
      }

   # if we get here, part of the line is visible, return that part

   cline := copy(line)
   cline[1] := vpara(line[1],line[2],t_in)
   cline[2] := vpara(line[1],line[2],t_out)

   return cline
end



#   - some interesting curves
###

############################################################################
# Draw a fractal snowflake or order N between two points
############################################################################
#
# Draw a fractal snowflake between two points
#
procedure fract_flake(win,A,C,n,lr,cp)
   local direction, t

   /lr := 1
   direction := Rel_angle(A,C)
   t := turtle(win, A, direction)
   f_flake(t, distance(A,C), n, lr, cp)
   return
end

procedure f_flake(t, len, n, lr, cp)
   local angle, p, nextcolor

   if n > 0 then {
      # if nextcolor is available, change the foreground color
      Fg ! ([t.win.vp.screen] ||| @\nextcolor)
      Left(t,lr*60)
      f_flake(t, len*0.333333, n-1, -lr, cp)
      f_flake(t, len*0.333333, n-1,  lr, cp)
      Right(t,lr*60)
      f_flake(t, len*0.333333, n-1,  lr, cp)
      Right(t,lr*60)
      f_flake(t, len*0.333333, n-1,  lr, cp)
      Right(t,lr*150)
      f_flake(t, len*0.19244, n-1,  lr, cp)
      f_flake(t, len*0.192498, n-1, -lr, cp)
      Left(t,lr*60)
      f_flake(t, len*0.192498, n-1, -lr, cp)
      Left(t,lr*60)
      f_flake(t, len*0.19244, n-1, -lr, cp)
      Left(t,lr*90)
      f_flake(t, len*0.333333, n-1,  lr, cp)
      Right(t,lr*150)
      f_flake(t, len*0.19247, n-1,  lr, cp)
      f_flake(t, len*0.19247, n-1, -lr, cp)
      Left(t,lr*150)
      f_flake(t, len*0.333333, n-1, -lr, cp)
      f_flake(t, len*0.333333, n-1,  lr, cp)
      }
   else {
      if \cp then {
         angle := dtor(t.direction)
         p := [t.pos[1]+len*cos(angle), t.pos[2]+len*sin(angle)]
         DrawConvexClipped(t.win, cp, t.pos, p)
         t.pos := p
         }
      else {
         Line_Forward(t, len)
         }
      }

   return
end
 
############################################################################
# Draw a koch curve of order N between two points
############################################################################
#
# Draw a koch curve from A to B
#
procedure koch_line(win,A,B,n)
   local t, direction

   direction := Rel_angle(A,B)
   t := turtle(win, A, direction)
   koch(t, direction, distance(A,B), n)
   return
end
#
# turtle graphics version
#
procedure koch(t, dir, len, n)

   if n > 0 then {
      koch(t, dir, len/3.0, n-1)
      Left(t,60)
      koch(t, dir, len/3.0, n-1)
      Right(t, 120)
      koch(t, dir, len/3.0, n-1)
      Left(t,60)
      koch(t, dir, len/3.0, n-1)
      }
   else
      Line_Forward(t, len)

   return
end


############################################################################
# Draw a fractal curve between two points
############################################################################
#
#
#  The parameter 'H' is a 'roughness' factor.  At H=0.5,
#     you get roughly brownian motion.
#
procedure fract_line(win,A,B,H,min_len,std_dev)
   local len_sq, direction, t, N, f, r, pt, len

   /H := 0.5
   /min_len := 0.01
   /std_dev := 0.12
   len := distance(A,B)
   direction := Rel_angle(A,B)
   t := turtle(win, A, direction)

   if len <= min_len then
      Line_Forward(t, len)
   else {
      f := exp((0.5-H)*log(2.0))
      r := gauss() * std_dev * f
      N := point()
      N.x := 0.5*(A[1] + B[1]) - r*(B[2]-A[2]);
      N.y := 0.5*(A[2] + B[2]) + r*(B[1]-A[1]);
      fract_line(win, A, N, H, min_len, std_dev)
      fract_line(win, N, B, H, min_len, std_dev)
      }

   return
end



# Simple drawing primitives
############################################################################

procedure DrwLine(w,pnts[])	# draw a polyline

   if *pnts < 2 then fail		# ... not enough points

   return DrawLine ! ([w.vp.screen]|||transform_points(pnts,w.xform_mat[1]))
end

procedure DrawConvexClipped(w,poly,pnts[])	# clip to polygon
   local i

   if (*pnts < 2) | (*poly < 3) then fail

   every i := 2 to *pnts do {
      DrwLine ! ([w]|||Convex_clip(poly,pnts[i-1],pnts[i]))
      }
      
   return
end

procedure DrawPolygon(args[])	# draw a polygon

   return DrwLine ! (args|||[args[2]])

end

procedure FillPolygon(w,pnts[]) # draw a filled polygon

   if *pnts < 2 then fail		# ... not enough points

   return FillPolygon ! ([w.vp.screen]|||
                          transform_points(pnts|||[pnts[1]],w.xform_mat[1]))
end



# Matrix operations
############################################################################

#   All matrices are stored as lists of lists, and all
#     operations determine the size of the matrix directly
#     from the matrix itself

procedure mwrite(m)		# output a matrix (usually for debugging)
   local r, c, row, col

   r := *m
   c := *m[1]

   writes("[")
   every row := 1 to r do {
      writes("[")
      every col := 1 to c do {
         writes(right(m[row][col],6),", ")
         }
      write("]")
      }
   write("]")
end

procedure newmat(n,m)		# create a matrix
   local M

   M := list(n)
   every !M := list(m)

   return M
end

procedure Imatrix(n,m)		# Identity matrix
   local M, r, c

   M := newmat(n,m)
   every r := 1 to n do {
      every c := 1 to m do {
         M[r][c] := if r = c then 1.0 else 0.0
         }
      }
   return M
end

procedure mmult(m1,m2)		# matrix multiply
   local m3, r, c, nk, k

   if (nk := *m1[1]) ~= *m2 then stop("Matrices are wrong size to multiply")

   m3 := newmat(*m1,*m2[1])
   every r := 1 to *m1 do {
      every c := 1 to *m2[1] do {
         m3[r][c] := 0.0
         every k := 1 to nk do {
             m3[r][c] +:= m1[r][k] * m2[k][c]
             }
         }
      }

   return m3
end


# low-level screen activity
############################################################################

record viewport(ul, lr, screen)
record window(ll, ur, vp, xform_mat)

procedure set_window(win, ll, ur, vp)	# construct new graphics window
   local x_scale, y_scale, x_trans, y_trans, xfrm

   if /vp then {	# make vp the entire 'screen'
      vp := viewport()
      vp.ul := [0,0]
      vp.lr := [numeric(WAttrib(win,"width")), numeric(WAttrib(win,"height"))]
      vp.screen := win
      }

   # determine scale and translate factors ...
   #   (note the strange viewpoint references to get lower left corner)
   x_scale := real(vp.lr[1]-vp.ul[1]) / (ur[1]-ll[1])
   y_scale := real(vp.ul[2]-vp.lr[2]) / (ur[2]-ll[2])
   x_trans := real(vp.ul[1])-(ll[1]*x_scale)
   y_trans := real(vp.lr[2])-(ll[2]*y_scale)

   # ... and set up the transformation matrix
   xfrm := [mmult(set_scale(x_scale, y_scale), set_trans(x_trans, y_trans))]

   return window(ll, ur, vp, xfrm)
end   

procedure change_viewport(window, ul, lr)
   local x_scale, y_scale, x_trans, y_trans, xfrm

   # determine scale and translate factors ...
   #   (note the strange viewpoint references to get lower left corner)
   x_scale := real(lr[1]-ul[1]) / (window.ur[1]-window.ll[1])
   y_scale := real(ul[2]-lr[2]) / (window.ur[2]-window.ll[2])
   x_trans := real(ul[1])-(window.ll[1]*x_scale)
   y_trans := real(lr[2])-(window.ll[2]*y_scale)

   # ... and set up the transformation matrix
   xfrm := [mmult(set_scale(x_scale, y_scale), set_trans(x_trans, y_trans))]

   window.xform_mat := xfrm
   window.vp.ul := ul
   window.vp.lr := lr

   return
end



# support.icn -- miscellaneous support routines
############################################################################

# para -- parametric equation for coordinate between two others
#
procedure para(a,b,t)
   return (1.0-t)*a + t*b
end

# vpara -- produce a vector that is parametrically between two others
#
procedure vpara(v1,v2,t)
   local v, i

   v := copy(v1)
   every i := 1 to *v1 do
      v[i] := para(v1[i],v2[i],t)

   return v
end

# sleep -- 'sleep' of n seconds (n may be fractional)
#
procedure sleep(n)
   local start

   start := &time
   while &time <= start+n*1000
end

procedure round(n,g)
   return integer((n + g/2.0)/g) * g
end

# Some nice random functions

# Do a Gaussian distribution about the value 'x'.
#   The value of 'f' can be used to alter the shape
#   of the Gaussian distribution (larger values flatten
#   the curve...)

procedure Gauss_random(x,f)
   # if 'f' not passed in, default to 1.0
   /f := 1.0
   return gauss()*f+x
end

# Produce a random value within a Gaussian distribution
#   about 0.0.  (Sum 12 random numbers between 0 and 1,
#   (expected mean is 6.0) and subtract 6 to center on 0.0

procedure gauss()
   local v

   v := 0.0
   every 1 to 12 do v +:= ?0
   return v-6.0
end


#
# A simple implementation of 'turtle' graphics for multiple windows
#   one can have more than one turtle simultaneously active
#   In a turtle, the color field (if used) must be a co-expressions
#   that produces the color.  This allows the turtle to change
#   color as it runs.  In the simplest case, construct the
#   turtle with a co-expression the repeatedly supplies the
#   the same color:   create |"red"
############################################################################

record turtle(win,pos,direction,color)

procedure moveto(t,p)
   return t.pos := p
end

procedure lineto(t,p)
   Fg(t.win.vp.screen, \@\(t.color))
   DrwLine(t.win, t.pos, p)
   return t.pos := p
end

procedure moverel(t, displacement)
   return moveto(t, add_vectors(t.pos, displacement))
end

procedure drawrel(t, displacement)
   return lineto(t, add_vectors(t.pos, displacement))
end

procedure Line_Forward(t, dist)
   local angle, p

   angle := dtor(t.direction)
   p := [t.pos[1]+dist*cos(angle), t.pos[2]+dist*sin(angle)]
   return lineto(t, p)
end

procedure Move_Forward(t, dist)
   local angle, p

   angle := dtor(t.direction)
   p := [t.pos[1]+dist*cos(angle), t.pos[2]+dist*sin(angle)]
   return moveto(t, p)
end

procedure Right(t, angle)
   return t.direction -:= angle
end

procedure Left(t, angle)
   return t.direction +:= angle
end



# Some vector operations
############################################################################

procedure add_vectors(v1,v2)
   local v3, i

   if *v1 ~= *v2 then stop("cannot add vectors of differing sizes")

   v3 := copy(v1)
   every i := 1 to *v3 do
      v3[i] := v1[i]+v2[i]

   return v3
end

procedure sub_vectors(v1,v2)
   local v3, i

   if *v1 ~= *v2 then stop("cannot subtract vectors of differing sizes")

   v3 := copy(v1)
   every i := 1 to *v3 do
      v3[i] := v1[i]-v2[i]

   return v3
end

procedure scale_vector(s,a)
   local v, i

   v := copy(a)
   every i := 1 to *v do
      v[i] *:= s

   return v
end

procedure len_vector(v)
   local sum_sq

   sum_sq := 0
   every sum_sq +:= (!v)^2
   return sqrt(sum_sq)
end

procedure unit_vector(v)
   return scale_vector(1.0/len_vector(v), v)
end

procedure dot(v1,v2)
   local sum, i

   if *v1 ~= *v2 then stop("dot product: vectors of differing sizes")
   sum := 0
   every i := 1 to *v1 do
      sum +:= v1[i]*v2[i]
   return sum
end

procedure angle_vectors(v1,v2)
   return rtod(acos(dot(unit_vector(v1),unit_vector(v2))))
end

procedure normal_vector(v)
   local n

   n := copy(v)
   n[1] := v[2]
   n[2] := -v[1]
   return n
end

#
# The following are special cases for points...
#

procedure make_vector(p1,p2)
   return sub_vectors(p2,p1)
end

procedure distance(p1,p2)
   return len_vector(sub_vectors(p2,p1))
end

procedure Rel_angle(A,B)
   # get angle of line through points A and B (2D only!)
   local rise, run

   rise := B[2]-A[2]
   run  := B[1]-A[1]

   return rtod(atan(rise, run))
end

procedure normal_line(p1,p2)
	# return a normal to a line
   return normal_vector(make_vector(p1,p2))
end

procedure inside_line(P,L,n)
	# is P inside line passing through L with normal n?
   return 0 <= dot(sub_vectors(P,L),n)
end



# Transformation operations
############################################################################

procedure transform(p,M)
   local pl, i

	# convert p to a matrix for matrix multiply...
   every put((pl := [[]])[1], (!p)|1.0)	# the 1.0 makes it homogeneous

	# do the conversion...
   pl := mmult(pl, M)

	# convert list back to a point...
   p := copy(p)
   every i := 1 to *p do
      p[i] := pl[1][i]

   return p
end

procedure transform_points(pl,M)
   local xformed

   every put(xformed := [], !transform(!pl,M))
   return xformed
end
   
procedure set_scale(x,y,z)	# set up an Xform matrix for scaling
   local M

   M := if /z then Imatrix(3,3)
              else Imatrix(4,4)

   M[1][1] := x
   M[2][2] := y
   M[3][3] := \z

   return M
end

procedure set_trans(x,y,z)	# set up an Xform matrix for translation
   local M

   M := if /z then Imatrix(3,3)
              else Imatrix(4,4)

   M[*M][1] := x
   M[*M][2] := y
   M[*M][3] := \z

   return M
end

procedure set_rotate(x,y,z)	# set up an Xform matrix for rotation
   local X, Y, Z

   if /y & /z then {	# 2-D rotation
      X := Imatrix(3,3)
      X[1][1] := cos(x)
      X[2][2] := X[1][1]
      X[1][2] := sin(x)
      X[2][1] := -X[1][2]
      return X
      }

   X := Imatrix(4,4)
   X[2][2] := cos(x)
   X[3][3] := X[2][2]
   X[2][3] := sin(x)
   X[3][2] := -X[2][3]

   Y := Imatrix(4,4)
   Y[1][1] := cos(y)
   Y[3][3] := Y[1][1]
   Y[3][1] := sin(y)
   Y[1][3] := -Y[3][1]

   Z := Imatrix(4,4)
   Z[1][1] := cos(z)
   Z[2][2] := Z[2][2]
   Z[1][2] := sin(z)
   Z[2][1] := -Z[1][2]

   return mmult(X,mmult(Y,Z))
end

#
# Generalized parametric curve drawing routine, using turtle t
#
procedure draw_curve(t,x,xa,y,ya,t1,t2,N)
   local incr, t0

   /t1 := 0.0
   /t2 := 1.0
   /N  := 500

   incr := (t2-t1)/(N-1)

   t0 := t1
   moveto(t, point( x!([t0]|||xa), y!([t0]|||ya)))
   every 1 to N-1 do {
      t0 +:= incr
      lineto(t, point( x!([t0]|||xa), y!([t0]|||ya)))
      }

end
