commit b03af23da5a5b9cea84cacb2e7c8b4b52ab0886f Author: Rokas Puzonas Date: Thu May 11 21:28:37 2023 +0300 initial commit diff --git a/BezierCurve.lua b/BezierCurve.lua new file mode 100644 index 0000000..a9cf609 --- /dev/null +++ b/BezierCurve.lua @@ -0,0 +1,99 @@ +local Vector2 = require("Vector2") +local BezierCurve = {} +local width = 5 +local interval = 1/10 +BezierCurve.maxDepth = -1 +BezierCurve.randSeed = love.math.random() +BezierCurve.__index = BezierCurve + +local function dist(x1,y1,x2,y2) + return ((x1-x2)^2+(y1-y2)^2)^0.5 +end + +local function linearBezier(p1, p2) + return (1-t)*p1 + t*p2 +end + +function BezierCurve.new(class, points) + return setmetatable({ + _curve = love.math.newBezierCurve(points or {100,180,250,250,200,500}), + selectedPoint = false + }, class) +end + +function BezierCurve:draw(drawPoints, drawLines, drawFinal) + love.graphics.setLineJoin("bevel") + love.graphics.setLineStyle("smooth") + + if drawLines then + love.graphics.setLineWidth(width*0.25) + local p = {} + for i=1, self._curve:getControlPointCount() do + table.insert(p, Vector2(self._curve:getControlPoint(i))) + end + BezierCurve.stringArt(p, 0) + end + + if drawFinal then + love.graphics.setLineWidth(width) + love.graphics.setColor(0.8,0,0) + love.graphics.line(self._curve:render()) + end + + if drawPoints then + love.graphics.setColor(0,0,0.8) + for i=1,self._curve:getControlPointCount() do + local x,y = self._curve:getControlPoint(i) + love.graphics.circle("fill",x,y,width*1.25) + end + end +end + +function BezierCurve:mousemoved(x, y) + if self.selectedPoint then + self._curve:setControlPoint(self.selectedPoint, x, y) + end +end + +function BezierCurve:mousepressed(x, y, button) + if button == 1 then + local isCtrl = love.keyboard.isDown("lctrl", "rctrl") + for i=1,self._curve:getControlPointCount() do + if dist(x,y,self._curve:getControlPoint(i)) < width*1.25 then + if isCtrl then + self._curve:removeControlPoint(i) + else + self.selectedPoint = i + end + return + end + end + if isCtrl then + self._curve:insertControlPoint(x, y) + end + end +end + +function BezierCurve:mousereleased(_, _, button) + if button == 1 then + self.selectedPoint = false + end +end + +function BezierCurve.stringArt(points, depth) + if #points > 2 and (BezierCurve.maxDepth < 0 or depth < BezierCurve.maxDepth) then + for t=0,1,interval do + love.graphics.setColor(0.3,0.5,0.1) + local p = {} + for pointI=1, #points-1 do + table.insert(p, (1-t)*points[pointI] + t*points[pointI+1]) + end + for pointI=1, #p-1 do + love.graphics.line(p[pointI].x, p[pointI].y, p[pointI+1].x, p[pointI+1].y) + end + BezierCurve.stringArt(p, depth+1) + end + end +end + +return BezierCurve \ No newline at end of file diff --git a/GUI-Framework.lua b/GUI-Framework.lua new file mode 100644 index 0000000..f255ca9 --- /dev/null +++ b/GUI-Framework.lua @@ -0,0 +1,166 @@ +local GUI = {} +local EMPTY = {} +local Vector2 = require("Vector2") +local templates = {} +GUI.__index = GUI + + +local function clone(data) + if type(data) ~= "table" then + return data + else + + local new = {} + for k, v in pairs(data) do + new[k] = clone(v) + end + return setmetatable(new, getmetatable(data)) + end +end + +function revipairs(t) + return function(t, i) i = i - 1 if i > 0 then return i, t[i] end end, t, #t+1 +end + +function GUI.sortByDepth(a, b) + return a.depth < b.depth +end + +function getPos(self) + if self._parent then + local x0, y0 = self._parent:getPos() + return self.pos.x + x0, self.pos.y + y0 + else + return self.pos.x, self.pos.y + end +end +function getSize(self) + return self.size.x, self.size.y +end +function getBounds(self) + local x, y = getPos(self) + return x, y, self.size.x, self.size.y +end +function inBounds(self, x, y) + local Ex, Ey, width, height = getBounds(self) + return (x > Ex and x < Ex + width and y > Ey and y < Ey + height) +end + +function GUI.createElement(elementType, data) + assert(templates[elementType], "Element type \""..elementType.."\" dosen't exist") + + local elem = data or {} + for k, v in pairs(templates[elementType]) do + if not (type(k) == "string" and k:find("^__")) and type(elem[k]) == "nil" then + elem[k] = clone(v) + end + end + setmetatable(elem, template) + + -- Ensure that the mandatory varaibles are declered + elem.depth = elem.depth or 0 + elem.pos = elem.pos or Vector2() + elem.size = elem.size or Vector2() + if type(elem.visible) ~= "boolean" then + elem.visible = true + end + + if elem.load then elem:load() end + return elem +end + +function GUI.new(class, width, height) + assert(width and height, "Width and height are undefined") + local self = setmetatable({}, class) + self.pos = Vector2() + self.size = Vector2(width, height) + + self.getPos = getPos + self.getSize = getSize + self.getBounds = getBounds + self.inBounds = inBounds + + self._elements = {} + return self +end + +function GUI:removeElement(element) + for i, elem in ipairs(self._elements) do + if elem == element then + table.remove(self._elements, i) + break + end + end +end + +function GUI:addElement(elementType, data) + local elem = GUI.createElement(elementType, data) + elem._parent = self + -- Add it to the list + table.insert(self._elements, elem) + table.sort(self._elements, GUI.sortByDepth) + return elem +end + +function GUI.newTemplate(name) + local template = {} + template.__index = template + + template.getPos = getPos + template.getSize = getSize + template.getBounds = getBounds + template.inBounds = inBounds + + templates[name] = template + return template +end + +function GUI:resize(...) + for _, element in ipairs(self._elements) do + if element.resize then + element:resize(...) + end + end +end + +function GUI.callMethod(parent, name, ...) + local useFallback = false + for _, element in revipairs(parent._elements) do + if element.visible then + if type(element._elements) == "table" then + if GUI.callMethod(element, name, ...) then break end + end + if useFallback and element[name.."Fallback"] then + element[name.."Fallback"](element, ...) + elseif element[name] then + if element[name](element, ...) then useFallback = true end + end + end + end + return useFallback +end + +function GUI:draw(...) + local useFallback = false + for _, element in ipairs(self._elements) do + if element.visible then + if useFallback and element["drawFallback"] then + element["drawFallback"](element, ...) + elseif element["draw"] then + if element["draw"](element, ...) then useFallback = true end + end + if type(element._elements) == "table" then + GUI.draw(element, "draw", ...) + end + end + end + return useFallback +end + +for i, name in pairs{"keypressed", "keyreleased", "mousemoved", "mousepressed", "mousereleased", "textinput"} do + GUI[name] = function(self, ...) + GUI.callMethod(self, name, ...) + end +end + +return GUI \ No newline at end of file diff --git a/GUI/Button.lua b/GUI/Button.lua new file mode 100644 index 0000000..b3c378c --- /dev/null +++ b/GUI/Button.lua @@ -0,0 +1,95 @@ +local GUI = require("GUI-Framework") +local Vector2 = require("Vector2") +local utils = require("utils") +local Button = GUI.newTemplate("Button") + +Button.isHover = false +Button.isDown = false +Button.text = "" +Button.font = love.graphics.getFont() + +function Button:load() + local minWidth = self.font:getWidth(self.text) + local minHeight = self.font:getHeight(self.text) * ((select(2, self.text:gsub('\n', '\n')) or 0) + 1) + + if self.size.x < minWidth then + self.size.x = minWidth + 10 + end + if self.size.y < minHeight then + self.size.y = minHeight + 10 + end +end + +function Button:draw() + if self.isDown then + self:drawDown() + elseif self.isHover then + self:drawHover() + else + self:drawDefault() + end +end + +function Button:drawHover() + local x, y, width, height = self:getBounds() + local cx, cy = x + width/2, y + height/2 + + -- Shadow + utils.smoothRectangle(x, y, width, height, 2, utils.rgba(0, 0, 0, 0.62)) + -- The actual button + utils.smoothRectangle(x, y-1, width, height, 2, utils.rgb(42, 147, 247)) + -- Text + love.graphics.setColor(1, 1, 1) + utils.alignedPrint(self.font, self.text, cx, cy-1) +end + +function Button:drawDefault() + local x, y, width, height = self:getBounds() + local cx, cy = x + width/2, y + height/2 + + -- Shadow + utils.smoothRectangle(x, y, width, height, 2, utils.rgba(0, 0, 0, 0.62)) + -- The actual button + utils.smoothRectangle(x, y-1, width, height, 2, utils.rgb(56, 158, 255)) + -- Text + love.graphics.setColor(1, 1, 1) + utils.alignedPrint(self.font, self.text, cx, cy-1) +end + +function Button:mousemoved(x, y) + if self:inBounds(x, y) then + self.isHover = true + return true + else + self.isHover = false + end +end + +function Button:mousemovedFallback() + self.isHover = false +end + +function Button:mousepressed() + if self.isHover then + self.isDown = true + return true + end +end + +function Button:mousereleased(x, y, ...) + if self.isDown and self:inBounds(x, y) then + if self.mouseClicked then self:mouseClicked(x, y, ...) end + end + self.isDown = false +end + +function Button:drawDown() + local x, y, width, height = self:getBounds() + local cx, cy = x + width/2, y + height/2 + + -- the actual button + utils.smoothRectangle(x, y, width, height, 2, utils.rgb(0, 116, 225)) + -- Text + love.graphics.setColor(1, 1, 1) + utils.alignedPrint(self.font, self.text, cx, cy) +end diff --git a/GUI/Label.lua b/GUI/Label.lua new file mode 100644 index 0000000..2bd5da2 --- /dev/null +++ b/GUI/Label.lua @@ -0,0 +1,14 @@ +local Vector2 = require("Vector2") +local utils = require("utils") +local GUI = require("GUI-Framework") +local Label = GUI.newTemplate("Label") +local textFont = love.graphics.getFont() + +Label.color = utils.rgb(255 , 255, 255) +Label.text = "Lorem ipsum..." + +function Label:draw() + local x, y = self:getPos() + love.graphics.setColor(self.color) + utils.alignedPrint(textFont, self.text, x , y, self.alignX, self.alignY) +end diff --git a/README.md b/README.md new file mode 100644 index 0000000..40b55b1 --- /dev/null +++ b/README.md @@ -0,0 +1,14 @@ +# Bezier curve string art + +![Screenshot](./screenshot.png) + +Visualize bezier curves as string art [String art](https://en.wikipedia.org/wiki/String_art) + +How to launch: +``` +love . +``` + +Controls: +* RCtrl + LMB - create/delete point +* RCtrl + LMB drag - move point diff --git a/Vector2.lua b/Vector2.lua new file mode 100644 index 0000000..44b31dc --- /dev/null +++ b/Vector2.lua @@ -0,0 +1,137 @@ +local ffi = assert(require("ffi"), "Vector2 needs ffi to be enabled") +local Vector2 = {} +setmetatable(Vector2, Vector2) + +ffi.cdef[[ +typedef struct { + double x, y; +} vector2; +]] + +function Vector2.copy(t) + return ffi.new("vector2", t.x, t.y) +end + +function Vector2.__call(t, x, y) + return ffi.new("vector2", x or 0, y or 0) +end + +function Vector2.__concat(v1, v2) + return v1 .. tostring(v2) +end + +function Vector2.__tostring(t) + return string.format("Vector2(%.4f,%.4f)", t.x, t.y) +end + +function Vector2.__eq(v1, v2) + if (not ffi.istype("vector2", v2)) or (not ffi.istype("vector2", v1)) then return false end + return v1.x == v2.x and v1.y == v2.y +end + +function Vector2.__unm(v) + return Vector2(-v.x, -v.y) +end + +function Vector2.__div(v1, op) + if type(op) ~= "number" then error("Vector2 must be divided by scalar") end + return Vector2(v1.x / op, v1.y / op) +end + +function Vector2.__mul(v1, op) + if type(op) == "number" then + return Vector2(v1.x * op, v1.y * op) + elseif type(v1) == "number" then + return Vector2(op.x * v1, op.y * v1) + end + return Vector2(v1.x * op.x, v1.y * op.y) +end + +function Vector2.__sub(v1, v2) + if type(v1) == "number" then + return Vector2(v1 - v2.x, v1 - v2.y) + elseif type(v2) == "number" then + return Vector2(v1.x - v2, v1.y - v2) + end + return Vector2(v1.x - v2.x, v1.y - v2.y) +end + +function Vector2.__add(v1, v2) + if type(v1) == "number" then + return Vector2(v1 + v2.x, v1 + v2.y) + elseif type(v2) == "number" then + return Vector2(v1.x + v2, v1.y + v2) + end + return Vector2(v1.x + v2.x, v1.y + v2.y) +end + +function Vector2.split(v) + return v.x, v.y +end + +function Vector2.setAngle(v, angle) + local magnitude = v.magnitude + return Vector2(math.cos(angle) * magnitude, math.sin(angle) * magnitude) +end + +function Vector2.setMagnitude(t, mag) + return t.normalized * mag +end + +function Vector2.__newindex(t, k, v) + if k == "magnitude" then + local result = t:setMagnitude(v) + t:set(result) + elseif k == "angle" then + local result = t:setAngle(v) + t:set(result) + elseif type(t) == "cdata" then + error("Cannot assign new property '" .. k .. "' to a Vector2") + else + rawset(t, k, v) + end +end + +function Vector2.getAngle(v) + return math.atan2(v.y, v.x) +end + +function Vector2.getNormalized(v) + local magnitude = v.magnitude + if magnitude == 0 then return Vector2(0, 0, 0) end + return Vector2(v.x / magnitude, v.y / magnitude) +end + +function Vector2.getMagnitude(v) + return (v.x^2 + v.y^2)^0.5 +end + +function Vector2.__index(t, k) + if k == "magnitude" then + return Vector2.getMagnitude(t) + elseif k == "normalized" then + return Vector2.getNormalized(t) + elseif k == "angle" then + return Vector2.getAngle(t) + end + return rawget(Vector2, k) +end + +function Vector2.rotate(t, rad) + return Vector2.setAngle(t, t:getAngle() + rad) +end + +function Vector2.set(t, v) + if ffi.istype("vector2", v) then + t.x = v.x + t.y = v.y + end +end + +function Vector2.distance(v1, v2) + return ((v1.x-v2.x)^2 + (v1.y-v2.y)^2)^0.5 +end + +ffi.metatype("vector2", Vector2) + +return Vector2 \ No newline at end of file diff --git a/conf.lua b/conf.lua new file mode 100644 index 0000000..ed8c640 --- /dev/null +++ b/conf.lua @@ -0,0 +1,4 @@ +function love.conf(t) + t.console = true + t.modules.joystick = false +end \ No newline at end of file diff --git a/main.lua b/main.lua new file mode 100644 index 0000000..a01dccc --- /dev/null +++ b/main.lua @@ -0,0 +1,64 @@ +local BezierCurve = require("BezierCurve") +local GUI = require("GUI-Framework") +local Vector2 = require("Vector2") + +function addCurve(points) + local newCurve = BezierCurve:new(points) + table.insert(allCurves, newCurve) +end + +function love.load(args, unfilteredArgs) + love.window.setTitle("Bezier Curves") + gui = GUI:new(love.graphics.getDimensions()) + allCurves = {} + show = {true, true , false} + + for _, file in pairs(love.filesystem.getDirectoryItems("GUI")) do + require("GUI."..file:sub(1, -5)) + end + + addCurve{200,200,300,150} + + gui:addElement("Button", {pos=Vector2(10,10), size = Vector2(100,25), text = "Hide points", mouseClicked = function(self) + show[1] = not show[1] + if show[1] then + self.text = "Hide points" + else + self.text = "Show points" + end + end}) + gui:addElement("Button", {pos=Vector2(10,50), size = Vector2(100,25), text = "Hide lines", mouseClicked = function(self) + show[2] = not show[2] + if show[2] then + self.text = "Hide lines" + else + self.text = "Show lines" + end + end}) + gui:addElement("Button", {pos=Vector2(10,90), size = Vector2(100,25), text = "Show curves", mouseClicked = function(self) + show[3] = not show[3] + if show[3] then + self.text = "Hide curves" + else + self.text = "Show curves" + end + end}) + + -- Make functions + for _, name in ipairs{"mousepressed", "mousereleased", "mousemoved", "load", "resize", "keypressed", "keyreleased", "textinput"} do + love[name] = function(...) + for _, curve in ipairs(allCurves) do + if curve[name] then curve[name](curve, ...) end + if gui[name] then gui[name](gui, ...) end + end + end + end +end + +function love.draw() + love.graphics.clear(0.9,0.9,0.9) + for _, curve in ipairs(allCurves) do + curve:draw(show[1], show[2], show[3]) + end + gui:draw() +end \ No newline at end of file diff --git a/screenshot.png b/screenshot.png new file mode 100644 index 0000000..737000c Binary files /dev/null and b/screenshot.png differ diff --git a/utils.lua b/utils.lua new file mode 100644 index 0000000..4116751 --- /dev/null +++ b/utils.lua @@ -0,0 +1,225 @@ +local utils = {} +local alignment = { + ["top"] = 0, + ["left"] = 0, + ["middle"] = -0.5, + ["center"] = -0.5, + ["bottom"] = -1, + ["right"] = -1 +} +local Vector2 = require("Vector2") +local _tx, _ty = 0, 0 +local _stack = { + sx = {}, + sy = {}, + sw = {}, + sh = {}, + tx = {}, + ty = {} +} + +--[[ +function atan2 (y, x) + return (x > 0 ) and math.atan(y/x) + or (x < 0 and y >= 0) and math.atan(y/x) + math.pi + or (x < 0 and y < 0) and math.atan(y/x) - math.pi + or (x == 0 and y > 0) and math.pi/2 + or (x == 0 and y < 0) and -math.pi/2 + or 0 -- this situration is technically undefined +end +]] + +function utils.lastIndex(t) + local res = -math.huge + for k in pairs(t) do + if k > res then res = k end + end + return res +end + +function utils.translate(dx, dy) + love.graphics.translate(dx, dy) + _tx, _ty = _tx + dx, _ty + dy +end + +function utils.pushRegion(x, y, w, h) + local sx, sy, sw, sh = love.graphics.getScissor() + table.insert(_stack.sx, sx) + table.insert(_stack.sy, sy) + table.insert(_stack.sw, sw) + table.insert(_stack.sh, sh) + table.insert(_stack.tx, _ty) + table.insert(_stack.ty, _tx) + + love.graphics.push() + if x and y then + if w and h then + love.graphics.intersectScissor(_tx + x, _ty + y, math.max(0, w), math.max(0, h)) + end + utils.translate(x, y) + end +end + +function utils.popRegion() + local sx = table.remove(_stack.sx) + local sy = table.remove(_stack.sy) + local sw = table.remove(_stack.sw) + local sh = table.remove(_stack.sh) + + _tx = table.remove(_stack.tx) or 0 + _ty = table.remove(_stack.ty) or 0 + + love.graphics.pop() + if sx and sy and sw and sh then + love.graphics.setScissor(sx, sy, sw, sh) + else + love.graphics.setScissor() + end +end + +function utils.smoothLine (x1, y1, x2, y2, width, color) + love.graphics.setLineStyle("smooth") + love.graphics.setLineWidth(width) + if color then love.graphics.setColor(color) end + love.graphics.line(x1, y1, x2, y2) +end + +function utils.clamp(x, A, B) + if x < A then + return A + elseif x > B then + return B + else + return x + end +end + +local function previous(t, i) + i = i - 1 + if i > 0 then return i, t[i] end +end + +function utils.revipairs(t) + return previous, t, #t+1 +end + +function utils.smoothRectangle(x, y, w, h, r, color) + love.graphics.setLineStyle("smooth") + love.graphics.setLineWidth(1) + if color then love.graphics.setColor(color) end + love.graphics.rectangle("fill", x, y, w, h, r) + love.graphics.rectangle("line", x, y, w, h, r) +end + +function utils.borderRectangle(x,y,w,h,r,lineWidth,color) + love.graphics.setLineStyle("smooth") + love.graphics.setLineWidth(lineWidth) + if color then love.graphics.setColor(color) end + love.graphics.rectangle("line", x, y, w, h, r) +end + +function utils.alignedPrint(font, text, x, y, alignX, alignY) + local line_count = (select(2, text:gsub('\n', '\n')) or 0) + 1 + local width = font:getWidth(text) + local height = font:getHeight()*line_count + + love.graphics.setFont(font) + love.graphics.print(text, utils.round(x + (alignment[alignX] or -0.5) * width), utils.round(y + (alignment[alignY] or -0.5) * height)) +end + +function utils.rgb(r, g, b) + return {r/255, g/255, b/255, 1} +end + +function utils.rgba(r, g, b, a) + return {r/255, g/255, b/255, a} +end + +function utils.round(num, numDecimalPlaces) + local mult = 10^(numDecimalPlaces or 0) + return math.floor(num * mult + 0.5) / mult +end + +function utils.map(x, A, B, C, D) + return (x - A) / (B - A) * (D - C) + C +end + +function utils.sign(x) + if x > 0 then + return 1 + elseif x < 0 then + return -1 + else + return 0 + end +end + +function utils.clone(data) + if type(data) ~= "table" then + return data + else + + local new = {} + for k, v in pairs(data) do + new[k] = utils.clone(v) + end + return setmetatable(new, getmetatable(data)) + end +end + +function utils.serialize(data) + if type(data) == "string" then + return "\""..data.."\"" + elseif type(data) == "table" then + local t = {} + for key,value in pairs(data) do + table.insert(t,"["..utils.serialize(key).."]="..utils.serialize(value)) + end + return "{"..table.concat(t,",").."}" + else + return tostring(data) + end +end + +function utils.unserialize(data) + return load("return "..data, nil, "t", {Vector2 = Vector2})() +end + +function utils.printTable(t,depth) + depth = depth or 0 + for k,v in pairs(t) do + if type(v) == "table" then + print(string.rep(" ", depth)..k, "table:") + utils.printTable(v,depth+1) + else + print(string.rep(" ", depth)..k, v) + end + end +end + +function utils.loadFile(fileName) + if not love.filesystem.getInfo(fileName) then return nil end + local file = love.filesystem.read(fileName) + local data = love.data.decompress("string","zlib", file) + return utils.unserialize(data) +end + +function utils.saveFile(data, fileName) + local rawstring = utils.serialize(data) + local file = io.open(fileName, "wb") + file:write(love.data.compress("string","zlib",rawstring)) +end + +function utils.pairsByKeys(t) + local tableKeys = {} + for key in pairs(t) do table.insert(tableKeys, key) end + table.sort(tableKeys) + local i = 0 + local n = table.getn(tableKeys) + return function () + i = i + 1 + if i <= n then return tableKeys[i], t[tableKeys[i]] end + end +end + +return utils \ No newline at end of file