Shaded Dial Pointer

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);
}