doodle/pkg/uix/actor_animation.go

137 lines
3.3 KiB
Go

package uix
import (
"errors"
"fmt"
"reflect"
"time"
"git.kirsle.net/SketchyMaze/doodle/pkg/log"
"github.com/dop251/goja"
)
// Animation holds a named animation for a doodad script.
type Animation struct {
Name string
Interval time.Duration
Layers []int
// runtime state variables
activeLayer int
nextFrameAt time.Time
}
/*
TickAnimation advances an animation forward.
This method is called by canvas.Loop() only when the actor is currently
`animating` and their current animation's nextFrameAt has been reached by the
current time.Now().
Returns true when the animation has finished and false if there is still more
frames left to animate.
*/
func (a *Actor) TickAnimation(an *Animation) bool {
an.activeLayer++
if an.activeLayer < len(an.Layers) {
a.ShowLayer(an.Layers[an.activeLayer])
} else if an.activeLayer >= len(an.Layers) {
// final layer has been shown for 2 ticks, return that the animation has
// been concluded.
return true
}
// Schedule the next frame of animation.
an.nextFrameAt = time.Now().Add(an.Interval)
return false
}
// AddAnimation installs a new animation into the scripting engine for this actor.
//
// The layers can be an array of string names or integer indexes.
func (a *Actor) AddAnimation(name string, interval int64, layers []interface{}) error {
if len(layers) == 0 {
return errors.New("no named layers given to AddAnimation()")
}
// Find all the layers by name.
var indexes []int
for _, name := range layers {
switch v := name.(type) {
case string:
var found bool
for i, layer := range a.Doodad().Layers {
if layer.Name == v {
indexes = append(indexes, i)
found = true
break
}
}
if !found {
return fmt.Errorf("layer named '%s' not found in doodad", v)
}
case int64:
// TODO: I want to find out if this is ever not an int64 coming from
// JavaScript.
if reflect.TypeOf(v).String() != "int64" {
log.Error("AddAnimation: expected an int64 from JavaScript but got a %s", reflect.TypeOf(v))
}
iv := int(v)
if iv < len(a.Doodad().Layers) {
indexes = append(indexes, iv)
} else {
return fmt.Errorf("layer numbered '%d' is out of bounds", iv)
}
default:
return fmt.Errorf(
"invalid type for layer '%+v': should be a string (named layer) "+
"or int (indexed layer) but was a %s", v, reflect.TypeOf(name))
}
}
a.animations[name] = &Animation{
Name: name,
Interval: time.Duration(interval) * time.Millisecond,
Layers: indexes,
}
return nil
}
// PlayAnimation starts an animation and then calls a JavaScript function when
// the last frame has played out. Set a null function to ignore the callback.
func (a *Actor) PlayAnimation(name string, callback goja.Value) error {
anim, ok := a.animations[name]
if !ok {
return fmt.Errorf("animation named '%s' not found", name)
}
a.activeAnimation = anim
a.animationCallback = callback
// Show the first layer.
anim.activeLayer = 0
anim.nextFrameAt = time.Now().Add(anim.Interval)
a.ShowLayer(anim.Layers[0])
return nil
}
// IsAnimating returns if the current actor is playing an animation.
func (a *Actor) IsAnimating() bool {
return a.activeAnimation != nil
}
// StopAnimation stops any current animations.
func (a *Actor) StopAnimation() {
if a.activeAnimation == nil {
return
}
a.activeAnimation = nil
a.animationCallback = goja.Null()
}