view semicongine/animation.nim @ 1055:4bf4c029b880

fix: possibly undeterministic queue selection in swapchain
author sam <sam@basx.dev>
date Sat, 30 Mar 2024 22:16:38 +0700
parents c66503386e8b
children 02e1d2658ff5
line wrap: on
line source

{.experimental: "notnil".}

import std/sugar
import std/tables
import std/math
import std/sequtils
import std/algorithm

import ./core/matrix

type
  Ease* = enum
    None
    Linear
    Pow2
    Pow3
    Pow4
    Pow5
    Expo
    Sine
    Circ
  AnimationTime* = 0'f32 .. 1'f32
  Direction* = enum
    Forward
    Backward
    Alternate
  Keyframe[T] = object
    timestamp: AnimationTime
    value: T
    easeIn: Ease
    easeOut: Ease
  Animation*[T] = object
    animationFunction: (state: AnimationState[T], dt: float32) -> T
    duration: float32
    direction: Direction
    iterations: int
  AnimationState*[T] = object
    animation*: Animation[T]
    currentTime*: float32
    playing*: bool
    currentDirection: int
    currentIteration: int
    currentValue*: T

func easeConst(x: float32): float32 = 0
func easeLinear(x: float32): float32 = x
func easePow2(x: float32): float32 = x * x
func easePow3(x: float32): float32 = x * x * x
func easePow4(x: float32): float32 = x * x * x * x
func easePow5(x: float32): float32 = x * x * x * x * x
func easeExpo(x: float32): float32 = (if x == 0: 0'f32 else: pow(2'f32, 10'f32 * x - 10'f32))
func easeSine(x: float32): float32 = 1'f32 - cos((x * PI) / 2'f32)
func easeCirc(x: float32): float32 = 1'f32 - sqrt(1'f32 - pow(x, 2'f32))

const EASEFUNC_MAP = {
    None: easeConst,
    Linear: easeLinear,
    Pow2: easePow2,
    Pow3: easePow3,
    Pow4: easePow4,
    Pow5: easePow5,
    Expo: easeExpo,
    Sine: easeSine,
    Circ: easeCirc,
}.toTable()

func makeEaseOut(f: proc(x: float32): float32 {.noSideEffect.}): auto =
  func wrapper(x: float32): float32 =
    1 - f(1 - x)
  return wrapper

func combine(f1: proc(x: float32): float32 {.noSideEffect.}, f2: proc(x: float32): float32 {.noSideEffect.}): auto =
  func wrapper(x: float32): float32 =
    if x < 0.5: f1(x * 2) * 0.5
    else: f2((x - 0.5) * 2) * 0.5 + 0.5
  return wrapper

func interpol(keyframe: Keyframe, t: float32): float32 =
  if keyframe.easeOut == None:
    return EASEFUNC_MAP[keyframe.easeIn](t)
  elif keyframe.easeIn == None:
    return EASEFUNC_MAP[keyframe.easeOut](t)
  else:
    return combine(EASEFUNC_MAP[keyframe.easeIn], makeEaseOut(EASEFUNC_MAP[keyframe.easeOut]))(t)

func keyframe*[T](timestamp: AnimationTime, value: T, easeIn = Linear, easeOut = None): Keyframe[T] =
  Keyframe[T](timestamp: timestamp, value: value, easeIn: easeIn, easeOut: easeOut)

func newAnimation*[T](keyframes: openArray[Keyframe[T]], duration: float32, direction = Forward, iterations = 1): Animation[T] =
  assert keyframes.len >= 2, "An animation needs at least 2 keyframes"
  assert keyframes[0].timestamp == 0, "An animation's first keyframe needs to have timestamp=0"
  assert keyframes[^1].timestamp == 1, "An animation's last keyframe needs to have timestamp=1"
  var last = keyframes[0].timestamp
  for kf in keyframes[1 .. ^1]:
    assert kf.timestamp > last, "Succeding keyframes must have increasing timestamps"
    last = kf.timestamp

  let theKeyframes = keyframes.toSeq

  proc animationFunc(state: AnimationState[T], dt: float32): T =
    var i = 0
    while i < theKeyframes.len - 1:
      if theKeyframes[i].timestamp > state.t:
        break
      inc i

    let
      keyFrameDist = theKeyframes[i].timestamp - theKeyframes[i - 1].timestamp
      timestampDist = state.t - theKeyframes[i - 1].timestamp
      x = timestampDist / keyFrameDist

    let value = theKeyframes[i - 1].interpol(x)
    return theKeyframes[i].value * value + theKeyframes[i - 1].value * (1 - value)

  Animation[T](
    animationFunction: animationFunc,
    duration: duration,
    direction: direction,
    iterations: iterations
  )

func newAnimation*[T](fun: (state: AnimationState[T], dt: float32) -> T, duration: float32, direction = Forward, iterations = 1): Animation[T] =
  assert fun != nil, "Animation function cannot be nil"
  Animation[T](
    animationFunction: fun,
    duration: duration,
    direction: direction,
    iterations: iterations
  )

proc resetState*[T](state: var AnimationState[T], initial: T) =
  state.currentValue = initial
  state.currentTime = 0
  state.currentDirection = if state.animation.direction == Backward: -1 else: 1
  state.currentIteration = state.animation.iterations

proc t*(state: AnimationState): AnimationTime =
  max(low(AnimationTime), min(state.currentTime / state.animation.duration, high(AnimationTime)))

proc newAnimationState*[T](animation: Animation[T], initial = default(T)): AnimationState[T] =
  result = AnimationState[T](animation: animation, playing: false)
  result.resetState(initial)

proc newAnimationState*[T](value: T = default(T)): AnimationState[T] =
  newAnimationState[T](newAnimation[T]((state: AnimationState[T], dt: float32) => value, 0), initial = value)

func start*(state: var AnimationState) =
  state.playing = true

func stop*(state: var AnimationState) =
  state.playing = false

proc advance*[T](state: var AnimationState[T], dt: float32): T =
  # TODO: check this function, not 100% correct I think
  if state.playing:
    state.currentTime += float32(state.currentDirection) * dt
    if not (0 <= state.currentTime and state.currentTime < state.animation.duration):
      dec state.currentIteration
      # last iteration reached
      if state.currentIteration <= 0 and state.animation.iterations != 0:
        state.stop()
      # more iterations
      else:
        case state.animation.direction:
          of Forward:
            state.currentTime = state.currentTime - state.animation.duration
          of Backward:
            state.currentTime = state.currentTime + state.animation.duration
          of Alternate:
            state.currentDirection = -state.currentDirection
            state.currentTime += float32(state.currentDirection) * dt * 2'f32

    assert state.animation.animationFunction != nil, "Animation func cannot be nil"
    state.currentValue = state.animation.animationFunction(state, dt)
  return state.currentValue