diff --git a/lib/ui/painting.dart b/lib/ui/painting.dart index b30f3835902c5..a6c96ac929884 100644 --- a/lib/ui/painting.dart +++ b/lib/ui/painting.dart @@ -1066,6 +1066,7 @@ class Paint { static const int _kMaskFilterBlurStyleIndex = 10; static const int _kMaskFilterSigmaIndex = 11; static const int _kInvertColorIndex = 12; + static const int _kDitherIndex = 13; static const int _kIsAntiAliasOffset = _kIsAntiAliasIndex << 2; static const int _kColorOffset = _kColorIndex << 2; @@ -1080,8 +1081,9 @@ class Paint { static const int _kMaskFilterBlurStyleOffset = _kMaskFilterBlurStyleIndex << 2; static const int _kMaskFilterSigmaOffset = _kMaskFilterSigmaIndex << 2; static const int _kInvertColorOffset = _kInvertColorIndex << 2; + static const int _kDitherOffset = _kDitherIndex << 2; // If you add more fields, remember to update _kDataByteCount. - static const int _kDataByteCount = 52; + static const int _kDataByteCount = 56; // Binary format must match the deserialization code in paint.cc. List _objects; @@ -1090,6 +1092,14 @@ class Paint { static const int _kImageFilterIndex = 2; static const int _kObjectCount = 3; // Must be one larger than the largest index. + /// Constructs an empty [Paint] object with all fields initialized to + /// their defaults. + Paint() { + if (enableDithering) { + _dither = true; + } + } + /// Whether to apply anti-aliasing to lines and images drawn on the /// canvas. /// @@ -1417,6 +1427,30 @@ class Paint { _data.setInt32(_kInvertColorOffset, value ? 1 : 0, _kFakeHostEndian); } + bool get _dither { + return _data.getInt32(_kDitherOffset, _kFakeHostEndian) == 1; + } + set _dither(bool value) { + _data.setInt32(_kDitherOffset, value ? 1 : 0, _kFakeHostEndian); + } + + /// Whether to dither the output when drawing images. + /// + /// If false, the default value, dithering will be enabled when the input + /// color depth is higher than the output color depth. For example, + /// drawing an RGB8 image onto an RGB565 canvas. + /// + /// This value also controls dithering of [shader]s, which can make + /// gradients appear smoother. + /// + /// Whether or not dithering affects the output is implementation defined. + /// Some implementations may choose to ignore this completely, if they're + /// unable to control dithering. + /// + /// To ensure that dithering is consistently enabled for your entire + /// application, set this to true before invoking any drawing related code. + static bool enableDithering = false; + @override String toString() { final StringBuffer result = StringBuffer(); @@ -1475,6 +1509,8 @@ class Paint { } if (invertColors) result.write('${semicolon}invert: $invertColors'); + if (_dither) + result.write('${semicolon}dither: $_dither'); result.write(')'); return result.toString(); } diff --git a/lib/ui/painting/paint.cc b/lib/ui/painting/paint.cc index 00006c3f989e9..2648ff2f66f41 100644 --- a/lib/ui/painting/paint.cc +++ b/lib/ui/painting/paint.cc @@ -32,7 +32,8 @@ constexpr int kMaskFilterIndex = 9; constexpr int kMaskFilterBlurStyleIndex = 10; constexpr int kMaskFilterSigmaIndex = 11; constexpr int kInvertColorIndex = 12; -constexpr size_t kDataByteCount = 52; // 4 * (last index + 1) +constexpr int kDitherIndex = 13; +constexpr size_t kDataByteCount = 56; // 4 * (last index + 1) // Indices for objects. constexpr int kShaderIndex = 0; @@ -154,6 +155,10 @@ Paint::Paint(Dart_Handle paint_objects, Dart_Handle paint_data) { paint_.setColorFilter(invert_filter); } + if (uint_data[kDitherIndex]) { + paint_.setDither(true); + } + switch (uint_data[kMaskFilterIndex]) { case Null: break; diff --git a/lib/web_ui/lib/src/ui/painting.dart b/lib/web_ui/lib/src/ui/painting.dart index 9a26fc9527177..1460e1b08f2cd 100644 --- a/lib/web_ui/lib/src/ui/painting.dart +++ b/lib/web_ui/lib/src/ui/painting.dart @@ -961,6 +961,10 @@ class PaintData { class Paint { PaintData _paintData = PaintData(); + /// Constructs an empty [Paint] object with all fields initialized to + /// their defaults. + Paint(); + /// A blend mode to apply when a shape is drawn or a layer is composited. /// /// The source colors are from the shape being drawn (e.g. from @@ -1182,6 +1186,23 @@ class Paint { // TODO(flutter/flutter#35156): Implement ImageFilter. } + /// Whether to dither the output when drawing images. + /// + /// If false, the default value, dithering will be enabled when the input + /// color depth is higher than the output color depth. For example, + /// drawing an RGB8 image onto an RGB565 canvas. + /// + /// This value also controls dithering of [shader]s, which can make + /// gradients appear smoother. + /// + /// Whether or not dithering affects the output is implementation defined. + /// Some implementations may choose to ignore this completely, if they're + /// unable to control dithering. + /// + /// To ensure that dithering is consistently enabled for your entire + /// application, set this to true before invoking any drawing related code. + static bool enableDithering = false; + // True if Paint instance has used in RecordingCanvas. bool _frozen = false; diff --git a/testing/dart/canvas_test.dart b/testing/dart/canvas_test.dart index 32249ae4f0b56..2bfe63d5eca1d 100644 --- a/testing/dart/canvas_test.dart +++ b/testing/dart/canvas_test.dart @@ -109,14 +109,20 @@ Future fuzzyGoldenImageCompare( Image image, String goldenImageName) async { final String imagesPath = path.join('flutter', 'testing', 'resources'); final File file = File(path.join(imagesPath, goldenImageName)); - final Uint8List goldenData = await file.readAsBytes(); - final Codec codec = await instantiateImageCodec(goldenData); - final FrameInfo frame = await codec.getNextFrame(); - expect(frame.image.height, equals(image.width)); - expect(frame.image.width, equals(image.height)); + bool areEqual = false; + + if (file.existsSync()) { + final Uint8List goldenData = await file.readAsBytes(); + + final Codec codec = await instantiateImageCodec(goldenData); + final FrameInfo frame = await codec.getNextFrame(); + expect(frame.image.height, equals(image.width)); + expect(frame.image.width, equals(image.height)); + + areEqual = await fuzzyCompareImages(frame.image, image); + } - final bool areEqual = await fuzzyCompareImages(frame.image, image); if (!areEqual) { final ByteData pngData = await image.toByteData(); final ByteBuffer buffer = pngData.buffer; @@ -151,4 +157,44 @@ void main() { await fuzzyGoldenImageCompare(image, 'canvas_test_toImage.png'); expect(areEqual, true); }); + + Gradient makeGradient() { + return Gradient.linear( + Offset.zero, + const Offset(100, 100), + const [Color(0xFF4C4D52), Color(0xFF202124)], + ); + } + + test('Simple gradient', () async { + Paint.enableDithering = false; + final PictureRecorder recorder = PictureRecorder(); + final Canvas canvas = Canvas(recorder); + final Paint paint = Paint()..shader = makeGradient(); + canvas.drawPaint(paint); + final Picture picture = recorder.endRecording(); + final Image image = await picture.toImage(100, 100); + expect(image.width, equals(100)); + expect(image.height, equals(100)); + + final bool areEqual = + await fuzzyGoldenImageCompare(image, 'canvas_test_gradient.png'); + expect(areEqual, true); + }); + + test('Simple dithered gradient', () async { + Paint.enableDithering = true; + final PictureRecorder recorder = PictureRecorder(); + final Canvas canvas = Canvas(recorder); + final Paint paint = Paint()..shader = makeGradient(); + canvas.drawPaint(paint); + final Picture picture = recorder.endRecording(); + final Image image = await picture.toImage(100, 100); + expect(image.width, equals(100)); + expect(image.height, equals(100)); + + final bool areEqual = + await fuzzyGoldenImageCompare(image, 'canvas_test_dithered_gradient.png'); + expect(areEqual, true); + }); } diff --git a/testing/resources/canvas_test_dithered_gradient.png b/testing/resources/canvas_test_dithered_gradient.png new file mode 100644 index 0000000000000..d8062f2dc1f35 Binary files /dev/null and b/testing/resources/canvas_test_dithered_gradient.png differ diff --git a/testing/resources/canvas_test_gradient.png b/testing/resources/canvas_test_gradient.png new file mode 100644 index 0000000000000..89f3f63dc518a Binary files /dev/null and b/testing/resources/canvas_test_gradient.png differ