diff --git a/Source/PluginEditor.cpp b/Source/PluginEditor.cpp index 2095e26..9b230a7 100644 --- a/Source/PluginEditor.cpp +++ b/Source/PluginEditor.cpp @@ -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::halfPi); + down = new DirWedge ("DOWN", juce::MathConstants::pi); + left = new DirWedge ("LEFT", -juce::MathConstants::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 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 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& 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 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::pi; diff -= juce::MathConstants::twoPi) {} + for (; diff < -juce::MathConstants::pi; diff += juce::MathConstants::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); } diff --git a/Source/PluginEditor.h b/Source/PluginEditor.h index 532ec3e..793023d 100644 --- a/Source/PluginEditor.h +++ b/Source/PluginEditor.h @@ -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; diff --git a/b3.png b/b3.png index f7b8718..7edff3b 100644 Binary files a/b3.png and b/b3.png differ