mirror of
https://github.com/igorski/bitmappery.git
synced 2026-08-03 12:31:37 +02:00
Optimise blend mode application using GPU powered composite operations when available
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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()
|
||||
|
||||
2
src/rendering/cache/blended-layer-cache.ts
vendored
2
src/rendering/cache/blended-layer-cache.ts
vendored
@@ -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
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user