史上最全canvas图层混合算法

在使用canvas做多图合成的时候,我们会需要用到各种图层混合模式,canvas原生支持部分全局图层混合方式globalCompositeOperation,主要有以下几种:

这里以红色矩形作为目标图像,蓝色矩形作为源图像。

1、source-over

默认,在目标图像上显示源图像。

2、source-atop

在目标图像顶部显示源图像,源图像位于目标图像之外的部分是不可见的。

3、source-in

在目标图像中显示源图像,只有目标图像之内的源图像部分会显示,目标图像是透明的。

4、source-out

在目标图像之外显示源图像,只有目标图像之外的源图像部分会显示,目标图像是透明的。

5、destination-over

在源图像上显示目标图像

6、destination-atop

在源图像顶部显示目标图像,目标图像位于源图像之外的部分是不可见的。

7、destination-in

在源图像中显示目标图像,只有源图像之内的目标图像部分会被显示,源图像是透明的。

8、destination-out

在源图像之外显示目标图像,只有源图像之外的目标图像部分会被显示,源图像是透明的。

9、lighter

显示源图像 + 目标图像

10、copy

显示源图像,忽略目标图像。

11、xor

使用异或操作对源图像与目标图像进行组合。

虽然canvas原生支持了这么多的混合方式,但相比Photoshop依然很少,在实际使用中还是会不够用,比如我们要用柔光、变暗等方式做图层融合,canvas原生的混合模式就没办法做到了,这里我们介绍一些其他混合模式的实现算法。

1、正常模式

结果色 = 源色 * 源色不透明度 +目标色 * (100%不透明 - 源色不透明度)

const normal = (target, source) => {
    const [targetR, targetG, targetB, targetA] = target;
    const [sourceR, sourceG, sourceB, sourceA] = source;

    const alpha = sourceA / 255;

    const r = sourceR * alpha + targetR * (1 - alpha);
    const g = sourceG * alpha + targetG * (1 - alpha);
    const b = sourceB * alpha + targetB * (1 - alpha);
    const a = sourceA * alpha + targetA * (1 - alpha);

    return [r, g, b, a];
}

2、溶解

结果色和源色相同,只是根据每个像素点所在的位置的透明度的不同,可随机以源色和目标色取代。透明度越大,溶解效果就越明显。

const dissolve = (target, source) => {
    const sourceA = source[3];
    if (sourceA === 0) {
        return target;
    }
    const threshold = Math.floor(Math.random() * 100);
    if (sourceA < 255 && threshold > 50) {
        return target;
    }
    return source;
}

3、变暗

结果色 = Min(源色, 目标色)

const darken = (target, source) => {
    const [targetR, targetG, targetB, targetA] = target;
    const [sourceR, sourceG, sourceB, sourceA] = source;

    if (sourceA === 0) {
        return target;
    }

    const r = Math.min(targetR, sourceR);
    const g = Math.min(targetG, sourceG);
    const b = Math.min(targetB, sourceB);
    const a = Math.min(targetA, sourceA);

    return [r, g, b, a];
}

4、正片叠底

结果色 = 源色 * 目标色 / 255

const multiply = (target, source) => {
    const [targetR, targetG, targetB, targetA] = target;
    const [sourceR, sourceG, sourceB, sourceA] = source;
    if (sourceA === 0) {
        return target;
    }
    return [
        (sourceR * targetR) / 255,
        (sourceG * targetG) / 255,
        (sourceB * targetB) / 255,
        (sourceA * targetA) / 255,
    ];
}

5、颜色加深

结果色 = (目标色 + 源色 - 255) * 255 /  源色值

const colorBurn = (target, source) => {
    const [targetR, targetG, targetB, targetA] = target;
    const [sourceR, sourceG, sourceB, sourceA] = source;

    if (sourceA === 0) {
        return target;
    }

    const r = (Math.max(0, targetR + sourceR - 255) * 255) / sourceR;
    const g = (Math.max(0, targetG + sourceG - 255) * 255) / sourceG;
    const b = (Math.max(0, targetB + sourceB - 255) * 255) / sourceB;
    const a = (Math.max(0, targetA + sourceA - 255) * 255) / sourceA;

    return [r, g, b, a];
}

6、线性加深

结果色 = 目标色 + 源色 - 255

const linearBurn = (target, source) => {
    const [targetR, targetG, targetB, targetA] = target;
    const [sourceR, sourceG, sourceB, sourceA] = source;
    if (sourceA === 0) {
        return target;
    }
    return [
        Math.max(0, targetR + sourceR - 255),
        Math.max(0, targetG + sourceG - 255),
        Math.max(0, targetB + sourceB - 255),
        Math.max(0, targetA + sourceA - 255)
    ];
}

7、深色

计算源色与目标色的所有通道的数值,然后选择数值较小的作为结果色

const darker = (target, source) => {
    const [targetR, targetG, targetB, targetA] = target;
    const [sourceR, sourceG, sourceB, sourceA] = source;

    if (sourceA === 0) {
        return target;
    }

    const totalTarget = targetR + targetG + targetB + targetA;
    const totalSource = sourceR + sourceG + sourceB + sourceA;

    if (totalTarget < totalSource) {
      return target;
    }

    return source;
}

8、变亮

结果色 = Max(源色, 目标色)

const lighten = (target, source) => {
    const [targetR, targetG, targetB, targetA] = target;
    const [sourceR, sourceG, sourceB, sourceA] = source;

    if (sourceA === 0) {
        return target;
    }
    const r = Math.max(targetR, sourceR);
    const g = Math.max(targetG, sourceG);
    const b = Math.max(targetB, sourceB);
    const a = Math.max(targetA, sourceA);

    return [r, g, b, a];
}

9、滤色

结果色 = 255 - (255 - 源色) * (255 - 目标色) / 255

const screen = (target, source) => {
    const [targetR, targetG, targetB, targetA] = target;
    const [sourceR, sourceG, sourceB, sourceA] = source;

    const r = 255 - ((255 - sourceR) * (255 - targetR)) / 255;
    const g = 255 - ((255 - sourceG) * (255 - targetG)) / 255;
    const b = 255 - ((255 - sourceB) * (255 - targetB)) / 255;
    const a = 255 - ((255 - sourceA) * (255 - targetA)) / 255;

    return [r, g, b, a];
}

10、颜色减淡

结果色 = 目标色+ (源色 * 目标色) / (255 - 源色)

const colorDodge = (target, source) => {
    const [targetR, targetG, targetB, targetA] = target;
    const [sourceR, sourceG, sourceB, sourceA] = source;

    const r = targetR + (sourceR * targetR) / (255 - sourceR);
    const g = targetG + (sourceG * targetG) / (255 - sourceG);
    const b = targetB + (sourceB * targetB) / (255 - sourceB);
    const a = targetA + (sourceA * targetA) / (255 - sourceA);

    return [r, g, b, a];
}

11、线性减淡

结果色 = 目标色 + 源色

注意:计算出的结果色值不能大于255

const linearDodge = (target, source) => {
    const [targetR, targetG, targetB, targetA] = target;
    const [sourceR, sourceG, sourceB, sourceA] = source;

    const r = Math.min(targetR + sourceR, 255);
    const g = Math.min(targetG + sourceG, 255);
    const b = Math.min(targetB + sourceB, 255);
    const a = Math.min(targetA + sourceA, 255);

    return [r, g, b, a];
}

12、浅色

计算源色与目标色所有通道的数值总和,哪个数值大就选为结果色

const lighter = (target, source) => {
    const [targetR, targetG, targetB, targetA] = target;
    const [sourceR, sourceG, sourceB, sourceA] = source;

    if (sourceA === 0) {
        return target;
    }

    const totalTarget = targetR + targetG + targetB + targetA;
    const totalSource = sourceR + sourceG + sourceB + sourceA;

    if (totalTarget > totalSource) {
      return target;
    }

    return source;
}

13、叠加

目标色 <= 128, 目标色 = 源色 * 目标色 / 128; 

目标色 > 128, 目标色 =255 - (255 - 源色) * (255 - 目标色) / 128;

const overlay = (target, source) => {
    const [targetR, targetG, targetB, targetA] = target;
    const [sourceR, sourceG, sourceB, sourceA] = source;

    if (sourceA === 0) {
        return target;
    }

    let r = sourceR * targetR / 128;
    if (targetR > 128) {
        r = 255 - ((255 - sourceR) * (255 - targetR)) / 128;
    }

    let g = sourceG * targetG / 128;
    if (targetG > 128) {
        g = 255 - ((255 - sourceG) * (255 - targetG)) / 128
    }

    let b = sourceB * targetB / 128;
    if (targetB > 128) {
        b = 255 - ((255 - sourceB) * (255 - targetB)) / 128;
    }

    let a = sourceA * targetA / 128;
    if (targetA > 128) {
        a = 255 - ((255 - sourceA) * (255 - targetA)) / 128;
    } 

    return [r, g, b, a];
}

14、柔光

源色 <= 128, 结果色 = 目标色 + (2 * 源色 - 255) * (目标色 - pow(目标色, 2) / 255) / 255;  

源色 > 128, 结果色 = 目标色 + (2 * 源色 - 255) * (sqrt(目标色/255) * 255 - 目标色) / 255;

const softLight = (target, source) => {
    const [targetR, targetG, targetB, targetA] = target;
    const [sourceR, sourceG, sourceB, sourceA] = source;

    if (sourceA === 0) {
        return target;
    }

    let r = targetR + (2 * sourceR - 255) * (targetR - Math.pow(targetR, 2) / 255) / 255;
    if (sourceR > 128) {
        r =
      targetR +
      ((2 * sourceR - 255) * (Math.sqrt(targetR / 255) * 255 - targetR)) /
        255;
    }

    let g = targetG + (2 * sourceG - 255) * (targetG - Math.pow(targetG, 2) / 255) / 255;
    if (sourceG > 128) {
        g =
      targetG +
      ((2 * sourceG - 255) * (Math.sqrt(targetG / 255) * 255 - targetG)) /
        255;
    }

    let b = targetB + (2 * sourceB - 255) * (targetB - Math.pow(targetB, 2) / 255) / 255;
    if (sourceB > 128) {
        b =
      targetB +
      ((2 * sourceB - 255) * (Math.sqrt(targetB / 255) * 255 - targetB)) /
        255;
    }

    let a = targetA + (2 * sourceA - 255) * (targetA - Math.pow(targetA, 2) / 255) / 255;
    if (sourceA > 128) {
        a =
      targetA +
      ((2 * sourceA - 255) * (Math.sqrt(targetA / 255) * 255 - targetA)) /
        255;
    }

    return [r, g, b, a];
}

15、强光

源色 <= 128, 结果色 = 目标色 * 源色 / 128;   

源色 > 128, 结果色 = 255 - (255 - 源色) * (255 - 目标色) / 128;

const hardLight = (target, source) => {
    const [targetR, targetG, targetB, targetA] = target;
    const [sourceR, sourceG, sourceB, sourceA] = source;
    
    if (sourceA === 0) {
         return target;
     }
     
    const r =
      sourceR > 128
        ? 255 - ((255 - sourceR) * (255 - targetR)) / 128
        : (sourceR * targetR) / 128;
    const g =
      sourceG > 128
        ? 255 - ((255 - sourceG) * (255 - targetG)) / 128
        : (sourceG * targetG) / 128;
    const b =
      sourceB > 128
        ? 255 - ((255 - sourceB) * (255 - targetB)) / 128
        : (sourceB * targetB) / 128;
    const a =
      sourceA > 128
        ? 255 - ((255 - sourceA) * (255 - targetA)) / 128
        : (sourceA * targetA) / 128;

    return [r, g, b, a];
}

16、亮光

源色 <= 128, 结果色 = 目标色 - (255 - 目标色) * (255 - 2 * 源色) / (2 * 源色);    

源色 > 128, 结果色 = 目标色 + 目标色 * (2 * 源色 - 255) / (2 * (255 - 源色));

const vividLight = (target, source) => {
    const [targetR, targetG, targetB, targetA] = target;
    const [sourceR, sourceG, sourceB, sourceA] = source;

    if (sourceA === 0) {
        return target;
    }

    let r = targetR - (255 - targetR) * (255 - 2 * sourceR) / (2 * sourceR);
    if (sourceR > 128) {
        r = targetR + (targetR * (2 * sourceR - 255) / (2 * (255 - sourceR)))
    }

    let g = targetG - (255 - targetG) * (255 - 2 * sourceG) / (2 * sourceG);
    if (sourceG > 128) {
        g = targetG + (targetG * (2 * sourceG - 255) / (2 * (255 - sourceG)))
    }

    let b = targetB - (255 - targetB) * (255 - 2 * sourceB) / (2 * sourceB);
    if (sourceB > 128) {
        b = targetB + (targetB * (2 * sourceB - 255) / (2 * (255 - sourceB)))
    }

    let a = targetA - (255 - targetA) * (255 - 2 * sourceA) / (2 * sourceA);
    if (sourceA > 128) {
        a = targetA + (targetA * (2 * sourceA - 255) / (2 * (255 - sourceA)))
    }

    return [r, g, b, a];
}

17、线性光

结果色 = 目标色 + 2 * 源色 - 255

const linearLight = (target, source) => {
    const [targetR, targetG, targetB, targetA] = target;
    const [sourceR, sourceG, sourceB, sourceA] = source;
    
    if (sourceA === 0) {
        return target;
    }

    const r = targetR + 2 * sourceR - 255;
    const g = targetG + 2 * sourceG - 255;
    const b = targetB + 2 * sourceB - 255;
    const a = targetA + 2 * sourceA - 255;

    return [r, g, b, a];
}

18、点光

源色 <= 128, 结果色 = Min(目标色, 2 * 源色);     

源色 > 128, 结果色 = Max(目标色, 2 * 源色 - 255);

const pinLight = (target, source) => {
    const [targetR, targetG, targetB, targetA] = target;
    const [sourceR, sourceG, sourceB, sourceA] = source;

    if (sourceA === 0) {
        return target;
    }

    let r = Math.min(targetR, 2 * sourceR);
    if (sourceR > 128) {
        r = Math.max(targetR, 2 * sourceR - 255);
    }
    
    let g = Math.min(targetG, 2 * sourceG);
    if (sourceG > 128) {
        g = Math.max(targetG, 2 * sourceG - 255);
    }

    let b = Math.min(targetB, 2 * sourceB);
    if (sourceB > 128) {
        b = Math.max(targetB, 2 * sourceB - 255);
    }

    let a = Math.min(targetA, 2 * sourceA);
    if (sourceA > 128) {
        a = Math.max(targetA, 2 * sourceA - 255);
    }

    return [r, g, b, a];
}

19、实色混合

当目标色+源色和>=255时,则结果色=255,否则为0。

const hardMix = (target, source) => {
    const [targetR, targetG, targetB, targetA] = target;
    const [sourceR, sourceG, sourceB, sourceA] = source;

    if (sourceA === 0) {
        return target;
    }

    const r = targetR + sourceR > 255 ? 255 : 0;
    const g = targetG + sourceG > 255 ? 255 : 0;
    const b = targetB + sourceB > 255 ? 255 : 0;
    const a = targetA + sourceA > 255 ? 255 : 0;

    return [r, g, b, a];
}

20、差值

结果色 = | 目标色 - 源色 |

注意:透明度不能按照公式处理

const difference = (target, source) => {
    const [targetR, targetG, targetB, targetA] = target;
    const [sourceR, sourceG, sourceB, sourceA] = source;

    if (sourceA === 0) {
        return target;
    }

    const r = Math.abs(targetR - sourceR);
    const g = Math.abs(targetG - sourceG);
    const b = Math.abs(targetB - sourceB);
    const a = sourceA;

    return [r, g, b, a];
}

21、排除

结果色 = 目标色 + 源色 - (目标色 * 源色) / 128

注意:透明度不能按照公式处理

const exclusion = (target, source) => {
    const [targetR, targetG, targetB, targetA] = target;
    const [sourceR, sourceG, sourceB, sourceA] = source;

    if (sourceA === 0) {
        return target;
    }
    const r = targetR + sourceR  - (targetR * sourceR) / 128;
    const g = targetG + sourceG  - (targetG * sourceG) / 128;
    const b = targetB + sourceB  - (targetB * sourceB) / 128;
    const a = sourceA;

    return [r, g, b, a];
}

22、减去

结果色 = Max(0, 目标色 - 源色)

注意:透明度不能按照公式处理

const subtract = (target, source) => {
    const [targetR, targetG, targetB, targetA] = target;
    const [sourceR, sourceG, sourceB, sourceA] = source;

    if (sourceA === 0) {
        return target;
    }

    const r = Math.max(0, targetR - sourceR);
    const g = Math.max(0, targetG - sourceG);
    const b = Math.max(0, targetB - sourceB);
    const a = sourceA;

    return [r, g, b, a];
}

23、划分

结果色 = 目标色 / 源色 * 255

const divide = (target, source) => {
    const [targetR, targetG, targetB, targetA] = target;
    const [sourceR, sourceG, sourceB, sourceA] = source;

    if (sourceA === 0) {
        return target;
    }

    const r = targetR / sourceR * 255;
    const g = targetG / sourceG * 255;
    const b = targetB / sourceB * 255;
    const a = targetA / sourceA * 255

    return [r, g, b, a];
}

注意:以上混合模式只适用于RGB模式,以上所有的混合模式的效果均和Photoshop做过一致性对比。

在线demo:https://demo.deanhan.cn/compose/

如需下载本文所涉及源码及素材包,请在关注本站公众号后发送:1675606283276

  • 支付宝二维码 支付宝
  • 微信二维码 微信

本文地址: /canvas-blende-mode.html

版权声明: 本文为原创文章,版权归 逐梦个人博客 所有,欢迎分享本文,转载请保留出处!

相关文章