commit 1894988ac3ba03a9ed884d1e3d8c9672f722c8bf Author: jill Date: Fri Sep 17 22:24:26 2021 +0300 init commit diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..90d4390 --- /dev/null +++ b/LICENSE @@ -0,0 +1,16 @@ +Box of Eases + +Copyright (C) 2021 Jill "oatmealine" Monoids + +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 . diff --git a/README.md b/README.md new file mode 100644 index 0000000..9abb1a9 --- /dev/null +++ b/README.md @@ -0,0 +1,19 @@ +# Box of Eases (working title) + +![logo](./logo.png) + +A simple Love2D application to create, preview and mix eases, meant for NITG modding. + +## How-to Install + +### Manual/Development (currently the only method) + +Download Love2D for your operating system here: https://love2d.org/ + +Then launch the game like so: https://love2d.org/wiki/Getting_Started#Running_Games + +## Screenshots + +![Previewing eases](./screenshot1.png) + +![Mixing eases](./screenshot2.png) \ No newline at end of file diff --git a/conf.lua b/conf.lua new file mode 100644 index 0000000..f9c66a3 --- /dev/null +++ b/conf.lua @@ -0,0 +1,5 @@ +function love.conf(t) + t.window.resizable = true + t.window.title = 'Box of Eases' + t.window.icon = 'logo.png' +end diff --git a/easelib.lua b/easelib.lua new file mode 100644 index 0000000..320ed59 --- /dev/null +++ b/easelib.lua @@ -0,0 +1,132 @@ +local self = {} + +local sqrt = math.sqrt +local sin = math.sin +local asin = math.asin +local cos = math.cos +local pow = math.pow +local exp = math.exp +local pi = math.pi +local abs = math.abs + +self = setmetatable(self, { + __index = function(s, i) + for _,v in ipairs(self) do + if v[1] == i then + return v[2] + end + end + end +}) + +table.insert(self, {'linear', function(t) return t end}) +table.insert(self, {'instant', function() return 1 end}) + +table.insert(self, {'bounce', function(t) return 4 * t * (1 - t) end}) +table.insert(self, {'tri', function(t) return 1 - abs(2 * t - 1) end}) +table.insert(self, {'bell', function(t) return self.inOutQuint(self.tri(t)) end}) +table.insert(self, {'pop', function(t) return 3.5 * (1 - t) * (1 - t) * sqrt(t) end}) +table.insert(self, {'tap', function(t) return 3.5 * t * t * sqrt(1 - t) end}) +table.insert(self, {'pulse', function(t) return t < .5 and self.tap(t * 2) or -self.pop(t * 2 - 1) end}) + +table.insert(self, {'spike', function(t) return exp(-10 * abs(2 * t - 1)) end}) +table.insert(self, {'inverse', function(t) return t * t * (1 - t) * (1 - t) / (0.5 - t) end}) + +table.insert(self, {'inSine', function(x) + return 1 - cos(x * (pi * 0.5)) +end}) + +table.insert(self, {'outSine', function(x) + return sin(x * (pi * 0.5)) +end}) + +table.insert(self, {'inOutSine', function(x) + return 0.5 - 0.5 * cos(x * pi) +end}) + +table.insert(self, {'inQuad', function(t) return t * t end}) +table.insert(self, {'outQuad', function(t) return -t * (t - 2) end}) +table.insert(self, {'inOutQuad', function(t) + t = t * 2 + if t < 1 then + return 0.5 * t ^ 2 + else + return 1 - 0.5 * (2 - t) ^ 2 + end +end}) +table.insert(self, {'inCubic', function(t) return t * t * t end}) +table.insert(self, {'outCubic', function(t) return 1 - (1 - t) ^ 3 end}) +table.insert(self, {'inOutCubic', function(t) + t = t * 2 + if t < 1 then + return 0.5 * t ^ 3 + else + return 1 - 0.5 * (2 - t) ^ 3 + end +end}) +table.insert(self, {'inQuart', function(t) return t * t * t * t end}) +table.insert(self, {'outQuart', function(t) return 1 - (1 - t) ^ 4 end}) +table.insert(self, {'inOutQuart', function(t) + t = t * 2 + if t < 1 then + return 0.5 * t ^ 4 + else + return 1 - 0.5 * (2 - t) ^ 4 + end +end}) +table.insert(self, {'inQuint', function(t) return t ^ 5 end}) +table.insert(self, {'outQuint', function(t) return 1 - (1 - t) ^ 5 end}) +table.insert(self, {'inOutQuint', function(t) + t = t * 2 + if t < 1 then + return 0.5 * t ^ 5 + else + return 1 - 0.5 * (2 - t) ^ 5 + end +end}) +table.insert(self, {'inExpo', function(t) return 1000 ^ (t - 1) - 0.001 end}) +table.insert(self, {'outExpo', function(t) return 1.001 - 1000 ^ -t end}) +table.insert(self, {'inOutExpo', function(t) + t = t * 2 + if t < 1 then + return 0.5 * 1000 ^ (t - 1) - 0.0005 + else + return 1.0005 - 0.5 * 1000 ^ (1 - t) + end +end}) +table.insert(self, {'inCirc', function(t) return 1 - sqrt(1 - t * t) end}) +table.insert(self, {'outCirc', function(t) return sqrt(-t * t + 2 * t) end}) +table.insert(self, {'inOutCirc', function(t) + t = t * 2 + if t < 1 then + return 0.5 - 0.5 * sqrt(1 - t * t) + else + t = t - 2 + return 0.5 + 0.5 * sqrt(1 - t * t) + end +end}) + +table.insert(self, {'outBounce', function(t) + if t < 1 / 2.75 then + return 7.5625 * t * t + elseif t < 2 / 2.75 then + t = t - 1.5 / 2.75 + return 7.5625 * t * t + 0.75 + elseif t < 2.5 / 2.75 then + t = t - 2.25 / 2.75 + return 7.5625 * t * t + 0.9375 + else + t = t - 2.625 / 2.75 + return 7.5625 * t * t + 0.984375 + end +end}) +table.insert(self, {'inBounce', function(t) return 1 - self.outBounce(1 - t) end}) +table.insert(self, {'inOutBounce', function(t) + if t < 0.5 then + return self.inBounce(t * 2) * 0.5 + else + return self.outBounce(t * 2 - 1) * 0.5 + 0.5 + end +end}) + +return self diff --git a/logo.png b/logo.png new file mode 100644 index 0000000..07fda80 Binary files /dev/null and b/logo.png differ diff --git a/main.lua b/main.lua new file mode 100644 index 0000000..14a104e --- /dev/null +++ b/main.lua @@ -0,0 +1,445 @@ +local easelib = require 'easelib' + +-- utils + +local function keys(t) + local k = {} + for n in pairs(t) do table.insert(k, n) end + return k +end +local function skeys(t) + local k = {} + for n,v in pairs(t) do table.insert(k, {n, v}) end + table.sort(k, function(a, b) return a[2].i < b[2].i end) + local k2 = {} + for _,v in ipairs(k) do table.insert(k2, v[1]) end + return k2 +end + +local function mix(x, y, a) + return x * (1 - a) + y * a +end + +-- eases + +local function mixEase(e1, e2, point) + if not point then point = 0.5 end + + return function(a) + if a < point then + return e1(a / point) * point + else + return e2((a - point) / (1 - point)) * (1 - point) + point + end + end +end + +local eases = {} +for i,v in pairs(easelib) do + local min = 0 + + local q = 10 + for i = 0, q do + local s = v[2](i / q) + if s < 0 then min = -1 end + end + + eases[v[1]] = { + f = v[2], + max = 1, + min = min, + i = i + } +end + +local ease +local minEase = false + +-- rendering constants + +local padding = 6 +local margin = 4 +local quality = 256 + +local mixpoint = 0.5 +local oldmixpoint = 0.5 +local mixpointtimer = 0 -- easter egg thing +local mixpointspin = 0 + +local maxDropdown = 16 + +-- graph + +local graph = {} + +-- dropdown bullshit + +local dropdowns = {} + +local dropdownValueCache = {} + +local openDropdown = 0 +local dropdownScroll = 0 +local dropdownScrollE = 0 + +local function selected(index) + return dropdowns[index].options[dropdowns[index].selected] +end + +local function kget(key) + for _, v in ipairs(dropdowns) do + if v.name == key then + return v + end + end +end + +local function kselected(key) + for _, v in ipairs(dropdowns) do + if v.name == key then + return v.options[v.selected] + end + end +end + +local dropdownId +local function insertDropdown(tab, f) + dropdownId = dropdownId + 1 + f.selected = (kget(f.name) or dropdownValueCache[f.name] or {selected = 1}).selected + f.selected = (f.selected - 1) % #f.options + 1 + return table.insert(tab, f) +end +local function createDropdowns() + local d = {} + dropdownId = 0 + insertDropdown(d, { + x = padding, + y = padding, + width = 128, + options = { + 'Preview Ease', + 'Mix Eases', + 'Create Ease' + }, + name = 'mode' + }) + + if d[dropdownId].selected == 1 then -- preview ease + insertDropdown(d, { + x = padding + 128 + padding, + y = padding, + width = 128, + options = skeys(eases), + name = 'ease1' + }) + ease = eases[d[dropdownId].options[d[dropdownId].selected]].f + elseif d[dropdownId].selected == 2 then -- mix eases + insertDropdown(d, { + x = padding + 128 + padding, + y = padding, + width = 128, + options = skeys(eases), + name = 'ease1' + }) + insertDropdown(d, { + x = padding + 128 + padding + 128 + padding, + y = padding, + width = 128, + options = skeys(eases), + name = 'ease2' + }) + ease = mixEase(eases[d[dropdownId - 1].options[d[dropdownId - 1].selected]].f, eases[d[dropdownId].options[d[dropdownId].selected]].f, mixpoint) + elseif d[dropdownId].selected == 3 then -- create eases + insertDropdown(d, { + x = padding + 128 + padding, + y = padding, + width = 128, + options = skeys(eases), + name = 'ease1' + }) + end + + minEase = (kselected('ease1') and eases[kselected('ease1')].min == -1) or (kselected('ease2') and eases[kselected('ease2')].min == -1) + + dropdowns = d +end + +-- rendering + +function love.load() + createDropdowns() +end + +function love.update(dt) + for i = 1, quality do + local a = (i - 1) / (quality - 1) + if not graph[i] then + graph[i] = ease(a) + end + end + for i,v in ipairs(graph) do + local a = (i - 1) / (quality - 1) + local b = ease(a) + if minEase then + b = b / 2 + 0.5 + end + graph[i] = mix(v, b, math.min(dt * 18, 1)) + end + + mixpointtimer = mix(mixpointtimer + math.abs(mixpoint - oldmixpoint), 0, math.min(dt * 8)) + oldmixpoint = mix(oldmixpoint, mixpoint, math.min(dt * 20, 1)) + + if mixpointtimer > 2 then + mixpointtimer = mixpointtimer - 2 + mixpointspin = mixpointspin + 4 + end + + mixpointspin = mix(mixpointspin, 0, dt * 3) + + if openDropdown ~= 0 then + dropdownScroll = math.max(dropdownScroll, -(#dropdowns[openDropdown].options - maxDropdown)) + dropdownScroll = math.min(dropdownScroll, 0) + dropdownScrollE = mix(dropdownScrollE, dropdownScroll, dt * 10) + end +end + +function love.draw() + local mode = kget('mode').selected + local sw, sh = love.graphics.getDimensions() + local mx, my = love.mouse.getPosition() + + love.graphics.setColor(0.09, 0.09, 0.12, 1) + love.graphics.rectangle('fill', 0, 0, sw, sh) + love.graphics.setColor(0.08, 0.08, 0.1, 1) + love.graphics.rectangle('line', 0, 0, sw, sh) + + love.graphics.setColor(1, 1, 1, 1) + love.graphics.print('Box of Eases by oatmealine', padding, sh - love.graphics.getFont():getHeight() - padding) + + -- sliders + -- yeah we do a lil' hardcoding + + if mode == 2 then + local x, y, w, h = padding, padding * 2 + love.graphics.getFont():getHeight() + margin, 128, 32 + + love.graphics.setColor(0.7, 0.7, 0.7, 0.4) + love.graphics.line(x, y + h/2, x + w, y + h/2) + + local sx, sy = x + w * oldmixpoint, y + h/2 + local ssize = h * 0.5 + + love.graphics.push() + + love.graphics.translate(sx, sy) + love.graphics.rotate((mixpoint - oldmixpoint) * 4 + mixpointspin * math.pi * 2) + + love.graphics.setColor(0, 0, 0, 1) + if mx > sx - ssize/2 and mx < sx + ssize/2 and my > sy - ssize/2 and my < sy + ssize/2 and openDropdown == 0 then + love.graphics.setColor(0.2, 0.2, 0.3, 1) + end + love.graphics.rectangle('fill', -ssize/2, -ssize/2, ssize, ssize) + love.graphics.setColor(1, 1, 1, 1) + love.graphics.rectangle('line', -ssize/2, -ssize/2, ssize, ssize) + + love.graphics.rotate((mixpoint - oldmixpoint) * -2) + + love.graphics.setColor(1, 1, 1, 1) + love.graphics.printf(math.floor(mixpoint * 100)/100, -ssize * 6, ssize - 2, ssize * 12, 'center') + + love.graphics.pop() + + if mx > x and mx < x + w and my > y and my < y + h and love.mouse.isDown(1) and openDropdown == 0 then + mixpoint = (mx - x) / w + createDropdowns() + end + end + + -- dropdowns + for i,v in ipairs(dropdowns) do + local x, y, w, h = v.x, v.y, v.width, love.graphics.getFont():getHeight() + margin + + love.graphics.setColor(0, 0, 0, 0.3) + if love.mouse.getX() > x and love.mouse.getX() < x + w and love.mouse.getY() > y and love.mouse.getY() < y + h then + love.graphics.setColor(0.8, 0.8, 1, love.mouse.isDown(1) and 0.4 or 0.3) + end + love.graphics.rectangle('fill', x, y, w, h) + + love.graphics.setColor(1, 1, 1, 1) + + love.graphics.rectangle('line', x, y, w, h) + love.graphics.print(selected(i), x + margin/2, y + margin/2) + love.graphics.rectangle('line', x + w - h, y, h, h) + love.graphics.polygon('line', x + w - h/2 + 0.3 * h, y + h/2 - 0.3 * h, x + w - h/2 - 0.3 * h, y + h/2 - 0.3 * h, x + w - h/2, y + h/2 + 0.3 * h) + + if openDropdown == i then + for i,o in ipairs(v.options) do + local x, y, w, h = x, y + i * h, w, h + + y = y + dropdownScrollE * h + local gi = y / h + if gi > maxDropdown or gi < 1 then + goto continue + end + + local a = 1 - math.min(math.max((1 - (maxDropdown - gi)) * (1 - (math.abs(dropdownScrollE) /(#v.options - maxDropdown))), 0), 1) + + love.graphics.setColor(0, 0, 0, 0.3 * a) + if mx > x and mx < x + w and my > y and my < y + h then + love.graphics.setColor(0.8, 0.8, 1, (love.mouse.isDown(1) and 0.4 or 0.3) * a) + end + love.graphics.rectangle('fill', x, y, w, h) + + love.graphics.setColor(1, 1, 1, 0.75 * a) + love.graphics.rectangle('line', x, y, w, h) + + love.graphics.setColor(1, 1, 1, 1 * a) + love.graphics.print(v.options[i], x + 2, y + 2) + + ::continue:: + end + + -- scrollwheel + + if #v.options > maxDropdown then + local displayed = maxDropdown / #v.options + local scroll = math.abs(dropdownScrollE) / (#v.options - maxDropdown) + local size = margin + + love.graphics.setColor(1, 1, 1, 0.9) + love.graphics.rectangle('fill', x + w - size, y + h + scroll * (1 - displayed) * (maxDropdown - 1) * h, size, displayed * (maxDropdown - 1) * h) + end + end + end + + + -- graph + + if mode == 1 or mode == 2 then + local csize = 10 -- preview point size + local size = math.min((sw - padding) - ((kget('ease2') or kget('ease1')).x + 128 + padding), sh - padding * 5 - csize) + + local x, y, w, h = sw - padding - size, padding, size, size + love.graphics.setColor(1, 1, 1, 1) + love.graphics.rectangle('line', x, y, w, h) + + -- grid + love.graphics.setColor(0.2, 0.2, 0.4, 0.2) + local gridsize = 64 + for gx = 1, gridsize - 2 do + love.graphics.line(x + margin + gx * w/gridsize, y + margin, x + margin + gx * w/gridsize, y + h - margin) + end + for gy = 1, gridsize - 2 do + love.graphics.line(x + margin, y + margin + gy * h/gridsize, x + w - margin, y + margin + gy * h/gridsize) + end + + -- mixease point + if mode == 2 then + love.graphics.setColor(1, 1, 1, 0.8) + love.graphics.line(x + margin + mixpoint * w, y, x + margin + mixpoint * w, y + h) + end + + -- preview point + local t = love.timer.getTime() % 1 + love.graphics.setColor(0.4, 0.4, 1, 0.4) + love.graphics.line(x + margin + t * w, y, x + margin + t * w, y + h) + + -- y = 0 point + -- todo: this will break with eases that dont have the first point at y0 + local py = graph[1] + love.graphics.setColor(0.7, 0.7, 0.7, 0.4 * (1 - math.abs(py - 0.5) / 0.5)) + love.graphics.line(x, y + h - py * h, x + w, y + py * h) + + -- polygone + -- this isnt done with a polygon because else itd waste a Bunch of ram and i kinda, dont want to do that? + love.graphics.setColor(1, 1, 1, 1) + local last = graph[1] or 0 + for gx = 1, quality - 1 do + local a = gx/quality + local b = graph[gx + 1] or 0 + local px, py = x + margin + gx * ((w - margin)/quality), y + h - margin - b * (h - margin * 2) + local ox, oy = x + margin + (gx - 1) * ((w - margin)/quality), y + h - margin - last * (h - margin * 2) + if math.abs(b - last) < 1 then + love.graphics.line(ox, oy, px, py) + end + last = b + end + + -- preview + love.graphics.setColor(1, 1, 1, 0.2) + love.graphics.line(x + margin, y + h + padding * 2 + csize/2, x + w - margin, y + h + padding * 2 + csize/2) + + love.graphics.setColor(0.4, 0.4, 1, 1) + local a1 = ease(t) + local a2 = ease(math.max(math.min(t - 0.1, 1), 0)) + local da = a1 + if love.timer.getTime() % 2 < 1 and math.floor(ease(0) + 0.5) ~= math.floor(ease(1) + 0.5) then + da = 1 - da + end + if minEase then + da = da / 2 + 0.5 + end + + love.graphics.ellipse('fill', x + margin + (w - margin * 2) * da, y + h + padding * 2 + csize/2, csize * (1 + math.min(math.abs(a1 - a2), 3) * 1.2), csize) + end +end + +function love.mousepressed(x, y, m) + local clickedDropdown = false + for i,v in ipairs(dropdowns) do + local h = love.graphics.getFont():getHeight() + margin + if openDropdown == 0 then + if x > v.x and x < v.x + v.width and y > v.y and y < v.y + h + margin then + if m == 1 then + openDropdown = i + clickedDropdown = true + dropdownScroll = 0 + dropdownScrollE = 0 + elseif m == 3 then + dropdowns[i].selected = math.random(1, #dropdowns[i].options) + createDropdowns() + end + end + end + if openDropdown == i then + if x > v.x and x < v.x + v.width and y > v.y + h and y < v.y + h * (math.min(#v.options, maxDropdown) + 1) and m == 1 then + clickedDropdown = true + end + end + end + + if not clickedDropdown and m == 1 then + openDropdown = 0 + end +end + +function love.mousereleased(x, y, m) + for i,v in ipairs(dropdowns) do + local h = love.graphics.getFont():getHeight() + margin + if openDropdown == i then + if x > v.x and x < v.x + v.width and y > v.y + h and y < v.y + h * (math.min(#v.options, maxDropdown) + 1) and m == 1 then + v.selected = math.floor((y - v.y) / h - dropdownScrollE) + openDropdown = 0 + dropdownValueCache[v.name] = {selected = v.selected} + createDropdowns() + end + end + end +end + +function love.wheelmoved(x, y) + if y == 0 then return end + + if openDropdown ~= 0 then + dropdownScroll = dropdownScroll + y + else + local mx, my = love.mouse.getPosition() + for i,v in ipairs(dropdowns) do + local h = love.graphics.getFont():getHeight() + margin + if mx > v.x and mx < v.x + v.width and my > v.y and my < v.y + h + margin then + dropdowns[i].selected = dropdowns[i].selected - math.floor(y) + dropdowns[i].selected = (dropdowns[i].selected - 1) % #dropdowns[i].options + 1 + createDropdowns() + end + end + end +end diff --git a/screenshot1.png b/screenshot1.png new file mode 100644 index 0000000..5a3faac Binary files /dev/null and b/screenshot1.png differ diff --git a/screenshot2.png b/screenshot2.png new file mode 100644 index 0000000..0d49b4f Binary files /dev/null and b/screenshot2.png differ