Masking two images in Flutter using a Custom Painter

3,274

The key is in when to call saveLayer and when to call restore.

From here:

When using Canvas.saveLayer and Canvas.restore, the blend mode of the Paint given to the Canvas.saveLayer will be applied when Canvas.restore is called. Each call to Canvas.saveLayer introduces a new layer onto which shapes and images are painted; when Canvas.restore is called, that layer is then composited onto the parent layer, with the source being the most-recently-drawn shapes and images, and the destination being the parent layer. (For the first Canvas.saveLayer call, the parent layer is the canvas itself.)

Working code

  @override
  void paint(Canvas canvas, Size size) {
    if (image != null && mask != null) {
      var rect = Rect.fromLTRB(0, 0, 200, 200);
      Size outputSize = rect.size;
      Paint paint = new Paint();

      //Mask
      Size maskInputSize = Size(mask.width.toDouble(), mask.height.toDouble());
      final FittedSizes maskFittedSizes =
          applyBoxFit(BoxFit.cover, maskInputSize, outputSize);
      final Size maskSourceSize = maskFittedSizes.source;

      final Rect maskSourceRect = Alignment.center
          .inscribe(maskSourceSize, Offset.zero & maskInputSize);

      canvas.saveLayer(rect, paint);
      canvas.drawImageRect(mask, maskSourceRect, rect, paint);

      //Image
      Size inputSize = Size(image.width.toDouble(), image.height.toDouble());
      final FittedSizes fittedSizes =
          applyBoxFit(BoxFit.cover, inputSize, outputSize);
      final Size sourceSize = fittedSizes.source;
      final Rect sourceRect =
          Alignment.center.inscribe(sourceSize, Offset.zero & inputSize);

      canvas.drawImageRect(
          image, sourceRect, rect, paint..blendMode = BlendMode.srcIn);
      canvas.restore();
    }
  }

Result:

enter image description here

Share:
3,274
Daniel Wilson
Author by

Daniel Wilson

Updated on November 21, 2022

Comments

  • Daniel Wilson
    Daniel Wilson over 1 year

    Hello can anyone tell me why the background to this masking attempt is black. This must be close but I just can't kill the background. I've seen others reference that saveLayer(rect, paint) is the key here as that shoves the whole canvas rect in to the masking operation. This question (no masking operation) and this one (no actual answer) are similar but were no use to me.

    main.dart

    import 'dart:typed_data';
    import 'dart:ui' as ui;
    
    import 'package:flutter/cupertino.dart';
    import 'package:flutter/material.dart';
    import 'package:flutter/services.dart' show rootBundle;
    
    void main() {
      runApp(MyApp());
    }
    
    class MyApp extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          home: MyHomePage(title: 'Flutter Demo Home Page'),
        );
      }
    }
    
    class MyHomePage extends StatefulWidget {
      MyHomePage({Key key, this.title}) : super(key: key);
    
      final String title;
    
      @override
      _MyHomePageState createState() => _MyHomePageState();
    }
    
    class _MyHomePageState extends State<MyHomePage> {
      ui.Image mask;
      ui.Image image;
    
      @override
      void initState() {
        super.initState();
        load('images/squircle.png').then((i) {
          setState(() {
            mask = i;
          });
        });
        load('images/noodlejpg.jpg').then((i) {
          setState(() {
            image = i;
          });
        });
      }
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(backgroundColor: Colors.blue, title: Text('I am a title')),
          body: SafeArea(
            child: SizedBox(
              width: 200.0,
              height: 200.0,
              child: CustomPaint(painter: OverlayPainter(mask, image)),
            ),
          ),
        );
      }
    
      Future<ui.Image> load(String asset) async {
        ByteData data = await rootBundle.load(asset);
        ui.Codec codec = await ui.instantiateImageCodec(data.buffer.asUint8List());
        ui.FrameInfo fi = await codec.getNextFrame();
        return fi.image;
      }
    }
    
    class OverlayPainter extends CustomPainter {
      ui.Image mask;
      ui.Image image;
    
      OverlayPainter(this.mask, this.image);
    
      @override
      void paint(Canvas canvas, Size size) {
        var paint = new Paint();
        paint.isAntiAlias = true;
    
        if (image != null) {
          var rect = Rect.fromLTRB(0, 0, 200, 200);
          Size outputSize = rect.size;
          Size inputSize = Size(image.width.toDouble(), image.height.toDouble());
          final FittedSizes fittedSizes =
              applyBoxFit(BoxFit.cover, inputSize, outputSize);
          final Size sourceSize = fittedSizes.source;
    
          canvas.save();
          final Rect sourceRect = Alignment.center.inscribe(
            sourceSize,
            Offset.zero & inputSize,
          );
          canvas.drawImageRect(image, sourceRect, rect, paint);
          canvas.restore();
        }
    
        if (mask != null) {
          var rect = Rect.fromLTRB(0, 0, 200, 200);
          Size outputSize = rect.size;
          Size inputSize = Size(mask.width.toDouble(), mask.height.toDouble());
          final FittedSizes fittedSizes =
              applyBoxFit(BoxFit.cover, inputSize, outputSize);
          final Size sourceSize = fittedSizes.source;
    
          canvas.saveLayer(rect, Paint()..blendMode = BlendMode.dstIn);
    
          final Rect sourceRect = Alignment.center.inscribe(
            sourceSize,
            Offset.zero & inputSize,
          );
          canvas.drawImageRect(mask, sourceRect, rect, paint);
          canvas.restore();
        }
      }
    
      @override
      bool shouldRepaint(OverlayPainter oldDelegate) {
        return mask != oldDelegate.mask || image != oldDelegate.image;
      }
    }
    

    noodlejpg.jpg

    enter image description here

    squircle.jpg

    enter image description here

    result

    enter image description here

  • Daniel Wilson
    Daniel Wilson about 4 years
    You are right thank you, I was getting confused sharing Paint objects or something. I've updated the code