diff --git a/.debpacker.yml b/.debpacker.yml index 24cd3c0..c32e053 100644 --- a/.debpacker.yml +++ b/.debpacker.yml @@ -5,6 +5,7 @@ copy-files: - "./matrix-package/themes/*:themes" - "./matrix-package/fonts/*:fonts" - "./matrix-package/images/*:images" + - "./matrix-package/animations/*:animations" - "./matrix-package/gamepad/public/*:gamepad/public" - "./matrix-package/emulator/public/*:emulator/public" version: "0.0.1" @@ -13,7 +14,7 @@ maintainer: "richard.le.terrier@gmail.com" systemd-configuration: user: "root" after: network.target - args: ["start", "--log-level", "info", "--gamepad-port", "80", "core", "device", "gamepad", "emulator", "demo", "zigzag", "yumyum", "clock", "draw", "blocks", "getout"] + args: ["start", "--log-level", "info", "--gamepad-port", "80", "core", "device", "gamepad", "emulator", "demo", "zigzag", "yumyum", "clock", "draw", "blocks", "getout", "animate"] stop-command: /bin/kill $MAINPID restart: always wanted-by: multi-user.target diff --git a/Makefile b/Makefile index c261435..48c1726 100644 --- a/Makefile +++ b/Makefile @@ -42,6 +42,7 @@ package: cp -R themes matrix-package/ cp -R fonts matrix-package/ cp -R images matrix-package/ + cp -R animations matrix-package/ cp -R gamepad/build/default/. matrix-package/gamepad/public/ cp -R emulator/client/public/. matrix-package/emulator/public/ zip -r matrix.zip matrix-package diff --git a/README.md b/README.md index 1ace536..a3b894c 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,7 @@ Video game console operating system that displays on a 16*9 RGB LED matrix. | | Blocks | A puzzle game, score a maximum of points by clearing complete lines. | | | | Getout | A labyrinth game, try to get out if you can. | | | | Rollup dice | Random dice generator (https://github.com/gwenker/matrix-rollup-dice). | | +| | Animate | Player for animations generated with Glediator (http://www.solderlab.de/index.php/software/glediator). | | ## Matrix types diff --git a/animate/daemon.go b/animate/daemon.go new file mode 100644 index 0000000..8973b9d --- /dev/null +++ b/animate/daemon.go @@ -0,0 +1,203 @@ +package animate + +import ( + "context" + "encoding/json" + "fmt" + "io/ioutil" + "os" + "path/filepath" + "strings" + "time" + + "github.com/ovh/cds/sdk" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" + + "github.com/richardlt/matrix/sdk-go/common" + "github.com/richardlt/matrix/sdk-go/software" +) + +// Start the animate software. +func Start(uri string) error { + logrus.Infof("Start animate for uri %s\n", uri) + + a := &animate{} + + // list animation headers and set index + if err := filepath.Walk("./animations", func(path string, info os.FileInfo, err error) error { + if !info.IsDir() && strings.HasSuffix(path, ".json") { + buf, err := ioutil.ReadFile(path) + if err != nil { + return sdk.WithStack(err) + } + + var h header + if err := json.Unmarshal(buf, &h); err != nil { + return errors.WithStack(err) + } + + a.headers = append(a.headers, h) + } + return nil + }); err != nil { + return errors.WithStack(err) + } + + return software.Connect(uri, a, true) +} + +type header struct { + Name string `json:"name"` + Width int `json:"width"` + Height int `json:"height"` + FPS int `json:"fps"` +} + +type animation []byte + +func (a animation) readFrame(width, height, index int) software.Image { + pixels := 3 * width * height + start := index * pixels + end := start + pixels + buf := a[start:end] + + var colors []*common.Color + mapColors := map[string]uint64{} + mask := make([]uint64, width*height) + var cursor int + for i := range mask { + c := common.Color{ + R: uint64(buf[cursor]), + G: uint64(buf[cursor+1]), + B: uint64(buf[cursor+2]), + A: 1, + } + key := fmt.Sprintf("%d%d%d", c.R, c.G, c.B) + if v, ok := mapColors[key]; !ok { + colors = append(colors, &c) + mask[i] = uint64(len(colors) - 1) + mapColors[key] = mask[i] + } else { + mask[i] = v + } + cursor += 3 + } + + return software.Image{ + Width: uint64(width), + Height: uint64(height), + Colors: colors, + Mask: mask, + } +} + +type animate struct { + api software.API + layer software.Layer + imageDriver *software.ImageDriver + cancel context.CancelFunc + headers []header + index int +} + +func (a *animate) Init(api software.API) (err error) { + logrus.Debug("Init animate") + + a.api = api + + i := api.GetImageFromLocal("animate") + + api.SetConfig(software.ConnectRequest_SoftwareData_Config{ + Logo: &i, + MinPlayerCount: 1, + MaxPlayerCount: 1, + }) + + a.layer, err = api.NewLayer() + if err != nil { + return err + } + + a.imageDriver, err = a.layer.NewImageDriver() + if err != nil { + return err + } + a.imageDriver.OnEnd(func() { a.api.Print() }) + + return api.Ready() +} + +func (a *animate) Start(playerCount uint64) { a.play() } + +func (a *animate) Close() { a.reset() } + +func (a *animate) ActionReceived(slot uint64, cmd common.Command) { + switch cmd { + case common.Command_LEFT_UP: + if a.index < 1 { + a.index = len(a.headers) - 1 + } else { + a.index-- + } + a.play() + case common.Command_RIGHT_UP: + if a.index+1 == len(a.headers) { + a.index = 0 + } else { + a.index++ + } + a.play() + } +} + +func (a *animate) reset() { + if a.cancel != nil { + a.cancel() + } +} + +func (a *animate) play() { + a.reset() + + a.layer.Clean() + a.api.Print() + + if len(a.headers) == 0 { + return + } + + var anim animation + var err error + anim, err = ioutil.ReadFile(fmt.Sprintf("./animations/%s", a.headers[a.index].Name)) + if err != nil { + logrus.Error(errors.WithStack(err)) + return + } + + ctx, cancel := context.WithCancel(context.Background()) + a.cancel = cancel + + h := a.headers[a.index] + + ticker := time.NewTicker(time.Second / time.Duration(h.FPS)) + defer ticker.Stop() + maxIndex := len(anim) / (h.Width * h.Height * 3) + index := 0 + for { + select { + case <-ctx.Done(): + return + case <-ticker.C: + a.imageDriver.Render( + anim.readFrame(h.Width, h.Height, index), + common.Coord{X: 8, Y: 4}, // middle of the screen + ) + if index+1 == maxIndex { + index = 0 + } else { + index++ + } + } + } +} diff --git a/animations/sample-one.dat b/animations/sample-one.dat new file mode 100644 index 0000000..2eb434b Binary files /dev/null and b/animations/sample-one.dat differ diff --git a/animations/sample-one.json b/animations/sample-one.json new file mode 100644 index 0000000..7d603f1 --- /dev/null +++ b/animations/sample-one.json @@ -0,0 +1,6 @@ +{ + "name": "sample-one.dat", + "width": 16, + "height": 9, + "fps": 25 +} \ No newline at end of file diff --git a/animations/sample-three.dat b/animations/sample-three.dat new file mode 100644 index 0000000..b7111b7 Binary files /dev/null and b/animations/sample-three.dat differ diff --git a/animations/sample-three.json b/animations/sample-three.json new file mode 100644 index 0000000..6e5b6ff --- /dev/null +++ b/animations/sample-three.json @@ -0,0 +1,6 @@ +{ + "name": "sample-three.dat", + "width": 16, + "height": 9, + "fps": 25 +} \ No newline at end of file diff --git a/animations/sample-two.dat b/animations/sample-two.dat new file mode 100644 index 0000000..4cec814 Binary files /dev/null and b/animations/sample-two.dat differ diff --git a/animations/sample-two.json b/animations/sample-two.json new file mode 100644 index 0000000..fb2a0e4 --- /dev/null +++ b/animations/sample-two.json @@ -0,0 +1,6 @@ +{ + "name": "sample-two.dat", + "width": 16, + "height": 9, + "fps": 25 +} \ No newline at end of file diff --git a/docs/animate.gif b/docs/animate.gif new file mode 100644 index 0000000..d5a881d Binary files /dev/null and b/docs/animate.gif differ diff --git a/docs/animate.png b/docs/animate.png new file mode 100644 index 0000000..227393f Binary files /dev/null and b/docs/animate.png differ diff --git a/images/animate.json b/images/animate.json new file mode 100644 index 0000000..cfed9e4 --- /dev/null +++ b/images/animate.json @@ -0,0 +1,24 @@ +{ + "name": "animate", + "height": 7, + "width": 8, + "colors": [ + { "r": 205, "g": 0, "b": 0, "a": 1 }, + { "r": 255, "g": 0, "b": 0, "a": 1 }, + { "r": 0, "g": 205, "b": 0, "a": 1 }, + { "r": 0, "g": 255, "b": 0, "a": 1 }, + { "r": 0, "g": 0, "b": 205, "a": 1 }, + { "r": 0, "g": 0, "b": 255, "a": 1 }, + { "r": 255, "g": 255, "b": 0, "a": 1 }, + { "r": 205, "g": 205, "b": 0, "a": 1 } + ], + "mask": [ + 3, 2, 3, 7, 6, 7, 5, 4, + 1, 3, 2, 3, 7, 6, 7, 5, + 0, 1, 3, 2, 3, 7, 6, 7, + 1, 0, 1, 3, 2, 3, 7, 6, + 5, 1, 0, 1, 3, 2, 3, 7, + 4, 5, 1, 0, 1, 3, 2, 3, + 5, 4, 5, 1, 0, 1, 3, 2 + ] +} diff --git a/main.go b/main.go index 2bc55d1..f7a65b5 100644 --- a/main.go +++ b/main.go @@ -5,6 +5,10 @@ import ( "os" "github.com/pkg/errors" + "github.com/sirupsen/logrus" + cli "gopkg.in/urfave/cli.v1" + + "github.com/richardlt/matrix/animate" "github.com/richardlt/matrix/blocks" "github.com/richardlt/matrix/clock" "github.com/richardlt/matrix/core" @@ -16,8 +20,6 @@ import ( "github.com/richardlt/matrix/getout" "github.com/richardlt/matrix/yumyum" "github.com/richardlt/matrix/zigzag" - "github.com/sirupsen/logrus" - cli "gopkg.in/urfave/cli.v1" ) func main() { @@ -98,6 +100,8 @@ func startAction(c *cli.Context) error { cs = append(cs, component(func() error { return blocks.Start(c.String("core-uri")) })) case "getout": cs = append(cs, component(func() error { return getout.Start(c.String("core-uri")) })) + case "animate": + cs = append(cs, component(func() error { return animate.Start(c.String("core-uri")) })) default: return errors.New("Invalid given component name") }