replace transition code

pull/10798/head
Rich Harris 2 years ago
parent ffb27f667a
commit edbf57ab36

@ -6,9 +6,13 @@ import {
set_current_hydration_fragment set_current_hydration_fragment
} from '../hydration.js'; } from '../hydration.js';
import { remove } from '../reconciler.js'; import { remove } from '../reconciler.js';
import { current_block, execute_effect } from '../../runtime.js'; import { current_block } from '../../runtime.js';
import { destroy_effect, render_effect } from '../../reactivity/effects.js'; import {
import { trigger_transitions } from '../elements/transitions.js'; destroy_effect,
pause_effect,
render_effect,
resume_effect
} from '../../reactivity/effects.js';
/** @returns {import('#client').IfBlock} */ /** @returns {import('#client').IfBlock} */
function create_if_block() { function create_if_block() {
@ -64,12 +68,60 @@ export function if_block(anchor_node, condition_fn, consequent_fn, alternate_fn)
*/ */
let current_branch_effect = null; let current_branch_effect = null;
/** @type {import('#client').Effect} */ /** @type {import('#client').Effect | null} */
let consequent_effect; let consequent_effect;
/** @type {import('#client').Effect} */ /** @type {import('#client').Effect | null} */
let alternate_effect; let alternate_effect;
function create_consequent_effect() {
return render_effect(
() => {
consequent_fn(anchor_node);
if (mismatch && current_branch_effect === null) {
set_current_hydration_fragment([]);
}
consequent_dom = block.d;
return () => {
// TODO make this unnecessary by linking the dom to the effect,
// and removing automatically on teardown
if (consequent_dom !== null) {
remove(consequent_dom);
consequent_dom = null;
}
};
},
block,
true
);
}
function create_alternate_effect() {
return render_effect(
() => {
/** @type {((anchor: Node) => void)} */ (alternate_fn)(anchor_node);
if (mismatch && current_branch_effect === null) {
set_current_hydration_fragment([]);
}
alternate_dom = block.d;
return () => {
// TODO make this unnecessary by linking the dom to the effect,
// and removing automatically on teardown
if (alternate_dom !== null) {
remove(alternate_dom);
alternate_dom = null;
}
};
},
block,
true
);
}
const if_effect = render_effect(() => { const if_effect = render_effect(() => {
const result = !!condition_fn(); const result = !!condition_fn();
@ -77,35 +129,35 @@ export function if_block(anchor_node, condition_fn, consequent_fn, alternate_fn)
block.v = result; block.v = result;
if (has_mounted) { if (has_mounted) {
const consequent_transitions = block.c;
const alternate_transitions = block.a;
if (result) { if (result) {
if (alternate_transitions === null || alternate_transitions.size === 0) { if (consequent_effect) {
execute_effect(alternate_effect); resume_effect(consequent_effect);
} else { } else {
trigger_transitions(alternate_transitions, 'out'); consequent_effect = create_consequent_effect();
} }
if (consequent_transitions === null || consequent_transitions.size === 0) { if (alternate_effect) {
execute_effect(consequent_effect); pause_effect(alternate_effect, () => {
} else { alternate_effect = null;
trigger_transitions(consequent_transitions, 'in'); if (alternate_dom) remove(alternate_dom);
});
} }
} else { } else {
if (consequent_transitions === null || consequent_transitions.size === 0) { if (alternate_effect) {
execute_effect(consequent_effect); resume_effect(alternate_effect);
} else { } else if (alternate_fn) {
trigger_transitions(consequent_transitions, 'out'); alternate_effect = create_alternate_effect();
} }
if (alternate_transitions === null || alternate_transitions.size === 0) { if (consequent_effect) {
execute_effect(alternate_effect); pause_effect(consequent_effect, () => {
} else { consequent_effect = null;
trigger_transitions(alternate_transitions, 'in'); if (consequent_dom) remove(consequent_dom);
});
} }
} }
} else if (hydrating) { } else {
if (hydrating) {
const comment_text = /** @type {Comment} */ (current_hydration_fragment?.[0])?.data; const comment_text = /** @type {Comment} */ (current_hydration_fragment?.[0])?.data;
if ( if (
@ -124,67 +176,20 @@ export function if_block(anchor_node, condition_fn, consequent_fn, alternate_fn)
} }
} }
has_mounted = true; if (result) {
} consequent_effect ??= create_consequent_effect();
} else if (alternate_fn) {
// create these here so they have the correct parent/child relationship alternate_effect ??= create_alternate_effect();
consequent_effect ??= render_effect(
(/** @type {any} */ _, /** @type {import('#client').Effect | null} */ consequent_effect) => {
const result = block.v;
if (!result && consequent_dom !== null) {
remove(consequent_dom);
consequent_dom = null;
}
if (result && current_branch_effect !== consequent_effect) {
consequent_fn(anchor_node);
if (mismatch && current_branch_effect === null) {
// Set fragment so that Svelte continues to operate in hydration mode
set_current_hydration_fragment([]);
}
current_branch_effect = consequent_effect;
consequent_dom = block.d;
}
block.d = null;
},
block,
true
);
block.ce = consequent_effect;
alternate_effect ??= render_effect(
(/** @type {any} */ _, /** @type {import('#client').Effect | null} */ alternate_effect) => {
const result = block.v;
if (result && alternate_dom !== null) {
remove(alternate_dom);
alternate_dom = null;
} }
if (!result && current_branch_effect !== alternate_effect) {
if (alternate_fn !== null) {
alternate_fn(anchor_node);
} }
if (mismatch && current_branch_effect === null) { has_mounted = true;
// Set fragment so that Svelte continues to operate in hydration mode
set_current_hydration_fragment([]);
}
current_branch_effect = alternate_effect;
alternate_dom = block.d;
} }
block.d = null;
},
block,
true
);
block.ae = alternate_effect;
}, block); }, block);
if_effect.ondestroy = () => { if_effect.ondestroy = () => {
// TODO make this unnecessary by linking the dom to the effect,
// and removing automatically on teardown
if (consequent_dom !== null) { if (consequent_dom !== null) {
remove(consequent_dom); remove(consequent_dom);
} }
@ -193,8 +198,12 @@ export function if_block(anchor_node, condition_fn, consequent_fn, alternate_fn)
remove(alternate_dom); remove(alternate_dom);
} }
if (consequent_effect) {
destroy_effect(consequent_effect); destroy_effect(consequent_effect);
}
if (alternate_effect) {
destroy_effect(alternate_effect); destroy_effect(alternate_effect);
}
}; };
block.e = if_effect; block.e = if_effect;

@ -1,31 +1,13 @@
import { EACH_IS_ANIMATED, EACH_IS_CONTROLLED } from '../../../../constants.js'; import { EACH_IS_ANIMATED, EACH_IS_CONTROLLED } from '../../../../constants.js';
import { run_all } from '../../../common.js'; import { noop } 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 '../blocks/each.js'; import { destroy_each_item_block, get_first_element } from '../blocks/each.js';
import { schedule_raf_task } from '../task.js'; import { schedule_raf_task } from '../task.js';
import { append_child, empty } from '../operations.js'; import { append_child, empty } from '../operations.js';
import { import { user_effect } from '../../reactivity/effects.js';
destroy_effect, import { current_effect, execute_effect, untrack } from '../../runtime.js';
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 { raf } from '../../timing.js'; import { raf } from '../../timing.js';
import { loop } from '../../loop.js';
import { run_transitions } from '../../render.js';
const active_tick_animations = new Set(); const active_tick_animations = new Set();
const DELAY_NEXT_TICK = Number.MIN_SAFE_INTEGER; const DELAY_NEXT_TICK = Number.MIN_SAFE_INTEGER;
@ -53,7 +35,8 @@ export function transition(dom, get_transition_fn, props, global = false) {
* @returns {void} * @returns {void}
*/ */
export function animate(dom, get_transition_fn, props) { 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 {Set<import('../../types.js').Transition>} transitions
* @param {((t: number, u: number) => string)} css_fn * @param {'in' | 'out' | 'key'} target_direction
* @param {number} duration * @param {DOMRect} [from]
* @param {string} direction * @returns {void}
* @param {boolean} reverse
*/ */
function create_keyframes(easing_fn, css_fn, duration, direction, reverse) { export function trigger_transitions(transitions, target_direction, from) {
/** @type {Keyframe[]} */ // noop, until we excise it from the codebase
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 * @template P
* @param {() => import('../../types.js').TransitionPayload} init * @param {HTMLElement} element
* @param {'in' | 'out' | 'both' | 'key'} direction * @param {() => import('#client').TransitionFn<P | undefined>} get_fn
* @param {import('../../types.js').Effect} effect * @param {(() => P) | null} get_params
* @returns {import('../../types.js').Transition} * @param {'in' | 'out' | 'both'} direction
* @param {boolean} global
* @returns {void}
*/ */
function create_transition(dom, init, direction, effect) { export function bind_transition(element, get_fn, get_params, direction, global) {
let curr_direction = 'in'; const effect = /** @type {import('#client').Effect} */ (current_effect);
/** @type {Array<() => void>} */ let p = direction === 'out' ? 1 : 0;
let subs = [];
/** @type {null | Animation | TickAnimation} */ /** @type {Animation | null} */
let animation = null; let current_animation;
let cancelled = false;
const create_animation = () => { /** @type {import('#client').Task | null} */
let payload = /** @type {import('../../types.js').TransitionPayload} */ (transition.p); let current_task;
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') { /** @type {import('#client').TransitionPayload | null} */
animation = new TickAnimation(tick_fn, duration, delay, direction === 'out'); let current_options;
} 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 = () => { let current_delta = 0;
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} */ /** @type {import('#client').Transition2} */
const transition = { const transition = {
e: effect, global,
i: init, to(target, callback) {
// payload if (current_task) {
p: null, current_task.abort();
current_task = 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();
} }
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;
}
/** current_options ??= get_fn()(element, get_params?.(), { direction });
* @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)
);
}
/** if (!current_options?.duration) {
* @template P current_options = null;
* @param {HTMLElement} dom callback?.();
* @param {() => import('../../types.js').TransitionFn<P | undefined> | import('../../types.js').AnimateFn<P | undefined>} get_transition_fn return;
* @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
});
});
transition = create_transition(dom, init, direction, transition_effect); const { delay = 0, duration, css, tick, easing = linear } = current_options;
const is_intro = direction === 'in';
const show_intro = can_show_intro_on_mount && (is_intro || direction === 'both');
if (show_intro && !already_mounted) { const n = current_options.duration / (1000 / 60);
transition.p = transition.i(); current_delta = target - p;
}
const effect = managed_pre_effect(() => { const adjusted_duration = duration * Math.abs(current_delta);
destroy_effect(effect);
dom.inert = false;
if (show_intro && !already_mounted) { if (css) {
transition.in(); // WAAPI
} const keyframes = [];
/** @type {import('../../types.js').Block | null} */ for (let i = 0; i <= n; i += 1) {
let transition_block = block; const eased = easing(i / n);
while (!is_intro && transition_block !== null) { const t = p + current_delta * eased;
const parent = transition_block.p; const styles = css(t, 1 - t);
if (is_transition_block(transition_block)) { keyframes.push(css_to_keyframe(styles));
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;
}
} }
transition_block = parent;
} current_animation = element.animate(keyframes, {
}); delay,
duration: adjusted_duration,
easing: 'linear',
fill: 'forwards'
}); });
if (direction === 'key') { current_animation.finished
effect(() => { .then(() => {
return () => { p = target;
transition.x(); 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;
}); });
}
}
/** current_options = null;
* @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();
} }
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);
});
});
}
}
/** // TODO don't pass strings around like this, it's silly
* @this {import('../../types.js').IfBlock} if (direction === 'in' || direction === 'both') {
* @param {import('../../types.js').Transition} transition (effect.in ??= []).push(transition);
* @returns {void}
*/ // if this is a local transition, we only want to run it if the parent (block) effect's
function if_block_transition(transition) { // parent (branch) effect is where the state change happened. we can determine that by
const block = this; // looking at whether the branch effect is currently initializing
// block.value === true const should_run =
if (block.v) { run_transitions && (global || /** @type {import('#client').Effect} */ (effect.parent).ran);
const consequent_transitions = (block.c ??= new Set());
consequent_transitions.add(transition); if (should_run) {
transition.f(() => { user_effect(() => {
const c = /** @type {Set<import('../../types.js').Transition>} */ (consequent_transitions); untrack(() => transition.to(1));
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));
}
}); });
} else { } else {
const alternate_transitions = (block.a ??= new Set()); p = 1;
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));
} }
});
} }
}
/** if (direction === 'out' || direction === 'both') {
* @this {import('../../types.js').EachItemBlock} (effect.out ??= []).push(transition);
* @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);
});
} }

@ -1,18 +1,29 @@
import { DEV } from 'esm-env'; import { DEV } from 'esm-env';
import { import {
check_dirtiness,
current_block, current_block,
current_component_context, current_component_context,
current_effect, current_effect,
current_reaction, current_reaction,
destroy_children, destroy_children,
execute_effect,
get, get,
remove_reactions, remove_reactions,
schedule_effect, schedule_effect,
set_signal_status, set_signal_status,
untrack untrack
} from '../runtime.js'; } from '../runtime.js';
import { DIRTY, MANAGED, RENDER_EFFECT, EFFECT, PRE_EFFECT, DESTROYED } from '../constants.js'; import {
DIRTY,
MANAGED,
RENDER_EFFECT,
EFFECT,
PRE_EFFECT,
DESTROYED,
INERT
} from '../constants.js';
import { set } from './sources.js'; import { set } from './sources.js';
import { noop } from '../../common.js';
/** /**
* @param {import('./types.js').EffectType} type * @param {import('./types.js').EffectType} type
@ -25,6 +36,7 @@ import { set } from './sources.js';
function create_effect(type, fn, sync, block = current_block, init = true) { function create_effect(type, fn, sync, block = current_block, init = true) {
/** @type {import('#client').Effect} */ /** @type {import('#client').Effect} */
const signal = { const signal = {
parent: current_effect,
block, block,
deps: null, deps: null,
f: type | DIRTY, f: type | DIRTY,
@ -34,7 +46,10 @@ function create_effect(type, fn, sync, block = current_block, init = true) {
deriveds: null, deriveds: null,
teardown: null, teardown: null,
ctx: current_component_context, ctx: current_component_context,
ondestroy: null ondestroy: null,
in: null,
out: null,
ran: false
}; };
if (current_effect !== null) { if (current_effect !== null) {
@ -236,3 +251,89 @@ export function destroy_effect(signal) {
signal.deps = signal.deps =
null; null;
} }
/**
* @param {import('#client').Effect} effect
* @param {() => void} callback
*/
export function pause_effect(effect, callback = noop) {
/** @type {import('#client').Transition2[]} */
const transitions = [];
pause_children(effect, transitions, true);
let remaining = transitions.length;
if (remaining > 0) {
const check = () => {
if (!--remaining) {
destroy_effect(effect);
callback();
}
};
for (const transition of transitions) {
transition.to(0, check);
}
} else {
destroy_effect(effect);
callback();
}
}
/**
* @param {import('#client').Effect} effect
* @param {import('#client').Transition2[]} transitions
* @param {boolean} local
*/
function pause_children(effect, transitions, local) {
if ((effect.f & INERT) !== 0) return;
effect.f |= INERT;
if (effect.out) {
for (const transition of effect.out) {
if (transition.global || local) {
transitions.push(transition);
}
}
}
if (effect.effects) {
for (const child of effect.effects) {
pause_children(child, transitions, false);
}
}
}
/**
* @param {import('#client').Effect} effect
*/
export function resume_effect(effect) {
resume_children(effect, true);
}
/**
* @param {import('#client').Effect} effect
* @param {boolean} local
*/
function resume_children(effect, local) {
if (check_dirtiness(effect)) {
execute_effect(effect);
}
if (effect.effects) {
for (const child of effect.effects) {
resume_children(child, false);
}
}
effect.f ^= INERT;
if (effect.in) {
for (const transition of effect.in) {
if (transition.global || local) {
transition.to(1);
}
}
}
}

@ -36,6 +36,7 @@ export interface Derived<V = unknown> extends Value<V>, Reaction {
} }
export interface Effect extends Reaction { export interface Effect extends Reaction {
parent: Effect | null;
/** The block associated with this effect */ /** The block associated with this effect */
block: null | Block; block: null | Block;
/** The associated component context */ /** The associated component context */
@ -48,6 +49,11 @@ export interface Effect extends Reaction {
teardown: null | (() => void); teardown: null | (() => void);
/** The depth from the root signal, used for ordering render/pre-effects topologically **/ /** The depth from the root signal, used for ordering render/pre-effects topologically **/
l: number; l: number;
// transitions TODO
in: null | any[];
out: null | any[];
ran: boolean; // TODO fold this into the bitmask
} }
export interface ValueDebug<V = unknown> extends Value<V> { export interface ValueDebug<V = unknown> extends Value<V> {

@ -21,6 +21,8 @@ export const all_registered_events = new Set();
/** @type {Set<(events: Array<string>) => void>} */ /** @type {Set<(events: Array<string>) => void>} */
export const root_event_handles = new Set(); export const root_event_handles = new Set();
export let run_transitions = true;
/** /**
* @param {Element} dom * @param {Element} dom
* @param {() => string} value * @param {() => string} value
@ -198,6 +200,8 @@ function _mount(Component, options) {
const registered_events = new Set(); const registered_events = new Set();
const container = options.target; const container = options.target;
run_transitions = options.intro ?? false;
/** @type {import('#client').RootBlock} */ /** @type {import('#client').RootBlock} */
const block = { const block = {
// dom // dom
@ -246,6 +250,8 @@ function _mount(Component, options) {
const bound_event_listener = handle_event_propagation.bind(null, container); const bound_event_listener = handle_event_propagation.bind(null, container);
const bound_document_event_listener = handle_event_propagation.bind(null, document); const bound_document_event_listener = handle_event_propagation.bind(null, document);
run_transitions = true;
/** @param {Array<string>} events */ /** @param {Array<string>} events */
const event_handle = (events) => { const event_handle = (events) => {
for (let i = 0; i < events.length; i++) { for (let i = 0; i < events.length; i++) {

@ -147,7 +147,7 @@ export function batch_inspect(target, prop, receiver) {
* @param {import('./types.js').Reaction} reaction * @param {import('./types.js').Reaction} reaction
* @returns {boolean} * @returns {boolean}
*/ */
function check_dirtiness(reaction) { export function check_dirtiness(reaction) {
var flags = reaction.f; var flags = reaction.f;
if ((flags & DIRTY) !== 0) { if ((flags & DIRTY) !== 0) {
@ -548,6 +548,8 @@ export function schedule_effect(signal, sync) {
} }
} }
} }
signal.ran = true;
} }
/** /**

@ -348,4 +348,11 @@ export type ProxyStateObject<T = Record<string | symbol, any>> = T & {
[STATE_SYMBOL]: ProxyMetadata; [STATE_SYMBOL]: ProxyMetadata;
}; };
// TODO remove the other transition types once we're
// happy we don't need them, and rename this
export interface Transition2 {
global: boolean;
to(target: number, callback?: () => void): void;
}
export * from './reactivity/types'; export * from './reactivity/types';

@ -30,6 +30,7 @@ function tick(time) {
} }
} }
// TODO tidy this up
class Animation { class Animation {
#keyframes; #keyframes;
#duration; #duration;
@ -38,6 +39,12 @@ class Animation {
#target; #target;
#paused; #paused;
#offset = 0;
#finished = () => {};
#cancelled = () => {};
currentTime = 0;
/** /**
* @param {HTMLElement} target * @param {HTMLElement} target
* @param {Keyframe[]} keyframes * @param {Keyframe[]} keyframes
@ -47,6 +54,7 @@ class Animation {
this.#target = target; this.#target = target;
this.#keyframes = keyframes; this.#keyframes = keyframes;
this.#duration = options.duration || 0; this.#duration = options.duration || 0;
this.#timeline_offset = 0; this.#timeline_offset = 0;
this.#reversed = false; this.#reversed = false;
this.#paused = false; this.#paused = false;
@ -59,6 +67,23 @@ class Animation {
this.#keyframes = keyframes; this.#keyframes = keyframes;
} }
}; };
this.#offset = raf.time;
// Promise-like semantics, but call callbacks immediately on raf.tick
this.finished = {
/** @param {() => void} callback */
then: (callback) => {
this.#finished = callback;
return {
/** @param {() => void} callback */
catch: (callback) => {
this.#cancelled = callback;
}
};
}
};
} }
play() { play() {
@ -80,6 +105,11 @@ class Animation {
} }
const target_frame = this.currentTime / this.#duration; const target_frame = this.currentTime / this.#duration;
this._applyKeyFrame(target_frame); this._applyKeyFrame(target_frame);
if (this.currentTime >= this.#duration) {
this.#finished();
raf.animations.delete(this);
}
} }
/** /**
@ -126,6 +156,9 @@ class Animation {
if (this.currentTime > 0 && this.currentTime < this.#duration) { if (this.currentTime > 0 && this.currentTime < this.#duration) {
this._applyKeyFrame(this.#reversed ? this.#keyframes.length - 1 : 0); this._applyKeyFrame(this.#reversed ? this.#keyframes.length - 1 : 0);
} }
this.#cancelled();
raf.animations.delete(this);
} }
pause() { pause() {
@ -150,6 +183,7 @@ class Animation {
*/ */
HTMLElement.prototype.animate = function (keyframes, options) { HTMLElement.prototype.animate = function (keyframes, options) {
const animation = new Animation(this, keyframes, options); const animation = new Animation(this, keyframes, options);
raf.animations.add(animation);
// @ts-ignore // @ts-ignore
return animation; return animation;
}; };

@ -11,6 +11,6 @@ export default test({
btn1.click(); btn1.click();
}); });
assert.htmlEqual(target.innerHTML, `<button>hide</button><div style="opacity: 0;">hello</div>`); assert.htmlEqual(target.innerHTML, `<button>hide</button><div>hello</div>`);
} }
}); });

@ -34,7 +34,7 @@ export default test({
assert.htmlEqual( assert.htmlEqual(
target.innerHTML, target.innerHTML,
`<button>show</button><button>animate</button><h1 style="opacity: 0;">Hello\n!</h1>` `<button>show</button><button>animate</button><h1>Hello\n!</h1>`
); );
} }
}); });

Loading…
Cancel
Save