129 lines
3.4 KiB
Go
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)
|
|
}
|