Gradients


#1

Is there any easy way in the juce framework to make gradients like this?

https://i.stack.imgur.com/JG0KB.png
https://i.stack.imgur.com/1tpCq.png

it’s super easy in html/css but damn near impossible via the tools in juce, I’ve found.

the html looks like this:

<svg xmlns="http://www.w3.org/2000/svg" version="1.1"
     xmlns:xlink="http://www.w3.org/1999/xlink"
     width="100%" height="100%"
     viewBox="0 0 100 100">

    <radialGradient id="radial" cx="50" cy="50" r="50"
        gradientUnits="userSpaceOnUse">
        <stop  offset="0.8" style="stop-color:#00FFFF"/>
        <stop  offset="1" style="stop-color:#004444"/>
    </radialGradient>

    <circle cx="50" cy="50" r="50"
            fill="url(#radial)"/>
</svg>

that <stop offset="0.8" style="stop-color:#00ffff"/> is similar to the ColourGradient::addColor() method. But getting a gradient to wrap around a shape is basically impossible, unless you make a gradient for every non-horizontal/vertical edge. which means shapes like this are impossible with Colour Gradients:

https://i.stack.imgur.com/F88Hd.png

I need to be able to resize shapes like this dynamically, that’s why i’m asking about doing it in code, instead of just using regular images.

code like this:

void SquareGradient::paint(Graphics& g)
{
    auto inner = getLocalBounds().withSizeKeepingCentre(3 * getWidth() / 4, 3 * getHeight() / 4);
    auto innerTopleftPoint = inner.getTopLeft();
    auto innerTopRightPoint = inner.getTopRight();
    auto innerBottomLeftPoint = inner.getBottomLeft();
    auto innerBottomRightPoint = inner.getBottomRight();
    auto obCenter = getLocalBounds().toFloat().getCentre();
    
    Point<float> topLeft{0,0};
    Point<float> topRight{static_cast<float>(getWidth()), 0};
    Point<float> bottomLeft{0,static_cast<float>(getHeight())};
    Point<float> bottomRight{static_cast<float>(getWidth()),static_cast<float>(getHeight())};
    
    g.setGradientFill({outerColor, 0, 0, centerColor, static_cast<float>(innerTopleftPoint.getX()), 0, false});
    Path path;
    path.addTriangle(topLeft, obCenter, bottomLeft);
    g.fillPath(path);

    g.setGradientFill( {centerColor, static_cast<float>(innerTopRightPoint.getX()), 0,
                        outerColor, static_cast<float>(getWidth()), 0,
                        false} );
    path.clear();
    path.addTriangle(obCenter, topRight, bottomRight);
    g.fillPath(path);

    g.setGradientFill( {outerColor, 0, 0,
                        centerColor, 0, static_cast<float>(innerTopleftPoint.getY()),
                        false } );
    path.clear();
    path.addTriangle(topLeft, topRight, obCenter);
    g.fillPath(path);
    

    g.setGradientFill( {centerColor, 0, static_cast<float>(innerBottomLeftPoint.getY()),
                        outerColor, 0, static_cast<float>(getHeight()),
                        false} );
    path.clear();
    path.addTriangle(bottomLeft, obCenter, bottomRight);
    g.fillPath(path);
}

gets me images like this:


#2

The short answer in my experience is “no”. I’ve tried getting similar effects but unfortunately it seems the ability for JUCE to automatically do any sort of non trivial abstract gradients (think gradients based on shape dilation, possibly with a non-zero origin, like the star) is not something that’s really built in.

If you must do it in code you’ll probably have to do some trickery with cross-coordinate-space math to dilate your shapes as desired to generate gradients. With symmetrical shapes you can probably run points through an AffineTransform scale operation to get them where you want, then build gradients from there - not unlike your square.

Realistically, you’ll probably want to make the shapes in some vector graphics tool like Affinity Designer/Adobe Illustrator/Sketch, export as SVG, load as a drawable and fiddle with them there. Unless you’re trying to animate the gradient points, then you’ll be forced to do it in code…


#3

the folks in ##c+±general suggested making a single path within a max radius, drawing it 1px wide with a certain color, then shrinking the radius 1px, and changing color. repeat as necessary


#4

I had some success here, using that technique:

void SquareGradient::paint(Graphics& g)
{
    auto inner = getLocalBounds().withSizeKeepingCentre(3 * getWidth() / 4, 3 * getHeight() / 4);
    
    auto innerTopleftPoint = inner.getTopLeft();
    auto obCenter = getLocalBounds().toFloat().getCentre();
   
    Colour outerColor{0xffffffff};
    Colour centerColor{ static_cast<uint8>(JUCE_LIVE_CONSTANT(0x46)),
        static_cast<uint8>(JUCE_LIVE_CONSTANT(0xa8)),
        static_cast<uint8>(JUCE_LIVE_CONSTANT(0xcd))
    };
    
    ColourGradient cg{outerColor, 0, 0, centerColor, static_cast<float>(innerTopleftPoint.getX()), 0, false};
    HeapBlock<juce::PixelARGB> colors;
    auto numColors = cg.createLookupTable(AffineTransform(), colors);
    DBG( "numColors: " + String(numColors)   );
    Path path;
    int i = 0;
    Path lastPath;
    for( ; i < 80 && i < numColors; )
    {
        Colour c( colors[i].getUnpremultiplied() ); //there's got to be a better way to turn PixelARGB's into Colour objects from ColourGradient lookup tables
        DBG( "Colour: " + c.toDisplayString(true));
        
        float radius = (getWidth()* 0.5f) - i;
        path.addStar(obCenter, 6, 100-i, radius);
        PathStrokeType pst(1);
        g.setColour(c);
        g.strokePath(path, pst);
        lastPath = path;
        path.clear();
        i++;
    }
    
    g.setColour( Colour(colors[i].getUnpremultiplied() ) );
    g.fillPath(lastPath);
}

#5

I recall there’s an interesting library here by @TheVinn where you may get some hints on these graphics effects.
Don’t think it’s up to date with juce 5 but the graphics class should be similar to today’s version.


#6

@jules why does ColourGradient::createLookupTable generate 3x as many colors as is required for a line of X length?

const int numEntries = jlimit (1, jmax (1, (colours.size() - 1) << 8),
                                   3 * (int) point1.transformedBy (transform)
                                                .getDistanceFrom (point2.transformedBy (transform)));

I’m creating a gradient like such:

ColourGradient cg{outerColor, 0, 0, centerColor, 30, 0, false};

the idea is to linearly morph from outerColor to centerColor over 30px.
However, when I call cg.createLookupTable(), 90 colors are generated.


#7

To provide some room for scale factors, sub-pixel accuracy, etc.


#8

ok. Perhaps a note in the documentation explaining that would help. Per my sample code a few posts up, I spent a while trying to figure out why I wasn’t getting my end color to fully show up out of that for() loop. eventually I settled on:

for( ; i < numColors; )
{
   ...
   i += (numColors / width);
}

where width is the length of the ColourGradient being drawn:

ColourGradient cg{outerColor, 0, 0, centerColor, 30, 0, false}; //fade between outer and center in 30 pixels

#9

There’s nothing wrong with the documentation, it tells you everything you need to know:

/** Creates a set of interpolated premultiplied ARGB values.
    This will resize the HeapBlock, fill it with the colours, and will return the number of
    colours that it added.
    When calling this, the ColourGradient must have at least 2 colour stops specified.
*/
int createLookupTable (const AffineTransform& transform, HeapBlock<PixelARGB>& resultLookupTable) const;

Like it says, it returns the number of colours it added, so obviously you can’t just ignore the return value and make some random assumption about how many there are.

Or just call the other version of that method where you ask for exactly how many colours you want!


#10

D’oh!!! that’s why you’re the GOAT