mirror of https://github.com/sveltejs/svelte
				
				
				
			
			You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
					
					
						
							250 lines
						
					
					
						
							6.6 KiB
						
					
					
				
			
		
		
	
	
							250 lines
						
					
					
						
							6.6 KiB
						
					
					
				import { writable } from '../store/index.mjs';
 | 
						|
import { now, loop, assign, identity } from '../internal/Component-cd97939e.mjs';
 | 
						|
 | 
						|
/**
 | 
						|
 * @param {any} obj
 | 
						|
 * @returns {boolean}
 | 
						|
 */
 | 
						|
function is_date(obj) {
 | 
						|
	return Object.prototype.toString.call(obj) === '[object Date]';
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * @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`);
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * @template T
 | 
						|
 * @param {T} [value]
 | 
						|
 * @param {import('./private.js').SpringOpts} [opts]
 | 
						|
 * @returns {import('./public.js').Spring<T>}
 | 
						|
 */
 | 
						|
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;
 | 
						|
}
 | 
						|
 | 
						|
/** @returns {(t: any) => any} */
 | 
						|
function get_interpolator(a, b) {
 | 
						|
	if (a === b || a !== a) return () => a;
 | 
						|
	const type = typeof a;
 | 
						|
	if (type !== typeof b || Array.isArray(a) !== Array.isArray(b)) {
 | 
						|
		throw new Error('Cannot interpolate values of different type');
 | 
						|
	}
 | 
						|
	if (Array.isArray(a)) {
 | 
						|
		const arr = b.map((bi, i) => {
 | 
						|
			return get_interpolator(a[i], bi);
 | 
						|
		});
 | 
						|
		return (t) => arr.map((fn) => fn(t));
 | 
						|
	}
 | 
						|
	if (type === 'object') {
 | 
						|
		if (!a || !b) throw new Error('Object cannot be null');
 | 
						|
		if (is_date(a) && is_date(b)) {
 | 
						|
			a = a.getTime();
 | 
						|
			b = b.getTime();
 | 
						|
			const delta = b - a;
 | 
						|
			return (t) => new Date(a + t * delta);
 | 
						|
		}
 | 
						|
		const keys = Object.keys(b);
 | 
						|
		const interpolators = {};
 | 
						|
		keys.forEach((key) => {
 | 
						|
			interpolators[key] = get_interpolator(a[key], b[key]);
 | 
						|
		});
 | 
						|
		return (t) => {
 | 
						|
			const result = {};
 | 
						|
			keys.forEach((key) => {
 | 
						|
				result[key] = interpolators[key](t);
 | 
						|
			});
 | 
						|
			return result;
 | 
						|
		};
 | 
						|
	}
 | 
						|
	if (type === 'number') {
 | 
						|
		const delta = b - a;
 | 
						|
		return (t) => a + t * delta;
 | 
						|
	}
 | 
						|
	throw new Error(`Cannot interpolate ${type} values`);
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * @template T
 | 
						|
 * @param {T} [value]
 | 
						|
 * @param {import('./private.js').TweenedOptions<T>} [defaults]
 | 
						|
 * @returns {import('./public.js').Tweened<T>}
 | 
						|
 */
 | 
						|
function tweened(value, defaults = {}) {
 | 
						|
	const store = writable(value);
 | 
						|
	/** @type {import('../internal/private.js').Task} */
 | 
						|
	let task;
 | 
						|
	let target_value = value;
 | 
						|
	/**
 | 
						|
	 * @param {T} new_value
 | 
						|
	 * @param {import('./private.js').TweenedOptions<T>} opts
 | 
						|
	 */
 | 
						|
	function set(new_value, opts) {
 | 
						|
		if (value == null) {
 | 
						|
			store.set((value = new_value));
 | 
						|
			return Promise.resolve();
 | 
						|
		}
 | 
						|
		target_value = new_value;
 | 
						|
		let previous_task = task;
 | 
						|
		let started = false;
 | 
						|
		let {
 | 
						|
			delay = 0,
 | 
						|
			duration = 400,
 | 
						|
			easing = identity,
 | 
						|
			interpolate = get_interpolator
 | 
						|
		} = assign(assign({}, defaults), opts);
 | 
						|
		if (duration === 0) {
 | 
						|
			if (previous_task) {
 | 
						|
				previous_task.abort();
 | 
						|
				previous_task = null;
 | 
						|
			}
 | 
						|
			store.set((value = target_value));
 | 
						|
			return Promise.resolve();
 | 
						|
		}
 | 
						|
		const start = now() + delay;
 | 
						|
		let fn;
 | 
						|
		task = loop((now) => {
 | 
						|
			if (now < start) return true;
 | 
						|
			if (!started) {
 | 
						|
				fn = interpolate(value, new_value);
 | 
						|
				if (typeof duration === 'function') duration = duration(value, new_value);
 | 
						|
				started = true;
 | 
						|
			}
 | 
						|
			if (previous_task) {
 | 
						|
				previous_task.abort();
 | 
						|
				previous_task = null;
 | 
						|
			}
 | 
						|
			const elapsed = now - start;
 | 
						|
			if (elapsed > /** @type {number} */ (duration)) {
 | 
						|
				store.set((value = new_value));
 | 
						|
				return false;
 | 
						|
			}
 | 
						|
			// @ts-ignore
 | 
						|
			store.set((value = fn(easing(elapsed / duration))));
 | 
						|
			return true;
 | 
						|
		});
 | 
						|
		return task.promise;
 | 
						|
	}
 | 
						|
	return {
 | 
						|
		set,
 | 
						|
		update: (fn, opts) => set(fn(target_value, value), opts),
 | 
						|
		subscribe: store.subscribe
 | 
						|
	};
 | 
						|
}
 | 
						|
 | 
						|
export { spring, tweened };
 |