svelte
Version:
Cybernetically enhanced web apps
301 lines (282 loc) • 10.2 kB
JavaScript
/** @import { BlurParams, CrossfadeParams, DrawParams, FadeParams, FlyParams, ScaleParams, SlideParams, TransitionConfig } from './public' */
import { DEV } from 'esm-env';
import * as w from '../internal/client/warnings.js';
/** @param {number} x */
const linear = (x) => x;
/** @param {number} t */
function cubic_out(t) {
const f = t - 1.0;
return f * f * f + 1.0;
}
/**
* @param {number} t
* @returns {number}
*/
function cubic_in_out(t) {
return t < 0.5 ? 4.0 * t * t * t : 0.5 * Math.pow(2.0 * t - 2.0, 3.0) + 1.0;
}
/** @param {number | string} value
* @returns {[number, string]}
*/
function split_css_unit(value) {
const split = typeof value === 'string' && value.match(/^\s*(-?[\d.]+)([^\s]*)\s*$/);
return split ? [parseFloat(split[1]), split[2] || 'px'] : [/** @type {number} */ (value), 'px'];
}
/**
* Animates a `blur` filter alongside an element's opacity.
*
* @param {Element} node
* @param {BlurParams} [params]
* @returns {TransitionConfig}
*/
export function blur(
node,
{ delay = 0, duration = 400, easing = cubic_in_out, amount = 5, opacity = 0 } = {}
) {
const style = getComputedStyle(node);
const target_opacity = +style.opacity;
const f = style.filter === 'none' ? '' : style.filter;
const od = target_opacity * (1 - opacity);
const [value, unit] = split_css_unit(amount);
return {
delay,
duration,
easing,
css: (_t, u) => `opacity: ${target_opacity - od * u}; filter: ${f} blur(${u * value}${unit});`
};
}
/**
* Animates the opacity of an element from 0 to the current opacity for `in` transitions and from the current opacity to 0 for `out` transitions.
*
* @param {Element} node
* @param {FadeParams} [params]
* @returns {TransitionConfig}
*/
export function fade(node, { delay = 0, duration = 400, easing = linear } = {}) {
const o = +getComputedStyle(node).opacity;
return {
delay,
duration,
easing,
css: (t) => `opacity: ${t * o}`
};
}
/**
* Animates the x and y positions and the opacity of an element. `in` transitions animate from the provided values, passed as parameters to the element's default values. `out` transitions animate from the element's default values to the provided values.
*
* @param {Element} node
* @param {FlyParams} [params]
* @returns {TransitionConfig}
*/
export function fly(
node,
{ delay = 0, duration = 400, easing = cubic_out, x = 0, y = 0, opacity = 0 } = {}
) {
const style = getComputedStyle(node);
const target_opacity = +style.opacity;
const transform = style.transform === 'none' ? '' : style.transform;
const od = target_opacity * (1 - opacity);
const [x_value, x_unit] = split_css_unit(x);
const [y_value, y_unit] = split_css_unit(y);
return {
delay,
duration,
easing,
css: (t, u) => `
transform: ${transform} translate(${(1 - t) * x_value}${x_unit}, ${(1 - t) * y_value}${y_unit});
opacity: ${target_opacity - od * u}`
};
}
var slide_warning = false;
/**
* Slides an element in and out.
*
* @param {Element} node
* @param {SlideParams} [params]
* @returns {TransitionConfig}
*/
export function slide(node, { delay = 0, duration = 400, easing = cubic_out, axis = 'y' } = {}) {
const style = getComputedStyle(node);
if (DEV && !slide_warning && /(contents|inline|table)/.test(style.display)) {
slide_warning = true;
Promise.resolve().then(() => (slide_warning = false));
w.transition_slide_display(style.display);
}
const opacity = +style.opacity;
const primary_property = axis === 'y' ? 'height' : 'width';
const primary_property_value = parseFloat(style[primary_property]);
const secondary_properties = axis === 'y' ? ['top', 'bottom'] : ['left', 'right'];
const capitalized_secondary_properties = secondary_properties.map(
(e) => /** @type {'Left' | 'Right' | 'Top' | 'Bottom'} */ (`${e[0].toUpperCase()}${e.slice(1)}`)
);
const padding_start_value = parseFloat(style[`padding${capitalized_secondary_properties[0]}`]);
const padding_end_value = parseFloat(style[`padding${capitalized_secondary_properties[1]}`]);
const margin_start_value = parseFloat(style[`margin${capitalized_secondary_properties[0]}`]);
const margin_end_value = parseFloat(style[`margin${capitalized_secondary_properties[1]}`]);
const border_width_start_value = parseFloat(
style[`border${capitalized_secondary_properties[0]}Width`]
);
const border_width_end_value = parseFloat(
style[`border${capitalized_secondary_properties[1]}Width`]
);
return {
delay,
duration,
easing,
css: (t) =>
'overflow: hidden;' +
`opacity: ${Math.min(t * 20, 1) * opacity};` +
`${primary_property}: ${t * primary_property_value}px;` +
`padding-${secondary_properties[0]}: ${t * padding_start_value}px;` +
`padding-${secondary_properties[1]}: ${t * padding_end_value}px;` +
`margin-${secondary_properties[0]}: ${t * margin_start_value}px;` +
`margin-${secondary_properties[1]}: ${t * margin_end_value}px;` +
`border-${secondary_properties[0]}-width: ${t * border_width_start_value}px;` +
`border-${secondary_properties[1]}-width: ${t * border_width_end_value}px;` +
`min-${primary_property}: 0`
};
}
/**
* Animates the opacity and scale of an element. `in` transitions animate from the provided values, passed as parameters, to an element's current (default) values. `out` transitions animate from an element's default values to the provided values.
*
* @param {Element} node
* @param {ScaleParams} [params]
* @returns {TransitionConfig}
*/
export function scale(
node,
{ delay = 0, duration = 400, easing = cubic_out, start = 0, opacity = 0 } = {}
) {
const style = getComputedStyle(node);
const target_opacity = +style.opacity;
const transform = style.transform === 'none' ? '' : style.transform;
const sd = 1 - start;
const od = target_opacity * (1 - opacity);
return {
delay,
duration,
easing,
css: (_t, u) => `
transform: ${transform} scale(${1 - sd * u});
opacity: ${target_opacity - od * u}
`
};
}
/**
* Animates the stroke of an SVG element, like a snake in a tube. `in` transitions begin with the path invisible and draw the path to the screen over time. `out` transitions start in a visible state and gradually erase the path. `draw` only works with elements that have a `getTotalLength` method, like `<path>` and `<polyline>`.
*
* @param {SVGElement & { getTotalLength(): number }} node
* @param {DrawParams} [params]
* @returns {TransitionConfig}
*/
export function draw(node, { delay = 0, speed, duration, easing = cubic_in_out } = {}) {
let len = node.getTotalLength();
const style = getComputedStyle(node);
if (style.strokeLinecap !== 'butt') {
len += parseInt(style.strokeWidth);
}
if (duration === undefined) {
if (speed === undefined) {
duration = 800;
} else {
duration = len / speed;
}
} else if (typeof duration === 'function') {
duration = duration(len);
}
return {
delay,
duration,
easing,
css: (_, u) => `
stroke-dasharray: ${len};
stroke-dashoffset: ${u * len};
`
};
}
/**
* @template T
* @template S
* @param {T} tar
* @param {S} src
* @returns {T & S}
*/
function assign(tar, src) {
// @ts-ignore
for (const k in src) tar[k] = src[k];
return /** @type {T & S} */ (tar);
}
/**
* The `crossfade` function creates a pair of [transitions](https://svelte.dev/docs/svelte/transition) called `send` and `receive`. When an element is 'sent', it looks for a corresponding element being 'received', and generates a transition that transforms the element to its counterpart's position and fades it out. When an element is 'received', the reverse happens. If there is no counterpart, the `fallback` transition is used.
*
* @param {CrossfadeParams & {
* fallback?: (node: Element, params: CrossfadeParams, intro: boolean) => TransitionConfig;
* }} params
* @returns {[(node: any, params: CrossfadeParams & { key: any; }) => () => TransitionConfig, (node: any, params: CrossfadeParams & { key: any; }) => () => TransitionConfig]}
*/
export function crossfade({ fallback, ...defaults }) {
/** @type {Map<any, Element>} */
const to_receive = new Map();
/** @type {Map<any, Element>} */
const to_send = new Map();
/**
* @param {Element} from_node
* @param {Element} node
* @param {CrossfadeParams} params
* @returns {TransitionConfig}
*/
function crossfade(from_node, node, params) {
const {
delay = 0,
duration = /** @param {number} d */ (d) => Math.sqrt(d) * 30,
easing = cubic_out
} = assign(assign({}, defaults), params);
const from = from_node.getBoundingClientRect();
const to = node.getBoundingClientRect();
const dx = from.left - to.left;
const dy = from.top - to.top;
const dw = from.width / to.width;
const dh = from.height / to.height;
const d = Math.sqrt(dx * dx + dy * dy);
const style = getComputedStyle(node);
const transform = style.transform === 'none' ? '' : style.transform;
const opacity = +style.opacity;
return {
delay,
duration: typeof duration === 'function' ? duration(d) : duration,
easing,
css: (t, u) => `
opacity: ${t * opacity};
transform-origin: top left;
transform: ${transform} translate(${u * dx}px,${u * dy}px) scale(${t + (1 - t) * dw}, ${
t + (1 - t) * dh
});
`
};
}
/**
* @param {Map<any, Element>} items
* @param {Map<any, Element>} counterparts
* @param {boolean} intro
* @returns {(node: any, params: CrossfadeParams & { key: any; }) => () => TransitionConfig}
*/
function transition(items, counterparts, intro) {
// @ts-expect-error TODO improve typings (are the public types wrong?)
return (node, params) => {
items.set(params.key, node);
return () => {
if (counterparts.has(params.key)) {
const other_node = counterparts.get(params.key);
counterparts.delete(params.key);
return crossfade(/** @type {Element} */ (other_node), node, params);
}
// if the node is disappearing altogether
// (i.e. wasn't claimed by the other list)
// then we need to supply an outro
items.delete(params.key);
return fallback && fallback(node, params, intro);
};
};
}
return [transition(to_send, to_receive, false), transition(to_receive, to_send, true)];
}