From a73dec9f318a02b82f7c86d7a000cba288586425 Mon Sep 17 00:00:00 2001 From: Noah Petherbridge Date: Sun, 5 May 2019 16:32:30 -0700 Subject: [PATCH] Doodad Animations Managed In-Engine * Add animation support for Doodad actors (Play Mode) into the core engine, so that the Doodad script can register named animations and play them without managing all the details themselves. * Doodad API functions on Self: AddAnimation, PlayAnimation, StopAnimation, IsAnimating * CLI: the `doodad convert` command will name each layer after the filename used as the input image. * CLI: fix the `doodad convert` command creating duplicate Palette colors when converting a series of input images into a Doodad. --- Makefile | 5 + cmd/doodad/commands/convert.go | 20 +++- dev-assets/doodads/build.sh | 63 +++++++++- dev-assets/doodads/doors/electric-door.js | 35 ++---- dev-assets/doodads/doors/locked-door.js | 10 +- dev-assets/doodads/trapdoors/down.js | 64 +++------- pkg/uix/actor.go | 13 +- pkg/uix/actor_animation.go | 138 ++++++++++++++++++++++ pkg/uix/canvas.go | 18 +++ 9 files changed, 278 insertions(+), 88 deletions(-) create mode 100644 pkg/uix/actor_animation.go diff --git a/Makefile b/Makefile index d5c3c3b..172328b 100644 --- a/Makefile +++ b/Makefile @@ -34,6 +34,11 @@ build-debug: go build $(LDFLAGS) -tags="developer" -i -o bin/doodle cmd/doodle/main.go go build $(LDFLAGS) -tags="developer" -i -o bin/doodad cmd/doodad/main.go +# `make install` to install the Go binaries to your GOPATH. +.PHONY: install +install: + go install git.kirsle.net/apps/doodle/cmd/... + # `make doodads` to build the doodads from the dev-assets folder. .PHONY: doodads doodads: diff --git a/cmd/doodad/commands/convert.go b/cmd/doodad/commands/convert.go index 790d778..ca015ca 100644 --- a/cmd/doodad/commands/convert.go +++ b/cmd/doodad/commands/convert.go @@ -135,6 +135,12 @@ func imageToDrawing(c *cli.Context, chroma render.Color, inputFiles []string, ou images = append(images, img) } + // Helper function to translate image filenames into layer names. + toLayerName := func(filename string) string { + ext := filepath.Ext(filename) + return strings.TrimSuffix(filepath.Base(filename), ext) + } + // Generate the output drawing file. switch strings.ToLower(filepath.Ext(outputFile)) { case extDoodad: @@ -152,14 +158,16 @@ func imageToDrawing(c *cli.Context, chroma render.Color, inputFiles []string, ou palette, layer0 := imageToChunker(images[0], chroma, nil, chunkSize) doodad.Palette = palette doodad.Layers[0].Chunker = layer0 + doodad.Layers[0].Name = toLayerName(inputFiles[0]) // Write any additional layers. if len(images) > 1 { - for i, img := range images[1:] { - log.Info("Converting extra layer %d", i+1) + for i := 1; i < len(images); i++ { + img := images[i] + log.Info("Converting extra layer %d", i) _, chunker := imageToChunker(img, chroma, palette, chunkSize) doodad.Layers = append(doodad.Layers, doodads.Layer{ - Name: fmt.Sprintf("layer-%d", i+1), + Name: toLayerName(inputFiles[i]), Chunker: chunker, }) } @@ -289,6 +297,7 @@ func imageToChunker(img image.Image, chroma render.Color, palette *level.Palette // Cache a palette of unique colors as we go. var uniqueColor = map[string]*level.Swatch{} + var newColors = map[string]*level.Swatch{} // new ones discovered this time for _, swatch := range palette.Swatches { uniqueColor[swatch.Color.String()] = swatch } @@ -310,6 +319,7 @@ func imageToChunker(img image.Image, chroma render.Color, palette *level.Palette Color: color, } uniqueColor[color.String()] = swatch + newColors[color.String()] = swatch } chunker.Set(render.NewPoint(int32(x), int32(y)), swatch) @@ -323,7 +333,9 @@ func imageToChunker(img image.Image, chroma render.Color, palette *level.Palette } sort.Strings(sortedColors) for _, hex := range sortedColors { - palette.Swatches = append(palette.Swatches, uniqueColor[hex]) + if _, ok := newColors[hex]; ok { + palette.Swatches = append(palette.Swatches, uniqueColor[hex]) + } } palette.Inflate() diff --git a/dev-assets/doodads/build.sh b/dev-assets/doodads/build.sh index a850ad8..33dd6ea 100755 --- a/dev-assets/doodads/build.sh +++ b/dev-assets/doodads/build.sh @@ -11,17 +11,61 @@ mkdir -p ../../assets/doodads buttons() { cd buttons/ - doodad convert -t "Sticky Button" sticky1.png sticky2.png sticky-button.doodad - doodad install-script sticky.js sticky-button.doodad - cp sticky-button.doodad ../../../assets/doodads/ + doodad convert -t "Sticky Button" sticky1.png sticky2.png button-sticky.doodad + doodad install-script sticky.js button-sticky.doodad doodad convert -t "Button" button1.png button2.png button.doodad doodad install-script button.js button.doodad - cp button.doodad ../../../assets/doodads/ doodad convert -t "Button Type B" typeB1.png typeB2.png button-typeB.doodad doodad install-script button.js button-typeB.doodad - cp button-typeB.doodad ../../../assets/doodads/ + + cp button*.doodad ../../../assets/doodads/ + cd .. +} + +doors() { + cd doors/ + + doodad convert -t "Red Door" red1.png red2.png door-red.doodad + doodad install-script locked-door.js door-red.doodad + + doodad convert -t "Blue Door" blue1.png blue2.png door-blue.doodad + doodad install-script locked-door.js door-blue.doodad + + doodad convert -t "Green Door" green1.png green2.png door-green.doodad + doodad install-script locked-door.js door-green.doodad + + doodad convert -t "Yellow Door" yellow1.png yellow2.png door-yellow.doodad + doodad install-script locked-door.js door-yellow.doodad + + doodad convert -t "Red Key" red-key.png key-red.doodad + doodad install-script keys.js key-red.doodad + + doodad convert -t "Blue Key" blue-key.png key-blue.doodad + doodad install-script keys.js key-blue.doodad + + doodad convert -t "Green Key" green-key.png key-green.doodad + doodad install-script keys.js key-green.doodad + + doodad convert -t "Yellow Key" yellow-key.png key-yellow.doodad + doodad install-script keys.js key-yellow.doodad + + doodad convert -t "Electric Door" electric{1,2,3,4}.png door-electric.doodad + doodad install-script electric-door.js door-electric.doodad + + cp door-*.doodad key-*.doodad ../../../assets/doodads/ + + cd .. +} + +trapdoors() { + cd trapdoors/ + + doodad convert -t "Trapdoor" down{1,2,3,4}.png trapdoor-down.doodad + doodad install-script down.js trapdoor-down.doodad + + cp trapdoor-*.doodad ../../../assets/doodads/ cd .. } @@ -32,10 +76,17 @@ azulians() { doodad convert -t "Blue Azulian" blu-front.png blu-back.png \ blu-wr{1,2,3,4}.png blu-wl{1,2,3,4}.png azu-blu.doodad doodad install-script azulian.js azu-blu.doodad - cp azu-blu.doodad ../../../assets/doodads/ + + doodad convert -t "Red Azulian" red-front.png red-back.png \ + red-wr{1,2,3,4}.png red-wl{1,2,3,4}.png azu-red.doodad + doodad install-script azulian-red.js azu-red.doodad + + cp azu-*.doodad ../../../assets/doodads/ cd .. } buttons +doors +trapdoors azulians diff --git a/dev-assets/doodads/doors/electric-door.js b/dev-assets/doodads/doors/electric-door.js index dedae0d..b665d73 100644 --- a/dev-assets/doodads/doors/electric-door.js +++ b/dev-assets/doodads/doors/electric-door.js @@ -1,35 +1,16 @@ function main() { console.log("%s initialized!", Self.Doodad.Title); - var timer = 0; + var err = Self.AddAnimation("open", 100, [0, 1, 2, 3]); + console.error("door error: %s", err) + var animating = false; - // Animation frames. - var frame = 0; - var frames = Self.LayerCount(); - var animationDirection = 1; // forward or backward - var animationSpeed = 100; // interval between frames when animating - var animating = false; // true if animation is actively happening - - console.warn("Electric Door has %d frames", frames); - - // Animation interval function. - setInterval(function() { - if (!animating) { + Events.OnCollide(function() { + if (animating) { return; } - // Advance the frame forwards or backwards. - frame += animationDirection; - if (frame >= frames) { - // Reached the last frame, start the pause and reverse direction. - animating = false; - frame = frames - 1; - } - - Self.ShowLayer(frame); - }, animationSpeed); - - Events.OnCollide( function() { - animating = true; // start the animation - }) + animating = true; + Self.PlayAnimation("open", null); + }); } diff --git a/dev-assets/doodads/doors/locked-door.js b/dev-assets/doodads/doors/locked-door.js index 7d1ee7a..75545f9 100644 --- a/dev-assets/doodads/doors/locked-door.js +++ b/dev-assets/doodads/doors/locked-door.js @@ -1,5 +1,13 @@ function main() { + Self.AddAnimation("open", 0, [1]); + var unlocked = false; + Events.OnCollide(function(e) { - Self.ShowLayer(1); + if (unlocked) { + return; + } + + unlocked = true; + Self.PlayAnimation("open", null); }); } diff --git a/dev-assets/doodads/trapdoors/down.js b/dev-assets/doodads/trapdoors/down.js index a38b70d..209eedc 100644 --- a/dev-assets/doodads/trapdoors/down.js +++ b/dev-assets/doodads/trapdoors/down.js @@ -3,53 +3,23 @@ function main() { var timer = 0; - // Animation frames. - var frame = 0; - var frames = Self.LayerCount(); - var animationDirection = 1; // forward or backward - var animationSpeed = 100; // interval between frames when animating - var animationDelay = 8; // delay ticks at the end before reversing, in - // multiples of animationSpeed - var delayCountdown = 0; - var animating = false; // true if animation is actively happening - - console.warn("Trapdoor has %d frames", frames); - - // Animation interval function. - setInterval(function() { - if (!animating) { - return; - } - - // At the end of the animation (door is open), delay before resuming - // the close animation. - if (delayCountdown > 0) { - delayCountdown--; - return; - } - - // Advance the frame forwards or backwards. - frame += animationDirection; - if (frame >= frames) { - // Reached the last frame, start the pause and reverse direction. - delayCountdown = animationDelay; - animationDirection = -1; - - // also bounds check it - frame = frames - 1; - } - - if (frame < 0) { - // reached the start again - frame = 0; - animationDirection = 1; - animating = false; - } - - Self.ShowLayer(frame); - }, animationSpeed); + var animationSpeed = 100; + var animating = false; + Self.AddAnimation("open", animationSpeed, ["down1", "down2", "down3", "down4"]); + Self.AddAnimation("close", animationSpeed, ["down4", "down3", "down2", "down1"]); Events.OnCollide( function() { - animating = true; // start the animation - }) + if (animating) { + return; + } + + animating = true; + Self.PlayAnimation("open", function() { + setTimeout(function() { + Self.PlayAnimation("close", function() { + animating = false; + }); + }, 3000); + }) + }); } diff --git a/pkg/uix/actor.go b/pkg/uix/actor.go index 09c4e34..4405a5f 100644 --- a/pkg/uix/actor.go +++ b/pkg/uix/actor.go @@ -7,6 +7,7 @@ import ( "git.kirsle.net/apps/doodle/lib/render" "git.kirsle.net/apps/doodle/pkg/doodads" "git.kirsle.net/apps/doodle/pkg/level" + "github.com/robertkrimen/otto" uuid "github.com/satori/go.uuid" ) @@ -25,6 +26,11 @@ type Actor struct { activeLayer int // active drawing frame for display flagDestroy bool // flag the actor for destruction + + // Animation variables. + animations map[string]*Animation + activeAnimation *Animation + animationCallback otto.Value } // NewActor sets up a uix.Actor. @@ -46,9 +52,10 @@ func NewActor(id string, levelActor *level.Actor, doodad *doodads.Doodad) *Actor can.Resize(render.NewRect(size, size)) actor := &Actor{ - Drawing: doodads.NewDrawing(id, doodad), - Actor: levelActor, - Canvas: can, + Drawing: doodads.NewDrawing(id, doodad), + Actor: levelActor, + Canvas: can, + animations: map[string]*Animation{}, } // Give the Canvas a pointer to its (parent) Actor so it can draw its debug diff --git a/pkg/uix/actor_animation.go b/pkg/uix/actor_animation.go new file mode 100644 index 0000000..585163c --- /dev/null +++ b/pkg/uix/actor_animation.go @@ -0,0 +1,138 @@ +package uix + +import ( + "errors" + "fmt" + "reflect" + "time" + + "git.kirsle.net/apps/doodle/pkg/log" + "github.com/robertkrimen/otto" +) + +// 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) { + log.Warn("TickAnimation(%s): new layer=%d", a.activeAnimation.Name, an.Layers[an.activeLayer]) + 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. + log.Warn("TickAnimation(%s): finished", a.activeAnimation.Name) + 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 otto.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 = otto.NullValue() +} diff --git a/pkg/uix/canvas.go b/pkg/uix/canvas.go index 38278c3..f0ed9ff 100644 --- a/pkg/uix/canvas.go +++ b/pkg/uix/canvas.go @@ -4,6 +4,7 @@ import ( "fmt" "os" "strings" + "time" "git.kirsle.net/apps/doodle/lib/events" "git.kirsle.net/apps/doodle/lib/render" @@ -15,6 +16,7 @@ import ( "git.kirsle.net/apps/doodle/pkg/log" "git.kirsle.net/apps/doodle/pkg/scripting" "git.kirsle.net/apps/doodle/pkg/wallpaper" + "github.com/robertkrimen/otto" ) // Canvas is a custom ui.Widget that manages a single drawing. @@ -171,6 +173,9 @@ func (w *Canvas) Loop(ev *events.State) error { } _ = w.loopConstrainScroll() + // Current time of this loop so we can advance animations. + now := time.Now() + // Remove any actors that were destroyed the previous tick. var newActors []*Actor for _, a := range w.actors { @@ -187,6 +192,19 @@ func (w *Canvas) Loop(ev *events.State) error { // rectangles so we can later see if any pair of actors intersect each other. boxes := make([]render.Rect, len(w.actors)) for i, a := range w.actors { + // Advance any animations for this actor. + if a.activeAnimation != nil && a.activeAnimation.nextFrameAt.Before(now) { + if done := a.TickAnimation(a.activeAnimation); done { + // Animation has finished, run the callback script. + if a.animationCallback.IsFunction() { + a.animationCallback.Call(otto.NullValue()) + } + + // Clean up the animation state. + a.StopAnimation() + } + } + // Get the actor's velocity to see if it's moving this tick. v := a.Velocity()