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

129 lines
3.4 KiB
Go

package ui
import "time"
// geyserDriver draws a particle fountain rooted at the bottom of the panel.
// Sustained loudness keeps a steady column of mist, bass transients launch
// strong vertical jets, and every particle then arcs back down under gravity
// with a touch of lateral spray. Particles inherit a tier from the band that
// produced them, so dense bass passages paint the column red and treble
// embellishments add green sparkles to the canopy.
type geyserDriver struct {
grid brailleGrid
particles []geyserParticle
rng uint64
prevBass float64
}
type geyserParticle struct {
x, y float64
vx, vy float64
tier int8
life int
}
func newGeyserDriver() visModeDriver { return &geyserDriver{rng: 0xFEED5EED} }
func (*geyserDriver) AnalysisSpec(*Visualizer) VisAnalysisSpec {
return spectrumAnalysisSpec(DefaultSpectrumBands)
}
func (d *geyserDriver) Tick(v *Visualizer, ctx VisTickContext) {
defaultDriverTick(v, ctx, d.AnalysisSpec(v))
if ctx.OverlayActive {
return
}
dotRows, dotCols := v.Rows*4, PanelWidth*2
if dotRows < 4 || dotCols < 4 {
return
}
d.grid.ensure(dotRows, dotCols)
d.grid.clear()
bands := v.SmoothedBands()
if len(bands) == 0 {
return
}
bass := bandAvg(bands, 0, max(1, len(bands)/3))
mid := bandAvg(bands, len(bands)/3, 2*len(bands)/3)
high := bandAvg(bands, 2*len(bands)/3, len(bands))
delta := bass - d.prevBass
d.prevBass = bass
jetX := dotCols / 2
jetSpread := max(2, dotCols/16)
// Steady drizzle: spawn rate scales with overall loudness so quiet passages
// idle a thin trickle and loud passages keep a column going. Bass weights
// most heavily so a heavy bassline alone keeps the column flowing.
steady := bass*0.85 + mid*0.25 + high*0.08
for i := 0; i < int(steady*6); i++ {
d.spawn(jetX, dotRows-1, jetSpread, 1.5+steady*4.5, &bass, &mid, &high)
}
// Transient kick: shoot a thick burst. Triggers on smaller deltas now so
// even gentler kick drums register.
if delta > 0.06 && bass > 0.15 {
burst := 40 + int(delta*180)
for i := 0; i < burst; i++ {
d.spawn(jetX, dotRows-1, jetSpread*2, 4.5+delta*10.0+bass*4.0, &bass, &mid, &high)
}
}
// Advance particles.
const gravity = 0.30
const drag = 0.992
live := d.particles[:0]
for _, p := range d.particles {
p.vy += gravity
p.vx *= drag
p.x += p.vx
p.y += p.vy
p.life++
ix, iy := int(p.x), int(p.y)
if iy >= dotRows || ix < 0 || ix >= dotCols || p.life > 200 {
continue
}
if iy < 0 {
iy = 0
}
d.grid.set(ix, iy, p.tier)
live = append(live, p)
}
d.particles = live
}
func (d *geyserDriver) spawn(x, y, spread int, vy float64, bass, mid, high *float64) {
jx := x + int(rng64(&d.rng)*float64(2*spread+1)) - spread
vyJitter := vy * (0.6 + rng64(&d.rng)*0.5)
vxJitter := (rng64(&d.rng) - 0.5) * (1.0 + vy*0.4)
r := rng64(&d.rng)
var tier int8 = 1
switch {
case r < *bass:
tier = 3
case r < *bass+*mid:
tier = 2
default:
_ = high
}
d.particles = append(d.particles, geyserParticle{
x: float64(jx), y: float64(y),
vx: vxJitter, vy: -vyJitter,
tier: tier,
})
}
func (*geyserDriver) TickInterval(_ *Visualizer, ctx VisTickContext) time.Duration {
return defaultDriverTickInterval(ctx)
}
func (d *geyserDriver) OnEnter(*Visualizer) {
d.grid = brailleGrid{}
d.particles = nil
d.prevBass = 0
}
func (*geyserDriver) OnLeave(*Visualizer) {}
func (d *geyserDriver) Render(v *Visualizer) string {
return d.grid.render(v.Rows)
}