MayaFlux 1.0.0
Digital-First Multimedia Processing Framework
Loading...
Searching...
No Matches
Projection.hpp
Go to the documentation of this file.
1#pragma once
2
3#include "Bounds.hpp"
4
5namespace MayaFlux::Kinesis {
6
7/**
8 * @file Projection.hpp
9 * @brief NDC-to-value mappings, inverse to the forward placement performed by
10 * geometry functions.
11 *
12 * Each function returns a callable suitable for direct use as the projection
13 * argument of Portal::Forma::Geometry::wire_drag. They are pure: no window,
14 * no context, no element. A projection paired with the geometry function that
15 * shares its parameters is what makes a control draggable.
16 *
17 * Every function below also takes an optional @c live_offset callable,
18 * defaulted to empty and a no-op when omitted. Every existing call site is
19 * unaffected. When supplied, it shifts the cursor position into the frame
20 * the projection's fixed geometry (bounds/center/path, captured at
21 * construction) was computed in, rather than re-deriving that geometry per
22 * call. This is the inverse of translating the drawn control by the same
23 * offset. It has no dependency on Portal::Forma: the caller supplies the
24 * accessor (e.g. a Scrollable's live offset), and this file only ever
25 * calls it.
26 */
27
28/**
29 * @brief Fraction along one axis of @p bounds, inverse to fader placement.
30 *
31 * horizontal_fader and vertical_fader place the handle's leading edge at
32 * @c min + value * (extent - handle_extent), so the handle centre travels a
33 * span shorter than the track by one handle. This subtracts half a handle
34 * before dividing by the reduced span, so the handle centre tracks the cursor
35 * across the whole range rather than lagging by up to half a handle at the
36 * extremes.
37 *
38 * Pass @p handle_extent of zero for mappings with no handle inset, such as a
39 * level_meter used as a scrub region.
40 *
41 * @param bounds Track region in NDC.
42 * @param handle_extent Handle width for horizontal, height for vertical.
43 * @param horizontal True to map the x axis, false for y.
44 * @param live_offset Optional. Current translation of the drawn control,
45 * e.g. a Scrollable's live scroll offset.
46 * @return Callable producing a value in [0, 1].
47 */
48[[nodiscard]] inline std::function<float(glm::vec2)>
49axis_fraction(AABB2D bounds, float handle_extent = 0.F, bool horizontal = true,
50 const std::function<glm::vec2()>& live_offset = {}) noexcept
51{
52 const float extent = horizontal ? bounds.width() : bounds.height();
53 const float span = extent - handle_extent;
54 const float origin = horizontal ? bounds.min.x : bounds.min.y;
55 const float half = handle_extent * 0.5F;
56
57 return [origin, half, span, horizontal, live_offset](glm::vec2 p) -> float {
58 if (span <= 0.F)
59 return 0.F;
60 const float shift = live_offset ? (horizontal ? live_offset().x : live_offset().y) : 0.F;
61 const float pos = (horizontal ? p.x : p.y) - shift;
62 return std::clamp((pos - origin - half) / span, 0.F, 1.F);
63 };
64}
65
66/**
67 * @brief Unit-square coordinates of a point within @p bounds.
68 *
69 * Inverse to position_picker, which maps [0,1]² onto the region.
70 *
71 * @param bounds Region in NDC.
72 * @param live_offset Optional. Current translation of the drawn control.
73 * @return Callable producing a value in [0, 1]².
74 */
75[[nodiscard]] inline std::function<glm::vec2(glm::vec2)>
76unit_square(AABB2D bounds, const std::function<glm::vec2()>& live_offset = {}) noexcept
77{
78 const glm::vec2 origin = bounds.min;
79 const glm::vec2 extent { bounds.width(), bounds.height() };
80
81 return [origin, extent, live_offset](glm::vec2 p) -> glm::vec2 {
82 if (extent.x <= 0.F || extent.y <= 0.F)
83 return glm::vec2(0.F);
84 const glm::vec2 shift = live_offset ? live_offset() : glm::vec2(0.F);
85 return glm::clamp((p - shift - origin) / extent, glm::vec2(0.F), glm::vec2(1.F));
86 };
87}
88
89/**
90 * @brief Fraction along an angular sweep about @p center.
91 *
92 * Inverse to radial, which places the indicator at
93 * @c angle_start + value * (angle_end - angle_start). Handles sweeps in
94 * either direction and sweeps crossing the atan2 discontinuity. The cursor's
95 * distance from the centre is ignored, so the control remains responsive
96 * outside the drawn radius, which is what a knob gesture expects.
97 *
98 * Angles outside the sweep fall in a dead zone and snap to whichever end
99 * is nearer, so a knob dragged past its minimum stays at 0 instead of
100 * jumping to 1.
101 *
102 * Points at the exact centre return zero rather than an undefined angle.
103 *
104 * @param center Sweep centre in NDC.
105 * @param angle_start Angle in radians corresponding to value 0.
106 * @param angle_end Angle in radians corresponding to value 1.
107 * @param live_offset Optional. Current translation of the drawn control.
108 * @return Callable producing a value in [0, 1].
109 */
110[[nodiscard]] inline std::function<float(glm::vec2)>
111angle_fraction(glm::vec2 center, float angle_start, float angle_end,
112 const std::function<glm::vec2()>& live_offset = {}) noexcept
113{
114 const float delta = angle_end - angle_start;
115
116 return [center, angle_start, delta, live_offset](glm::vec2 p) -> float {
117 constexpr float k_two_pi = 6.283185307179586F;
118
119 if (std::abs(delta) < 1e-6F)
120 return 0.F;
121
122 const glm::vec2 shift = live_offset ? live_offset() : glm::vec2(0.F);
123 const glm::vec2 d = (p - shift) - center;
124 if (glm::dot(d, d) < 1e-12F)
125 return 0.F;
126
127 float rel = std::fmod(std::atan2(d.y, d.x) - angle_start, k_two_pi);
128 if (delta > 0.F && rel < 0.F) {
129 rel += k_two_pi;
130 } else if (delta < 0.F && rel > 0.F) {
131 rel -= k_two_pi;
132 }
133
134 const float travelled = std::abs(rel);
135 const float sweep = std::abs(delta);
136 const float gap = k_two_pi - sweep;
137 if (travelled <= sweep || gap <= 0.F)
138 return std::clamp(travelled / sweep, 0.F, 1.F);
139
140 return (travelled - sweep) < gap * 0.5F ? 1.F : 0.F;
141 };
142}
143
144/**
145 * @brief angle_fraction centred on a region.
146 *
147 * Companion to the region-taking radial overload, which derives its centre
148 * from the region in the same way.
149 */
150[[nodiscard]] inline std::function<float(glm::vec2)>
151angle_fraction(AABB2D region, float angle_start, float angle_end,
152 const std::function<glm::vec2()>& live_offset = {}) noexcept
153{
154 return angle_fraction(region.center(), angle_start, angle_end, live_offset);
155}
156
157/**
158 * @brief Normalized arc-length position of the closest point on a polyline.
159 *
160 * Inverse to stroke_slider, which places the handle at a fraction of the
161 * path's total arc length. Finds the nearest point across all segments and
162 * returns its cumulative length divided by the total, so the handle follows
163 * the cursor along the path regardless of how far off the path it strays.
164 *
165 * Cumulative lengths are computed once and captured; the returned callable
166 * allocates nothing per invocation beyond an optional live_offset() call.
167 *
168 * @param points Ordered polyline vertices in NDC. Copied into the closure.
169 * @param live_offset Optional. Current translation of the drawn path -
170 * shifts the cursor into the path's own frame rather
171 * than re-translating every point per call.
172 * @return Callable producing a value in [0, 1]. Returns 0 for paths with
173 * fewer than two points or zero total length.
174 */
175[[nodiscard]] inline std::function<float(glm::vec2)>
176path_fraction(std::span<const glm::vec2> points, const std::function<glm::vec2()>& live_offset = {})
177{
178 std::vector<glm::vec2> pts(points.begin(), points.end());
179 std::vector<float> cumulative(pts.size(), 0.F);
180
181 for (size_t i = 1; i < pts.size(); ++i)
182 cumulative[i] = cumulative[i - 1] + glm::length(pts[i] - pts[i - 1]);
183
184 const float total = pts.empty() ? 0.F : cumulative.back();
185
186 return [pts = std::move(pts), cumulative = std::move(cumulative), total, live_offset](
187 glm::vec2 p) -> float {
188 if (pts.size() < 2 || total <= 0.F)
189 return 0.F;
190
191 const glm::vec2 shift = live_offset ? live_offset() : glm::vec2(0.F);
192 const glm::vec2 q = p - shift;
193
194 float best_d2 = std::numeric_limits<float>::max();
195 float best_s = 0.F;
196
197 for (size_t i = 0; i + 1 < pts.size(); ++i) {
198 const glm::vec2 a = pts[i];
199 const glm::vec2 ab = pts[i + 1] - a;
200 const float len2 = glm::dot(ab, ab);
201
202 const float t = len2 > 1e-12F
203 ? glm::clamp(glm::dot(q - a, ab) / len2, 0.F, 1.F)
204 : 0.F;
205
206 const glm::vec2 diff = q - (a + t * ab);
207 const float d2 = glm::dot(diff, diff);
208
209 if (d2 < best_d2) {
210 best_d2 = d2;
211 best_s = cumulative[i] + t * std::sqrt(len2);
212 }
213 }
214
215 return std::clamp(best_s / total, 0.F, 1.F);
216 };
217}
218
219/**
220 * @brief Rescale a normalized projection onto an arbitrary range.
221 *
222 * Composes over any callable producing [0, 1], so a fader, knob, or stroke
223 * slider can drive a frequency, gain, or index without the call site
224 * repeating the remap.
225 *
226 * @param norm Projection producing a value in [0, 1].
227 * @param lo Value corresponding to 0.
228 * @param hi Value corresponding to 1.
229 */
230[[nodiscard]] inline std::function<float(glm::vec2)>
231scaled(std::function<float(glm::vec2)> norm, float lo, float hi)
232{
233 return [norm = std::move(norm), lo, hi](glm::vec2 p) -> float {
234 return lo + norm(p) * (hi - lo);
235 };
236}
237
238} // namespace MayaFlux::Kinesis
std::vector< glm::vec2 > * points
uint32_t total
size_t a
double q
std::function< glm::vec2(glm::vec2)> unit_square(AABB2D bounds, const std::function< glm::vec2()> &live_offset={}) noexcept
Unit-square coordinates of a point within bounds.
std::function< float(glm::vec2)> path_fraction(std::span< const glm::vec2 > points, const std::function< glm::vec2()> &live_offset={})
Normalized arc-length position of the closest point on a polyline.
std::function< float(glm::vec2)> scaled(std::function< float(glm::vec2)> norm, float lo, float hi)
Rescale a normalized projection onto an arbitrary range.
std::function< float(glm::vec2)> angle_fraction(glm::vec2 center, float angle_start, float angle_end, const std::function< glm::vec2()> &live_offset={}) noexcept
Fraction along an angular sweep about center.
std::function< float(glm::vec2)> axis_fraction(AABB2D bounds, float handle_extent=0.F, bool horizontal=true, const std::function< glm::vec2()> &live_offset={}) noexcept
Fraction along one axis of bounds, inverse to fader placement.
float height() const noexcept
Definition Bounds.hpp:80
glm::vec2 center() const noexcept
Definition Bounds.hpp:82
float width() const noexcept
Definition Bounds.hpp:79
Axis-aligned bounding rectangle in a 2D coordinate space.
Definition Bounds.hpp:63