harmony function
- Hct x,
- int n, {
- bool balanced = false,
- HarmonyTonePolicy tonePolicy = HarmonyTonePolicy.preserveTone,
n colors whose chromatic directions are evenly spaced around the white
point, starting at x. Because the directions cancel, an additive mixture
of the set (at some ratio) is neutral gray: this is the checkable property
behind complement (n=2), triad (n=3), and quad (n=4).
By default each color is as vivid as the gamut allows in its direction.
With balanced, all are capped at the weakest direction's strength for
an even ensemble (equal HctUvCoordinates.uvChroma, not equal HCT
chroma; the two differ by hue).
tonePolicy controls L*: preserve tone for theming, reflect tone for the
dramatic painterly complement, move tone only as far as necessary to
preserve u'v' chroma, or move tone to preserve HCT chroma.
Achromatic inputs return n copies of themselves: no direction to spread.
Implementation
List<Hct> harmony(
Hct x,
int n, {
bool balanced = false,
HarmonyTonePolicy tonePolicy = HarmonyTonePolicy.preserveTone,
}) {
assert(n >= 2, 'harmony needs at least 2 colors');
final p = uvOfArgb(x.toInt());
if (p == null || x.chroma < 1e-4) return List.filled(n, x);
final (wu, wv) = whiteUvPoint;
final offset = (p.$1 - wu, p.$2 - wv);
final angles = [for (var i = 0; i < n; i++) i * 360.0 / n];
var shared = 1.0;
if (balanced) {
for (final a in angles) {
final rotated = _rotateOffset(offset, a);
final targetU = wu + rotated.$1, targetV = wv + rotated.$2;
final tone = _targetTone(x, a, tonePolicy, targetU, targetV);
final exit = SliceCache.of(tone).rayExit(rotated.$1, rotated.$2);
if (exit != null) shared = math.min(shared, exit.$1 * 0.995);
}
}
return [
for (final a in angles)
if (a == 0.0 && !balanced)
x // the seed itself, untouched
else
_rotated(x, offset, a, shared, tonePolicy),
];
}