summaryrefslogtreecommitdiff
path: root/drawinglayer/source/primitive2d/glowprimitive2d.cxx
diff options
context:
space:
mode:
Diffstat (limited to 'drawinglayer/source/primitive2d/glowprimitive2d.cxx')
-rw-r--r--drawinglayer/source/primitive2d/glowprimitive2d.cxx307
1 files changed, 302 insertions, 5 deletions
diff --git a/drawinglayer/source/primitive2d/glowprimitive2d.cxx b/drawinglayer/source/primitive2d/glowprimitive2d.cxx
index f6dfab2b3c45..6bf9dea8af83 100644
--- a/drawinglayer/source/primitive2d/glowprimitive2d.cxx
+++ b/drawinglayer/source/primitive2d/glowprimitive2d.cxx
@@ -18,23 +18,38 @@
*/
#include <drawinglayer/primitive2d/glowprimitive2d.hxx>
+#include <drawinglayer/primitive2d/transformprimitive2d.hxx>
#include <drawinglayer/primitive2d/drawinglayer_primitivetypes2d.hxx>
+#include <drawinglayer/primitive2d/bitmapprimitive2d.hxx>
+#include <basegfx/matrix/b2dhommatrixtools.hxx>
+#include <toolkit/helper/vclunohelper.hxx>
+#include <drawinglayer/converters.hxx>
+#include "GlowSoftEgdeShadowTools.hxx"
+
+#ifdef DBG_UTIL
+#include <tools/stream.hxx>
+#include <vcl/filter/PngImageWriter.hxx>
+#endif
using namespace com::sun::star;
namespace drawinglayer::primitive2d
{
GlowPrimitive2D::GlowPrimitive2D(const Color& rGlowColor, double fRadius,
- const Primitive2DContainer& rChildren)
- : GroupPrimitive2D(rChildren)
+ Primitive2DContainer&& rChildren)
+ : BufferedDecompositionGroupPrimitive2D(std::move(rChildren))
, maGlowColor(rGlowColor)
, mfGlowRadius(fRadius)
+ , mfLastDiscreteGlowRadius(0.0)
+ , maLastClippedRange()
{
+ // activate callback to flush buffered decomposition content
+ setCallbackSeconds(15);
}
bool GlowPrimitive2D::operator==(const BasePrimitive2D& rPrimitive) const
{
- if (BasePrimitive2D::operator==(rPrimitive))
+ if (BufferedDecompositionGroupPrimitive2D::operator==(rPrimitive))
{
const GlowPrimitive2D& rCompare = static_cast<const GlowPrimitive2D&>(rPrimitive);
@@ -45,17 +60,299 @@ bool GlowPrimitive2D::operator==(const BasePrimitive2D& rPrimitive) const
return false;
}
+bool GlowPrimitive2D::prepareValuesAndcheckValidity(
+ basegfx::B2DRange& rGlowRange, basegfx::B2DRange& rClippedRange,
+ basegfx::B2DVector& rDiscreteGlowSize, double& rfDiscreteGlowRadius,
+ const geometry::ViewInformation2D& rViewInformation) const
+{
+ // no GlowRadius defined, done
+ if (getGlowRadius() <= 0.0)
+ return false;
+
+ // no geometry, done
+ if (getChildren().empty())
+ return false;
+
+ // no pixel target, done
+ if (rViewInformation.getObjectToViewTransformation().isIdentity())
+ return false;
+
+ // get geometry range that defines area that needs to be pixelated
+ rGlowRange = getChildren().getB2DRange(rViewInformation);
+
+ // no range of geometry, done
+ if (rGlowRange.isEmpty())
+ return false;
+
+ // extend range by GlowRadius in all directions
+ rGlowRange.grow(getGlowRadius());
+
+ // initialize ClippedRange to full GlowRange -> all is visible
+ rClippedRange = rGlowRange;
+
+ // get Viewport and check if used. If empty, all is visible (see
+ // ViewInformation2D definition in viewinformation2d.hxx)
+ if (!rViewInformation.getViewport().isEmpty())
+ {
+ // if used, extend by GlowRadius to ensure needed parts are included
+ basegfx::B2DRange aVisibleArea(rViewInformation.getViewport());
+ aVisibleArea.grow(getGlowRadius());
+
+ // To do this correctly, it needs to be done in discrete coordinates.
+ // The object may be transformed relative to the original#
+ // ObjectTransformation, e.g. when re-used in shadow
+ aVisibleArea.transform(rViewInformation.getViewTransformation());
+ rClippedRange.transform(rViewInformation.getObjectToViewTransformation());
+
+ // calculate ClippedRange
+ rClippedRange.intersect(aVisibleArea);
+
+ // if GlowRange is completely outside of VisibleArea, ClippedRange
+ // will be empty and we are done
+ if (rClippedRange.isEmpty())
+ return false;
+
+ // convert result back to object coordinates
+ rClippedRange.transform(rViewInformation.getInverseObjectToViewTransformation());
+ }
+
+ // calculate discrete pixel size of GlowRange. If it's too small to visualize, we are done
+ rDiscreteGlowSize = rViewInformation.getObjectToViewTransformation() * rGlowRange.getRange();
+ if (ceil(rDiscreteGlowSize.getX()) < 2.0 || ceil(rDiscreteGlowSize.getY()) < 2.0)
+ return false;
+
+ // calculate discrete pixel size of GlowRadius. If it's too small to visualize, we are done
+ rfDiscreteGlowRadius = ceil(
+ (rViewInformation.getObjectToViewTransformation() * basegfx::B2DVector(getGlowRadius(), 0))
+ .getLength());
+ if (rfDiscreteGlowRadius < 1.0)
+ return false;
+
+ return true;
+}
+
+void GlowPrimitive2D::create2DDecomposition(
+ Primitive2DContainer& rContainer, const geometry::ViewInformation2D& rViewInformation) const
+{
+ basegfx::B2DRange aGlowRange;
+ basegfx::B2DRange aClippedRange;
+ basegfx::B2DVector aDiscreteGlowSize;
+ double fDiscreteGlowRadius(0.0);
+
+ // Check various validity details and calculate/prepare values. If false, we are done
+ if (!prepareValuesAndcheckValidity(aGlowRange, aClippedRange, aDiscreteGlowSize,
+ fDiscreteGlowRadius, rViewInformation))
+ return;
+
+ // Create embedding transformation from object to top-left zero-aligned
+ // target pixel geometry (discrete form of ClippedRange)
+ // First, move to top-left of GlowRange
+ const sal_uInt32 nDiscreteGlowWidth(ceil(aDiscreteGlowSize.getX()));
+ const sal_uInt32 nDiscreteGlowHeight(ceil(aDiscreteGlowSize.getY()));
+ basegfx::B2DHomMatrix aEmbedding(basegfx::utils::createTranslateB2DHomMatrix(
+ -aClippedRange.getMinX(), -aClippedRange.getMinY()));
+ // Second, scale to discrete bitmap size
+ // Even when using the offset from ClippedRange, we need to use the
+ // scaling from the full representation, thus from GlowRange
+ aEmbedding.scale(nDiscreteGlowWidth / aGlowRange.getWidth(),
+ nDiscreteGlowHeight / aGlowRange.getHeight());
+
+ // Embed content graphics to TransformPrimitive2D
+ const primitive2d::Primitive2DReference xEmbedRef(
+ new primitive2d::TransformPrimitive2D(aEmbedding, Primitive2DContainer(getChildren())));
+ primitive2d::Primitive2DContainer xEmbedSeq{ xEmbedRef };
+
+ // Create BitmapEx using drawinglayer tooling, including a MaximumQuadraticPixel
+ // limitation to be safe and not go runtime/memory havoc. Use a pretty small
+ // limit due to this is glow functionality and will look good with bitmap scaling
+ // anyways. The value of 250.000 square pixels below maybe adapted as needed.
+ const basegfx::B2DVector aDiscreteClippedSize(rViewInformation.getObjectToViewTransformation()
+ * aClippedRange.getRange());
+ const sal_uInt32 nDiscreteClippedWidth(ceil(aDiscreteClippedSize.getX()));
+ const sal_uInt32 nDiscreteClippedHeight(ceil(aDiscreteClippedSize.getY()));
+ const geometry::ViewInformation2D aViewInformation2D;
+ const sal_uInt32 nMaximumQuadraticPixels(250000);
+
+ // I have now added a helper that just creates the mask without having
+ // to render the content, use it, it's faster
+ const AlphaMask aAlpha(::drawinglayer::createAlphaMask(
+ std::move(xEmbedSeq), aViewInformation2D, nDiscreteClippedWidth, nDiscreteClippedHeight,
+ nMaximumQuadraticPixels));
+
+ if (aAlpha.IsEmpty())
+ return;
+
+ const Size& rBitmapExSizePixel(aAlpha.GetSizePixel());
+
+ if (rBitmapExSizePixel.Width() <= 0 || rBitmapExSizePixel.Height() <= 0)
+ return;
+
+ // We may have to take a corrective scaling into account when the
+ // MaximumQuadraticPixel limit was used/triggered
+ double fScale(1.0);
+
+ if (static_cast<sal_uInt32>(rBitmapExSizePixel.Width()) != nDiscreteClippedWidth
+ || static_cast<sal_uInt32>(rBitmapExSizePixel.Height()) != nDiscreteClippedHeight)
+ {
+ // scale in X and Y should be the same (see fReduceFactor in createAlphaMask),
+ // so adapt numerically to a single scale value, they are integer rounded values
+ const double fScaleX(static_cast<double>(rBitmapExSizePixel.Width())
+ / static_cast<double>(nDiscreteClippedWidth));
+ const double fScaleY(static_cast<double>(rBitmapExSizePixel.Height())
+ / static_cast<double>(nDiscreteClippedHeight));
+
+ fScale = (fScaleX + fScaleY) * 0.5;
+ }
+
+ // fDiscreteGlowRadius 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.
+ // Consider glow transparency (initial transparency near the object edge)
+ AlphaMask mask(ProcessAndBlurAlphaMask(aAlpha, fDiscreteGlowRadius * fScale / 2.0,
+ fDiscreteGlowRadius * fScale / 2.0,
+ 255 - getGlowColor().GetAlpha()));
+
+ // The end result is the bitmap filled with glow color and blurred 8-bit alpha mask
+ Bitmap bmp(aAlpha.GetSizePixel(), vcl::PixelFormat::N24_BPP);
+ bmp.Erase(getGlowColor());
+ BitmapEx result(bmp, mask);
+
+#ifdef DBG_UTIL
+ static bool bDoSaveForVisualControl(false); // loplugin:constvars:ignore
+ if (bDoSaveForVisualControl)
+ {
+ // VCL_DUMP_BMP_PATH should be like C:/path/ or ~/path/
+ static const OUString sDumpPath(
+ OUString::createFromAscii(std::getenv("VCL_DUMP_BMP_PATH")));
+ if (!sDumpPath.isEmpty())
+ {
+ SvFileStream aNew(sDumpPath + "test_glow.png", StreamMode::WRITE | StreamMode::TRUNC);
+ vcl::PngImageWriter aPNGWriter(aNew);
+ aPNGWriter.write(result);
+ }
+ }
+#endif
+
+ // Independent from discrete sizes of glow alpha creation, always
+ // map and project glow result to geometry range extended by glow
+ // radius, but to the eventually clipped instance (ClippedRange)
+ const primitive2d::Primitive2DReference xEmbedRefBitmap(
+ new BitmapPrimitive2D(result, basegfx::utils::createScaleTranslateB2DHomMatrix(
+ aClippedRange.getWidth(), aClippedRange.getHeight(),
+ aClippedRange.getMinX(), aClippedRange.getMinY())));
+
+ rContainer = primitive2d::Primitive2DContainer{ xEmbedRefBitmap };
+}
+
+// Using tooling class BufferedDecompositionGroupPrimitive2D now, so
+// no more need to locally do the buffered get2DDecomposition here,
+// see BufferedDecompositionGroupPrimitive2D::get2DDecomposition
+void GlowPrimitive2D::get2DDecomposition(Primitive2DDecompositionVisitor& rVisitor,
+ const geometry::ViewInformation2D& rViewInformation) const
+{
+ basegfx::B2DRange aGlowRange;
+ basegfx::B2DRange aClippedRange;
+ basegfx::B2DVector aDiscreteGlowSize;
+ double fDiscreteGlowRadius(0.0);
+
+ // Check various validity details and calculate/prepare values. If false, we are done
+ if (!prepareValuesAndcheckValidity(aGlowRange, aClippedRange, aDiscreteGlowSize,
+ fDiscreteGlowRadius, rViewInformation))
+ return;
+
+ if (!getBuffered2DDecomposition().empty())
+ {
+ // First check is to detect if the last created decompose is capable
+ // to represent the now requested visualization.
+ // ClippedRange is the needed visualizationArea for the current glow
+ // effect, LastClippedRange is the one from the existing/last rendering.
+ // Check if last created area is sufficient and can be re-used
+ if (!maLastClippedRange.isEmpty() && !maLastClippedRange.isInside(aClippedRange))
+ {
+ // To avoid unnecessary invalidations due to being *very* correct
+ // with HairLines (which are view-dependent and thus change the
+ // result(s) here slightly when changing zoom), add a slight unsharp
+ // component if we have a ViewTransform. The derivation is inside
+ // the range of half a pixel (due to one pixel hairline)
+ basegfx::B2DRange aLastClippedRangeAndHairline(maLastClippedRange);
+
+ if (!rViewInformation.getObjectToViewTransformation().isIdentity())
+ {
+ // Grow by view-dependent size of 1/2 pixel
+ const double fHalfPixel((rViewInformation.getInverseObjectToViewTransformation()
+ * basegfx::B2DVector(0.5, 0))
+ .getLength());
+ aLastClippedRangeAndHairline.grow(fHalfPixel);
+ }
+
+ if (!aLastClippedRangeAndHairline.isInside(aClippedRange))
+ {
+ // Conditions of last local decomposition have changed, delete
+ const_cast<GlowPrimitive2D*>(this)->setBuffered2DDecomposition(
+ Primitive2DContainer());
+ }
+ }
+ }
+
+ if (!getBuffered2DDecomposition().empty())
+ {
+ // Second check is to react on changes of the DiscreteGlowRadius when
+ // zooming in/out.
+ // Use the known last and current DiscreteGlowRadius to decide
+ // if the visualization can be re-used. Be a little 'creative' here
+ // and make it dependent on a *relative* change - it is not necessary
+ // to re-create everytime if the exact value is missed since zooming
+ // pixel-based glow effect is pretty good due to it's smooth nature
+ bool bFree(mfLastDiscreteGlowRadius <= 0.0 || fDiscreteGlowRadius <= 0.0);
+
+ if (!bFree)
+ {
+ const double fDiff(fabs(mfLastDiscreteGlowRadius - fDiscreteGlowRadius));
+ const double fLen(fabs(mfLastDiscreteGlowRadius) + fabs(fDiscreteGlowRadius));
+ const double fRelativeChange(fDiff / fLen);
+
+ // Use lower fixed values here to change more often, higher to change less often.
+ // Value is in the range of ]0.0 .. 1.0]
+ bFree = fRelativeChange >= 0.15;
+ }
+
+ if (bFree)
+ {
+ // Conditions of last local decomposition have changed, delete
+ const_cast<GlowPrimitive2D*>(this)->setBuffered2DDecomposition(Primitive2DContainer());
+ }
+ }
+
+ if (getBuffered2DDecomposition().empty())
+ {
+ // refresh last used DiscreteGlowRadius and ClippedRange to new remembered values
+ const_cast<GlowPrimitive2D*>(this)->mfLastDiscreteGlowRadius = fDiscreteGlowRadius;
+ const_cast<GlowPrimitive2D*>(this)->maLastClippedRange = aClippedRange;
+ }
+
+ // call parent, that will check for empty, call create2DDecomposition and
+ // set as decomposition
+ BufferedDecompositionGroupPrimitive2D::get2DDecomposition(rVisitor, rViewInformation);
+}
+
basegfx::B2DRange
GlowPrimitive2D::getB2DRange(const geometry::ViewInformation2D& rViewInformation) const
{
- basegfx::B2DRange aRetval(GroupPrimitive2D::getB2DRange(rViewInformation));
+ // Hint: Do *not* use GroupPrimitive2D::getB2DRange, that will (unnecessarily)
+ // use the decompose - what works, but is not needed here.
+ // We know the to-be-visualized geometry and the radius it needs to be extended,
+ // so simply calculate the exact needed range.
+ basegfx::B2DRange aRetval(getChildren().getB2DRange(rViewInformation));
+
// We need additional space for the glow from all sides
aRetval.grow(getGlowRadius());
+
return aRetval;
}
// provide unique ID
-ImplPrimitive2DIDBlock(GlowPrimitive2D, PRIMITIVE2D_ID_GLOWPRIMITIVE2D)
+sal_uInt32 GlowPrimitive2D::getPrimitive2DID() const { return PRIMITIVE2D_ID_GLOWPRIMITIVE2D; }
} // end of namespace