fresco

package module
v1.3.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 21, 2026 License: MIT Imports: 12 Imported by: 0

README

fresco

CI codecov Go Reference

Generative, free-running animated fields for the terminal. fresco paints the whole pane with a slow-drifting, theme-coloured procedural field — a nebula, a receding tunnel, a pool of ripples, a turning galaxy, a sky of polar curtains — as plain ANSI text. It is a pure rendering engine, not a widget: you give it a size and a frame number, it hands you a string.

frame := fresco.Render(width, height, tick, fresco.Options{
    Palette: fresco.Palette{A0: "#f7768e", A1: "#bb9af7", A2: "#7aa2f7", A3: "#7dcfff", Highlight: "#c0caf5"},
    Variant: fresco.Ripple,
})

Run go run github.com/ZviBaratz/fresco/cmd/fresco@latest to see it move — see Run it.

What it is (and isn't)

fresco renders open-ended, free-running fields: continuous procedural motion that never resolves, meant for splash screens, idle/empty states, and ambient backdrops. That's a different thing from the neighbours:

  • Not a spinner or progress indicator (briandowns/spinner).
  • Not a figlet/banner generator (go-figure) — it draws fields, not letters.
  • Not an image→ASCII converter (chafa, ascii-image-converter) — nothing is converted; the image is generated from math.
  • Not a preset effects roster that animates your text in and out. fresco's fields have no subject; they just drift.

Everything is deterministic and pure over its inputs — the same (width, height, frame, Options) always yields the same bytes — so it is snapshot-testable and trivial to drive from any render loop.

Run it

fresco is directly runnable, not only importable — it ships a screensaver binary that fills the terminal with the field and animates it full-screen:

go install github.com/ZviBaratz/fresco/cmd/fresco@latest
fresco                      # cycle the whole roster, auto-sized to your terminal

Or without installing: go run github.com/ZviBaratz/fresco/cmd/fresco@latest.

No Go toolchain? Grab a prebuilt binary for your OS from the Releases page.

It animates in the alternate screen and restores your screen, scrollback, and cursor on exit — including on Ctrl-C. A few things to try:

fresco --variant ripple           # pin one field
fresco --palette nord --fps 60    # a preset palette, faster
fresco --duration 30s             # run for half a minute, then exit
fresco --list                     # list variants and palettes
fresco --once > frame.txt         # capture a single frame (also what a pipe/CI gets)

Keys: q or Ctrl-C quit · space next variant · p pause. NO_COLOR is honoured, and off a TTY (a pipe, a redirect, CI) fresco renders one frame and exits instead of animating. Run fresco -h for the full flag list.

Install

go get github.com/ZviBaratz/fresco

Requires Go 1.25+. Depends only on the Charm colour stack (lipgloss, x/ansi) and go-colorful.

The variants

Variant What it is
fresco.Rain Matrix-style digital rain — per-column streams with bright heads and fading tails, layered at four depths. Shades by luminance alone.
fresco.Tunnel A textured wall flying past a vanishing point — screen position maps to (depth, angle), z-fog carries distance in luminance, hue bands by depth.
fresco.Ripple Drops falling on a dark pool — each flashes where it lands and expands into a hue-shifting ring; rings interfere where they cross.
fresco.Galaxy An inclined spiral turning on the focal point — arms studded with star-forming knots, a bright core, an occluding dust lane for depth.
fresco.Aurora Northern-lights curtains over dark sky — tall vertical drapes that snake and drift sideways, the hue sliding warm↔cool with altitude.

fresco.Variants() returns them all (the natural rotation order); fresco.ParseVariant("ripple") resolves a name.

Quickstart: an animation loop

package main

import (
	"fmt"
	"time"

	"github.com/ZviBaratz/fresco"
)

func main() {
	pal := fresco.Palette{A0: "#f7768e", A1: "#bb9af7", A2: "#7aa2f7", A3: "#7dcfff", Highlight: "#c0caf5"}
	for frame := 0; ; frame++ {
		fmt.Printf("\x1b[H%s", fresco.Render(96, 30, frame, fresco.Options{
			Palette: pal,
			Variant: fresco.Galaxy,
		}))
		time.Sleep(time.Second / 30)
	}
}

See cmd/fresco for the full screensaver CLI built on this loop — auto-sizing, resize handling, single-key controls, and a terminal restore on every exit path.

API

Render(w, h, frame int, opts Options) string — the whole surface. It returns exactly h lines of exactly w visible cells (or "" for a degenerate pane).

For a per-frame render loop, AppendRender(dst []byte, w, h, frame int, opts Options) []byte appends the same bytes to a buffer you own, so you can reuse it across frames (buf = fresco.AppendRender(buf[:0], w, h, frame, opts)) instead of allocating a fresh string each tick. Render is a thin wrapper over it and the two are byte-identical.

type Options struct {
	Palette  Palette      // the five colour anchors (required for colour)
	Variant  Variant      // Rain, Tunnel, Ripple, Galaxy, or Aurora
	FocalRow int          // the row the field emanates from; negative = centre
	LumRange *float64     // optional override for the density↔luminance split
	Profile  ColorProfile // colour depth; the zero value Auto = auto-detect
}

type Palette struct {
	A0, A1, A2, A3 string // "#rrggbb" warm→cool gradient anchors
	Highlight      string // the star / rain-head near-white
}

Colour depth. By default (Profile: Auto, the zero value) fresco auto-detects the terminal's colour profile. Pin it — fresco.TrueColor, ANSI256, ANSI16, or NoColor — for tests, for writing to a non-TTY, or to force colour when piping; with a profile pinned, Render is pure over its inputs regardless of the ambient terminal. ColorProfile is fresco's own type, so pinning depth needs no termenv import.

Roadmap

See docs/ROADMAP.md for where fresco is headed — the v0.2.0 — Open the doors and v0.3.0 — Refine & prove milestones — and for issues labelled good first issue.

Origin

fresco was extracted from Atrium, where it is the animated empty-state splash. The engine is original work; the app-side scene composition (a logo overlay, variant selection) stayed behind in Atrium.

License

MIT © Zvi Baratz

Documentation

Overview

Package fresco is a generative terminal field engine: it renders a slow-drifting, palette-coloured pattern that appears to emanate from a focal row. It is a self-contained engine — the field math, the gradient LUT, and the variant vocabulary — that the caller drives through Render. The caller owns any scene composition (an overlay/message on top) and variant selection.

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

func AppendRender added in v0.3.0

func AppendRender(dst []byte, w, h, frame int, opts Options) []byte

AppendRender appends the rendered frame to dst and returns the extended slice. It is the allocation-free core Render wraps: a caller driving an animation can reuse one buffer across frames — AppendRender(buf[:0], …) — instead of letting Render allocate a fresh string every time. Pass a nil dst to start fresh.

It is otherwise identical to Render: the same inputs yield the same bytes, and it writes exactly h rows of exactly w visible cells (appending nothing for a degenerate pane). Pure over its inputs — deterministic and snapshot-testable.

The returned slice aliases dst's backing array whenever it has room, so bytes read out of a previous frame are invalidated by the next AppendRender(buf[:0], …): fully consume (write or composite) one frame before rendering the next into the same buffer.

func Render

func Render(w, h, frame int, opts Options) string

Render builds the colored splash field background: exactly h rows of exactly w visible cells, or "" on a degenerate pane. Pure over its inputs (deterministic, snapshot-testable). It is a thin convenience wrapper over AppendRender — the two produce byte-identical output for identical inputs.

Example

ExampleRender shows the core call and its contract: Render returns exactly h lines of exactly w visible cells. Pinning Options.Profile makes the output deterministic regardless of the ambient terminal — useful in tests and when writing to a non-TTY.

package main

import (
	"fmt"
	"strings"
	"unicode/utf8"

	"github.com/ZviBaratz/fresco"
)

// examplePalette is the tokyo-night-leaning gradient used across the examples:
// A0..A3 are the warm→cool anchors, Highlight is the star/rain-head near-white.
var examplePalette = fresco.Palette{
	A0:        "#f7768e",
	A1:        "#bb9af7",
	A2:        "#7aa2f7",
	A3:        "#7dcfff",
	Highlight: "#c0caf5",
}

func main() {
	frame := fresco.Render(12, 3, 0, fresco.Options{
		Palette: examplePalette,
		Variant: fresco.Ripple,
		Profile: fresco.NoColor, // pin the colour depth so the output is stable
	})

	lines := strings.Split(frame, "\n")
	fmt.Printf("%d lines\n", len(lines))
	fmt.Printf("%d cells per line\n", utf8.RuneCountInString(lines[0]))
}
Output:
3 lines
12 cells per line

Types

type ColorProfile added in v0.3.0

type ColorProfile int

ColorProfile selects the colour depth Render bakes its SGR bytes for.

The zero value, Auto, defers to the terminal's auto-detected profile — so an Options with no Profile set behaves exactly as the old unset (nil) *termenv.Profile did, and a caller that never pins depth needs to name neither this type nor termenv. Pinning any other value makes Render pure over its inputs: the same Options yield the same bytes regardless of ambient stdout state, which is what a standalone consumer (and a snapshot test) needs.

termenv's own Profile cannot serve this role directly: its zero value is TrueColor, so "unset" could not mean auto-detect. That asymmetry — not merely the wish to hide termenv — is why fresco owns the type (see resolve).

const (
	// Auto auto-detects the terminal's colour profile (the pre-enum nil default).
	Auto ColorProfile = iota
	// TrueColor pins 24-bit colour.
	TrueColor
	// ANSI256 pins the 256-colour palette.
	ANSI256
	// ANSI16 pins the 16-colour palette.
	ANSI16
	// NoColor emits no colour at all; the glyphs still render as plain text.
	NoColor
)

type Options

type Options struct {
	// Palette supplies the four warm→cool gradient anchors plus the star /
	// rain-head highlight.
	Palette Palette
	// Variant selects the field generator.
	Variant Variant
	// FocalRow is the pane row the pattern emanates from — the origin of the
	// focal-relative coordinates every field is evaluated in. A negative value
	// centres it on the pane.
	FocalRow int
	// LumRange, when non-nil, overrides the variant's shipped luminance-range
	// policy (the split between glyph density and colour luminance); nil keeps
	// the variant's default. It stays a pointer because 0 is a meaningful value
	// (density carries all the brightness), so no float64 sentinel could stand
	// for "unset". It is how a caller applies a luminance-range override without
	// this package reading the environment.
	LumRange *float64
	// Profile pins the colour depth Render emits at, independent of the
	// process-global colour profile. The zero value, Auto, defers to the
	// auto-detected terminal profile — what a caller wants when rendering to a
	// real pane, and the behaviour of an unset field. Pinning any other value
	// (TrueColor / ANSI256 / ANSI16 / NoColor) makes Render pure over its
	// inputs: the same Options yield the same bytes regardless of ambient stdout
	// state, which is what a standalone consumer (and a snapshot test) needs.
	// It is a fresco-owned ColorProfile so a caller need not import termenv.
	Profile ColorProfile
}

Options configures Render.

type Palette

type Palette struct {
	A0, A1, A2, A3 string
	Highlight      string
}

Palette is the splash field's colour input: four warm→cool gradient anchors plus a highlight, each a hex string ("#rgb" or "#rrggbb"). A0..A3 are the nebula gradient (e.g. pink, purple, blue, cyan); consecutive anchors are meant to be hue-adjacent so HCL blending between them stays smooth. A3 doubles as rain's stream hue. Highlight is the star / rain-head white — the brightest colour the field can reach (the foreground / near-white).

Render never rejects a Palette: an anchor that is not parseable hex degrades gracefully to a documented fallback (see splashGradientColors, splashShadeParse, rainRampHexAt), so the exactly-h×w-cells contract holds for any Palette. Call Validate to check a palette up front instead of discovering a typo on screen.

func (Palette) Validate added in v0.3.0

func (p Palette) Validate() error

Validate reports whether every anchor is a canonical hex colour ("#rgb" or "#rrggbb"). It is advisory and opt-in: Render never returns an error and never panics on a malformed Palette — each unparseable anchor degrades to a documented fallback, so the h×w-cells contract always holds. Validate exists so a caller loading a theme can surface a bad colour deliberately rather than see it painted as a fallback.

It is intentionally stricter than Render's parser (go-colorful's Hex, which also accepts a missing digit or trailing garbage — "#12345g" parses there): Validate flags anything a human would call a typo (a missing '#', a non-hex digit, a wrong length, trailing characters) even when Render would still paint something for it. So Validate may reject a string Render nonetheless colours; that gap is the point.

The returned error names every offending field and its value (joined, so one call reports all of them); it is nil when all five anchors are canonical.

type Variant

type Variant int

Variant selects the field generator + glyph technique. The caller pins one (or keeps a random per-launch rotation) and passes it in through Options; a caller-side override affordance can trump that.

const (
	// Rain ("f") is Matrix-style digital rain: per-column streams with bright
	// heads and fading tails, layered at four depths. The roster's motion entry
	// — and the only variant that shades by luminance alone, with no hue of its
	// own to spend (see buildRainRamp).
	//
	// It is also the fallback: a variant with no case in splashFieldAt or ops
	// renders as rain, and a caller resolves an unrecognized override name to
	// it too.
	Rain Variant = iota
	// Tunnel ("g") is a textured wall flying past a vanishing point that sits on
	// the focal point: screen position maps to (depth, angle), so a plain noise
	// lookup becomes an infinite corridor. The roster's depth entry — z-fog
	// carries distance in luminance, and hue bands by depth into coloured rings
	// receding down the wall.
	Tunnel
	// Ripple ("h") is drops falling on a dark pool: each one flashes where it
	// lands and expands into a ring that shifts hue as it ages, and the rings
	// interfere where they cross. The roster's event entry — the only field with
	// a birth and a death in it rather than a steady state.
	Ripple
	// Galaxy ("i") is an inclined spiral turning around the focal point: a soft
	// bright bulge, arms mottled with turbulence and star-knots, a dust lane
	// silhouetting the disk's near edge, warm at the core and cool at the rim. The
	// tunnel's single-object sibling — brightness is the whole subject, and the
	// arms are a rigidly rotating density wave rather than winding matter (see
	// splashGalaxyAtFor).
	Galaxy
	// Aurora ("j") is a sky of polar light: tall curtains that drift sideways as a
	// body while each one's spine snakes, hung over dark sky with the hue sliding
	// warm↔cool by altitude. The roster's weather entry, and rain's absolute-field
	// sibling — a bigger pane shows more sky rather than the same object scaled up,
	// and its hue is a property of altitude rather than of any object's radius (see
	// splashAuroraAt).
	Aurora
)

func ParseVariant

func ParseVariant(s string) (Variant, bool)

ParseVariant resolves a pinnable pattern name to its variant. It knows only the user-facing names, not the dev letters (f/g/h) — those are a caller-side affordance layered on top. The bool is false for an unknown name.

Example

ExampleParseVariant resolves a pinnable variant name; the bool reports whether the name was recognized.

package main

import (
	"fmt"

	"github.com/ZviBaratz/fresco"
)

func main() {
	v, ok := fresco.ParseVariant("ripple")
	fmt.Println(v, ok)

	_, ok = fresco.ParseVariant("nope")
	fmt.Println(ok)
}
Output:
ripple true
false

func Variants

func Variants() []Variant

Variants lists the shipped variants — the pool a caller's random rotation draws from. The order is the rotation order; the returned slice is a fresh copy, so callers cannot mutate the pool.

Example

ExampleVariants lists the shipped variants in their rotation order.

package main

import (
	"fmt"

	"github.com/ZviBaratz/fresco"
)

func main() {
	for _, v := range fresco.Variants() {
		fmt.Println(v)
	}
}
Output:
rain
tunnel
ripple
galaxy
aurora

func (Variant) String

func (v Variant) String() string

String returns the variant's pinnable pattern name, or "unknown" for a value outside the shipped set.

Directories

Path Synopsis
cmd
fresco command
Command fresco is a terminal screensaver for the fresco field engine: it fills the pane with a slow-drifting, palette-coloured generative field and animates it full-screen until you quit.
Command fresco is a terminal screensaver for the fresco field engine: it fills the pane with a slow-drifting, palette-coloured generative field and animates it full-screen until you quit.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL