Releases: ZviBaratz/fresco
Release list
v1.3.0
Added
- Prebuilt
frescobinaries on every tagged release. A.goreleaser.yamland
a tag-triggeredReleaseworkflow buildcmd/frescofor linux/darwin/windows ×
amd64/arm64 on eachvX.Y.Zpush, publishing archives (each withLICENSE,
README.md, andCHANGELOG.md), achecksums.txt, and a GitHub Release whose
notes come from this file's matching section — so fresco is installable without a
Go toolchain. The binary also gains a--versionflag backed by an
ldflags-injectedmain.version.
v1.2.0
Added
-
A runnable
frescoscreensaver CLI (cmd/fresco). fresco is now directly
runnable, not only importable:go install github.com/ZviBaratz/fresco/cmd/fresco@latest
builds a full-screen terminal screensaver around the pureRenderengine. It
auto-sizes to the terminal and re-renders on resize (SIGWINCH on Unix, a size
poll where there is none), animates in the alternate screen so scrollback is
preserved, and takes single-key controls (q/Ctrl-C quit, space next variant,
ppause). The terminal is restored on every exit path — normal end,
Ctrl-C/SIGTERM, error, and panic — through a LIFO teardown stack behind a
deferred restore, so the screensaver can't wedge the terminal. Flags cover the
variant (a name orcycle/all), palette (presets or five custom hex anchors,
validated throughPalette.Validate), fps, seconds-per-variant, focal-row,
lum-range, colour profile, duration,--size,--once, and--list;
NO_COLOR/FORCE_COLORare honoured, and a non-TTY stdout degrades to a single
frame so a pipe or CI never hangs on a size query or spews control codes. The
frescopackage gains no new dependency and no impurity — all terminal I/O,
the clock, and signals live undercmd/, and the logic is factored into a pure,
TTY-free-testable core (flag/profile resolution, frame composition, the variant
schedule) plus a thin impure driver exercised through a fakeConsole. -
galaxy_figures_test.gomakes the galaxy's shipped figures executable. Every
quantitative "because" in the galaxy's comments is backed by a measurement, but
those measurements lived only in prose, which rots — the #60/#61 work alone quoted
three figures that had already gone stale (118.5 → 135.5, the ruler "odd → even",
"~3% → ~0.7%"). The new test asserts the shipped figures the compiled renderer
produces — whole-pane/arm clipping (1.36% / 0.92%), nucleus glyph contrast
(0.430), core/arm/outskirts bead density (58.4 / 135.5 / 67.3), core/mid
colour-stop density (11.46 / 6.13), and the lumRange0.75 → 0.60arm-annulus
A/B (93.1 → 135.5) — each within ±5%, measured through the existing helpers and
the publicOptions.LumRangeoverride, with no twin of the renderer's
internals. It passes onmainand fails on a deliberategalBulgeAmptweak;
each assertion names the prose that quotes it. It does not assert the ridge/gas
ratio (243.8 / 26.8), which is defined on the rawarmvalue the point function
never returns and so cannot be measured without such a twin — that stays a
tuning-probe finding in prose. SKILL.md §7 gains the tuning-probe (throwaway) vs
figures-test (permanent) distinction. No rendered-byte change; anunparam
nolint on two shared test helpers (now called from a second file) is the only
edit to existing code. -
rain_figures_test.gomakes the rain's shipped figures executable, the second
application of that convention — and the one it earns hardest, since rain is the
field whose figure already rotted on screen: the layer cascade shipped pinned in
raw field units, upstream of thesmoothstepcontrast curve, and rendered3.9 L*
apart against the16.2its comment claimed (fixed in[1.1.0]). The new test
measures the cascade on the side of the curve the eye sees, two ways: it pins the
layer head cascade (81.9 / 65.7 / 47.4 / 29.1, separations16.2 / 18.3 / 18.3)
and the per-layer head-outshines-tail steps (28.2 / 36.6 / 30.4 / 18.1) through
rainScreenStopFor— the realsmoothstepand stop quantization applied to each
layer's shippedbright, read off the real ramp — and it anchors those with two
rendered-byte figures: the lit fraction at rain's quoted 96×30 pane (27.0%,
guardingrainDensity) and the rendered proof that the mid layer's heads land in
the L* 60–69 band, which must dominate the 70–79 band they vacated. All within ±5%
(reusing the galaxy file'srequireFigure), passing onmainand failing on the
pre-fixrainLayersmidbright 0.64 → 0.72, which inverts the rendered bands
exactly as[1.1.0]describes. It does not assert the one-off histogram counts
(239 → 53,50 → 193), which do not reproduce without the demonstration's
unrecorded pane/frame span — their robust relation is the test, the counts stay
prose — and there is no lumRange A/B, sincelumRangeis a dead lever for rain
(pinned at1). SKILL.md §7 gains the rain refinements (post-curve + rendered-byte
anchor; demonstration-count-vs-relation; theunparamdodge). No rendered-byte
change, and this time no edit to existing code at all — the sharedrainStopGrid
helper is called at a pane distinct from its other caller, sounparamstays quiet
without a nolint. -
tunnel_figures_test.gomakes the tunnel's fog geometry executable, the third
application of the convention. The tunnel already carries a thorough invariant
suite, but its one quantitative geometry claim — how deep the fog's black core
reaches — lived only in prose. The test pins it from both sides: the analytic
half-lit radius the shipped fog constants imply (0.5·(tunFogA/tunRefD)/(tunFogGain − 0.5) = 18.4%of maxD, so it fails the momenttunFogA,tunRefDortunFogGain
drift under the quoted figure), and the rendered half-peak crossing of the
radial luminance profile (≈15%of maxD), asserted to be the same curve across
96×30 / 160×44 / 240×60 / 300×80 — the tunnel's defining scale invariance — and to
sit inside the analytic 18.4%. Within ±5% (reusingrequireFigure), passing on
mainand failing on atunFogGaintweak. Measuring it corrected a stale prose
figure: the profile crosses at≈15%(13.9–15.4%), not the16%the comment
claimed, so tunnel.go and the test now agree. It does not assert the
ring-spacing product — tunnel.go quotes a good value of400against the shipped
tunDepthK·tunFreqU = 70, a genuinely stale figure, but rendered ring spatial
frequency has no single value to pin (it varies as r² by design), so the
discrepancy is surfaced here rather than tested. One new file plus the two prose
fixes; no rendered-byte change. -
ripple_figures_test.gomakes the ripple's packet-shape figures executable, the
fourth application of the convention — and ripple is the field that proves why the
gap matters: its row-pitch capture comment carried a stale87.3%for three PRs
afterrippleCycmoved1.5 → 1.8, because the figure was folded to a literal
cos(0.15pi)that outlived the constant it came from, and the 75% floor guarding it
never noticed. The new test pins the shipped packet shape on the compiled
rippleDropWave: the worst row-pitch capture (82.8%, the anti-blink margin), the
packet trough as a share of its crest (54%at the shipped cyc, only floored at 30%
before) and where that trough sits (x = 0.48), the ring-open fraction
(rippleHueOpen = 0.38) and a lone crest's amplitude (0.65 = rippleAmp, asserted
under the clamp so two rings adding have headroom to read as constructive). Each
within ±5% (reusingrequireFigure), passing onmainand failing on the pre-1.8
rippleCyc 1.8 → 1.5, which drifts the capture back to exactly the stale87.33%.
It does not re-run therippleCycsweep (15/29/41/54/61%) — that is a tuning
probe, so only the shipped point is pinned — nor the candidate-skip distribution
(49.7 / 43.7 / 6.6%), an internal branch statistic the point function never
exposes, which stays a prose measurement like galaxy's ridge/gas. One new file, no
edit to existing code, no rendered-byte change. -
aurora_figures_test.gomakes the aurora's tuning figures executable, completing
the roster (all five variants now guard their shipped figures). It pins the two
numbers aurora is actually tuned around: the drift-trap Nyquist figure — the
finest fBm octave sits atauroraFreqX·2^(auroraOct−1) = 0.40cycles/cell, below the
0.5limit, so no octave crawls sideways as the curtains drift (a fourth would land
at0.80and alias) — and the fBm window placement: measured on the compiled
auroraFBM, its median is~0.50and its 90th percentile~0.67, and the curtain
window must straddle the upper third (auroraLojust above the median,auroraHi
near the 90th percentile). Within ±5% (reusingrequireFigure), passing onmain
and failing onauroraFreqX 0.10 → 0.15(finest drifts to0.60, past Nyquist) or
auroraHi 0.70 → 0.80(off the fitted p90). Measuring corrected two stale prose
numbers: the fBm clusters around0.50(not0.53) and tails to~0.79at the 99th
percentile (not "near0.82") — aurora.go now matches. One new file plus that prose
fix; no rendered-byte change.With this the executable-figures convention is applied across the whole roster
(galaxy, rain, tunnel, ripple, aurora), and SKILL.md §7 documents it as the standing
practice for a shipped figure.
Changed
fresco.Tunnel's ring-spacing comment (tunDepthK) corrected to match the
shipped product, resolving the discrepancy the tunnel figures test surfaced. The
comment quoted a good product of400against a bad72, but the shipped
tunDepthK·tunFreqUis70— at the "bad" end. That framing predated the octave
stack and the per-octave mip:70is the coarsest octave's ring spacing,
deliberately coarse (tunFreqUis low for the dead-zone reason its own comment
gives), and the five-octave stack plus the mip carry the rings out to the rim, not a
single high product. Verified before rewriting — the wall's texture variance is flat
at the mip-quieted centre and carries out into the pane (already pinned by
TestSplashTunnelMipQuietsTheVanishingPoint), the opposite of the "coloured haze"
the old comment feared. Comment only; no code or rendered-byte change.
Removed
- *...
v1.1.0
Fixed
fresco.Rainrenders its four parallax depths as four, not three. The layer
cascade was pinned in raw field units, upstream of thesmoothstepcontrast curve
every value passes through before Pass 2. Smoothstep flattens approaching1, so
the near and mid layers —1.00and0.72, comfortably apart in the field — both
landed near the ramp's top and rendered 3.9 L* apart, against the 10-point
separation the design calls for and the16.2its own comment claimed. The
documented cascade81.9 / 65.7 / 47.4 / 35.2was never what reached the screen;
that was81.9 / 78.0 / 47.4 / 29.1.
TestRainLayersSeparateInBrightnessandTestRainHeadOutshinesItsTailnow measure
through the curve (rainScreenStopFor), which makes the guard fail on the shipped
value, and the mid layer'sbrightmoves0.72 → 0.64to satisfy it: the rendered
cascade is now81.9 / 65.7 / 47.4 / 29.1, separations16.2 / 18.3 / 18.3.0.64
is the centre of the stop-10 plateau rather than an edge of it, so a later palette
change cannot quietly tip it to a neighbouring stop. Measured on rendered output:
the mid layer's heads move out of theL* 70–79band (239 cells → 53) and into
60–69(50 → 193), opening the gap under the near layer that the parallax reads
from. Rendered bytes change by design; determinism, bounds and rain's other
invariants are untouched.
Changed
-
The README demo GIF re-recorded to tour all five variants — it previously
showed only the pre-campaign four (rain, tunnel, ripple, galaxy) and now includes
aurora, with every field at the re-art campaign's current look (including galaxy's
#60 bulge)..github/vhs/demo.tapeextends its tour by one variant accordingly.
Docs/asset only; no code change. -
fresco.Galaxy's bulge grades into the disk instead of saturating into a flat
bright mass (#60).galBulgeAmpdrops1.0 → 0.60. At 1.0 the pedestal sat near 1.0
across the inner disk, so the disk it rides on — floor + arm + knot, the knot term
doubled togalKnotAmp 2.0in #56 — stacked past 1 andclamp01flattened the sum:
2.84% of the pane clipped (val == 1.0, a field-level fact upstream of Pass 2) and
the core rendered as a solid block of two glyphs. The clamp was discarding real
structure, not a smooth saturated field — across the clipped cells the pre-clamp
bulge + diskspans1.02..2.75— so dropping the pedestal is what lets it show,
rather than making room for structure never computed. At 0.60 clipping falls to 1.36%
(the residue is sparse knot peaks, not a block) and the nucleus's local glyph
contrast —|centre − mean(8-ring)|in ramp steps, the flat-mass metric — rises
0.13 → 0.43, while the core stays the field's brightest region (core colour-stop
density 11.5 against the mid-disk's 6.1). A soft-knee was measured and rejected:
removing the clamp without dropping the pedestal compresses the1.02..2.75spread
into a band near 1.0 the glyph ramp cannot resolve, and the nucleus goes flatter
(contrast 0.008).galBulgeAmpwas settled by rendering{0.50, 0.55, 0.60, 0.65}
in colour and mono and looking: 0.65 fills the nucleus back toward the old solid look,
0.50 shrinks it to a dim dot that no longer reads as the bright core.
TestSplashGalaxyRendersABrightCoreAndDimmingArmsgains a core-structure floor
(nucleus glyph contrast> 0.25, placed between the measured flat0.13and graded
0.43): itscoreLit > 0.9andcoreDens > midDenswere both satisfied by the flat
block they were meant to guard and so could not have caught this. Rendered bytes
change by design; determinism, bounds and purity hold. -
The galaxy band tests measure against the renderer's own length scale (#61). The
radial ruler was computed two ways —renderFieldtook the vertical term as
max(cyFocal, h-1-cyFocal),galaxy_test.gotookcyFocal— which differ by one on
an even-height pane (29 vs 30 at 240×60; on odd heights the two are equal), so the
test'srhoran ~0.7% large — the raw vertical term differs ~3%, diluted through the
hypotenuse by the dominant half-width — and slid every band boundary inward. Both
now call onesplashMaxD(w, h, focalRow)helper (rain's and ripple's band tests
too), so they cannot diverge again. The knot entry
below quotes its arm-annulus bead figures on the pre-#61 ruler and the pre-#60 bulge
(118.5per 1000, ridge/gas213.8 / 19.7,10.9×); on the corrected ruler and the
shipped field they are135.5per 1000, and — taking ridges asarm ≥ 0.75and gas
asarm ≤ 0.30(the raised-cosine spiral phase, before the turbulence lift) across
the0.35 ≤ rho < 0.60annulus, over frames 0/30/60 at 240×60 —243.8 / 26.8, a
9.1×ratio (the lower bulge lifts the inner-annulus gas cells, so gas beads rise a
little more than ridge). No rendered-byte change. -
fresco.Tunnelretuned for a warmer, textured corridor. ItslumRangemoves
from1to0.75, so the ring texture takes the glyph-density ramp
(o → O → 0 → @) and reads as a tactile, receding surface rather than a flat
field of a single@; and the depth hue now relaxes to a warm base
(tunHueBase) where the sampling-rate mip leaves the sweep unresolved — the near
field and, on a real pane, most of the view — so the corridor reads as a warm
interior receding into the cool cyan rings that survive out where the sweep
resolves, instead of a uniform purple-blue wash. The field's brightness (the fog
depth cue) and its mip/angular-seam guarantees are unchanged; only the Pass-2
luminance split and the hue's unresolved fallback moved. Rendered bytes change by
design; determinism, bounds, and the tunnel's invariants still hold. -
fresco.Galaxycaught mid-turn. Its rigid pattern rotationgalRotSpddoubles
(1.0 → 2.0) so the spiral's turn — the roster's weakest motion — is plainly alive
within a second or two of viewing while staying stately rather than spinning (phase
is the field's only time term, so the per-frame step is far below any strobe). The
arm turbulence is also raised (galTurbAmp 0.62 → 0.72,galKnotThr 0.68 → 0.63,
galKnotAmp 0.70 → 0.85), which adds grain and lifts the brightest turbulence peaks
into local highlights. The texture stays additive-on-peaks, so it opens no holes; the
bright core still outshines the disk, the arm mip/anisotropy and core-finite
guarantees are unchanged, and hue still moves without touching brightness. Rendered
bytes change by design; determinism, bounds, and the galaxy's invariants still hold.An earlier draft of this entry claimed the knots "read as distinct bright beads
strung along filamentary arms". Measured againstgalKnotAmp = 0, that overstates
what the change does. The knot term is not inert — it produces 12× the local maxima
(73 against 6 across three frames, at ≥8 L* over their neighbours) — but those
maxima land in the bright core and the faint outskirts, not on the arms: per 1000
lit cells the arm annulus carries0.0and3.1beads against14.9at the core
and12.3further out, the lowest density in the field. In the glyph-density channel
they do nothing measurable at any radius (mean glyph weight9.24against9.22in
the arms). That draft went on to blame saturation — "the arms already sit at 8.4–9.2
of 11 on the ramp, so an additive term there clips instead of studding" — and that
mechanism is false too: re-measured at 240×60 over three frames, the arm annulus
clips (val == 1.0) on 11 of 14,712 cells, 0.07%, and sits at a meanvalof
0.40 of 1.0, so there was headroom where the claim said there was none. Clipping
in the field is real but lives at the bulge, not the arms (508 cells, 1.2% of the
pane). Both the mean-glyph figure and the local-maxima distribution above are also
single-channel readings taken before the instrument was sound. The
actual cause is measured in the entry below. The rotation half of this entry is
unaffected. -
fresco.Galaxy's arms are actually studded now, and the reason the previous
attempt could not have worked is a spatial-frequency one rather than a gain or a
headroom one. The knots were gated onsplashGalaxyTurbulence, an fBm carrying 47%
of its energy at a period of 7.7 columns and 74% at 3.8 columns or wider, so the term
brightened a region four to eight cells across — and a brightened region reads as a
brighter arm, not as a knot. Measured against the term switched off, only 40% of
the brightest decile of such brightenings survived subtracting its own eight
neighbours (27.6% across every cell the term brightened at all): the blob's own skirt
lifted the background it had to stand above.A bigger
galKnotAmpcould only buy that back by blowing the arms out. Swept on the
old term, arm beads per 1000 lit go13.1 → 32.2 → 50.8 → 70.4 → 84.7at
galKnotAmp 0.85 / 2 / 4 / 8 / 16, so gain alone does eventually clear this PR's own
guard (TestSplashGalaxyArmsCarryKnotsfloors at 40) — the flat "no amount of gain
could have fixed it" an earlier draft of this entry claimed is false. What it costs
is the field: arm clipping rises0.07% → 1.90% → 6.36%across that sweep, against
118.5 beads at 1.50% clipping from the lattice below. Four times the gain for
half the beads at worse clipping is the case for changing the term rather than
tuning it.So the knots now ride their own high-frequency lattice (
galKnotFreq 0.9,
galKnotPeak 0.5,galKnotAmp 0.85 → 2.0), sampled in screen cells rather than
in-plane ones — the in-plane axes are anisotropic bycellAspect/cos(galInc)= 2.17,
so a frequency compact enough to read horizontally packs past Nyquist vertically. The
turbulence now only gates them, softly (galKnotGas 0.35), because multiplying two
sparse gates together starves the count faste...
v1.0.0
The 1.0 release. The public API is now stable and committed to under Semantic
Versioning: Render, AppendRender, Options, Palette (+ Validate), the
Variant set (+ Variants, ParseVariant, String), and the ColorProfile
enum. No exported identifier will be renamed, removed, or retyped before a
2.0.0; new variants, options, profiles, and methods may still be added, since
those are additive. The core contract is unchanged and now permanent: Render
and AppendRender are pure over their inputs and emit exactly h lines of
exactly w visible cells, never erroring or panicking on any Options — a
malformed Palette degrades to documented fallbacks, and Palette.Validate is
the opt-in check. The surface was validated against its real downstream consumer
(atrium bumped to v0.3.0 with zero code changes) and given a final last-look
review before freezing; see docs/api-review-v1.0.md.
Added
fresco.Aurora— a fifth splash variant: northern-lights curtains that drift
slowly sideways over dark sky and snake as they go, the hue sliding warm→cool with
altitude. An absolute field likeRainandRipple(a bigger pane shows more
sky), shaded atlumRange0.75 so the filament cores keep the density ramp while
the soft halo rides the colour. Registered inVariants(),ParseVariant, and
the rotation.
v0.3.0
The "Refine & prove" release. It settles the pre-1.0 API — a fresco-owned
ColorProfile enum, up-front Palette.Validate, and a buffer-reusing
AppendRender — then proves the hot path with an allocation pass and validates
the whole surface against its real downstream consumer (atrium).
Added
AppendRender(dst []byte, w, h, frame int, opts Options) []byte— a
buffer-friendly render path that appends the frame to a caller-owned slice, so
a per-frame loop can reuse one buffer (buf = AppendRender(buf[:0], …))
instead of allocating a fresh string every tick (#17).Renderbecomes a thin,
byte-identical wrapper over it. Purely additive: measured at 120×40 truecolor,
reusing the buffer removes the per-frame output allocation (≈172 KB → 82 KB).Palette.Validate() error— an advisory, opt-in check that reports any
anchor that is not a canonical hex colour ("#rgb"or"#rrggbb"), naming
every offending field (#18).Renderis unchanged: it still never errors or
panics on a malformed palette — each bad anchor degrades to a documented
fallback, so the exactly-h×w-cells contract always holds.Validateis
deliberately stricter than the renderer's parser, so it flags typos (a missing
#, a wrong length, trailing garbage) the renderer would otherwise paint.
Changed
Options.Profileis now a fresco-ownedColorProfileenum instead of
*termenv.Profile, so pinning colour depth no longer requires importing
termenv(#15). The zero value,Auto, auto-detects the terminal exactly as
an unset (nil) profile did before; pinTrueColor,ANSI256,ANSI16, or
NoColorfor a fixed depth. This also settles the options-ergonomics review
(#16):Optionsstays a plain struct — functional options were rejected as
per-frame allocation churn for a 60fps render call — andLumRangestays a
*float64because0is a meaningful value, so no sentinel can mean "unset".
Breaking:Profile: &pbecomesProfile: p(aColorProfilevalue);
callers pinning depth swaptermenv.Ascii→NoColor,termenv.ANSI→ANSI16,
and drop thetermenvimport.
Performance
- Hot-path allocation pass (#21) — the render loop no longer materialises the
field into two per-frame[]float64buffers (the "Pass 1"/"Pass 2" split is
fused: each cell is evaluated inline as it is emitted), and the per-palette LUT
cache is keyed by a comparable struct instead of a freshly joined string on every
call. Output is byte-identical (verified by a 16,896-frame SHA-256 sweep across
variants × profiles × frames × sizes × luminance ranges). On a reused
AppendRenderbuffer this takes the warm hot path from 4 allocations/frame
(≈82 KB at 120×40) to 1 (Rain/Ripple: to zero); viaRender, per-frame
B/oproughly halves. Wall-clock gains are modest and variant-dependent (≈8–12%
for the lighter variants, flat for tunnel and the shaded path) — the pass targets
garbage, not latency. Baselines and method are recorded in
docs/perf.md.
v0.2.0 — Open the doors
fresco v0.2.0 — Open the doors
The first milestone of the open-source hardening effort. No changes to the
rendering engine or its public API — this release is entirely about turning a
good repository into a well-run, contributable open-source project.
Highlights
- ✅ Green CI —
build/vet/gofmt/ tests across Go 1.25 & 1.26 on
Ubuntu, macOS, and Windows, plus a race-detector job andgolangci-lint. - 🤝 Community health files — Code of Conduct, security policy, issue & PR
templates (including a "propose a new variant" form), andCODEOWNERS. - 📖 Contributor docs —
CONTRIBUTING.md, a variant-authoring guide
(docs/variants.md), and a published roadmap (docs/ROADMAP.md). - 🎇 A demo you can see — an animated GIF at the top of the README, with its
reproduciblevhs.tapesource committed. - 🧪 More tests — runnable
Examples, fuzz targets, and a determinism guard. - 🏷️ README badges (CI + Codecov coverage), this changelog, and a pre-1.0
versioning policy.
See the full changelog
for the complete, issue-linked list.
Full diff: v0.1.0...v0.2.0
v0.1.0
fresco v0.1.0
Initial release — the engine, extracted from
Atrium as a standalone module.
fresco is a pure, deterministic (width, height, frame, Options) → ANSI
rendering engine for the terminal: generative, free-running animated fields you
can drive from any render loop.
Included
- Four variants:
Rain,Tunnel,Ripple, andGalaxy. - The
Options/PaletteAPI. - Automatic terminal colour-profile detection, with
Options.Profileto pin it
(makingRenderpure over its inputs). - The
cmd/fresco-demorunnable demo that cycles every variant.
This release page was added retroactively during the v0.2.0 milestone; see the
changelog for the
full history.