view semicongine/animation.nim @ 843:1c3e74f24db0

Merge branch 'main' of github.com:saemideluxe/semicongine
author Sam <sam@basx.dev>
date Sat, 02 Dec 2023 22:26:26 +0700
parents 44ec744fbedc
children 904e0a827ef3
line wrap: on
line source

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: (t: AnimationTime) -> 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))

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(t: AnimationTime): T =
    var i = 0
    while i < theKeyframes.len - 1:
      if theKeyframes[i].timestamp > t:
        break
      inc i

    let
      keyFrameDist = theKeyframes[i].timestamp - theKeyframes[i - 1].timestamp
      timestampDist = 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: (t: AnimationTime) -> T, duration: float32, direction=Forward, iterations=1): Animation[T] =
  Animation[T](
    animationFunction: fun,
    duration: duration,
    direction: direction,
    iterations: iterations
  )

proc reset*(player: var AnimationPlayer) =
  player.currentValue = player.animation.animationFunction(0)
  player.currentTime = 0
  player.currentDirection = if player.animation.direction == Backward: -1 else : 1
  player.currentIteration = player.animation.iterations


proc newAnimationPlayer*[T](animation: Animation[T]): AnimationPlayer[T] =
  result = AnimationPlayer[T](animation: animation, playing: false)
  result.reset()

proc newAnimationPlayer*[T](value: T = default(T)): AnimationPlayer[T] =
  newAnimationPlayer[T](newAnimation[T]((t: AnimationTime) => value, 0))

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

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

proc advance*[T](player: var AnimationPlayer[T], dt: float32): T =
  # TODO: check this function, not 100% correct I think
  if player.playing:
    player.currentTime += float32(player.currentDirection) * dt
    if not (0 <= player.currentTime and player.currentTime < player.animation.duration):
      dec player.currentIteration
      # last iteration reached
      if player.currentIteration <= 0 and player.animation.iterations != 0:
        player.stop()
      # more iterations
      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 = player.animation.animationFunction(
      max(low(AnimationTime), min(player.currentTime / player.animation.duration, high(AnimationTime)))
    )
  return player.currentValue