Add 250% scale option, default 150%, widen base layout; polish trail scaling

This commit is contained in:
Armin 2026-09-16 21:16:37 +02:00
commit e4af0eb793
7 changed files with 123 additions and 67 deletions

View file

@ -39,42 +39,56 @@ void VectorScope::paint(juce::Graphics& g)
const float dotSize = processor.apvts.getRawParameterValue("dotSize")->load();
const float frozen = processor.apvts.getRawParameterValue("freeze")->load();
// Rebuild the trail from live points each frame (only if not frozen).
// The trail is fully cleared first, then every point with age < fadeTime
// is redrawn with its age-based alpha, so dots past fadeTime disappear
// completely instead of leaving an asymptotic dark-grey residue.
if (trail.isValid() && frozen < 0.5f)
// Persistent glow trail: each frame the image itself is faded by a
// decay factor (so it keeps dying off even after audio stops), then the
// dots captured since the last frame are stamped in at full brightness.
// Older history is retained purely in the image, so lowering the level
// never erases the trail prematurely.
if (trail.isValid())
{
juce::Graphics trailG(trail);
trailG.setColour(juce::Colours::black);
trailG.fillRect(trail.getBounds().toFloat());
const auto& points = processor.getPoints();
const float fadeTime = processor.apvts.getRawParameterValue("fadeTime")->load();
for (const auto& pt : points)
if (frozen < 0.5f)
{
if (pt.age > fadeTime)
continue;
const float fadeTime = processor.apvts.getRawParameterValue("fadeTime")->load();
const float afterglow = processor.apvts.getRawParameterValue("afterglow")->load();
const float totalLife = fadeTime + afterglow;
const float fps = 30.0f;
float normAge = pt.age / fadeTime;
float alpha = 1.0f - normAge;
alpha = alpha * alpha;
if (totalLife > 0.0f)
{
// Per-frame alpha multiplier: the trail falls to ~0.1% of its
// brightness over fadeTime + afterglow seconds.
const float decay = std::exp(-6.9f / (totalLife * fps));
trail.multiplyAllAlphas(decay);
}
float x = cx + pt.side * radius * 2.0f;
float y = cy - pt.mid * radius * 2.0f;
// Stamp only points captured within roughly the last frame and a
// half; everything older lives on in the decaying image above.
const float newAge = 1.5f / (fps * fadeTime);
const float newAlpha = 0.9f;
const auto& points = processor.getPoints();
float phase = std::abs(pt.side) / (std::abs(pt.mid) + 0.0001f);
phase = juce::jmin(phase, 1.0f);
juce::Graphics trailG(trail);
juce::Colour color = juce::Colour::fromFloatRGBA(
0.2f + phase * 0.8f,
1.0f - phase * 0.4f,
1.0f - phase * 0.6f,
alpha * 0.9f);
for (const auto& pt : points)
{
if (pt.age > newAge)
continue;
trailG.setColour(color);
trailG.fillRect(x - dotSize * 0.5f, y - dotSize * 0.5f, dotSize, dotSize);
float x = cx + pt.side * radius * 2.0f;
float y = cy - pt.mid * radius * 2.0f;
float phase = std::abs(pt.side) / (std::abs(pt.mid) + 0.0001f);
phase = juce::jmin(phase, 1.0f);
juce::Colour color = juce::Colour::fromFloatRGBA(
0.2f + phase * 0.8f,
1.0f - phase * 0.4f,
1.0f - phase * 0.6f,
newAlpha);
trailG.setColour(color);
trailG.fillRect(x - dotSize * 0.5f, y - dotSize * 0.5f, dotSize, dotSize);
}
}
}
@ -96,9 +110,10 @@ void VectorScope::paint(juce::Graphics& g)
// dB label at top-right of each circle
g.setColour(juce::Colour(0xff444455));
g.setFont(9.0f);
g.setFont(juce::Font(juce::FontOptions(9.0f * currentScale)));
juce::String dbLabel = juce::String(dBValues[i], 0) + "dB";
g.drawText(dbLabel, cx + r + 3, cy - r - 2, 40, 14, juce::Justification::centredLeft);
g.drawText(dbLabel, cx + r + 3.0f * currentScale, cy - r - 2.0f * currentScale,
40.0f * currentScale, 14.0f * currentScale, juce::Justification::centredLeft);
g.setColour(juce::Colour(0xff1a1a25));
}
@ -126,7 +141,7 @@ void VectorScope::drawHairCross(juce::Graphics& g, float cx, float cy)
auto bounds = getLocalBounds().toFloat();
// Bright center cross — two short, bright strokes
g.setColour(juce::Colour(0xff666680));
g.setColour(juce::Colour(0xff9999b3));
// Horizontal
g.drawLine(bounds.getX() + 8.0f, cy, bounds.getRight() - 8.0f, cy, 0.5f);
@ -140,50 +155,59 @@ void VectorScope::drawHairCross(juce::Graphics& g, float cx, float cy)
void VectorScope::drawLegends(juce::Graphics& g, float cx, float cy, float radius)
{
const float s = currentScale;
g.setColour(juce::Colour(0xff666677));
g.setFont(10.0f);
g.setFont(juce::Font(juce::FontOptions(10.0f * s)));
// Side label (X axis, right side)
g.drawText("Side", cx + radius + 4.0f, cy + 4.0f, 40, 14, juce::Justification::centredLeft);
g.drawText("Side", cx + radius + 4.0f * s, cy + 4.0f * s, 40.0f * s, 14.0f * s,
juce::Justification::centredLeft);
// Mid label (Y axis, top)
g.drawText("Mid", cx + 4.0f, cy - radius - 16.0f, 40, 14, juce::Justification::centredLeft);
g.drawText("Mid", cx + 4.0f * s, cy - radius - 16.0f * s, 40.0f * s, 14.0f * s,
juce::Justification::centredLeft);
// Diagonal labels: in M/S space these are the pure-channel loci
g.setColour(juce::Colour(0xff444455));
g.setFont(8.0f);
g.setFont(juce::Font(juce::FontOptions(8.0f * s)));
// L = 0 (right channel only): line y = x through centre
float diagX = cx + radius * 0.71f;
g.drawText("R only", diagX + 4.0f, cy + radius * 0.71f, 30, 12, juce::Justification::centredLeft);
g.drawText("R only", diagX + 4.0f * s, cy + radius * 0.71f, 30.0f * s, 12.0f * s,
juce::Justification::centredLeft);
// R = 0 (left channel only): line y = -x through centre
g.drawText("L only", diagX + 4.0f, cy - radius * 0.71f - 12.0f, 30, 12, juce::Justification::centredLeft);
g.drawText("L only", diagX + 4.0f * s, cy - radius * 0.71f - 12.0f * s, 30.0f * s, 12.0f * s,
juce::Justification::centredLeft);
}
void VectorScope::drawMetricsOverlay(juce::Graphics& g, const juce::Rectangle<int>& bounds)
{
const auto& metrics = processor.getMetrics();
const int margin = 10;
const int lineHeight = 14;
const float s = currentScale;
const int margin = static_cast<int>(10.0f * s);
const int lineHeight = static_cast<int>(14.0f * s);
const int startY = bounds.getBottom() - margin - (7 * lineHeight);
// Background panel
juce::Rectangle<int> panelRect(bounds.getX() + margin, startY - 5, 120, 7 * lineHeight + 10);
juce::Rectangle<int> panelRect(bounds.getX() + margin, startY - static_cast<int>(5.0f * s),
static_cast<int>(120.0f * s), 7 * lineHeight + static_cast<int>(10.0f * s));
g.setColour(juce::Colour(0xff0a0a0f).withAlpha(0.8f));
g.fillRect(panelRect);
g.setColour(juce::Colour(0xff333340));
g.drawRect(panelRect, 1);
// Metrics text
g.setFont(9.0f);
g.setFont(juce::Font(juce::FontOptions(9.0f * s)));
g.setColour(juce::Colour(0xff888899));
auto drawMetric = [&](const juce::String& label, float value, const juce::String& unit, int y)
{
g.drawText(label, panelRect.getX() + 5, y, 50, lineHeight, juce::Justification::centredLeft);
g.drawText(label, panelRect.getX() + static_cast<int>(5.0f * s), y,
static_cast<int>(50.0f * s), lineHeight, juce::Justification::centredLeft);
g.setColour(juce::Colour(0xffccccdd));
g.drawText(juce::String(value, 1) + unit, panelRect.getX() + 55, y, 60, lineHeight, juce::Justification::centredRight);
g.drawText(juce::String(value, 1) + unit, panelRect.getX() + static_cast<int>(55.0f * s), y,
static_cast<int>(60.0f * s), lineHeight, juce::Justification::centredRight);
g.setColour(juce::Colour(0xff888899));
};