@glissade/core 0.1.0 → 0.2.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.
package/dist/index.d.ts CHANGED
@@ -49,12 +49,20 @@ declare function untracked<T>(fn: () => T): T;
49
49
  * (spring overshoot); non-extrapolating types clamp.
50
50
  */
51
51
  type Vec2 = readonly [number, number];
52
+ /** Transition handoff policies (v2 addendum §A.4/§B.1); 'crossfade' reserved. */
53
+ type HandoffKind = 'cut' | 'decay' | 'spring' | 'blend-from-frozen';
52
54
  interface ValueType<T> {
53
55
  id: string;
54
56
  lerp(a: T, b: T, t: number): T;
55
57
  /** Accepts easedT outside [0,1] (spring overshoot)? Otherwise clamped. */
56
58
  extrapolates: boolean;
57
59
  equals(a: T, b: T): boolean;
60
+ /** Optional linear-space operators (offset decay + reserved additive blending, §B.6). */
61
+ add?(a: T, b: T): T;
62
+ sub?(a: T, b: T): T;
63
+ scale?(a: T, k: number): T;
64
+ /** Type-class handoff default (§B.1): spring for kinetic, cut for hold-only. */
65
+ defaultHandoff?: HandoffKind;
58
66
  }
59
67
  type ValueTypeId = 'number' | 'vec2' | 'color' | 'string' | 'boolean' | (string & {});
60
68
  declare function registerValueType<T>(vt: ValueType<T>): void;
@@ -104,6 +112,12 @@ type EaseSpec = string | {
104
112
  mass: number;
105
113
  };
106
114
  declare const easings: Record<string, EasingFn>;
115
+ /**
116
+ * Analytic derivatives d(u) of every named ease (§B.6) — closed-form, used
117
+ * for reading velocity off in-flight curves at interruption time. Property-
118
+ * tested against central differences at interior points.
119
+ */
120
+ declare const easingDerivatives: Record<string, EasingFn>;
107
121
  /** Default property-tween ease (Motion Canvas precedent). */
108
122
  declare const DEFAULT_EASE = "easeInOutCubic";
109
123
  /**
@@ -111,6 +125,8 @@ declare const DEFAULT_EASE = "easeInOutCubic";
111
125
  * with a bisection fallback for the flat-derivative regions.
112
126
  */
113
127
  declare function cubicBezier(p1x: number, p1y: number, p2x: number, p2y: number): EasingFn;
128
+ /** Analytic dy/dx of a cubic bézier ease: y'(s)/x'(s) at the solved parameter (§B.6). */
129
+ declare function cubicBezierDerivative(p1x: number, p1y: number, p2x: number, p2y: number): EasingFn;
114
130
  declare class UnknownEasingError extends Error {
115
131
  constructor(name: string);
116
132
  }
@@ -144,10 +160,24 @@ declare function duration(cfg: SpringConfig, opts?: {
144
160
  declare function value(cfg: SpringConfig, t: number, opts?: {
145
161
  settleTolerance?: number;
146
162
  }): number;
163
+ /**
164
+ * Velocity-matched retarget oscillator (v2 addendum §B.3): the same damped
165
+ * harmonic oscillator on an OFFSET in value units with nonzero initial
166
+ * velocity — y(0)=x0, y'(0)=v0, decaying to 0. No affine rescale (the target
167
+ * is exactly 0). Pure closed forms; seek-safe at any τ.
168
+ */
169
+ interface RetargetSpring {
170
+ value(tau: number): number;
171
+ velocity(tau: number): number;
172
+ /** Earliest τ after which |value| stays within tol (default 0.005·|x0|+1e-6 floor). */
173
+ settleTime(tol?: number): number;
174
+ }
175
+ declare function retarget(cfg: SpringConfig, x0: number, v0: number): RetargetSpring;
147
176
  interface SpringFactory {
148
177
  (cfg: SpringConfig): SpringEase;
149
178
  duration: typeof duration;
150
179
  value: typeof value;
180
+ retarget: typeof retarget;
151
181
  }
152
182
  declare const spring: SpringFactory;
153
183
  /**
@@ -155,6 +185,12 @@ declare const spring: SpringFactory;
155
185
  * spring.duration(cfg) (validated at the document layer, §2.7).
156
186
  */
157
187
  declare function springEasing(cfg: SpringConfig): EasingFn;
188
+ /**
189
+ * Analytic d/dp of springEasing (§B.6): oscillator derivative × the affine
190
+ * rescale factor × duration (chain rule p → t = p·D). Flat past p=1,
191
+ * matching value()'s clamp (right-derivative convention).
192
+ */
193
+ declare function springEasingDerivative(cfg: SpringConfig): EasingFn;
158
194
  //#endregion
159
195
  //#region src/color.d.ts
160
196
  /**
@@ -215,9 +251,29 @@ declare function track<T>(target: string, type: ValueTypeId, keys: Key<T>[], opt
215
251
  editable?: boolean;
216
252
  }): Track<T>;
217
253
  declare function resolveEase(spec: EaseSpec | undefined): EasingFn;
254
+ /**
255
+ * Analytic d(u) for an ease spec (§B.6). Custom-registered eases without a
256
+ * derivative fall back to a symmetric difference with a one-time dev warning.
257
+ */
258
+ declare function resolveEaseDerivative(spec: EaseSpec | undefined): EasingFn;
259
+ /**
260
+ * Analytic track derivative at time t, in value-units per second of local
261
+ * track time (v2 addendum §B.3/§B.6 conventions, pinned):
262
+ * (a) at a key boundary, velocity is the RIGHT derivative;
263
+ * (b) hold segments and the clamped regions outside the keys have v = 0;
264
+ * (c) types without sub/scale operators return null (no kinetic velocity).
265
+ */
266
+ declare function velocityAt<T>(tr: Track<T>, t: number): T | null;
218
267
  /** Pure sample of a track at time t (§2.4). */
219
268
  declare function sampleTrack<T>(tr: Track<T>, t: number): T;
220
269
  //#endregion
270
+ //#region src/devWarning.d.ts
271
+ /** The configurable dev-warning channel (no DOM lib in core; console may not exist). */
272
+ type DevWarning = (message: string) => void;
273
+ declare function setDevWarning(fn: DevWarning): void;
274
+ /** Internal: emit through the configurable channel. */
275
+ declare function emitDevWarning(message: string): void;
276
+ //#endregion
221
277
  //#region src/timeline.d.ts
222
278
  type Json = null | boolean | number | string | Json[] | {
223
279
  [k: string]: Json;
@@ -293,10 +349,6 @@ interface CompiledTimeline {
293
349
  /** Audio clips rebased to the root time axis (§5.3); sync timeScale scales playbackRate. */
294
350
  audio: AudioClip[];
295
351
  }
296
- type DevWarning = (message: string) => void;
297
- declare function setDevWarning(fn: DevWarning): void;
298
- /** Internal: emit through the configurable dev-warning channel. */
299
-
300
352
  declare function compileTimeline(doc: Timeline): CompiledTimeline;
301
353
  //#endregion
302
354
  //#region src/targetRef.d.ts
@@ -365,8 +417,18 @@ interface BindTarget {
365
417
  bindSource(fn: () => unknown): void;
366
418
  unbindSource(): void;
367
419
  }
420
+ /** Analytic value/velocity access to one bound target (v2 addendum §B.6). */
421
+ interface CurveSampler {
422
+ readonly track: Track;
423
+ /** Pure sample at local timeline time t. */
424
+ value(t: number): unknown;
425
+ /** Analytic derivative per §B.3 conventions; null for types without operators. */
426
+ velocity(t: number): unknown | null;
427
+ }
368
428
  interface BoundTimeline {
369
429
  playhead: Playhead;
430
+ /** Per-target analytic samplers (additive, v2 §B.6); machines read these. */
431
+ samplers: ReadonlyMap<string, CurveSampler>;
370
432
  /** Detach every track binding, freezing signals at their last values. */
371
433
  unbind(): void;
372
434
  }
@@ -459,4 +521,4 @@ declare function normalizeEditedKeys(keys: Key[]): Key[];
459
521
  */
460
522
  declare function mergeSidecar(code: Timeline, sidecar: SidecarDoc | null | undefined): Timeline;
461
523
  //#endregion
462
- export { type AssetRef, type AudioClip, type BakeConfig, BakeError, type BindTarget, type BindableSignal, type BoundTimeline, type CheckpointedBakeConfig, type CheckpointedSim, type ChildEntry, CircularDependencyError, ColorParseError, type CompiledTimeline, DEFAULT_EASE, type DevWarning, type EaseSpec, type EasingFn, type Equals, type Json, type Key, type KeyOpts, type Marker, type OkLab, type Playhead, type Position, PositionError, type ReadonlySignal, type Rgba, type Rng, type SidecarDoc, SidecarVersionError, type Signal, type SignalOptions, type SpringConfig, type SpringEase, TARGET_PATH, type TargetCarrier, type Timeline, type TimelineBuilder, type TimelineInit, TimelineValidationError, type Track, TrackValidationError, type TweenOpts, type TweenTarget, UnboundTargetError, UnknownEasingError, UnknownValueTypeError, UnresolvableTargetError, type ValueType, type ValueTypeId, ValueTypeInferenceError, type Vec2, type Vec2Signal, WriteDuringEvaluationError, bake, bakeCheckpointed, beginReadPhase, bindTimeline, booleanType, buildTimeline, colorType, compileTimeline, computed, createPlayhead, cubicBezier, easings, emptySidecar, endReadPhase, evaluateAt, formatColor, getTimelineCallbacks, getValueType, inReadPhase, inferValueType, key, lerpColor, mergeSidecar, namedEasing, normalizeEditedKeys, numberType, oklabToRgba, parseColor, random, registerValueType, resolveEase, resolveTweenTarget, rgbaToOklab, sampleTrack, setDevWarning, signal, spring, springEasing, stringType, timeline, track, untracked, validateTrack, vec2Equals, vec2Signal, vec2Type };
524
+ export { type AssetRef, type AudioClip, type BakeConfig, BakeError, type BindTarget, type BindableSignal, type BoundTimeline, type CheckpointedBakeConfig, type CheckpointedSim, type ChildEntry, CircularDependencyError, ColorParseError, type CompiledTimeline, type CurveSampler, DEFAULT_EASE, type DevWarning, type EaseSpec, type EasingFn, type Equals, type HandoffKind, type Json, type Key, type KeyOpts, type Marker, type OkLab, type Playhead, type Position, PositionError, type ReadonlySignal, type RetargetSpring, type Rgba, type Rng, type SidecarDoc, SidecarVersionError, type Signal, type SignalOptions, type SpringConfig, type SpringEase, TARGET_PATH, type TargetCarrier, type Timeline, type TimelineBuilder, type TimelineInit, TimelineValidationError, type Track, TrackValidationError, type TweenOpts, type TweenTarget, UnboundTargetError, UnknownEasingError, UnknownValueTypeError, UnresolvableTargetError, type ValueType, type ValueTypeId, ValueTypeInferenceError, type Vec2, type Vec2Signal, WriteDuringEvaluationError, bake, bakeCheckpointed, beginReadPhase, bindTimeline, booleanType, buildTimeline, colorType, compileTimeline, computed, createPlayhead, cubicBezier, cubicBezierDerivative, easingDerivatives, easings, emitDevWarning, emptySidecar, endReadPhase, evaluateAt, formatColor, getTimelineCallbacks, getValueType, inReadPhase, inferValueType, key, lerpColor, mergeSidecar, namedEasing, normalizeEditedKeys, numberType, oklabToRgba, parseColor, random, registerValueType, resolveEase, resolveEaseDerivative, resolveTweenTarget, rgbaToOklab, sampleTrack, setDevWarning, signal, spring, springEasing, springEasingDerivative, stringType, timeline, track, untracked, validateTrack, vec2Equals, vec2Signal, vec2Type, velocityAt };
package/dist/index.js CHANGED
@@ -312,20 +312,29 @@ const numberType = {
312
312
  id: "number",
313
313
  lerp: (a, b, t) => a + (b - a) * t,
314
314
  extrapolates: true,
315
- equals: Object.is
315
+ equals: Object.is,
316
+ add: (a, b) => a + b,
317
+ sub: (a, b) => a - b,
318
+ scale: (a, k) => a * k,
319
+ defaultHandoff: "spring"
316
320
  };
317
321
  const vec2Equals = (a, b) => a[0] === b[0] && a[1] === b[1];
318
322
  const vec2Type = {
319
323
  id: "vec2",
320
324
  lerp: (a, b, t) => [a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t],
321
325
  extrapolates: true,
322
- equals: vec2Equals
326
+ equals: vec2Equals,
327
+ add: (a, b) => [a[0] + b[0], a[1] + b[1]],
328
+ sub: (a, b) => [a[0] - b[0], a[1] - b[1]],
329
+ scale: (a, k) => [a[0] * k, a[1] * k],
330
+ defaultHandoff: "spring"
323
331
  };
324
332
  const colorType = {
325
333
  id: "color",
326
334
  lerp: lerpColor,
327
335
  extrapolates: true,
328
- equals: (a, b) => a === b
336
+ equals: (a, b) => a === b,
337
+ defaultHandoff: "blend-from-frozen"
329
338
  };
330
339
  /** Discrete types: hold-only by construction (§2.2); lerp snaps at t=1. */
331
340
  function discrete(id) {
@@ -333,7 +342,8 @@ function discrete(id) {
333
342
  id,
334
343
  lerp: (a, b, t) => t >= 1 ? b : a,
335
344
  extrapolates: false,
336
- equals: (a, b) => Object.is(a, b)
345
+ equals: (a, b) => Object.is(a, b),
346
+ defaultHandoff: "cut"
337
347
  };
338
348
  }
339
349
  const stringType = discrete("string");
@@ -456,13 +466,75 @@ const easings = {
456
466
  easeOutBounce: bounceOut,
457
467
  easeInOutBounce: (t) => t < .5 ? (1 - bounceOut(1 - 2 * t)) / 2 : (1 + bounceOut(2 * t - 1)) / 2
458
468
  };
459
- /** Default property-tween ease (Motion Canvas precedent). */
460
- const DEFAULT_EASE = "easeInOutCubic";
469
+ function bounceOutD(t) {
470
+ const n1 = 7.5625;
471
+ const d1 = 2.75;
472
+ if (t < 1 / d1) return 2 * n1 * t;
473
+ if (t < 2 / d1) return 2 * n1 * (t - 1.5 / d1);
474
+ if (t < 2.5 / d1) return 2 * n1 * (t - 2.25 / d1);
475
+ return 2 * n1 * (t - 2.625 / d1);
476
+ }
477
+ const LN2 = Math.LN2;
461
478
  /**
462
- * CSS-style cubic bézier where x is time and y is progress. Newton's method
463
- * with a bisection fallback for the flat-derivative regions.
479
+ * Analytic derivatives d(u) of every named ease (§B.6) closed-form, used
480
+ * for reading velocity off in-flight curves at interruption time. Property-
481
+ * tested against central differences at interior points.
464
482
  */
465
- function cubicBezier(p1x, p1y, p2x, p2y) {
483
+ const easingDerivatives = {
484
+ linear: () => 1,
485
+ easeInQuad: (t) => 2 * t,
486
+ easeOutQuad: (t) => 2 * (1 - t),
487
+ easeInOutQuad: (t) => t < .5 ? 4 * t : 4 * (1 - t),
488
+ easeInCubic: (t) => 3 * t ** 2,
489
+ easeOutCubic: (t) => 3 * (1 - t) ** 2,
490
+ easeInOutCubic: (t) => t < .5 ? 12 * t ** 2 : 12 * (1 - t) ** 2,
491
+ easeInQuart: (t) => 4 * t ** 3,
492
+ easeOutQuart: (t) => 4 * (1 - t) ** 3,
493
+ easeInOutQuart: (t) => t < .5 ? 32 * t ** 3 : 32 * (1 - t) ** 3,
494
+ easeInQuint: (t) => 5 * t ** 4,
495
+ easeOutQuint: (t) => 5 * (1 - t) ** 4,
496
+ easeInOutQuint: (t) => t < .5 ? 80 * t ** 4 : 80 * (1 - t) ** 4,
497
+ easeInSine: (t) => Math.PI / 2 * Math.sin(t * Math.PI / 2),
498
+ easeOutSine: (t) => Math.PI / 2 * Math.cos(t * Math.PI / 2),
499
+ easeInOutSine: (t) => Math.PI / 2 * Math.sin(Math.PI * t),
500
+ easeInExpo: (t) => t === 0 ? 0 : 10 * LN2 * 2 ** (10 * t - 10),
501
+ easeOutExpo: (t) => t === 1 ? 0 : 10 * LN2 * 2 ** (-10 * t),
502
+ easeInOutExpo: (t) => t === 0 || t === 1 ? 0 : t < .5 ? 10 * LN2 * 2 ** (20 * t - 10) : 10 * LN2 * 2 ** (-20 * t + 10),
503
+ easeInCirc: (t) => t / Math.sqrt(1 - t * t),
504
+ easeOutCirc: (t) => (1 - t) / Math.sqrt(1 - (t - 1) * (t - 1)),
505
+ easeInOutCirc: (t) => t < .5 ? 2 * t / Math.sqrt(1 - 4 * t * t) : (2 - 2 * t) / Math.sqrt(1 - (-2 * t + 2) ** 2),
506
+ easeInBack: (t) => 3 * c3 * t ** 2 - 2 * c1 * t,
507
+ easeOutBack: (t) => 3 * c3 * (t - 1) ** 2 + 2 * c1 * (t - 1),
508
+ easeInOutBack: (t) => t < .5 ? 12 * 3.5949095 * t ** 2 - 4 * c2 * t : 3 * 3.5949095 * (2 * t - 2) ** 2 + 2 * c2 * (2 * t - 2),
509
+ easeInElastic: (t) => {
510
+ if (t === 0 || t === 1) return 0;
511
+ const theta = (t * 10 - 10.75) * c4;
512
+ const amp = 2 ** (10 * t - 10);
513
+ return -(10 * LN2 * amp * Math.sin(theta) + amp * 10 * c4 * Math.cos(theta));
514
+ },
515
+ easeOutElastic: (t) => {
516
+ if (t === 0 || t === 1) return 0;
517
+ const phi = (t * 10 - .75) * c4;
518
+ const amp = 2 ** (-10 * t);
519
+ return -10 * LN2 * amp * Math.sin(phi) + amp * 10 * c4 * Math.cos(phi);
520
+ },
521
+ easeInOutElastic: (t) => {
522
+ if (t === 0 || t === 1) return 0;
523
+ const psi = (20 * t - 11.125) * c5;
524
+ if (t < .5) {
525
+ const amp = 2 ** (20 * t - 10);
526
+ return -(20 * LN2 * amp * Math.sin(psi) + amp * 20 * c5 * Math.cos(psi)) / 2;
527
+ }
528
+ const amp = 2 ** (-20 * t + 10);
529
+ return (-20 * LN2 * amp * Math.sin(psi) + amp * 20 * c5 * Math.cos(psi)) / 2;
530
+ },
531
+ easeInBounce: (t) => bounceOutD(1 - t),
532
+ easeOutBounce: bounceOutD,
533
+ easeInOutBounce: (t) => t < .5 ? bounceOutD(1 - 2 * t) : bounceOutD(2 * t - 1)
534
+ };
535
+ /** Default property-tween ease (Motion Canvas precedent). */
536
+ const DEFAULT_EASE = "easeInOutCubic";
537
+ function bezierKernel(p1x, p1y, p2x, p2y) {
466
538
  const ax = 3 * p1x - 3 * p2x + 1;
467
539
  const bx = 3 * p2x - 6 * p1x;
468
540
  const cx = 3 * p1x;
@@ -472,6 +544,7 @@ function cubicBezier(p1x, p1y, p2x, p2y) {
472
544
  const sampleX = (u) => ((ax * u + bx) * u + cx) * u;
473
545
  const sampleY = (u) => ((ay * u + by) * u + cy) * u;
474
546
  const sampleDX = (u) => (3 * ax * u + 2 * bx) * u + cx;
547
+ const sampleDY = (u) => (3 * ay * u + 2 * by) * u + cy;
475
548
  const solveU = (x) => {
476
549
  let u = x;
477
550
  for (let i = 0; i < 8; i++) {
@@ -491,10 +564,33 @@ function cubicBezier(p1x, p1y, p2x, p2y) {
491
564
  }
492
565
  return u;
493
566
  };
567
+ return {
568
+ sampleY,
569
+ sampleDX,
570
+ sampleDY,
571
+ solveU
572
+ };
573
+ }
574
+ /**
575
+ * CSS-style cubic bézier where x is time and y is progress. Newton's method
576
+ * with a bisection fallback for the flat-derivative regions.
577
+ */
578
+ function cubicBezier(p1x, p1y, p2x, p2y) {
579
+ const k = bezierKernel(p1x, p1y, p2x, p2y);
494
580
  return (t) => {
495
581
  if (t <= 0) return 0;
496
582
  if (t >= 1) return 1;
497
- return sampleY(solveU(t));
583
+ return k.sampleY(k.solveU(t));
584
+ };
585
+ }
586
+ /** Analytic dy/dx of a cubic bézier ease: y'(s)/x'(s) at the solved parameter (§B.6). */
587
+ function cubicBezierDerivative(p1x, p1y, p2x, p2y) {
588
+ const k = bezierKernel(p1x, p1y, p2x, p2y);
589
+ return (t) => {
590
+ const u = k.solveU(Math.min(1, Math.max(0, t)));
591
+ const dx = k.sampleDX(u);
592
+ if (Math.abs(dx) < 1e-9) return 0;
593
+ return k.sampleDY(u) / dx;
498
594
  };
499
595
  }
500
596
  var UnknownEasingError = class extends Error {
@@ -519,6 +615,20 @@ function params(cfg) {
519
615
  zeta: cfg.damping / (2 * Math.sqrt(cfg.stiffness * mass))
520
616
  };
521
617
  }
618
+ /** Raw closed-form spring velocity at time t — d/dt of rawValue's three branches. */
619
+ function rawDerivative(cfg, t) {
620
+ if (t <= 0) return 0;
621
+ const { w0, zeta } = params(cfg);
622
+ if (Math.abs(zeta - 1) < 1e-9) return w0 * w0 * t * Math.exp(-w0 * t);
623
+ if (zeta < 1) {
624
+ const wd = w0 * Math.sqrt(1 - zeta * zeta);
625
+ return Math.exp(-zeta * w0 * t) * (w0 * w0 / wd) * Math.sin(wd * t);
626
+ }
627
+ const s = Math.sqrt(zeta * zeta - 1);
628
+ const r1 = -w0 * (zeta - s);
629
+ const r2 = -w0 * (zeta + s);
630
+ return r1 * r2 * (Math.exp(r1 * t) - Math.exp(r2 * t)) / (r1 - r2);
631
+ }
522
632
  /** Raw closed-form spring position at time t (seconds). Approaches 1, may overshoot. */
523
633
  function rawValue(cfg, t) {
524
634
  if (t <= 0) return 0;
@@ -565,6 +675,53 @@ function value(cfg, t, opts) {
565
675
  const d = duration(cfg, opts);
566
676
  return rawValue(cfg, Math.min(t, d)) / rawValue(cfg, d);
567
677
  }
678
+ function retarget(cfg, x0, v0) {
679
+ const { w0, zeta } = params(cfg);
680
+ let value0;
681
+ let velocity0;
682
+ let envelope;
683
+ if (Math.abs(zeta - 1) < 1e-9) {
684
+ const b = v0 + w0 * x0;
685
+ value0 = (tau) => tau <= 0 ? x0 : Math.exp(-w0 * tau) * (x0 + b * tau);
686
+ velocity0 = (tau) => tau <= 0 ? v0 : Math.exp(-w0 * tau) * (v0 - w0 * b * tau);
687
+ envelope = (tau) => Math.exp(-w0 * tau) * (Math.abs(x0) + Math.abs(b) * tau);
688
+ } else if (zeta < 1) {
689
+ const wd = w0 * Math.sqrt(1 - zeta * zeta);
690
+ const c2v = (v0 + zeta * w0 * x0) / wd;
691
+ const amp = Math.hypot(x0, c2v);
692
+ value0 = (tau) => tau <= 0 ? x0 : Math.exp(-zeta * w0 * tau) * (x0 * Math.cos(wd * tau) + c2v * Math.sin(wd * tau));
693
+ velocity0 = (tau) => tau <= 0 ? v0 : Math.exp(-zeta * w0 * tau) * (v0 * Math.cos(wd * tau) - (w0 * w0 * x0 + zeta * w0 * v0) / wd * Math.sin(wd * tau));
694
+ envelope = (tau) => Math.exp(-zeta * w0 * tau) * amp;
695
+ } else {
696
+ const s = Math.sqrt(zeta * zeta - 1);
697
+ const rp = w0 * (-zeta + s);
698
+ const rm = w0 * (-zeta - s);
699
+ const cp = (v0 - rm * x0) / (rp - rm);
700
+ const cm = (rp * x0 - v0) / (rp - rm);
701
+ value0 = (tau) => tau <= 0 ? x0 : cp * Math.exp(rp * tau) + cm * Math.exp(rm * tau);
702
+ velocity0 = (tau) => tau <= 0 ? v0 : cp * rp * Math.exp(rp * tau) + cm * rm * Math.exp(rm * tau);
703
+ envelope = (tau) => Math.abs(cp) * Math.exp(rp * tau) + Math.abs(cm) * Math.exp(rm * tau);
704
+ }
705
+ const settleTime = (tol) => {
706
+ const eps = tol ?? Math.abs(x0) * .005 + 1e-6;
707
+ if (envelope(0) <= eps && Math.abs(v0) < 1e-12) return 0;
708
+ let hi = 1 / w0;
709
+ let guard = 0;
710
+ while (envelope(hi) > eps && guard++ < 64) hi *= 2;
711
+ let lo = 0;
712
+ for (let i = 0; i < 64 && hi - lo > 1e-9; i++) {
713
+ const mid = (lo + hi) / 2;
714
+ if (Math.max(envelope(mid), envelope(mid * 1.05 + 1e-6)) > eps) lo = mid;
715
+ else hi = mid;
716
+ }
717
+ return hi;
718
+ };
719
+ return {
720
+ value: value0,
721
+ velocity: velocity0,
722
+ settleTime
723
+ };
724
+ }
568
725
  const spring = Object.assign((cfg) => {
569
726
  params(cfg);
570
727
  return {
@@ -575,7 +732,8 @@ const spring = Object.assign((cfg) => {
575
732
  };
576
733
  }, {
577
734
  duration,
578
- value
735
+ value,
736
+ retarget
579
737
  });
580
738
  /**
581
739
  * The spring as a normalized easing over a segment whose length must equal
@@ -585,6 +743,28 @@ function springEasing(cfg) {
585
743
  const d = duration(cfg);
586
744
  return (p) => value(cfg, p * d);
587
745
  }
746
+ /**
747
+ * Analytic d/dp of springEasing (§B.6): oscillator derivative × the affine
748
+ * rescale factor × duration (chain rule p → t = p·D). Flat past p=1,
749
+ * matching value()'s clamp (right-derivative convention).
750
+ */
751
+ function springEasingDerivative(cfg) {
752
+ const d = duration(cfg);
753
+ const scale = d / rawValue(cfg, d);
754
+ return (p) => p >= 1 ? 0 : rawDerivative(cfg, p * d) * scale;
755
+ }
756
+ //#endregion
757
+ //#region src/devWarning.ts
758
+ let devWarn = (msg) => {
759
+ globalThis.console?.warn(`[glissade] ${msg}`);
760
+ };
761
+ function setDevWarning(fn) {
762
+ devWarn = fn;
763
+ }
764
+ /** Internal: emit through the configurable channel. */
765
+ function emitDevWarning(message) {
766
+ devWarn(message);
767
+ }
588
768
  //#endregion
589
769
  //#region src/track.ts
590
770
  /**
@@ -645,6 +825,27 @@ function resolveEase(spec) {
645
825
  if (spec.kind === "cubicBezier") return cubicBezier(...spec.pts);
646
826
  return springEasing(spec);
647
827
  }
828
+ const warnedNumericDerivative = /* @__PURE__ */ new Set();
829
+ /**
830
+ * Analytic d(u) for an ease spec (§B.6). Custom-registered eases without a
831
+ * derivative fall back to a symmetric difference with a one-time dev warning.
832
+ */
833
+ function resolveEaseDerivative(spec) {
834
+ if (spec === void 0) return easingDerivatives["linear"];
835
+ if (typeof spec === "string") {
836
+ const d = easingDerivatives[spec];
837
+ if (d) return d;
838
+ const fn = namedEasing(spec);
839
+ if (!warnedNumericDerivative.has(spec)) {
840
+ warnedNumericDerivative.add(spec);
841
+ emitDevWarning(`easing '${spec}' has no registered derivative; velocity uses a numeric fallback — register one in easingDerivatives for exact interruption handoff`);
842
+ }
843
+ const h = 1e-5;
844
+ return (u) => (fn(Math.min(1, u + h)) - fn(Math.max(0, u - h))) / (Math.min(1, u + h) - Math.max(0, u - h));
845
+ }
846
+ if (spec.kind === "cubicBezier") return cubicBezierDerivative(...spec.pts);
847
+ return springEasingDerivative(spec);
848
+ }
648
849
  const samplerStates = /* @__PURE__ */ new WeakMap();
649
850
  function state(tr) {
650
851
  let s = samplerStates.get(tr);
@@ -684,6 +885,34 @@ function findSegment(keys, t, hint) {
684
885
  }
685
886
  return lo;
686
887
  }
888
+ /**
889
+ * Analytic track derivative at time t, in value-units per second of local
890
+ * track time (v2 addendum §B.3/§B.6 conventions, pinned):
891
+ * (a) at a key boundary, velocity is the RIGHT derivative;
892
+ * (b) hold segments and the clamped regions outside the keys have v = 0;
893
+ * (c) types without sub/scale operators return null (no kinetic velocity).
894
+ */
895
+ function velocityAt(tr, t) {
896
+ const vt = getValueType(tr.type);
897
+ if (!vt.sub || !vt.scale) return null;
898
+ const keys = tr.keys;
899
+ const n = keys.length;
900
+ const s = state(tr);
901
+ const i = findSegment(keys, t, s.cursor);
902
+ const zero = vt.scale(vt.sub(keys[0].value, keys[0].value), 0);
903
+ if (i === 0 || i >= n) return zero;
904
+ const arrival = keys[i];
905
+ if (arrival.interp === "hold") return zero;
906
+ const prev = keys[i - 1];
907
+ const segDur = arrival.t - prev.t;
908
+ const p = (t - prev.t) / segDur;
909
+ if (!vt.extrapolates) {
910
+ const eased = easeFor(tr, s, i)(p);
911
+ if (eased < 0 || eased > 1) return zero;
912
+ }
913
+ const d = resolveEaseDerivative(arrival.ease)(p);
914
+ return vt.scale(vt.sub(arrival.value, prev.value), d / segDur);
915
+ }
687
916
  /** Pure sample of a track at time t (§2.4). */
688
917
  function sampleTrack(tr, t) {
689
918
  const keys = tr.keys;
@@ -740,16 +969,6 @@ function validateSpringKeys(tr) {
740
969
  }
741
970
  }
742
971
  }
743
- let devWarn = (msg) => {
744
- globalThis.console?.warn(`[glissade] ${msg}`);
745
- };
746
- function setDevWarning(fn) {
747
- devWarn = fn;
748
- }
749
- /** Internal: emit through the configurable dev-warning channel. */
750
- function emitDevWarning(message) {
751
- devWarn(message);
752
- }
753
972
  function rebaseKeys(keys, at, timeScale) {
754
973
  return keys.map((k) => ({
755
974
  ...k,
@@ -797,7 +1016,7 @@ function coalesce(entries) {
797
1016
  const existingStart = existing.keys[0].t;
798
1017
  const existingEnd = existing.keys[existing.keys.length - 1].t;
799
1018
  const kept = existing.keys.filter((k) => k.t < start || k.t > end);
800
- if (existingStart <= end && start <= existingEnd) devWarn(`overlapping tracks for '${tr.target}' in [${start}, ${end}]: later insertion wins (${existing.keys.length - kept.length} earlier key(s) dropped)`);
1019
+ if (existingStart <= end && start <= existingEnd) emitDevWarning(`overlapping tracks for '${tr.target}' in [${start}, ${end}]: later insertion wins (${existing.keys.length - kept.length} earlier key(s) dropped)`);
801
1020
  existing.keys = [...kept, ...tr.keys].sort((a, b) => a.t - b.t);
802
1021
  }
803
1022
  return byTarget;
@@ -1103,14 +1322,21 @@ var UnboundTargetError = class extends Error {
1103
1322
  */
1104
1323
  function bindTimeline(compiled, resolve, playhead = createPlayhead()) {
1105
1324
  const bound = [];
1325
+ const samplers = /* @__PURE__ */ new Map();
1106
1326
  for (const [target, tr] of compiled.tracks) {
1107
1327
  const sig = resolve(target);
1108
1328
  if (!sig) throw new UnboundTargetError(target);
1109
1329
  sig.bindSource(() => sampleTrack(tr, playhead()));
1110
1330
  bound.push(sig);
1331
+ samplers.set(target, {
1332
+ track: tr,
1333
+ value: (t) => sampleTrack(tr, t),
1334
+ velocity: (t) => velocityAt(tr, t)
1335
+ });
1111
1336
  }
1112
1337
  return {
1113
1338
  playhead,
1339
+ samplers,
1114
1340
  unbind: () => {
1115
1341
  for (const sig of bound) sig.unbindSource();
1116
1342
  }
@@ -1328,4 +1554,4 @@ function mergeSidecar(code, sidecar) {
1328
1554
  return merged;
1329
1555
  }
1330
1556
  //#endregion
1331
- export { BakeError, CircularDependencyError, ColorParseError, DEFAULT_EASE, PositionError, SidecarVersionError, TARGET_PATH, TimelineValidationError, TrackValidationError, UnboundTargetError, UnknownEasingError, UnknownValueTypeError, UnresolvableTargetError, ValueTypeInferenceError, WriteDuringEvaluationError, bake, bakeCheckpointed, beginReadPhase, bindTimeline, booleanType, buildTimeline, colorType, compileTimeline, computed, createPlayhead, cubicBezier, easings, emptySidecar, endReadPhase, evaluateAt, formatColor, getTimelineCallbacks, getValueType, inReadPhase, inferValueType, key, lerpColor, mergeSidecar, namedEasing, normalizeEditedKeys, numberType, oklabToRgba, parseColor, random, registerValueType, resolveEase, resolveTweenTarget, rgbaToOklab, sampleTrack, setDevWarning, signal, spring, springEasing, stringType, timeline, track, untracked, validateTrack, vec2Equals, vec2Signal, vec2Type };
1557
+ export { BakeError, CircularDependencyError, ColorParseError, DEFAULT_EASE, PositionError, SidecarVersionError, TARGET_PATH, TimelineValidationError, TrackValidationError, UnboundTargetError, UnknownEasingError, UnknownValueTypeError, UnresolvableTargetError, ValueTypeInferenceError, WriteDuringEvaluationError, bake, bakeCheckpointed, beginReadPhase, bindTimeline, booleanType, buildTimeline, colorType, compileTimeline, computed, createPlayhead, cubicBezier, cubicBezierDerivative, easingDerivatives, easings, emitDevWarning, emptySidecar, endReadPhase, evaluateAt, formatColor, getTimelineCallbacks, getValueType, inReadPhase, inferValueType, key, lerpColor, mergeSidecar, namedEasing, normalizeEditedKeys, numberType, oklabToRgba, parseColor, random, registerValueType, resolveEase, resolveEaseDerivative, resolveTweenTarget, rgbaToOklab, sampleTrack, setDevWarning, signal, spring, springEasing, springEasingDerivative, stringType, timeline, track, untracked, validateTrack, vec2Equals, vec2Signal, vec2Type, velocityAt };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@glissade/core",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "glissade core: signals, tracks, timeline document, evaluation, easing, springs, seeded RNG. Zero DOM/Node dependencies.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",