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

1 year ago
1 year ago
  1. package matrix
  2. import (
  3. "image"
  4. "image/color"
  5. "image/draw"
  6. "strings"
  7. "time"
  8. "golang.org/x/image/colornames"
  9. "golang.org/x/image/font/gofont/gomono"
  10. )
  11. var defaultClockConfig = &ClockConfig{
  12. Format: "15:04:05",
  13. Font: "data/font/quadrit.ttf",
  14. FontSize: 10,
  15. Blink: true,
  16. Alpha: 0xff,
  17. }
  18. type ClockConfig struct {
  19. Format string `toml:"format"`
  20. Font string `toml:"font"`
  21. FontSize int `toml:"font_size"`
  22. Blink bool `toml:"blink"`
  23. Slide bool `toml:"slide"`
  24. Color string `toml:"color"`
  25. Brightness uint16 `toml:"brightness"`
  26. Alpha uint8 `toml:"alpha"`
  27. }
  28. type Clock struct {
  29. config *ClockConfig
  30. buffer *image.RGBA
  31. bounds image.Rectangle
  32. font Font
  33. fontSize image.Point
  34. color color.RGBA
  35. glyphs map[rune]*image.RGBA
  36. colorStep uint8
  37. }
  38. func NewClock(config *ClockConfig) (*Clock, error) {
  39. var c = colornames.Gray
  40. if config.Color != "" {
  41. var err error
  42. if c, err = parseColor(config.Color); err != nil {
  43. return nil, err
  44. }
  45. }
  46. c.A = config.Alpha
  47. if config.Brightness == 0 || config.Brightness >= 0x100 {
  48. config.Brightness = 0x100
  49. }
  50. c = adjustBrightness(c, uint32(config.Brightness))
  51. var (
  52. f Font
  53. err error
  54. )
  55. if config.Font == "" {
  56. f, _ = UseTTFFont(gomono.TTF, 16)
  57. } else if f, err = LoadTTFFont(config.Font, config.FontSize); err != nil {
  58. return nil, err
  59. }
  60. return &Clock{
  61. config: config,
  62. buffer: newLayer(image.Pt(128, 32), 8).(*image.RGBA),
  63. bounds: image.Rect(0, 0, 128, 32),
  64. font: f,
  65. fontSize: f.Size(),
  66. color: c,
  67. glyphs: make(map[rune]*image.RGBA),
  68. }, nil
  69. }
  70. func (c *Clock) Frames() <-chan *image.RGBA {
  71. frames := make(chan *image.RGBA)
  72. if c.config.Slide {
  73. go c.slide(frames)
  74. } else {
  75. go c.tick(frames)
  76. }
  77. return frames
  78. }
  79. func (c *Clock) tick(frames chan<- *image.RGBA) {
  80. ticker := time.NewTicker(time.Second)
  81. defer ticker.Stop()
  82. for {
  83. var (
  84. now = <-ticker.C
  85. text = now.Format(c.config.Format)
  86. offset = image.Pt(0, (32-c.fontSize.Y)/2)
  87. )
  88. if c.config.Blink && now.Second()&1 == 1 {
  89. text = strings.Replace(text, ":", " ", -1)
  90. }
  91. for _, r := range text {
  92. //if g := c.font.Glyph(r); g != nil {
  93. if g := c.glyph(r); g != nil {
  94. //draw.DrawMask(c.buffer, c.bounds.Add(offset), c.mask, image.Point{}, g, image.Point{}, draw.Src)
  95. if r == ':' {
  96. offset.X += 8
  97. }
  98. draw.Draw(c.buffer, c.bounds.Add(offset), g, image.Point{}, draw.Src)
  99. if r == ':' {
  100. offset.X -= 8
  101. }
  102. }
  103. offset.X += c.fontSize.X
  104. }
  105. frames <- c.buffer
  106. }
  107. }
  108. func (c *Clock) slide(frames chan<- *image.RGBA) {
  109. // Render at 25 fps
  110. ticker := time.NewTicker(time.Millisecond * 40)
  111. defer ticker.Stop()
  112. var (
  113. last string
  114. step = int(c.fontSize.Y / 10) // 20 fps, fall in the first half second
  115. fall []int
  116. empty = image.NewUniform(color.Transparent)
  117. )
  118. if step == 0 {
  119. step = 1
  120. }
  121. var prev = time.Now()
  122. for {
  123. var (
  124. now = <-ticker.C
  125. text = now.Format(c.config.Format)
  126. offset = image.Pt((128-len(text)*c.fontSize.X)/2, (32-c.fontSize.Y)/2)
  127. )
  128. if c.config.Blink && now.Nanosecond() > int(time.Second/2/time.Nanosecond) {
  129. text = strings.Replace(text, ":", " ", -1)
  130. }
  131. if last == "" {
  132. last = text
  133. fall = make([]int, len(last))
  134. } else {
  135. prev := []rune(last)
  136. for i, r := range text {
  137. if r != prev[i] && r != ':' && r != ' ' {
  138. //fall[i] = c.fontSize.Y
  139. fall[i] = 32 - c.fontSize.Y
  140. }
  141. }
  142. }
  143. // Clear the line below if we're falling.
  144. for _, i := range fall {
  145. if i > 0 {
  146. y := offset.Y + c.fontSize.Y - i
  147. draw.Draw(c.buffer, image.Rect(0, y-1, 128, y+1), empty, image.Point{}, draw.Src)
  148. break
  149. }
  150. }
  151. drawing:
  152. for i, r := range text {
  153. //if g := c.font.Glyph(r); g != nil {
  154. if g := c.glyph(r); g != nil {
  155. // c.colorize(g)
  156. if r == ':' && c.config.Blink && now.Sub(prev) > 500*time.Millisecond {
  157. draw.Draw(c.buffer, c.bounds.Add(offset), empty, image.Point{}, draw.Src)
  158. offset.X += c.fontSize.X
  159. continue drawing
  160. }
  161. draw.Draw(c.buffer, c.bounds.Add(offset).Add(image.Pt(0, -fall[i])), g, image.Point{}, draw.Src)
  162. }
  163. offset.X += c.fontSize.X
  164. if fall[i] > 0 {
  165. fall[i] -= step
  166. if fall[i] < 0 {
  167. fall[i] = 0
  168. }
  169. }
  170. }
  171. frames <- c.buffer
  172. last = text
  173. prev = now
  174. // c.advanceColor()
  175. }
  176. }
  177. func (c *Clock) glyph(r rune) image.Image {
  178. if g, ok := c.glyphs[r]; ok {
  179. return g
  180. }
  181. g := c.font.Glyph(r)
  182. if g != nil {
  183. // Colorize image.
  184. for o := 0; o < len(g.Pix); o += 4 {
  185. if g.Pix[o+3] != 0 {
  186. g.Pix[o+0] = c.color.R
  187. g.Pix[o+1] = c.color.G
  188. g.Pix[o+2] = c.color.B
  189. g.Pix[o+3] = c.color.A
  190. }
  191. }
  192. }
  193. c.glyphs[r] = g
  194. return g
  195. }