UNPKG

6.79 kBJavaScriptView Raw
1import { writable } from '../store';
2import { now, loop, assign } from '../internal';
3import { linear } from '../easing';
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 // @ts-ignore
39 return next_value;
40 }
41 else {
42 throw new Error(`Cannot spring ${typeof current_value} values`);
43 }
44}
45function 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; // cancel any running animation
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; // infinite mass, unaffected by spring forces
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
114function 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}
155function 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 const start = now() + delay;
169 let fn;
170 task = loop(now => {
171 if (now < start)
172 return true;
173 if (!started) {
174 fn = interpolate(value, new_value);
175 if (typeof duration === 'function')
176 duration = duration(value, new_value);
177 started = true;
178 }
179 if (previous_task) {
180 previous_task.abort();
181 previous_task = null;
182 }
183 const elapsed = now - start;
184 if (elapsed > duration) {
185 store.set(value = new_value);
186 return false;
187 }
188 // @ts-ignore
189 store.set(value = fn(easing(elapsed / duration)));
190 return true;
191 });
192 return task.promise;
193 }
194 return {
195 set,
196 update: (fn, opts) => set(fn(target_value, value), opts),
197 subscribe: store.subscribe
198 };
199}
200
201export { spring, tweened };