|
|
|
|
@ -1,31 +1,13 @@
|
|
|
|
|
import { EACH_IS_ANIMATED, EACH_IS_CONTROLLED } from '../../../../constants.js';
|
|
|
|
|
import { run_all } from '../../../common.js';
|
|
|
|
|
import {
|
|
|
|
|
AWAIT_BLOCK,
|
|
|
|
|
DYNAMIC_COMPONENT_BLOCK,
|
|
|
|
|
EACH_BLOCK,
|
|
|
|
|
EACH_ITEM_BLOCK,
|
|
|
|
|
IF_BLOCK,
|
|
|
|
|
KEY_BLOCK,
|
|
|
|
|
ROOT_BLOCK
|
|
|
|
|
} from '../../constants.js';
|
|
|
|
|
import { noop } from '../../../common.js';
|
|
|
|
|
import { destroy_each_item_block, get_first_element } from '../blocks/each.js';
|
|
|
|
|
import { schedule_raf_task } from '../task.js';
|
|
|
|
|
import { append_child, empty } from '../operations.js';
|
|
|
|
|
import {
|
|
|
|
|
destroy_effect,
|
|
|
|
|
effect,
|
|
|
|
|
managed_effect,
|
|
|
|
|
managed_pre_effect
|
|
|
|
|
} from '../../reactivity/effects.js';
|
|
|
|
|
import {
|
|
|
|
|
current_block,
|
|
|
|
|
current_effect,
|
|
|
|
|
execute_effect,
|
|
|
|
|
mark_subtree_inert,
|
|
|
|
|
untrack
|
|
|
|
|
} from '../../runtime.js';
|
|
|
|
|
import { user_effect } from '../../reactivity/effects.js';
|
|
|
|
|
import { current_effect, execute_effect, untrack } from '../../runtime.js';
|
|
|
|
|
import { raf } from '../../timing.js';
|
|
|
|
|
import { loop } from '../../loop.js';
|
|
|
|
|
import { run_transitions } from '../../render.js';
|
|
|
|
|
|
|
|
|
|
const active_tick_animations = new Set();
|
|
|
|
|
const DELAY_NEXT_TICK = Number.MIN_SAFE_INTEGER;
|
|
|
|
|
@ -53,7 +35,8 @@ export function transition(dom, get_transition_fn, props, global = false) {
|
|
|
|
|
* @returns {void}
|
|
|
|
|
*/
|
|
|
|
|
export function animate(dom, get_transition_fn, props) {
|
|
|
|
|
bind_transition(dom, get_transition_fn, props, 'key', false);
|
|
|
|
|
// TODO
|
|
|
|
|
// bind_transition(dom, get_transition_fn, props, 'key', false);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
@ -305,517 +288,152 @@ function handle_raf(time) {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** @param {number} t */
|
|
|
|
|
const linear = (t) => t;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @param {{(t: number): number;(t: number): number;(arg0: number): any;}} easing_fn
|
|
|
|
|
* @param {((t: number, u: number) => string)} css_fn
|
|
|
|
|
* @param {number} duration
|
|
|
|
|
* @param {string} direction
|
|
|
|
|
* @param {boolean} reverse
|
|
|
|
|
* @param {Set<import('../../types.js').Transition>} transitions
|
|
|
|
|
* @param {'in' | 'out' | 'key'} target_direction
|
|
|
|
|
* @param {DOMRect} [from]
|
|
|
|
|
* @returns {void}
|
|
|
|
|
*/
|
|
|
|
|
function create_keyframes(easing_fn, css_fn, duration, direction, reverse) {
|
|
|
|
|
/** @type {Keyframe[]} */
|
|
|
|
|
const keyframes = [];
|
|
|
|
|
// We need at least two frames
|
|
|
|
|
const frame_time = 16.666;
|
|
|
|
|
const max_duration = Math.max(duration, frame_time);
|
|
|
|
|
// Have a keyframe every fame for 60 FPS
|
|
|
|
|
for (let i = 0; i <= max_duration; i += frame_time) {
|
|
|
|
|
let time;
|
|
|
|
|
if (i + frame_time > max_duration) {
|
|
|
|
|
time = 1;
|
|
|
|
|
} else if (i === 0) {
|
|
|
|
|
time = 0;
|
|
|
|
|
} else {
|
|
|
|
|
time = i / max_duration;
|
|
|
|
|
}
|
|
|
|
|
let t = easing_fn(time);
|
|
|
|
|
if (reverse) {
|
|
|
|
|
t = 1 - t;
|
|
|
|
|
}
|
|
|
|
|
keyframes.push(css_to_keyframe(css_fn(t, 1 - t)));
|
|
|
|
|
}
|
|
|
|
|
if (direction === 'out' || reverse) {
|
|
|
|
|
keyframes.reverse();
|
|
|
|
|
}
|
|
|
|
|
return keyframes;
|
|
|
|
|
export function trigger_transitions(transitions, target_direction, from) {
|
|
|
|
|
// noop, until we excise it from the codebase
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** @param {number} t */
|
|
|
|
|
const linear = (t) => t;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @param {HTMLElement} dom
|
|
|
|
|
* @param {() => import('../../types.js').TransitionPayload} init
|
|
|
|
|
* @param {'in' | 'out' | 'both' | 'key'} direction
|
|
|
|
|
* @param {import('../../types.js').Effect} effect
|
|
|
|
|
* @returns {import('../../types.js').Transition}
|
|
|
|
|
* @template P
|
|
|
|
|
* @param {HTMLElement} element
|
|
|
|
|
* @param {() => import('#client').TransitionFn<P | undefined>} get_fn
|
|
|
|
|
* @param {(() => P) | null} get_params
|
|
|
|
|
* @param {'in' | 'out' | 'both'} direction
|
|
|
|
|
* @param {boolean} global
|
|
|
|
|
* @returns {void}
|
|
|
|
|
*/
|
|
|
|
|
function create_transition(dom, init, direction, effect) {
|
|
|
|
|
let curr_direction = 'in';
|
|
|
|
|
export function bind_transition(element, get_fn, get_params, direction, global) {
|
|
|
|
|
const effect = /** @type {import('#client').Effect} */ (current_effect);
|
|
|
|
|
|
|
|
|
|
/** @type {Array<() => void>} */
|
|
|
|
|
let subs = [];
|
|
|
|
|
let p = direction === 'out' ? 1 : 0;
|
|
|
|
|
|
|
|
|
|
/** @type {null | Animation | TickAnimation} */
|
|
|
|
|
let animation = null;
|
|
|
|
|
let cancelled = false;
|
|
|
|
|
/** @type {Animation | null} */
|
|
|
|
|
let current_animation;
|
|
|
|
|
|
|
|
|
|
const create_animation = () => {
|
|
|
|
|
let payload = /** @type {import('../../types.js').TransitionPayload} */ (transition.p);
|
|
|
|
|
if (typeof payload === 'function') {
|
|
|
|
|
// @ts-ignore
|
|
|
|
|
payload = payload({ direction: curr_direction });
|
|
|
|
|
}
|
|
|
|
|
if (payload == null) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
const duration = payload.duration ?? 300;
|
|
|
|
|
const delay = payload.delay ?? 0;
|
|
|
|
|
const css_fn = payload.css;
|
|
|
|
|
const tick_fn = payload.tick;
|
|
|
|
|
const easing_fn = payload.easing || linear;
|
|
|
|
|
/** @type {import('#client').Task | null} */
|
|
|
|
|
let current_task;
|
|
|
|
|
|
|
|
|
|
if (typeof tick_fn === 'function') {
|
|
|
|
|
animation = new TickAnimation(tick_fn, duration, delay, direction === 'out');
|
|
|
|
|
} else {
|
|
|
|
|
const keyframes =
|
|
|
|
|
typeof css_fn === 'function'
|
|
|
|
|
? create_keyframes(easing_fn, css_fn, duration, direction, false)
|
|
|
|
|
: [];
|
|
|
|
|
animation = dom.animate(keyframes, {
|
|
|
|
|
duration,
|
|
|
|
|
endDelay: delay,
|
|
|
|
|
delay,
|
|
|
|
|
fill: 'both'
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
animation.pause();
|
|
|
|
|
/** @type {import('#client').TransitionPayload | null} */
|
|
|
|
|
let current_options;
|
|
|
|
|
|
|
|
|
|
animation.onfinish = () => {
|
|
|
|
|
const is_outro = curr_direction === 'out';
|
|
|
|
|
/** @type {Animation | TickAnimation} */ (animation).cancel();
|
|
|
|
|
if (is_outro) {
|
|
|
|
|
run_all(subs);
|
|
|
|
|
subs = [];
|
|
|
|
|
}
|
|
|
|
|
dispatch_event(dom, is_outro ? 'outroend' : 'introend');
|
|
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
let current_delta = 0;
|
|
|
|
|
|
|
|
|
|
/** @type {import('../../types.js').Transition} */
|
|
|
|
|
/** @type {import('#client').Transition2} */
|
|
|
|
|
const transition = {
|
|
|
|
|
e: effect,
|
|
|
|
|
i: init,
|
|
|
|
|
// payload
|
|
|
|
|
p: null,
|
|
|
|
|
|
|
|
|
|
// finished
|
|
|
|
|
/** @param {() => void} fn */
|
|
|
|
|
f(fn) {
|
|
|
|
|
subs.push(fn);
|
|
|
|
|
},
|
|
|
|
|
in() {
|
|
|
|
|
const needs_reverse = curr_direction !== 'in';
|
|
|
|
|
curr_direction = 'in';
|
|
|
|
|
if (animation === null || cancelled) {
|
|
|
|
|
cancelled = false;
|
|
|
|
|
create_animation();
|
|
|
|
|
}
|
|
|
|
|
if (animation === null) {
|
|
|
|
|
transition.x();
|
|
|
|
|
} else {
|
|
|
|
|
dispatch_event(dom, 'introstart');
|
|
|
|
|
if (needs_reverse) {
|
|
|
|
|
/** @type {Animation | TickAnimation} */ (animation).reverse();
|
|
|
|
|
}
|
|
|
|
|
/** @type {Animation | TickAnimation} */ (animation).play();
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
// out
|
|
|
|
|
o() {
|
|
|
|
|
// @ts-ignore
|
|
|
|
|
const has_keyed_transition = dom.__animate;
|
|
|
|
|
// If we're outroing an element that has an animation, then we need to fix
|
|
|
|
|
// its position to ensure it behaves nicely without causing layout shift.
|
|
|
|
|
if (has_keyed_transition) {
|
|
|
|
|
const style = getComputedStyle(dom);
|
|
|
|
|
const position = style.position;
|
|
|
|
|
|
|
|
|
|
if (position !== 'absolute' && position !== 'fixed') {
|
|
|
|
|
const { width, height } = style;
|
|
|
|
|
const a = dom.getBoundingClientRect();
|
|
|
|
|
dom.style.position = 'absolute';
|
|
|
|
|
|
|
|
|
|
dom.style.width = width;
|
|
|
|
|
dom.style.height = height;
|
|
|
|
|
const b = dom.getBoundingClientRect();
|
|
|
|
|
if (a.left !== b.left || a.top !== b.top) {
|
|
|
|
|
const translate = `translate(${a.left - b.left}px, ${a.top - b.top}px)`;
|
|
|
|
|
const existing_transform = style.transform;
|
|
|
|
|
if (existing_transform === 'none') {
|
|
|
|
|
dom.style.transform = translate;
|
|
|
|
|
} else {
|
|
|
|
|
// Previously, in the Svelte 4, we'd just apply the transform the the DOM element. However,
|
|
|
|
|
// because we're now using Web Animations, we can't do that as it won't work properly if the
|
|
|
|
|
// animation is also making use of the same transformations. So instead, we apply an
|
|
|
|
|
// instantaneous animation and pause it on the first frame, just applying the same behavior.
|
|
|
|
|
// We also need to take into consideration matrix transforms and how they might combine with
|
|
|
|
|
// an existing behavior that is already in progress (such as scale).
|
|
|
|
|
// > Follow the white rabbit.
|
|
|
|
|
const transform = existing_transform.startsWith('matrix(1,')
|
|
|
|
|
? translate
|
|
|
|
|
: `matrix(1,0,0,1,0,0)`;
|
|
|
|
|
const frame = {
|
|
|
|
|
transform
|
|
|
|
|
};
|
|
|
|
|
const animation = dom.animate([frame, frame], { duration: 1 });
|
|
|
|
|
animation.pause();
|
|
|
|
|
global,
|
|
|
|
|
to(target, callback) {
|
|
|
|
|
if (current_task) {
|
|
|
|
|
current_task.abort();
|
|
|
|
|
current_task = null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (current_animation && current_options) {
|
|
|
|
|
const time = /** @type {number} */ (current_animation.currentTime);
|
|
|
|
|
const duration = /** @type {number} */ (current_options.duration);
|
|
|
|
|
p = (Math.abs(current_delta) * time) / duration;
|
|
|
|
|
current_animation.cancel();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
const needs_reverse = direction === 'both' && curr_direction !== 'out';
|
|
|
|
|
curr_direction = 'out';
|
|
|
|
|
if (animation === null || cancelled) {
|
|
|
|
|
cancelled = false;
|
|
|
|
|
create_animation();
|
|
|
|
|
}
|
|
|
|
|
if (animation === null) {
|
|
|
|
|
transition.x();
|
|
|
|
|
} else {
|
|
|
|
|
dispatch_event(dom, 'outrostart');
|
|
|
|
|
if (needs_reverse) {
|
|
|
|
|
const payload = transition.p;
|
|
|
|
|
const current_animation = /** @type {Animation} */ (animation);
|
|
|
|
|
// If we are working with CSS animations, then before we call reverse, we also need to ensure
|
|
|
|
|
// that we reverse the easing logic. To do this we need to re-create the keyframes so they're
|
|
|
|
|
// in reverse with easing properly reversed too.
|
|
|
|
|
if (
|
|
|
|
|
payload !== null &&
|
|
|
|
|
payload.css !== undefined &&
|
|
|
|
|
current_animation.playState === 'idle'
|
|
|
|
|
) {
|
|
|
|
|
const duration = payload.duration ?? 300;
|
|
|
|
|
const css_fn = payload.css;
|
|
|
|
|
const easing_fn = payload.easing || linear;
|
|
|
|
|
const keyframes = create_keyframes(easing_fn, css_fn, duration, direction, true);
|
|
|
|
|
const effect = current_animation.effect;
|
|
|
|
|
if (effect !== null) {
|
|
|
|
|
// @ts-ignore
|
|
|
|
|
effect.setKeyframes(keyframes);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
/** @type {Animation | TickAnimation} */ (animation).reverse();
|
|
|
|
|
} else {
|
|
|
|
|
/** @type {Animation | TickAnimation} */ (animation).play();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
// cancel
|
|
|
|
|
c() {
|
|
|
|
|
if (animation !== null) {
|
|
|
|
|
/** @type {Animation | TickAnimation} */ (animation).cancel();
|
|
|
|
|
}
|
|
|
|
|
cancelled = true;
|
|
|
|
|
},
|
|
|
|
|
// cleanup
|
|
|
|
|
x() {
|
|
|
|
|
run_all(subs);
|
|
|
|
|
subs = [];
|
|
|
|
|
},
|
|
|
|
|
r: direction,
|
|
|
|
|
d: dom
|
|
|
|
|
};
|
|
|
|
|
return transition;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @param {import('../../types.js').Block} block
|
|
|
|
|
* @returns {boolean}
|
|
|
|
|
*/
|
|
|
|
|
function is_transition_block(block) {
|
|
|
|
|
const type = block.t;
|
|
|
|
|
return (
|
|
|
|
|
type === IF_BLOCK ||
|
|
|
|
|
type === EACH_ITEM_BLOCK ||
|
|
|
|
|
type === KEY_BLOCK ||
|
|
|
|
|
type === AWAIT_BLOCK ||
|
|
|
|
|
type === DYNAMIC_COMPONENT_BLOCK ||
|
|
|
|
|
(type === EACH_BLOCK && block.v.length === 0)
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
current_options ??= get_fn()(element, get_params?.(), { direction });
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @template P
|
|
|
|
|
* @param {HTMLElement} dom
|
|
|
|
|
* @param {() => import('../../types.js').TransitionFn<P | undefined> | import('../../types.js').AnimateFn<P | undefined>} get_transition_fn
|
|
|
|
|
* @param {(() => P) | null} props_fn
|
|
|
|
|
* @param {'in' | 'out' | 'both' | 'key'} direction
|
|
|
|
|
* @param {boolean} global
|
|
|
|
|
* @returns {void}
|
|
|
|
|
*/
|
|
|
|
|
function bind_transition(dom, get_transition_fn, props_fn, direction, global) {
|
|
|
|
|
const transition_effect = /** @type {import('../../types.js').Effect} */ (current_effect);
|
|
|
|
|
const block = current_block;
|
|
|
|
|
const is_keyed_transition = direction === 'key';
|
|
|
|
|
|
|
|
|
|
let can_show_intro_on_mount = true;
|
|
|
|
|
let can_apply_lazy_transitions = false;
|
|
|
|
|
|
|
|
|
|
if (is_keyed_transition) {
|
|
|
|
|
// @ts-ignore
|
|
|
|
|
dom.__animate = true;
|
|
|
|
|
}
|
|
|
|
|
/** @type {import('../../types.js').Block | null} */
|
|
|
|
|
let transition_block = block;
|
|
|
|
|
main: while (transition_block !== null) {
|
|
|
|
|
if (is_transition_block(transition_block)) {
|
|
|
|
|
if (transition_block.t === EACH_ITEM_BLOCK) {
|
|
|
|
|
// Lazily apply the each block transition
|
|
|
|
|
transition_block.r = each_item_transition;
|
|
|
|
|
transition_block.a = each_item_animate;
|
|
|
|
|
transition_block = transition_block.p;
|
|
|
|
|
} else if (transition_block.t === AWAIT_BLOCK && transition_block.n /* pending */) {
|
|
|
|
|
can_show_intro_on_mount = true;
|
|
|
|
|
} else if (transition_block.t === IF_BLOCK) {
|
|
|
|
|
transition_block.r = if_block_transition;
|
|
|
|
|
if (can_show_intro_on_mount) {
|
|
|
|
|
/** @type {import('../../types.js').Block | null} */
|
|
|
|
|
let if_block = transition_block;
|
|
|
|
|
while (if_block.t === IF_BLOCK) {
|
|
|
|
|
// If we have an if block parent that is currently falsy then
|
|
|
|
|
// we can show the intro on mount as long as that block is mounted
|
|
|
|
|
if (if_block.e !== null && !if_block.v) {
|
|
|
|
|
can_show_intro_on_mount = true;
|
|
|
|
|
break main;
|
|
|
|
|
}
|
|
|
|
|
if_block = if_block.p;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (!can_apply_lazy_transitions && can_show_intro_on_mount) {
|
|
|
|
|
can_show_intro_on_mount = transition_block.e !== null;
|
|
|
|
|
}
|
|
|
|
|
if (can_show_intro_on_mount || !global) {
|
|
|
|
|
can_apply_lazy_transitions = true;
|
|
|
|
|
}
|
|
|
|
|
} else if (transition_block.t === ROOT_BLOCK && !can_apply_lazy_transitions) {
|
|
|
|
|
can_show_intro_on_mount = transition_block.e !== null || transition_block.i;
|
|
|
|
|
}
|
|
|
|
|
transition_block = transition_block.p;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** @type {import('../../types.js').Transition} */
|
|
|
|
|
let transition;
|
|
|
|
|
|
|
|
|
|
effect(() => {
|
|
|
|
|
let already_mounted = false;
|
|
|
|
|
if (transition !== undefined) {
|
|
|
|
|
already_mounted = true;
|
|
|
|
|
// Destroy any existing transitions first
|
|
|
|
|
transition.x();
|
|
|
|
|
}
|
|
|
|
|
const transition_fn = get_transition_fn();
|
|
|
|
|
/** @param {DOMRect} [from] */
|
|
|
|
|
const init = (from) =>
|
|
|
|
|
untrack(() => {
|
|
|
|
|
const props = props_fn === null ? {} : props_fn();
|
|
|
|
|
return is_keyed_transition
|
|
|
|
|
? /** @type {import('../../types.js').AnimateFn<any>} */ (transition_fn)(
|
|
|
|
|
dom,
|
|
|
|
|
{ from: /** @type {DOMRect} */ (from), to: dom.getBoundingClientRect() },
|
|
|
|
|
props,
|
|
|
|
|
{}
|
|
|
|
|
)
|
|
|
|
|
: /** @type {import('../../types.js').TransitionFn<any>} */ (transition_fn)(dom, props, {
|
|
|
|
|
direction
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
if (!current_options?.duration) {
|
|
|
|
|
current_options = null;
|
|
|
|
|
callback?.();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
transition = create_transition(dom, init, direction, transition_effect);
|
|
|
|
|
const is_intro = direction === 'in';
|
|
|
|
|
const show_intro = can_show_intro_on_mount && (is_intro || direction === 'both');
|
|
|
|
|
const { delay = 0, duration, css, tick, easing = linear } = current_options;
|
|
|
|
|
|
|
|
|
|
if (show_intro && !already_mounted) {
|
|
|
|
|
transition.p = transition.i();
|
|
|
|
|
}
|
|
|
|
|
const n = current_options.duration / (1000 / 60);
|
|
|
|
|
current_delta = target - p;
|
|
|
|
|
|
|
|
|
|
const effect = managed_pre_effect(() => {
|
|
|
|
|
destroy_effect(effect);
|
|
|
|
|
dom.inert = false;
|
|
|
|
|
const adjusted_duration = duration * Math.abs(current_delta);
|
|
|
|
|
|
|
|
|
|
if (show_intro && !already_mounted) {
|
|
|
|
|
transition.in();
|
|
|
|
|
}
|
|
|
|
|
if (css) {
|
|
|
|
|
// WAAPI
|
|
|
|
|
const keyframes = [];
|
|
|
|
|
|
|
|
|
|
/** @type {import('../../types.js').Block | null} */
|
|
|
|
|
let transition_block = block;
|
|
|
|
|
while (!is_intro && transition_block !== null) {
|
|
|
|
|
const parent = transition_block.p;
|
|
|
|
|
if (is_transition_block(transition_block)) {
|
|
|
|
|
if (transition_block.r !== null) {
|
|
|
|
|
transition_block.r(transition);
|
|
|
|
|
}
|
|
|
|
|
if (
|
|
|
|
|
parent === null ||
|
|
|
|
|
(!global && (transition_block.t !== IF_BLOCK || parent.t !== IF_BLOCK || parent.v))
|
|
|
|
|
) {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
for (let i = 0; i <= n; i += 1) {
|
|
|
|
|
const eased = easing(i / n);
|
|
|
|
|
const t = p + current_delta * eased;
|
|
|
|
|
const styles = css(t, 1 - t);
|
|
|
|
|
keyframes.push(css_to_keyframe(styles));
|
|
|
|
|
}
|
|
|
|
|
transition_block = parent;
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
current_animation = element.animate(keyframes, {
|
|
|
|
|
delay,
|
|
|
|
|
duration: adjusted_duration,
|
|
|
|
|
easing: 'linear',
|
|
|
|
|
fill: 'forwards'
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (direction === 'key') {
|
|
|
|
|
effect(() => {
|
|
|
|
|
return () => {
|
|
|
|
|
transition.x();
|
|
|
|
|
};
|
|
|
|
|
current_animation.finished
|
|
|
|
|
.then(() => {
|
|
|
|
|
p = target;
|
|
|
|
|
current_animation = current_options = null;
|
|
|
|
|
callback?.();
|
|
|
|
|
})
|
|
|
|
|
.catch(noop);
|
|
|
|
|
} else if (tick) {
|
|
|
|
|
// Timer
|
|
|
|
|
let running = true;
|
|
|
|
|
const start_time = raf.now() + delay;
|
|
|
|
|
const start_p = p;
|
|
|
|
|
const end_time = start_time + adjusted_duration;
|
|
|
|
|
|
|
|
|
|
tick(p, 1 - p); // TODO put in nested effect, to avoid interleaved reads/writes?
|
|
|
|
|
|
|
|
|
|
current_task = loop((now) => {
|
|
|
|
|
if (running) {
|
|
|
|
|
if (now >= end_time) {
|
|
|
|
|
p = target;
|
|
|
|
|
tick(target, 1 - target);
|
|
|
|
|
// dispatch(node, true, 'end'); TODO
|
|
|
|
|
current_task = null;
|
|
|
|
|
callback?.();
|
|
|
|
|
return (running = false);
|
|
|
|
|
}
|
|
|
|
|
if (now >= start_time) {
|
|
|
|
|
p = start_p + current_delta * easing((now - start_time) / adjusted_duration);
|
|
|
|
|
tick(p, 1 - p);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return running;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @param {Set<import('../../types.js').Transition>} transitions
|
|
|
|
|
* @param {'in' | 'out' | 'key'} target_direction
|
|
|
|
|
* @param {DOMRect} [from]
|
|
|
|
|
* @returns {void}
|
|
|
|
|
*/
|
|
|
|
|
export function trigger_transitions(transitions, target_direction, from) {
|
|
|
|
|
/** @type {Array<() => void>} */
|
|
|
|
|
const outros = [];
|
|
|
|
|
for (const transition of transitions) {
|
|
|
|
|
const direction = transition.r;
|
|
|
|
|
const effect = transition.e;
|
|
|
|
|
if (target_direction === 'in') {
|
|
|
|
|
if (direction === 'in' || direction === 'both') {
|
|
|
|
|
transition.in();
|
|
|
|
|
} else {
|
|
|
|
|
transition.c();
|
|
|
|
|
}
|
|
|
|
|
transition.d.inert = false;
|
|
|
|
|
mark_subtree_inert(effect, false);
|
|
|
|
|
} else if (target_direction === 'key') {
|
|
|
|
|
if (direction === 'key') {
|
|
|
|
|
if (!transition.p) {
|
|
|
|
|
transition.p = transition.i(/** @type {DOMRect} */ (from));
|
|
|
|
|
}
|
|
|
|
|
transition.in();
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
if (direction === 'out' || direction === 'both') {
|
|
|
|
|
if (!transition.p) {
|
|
|
|
|
transition.p = transition.i();
|
|
|
|
|
current_options = null;
|
|
|
|
|
}
|
|
|
|
|
outros.push(transition.o);
|
|
|
|
|
}
|
|
|
|
|
transition.d.inert = true;
|
|
|
|
|
mark_subtree_inert(effect, true);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (outros.length > 0) {
|
|
|
|
|
// Defer the outros to a microtask
|
|
|
|
|
const e = managed_pre_effect(() => {
|
|
|
|
|
destroy_effect(e);
|
|
|
|
|
const e2 = managed_effect(() => {
|
|
|
|
|
destroy_effect(e2);
|
|
|
|
|
run_all(outros);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @this {import('../../types.js').IfBlock}
|
|
|
|
|
* @param {import('../../types.js').Transition} transition
|
|
|
|
|
* @returns {void}
|
|
|
|
|
*/
|
|
|
|
|
function if_block_transition(transition) {
|
|
|
|
|
const block = this;
|
|
|
|
|
// block.value === true
|
|
|
|
|
if (block.v) {
|
|
|
|
|
const consequent_transitions = (block.c ??= new Set());
|
|
|
|
|
consequent_transitions.add(transition);
|
|
|
|
|
transition.f(() => {
|
|
|
|
|
const c = /** @type {Set<import('../../types.js').Transition>} */ (consequent_transitions);
|
|
|
|
|
c.delete(transition);
|
|
|
|
|
// If the block has changed to falsy and has transitions
|
|
|
|
|
if (!block.v && c.size === 0) {
|
|
|
|
|
const consequent_effect = block.ce;
|
|
|
|
|
execute_effect(/** @type {import('../../types.js').Effect} */ (consequent_effect));
|
|
|
|
|
}
|
|
|
|
|
// TODO don't pass strings around like this, it's silly
|
|
|
|
|
if (direction === 'in' || direction === 'both') {
|
|
|
|
|
(effect.in ??= []).push(transition);
|
|
|
|
|
|
|
|
|
|
// if this is a local transition, we only want to run it if the parent (block) effect's
|
|
|
|
|
// parent (branch) effect is where the state change happened. we can determine that by
|
|
|
|
|
// looking at whether the branch effect is currently initializing
|
|
|
|
|
const should_run =
|
|
|
|
|
run_transitions && (global || /** @type {import('#client').Effect} */ (effect.parent).ran);
|
|
|
|
|
|
|
|
|
|
if (should_run) {
|
|
|
|
|
user_effect(() => {
|
|
|
|
|
untrack(() => transition.to(1));
|
|
|
|
|
});
|
|
|
|
|
} else {
|
|
|
|
|
const alternate_transitions = (block.a ??= new Set());
|
|
|
|
|
alternate_transitions.add(transition);
|
|
|
|
|
transition.f(() => {
|
|
|
|
|
const a = /** @type {Set<import('../../types.js').Transition>} */ (alternate_transitions);
|
|
|
|
|
a.delete(transition);
|
|
|
|
|
// If the block has changed to truthy and has transitions
|
|
|
|
|
if (block.v && a.size === 0) {
|
|
|
|
|
const alternate_effect = block.ae;
|
|
|
|
|
execute_effect(/** @type {import('../../types.js').Effect} */ (alternate_effect));
|
|
|
|
|
p = 1;
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @this {import('../../types.js').EachItemBlock}
|
|
|
|
|
* @param {import('../../types.js').Transition} transition
|
|
|
|
|
* @returns {void}
|
|
|
|
|
*/
|
|
|
|
|
function each_item_transition(transition) {
|
|
|
|
|
const block = this;
|
|
|
|
|
const each_block = block.p;
|
|
|
|
|
const is_controlled = (each_block.f & EACH_IS_CONTROLLED) !== 0;
|
|
|
|
|
// Disable optimization
|
|
|
|
|
if (is_controlled) {
|
|
|
|
|
const anchor = empty();
|
|
|
|
|
each_block.f ^= EACH_IS_CONTROLLED;
|
|
|
|
|
append_child(/** @type {Element} */ (each_block.a), anchor);
|
|
|
|
|
each_block.a = anchor;
|
|
|
|
|
}
|
|
|
|
|
if (transition.r === 'key' && (each_block.f & EACH_IS_ANIMATED) === 0) {
|
|
|
|
|
each_block.f |= EACH_IS_ANIMATED;
|
|
|
|
|
}
|
|
|
|
|
const transitions = (block.s ??= new Set());
|
|
|
|
|
transition.f(() => {
|
|
|
|
|
transitions.delete(transition);
|
|
|
|
|
if (transition.r !== 'key') {
|
|
|
|
|
for (let other of transitions) {
|
|
|
|
|
const type = other.r;
|
|
|
|
|
if (type === 'key' || type === 'in') {
|
|
|
|
|
transitions.delete(other);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (transitions.size === 0) {
|
|
|
|
|
block.s = null;
|
|
|
|
|
destroy_each_item_block(block, null, true);
|
|
|
|
|
}
|
|
|
|
|
if (direction === 'out' || direction === 'both') {
|
|
|
|
|
(effect.out ??= []).push(transition);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
transitions.add(transition);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
*
|
|
|
|
|
* @param {import('../../types.js').EachItemBlock} block
|
|
|
|
|
* @param {Set<import('../../types.js').Transition>} transitions
|
|
|
|
|
*/
|
|
|
|
|
function each_item_animate(block, transitions) {
|
|
|
|
|
const from_dom = /** @type {Element} */ (get_first_element(block));
|
|
|
|
|
const from = from_dom.getBoundingClientRect();
|
|
|
|
|
// Cancel any existing key transitions
|
|
|
|
|
for (const transition of transitions) {
|
|
|
|
|
const type = transition.r;
|
|
|
|
|
if (type === 'key') {
|
|
|
|
|
transition.c();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
schedule_raf_task(() => {
|
|
|
|
|
trigger_transitions(transitions, 'key', from);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|