fix shadows and mask filter blurs (flutter/engine#16963)
* fix shadows and mask filter blurs * update goldens * clarify the choice to min the shadows
This commit is contained in:
@@ -1,2 +1,2 @@
|
||||
repository: https://github.com/flutter/goldens.git
|
||||
revision: 1699ba6fd7093a0a610f82618fa30546e7974777
|
||||
revision: 8f692819e8881b7d2131dbd61d965c21d5e3e345
|
||||
|
||||
@@ -353,17 +353,16 @@ class BitmapCanvas extends EngineCanvas {
|
||||
|
||||
@override
|
||||
void drawImage(ui.Image image, ui.Offset p, SurfacePaintData 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);
|
||||
_drawImage(image, p, paint);
|
||||
_childOverdraw = true;
|
||||
_canvasPool.allocateExtraCanvas();
|
||||
}
|
||||
|
||||
void _drawImage(html.ImageElement imgElement, ui.Offset p) {
|
||||
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);
|
||||
if (_canvasPool.isClipped) {
|
||||
final List<html.Element> clipElements = _clipContent(
|
||||
_canvasPool._clipStack, imgElement, p, _canvasPool.currentTransform);
|
||||
@@ -380,12 +379,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 ||
|
||||
@@ -395,9 +394,6 @@ 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);
|
||||
@@ -414,7 +410,8 @@ class BitmapCanvas extends EngineCanvas {
|
||||
targetTop += topMargin;
|
||||
}
|
||||
}
|
||||
_drawImage(imgElement, ui.Offset(targetLeft, targetTop));
|
||||
|
||||
final html.ImageElement imgElement = _drawImage(image, ui.Offset(targetLeft, targetTop), paint);
|
||||
// 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
|
||||
|
||||
@@ -581,49 +581,46 @@ class _CanvasPool extends _SaveStackTracking {
|
||||
|
||||
void drawShadow(ui.Path path, ui.Color color, double elevation,
|
||||
bool transparentOccluder) {
|
||||
final List<CanvasShadow> 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();
|
||||
}
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -286,10 +286,6 @@ js.JsArray<double> makeSkiaColorStops(List<double> 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,
|
||||
|
||||
@@ -81,6 +81,7 @@ 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);
|
||||
@@ -88,7 +89,7 @@ class DomCanvas extends EngineCanvas with SaveElementStackTracking {
|
||||
if (currentTransform.isIdentity()) {
|
||||
if (isStroke) {
|
||||
effectiveTransform =
|
||||
'translate(${left - (paint.strokeWidth / 2.0)}px, ${top - (paint.strokeWidth / 2.0)}px)';
|
||||
'translate(${left - (strokeWidth / 2.0)}px, ${top - (strokeWidth / 2.0)}px)';
|
||||
} else {
|
||||
effectiveTransform = 'translate(${left}px, ${top}px)';
|
||||
}
|
||||
@@ -97,7 +98,7 @@ class DomCanvas extends EngineCanvas with SaveElementStackTracking {
|
||||
final Matrix4 translated = currentTransform.clone();
|
||||
if (isStroke) {
|
||||
translated.translate(
|
||||
left - (paint.strokeWidth / 2.0), top - (paint.strokeWidth / 2.0));
|
||||
left - (strokeWidth / 2.0), top - (strokeWidth / 2.0));
|
||||
} else {
|
||||
translated.translate(left, top);
|
||||
}
|
||||
@@ -109,8 +110,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)';
|
||||
@@ -118,9 +119,9 @@ class DomCanvas extends EngineCanvas with SaveElementStackTracking {
|
||||
|
||||
if (isStroke) {
|
||||
style
|
||||
..width = '${right - left - paint.strokeWidth}px'
|
||||
..height = '${bottom - top - paint.strokeWidth}px'
|
||||
..border = '${paint.strokeWidth}px solid $cssColor';
|
||||
..width = '${right - left - strokeWidth}px'
|
||||
..height = '${bottom - top - strokeWidth}px'
|
||||
..border = '${strokeWidth}px solid $cssColor';
|
||||
} else {
|
||||
style
|
||||
..width = '${right - left}px'
|
||||
|
||||
@@ -5,366 +5,135 @@
|
||||
// @dart = 2.6
|
||||
part of engine;
|
||||
|
||||
/// This code is ported from the AngularDart SCSS.
|
||||
/// How far is the light source from the surface of the UI.
|
||||
///
|
||||
/// 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)';
|
||||
/// Must be kept in sync with `flow/layers/physical_shape_layer.cc`.
|
||||
const double kLightHeight = 600.0;
|
||||
|
||||
/// 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;
|
||||
|
||||
/// 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;
|
||||
|
||||
/// 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;
|
||||
|
||||
/// 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;
|
||||
}
|
||||
|
||||
/// Disables box-shadow.
|
||||
static void applyShadowNone(html.CssStyleDeclaration style) {
|
||||
style.boxShadow = 'none';
|
||||
}
|
||||
|
||||
/// 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;
|
||||
|
||||
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)';
|
||||
}
|
||||
}
|
||||
|
||||
/// 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);
|
||||
}
|
||||
}
|
||||
|
||||
static List<CanvasShadow> computeCanvasShadows(
|
||||
double elevation, ui.Color color) {
|
||||
if (elevation <= 0.0) {
|
||||
return const <CanvasShadow>[];
|
||||
} 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);
|
||||
}
|
||||
}
|
||||
|
||||
/// 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);
|
||||
}
|
||||
}
|
||||
|
||||
static List<CanvasShadow> computeShadowElevation(
|
||||
{@required int dp, @required ui.Color color}) {
|
||||
final int red = color.red;
|
||||
final int green = color.green;
|
||||
final int blue = color.blue;
|
||||
|
||||
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);
|
||||
|
||||
final List<CanvasShadow> result = <CanvasShadow>[];
|
||||
if (dp == 2) {
|
||||
result.add(CanvasShadow(
|
||||
offsetX: 0.0,
|
||||
offsetY: 2.0,
|
||||
blur: 1.0,
|
||||
spread: 0.0,
|
||||
color: penumbraColor,
|
||||
));
|
||||
|
||||
result.add(CanvasShadow(
|
||||
offsetX: 0.0,
|
||||
offsetY: 3.0,
|
||||
blur: 0.5,
|
||||
spread: -2.0,
|
||||
color: ambientShadowColor,
|
||||
));
|
||||
|
||||
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,
|
||||
));
|
||||
|
||||
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;
|
||||
}
|
||||
final double dx = -kLightOffsetX * elevation / kLightHeight;
|
||||
final double dy = -kLightOffsetY * elevation / kLightHeight;
|
||||
return ui.Offset(dx, dy);
|
||||
}
|
||||
|
||||
class CanvasShadow {
|
||||
CanvasShadow({
|
||||
@required this.offsetX,
|
||||
@required this.offsetY,
|
||||
@required this.blur,
|
||||
@required this.spread,
|
||||
@required this.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;
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
/// Information needed to render a shadow using CSS or canvas.
|
||||
@immutable
|
||||
class SurfaceShadowData {
|
||||
const SurfaceShadowData({
|
||||
@required this.blurWidth,
|
||||
@required this.offset,
|
||||
});
|
||||
|
||||
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;
|
||||
/// 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;
|
||||
}
|
||||
|
||||
/// 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;
|
||||
}
|
||||
|
||||
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),
|
||||
);
|
||||
}
|
||||
|
||||
/// 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})';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -162,9 +162,11 @@ 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;
|
||||
@@ -195,7 +197,7 @@ class PersistedPhysicalShape extends PersistedContainerSurface
|
||||
}
|
||||
|
||||
void _applyShadow() {
|
||||
ElevationShadow.applyShadow(rootElement.style, elevation, shadowColor);
|
||||
applyCssShadow(rootElement, pathBounds, elevation, shadowColor);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -279,7 +281,6 @@ class PersistedPhysicalShape extends PersistedContainerSurface
|
||||
}
|
||||
}
|
||||
|
||||
final ui.Rect pathBounds = path.getBounds();
|
||||
final String svgClipPath = _pathToSvgClipPath(path,
|
||||
offsetX: -pathBounds.left,
|
||||
offsetY: -pathBounds.top,
|
||||
|
||||
@@ -188,7 +188,7 @@ class RecordingCanvas {
|
||||
}
|
||||
|
||||
void drawLine(ui.Offset p1, ui.Offset p2, SurfacePaint paint) {
|
||||
final double strokeWidth = math.max(paint.strokeWidth, 1.0);
|
||||
final double paintSpread = math.max(_getPaintSpread(paint), 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,10 +197,11 @@ 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) - strokeWidth,
|
||||
math.min(p1.dy, p2.dy) - strokeWidth,
|
||||
math.max(p1.dx, p2.dx) + strokeWidth,
|
||||
math.max(p1.dy, p2.dy) + strokeWidth);
|
||||
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,
|
||||
);
|
||||
_hasArbitraryPaint = true;
|
||||
_didDraw = true;
|
||||
_commands.add(PaintDrawLine(p1, p2, paint.paintData));
|
||||
@@ -218,8 +219,9 @@ class RecordingCanvas {
|
||||
_hasArbitraryPaint = true;
|
||||
}
|
||||
_didDraw = true;
|
||||
if (paint.strokeWidth != null && paint.strokeWidth != 0) {
|
||||
_paintBounds.grow(rect.inflate(paint.strokeWidth / 2.0));
|
||||
final double paintSpread = _getPaintSpread(paint);
|
||||
if (paintSpread != 0.0) {
|
||||
_paintBounds.grow(rect.inflate(paintSpread));
|
||||
} else {
|
||||
_paintBounds.grow(rect);
|
||||
}
|
||||
@@ -231,12 +233,11 @@ class RecordingCanvas {
|
||||
_hasArbitraryPaint = true;
|
||||
}
|
||||
_didDraw = true;
|
||||
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;
|
||||
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;
|
||||
_paintBounds.growLTRB(left, top, right, bottom);
|
||||
_commands.add(PaintDrawRRect(rrect, paint.paintData));
|
||||
}
|
||||
@@ -281,18 +282,22 @@ class RecordingCanvas {
|
||||
|
||||
_hasArbitraryPaint = true;
|
||||
_didDraw = true;
|
||||
final double strokeWidth =
|
||||
paint.strokeWidth == null ? 0 : paint.strokeWidth;
|
||||
_paintBounds.growLTRB(outer.left - strokeWidth, outer.top - strokeWidth,
|
||||
outer.right + strokeWidth, outer.bottom + strokeWidth);
|
||||
final double paintSpread = _getPaintSpread(paint);
|
||||
_paintBounds.growLTRB(
|
||||
outer.left - paintSpread,
|
||||
outer.top - paintSpread,
|
||||
outer.right + paintSpread,
|
||||
outer.bottom + paintSpread,
|
||||
);
|
||||
_commands.add(PaintDrawDRRect(outer, inner, paint.paintData));
|
||||
}
|
||||
|
||||
void drawOval(ui.Rect rect, SurfacePaint paint) {
|
||||
_hasArbitraryPaint = true;
|
||||
_didDraw = true;
|
||||
if (paint.strokeWidth != null) {
|
||||
_paintBounds.grow(rect.inflate(paint.strokeWidth));
|
||||
final double paintSpread = _getPaintSpread(paint);
|
||||
if (paintSpread != 0.0) {
|
||||
_paintBounds.grow(rect.inflate(paintSpread));
|
||||
} else {
|
||||
_paintBounds.grow(rect);
|
||||
}
|
||||
@@ -302,13 +307,13 @@ class RecordingCanvas {
|
||||
void drawCircle(ui.Offset c, double radius, SurfacePaint paint) {
|
||||
_hasArbitraryPaint = true;
|
||||
_didDraw = true;
|
||||
final double strokeWidth =
|
||||
paint.strokeWidth == null ? 0 : paint.strokeWidth;
|
||||
final double paintSpread = _getPaintSpread(paint);
|
||||
_paintBounds.growLTRB(
|
||||
c.dx - radius - strokeWidth,
|
||||
c.dy - radius - strokeWidth,
|
||||
c.dx + radius + strokeWidth,
|
||||
c.dy + radius + strokeWidth);
|
||||
c.dx - radius - paintSpread,
|
||||
c.dy - radius - paintSpread,
|
||||
c.dx + radius + paintSpread,
|
||||
c.dy + radius + paintSpread,
|
||||
);
|
||||
_commands.add(PaintDrawCircle(c, radius, paint.paintData));
|
||||
}
|
||||
|
||||
@@ -331,8 +336,9 @@ class RecordingCanvas {
|
||||
_hasArbitraryPaint = true;
|
||||
_didDraw = true;
|
||||
ui.Rect pathBounds = path.getBounds();
|
||||
if (paint.strokeWidth != null) {
|
||||
pathBounds = pathBounds.inflate(paint.strokeWidth);
|
||||
final double paintSpread = _getPaintSpread(paint);
|
||||
if (paintSpread != 0.0) {
|
||||
pathBounds = pathBounds.inflate(paintSpread);
|
||||
}
|
||||
_paintBounds.grow(pathBounds);
|
||||
// Clone path so it can be reused for subsequent draw calls.
|
||||
@@ -381,7 +387,7 @@ class RecordingCanvas {
|
||||
_hasArbitraryPaint = true;
|
||||
_didDraw = true;
|
||||
final ui.Rect shadowRect =
|
||||
ElevationShadow.computeShadowRect(path.getBounds(), elevation);
|
||||
computePenumbraBounds(path.getBounds(), elevation);
|
||||
_paintBounds.grow(shadowRect);
|
||||
_commands.add(PaintDrawShadow(path, color, elevation, transparentOccluder));
|
||||
}
|
||||
@@ -390,23 +396,23 @@ class RecordingCanvas {
|
||||
ui.Vertices vertices, ui.BlendMode blendMode, SurfacePaint paint) {
|
||||
_hasArbitraryPaint = true;
|
||||
_didDraw = true;
|
||||
_growPaintBoundsByPoints(vertices.positions, 0);
|
||||
_growPaintBoundsByPoints(vertices.positions, 0, paint);
|
||||
_commands.add(PaintVertices(vertices, blendMode, paint.paintData));
|
||||
}
|
||||
|
||||
void drawRawPoints(
|
||||
ui.PointMode pointMode, Float32List points, ui.Paint paint) {
|
||||
ui.PointMode pointMode, Float32List points, SurfacePaint paint) {
|
||||
if (paint.strokeWidth == null) {
|
||||
return;
|
||||
}
|
||||
_hasArbitraryPaint = true;
|
||||
_didDraw = true;
|
||||
_growPaintBoundsByPoints(points, paint.strokeWidth);
|
||||
_growPaintBoundsByPoints(points, paint.strokeWidth, paint);
|
||||
_commands
|
||||
.add(PaintPoints(pointMode, points, paint.strokeWidth, paint.color));
|
||||
}
|
||||
|
||||
void _growPaintBoundsByPoints(Float32List points, double thickness) {
|
||||
void _growPaintBoundsByPoints(Float32List points, double thickness, SurfacePaint paint) {
|
||||
double minValueX, maxValueX, minValueY, maxValueY;
|
||||
minValueX = maxValueX = points[0];
|
||||
minValueY = maxValueY = points[1];
|
||||
@@ -424,8 +430,13 @@ class RecordingCanvas {
|
||||
maxValueY = math.max(maxValueY, y);
|
||||
}
|
||||
final double distance = thickness / 2.0;
|
||||
_paintBounds.growLTRB(minValueX - distance, minValueY - distance,
|
||||
maxValueX + distance, maxValueY + distance);
|
||||
final double paintSpread = _getPaintSpread(paint);
|
||||
_paintBounds.growLTRB(
|
||||
minValueX - distance - paintSpread,
|
||||
minValueY - distance - paintSpread,
|
||||
maxValueX + distance + paintSpread,
|
||||
maxValueY + distance + paintSpread,
|
||||
);
|
||||
}
|
||||
|
||||
int _saveCount = 1;
|
||||
@@ -1937,3 +1948,28 @@ 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;
|
||||
}
|
||||
|
||||
@@ -244,6 +244,7 @@ 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,
|
||||
|
||||
@@ -341,7 +341,9 @@ void main() async {
|
||||
path.addRect(const Rect.fromLTRB(20, 30, 100, 110));
|
||||
rc.drawShadow(path, const Color(0xFFFF0000), 2.0, true);
|
||||
expect(
|
||||
rc.computePaintBounds(), const Rect.fromLTRB(15.0, 27.0, 106.0, 117.0));
|
||||
rc.computePaintBounds(),
|
||||
within(distance: 0.05, from: const Rect.fromLTRB(17.9, 28.5, 103.5, 114.1)),
|
||||
);
|
||||
await _checkScreenshot(rc, 'path_with_shadow');
|
||||
});
|
||||
|
||||
@@ -440,6 +442,177 @@ 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<PaintSpreadPainter> painters = <PaintSpreadPainter>[
|
||||
(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 {
|
||||
|
||||
@@ -0,0 +1,162 @@
|
||||
// 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,
|
||||
);
|
||||
},
|
||||
timeout: const Timeout(Duration(seconds: 10)),
|
||||
testOn: 'chrome',
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user