feat(Segmentation): Segmentation highlight animation function selection (#5401)

This commit is contained in:
Vinícius Alves de Faria Resende 2025-09-21 23:48:35 -03:00 committed by GitHub
parent de201d567f
commit 69dbe27784
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 734 additions and 67 deletions

View File

@ -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",

View File

@ -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
);
},
/**

View File

@ -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),
}
);

View 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);
});
});
});

View File

@ -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],
]);

View File

@ -346,7 +346,7 @@ jumpToSegmentCenter(
highlightSegment = true,
animationLength = 750,
highlightHideOthers = false,
highlightFunctionType = 'ease-in-out'
animationFunctionType = 'ease-in-out'
)
```

View File

@ -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'
},
},
},
],
};
`,
},

View File

@ -346,7 +346,7 @@ jumpToSegmentCenter(
highlightSegment = true,
animationLength = 750,
highlightHideOthers = false,
highlightFunctionType = 'ease-in-out'
animationFunctionType = 'ease-in-out'
)
```