mirror of
https://github.com/igorski/bitmappery.git
synced 2026-07-05 14:59:15 +02:00
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:
@@ -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;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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();
|
||||
};
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user