This commit is contained in:
Danilo Reyes
2025-11-02 14:13:56 -06:00
parent cd8ce33f5e
commit ce0b692aee
23 changed files with 2049 additions and 50 deletions

View File

@@ -624,4 +624,3 @@ describe('Integration Tests', () => {
expect(state.zoom).toBe(1.5);
});
});

View File

@@ -7,7 +7,12 @@ import { describe, it, expect, beforeEach, vi } from 'vitest';
import Konva from 'konva';
import { get } from 'svelte/store';
import { selection } from '$lib/stores/selection';
import { setupImageDrag, moveImageTo, moveImageBy, isDragging } from '$lib/canvas/interactions/drag';
import {
setupImageDrag,
moveImageTo,
moveImageBy,
isDragging,
} from '$lib/canvas/interactions/drag';
describe('Image Dragging', () => {
let stage: Konva.Stage;
@@ -33,8 +38,9 @@ describe('Image Dragging', () => {
// Create test image
const imageElement = new Image();
imageElement.src = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==';
imageElement.src =
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==';
image = new Konva.Image({
image: imageElement,
x: 100,
@@ -300,4 +306,3 @@ describe('Image Dragging', () => {
});
});
});

View File

@@ -44,8 +44,9 @@ describe('Image Selection', () => {
// Create test image
const imageElement = new Image();
imageElement.src = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==';
imageElement.src =
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==';
image = new Konva.Image({
image: imageElement,
x: 100,
@@ -79,7 +80,7 @@ describe('Image Selection', () => {
it('cleanup function removes click handlers', () => {
const cleanup = setupImageSelection(image, imageId);
// Select the image
image.fire('click', { evt: { ctrlKey: false, metaKey: false } });
expect(get(selection).selectedIds.has(imageId)).toBe(true);
@@ -420,4 +421,3 @@ describe('Image Selection', () => {
});
});
});

View File

@@ -0,0 +1,626 @@
/**
* Tests for canvas image transformations
* Tests rotate, scale, flip, crop, opacity, greyscale, and reset
*/
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import Konva from 'konva';
import {
rotateImageTo,
rotateImageBy,
rotateImage90CW,
rotateImage90CCW,
rotateImage180,
resetImageRotation,
getImageRotation,
} from '$lib/canvas/transforms/rotate';
import {
scaleImageTo,
scaleImageBy,
resetImageScale,
doubleImageSize,
halfImageSize,
getImageScale,
isAtMinScale,
isAtMaxScale,
} from '$lib/canvas/transforms/scale';
import {
flipImageHorizontal,
flipImageVertical,
isFlippedHorizontal,
isFlippedVertical,
resetFlipHorizontal,
resetFlipVertical,
resetAllFlips,
setFlipState,
} from '$lib/canvas/transforms/flip';
import {
setImageOpacity,
setImageOpacityPercent,
increaseOpacity,
decreaseOpacity,
resetImageOpacity,
getImageOpacity,
getImageOpacityPercent,
isFullyOpaque,
isFullyTransparent,
} from '$lib/canvas/transforms/opacity';
import {
applyGreyscale,
removeGreyscale,
toggleGreyscale,
isGreyscaleApplied,
setGreyscale,
} from '$lib/canvas/transforms/greyscale';
import {
cropImage,
removeCrop,
getCropRegion,
isCropped,
cropToSquare,
} from '$lib/canvas/transforms/crop';
import {
resetAllTransformations,
resetGeometricTransformations,
resetVisualTransformations,
hasTransformations,
getTransformationSummary,
} from '$lib/canvas/transforms/reset';
describe('Image Rotation', () => {
let image: Konva.Image;
beforeEach(() => {
const imageElement = new Image();
imageElement.src =
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==';
image = new Konva.Image({
image: imageElement,
width: 200,
height: 200,
});
});
it('rotates to specific angle', () => {
rotateImageTo(image, 45);
expect(getImageRotation(image)).toBe(45);
});
it('normalizes rotation to 0-360', () => {
rotateImageTo(image, 450);
expect(getImageRotation(image)).toBe(90);
rotateImageTo(image, -90);
expect(getImageRotation(image)).toBe(270);
});
it('rotates by delta', () => {
rotateImageTo(image, 30);
rotateImageBy(image, 15);
expect(getImageRotation(image)).toBe(45);
});
it('rotates 90° clockwise', () => {
rotateImage90CW(image);
expect(getImageRotation(image)).toBe(90);
});
it('rotates 90° counter-clockwise', () => {
rotateImageTo(image, 90);
rotateImage90CCW(image);
expect(getImageRotation(image)).toBe(0);
});
it('rotates to 180°', () => {
rotateImage180(image);
expect(getImageRotation(image)).toBe(180);
});
it('resets rotation to 0', () => {
rotateImageTo(image, 135);
resetImageRotation(image);
expect(getImageRotation(image)).toBe(0);
});
});
describe('Image Scaling', () => {
let image: Konva.Image;
beforeEach(() => {
const imageElement = new Image();
imageElement.src =
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==';
image = new Konva.Image({
image: imageElement,
width: 200,
height: 200,
});
});
it('scales to specific factor', () => {
scaleImageTo(image, 2.0);
expect(getImageScale(image)).toBe(2.0);
});
it('clamps scale to minimum', () => {
scaleImageTo(image, 0.001);
expect(getImageScale(image)).toBe(0.01);
});
it('clamps scale to maximum', () => {
scaleImageTo(image, 15.0);
expect(getImageScale(image)).toBe(10.0);
});
it('scales by factor', () => {
scaleImageTo(image, 2.0);
scaleImageBy(image, 1.5);
expect(getImageScale(image)).toBe(3.0);
});
it('doubles image size', () => {
scaleImageTo(image, 1.0);
doubleImageSize(image);
expect(getImageScale(image)).toBe(2.0);
});
it('halves image size', () => {
scaleImageTo(image, 2.0);
halfImageSize(image);
expect(getImageScale(image)).toBe(1.0);
});
it('resets scale to 1.0', () => {
scaleImageTo(image, 3.5);
resetImageScale(image);
expect(getImageScale(image)).toBe(1.0);
});
it('detects minimum scale', () => {
scaleImageTo(image, 0.01);
expect(isAtMinScale(image)).toBe(true);
scaleImageTo(image, 1.0);
expect(isAtMinScale(image)).toBe(false);
});
it('detects maximum scale', () => {
scaleImageTo(image, 10.0);
expect(isAtMaxScale(image)).toBe(true);
scaleImageTo(image, 1.0);
expect(isAtMaxScale(image)).toBe(false);
});
});
describe('Image Flipping', () => {
let image: Konva.Image;
beforeEach(() => {
const imageElement = new Image();
imageElement.src =
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==';
image = new Konva.Image({
image: imageElement,
width: 200,
height: 200,
});
});
it('flips horizontally', () => {
flipImageHorizontal(image);
expect(isFlippedHorizontal(image)).toBe(true);
expect(image.scaleX()).toBeLessThan(0);
});
it('flips vertically', () => {
flipImageVertical(image);
expect(isFlippedVertical(image)).toBe(true);
expect(image.scaleY()).toBeLessThan(0);
});
it('can flip both directions', () => {
flipImageHorizontal(image);
flipImageVertical(image);
expect(isFlippedHorizontal(image)).toBe(true);
expect(isFlippedVertical(image)).toBe(true);
});
it('resets horizontal flip', () => {
flipImageHorizontal(image);
resetFlipHorizontal(image);
expect(isFlippedHorizontal(image)).toBe(false);
expect(image.scaleX()).toBeGreaterThan(0);
});
it('resets vertical flip', () => {
flipImageVertical(image);
resetFlipVertical(image);
expect(isFlippedVertical(image)).toBe(false);
expect(image.scaleY()).toBeGreaterThan(0);
});
it('resets all flips', () => {
flipImageHorizontal(image);
flipImageVertical(image);
resetAllFlips(image);
expect(isFlippedHorizontal(image)).toBe(false);
expect(isFlippedVertical(image)).toBe(false);
});
it('sets flip state explicitly', () => {
setFlipState(image, true, false);
expect(isFlippedHorizontal(image)).toBe(true);
expect(isFlippedVertical(image)).toBe(false);
setFlipState(image, false, true);
expect(isFlippedHorizontal(image)).toBe(false);
expect(isFlippedVertical(image)).toBe(true);
});
it('preserves scale when flipping', () => {
image.scale({ x: 2.0, y: 2.0 });
flipImageHorizontal(image);
expect(Math.abs(image.scaleX())).toBe(2.0);
expect(image.scaleY()).toBe(2.0);
});
});
describe('Image Opacity', () => {
let image: Konva.Image;
beforeEach(() => {
const imageElement = new Image();
imageElement.src =
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==';
image = new Konva.Image({
image: imageElement,
width: 200,
height: 200,
});
});
it('sets opacity to specific value', () => {
setImageOpacity(image, 0.5);
expect(getImageOpacity(image)).toBe(0.5);
});
it('clamps opacity to 0-1 range', () => {
setImageOpacity(image, 1.5);
expect(getImageOpacity(image)).toBe(1.0);
setImageOpacity(image, -0.5);
expect(getImageOpacity(image)).toBe(0.0);
});
it('sets opacity by percentage', () => {
setImageOpacityPercent(image, 75);
expect(getImageOpacity(image)).toBe(0.75);
expect(getImageOpacityPercent(image)).toBe(75);
});
it('increases opacity', () => {
setImageOpacity(image, 0.5);
increaseOpacity(image, 0.2);
expect(getImageOpacity(image)).toBeCloseTo(0.7);
});
it('decreases opacity', () => {
setImageOpacity(image, 0.8);
decreaseOpacity(image, 0.3);
expect(getImageOpacity(image)).toBeCloseTo(0.5);
});
it('resets opacity to 1.0', () => {
setImageOpacity(image, 0.3);
resetImageOpacity(image);
expect(getImageOpacity(image)).toBe(1.0);
});
it('detects fully opaque', () => {
setImageOpacity(image, 1.0);
expect(isFullyOpaque(image)).toBe(true);
setImageOpacity(image, 0.99);
expect(isFullyOpaque(image)).toBe(false);
});
it('detects fully transparent', () => {
setImageOpacity(image, 0.0);
expect(isFullyTransparent(image)).toBe(true);
setImageOpacity(image, 0.01);
expect(isFullyTransparent(image)).toBe(false);
});
});
describe('Image Greyscale', () => {
let image: Konva.Image;
let layer: Konva.Layer;
beforeEach(() => {
const container = document.createElement('div');
document.body.appendChild(container);
const stage = new Konva.Stage({
container,
width: 800,
height: 600,
});
layer = new Konva.Layer();
stage.add(layer);
const imageElement = new Image();
imageElement.src =
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==';
image = new Konva.Image({
image: imageElement,
width: 200,
height: 200,
});
layer.add(image);
});
afterEach(() => {
document.body.innerHTML = '';
});
it('applies greyscale filter', () => {
applyGreyscale(image);
expect(isGreyscaleApplied(image)).toBe(true);
});
it('removes greyscale filter', () => {
applyGreyscale(image);
removeGreyscale(image);
expect(isGreyscaleApplied(image)).toBe(false);
});
it('toggles greyscale on and off', () => {
toggleGreyscale(image);
expect(isGreyscaleApplied(image)).toBe(true);
toggleGreyscale(image);
expect(isGreyscaleApplied(image)).toBe(false);
});
it('sets greyscale state explicitly', () => {
setGreyscale(image, true);
expect(isGreyscaleApplied(image)).toBe(true);
setGreyscale(image, false);
expect(isGreyscaleApplied(image)).toBe(false);
});
it('does not apply duplicate filter', () => {
applyGreyscale(image);
const filters1 = image.filters() || [];
applyGreyscale(image);
const filters2 = image.filters() || [];
expect(filters1.length).toBe(filters2.length);
});
});
describe('Image Cropping', () => {
let image: Konva.Image;
beforeEach(() => {
const imageElement = new Image();
imageElement.src =
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==';
image = new Konva.Image({
image: imageElement,
width: 200,
height: 200,
});
});
it('applies crop region', () => {
const cropRegion = { x: 10, y: 10, width: 100, height: 100 };
cropImage(image, cropRegion);
expect(isCropped(image)).toBe(true);
const crop = getCropRegion(image);
expect(crop).toEqual(cropRegion);
});
it('removes crop', () => {
const cropRegion = { x: 10, y: 10, width: 100, height: 100 };
cropImage(image, cropRegion);
removeCrop(image);
expect(isCropped(image)).toBe(false);
expect(getCropRegion(image)).toBeNull();
});
it('validates crop region bounds', () => {
// Try to crop outside image bounds
const invalidCrop = { x: 150, y: 150, width: 200, height: 200 };
cropImage(image, invalidCrop);
const crop = getCropRegion(image);
expect(crop).not.toBeNull();
// Crop should be adjusted to fit within image
if (crop) {
expect(crop.x).toBeLessThanOrEqual(200);
expect(crop.y).toBeLessThanOrEqual(200);
}
});
it('crops to square', () => {
cropToSquare(image);
const crop = getCropRegion(image);
expect(crop).not.toBeNull();
if (crop) {
expect(crop.width).toBe(crop.height);
}
});
});
describe('Reset Transformations', () => {
let image: Konva.Image;
let layer: Konva.Layer;
beforeEach(() => {
const container = document.createElement('div');
document.body.appendChild(container);
const stage = new Konva.Stage({
container,
width: 800,
height: 600,
});
layer = new Konva.Layer();
stage.add(layer);
const imageElement = new Image();
imageElement.src =
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==';
image = new Konva.Image({
image: imageElement,
width: 200,
height: 200,
});
layer.add(image);
});
afterEach(() => {
document.body.innerHTML = '';
});
it('detects when transformations are applied', () => {
expect(hasTransformations(image)).toBe(false);
rotateImageTo(image, 45);
expect(hasTransformations(image)).toBe(true);
});
it('resets all transformations', () => {
rotateImageTo(image, 90);
scaleImageTo(image, 2.0);
setImageOpacity(image, 0.5);
flipImageHorizontal(image);
applyGreyscale(image);
resetAllTransformations(image);
expect(getImageRotation(image)).toBe(0);
expect(getImageScale(image)).toBe(1.0);
expect(getImageOpacity(image)).toBe(1.0);
expect(isFlippedHorizontal(image)).toBe(false);
expect(isGreyscaleApplied(image)).toBe(false);
});
it('resets only geometric transformations', () => {
rotateImageTo(image, 90);
scaleImageTo(image, 2.0);
setImageOpacity(image, 0.5);
resetGeometricTransformations(image);
expect(getImageRotation(image)).toBe(0);
expect(getImageScale(image)).toBe(1.0);
expect(getImageOpacity(image)).toBe(0.5); // Unchanged
});
it('resets only visual transformations', () => {
rotateImageTo(image, 90);
setImageOpacity(image, 0.5);
applyGreyscale(image);
resetVisualTransformations(image);
expect(getImageRotation(image)).toBe(90); // Unchanged
expect(getImageOpacity(image)).toBe(1.0);
expect(isGreyscaleApplied(image)).toBe(false);
});
it('gets transformation summary', () => {
rotateImageTo(image, 45);
scaleImageTo(image, 1.5);
setImageOpacity(image, 0.8);
flipImageHorizontal(image);
applyGreyscale(image);
const summary = getTransformationSummary(image);
expect(summary.rotation).toBe(45);
expect(summary.scale).toBe(1.5);
expect(summary.opacity).toBe(0.8);
expect(summary.flippedH).toBe(true);
expect(summary.flippedV).toBe(false);
expect(summary.greyscale).toBe(true);
});
});
describe('Combined Transformations', () => {
let image: Konva.Image;
beforeEach(() => {
const imageElement = new Image();
imageElement.src =
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==';
image = new Konva.Image({
image: imageElement,
width: 200,
height: 200,
});
});
it('applies multiple transformations independently', () => {
rotateImageTo(image, 45);
scaleImageTo(image, 2.0);
setImageOpacity(image, 0.7);
flipImageHorizontal(image);
expect(getImageRotation(image)).toBe(45);
expect(Math.abs(getImageScale(image))).toBe(2.0);
expect(getImageOpacity(image)).toBe(0.7);
expect(isFlippedHorizontal(image)).toBe(true);
});
it('transformations do not interfere with each other', () => {
scaleImageTo(image, 3.0);
const scale1 = getImageScale(image);
rotateImageTo(image, 90);
const scale2 = getImageScale(image);
expect(scale1).toBe(scale2); // Rotation doesn't affect scale
});
it('can undo transformations individually', () => {
rotateImageTo(image, 90);
scaleImageTo(image, 2.0);
setImageOpacity(image, 0.5);
resetImageRotation(image);
expect(getImageRotation(image)).toBe(0);
expect(getImageScale(image)).toBe(2.0); // Unchanged
expect(getImageOpacity(image)).toBe(0.5); // Unchanged
});
});