UNPKG

animate-css-grid

Version:

Buttery smooth, easy transitions for CSS grid

1 lines 11.9 kB
{"mappings":";AAiCA;IACE,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED;IACE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,eAAe,CAAC;IAC/B,OAAO,CAAC,EAAE,CAAC,gBAAgB,EAAE,WAAW,EAAE,KAAK,IAAI,CAAC;IACpD,KAAK,CAAC,EAAE,CAAC,gBAAgB,EAAE,WAAW,EAAE,KAAK,IAAI,CAAC;CACnD;AC4CD,OAAO,MAAM,sBACA,WAAW,mDAOnB,iBAAiB;;;CA+OrB,CAAC","sources":["src/src/types.ts","src/src/index.ts","src/index.ts"],"sourcesContent":[null,null,"/* eslint-disable @typescript-eslint/no-empty-function */\nimport {\n anticipate,\n backIn,\n backInOut,\n backOut,\n circIn,\n circInOut,\n circOut,\n easeIn,\n easeInOut,\n easeOut,\n linear,\n} from '@popmotion/easing';\nimport sync from 'framesync';\nimport throttle from 'lodash.throttle';\nimport { tween } from 'popmotion';\nimport {\n BoundingClientRect,\n CachedPositionData,\n ChildBoundingClientRect,\n Coords,\n ItemPosition,\n PopmotionEasing,\n WrapGridArguments,\n} from './types';\n\nconst popmotionEasing: PopmotionEasing = {\n anticipate,\n backIn,\n backInOut,\n backOut,\n circIn,\n circInOut,\n circOut,\n easeIn,\n easeInOut,\n easeOut,\n linear,\n};\n\nconst DATASET_KEY = 'animateGridId';\n\n// in order to account for scroll, (which we're not listening for)\n// always cache the item's position relative\n// to the top and left of the grid container\nconst getGridAwareBoundingClientRect = (\n gridBoundingClientRect: BoundingClientRect,\n el: HTMLElement\n): BoundingClientRect => {\n const { top, left, width, height } = el.getBoundingClientRect();\n const rect = { top, left, width, height };\n rect.top -= gridBoundingClientRect.top;\n rect.left -= gridBoundingClientRect.left;\n // if an element is display:none it will return top: 0 and left:0\n // rather than saying it's still in the containing element\n // so we need to use Math.max to make sure the coordinates stay\n // within the container\n rect.top = Math.max(rect.top, 0);\n rect.left = Math.max(rect.left, 0);\n return rect;\n};\n\n// the function used during the tweening\nconst applyCoordTransform = (\n el: HTMLElement,\n { translateX, translateY, scaleX, scaleY }: Coords,\n { immediate }: { immediate?: boolean } = {}\n): void => {\n const isFinished =\n translateX === 0 && translateY === 0 && scaleX === 1 && scaleY === 1;\n const styleEl = () => {\n el.style.transform = isFinished\n ? ''\n : `translateX(${translateX}px) translateY(${translateY}px) scaleX(${scaleX}) scaleY(${scaleY})`;\n };\n if (immediate) {\n styleEl();\n } else {\n sync.render(styleEl);\n }\n const firstChild = el.children[0] as HTMLElement;\n if (firstChild) {\n const styleChild = () => {\n firstChild.style.transform = isFinished\n ? ''\n : `scaleX(${1 / scaleX}) scaleY(${1 / scaleY})`;\n };\n if (immediate) {\n styleChild();\n } else {\n sync.render(styleChild);\n }\n }\n};\n\n// return a function that take a reference to a grid dom node and optional config\nexport const wrapGrid = (\n container: HTMLElement,\n {\n duration = 250,\n stagger = 0,\n easing = 'easeInOut',\n onStart = () => { },\n onEnd = () => { },\n }: WrapGridArguments = {}\n) => {\n if (!popmotionEasing[easing]) {\n throw new Error(`${easing} is not a valid easing name`);\n }\n\n let mutationsDisabled = false;\n\n const disableMutationsWhileFunctionRuns = (func: () => void) => {\n mutationsDisabled = true;\n func();\n setTimeout(() => {\n mutationsDisabled = false;\n }, 0);\n };\n\n // all cached position data, and in-progress tween data, is stored here\n const cachedPositionData: CachedPositionData = {};\n // initially and after every transition, record element positions\n const recordPositions = (\n elements: HTMLCollectionOf<HTMLElement> | HTMLElement[]\n ) => {\n const gridBoundingClientRect = container.getBoundingClientRect();\n Array.from(elements).forEach(el => {\n if (typeof el.getBoundingClientRect !== 'function') {\n return;\n }\n if (!el.dataset[DATASET_KEY]) {\n const newId = `${Math.random()}`;\n el.dataset[DATASET_KEY] = newId;\n }\n const animateGridId = el.dataset[DATASET_KEY] as string;\n\n if (!cachedPositionData[animateGridId]) {\n cachedPositionData[animateGridId] = {} as ItemPosition;\n }\n\n const rect = getGridAwareBoundingClientRect(gridBoundingClientRect, el);\n cachedPositionData[animateGridId].rect = rect;\n cachedPositionData[\n animateGridId\n ].gridBoundingClientRect = gridBoundingClientRect;\n });\n };\n recordPositions(container.children as HTMLCollectionOf<HTMLElement>);\n\n const throttledResizeListener = throttle(() => {\n const bodyElement = document.querySelector('body');\n const containerIsNoLongerInPage =\n bodyElement && !bodyElement.contains(container);\n if (!container || containerIsNoLongerInPage) {\n window.removeEventListener('resize', throttledResizeListener);\n }\n recordPositions(container.children as HTMLCollectionOf<HTMLElement>);\n }, 250);\n window.addEventListener('resize', throttledResizeListener);\n\n const throttledScrollListener = throttle(() => {\n recordPositions(container.children as HTMLCollectionOf<HTMLElement>);\n }, 20);\n container.addEventListener('scroll', throttledScrollListener);\n\n const mutationCallback = (\n mutationsList: MutationRecord[] | 'forceGridAnimation'\n ) => {\n if (mutationsList !== 'forceGridAnimation') {\n // check if we care about the mutation\n const relevantMutationHappened = mutationsList.filter(\n (m: MutationRecord) =>\n m.attributeName === 'class' ||\n m.addedNodes.length ||\n m.removedNodes.length\n ).length;\n if (!relevantMutationHappened) {\n return;\n }\n if (mutationsDisabled) return;\n }\n const gridBoundingClientRect = container.getBoundingClientRect();\n const childrenElements = Array.from(container.children) as HTMLElement[];\n // stop current transitions and remove transforms on transitioning elements\n childrenElements\n .filter(el => {\n const itemPosition =\n cachedPositionData[el.dataset[DATASET_KEY] as string];\n if (itemPosition && itemPosition.stopTween) {\n itemPosition.stopTween();\n delete itemPosition.stopTween;\n return true;\n }\n })\n .forEach(el => {\n el.style.transform = '';\n const firstChild = el.children[0] as HTMLElement;\n if (firstChild) {\n firstChild.style.transform = '';\n }\n });\n const animatedGridChildren = childrenElements\n .map(el => ({\n childCoords: {} as ChildBoundingClientRect,\n el,\n boundingClientRect: getGridAwareBoundingClientRect(\n gridBoundingClientRect,\n el\n ),\n }))\n .filter(({ el, boundingClientRect }) => {\n const itemPosition =\n cachedPositionData[el.dataset[DATASET_KEY] as string];\n // don't animate the initial appearance of elements,\n // just cache their position so they can be animated later\n if (!itemPosition) {\n recordPositions([el]);\n return false;\n } else if (\n boundingClientRect.top === itemPosition.rect.top &&\n boundingClientRect.left === itemPosition.rect.left &&\n boundingClientRect.width === itemPosition.rect.width &&\n boundingClientRect.height === itemPosition.rect.height\n ) {\n // if it hasn't moved, dont animate it\n return false;\n }\n return true;\n });\n\n // having more than one child in the animated item is not supported\n animatedGridChildren.forEach(({ el }) => {\n if (Array.from(el.children).length > 1) {\n throw new Error(\n 'Make sure every grid item has a single container element surrounding its children'\n );\n }\n });\n\n if (!animatedGridChildren.length) {\n return;\n }\n\n const animatedElements = animatedGridChildren.map(({ el }) => el);\n disableMutationsWhileFunctionRuns(() => onStart(animatedElements));\n\n const completionPromises: Array<Promise<unknown>> = [];\n\n animatedGridChildren\n // do this measurement first so as not to cause layout thrashing\n .map(data => {\n const firstChild = data.el.children[0] as HTMLElement;\n // different transform origins give different effects. \"50% 50%\" is default\n if (firstChild) {\n data.childCoords = getGridAwareBoundingClientRect(\n gridBoundingClientRect,\n firstChild\n );\n }\n return data;\n })\n .forEach(\n (\n {\n el,\n boundingClientRect: { top, left, width, height },\n childCoords: { top: childTop, left: childLeft },\n },\n i\n ) => {\n const firstChild = el.children[0] as HTMLElement;\n const itemPosition =\n cachedPositionData[el.dataset[DATASET_KEY] as string];\n const coords: Coords = {\n scaleX: itemPosition.rect.width / width,\n scaleY: itemPosition.rect.height / height,\n translateX: itemPosition.rect.left - left,\n translateY: itemPosition.rect.top - top,\n };\n\n el.style.transformOrigin = '0 0';\n if (firstChild && childLeft === left && childTop === top) {\n firstChild.style.transformOrigin = '0 0';\n }\n\n let cachedResolve: () => void;\n\n const completionPromise = new Promise(resolve => {\n cachedResolve = resolve as () => void;\n });\n\n completionPromises.push(completionPromise);\n\n applyCoordTransform(el, coords, { immediate: true });\n // now start the animation\n const startAnimation = () => {\n const { stop } = tween({\n from: coords,\n to: { translateX: 0, translateY: 0, scaleX: 1, scaleY: 1 },\n duration,\n ease: popmotionEasing[easing],\n }).start({\n update: (transforms: Coords) => {\n applyCoordTransform(el, transforms);\n // this helps prevent layout thrashing\n sync.postRender(() => recordPositions([el]));\n },\n complete: cachedResolve,\n });\n itemPosition.stopTween = stop;\n };\n\n if (typeof stagger !== 'number') {\n startAnimation();\n } else {\n const timeoutId = setTimeout(() => {\n sync.update(startAnimation);\n }, stagger * i);\n itemPosition.stopTween = () => clearTimeout(timeoutId);\n }\n }\n );\n\n Promise.all(completionPromises).then(() => {\n onEnd(animatedElements);\n });\n };\n\n const observer = new MutationObserver(mutationCallback);\n observer.observe(container, {\n childList: true,\n attributes: true,\n subtree: true,\n attributeFilter: ['class'],\n });\n const unwrapGrid = () => {\n window.removeEventListener('resize', throttledResizeListener);\n container.removeEventListener('scroll', throttledScrollListener);\n observer.disconnect();\n };\n const forceGridAnimation = () => mutationCallback('forceGridAnimation');\n return { unwrapGrid, forceGridAnimation };\n};\n"],"names":[],"version":3,"file":"index.d.ts.map"}