dphfox / Fusion

A modern reactive UI library, built specifically for Roblox and Luau.
https://elttob.uk/Fusion/
MIT License
530 stars 92 forks source link

Animation timelines/sequences/keyframes #11

Open dphfox opened 2 years ago

dphfox commented 2 years ago

Right now, it can be awkward to work with animations in Fusion that involve multiple keyframes or values.

Fusion could implement a Timeline object which allows the user to define 'keyframes' for a value, which can then be moved/interpolated between by some state representing the current time:

local currentTime = State(0)

local colour = Timeline(currentTime, {
  [0] = Color3.new(1, 0, 0)
  [0.5] = Color3.new(0, 1, 0)
  [1] = Color3.new(0, 0, 1)
})

print(colour:get()) -- red

currentTime:set(0.5)
print(colour:get()) -- green

currentTime:set(0.75)
print(colour:get()) -- halfway between green and blue

currentTime:set(1)
print(colour:get()) --blue

This would be useful for defining more complex declarative animations in Fusion.

dphfox commented 2 years ago

For reference, the above code might currently look like this today:

local currentTime = State(0)

local RED = Color3.new(1, 0, 0)
local GREEN = Color3.new(0, 1, 0)
local BLUE = Color3.new(0, 0, 1)

local color = Computed(function()
    local t = currentTime:get()
    if t <= 0 then
        return RED
    elseif t <= 0.5 then
        return RED:Lerp(GREEN, t*2)
    elseif t <= 1 then
        return GREEN:Lerp(BLUE, t*2 - 1)
    else  
        return BLUE
    end
end)

Also worth mentioning Fusion can also provide perceptually uniform colour blending (Oklab) which looks better than default Roblox lerping (sRGB)

dphfox commented 2 years ago

Plus, this could even be used beyond just animation - because this is all done with state objects, you can use it to process any kind of data you'd like!

Here's an example where you can use it to map values from one range (50-100) to another (0-1):

local input = State(50)

local output = Timeline(input, {[50] = 0, [100] = 1})

print(input:get(), "=", output:get()) -- 50 = 0

input:set(75)
print(input:get(), "=", output:get()) -- 75 = 0.5

input:set(100)
print(input:get(), "=", output:get()) -- 100 = 1
BPilot253 commented 2 years ago

Comparing the code with the Timeline component to the one without it, I definitely think that the Timeline component will be a great addition to Fusion. Is there a way to "tween" the currentTime value?

dphfox commented 2 years ago

Yup - using a Tween or Spring object:

local currentTime = State(0)

local colour = Timeline(
  Tween(currentTime, TweenInfo.new(2, Enum.EasingStyle.Linear, Enum.EasingDirection.Out)), 
  {
    [0] = Color3.new(1, 0, 0)
    [0.5] = Color3.new(0, 1, 0)
    [1] = Color3.new(0, 0, 1)
  }
)
BPilot253 commented 2 years ago

Yup - using a Tween or Spring object:


local currentTime = State(0)

local colour = Timeline(

  Tween(currentTime, TweenInfo.new(2, Enum.EasingStyle.Linear, Enum.EasingDirection.Out)), 

  {

    [0] = Color3.new(1, 0, 0)

    [0.5] = Color3.new(0, 1, 0)

    [1] = Color3.new(0, 0, 1)

  }

)

Then that's perfect!

dphfox commented 2 years ago

Could also be useful in conjunction with timers: https://github.com/Elttob/Fusion/issues/12

dphfox commented 2 years ago

Another thing to consider is 'extrapolation behaviour' - what if you pass in a value before the start of the timeline, or after the end?

For animation uses, clamping to the nearest value would be good enough, but perhaps for some uses it'd be worth providing an option to allow for extrapolation?

dphfox commented 2 years ago

Perhaps 'Keyframes' would be a more appropriate name for this API?