UNPKG

svelte

Version:

Cybernetically enhanced web apps

301 lines (282 loc) 10.2 kB
/** @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)]; }