diff --git a/.gitignore b/.gitignore index 71c1dad10b..a999190ceb 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ node_modules /internal.* /store.js /easing.js +/motion.* /transition.js /scratch/ /coverage/ diff --git a/easing.mjs b/easing.mjs index 855713b03d..759c1046e5 100644 --- a/easing.mjs +++ b/easing.mjs @@ -3,6 +3,8 @@ Adapted from https://github.com/mattdesl Distributed under MIT License https://github.com/mattdesl/eases/blob/master/LICENSE.md */ +export { identity as linear } from './internal'; + export function backInOut(t) { var s = 1.70158 * 1.525; if ((t *= 2) < 1) return 0.5 * (t * t * ((s + 1) * t - s)); @@ -112,10 +114,6 @@ export function expoOut(t) { return t === 1.0 ? t : 1.0 - Math.pow(2.0, -10.0 * t); } -export function linear(t) { - return t; -} - export function quadInOut(t) { t /= 0.5; if (t < 1) return 0.5 * t * t; diff --git a/rollup.config.js b/rollup.config.js index 7e1947ec4e..6f15775016 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -52,27 +52,31 @@ export default [ experimentalCodeSplitting: true }, - /* internal.[m]js */ - { - input: 'src/internal/index.js', + /* internal.[m]js, motion.mjs */ + ...['internal', 'motion'].map(name => ({ + input: `src/${name}/index.js`, output: [ { - file: 'internal.mjs', - format: 'esm' + file: `${name}.mjs`, + format: 'esm', + paths: id => id.startsWith('svelte/') && id.replace('svelte', '.') }, { - file: 'internal.js', - format: 'cjs' + file: `${name}.js`, + format: 'cjs', + paths: id => id.startsWith('svelte/') && id.replace('svelte', '.') } - ] - }, + ], + external: id => id.startsWith('svelte/') + })), - // runtime API + // everything else ...['index', 'store', 'easing', 'transition'].map(name => ({ input: `${name}.mjs`, output: { file: `${name}.js`, - format: 'cjs' + format: 'cjs', + paths: id => id.startsWith('svelte/') && id.replace('svelte', '.') }, external: id => id !== `${name}.mjs` })) diff --git a/src/compile/render-dom/wrappers/EachBlock.ts b/src/compile/render-dom/wrappers/EachBlock.ts index 603920070d..397a5629a7 100644 --- a/src/compile/render-dom/wrappers/EachBlock.ts +++ b/src/compile/render-dom/wrappers/EachBlock.ts @@ -341,7 +341,7 @@ export default class EachBlockWrapper extends Wrapper { block.builders.update.addBlock(deindent` const ${this.vars.each_block_value} = ${snippet}; - ${this.block.hasOutros && `@groupOutros();`} + ${this.block.hasOutros && `@group_outros();`} ${this.node.hasAnimation && `for (let #i = 0; #i < ${blocks}.length; #i += 1) ${blocks}[#i].r();`} ${blocks} = @updateKeyedEach(${blocks}, #component, changed, ${get_key}, ${dynamic ? '1' : '0'}, ctx, ${this.vars.each_block_value}, ${lookup}, ${updateMountNode}, ${destroy}, ${create_each_block}, "${mountOrIntro}", ${anchor}, ${this.vars.get_each_context}); ${this.node.hasAnimation && `for (let #i = 0; #i < ${blocks}.length; #i += 1) ${blocks}[#i].a();`} @@ -466,7 +466,7 @@ export default class EachBlockWrapper extends Wrapper { if (this.block.hasOutros) { destroy = deindent` - @groupOutros(); + @group_outros(); for (; #i < ${iterations}.length; #i += 1) ${outroBlock}(#i, 1); `; } else { diff --git a/src/compile/render-dom/wrappers/Element/index.ts b/src/compile/render-dom/wrappers/Element/index.ts index 1178eca4ab..e08c77dac4 100644 --- a/src/compile/render-dom/wrappers/Element/index.ts +++ b/src/compile/render-dom/wrappers/Element/index.ts @@ -595,13 +595,13 @@ export default class ElementWrapper extends Wrapper { if (${name}) ${name}.invalidate(); @add_render_callback(() => { - if (!${name}) ${name} = @wrapTransition(#component, ${this.var}, ${fn}, ${snippet}, true); + if (!${name}) ${name} = @create_transition(${this.var}, ${fn}, ${snippet}, true); ${name}.run(1); }); `); block.builders.outro.addBlock(deindent` - if (!${name}) ${name} = @wrapTransition(#component, ${this.var}, ${fn}, ${snippet}, false); + if (!${name}) ${name} = @create_transition(${this.var}, ${fn}, ${snippet}, false); ${name}.run(0, () => { #outrocallback(); ${name} = null; @@ -630,7 +630,7 @@ export default class ElementWrapper extends Wrapper { block.builders.intro.addConditional(`@intros.enabled`, deindent` @add_render_callback(() => { - ${introName} = @wrapTransition(#component, ${this.var}, ${fn}, ${snippet}, true); + ${introName} = @create_transition(${this.var}, ${fn}, ${snippet}, true); ${introName}.run(1); }); `); @@ -651,7 +651,7 @@ export default class ElementWrapper extends Wrapper { // TODO hide elements that have outro'd (unless they belong to a still-outroing // group) prior to their removal from the DOM block.builders.outro.addBlock(deindent` - ${outroName} = @wrapTransition(#component, ${this.var}, ${fn}, ${snippet}, false); + ${outroName} = @create_transition(${this.var}, ${fn}, ${snippet}, false); ${outroName}.run(0, #outrocallback); `); @@ -666,18 +666,18 @@ export default class ElementWrapper extends Wrapper { const { component } = this.renderer; const rect = block.getUniqueName('rect'); - const animation = block.getUniqueName('animation'); + const stop_animation = block.getUniqueName('stop_animation'); block.addVariable(rect); - block.addVariable(animation); + block.addVariable(stop_animation, '@noop'); block.builders.measure.addBlock(deindent` ${rect} = ${this.var}.getBoundingClientRect(); `); block.builders.fix.addBlock(deindent` - @fixPosition(${this.var}); - if (${animation}) ${animation}.stop(); + @fix_position(${this.var}); + ${stop_animation}(); `); const params = this.node.animation.expression ? this.node.animation.expression.render() : '{}'; @@ -685,8 +685,8 @@ export default class ElementWrapper extends Wrapper { const name = component.qualify(this.node.animation.name); block.builders.animate.addBlock(deindent` - if (${animation}) ${animation}.stop(); - ${animation} = @wrapAnimation(${this.var}, ${rect}, ${name}, ${params}); + ${stop_animation}(); + ${stop_animation} = @animate(${this.var}, ${rect}, ${name}, ${params}); `); } diff --git a/src/compile/render-dom/wrappers/IfBlock.ts b/src/compile/render-dom/wrappers/IfBlock.ts index 4e4d3265b3..594f811090 100644 --- a/src/compile/render-dom/wrappers/IfBlock.ts +++ b/src/compile/render-dom/wrappers/IfBlock.ts @@ -322,7 +322,7 @@ export default class IfBlockWrapper extends Wrapper { const updateMountNode = this.getUpdateMountNode(anchor); const destroyOldBlock = deindent` - @groupOutros(); + @group_outros(); ${name}.o(function() { ${if_blocks}[${previous_block_index}].d(1); ${if_blocks}[${previous_block_index}] = null; @@ -445,7 +445,7 @@ export default class IfBlockWrapper extends Wrapper { // as that will typically result in glitching const exit = branch.block.hasOutroMethod ? deindent` - @groupOutros(); + @group_outros(); ${name}.o(function() { ${name}.d(1); ${name} = null; diff --git a/src/compile/render-dom/wrappers/InlineComponent/index.ts b/src/compile/render-dom/wrappers/InlineComponent/index.ts index 02227b86e3..a5427ef073 100644 --- a/src/compile/render-dom/wrappers/InlineComponent/index.ts +++ b/src/compile/render-dom/wrappers/InlineComponent/index.ts @@ -368,7 +368,7 @@ export default class InlineComponentWrapper extends Wrapper { block.builders.update.addBlock(deindent` if (${switch_value} !== (${switch_value} = ${snippet})) { if (${name}) { - @groupOutros(); + @group_outros(); const old_component = ${name}; old_component.$$.fragment.o(() => { old_component.$destroy(); diff --git a/src/internal/animations.js b/src/internal/animations.js index 355c0254e7..fcbfba7521 100644 --- a/src/internal/animations.js +++ b/src/internal/animations.js @@ -1,87 +1,85 @@ -import { transitionManager, linear, generateRule, hash } from './transitions.js'; +import { identity as linear, noop } from './utils.js'; +import { loop } from './loop.js'; +import { create_rule, delete_rule } from './style_manager.js'; -export function wrapAnimation(node, from, fn, params) { +export function animate(node, from, fn, params) { if (!from) return; const to = node.getBoundingClientRect(); if (from.left === to.left && from.right === to.right && from.top === to.top && from.bottom === to.bottom) return; - const info = fn(node, { from, to }, params); + const { + delay = 0, + duration = 300, + easing = linear, + start: start_time = window.performance.now() + delay, + end = start_time + duration, + tick = noop, + css + } = fn(node, { from, to }, params); - const duration = 'duration' in info ? info.duration : 300; - const delay = 'delay' in info ? info.delay : 0; - const ease = info.easing || linear; - const start = window.performance.now() + delay; - const end = start + duration; - - const program = { - a: 0, - t: 0, - b: 1, - delta: 1, - duration, - start, - end - }; + let running = true; + let started = false; + let name; const cssText = node.style.cssText; - const animation = { - pending: delay ? program : null, - program: delay ? null : program, - running: true, - - start() { - if (info.css) { - if (delay) node.style.cssText = cssText; - - const rule = generateRule(program, ease, info.css); - program.name = `__svelte_${hash(rule)}`; - - transitionManager.addRule(rule, program.name); - - node.style.animation = (node.style.animation || '') - .split(', ') - .filter(anim => anim && (program.delta < 0 || !/__svelte/.test(anim))) - .concat(`${program.name} ${program.duration}ms linear 1 forwards`) - .join(', '); - } - - animation.program = program; - animation.pending = null; - }, - - update: now => { - const p = now - program.start; - const t = program.a + program.delta * ease(p / program.duration); - if (info.tick) info.tick(t, 1 - t); - }, - - done() { - if (info.tick) info.tick(1, 0); - animation.stop(); - }, - - stop() { - if (info.css) transitionManager.deleteRule(node, program.name); - animation.running = false; + function start() { + if (css) { + if (delay) node.style.cssText = cssText; + + name = create_rule({ a: 0, b: 1, delta: 1, duration }, easing, css); + + node.style.animation = (node.style.animation || '') + .split(', ') + .filter(anim => anim && !/__svelte/.test(anim)) + .concat(`${name} ${duration}ms linear 1 forwards`) + .join(', '); } - }; - transitionManager.add(animation); + started = true; + } + + function stop() { + if (css) delete_rule(node, name); + running = false; + } - if (info.tick) info.tick(0, 1); + const { abort, promise } = loop(now => { + if (!started && now >= start_time) { + start(); + } + + if (started && now >= end) { + tick(1, 0); + stop(); + } + + if (!running) { + return false; + } + + if (started) { + const p = now - start_time; + const t = 0 + 1 * easing(p / duration); + tick(t, 1 - t); + } + + return true; + }); if (delay) { - if (info.css) node.style.cssText += info.css(0, 1); + if (css) node.style.cssText += css(0, 1); } else { - animation.start(); + start(); } - return animation; + tick(0, 1); + + return stop; } -export function fixPosition(node) { +export function fix_position(node) { const style = getComputedStyle(node); if (style.position !== 'absolute' && style.position !== 'fixed') { diff --git a/src/internal/await-block.js b/src/internal/await-block.js index c579ef2242..5db7c8f1e5 100644 --- a/src/internal/await-block.js +++ b/src/internal/await-block.js @@ -1,5 +1,5 @@ import { assign, run_all, isPromise } from './utils.js'; -import { groupOutros } from './transitions.js'; +import { group_outros } from './transitions.js'; import { flush } from '../internal/scheduler.js'; export function handlePromise(promise, info) { @@ -17,7 +17,7 @@ export function handlePromise(promise, info) { if (info.blocks) { info.blocks.forEach((block, i) => { if (i !== index && block) { - groupOutros(); + group_outros(); block.o(() => { block.d(1); info.blocks[i] = null; diff --git a/src/internal/index.js b/src/internal/index.js index 6b134b677d..f3654f6b77 100644 --- a/src/internal/index.js +++ b/src/internal/index.js @@ -3,6 +3,7 @@ export * from './await-block.js'; export * from './dom.js'; export * from './keyed-each.js'; export * from './lifecycle.js'; +export * from './loop.js'; export * from './scheduler.js'; export * from './spread.js'; export * from './ssr.js'; diff --git a/src/internal/keyed-each.js b/src/internal/keyed-each.js index d3afde60fb..745ead6ff2 100644 --- a/src/internal/keyed-each.js +++ b/src/internal/keyed-each.js @@ -105,19 +105,4 @@ export function measure(blocks) { let i = blocks.length; while (i--) rects[blocks[i].key] = blocks[i].node.getBoundingClientRect(); return rects; -} - -export function animate(blocks, rects, fn, params) { - let i = blocks.length; - while (i--) { - const block = blocks[i]; - const from = rects[block.key]; - - if (!from) continue; - const to = block.node.getBoundingClientRect(); - - if (from.left === to.left && from.right === to.right && from.top === to.top && from.bottom === to.bottom) continue; - - - } } \ No newline at end of file diff --git a/src/internal/loop.js b/src/internal/loop.js new file mode 100644 index 0000000000..fd75f9b1a6 --- /dev/null +++ b/src/internal/loop.js @@ -0,0 +1,38 @@ +const tasks = new Set(); +let running = false; + +function run_tasks() { + tasks.forEach(task => { + if (!task[0](window.performance.now())) { + tasks.delete(task); + task[1](); + } + }); + + running = tasks.size > 0; + if (running) requestAnimationFrame(run_tasks); +} + +export function clear_loops() { + // for testing... + tasks.forEach(task => tasks.delete(task)); + running = false; +} + +export function loop(fn) { + let task; + + if (!running) { + running = true; + requestAnimationFrame(run_tasks); + } + + return { + promise: new Promise(fulfil => { + tasks.add(task = [fn, fulfil]); + }), + abort() { + tasks.delete(task); + } + }; +} \ No newline at end of file diff --git a/src/internal/style_manager.js b/src/internal/style_manager.js new file mode 100644 index 0000000000..6af5c091ed --- /dev/null +++ b/src/internal/style_manager.js @@ -0,0 +1,56 @@ +import { createElement } from './dom.js'; + +let stylesheet; +let active = 0; +let current_rules = {}; + +// https://github.com/darkskyapp/string-hash/blob/master/index.js +function hash(str) { + let hash = 5381; + let i = str.length; + + while (i--) hash = ((hash << 5) - hash) ^ str.charCodeAt(i); + return hash >>> 0; +} + +export function create_rule({ a, b, delta, duration }, ease, fn) { + const step = 16.666 / duration; + let keyframes = '{\n'; + + for (let p = 0; p <= 1; p += step) { + const t = a + delta * ease(p); + keyframes += p * 100 + `%{${fn(t, 1 - t)}}\n`; + } + + const rule = keyframes + `100% {${fn(b, 1 - b)}}\n}`; + const name = `__svelte_${hash(rule)}`; + + if (!current_rules[name]) { + if (!stylesheet) { + const style = createElement('style'); + document.head.appendChild(style); + stylesheet = style.sheet; + } + + current_rules[name] = true; + stylesheet.insertRule(`@keyframes ${name} ${rule}`, stylesheet.cssRules.length); + } + + active += 1; + return name; +} + +export function delete_rule(node, name) { + node.style.animation = node.style.animation + .split(', ') + .filter(anim => anim && anim.indexOf(name) === -1) + .join(', '); + + if (--active <= 0) clear_rules(); +} + +export function clear_rules() { + let i = stylesheet.cssRules.length; + while (i--) stylesheet.deleteRule(i); + current_rules = {}; +} \ No newline at end of file diff --git a/src/internal/transitions.js b/src/internal/transitions.js index 98418556a0..714f82c6a9 100644 --- a/src/internal/transitions.js +++ b/src/internal/transitions.js @@ -1,256 +1,175 @@ -import { createElement } from './dom.js'; -import { noop, run } from './utils.js'; - -export function linear(t) { - return t; -} - -export function generateRule({ a, b, delta, duration }, ease, fn) { - const step = 16.666 / duration; - let keyframes = '{\n'; - - for (let p = 0; p <= 1; p += step) { - const t = a + delta * ease(p); - keyframes += p * 100 + `%{${fn(t, 1 - t)}}\n`; +import { identity as linear, noop, run } from './utils.js'; +import { loop } from './loop.js'; +import { create_rule, delete_rule } from './style_manager.js'; + +let promise; + +function wait() { + if (!promise) { + promise = Promise.resolve(); + promise.then(() => { + promise = null; + }); } - return keyframes + `100% {${fn(b, 1 - b)}}\n}`; + return promise; } -// https://github.com/darkskyapp/string-hash/blob/master/index.js -export function hash(str) { - let hash = 5381; - let i = str.length; +let outros; - while (i--) hash = ((hash << 5) - hash) ^ str.charCodeAt(i); - return hash >>> 0; +export function group_outros() { + outros = { + remaining: 0, + callbacks: [] + }; } -export function wrapTransition(component, node, fn, params, intro) { - let obj = fn.call(component, node, params); - let duration; - let ease; +export function create_transition(node, fn, params, intro) { + let config = fn(node, params); let cssText; - let initialised = false; + let ready = !intro; + let t = intro ? 0 : 1; - return { - t: intro ? 0 : 1, - running: false, - program: null, - pending: null, - - run(b, callback) { - if (typeof obj === 'function') { - transitionManager.wait().then(() => { - obj = obj(); - this._run(b, callback); - }); - } else { - this._run(b, callback); - } - }, + let running = false; + let running_program = null; + let pending_program = null; - _run(b, callback) { - duration = obj.duration || 300; - ease = obj.easing || linear; + function start(program, delay, duration, easing) { + node.dispatchEvent(new window.CustomEvent(`${program.b ? 'intro' : 'outro'}start`)); - const program = { - start: window.performance.now() + (obj.delay || 0), - b, - callback: callback || noop - }; + program.a = t; + program.d = program.b - program.a; + program.duration = duration * Math.abs(program.b - program.a); + program.end = program.start + program.duration; - if (intro && !initialised) { - if (obj.css && obj.delay) { - cssText = node.style.cssText; - node.style.cssText += obj.css(0, 1); - } + if (config.css) { + if (delay) node.style.cssText = cssText; - if (obj.tick) obj.tick(0, 1); - initialised = true; - } + program.name = create_rule(program, easing, config.css); - if (!b) { - program.group = outros.current; - outros.current.remaining += 1; - } + node.style.animation = (node.style.animation || '') + .split(', ') + .filter(anim => anim && (program.d < 0 || !/__svelte/.test(anim))) + .concat(`${program.name} ${program.duration}ms linear 1 forwards`) + .join(', '); + } - if (obj.delay) { - this.pending = program; - } else { - this.start(program); - } + running_program = program; + pending_program = null; + } - if (!this.running) { - this.running = true; - transitionManager.add(this); - } - }, + function done() { + const program = running_program; + running_program = null; - start(program) { - node.dispatchEvent(new window.CustomEvent(`${program.b ? 'intro' : 'outro'}start`)); + t = program.b; - program.a = this.t; - program.delta = program.b - program.a; - program.duration = duration * Math.abs(program.b - program.a); - program.end = program.start + program.duration; + if (config.tick) config.tick(t, 1 - t); - if (obj.css) { - if (obj.delay) node.style.cssText = cssText; + node.dispatchEvent(new window.CustomEvent(`${program.b ? 'intro' : 'outro'}end`)); - const rule = generateRule(program, ease, obj.css); - transitionManager.addRule(rule, program.name = '__svelte_' + hash(rule)); + if (!program.b && !program.invalidated) { + program.group.callbacks.push(() => { + program.callback(); + if (config.css) delete_rule(node, program.name); + }); - node.style.animation = (node.style.animation || '') - .split(', ') - .filter(anim => anim && (program.delta < 0 || !/__svelte/.test(anim))) - .concat(`${program.name} ${program.duration}ms linear 1 forwards`) - .join(', '); + if (--program.group.remaining === 0) { + program.group.callbacks.forEach(run); } + } else { + if (config.css) delete_rule(node, program.name); + } - this.program = program; - this.pending = null; - }, + running = !!pending_program; + } - update(now) { - const program = this.program; - if (!program) return; + function go(b, callback) { + const { + delay = 0, + duration = 300, + easing = linear + } = config; + + const program = { + start: window.performance.now() + delay, + b, + callback + }; + + if (!ready) { + if (config.css && delay) { + cssText = node.style.cssText; + node.style.cssText += config.css(0, 1); + } - const p = now - program.start; - this.t = program.a + program.delta * ease(p / program.duration); - if (obj.tick) obj.tick(this.t, 1 - this.t); - }, + if (config.tick) config.tick(0, 1); + ready = true; + } - done() { - const program = this.program; - this.t = program.b; + if (!b) { + program.group = outros; + outros.remaining += 1; + } - if (obj.tick) obj.tick(this.t, 1 - this.t); + if (delay) { + pending_program = program; + } else { + start(program, delay, duration, easing); + } - node.dispatchEvent(new window.CustomEvent(`${program.b ? 'intro' : 'outro'}end`)); + if (!running) { + running = true; - if (!program.b && !program.invalidated) { - program.group.callbacks.push(() => { - program.callback(); - if (obj.css) transitionManager.deleteRule(node, program.name); - }); + const { abort, promise } = loop(now => { + if (running_program && now >= running_program.end) { + done(); + } - if (--program.group.remaining === 0) { - program.group.callbacks.forEach(run); + if (pending_program && now >= pending_program.start) { + start(pending_program, delay, duration, easing); } + + if (running) { + if (running_program) { + const p = now - running_program.start; + t = running_program.a + running_program.d * easing(p / running_program.duration); + if (config.tick) config.tick(t, 1 - t); + } + + return true; + } + }); + } + } + + return { + run(b, callback = noop) { + if (typeof config === 'function') { + wait().then(() => { + config = config(); + go(b, callback); + }); } else { - if (obj.css) transitionManager.deleteRule(node, program.name); + go(b, callback); } - - this.running = !!this.pending; }, abort(reset) { - if (this.program) { - if (reset && obj.tick) obj.tick(1, 0); - if (obj.css) transitionManager.deleteRule(node, this.program.name); - this.program = this.pending = null; - this.running = false; + if (reset && config.tick) config.tick(1, 0); + + if (running_program) { + if (config.css) delete_rule(node, running_program.name); + running_program = pending_program = null; + running = false; } }, invalidate() { - if (this.program) { - this.program.invalidated = true; + if (running_program) { + running_program.invalidated = true; } } }; -} - -export let outros = {}; - -export function groupOutros() { - outros.current = { - remaining: 0, - callbacks: [] - }; -} - -export var transitionManager = { - running: false, - transitions: [], - bound: null, - stylesheet: null, - activeRules: {}, - promise: null, - - add(transition) { - this.transitions.push(transition); - - if (!this.running) { - this.running = true; - requestAnimationFrame(this.bound || (this.bound = this.next.bind(this))); - } - }, - - addRule(rule, name) { - if (!this.stylesheet) { - const style = createElement('style'); - document.head.appendChild(style); - transitionManager.stylesheet = style.sheet; - } - - if (!this.activeRules[name]) { - this.activeRules[name] = true; - this.stylesheet.insertRule(`@keyframes ${name} ${rule}`, this.stylesheet.cssRules.length); - } - }, - - next() { - this.running = false; - - const now = window.performance.now(); - let i = this.transitions.length; - - while (i--) { - const transition = this.transitions[i]; - - if (transition.program && now >= transition.program.end) { - transition.done(); - } - - if (transition.pending && now >= transition.pending.start) { - transition.start(transition.pending); - } - - if (transition.running) { - transition.update(now); - this.running = true; - } else if (!transition.pending) { - this.transitions.splice(i, 1); - } - } - - if (this.running) { - requestAnimationFrame(this.bound); - } else if (this.stylesheet) { - let i = this.stylesheet.cssRules.length; - while (i--) this.stylesheet.deleteRule(i); - this.activeRules = {}; - } - }, - - deleteRule(node, name) { - node.style.animation = node.style.animation - .split(', ') - .filter(anim => anim && anim.indexOf(name) === -1) - .join(', '); - }, - - wait() { - if (!transitionManager.promise) { - transitionManager.promise = Promise.resolve(); - transitionManager.promise.then(() => { - transitionManager.promise = null; - }); - } - - return transitionManager.promise; - } -}; +} \ No newline at end of file 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/src/motion/spring.js b/src/motion/spring.js new file mode 100644 index 0000000000..1b5413db63 --- /dev/null +++ b/src/motion/spring.js @@ -0,0 +1,155 @@ +import { writable } from 'svelte/store'; +import { loop } from 'svelte/internal'; +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; + + 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); + } else { + value = current_value + d; + } + + if (Math.abs(d) > 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; + } + } + + 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 } = opts; + const velocity = get_initial_velocity(value); + + let task; + let target_value = value; + let last_time; + let settled; + let threshold; + + function set(new_value) { + target_value = new_value; + threshold = get_threshold(value, target_value, 0.000001); // TODO make precision configurable? + + if (!task) { + last_time = window.performance.now(); + settled = false; + + task = loop(now=> { + ({ value, settled } = tick_spring( + velocity, + value, + target_value, + spring.stiffness, + spring.damping, + (now - last_time) * 60 / 1000, + threshold + )); + + last_time = now; + + if (settled) { + value = target_value; + task = null; + } + + store.set(value); + return !settled; + }); + } + + return task.promise; + } + + const spring = { + set, + update: fn => set(fn(target_value, value)), + subscribe: store.subscribe, + stiffness, + damping + }; + + return spring; +} \ No newline at end of file diff --git a/src/motion/tweened.js b/src/motion/tweened.js new file mode 100644 index 0000000000..49f1217a4e --- /dev/null +++ b/src/motion/tweened.js @@ -0,0 +1,112 @@ +import { writable } from 'svelte/store'; +import { assign, loop } from 'svelte/internal'; +import { linear } from 'svelte/easing'; +import { 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 = 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 > 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..97a764bf46 --- /dev/null +++ b/src/motion/utils.js @@ -0,0 +1,3 @@ +export function is_date(obj) { + return Object.prototype.toString.call(obj) === '[object Date]'; +} \ No newline at end of file diff --git a/test/js/samples/each-block-keyed-animated/expected.js b/test/js/samples/each-block-keyed-animated/expected.js index 9c9bbff7f9..66db899b65 100644 --- a/test/js/samples/each-block-keyed-animated/expected.js +++ b/test/js/samples/each-block-keyed-animated/expected.js @@ -1,5 +1,5 @@ /* generated by Svelte vX.Y.Z */ -import { SvelteComponent as SvelteComponent_1, append, blankObject, createComment, createElement, createText, detachNode, fixAndOutroAndDestroyBlock, fixPosition, flush, init, insert, run, safe_not_equal, setData, updateKeyedEach, wrapAnimation } from "svelte/internal"; +import { SvelteComponent as SvelteComponent_1, animate, append, blankObject, createComment, createElement, createText, detachNode, fixAndOutroAndDestroyBlock, fix_position, flush, init, insert, noop, run, safe_not_equal, setData, updateKeyedEach } from "svelte/internal"; function get_each_context(ctx, list, i) { const child_ctx = Object.create(ctx); @@ -9,7 +9,7 @@ function get_each_context(ctx, list, i) { // (19:0) {#each things as thing (thing.id)} function create_each_block(component, key_1, ctx) { - var div, text_value = ctx.thing.name, text, rect, animation; + var div, text_value = ctx.thing.name, text, rect, stop_animation = noop; return { key: key_1, @@ -38,13 +38,13 @@ function create_each_block(component, key_1, ctx) { }, f() { - fixPosition(div); - if (animation) animation.stop(); + fix_position(div); + stop_animation(); }, a() { - if (animation) animation.stop(); - animation = wrapAnimation(div, rect, foo, {}); + stop_animation(); + stop_animation = animate(div, rect, foo, {}); }, d(detach) { diff --git a/test/runtime/index.js b/test/runtime/index.js index 88e2d0fe84..334426c424 100644 --- a/test/runtime/index.js +++ b/test/runtime/index.js @@ -3,7 +3,7 @@ import * as path from "path"; import * as fs from "fs"; import { rollup } from 'rollup'; import * as virtual from 'rollup-plugin-virtual'; -import { transitionManager } from "../../internal.js"; +import { clear_loops } from "../../internal.js"; import { showOutput, @@ -93,9 +93,8 @@ describe("runtime", () => { return Promise.resolve() .then(() => { - // set of hacks to support transition tests - transitionManager.running = false; - transitionManager.transitions = []; + // hack to support transition tests + clear_loops(); const raf = { time: 0,