feat(Segmentation): Segmentation highlight animation function selection (#5401)
This commit is contained in:
parent
de201d567f
commit
69dbe27784
@ -31,7 +31,9 @@
|
||||
"dev:cornerstone": "yarn run dev",
|
||||
"build": "cross-env NODE_ENV=production webpack --progress --config .webpack/webpack.prod.js",
|
||||
"build:package-1": "yarn run build",
|
||||
"start": "yarn run dev"
|
||||
"start": "yarn run dev",
|
||||
"test:unit": "jest --watchAll",
|
||||
"test:unit:ci": "jest --ci --runInBand --collectCoverage --passWithNoTests"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@cornerstonejs/codec-charls": "^1.2.3",
|
||||
|
||||
@ -42,6 +42,7 @@ import { getUpdatedViewportsForSegmentation } from './utils/hydrationUtils';
|
||||
import { SegmentationRepresentations } from '@cornerstonejs/tools/enums';
|
||||
import { isMeasurementWithinViewport } from './utils/isMeasurementWithinViewport';
|
||||
import { getCenterExtent } from './utils/getCenterExtent';
|
||||
import { EasingFunctionEnum } from './utils/transitions';
|
||||
|
||||
const { DefaultHistoryMemo } = csUtils.HistoryMemo;
|
||||
const toggleSyncFunctions = {
|
||||
@ -1518,7 +1519,33 @@ function commandsModule({
|
||||
segmentationId
|
||||
);
|
||||
segmentationService.setActiveSegment(segmentationId, segmentIndex);
|
||||
segmentationService.jumpToSegmentCenter(segmentationId, segmentIndex);
|
||||
|
||||
const { highlightAlpha, highlightSegment, animationLength, animationFunctionType } =
|
||||
(customizationService.getCustomization(
|
||||
'panelSegmentation.jumpToSegmentHighlightAnimationConfig'
|
||||
) as Object as {
|
||||
highlightAlpha?: number;
|
||||
highlightSegment?: boolean;
|
||||
animationLength?: number;
|
||||
animationFunctionType?: EasingFunctionEnum;
|
||||
}) ?? {};
|
||||
|
||||
const validAnimationFunctionType = Object.values(EasingFunctionEnum).includes(
|
||||
animationFunctionType
|
||||
)
|
||||
? animationFunctionType
|
||||
: undefined;
|
||||
|
||||
segmentationService.jumpToSegmentCenter(
|
||||
segmentationId,
|
||||
segmentIndex,
|
||||
undefined,
|
||||
highlightAlpha,
|
||||
highlightSegment,
|
||||
animationLength,
|
||||
undefined,
|
||||
validAnimationFunctionType
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
|
||||
@ -16,7 +16,7 @@ import {
|
||||
} from '@cornerstonejs/tools';
|
||||
import { PubSubService, Types as OHIFTypes } from '@ohif/core';
|
||||
import i18n from '@ohif/i18n';
|
||||
import { easeInOutBell, easeInOutBellRelative } from '../../utils/transitions';
|
||||
import { EasingFunctionEnum, EasingFunctionMap } from '../../utils/transitions';
|
||||
import { mapROIContoursToRTStructData } from './RTSTRUCT/mapROIContoursToRTStructData';
|
||||
import { SegmentationRepresentations } from '@cornerstonejs/tools/enums';
|
||||
import { addColorLUT } from '@cornerstonejs/tools/segmentation/addColorLUT';
|
||||
@ -1178,7 +1178,7 @@ class SegmentationService extends PubSubService {
|
||||
highlightSegment = true,
|
||||
animationLength = 750,
|
||||
highlightHideOthers = false,
|
||||
highlightFunctionType = 'ease-in-out' // todo: make animation functions configurable from outside
|
||||
animationFunctionType: EasingFunctionEnum = EasingFunctionEnum.EASE_IN_OUT
|
||||
): void {
|
||||
const center = this._getSegmentCenter(segmentationId, segmentIndex);
|
||||
if (!center) {
|
||||
@ -1204,7 +1204,8 @@ class SegmentationService extends PubSubService {
|
||||
viewportId,
|
||||
highlightAlpha,
|
||||
animationLength,
|
||||
highlightHideOthers
|
||||
highlightHideOthers,
|
||||
animationFunctionType
|
||||
);
|
||||
});
|
||||
}
|
||||
@ -1216,7 +1217,7 @@ class SegmentationService extends PubSubService {
|
||||
alpha = 0.9,
|
||||
animationLength = 750,
|
||||
hideOthers = true,
|
||||
highlightFunctionType = 'ease-in-out'
|
||||
animationFunctionType: EasingFunctionEnum = EasingFunctionEnum.EASE_IN_OUT
|
||||
): void {
|
||||
if (this.highlightIntervalId) {
|
||||
clearInterval(this.highlightIntervalId);
|
||||
@ -1249,7 +1250,8 @@ class SegmentationService extends PubSubService {
|
||||
segments,
|
||||
viewportId,
|
||||
animationLength,
|
||||
representation
|
||||
representation,
|
||||
animationFunctionType
|
||||
);
|
||||
});
|
||||
}
|
||||
@ -1614,7 +1616,8 @@ class SegmentationService extends PubSubService {
|
||||
segments: Segment[],
|
||||
viewportId: string,
|
||||
animationLength: number,
|
||||
representation: cstTypes.SegmentationRepresentation
|
||||
representation: cstTypes.SegmentationRepresentation,
|
||||
animationFunctionType: EasingFunctionEnum
|
||||
) {
|
||||
const { segmentationId } = representation;
|
||||
const newSegmentSpecificConfig = {
|
||||
@ -1648,6 +1651,8 @@ class SegmentationService extends PubSubService {
|
||||
const elapsed = timestamp - startTime;
|
||||
const progress = Math.min(elapsed / animationLength, 1);
|
||||
|
||||
const easingFunction = EasingFunctionMap.get(animationFunctionType);
|
||||
|
||||
cstSegmentation.config.style.setStyle(
|
||||
{
|
||||
segmentationId,
|
||||
@ -1655,7 +1660,7 @@ class SegmentationService extends PubSubService {
|
||||
type: LABELMAP,
|
||||
},
|
||||
{
|
||||
fillAlpha: easeInOutBell(progress, fillAlpha),
|
||||
fillAlpha: easingFunction(progress, fillAlpha),
|
||||
}
|
||||
);
|
||||
|
||||
@ -1683,7 +1688,8 @@ class SegmentationService extends PubSubService {
|
||||
segments: Segment[],
|
||||
viewportId: string,
|
||||
animationLength: number,
|
||||
representation: cstTypes.SegmentationRepresentation
|
||||
representation: cstTypes.SegmentationRepresentation,
|
||||
animationFunctionType: EasingFunctionEnum
|
||||
) {
|
||||
const { segmentationId } = representation;
|
||||
const startTime = performance.now();
|
||||
@ -1693,8 +1699,6 @@ class SegmentationService extends PubSubService {
|
||||
}) as ContourStyle;
|
||||
|
||||
const prevOutlineWidth = prevStyle.outlineWidth;
|
||||
// make this configurable
|
||||
const baseline = Math.max(prevOutlineWidth * 3.5, 5);
|
||||
|
||||
const animate = (currentTime: number) => {
|
||||
const progress = (currentTime - startTime) / animationLength;
|
||||
@ -1703,7 +1707,8 @@ class SegmentationService extends PubSubService {
|
||||
return;
|
||||
}
|
||||
|
||||
const reversedProgress = easeInOutBellRelative(progress, baseline, prevOutlineWidth);
|
||||
const OUTLINE_ANIMATION_SCALE_FACTOR = 5;
|
||||
const easingFunction = EasingFunctionMap.get(animationFunctionType);
|
||||
|
||||
cstSegmentation.config.style.setStyle(
|
||||
{
|
||||
@ -1712,7 +1717,7 @@ class SegmentationService extends PubSubService {
|
||||
type: CONTOUR,
|
||||
},
|
||||
{
|
||||
outlineWidth: reversedProgress,
|
||||
outlineWidth: easingFunction(progress, prevOutlineWidth, OUTLINE_ANIMATION_SCALE_FACTOR),
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
367
extensions/cornerstone/src/utils/transitions.test.ts
Normal file
367
extensions/cornerstone/src/utils/transitions.test.ts
Normal file
@ -0,0 +1,367 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -1,63 +1,300 @@
|
||||
/**
|
||||
* It is a bell curved function that uses ease in out quadratic for css
|
||||
* transition timing function for each side of the curve.
|
||||
*
|
||||
* @param {number} x - The current time, in the range [0, 1].
|
||||
* @param {number} baseline - The baseline value to start from and return to.
|
||||
* @returns the value of the transition at time x.
|
||||
*/
|
||||
export function easeInOutBell(x: number, baseline: number): number {
|
||||
const alpha = 1 - baseline;
|
||||
/** Cubic Bezier Implementation */
|
||||
|
||||
// prettier-ignore
|
||||
if (x < 1 / 4) {
|
||||
return 4 * Math.pow(2 * x, 3) * alpha + baseline;
|
||||
} else if (x < 1 / 2) {
|
||||
return (1 - Math.pow(-4 * x + 2, 3) / 2) * alpha + baseline;
|
||||
} else if (x < 3 / 4) {
|
||||
return (1 - Math.pow(4 * x - 2, 3) / 2) * alpha + baseline;
|
||||
} else {
|
||||
return (- 4 * Math.pow(2 * x - 2, 3)) * alpha + baseline;
|
||||
/**
|
||||
* Finds the parameter t for a given x value on a cubic Bézier curve.
|
||||
* Uses Newton's method first (fast), then falls back to binary search (reliable).
|
||||
* Based on WebKit/Chromium's UnitBezier implementation.
|
||||
*
|
||||
* @param {number} targetX - The x value to find parameter t for
|
||||
* @param {number} x1 - x-coordinate of first control point
|
||||
* @param {number} x2 - x-coordinate of second control point
|
||||
* @returns {number} The parameter t that gives the specified x value
|
||||
*/
|
||||
function solveCubicBezierX(targetX: number, x1: number, x2: number): number {
|
||||
const epsilon = 1e-6;
|
||||
|
||||
// Pre-compute polynomial coefficients for performance
|
||||
const cx = 3.0 * x1;
|
||||
const bx = 3.0 * (x2 - x1) - cx;
|
||||
const ax = 1.0 - cx - bx;
|
||||
|
||||
function sampleCurveX(t: number): number {
|
||||
// Evaluate: ax*t³ + bx*t² + cx*t using Horner's rule
|
||||
return ((ax * t + bx) * t + cx) * t;
|
||||
}
|
||||
|
||||
function sampleCurveDerivativeX(t: number): number {
|
||||
return (3.0 * ax * t + 2.0 * bx) * t + cx;
|
||||
}
|
||||
|
||||
/**
|
||||
* Newton's method - fast convergence with good initial guess
|
||||
* Try this first as it's normally very fast
|
||||
*/
|
||||
function newtonRaphsonMethod(targetX: number): number | null {
|
||||
let t = targetX;
|
||||
|
||||
for (let iteration = 0; iteration < 8; iteration++) {
|
||||
const currentX = sampleCurveX(t);
|
||||
const error = currentX - targetX;
|
||||
|
||||
if (Math.abs(error) < epsilon) {
|
||||
return t;
|
||||
}
|
||||
|
||||
const derivative = sampleCurveDerivativeX(t);
|
||||
|
||||
// Break if derivative is too small (avoid division by zero)
|
||||
if (Math.abs(derivative) < epsilon) {
|
||||
break;
|
||||
}
|
||||
|
||||
// Newton-Raphson step: t_new = t_old - f(t_old) / f'(t_old)
|
||||
t = t - error / derivative;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Binary search fallback - guaranteed to converge
|
||||
* Use this when Newton's method fails
|
||||
*/
|
||||
function binarySearchFallback(targetX: number): number {
|
||||
let lowerBound = 0.0;
|
||||
let upperBound = 1.0;
|
||||
let t = targetX;
|
||||
|
||||
while (lowerBound < upperBound) {
|
||||
const currentX = sampleCurveX(t);
|
||||
|
||||
if (Math.abs(currentX - targetX) < epsilon) {
|
||||
return t;
|
||||
}
|
||||
|
||||
if (targetX > currentX) {
|
||||
lowerBound = t;
|
||||
} else {
|
||||
upperBound = t;
|
||||
}
|
||||
|
||||
t = (upperBound - lowerBound) * 0.5 + lowerBound;
|
||||
}
|
||||
|
||||
return t;
|
||||
}
|
||||
|
||||
const newtonResult = newtonRaphsonMethod(targetX);
|
||||
if (newtonResult !== null) {
|
||||
return newtonResult;
|
||||
}
|
||||
|
||||
return binarySearchFallback(targetX);
|
||||
}
|
||||
|
||||
/**
|
||||
* A reversed bell curved function that starts from 1 and goes to baseline and
|
||||
* come back to 1 again. It uses ease in out quadratic for css transition
|
||||
* timing function for each side of the curve.
|
||||
* Evaluates the Y coordinate of a cubic Bézier curve at parameter t.
|
||||
* Optimized for CSS timing functions where P0=(0,0) and P3=(1,1).
|
||||
* Uses pre-computed polynomial coefficients and Horner's rule for performance.
|
||||
*
|
||||
* @param {number} x - The current time, in the range [0, 1].
|
||||
* @param {number} baseline - The baseline value to start from and return to.
|
||||
* @returns the value of the transition at time x.
|
||||
* @param {number} t - Parameter value along the curve, typically in [0, 1]
|
||||
* @param {number} y1 - y-coordinate of first control point
|
||||
* @param {number} y2 - y-coordinate of second control point
|
||||
* @returns {number} The Y value of the curve at parameter t
|
||||
*/
|
||||
export function reverseEaseInOutBell(x: number, baseline: number): number {
|
||||
const y = easeInOutBell(x, baseline);
|
||||
return -y + 1 + baseline;
|
||||
function sampleCurveY(t: number, y1: number, y2: number): number {
|
||||
// Pre-compute polynomial coefficients for performance
|
||||
const cy = 3.0 * y1;
|
||||
const by = 3.0 * (y2 - y1) - cy;
|
||||
const ay = 1.0 - cy - by;
|
||||
|
||||
// Evaluate: ay*t³ + by*t² + cy*t using Horner's rule
|
||||
return ((ay * t + by) * t + cy) * t;
|
||||
}
|
||||
|
||||
export function easeInOutBellRelative(
|
||||
x: number,
|
||||
baseline: number,
|
||||
prevOutlineWidth: number
|
||||
): number {
|
||||
const range = baseline - prevOutlineWidth;
|
||||
|
||||
if (x < 1 / 4) {
|
||||
return prevOutlineWidth + 4 * Math.pow(2 * x, 3) * range;
|
||||
} else if (x < 1 / 2) {
|
||||
return prevOutlineWidth + (1 - Math.pow(-4 * x + 2, 3) / 2) * range;
|
||||
} else if (x < 3 / 4) {
|
||||
return prevOutlineWidth + (1 - Math.pow(4 * x - 2, 3) / 2) * range;
|
||||
} else {
|
||||
return prevOutlineWidth + -4 * Math.pow(2 * x - 2, 3) * range;
|
||||
/**
|
||||
* Cubic Bézier easing function implementation following CSS specifications.
|
||||
*
|
||||
* A cubic Bézier curve is defined by four points: P0, P1, P2, and P3.
|
||||
* In CSS animations, P0 is fixed at (0, 0) and P3 is fixed at (1, 1).
|
||||
* This function allows you to specify the intermediate control points P1 and P2.
|
||||
*
|
||||
* @param {number} x1 - x-coordinate of the first control point P1 (must be in [0, 1])
|
||||
* @param {number} y1 - y-coordinate of the first control point P1
|
||||
* @param {number} x2 - x-coordinate of the second control point P2 (must be in [0, 1])
|
||||
* @param {number} y2 - y-coordinate of the second control point P2
|
||||
* @returns {function} A function that takes time t ∈ [0, 1] and returns the eased value
|
||||
*/
|
||||
export function cubicBezier(x1: number, y1: number, x2: number, y2: number) {
|
||||
if (x1 < 0 || x1 > 1 || x2 < 0 || x2 > 1) {
|
||||
throw new Error('x1 and x2 must be in the range [0, 1]');
|
||||
}
|
||||
|
||||
return function (timeProgress: number): number {
|
||||
if (timeProgress <= 0) return 0;
|
||||
if (timeProgress >= 1) return 1;
|
||||
|
||||
if (x1 === y1 && x2 === y2) {
|
||||
return timeProgress;
|
||||
}
|
||||
|
||||
const curveParameter = solveCubicBezierX(timeProgress, x1, x2);
|
||||
return sampleCurveY(curveParameter, y1, y2);
|
||||
};
|
||||
}
|
||||
|
||||
export function reverseEaseInOutBellRelative(
|
||||
x: number,
|
||||
baseline: number,
|
||||
prevOutlineWidth: number
|
||||
): number {
|
||||
const y = easeInOutBellRelative(x, baseline, prevOutlineWidth);
|
||||
return y;
|
||||
/** Core Easing Functions */
|
||||
|
||||
/**
|
||||
* Linear easing function - constant speed throughout the animation.
|
||||
* Equivalent to CSS: cubic-bezier(0.0, 0.0, 1.0, 1.0)
|
||||
*
|
||||
* @param {number} timeProgress - The animation progress, in the range [0, 1].
|
||||
* @returns {number} The linear eased value.
|
||||
*/
|
||||
const linearCore = cubicBezier(0.0, 0.0, 1.0, 1.0);
|
||||
|
||||
/**
|
||||
* Ease-in easing function - starts slow and accelerates.
|
||||
* Equivalent to CSS: cubic-bezier(0.42, 0, 1.0, 1.0)
|
||||
*
|
||||
* @param {number} timeProgress - The animation progress, in the range [0, 1].
|
||||
* @returns {number} The eased value.
|
||||
*/
|
||||
const easeInCore = cubicBezier(0.42, 0, 1.0, 1.0);
|
||||
|
||||
/**
|
||||
* Ease-out easing function - starts fast and decelerates.
|
||||
* Equivalent to CSS: cubic-bezier(0, 0, 0.58, 1.0)
|
||||
*
|
||||
* @param {number} timeProgress - The animation progress, in the range [0, 1].
|
||||
* @returns {number} The eased value.
|
||||
*/
|
||||
const easeOutCore = cubicBezier(0, 0, 0.58, 1.0);
|
||||
|
||||
/**
|
||||
* Ease-in-out easing function - starts slow, accelerates, then decelerates.
|
||||
* Equivalent to CSS: cubic-bezier(0.42, 0, 0.58, 1.0)
|
||||
*
|
||||
* @param {number} timeProgress - The animation progress, in the range [0, 1].
|
||||
* @returns {number} The eased value.
|
||||
*/
|
||||
const easeInOutCore = cubicBezier(0.42, 0, 0.58, 1.0);
|
||||
|
||||
/**
|
||||
* Standard ease easing function (CSS default).
|
||||
* Equivalent to CSS: cubic-bezier(0.25, 0.1, 0.25, 1.0)
|
||||
*
|
||||
* @param {number} timeProgress - The animation progress, in the range [0, 1].
|
||||
* @returns {number} The eased value.
|
||||
*/
|
||||
const easeCore = cubicBezier(0.25, 0.1, 0.25, 1.0);
|
||||
|
||||
/** Flexible Easing Function Factory */
|
||||
|
||||
/**
|
||||
* Flexible factory function that creates easing functions with optional baseline and scale support.
|
||||
* Provides bell-curve behavior: baseline → baseline*scale → baseline
|
||||
*
|
||||
* @param {Function} coreEasingFn - The core easing function to wrap
|
||||
* @returns {Function} A function that accepts (timeProgress, baseline?, scale?) and provides flexible behavior
|
||||
*/
|
||||
function flexibleEasingFunctionFactory(coreEasingFn: (timeProgress: number) => number) {
|
||||
return function (timeProgress: number, baseline: number = 0, scale?: number): number {
|
||||
if (baseline === 0) {
|
||||
return coreEasingFn(timeProgress);
|
||||
}
|
||||
|
||||
const easedProgress = coreEasingFn(timeProgress);
|
||||
|
||||
const targetValue = scale ? baseline * scale : 1;
|
||||
const range = targetValue - baseline;
|
||||
|
||||
// Create bell-curve: baseline → targetValue → baseline
|
||||
const bellMultiplier = 1 - Math.abs(2 * easedProgress - 1);
|
||||
return baseline + bellMultiplier * range;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Linear easing function with optional baseline and scale support.
|
||||
* - No params: standard linear progression [0, 1]
|
||||
* - With baseline only: bell-curve baseline → 1 → baseline
|
||||
* - With baseline+scale: bell-curve baseline → baseline*scale → baseline
|
||||
*
|
||||
* @param {number} timeProgress - The animation progress, in the range [0, 1].
|
||||
* @param {number} baseline - Optional baseline value (default: 0). Creates bell-curve effect.
|
||||
* @param {number} scale - Optional scale multiplier (default: 1). Peak value = baseline * scale.
|
||||
* @returns {number} The linear eased value.
|
||||
*/
|
||||
export const linear = flexibleEasingFunctionFactory(linearCore);
|
||||
|
||||
/**
|
||||
* Standard ease easing function with optional baseline and scale support.
|
||||
* - No params: standard ease progression [0, 1]
|
||||
* - With baseline only: bell-curve baseline → 1 → baseline
|
||||
* - With baseline+scale: bell-curve baseline → baseline*scale → baseline
|
||||
*
|
||||
* @param {number} timeProgress - The animation progress, in the range [0, 1].
|
||||
* @param {number} baseline - Optional baseline value (default: 0). Creates bell-curve effect.
|
||||
* @param {number} scale - Optional scale multiplier (default: 1). Peak value = baseline * scale.
|
||||
* @returns {number} The eased value.
|
||||
*/
|
||||
export const ease = flexibleEasingFunctionFactory(easeCore);
|
||||
|
||||
/**
|
||||
* Ease-in easing function with optional baseline and scale support.
|
||||
* - No params: standard ease-in progression [0, 1]
|
||||
* - With baseline only: bell-curve baseline → 1 → baseline
|
||||
* - With baseline+scale: bell-curve baseline → baseline*scale → baseline
|
||||
*
|
||||
* @param {number} timeProgress - The animation progress, in the range [0, 1].
|
||||
* @param {number} baseline - Optional baseline value (default: 0). Creates bell-curve effect.
|
||||
* @param {number} scale - Optional scale multiplier (default: 1). Peak value = baseline * scale.
|
||||
* @returns {number} The eased value.
|
||||
*/
|
||||
export const easeIn = flexibleEasingFunctionFactory(easeInCore);
|
||||
|
||||
/**
|
||||
* Ease-out easing function with optional baseline and scale support.
|
||||
* - No params: standard ease-out progression [0, 1]
|
||||
* - With baseline only: bell-curve baseline → 1 → baseline
|
||||
* - With baseline+scale: bell-curve baseline → baseline*scale → baseline
|
||||
*
|
||||
* @param {number} timeProgress - The animation progress, in the range [0, 1].
|
||||
* @param {number} baseline - Optional baseline value (default: 0). Creates bell-curve effect.
|
||||
* @param {number} scale - Optional scale multiplier (default: 1). Peak value = baseline * scale.
|
||||
* @returns {number} The eased value.
|
||||
*/
|
||||
export const easeOut = flexibleEasingFunctionFactory(easeOutCore);
|
||||
|
||||
/**
|
||||
* Ease-in-out easing function with optional baseline and scale support.
|
||||
* - No params: standard ease-in-out progression [0, 1]
|
||||
* - With baseline only: bell-curve baseline → 1 → baseline
|
||||
* - With baseline+scale: bell-curve baseline → baseline*scale → baseline
|
||||
*
|
||||
* @param {number} timeProgress - The animation progress, in the range [0, 1].
|
||||
* @param {number} baseline - Optional baseline value (default: 0). Creates bell-curve effect.
|
||||
* @param {number} scale - Optional scale multiplier (default: 1). Peak value = baseline * scale.
|
||||
* @returns {number} The eased value.
|
||||
*/
|
||||
export const easeInOut = flexibleEasingFunctionFactory(easeInOutCore);
|
||||
|
||||
/** Export interfaces */
|
||||
|
||||
export enum EasingFunctionEnum {
|
||||
EASE = 'ease',
|
||||
EASE_IN = 'ease-in',
|
||||
EASE_OUT = 'ease-out',
|
||||
EASE_IN_OUT = 'ease-in-out',
|
||||
LINEAR = 'linear',
|
||||
}
|
||||
|
||||
export const EasingFunctionMap = new Map([
|
||||
[EasingFunctionEnum.EASE, ease],
|
||||
[EasingFunctionEnum.EASE_IN, easeIn],
|
||||
[EasingFunctionEnum.EASE_OUT, easeOut],
|
||||
[EasingFunctionEnum.EASE_IN_OUT, easeInOut],
|
||||
[EasingFunctionEnum.LINEAR, linear],
|
||||
]);
|
||||
|
||||
@ -346,7 +346,7 @@ jumpToSegmentCenter(
|
||||
highlightSegment = true,
|
||||
animationLength = 750,
|
||||
highlightHideOthers = false,
|
||||
highlightFunctionType = 'ease-in-out'
|
||||
animationFunctionType = 'ease-in-out'
|
||||
)
|
||||
```
|
||||
|
||||
|
||||
@ -1313,7 +1313,7 @@ window.config = {
|
||||
};
|
||||
`,
|
||||
},
|
||||
{
|
||||
{
|
||||
id: 'panelSegmentation.disableUpdateSegmentationStats',
|
||||
description: 'Disables the automatic update of segmentation statistics in the panel.',
|
||||
default: false,
|
||||
@ -1328,6 +1328,35 @@ window.config = {
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
`,
|
||||
},
|
||||
{
|
||||
id: 'panelSegmentation.jumpToSegmentHighlightAnimationConfig',
|
||||
description:
|
||||
'Customize the highlight animation when clicking on a segment at the segmentation panel and jumping to it.',
|
||||
default: {
|
||||
highlightAlpha: 0.9,
|
||||
highlightSegment: true,
|
||||
animationLength: 750,
|
||||
animationFunctionType: 'ease-in-out',
|
||||
},
|
||||
image: [],
|
||||
configuration: `
|
||||
window.config = {
|
||||
// rest of window config
|
||||
customizationService: [
|
||||
{
|
||||
'panelSegmentation.jumpToSegmentHighlightAnimationConfig': {
|
||||
$set: {
|
||||
highlightAlpha: 1.0,
|
||||
highlightSegment: true,
|
||||
animationLength: 900,
|
||||
animationFunctionType: 'linear', // one of 'ease-in-out', 'ease-in', 'ease-out', 'ease', 'linear'
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
`,
|
||||
},
|
||||
|
||||
@ -346,7 +346,7 @@ jumpToSegmentCenter(
|
||||
highlightSegment = true,
|
||||
animationLength = 750,
|
||||
highlightHideOthers = false,
|
||||
highlightFunctionType = 'ease-in-out'
|
||||
animationFunctionType = 'ease-in-out'
|
||||
)
|
||||
```
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user