summaryrefslogtreecommitdiff
path: root/graphics/pgf/contrib/pixelart/pixelart.lua
diff options
context:
space:
mode:
Diffstat (limited to 'graphics/pgf/contrib/pixelart/pixelart.lua')
-rw-r--r--graphics/pgf/contrib/pixelart/pixelart.lua680
1 files changed, 680 insertions, 0 deletions
diff --git a/graphics/pgf/contrib/pixelart/pixelart.lua b/graphics/pgf/contrib/pixelart/pixelart.lua
new file mode 100644
index 0000000000..97a76d8f76
--- /dev/null
+++ b/graphics/pgf/contrib/pixelart/pixelart.lua
@@ -0,0 +1,680 @@
+require("lualibs-lpeg")
+local luakeys = require("luakeys")
+
+pixelart = {
+ _debug = false,
+ _counter = 0,
+}
+
+--------------------------------------------------------------------------------
+--[[ Debug on/off
+--]]
+
+local function pixelart_setpixelartdebug(flag)
+ luakeys.opts.debug = flag
+ pixelart._debug = flag
+end
+
+pixelart.setpixelartdebug = pixelart_setpixelartdebug
+pixelart.setpixelartdebug = pixelart_setpixelartdebug
+
+--------------------------------------------------------------------------------
+--[[ Print
+--]]
+
+local function tex_sprint(text)
+ tex.sprint(text)
+ if pixelart._debug then
+ io.write(text)
+ end
+end
+
+local function tex_print(text)
+ tex.print(text)
+ if pixelart._debug then
+ io.write(text, "\n")
+ end
+end
+
+--------------------------------------------------------------------------------
+--[[ Define and use colors
+-- ]]
+pixelart._colors = {}
+
+local function pixelart_parsecolors(argument)
+ return luakeys.parse(
+ argument,
+ {
+ naked_as_value = true,
+ hooks = {
+ keys = function(key, value, depth, current, result)
+ return tostring(key), value
+ end,
+ }
+ }
+ )
+end
+
+local function pixelart_newpixelartcolors(name, argument)
+ if pixelart._colors[name] == nil then
+ pixelart._colors[name] = pixelart_parsecolors(argument)
+ else
+ error(string.format("Error: Colors '%s' is already defined.", name))
+ end
+end
+
+local function pixelart_renewpixelartcolors(name, argument)
+ pixelart._colors[name] = pixelart_parsecolors(argument)
+end
+
+pixelart.newpixelartcolors = pixelart_newpixelartcolors
+pixelart.renewpixelartcolors = pixelart_renewpixelartcolors
+
+-- Default color sets
+pixelart._colors["explicit"] = {}
+
+pixelart._colors["RGB"] = {
+ R = "red",
+ G = "green",
+ B = "blue",
+ W = "white",
+ K = "black"
+}
+
+pixelart._colors["BW"] = {
+ ["0"] = "white",
+ ["1"] = "black",
+}
+
+pixelart._colors["gray"] = {
+ ["0"] = "white",
+ ["1"] = "white!89!black",
+ ["2"] = "white!78!black",
+ ["3"] = "white!67!black",
+ ["4"] = "white!56!black",
+ ["5"] = "white!44!black",
+ ["6"] = "white!33!black",
+ ["7"] = "white!22!black",
+ ["8"] = "white!11!black",
+ ["9"] = "black",
+}
+
+pixelart._colors["mono"] = {}
+for _, char in pairs(string.explode("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789", "")) do
+ pixelart._colors["mono"][char] = ""
+end
+
+
+--------------------------------------------------------------------------------
+--[[ Options
+--]]
+
+local ALGORITHMS = {squares = true, stack = true}
+
+local function parse(str, default)
+ local parser = luakeys.define({
+ tikz = {},
+ colors = {
+ process = function(value, input, result, unknown)
+ -- If argument is a string, convert numeric keys into strings
+ if type(value) ~= "table" then
+ return value
+ end
+
+ local converted = {}
+ for k, v in pairs(value) do
+ converted[tostring(k)] = v
+ end
+ return converted
+ end,
+ },
+ margin = {
+ data_type = "number",
+ default = 0,
+ always_present = true,
+ },
+ style = {
+ data_type = "string",
+ default = "pixelart",
+ always_present = true,
+ },
+ squares = {
+ exclusive_group = "algo",
+ },
+ stack = {
+ exclusive_group = "algo",
+ },
+ draft = {
+ data_type = "boolean",
+ },
+ })
+
+ options = parser(str, {
+ defaults = default,
+ })
+
+ -- Algo option
+ local algo = nil
+ for key, value in pairs(ALGORITHMS) do
+ if options[key] then
+ algo = key
+ end
+ end
+ if not algo then
+ options[pixelart._default_algo[1]] = pixelart._default_algo[2]
+ end
+
+ -- Convert Tikz option back to string
+ if type(options.tikz) == type({}) then
+ options.tikz = luakeys.render(options.tikz)
+ end
+
+ return options
+end
+
+local DEFAULT = {colors = "mono"}
+local function pixelart_setpixelartdefault(str)
+ pixelart._default = parse(str, DEFAULT)
+
+ for key, value in pairs(pixelart._default) do
+ if ALGORITHMS[key] then
+ pixelart._default_algo = {key, value}
+ pixelart._default[key] = nil
+ end
+ end
+end
+
+pixelart._default = {colors = "mono"}
+pixelart._default_algo = {"stack", {}}
+pixelart.setpixelartdefault = pixelart_setpixelartdefault
+
+--------------------------------------------------------------------------------
+--[[ Parse pixelart string
+--Parse arguments, and build a table of tables.
+--]]
+--
+local lineRE = lpeg.Ct(
+ -- Regular expression to match a line of colors
+ (
+ (lpeg.P("{") * lpeg.C((lpeg.P(1) - lpeg.S("{}"))^0) * lpeg.P("}"))
+ +
+ lpeg.C((lpeg.P(1) - lpeg.S("{}")))
+ )^0
+)
+
+local function str2arrays(str)
+ -- Turn the \pixelart{} argument into a table (of lines) of tables (of colors).
+
+ -- Trim string: remove leading and trailing whitespaces
+ str = str:gsub("^%s*(.-)%s*$", "%1")
+
+ -- Turn it into a table (which is called "array" not to clash with the "table" library)
+ local array = {}
+ for k, line in ipairs(string.explode(str, " ")) do
+ array[k] = lineRE:match(line)
+ end
+
+ -- Ensure each row has the same number of columns
+ local length = 0
+ for k, line in ipairs(array) do
+ if #line > length then
+ length = #line
+ end
+ end
+ for k, line in ipairs(array) do
+ if #line < length then
+ for i=#line+1, length do
+ line[i] = false
+ end
+ end
+ end
+
+ -- Flip array so that flipped[x][y] gives the pixel with coordinates (x, y), in the standard frame
+ local flipped = {}
+ for y, line in ipairs(array) do
+ for x, color in ipairs(array[y]) do
+ if y == 1 then
+ flipped[x] = {}
+ end
+ flipped[x][#array - y + 1] = color
+ end
+ end
+
+ return flipped
+end
+
+--------------------------------------------------------------------------------
+--[[ Color tools
+--]]
+local function color2color(colors, color)
+ -- Convert a color (as given by the user) into a color (usable by TikZ).
+ if color == "." or not color then
+ -- Transparent pixel: do not print anything
+ return nil
+ elseif colors[color] and colors[color] ~= "" then
+ -- A color has been defined: use it
+ return string.format("color=%s", colors[color])
+ elseif colors[color] and colors[color] == "" then
+ -- An empty color has been defined: use the default TikZ color
+ return ""
+ else
+ -- No color has been defined: use the argument as the TikZ color
+ return color
+ end
+end
+
+--------------------------------------------------------------------------------
+-- Turn pixelart string into TikZ code, using the SQUARES algorithm
+
+local function pixelart_body_squares(array, colors, options)
+ -- Draw the tikz pixels, as a set of squares.
+
+ tex_print(string.format(
+ [[\clip ({0-%s}, {0-%s}) rectangle (%s, %s); ]],
+ options.margin,
+ options.margin,
+ #array + options.margin,
+ #array[1] + options.margin
+ ))
+
+ for x, column in ipairs(array) do
+ for y, color in ipairs(column) do
+ color = color2color(colors, color)
+
+ ---------------------
+ -- Which pixel size?
+ local overlap
+ if type(options.squares) == type({}) then
+ overlap = options.squares["overlap"] or "0"
+ else
+ overlap = "0"
+ end
+
+ ---------------------
+ -- At last, we can display the pixel…
+ if color ~= nil then
+ tex_print(string.format([[\fill[%s, %s] (%s, %s) rectangle ++(1+%s, 1+%s);]],
+ options.style,
+ color,
+ string.format("{%s-%s}", x-1, overlap),
+ string.format("{%s-%s}", y-1, overlap),
+ 2*overlap,
+ 2*overlap
+ ))
+ end
+ end
+ end
+end
+
+--------------------------------------------------------------------------------
+-- Turn pixelart string into TikZ code, using the STACK algorithm
+
+local function remove_zone(write, coord, read)
+ -- Remove the zone of the pixel `coord`
+ -- If both write and read are present (they are expected to be 2D-arrays of the same size, then read zones from `read`, and remove them from `write`.
+
+ -- Default values for options
+ if read == nil then
+ read = write
+ end
+ if colorblind == nil then
+ colorblind = false
+ end
+
+ local originalcolor = read[coord[1]][coord[2]]
+ local samecolor = function(color) return color == originalcolor end
+
+ -- Go!
+ local stack = {}
+ table.insert(stack, coord)
+ while #stack ~= 0 do
+ local current = table.remove(stack)
+ for _, neighbour in pairs({
+ {current[1]-1, current[2]},
+ {current[1]+1, current[2]},
+ {current[1], current[2]-1},
+ {current[1], current[2]+1},
+ }) do
+ if (
+ neighbour[1] >= 1 and neighbour[1] <= #read -- First coordinate inside the array
+ and
+ neighbour[2] >= 1 and neighbour[2] <= #read[1] -- Second coordinate inside the array
+ and
+ write[neighbour[1]][neighbour[2]] -- Not processed yet
+ and
+ samecolor(read[neighbour[1]][neighbour[2]]) -- Same color
+ ) then
+ table.insert(stack, neighbour)
+ end
+ end
+ write[current[1]][current[2]] = false
+ end
+end
+
+local border_transitions = {
+ westtop = {
+ tests = {
+ {1, 1},
+ {1, 0},
+ },
+ next = {
+ ["true true"] = {
+ step = {1, 1},
+ state = "northleft",
+ mark = {0, 0},
+ },
+ ["false true"] = {
+ step = {1, 0},
+ state = "westtop",
+ mark = nil,
+ },
+ ["true false"] = {
+ step = {0, 0},
+ state = "southright",
+ mark = {1, 1},
+ },
+ ["false false"] = {
+ step = {0, 0},
+ state = "southright",
+ mark = {1, 1},
+ },
+ },
+ },
+ northleft = {
+ tests = {
+ {-1, 1},
+ {0, 1},
+ },
+ next = {
+ ["true true"] = {
+ step = {-1, 1},
+ state = "eastbottom",
+ mark = {1, 0},
+ },
+ ["true false"] = {
+ step = {0, 0},
+ state = "westtop",
+ mark = {0, 1},
+ },
+ ["false true"] = {
+ step = {0, 1},
+ state = "northleft",
+ mark = nil,
+ },
+ ["false false"] = {
+ step = {0, 0},
+ state = "westtop",
+ mark = {0, 1},
+ },
+ },
+ },
+ southright = {
+ tests = {
+ {0, -1},
+ {1, -1},
+ },
+ next = {
+ ["true true"] = {
+ step = {1, -1},
+ state = "westtop",
+ mark = {0, 1},
+ },
+ ["false true"] = {
+ step = {0, 0},
+ state = "eastbottom",
+ mark = {1, 0},
+ },
+ ["true false"] = {
+ step = {0, -1},
+ state = "southright",
+ mark = nil,
+ },
+ ["false false"] = {
+ step = {0, 0},
+ state = "eastbottom",
+ mark = {1, 0},
+ },
+ },
+ },
+ eastbottom = {
+ tests = {
+ {-1, 0},
+ {-1, -1},
+ },
+ next = {
+ ["true true"] = {
+ step = {-1, -1},
+ state = "southright",
+ mark = {1, 1},
+ },
+ ["false true"] = {
+ step = {0, 0},
+ state = "northleft",
+ mark = {0, 0},
+ },
+ ["true false"] = {
+ step = {-1, 0},
+ state = "eastbottom",
+ mark = nil,
+ },
+ ["false false"] = {
+ step = {0, 0},
+ state = "northleft",
+ mark = {0, 0},
+ },
+ },
+ },
+}
+
+local function iter_border(array, start, colorblind)
+ -- If colorblind==true, all colors are considered the same (that is, a pixel is either transparent or colored, with no difference between the colors)
+ local samecolor
+ if colorblind then
+ samecolor = function(current, test)
+ if (
+ current[1] + test[1] < 1
+ or
+ current[1] + test[1] > #array
+ ) then
+ return false
+ end
+ color = array[current[1] + test[1]][current[2] + test[2]]
+ if color == nil or color == false then
+ return false
+ end
+ return (
+ color == "." and array[start[1]][start[2]] == "."
+ ) or (
+ color ~= "." and array[start[1]][start[2]] ~= "."
+ )
+ end
+ else
+ samecolor = function(current, test)
+ if (
+ current[1] + test[1] < 1
+ or
+ current[1] + test[1] > #array
+ ) then
+ return false
+ end
+ color = array[current[1] + test[1]][current[2] + test[2]]
+ return color == array[start[1]][start[2]]
+ end
+ end
+
+ local current = {start[1], start[2]}
+ local state = "northleft"
+
+ return function()
+ while true do
+ tests = border_transitions[state].tests
+ local transition = border_transitions[state].next[string.format(
+ "%s %s",
+ samecolor(current, tests[1]),
+ samecolor(current, tests[2])
+ )]
+ state = transition.state
+ current = {
+ current[1] + transition.step[1],
+ current[2] + transition.step[2]
+ }
+ if transition.mark then
+ if current[1] == start[1] and current[2] == start[2] and state == "northleft" then
+ return
+ else
+ return {
+ current[1] + transition.mark[1],
+ current[2] + transition.mark[2],
+ }
+ end
+ end
+ end
+ end
+end
+
+local function iter_unprocessed_zones(array)
+ -- Iterate coordinates of pixels that haven't been processed yet (their value is not false)
+ local x = 1
+ local y = 1
+
+ return function()
+ while not array[x][y] do
+ x = x + 1
+ if x > #array then
+ x = 1
+ y = y + 1
+ if y > #array[1] then
+ return
+ end
+ end
+ end
+ return {x, y}
+ end
+end
+
+local function pixelart_body_stack(array, colors, options)
+ -- The first argument is an array of lines, each of them being an array of colors (i.e. color of the first pixel of the line, color of the second pixel of the line, etc).
+ -- Some "colors" have meaning:
+ -- - false (the boolean): pixel has already been processed
+ -- - "." (the character dot): pixel is transparent
+ -- - anything else: the color of the pixel
+
+ if #array == 0 then
+ return
+ end
+
+ tex_print([[\begin{scope}[even odd rule] ]])
+
+ -- Clip away transparent zones
+ tex_print(string.format(
+ [[\clip ({0-%s}, {0-%s}) rectangle (%s, %s) ]],
+ options.margin,
+ options.margin,
+ #array + options.margin,
+ #array[1] + options.margin
+ ))
+
+ for x = 1, #array do
+ for y = 1, #array[x] do
+ if array[x][y] == "." then
+ tex_print(string.format("(%s, %s) rectangle ++(1, 1) ", x-1, y-1))
+ end
+ end
+ end
+
+ tex_print(";")
+
+ -- Draw color zones
+ for current in iter_unprocessed_zones(array) do
+ local color = color2color(colors, array[current[1]][current[2]])
+ if color == nil then
+ -- Nothing to do: transparent zone
+ else
+ tex_sprint(string.format([[\fill[%s, %s] (%s, %s) ]], options.style, color, current[1]-1, current[2]-1))
+ for coord in iter_border(array, current, false) do
+ tex_sprint(string.format([[ -- (%s, %s) ]], coord[1]-1, coord[2]-1))
+ end
+ tex_print(string.format([[ -- cycle ;]]))
+ end
+ remove_zone(array, {current[1], current[2]})
+ end
+
+ tex_print([[\end{scope} ]])
+end
+
+--------------------------------------------------------------------------------
+-- Functions pixelart() and tikzpixelart()
+
+local function pixelart_body(str, options)
+ -- Debug
+ pixelart._counter = pixelart._counter + 1
+ if pixelart._debug then
+ io.write("\n%", str, "\n")
+ io.write("% pixelart ", pixelart._counter, ", file ", status.filename, ", input line ", tex.inputlineno, "\n")
+ else
+ io.write("(pixelart ", pixelart._counter, ", file ", status.filename, ", input line ", tex.inputlineno)
+ end
+
+ -- Colors
+ local colors
+ if type(options.colors) == "table" then
+ colors = options.colors
+ else
+ colors = pixelart._colors[options.colors]
+ end
+
+ if (pixelart._draft or options.draft) and options.draft ~= false then
+ local array = str2arrays(str)
+ tex_print(string.format(
+ [[ \draw[pattern=checkerboard] (0, 0) rectangle (%s, %s); ]],
+ #array, #array[1]
+ ))
+ elseif options.stack then
+ pixelart_body_stack(str2arrays(str), colors, options)
+ else -- options.squares is the default
+ pixelart_body_squares(str2arrays(str), colors, options)
+ end
+
+ if pixelart._debug then
+ -- Nothing
+ else
+ io.write(")")
+ end
+end
+
+local function pixelart_tikzpixelart(coord, str, options)
+ -- Parse options
+ local options = parse(options, pixelart._default)
+
+ if options.tikz then
+ tex_sprint(string.format([[\begin{scope}[%s] ]], options.tikz))
+ end
+ tex_print(string.format(
+ [[\begin{scope}[shift={%s}] ]],
+ coord
+ ))
+
+ pixelart_body(str, options)
+
+ tex_print([[\end{scope} ]])
+ if options.tikz then
+ tex_print([[\end{scope} ]])
+ end
+end
+
+local function pixelart_pixelart(str, options)
+ -- Parse options
+ local options = parse(options, pixelart._default)
+ -- Tikz environment
+ tex_print([[\begin{tikzpicture}]])
+ if options.tikz then
+ tex_sprint(string.format("[%s]", options.tikz))
+ end
+
+ pixelart_body(str, options)
+
+ tex_print([[\end{tikzpicture}]])
+end
+
+pixelart.pixelart = pixelart_pixelart
+pixelart.tikzpixelart = pixelart_tikzpixelart