@penner/responsive-easing 0.0.4 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,991 @@
1
+ import { EasingFn } from '@penner/easing';
2
+ import { EasingKit } from '@penner/easing';
3
+ import { NormalizedProgress } from '@penner/smart-primitive';
4
+ import { NormalizedTime } from '@penner/smart-primitive';
5
+ import { NormalizedVelocity } from '@penner/smart-primitive';
6
+ import { PercentProgress } from '@penner/smart-primitive';
7
+ import { SwimConfig } from '@penner/easing';
8
+ import { VelocityFn } from '@penner/easing';
9
+
10
+ /** Discriminated union of all easing definitions */
11
+ export declare type AnyEasingDef = PowerDef | BackDef | PowerBackDef | BezierDef | ExpoDef | SpringDef | BounceDef | SwimDef;
12
+
13
+ declare interface BackDef extends EasingDefBase<'back'>, BackRemParams {
14
+ }
15
+
16
+ export declare class BackRem extends EasingRem<BackRemParams> {
17
+ static readonly OVERSHOOT_KNOB: PercentKnobSpec;
18
+ readonly kind: "back";
19
+ readonly metadata: EasingRemMetadata;
20
+ readonly knobSpecs: readonly KnobSpec[];
21
+ readonly easeInBoundary: VelocityBoundary;
22
+ /** Cached strength parameter derived from overshoot */
23
+ private readonly strength;
24
+ constructor(params: BackRemParams);
25
+ easeIn: (u: NormalizedTime) => NormalizedProgress;
26
+ easeInVelocity: (u: NormalizedTime) => NormalizedVelocity;
27
+ easeOutVelocity: (u: NormalizedTime) => NormalizedVelocity;
28
+ with(overrides: Partial<BackRemParams>): BackRem;
29
+ static create(init?: Partial<BackRemParams>): BackRem;
30
+ }
31
+
32
+ declare interface BackRemParams {
33
+ /** Overshoot depth, normalized 0-1 (e.g., 0.1 = 10% overshoot) */
34
+ readonly overshoot: number;
35
+ }
36
+
37
+ declare interface BaseKnobSpec<T extends KnobPrimitive = number> {
38
+ readonly key: string;
39
+ readonly label: string;
40
+ readonly default: T;
41
+ /** Custom display value formatter for UI */
42
+ readonly displayFormatter?: (value: T) => string;
43
+ /** Whether this knob should be shown as primary (thicker slider, emphasized styling) */
44
+ readonly isPrimary?: boolean;
45
+ }
46
+
47
+ declare interface BezierDef extends EasingDefBase<'bezier'>, BezierRemParams {
48
+ }
49
+
50
+ export declare class BezierRem extends EasingRem<BezierRemParams> {
51
+ static readonly X1_KNOB: NumberKnobSpec;
52
+ static readonly Y1_KNOB: NumberKnobSpec;
53
+ static readonly X2_KNOB: NumberKnobSpec;
54
+ static readonly Y2_KNOB: NumberKnobSpec;
55
+ readonly kind: "bezier";
56
+ readonly metadata: EasingRemMetadata;
57
+ readonly knobSpecs: readonly KnobSpec[];
58
+ /** Both ends depend on control points — always nonzero */
59
+ readonly easeInBoundary: VelocityBoundary;
60
+ private readonly bezier;
61
+ easeIn: (u: NormalizedTime) => NormalizedProgress;
62
+ easeOut: (u: NormalizedTime) => NormalizedProgress;
63
+ constructor(params: BezierRemParams);
64
+ with(overrides: Partial<BezierRemParams>): BezierRem;
65
+ static create(init?: Partial<BezierRemParams>): BezierRem;
66
+ }
67
+
68
+ declare interface BezierRemParams {
69
+ readonly x1: NormalizedTime;
70
+ readonly y1: NormalizedProgress;
71
+ readonly x2: NormalizedTime;
72
+ readonly y2: NormalizedProgress;
73
+ }
74
+
75
+ /** Boolean toggle knob. */
76
+ declare interface BooleanKnobSpec extends BaseKnobSpec<boolean> {
77
+ readonly type: 'boolean';
78
+ }
79
+
80
+ declare interface BounceDef extends EasingDefBase<'bounce'>, BounceRemParams {
81
+ }
82
+
83
+ export declare class BounceRem extends EasingRem<BounceRemParams> {
84
+ static readonly BOUNCES_KNOB: NumberKnobSpec;
85
+ static readonly DECAY_KNOB: PercentKnobSpec;
86
+ readonly kind: "bounce";
87
+ readonly metadata: EasingRemMetadata;
88
+ readonly knobSpecs: readonly KnobSpec[];
89
+ readonly easeInBoundary: VelocityBoundary;
90
+ /** Cached standalone easeOut function built with minimal head defaults */
91
+ private readonly builtP;
92
+ private readonly builtV;
93
+ constructor(params: BounceRemParams);
94
+ easeIn: (u: NormalizedTime) => NormalizedProgress;
95
+ easeInVelocity: (u: NormalizedTime) => number;
96
+ easeOut: (u: NormalizedTime) => NormalizedProgress;
97
+ easeOutVelocity: (u: NormalizedTime) => number;
98
+ /** Access the underlying TailStrategy for full fuse composition */
99
+ static get tailStrategy(): typeof BounceTail;
100
+ with(overrides: Partial<BounceRemParams>): BounceRem;
101
+ static create(init?: Partial<BounceRemParams>): BounceRem;
102
+ }
103
+
104
+ declare interface BounceRemParams {
105
+ /** Number of bounce impacts */
106
+ readonly bounces: number;
107
+ /** Total decay from first to last bounce, normalized 0-1 */
108
+ readonly decay: number;
109
+ }
110
+
111
+ /**
112
+ * Hard-surface bounce pulse tail strategy
113
+ *
114
+ * Creates a bouncing effect where the tail follows parabolic trajectories,
115
+ * simulating gravity-based bouncing off a hard surface. The amplitude
116
+ * decreases by a total percentage over all bounces.
117
+ *
118
+ * This is a pulse tail with net-zero displacement - it oscillates around
119
+ * the target position and settles back to it.
120
+ *
121
+ * Physics model:
122
+ * - Each bounce is a parabola (projectile motion)
123
+ * - Constant downward acceleration throughout
124
+ * - Velocity reversal and reduction at each impact
125
+ * - Total decay applied across all N bounces (not per bounce)
126
+ */
127
+ declare const BounceTail: TailStrategy;
128
+
129
+ /**
130
+ * Calculate head p2 from tail p1 to maintain symmetric smooth node
131
+ *
132
+ * Given the tail's first control point (p1), this calculates where the head's
133
+ * second control point (p2) should be placed to maintain a symmetric smooth Bezier node.
134
+ *
135
+ * Uses simple coordinate symmetry in normalized [0,1] space:
136
+ * - head_p2 = (1 - tail_p1.x, 1 - tail_p1.y)
137
+ * - Control points are collinear (C¹ continuity)
138
+ * - Slider values mirror each other (predictable UI)
139
+ * - Creates "smooth" or "symmetrical" node like in design tools
140
+ *
141
+ * @param tailP1 - Tail's first control point in normalized coordinates [0,1]
142
+ * @param _joinTime - Join point time (not used in calculation, but kept for API consistency)
143
+ * @returns Calculated head p2 position
144
+ *
145
+ * @example
146
+ * ```ts
147
+ * // User drags tail p1 to (0.3, 0.7)
148
+ * const result = calculateHeadP2FromTailP1(
149
+ * { x: 0.3, y: 0.7 },
150
+ * 0.6
151
+ * );
152
+ * // Result: head p2 = (0.7, 0.3) - simple inversion
153
+ * // Update UI: set head p2 sliders to result.point.x, result.point.y
154
+ * ```
155
+ */
156
+ export declare function calculateHeadP2FromTailP1(tailP1: Point2D, _joinTime: number): ConstraintResult;
157
+
158
+ /**
159
+ * Calculate tail p1 from head p2 to maintain symmetric smooth node
160
+ *
161
+ * Given the head's second control point (p2), this calculates where the tail's
162
+ * first control point (p1) should be placed to maintain a symmetric smooth Bezier node.
163
+ *
164
+ * Uses simple coordinate symmetry in normalized [0,1] space:
165
+ * - tail_p1 = (1 - head_p2.x, 1 - head_p2.y)
166
+ * - Control points are collinear (C¹ continuity)
167
+ * - Slider values mirror each other (predictable UI)
168
+ * - Creates "smooth" or "symmetrical" node like in design tools
169
+ *
170
+ * @param headP2 - Head's second control point in normalized coordinates [0,1]
171
+ * @param _joinTime - Join point time (not used in calculation, but kept for API consistency)
172
+ * @returns Calculated tail p1 position
173
+ *
174
+ * @example
175
+ * ```ts
176
+ * // User drags head p2 to (0.8, 0.9)
177
+ * const result = calculateTailP1FromHeadP2(
178
+ * { x: 0.8, y: 0.9 },
179
+ * 0.6
180
+ * );
181
+ * // Result: tail p1 = (0.2, 0.1) - simple inversion
182
+ * // Update UI: set tail p1 sliders to result.point.x, result.point.y
183
+ * ```
184
+ */
185
+ export declare function calculateTailP1FromHeadP2(headP2: Point2D, _joinTime: number): ConstraintResult;
186
+
187
+ /**
188
+ * Clamp Bezier y1 value based on head type and feature flags
189
+ *
190
+ * When combining power-back head with Bezier tail, y1 must be > 0 to prevent
191
+ * negative start slope (which breaks power-back head behavior). However, y1 can
192
+ * exceed 1.0 to allow flexibility in curve design.
193
+ *
194
+ * UI components should use this function to constrain slider/control point ranges.
195
+ *
196
+ * @param y1 - The desired y1 value
197
+ * @param headType - The head type being used
198
+ * @param allowGeneralizedBackHead - Whether to allow y1 <= 0 (advanced feature)
199
+ * @returns The clamped y1 value
200
+ *
201
+ * @example
202
+ * ```ts
203
+ * // In UI code:
204
+ * const clampedY1 = clampBezierY1ForHead(userY1, 'power-back', false);
205
+ * // Use clampedY1 for slider min or control point bounds
206
+ * ```
207
+ */
208
+ export declare function clampBezierY1ForHead(y1: number, headType: EasingKind, allowGeneralizedBackHead?: boolean): number;
209
+
210
+ /**
211
+ * Clamp Bezier y2 value based on tail type and feature flags
212
+ *
213
+ * When combining Bezier head with power-back/back/power tails, y2 must be < 1.0
214
+ * to prevent negative end slope (which breaks tail behavior). However, y2 can be
215
+ * negative to allow flexibility in curve design.
216
+ *
217
+ * Since power-back is the generalized form containing both power and back as
218
+ * special cases, all three tail types use the same clamping logic.
219
+ *
220
+ * UI components should use this function to constrain slider/control point ranges.
221
+ *
222
+ * @param y2 - The desired y2 value
223
+ * @param tailType - The tail type being used
224
+ * @param allowGeneralizedBackTail - Whether to allow y2 >= 1.0 (advanced feature)
225
+ * @returns The clamped y2 value
226
+ *
227
+ * @example
228
+ * ```ts
229
+ * // In UI code:
230
+ * const clampedY2 = clampBezierY2ForTail(userY2, 'power-back', false);
231
+ * // Use clampedY2 for slider max or control point bounds
232
+ * ```
233
+ */
234
+ export declare function clampBezierY2ForTail(y2: number, tailType: EasingKind, allowGeneralizedBackTail?: boolean): number;
235
+
236
+ /**
237
+ * Result from constraint calculation
238
+ */
239
+ declare interface ConstraintResult {
240
+ /** Whether the calculation was successful */
241
+ isValid: boolean;
242
+ /** The calculated control point */
243
+ point?: Point2D;
244
+ /** Optional warning message */
245
+ warning?: string;
246
+ }
247
+
248
+ /**
249
+ * Create a back (overshoot) tail strategy - ease out
250
+ */
251
+ declare function createBackTail(overshoot?: number): RegularTailStrategy;
252
+
253
+ /**
254
+ * Create a Bezier tail strategy with custom control points
255
+ *
256
+ * Uses cubic Bezier curve with locked endpoints.
257
+ * In the tail phase, the curve goes from (joinTime, joinHeight) to (1, 1).
258
+ * The control points (x1, y1) and (x2, y2) are specified in normalized coordinates (0-1)
259
+ * relative to the tail segment.
260
+ *
261
+ * IMPORTANT: For C¹ continuity, the slope from p0 to p1 must match the slope
262
+ * from the head's p2 to p3. This constraint is enforced at the fuse level,
263
+ * not in this factory function.
264
+ *
265
+ * @param x1 - First control point X coordinate (0 to 1, normalized time)
266
+ * @param y1 - First control point Y coordinate (0 to 1, normalized progress)
267
+ * @param x2 - Second control point X coordinate (0 to 1, normalized time)
268
+ * @param y2 - Second control point Y coordinate (0 to 1, normalized progress)
269
+ */
270
+ declare function createBezierTail(x1?: NormalizedTime, y1?: NormalizedProgress, x2?: NormalizedTime, y2?: NormalizedProgress): RegularTailStrategy;
271
+
272
+ /**
273
+ * Create an exponential ease-out tail strategy with a configurable decay parameter.
274
+ *
275
+ * Uses the canonical normalized expo ease-out from @penner/easing.
276
+ * The analytical derivative is computed for C¹ continuity at the fuse join point.
277
+ *
278
+ * @param decay - Fraction of initial velocity lost over the segment, in [0, 1].
279
+ * - 0 = linear (no decay)
280
+ * - 0.95 = typical smooth deceleration (default)
281
+ * - approaching 1 = very steep initial curve
282
+ */
283
+ declare function createExpoTail(decay?: number): RegularTailStrategy;
284
+
285
+ /**
286
+ * Create a power-back ease-out tail strategy
287
+ *
288
+ * Combines power curve (exponent) with back overshoot for ease-out.
289
+ * Uses the unified power-back mathematics to maintain C¹ continuity with head phases.
290
+ *
291
+ * For ease-out, we reverse the ease-in formula:
292
+ * easeOutPowerBack(u) = 1 - easeInPowerBack(1 - u)
293
+ *
294
+ * @param exponent - Power exponent n (must be > 1 for overshoot, default 2)
295
+ * @param overshoot - Overshoot depth O (0-1, default 0.1)
296
+ */
297
+ declare function createPowerBackTail(exponent?: number, overshoot?: number): RegularTailStrategy;
298
+
299
+ /**
300
+ * Create a power ease-out tail strategy with a configurable exponent
301
+ */
302
+ declare function createPowerTail(exponent?: number): RegularTailStrategy;
303
+
304
+ /** Base for all easing definitions — discriminated on `kind` */
305
+ declare interface EasingDefBase<K extends EasingKind> {
306
+ readonly kind: K;
307
+ }
308
+
309
+ export { EasingFn }
310
+
311
+ /**
312
+ * All supported easing kinds — the canonical union.
313
+ * HeadType, RegularTailType, PulseTailType are subsets of this.
314
+ */
315
+ export declare type EasingKind = 'power' | 'back' | 'power-back' | 'bezier' | 'expo' | 'spring' | 'bounce' | 'swim';
316
+
317
+ /** Map from kind to its params type */
318
+ declare interface EasingParamsMap {
319
+ power: Partial<PowerRemParams>;
320
+ back: Partial<BackRemParams>;
321
+ 'power-back': Partial<PowerBackRemParams>;
322
+ bezier: Partial<BezierRemParams>;
323
+ expo: Partial<ExpoRemParams>;
324
+ spring: Partial<SpringRemParams>;
325
+ bounce: Partial<BounceRemParams>;
326
+ swim: Partial<SwimRemParams>;
327
+ }
328
+
329
+ declare abstract class EasingRem<P extends object = Record<string, unknown>> {
330
+ /** Unique identifier for this REM type (e.g., 'power', 'back') */
331
+ abstract readonly kind: string;
332
+ /** Intrinsic metadata: variants, modes */
333
+ abstract readonly metadata: EasingRemMetadata;
334
+ /** Knob specs for dynamic UI generation (practical slider ranges, not math constraints) */
335
+ abstract readonly knobSpecs: readonly KnobSpec[];
336
+ /** Velocity boundary for ease-in variant (can depend on current params) */
337
+ abstract readonly easeInBoundary: VelocityBoundary;
338
+ /** Immutable parameter object */
339
+ readonly params: Readonly<P>;
340
+ constructor(params: P);
341
+ /** Ease-in function */
342
+ abstract easeIn(u: NormalizedTime): NormalizedProgress;
343
+ /** Ease-in velocity — default: lazy numerical derivative of easeIn.
344
+ * Subclasses may override with an analytical derivative. */
345
+ easeInVelocity: VelocityFn;
346
+ /** Ease-out function — default: reverse of easeIn */
347
+ easeOut: (u: NormalizedTime) => NormalizedProgress;
348
+ /** Ease-out velocity — default: lazy numerical derivative of easeOut.
349
+ * Automatically correct even when easeOut is overridden. */
350
+ easeOutVelocity: VelocityFn;
351
+ /** Velocity boundary for ease-out variant — default: reversed easeIn */
352
+ get easeOutBoundary(): VelocityBoundary;
353
+ /** Returns a new REM with patched params (immutable update) */
354
+ abstract with(overrides: Partial<P>): EasingRem<P>;
355
+ }
356
+
357
+ /**
358
+ * Intrinsic REM metadata — objective facts, not derived heuristics.
359
+ * Composition rules (e.g., "which REMs can be heads?") are inferred
360
+ * from these facts by the Fuse logic, not baked into the metadata.
361
+ */
362
+ declare interface EasingRemMetadata {
363
+ /** Which easing variants this REM can produce */
364
+ readonly variants: {
365
+ readonly easeIn: boolean;
366
+ readonly easeOut: boolean;
367
+ };
368
+ /** Which movement modes this REM supports
369
+ * (e.g., Spring supports both: natively pulse, transition via bridge) */
370
+ readonly modes: {
371
+ readonly transition: boolean;
372
+ readonly pulse: boolean;
373
+ };
374
+ }
375
+
376
+ /**
377
+ * Evaluate the unified power-Back ease-in at point u.
378
+ *
379
+ * Unified formula for all n > 1:
380
+ * f_n(u; s) = (s+1)u^n − su^(n-1)
381
+ *
382
+ * Factored form (computational):
383
+ * f_n(u; s) = u^(n-1)[(s+1)u - s]
384
+ *
385
+ * This saves one Math.pow() call and reveals the geometric structure:
386
+ * - Zero crossing at u = s/(s+1)
387
+ * - Amplitude scaling by (s+1)
388
+ * - Power envelope u^(n-1)
389
+ *
390
+ * For s = 0 or n ≤ 1: falls back to pure power u^n
391
+ *
392
+ * @param u - Normalized time parameter [0, 1]
393
+ * @param exponent - Power exponent n
394
+ * @param strength - Strength parameter (from solvePowerBackStrength)
395
+ * @returns Position at time u
396
+ */
397
+ export declare function evalPowerBackIn(u: number, exponent: number, strength: number): number;
398
+
399
+ declare interface ExpoDef extends EasingDefBase<'expo'>, ExpoRemParams {
400
+ }
401
+
402
+ export declare class ExpoRem extends EasingRem<ExpoRemParams> {
403
+ static readonly DECAY_KNOB: PercentKnobSpec;
404
+ readonly kind: "expo";
405
+ readonly metadata: EasingRemMetadata;
406
+ readonly knobSpecs: readonly KnobSpec[];
407
+ readonly easeInBoundary: VelocityBoundary;
408
+ /** Cached ease-out function and derivative constants */
409
+ private readonly easeOutFn;
410
+ private readonly negK;
411
+ private readonly scale;
412
+ private readonly d;
413
+ constructor(params: ExpoRemParams);
414
+ /** easeIn is the reverse of easeOut (not natively supported, metadata.variants.easeIn = false) */
415
+ easeIn: (u: NormalizedTime) => NormalizedProgress;
416
+ easeInVelocity: (u: NormalizedTime) => number;
417
+ easeOut: (u: NormalizedTime) => NormalizedProgress;
418
+ easeOutVelocity: (u: NormalizedTime) => number;
419
+ private easeOutVelocityImpl;
420
+ with(overrides: Partial<ExpoRemParams>): ExpoRem;
421
+ static create(init?: Partial<ExpoRemParams>): ExpoRem;
422
+ }
423
+
424
+ declare interface ExpoRemParams {
425
+ /** Decay fraction (0-1). 0 = linear, 0.95 = typical, approaching 1 = very steep. */
426
+ readonly decay: number;
427
+ }
428
+
429
+ /**
430
+ * Find the exponent that places the local minimum at a target u-coordinate.
431
+ *
432
+ * This is the inverse of getLocalMinimumPosition: given a fixed overshoot
433
+ * and a desired position for the local minimum, find the exponent n that
434
+ * achieves that position.
435
+ *
436
+ * The relationship between u*, n, and x is:
437
+ * u* = (n-1)x/n
438
+ *
439
+ * where x = s/(s+1) depends on both n and overshoot O.
440
+ *
441
+ * Since x depends on n (through the overshoot equation), we use bisection
442
+ * to solve for n numerically.
443
+ *
444
+ * @param overshoot - Fixed overshoot percentage (0-1), e.g., 0.3 for 30%
445
+ * @param targetUMin - Desired u-coordinate of local minimum (0-1)
446
+ * @param minExponent - Minimum allowed exponent (default 1.01)
447
+ * @param maxExponent - Maximum allowed exponent (default 10)
448
+ * @returns Exponent n that places the minimum at targetUMin, or null if impossible
449
+ */
450
+ export declare function findExponentForMinimumPosition(overshoot: number, targetUMin: number, minExponent?: number, maxExponent?: number): number | null;
451
+
452
+ /**
453
+ * Create a fused easing from a serializable FuseConfig.
454
+ *
455
+ * Resolves each EasingDef to an EasingRem, then delegates to composeFuse
456
+ * for composition. Returns an EasingKit with position, velocity, and metadata.
457
+ *
458
+ * @example
459
+ * const result = fuse({
460
+ * head: { kind: 'power', exponent: 2 },
461
+ * tail: { kind: 'back', overshoot: 0.1 },
462
+ * joinTime: 0.5,
463
+ * });
464
+ * const y = result.easingFn(0.5);
465
+ */
466
+ export declare function fuse(config: FuseConfig): EasingKit;
467
+
468
+ /**
469
+ * Serializable fuse configuration — generic over head and tail types.
470
+ *
471
+ * @example
472
+ * const config: FuseConfig = {
473
+ * head: { kind: 'power-back', exponent: 3, overshoot: 0.15 },
474
+ * tail: { kind: 'spring', bounces: 4, decay: 0.95 },
475
+ * joinTime: 0.6,
476
+ * };
477
+ * const result = fuse(config);
478
+ */
479
+ export declare interface FuseConfig<H extends AnyEasingDef = AnyEasingDef, T extends AnyEasingDef = AnyEasingDef> {
480
+ /** @default 'transition' — most common; pulse is the special case */
481
+ movement?: 'transition' | 'pulse';
482
+ head: H;
483
+ tail: T;
484
+ joinTime?: NormalizedTime;
485
+ /** When true, the tail is created by reversing the head easing (easeIn → easeOut),
486
+ * so both phases use the same curve shape. The tail EasingDef is ignored.
487
+ * This produces symmetric "inOut" style curves from a single head definition.
488
+ * Only applies to regular tails (not spring/bounce pulse tails). */
489
+ mirror?: boolean;
490
+ /** Enable bridge for pulse tails. @default true */
491
+ useBridge?: boolean;
492
+ /** Bridge tuning parameters */
493
+ bridgeTuning?: {
494
+ headWeight?: number;
495
+ freqWeight?: number;
496
+ freqExponent?: number;
497
+ baseMultiplier?: number;
498
+ };
499
+ /** Allow generalized back tail when joinHeight is clamped to 1.0 */
500
+ allowGeneralizedBackTail?: boolean;
501
+ /**
502
+ * Maximum velocity at the join point. When the head's natural ending
503
+ * velocity exceeds this, a constant-velocity cruise phase is inserted
504
+ * between the head and tail. The head ends early (when it hits maxSpeed),
505
+ * then cruises at maxSpeed until the join point.
506
+ *
507
+ * Only applies to transition tails (power, back, power-back).
508
+ * When undefined or Infinity, no cruise — current behavior.
509
+ */
510
+ maxSpeed?: number;
511
+ }
512
+
513
+ /**
514
+ * Create an EasingRem instance from a kind string and params.
515
+ *
516
+ * @param kind - The easing type identifier
517
+ * @param params - Optional parameters (uses defaults if omitted)
518
+ * @returns A new EasingRem instance
519
+ *
520
+ * @example
521
+ * const rem = getEasingRem('power', { exponent: 3 });
522
+ * const rem2 = getEasingRem('back'); // uses defaults
523
+ */
524
+ export declare function getEasingRem<K extends EasingKind>(kind: K, params?: EasingParamsMap[K]): EasingRem;
525
+
526
+ /**
527
+ * Compute the u-coordinate of the local minimum (critical point) for power-back ease-in.
528
+ *
529
+ * The critical point occurs where f'(u) = 0:
530
+ * u* = (n-1)s / [n(s+1)] = (n-1)x/n
531
+ *
532
+ * where x = s/(s+1) is the zero-crossing position.
533
+ *
534
+ * This is the position of the "bulge" in the overshoot curve - the deepest
535
+ * point of anticipation before the curve rises toward 1.
536
+ *
537
+ * @param exponent - Power exponent n (must be > 1)
538
+ * @param overshoot - Overshoot percentage (0-1), e.g., 0.3 for 30%
539
+ * @returns u-coordinate of local minimum, or 0 if no overshoot exists
540
+ */
541
+ export declare function getLocalMinimumPosition(exponent: number, overshoot: number): number;
542
+
543
+ /**
544
+ * Compute the canonical grab point on a pure power ease-in curve u^n.
545
+ *
546
+ * This is the point of maximum perpendicular distance from the diagonal (p = u),
547
+ * found by maximizing u^n − u:
548
+ *
549
+ * d/du [u^n − u] = n·u^(n-1) − 1 = 0
550
+ * → u_peak = n^(−1/(n−1))
551
+ * → p_peak = u_peak^n = n^(−n/(n−1))
552
+ *
553
+ * This serves as the natural "visual center" of the curve's bend — the intrinsic
554
+ * analog of the extremum for the zero-overshoot case. Used to position the
555
+ * affordance dot when overshoot/anticipation is 0.
556
+ *
557
+ * For the ease-out mirror: u_tail = 1 − u_peak, p_tail = 1 − p_peak.
558
+ *
559
+ * @param exponent - Power exponent n (> 1 for a meaningful result)
560
+ * @returns { u, p } coordinates of the canonical point on the pure power curve
561
+ */
562
+ export declare function getPowerCurveCanonicalPoint(exponent: number): {
563
+ u: number;
564
+ p: number;
565
+ };
566
+
567
+ /**
568
+ * Get a tail strategy by type
569
+ */
570
+ export declare function getTailStrategy(type: TailType_2, config?: {
571
+ overshoot?: number;
572
+ exponent?: number;
573
+ x1?: number;
574
+ y1?: number;
575
+ x2?: number;
576
+ y2?: number;
577
+ decay?: number;
578
+ }): RegularTailStrategy;
579
+
580
+ /**
581
+ * Predefined head easing types (subset of EasingKind)
582
+ */
583
+ export declare type HeadType = Extract<EasingKind, 'power' | 'back' | 'bezier' | 'spring' | 'power-back' | 'swim'>;
584
+
585
+ declare type KnobPrimitive = boolean | number | string;
586
+
587
+ /** Union of all supported knob flavours. */
588
+ export declare type KnobSpec = NumberKnobSpec | PercentKnobSpec | BooleanKnobSpec | StringKnobSpec;
589
+
590
+ /** Numeric slider knob (min / max / step valid only here). */
591
+ export declare interface NumberKnobSpec extends BaseKnobSpec<number> {
592
+ readonly type: 'number';
593
+ readonly min?: number;
594
+ readonly max?: number;
595
+ readonly step?: number;
596
+ }
597
+
598
+ /** Percent slider knob — domain value 0-1, UI displays 0-100. */
599
+ export declare interface PercentKnobSpec extends BaseKnobSpec<number> {
600
+ readonly type: 'percent';
601
+ readonly min?: number;
602
+ readonly max?: number;
603
+ readonly step?: number;
604
+ }
605
+
606
+ /**
607
+ * Bezier-to-Bezier Join Constraint Utilities
608
+ *
609
+ * When fusing two Bezier curves with the custom join strategy:
610
+ * - joinHeight = joinTime (diagonal constraint)
611
+ * - Symmetric smooth node: control points use simple coordinate inversion in local [0,1] space
612
+ * - tail_p1 = (1 - head_p2.x, 1 - head_p2.y) and vice versa
613
+ * - C¹ continuity via collinear control points (shared slope)
614
+ * - Predictable UI: slider values mirror each other
615
+ *
616
+ * These utilities help UI implementations maintain the constraint bidirectionally:
617
+ * - Moving head p2 automatically calculates tail p1
618
+ * - Moving tail p1 automatically calculates head p2
619
+ */
620
+ /**
621
+ * Control point in 2D space
622
+ */
623
+ declare interface Point2D {
624
+ x: number;
625
+ y: number;
626
+ }
627
+
628
+ declare interface PowerBackDef extends EasingDefBase<'power-back'>, PowerBackRemParams {
629
+ }
630
+
631
+ export declare class PowerBackRem extends EasingRem<PowerBackRemParams> {
632
+ static readonly EXPONENT_KNOB: NumberKnobSpec;
633
+ static readonly OVERSHOOT_KNOB: PercentKnobSpec;
634
+ readonly kind: "power-back";
635
+ readonly metadata: EasingRemMetadata;
636
+ readonly knobSpecs: readonly KnobSpec[];
637
+ /** Cached strength parameter derived from overshoot + exponent */
638
+ private readonly strength;
639
+ constructor(params: PowerBackRemParams);
640
+ get easeInBoundary(): VelocityBoundary;
641
+ easeIn: (u: NormalizedTime) => NormalizedProgress;
642
+ easeInVelocity: (u: NormalizedTime) => number;
643
+ easeOutVelocity: (u: NormalizedTime) => number;
644
+ with(overrides: Partial<PowerBackRemParams>): PowerBackRem;
645
+ static create(init?: Partial<PowerBackRemParams>): PowerBackRem;
646
+ }
647
+
648
+ declare interface PowerBackRemParams {
649
+ /** Power exponent (>= 0.1) */
650
+ readonly exponent: number;
651
+ /** Overshoot depth, normalized 0-1 */
652
+ readonly overshoot: number;
653
+ }
654
+
655
+ /** Per-kind definitions with type-safe params (all normalized 0-1) */
656
+ declare interface PowerDef extends EasingDefBase<'power'>, PowerRemParams {
657
+ }
658
+
659
+ export declare class PowerRem extends EasingRem<PowerRemParams> {
660
+ static readonly EXPONENT_KNOB: NumberKnobSpec;
661
+ readonly kind: "power";
662
+ readonly metadata: EasingRemMetadata;
663
+ readonly knobSpecs: readonly KnobSpec[];
664
+ constructor(params: PowerRemParams);
665
+ get easeInBoundary(): VelocityBoundary;
666
+ easeIn: (u: NormalizedTime) => NormalizedProgress;
667
+ easeInVelocity: (u: NormalizedTime) => number;
668
+ easeOutVelocity: (u: NormalizedTime) => number;
669
+ with(overrides: Partial<PowerRemParams>): PowerRem;
670
+ static create(init?: Partial<PowerRemParams>): PowerRem;
671
+ }
672
+
673
+ declare interface PowerRemParams {
674
+ readonly exponent: number;
675
+ }
676
+
677
+ /**
678
+ * Pulse tail types with net-zero displacement (used by createBounceFuse and createSpringFuse)
679
+ * These tails oscillate around the target position and settle back to it.
680
+ */
681
+ declare type PulseTailType = Extract<EasingKind, 'spring' | 'bounce'>;
682
+
683
+ /**
684
+ * Tail strategy interface for regular (non-pulse) tails
685
+ *
686
+ * Regular tails have net displacement - they move from one position to another.
687
+ * In contrast, pulse tails oscillate around a target and settle back to it (net-zero displacement).
688
+ */
689
+ declare interface RegularTailStrategy {
690
+ /**
691
+ * Unique identifier for this tail strategy
692
+ */
693
+ id: string;
694
+ /**
695
+ * Human-readable label
696
+ */
697
+ label: string;
698
+ /**
699
+ * The easing function (should be an "out" variant)
700
+ */
701
+ easingFn: EasingFn;
702
+ /**
703
+ * Derivative of the easing function
704
+ */
705
+ velocityFn: VelocityFn;
706
+ /**
707
+ * Optional configuration object
708
+ */
709
+ config?: Record<string, unknown>;
710
+ }
711
+
712
+ /**
713
+ * Regular tail types with net displacement (used by createSimpleFuse)
714
+ * These tails move from one position to another position.
715
+ */
716
+ declare type RegularTailType = Extract<EasingKind, 'power' | 'back' | 'power-back' | 'bezier' | 'expo'>;
717
+
718
+ /**
719
+ * Inverse mapping: find parameter s given overshoot and exponent
720
+ *
721
+ * Uses bisection on x = s/(s+1) to solve the inverse problem:
722
+ * given target overshoot O and exponent n, find s such that f_n(u; s)
723
+ * has minimum value −O.
724
+ *
725
+ * Strategy:
726
+ * - Re-parameterize s with x = s/(s+1), so s = x/(1−x), x ∈ [0,1)
727
+ * - For fixed n, overshoot O(x; n) is strictly increasing in x
728
+ * - Use bisection to solve O(x; n) = target
729
+ *
730
+ * @param overshoot - Target overshoot depth O (0 to ~1) as NormalizedProgress
731
+ * @param exponent - Power exponent n (must be > 1 for Back behavior)
732
+ * @returns The strength parameter (≥ 0) for the PowerBack anticipation term
733
+ */
734
+ export declare function solvePowerBackStrength(overshoot: NormalizedProgress, exponent: number): number;
735
+
736
+ declare interface SpringDef extends EasingDefBase<'spring'>, SpringRemParams {
737
+ }
738
+
739
+ export declare class SpringRem extends EasingRem<SpringRemParams> {
740
+ static readonly BOUNCES_KNOB: NumberKnobSpec;
741
+ static readonly DECAY_KNOB: PercentKnobSpec;
742
+ readonly kind: "spring";
743
+ readonly metadata: EasingRemMetadata;
744
+ readonly knobSpecs: readonly KnobSpec[];
745
+ readonly easeInBoundary: VelocityBoundary;
746
+ /** Cached standalone easeOut function built with minimal head defaults */
747
+ private readonly builtP;
748
+ private readonly builtV;
749
+ constructor(params: SpringRemParams);
750
+ /** easeIn is the reverse of easeOut (not natively supported) */
751
+ easeIn: (u: NormalizedTime) => NormalizedProgress;
752
+ easeInVelocity: (u: NormalizedTime) => number;
753
+ easeOut: (u: NormalizedTime) => NormalizedProgress;
754
+ easeOutVelocity: (u: NormalizedTime) => number;
755
+ /** Access the underlying TailStrategy for full fuse composition */
756
+ static get tailStrategy(): typeof SpringZeroLockedTail;
757
+ with(overrides: Partial<SpringRemParams>): SpringRem;
758
+ static create(init?: Partial<SpringRemParams>): SpringRem;
759
+ }
760
+
761
+ declare interface SpringRemParams {
762
+ /** Number of half-cycles in the oscillation */
763
+ readonly bounces: number;
764
+ /** Total decay from first to last peak, normalized 0-1 (e.g., 0.95 = last peak is 5% of first) */
765
+ readonly decay: number;
766
+ }
767
+
768
+ /**
769
+ * Spring phase-locked pulse tail strategy
770
+ *
771
+ * Creates a spring-like oscillation where the tail follows a damped sinusoid
772
+ * that's phase-locked at the START to ensure C¹ continuity with the head.
773
+ * The amplitude decreases by a total percentage over all half-cycles.
774
+ *
775
+ * This is a pulse tail with net-zero displacement - it oscillates around
776
+ * the target position and settles back to it.
777
+ *
778
+ * Physics model:
779
+ * - Damped harmonic oscillator: A*e^(-kt)*sin(Bt)
780
+ * - START-LOCKED: Sine wave always begins at zero-crossing (sin(0) = 0)
781
+ * - Phase-locked so that N half-cycles fit exactly in the remaining time
782
+ * - Total decay applied across all N half-cycles (not per half-cycle)
783
+ *
784
+ * The start-locking ensures that the oscillation begins at exactly zero,
785
+ * maintaining smooth C¹ continuity at the join point even with fractional
786
+ * bounce counts. For integer bounces, the endpoint also lands on a zero-crossing.
787
+ * For fractional bounces, the endpoint may be slightly off zero but is visually
788
+ * acceptable during parameter adjustment (and snaps to integer on release).
789
+ */
790
+ declare const SpringZeroLockedTail: TailStrategy;
791
+
792
+ /** Text/select knob. */
793
+ declare interface StringKnobSpec extends BaseKnobSpec {
794
+ readonly type: 'string';
795
+ readonly options?: readonly string[];
796
+ }
797
+
798
+ declare interface SwimDef extends EasingDefBase<'swim'>, SwimRemParams {
799
+ }
800
+
801
+ export declare class SwimRem extends EasingRem<SwimRemParams> {
802
+ static readonly STROKES_KNOB: NumberKnobSpec;
803
+ static readonly EFFORT_KNOB: PercentKnobSpec;
804
+ static readonly DRAG_KNOB: NumberKnobSpec;
805
+ readonly kind: "swim";
806
+ readonly metadata: EasingRemMetadata;
807
+ readonly knobSpecs: readonly KnobSpec[];
808
+ readonly easeInBoundary: VelocityBoundary;
809
+ constructor(params: SwimRemParams);
810
+ easeIn: (u: NormalizedTime) => NormalizedProgress;
811
+ with(overrides: Partial<SwimRemParams>): SwimRem;
812
+ static create(init?: Partial<SwimRemParams>): SwimRem;
813
+ }
814
+
815
+ /** Derived from SwimConfig — single source of truth for swim parameter names. */
816
+ declare type SwimRemParams = Readonly<Required<SwimConfig>>;
817
+
818
+ /**
819
+ * Parameters for building a tail strategy
820
+ */
821
+ declare interface TailBuildParams {
822
+ /**
823
+ * Join point in normalized time [0,1]
824
+ * This is the fraction of the timeline where head ends and tail begins
825
+ */
826
+ joinTime: NormalizedTime;
827
+ /**
828
+ * Number of bounces in the tail
829
+ */
830
+ bounces: number;
831
+ /**
832
+ * Decay percentage per oscillation (0-100)
833
+ * This represents the TOTAL decay from first to last oscillation.
834
+ * For example, decay=90 means the last oscillation is 10% of the first.
835
+ *
836
+ * For bounce: total decay from first bounce peak to last bounce peak
837
+ * For spring: total decay from first peak to last peak
838
+ *
839
+ * Special case for single oscillation (N=1):
840
+ * - decay=0 means full energy (no reduction)
841
+ * - decay=50 means the single oscillation is 50% smaller
842
+ * - decay=100 means no oscillation (settles immediately)
843
+ */
844
+ decayPct: PercentProgress /**
845
+ * Head easing function (defaults to easeInBack)
846
+ */;
847
+ L?: EasingFn;
848
+ /**
849
+ * Head velocity function (derivative of L, defaults to easeInBack derivative)
850
+ */
851
+ Ld?: VelocityFn;
852
+ /**
853
+ * Bridge mode parameters (optional, only for pulse tails)
854
+ * When provided, a constant-velocity bridge connects the head to the pulse
855
+ */
856
+ bridge?: {
857
+ /**
858
+ * Position where head ends (and bridge begins)
859
+ * This is less than 1, allowing the head to scale more naturally
860
+ */
861
+ joinHeight: NormalizedProgress;
862
+ /**
863
+ * Duration of the bridge phase in normalized time
864
+ * The bridge goes from time `a` to `a + duration`
865
+ */
866
+ duration: number;
867
+ /**
868
+ * Constant velocity of the bridge (matches head ending velocity and pulse starting velocity)
869
+ */
870
+ velocity: number;
871
+ };
872
+ }
873
+
874
+ /**
875
+ * Result from building a tail strategy
876
+ * Contains the easing function, velocity function, and metadata
877
+ */
878
+ declare interface TailBuildResult {
879
+ /**
880
+ * Easing function for the tail segment (normalized time [0,1] → normalized position)
881
+ */
882
+ p: EasingFn;
883
+ /**
884
+ * Velocity function for the tail segment (derivative of position)
885
+ */
886
+ v: VelocityFn;
887
+ /**
888
+ * Metadata about the tail configuration
889
+ */
890
+ meta: {
891
+ /** Restitution coefficient or amplitude decay ratio */
892
+ r: number;
893
+ /** Final amplitude as percentage of initial */
894
+ finalPct: number;
895
+ /** Decay rate constant (spring tail) */
896
+ k?: number;
897
+ /** Angular frequency (spring tail) */
898
+ B?: number;
899
+ /** C¹ scaling factor (spring tail) */
900
+ P?: number;
901
+ /** Guard message indicating any robustness fallbacks applied */
902
+ guard?: string;
903
+ /** Whether bridge mode was used */
904
+ useBridge?: boolean;
905
+ /** Additional metadata specific to the tail type */
906
+ [key: string]: unknown;
907
+ };
908
+ }
909
+
910
+ /**
911
+ * Map of all predefined regular tail strategy factories
912
+ *
913
+ * Regular tails have net displacement - they move from one position to another.
914
+ * These factories create tail strategies with easingFn/velocityFn for use by createSimpleFuse.
915
+ *
916
+ * For pulse tails with net-zero displacement (oscillate and settle), use:
917
+ * - HardBounceTail (bounce) - gravity-based bouncing physics
918
+ * - ElasticZeroLockedTail (spring) - spring-like oscillation
919
+ */
920
+ declare const tailStrategies: {
921
+ readonly back: typeof createBackTail;
922
+ readonly power: typeof createPowerTail;
923
+ readonly 'power-back': typeof createPowerBackTail;
924
+ readonly bezier: typeof createBezierTail;
925
+ readonly expo: typeof createExpoTail;
926
+ };
927
+
928
+ /**
929
+ * Tail strategy interface
930
+ * Implementations define how to build a specific type of tail (bounce, spring, etc.)
931
+ */
932
+ declare interface TailStrategy {
933
+ /**
934
+ * Unique identifier for this tail strategy
935
+ */
936
+ id: string;
937
+ /**
938
+ * Human-readable label
939
+ */
940
+ label: string;
941
+ /**
942
+ * Build the tail easing function from parameters
943
+ */
944
+ build: (params: TailBuildParams) => TailBuildResult;
945
+ /**
946
+ * Get the natural starting velocity for this tail type (optional, for pulse tails)
947
+ *
948
+ * This is used in bridge mode to calculate the appropriate joinHeight (join height)
949
+ * and bridge duration. The velocity is what the pulse would naturally start
950
+ * with given its oscillation and decay parameters.
951
+ *
952
+ * @param params - Parameters for calculating natural velocity (N cycles, decay %)
953
+ * @param headVelocityFn - The head's velocity function for adaptive bridge calculation
954
+ * @param tuning - Optional tuning parameters for bridge velocity calculation (defaults: all 1.0)
955
+ * @returns The natural starting velocity magnitude, or undefined if not applicable
956
+ */
957
+ getNaturalStartVelocity?: (params: {
958
+ bounces: number;
959
+ decayPct: number;
960
+ }, headVelocityFn: (u: number) => number, tuning?: {
961
+ headWeight?: number;
962
+ freqWeight?: number;
963
+ freqExponent?: number;
964
+ baseMultiplier?: number;
965
+ }) => number;
966
+ }
967
+
968
+ /**
969
+ * All predefined tail types
970
+ */
971
+ export declare type TailType = RegularTailType | PulseTailType;
972
+
973
+ /**
974
+ * Regular tail type union (tails with net displacement)
975
+ */
976
+ declare type TailType_2 = keyof typeof tailStrategies;
977
+
978
+ /** Velocity boundary metadata for an easing variant */
979
+ declare interface VelocityBoundary {
980
+ readonly start: VelocityConstraint;
981
+ readonly end: VelocityConstraint;
982
+ }
983
+
984
+ /**
985
+ * Velocity constraint at a boundary.
986
+ * Kept as a union (not boolean) for future extensibility —
987
+ * e.g., Bezier joins may need a 'tangent-coupled' constraint.
988
+ */
989
+ declare type VelocityConstraint = 'zero' | 'nonzero';
990
+
991
+ export { }