Files
wehub-resource-sync ead81af521
Deploy to GitHub Pages / deploy (push) Failing after 0s
CI / go (push) Has been cancelled
CI / build (darwin, macos-14) (push) Has been cancelled
CI / build (windows, windows-2025) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:31:13 +08:00

152 lines
3.7 KiB
Go
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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")
}