diff --git a/lib/web_ui/dev/goldens_lock.yaml b/lib/web_ui/dev/goldens_lock.yaml index 1b73145f48a3b..9d981a4c66700 100644 --- a/lib/web_ui/dev/goldens_lock.yaml +++ b/lib/web_ui/dev/goldens_lock.yaml @@ -1,2 +1,2 @@ repository: https://github.com/flutter/goldens.git -revision: 8f692819e8881b7d2131dbd61d965c21d5e3e345 +revision: 1699ba6fd7093a0a610f82618fa30546e7974777 diff --git a/lib/web_ui/lib/src/engine/bitmap_canvas.dart b/lib/web_ui/lib/src/engine/bitmap_canvas.dart index c9fdb211b9677..bc589172e4be8 100644 --- a/lib/web_ui/lib/src/engine/bitmap_canvas.dart +++ b/lib/web_ui/lib/src/engine/bitmap_canvas.dart @@ -353,16 +353,17 @@ class BitmapCanvas extends EngineCanvas { @override void drawImage(ui.Image image, ui.Offset p, SurfacePaintData paint) { - _drawImage(image, p, paint); + //_applyPaint(paint); + final HtmlImage htmlImage = image; + final html.ImageElement imgElement = htmlImage.cloneImageElement(); + String blendMode = _stringForBlendMode(paint.blendMode); + imgElement.style.mixBlendMode = blendMode; + _drawImage(imgElement, p); _childOverdraw = true; _canvasPool.allocateExtraCanvas(); } - html.ImageElement _drawImage(ui.Image image, ui.Offset p, SurfacePaintData paint) { - final HtmlImage htmlImage = image; - final html.Element imgElement = htmlImage.cloneImageElement(); - final ui.BlendMode blendMode = paint.blendMode; - imgElement.style.mixBlendMode = _stringForBlendMode(blendMode); + void _drawImage(html.ImageElement imgElement, ui.Offset p) { if (_canvasPool.isClipped) { final List clipElements = _clipContent( _canvasPool._clipStack, imgElement, p, _canvasPool.currentTransform); @@ -379,12 +380,12 @@ class BitmapCanvas extends EngineCanvas { rootElement.append(imgElement); _children.add(imgElement); } - return imgElement; } @override void drawImageRect( ui.Image image, ui.Rect src, ui.Rect dst, SurfacePaintData paint) { + final HtmlImage htmlImage = image; final bool requiresClipping = src.left != 0 || src.top != 0 || src.width != image.width || @@ -394,6 +395,9 @@ class BitmapCanvas extends EngineCanvas { !requiresClipping) { drawImage(image, dst.topLeft, paint); } else { + final html.Element imgElement = htmlImage.cloneImageElement(); + final ui.BlendMode blendMode = paint.blendMode; + imgElement.style.mixBlendMode = _stringForBlendMode(blendMode); if (requiresClipping) { save(); clipRect(dst); @@ -410,8 +414,7 @@ class BitmapCanvas extends EngineCanvas { targetTop += topMargin; } } - - final html.ImageElement imgElement = _drawImage(image, ui.Offset(targetLeft, targetTop), paint); + _drawImage(imgElement, ui.Offset(targetLeft, targetTop)); // To scale set width / height on destination image. // For clipping we need to scale according to // clipped-width/full image width and shift it according to left/top of diff --git a/lib/web_ui/lib/src/engine/canvas_pool.dart b/lib/web_ui/lib/src/engine/canvas_pool.dart index b5119afb82c10..9df8d14f9eb82 100644 --- a/lib/web_ui/lib/src/engine/canvas_pool.dart +++ b/lib/web_ui/lib/src/engine/canvas_pool.dart @@ -581,46 +581,49 @@ class _CanvasPool extends _SaveStackTracking { void drawShadow(ui.Path path, ui.Color color, double elevation, bool transparentOccluder) { - final SurfaceShadowData shadow = computeShadow(path.getBounds(), elevation); - if (shadow != null) { - // TODO(het): Shadows with transparent occluders are not supported - // on webkit since filter is unsupported. - if (transparentOccluder && browserEngine != BrowserEngine.webkit) { - // We paint shadows using a path and a mask filter instead of the - // built-in shadow* properties. This is because the color alpha of the - // paint is added to the shadow. The effect we're looking for is to just - // paint the shadow without the path itself, but if we use a non-zero - // alpha for the paint the path is painted in addition to the shadow, - // which is undesirable. - context.save(); - context.translate(shadow.offset.dx, shadow.offset.dy); - context.filter = _maskFilterToCss( - ui.MaskFilter.blur(ui.BlurStyle.normal, shadow.blurWidth)); - context.strokeStyle = ''; - context.fillStyle = colorToCssString(color); - _runPath(context, path); - context.fill(); - context.restore(); - } else { - // TODO(het): We fill the path with this paint, then later we clip - // by the same path and fill it with a fully opaque color (we know - // the color is fully opaque because `transparentOccluder` is false. - // However, due to anti-aliasing of the clip, a few pixels of the - // path we are about to paint may still be visible after we fill with - // the opaque occluder. For that reason, we fill with the shadow color, - // and set the shadow color to fully opaque. This way, the visible - // pixels are less opaque and less noticeable. - context.save(); - context.filter = 'none'; - context.strokeStyle = ''; - context.fillStyle = colorToCssString(color); - context.shadowBlur = shadow.blurWidth; - context.shadowColor = colorToCssString(color.withAlpha(0xff)); - context.shadowOffsetX = shadow.offset.dx; - context.shadowOffsetY = shadow.offset.dy; - _runPath(context, path); - context.fill(); - context.restore(); + final List shadows = + ElevationShadow.computeCanvasShadows(elevation, color); + if (shadows.isNotEmpty) { + for (final CanvasShadow shadow in shadows) { + // TODO(het): Shadows with transparent occluders are not supported + // on webkit since filter is unsupported. + if (transparentOccluder && browserEngine != BrowserEngine.webkit) { + // We paint shadows using a path and a mask filter instead of the + // built-in shadow* properties. This is because the color alpha of the + // paint is added to the shadow. The effect we're looking for is to just + // paint the shadow without the path itself, but if we use a non-zero + // alpha for the paint the path is painted in addition to the shadow, + // which is undesirable. + context.save(); + context.translate(shadow.offsetX, shadow.offsetY); + context.filter = _maskFilterToCss( + ui.MaskFilter.blur(ui.BlurStyle.normal, shadow.blur)); + context.strokeStyle = ''; + context.fillStyle = colorToCssString(shadow.color); + _runPath(context, path); + context.fill(); + context.restore(); + } else { + // TODO(het): We fill the path with this paint, then later we clip + // by the same path and fill it with a fully opaque color (we know + // the color is fully opaque because `transparentOccluder` is false. + // However, due to anti-aliasing of the clip, a few pixels of the + // path we are about to paint may still be visible after we fill with + // the opaque occluder. For that reason, we fill with the shadow color, + // and set the shadow color to fully opaque. This way, the visible + // pixels are less opaque and less noticeable. + context.save(); + context.filter = 'none'; + context.strokeStyle = ''; + context.fillStyle = colorToCssString(shadow.color); + context.shadowBlur = shadow.blur; + context.shadowColor = colorToCssString(shadow.color.withAlpha(0xff)); + context.shadowOffsetX = shadow.offsetX; + context.shadowOffsetY = shadow.offsetY; + _runPath(context, path); + context.fill(); + context.restore(); + } } } } diff --git a/lib/web_ui/lib/src/engine/compositor/util.dart b/lib/web_ui/lib/src/engine/compositor/util.dart index 72d53c6d9b52d..218d3a0de71db 100644 --- a/lib/web_ui/lib/src/engine/compositor/util.dart +++ b/lib/web_ui/lib/src/engine/compositor/util.dart @@ -286,6 +286,10 @@ js.JsArray makeSkiaColorStops(List colorStops) { return jsColorStops; } +// These must be kept in sync with `flow/layers/physical_shape_layer.cc`. +const double kLightHeight = 600.0; +const double kLightRadius = 800.0; + void drawSkShadow( js.JsObject skCanvas, SkPath path, diff --git a/lib/web_ui/lib/src/engine/dom_canvas.dart b/lib/web_ui/lib/src/engine/dom_canvas.dart index c774578df2525..970e1be69010a 100644 --- a/lib/web_ui/lib/src/engine/dom_canvas.dart +++ b/lib/web_ui/lib/src/engine/dom_canvas.dart @@ -81,7 +81,6 @@ class DomCanvas extends EngineCanvas with SaveElementStackTracking { }()); String effectiveTransform; final bool isStroke = paint.style == ui.PaintingStyle.stroke; - final double strokeWidth = paint.strokeWidth ?? 0.0; final double left = math.min(rect.left, rect.right); final double right = math.max(rect.left, rect.right); final double top = math.min(rect.top, rect.bottom); @@ -89,7 +88,7 @@ class DomCanvas extends EngineCanvas with SaveElementStackTracking { if (currentTransform.isIdentity()) { if (isStroke) { effectiveTransform = - 'translate(${left - (strokeWidth / 2.0)}px, ${top - (strokeWidth / 2.0)}px)'; + 'translate(${left - (paint.strokeWidth / 2.0)}px, ${top - (paint.strokeWidth / 2.0)}px)'; } else { effectiveTransform = 'translate(${left}px, ${top}px)'; } @@ -98,7 +97,7 @@ class DomCanvas extends EngineCanvas with SaveElementStackTracking { final Matrix4 translated = currentTransform.clone(); if (isStroke) { translated.translate( - left - (strokeWidth / 2.0), top - (strokeWidth / 2.0)); + left - (paint.strokeWidth / 2.0), top - (paint.strokeWidth / 2.0)); } else { translated.translate(left, top); } @@ -110,8 +109,8 @@ class DomCanvas extends EngineCanvas with SaveElementStackTracking { ..transformOrigin = '0 0 0' ..transform = effectiveTransform; - final String cssColor = - paint.color == null ? '#000000' : colorToCssString(paint.color); + final String cssColor = paint.color == null ? '#000000' + : colorToCssString(paint.color); if (paint.maskFilter != null) { style.filter = 'blur(${paint.maskFilter.webOnlySigma}px)'; @@ -119,9 +118,9 @@ class DomCanvas extends EngineCanvas with SaveElementStackTracking { if (isStroke) { style - ..width = '${right - left - strokeWidth}px' - ..height = '${bottom - top - strokeWidth}px' - ..border = '${strokeWidth}px solid $cssColor'; + ..width = '${right - left - paint.strokeWidth}px' + ..height = '${bottom - top - paint.strokeWidth}px' + ..border = '${paint.strokeWidth}px solid $cssColor'; } else { style ..width = '${right - left}px' diff --git a/lib/web_ui/lib/src/engine/shadow.dart b/lib/web_ui/lib/src/engine/shadow.dart index 3c46dac7681b5..5e63f74e186ea 100644 --- a/lib/web_ui/lib/src/engine/shadow.dart +++ b/lib/web_ui/lib/src/engine/shadow.dart @@ -5,135 +5,366 @@ // @dart = 2.6 part of engine; -/// How far is the light source from the surface of the UI. +/// This code is ported from the AngularDart SCSS. /// -/// Must be kept in sync with `flow/layers/physical_shape_layer.cc`. -const double kLightHeight = 600.0; +/// See: https://github.com/dart-lang/angular_components/blob/master/lib/css/material/_shadow.scss +class ElevationShadow { + /// Applies a standard transition style for box-shadow to box-shadow. + static void applyShadowTransition(html.CssStyleDeclaration style) { + style.transition = 'box-shadow .28s cubic-bezier(.4, 0, .2, 1)'; + } -/// The radius of the light source. The positive radius creates a penumbra in -/// the shadow, which we express using a blur effect. -/// -/// Must be kept in sync with `flow/layers/physical_shape_layer.cc`. -const double kLightRadius = 800.0; + /// Disables box-shadow. + static void applyShadowNone(html.CssStyleDeclaration style) { + style.boxShadow = 'none'; + } -/// The X offset of the list source relative to the center of the shape. -/// -/// This shifts the shadow along the X asix as if the light beams at an angle. -const double kLightOffsetX = -200.0; + /// Applies a standard shadow to the selected element(s). + /// + /// This rule is great for things that need a static shadow. If the elevation + /// of the shadow needs to be changed dynamically, use [applyShadow]. + /// + /// Valid values: 2, 3, 4, 6, 8, 12, 16, 24 + static void applyShadowElevation(html.CssStyleDeclaration style, + {@required int dp, @required ui.Color color}) { + const double keyUmbraOpacity = 0.2; + const double keyPenumbraOpacity = 0.14; + const double ambientShadowOpacity = 0.12; -/// The Y offset of the list source relative to the center of the shape. -/// -/// This shifts the shadow along the Y asix as if the light beams at an angle. -const double kLightOffsetY = -400.0; + final String rgb = '${color.red}, ${color.green}, ${color.blue}'; + if (dp == 2) { + style.boxShadow = '0 2px 2px 0 rgba($rgb, $keyPenumbraOpacity), ' + '0 3px 1px -2px rgba($rgb, $ambientShadowOpacity), ' + '0 1px 5px 0 rgba($rgb, $keyUmbraOpacity)'; + } else if (dp == 3) { + style.boxShadow = '0 3px 4px 0 rgba($rgb, $keyPenumbraOpacity), ' + '0 3px 3px -2px rgba($rgb, $ambientShadowOpacity), ' + '0 1px 8px 0 rgba($rgb, $keyUmbraOpacity)'; + } else if (dp == 4) { + style.boxShadow = '0 4px 5px 0 rgba($rgb, $keyPenumbraOpacity), ' + '0 1px 10px 0 rgba($rgb, $ambientShadowOpacity), ' + '0 2px 4px -1px rgba($rgb, $keyUmbraOpacity)'; + } else if (dp == 6) { + style.boxShadow = '0 6px 10px 0 rgba($rgb, $keyPenumbraOpacity), ' + '0 1px 18px 0 rgba($rgb, $ambientShadowOpacity), ' + '0 3px 5px -1px rgba($rgb, $keyUmbraOpacity)'; + } else if (dp == 8) { + style.boxShadow = '0 8px 10px 1px rgba($rgb, $keyPenumbraOpacity), ' + '0 3px 14px 2px rgba($rgb, $ambientShadowOpacity), ' + '0 5px 5px -3px rgba($rgb, $keyUmbraOpacity)'; + } else if (dp == 12) { + style.boxShadow = '0 12px 17px 2px rgba($rgb, $keyPenumbraOpacity), ' + '0 5px 22px 4px rgba($rgb, $ambientShadowOpacity), ' + '0 7px 8px -4px rgba($rgb, $keyUmbraOpacity)'; + } else if (dp == 16) { + style.boxShadow = '0 16px 24px 2px rgba($rgb, $keyPenumbraOpacity), ' + '0 6px 30px 5px rgba($rgb, $ambientShadowOpacity), ' + '0 8px 10px -5px rgba($rgb, $keyUmbraOpacity)'; + } else { + style.boxShadow = '0 24px 38px 3px rgba($rgb, $keyPenumbraOpacity), ' + '0 9px 46px 8px rgba($rgb, $ambientShadowOpacity), ' + '0 11px 15px -7px rgba($rgb, $keyUmbraOpacity)'; + } + } -/// Computes the offset that moves the shadow due to the light hitting the -/// shape at an angle. -/// -/// ------ light -/// \ -/// \ -/// \ -/// \ -/// \ -/// --------- shape -/// |\ -/// | \ -/// | \ -/// ------------x---x------------ -/// |<->| offset -/// -/// This is not a complete physical model. For example, this does not take into -/// account the size of the shape (this function doesn't even take the shape as -/// a parameter). It's just a good enough approximation. -ui.Offset computeShadowOffset(elevation) { - if (elevation == 0.0) { - return ui.Offset.zero; + /// Applies the shadow styles to the selected element. + /// + /// Use the attributes below to control the shadow. + /// + /// - `animated` -- Whether to animate the shadow transition. + /// - `elevation` -- Z-elevation of shadow. Valid Values: 1,2,3,4,5 + static void applyShadow( + html.CssStyleDeclaration style, double elevation, ui.Color color) { + applyShadowTransition(style); + + if (elevation <= 0.0) { + applyShadowNone(style); + } else if (elevation <= 1.0) { + applyShadowElevation(style, dp: 2, color: color); + } else if (elevation <= 2.0) { + applyShadowElevation(style, dp: 4, color: color); + } else if (elevation <= 3.0) { + applyShadowElevation(style, dp: 6, color: color); + } else if (elevation <= 4.0) { + applyShadowElevation(style, dp: 8, color: color); + } else if (elevation <= 5.0) { + applyShadowElevation(style, dp: 16, color: color); + } else { + applyShadowElevation(style, dp: 24, color: color); + } } - final double dx = -kLightOffsetX * elevation / kLightHeight; - final double dy = -kLightOffsetY * elevation / kLightHeight; - return ui.Offset(dx, dy); -} + static List computeCanvasShadows( + double elevation, ui.Color color) { + if (elevation <= 0.0) { + return const []; + } else if (elevation <= 1.0) { + return computeShadowElevation(dp: 2, color: color); + } else if (elevation <= 2.0) { + return computeShadowElevation(dp: 4, color: color); + } else if (elevation <= 3.0) { + return computeShadowElevation(dp: 6, color: color); + } else if (elevation <= 4.0) { + return computeShadowElevation(dp: 8, color: color); + } else if (elevation <= 5.0) { + return computeShadowElevation(dp: 16, color: color); + } else { + return computeShadowElevation(dp: 24, color: color); + } + } -/// Computes the rectangle that contains the penumbra of the shadow cast by -/// the [shape] that's elevated above the surface of the screen at [elevation]. -ui.Rect computePenumbraBounds(ui.Rect shape, double elevation) { - if (elevation == 0.0) { - return shape; + /// Expands rect to include size of shadow. + /// + /// Computed from shadow elevation offset + spread, blur + static ui.Rect computeShadowRect(ui.Rect r, double elevation) { + // We are computing this rect by computing the maximum "reach" of the shadow + // by summing the computed shadow offset and the blur for the given + // elevation. We are assuming that a blur of '1' corresponds to 1 pixel, + // although the web spec says that this is not necessarily the case. + // However, it seems to be a good conservative estimate. + if (elevation <= 0.0) { + return r; + } else if (elevation <= 1.0) { + return ui.Rect.fromLTRB( + r.left - 2.5, r.top - 1.5, r.right + 3, r.bottom + 4); + } else if (elevation <= 2.0) { + return ui.Rect.fromLTRB(r.left - 5, r.top - 3, r.right + 6, r.bottom + 7); + } else if (elevation <= 3.0) { + return ui.Rect.fromLTRB( + r.left - 9, r.top - 8, r.right + 9, r.bottom + 11); + } else if (elevation <= 4.0) { + return ui.Rect.fromLTRB( + r.left - 10, r.top - 6, r.right + 10, r.bottom + 14); + } else if (elevation <= 5.0) { + return ui.Rect.fromLTRB( + r.left - 15, r.top - 9, r.right + 20, r.bottom + 30); + } else { + return ui.Rect.fromLTRB( + r.left - 23, r.top - 14, r.right + 23, r.bottom + 45); + } } - // tangent for x - final double tx = (kLightRadius + shape.width * 0.5) / kLightHeight; - // tangent for y - final double ty = (kLightRadius + shape.height * 0.5) / kLightHeight; - final double dx = elevation * tx; - final double dy = elevation * ty; - final ui.Offset offset = computeShadowOffset(elevation); - return ui.Rect.fromLTRB( - shape.left - dx, - shape.top - dy, - shape.right + dx, - shape.bottom + dy, - ).shift(offset); -} + static List computeShadowElevation( + {@required int dp, @required ui.Color color}) { + final int red = color.red; + final int green = color.green; + final int blue = color.blue; -/// Information needed to render a shadow using CSS or canvas. -@immutable -class SurfaceShadowData { - const SurfaceShadowData({ - @required this.blurWidth, - @required this.offset, - }); + final ui.Color penumbraColor = ui.Color.fromARGB(36, red, green, blue); + final ui.Color ambientShadowColor = ui.Color.fromARGB(31, red, green, blue); + final ui.Color umbraColor = ui.Color.fromARGB(51, red, green, blue); - /// The length in pixels of the shadow. - /// - /// This is different from the `sigma` used by blur filters. This value - /// contains the entire shadow, so, for example, to compute the shadow - /// bounds it is sufficient to add this value to the width of the shape - /// that casts it. - final double blurWidth; - - /// The offset of the shadow relative to the shape as computed by - /// [computeShadowOffset]. - final ui.Offset offset; -} + final List result = []; + if (dp == 2) { + result.add(CanvasShadow( + offsetX: 0.0, + offsetY: 2.0, + blur: 1.0, + spread: 0.0, + color: penumbraColor, + )); -/// Computes the shadow for [shape] based on its [elevation] from the surface -/// of the screen. -/// -/// The algorithm approximates the math done by the C++ implementation from -/// `physical_shape_layer.cc` but it's not exact, since on the Web we do not -/// (cannot) use Skia's shadow API directly. However, this algorithms is -/// consistent with [computePenumbraBounds] used by [RecordingCanvas] during -/// bounds estimation. -SurfaceShadowData computeShadow(ui.Rect shape, double elevation) { - if (elevation == 0.0) { - return null; - } + result.add(CanvasShadow( + offsetX: 0.0, + offsetY: 3.0, + blur: 0.5, + spread: -2.0, + color: ambientShadowColor, + )); - final double penumbraTangentX = - (kLightRadius + shape.width * 0.5) / kLightHeight; - final double penumbraTangentY = - (kLightRadius + shape.height * 0.5) / kLightHeight; - final double penumbraWidth = elevation * penumbraTangentX; - final double penumbraHeight = elevation * penumbraTangentY; - return SurfaceShadowData( - // There's no way to express different blur along different dimensions, so - // we use the narrower of the two to prevent the shadow blur from being longer - // than the shape itself, using min instead of average of penumbra values. - blurWidth: math.min(penumbraWidth, penumbraHeight), - offset: computeShadowOffset(elevation), - ); -} + result.add(CanvasShadow( + offsetX: 0.0, + offsetY: 1.0, + blur: 2.5, + spread: 0.0, + color: umbraColor, + )); + } else if (dp == 3) { + result.add(CanvasShadow( + offsetX: 0.0, + offsetY: 1.5, + blur: 4.0, + spread: 0.0, + color: penumbraColor, + )); + + result.add(CanvasShadow( + offsetX: 0.0, + offsetY: 3.0, + blur: 1.5, + spread: -2.0, + color: ambientShadowColor, + )); + + result.add(CanvasShadow( + offsetX: 0.0, + offsetY: 1.0, + blur: 4.0, + spread: 0.0, + color: umbraColor, + )); + } else if (dp == 4) { + result.add(CanvasShadow( + offsetX: 0.0, + offsetY: 4.0, + blur: 2.5, + spread: 0.0, + color: penumbraColor, + )); + + result.add(CanvasShadow( + offsetX: 0.0, + offsetY: 1.0, + blur: 5.0, + spread: 0.0, + color: ambientShadowColor, + )); + + result.add(CanvasShadow( + offsetX: 0.0, + offsetY: 2.0, + blur: 2.0, + spread: -1.0, + color: umbraColor, + )); + } else if (dp == 6) { + result.add(CanvasShadow( + offsetX: 0.0, + offsetY: 6.0, + blur: 5.0, + spread: 0.0, + color: penumbraColor, + )); + + result.add(CanvasShadow( + offsetX: 0.0, + offsetY: 1.0, + blur: 9.0, + spread: 0.0, + color: ambientShadowColor, + )); + + result.add(CanvasShadow( + offsetX: 0.0, + offsetY: 3.0, + blur: 2.5, + spread: -1.0, + color: umbraColor, + )); + } else if (dp == 8) { + result.add(CanvasShadow( + offsetX: 0.0, + offsetY: 4.0, + blur: 10.0, + spread: 1.0, + color: penumbraColor, + )); + + result.add(CanvasShadow( + offsetX: 0.0, + offsetY: 3.0, + blur: 7.0, + spread: 2.0, + color: ambientShadowColor, + )); -/// Applies a CSS shadow to the [shape]. -void applyCssShadow( - html.Element element, ui.Rect shape, double elevation, ui.Color color) { - final SurfaceShadowData shadow = computeShadow(shape, elevation); - if (shadow == null) { - element.style.boxShadow = 'none'; - } else { - element.style.boxShadow = '${shadow.offset.dx}px ${shadow.offset.dy}px ' - '${shadow.blurWidth}px 0px rgb(${color.red}, ${color.green}, ${color.blue})'; + result.add(CanvasShadow( + offsetX: 0.0, + offsetY: 5.0, + blur: 2.5, + spread: -3.0, + color: umbraColor, + )); + } else if (dp == 12) { + result.add(CanvasShadow( + offsetX: 0.0, + offsetY: 12.0, + blur: 8.5, + spread: 2.0, + color: penumbraColor, + )); + + result.add(CanvasShadow( + offsetX: 0.0, + offsetY: 5.0, + blur: 11.0, + spread: 4.0, + color: ambientShadowColor, + )); + + result.add(CanvasShadow( + offsetX: 0.0, + offsetY: 7.0, + blur: 4.0, + spread: -4.0, + color: umbraColor, + )); + } else if (dp == 16) { + result.add(CanvasShadow( + offsetX: 0.0, + offsetY: 16.0, + blur: 12.0, + spread: 2.0, + color: penumbraColor, + )); + + result.add(CanvasShadow( + offsetX: 0.0, + offsetY: 6.0, + blur: 15.0, + spread: 5.0, + color: ambientShadowColor, + )); + + result.add(CanvasShadow( + offsetX: 0.0, + offsetY: 0.0, + blur: 5.0, + spread: -5.0, + color: umbraColor, + )); + } else { + result.add(CanvasShadow( + offsetX: 0.0, + offsetY: 24.0, + blur: 18.0, + spread: 3.0, + color: penumbraColor, + )); + + result.add(CanvasShadow( + offsetX: 0.0, + offsetY: 9.0, + blur: 23.0, + spread: 8.0, + color: ambientShadowColor, + )); + + result.add(CanvasShadow( + offsetX: 0.0, + offsetY: 11.0, + blur: 7.5, + spread: -7.0, + color: umbraColor, + )); + } + return result; } } + +class CanvasShadow { + CanvasShadow({ + @required this.offsetX, + @required this.offsetY, + @required this.blur, + @required this.spread, + @required this.color, + }); + + final double offsetX; + final double offsetY; + final double blur; + // TODO(yjbanov): is there a way to implement/emulate spread on Canvas2D? + final double spread; + final ui.Color color; +} diff --git a/lib/web_ui/lib/src/engine/surface/clip.dart b/lib/web_ui/lib/src/engine/surface/clip.dart index 43c683b21b004..55692e092ac83 100644 --- a/lib/web_ui/lib/src/engine/surface/clip.dart +++ b/lib/web_ui/lib/src/engine/surface/clip.dart @@ -162,11 +162,9 @@ class PersistedPhysicalShape extends PersistedContainerSurface this.elevation, int color, int shadowColor, this.clipBehavior) : color = ui.Color(color), shadowColor = ui.Color(shadowColor), - pathBounds = path.getBounds(), super(oldLayer); final SurfacePath path; - final ui.Rect pathBounds; final double elevation; final ui.Color color; final ui.Color shadowColor; @@ -197,7 +195,7 @@ class PersistedPhysicalShape extends PersistedContainerSurface } void _applyShadow() { - applyCssShadow(rootElement, pathBounds, elevation, shadowColor); + ElevationShadow.applyShadow(rootElement.style, elevation, shadowColor); } @override @@ -281,6 +279,7 @@ class PersistedPhysicalShape extends PersistedContainerSurface } } + final ui.Rect pathBounds = path.getBounds(); final String svgClipPath = _pathToSvgClipPath(path, offsetX: -pathBounds.left, offsetY: -pathBounds.top, diff --git a/lib/web_ui/lib/src/engine/surface/recording_canvas.dart b/lib/web_ui/lib/src/engine/surface/recording_canvas.dart index 5b26210ae2af2..92ce62b93e303 100644 --- a/lib/web_ui/lib/src/engine/surface/recording_canvas.dart +++ b/lib/web_ui/lib/src/engine/surface/recording_canvas.dart @@ -188,7 +188,7 @@ class RecordingCanvas { } void drawLine(ui.Offset p1, ui.Offset p2, SurfacePaint paint) { - final double paintSpread = math.max(_getPaintSpread(paint), 1.0); + final double strokeWidth = math.max(paint.strokeWidth, 1.0); // TODO(yjbanov): This can be optimized. Currently we create a box around // the line and then apply the transform on the box to get // the bounding box. If you have a 45-degree line and a @@ -197,11 +197,10 @@ class RecordingCanvas { // algorithm produces a square with each side of the length // matching the length of the line. _paintBounds.growLTRB( - math.min(p1.dx, p2.dx) - paintSpread, - math.min(p1.dy, p2.dy) - paintSpread, - math.max(p1.dx, p2.dx) + paintSpread, - math.max(p1.dy, p2.dy) + paintSpread, - ); + math.min(p1.dx, p2.dx) - strokeWidth, + math.min(p1.dy, p2.dy) - strokeWidth, + math.max(p1.dx, p2.dx) + strokeWidth, + math.max(p1.dy, p2.dy) + strokeWidth); _hasArbitraryPaint = true; _didDraw = true; _commands.add(PaintDrawLine(p1, p2, paint.paintData)); @@ -219,9 +218,8 @@ class RecordingCanvas { _hasArbitraryPaint = true; } _didDraw = true; - final double paintSpread = _getPaintSpread(paint); - if (paintSpread != 0.0) { - _paintBounds.grow(rect.inflate(paintSpread)); + if (paint.strokeWidth != null && paint.strokeWidth != 0) { + _paintBounds.grow(rect.inflate(paint.strokeWidth / 2.0)); } else { _paintBounds.grow(rect); } @@ -233,11 +231,12 @@ class RecordingCanvas { _hasArbitraryPaint = true; } _didDraw = true; - final double paintSpread = _getPaintSpread(paint); - final double left = math.min(rrect.left, rrect.right) - paintSpread; - final double top = math.min(rrect.top, rrect.bottom) - paintSpread; - final double right = math.max(rrect.left, rrect.right) + paintSpread; - final double bottom = math.max(rrect.top, rrect.bottom) + paintSpread; + final double strokeWidth = + paint.strokeWidth == null ? 0 : paint.strokeWidth; + final double left = math.min(rrect.left, rrect.right) - strokeWidth; + final double right = math.max(rrect.left, rrect.right) + strokeWidth; + final double top = math.min(rrect.top, rrect.bottom) - strokeWidth; + final double bottom = math.max(rrect.top, rrect.bottom) + strokeWidth; _paintBounds.growLTRB(left, top, right, bottom); _commands.add(PaintDrawRRect(rrect, paint.paintData)); } @@ -282,22 +281,18 @@ class RecordingCanvas { _hasArbitraryPaint = true; _didDraw = true; - final double paintSpread = _getPaintSpread(paint); - _paintBounds.growLTRB( - outer.left - paintSpread, - outer.top - paintSpread, - outer.right + paintSpread, - outer.bottom + paintSpread, - ); + final double strokeWidth = + paint.strokeWidth == null ? 0 : paint.strokeWidth; + _paintBounds.growLTRB(outer.left - strokeWidth, outer.top - strokeWidth, + outer.right + strokeWidth, outer.bottom + strokeWidth); _commands.add(PaintDrawDRRect(outer, inner, paint.paintData)); } void drawOval(ui.Rect rect, SurfacePaint paint) { _hasArbitraryPaint = true; _didDraw = true; - final double paintSpread = _getPaintSpread(paint); - if (paintSpread != 0.0) { - _paintBounds.grow(rect.inflate(paintSpread)); + if (paint.strokeWidth != null) { + _paintBounds.grow(rect.inflate(paint.strokeWidth)); } else { _paintBounds.grow(rect); } @@ -307,13 +302,13 @@ class RecordingCanvas { void drawCircle(ui.Offset c, double radius, SurfacePaint paint) { _hasArbitraryPaint = true; _didDraw = true; - final double paintSpread = _getPaintSpread(paint); + final double strokeWidth = + paint.strokeWidth == null ? 0 : paint.strokeWidth; _paintBounds.growLTRB( - c.dx - radius - paintSpread, - c.dy - radius - paintSpread, - c.dx + radius + paintSpread, - c.dy + radius + paintSpread, - ); + c.dx - radius - strokeWidth, + c.dy - radius - strokeWidth, + c.dx + radius + strokeWidth, + c.dy + radius + strokeWidth); _commands.add(PaintDrawCircle(c, radius, paint.paintData)); } @@ -336,9 +331,8 @@ class RecordingCanvas { _hasArbitraryPaint = true; _didDraw = true; ui.Rect pathBounds = path.getBounds(); - final double paintSpread = _getPaintSpread(paint); - if (paintSpread != 0.0) { - pathBounds = pathBounds.inflate(paintSpread); + if (paint.strokeWidth != null) { + pathBounds = pathBounds.inflate(paint.strokeWidth); } _paintBounds.grow(pathBounds); // Clone path so it can be reused for subsequent draw calls. @@ -387,7 +381,7 @@ class RecordingCanvas { _hasArbitraryPaint = true; _didDraw = true; final ui.Rect shadowRect = - computePenumbraBounds(path.getBounds(), elevation); + ElevationShadow.computeShadowRect(path.getBounds(), elevation); _paintBounds.grow(shadowRect); _commands.add(PaintDrawShadow(path, color, elevation, transparentOccluder)); } @@ -396,23 +390,23 @@ class RecordingCanvas { ui.Vertices vertices, ui.BlendMode blendMode, SurfacePaint paint) { _hasArbitraryPaint = true; _didDraw = true; - _growPaintBoundsByPoints(vertices.positions, 0, paint); + _growPaintBoundsByPoints(vertices.positions, 0); _commands.add(PaintVertices(vertices, blendMode, paint.paintData)); } void drawRawPoints( - ui.PointMode pointMode, Float32List points, SurfacePaint paint) { + ui.PointMode pointMode, Float32List points, ui.Paint paint) { if (paint.strokeWidth == null) { return; } _hasArbitraryPaint = true; _didDraw = true; - _growPaintBoundsByPoints(points, paint.strokeWidth, paint); + _growPaintBoundsByPoints(points, paint.strokeWidth); _commands .add(PaintPoints(pointMode, points, paint.strokeWidth, paint.color)); } - void _growPaintBoundsByPoints(Float32List points, double thickness, SurfacePaint paint) { + void _growPaintBoundsByPoints(Float32List points, double thickness) { double minValueX, maxValueX, minValueY, maxValueY; minValueX = maxValueX = points[0]; minValueY = maxValueY = points[1]; @@ -430,13 +424,8 @@ class RecordingCanvas { maxValueY = math.max(maxValueY, y); } final double distance = thickness / 2.0; - final double paintSpread = _getPaintSpread(paint); - _paintBounds.growLTRB( - minValueX - distance - paintSpread, - minValueY - distance - paintSpread, - maxValueX + distance + paintSpread, - maxValueY + distance + paintSpread, - ); + _paintBounds.growLTRB(minValueX - distance, minValueY - distance, + maxValueX + distance, maxValueY + distance); } int _saveCount = 1; @@ -1948,28 +1937,3 @@ class _PaintBounds { } } } - -/// Computes the length of the visual effect caused by paint parameters, such -/// as blur and stroke width. -/// -/// This paint spread should be taken into accound when estimating bounding -/// boxes for paint operations that apply the paint. -double _getPaintSpread(SurfacePaint paint) { - double spread = 0.0; - final ui.MaskFilter maskFilter = paint?.maskFilter; - if (maskFilter != null) { - // Multiply by 2 because the sigma is the standard deviation rather than - // the length of the blur. - // See also: https://developer.mozilla.org/en-US/docs/Web/CSS/filter-function/blur - spread += maskFilter.webOnlySigma * 2.0; - } - if (paint.strokeWidth != null && paint.strokeWidth != 0) { - // The multiplication by sqrt(2) is to account for line joints that - // meet at 90-degree angle. Division by 2 is because only half of the - // stroke is sticking out of the original shape. The other half is - // inside the shape. - const double sqrtOfTwoDivByTwo = 0.70710678118; - spread += paint.strokeWidth * sqrtOfTwoDivByTwo; - } - return spread; -} diff --git a/lib/web_ui/lib/src/engine/surface/scene_builder.dart b/lib/web_ui/lib/src/engine/surface/scene_builder.dart index 3f9a45f3d16e4..cbd0b2913fe62 100644 --- a/lib/web_ui/lib/src/engine/surface/scene_builder.dart +++ b/lib/web_ui/lib/src/engine/surface/scene_builder.dart @@ -244,7 +244,6 @@ class SurfaceSceneBuilder implements ui.SceneBuilder { ui.Clip clipBehavior = ui.Clip.none, ui.PhysicalShapeEngineLayer oldLayer, }) { - assert(color != null, 'color must not be null'); return _pushSurface(PersistedPhysicalShape( oldLayer, path, diff --git a/lib/web_ui/test/golden_tests/engine/recording_canvas_golden_test.dart b/lib/web_ui/test/golden_tests/engine/recording_canvas_golden_test.dart index c015a1ea4ec45..44c9f44720357 100644 --- a/lib/web_ui/test/golden_tests/engine/recording_canvas_golden_test.dart +++ b/lib/web_ui/test/golden_tests/engine/recording_canvas_golden_test.dart @@ -341,9 +341,7 @@ void main() async { path.addRect(const Rect.fromLTRB(20, 30, 100, 110)); rc.drawShadow(path, const Color(0xFFFF0000), 2.0, true); expect( - rc.computePaintBounds(), - within(distance: 0.05, from: const Rect.fromLTRB(17.9, 28.5, 103.5, 114.1)), - ); + rc.computePaintBounds(), const Rect.fromLTRB(15.0, 27.0, 106.0, 117.0)); await _checkScreenshot(rc, 'path_with_shadow'); }); @@ -442,177 +440,6 @@ void main() async { rc.restore(); await _checkScreenshot(rc, 'path_with_line_and_roundrect'); }); - - test('should include paint spread in bounds estimates', () async { - final SurfaceSceneBuilder sb = SurfaceSceneBuilder(); - - final List painters = [ - (RecordingCanvas canvas, SurfacePaint paint) { - canvas.drawLine( - const Offset(0.0, 0.0), - const Offset(20.0, 20.0), - paint, - ); - }, - (RecordingCanvas canvas, SurfacePaint paint) { - canvas.drawRect( - const Rect.fromLTRB(0.0, 0.0, 20.0, 20.0), - paint, - ); - }, - (RecordingCanvas canvas, SurfacePaint paint) { - canvas.drawRRect( - RRect.fromLTRBR(0.0, 0.0, 20.0, 20.0, Radius.circular(7.0)), - paint, - ); - }, - (RecordingCanvas canvas, SurfacePaint paint) { - canvas.drawDRRect( - RRect.fromLTRBR(0.0, 0.0, 20.0, 20.0, Radius.circular(5.0)), - RRect.fromLTRBR(4.0, 4.0, 16.0, 16.0, Radius.circular(5.0)), - paint, - ); - }, - (RecordingCanvas canvas, SurfacePaint paint) { - canvas.drawOval( - const Rect.fromLTRB(0.0, 5.0, 20.0, 15.0), - paint, - ); - }, - (RecordingCanvas canvas, SurfacePaint paint) { - canvas.drawCircle( - const Offset(10.0, 10.0), - 10.0, - paint, - ); - }, - (RecordingCanvas canvas, SurfacePaint paint) { - final SurfacePath path = SurfacePath() - ..moveTo(10, 0) - ..lineTo(20, 10) - ..lineTo(10, 20) - ..lineTo(0, 10) - ..close(); - canvas.drawPath(path, paint); - }, - - // Images are not affected by mask filter or stroke width. They use image - // filter instead. - (RecordingCanvas canvas, SurfacePaint paint) { - canvas.drawImage(_createRealTestImage(), Offset.zero, paint); - }, - (RecordingCanvas canvas, SurfacePaint paint) { - canvas.drawImageRect( - _createRealTestImage(), - const Rect.fromLTRB(0, 0, 20, 20), - const Rect.fromLTRB(5, 5, 15, 15), - paint, - ); - }, - ]; - - Picture drawBounds(Rect bounds) { - final EnginePictureRecorder recorder = EnginePictureRecorder(); - final RecordingCanvas canvas = recorder.beginRecording(Rect.largest); - canvas.drawRect( - bounds, - SurfacePaint() - ..style = PaintingStyle.stroke - ..strokeWidth = 1.0 - ..color = const Color.fromARGB(255, 0, 255, 0), - ); - return recorder.endRecording(); - } - - for (int i = 0; i < painters.length; i++) { - sb.pushOffset(0.0, 20.0 + 60.0 * i); - final PaintSpreadPainter painter = painters[i]; - - // Paint with zero paint spread. - { - sb.pushOffset(20.0, 0.0); - final EnginePictureRecorder recorder = EnginePictureRecorder(); - final RecordingCanvas canvas = recorder.beginRecording(Rect.largest); - final SurfacePaint zeroSpreadPaint = SurfacePaint(); - painter(canvas, zeroSpreadPaint); - sb.addPicture(Offset.zero, recorder.endRecording()); - sb.addPicture(Offset.zero, drawBounds(canvas.computePaintBounds())); - sb.pop(); - } - - // Paint with a thick stroke paint. - { - sb.pushOffset(80.0, 0.0); - final EnginePictureRecorder recorder = EnginePictureRecorder(); - final RecordingCanvas canvas = recorder.beginRecording(Rect.largest); - final SurfacePaint thickStrokePaint = SurfacePaint() - ..style = PaintingStyle.stroke - ..strokeWidth = 5.0; - painter(canvas, thickStrokePaint); - sb.addPicture(Offset.zero, recorder.endRecording()); - sb.addPicture(Offset.zero, drawBounds(canvas.computePaintBounds())); - sb.pop(); - } - - // Paint with a mask filter blur. - { - sb.pushOffset(140.0, 0.0); - final EnginePictureRecorder recorder = EnginePictureRecorder(); - final RecordingCanvas canvas = recorder.beginRecording(Rect.largest); - final SurfacePaint maskFilterBlurPaint = SurfacePaint() - ..maskFilter = const MaskFilter.blur(BlurStyle.normal, 5.0); - painter(canvas, maskFilterBlurPaint); - sb.addPicture(Offset.zero, recorder.endRecording()); - sb.addPicture(Offset.zero, drawBounds(canvas.computePaintBounds())); - sb.pop(); - } - - // Paint with a thick stroke paint and a mask filter blur. - { - sb.pushOffset(200.0, 0.0); - final EnginePictureRecorder recorder = EnginePictureRecorder(); - final RecordingCanvas canvas = recorder.beginRecording(Rect.largest); - final SurfacePaint thickStrokeAndBlurPaint = SurfacePaint() - ..style = PaintingStyle.stroke - ..strokeWidth = 5.0 - ..maskFilter = const MaskFilter.blur(BlurStyle.normal, 5.0); - painter(canvas, thickStrokeAndBlurPaint); - sb.addPicture(Offset.zero, recorder.endRecording()); - sb.addPicture(Offset.zero, drawBounds(canvas.computePaintBounds())); - sb.pop(); - } - - sb.pop(); - } - - final html.Element sceneElement = sb.build().webOnlyRootElement; - html.document.body.append(sceneElement); - try { - await matchGoldenFile( - 'paint_spread_bounds.png', - region: const Rect.fromLTRB(0, 0, 250, 600), - maxDiffRatePercent: 0.0, - pixelComparison: PixelComparison.precise, - ); - } finally { - sceneElement.remove(); - } - }); -} - -typedef PaintSpreadPainter = void Function(RecordingCanvas canvas, SurfacePaint paint); - -const String _base64Encoded20x20TestImage = 'iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAIAAAAC64paAAAACXBIWXMAAC4jAAAuIwF4pT92AAAA' - 'B3RJTUUH5AMFFBksg4i3gQAAABl0RVh0Q29tbWVudABDcmVhdGVkIHdpdGggR0lNUFeBDhcAAAAj' - 'SURBVDjLY2TAC/7jlWVioACMah4ZmhnxpyHG0QAb1UyZZgBjWAIm/clP0AAAAABJRU5ErkJggg=='; - -HtmlImage _createRealTestImage() { - return HtmlImage( - html.ImageElement() - ..src = 'data:text/plain;base64,$_base64Encoded20x20TestImage', - 20, - 20, - ); } class TestImage implements Image { diff --git a/lib/web_ui/test/golden_tests/engine/shadow_golden_test.dart b/lib/web_ui/test/golden_tests/engine/shadow_golden_test.dart deleted file mode 100644 index d5f3dc8388e7c..0000000000000 --- a/lib/web_ui/test/golden_tests/engine/shadow_golden_test.dart +++ /dev/null @@ -1,161 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:html' as html; - -import 'package:ui/src/engine.dart'; -import 'package:ui/ui.dart'; -import 'package:test/test.dart'; - -import 'package:web_engine_tester/golden_tester.dart'; - -import 'scuba.dart'; - -const Color _kShadowColor = Color.fromARGB(255, 255, 0, 0); - -void main() async { - final Rect region = Rect.fromLTWH(0, 0, 550, 300); - - SurfaceSceneBuilder builder; - - setUpStableTestFonts(); - - setUp(() { - builder = SurfaceSceneBuilder(); - }); - - void _paintShapeOutline() { - final EnginePictureRecorder recorder = PictureRecorder(); - final RecordingCanvas canvas = recorder.beginRecording(Rect.largest); - canvas.drawRect( - const Rect.fromLTRB(0.0, 0.0, 20.0, 20.0), - SurfacePaint() - ..color = Color.fromARGB(255, 0, 0, 255) - ..style = PaintingStyle.stroke - ..strokeWidth = 1.0, - ); - builder.addPicture(Offset.zero, recorder.endRecording()); - } - - void _paintShadowBounds(SurfacePath path, double elevation) { - final Rect shadowBounds = - computePenumbraBounds(path.getBounds(), elevation); - final EnginePictureRecorder recorder = PictureRecorder(); - final RecordingCanvas canvas = recorder.beginRecording(Rect.largest); - canvas.drawRect( - shadowBounds, - SurfacePaint() - ..color = Color.fromARGB(255, 0, 255, 0) - ..style = PaintingStyle.stroke - ..strokeWidth = 1.0, - ); - builder.addPicture(Offset.zero, recorder.endRecording()); - } - - void _paintPhysicalShapeShadow(double elevation, Offset offset) { - final SurfacePath path = SurfacePath() - ..addRect(const Rect.fromLTRB(0, 0, 20, 20)); - builder.pushOffset(offset.dx, offset.dy); - builder.pushPhysicalShape( - path: path, - elevation: elevation, - shadowColor: _kShadowColor, - color: Color.fromARGB(255, 255, 255, 255), - ); - builder.pop(); // physical shape - _paintShapeOutline(); - _paintShadowBounds(path, elevation); - builder.pop(); // offset - } - - void _paintBitmapCanvasShadow(double elevation, Offset offset) { - final SurfacePath path = SurfacePath() - ..addRect(const Rect.fromLTRB(0, 0, 20, 20)); - builder.pushOffset(offset.dx, offset.dy); - - final EnginePictureRecorder recorder = PictureRecorder(); - final RecordingCanvas canvas = recorder.beginRecording(Rect.largest); - canvas - .debugEnforceArbitraryPaint(); // make sure DOM canvas doesn't take over - canvas.drawShadow( - path, - _kShadowColor, - elevation, - false, - ); - builder.addPicture(Offset.zero, recorder.endRecording()); - _paintShapeOutline(); - _paintShadowBounds(path, elevation); - - builder.pop(); // offset - } - - void _paintBitmapCanvasComplexPathShadow(double elevation, Offset offset) { - final SurfacePath path = SurfacePath() - ..moveTo(10, 0) - ..lineTo(20, 10) - ..lineTo(10, 20) - ..lineTo(0, 10) - ..close(); - builder.pushOffset(offset.dx, offset.dy); - - final EnginePictureRecorder recorder = PictureRecorder(); - final RecordingCanvas canvas = recorder.beginRecording(Rect.largest); - canvas - .debugEnforceArbitraryPaint(); // make sure DOM canvas doesn't take over - canvas.drawShadow( - path, - _kShadowColor, - elevation, - false, - ); - canvas.drawPath( - path, - SurfacePaint() - ..style = PaintingStyle.stroke - ..strokeWidth = 1 - ..color = Color.fromARGB(255, 0, 0, 255), - ); - builder.addPicture(Offset.zero, recorder.endRecording()); - _paintShadowBounds(path, elevation); - - builder.pop(); // offset - } - - test( - 'renders shadows correctly', - () async { - // Physical shape clips. We want to see that clipping in the screenshot. - debugShowClipLayers = false; - - builder.pushOffset(10, 20); - - for (int i = 0; i < 10; i++) { - _paintPhysicalShapeShadow(i.toDouble(), Offset(50.0 * i, 0)); - } - - for (int i = 0; i < 10; i++) { - _paintBitmapCanvasShadow(i.toDouble(), Offset(50.0 * i, 60)); - } - - for (int i = 0; i < 10; i++) { - _paintBitmapCanvasComplexPathShadow( - i.toDouble(), Offset(50.0 * i, 120)); - } - - builder.pop(); - - final html.Element sceneElement = builder.build().webOnlyRootElement; - html.document.body.append(sceneElement); - - await matchGoldenFile( - 'shadows.png', - region: region, - maxDiffRatePercent: 0.0, - pixelComparison: PixelComparison.precise, - ); - }, - testOn: 'chrome', - ); -}