blob: 7700abaf28ce7915c7609de046833fc6ac3e2e5d [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.
import 'dart:math' as math;
import 'dart:typed_data';
import 'package:ui/ui.dart' as ui;
import '../util.dart';
import '../vector_math.dart';
import 'canvaskit_api.dart';
import 'path.dart';
/// An error related to the CanvasKit rendering backend.
class CanvasKitError extends Error {
CanvasKitError(this.message);
/// Describes this error.
final String message;
@override
String toString() => 'CanvasKitError: $message';
}
/// Creates a new color array.
Float32List makeFreshSkColor(ui.Color color) {
final Float32List result = Float32List(4);
result[0] = color.red / 255.0;
result[1] = color.green / 255.0;
result[2] = color.blue / 255.0;
result[3] = color.alpha / 255.0;
return result;
}
ui.TextPosition fromPositionWithAffinity(SkTextPosition positionWithAffinity) {
final ui.TextAffinity affinity =
ui.TextAffinity.values[positionWithAffinity.affinity.value];
return ui.TextPosition(
offset: positionWithAffinity.pos,
affinity: affinity,
);
}
/// Shadow flag constants derived from Skia's SkShadowFlags.h.
class SkiaShadowFlags {
/// The occluding object is opaque, making the part of the shadow under the
/// occluder invisible. This allows some optimizations because some parts of
/// the shadow do not need to be accurate.
static const int kNone_ShadowFlag = 0x00;
/// The occluding object is not opaque, making the part of the shadow under the
/// occluder visible. This requires that the shadow is rendered more accurately
/// and therefore is slightly more expensive.
static const int kTransparentOccluder_ShadowFlag = 0x01;
/// Light position represents a direction, light radius is blur radius at
/// elevation 1.
///
/// This makes the shadow to have a fixed position relative to the shape that
/// casts it.
static const int kDirectionalLight_ShadowFlag = 0x04;
/// Complete value for the `flags` argument for opaque occluder.
static const int kDefaultShadowFlags =
kDirectionalLight_ShadowFlag | kNone_ShadowFlag;
/// Complete value for the `flags` argument for transparent occluder.
static const int kTransparentOccluderShadowFlags =
kDirectionalLight_ShadowFlag | kTransparentOccluder_ShadowFlag;
}
// These numbers have been chosen empirically to give a result closest to the
// material spec.
const double ckShadowAmbientAlpha = 0.039;
const double ckShadowSpotAlpha = 0.25;
const double ckShadowLightRadius = 1.1;
const double ckShadowLightHeight = 600.0;
const double ckShadowLightXOffset = 0;
const double ckShadowLightYOffset = -450;
const double ckShadowLightXTangent = ckShadowLightXOffset / ckShadowLightHeight;
const double ckShadowLightYTangent = ckShadowLightYOffset / ckShadowLightHeight;
/// Computes the smallest rectangle that contains the shadow.
// Most of this logic is borrowed from SkDrawShadowInfo.cpp in Skia.
// TODO(yjbanov): switch to SkDrawShadowMetrics::GetLocalBounds when available
// See:
// - https://bugs.chromium.org/p/skia/issues/detail?id=11146
// - https://github.com/flutter/flutter/issues/73492
ui.Rect computeSkShadowBounds(
CkPath path,
double elevation,
double devicePixelRatio,
Matrix4 matrix,
) {
ui.Rect pathBounds = path.getBounds();
if (elevation == 0) {
return pathBounds;
}
// For visual correctness the shadow offset and blur does not change with
// parent transforms. Therefore, in general case we have to first transform
// the shape bounds to device coordinates, then compute the shadow bounds,
// then transform the bounds back to local coordinates. However, if the
// transform is an identity or translation (a common case), we can skip this
// step. With directional lighting translation does not affect the size or
// shape of the shadow. Skipping this step saves us two transformRects and
// one matrix inverse.
final bool isComplex = !matrix.isIdentityOrTranslation();
if (isComplex) {
pathBounds = transformRect(matrix, pathBounds);
}
double left = pathBounds.left;
double top = pathBounds.top;
double right = pathBounds.right;
double bottom = pathBounds.bottom;
final double ambientBlur = ambientBlurRadius(elevation);
final double spotBlur = ckShadowLightRadius * elevation;
final double spotOffsetX = -elevation * ckShadowLightXTangent;
final double spotOffsetY = -elevation * ckShadowLightYTangent;
// The extra +1/-1 are to cover possible floating point errors.
left = left - 1 + (spotOffsetX - ambientBlur - spotBlur) * devicePixelRatio;
top = top - 1 + (spotOffsetY - ambientBlur - spotBlur) * devicePixelRatio;
right = right + 1 + (spotOffsetX + ambientBlur + spotBlur) * devicePixelRatio;
bottom =
bottom + 1 + (spotOffsetY + ambientBlur + spotBlur) * devicePixelRatio;
final ui.Rect shadowBounds = ui.Rect.fromLTRB(left, top, right, bottom);
if (isComplex) {
final Matrix4 inverse = Matrix4.zero();
// The inverse only makes sense if the determinat is non-zero.
if (inverse.copyInverse(matrix) != 0.0) {
return transformRect(inverse, shadowBounds);
} else {
return shadowBounds;
}
} else {
return shadowBounds;
}
}
const double kAmbientHeightFactor = 1.0 / 128.0;
const double kAmbientGeomFactor = 64.0;
const double kMaxAmbientRadius =
300 * kAmbientHeightFactor * kAmbientGeomFactor;
double ambientBlurRadius(double height) {
return math.min(
height * kAmbientHeightFactor * kAmbientGeomFactor, kMaxAmbientRadius);
}
void drawSkShadow(
SkCanvas skCanvas,
CkPath path,
ui.Color color,
double elevation,
bool transparentOccluder,
double devicePixelRatio,
) {
final int flags = transparentOccluder
? SkiaShadowFlags.kTransparentOccluderShadowFlags
: SkiaShadowFlags.kDefaultShadowFlags;
final ui.Color inAmbient =
color.withAlpha((color.alpha * ckShadowAmbientAlpha).round());
final ui.Color inSpot = color.withAlpha((color.alpha * ckShadowSpotAlpha).round());
final SkTonalColors inTonalColors = SkTonalColors(
ambient: makeFreshSkColor(inAmbient),
spot: makeFreshSkColor(inSpot),
);
final SkTonalColors tonalColors = canvasKit.computeTonalColors(inTonalColors);
skCanvas.drawShadow(
path.skiaObject,
Float32List(3)..[2] = devicePixelRatio * elevation,
Float32List(3)
..[0] = ckShadowLightXOffset
..[1] = ckShadowLightYOffset
..[2] = devicePixelRatio * ckShadowLightHeight,
devicePixelRatio * ckShadowLightRadius,
tonalColors.ambient,
tonalColors.spot,
flags,
);
}