Skip to content

Commit

Permalink
WIP Retina scaling
Browse files Browse the repository at this point in the history
  • Loading branch information
sudara committed Nov 29, 2023
1 parent e3eaa89 commit b887c33
Show file tree
Hide file tree
Showing 12 changed files with 610 additions and 64 deletions.
3 changes: 2 additions & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ if (MelatoninBlur_IS_TOP_LEVEL)
add_executable(Tests ${TestFiles})
target_compile_features(Tests PUBLIC cxx_std_17)

target_sources(Tests PRIVATE "tests/blur_implementations.cpp" "tests/drop_shadow.cpp" "tests/inner_shadow.cpp")
file(GLOB_RECURSE BlurTests CONFIGURE_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/tests/*.cpp")
target_sources(Tests PRIVATE ${BlurTests})

# Our test executable also wants to know about our plugin code...
target_include_directories(Tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/source)
Expand Down
11 changes: 5 additions & 6 deletions melatonin/cached_blur.h
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,14 @@ namespace melatonin
class CachedBlur
{
public:
explicit CachedBlur (size_t r)
: radius (r)
explicit CachedBlur (size_t r) : radius (r)
{
jassert (radius > 0);
}

// we are passing the source by value here
// (but it's a value object of sorts since its reference counted)
void update (juce::Image newSource)
void update (const juce::Image& newSource)
{
if (newSource != src)
{
Expand Down Expand Up @@ -45,8 +44,8 @@ namespace melatonin
// juce::Images are value objects, reference counted behind the scenes
// We want to store a reference to the src so we can compare on render
// And we actually are the owner of the dst
juce::Image src = juce::Image();
juce::Image dst = juce::Image();
size_t radius;
size_t radius = 0;
juce::Image src;
juce::Image dst;
};
}
51 changes: 41 additions & 10 deletions melatonin/shadows.h
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
#pragma once
#include "implementations/gin.h"
#include "juce_gui_basics/juce_gui_basics.h"
#include "support/cached_shadow.h"
#include "support/cached_shadows.h"

namespace melatonin
{

/* A drop shadow is a path filled by a single color and then blurred.
These shadows are cached.
Expand All @@ -23,25 +22,57 @@ namespace melatonin
const int radius = 1;
const juce::Point<int> offset = { 0, 0 };
// Spread expands or contracts the path size
// Spread literally just expands or contracts the *path* size
// Inverted for inner shadows
const int spread = 0;
// by default we match the main graphics context's scaling factor
bool lowQuality = false;
}
*/
class DropShadow : public CachedShadow
class DropShadow : public CachedShadows
{
public:
DropShadow (std::initializer_list<ShadowParameters> p) : CachedShadow (p) {}
// multiple shadows
DropShadow (std::initializer_list<ShadowParameters> p) : CachedShadows (p) {}

// single ShadowParameters
// melatonin::DropShadow ({Colour::fromRGBA (255, 183, 43, 111), pulse (6)}).render (g, button);
explicit DropShadow (ShadowParameters p) : CachedShadows ({ p }) {}

// individual arguments
DropShadow (juce::Colour color, int radius, juce::Point<int> offset = { 0, 0 }, int spread = 0)
: CachedShadows ({ { color, radius, offset, spread } }) {}
};

// An inner shadow is basically the *inverted* filled path, blurred and clipped to the path
// so the blur is only visible *inside* the path.
class InnerShadow : public CachedShadow
class InnerShadow : public CachedShadows
{
public:
InnerShadow (std::initializer_list<ShadowParameters> p) : CachedShadow (p)
{
std::for_each (shadowParameters.begin(), shadowParameters.end(), [] (auto& s) { s.inner = true; });
}
// multiple shadows
InnerShadow (std::initializer_list<ShadowParameters> p) : CachedShadows (p, true) {}

// single initializer list
explicit InnerShadow (ShadowParameters p) : CachedShadows ({ p }, true) {}

// individual arguments
InnerShadow (juce::Colour color, int radius, juce::Point<int> offset = { 0, 0 }, int spread = 0)
: CachedShadows ({ { color, radius, offset, spread } }, true) {}
};

// Renders a collection of inner and drop shadows plus a path
class PathWithShadows : public CachedShadows
{
// multiple shadows
PathWithShadows (std::initializer_list<ShadowParameters> p) : CachedShadows (p) {}

// single ShadowParameters
// melatonin::DropShadow ({Colour::fromRGBA (255, 183, 43, 111), pulse (6)}).render (g, button);
explicit PathWithShadows (ShadowParameters p) : CachedShadows ({ p }) {}

// individual arguments
PathWithShadows (juce::Colour color, int radius, juce::Point<int> offset = { 0, 0 }, int spread = 0)
: CachedShadows ({ { color, radius, offset, spread } }) {}
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,30 +3,23 @@

namespace melatonin
{
// This class isn't meant for direct usage!
// Use DropShadow and InnerShadow
class CachedShadow
// This class isn't meant for direct usage and may change its API over time!
// Use DropShadow and InnerShadow and PathWithShadows instead, you've been warned...
class CachedShadows
{
protected:
std::vector<ShadowParameters> shadowParameters;

CachedShadow (std::initializer_list<ShadowParameters> p)
CachedShadows (std::initializer_list<ShadowParameters> p, bool inner = false)
: shadowParameters (p)
{
jassert (!shadowParameters.empty());

for (auto& shadow : shadowParameters)
{
// 0 radius means no shadow..
if (shadow.radius < 1)
{
jassertfalse;
continue;
}
renderedShadows.resize (shadowParameters.size());

// each shadow is backed by a JUCE image
renderedShadows.emplace_back();
}
// allow specifying of inner shadows in bulk
if (inner)
std::for_each (shadowParameters.begin(), shadowParameters.end(), [] (auto& s) { s.inner = true; });
}

public:
Expand All @@ -38,23 +31,25 @@ namespace melatonin
if (newPath != path)
{
path = newPath;
recalculateBlurs();
recalculateBlurs (g);
}

drawCachedBlurs (g, optimizeClipBounds);
}


private:
juce::Path path;
std::vector<juce::Image> renderedShadows;

void recalculateBlurs ()
void recalculateBlurs (juce::Graphics& g)
{
for (size_t i = 0; i < shadowParameters.size(); ++i)
{
auto& s = shadowParameters[i];
renderedShadows[i] = renderShadowToARGB (s, path);
if (s.radius < 1)
continue;

renderedShadows[i] = renderShadowToARGB (s, path, g.getInternalContext().getPhysicalPixelScaleFactor());
}
}

Expand All @@ -63,26 +58,37 @@ namespace melatonin
for (size_t i = 0; i < shadowParameters.size(); ++i)
{
auto& s = shadowParameters[i];
float scale = 1.0;
if (!s.lowQuality)
scale = g.getInternalContext().getPhysicalPixelScaleFactor();

// needed to allow this to support 0 radius blurs (for animation, etc)
if (s.radius < 1)
continue;

// resets the Clip Region when this scope ends
juce::Graphics::ScopedSaveState saveState (g);

// for inner shadows we don't want anything outside the path bounds
if (s.inner)
g.reduceClipRegion (path);
g.reduceClipRegion ((path.getBounds() * scale).getSmallestIntegerContainer());

// TODO: requires testing/benchmarking
else if (optimizeClipBounds)
{
// don't bother drawing what's inside the path's bounds
// TODO: requires testing/benchmarking
g.excludeClipRegion (path.getBounds().toNearestIntEdges());
}

// Not sure why, but this is required despite fillAlphaChannelWithCurrentBrush=false
// TODO: consider setting opacity here, not in the shadow rendering
// That would let us cheaply do things like fade shadows in without rerendering!
g.setColour (s.color);

// Specifying `false` for `fillAlphaChannelWithCurrentBrush` here
// is a 2-3x speedup on the actual image rendering
g.drawImageAt (renderedShadows[i], s.area.getX(), s.area.getY());
// `s.area` has been scaled by the physical pixel scale factor
// (unless lowQuality is true)
// we have to pass a 1/scale transform because the context will otherwise try to scale the image up
// (which is not what we want, at this point we are 1:1)
g.drawImageTransformed (renderedShadows[i], juce::AffineTransform::translation (s.area.getX(), s.area.getY()).scaled(1/scale));
}
}
};
Expand Down
76 changes: 56 additions & 20 deletions melatonin/support/helpers.h
Original file line number Diff line number Diff line change
@@ -1,51 +1,67 @@
#pragma once
#include "juce_gui_basics/juce_gui_basics.h"

#include "implementations.h"
#include "juce_gui_basics/juce_gui_basics.h"
#include <melatonin_blur/tests/helpers/pixel_helpers.h>

namespace melatonin
{
// these are the parameters required to represent a single drop or inner shadow
// wish i could put these in shadows.h to help people
// wish I could put these in shadows.h to help people
struct ShadowParameters
{
// one single color per shadow
const juce::Colour color = {};
const int radius = 1;
const juce::Point<int> offset = { 0, 0 };

// Spread literally just expands or contracts the path size
// Spread literally just expands or contracts the *path* size
// Inverted for inner shadows
const int spread = 0;

// by default we match the main graphics context's scaling factor
bool lowQuality = false;

// an inner shadow is just a modified drop shadow
bool inner = false;

// each shadow takes up a different amount of space depending on it's radius, spread, etc
// Internal: each shadow takes up a different amount of space depending on its radius, spread, etc
juce::Rectangle<int> area = {};
};

// this caches the expensive shadow creation into a ARGB juce::Image for fast compositing
static inline juce::Image renderShadowToARGB (ShadowParameters& s, juce::Path& originalPath)
// this caches expensive shadow creation into a ARGB juce::Image for fast compositing
// WARNING: Internal only, don't use unless you know what you are doing
// Scale is piped in from CachedShadows::getPhysicalPixelScaleFactor
[[nodiscard]] static inline juce::Image renderShadowToARGB (ShadowParameters& s, const juce::Path& originalPath, float scale)
{
if (s.lowQuality)
scale = 1.0f;

// by default, match the main graphics context's scaling factor (render high quality)
int scaledRadius = juce::roundToInt ((float) s.radius * scale);
int scaledSpread = juce::roundToInt ((float) s.spread * scale);
auto scaledOffset = (s.offset * scale).roundToInt();

// the area of each cached blur depends on its radius and spread
s.area = (originalPath.getBounds().getSmallestIntegerContainer() + s.offset)
.expanded (s.radius + s.spread + 1);
// 0,0 remains fixed the same thanks to .expanded (it goes negative)
// we also have to scale s.area itself, because it's later used for compositing
// so it must reflect the scale of the ARGB image we're returning
s.area = ((originalPath.getBounds() * scale).getSmallestIntegerContainer() + scaledOffset)
.expanded (scaledRadius + scaledSpread);

// TODO: Investigate/test what this line does — makes the clip smaller for certain cases?
// TODO: Investigate/test if this is ever relevant
// I'm guessing reduces the clip size in the edge case
// where it wouldn't overlap the main context?
//.getIntersection (g.getClipBounds().expanded (s.radius + s.spread + 1));

// Reconsider your parameters: one of the dimensions is 0 so the blur doesn't exist!
if (s.area.getWidth() < 1 || s.area.getHeight() < 1)
jassertfalse;

// we don't want to modify our original path (it would break cache)
// additionally, inner shadows must render a modified path
// additionally, inner shadows render a modified path
auto shadowPath = juce::Path (originalPath);

if (s.spread != 0)
if (scaledSpread != 0)
{
// TODO: drop shadow tests for s.area to understand why this is still needed (we expanded above!)
s.area.expand (s.spread, s.spread);
// expand the actual path itself
// note: this is 1x, since it'll be upscaled as needed by fillPath
auto bounds = originalPath.getBounds().expanded (s.inner ? (float) -s.spread : (float) s.spread);
shadowPath.scaleToFit (bounds.getX(), bounds.getY(), bounds.getWidth(), bounds.getHeight(), true);
}
Expand All @@ -54,7 +70,21 @@ namespace melatonin
if (s.inner)
{
shadowPath.setUsingNonZeroWinding (false);
shadowPath.addRectangle (s.area.expanded (10));

// add an arbitrary amount of extra padding
// since the outside will be filled, this lets us
// reliably cast a blurred shadow into the path's area
// TODO: test if edge bleed lets us happily cheat here or if this should be 'radius'
shadowPath.addRectangle (shadowPath.getBounds().expanded (2));
}

// Check your parameters: the blur image ended up with a dimension of 0
// Did you set a negative spread? Check that your path still exists after applying the spread.
// For example, you can't have a 3x3px path with a -2px spread
if (s.area.isEmpty())
{
jassertfalse;
return {};
}

// each shadow is its own single channel image associated with a color
Expand All @@ -64,12 +94,15 @@ namespace melatonin
{
juce::Graphics g2 (renderedSingleChannel);

// ensure we're working at the correct scale
g2.addTransform (juce::AffineTransform::scale (scale));

g2.setColour (juce::Colours::white);
g2.fillPath (shadowPath, juce::AffineTransform::translation ((float) (s.offset.x - s.area.getX()), (float) (s.offset.y - s.area.getY())));
g2.fillPath (shadowPath, juce::AffineTransform::translation ((float) (s.offset.x - s.area.getX() / scale), (float) (s.offset.y - s.area.getY() / scale)));
}

// perform the blur with the fastest algorithm available
melatonin::blur::singleChannel (renderedSingleChannel, s.radius);
melatonin::blur::singleChannel (renderedSingleChannel, scaledRadius);

// YET ANOTHER graphics context to efficiently convert the image to ARGB
// why? Because later, compositing to the main graphics context becomes (g) faster
Expand All @@ -78,7 +111,10 @@ namespace melatonin
// see: https://forum.juce.com/t/faster-blur-glassmorphism-ui/43086/76
juce::Image renderedARGB (juce::Image::ARGB, s.area.getWidth(), s.area.getHeight(), true);
{
// note that there's no transform for this context
// we already scaled up (if needed) the last round
juce::Graphics g2 (renderedARGB);

g2.setColour (s.color);
g2.drawImageAt (renderedSingleChannel, 0, 0, true);
}
Expand Down
1 change: 1 addition & 0 deletions melatonin_blur.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@
#include "tests/blur_implementations.cpp"
#include "tests/drop_shadow.cpp"
#include "tests/inner_shadow.cpp"
#include "tests/shadow_scaling.cpp"
#endif
6 changes: 6 additions & 0 deletions melatonin_blur.h
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,9 @@ END_JUCE_MODULE_DECLARATION
#include "juce_graphics/juce_graphics.h"
#include "melatonin/cached_blur.h"
#include "melatonin/shadows.h"

// These are juce::Component ImageEffects
// see https://docs.juce.com/master/classImageEffectFilter.html
#include "melatonin/image_effects/blur_effect.h"
#include "melatonin/image_effects/drop_shadow_effect.h"
#include "melatonin/image_effects/reflection_effect.h"
Loading

0 comments on commit b887c33

Please sign in to comment.