UNPKG

7.1 kBJavaScriptView Raw
1import { writable } from '../store/index.mjs';
2import { now, loop, assign } from '../internal/index.mjs';
3import { linear } from '../easing/index.mjs';
4
5function is_date(obj) {
6 return Object.prototype.toString.call(obj) === '[object Date]';
7}
8
9function tick_spring(ctx, last_value, current_value, target_value) {
10 if (typeof current_value === 'number' || is_date(current_value)) {
11 // @ts-ignore
12 const delta = target_value - current_value;
13 // @ts-ignore
14 const velocity = (current_value - last_value) / (ctx.dt || 1 / 60); // guard div by 0
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; // settled
21 }
22 else {
23 ctx.settled = false; // signal loop to keep ticking
24 // @ts-ignore
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 // @ts-ignore
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 // @ts-ignore
37 next_value[k] = tick_spring(ctx, last_value[k], current_value[k], target_value[k]);
38 }
39 // @ts-ignore
40 return next_value;
41 }
42 else {
43 throw new Error(`Cannot spring ${typeof current_value} values`);
44 }
45}
46function 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; // cancel any running animation
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; // infinite mass, unaffected by spring forces
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
116function 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}
157function 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 // @ts-ignore
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
211export { spring, tweened };