first commit

This commit is contained in:
2021-12-18 20:10:30 +01:00
parent 4d04f63b99
commit 215bed9faf
108 changed files with 3630 additions and 0 deletions

100
lib/class.lua Normal file
View File

@@ -0,0 +1,100 @@
-- require('mobdebug').start()
--[[
Copyright (c) 2010-2013 Matthias Richter
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
Except as contained in this notice, the name(s) of the above copyright holders
shall not be used in advertising or otherwise to promote the sale, use or
other dealings in this Software without prior written authorization.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
]]--
local function include_helper(to, from, seen)
if from == nil then
return to
elseif type(from) ~= 'table' then
return from
elseif seen[from] then
return seen[from]
end
seen[from] = to
for k,v in pairs(from) do
k = include_helper({}, k, seen) -- keys might also be tables
if to[k] == nil then
to[k] = include_helper({}, v, seen)
end
end
return to
end
-- deeply copies `other' into `class'. keys in `other' that are already
-- defined in `class' are omitted
local function include(class, other)
return include_helper(class, other, {})
end
-- returns a deep copy of `other'
local function clone(other)
return setmetatable(include({}, other), getmetatable(other))
end
local function new(class)
-- mixins
class = class or {} -- class can be nil
local inc = class.__includes or {}
if getmetatable(inc) then inc = {inc} end
for _, other in ipairs(inc) do
if type(other) == "string" then
other = _G[other]
end
include(class, other)
end
-- class implementation
class.__index = class
class.init = class.init or class[1] or function() end
class.include = class.include or include
class.clone = class.clone or clone
-- constructor call
return setmetatable(class, {__call = function(c, ...)
local o = setmetatable({}, c)
o:init(...)
return o
end})
end
-- interface for cross class-system compatibility (see https://github.com/bartbes/Class-Commons).
if class_commons ~= false and not common then
common = {}
function common.class(name, prototype, parent)
return new{__includes = {prototype, parent}}
end
function common.instance(class, ...)
return class(...)
end
end
-- the module
return setmetatable({new = new, include = include, clone = clone},
{__call = function(_,...) return new(...) end})

15
lib/knife/base.lua Normal file
View File

@@ -0,0 +1,15 @@
return {
extend = function (self, subtype)
subtype = subtype or {}
local meta = { __index = subtype }
return setmetatable(subtype, {
__index = self,
__call = function (self, ...)
local instance = setmetatable({}, meta)
return instance, instance:constructor(...)
end
})
end,
constructor = function () end,
}

78
lib/knife/behavior.lua Normal file
View File

@@ -0,0 +1,78 @@
-- behavior.lua -- a state manager
-- internal/external api
local function getCurrentFrame (behavior)
return behavior.states[behavior.state][behavior.index]
end
local function advanceFrame (behavior)
local nextState = behavior.frame.after
local nextIndex = behavior.index + 1
local maxIndex = #behavior.states[behavior.state]
if nextState then
behavior.state = nextState
nextIndex = 1
elseif nextIndex > maxIndex then
nextIndex = 1
end
behavior.index = nextIndex
behavior.frame = behavior:getCurrentFrame()
end
local function performAction (behavior)
local act = behavior.frame.action
if act then
act(behavior, behavior.subject)
end
end
-- external api
local function update (behavior, dt)
behavior.elapsed = behavior.elapsed + dt
while behavior.elapsed >= behavior.frame.duration do
behavior.elapsed = behavior.elapsed - behavior.frame.duration
behavior:advanceFrame()
behavior:performAction()
end
return behavior
end
local function setState (behavior, state, index)
behavior.state = state
behavior.index = index or 1
behavior.elapsed = 0
behavior.frame = behavior:getCurrentFrame()
behavior:performAction()
return behavior
end
-- behavior factory
return function (states, subject)
local behavior = {
states = states,
subject = subject,
elapsed = 0,
state = 'default',
index = 1,
-- internal api
getCurrentFrame = getCurrentFrame,
advanceFrame = advanceFrame,
performAction = performAction,
-- external api
update = update,
setState = setState
}
behavior.frame = behavior:getCurrentFrame()
behavior:performAction()
return behavior
end

27
lib/knife/bind.lua Normal file
View File

@@ -0,0 +1,27 @@
local loadstring = _G.loadstring or _G.load
local tconcat = table.concat
local helperCache = {}
local function buildHelper (argCount)
if helperCache[argCount] then
return helperCache[argCount]
end
local argList1 = { 'f' }
local argList2 = {}
for index = 1, argCount do
argList1[index + 1] = 'a' .. index
argList2[index] = 'a' .. index
end
argList2[argCount + 1] = '...'
local source = 'return function(' .. tconcat(argList1, ', ') ..
') return function(...) return f(' .. tconcat(argList2, ', ') ..
') end end'
local helper = loadstring(source)()
helperCache[argCount] = helper
return helper
end
return function (func, ...)
return buildHelper(select('#', ...))(func, ...)
end

31
lib/knife/chain.lua Normal file
View File

@@ -0,0 +1,31 @@
local function Invoker (links, index)
return function (...)
local link = links[index]
if not link then
return
end
local continue = Invoker(links, index + 1)
local returned = link(continue, ...)
if returned then
returned(function (_, ...) continue(...) end)
end
end
end
return function (...)
local links = { ... }
local function chain (...)
if not (...) then
return Invoker(links, 1)(select(2, ...))
end
local offset = #links
for index = 1, select('#', ...) do
links[offset + index] = select(index, ...)
end
return chain
end
return chain
end

54
lib/knife/convoke.lua Normal file
View File

@@ -0,0 +1,54 @@
return function (routine)
local routines = { routine }
local routineIndex = 1
local isFinished = false
local function execute ()
local continueCount = 0
local run
local function continue ()
continueCount = continueCount + 1
return function (...)
continueCount = continueCount - 1
if continueCount == 0 then
return run(...)
end
end
end
local function wait (...)
return coroutine.yield(...)
end
local r = coroutine.create(function ()
isFinished = false
while routineIndex <= #routines do
routines[routineIndex](continue, wait)
continueCount = 0
routineIndex = routineIndex + 1
end
isFinished = true
end)
run = function (...)
return coroutine.resume(r, ...)
end
run()
end
local function appendOrExecute (routine)
if routine then
routines[#routines + 1] = routine
if isFinished then
execute()
end
return appendOrExecute
else
execute()
end
end
return appendOrExecute
end

111
lib/knife/event.lua Normal file
View File

@@ -0,0 +1,111 @@
-- require('mobdebug').start()
-- Event module
local Event = {}
-- Event handler registry
Event.handlers = {}
-- Remove an event handler from the registry
local function remove (self)
if not self.isRegistered then
return self
end
if self.prevHandler then
self.prevHandler.nextHandler = self.nextHandler
end
if self.nextHandler then
self.nextHandler.prevHandler = self.prevHandler
end
if Event.handlers[self.name] == self then
Event.handlers[self.name] = self.nextHandler
end
self.prevHandler = nil
self.nextHandler = nil
self.isRegistered = false
return self
end
-- Insert an event handler into the registry
local function register (self)
if self.isRegistered then
return self
end
self.nextHandler = Event.handlers[self.name]
if self.nextHandler then
self.nextHandler.prevHandler = self
end
Event.handlers[self.name] = self
self.isRegistered = true
return self
end
-- Create an event handler
local function Handler (name, callback)
return {
name = name,
callback = callback,
isRegistered = false,
remove = remove,
register = register
}
end
-- Create and register a new event handler
function Event.on (name, callback)
return register(Handler(name, callback))
end
-- Dispatch an event
function Event.dispatch (name, ...)
local handler = Event.handlers[name]
while handler do
if handler.callback(...) == false then
return handler
end
handler = handler.nextHandler
end
end
local function isCallable (value)
return type(value) == 'function' or
getmetatable(value) and getmetatable(value).__call
end
-- Inject a dispatcher into a table.
local function hookDispatcher (t, key)
local original = t[key]
if isCallable(original) then
t[key] = function (...)
original(...)
return Event.dispatch(key, ...)
end
else
t[key] = function (...)
return Event.dispatch(key, ...)
end
end
end
-- Inject dispatchers into a table. Examples:
-- Event.hook(love.handlers)
-- Event.hook(love, { 'load', 'update', 'draw' })
function Event.hook (t, keys)
if keys then
for _, key in ipairs(keys) do
hookDispatcher(t, key)
end
else
for key in pairs(t) do
hookDispatcher(t, key)
end
end
end
return Event

2
lib/knife/gun.lua Normal file
View File

@@ -0,0 +1,2 @@
-- GUN (Give Up Now)
os.exit()

77
lib/knife/memoize.lua Normal file
View File

@@ -0,0 +1,77 @@
local loadstring = _G.loadstring or _G.load
local weakKeys = { __mode = 'k' }
local cache = setmetatable({}, weakKeys)
local resultsKey = {}
local nilKey = {}
local function getMetaCall (callable)
local meta = getmetatable(callable)
return meta and meta.__call
end
local tupleConstructorCache = {}
local function buildTupleConstructor (n)
if tupleConstructorCache[n] then
return tupleConstructorCache[n]
end
local t = {}
for i = 1, n do
t[i] = "a" .. i
end
local args = table.concat(t, ',')
local ctor = loadstring('return function(' .. args ..
') return function() return ' .. args .. ' end end')()
tupleConstructorCache[n] = ctor
return ctor
end
local function tuple (...)
return buildTupleConstructor(select('#', ...))(...)
end
return function (callable)
local metaCall = getMetaCall(callable)
if type(callable) ~= 'function' and not metaCall then
error 'Attempted to memoize a non-callable value.'
end
cache[callable] = setmetatable({}, weakKeys)
local function run (...)
local node = cache[callable]
local argc = select('#', ...)
for i = 1, argc do
local key = select(i, ...)
if key == nil then
key = nilKey
end
if not node[key] then
node[key] = setmetatable({}, weakKeys)
end
node = node[key]
end
if not node[resultsKey] then
node[resultsKey] = tuple(callable(...))
end
return node[resultsKey]()
end
if metaCall then
return function (...)
local call = getMetaCall(callable)
if call ~= metaCall then
cache[callable] = setmetatable({}, weakKeys)
metaCall = call
end
return run(...)
end, cache, resultsKey, nilKey
end
return run, cache, resultsKey, nilKey
end

78
lib/knife/serialize.lua Normal file
View File

@@ -0,0 +1,78 @@
local tinsert, tconcat, infinity = table.insert, table.concat, math.huge
return function (value)
local intro, outro, ready, known = {}, {}, {}, {}
local knownCount = 0
local writer = {}
-- get writer delegate for this value's type
local function getWriter (value)
return writer[type(value)]
end
-- check if a value has a representation yet
local function isReady (value)
return type(value) ~= 'table' or ready[value]
end
-- serialize tables
function writer.table (value)
if known[value] then
return known[value]
end
knownCount = knownCount + 1
local variable = ('v%i'):format(knownCount)
known[value] = variable
local parts = {}
for k, v in pairs(value) do
local writeKey, writeValue = getWriter(k), getWriter(v)
if writeKey and writeValue then
local key, value = writeKey(k), writeValue(v)
if isReady(k) and isReady(v) then
tinsert(parts, ('[%s]=%s'):format(key, value))
else
tinsert(outro, ('%s[%s]=%s'):format(variable, key, value))
end
end
end
local fields = tconcat(parts, ',')
tinsert(intro, ('local %s={%s}'):format(variable, fields))
ready[value] = true
return variable
end
-- preserve sign bit on NaN, since Lua prints "nan" or "-nan"
local function writeNan (n)
return tostring(n) == tostring(0/0) and '0/0' or '-(0/0)'
end
-- serialize numbers
function writer.number (value)
return value == infinity and '1/0'
or value == -infinity and '-1/0'
or value ~= value and writeNan(value)
or ('%.17G'):format(value)
end
-- serialize strings
function writer.string (value)
return ('%q'):format(value)
end
-- serialize booleans
writer.boolean = tostring
-- concatenate array, joined by and terminated with line break
local function lines (t)
return #t == 0 and '' or tconcat(t, '\n') .. '\n'
end
-- generate serialized result
local write = getWriter(value)
local result = write and write(value) or 'nil'
return lines(intro) .. lines(outro) .. 'return ' .. result
end

69
lib/knife/system.lua Normal file
View File

@@ -0,0 +1,69 @@
local loadstring = _G.loadstring or _G.load
local tconcat = table.concat
local type = type
local function hasSigil (sigil, value)
return type(value) == 'string' and sigil:byte() == value:byte()
end
return function (aspects, process)
local args = {}
local cond = {}
local results = {}
local localIndex = 0
local choicePattern = '([^|]+)'
local function suppress (aspect, condition)
cond[#cond + 1] = 'if nil'
for option in aspect:gmatch(choicePattern) do
cond[#cond + 1] = condition:format(option)
end
cond[#cond + 1] = 'then return end'
end
local function supply (aspect, isOptional, isReturned)
localIndex = localIndex + 1
cond[#cond + 1] = ('local l%d = nil'):format(localIndex)
for option in aspect:gmatch(choicePattern) do
cond[#cond + 1] = ('or _entity[%q]'):format(option)
end
if not isOptional then
cond[#cond + 1] = ('if not l%d then return end'):format(localIndex)
end
if isReturned then
results[#results + 1] = ('_entity[%q]'):format(aspect)
end
args[#args + 1] = ('l%d'):format(localIndex)
end
for index = 1, #aspects do
local aspect = aspects[index]
if hasSigil('_', aspect) then
args[#args + 1] = aspect
elseif hasSigil('!', aspect) or hasSigil('~', aspect) then
suppress(aspect:sub(2), 'or _entity[%q]')
elseif hasSigil('-', aspect) then
suppress(aspect:sub(2), 'or not _entity[%q]')
elseif hasSigil('?', aspect) then
supply(aspect:sub(2), true)
elseif hasSigil('=', aspect) then
supply(aspect:sub(2), false, true)
else
supply(aspect, false)
end
end
local source = ([[
local _aspects, _process = ...
return function (_entity, ...)
%s
%s _process(%s ...)
return true
end]]):format(
tconcat(cond, ' '),
results[1] and (tconcat(results, ',') .. ' = ') or '',
args[1] and (tconcat(args, ', ') .. ', ') or '')
return loadstring(source)(aspects, process)
end

128
lib/knife/test.lua Normal file
View File

@@ -0,0 +1,128 @@
local test, testAssert, testError
-- Create a node representing a test section
local function createNode (parent, description, process)
return setmetatable({
parent = parent,
description = description,
process = process,
nodes = {},
activeNodeIndex = 1,
currentNodeIndex = 0,
assert = testAssert,
error = testError,
}, { __call = test })
end
-- Run a node
local function runNode (node)
node.currentNodeIndex = 0
return node:process()
end
-- Get the root node for a given node
local function getRootNode (node)
local parent = node.parent
return parent and getRootNode(parent) or node
end
-- Update the active child node of the given node
local function updateActiveNode (node, description, process)
local activeNodeIndex = node.activeNodeIndex
local nodes = node.nodes
local activeNode = nodes[activeNodeIndex]
if not activeNode then
activeNode = createNode(node, description, process)
nodes[activeNodeIndex] = activeNode
else
activeNode.process = process
end
getRootNode(node).lastActiveLeaf = activeNode
return activeNode
end
-- Run the active child node of the given node
local function runActiveNode (node, description, process)
local activeNode = updateActiveNode(node, description, process)
return runNode(activeNode)
end
-- Get ancestors of a node, including the node
local function getAncestors (node)
local ancestors = { node }
for ancestor in function () return node.parent end do
ancestors[#ancestors + 1] = ancestor
node = ancestor
end
return ancestors
end
-- Print a message describing one execution path in the test scenario
local function printScenario (node)
local ancestors = getAncestors(node)
for i = #ancestors, 1, -1 do
io.stderr:write(ancestors[i].description or '')
io.stderr:write('\n')
end
end
-- Print a message and stop the test scenario when an assertion fails
local function failAssert (node, description, message)
io.stderr:write(message or '')
io.stderr:write('\n\n')
printScenario(node)
io.stderr:write(description or '')
io.stderr:write('\n\n')
error(message or '', 2)
end
-- Create a branch node for a test scenario
test = function (node, description, process)
node.currentNodeIndex = node.currentNodeIndex + 1
if node.currentNodeIndex == node.activeNodeIndex then
return runActiveNode(node, description, process)
end
end
-- Test an assertion
testAssert = function (self, value, description)
if not value then
return failAssert(self, description, 'Test failed: assertion failed')
end
return value
end
-- Expect function f to fail
testError = function (self, f, description)
if pcall(f) then
return failAssert(self, description, 'Test failed: expected error')
end
end
-- Create the root node for a test scenario
local function T (description, process)
local root = createNode(nil, description, process)
runNode(root)
while root.activeNodeIndex <= #root.nodes do
local lastActiveBranch = root.lastActiveLeaf.parent
lastActiveBranch.activeNodeIndex = lastActiveBranch.activeNodeIndex + 1
runNode(root)
end
return root
end
-- Run any other files passed from CLI.
if arg and arg[0] and arg[0]:gmatch('test.lua') then
_G.T = T
for i = 1, #arg do
dofile(arg[i])
end
_G.T = nil
end
return T

246
lib/knife/timer.lua Normal file
View File

@@ -0,0 +1,246 @@
-- require('mobdebug').start()
local Timer = {}
-- group management
local function detach (group, item)
local index = item.index
group[index] = group[#group]
group[index].index = index
group[#group] = nil
item.groupField = nil
end
local function attach (group, item)
if item.groupField then
detach (item.groupField, item)
end
local index = #group + 1
item.index = index
group[index] = item
item.groupField = group
item.lastGroup = group
end
-- instance update methods
local function updateContinuous (self, dt)
local cutoff = self.cutoff
local elapsed = self.elapsed + dt
if self:callback(dt) == false or elapsed >= cutoff then
if self.finishField then
self:finishField(elapsed - cutoff)
end
self:remove()
end
self.elapsed = elapsed
return
end
local function updateIntermittent (self, dt)
local duration = self.delay or self.interval
local elapsed = self.elapsed + dt
while elapsed >= duration do
elapsed = elapsed - duration
if self.limitField then
self.limitField = self.limitField - 1
end
if self:callback(elapsed) == false
or self.delay or self.limitField == 0 then
if self.finishField then
self:finishField(elapsed)
end
self:remove()
return
end
end
self.elapsed = elapsed
end
local function updateTween (self, dt)
local elapsed = self.elapsed + dt
local plan = self.plan
local duration = self.duration
self.elapsed = elapsed
if elapsed >= duration then
for index = 1, #plan do
local task = plan[index]
task.target[task.key] = task.final
end
if self.finishField then
self:finishField(elapsed - duration)
end
self:remove()
return
end
local ease = self.easeField
for index = 1, #plan do
local task = plan[index]
local target, key = task.target, task.key
local initial, change = task.initial, task.change
target[key] = ease(elapsed, initial, change, duration)
end
end
-- shared instance methods
local defaultGroup = {}
local function group (self, group)
if not group then
group = defaultGroup
end
attach(group, self)
return self
end
local function remove (self)
if self.groupField then
detach(self.groupField, self)
end
return self
end
local function register (self)
attach(self.lastGroup, self)
return self
end
local function limit (self, limitField)
self.limitField = limitField
return self
end
local function finish (self, finishField)
self.finishField = finishField
return self
end
local function ease (self, easeField)
self.easeField = easeField
return self
end
-- tweening helper functions
local function planTween (definition)
local plan = {}
for target, values in pairs(definition) do
for key, final in pairs(values) do
local initial = target[key]
plan[#plan + 1] = {
target = target,
key = key,
initial = initial,
final = final,
change = final - initial,
}
end
end
return plan
end
local function easeLinear (elapsed, initial, change, duration)
return change * elapsed / duration + initial
end
-- instance initializer
local function initialize (timer)
timer.elapsed = 0
timer.group = group
timer.remove = remove
timer.register = register
attach(defaultGroup, timer)
return timer
end
-- static api
function Timer.after (delay, callback)
return initialize {
delay = delay,
callback = callback,
update = updateIntermittent,
}
end
function Timer.every (interval, callback)
return initialize {
interval = interval,
callback = callback,
update = updateIntermittent,
limit = limit,
finish = finish,
}
end
function Timer.prior (cutoff, callback)
return initialize {
cutoff = cutoff,
callback = callback,
update = updateContinuous,
finish = finish,
}
end
function Timer.tween (duration, definition)
return initialize {
duration = duration,
plan = planTween(definition),
update = updateTween,
easeField = easeLinear,
ease = ease,
finish = finish,
}
end
function Timer.update (dt, group)
if not group then
group = defaultGroup
end
for index = #group, 1, -1 do
group[index]:update(dt)
end
end
function Timer.clear (group)
if not group then
group = defaultGroup
end
for i = 1, #group do
group[i] = nil
end
end
Timer.defaultGroup = defaultGroup
return Timer

281
lib/push.lua Normal file
View File

@@ -0,0 +1,281 @@
-- push.lua v0.4
-- Copyright (c) 2020 Ulysse Ramage
-- Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
-- The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
local love11 = love.getVersion() == 11
local getDPI = love11 and love.window.getDPIScale or love.window.getPixelScale
local windowUpdateMode = love11 and love.window.updateMode or function(width, height, settings)
local _, _, flags = love.window.getMode()
for k, v in pairs(settings) do flags[k] = v end
love.window.setMode(width, height, flags)
end
local push = {
defaults = {
fullscreen = false,
resizable = false,
pixelperfect = false,
highdpi = true,
canvas = true,
stencil = true
}
}
setmetatable(push, push)
function push:applySettings(settings)
for k, v in pairs(settings) do
self["_" .. k] = v
end
end
function push:resetSettings() return self:applySettings(self.defaults) end
function push:setupScreen(WWIDTH, WHEIGHT, RWIDTH, RHEIGHT, settings)
settings = settings or {}
self._WWIDTH, self._WHEIGHT = WWIDTH, WHEIGHT
self._RWIDTH, self._RHEIGHT = RWIDTH, RHEIGHT
self:applySettings(self.defaults) --set defaults first
self:applySettings(settings) --then fill with custom settings
windowUpdateMode(self._RWIDTH, self._RHEIGHT, {
fullscreen = self._fullscreen,
resizable = self._resizable,
highdpi = self._highdpi
})
self:initValues()
if self._canvas then
self:setupCanvas({ "default" }) --setup canvas
end
self._borderColor = {0, 0, 0}
self._drawFunctions = {
["start"] = self.start,
["end"] = self.finish
}
return self
end
function push:setupCanvas(canvases)
table.insert(canvases, { name = "_render", private = true }) --final render
self._canvas = true
self.canvases = {}
for i = 1, #canvases do
push:addCanvas(canvases[i])
end
return self
end
function push:addCanvas(params)
table.insert(self.canvases, {
name = params.name,
private = params.private,
shader = params.shader,
canvas = love.graphics.newCanvas(self._WWIDTH, self._WHEIGHT),
stencil = params.stencil or self._stencil
})
end
function push:setCanvas(name)
if not self._canvas then return true end
local canvasTable = self:getCanvasTable(name)
return love.graphics.setCanvas({ canvasTable.canvas, stencil = canvasTable.stencil })
end
function push:getCanvasTable(name)
for i = 1, #self.canvases do
if self.canvases[i].name == name then
return self.canvases[i]
end
end
end
function push:setShader(name, shader)
if not shader then
self:getCanvasTable("_render").shader = name
else
self:getCanvasTable(name).shader = shader
end
end
function push:initValues()
self._PSCALE = (not love11 and self._highdpi) and getDPI() or 1
self._SCALE = {
x = self._RWIDTH/self._WWIDTH * self._PSCALE,
y = self._RHEIGHT/self._WHEIGHT * self._PSCALE
}
if self._stretched then --if stretched, no need to apply offset
self._OFFSET = {x = 0, y = 0}
else
local scale = math.min(self._SCALE.x, self._SCALE.y)
if self._pixelperfect then scale = math.floor(scale) end
self._OFFSET = {x = (self._SCALE.x - scale) * (self._WWIDTH/2), y = (self._SCALE.y - scale) * (self._WHEIGHT/2)}
self._SCALE.x, self._SCALE.y = scale, scale --apply same scale to X and Y
end
self._GWIDTH = self._RWIDTH * self._PSCALE - self._OFFSET.x * 2
self._GHEIGHT = self._RHEIGHT * self._PSCALE - self._OFFSET.y * 2
end
function push:apply(operation, shader)
self._drawFunctions[operation](self, shader)
end
function push:start()
if self._canvas then
love.graphics.push()
love.graphics.setCanvas({ self.canvases[1].canvas, stencil = self.canvases[1].stencil })
else
love.graphics.translate(self._OFFSET.x, self._OFFSET.y)
love.graphics.setScissor(self._OFFSET.x, self._OFFSET.y, self._WWIDTH*self._SCALE.x, self._WHEIGHT*self._SCALE.y)
love.graphics.push()
love.graphics.scale(self._SCALE.x, self._SCALE.y)
end
end
function push:applyShaders(canvas, shaders)
local _shader = love.graphics.getShader()
if #shaders <= 1 then
love.graphics.setShader(shaders[1])
love.graphics.draw(canvas)
else
local _canvas = love.graphics.getCanvas()
local _tmp = self:getCanvasTable("_tmp")
if not _tmp then --create temp canvas only if needed
self:addCanvas({ name = "_tmp", private = true, shader = nil })
_tmp = self:getCanvasTable("_tmp")
end
love.graphics.push()
love.graphics.origin()
local outputCanvas
for i = 1, #shaders do
local inputCanvas = i % 2 == 1 and canvas or _tmp.canvas
outputCanvas = i % 2 == 0 and canvas or _tmp.canvas
love.graphics.setCanvas(outputCanvas)
love.graphics.clear()
love.graphics.setShader(shaders[i])
love.graphics.draw(inputCanvas)
love.graphics.setCanvas(inputCanvas)
end
love.graphics.pop()
love.graphics.setCanvas(_canvas)
love.graphics.draw(outputCanvas)
end
love.graphics.setShader(_shader)
end
function push:finish(shader)
love.graphics.setBackgroundColor(unpack(self._borderColor))
if self._canvas then
local _render = self:getCanvasTable("_render")
love.graphics.pop()
local white = love11 and 1 or 255
love.graphics.setColor(white, white, white)
--draw canvas
love.graphics.setCanvas(_render.canvas)
for i = 1, #self.canvases do --do not draw _render yet
local _table = self.canvases[i]
if not _table.private then
local _canvas = _table.canvas
local _shader = _table.shader
self:applyShaders(_canvas, type(_shader) == "table" and _shader or { _shader })
end
end
love.graphics.setCanvas()
--draw render
love.graphics.translate(self._OFFSET.x, self._OFFSET.y)
local shader = shader or _render.shader
love.graphics.push()
love.graphics.scale(self._SCALE.x, self._SCALE.y)
self:applyShaders(_render.canvas, type(shader) == "table" and shader or { shader })
love.graphics.pop()
--clear canvas
for i = 1, #self.canvases do
love.graphics.setCanvas(self.canvases[i].canvas)
love.graphics.clear()
end
love.graphics.setCanvas()
love.graphics.setShader()
else
love.graphics.pop()
love.graphics.setScissor()
end
end
function push:setBorderColor(color, g, b)
self._borderColor = g and {color, g, b} or color
end
function push:toGame(x, y)
x, y = x - self._OFFSET.x, y - self._OFFSET.y
local normalX, normalY = x / self._GWIDTH, y / self._GHEIGHT
x = (x >= 0 and x <= self._WWIDTH * self._SCALE.x) and normalX * self._WWIDTH or nil
y = (y >= 0 and y <= self._WHEIGHT * self._SCALE.y) and normalY * self._WHEIGHT or nil
return x, y
end
function push:toReal(x, y)
local realX = self._OFFSET.x + (self._GWIDTH * x)/self._WWIDTH
local realY = self._OFFSET.y + (self._GHEIGHT * y)/self._WHEIGHT
return realX, realY
end
function push:switchFullscreen(winw, winh)
self._fullscreen = not self._fullscreen
local windowWidth, windowHeight = love.window.getDesktopDimensions()
if self._fullscreen then --save windowed dimensions for later
self._WINWIDTH, self._WINHEIGHT = self._RWIDTH, self._RHEIGHT
elseif not self._WINWIDTH or not self._WINHEIGHT then
self._WINWIDTH, self._WINHEIGHT = windowWidth * .5, windowHeight * .5
end
self._RWIDTH = self._fullscreen and windowWidth or winw or self._WINWIDTH
self._RHEIGHT = self._fullscreen and windowHeight or winh or self._WINHEIGHT
self:initValues()
love.window.setFullscreen(self._fullscreen, "desktop")
if not self._fullscreen and (winw or winh) then
windowUpdateMode(self._RWIDTH, self._RHEIGHT) --set window dimensions
end
end
function push:resize(w, h)
if self._highdpi then w, h = w / self._PSCALE, h / self._PSCALE end
self._RWIDTH = w
self._RHEIGHT = h
self:initValues()
end
function push:getWidth() return self._WWIDTH end
function push:getHeight() return self._WHEIGHT end
function push:getDimensions() return self._WWIDTH, self._WHEIGHT end
return push