fixed drift in timer
This commit is contained in:
+9
-9
@@ -1,22 +1,22 @@
|
||||
local color={}
|
||||
local mt = {
|
||||
__add = function (c1,c2)
|
||||
return color.new(c1[1]+c2[1],c1[2]+c2[2],c1[2]+c2[2])
|
||||
return color.new(c1[1]+c2[1],c1[2]+c2[2],c1[3]+c2[3])
|
||||
end,
|
||||
__sub = function (c1,c2)
|
||||
return color.new(c1[1]-c2[1],c1[2]-c2[2],c1[2]-c2[2])
|
||||
return color.new(c1[1]-c2[1],c1[2]-c2[2],c1[3]-c2[3])
|
||||
end,
|
||||
__mul = function (c1,c2)
|
||||
return color.new(c1[1]*c2[1],c1[2]*c2[2],c1[2]*c2[2])
|
||||
return color.new(c1[1]*c2[1],c1[2]*c2[2],c1[3]*c2[3])
|
||||
end,
|
||||
__div = function (c1,c2)
|
||||
return color.new(c1[1]/c2[1],c1[2]/c2[2],c1[2]/c2[2])
|
||||
return color.new(c1[1]/c2[1],c1[2]/c2[2],c1[3]/c2[3])
|
||||
end,
|
||||
__mod = function (c1,c2)
|
||||
return color.new(c1[1]%c2[1],c1[2]%c2[2],c1[2]%c2[2])
|
||||
return color.new(c1[1]%c2[1],c1[2]%c2[2],c1[3]%c2[3])
|
||||
end,
|
||||
__pow = function (c1,c2)
|
||||
return color.new(c1[1]^c2[1],c1[2]^c2[2],c1[2]^c2[2])
|
||||
return color.new(c1[1]^c2[1],c1[2]^c2[2],c1[3]^c2[3])
|
||||
end,
|
||||
__unm = function (c1)
|
||||
return color.new(-c1[1],-c1[2],-c1[2])
|
||||
@@ -25,13 +25,13 @@ local mt = {
|
||||
return "("..c[1]..","..c[2]..","..c[3]..",".. (c[4] or "1") ..")"
|
||||
end,
|
||||
__eq = function (c1,c2)
|
||||
return (c1[1]==c2[1] and c1[2]==c2[2] and c1[2]==c2[2])
|
||||
return (c1[1]==c2[1] and c1[2]==c2[2] and c1[3]==c2[3])
|
||||
end,
|
||||
__lt = function (c1,c2)
|
||||
return (c1[1]<c2[1] and c1[2]<c2[2] and c1[2]<c2[2])
|
||||
return (c1[1]<c2[1] and c1[2]<c2[2] and c1[3]<c2[3])
|
||||
end,
|
||||
__le = function (c1,c2)
|
||||
return (c1[1]<=c2[1] and c1[2]<=c2[2] and c1[2]<=c2[2])
|
||||
return (c1[1]<=c2[1] and c1[2]<=c2[2] and c1[3]<=c2[3])
|
||||
end
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,437 @@
|
||||
-- GIF Loader for Love2D with LZW Decompression
|
||||
-- Note: love.data.compress/decompress don't support LZW, so we implement it
|
||||
|
||||
local GifLoader = {}
|
||||
|
||||
-- Pure Lua LZW decompression for GIF
|
||||
local function decompressLZW(data, minCodeSize)
|
||||
local clearCode = 2 ^ minCodeSize
|
||||
local endCode = clearCode + 1
|
||||
local nextCode = endCode + 1
|
||||
local codeSize = minCodeSize + 1
|
||||
|
||||
local dict = {}
|
||||
for i = 0, clearCode - 1 do
|
||||
dict[i] = {string.byte(string.char(i))}
|
||||
end
|
||||
|
||||
local output = {}
|
||||
local bits = 0
|
||||
local bitBuffer = 0
|
||||
local pos = 1
|
||||
local prevCode = nil
|
||||
|
||||
local function readCode()
|
||||
while bits < codeSize do
|
||||
if pos > #data then return nil end
|
||||
bitBuffer = bitBuffer + bit.lshift(string.byte(data, pos), bits)
|
||||
bits = bits + 8
|
||||
pos = pos + 1
|
||||
end
|
||||
|
||||
local code = bit.band(bitBuffer, bit.lshift(1, codeSize) - 1)
|
||||
bitBuffer = bit.rshift(bitBuffer, codeSize)
|
||||
bits = bits - codeSize
|
||||
return code
|
||||
end
|
||||
|
||||
local first = true
|
||||
|
||||
while true do
|
||||
local code = readCode()
|
||||
if not code or code == endCode then break end
|
||||
|
||||
if code == clearCode then
|
||||
dict = {}
|
||||
for i = 0, clearCode - 1 do
|
||||
dict[i] = {string.byte(string.char(i))}
|
||||
end
|
||||
nextCode = endCode + 1
|
||||
codeSize = minCodeSize + 1
|
||||
prevCode = nil
|
||||
first = true
|
||||
else
|
||||
local entry
|
||||
if dict[code] then
|
||||
entry = dict[code]
|
||||
elseif code == nextCode and prevCode then
|
||||
-- Special case: code not in dict yet
|
||||
entry = {}
|
||||
for i = 1, #dict[prevCode] do
|
||||
entry[i] = dict[prevCode][i]
|
||||
end
|
||||
entry[#entry + 1] = dict[prevCode][1]
|
||||
else
|
||||
-- Invalid code, stop
|
||||
break
|
||||
end
|
||||
|
||||
-- Output the entry
|
||||
for i = 1, #entry do
|
||||
table.insert(output, entry[i])
|
||||
end
|
||||
|
||||
-- Add new entry to dictionary
|
||||
if not first and prevCode and nextCode < 4096 then
|
||||
local newEntry = {}
|
||||
for i = 1, #dict[prevCode] do
|
||||
newEntry[i] = dict[prevCode][i]
|
||||
end
|
||||
newEntry[#newEntry + 1] = entry[1]
|
||||
dict[nextCode] = newEntry
|
||||
nextCode = nextCode + 1
|
||||
|
||||
-- Increase code size when needed
|
||||
if nextCode >= bit.lshift(1, codeSize) and codeSize < 12 then
|
||||
codeSize = codeSize + 1
|
||||
end
|
||||
end
|
||||
|
||||
prevCode = code
|
||||
first = false
|
||||
end
|
||||
end
|
||||
|
||||
-- Convert output bytes to string
|
||||
local result = {}
|
||||
for i = 1, #output do
|
||||
result[i] = string.char(output[i])
|
||||
end
|
||||
return table.concat(result)
|
||||
end
|
||||
|
||||
function GifLoader.load(filepath)
|
||||
local fileData = love.filesystem.read(filepath)
|
||||
if not fileData then
|
||||
error("Could not read GIF file: " .. filepath)
|
||||
end
|
||||
|
||||
local gif = {
|
||||
frames = {},
|
||||
frameData = {}, -- Store ImageData for frame composition
|
||||
delays = {},
|
||||
currentFrame = 1,
|
||||
timer = 0,
|
||||
width = 0,
|
||||
height = 0,
|
||||
playing = true,
|
||||
loop = true,
|
||||
getWidth = function(self)
|
||||
return self.width
|
||||
end,
|
||||
getHeight = function(self)
|
||||
return self.height
|
||||
end,
|
||||
}
|
||||
|
||||
-- Parse GIF header
|
||||
local header = fileData:sub(1, 6)
|
||||
if header ~= "GIF87a" and header ~= "GIF89a" then
|
||||
error("Not a valid GIF file")
|
||||
end
|
||||
|
||||
-- Read logical screen descriptor
|
||||
local pos = 7
|
||||
gif.width = string.byte(fileData, pos) + string.byte(fileData, pos + 1) * 256
|
||||
gif.height = string.byte(fileData, pos + 2) + string.byte(fileData, pos + 3) * 256
|
||||
|
||||
local packed = string.byte(fileData, pos + 4)
|
||||
local hasGlobalColorTable = bit.band(packed, 0x80) ~= 0
|
||||
local backgroundColorIndex = string.byte(fileData, pos + 5)
|
||||
|
||||
pos = pos + 7
|
||||
|
||||
-- Read global color table
|
||||
local globalColorTable = {}
|
||||
if hasGlobalColorTable then
|
||||
local size = 2 ^ (bit.band(packed, 0x07) + 1)
|
||||
for i = 1, size do
|
||||
local r = string.byte(fileData, pos) / 255
|
||||
local g = string.byte(fileData, pos + 1) / 255
|
||||
local b = string.byte(fileData, pos + 2) / 255
|
||||
table.insert(globalColorTable, {r, g, b, 1})
|
||||
pos = pos + 3
|
||||
end
|
||||
end
|
||||
|
||||
-- Parse blocks
|
||||
local delay = 0.1
|
||||
local transparentIndex = nil
|
||||
local disposalMethod = 0
|
||||
local delayForNextFrame = 0.1 -- Track delay for next frame
|
||||
|
||||
while pos <= #fileData do
|
||||
local separator = string.byte(fileData, pos)
|
||||
|
||||
if separator == 0x21 then -- Extension
|
||||
local label = string.byte(fileData, pos + 1)
|
||||
pos = pos + 2
|
||||
|
||||
if label == 0xF9 then -- Graphic Control Extension
|
||||
local blockSize = string.byte(fileData, pos)
|
||||
pos = pos + 1
|
||||
|
||||
local flags = string.byte(fileData, pos)
|
||||
disposalMethod = bit.rshift(bit.band(flags, 0x1C), 2)
|
||||
local hasTransparency = bit.band(flags, 0x01) ~= 0
|
||||
|
||||
local delayTime = string.byte(fileData, pos + 1) + string.byte(fileData, pos + 2) * 256
|
||||
-- GIF delay is in hundredths of a second, convert to seconds
|
||||
-- Many GIFs use 0 or very small delays, set a minimum
|
||||
if delayTime == 0 then
|
||||
delayForNextFrame = 0.1 -- Default 100ms
|
||||
elseif delayTime <= 2 then
|
||||
delayForNextFrame = 0.02 -- Minimum 20ms for very fast animations
|
||||
else
|
||||
delayForNextFrame = delayTime / 100
|
||||
end
|
||||
|
||||
if hasTransparency then
|
||||
transparentIndex = string.byte(fileData, pos + 3)
|
||||
else
|
||||
transparentIndex = nil
|
||||
end
|
||||
|
||||
pos = pos + blockSize + 1
|
||||
else
|
||||
-- Skip other extensions
|
||||
repeat
|
||||
local blockSize = string.byte(fileData, pos)
|
||||
pos = pos + 1
|
||||
if blockSize > 0 then
|
||||
pos = pos + blockSize
|
||||
end
|
||||
until blockSize == 0
|
||||
end
|
||||
|
||||
elseif separator == 0x2C then -- Image Descriptor
|
||||
pos = pos + 1
|
||||
|
||||
local left = string.byte(fileData, pos) + string.byte(fileData, pos + 1) * 256
|
||||
local top = string.byte(fileData, pos + 2) + string.byte(fileData, pos + 3) * 256
|
||||
local width = string.byte(fileData, pos + 4) + string.byte(fileData, pos + 5) * 256
|
||||
local height = string.byte(fileData, pos + 6) + string.byte(fileData, pos + 7) * 256
|
||||
|
||||
local imgPacked = string.byte(fileData, pos + 8)
|
||||
local hasLocalColorTable = bit.band(imgPacked, 0x80) ~= 0
|
||||
local interlaced = bit.band(imgPacked, 0x40) ~= 0
|
||||
|
||||
pos = pos + 9
|
||||
|
||||
local colorTable = globalColorTable
|
||||
if hasLocalColorTable then
|
||||
local size = 2 ^ (bit.band(imgPacked, 0x07) + 1)
|
||||
colorTable = {}
|
||||
for i = 1, size do
|
||||
local r = string.byte(fileData, pos) / 255
|
||||
local g = string.byte(fileData, pos + 1) / 255
|
||||
local b = string.byte(fileData, pos + 2) / 255
|
||||
table.insert(colorTable, {r, g, b, 1})
|
||||
pos = pos + 3
|
||||
end
|
||||
end
|
||||
|
||||
-- Read LZW minimum code size
|
||||
local minCodeSize = string.byte(fileData, pos)
|
||||
pos = pos + 1
|
||||
|
||||
-- Read compressed image data blocks
|
||||
local compressedData = {}
|
||||
while true do
|
||||
local blockSize = string.byte(fileData, pos)
|
||||
pos = pos + 1
|
||||
if blockSize == 0 then break end
|
||||
table.insert(compressedData, fileData:sub(pos, pos + blockSize - 1))
|
||||
pos = pos + blockSize
|
||||
end
|
||||
|
||||
-- Decompress image data
|
||||
local indexStream = decompressLZW(table.concat(compressedData), minCodeSize)
|
||||
|
||||
-- Create image data
|
||||
local imageData = love.image.newImageData(gif.width, gif.height)
|
||||
|
||||
-- Fill with background if first frame
|
||||
if #gif.frames == 0 and backgroundColorIndex and globalColorTable[backgroundColorIndex + 1] then
|
||||
local bg = globalColorTable[backgroundColorIndex + 1]
|
||||
for y = 0, gif.height - 1 do
|
||||
for x = 0, gif.width - 1 do
|
||||
imageData:setPixel(x, y, bg[1], bg[2], bg[3], bg[4])
|
||||
end
|
||||
end
|
||||
elseif #gif.frames > 0 then
|
||||
-- Copy previous frame if needed
|
||||
local prevData = gif.frameData[#gif.frameData]
|
||||
imageData:paste(prevData, 0, 0, 0, 0, gif.width, gif.height)
|
||||
end
|
||||
|
||||
-- Draw current frame
|
||||
if indexStream and #indexStream > 0 then
|
||||
local idx = 1
|
||||
|
||||
-- Use mapPixel for faster pixel operations
|
||||
local function setPixels(x, y, r, g, b, a)
|
||||
if x >= left and x < left + width and y >= top and y < top + height then
|
||||
local pixelIdx = (y - top) * width + (x - left) + 1
|
||||
if pixelIdx <= #indexStream then
|
||||
local colorIndex = string.byte(indexStream, pixelIdx)
|
||||
|
||||
-- Skip transparent pixels
|
||||
if transparentIndex == nil or colorIndex ~= transparentIndex then
|
||||
if colorTable[colorIndex + 1] then
|
||||
local color = colorTable[colorIndex + 1]
|
||||
return color[1], color[2], color[3], color[4]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
return r, g, b, a
|
||||
end
|
||||
|
||||
-- Only update the region where the frame is located
|
||||
for y = top, top + height - 1 do
|
||||
for x = left, left + width - 1 do
|
||||
local pixelIdx = (y - top) * width + (x - left) + 1
|
||||
if pixelIdx <= #indexStream then
|
||||
local colorIndex = string.byte(indexStream, pixelIdx)
|
||||
|
||||
-- Skip transparent pixels
|
||||
if transparentIndex == nil or colorIndex ~= transparentIndex then
|
||||
if colorTable[colorIndex + 1] then
|
||||
local color = colorTable[colorIndex + 1]
|
||||
imageData:setPixel(x, y, color[1], color[2], color[3], color[4])
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
table.insert(gif.frameData, imageData)
|
||||
table.insert(gif.frames, love.graphics.newImage(imageData))
|
||||
table.insert(gif.delays, delayForNextFrame)
|
||||
|
||||
-- Reset delay for next frame
|
||||
delayForNextFrame = 0.1
|
||||
|
||||
elseif separator == 0x3B then -- Trailer
|
||||
break
|
||||
else
|
||||
pos = pos + 1
|
||||
end
|
||||
end
|
||||
|
||||
if #gif.frames == 0 then
|
||||
error("No frames found in GIF")
|
||||
end
|
||||
|
||||
-- Ensure all frames have valid delays
|
||||
for i = 1, #gif.delays do
|
||||
if gif.delays[i] <= 0 or gif.delays[i] ~= gif.delays[i] then -- check for 0 or NaN
|
||||
gif.delays[i] = 0.1
|
||||
end
|
||||
end
|
||||
|
||||
return gif
|
||||
end
|
||||
|
||||
function GifLoader.Updater(gif, proc)
|
||||
local wait = function()
|
||||
return gif.playing or gif.kill
|
||||
end
|
||||
proc:newThread("Gif Handler",function()
|
||||
while true do
|
||||
-- Only run if not paused
|
||||
if gif.kill then -- When we want to clean up
|
||||
thread.kill()
|
||||
end
|
||||
thread.hold(wait)
|
||||
thread.sleep(gif.delays[gif.currentFrame] * 4)
|
||||
gif.currentFrame = gif.currentFrame + 1
|
||||
|
||||
if gif.currentFrame > #gif.frames then
|
||||
if gif.loop then
|
||||
gif.currentFrame = 1
|
||||
else
|
||||
gif.currentFrame = #gif.frames
|
||||
gif.playing = false
|
||||
gif.timer = 0
|
||||
end
|
||||
end
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
function GifLoader.update(gif, dt)
|
||||
if not gif.playing or #gif.frames <= 1 then return end
|
||||
|
||||
gif.timer = gif.timer + dt
|
||||
|
||||
-- Simple, accurate frame advancement
|
||||
if gif.timer >= gif.delays[gif.currentFrame] then
|
||||
-- Subtract the current frame's delay
|
||||
gif.timer = gif.timer - gif.delays[gif.currentFrame]
|
||||
|
||||
-- Move to next frame
|
||||
gif.currentFrame = gif.currentFrame + 1
|
||||
|
||||
if gif.currentFrame > #gif.frames then
|
||||
if gif.loop then
|
||||
gif.currentFrame = 1
|
||||
else
|
||||
gif.currentFrame = #gif.frames
|
||||
gif.playing = false
|
||||
gif.timer = 0
|
||||
end
|
||||
end
|
||||
|
||||
-- If we've accumulated too much time (lag spike), cap it
|
||||
if gif.timer > 0.5 then
|
||||
gif.timer = 0
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function GifLoader.draw(gif, x, y, r, sx, sy, ox, oy)
|
||||
if gif.frames[gif.currentFrame] then
|
||||
love.graphics.draw(gif.frames[gif.currentFrame], x, y, r or 0, sx or 1, sy or 1, ox or 0, oy or 0)
|
||||
end
|
||||
end
|
||||
|
||||
function GifLoader.play(gif)
|
||||
gif.playing = true
|
||||
end
|
||||
|
||||
function GifLoader.pause(gif)
|
||||
gif.playing = false
|
||||
end
|
||||
|
||||
function GifLoader.reset(gif)
|
||||
gif.currentFrame = 1
|
||||
gif.timer = 0
|
||||
end
|
||||
|
||||
function GifLoader.setFixedFramerate(gif, fps)
|
||||
local delay = 1 / fps
|
||||
for i = 1, #gif.delays do
|
||||
gif.delays[i] = delay
|
||||
end
|
||||
end
|
||||
|
||||
function GifLoader.getInfo(gif)
|
||||
return {
|
||||
width = gif.width,
|
||||
height = gif.height,
|
||||
frameCount = #gif.frames,
|
||||
delays = gif.delays, -- Show actual delays for debugging
|
||||
totalDuration = (function()
|
||||
local total = 0
|
||||
for i = 1, #gif.delays do
|
||||
total = total + gif.delays[i]
|
||||
end
|
||||
return total
|
||||
end)()
|
||||
}
|
||||
end
|
||||
|
||||
return GifLoader
|
||||
@@ -0,0 +1,115 @@
|
||||
--[[
|
||||
scheduler_probe.lua
|
||||
-------------------
|
||||
A drop-in replacement for multi:getLoad() based on scheduler tick-slip
|
||||
rather than step-count benchmarking.
|
||||
|
||||
THEORY
|
||||
------
|
||||
Schedule a repeating timer at a fixed interval T. Each time it fires,
|
||||
measure how much later than T it actually arrived. On an idle scheduler
|
||||
the slip is near zero. Under load the main loop is busy with other tasks
|
||||
between iterations, so ticks are delayed.
|
||||
|
||||
We express load as:
|
||||
|
||||
lag = actual_interval - target_interval (seconds)
|
||||
lag_ratio = lag / target_interval (0 = perfect, 1 = 1 full interval late)
|
||||
load% = clamp(lag_ratio * 100, 0, 100)
|
||||
|
||||
To smooth out single-frame spikes we keep an exponential moving average
|
||||
(EMA) of lag_ratio with a configurable smoothing factor.
|
||||
|
||||
WHY THIS IS BETTER THAN THE STEP-COUNT APPROACH
|
||||
------------------------------------------------
|
||||
- No calibration baseline that drifts with object count or warm-up
|
||||
- No magic exponents or divisors
|
||||
- Does not block or create temporary objects on each call
|
||||
- Measures actual scheduler responsiveness, not raw throughput
|
||||
- Works correctly regardless of which processor calls it
|
||||
- A single lightweight TLoop is the only permanent overhead
|
||||
|
||||
USAGE
|
||||
-----
|
||||
local probe = require("scheduler_probe")
|
||||
probe:install(multi) -- once, at startup
|
||||
|
||||
-- anywhere, non-blocking:
|
||||
local load, lagMs = multi:getLoad()
|
||||
-- load : integer 0-100
|
||||
-- lagMs : smoothed lag in milliseconds (useful for display)
|
||||
|
||||
OPTIONAL PARAMETERS
|
||||
-------------------
|
||||
probe:install(multi, {
|
||||
interval = 0.05, -- probe fires every N seconds (default 0.05 = 50ms)
|
||||
alpha = 0.15, -- EMA smoothing factor 0-1 (default 0.15)
|
||||
-- lower = smoother but slower to react
|
||||
-- higher = more reactive but noisier
|
||||
maxLag = 0.5, -- lag value (seconds) that maps to 100% load (default 0.5)
|
||||
-- tune this to match your target frame budget
|
||||
})
|
||||
]]
|
||||
|
||||
local probe = {}
|
||||
|
||||
-- EMA state — written by the TLoop callback, read by getLoad()
|
||||
-- Both are plain numbers so Lua's assignment is atomic within one thread.
|
||||
local _emaRatio = 0 -- smoothed lag / maxLag, clamped 0-1
|
||||
local _lagMs = 0 -- smoothed lag in milliseconds for display
|
||||
local _installed = false
|
||||
|
||||
function probe:install(multi_obj, opts)
|
||||
if _installed then return end
|
||||
_installed = true
|
||||
|
||||
opts = opts or {}
|
||||
local INTERVAL = opts.interval or 0.05 -- seconds between probes
|
||||
local ALPHA = opts.alpha or 0.15 -- EMA weight for new sample
|
||||
local MAX_LAG = opts.maxLag or 0.5 -- seconds of lag = 100% load
|
||||
|
||||
local clock = os.clock
|
||||
|
||||
-- Track when the tick *should* have fired so we can compute slip
|
||||
-- relative to the scheduled time, not relative to the previous firing.
|
||||
-- This avoids error accumulation over long runs.
|
||||
local expectedTime = clock() + INTERVAL
|
||||
|
||||
local tloop = multi_obj:newTLoop(nil, INTERVAL)
|
||||
tloop:setName("SchedulerProbe")
|
||||
tloop:setPriority("core") -- run as early as possible each frame
|
||||
|
||||
tloop.OnLoop(function(self, life, dt)
|
||||
local now = clock()
|
||||
local lag = math.max(0, now - expectedTime) -- never negative
|
||||
local ratio = math.min(lag / MAX_LAG, 1) -- clamp to [0,1]
|
||||
|
||||
-- Exponential moving average: new = alpha*sample + (1-alpha)*old
|
||||
_emaRatio = ALPHA * ratio + (1 - ALPHA) * _emaRatio
|
||||
_lagMs = ALPHA * lag*1000 + (1 - ALPHA) * _lagMs
|
||||
|
||||
-- Advance expected time by one interval from where it *should* have been,
|
||||
-- not from now — prevents the probe from drifting under sustained load.
|
||||
expectedTime = expectedTime + INTERVAL
|
||||
-- If we fall more than one interval behind (e.g. after a long GC pause),
|
||||
-- re-anchor so we don't fire in a catch-up burst.
|
||||
if now > expectedTime + INTERVAL then
|
||||
expectedTime = now + INTERVAL
|
||||
end
|
||||
end)
|
||||
|
||||
-- Replace multi:getLoad() with a non-blocking version that just reads the EMA
|
||||
function multi_obj:getLoad()
|
||||
local pct = math.ceil(_emaRatio * 100)
|
||||
return pct, _lagMs
|
||||
end
|
||||
|
||||
-- Also expose raw probe state for diagnostics
|
||||
function multi_obj:getSchedulerLag()
|
||||
return _lagMs, _emaRatio
|
||||
end
|
||||
|
||||
return tloop
|
||||
end
|
||||
|
||||
return probe
|
||||
@@ -1,6 +1,6 @@
|
||||
local gui = require("gui")
|
||||
local multi, thread = require("multi"):init()
|
||||
local transition = require("gui.elements.transitions")
|
||||
local transition = require("gui.core.transitions")
|
||||
|
||||
-- Triggers press then release
|
||||
local function getPosition(obj, x, y)
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
local gui = require("gui")
|
||||
local multi, thread = require("multi"):init()
|
||||
local processor = gui:newProcessor("Transistion Processor")
|
||||
local transition = {}
|
||||
|
||||
local width, height, flags = love.window.getMode()
|
||||
local fps = 60
|
||||
|
||||
if flags.refreshrate > 0 then
|
||||
fps = flags.refreshrate
|
||||
end
|
||||
|
||||
transition.__index = transition
|
||||
transition.__call = function(t, start, stop, time, ...)
|
||||
local args = {...}
|
||||
return function(st, sp, ti) -- allow these values to be overridden
|
||||
if not (st or start) or not (sp or stop) then return multi.error("start and stop must be supplied") end
|
||||
if start == stop then
|
||||
local temp = {
|
||||
OnStep = function() end,
|
||||
OnStop = multi:newConnection()
|
||||
}
|
||||
proc:newTask(function()
|
||||
temp.OnStop:Fire()
|
||||
end)
|
||||
return temp
|
||||
end
|
||||
local handle = t.func(t, start, stop, (ti or time) or 1, unpack(args))
|
||||
return {
|
||||
OnStep = handle.OnStatus,
|
||||
OnStop = handle.OnReturn + handle.OnError,
|
||||
Kill = t.Kill
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
function transition:newTransition(func)
|
||||
local c = {}
|
||||
setmetatable(c, self)
|
||||
|
||||
c.fps = fps
|
||||
c.func = processor:newFunction(func)
|
||||
c.OnStop = multi:newConnection()
|
||||
c.kill = false
|
||||
|
||||
function c:SetFPS(fps)
|
||||
self.fps = fps
|
||||
end
|
||||
|
||||
function c:GetFPS(fps)
|
||||
return self.fps
|
||||
end
|
||||
|
||||
function c:Kill()
|
||||
if c.running then
|
||||
c.kill = true
|
||||
end
|
||||
end
|
||||
|
||||
return c
|
||||
end
|
||||
|
||||
transition.glide = transition:newTransition(function(t, start, stop, time, ...)
|
||||
local steps = t.fps*time
|
||||
local piece = time/steps
|
||||
local split = stop-start
|
||||
t.running = true
|
||||
for i = 0, steps do
|
||||
if not(t.kill) then
|
||||
thread.sleep(piece)
|
||||
thread.pushStatus(start + i*(split/steps),piece*i)
|
||||
end
|
||||
end
|
||||
t.running = false
|
||||
t.kill = false
|
||||
end)
|
||||
|
||||
return transition
|
||||
Reference in New Issue
Block a user