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.
This commit is contained in:
Noah 2019-05-05 16:32:30 -07:00
parent ac490473b3
commit a73dec9f31
9 changed files with 278 additions and 88 deletions

View File

@ -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/doodle cmd/doodle/main.go
go build $(LDFLAGS) -tags="developer" -i -o bin/doodad cmd/doodad/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. # `make doodads` to build the doodads from the dev-assets folder.
.PHONY: doodads .PHONY: doodads
doodads: doodads:

View File

@ -135,6 +135,12 @@ func imageToDrawing(c *cli.Context, chroma render.Color, inputFiles []string, ou
images = append(images, img) 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. // Generate the output drawing file.
switch strings.ToLower(filepath.Ext(outputFile)) { switch strings.ToLower(filepath.Ext(outputFile)) {
case extDoodad: 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) palette, layer0 := imageToChunker(images[0], chroma, nil, chunkSize)
doodad.Palette = palette doodad.Palette = palette
doodad.Layers[0].Chunker = layer0 doodad.Layers[0].Chunker = layer0
doodad.Layers[0].Name = toLayerName(inputFiles[0])
// Write any additional layers. // Write any additional layers.
if len(images) > 1 { if len(images) > 1 {
for i, img := range images[1:] { for i := 1; i < len(images); i++ {
log.Info("Converting extra layer %d", i+1) img := images[i]
log.Info("Converting extra layer %d", i)
_, chunker := imageToChunker(img, chroma, palette, chunkSize) _, chunker := imageToChunker(img, chroma, palette, chunkSize)
doodad.Layers = append(doodad.Layers, doodads.Layer{ doodad.Layers = append(doodad.Layers, doodads.Layer{
Name: fmt.Sprintf("layer-%d", i+1), Name: toLayerName(inputFiles[i]),
Chunker: chunker, 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. // Cache a palette of unique colors as we go.
var uniqueColor = map[string]*level.Swatch{} var uniqueColor = map[string]*level.Swatch{}
var newColors = map[string]*level.Swatch{} // new ones discovered this time
for _, swatch := range palette.Swatches { for _, swatch := range palette.Swatches {
uniqueColor[swatch.Color.String()] = swatch uniqueColor[swatch.Color.String()] = swatch
} }
@ -310,6 +319,7 @@ func imageToChunker(img image.Image, chroma render.Color, palette *level.Palette
Color: color, Color: color,
} }
uniqueColor[color.String()] = swatch uniqueColor[color.String()] = swatch
newColors[color.String()] = swatch
} }
chunker.Set(render.NewPoint(int32(x), int32(y)), swatch) chunker.Set(render.NewPoint(int32(x), int32(y)), swatch)
@ -323,8 +333,10 @@ func imageToChunker(img image.Image, chroma render.Color, palette *level.Palette
} }
sort.Strings(sortedColors) sort.Strings(sortedColors)
for _, hex := range sortedColors { for _, hex := range sortedColors {
if _, ok := newColors[hex]; ok {
palette.Swatches = append(palette.Swatches, uniqueColor[hex]) palette.Swatches = append(palette.Swatches, uniqueColor[hex])
} }
}
palette.Inflate() palette.Inflate()
return palette, chunker return palette, chunker

View File

@ -11,17 +11,61 @@ mkdir -p ../../assets/doodads
buttons() { buttons() {
cd buttons/ cd buttons/
doodad convert -t "Sticky Button" sticky1.png sticky2.png sticky-button.doodad doodad convert -t "Sticky Button" sticky1.png sticky2.png button-sticky.doodad
doodad install-script sticky.js sticky-button.doodad doodad install-script sticky.js button-sticky.doodad
cp sticky-button.doodad ../../../assets/doodads/
doodad convert -t "Button" button1.png button2.png button.doodad doodad convert -t "Button" button1.png button2.png button.doodad
doodad install-script button.js 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 convert -t "Button Type B" typeB1.png typeB2.png button-typeB.doodad
doodad install-script button.js 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 .. cd ..
} }
@ -32,10 +76,17 @@ azulians() {
doodad convert -t "Blue Azulian" blu-front.png blu-back.png \ 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 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 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 .. cd ..
} }
buttons buttons
doors
trapdoors
azulians azulians

View File

@ -1,35 +1,16 @@
function main() { function main() {
console.log("%s initialized!", Self.Doodad.Title); 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. Events.OnCollide(function() {
var frame = 0; if (animating) {
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) {
return; return;
} }
// Advance the frame forwards or backwards. animating = true;
frame += animationDirection; Self.PlayAnimation("open", null);
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
})
} }

View File

@ -1,5 +1,13 @@
function main() { function main() {
Self.AddAnimation("open", 0, [1]);
var unlocked = false;
Events.OnCollide(function(e) { Events.OnCollide(function(e) {
Self.ShowLayer(1); if (unlocked) {
return;
}
unlocked = true;
Self.PlayAnimation("open", null);
}); });
} }

View File

@ -3,53 +3,23 @@ function main() {
var timer = 0; var timer = 0;
// Animation frames. var animationSpeed = 100;
var frame = 0; var animating = false;
var frames = Self.LayerCount(); Self.AddAnimation("open", animationSpeed, ["down1", "down2", "down3", "down4"]);
var animationDirection = 1; // forward or backward Self.AddAnimation("close", animationSpeed, ["down4", "down3", "down2", "down1"]);
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);
Events.OnCollide( function() { 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);
})
});
} }

View File

@ -7,6 +7,7 @@ import (
"git.kirsle.net/apps/doodle/lib/render" "git.kirsle.net/apps/doodle/lib/render"
"git.kirsle.net/apps/doodle/pkg/doodads" "git.kirsle.net/apps/doodle/pkg/doodads"
"git.kirsle.net/apps/doodle/pkg/level" "git.kirsle.net/apps/doodle/pkg/level"
"github.com/robertkrimen/otto"
uuid "github.com/satori/go.uuid" uuid "github.com/satori/go.uuid"
) )
@ -25,6 +26,11 @@ type Actor struct {
activeLayer int // active drawing frame for display activeLayer int // active drawing frame for display
flagDestroy bool // flag the actor for destruction flagDestroy bool // flag the actor for destruction
// Animation variables.
animations map[string]*Animation
activeAnimation *Animation
animationCallback otto.Value
} }
// NewActor sets up a uix.Actor. // NewActor sets up a uix.Actor.
@ -49,6 +55,7 @@ func NewActor(id string, levelActor *level.Actor, doodad *doodads.Doodad) *Actor
Drawing: doodads.NewDrawing(id, doodad), Drawing: doodads.NewDrawing(id, doodad),
Actor: levelActor, Actor: levelActor,
Canvas: can, Canvas: can,
animations: map[string]*Animation{},
} }
// Give the Canvas a pointer to its (parent) Actor so it can draw its debug // Give the Canvas a pointer to its (parent) Actor so it can draw its debug

138
pkg/uix/actor_animation.go Normal file
View File

@ -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()
}

View File

@ -4,6 +4,7 @@ import (
"fmt" "fmt"
"os" "os"
"strings" "strings"
"time"
"git.kirsle.net/apps/doodle/lib/events" "git.kirsle.net/apps/doodle/lib/events"
"git.kirsle.net/apps/doodle/lib/render" "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/log"
"git.kirsle.net/apps/doodle/pkg/scripting" "git.kirsle.net/apps/doodle/pkg/scripting"
"git.kirsle.net/apps/doodle/pkg/wallpaper" "git.kirsle.net/apps/doodle/pkg/wallpaper"
"github.com/robertkrimen/otto"
) )
// Canvas is a custom ui.Widget that manages a single drawing. // 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() _ = w.loopConstrainScroll()
// Current time of this loop so we can advance animations.
now := time.Now()
// Remove any actors that were destroyed the previous tick. // Remove any actors that were destroyed the previous tick.
var newActors []*Actor var newActors []*Actor
for _, a := range w.actors { 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. // rectangles so we can later see if any pair of actors intersect each other.
boxes := make([]render.Rect, len(w.actors)) boxes := make([]render.Rect, len(w.actors))
for i, a := range 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. // Get the actor's velocity to see if it's moving this tick.
v := a.Velocity() v := a.Velocity()