Skip to content

Instantly share code, notes, and snippets.

@Luigi-Pizzolito
Created August 4, 2025 17:57
Show Gist options
  • Select an option

  • Save Luigi-Pizzolito/90989ab192990930b654d41fc80525f8 to your computer and use it in GitHub Desktop.

Select an option

Save Luigi-Pizzolito/90989ab192990930b654d41fc80525f8 to your computer and use it in GitHub Desktop.
Parallax starfield effect implemented using text mode terminal interface with tcell.
package main
import (
"fmt"
"math"
"math/rand"
"os"
"time"
"github.com/gdamore/tcell/v2"
)
// Star represents a star with position, depth, and velocity
type Star struct {
x, y float64 // Current position
dx, dy float64 // Velocity (based on angle and depth)
depth float64 // 0.1 (close, fast) to 1.0 (far, slow)
symbol rune // '*', '+', or '.' based on depth
}
// Screensaver manages the starfield
type Screensaver struct {
screen tcell.Screen
stars []Star
numStars int
width, height int
}
// NewScreensaver initializes the screensaver
func NewScreensaver() (*Screensaver, error) {
s, err := tcell.NewScreen()
if err != nil {
return nil, err
}
if err := s.Init(); err != nil {
return nil, err
}
w, h := s.Size()
return &Screensaver{
screen: s,
numStars: 250,
stars: make([]Star, 250),
width: w,
height: h,
}, nil
}
// initStar initializes a star at a random position in the center rectangle with outward velocity
func (s *Screensaver) initStar() Star {
centerX, centerY := float64(s.width)/2, float64(s.height)/2
rectW := math.Max(2, float64(s.width)*0.2)
rectH := math.Max(2, float64(s.height)*0.3)
rectLeft := centerX - rectW/2
rectTop := centerY - rectH/2
x := rectLeft + rand.Float64()*rectW
y := rectTop + rand.Float64()*rectH
dx := x - centerX
dy := y - centerY
if dx == 0 && dy == 0 {
dx = rand.Float64() - 0.5
dy = rand.Float64() - 0.5
}
norm := math.Hypot(dx, dy)
dx /= norm
dy /= norm
depth := 0.1 + rand.Float64()*0.9
speed := 0.25 / depth
return Star{
x: x,
y: y,
dx: dx * speed,
dy: dy * speed,
depth: depth,
symbol: getSymbol(depth),
}
}
// InitStars populates initial star positions at center
func (s *Screensaver) InitStars() {
for i := 0; i < s.numStars; i++ {
s.stars[i] = s.initStar()
}
}
// getSymbol assigns '*' (close), '+' (mid), or '.' (far) based on depth
func getSymbol(depth float64) rune {
if depth < 0.4 {
return '*'
} else if depth < 0.7 {
return '+'
}
return '.'
}
// Draw renders the starfield
func (s *Screensaver) Draw() {
s.screen.Clear()
for _, star := range s.stars {
if star.x >= 0 && star.x < float64(s.width) && star.y >= 0 && star.y < float64(s.height) {
s.screen.SetContent(int(star.x), int(star.y), star.symbol, nil, tcell.StyleDefault.Foreground(tcell.ColorWhite))
}
}
// Print current terminal size at the top left
sizeStr := fmt.Sprintf("%dx%d", s.width, s.height)
for i, r := range sizeStr {
s.screen.SetContent(i+2, 1, r, nil, tcell.StyleDefault.Foreground(tcell.ColorWhite))
}
s.screen.Show()
}
// Update moves stars and checks inactivity
func (s *Screensaver) Update() {
for i := 0; i < s.numStars; i++ {
s.stars[i].x += s.stars[i].dx
s.stars[i].y += s.stars[i].dy
// Respawn at center rectangle if star leaves screen
if s.stars[i].x < 0 || s.stars[i].x >= float64(s.width) || s.stars[i].y < 0 || s.stars[i].y >= float64(s.height) {
s.stars[i] = s.initStar()
}
}
}
// Run handles the main loop
func (s *Screensaver) Run() {
rand.Seed(time.Now().UnixNano())
for {
s.Update()
s.Draw()
// Non-blocking event polling
for s.screen.HasPendingEvent() {
ev := s.screen.PollEvent()
if ev == nil {
break
}
switch tev := ev.(type) {
case *tcell.EventKey:
if tev.Key() == tcell.KeyCtrlC {
return
}
case *tcell.EventResize:
w, h := s.screen.Size()
s.width, s.height = w, h
}
}
time.Sleep(time.Second / 60) // 20 FPS
}
}
func main() {
s, err := NewScreensaver()
if err != nil {
// Print error to stderr and exit
println("Failed to initialize screensaver:", err.Error())
os.Exit(1)
}
s.InitStars()
defer s.screen.Fini()
s.Run()
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment