diff --git a/src/rendering/canvas-elements/layer-sprite.ts b/src/rendering/canvas-elements/layer-sprite.ts index a2a2757..66eb6cb 100644 --- a/src/rendering/canvas-elements/layer-sprite.ts +++ b/src/rendering/canvas-elements/layer-sprite.ts @@ -43,6 +43,7 @@ import { clipContextToSelection } from "@/rendering/clipping"; import { renderClonedStroke, setCloneSource } from "@/rendering/cloning"; import { renderBrushStroke } from "@/rendering/drawing"; import { floodFill } from "@/rendering/fill"; +import { getMaskComposite, disposeMaskComposite } from "@/rendering/masking"; import { snapSpriteToGuide } from "@/rendering/snapping"; import { applyTransformation } from "@/rendering/transforming"; import { flushLayerCache, clearCacheProperty } from "@/rendering/cache/bitmap-cache"; @@ -625,6 +626,7 @@ export default class LayerSprite extends ZoomableSprite { this.layer, this.getPaintSource(), this.getPaintSize(), this.canvas, this._brush.options.opacity, this._toolType === ToolTypes.ERASER ? "destination-out" : undefined ); + disposeMaskComposite(); disposeDrawableCanvas(); this.resetFilterAndRecache(); @@ -711,7 +713,10 @@ export default class LayerSprite extends ZoomableSprite { } let drawContext: CanvasRenderingContext2D = documentContext; - const applyBlending = enabled && blendMode !== BlendModes.NORMAL; + + const isPainting = this.isPainting(); + const isDrawingOnMask = isPainting && this.isMaskable() && this._toolType !== ToolTypes.ERASER; // erasing from mask needs some work ;-) + const applyBlending = enabled && blendMode !== BlendModes.NORMAL && !isDrawingOnMask; if ( applyBlending ) { drawContext = getBlendContext( documentContext.canvas ); @@ -719,6 +724,12 @@ export default class LayerSprite extends ZoomableSprite { drawContext.scale( scaleFactor, scaleFactor ); } + let maskComposite: CanvasContextPairing | undefined; + if ( isDrawingOnMask ) { + maskComposite = getMaskComposite( this.getPaintSize() ); // temporary canvas to combine paintCanvas with source + drawContext = maskComposite.ctx; + } + drawContext.save(); // transformation save() const transformedBounds = applyTransformation( drawContext, this.layer, viewport ); @@ -730,7 +741,7 @@ export default class LayerSprite extends ZoomableSprite { // invoke base class behaviour to render bitmap super.draw( drawContext, transformCanvas ? undefined : viewport, drawBounds ); - + if ( applyBlending ) { blendLayer( documentContext, drawContext, blendMode ); } @@ -738,11 +749,14 @@ export default class LayerSprite extends ZoomableSprite { drawContext.restore(); // transformation restore() // user is currently drawing on this layer, render contents of drawableCanvas onto screen - if ( this.isPainting()) { + if ( isPainting ) { renderDrawableCanvas( - documentContext, this.getPaintSize(), this.canvas, this._brush.options.opacity, + isDrawingOnMask ? drawContext : documentContext, this.getPaintSize(), this.canvas, this._brush.options.opacity, this._toolType === ToolTypes.ERASER || this.isMaskable() ? "destination-out" : undefined, ); + if ( isDrawingOnMask ) { + documentContext.drawImage( maskComposite.cvs, 0, 0 ); // draw temporary masked canvas onto underlying document + } } if ( altOpacity ) { diff --git a/src/rendering/masking.ts b/src/rendering/masking.ts index 0119fea..a97d773 100644 --- a/src/rendering/masking.ts +++ b/src/rendering/masking.ts @@ -20,6 +20,40 @@ * 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 Size } from "zcanvas"; +import type { CanvasContextPairing } from "@/definitions/editor"; +import { createCanvas, setCanvasDimensions } from "@/utils/canvas-util"; + +let tempCanvas: CanvasContextPairing; + +/** + * Get a reference to a temporary Canvas used to make a composite of the + * source layer and the mask layer while drawing (to be rendered above + * the background for instant previewing purposes). + */ +export const getMaskComposite = ( size: Size ): CanvasContextPairing => { + if ( !tempCanvas ) { + tempCanvas = createCanvas( size.width, size.height ); + } else { + setCanvasDimensions( tempCanvas, size.width, size.height ); + } + return tempCanvas; +}; + +/** + * Free memory allocated to the temporary Canvas. The Canvas will + * remained pooled but by shrinking its size it will reduce memory usage. + */ +export const disposeMaskComposite = (): void => { + if ( tempCanvas ) { + setCanvasDimensions( tempCanvas, 1, 1 ); + } +}; + +/** + * Apply a mask defined in provided mask property onto provided image source where + * the output is drawn onto provided destinationContext. + */ export const maskImage = ( destinationContext: CanvasRenderingContext2D, image: HTMLCanvasElement, mask: HTMLCanvasElement, width: number, height: number, maskOffsetX = 0, maskOffsetY = 0 @@ -33,4 +67,4 @@ export const maskImage = ( destinationContext.drawImage( mask, maskOffsetX, maskOffsetY ); destinationContext.restore(); -}; \ No newline at end of file +};