Inkscape
Vector Graphics Editor
Loading...
Searching...
No Matches
oklch.cpp
Go to the documentation of this file.
1// SPDX-License-Identifier: GPL-2.0-or-later
/*
4 * Authors:
5 * RafaƂ Siejakowski <rs@rs-math.net>
6 * Martin Owens <doctormo@geek-2.com>
7 *
8 * Copyright (C) 2023 Authors
9 * Released under GNU GPL v2+, read the file 'COPYING' for more information.
10 */
11
12#include "oklch.h"
13
14#include <2geom/angle.h>
15#include <2geom/polynomial.h>
16#include <cmath>
17
18#include "colors/color.h"
19#include "colors/printer.h"
20
22
23/* Chroma is technically unbounded in the actual calculations but is
24 * defined between 0.0 and 0.4 by the CSS Color Module specification
25 * as the reasonable upper and lower limits for display. Our internal model
26 * always scales from 0 to 1 of this expected range of values.
27 */
28constexpr double CHROMA_SCALE = 0.4;
29constexpr double HUE_SCALE = 360;
30
36void OkLch::toOkLab(std::vector<double> &in_out)
37{
38 // c and h are polar coordinates; convert to Cartesian a, b coords.
39 double c = in_out[1];
40 Geom::sincos(Geom::Angle::from_degrees(in_out[2] * HUE_SCALE), in_out[2], in_out[1]);
41 in_out[1] *= c;
42 in_out[2] *= c;
43}
44
50void OkLch::fromOkLab(std::vector<double> &in_out)
51{
52 // Convert a, b to polar coordinates c, h.
53 double c = std::hypot(in_out[1], in_out[2]);
54 if (c > 0.001) {
55 Geom::Angle const hue_angle = std::atan2(in_out[2], in_out[1]);
56 in_out[2] = Geom::deg_from_rad(hue_angle.radians0()) / HUE_SCALE;
57 } else {
58 in_out[2] = 0;
59 }
60 in_out[1] = c;
61}
62
67struct ChromaLineCoefficients
68{
69 // Variable naming: `c%d` contains coefficients of c^%d in the polynomial, where c is
70 // the OKLch chroma. l refers to the luminosity, cos and sin to the cosine and sine of
71 // the hue angle. Trailing digits are exponents. For example,
72 // c2.lcos2 is the coefficient of (l * cos(hue_angle)^2) in the overall coefficient of c^2.
73 struct
74 {
75 double l2cos, l2sin;
76 } c1;
77 struct
78 {
79 double lcos2, lcossin, lsin2;
80 } c2;
81 struct
82 {
83 double cos3, cos2sin, cossin2, sin3;
84 } c3;
85};
86
87// clang-format off
88ChromaLineCoefficients const LAB_BOUNDS[] = {
89 // Red polynomial
90 {
91 .c1 = {
92 .l2cos = 5.83279532899080641005754476131631984,
93 .l2sin = 2.3780791275435732378965655753413412
94 },
95 .c2 = {
96 .lcos2 = 1.81614129917652075864819542521099165275,
97 .lcossin = 2.11851258971260413543962953223104329409,
98 .lsin2 = 1.68484527361538384522450980300698198391
99 },
100 .c3 = {
101 .cos3 = 0.257535869797624151773507242289856932594,
102 .cos2sin = 0.414490345667882332785000888243122224651,
103 .cossin2 = 0.126596511492002610582126014059213892767,
104 .sin3 = -0.455702039844046560333204117380816048203
105 }
106 },
107 // Green polynomial
108 {
109 .c1 = {
110 .l2cos = -2.243030176177044107983968331289088261,
111 .l2sin = 0.00129441240977850026657772225608
112 },
113 .c2 = {
114 .lcos2 = -0.5187087369791308621879921351291952375,
115 .lcossin = -0.7820717390897833607054953914674219281,
116 .lsin2 = -1.8531911425339782749638630868227383795
117 },
118 .c3 = {
119 .cos3 = -0.0817959138495637068389017598370049459,
120 .cos2sin = -0.1239788660641220973883495153116480854,
121 .cossin2 = 0.0792215342150077349794741576353537047,
122 .sin3 = 0.7218132301017783162780535454552058572
123 }
124 },
125 // Blue polynomial
126 {
127 .c1 = {
128 .l2cos = -0.2406412780923628220925350522352767957,
129 .l2sin = -6.48404701978782955733370693958213669
130 },
131 .c2 = {
132 .lcos2 = 0.015528352128452044798222201797574285162,
133 .lcossin = 1.153466975472590255156068122829360981648,
134 .lsin2 = 8.535379923500727607267514499627438513637
135 },
136 .c3 = {
137 .cos3 = -0.0006573855374563134769075967180540368,
138 .cos2sin = -0.0519029179849443823389557527273309386,
139 .cossin2 = -0.763927972885238036962716856256210617,
140 .sin3 = -3.67825541507929556013845659620477582
141 }
142 }
143};
144// clang-format on
145
147struct ConstraintMonomials
148{
149 double l, l2, l3, c, c2, c3, s, s2, s3;
150 ConstraintMonomials(double l, double h)
151 : l{l}
152 {
153 l2 = Geom::sqr(l);
154 l3 = l2 * l;
155 Geom::sincos(Geom::rad_from_deg(h), s, c);
156 c2 = Geom::sqr(c);
157 c3 = c2 * c;
158 s2 = 1.0 - c2; // Use sin^2 = 1 - cos^2.
159 s3 = s2 * s;
160 }
161};
162
173static std::array<double, 4> component_coefficients(unsigned index, ConstraintMonomials const &m)
174{
175 auto const &coeffs = LAB_BOUNDS[index];
176 std::array<double, 4> result;
177 // Multiply the coefficients by the corresponding monomials.
178 result[0] = m.l3; // The coefficient of l^3 is always 1
179 result[1] = coeffs.c1.l2cos * m.l2 * m.c + coeffs.c1.l2sin * m.l2 * m.s;
180 result[2] = coeffs.c2.lcos2 * m.l * m.c2 + coeffs.c2.lcossin * m.l * m.c * m.s + coeffs.c2.lsin2 * m.l * m.s2;
181 result[3] =
182 coeffs.c3.cos3 * m.c3 + coeffs.c3.cos2sin * m.c2 * m.s + coeffs.c3.cossin2 * m.c * m.s2 + coeffs.c3.sin3 * m.s3;
183 return result;
184}
185
186/* Compute the maximum Lch chroma for the given luminosity and hue.
187 *
188 * Implementation notes:
189 * The space of Lch colors is a complicated solid with curved faces in the
190 * (L, c, h)-space. So it is not easy to find the maximum chroma for the given
191 * luminosity and hue. (By maximum chroma, we mean the maximum value of c such
192 * that the color oklch(L c h) still fits in the sRGB gamut.)
193 *
194 * We consider an abstract ray (L, c, h) where L and h are fixed and c varies
195 * from 0 to infinity. Conceptually, we transform this ray to the linear RGB space,
196 * which is the unit cube. The ray thus becomes a 3D cubic curve in the RGB cube
197 * and the coordinates R(c), G(c) and B(c) are degree 3 polynomials in the chroma
198 * variable c. The coefficients of c^i in those polynomials will depend on L and h.
199 *
200 * To find the smallest positive value of c for which the curve leaves the unit
201 * cube, we must solve the equations R(c) = 0, R(c) = 1 and similarly for G(c)
202 * and B(c). The desired value is the smallest positive solution among those 6
203 * equations.
204 *
205 * The case of very small or very large luminosity is handled separately.
206 */
207double OkLch::max_chroma(double l, double h)
208{
209 static double const EPS = 1e-7;
210 if (l < EPS || l > 1.0 - EPS) { // Black or white allow no chroma.
211 return 0;
212 }
213
214 double chroma_bound = Geom::infinity();
215 auto const process_root = [&](double root) -> bool {
216 if (root < EPS) { // Ignore roots less than epsilon
217 return false;
218 }
219 if (chroma_bound > root) {
220 chroma_bound = root;
221 }
222 return true;
223 };
224
225 // Check relevant chroma constraints for all three coordinates R, G, B.
226 auto const monomials = ConstraintMonomials(l, h);
227 for (unsigned i = 0; i < 3; i++) {
228 auto const coeffs = component_coefficients(i, monomials);
229 // The cubic polynomial is coeffs[3]*c^3 + coeffs[2]*c^2 + coeffs[1]*c + coeffs[0]
230
231 // First we solve for the R/G/B component equal to zero.
232 for (double root : Geom::solve_cubic(coeffs[3], coeffs[2], coeffs[1], coeffs[0])) {
233 if (process_root(root)) {
234 break;
235 }
236 }
237
238 // Now solve for the component equal to 1 by subtracting 1.0 from coeffs[0].
239 for (double root : Geom::solve_cubic(coeffs[3], coeffs[2], coeffs[1], coeffs[0] - 1.0)) {
240 if (process_root(root)) {
241 break;
242 }
243 }
244 }
245 if (chroma_bound == Geom::infinity()) { // No bound was found, so everything was < EPS
246 return 0;
247 }
248 return chroma_bound;
249}
250
263unsigned const COLOR_SCALE_INTERVALS = 32; // Must be a power of 2 and less than 1024.
264
265uint8_t const *render_hue_scale(double s, double l, std::array<uint8_t, 4 * 1024> *map)
266{
267 auto const data = map->data();
268 auto pos = data;
269 unsigned const interval_length = 1024 / COLOR_SCALE_INTERVALS;
270
271 double h = 0; // Variable hue
272 double chroma_bound = OkLch::max_chroma(l, h);
273 double next_chroma_bound;
274 double const step = 360.0 / 1024.0;
275 double const interpolation_step = 360.0 / COLOR_SCALE_INTERVALS;
276
277 for (unsigned i = 0; i < COLOR_SCALE_INTERVALS; i++) {
278 double const initial_chroma = chroma_bound * s;
279 next_chroma_bound = OkLch::max_chroma(l, h + interpolation_step);
280 double const final_chroma = next_chroma_bound * s;
281
282 for (unsigned j = 0; j < interval_length; j++) {
283 double const c = Geom::lerp(static_cast<double>(j) / interval_length, initial_chroma, final_chroma);
284 auto rgb = *Color(Space::Type::OKLCH, {l, c, h / 360}).converted(Space::Type::RGB);
285 *pos++ = (uint8_t)SP_COLOR_F_TO_U(rgb[0]);
286 *pos++ = (uint8_t)SP_COLOR_F_TO_U(rgb[1]);
287 *pos++ = (uint8_t)SP_COLOR_F_TO_U(rgb[2]);
288 *pos++ = 0xFF;
289 h += step;
290 }
291 chroma_bound = next_chroma_bound;
292 }
293 return data;
294}
295
296uint8_t const *render_saturation_scale(double h, double l, std::array<uint8_t, 4 * 1024> *map)
297{
298 auto const data = map->data();
299 auto pos = data;
300 auto chromax = OkLch::max_chroma(l, h);
301 if (chromax == 0.0) { // Render black or white strip.
302 uint8_t const bw = (l > 0.9) ? 0xFF : 0x00;
303 for (size_t i = 0; i < 1024; i++) {
304 *pos++ = bw; // red
305 *pos++ = bw; // green
306 *pos++ = bw; // blue
307 *pos++ = 0xFF; // alpha
308 }
309 } else { // Render strip of varying chroma.
310 double const chroma_step = chromax / 1024.0;
311 double c = 0.0;
312 for (size_t i = 0; i < 1024; i++) {
313 auto rgb = *Color(Space::Type::OKLCH, {l, c, h}).converted(Space::Type::RGB);
314 *pos++ = (uint8_t)SP_COLOR_F_TO_U(rgb[0]);
315 *pos++ = (uint8_t)SP_COLOR_F_TO_U(rgb[1]);
316 *pos++ = (uint8_t)SP_COLOR_F_TO_U(rgb[2]);
317 *pos++ = 0xFF;
318 c += chroma_step;
319 }
320 }
321 return data;
322}
323
324uint8_t const *render_lightness_scale(double h, double s, std::array<uint8_t, 4 * 1024> *map)
325{
326 auto const data = map->data();
327 auto pos = data;
328 unsigned const interval_length = 1024 / COLOR_SCALE_INTERVALS;
329
330 double l = 0; // Variable lightness
331
332 double chroma_bound = OkLch::max_chroma(l, h);
333 double next_chroma_bound;
334 double const step = 1.0 / 1024.0;
335 double const interpolation_step = 1.0 / COLOR_SCALE_INTERVALS;
336
337 for (unsigned i = 0; i < COLOR_SCALE_INTERVALS; i++) {
338 double const initial_chroma = chroma_bound * s;
339 next_chroma_bound = OkLch::max_chroma(l + interpolation_step, h);
340 double const final_chroma = next_chroma_bound * s;
341
342 for (unsigned j = 0; j < interval_length; j++) {
343 double const c = Geom::lerp(static_cast<double>(j) / interval_length, initial_chroma, final_chroma);
344 auto rgb = *Color(Space::Type::OKLCH, {l, c, h}).converted(Space::Type::RGB);
345 *pos++ = (uint8_t)SP_COLOR_F_TO_U(rgb[0]);
346 *pos++ = (uint8_t)SP_COLOR_F_TO_U(rgb[1]);
347 *pos++ = (uint8_t)SP_COLOR_F_TO_U(rgb[2]);
348 *pos++ = 0xFF;
349 l += step;
350 }
351 chroma_bound = next_chroma_bound;
352 }
353 return data;
354}
355
356bool OkLch::Parser::parse(std::istringstream &ss, std::vector<double> &output) const
357{
358 bool end = false;
359 if (append_css_value(ss, output, end, ',') // Luminance
360 && append_css_value(ss, output, end, ',', CHROMA_SCALE) // Chroma
361 && append_css_value(ss, output, end, '/', HUE_SCALE) // Hue
362 && (append_css_value(ss, output, end) || true) // Optional opacity
363 && end) {
364 return true;
365 }
366 return false;
367}
368
375std::string OkLch::toString(std::vector<double> const &values, bool opacity) const
376{
377 auto os = CssFuncPrinter(3, "oklch");
378
379 os << values[0] // Luminance
380 << values[1] * CHROMA_SCALE // Chroma
381 << values[2] * HUE_SCALE; // Hue
382
383 if (opacity && values.size() == 4)
384 os << values[3]; // Optional opacity
385
386 return os;
387}
388
389}; // namespace Inkscape::Colors::Space
Various trigoniometric helper functions.
Wrapper for angular values.
Definition angle.h:73
static Angle from_degrees(Coord d)
Create an angle from its measure in degrees.
Definition angle.h:136
Coord radians0() const
Get the angle as positive radians.
Definition angle.h:112
static bool append_css_value(std::istringstream &ss, std::vector< double > &output, bool &end, char const sep=0x0, double scale=1.0)
Parse a CSS color number and format it according to it's unit.
Definition parser.cpp:324
bool parse(std::istringstream &input, std::vector< double > &output) const override
Definition oklch.cpp:356
static void toOkLab(std::vector< double > &output)
Convert a color from the the OkLch colorspace to the OKLab colorspace.
Definition oklch.cpp:36
static double max_chroma(double l, double h)
Definition oklch.cpp:207
std::string toString(std::vector< double > const &values, bool opacity) const override
Print the Lab color to a CSS string.
Definition oklch.cpp:375
static void fromOkLab(std::vector< double > &output)
Convert a color from the the OKLab colorspace to the OkLch colorspace.
Definition oklch.cpp:50
constexpr uint32_t SP_COLOR_F_TO_U(double v)
Definition utils.h:23
vector< vpsc::Rectangle * > rs
RootCluster root
Css & result
std::unordered_map< std::string, std::unique_ptr< SPDocument > > map
double c[8][4]
constexpr Coord lerp(Coord t, Coord a, Coord b)
Numerically stable linear interpolation.
Definition coord.h:97
constexpr Coord infinity()
Get a value representing infinity.
Definition coord.h:88
Geom::Point end
void sincos(double angle, double &sin_, double &cos_)
Simultaneously compute a sine and a cosine of the same angle.
Definition math-utils.h:89
std::vector< Coord > solve_cubic(Coord a, Coord b, Coord c, Coord d)
Analytically solve cubic equation.
T sqr(const T &x)
Definition math-utils.h:57
uint8_t const * render_hue_scale(double s, double l, std::array< uint8_t, 4 *1024 > *map)
Definition oklch.cpp:265
uint8_t const * render_saturation_scale(double h, double l, std::array< uint8_t, 4 *1024 > *map)
Definition oklch.cpp:296
unsigned const COLOR_SCALE_INTERVALS
How many intervals a color scale should be subdivided into for the chroma bounds probing.
Definition oklch.cpp:263
constexpr double CHROMA_SCALE
Definition lch.cpp:24
constexpr double HUE_SCALE
Definition lch.cpp:25
uint8_t const * render_lightness_scale(double h, double s, std::array< uint8_t, 4 *1024 > *map)
Definition oklch.cpp:324
ChromaLineCoefficients const LAB_BOUNDS[]
Definition oklch.cpp:88
static std::array< double, 4 > component_coefficients(unsigned index, ConstraintMonomials const &m)
Find the coefficients of the cubic polynomial expressing the linear R, G or B component as a function...
Definition oklch.cpp:173
Polynomial in canonical (monomial) basis.
RGB rgb
Definition quantize.cpp:36
static const Point data[]
int index