summaryrefslogtreecommitdiff
path: root/drawinglayer
diff options
context:
space:
mode:
authorMike Kaganski <mike.kaganski@collabora.com>2020-05-01 01:05:24 +0300
committerMike Kaganski <mike.kaganski@collabora.com>2020-05-07 21:57:37 +0200
commit08ebcff89f56bec3b0f9b346504748e4eb1687af (patch)
treed511115b648fd5f2340fd88eacc4ad9971ad7348 /drawinglayer
parenta98bdbae459ad7341bf7f484c402e77e4062cd16 (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.cxx27
-rw-r--r--drawinglayer/source/primitive2d/glowprimitive2d.cxx24
-rw-r--r--drawinglayer/source/processor2d/vclpixelprocessor2d.cxx80
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");