-- $Id: domaincoloring.lua 965 2024-08-26 17:50:33Z herbert $

-----------------------------------------------------------------------
--         FILE:  domaincoloring.lua
--  DESCRIPTION:  create a visual reprensation of a complex function
-- REQUIREMENTS:  
--       AUTHOR:  Herbert Voß
--      LICENSE:  LPPL 1.3
--
-----------------------------------------------------------------------

local version = 0.03

kpse.set_program_name("luatex")

require("domaincoloring-complex-numbers")

local func = io.open("domaincoloring-functions.lua","r")
if func~=nil then
  io.close(func) 
  dofile("domaincoloring-functions.lua")
end

local function HSVtoRGB(h, s, v)
    local i = math.floor(h * 6)
    local f = h * 6 - i
    local p = v * (1 - s)
    local q = v * (1 - f * s)
    local t = v * (1 - (1 - f) * s)
    local no = i % 6
    if no == 0 then        return v,t,p
      elseif no == 1 then  return q,v,p
      elseif no == 2 then  return p,v,t
      elseif no == 3 then  return p,q,v
      elseif no == 4 then  return t,p,v
      else                 return v,p,q
    end
end

function my_color_scheme(z,hsvrgb)
  r, phi = cmath.polar(z)
-- phi is in -pi,+pi; change it to [0;2pi]
  phi = phi+pi
  h_str,s_str,v_str = hsvrgb:match("([^,]+),([^,]+),([^,]+)")
--  h = load("return "..h_str)()
--  s = load("return "..s_str)()
--  v = load("return "..v_str)()
  --h,s,v = loadstring("return"..hsvrgb)
  -- R,G,B, is [0,...,1]
  R,G,B = HSVtoRGB(load("return "..h_str)(),load("return "..s_str)(),load("return "..v_str)())   --    h,s,v)     -- (phi+3.14,1,r)
  return { math.fmod(math.floor(R*255+0.5),255), 
           math.fmod(math.floor(G*255+0.5),255),
           math.fmod(math.floor(B*255+0.5),255) }
end


--[[
A raster of Height rows, in order from top to bottom. 
Each row consists of Width pixels, in order from left to 
right. Each pixel is a triplet of red, green, and blue samples, 
in that order. Each sample is represented in pure binary by 
either 1 or 2 bytes. If the Maxval is less than 256, it is 
1 byte. Otherwise, it is 2 bytes. The most significant byte is first.
A row of an image is horizontal. A column is vertical. 
The pixels in the image are square and contiguous.
]]  

function write_eps_data(name,data,domain,res,grid)
  print("Lua: writing data file "..name.." ...")
--  print(data)
  outFile = io.open(name,"w+") -- Dateiname
  outFile:write([[
%!PS-Adobe-3.0 EPSF-3.0
%%Creator: (Herbert Voss)
%%Title: (domain.eps)
%%CreationDate: (]])
  outFile:write(os.date("%Y-%m-%d-%H.%M.%S")..")\n")
  outFile:write("%%BoundingBox: -0 -0 "..res[1].." "..res[2].."\n")
  outFile:write("%%HiResBoundingBox: 0 0 "..res[1].." "..res[2].."\n")
  outFile:write([[
%%DocumentData: Clean7Bit
%%LanguageLevel: 2
%%Pages: 1
%%EndComments
%%BeginDefaults
%%EndDefaults
%%BeginProlog
/DirectClassPacket {
  currentfile color_packet readhexstring pop pop
  /number_pixels 3 def
  0 3 number_pixels 1 sub { pixels exch color_packet putinterval } for
  pixels 0 number_pixels getinterval
} bind def
/DirectClassImage{
    columns rows 8 [ columns 0 0 rows neg 0 rows ]
    { DirectClassPacket } false 3 colorimage 
} bind def
/DisplayImage {
  gsave
  /buffer 512 string def
  /byte 1 string def
  /color_packet 3 string def
  /pixels 768 string def
  currentfile buffer readline pop
  token pop /x exch def
  token pop /y exch def pop
  x y translate
  currentfile buffer readline pop
  token pop /x exch def
  token pop /y exch def pop
  currentfile buffer readline pop
  token pop /pointsize exch def pop
  x y scale
  currentfile buffer readline pop
  token pop /columns exch def
  token pop /rows exch def pop
  currentfile buffer readline pop
  token pop /class exch def pop
  currentfile buffer readline pop
  token pop /compression exch def pop
  DirectClassImage
  grestore
} bind def
%%EndProlog
%%Page:  1 1
]])
  outFile:write("%%PageBoundingBox: 0 0 "..res[1].." "..res[2].."\n")
  outFile:write([[userdict begin
DisplayImage
0 0
]])
  outFile:write(res[1].." "..res[2].."\n")
  outFile:write("12\n")
  outFile:write(res[1].." "..res[2].."\n")
  outFile:write([[0
0
]])
  local i = 0
  for row = 1, res[2] do                -- alle Zeilen
    for col = 1, res[1] do              -- alle Spalten
      for k = 1,3 do
        if data[row][col][k] ~= data[row][col][k] then -- check for nan
          outFile:write("FF")
        else
          local num = data[row][col][k]
          if num < 16 then
            hex = string.format("0%X", num)
          else
            hex = string.format("%X", num)
          end
          outFile:write(hex)
        end
        i = i + 1
        if i == 39 then
          outFile:write("\n")        -- neue Zeile 
          i = 0
        end
      end
    end
  end
  outFile:write("\n")        -- neue Zeile 
  if grid == "true" then
    outFile:write("gsave \n /xMin "..domain[1].." def\n")
    outFile:write("/xMax "..domain[2].." def\n")
    outFile:write("/yMin "..domain[3].." def\n")
    outFile:write("/yMax "..domain[4].." def\n")
    outFile:write(res[1].." xMax xMin sub div dup /xScale exch def \n")
    outFile:write(res[2].." yMax yMin sub div dup /yScale exch def \n")
    outFile:write("scale\n")
    outFile:write([[
xMin neg yMin neg translate % the new origin\n"
gsave
xMin floor 0.5 yScale div sub
  0.5 xMax 0.5 add round { % loop variable on stack\n"
  dup yMin moveto yMax lineto
} for
2 ]])
    outFile:write(res[2]) 
    outFile:write([[  div  setlinewidth 0.4 setgray [5 5 ] { ]])
    outFile:write(res[2]..[[ div } forall 2 array astore 0 setdash
stroke
grestore
gsave
yMin floor 0.5 yScale div add          % startValue 
  % with shift 0.5 to be in the middle of the pixel
  0.5   % step
  yMax 0.5 add round { % loop variable on stack
  dup xMin exch moveto xMax exch lineto
} for
2 ]])
    outFile:write(res[1]) 
    outFile:write([[  div  setlinewidth 0.4 setgray [5 5 ] { ]])
    outFile:write(res[1]..[[ div } forall 2 array astore 0 setdash
stroke
grestore 
gsave
xMin floor 0.5 yScale div sub
  1 xMax 0.5 add round { % loop variable on stack\n"
  dup yMin moveto yMax lineto
} for
2 ]])
    outFile:write(res[2]) 
    outFile:write([[ div  setlinewidth 0.3 setgray stroke
grestore
gsave
yMin floor 0.5 yScale div add
  1 yMax 0.5 add round { % loop variable on stack
  dup xMin exch moveto xMax exch lineto
} for 
2 ]])
    outFile:write(res[1]) 
    outFile:write([[ div  setlinewidth 0.3 setgray stroke
grestore
]])
  end
  outFile:write([[
end % of userdict
%%PageTrailer
%%Trailer
%%EOF
    ]])
  outFile:close() -- Datei schließen 
  --os.execute("pnmtopng apfel.ppm >"..fileName..".png") -- ppm -> png Format 
end

function write_data(name,data,domain,res)  -- only valid for writing ppm files
  print("Lua: writing data to file "..name.." ...")
--  print(data)
  outFile = io.open(name,"w+") -- Dateiname
  outFile:write("P3 ")                -- Grafictype R G B
  outFile:write(res[1].." "..res[2].." ") -- Anzahl Pixel: Breite x Höhe (cols x rows) 
  outFile:write("255\n")           -- Höchster Zahlenwert (0..255) 
  for row = 1, res[2] do                -- alle Zeilen
    for col = 1, res[1] do              -- alle Spalten
      for k = 1,3 do
        if data[row][col][k] ~= data[row][col][k] then -- check for nan
          outFile:write("255 ")
        else
          outFile:write(data[row][col][k].." " )
        end
      end
    end
    outFile:write("\n")        -- neue Zeile 
  end
  outFile:close() -- Datei schließen 
  --os.execute("pnmtopng apfel.ppm >"..fileName..".png") -- ppm -> png Format 
end

function plot(fString, domain, res, funcName, hsvrgb, bgColor, invers)
  print("\nLua: calculating data with "..tostring(fString).." ... ")
  local left = domain[1]
  local right = domain[2]
  local bottom = domain[3]
  local top = domain[4]
  local dx = (right-left)/res[1]
  local dy = (top-bottom)/res[2]
  local img = {}      -- mxn-Matrix  (rows x colums) img[row][col]
  for row = 1,res[2] do
    img[row] = {}
    for col = 1,res[1] do
      img[row][col] = 0 
    end
  end
  for row = 1, res[2] do 
    y = top-dy*row
    for col = 1,res[1] do 
--      print(j,i)
      x = left+dx*col
      z = complex(x, y)
      if funcName then
        fz = fString(z)   -- use func number from external file
      else
        fz = load("return "..fString)() -- for example: cmath.cos(z)/cmath.sin(z^4-1)
      end        
      color = my_color_scheme(fz,hsvrgb)
      if invers == "true" then
        for c=1,#color do
          color[c] = math.abs(color[c]-255)
        end
      end
      if bgColor > -1 then
        if color[1] + color[2] + color[3] <= bgColor then
          color = { 255, 255, 255 }
        end
      end
--      print(x,y,color[1],color[2],color[3])
      img[row][col] = color
    end
  end
  return img
end

--[[
  f: function as string if funcName is False
     as funcName from external file if funcName is TRUE
  Domain:  xMin,xMax,yMin,yMax
  Resolution: (points per Interval) xRes,yRes
  funcName: true/false (see f)    
]]
function createData(jobname,f,funcName,Domain,Resolution,hsvrgb,bgColor,invers,grid)
    if funcName == "" then
      img = plot(f,Domain,Resolution,false,hsvrgb,bgColor,invers)
    else
      img = plot(load("return "..funcName)(),Domain,Resolution,true,hsvrgb,bgColor,invers)
    end
--  write_data("domain.ppm",img,Resolution)
    write_eps_data(jobname.."-domain.eps",img,Domain,Resolution,grid)
end

