@@ -25,6 +25,7 @@ const (
2525 VisWave // braille waveform oscilloscope
2626 VisScatter // braille particle sparkle
2727 VisFlame // braille rising flame tendrils
28+ VisRetro // 80s synthwave perspective grid with wave
2829 VisNone // hidden — no visualizer
2930 visCount // sentinel for cycling
3031)
@@ -88,6 +89,8 @@ func (v *Visualizer) ModeName() string {
8889 return "Scatter"
8990 case VisFlame :
9091 return "Flame"
92+ case VisRetro :
93+ return "Retro"
9194 case VisNone :
9295 return "None"
9396 default :
@@ -189,6 +192,8 @@ func (v *Visualizer) Render(bands [numBands]float64) string {
189192 return v .renderScatter (bands )
190193 case VisFlame :
191194 return v .renderFlame (bands )
195+ case VisRetro :
196+ return v .renderRetro (bands )
192197 case VisNone :
193198 return ""
194199 default :
@@ -523,6 +528,177 @@ func (v *Visualizer) renderFlame(bands [numBands]float64) string {
523528 return strings .Join (lines , "\n " )
524529}
525530
531+ // renderRetro draws a retro 80s synthwave scene: a striped setting sun above
532+ // the horizon, a smooth audio-reactive wave, and a perspective grid floor that
533+ // scrolls toward the viewer. Uses Braille characters for sub-cell resolution.
534+ func (v * Visualizer ) renderRetro (bands [numBands ]float64 ) string {
535+ height := v .Rows
536+ const charCols = 69
537+ dotRows := height * 4
538+ dotCols := charCols * 2
539+
540+ // Horizon at 40% from top — gives room for wave and sun above.
541+ horizonDot := dotRows * 2 / 5
542+ if horizonDot < 2 {
543+ horizonDot = 2
544+ }
545+ floorRows := dotRows - horizonDot
546+ centerX := float64 (dotCols - 1 ) / 2.0
547+
548+ // Dot types: 0=empty, 1=grid, 2=wave, 3=sun.
549+ grid := make ([][]byte , dotRows )
550+ for i := range grid {
551+ grid [i ] = make ([]byte , dotCols )
552+ }
553+
554+ // ── SUN ── striped semicircle above horizon.
555+ sunR := float64 (horizonDot ) * 0.85
556+ for dy := range horizonDot {
557+ rowDist := float64 (horizonDot - dy ) // dots above horizon
558+ if rowDist > sunR {
559+ continue
560+ }
561+ halfW := math .Sqrt (sunR * sunR - rowDist * rowDist )
562+
563+ // Bottom half of sun has horizontal stripe gaps.
564+ if rowDist < sunR * 0.5 {
565+ sw := max (1 , int (sunR * 0.15 ))
566+ if (int (rowDist )/ sw )% 2 == 1 {
567+ continue
568+ }
569+ }
570+
571+ left := max (0 , int (centerX - halfW ))
572+ right := min (dotCols - 1 , int (centerX + halfW ))
573+ for dx := left ; dx <= right ; dx ++ {
574+ grid [dy ][dx ] = 3
575+ }
576+ }
577+
578+ // ── HORIZON LINE ──
579+ for dx := range dotCols {
580+ grid [horizonDot ][dx ] = 1
581+ }
582+
583+ // ── PERSPECTIVE GRID FLOOR ──
584+
585+ // Vertical lines converging to vanishing point at (centerX, horizonDot).
586+ const numVLines = 18
587+ for i := range numVLines + 1 {
588+ bottomX := float64 (i ) * float64 (dotCols - 1 ) / float64 (numVLines )
589+ for dy := horizonDot + 1 ; dy < dotRows ; dy ++ {
590+ t := float64 (dy - horizonDot ) / float64 (max (1 , floorRows - 1 ))
591+ screenX := centerX + (bottomX - centerX )* t
592+ ix := int (math .Round (screenX ))
593+ if ix >= 0 && ix < dotCols {
594+ grid [dy ][ix ] = 1
595+ }
596+ }
597+ }
598+
599+ // Horizontal lines scrolling toward the viewer.
600+ scroll := math .Mod (float64 (v .frame )* 0.08 , 1.0 )
601+ const numHLines = 10
602+ for i := range numHLines {
603+ z := (float64 (i ) + scroll ) / float64 (numHLines )
604+ if z > 1.0 {
605+ z -= 1.0
606+ }
607+ // Quadratic perspective: dense near horizon, spread near viewer.
608+ dy := horizonDot + 1 + int (z * z * float64 (max (1 , floorRows - 2 )))
609+ if dy > horizonDot && dy < dotRows {
610+ for dx := range dotCols {
611+ grid [dy ][dx ] = 1
612+ }
613+ }
614+ }
615+
616+ // ── AUDIO WAVE AT HORIZON ──
617+ waveY := make ([]int , dotCols )
618+ maxWave := float64 (horizonDot ) * 0.85
619+ for dx := range dotCols {
620+ bandF := float64 (dx ) / float64 (max (1 , dotCols - 1 )) * float64 (numBands - 1 )
621+ bi := int (bandF )
622+ frac := bandF - float64 (bi )
623+
624+ // Cosine interpolation for a smooth curve between bands.
625+ t := (1 - math .Cos (frac * math .Pi )) / 2
626+
627+ var level float64
628+ if bi >= numBands - 1 {
629+ level = bands [numBands - 1 ]
630+ } else {
631+ level = bands [bi ]* (1 - t ) + bands [bi + 1 ]* t
632+ }
633+
634+ // Small floor so the wave never fully vanishes.
635+ level = max (0.03 , level )
636+
637+ wy := horizonDot - int (level * maxWave )
638+ waveY [dx ] = max (0 , min (dotRows - 1 , wy ))
639+ }
640+
641+ // Draw wave with continuous line connections.
642+ for dx := range dotCols {
643+ y := waveY [dx ]
644+ grid [y ][dx ] = 2
645+ if dx > 0 {
646+ lo , hi := min (y , waveY [dx - 1 ]), max (y , waveY [dx - 1 ])
647+ for fy := lo ; fy <= hi ; fy ++ {
648+ grid [fy ][dx ] = 2
649+ }
650+ }
651+ }
652+
653+ // ── RENDER BRAILLE ──
654+ lines := make ([]string , height )
655+ for row := range height {
656+ var sb strings.Builder
657+ base := row * 4
658+
659+ for ch := range charCols {
660+ var braille rune = '\u2800'
661+ colBase := ch * 2
662+ hasWave , hasSun := false , false
663+
664+ for dr := range 4 {
665+ for dc := range 2 {
666+ dy := base + dr
667+ dx := colBase + dc
668+ if dy >= dotRows || dx >= dotCols {
669+ continue
670+ }
671+ switch grid [dy ][dx ] {
672+ case 1 :
673+ braille |= brailleBit [dr ][dc ]
674+ case 2 :
675+ braille |= brailleBit [dr ][dc ]
676+ hasWave = true
677+ case 3 :
678+ braille |= brailleBit [dr ][dc ]
679+ hasSun = true
680+ }
681+ }
682+ }
683+
684+ // Priority: wave (red) > sun (yellow) > grid (green).
685+ var style lipgloss.Style
686+ switch {
687+ case hasWave :
688+ style = specHighStyle
689+ case hasSun :
690+ style = specMidStyle
691+ default :
692+ style = specLowStyle
693+ }
694+ sb .WriteString (style .Render (string (braille )))
695+ }
696+ lines [row ] = sb .String ()
697+ }
698+
699+ return strings .Join (lines , "\n " )
700+ }
701+
526702// specStyle returns the spectrum color style for a given row height (0-1).
527703func specStyle (rowBottom float64 ) lipgloss.Style {
528704 switch {
0 commit comments