Terminal Animations: Starfields, Warps, and Living Backgrounds

June 21, 2026

A BBS login screen used to be a static ASCII banner. You’d connect, see a logo made of text characters, type your username and password, and that was it. No motion, no life, no atmosphere.

jterm changes that. The login screen in Phosphor BBS has a living, animated background — stars twinkling, plasma flowing, cells dividing. Every time a user connects, the system randomly selects one of six animated backgrounds and renders it behind the login form. The terminal isn’t a static page anymore. It’s a window into a small, alive world.

The AnimatedBackground Contract

Every animation in jterm implements a single interface:

public interface AnimatedBackground {
    void renderFrame(TextGraphics graphics, TerminalSize size);
    void onResize(TerminalSize newSize);
    void start();
    void stop();
    boolean isRunning();
    default int targetFps() { return 15; }
    default void tick(long nowNanos) {}
}

That’s it. Five methods, one default. The animation receives a TextGraphics buffer (a 2D grid of TextCell objects — character + foreground color + background color) and the terminal size. It draws into the buffer. The AnimationManager calls renderFrame on each tick, throttled to the animation’s target FPS.

The TRANSPARENT window hint is what makes this work. The login screen window is marked TRANSPARENT, which means jterm doesn’t fill its background with the default theme color. Instead, the animated background window renders first, and the login form draws on top — the stars show through the spaces between characters.

The Six Login Animations

The AnimationFactory randomly selects one of six animations each time a user connects:

private static final List<Supplier<AnimatedBackground>> FACTORIES = List.of(
        () -> new TwinkleStarfield(new Random(), true),
        () -> new PlasmaWash(DEFAULT_SIZE),
        () -> new StarfieldBackground(DEFAULT_SIZE),
        () -> new VoronoiCells(DEFAULT_SIZE),
        () -> new MoirePatterns(DEFAULT_SIZE),
        () -> new WarpStarfield(DEFAULT_SIZE)
);

1. Twinkle Starfield

A calm, twinkling night sky. 120 stars at fixed normalized positions, each cycling through four brightness levels via a sine wave. About 10% of stars carry a subtle color tint — blue, cyan, yellow, or red. The rest are white and gray.

private static final char[] CP437_SAFE_GLYPHS = {'.', '`', '\'', '+', '*'};
private static final char[] UNICODE_GLYPHS = {'.', '`', '\'', '✦', '✧', '★', '✱', '+', '*'};

The cp437Safe flag controls which glyph set is used. CP437 terminals (like old DOS clients) get the safe set — only ASCII characters. Unicode terminals get the fancy set with actual star glyphs. The brightness oscillation uses per-star phase and speed, so the twinkling is organic — no two stars pulse in sync:

// Each star has: normalizedX, normalizedY, glyph, baseBrightness, amplitude, speed
double brightness = baseBrightness + amplitude * Math.sin(time * speed + phase);

This is the default animation — the one most users see on their first connect. It’s gentle, readable, and doesn’t compete with the login form.

2. Plasma Wash

A full-screen color wash using classic sine-wave interference. Four waves — horizontal, vertical, diagonal, and radial — combine to create flowing plasma patterns:

private static final double X_FREQ = 0.10;
private static final double Y_FREQ = 0.15;
private static final double DIAG_FREQ = 0.08;
private static final double RADIAL_FREQ = 0.10;

The result is rendered using character-density shading — six levels from space to full block:

private static final char[] SHADING_CHARS = {' ', '.', '░', '▒', '▓', '█'};

This creates a gradient effect that’s readable behind text. The plasma shifts and flows continuously, with waves interfering to create new patterns. It’s like watching lava lamp — mesmerizing but not distracting.

3. Starfield Background

A traditional scrolling starfield — stars drift across the screen at varying speeds, creating a sense of forward motion. Unlike the Twinkle Starfield (which is static positions with brightness changes), this one actually moves. Stars wrap around the screen edges, creating an infinite field.

The theme color is configurable — cyan by default, but it can be set to any ANSI color. This is the “classic BBS” animation — the one that feels most like a 90s screensaver.

4. Voronoi Cells

Colored regions grow and shrink from random seed points that drift slowly across the screen. Where two regions meet, the boundary glows bright. Where a region’s interior is, the cells are dim.

private static final int SEED_COUNT = 8;
private static final char INTERIOR_GLYPH = '°';  // 176
private static final char BOUNDARY_GLYPH = '±';  // 177

The seeds drift, and the Voronoi diagram recomputes each frame. The effect is organic — regions expand and contract, boundaries shift, colors blend. It looks like watching cell division under a microscope, or a slowly shifting map of territories.

Eight colors from the ANSI palette cycle through the seeds, so the screen is a patchwork of red, green, yellow, blue, magenta, cyan, and bright variants.

5. Moiré Patterns

Two square dot grids rotate at different angles and speeds. Where the grids align, the dots overlap and glow bright. Where they miss, the cells fall back to dim. The slow relative rotation creates shifting interference fringes:

private static final double ANGLE_A_SPEED = 0.03;
private static final double ANGLE_B_SPEED = -0.045;
private static final char[] CHARS = {'.', '+', '#', '*'};

The Moiré effect is hypnotic — you see patterns that don’t actually exist in the individual grids. The interference fringes shift and rotate as the two grids move relative to each other. It’s a mathematical artifact made visible, and it’s beautiful.

6. Warp Starfield

The most dramatic animation. Stars stream from the center of the screen toward the viewer, getting brighter and larger as they approach. It’s the classic “warp speed” effect from science fiction — you’re hurtling through space.

public class WarpStarfield extends AbstractComponent implements AnimatedBackground {
    private static final double MIN_DEPTH = 0.1;
    private static final double MAX_DEPTH = 10.0;
    private static final double DEFAULT_WARP_SPEED = 0.075;

Each star has a 3D position (x, y, depth). The depth decreases over time, and the star’s 2D position is projected from the center of the screen with perspective scaling. As depth approaches zero, the star moves faster and gets brighter — the classic warp tunnel effect.

// Project 3D star to 2D screen position
double scale = 1.0 / star.depth;
int screenX = centerX + (int)(star.x * scale * centerX);
int screenY = centerY + (int)(star.y * scale * centerY);

When a star’s depth drops below the minimum, it’s respawned at the far edge with a new random position. The result is an infinite stream of stars hurtling past the viewer.

The AnimationManager

All six animations are driven by a single AnimationManager attached to the GUI’s render loop:

public class AnimationManager {
    private final DefaultTextGUI gui;
    private final CopyOnWriteArrayList<AnimatedBackground> backgrounds;

    public void tick(long nowNanos) {
        // Throttle to target FPS
        // For each registered background:
        //   background.tick(nowNanos);
        //   background.renderFrame(graphics, size);
        //   gui.requestScreenRefresh();
    }
}

The manager throttles frame updates to each animation’s target FPS — typically 8-15 FPS for backgrounds. BBS terminals are slow; there’s no need to render at 60fps. The animations are designed to look good at low frame rates, and the low FPS keeps CPU usage minimal even on a Raspberry Pi.

Screen Transitions

Beyond background animations, jterm also supports screen transitions — short-lived effects that play when navigating between screens. Unlike backgrounds (which loop forever), transitions run once and complete:

public interface TransitionEffect {
    long durationMs();
    void renderFrame(TextGraphics graphics, TerminalSize size, double progress);
    default int targetFps() { return 30; }
}

Four transition effects are available:

  • Fade — a top-to-bottom curtain sweep. A horizontal sweep line moves from row 0 to the last row. Above the line, the new screen is visible. Below, the old screen remains. At the sweep line, a band of progressively dimmer block characters ( ) creates a smooth fade edge. Since ANSI terminals can’t do true alpha blending, the fade uses BRIGHT_BLACK (dim gray) on block characters to simulate dimming.

  • Wipe — a left-to-right pixel wipe. The new screen reveals itself column by column, left to right.

  • Slide — the new screen slides in from a direction (left, right, top, bottom). The old screen slides out in the opposite direction.

  • Dissolve — random cells transition from old to new content. Each cell has a random threshold; when the progress value exceeds the threshold, the cell flips to the new content. The result is a “sparkling” dissolve effect.

Transitions run at 30 FPS with a typical duration of 400-600ms — fast enough to feel responsive, slow enough to be visible. They’re driven by the same render loop as the background animations, but they self-terminate when progress reaches 1.0.

Why This Matters

Terminal animation is about atmosphere. A BBS with a static login screen is functional. A BBS with a starfield or warp effect behind the login form is an experience.

The 90s BBSes that people remember weren’t the plain ones. They were the ones with ANSI art, with scrolling text, with that sense that you’d entered a place that was alive. jterm’s animation system brings that back — not as a gimmick, but as a core part of the terminal experience.

Six background animations. Four transition effects. Sprite-based animation for door games. All running on virtual threads, all rendering into the same TextCell grid, all composable with the full widget library. The terminal is alive again.