izzi
SVG SUBSET C++ API
Loading...
Searching...
No Matches
a60-svg-curves-grignani.h
Go to the documentation of this file.
1// izzi ribbon curves -*- mode: C++ -*-
2
3// Copyright (c) 2026, Benjamin De Kosnik <b.dekosnik@gmail.com>
4
5// This file is part of the alpha60 library. This library is free
6// software; you can redistribute it and/or modify it under the terms
7// of the GNU General Public License as published by the Free Software
8// Foundation; either version 3, or (at your option) any later
9// version.
10
11// This library is distributed in the hope that it will be useful, but
12// WITHOUT ANY WARRANTY; without even the implied warranty of
13// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14// General Public License for more details.
15
16#ifndef a60_SVG_CURVES_GRIGNANI_H
17#define a60_SVG_CURVES_GRIGNANI_H 1
18
19#include <iostream>
20#include <vector>
21#include <string>
22#include <numbers>
23#include <cmath>
24#include <format>
25#include <tuple>
26#include <algorithm>
27#include <array>
28
29namespace svg {
30
31/// Define types using std::tuple as requested
32using point_2d = std::tuple<double, double>;
33using point_3d = std::tuple<double, double, double>;
34
35
36/// Helper: Normalize a vector tuple
39{
40 auto [x, y, z] = v;
41 double len = std::hypot(x, y, z);
42 if (len == 0.0)
43 return {0, 0, 0};
44 return {x / len, y / len, z / len};
45}
46
47//// Helper: Cross product of two tuples
50{
51 auto [ax, ay, az] = a;
52 auto [bx, by, bz] = b;
53 return { ay * bz - az * by, az * bx - ax * bz, ax * by - ay * bx };
54}
55
56
57/**
58 * Rotates a 3D point tuple to simulate camera orientation.
59 */
61rotate_point(point_3d p, double angle_x, double angle_y)
62{
63 auto [x, y, z] = p;
64
65 // Rotate around X axis
66 double y1 = y * std::cos(angle_x) - z * std::sin(angle_x);
67 double z1 = y * std::sin(angle_x) + z * std::cos(angle_x);
68 double x1 = x;
69
70 // Rotate around Y axis
71 double x2 = x1 * std::cos(angle_y) + z1 * std::sin(angle_y);
72 double z2 = -x1 * std::sin(angle_y) + z1 * std::cos(angle_y);
73 double y2 = y1;
74
75 return {x2, y2, z2};
76}
77
78
79/**
80 * Configuration for the 3D ribbon folding algorithm.
81 * Identifiers are now strictly snake_case.
82 */
84{
85 double fold_amplitude = 1.0; // Radius of the main loops
86 double fold_frequency = 3.0; // How many times it loops
87 double torsion = 0.8; // The vertical depth (Z-height)
88 double roll_speed = 1.5; // How fast the ribbon twists
89 double view_tilt_x = 0.6; // Camera angle X (radians)
90 double view_tilt_y = 0.7; // Camera angle Y (radians)
91};
92
93
94/**
95 * Generates an SVG path by simulating a 3D rolling ribbon and
96 * projecting it to 2D.
97 */
98string
99make_rolling_ribbon(double origin_x, double origin_y, double scale,
100 int ribbon_strands, double ribbon_width,
101 ribbon_config config)
102{
103 using std::numbers::pi;
104
105 // Higher steps = smoother curves
106 const int steps = 360;
107
108 double gap = ribbon_width;
109 double stride = ribbon_width + gap;
110 double total_bundle_width = (ribbon_strands * stride) - gap;
111
112 std::string path_data;
113
114 // --- 1. Define the 3D Spine Function ---
115 auto get_spine_3d = [&](double t) -> point_3d
116 {
117 double angle_k = config.fold_frequency * t;
118
119 // Modulate radius for loops
120 double r = config.fold_amplitude + 0.4 * std::cos(angle_k);
121
122 double x = r * std::cos(t);
123 double y = r * std::sin(t);
124 double z = config.torsion * std::sin(angle_k);
125 return { x, y, z };
126 };
127
128 // --- 2. Generate Each Strand ---
129 for (int s = 0; s < ribbon_strands; ++s)
130 {
131 double offset = (s * stride) - (total_bundle_width / 2.0);
132
133 std::vector<point_2d> edge_left_2d;
134 std::vector<point_2d> edge_right_2d;
135
136 for (int i = 0; i <= steps; ++i)
137 {
138 double t = (static_cast<double>(i) / steps) * 2.0 * pi;
139 double epsilon = 0.005;
140
141 // A. Get Spine Point & Tangent
142 point_3d p0 = get_spine_3d(t);
143 point_3d p1 = get_spine_3d(t + epsilon);
144
145 auto [p0x, p0y, p0z] = p0;
146 auto [p1x, p1y, p1z] = p1;
147
148 point_3d tangent = normalize({ p1x - p0x, p1y - p0y, p1z - p0z });
149
150 // B. Calculate Surface Normal
151 // Arbitrary "Up" vector to calculate frame
152 point_3d up = {0, 0, 1};
153 if (std::abs(std::get<2>(tangent)) > 0.95)
154 up = {1, 0, 0};
155
156 point_3d binormal = normalize(cross_product(tangent, up));
157 point_3d normal = cross_product(binormal, tangent); // Already normalized
158
159 // C. Apply "Roll" (Twist)
160 // Rotate the Normal vector around the Tangent axis
161 double current_roll = config.roll_speed * t * 2.0;
162
163 // Unpack vectors for rotation math
164 auto [nx, ny, nz] = normal;
165 // auto [tx, ty, tz] = tangent; unused
166
167 // Calculate (Tangent x Normal) for Rodrigues formula
168 auto [kx, ky, kz] = cross_product(tangent, normal);
169
170 double c = std::cos(current_roll);
171 double s_val = std::sin(current_roll); // 's' is taken by loop var
172
173 // Rodrigues rotation formula: V_rot = V*cos + (K x V)*sin
174 point_3d surface_vec = {
175 nx * c + kx * s_val,
176 ny * c + ky * s_val,
177 nz * c + kz * s_val
178 };
179 auto [svx, svy, svz] = surface_vec;
180
181 // D. Compute 3D positions
182 point_3d p_start_3d = {
183 p0x + svx * offset,
184 p0y + svy * offset,
185 p0z + svz * offset
186 };
187
188 point_3d p_end_3d = {
189 p0x + svx * (offset + ribbon_width),
190 p0y + svy * (offset + ribbon_width),
191 p0z + svz * (offset + ribbon_width)
192 };
193
194 // E. Project to 2D
195 auto [rx_start, ry_start, rz_start] = rotate_point(p_start_3d, config.view_tilt_x, config.view_tilt_y);
196 auto [rx_end, ry_end, rz_end] = rotate_point(p_end_3d, config.view_tilt_x, config.view_tilt_y);
197
198 edge_left_2d.emplace_back(origin_x + rx_start * scale, origin_y + ry_start * scale);
199 edge_right_2d.emplace_back(origin_x + rx_end * scale, origin_y + ry_end * scale);
200 }
201
202 // --- 3. Construct SVG Path String ---
203 auto [start_x, start_y] = edge_left_2d[0];
204 path_data += std::format("M {:.2f} {:.2f} ", start_x, start_y);
205
206 for (size_t k = 1; k < edge_left_2d.size(); ++k)
207 {
208 auto [lx, ly] = edge_left_2d[k];
209 path_data += std::format("L {:.2f} {:.2f} ", lx, ly);
210 }
211
212 auto [last_rx, last_ry] = edge_right_2d.back();
213 path_data += std::format("L {:.2f} {:.2f} ", last_rx, last_ry);
214
215 for (int k = static_cast<int>(edge_right_2d.size()) - 2; k >= 0; --k)
216 {
217 auto [rx, ry] = edge_right_2d[k];
218 path_data += std::format("L {:.2f} {:.2f} ", rx, ry);
219 }
220
221 path_data += "Z ";
222 }
223
224 return std::format("<path d=\"{}\" fill=\"black\" stroke=\"none\" />", path_data);
225}
226
227
228
229/// Config struct.
231{
232 double amplitude = 1.0;
233 double frequency = 1.0;
234 double phase_shift = 0.0;
235 double decay = 0.0;
236 double view_tilt_x = 0.5;
237 double view_tilt_y = 0.6;
238};
239
240
241std::string
242make_ripple_ribbon(double origin_x, double origin_y, double length,
243 int ribbon_strands, double ribbon_width, ripple_config config)
244{
245 using std::numbers::pi;
246
247 const int steps = 180;
248 double gap = ribbon_width;
249 double stride = ribbon_width + gap;
250 double total_bundle_width = (ribbon_strands * stride) - gap;
251
252 // Local lambda
253 auto get_spine_3d = [&](double t) -> point_3d
254 {
255 // t: -1.0 to 1.0
256 double x = t * (length / 2.0);
257 double wave_arg = (t * pi * config.frequency) + config.phase_shift;
258
259 double amp_mod = 1.0;
260 if (config.decay > 0.0)
261 {
262 // Linear decay: 1.0 at start, decreasing based on decay factor
263 // To make it look nice, we decay based on progress 0..1
264 double progress = (t + 1.0) / 2.0;
265 amp_mod = std::max(0.0, 1.0 - (config.decay * progress));
266 }
267
268 double z = config.amplitude * std::sin(wave_arg) * amp_mod;
269 return {x, 0.0, z};
270 };
271
272 std::string path_data;
273 for (int s = 0; s < ribbon_strands; ++s)
274 {
275 double offset_val = (s * stride) - (total_bundle_width / 2.0);
276 std::vector<point_2d> edge_left, edge_right;
277
278 for (int i = 0; i <= steps; ++i)
279 {
280 double t = -1.0 + (2.0 * static_cast<double>(i) / steps);
281 double epsilon = 0.01;
282
283 point_3d p0 = get_spine_3d(t);
284 point_3d p1 = get_spine_3d(t + epsilon);
285 auto [p0x, p0y, p0z] = p0;
286 auto [p1x, p1y, p1z] = p1;
287
288 point_3d tangent = normalize({ p1x - p0x, p1y - p0y, p1z - p0z });
289 point_3d up = {0, 0, 1};
290 point_3d binormal = normalize(cross_product(tangent, up));
291
292 // Ensure consistent orientation
293 if (std::get<1>(binormal) < 0)
294 {
295 binormal = { -std::get<0>(binormal), -std::get<1>(binormal), -std::get<2>(binormal) };
296 }
297 auto [bx, by, bz] = binormal;
298
299 point_3d p_start_3d = { p0x + bx * offset_val, p0y + by * offset_val, p0z + bz * offset_val };
300 point_3d p_end_3d = { p0x + bx * (offset_val + ribbon_width), p0y + by * (offset_val + ribbon_width), p0z + bz * (offset_val + ribbon_width) };
301
302 auto [rx_s, ry_s, rz_s] = rotate_point(p_start_3d, config.view_tilt_x, config.view_tilt_y);
303 auto [rx_e, ry_e, rz_e] = rotate_point(p_end_3d, config.view_tilt_x, config.view_tilt_y);
304
305 edge_left.emplace_back(origin_x + rx_s, origin_y + ry_s);
306 edge_right.emplace_back(origin_x + rx_e, origin_y + ry_e);
307 }
308
309 auto [sx, sy] = edge_left[0];
310 path_data += std::format("M {:.2f} {:.2f} ", sx, sy);
311 for (size_t k = 1; k < edge_left.size(); ++k)
312 {
313 auto [lx, ly] = edge_left[k];
314 path_data += std::format("L {:.2f} {:.2f} ", lx, ly);
315 }
316 auto [ex, ey] = edge_right.back();
317 path_data += std::format("L {:.2f} {:.2f} ", ex, ey);
318 for (int k = static_cast<int>(edge_right.size()) - 2; k >= 0; --k)
319 {
320 auto [rx, ry] = edge_right[k];
321 path_data += std::format("L {:.2f} {:.2f} ", rx, ry);
322 }
323 path_data += "Z ";
324 }
325 return std::format("<path d=\"{}\" fill=\"black\" />", path_data);
326}
327
328
329} // namespace svg
330
331#endif
std::string make_ripple_ribbon(double origin_x, double origin_y, double length, int ribbon_strands, double ribbon_width, ripple_config config)
string make_rolling_ribbon(double origin_x, double origin_y, double scale, int ribbon_strands, double ribbon_width, ribbon_config config)
std::tuple< double, double > point_2d
Define types using std::tuple as requested.
point_3d rotate_point(point_3d p, double angle_x, double angle_y)
std::tuple< double, double, double > point_3d
point_3d normalize(point_3d v)
Helper: Normalize a vector tuple.
point_3d cross_product(point_3d a, point_3d b)