m3-svelte
Version:
M3 Svelte implements the Material 3 design system in Svelte. See the [website](https://kendell.dev/m3-svelte/) for demos and usage instructions.
178 lines (177 loc) • 8.53 kB
JavaScript
import { easeEmphasized } from "./easing";
const parseSize = (size) => (size.endsWith("px")
? +size.slice(0, -2)
: size.endsWith("rem")
? +size.slice(0, -3) * 16
: null) || 0;
const getBackgroundColor = (node, defaultColor) => {
if (!defaultColor) {
const tmp = document.createElement("div");
document.body.appendChild(tmp);
defaultColor = getComputedStyle(tmp).backgroundColor;
tmp.remove();
}
const color = getComputedStyle(node).backgroundColor;
if (color != defaultColor)
return color;
if (node.parentElement)
return getBackgroundColor(node.parentElement, defaultColor);
return defaultColor;
};
const parseColor = (color) => {
const match = color.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*([\d.]+))?\)/);
if (match) {
const [r, g, b, opacity = 1.0] = match.slice(1, 5).map((val) => val && parseFloat(val));
if (typeof r != "number" ||
typeof g != "number" ||
typeof b != "number" ||
typeof opacity != "number") {
console.log(color, match, r, g, b, opacity);
throw new Error("something went down in the color parser, see previous info");
}
return [r, g, b, opacity];
}
return [0, 0, 0, 0];
};
export const containerTransform = ({ fallback, ...defaults }) => {
/* This code is based on the crossfade function from Svelte. Svelte is under the MIT license.
https://github.com/sveltejs/svelte/blob/master/src/runtime/transition/index.ts
If you have an idea for cleaning up this mess of code, please make a PR. */
const to_receive = new Map();
const to_send = new Map();
function calcTransition(from, fromNode, node, params) {
const to = node.getBoundingClientRect();
const isEntering = from.width * from.height < to.width * to.height;
const dx = from.left + from.width / 2 - (to.left + to.width / 2);
const dy = from.top - to.top;
const style = getComputedStyle(node);
const transform = style.transform == "none" ? "" : style.transform;
const opacity = +style.opacity;
const bgContainerZ = params.bgContainerZ || defaults.bgContainerZ || 4;
const fgContainerZ = params.fgContainerZ || defaults.fgContainerZ || 5;
let container = {
fromColor: parseColor(getBackgroundColor(node)),
fromRadius: parseSize(style.borderRadius),
fromBorderWidth: parseSize(style.borderLeftWidth),
fromBorderColor: parseColor(style.borderLeftColor),
toColor: parseColor(getBackgroundColor(fromNode)),
toRadius: parseSize(getComputedStyle(fromNode).borderRadius),
toBorderWidth: parseSize(getComputedStyle(fromNode).borderLeftWidth),
toBorderColor: parseColor(getComputedStyle(fromNode).borderLeftColor),
};
return {
delay: params.delay || defaults.delay || 0,
duration: params.duration || defaults.duration || 500,
easing: params.easing || defaults.easing || easeEmphasized,
css: (t, u) => {
const dw = t + u * (from.width / to.width);
const dh = t + u * (from.height / to.height);
const tOpacity = (isEntering ? (10 * t - 3) / 7 : (-10 / 3) * u + 1) * opacity;
const tScale = isEntering ? Math.max(dw, dh) : Math.min(dw, dh);
const horizontalTrim = ((tScale - dw) * to.width) / tScale / 2;
const verticalTrim = ((tScale - dh) * to.height) / tScale;
return `
opacity: ${tOpacity};
transform-origin: top center;
transform: ${transform} translate(${u * dx}px, ${u * dy}px) scale(${tScale});
clip-path: inset(0 ${horizontalTrim}px ${verticalTrim}px ${horizontalTrim}px);
z-index: ${fgContainerZ};
${t < 0.98 ? "background-color: transparent;" : ""}
border-color: transparent;
pointer-events: none;
`;
},
tick: (t, u) => {
if (!isEntering || !container)
return;
if (container.backwards == null)
container.backwards = Boolean(t);
if (!container.e) {
container.e = document.createElement("div");
container.e.style.position = "fixed";
container.e.style.zIndex = bgContainerZ.toString();
container.e.style.boxSizing = "border-box";
container.e.style.borderStyle = "solid";
document.body.appendChild(container.e);
}
else if (t == (container.backwards ? 0 : 1)) {
document.body.removeChild(container.e);
return (container = null);
}
container.e.style.top = (u * from.top + t * to.top).toFixed(1) + "px";
container.e.style.left = (u * from.left + t * to.left).toFixed(1) + "px";
container.e.style.width = (u * from.width + t * to.width).toFixed(1) + "px";
container.e.style.height = (u * from.height + t * to.height).toFixed(1) + "px";
const { fromColor, fromRadius, fromBorderWidth, fromBorderColor, toColor, toRadius, toBorderWidth, toBorderColor, } = container;
const interpColor = [0, 0, 0, 0].map((_, i) => Math.trunc(t * fromColor[i] + u * toColor[i]));
container.e.style.backgroundColor = `rgba(${interpColor.join(",")})`;
container.e.style.borderRadius = (t * fromRadius + u * toRadius).toFixed(1) + "px";
container.e.style.borderWidth = (t * fromBorderWidth + u * toBorderWidth).toFixed(1) + "px";
const interpBorder = [0, 0, 0, 0].map((_, i) => Math.trunc(t * fromBorderColor[i] + u * toBorderColor[i]));
container.e.style.borderColor = `rgba(${interpBorder.join(",")})`;
},
};
}
function makeTransition(items, counterparts, intro) {
return (node, params) => {
items.set(params.key, {
rect: node.getBoundingClientRect(),
node,
});
return () => {
const counterpart = counterparts.get(params.key);
if (counterpart) {
counterparts.delete(params.key);
return calcTransition(counterpart.rect, counterpart.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 [makeTransition(to_send, to_receive, false), makeTransition(to_receive, to_send, true)];
};
/* protip: set a background color on the items, and utilize position relative + absolute to let them overlap */
export const sharedAxisTransition = (node, options) => {
return {
delay: options.delay,
duration: options.duration || 500,
easing: options.easing || easeEmphasized,
css: (t, u) => {
const opacity = (t - 0.35) * (1 / 0.35);
if (options.direction == "Z") {
const factor = options.leaving ? u * 0.1 + 1 : t * 0.2 + 0.8;
let css = `transform: scale(${factor.toFixed(3)});`;
if (!options.leaving)
css += `opacity: ${opacity.toFixed(3)};`;
return css;
}
const factor = u * (options.rightSeam ? -30 : 30);
return (`transform: translate${options.direction}(${factor.toFixed(3)}px);` +
`opacity: ${opacity.toFixed(3)}`);
},
};
};
export const outroClass = (node) => {
const addClass = (e) => {
if (!(e.target instanceof Element))
return;
e.target.classList.add("leaving");
};
const removeClass = (e) => {
if (!(e.target instanceof Element))
return;
e.target.classList.remove("leaving");
};
node.addEventListener("outrostart", addClass);
node.addEventListener("outroend", removeClass);
return {
destroy() {
node.removeEventListener("outrostart", addClass);
node.removeEventListener("outroend", removeClass);
},
};
};