1 | import { writable } from '../store/index.mjs';
|
2 | import { now, loop, assign } from '../internal/index.mjs';
|
3 | import { linear } from '../easing/index.mjs';
|
4 |
|
5 | function is_date(obj) {
|
6 | return Object.prototype.toString.call(obj) === '[object Date]';
|
7 | }
|
8 |
|
9 | function tick_spring(ctx, last_value, current_value, target_value) {
|
10 | if (typeof current_value === 'number' || is_date(current_value)) {
|
11 |
|
12 | const delta = target_value - current_value;
|
13 |
|
14 | const velocity = (current_value - last_value) / (ctx.dt || 1 / 60);
|
15 | const spring = ctx.opts.stiffness * delta;
|
16 | const damper = ctx.opts.damping * velocity;
|
17 | const acceleration = (spring - damper) * ctx.inv_mass;
|
18 | const d = (velocity + acceleration) * ctx.dt;
|
19 | if (Math.abs(d) < ctx.opts.precision && Math.abs(delta) < ctx.opts.precision) {
|
20 | return target_value;
|
21 | }
|
22 | else {
|
23 | ctx.settled = false;
|
24 |
|
25 | return is_date(current_value) ?
|
26 | new Date(current_value.getTime() + d) : current_value + d;
|
27 | }
|
28 | }
|
29 | else if (Array.isArray(current_value)) {
|
30 |
|
31 | return current_value.map((_, i) => tick_spring(ctx, last_value[i], current_value[i], target_value[i]));
|
32 | }
|
33 | else if (typeof current_value === 'object') {
|
34 | const next_value = {};
|
35 | for (const k in current_value) {
|
36 |
|
37 | next_value[k] = tick_spring(ctx, last_value[k], current_value[k], target_value[k]);
|
38 | }
|
39 |
|
40 | return next_value;
|
41 | }
|
42 | else {
|
43 | throw new Error(`Cannot spring ${typeof current_value} values`);
|
44 | }
|
45 | }
|
46 | function spring(value, opts = {}) {
|
47 | const store = writable(value);
|
48 | const { stiffness = 0.15, damping = 0.8, precision = 0.01 } = opts;
|
49 | let last_time;
|
50 | let task;
|
51 | let current_token;
|
52 | let last_value = value;
|
53 | let target_value = value;
|
54 | let inv_mass = 1;
|
55 | let inv_mass_recovery_rate = 0;
|
56 | let cancel_task = false;
|
57 | function set(new_value, opts = {}) {
|
58 | target_value = new_value;
|
59 | const token = current_token = {};
|
60 | if (value == null || opts.hard || (spring.stiffness >= 1 && spring.damping >= 1)) {
|
61 | cancel_task = true;
|
62 | last_time = now();
|
63 | last_value = new_value;
|
64 | store.set(value = target_value);
|
65 | return Promise.resolve();
|
66 | }
|
67 | else if (opts.soft) {
|
68 | const rate = opts.soft === true ? .5 : +opts.soft;
|
69 | inv_mass_recovery_rate = 1 / (rate * 60);
|
70 | inv_mass = 0;
|
71 | }
|
72 | if (!task) {
|
73 | last_time = now();
|
74 | cancel_task = false;
|
75 | task = loop(now => {
|
76 | if (cancel_task) {
|
77 | cancel_task = false;
|
78 | task = null;
|
79 | return false;
|
80 | }
|
81 | inv_mass = Math.min(inv_mass + inv_mass_recovery_rate, 1);
|
82 | const ctx = {
|
83 | inv_mass,
|
84 | opts: spring,
|
85 | settled: true,
|
86 | dt: (now - last_time) * 60 / 1000
|
87 | };
|
88 | const next_value = tick_spring(ctx, last_value, value, target_value);
|
89 | last_time = now;
|
90 | last_value = value;
|
91 | store.set(value = next_value);
|
92 | if (ctx.settled) {
|
93 | task = null;
|
94 | }
|
95 | return !ctx.settled;
|
96 | });
|
97 | }
|
98 | return new Promise(fulfil => {
|
99 | task.promise.then(() => {
|
100 | if (token === current_token)
|
101 | fulfil();
|
102 | });
|
103 | });
|
104 | }
|
105 | const spring = {
|
106 | set,
|
107 | update: (fn, opts) => set(fn(target_value, value), opts),
|
108 | subscribe: store.subscribe,
|
109 | stiffness,
|
110 | damping,
|
111 | precision
|
112 | };
|
113 | return spring;
|
114 | }
|
115 |
|
116 | function get_interpolator(a, b) {
|
117 | if (a === b || a !== a)
|
118 | return () => a;
|
119 | const type = typeof a;
|
120 | if (type !== typeof b || Array.isArray(a) !== Array.isArray(b)) {
|
121 | throw new Error('Cannot interpolate values of different type');
|
122 | }
|
123 | if (Array.isArray(a)) {
|
124 | const arr = b.map((bi, i) => {
|
125 | return get_interpolator(a[i], bi);
|
126 | });
|
127 | return t => arr.map(fn => fn(t));
|
128 | }
|
129 | if (type === 'object') {
|
130 | if (!a || !b)
|
131 | throw new Error('Object cannot be null');
|
132 | if (is_date(a) && is_date(b)) {
|
133 | a = a.getTime();
|
134 | b = b.getTime();
|
135 | const delta = b - a;
|
136 | return t => new Date(a + t * delta);
|
137 | }
|
138 | const keys = Object.keys(b);
|
139 | const interpolators = {};
|
140 | keys.forEach(key => {
|
141 | interpolators[key] = get_interpolator(a[key], b[key]);
|
142 | });
|
143 | return t => {
|
144 | const result = {};
|
145 | keys.forEach(key => {
|
146 | result[key] = interpolators[key](t);
|
147 | });
|
148 | return result;
|
149 | };
|
150 | }
|
151 | if (type === 'number') {
|
152 | const delta = b - a;
|
153 | return t => a + t * delta;
|
154 | }
|
155 | throw new Error(`Cannot interpolate ${type} values`);
|
156 | }
|
157 | function tweened(value, defaults = {}) {
|
158 | const store = writable(value);
|
159 | let task;
|
160 | let target_value = value;
|
161 | function set(new_value, opts) {
|
162 | if (value == null) {
|
163 | store.set(value = new_value);
|
164 | return Promise.resolve();
|
165 | }
|
166 | target_value = new_value;
|
167 | let previous_task = task;
|
168 | let started = false;
|
169 | let { delay = 0, duration = 400, easing = linear, interpolate = get_interpolator } = assign(assign({}, defaults), opts);
|
170 | if (duration === 0) {
|
171 | if (previous_task) {
|
172 | previous_task.abort();
|
173 | previous_task = null;
|
174 | }
|
175 | store.set(value = target_value);
|
176 | return Promise.resolve();
|
177 | }
|
178 | const start = now() + delay;
|
179 | let fn;
|
180 | task = loop(now => {
|
181 | if (now < start)
|
182 | return true;
|
183 | if (!started) {
|
184 | fn = interpolate(value, new_value);
|
185 | if (typeof duration === 'function')
|
186 | duration = duration(value, new_value);
|
187 | started = true;
|
188 | }
|
189 | if (previous_task) {
|
190 | previous_task.abort();
|
191 | previous_task = null;
|
192 | }
|
193 | const elapsed = now - start;
|
194 | if (elapsed > duration) {
|
195 | store.set(value = new_value);
|
196 | return false;
|
197 | }
|
198 |
|
199 | store.set(value = fn(easing(elapsed / duration)));
|
200 | return true;
|
201 | });
|
202 | return task.promise;
|
203 | }
|
204 | return {
|
205 | set,
|
206 | update: (fn, opts) => set(fn(target_value, value), opts),
|
207 | subscribe: store.subscribe
|
208 | };
|
209 | }
|
210 |
|
211 | export { spring, tweened };
|