diff --git a/src/compiler/compile/render_dom/wrappers/Element/index.ts b/src/compiler/compile/render_dom/wrappers/Element/index.ts index 28ad3791c2..131d17cf33 100644 --- a/src/compiler/compile/render_dom/wrappers/Element/index.ts +++ b/src/compiler/compile/render_dom/wrappers/Element/index.ts @@ -740,15 +740,16 @@ export default class ElementWrapper extends Wrapper { } add_bidi_transition(block: Block, intro: Transition) { - const name = block.get_unique_name(`${this.var.name}_transition`); + const transition = block.get_unique_name(`${this.var.name}_transition`); const snippet = intro.expression ? intro.expression.manipulate(block) : null; + const fn = this.renderer.reference(intro.name); - block.add_variable(name, x`@noop`); + block.add_variable(transition); + block.chunks.create.push(b`${transition} = @create_bidirectional_transition(${this.var}, ${fn}, ${snippet})`) - const fn = this.renderer.reference(intro.name); - let intro_block = b`${name} = @run_bidirectional_transition(${this.var}, ${fn}, 1, ${snippet});`; - let outro_block = b`${name} = @run_bidirectional_transition(${this.var}, ${fn}, 2, ${snippet});`; + let intro_block = b`${transition}.i();`; + let outro_block = b`${transition}.o();`; if (intro.is_local) { intro_block = b`if (#local) { ${intro_block} }`; @@ -757,7 +758,7 @@ export default class ElementWrapper extends Wrapper { block.chunks.intro.push(intro_block); block.chunks.outro.push(outro_block); - block.chunks.destroy.push(b`if (detaching) ${name}();`); + block.chunks.destroy.push(b`if (detaching) ${transition}.d();`); } add_intro(block: Block, intro: Transition, outro: Transition) { if (outro) { @@ -779,7 +780,7 @@ export default class ElementWrapper extends Wrapper { const [intro_var, node, transitionFn, params] = run_transition(this, block, intro, `intro`); block.add_variable(intro_var, outro ? x`@noop`: null); - let start_intro = b`${intro_var} = @run_transition(${node}, ${transitionFn}, 1, ${params});`; + let start_intro = b`${intro_var} = @run_in(${node}, ${transitionFn}, ${params}, this);`; if (!outro) start_intro = b`if (!${intro_var}) { ${start_intro} }`; if (intro.is_local) start_intro = b`if (#local) { ${start_intro} }`; block.chunks.intro.push(start_intro); @@ -797,7 +798,7 @@ export default class ElementWrapper extends Wrapper { const [outro_var, node, transitionFn, params] = run_transition(this, block, outro, `outro`); block.add_variable(outro_var, x`@noop`); - let start_outro = b`${outro_var} = @run_transition(${node}, ${transitionFn}, 2, ${params});`; + let start_outro = b`${outro_var} = @run_out(${node}, ${transitionFn}, ${params}, this);`; if (outro.is_local) start_outro = b`if (#local) { ${start_outro} }`; block.chunks.outro.push(start_outro); @@ -824,7 +825,7 @@ export default class ElementWrapper extends Wrapper { `); block.chunks.animate.push(b` - if (${unfreeze_var}) return + if (${unfreeze_var} || !${rect_var}) return else { ${stop_animation_var}(); ${stop_animation_var} = @run_animation(${this.var}, ${rect_var}, ${name_var}, ${params_var}); diff --git a/src/runtime/animate/index.ts b/src/runtime/animate/index.ts index 58aeceff7a..b1d4e509a6 100644 --- a/src/runtime/animate/index.ts +++ b/src/runtime/animate/index.ts @@ -1,6 +1,5 @@ import { cubicOut } from "svelte/easing"; -import { run_duration } from "svelte/internal"; -import { CssTransitionConfig, TimeableConfig } from "svelte/transition"; +import { run_duration, TimeableConfig, CssTransitionConfig } from "svelte/internal"; export function flip( node: Element, animation: { from: DOMRect; to: DOMRect }, diff --git a/src/runtime/internal/animations.ts b/src/runtime/internal/animations.ts deleted file mode 100644 index 5f18b073a5..0000000000 --- a/src/runtime/internal/animations.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { run_transition } from './transitions'; -import { methodify, noop } from './utils'; -import { CssTransitionConfig } from 'svelte/transition'; - -type Rect = DOMRect | ClientRect; -type AnimationFn = (node: Element, { from, to }: { from: Rect; to: Rect }, params: any) => CssTransitionConfig; - -export const run_animation = /*#__PURE__*/ methodify( - function run_animation(this: HTMLElement, from: Rect, fn: AnimationFn, params = {}) { - if (!from) return noop; - return run_transition( - this, - (_, params) => { - const to = this.getBoundingClientRect(); - if (from.left !== to.left || from.right !== to.right || from.top !== to.top || from.bottom !== to.bottom) { - return fn(this, { from, to }, params); - } else return null; - }, - 9, - params - ); - } -); - -export const fix_position = /*#__PURE__*/ methodify( - function fix_position(this: HTMLElement, { left, top }: Rect) { - const { position, width, height, transform } = getComputedStyle(this); - if (position === 'absolute' || position === 'fixed') return noop; - const { position: og_position, width: og_width, height: og_height } = this.style; - this.style.position = 'absolute'; - this.style.width = width; - this.style.height = height; - const b = this.getBoundingClientRect(); - this.style.transform = `${transform === 'none' ? '' : transform} translate(${left - b.left}px, ${top - b.top}px)`; - return () => { - this.style.position = og_position; - this.style.width = og_width; - this.style.height = og_height; - this.style.transform = ''; // unsafe - }; - } -); diff --git a/src/runtime/internal/environment.ts b/src/runtime/internal/environment.ts index b424c769ab..178f167a9c 100644 --- a/src/runtime/internal/environment.ts +++ b/src/runtime/internal/environment.ts @@ -1,20 +1,12 @@ -import { noop } from './utils'; +import { noop } from "./utils"; -export const is_client = typeof window !== 'undefined'; +export const is_client = typeof window !== "undefined"; export let now: () => number = is_client ? () => window.performance.now() : () => Date.now(); -export let raf = is_client ? cb => requestAnimationFrame(cb) : noop; -export let framerate = 1000 / 60; -/*#__PURE__*/ raf((t1) => { - raf((d) => { - const f24 = 1000 / 24; - const f144 = 1000 / 144; - framerate = (d = d - t1) > f144 ? f144 : d < f24 ? f24 : d; - }); -}); +export let raf = /*#__PURE__*/ is_client ? (cb: FrameRequestCallback) => requestAnimationFrame(cb) : noop; // used internally for testing export function set_now(fn) { diff --git a/src/runtime/internal/index.ts b/src/runtime/internal/index.ts index e1dd2a1fcf..253b8fccc8 100644 --- a/src/runtime/internal/index.ts +++ b/src/runtime/internal/index.ts @@ -1,4 +1,3 @@ -export * from './animations'; export * from './await_block'; export * from './dom'; export * from './environment'; @@ -12,4 +11,5 @@ export * from './ssr'; export * from './transitions'; export * from './utils'; export * from './Component'; +export * from './style_manager' export * from './dev'; diff --git a/src/runtime/internal/loop.ts b/src/runtime/internal/loop.ts index 993f565f63..bdecd318cc 100644 --- a/src/runtime/internal/loop.ts +++ b/src/runtime/internal/loop.ts @@ -1,5 +1,31 @@ -import { now, raf, framerate } from './environment'; +import { now, raf } from './environment'; import { noop } from './utils'; + +export const frame = { + rate: 1000 / 60, + time: 0.0, + sync() { + return n ? this.time : (this.time = now()); + }, +}; + +function calc_framerate() { + raf((t1) => { + raf((t2) => { + const delta = t2 - t1; + raf((t3) => { + if (Math.abs(t3 - t2 - delta) > 1) { + calc_framerate(); + } else { + const f24 = 1000 / 24; + const f144 = 1000 / 144; + frame.rate = delta > f144 ? f144 : delta < f24 ? f24 : delta; + } + }); + }); + }); +} +calc_framerate(); type TaskCallback = (t: number) => boolean; type TaskCanceller = () => void; @@ -13,7 +39,7 @@ let next_frame: TaskCallback[] = []; const run = (t: number) => { [running_frame, next_frame] = [next_frame, running_frame]; - for (t = now(), i = n = 0, j = running_frame.length; i < j; i++) { + for (t = (frame.time = now()), i = n = 0, j = running_frame.length; i < j; i++) { if ((v = running_frame[i])(t)) { next_frame[n++] = v; } @@ -33,22 +59,26 @@ let running_timed = false; const run_timed = (now: number) => { let last_index = timed_tasks.length - 1; - while (~last_index && now >= timed_tasks[last_index].timestamp) timed_tasks[last_index--].callback(now); + while (-1 !== last_index && now >= timed_tasks[last_index].timestamp) { + timed_tasks[last_index--].callback(now); + } if (pending_inserts) { - for (let i = 0, j = 0, this_task: TimeoutTask, that_task: TimeoutTask; i < pending_insert_timed.length; i++) - if (now >= (this_task = pending_insert_timed[i]).timestamp) this_task.callback(now); - else { - for (j = last_index; ~j && this_task.timestamp > (that_task = timed_tasks[j]).timestamp; j--) + for (let i = 0, j = 0, this_task: TimeoutTask, that_task: TimeoutTask; i < pending_insert_timed.length; i++) { + if (now >= (this_task = pending_insert_timed[i]).timestamp) { + this_task.callback(now); + } else { + for (j = last_index; -1 !== j && this_task.timestamp > (that_task = timed_tasks[j]).timestamp; j--) { timed_tasks[j + 1] = that_task; + } timed_tasks[j + 1] = this_task; last_index++; } + } pending_insert_timed.length = 0; pending_inserts = false; } return (running_timed = !!(timed_tasks.length = last_index + 1)); }; - const unsafe_loop = (fn) => { if (0 === n) raf(run); next_frame[n++] = fn; @@ -56,8 +86,7 @@ const unsafe_loop = (fn) => { export const loop = (fn) => { let running = true; - if (0 === n) raf(run); - next_frame[n++] = (t) => !running || fn(t); + unsafe_loop((t) => running && fn(t)); return () => void (running = false); }; @@ -70,52 +99,23 @@ export const setFrameTimeout = (callback: (t: number) => void, timestamp: number running_timed = true; timed_tasks.push(task); } - return () => void (task.callback = noop); + return () => { + task.callback = noop; + }; }; - -/** - * Calls function every frame with linear tween from 0 to 1 - */ export const setTweenTimeout = ( stop: (now: number) => void, end_time: number, run: (now: number) => void, - duration = end_time - now(), - is_outro = false + duration = end_time - frame.sync() ): TaskCanceller => { - let running = true; - let t = 1 - (end_time - now()) / duration || 0; - if (!is_outro && t <= 1.0) run(t >= 0.0 ? t : 0); - unsafe_loop((now) => { - if (!running) return false; + let t = 0.0; + return loop((now) => { t = 1 - (end_time - now) / duration; if (t >= 1.0) return run(1), stop(now), false; if (t >= 0.0) run(t); - return running; + return true; }); - return (run_last = false) => { - if (run_last) run(1); - running = false; - }; -}; -/** - * Calls function every frame with time elapsed in seconds - */ -export const onEachFrame = ( - callback: (seconds_elapsed: number) => boolean, - on_stop?, - max_skipped_frames = 4 -): TaskCanceller => { - max_skipped_frames *= framerate; - let lastTime = now(); - let running = true; - const cancel = (t) => (on_stop && on_stop(t), false); - unsafe_loop((t: number) => { - if (!running) return cancel(t); - if (t > lastTime + max_skipped_frames) t = lastTime + max_skipped_frames; - return callback((-lastTime + (lastTime = t)) / 1000) ? true : cancel(t); - }); - return () => void (running = false); }; /** tests only */ diff --git a/src/runtime/internal/scheduler.ts b/src/runtime/internal/scheduler.ts index 99b6dd940f..b7433cb311 100644 --- a/src/runtime/internal/scheduler.ts +++ b/src/runtime/internal/scheduler.ts @@ -1,6 +1,6 @@ import { set_current_component } from './lifecycle'; -import { now } from './environment'; import { T$$ } from './Component'; +import { frame } from './loop'; const resolved_promise = Promise.resolve(); @@ -17,7 +17,9 @@ const flush_callbacks = []; // todo : remove add_flush_callback export const add_flush_callback = /*#__PURE__*/ Array.prototype.push.bind(flush_callbacks); -export const add_measure_callback = /*#__PURE__*/ Array.prototype.push.bind(measure_callbacks); +type MeasureCallback = () => FlushCallback +type FlushCallback = (current_frame_time: number) => void +export const add_measure_callback: (...args: MeasureCallback[]) => number = /*#__PURE__*/ Array.prototype.push.bind(measure_callbacks); const seen_render_callbacks = new Set(); export const add_render_callback = (fn) => { @@ -48,9 +50,10 @@ export const flush = () => { if (is_flushing) return; else is_flushing = true; + frame.sync(); + let i = 0; let j = 0; - let t = 0; let $$: T$$; let dirty; let before_update; @@ -103,7 +106,7 @@ export const flush = () => { // apply styles // todo : remove every non style callback from flush_callbacks - for (t = now(); i < j; i++) flush_callbacks[i](t); + for (const t = frame.time; i < j; i++) flush_callbacks[i](t); flush_callbacks.length = i = j = 0; is_flushing = false; diff --git a/src/runtime/internal/style_manager.ts b/src/runtime/internal/style_manager.ts index 2efbae0ec9..8090417c02 100644 --- a/src/runtime/internal/style_manager.ts +++ b/src/runtime/internal/style_manager.ts @@ -1,5 +1,5 @@ -import { framerate } from './environment'; -import { methodify } from './utils'; +import { frame } from './loop'; +import { methodify, noop } from './utils'; let documents_uid = 0; let running_animations = 0; @@ -9,7 +9,7 @@ const document_stylesheets = new Map(); const current_rules = new Set(); export const animate_css = /*#__PURE__*/ methodify( - function animate_css(this: HTMLElement, css: (t: number) => string, duration: number, delay = 0) { + function (this: HTMLElement, css: (t: number) => string, duration: number, delay = 0) { if (!document_uid.has(this.ownerDocument)) { document_uid.set(this.ownerDocument, documents_uid++); document_stylesheets.set( @@ -18,7 +18,7 @@ export const animate_css = /*#__PURE__*/ methodify( ); } let rule = '{\n'; - for (let t = 0, step = framerate / duration; t < 1; t += step) rule += `${100 * t}%{${css(t)}}\n`; + for (let t = 0, step = frame.rate / duration; t < 1; t += step) rule += `${100 * t}%{${css(t)}}\n`; rule += `100% {${css(1)}}\n}`; // darkskyapp/string-hash @@ -34,6 +34,7 @@ export const animate_css = /*#__PURE__*/ methodify( } const previous = this.style.animation; + if (previous) {console.error("stacked animations"); return noop} this.style.animation = `${ previous ? `${previous}, ` : '' }${duration}ms linear ${delay}ms 1 normal both running ${name}`; @@ -58,4 +59,23 @@ export const animate_css = /*#__PURE__*/ methodify( } }; } +); +export const fix_position = /*#__PURE__*/ methodify( + function (this: HTMLElement, { left, top }: DOMRect | ClientRect) { + const { position, width, height, transform } = getComputedStyle(this); + if (position === 'absolute' || position === 'fixed') return noop; + const { position: og_position, width: og_width, height: og_height, transform: og_transform } = this.style; + this.style.position = 'absolute'; + this.style.width = width; + this.style.height = height; + const b = this.getBoundingClientRect(); + this.style.transform = `${transform === 'none' ? '' : transform} translate(${left - b.left}px, ${top - b.top}px)`; + return () => { + // unsafe + this.style.position = og_position; + this.style.width = og_width; + this.style.height = og_height; + this.style.transform = og_transform; + }; + } ); \ No newline at end of file diff --git a/src/runtime/internal/transitions.ts b/src/runtime/internal/transitions.ts index 0776a4e5d6..3eb64bc6ee 100644 --- a/src/runtime/internal/transitions.ts +++ b/src/runtime/internal/transitions.ts @@ -1,44 +1,70 @@ -import { CssTransitionConfig } from '../transition'; -import { Fragment } from './Component'; -import { custom_event } from './dom'; -import { now } from './environment'; -import { setFrameTimeout, setTweenTimeout } from './loop'; -import { add_measure_callback } from './scheduler'; -import { animate_css } from './style_manager'; -import { methodify, noop } from './utils'; - -type TransitionFn = (node: HTMLElement, params: any) => CssTransitionConfig; -export type StopResetReverseFn = (t?: number | -1) => StopResetReverseFn | void; +import { Fragment } from "./Component"; +import { custom_event } from "./dom"; +import { setFrameTimeout, setTweenTimeout, frame } from "./loop"; +import { add_measure_callback, tick } from "./scheduler"; +import { animate_css } from "./style_manager"; +import { methodify, noop } from "./utils"; + +export interface CssAnimationConfig { + delay?: number; + duration?: number; + easing?: (t: number) => number; +} +export interface CssTransitionConfig extends CssAnimationConfig { + css?: (t: number, u?: number) => string; + tick?: (t: number, u?: number) => void; + strategy?: EasingStrategy; +} +export type TimeableConfig = Omit & { duration?: number | ((len: number) => number) }; +const enum EasingStrategy { + reversed = "reversed", + balanced = "balanced", + mirrored = "mirrored", +} +const enum TransitionEvent { + introstart = "introstart", + introend = "introend", + outrostart = "outrostart", + outroend = "outroend" +} export const transition_in = (block: Fragment, local?) => { + // todo : is `!block` necessary ? if (!block || !block.i) return; outroing.delete(block); block.i(local); }; export const transition_out = (block: Fragment, local?) => { + // todo : are `!block` and `outroing.has` checks necessary ? if (!block || !block.o || outroing.has(block)) return; outroing.add(block); block.o(local); }; type TransitionGroup = { /* parent group */ p: TransitionGroup; - /* callbacks */ c: Array<((cancelled: boolean) => void)>; + /* callbacks */ c: Array<(cancelled: boolean) => void>; /* running outros */ r: number; - /* stop callbacks */ s: Array<((t: number) => void)>; + /* stop callbacks */ s: Array<(t: number) => void>; /* outro timeout */ t: number; }; let transition_group: TransitionGroup; const outroing = new Set(); export const group_transition_out = (fn) => { const c = []; - const current_group = (transition_group = { p: transition_group, c, r: 0, s: [], t: 0 }); + const current_group = (transition_group = { + p: transition_group, + c, + r: 0, + s: [], + t: 0, + }); fn((block, callback, detach = true) => { if (!block || !block.o || outroing.has(block)) return; outroing.add(block); c.push((cancelled = false) => { if (cancelled) { - // block was destroyed before outro ended + // block destroyed before outro ended outroing.delete(block); } else if (outroing.has(block)) { outroing.delete(block); @@ -51,149 +77,256 @@ export const group_transition_out = (fn) => { if (!current_group.r) for (let i = 0; i < c.length; i++) c[i](); transition_group = transition_group.p; }; +type Rect = DOMRect | ClientRect; +type MeasureCallback = () => CssTransitionConfig +type CustomTransitionFunction = (node: HTMLElement, params: any) => MeasureCallback | CssTransitionConfig; +type AnimationFn = (node: Element, { from, to }: { from: Rect; to: Rect }, params: any) => CssTransitionConfig; +type StopResetReverseFn = (t?: number | 1 | -1) => StopResetReverseFn | void; -const swap = (fn, rx) => +const swap = (fn, is_intro) => fn.length === 1 - ? rx & tx.intro + ? is_intro ? fn : (t) => fn(1 - t) - : rx & tx.intro + : is_intro ? (t) => fn(t, 1 - t) : (t) => fn(1 - t, t); -const mirrored = (fn, rx, easing, _start, _end) => { - const run = swap(fn, rx); +const mirrored = (fn, is_intro, easing, _start?, _end?) => { + const run = swap(fn, is_intro); return easing - ? rx & tx.intro + ? is_intro ? (t) => run(easing(t)) : (t) => run(1 - easing(1 - t)) : run; }; -const reversed = (fn, rx, easing, start = 0, end = 1) => { - const run = swap(fn, rx); + +const reversed = (fn, is_intro, easing, start = 0, end = 1) => { + const run = swap(fn, is_intro); const difference = end - start; return easing ? (t) => run(start + difference * easing(t)) : (t) => run(start + difference * t); }; -const enum tx { - intro = 1, - outro = 2, - reverse = 3, - bidirectional = 4, - animation = 8, -} -export const run_transition = /*#__PURE__*/ methodify(function transition( - this: HTMLElement, - fn: TransitionFn, - rx: tx, - params = {}, - /* internal to this file */ - elapsed_duration = 0, - delay_left = -1, - elapsed_ratio = 0 -) { + +export const run_animation = /*#__PURE__*/ methodify(function (this: HTMLElement, from : Rect, fn: AnimationFn, params: CssTransitionConfig = {}) { + let running = true; + let cancel_css; + let cancel_raf; + add_measure_callback(() => { + const to = this.getBoundingClientRect(); + if (from.top === to.top && from.left === to.left && from.right === to.right && from.bottom === to.bottom) return noop; + const config = fn(this, { from, to }, params); + return (current_frame_time) => { + if (false === running) return; + const { delay = 0, duration = 300, easing, tick, css }: CssTransitionConfig = config; + const end_time = current_frame_time + delay + duration; + const runner = (fn) => reversed(fn, true, easing); + if (css) cancel_css = animate_css(this, runner(css), duration, delay); + cancel_raf = tick ? setTweenTimeout(stop, end_time, runner(tick), duration) : setFrameTimeout(stop, end_time); + }; + }); + const stop = () => { + if (false === running) return; + else running = false; + if (cancel_css) cancel_css(); + if (cancel_raf) cancel_raf(); + } + return stop +}); +export const run_in = /*#__PURE__*/ methodify(function (this: HTMLElement, fn: CustomTransitionFunction, params: CssTransitionConfig = {}) { let config; - let running = true; - let cancel_css; - let cancel_raf; - - let start_time = 0; - let end_time = 0; - + let cancel_raf; + let end_time; + add_measure_callback(() => { + config = fn(this, params); + return (current_frame_time) => { + let { delay = 0, duration = 300, easing, tick, css, strategy = EasingStrategy.balanced }: CssTransitionConfig = + "function" === typeof config ? (config = config()) : config; + const solver = EasingStrategy.balanced === strategy ? reversed : mirrored; + const runner = (fn) => solver(fn, true, easing); + end_time = current_frame_time + delay + duration; + this.dispatchEvent(custom_event(TransitionEvent.introstart)); + if (css) cancel_css = animate_css(this, runner(css), duration, delay); + cancel_raf = tick ? setTweenTimeout(stop, end_time, runner(tick), duration ) : setFrameTimeout(stop, end_time); + }; + }); + const stop = (t?: number) => { + if (false === running) return; + else running = false; + if (cancel_css) cancel_css(); + if (cancel_raf) cancel_raf(); + if (t && t >= end_time) this.dispatchEvent(custom_event(TransitionEvent.introend)); + } + return stop +}); +export const run_out = /*#__PURE__*/ methodify(function (this: HTMLElement, fn: CustomTransitionFunction, params: CssTransitionConfig = {}) { + let config; + let running = true; + let cancel_css; + let cancel_raf; + let end_time; const current_group = transition_group; - if (rx & tx.outro) current_group.r++; - + current_group.r++; add_measure_callback(() => { - if (null === (config = fn(this, params))) return noop; + config = fn(this, params); return (current_frame_time) => { - if (false === running) return; + let { delay = 0, duration = 300, easing, tick, css, strategy = EasingStrategy.balanced }: CssTransitionConfig = + "function" === typeof config ? (config = config()) : config; + const solver = EasingStrategy.balanced === strategy ? reversed : mirrored; + const runner = (fn) => solver(fn, false, easing); + end_time = current_frame_time + delay + duration; + current_group.t = Math.max(end_time, current_group.t); + if (current_group.s.push(stop) === current_group.r) { + setFrameTimeout((t) => { + for (let i = 0; i < current_group.s.length; i++) { + current_group.s[i](t); + } + }, current_group.t); + } + this.dispatchEvent(custom_event(TransitionEvent.outrostart)); + if (css) cancel_css = animate_css(this, runner(css), duration, delay); + if (tick) cancel_raf = setTweenTimeout(noop, end_time, runner(tick), duration); + }; + }); + const stop = (t?: number) => { + if (1 === t && "tick" in config) config.tick(1, 0); + if (false === running) return; + else running = false; + if (cancel_css) cancel_css(); + if (cancel_raf) cancel_raf(); + if (t && t >= end_time) { + if ("tick" in config) config.tick(0, 1); + this.dispatchEvent(custom_event(TransitionEvent.outroend)); + } + if(!--current_group.r) for (let i = 0, { c } = current_group, r = t === void 0;i < c.length;i++) c[i](r); + } + return stop +}); +export const create_bidirectional_transition = /*#__PURE__*/ methodify(function(this: HTMLElement, fn: CustomTransitionFunction, params?: CssTransitionConfig) { + let transition_delay; + let pending = 0; + let prev; - let { delay = 0, duration = 300, easing, tick, css, strategy = 'reverse' }: CssTransitionConfig = - 'function' === typeof config ? (config = config()) : config; + const u = (new_fn = fn, new_params = params) => { + let test_config; + if (typeof (test_config = (fn = new_fn)(this,(params = new_params))) === "function") test_config = test_config(); + transition_delay = test_config.delay || 0.0; + } + u(); - const solver = 'reverse' === strategy ? reversed : mirrored; - const runner = (fn) => solver(fn, rx, easing, elapsed_ratio, 1); + const run_transition = (is_intro:boolean, cancel_previous?) => { + const delayed_start = transition_delay && cancel_previous && pending; - if (rx & tx.bidirectional) { - if (-1 !== delay_left) delay = delay_left; - if (solver === reversed) duration -= elapsed_duration; - else if (solver === mirrored) delay -= elapsed_duration; - } + let config; + + let running = true; + let cancelled = false; + + let cancel_css; + let cancel_raf; - end_time = (start_time = current_frame_time + delay) + duration; + let start_time = 0.0; + let end_time = 0.0; + let ratio_left = 0.0; - if (0 === (rx & tx.animation)) { - this.dispatchEvent(custom_event(`${rx & tx.intro ? 'in' : 'ou'}trostart`)); + const current_group = transition_group; + if (!is_intro) current_group.r++; + + const run = (flush_frame_time) => { + pending++; + const [prev_duration_left, prev_ratio_left] = ((cancel_previous && cancel_previous(flush_frame_time)) || [0.0, 0.0] ); + ratio_left = prev_ratio_left; + return () => { + config = fn(this, params); + return (current_frame_time) => { + let { tick, css, duration = 300.0, delay = 0.0, easing, strategy = EasingStrategy.balanced }: CssTransitionConfig = + "function" === typeof config ? (config = config()) : config; + const solver = EasingStrategy.balanced === strategy ? reversed : mirrored; + const runner = (fn) => solver(fn, is_intro, easing, ratio_left, 1); + if (delayed_start) delay = 0; + if (solver === reversed) duration -= prev_duration_left; + else if (solver === mirrored) delay -= prev_duration_left; + start_time = current_frame_time + delay; + end_time = start_time + duration; + if (cancelled) return; + if (!is_intro) { + current_group.t = Math.max(end_time, current_group.t); + if (current_group.s.push(stop) === current_group.r) { + setFrameTimeout((t) => { + for (let i = 0; i < current_group.s.length; i++) { + current_group.s[i](t); + } + }, current_group.t); + } + } + this.dispatchEvent(custom_event(is_intro ? TransitionEvent.introstart : TransitionEvent.outrostart)); + if (css) cancel_css = animate_css(this, runner(css), duration, delay); + if (tick) cancel_raf = setTweenTimeout(is_intro ? stop : noop, end_time, runner(tick), duration); + else if (is_intro) cancel_raf = setFrameTimeout(stop, end_time); + }; + }; + } + + const cancel = (t) => { + if (!cancelled) { + pending--; + cancelled = true; + if (cancel_css) cancel_css(); + if (cancel_raf) cancel_raf(); + if (1 === t && cancel_previous) cancel_previous(); } + if (!config) return; + const duration_left = end_time - t; + const next_ratio_left = 1 - duration_left / (end_time - start_time); + return duration_left > 0 && next_ratio_left > 0 && [duration_left, (1 - ratio_left) * (1 - (config.easing || ((v) => v))(next_ratio_left))]; + }; - if (css) cancel_css = animate_css(this, runner(css), duration, delay); - - if (rx & tx.outro) { - if (current_group.s.push(stop) === current_group.r) { - setFrameTimeout((t) => { - for (let i = 0; i < current_group.s.length; i++) current_group.s[i](t); - }, Math.max(end_time, current_group.t)); - } else { - current_group.t = Math.max(end_time, current_group.t); + const stop: StopResetReverseFn = (t?: number | -1 | 1) => { + if (running) { + running = false; + if (config) { + if (t >= end_time) { + if (!is_intro && "tick" in config) config.tick(0, 1); + if (pending === 1) cancel(t); + this.dispatchEvent(custom_event(is_intro ? TransitionEvent.introend : TransitionEvent.outroend)); + } + if (!is_intro) { + if (!--current_group.r) { + for (let i = 0, { c } = current_group, r = t === 1; i < c.length; i++) c[i](r); + } + } } - if (tick) cancel_raf = setTweenTimeout(noop, end_time, runner(tick), duration, true); - } else { - cancel_raf = tick ? setTweenTimeout(stop, end_time, runner(tick), duration) : setFrameTimeout(stop, end_time); } + if (t === -1) return run_transition(!is_intro, cancel); }; - }); - - const stop: StopResetReverseFn = (t?: number | 1 | -1) => { - if (t === 1 && rx & tx.outro && 0 === (rx & tx.bidirectional) && 'tick' in config) config.tick(1, 0); - if (false === running) return; - else running = false; + if (delayed_start) { + setFrameTimeout((t) => { + add_measure_callback(run(t)); + tick(); + }, frame.time + transition_delay); + } else { + add_measure_callback(run(frame.time)); + } - if (cancel_css) cancel_css(); - if (cancel_raf) cancel_raf(rx & tx.outro && t >= end_time); - - if (rx & tx.animation) return; - - if (t >= end_time) this.dispatchEvent(custom_event(`${rx & tx.intro ? 'in' : 'ou'}troend`)); - - if (rx & tx.outro && !--current_group.r) - for (let i = 0; i < current_group.c.length; i++) current_group.c[i](t === void 0); - - if (0 === (rx & tx.bidirectional)) return; - - if (-1 === t) - return ( - (t = now()) < end_time && - run_transition( - this, - () => config, - rx ^ tx.reverse, - params, - end_time - t, - start_time > t ? start_time - t : 0, - (1 - elapsed_ratio) * (1 - (config.easing || ((v) => v))(1 - (end_time - t) / (end_time - start_time))) - ) - ); - else running_bidi.delete(this); + return stop }; - return stop; -}); - -const running_bidi: Map = new Map(); -export const run_bidirectional_transition = /*#__PURE__*/ methodify( - function bidirectional(this: HTMLElement, fn: TransitionFn, rx: tx.intro | tx.outro, params: any ) { - let cancel; - running_bidi.set( - this, - (cancel = - (running_bidi.has(this) && running_bidi.get(this)(-1)) || run_transition(this, fn, rx | tx.bidirectional, params)) - ); - return cancel; + return { + u, + o() { + prev = prev ? prev(-1) : run_transition(false); + }, + i() { + prev = prev ? prev(-1) : run_transition(true); + }, + d() { + prev = prev(1); + } } -); +}); export const run_duration = (duration, value1, value2?): number => - typeof duration === 'function' ? duration(value1, value2) : duration; + typeof duration === "function" ? duration(value1, value2) : duration; diff --git a/src/runtime/internal/utils.ts b/src/runtime/internal/utils.ts index 9f8addb65e..65d6036b61 100644 --- a/src/runtime/internal/utils.ts +++ b/src/runtime/internal/utils.ts @@ -146,8 +146,8 @@ export const has_prop = (obj, prop) => Object.prototype.hasOwnProperty.call(obj, export function action_destroyer(action_result) { return action_result && is_function(action_result.destroy) ? action_result.destroy : noop; } - -export const methodify = /*#__PURE__*/ (function() { +type Methodify any> = (thisType : ThisParameterType, ...parameters :Parameters) => T extends (...args: Parameters) => infer R ? R : any +export const methodify: any>(fn: T) => Methodify = /*#__PURE__*/ (function () { const call = Function.prototype.call; return call.bind.bind(call); })(); \ No newline at end of file diff --git a/src/runtime/transition/index.ts b/src/runtime/transition/index.ts index 7c262b194b..88daba3c88 100644 --- a/src/runtime/transition/index.ts +++ b/src/runtime/transition/index.ts @@ -1,25 +1,12 @@ import { cubicOut, cubicInOut } from 'svelte/easing'; -import { run_duration } from 'svelte/internal'; +import { run_duration, CssAnimationConfig, CssTransitionConfig, TimeableConfig } from 'svelte/internal'; -interface CssAnimationConfig { - delay?: number; - duration?: number; - easing?: (t: number) => number; - strategy?: 'reverse' | 'mirror'; -} - -export interface CssTransitionConfig extends CssAnimationConfig { - css?: (t: number, u?: number) => string; - tick?: (t: number, u?: number) => void; -} - -type FlyParams = FadingConfig & { x: number; y: number }; +type FlyParams = FadingConfig & { x: number; y: number; rotate: number }; type BlurParams = FadingConfig & { amount: number }; type ScaleParams = FadingConfig & { start: number }; type DrawParams = CssAnimationConfig & { speed: number }; type FadingConfig = CssAnimationConfig & { opacity: number }; type MarkedCrossFadeConfig = TimeableConfig & { key: any }; -export type TimeableConfig = Omit & { duration?: number | ((len: number) => number) }; type CrossFadeConfig = TimeableConfig & { fallback(node: Element, params: TimeableConfig, intro: boolean): CssTransitionConfig }; type ElementMap = Map; @@ -41,7 +28,7 @@ export function fade(node: Element, { delay = 0, duration = 400, easing }: CssAn return { delay, duration, easing, css: (t) => `opacity: ${t * o};` }; } -export function fly(node: Element, { delay = 0, duration = 400, easing = cubicOut, x = 0, y = 0, opacity = 0 }: FlyParams ): CssTransitionConfig { +export function fly(node: Element, { delay = 0, duration = 400, easing = cubicOut, x = 0, y = 0, opacity = 0, rotate = 0 }: FlyParams ): CssTransitionConfig { const style = getComputedStyle(node); const target_opacity = +style.opacity; const prev = style.transform === 'none' ? '' : style.transform; @@ -50,7 +37,7 @@ export function fly(node: Element, { delay = 0, duration = 400, easing = cubicOu delay, duration, easing, - css: (_t, u) => `transform: ${prev} translate(${u * x}px, ${u * y}px); opacity: ${target_opacity - od * u};`, + css: (_t, u) => `transform: ${prev} translate(${u * x}px, ${u * y}px) rotate(${u * rotate}deg); opacity: ${target_opacity - od * u};`, }; } @@ -129,22 +116,23 @@ export function crossfade({ delay: default_delay = 0, duration: default_duration } as CssTransitionConfig; }; - const transition = (a: ElementMap, b: ElementMap, is_intro: boolean) => ( node: Element, params: MarkedCrossFadeConfig ) => { + const transition = (a: ElementMap, b: ElementMap, is_intro: boolean) => ( to_node: Element, params: MarkedCrossFadeConfig ) => { const { key } = params; - a.set(key, node); + a.set(key, to_node); if (b.has(key)) { const from_node = b.get(key); b.delete(key); - return crossfade(from_node, node, params); + return crossfade(from_node, to_node, params); } else { return () => { if (b.has(key)) { const from_node = b.get(key); b.delete(key); - return crossfade(from_node, node, params); + return crossfade(from_node, to_node, params); } else { + debugger a.delete(key); - return fallback && fallback(node, params, is_intro); + return fallback && fallback(to_node, params, is_intro); } }; }