diff --git a/src/compile/render-dom/Block.ts b/src/compile/render-dom/Block.ts index bee2072ef1..947185d535 100644 --- a/src/compile/render-dom/Block.ts +++ b/src/compile/render-dom/Block.ts @@ -220,9 +220,10 @@ export default class Block { if (this.hasIntroMethod || this.hasOutroMethod) { this.addVariable('#current'); + this.addVariable('#mounted'); if (!this.builders.mount.isEmpty()) { - this.builders.mount.addLine(`#current = true;`); + this.builders.mount.addLine(`#current = #mounted = true;`); } if (!this.builders.outro.isEmpty()) { @@ -352,9 +353,8 @@ export default class Block { } else { properties.addBlock(deindent` ${dev ? 'i: function intro' : 'i'}(#target, anchor) { - if (#current) return; ${this.builders.intro} - this.m(#target, anchor); + if (!#mounted) this.m(#target, anchor); }, `); } @@ -364,8 +364,6 @@ export default class Block { } else { properties.addBlock(deindent` ${dev ? 'o: function outro' : 'o'}(#outrocallback) { - if (!#current) return; - ${this.outros > 1 && `#outrocallback = @callAfter(#outrocallback, ${this.outros});`} ${this.builders.outro} diff --git a/src/compile/render-dom/wrappers/Element/index.ts b/src/compile/render-dom/wrappers/Element/index.ts index cc479e55e7..b9bf2ef734 100644 --- a/src/compile/render-dom/wrappers/Element/index.ts +++ b/src/compile/render-dom/wrappers/Element/index.ts @@ -621,27 +621,24 @@ export default class ElementWrapper extends Wrapper { ? intro.expression.render(block) : '{}'; - const fn = component.qualify(intro.name); // TODO add built-in transitions? + const fn = component.qualify(intro.name); if (outro) { - block.builders.intro.addBlock(deindent` - if (${introName}) ${introName}.abort(1); - if (${outroName}) ${outroName}.abort(1); + block.builders.intro.addConditional(`@intros.enabled`, deindent` + @add_render_callback(() => { + if (${introName}) ${introName}.end(); + ${introName} = @create_in_transition(${this.var}, ${fn}, ${snippet}); + }); + `); + } else { + block.builders.intro.addConditional(`@intros.enabled`, deindent` + if (!${introName}) { + @add_render_callback(() => { + ${introName} = @create_in_transition(${this.var}, ${fn}, ${snippet}); + }); + } `); } - - block.builders.intro.addConditional(`@intros.enabled`, deindent` - @add_render_callback(() => { - ${introName} = @create_transition(${this.var}, ${fn}, ${snippet}, true); - ${introName}.run(1, () => { - ${introName} = null; - }); - }); - `); - - block.builders.outro.addBlock(deindent` - if (${introName}) ${introName}.abort(); - `); } if (outro) { @@ -653,17 +650,16 @@ export default class ElementWrapper extends Wrapper { const fn = component.qualify(outro.name); block.builders.intro.addBlock(deindent` - if (${outroName}) ${outroName}.abort(1); + if (${outroName}) ${outroName}.end(); `); // 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} = @create_transition(${this.var}, ${fn}, ${snippet}, false); - ${outroName}.run(0, #outrocallback); + ${outroName} = @create_out_transition(${this.var}, ${fn}, ${snippet}, #outrocallback); `); - block.builders.destroy.addConditional('detach', `if (${outroName}) ${outroName}.abort();`); + block.builders.destroy.addConditional('detach', `if (${outroName}) ${outroName}.end();`); } } } diff --git a/src/compile/render-dom/wrappers/shared/Tag.ts b/src/compile/render-dom/wrappers/shared/Tag.ts index 510c2cf756..5fe91b74cc 100644 --- a/src/compile/render-dom/wrappers/shared/Tag.ts +++ b/src/compile/render-dom/wrappers/shared/Tag.ts @@ -27,6 +27,7 @@ export default class Tag extends Wrapper { if (this.node.shouldCache) block.addVariable(value, snippet); if (dependencies.size) { + // TODO can we check `#current` at the top of the `update` method, instead of in individual updates? const changedCheck = ( (block.hasOutros ? `!#current || ` : '') + [...dependencies].map((dependency: string) => `changed.${dependency}`).join(' || ') diff --git a/src/internal/animations.js b/src/internal/animations.js index 97d0cd734a..cf02827dd8 100644 --- a/src/internal/animations.js +++ b/src/internal/animations.js @@ -28,7 +28,7 @@ export function animate(node, from, fn, params) { if (css) { if (delay) node.style.cssText = cssText; - name = create_rule({ a: 0, b: 1, d: 1, duration }, easing, css); + name = create_rule(0, 1, duration, easing, css); node.style.animation = (node.style.animation || '') .split(', ') diff --git a/src/internal/style_manager.js b/src/internal/style_manager.js index cb0a2dde57..0ec992025e 100644 --- a/src/internal/style_manager.js +++ b/src/internal/style_manager.js @@ -13,12 +13,12 @@ function hash(str) { return hash >>> 0; } -export function create_rule({ a, b, d, duration }, ease, fn) { +export function create_rule(a, b, duration, ease, fn) { const step = 16.666 / duration; let keyframes = '{\n'; for (let p = 0; p <= 1; p += step) { - const t = a + d * ease(p); + const t = a + (b - a) * ease(p); keyframes += p * 100 + `%{${fn(t, 1 - t)}}\n`; } @@ -41,6 +41,8 @@ export function create_rule({ a, b, d, duration }, ease, fn) { } export function delete_rule(node, name) { + console.log(`delete ${name} from ${node.textContent}`, { active }); + node.style.animation = node.style.animation .split(', ') .filter(anim => anim.indexOf(name) < 0) @@ -53,6 +55,7 @@ export function clear_rules() { requestAnimationFrame(() => { if (active) return; let i = stylesheet.cssRules.length; + console.log(`clear_rules ${i}`); while (i--) stylesheet.deleteRule(i); current_rules = {}; }); diff --git a/src/internal/transitions.js b/src/internal/transitions.js index 9b3e897960..f353bd5112 100644 --- a/src/internal/transitions.js +++ b/src/internal/transitions.js @@ -1,4 +1,4 @@ -import { identity as linear, noop, run } from './utils.js'; +import { identity as linear, noop, run, run_all } from './utils.js'; import { loop } from './loop.js'; import { create_rule, delete_rule } from './style_manager.js'; @@ -24,6 +24,127 @@ export function group_outros() { }; } +export function create_in_transition(node, fn, params) { + let config = fn(node, params); + let running = true; + let animation_name; + + function cleanup() { + delete_rule(node, animation_name); + } + + wait().then(() => { + if (typeof config === 'function') config = config(); + + const { + delay = 0, + duration = 300, + easing = linear, + tick = noop, + css + } = config; + + if (css) { + animation_name = create_rule(0, 1, duration, easing, css); + node.style.animation = (node.style.animation ? ', ' : '') + `${animation_name} ${duration}ms linear ${delay}ms 1 both`; + } + + tick(0, 1); + + const start_time = window.performance.now() + delay; + const end_time = start_time + duration; + + loop(now => { + if (running) { + if (now > end_time) { + tick(1, 0); + cleanup(); + return running = false; + } + + if (now > start_time) { + const t = easing((now - start_time) / duration); + tick(t, 1 - t); + } + } + + return running; + }); + }); + + return { + end() { + if (running) { + cleanup(); + running = false; + } + } + }; +} + +export function create_out_transition(node, fn, params, callback) { + let config = fn(node, params); + let running = true; + let animation_name; + + const group = outros; + + group.remaining += 1; + group.callbacks.push(callback); // TODO do we even need multiple callbacks? can we just have the one? + + wait().then(() => { + if (typeof config === 'function') config = config(); + + const { + delay = 0, + duration = 300, + easing = linear, + tick = noop, + css + } = config; + + if (css) { + animation_name = create_rule(1, 0, duration, easing, css); + node.style.animation += (node.style.animation ? ', ' : '') + `${animation_name} ${duration}ms linear ${delay}ms 1 both`; + } + + const start_time = window.performance.now() + delay; + const end_time = start_time + duration; + + loop(now => { + if (running) { + if (now > end_time) { + tick(0, 1); + + if (!--group.remaining) { + // this will result in `end()` being called, + // so we don't need to clean up here + run_all(group.callbacks); + } + + return false; + } + + if (now > start_time) { + const t = easing((now - start_time) / duration); + tick(1 - t, t); + } + } + + return running; + }); + }); + + return { + end() { + if (running) { + delete_rule(node, animation_name); + running = false; + } + } + }; +} + export function create_transition(node, fn, params, intro) { let config = fn(node, params); @@ -40,17 +161,17 @@ export function create_transition(node, fn, params, intro) { animation_name = null; } - function start(program, delay, duration, easing) { + function start(program, duration, easing) { node.dispatchEvent(new window.CustomEvent(`${program.b ? 'intro' : 'outro'}start`)); program.a = t; - program.d = program.b - program.a; - program.duration = duration * Math.abs(program.b - program.a); + program.d = program.b - t; + program.duration = duration * Math.abs(program.d); program.end = program.start + program.duration; if (config.css) { clear_animation(); - animation_name = create_rule(program, easing, config.css); + animation_name = create_rule(t, program.b, program.duration, easing, config.css); node.style.animation = (node.style.animation ? ', ' : '') + `${animation_name} ${program.duration}ms linear 1 forwards`; } @@ -101,7 +222,7 @@ export function create_transition(node, fn, params, intro) { if (!ready) { if (config.css && delay) { // TODO can we just use the normal rule, but delay it? - animation_name = create_rule({ a: 0, b: 1, d: 1, duration: 1, }, linear, () => config.css(0, 1)); + animation_name = create_rule(0, 1, 1, linear, () => config.css(0, 1)); node.style.animation = (node.style.animation ? ', ' : '') + `${animation_name} 9999s linear 1 both`; } @@ -117,7 +238,7 @@ export function create_transition(node, fn, params, intro) { if (delay) { pending_program = program; } else { - start(program, delay, duration, easing); + start(program, duration, easing); } if (!running) { @@ -129,7 +250,7 @@ export function create_transition(node, fn, params, intro) { } if (pending_program && now >= pending_program.start) { - start(pending_program, delay, duration, easing); + start(pending_program, duration, easing); } if (running) {