Skip to content

April 15: Color Controls and Cleaner Signatures

Added saturation, vibrancy, and hue rotation controls to the composite pipeline — then immediately refactored compositeService to replace the 10+ positional parameters with an options object so adding the new controls wasn't a mess.

Developer Journal

Saturation, vibrancy, and hue rotation (#1203)

Three new global adjustments on the composite pipeline, applied in HSL space after color mapping:

  • Saturation (-1.0 to 1.0, default 0) — uniform multiply on S channel. Negative desaturates toward grayscale, positive pushes everything more vivid.
  • Vibrancy (-1.0 to 1.0, default 0) — non-linear saturation that targets the less-saturated pixels more than the already-saturated ones. Useful when the image has a color-saturated galaxy core plus a muted background — vibrancy brings up the background without blowing out the core. Implemented as s += amount * (1 - s) * s which peaks the adjustment around s=0.5.
  • Hue rotation (-180° to 180°, default 0) — rotates the whole palette around the color wheel. Useful for tweaking hue relationships without rebuilding the palette from scratch.

All three are composable — applying saturation and then hue rotation produces the expected result. All three are optional and default to no-op so existing saved recipes keep their exact output.

Options objects replace positional params (#1205)

The compositeService functions had grown to 10+ positional parameters. Adding the three new controls from #1203 would have pushed several call sites to 15+ positional args, which is unreadable and guaranteed to produce argument-swap bugs in call sites.

Refactored to options objects:

// before
await runComposite(fits, wcs, channels, weights, stretch, black, white, gamma,
                   sharpness, feather, blend, instrumentBlend, saturation, vibrancy, hueRotation);

// after
await runComposite({ fits, wcs, channels, weights, stretch, black, white, gamma,
                     sharpness, feather, blend, instrumentBlend, saturation, vibrancy, hueRotation });

All call sites use named fields now, which makes arg-swap bugs impossible, makes the defaults explicit at the call site, and makes adding future fields non-breaking. TypeScript's partial-object pattern with required Picks for the mandatory fields gives the best of both worlds.

Also deleted a handful of utility functions that only existed to work around the positional-arg ordering. Net reduction: ~60 lines despite the interface changes.

What shipped

PR Title
#1205 refactor: replace positional params with options objects in compositeService
#1203 feat: add saturation, vibrancy, and hue rotation controls to composite pipeline