@@ -11,6 +11,7 @@ use nih_plug_egui::egui::{
|
||||
};
|
||||
|
||||
use crate::params::Bari206Params;
|
||||
use super::palette;
|
||||
|
||||
/// Height of the handle strip.
|
||||
const GRAPH_H: f32 = 54.0;
|
||||
@@ -55,19 +56,19 @@ pub(super) fn draw(ui: &mut egui::Ui, params: &Bari206Params, setter: &ParamSett
|
||||
let xhi = x_for(hi);
|
||||
|
||||
// Three band regions.
|
||||
p.rect_filled(Rect::from_min_max(pos2(left, top), pos2(xlo, bottom)), CornerRadius::ZERO, Color32::from_rgb(28, 38, 52));
|
||||
p.rect_filled(Rect::from_min_max(pos2(xlo, top), pos2(xhi, bottom)), CornerRadius::ZERO, Color32::from_rgb(30, 48, 36));
|
||||
p.rect_filled(Rect::from_min_max(pos2(xhi, top), pos2(right, bottom)), CornerRadius::ZERO, Color32::from_rgb(52, 38, 30));
|
||||
p.rect_filled(Rect::from_min_max(pos2(left, top), pos2(xlo, bottom)), CornerRadius::ZERO, palette::CHANNELS[0].strip);
|
||||
p.rect_filled(Rect::from_min_max(pos2(xlo, top), pos2(xhi, bottom)), CornerRadius::ZERO, palette::CHANNELS[1].strip);
|
||||
p.rect_filled(Rect::from_min_max(pos2(xhi, top), pos2(right, bottom)), CornerRadius::ZERO, palette::CHANNELS[2].strip);
|
||||
|
||||
let band_label = |cx: f32, t: &str| {
|
||||
p.text(pos2(cx, top + 2.0), Align2::CENTER_TOP, t, FontId::proportional(11.0), Color32::from_gray(160));
|
||||
p.text(pos2(cx, top + 2.0), Align2::CENTER_TOP, t, FontId::proportional(11.0), palette::TEXT);
|
||||
};
|
||||
band_label((left + xlo) * 0.5, "LOW");
|
||||
band_label((xlo + xhi) * 0.5, "MID");
|
||||
band_label((xhi + right) * 0.5, "HIGH");
|
||||
|
||||
handle(ui, &p, setter, "xover_lo", xlo, lo, ¶ms.crossover_low_hz, lo_min, lo_upper, left, width, top, bottom, Color32::from_rgb(120, 170, 230));
|
||||
handle(ui, &p, setter, "xover_hi", xhi, hi, ¶ms.crossover_high_hz, hi_lower, hi_max, left, width, top, bottom, Color32::from_rgb(230, 150, 120));
|
||||
handle(ui, &p, setter, "xover_lo", xlo, lo, ¶ms.crossover_low_hz, lo_min, lo_upper, left, width, top, bottom, palette::CHANNELS[0].handle);
|
||||
handle(ui, &p, setter, "xover_hi", xhi, hi, ¶ms.crossover_high_hz, hi_lower, hi_max, left, width, top, bottom, palette::CHANNELS[2].handle);
|
||||
|
||||
// Number boxes (double-click to type). Clamped to the same dynamic limits.
|
||||
ui.horizontal(|ui| {
|
||||
@@ -129,9 +130,9 @@ fn handle(
|
||||
setter.end_set_parameter(param);
|
||||
}
|
||||
|
||||
let col = if resp.dragged() || resp.hovered() { Color32::WHITE } else { color };
|
||||
let col = if resp.dragged() || resp.hovered() { palette::TEXT_HOVER } else { color };
|
||||
p.line_segment([pos2(x, top), pos2(x, bottom)], Stroke::new(2.0, col));
|
||||
p.text(pos2(x, bottom - 1.0), Align2::CENTER_BOTTOM, fmt_hz(hz), FontId::proportional(10.0), Color32::from_gray(220));
|
||||
p.text(pos2(x, bottom - 1.0), Align2::CENTER_BOTTOM, fmt_hz(hz), FontId::proportional(10.0), palette::TEXT_BRIGHT);
|
||||
}
|
||||
|
||||
/// Set a param to `value` clamped to `[min, max]`, wrapped in its own gesture (for the number box).
|
||||
|
||||
+12
-11
@@ -5,12 +5,13 @@
|
||||
//! the channel's current input level — its right edge rides the curve (width = input, height = out).
|
||||
|
||||
use nih_plug::prelude::util;
|
||||
use nih_plug_egui::egui::{self, pos2, vec2, Align2, Color32, CornerRadius, FontId, Sense, Stroke};
|
||||
use nih_plug_egui::egui::{self, pos2, vec2, Align2, CornerRadius, FontId, Sense, Stroke};
|
||||
use std::sync::atomic::Ordering;
|
||||
|
||||
use crate::dsp::compressor::Compressor;
|
||||
use crate::meters::Meters;
|
||||
use crate::params::Bari206Params;
|
||||
use super::palette;
|
||||
|
||||
/// Side length of the square plot.
|
||||
const CURVE_SIZE: f32 = 150.0;
|
||||
@@ -37,7 +38,7 @@ pub(super) fn draw(ui: &mut egui::Ui, params: &Bari206Params, selected: usize, m
|
||||
ui.label(format!("Curve: {}", labels[ch]));
|
||||
let (rect, _) = ui.allocate_exact_size(vec2(CURVE_SIZE, CURVE_SIZE), Sense::hover());
|
||||
let p = ui.painter_at(rect);
|
||||
p.rect_filled(rect, CornerRadius::ZERO, Color32::from_rgb(16, 16, 20));
|
||||
p.rect_filled(rect, CornerRadius::ZERO, palette::SURFACE);
|
||||
|
||||
let inset = 2.0;
|
||||
let (left, right, top, bottom) =
|
||||
@@ -48,17 +49,17 @@ pub(super) fn draw(ui: &mut egui::Ui, params: &Bari206Params, selected: usize, m
|
||||
let y_for = |db: f32| bottom - (db - FLOOR_DB) / -FLOOR_DB * h;
|
||||
|
||||
// -6 dBFS reference lines on both axes.
|
||||
let g6 = Color32::from_gray(38);
|
||||
let g6 = palette::GRID_FAINT;
|
||||
let x6 = x_for(-6.0);
|
||||
let y6 = y_for(-6.0);
|
||||
p.line_segment([pos2(x6, top), pos2(x6, bottom)], Stroke::new(1.0, g6));
|
||||
p.line_segment([pos2(left, y6), pos2(right, y6)], Stroke::new(1.0, g6));
|
||||
|
||||
// Unity reference (out = in), bottom-left to top-right.
|
||||
p.line_segment([pos2(left, bottom), pos2(right, top)], Stroke::new(1.0, Color32::from_gray(45)));
|
||||
p.line_segment([pos2(left, bottom), pos2(right, top)], Stroke::new(1.0, palette::GRID));
|
||||
// Threshold marker on the input axis — shifted left by pre-gain (the comp sees in + pre).
|
||||
let tx = x_for((threshold - pre).clamp(FLOOR_DB, 0.0));
|
||||
p.line_segment([pos2(tx, top), pos2(tx, bottom)], Stroke::new(1.0, Color32::from_rgb(80, 60, 45)));
|
||||
p.line_segment([pos2(tx, top), pos2(tx, bottom)], Stroke::new(1.0, palette::THRESHOLD_LINE));
|
||||
|
||||
// Full wet transfer: drive into the comp, then makeup. (Mix not folded in.)
|
||||
let n = 96;
|
||||
@@ -74,7 +75,7 @@ pub(super) fn draw(ui: &mut egui::Ui, params: &Bari206Params, selected: usize, m
|
||||
let driven_now = util::gain_to_db(meters.input_level[ch].load(Ordering::Relaxed));
|
||||
let ext_in = (driven_now - pre).clamp(FLOOR_DB, 0.0); // external input -> curve x
|
||||
let x_now = x_for(ext_in);
|
||||
let fill_col = Color32::from_rgba_unmultiplied(120, 200, 160, 45);
|
||||
let fill_col = palette::with_alpha(palette::CURVE, 45);
|
||||
for seg in pts.windows(2) {
|
||||
let a = seg[0];
|
||||
let mut b = seg[1];
|
||||
@@ -92,16 +93,16 @@ pub(super) fn draw(ui: &mut egui::Ui, params: &Bari206Params, selected: usize, m
|
||||
));
|
||||
}
|
||||
|
||||
p.add(egui::Shape::line(pts, Stroke::new(1.6, Color32::from_rgb(120, 200, 160))));
|
||||
p.add(egui::Shape::line(pts, Stroke::new(1.6, palette::CURVE)));
|
||||
|
||||
// Operating-point dot, on the curve at the current input.
|
||||
let driven = ext_in + pre;
|
||||
let gr = Compressor::gain_computer(driven, threshold, ratio, knee, low_slope, low_curve);
|
||||
let out_op = (driven + gr + makeup).clamp(FLOOR_DB, 0.0);
|
||||
p.circle_filled(pos2(x_now, y_for(out_op)), 3.0, Color32::from_rgb(235, 240, 235));
|
||||
p.circle_filled(pos2(x_now, y_for(out_op)), 3.0, palette::OPERATING_POINT);
|
||||
|
||||
// Corner dB ticks + the -6 dB reference.
|
||||
p.text(pos2(left + 1.0, top + 1.0), Align2::LEFT_TOP, "0", FontId::proportional(9.0), Color32::from_gray(90));
|
||||
p.text(pos2(left + 1.0, bottom - 1.0), Align2::LEFT_BOTTOM, "-60", FontId::proportional(9.0), Color32::from_gray(90));
|
||||
p.text(pos2(x6 + 2.0, bottom - 1.0), Align2::LEFT_BOTTOM, "-6", FontId::proportional(9.0), Color32::from_gray(80));
|
||||
p.text(pos2(left + 1.0, top + 1.0), Align2::LEFT_TOP, "0", FontId::proportional(9.0), palette::TEXT_DIM);
|
||||
p.text(pos2(left + 1.0, bottom - 1.0), Align2::LEFT_BOTTOM, "-60", FontId::proportional(9.0), palette::TEXT_DIM);
|
||||
p.text(pos2(x6 + 2.0, bottom - 1.0), Align2::LEFT_BOTTOM, "-6", FontId::proportional(9.0), palette::TEXT_DIM);
|
||||
}
|
||||
|
||||
+25
-19
@@ -15,6 +15,7 @@ use std::sync::atomic::Ordering;
|
||||
|
||||
use super::METER_FLOOR_DB;
|
||||
use crate::meters::{Meters, NUM_CHANNELS};
|
||||
use super::palette;
|
||||
|
||||
/// Full-scale of the gain-reduction bar (fills downward from the top).
|
||||
const GR_FULL_DB: f32 = 24.0;
|
||||
@@ -47,7 +48,7 @@ pub(super) fn draw(ui: &mut egui::Ui, meters: &Meters, state: &mut MeterState) {
|
||||
let (rect, _) =
|
||||
ui.allocate_exact_size(vec2(ui.available_width(), METER_PANEL_H), Sense::hover());
|
||||
let p = ui.painter_at(rect);
|
||||
p.rect_filled(rect, CornerRadius::ZERO, Color32::from_rgb(20, 20, 24));
|
||||
p.rect_filled(rect, CornerRadius::ZERO, palette::SURFACE_RAISED);
|
||||
|
||||
let top = rect.top() + 22.0; // leave a row at the top for the lamps
|
||||
let bottom = rect.bottom() - 18.0; // and a row at the bottom for the labels
|
||||
@@ -58,6 +59,22 @@ pub(super) fn draw(ui: &mut egui::Ui, meters: &Meters, state: &mut MeterState) {
|
||||
|
||||
for i in 0..NUM_CHANNELS {
|
||||
let cell_left = rect.left() + i as f32 * cell_w;
|
||||
|
||||
// Channel chrome. The four clusters are otherwise identical and told apart only by the
|
||||
// label underneath, which is the one place channel identity is hardest to read. A dark
|
||||
// wash behind the cell plus an accent rule under the bars fixes that without touching
|
||||
// the bars themselves - see `palette`: hue is chrome, functional colour is data.
|
||||
let cell = Rect::from_min_max(
|
||||
pos2(cell_left + 1.0, rect.top()),
|
||||
pos2(cell_left + cell_w - 1.0, rect.bottom()),
|
||||
);
|
||||
p.rect_filled(cell, CornerRadius::ZERO, palette::CHANNELS[i].chrome);
|
||||
p.rect_filled(
|
||||
Rect::from_min_max(pos2(cell.left(), bottom + 2.0), pos2(cell.right(), bottom + 4.0)),
|
||||
CornerRadius::ZERO,
|
||||
palette::CHANNELS[i].edge,
|
||||
);
|
||||
|
||||
let group_w = bar_w * 3.0 + gap * 2.0;
|
||||
let bx = cell_left + (cell_w - group_w) * 0.5;
|
||||
|
||||
@@ -71,16 +88,16 @@ pub(super) fn draw(ui: &mut egui::Ui, meters: &Meters, state: &mut MeterState) {
|
||||
let gr_db = meters.gain_reduction_db[i].load(Ordering::Relaxed);
|
||||
let gr_frac = (gr_db / GR_FULL_DB).clamp(0.0, 1.0);
|
||||
|
||||
v_bar(&p, bx, bar_w, top, bottom, l_frac, level_color(l_db), false);
|
||||
v_bar(&p, bx + bar_w + gap, bar_w, top, bottom, gr_frac, Color32::from_rgb(240, 150, 60), true);
|
||||
v_bar(&p, bx + 2.0 * (bar_w + gap), bar_w, top, bottom, r_frac, level_color(r_db), false);
|
||||
v_bar(&p, bx, bar_w, top, bottom, l_frac, palette::level_color(l_db), false);
|
||||
v_bar(&p, bx + bar_w + gap, bar_w, top, bottom, gr_frac, palette::SIGNAL_GR, true);
|
||||
v_bar(&p, bx + 2.0 * (bar_w + gap), bar_w, top, bottom, r_frac, palette::level_color(r_db), false);
|
||||
|
||||
p.text(
|
||||
pos2(cell_left + cell_w * 0.5, rect.bottom() - 2.0),
|
||||
Align2::CENTER_BOTTOM,
|
||||
labels[i],
|
||||
FontId::proportional(12.0),
|
||||
Color32::from_gray(200),
|
||||
palette::TEXT_BRIGHT,
|
||||
);
|
||||
|
||||
// Per-channel lamp: latch on output reaching 0 dBFS; the ALL channel also latches when the
|
||||
@@ -109,9 +126,9 @@ pub(super) fn draw(ui: &mut egui::Ui, meters: &Meters, state: &mut MeterState) {
|
||||
}
|
||||
}
|
||||
let lamp = if state.ceiling_trigger[i].is_some() {
|
||||
Color32::from_rgb(255, 40, 40)
|
||||
palette::LAMP_ON
|
||||
} else {
|
||||
Color32::from_rgb(40, 12, 12)
|
||||
palette::LAMP_OFF
|
||||
};
|
||||
p.circle_filled(lamp_center, 5.0, lamp);
|
||||
}
|
||||
@@ -120,7 +137,7 @@ pub(super) fn draw(ui: &mut egui::Ui, meters: &Meters, state: &mut MeterState) {
|
||||
/// Draw a vertical bar within `[top, bottom]`. `frac` is 0..1; `from_top` fills downward from the
|
||||
/// top (gain reduction) instead of upward from the bottom (level).
|
||||
fn v_bar(p: &Painter, x: f32, w: f32, top: f32, bottom: f32, frac: f32, fill: Color32, from_top: bool) {
|
||||
let track = Color32::from_rgb(34, 34, 40);
|
||||
let track = palette::TRACK;
|
||||
p.rect_filled(Rect::from_min_max(pos2(x, top), pos2(x + w, bottom)), CornerRadius::ZERO, track);
|
||||
|
||||
let h = (bottom - top) * frac.clamp(0.0, 1.0);
|
||||
@@ -131,14 +148,3 @@ fn v_bar(p: &Painter, x: f32, w: f32, top: f32, bottom: f32, frac: f32, fill: Co
|
||||
};
|
||||
p.rect_filled(filled, CornerRadius::ZERO, fill);
|
||||
}
|
||||
|
||||
/// Level-bar colour: green below -6 dB, yellow approaching, red near 0 dBFS.
|
||||
fn level_color(db: f32) -> Color32 {
|
||||
if db >= -1.0 {
|
||||
Color32::from_rgb(235, 70, 60)
|
||||
} else if db >= -6.0 {
|
||||
Color32::from_rgb(230, 200, 70)
|
||||
} else {
|
||||
Color32::from_rgb(90, 200, 110)
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -23,6 +23,7 @@ use crate::Bari206;
|
||||
mod crossover;
|
||||
mod gain_curve;
|
||||
mod meter;
|
||||
mod palette;
|
||||
mod plot;
|
||||
|
||||
/// Bottom of the dB scale shared by the meters and the plot (top is 0 dBFS).
|
||||
@@ -41,7 +42,7 @@ pub(crate) fn create(params: Arc<Bari206Params>, meters: Arc<Meters>) -> Option<
|
||||
create_egui_editor(
|
||||
params.editor_state.clone(),
|
||||
EditorState::default(),
|
||||
|_, _| {},
|
||||
|egui_ctx, _| palette::apply_visuals(egui_ctx),
|
||||
move |egui_ctx, setter, state| {
|
||||
// Keep frames coming so the meters animate and the lamp can time out while open.
|
||||
egui_ctx.request_repaint();
|
||||
|
||||
@@ -0,0 +1,265 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
//! The plugin's colour system.
|
||||
//!
|
||||
//! # The rule
|
||||
//!
|
||||
//! **Channel hue is chrome; functional colour is data.** Before picking a colour, ask what the
|
||||
//! pixel is for. Is it showing a *value* — a level, a reduction, a clip? Then it comes from the
|
||||
//! functional palette, and it means the same thing in every channel. Is it telling you *which
|
||||
//! channel you are looking at*? Then it comes from that channel's hue, and it never touches the
|
||||
//! data itself.
|
||||
//!
|
||||
//! This is what keeps the two systems from fighting. An earlier version of this UI used blue,
|
||||
//! green and orange for LOW/MID/HIGH in the crossover *and* for input/output/gain-reduction in
|
||||
//! the scope, so orange meant "the high band" in one widget and "gain reduction" in the one next
|
||||
//! to it.
|
||||
//!
|
||||
//! # The two tiers
|
||||
//!
|
||||
//! 1. **Seeds** — the short, hand-picked list in the first half of this file. This is the part
|
||||
//! you edit. Everything else is derived from it.
|
||||
//! 2. **Derived** — [`Channel`] palettes built from a hue by [`channel`]. Change a hue and every
|
||||
//! tint, edge and strip using it moves with it.
|
||||
//!
|
||||
//! All derivation is `const fn`, so the whole table is computed at compile time and costs nothing
|
||||
//! at runtime. That is also why it uses plain HSV rather than `ecolor::Hsva`, whose conversion is
|
||||
//! not `const`.
|
||||
//!
|
||||
//! # Editing this file
|
||||
//!
|
||||
//! * To recolour a channel, change one `HUE_*` constant. Never hand-edit a derived value.
|
||||
//! * To retune the meter's headroom ramp, edit [`LEVEL_OK`] / [`LEVEL_WARN`] / [`LEVEL_HOT`] and
|
||||
//! the thresholds in [`level_color`]. Both are deliberately literal rather than interpolated,
|
||||
//! so you can set each one by eye without fighting a gradient.
|
||||
//! * To change how loudly channel identity reads, edit the `(saturation, value)` pairs in
|
||||
//! [`channel`].
|
||||
//! * Anything new belongs here and must be reachable from a seed — a colour written at a call
|
||||
//! site is the next magic number.
|
||||
|
||||
use nih_plug_egui::egui::Color32;
|
||||
|
||||
// ===========================================================================
|
||||
// TIER 1 — SEEDS. This is the part to edit.
|
||||
// ===========================================================================
|
||||
|
||||
// --- Channel identity ------------------------------------------------------
|
||||
// Hues in degrees: 0 = red, 120 = green, 240 = blue. Only the hue is chosen here; saturation and
|
||||
// brightness are decided per role by `channel()`, so a channel reads as the same colour whether
|
||||
// it is a 12%-brightness background wash or a full-strength drag handle.
|
||||
//
|
||||
// Keep neighbours far enough apart to survive being dimmed — most chrome lives at low
|
||||
// saturation, where close hues merge. Roughly 60–90 degrees apart works; below ~40 does not.
|
||||
pub const HUE_LOW: f32 = 212.0; // blue
|
||||
pub const HUE_MID: f32 = 144.0; // green
|
||||
pub const HUE_HIGH: f32 = 26.0; // orange
|
||||
pub const HUE_ALL: f32 = 280.0; // violet — the aggregate, distinct from its three inputs
|
||||
|
||||
// --- Functional colours ----------------------------------------------------
|
||||
// Outside the hue system on purpose: these mean the same thing in every channel, so they must
|
||||
// NOT be derived from a channel hue. Red is always "you have hit a limit".
|
||||
|
||||
/// Meter bar with comfortable headroom.
|
||||
pub const LEVEL_OK: Color32 = Color32::from_rgb(90, 200, 110);
|
||||
/// Meter bar approaching full scale.
|
||||
pub const LEVEL_WARN: Color32 = Color32::from_rgb(230, 200, 70);
|
||||
/// Meter bar at or near full scale.
|
||||
pub const LEVEL_HOT: Color32 = Color32::from_rgb(235, 70, 60);
|
||||
|
||||
/// Latched over/ceiling lamp, lit.
|
||||
pub const LAMP_ON: Color32 = Color32::from_rgb(255, 40, 40);
|
||||
/// The same lamp unlit — dark red rather than grey, so it reads as "off", not "absent".
|
||||
pub const LAMP_OFF: Color32 = Color32::from_rgb(40, 12, 12);
|
||||
/// Ceiling-hit tick marks along the top edge of the scope.
|
||||
pub const CLIP: Color32 = Color32::from_rgb(235, 45, 45);
|
||||
|
||||
// Scope traces. The scope is tabbed — one channel at a time — so these never compete with a
|
||||
// channel hue: the tab chrome says which channel, these three say what each line is.
|
||||
pub const SIGNAL_IN: Color32 = Color32::from_rgb(90, 170, 235);
|
||||
pub const SIGNAL_OUT: Color32 = Color32::from_rgb(90, 200, 110);
|
||||
pub const SIGNAL_GR: Color32 = Color32::from_rgb(240, 150, 60);
|
||||
|
||||
/// The transfer curve in the gain-curve display, and its translucent fill.
|
||||
pub const CURVE: Color32 = Color32::from_rgb(120, 200, 160);
|
||||
/// The live operating-point dot riding that curve — near-white, so it reads on top of the fill.
|
||||
pub const OPERATING_POINT: Color32 = Color32::from_rgb(235, 240, 235);
|
||||
/// Threshold marker on the gain curve. Dim on purpose: a reference line, not data.
|
||||
pub const THRESHOLD_LINE: Color32 = Color32::from_rgb(80, 60, 45);
|
||||
|
||||
// --- Neutrals --------------------------------------------------------------
|
||||
// Eight ad-hoc greys collapsed to six named steps. If you need another, add it here rather than
|
||||
// reaching for `from_gray` at a call site.
|
||||
|
||||
/// Data panels — the scope and the gain curve.
|
||||
pub const SURFACE: Color32 = Color32::from_rgb(16, 16, 20);
|
||||
/// The meter panel, one step lighter so the cluster reads as its own surface. These were two
|
||||
/// different blacks by accident before; they are two on purpose now. Set this equal to
|
||||
/// [`SURFACE`] if you would rather they sat flush.
|
||||
pub const SURFACE_RAISED: Color32 = Color32::from_rgb(20, 20, 24);
|
||||
/// The empty part of a meter bar.
|
||||
pub const TRACK: Color32 = Color32::from_rgb(34, 34, 40);
|
||||
|
||||
/// Structural lines — the unity diagonal, panel edges.
|
||||
pub const GRID: Color32 = Color32::from_gray(45);
|
||||
/// Reference lines that should recede — dB gridlines, −6 dB marks.
|
||||
pub const GRID_FAINT: Color32 = Color32::from_gray(38);
|
||||
|
||||
/// Axis numbers and other text that must not compete with the data.
|
||||
pub const TEXT_DIM: Color32 = Color32::from_gray(90);
|
||||
/// Body text — band names on the crossover strip.
|
||||
pub const TEXT: Color32 = Color32::from_gray(160);
|
||||
/// Emphasised text — channel labels, readouts.
|
||||
pub const TEXT_BRIGHT: Color32 = Color32::from_gray(210);
|
||||
/// Hover and drag feedback, where the point is to be unmistakable.
|
||||
pub const TEXT_HOVER: Color32 = Color32::WHITE;
|
||||
|
||||
// ===========================================================================
|
||||
// TIER 2 — DERIVED. Generated from the seeds above; do not hand-edit.
|
||||
// ===========================================================================
|
||||
|
||||
/// One channel's colours, all derived from a single hue.
|
||||
pub struct Channel {
|
||||
/// Background wash behind a channel's cell. Very dark — it must associate the bars drawn on
|
||||
/// top of it with a channel without competing with them.
|
||||
pub chrome: Color32,
|
||||
/// Band strip on the crossover widget: between [`Channel::chrome`] and [`Channel::edge`].
|
||||
pub strip: Color32,
|
||||
/// Accent rule under a meter cell, and the scope's tab underline. Bright enough to identify
|
||||
/// at a glance.
|
||||
pub edge: Color32,
|
||||
/// Full strength — drag handles and anything else the user grabs.
|
||||
pub handle: Color32,
|
||||
}
|
||||
|
||||
/// Build a channel's palette from its hue.
|
||||
///
|
||||
/// The `(saturation, value)` pairs below decide how loudly channel identity reads. Raise
|
||||
/// `CHROME`'s value and the cell washes get more obvious; lower it and they recede into the
|
||||
/// panel background. `CHROME` is deliberately near the floor: it sits *behind* meter bars.
|
||||
pub const fn channel(hue: f32) -> Channel {
|
||||
const CHROME: (f32, f32) = (0.45, 0.115);
|
||||
const STRIP: (f32, f32) = (0.42, 0.20);
|
||||
const EDGE: (f32, f32) = (0.60, 0.55);
|
||||
const HANDLE: (f32, f32) = (0.55, 0.90);
|
||||
Channel {
|
||||
chrome: hsv(hue, CHROME.0, CHROME.1),
|
||||
strip: hsv(hue, STRIP.0, STRIP.1),
|
||||
edge: hsv(hue, EDGE.0, EDGE.1),
|
||||
handle: hsv(hue, HANDLE.0, HANDLE.1),
|
||||
}
|
||||
}
|
||||
|
||||
/// Indexed by the `LOW`/`MID`/`HIGH`/`ALL` consts in `lib.rs`, so `CHANNELS[i]` lines up with
|
||||
/// `meters.level_l[i]` and friends without a lookup step.
|
||||
pub const CHANNELS: [Channel; 4] = [
|
||||
channel(HUE_LOW),
|
||||
channel(HUE_MID),
|
||||
channel(HUE_HIGH),
|
||||
channel(HUE_ALL),
|
||||
];
|
||||
|
||||
/// HSV to RGB, `const` so [`CHANNELS`] is built at compile time.
|
||||
///
|
||||
/// `h` in degrees (wrapped), `s` and `v` in `0..=1`. The standard sextant algorithm, written out
|
||||
/// here only because `ecolor::Hsva`'s conversion is not a `const fn`.
|
||||
pub const fn hsv(h: f32, s: f32, v: f32) -> Color32 {
|
||||
// Wrap into 0..360. A loop rather than `%` because float remainder is not available in a
|
||||
// const context on this toolchain; hues are small numbers, so this costs nothing.
|
||||
let mut h = h;
|
||||
while h < 0.0 {
|
||||
h += 360.0;
|
||||
}
|
||||
while h >= 360.0 {
|
||||
h -= 360.0;
|
||||
}
|
||||
let sector = h / 60.0;
|
||||
let i = sector as i32;
|
||||
let f = sector - i as f32;
|
||||
let p = v * (1.0 - s);
|
||||
let q = v * (1.0 - s * f);
|
||||
let t = v * (1.0 - s * (1.0 - f));
|
||||
let (r, g, b) = match i {
|
||||
0 => (v, t, p),
|
||||
1 => (q, v, p),
|
||||
2 => (p, v, t),
|
||||
3 => (p, q, v),
|
||||
4 => (t, p, v),
|
||||
_ => (v, p, q),
|
||||
};
|
||||
Color32::from_rgb(to_u8(r), to_u8(g), to_u8(b))
|
||||
}
|
||||
|
||||
/// Clamping float-to-byte, `const` for the same reason as [`hsv`].
|
||||
const fn to_u8(x: f32) -> u8 {
|
||||
let v = x * 255.0 + 0.5;
|
||||
if v <= 0.0 {
|
||||
0
|
||||
} else if v >= 255.0 {
|
||||
255
|
||||
} else {
|
||||
v as u8
|
||||
}
|
||||
}
|
||||
|
||||
/// Meter bar colour for an output level in dB.
|
||||
///
|
||||
/// Functional, not per-channel: −6 dB means the same thing in every band. Thresholds and colours
|
||||
/// are both literal so either can be tuned by eye.
|
||||
pub fn level_color(db: f32) -> Color32 {
|
||||
if db >= -1.0 {
|
||||
LEVEL_HOT
|
||||
} else if db >= -6.0 {
|
||||
LEVEL_WARN
|
||||
} else {
|
||||
LEVEL_OK
|
||||
}
|
||||
}
|
||||
|
||||
/// The same colour at `a`/255 opacity, for translucent fills under a trace.
|
||||
///
|
||||
/// Not `const`: `Color32::from_rgba_unmultiplied` is not a `const fn`, and the premultiplied
|
||||
/// constructor that is would push the multiply out to every call site.
|
||||
pub fn with_alpha(c: Color32, a: u8) -> Color32 {
|
||||
Color32::from_rgba_unmultiplied(c.r(), c.g(), c.b(), a)
|
||||
}
|
||||
|
||||
/// Apply the palette to egui's own widgets, so `ParamSlider`, labels and separators sit in the
|
||||
/// same world as the hand-painted ones instead of on stock egui defaults.
|
||||
///
|
||||
/// Call once when the editor is created. Widget colours are per interaction state: `weak_bg_fill`
|
||||
/// is an unfilled slider track, `bg_fill` the filled portion. Interactive states borrow the ALL
|
||||
/// channel's hue, since egui widgets are not per-band — change that to a dedicated accent seed if
|
||||
/// you would rather they were neutral.
|
||||
pub fn apply_visuals(ctx: &nih_plug_egui::egui::Context) {
|
||||
use nih_plug_egui::egui::{Stroke, Visuals};
|
||||
|
||||
let accent = &CHANNELS[3];
|
||||
let mut v = Visuals::dark();
|
||||
|
||||
v.panel_fill = SURFACE;
|
||||
v.window_fill = SURFACE;
|
||||
v.extreme_bg_color = SURFACE;
|
||||
v.faint_bg_color = SURFACE_RAISED;
|
||||
|
||||
let w = &mut v.widgets;
|
||||
w.noninteractive.bg_fill = SURFACE_RAISED;
|
||||
w.noninteractive.weak_bg_fill = SURFACE_RAISED;
|
||||
w.noninteractive.bg_stroke = Stroke::new(1.0, GRID_FAINT);
|
||||
w.noninteractive.fg_stroke = Stroke::new(1.0, TEXT);
|
||||
|
||||
w.inactive.bg_fill = TRACK;
|
||||
w.inactive.weak_bg_fill = TRACK;
|
||||
w.inactive.bg_stroke = Stroke::new(1.0, GRID);
|
||||
w.inactive.fg_stroke = Stroke::new(1.0, TEXT_BRIGHT);
|
||||
|
||||
w.hovered.bg_fill = accent.edge;
|
||||
w.hovered.weak_bg_fill = accent.strip;
|
||||
w.hovered.bg_stroke = Stroke::new(1.0, accent.edge);
|
||||
w.hovered.fg_stroke = Stroke::new(1.0, TEXT_HOVER);
|
||||
|
||||
w.active.bg_fill = accent.handle;
|
||||
w.active.weak_bg_fill = accent.edge;
|
||||
w.active.bg_stroke = Stroke::new(1.0, accent.handle);
|
||||
w.active.fg_stroke = Stroke::new(1.0, TEXT_HOVER);
|
||||
|
||||
ctx.set_visuals(v);
|
||||
}
|
||||
+9
-8
@@ -12,15 +12,16 @@ use nih_plug_egui::egui::{self, pos2, vec2, Align2, Color32, CornerRadius, FontI
|
||||
|
||||
use super::METER_FLOOR_DB;
|
||||
use crate::meters::{Meters, BUCKET_HZ, NUM_CHANNELS};
|
||||
use super::palette;
|
||||
|
||||
/// Height of the plot panel.
|
||||
const PLOT_PANEL_H: f32 = 150.0;
|
||||
/// Number of columns held in the history ring.
|
||||
const PLOT_N: usize = 256;
|
||||
|
||||
const COLOR_IN: Color32 = Color32::from_rgb(90, 170, 235);
|
||||
const COLOR_OUT: Color32 = Color32::from_rgb(90, 200, 110);
|
||||
const COLOR_GR: Color32 = Color32::from_rgb(240, 150, 60);
|
||||
const COLOR_IN: Color32 = palette::SIGNAL_IN;
|
||||
const COLOR_OUT: Color32 = palette::SIGNAL_OUT;
|
||||
const COLOR_GR: Color32 = palette::SIGNAL_GR;
|
||||
|
||||
/// Rolling history for all channels: a per-channel ring of (in_db, out_db, gr_db) columns.
|
||||
struct PlotHistory {
|
||||
@@ -156,7 +157,7 @@ pub(super) fn draw(ui: &mut egui::Ui, meters: &Meters, state: &mut PlotState) {
|
||||
let (rect, _) =
|
||||
ui.allocate_exact_size(vec2(ui.available_width(), PLOT_PANEL_H), Sense::hover());
|
||||
let p = ui.painter_at(rect);
|
||||
p.rect_filled(rect, CornerRadius::ZERO, Color32::from_rgb(16, 16, 20));
|
||||
p.rect_filled(rect, CornerRadius::ZERO, palette::SURFACE);
|
||||
|
||||
let (top, bottom, left, right) =
|
||||
(rect.top() + 4.0, rect.bottom() - 4.0, rect.left() + 4.0, rect.right() - 4.0);
|
||||
@@ -169,13 +170,13 @@ pub(super) fn draw(ui: &mut egui::Ui, meters: &Meters, state: &mut PlotState) {
|
||||
// Gridlines (dB).
|
||||
for &g in &[0.0f32, -12.0, -24.0, -48.0] {
|
||||
let y = y_for_db(g);
|
||||
p.line_segment([pos2(left, y), pos2(right, y)], Stroke::new(1.0, Color32::from_gray(40)));
|
||||
p.line_segment([pos2(left, y), pos2(right, y)], Stroke::new(1.0, palette::GRID_FAINT));
|
||||
p.text(
|
||||
pos2(left + 2.0, y),
|
||||
Align2::LEFT_BOTTOM,
|
||||
format!("{g:.0}"),
|
||||
FontId::proportional(9.0),
|
||||
Color32::from_gray(90),
|
||||
palette::TEXT_DIM,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -193,7 +194,7 @@ pub(super) fn draw(ui: &mut egui::Ui, meters: &Meters, state: &mut PlotState) {
|
||||
// Fill as a strip of per-segment convex quads down to the baseline. A single
|
||||
// concave polygon mis-tessellates in egui (it fans from one corner, leaving stray
|
||||
// triangles), so build convex pieces — one box per time unit — instead.
|
||||
let fill_col = Color32::from_rgba_unmultiplied(color.r(), color.g(), color.b(), 40);
|
||||
let fill_col = palette::with_alpha(color, 40);
|
||||
for seg in pts.windows(2) {
|
||||
let (a, b) = (seg[0], seg[1]);
|
||||
p.add(egui::Shape::convex_polygon(
|
||||
@@ -214,7 +215,7 @@ pub(super) fn draw(ui: &mut egui::Ui, meters: &Meters, state: &mut PlotState) {
|
||||
// hit the ceiling (global — shown on every channel's view). One column wide, so runs of
|
||||
// hits merge into a continuous segment and a lone hit is just a dot. Nothing otherwise.
|
||||
let dx = width / (PLOT_N - 1) as f32;
|
||||
let marker = Color32::from_rgb(235, 45, 45);
|
||||
let marker = palette::CLIP;
|
||||
for k in 0..len {
|
||||
let idx = (write + PLOT_N - len + k) % PLOT_N;
|
||||
if state.history.hit[idx] {
|
||||
|
||||
Reference in New Issue
Block a user