ohif-viewer/extensions/cornerstone/src/utils/transitions.test.ts
2025-09-21 22:48:35 -04:00

368 lines
12 KiB
TypeScript

import { linear, ease, easeIn, easeOut, easeInOut, cubicBezier } from './transitions';
describe('Transitions Module', () => {
const EPSILON = 1e-6;
/**
* Helper function to check if two numbers are approximately equal
*/
const approxEqual = (actual: number, expected: number, tolerance = EPSILON): boolean => {
return Math.abs(actual - expected) < tolerance;
};
describe('Standard CSS Easing (Mode 1: no baseline)', () => {
describe('linear', () => {
it('should return 0 at timeProgress=0', () => {
expect(linear(0)).toBe(0);
});
it('should return 1 at timeProgress=1', () => {
expect(linear(1)).toBe(1);
});
it('should return timeProgress for linear progression', () => {
expect(linear(0.25)).toBe(0.25);
expect(linear(0.5)).toBe(0.5);
expect(linear(0.75)).toBe(0.75);
});
});
describe('easeInOut', () => {
it('should return 0 at timeProgress=0', () => {
expect(easeInOut(0)).toBe(0);
});
it('should return 1 at timeProgress=1', () => {
expect(easeInOut(1)).toBe(1);
});
it('should return 0.5 at timeProgress=0.5', () => {
const result = easeInOut(0.5);
expect(approxEqual(result, 0.5)).toBe(true);
});
it('should be slower at start than linear', () => {
const easedValue = easeInOut(0.2);
expect(easedValue).toBeLessThan(0.2);
});
it('should provide smoothing toward the end', () => {
const baseTime = 0.9;
const controlTimeProgressionDifference = 0.2;
const result1 = easeInOut(baseTime);
const result2 = easeInOut(baseTime - controlTimeProgressionDifference);
expect(result1 - result2).toBeLessThan(controlTimeProgressionDifference);
});
});
describe('easeIn', () => {
it('should return 0 at timeProgress=0', () => {
expect(easeIn(0)).toBe(0);
});
it('should return 1 at timeProgress=1', () => {
expect(easeIn(1)).toBe(1);
});
it('should be slower at start', () => {
const easedValue = easeIn(0.4);
expect(easedValue).toBeLessThan(0.4);
});
it('should accelerate toward the end', () => {
const baseTime = 0.9;
const controlTimeProgressionDifference = 0.2;
const result1 = easeIn(baseTime);
const result2 = easeIn(baseTime - controlTimeProgressionDifference);
expect(result1 - result2).toBeGreaterThan(controlTimeProgressionDifference);
});
});
describe('easeOut', () => {
it('should return 0 at timeProgress=0', () => {
expect(easeOut(0)).toBe(0);
});
it('should return 1 at timeProgress=1', () => {
expect(easeOut(1)).toBe(1);
});
it('should be faster at start', () => {
const easedValue = easeOut(0.4);
expect(easedValue).toBeGreaterThan(0.4);
});
it('should provide smoothing toward the end', () => {
const baseTime = 0.9;
const controlTimeProgressionDifference = 0.2;
const result1 = ease(baseTime);
const result2 = ease(baseTime - controlTimeProgressionDifference);
expect(result1 - result2).toBeLessThan(controlTimeProgressionDifference);
});
});
describe('ease', () => {
it('should return 0 at timeProgress=0', () => {
expect(ease(0)).toBe(0);
});
it('should return 1 at timeProgress=1', () => {
expect(ease(1)).toBe(1);
});
it('should provide significant growth at first half', () => {
const result = ease(0.5);
expect(result).toBeGreaterThan(0.75);
});
it('should provide smoothing toward the end', () => {
const baseTime = 0.9;
const controlTimeProgressionDifference = 0.2;
const result1 = ease(baseTime);
const result2 = ease(baseTime - controlTimeProgressionDifference);
expect(result1 - result2).toBeLessThan(controlTimeProgressionDifference);
});
});
});
describe('Baseline Bell-Curve (Mode 2: baseline only)', () => {
const baseline = 0.2;
describe('linear with baseline', () => {
it('should start and end at baseline', () => {
expect(linear(0, baseline)).toBe(baseline);
expect(linear(1, baseline)).toBe(baseline);
});
it('should peak at 1.0 at timeProgress=0.5', () => {
const result = linear(0.5, baseline);
expect(approxEqual(result, 1.0)).toBe(true);
});
it('should create symmetric bell curve', () => {
const quarterValue = linear(0.25, baseline);
const threeQuarterValue = linear(0.75, baseline);
expect(approxEqual(quarterValue, threeQuarterValue)).toBe(true);
});
it('should be between baseline and 1.0', () => {
for (let t = 0; t <= 1; t += 0.1) {
const result = linear(t, baseline);
expect(result).toBeGreaterThanOrEqual(baseline);
expect(result).toBeLessThanOrEqual(1.0);
}
});
});
describe('easeInOut with baseline', () => {
it('should start and end at baseline', () => {
expect(easeInOut(0, baseline)).toBe(baseline);
expect(easeInOut(1, baseline)).toBe(baseline);
});
it('should peak near 1.0 around timeProgress=0.5', () => {
const result = easeInOut(0.5, baseline);
expect(result).toBeGreaterThan(0.8);
expect(result).toBeLessThanOrEqual(1.0);
});
it('should create bell curve with easing', () => {
const quarterValue = easeInOut(0.25, baseline);
const threeQuarterValue = easeInOut(0.75, baseline);
expect(approxEqual(quarterValue, threeQuarterValue, 0.01)).toBe(true);
});
});
describe('all functions with baseline', () => {
const testBaseline = 0.3;
it('should all start and end at baseline', () => {
const functions = [linear, ease, easeIn, easeOut, easeInOut];
functions.forEach(fn => {
expect(fn(0, testBaseline)).toBe(testBaseline);
expect(fn(1, testBaseline)).toBe(testBaseline);
});
});
});
});
describe('Scaled Bell-Curve (Mode 3: baseline + scale)', () => {
const baseline = 2.0;
const scale = 3.0;
const expectedPeak = baseline * scale;
describe('linear with baseline and scale', () => {
it('should start and end at baseline', () => {
expect(linear(0, baseline, scale)).toBe(baseline);
expect(linear(1, baseline, scale)).toBe(baseline);
});
it('should peak at baseline*scale at timeProgress=0.5', () => {
const result = linear(0.5, baseline, scale);
expect(approxEqual(result, expectedPeak)).toBe(true);
});
it('should create symmetric scaled bell curve', () => {
const quarterValue = linear(0.25, baseline, scale);
const threeQuarterValue = linear(0.75, baseline, scale);
expect(approxEqual(quarterValue, threeQuarterValue)).toBe(true);
});
});
describe('easeInOut with baseline and scale', () => {
it('should start and end at baseline', () => {
expect(easeInOut(0, baseline, scale)).toBe(baseline);
expect(easeInOut(1, baseline, scale)).toBe(baseline);
});
it('should peak near baseline*scale around timeProgress=0.5', () => {
const result = easeInOut(0.5, baseline, scale);
expect(result).toBeGreaterThan(baseline + (expectedPeak - baseline) * 0.8);
expect(result).toBeLessThanOrEqual(expectedPeak);
});
});
describe('different scale values', () => {
it('should handle scale < 1 (shrinking)', () => {
const shrinkScale = 0.5;
const result = linear(0.5, 1.0, shrinkScale);
expect(approxEqual(result, 0.5)).toBe(true);
});
it('should handle scale > 1 (growing)', () => {
const growScale = 2.5;
const result = linear(0.5, 1.0, growScale);
expect(approxEqual(result, 2.5)).toBe(true);
});
it('should handle scale = 1 (baseline to baseline)', () => {
const result = linear(0.5, 0.5, 1);
expect(approxEqual(result, 0.5)).toBe(true);
});
});
});
describe('Edge Cases', () => {
describe('baseline = 0 behavior', () => {
it('should behave like standard easing when baseline=0', () => {
expect(linear(0.5, 0)).toBe(linear(0.5));
expect(easeInOut(0.5, 0)).toBe(easeInOut(0.5));
});
});
describe('extreme values', () => {
it('should handle negative baseline', () => {
const result = linear(0.5, -1.0, 2.0);
expect(approxEqual(result, -2.0)).toBe(true);
});
it('should handle large baseline values', () => {
const result = linear(0.5, 100, 1.5);
expect(approxEqual(result, 150)).toBe(true);
});
});
describe('timeProgress edge cases', () => {
it('should handle timeProgress < 0', () => {
expect(linear(-0.1)).toBe(0);
expect(easeInOut(-0.1, 0.2)).toBe(0.2);
});
it('should handle timeProgress > 1', () => {
expect(linear(1.1)).toBe(1);
expect(easeInOut(1.1, 0.2)).toBe(0.2);
});
});
});
describe('Mathematical Properties', () => {
describe('bell curve symmetry for symmetric cubic bezier functions', () => {
it('should be symmetric around timeProgress=0.5', () => {
const baseline = 0.1;
const scale = 2.0;
[linear, easeInOut].forEach(fn => {
for (let offset = 0.1; offset <= 0.4; offset += 0.1) {
const leftValue = fn(0.5 - offset, baseline, scale);
const rightValue = fn(0.5 + offset, baseline, scale);
expect(approxEqual(leftValue, rightValue, 0.01)).toBe(true);
}
});
});
});
describe('monotonicity in first half', () => {
it('should be non-decreasing from 0 to 0.5 for functions that grow under linear in first half', () => {
const baseline = 0.2;
[linear, easeIn, easeInOut].forEach(fn => {
let prevValue = fn(0, baseline);
for (let time = 0.1; time <= 0.5; time += 0.1) {
const currentValue = fn(time, baseline);
expect(currentValue).toBeGreaterThanOrEqual(prevValue - EPSILON);
prevValue = currentValue;
}
});
});
});
});
describe('Utility Functions', () => {
describe('cubicBezier', () => {
it('should create functions that return 0 at t=0 and 1 at t=1', () => {
const customEasing = cubicBezier(0.25, 0.1, 0.75, 0.9);
expect(customEasing(0)).toBe(0);
expect(customEasing(1)).toBe(1);
});
it('should throw error for invalid x values', () => {
expect(() => cubicBezier(-0.1, 0, 1, 1)).toThrow();
expect(() => cubicBezier(0, 0, 1.1, 1)).toThrow();
});
it('should fallback to binary search when Newton-Raphson fails due to small derivative', () => {
/**
* Create a cubic-bezier curve that has a very flat section (small derivative)
* This will cause Newton-Raphson to fail and use binary search fallback
* Using control points that create a nearly horizontal tangent
* Obs: Values obtained by imperative testing
*/
const problematicEasing = cubicBezier(0.98888, 0.00001, 0.00001, 0.98888);
/** Test a value in the problematic region where derivative is very small*/
const result = problematicEasing(0.5);
expect(typeof result).toBe('number');
expect(isNaN(result)).toBe(false);
expect(result).toBeGreaterThanOrEqual(0);
expect(result).toBeLessThanOrEqual(1);
});
});
});
describe('Performance Validation', () => {
it('should handle many rapid calls without issues', () => {
const startTime = performance.now();
for (let i = 0; i < 1000; i++) {
const t = i / 1000;
linear(t);
easeInOut(t, 0.1);
ease(t, 0.2, 1.5);
}
const endTime = performance.now();
expect(endTime - startTime).toBeLessThan(10);
});
});
});