When drawing on a mask using the eraser tool, the live preview will now correctly show the result of the operation

This commit is contained in:
Igor Zinken
2025-03-27 21:22:59 +01:00
committed by GitHub
parent f74df9098e
commit 32e308bb6b
5 changed files with 76 additions and 62 deletions

View File

@@ -39,14 +39,14 @@ import { clipContextToSelection, clipLayer } from "@/rendering/operations/clippi
import { renderClonedStroke, setCloneSource } from "@/rendering/operations/cloning";
import { renderBrushStroke } from "@/rendering/operations/drawing";
import { floodFill } from "@/rendering/operations/fill";
import { getMaskComposite, disposeMaskComposite } from "@/rendering/operations/masking";
import { getMaskComposite, disposeMaskComposite, maskImage } from "@/rendering/operations/masking";
import { snapToGuide } from "@/rendering/operations/snapping";
import { applyTransformation } from "@/rendering/operations/transforming";
import { flushLayerCache, clearCacheProperty } from "@/rendering/cache/bitmap-cache";
import { cacheBlendedLayer, flushBlendedLayerCache, getBlendCache, getBlendableLayers, isBlendCached, pauseBlendCaching, useBlendCaching } from "@/rendering/cache/blended-layer-cache";
import { renderBrushOutline } from "@/rendering/cursors/brush";
import {
getDrawableCanvas, renderDrawableCanvas, disposeDrawableCanvas, commitDrawingToLayer, sliceBrushPointers, createOverrideConfig
getDrawableCanvas, renderDrawableCanvas, disposeDrawableCanvas, sliceBrushPointers, createOverrideConfig
} from "@/rendering/utils/drawable-canvas-utils";
import BrushFactory from "@/factories/brush-factory";
import { getRendererForLayer } from "@/factories/renderer-factory";
@@ -89,6 +89,7 @@ export default class LayerRenderer extends ZoomableSprite {
protected _orgSourceToStore: HTMLCanvasElement | undefined;
protected _pendingPaintState: number | undefined; // ReturnType<typeof setTimeout>;
protected _pendingEffectsRender: boolean;
protected _unmaskedBitmap: HTMLCanvasElement | undefined; // a reference to the effected source w/out mask applied
constructor( layer: Layer ) {
const { left, top, width, height } = layer;
@@ -117,6 +118,10 @@ export default class LayerRenderer extends ZoomableSprite {
this.actionTarget = target;
}
setUnmaskedBitmap( unmaskedBitmap?: HTMLCanvasElement ): void {
this._unmaskedBitmap = unmaskedBitmap; // this bitmap will only be defined when the layer has a mask
}
getStore(): Store<BitMapperyState> {
return this.canvas?.store;
}
@@ -597,9 +602,10 @@ export default class LayerRenderer extends ZoomableSprite {
const { getters } = this.getStore();
if ( this.isPainting() ) {
commitDrawingToLayer(
this.layer, this.getPaintSource(), this.getPaintSize(), this.canvas, this._brush.options.opacity,
this._toolType === ToolTypes.ERASER ? "destination-out" : undefined
// commit the drawable canvas content onto the destination source
renderDrawableCanvas(
this.getPaintSource().getContext( "2d" ), this.getPaintSize(), this.canvas, this._brush.options.opacity,
this._toolType === ToolTypes.ERASER ? "destination-out" : undefined, this.layer
);
disposeMaskComposite();
disposeDrawableCanvas();
@@ -635,14 +641,14 @@ export default class LayerRenderer extends ZoomableSprite {
}
}
override drawCropped( canvasContext: CanvasRenderingContext2D, transformedBounds: TransformedDrawBounds ): void {
override drawCropped( canvasContext: CanvasRenderingContext2D, bitmap: HTMLCanvasElement, transformedBounds: TransformedDrawBounds ): void {
if ( !isScaled( this.layer ) ) {
return super.drawCropped( canvasContext, transformedBounds );
return super.drawCropped( canvasContext, bitmap, transformedBounds );
}
const scale = 1 / this.layer.effects.scale;
const { src, dest } = transformedBounds;
canvasContext.drawImage(
this._bitmap,
bitmap,
( HALF + src.left * scale ) << 0,
( HALF + src.top * scale ) << 0,
( HALF + src.width * scale ) << 0,
@@ -689,7 +695,8 @@ export default class LayerRenderer extends ZoomableSprite {
let drawContext: CanvasRenderingContext2D = documentContext;
const isPainting = this.isPainting();
const isDrawingOnMask = isPainting && isMaskable( this.layer, this.getStore() ) && this._toolType !== ToolTypes.ERASER; // erasing from mask needs some work ;-)
const isDrawingOnMask = isPainting && isMaskable( this.layer, this.getStore() );
const isErasingOnMask = isDrawingOnMask && this._toolType === ToolTypes.ERASER;
const applyBlending = enabled && blendMode !== BlendModes.NORMAL && !isDrawingOnMask;
if ( applyBlending ) {
@@ -701,9 +708,11 @@ export default class LayerRenderer extends ZoomableSprite {
let maskComposite: CanvasContextPairing | undefined;
if ( isDrawingOnMask ) {
maskComposite = getMaskComposite( this.getPaintSize() ); // temporary canvas to combine paintCanvas with source
drawContext = maskComposite.ctx;
if ( !isErasingOnMask ) {
drawContext = maskComposite.ctx;
}
}
drawContext.save(); // transformation save()
const transformedBounds = applyTransformation( drawContext, this.layer, viewport );
@@ -715,15 +724,27 @@ export default class LayerRenderer extends ZoomableSprite {
// invoke base class behaviour to render bitmap
super.draw( drawContext, transformCanvas ? undefined : viewport, drawBounds );
if ( isErasingOnMask ) {
const tempMask = cloneCanvas( this._unmaskedBitmap ); // will contain drawable canvas contents to be used as eraser
renderDrawableCanvas(
tempMask.getContext( "2d" )!, this.getPaintSize(), this.canvas,
this._brush.options.opacity, "destination-out", this.layer
);
const tempSource = cloneCanvas( this._bitmap as HTMLCanvasElement ); // used to stamp the temporary mask on
maskImage( tempSource.getContext( "2d" )!, this._unmaskedBitmap, tempMask, this.layer.source.width, this.layer.source.height, this.layer.maskX, this.layer.maskY );
this.drawBitmap( documentContext, tempSource, transformCanvas ? undefined : viewport, drawBounds );
}
if ( applyBlending ) {
blendLayer( documentContext, drawContext, blendMode );
}
drawContext.restore(); // transformation restore()
// user is currently drawing on this layer, render contents of drawableCanvas onto screen
if ( isPainting ) {
// user is currently drawing on this layer, render contents of drawableCanvas onto screen for live preview
if ( isPainting && !isErasingOnMask ) {
const clipContext = !this._selection && ( this._bounds.left !== 0 || this._bounds.top !== 0 || transformedBounds );
if ( clipContext ) {
// when the layer if offset/transformed and there is no active selection, clip the out of bounds content
@@ -762,7 +783,7 @@ export default class LayerRenderer extends ZoomableSprite {
flushLayerCache( this.layer );
this._bitmap = null;
this._bitmapReady = false;
this._bitmap = null;
this._unmaskedBitmap = null;
}
};

View File

@@ -31,9 +31,9 @@ class ZoomableSprite extends sprite {
super( opts );
}
drawCropped( canvasContext: CanvasRenderingContext2D, { src, dest }: TransformedDrawBounds ): void {
drawCropped( canvasContext: CanvasRenderingContext2D, bitmap: HTMLCanvasElement, { src, dest }: TransformedDrawBounds ): void {
canvasContext.drawImage(
this._bitmap,
bitmap,
( HALF + src.left ) << 0,
( HALF + src.top ) << 0,
( HALF + src.width ) << 0,
@@ -54,19 +54,21 @@ class ZoomableSprite extends sprite {
// multiple transformations take place on the source (see LayerRenderer#draw())
draw( canvasContext: CanvasRenderingContext2D, viewport?: Viewport, bounds: Rectangle = this._bounds ): void {
let render = this._bitmapReady;
if ( render && viewport ) {
render = isInsideViewport( bounds, viewport );
if ( this._bitmapReady ) {
this.drawBitmap( canvasContext, this._bitmap as HTMLCanvasElement, viewport, bounds );
}
if ( !render ) {
}
drawBitmap( canvasContext: CanvasRenderingContext2D, bitmap: HTMLCanvasElement, viewport?: Viewport, bounds: Rectangle = this._bounds ): void {
if ( viewport && !isInsideViewport( bounds, viewport )) {
return;
}
if ( viewport ) {
this.drawCropped( canvasContext, calculateDrawRectangle( bounds, viewport ));
this.drawCropped( canvasContext, bitmap, calculateDrawRectangle( bounds, viewport ));
} else {
const { left, top, width, height } = bounds;
canvasContext.drawImage(
this._bitmap,
bitmap,
( HALF + left ) << 0,
( HALF + top ) << 0,
( HALF + width ) << 0,

View File

@@ -55,11 +55,11 @@ export const disposeMaskComposite = (): void => {
* the output is drawn onto provided destinationContext.
*/
export const maskImage = (
destinationContext: CanvasRenderingContext2D, image: HTMLCanvasElement, mask: HTMLCanvasElement,
width: number, height: number, maskOffsetX = 0, maskOffsetY = 0
destinationContext: CanvasRenderingContext2D, source: HTMLCanvasElement, mask: HTMLCanvasElement,
sourceWidth: number, sourceHeight: number, maskOffsetX = 0, maskOffsetY = 0
): void => {
destinationContext.clearRect( 0, 0, width, height );
destinationContext.drawImage( image, 0, 0 );
destinationContext.clearRect( 0, 0, sourceWidth, sourceHeight );
destinationContext.drawImage( source, 0, 0 );
destinationContext.save();

View File

@@ -55,18 +55,35 @@ export const getDrawableCanvas = ( size: Size ): CanvasContextPairing => {
* Render the contents of the drawableCanvas onto given destinationContext using the scaling properties
* corresponding to provided documentScale. This can be used to render the contents of the drawable canvas
* while drawing is still taking place for live preview purposes.
*
* When Layer is provided, the associated transformation properties are taken into account, ensuring that the visual
* location of the drawableCanvas is correctly inserted into the destination context, relative to the optional transformation
* effects of the Layer, to be used when committing the effects permanently when drawing has completed.
*/
export const renderDrawableCanvas = (
destinationContext: CanvasRenderingContext2D, destinationSize: Size, zoomableCanvas: ZoomableCanvas,
alpha = 1, compositeOperation?: GlobalCompositeOperation, offset?: Point
alpha = 1, compositeOperation?: GlobalCompositeOperation, layer?: Layer
): void => {
const source = drawableCanvas.cvs;
const { documentScale } = zoomableCanvas;
const viewport = offset ? zoomableCanvas.getViewport() : undefined;
destinationContext.save();
// correct for the optional layer transformation effects when Layer is provided
let offset: Point | undefined;
if ( layer ) {
const { width, height } = layer;
const { scale } = layer.effects;
offset = {
x: ( width * scale / 2 ) - ( width / 2 ) - layer.left,
y: ( height * scale / 2 ) - ( height / 2 ) - layer.top,
};
reverseTransformation( destinationContext, layer );
}
const viewport = offset ? zoomableCanvas.getViewport() : undefined;
destinationContext.globalAlpha = alpha;
if ( compositeOperation !== undefined ) {
destinationContext.globalCompositeOperation = compositeOperation;
@@ -79,36 +96,6 @@ export const renderDrawableCanvas = (
(( viewport?.top ?? 0 ) * documentScale ) + ( offset?.y ?? 0 ),
destinationSize.width, destinationSize.height
);
destinationContext.restore();
};
/**
* Commit the contents of the drawableCanvas onto provided Layers source Canvas, to be invoked when drawing has completed.
* This takes the associated destination Layer properties into account, ensuring that the visual location of the drawableCanvas
* is correctly inserted into the destination Canvas, relative to the optional transformation effects of the Layer.
*/
export const commitDrawingToLayer = (
layer: Layer, destinationCanvas: HTMLCanvasElement, destinationSize: Size, zoomableCanvas: ZoomableCanvas,
alpha = 1, compositeOperation?: GlobalCompositeOperation
) => {
const destinationContext = destinationCanvas.getContext( "2d" ) as CanvasRenderingContext2D;
destinationContext.save();
// correct for the optional layer transformation effects
reverseTransformation( destinationContext, layer );
const { width, height } = layer;
const { scale } = layer.effects;
const x = ( width * scale / 2 ) - ( width / 2 ) - layer.left;
const y = ( height * scale / 2 ) - ( height / 2 ) - layer.top;
// render
renderDrawableCanvas( destinationContext, destinationSize, zoomableCanvas, alpha, compositeOperation, { x, y });
destinationContext.restore();
};

View File

@@ -133,7 +133,11 @@ export const renderEffectsForLayer = async ( layer: Layer, useCaching = true ):
if ( applyMask ) {
//console.info( "apply mask" );
renderMask( layer, ctx, applyFilter ? cloneCanvas( cvs ) : layer.source, width, height );
const unmaskedBitmap = cloneCanvas( cvs );
renderer.setUnmaskedBitmap( unmaskedBitmap );
renderMask( layer, ctx, applyFilter ? unmaskedBitmap : layer.source, width, height );
} else {
renderer.setUnmaskedBitmap( undefined );
}
// step 4. update cache and on-screen canvas contents