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:
parent
ac490473b3
commit
a73dec9f31
5
Makefile
5
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:
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
})
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
138
pkg/uix/actor_animation.go
Normal file
138
pkg/uix/actor_animation.go
Normal 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()
|
||||
}
|
|
@ -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()
|
||||
|
||||
|
|
Loading…
Reference in New Issue
Block a user