From 31e387e76ca00d90a1cee600b6c562322ad29dab Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 15 May 2018 12:01:24 -0400 Subject: [PATCH] allow animations to be aborted - fixes #1458 --- src/compile/dom/Block.ts | 35 +++++++++---- src/compile/nodes/Animation.ts | 9 ++++ src/compile/nodes/EachBlock.ts | 6 +-- src/compile/nodes/Element.ts | 28 ++++++++++- src/shared/animations.js | 92 ++++++++++++++++++++++++++++++++++ src/shared/index.js | 1 + src/shared/keyed-each.js | 89 ++------------------------------ 7 files changed, 163 insertions(+), 97 deletions(-) create mode 100644 src/shared/animations.js diff --git a/src/compile/dom/Block.ts b/src/compile/dom/Block.ts index cde0eb0f3f54..a6cf1e75d265 100644 --- a/src/compile/dom/Block.ts +++ b/src/compile/dom/Block.ts @@ -33,6 +33,9 @@ export default class Block { claim: CodeBuilder; hydrate: CodeBuilder; mount: CodeBuilder; + measure: CodeBuilder; + fix: CodeBuilder; + animate: CodeBuilder; intro: CodeBuilder; update: CodeBuilder; outro: CodeBuilder; @@ -40,7 +43,7 @@ export default class Block { }; maintainContext: boolean; - animation?: string; + hasAnimation: boolean; hasIntroMethod: boolean; hasOutroMethod: boolean; outros: number; @@ -72,13 +75,16 @@ export default class Block { claim: new CodeBuilder(), hydrate: new CodeBuilder(), mount: new CodeBuilder(), + measure: new CodeBuilder(), + fix: new CodeBuilder(), + animate: new CodeBuilder(), intro: new CodeBuilder(), update: new CodeBuilder(), outro: new CodeBuilder(), destroy: new CodeBuilder(), }; - this.animation = null; + this.hasAnimation = false; this.hasIntroMethod = false; // a block could have an intro method but not intro transitions, e.g. if a sibling block has intros this.hasOutroMethod = false; this.outros = 0; @@ -129,8 +135,8 @@ export default class Block { this.outros += 1; } - addAnimation(name) { - this.animation = name; + addAnimation() { + this.hasAnimation = true; } addVariable(name: string, init?: string) { @@ -189,11 +195,6 @@ export default class Block { this.builders.hydrate.addLine(`this.first = ${this.first};`); } - if (this.animation) { - properties.addBlock(`node: null,`); - this.builders.hydrate.addLine(`this.node = ${this.animation};`); - } - if (this.builders.create.isEmpty() && this.builders.hydrate.isEmpty()) { properties.addBlock(`c: @noop,`); } else { @@ -255,6 +256,22 @@ export default class Block { } } + if (this.hasAnimation) { + properties.addBlock(deindent` + ${dev ? `r: function measure` : `r`}() { + ${this.builders.measure} + }, + + ${dev ? `f: function fix` : `f`}() { + ${this.builders.fix} + }, + + ${dev ? `a: function animate` : `a`}() { + ${this.builders.animate} + }, + `); + } + if (this.hasIntroMethod || this.hasOutroMethod) { if (hasIntros) { properties.addBlock(deindent` diff --git a/src/compile/nodes/Animation.ts b/src/compile/nodes/Animation.ts index 8041f2648557..cf9ec3514374 100644 --- a/src/compile/nodes/Animation.ts +++ b/src/compile/nodes/Animation.ts @@ -1,3 +1,4 @@ +import Block from '../dom/Block'; import Node from './shared/Node'; import Expression from './shared/Expression'; @@ -15,4 +16,12 @@ export default class Animation extends Node { ? new Expression(compiler, this, scope, info.expression) : null; } + + build( + block: Block, + parentNode: string, + parentNodes: string + ) { + + } } \ No newline at end of file diff --git a/src/compile/nodes/EachBlock.ts b/src/compile/nodes/EachBlock.ts index 35c314bfda7b..70ce3ab2189d 100644 --- a/src/compile/nodes/EachBlock.ts +++ b/src/compile/nodes/EachBlock.ts @@ -315,7 +315,7 @@ export default class EachBlock extends Node { const dynamic = this.block.hasUpdateMethod; const rects = block.getUniqueName('rects'); - const destroy = this.block.animation + const destroy = this.block.hasAnimation ? `@fixAndOutroAndDestroyBlock` : this.block.hasOutroMethod ? `@outroAndDestroyBlock` @@ -325,9 +325,9 @@ export default class EachBlock extends Node { const ${this.each_block_value} = ${snippet}; ${this.block.hasOutroMethod && `@transitionManager.groupOutros();`} - ${this.block.animation && `const ${rects} = @measure(${blocks});`} + ${this.block.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.each_block_value}, ${lookup}, ${updateMountNode}, ${destroy}, ${create_each_block}, "${mountOrIntro}", ${anchor}, ${this.get_each_context}); - ${this.block.animation && `@animate(${blocks}, ${rects}, %animations-${this.children[0].animation.name}, {});`} + ${this.block.hasAnimation && `for (let #i = 0; #i < ${blocks}.length; #i += 1) ${blocks}[#i].a();`} `); if (this.compiler.options.nestedTransitions) { diff --git a/src/compile/nodes/Element.ts b/src/compile/nodes/Element.ts index 9749aa09fb5f..e6cb75d9f550 100644 --- a/src/compile/nodes/Element.ts +++ b/src/compile/nodes/Element.ts @@ -195,7 +195,7 @@ export default class Element extends Node { if (this.intro) block.addIntro(); if (this.outro) block.addOutro(); - if (this.animation) block.addAnimation(this.var); + if (this.animation) block.addAnimation(); const valueAttribute = this.attributes.find((attribute: Attribute) => attribute.name === 'value'); @@ -367,6 +367,7 @@ export default class Element extends Node { if (this.ref) this.addRef(block); this.addAttributes(block); this.addTransitions(block); + this.addAnimation(block); this.addActions(block); if (this.initialUpdate) { @@ -763,6 +764,31 @@ export default class Element extends Node { } } + addAnimation(block: Block) { + if (!this.animation) return; + + const rect = block.getUniqueName('rect'); + const animation = block.getUniqueName('animation'); + + block.addVariable(rect); + block.addVariable(animation); + + block.builders.measure.addBlock(deindent` + ${rect} = ${this.var}.getBoundingClientRect(); + `); + + block.builders.fix.addBlock(deindent` + @fixPosition(${this.var}); + if (${animation}) ${animation}.stop(); + `); + + const params = this.animation.expression ? this.animation.expression.snippet : '{}'; + block.builders.animate.addBlock(deindent` + if (${animation}) ${animation}.stop(); + ${animation} = @wrapAnimation(${this.var}, ${rect}, %animations-${this.animation.name}, ${params}); + `); + } + addActions(block: Block) { this.actions.forEach(action => { const { expression } = action; diff --git a/src/shared/animations.js b/src/shared/animations.js new file mode 100644 index 000000000000..8e05eb11f25a --- /dev/null +++ b/src/shared/animations.js @@ -0,0 +1,92 @@ +import { transitionManager, linear, generateRule, hash } from './transitions.js'; + +export function wrapAnimation(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; + + // console.log({ x: from.x, y: from.y }, { x: to.x, y: to.y }, node.textContent.trim()) + + const info = 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 + }; + + const animation = { + pending: delay ? program : null, + program: delay ? null : program, + running: !delay, + + start() { + if (info.css) { + 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(', '); + } + }, + + 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); + this.stop(); + }, + + stop() { + if (info.css) transitionManager.deleteRule(node, program.name); + animation.running = false; + } + }; + + transitionManager.add(animation); + + if (info.tick) info.tick(0, 1); + if (!delay) animation.start(); + + return animation; +} + +export function fixPosition(node) { + const style = getComputedStyle(node); + + if (style.position !== 'absolute' && style.position !== 'fixed') { + const { width, height } = style; + const a = node.getBoundingClientRect(); + node.style.position = 'absolute'; + node.style.width = width; + node.style.height = height; + const b = node.getBoundingClientRect(); + + if (a.left !== b.left || a.top !== b.top) { + const style = getComputedStyle(node); + const transform = style.transform === 'none' ? '' : style.transform; + + node.style.transform = `${transform} translate(${a.left - b.left}px, ${a.top - b.top}px)`; + } + } +} \ No newline at end of file diff --git a/src/shared/index.js b/src/shared/index.js index cf82282ad809..c9d901af64d1 100644 --- a/src/shared/index.js +++ b/src/shared/index.js @@ -1,5 +1,6 @@ import { assign } from './utils.js'; import { noop } from './utils.js'; +export * from './animations.js'; export * from './await-block.js'; export * from './dom.js'; export * from './keyed-each.js'; diff --git a/src/shared/keyed-each.js b/src/shared/keyed-each.js index 5e7d7281c63c..9ec3d1c6229f 100644 --- a/src/shared/keyed-each.js +++ b/src/shared/keyed-each.js @@ -12,25 +12,7 @@ export function outroAndDestroyBlock(block, lookup) { } export function fixAndOutroAndDestroyBlock(block, lookup) { - const { node } = block; - const style = getComputedStyle(node); - - if (style.position !== 'absolute' && style.position !== 'fixed') { - const { width, height } = style; - const a = node.getBoundingClientRect(); - node.style.position = 'absolute'; - node.style.width = width; - node.style.height = height; - const b = node.getBoundingClientRect(); - - if (a.left !== b.left || a.top !== b.top) { - const style = getComputedStyle(node); - const transform = style.transform === 'none' ? '' : style.transform; - - node.style.transform = `${transform} translate(${a.left - b.left}px, ${a.top - b.top}px)`; - } - } - + block.f(); outroAndDestroyBlock(block, lookup); } @@ -121,10 +103,10 @@ export function updateKeyedEach(old_blocks, component, changed, get_key, dynamic } export function measure(blocks) { - const measurements = {}; + const rects = {}; let i = blocks.length; - while (i--) measurements[blocks[i].key] = blocks[i].node.getBoundingClientRect(); - return measurements; + while (i--) rects[blocks[i].key] = blocks[i].node.getBoundingClientRect(); + return rects; } export function animate(blocks, rects, fn, params) { @@ -138,67 +120,6 @@ export function animate(blocks, rects, fn, params) { if (from.left === to.left && from.right === to.right && from.top === to.top && from.bottom === to.bottom) continue; - const info = fn(block.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 - }; - - const animation = { - pending: delay ? program : null, - program: delay ? null : program, - running: !delay, - - start() { - if (info.css) { - const rule = generateRule(program, ease, info.css); - program.name = `__svelte_${hash(rule)}`; - - transitionManager.addRule(rule, program.name); - - block.node.style.animation = (block.node.style.animation || '') - .split(', ') - .filter(anim => anim && (program.delta < 0 || !/__svelte/.test(anim))) - .concat(`${program.name} ${program.duration}ms linear 1 forwards`) - .join(', '); - } - }, - - 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.css) { - transitionManager.deleteRule(block.node, program.name); - } - - if (info.tick) { - info.tick(1, 0); - } - - animation.running = false; - } - }; - - transitionManager.add(animation); - - if (info.tick) info.tick(0, 1); - - if (!delay) animation.start(); + } } \ No newline at end of file