|
|
|
@ -1,32 +1,10 @@
|
|
|
|
|
import { EACH_IS_ANIMATED, EACH_IS_CONTROLLED } from '../../constants.js';
|
|
|
|
|
import { noop, 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 { destroy_each_item_block, get_first_element } from './dom/blocks/each.js';
|
|
|
|
|
import { schedule_raf_task } from './dom/task.js';
|
|
|
|
|
import { append_child, empty } from './operations.js';
|
|
|
|
|
import {
|
|
|
|
|
effect,
|
|
|
|
|
managed_effect,
|
|
|
|
|
managed_pre_effect,
|
|
|
|
|
user_effect
|
|
|
|
|
} from './reactivity/computations.js';
|
|
|
|
|
import { managed_effect, managed_pre_effect, user_effect } from './reactivity/computations.js';
|
|
|
|
|
import { run_transitions } from './render.js';
|
|
|
|
|
import {
|
|
|
|
|
current_block,
|
|
|
|
|
current_effect,
|
|
|
|
|
destroy_signal,
|
|
|
|
|
execute_effect,
|
|
|
|
|
mark_subtree_inert,
|
|
|
|
|
untrack
|
|
|
|
|
} from './runtime.js';
|
|
|
|
|
import { current_effect, destroy_signal, mark_subtree_inert, untrack } from './runtime.js';
|
|
|
|
|
import { raf } from './timing.js';
|
|
|
|
|
|
|
|
|
|
const active_tick_animations = new Set();
|
|
|
|
@ -259,246 +237,9 @@ function handle_raf(time) {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @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
|
|
|
|
|
*/
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** @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').EffectSignal} effect
|
|
|
|
|
* @returns {import('./types.js').Transition}
|
|
|
|
|
*/
|
|
|
|
|
function create_transition(dom, init, direction, effect) {
|
|
|
|
|
let curr_direction = 'in';
|
|
|
|
|
|
|
|
|
|
/** @type {Array<() => void>} */
|
|
|
|
|
let subs = [];
|
|
|
|
|
|
|
|
|
|
/** @type {null | Animation | TickAnimation} */
|
|
|
|
|
let animation = null;
|
|
|
|
|
let cancelled = false;
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
|
|
|
|
|
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();
|
|
|
|
|
|
|
|
|
|
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');
|
|
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/** @type {import('./types.js').Transition} */
|
|
|
|
|
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();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
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)
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @template P
|
|
|
|
|
* @param {HTMLElement} element
|
|
|
|
@ -640,96 +381,3 @@ export function trigger_transitions(transitions, target_direction, from) {
|
|
|
|
|
}, false);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @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').EffectSignal} */ (consequent_effect));
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
} 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').EffectSignal} */ (alternate_effect));
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @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);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
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);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|