Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Subtle tweaks to spring animations #2627

Merged
merged 1 commit into from
May 4, 2019
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
182 changes: 65 additions & 117 deletions src/motion/spring.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,144 +2,92 @@ import { writable } from 'svelte/store'; // eslint-disable-line import/no-unreso
import { loop } from 'svelte/internal'; // eslint-disable-line import/no-unresolved
import { is_date } from './utils.js';

function get_initial_velocity(value) {
if (typeof value === 'number' || is_date(value)) return 0;

if (Array.isArray(value)) return value.map(get_initial_velocity);

if (value && typeof value === 'object') {
const velocities = {};
for (const k in value) velocities[k] = get_initial_velocity(value[k]);
return velocities;
}

throw new Error(`Cannot spring ${typeof value} values`);
}

function get_threshold(value, target_value, precision) {
if (typeof value === 'number' || is_date(value)) return precision * Math.abs((target_value - value));

if (Array.isArray(value)) return value.map((v, i) => get_threshold(v, target_value[i], precision));

if (value && typeof value === 'object') {
const threshold = {};
for (const k in value) threshold[k] = get_threshold(value[k], target_value[k], precision);
return threshold;
}

throw new Error(`Cannot spring ${typeof value} values`);
}

function tick_spring(velocity, current_value, target_value, stiffness, damping, multiplier, threshold) {
let settled = true;
let value;

function tick_spring(ctx, last_value, current_value, target_value) {
if (typeof current_value === 'number' || is_date(current_value)) {
const delta = target_value - current_value;
const spring = stiffness * delta;
const damper = damping * velocity;

const acceleration = spring - damper;

velocity += acceleration;
const d = velocity * multiplier;

if (is_date(current_value)) {
value = new Date(current_value.getTime() + d);
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 {
value = current_value + d;
}

if (Math.abs(d) > threshold || Math.abs(delta) > threshold) settled = false;
}

else if (Array.isArray(current_value)) {
value = current_value.map((v, i) => {
const result = tick_spring(
velocity[i],
v,
target_value[i],
stiffness,
damping,
multiplier,
threshold[i]
);

velocity[i] = result.velocity;
if (!result.settled) settled = false;
return result.value;
});
}

else if (typeof current_value === 'object') {
value = {};
for (const k in current_value) {
const result = tick_spring(
velocity[k],
current_value[k],
target_value[k],
stiffness,
damping,
multiplier,
threshold[k]
);

velocity[k] = result.velocity;
if (!result.settled) settled = false;
value[k] = result.value;
ctx.settled = false; // signal loop to keep ticking
return is_date(current_value) ?
new Date(current_value.getTime() + d) : current_value + d;
}
}

else {
} else if (Array.isArray(current_value)) {
return current_value.map((_, i) =>
tick_spring(ctx, last_value[i], current_value[i], target_value[i]));
} else if (typeof current_value === 'object') {
let next_value = {};
for (const k in current_value)
next_value[k] = tick_spring(ctx, last_value[k], current_value[k], target_value[k]);
return next_value;
} else {
throw new Error(`Cannot spring ${typeof value} values`);
}

return { velocity, value, settled };
}

export function spring(value, opts = {}) {
const store = writable(value);
const { stiffness = 0.15, damping = 0.8, precision = 0.01 } = opts;

const { stiffness = 0.15, damping = 0.8, precision = 0.001 } = opts;
const velocity = get_initial_velocity(value);

let task;
let last_time, task, current_token;
let last_value = value;
let target_value = value;
let last_time;
let settled;
let threshold;
let current_token;

function set(new_value) {
target_value = new_value;
threshold = get_threshold(value, target_value, spring.precision);
let inv_mass = 1;
let inv_mass_recovery_rate = 0;
let cancel_task = false;

function set(new_value, opts = {}) {
target_value = new_value;
const token = current_token = {};

if (opts.hard || (spring.stiffness >= 1 && spring.damping >= 1)) {
cancel_task = true; // cancel any running animation
last_time = window.performance.now();
last_value = value;
store.set(value = target_value);
return new Promise(f => f()); // fulfil immediately
} else if (opts.soft) {
let rate = opts.soft === true ? .5 : +opts.soft;
inv_mass_recovery_rate = 1 / (rate * 60);
inv_mass = 0; // infinite mass, unaffected by spring forces
}

if (!task) {
last_time = window.performance.now();
settled = false;

cancel_task = false;
task = loop(now => {
({ value, settled } = tick_spring(
velocity,
value,
target_value,
spring.stiffness,
spring.damping,
(now - last_time) * 60 / 1000,
threshold
));

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, // tick_spring may signal false
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 (settled) {
value = target_value;
if (ctx.settled)
task = null;
}

store.set(value);
return !settled;
return !ctx.settled;
});
}

Expand All @@ -152,12 +100,12 @@ export function spring(value, opts = {}) {

const spring = {
set,
update: fn => set(fn(target_value, value)),
update: (fn, opts) => set(fn(target_value, value), opts),
subscribe: store.subscribe,
stiffness,
damping,
precision
};

return spring;
}
}