368 lines
12 KiB
TypeScript
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);
|
|
});
|
|
});
|
|
});
|