File size: 4,265 Bytes
bc20498 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 |
import { writable } from '../store/index.js';
import { loop, now } from '../internal/index.js';
import { is_date } from './utils.js';
/**
* @template T
* @param {import('./private.js').TickContext<T>} ctx
* @param {T} last_value
* @param {T} current_value
* @param {T} target_value
* @returns {T}
*/
function tick_spring(ctx, last_value, current_value, target_value) {
if (typeof current_value === 'number' || is_date(current_value)) {
// @ts-ignore
const delta = target_value - current_value;
// @ts-ignore
const velocity = (current_value - last_value) / (ctx.dt || 1 / 60); // guard div by 0
const spring = ctx.opts.stiffness * delta;
const damper = ctx.opts.damping * velocity;
const acceleration = (spring - damper) * ctx.inv_mass;
const d = (velocity + acceleration) * ctx.dt;
if (Math.abs(d) < ctx.opts.precision && Math.abs(delta) < ctx.opts.precision) {
return target_value; // settled
} else {
ctx.settled = false; // signal loop to keep ticking
// @ts-ignore
return is_date(current_value) ? new Date(current_value.getTime() + d) : current_value + d;
}
} else if (Array.isArray(current_value)) {
// @ts-ignore
return current_value.map((_, i) =>
tick_spring(ctx, last_value[i], current_value[i], target_value[i])
);
} else if (typeof current_value === 'object') {
const next_value = {};
for (const k in current_value) {
// @ts-ignore
next_value[k] = tick_spring(ctx, last_value[k], current_value[k], target_value[k]);
}
// @ts-ignore
return next_value;
} else {
throw new Error(`Cannot spring ${typeof current_value} values`);
}
}
/**
* The spring function in Svelte creates a store whose value is animated, with a motion that simulates the behavior of a spring. This means when the value changes, instead of transitioning at a steady rate, it "bounces" like a spring would, depending on the physics parameters provided. This adds a level of realism to the transitions and can enhance the user experience.
*
* https://svelte.dev/docs/svelte-motion#spring
* @template [T=any]
* @param {T} [value]
* @param {import('./private.js').SpringOpts} [opts]
* @returns {import('./public.js').Spring<T>}
*/
export function spring(value, opts = {}) {
const store = writable(value);
const { stiffness = 0.15, damping = 0.8, precision = 0.01 } = opts;
/** @type {number} */
let last_time;
/** @type {import('../internal/private.js').Task} */
let task;
/** @type {object} */
let current_token;
/** @type {T} */
let last_value = value;
/** @type {T} */
let target_value = value;
let inv_mass = 1;
let inv_mass_recovery_rate = 0;
let cancel_task = false;
/**
* @param {T} new_value
* @param {import('./private.js').SpringUpdateOpts} opts
* @returns {Promise<void>}
*/
function set(new_value, opts = {}) {
target_value = new_value;
const token = (current_token = {});
if (value == null || opts.hard || (spring.stiffness >= 1 && spring.damping >= 1)) {
cancel_task = true; // cancel any running animation
last_time = now();
last_value = new_value;
store.set((value = target_value));
return Promise.resolve();
} else if (opts.soft) {
const rate = opts.soft === true ? 0.5 : +opts.soft;
inv_mass_recovery_rate = 1 / (rate * 60);
inv_mass = 0; // infinite mass, unaffected by spring forces
}
if (!task) {
last_time = now();
cancel_task = false;
task = loop((now) => {
if (cancel_task) {
cancel_task = false;
task = null;
return false;
}
inv_mass = Math.min(inv_mass + inv_mass_recovery_rate, 1);
const ctx = {
inv_mass,
opts: spring,
settled: true,
dt: ((now - last_time) * 60) / 1000
};
const next_value = tick_spring(ctx, last_value, value, target_value);
last_time = now;
last_value = value;
store.set((value = next_value));
if (ctx.settled) {
task = null;
}
return !ctx.settled;
});
}
return new Promise((fulfil) => {
task.promise.then(() => {
if (token === current_token) fulfil();
});
});
}
/** @type {import('./public.js').Spring<T>} */
const spring = {
set,
update: (fn, opts) => set(fn(target_value, value), opts),
subscribe: store.subscribe,
stiffness,
damping,
precision
};
return spring;
}
|