MayaFlux 1.0.0
Digital-First Multimedia Processing Framework
Loading...
Searching...
No Matches
Scalar.hpp
Go to the documentation of this file.
1#pragma once
2
3namespace MayaFlux::Kinesis {
4
5// =============================================================================
6// Range mapping
7// =============================================================================
8
9/**
10 * @brief Map @p x from [in_lo, in_hi] to [out_lo, out_hi], unclamped.
11 *
12 * Values outside [in_lo, in_hi] extrapolate linearly. Use map_clamped()
13 * when the output must stay within [out_lo, out_hi].
14 *
15 * Degenerate input range (in_lo == in_hi) returns out_lo.
16 */
17template <typename T>
18[[nodiscard]] constexpr T map(T x, T in_lo, T in_hi, T out_lo, T out_hi) noexcept
19{
20 const T in_range = in_hi - in_lo;
21 if (in_range == T { 0 })
22 return out_lo;
23 return out_lo + (x - in_lo) / in_range * (out_hi - out_lo);
24}
25
26/**
27 * @brief Map @p x from [in_lo, in_hi] to [out_lo, out_hi], clamped to
28 * [out_lo, out_hi].
29 */
30template <typename T>
31[[nodiscard]] constexpr T map_clamped(T x, T in_lo, T in_hi, T out_lo, T out_hi) noexcept
32{
33 const T v = map(x, in_lo, in_hi, out_lo, out_hi);
34 return std::clamp(v, std::min(out_lo, out_hi), std::max(out_lo, out_hi));
35}
36
37/**
38 * @brief Normalize @p x in [lo, hi] to [0, 1], unclamped.
39 *
40 * Distinct from Kinesis::Discrete::normalize, which operates in-place on
41 * a span and computes its own min/max. This is the scalar equivalent.
42 *
43 * Degenerate range (lo == hi) returns 0.
44 */
45template <typename T>
46[[nodiscard]] constexpr T normalize(T x, T lo, T hi) noexcept
47{
48 const T range = hi - lo;
49 if (range == T { 0 })
50 return T { 0 };
51 return (x - lo) / range;
52}
53
54/**
55 * @brief Denormalize @p t from [0, 1] to [lo, hi].
56 *
57 * Equivalent to glm::mix(lo, hi, t) but named for legibility at call sites
58 * where the intent is range reconstruction rather than interpolation.
59 */
60template <typename T>
61[[nodiscard]] constexpr T denormalize(T t, T lo, T hi) noexcept
62{
63 return lo + t * (hi - lo);
64}
65
66// =============================================================================
67// Tables
68// =============================================================================
69
70/**
71 * @brief Read an evenly spaced table at a position in [0, 1], interpolating linearly.
72 * @param table Samples spread evenly from position 0 (first) to 1 (last)
73 * @param t Position, clamped to [0, 1]
74 * @return Interpolated value; zero for an empty table, the entry for a single one
75 *
76 * The lookup under TimeMaps::piecewise_linear and the table kernels in
77 * Discrete/Kernels.
78 */
79[[nodiscard]] inline double sample_table(std::span<const double> table, double t) noexcept
80{
81 const size_t n = table.size();
82 if (n == 0)
83 return 0.0;
84 if (n == 1)
85 return table.front();
86
87 const double x = std::clamp(t, 0.0, 1.0) * static_cast<double>(n - 1);
88 const auto i = std::min(static_cast<size_t>(x), n - 2);
89 const double f = x - static_cast<double>(i);
90 return table[i] + (table[i + 1] - table[i]) * f;
91}
92
93// =============================================================================
94// Smoothing
95// =============================================================================
96
97/**
98 * @brief GLSL smoothstep: Hermite interpolation in [lo, hi].
99 *
100 * Returns 0 when x <= lo, 1 when x >= hi, and a smooth cubic curve
101 * in between. Equivalent to GLSL smoothstep(lo, hi, x).
102 *
103 * GLM provides this as glm::smoothstep but requires glm/gtx/compatibility.hpp
104 * and a vector type. This overload accepts any scalar T.
105 */
106template <typename T>
107[[nodiscard]] constexpr T smoothstep(T lo, T hi, T x) noexcept
108{
109 const T t = std::clamp((x - lo) / (hi - lo), T { 0 }, T { 1 });
110 return t * t * (T { 3 } - T { 2 } * t);
111}
112
113/**
114 * @brief Ken Perlin's quintic smootherstep: C2-continuous in [lo, hi].
115 *
116 * Derivative is zero at both endpoints (unlike smoothstep which has zero
117 * first derivative only). Use when second-derivative continuity matters:
118 * camera motion, envelope transitions, SDF blending weights.
119 */
120template <typename T>
121[[nodiscard]] constexpr T smootherstep(T lo, T hi, T x) noexcept
122{
123 const T t = std::clamp((x - lo) / (hi - lo), T { 0 }, T { 1 });
124 return t * t * t * (t * (t * T { 6 } - T { 15 }) + T { 10 });
125}
126
127/**
128 * @brief Exponential smoothing: move @p current toward @p target at rate
129 * @p smoothing over time step @p dt.
130 *
131 * @p smoothing is a time constant in seconds: the distance to target falls
132 * by a factor of e (to about 37 percent) every @p smoothing seconds, and
133 * halves every 0.69 * @p smoothing seconds. At smoothing = 0.1 the value
134 * tracks quickly; at smoothing = 2.0 it lags heavily.
135 *
136 * Framerate-independent. Equivalent to the "lerp every frame" pattern
137 * done correctly:
138 * current = lerp(current, target, 1 - exp(-dt / smoothing))
139 *
140 * Degenerate case (smoothing <= 0) snaps to target immediately.
141 */
142template <typename T>
143[[nodiscard]] inline T damp(T current, T target, T smoothing, T dt) noexcept
144{
145 if (smoothing <= T { 0 })
146 return target;
147 return current + (target - current) * (T { 1 } - std::exp(-dt / smoothing));
148}
149
150/**
151 * @brief damp with separate time constants for rising and falling.
152 *
153 * Moves @p current toward @p target using @p rise when the target is above
154 * it and @p fall when below, both on the same scale as the single value of
155 * damp. A level that swells slowly and fades quickly, or the reverse, is one
156 * pair of numbers.
157 */
158template <typename T>
159[[nodiscard]] inline T damp_asymmetric(T current, T target, T rise, T fall, T dt) noexcept
160{
161 return damp(current, target, target > current ? rise : fall, dt);
162}
163
164/**
165 * @brief damp_asymmetric over arrays, in place.
166 * @param current Levels, updated
167 * @param target Where each level is heading
168 * @param rise Rise time constant per level
169 * @param fall Fall time constant per level
170 * @param dt Time step
171 *
172 * Runs over the shortest of the four spans. Each level has its own pair of
173 * numbers, so levels given different values move independently.
174 */
175inline void damp_asymmetric(
176 std::span<double> current,
177 std::span<const double> target,
178 std::span<const double> rise,
179 std::span<const double> fall,
180 double dt) noexcept
181{
182 const size_t count = std::min({ current.size(), target.size(), rise.size(), fall.size() });
183 for (size_t i = 0; i < count; ++i)
184 current[i] = damp_asymmetric(current[i], target[i], rise[i], fall[i], dt);
185}
186
187// =============================================================================
188// Bounding
189// =============================================================================
190
191/**
192 * @brief Smooth bound to (-limit, limit): limit * tanh(x / limit).
193 *
194 * Close to the identity for small |x|, approaches +-limit for large |x|.
195 * A non-positive limit gives zero.
196 */
197template <typename T>
198[[nodiscard]] inline T soft_clip(T x, T limit = T { 1 }) noexcept
199{
200 if (limit <= T { 0 })
201 return T { 0 };
202 return limit * std::tanh(x / limit);
203}
204
205// =============================================================================
206// Wrapping and folding
207// =============================================================================
208
209/**
210 * @brief Wrap @p x into [lo, hi) with modulo semantics.
211 *
212 * Unlike std::fmod, handles negative values and arbitrary lo correctly.
213 * Equivalent to the GLSL mod() applied after shifting by lo.
214 *
215 * Example: wrap(-0.1F, 0.F, 1.F) -> 0.9F
216 */
217template <typename T>
218[[nodiscard]] inline T wrap(T x, T lo, T hi) noexcept
219{
220 const T range = hi - lo;
221 if (range <= T { 0 })
222 return lo;
223 return x - range * std::floor((x - lo) / range);
224}
225
226/**
227 * @brief Ping-pong (triangle wave): fold @p x back and forth in [lo, hi].
228 *
229 * At x = lo the value is lo. It increases to hi then folds back toward lo.
230 * Period is 2 * (hi - lo). Use for oscillating parameters, bounce animations,
231 * or any signal that should reverse at the boundary instead of wrapping.
232 *
233 * Example: ping_pong(1.3F, 0.F, 1.F) -> 0.7F
234 */
235template <typename T>
236[[nodiscard]] inline T ping_pong(T x, T lo, T hi) noexcept
237{
238 const T range = hi - lo;
239 if (range <= T { 0 })
240 return lo;
241 const T shifted = x - lo;
242 const T period = T { 2 } * range;
243 const T t = shifted - period * std::floor(shifted / period);
244 return lo + (t < range ? t : period - t);
245}
246
247// =============================================================================
248// Snapping
249// =============================================================================
250
251/**
252 * @brief Round @p x to the nearest multiple of @p step.
253 *
254 * std::round(x / step) * step but named. step must be > 0.
255 *
256 * Example: snap(0.73F, 0.25F) -> 0.75F
257 */
258template <typename T>
259[[nodiscard]] inline T snap(T x, T step) noexcept
260{
261 if (step <= T { 0 })
262 return x;
263 return std::round(x / step) * step;
264}
265
266/**
267 * @brief Snap @p x to the nearest multiple of @p step within [lo, hi].
268 */
269template <typename T>
270[[nodiscard]] inline T snap(T x, T lo, T hi, T step) noexcept
271{
272 return std::clamp(snap(x, step), lo, hi);
273}
274
275// =============================================================================
276// Easing
277// =============================================================================
278
279/**
280 * @brief Cubic ease-in: slow start, fast end.
281 * @param t Normalized time in [0, 1].
282 */
283template <typename T>
284[[nodiscard]] constexpr T ease_in(T t) noexcept
285{
286 return t * t * t;
287}
288
289/**
290 * @brief Cubic ease-out: fast start, slow end.
291 * @param t Normalized time in [0, 1].
292 */
293template <typename T>
294[[nodiscard]] constexpr T ease_out(T t) noexcept
295{
296 const T u = T { 1 } - t;
297 return T { 1 } - u * u * u;
298}
299
300/**
301 * @brief Cubic ease-in-out: slow start, fast middle, slow end.
302 * @param t Normalized time in [0, 1].
303 */
304template <typename T>
305[[nodiscard]] constexpr T ease_in_out(T t) noexcept
306{
307 return t < T { 0.5 }
308 ? T { 4 } * t * t * t
309 : T { 1 } - T { 4 } * (T { 1 } - t) * (T { 1 } - t) * (T { 1 } - t) * T { 0.5 };
310}
311
312// =============================================================================
313// Deadzone and sign
314// =============================================================================
315
316/**
317 * @brief Zero values within [-threshold, threshold] and rescale the remainder
318 * to fill [0, 1] (or [-1, 1] for negative inputs).
319 *
320 * Standard controller/HID deadzone. Input and output are in [-1, 1].
321 * threshold must be in [0, 1).
322 *
323 * Example: deadzone(0.1F, 0.2F) -> 0.0F (inside dead zone)
324 * deadzone(0.6F, 0.2F) -> 0.5F (rescaled from [0.2, 1.0] to [0, 1])
325 */
326template <typename T>
327[[nodiscard]] constexpr T deadzone(T x, T threshold) noexcept
328{
329 if (std::abs(x) <= threshold)
330 return T { 0 };
331 const T sign = x > T { 0 } ? T { 1 } : T { -1 };
332 return sign * (std::abs(x) - threshold) / (T { 1 } - threshold);
333}
334
335/**
336 * @brief Sign of @p x, returning -1 or +1, never 0.
337 *
338 * std::copysign(1, x) but readable. Useful when zero must be treated as
339 * positive (e.g. initial state of a direction accumulator).
340 */
341template <typename T>
342[[nodiscard]] constexpr T sign_nonzero(T x) noexcept
343{
344 return x < T { 0 } ? T { -1 } : T { 1 };
345}
346
347} // namespace MayaFlux::Kinesis
glm::vec2 current
size_t count
float threshold
T ping_pong(T x, T lo, T hi) noexcept
Ping-pong (triangle wave): fold x back and forth in [lo, hi].
Definition Scalar.hpp:236
T damp(T current, T target, T smoothing, T dt) noexcept
Exponential smoothing: move current toward target at rate smoothing over time step dt.
Definition Scalar.hpp:143
T damp_asymmetric(T current, T target, T rise, T fall, T dt) noexcept
damp with separate time constants for rising and falling.
Definition Scalar.hpp:159
constexpr T denormalize(T t, T lo, T hi) noexcept
Denormalize t from [0, 1] to [lo, hi].
Definition Scalar.hpp:61
T snap(T x, T step) noexcept
Round x to the nearest multiple of step.
Definition Scalar.hpp:259
double sample_table(std::span< const double > table, double t) noexcept
Read an evenly spaced table at a position in [0, 1], interpolating linearly.
Definition Scalar.hpp:79
constexpr T ease_in(T t) noexcept
Cubic ease-in: slow start, fast end.
Definition Scalar.hpp:284
constexpr T smootherstep(T lo, T hi, T x) noexcept
Ken Perlin's quintic smootherstep: C2-continuous in [lo, hi].
Definition Scalar.hpp:121
T soft_clip(T x, T limit=T { 1 }) noexcept
Smooth bound to (-limit, limit): limit * tanh(x / limit).
Definition Scalar.hpp:198
constexpr T ease_in_out(T t) noexcept
Cubic ease-in-out: slow start, fast middle, slow end.
Definition Scalar.hpp:305
constexpr T map(T x, T in_lo, T in_hi, T out_lo, T out_hi) noexcept
Map x from [in_lo, in_hi] to [out_lo, out_hi], unclamped.
Definition Scalar.hpp:18
constexpr T ease_out(T t) noexcept
Cubic ease-out: fast start, slow end.
Definition Scalar.hpp:294
constexpr T map_clamped(T x, T in_lo, T in_hi, T out_lo, T out_hi) noexcept
Map x from [in_lo, in_hi] to [out_lo, out_hi], clamped to [out_lo, out_hi].
Definition Scalar.hpp:31
constexpr T smoothstep(T lo, T hi, T x) noexcept
GLSL smoothstep: Hermite interpolation in [lo, hi].
Definition Scalar.hpp:107
T wrap(T x, T lo, T hi) noexcept
Wrap x into [lo, hi) with modulo semantics.
Definition Scalar.hpp:218
constexpr T sign_nonzero(T x) noexcept
Sign of x, returning -1 or +1, never 0.
Definition Scalar.hpp:342
constexpr T deadzone(T x, T threshold) noexcept
Zero values within [-threshold, threshold] and rescale the remainder to fill [0, 1] (or [-1,...
Definition Scalar.hpp:327