107 lines
2.9 KiB
Go
107 lines
2.9 KiB
Go
package ui
|
|
|
|
import (
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// terrainDriver draws a scrolling side-view landscape where terrain height
|
|
// is the current spectrum energy. New data enters from the right and scrolls
|
|
// left, creating a moving mountain range silhouette. Braille dots give smooth
|
|
// sub-cell edges; spectrum coloring paints green valleys, yellow slopes, red peaks.
|
|
type terrainDriver struct {
|
|
buf []float64
|
|
}
|
|
|
|
func newTerrainDriver() visModeDriver {
|
|
return &terrainDriver{}
|
|
}
|
|
|
|
func (*terrainDriver) AnalysisSpec(*Visualizer) VisAnalysisSpec {
|
|
return spectrumAnalysisSpec(DefaultSpectrumBands)
|
|
}
|
|
|
|
func resizeTerrainBuf(buf []float64, dotCols int) []float64 {
|
|
if dotCols <= 0 {
|
|
return nil
|
|
}
|
|
if len(buf) == dotCols {
|
|
return buf
|
|
}
|
|
// Ensure terrain buffer matches current panel width.
|
|
next := make([]float64, dotCols)
|
|
copyLen := min(len(buf), dotCols)
|
|
copy(next[dotCols-copyLen:], buf[len(buf)-copyLen:])
|
|
return next
|
|
}
|
|
|
|
func (d *terrainDriver) Render(v *Visualizer) string {
|
|
height := v.Rows
|
|
dotRows := height * 4
|
|
dotCols := PanelWidth * 2
|
|
buf := resizeTerrainBuf(d.buf, dotCols)
|
|
|
|
// Render: each dot column is filled from its terrain height down to the bottom.
|
|
lines := make([]string, height)
|
|
for row := range height {
|
|
var content strings.Builder
|
|
for ch := range PanelWidth {
|
|
var braille rune = '\u2800'
|
|
for dc := range 2 {
|
|
x := ch*2 + dc
|
|
terrainH := buf[x]
|
|
// Top dot position — invert so 0 is bottom.
|
|
topDot := dotRows - 1 - int(terrainH*float64(dotRows-1))
|
|
for dr := range 4 {
|
|
dotY := row*4 + dr
|
|
if dotY >= topDot {
|
|
braille |= brailleBit[dr][dc]
|
|
}
|
|
}
|
|
}
|
|
content.WriteRune(braille)
|
|
}
|
|
// Color by row height: green base, yellow middle, red peaks.
|
|
lines[row] = specWrap(float64(height-1-row)/float64(height), content.String())
|
|
}
|
|
|
|
return strings.Join(lines, "\n")
|
|
}
|
|
|
|
func (d *terrainDriver) Tick(v *Visualizer, ctx VisTickContext) {
|
|
defaultDriverTick(v, ctx, d.AnalysisSpec(v))
|
|
if ctx.OverlayActive {
|
|
return
|
|
}
|
|
|
|
dotCols := PanelWidth * 2
|
|
d.buf = resizeTerrainBuf(d.buf, dotCols)
|
|
if len(d.buf) < 2 {
|
|
return
|
|
}
|
|
|
|
// Scroll left by 2 dot columns per frame for visible movement.
|
|
copy(d.buf, d.buf[2:])
|
|
|
|
// Compute new rightmost height from average smoothed spectrum energy
|
|
// so successive scrolled columns glide instead of stepping at the FFT rate.
|
|
bands := v.SmoothedBands()
|
|
var totalEnergy float64
|
|
for _, e := range bands {
|
|
totalEnergy += e
|
|
}
|
|
avg := totalEnergy / float64(max(1, len(bands)))
|
|
|
|
// Two new columns with slight noise for organic ridge edges.
|
|
d.buf[dotCols-2] = min(1.0, avg+scatterHash(0, 0, 0, v.frame)*0.12)
|
|
d.buf[dotCols-1] = min(1.0, avg+scatterHash(0, 0, 1, v.frame)*0.12)
|
|
}
|
|
|
|
func (*terrainDriver) TickInterval(_ *Visualizer, ctx VisTickContext) time.Duration {
|
|
return defaultDriverTickInterval(ctx)
|
|
}
|
|
|
|
func (*terrainDriver) OnEnter(*Visualizer) {}
|
|
|
|
func (*terrainDriver) OnLeave(*Visualizer) {}
|