shadows method

Shadows shadows({
  1. required double blurRadius,
  2. double contentRadius = -1.0,
  3. int maxLayers = 8,
})

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