new bg design, add navi joystick

This commit is contained in:
Armin 2026-10-02 18:33:31 +02:00
commit 48ac58555f
3 changed files with 203 additions and 80 deletions

View file

@ -12,24 +12,26 @@ RackEditor::RackEditor (RMX19AudioProcessor& p)
backgroundImage = juce::ImageFileFormat::loadFrom (BinaryData::b3_png,
BinaryData::b3_pngSize);
up = new NavButton (-1, "^", "UP");
down = new NavButton (1, "v", "DOWN");
left = new NavButton (-1, "<", "LEFT");
right = new NavButton (1, ">", "RIGHT");
enterB = new OkButton ("ENTER", true);
backB = new OkButton ("BACK", false);
// circular joystick ring: angles run clockwise from 12 o'clock
up = new DirWedge ("UP", 0.0);
right = new DirWedge ("RIGHT", juce::MathConstants<double>::halfPi);
down = new DirWedge ("DOWN", juce::MathConstants<double>::pi);
left = new DirWedge ("LEFT", -juce::MathConstants<double>::halfPi);
okB = new CentreOkButton ("OK");
escB = new EscButton ("ESC");
for (auto* b : { (juce::Button*) up, (juce::Button*) down,
(juce::Button*) left, (juce::Button*) right,
(juce::Button*) enterB, (juce::Button*) backB })
(juce::Button*) okB, (juce::Button*) escB })
addAndMakeVisible (b);
up->onClick = [this] { display.navUp(); };
down->onClick = [this] { display.navDown(); };
left->onClick = [this] { display.navLeft(); };
right->onClick = [this] { display.navRight(); };
enterB->onClick = [this] { display.navEnter(); };
backB->onClick = [this] { display.navExit(); };
okB->onClick = [this] { display.navEnter(); };
escB->onClick = [this] { display.navExit(); };
addAndMakeVisible (display);
@ -158,20 +160,34 @@ void RackEditor::resized()
display.setTransform (tf);
display.setBounds (lcdArea);
// nav diamond + enter/back to the right of the LCD
// circular joystick ring + separate ESC key, to the right of the LCD
const juce::Rectangle<int> navArea (lcdArea.getRight() + 16, 26, baseW - lcdArea.getRight() - 30, 164);
const int bw = 58, bh = 20;
const int cx = navArea.getX() + (navArea.getWidth() - bw) / 2;
const int rowY = navArea.getY();
constexpr float ro = DirWedge::outerR;
const float ringCx = (float) navArea.getCentreX();
const float ringCy = (float) lcdArea.getCentreY(); // ring sits centred on the display
up->setTransform (tf); up->setBounds (cx + 6, rowY + 1, bw + 3, bh);
left->setTransform (tf); left->setBounds (navArea.getX() + 1, rowY + bh + 6, bw + 2, bh);
down->setTransform (tf); down->setBounds (cx + 6, rowY + bh + 6 + 1, bw + 3, bh);
right->setTransform (tf); right->setBounds (navArea.getRight() - bw + 3, rowY + bh + 6, bw - 3, bh);
// all four wedges share the ring's bounding box; each paints only its own sector
const juce::Rectangle<int> ringBox ((int) (ringCx - ro), (int) (ringCy - ro),
(int) (ro * 2.0f), (int) (ro * 2.0f));
enterB->setTransform (tf); enterB->setBounds (navArea.getX() + 2, rowY + 2 * (bh + 6) + 4, bw + 2, bh);
backB->setTransform (tf); backB->setBounds (navArea.getRight() - bw + 3, rowY + 2 * (bh + 6) + 4, bw - 3, bh);
up->setTransform (tf); up->setBounds (ringBox);
right->setTransform (tf); right->setBounds (ringBox);
down->setTransform (tf); down->setBounds (ringBox);
left->setTransform (tf); left->setBounds (ringBox);
// OK, dead centre of the ring
okB->setTransform (tf);
okB->setBounds ((int) (ringCx - CentreOkButton::radius), (int) (ringCy - CentreOkButton::radius),
(int) (CentreOkButton::radius * 2.0f), (int) (CentreOkButton::radius * 2.0f));
// ESC, a round key centred on the point the old rectangular key occupied
constexpr float escD = EscButton::diameter;
const float escCx = (float) navArea.getX() + 54.0f;
const float escCy = (float) ringBox.getBottom() - 10.0f;
escB->setTransform (tf);
escB->setBounds ((int) (escCx - escD * 0.5f), (int) (escCy - escD * 0.5f),
(int) escD, (int) escD);
}
void RackEditor::setScale (float s)
@ -201,58 +217,138 @@ bool RackEditor::keyPressed (const juce::KeyPress& k)
// ---- buttons -------------------------------------------------------------
void RackEditor::NavButton::paintButton (juce::Graphics& g, bool, bool down)
// shared palette for the black joystick keys
static const juce::Colour keyFace (0xff080808); // black key body
static const juce::Colour keyDown (0xff1c1c1c); // pressed
static const juce::Colour keyRim (0xff4a4741); // keeps black keys legible on dark metal
static const juce::Colour keyRimDn (0xff8a8a86);
static const juce::Colour legendIdle (0xfff2f2f2); // white arrow legends
static const juce::Colour legendHot (0xffffffff);
// solid triangular arrow centred on `centre`, pointing along `angleRad`
// (radians clockwise from 12 o'clock, screen y pointing down)
static void drawArrow (juce::Graphics& g, const juce::Point<float>& centre,
double angleRad, float size)
{
const auto b = getLocalBounds();
const juce::Colour base = juce::Colour (0xff221d18);
const juce::Colour edge = juce::Colour (0xff6b5f4e);
const juce::Colour text = juce::Colour (0xfffff3d8);
const float dx = (float) std::sin (angleRad);
const float dy = (float) -std::cos (angleRad);
const float px = -dy, py = dx; // perpendicular, for the two rear corners
if (down)
{
g.setColour (edge.darker (0.4f));
g.fillRect (b);
g.setColour (base.brighter (0.1f));
g.fillRect (b.reduced (2, 2));
}
else
{
g.setColour (base);
g.fillRect (b);
g.setColour (edge);
g.drawRect (b, 1);
}
const float back = size * 0.80f;
const float wide = size * 0.85f;
// glyph
g.setColour (text);
g.setFont (juce::FontOptions ("Arial", 12.0f, juce::Font::bold));
g.drawFittedText (mark, b.withTrimmedBottom (9), juce::Justification::centred, 1);
g.setFont (juce::FontOptions ("Arial", 7.0f, juce::Font::plain));
g.drawFittedText (getName(), b.withTrimmedTop (b.getHeight() - 8), juce::Justification::centred, 1);
juce::Path t;
t.startNewSubPath (centre.x + dx * size, centre.y + dy * size);
t.lineTo (centre.x - dx * back + px * wide, centre.y - dy * back + py * wide);
t.lineTo (centre.x - dx * back - px * wide, centre.y - dy * back - py * wide);
t.closeSubPath();
g.fillPath (t);
}
void RackEditor::OkButton::paintButton (juce::Graphics& g, bool over, bool down)
void RackEditor::DirWedge::paintButton (juce::Graphics& g, bool over, bool down)
{
const auto b = getLocalBounds();
const juce::Colour base = enter ? juce::Colour (0xff263a28) : juce::Colour (0xff332a20);
const juce::Colour edge = juce::Colour (0xff8a9b6a);
const juce::Point<float> c (outerR, outerR);
if (down)
{
g.setColour (edge.darker (0.35f));
g.fillRect (b);
g.setColour (base.brighter (0.12f));
g.fillRect (b.reduced (2, 2));
}
else
{
g.setColour (base);
g.fillRect (b);
g.setColour (edge);
g.drawRect (b, 1);
}
// a pressed key sinks slightly into the ring
const float ro = down ? outerR - 1.5f : outerR;
const float ri = down ? innerR + 1.5f : innerR;
g.setColour (over ? juce::Colour (0xffffd977) : juce::Colour (0xffd8d2c6));
g.setFont (juce::FontOptions ("Arial", 9.0f, juce::Font::bold));
g.drawFittedText (getName(), b, juce::Justification::centred, 1);
const double a0 = angle - halfSpan;
const double a1 = angle + halfSpan;
juce::Path wedge;
wedge.startNewSubPath (c.getPointOnCircumference (ro, a0));
wedge.addCentredArc (c.x, c.y, ro, ro, 0.0f, a0, a1, false);
wedge.lineTo (c.getPointOnCircumference (ri, a1));
wedge.addCentredArc (c.x, c.y, ri, ri, 0.0f, a1, a0, false);
wedge.closeSubPath();
g.setColour (down ? keyDown : keyFace);
g.fillPath (wedge);
g.setColour (down ? keyRimDn : keyRim);
g.strokePath (wedge, juce::PathStrokeType (1.0f));
// white arrow legend, centred across the wedge band and pointing outwards
g.setColour (over || down ? legendHot : legendIdle);
drawArrow (g, c.getPointOnCircumference ((innerR + outerR) * 0.5f, angle), angle, 11.0f);
}
bool RackEditor::DirWedge::hitTest (int x, int y)
{
if (! juce::Component::hitTest (x, y))
return false;
const float dx = (float) x - outerR;
const float dy = (float) y - outerR;
const float r = std::sqrt (dx * dx + dy * dy);
if (r < innerR || r > outerR)
return false;
// same convention as the painted sector: radians clockwise from 12 o'clock
double diff = std::atan2 (dx, -dy) - angle;
for (; diff > juce::MathConstants<double>::pi; diff -= juce::MathConstants<double>::twoPi) {}
for (; diff < -juce::MathConstants<double>::pi; diff += juce::MathConstants<double>::twoPi) {}
return std::abs (diff) <= halfSpan;
}
bool RackEditor::CentreOkButton::hitTest (int x, int y)
{
if (! juce::Component::hitTest (x, y))
return false;
const float dx = (float) x - radius;
const float dy = (float) y - radius;
return dx * dx + dy * dy <= radius * radius;
}
void RackEditor::CentreOkButton::paintButton (juce::Graphics& g, bool over, bool down)
{
const auto c = getLocalBounds().getCentre().toFloat();
const float r = radius - (down ? 1.0f : 0.0f);
juce::Path disc;
disc.addEllipse (c.x - r, c.y - r, r * 2.0f, r * 2.0f);
g.setColour (down ? keyDown : keyFace);
g.fillPath (disc);
g.setColour (down ? keyRimDn : keyRim);
g.strokePath (disc, juce::PathStrokeType (1.0f));
g.setColour (over || down ? legendHot : legendIdle);
g.setFont (juce::FontOptions ("Arial", 9.5f, juce::Font::bold));
g.drawText (getName(), getLocalBounds(), juce::Justification::centred, true);
}
bool RackEditor::EscButton::hitTest (int x, int y)
{
if (! juce::Component::hitTest (x, y))
return false;
const float r = diameter * 0.5f;
const float dx = (float) x - r;
const float dy = (float) y - r;
return dx * dx + dy * dy <= r * r;
}
void RackEditor::EscButton::paintButton (juce::Graphics& g, bool over, bool down)
{
const auto c = getLocalBounds().getCentre().toFloat();
const float r = diameter * 0.5f - (down ? 1.0f : 0.0f);
juce::Path disc;
disc.addEllipse (c.x - r, c.y - r, r * 2.0f, r * 2.0f);
g.setColour (down ? keyDown : keyFace);
g.fillPath (disc);
g.setColour (down ? keyRimDn : keyRim);
g.strokePath (disc, juce::PathStrokeType (1.0f));
g.setColour (over || down ? legendHot : legendIdle);
g.setFont (juce::FontOptions ("Arial", 7.0f, juce::Font::bold));
g.drawText (getName(), getLocalBounds(), juce::Justification::centred, true);
}

View file

@ -18,30 +18,57 @@ public:
private:
RMX19AudioProcessor& proc;
class NavButton : public juce::Button
// one of the four wedge-shaped keys of the circular joystick ring. every wedge
// component covers the whole ring bounding box and paints only its own sector,
// so the ring centre is always at (outerR, outerR) in local coordinates.
class DirWedge : public juce::Button
{
public:
NavButton (int d, const juce::String& glyph, const juce::String& label)
: juce::Button (label), dir (d), mark (glyph)
DirWedge (const juce::String& label, double centreAngleRad)
: juce::Button (label), angle (centreAngleRad)
{ setWantsKeyboardFocus (true); setRepeatSpeed (320, 80); }
void paintButton (juce::Graphics&, bool, bool) override;
int dir;
juce::String mark;
// all four wedges share the ring's bounding box, so the box alone would let the
// topmost one swallow every click in the ring. restrict hits to this wedge's sector.
bool hitTest (int x, int y) override;
static constexpr float outerR = 66.0f;
static constexpr float innerR = 24.0f;
// half of a quadrant, minus a gap so neighbouring wedges read as separate keys
static constexpr double halfSpan = 0.7156;
double angle; // radians clockwise from 12 o'clock
};
class OkButton : public juce::Button
// the OK key sitting in the middle of the ring
class CentreOkButton : public juce::Button
{
public:
OkButton (const juce::String& label, bool isEnter)
: juce::Button (label), enter (isEnter)
{ setWantsKeyboardFocus (true); setRepeatSpeed (320, 80); }
explicit CentreOkButton (const juce::String& label) : juce::Button (label)
{ setWantsKeyboardFocus (true); }
void paintButton (juce::Graphics&, bool, bool) override;
bool enter;
// the key paints as a disc, so hit only the disc rather than its square bounds
bool hitTest (int x, int y) override;
static constexpr float radius = 21.0f;
};
NavButton* up = nullptr; NavButton* down = nullptr;
NavButton* left = nullptr; NavButton* right = nullptr;
OkButton* enterB = nullptr; OkButton* backB = nullptr;
// the separate ESC key, outside the ring at the bottom left
class EscButton : public juce::Button
{
public:
explicit EscButton (const juce::String& label) : juce::Button (label)
{ setWantsKeyboardFocus (true); }
void paintButton (juce::Graphics&, bool, bool) override;
bool hitTest (int x, int y) override;
static constexpr float diameter = 30.0f;
};
DirWedge* up = nullptr; DirWedge* down = nullptr;
DirWedge* left = nullptr; DirWedge* right = nullptr;
CentreOkButton* okB = nullptr;
EscButton* escB = nullptr;
juce::ComboBox scaleBox;
LcdPanel display;