Pinball dot-matrix clock animation thingy
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

360 lines
7.9 KiB

package matrix
import (
"fmt"
"image"
"image/color"
"image/draw"
"io"
"log"
"math/rand"
"time"
"golang.org/x/image/colornames"
"maze.io/x/dmd"
"maze.io/x/pixel"
"maze.io/x/pixel/pixeleffect"
"maze.io/matrix/driver"
"maze.io/matrix/driver/text"
_ "maze.io/matrix/driver/text" // Text driver
)
const (
layerBackground = iota
layerClockBackground
layerAnimation
layerClockForeground
animationFadeOut = 750 * time.Millisecond
animationFadeOutStep = 75 * time.Millisecond
animationShort = time.Second / 3
animationShortRepeat = 5
)
var (
monoEffects = []effectFunc{
pixeleffect.FadeOutHorizontal,
pixeleffect.FadeOutVertical,
pixeleffect.FadeOutDiagonal,
pixeleffect.FadeOutDither,
}
colorEffects = append(monoEffects,
pixeleffect.FadeOutShift,
)
)
type DisplayConfig struct {
Type string `toml:"type"`
Color string `toml:"color"`
ClockOnTop bool `toml:"clock_on_top"`
}
var defaultDisplayConfig = &DisplayConfig{
ClockOnTop: true,
}
type Display struct {
driver.Matrix
config *DisplayConfig
bounds image.Rectangle
buffer draw.Image
layers [4]draw.Image
animationLayer int
animationFrame draw.Image
clockLayer int
color color.RGBA
mask image.Image
update chan displayUpdateLayer
render chan struct{}
}
type displayUpdateLayer struct {
layer int
image image.Image
mask image.Image
}
func newDisplayUpdateLayer(layer int, image, mask image.Image) displayUpdateLayer {
return displayUpdateLayer{
layer: layer,
image: image,
mask: mask,
}
}
func New(config *DisplayConfig, driverConfig *driver.Config) (*Display, error) {
if config.Type == "" {
log.Printf("no display type configured, using text")
config.Type = "text"
}
matrix, err := driver.Load(config.Type, driverConfig)
if err != nil {
return nil, err
}
var (
bits = matrix.ColorDepth()
size = matrix.Bounds().Size()
)
// log.Printf("display: using %dx%d driver %s", size.X, size.Y, matrix.Name())
d := &Display{
Matrix: matrix,
config: config,
bounds: image.Rectangle{Max: size},
buffer: newLayer(size, bits),
layers: [4]draw.Image{
newLayer(size, bits), // Background
newLayer(size, bits), // Clock background
newLayer(size, bits), // Animation
newLayer(size, bits), // Clock foreground
},
animationLayer: layerAnimation,
animationFrame: newLayer(size, bits),
clockLayer: layerClockForeground,
color: colornames.Green,
mask: image.NewUniform(colornames.Green),
update: make(chan displayUpdateLayer, 64),
render: make(chan struct{}, 1),
}
fillLayer(d.layers[layerBackground], color.Black)
go d.loop()
return d, nil
}
func newLayer(size image.Point, bpp int) draw.Image {
switch bpp {
case 2:
return pixel.NewBitmap(size.X, size.Y)
default:
return image.NewRGBA(image.Rectangle{Max: size})
}
}
var zeros = make([]byte, 128*32*4)
func clearLayer(i draw.Image) {
switch i := i.(type) {
case *image.RGBA:
copy(i.Pix, zeros)
case *pixel.Bitmap:
copy(i.Pix, zeros)
default:
draw.Draw(i, i.Bounds(), image.Transparent, image.Point{}, draw.Src)
}
}
func fillLayer(i draw.Image, c color.Color) {
switch i := i.(type) {
case *image.RGBA:
var (
rm, gm, bm, am = c.RGBA()
r = uint8(rm)
g = uint8(gm)
b = uint8(bm)
a = uint8(am)
)
for n := 0; n < len(i.Pix); n += 4 {
i.Pix[n+0] = r
i.Pix[n+1] = g
i.Pix[n+2] = b
i.Pix[n+3] = a
}
default:
draw.Draw(i, i.Bounds(), &image.Uniform{C: c}, image.Point{}, draw.Src)
}
}
func drawLayer(dst draw.Image, src, mask image.Image) {
switch dst := dst.(type) {
case *image.RGBA:
switch src := src.(type) {
case *image.RGBA:
copy(dst.Pix, src.Pix)
return
}
case *pixel.Bitmap:
switch src := src.(type) {
case *pixel.Bitmap:
copy(dst.Pix, src.Pix)
return
}
}
if mask != nil {
draw.DrawMask(dst, dst.Bounds(), mask, image.Point{}, src, image.Point{}, draw.Src)
} else {
draw.Draw(dst, dst.Bounds(), src, image.Point{}, draw.Src)
}
}
func convertPalette(p color.Palette) []driver.Color {
o := make([]driver.Color, len(p))
for i, c := range p {
o[i] = driver.ToColor(c)
}
return o
}
func (d *Display) loop() {
for update := range d.update {
d.renderLayer(update.layer, update.image, update.mask)
}
}
func (d *Display) renderLayer(layer int, frame, mask image.Image) {
if frame == nil {
return
}
// log.Printf("update: %T %s", frame, frame.Bounds())
defer func() {
if err := recover(); err != nil {
log.Printf("update failed: %T %v", err, err)
}
}()
drawLayer(d.layers[layer], frame, mask)
d.doRender()
}
func (d *Display) Render() error {
select {
case d.render <- struct{}{}:
default:
}
return nil
}
func (d *Display) doRender() {
// log.Println("display: rendering")
//copy(d.buffer.Pix, d.layers[0].Pix)
clearLayer(d.buffer)
if d.clockLayer == layerClockBackground {
draw.Draw(d.buffer, d.bounds, d.layers[d.clockLayer], image.Point{}, draw.Over)
}
draw.Draw(d.buffer, d.bounds, d.layers[d.animationLayer], image.Point{}, draw.Over)
if d.clockLayer == layerClockForeground {
draw.Draw(d.buffer, d.bounds, d.layers[d.clockLayer], image.Point{}, draw.Over)
}
draw.Draw(d.Matrix, d.bounds, d.buffer, image.Point{}, draw.Src)
//draw.Draw(d.Matrix, d.Bounds(), d.layers[layerAnimation], image.Point{}, draw.Src)
d.Matrix.Render()
}
func (d *Display) RenderBackground(back *image.RGBA) {
drawLayer(d.layers[layerBackground], back, nil)
}
func (d *Display) RenderClock(frames <-chan *image.RGBA) {
for frame := range frames {
select {
case d.update <- newDisplayUpdateLayer(d.clockLayer, frame, nil):
// log.Println("display: update clock frame")
default:
log.Println("display: update buffer full, could not update clock!")
}
}
}
type effectFunc func(time.Duration, pixel.Image) <-chan time.Time
func (d *Display) RenderAnimation(a dmd.Animation) error {
//log.Printf("display: render animation %q for %s", a.Name(), a.Duration())
if m, ok := d.Matrix.(*text.Matrix); ok {
m.Title = fmt.Sprintf("%s (%s)", a.Name(), a.Duration())
}
repeat := 1
if a.Duration() <= animationShort {
repeat = animationShortRepeat
}
// Swap clock layers (maybe).
if !d.config.ClockOnTop {
clockLayer := layerClockBackground
if a.ClockInFront() {
clockLayer = layerClockForeground
}
if d.clockLayer != clockLayer {
drawLayer(d.layers[clockLayer], d.layers[d.clockLayer], nil)
clearLayer(d.layers[d.clockLayer])
d.clockLayer = clockLayer
}
}
var mask image.Image
if a.IsMask() {
mask = d.mask
}
var lastFrame image.Image
for i := 0; i < repeat; i++ {
if err := a.SeekFrame(0); err != nil {
return err
}
var (
frame image.Image
delay time.Duration
err error
)
for {
if frame, delay, err = a.NextFrame(); err == io.EOF {
break
} else if err != nil {
// log.Printf("display: animation error: %v", err)
return err
}
//log.Printf("display: animation frame %T of %s", frame, frame.Bounds())
wait := time.After(delay)
select {
case d.update <- newDisplayUpdateLayer(layerAnimation, frame, mask):
default:
log.Println("display: frame drop on animation; update buffer full")
}
<-wait
lastFrame = frame
}
}
if lastFrame != nil {
d.showEffect(lastFrame, mask)
}
if m, ok := d.Matrix.(*text.Matrix); ok {
m.Title = ""
}
return nil
}
func (d *Display) showEffect(frame, mask image.Image) {
draw.Draw(d.animationFrame, d.bounds, frame, image.Point{}, draw.Src)
var effect effectFunc
switch d.Matrix.ColorDepth() {
case 2:
// Monochrome fade out.
effect = monoEffects[rand.Intn(len(monoEffects))]
default:
effect = colorEffects[rand.Intn(len(colorEffects))]
}
for range effect(animationFadeOut, d.animationFrame) {
d.update <- newDisplayUpdateLayer(layerAnimation, d.animationFrame, mask)
}
clearLayer(d.animationFrame) // TODO (maze): is this required?
d.update <- newDisplayUpdateLayer(layerAnimation, d.animationFrame, mask)
}