diff --git a/.gitignore b/.gitignore index 380e562b06..a999190ceb 100644 --- a/.gitignore +++ b/.gitignore @@ -8,7 +8,7 @@ node_modules /internal.* /store.js /easing.js -/motion.js +/motion.* /transition.js /scratch/ /coverage/ diff --git a/rollup.config.js b/rollup.config.js index f0546fceb2..9ba2b667ea 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -67,6 +67,24 @@ export default [ ] }, + /* motion.[m]js */ + { + input: 'src/motion/index.js', + output: [ + { + file: 'motion.mjs', + format: 'esm', + paths: id => id.replace('svelte', '.') + }, + { + file: 'motion.js', + format: 'cjs', + paths: id => id.replace('svelte', '.') + } + ], + external: id => id.startsWith('svelte/') + }, + // runtime API ...['index', 'store', 'easing', 'motion', 'transition'].map(name => ({ input: `${name}.mjs`, diff --git a/src/motion/index.js b/src/motion/index.js new file mode 100644 index 0000000000..e0e9bcf1ae --- /dev/null +++ b/src/motion/index.js @@ -0,0 +1,2 @@ +export * from './spring.js'; +export * from './tweened.js'; \ No newline at end of file diff --git a/motion.mjs b/src/motion/spring.js similarity index 54% rename from motion.mjs rename to src/motion/spring.js index 395c678366..ebac993f06 100644 --- a/motion.mjs +++ b/src/motion/spring.js @@ -1,152 +1,5 @@ -import { writable } from './store'; -import { assign } from './internal'; -import { linear } from './easing'; - -const tasks = new Set(); -let running = false; - -function run_tasks() { - tasks.forEach(task => { - if (!task[0]()) { - tasks.delete(task); - task[1](); - } - }); - - running = tasks.size > 0; - if (running) requestAnimationFrame(run_tasks); -} - -function add_task(fn) { - let task; - - if (!running) { - running = true; - requestAnimationFrame(run_tasks); - } - - return { - promise: new Promise(fulfil => { - tasks.add(task = [fn, fulfil]); - }), - abort() { - tasks.delete(task); - } - }; -} - -function is_date(obj) { - return Object.prototype.toString.call(obj) === '[object Date]'; -} - -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`); -} - -export function tweened(value, defaults = {}) { - const store = writable(value); - - let task; - let target_value = value; - - function set(new_value, opts) { - target_value = new_value; - - let previous_task = task; - let started = false; - - let { - delay = 0, - duration = 400, - easing = linear, - interpolate = get_interpolator - } = assign(assign({}, defaults), opts); - - const start = window.performance.now() + delay; - let fn; - - task = add_task(() => { - const time = window.performance.now(); - if (time < 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 = time - start; - - if (elapsed > duration) { - store.set(value = new_value); - return false; - } - - 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 - }; -} +import { writable } from 'svelte/store'; +import { add_task, is_date } from './utils.js'; function get_initial_velocity(value) { if (typeof value === 'number' || is_date(value)) return 0; diff --git a/src/motion/tweened.js b/src/motion/tweened.js new file mode 100644 index 0000000000..020378408c --- /dev/null +++ b/src/motion/tweened.js @@ -0,0 +1,113 @@ +import { writable } from 'svelte/store'; +import { assign } from 'svelte/internal'; +import { linear } from 'svelte/easing'; +import { add_task, is_date } from './utils.js'; + +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`); +} + +export function tweened(value, defaults = {}) { + const store = writable(value); + + let task; + let target_value = value; + + function set(new_value, opts) { + target_value = new_value; + + let previous_task = task; + let started = false; + + let { + delay = 0, + duration = 400, + easing = linear, + interpolate = get_interpolator + } = assign(assign({}, defaults), opts); + + const start = window.performance.now() + delay; + let fn; + + task = add_task(() => { + const time = window.performance.now(); + if (time < 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 = time - start; + + if (elapsed > duration) { + store.set(value = new_value); + return false; + } + + 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 + }; +} \ No newline at end of file diff --git a/src/motion/utils.js b/src/motion/utils.js new file mode 100644 index 0000000000..986cec3ef4 --- /dev/null +++ b/src/motion/utils.js @@ -0,0 +1,36 @@ +const tasks = new Set(); +let running = false; + +function run_tasks() { + tasks.forEach(task => { + if (!task[0]()) { + tasks.delete(task); + task[1](); + } + }); + + running = tasks.size > 0; + if (running) requestAnimationFrame(run_tasks); +} + +export function add_task(fn) { + let task; + + if (!running) { + running = true; + requestAnimationFrame(run_tasks); + } + + return { + promise: new Promise(fulfil => { + tasks.add(task = [fn, fulfil]); + }), + abort() { + tasks.delete(task); + } + }; +} + +export function is_date(obj) { + return Object.prototype.toString.call(obj) === '[object Date]'; +} \ No newline at end of file