initial commit

This commit is contained in:
Rokas Puzonas 2023-05-11 21:28:37 +03:00
commit b03af23da5
10 changed files with 818 additions and 0 deletions

99
BezierCurve.lua Normal file
View File

@ -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

166
GUI-Framework.lua Normal file
View File

@ -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

95
GUI/Button.lua Normal file
View File

@ -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

14
GUI/Label.lua Normal file
View File

@ -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

14
README.md Normal file
View File

@ -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

137
Vector2.lua Normal file
View File

@ -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

4
conf.lua Normal file
View File

@ -0,0 +1,4 @@
function love.conf(t)
t.console = true
t.modules.joystick = false
end

64
main.lua Normal file
View File

@ -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

BIN
screenshot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

225
utils.lua Normal file
View File

@ -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