/
local_minde.dart
93 lines (77 loc) · 3.26 KB
/
local_minde.dart
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
// Copyright 2024 Google Inc. Use of this source code is governed by an
// MIT-style license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.
import 'dart:math' as math;
import 'package:meta/meta.dart';
import '../../../util/number.dart';
import '../../color.dart';
/// Gamut mapping using the deltaEOK difference formula and the local-MINDE
/// improvement.
///
/// @nodoc
@internal
final class LocalMindeGamutMap extends GamutMapMethod {
/// A constant from the gamut-mapping algorithm.
static const _jnd = 0.02;
/// A constant from the gamut-mapping algorithm.
static const _epsilon = 0.0001;
const LocalMindeGamutMap() : super("local-minde");
SassColor map(SassColor color) {
// Algorithm from https://www.w3.org/TR/2022/CRD-css-color-4-20221101/#css-gamut-mapping-algorithm
var originOklch = color.toSpace(ColorSpace.oklch);
// The channel equivalents to `current` in the Color 4 algorithm.
var lightness = originOklch.channel0OrNull;
var hue = originOklch.channel2OrNull;
var alpha = originOklch.alphaOrNull;
if (fuzzyGreaterThanOrEquals(lightness ?? 0, 1)) {
return color.isLegacy
? SassColor.rgb(255, 255, 255, color.alphaOrNull).toSpace(color.space)
: SassColor.forSpaceInternal(color.space, 1, 1, 1, color.alphaOrNull);
} else if (fuzzyLessThanOrEquals(lightness ?? 0, 0)) {
return SassColor.rgb(0, 0, 0, color.alphaOrNull).toSpace(color.space);
}
var clipped = color.toGamut(GamutMapMethod.clip);
if (_deltaEOK(clipped, color) < _jnd) return clipped;
var min = 0.0;
var max = originOklch.channel1;
var minInGamut = true;
while (max - min > _epsilon) {
var chroma = (min + max) / 2;
// In the Color 4 algorithm `current` is in Oklch, but all its actual uses
// other than modifying chroma convert it to `color.space` first so we
// just store it in that space to begin with.
var current =
ColorSpace.oklch.convert(color.space, lightness, chroma, hue, alpha);
// Per [this comment], the intention of the algorithm is to fall through
// this clause if `minInGamut = false` without checking
// `current.isInGamut` at all, even though that's unclear from the
// pseudocode. `minInGamut = false` *should* imply `current.isInGamut =
// false`.
//
// [this comment]: https://github.com/w3c/csswg-drafts/issues/10226#issuecomment-2065534713
if (minInGamut && current.isInGamut) {
min = chroma;
continue;
}
clipped = current.toGamut(GamutMapMethod.clip);
var e = _deltaEOK(clipped, current);
if (e < _jnd) {
if (_jnd - e < _epsilon) return clipped;
minInGamut = false;
min = chroma;
} else {
max = chroma;
}
}
return clipped;
}
/// Returns the ΔEOK measure between [color1] and [color2].
double _deltaEOK(SassColor color1, SassColor color2) {
// Algorithm from https://www.w3.org/TR/css-color-4/#color-difference-OK
var lab1 = color1.toSpace(ColorSpace.oklab);
var lab2 = color2.toSpace(ColorSpace.oklab);
return math.sqrt(math.pow(lab1.channel0 - lab2.channel0, 2) +
math.pow(lab1.channel1 - lab2.channel1, 2) +
math.pow(lab1.channel2 - lab2.channel2, 2));
}
}