mirror of
https://codeberg.org/armin/sm-26.git
synced 2026-10-11 17:01:53 +02:00
287 lines
8.7 KiB
C++
287 lines
8.7 KiB
C++
|
|
#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);
|
||
|
|
}
|
||
|
|
}
|