Bug 1155828 - Draw box-shadows using an approach inspired by border-image. r=mstange
authorMason Chang <mchang@mozilla.com>
Wed, 13 May 2015 15:19:27 -0400
changeset 243808 fde31eaa2638
parent 243807 f9a7c383cd05
child 243809 212cc3156d16
push id28753
push userkwierso@gmail.com
push dateThu, 14 May 2015 22:33:43 +0000
treeherdermozilla-central@07e2e15703cb [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmstange
bugs1155828
milestone41.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1155828 - Draw box-shadows using an approach inspired by border-image. r=mstange
gfx/thebes/gfxBlur.cpp
layout/reftests/bugs/1155828-1-ref.html
layout/reftests/bugs/1155828-1.html
layout/reftests/bugs/reftest.list
--- a/gfx/thebes/gfxBlur.cpp
+++ b/gfx/thebes/gfxBlur.cpp
@@ -9,16 +9,17 @@
 #include "gfxContext.h"
 #include "gfxPlatform.h"
 #include "mozilla/gfx/2D.h"
 #include "mozilla/gfx/Blur.h"
 #include "mozilla/gfx/PathHelpers.h"
 #include "mozilla/UniquePtr.h"
 #include "nsExpirationTracker.h"
 #include "nsClassHashtable.h"
+#include "gfxUtils.h"
 
 using namespace mozilla;
 using namespace mozilla::gfx;
 
 gfxAlphaBoxBlur::gfxAlphaBoxBlur()
 {
 }
 
@@ -356,57 +357,259 @@ CacheBlur(DrawTarget *aDT,
 
 void
 gfxAlphaBoxBlur::ShutdownBlurCache()
 {
   delete gBlurCache;
   gBlurCache = nullptr;
 }
 
+static IntSize
+ComputeMinimalSizeForShadowShape(RectCornerRadii* aCornerRadii,
+                                 gfxIntSize aBlurRadius,
+                                 IntMargin& aSlice)
+{
+  float cornerWidth = 0;
+  float cornerHeight = 0;
+  if (aCornerRadii) {
+    RectCornerRadii corners = *aCornerRadii;
+    for (size_t i = 0; i < 4; i++) {
+      cornerWidth = std::max(cornerWidth, corners[i].width);
+      cornerHeight = std::max(cornerHeight, corners[i].height);
+    }
+  }
+
+  aSlice = IntMargin(ceil(cornerHeight) + aBlurRadius.height,
+                     ceil(cornerWidth) + aBlurRadius.width,
+                     ceil(cornerHeight) + aBlurRadius.height,
+                     ceil(cornerWidth) + aBlurRadius.width);
+
+  // Include 1 pixel for the stretchable strip in the middle.
+  return IntSize(aSlice.LeftRight() + 1,
+                 aSlice.TopBottom() + 1);
+}
+
+// Blurs a small surface and creates the mask.
+static TemporaryRef<SourceSurface>
+CreateBlurMask(const IntSize& aRectSize,
+               RectCornerRadii* aCornerRadii,
+               gfxIntSize aBlurRadius,
+               IntMargin& aExtendDestBy,
+               IntMargin& aSliceBorder,
+               DrawTarget& aDestDrawTarget)
+{
+  IntMargin slice;
+  IntSize minimalSize =
+    ComputeMinimalSizeForShadowShape(aCornerRadii, aBlurRadius, slice);
+
+  // If aRectSize is smaller than minimalSize, the border-image approach won't
+  // work; there's no way to squeeze parts of the minimal box-shadow source
+  // image such that the result looks correct. So we need to adjust minimalSize
+  // in such a way that we can later draw it without stretching in the affected
+  // dimension. We also need to adjust "slice" to ensure that we're not trying
+  // to slice away more than we have.
+  if (aRectSize.width < minimalSize.width) {
+    minimalSize.width = aRectSize.width;
+    slice.left = 0;
+    slice.right = 0;
+  }
+  if (aRectSize.height < minimalSize.height) {
+    minimalSize.height = aRectSize.height;
+    slice.top = 0;
+    slice.bottom = 0;
+  }
+
+  MOZ_ASSERT(slice.LeftRight() <= minimalSize.width);
+  MOZ_ASSERT(slice.TopBottom() <= minimalSize.height);
+
+  IntRect minimalRect(IntPoint(), minimalSize);
+
+  gfxAlphaBoxBlur blur;
+  gfxContext* blurCtx = blur.Init(ThebesRect(Rect(minimalRect)), gfxIntSize(),
+                                  aBlurRadius, nullptr, nullptr);
+  if (!blurCtx) {
+    return nullptr;
+  }
+
+  DrawTarget* blurDT = blurCtx->GetDrawTarget();
+  ColorPattern black(Color(0.f, 0.f, 0.f, 1.f));
+
+  if (aCornerRadii) {
+    RefPtr<Path> roundedRect =
+      MakePathForRoundedRect(*blurDT, Rect(minimalRect), *aCornerRadii);
+    blurDT->Fill(roundedRect, black);
+  } else {
+    blurDT->FillRect(Rect(minimalRect), black);
+  }
+
+  IntPoint topLeft;
+  RefPtr<SourceSurface> result = blur.DoBlur(&aDestDrawTarget, &topLeft);
+
+  IntRect expandedMinimalRect(topLeft, result->GetSize());
+  aExtendDestBy = expandedMinimalRect - minimalRect;
+  aSliceBorder = slice + aExtendDestBy;
+
+  MOZ_ASSERT(aSliceBorder.LeftRight() <= expandedMinimalRect.width);
+  MOZ_ASSERT(aSliceBorder.TopBottom() <= expandedMinimalRect.height);
+
+  return result.forget();
+}
+
+static TemporaryRef<SourceSurface>
+CreateBoxShadow(SourceSurface* aBlurMask, const gfxRGBA& aShadowColor)
+{
+  IntSize blurredSize = aBlurMask->GetSize();
+  gfxPlatform* platform = gfxPlatform::GetPlatform();
+  RefPtr<DrawTarget> boxShadowDT =
+    platform->CreateOffscreenContentDrawTarget(blurredSize, SurfaceFormat::B8G8R8A8);
+
+  ColorPattern shadowColor(ToDeviceColor(aShadowColor));
+  boxShadowDT->MaskSurface(shadowColor, aBlurMask, Point(0, 0));
+  return boxShadowDT->Snapshot();
+}
+
+static Rect
+RectWithEdgesTRBL(Float aTop, Float aRight, Float aBottom, Float aLeft)
+{
+  return Rect(aLeft, aTop, aRight - aLeft, aBottom - aTop);
+}
+
+static void
+RepeatOrStretchSurface(DrawTarget& aDT, SourceSurface* aSurface,
+                       const Rect& aDest, const Rect& aSrc)
+{
+  if (!aDT.GetTransform().IsRectilinear() &&
+      aDT.GetBackendType() != BackendType::CAIRO) {
+    // Use stretching if possible, since it leads to less seams when the
+    // destination is transformed. However, don't do this if we're using cairo,
+    // because if cairo is using pixman it won't render anything for large
+    // stretch factors because pixman's internal fixed point precision is not
+    // high enough to handle those scale factors.
+    aDT.DrawSurface(aSurface, aDest, aSrc);
+    return;
+  }
+
+  SurfacePattern pattern(aSurface, ExtendMode::REPEAT,
+                         Matrix::Translation(aDest.TopLeft() - aSrc.TopLeft()),
+                         Filter::GOOD, RoundedToInt(aSrc));
+  aDT.FillRect(aDest, pattern);
+}
+
+/***
+ * We draw a blurred a rectangle by only blurring a smaller rectangle and
+ * splitting the rectangle into 9 parts.
+ * First, a small minimum source rect is calculated and used to create a blur
+ * mask since the actual blurring itself is expensive. Next, we use the mask
+ * with the given shadow color to create a minimally-sized box shadow of the
+ * right color. Finally, we cut out the 9 parts from the box-shadow source and
+ * paint each part in the right place, stretching the non-corner parts to fill
+ * the space between the corners.
+ */
 /* static */ void
 gfxAlphaBoxBlur::BlurRectangle(gfxContext *aDestinationCtx,
                                const gfxRect& aRect,
                                RectCornerRadii* aCornerRadii,
                                const gfxPoint& aBlurStdDev,
                                const gfxRGBA& aShadowColor,
                                const gfxRect& aDirtyRect,
                                const gfxRect& aSkipRect)
 {
-  DrawTarget& aDrawTarget = *aDestinationCtx->GetDrawTarget();
+  DrawTarget& destDrawTarget = *aDestinationCtx->GetDrawTarget();
 
   gfxIntSize blurRadius = CalculateBlurRadius(aBlurStdDev);
 
-  IntPoint topLeft;
-  RefPtr<SourceSurface> surface = GetCachedBlur(&aDrawTarget, aRect, blurRadius, aSkipRect, aDirtyRect, &topLeft);
-  if (!surface) {
-    // Create the temporary surface for blurring
-    gfxAlphaBoxBlur blur;
-    gfxContext* blurCtx = blur.Init(aRect, gfxIntSize(), blurRadius, &aDirtyRect, &aSkipRect);
-    if (!blurCtx) {
-      return;
-    }
-    DrawTarget* blurDT = blurCtx->GetDrawTarget();
-
-    Rect shadowGfxRect = ToRect(aRect);
-    shadowGfxRect.Round();
-
-    ColorPattern black(Color(0.f, 0.f, 0.f, 1.f)); // For masking, so no ToDeviceColor!
-    if (aCornerRadii) {
-      RefPtr<Path> roundedRect = MakePathForRoundedRect(*blurDT,
-                                                        shadowGfxRect,
-                                                        *aCornerRadii);
-      blurDT->Fill(roundedRect, black);
-    } else {
-      blurDT->FillRect(shadowGfxRect, black);
-    }
-
-    surface = blur.DoBlur(&aDrawTarget, &topLeft);
-    if (!surface) {
-      return;
-    }
-    CacheBlur(&aDrawTarget, aRect, blurRadius, aSkipRect, surface, topLeft, aDirtyRect);
+  IntRect rect = RoundedToInt(ToRect(aRect));
+  IntMargin extendDestBy;
+  IntMargin slice;
+  RefPtr<SourceSurface> blurMask =
+    CreateBlurMask(rect.Size(), aCornerRadii, blurRadius, extendDestBy, slice,
+                   destDrawTarget);
+  if (!blurMask) {
+    return;
   }
 
-  aDestinationCtx->SetColor(aShadowColor);
-  Rect dirtyRect(aDirtyRect.x, aDirtyRect.y, aDirtyRect.width, aDirtyRect.height);
-  DrawBlur(aDestinationCtx, surface, topLeft, &dirtyRect);
+  RefPtr<SourceSurface> boxShadow = CreateBoxShadow(blurMask, aShadowColor);
+
+  destDrawTarget.PushClipRect(ToRect(aDirtyRect));
+
+  // Copy the right parts from boxShadow into destDrawTarget. The middle parts
+  // will be stretched, border-image style.
+
+  Rect srcOuter(Point(), Size(boxShadow->GetSize()));
+  Rect srcInner = srcOuter;
+  srcInner.Deflate(Margin(slice));
+
+  rect.Inflate(extendDestBy);
+  Rect dstOuter(rect);
+  Rect dstInner(rect);
+  dstInner.Deflate(Margin(slice));
+
+  // Corners: top left, top right, bottom left, bottom right
+  destDrawTarget.DrawSurface(boxShadow,
+                             RectWithEdgesTRBL(dstOuter.Y(), dstInner.X(),
+                                               dstInner.Y(), dstOuter.X()),
+                             RectWithEdgesTRBL(srcOuter.Y(), srcInner.X(),
+                                               srcInner.Y(), srcOuter.X()));
+  destDrawTarget.DrawSurface(boxShadow,
+                             RectWithEdgesTRBL(dstOuter.Y(), dstOuter.XMost(),
+                                               dstInner.Y(), dstInner.XMost()),
+                             RectWithEdgesTRBL(srcOuter.Y(), srcOuter.XMost(),
+                                               srcInner.Y(), srcInner.XMost()));
+  destDrawTarget.DrawSurface(boxShadow,
+                             RectWithEdgesTRBL(dstInner.YMost(), dstInner.X(),
+                                               dstOuter.YMost(), dstOuter.X()),
+                             RectWithEdgesTRBL(srcInner.YMost(), srcInner.X(),
+                                               srcOuter.YMost(), srcOuter.X()));
+  destDrawTarget.DrawSurface(boxShadow,
+                             RectWithEdgesTRBL(dstInner.YMost(), dstOuter.XMost(),
+                                               dstOuter.YMost(), dstInner.XMost()),
+                             RectWithEdgesTRBL(srcInner.YMost(), srcOuter.XMost(),
+                                               srcOuter.YMost(), srcInner.XMost()));
+
+  // Edges: top, left, right, bottom
+  RepeatOrStretchSurface(destDrawTarget, boxShadow,
+                         RectWithEdgesTRBL(dstOuter.Y(), dstInner.XMost(),
+                                           dstInner.Y(), dstInner.X()),
+                         RectWithEdgesTRBL(srcOuter.Y(), srcInner.XMost(),
+                                           srcInner.Y(), srcInner.X()));
+  RepeatOrStretchSurface(destDrawTarget, boxShadow,
+                         RectWithEdgesTRBL(dstInner.Y(), dstInner.X(),
+                                           dstInner.YMost(), dstOuter.X()),
+                         RectWithEdgesTRBL(srcInner.Y(), srcInner.X(),
+                                           srcInner.YMost(), srcOuter.X()));
+  RepeatOrStretchSurface(destDrawTarget, boxShadow,
+                         RectWithEdgesTRBL(dstInner.Y(), dstOuter.XMost(),
+                                           dstInner.YMost(), dstInner.XMost()),
+                         RectWithEdgesTRBL(srcInner.Y(), srcOuter.XMost(),
+                                           srcInner.YMost(), srcInner.XMost()));
+  RepeatOrStretchSurface(destDrawTarget, boxShadow,
+                         RectWithEdgesTRBL(dstInner.YMost(), dstInner.XMost(),
+                                           dstOuter.YMost(), dstInner.X()),
+                         RectWithEdgesTRBL(srcInner.YMost(), srcInner.XMost(),
+                                           srcOuter.YMost(), srcInner.X()));
+
+  // Middle part
+  RepeatOrStretchSurface(destDrawTarget, boxShadow,
+                         RectWithEdgesTRBL(dstInner.Y(), dstInner.XMost(),
+                                           dstInner.YMost(), dstInner.X()),
+                         RectWithEdgesTRBL(srcInner.Y(), srcInner.XMost(),
+                                           srcInner.YMost(), srcInner.X()));
+
+  // A note about anti-aliasing and seems between adjacent parts:
+  // We don't explicitly disable anti-aliasing in the DrawSurface calls above,
+  // so if there's a transform on destDrawTarget that is not pixel-aligned,
+  // there will be seams between adjacent parts of the box-shadow. It's hard to
+  // avoid those without the use of an intermediate surface.
+  // You might think that we could avoid those by just turning of AA, but there
+  // is a problem with that: Box-shadow rendering needs to clip out the
+  // element's border box, and we'd like that clip to have anti-aliasing -
+  // especially if the element has rounded corners! So we can't do that unless
+  // we have a way to say "Please anti-alias the clip, but don't antialias the
+  // destination rect of the DrawSurface call".
+  // On OS X there is an additional problem with turning off AA: CoreGraphics
+  // will not just fill the pixels that have their pixel center inside the
+  // filled shape. Instead, it will fill all the pixels which are partially
+  // covered by the shape. So for pixels on the edge between two adjacent parts,
+  // all those pixels will be painted to by both parts, which looks very bad.
+
+  destDrawTarget.PopClip();
 }
 
new file mode 100644
--- /dev/null
+++ b/layout/reftests/bugs/1155828-1-ref.html
@@ -0,0 +1,25 @@
+<!DOCTYPE HTML>
+<html class="reftest-wait">
+<head>
+<style type="text/css"> 
+#rear {
+  width: 500px;
+  height: 1500px;
+  box-shadow: 0 0 71px #667;
+  display: block;
+}
+</style>
+<script>
+document.documentElement.scrollTop = 0;
+
+function doTest() {
+  document.documentElement.scrollTop = 108;
+  document.documentElement.removeAttribute("class");
+}
+window.addEventListener("MozReftestInvalidate", doTest);
+</script>
+</head>
+<body>
+<div id="rear"></div>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/layout/reftests/bugs/1155828-1.html
@@ -0,0 +1,26 @@
+<!DOCTYPE HTML>
+<html class="reftest-wait">
+<meta charset="utf-8">
+<title>Scrolling shouldn't cause gaps in the shadow</title>
+<style type="text/css"> 
+#rear {
+	width: 500px;
+	height: 1500px;
+	box-shadow: 0 0 71px #667;
+	display: block;
+}
+</style>
+
+<div id="rear"></div>
+
+<script>
+
+function doTest() {
+  document.documentElement.scrollTop = 108;
+  document.documentElement.removeAttribute("class");
+}
+window.addEventListener("MozReftestInvalidate", doTest);
+
+document.documentElement.scrollTop = 112;
+
+</script>
--- a/layout/reftests/bugs/reftest.list
+++ b/layout/reftests/bugs/reftest.list
@@ -1918,9 +1918,10 @@ skip-if(!asyncPanZoom) fuzzy-if(B2G,22,1
 skip-if(!asyncPanZoom) fuzzy-if(B2G,62,175) == 1133905-2-vh-rtl.html 1133905-ref-vh-rtl.html
 skip-if(!asyncPanZoom) fuzzy-if(B2G,23,174) == 1133905-3-vh-rtl.html 1133905-ref-vh-rtl.html
 skip-if(!asyncPanZoom) == 1133905-4-vh-rtl.html 1133905-ref-vh-rtl.html
 skip-if(!asyncPanZoom) fuzzy-if(B2G,102,545) == 1133905-5-vh-rtl.html 1133905-ref-vh-rtl.html
 skip-if(!asyncPanZoom) fuzzy-if(B2G,101,887) == 1133905-6-vh-rtl.html 1133905-ref-vh-rtl.html
 skip-if(B2G||Mulet) == 1150021-1.xul 1150021-1-ref.xul
 == 1151145-1.html 1151145-1-ref.html
 == 1151306-1.html 1151306-1-ref.html
+== 1155828-1.html 1155828-1-ref.html
 == 1156129-1.html 1156129-1-ref.html