view src/semicongine/animation.nim @ 753:eb5ae1f1bc87

fix: do not apply last frame value after animation has stopped
author Sam <sam@basx.dev>
date Tue, 20 Jun 2023 00:30:58 +0700
parents 0b5566fc214d
children da0bd61abe91
line wrap: on
line source

import std/tables
import std/math
import std/sequtils
import std/strformat
import std/algorithm

import ./scene
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
    keyframes*: seq[Keyframe[T]]
    duration: float32
    direction: Direction
    iterations: int
  AnimationPlayer*[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))

func `$`*(animation: Animation): string =
  &"{animation.keyframes.len} keyframes, {animation.duration}s"

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

  Animation[T](
    keyframes: keyframes.toSeq,
    duration: duration,
    direction: direction,
    iterations: iterations
  )

func valueAt[T](animation: Animation[T], timestamp: AnimationTime): T =
  var i = 0
  while i < animation.keyframes.len - 1:
    if animation.keyframes[i].timestamp > timestamp:
      break
    inc i

  let
    keyFrameDist = animation.keyframes[i].timestamp - animation.keyframes[i - 1].timestamp
    timestampDist = timestamp - animation.keyframes[i - 1].timestamp
    x = timestampDist / keyFrameDist

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

func resetPlayer*(player: var AnimationPlayer) =
  player.currentTime = 0
  player.currentDirection = if player.animation.direction == Backward: -1 else : 1
  player.currentIteration = player.animation.iterations

func newAnimator*[T](animation: Animation[T]): AnimationPlayer[T] =
  result = AnimationPlayer[T]( animation: animation, playing: false,)
  result.resetPlayer()

func start*(player: var AnimationPlayer) =
  player.playing = true

func stop*(player: var AnimationPlayer) =
  player.playing = false

func advance*[T](player: var AnimationPlayer[T], dt: float32) =
  # TODO: check, not 100% correct I think
  if player.playing:
    player.currentTime += float32(player.currentDirection) * dt
    if abs(player.currentTime) > player.animation.duration:
      dec player.currentIteration
      if player.currentIteration <= 0 and player.animation.iterations != 0:
        player.stop()
        player.resetPlayer()
      else:
        case player.animation.direction:
          of Forward:
            player.currentTime = player.currentTime - player.animation.duration
          of Backward:
            player.currentTime = player.currentTime + player.animation.duration
          of Alternate:
            player.currentDirection = -player.currentDirection
            player.currentTime += float32(player.currentDirection) * dt * 2'f32

  player.currentValue = valueAt(player.animation, abs(player.currentTime) / player.animation.duration)