GradientVisualiser Interface and Concrete Implementations
Issue: #655 (scoped from #457 point .3)
Depends on: Issue #2 (Gradient Model Extension) — completed
Status: Planning
1. Scope
Standalone visualiser package with no TUI wiring. The package
provides the GradientVisualiser interface, a registry, and four
concrete implementations. Fully testable in isolation via a spy
renderer.
This issue does not include:
2. What Already Exists
| Component |
File |
Status |
GradientDef Curve/Easing |
contract/palette.go |
Done |
ResolvedGradient fields |
contract/palette.go |
Done |
CurveKind enum (5 values) |
enums/curve-kind-en.go |
Done |
EasingKind enum (4 values) |
enums/easing-kind-en.go |
Done |
InterpolateBetween |
contract/gradients.go |
Done |
contract.Color type |
contract/colour.go |
Done |
GradientState/ApplyGrad* |
effects/gradient-state.go |
Done |
| Half-block chars |
movies/animation-skins.go |
Available |
| Braille chars |
movies/unicode-spinners.go |
Available |
The design doc (section 10) uses CurveType and
EasingPreset as parameter types in the Render signature.
The actual implemented types are enums.CurveKind and
enums.EasingKind. The implementation uses the real types.
3. Package Layout
src/prism/workshop/
├── visualiser.go # interface, registry
├── waveform.go # default: sine wave + sweep
├── sweep.go # plain animated sweep bar
├── bloom.go # radial bloom (low priority)
├── bands.go # bands + braille curve
├── visualiser_test.go # shared spy, registry tests
├── waveform_test.go
├── sweep_test.go
├── bloom_test.go
└── bands_test.go
Package name: workshop.
4. Interface and Registry
4.1 GradientVisualiser Interface
// GradientVisualiser renders a realtime visual
// representation of a gradient for display in the
// gradient workshop seed screen. Implementations are
// interchangeable — the workshop calls Render on every
// state change and displays the result without knowing
// which visualiser is active.
type GradientVisualiser interface {
// Render produces the terminal string for the
// current gradient. steps is the pre-interpolated
// colour slice from the working state. animFrame
// is the current animation tick counter, used by
// animated visualisers to advance their state.
Render(
steps []contract.Color,
curve enums.CurveKind,
easing enums.EasingKind,
animFrame int,
) string
// Name returns the display name shown in the
// visualiser picker.
Name() string
}
4.2 Registry
Package-level registry with explicit registration, no init().
var visualiserRegistry []GradientVisualiser
// Register adds a visualiser to the package-level
// registry. Intended for use only from
// RegisterVisualisers.
func Register(v GradientVisualiser) {
visualiserRegistry = append(
visualiserRegistry, v,
)
}
// Visualisers returns a copy of the registered
// visualisers. The slice is safe to iterate; mutations
// do not affect the registry.
func Visualisers() []GradientVisualiser {
out := make(
[]GradientVisualiser,
len(visualiserRegistry),
)
copy(out, visualiserRegistry)
return out
}
// Reset clears the registry.
// Intended for test isolation only.
func Reset() {
visualiserRegistry = visualiserRegistry[:0]
}
4.3 RegisterVisualisers
// RegisterVisualisers registers all GradientVisualiser
// implementations for use in the gradient workshop.
// Called only from the tweak bootstrap path.
func RegisterVisualisers() {
Register(&WaveformVisualiser{})
Register(&SweepVisualiser{})
Register(&BloomVisualiser{})
Register(&BandsVisualiser{})
}
The tweak cobra command's bootstrap calls this. Other
command paths (walk, sprint, query) do not. The
registry is empty until RegisterVisualisers is called, so
no visualiser code is loaded by the other commands.
5. Concrete Implementations
5.1 WaveformVisualiser (primary, default)
A sine wave of half-block characters (▄ U+2584) whose
colour follows the gradient sweep. The wave shape reflects
the curve type; animation speed reflects the easing preset.
A flat sweep bar beneath provides the accurate colour
reference.
Rendering approach:
- Two rows of output per render call.
- Row 1 (waveform): For each column position
x in
the terminal width, compute a height h from
sin(x * frequency + animFrame * speed). Map h to a
half-block character. The colour at position x is
taken from the gradient step array using the column's
fractional index.
- Row 2 (sweep bar): A flat row of
█ characters,
one per gradient step, evenly distributed across the
terminal width. Each cell is coloured with its
corresponding step.
Parameters derived from curve/easing:
curve controls the mapping from step index to colour
intensity in the sweep bar (higher curve values bunch
colours toward one end).
easing controls the animation speed multiplier —
EaseIn accelerates, EaseOut decelerates,
EaseInOut oscillates.
File: waveform.go
5.2 SweepVisualiser (simple fallback)
A plain animated horizontal sweep bar. The simplest
visualiser.
Rendering approach:
- Single row of
█ characters spanning the terminal
width.
- The
animFrame offset shifts which gradient step maps
to the left edge, producing a scrolling animation.
- Each character position is coloured with the
corresponding gradient step via lipgloss.
File: sweep.go
5.3 BloomVisualiser (lower priority)
Gradient radiates outward from a central point. Each ring
takes one gradient step. Pulses slowly using the easing
preset.
Rendering approach:
- Multiple concentric rings rendered as ASCII circles
(using characters like ○, ◎, ● or braille dots).
- Each ring is coloured with one gradient step, cycling
through the step array.
- The
animFrame offset shifts the ring-to-step
mapping, producing a pulsing effect.
File: bloom.go
5.4 BandsVisualiser (lower priority)
Flat gradient bar with a braille curve indicator row
above it showing the easing curve shape.
Rendering approach:
- Row 1 (curve indicator): A braille character row
that traces the easing curve shape. For each column,
evaluate easedT(t) where t is the column's
fractional position, map the result to a braille dot
pattern, and render in a muted colour.
- Row 2 (gradient bar): Same as the sweep bar —
flat █ row coloured by gradient steps.
File: bands.go
6. ANSI Rendering Strategy
All visualisers produce strings using lipgloss, consistent
with the existing effects.ApplyGradientStyled pattern:
style := lipgloss.NewStyle().Foreground(
lipgloss.Color(
fmt.Sprintf(
"#%02x%02x%02x", c.R, c.G, c.B,
),
),
)
output.WriteString(style.Render("█"))
This produces true-color ANSI escape sequences
(\x1b[38;2;R;G;Bm). No ANSI256 or ANSI16 rendering is
required at this layer — the preview tier switching (design
section 9.3) is handled by the workshop screen, not the
visualiser.
Terminal width is obtained from lipgloss.Width or passed
as a parameter. The Render signature does not include
width; the implementations read it from a configurable
field set during construction (see section 7).
7. Configuration
Each visualiser is constructed with a width parameter:
type WaveformVisualiser struct {
Width int
}
func (v *WaveformVisualiser) Render(
steps []contract.Color,
curve enums.CurveKind,
easing enums.EasingKind,
animFrame int,
) string {
// uses v.Width for column count
}
Default width: 80. The workshop screen sets the width to
the actual terminal width before calling Render.
The workshop screen must respond to terminal resize events.
When the terminal size changes, the screen updates the
width field on each registered visualiser before the next
Render call. This ensures the gradient output always
matches the available terminal width. Handling this is
straightforward at this layer — the visualiser itself is
stateless with respect to width; it reads the field on
every call. The resize event originates from the Bubble Tea
WindowMsg and is handled by the workshop screen's
Update method, which propagates the new width to all
visualisers. This is not a concern for this issue (no TUI
wiring yet), but the design accommodates it cleanly.
8. Testing Strategy
8.1 Spy Renderer
A test double that records calls without producing
terminal output:
type spyRenderer struct {
calls []spyCall
}
type spyCall struct {
Steps []contract.Color
Curve enums.CurveKind
Easing enums.EasingKind
AnimFrame int
}
func (s *spyRenderer) Render(
steps []contract.Color,
curve enums.CurveKind,
easing enums.EasingKind,
animFrame int,
) string {
s.calls = append(s.calls, spyCall{
Steps: steps,
Curve: curve,
Easing: easing,
AnimFrame: animFrame,
})
return ""
}
8.2 Test Matrix
Each visualiser is tested with a table-driven
DescribeTable covering:
| Dimension |
Values |
| Step count |
2, 8, 64, 256 |
| Curve |
Linear, Sine, QuadraticIn, QuadraticOut, Cubic |
| Easing |
Uniform, EaseIn, EaseOut, EaseInOut |
| AnimFrame |
0, 10, 100 |
For each combination:
Render returns a non-empty string.
- Output contains ANSI escape sequences (
\x1b[38;2;).
- Output contains expected half-block, block, or
braille characters for the visualiser type.
Name() returns a non-empty, stable string.
8.3 Registry Tests
Register adds to the registry; Visualisers returns
the registered items.
Reset clears the registry.
RegisterVisualisers registers exactly four
visualisers.
Visualisers returns a copy; mutating the returned
slice does not affect the registry.
8.4 Test File Layout
src/prism/workshop/
├── workshop-suite_test.go
├── visualiser_test.go
├── waveform_test.go
├── sweep_test.go
├── bloom_test.go
└── bands_test.go
All test files use external test packages
(package workshop_test).
9. Implementation Order
- Create
src/prism/workshop/ directory.
visualiser.go — interface, registry, Register,
Visualisers, Reset, RegisterVisualisers.
workshop-suite_test.go — Ginkgo bootstrap.
visualiser_test.go — spy renderer and registry
tests. Verify registry is empty before
RegisterVisualisers, populated after.
sweep.go + sweep_test.go — simplest visualiser
first; validates the rendering pipeline end-to-end.
waveform.go + waveform_test.go — primary
visualiser; half-block wave rendering.
bloom.go + bloom_test.go — radial bloom.
bands.go + bands_test.go — braille curve
overlay.
Steps 7 and 8 are lower priority and can follow in a
sub-issue if scope needs trimming.
10. Acceptance Criteria
11. Files to Create
| File |
Description |
visualiser.go |
Interface, registry |
waveform.go |
Waveform visualiser |
sweep.go |
Sweep visualiser |
bloom.go |
Bloom visualiser |
bands.go |
Bands visualiser |
workshop-suite_test.go |
Ginkgo suite bootstrap |
visualiser_test.go |
Spy, registry tests |
waveform_test.go |
Waveform specs |
sweep_test.go |
Sweep specs |
bloom_test.go |
Bloom specs |
bands_test.go |
Bands specs |
All files under src/prism/workshop/.
12. Files Modified
None. This issue is purely additive — new package, no
changes to existing code.
13. Dependencies
| Import |
Source |
Used for |
contract |
src/prism/contract |
contract.Color type |
enums |
src/agenor/enums |
CurveKind, EasingKind |
lipgloss |
charm.land/lipgloss/v2 |
ANSI colour rendering |
ginkgo |
github.com/onsi/ginkgo/v2 |
Test framework |
gomega |
github.com/onsi/gomega |
Test matchers |
No new external dependencies required.
GradientVisualiser Interface and Concrete Implementations
Issue: #655 (scoped from #457 point .3)
Depends on: Issue #2 (Gradient Model Extension) — completed
Status: Planning
1. Scope
Standalone visualiser package with no TUI wiring. The package
provides the
GradientVisualiserinterface, a registry, and fourconcrete implementations. Fully testable in isolation via a spy
renderer.
This issue does not include:
RegisterVisualisers()call site in the tweak bootstrap(Issue define options and registry #5)
2. What Already Exists
GradientDefCurve/Easingcontract/palette.goResolvedGradientfieldscontract/palette.goCurveKindenum (5 values)enums/curve-kind-en.goEasingKindenum (4 values)enums/easing-kind-en.goInterpolateBetweencontract/gradients.gocontract.Colortypecontract/colour.goGradientState/ApplyGrad*effects/gradient-state.gomovies/animation-skins.gomovies/unicode-spinners.goThe design doc (section 10) uses
CurveTypeandEasingPresetas parameter types in theRendersignature.The actual implemented types are
enums.CurveKindandenums.EasingKind. The implementation uses the real types.3. Package Layout
Package name:
workshop.4. Interface and Registry
4.1 GradientVisualiser Interface
4.2 Registry
Package-level registry with explicit registration, no
init().4.3 RegisterVisualisers
The
tweakcobra command's bootstrap calls this. Othercommand paths (
walk,sprint,query) do not. Theregistry is empty until
RegisterVisualisersis called, sono visualiser code is loaded by the other commands.
5. Concrete Implementations
5.1 WaveformVisualiser (primary, default)
A sine wave of half-block characters (
▄U+2584) whosecolour follows the gradient sweep. The wave shape reflects
the curve type; animation speed reflects the easing preset.
A flat sweep bar beneath provides the accurate colour
reference.
Rendering approach:
xinthe terminal width, compute a height
hfromsin(x * frequency + animFrame * speed). Maphto ahalf-block character. The colour at position
xistaken from the gradient step array using the column's
fractional index.
█characters,one per gradient step, evenly distributed across the
terminal width. Each cell is coloured with its
corresponding step.
Parameters derived from curve/easing:
curvecontrols the mapping from step index to colourintensity in the sweep bar (higher curve values bunch
colours toward one end).
easingcontrols the animation speed multiplier —EaseInaccelerates,EaseOutdecelerates,EaseInOutoscillates.File:
waveform.go5.2 SweepVisualiser (simple fallback)
A plain animated horizontal sweep bar. The simplest
visualiser.
Rendering approach:
█characters spanning the terminalwidth.
animFrameoffset shifts which gradient step mapsto the left edge, producing a scrolling animation.
corresponding gradient step via lipgloss.
File:
sweep.go5.3 BloomVisualiser (lower priority)
Gradient radiates outward from a central point. Each ring
takes one gradient step. Pulses slowly using the easing
preset.
Rendering approach:
(using characters like
○,◎,●or braille dots).through the step array.
animFrameoffset shifts the ring-to-stepmapping, producing a pulsing effect.
File:
bloom.go5.4 BandsVisualiser (lower priority)
Flat gradient bar with a braille curve indicator row
above it showing the easing curve shape.
Rendering approach:
that traces the easing curve shape. For each column,
evaluate
easedT(t)wheretis the column'sfractional position, map the result to a braille dot
pattern, and render in a muted colour.
flat
█row coloured by gradient steps.File:
bands.go6. ANSI Rendering Strategy
All visualisers produce strings using lipgloss, consistent
with the existing
effects.ApplyGradientStyledpattern:This produces true-color ANSI escape sequences
(
\x1b[38;2;R;G;Bm). No ANSI256 or ANSI16 rendering isrequired at this layer — the preview tier switching (design
section 9.3) is handled by the workshop screen, not the
visualiser.
Terminal width is obtained from
lipgloss.Widthor passedas a parameter. The
Rendersignature does not includewidth; the implementations read it from a configurable
field set during construction (see section 7).
7. Configuration
Each visualiser is constructed with a width parameter:
Default width: 80. The workshop screen sets the width to
the actual terminal width before calling
Render.The workshop screen must respond to terminal resize events.
When the terminal size changes, the screen updates the
width field on each registered visualiser before the next
Rendercall. This ensures the gradient output alwaysmatches the available terminal width. Handling this is
straightforward at this layer — the visualiser itself is
stateless with respect to width; it reads the field on
every call. The resize event originates from the Bubble Tea
WindowMsgand is handled by the workshop screen'sUpdatemethod, which propagates the new width to allvisualisers. This is not a concern for this issue (no TUI
wiring yet), but the design accommodates it cleanly.
8. Testing Strategy
8.1 Spy Renderer
A test double that records calls without producing
terminal output:
8.2 Test Matrix
Each visualiser is tested with a table-driven
DescribeTablecovering:For each combination:
Renderreturns a non-empty string.\x1b[38;2;).braille characters for the visualiser type.
Name()returns a non-empty, stable string.8.3 Registry Tests
Registeradds to the registry;Visualisersreturnsthe registered items.
Resetclears the registry.RegisterVisualisersregisters exactly fourvisualisers.
Visualisersreturns a copy; mutating the returnedslice does not affect the registry.
8.4 Test File Layout
All test files use external test packages
(
package workshop_test).9. Implementation Order
src/prism/workshop/directory.visualiser.go— interface, registry,Register,Visualisers,Reset,RegisterVisualisers.workshop-suite_test.go— Ginkgo bootstrap.visualiser_test.go— spy renderer and registrytests. Verify registry is empty before
RegisterVisualisers, populated after.sweep.go+sweep_test.go— simplest visualiserfirst; validates the rendering pipeline end-to-end.
waveform.go+waveform_test.go— primaryvisualiser; half-block wave rendering.
bloom.go+bloom_test.go— radial bloom.bands.go+bands_test.go— braille curveoverlay.
Steps 7 and 8 are lower priority and can follow in a
sub-issue if scope needs trimming.
10. Acceptance Criteria
GradientVisualiserinterface defined insrc/prism/workshop/visualiser.go.Register,Visualisers,Reset.RegisterVisualisers()registers all fourimplementations.
WaveformVisualiserrenders a half-block sinewave with gradient colouring and a flat sweep bar.
SweepVisualiserrenders an animated horizontalsweep bar.
BloomVisualiserrenders concentric rings withgradient colouring.
BandsVisualiserrenders a braille curveindicator with a gradient bar.
containing ANSI escape sequences for a range of
step counts, curve types, and easing presets.
RegisterVisualisersisexplicitly called.
go vetand linter reportno issues.
11. Files to Create
visualiser.gowaveform.gosweep.gobloom.gobands.goworkshop-suite_test.govisualiser_test.gowaveform_test.gosweep_test.gobloom_test.gobands_test.goAll files under
src/prism/workshop/.12. Files Modified
None. This issue is purely additive — new package, no
changes to existing code.
13. Dependencies
contractsrc/prism/contractcontract.Colortypeenumssrc/agenor/enumsCurveKind,EasingKindlipglosscharm.land/lipgloss/v2ginkgogithub.com/onsi/ginkgo/v2gomegagithub.com/onsi/gomegaNo new external dependencies required.