blob: c5bebaf7fa3506fd33c026e96963dbebb34fbbdf [file] [log] [blame]
// 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.
// @dart = 2.6
part of engine;
/// How far is the light source from the surface of the UI.
///
/// 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(double elevation) {
if (elevation == 0.0) {
return ui.Offset.zero;
}
final double dx = -kLightOffsetX * elevation / kLightHeight;
final double dy = -kLightOffsetY * elevation / kLightHeight;
return ui.Offset(dx, dy);
}
/// 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,
});
/// 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 {
color = toShadowColor(color);
element.style.boxShadow = '${shadow.offset.dx}px ${shadow.offset.dy}px '
'${shadow.blurWidth}px 0px rgba(${color.red}, ${color.green}, ${color.blue}, ${color.alpha / 255})';
}
}
/// Converts a shadow color specified by the framework to the color that should
/// actually be applied when rendering the shadow.
///
/// Flutter shadows look softer than the color specified by the developer. For
/// example, it is common to get a solid black for a shadow and see a very soft
/// shadow. This function softens the color by reducing its alpha by a constant
/// factor.
ui.Color toShadowColor(ui.Color color) {
// Reduce alpha to make shadows less aggressive:
//
// - https://github.com/flutter/flutter/issues/52734
// - https://github.com/flutter/gallery/issues/118
final int reducedAlpha = (0.3 * color.alpha).round();
return ui.Color((reducedAlpha & 0xff) << 24 | (color.value & 0x00ffffff));
}