UNPKG

6.8 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 /* eslint-disable @typescript-eslint/no-use-before-define */
57 function set(new_value, opts = {}) {
58 target_value = new_value;
59 const token = current_token = {};
60 if (opts.hard || (spring.stiffness >= 1 && spring.damping >= 1)) {
61 cancel_task = true; // cancel any running animation
62 last_time = now();
63 last_value = value;
64 store.set(value = target_value);
65 return new Promise(f => f()); // fulfil immediately
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 return !ctx.settled;
95 });
96 }
97 return new Promise(fulfil => {
98 task.promise.then(() => {
99 if (token === current_token)
100 fulfil();
101 });
102 });
103 }
104 /* eslint-enable @typescript-eslint/no-use-before-define */
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 target_value = new_value;
163 let previous_task = task;
164 let started = false;
165 let { delay = 0, duration = 400, easing = linear, interpolate = get_interpolator } = assign(assign({}, defaults), opts);
166 const start = now() + delay;
167 let fn;
168 task = loop(now => {
169 if (now < start)
170 return true;
171 if (!started) {
172 fn = interpolate(value, new_value);
173 if (typeof duration === 'function')
174 duration = duration(value, new_value);
175 started = true;
176 }
177 if (previous_task) {
178 previous_task.abort();
179 previous_task = null;
180 }
181 const elapsed = now - start;
182 if (elapsed > duration) {
183 store.set(value = new_value);
184 return false;
185 }
186 // @ts-ignore
187 store.set(value = fn(easing(elapsed / duration)));
188 return true;
189 });
190 return task.promise;
191 }
192 return {
193 set,
194 update: (fn, opts) => set(fn(target_value, value), opts),
195 subscribe: store.subscribe
196 };
197}
198
199export { spring, tweened };