Optimise blend mode application using GPU powered composite operations when available

This commit is contained in:
Igor Zinken
2026-08-02 10:34:13 +02:00
parent 53ec6ca471
commit 03013a6c66
4 changed files with 216 additions and 111 deletions

View File

@@ -14,7 +14,7 @@
"lighten": "Lighten",
"screen": "Screen",
"colorDodge": "Color dodge",
"linearDodgeAdd": "Linear Dodge / Add",
"linearDodgeAdd": "Linear dodge / Add",
"lighterColor": "Lighter color",
"overlay": "Overlay",
"softLight": "Soft light",

View File

@@ -36,7 +36,7 @@ import BrushFactory from "@/model/factories/brush-factory";
import { scaleRectangle, rotateRectangle } from "@/math/rectangle-math";
import { translatePointerRotation, rotatePointer } from "@/math/point-math";
import { fastRound } from "@/math/unit-math";
import { getBlendContext, blendLayer } from "@/rendering/operations/blending";
import { applyBlend, prepareBlend, isNativeBlendMode } from "@/rendering/operations/blending";
import { clipContextToSelection, clipLayer } from "@/rendering/operations/clipping";
import { renderClonedStroke, setCloneSource } from "@/rendering/operations/cloning";
import { renderBrushStroke } from "@/rendering/operations/drawing";
@@ -667,7 +667,10 @@ export default class LayerRenderer extends ZoomableSprite {
override draw( documentContext: CanvasRenderingContext2D, viewport: Viewport, isSnapshotMode = false ): void {
let renderedFromCache = false;
if ( !isSnapshotMode && useBlendCaching() && !this._pendingEffectsRender ) {
if (
!isSnapshotMode && !this._pendingEffectsRender
&& useBlendCaching() && !isNativeBlendMode( this.layer.filters.blendMode )
) {
const { layerIndex } = this;
if ( isBlendCached( layerIndex )) {
return; // render will be executed by higher order layer
@@ -704,10 +707,9 @@ export default class LayerRenderer extends ZoomableSprite {
const applyBlending = hasBlend( this.layer ) && !isDrawingOnMask;
if ( applyBlending ) {
drawContext = getBlendContext( documentContext.canvas );
drawContext.globalAlpha = altOpacity ? opacity : 1;
const scaleFactor = isSnapshotMode ? getPixelRatio() : getPixelRatio() * this.canvas.zoomFactor;
drawContext.scale( scaleFactor, scaleFactor );
drawContext = prepareBlend(
documentContext, blendMode, altOpacity ? opacity : 1, isSnapshotMode, this.canvas.zoomFactor
);
}
let maskComposite: CanvasContextPairing | undefined;
@@ -755,7 +757,7 @@ export default class LayerRenderer extends ZoomableSprite {
}
if ( applyBlending ) {
blendLayer( documentContext, drawContext, blendMode );
applyBlend( drawContext, documentContext, blendMode );
}
drawContext.restore(); // transformation restore()

View File

@@ -1,7 +1,7 @@
/**
* The MIT License (MIT)
*
* Igor Zinken 2025 - https://www.igorski.nl
* Igor Zinken 2025-2026 - https://www.igorski.nl
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in

View File

@@ -1,7 +1,7 @@
/**
* The MIT License (MIT)
*
* Igor Zinken 2023-2025 - https://www.igorski.nl, adapted from source of:
* Igor Zinken 2023-2026 - https://www.igorski.nl, math fallback adapted from source of:
* Context Blender JavaScript Library
* Copyright © 2010 Gavin Kistner
*
@@ -22,47 +22,83 @@
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import type { Rectangle } from "zcanvas";
import { BlendModes } from "@/definitions/blend-modes";
import type { RGB, HSV } from "@/definitions/colors";
import { createCanvas } from "@/utils/canvas-util";
import { rgb2YCbCr, YCbCr2rgb, rgb2hsv, hsv2rgb } from "@/utils/color-util";
import type { RGB /*, HSV */ } from "@/definitions/colors";
import { createCanvas, getPixelRatio } from "@/utils/canvas-util";
// import { rgb2YCbCr, YCbCr2rgb, rgb2hsv, hsv2rgb } from "@/utils/color-util";
const { cvs, ctx } = createCanvas();
/**
* Retrieve a pooled empty context to use for drawing layer content on prior to blending
* onto underlying Document context
*
* @param {HTMLCanvasElement} documentCanvas to match dimensions of
* All blend modes can be performed using globalCompositeOperation omitting
* the need for an expensive call to getImageData() while providing the benefit
* that it is GPU powered. There are some modes that aren't supported out of the box though...
*/
export const getBlendContext = ( documentCanvas: HTMLCanvasElement ): CanvasRenderingContext2D => {
const { width, height } = documentCanvas;
cvs.width = width;
cvs.height = height;
return ctx;
export const isNativeBlendMode = ( blendMode: BlendModes ): boolean => {
return blendMode !== BlendModes.LIGHTER_COLOR
&& blendMode !== BlendModes.DARKER_COLOR;
};
/**
* Blend a Layer onto the underlying Document content
* Return the appropriate context for drawing layer content on using the provided blendMode.
* This will just apply the appropriate composite operation on the existing drawContext for
* all native blend modes. For the non-native blend modes, a pooled empty context is
* returned on which ImageData manipulations can take place.
*
* @param {CanvasRenderingContext2D} dest the context that contains all rendered content below the Layer
* @param {CanvasRenderingContext2D} layer the context that contains the content of the Layer (see getBlendContext)
* @param {BlendModes} blendMode the blend mode to use
* @param {Rectangle?} bounds optional bounds to constrain blending within. defaults to full dest size
* @param {CanvasRenderingContext2D} documentContext references canvas to match dimensions of
* @param {BlendModes} blendMode which will be used during the blendLayer() call
* @param {number=} opacity optional opacity to apply
* @param {boolean=} isSnapshotMode whether blending will be applied within high res snapshot mode
* @param {number=} zoomFactor current zoom level of the document canvas
*/
export const blendLayer = ( dest: CanvasRenderingContext2D, layer: CanvasRenderingContext2D,
blendMode: BlendModes, bounds?: Rectangle ): void => {
export const prepareBlend = (
documentContext: CanvasRenderingContext2D, blendMode: BlendModes, opacity = 1, isSnapshotMode = false, zoomFactor = 1
): CanvasRenderingContext2D => {
let context = documentContext;
if ( isNativeBlendMode( blendMode )) {
context.save(); // restored on applyBlend()
applyNativeCompositeOperation( context, blendMode );
} else {
context = ctx; // use pooled canvas
const { width, height } = documentContext.canvas;
cvs.width = width;
cvs.height = height;
const scaleFactor = isSnapshotMode ? getPixelRatio() : getPixelRatio() * zoomFactor;
ctx.scale( scaleFactor, scaleFactor );
}
context.globalAlpha = opacity;
return context;
};
/**
* Blend the contents of a Layer onto the underlying Document content
*
* @param {CanvasRenderingContext2D} source the context that contains the content of the Layer (see prepareBlend())
* @param {CanvasRenderingContext2D} dest the context that contains all rendered content below the Layer
* @param {BlendModes} blendMode the blend mode to use
*/
export const applyBlend = (
source: CanvasRenderingContext2D, dest: CanvasRenderingContext2D, blendMode: BlendModes,
): void => {
if ( isNativeBlendMode( blendMode )) {
// native blend will have been applied directly during the regular bitmap render routine using
// the appropriate composite operation applied in prepareBlend(), just restore the original context
source.restore();
return;
}
let left = 0;
let top = 0;
let { width, height } = dest.canvas;
if ( bounds ) {
({ left, top, width, height } = bounds );
}
const srcImageData: ImageData = layer.getImageData( left, top, width, height );
// math based blending, this comes with a higher performance cost
const srcImageData: ImageData = source.getImageData( left, top, width, height );
const dstImageData: ImageData = dest.getImageData( left, top, width, height );
const src: Uint8ClampedArray = srcImageData.data;
@@ -70,10 +106,10 @@ export const blendLayer = ( dest: CanvasRenderingContext2D, layer: CanvasRenderi
const size = dst.length;
let rgb: RGB;
let hsl: RGB;
let hsl2: RGB;
let hsv: HSV;
let hsv2: HSV;
// let hsl: RGB;
// let hsl2: RGB;
// let hsv: HSV;
// let hsv2: HSV;
for ( let px = 0; px < size; px += 4 ) {
// alpha channel
@@ -84,143 +120,150 @@ export const blendLayer = ( dest: CanvasRenderingContext2D, layer: CanvasRenderi
dst[ px + 3 ] = dA2 * 255;
// grab RGB values
const r1 = dst[ px ], g1 = dst[ px + 1 ], b1 = dst[ px + 2 ];
const r2 = src[ px ], g2 = src[ px + 1 ], b2 = src[ px + 2 ];
const r1 = dst[ px ];
const g1 = dst[ px + 1 ];
const b1 = dst[ px + 2 ];
const r2 = src[ px ];
const g2 = src[ px + 1 ];
const b2 = src[ px + 2 ];
const sRA = r2 / 255 * sA;
const dRA = r1 / 255 * dA;
const sGA = g2 / 255 * sA;
const dGA = g1 / 255 * dA;
const sBA = b2 / 255 * sA;
const dBA = b1 / 255 * dA;
// const sRA = r2 / 255 * sA;
// const dRA = r1 / 255 * dA;
// const sGA = g2 / 255 * sA;
// const dGA = g1 / 255 * dA;
// const sBA = b2 / 255 * sA;
// const dBA = b1 / 255 * dA;
const demultiply = 255 / dA2;
// const demultiply = 255 / dA2;
const f1 = dA * sA, f2 = dA - f1, f3 = sA - f1;
switch( blendMode ) {
default:
throw new Error( `native composite operation should be used instead for blendMode ${blendMode}` );
break;
/*
case BlendModes.SCREEN:
dst[ px ] = ( sRA + dRA - sRA * dRA ) * demultiply;
dst[ px + 1 ] = ( sGA + dGA - sGA * dGA ) * demultiply;
dst[ px + 2 ] = ( sBA + dBA - sBA * dBA ) * demultiply;
dst[ px ] = ( sRA + dRA - sRA * dRA ) * demultiply;
dst[ px + 1 ] = ( sGA + dGA - sGA * dGA ) * demultiply;
dst[ px + 2 ] = ( sBA + dBA - sBA * dBA ) * demultiply;
break;
case BlendModes.MULTIPLY:
dst[ px ] = ( sRA * dRA + sRA * ( 1 - dA ) + dRA * ( 1 - sA )) * demultiply;
dst[ px + 1 ] = ( sGA * dGA + sGA * ( 1 - dA ) + dGA * ( 1 - sA )) * demultiply;
dst[ px + 2 ] = ( sBA * dBA + sBA * ( 1 - dA ) + dBA * ( 1 - sA )) * demultiply;
dst[ px ] = ( sRA * dRA + sRA * ( 1 - dA ) + dRA * ( 1 - sA )) * demultiply;
dst[ px + 1 ] = ( sGA * dGA + sGA * ( 1 - dA ) + dGA * ( 1 - sA )) * demultiply;
dst[ px + 2 ] = ( sBA * dBA + sBA * ( 1 - dA ) + dBA * ( 1 - sA )) * demultiply;
break;
case BlendModes.DIFFERENCE:
dst[ px ] = ( sRA + dRA - 2 * Math.min( sRA * dA, dRA * sA )) * demultiply;
dst[ px + 1 ] = ( sGA + dGA - 2 * Math.min( sGA * dA, dGA * sA )) * demultiply;
dst[ px + 2 ] = ( sBA + dBA - 2 * Math.min( sBA * dA, dBA * sA )) * demultiply;
dst[ px ] = ( sRA + dRA - 2 * Math.min( sRA * dA, dRA * sA )) * demultiply;
dst[ px + 1 ] = ( sGA + dGA - 2 * Math.min( sGA * dA, dGA * sA )) * demultiply;
dst[ px + 2 ] = ( sBA + dBA - 2 * Math.min( sBA * dA, dBA * sA )) * demultiply;
break;
case BlendModes.LINEAR_DODGE:
dst[ px ] = Math.min( sRA + dRA, 1 ) * demultiply;
dst[ px + 1 ] = Math.min( sGA + dGA, 1 ) * demultiply;
dst[ px + 2 ] = Math.min( sBA + dBA, 1 ) * demultiply;
dst[ px ] = Math.min( sRA + dRA, 1 ) * demultiply;
dst[ px + 1 ] = Math.min( sGA + dGA, 1 ) * demultiply;
dst[ px + 2 ] = Math.min( sBA + dBA, 1 ) * demultiply;
break;
case BlendModes.OVERLAY:
dst[ px] = f1 * blendOverlay( r1, r2 ) + f2 * r1 + f3 * r2;
dst[ px + 1 ] = f1 * blendOverlay( g1, g2 ) + f2 * g1 + f3 * g2;
dst[ px + 2 ] = f1 * blendOverlay( b1, b2 ) + f2 * b1 + f3 * b2;
dst[ px] = f1 * blendOverlay( r1, r2 ) + f2 * r1 + f3 * r2;
dst[ px + 1 ] = f1 * blendOverlay( g1, g2 ) + f2 * g1 + f3 * g2;
dst[ px + 2 ] = f1 * blendOverlay( b1, b2 ) + f2 * b1 + f3 * b2;
break;
case BlendModes.HARD_LIGHT:
dst[ px] = f1 * blendOverlay( r2, r1 ) + f2 * r1 + f3 * r2;
dst[ px + 1 ] = f1 * blendOverlay( g2, g1 ) + f2 * g1 + f3 * g2;
dst[ px + 2 ] = f1 * blendOverlay( b2, b1 ) + f2 * b1 + f3 * b2;
dst[ px] = f1 * blendOverlay( r2, r1 ) + f2 * r1 + f3 * r2;
dst[ px + 1 ] = f1 * blendOverlay( g2, g1 ) + f2 * g1 + f3 * g2;
dst[ px + 2 ] = f1 * blendOverlay( b2, b1 ) + f2 * b1 + f3 * b2;
break;
case BlendModes.COLOR_DODGE:
dst[ px] = f1 * blendDodge( r1, r2 ) + f2 * r1 + f3 * r2;
dst[ px + 1 ] = f1 * blendDodge( g1, g2 ) + f2 * g1 + f3 * g2;
dst[ px + 2 ] = f1 * blendDodge( b1, b2 ) + f2 * b1 + f3 * b2;
dst[ px] = f1 * blendDodge( r1, r2 ) + f2 * r1 + f3 * r2;
dst[ px + 1 ] = f1 * blendDodge( g1, g2 ) + f2 * g1 + f3 * g2;
dst[ px + 2 ] = f1 * blendDodge( b1, b2 ) + f2 * b1 + f3 * b2;
break;
case BlendModes.COLOR_BURN:
dst[ px ] = f1 * blendBurn( r1, r2 ) + f2 * r1 + f3 * r2;
dst[ px + 1 ] = f1 * blendBurn( g1, g2 ) + f2 * g1 + f3 * g2;
dst[ px + 2 ] = f1 * blendBurn( b1, b2 ) + f2 * b1 + f3 * b2;
dst[ px ] = f1 * blendBurn( r1, r2 ) + f2 * r1 + f3 * r2;
dst[ px + 1 ] = f1 * blendBurn( g1, g2 ) + f2 * g1 + f3 * g2;
dst[ px + 2 ] = f1 * blendBurn( b1, b2 ) + f2 * b1 + f3 * b2;
break;
case BlendModes.DARKEN:
dst[ px ] = f1 * ( r1 < r2 ? r1 : r2 ) + f2 * r1 + f3 * r2;
dst[ px + 1 ] = f1 * ( g1 < g2 ? g1 : g2 ) + f2 * g1 + f3 * g2;
dst[ px + 2 ] = f1 * ( b1 < b2 ? b1 : b2 ) + f2 * b1 + f3 * b2;
dst[ px ] = f1 * ( r1 < r2 ? r1 : r2 ) + f2 * r1 + f3 * r2;
dst[ px + 1 ] = f1 * ( g1 < g2 ? g1 : g2 ) + f2 * g1 + f3 * g2;
dst[ px + 2 ] = f1 * ( b1 < b2 ? b1 : b2 ) + f2 * b1 + f3 * b2;
break;
case BlendModes.LIGHTEN:
dst[ px ] = ( sRA < dRA ? dRA : sRA ) * demultiply;
dst[ px + 1 ] = ( sGA < dGA ? dGA : sGA ) * demultiply;
dst[ px + 2 ] = ( sBA < dBA ? dBA : sBA ) * demultiply;
dst[ px ] = ( sRA < dRA ? dRA : sRA ) * demultiply;
dst[ px + 1 ] = ( sGA < dGA ? dGA : sGA ) * demultiply;
dst[ px + 2 ] = ( sBA < dBA ? dBA : sBA ) * demultiply;
break;
case BlendModes.EXCLUSION:
dst[ px ] = ( dRA + sRA - 2 * dRA * sRA ) * demultiply;
dst[ px + 1 ] = ( dGA + sGA - 2 * dGA * sGA ) * demultiply;
dst[ px + 2 ] = ( dBA + sBA - 2 * dBA * sBA ) * demultiply;
dst[ px ] = ( dRA + sRA - 2 * dRA * sRA ) * demultiply;
dst[ px + 1 ] = ( dGA + sGA - 2 * dGA * sGA ) * demultiply;
dst[ px + 2 ] = ( dBA + sBA - 2 * dBA * sBA ) * demultiply;
break;
case BlendModes.SOFT_LIGHT:
dst[ px ] = f1 * blendSoftlight( r1, r2 ) + f2 * r1 + f3 * r2;
dst[ px + 1 ] = f1 * blendSoftlight( g1, g2 ) + f2 * g1 + f3 * g2;
dst[ px + 2 ] = f1 * blendSoftlight( b1, b2 ) + f2 * b1 + f3 * b2;
dst[ px ] = f1 * blendSoftlight( r1, r2 ) + f2 * r1 + f3 * r2;
dst[ px + 1 ] = f1 * blendSoftlight( g1, g2 ) + f2 * g1 + f3 * g2;
dst[ px + 2 ] = f1 * blendSoftlight( b1, b2 ) + f2 * b1 + f3 * b2;
break;
case BlendModes.LUMINOSITY:
hsl = rgb2YCbCr( r1, g1, b1 );
hsl2 = rgb2YCbCr( r2, g2, b2 );
hsl = rgb2YCbCr( r1, g1, b1 );
hsl2 = rgb2YCbCr( r2, g2, b2 );
rgb = YCbCr2rgb( hsl2.r, hsl.g, hsl.b );
dst[ px ] = f1 * rgb.r + f2 * r1 + f3 * r2;
dst[ px + 1 ] = f1 * rgb.g + f2 * g1 + f3 * g2;
dst[ px + 2 ] = f1 * rgb.b + f2 * b1 + f3 * b2;
dst[ px + 1 ] = f1 * rgb.g + f2 * g1 + f3 * g2;
dst[ px + 2 ] = f1 * rgb.b + f2 * b1 + f3 * b2;
break;
case BlendModes.COLOR:
hsl = rgb2YCbCr( r1, g1, b1 );
hsl2 = rgb2YCbCr( r2, g2, b2 );
hsl = rgb2YCbCr( r1, g1, b1 );
hsl2 = rgb2YCbCr( r2, g2, b2 );
rgb = YCbCr2rgb( hsl.r, hsl2.g, hsl2.b );
dst[ px ] = f1 * rgb.r + f2 * r1 + f3 * r2;
dst[ px + 1 ] = f1 * rgb.g + f2 * g1 + f3 * g2;
dst[ px + 2 ] = f1 * rgb.b + f2 * b1 + f3 * b2;
dst[ px + 1 ] = f1 * rgb.g + f2 * g1 + f3 * g2;
dst[ px + 2 ] = f1 * rgb.b + f2 * b1 + f3 * b2;
break;
case BlendModes.HUE:
hsv = rgb2hsv( r1, g1, b1 );
hsv2 = rgb2hsv( r2, g2, b2 );
hsv = rgb2hsv( r1, g1, b1 );
hsv2 = rgb2hsv( r2, g2, b2 );
rgb = hsv2rgb( hsv2.h, hsv.s, hsv.v );
dst[ px ] = f1 * rgb.r + f2 * r1 + f3 * r2;
dst[ px + 1 ] = f1 * rgb.g + f2 * g1 + f3 * g2;
dst[ px + 2 ] = f1 * rgb.b + f2 * b1 + f3 * b2;
dst[ px ] = f1 * rgb.r + f2 * r1 + f3 * r2;
dst[ px + 1 ] = f1 * rgb.g + f2 * g1 + f3 * g2;
dst[ px + 2 ] = f1 * rgb.b + f2 * b1 + f3 * b2;
break;
case BlendModes.SATURATION:
hsv = rgb2hsv( r1, g1, b1 );
hsv2 = rgb2hsv( r2, g2, b2 );
hsv = rgb2hsv( r1, g1, b1 );
hsv2 = rgb2hsv( r2, g2, b2 );
rgb = hsv2rgb( hsv.h, hsv2.s, hsv.v );
dst[ px ] = f1 * rgb.r + f2 * r1 + f3 * r2;
dst[ px + 1 ] = f1 * rgb.g + f2 * g1 + f3 * g2;
dst[ px + 2 ] = f1 * rgb.b + f2 * b1 + f3 * b2;
dst[ px ] = f1 * rgb.r + f2 * r1 + f3 * r2;
dst[ px + 1 ] = f1 * rgb.g + f2 * g1 + f3 * g2;
dst[ px + 2 ] = f1 * rgb.b + f2 * b1 + f3 * b2;
break;
*/
case BlendModes.LIGHTER_COLOR:
if ( isLighter( r1, g1, b1, r2, g2, b2 )) {
if ( isLighter( r1, g1, b1, r2, g2, b2 )) {
rgb = { r: r1, g: g1, b: b1 };
} else {
rgb = { r: r2, g: g2, b: b2 };
}
dst[ px ] = f1 * rgb.r + f2 * r1 + f3 * r2;
dst[ px + 1 ] = f1 * rgb.g + f2 * g1 + f3 * g2;
dst[ px + 2 ] = f1 * rgb.b + f2 * b1 + f3 * b2;
dst[ px + 1 ] = f1 * rgb.g + f2 * g1 + f3 * g2;
dst[ px + 2 ] = f1 * rgb.b + f2 * b1 + f3 * b2;
break;
case BlendModes.DARKER_COLOR:
@@ -229,20 +272,80 @@ export const blendLayer = ( dest: CanvasRenderingContext2D, layer: CanvasRenderi
} else {
rgb = { r: r2, g: g2, b: b2 };
}
dst[ px ] = f1 * rgb.r + f2 * r1 + f3 * r2;
dst[ px + 1 ] = f1 * rgb.g + f2 * g1 + f3 * g2;
dst[ px + 2 ] = f1 * rgb.b + f2 * b1 + f3 * b2;
dst[ px ] = f1 * rgb.r + f2 * r1 + f3 * r2;
dst[ px + 1 ] = f1 * rgb.g + f2 * g1 + f3 * g2;
dst[ px + 2 ] = f1 * rgb.b + f2 * b1 + f3 * b2;
break;
}
}
dest.putImageData( dstImageData, left, top );
// free allocated memory of temp context
layer.canvas.width = layer.canvas.height = 1;
source.canvas.width = source.canvas.height = 1;
};
/* internal methods */
function applyNativeCompositeOperation( ctx: CanvasRenderingContext2D, blendMode: BlendModes ): void {
let op = ctx.globalCompositeOperation;
switch ( blendMode ) {
default:
case BlendModes.NORMAL:
op = "source-over";
break;
case BlendModes.DARKEN:
op = "darken";
break;
case BlendModes.MULTIPLY:
op = "multiply";
break;
case BlendModes.COLOR_BURN:
op = "color-burn";
break;
case BlendModes.LIGHTEN:
op = "lighten";
break;
case BlendModes.SCREEN:
op = "screen";
break;
case BlendModes.COLOR_DODGE:
op = "color-dodge";
break;
case BlendModes.LINEAR_DODGE:
op = "lighter";
break;
case BlendModes.OVERLAY:
op = "overlay";
break;
case BlendModes.SOFT_LIGHT:
op = "soft-light";
break;
case BlendModes.HARD_LIGHT:
op = "hard-light";
break;
case BlendModes.DIFFERENCE:
op = "difference";
break;
case BlendModes.EXCLUSION:
op = "exclusion";
break;
case BlendModes.HUE:
op = "hue";
break;
case BlendModes.SATURATION:
op = "saturation";
break;
case BlendModes.COLOR:
op = "color";
break;
case BlendModes.LUMINOSITY:
op = "luminosity";
break;
}
ctx.globalCompositeOperation = op;
}
/**
* Verifies whether the first given RGB values are lighter than the second
*/