@sweberdev/lagrangian 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,382 @@
1
+ /** Position and velocity of a motion at one moment. */
2
+ interface MotionState {
3
+ value: number;
4
+ velocity: number;
5
+ done: boolean;
6
+ }
7
+ /** A motion maps elapsed seconds to a state. Springs, decay and your own functions fit this. */
8
+ type Motion = (t: number) => MotionState;
9
+ interface SpringOptions {
10
+ /** Spring constant k. Higher is snappier. Default 170. */
11
+ stiffness?: number;
12
+ /** Damping coefficient c. Lower bounces more. Default 26. */
13
+ damping?: number;
14
+ /** Mass m. Heavier is slower and carries more momentum. Default 1. */
15
+ mass?: number;
16
+ /**
17
+ * Perceptual duration in seconds, used together with `bounce` instead of
18
+ * stiffness and damping (as in SwiftUI). It is the period of the undamped spring.
19
+ */
20
+ duration?: number;
21
+ /** -1 to 1. 0 is critically damped, 0.3 is a gentle bounce, negative values are overdamped. */
22
+ bounce?: number;
23
+ /** The motion counts as finished once it is this close to the target... */
24
+ restDelta?: number;
25
+ /** ...and slower than this (units per second). */
26
+ restSpeed?: number;
27
+ }
28
+ interface SpringParams {
29
+ stiffness: number;
30
+ damping: number;
31
+ mass: number;
32
+ }
33
+ /** Resolves stiffness, damping and mass, also from `duration` and `bounce`. */
34
+ declare function springParams(options?: SpringOptions): SpringParams;
35
+ /** Damping ratio: below 1 the spring overshoots, 1 is critical, above 1 it creeps. */
36
+ declare function dampingRatio(options?: SpringOptions): number;
37
+ /**
38
+ * A spring from `from` to `to`, starting with `velocity` (units per second).
39
+ * Returns the exact state for any elapsed time.
40
+ */
41
+ declare function spring(from: number, to: number, velocity?: number, options?: SpringOptions): Motion;
42
+ /**
43
+ * Seconds until a motion settles, sampled at 1 ms. Returns Infinity when it does not settle
44
+ * within `limit` seconds (an undamped spring never does).
45
+ */
46
+ declare function settleTime(motion: Motion, limit?: number): number;
47
+ /** The value a motion comes to rest at, or its value after `limit` seconds. */
48
+ declare function restValue(motion: Motion, limit?: number): number;
49
+
50
+ type Snap = readonly number[] | ((restingPoint: number) => number);
51
+ interface DecayOptions {
52
+ /** Starting velocity in units per second. Defaults to the value's current velocity. */
53
+ velocity?: number;
54
+ /**
55
+ * Fraction of speed kept per millisecond. 0.998 is the iOS "normal" scroll feel (default),
56
+ * 0.99 is "fast" and stops much sooner.
57
+ */
58
+ deceleration?: number;
59
+ /** Lower bound. Crossing it hands over to a spring that settles on the edge. */
60
+ min?: number;
61
+ /** Upper bound. */
62
+ max?: number;
63
+ /** Snap points, or a function that maps the natural resting point to a snapped one. */
64
+ snap?: Snap;
65
+ /** Spring used at the edges and when a snap point needs a spring. */
66
+ bounce?: SpringOptions;
67
+ /** The motion ends once it is this close to its resting point. Default 0.5. */
68
+ restDelta?: number;
69
+ }
70
+ declare function timeConstant(deceleration?: number): number;
71
+ /** Where a throw with this velocity comes to rest without bounds or snap points. */
72
+ declare function projectRest(from: number, velocity: number, deceleration?: number): number;
73
+ /** Nearest snap point to `value`. */
74
+ declare function nearest(points: readonly number[], value: number): number;
75
+ /** A throw with friction from `from`, optionally with bounds and snap points. */
76
+ declare function decay(from: number, options?: DecayOptions): Motion;
77
+ /**
78
+ * Elastic overscroll, as on iOS: the further past the edge, the harder it pulls back.
79
+ * `distance` is how far past the edge, `dimension` the size of the visible area.
80
+ */
81
+ declare function rubberband(distance: number, dimension: number, constant?: number): number;
82
+ /** Applies `rubberband` to a value outside [min, max]. Values inside stay unchanged. */
83
+ declare function rubberClamp(value: number, min: number, max: number, dimension: number, constant?: number): number;
84
+
85
+ /** A running animation. Await `finished` to know whether it completed. */
86
+ interface Animation {
87
+ /** Resolves with true when the motion came to rest, false when it was interrupted. */
88
+ readonly finished: Promise<boolean>;
89
+ stop(): void;
90
+ }
91
+ type Listener = (value: number, velocity: number) => void;
92
+ declare class PhysicsValue {
93
+ #private;
94
+ constructor(initial?: number);
95
+ get(): number;
96
+ /** Units per second, from the running motion or from recent `set` calls. */
97
+ get velocity(): number;
98
+ get animating(): boolean;
99
+ /** Sets the value directly (e.g. while dragging). Stops any motion and tracks the velocity. */
100
+ set(value: number): void;
101
+ /** Sets the value and drops all momentum. */
102
+ jump(value: number): void;
103
+ /** Springs to `target`, starting from the current position and velocity. */
104
+ to(target: number, options?: SpringOptions & {
105
+ velocity?: number;
106
+ }): Animation;
107
+ /** Throws the value with friction, optionally into bounds or onto snap points. */
108
+ throw(options?: DecayOptions): Animation;
109
+ /** Runs any motion. Elapsed time starts at 0 on the next frame. */
110
+ start(motion: Motion): Animation;
111
+ /** Stops where it is. The velocity is kept, so a following `to` or `throw` continues it. */
112
+ stop(): void;
113
+ /** Calls `listener` on every change. Returns the unsubscribe function. */
114
+ on(listener: Listener): () => void;
115
+ }
116
+ /** Creates a value with momentum. */
117
+ declare function value(initial?: number): PhysicsValue;
118
+
119
+ type TransformProp = "x" | "y" | "z" | "scale" | "scaleX" | "scaleY" | "rotate";
120
+ type Prop = TransformProp | "opacity" | `--${string}`;
121
+ type Props = Partial<Record<Prop, number>>;
122
+ interface AnimateOptions extends SpringOptions {
123
+ /** Starting velocity per property, e.g. from a gesture. */
124
+ velocity?: Props;
125
+ /** Seconds to wait before starting. */
126
+ delay?: number;
127
+ /** Extra delay per element when animating several, in seconds. */
128
+ stagger?: number;
129
+ }
130
+ type Target = Element | Iterable<Element> | ArrayLike<Element> | string;
131
+ /**
132
+ * The physical value behind one property of an element. Use it to read the live velocity,
133
+ * to drive the element from a gesture with `set`, or to listen to changes.
134
+ */
135
+ declare function getValue(element: Element, prop: Prop): PhysicsValue;
136
+ /**
137
+ * Springs one or more elements to the given values.
138
+ *
139
+ * ```ts
140
+ * animate(".card", { x: 120, rotate: 4, opacity: 1 }, { duration: 0.6, bounce: 0.3 })
141
+ * ```
142
+ */
143
+ declare function animate(target: Target, props: Props, options?: AnimateOptions): Animation;
144
+ /** Sets values immediately, without animation, and drops their momentum. */
145
+ declare function set(target: Target, props: Props): void;
146
+
147
+ interface Bounds {
148
+ left?: number;
149
+ right?: number;
150
+ top?: number;
151
+ bottom?: number;
152
+ }
153
+ interface DragInfo {
154
+ x: number;
155
+ y: number;
156
+ velocityX: number;
157
+ velocityY: number;
158
+ }
159
+ interface DraggableOptions {
160
+ /** Restrict movement to one axis. Default "both". */
161
+ axis?: "x" | "y" | "both";
162
+ /**
163
+ * Movement limits in pixels relative to the start position, or an element the dragged
164
+ * element has to stay inside.
165
+ */
166
+ bounds?: Bounds | Element | (() => Bounds | Element);
167
+ /** Resistance past the bounds, 0 to 1 (0.55 like iOS), or false to stop hard at the edge. */
168
+ rubberband?: number | false;
169
+ /** Keep moving with friction after release. Default true. */
170
+ inertia?: boolean;
171
+ /** Fraction of speed kept per millisecond during the throw. Default 0.998. */
172
+ deceleration?: number;
173
+ /** Snap points per axis, in pixels relative to the start position. */
174
+ snap?: {
175
+ x?: Snap;
176
+ y?: Snap;
177
+ };
178
+ /** Spring for snapping and for bouncing off the bounds. */
179
+ spring?: SpringOptions;
180
+ onStart?: (info: DragInfo) => void;
181
+ onMove?: (info: DragInfo) => void;
182
+ /** Called on release with the throw velocity. */
183
+ onEnd?: (info: DragInfo) => void;
184
+ }
185
+ interface Draggable {
186
+ readonly x: PhysicsValue;
187
+ readonly y: PhysicsValue;
188
+ readonly dragging: boolean;
189
+ destroy(): void;
190
+ }
191
+ /** Makes an element draggable and throwable. */
192
+ declare function draggable(element: HTMLElement, options?: DraggableOptions): Draggable;
193
+
194
+ /** Returns d(state)/dt for a state at time t. */
195
+ type Derivatives = (state: readonly number[], t: number) => number[];
196
+ /** One classic fourth-order Runge-Kutta step of size `dt`. */
197
+ declare function rk4(state: readonly number[], t: number, dt: number, f: Derivatives): number[];
198
+ interface System {
199
+ /** Current state vector. */
200
+ readonly state: readonly number[];
201
+ /** Simulated time in seconds. */
202
+ readonly time: number;
203
+ /** Advances by `seconds` in fixed steps, so results do not depend on the frame rate. */
204
+ advance(seconds: number): readonly number[];
205
+ /** Replaces the state, e.g. after the user grabbed something. */
206
+ reset(state: readonly number[]): void;
207
+ }
208
+ /** A system of ODEs stepped with RK4 at a fixed step (default 1/240 s). */
209
+ declare function system(f: Derivatives, initial: readonly number[], step?: number): System;
210
+
211
+ type Task = (dt: number, now: number) => void;
212
+ /** Current time in milliseconds, from the manual clock while the loop is in manual mode. */
213
+ declare function now(): number;
214
+ declare const loop: {
215
+ /** Runs `task` on every frame until the returned function is called. */
216
+ add(task: Task): () => void;
217
+ /**
218
+ * Switches to manual stepping (for tests, server rendering or video export) or back to
219
+ * requestAnimationFrame.
220
+ */
221
+ manual(on?: boolean): void;
222
+ /** Advances the manual clock by `ms` and runs one frame. Only has an effect in manual mode. */
223
+ step(ms?: number): void;
224
+ /** Number of running tasks. */
225
+ readonly size: number;
226
+ };
227
+
228
+ type ReducedMotion = "user" | "always" | "never";
229
+ /**
230
+ * "user" (default) follows the prefers-reduced-motion setting, "always" skips animated
231
+ * transitions everywhere, "never" ignores the setting. Direct manipulation (dragging, the
232
+ * world simulation) always follows the pointer; only automatic motion is skipped.
233
+ */
234
+ declare function setReducedMotion(value: ReducedMotion): void;
235
+ declare function reducedMotion(): boolean;
236
+
237
+ declare class VelocityTracker {
238
+ #private;
239
+ /** Adds a sample: `time` in milliseconds, `value` in any unit. */
240
+ add(time: number, value: number): void;
241
+ /** Velocity in units per second at `time` (ms). 0 when the pointer has been resting. */
242
+ velocity(time?: number): number;
243
+ reset(): void;
244
+ }
245
+
246
+ interface Vector {
247
+ x: number;
248
+ y: number;
249
+ }
250
+ interface Rect {
251
+ left: number;
252
+ top: number;
253
+ right: number;
254
+ bottom: number;
255
+ }
256
+ interface BodyOptions {
257
+ /** Centre in pixels. */
258
+ x: number;
259
+ y: number;
260
+ radius: number;
261
+ /** Mass in kg. Defaults to the area times `density`. */
262
+ mass?: number;
263
+ /** kg per square meter, used when `mass` is not given. Default 1. */
264
+ density?: number;
265
+ /** Bounciness, 0 (clay) to 1 (perfectly elastic). Default 0.5. */
266
+ restitution?: number;
267
+ /** Coulomb friction coefficient. Default 0.4. */
268
+ friction?: number;
269
+ /** Fixed bodies never move but others collide with them. */
270
+ fixed?: boolean;
271
+ /** Initial velocity in pixels per second. */
272
+ vx?: number;
273
+ vy?: number;
274
+ /** Angle in radians and angular velocity in radians per second. */
275
+ angle?: number;
276
+ spin?: number;
277
+ /** An element positioned at the top left of the world that follows this body. */
278
+ element?: HTMLElement | SVGElement | null;
279
+ }
280
+ interface Body {
281
+ x: number;
282
+ y: number;
283
+ vx: number;
284
+ vy: number;
285
+ angle: number;
286
+ spin: number;
287
+ readonly radius: number;
288
+ readonly mass: number;
289
+ restitution: number;
290
+ friction: number;
291
+ readonly fixed: boolean;
292
+ element: HTMLElement | SVGElement | null;
293
+ /** @internal */ invMass: number;
294
+ /** @internal */ invInertia: number;
295
+ }
296
+ interface LinkOptions {
297
+ /** Rest length in pixels. Defaults to the current distance. */
298
+ length?: number;
299
+ /** Oscillation frequency in Hz. Higher is stiffer. Default 4. */
300
+ frequency?: number;
301
+ /** Damping ratio, 0 to 1. Default 0.3. */
302
+ damping?: number;
303
+ }
304
+ interface Link {
305
+ a: Body;
306
+ b: Body;
307
+ length: number;
308
+ frequency: number;
309
+ damping: number;
310
+ }
311
+ interface WorldOptions {
312
+ /** Gravity in m/s². Default { x: 0, y: 9.81 }. */
313
+ gravity?: Vector;
314
+ /** Scale between the simulation (meters) and the screen. Default 500 pixels per meter. */
315
+ pixelsPerMeter?: number;
316
+ /** Linear air drag per second. Default 0.05. */
317
+ airDrag?: number;
318
+ /**
319
+ * Rolling resistance coefficient: a braking torque proportional to the contact force that
320
+ * makes rolling bodies come to a stop and keeps resting piles still. Default 0.05.
321
+ */
322
+ rollingResistance?: number;
323
+ /** Walls in pixels, or an element whose content box becomes the walls. */
324
+ bounds?: Rect | HTMLElement | null;
325
+ /** Simulation steps per second. Default 240. */
326
+ rate?: number;
327
+ /** Collision solver passes per step. More gives steadier stacks. Default 4. */
328
+ iterations?: number;
329
+ /** Called after every frame, e.g. to draw on a canvas. */
330
+ onFrame?: (world: World) => void;
331
+ /** Called when two bodies (or a body and a wall) hit with an impact speed in px/s. */
332
+ onCollide?: (a: Body, b: Body | null, speed: number) => void;
333
+ }
334
+ interface Grab {
335
+ move(x: number, y: number): void;
336
+ release(): void;
337
+ }
338
+ declare class World {
339
+ #private;
340
+ readonly bodies: Body[];
341
+ readonly links: Link[];
342
+ gravity: Vector;
343
+ pixelsPerMeter: number;
344
+ airDrag: number;
345
+ rollingResistance: number;
346
+ iterations: number;
347
+ bounds: Rect | null;
348
+ constructor(options?: WorldOptions);
349
+ /** Sets the walls. With an element, they follow its size. */
350
+ setBounds(bounds: Rect | HTMLElement | null): void;
351
+ add(options: BodyOptions): Body;
352
+ remove(body: Body): void;
353
+ /** Connects two bodies with a damped spring. */
354
+ link(a: Body, b: Body, options?: LinkOptions): Link;
355
+ unlink(link: Link): void;
356
+ /** Body under a point, if any. */
357
+ bodyAt(x: number, y: number): Body | null;
358
+ /** Pulls a body towards a moving point with a stiff spring. Released bodies keep their speed. */
359
+ grab(body: Body, x: number, y: number): Grab;
360
+ /** Adds an impulse in kg·px/s at the centre (or `at` a point on the body for spin). */
361
+ push(body: Body, impulse: Vector, at?: Vector): void;
362
+ /**
363
+ * Lets the pointer grab and throw bodies inside `element` (default: the bounds element).
364
+ * Returns a function that removes the listeners.
365
+ */
366
+ bindPointer(element?: HTMLElement | null): () => void;
367
+ /** Starts the simulation on the shared frame loop. */
368
+ start(): this;
369
+ /** Pauses the simulation. */
370
+ stop(): this;
371
+ /** True while the world runs and something moves. A resting world uses no CPU. */
372
+ get awake(): boolean;
373
+ /** Resumes a resting world, e.g. after you changed bodies by hand. */
374
+ wake(): void;
375
+ destroy(): void;
376
+ /** Advances the simulation by `dt` seconds. Called for you while the world runs. */
377
+ step(dt: number): void;
378
+ }
379
+ /** Creates a world. Call `start()` to run it on the frame loop, or `step()` it yourself. */
380
+ declare function world(options?: WorldOptions): World;
381
+
382
+ export { type AnimateOptions, type Animation, type Body, type BodyOptions, type Bounds, type DecayOptions, type Derivatives, type DragInfo, type Draggable, type DraggableOptions, type Grab, type Link, type LinkOptions, type Listener, type Motion, type MotionState, PhysicsValue, type Prop, type Props, type Rect, type ReducedMotion, type Snap, type SpringOptions, type SpringParams, type System, type Task, type TransformProp, type Vector, VelocityTracker, World, type WorldOptions, animate, dampingRatio, decay, draggable, getValue, loop, nearest, now, projectRest, reducedMotion, restValue, rk4, rubberClamp, rubberband, set, setReducedMotion, settleTime, spring, springParams, system, timeConstant, value, world };