94 lines
2.4 KiB
Go
94 lines
2.4 KiB
Go
package ui
|
|
|
|
import (
|
|
"math"
|
|
"strings"
|
|
)
|
|
|
|
// renderButterfly draws a symmetric Rorschach/butterfly pattern using Braille
|
|
// dots. The spectrum bands are mirrored horizontally from the center, and
|
|
// organic variation is added via sine wobble and scatterHash to create
|
|
// ink-blot-like shapes that pulse with the music.
|
|
func (v *Visualizer) renderButterfly(bands []float64) string {
|
|
height := v.Rows
|
|
dotRows := height * 4
|
|
dotCols := PanelWidth * 2
|
|
centerX := dotCols / 2
|
|
bandCount := len(bands)
|
|
|
|
grid := make([]bool, dotRows*dotCols)
|
|
|
|
for dy := range dotRows {
|
|
// Map vertical position to a band index.
|
|
bandF := float64(dy) / float64(max(1, dotRows-1)) * float64(bandCount-1)
|
|
bi := int(bandF)
|
|
frac := bandF - float64(bi)
|
|
var energy float64
|
|
if bi >= bandCount-1 {
|
|
energy = bands[bandCount-1]
|
|
} else {
|
|
energy = bands[bi]*(1-frac) + bands[bi+1]*frac
|
|
}
|
|
|
|
// Wing width: how far from center the pattern extends.
|
|
t := float64(v.frame)*0.08 + float64(dy)*0.3
|
|
wobble := math.Sin(t) * 0.15
|
|
wingWidth := int(float64(centerX) * (energy + wobble) * 0.9)
|
|
|
|
for dx := range wingWidth {
|
|
// Distance from center normalized to wing width.
|
|
norm := float64(dx) / float64(max(1, wingWidth))
|
|
|
|
// Organic edge: denser near center, sparser at edges.
|
|
threshold := (1.0 - norm*norm) * energy
|
|
// Add frame-based flicker at the edges.
|
|
if norm > 0.6 {
|
|
threshold *= 0.5 + 0.5*math.Sin(float64(v.frame)*0.1+float64(dy)*0.5+float64(dx)*0.3)
|
|
}
|
|
|
|
if scatterHash(bi, dy, dx, v.frame/3) < threshold {
|
|
// Right wing.
|
|
rx := centerX + dx
|
|
if rx < dotCols {
|
|
grid[dy*dotCols+rx] = true
|
|
}
|
|
// Left wing (mirror).
|
|
lx := centerX - 1 - dx
|
|
if lx >= 0 {
|
|
grid[dy*dotCols+lx] = true
|
|
}
|
|
}
|
|
}
|
|
|
|
// Central spine — always drawn.
|
|
if energy > 0.05 {
|
|
grid[dy*dotCols+centerX] = true
|
|
if centerX > 0 {
|
|
grid[dy*dotCols+centerX-1] = true
|
|
}
|
|
}
|
|
}
|
|
|
|
// Render braille with row-based coloring.
|
|
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)
|
|
}
|
|
// Color gradient from top to bottom.
|
|
norm := float64(row) / float64(max(1, height-1))
|
|
lines[row] = specWrap(norm, content.String())
|
|
}
|
|
|
|
return strings.Join(lines, "\n")
|
|
}
|