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.
 
 

216 lines
4.6 KiB

package matrix
import (
"image"
"image/color"
"image/draw"
"strings"
"time"
"golang.org/x/image/colornames"
"golang.org/x/image/font/gofont/gomono"
)
var defaultClockConfig = &ClockConfig{
Format: "15:04:05",
Font: "data/font/quadrit.ttf",
FontSize: 10,
Blink: true,
Alpha: 0xff,
}
type ClockConfig struct {
Format string `toml:"format"`
Font string `toml:"font"`
FontSize int `toml:"font_size"`
Blink bool `toml:"blink"`
Slide bool `toml:"slide"`
Color string `toml:"color"`
Brightness uint16 `toml:"brightness"`
Alpha uint8 `toml:"alpha"`
}
type Clock struct {
config *ClockConfig
buffer *image.RGBA
bounds image.Rectangle
font Font
fontSize image.Point
color color.RGBA
glyphs map[rune]*image.RGBA
colorStep uint8
}
func NewClock(config *ClockConfig) (*Clock, error) {
var c = colornames.Gray
if config.Color != "" {
var err error
if c, err = parseColor(config.Color); err != nil {
return nil, err
}
}
c.A = config.Alpha
if config.Brightness == 0 || config.Brightness >= 0x100 {
config.Brightness = 0x100
}
c = adjustBrightness(c, uint32(config.Brightness))
var (
f Font
err error
)
if config.Font == "" {
f, _ = UseTTFFont(gomono.TTF, 16)
} else if f, err = LoadTTFFont(config.Font, config.FontSize); err != nil {
return nil, err
}
return &Clock{
config: config,
buffer: newLayer(image.Pt(128, 32), 8).(*image.RGBA),
bounds: image.Rect(0, 0, 128, 32),
font: f,
fontSize: f.Size(),
color: c,
glyphs: make(map[rune]*image.RGBA),
}, nil
}
func (c *Clock) Frames() <-chan *image.RGBA {
frames := make(chan *image.RGBA)
if c.config.Slide {
go c.slide(frames)
} else {
go c.tick(frames)
}
return frames
}
func (c *Clock) tick(frames chan<- *image.RGBA) {
ticker := time.NewTicker(time.Second)
defer ticker.Stop()
for {
var (
now = <-ticker.C
text = now.Format(c.config.Format)
offset = image.Pt(0, (32-c.fontSize.Y)/2)
)
if c.config.Blink && now.Second()&1 == 1 {
text = strings.Replace(text, ":", " ", -1)
}
for _, r := range text {
//if g := c.font.Glyph(r); g != nil {
if g := c.glyph(r); g != nil {
//draw.DrawMask(c.buffer, c.bounds.Add(offset), c.mask, image.Point{}, g, image.Point{}, draw.Src)
if r == ':' {
offset.X += 8
}
draw.Draw(c.buffer, c.bounds.Add(offset), g, image.Point{}, draw.Src)
if r == ':' {
offset.X -= 8
}
}
offset.X += c.fontSize.X
}
frames <- c.buffer
}
}
func (c *Clock) slide(frames chan<- *image.RGBA) {
// Render at 25 fps
ticker := time.NewTicker(time.Millisecond * 40)
defer ticker.Stop()
var (
last string
step = int(c.fontSize.Y / 10) // 20 fps, fall in the first half second
fall []int
empty = image.NewUniform(color.Transparent)
)
if step == 0 {
step = 1
}
var prev = time.Now()
for {
var (
now = <-ticker.C
text = now.Format(c.config.Format)
offset = image.Pt((128-len(text)*c.fontSize.X)/2, (32-c.fontSize.Y)/2)
)
if c.config.Blink && now.Nanosecond() > int(time.Second/2/time.Nanosecond) {
text = strings.Replace(text, ":", " ", -1)
}
if last == "" {
last = text
fall = make([]int, len(last))
} else {
prev := []rune(last)
for i, r := range text {
if r != prev[i] && r != ':' && r != ' ' {
//fall[i] = c.fontSize.Y
fall[i] = 32 - c.fontSize.Y
}
}
}
// Clear the line below if we're falling.
for _, i := range fall {
if i > 0 {
y := offset.Y + c.fontSize.Y - i
draw.Draw(c.buffer, image.Rect(0, y-1, 128, y+1), empty, image.Point{}, draw.Src)
break
}
}
drawing:
for i, r := range text {
//if g := c.font.Glyph(r); g != nil {
if g := c.glyph(r); g != nil {
// c.colorize(g)
if r == ':' && c.config.Blink && now.Sub(prev) > 500*time.Millisecond {
draw.Draw(c.buffer, c.bounds.Add(offset), empty, image.Point{}, draw.Src)
offset.X += c.fontSize.X
continue drawing
}
draw.Draw(c.buffer, c.bounds.Add(offset).Add(image.Pt(0, -fall[i])), g, image.Point{}, draw.Src)
}
offset.X += c.fontSize.X
if fall[i] > 0 {
fall[i] -= step
if fall[i] < 0 {
fall[i] = 0
}
}
}
frames <- c.buffer
last = text
prev = now
// c.advanceColor()
}
}
func (c *Clock) glyph(r rune) image.Image {
if g, ok := c.glyphs[r]; ok {
return g
}
g := c.font.Glyph(r)
if g != nil {
// Colorize image.
for o := 0; o < len(g.Pix); o += 4 {
if g.Pix[o+3] != 0 {
g.Pix[o+0] = c.color.R
g.Pix[o+1] = c.color.G
g.Pix[o+2] = c.color.B
g.Pix[o+3] = c.color.A
}
}
}
c.glyphs[r] = g
return g
}