shadows method
Draw this scrim as stacked blurred copies of the glyph, for renderers that can't widen it (Flutter text Shadow, CSS text-shadow). Approximate; prefer shadowsWithStroke.
blurRadius is the blur you intend to draw (Skia/CSS radius). It is
rounded to a whole pixel; draw with Shadows.blurRadius.
contentRadius is half the stroke thickness being protected, roughly
half the font's stem width. The default, -1, assumes strokes at least
as wide as the blur. Thinner strokes need more layers: pass the real
value, or Shadows.meetsTarget overstates the result.
maxLayers caps the stack. Past it, Shadows.meetsTarget is false
and the layers are a best effort.
Implementation
Shadows shadows({
required double blurRadius,
double contentRadius = -1.0,
int maxLayers = 8,
}) {
if (blurRadius.isNaN || blurRadius.isInfinite || blurRadius < 0) {
throw ArgumentError.value(blurRadius, 'blurRadius');
}
if (contentRadius.isNaN || contentRadius.isInfinite) {
throw ArgumentError.value(contentRadius, 'contentRadius');
}
if (maxLayers < 1) {
throw ArgumentError.value(maxLayers, 'maxLayers');
}
final alpha = opacity;
if (alpha == 0.0) {
return Shadows._(this, 0, const [], meetsTarget, 0);
}
// Round the radius once: sigma and coverage must come from the same
// whole-pixel radius the caller draws with.
final r = blurRadius.round();
if (r == 0) {
// Needed, but a zero-blur shadow is invisible.
return Shadows._(this, 0, const [], false, 0);
}
// Edge coverage: the Gaussian edge profile at the first outside pixel's
// center, 0.5px past the edge. The content spans [0.5, 0.5 + 2*cr] away
// from it. Calibrated against Skia pixels in
// shadow_pixel_calibration_test.dart.
final sigma = _convertRadiusToSigma(r.toDouble());
final cr = contentRadius < 0 ? r : contentRadius.round();
final e = contentRadius < 0
? 1.0 - _phi(0.5 / sigma)
: _phi((0.5 + 2.0 * cr) / sigma) - _phi(0.5 / sigma);
if (e <= 0.0) {
return Shadows._(this, r.toDouble(), const [], false, e);
}
// n full layers reach 1 - (1-e)^n at the first outside pixel, so the
// fewest that reach alpha is ceil(log(1 - alpha) / log(1 - e)).
// alpha == 1 is unreachable; the cap reports shortfalls via meetsTarget.
final n = alpha >= 1.0
? maxLayers + 1
: (math.log(1 - alpha) / math.log(1 - e)).ceil().clamp(
1,
maxLayers + 1,
);
if (n > maxLayers) {
return Shadows._(
this,
r.toDouble(),
List.filled(maxLayers, 1.0),
false,
e,
);
}
// Even split hits alpha exactly; round up to the next 1/255 so the drawn
// stack never lands below the proven minimum.
final perLayerRaw = (1 - math.pow(1 - alpha, 1 / n)) / e;
final perLayer = (perLayerRaw * 255).ceil().clamp(0, 255) / 255.0;
return Shadows._(
this,
r.toDouble(),
List.filled(n, perLayer),
meetsTarget,
e,
);
}