Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
364 changes: 265 additions & 99 deletions docs/ADDING_EFFECTS.md

Large diffs are not rendered by default.

44 changes: 1 addition & 43 deletions src/api/effects_handler.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -77,52 +77,10 @@ void handleGetEffects(AsyncWebServerRequest* request) {
effect["name"] = info->displayName;
effect["category"] = info->categoryName();

// Include schema if available
// All effects now have schemas - serialize params from schema
JsonArray params = effect["params"]. to<JsonArray>();
if (info->hasSchema()) {
schemaToJson(params, info->schema);
} else {
// Legacy: generate params from flags
if (info->usesSpeed) {
JsonObject p = params.add<JsonObject>();
p["id"] = "speed";
p["name"] = "Speed";
p["type"] = "int";
p["min"] = 0;
p["max"] = 255;
p["default"] = 128;
}
if (info->usesIntensity) {
JsonObject p = params.add<JsonObject>();
p["id"] = "intensity";
p["name"] = "Intensity";
p["type"] = "int";
p["min"] = 0;
p["max"] = 255;
p["default"] = 128;
}
// Generate color params based on colorCount
for (uint8_t c = 0; c < info->colorCount; c++) {
JsonObject p = params.add<JsonObject>();
if (c == 0) {
p["id"] = "color";
p["name"] = "Color";
} else {
char idBuf[16], nameBuf[16];
snprintf(idBuf, sizeof(idBuf), "color%d", c);
snprintf(nameBuf, sizeof(nameBuf), "Color %d", c + 1);
p["id"] = idBuf;
p["name"] = nameBuf;
}
p["type"] = "color";
p["default"] = "#ff0000";
}
if (info->usesPalette) {
JsonObject p = params.add<JsonObject>();
p["id"] = "palette";
p["name"] = "Palette";
p["type"] = "palette";
}
}
}

Expand Down
54 changes: 6 additions & 48 deletions src/api/segments.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -535,59 +535,17 @@ void handleApiV2EffectsList(AsyncWebServerRequest* request) {
effect["name"] = info->displayName;
effect["category"] = info->categoryName();

// Include schema if available
// All effects now have schemas - serialize params from schema
JsonArray params = effect["params"].to<JsonArray>();
if (info->hasSchema()) {
schemaToJson(params, info->schema);
} else {
// Legacy: generate params from flags
if (info->usesSpeed) {
JsonObject p = params.add<JsonObject>();
p["id"] = "speed";
p["name"] = "Speed";
p["type"] = "int";
p["min"] = 0;
p["max"] = 255;
p["default"] = 128;
}
if (info->usesIntensity) {
JsonObject p = params.add<JsonObject>();
p["id"] = "intensity";
p["name"] = "Intensity";
p["type"] = "int";
p["min"] = 0;
p["max"] = 255;
p["default"] = 128;
}
// Generate color params based on colorCount
for (uint8_t c = 0; c < info->colorCount; c++) {
JsonObject p = params.add<JsonObject>();
if (c == 0) {
p["id"] = "color";
p["name"] = "Color";
} else {
char idBuf[16], nameBuf[16];
snprintf(idBuf, sizeof(idBuf), "color%d", c);
snprintf(nameBuf, sizeof(nameBuf), "Color %d", c + 1);
p["id"] = idBuf;
p["name"] = nameBuf;
}
p["type"] = "color";
p["default"] = "#ff0000";
}
if (info->usesPalette) {
JsonObject p = params.add<JsonObject>();
p["id"] = "palette";
p["name"] = "Palette";
p["type"] = "palette";
}
}

// Effect metadata
effect["usesPalette"] = info->usesPalette;
effect["colorCount"] = info->colorCount;
effect["usesSpeed"] = info->usesSpeed;
effect["usesIntensity"] = info->usesIntensity;
// Effect metadata (derived from schema)
effect["usesPalette"] = info->usesPalette();
effect["colorCount"] = info->colorCount();
effect["usesSpeed"] = info->hasParam("speed");
effect["usesIntensity"] = info->hasParam("intensity");
}

String output;
Expand Down
19 changes: 0 additions & 19 deletions src/core/effect_params.h
Original file line number Diff line number Diff line change
Expand Up @@ -5,25 +5,6 @@

namespace lume {

/**
* EffectParams - Legacy parameters struct (being phased out)
*
* Now only contains palette. All other parameters moved to ParamValues.
* Kept temporarily for palette initialization until Segment class refactor complete.
*/
struct EffectParams {
// Palette for multi-color effects
CRGBPalette16 palette;

// Defaults
EffectParams()
: palette(RainbowColors_p) {}

// Constructor from palette
EffectParams(CRGBPalette16 pal)
: palette(pal) {}
};

/**
* BlendMode - How overlapping segments combine
*/
Expand Down
100 changes: 33 additions & 67 deletions src/core/effect_registry.h
Original file line number Diff line number Diff line change
Expand Up @@ -11,24 +11,23 @@ namespace lume {
* Effect function signature
*
* All effects are pure functions with this signature:
* void effectName(SegmentView& view, const EffectParams& params,
* const ParamValues& paramValues, uint32_t frame, bool firstFrame)
* void effectName(SegmentView& view, const ParamValues& params,
* uint32_t frame, bool firstFrame)
*
* - view: The segment to render to (LED array slice)
* - params: Legacy colors, speed, palette (for backward compatibility)
* - paramValues: Schema-aware typed parameter values
* - view: The segment to render to (LED array slice with scratchpad access)
* - params: Schema-aware typed parameter values (includes palette via getPalette())
* - frame: Global frame counter (for timing, use with beatsin8 etc.)
* - firstFrame: True when scratchpad was just reset (effect change)
*
* Effects should:
* - Write colors to view[0..view.size()-1]
* - Use frame for animation timing (not millis())
* - Initialize scratchpad state when firstFrame is true
* - Initialize scratchpad state when firstFrame is true via view.getScratchpad<T>()
* - Avoid global/static state - use segment scratchpad instead
* - Be deterministic given the same inputs
*/
using EffectFn = void (*)(SegmentView& view, const EffectParams& params,
const ParamValues& paramValues, uint32_t frame, bool firstFrame);
using EffectFn = void (*)(SegmentView& view, const ParamValues& params,
uint32_t frame, bool firstFrame);

/**
* Effect categories for UI grouping and filtering
Expand All @@ -48,24 +47,40 @@ struct EffectInfo {
const char* displayName; // Human-readable name: "Fire"
EffectCategory category; // For UI grouping

// NEW: Schema pointer (nullptr = legacy effect, uses old EffectParams)
// Schema pointer (all effects must have schema now)
const ParamSchema* schema;

// Parameter support flags (enables smart UI)
bool usesPalette; // Responds to palette changes
uint8_t colorCount; // Number of colors used (0-3): 0=none, 1=colors[0], 2=colors[0-1], 3=all
bool usesSpeed; // Responds to speed param
bool usesIntensity; // Responds to intensity param

// Resource hints
uint16_t stateSize; // Bytes needed in scratchpad (0 = stateless)
uint16_t minLeds; // Minimum LEDs for effect to look good

EffectFn fn; // The actual effect function

// Helper: has new-style schema?
// Helper: has schema
bool hasSchema() const { return schema != nullptr && schema->count > 0; }

// Helper: check if effect uses palette parameter
bool usesPalette() const {
return hasSchema() && schema->find("palette") != nullptr;
}

// Helper: count color parameters
uint8_t colorCount() const {
if (!hasSchema()) return 0;
uint8_t count = 0;
for (uint8_t i = 0; i < schema->count; i++) {
if (schema->params[i].type == ParamType::Color) {
count++;
}
}
return count;
}

// Helper: check if parameter exists
bool hasParam(const char* id) const {
return hasSchema() && schema->find(id) != nullptr;
}

// Helper to get category name
const char* categoryName() const {
switch (category) {
Expand Down Expand Up @@ -172,71 +187,22 @@ class EffectRegistrar {
}
};

/**
* REGISTER_EFFECT_FULL macro - All parameters explicit
*
* Usage:
* REGISTER_EFFECT_FULL(effectFire, "fire", "Fire", Animated,
* false, 0, true, true, sizeof(FireState), 10);
* // usesPal, colorCount, usesSpd, usesInt, stateSize, minLeds
*/
#define REGISTER_EFFECT_FULL(fn, idStr, dispName, cat, usesPal, colCnt, usesSpd, usesInt, stateSz, minL) \
static lume::EffectRegistrar _registrar_##fn({ \
idStr, dispName, lume::EffectCategory::cat, \
nullptr, \
usesPal, colCnt, usesSpd, usesInt, \
stateSz, minL, fn \
})

// NEW: Schema-aware registration macro
// Schema-aware registration macro
#define REGISTER_EFFECT_SCHEMA(fn, idStr, dispName, cat, schemaRef, stateSz) \
static lume::EffectRegistrar _registrar_##fn({ \
idStr, dispName, lume::EffectCategory::cat, \
&schemaRef, \
false, 0, false, false, \
stateSz, 1, fn \
})

// NEW: Convenience macro to define schema inline
// Convenience macro to define schema inline
#define DEFINE_EFFECT_SCHEMA(name, ...) \
static const lume::ParamDesc name##_params[] = { __VA_ARGS__ }; \
static const lume::ParamSchema name = { \
name##_params, \
sizeof(name##_params) / sizeof(name##_params[0]) \
}

// Simple effect: animated, uses speed only
#define REGISTER_EFFECT_SIMPLE(fn, idStr) \
REGISTER_EFFECT_FULL(fn, idStr, idStr, Animated, false, 0, true, false, 0, 1)

// Simple effect with display name
#define REGISTER_EFFECT_SIMPLE_NAMED(fn, idStr, dispName) \
REGISTER_EFFECT_FULL(fn, idStr, dispName, Animated, false, 0, true, false, 0, 1)

// Static solid-type effect: no animation, uses colors[0]
#define REGISTER_EFFECT_SOLID(fn, idStr, dispName) \
REGISTER_EFFECT_FULL(fn, idStr, dispName, Solid, false, 1, false, false, 0, 1)

// Animated effect with speed + intensity
#define REGISTER_EFFECT_ANIMATED(fn, idStr, dispName) \
REGISTER_EFFECT_FULL(fn, idStr, dispName, Animated, false, 0, true, true, 0, 1)

// Moving effect with speed + intensity
#define REGISTER_EFFECT_MOVING(fn, idStr, dispName) \
REGISTER_EFFECT_FULL(fn, idStr, dispName, Moving, false, 0, true, true, 0, 1)

// Palette-based effect: uses palette and speed
#define REGISTER_EFFECT_PALETTE(fn, idStr, dispName) \
REGISTER_EFFECT_FULL(fn, idStr, dispName, Animated, true, 0, true, false, 0, 1)

// Effect using colors[0] + colors[1]
#define REGISTER_EFFECT_COLORS(fn, idStr, dispName) \
REGISTER_EFFECT_FULL(fn, idStr, dispName, Animated, false, 2, true, false, 0, 1)

// Stateful effect: requires scratchpad state
#define REGISTER_EFFECT_STATEFUL(fn, idStr, dispName, stateType) \
REGISTER_EFFECT_FULL(fn, idStr, dispName, Animated, false, 0, true, true, sizeof(stateType), 1)

// Convenience function to get registry
inline EffectRegistry& effects() {
return EffectRegistry::instance();
Expand Down
Loading