116 lines
3.3 KiB
Go
116 lines
3.3 KiB
Go
package ui
|
||
|
||
import (
|
||
"math"
|
||
"strings"
|
||
)
|
||
|
||
// logoGlyphs holds 5×7 pixel bitmaps for each letter in "CLIAMP".
|
||
// Each row is 5 bits wide; bit 4 (0x10) is the leftmost pixel.
|
||
var logoGlyphs = [6][7]uint8{
|
||
{0x0E, 0x10, 0x10, 0x10, 0x10, 0x10, 0x0E}, // C
|
||
{0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x1F}, // L
|
||
{0x1F, 0x04, 0x04, 0x04, 0x04, 0x04, 0x1F}, // I
|
||
{0x0E, 0x11, 0x11, 0x1F, 0x11, 0x11, 0x11}, // A
|
||
{0x11, 0x1B, 0x15, 0x11, 0x11, 0x11, 0x11}, // M
|
||
{0x1E, 0x11, 0x11, 0x1E, 0x10, 0x10, 0x10}, // P
|
||
}
|
||
|
||
const (
|
||
logoLetterW = 5 // pixel columns per glyph
|
||
logoLetterH = 7 // pixel rows per glyph
|
||
logoNumLetters = 6
|
||
logoGap = 2 // pixel gap between letters
|
||
logoTotalW = logoNumLetters*logoLetterW + (logoNumLetters-1)*logoGap // 40
|
||
)
|
||
|
||
// renderLogo draws "CLIAMP" in pixel art using Braille dots. Individual dots
|
||
// within each letter appear and disappear based on the associated frequency
|
||
// band's energy — loud passages fill the text solid, silence dissolves it
|
||
// into scattered pixels. A gentle bounce and wave keep things alive.
|
||
func (v *Visualizer) renderLogo(bands []float64) string {
|
||
height := v.Rows
|
||
dotRows := height * 4
|
||
dotCols := PanelWidth * 2
|
||
|
||
grid := make([]bool, dotRows*dotCols)
|
||
|
||
// Scale letters to fill the panel (75% of height for bounce headroom).
|
||
scaleX := dotCols / logoTotalW
|
||
scaleY := (dotRows * 3 / 4) / logoLetterH
|
||
if scaleX < 1 {
|
||
scaleX = 1
|
||
}
|
||
if scaleY < 1 {
|
||
scaleY = 1
|
||
}
|
||
|
||
renderedW := logoTotalW * scaleX
|
||
renderedH := logoLetterH * scaleY
|
||
offsetX := (dotCols - renderedW) / 2
|
||
baseOffsetY := (dotRows - renderedH) / 2
|
||
|
||
// Map 6 letters across the 10 frequency bands.
|
||
letterBand := [6]int{0, 2, 4, 5, 7, 9}
|
||
|
||
for li := range logoNumLetters {
|
||
energy := bands[letterBand[li]]
|
||
|
||
// Gentle traveling wave for life during silence + subtle bounce.
|
||
wave := math.Sin(float64(v.frame)*0.06+float64(li)*0.9) * 1.5
|
||
bounce := int(energy*float64(baseOffsetY)*0.3 + wave)
|
||
|
||
letterX := offsetX + li*(logoLetterW+logoGap)*scaleX
|
||
letterY := baseOffsetY - bounce
|
||
|
||
for py := range logoLetterH {
|
||
row := logoGlyphs[li][py]
|
||
for px := range logoLetterW {
|
||
if row&(1<<(logoLetterW-1-px)) == 0 {
|
||
continue
|
||
}
|
||
|
||
// Stamp each glyph pixel as a scaled block of dots.
|
||
// Each dot's visibility is gated by energy — loud fills
|
||
// the text solid, silence dissolves it to scattered pixels.
|
||
fill := energy*energy*0.75 + 0.15
|
||
for sy := range scaleY {
|
||
for sx := range scaleX {
|
||
dx := letterX + px*scaleX + sx
|
||
dy := letterY + py*scaleY + sy
|
||
if dx < 0 || dx >= dotCols || dy < 0 || dy >= dotRows {
|
||
continue
|
||
}
|
||
|
||
if scatterHash(li, py*scaleY+sy, px*scaleX+sx, v.frame) > fill {
|
||
continue
|
||
}
|
||
|
||
grid[dy*dotCols+dx] = true
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// Convert dot grid to Braille characters.
|
||
lines := make([]string, height)
|
||
for row := range height {
|
||
var content strings.Builder
|
||
for ch := range PanelWidth {
|
||
var braille rune = '\u2800'
|
||
for dr := range 4 {
|
||
for dc := range 2 {
|
||
if grid[(row*4+dr)*dotCols+ch*2+dc] {
|
||
braille |= brailleBit[dr][dc]
|
||
}
|
||
}
|
||
}
|
||
content.WriteRune(braille)
|
||
}
|
||
lines[row] = specWrap(float64(height-1-row)/float64(height), content.String())
|
||
}
|
||
|
||
return strings.Join(lines, "\n")
|
||
}
|