refactor: integrate tweens into Task system and simplify camera animations

This commit is contained in:
Kitsune-Jesus 2026-02-02 02:39:28 +01:00
parent 7f1c31f67e
commit 0017b6e104
3 changed files with 119 additions and 84 deletions

View File

@ -1,5 +1,6 @@
local Vec3 = require "lib.utils.vec3"
local utils = require "lib.utils.utils"
local task = require "lib.utils.task"
local EPSILON = 0.001
@ -9,9 +10,6 @@ local EPSILON = 0.001
--- @field speed number
--- @field pixelsPerMeter integer
--- @field scale number
--- @field animationNode AnimationNode?
--- @field animationEndPosition Vec3
--- @field animationBeginPosition Vec3
local camera = {
position = Vec3 {},
velocity = Vec3 {},
@ -38,12 +36,6 @@ local controlMap = {
}
function camera:update(dt)
if self.animationNode and self.animationNode.state == "running" then
self.animationNode:update(dt) -- тик анимации
self.position = utils.lerp(self.animationBeginPosition, self.animationEndPosition, self.animationNode:getValue())
return
end
-------------------- зум на колесо ---------------------
local y = Tree.controls.mouseWheelY
if camera.scale > camera:getDefaultScale() * 5 and y > 0 then return end;
@ -97,14 +89,14 @@ function camera:detach()
love.graphics.pop()
end
--- Плавно перемещает камеру к указанной точке.
--- @param position Vec3
--- @param animationNode AnimationNode
function camera:animateTo(position, animationNode)
if self.animationNode and self.animationNode.state ~= "finished" then self.animationNode:finish() end
self.animationNode = animationNode
self.animationEndPosition = position
self.animationBeginPosition = self.position
self.velocity = Vec3 {}
--- @param duration number?
--- @param easing function?
--- @return Task
function camera:animateTo(position, duration, easing)
self.velocity = Vec3 {} -- Сбрасываем инерцию перед началом анимации
return task.tween(self, { position = position }, duration or 1000, easing)
end
--- @return Camera

View File

@ -1,83 +1,125 @@
--- Обобщенная асинхронная функция
---
--- Использование в общих чертах выглядит так:
--- ```lua
--- local multiplyByTwoCallback = nil
--- local n = nil
--- local function multiplyByTwoAsync(number)
--- -- императивно сохраняем/обрабатываем параметр
--- n = number
--- return function(callback) -- это функция, которая запускает задачу
--- multiplyByTwoCallback = callback
--- end
--- end
---
--- local function update(dt)
--- --- ждем нужного момента времени...
---
--- if multiplyByTwoCallback then -- завершаем вычисление
--- local result = n * 2
--- multiplyByTwoCallback(result) -- результат асинхронного вычисления идет в параметр коллбека!
--- multiplyByTwoCallback = nil
--- end
--- end
---
---
--- --- потом это можно вызывать так:
--- local task = multiplyByTwoAsync(21)
--- -- это ленивое вычисление, так что в этот момент ничего не произойдет
--- -- запускаем
--- task(
--- function(result) print(result) end -- выведет 42 после завершения вычисления, т.е. аналогично `task.then((res) => print(res))` на JS
--- )
---
--- ```
local easing_lib = require "lib.utils.easing"
--- Обобщенная асинхронная функция (Task).
--- По сути это функция, принимающая коллбэк: `fun(callback: fun(value: T): nil): nil`
--- @generic T
--- @alias Task<T> fun(callback: fun(value: T): nil): nil
--- @alias Task fun(callback: fun(value: T): nil): nil
--- Возвращает новый Task, который завершится после завершения всех переданных `tasks`.
---
--- Значение созданного Task будет содержать список значений `tasks` в том же порядке.
---
--- См. также https://api.dart.dev/dart-async/Future/wait.html
--- @generic T
--- @param tasks Task[]
--- @return Task<T[]>
local function wait(tasks)
local count = #tasks
local results = {}
local task = {}
local activeTweens = {}
return function(callback)
for i, task in ipairs(tasks) do
task(
function(result)
results[i] = result
--- Внутренний хелпер для интерполяции (числа или Vec3)
local function lerp(a, b, t)
if type(a) == "number" then
return a + (b - a) * t
elseif type(a) == "table" and getmetatable(a) then
-- Предполагаем, что это Vec3 или другой объект с операторами +, -, *
return a + (b - a) * t
end
return b
end
count = count - 1
if count == 0 then callback(results) end
end
)
--- Обновление всех активных анимаций (твинов).
--- Нужно вызывать в love.update(dt)
function task.update(dt)
for i = #activeTweens, 1, -1 do
local t = activeTweens[i]
t.elapsed = t.elapsed + dt * 1000
local progress = math.min(t.elapsed / t.duration, 1)
local value = t.easing(progress)
for key, targetValue in pairs(t.properties) do
t.target[key] = lerp(t.initial[key], targetValue, value)
end
if progress >= 1 then
table.remove(activeTweens, i)
if t.resolve then t.resolve() end
end
end
end
--- Возвращает Completer — объект, который позволяет вручную завершить таску.
--- @return table completer { complete: fun(val: T) }, Task<T> future
function task.completer()
local c = { completed = false, value = nil, cb = nil }
function c:complete(val)
if self.completed then return end
self.completed = true
self.value = val
if self.cb then self.cb(val) end
end
local future = function(callback)
if c.completed then callback(c.value)
else c.cb = callback end
end
return c, future
end
--- Создает таску, которая плавно меняет свойства объекта.
--- @param target table Объект, свойства которого меняем
--- @param properties table Набор конечных значений { key = value }
--- @param duration number Длительность в мс
--- @param easing function? Функция смягчения (по умолчанию linear)
--- @return Task<nil>
function task.tween(target, properties, duration, easing)
local initial = {}
for k, _ in pairs(properties) do
initial[k] = target[k]
if type(initial[k]) == "table" and initial[k].copy then
initial[k] = initial[k]:copy() -- Для Vec3
end
end
local comp, future = task.completer()
table.insert(activeTweens, {
target = target,
initial = initial,
properties = properties,
duration = duration or 1000,
easing = easing or easing_lib.linear,
elapsed = 0,
resolve = function() comp:complete() end
})
return future
end
--- Возвращает таску, которая завершится сразу с переданным значением.
function task.fromValue(val)
return function(callback) callback(val) end
end
--- Возвращает новый Task, который завершится после завершения всех переданных `tasks`.
--- @param tasks Task[]
--- @return Task<any[]>
function task.wait(tasks)
if #tasks == 0 then return task.fromValue({}) end
local count = #tasks
local results = {}
return function(callback)
for i, t in ipairs(tasks) do
t(function(result)
results[i] = result
count = count - 1
if count == 0 then callback(results) end
end)
end
end
end
--- Последовательно объединяет два `Task` в один.
--- @generic T
--- @generic R
--- @param task Task<T> `Task`, который выполнится первым
--- @param onCompleted fun(value: T): Task<R> Конструктор второго `Task`. Принимает результат выполнения первого `Task`
--- @return Task<R>
local function chain(task, onCompleted)
--- @param t Task
--- @param onCompleted fun(value: any): Task
--- @return Task
function task.chain(t, onCompleted)
return function(callback)
task(function(value)
local task2 = onCompleted(value)
task2(callback)
t(function(value)
local t2 = onCompleted(value)
t2(callback)
end)
end
end
return {
wait = wait,
chain = chain
}
return task

View File

@ -72,6 +72,7 @@ function love.update(dt)
TestRunner:update(dt) -- закомментировать для отключения тестов
local t1 = love.timer.getTime()
require('lib.utils.task').update(dt)
Tree.controls:poll()
Tree.level.camera:update(dt) -- сначала логика камеры, потому что на нее завязан UI
testLayout:update(dt) -- потом UI, потому что нужно перехватить жесты и не пустить их дальше