Perceptual Color

helpermath.h
1// SPDX-FileCopyrightText: Lukas Sommer <sommerluk@gmail.com>
2// SPDX-License-Identifier: BSD-2-Clause OR MIT
3
4#ifndef HELPERMATH_H
5#define HELPERMATH_H
6
7#include <cmath>
8#include <limits>
9#include <optional>
10#include <qgenericmatrix.h>
11#include <qglobal.h>
12#include <qmetatype.h>
13#include <stdlib.h>
14#include <type_traits>
15
16/** @internal
17 *
18 * @file
19 *
20 * Mathematical helper functions. */
21
22namespace PerceptualColor
23{
24
25/** @internal
26 *
27 * @brief A vector with 4 elements (double precision).
28 *
29 * This type is declared as type to Qt’s type system via
30 * <tt>Q_DECLARE_METATYPE</tt>. Depending on your use case (for
31 * example if you want to use for <em>queued</em> signal-slot connections),
32 * you might consider calling <tt>qRegisterMetaType()</tt> for
33 * this type, once you have a QApplication object. */
34using Quartet = QGenericMatrix<1, 4, double>;
35
36/** @internal
37 *
38 * @brief A 3×3 matrix (double precision).
39 *
40 * This type is declared as type to Qt’s type system via
41 * <tt>Q_DECLARE_METATYPE</tt>. Depending on your use case (for
42 * example if you want to use for <em>queued</em> signal-slot connections),
43 * you might consider calling <tt>qRegisterMetaType()</tt> for
44 * this type, once you have a QApplication object. */
45using SquareMatrix3 = QGenericMatrix<3, 3, double>;
46
47/** @internal
48 *
49 * @brief A vector with 3 elements (double precision).
50 *
51 * This type is declared as type to Qt’s type system via
52 * <tt>Q_DECLARE_METATYPE</tt>. Depending on your use case (for
53 * example if you want to use for <em>queued</em> signal-slot connections),
54 * you might consider calling <tt>qRegisterMetaType()</tt> for
55 * this type, once you have a QApplication object.
56 *
57 * @sa @ref createTrio() */
59
60/** @internal
61 *
62 * @brief Convenience constructor for QGenericMatrix.
63 *
64 * @tparam N columns
65 * @tparam M rows
66 * @tparam T typename
67 * @param args Initialization values. The number of arguments must be
68 * exactly N × M.
69 *
70 * @returns The corresponding QGenericMatrix. */
71template<int N, int M, typename T, typename... Args>
72[[nodiscard]] constexpr QGenericMatrix<N, M, T> createMatrix(Args... args)
73{
74 // Too few arguments leave values uninitialized, too many arguments
75 // result in compiler warnings.
76 static_assert(sizeof...(args) == N * M, "Invalid number of arguments.");
77 const T valueArray[] = {args...};
78 return QGenericMatrix<N, M, T>(valueArray);
79}
80
81SquareMatrix3 createSquareMatrix3(double r0c0, double r0c1, double r0c2, double r1c0, double r1c1, double r1c2, double r2c0, double r2c1, double r2c2);
82
83Trio createTrio(double first, double second, double third);
84
85int decimalPlaces(const int rangeMax, const int significantFigures);
86
87std::optional<SquareMatrix3> inverseMatrix(const SquareMatrix3 &matrix);
88
89/** @internal
90 *
91 * @brief Template function to test if a value is within a certain range
92 * @param low the lower limit
93 * @param x the value that will be tested
94 * @param high the higher limit
95 * @returns @snippet this Helper isInRange */
96template<typename T>
97[[nodiscard]] constexpr bool isInRange(const T &low, const T &x, const T &high)
98{
99 return (
100 // The Doxygen comments contain @private because apparently
101 // the tag @internal is not enough to hide it in the API documentation.
102 // The snippet marker [] is hidden within HTML comments to avoid
103 // that is shows up literally in the private documentation, and this
104 // independent from the HIDE_IN_BODY_DOCS parameter in Doxyfile.
105 //! @private @internal <!-- [Helper isInRange] -->
106 (low <= x) && (x <= high)
107 //! @private @internal <!-- [Helper isInRange] -->
108 );
109}
110
111/** @internal
112 *
113 * @brief Test if an integer is odd.
114 *
115 * @param number The number to test. Must be an integer type
116 *
117 * @returns <tt>true</tt> if the number is odd, <tt>false</tt> otherwise. */
118template<typename T>
119[[nodiscard]] constexpr bool isOdd(const T &number)
120{
121 static_assert(std::is_integral_v<T>, //
122 "Template isOdd() only works with integer types.");
123 constexpr T two = 2;
124 return static_cast<bool>(number % two);
125}
126
127/** @internal
128 *
129 * @brief Test if two floating point values are nearly equal.
130 *
131 * Comparison is done in a relative way, where the
132 * exactness is stronger the smaller the numbers are.
133 * <a href="https://embeddeduse.com/2019/08/26/qt-compare-two-floats/">
134 * This is the reasonable behaviour for floating point comparisons.</a>
135 * Unlike <tt>qFuzzyCompare</tt> and <tt>qFuzzyIsNull</tt> this function
136 * works for both cases: numbers near to 0 and numbers far from 0.
137 *
138 * @tparam T Must be a floating point type.
139 * @param a one of the values to compare
140 * @param b one of the values to compare
141 * @param epsilon indicator for desired precision assuming that the values
142 * to compare are close to 1. Values lower the the compiler-epsilon
143 * from type T will be replaced by the compiler-epsilon from type T.
144 * If epsilon is infinity or near to the maximum value of type T,
145 * the result of this function might be wrong.
146 * @returns <tt>true</tt> if the values are nearly equal. <tt>false</tt>
147 * otherwise.
148 *
149 * @sa @ref isNearlyEqual(A a, B b) provides a default epsilon. */
150template<typename T>
151[[nodiscard]] constexpr bool isNearlyEqual(T a, T b, T epsilon)
152{
153 static_assert( //
154 std::is_floating_point<T>::value, //
155 "Template isNearlyEqual(T a, T b, T epsilon) only works with floating point types");
156
157 // Implementation based on https://stackoverflow.com/a/32334103
158 const auto actualEpsilon = //
159 qMax(std::numeric_limits<T>::epsilon(), epsilon);
160
161 if ((a == b) && (!std::isnan(epsilon))) {
162 // Not explicitly checking if a or b are NaN, because if any of those
163 // is NaN, the code above will return “false” anyway.
164 return true;
165 }
166
167 const auto norm = qMin<T>( //
168 qAbs(a) + qAbs(b), //
169 std::numeric_limits<T>::max());
170 return std::abs(a - b) < std::max(actualEpsilon, actualEpsilon * norm);
171}
172
173/** @internal
174 *
175 * @brief Test if two floating point values are nearly equal, using
176 * a default epsilon.
177 *
178 * Calls @ref isNearlyEqual(T a, T b, T epsilon) with a default epsilon
179 * who’s value depends on the type with <em>less</em> precision among A and B.
180 *
181 * @tparam A Must be a floating point type.
182 * @tparam B Must be a floating point type.
183 * @param a one of the values to compare
184 * @param b one of the values to compare
185 * @returns <tt>true</tt> if the values are nearly equal. <tt>false</tt>
186 * otherwise. */
187template<typename A, typename B>
188[[nodiscard]] constexpr bool isNearlyEqual(A a, B b)
189{
190 static_assert( //
191 std::is_floating_point<A>::value, //
192 "Template isNearlyEqual(A a, B b) only works with floating point types");
193 static_assert( //
194 std::is_floating_point<B>::value, //
195 "Template isNearlyEqual(A a, B b) only works with floating point types");
196
197 // Define a factor to multiply with. Our epsilon has to be bigger than
198 // std::numeric_limits<>::epsilon(), which represents the smallest
199 // representable difference for the value 1.0. Doing various consecutive
200 // floating point operations will increase the error, therefore we need
201 // a factor with which we multiply std::numeric_limits<>::epsilon().
202 // The choice is somewhat arbitrary. Qt’s qFuzzyCompare uses this:
203 //
204 // float:
205 // std::numeric_limits<>::epsilon() is around 1.2e-07.
206 // Qt uses 1e-5.
207 // Factor is around 100
208 //
209 // double:
210 // std::numeric_limits<>::epsilon() is around 2.2e-16.
211 // Qt uses 1e-12.
212 // Factor is around 5000
213 //
214 // long double:
215 // std::numeric_limits<>::epsilon() might vary depending on implementation,
216 // as “long double” might have different sizes on different implementations.
217 // Qt does not support “long double” in qFuzzyCompare.
218 constexpr auto factor = 100;
219
220 // Use the type with less precision to get epsilon, but use the type
221 // with more precision to the the actual comparison.
222 if constexpr (sizeof(A) > sizeof(B)) {
223 return PerceptualColor::isNearlyEqual<A>( //
224 a, //
225 static_cast<A>(b), //
226 static_cast<A>(std::numeric_limits<B>::epsilon() * factor));
227 } else {
228 return PerceptualColor::isNearlyEqual<B>( //
229 static_cast<B>(a), //
230 b, //
231 static_cast<B>(std::numeric_limits<A>::epsilon() * factor));
232 }
233}
234
235/** @internal
236 *
237 * @brief Normalizes an angle.
238 *
239 * | Value | Normalized Value |
240 * | :-------------: | :--------------: |
241 * | <tt>  0°  </tt> | <tt>  0°  </tt> |
242 * | <tt>359.9°</tt> | <tt>359.9°</tt> |
243 * | <tt>360°  </tt> | <tt>  0°  </tt> |
244 * | <tt>361.2°</tt> | <tt>  1.2°</tt> |
245 * | <tt>720°  </tt> | <tt>  0°  </tt> |
246 * | <tt> −1°  </tt> | <tt>359°  </tt> |
247 * | <tt> −1.3°</tt> | <tt>358.7°</tt> |
248 *
249 * @param value an angle (coordinates in degree)
250 * @returns the value, normalized to the range 0° ≤ value < 360° */
251template<typename T>
252T normalizedAngle360(T value)
253{
254 static_assert( //
255 std::is_floating_point<T>::value, //
256 "Template normalizeAngle360() only works with floating point types");
257 constexpr T min = 0;
258 constexpr T max = 360;
259 qreal temp = fmod(value, max);
260 if (temp < min) {
261 temp += max;
262 }
263 return temp;
264}
265
266/** @internal
267 *
268 * @brief Normalizes polar coordinates.
269 *
270 * @param radius Reference to the radius. It will get normalized to value ≥ 0.
271 * If it was < 0 (but not if it was 0 with a negative sign) its sign
272 * is changed and angleDegree is turned by 180°.
273 * @param angleDegree Reference to the angle (measured in degree). It will get
274 * normalized to 0° ≤ value < 360° (see @ref normalizedAngle360() for
275 * details)
276 *
277 * @note When the radius is 0, one could set by convention the (meaningless)
278 * angle also 0. However, note that this function does <em>not</em> do that! */
279template<typename T>
280void normalizePolar360(T &radius, T &angleDegree)
281{
282 if (radius < 0) {
283 radius *= (-1);
284 angleDegree = normalizedAngle360(angleDegree + 180);
285 } else {
286 angleDegree = normalizedAngle360(angleDegree);
287 }
288}
289
290/** @internal
291 *
292 * @brief Round floating point numbers to a certain number of digits
293 *
294 * @tparam T a floating point type
295 * @param value the value that will be rounded
296 * @param precision the number of decimal places to which rounding takes place
297 * @returns the rounded value */
298template<typename T>
299[[nodiscard]] constexpr T roundToDigits(T value, int precision)
300{
301 static_assert( //
302 std::is_floating_point<T>::value, //
303 "Template roundToDigits() only works with floating point types");
304 const T multiplier = std::pow(
305 // Make sure that pow returns a T:
306 static_cast<T>(10),
307 precision);
308 return std::round(value * multiplier) / multiplier;
309}
310
311} // namespace PerceptualColor
312
313Q_DECLARE_METATYPE(PerceptualColor::Quartet)
314Q_DECLARE_METATYPE(PerceptualColor::SquareMatrix3)
315Q_DECLARE_METATYPE(PerceptualColor::Trio)
316
317#endif // HELPERMATH_H
KIOCORE_EXPORT QString number(KIO::filesize_t size)
The namespace of this library.
int decimalPlaces(const int rangeMax, const int significantFigures)
Calculates the required number of decimals to achieve the requested number of significant figures wit...
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Fri May 24 2024 11:48:43 by doxygen 1.10.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.