--[[ HexArt Image Editor Copyright (C) 2019 Soni L. This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . --]] local kt = {} local t = 0.05 local t0 = 0.5 -- initial cooldown local TIMEOUT_FIRST = -32767 -- a very negative number local saved = 0xFFFFFFFFFFFF function love.keypressed(k) if k == "escape" then if saved and saved > love.timer.getTime() then love.event.quit() else saved = love.timer.getTime() + 0.5 end end kt[k] = TIMEOUT_FIRST end function love.keyreleased(k) kt[k] = nil end local visual_start local cp = 1 local palt = {} local imgpalt = {} local imgt = {} for i=1,256 do imgt[i] = 0 end for i=1,256 do imgpalt[i] = 0 end for i=0,255 do palt[i] = 0 end for i=0,15 do palt[i] = i*0x11111100 + 0xFF imgpalt[i*16 + 1] = 0x11*i imgpalt[i*16 + 2] = 0x11*i imgpalt[i*16 + 3] = 0x11*i imgpalt[i*16 + 4] = 0xFF end local batch local batchid = {} -- should have 512 entries local hexquads = {} local palettedata local paletting local imagedata local backimage local backimagedata local image -- PAL_MAX is the max value of a pixel. -- this is 255 in paletting mode, 15 otherwise. local PAL_MAX = 15 local mul = 36 local function markupdated(pos, redraw) local x = (pos-1) % 16 local y = math.floor((pos-1) / 16) if not batchid[pos * 2] then batchid[pos * 2] = batch:add(hexquads[math.floor(imgt[pos]/16)], x * mul, y * mul) batchid[pos * 2 + 1] = batch:add(hexquads[imgt[pos]%16], x * mul + 16, y * mul) else batch:set(batchid[pos * 2], hexquads[math.floor(imgt[pos]/16)], x * mul, y * mul) batch:set(batchid[pos * 2 + 1], hexquads[imgt[pos]%16], x * mul + 16, y * mul) end local rgba = palt[imgt[pos]] local r = math.floor(rgba / 16777216) % 256 local g = math.floor(rgba / 65536) % 256 local b = math.floor(rgba / 256) % 256 local a = rgba % 256 imagedata:setPixel(x, y, r/255, g/255, b/255, a/255) if paletting then local r, g, b, a = imgt[y*16+1], imgt[y*16+2], imgt[y*16+3], imgt[y*16+4] imagedata:setPixel(3, y, r/255, g/255, b/255, a/255) end if redraw then image:replacePixels(imagedata) end end local function change_val(amt) for i=math.min(visual_start or cp, cp), math.max(visual_start or cp, cp) do imgt[i] = math.max(math.min(imgt[i] + amt, PAL_MAX), 0) markupdated(i, true) end end function love.wheelmoved(x, y) saved = nil change_val(y) end function setback(pos, highlight) local x = (pos-1)%16 local y = math.floor((pos-1)/16) if highlight then backimagedata:setPixel(x, y, 0.5, 0, 0, 1) else backimagedata:setPixel(x, y, 0, 0, 0, 0) end end local in_mouse = false local function palette() -- Called to enable/disable palette editing mode paletting = not paletting imagedata, palettedata = palettedata, imagedata imgt, imgpalt = imgpalt, imgt PAL_MAX = paletting and 255 or 15 if paletting then for i=0,255 do palt[i] = i*0x01010100 + 0xff end else for i=0,15 do local r = imgpalt[i*16+1] local g = imgpalt[i*16+2] local b = imgpalt[i*16+3] local a = imgpalt[i*16+4] palt[i] = ((r*256+g)*256+b)*256+a end end for i=1,256 do markupdated(i, false) end image:replacePixels(imagedata) end function love.mousepressed(x, y, button, istouch, presses) if button == 1 then local rawx, rawy = x, y local x, y = math.floor(x / 36), math.floor(y / 36) if x > 15 or y > 15 then if rawx > 576 then -- TODO replace with more efficient graphics if rawy > 130 and rawy <= 130 + love.graphics.getFont():getHeight() then palette() end end return end if visual_start then for i=math.min(visual_start or cp, cp), math.max(visual_start or cp, cp) do setback(i, false) end visual_start = false else setback(cp, false) end cp = y * 16 + x + 1 setback(cp, true) backimage:replacePixels(backimagedata) in_mouse = true end end function love.mousereleased(x, y, button, istouch, presses) in_mouse = false end function love.mousemoved(x, y, dx, dy, istouch) if in_mouse then if visual_start then local x, y = math.floor(x / 36), math.floor(y / 36) if x > 15 or y > 15 then return end cp = y * 16 + x + 1 else visual_start = cp end end end function love.update(dt) local function ktup(key, func) if kt[key] then local nt = kt[key] - dt kt[key] = nt if nt <= 0 then if nt <= TIMEOUT_FIRST then kt[key] = t0 else kt[key] = t end return func() end end end local function imgup() setback(cp, false) cp = math.max(cp - 16, 1) setback(cp, true) backimage:replacePixels(backimagedata) end local function imgdown() setback(cp, false) cp = math.min(cp + 16, 256) setback(cp, true) backimage:replacePixels(backimagedata) end local function imgleft() setback(cp, false) cp = math.max(cp - 1, 1) setback(cp, true) backimage:replacePixels(backimagedata) end local function imgright() setback(cp, false) cp = math.min(cp + 1, 256) setback(cp, true) backimage:replacePixels(backimagedata) end local function decr() saved = nil local amt = -(kt['lshift'] and 16 or 1) change_val(amt) end local function incr() saved = nil local amt = (kt['lshift'] and 16 or 1) change_val(amt) end local function light_propagation_up() if visual_start then print("[NYI] light_propagation_up + visual_start") return end saved = nil local initialValue = imgt[cp] local newValue = math.min(initialValue + 1, 15) if initialValue == newValue then return end local function recurse(pos, dx, dy, targetValue) local xpos = (pos - 1) % 16 + 1 local ypos = ((pos - xpos)/16) + 1 local npos if dx < 0 and xpos > 1 then npos = pos - 1 elseif dx > 0 and xpos < 16 then npos = pos + 1 elseif dy > 0 and ypos < 16 then npos = pos + 16 flag = print elseif dy < 0 and ypos > 1 then npos = pos - 16 end if npos and imgt[npos] < targetValue then imgt[npos] = targetValue markupdated(npos, false) recurse(npos, dx, dy, targetValue - 1) recurse(npos, dy, -dx, targetValue - 1) end end imgt[cp] = newValue markupdated(cp, false) recurse(cp, 0, 1, newValue - 1) recurse(cp, 1, 0, newValue - 1) recurse(cp, 0, -1, newValue - 1) recurse(cp, -1, 0, newValue - 1) image:replacePixels(imagedata) end local function light_propagation_down() if true then print("[NYI] light_propagation_down") return end if visual_start then print("[NYI] light_propagation_down + visual_start") return end saved = nil local initialValue = imgt[cp] local newValue = math.max(initialValue - 1, 0) if initialValue == newValue then return end local function recurse(pos, dx, dy, targetValue) local xpos = (pos - 1) % 16 + 1 local ypos = ((pos - xpos)/16) + 1 local npos if dx < 0 and xpos > 1 then npos = pos - 1 elseif dx > 0 and xpos < 16 then npos = pos + 1 elseif dy > 0 and ypos < 16 then npos = pos + 16 flag = print elseif dy < 0 and ypos > 1 then npos = pos - 16 end if npos and imgt[npos] < targetValue then imgt[npos] = targetValue markupdated(npos, false) recurse(npos, dx, dy, targetValue - 1) recurse(npos, dy, -dx, targetValue - 1) end end imgt[cp] = newValue markupdated(cp, false) recurse(cp, 0, 1, newValue - 1) recurse(cp, 1, 0, newValue - 1) recurse(cp, 0, -1, newValue - 1) recurse(cp, -1, 0, newValue - 1) image:replacePixels(imagedata) end local function save() saved = 0xFFFFFFFFFFFF local data = imagedata:encode("png") local name = love.data.encode("string", "hex", love.data.hash("sha1", data):sub(1, 7)) .. ".png" love.filesystem.write("img-" .. name, data) end local function visual() if visual_start then for i=math.min(visual_start or cp, cp), math.max(visual_start or cp, cp) do setback(i, false) end setback(cp, true) backimage:replacePixels(backimagedata) end visual_start = not visual_start and cp end ktup('k', imgup) ktup('up', imgup) ktup('h', imgleft) ktup('left', imgleft) ktup('j', imgdown) ktup('down', imgdown) ktup('l', imgright) ktup('right', imgright) ktup('z', decr) ktup('x', incr) ktup('a', light_propagation_up) ktup('s', light_propagation_down) ktup('w', save) ktup('p', palette) ktup('v', visual) if visual_start then -- TODO optimize visual mode for i=1,255 do setback(i, false) end for i=math.min(visual_start, cp), math.max(visual_start, cp) do setback(i, true) end backimage:replacePixels(backimagedata) end end local quad local backquad local heximage function love.load() imagedata = love.image.newImageData(16, 16) backimagedata = love.image.newImageData(16, 16) palettedata = love.image.newImageData(16, 16) image = love.graphics.newImage(imagedata, {mipmaps=false}) backimage = love.graphics.newImage(backimagedata, {mipmaps=false}) image:setFilter("nearest", "nearest") backimage:setFilter("nearest", "nearest") quad = love.graphics.newQuad(0, 0, 16, 16, 16, 16) backquad = love.graphics.newQuad(0, 0, 16, 16, 16, 16) heximage = love.graphics.newImage("hex.png") for i = 0, 15 do hexquads[i] = love.graphics.newQuad(i*16, 0, 16, 32, heximage:getDimensions()) end batch = love.graphics.newSpriteBatch(heximage) for i=1,256 do markupdated(i, false) end image:replacePixels(imagedata) for x=0, 15 do for y=0, 15 do backimagedata:setPixel(x, y, 0, 0, 0, 0) end end local x = (cp-1)%16 local y = math.floor((cp-1)/16) backimagedata:setPixel(x, y, 0.5, 0, 0, 1) backimage:replacePixels(backimagedata) end function love.draw() love.graphics.draw(image, quad, 576, 0, 0, 8) love.graphics.draw(backimage, backquad, 0, 0, 0, mul) local min, max = math.min(visual_start or cp, cp), math.max(visual_start or cp, cp) love.graphics.print((visual_start and tostring(min) .. ":" or "") .. tostring(max), 0, 588) love.graphics.print(paletting and "palette" or "normal", 576, 130) love.graphics.draw(batch, 2, 2) end -- vim: set sw=2 sts=2: