feat(ui): add flip & reset to transform

This commit is contained in:
psychedelicious 2024-08-28 07:57:15 +10:00
parent 6d209c6cc3
commit e6153e6fa4
4 changed files with 103 additions and 26 deletions

View File

@ -1738,6 +1738,8 @@
"unlocked": "Unlocked",
"deleteSelected": "Delete Selected",
"deleteAll": "Delete All",
"flipHorizontal": "Flip Horizontal",
"flipVertical": "Flip Vertical",
"fill": {
"fillStyle": "Fill Style",
"solid": "Solid",

View File

@ -32,14 +32,14 @@ export const CanvasEditor = memo(() => {
>
<ControlLayersToolbar />
<StageComponent />
<Flex position="absolute" bottom={16} gap={2} align="center" justify="center">
<Flex position="absolute" bottom={8} gap={2} align="center" justify="center">
<CanvasManagerProviderGate>
<StagingAreaIsStagingGate>
<StagingAreaToolbar />
</StagingAreaIsStagingGate>
</CanvasManagerProviderGate>
</Flex>
<Flex position="absolute" bottom={16}>
<Flex position="absolute" bottom={8}>
<CanvasManagerProviderGate>
<Filter />
<Transform />

View File

@ -6,9 +6,15 @@ import {
useEntityIdentifierContext,
} from 'features/controlLayers/contexts/EntityIdentifierContext';
import { useEntityAdapter } from 'features/controlLayers/hooks/useEntityAdapter';
import { memo, useCallback } from 'react';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import { PiCheckBold, PiXBold } from 'react-icons/pi';
import {
PiArrowsCounterClockwiseBold,
PiCheckBold,
PiFlipHorizontalFill,
PiFlipVerticalFill,
PiXBold,
} from 'react-icons/pi';
const TransformBox = memo(() => {
const { t } = useTranslation();
@ -16,14 +22,6 @@ const TransformBox = memo(() => {
const adapter = useEntityAdapter(entityIdentifier);
const isProcessing = useStore(adapter.transformer.$isProcessing);
const applyTransform = useCallback(() => {
adapter.transformer.applyTransform();
}, [adapter.transformer]);
const cancelFilter = useCallback(() => {
adapter.transformer.stopTransform();
}, [adapter.transformer]);
return (
<Flex
bg="base.800"
@ -40,16 +38,49 @@ const TransformBox = memo(() => {
<Heading size="md" color="base.300" userSelect="none">
{t('controlLayers.tool.transform')}
</Heading>
<ButtonGroup isAttached={false} size="sm" justifyContent="center">
<Button
leftIcon={<PiFlipHorizontalFill />}
onClick={adapter.transformer.flipHorizontal}
isLoading={isProcessing}
loadingText={t('controlLayers.flipHorizontal')}
>
{t('controlLayers.flipHorizontal')}
</Button>
<Button
leftIcon={<PiFlipVerticalFill />}
onClick={adapter.transformer.flipVertical}
isLoading={isProcessing}
loadingText={t('controlLayers.flipVertical')}
>
{t('controlLayers.flipVertical')}
</Button>
<Button
leftIcon={<PiArrowsCounterClockwiseBold />}
onClick={adapter.transformer.resetTransform}
isLoading={isProcessing}
loadingText={t('controlLayers.reset')}
>
{t('accessibility.reset')}
</Button>
</ButtonGroup>
<ButtonGroup isAttached={false} size="sm" alignSelf="self-end">
<Button
leftIcon={<PiCheckBold />}
onClick={applyTransform}
onClick={adapter.transformer.applyTransform}
isLoading={isProcessing}
loadingText={t('common.apply')}
variant="ghost"
>
{t('common.apply')}
</Button>
<Button leftIcon={<PiXBold />} onClick={cancelFilter} isLoading={isProcessing} loadingText={t('common.cancel')}>
<Button
leftIcon={<PiXBold />}
onClick={adapter.transformer.stopTransform}
isLoading={isProcessing}
loadingText={t('common.cancel')}
variant="ghost"
>
{t('common.cancel')}
</Button>
</ButtonGroup>

View File

@ -259,13 +259,7 @@ export class CanvasEntityTransformer extends CanvasModuleABC {
// This is called when a transform anchor is dragged. By this time, the transform constraints in the above
// callbacks have been enforced, and the transformer has updated its nodes' attributes. We need to pass the
// updated attributes to the object group, propagating the transformation on down.
this.parent.renderer.konva.objectGroup.setAttrs({
x: this.konva.proxyRect.x(),
y: this.konva.proxyRect.y(),
scaleX: this.konva.proxyRect.scaleX(),
scaleY: this.konva.proxyRect.scaleY(),
rotation: this.konva.proxyRect.rotation(),
});
this.syncObjectGroupWithProxyRect();
});
this.konva.transformer.on('transformend', () => {
@ -395,6 +389,53 @@ export class CanvasEntityTransformer extends CanvasModuleABC {
this.parent.konva.layer.add(this.konva.transformer);
}
flipHorizontal = () => {
if (!this.isTransforming || this.$isProcessing.get()) {
return;
}
// Flipping horizontally = flipping across the vertical axis:
// - Flip by negating the x scale
// - Restore position by translating the rect rightwards by the width of the rect
const x = this.konva.proxyRect.x();
const width = this.konva.proxyRect.width();
const scaleX = this.konva.proxyRect.scaleX();
this.konva.proxyRect.setAttrs({
scaleX: -scaleX,
x: x + width * scaleX,
});
this.syncObjectGroupWithProxyRect();
};
flipVertical = () => {
if (!this.isTransforming || this.$isProcessing.get()) {
return;
}
// Flipping vertically = flipping across the horizontal axis:
// - Flip by negating the y scale
// - Restore position by translating the rect downwards by the height of the rect
const y = this.konva.proxyRect.y();
const height = this.konva.proxyRect.height();
const scaleY = this.konva.proxyRect.scaleY();
this.konva.proxyRect.setAttrs({
scaleY: -scaleY,
y: y + height * scaleY,
});
this.syncObjectGroupWithProxyRect();
};
syncObjectGroupWithProxyRect = () => {
this.parent.renderer.konva.objectGroup.setAttrs({
x: this.konva.proxyRect.x(),
y: this.konva.proxyRect.y(),
scaleX: this.konva.proxyRect.scaleX(),
scaleY: this.konva.proxyRect.scaleY(),
rotation: this.konva.proxyRect.rotation(),
});
};
/**
* Updates the transformer's visual components to match the parent entity's position and bounding box.
* @param position The position of the parent entity
@ -510,6 +551,12 @@ export class CanvasEntityTransformer extends CanvasModuleABC {
this.stopTransform();
};
resetTransform = () => {
this.resetScale();
this.updatePosition();
this.updateBbox();
};
/**
* Stops the transformation of the entity. If the transformation is in progress, the entity will be reset to its
* original state.
@ -520,12 +567,9 @@ export class CanvasEntityTransformer extends CanvasModuleABC {
this.isTransforming = false;
this.setInteractionMode('off');
// Reset the scale of the the entity. We've either replaced the transformed objects with a rasterized image, or
// Reset the transform of the the entity. We've either replaced the transformed objects with a rasterized image, or
// canceled a transformation. In either case, the scale should be reset.
this.resetScale();
this.updatePosition();
this.updateBbox();
this.resetTransform();
this.syncInteractionState();
this.manager.stateApi.$transformingEntity.set(null);
this.$isProcessing.set(false);