MayaFlux 0.5.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
18/**
19 * @brief Fraction along one axis of @p bounds, inverse to fader placement.
20 *
21 * horizontal_fader and vertical_fader place the handle's leading edge at
22 * @c min + value * (extent - handle_extent), so the handle centre travels a
23 * span shorter than the track by one handle. This subtracts half a handle
24 * before dividing by the reduced span, so the handle centre tracks the cursor
25 * across the whole range rather than lagging by up to half a handle at the
26 * extremes.
27 *
28 * Pass @p handle_extent of zero for mappings with no handle inset, such as a
29 * level_meter used as a scrub region.
30 *
31 * @param bounds Track region in NDC.
32 * @param handle_extent Handle width for horizontal, height for vertical.
33 * @param horizontal True to map the x axis, false for y.
34 * @return Callable producing a value in [0, 1].
35 */
36[[nodiscard]] inline std::function<float(glm::vec2)>
37axis_fraction(AABB2D bounds, float handle_extent = 0.F, bool horizontal = true) noexcept
38{
39 const float extent = horizontal ? bounds.width() : bounds.height();
40 const float span = extent - handle_extent;
41 const float origin = horizontal ? bounds.min.x : bounds.min.y;
42 const float half = handle_extent * 0.5F;
43
44 return [origin, half, span, horizontal](glm::vec2 p) -> float {
45 if (span <= 0.F)
46 return 0.F;
47 const float pos = horizontal ? p.x : p.y;
48 return std::clamp((pos - origin - half) / span, 0.F, 1.F);
49 };
50}
51
52/**
53 * @brief Unit-square coordinates of a point within @p bounds.
54 *
55 * Inverse to position_picker, which maps [0,1]² onto the region.
56 *
57 * @param bounds Region in NDC.
58 * @return Callable producing a value in [0, 1]².
59 */
60[[nodiscard]] inline std::function<glm::vec2(glm::vec2)>
61unit_square(AABB2D bounds) noexcept
62{
63 const glm::vec2 origin = bounds.min;
64 const glm::vec2 extent { bounds.width(), bounds.height() };
65
66 return [origin, extent](glm::vec2 p) -> glm::vec2 {
67 if (extent.x <= 0.F || extent.y <= 0.F)
68 return glm::vec2(0.F);
69 return glm::clamp((p - origin) / extent, glm::vec2(0.F), glm::vec2(1.F));
70 };
71}
72
73/**
74 * @brief Fraction along an angular sweep about @p center.
75 *
76 * Inverse to radial, which places the indicator at
77 * @c angle_start + value * (angle_end - angle_start). Handles sweeps in
78 * either direction and sweeps crossing the atan2 discontinuity. The cursor's
79 * distance from the centre is ignored, so the control remains responsive
80 * outside the drawn radius, which is what a knob gesture expects.
81 *
82 * Points at the exact centre return zero rather than an undefined angle.
83 *
84 * @param center Sweep centre in NDC.
85 * @param angle_start Angle in radians corresponding to value 0.
86 * @param angle_end Angle in radians corresponding to value 1.
87 * @return Callable producing a value in [0, 1].
88 */
89[[nodiscard]] inline std::function<float(glm::vec2)>
90angle_fraction(glm::vec2 center, float angle_start, float angle_end) noexcept
91{
92 const float delta = angle_end - angle_start;
93
94 return [center, angle_start, delta](glm::vec2 p) -> float {
95 constexpr float k_two_pi = 6.283185307179586F;
96
97 if (std::abs(delta) < 1e-6F)
98 return 0.F;
99
100 const glm::vec2 d = p - center;
101 if (glm::dot(d, d) < 1e-12F)
102 return 0.F;
103
104 float rel = std::fmod(std::atan2(d.y, d.x) - angle_start, k_two_pi);
105 if (delta > 0.F && rel < 0.F) {
106 rel += k_two_pi;
107 } else if (delta < 0.F && rel > 0.F) {
108 rel -= k_two_pi;
109 }
110
111 return std::clamp(rel / delta, 0.F, 1.F);
112 };
113}
114
115/**
116 * @brief angle_fraction centred on a region.
117 *
118 * Companion to the region-taking radial overload, which derives its centre
119 * from the region in the same way.
120 */
121[[nodiscard]] inline std::function<float(glm::vec2)>
122angle_fraction(AABB2D region, float angle_start, float angle_end) noexcept
123{
124 return angle_fraction(region.center(), angle_start, angle_end);
125}
126
127/**
128 * @brief Normalized arc-length position of the closest point on a polyline.
129 *
130 * Inverse to stroke_slider, which places the handle at a fraction of the
131 * path's total arc length. Finds the nearest point across all segments and
132 * returns its cumulative length divided by the total, so the handle follows
133 * the cursor along the path regardless of how far off the path it strays.
134 *
135 * Cumulative lengths are computed once and captured; the returned callable
136 * allocates nothing per invocation.
137 *
138 * @param points Ordered polyline vertices in NDC. Copied into the closure.
139 * @return Callable producing a value in [0, 1]. Returns 0 for paths with
140 * fewer than two points or zero total length.
141 */
142[[nodiscard]] inline std::function<float(glm::vec2)>
143path_fraction(std::span<const glm::vec2> points)
144{
145 std::vector<glm::vec2> pts(points.begin(), points.end());
146 std::vector<float> cumulative(pts.size(), 0.F);
147
148 for (size_t i = 1; i < pts.size(); ++i)
149 cumulative[i] = cumulative[i - 1] + glm::length(pts[i] - pts[i - 1]);
150
151 const float total = pts.empty() ? 0.F : cumulative.back();
152
153 return [pts = std::move(pts), cumulative = std::move(cumulative), total](
154 glm::vec2 p) -> float {
155 if (pts.size() < 2 || total <= 0.F)
156 return 0.F;
157
158 float best_d2 = std::numeric_limits<float>::max();
159 float best_s = 0.F;
160
161 for (size_t i = 0; i + 1 < pts.size(); ++i) {
162 const glm::vec2 a = pts[i];
163 const glm::vec2 ab = pts[i + 1] - a;
164 const float len2 = glm::dot(ab, ab);
165
166 const float t = len2 > 1e-12F
167 ? glm::clamp(glm::dot(p - a, ab) / len2, 0.F, 1.F)
168 : 0.F;
169
170 const glm::vec2 diff = p - (a + t * ab);
171 const float d2 = glm::dot(diff, diff);
172
173 if (d2 < best_d2) {
174 best_d2 = d2;
175 best_s = cumulative[i] + t * std::sqrt(len2);
176 }
177 }
178
179 return std::clamp(best_s / total, 0.F, 1.F);
180 };
181}
182
183/**
184 * @brief Rescale a normalized projection onto an arbitrary range.
185 *
186 * Composes over any callable producing [0, 1], so a fader, knob, or stroke
187 * slider can drive a frequency, gain, or index without the call site
188 * repeating the remap.
189 *
190 * @param norm Projection producing a value in [0, 1].
191 * @param lo Value corresponding to 0.
192 * @param hi Value corresponding to 1.
193 */
194[[nodiscard]] inline std::function<float(glm::vec2)>
195scaled(std::function<float(glm::vec2)> norm, float lo, float hi)
196{
197 return [norm = std::move(norm), lo, hi](glm::vec2 p) -> float {
198 return lo + norm(p) * (hi - lo);
199 };
200}
201
202} // namespace MayaFlux::Kinesis
std::vector< glm::vec2 > * points
size_t a
float lo
float hi
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)> path_fraction(std::span< const glm::vec2 > points)
Normalized arc-length position of the closest point on a polyline.
std::function< float(glm::vec2)> angle_fraction(glm::vec2 center, float angle_start, float angle_end) noexcept
Fraction along an angular sweep about center.
std::function< glm::vec2(glm::vec2)> unit_square(AABB2D bounds) noexcept
Unit-square coordinates of a point within bounds.
std::function< float(glm::vec2)> axis_fraction(AABB2D bounds, float handle_extent=0.F, bool horizontal=true) noexcept
Fraction along one axis of bounds, inverse to fader placement.
float height() const noexcept
Definition Bounds.hpp:38
float width() const noexcept
Definition Bounds.hpp:37
Axis-aligned bounding rectangle in a 2D coordinate space.
Definition Bounds.hpp:21