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") }