diff --git a/src/rendering/actors/layer-renderer.ts b/src/rendering/actors/layer-renderer.ts index 7f84957..d4f27c0 100644 --- a/src/rendering/actors/layer-renderer.ts +++ b/src/rendering/actors/layer-renderer.ts @@ -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; 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 { 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; } }; diff --git a/src/rendering/actors/zoomable-sprite.ts b/src/rendering/actors/zoomable-sprite.ts index f51ae31..b219b09 100644 --- a/src/rendering/actors/zoomable-sprite.ts +++ b/src/rendering/actors/zoomable-sprite.ts @@ -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, diff --git a/src/rendering/operations/masking.ts b/src/rendering/operations/masking.ts index a97d773..4c4ce02 100644 --- a/src/rendering/operations/masking.ts +++ b/src/rendering/operations/masking.ts @@ -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(); diff --git a/src/rendering/utils/drawable-canvas-utils.ts b/src/rendering/utils/drawable-canvas-utils.ts index b757baf..69de8fb 100644 --- a/src/rendering/utils/drawable-canvas-utils.ts +++ b/src/rendering/utils/drawable-canvas-utils.ts @@ -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(); }; diff --git a/src/services/render-service.ts b/src/services/render-service.ts index 4e4477c..bfbbba7 100644 --- a/src/services/render-service.ts +++ b/src/services/render-service.ts @@ -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