|
| 1 | +package ui |
| 2 | + |
| 3 | +import "time" |
| 4 | + |
| 5 | +// geyserDriver draws a particle fountain rooted at the bottom of the panel. |
| 6 | +// Sustained loudness keeps a steady column of mist, bass transients launch |
| 7 | +// strong vertical jets, and every particle then arcs back down under gravity |
| 8 | +// with a touch of lateral spray. Particles inherit a tier from the band that |
| 9 | +// produced them, so dense bass passages paint the column red and treble |
| 10 | +// embellishments add green sparkles to the canopy. |
| 11 | +type geyserDriver struct { |
| 12 | + grid brailleGrid |
| 13 | + particles []geyserParticle |
| 14 | + rng uint64 |
| 15 | + prevBass float64 |
| 16 | +} |
| 17 | + |
| 18 | +type geyserParticle struct { |
| 19 | + x, y float64 |
| 20 | + vx, vy float64 |
| 21 | + tier int8 |
| 22 | + life int |
| 23 | +} |
| 24 | + |
| 25 | +func newGeyserDriver() visModeDriver { return &geyserDriver{rng: 0xFEED5EED} } |
| 26 | + |
| 27 | +func (*geyserDriver) AnalysisSpec(*Visualizer) VisAnalysisSpec { |
| 28 | + return spectrumAnalysisSpec(DefaultSpectrumBands) |
| 29 | +} |
| 30 | + |
| 31 | +func (d *geyserDriver) Tick(v *Visualizer, ctx VisTickContext) { |
| 32 | + defaultDriverTick(v, ctx, d.AnalysisSpec(v)) |
| 33 | + if ctx.OverlayActive { |
| 34 | + return |
| 35 | + } |
| 36 | + dotRows, dotCols := v.Rows*4, PanelWidth*2 |
| 37 | + if dotRows < 4 || dotCols < 4 { |
| 38 | + return |
| 39 | + } |
| 40 | + d.grid.ensure(dotRows, dotCols) |
| 41 | + d.grid.clear() |
| 42 | + |
| 43 | + bands := v.SmoothedBands() |
| 44 | + if len(bands) == 0 { |
| 45 | + return |
| 46 | + } |
| 47 | + bass := bandAvg(bands, 0, max(1, len(bands)/3)) |
| 48 | + mid := bandAvg(bands, len(bands)/3, 2*len(bands)/3) |
| 49 | + high := bandAvg(bands, 2*len(bands)/3, len(bands)) |
| 50 | + delta := bass - d.prevBass |
| 51 | + d.prevBass = bass |
| 52 | + |
| 53 | + jetX := dotCols / 2 |
| 54 | + jetSpread := max(2, dotCols/16) |
| 55 | + |
| 56 | + // Steady drizzle: spawn rate scales with overall loudness so quiet passages |
| 57 | + // idle a thin trickle and loud passages keep a column going. Bass weights |
| 58 | + // most heavily so a heavy bassline alone keeps the column flowing. |
| 59 | + steady := bass*0.85 + mid*0.25 + high*0.08 |
| 60 | + for i := 0; i < int(steady*6); i++ { |
| 61 | + d.spawn(jetX, dotRows-1, jetSpread, 1.5+steady*4.5, &bass, &mid, &high) |
| 62 | + } |
| 63 | + |
| 64 | + // Transient kick: shoot a thick burst. Triggers on smaller deltas now so |
| 65 | + // even gentler kick drums register. |
| 66 | + if delta > 0.06 && bass > 0.15 { |
| 67 | + burst := 40 + int(delta*180) |
| 68 | + for i := 0; i < burst; i++ { |
| 69 | + d.spawn(jetX, dotRows-1, jetSpread*2, 4.5+delta*10.0+bass*4.0, &bass, &mid, &high) |
| 70 | + } |
| 71 | + } |
| 72 | + |
| 73 | + // Advance particles. |
| 74 | + const gravity = 0.30 |
| 75 | + const drag = 0.992 |
| 76 | + live := d.particles[:0] |
| 77 | + for _, p := range d.particles { |
| 78 | + p.vy += gravity |
| 79 | + p.vx *= drag |
| 80 | + p.x += p.vx |
| 81 | + p.y += p.vy |
| 82 | + p.life++ |
| 83 | + ix, iy := int(p.x), int(p.y) |
| 84 | + if iy >= dotRows || ix < 0 || ix >= dotCols || p.life > 200 { |
| 85 | + continue |
| 86 | + } |
| 87 | + if iy < 0 { |
| 88 | + iy = 0 |
| 89 | + } |
| 90 | + d.grid.set(ix, iy, p.tier) |
| 91 | + live = append(live, p) |
| 92 | + } |
| 93 | + d.particles = live |
| 94 | +} |
| 95 | + |
| 96 | +func (d *geyserDriver) spawn(x, y, spread int, vy float64, bass, mid, high *float64) { |
| 97 | + jx := x + int(rng64(&d.rng)*float64(2*spread+1)) - spread |
| 98 | + vyJitter := vy * (0.6 + rng64(&d.rng)*0.5) |
| 99 | + vxJitter := (rng64(&d.rng) - 0.5) * (1.0 + vy*0.4) |
| 100 | + r := rng64(&d.rng) |
| 101 | + var tier int8 = 1 |
| 102 | + switch { |
| 103 | + case r < *bass: |
| 104 | + tier = 3 |
| 105 | + case r < *bass+*mid: |
| 106 | + tier = 2 |
| 107 | + default: |
| 108 | + _ = high |
| 109 | + } |
| 110 | + d.particles = append(d.particles, geyserParticle{ |
| 111 | + x: float64(jx), y: float64(y), |
| 112 | + vx: vxJitter, vy: -vyJitter, |
| 113 | + tier: tier, |
| 114 | + }) |
| 115 | +} |
| 116 | + |
| 117 | +func (*geyserDriver) TickInterval(_ *Visualizer, ctx VisTickContext) time.Duration { |
| 118 | + return defaultDriverTickInterval(ctx) |
| 119 | +} |
| 120 | +func (d *geyserDriver) OnEnter(*Visualizer) { |
| 121 | + d.grid = brailleGrid{} |
| 122 | + d.particles = nil |
| 123 | + d.prevBass = 0 |
| 124 | +} |
| 125 | +func (*geyserDriver) OnLeave(*Visualizer) {} |
| 126 | +func (d *geyserDriver) Render(v *Visualizer) string { |
| 127 | + return d.grid.render(v.Rows) |
| 128 | +} |
0 commit comments