diff --git a/lib/level/camera.lua b/lib/level/camera.lua index 439330c..f2bb368 100644 --- a/lib/level/camera.lua +++ b/lib/level/camera.lua @@ -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 diff --git a/lib/utils/task.lua b/lib/utils/task.lua index 04ef2ae..ae71632 100644 --- a/lib/utils/task.lua +++ b/lib/utils/task.lua @@ -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 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 -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 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 +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 +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 `Task`, который выполнится первым ---- @param onCompleted fun(value: T): Task Конструктор второго `Task`. Принимает результат выполнения первого `Task` ---- @return Task -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 diff --git a/main.lua b/main.lua index b5064d8..a0127ce 100644 --- a/main.lua +++ b/main.lua @@ -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, потому что нужно перехватить жесты и не пустить их дальше