local utils = require "utils" local motion = {} local MIN_ADVANCE_DELTA_TIME_S = 0.01 local tween_mt = { __call = function(t, v) if v ~= nil then return t.set(v) end return t.get() end, } --- Create a motion value tween. Default values: duration 1.0 seconds, delay 0.0 seconds. function motion.tween(value, duration, delay, curve) local state = { initial = value, current = value, target = value, tween_delta_s = 0.0, current_time_s = 0.0, duration = duration or 1.0, delay = delay or 0.0, curve = curve, running = false, } local function advance() local t = nl_time() local dt = t - state.current_time_s if dt < MIN_ADVANCE_DELTA_TIME_S then return end state.tween_delta_s = state.tween_delta_s + dt local progress = math.clamp((state.tween_delta_s - state.delay) / state.duration, 0.0, 1.0) state.current_time_s = t local eased_progress if state.curve then eased_progress = nl_curve_sample(state.curve, progress) else eased_progress = progress end local new_value = utils.lerp(state.initial, state.target, eased_progress) local finished = progress >= 1.0 if finished then state.current = state.target else state.current = new_value end state.running = not finished end function state.get() advance() return state.current end function state.set(v) state.current_time_s = nl_time() state.tween_delta_s = 0.0 state.initial = state.current state.target = v state.running = true advance() return state.current end function state.is_at_rest() return not state.running end setmetatable(state, tween_mt) return state end -- Spring local spring_mt = { __call = function(t, v) if v ~= nil then return t.set(v) end return t.get() end, } local REST_THRESHOLD = 0.001 ^ 2.0 local spring_at_rest_funcs = { number = function(value, target, velocity) return velocity * velocity < REST_THRESHOLD and (value - target) * (value - target) < REST_THRESHOLD end, vector = function(value, target, velocity) return length_squared4(velocity) < REST_THRESHOLD and distance_squared4(value, target) < REST_THRESHOLD end, } --- motion.get_smooth_damped_value(current_value, current_speed, target_value, target_speed, frequency, damping, responsiveness) --- @param current_value - Current position of the element you want to move - Feed returned y back into the function in the next update --- @param current_speed - Current speed of the element you want to move - Feed returned yd back into the function in the next update --- @param target_value - The desired position to interpolate towards --- @param target_speed - OPTIONAL - The speed of the target position. Input zero vector or float in applications where this does not matter. Note that "responsiveness" will not take effect when target_speed is not known. --- @param frequency - OPTIONAL - Natural oscillation frequency of the system in hz. High frequency / time delta ration causes damping to increase to ensure stability. Default 2.0 --- @param damping - OPTIONAL - Damping coefficient. 0 is no damping. 1 is critical damping to achieve zero oscillations. Values over 1 approach target even more slowly. Default 1.0 --- @param responsiveness - OPTIONAL - Responsiveness of the system - 0 accelerates slowly. 100 accelerates almost instantly. Large negative numbers yield opposite direction anticipation kickback. Default 0.0 --- Stateless interpolation to give a smooth step towards a target. Use in an update loop. Can handle floats or vectors automatically. function motion.get_smooth_damped_value(current_value, current_speed, target_value, target_speed, frequency, damping, responsiveness) local dt = nl_delta_time() --Defaults for optionals: target_speed = target_speed or current_value * 0.0 -- Default target speed must be zero but of the same type (vector or float) as current_value input frequency = frequency or 2.0 -- About 1 second motion by default damping = damping or 1.0 -- Critical damping by default (The threshold of no oscillations for smooth blend-out) responsiveness = responsiveness or 0.0 -- Default to slow responsiveness for smooth blend-in -- Calculate coefficients. These could be done only once as they are typically constants, if we want to optimize. local k1 = damping / (math.pi * frequency) local k2 = 1 / (2 * math.pi * frequency)^2 local k3 = responsiveness * damping / (2 * math.pi * frequency) -- Stabilizing for cases of too high frequencies / dt by increasing k2. This clamps the motion just below unstable energy accumulation. k2 = math.max(k2, 1.1 * dt^2 / 4 + dt * k1 / 2) -- Update position and speed current_value = current_value + current_speed * dt current_speed = current_speed + dt * (target_value + k3 * target_speed - current_value - k1 * current_speed) / k2 return current_value, current_speed end function motion.spring(value, spring_constant, damping) local state = { value = value, velocity = value * 0.0, -- sets zero velocity, works for scalars and vectors target = value, spring_constant = spring_constant or 100.0, damping = damping or 40.0, current_time_s = 0.0, } -- determine which at rest function to use -- NOTE: if we ever need to support user-defined types, we could pass in an user-defined at rest function as argument to motion.spring local at_rest_func = spring_at_rest_funcs[type(value)] assert(at_rest_func, "invalid spring value type (number or vector expected)") local function advance() local t = nl_time() local dt = t - state.current_time_s if dt < MIN_ADVANCE_DELTA_TIME_S then return end local value = state.value local velocity = state.velocity local target = state.target local k = state.spring_constant local damping = state.damping -- simulate spring local r = 1.0 / (1.0 + dt * dt * k) velocity = (velocity + (-k * (value - target) - damping * velocity) * dt) * r value = (value - target + velocity * dt - damping * velocity * dt * dt) * r + target local at_rest = at_rest_func(value, target, velocity) if at_rest then state.value = target state.velocity = state.velocity * 0.0 else state.value = value state.velocity = velocity end state.current_time_s = t end function state.get() advance() return state.value end function state.set(v) state.target = v advance() return state.value end function state.is_at_rest() return at_rest_func(state.value, state.target, state.velocity) end setmetatable(state, spring_mt) return state end return motion