627 lines
15 KiB
TypeScript
627 lines
15 KiB
TypeScript
/**
|
|
* 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
|
|
});
|
|
});
|