Skip to content

create tweak GradientVisualiser #655

Description

@plastikfan

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

  1. Create src/prism/workshop/ directory.
  2. visualiser.go — interface, registry, Register,
    Visualisers, Reset, RegisterVisualisers.
  3. workshop-suite_test.go — Ginkgo bootstrap.
  4. visualiser_test.go — spy renderer and registry
    tests. Verify registry is empty before
    RegisterVisualisers, populated after.
  5. sweep.go + sweep_test.go — simplest visualiser
    first; validates the rendering pipeline end-to-end.
  6. waveform.go + waveform_test.go — primary
    visualiser; half-block wave rendering.
  7. bloom.go + bloom_test.go — radial bloom.
  8. 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

  • GradientVisualiser interface defined in
    src/prism/workshop/visualiser.go.
  • Registry supports Register, Visualisers,
    Reset.
  • RegisterVisualisers() registers all four
    implementations.
  • WaveformVisualiser renders a half-block sine
    wave with gradient colouring and a flat sweep bar.
  • SweepVisualiser renders an animated horizontal
    sweep bar.
  • BloomVisualiser renders concentric rings with
    gradient colouring.
  • BandsVisualiser renders a braille curve
    indicator with a gradient bar.
  • All visualisers produce non-empty output
    containing ANSI escape sequences for a range of
    step counts, curve types, and easing presets.
  • Registry is empty until RegisterVisualisers is
    explicitly called.
  • All Ginkgo specs pass; go vet and linter report
    no issues.

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.

Metadata

Metadata

Assignees

Labels

featureNew feature or request

Fields

No fields configured for Feature.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions