Photoshop Layer Effects for JUCE Controls!

As discussed in one of my other posts, here is the first iteration of Photoshop-style “Layer Effects” using the JUCE software renderer. Here’s how it works:

  1. Create a BackgroundContext from your Graphics and draw into it using normal Graphics operations (BackgroundContext is derived from Graphics). You have to fill the whole thing (this is a temporary JUCE limitation for now). This means your Component must have setOpaque (true).

  2. Create a LayerContext from your BackgroundContext and draw into it using normal Graphics operations. As you draw, JUCE builds up the alpha channel of your layer.

  3. Set the layer options for the fill transfer mode and opacity, your general blend options (including transfer mode), and your optional Drop Shadow layer effect options. These options map almost identically to the controls found in the Photoshop “Layer Effects” dialog box.

  4. When the LayerContext goes out of scope the layer will be composited into the background.

  5. Repeat steps 2, 3, and 4 as desired to build up a rich control.

Here’s an example with code and output:

[attachment=0]DropShadow.png[/attachment]

The following blending modes are supported:


normal,
lighten,
darken,
multiply,
average,
add,
subtract,
difference,
negation,
screen,
exclusion,
overlay,
softLight,
hardLight,
colorDodge,
colorBurn,
linearDodge,
linearBurn,
linearLight,
vividLight,
pinLight,
hardMix,
reflect,
glow,
phoenix,

The LayerEffects Demo repository is located here:

BackgroundContext and LayerContext are part of VFLib!

P.S. If anyone knows anything about grayscale morphological image operators using radially symmetric structure elements, message me!

6 Likes

Nice job, Vinnie!

Wow, nice work. This might come in very handy in the near future…

Awesome! How about replicating the Photoshop bevel plugin?!? 8)

Well I plan on implementing all of the layer effects but if you have any ideas on how I can work around the premultiplied ARGB problem (http://www.rawmaterialsoftware.com/viewtopic.php?f=2&t=9544) I would like to hear it, or else this is dead in the water.

A little something to whet the appetite, Inner Shadow (plus Drop Shadow):

[attachment=0]InnerShadow.png[/attachment]

Code:

void CTextDemo::paint (Graphics& g)
{
  Rectangle <int> const b (getLocalBounds ());

  // Photoshop "Background" Layer
  vf::BackgroundContext bc (g, b);
  bc.setGradientFill (ColourGradient (
    Colours::black, b.getX (), b.getY (),
    Colours::white, b.getX (), b.getBottom (), false));
  bc.fillRect (b);

  // Photoshop Layer + Mask, with Layer Effects
  vf::LayerContext lc (bc, b);
  vf::LayerContext::Options& opt (lc.getOptions ());

  lc.setGradientFill (ColourGradient (
    Colours::red, b.getX (), b.getY (),
    Colours::yellow, b.getRight (), b.getBottom (),
    false));
  lc.setFont (b.getHeight () / 3.f);
  lc.drawFittedText ("Layer\nEffects",
    b, Justification::centred, 2);

  opt.general.mode = vf::normal;
  opt.general.opacity = 1;
  opt.fill.opacity = 1;

  opt.dropShadow.active   = true;
  opt.dropShadow.mode     = vf::normal;
  opt.dropShadow.colour   = Colours::black;
  opt.dropShadow.angle    = 2*3.14159 * 135 / 360;
  opt.dropShadow.distance = 4;
  opt.dropShadow.spread   = 0.5;
  opt.dropShadow.size     = 8;
  opt.dropShadow.knockout = true;

  opt.innerShadow.active   = true;
  opt.innerShadow.mode     = vf::normal;
  opt.innerShadow.colour   = Colours::yellow;
  opt.innerShadow.angle    = 2*3.14159 * 135 / 360;
  opt.innerShadow.distance = 4;
  opt.innerShadow.choke    = 0.5;
  opt.innerShadow.size     = 4;
}

I’m going to pay no attention to professionalism and just say that screenshot takes me from six to midnight.

Here’s a comparison of the Photoshop Layer Effects versus the equivalent procedural code in VFLib:

[attachment=1]PhotoshopComparison.png[/attachment]

This is the code for producing the image:

[attachment=0]PhotoshopComparisonCode.png[/attachment]

Need these changes in order to continue:

Graphics, ComponentPeer, LowLevel… changes

That will surely be needed - do you have any pointers or tips?

Hi Vinn, I’m seriously interested in this project. I was wondering if you have plans to implement the angle gradient style overlay. This is probably something that would be brilliant to add to the main JUCE ColourGradient but I know Jules has his hands full at the moment. I would offer to investigate but really know nothing about graphics operations.

I think this is something thats missing and adding it would really enable all those knob emulations designers are so fond of. Take a look at the following photoshop tutorial, JUCE can already do most of this with various gradient overlays and your gaussian blur class comes in extremely useful but I’m stuck when I get to the gradient overlay stage just before the “Creating The Knob Shadow” section.

Alternatively does anyone have any tips for creating similar effects? Possibly a large amount of linear gradients with triangular clip regions? Any thoughts?

You’re asking if I’m going to implement the Photoshop “Angle Gradient” from the blend tool? I wasn’t planning on it. You could do it yourself, see the notes below.

Gradient fills in JUCE need some loving that’s for sure. When I’m done with LayerEffects I will propose some changes for JUCE’s treatment of blends. Note that with my LayerEffects code, you can bypass the Graphics object and implement your own custom drawing.

Yes I have some tips. No, you don’t want linear blends. This is what you need to do: for each pixel, compute the angle of the line segment that runs from that pixel to the center of your blend. The straightforward but slow approach would be to brute force the computation:

  float dy = y - y0;
  float dx = x - x0;
  float angleInRadians = atan (dy / dx);

(x,y) is the current pixel and (x0,y0) is the center of your blend. Someone please correct me if my math is wrong.

The angle will from (-pi, +pi) you can map that into the range (0,1), look up the colour in the gradient fill lookup table and then output that to the destination image.

To optimize the slow version that calls atan for each pixel you may observe that dy is constant across a row and dx increases by one with each output pixel. So you could in theory first substitute a polynomial approximation to atan, perhaps a Taylor series, and then differentiate with respect to x.

atan2() is the ideal function:

Apparently atan2() can be made blindingly fast if you’re willing to accept a small amount of error:

http://lists.apple.com/archives/perfoptimization-dev/2005/Jan/msg00051.html

It was easy. Here’s the code, you’ll need to adapt it to output colour and use a transparency mask:

// |error| < 0.005
float fast_atan2f (float y, float x)
{
  float const PI_FLOAT = 3.14159265f;
  float const PIBY2_FLOAT = 1.5707963f;

  if ( x == 0.0f )
  {
    if ( y > 0.0f ) return PIBY2_FLOAT;
    if ( y == 0.0f ) return 0.0f;
    return -PIBY2_FLOAT;
  }
  float atan;
  float z = y/x;
  if ( fabsf( z ) < 1.0f )
  {
    atan = z/(1.0f + 0.28f*z*z);
    if ( x < 0.0f )
    {
      if ( y < 0.0f ) return atan - PI_FLOAT;
      return atan + PI_FLOAT;
    }
  }
  else
  {
    atan = PIBY2_FLOAT - z/(z*z + 0.28f);
    if ( y < 0.0f ) return atan - PI_FLOAT;
  }
  return atan;
}

Image LayerGraphics::renderAngleGradient (Rectangle <int> const& bounds)
{
  float const x0 = float (bounds.getWidth ()) / 2;
  float const y0 = float (bounds.getHeight ()) / 2;

  Image destImage (Image::RGB, bounds.getWidth (), bounds.getHeight (), false);

  Pixels dest (destImage);

  float inv = 255.f / (2 * 3.14159265f);

  for (int y = 0; y < dest.height; ++y)
  {
    for (int x = 0; x < dest.width; ++x)
    {
      float a = inv * (fast_atan2f (y - y0, x - x0) + 1.5707963f);

      uint8 v = uint8 (a);

      uint8* p = dest.getPixelPointer (x, y);
      p [0] = v;
      p [1] = v;
      p [2] = v;
    }
  }

  return destImage;
}

[attachment=0]AngleGradient.png[/attachment]

1 Like

FYI these angle gradients are incredibly fast!!!

Cheers Vinn, this is super cool and could really open up the doors for some nice effects. I will try to experiment with it and somehow work it into ColourGradient or create a new AngleColourGradient class and matching changes to FillType so this can be used in the general Graphics calls.

I guess what needs to be done is replace your cast from ‘a’ to ‘v’ and use that value as an index into a pre-computed linear gradient look-up table or even do this on the fly. Then we need to add support for multiple points to be added to complete the effect.

I can’t promise when I’ll be able to get to this though as I’m major busy at the moment and not even really supposed to be working on the graphics side of the project, I was really enquiring on behalf of our graphics guy. This is something that definitely needs doing however and I’m sure many others will make use of it so I’ll try to find some time in a few weeks time.

Thanks again Vinn, all your current work on graphics is really gonna put JUCE head and shoulders above other frameworks. (Imaging adding a PSD to the Introjucer and have it create the graphics code for you?! Goosebumps.)

I’m already working on that. juce::ColourGradient is inadequate for expressing the richness of the colour blends found in Photoshop. I’ve developed a new class for dealing with this, which includes transparency stops separate from the colour stops, and also allows a gamma adjustment for each set of stops. My class uses a reference counted handle so its efficient to pass by value. I’ve added a new class SharedTable which implements a templated lookup table, also using a reference counted handle. My current implementation of angle gradient fill is a template function that accepts a functor to perform the per-pixel operation. With the right functor you can efficiently implement lookup into the table along with all of the blending modes, across all image types (using a suitable family of instantiations).

The JUCE interface is broken when it comes to blends. It is not possible to have an abstract source of colour table entries, and the ColourGradient class doesn’t separate concerns. The parameters for linear versus radial blends (like starting point / ending point) should not be mixed in with the specification of the colours. It’s a fairly trivial matter to add support for an angle gradient edge table fill in the software renderer. I have doubts about how that would be implemented natively (CoreGraphics? OpenGL?).

But there’s a bigger problem. All the cool things that we want to do are not possible with having only one blend mode (“normal”). Without darken, lighten, soft light, etc… we close the door to most if not all of the cool Photoshop effects found in tutorials. And it is possible to optimize the drawing of linear blends by quite a large factor by using a difference equation per scanline. This is not compatible with the current templated interface of edge table rendering. The optimized radial fill in the PDF that I linked in another post is totally incompatible with RenderingHelpers interfaces. And some of the blending modes are not practical to implement and maintain in hardware. I’m certainly not going to be writing any GPU shaders.

Based on my experience with LayerEffects, I have come to these conclusions:

  • It is not practical or desired to expand the JUCE Graphics pipeline to support new and difficult to implement fills, blends, or compositing modes

  • It is reasonable to require the use of software rendering in your ComponentPeer to access these enhanced effects. In order to calculate blending modes other than “normal”, we need to get at the Image corresponding to a Graphics context. This is only possible with the software renderer.

  • Since the software renderer is a requirement, the most logical implementation of these effects is as a separate family of classes that render onto an Image and can be developed in parallel without depending on a continuing evolution of changes in JUCE.

[size=150]These are the fixed requirements that JUCE needs to offer to make these things possible:[/size]

  • Possible to force a ComponentPeer to always use the software renderer. LookAndFeel supports this now, in cooperation with the corresponding native definitions of ComponentPeer.

  • Possible to retrieve the underlying software renderer of the Graphics object through dynamic_cast<>. This is currently possible.

  • Able to retrieve the underlying Image and TranslationOrTransform of the LowLevelGraphicsSoftwareRenderer. This was recently added (thanks!).

The latest JUCE tip fulfills all these requirements (thanks!), please let’s keep it that way!

These are JUCE improvements are “nice to have” but not required:

  • Abstract interface for setting the colours on a blend in the Graphics context (i.e. separate colours from blend style: linear, radial, angle).

  • Ref counted Pimpl for ColourGradient, so they can be passed by value efficiently.

  • Refactoring of the RenderingHelpers and slight adjustments to LowLevelGraphicsContext so it is easier to reuse those objects to create a new software renderer. For example, a huge optimization of my layer effects would be to have a custom software renderer that does everything the current renderer does, but also maintains a “smallest bounding box” of all the pixels written to, to reduce the number of pixels that have to be composited in subsequent layer effect calculations.

  • Refactoring to allow a custom software renderer to a non-premultiplied ARGB image.

Angle gradient with a new vf::GradientColours class which has colour and alpha stop points:

[attachment=0]GradientOverlay.png[/attachment]

What I really need is a Component that lets you edit the colour / alpha ramp for a gradient, like this one:

[attachment=0]GradientComponent.png[/attachment]

Perhaps you can clean up this old bit of code that I had lying around… If you can polish it up into a component I could add to the library, that’d be cool…

class GradientDesigner  : public Component,
                          public ChangeBroadcaster,
                          public ChangeListener,
                          public ButtonListener
{
public:
    GradientDesigner (const ColourGradient& gradient_)
        : gradient (gradient_),
          selectedPoint (-1),
          dragging (false),
          draggingNewPoint (false),
          draggingPos (0)
    {
        addChildComponent (&colourPicker);

        linearButton.setButtonText ("Linear");
        linearButton.setRadioGroupId (321);
        linearButton.setConnectedEdges (TextButton::ConnectedOnRight | TextButton::ConnectedOnLeft);
        radialButton.setButtonText ("Radial");
        radialButton.setRadioGroupId (321);
        radialButton.setConnectedEdges (TextButton::ConnectedOnRight | TextButton::ConnectedOnLeft);

        addAndMakeVisible (&linearButton);
        addAndMakeVisible (&radialButton);

        linearButton.addListener (this);
        radialButton.addListener (this);
        colourPicker.addChangeListener (this);
    }

    void paint (Graphics& g)
    {
        g.fillAll (getLookAndFeel().findColour (ColourSelector::backgroundColourId));
        g.fillCheckerBoard (previewArea, 10, 10, Colour (0xffdddddd), Colour (0xffffffff));

        FillType f (gradient);
        f.gradient->point1.setXY ((float) previewArea.getX(), (float) previewArea.getCentreY());
        f.gradient->point2.setXY ((float) previewArea.getRight(), (float) previewArea.getCentreY());
        g.setFillType (f);
        g.fillRect (previewArea);

        Path marker;
        const float headSize = 4.5f;
        marker.addLineSegment (Line<float> (0.0f, -2.0f, 0.0f, previewArea.getHeight() + 2.0f), 1.5f);
        marker.addTriangle (0.0f, 1.0f, -headSize, -headSize, headSize, -headSize);

        for (int i = 0; i < gradient.getNumColours(); ++i)
        {
            const double pos = gradient.getColourPosition (i);
            const Colour col (gradient.getColour (i));

            const AffineTransform t (AffineTransform::translation (previewArea.getX() + 0.5f + (float) (previewArea.getWidth() * pos),
                                                                   (float) previewArea.getY()));

            g.setColour (Colours::black.withAlpha (0.8f));
            g.strokePath (marker, PathStrokeType (i == selectedPoint ? 2.0f : 1.5f), t);
            g.setColour (i == selectedPoint ? Colours::lightblue : Colours::white);
            g.fillPath (marker, t);
        }
    }

    void resized()
    {
        previewArea.setBounds (7, 35, getWidth() - 14, 24);

        const int w = 60;
        linearButton.setBounds (getWidth() / 2 - w, 2, w, 20);
        radialButton.setBounds (getWidth() / 2, 2, w, 20);

        colourPicker.setBounds (0, previewArea.getBottom() + 16,
                                getWidth(), getHeight() - previewArea.getBottom() - 16);
    }

    void mouseDown (const MouseEvent& e)
    {
        dragging = false;
        draggingNewPoint = false;
        int point = getPointAt (e.x);

        if (point >= 0)
            setSelectedPoint (point);
    }

    void mouseDrag (const MouseEvent& e)
    {
        if ((! dragging) && ! e.mouseWasClicked())
        {
            preDragGradient = gradient;
            const int mouseDownPoint = getPointAt (e.getMouseDownX());

            if (mouseDownPoint >= 0)
            {
                if (mouseDownPoint > 0 && mouseDownPoint < gradient.getNumColours() - 1)
                {
                    dragging = true;
                    draggingNewPoint = false;
                    draggingColour = gradient.getColour (mouseDownPoint);
                    preDragGradient.removeColour (mouseDownPoint);
                    selectedPoint = -1;
                }
            }
            else
            {
                dragging = true;
                draggingNewPoint = true;
                selectedPoint = -1;
            }
        }

        if (dragging)
        {
            draggingPos = jlimit (0.001, 0.999, (e.x - previewArea.getX()) / (double) previewArea.getWidth());
            gradient = preDragGradient;

            if (previewArea.expanded (6, 6).contains (e.x, e.y))
            {
                if (draggingNewPoint)
                    draggingColour = preDragGradient.getColourAtPosition (draggingPos);

                selectedPoint = gradient.addColour (draggingPos, draggingColour);
                updatePicker();
            }
            else
            {
                selectedPoint = -1;
            }

            sendChangeMessage();
            repaint (previewArea.expanded (30, 30));
        }
    }

    void mouseUp (const MouseEvent&)
    {
        dragging = false;
    }

    const ColourGradient& getGradient() const noexcept   { return gradient; }

    void setGradient (const ColourGradient& newGradient)
    {
        if (newGradient != gradient || selectedPoint < 0)
        {
            jassert (newGradient.getNumColours() > 1);

            gradient = newGradient;

            if (selectedPoint < 0)
                selectedPoint = 0;

            linearButton.setToggleState (! gradient.isRadial, false);
            radialButton.setToggleState (gradient.isRadial, false);

            updatePicker();
            sendChangeMessage();
            repaint();
        }
    }

    void setSelectedPoint (int newIndex)
    {
        if (selectedPoint != newIndex)
        {
            selectedPoint = newIndex;
            updatePicker();
            repaint();
        }
    }

    void changeListenerCallback (ChangeBroadcaster*)
    {
        if (selectedPoint >= 0 && (! dragging) && gradient.getColour (selectedPoint) != colourPicker.getCurrentColour())
        {
            gradient.setColour (selectedPoint, colourPicker.getCurrentColour());
            repaint (previewArea);
            sendChangeMessage();
        }
    }

    void buttonClicked (Button* b)
    {
        ColourGradient g (gradient);
        g.isRadial = (b == &radialButton);
        setGradient (g);
    }

private:
    StoredSettings::ColourSelectorWithSwatches colourPicker;
    ColourGradient gradient;
    int selectedPoint;
    bool dragging, draggingNewPoint;
    double draggingPos;
    Colour draggingColour;
    ColourGradient preDragGradient;

    Rectangle<int> previewArea;
    TextButton linearButton, radialButton;

    void updatePicker()
    {
        colourPicker.setVisible (selectedPoint >= 0);
        if (selectedPoint >= 0)
            colourPicker.setCurrentColour (gradient.getColour (selectedPoint));
    }

    int getPointAt (const int x) const
    {
        int best = -1;
        double bestDiff = 6;

        for (int i = gradient.getNumColours(); --i >= 0;)
        {
            const double pos = previewArea.getX() + previewArea.getWidth() * gradient.getColourPosition (i);
            const double diff = std::abs (pos - x);

            if (diff < bestDiff)
            {
                bestDiff = diff;
                best = i;
            }
        }

        return best;
    }
};