I regularly use a realistic, textural style for my UIs.
I create the background assets in something like Sketch, Figma or even Blender, and a lot times, I bake my knobs into the background and all I really need to draw at runtime is the dial pointer. Thing is, to achieve a cohesive, even if simple, look, it can be tricky to get the lighting right.
So, being as lazy as I am, I reeeeally suffer whenever I need to get into rendered png strips.
Here’s a very simple but effective solution for my case that I want to share with you all.
Hope it comes in handy to someone!
/**
* Defines the parameters for drawing a dial pointer with `drawShadedDialPointer`
*
* @param fillColour The colour used to fill the pointer
* @param pointerWidth The width of the pointer
* @param distanceFromCenter The distance from the center of `dialBounds` to draw the pointer
* @param strokeThickness The thickness of the pointer's outline stroke
* @param shadingOpacity The opacity of the stroke (essentially affects the lighting effect strength)
* @param lightSourceAngle The angle of the light source (in radians, 0.0 = 12 o'clock)
* @param shape The perceptual shape of the surface of the pointer relative to its sorroundings
*
*/
struct DialPointerStyle {
const juce::Colour& fillColour;
int pointerWidth;
int distanceFromCenter;
int strokeThickness;
float shadingOpacity;
float lightSourceAngle;
enum { Recessed, Raised } shape;
};
/**
* Draws a stroked dial pointer with an approximated (cheap) raised/recessed lighting effect.
*
* The function draws a rounded rectangle pointer that rotates around the center of the dial bounds,
* with a gradient-filled stroke that simulates raised/recessed lighting based on the pointer's angle relative to a light source.
* The drawn pointer extends from `distanceFromCenter` to the edges of `dialBounds`.
*
* @param g The Graphics context to draw into
* @param bounds The bounding rectangle of the dial
* @param angle The current rotation angle of the pointer (in radians, 0.0 = 12 o'clock)
* @param style @see DialPointerStyle
*
*/
inline void drawShadedDialPointer(juce::Graphics& g, const juce::Rectangle<int>& bounds, float angle, const DialPointerStyle& style)
{
float halfPointerWidth = (float)style.pointerWidth * 0.5f;
int pointerHeight = (bounds.getHeight() / 2) - style.distanceFromCenter;
jassert(pointerHeight > 0);
auto pointerBounds = bounds.withSize(style.pointerWidth, pointerHeight).toFloat().withX(((float)bounds.getWidth() * 0.5f) - halfPointerWidth);
float brightness = abs(fmod(angle - M_PI_2 - style.lightSourceAngle, M_PI*2.0) - M_PI) / M_PI;
if (style.shape == DialPointerStyle::Raised) brightness = 1.0f - brightness;
auto rightColour = juce::Colour::greyLevel(brightness).withAlpha(style.shadingOpacity);
auto leftColour = juce::Colour::greyLevel(1.0f - brightness).withAlpha(style.shadingOpacity);
g.addTransform(juce::AffineTransform::rotation(angle, bounds.getCentreX(), bounds.getCentreY()));
g.setColour(style.fillColour);
g.fillRoundedRectangle(pointerBounds, halfPointerWidth);
g.setGradientFill(juce::ColourGradient::horizontal(leftColour, pointerBounds.getX(), rightColour, pointerBounds.getRight()));
g.drawRoundedRectangle(pointerBounds, halfPointerWidth, style.strokeThickness);
}
