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

115 lines
3.4 KiB
Go

package ui
import (
"math"
"strings"
)
// renderFirework draws firework bursts using Braille dots. Each burst launches
// from the bottom with a rising trail, then explodes into a sphere of particles
// that drift downward with gravity and fade. Audio energy drives the number of
// simultaneous bursts and the size of each explosion.
func (v *Visualizer) renderFirework(bands []float64) string {
height := v.Rows
dotRows := height * 4
dotCols := PanelWidth * 2
if dotRows < 4 || dotCols < 4 {
return strings.Repeat("\n", max(0, height-1))
}
grid := make([]bool, dotRows*dotCols)
var totalEnergy float64
for _, e := range bands {
totalEnergy += e
}
avgEnergy := totalEnergy / float64(len(bands))
// Number of simultaneous firework bursts: 5 quiet, up to 14 loud.
numBursts := 5 + int(avgEnergy*9)
cycleLen := uint64(48)
launchLen := uint64(10)
for i := range numBursts {
// Seed changes each cycle so bursts appear in new positions.
cycle := (v.frame + uint64(i)*7) / cycleLen
seed := cycle*104729 + uint64(i)*7919
// Stagger starts so bursts don't all fire simultaneously.
offset := (uint64(i)*cycleLen/uint64(numBursts) + (seed/3)%5)
localFrame := (v.frame + offset) % cycleLen
// Burst center — spread across the panel, upper portion.
cx := int((seed * 6271) % uint64(dotCols))
cy := int((seed*4391)%uint64(dotRows/2)) + dotRows/8
// Associated band for energy-driven sizing.
bandIdx := int(seed % uint64(len(bands)))
energy := bands[bandIdx]
if localFrame < launchLen {
// Rising trail from bottom to burst center.
progress := float64(localFrame) / float64(launchLen)
trailY := dotRows - 1 - int(float64(dotRows-1-cy)*progress)
// Short trail of a few dots.
for dy := range 4 {
ty := trailY + dy
if ty >= 0 && ty < dotRows && cx >= 0 && cx < dotCols {
grid[ty*dotCols+cx] = true
}
}
} else {
// Burst expansion and fade.
burstT := float64(localFrame-launchLen) / float64(cycleLen-launchLen)
maxRadius := 3.0 + energy*8.0
// Fast expansion, then slow drift.
radius := maxRadius * math.Min(burstT*3.0, 1.0)
// Gravity pulls particles down over time.
gravity := burstT * burstT * 5.0
// Particles fade out over time.
fade := max(0.0, 1.0-burstT*1.3)
numParticles := 18 + int(energy*18)
for p := range numParticles {
angle := float64(p) / float64(numParticles) * 2 * math.Pi
pSeed := seed + uint64(p)*2909
speed := 0.6 + float64(pSeed%400)/1000.0
px := cx + int(math.Cos(angle)*radius*speed)
py := cy + int(math.Sin(angle)*radius*speed+gravity)
// Stochastic fade — more particles disappear as time passes.
if scatterHash(bandIdx, p, int(seed%100), v.frame) > fade {
continue
}
if px >= 0 && px < dotCols && py >= 0 && py < dotRows {
grid[py*dotCols+px] = true
}
}
}
}
// Convert dot grid to Braille characters with row-based spectrum color.
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)
}
// Top rows bright (red), bottom dimmer (green) — fireworks in a night sky.
lines[row] = specWrap(float64(height-1-row)/float64(height), content.String())
}
return strings.Join(lines, "\n")
}