Flutter绘制-04-BlendMode专项

3,185 阅读8分钟

查看目录-->

魔障

BlendMode 一直以来是个让我看到就躲的角色,那几十个模式,一看就头大。最近决心研究透这个东西,一路艰难,如:

  • 好像没有什么可参考的代码;
  • 好像也没什么具体的博客文章可参考(除了原文的翻译)
  • 最大的问题是,你写出的效果,跟文档描述不一样,那个四色条和鸟的效果你看不懂,这是最要命的。

不知你是否也遇到过这些问题。

有条件的总结

先说官方api:api.flutter.dev/flutter/dar… 看上去很详尽,但不能立刻让你下笔如有神。

BlendMode在很多地方都用到了,我这里所谓的“有条件”,仅是指在一些局部条件下,感觉有些理解了,以此做记录。

demo前提条件

  • 在使用Canvas绘制时,saveLayer与restore之间生效。

BlendMode是什么

从官方api抽取关键点:

  • 在画布上绘制时使用的算法。
  • BlendMode是一个枚举,每个枚举值代表一个算法。
  • 在画布上绘制形状或图像时,可以使用不同的算法来混合像素。
  • 混合像素?混合的是谁呢,src和dst。看下面示意图。
  • src和dst,可以看做是屏幕上的两层像素点,BlendMode决定这两层像素点如何叠加。

  • 如图,在框与圆的重合部分,后画的覆盖先画的,也就是3在2上面,6在5上面。
  • 注意,有个红框bg,不参与blendmode计算。只有src与dst参与计算。

注意

  • 关于图形的亮度、饱和度和色调,请了解下,可参考:jingyan.baidu.com/article/d2b…
  • 模式解释,也是来源于官方api,部分会加上个人理解

那二十多个模式

先看示意图,请记住代表src dst和bg的图形:

原始代码如下:

import 'package:flutter/material.dart';
import 'dart:math';
import 'dart:ui';
import 'dart:ui' as ui;

import 'package:flutter/services.dart';

class TestPaintBlendMode extends StatefulWidget {
  @override
  State<StatefulWidget> createState() {
    return TestPaintState();
  }
}

class TestPaintState extends State<TestPaintBlendMode> {
  ui.Image _src;
  ui.Image _dst;
  @override
  void initState() {
    super.initState();
    _loadImage();
  }

  void _loadImage() async {
    _src = await loadImageFromAssets('images/a2.png');
    _dst = await loadImageFromAssets('images/dst.jpeg');
    setState(() {});
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("test paint"),
      ),
      body: CustomPaint(
        painter: _MyPaint(_src, _dst),
        size: MediaQuery.of(context).size,
      ),
      backgroundColor: Colors.white,
    );
  }

  Future<ui.Image> loadImageFromAssets(String path) async {
    ByteData data = await rootBundle.load(path);
    List<int> bytes =
        data.buffer.asUint8List(data.offsetInBytes, data.lengthInBytes);
    return decodeImageFromList(bytes);
  }
}

class _MyPaint extends CustomPainter {
  ui.Image _src;
  ui.Image _dst;
  _MyPaint(this._src, this._dst);
  @override
  void paint(Canvas canvas, Size size) {
//    translateToCenter(canvas, size);
    print("size=" + size.width.toString() + " " + size.height.toString());
    _test(canvas, size);
  }

  void _test(Canvas canvas, Size size) {
    Paint _paint = Paint();
    _paint.isAntiAlias = true;
    canvas.drawPaint(_paint..color = Colors.black12);// 画背景

    _paint.color = Colors.red;
    canvas.drawCircle(Offset(150, 100), 100, _paint); // 画红圆

    canvas.saveLayer(Offset.zero & size, Paint()); // 新建层

    _paint.color = Colors.yellowAccent;
    canvas.drawCircle(Offset(250, 100), 100, _paint); // 画黄圆

    canvas.drawImage(_dst, Offset(0,550), _paint); // 画底部绿色图

//    _paint.blendMode = BlendMode.clear;
//    _paint.blendMode = BlendMode.src;
//    _paint.blendMode = BlendMode.dst;
//    _paint.blendMode = BlendMode.srcOver;
//    _paint.blendMode = BlendMode.dstOver;
//    _paint.blendMode = BlendMode.srcIn;
//    _paint.blendMode = BlendMode.dstIn;
//    _paint.blendMode = BlendMode.srcOut;
//    _paint.blendMode = BlendMode.dstOut;
//    _paint.blendMode = BlendMode.srcATop;
//    _paint.blendMode = BlendMode.dstATop;
//    _paint.blendMode = BlendMode.xor;
//    _paint.blendMode = BlendMode.plus;
//    _paint.blendMode = BlendMode.modulate;
//    _paint.blendMode = BlendMode.screen;
//    _paint.blendMode = BlendMode.overlay;
//    _paint.blendMode = BlendMode.darken;
//    _paint.blendMode = BlendMode.lighten;
//    _paint.blendMode = BlendMode.colorDodge;
//    _paint.blendMode = BlendMode.colorBurn;
//    _paint.blendMode = BlendMode.hardLight;
//    _paint.blendMode = BlendMode.softLight;
//    _paint.blendMode = BlendMode.difference;
//    _paint.blendMode = BlendMode.exclusion;
//    _paint.blendMode = BlendMode.multiply;
//    _paint.blendMode = BlendMode.hue;
//    _paint.blendMode = BlendMode.saturation;
//    _paint.blendMode = BlendMode.color;
//    _paint.blendMode = BlendMode.luminosity;

    _paint.color = Colors.blueAccent;
    canvas.drawCircle(Offset(250, 250), 100, _paint);  // 画蓝圆
    _paint.color = Colors.green;
    canvas.drawCircle(Offset(150, 250), 100, _paint); // 画绿圆

    canvas.drawImage(_src, Offset(0,400), _paint); // 画儿童图

    canvas.restore();
  }



  // 画布起点移到屏幕中心
  void translateToCenter(Canvas canvas, Size size) {
    canvas.translate(size.width / 2, size.height / 2);
  }

  @override
  bool shouldRepaint(CustomPainter oldDelegate) {
    return true;
  }
}

后面的效果,都是修改设置blendMode部分的注释然后生成的,想要看哪个效果就放开哪个就行。就不再贴代码了。

clear

官方api描述:

但实际效果是: 这一点,折腾了好久,目前仍然疑惑中。

src

官方api描述:

但实际效果是:

疑惑中。。。。

dst
  • 删除src,只绘制dst。
  • 从概念上讲,src被丢弃,而dst保持不变。 实际效果是:
srcOver
  • 将src绘制到dst上方。
  • 这是默认值。它代表最直观的情况,如果src有透明区域则显示dst。 实际效果是:
dstOver
  • 将dst绘制在src之上,如果dst有透明区域则显示src。
  • 这与srcOver相反。 实际效果是:
srcIn
  • 显示dst,并在dst上显示src与dst的重叠的部分。 实际效果是:
dstIn

但实际效果是,src不显示:

srcOut
  • 显示src与dst不重叠部分,与官方描述不一致。 实际效果是:
dstOut
  • 看字面意思效果应与srcOut相反,但实际效果无法理解 实际效果是:
srcATop
  • 将src合成到dst上,dst显示,重叠部分显示。 实际效果是:
dstATop
  • 看字面意思应与srcATop效果相反,但是实际效果不是这样的。 实际效果是:
xor
  • 对src和dst应用按位异或运算符。这就使得它们重叠的地方保持透明。 实际效果是:
plus
  • 对重叠部分的透明度相加,相当于增加了重叠部分的透明度。 实际效果是:
modulate
  • 将src和dst的颜色分量相乘。
  • 乘以白色,1.0,结果不变;乘以黑色,0.0,结果为黑色。
  • 颜色分量都是小于1的数,因此,重叠部分相乘,颜色分量更小,相当于让图片更暗。对于不重合部分,src乘以透明,就变透明了,也就是不显示了。

总之,重合部分变的更暗,不重合部分src不显示了。 实际效果是:

screen
  • 将src和dst的分量的逆相乘,然后求逆结果。
  • 反转组件意味着完全饱和的通道(不透明白色)被视为值0.0,而通常被视为0.0(黑色,透明)的值被视为1.0。
  • 这基本上与调制混合模式相同,但是在乘法之前颜色值反转,结果在渲染之前反转回来。
  • 这只能产生相同或较浅的颜色(乘以黑色,1.0,结果不变;乘以白色,0.0,结果为白色)。类似地,在alpha通道中,它只能产生更不透明的颜色。
  • 这与两台投影仪同时在同一屏幕上显示图像的效果相似。

总之,重合部分变的更浅,不重合部分不变 实际效果是:

overlay
  • 将src和dst的颜色分量相乘,然后调整它们以支持。
  • 具体来说,如果dst颜色分量较小,则将其与src值相乘,而如果src值颜色分量较小,则将src值的倒数与dst值的倒数相乘,然后反转结果。
  • 反转组件意味着完全饱和的通道(不透明白色)被视为值0.0,而通常被视为0.0(黑色,透明)的值被视为1.0。

总之,不重合部分不变,重合部分颜色效果向着dst靠近。 实际效果是:

darken
  • 通过从每个颜色通道中选择最低值来合成源图像和目标图像。

总之,重叠部分哪个颜色深用哪个,重叠部分倾向于颜色深的,不重叠部分不变。 实际效果是:

lighten
  • 从每个颜色通道中选择最高值来合成src和dst。

总之,重叠部分哪个颜色浅用哪个,重叠部分倾向于颜色浅的,不重叠部分不变。 实际效果是:

colorDodge
  • 将dst除以src的倒数。
  • 反转颜色分量意味着完全饱和的通道(不透明白色)被视为值0.0,而通常被视为0.0(黑色,透明)的值被视为1.0。
  • 重叠部分产生效果

总之,效果无法总结,惭愧。 实际效果是:

colorBurn
  • 将dst的倒数除以src,然后求结果的倒数。
  • 反转颜色分量意味着完全饱和的通道(不透明白色)被视为值0.0,而通常被视为0.0(黑色,透明)的值被视为1.0。

总之,重叠部分有效果,src不透明部分变透明。

实际效果是:

hardLight
  • 将src和dst的分量相乘,然后调整它们以有利于src。
  • 具体来说,如果src值较小,则将其与dst值相乘,而如果dst值较小,则将dst值的倒数与src值的倒数相乘,然后反转结果。
  • 反转组件意味着完全饱和的通道(不透明白色)被视为值0.0,而通常被视为0.0(黑色,透明)的值被视为1.0。

总之,重叠部分效果向着src靠近。 实际效果是:

softLight
  • 对于低于0.5的源值,使用colorDodge;对于高于0.5的源值,使用colorBurn。

总之,重叠部分效果倾向于dst。 实际效果是:

difference
  • 从每个通道的较大值中减去较小的值。
  • 合成黑色没有效果;合成白色反转其他图像的颜色。
  • 输出图像的不透明度的计算方法与srcOver相同。
  • 这种影响类似于exclusion,但更为严厉。

重叠部分有效果,颜色总体向更深发展,但具体无法总结,惭愧。 实际效果是:

exclusion
  • 从两个图像的总和中减去两个图像乘积的两倍。
  • 合成黑色没有效果;合成白色反转其他图像的颜色。
  • 输出图像的不透明度的计算方法与srcOver相同。
  • 效果类似于difference,但更柔和。

重叠部分有效果,颜色总体向更浅发展,但具体无法总结,惭愧。 实际效果是:

multiply
  • 将src和目标图像的分量相乘,包括alpha通道。
  • 这只能产生相同或更暗的颜色(乘以白色,1.0,结果不变;乘以黑色,0.0,结果为黑色)。
  • 由于alpha通道也会相乘,因此一个图像中的完全透明像素(不透明度0.0)会导致输出中的完全透明像素。这与dstIn类似,但颜色组合在一起。
  • 对于将颜色相乘但不将alpha通道相乘的变体,请考虑modulate。

总之,重叠部分,更暗更透明 实际效果是:

hue
  • 获取src的色调,以及目标图像的饱和度和亮度。
  • 其效果是用src着色目标图像。
  • 在src中完全透明的区域从目标图像获取其色调。 实际效果是:
saturation
  • 获取src的饱和度,以及dst的色调和亮度。
  • 输出图像的不透明度的计算方法与srcOver相同。在src中完全透明的区域从dst获取其饱和度。 实际效果是:
color
  • 获取src的色调和饱和度,以及dst的亮度。
  • 其效果是用src着色dst。
  • 输出图像的不透明度的计算方法与srcOver相同。在src中完全透明的区域从dst获取其色调和饱和度。 实际效果是:
luminosity
  • 获取src的亮度,以及dst的色调和饱和度。
  • 输出图像的不透明度的计算方法与srcOver相同。在src中完全透明的区域从dst获取其亮度。 实际效果是:

以上仅为个人试验结果,如有错误,期待指正!

over-