Phosphor
Qt6 / Wayland library suite for window-management tools
 
Loading...
Searching...
No Matches
Interpolate.h
Go to the documentation of this file.
1// SPDX-FileCopyrightText: 2026 fuddlesworth
2// SPDX-License-Identifier: LGPL-2.1-or-later
3
4#pragma once
5
6#include <PhosphorAnimation/phosphoranimation_export.h>
7
8#include <QColor>
9#include <QLineF>
10#include <QPointF>
11#include <QRectF>
12#include <QSizeF>
13#include <QTransform>
14
15#include <QtGlobal>
16#include <QtMath>
17
18#include <cmath>
19#include <concepts>
20#include <type_traits>
21
22namespace PhosphorAnimation {
23
26inline constexpr qreal kRectSizeEpsilonPx = 1.0;
27
29enum class ColorSpace {
30 Linear,
31 OkLab,
32};
33
35template<typename T>
37
38template<>
39struct Interpolate<qreal>
40{
41 static qreal lerp(qreal from, qreal to, qreal t)
42 {
43 return from + (to - from) * t;
44 }
45 static qreal distance(qreal from, qreal to)
46 {
47 return qAbs(to - from);
48 }
49 static constexpr qreal retargetEpsilon = 1.0e-6;
50 static bool isFinite(qreal v)
51 {
52 return std::isfinite(v);
53 }
54};
55
56template<>
57struct Interpolate<QPointF>
58{
59 static QPointF lerp(const QPointF& from, const QPointF& to, qreal t)
60 {
61 return QPointF(from.x() + (to.x() - from.x()) * t, from.y() + (to.y() - from.y()) * t);
62 }
63 static qreal distance(const QPointF& from, const QPointF& to)
64 {
65 return QLineF(from, to).length();
66 }
67 static constexpr qreal retargetEpsilon = 0.5;
68 static bool isFinite(const QPointF& p)
69 {
70 return std::isfinite(p.x()) && std::isfinite(p.y());
71 }
72};
73
74template<>
75struct Interpolate<QSizeF>
76{
77 static QSizeF lerp(const QSizeF& from, const QSizeF& to, qreal t)
78 {
79 return QSizeF(from.width() + (to.width() - from.width()) * t,
80 from.height() + (to.height() - from.height()) * t);
81 }
82 static qreal distance(const QSizeF& from, const QSizeF& to)
83 {
84 const qreal dw = to.width() - from.width();
85 const qreal dh = to.height() - from.height();
86 return std::sqrt(dw * dw + dh * dh);
87 }
88 static constexpr qreal retargetEpsilon = 0.5;
89 static bool isFinite(const QSizeF& s)
90 {
91 return std::isfinite(s.width()) && std::isfinite(s.height());
92 }
93};
94
95template<>
96struct Interpolate<QRectF>
97{
98 static QRectF lerp(const QRectF& from, const QRectF& to, qreal t)
99 {
100 return QRectF(Interpolate<QPointF>::lerp(from.topLeft(), to.topLeft(), t),
101 Interpolate<QSizeF>::lerp(from.size(), to.size(), t));
102 }
106 static qreal distance(const QRectF& from, const QRectF& to)
107 {
108 const qreal dp = Interpolate<QPointF>::distance(from.topLeft(), to.topLeft());
109 const qreal ds = Interpolate<QSizeF>::distance(from.size(), to.size());
110 return std::sqrt(dp * dp + ds * ds);
111 }
112 static constexpr qreal retargetEpsilon = 0.5;
113 static bool isFinite(const QRectF& r)
114 {
115 return std::isfinite(r.x()) && std::isfinite(r.y()) && std::isfinite(r.width()) && std::isfinite(r.height());
116 }
117};
118
119namespace detail {
120
121// sRGB <-> linear — IEC 61966-2-1 piecewise definition (2.4 exponent, not 2.2).
122inline qreal srgbToLinear(qreal c)
123{
124 return c <= 0.04045 ? c / 12.92 : qPow((c + 0.055) / 1.055, 2.4);
125}
126inline qreal linearToSrgb(qreal c)
127{
128 return c <= 0.0031308 ? c * 12.92 : 1.055 * qPow(c, 1.0 / 2.4) - 0.055;
129}
130
131inline QColor lerpColorLinear(const QColor& from, const QColor& to, qreal t)
132{
133 const qreal fR = srgbToLinear(from.redF());
134 const qreal fG = srgbToLinear(from.greenF());
135 const qreal fB = srgbToLinear(from.blueF());
136 const qreal tR = srgbToLinear(to.redF());
137 const qreal tG = srgbToLinear(to.greenF());
138 const qreal tB = srgbToLinear(to.blueF());
139
140 const qreal rLin = fR + (tR - fR) * t;
141 const qreal gLin = fG + (tG - fG) * t;
142 const qreal bLin = fB + (tB - fB) * t;
143 const qreal a = from.alphaF() + (to.alphaF() - from.alphaF()) * t;
144
145 QColor result;
146 result.setRgbF(qBound(0.0, linearToSrgb(rLin), 1.0), qBound(0.0, linearToSrgb(gLin), 1.0),
147 qBound(0.0, linearToSrgb(bLin), 1.0), qBound(0.0, a, 1.0));
148 return result;
149}
150
151// OkLab — Bjorn Ottosson's perceptually-uniform lab space.
152// Conversion: sRGB -> linear RGB -> LMS -> cube root -> OkLab.
153
154struct OkLab
155{
156 qreal L, a, b;
157};
158
159inline OkLab linearToOkLab(qreal r, qreal g, qreal bv)
160{
161 const qreal l = 0.4122214708 * r + 0.5363325363 * g + 0.0514459929 * bv;
162 const qreal m = 0.2119034982 * r + 0.6806995451 * g + 0.1073969566 * bv;
163 const qreal s = 0.0883024619 * r + 0.2817188376 * g + 0.6299787005 * bv;
164
165 const qreal lCbrt = std::cbrt(l);
166 const qreal mCbrt = std::cbrt(m);
167 const qreal sCbrt = std::cbrt(s);
168
169 OkLab out;
170 out.L = 0.2104542553 * lCbrt + 0.7936177850 * mCbrt - 0.0040720468 * sCbrt;
171 out.a = 1.9779984951 * lCbrt - 2.4285922050 * mCbrt + 0.4505937099 * sCbrt;
172 out.b = 0.0259040371 * lCbrt + 0.7827717662 * mCbrt - 0.8086757660 * sCbrt;
173 return out;
174}
175
176inline void okLabToLinear(const OkLab& lab, qreal& r, qreal& g, qreal& bv)
177{
178 const qreal lCbrt = lab.L + 0.3963377774 * lab.a + 0.2158037573 * lab.b;
179 const qreal mCbrt = lab.L - 0.1055613458 * lab.a - 0.0638541728 * lab.b;
180 const qreal sCbrt = lab.L - 0.0894841775 * lab.a - 1.2914855480 * lab.b;
181
182 const qreal l = lCbrt * lCbrt * lCbrt;
183 const qreal m = mCbrt * mCbrt * mCbrt;
184 const qreal s = sCbrt * sCbrt * sCbrt;
185
186 r = +4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s;
187 g = -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s;
188 bv = -0.0041960863 * l - 0.7034186147 * m + 1.7076147010 * s;
189}
190
191inline QColor lerpColorOkLab(const QColor& from, const QColor& to, qreal t)
192{
193 const qreal fR = srgbToLinear(from.redF());
194 const qreal fG = srgbToLinear(from.greenF());
195 const qreal fB = srgbToLinear(from.blueF());
196 const qreal tR = srgbToLinear(to.redF());
197 const qreal tG = srgbToLinear(to.greenF());
198 const qreal tB = srgbToLinear(to.blueF());
199
200 const OkLab fLab = linearToOkLab(fR, fG, fB);
201 const OkLab tLab = linearToOkLab(tR, tG, tB);
202
203 OkLab mid;
204 mid.L = fLab.L + (tLab.L - fLab.L) * t;
205 mid.a = fLab.a + (tLab.a - fLab.a) * t;
206 mid.b = fLab.b + (tLab.b - fLab.b) * t;
207
208 qreal rLin, gLin, bLin;
209 okLabToLinear(mid, rLin, gLin, bLin);
210
211 const qreal a = from.alphaF() + (to.alphaF() - from.alphaF()) * t;
212
213 QColor result;
214 result.setRgbF(qBound(0.0, linearToSrgb(rLin), 1.0), qBound(0.0, linearToSrgb(gLin), 1.0),
215 qBound(0.0, linearToSrgb(bLin), 1.0), qBound(0.0, a, 1.0));
216 return result;
217}
218
221inline qreal colorDistance(const QColor& from, const QColor& to)
222{
223 const qreal dR = srgbToLinear(to.redF()) - srgbToLinear(from.redF());
224 const qreal dG = srgbToLinear(to.greenF()) - srgbToLinear(from.greenF());
225 const qreal dB = srgbToLinear(to.blueF()) - srgbToLinear(from.blueF());
226 const qreal dA = to.alphaF() - from.alphaF();
227 return std::sqrt(dR * dR + dG * dG + dB * dB + dA * dA);
228}
229
230} // namespace detail
231
232template<>
233struct Interpolate<QColor>
234{
237 static QColor lerp(const QColor& from, const QColor& to, qreal t)
238 {
239 return detail::lerpColorLinear(from, to, t);
240 }
241 static qreal distance(const QColor& from, const QColor& to)
242 {
243 return detail::colorDistance(from, to);
244 }
245 static constexpr qreal retargetEpsilon = 1.0e-4;
246 static bool isFinite(const QColor& c)
247 {
248 return std::isfinite(c.redF()) && std::isfinite(c.greenF()) && std::isfinite(c.blueF())
249 && std::isfinite(c.alphaF());
250 }
251};
252
253// QTransform polar-decomposed interpolation: decompose into translate +
254// rotate + scale + shear, interpolate each independently, recompose.
255// Component-wise lerp would shear during rotation.
256
257namespace detail {
258
260{
261 qreal tx = 0.0, ty = 0.0;
262 qreal rotation = 0.0; // radians
263 qreal sx = 1.0, sy = 1.0;
264 qreal shear = 0.0;
265};
266
269inline DecomposedTransform decomposeTransform(const QTransform& t)
270{
272 d.tx = t.dx();
273 d.ty = t.dy();
274
275 const qreal m11 = t.m11();
276 const qreal m12 = t.m12();
277 const qreal m21 = t.m21();
278 const qreal m22 = t.m22();
279
280 d.sx = std::sqrt(m11 * m11 + m21 * m21);
281 d.rotation = d.sx > 1.0e-9 ? std::atan2(-m21, m11) : 0.0;
282
283 const qreal cosR = std::cos(d.rotation);
284 const qreal sinR = std::sin(d.rotation);
285
286 d.sy = m22 * cosR + m12 * sinR;
287 const qreal shearTimesSy = m12 * cosR - m22 * sinR;
288 d.shear = qAbs(d.sy) > 1.0e-9 ? shearTimesSy / d.sy : 0.0;
289 return d;
290}
291
292inline QTransform recomposeTransform(const DecomposedTransform& d)
293{
294 const qreal cosR = std::cos(d.rotation);
295 const qreal sinR = std::sin(d.rotation);
296
297 const qreal m11 = d.sx * cosR;
298 const qreal m12 = d.sy * (d.shear * cosR + sinR);
299 const qreal m21 = -d.sx * sinR;
300 const qreal m22 = d.sy * (cosR - d.shear * sinR);
301
302 return QTransform(m11, m12, m21, m22, d.tx, d.ty);
303}
304
306inline bool isPureTranslate(const QTransform& t)
307{
308 constexpr qreal kIdentityEps = 1.0e-9;
309 return qAbs(t.m11() - 1.0) < kIdentityEps && qAbs(t.m22() - 1.0) < kIdentityEps && qAbs(t.m12()) < kIdentityEps
310 && qAbs(t.m21()) < kIdentityEps;
311}
312
313inline QTransform lerpTransform(const QTransform& from, const QTransform& to, qreal t)
314{
315 // Exact at endpoints — decompose/recompose is lossy (~1 ulp).
316 if (t <= 0.0) {
317 return from;
318 }
319 if (t >= 1.0) {
320 return to;
321 }
322
323 // Reflection (det sign change) or near-singular endpoints: fall back
324 // to component-wise lerp. Polar decomposition cannot smoothly
325 // interpolate across a determinant sign change (must pass through
326 // singular), and near-singular matrices decompose unreliably.
327 constexpr qreal kNearSingularDet = 1.0e-6;
328 const qreal detFrom = from.m11() * from.m22() - from.m12() * from.m21();
329 const qreal detTo = to.m11() * to.m22() - to.m12() * to.m21();
330 const bool reflection = detFrom * detTo < 0.0;
331 const bool nearSingular = std::abs(detFrom) < kNearSingularDet || std::abs(detTo) < kNearSingularDet;
332 if (reflection || nearSingular) {
333 using S = Interpolate<qreal>;
334 return QTransform(S::lerp(from.m11(), to.m11(), t), S::lerp(from.m12(), to.m12(), t),
335 S::lerp(from.m21(), to.m21(), t), S::lerp(from.m22(), to.m22(), t),
336 S::lerp(from.dx(), to.dx(), t), S::lerp(from.dy(), to.dy(), t));
337 }
338
341
343 r.tx = df.tx + (dt.tx - df.tx) * t;
344 r.ty = df.ty + (dt.ty - df.ty) * t;
345 r.sx = df.sx + (dt.sx - df.sx) * t;
346 r.sy = df.sy + (dt.sy - df.sy) * t;
347 r.shear = df.shear + (dt.shear - df.shear) * t;
348
349 // Shortest-arc slerp: unwrap delta into (-pi, pi].
350 qreal dRot = dt.rotation - df.rotation;
351 while (dRot > M_PI) {
352 dRot -= 2.0 * M_PI;
353 }
354 while (dRot < -M_PI) {
355 dRot += 2.0 * M_PI;
356 }
357 r.rotation = df.rotation + dRot * t;
358
359 return recomposeTransform(r);
360}
361
362} // namespace detail
363
364template<>
365struct Interpolate<QTransform>
366{
367 static QTransform lerp(const QTransform& from, const QTransform& to, qreal t)
368 {
369 return detail::lerpTransform(from, to, t);
370 }
374 static qreal distance(const QTransform& from, const QTransform& to)
375 {
376 const qreal d11 = to.m11() - from.m11();
377 const qreal d12 = to.m12() - from.m12();
378 const qreal d21 = to.m21() - from.m21();
379 const qreal d22 = to.m22() - from.m22();
380 const qreal dtx = to.dx() - from.dx();
381 const qreal dty = to.dy() - from.dy();
382 return std::sqrt(d11 * d11 + d12 * d12 + d21 * d21 + d22 * d22 + dtx * dtx + dty * dty);
383 }
384 static constexpr qreal retargetEpsilon = 0.5;
385 static bool isFinite(const QTransform& t)
386 {
387 return std::isfinite(t.m11()) && std::isfinite(t.m12()) && std::isfinite(t.m21()) && std::isfinite(t.m22())
388 && std::isfinite(t.dx()) && std::isfinite(t.dy());
389 }
390};
391
392namespace detail {
393
395template<typename T>
396concept PositionalGeometric = std::same_as<T, QPointF> || std::same_as<T, QRectF>;
397
399template<typename T>
400concept SizeGeometric = std::same_as<T, QSizeF>;
401
402template<typename T>
403concept ScalarValue = std::is_arithmetic_v<T>;
404
405} // namespace detail
406
407} // namespace PhosphorAnimation
Position-carrying T whose damage region is fully determined by endpoints + overshoot.
Definition Interpolate.h:396
Definition Interpolate.h:403
Size-only T — caller must supply an anchor for damage rect.
Definition Interpolate.h:400
qreal srgbToLinear(qreal c)
Definition Interpolate.h:122
void okLabToLinear(const OkLab &lab, qreal &r, qreal &g, qreal &bv)
Definition Interpolate.h:176
qreal linearToSrgb(qreal c)
Definition Interpolate.h:126
QTransform lerpTransform(const QTransform &from, const QTransform &to, qreal t)
Definition Interpolate.h:313
QColor lerpColorOkLab(const QColor &from, const QColor &to, qreal t)
Definition Interpolate.h:191
qreal colorDistance(const QColor &from, const QColor &to)
L2 norm in linear-RGB space — matches the lerp space so velocity rescale uses a consistent metric.
Definition Interpolate.h:221
DecomposedTransform decomposeTransform(const QTransform &t)
QR-style decomposition on the 2x2 linear part under Qt's post-multiply row-vector convention: M = R x...
Definition Interpolate.h:269
OkLab linearToOkLab(qreal r, qreal g, qreal bv)
Definition Interpolate.h:159
bool isPureTranslate(const QTransform &t)
True if the 2x2 linear part is identity (only translation present).
Definition Interpolate.h:306
QTransform recomposeTransform(const DecomposedTransform &d)
Definition Interpolate.h:292
QColor lerpColorLinear(const QColor &from, const QColor &to, qreal t)
Definition Interpolate.h:131
Definition AnimatedValue.h:31
constexpr qreal kRectSizeEpsilonPx
Sub-pixel epsilon for rect size-change detection.
Definition Interpolate.h:26
ColorSpace
Interpolation space selector for AnimatedValue<QColor, ...>.
Definition Interpolate.h:29
@ Linear
sRGB -> linear lerp -> sRGB (radiometrically correct)
@ OkLab
sRGB -> OkLab lerp -> sRGB (perceptually uniform)
static qreal distance(const QColor &from, const QColor &to)
Definition Interpolate.h:241
static bool isFinite(const QColor &c)
Definition Interpolate.h:246
static QColor lerp(const QColor &from, const QColor &to, qreal t)
Linear-space sRGB lerp.
Definition Interpolate.h:237
static bool isFinite(const QPointF &p)
Definition Interpolate.h:68
static QPointF lerp(const QPointF &from, const QPointF &to, qreal t)
Definition Interpolate.h:59
static qreal distance(const QPointF &from, const QPointF &to)
Definition Interpolate.h:63
static bool isFinite(const QRectF &r)
Definition Interpolate.h:113
static QRectF lerp(const QRectF &from, const QRectF &to, qreal t)
Definition Interpolate.h:98
static qreal distance(const QRectF &from, const QRectF &to)
4-D Euclidean norm over (x, y, w, h).
Definition Interpolate.h:106
static QSizeF lerp(const QSizeF &from, const QSizeF &to, qreal t)
Definition Interpolate.h:77
static qreal distance(const QSizeF &from, const QSizeF &to)
Definition Interpolate.h:82
static bool isFinite(const QSizeF &s)
Definition Interpolate.h:89
static qreal distance(const QTransform &from, const QTransform &to)
Frobenius norm — mixes translation-pixels and rotation/scale units.
Definition Interpolate.h:374
static QTransform lerp(const QTransform &from, const QTransform &to, qreal t)
Definition Interpolate.h:367
static bool isFinite(const QTransform &t)
Definition Interpolate.h:385
static qreal distance(qreal from, qreal to)
Definition Interpolate.h:45
static bool isFinite(qreal v)
Definition Interpolate.h:50
static qreal lerp(qreal from, qreal to, qreal t)
Definition Interpolate.h:41
Type-specific linear interpolation and path-distance for AnimatedValue<T>.
Definition Interpolate.h:36
qreal sy
Definition Interpolate.h:263
qreal sx
Definition Interpolate.h:263
qreal shear
Definition Interpolate.h:264
qreal rotation
Definition Interpolate.h:262
qreal tx
Definition Interpolate.h:261
qreal ty
Definition Interpolate.h:261
Definition Interpolate.h:155
qreal b
Definition Interpolate.h:156
qreal L
Definition Interpolate.h:156
qreal a
Definition Interpolate.h:156