afterimageComplement function

Hct afterimageComplement(
  1. Hct input
)

The afterimage complement: the color the eye produces after adapting to input.

Cone fatigue shifts perception of a neutral field in the direction opposite the stimulus chromaticity (Zaidi et al., "Neural locus of color afterimages", Current Biology 2012: the rebound is the chromatically opposite signal), so the afterimage chromaticity is the point reflection of the input's u'v' through the white point:

target = white + (white - input)

equal strength, opposite direction — rendered at the input's tone. This gives the pair its defining, physically checkable property: an additive mixture of input and complement is neutral. Verified across the RGB cube in test/effects/afterimage_test.dart (99.9% of colors mix to within one u'v' JND of white).

If the full-strength target is out of gamut at the input's tone, strength is clamped ALONG THE RAY from white: direction is physiology, only magnitude yields to the gamut. Consequently chroma is NOT preserved — yellow's complement is a far dimmer violet-blue than blue's complement is a gold — which is what distinguishes this from hue-rotation complements.

Achromatic inputs return themselves: with no chromatic fatigue there is no afterimage.

Implementation

Hct afterimageComplement(Hct input) {
  final p = uvOfArgb(input.toInt());
  if (p == null) return input; // black: no chromaticity to fatigue against
  final (u, v) = p;
  final (wu, wv) = whiteUvPoint;
  final du = wu - u, dv = wv - v; // opposite direction; s=1 is full strength
  final slice = SliceCache.of(input.tone);
  final exit = slice.rayExit(du, dv);
  final result = exit == null
      ? Hct.from(0.0, 0.0, input.tone)
      // Clamp strength along the ray to just inside the boundary.
      : slice.nearest(
          wu + math.min(1.0, exit.$1 * 0.995) * du,
          wv + math.min(1.0, exit.$1 * 0.995) * dv,
        );
  // Slice tone is quantized (0.5 steps); restore the exact input tone, and
  // re-express in the input's color model.
  final retoned = Hct.from(result.hue, result.chroma, input.tone);
  return Hct.fromInt(retoned.toInt(), model: input.colorModel);
}