mirror of
https://codeberg.org/armin/sm-26.git
synced 2026-10-11 17:01:53 +02:00
Replace nav buttons with a virtual joystick pad
This commit is contained in:
parent
7fd884141b
commit
c7ebd3ed4d
6 changed files with 418 additions and 76 deletions
287
Source/JoystickPad.cpp
Normal file
287
Source/JoystickPad.cpp
Normal file
|
|
@ -0,0 +1,287 @@
|
|||
#include "JoystickPad.h"
|
||||
|
||||
#include <cmath>
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr float kPi = juce::MathConstants<float>::pi;
|
||||
|
||||
float cardinalAngle (int direction)
|
||||
{
|
||||
switch (direction)
|
||||
{
|
||||
case JoystickPad::Right: return 0.0f;
|
||||
case JoystickPad::Down: return kPi * 0.5f;
|
||||
case JoystickPad::Left: return kPi;
|
||||
case JoystickPad::Up: return -kPi * 0.5f;
|
||||
default: return 0.0f;
|
||||
}
|
||||
}
|
||||
|
||||
juce::Path withCentre (juce::Path p, float cx, float cy)
|
||||
{
|
||||
auto bounds = p.getBounds();
|
||||
p.applyTransform (juce::AffineTransform::translation (cx - bounds.getCentreX(),
|
||||
cy - bounds.getCentreY()));
|
||||
return p;
|
||||
}
|
||||
|
||||
// Triangle pointing "up" (in screen space) centred on the origin.
|
||||
juce::Path arrowUp (float s)
|
||||
{
|
||||
juce::Path p;
|
||||
p.addTriangle (0.0f, -s * 0.90f,
|
||||
-s * 0.55f, s * 0.55f,
|
||||
s * 0.55f, s * 0.55f);
|
||||
return p;
|
||||
}
|
||||
|
||||
void drawArrow (juce::Graphics& g, float cx, float cy, int direction,
|
||||
float r, juce::Colour col)
|
||||
{
|
||||
auto p = arrowUp (r);
|
||||
float angle = cardinalAngle (direction);
|
||||
p.applyTransform (juce::AffineTransform::rotation (angle + kPi * 0.5f, 0.0f, 0.0f));
|
||||
p = withCentre (p, cx, cy);
|
||||
g.setColour (col);
|
||||
g.fillPath (p);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
JoystickPad::JoystickPad()
|
||||
{
|
||||
setOpaque (true);
|
||||
}
|
||||
|
||||
void JoystickPad::setRepeatSpeed (int initialDelayMs_, int repeatMs_)
|
||||
{
|
||||
initialDelayMs = initialDelayMs_;
|
||||
repeatMs = repeatMs_;
|
||||
}
|
||||
|
||||
int JoystickPad::directionFrom (const juce::MouseEvent& e) const
|
||||
{
|
||||
const float w = (float) getWidth();
|
||||
const float h = (float) getHeight();
|
||||
const float cx = w * 0.5f;
|
||||
const float cy = h * 0.5f;
|
||||
const float wr = juce::jmin (w, h) * 0.5f * 0.78f;
|
||||
|
||||
const float dx = e.position.x - cx;
|
||||
const float dy = e.position.y - cy;
|
||||
const float rad = std::hypot (dy, dx);
|
||||
if (rad < wr * 0.12f)
|
||||
return None;
|
||||
|
||||
const float a = std::atan2 (dy, dx); // -pi..pi, y grows downwards
|
||||
const float oct = kPi * 0.25f;
|
||||
if (a >= -oct && a < oct) return Right;
|
||||
if (a >= oct && a < 3.0f * oct) return Down;
|
||||
if (a >= 3.0f * oct || a < -3.0f * oct) return Left;
|
||||
return Up;
|
||||
}
|
||||
|
||||
void JoystickPad::beginHold (int direction)
|
||||
{
|
||||
active = (Direction) direction;
|
||||
holdStartMs = juce::Time::getMillisecondCounter();
|
||||
lastFireMs = holdStartMs;
|
||||
repeatsStarted = false;
|
||||
|
||||
if (onAction)
|
||||
onAction (direction);
|
||||
|
||||
startTimer (16);
|
||||
repaint();
|
||||
}
|
||||
|
||||
void JoystickPad::endHold()
|
||||
{
|
||||
active = None;
|
||||
stopTimer();
|
||||
repaint();
|
||||
}
|
||||
|
||||
void JoystickPad::mouseDown (const juce::MouseEvent& e)
|
||||
{
|
||||
const int d = directionFrom (e);
|
||||
if (d != None)
|
||||
beginHold (d);
|
||||
else
|
||||
endHold();
|
||||
}
|
||||
|
||||
void JoystickPad::mouseDrag (const juce::MouseEvent& e)
|
||||
{
|
||||
const int d = directionFrom (e);
|
||||
if (d != None && d != (int) active)
|
||||
beginHold (d);
|
||||
else if (d == None && active != None)
|
||||
endHold();
|
||||
}
|
||||
|
||||
void JoystickPad::mouseUp (const juce::MouseEvent&)
|
||||
{
|
||||
endHold();
|
||||
}
|
||||
|
||||
void JoystickPad::timerCallback()
|
||||
{
|
||||
if (active == None)
|
||||
{
|
||||
stopTimer();
|
||||
return;
|
||||
}
|
||||
|
||||
const int64 now = juce::Time::getMillisecondCounter();
|
||||
if (! repeatsStarted)
|
||||
{
|
||||
if (now - lastFireMs >= initialDelayMs)
|
||||
{
|
||||
repeatsStarted = true;
|
||||
lastFireMs = now;
|
||||
if (onAction)
|
||||
onAction ((int) active);
|
||||
}
|
||||
}
|
||||
else if (now - lastFireMs >= repeatMs)
|
||||
{
|
||||
lastFireMs = now;
|
||||
if (onAction)
|
||||
onAction ((int) active);
|
||||
}
|
||||
}
|
||||
|
||||
void JoystickPad::paint (juce::Graphics& g)
|
||||
{
|
||||
const float w = (float) getWidth();
|
||||
const float h = (float) getHeight();
|
||||
const float cx = w * 0.5f;
|
||||
const float cy = h * 0.5f;
|
||||
|
||||
const float bezelR = juce::jmin (w, h) * 0.5f - 1.0f;
|
||||
const float wellR = bezelR * 0.78f;
|
||||
|
||||
// Clear the full component first (opaque component: every pixel must be
|
||||
// repainted or stale content tears/flickers during redraws).
|
||||
g.fillAll (juce::Colour (0xFF000000));
|
||||
|
||||
// Ambient under-shadow (soft, offset downward).
|
||||
{
|
||||
float alpha = 0.5f;
|
||||
for (int i = 3; i >= 1; --i)
|
||||
{
|
||||
g.setColour (juce::Colour::fromFloatRGBA (0.0f, 0.0f, 0.0f, alpha / (float) i));
|
||||
g.fillEllipse (cx - bezelR - (float) i, cy - bezelR - (float) i + 3.0f,
|
||||
2.0f * (bezelR + (float) i), 2.0f * (bezelR + (float) i));
|
||||
}
|
||||
}
|
||||
|
||||
// Outer bezel: soft radial metal.
|
||||
{
|
||||
juce::ColourGradient bezel (juce::Colour (0xFF2E2E2E), cx - bezelR * 0.35f, cy - bezelR * 0.35f,
|
||||
juce::Colour (0xFF0A0A0A), cx + bezelR * 0.45f, cy + bezelR * 0.45f, true);
|
||||
g.setGradientFill (bezel);
|
||||
g.fillEllipse (cx - bezelR, cy - bezelR, 2.0f * bezelR, 2.0f * bezelR);
|
||||
}
|
||||
|
||||
// Bezel rim shading for a 3D ring (dark only - no bright top-light arc).
|
||||
{
|
||||
auto ring = [&](float r0, float r1)
|
||||
{
|
||||
juce::Path p;
|
||||
p.addEllipse (cx - r1, cy - r1, r1 * 2.0f, r1 * 2.0f);
|
||||
p.addEllipse (cx - r0, cy - r0, r0 * 2.0f, r0 * 2.0f);
|
||||
p.setUsingNonZeroWinding (false);
|
||||
return p;
|
||||
};
|
||||
g.setColour (juce::Colour (0xFF050505));
|
||||
g.fillPath (ring (bezelR * 0.93f, bezelR));
|
||||
}
|
||||
|
||||
// Recessed well (flat).
|
||||
{
|
||||
g.setColour (juce::Colour (0xFF0A0A0A));
|
||||
g.fillEllipse (cx - wellR, cy - wellR, 2.0f * wellR, 2.0f * wellR);
|
||||
}
|
||||
|
||||
auto wedgePath = [&](int direction) -> juce::Path
|
||||
{
|
||||
// JUCE measures pie/arc angles clockwise from 12 o'clock, so add
|
||||
// 90 degrees to our visual heading (0 = right, +90 = down).
|
||||
const float a = cardinalAngle (direction) + kPi * 0.5f;
|
||||
const float start = a - kPi * 0.25f;
|
||||
const float end = a + kPi * 0.25f;
|
||||
juce::Path p;
|
||||
p.addPieSegment (cx - wellR, cy - wellR, wellR * 2.0f, wellR * 2.0f,
|
||||
start, end, 0.0f);
|
||||
return p;
|
||||
};
|
||||
|
||||
// Quarter-button wedges (flat).
|
||||
for (int d = Up; d <= Right; ++d)
|
||||
{
|
||||
auto p = wedgePath (d);
|
||||
g.setColour (juce::Colour (0xFF151515));
|
||||
g.fillPath (p);
|
||||
g.setColour (juce::Colour (0xFF242424));
|
||||
g.strokePath (p, juce::PathStrokeType (1.0f));
|
||||
}
|
||||
|
||||
// Flat seams between the quarters (a single dark channel).
|
||||
for (int i = 0; i < 4; ++i)
|
||||
{
|
||||
const float a = juce::degreesToRadians (45.0f + 90.0f * (float) i);
|
||||
const float dx = std::cos (a);
|
||||
const float dy = std::sin (a);
|
||||
g.setColour (juce::Colour (0xFF050505));
|
||||
g.drawLine (cx + dx * 2.0f, cy + dy * 2.0f, cx + dx * (wellR - 2.0f), cy + dy * (wellR - 2.0f), 2.0f);
|
||||
}
|
||||
|
||||
// Resting glyphs on the four quarters.
|
||||
for (int d = Up; d <= Right; ++d)
|
||||
if (d != (int) active)
|
||||
drawArrow (g, cx + std::cos (cardinalAngle (d)) * wellR * 0.55f,
|
||||
cy + std::sin (cardinalAngle (d)) * wellR * 0.55f,
|
||||
d, wellR * 0.16f, juce::Colour (0xFF454545));
|
||||
|
||||
// Lit quarter (active direction) - flat bright fill, black glyph.
|
||||
if (active != None)
|
||||
{
|
||||
auto p = wedgePath ((int) active);
|
||||
g.setColour (juce::Colour (0xFFECECEC));
|
||||
g.fillPath (p);
|
||||
drawArrow (g, cx + std::cos (cardinalAngle ((int) active)) * wellR * 0.55f,
|
||||
cy + std::sin (cardinalAngle ((int) active)) * wellR * 0.55f,
|
||||
(int) active, wellR * 0.16f, juce::Colour (0xFF0A0A0A));
|
||||
}
|
||||
|
||||
// Center cap that ties the four quarters together (static, no stick).
|
||||
{
|
||||
const float sr = wellR * 0.30f;
|
||||
juce::ColourGradient cap (juce::Colour (0xFF181818), cx, cy - sr,
|
||||
juce::Colour (0xFF080808), cx, cy + sr, false);
|
||||
g.setGradientFill (cap);
|
||||
g.fillEllipse (cx - sr, cy - sr, sr * 2.0f, sr * 2.0f);
|
||||
g.setColour (juce::Colour (0xFF060606));
|
||||
g.drawEllipse (cx - sr, cy - sr, sr * 2.0f, sr * 2.0f, 1.0f);
|
||||
g.setColour (juce::Colour (0xFF2A2A2A));
|
||||
g.drawEllipse (cx - sr, cy - sr, sr * 2.0f, sr * 2.0f, 0.5f);
|
||||
g.setColour (juce::Colour (0xFF3A3A3A));
|
||||
g.drawEllipse (cx + sr * 0.30f, cy + sr * 0.20f, sr * 0.28f, sr * 0.28f, 2.0f);
|
||||
}
|
||||
|
||||
// Mechanical screws around the bezel.
|
||||
for (int i = 0; i < 4; ++i)
|
||||
{
|
||||
const float a = juce::degreesToRadians (45.0f + 90.0f * (float) i);
|
||||
const float srx = cx + std::cos (a) * bezelR * 0.92f;
|
||||
const float sry = cy + std::sin (a) * bezelR * 0.92f;
|
||||
const float sr0 = 2.0f;
|
||||
g.setColour (juce::Colour (0xFF222222));
|
||||
g.fillEllipse (srx - sr0, sry - sr0, sr0 * 2.0f, sr0 * 2.0f);
|
||||
g.setColour (juce::Colour (0xFF080808));
|
||||
g.fillEllipse (srx - sr0 * 0.4f, sry - sr0 * 0.4f, sr0 * 0.8f, sr0 * 0.8f);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue