memlnaut-nisps/vcv/src/LedRing.hpp
monkey-w1n5t0n fbc68ebfab feat(vcv): evolve module to 8x16 + LED rings + token palette + WS-OSC bridge
8 inputs x 16 outputs; per-output LED ring widget (drawLayer+nvgArc); palette
from frontend tokens; OSC bridge verbs for bidirectional browser training;
vendored self-contained iml.hpp (retired nisps-core); compiles against Rack
SDK 2.6.4. See SPEC.md BUILD DELTAS.
2026-06-28 04:14:30 +02:00

75 lines
2.8 KiB
C++

// LedRing.hpp — Custom LED-ring widget encircling each output jack.
//
// The arc fills clockwise from 12 o'clock in proportion to the output's value
// (0..1 → 0..2π) and is drawn on drawLayer() layer 1 (so it stays bright when
// room brightness is lowered, per the VCV custom-light convention). A dim track
// ring sits underneath. Colour comes from the frontend design tokens
// (palette.hpp) so the module matches the browser.
//
// Templated on the module type so it can read the per-output value without a
// hard include cycle; MEMLNaut.cpp instantiates LedRingWidget<MEMLNaut>.
#pragma once
#include <rack.hpp>
#include "palette.hpp"
using namespace rack;
template<typename TModule>
struct LedRingWidget : Widget {
TModule* module = nullptr;
int outIdx = 0;
NVGcolor ringColor = memlnaut::palette::accent();
float radius = 9.f; // px around a PJ301M jack
LedRingWidget() {
// Box large enough to host the ring around a ~22px jack.
box.size = Vec(radius * 2.f + 6.f, radius * 2.f + 6.f);
}
float valueOf() const {
if (!module) return 0.f;
return clamp(module->ringValue(outIdx), 0.f, 1.f);
}
void drawLayer(const DrawArgs& args, int layer) override {
if (layer != 1) {
Widget::drawLayer(args, layer);
return;
}
float v = valueOf();
Vec c = box.size.div(2.f);
const float start = -M_PI / 2.f; // 12 o'clock
const float end = start + 2.f * M_PI; // full circle
// Dim track ring (full circle).
nvgBeginPath(args.vg);
nvgArc(args.vg, c.x, c.y, radius, start, end, NVG_CW);
nvgStrokeColor(args.vg, nvgRGBA((unsigned char)(ringColor.r * 255),
(unsigned char)(ringColor.g * 255),
(unsigned char)(ringColor.b * 255), 40));
nvgStrokeWidth(args.vg, 1.4f);
nvgStroke(args.vg);
// Proportional value arc.
if (v > 0.001f) {
nvgBeginPath(args.vg);
nvgArc(args.vg, c.x, c.y, radius, start, start + v * 2.f * M_PI, NVG_CW);
nvgStrokeColor(args.vg, ringColor);
nvgStrokeWidth(args.vg, 1.9f);
nvgLineCap(args.vg, NVG_ROUND);
nvgStroke(args.vg);
// Soft glow halo — the frontend "glow not shadow" signature.
nvgGlobalCompositeOperation(args.vg, NVG_LIGHTER);
nvgBeginPath(args.vg);
nvgArc(args.vg, c.x, c.y, radius, start, start + v * 2.f * M_PI, NVG_CW);
nvgStrokeColor(args.vg, nvgRGBAf(ringColor.r, ringColor.g, ringColor.b, 0.25f * v));
nvgStrokeWidth(args.vg, 4.0f);
nvgStroke(args.vg);
nvgGlobalCompositeOperation(args.vg, NVG_SOURCE_OVER);
}
Widget::drawLayer(args, layer);
}
};