Gradient Model Extension - Implementation Plan (Issue 2)
Context
The current gradient model performs a pure linear interpolation between two
colour endpoints (Hi/Lo). Issue 2 extends it with two orthogonal concepts:
- Curve - controls the shape of the interpolation path (linear, sine,
quadratic-in, quadratic-out, cubic).
- Easing - controls the distribution of steps along that path (uniform,
ease-in, ease-out, ease-in-out).
This is a pure data-model extension. No TUI, no new commands, no new cobra
wiring.
The banner config cascade - where BannerSubConfig.Steps can override the
gradient's own Steps for the banner widget only - already exists
structurally. This issue extends that same cascade pattern to carry Curve
and Easing through the same path.
Broader context: This issue is part of the jay tweak feature set
described in docs/jay-tweak.design.md (sections 8, 10, 17). The curve and
easing concepts defined here feed into the gradient workshop (Issue 6) and
visualiser (Issue 3) in that design.
Resolved Decisions
Option A for threading curve/easing through ApplyGradient. The
hiCol, loCol color.Color parameters are replaced by a single
contract.ResolvedGradient parameter. Callers already hold a
ResolvedGradient at every call site, so no extra state is required. This
reduces parameter count and makes the interface cleaner.
GradientState requires no new fields. It is a pure animation-tick
counter. Curve and easing are resolved at the point where the colour array is
computed, not during tick advancement.
Default values produce identical output to pre-issue behaviour. Both
CurveKind and EasingKind are string types. Their zero value ("") is
treated as the default (linear/uniform respectively). Every existing call site
that never sets these fields will produce byte-for-byte identical gradient
output after the change. No backwards compatibility concern - the project is
unreleased.
Consolidate the two InterpolateBetweenRGBA functions. There are currently
two functions with the same name:
contract.InterpolateBetweenRGBA returns []color.Color
effects.InterpolateBetweenRGBA returns []contract.Color
These are near-duplicates doing the same interpolation with different return
types. The effects variant is eliminated. The contract variant is updated
to return []contract.Color directly, since contract.Color is the
project's own colour type and all consumers ultimately need it. This avoids
a redundant conversion layer and removes the image/color dependency from
the interpolation hot path.
Scope
Primary file changes
| File |
Change |
src/prism/contract/gradients.go |
Add CurveKind/EasingKind enums, easedT helper; update InterpolateBetween and InterpolateBetweenRGBA signatures; change both return types to []contract.Color |
src/prism/contract/palette.go |
Add Curve and Easing fields to GradientDef and ResolvedGradient |
src/prism/effects/gradient-state.go |
Delete local InterpolateBetweenRGBA; update ApplyGradient and ApplyGradientStatic to accept contract.ResolvedGradient (Option A) |
src/app/bedrock/types.go |
Add Curve, Easing, and Animate fields to BannerSubConfig |
src/prism/contract/theme.go |
Forward Curve/Easing through gradient construction; change GradientCaches type to map[string][]Color |
src/app/ui/registry.go |
Add CurveOverride, EasingOverride, AnimateOverride to BannerConfig; wire resolveBannerConfig |
src/app/ui/highway.go |
Apply cascade overrides in buildBannerInfo |
src/app/ui/porthole.go |
Apply cascade overrides in buildBannerInfo |
src/app/ui/linear.go |
Apply cascade overrides in buildBannerInfo |
Caller updates (required by signature changes)
These files must be updated because the functions they call change signature.
The changes are mechanical: pass the ResolvedGradient directly instead of
extracting .Hi/.Lo, and forward Curve/Easing where needed.
| File |
Call site |
Change |
src/prism/contract/theme.go:324 |
InterpolateBetween(hiCol, loCol, steps) |
Forward gd.Curve, gd.Easing; cache type changes to []contract.Color |
src/prism/widgets/activity/activity.go:36 |
effects.ApplyGradient(effect.Gradient.Hi, effect.Gradient.Lo, ...) |
Pass effect.Gradient directly |
src/prism/widgets/periscope/periscope.go:36 |
effects.ApplyGradient(effect.Gradient.Hi, effect.Gradient.Lo, ...) |
Pass effect.Gradient directly |
src/prism/widgets/periscope/periscope.go:47 |
effects.ApplyGradientStatic(effect.Gradient.Hi, effect.Gradient.Lo, ...) |
Pass effect.Gradient directly |
src/prism/widgets/banner/banner.go:292 |
effects.InterpolateBetweenRGBA(...) |
Remove; use pre-computed cache from ResolvedGradient or call contract.InterpolateBetweenRGBA directly |
src/prism/views/porthole/model.go:139 |
effects.InterpolateBetweenRGBA(...) |
Remove; use pre-computed cache or call contract.InterpolateBetweenRGBA directly |
Test file updates
| File |
Change |
src/prism/effects/gradient-state_test.go:26 |
Update ApplyGradient call to pass ResolvedGradient |
src/prism/widgets/banner/model_test.go:28 |
Update effects.InterpolateBetweenRGBA call to contract.InterpolateBetweenRGBA |
src/prism/views/highway/banner_test.go:45 |
Update effects.InterpolateBetweenRGBA call to contract.InterpolateBetweenRGBA |
New test file
| File |
Change |
src/prism/contract/easing_test.go |
New file. DescribeTable specs for easedT covering all curve/easing combinations |
Step 1 - gradients.go: Enums, Easing Helper, and Interpolation Consolidation
1.1 Enum types
// CurveKind controls the shape of the interpolation path between the
// Hi and Lo gradient endpoints. The zero value is treated as CurveLinear.
type CurveKind string
const (
// CurveLinear interpolates each channel at a constant rate.
// This is the default and matches pre-issue behaviour exactly.
CurveLinear CurveKind = "linear"
// CurveSine applies a sine-wave shape to the interpolation.
CurveSine CurveKind = "sine"
// CurveQuadraticIn accelerates toward the Lo endpoint.
CurveQuadraticIn CurveKind = "quadratic-in"
// CurveQuadraticOut decelerates toward the Lo endpoint.
CurveQuadraticOut CurveKind = "quadratic-out"
// CurveCubic applies a cubic (S-curve) shape.
CurveCubic CurveKind = "cubic"
)
// EasingKind controls the distribution of steps along the curve.
// The zero value is treated as EasingUniform.
type EasingKind string
const (
// EasingUniform distributes steps evenly. Default; matches
// pre-issue behaviour exactly.
EasingUniform EasingKind = "uniform"
// EasingEaseIn clusters steps toward the start of the gradient.
EasingEaseIn EasingKind = "ease-in"
// EasingEaseOut clusters steps toward the end of the gradient.
EasingEaseOut EasingKind = "ease-out"
// EasingEaseInOut clusters steps toward both ends.
EasingEaseInOut EasingKind = "ease-in-out"
)
1.2 easedT helper
Package-private function. Accepts a linear t in [0,1] and returns a
shaped value. Curve is applied first, easing second. Both zero values
("") pass through without modification.
// easedT applies curve shaping then easing distribution to a linear
// parameter t in [0,1]. Either parameter may be empty (zero value),
// in which case that stage is a no-op passthrough.
func easedT(t float64, curve CurveKind, easing EasingKind) float64 {
t = applyCurve(t, curve)
t = applyEasing(t, easing)
return t
}
1.3 Mathematical definitions
Curve functions (applied to raw t):
| Curve |
Formula |
Behaviour |
CurveLinear ("") |
t |
Passthrough |
CurveSine |
(1 - cos(π * t)) / 2 |
Smooth S-ramp, slow at both ends |
CurveQuadraticIn |
t² |
Accelerates (slow start, fast end) |
CurveQuadraticOut |
1 - (1-t)² |
Decelerates (fast start, slow end) |
CurveCubic |
3t² - 2t³ |
Smoothstep S-curve |
Easing functions (applied to curve-shaped t):
| Easing |
Formula |
Behaviour |
EasingUniform ("") |
t |
Passthrough |
EasingEaseIn |
t² |
Clusters steps toward start (hi end) |
EasingEaseOut |
1 - (1-t)² |
Clusters steps toward end (lo end) |
EasingEaseInOut |
(1 - cos(π * t)) / 2 |
Clusters toward both ends |
All functions satisfy the boundary contract: f(0) = 0.0 and f(1) = 1.0.
1.4 Consolidate InterpolateBetweenRGBA
The existing InterpolateBetweenRGBA in gradients.go currently returns
[]color.Color. Change it to return []contract.Color and add
curve CurveKind, easing EasingKind trailing parameters:
// InterpolateBetweenRGBA creates a gradient of 'steps' colours between
// hi and lo RGB values. Returns nil if steps <= 0.
func InterpolateBetweenRGBA(hiR, hiG, hiB, loR, loG, loB uint8, steps int,
curve CurveKind, easing EasingKind) []contract.Color {
steps = max(steps, 2)
gradient := make([]contract.Color, steps)
for i := 0; i < steps; i++ {
t := float64(i) / float64(steps-1)
t = easedT(t, curve, easing)
r := uint8(math.Max(0, math.Min(255, float64(hiR)+(float64(loR)-float64(hiR))*t)))
g := uint8(math.Max(0, math.Min(255, float64(hiG)+(float64(loG)-float64(hiG))*t)))
b := uint8(math.Max(0, math.Min(255, float64(hiB)+(float64(loB)-float64(hiB))*t)))
gradient[i] = contract.Color{R: r, G: g, B: b}
}
return gradient
}
1.5 Update InterpolateBetween
Add curve CurveKind, easing EasingKind trailing parameters and apply
easedT per step. The return type stays []color.Color since theme.go
uses it for the GradientCaches which stores []color.Color.
func InterpolateBetween(hiCol, loCol color.Color, steps int,
curve CurveKind, easing EasingKind) []color.Color {
// ... existing setup ...
for i := 0; i < steps; i++ {
t := float64(i) / float64(steps-1)
t = easedT(t, curve, easing)
// ... existing interpolation using t ...
}
}
Step 2 - palette.go: Extend GradientDef and ResolvedGradient
type GradientDef struct {
Steps int `mapstructure:"steps,omitempty" yaml:"steps,omitempty"`
Hi *SemanticColour `mapstructure:"hi,omitempty" yaml:"hi,omitempty"`
Lo *SemanticColour `mapstructure:"lo,omitempty" yaml:"lo,omitempty"`
Animate *bool `mapstructure:"animate,omitempty" yaml:"animate,omitempty"`
// Curve controls the interpolation shape between Hi and Lo.
// Omit to use linear interpolation (default).
Curve CurveKind `mapstructure:"curve,omitempty" yaml:"curve,omitempty"`
// Easing controls step distribution along the curve.
// Omit for uniform distribution (default).
Easing EasingKind `mapstructure:"easing,omitempty" yaml:"easing,omitempty"`
}
type ResolvedGradient struct {
Steps int
Hi color.Color
Lo color.Color
Animate bool
// Curve and Easing are copied from GradientDef during theme construction.
Curve CurveKind
Easing EasingKind
}
Step 3 - theme.go: Wire Curve/Easing Through Theme Construction
In NewTheme, the gradient-building loop (lines 283-330) is updated:
- Copy
Curve and Easing from GradientDef into ResolvedGradient.
- Forward
Curve and Easing to the InterpolateBetween call that
pre-fills highlightCaches.
- The cache type changes from
map[string][]color.Color to
map[string][]contract.Color (matching the new InterpolateBetweenRGBA
return type). Verify that Theme.GradientCaches field type is updated
accordingly.
highlightGradients[name] = ResolvedGradient{
Steps: steps,
Hi: hiCol,
Lo: loCol,
Animate: animate,
Curve: gd.Curve,
Easing: gd.Easing,
}
if steps > 0 {
cached := InterpolateBetween(hiCol, loCol, steps, gd.Curve, gd.Easing)
highlightCaches[name] = cached
}
Step 4 - gradient-state.go: Thread Curve and Easing Through
4.1 Delete local InterpolateBetweenRGBA
The effects.InterpolateBetweenRGBA function (lines 262-279) is deleted
entirely. All callers switch to contract.InterpolateBetweenRGBA.
4.2 Update ApplyGradient (Option A)
Replace hiCol, loCol color.Color with gradient contract.ResolvedGradient:
func ApplyGradient(gradient contract.ResolvedGradient, frameContent string,
state *GradientState) []RunWithColor {
hiR, hiG, hiB, _ := gradient.Hi.RGBA()
loR, loG, loB, _ := gradient.Lo.RGBA()
steps := contract.InterpolateBetweenRGBA(
uint8(hiR>>8), uint8(hiG>>8), uint8(hiB>>8),
uint8(loR>>8), uint8(loG>>8), uint8(loB>>8),
state.TotalSteps,
gradient.Curve, gradient.Easing,
)
// ... rest unchanged ...
}
4.3 Update ApplyGradientStatic (Option A)
Replace hiCol, loCol color.Color with gradient contract.ResolvedGradient:
func ApplyGradientStatic(gradient contract.ResolvedGradient, content string,
totalSteps int) []RunWithColor {
hiR, hiG, hiB, _ := gradient.Hi.RGBA()
loR, loG, loB, _ := gradient.Lo.RGBA()
steps := contract.InterpolateBetweenRGBA(
uint8(hiR>>8), uint8(hiG>>8), uint8(hiB>>8),
uint8(loR>>8), uint8(loG>>8), uint8(loB>>8),
totalSteps,
gradient.Curve, gradient.Easing,
)
// ... rest unchanged ...
}
4.4 GradientState is not modified
Step 5 - types.go: Extend BannerSubConfig
type BannerSubConfig struct {
Disable bool `mapstructure:"disable" yaml:"disable"`
Position string `mapstructure:"position,omitempty" yaml:"position,omitempty"`
Tick int `mapstructure:"tick,omitempty" yaml:"tick,omitempty"`
Justify string `mapstructure:"justify,omitempty" yaml:"justify,omitempty"`
Steps int `mapstructure:"steps,omitempty" yaml:"steps,omitempty"`
// Curve overrides the gradient's interpolation shape for the banner
// widget only. Omit to inherit the bound gradient's curve.
Curve CurveKind `mapstructure:"curve,omitempty" yaml:"curve,omitempty"`
// Easing overrides the gradient's step distribution for the banner
// widget only. Omit to inherit the bound gradient's easing.
Easing EasingKind `mapstructure:"easing,omitempty" yaml:"easing,omitempty"`
// Animate overrides the gradient's animate flag for the banner
// widget only. Omit to inherit the bound gradient's animate value.
Animate *bool `mapstructure:"animate,omitempty" yaml:"animate,omitempty"`
}
The Animate field is added because the design doc (docs/jay-tweak.design.md
section 8.5) specifies it as a banner cascade override. Using *bool with
omitempty matches the pattern in GradientDef and allows distinguishing
"not set" (inherit) from "explicitly false".
The cascade rule mirrors the existing Steps override: a non-empty
BannerSubConfig.Curve or Easing overrides the bound gradient's own value
for the banner only.
Cascade resolution is in scope for this issue. The resolution point is
resolveBannerConfig in src/app/ui/registry.go. The BannerConfig struct
gains Curve, Easing, and Animate fields, and resolveBannerConfig
copies the override values (non-empty BannerSubConfig fields win over the
gradient's defaults).
Step 6 - Update Callers
6.1 src/prism/widgets/activity/activity.go
// Before:
gradientRuns := effects.ApplyGradient(
effect.Gradient.Hi,
effect.Gradient.Lo,
inner,
effect.State,
)
// After:
gradientRuns := effects.ApplyGradient(
effect.Gradient,
inner,
effect.State,
)
6.2 src/prism/widgets/periscope/periscope.go
// Before (animated):
runs := effects.ApplyGradient(
effect.Gradient.Hi,
effect.Gradient.Lo,
barContent,
effect.State,
)
// After:
runs := effects.ApplyGradient(
effect.Gradient,
barContent,
effect.State,
)
// Before (static):
runs := effects.ApplyGradientStatic(
effect.Gradient.Hi,
effect.Gradient.Lo,
barContent,
effect.Gradient.Steps,
)
// After:
runs := effects.ApplyGradientStatic(
effect.Gradient,
barContent,
effect.Gradient.Steps,
)
6.3 src/prism/widgets/banner/banner.go
The paletteSteps function (line 280) currently calls
effects.InterpolateBetweenRGBA directly. Replace with
contract.InterpolateBetweenRGBA:
func paletteSteps(g *contract.ResolvedGradient, st *effects.GradientState) []contract.Color {
if g == nil || st == nil {
return nil
}
if st.TotalSteps <= 0 {
st.TotalSteps = g.Steps
}
if st.TotalSteps <= 0 {
st.TotalSteps = contract.DefaultStepCount()
}
hiR, hiG, hiB, _ := g.Hi.RGBA()
loR, loG, loB, _ := g.Lo.RGBA()
steps := contract.InterpolateBetweenRGBA(
uint8(hiR>>8), uint8(hiG>>8), uint8(hiB>>8),
uint8(loR>>8), uint8(loG>>8), uint8(loB>>8),
st.TotalSteps,
g.Curve, g.Easing,
)
if len(steps) > 0 {
st.SetSteps(steps)
}
return steps
}
6.4 src/prism/views/porthole/model.go
In SetActivity (line 130), replace effects.InterpolateBetweenRGBA with
contract.InterpolateBetweenRGBA:
func (m *Model) SetActivity(frameFn contract.FrameFunc, gradient *contract.ResolvedGradient) {
m.frameFn = frameFn
m.activityGradient = gradient
if gradient != nil && gradient.Steps > 0 {
hiR, hiG, hiB, _ := gradient.Hi.RGBA()
loR, loG, loB, _ := gradient.Lo.RGBA()
steps := contract.InterpolateBetweenRGBA(
uint8(hiR>>8), uint8(hiG>>8), uint8(hiB>>8),
uint8(loR>>8), uint8(loG>>8), uint8(loB>>8),
gradient.Steps,
gradient.Curve, gradient.Easing,
)
gs := effects.NewGradientState()
gs.SetSteps(steps)
m.gradientState = gs
}
}
Step 7 - Tests
7.1 New easing_test.go in src/prism/contract/
package contract
import (
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
var _ = Describe("easedT", func() {
DescribeTable("boundary values",
func(curve CurveKind, easing EasingKind, tIn, expected float64) {
result := easedT(tIn, curve, easing)
Expect(result).To(BeNumerically("~", expected, 1e-10))
},
// t=0 must always return 0 regardless of curve/easing
Entry("linear/uniform t=0", CurveLinear, EasingUniform, 0.0, 0.0),
Entry("sine/ease-in t=0", CurveSine, EasingEaseIn, 0.0, 0.0),
Entry("cubic/ease-in-out t=0", CurveCubic, EasingEaseInOut, 0.0, 0.0),
// t=1 must always return 1 regardless of curve/easing
Entry("linear/uniform t=1", CurveLinear, EasingUniform, 1.0, 1.0),
Entry("sine/ease-out t=1", CurveSine, EasingEaseOut, 1.0, 1.0),
Entry("cubic/ease-in-out t=1", CurveCubic, EasingEaseInOut, 1.0, 1.0),
// t=0.5 midpoints (shape-dependent, not exact 0.5)
Entry("linear/uniform t=0.5", CurveLinear, EasingUniform, 0.5, 0.5),
Entry("sine t=0.5", CurveSine, EasingUniform, 0.5, 0.5),
Entry("quadratic-in t=0.5", CurveQuadraticIn, EasingUniform, 0.5, 0.25),
Entry("quadratic-out t=0.5", CurveQuadraticOut, EasingUniform, 0.5, 0.75),
Entry("cubic t=0.5", CurveCubic, EasingUniform, 0.5, 0.5),
// empty curve/easing (zero values) are passthrough
Entry("empty curve/easing t=0.3", "", "", 0.3, 0.3),
)
DescribeTable("linear passthrough",
func(tIn float64) {
result := easedT(tIn, CurveLinear, EasingUniform)
Expect(result).To(BeNumerically("~", tIn, 1e-10))
},
Entry("t=0", 0.0),
Entry("t=0.25", 0.25),
Entry("t=0.5", 0.5),
Entry("t=0.75", 0.75),
Entry("t=1", 1.0),
)
})
7.2 Updated test files
All existing test call sites that use the old signatures must be updated.
The changes are mechanical - no test logic changes, only parameter
adjustments:
gradient-state_test.go:26: ApplyGradient(hi, lo, ...) becomes
ApplyGradient(contract.ResolvedGradient{Hi: hi, Lo: lo}, ...)
banner/model_test.go:28: effects.InterpolateBetweenRGBA(...) becomes
contract.InterpolateBetweenRGBA(...) with trailing curve, easing args
views/highway/banner_test.go:45: same pattern as above
Acceptance Criteria
go test ./src/prism/... and go test ./src/app/bedrock/... and
go test ./src/app/ui/... pass.
InterpolateBetween called with CurveLinear/EasingUniform (or empty
strings) produces output byte-for-byte identical to pre-issue output.
GradientDef and ResolvedGradient each carry Curve and Easing with
correct mapstructure and yaml tags.
BannerSubConfig carries Curve, Easing, and Animate with correct
mapstructure and yaml tags.
GradientState is unchanged.
effects.InterpolateBetweenRGBA is deleted; all callers use
contract.InterpolateBetweenRGBA.
ApplyGradient and ApplyGradientStatic accept contract.ResolvedGradient
instead of separate hiCol/loCol parameters.
easedT is tested directly in easing_test.go with a DescribeTable
covering t=0.0, t=0.5, and t=1.0 for each CurveKind value,
verifying that CurveLinear returns t unchanged and that boundary
values (t=0, t=1) always return 0.0 and 1.0 respectively
regardless of curve or easing.
BannerConfig in src/app/ui/registry.go carries CurveOverride,
EasingOverride, and AnimateOverride fields; resolveBannerConfig
copies override values from BannerSubConfig.
- All three view presenters (
highway.go, porthole.go, linear.go)
apply cascade overrides in buildBannerInfo: non-empty
BannerConfig.CurveOverride/EasingOverride win over the gradient's
own Curve/Easing.
- No cyclic package imports.
contract does not import effects or widgets.
effects imports contract (one-directional).
Gradient Model Extension - Implementation Plan (Issue 2)
Context
The current gradient model performs a pure linear interpolation between two
colour endpoints (
Hi/Lo). Issue 2 extends it with two orthogonal concepts:quadratic-in, quadratic-out, cubic).
ease-in, ease-out, ease-in-out).
This is a pure data-model extension. No TUI, no new commands, no new cobra
wiring.
The banner config cascade - where
BannerSubConfig.Stepscan override thegradient's own
Stepsfor the banner widget only - already existsstructurally. This issue extends that same cascade pattern to carry
Curveand
Easingthrough the same path.Broader context: This issue is part of the
jay tweakfeature setdescribed in
docs/jay-tweak.design.md(sections 8, 10, 17). The curve andeasing concepts defined here feed into the gradient workshop (Issue 6) and
visualiser (Issue 3) in that design.
Resolved Decisions
Option A for threading curve/easing through
ApplyGradient. ThehiCol, loCol color.Colorparameters are replaced by a singlecontract.ResolvedGradientparameter. Callers already hold aResolvedGradientat every call site, so no extra state is required. Thisreduces parameter count and makes the interface cleaner.
GradientStaterequires no new fields. It is a pure animation-tickcounter. Curve and easing are resolved at the point where the colour array is
computed, not during tick advancement.
Default values produce identical output to pre-issue behaviour. Both
CurveKindandEasingKindare string types. Their zero value ("") istreated as the default (linear/uniform respectively). Every existing call site
that never sets these fields will produce byte-for-byte identical gradient
output after the change. No backwards compatibility concern - the project is
unreleased.
Consolidate the two
InterpolateBetweenRGBAfunctions. There are currentlytwo functions with the same name:
contract.InterpolateBetweenRGBAreturns[]color.Coloreffects.InterpolateBetweenRGBAreturns[]contract.ColorThese are near-duplicates doing the same interpolation with different return
types. The
effectsvariant is eliminated. Thecontractvariant is updatedto return
[]contract.Colordirectly, sincecontract.Coloris theproject's own colour type and all consumers ultimately need it. This avoids
a redundant conversion layer and removes the
image/colordependency fromthe interpolation hot path.
Scope
Primary file changes
src/prism/contract/gradients.goCurveKind/EasingKindenums,easedThelper; updateInterpolateBetweenandInterpolateBetweenRGBAsignatures; change both return types to[]contract.Colorsrc/prism/contract/palette.goCurveandEasingfields toGradientDefandResolvedGradientsrc/prism/effects/gradient-state.goInterpolateBetweenRGBA; updateApplyGradientandApplyGradientStaticto acceptcontract.ResolvedGradient(Option A)src/app/bedrock/types.goCurve,Easing, andAnimatefields toBannerSubConfigsrc/prism/contract/theme.goCurve/Easingthrough gradient construction; changeGradientCachestype tomap[string][]Colorsrc/app/ui/registry.goCurveOverride,EasingOverride,AnimateOverridetoBannerConfig; wireresolveBannerConfigsrc/app/ui/highway.gobuildBannerInfosrc/app/ui/porthole.gobuildBannerInfosrc/app/ui/linear.gobuildBannerInfoCaller updates (required by signature changes)
These files must be updated because the functions they call change signature.
The changes are mechanical: pass the
ResolvedGradientdirectly instead ofextracting
.Hi/.Lo, and forwardCurve/Easingwhere needed.src/prism/contract/theme.go:324InterpolateBetween(hiCol, loCol, steps)gd.Curve,gd.Easing; cache type changes to[]contract.Colorsrc/prism/widgets/activity/activity.go:36effects.ApplyGradient(effect.Gradient.Hi, effect.Gradient.Lo, ...)effect.Gradientdirectlysrc/prism/widgets/periscope/periscope.go:36effects.ApplyGradient(effect.Gradient.Hi, effect.Gradient.Lo, ...)effect.Gradientdirectlysrc/prism/widgets/periscope/periscope.go:47effects.ApplyGradientStatic(effect.Gradient.Hi, effect.Gradient.Lo, ...)effect.Gradientdirectlysrc/prism/widgets/banner/banner.go:292effects.InterpolateBetweenRGBA(...)ResolvedGradientor callcontract.InterpolateBetweenRGBAdirectlysrc/prism/views/porthole/model.go:139effects.InterpolateBetweenRGBA(...)contract.InterpolateBetweenRGBAdirectlyTest file updates
src/prism/effects/gradient-state_test.go:26ApplyGradientcall to passResolvedGradientsrc/prism/widgets/banner/model_test.go:28effects.InterpolateBetweenRGBAcall tocontract.InterpolateBetweenRGBAsrc/prism/views/highway/banner_test.go:45effects.InterpolateBetweenRGBAcall tocontract.InterpolateBetweenRGBANew test file
src/prism/contract/easing_test.goDescribeTablespecs foreasedTcovering all curve/easing combinationsStep 1 -
gradients.go: Enums, Easing Helper, and Interpolation Consolidation1.1 Enum types
1.2
easedThelperPackage-private function. Accepts a linear
tin[0,1]and returns ashaped value. Curve is applied first, easing second. Both zero values
(
"") pass through without modification.1.3 Mathematical definitions
Curve functions (applied to raw
t):CurveLinear("")tCurveSine(1 - cos(π * t)) / 2CurveQuadraticInt²CurveQuadraticOut1 - (1-t)²CurveCubic3t² - 2t³Easing functions (applied to curve-shaped
t):EasingUniform("")tEasingEaseInt²EasingEaseOut1 - (1-t)²EasingEaseInOut(1 - cos(π * t)) / 2All functions satisfy the boundary contract:
f(0) = 0.0andf(1) = 1.0.1.4 Consolidate
InterpolateBetweenRGBAThe existing
InterpolateBetweenRGBAingradients.gocurrently returns[]color.Color. Change it to return[]contract.Colorand addcurve CurveKind, easing EasingKindtrailing parameters:1.5 Update
InterpolateBetweenAdd
curve CurveKind, easing EasingKindtrailing parameters and applyeasedTper step. The return type stays[]color.Colorsincetheme.gouses it for the
GradientCacheswhich stores[]color.Color.Step 2 -
palette.go: ExtendGradientDefandResolvedGradientStep 3 -
theme.go: Wire Curve/Easing Through Theme ConstructionIn
NewTheme, the gradient-building loop (lines 283-330) is updated:CurveandEasingfromGradientDefintoResolvedGradient.CurveandEasingto theInterpolateBetweencall thatpre-fills
highlightCaches.map[string][]color.Colortomap[string][]contract.Color(matching the newInterpolateBetweenRGBAreturn type). Verify that
Theme.GradientCachesfield type is updatedaccordingly.
Step 4 -
gradient-state.go: Thread Curve and Easing Through4.1 Delete local
InterpolateBetweenRGBAThe
effects.InterpolateBetweenRGBAfunction (lines 262-279) is deletedentirely. All callers switch to
contract.InterpolateBetweenRGBA.4.2 Update
ApplyGradient(Option A)Replace
hiCol, loCol color.Colorwithgradient contract.ResolvedGradient:4.3 Update
ApplyGradientStatic(Option A)Replace
hiCol, loCol color.Colorwithgradient contract.ResolvedGradient:4.4
GradientStateis not modifiedStep 5 -
types.go: ExtendBannerSubConfigThe
Animatefield is added because the design doc (docs/jay-tweak.design.mdsection 8.5) specifies it as a banner cascade override. Using
*boolwithomitemptymatches the pattern inGradientDefand allows distinguishing"not set" (inherit) from "explicitly false".
The cascade rule mirrors the existing
Stepsoverride: a non-emptyBannerSubConfig.CurveorEasingoverrides the bound gradient's own valuefor the banner only.
Cascade resolution is in scope for this issue. The resolution point is
resolveBannerConfiginsrc/app/ui/registry.go. TheBannerConfigstructgains
Curve,Easing, andAnimatefields, andresolveBannerConfigcopies the override values (non-empty
BannerSubConfigfields win over thegradient's defaults).
Step 6 - Update Callers
6.1
src/prism/widgets/activity/activity.go6.2
src/prism/widgets/periscope/periscope.go6.3
src/prism/widgets/banner/banner.goThe
paletteStepsfunction (line 280) currently callseffects.InterpolateBetweenRGBAdirectly. Replace withcontract.InterpolateBetweenRGBA:6.4
src/prism/views/porthole/model.goIn
SetActivity(line 130), replaceeffects.InterpolateBetweenRGBAwithcontract.InterpolateBetweenRGBA:Step 7 - Tests
7.1 New
easing_test.goinsrc/prism/contract/7.2 Updated test files
All existing test call sites that use the old signatures must be updated.
The changes are mechanical - no test logic changes, only parameter
adjustments:
gradient-state_test.go:26:ApplyGradient(hi, lo, ...)becomesApplyGradient(contract.ResolvedGradient{Hi: hi, Lo: lo}, ...)banner/model_test.go:28:effects.InterpolateBetweenRGBA(...)becomescontract.InterpolateBetweenRGBA(...)with trailingcurve, easingargsviews/highway/banner_test.go:45: same pattern as aboveAcceptance Criteria
go test ./src/prism/...andgo test ./src/app/bedrock/...andgo test ./src/app/ui/...pass.InterpolateBetweencalled withCurveLinear/EasingUniform(or emptystrings) produces output byte-for-byte identical to pre-issue output.
GradientDefandResolvedGradienteach carryCurveandEasingwithcorrect
mapstructureandyamltags.BannerSubConfigcarriesCurve,Easing, andAnimatewith correctmapstructureandyamltags.GradientStateis unchanged.effects.InterpolateBetweenRGBAis deleted; all callers usecontract.InterpolateBetweenRGBA.ApplyGradientandApplyGradientStaticacceptcontract.ResolvedGradientinstead of separate
hiCol/loColparameters.easedTis tested directly ineasing_test.gowith aDescribeTablecovering
t=0.0,t=0.5, andt=1.0for eachCurveKindvalue,verifying that
CurveLinearreturnstunchanged and that boundaryvalues (
t=0,t=1) always return0.0and1.0respectivelyregardless of curve or easing.
BannerConfiginsrc/app/ui/registry.gocarriesCurveOverride,EasingOverride, andAnimateOverridefields;resolveBannerConfigcopies override values from
BannerSubConfig.highway.go,porthole.go,linear.go)apply cascade overrides in
buildBannerInfo: non-emptyBannerConfig.CurveOverride/EasingOverridewin over the gradient'sown
Curve/Easing.contractdoes not importeffectsorwidgets.effectsimportscontract(one-directional).