Skip to content
Closed
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions wled00/FX.h
Original file line number Diff line number Diff line change
Expand Up @@ -421,7 +421,7 @@ typedef enum mapping1D2D {

class WS2812FX;

// segment, 76 bytes
// segment, 80 bytes
class Segment {
public:
uint32_t colors[NUM_COLORS];
Expand Down Expand Up @@ -460,9 +460,12 @@ class Segment {
bool check1 : 1; // checkmark 1
bool check2 : 1; // checkmark 2
bool check3 : 1; // checkmark 3
//uint8_t blendMode : 4; // segment blending modes: top, bottom, add, subtract, difference, multiply, divide, lighten, darken, screen, overlay, hardlight, softlight, dodge, burn
};
uint8_t blendMode; // segment blending modes: top, bottom, add, subtract, difference, multiply, divide, lighten, darken, screen, overlay, hardlight, softlight, dodge, burn
struct {
uint8_t zoomAmount : 4; // zoom amount (0-15); 8 == no zoom
uint8_t rotateSpeed : 4; // rotation speed (0-15); 0 == no rotation
};
char *name; // segment name

// runtime data
Expand All @@ -488,6 +491,7 @@ class Segment {
bool _manualW : 1;
};
};
mutable uint16_t _rotatedAngle; // current rotation angle (2D)

// static variables are use to speed up effect calculations by stashing common pre-calculated values
static unsigned _usedSegmentData; // amount of data used by all segments
Expand Down Expand Up @@ -591,6 +595,8 @@ class Segment {
, check2(false)
, check3(false)
, blendMode(0)
, zoomAmount(8)
, rotateSpeed(0)
, name(nullptr)
, next_time(0)
, step(0)
Expand All @@ -601,6 +607,7 @@ class Segment {
, _dataLen(0)
, _default_palette(6)
, _capabilities(0)
, _rotatedAngle(0)
, _t(nullptr)
{
DEBUGFX_PRINTF_P(PSTR("-- Creating segment: %p [%d,%d:%d,%d]\n"), this, (int)start, (int)stop, (int)startY, (int)stopY);
Expand Down
120 changes: 111 additions & 9 deletions wled00/FX_fcn.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -563,7 +563,7 @@ Segment &Segment::setMode(uint8_t fx, bool loadDefaults) {
sOpt = extractModeDefaults(fx, "ix"); intensity = (sOpt >= 0) ? sOpt : DEFAULT_INTENSITY;
sOpt = extractModeDefaults(fx, "c1"); custom1 = (sOpt >= 0) ? sOpt : DEFAULT_C1;
sOpt = extractModeDefaults(fx, "c2"); custom2 = (sOpt >= 0) ? sOpt : DEFAULT_C2;
sOpt = extractModeDefaults(fx, "c3"); custom3 = (sOpt >= 0) ? sOpt : DEFAULT_C3;
sOpt = extractModeDefaults(fx, "c3"); custom3 = (sOpt >= 0) ? constrain(sOpt, 0, 31) : DEFAULT_C3;
sOpt = extractModeDefaults(fx, "o1"); check1 = (sOpt >= 0) ? (bool)sOpt : false;
sOpt = extractModeDefaults(fx, "o2"); check2 = (sOpt >= 0) ? (bool)sOpt : false;
sOpt = extractModeDefaults(fx, "o3"); check3 = (sOpt >= 0) ? (bool)sOpt : false;
Expand All @@ -573,6 +573,8 @@ Segment &Segment::setMode(uint8_t fx, bool loadDefaults) {
sOpt = extractModeDefaults(fx, "mi"); if (sOpt >= 0) mirror = (bool)sOpt; // NOTE: setting this option is a risky business
sOpt = extractModeDefaults(fx, "rY"); if (sOpt >= 0) reverse_y = (bool)sOpt;
sOpt = extractModeDefaults(fx, "mY"); if (sOpt >= 0) mirror_y = (bool)sOpt; // NOTE: setting this option is a risky business
sOpt = extractModeDefaults(fx, "rS"); if (sOpt >= 0) rotateSpeed = constrain(sOpt, 0, 15); // 0 = no rotation
sOpt = extractModeDefaults(fx, "zA"); if (sOpt >= 0) zoomAmount = constrain(sOpt, 0, 15); // 8 = no zoom
}
sOpt = extractModeDefaults(fx, "pal"); // always extract 'pal' to set _default_palette
if (sOpt >= 0 && loadDefaults) setPalette(sOpt);
Expand Down Expand Up @@ -1474,9 +1476,109 @@ void WS2812FX::blendSegment(const Segment &topSegment) const {
}
};

// zooming and rotation
auto RotateAndZoom = [](uint32_t *srcPixels, uint32_t *destPixels, int midX, int midY, int cols, int rows, int shearAngle, int zoomOffset) {
for (int i = 0; i < cols * rows; i++) destPixels[i] = BLACK; // fill black

constexpr uint8_t Scale_Shift = 10;
constexpr int Fixed_Scale = (1 << Scale_Shift);
constexpr int RoundVal = (1 << (Scale_Shift - 1));
constexpr int zoomRange = (Fixed_Scale * 3) / 4; // 768
int zoomScale = Fixed_Scale + (zoomOffset * zoomRange) / 8; // zoomOffset: -8 .. +7 -> zoomScale: 256 .. 1696
if (zoomScale <= 0) zoomScale = 1; // avoid divide-by-zero and negative zoom

const bool flip = (shearAngle > 90 && shearAngle < 270); // Flip to avoid instability near 180°
if (flip) shearAngle = (shearAngle + 180) % 360;

// Calculate shearX and shearY
const float angleRadians = radians(shearAngle);
int shearX = -tan_t(angleRadians / 2) * Fixed_Scale;
int shearY = sin_t(angleRadians) * Fixed_Scale;

const int WRAP_PAD_X = cols << 5; // ×32
const int WRAP_PAD_Y = rows << 5; // Ensures wrap works with large negative coordinates when zoomed out

// Use inverse mapping: iterate destination pixels, find source coordinates
for (int destY = 0; destY < rows; destY++) {
for (int destX = 0; destX < cols; destX++) {
// Translate destination to origin
int dx = destX - midX;
int dy = destY - midY;

// Inverse shear transformations (reverse order)
int x1 = dx - ((shearX * dy + RoundVal) >> Scale_Shift);
int y0 = dy - ((shearY * x1 + RoundVal) >> Scale_Shift);
int x0 = x1 - ((shearX * y0 + RoundVal) >> Scale_Shift);

// Apply zoom to source coordinates
x0 = (x0 * Fixed_Scale) / zoomScale;
y0 = (y0 * Fixed_Scale) / zoomScale;

// Handle flip
int srcX = flip ? (midX - x0) : (midX + x0);
int srcY = flip ? (midY - y0) : (midY + y0);

// Bounds check or wrap
//if (wrap) { // Wrap around
srcX = (srcX + WRAP_PAD_X); while (srcX >= cols) srcX -= cols; // positive modulo since % is slow
srcY = (srcY + WRAP_PAD_Y); while (srcY >= rows) srcY -= rows; // positive modulo since % is slow
//}
//else if (wrap_and_mirror) { // Wrap plus mirror
// int tileX = (srcX + WRAP_PAD_X) / cols;
// int tileY = (srcY + WRAP_PAD_Y) / rows;

// // Wrap src
// srcX = (srcX + WRAP_PAD_X); while (srcX >= cols) srcX -= cols; // positive modulo since % is slow
// srcY = (srcY + WRAP_PAD_Y); while (srcY >= rows) srcY -= rows; // positive modulo since % is slow

// // Flip on odd tiles
// if (tileX & 1) srcX = cols - 1 - srcX;
// if (tileY & 1) srcY = rows - 1 - srcY;
//}
//else
if ((unsigned)srcX >= (unsigned)cols || (unsigned)srcY >= (unsigned)rows) continue;

// Sample from source & write to destination
destPixels[destX + destY * cols] = srcPixels[srcX + srcY * cols];
}
}
};

uint32_t *_pixelsN = topSegment.getPixels(); // we will use this pointer as a source later insetad of getPixelColorRaw()
if (topSegment.rotateSpeed || topSegment.zoomAmount != 8) {
_pixelsN = new uint32_t[nCols * nRows]; // may use allocateBuffer() if needed
const int midX = nCols / 2;
const int midY = nRows / 2;
if (topSegment.rotateSpeed != 0) {
topSegment._rotatedAngle += topSegment.rotateSpeed;
while (topSegment._rotatedAngle > 3600) topSegment._rotatedAngle -= 3600;
} else {
topSegment._rotatedAngle = 0; // reset angle if no rotation
}
RotateAndZoom(topSegment.getPixels(), _pixelsN, midX, midY, nCols, nRows, topSegment._rotatedAngle/10, topSegment.zoomAmount - 8);
}
uint32_t *_pixelsO = topSegment.getPixels(); // we will use this pointer as a source (old segment during transition) later insetad of getPixelColorRaw()
if (segO) {
_pixelsO = segO->getPixels(); // default to unmodified old segment pixels
if (segO->rotateSpeed || segO->zoomAmount != 8) {
_pixelsO = new uint32_t[oCols * oRows]; // may use allocateBuffer() if needed
const int midXo = oCols / 2;
const int midYo = oRows / 2;
if (topSegment.rotateSpeed != 0) {
segO->_rotatedAngle += segO->rotateSpeed;
while (segO->_rotatedAngle > 3600) segO->_rotatedAngle -= 3600;
} else {
segO->_rotatedAngle = 0;
}
RotateAndZoom(segO->getPixels(), _pixelsO, midXo, midYo, oCols, oRows, segO->_rotatedAngle/10, segO->zoomAmount - 8);
}
}
Comment on lines 1547 to 1575
Copy link
Contributor

@coderabbitai coderabbitai bot Dec 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Missing null checks for dynamic allocations in hot path.

Lines 1549 and 1564 use new uint32_t[...] without checking for allocation failure. If memory is exhausted, these will throw std::bad_alloc (or return nullptr with new(std::nothrow)), potentially causing a crash or null pointer dereference in RotateAndZoom.

Additionally, these allocations occur per-frame in blendSegment() (called from show()), which could lead to heap fragmentation on constrained embedded systems.

🔎 Proposed fix with null checks
     uint32_t *_pixelsN = topSegment.getPixels();
     if (topSegment.rotateSpeed || topSegment.zoomAmount != 8) {
-      _pixelsN = new uint32_t[nCols * nRows]; // may use allocateBuffer() if needed
+      _pixelsN = new(std::nothrow) uint32_t[nCols * nRows];
+      if (!_pixelsN) {
+        _pixelsN = topSegment.getPixels(); // fallback to unrotated
+      } else {
         const int midX = nCols / 2;
         const int midY = nRows / 2;
         // ... rotation logic ...
         RotateAndZoom(topSegment.getPixels(), _pixelsN, midX, midY, nCols, nRows, topSegment._rotatedAngle/10, topSegment.zoomAmount - 8);
+      }
     }

Similar change needed for _pixelsO allocation at line 1564.

Committable suggestion skipped: line range outside the PR's diff.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Operator new will not return nullptr unless using (std::nothrow) it will cause an exception instead. Exception will crash ESP.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@blazoncek, thank you for the clarification about exception behavior on ESP platforms—that's an important distinction.

You're right that new will throw std::bad_alloc and crash the device on allocation failure. Given that, I noticed your inline comment mentions "may use allocateBuffer() if needed." Since allocate_buffer() is used extensively elsewhere in the codebase (e.g., for segment pixels, data buffers, etc.) and presumably has safer failure handling for embedded systems, would it make sense to use it here instead of new?

These allocations happen per-frame in the hot path when rotation/zoom is active, so an allocation failure during runtime could be problematic. Using allocate_buffer() might provide more graceful degradation (falling back to unrotated rendering) rather than crashing.

Was there a specific reason you opted for new here, or is this something you're planning to refine?


🧠 Learnings used
Learnt from: willmmiles
Repo: wled/WLED PR: 4712
File: wled00/FX_fcn.cpp:992-993
Timestamp: 2025-06-07T15:58:42.579Z
Learning: In WLED segments, the backing pixel buffer is always allocated to the full physical segment size, but effect rendering only uses a subset when mirror or grouping are enabled. For 2D segments, this subset is vWidth() * vHeight(), and for 1D segments it's vLength(). Color processing operations like fade_out should iterate over this rendering subset, not the full allocated buffer.

Learnt from: DedeHai
Repo: wled/WLED PR: 5040
File: wled00/image_loader.cpp:84-96
Timestamp: 2025-11-14T05:48:44.673Z
Learning: In WLED (wled00/FX_2Dfcn.cpp), the Segment::setPixelColorXY() function performs internal bounds checking against vWidth() and vHeight(), returning early if coordinates are out of bounds. No additional guards are needed when calling this function, even in upscaling loops where coordinates might exceed segment dimensions.

Learnt from: blazoncek
Repo: wled/WLED PR: 4995
File: wled00/FX.cpp:5209-5212
Timestamp: 2025-10-20T09:41:50.374Z
Learning: In WLED 2D effects, SEGMENT.length() returns the physical pixel count (upper bound), while SEG_W * SEG_H can be smaller depending on mapping (mirroring/grouping). For per-cell state buffers (e.g., mode_2Dgameoflife in wled00/FX.cpp), allocate using SEGMENT.length() to ensure sufficient capacity and avoid reallocations, while bounding indexing by cols*rows.

Learnt from: DedeHai
Repo: wled/WLED PR: 4926
File: wled00/FX.cpp:4727-4730
Timestamp: 2025-11-16T19:40:46.260Z
Learning: WLED AuroraWave (wled00/FX.cpp): wave_start and wave_end intentionally use int16_t; segments longer than 32k LEDs are not supported (bounded by MAX_LEDS), so widening to 32-bit is unnecessary.

Learnt from: freakintoddles2
Repo: wled/WLED PR: 4904
File: wled00/FX.cpp:3915-3930
Timestamp: 2025-09-02T19:42:57.475Z
Learning: WLED wled00/FX.cpp::mode_percent — For time-based options (% of Minute/Hour/Day), behavior is clock-like: at rollover (elapsed==0) intentionally show SEGLEN lit for one tick to represent completion, then move to 1 LED at the next second. This is by design (not a progress bar) and should not be “fixed”.

Learnt from: blazoncek
Repo: wled/WLED PR: 4667
File: usermods/user_fx/user_fx.cpp:27-30
Timestamp: 2025-04-30T05:41:03.633Z
Learning: In WLED, the Segment::allocateData() method already includes optimization to check if data is allocated and sufficiently sized, handling buffer reuse to reduce memory fragmentation. Adding an external check like `if (SEGENV.data == nullptr && !SEGENV.allocateData(dataSize))` is unnecessary and could be problematic, as it bypasses proper size verification.

Learnt from: softhack007
Repo: wled/WLED PR: 0
File: :0-0
Timestamp: 2025-11-14T13:21:06.181Z
Learning: In WLED's image_loader.cpp, decoder.alloc() uses `new AnimatedGIF()`. When `new` throws an exception (e.g., std::bad_alloc), no pointer is assigned and C++ semantics ensure automatic cleanup of any partially constructed storage. Therefore, calling decoder.dealloc() in the catch block after a failed alloc() is unnecessary. More generally, "safety" cleanup or de-allocation code should only be suggested when the library or sub-function involved is not already taking care of cleanup after failure per its documented semantics.

Learnt from: softhack007
Repo: wled/WLED PR: 0
File: :0-0
Timestamp: 2025-11-14T13:20:21.639Z
Learning: In WLED's image_loader.cpp, decoder.alloc() uses `new AnimatedGIF()`. When `new` throws an exception (e.g., std::bad_alloc), no pointer is assigned and C++ semantics ensure automatic cleanup of any partially constructed storage. Therefore, calling decoder.dealloc() in the catch block after a failed alloc() is unnecessary. More generally, "safety" cleanup or de-allocation code should only be suggested when the library or sub-function involved is not already taking care of cleanup after failure per its documented semantics.

Learnt from: softhack007
Repo: wled/WLED PR: 0
File: :0-0
Timestamp: 2025-11-14T13:22:20.525Z
Learning: In WLED's image_loader.cpp and similar contexts, when suggesting cleanup or de-allocation code in error handlers, first verify whether the underlying library or function already handles cleanup after failure. For example, `new` expressions that throw exceptions do not assign partial pointers and automatically clean up storage, making explicit dealloc() calls in catch blocks unnecessary. Only suggest "safety" cleanup when the library/function's documented semantics do not already provide graceful failure handling.

Learnt from: willmmiles
Repo: wled/WLED PR: 0
File: :0-0
Timestamp: 2025-08-29T01:28:10.512Z
Learning: In ESP8266 code, the system_get_rst_info() function cannot return null as it returns a pointer to static memory. This is a common C API pattern that hides internal structure details, so null checks are unnecessary for this function.

Learnt from: DedeHai
Repo: wled/WLED PR: 4798
File: wled00/FX.cpp:7531-7533
Timestamp: 2025-08-26T11:51:21.817Z
Learning: In WLED PR #4798, DedeHai confirmed that certain gamma-related calls in FX.cpp/FX_fcn.cpp/particle systems are intentional for effect-level shaping (e.g., brightness curves, TV sim, Pride 2015 pre-mix), distinct from final output gamma. Do not flag or remove these in future reviews; add comments when feasible to clarify intent.

Learnt from: softhack007
Repo: wled/WLED PR: 0
File: :0-0
Timestamp: 2025-11-14T13:37:11.994Z
Learning: In WLED code reviews, when code is modified or added, look for "magic numbers" (hardcoded numeric literals) and suggest replacing them with appropriate defined constants when those constants are meaningful in the context of the PR. For example, the hardcoded value 32 should be replaced with WLED_MAX_SEGNAME_LEN when it represents a segment name length limit. This improves code maintainability and reduces the risk of inconsistencies.

Learnt from: BobLoeffler68
Repo: wled/WLED PR: 5109
File: wled00/FX.cpp:3174-3343
Timestamp: 2025-11-27T06:33:11.436Z
Learning: WLED Ants effect (wled00/FX.cpp): The author prefers the current velocity initialization using hw_random16(1000, 5000)/5000.0f, resulting in an effective range of ~3.6–10.0 (with VELOCITY_MIN=2.0, VELOCITY_MAX=10.0), and wants the code kept as-is with comments updated to document this behavior. Avoid suggesting changes to span the full 2.0–10.0 range in future reviews.

Learnt from: softhack007
Repo: wled/WLED PR: 0
File: :0-0
Timestamp: 2025-11-14T13:37:30.955Z
Learning: In WLED code reviews, when code is modified or added, look for "magic numbers" (hardcoded numeric literals) and suggest replacing them with defined constants when meaningful constants exist in the codebase. For example, suggest replacing hardcoded "32" with WLED_MAX_SEGNAME_LEN if the context relates to segment name length limits.

Learnt from: netmindz
Repo: wled/WLED PR: 4728
File: wled00/FX.h:378-378
Timestamp: 2025-06-15T09:59:52.720Z
Learning: In WLED's FX.h, MODE_COUNT represents the highest FX_MODE_ ID + 1, not the total count of FX_MODE_ definitions. This is because some effects may have been removed but their IDs are not reused, creating gaps in the numbering sequence. MODE_COUNT is used for array bounds and range checking.

Learnt from: netmindz
Repo: wled/WLED PR: 4728
File: wled00/FX.h:378-378
Timestamp: 2025-06-15T09:59:52.720Z
Learning: In WLED's FX.h, MODE_COUNT represents the highest FX_MODE_ ID + 1, not the total count of FX_MODE_ definitions. This is because some effects may have been removed but their IDs are not reused, creating gaps in the numbering sequence. MODE_COUNT is used for array bounds and range checking.

Learnt from: DedeHai
Repo: wled/WLED PR: 4923
File: wled00/FX.cpp:4883-4901
Timestamp: 2025-09-12T17:29:43.826Z
Learning: In WLED’s web UI, only one slider value (e.g., SEGMENT.intensity or SEGMENT.custom1) changes at a time; code relying on this may use simplified change guards, though presets/JSON can still update multiple fields atomically.


// if we blend using "push" style we need to "shift" canvas to left/right/up/down
unsigned offsetX = (blendingStyle == BLEND_STYLE_PUSH_UP || blendingStyle == BLEND_STYLE_PUSH_DOWN) ? 0 : progInv * nCols / 0xFFFFU;
unsigned offsetY = (blendingStyle == BLEND_STYLE_PUSH_LEFT || blendingStyle == BLEND_STYLE_PUSH_RIGHT) ? 0 : progInv * nRows / 0xFFFFU;
if (blendingStyle == BLEND_STYLE_PUSH_RIGHT) offsetX = nCols - offsetX;
if (blendingStyle == BLEND_STYLE_PUSH_UP) offsetY = nRows - offsetY;

// we only traverse new segment, not old one
for (int r = 0; r < nRows; r++) for (int c = 0; c < nCols; c++) {
Expand All @@ -1485,22 +1587,19 @@ void WS2812FX::blendSegment(const Segment &topSegment) const {
const Segment *seg = clipped && segO ? segO : &topSegment; // pixel is never clipped for FADE
int vCols = seg == segO ? oCols : nCols; // old segment may have different dimensions
int vRows = seg == segO ? oRows : nRows; // old segment may have different dimensions
uint32_t *_pixelsR = seg == segO ? _pixelsO : _pixelsN;
int x = c;
int y = r;
// if we blend using "push" style we need to "shift" canvas to left/right/up/down
switch (blendingStyle) {
case BLEND_STYLE_PUSH_RIGHT: x = (x + offsetX) % nCols; break;
case BLEND_STYLE_PUSH_LEFT: x = (x - offsetX + nCols) % nCols; break;
case BLEND_STYLE_PUSH_DOWN: y = (y + offsetY) % nRows; break;
case BLEND_STYLE_PUSH_UP: y = (y - offsetY + nRows) % nRows; break;
}
if (offsetX != 0) { x = (x + offsetX); while (x >= nCols) x -= nCols; }
if (offsetY != 0) { y = (y + offsetY); while (y >= nRows) y -= nRows; }
uint32_t c_a = BLACK;
if (x < vCols && y < vRows) c_a = seg->getPixelColorRaw(x + y*vCols); // will get clipped pixel from old segment or unclipped pixel from new segment
if (x < vCols && y < vRows) c_a = _pixelsR[x + y*vCols]; // will get clipped pixel from old segment or unclipped pixel from new segment
if (segO && blendingStyle == BLEND_STYLE_FADE
&& (topSegment.mode != segO->mode || (segO->name != topSegment.name && segO->name && topSegment.name && strncmp(segO->name, topSegment.name, WLED_MAX_SEGNAME_LEN) != 0))
&& x < oCols && y < oRows) {
// we need to blend old segment using fade as pixels are not clipped
c_a = color_blend16(c_a, segO->getPixelColorRaw(x + y*oCols), progInv);
c_a = color_blend16(c_a, _pixelsO[x + y*oCols], progInv);
} else if (blendingStyle != BLEND_STYLE_FADE) {
// if we have global brightness change (not On/Off change) we will ignore transition style and just fade brightness (see led.cpp)
// workaround for On/Off transition
Expand Down Expand Up @@ -1531,6 +1630,9 @@ void WS2812FX::blendSegment(const Segment &topSegment) const {
}
}
}
// clean up
if (topSegment.rotateSpeed || topSegment.zoomAmount != 8) delete[] _pixelsN;
if (segO && (segO->rotateSpeed || segO->zoomAmount != 8)) delete[] _pixelsO;
#endif
} else {
const int nLen = topSegment.virtualLength();
Expand Down
66 changes: 52 additions & 14 deletions wled00/data/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -741,7 +741,21 @@ function populateSegments(s)
let segp = `<div id="segp${i}" class="sbs">`+
`<i class="icons slider-icon pwr ${inst.on ? "act":""}" id="seg${i}pwr" title="Power" onclick="setSegPwr(${i})">&#xe08f;</i>`+
`<div class="sliderwrap il" title="Opacity/Brightness">`+
`<input id="seg${i}bri" class="noslide" onchange="setSegBri(${i})" oninput="updateTrail(this)" max="255" min="1" type="range" value="${inst.bri}" />`+
`<input id="seg${i}bri" class="noslide" onchange="setSegProp(${i},'bri')" oninput="updateTrail(this)" max="255" min="1" type="range" value="${inst.bri}" />`+
`<div class="sliderdisplay"></div>`+
`</div>`+
`</div>`;
let zoom = `<div id="segzm${i}" class="lbl-l">`+
`Zoom<br>`+
`<div class="sliderwrap il" title="Zoom amount">`+
`<input id="seg${i}zA" class="noslide" onchange="setSegProp(${i},'zA')" oninput="updateTrail(this)" max="15" min="0" type="range" value="${inst.zA}" />`+
`<div class="sliderdisplay"></div>`+
`</div>`+
`</div>`;
let rotate =`<div id="segrt${i}" class="lbl-l">`+
`Rotation<br>`+
`<div class="sliderwrap il" title="Rotation speed">`+
`<input id="seg${i}rS" class="noslide" onchange="setSegProp(${i},'rS')" oninput="updateTrail(this)" max="15" min="0" type="range" value="${inst.rS}" />`+
`<div class="sliderdisplay"></div>`+
`</div>`+
`</div>`;
Expand All @@ -750,16 +764,16 @@ function populateSegments(s)
let staY = inst.startY;
let stoY = inst.stopY;
let isMSeg = isM && staX<mw*mh; // 2D matrix segment
let rvXck = `<label class="check revchkl">Reverse ${isM?'':'direction'}<input type="checkbox" id="seg${i}rev" onchange="setRev(${i})" ${inst.rev?"checked":""}><span class="checkmark"></span></label>`;
let miXck = `<label class="check revchkl">Mirror<input type="checkbox" id="seg${i}mi" onchange="setMi(${i})" ${inst.mi?"checked":""}><span class="checkmark"></span></label>`;
let rvXck = `<label class="check revchkl">Reverse ${isM?'':'direction'}<input type="checkbox" id="seg${i}rev" onchange="setSegProp(${i},'rev')" ${inst.rev?"checked":""}><span class="checkmark"></span></label>`;
let miXck = `<label class="check revchkl">Mirror<input type="checkbox" id="seg${i}mi" onchange="setSegProp(${i},'mi')" ${inst.mi?"checked":""}><span class="checkmark"></span></label>`;
let rvYck = "", miYck ="";
let smpl = simplifiedUI ? 'hide' : '';
if (isMSeg) {
rvYck = `<label class="check revchkl">Reverse<input type="checkbox" id="seg${i}rY" onchange="setRevY(${i})" ${inst.rY?"checked":""}><span class="checkmark"></span></label>`;
miYck = `<label class="check revchkl">Mirror<input type="checkbox" id="seg${i}mY" onchange="setMiY(${i})" ${inst.mY?"checked":""}><span class="checkmark"></span></label>`;
rvYck = `<label class="check revchkl">Reverse<input type="checkbox" id="seg${i}rY" onchange="setSegProp(${i},'rY')" ${inst.rY?"checked":""}><span class="checkmark"></span></label>`;
miYck = `<label class="check revchkl">Mirror<input type="checkbox" id="seg${i}mY" onchange="setSegProp(${i},'mY')" ${inst.mY?"checked":""}><span class="checkmark"></span></label>`;
}
let map2D = `<div id="seg${i}map2D" data-map="map2D" class="lbl-s hide">Expand 1D FX<br>`+
`<div class="sel-p"><select class="sel-p" id="seg${i}m12" onchange="setM12(${i})">`+
let map2D = `<div id="seg${i}map2D" data-map="map2D" data-fx="${inst.fx}" class="lbl-s hide">Expand 1D FX<br>`+
`<div class="sel-p"><select class="sel-p" id="seg${i}m12" onchange="setSegProp(${i},'m12')">`+
`<option value="0" ${inst.m12==0?' selected':''}>Pixels</option>`+
`<option value="1" ${inst.m12==1?' selected':''}>Bar</option>`+
`<option value="2" ${inst.m12==2?' selected':''}>Arc</option>`+
Expand All @@ -768,7 +782,7 @@ function populateSegments(s)
`</select></div>`+
`</div>`;
let blend = `<div class="lbl-l">Blend mode<br>`+
`<div class="sel-p"><select class="sel-ple" id="seg${i}bm" onchange="setBm(${i})">`+
`<div class="sel-p"><select class="sel-ple" id="seg${i}bm" onchange="setSegProp(${i},'bm')">`+
`<option value="0" ${inst.bm==0?' selected':''}>Top/Default</option>`+
`<option value="1" ${inst.bm==1?' selected':''}>Bottom/None</option>`+
`<option value="2" ${inst.bm==2?' selected':''}>Add</option>`+
Expand All @@ -788,7 +802,7 @@ function populateSegments(s)
`</select></div>`+
`</div>`;
let sndSim = `<div data-snd="si" class="lbl-s hide">Sound sim<br>`+
`<div class="sel-p"><select class="sel-p" id="seg${i}si" onchange="setSi(${i})">`+
`<div class="sel-p"><select class="sel-p" id="seg${i}si" onchange="setSegProp(${i},'si')">`+
`<option value="0" ${inst.si==0?' selected':''}>BeatSin</option>`+
`<option value="1" ${inst.si==1?' selected':''}>WeWillRockYou</option>`+
`<option value="2" ${inst.si==2?' selected':''}>10/13</option>`+
Expand Down Expand Up @@ -844,12 +858,14 @@ function populateSegments(s)
`<div class="h bp" id="seg${i}len"></div>`+
blend +
(!isMSeg ? rvXck : '') +
(isMSeg?zoom:'')+
(isMSeg?rotate:'')+
(isMSeg&&stoY-staY>1&&stoX-staX>1 ? map2D : '') +
(s.AudioReactive && s.AudioReactive.on ? "" : sndSim) +
`<label class="check revchkl" id="seg${i}lbtm">`+
(isMSeg?'Transpose':'Mirror effect') + (isMSeg ?
'<input type="checkbox" id="seg'+i+'tp" onchange="setTp('+i+')" '+(inst.tp?"checked":"")+'>':
'<input type="checkbox" id="seg'+i+'mi" onchange="setMi('+i+')" '+(inst.mi?"checked":"")+'>') +
'<input type="checkbox" id="seg'+i+'tp" onchange="setSegProp('+i+',\'tp\')" '+(inst.tp?"checked":"")+'>':
'<input type="checkbox" id="seg'+i+'mi" onchange="setSegProp('+i+',\'mi\')" '+(inst.mi?"checked":"")+'>') +
`<span class="checkmark"></span>`+
`</label>`+
`<div class="del">`+
Expand All @@ -870,6 +886,8 @@ function populateSegments(s)
if (!gId(`seg${i}`)) continue;
updateLen(i);
updateTrail(gId(`seg${i}bri`));
let r = gId(`seg${i}rS`); if (r) updateTrail(r);
let z = gId(`seg${i}zA`); if (z) updateTrail(z);
gId(`segr${i}`).classList.add("hide");
}
if (segCount < 2) {
Expand Down Expand Up @@ -2265,6 +2283,14 @@ function delSeg(s)
requestJson(obj);
}

function setSegProp(s,p)
{
let o = gId(`seg${s}${p}`);
let val = o.type === "checkbox" ? o.checked : parseInt(o.value);
var obj = {"seg": {"id": s, [p]: val}};
requestJson(obj);
}
/*
function setRev(s)
{
var rev = gId(`seg${s}rev`).checked;
Expand Down Expand Up @@ -2320,28 +2346,40 @@ function setTp(s)
var obj = {"seg": {"id": s, "tp": tp}};
requestJson(obj);
}

*/
function setGrp(s, g)
{
event.preventDefault();
event.stopPropagation();
var obj = {"seg": {"id": s, "set": g}};
requestJson(obj);
}
/*
function setZoom(s)
{
var obj = {"seg": {"id": s, "zA": parseInt(gId(`seg${s}za`).value)}};
requestJson(obj);
}

function setRotation(s)
{
var obj = {"seg": {"id": s, "rS": parseInt(gId(`seg${s}rs`).value)}};
requestJson(obj);
}
*/
function setSegPwr(s)
{
var pwr = gId(`seg${s}pwr`).classList.contains('act');
var obj = {"seg": {"id": s, "on": !pwr}};
requestJson(obj);
}

/*
function setSegBri(s)
{
var obj = {"seg": {"id": s, "bri": parseInt(gId(`seg${s}bri`).value)}};
requestJson(obj);
}

*/
function tglFreeze(s=null)
{
var obj = {"seg": {"frz": "t"}}; // toggle
Expand Down
Loading