Skip to content

Custom Gradient Transformation Causes Dithering Artifacts in Radial Gradient #145481

@tguerin

Description

@tguerin

Steps to reproduce

When utilizing a GradientTransform to scale and translate a RadialGradient, dithering artifacts are observed with the Impeller rendering engine, an issue that is not present when using Skia.

Expected results

Non dithering artifacts on Impeller

Screenshot 2024-03-20 at 17 21 41

Actual results

Dithering artifacts on Impeller

Screenshot 2024-03-20 at 17 22 04

Code sample

Code sample
import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return Center(
      child: Container(
        width: 300,
        height: 100,
        decoration: BoxDecoration(
          gradient: EllipticalGradient(
            colors: [
              Color(0xFF01F6BB).withOpacity(0.15),
              Color(0xFF01F6BB).withOpacity(0),
            ],
            stops: const [0.0, 1.0],
            ellipseRelativeCenter: const Offset(0.5, 1),
            ellipseScale: const Scale(widthFactor: 0.9, heightFactor: 0.6),
            backgroundColor: Color(0xFF1C2B3E),
          ),
        ),
      ),
    );
  }
}

class EllipticalGradient extends Gradient {
  final Color backgroundColor;

  /// This offset is relative to top left of the widget, [0,0] means top left and [1,1] bottom right.
  ///
  /// The range is not limited to [0,1] for both x and y axis.
  final Offset ellipseRelativeCenter;
  final Scale ellipseScale;

  const EllipticalGradient({
    required super.colors,
    required this.backgroundColor,
    required this.ellipseRelativeCenter,
    required this.ellipseScale,
    super.stops,
  });

  @override
  Shader createShader(Rect rect, {TextDirection? textDirection}) {
    return RadialGradient(
      center: Alignment(ellipseRelativeCenter.dx * 2 - 1, ellipseRelativeCenter.dy * 2 - 1),
      colors: colors.map((color) => Color.alphaBlend(color, backgroundColor)).toList(),
      radius: 1,
      stops: stops,
      transform: _EllipseTransform(
        ellipseRelativeCenter: ellipseRelativeCenter,
        ellipseScale: ellipseScale,
      ),
    ).createShader(rect, textDirection: textDirection);
  }

  @override
  EllipticalGradient scale(double factor) {
    return EllipticalGradient(
      backgroundColor: backgroundColor,
      colors: colors.map<Color>((Color color) => Color.lerp(null, color, factor)!).toList(),
      ellipseRelativeCenter: ellipseRelativeCenter,
      ellipseScale: ellipseScale,
    );
  }
}

class _EllipseTransform extends GradientTransform {
  final Offset ellipseRelativeCenter;
  final Scale ellipseScale;

  const _EllipseTransform({
    required this.ellipseRelativeCenter,
    required this.ellipseScale,
  });

  @override
  Matrix4? transform(Rect bounds, {TextDirection? textDirection}) {
    final double widthFactor;
    final double heightFactor;
    if (bounds.width > bounds.height) {
      heightFactor = ellipseScale.heightFactor;
      widthFactor = ellipseScale.widthFactor * bounds.width / bounds.height;
    } else {
      heightFactor = ellipseScale.heightFactor * bounds.height / bounds.width;
      widthFactor = ellipseScale.widthFactor;
    }
    final transformMatrix = Matrix4.identity()..scale(widthFactor, heightFactor);
    final Offset originalCenterOffset = Offset(
      bounds.left + bounds.width * ellipseRelativeCenter.dx,
      bounds.top + bounds.height * ellipseRelativeCenter.dy,
    );

    final List<double> offsetLocation =
        transformMatrix.applyToVector3Array([originalCenterOffset.dx, originalCenterOffset.dy, 0.0]);
    final dx = originalCenterOffset.dx - offsetLocation[0];
    final dy = originalCenterOffset.dy - offsetLocation[1];
    return transformMatrix..translate(dx / widthFactor, dy / heightFactor);
  }
}

@immutable
class Scale {
  final double heightFactor;
  final double widthFactor;

  const Scale({
    required this.heightFactor,
    required this.widthFactor,
  });
}

Screenshots or Video

Screenshots / Video demonstration

[Upload media here]

Logs

Logs
[Paste your logs here]

Flutter Doctor output

Doctor output
[✓] Flutter (Channel stable, 3.19.0, on macOS 14.0 23A344 darwin-arm64, locale en-FR)
    • Flutter version 3.19.0 on channel stable at /Users/t.guerin-ext/fvm/versions/3.19.0
    • Upstream repository https://github.com/flutter/flutter.git
    • Framework revision bae5e49bc2 (5 weeks ago), 2024-02-13 17:46:18 -0800
    • Engine revision 04817c99c9
    • Dart version 3.3.0
    • DevTools version 2.31.1

[!] Android toolchain - develop for Android devices (Android SDK version 34.0.0)
    • Android SDK at /Users/t.guerin-ext/Library/Android/sdk
    ✗ cmdline-tools component is missing
      Run `path/to/sdkmanager --install "cmdline-tools;latest"`
      See https://developer.android.com/studio/command-line for more details.
    ✗ Android license status unknown.
      Run `flutter doctor --android-licenses` to accept the SDK licenses.
      See https://flutter.dev/docs/get-started/install/macos#android-setup for more details.

[✓] Xcode - develop for iOS and macOS (Xcode 15.3)
    • Xcode at /Applications/Xcode.app/Contents/Developer
    • Build 15E204a
    • CocoaPods version 1.13.0

[✓] Chrome - develop for the web
    • Chrome at /Applications/Google Chrome.app/Contents/MacOS/Google Chrome

[✓] Android Studio (version 2023.2)
    • Android Studio at /Applications/Android Studio.app/Contents
    • Flutter plugin can be installed from:
      🔨 https://plugins.jetbrains.com/plugin/9212-flutter
    • Dart plugin can be installed from:
      🔨 https://plugins.jetbrains.com/plugin/6351-dart
    • Java version OpenJDK Runtime Environment (build 17.0.9+0-17.0.9b1087.7-11185874)

[✓] IntelliJ IDEA Ultimate Edition (version 2023.3.5)
    • IntelliJ at /Users/t.guerin-ext/Applications/IntelliJ IDEA Ultimate.app
    • Flutter plugin version 78.1.1
    • Dart plugin version 233.13763.5

[✓] Connected device (3 available)
    • iPhone 15 (mobile) • E120F861-78FD-4111-A954-4EB49AC9A2C9 • ios            • com.apple.CoreSimulator.SimRuntime.iOS-17-4 (simulator)
    • macOS (desktop)    • macos                                • darwin-arm64   • macOS 14.0 23A344 darwin-arm64
    • Chrome (web)       • chrome                               • web-javascript • Google Chrome 122.0.6261.129

[✓] Network resources
    • All expected network resources are available.

Metadata

Metadata

Assignees

Labels

P2Important issues not at the top of the work liste: impellerImpeller rendering backend issues and features requestsengineflutter/engine related. See also e: labels.found in release: 3.19Found to occur in 3.19found in release: 3.21Found to occur in 3.21has reproducible stepsThe issue has been confirmed reproducible and is ready to work onr: fixedIssue is closed as already fixed in a newer versionteam-engineOwned by Engine teamtriaged-engineTriaged by Engine team

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions