FastLED 3.9.15
Loading...
Searching...
No Matches
corkscrew.cpp
Go to the documentation of this file.
1#include "fl/corkscrew.h"
2#include "fl/algorithm.h"
3#include "fl/assert.h"
4#include "fl/math.h"
5#include "fl/splat.h"
6#include "fl/warn.h"
7#include "fl/tile2x2.h"
8#include "fl/math_macros.h"
9#include "fl/unused.h"
10#include "fl/map_range.h"
11#include "fl/leds.h"
12#include "fl/grid.h"
13#include "fl/screenmap.h"
14#include "fl/memory.h"
15#include "fl/int.h"
16
17
18
19
20namespace fl {
21
22namespace {
23
24// New helper function to calculate individual LED position
25vec2f calculateLedPositionExtended(fl::u16 ledIndex, fl::u16 numLeds, float totalTurns, const Gap& gapParams, fl::u16 width, fl::u16 height) {
26 FL_UNUSED(height);
27 FL_UNUSED(totalTurns);
28
29 // Check if gap feature is active AND will actually be triggered
30 bool gapActive = (gapParams.num_leds > 0 && gapParams.gap > 0.0f && numLeds > static_cast<fl::u16>(gapParams.num_leds));
31
32 if (!gapActive) {
33 // Original behavior when no gap or gap never triggers
34 const float ledProgress = static_cast<float>(ledIndex) / static_cast<float>(numLeds - 1);
35 const fl::u16 row = ledIndex / width;
36 const fl::u16 remainder = ledIndex % width;
37 const float alpha = static_cast<float>(remainder) / static_cast<float>(width);
38 const float width_pos = ledProgress * numLeds;
39 const float height_pos = static_cast<float>(row) + alpha;
40 return vec2f(width_pos, height_pos);
41 }
42
43 // Simplified gap calculation based on user expectation
44 // User wants: LED0=0, LED1=3, LED2=6(wraps to 0) with width=3
45 // This suggests they want regular spacing of width units per LED
46
47 // Simple spacing: each LED is separated by exactly width units
48 float width_pos = static_cast<float>(ledIndex) * static_cast<float>(width);
49
50 // For height, divide by width to get turn progress
51 float height_pos = width_pos / static_cast<float>(width);
52
53 return vec2f(width_pos, height_pos);
54}
55
56void calculateDimensions(float totalTurns, fl::u16 numLeds, const Gap& gapParams, fl::u16 *width, fl::u16 *height) {
57 FL_UNUSED(gapParams);
58
59 // Calculate optimal width and height
60 float ledsPerTurn = static_cast<float>(numLeds) / totalTurns;
61 fl::u16 calc_width = static_cast<fl::u16>(fl::ceil(ledsPerTurn));
62
63 fl::u16 height_from_turns = static_cast<fl::u16>(fl::ceil(totalTurns));
64 fl::u16 calc_height;
65
66 // If the grid would have more pixels than LEDs, adjust height to better match
67 if (calc_width * height_from_turns > numLeds) {
68 // Calculate height that better matches LED count
69 calc_height = static_cast<fl::u16>(fl::ceil(static_cast<float>(numLeds) / static_cast<float>(calc_width)));
70 } else {
71 calc_height = height_from_turns;
72 }
73
74 *width = calc_width;
75 *height = calc_height;
76}
77
78} // namespace
79
80// New primary constructor
81Corkscrew::Corkscrew(float totalTurns, fl::u16 numLeds, bool invert, const Gap& gapParams)
82 : mTotalTurns(totalTurns), mNumLeds(numLeds), mGapParams(gapParams), mInvert(invert) {
83 fl::calculateDimensions(mTotalTurns, mNumLeds, mGapParams, &mWidth, &mHeight);
84 mOwnsPixels = false;
85}
86
87// Constructor with external pixel buffer
88Corkscrew::Corkscrew(float totalTurns, fl::span<CRGB> dstPixels, bool invert, const Gap& gapParams)
89 : mTotalTurns(totalTurns), mNumLeds(static_cast<fl::u16>(dstPixels.size())),
90 mGapParams(gapParams), mInvert(invert) {
91 fl::calculateDimensions(mTotalTurns, mNumLeds, mGapParams, &mWidth, &mHeight);
92 mPixelStorage = dstPixels;
93 mOwnsPixels = false; // External span
94}
95
96
97
98vec2f Corkscrew::at_no_wrap(fl::u16 i) const {
99 if (i >= mNumLeds) {
100 // Handle out-of-bounds access, possibly by returning a default value
101 return vec2f(0, 0);
102 }
103
104 // Compute position on-the-fly
105 vec2f position = calculateLedPositionExtended(i, mNumLeds, mTotalTurns,
107
108 // // Apply inversion if requested
109 // if (mInvert) {
110 // fl::u16 invertedIndex = mNumLeds - 1 - i;
111 // position = calculateLedPositionExtended(invertedIndex, mNumLeds, mTotalTurns,
112 // mGapParams, mState.width, mState.height);
113 // }
114
115 // now wrap the x-position
116 //position.x = fmodf(position.x, static_cast<float>(mState.width));
117
118 return position;
119}
120
121vec2f Corkscrew::at_exact(fl::u16 i) const {
122 // Get the unwrapped position
123 vec2f position = at_no_wrap(i);
124
125 // Apply cylindrical wrapping to the x-position (like at_wrap does)
126 position.x = fmodf(position.x, static_cast<float>(mWidth));
127
128 return position;
129}
130
131
133 if (i >= mNumLeds) {
134 // Handle out-of-bounds access, possibly by returning a default
135 // Tile2x2_u8
136 FASTLED_ASSERT(false, "Out of bounds access in Corkscrew at_splat: "
137 << i << " size: " << mNumLeds);
138 return Tile2x2_u8();
139 }
140
141 // Use the splat function to convert the vec2f to a Tile2x2_u8
142 float i_floor = floorf(i);
143 float i_ceil = ceilf(i);
144 if (ALMOST_EQUAL_FLOAT(i_floor, i_ceil)) {
145 // If the index is the same, just return the splat of that index
146 vec2f position = at_no_wrap(static_cast<fl::u16>(i_floor));
147 return splat(position);
148 } else {
149 // Interpolate between the two points and return the splat of the result
150 vec2f pos1 = at_no_wrap(static_cast<fl::u16>(i_floor));
151 vec2f pos2 = at_no_wrap(static_cast<fl::u16>(i_ceil));
152 float t = i - i_floor;
153 vec2f interpolated_pos = map_range(t, 0.0f, 1.0f, pos1, pos2);
154 return splat(interpolated_pos);
155 }
156}
157
158fl::size Corkscrew::size() const { return mNumLeds; }
159
160
162 if (mCachingEnabled) {
163 // Use cache if enabled
165
166 // Convert float index to integer for cache lookup
167 fl::size cache_index = static_cast<fl::size>(i);
168 if (cache_index < mTileCache.size()) {
169 return mTileCache[cache_index];
170 }
171 }
172
173 // Fall back to dynamic calculation if cache disabled or index out of bounds
174 return calculateTileAtWrap(i);
175}
176
178 // This is a splatted pixel, but wrapped around the cylinder.
179 // This is useful for rendering the corkscrew in a cylindrical way.
182 vec2<u16> origin = tile.origin();
183 for (fl::u8 x = 0; x < 2; x++) {
184 for (fl::u8 y = 0; y < 2; y++) {
185 // For each pixel in the tile, map it to the cylinder so that each subcomponent
186 // is mapped to the correct position on the cylinder.
187 vec2<u16> pos = origin + vec2<u16>(x, y);
188 // now wrap the x-position
189 pos.x = fmodf(pos.x, static_cast<float>(mWidth));
190 data[x][y] = {pos, tile.at(x, y)};
191 }
192 }
193 return Tile2x2_u8_wrap(data);
194}
195
197 if (!enabled && mCachingEnabled) {
198 // Caching was enabled, now disabling - clear the cache
199 mTileCache.clear();
200 mCacheInitialized = false;
201 }
202 mCachingEnabled = enabled;
203}
204
207 // Initialize cache with tiles for each LED position
208 mTileCache.resize(mNumLeds);
209
210 // Populate cache lazily
211 for (fl::size i = 0; i < mNumLeds; ++i) {
212 mTileCache[i] = calculateTileAtWrap(static_cast<float>(i));
213 }
214
215 mCacheInitialized = true;
216 }
217}
218
220 // Use variant storage if available, otherwise fall back to input surface
221 if (!mPixelStorage.empty()) {
222 if (mPixelStorage.template is<fl::span<CRGB>>()) {
223 return mPixelStorage.template get<fl::span<CRGB>>().data();
224 } else if (mPixelStorage.template is<fl::vector<CRGB, fl::allocator_psram<CRGB>>>()) {
226 }
227 }
228
229 // Fall back to input surface data
231 return surface->data();
232}
233
235 // Use variant storage if available, otherwise fall back to input surface
236 if (!mPixelStorage.empty()) {
237 if (mPixelStorage.template is<fl::span<CRGB>>()) {
238 return mPixelStorage.template get<fl::span<CRGB>>();
239 } else if (mPixelStorage.template is<fl::vector<CRGB, fl::allocator_psram<CRGB>>>()) {
241 return fl::span<CRGB>(vec.data(), vec.size());
242 }
243 }
244
245 // Fall back to input surface data as span
247 return fl::span<CRGB>(surface->data(), surface->size());
248}
249
250
251void Corkscrew::readFrom(const fl::Grid<CRGB>& source_grid, bool use_multi_sampling) {
252
253 if (use_multi_sampling) {
254 readFromMulti(source_grid);
255 return;
256 }
257
258 // Get or create the input surface
259 auto target_surface = getOrCreateInputSurface();
260
261 // Clear surface first
262 target_surface->clear();
263
264 // Iterate through each LED in the corkscrew
265 for (fl::size led_idx = 0; led_idx < mNumLeds; ++led_idx) {
266 // Get the rectangular coordinates for this corkscrew LED
267 vec2f rect_pos = at_no_wrap(static_cast<fl::u16>(led_idx));
268
269 // Convert to integer coordinates for indexing
270 vec2i16 coord(static_cast<fl::i16>(rect_pos.x + 0.5f),
271 static_cast<fl::i16>(rect_pos.y + 0.5f));
272
273 // Clamp coordinates to grid bounds
274 coord.x = MAX(0, MIN(coord.x, static_cast<fl::i16>(source_grid.width()) - 1));
275 coord.y = MAX(0, MIN(coord.y, static_cast<fl::i16>(source_grid.height()) - 1));
276
277 // Sample from the source fl::Grid using its at() method
278 CRGB sampled_color = source_grid.at(coord.x, coord.y);
279
280 // Store the sampled color directly in the target surface
281 if (led_idx < target_surface->size()) {
282 target_surface->data()[led_idx] = sampled_color;
283 }
284 }
285}
286
288 // Clear input surface if it exists
289 if (mInputSurface) {
290 mInputSurface->clear();
291 mInputSurface.reset(); // Free the shared_ptr memory
292 }
293
294 // Clear pixel storage if we own it (vector variant)
295 if (!mPixelStorage.empty()) {
298 vec.clear();
299 // Note: fl::vector doesn't have shrink_to_fit(), but clear() frees the memory
300 }
301 // Note: Don't clear external spans as we don't own that memory
302 }
303
304 // Clear tile cache
305 mTileCache.clear();
306 // Note: fl::vector doesn't have shrink_to_fit(), but clear() frees the memory
307 mCacheInitialized = false;
308}
309
311 auto target_surface = getOrCreateInputSurface();
312 for (fl::size i = 0; i < target_surface->size(); ++i) {
313 target_surface->data()[i] = color;
314 }
315}
316
317void Corkscrew::draw(bool use_multi_sampling) {
318 // The draw method should map from the rectangular surface to the LED pixel data
319 // This is the reverse of readFrom - we read from our surface and populate LED data
320 auto source_surface = getOrCreateInputSurface();
321
322 // Make sure we have pixel storage
323 if (mPixelStorage.empty()) {
324 // If no pixel storage is configured, there's nothing to draw to
325 return;
326 }
327
328 CRGB* led_data = rawData();
329 if (!led_data) return;
330
331 if (use_multi_sampling) {
332 // Use multi-sampling to get better accuracy
333 for (fl::size led_idx = 0; led_idx < mNumLeds; ++led_idx) {
334 // Get the wrapped tile for this LED position
335 Tile2x2_u8_wrap tile = at_wrap(static_cast<float>(led_idx));
336
337 // Accumulate color from the 4 sample points with their weights
338 fl::u32 r_accum = 0, g_accum = 0, b_accum = 0;
339 fl::u32 total_weight = 0;
340
341 // Sample from each of the 4 corners of the tile
342 for (fl::u8 x = 0; x < 2; x++) {
343 for (fl::u8 y = 0; y < 2; y++) {
344 const auto& entry = tile.at(x, y);
345 vec2<u16> pos = entry.first;
346 fl::u8 weight = entry.second;
347
348 // Bounds check for the source surface
349 if (pos.x < source_surface->width() && pos.y < source_surface->height()) {
350 // Sample from the source surface
351 CRGB sample_color = source_surface->at(pos.x, pos.y);
352
353 // Accumulate weighted color components
354 r_accum += static_cast<fl::u32>(sample_color.r) * weight;
355 g_accum += static_cast<fl::u32>(sample_color.g) * weight;
356 b_accum += static_cast<fl::u32>(sample_color.b) * weight;
357 total_weight += weight;
358 }
359 }
360 }
361
362 // Calculate final color by dividing by total weight
363 CRGB final_color = CRGB::Black;
364 if (total_weight > 0) {
365 final_color.r = static_cast<fl::u8>(r_accum / total_weight);
366 final_color.g = static_cast<fl::u8>(g_accum / total_weight);
367 final_color.b = static_cast<fl::u8>(b_accum / total_weight);
368 }
369
370 // Store the result in the LED data
371 led_data[led_idx] = final_color;
372 }
373 } else {
374 // Simple non-multi-sampling version
375 for (fl::size led_idx = 0; led_idx < mNumLeds; ++led_idx) {
376 // Get the rectangular coordinates for this corkscrew LED
377 vec2f rect_pos = at_no_wrap(static_cast<fl::u16>(led_idx));
378
379 // Convert to integer coordinates for indexing
380 vec2i16 coord(static_cast<fl::i16>(rect_pos.x + 0.5f),
381 static_cast<fl::i16>(rect_pos.y + 0.5f));
382
383 // Clamp coordinates to surface bounds
384 coord.x = MAX(0, MIN(coord.x, static_cast<fl::i16>(source_surface->width()) - 1));
385 coord.y = MAX(0, MIN(coord.y, static_cast<fl::i16>(source_surface->height()) - 1));
386
387 // Sample from the source surface
388 CRGB sampled_color = source_surface->at(coord.x, coord.y);
389
390 // Store the sampled color in the LED data
391 led_data[led_idx] = sampled_color;
392 }
393 }
394}
395
396void Corkscrew::readFromMulti(const fl::Grid<CRGB>& source_grid) const {
397 // Get the target surface and clear it
398 auto target_surface = const_cast<Corkscrew*>(this)->getOrCreateInputSurface();
399 target_surface->clear();
400 const u16 width = static_cast<u16>(source_grid.width());
401 const u16 height = static_cast<u16>(source_grid.height());
402
403 // Iterate through each LED in the corkscrew
404 for (fl::size led_idx = 0; led_idx < mNumLeds; ++led_idx) {
405 // Get the wrapped tile for this LED position
406 Tile2x2_u8_wrap tile = at_wrap(static_cast<float>(led_idx));
407
408 // Accumulate color from the 4 sample points with their weights
409 fl::u32 r_accum = 0, g_accum = 0, b_accum = 0;
410 fl::u32 total_weight = 0;
411
412 // Sample from each of the 4 corners of the tile
413 for (fl::u8 x = 0; x < 2; x++) {
414 for (fl::u8 y = 0; y < 2; y++) {
415 const auto& entry = tile.at(x, y);
416 vec2<u16> pos = entry.first; // position is the first element of the pair
417 fl::u8 weight = entry.second; // weight is the second element of the pair
418
419 // Bounds check for the source grid
420 if (pos.x >= 0 && pos.x < width &&
421 pos.y >= 0 && pos.y < height) {
422
423 // Sample from the source grid
424 CRGB sample_color = source_grid.at(pos.x, pos.y);
425
426 // Accumulate weighted color components
427 r_accum += static_cast<fl::u32>(sample_color.r) * weight;
428 g_accum += static_cast<fl::u32>(sample_color.g) * weight;
429 b_accum += static_cast<fl::u32>(sample_color.b) * weight;
430 total_weight += weight;
431 }
432 }
433 }
434
435 // Calculate final color by dividing by total weight
436 CRGB final_color = CRGB::Black;
437 if (total_weight > 0) {
438 final_color.r = static_cast<fl::u8>(r_accum / total_weight);
439 final_color.g = static_cast<fl::u8>(g_accum / total_weight);
440 final_color.b = static_cast<fl::u8>(b_accum / total_weight);
441 }
442
443 // Store the result in the target surface at the LED index position
444 auto target_surface = const_cast<Corkscrew*>(this)->getOrCreateInputSurface();
445 if (led_idx < target_surface->size()) {
446 target_surface->data()[led_idx] = final_color;
447 }
448 }
449}
450
451// Iterator implementation
453 return corkscrew_->at_no_wrap(static_cast<fl::u16>(position_));
454}
455
457 // Create a ScreenMap with the correct number of LEDs
459
460 // For each LED index, calculate its position and set it in the ScreenMap
461 for (fl::u16 i = 0; i < mNumLeds; ++i) {
462 // Get the wrapped 2D position for this LED index in the cylindrical mapping
463 vec2f position = at_exact(i);
464
465 // Set the wrapped position in the ScreenMap
466 screenMap.set(i, position);
467 }
468
469 return screenMap;
470}
471
472// Enhanced surface handling methods
474 if (!mInputSurface) {
475 // Create a new Grid with cylinder dimensions using PSRAM allocation
477 }
478 return mInputSurface;
479}
480
484
485
486fl::size Corkscrew::pixelCount() const {
487 // Use variant storage if available, otherwise fall back to legacy buffer size
488 if (!mPixelStorage.empty()) {
489 if (mPixelStorage.template is<fl::span<CRGB>>()) {
490 return mPixelStorage.template get<fl::span<CRGB>>().size();
491 } else if (mPixelStorage.template is<fl::vector<CRGB, fl::allocator_psram<CRGB>>>()) {
493 }
494 }
495
496 // Fall back to input size
497 return mNumLeds;
498}
499
500
501
502} // namespace fl
int y
Definition simple.h:93
int x
Definition simple.h:92
uint8_t pos
Definition Blur.ino:11
vec2f operator*() const
const Corkscrew * corkscrew_
Definition corkscrew.h:140
fl::shared_ptr< fl::Grid< CRGB > > & getOrCreateInputSurface()
Tile2x2_u8 at_splat_extrapolate(float i) const
Tile2x2_u8_wrap calculateTileAtWrap(float i) const
void setCachingEnabled(bool enabled)
fl::ScreenMap toScreenMap(float diameter=0.5f) const
Corkscrew(float totalTurns, fl::u16 numLeds, bool invert=false, const Gap &gapParams=Gap())
Definition corkscrew.cpp:81
fl::Grid< CRGB > & surface()
fl::u16 mWidth
Definition corkscrew.h:235
fl::span< CRGB > data()
fl::u16 mNumLeds
Definition corkscrew.h:230
void readFromMulti(const fl::Grid< CRGB > &target_grid) const
bool mCacheInitialized
Definition corkscrew.h:247
float mTotalTurns
Definition corkscrew.h:229
void fillInputSurface(const CRGB &color)
fl::u16 mHeight
Definition corkscrew.h:236
PixelStorage mPixelStorage
Definition corkscrew.h:239
CRGB * rawData()
vec2f at_exact(fl::u16 i) const
fl::shared_ptr< fl::Grid< CRGB > > mInputSurface
Definition corkscrew.h:243
fl::vector< Tile2x2_u8_wrap > mTileCache
Definition corkscrew.h:246
void initializeCache() const
Tile2x2_u8_wrap at_wrap(float i) const
fl::size size() const
void draw(bool use_multi_sampling=true)
vec2f at_no_wrap(fl::u16 i) const
Definition corkscrew.cpp:98
void readFrom(const fl::Grid< CRGB > &source_grid, bool use_multi_sampling=true)
fl::size pixelCount() const
bool mCachingEnabled
Definition corkscrew.h:248
void clear()
Definition grid.h:30
u32 height() const
Definition grid.h:60
T & at(u32 x, u32 y)
Definition grid.h:53
u32 width() const
Definition grid.h:59
Entry & at(u16 x, u16 y)
Definition tile2x2.cpp:65
fl::pair< vec2< u16 >, u8 > Entry
Definition tile2x2.h:108
u8 & at(int x, int y)
Definition tile2x2.h:39
vec2< u16 > origin() const
Definition tile2x2.h:56
fl::ScreenMap screenMap
Definition Corkscrew.h:103
static uint32_t t
Definition Luminova.h:54
#define MIN(a, b)
Definition math_macros.h:41
#define ALMOST_EQUAL_FLOAT(a, b)
Definition math_macros.h:63
#define MAX(a, b)
Definition math_macros.h:37
void calculateDimensions(float totalTurns, fl::u16 numLeds, const Gap &gapParams, fl::u16 *width, fl::u16 *height)
Definition corkscrew.cpp:56
vec2f calculateLedPositionExtended(fl::u16 ledIndex, fl::u16 numLeds, float totalTurns, const Gap &gapParams, fl::u16 width, fl::u16 height)
Definition corkscrew.cpp:25
unsigned char u8
Definition int.h:17
Slice< T > span
Definition span.h:8
Tile2x2_u8 splat(vec2f xy)
"Splat" as in "splat pixel rendering" takes a pixel value in float x,y coordinates and "splats" it in...
Definition splat.cpp:14
T ceil(T value)
Definition math.h:39
vec2< float > vec2f
Definition geometry.h:333
shared_ptr< T > make_shared(Args &&... args)
Definition shared_ptr.h:348
vec2< i16 > vec2i16
Definition geometry.h:335
pair_element< I, T1, T2 >::type & get(pair< T1, T2 > &p) noexcept
Definition pair.h:113
HeapVector< T, Allocator > vector
Definition vector.h:1214
IMPORTANT!
Definition crgb.h:20
Corkscrew LED strip projection and rendering.
@ Black
<div style='background:#000000;width:4em;height:4em;'></div>
Definition crgb.h:567
Representation of an RGB pixel (Red, Green, Blue)
Definition crgb.h:86
int num_leds
Definition corkscrew.h:67
float gap
Definition corkscrew.h:68
Struct representing gap parameters for corkscrew mapping.
Definition corkscrew.h:66
value_type y
Definition geometry.h:191
value_type x
Definition geometry.h:190
#define FL_UNUSED(x)
Definition unused.h:8