diff options
author | Mike Kaganski <mike.kaganski@collabora.com> | 2020-05-01 01:05:24 +0300 |
---|---|---|
committer | Mike Kaganski <mike.kaganski@collabora.com> | 2020-05-07 21:57:37 +0200 |
commit | 08ebcff89f56bec3b0f9b346504748e4eb1687af (patch) | |
tree | d511115b648fd5f2340fd88eacc4ad9971ad7348 /drawinglayer | |
parent | a98bdbae459ad7341bf7f484c402e77e4062cd16 (diff) |
tdf#101181: improve glow effect
The shadow of objects must not be scaled: this displaces any internal
areas that need blur, e.g. holes. Instead, it needs to dilate the
shadow using kernel with radius equal to blur radius: this allows the
borders of dilated objects to be in the middle of the blur area. The
following blur makes those new margin points to have 50% intensity,
and full glow intensity at the point of old object margins. This also
removed artifacts when moving objects with glow effect caused by
mismatch between scaling and D2D range calculation.
The D2D range therefore is not calculated by scaling, but using grow.
Blur filter's "extend bitmap by blur radius" option got obsoleted and
removed.
There's no need to blur the glow color (24-bit RGB). Instead, glow
bitmap must be filled by glow color, and have an alpha mask that is
blurred accordingly. This makes the glow properly transparent, and
also reduces the blur complexity which now only needs to process 8
bits of alpha channel.
The object shadow is created using basegfx::BColorModifier_replace
inserted into the 2d decomposition of the effect, as before. To make
sure that any non-fully-transparent pixel will become black pixel in
the shadow, black color is used, and the result is further processed
in VclPixelProcessor2D::processGlowPrimitive2D with monochrome filter
using threshold 255.
Glow transparency attribute is taken into account: the initial value
at the margins of the objects. Color replacement filter is used to
replace the object shadow with the attribute value before blur pass.
Correct blur radius is used, calculated from glow effect radius,
instead of hardcoded value of 5 pixels. This makes the glow to fade
gradually along the full width of the effect, instead of only fading
in narrow outer border previously.
Since blur filter is only implemented for radius up to 254 pixels,
and since downsampling the shadow before blur increases performance
without noticeable quality loss, the image is downsampled before
filtering.
It should be noted that the glow effect is almost identical to soft
shadow effect, likely with the only difference of using dilation in
the former, but not in the latter. The code might be reused later to
implement soft shadow as well.
Change-Id: I728c532f9df7ccf85f353c23c6c7d8352d7b2086
Reviewed-on: https://gerrit.libreoffice.org/c/core/+/93235
Tested-by: Jenkins
Reviewed-by: Tomaž Vajngerl <quikee@gmail.com>
Reviewed-by: Mike Kaganski <mike.kaganski@collabora.com>
Diffstat (limited to 'drawinglayer')
-rw-r--r-- | drawinglayer/source/attribute/sdrglowattribute.cxx | 27 | ||||
-rw-r--r-- | drawinglayer/source/primitive2d/glowprimitive2d.cxx | 24 | ||||
-rw-r--r-- | drawinglayer/source/processor2d/vclpixelprocessor2d.cxx | 80 |
3 files changed, 69 insertions, 62 deletions
diff --git a/drawinglayer/source/attribute/sdrglowattribute.cxx b/drawinglayer/source/attribute/sdrglowattribute.cxx index 4295aef23ae3..90367ff67ea0 100644 --- a/drawinglayer/source/attribute/sdrglowattribute.cxx +++ b/drawinglayer/source/attribute/sdrglowattribute.cxx @@ -21,7 +21,7 @@ namespace drawinglayer { namespace attribute { -SdrGlowAttribute::SdrGlowAttribute(sal_Int32 nRadius, const basegfx::BColor& rColor) +SdrGlowAttribute::SdrGlowAttribute(sal_Int32 nRadius, const Color& rColor) : m_nRadius(nRadius) , m_color(rColor) { @@ -44,31 +44,6 @@ bool SdrGlowAttribute::operator==(const SdrGlowAttribute& rCandidate) const return m_nRadius == rCandidate.m_nRadius && m_color == rCandidate.m_color; } -const basegfx::B2DHomMatrix& SdrGlowAttribute::GetTransfMatrix(basegfx::B2DRange nRange) const -{ - if (!m_oTransfCache) - { - double dRadius100mm = static_cast<double>(m_nRadius) / 360.0; - // Apply a scaling with the center point of the shape as origin. - // 1) translate shape to the origin - basegfx::B2DHomMatrix matrix = basegfx::utils::createCoordinateSystemTransform( - nRange.getCenter(), basegfx::B2DVector(-1, 0), basegfx::B2DVector(0, -1)); - - basegfx::B2DHomMatrix inverse(matrix); - inverse.invert(); - - // 2) Scale up - double scale_x = (nRange.getWidth() + dRadius100mm) / nRange.getWidth(); - double scale_y = (nRange.getHeight() + dRadius100mm) / nRange.getHeight(); - matrix *= basegfx::utils::createScaleB2DHomMatrix(scale_x, scale_y); - - // 3) Translate shape back to its place - matrix *= inverse; - m_oTransfCache = std::move(matrix); - } - return *m_oTransfCache; -} - } // end of namespace attribute } // end of namespace drawinglayer diff --git a/drawinglayer/source/primitive2d/glowprimitive2d.cxx b/drawinglayer/source/primitive2d/glowprimitive2d.cxx index 7d6c23a322ad..bf49b8e215b5 100644 --- a/drawinglayer/source/primitive2d/glowprimitive2d.cxx +++ b/drawinglayer/source/primitive2d/glowprimitive2d.cxx @@ -30,12 +30,11 @@ using namespace com::sun::star; namespace drawinglayer::primitive2d { -GlowPrimitive2D::GlowPrimitive2D(const basegfx::B2DHomMatrix& rGlowTransform, - const basegfx::BColor& rGlowColor, +GlowPrimitive2D::GlowPrimitive2D(const Color& rGlowColor, double fRadius, const Primitive2DContainer& rChildren) : GroupPrimitive2D(rChildren) - , maGlowTransform(rGlowTransform) , maGlowColor(rGlowColor) + , mfGlowRadius(fRadius) { } @@ -45,7 +44,7 @@ bool GlowPrimitive2D::operator==(const BasePrimitive2D& rPrimitive) const { const GlowPrimitive2D& rCompare = static_cast<const GlowPrimitive2D&>(rPrimitive); - return (getGlowTransform() == rCompare.getGlowTransform() + return (getGlowRadius() == rCompare.getGlowRadius() && getGlowColor() == rCompare.getGlowColor()); } @@ -55,8 +54,9 @@ bool GlowPrimitive2D::operator==(const BasePrimitive2D& rPrimitive) const basegfx::B2DRange GlowPrimitive2D::getB2DRange(const geometry::ViewInformation2D& rViewInformation) const { - basegfx::B2DRange aRetval(getChildren().getB2DRange(rViewInformation)); - aRetval.transform(getGlowTransform()); + basegfx::B2DRange aRetval(GroupPrimitive2D::getB2DRange(rViewInformation)); + // We need additional space for the glow from all sides + aRetval.grow(getGlowRadius()); return aRetval; } @@ -67,15 +67,13 @@ void GlowPrimitive2D::get2DDecomposition( if (getChildren().empty()) return; - // create a modifiedColorPrimitive containing the Glow color and the content + // create a modifiedColorPrimitive containing the *black* color and the content. Using black + // on white allows creating useful mask in VclPixelProcessor2D::processGlowPrimitive2D. basegfx::BColorModifierSharedPtr aBColorModifier - = std::make_shared<basegfx::BColorModifier_replace>(getGlowColor()); + = std::make_shared<basegfx::BColorModifier_replace>(basegfx::BColor()); - const Primitive2DReference xRefA(new ModifiedColorPrimitive2D(getChildren(), aBColorModifier)); - const Primitive2DContainer aSequenceB{ xRefA }; - - // build transformed primitiveVector with Glow offset and add to target - rVisitor.append(new TransformPrimitive2D(getGlowTransform(), aSequenceB)); + const Primitive2DReference xRef(new ModifiedColorPrimitive2D(getChildren(), aBColorModifier)); + rVisitor.append(xRef); } // provide unique ID diff --git a/drawinglayer/source/processor2d/vclpixelprocessor2d.cxx b/drawinglayer/source/processor2d/vclpixelprocessor2d.cxx index bba66e2d4272..6c14c09c1a19 100644 --- a/drawinglayer/source/processor2d/vclpixelprocessor2d.cxx +++ b/drawinglayer/source/processor2d/vclpixelprocessor2d.cxx @@ -23,7 +23,10 @@ #include <sal/log.hxx> #include <tools/stream.hxx> +#include <vcl/BitmapBasicMorphologyFilter.hxx> +#include <vcl/BitmapColorReplaceFilter.hxx> #include <vcl/BitmapFilterStackBlur.hxx> +#include <vcl/BitmapMonochromeFilter.hxx> #include <vcl/outdev.hxx> #include <vcl/dibtools.hxx> #include <vcl/hatch.hxx> @@ -912,42 +915,73 @@ void VclPixelProcessor2D::processGlowPrimitive2D(const primitive2d::GlowPrimitiv { basegfx::B2DRange aRange(rCandidate.getB2DRange(getViewInformation2D())); aRange.transform(maCurrentTransformation); - aRange.grow(10.0); + basegfx::B2DVector aGlowRadiusVector(rCandidate.getGlowRadius(), 0); + // Calculate the pixel size of glow radius in current transformation + aGlowRadiusVector *= maCurrentTransformation; + const double fGlowRadius = aGlowRadiusVector.getLength(); impBufferDevice aBufferDevice(*mpOutputDevice, aRange); if (aBufferDevice.isVisible()) { // remember last OutDev and set to content OutputDevice* pLastOutputDevice = mpOutputDevice; - mpOutputDevice = &aBufferDevice.getTransparence(); - // paint content to virtual device + mpOutputDevice = &aBufferDevice.getContent(); + // Processing will draw whatever geometry on white background, applying *black* + // replacement color. The black color replacement is added in 2d decomposition of + // glow primitive. mpOutputDevice->Erase(); process(rCandidate); - - // obtain result as a bitmap - auto bitmap = mpOutputDevice->GetBitmapEx(Point(aRange.getMinX(), aRange.getMinY()), + Bitmap bitmap = mpOutputDevice->GetBitmap(Point(aRange.getMinX(), aRange.getMinY()), Size(aRange.getWidth(), aRange.getHeight())); - constexpr double nRadius = 5.0; - bitmap.Scale(Size(aRange.getWidth() - nRadius, aRange.getHeight() - nRadius)); - // use bitmap later as mask - auto mask = bitmap.GetBitmap(); - mpOutputDevice = &aBufferDevice.getContent(); - process(rCandidate); - bitmap = mpOutputDevice->GetBitmapEx(Point(aRange.getMinX(), aRange.getMinY()), - Size(aRange.getWidth(), aRange.getHeight())); - bitmap.Scale(Size(aRange.getWidth() - nRadius, aRange.getHeight() - nRadius)); + BitmapEx mask(bitmap); // copy the bitmap to mask + // Only completely transparent parts will be completely white; only those must be + // considered white on the initial B&W alpha mask. Any other color must be treated + // as black. + BitmapFilter::Filter(mask, BitmapMonochromeFilter(255)); + + // Scaling down increases performance without noticeable quality loss. Additionally, + // current blur implementation can only handle blur radius between 2 and 254. + Size aSize = mask.GetSizePixel(); + double fScale = 1.0; + // Glow radius is the size of the halo from each side of the object. The halo is the + // border of glow color that fades from glow transparency level to fully transparent + // When blurring a sharp boundary (our case), it gets 50% of original intensity, and + // fades to both sides by the blur radius; thus blur radius is half of glow radius. + double fBlurRadius = fGlowRadius / 2; + while (fBlurRadius > 254 || aSize.Height() > 1000 || aSize.Width() > 1000) + { + fScale /= 2; + fBlurRadius /= 2; + aSize.setHeight(aSize.Height() / 2); + aSize.setWidth(aSize.Width() / 2); + } + + // BmpScaleFlag::Fast is important for following color replacement + mask.Scale(fScale, fScale, BmpScaleFlag::Fast); + + // Dilate the black pixels using blur radius, to make blur start at actual object margins. + // This differentiates glow from blurry shadow; so potentially extend this function to also + // handle blurry shadow, and conditionally skip this step + BitmapFilter::Filter(mask, BitmapDilateFilter(fBlurRadius)); + + // We need 8-bit grey mask for blurring + mask.Convert(BmpConversion::N8BitGreys); + // Consider glow transparency (initial transparency near the object edge) + const sal_uInt8 nTransparency = rCandidate.getGlowColor().GetTransparency(); + const Color aTransparency(nTransparency, nTransparency, nTransparency); + BitmapFilter::Filter(mask, BitmapColorReplaceFilter(COL_BLACK, aTransparency)); // calculate blurry effect - BitmapFilterStackBlur glowFilter(nRadius); - BitmapFilter::Filter(bitmap, glowFilter); + BitmapFilter::Filter(mask, BitmapFilterStackBlur(fBlurRadius)); + + // The end result is the bitmap filled with glow color and blurred 8-bit alpha mask + bitmap.Erase(rCandidate.getGlowColor()); + // alpha mask will be scaled up automatically to match bitmap + BitmapEx result(bitmap, AlphaMask(mask.GetBitmap())); + // back to old OutDev mpOutputDevice = pLastOutputDevice; - mpOutputDevice->DrawBitmapEx( - Point(aRange.getMinX() - nRadius / 2, aRange.getMinY() - nRadius / 2), - BitmapEx(bitmap.GetBitmap(), mask)); - - // paint result - //aBufferDevice.paint(); + mpOutputDevice->DrawBitmapEx(Point(aRange.getMinX(), aRange.getMinY()), result); } else SAL_WARN("drawinglayer", "Temporary buffered virtual device is not visible"); |