| // 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)); |
| } |