152 lines
3.7 KiB
Go
152 lines
3.7 KiB
Go
package ui
|
||
|
||
import (
|
||
"math"
|
||
"strings"
|
||
)
|
||
|
||
// bandAvg returns the mean of bands[lo:hi], guarded against out-of-range
|
||
// arguments. Shared by visualizers that consume "bass / mid / high" subbands
|
||
// rather than the full array.
|
||
func bandAvg(b []float64, lo, hi int) float64 {
|
||
if lo < 0 {
|
||
lo = 0
|
||
}
|
||
if hi > len(b) {
|
||
hi = len(b)
|
||
}
|
||
if hi <= lo {
|
||
return 0
|
||
}
|
||
s := 0.0
|
||
for _, x := range b[lo:hi] {
|
||
s += x
|
||
}
|
||
return s / float64(hi-lo)
|
||
}
|
||
|
||
// renderFirefly draws a meadow at dusk: a low silhouette of grass at the
|
||
// bottom and many fireflies drifting above. Each firefly traces a slow Lissajous
|
||
// curve seeded per index so they never collide rigidly. High-frequency energy
|
||
// raises the population's brightness and the chance any given firefly is "lit"
|
||
// this frame; bass tilts a gentle wind that nudges them sideways.
|
||
func (v *Visualizer) renderFirefly(bands []float64) string {
|
||
height := v.Rows
|
||
dotRows := height * 4
|
||
dotCols := PanelWidth * 2
|
||
if dotRows < 4 || dotCols < 8 {
|
||
return strings.Repeat("\n", max(0, height-1))
|
||
}
|
||
|
||
bass := bandAvg(bands, 0, len(bands)/3)
|
||
high := bandAvg(bands, 2*len(bands)/3, len(bands))
|
||
|
||
// Grass silhouette: bottom 1-2 rows, ragged edge.
|
||
grass := make([]bool, dotRows*dotCols)
|
||
for x := 0; x < dotCols; x++ {
|
||
// Pseudo-noise heights.
|
||
h := 1 + int(2.5+1.5*math.Sin(float64(x)*0.41)+1.0*math.Sin(float64(x)*0.17+2.3))
|
||
for d := 0; d < h; d++ {
|
||
y := dotRows - 1 - d
|
||
if y >= 0 {
|
||
grass[y*dotCols+x] = true
|
||
}
|
||
}
|
||
}
|
||
|
||
// Fireflies.
|
||
const numFlies = 26
|
||
wind := bass * 1.5
|
||
|
||
dim := make([]bool, dotRows*dotCols)
|
||
bright := make([]bool, dotRows*dotCols)
|
||
|
||
for i := 0; i < numFlies; i++ {
|
||
seed := uint64(i)*2246822519 + 11
|
||
// Two slightly incommensurate frequencies for Lissajous-like wandering.
|
||
fx := 0.012 + float64(seed%17)/3500.0
|
||
fy := 0.018 + float64((seed>>4)%19)/2900.0
|
||
phx := float64(seed%1000) / 1000.0 * 2 * math.Pi
|
||
phy := float64((seed>>8)%1000) / 1000.0 * 2 * math.Pi
|
||
|
||
t := float64(v.frame)
|
||
baseX := float64(dotCols/2) + math.Cos(t*fx+phx)*float64(dotCols-6)*0.45
|
||
baseY := float64(dotRows-4)*0.5 + math.Sin(t*fy+phy)*float64(dotRows-6)*0.4
|
||
x := int(baseX + wind*math.Sin(t*0.02+phx))
|
||
y := int(baseY)
|
||
if x < 0 || x >= dotCols || y < 0 || y >= dotRows-1 {
|
||
continue
|
||
}
|
||
// Skip if it would land in the grass silhouette.
|
||
if grass[y*dotCols+x] {
|
||
continue
|
||
}
|
||
|
||
// Blink: chance of being "on" depends on per-fly phase plus high band.
|
||
blinkPhase := math.Sin(t*0.18+float64(i)*1.31) * 0.5
|
||
on := blinkPhase+0.5+high*0.4 > 0.55
|
||
if !on {
|
||
// Half-brightness halo so the fly is faintly there.
|
||
dim[y*dotCols+x] = true
|
||
continue
|
||
}
|
||
|
||
bright[y*dotCols+x] = true
|
||
// Glow halo (one-dot ring).
|
||
for _, d := range [4][2]int{{-1, 0}, {1, 0}, {0, -1}, {0, 1}} {
|
||
gx := x + d[1]
|
||
gy := y + d[0]
|
||
if gx >= 0 && gx < dotCols && gy >= 0 && gy < dotRows && !grass[gy*dotCols+gx] {
|
||
dim[gy*dotCols+gx] = true
|
||
}
|
||
}
|
||
}
|
||
|
||
lines := make([]string, height)
|
||
for row := 0; row < height; row++ {
|
||
var sb, run strings.Builder
|
||
tag := -1
|
||
for col := 0; col < PanelWidth; col++ {
|
||
var braille rune = '⠀'
|
||
cellTag := -1
|
||
for dr := 0; dr < 4; dr++ {
|
||
for dc := 0; dc < 2; dc++ {
|
||
y := row*4 + dr
|
||
x := col*2 + dc
|
||
idx := y*dotCols + x
|
||
on := false
|
||
t := 0
|
||
switch {
|
||
case bright[idx]:
|
||
on = true
|
||
t = 2
|
||
case dim[idx]:
|
||
on = true
|
||
t = 1
|
||
case grass[idx]:
|
||
on = true
|
||
t = 0
|
||
}
|
||
if on {
|
||
braille |= brailleBit[dr][dc]
|
||
if t > cellTag {
|
||
cellTag = t
|
||
}
|
||
}
|
||
}
|
||
}
|
||
if cellTag < 0 {
|
||
cellTag = 0
|
||
}
|
||
if cellTag != tag {
|
||
flushStyleRun(&sb, &run, tag)
|
||
tag = cellTag
|
||
}
|
||
run.WriteRune(braille)
|
||
}
|
||
flushStyleRun(&sb, &run, tag)
|
||
lines[row] = sb.String()
|
||
}
|
||
return strings.Join(lines, "\n")
|
||
}
|