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