Address issue where stepping through state history for mask drawing is not restoring state correctly

This commit is contained in:
Igor Zinken
2025-03-04 10:27:29 +01:00
parent f63619ddd8
commit 2c4e1759d4
3 changed files with 51 additions and 21 deletions

View File

@@ -85,7 +85,7 @@ class LayerSprite extends ZoomableSprite {
protected _invertSelection: boolean;
protected _toolType: ToolTypes;
protected _orgSourceToStore: string;
protected _pendingPaintState: number;
protected _pendingPaintState: number; // ReturnType<typeof setTimeout>;
protected _rafFx: boolean;
constructor( layer: Layer ) {
@@ -391,14 +391,14 @@ class LayerSprite extends ZoomableSprite {
* not delay to history state UI from updating more than necessary.
*/
preparePendingPaintState(): void {
canvasToBlob( this.layer.source ).then( blob => {
canvasToBlob( this.getPaintSource() ).then( blob => {
this._orgSourceToStore = blobToResource( blob );
});
this.debouncePaintStore();
}
debouncePaintStore( timeout: number = 5000 ): void {
this._pendingPaintState = window.setTimeout( this.storePaintState.bind( this ), timeout ) as unknown as number;
this._pendingPaintState = window.setTimeout( this.storePaintState.bind( this ), timeout );
}
getPaintSource(): HTMLCanvasElement {
@@ -430,14 +430,15 @@ class LayerSprite extends ZoomableSprite {
this._orgSourceToStore = null;
const blob = await canvasToBlob( layer.source );
const isMask = this.isMaskable();
const blob = await canvasToBlob( this.getPaintSource() );
const newState = blobToResource( blob );
enqueueState( `spritePaint_${layer.id}`, {
undo(): void {
restorePaintFromHistory( layer, orgState );
restorePaintFromHistory( layer, orgState, isMask );
},
redo(): void {
restorePaintFromHistory( layer, newState);
restorePaintFromHistory( layer, newState, isMask );
},
resources: [ orgState, newState ],
});
@@ -781,9 +782,10 @@ function positionSpriteFromHistory( layer: Layer, x: number, y: number ): void {
}
}
function restorePaintFromHistory( layer: Layer, state: string ): void {
const ctx = layer.source.getContext( "2d" );
ctx.clearRect( 0, 0, layer.source.width, layer.source.height );
function restorePaintFromHistory( layer: Layer, state: string, isMask: boolean ): void {
const source = isMask ? layer.mask : layer.source;
const ctx = source.getContext( "2d" ) as CanvasRenderingContext2D;
ctx.clearRect( 0, 0, source.width, source.height );
const image = new Image();
image.onload = () => {
ctx.drawImage( image, 0, 0 );

36
src/rendering/masking.ts Normal file
View File

@@ -0,0 +1,36 @@
/**
* The MIT License (MIT)
*
* Igor Zinken 2021-2025 - 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
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* 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.
*/
export const maskImage = (
destinationContext: CanvasRenderingContext2D, image: HTMLCanvasElement, mask: HTMLCanvasElement,
width: number, height: number, maskOffsetX = 0, maskOffsetY = 0
): void => {
destinationContext.clearRect( 0, 0, width, height );
destinationContext.drawImage( image, 0, 0 );
destinationContext.save();
destinationContext.globalCompositeOperation = "destination-out";
destinationContext.drawImage( mask, maskOffsetX, maskOffsetY );
destinationContext.restore();
};

View File

@@ -31,6 +31,7 @@ import { replaceLayerSource } from "@/utils/layer-util";
import { clone } from "@/utils/object-util";
import { getLayerCache, setLayerCache } from "@/rendering/cache/bitmap-cache";
import type { RenderCache } from "@/rendering/cache/bitmap-cache";
import { maskImage } from "@/rendering/masking";
import { renderMultiLineText } from "@/rendering/text";
import { loadGoogleFont } from "@/services/font-service";
import FilterWorker from "@/workers/filter.worker?worker";
@@ -132,7 +133,7 @@ export const renderEffectsForLayer = async ( layer: Layer, useCaching = true ):
if ( applyMask ) {
//console.info( "apply mask" );
await renderMask( layer, ctx, applyFilter ? cloneCanvas( cvs ) : layer.source, width, height );
renderMask( layer, ctx, applyFilter ? cloneCanvas( cvs ) : layer.source, width, height );
}
// step 4. update cache and on-screen canvas contents
@@ -222,20 +223,11 @@ const renderText = async ( layer: Layer ): Promise<HTMLCanvasElement> => {
return cvs;
};
const renderMask = async ( layer: Layer, ctx: CanvasRenderingContext2D, sourceBitmap: HTMLCanvasElement,
width: number, height: number ): Promise<void> => {
const renderMask = ( layer: Layer, ctx: CanvasRenderingContext2D, sourceBitmap: HTMLCanvasElement, width: number, height: number ): void => {
if ( !layer.mask ) {
return;
}
ctx.clearRect( 0, 0, width, height );
ctx.drawImage( sourceBitmap, 0, 0 );
ctx.save();
// commenting out the next line allows you to debug by seeing the mask
ctx.globalCompositeOperation = "destination-out";
ctx.drawImage( layer.mask, layer.maskX, layer.maskY );
ctx.restore();
maskImage( ctx, sourceBitmap, layer.mask, width, height, layer.maskX, layer.maskY );
};
function getJobFromQueue( jobId: number ): RenderJob | undefined {