coily 0.1.1

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2022 Tim Morris
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,256 @@
1
+ # `coily`
2
+
3
+ Spring Physics Utilities
4
+
5
+ ## Installation & Setup
6
+
7
+ `coily` is available on npm and can be installed with your favorite package manager:
8
+
9
+ ```shell
10
+ $ yarn add coily
11
+ # or
12
+ $ npm i coily
13
+ ```
14
+
15
+ Then import the creator from the package in your code:
16
+
17
+ ```typescript
18
+ import { createSpring } from 'coily'
19
+
20
+ const spring = createSpring(0, { mass: 1, tension: 100, friction: 25 })
21
+ ```
22
+
23
+ ## Usage
24
+
25
+ ### Creating a Spring
26
+
27
+ The `createSpring` function takes the following arguments:
28
+
29
+ - `initial: number`
30
+ The initial value of the spring, and its target. All springs start in a resting state, but you can set `spring.target`
31
+ at any time
32
+
33
+ - `config: SpringConfig`
34
+ The configuration for the spring. `SpringConfig` takes the following parameters:
35
+
36
+ - `config.mass: number`
37
+ How massive the spring is. A lower number will allow the spring to speed up and slow down more quickly than a
38
+ larger number. Larger numbers are more likely to overshoot the `target`
39
+
40
+ - `config.tension: number`
41
+ How much force the spring experiences toward its `target`. A higher value will move the spring more quickly, and
42
+ allow the spring to potentially overshoot.
43
+
44
+ - `config.friction: number`
45
+ How much dampening the spring experiences against its tension. A higher friction will make sure the spring doesn't
46
+ move too quickly, and slows down dramatically as it approaches the `target`
47
+
48
+ - `options: SpringOptions`
49
+ Additional options for the spring. `SpringOptions` takes the following parameters:
50
+
51
+ - `options.restingDistance: number`
52
+ The minimum difference between the `target` and `value` at which the spring will consider itself "at" the `target`
53
+
54
+ - `options.restingVelocity: number`
55
+ The minimum velocity at which the spring will consider itself "not moving"
56
+
57
+ - `options.arrivalBehavior: 'bounce' | 'clamp' | 'none' | number | ((velocity: number) => number)`
58
+ How the spring's `velocity` behaves when it arrives at its `target` value.
59
+ `velocity` is expressed in unit/second. Defaults to `none`.
60
+ - `'bounce'`: multiplies the velocity by -1, bouncing the value back in the direction from which it came
61
+ - `'clamp'`: multiplies the velocity by 0, instantly stopping the spring at its value
62
+ - `'none'`: multiplies the velocity by 1, allowing the spring to overshoot as necessary
63
+ - `number`: multiplies the velocity by the given number
64
+ - `(velocity: number) => number`: sets the velocity to the result of a function whose only argument is the
65
+ previous velocity
66
+
67
+ #### Spring Config Tips
68
+
69
+ The configuration object above provides a good starting point when picking your config:
70
+
71
+ ```typescript
72
+ createSpring(0, { mass: 1, tension: 100, friction: 25 })
73
+ ```
74
+
75
+ Don't be afraid to make `mass` a decimal, and don't be afraid of large tensions and frictions either. Picking the
76
+ configuration is more of an art than a science.
77
+
78
+ #### Spring Options
79
+
80
+ The spring options object is helpful in reducing the amount of computation your springs perform. If the spring was
81
+ allowed to wait until its `velocity` and difference (`value` - `target`) were _at_ zero, it would imperceptibly update
82
+ every frame for a very long time.
83
+
84
+ The `restingDistance` and `restingVelocity` options help combat this, by telling the spring at what point it should
85
+ consider itself on target and resting. Once the velocity is less than `restingVelocity` and the difference
86
+ between `value` and `target` is less than `restingDistance`, the spring sets its `value` to its `target` and enters
87
+ the `resting` state.
88
+
89
+ ### Working With Springs
90
+
91
+ #### Reading Information
92
+
93
+ The spring exposes some readonly information such as its `value`, `state` and `velocity`:
94
+
95
+ ```typescript
96
+ /**
97
+ * The current value of the spring
98
+ */
99
+ spring.value
100
+
101
+ /**
102
+ * In which state the spring is currently. Will be one of:
103
+ * - "resting"
104
+ * - "moving"
105
+ * - "frozen"
106
+ */
107
+ spring.state
108
+
109
+ /**
110
+ * How fast the spring is currently moving, and in which direction
111
+ */
112
+ spring.velocity
113
+ ```
114
+
115
+ Additionally, `target` and `config` can be updated whenever:
116
+
117
+ ```typescript
118
+ /**
119
+ * Get & Set the target of the spring
120
+ */
121
+ console.log(spring.target)
122
+ spring.target = 10
123
+
124
+ /**
125
+ * Get & Set the config of the spring
126
+ */
127
+ console.log(spring.config)
128
+ spring.config = { mass: 0.1, tension: 500, friction: 5 }
129
+ ```
130
+
131
+ #### Updating
132
+
133
+ To simulate the spring and update its value, call the `simulate` method and pass in the number of milliseconds since the
134
+ last update:
135
+
136
+ ```typescript
137
+ // Tickloop is a library that exposes a simple requestAnimationFrame based update loop
138
+ import { createTicker } from 'tickloop'
139
+ import { createSpring } from 'coily'
140
+
141
+ const spring = createSpring(0, { mass: 1, tension: 100, friction: 20 })
142
+
143
+ const ticker = createTicker()
144
+ ticker.add((_, delta) => {
145
+ spring.simulate(delta)
146
+ })
147
+ ```
148
+
149
+ #### Freezing
150
+
151
+ To hold a spring in place (while maintaining its velocity), use the `freeze()` (and `unfreeze()`) methods.
152
+ Calling `freeze()` will set the spring's `state` to `'frozen'`
153
+
154
+ ```typescript
155
+ const spring = createSpring(0, { mass: 1, tension: 100, friction: 20 })
156
+
157
+ spring.target = 100
158
+
159
+ console.log(spring.value) // 0
160
+
161
+ spring.freeze()
162
+ spring.simulate(1000)
163
+
164
+ console.log(spring.value) // 0
165
+ ```
166
+
167
+ ### Creating a Spring System
168
+
169
+ Manually updating every spring with its `simulate` method would be tedious to say the least. Where possible, prefer
170
+ using a `SpringSystem` to group springs together and provide a single call site for updating each spring:
171
+
172
+ ```typescript
173
+ import { createSpringSystem } from 'coily'
174
+
175
+ // First, create the system
176
+ const system = createSpringSystem()
177
+
178
+ // Then, create some springs
179
+ const spring1 = system.createSpring(0, { mass: 1, tension: 100, friction: 20 })
180
+ const spring2 = system.createSpring(0, { mass: 0.25, tension: 50, friction: 10 })
181
+
182
+ // Simulate all springs at once from the system
183
+ system.simulate(16)
184
+ ```
185
+
186
+ Should a spring become irrelevant for some reason, remember to call the `cleanup` method so that the system stops
187
+ updating it:
188
+
189
+ ```typescript
190
+ import { createSpringSystem } from 'coily'
191
+
192
+ const system = createSpringSystem()
193
+
194
+ const spring = system.createSpring(0, { mass: 1, tension: 100, friction: 20 })
195
+
196
+ // Later
197
+
198
+ system.cleanup(spring)
199
+ ```
200
+
201
+ ### Spring Chains
202
+
203
+ Sometimes the `target` of one spring should be the `value` of another.
204
+ This can be tedious to set up manually and involves juggling multiple updates per frame to get a good look at the
205
+ current state.
206
+
207
+ Spring Chains help with this problem by building on the `createSpring` API. You'll still supply an `initial` target,
208
+ a `SpringConfig`, and an optional `SpringOptions`. You'll also pass an array of chain "links" that describe additional
209
+ values in the chain.
210
+
211
+ The links take one of two forms:
212
+
213
+ - A fixed, additive offset from the previous value, e.g. `50` will make Spring 2 target Spring 1's current value plus 50.
214
+ - A function with the following signature:
215
+ ```typescript
216
+ const link = (
217
+ previousSpringValue: number,
218
+ previousSpringTarget: number,
219
+ currentSpringValue?: number,
220
+ currentSpringTarget?: number
221
+ ): number => /* ... */
222
+ ```
223
+ The `currentSpringTarget` and `currentSpringValue` will be `undefined` the first time the function is called, as the
224
+ targets are initialized for all springs.
225
+
226
+ #### Creating
227
+
228
+ ```typescript
229
+ const chain = createSpringChain(
230
+ 0, // initial
231
+ [50, 50, (value) => value * 1.25],
232
+ { mass: 10, tension: 3000, friction: 300 } // config
233
+ )
234
+ ```
235
+
236
+ #### Reading State
237
+
238
+ Spring Chains have almost the same interface as Springs do.
239
+
240
+ Instead of a single `value`, they have an array of numbers as their value. The same goes for `targets`, `states`,
241
+ and `velocities`. Each of these arrays are updated _in place_, which means it is safe to grab a reference to the value
242
+ and read from it later.
243
+
244
+ Additionally, Spring Chains have a singular `state` property which indicates if _any_ of its links are in a `'moving'` state or not.
245
+
246
+ There is also a singular `target` property which returns the first target in the chain. Setting this will update the springs as well.
247
+
248
+ ```typescript
249
+ chain.values // readonly number[]
250
+ chain.velocities // readonly number[]
251
+ chain.targets // readonly number[]
252
+ chain.states // readonly number[]
253
+
254
+ chain.target = 10 // get and set the first target
255
+ chain.state // if any children are 'moving', this will be moving. if the chain is frozen, it will be 'frozen'. otherwise, it is 'resting'.
256
+ ```
package/dist/index.cjs ADDED
@@ -0,0 +1,317 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod));
21
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
22
+
23
+ // src/index.ts
24
+ var src_exports = {};
25
+ __export(src_exports, {
26
+ createSpring: () => createSpring,
27
+ createSpringChain: () => createSpringChain,
28
+ createSpringSystem: () => createSpringSystem
29
+ });
30
+ module.exports = __toCommonJS(src_exports);
31
+
32
+ // src/lib/createSpring.ts
33
+ var import_mitt = __toESM(require("mitt"), 1);
34
+ var getArrivalFunction = (behavior) => {
35
+ let result;
36
+ if (typeof behavior === "function") {
37
+ result = behavior;
38
+ } else {
39
+ let scalar;
40
+ if (typeof behavior === "number") {
41
+ scalar = behavior;
42
+ } else if (behavior === "bounce") {
43
+ scalar = -1;
44
+ } else if (behavior === "clamp") {
45
+ scalar = 0;
46
+ } else if (behavior === "none") {
47
+ scalar = 1;
48
+ }
49
+ result = (velocity) => velocity * scalar;
50
+ }
51
+ return result;
52
+ };
53
+ var createSpringImpl = (initial, config, options) => {
54
+ let state = "resting";
55
+ let velocity = 0;
56
+ let target = initial;
57
+ let value = initial;
58
+ const restingDistance = Math.max((options == null ? void 0 : options.restingDistance) ?? 1e-4, 0);
59
+ const restingVelocity = Math.max((options == null ? void 0 : options.restingVelocity) ?? 1e-4, 0);
60
+ const arrivalFunction = getArrivalFunction((options == null ? void 0 : options.arrivalBehavior) ?? 1);
61
+ const emitter = (0, import_mitt.default)();
62
+ const { emit, ...emitterApi } = emitter;
63
+ const spring = {
64
+ ...emitterApi,
65
+ get target() {
66
+ return target;
67
+ },
68
+ set target(val) {
69
+ target = val;
70
+ if (Math.abs(value - target) > restingDistance) {
71
+ state = "moving";
72
+ emit("update:state", state);
73
+ }
74
+ },
75
+ get value() {
76
+ return value;
77
+ },
78
+ get velocity() {
79
+ return velocity;
80
+ },
81
+ get state() {
82
+ return state;
83
+ },
84
+ get config() {
85
+ return config;
86
+ },
87
+ set config(value2) {
88
+ config = value2;
89
+ },
90
+ freeze: () => {
91
+ state = "frozen";
92
+ emit("update:state", state);
93
+ },
94
+ unfreeze: () => {
95
+ state = Math.abs(velocity) > restingVelocity || Math.abs(value - target) > restingDistance ? "moving" : "resting";
96
+ emit("update:state", state);
97
+ }
98
+ };
99
+ const simulate = (deltaTime) => {
100
+ if (state === "moving") {
101
+ const iterations = Math.ceil(deltaTime);
102
+ for (let n = 0; n < iterations; n++) {
103
+ const previousDelta = value - target;
104
+ const springForce = -config.tension * 1e-6 * previousDelta;
105
+ const dampingForce = -config.friction * 1e-3 * velocity;
106
+ const acceleration = (springForce + dampingForce) / config.mass;
107
+ velocity = velocity + acceleration;
108
+ value = value + velocity;
109
+ const delta = value - target;
110
+ const absDelta = Math.abs(delta);
111
+ if (Math.sign(delta) !== Math.sign(previousDelta)) {
112
+ velocity = arrivalFunction(velocity * 1e3) * 1e-3;
113
+ value = target + velocity * (absDelta / (absDelta + Math.abs(previousDelta)));
114
+ }
115
+ if (absDelta < restingDistance && Math.abs(velocity) < restingVelocity) {
116
+ value = target;
117
+ state = "resting";
118
+ emit("update:state", state);
119
+ break;
120
+ }
121
+ }
122
+ emit("update:value", value);
123
+ }
124
+ };
125
+ return [spring, simulate];
126
+ };
127
+
128
+ // src/lib/createSpringChain.ts
129
+ var import_mitt2 = __toESM(require("mitt"), 1);
130
+ var createSpringChainImpl = (initial, links, config, options) => {
131
+ const simulateFns = [];
132
+ const springs = [];
133
+ const targets = [];
134
+ const values = [];
135
+ const velocities = [];
136
+ const states = [];
137
+ let currentTarget = initial;
138
+ let currentLinks = links;
139
+ let state = "resting";
140
+ const updateChainState = (target, links2) => {
141
+ if (springs.length === 0) {
142
+ const [spring, simulate2] = createSpringImpl(target, config, options);
143
+ springs.push(spring);
144
+ simulateFns.push(simulate2);
145
+ targets.push(spring.target);
146
+ values.push(spring.value);
147
+ velocities.push(spring.velocity);
148
+ states.push(spring.state);
149
+ } else {
150
+ springs[0].target = target;
151
+ targets[0] = target;
152
+ states[0] = springs[0].state;
153
+ if (state === "resting" && springs[0].state === "moving") {
154
+ state = "moving";
155
+ }
156
+ }
157
+ for (const [index, link] of links2.entries()) {
158
+ const previousSpring = springs[index];
159
+ if (springs[index + 1]) {
160
+ const spring = springs[index + 1];
161
+ if (typeof link === "number") {
162
+ spring.target = previousSpring.value + link;
163
+ } else {
164
+ spring.target = link(previousSpring.value, previousSpring.target, spring.value, spring.target);
165
+ }
166
+ targets[index + 1] = spring.target;
167
+ states[index + 1] = spring.state;
168
+ if (state === "resting" && spring.state === "moving") {
169
+ state = "moving";
170
+ }
171
+ } else {
172
+ const [spring, simulate2] = createSpringImpl(typeof link === "number" ? previousSpring.value + link : link(previousSpring.value, previousSpring.target), config, options);
173
+ springs.push(spring);
174
+ simulateFns.push(simulate2);
175
+ targets[index + 1] = spring.target;
176
+ values[index + 1] = spring.value;
177
+ velocities[index + 1] = spring.velocity;
178
+ states[index + 1] = spring.state;
179
+ }
180
+ }
181
+ const cutoff = springs.length + 1;
182
+ if (springs.length > cutoff) {
183
+ const deleteCount = springs.length - cutoff;
184
+ springs.splice(cutoff, deleteCount);
185
+ simulateFns.splice(cutoff, deleteCount);
186
+ targets.splice(cutoff, deleteCount);
187
+ values.splice(cutoff, deleteCount);
188
+ velocities.splice(cutoff, deleteCount);
189
+ states.splice(cutoff, deleteCount);
190
+ }
191
+ };
192
+ updateChainState(currentTarget, currentLinks);
193
+ const emitter = (0, import_mitt2.default)();
194
+ const { emit, ...emitterApi } = emitter;
195
+ const simulate = (delta) => {
196
+ if (state === "moving") {
197
+ let moving = false;
198
+ for (let i = 0; i < springs.length; i++) {
199
+ simulateFns[i](delta);
200
+ if (i < springs.length - 1) {
201
+ const link = links[i];
202
+ springs[i + 1].target = typeof link === "number" ? springs[i].value + link : link(springs[i].value, springs[i].target, springs[i + 1].value);
203
+ }
204
+ targets[i] = springs[i].target;
205
+ values[i] = springs[i].value;
206
+ velocities[i] = springs[i].velocity;
207
+ states[i] = springs[i].state;
208
+ moving = moving || springs[i].state === "moving";
209
+ }
210
+ emit("update:value", values);
211
+ if (!moving) {
212
+ state = "resting";
213
+ emit("update:state", state);
214
+ }
215
+ }
216
+ };
217
+ return [
218
+ {
219
+ ...emitterApi,
220
+ get target() {
221
+ return targets[0];
222
+ },
223
+ set target(val) {
224
+ currentTarget = val;
225
+ updateChainState(currentTarget, currentLinks);
226
+ },
227
+ get targets() {
228
+ return targets;
229
+ },
230
+ get links() {
231
+ return links;
232
+ },
233
+ set links(val) {
234
+ currentLinks = val;
235
+ updateChainState(currentTarget, currentLinks);
236
+ },
237
+ get config() {
238
+ return config;
239
+ },
240
+ get values() {
241
+ return values;
242
+ },
243
+ get velocities() {
244
+ return velocities;
245
+ },
246
+ get state() {
247
+ return state;
248
+ },
249
+ get states() {
250
+ return states;
251
+ },
252
+ freeze: () => {
253
+ for (const spring of springs) {
254
+ spring.freeze();
255
+ }
256
+ state = "frozen";
257
+ emit("update:state", state);
258
+ },
259
+ unfreeze: () => {
260
+ let moving = false;
261
+ for (const spring of springs) {
262
+ spring.unfreeze();
263
+ moving = moving || spring.state === "moving";
264
+ }
265
+ state = moving ? "moving" : "resting";
266
+ emit("update:state", state);
267
+ }
268
+ },
269
+ simulate
270
+ ];
271
+ };
272
+
273
+ // src/createSpring.ts
274
+ var createSpring = (initial, config, options) => {
275
+ const [spring, simulate] = createSpringImpl(initial, config, options);
276
+ return Object.assign(spring, { simulate });
277
+ };
278
+
279
+ // src/createSpringChain.ts
280
+ var createSpringChain = (initial, links, config, options) => {
281
+ const [chain, simulate] = createSpringChainImpl(initial, links, config, options);
282
+ return Object.assign(chain, { simulate });
283
+ };
284
+
285
+ // src/createSpringSystem.ts
286
+ var createSpringSystem = () => {
287
+ const springs = /* @__PURE__ */ new Map();
288
+ return {
289
+ createSpring: (initial, config, options) => {
290
+ const [spring, simulate] = createSpringImpl(initial, config, options);
291
+ springs.set(spring, simulate);
292
+ return spring;
293
+ },
294
+ createSpringChain: (initial, links, config, options) => {
295
+ const [chain, simulate] = createSpringChainImpl(initial, links, config, options);
296
+ springs.set(chain, simulate);
297
+ return chain;
298
+ },
299
+ cleanup: (spring) => {
300
+ springs.delete(spring);
301
+ spring.off("*");
302
+ spring.off("update:state");
303
+ spring.off("update:value");
304
+ },
305
+ simulate: (delta) => {
306
+ for (const simulate of springs.values()) {
307
+ simulate(delta);
308
+ }
309
+ }
310
+ };
311
+ };
312
+ // Annotate the CommonJS export names for ESM import in node:
313
+ 0 && (module.exports = {
314
+ createSpring,
315
+ createSpringChain,
316
+ createSpringSystem
317
+ });
@@ -0,0 +1,67 @@
1
+ import { Emitter } from 'mitt';
2
+
3
+ interface SpringConfig {
4
+ mass: number;
5
+ tension: number;
6
+ friction: number;
7
+ }
8
+ declare type SpringArrivalBehavior = 'bounce' | 'clamp' | 'none' | number | ((velocity: number) => number);
9
+ interface SpringOptions {
10
+ restingDistance?: number;
11
+ restingVelocity?: number;
12
+ arrivalBehavior?: SpringArrivalBehavior;
13
+ }
14
+ declare type SpringState = 'moving' | 'frozen' | 'resting';
15
+ declare type SpringEmitter = Emitter<{
16
+ 'update:value': number;
17
+ 'update:state': SpringState;
18
+ '*': number | SpringState;
19
+ }>;
20
+ interface Spring extends Omit<SpringEmitter, 'emit'> {
21
+ target: number;
22
+ config: Readonly<SpringConfig>;
23
+ readonly value: number;
24
+ readonly velocity: number;
25
+ readonly state: SpringState;
26
+ readonly freeze: () => void;
27
+ readonly unfreeze: () => void;
28
+ }
29
+ declare type SimulateFn = (delta: number) => void;
30
+ interface StandaloneSpring extends Spring {
31
+ readonly simulate: SimulateFn;
32
+ }
33
+ declare type SpringChainLinkGetter = number | ((previousValue: number, previousTarget: number, currentValue?: number, currentTarget?: number) => number);
34
+ declare type SpringChainEmitter = Emitter<{
35
+ 'update:value': number[];
36
+ 'update:state': SpringState;
37
+ '*': number[] | SpringState;
38
+ }>;
39
+ interface SpringChain extends Omit<SpringChainEmitter, 'emit'> {
40
+ target: number;
41
+ links: SpringChainLinkGetter[];
42
+ config: Readonly<SpringConfig>;
43
+ readonly targets: ReadonlyArray<number>;
44
+ readonly values: ReadonlyArray<number>;
45
+ readonly velocities: ReadonlyArray<number>;
46
+ readonly state: SpringState;
47
+ readonly states: ReadonlyArray<SpringState>;
48
+ readonly freeze: () => void;
49
+ readonly unfreeze: () => void;
50
+ }
51
+ interface StandaloneSpringChain extends SpringChain {
52
+ readonly simulate: SimulateFn;
53
+ }
54
+ interface SpringSystem {
55
+ createSpring: (initial: number, config: SpringConfig, options?: SpringOptions) => Spring;
56
+ createSpringChain: (initial: number, links: SpringChainLinkGetter[], config: SpringConfig, options?: SpringOptions) => SpringChain;
57
+ cleanup: (spring: Spring | SpringChain) => void;
58
+ simulate: SimulateFn;
59
+ }
60
+
61
+ declare const createSpring: (initial: number, config: SpringConfig, options?: SpringOptions) => StandaloneSpring;
62
+
63
+ declare const createSpringChain: (initial: number, links: SpringChainLinkGetter[], config: SpringConfig, options?: SpringOptions) => StandaloneSpringChain;
64
+
65
+ declare const createSpringSystem: () => SpringSystem;
66
+
67
+ export { SimulateFn, Spring, SpringArrivalBehavior, SpringChain, SpringChainLinkGetter, SpringConfig, SpringOptions, SpringState, SpringSystem, StandaloneSpring, StandaloneSpringChain, createSpring, createSpringChain, createSpringSystem };
package/dist/index.js ADDED
@@ -0,0 +1,285 @@
1
+ // src/lib/createSpring.ts
2
+ import mitt from "mitt";
3
+ var getArrivalFunction = (behavior) => {
4
+ let result;
5
+ if (typeof behavior === "function") {
6
+ result = behavior;
7
+ } else {
8
+ let scalar;
9
+ if (typeof behavior === "number") {
10
+ scalar = behavior;
11
+ } else if (behavior === "bounce") {
12
+ scalar = -1;
13
+ } else if (behavior === "clamp") {
14
+ scalar = 0;
15
+ } else if (behavior === "none") {
16
+ scalar = 1;
17
+ }
18
+ result = (velocity) => velocity * scalar;
19
+ }
20
+ return result;
21
+ };
22
+ var createSpringImpl = (initial, config, options) => {
23
+ let state = "resting";
24
+ let velocity = 0;
25
+ let target = initial;
26
+ let value = initial;
27
+ const restingDistance = Math.max((options == null ? void 0 : options.restingDistance) ?? 1e-4, 0);
28
+ const restingVelocity = Math.max((options == null ? void 0 : options.restingVelocity) ?? 1e-4, 0);
29
+ const arrivalFunction = getArrivalFunction((options == null ? void 0 : options.arrivalBehavior) ?? 1);
30
+ const emitter = mitt();
31
+ const { emit, ...emitterApi } = emitter;
32
+ const spring = {
33
+ ...emitterApi,
34
+ get target() {
35
+ return target;
36
+ },
37
+ set target(val) {
38
+ target = val;
39
+ if (Math.abs(value - target) > restingDistance) {
40
+ state = "moving";
41
+ emit("update:state", state);
42
+ }
43
+ },
44
+ get value() {
45
+ return value;
46
+ },
47
+ get velocity() {
48
+ return velocity;
49
+ },
50
+ get state() {
51
+ return state;
52
+ },
53
+ get config() {
54
+ return config;
55
+ },
56
+ set config(value2) {
57
+ config = value2;
58
+ },
59
+ freeze: () => {
60
+ state = "frozen";
61
+ emit("update:state", state);
62
+ },
63
+ unfreeze: () => {
64
+ state = Math.abs(velocity) > restingVelocity || Math.abs(value - target) > restingDistance ? "moving" : "resting";
65
+ emit("update:state", state);
66
+ }
67
+ };
68
+ const simulate = (deltaTime) => {
69
+ if (state === "moving") {
70
+ const iterations = Math.ceil(deltaTime);
71
+ for (let n = 0; n < iterations; n++) {
72
+ const previousDelta = value - target;
73
+ const springForce = -config.tension * 1e-6 * previousDelta;
74
+ const dampingForce = -config.friction * 1e-3 * velocity;
75
+ const acceleration = (springForce + dampingForce) / config.mass;
76
+ velocity = velocity + acceleration;
77
+ value = value + velocity;
78
+ const delta = value - target;
79
+ const absDelta = Math.abs(delta);
80
+ if (Math.sign(delta) !== Math.sign(previousDelta)) {
81
+ velocity = arrivalFunction(velocity * 1e3) * 1e-3;
82
+ value = target + velocity * (absDelta / (absDelta + Math.abs(previousDelta)));
83
+ }
84
+ if (absDelta < restingDistance && Math.abs(velocity) < restingVelocity) {
85
+ value = target;
86
+ state = "resting";
87
+ emit("update:state", state);
88
+ break;
89
+ }
90
+ }
91
+ emit("update:value", value);
92
+ }
93
+ };
94
+ return [spring, simulate];
95
+ };
96
+
97
+ // src/lib/createSpringChain.ts
98
+ import mitt2 from "mitt";
99
+ var createSpringChainImpl = (initial, links, config, options) => {
100
+ const simulateFns = [];
101
+ const springs = [];
102
+ const targets = [];
103
+ const values = [];
104
+ const velocities = [];
105
+ const states = [];
106
+ let currentTarget = initial;
107
+ let currentLinks = links;
108
+ let state = "resting";
109
+ const updateChainState = (target, links2) => {
110
+ if (springs.length === 0) {
111
+ const [spring, simulate2] = createSpringImpl(target, config, options);
112
+ springs.push(spring);
113
+ simulateFns.push(simulate2);
114
+ targets.push(spring.target);
115
+ values.push(spring.value);
116
+ velocities.push(spring.velocity);
117
+ states.push(spring.state);
118
+ } else {
119
+ springs[0].target = target;
120
+ targets[0] = target;
121
+ states[0] = springs[0].state;
122
+ if (state === "resting" && springs[0].state === "moving") {
123
+ state = "moving";
124
+ }
125
+ }
126
+ for (const [index, link] of links2.entries()) {
127
+ const previousSpring = springs[index];
128
+ if (springs[index + 1]) {
129
+ const spring = springs[index + 1];
130
+ if (typeof link === "number") {
131
+ spring.target = previousSpring.value + link;
132
+ } else {
133
+ spring.target = link(previousSpring.value, previousSpring.target, spring.value, spring.target);
134
+ }
135
+ targets[index + 1] = spring.target;
136
+ states[index + 1] = spring.state;
137
+ if (state === "resting" && spring.state === "moving") {
138
+ state = "moving";
139
+ }
140
+ } else {
141
+ const [spring, simulate2] = createSpringImpl(typeof link === "number" ? previousSpring.value + link : link(previousSpring.value, previousSpring.target), config, options);
142
+ springs.push(spring);
143
+ simulateFns.push(simulate2);
144
+ targets[index + 1] = spring.target;
145
+ values[index + 1] = spring.value;
146
+ velocities[index + 1] = spring.velocity;
147
+ states[index + 1] = spring.state;
148
+ }
149
+ }
150
+ const cutoff = springs.length + 1;
151
+ if (springs.length > cutoff) {
152
+ const deleteCount = springs.length - cutoff;
153
+ springs.splice(cutoff, deleteCount);
154
+ simulateFns.splice(cutoff, deleteCount);
155
+ targets.splice(cutoff, deleteCount);
156
+ values.splice(cutoff, deleteCount);
157
+ velocities.splice(cutoff, deleteCount);
158
+ states.splice(cutoff, deleteCount);
159
+ }
160
+ };
161
+ updateChainState(currentTarget, currentLinks);
162
+ const emitter = mitt2();
163
+ const { emit, ...emitterApi } = emitter;
164
+ const simulate = (delta) => {
165
+ if (state === "moving") {
166
+ let moving = false;
167
+ for (let i = 0; i < springs.length; i++) {
168
+ simulateFns[i](delta);
169
+ if (i < springs.length - 1) {
170
+ const link = links[i];
171
+ springs[i + 1].target = typeof link === "number" ? springs[i].value + link : link(springs[i].value, springs[i].target, springs[i + 1].value);
172
+ }
173
+ targets[i] = springs[i].target;
174
+ values[i] = springs[i].value;
175
+ velocities[i] = springs[i].velocity;
176
+ states[i] = springs[i].state;
177
+ moving = moving || springs[i].state === "moving";
178
+ }
179
+ emit("update:value", values);
180
+ if (!moving) {
181
+ state = "resting";
182
+ emit("update:state", state);
183
+ }
184
+ }
185
+ };
186
+ return [
187
+ {
188
+ ...emitterApi,
189
+ get target() {
190
+ return targets[0];
191
+ },
192
+ set target(val) {
193
+ currentTarget = val;
194
+ updateChainState(currentTarget, currentLinks);
195
+ },
196
+ get targets() {
197
+ return targets;
198
+ },
199
+ get links() {
200
+ return links;
201
+ },
202
+ set links(val) {
203
+ currentLinks = val;
204
+ updateChainState(currentTarget, currentLinks);
205
+ },
206
+ get config() {
207
+ return config;
208
+ },
209
+ get values() {
210
+ return values;
211
+ },
212
+ get velocities() {
213
+ return velocities;
214
+ },
215
+ get state() {
216
+ return state;
217
+ },
218
+ get states() {
219
+ return states;
220
+ },
221
+ freeze: () => {
222
+ for (const spring of springs) {
223
+ spring.freeze();
224
+ }
225
+ state = "frozen";
226
+ emit("update:state", state);
227
+ },
228
+ unfreeze: () => {
229
+ let moving = false;
230
+ for (const spring of springs) {
231
+ spring.unfreeze();
232
+ moving = moving || spring.state === "moving";
233
+ }
234
+ state = moving ? "moving" : "resting";
235
+ emit("update:state", state);
236
+ }
237
+ },
238
+ simulate
239
+ ];
240
+ };
241
+
242
+ // src/createSpring.ts
243
+ var createSpring = (initial, config, options) => {
244
+ const [spring, simulate] = createSpringImpl(initial, config, options);
245
+ return Object.assign(spring, { simulate });
246
+ };
247
+
248
+ // src/createSpringChain.ts
249
+ var createSpringChain = (initial, links, config, options) => {
250
+ const [chain, simulate] = createSpringChainImpl(initial, links, config, options);
251
+ return Object.assign(chain, { simulate });
252
+ };
253
+
254
+ // src/createSpringSystem.ts
255
+ var createSpringSystem = () => {
256
+ const springs = /* @__PURE__ */ new Map();
257
+ return {
258
+ createSpring: (initial, config, options) => {
259
+ const [spring, simulate] = createSpringImpl(initial, config, options);
260
+ springs.set(spring, simulate);
261
+ return spring;
262
+ },
263
+ createSpringChain: (initial, links, config, options) => {
264
+ const [chain, simulate] = createSpringChainImpl(initial, links, config, options);
265
+ springs.set(chain, simulate);
266
+ return chain;
267
+ },
268
+ cleanup: (spring) => {
269
+ springs.delete(spring);
270
+ spring.off("*");
271
+ spring.off("update:state");
272
+ spring.off("update:value");
273
+ },
274
+ simulate: (delta) => {
275
+ for (const simulate of springs.values()) {
276
+ simulate(delta);
277
+ }
278
+ }
279
+ };
280
+ };
281
+ export {
282
+ createSpring,
283
+ createSpringChain,
284
+ createSpringSystem
285
+ };
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "coily",
3
+ "version": "0.1.1",
4
+ "description": "Simulate values using spring physics",
5
+ "author": "Tim Morris <timthewebguy@gmail.com>",
6
+ "homepage": "https://github.com/tkofh/coily#readme",
7
+ "license": "MIT",
8
+ "sideEffects": false,
9
+ "type": "module",
10
+ "main": "./dist/index.cjs",
11
+ "module": "./dist/index.js",
12
+ "types": "./dist/index.d.ts",
13
+ "exports": {
14
+ ".": {
15
+ "require": "./dist/index.cjs",
16
+ "import": "./dist/index.js",
17
+ "types": "./dist/index.d.ts"
18
+ }
19
+ },
20
+ "files": [
21
+ "dist"
22
+ ],
23
+ "publishConfig": {
24
+ "access": "public"
25
+ },
26
+ "repository": {
27
+ "type": "git",
28
+ "url": "git+https://github.com/tkofh/coily.git"
29
+ },
30
+ "scripts": {
31
+ "build": "tsup",
32
+ "test": "vitest run",
33
+ "test:watch": "vitest watch",
34
+ "test:ui": "vitest --ui",
35
+ "prepublish": "yarn test && yarn build"
36
+ },
37
+ "bugs": {
38
+ "url": "https://github.com/tkofh/coily/issues"
39
+ },
40
+ "devDependencies": {
41
+ "@vitest/ui": "^0.11.0",
42
+ "tsup": "^6.1.3",
43
+ "vitest": "^0.16.0"
44
+ },
45
+ "dependencies": {
46
+ "mitt": "^3.0.0"
47
+ },
48
+ "gitHead": "ed6733ca1768b44341f0ed05f8750466089ddbc1"
49
+ }