fix(ui): prevent flash when applying transform

This commit is contained in:
psychedelicious 2024-07-31 14:27:23 +10:00
parent c9849a79ea
commit a57e618d47
4 changed files with 34 additions and 55 deletions

View File

@ -7,7 +7,7 @@ import { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
import { CanvasRect } from 'features/controlLayers/konva/CanvasRect'; import { CanvasRect } from 'features/controlLayers/konva/CanvasRect';
import { getBrushLineId, getEraserLineId, getRectShapeId } from 'features/controlLayers/konva/naming'; import { getBrushLineId, getEraserLineId, getRectShapeId } from 'features/controlLayers/konva/naming';
import { konvaNodeToBlob, mapId, previewBlob } from 'features/controlLayers/konva/util'; import { konvaNodeToBlob, mapId, previewBlob } from 'features/controlLayers/konva/util';
import { layerAllObjectsDeletedExceptOne, layerRasterized } from 'features/controlLayers/store/canvasV2Slice'; import { layerRasterized } from 'features/controlLayers/store/canvasV2Slice';
import { import {
type BrushLine, type BrushLine,
type CanvasV2State, type CanvasV2State,
@ -55,7 +55,6 @@ export class CanvasLayer {
isTransforming: boolean; isTransforming: boolean;
isPendingBboxCalculation: boolean; isPendingBboxCalculation: boolean;
rasterizedObjectId: string | null;
rect: Rect; rect: Rect;
bbox: Rect; bbox: Rect;
@ -191,7 +190,6 @@ export class CanvasLayer {
this._bboxNeedsUpdate = true; this._bboxNeedsUpdate = true;
this.isTransforming = false; this.isTransforming = false;
this._isFirstRender = true; this._isFirstRender = true;
this.rasterizedObjectId = null;
this.isPendingBboxCalculation = false; this.isPendingBboxCalculation = false;
this._log = this.manager.getLogger(`layer_${this.id}`); this._log = this.manager.getLogger(`layer_${this.id}`);
} }
@ -218,7 +216,7 @@ export class CanvasLayer {
return; return;
} }
const drawingBuffer = this._drawingBuffer; const drawingBuffer = this._drawingBuffer;
this.setDrawingBuffer(null); await this.setDrawingBuffer(null);
// We need to give the objects a fresh ID else they will be considered the same object when they are re-rendered as // We need to give the objects a fresh ID else they will be considered the same object when they are re-rendered as
// a non-buffer object, and we won't trigger things like bbox calculation // a non-buffer object, and we won't trigger things like bbox calculation
@ -248,12 +246,12 @@ export class CanvasLayer {
this._log.debug('Updating'); this._log.debug('Updating');
const { position, objects, opacity, isEnabled } = state; const { position, objects, opacity, isEnabled } = state;
if (this._isFirstRender || position !== this._state.position) {
await this.updatePosition({ position });
}
if (this._isFirstRender || objects !== this._state.objects) { if (this._isFirstRender || objects !== this._state.objects) {
await this.updateObjects({ objects }); await this.updateObjects({ objects });
} }
if (this._isFirstRender || position !== this._state.position) {
await this.updatePosition({ position });
}
if (this._isFirstRender || opacity !== this._state.opacity) { if (this._isFirstRender || opacity !== this._state.opacity) {
await this.updateOpacity({ opacity }); await this.updateOpacity({ opacity });
} }
@ -270,14 +268,14 @@ export class CanvasLayer {
this._isFirstRender = false; this._isFirstRender = false;
} }
async updateVisibility(arg?: { isEnabled: boolean }) { updateVisibility(arg?: { isEnabled: boolean }) {
this._log.trace('Updating visibility'); this._log.trace('Updating visibility');
const isEnabled = get(arg, 'isEnabled', this._state.isEnabled); const isEnabled = get(arg, 'isEnabled', this._state.isEnabled);
const hasObjects = this.objects.size > 0 || this._drawingBuffer !== null; const hasObjects = this.objects.size > 0 || this._drawingBuffer !== null;
this.konva.layer.visible(isEnabled || hasObjects); this.konva.layer.visible(isEnabled || hasObjects);
} }
async updatePosition(arg?: { position: Coordinate }) { updatePosition(arg?: { position: Coordinate }) {
this._log.trace('Updating position'); this._log.trace('Updating position');
const position = get(arg, 'position', this._state.position); const position = get(arg, 'position', this._state.position);
const bboxPadding = this.manager.getScaledBboxPadding(); const bboxPadding = this.manager.getScaledBboxPadding();
@ -331,23 +329,15 @@ export class CanvasLayer {
if (didUpdate) { if (didUpdate) {
this.calculateBbox(); this.calculateBbox();
} }
if (this.isTransforming && this.rasterizedObjectId) {
this.manager._store.dispatch(layerAllObjectsDeletedExceptOne({ id: this.id, objectId: this.rasterizedObjectId }));
this.isTransforming = false;
this.rasterizedObjectId = null;
}
} }
async updateOpacity(arg?: { opacity: number }) { updateOpacity(arg?: { opacity: number }) {
this._log.trace('Updating opacity'); this._log.trace('Updating opacity');
const opacity = get(arg, 'opacity', this._state.opacity); const opacity = get(arg, 'opacity', this._state.opacity);
this.konva.objectGroup.opacity(opacity); this.konva.objectGroup.opacity(opacity);
} }
async updateInteraction(arg?: { toolState: CanvasV2State['tool']; isSelected: boolean }) { updateInteraction(arg?: { toolState: CanvasV2State['tool']; isSelected: boolean }) {
this._log.trace('Updating interaction'); this._log.trace('Updating interaction');
const toolState = get(arg, 'toolState', this.manager.stateApi.getToolState()); const toolState = get(arg, 'toolState', this.manager.stateApi.getToolState());
@ -396,7 +386,7 @@ export class CanvasLayer {
} }
} }
async updateBbox() { updateBbox() {
this._log.trace('Updating bbox'); this._log.trace('Updating bbox');
if (this.isPendingBboxCalculation) { if (this.isPendingBboxCalculation) {
@ -442,7 +432,7 @@ export class CanvasLayer {
}); });
} }
async syncStageScale() { syncStageScale() {
this._log.trace('Syncing scale to stage'); this._log.trace('Syncing scale to stage');
const onePixel = this.manager.getScaledPixel(); const onePixel = this.manager.getScaledPixel();
@ -519,7 +509,7 @@ export class CanvasLayer {
return false; return false;
} }
async startTransform() { startTransform() {
this._log.debug('Starting transform'); this._log.debug('Starting transform');
this.isTransforming = true; this.isTransforming = true;
@ -539,7 +529,7 @@ export class CanvasLayer {
this.konva.bbox.visible(false); this.konva.bbox.visible(false);
} }
async resetScale() { resetScale() {
const attrs = { const attrs = {
scaleX: 1, scaleX: 1,
scaleY: 1, scaleY: 1,
@ -550,39 +540,37 @@ export class CanvasLayer {
this.konva.interactionRect.setAttrs(attrs); this.konva.interactionRect.setAttrs(attrs);
} }
async applyTransform() { async rasterizeLayer() {
this._log.debug('Applying transform'); this._log.debug('Rasterizing layer');
const objectGroupClone = this.konva.objectGroup.clone(); const objectGroupClone = this.konva.objectGroup.clone();
const interactionRectClone = this.konva.interactionRect.clone(); const interactionRectClone = this.konva.interactionRect.clone();
const rect = interactionRectClone.getClientRect(); const rect = interactionRectClone.getClientRect();
const blob = await konvaNodeToBlob(objectGroupClone, rect); const blob = await konvaNodeToBlob(objectGroupClone, rect);
if (this.manager._isDebugging) { if (this.manager._isDebugging) {
previewBlob(blob, 'transformed layer'); previewBlob(blob, 'Rasterized layer');
} }
const imageDTO = await uploadImage(blob, `${this.id}_transform.png`, 'other', true); const imageDTO = await uploadImage(blob, `${this.id}_rasterized.png`, 'other', true);
const { dispatch } = getStore(); const { dispatch } = getStore();
const imageObject = imageDTOToImageObject(this.id, uuidv4(), imageDTO); const imageObject = imageDTOToImageObject(this.id, uuidv4(), imageDTO);
dispatch(layerRasterized({ id: this.id, imageObject, position: { x: rect.x, y: rect.y } })); await this._renderObject(imageObject, true);
this.rasterizedObjectId = imageObject.id; for (const obj of this.objects.values()) {
if (obj.id !== imageObject.id) {
obj.konva.group.visible(false);
}
}
this.resetScale(); this.resetScale();
dispatch(layerRasterized({ id: this.id, imageObject, position: { x: rect.x, y: rect.y } }));
} }
async finalizeTransform() { stopTransform() {
// this._log.debug('Stopping transform');
}
async cancelTransform() {
this._log.debug('Canceling transform');
this.isTransforming = false; this.isTransforming = false;
this.resetScale(); this.resetScale();
await this.updatePosition({ position: this._state.position }); this.updatePosition();
await this.updateBbox(); this.updateBbox();
await this.updateInteraction({ this.updateInteraction();
toolState: this.manager.stateApi.getToolState(),
isSelected: this.manager.stateApi.getIsSelected(this.id),
});
} }
getDefaultRect(): Rect { getDefaultRect(): Rect {

View File

@ -299,10 +299,11 @@ export class CanvasManager {
this.onTransform?.(true); this.onTransform?.(true);
} }
applyTransform() { async applyTransform() {
const layer = this.getTransformingLayer(); const layer = this.getTransformingLayer();
if (layer) { if (layer) {
layer.applyTransform(); await layer.rasterizeLayer();
layer.stopTransform();
} }
this.onTransform?.(false); this.onTransform?.(false);
} }
@ -310,7 +311,7 @@ export class CanvasManager {
cancelTransform() { cancelTransform() {
const layer = this.getTransformingLayer(); const layer = this.getTransformingLayer();
if (layer) { if (layer) {
layer.cancelTransform(); layer.stopTransform();
} }
this.onTransform?.(false); this.onTransform?.(false);
} }

View File

@ -220,7 +220,6 @@ export const {
layerTranslated, layerTranslated,
layerBboxChanged, layerBboxChanged,
layerImageAdded, layerImageAdded,
layerAllObjectsDeletedExceptOne,
layerAllDeleted, layerAllDeleted,
layerImageCacheChanged, layerImageCacheChanged,
layerScaled, layerScaled,

View File

@ -259,19 +259,10 @@ export const layersReducers = {
if (!layer) { if (!layer) {
return; return;
} }
layer.objects.push(imageObject); layer.objects = [imageObject];
layer.position = position; layer.position = position;
state.layers.imageCache = null; state.layers.imageCache = null;
}, },
layerAllObjectsDeletedExceptOne: (state, action: PayloadAction<{ id: string; objectId: string }>) => {
const { id, objectId } = action.payload;
const layer = selectLayer(state, id);
if (!layer) {
return;
}
layer.objects = layer.objects.filter((obj) => obj.id === objectId);
state.layers.imageCache = null;
},
} satisfies SliceCaseReducers<CanvasV2State>; } satisfies SliceCaseReducers<CanvasV2State>;
const scalePoints = (points: number[], scaleX: number, scaleY: number) => { const scalePoints = (points: number[], scaleX: number, scaleY: number) => {