pull/10798/head
Rich Harris 2 years ago
parent 519573d7d0
commit acb95e8351

@ -1,4 +1,4 @@
import { noop, run } from '../../../common.js';
import { noop } from '../../../common.js';
import { user_effect } from '../../reactivity/effects.js';
import { current_effect, untrack } from '../../runtime.js';
import { raf } from '../../timing.js';
@ -7,18 +7,6 @@ import { run_transitions } from '../../render.js';
import { TRANSITION_GLOBAL, TRANSITION_IN, TRANSITION_OUT } from '../../constants.js';
import { is_function } from '../../utils.js';
/**
* @template P
* @param {HTMLElement} dom
* @param {() => import('#client').TransitionFn<P | undefined>} get_transition_fn
* @param {(() => P) | null} props
* @returns {void}
*/
export function animate(dom, get_transition_fn, props) {
// TODO
// bind_transition(dom, get_transition_fn, props, 'key', false);
}
/**
* @template T
* @param {string} type
@ -33,7 +21,7 @@ function custom_event(type, detail, { bubbles = false, cancelable = false } = {}
}
/**
* @param {HTMLElement} dom
* @param {Element} dom
* @param {'introstart' | 'introend' | 'outrostart' | 'outroend'} type
* @returns {void}
*/
@ -78,7 +66,11 @@ function css_to_keyframe(css) {
/** @param {number} t */
const linear = (t) => t;
/** @param {() => any} fn */
/**
* @template T
* @param {() => T} fn
* @returns {Promise<T>}
*/
function defer(fn) {
return new Promise((fulfil) => {
user_effect(() => {
@ -98,165 +90,76 @@ function defer(fn) {
export function transition(flags, element, get_fn, get_params) {
const effect = /** @type {import('#client').Effect} */ (current_effect);
const intro = (flags & TRANSITION_IN) !== 0;
const outro = (flags & TRANSITION_OUT) !== 0;
const global = (flags & TRANSITION_GLOBAL) !== 0;
const is_intro = (flags & TRANSITION_IN) !== 0;
const is_outro = (flags & TRANSITION_OUT) !== 0;
const is_global = (flags & TRANSITION_GLOBAL) !== 0;
const direction = intro && outro ? 'both' : intro ? 'in' : 'out';
const direction = is_intro && is_outro ? 'both' : is_intro ? 'in' : 'out';
let p = 0;
/** @type {Animation | null} */
let current_animation;
/** @type {import('#client').Task | null} */
let current_task;
/** @type {import('#client').TransitionPayload | null} */
/** @type {import('#client').TransitionPayload | ((opts: { direction: 'in' | 'out' }) => import('#client').TransitionPayload) | undefined} */
let current_options;
let current_delta = 0;
/** @type {(() => void) | undefined} */
let callback;
/** @param {number} target */
async function start(target) {
stop();
dispatch_event(element, target === 1 ? 'introstart' : 'outrostart');
if (!current_options) {
var result = get_fn()(element, get_params?.(), { direction });
current_options = is_function(result)
? await defer(() =>
/** @type {Function} */ (result)({ direction: target === 1 ? 'in' : 'out' })
)
: result;
}
if (!current_options?.duration) {
current_options = null;
callback?.();
return;
}
const { delay = 0, duration, css, tick, easing = linear } = current_options;
const n = current_options.duration / (1000 / 60);
current_delta = target - p;
const adjusted_duration = duration * Math.abs(current_delta);
if (css) {
// WAAPI
const keyframes = [];
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));
}
current_animation = element.animate(keyframes, {
delay,
duration: adjusted_duration,
easing: 'linear',
fill: 'forwards'
});
current_animation.finished
.then(() => {
if (target === 1) {
current_options = null;
current_animation = null;
}
p = target;
callback?.();
dispatch_event(element, target === 1 ? 'introend' : 'outroend');
})
.catch(noop);
} else {
// Timer
const start_time = raf.now() + delay;
const start_p = p;
const end_time = start_time + adjusted_duration;
if (p === 0) {
tick?.(p, 1 - p); // TODO put in nested effect, to avoid interleaved reads/writes?
}
current_task = loop((now) => {
if (now >= end_time) {
if (target === 1) {
current_task = null;
current_options = null;
}
p = target;
tick?.(target, 1 - target);
callback?.();
dispatch_event(element, target === 1 ? 'introend' : 'outroend');
return false;
}
if (now >= start_time) {
p = start_p + current_delta * easing((now - start_time) / adjusted_duration);
tick?.(p, 1 - p);
}
return true;
});
}
function get_options() {
return (current_options ??= get_fn()(element, get_params?.(), { direction }));
}
function stop() {
if (current_task) {
current_task.abort();
current_task = null;
}
var inert = element.inert;
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;
}
}
/** @type {import('#client').Animation | undefined} */
var intro;
var inert = element.inert;
/** @type {import('#client').Animation | undefined} */
var outro;
/** @type {(() => void) | undefined} */
var reset;
/** @type {import('#client').Transition} */
const transition = {
global,
in() {
is_global,
async in() {
callback = undefined;
element.inert = inert;
if (current_animation) {
current_animation.cancel();
current_animation = null;
}
if (is_intro) {
intro = animate(element, get_options(), outro ? outro.p() : 0, 1, () => {
intro = current_options = undefined;
});
if (intro) {
start(1);
outro?.neuter();
} else {
current_options?.tick?.(1, 0);
stop();
outro?.abort();
reset?.();
}
reset = undefined;
},
out(fn) {
if (outro) {
async out(fn) {
if (is_outro) {
callback = fn;
element.inert = true;
start(0);
outro = animate(element, get_options(), intro ? intro.p() : 1, 0, () => {
outro = current_options = undefined;
fn?.();
});
reset = outro.reset;
intro?.neuter();
} else {
fn?.();
}
},
stop
stop: () => {
intro?.abort();
outro?.abort();
}
};
(effect.transitions ??= []).push(transition);
@ -265,9 +168,9 @@ export function transition(flags, element, get_fn, get_params) {
// parent (branch) effect is where the state change happened. we can determine that by
// looking at whether the branch effect is currently initializing
if (
intro &&
is_intro &&
run_transitions &&
(global || /** @type {import('#client').Effect} */ (effect.parent).ran)
(is_global || /** @type {import('#client').Effect} */ (effect.parent).ran)
) {
user_effect(() => {
untrack(() => transition.in());
@ -276,3 +179,152 @@ export function transition(flags, element, get_fn, get_params) {
p = 1;
}
}
/**
* @param {Element} element
* @param {import('#client').TransitionPayload | ((opts: { direction: 'in' | 'out' }) => import('#client').TransitionPayload)} options
* @param {number} p
* @param {number} target
* @param {(() => void) | undefined} callback
* @returns {import('#client').Animation}
*/
function animate(element, options, p, target, callback) {
dispatch_event(element, target === 1 ? 'introstart' : 'outrostart');
if (is_function(options)) {
/** @type {import('#client').Animation} */
var a;
user_effect(() => {
var o = untrack(() => options({ direction: target === 1 ? 'in' : 'out' }));
a = animate(element, o, p, target, callback);
});
return {
abort: () => a.abort(),
neuter: () => a.neuter(),
reset: noop,
p: () => a.p()
};
}
if (!options?.duration) {
callback?.();
return {
abort: noop,
neuter: noop,
reset: noop,
p: () => target
};
}
const { delay = 0, duration, css, tick, easing = linear } = options;
const n = options.duration / (1000 / 60);
const current_delta = target - p;
const adjusted_duration = duration * Math.abs(current_delta);
const start_time = raf.now() + delay;
const start_p = p;
const end_time = start_time + adjusted_duration;
if (css) {
// WAAPI
const keyframes = [];
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));
}
var animation = element.animate(keyframes, {
delay,
duration: adjusted_duration,
easing: 'linear',
fill: 'forwards'
});
animation.finished
.then(() => {
// if (target === 1) {
// current_options = null;
// current_animation = null;
// }
// p = target;
callback?.();
dispatch_event(element, target === 1 ? 'introend' : 'outroend');
})
.catch(noop);
return {
abort: () => {
animation.cancel();
},
neuter: () => {
callback = undefined;
},
reset: noop,
p: () => {
return clamp(
start_p + current_delta * easing((raf.now() - start_time) / adjusted_duration),
0,
1
);
}
};
}
// Timer
if (p === 0) {
tick?.(p, 1 - p); // TODO put in nested effect, to avoid interleaved reads/writes?
}
var task = loop((now) => {
if (now >= end_time) {
// if (target === 1) {
// current_task = null;
// current_options = null;
// }
// p = target;
tick?.(target, 1 - target);
callback?.();
dispatch_event(element, target === 1 ? 'introend' : 'outroend');
return false;
}
if (now >= start_time) {
p = start_p + current_delta * easing((now - start_time) / adjusted_duration);
tick?.(p, 1 - p);
}
return true;
});
return {
abort: () => {
if (target === 0) tick?.(1, 0);
task.abort();
},
neuter: () => {
callback = undefined;
},
reset: () => {
if (target === 0) tick?.(1, 0);
},
p: () => p
};
}
/**
* @param {number} n
* @param {number} min
* @param {number} max
*/
function clamp(n, min, max) {
return Math.max(min, Math.min(max, n));
}

@ -299,7 +299,7 @@ function pause_children(effect, transitions, local) {
if (effect.transitions) {
for (const transition of effect.transitions) {
if (transition.global || local) {
if (transition.is_global || local) {
transitions.push(transition);
}
}
@ -341,7 +341,7 @@ function resume_children(effect, local) {
if (effect.transitions) {
for (const transition of effect.transitions) {
if (transition.global || local) {
if (transition.is_global || local) {
transition.in();
}
}

@ -156,17 +156,24 @@ export type Block =
| SnippetBlock;
export interface Transition {
global: boolean;
is_global: boolean;
in: () => void;
out: (callback?: () => void) => void;
stop: () => void;
}
export interface Animation {
abort: () => void;
neuter: () => void;
reset: () => void;
p: () => number;
}
export type TransitionFn<P> = (
element: Element,
props: P,
options: { direction?: 'in' | 'out' | 'both' }
) => TransitionPayload | ((options: { direction?: 'in' | 'out' | 'both' }) => TransitionPayload);
) => TransitionPayload | ((options: { direction?: 'in' | 'out' }) => TransitionPayload);
export type AnimateFn<P> = (
element: Element,

Loading…
Cancel
Save