build: ship pre-built API bundle; images copy output instead of installing
- API: `pnpm build` bundles dist/ into apps/api/.output (esbuild), with Linux musl native packages for sharp and argon2 kept alongside - docker/api.Dockerfile copies that output; no pnpm install on the server - add dist/migrate.js (prisma migrate deploy equivalent, same _prisma_migrations table and lock) so the Prisma CLI is not needed at runtime - pnpm-workspace: supportedArchitectures for linux/x64/musl - admin: apps/admin/.env.production sets VITE_SITE_URL for the production build - drop NPM_REGISTRY / SITE_URL build args from compose and .env.example - gitignore: anchor node_modules rules so pre-built node_modules are committed - README: describe the pre-built deploy flow Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Haiku 5.5
parent
ee8258556d
commit
10e453e4be
Generated
Vendored
+67
@@ -0,0 +1,67 @@
|
||||
import { removeEffect, addEffect, GroupAnimationWithThen } from 'motion-dom';
|
||||
import { removeItem } from 'motion-utils';
|
||||
import { animateSequence } from './sequence.mjs';
|
||||
import { animateSubject } from './subject.mjs';
|
||||
|
||||
function isSequence(value) {
|
||||
return Array.isArray(value) && value.some(Array.isArray);
|
||||
}
|
||||
/**
|
||||
* Creates an animation function that is optionally scoped
|
||||
* to a specific element.
|
||||
*/
|
||||
function createScopedAnimate(options = {}) {
|
||||
const { scope, reduceMotion, skipAnimations } = options;
|
||||
/**
|
||||
* Implementation
|
||||
*/
|
||||
function scopedAnimate(subjectOrSequence, optionsOrKeyframes, options) {
|
||||
let animations = [];
|
||||
let animationOnComplete;
|
||||
const inherited = {};
|
||||
if (reduceMotion !== undefined)
|
||||
inherited.reduceMotion = reduceMotion;
|
||||
if (skipAnimations !== undefined)
|
||||
inherited.skipAnimations = skipAnimations;
|
||||
if (isSequence(subjectOrSequence)) {
|
||||
const { onComplete, ...sequenceOptions } = optionsOrKeyframes || {};
|
||||
if (typeof onComplete === "function") {
|
||||
animationOnComplete = onComplete;
|
||||
}
|
||||
animations = animateSequence(subjectOrSequence, { ...inherited, ...sequenceOptions }, scope);
|
||||
}
|
||||
else {
|
||||
// Extract top-level onComplete so it doesn't get applied per-value
|
||||
const { onComplete, ...rest } = options || {};
|
||||
if (typeof onComplete === "function") {
|
||||
animationOnComplete = onComplete;
|
||||
}
|
||||
animations = animateSubject(subjectOrSequence, optionsOrKeyframes, { ...inherited, ...rest }, scope);
|
||||
}
|
||||
const animation = new GroupAnimationWithThen(animations);
|
||||
if (animationOnComplete) {
|
||||
animation.finished.then(animationOnComplete);
|
||||
}
|
||||
if (scope) {
|
||||
scope.animations.push(animation);
|
||||
animation.finished.then(() => {
|
||||
removeItem(scope.animations, animation);
|
||||
});
|
||||
}
|
||||
return animation;
|
||||
}
|
||||
return scopedAnimate;
|
||||
}
|
||||
const animate = Object.assign(createScopedAnimate(), {
|
||||
/**
|
||||
* Register an effect so `animate()` can animate the subjects it
|
||||
* claims, for instance `animate.addEffect(threeEffect)`. The most
|
||||
* recently added effect is tested first. DOM elements are always
|
||||
* animated directly.
|
||||
*/
|
||||
addEffect,
|
||||
removeEffect,
|
||||
});
|
||||
|
||||
export { animate, createScopedAnimate };
|
||||
//# sourceMappingURL=index.mjs.map
|
||||
Generated
Vendored
+23
@@ -0,0 +1,23 @@
|
||||
import { resolveElements } from 'motion-dom';
|
||||
import { isDOMKeyframes } from '../utils/is-dom-keyframes.mjs';
|
||||
|
||||
function resolveSubjects(subject, keyframes, scope, selectorCache) {
|
||||
if (subject == null) {
|
||||
return [];
|
||||
}
|
||||
if (typeof subject === "string" && isDOMKeyframes(keyframes)) {
|
||||
return resolveElements(subject, scope, selectorCache);
|
||||
}
|
||||
else if (subject instanceof NodeList) {
|
||||
return Array.from(subject);
|
||||
}
|
||||
else if (Array.isArray(subject)) {
|
||||
return subject.filter((s) => s != null);
|
||||
}
|
||||
else {
|
||||
return [subject];
|
||||
}
|
||||
}
|
||||
|
||||
export { resolveSubjects };
|
||||
//# sourceMappingURL=resolve-subjects.mjs.map
|
||||
Generated
Vendored
+36
@@ -0,0 +1,36 @@
|
||||
import { motionValue, spring } from 'motion-dom';
|
||||
import { createAnimationsFromSequence } from '../sequence/create.mjs';
|
||||
import { animateSubject } from './subject.mjs';
|
||||
|
||||
function animateSequence(sequence, options, scope) {
|
||||
const animations = [];
|
||||
/**
|
||||
* Pre-process: replace function segments with MotionValue segments,
|
||||
* subscribe callbacks immediately
|
||||
*/
|
||||
const processedSequence = sequence.map((segment) => {
|
||||
if (Array.isArray(segment) && typeof segment[0] === "function") {
|
||||
const callback = segment[0];
|
||||
const mv = motionValue(0);
|
||||
mv.on("change", callback);
|
||||
if (segment.length === 1) {
|
||||
return [mv, [0, 1]];
|
||||
}
|
||||
else if (segment.length === 2) {
|
||||
return [mv, [0, 1], segment[1]];
|
||||
}
|
||||
else {
|
||||
return [mv, segment[1], segment[2]];
|
||||
}
|
||||
}
|
||||
return segment;
|
||||
});
|
||||
const animationDefinitions = createAnimationsFromSequence(processedSequence, options, scope, { spring });
|
||||
animationDefinitions.forEach(({ keyframes, transition }, subject) => {
|
||||
animations.push(...animateSubject(subject, keyframes, transition));
|
||||
});
|
||||
return animations;
|
||||
}
|
||||
|
||||
export { animateSequence };
|
||||
//# sourceMappingURL=sequence.mjs.map
|
||||
Generated
Vendored
+62
@@ -0,0 +1,62 @@
|
||||
import { animateSingleValue, animateElement, visualElementStore, animateEffectSubject, propEffect, findEffect, isMotionValue } from 'motion-dom';
|
||||
import { invariant } from 'motion-utils';
|
||||
import { isDOMKeyframes } from '../utils/is-dom-keyframes.mjs';
|
||||
import { resolveSubjects } from './resolve-subjects.mjs';
|
||||
|
||||
function isSingleValue(subject, keyframes) {
|
||||
return (isMotionValue(subject) ||
|
||||
typeof subject === "number" ||
|
||||
(typeof subject === "string" && !isDOMKeyframes(keyframes)));
|
||||
}
|
||||
/**
|
||||
* Implementation
|
||||
*/
|
||||
function animateSubject(subject, keyframes, options, scope) {
|
||||
const animations = [];
|
||||
if (isSingleValue(subject, keyframes)) {
|
||||
animations.push(animateSingleValue(subject, isDOMKeyframes(keyframes)
|
||||
? keyframes.default || keyframes
|
||||
: keyframes, options ? options.default || options : options));
|
||||
}
|
||||
else {
|
||||
// Gracefully handle null/undefined subjects (e.g., from querySelector returning null)
|
||||
if (subject == null) {
|
||||
return animations;
|
||||
}
|
||||
const subjects = resolveSubjects(subject, keyframes, scope);
|
||||
const numSubjects = subjects.length;
|
||||
invariant(Boolean(numSubjects), "No valid elements provided.", "no-valid-elements");
|
||||
for (let i = 0; i < numSubjects; i++) {
|
||||
const thisSubject = subjects[i];
|
||||
const transition = { ...options };
|
||||
/**
|
||||
* Resolve stagger function if provided.
|
||||
*/
|
||||
if ("delay" in transition &&
|
||||
typeof transition.delay === "function") {
|
||||
transition.delay = transition.delay(i, numSubjects);
|
||||
}
|
||||
if (thisSubject instanceof Element) {
|
||||
/**
|
||||
* An element already owned by a VisualElement (a <motion.*>
|
||||
* component or animateLayout()) animates its values so they
|
||||
* share one renderer. Anything else is driven through
|
||||
* styleEffect (or svgEffect) and the DOM keyframe resolver,
|
||||
* without creating a VisualElement.
|
||||
*/
|
||||
animations.push(...animateElement(thisSubject, keyframes, transition, visualElementStore.get(thisSubject)));
|
||||
}
|
||||
else {
|
||||
/**
|
||||
* Registered effects (animate.addEffect) claim non-DOM
|
||||
* subjects before we fall back to writing plain properties.
|
||||
*/
|
||||
animations.push(...animateEffectSubject(findEffect(thisSubject) ?? propEffect, thisSubject, keyframes, transition));
|
||||
}
|
||||
}
|
||||
}
|
||||
return animations;
|
||||
}
|
||||
|
||||
export { animateSubject };
|
||||
//# sourceMappingURL=subject.mjs.map
|
||||
Generated
Vendored
+110
@@ -0,0 +1,110 @@
|
||||
import { resolveElements, getValueTransition, getAnimationMap, animationMapKey, getComputedStyle, fillWildcards, applyPxDefaults, NativeAnimation } from 'motion-dom';
|
||||
import { invariant, secondsToMilliseconds } from 'motion-utils';
|
||||
|
||||
function animateElements(elementOrSelector, keyframes, options, scope) {
|
||||
// Gracefully handle null/undefined elements (e.g., from querySelector returning null)
|
||||
if (elementOrSelector == null) {
|
||||
return [];
|
||||
}
|
||||
const elements = resolveElements(elementOrSelector, scope);
|
||||
const numElements = elements.length;
|
||||
invariant(Boolean(numElements), "No valid elements provided.", "no-valid-elements");
|
||||
/**
|
||||
* WAAPI doesn't support interrupting animations.
|
||||
*
|
||||
* Therefore, starting animations requires a three-step process:
|
||||
* 1. Stop existing animations (write styles to DOM)
|
||||
* 2. Resolve keyframes (read styles from DOM)
|
||||
* 3. Create new animations (write styles to DOM)
|
||||
*
|
||||
* The hybrid `animate()` function uses AsyncAnimation to resolve
|
||||
* keyframes before creating new animations, which removes style
|
||||
* thrashing. Here, we have much stricter filesize constraints.
|
||||
* Therefore we do this in a synchronous way that ensures that
|
||||
* at least within `animate()` calls there is no style thrashing.
|
||||
*
|
||||
* In the motion-native-animate-mini-interrupt benchmark this
|
||||
* was 80% faster than a single loop.
|
||||
*/
|
||||
const animationDefinitions = [];
|
||||
/**
|
||||
* Step 1: Build options and stop existing animations (write)
|
||||
*/
|
||||
for (let i = 0; i < numElements; i++) {
|
||||
const element = elements[i];
|
||||
const elementTransition = { ...options };
|
||||
/**
|
||||
* Resolve stagger function if provided.
|
||||
*/
|
||||
if (typeof elementTransition.delay === "function") {
|
||||
elementTransition.delay = elementTransition.delay(i, numElements);
|
||||
}
|
||||
for (const valueName in keyframes) {
|
||||
let valueKeyframes = keyframes[valueName];
|
||||
if (!Array.isArray(valueKeyframes)) {
|
||||
valueKeyframes = [valueKeyframes];
|
||||
}
|
||||
const valueOptions = {
|
||||
...getValueTransition(elementTransition, valueName),
|
||||
};
|
||||
valueOptions.duration && (valueOptions.duration = secondsToMilliseconds(valueOptions.duration));
|
||||
valueOptions.delay && (valueOptions.delay = secondsToMilliseconds(valueOptions.delay));
|
||||
/**
|
||||
* If there's an existing animation playing on this element then stop it
|
||||
* before creating a new one.
|
||||
*/
|
||||
const map = getAnimationMap(element);
|
||||
const key = animationMapKey(valueName, valueOptions.pseudoElement || "");
|
||||
const currentAnimation = map.get(key);
|
||||
currentAnimation && currentAnimation.stop();
|
||||
animationDefinitions.push({
|
||||
map,
|
||||
key,
|
||||
unresolvedKeyframes: valueKeyframes,
|
||||
options: {
|
||||
...valueOptions,
|
||||
element,
|
||||
name: valueName,
|
||||
allowFlatten: !elementTransition.type && !elementTransition.ease,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Step 2: Resolve keyframes (read)
|
||||
*/
|
||||
for (let i = 0; i < animationDefinitions.length; i++) {
|
||||
const { unresolvedKeyframes, options: animationOptions } = animationDefinitions[i];
|
||||
const { element, name, pseudoElement } = animationOptions;
|
||||
if (!pseudoElement && unresolvedKeyframes[0] === null) {
|
||||
unresolvedKeyframes[0] = getComputedStyle(element, name);
|
||||
}
|
||||
fillWildcards(unresolvedKeyframes);
|
||||
applyPxDefaults(unresolvedKeyframes, name);
|
||||
/**
|
||||
* If we only have one keyframe, explicitly read the initial keyframe
|
||||
* from the computed style. This is to ensure consistency with WAAPI behaviour
|
||||
* for restarting animations, for instance .play() after finish, when it
|
||||
* has one vs two keyframes.
|
||||
*/
|
||||
if (!pseudoElement && unresolvedKeyframes.length < 2) {
|
||||
unresolvedKeyframes.unshift(getComputedStyle(element, name));
|
||||
}
|
||||
animationOptions.keyframes = unresolvedKeyframes;
|
||||
}
|
||||
/**
|
||||
* Step 3: Create new animations (write)
|
||||
*/
|
||||
const animations = [];
|
||||
for (let i = 0; i < animationDefinitions.length; i++) {
|
||||
const { map, key, options: animationOptions } = animationDefinitions[i];
|
||||
const animation = new NativeAnimation(animationOptions);
|
||||
map.set(key, animation);
|
||||
animation.finished.finally(() => map.delete(key));
|
||||
animations.push(animation);
|
||||
}
|
||||
return animations;
|
||||
}
|
||||
|
||||
export { animateElements };
|
||||
//# sourceMappingURL=animate-elements.mjs.map
|
||||
Generated
Vendored
+13
@@ -0,0 +1,13 @@
|
||||
import { GroupAnimationWithThen } from 'motion-dom';
|
||||
import { animateElements } from './animate-elements.mjs';
|
||||
|
||||
const createScopedWaapiAnimate = (scope) => {
|
||||
function scopedAnimate(elementOrSelector, keyframes, options) {
|
||||
return new GroupAnimationWithThen(animateElements(elementOrSelector, keyframes, options, scope));
|
||||
}
|
||||
return scopedAnimate;
|
||||
};
|
||||
const animateMini = /*@__PURE__*/ createScopedWaapiAnimate();
|
||||
|
||||
export { animateMini, createScopedWaapiAnimate };
|
||||
//# sourceMappingURL=animate-style.mjs.map
|
||||
Generated
Vendored
+308
@@ -0,0 +1,308 @@
|
||||
import { isMotionValue, defaultOffset, isGenerator, createGeneratorEasing, fillOffset } from 'motion-dom';
|
||||
import { progress, secondsToMilliseconds, warning, reverseEasing, getEasingForSegment } from 'motion-utils';
|
||||
import { resolveSubjects } from '../animate/resolve-subjects.mjs';
|
||||
import { calculateRepeatDuration } from './utils/calc-repeat-duration.mjs';
|
||||
import { calcNextTime } from './utils/calc-time.mjs';
|
||||
import { addKeyframes } from './utils/edit.mjs';
|
||||
import { normalizeTimes } from './utils/normalize-times.mjs';
|
||||
import { compareByTime } from './utils/sort.mjs';
|
||||
|
||||
const defaultSegmentEasing = "easeInOut";
|
||||
const MAX_REPEAT = 20;
|
||||
function createAnimationsFromSequence(sequence, { defaultTransition = {}, ...sequenceTransition } = {}, scope, generators) {
|
||||
const defaultDuration = defaultTransition.duration || 0.3;
|
||||
const animationDefinitions = new Map();
|
||||
const sequences = new Map();
|
||||
const elementCache = {};
|
||||
const timeLabels = new Map();
|
||||
let prevTime = 0;
|
||||
let currentTime = 0;
|
||||
let totalDuration = 0;
|
||||
/**
|
||||
* Build the timeline by mapping over the sequence array and converting
|
||||
* the definitions into keyframes and offsets with absolute time values.
|
||||
* These will later get converted into relative offsets in a second pass.
|
||||
*/
|
||||
for (let i = 0; i < sequence.length; i++) {
|
||||
const segment = sequence[i];
|
||||
/**
|
||||
* If this is a timeline label, mark it and skip the rest of this iteration.
|
||||
*/
|
||||
if (typeof segment === "string") {
|
||||
timeLabels.set(segment, currentTime);
|
||||
continue;
|
||||
}
|
||||
else if (!Array.isArray(segment)) {
|
||||
timeLabels.set(segment.name, calcNextTime(currentTime, segment.at, prevTime, timeLabels));
|
||||
continue;
|
||||
}
|
||||
let [subject, keyframes, transition = {}] = segment;
|
||||
/**
|
||||
* If a relative or absolute time value has been specified we need to resolve
|
||||
* it in relation to the currentTime.
|
||||
*/
|
||||
if (transition.at !== undefined) {
|
||||
currentTime = calcNextTime(currentTime, transition.at, prevTime, timeLabels);
|
||||
}
|
||||
/**
|
||||
* Keep track of the maximum duration in this definition. This will be
|
||||
* applied to currentTime once the definition has been parsed.
|
||||
*/
|
||||
let maxDuration = 0;
|
||||
const resolveValueSequence = (valueKeyframes, valueTransition, valueSequence, elementIndex = 0, numSubjects = 0) => {
|
||||
const valueKeyframesAsList = keyframesAsList(valueKeyframes);
|
||||
const { delay = 0, times = defaultOffset(valueKeyframesAsList), type = defaultTransition.type || "keyframes", repeat, repeatType, repeatDelay = 0, ...remainingTransition } = valueTransition;
|
||||
let { ease = defaultTransition.ease || "easeOut", duration } = valueTransition;
|
||||
/**
|
||||
* Resolve stagger() if defined.
|
||||
*/
|
||||
const calculatedDelay = typeof delay === "function"
|
||||
? delay(elementIndex, numSubjects)
|
||||
: delay;
|
||||
/**
|
||||
* If this animation should and can use a spring, generate a spring easing function.
|
||||
*/
|
||||
const numKeyframes = valueKeyframesAsList.length;
|
||||
const createGenerator = isGenerator(type)
|
||||
? type
|
||||
: generators?.[type || "keyframes"];
|
||||
if (numKeyframes <= 2 && createGenerator) {
|
||||
/**
|
||||
* As we're creating an easing function from a spring,
|
||||
* ideally we want to generate it using the real distance
|
||||
* between the two keyframes. However this isn't always
|
||||
* possible - in these situations we use 0-100.
|
||||
*/
|
||||
let absoluteDelta = 100;
|
||||
if (numKeyframes === 2 &&
|
||||
isNumberKeyframesArray(valueKeyframesAsList)) {
|
||||
const delta = valueKeyframesAsList[1] - valueKeyframesAsList[0];
|
||||
absoluteDelta = Math.abs(delta);
|
||||
}
|
||||
const springTransition = {
|
||||
...defaultTransition,
|
||||
...remainingTransition,
|
||||
};
|
||||
if (duration !== undefined) {
|
||||
springTransition.duration = secondsToMilliseconds(duration);
|
||||
}
|
||||
const springEasing = createGeneratorEasing(springTransition, absoluteDelta, createGenerator);
|
||||
ease = springEasing.ease;
|
||||
duration = springEasing.duration;
|
||||
}
|
||||
duration ?? (duration = defaultDuration);
|
||||
const startTime = currentTime + calculatedDelay;
|
||||
/**
|
||||
* If there's only one time offset of 0, fill in a second with length 1
|
||||
*/
|
||||
if (times.length === 1 && times[0] === 0) {
|
||||
times[1] = 1;
|
||||
}
|
||||
/**
|
||||
* Fill out if offset if fewer offsets than keyframes
|
||||
*/
|
||||
const remainder = times.length - valueKeyframesAsList.length;
|
||||
remainder > 0 && fillOffset(times, remainder);
|
||||
/**
|
||||
* If only one value has been set, ie [1], push a null to the start of
|
||||
* the keyframe array. This will let us mark a keyframe at this point
|
||||
* that will later be hydrated with the previous value.
|
||||
*/
|
||||
valueKeyframesAsList.length === 1 &&
|
||||
valueKeyframesAsList.unshift(null);
|
||||
/**
|
||||
* Segments can't express `repeat: Infinity` or very large
|
||||
* counts — they'd leave dead time after the segment or
|
||||
* explode the keyframe array. Ignore with a warning.
|
||||
*/
|
||||
if (repeat) {
|
||||
warning(repeat < MAX_REPEAT, `Sequence segments can't repeat ${repeat} times — ignoring repeat option. Use a value below ${MAX_REPEAT} or apply repeat at the sequence level instead.`);
|
||||
}
|
||||
if (repeat && repeat < MAX_REPEAT) {
|
||||
/**
|
||||
* Express repeatDelay in units of a single iteration's duration
|
||||
* so it can be added to the per-iteration time offsets below
|
||||
* before they're normalized to 0-1.
|
||||
*/
|
||||
const repeatDelayUnits = duration > 0 ? repeatDelay / duration : 0;
|
||||
duration = calculateRepeatDuration(duration, repeat, repeatDelay);
|
||||
const originalKeyframes = [...valueKeyframesAsList];
|
||||
const originalTimes = [...times];
|
||||
ease = Array.isArray(ease) ? [...ease] : [ease];
|
||||
const originalEase = [...ease];
|
||||
/**
|
||||
* For reverse/mirror, alternate iterations play the segment
|
||||
* backwards. mirror matches JSAnimation's mirroredGenerator:
|
||||
* reversed keyframes, easings unchanged. reverse matches
|
||||
* JSAnimation's iterationProgress = 1 - p: reversed
|
||||
* keyframes, easing array reversed AND each function easing
|
||||
* mapped through reverseEasing (string easings unchanged —
|
||||
* they're resolved later by the keyframes engine).
|
||||
*/
|
||||
const isFlipping = repeatType === "reverse" || repeatType === "mirror";
|
||||
let flippedKeyframes = originalKeyframes;
|
||||
let flippedEases = originalEase;
|
||||
if (isFlipping) {
|
||||
flippedKeyframes = [...originalKeyframes].reverse();
|
||||
if (repeatType === "reverse") {
|
||||
flippedEases = [...originalEase]
|
||||
.reverse()
|
||||
.map((e) => typeof e === "function"
|
||||
? reverseEasing(e)
|
||||
: e);
|
||||
}
|
||||
}
|
||||
for (let repeatIndex = 0; repeatIndex < repeat; repeatIndex++) {
|
||||
const isFlipped = isFlipping && repeatIndex % 2 === 0;
|
||||
const iterKeyframes = isFlipped
|
||||
? flippedKeyframes
|
||||
: originalKeyframes;
|
||||
const iterEase = isFlipped ? flippedEases : originalEase;
|
||||
const iterStartOffset = (repeatIndex + 1) * (1 + repeatDelayUnits);
|
||||
/**
|
||||
* If repeatDelay is set, hold the previous iteration's
|
||||
* final value through the delay by inserting a keyframe
|
||||
* at the moment the next iteration begins.
|
||||
*/
|
||||
if (repeatDelayUnits > 0) {
|
||||
valueKeyframesAsList.push(valueKeyframesAsList[valueKeyframesAsList.length - 1]);
|
||||
times.push(iterStartOffset);
|
||||
ease.push("linear");
|
||||
}
|
||||
valueKeyframesAsList.push(...iterKeyframes);
|
||||
for (let keyframeIndex = 0; keyframeIndex < iterKeyframes.length; keyframeIndex++) {
|
||||
times.push(originalTimes[keyframeIndex] + iterStartOffset);
|
||||
ease.push(keyframeIndex === 0
|
||||
? "linear"
|
||||
: getEasingForSegment(iterEase, keyframeIndex - 1));
|
||||
}
|
||||
}
|
||||
normalizeTimes(times, repeat, repeatDelayUnits);
|
||||
}
|
||||
const targetTime = startTime + duration;
|
||||
/**
|
||||
* Add keyframes, mapping offsets to absolute time.
|
||||
*/
|
||||
addKeyframes(valueSequence, valueKeyframesAsList, ease, times, startTime, targetTime);
|
||||
maxDuration = Math.max(calculatedDelay + duration, maxDuration);
|
||||
totalDuration = Math.max(targetTime, totalDuration);
|
||||
};
|
||||
if (isMotionValue(subject)) {
|
||||
const subjectSequence = getSubjectSequence(subject, sequences);
|
||||
resolveValueSequence(keyframes, transition, getValueSequence("default", subjectSequence));
|
||||
}
|
||||
else {
|
||||
const subjects = resolveSubjects(subject, keyframes, scope, elementCache);
|
||||
const numSubjects = subjects.length;
|
||||
/**
|
||||
* For every element in this segment, process the defined values.
|
||||
*/
|
||||
for (let subjectIndex = 0; subjectIndex < numSubjects; subjectIndex++) {
|
||||
/**
|
||||
* Cast necessary, but we know these are of this type
|
||||
*/
|
||||
keyframes = keyframes;
|
||||
transition = transition;
|
||||
const thisSubject = subjects[subjectIndex];
|
||||
const subjectSequence = getSubjectSequence(thisSubject, sequences);
|
||||
for (const key in keyframes) {
|
||||
resolveValueSequence(keyframes[key], getValueTransition(transition, key), getValueSequence(key, subjectSequence), subjectIndex, numSubjects);
|
||||
}
|
||||
}
|
||||
}
|
||||
prevTime = currentTime;
|
||||
currentTime += maxDuration;
|
||||
}
|
||||
/**
|
||||
* For every element and value combination create a new animation.
|
||||
*/
|
||||
sequences.forEach((valueSequences, element) => {
|
||||
for (const key in valueSequences) {
|
||||
const valueSequence = valueSequences[key];
|
||||
/**
|
||||
* Arrange all the keyframes in ascending time order.
|
||||
*/
|
||||
valueSequence.sort(compareByTime);
|
||||
const keyframes = [];
|
||||
const valueOffset = [];
|
||||
const valueEasing = [];
|
||||
/**
|
||||
* For each keyframe, translate absolute times into
|
||||
* relative offsets based on the total duration of the timeline.
|
||||
*/
|
||||
for (let i = 0; i < valueSequence.length; i++) {
|
||||
const { at, value, easing } = valueSequence[i];
|
||||
keyframes.push(value);
|
||||
valueOffset.push(progress(0, totalDuration, at));
|
||||
valueEasing.push(easing || "easeOut");
|
||||
}
|
||||
/**
|
||||
* If the first keyframe doesn't land on offset: 0
|
||||
* provide one by duplicating the initial keyframe. This ensures
|
||||
* it snaps to the first keyframe when the animation starts.
|
||||
*/
|
||||
if (valueOffset[0] !== 0) {
|
||||
valueOffset.unshift(0);
|
||||
keyframes.unshift(keyframes[0]);
|
||||
valueEasing.unshift(defaultSegmentEasing);
|
||||
}
|
||||
/**
|
||||
* If the last keyframe doesn't land on offset: 1
|
||||
* provide one with a null wildcard value. This will ensure it
|
||||
* stays static until the end of the animation.
|
||||
*/
|
||||
if (valueOffset[valueOffset.length - 1] !== 1) {
|
||||
valueOffset.push(1);
|
||||
keyframes.push(null);
|
||||
}
|
||||
if (!animationDefinitions.has(element)) {
|
||||
animationDefinitions.set(element, {
|
||||
keyframes: {},
|
||||
transition: {},
|
||||
});
|
||||
}
|
||||
const definition = animationDefinitions.get(element);
|
||||
definition.keyframes[key] = keyframes;
|
||||
/**
|
||||
* Exclude `type` from defaultTransition since springs have been
|
||||
* converted to duration-based easing functions in resolveValueSequence.
|
||||
* Including `type: "spring"` would cause JSAnimation to error when
|
||||
* the merged keyframes array has more than 2 keyframes.
|
||||
*/
|
||||
const { type: _type, ...remainingDefaultTransition } = defaultTransition;
|
||||
definition.transition[key] = {
|
||||
...remainingDefaultTransition,
|
||||
duration: totalDuration,
|
||||
ease: valueEasing,
|
||||
times: valueOffset,
|
||||
...sequenceTransition,
|
||||
};
|
||||
}
|
||||
});
|
||||
return animationDefinitions;
|
||||
}
|
||||
function getSubjectSequence(subject, sequences) {
|
||||
!sequences.has(subject) && sequences.set(subject, {});
|
||||
return sequences.get(subject);
|
||||
}
|
||||
function getValueSequence(name, sequences) {
|
||||
if (!sequences[name])
|
||||
sequences[name] = [];
|
||||
return sequences[name];
|
||||
}
|
||||
function keyframesAsList(keyframes) {
|
||||
return Array.isArray(keyframes) ? keyframes : [keyframes];
|
||||
}
|
||||
function getValueTransition(transition, key) {
|
||||
return transition && transition[key]
|
||||
? {
|
||||
...transition,
|
||||
...transition[key],
|
||||
}
|
||||
: { ...transition };
|
||||
}
|
||||
const isNumber = (keyframe) => typeof keyframe === "number";
|
||||
const isNumberKeyframesArray = (keyframes) => keyframes.every(isNumber);
|
||||
|
||||
export { createAnimationsFromSequence, getValueTransition };
|
||||
//# sourceMappingURL=create.mjs.map
|
||||
Generated
Vendored
+6
@@ -0,0 +1,6 @@
|
||||
function calculateRepeatDuration(duration, repeat, repeatDelay) {
|
||||
return duration * (repeat + 1) + repeatDelay * repeat;
|
||||
}
|
||||
|
||||
export { calculateRepeatDuration };
|
||||
//# sourceMappingURL=calc-repeat-duration.mjs.map
|
||||
Generated
Vendored
+24
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* Given a absolute or relative time definition and current/prev time state of the sequence,
|
||||
* calculate an absolute time for the next keyframes.
|
||||
*/
|
||||
function calcNextTime(current, next, prev, labels) {
|
||||
if (typeof next === "number") {
|
||||
return next;
|
||||
}
|
||||
else if (next.startsWith("-") || next.startsWith("+")) {
|
||||
return Math.max(0, current + parseFloat(next));
|
||||
}
|
||||
else if (next === "<") {
|
||||
return prev;
|
||||
}
|
||||
else if (next.startsWith("<")) {
|
||||
return Math.max(0, prev + parseFloat(next.slice(1)));
|
||||
}
|
||||
else {
|
||||
return labels.get(next) ?? current;
|
||||
}
|
||||
}
|
||||
|
||||
export { calcNextTime };
|
||||
//# sourceMappingURL=calc-time.mjs.map
|
||||
Generated
Vendored
+31
@@ -0,0 +1,31 @@
|
||||
import { mixNumber } from 'motion-dom';
|
||||
import { getEasingForSegment, removeItem } from 'motion-utils';
|
||||
|
||||
function eraseKeyframes(sequence, startTime, endTime) {
|
||||
for (let i = 0; i < sequence.length; i++) {
|
||||
const keyframe = sequence[i];
|
||||
if (keyframe.at > startTime && keyframe.at < endTime) {
|
||||
removeItem(sequence, keyframe);
|
||||
// If we remove this item we have to push the pointer back one
|
||||
i--;
|
||||
}
|
||||
}
|
||||
}
|
||||
function addKeyframes(sequence, keyframes, easing, offset, startTime, endTime) {
|
||||
/**
|
||||
* Erase every existing value between currentTime and targetTime,
|
||||
* this will essentially splice this timeline into any currently
|
||||
* defined ones.
|
||||
*/
|
||||
eraseKeyframes(sequence, startTime, endTime);
|
||||
for (let i = 0; i < keyframes.length; i++) {
|
||||
sequence.push({
|
||||
value: keyframes[i],
|
||||
at: mixNumber(startTime, endTime, offset[i]),
|
||||
easing: getEasingForSegment(easing, i),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export { addKeyframes, eraseKeyframes };
|
||||
//# sourceMappingURL=edit.mjs.map
|
||||
Generated
Vendored
+18
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* Take an array of times that represent repeated keyframes. For instance
|
||||
* if we have original times of [0, 0.5, 1] then our repeated times will
|
||||
* be [0, 0.5, 1, 1, 1.5, 2]. Loop over the times and scale them back
|
||||
* down to a 0-1 scale.
|
||||
*
|
||||
* `repeatDelayUnits` is the repeatDelay expressed in units of a single
|
||||
* iteration's duration, so the total span equals `(repeat + 1) + repeat * repeatDelayUnits`.
|
||||
*/
|
||||
function normalizeTimes(times, repeat, repeatDelayUnits = 0) {
|
||||
const totalUnits = repeat + 1 + repeat * repeatDelayUnits;
|
||||
for (let i = 0; i < times.length; i++) {
|
||||
times[i] = times[i] / totalUnits;
|
||||
}
|
||||
}
|
||||
|
||||
export { normalizeTimes };
|
||||
//# sourceMappingURL=normalize-times.mjs.map
|
||||
Generated
Vendored
+15
@@ -0,0 +1,15 @@
|
||||
function compareByTime(a, b) {
|
||||
if (a.at === b.at) {
|
||||
if (a.value === null)
|
||||
return 1;
|
||||
if (b.value === null)
|
||||
return -1;
|
||||
return 0;
|
||||
}
|
||||
else {
|
||||
return a.at - b.at;
|
||||
}
|
||||
}
|
||||
|
||||
export { compareByTime };
|
||||
//# sourceMappingURL=sort.mjs.map
|
||||
Generated
Vendored
+6
@@ -0,0 +1,6 @@
|
||||
function isDOMKeyframes(keyframes) {
|
||||
return typeof keyframes === "object" && !Array.isArray(keyframes);
|
||||
}
|
||||
|
||||
export { isDOMKeyframes };
|
||||
//# sourceMappingURL=is-dom-keyframes.mjs.map
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
export * from 'motion-dom';
|
||||
export { delayInSeconds as delay } from 'motion-dom';
|
||||
export * from 'motion-utils';
|
||||
export { animate, createScopedAnimate } from './animation/animate/index.mjs';
|
||||
export { animateMini } from './animation/animators/waapi/animate-style.mjs';
|
||||
export { scroll } from './render/dom/scroll/index.mjs';
|
||||
export { scrollInfo } from './render/dom/scroll/track.mjs';
|
||||
export { inView } from './render/dom/viewport/index.mjs';
|
||||
export { distance, distance2D } from './utils/distance.mjs';
|
||||
//# sourceMappingURL=dom.mjs.map
|
||||
Generated
Vendored
+102
@@ -0,0 +1,102 @@
|
||||
import { resize } from 'motion-dom';
|
||||
import { scrollInfo } from './track.mjs';
|
||||
import { canUseNativeTimeline } from './utils/can-use-native-timeline.mjs';
|
||||
import { getTimeline } from './utils/get-timeline.mjs';
|
||||
import { offsetToViewTimelineRange } from './utils/offset-to-range.mjs';
|
||||
|
||||
function attachToAnimation(animation, options) {
|
||||
const { target, container, axis } = options;
|
||||
const range = target && offsetToViewTimelineRange(options.offset);
|
||||
/**
|
||||
* Use native timeline when:
|
||||
* - No target and no offset: ScrollTimeline
|
||||
* - Target with mappable offset: ViewTimeline with named range
|
||||
* - Otherwise: fall back to JS observe. A ScrollTimeline has no offset,
|
||||
* so page offsets are applied by the JS timeline.
|
||||
*/
|
||||
const native = canUseNativeTimeline(target) && (target ? !!range : !options.offset);
|
||||
const animations = new Map();
|
||||
const observed = new Set();
|
||||
let stopObserving;
|
||||
let reverse = false;
|
||||
/**
|
||||
* When an offset's second point comes first, its range is swapped and
|
||||
* the animation played backwards. The range is set after the direction,
|
||||
* as that's what realigns a running animation with its timeline. Chrome
|
||||
* skips that realignment when the range changes in the frame the
|
||||
* animation starts, leaving it offset for good, so it's replayed too.
|
||||
*/
|
||||
const apply = (direction, waapi) => {
|
||||
const [start, end] = range.points;
|
||||
waapi.effect.updateTiming({
|
||||
direction: reverse
|
||||
? direction === "normal"
|
||||
? "reverse"
|
||||
: "alternate-reverse"
|
||||
: direction,
|
||||
});
|
||||
Object.assign(waapi, {
|
||||
rangeStart: reverse ? end : start,
|
||||
rangeEnd: reverse ? start : end,
|
||||
});
|
||||
waapi.play();
|
||||
};
|
||||
const stops = [
|
||||
animation.attachTimeline({
|
||||
/**
|
||||
* Read only by WAAPI animations, so values driven from JS don't
|
||||
* create a native timeline they won't use.
|
||||
*/
|
||||
get timeline() {
|
||||
return native ? getTimeline(options) : undefined;
|
||||
},
|
||||
onAttach: range &&
|
||||
((waapi) => {
|
||||
const direction = waapi.effect.getTiming().direction;
|
||||
animations.set(waapi, direction);
|
||||
apply(direction, waapi);
|
||||
}),
|
||||
/**
|
||||
* Values driven from JS all track the offset with scrollInfo,
|
||||
* which measures once per frame for all of them.
|
||||
*/
|
||||
observe: (valueAnimation) => {
|
||||
valueAnimation.pause();
|
||||
observed.add(valueAnimation);
|
||||
stopObserving || (stopObserving = scrollInfo((info) => {
|
||||
observed.forEach((observedAnimation) => {
|
||||
observedAnimation.time =
|
||||
observedAnimation.iterationDuration *
|
||||
info[axis].progress;
|
||||
});
|
||||
}, options));
|
||||
return () => observed.delete(valueAnimation);
|
||||
},
|
||||
}),
|
||||
];
|
||||
if (native && range) {
|
||||
const { a, b } = range;
|
||||
const length = axis === "y" ? "clientHeight" : "clientWidth";
|
||||
/**
|
||||
* Offsets like All only run forwards when the target is longer than
|
||||
* the container, so their direction is remeasured on resize.
|
||||
*/
|
||||
const update = () => {
|
||||
if (reverse !==
|
||||
(reverse = a * target[length] + b * container[length] < 0)) {
|
||||
animations.forEach(apply);
|
||||
}
|
||||
};
|
||||
update();
|
||||
if (a * b < 0) {
|
||||
stops.push(resize(update), resize([target, container], update));
|
||||
}
|
||||
}
|
||||
return () => {
|
||||
stops.forEach((stop) => stop());
|
||||
stopObserving?.();
|
||||
};
|
||||
}
|
||||
|
||||
export { attachToAnimation };
|
||||
//# sourceMappingURL=attach-animation.mjs.map
|
||||
Generated
Vendored
+19
@@ -0,0 +1,19 @@
|
||||
import { noop } from 'motion-utils';
|
||||
import { attachToAnimation } from './attach-animation.mjs';
|
||||
import { scrollInfo } from './track.mjs';
|
||||
|
||||
function scroll(onScroll, { axis = "y", source, container = document.scrollingElement, ...options } = {}) {
|
||||
if (!container)
|
||||
return noop;
|
||||
const optionsWithDefaults = {
|
||||
axis,
|
||||
container: source || container,
|
||||
...options,
|
||||
};
|
||||
return typeof onScroll === "function"
|
||||
? scrollInfo((info) => onScroll(info[axis].progress, info), optionsWithDefaults)
|
||||
: attachToAnimation(onScroll, optionsWithDefaults);
|
||||
}
|
||||
|
||||
export { scroll };
|
||||
//# sourceMappingURL=index.mjs.map
|
||||
Generated
Vendored
+102
@@ -0,0 +1,102 @@
|
||||
import { clamp, progress, velocityPerSecond } from 'motion-utils';
|
||||
|
||||
/**
|
||||
* A time in milliseconds, beyond which we consider the scroll velocity to be 0.
|
||||
*/
|
||||
const maxElapsed = 50;
|
||||
const createAxisInfo = () => ({
|
||||
current: 0,
|
||||
offset: [],
|
||||
progress: 0,
|
||||
scrollLength: 0,
|
||||
targetOffset: 0,
|
||||
targetLength: 0,
|
||||
containerLength: 0,
|
||||
velocity: 0,
|
||||
});
|
||||
const createScrollInfo = () => ({
|
||||
time: 0,
|
||||
x: createAxisInfo(),
|
||||
y: createAxisInfo(),
|
||||
});
|
||||
/**
|
||||
* Also iterated with for...in as the list of axes.
|
||||
*/
|
||||
const axisKeys = {
|
||||
x: {
|
||||
length: "Width",
|
||||
position: "Left",
|
||||
},
|
||||
y: {
|
||||
length: "Height",
|
||||
position: "Top",
|
||||
},
|
||||
};
|
||||
const styles = new WeakMap();
|
||||
/**
|
||||
* Whether an axis scrolls from its end, which browsers report as a scroll
|
||||
* position running from 0 to -scrollLength (#3340). This is read from style
|
||||
* rather than the sign of the scroll position, as elastic overscroll pushes
|
||||
* the position past 0 in either kind of axis (#3791).
|
||||
*/
|
||||
function isReversed(element, axisName) {
|
||||
/**
|
||||
* The viewport takes its writing mode and direction from <body>, which
|
||||
* not every browser reflects in the root element's computed style. It
|
||||
* doesn't adopt the root element's flex layout.
|
||||
*/
|
||||
const isRoot = element === document.scrollingElement;
|
||||
const source = (isRoot && document.body) || element;
|
||||
let style = styles.get(source);
|
||||
if (!style)
|
||||
styles.set(source, (style = getComputedStyle(source)));
|
||||
const { writingMode, flexDirection } = style;
|
||||
const isInline = (axisName === "x") !== /^[vs]/.test(writingMode);
|
||||
return ((isInline
|
||||
? (style.direction === "rtl") !== (writingMode === "sideways-lr")
|
||||
: writingMode.endsWith("rl")) !==
|
||||
/**
|
||||
* A reversed flex axis flips this. The main axis is the inline axis
|
||||
* unless flex-direction is a column.
|
||||
*/
|
||||
(!isRoot &&
|
||||
style.display.includes("flex") &&
|
||||
(isInline !== (flexDirection[0] === "c")
|
||||
? flexDirection
|
||||
: style.flexWrap).endsWith("reverse")));
|
||||
}
|
||||
function updateAxisInfo(element, axisName, info, time) {
|
||||
const axis = info[axisName];
|
||||
const { length, position } = axisKeys[axisName];
|
||||
const prev = axis.current;
|
||||
const prevTime = info.time;
|
||||
const scrollPosition = element[`scroll${position}`];
|
||||
axis.current = isReversed(element, axisName)
|
||||
? -scrollPosition || 0 // Avoid -0
|
||||
: scrollPosition;
|
||||
axis.containerLength = element[`client${length}`];
|
||||
axis.targetLength = element[`scroll${length}`];
|
||||
axis.scrollLength = axis.targetLength - axis.containerLength;
|
||||
axis.offset.length = 0;
|
||||
axis.offset[0] = 0;
|
||||
axis.offset[1] = axis.scrollLength;
|
||||
axis.progress = clamp(0, 1, progress(0, axis.scrollLength, axis.current));
|
||||
const elapsed = time - prevTime;
|
||||
axis.velocity =
|
||||
elapsed > maxElapsed
|
||||
? 0
|
||||
: velocityPerSecond(axis.current - prev, elapsed);
|
||||
}
|
||||
/**
|
||||
* Measures a scroll container. Runs once per container per frame; every
|
||||
* handler on that container derives its info from the result.
|
||||
*/
|
||||
function updateScrollInfo(element, info, time) {
|
||||
for (const axis in axisKeys) {
|
||||
updateAxisInfo(element, axis, info, time);
|
||||
}
|
||||
info.time = time;
|
||||
}
|
||||
|
||||
export { axisKeys, createScrollInfo, updateScrollInfo };
|
||||
//# sourceMappingURL=info.mjs.map
|
||||
Generated
Vendored
+46
@@ -0,0 +1,46 @@
|
||||
const namedEdges = {
|
||||
start: 0,
|
||||
center: 0.5,
|
||||
end: 1,
|
||||
};
|
||||
function resolveEdge(edge, length, inset = 0) {
|
||||
let delta = 0;
|
||||
/**
|
||||
* If we have this edge defined as a preset, replace the definition
|
||||
* with the numerical value.
|
||||
*/
|
||||
if (edge in namedEdges) {
|
||||
edge = namedEdges[edge];
|
||||
}
|
||||
/**
|
||||
* Handle unit values
|
||||
*/
|
||||
if (typeof edge === "string") {
|
||||
const asNumber = parseFloat(edge);
|
||||
if (edge.endsWith("px")) {
|
||||
delta = asNumber;
|
||||
}
|
||||
else if (edge.endsWith("%")) {
|
||||
edge = asNumber / 100;
|
||||
}
|
||||
else if (edge.endsWith("vw")) {
|
||||
delta = (asNumber / 100) * document.documentElement.clientWidth;
|
||||
}
|
||||
else if (edge.endsWith("vh")) {
|
||||
delta = (asNumber / 100) * document.documentElement.clientHeight;
|
||||
}
|
||||
else {
|
||||
edge = asNumber;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* If the edge is defined as a number, handle as a progress value.
|
||||
*/
|
||||
if (typeof edge === "number") {
|
||||
delta = length * edge;
|
||||
}
|
||||
return inset + delta;
|
||||
}
|
||||
|
||||
export { namedEdges, resolveEdge };
|
||||
//# sourceMappingURL=edge.mjs.map
|
||||
Generated
Vendored
+46
@@ -0,0 +1,46 @@
|
||||
import { clamp, progress } from 'motion-utils';
|
||||
import { resolveOffset } from './offset.mjs';
|
||||
import { ScrollOffset } from './presets.mjs';
|
||||
|
||||
/**
|
||||
* Resolved offsets map to evenly spaced progress values, so progress is
|
||||
* derived from the segment index rather than building an interpolator.
|
||||
*/
|
||||
function offsetsToProgress(offsets, v) {
|
||||
const n = offsets.length - 1;
|
||||
if (n < 1)
|
||||
return 0;
|
||||
const reverse = offsets[0] > offsets[n];
|
||||
const at = (i) => offsets[reverse ? n - i : i];
|
||||
/**
|
||||
* Matches interpolate(), which checks for a zero-length first range
|
||||
* before reversing descending offsets.
|
||||
*/
|
||||
if (offsets[0] === offsets[1] && v < at(0))
|
||||
return reverse ? 1 : 0;
|
||||
let i = 0;
|
||||
while (i < n - 1 && v >= at(i + 1))
|
||||
i++;
|
||||
const p = (i + progress(at(i), at(i + 1), v)) / n;
|
||||
return reverse ? 1 - p : p;
|
||||
}
|
||||
function resolveOffsets(info, options) {
|
||||
const { offset: offsetDefinition = ScrollOffset.All, axis = "y" } = options;
|
||||
const axisInfo = info[axis];
|
||||
/**
|
||||
* Reset the length of the resolved offset array rather than creating a new one.
|
||||
*/
|
||||
axisInfo.offset.length = 0;
|
||||
/**
|
||||
* Populate the offset array by resolving the user's offset definition into
|
||||
* a list of pixel scroll offsets.
|
||||
*/
|
||||
const numOffsets = offsetDefinition.length;
|
||||
for (let i = 0; i < numOffsets; i++) {
|
||||
axisInfo.offset[i] = resolveOffset(offsetDefinition[i], axisInfo.containerLength, axisInfo.targetLength, axisInfo.targetOffset);
|
||||
}
|
||||
axisInfo.progress = clamp(0, 1, offsetsToProgress(axisInfo.offset, axisInfo.current));
|
||||
}
|
||||
|
||||
export { resolveOffsets };
|
||||
//# sourceMappingURL=index.mjs.map
|
||||
Generated
Vendored
+48
@@ -0,0 +1,48 @@
|
||||
import { isHTMLElement } from 'motion-dom';
|
||||
|
||||
function calcInset(element, container) {
|
||||
const inset = { x: 0, y: 0 };
|
||||
let current = element;
|
||||
while (current && current !== container) {
|
||||
if (isHTMLElement(current)) {
|
||||
inset.x += current.offsetLeft;
|
||||
inset.y += current.offsetTop;
|
||||
current = current.offsetParent;
|
||||
}
|
||||
else if (current.tagName === "svg") {
|
||||
/**
|
||||
* This isn't an ideal approach to measuring the offset of <svg /> tags.
|
||||
* It would be preferable, given they behave like HTMLElements in most ways
|
||||
* to use offsetLeft/Top. But these don't exist on <svg />. Likewise we
|
||||
* can't use .getBBox() like most SVG elements as these provide the offset
|
||||
* relative to the SVG itself, which for <svg /> is usually 0x0.
|
||||
*/
|
||||
const svgBoundingBox = current.getBoundingClientRect();
|
||||
current = current.parentElement;
|
||||
const parentBoundingBox = current.getBoundingClientRect();
|
||||
inset.x += svgBoundingBox.left - parentBoundingBox.left;
|
||||
inset.y += svgBoundingBox.top - parentBoundingBox.top;
|
||||
}
|
||||
else if (current instanceof SVGGraphicsElement) {
|
||||
const { x, y } = current.getBBox();
|
||||
inset.x += x;
|
||||
inset.y += y;
|
||||
let svg = null;
|
||||
let parent = current.parentNode;
|
||||
while (!svg) {
|
||||
if (parent.tagName === "svg") {
|
||||
svg = parent;
|
||||
}
|
||||
parent = current.parentNode;
|
||||
}
|
||||
current = svg;
|
||||
}
|
||||
else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return inset;
|
||||
}
|
||||
|
||||
export { calcInset };
|
||||
//# sourceMappingURL=inset.mjs.map
|
||||
Generated
Vendored
+36
@@ -0,0 +1,36 @@
|
||||
import { resolveEdge, namedEdges } from './edge.mjs';
|
||||
|
||||
const defaultOffset = [0, 0];
|
||||
function resolveOffset(offset, containerLength, targetLength, targetInset) {
|
||||
let offsetDefinition = Array.isArray(offset) ? offset : defaultOffset;
|
||||
let targetPoint = 0;
|
||||
let containerPoint = 0;
|
||||
if (typeof offset === "number") {
|
||||
/**
|
||||
* If we're provided offset: [0, 0.5, 1] then each number x should become
|
||||
* [x, x], so we default to the behaviour of mapping 0 => 0 of both target
|
||||
* and container etc.
|
||||
*/
|
||||
offsetDefinition = [offset, offset];
|
||||
}
|
||||
else if (typeof offset === "string") {
|
||||
offset = offset.trim();
|
||||
if (offset.includes(" ")) {
|
||||
offsetDefinition = offset.split(" ");
|
||||
}
|
||||
else {
|
||||
/**
|
||||
* If we're provided a definition like "100px" then we want to apply
|
||||
* that only to the top of the target point, leaving the container at 0.
|
||||
* Whereas a named offset like "end" should be applied to both.
|
||||
*/
|
||||
offsetDefinition = [offset, namedEdges[offset] ? offset : `0`];
|
||||
}
|
||||
}
|
||||
targetPoint = resolveEdge(offsetDefinition[0], targetLength, targetInset);
|
||||
containerPoint = resolveEdge(offsetDefinition[1], containerLength);
|
||||
return targetPoint - containerPoint;
|
||||
}
|
||||
|
||||
export { resolveOffset };
|
||||
//# sourceMappingURL=offset.mjs.map
|
||||
Generated
Vendored
+9
@@ -0,0 +1,9 @@
|
||||
const ScrollOffset = {
|
||||
All: [
|
||||
[0, 0],
|
||||
[1, 1],
|
||||
],
|
||||
};
|
||||
|
||||
export { ScrollOffset };
|
||||
//# sourceMappingURL=presets.mjs.map
|
||||
Generated
Vendored
+58
@@ -0,0 +1,58 @@
|
||||
import { warnOnce } from 'motion-utils';
|
||||
import { axisKeys } from './info.mjs';
|
||||
import { resolveOffsets } from './offsets/index.mjs';
|
||||
import { calcInset } from './offsets/inset.mjs';
|
||||
|
||||
function getTargetSize(target) {
|
||||
return "getBBox" in target && target.tagName !== "svg"
|
||||
? target.getBBox()
|
||||
: { width: target.clientWidth, height: target.clientHeight };
|
||||
}
|
||||
function createOnScrollHandler(container, onScroll, info, options = {}) {
|
||||
const { target } = options;
|
||||
/**
|
||||
* In development mode ensure scroll containers aren't position: static as this makes
|
||||
* it difficult to measure their relative positions. The document scrolling element
|
||||
* is exempt: offsetParent measurements naturally resolve relative to the document.
|
||||
*/
|
||||
if (process.env.NODE_ENV !== "production") {
|
||||
if (target &&
|
||||
target !== container &&
|
||||
container !== document.documentElement &&
|
||||
container !== document.scrollingElement &&
|
||||
container !== document.body) {
|
||||
warnOnce(getComputedStyle(container).position !== "static", "Please ensure that the container has a non-static position, like 'relative', 'fixed', or 'absolute' to ensure scroll offset is calculated correctly.");
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Handlers without a target or offset are notified with the container's
|
||||
* shared info object, so they measure nothing themselves.
|
||||
*/
|
||||
const needsOwnInfo = target || options.offset;
|
||||
return {
|
||||
measure: (containerInfo) => {
|
||||
if (!needsOwnInfo)
|
||||
return;
|
||||
info.time = containerInfo.time;
|
||||
for (const key in axisKeys) {
|
||||
const axis = key;
|
||||
const { offset } = info[axis];
|
||||
Object.assign(info[axis], containerInfo[axis]);
|
||||
info[axis].offset = offset;
|
||||
}
|
||||
if (target && target !== container) {
|
||||
const inset = calcInset(target, container);
|
||||
const size = getTargetSize(target);
|
||||
info.x.targetOffset = inset.x;
|
||||
info.y.targetOffset = inset.y;
|
||||
info.x.targetLength = size.width;
|
||||
info.y.targetLength = size.height;
|
||||
}
|
||||
resolveOffsets(info, options);
|
||||
},
|
||||
notify: (containerInfo) => onScroll(needsOwnInfo ? info : containerInfo),
|
||||
};
|
||||
}
|
||||
|
||||
export { createOnScrollHandler };
|
||||
//# sourceMappingURL=on-scroll-handler.mjs.map
|
||||
Generated
Vendored
+117
@@ -0,0 +1,117 @@
|
||||
import { resize, frame, cancelFrame, frameData } from 'motion-dom';
|
||||
import { noop } from 'motion-utils';
|
||||
import { createScrollInfo, updateScrollInfo } from './info.mjs';
|
||||
import { createOnScrollHandler } from './on-scroll-handler.mjs';
|
||||
|
||||
const scrollListeners = new WeakMap();
|
||||
const resizeListeners = new WeakMap();
|
||||
const onScrollHandlers = new WeakMap();
|
||||
const dimensionCheckProcesses = new WeakMap();
|
||||
const getEventTarget = (element) => element === document.scrollingElement ? window : element;
|
||||
function scrollInfo(onScroll, { container = document.scrollingElement, trackContentSize = false, ...options } = {}) {
|
||||
if (!container)
|
||||
return noop;
|
||||
let containerHandlers = onScrollHandlers.get(container);
|
||||
/**
|
||||
* Get the onScroll handlers for this container.
|
||||
* If one isn't found, create a new one.
|
||||
*/
|
||||
if (!containerHandlers) {
|
||||
containerHandlers = new Set();
|
||||
onScrollHandlers.set(container, containerHandlers);
|
||||
}
|
||||
/**
|
||||
* Create a new onScroll handler for the provided callback.
|
||||
*/
|
||||
const info = createScrollInfo();
|
||||
const containerHandler = createOnScrollHandler(container, onScroll, info, options);
|
||||
containerHandlers.add(containerHandler);
|
||||
/**
|
||||
* Check if there's a scroll event listener for this container.
|
||||
* If not, create one.
|
||||
*/
|
||||
if (!scrollListeners.has(container)) {
|
||||
const containerInfo = createScrollInfo();
|
||||
const measureAll = () => {
|
||||
updateScrollInfo(container, containerInfo, frameData.timestamp);
|
||||
for (const handler of containerHandlers) {
|
||||
handler.measure(containerInfo);
|
||||
}
|
||||
frame.preUpdate(notifyAll);
|
||||
};
|
||||
const notifyAll = () => {
|
||||
for (const handler of containerHandlers) {
|
||||
handler.notify(containerInfo);
|
||||
}
|
||||
};
|
||||
const listener = () => frame.read(measureAll);
|
||||
scrollListeners.set(container, listener);
|
||||
const target = getEventTarget(container);
|
||||
window.addEventListener("resize", listener);
|
||||
if (container !== document.documentElement) {
|
||||
resizeListeners.set(container, resize(container, listener));
|
||||
}
|
||||
target.addEventListener("scroll", listener);
|
||||
listener();
|
||||
}
|
||||
/**
|
||||
* Enable content size tracking if requested and not already enabled.
|
||||
*/
|
||||
if (trackContentSize && !dimensionCheckProcesses.has(container)) {
|
||||
const listener = scrollListeners.get(container);
|
||||
// Store initial scroll dimensions (object is reused to avoid allocation)
|
||||
const size = {
|
||||
width: container.scrollWidth,
|
||||
height: container.scrollHeight,
|
||||
};
|
||||
// Add frame-based scroll dimension checking to detect content changes
|
||||
const checkScrollDimensions = () => {
|
||||
const newWidth = container.scrollWidth;
|
||||
const newHeight = container.scrollHeight;
|
||||
if (size.width !== newWidth || size.height !== newHeight) {
|
||||
listener();
|
||||
size.width = newWidth;
|
||||
size.height = newHeight;
|
||||
}
|
||||
};
|
||||
// Schedule with keepAlive=true to run every frame
|
||||
const dimensionCheckProcess = frame.read(checkScrollDimensions, true);
|
||||
dimensionCheckProcesses.set(container, dimensionCheckProcess);
|
||||
}
|
||||
const listener = scrollListeners.get(container);
|
||||
frame.read(listener, false, true);
|
||||
return () => {
|
||||
cancelFrame(listener);
|
||||
/**
|
||||
* Check if we even have any handlers for this container.
|
||||
*/
|
||||
const currentHandlers = onScrollHandlers.get(container);
|
||||
if (!currentHandlers)
|
||||
return;
|
||||
currentHandlers.delete(containerHandler);
|
||||
if (currentHandlers.size)
|
||||
return;
|
||||
/**
|
||||
* If no more handlers, remove the scroll listener too. The handler
|
||||
* set goes with it, as a measure still queued from this listener
|
||||
* would otherwise notify handlers added by a later scrollInfo call.
|
||||
*/
|
||||
onScrollHandlers.delete(container);
|
||||
const scrollListener = scrollListeners.get(container);
|
||||
scrollListeners.delete(container);
|
||||
if (scrollListener) {
|
||||
getEventTarget(container).removeEventListener("scroll", scrollListener);
|
||||
resizeListeners.get(container)?.();
|
||||
window.removeEventListener("resize", scrollListener);
|
||||
}
|
||||
// Clean up scroll dimension checking
|
||||
const dimensionCheckProcess = dimensionCheckProcesses.get(container);
|
||||
if (dimensionCheckProcess) {
|
||||
cancelFrame(dimensionCheckProcess);
|
||||
dimensionCheckProcesses.delete(container);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export { scrollInfo };
|
||||
//# sourceMappingURL=track.mjs.map
|
||||
Generated
Vendored
+10
@@ -0,0 +1,10 @@
|
||||
import { supportsViewTimeline, supportsScrollTimeline } from 'motion-dom';
|
||||
|
||||
function canUseNativeTimeline(target) {
|
||||
if (typeof window === "undefined")
|
||||
return false;
|
||||
return target ? supportsViewTimeline() : supportsScrollTimeline();
|
||||
}
|
||||
|
||||
export { canUseNativeTimeline };
|
||||
//# sourceMappingURL=can-use-native-timeline.mjs.map
|
||||
Generated
Vendored
+25
@@ -0,0 +1,25 @@
|
||||
const timelineCache = new Map();
|
||||
/**
|
||||
* Native timelines are only attached to WAAPI animations. Offsets are
|
||||
* applied to each animation as a range, so a timeline is shared by every
|
||||
* offset.
|
||||
*/
|
||||
function getTimeline({ container, target, axis, }) {
|
||||
let containerCache = timelineCache.get(container);
|
||||
if (!containerCache) {
|
||||
containerCache = new Map();
|
||||
timelineCache.set(container, containerCache);
|
||||
}
|
||||
const targetKey = target ?? "self";
|
||||
let targetCache = containerCache.get(targetKey);
|
||||
if (!targetCache) {
|
||||
targetCache = {};
|
||||
containerCache.set(targetKey, targetCache);
|
||||
}
|
||||
return (targetCache[axis] || (targetCache[axis] = target
|
||||
? new ViewTimeline({ subject: target, axis })
|
||||
: new ScrollTimeline({ source: container, axis })));
|
||||
}
|
||||
|
||||
export { getTimeline };
|
||||
//# sourceMappingURL=get-timeline.mjs.map
|
||||
Generated
Vendored
+41
@@ -0,0 +1,41 @@
|
||||
import { resolveOffset } from '../offsets/offset.mjs';
|
||||
import { ScrollOffset } from '../offsets/presets.mjs';
|
||||
|
||||
/**
|
||||
* Resolved offsets are linear in the target and container lengths, so
|
||||
* probing them gives [target progress, container progress, pixels].
|
||||
* vw/vh can resolve to 0px, so they're rejected up front.
|
||||
*/
|
||||
const toIntersection = (o) => {
|
||||
if (/v/u.test(o))
|
||||
return [];
|
||||
const px = resolveOffset(o, 0, 0, 0);
|
||||
return [resolveOffset(o, 0, 1, 0) - px, px - resolveOffset(o, 1, 0, 0), px];
|
||||
};
|
||||
const toRange = ([t, c, px]) => !px &&
|
||||
(c === 0 || c === 1) &&
|
||||
`${c ? "entry" : "exit"}-crossing ${t * 100}%`;
|
||||
/**
|
||||
* Maps an offset to an equivalent ViewTimeline range. Returns undefined when
|
||||
* there isn't one, which signals the caller to fall back to JS-based scroll
|
||||
* tracking.
|
||||
*/
|
||||
function offsetToViewTimelineRange(offset = ScrollOffset.All) {
|
||||
if (offset.length !== 2)
|
||||
return;
|
||||
const [start, end] = offset.map(toIntersection);
|
||||
const points = [toRange(start), toRange(end)];
|
||||
const a = end[0] - start[0];
|
||||
const b = start[1] - end[1];
|
||||
if (points[0] && points[1] && (a || b)) {
|
||||
return {
|
||||
points: points,
|
||||
a,
|
||||
b,
|
||||
cover: !start[0] && a === 1 && b === 1,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export { offsetToViewTimelineRange };
|
||||
//# sourceMappingURL=offset-to-range.mjs.map
|
||||
Generated
Vendored
+44
@@ -0,0 +1,44 @@
|
||||
import { resolveElements } from 'motion-dom';
|
||||
|
||||
const thresholds = {
|
||||
some: 0,
|
||||
all: 1,
|
||||
};
|
||||
function inView(elementOrSelector, onStart, { root, margin: rootMargin, amount = "some" } = {}) {
|
||||
const elements = resolveElements(elementOrSelector);
|
||||
const activeIntersections = new WeakMap();
|
||||
const onIntersectionChange = (entries) => {
|
||||
entries.forEach((entry) => {
|
||||
const onEnd = activeIntersections.get(entry.target);
|
||||
/**
|
||||
* If there's no change to the intersection, we don't need to
|
||||
* do anything here.
|
||||
*/
|
||||
if (entry.isIntersecting === Boolean(onEnd))
|
||||
return;
|
||||
if (entry.isIntersecting) {
|
||||
const newOnEnd = onStart(entry.target, entry);
|
||||
if (typeof newOnEnd === "function") {
|
||||
activeIntersections.set(entry.target, newOnEnd);
|
||||
}
|
||||
else {
|
||||
observer.unobserve(entry.target);
|
||||
}
|
||||
}
|
||||
else if (typeof onEnd === "function") {
|
||||
onEnd(entry);
|
||||
activeIntersections.delete(entry.target);
|
||||
}
|
||||
});
|
||||
};
|
||||
const observer = new IntersectionObserver(onIntersectionChange, {
|
||||
root,
|
||||
rootMargin,
|
||||
threshold: typeof amount === "number" ? amount : thresholds[amount],
|
||||
});
|
||||
elements.forEach((element) => observer.observe(element));
|
||||
return () => observer.disconnect();
|
||||
}
|
||||
|
||||
export { inView };
|
||||
//# sourceMappingURL=index.mjs.map
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
const distance = (a, b) => Math.abs(a - b);
|
||||
function distance2D(a, b) {
|
||||
// Multi-dimensional
|
||||
const xDelta = distance(a.x, b.x);
|
||||
const yDelta = distance(a.y, b.y);
|
||||
return Math.sqrt(xDelta ** 2 + yDelta ** 2);
|
||||
}
|
||||
|
||||
export { distance, distance2D };
|
||||
//# sourceMappingURL=distance.mjs.map
|
||||
+152
@@ -0,0 +1,152 @@
|
||||
{
|
||||
"name": "framer-motion",
|
||||
"version": "14.3.1",
|
||||
"description": "A simple and powerful JavaScript animation library",
|
||||
"main": "dist/cjs/index.js",
|
||||
"module": "dist/es/index.mjs",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"require": "./dist/cjs/index.js",
|
||||
"import": "./dist/es/index.mjs",
|
||||
"default": "./dist/cjs/index.js"
|
||||
},
|
||||
"./debug": {
|
||||
"types": "./dist/debug.d.ts",
|
||||
"require": "./dist/cjs/debug.js",
|
||||
"import": "./dist/es/debug.mjs",
|
||||
"default": "./dist/cjs/debug.js"
|
||||
},
|
||||
"./dom/mini": {
|
||||
"types": "./dist/dom-mini.d.ts",
|
||||
"require": "./dist/cjs/dom-mini.js",
|
||||
"import": "./dist/es/dom-mini.mjs",
|
||||
"default": "./dist/cjs/dom-mini.js"
|
||||
},
|
||||
"./dom": {
|
||||
"types": "./dist/dom.d.ts",
|
||||
"require": "./dist/cjs/dom.js",
|
||||
"import": "./dist/es/dom.mjs",
|
||||
"default": "./dist/cjs/dom.js"
|
||||
},
|
||||
"./client": {
|
||||
"types": "./dist/client.d.ts",
|
||||
"require": "./dist/cjs/client.js",
|
||||
"import": "./dist/es/client.mjs",
|
||||
"default": "./dist/cjs/client.js"
|
||||
},
|
||||
"./m": {
|
||||
"types": "./dist/m.d.ts",
|
||||
"require": "./dist/cjs/m.js",
|
||||
"import": "./dist/es/m.mjs",
|
||||
"default": "./dist/cjs/m.js"
|
||||
},
|
||||
"./mini": {
|
||||
"types": "./dist/mini.d.ts",
|
||||
"require": "./dist/cjs/mini.js",
|
||||
"import": "./dist/es/mini.mjs",
|
||||
"default": "./dist/cjs/mini.js"
|
||||
},
|
||||
"./projection": {
|
||||
"types": "./dist/projection.d.ts",
|
||||
"import": "./dist/es/projection.mjs",
|
||||
"default": "./dist/es/projection.mjs"
|
||||
},
|
||||
"./animate-view": {
|
||||
"types": "./dist/animate-view.d.ts",
|
||||
"require": "./dist/cjs/animate-view.js",
|
||||
"import": "./dist/es/animate-view.mjs",
|
||||
"default": "./dist/cjs/animate-view.js"
|
||||
},
|
||||
"./package.json": "./package.json"
|
||||
},
|
||||
"types": "dist/index.d.ts",
|
||||
"author": "Matt Perry",
|
||||
"license": "MIT",
|
||||
"repository": "https://github.com/motiondivision/motion/",
|
||||
"sideEffects": false,
|
||||
"keywords": [
|
||||
"react animation",
|
||||
"react",
|
||||
"pose",
|
||||
"react pose",
|
||||
"animation",
|
||||
"gestures",
|
||||
"drag",
|
||||
"spring",
|
||||
"popmotion",
|
||||
"framer",
|
||||
"waapi"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "yarn clean && tsc --noEmitOnError -p . && rollup -c && node ./scripts/check-bundle.js",
|
||||
"dev": "yarn watch",
|
||||
"clean": "rm -rf types dist lib",
|
||||
"test": "yarn test-server && yarn test-client",
|
||||
"test-client": "jest --config jest.config.json --max-workers=2",
|
||||
"test-server": "jest --config jest.config.ssr.json",
|
||||
"prettier": "prettier ./src/* --write",
|
||||
"watch": "concurrently -c blue,red -n tsc --noEmitOnError ,rollup --kill-others \"tsc --noEmitOnError --watch -p . --preserveWatchOutput\" \"rollup --config --watch --no-watch.clearScreen\"",
|
||||
"prepack": "yarn build && yarn measure && node ../../dev/inc/bundlesize.mjs framer-motion",
|
||||
"postpublish": "git push --tags",
|
||||
"measure": "rollup -c ./rollup.size.config.mjs"
|
||||
},
|
||||
"dependencies": {
|
||||
"motion-dom": "14.3.1",
|
||||
"motion-utils": "14.1.1",
|
||||
"tslib": "^2.4.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@radix-ui/react-dialog": "^1.1.15",
|
||||
"@thednp/dommatrix": "^2.0.11",
|
||||
"@types/three": "0.137.0",
|
||||
"concurrently": "^7.3.0",
|
||||
"jest": "^29.7.0",
|
||||
"rollup": "4.59.0",
|
||||
"three": "0.137.0",
|
||||
"typescript": "5.4.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^18.0.0 || ^19.0.0",
|
||||
"react-dom": "^18.0.0 || ^19.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"react": {
|
||||
"optional": true
|
||||
},
|
||||
"react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
},
|
||||
"bundlesize": [
|
||||
{
|
||||
"path": "./dist/size-rollup-motion.js",
|
||||
"maxSize": "42.2 kB"
|
||||
},
|
||||
{
|
||||
"path": "./dist/size-rollup-m.js",
|
||||
"maxSize": "5 kB"
|
||||
},
|
||||
{
|
||||
"path": "./dist/size-rollup-m-dom-animation.js",
|
||||
"maxSize": "28.6 kB"
|
||||
},
|
||||
{
|
||||
"path": "./dist/size-rollup-m-dom-max.js",
|
||||
"maxSize": "42.3 kB"
|
||||
},
|
||||
{
|
||||
"path": "./dist/size-rollup-animate.js",
|
||||
"maxSize": "22 kB"
|
||||
},
|
||||
{
|
||||
"path": "./dist/size-rollup-scroll.js",
|
||||
"maxSize": "3.7 kB"
|
||||
},
|
||||
{
|
||||
"path": "./dist/size-rollup-waapi-animate.js",
|
||||
"maxSize": "3.2 kB"
|
||||
}
|
||||
],
|
||||
"gitHead": "1785a8816da2682ef53bfc1fe8b3f61d86f17924"
|
||||
}
|
||||
Reference in New Issue
Block a user