Mission Control Dashboard - Initial implementation
This commit is contained in:
269
node_modules/motion-dom/dist/es/layout/LayoutAnimationBuilder.mjs
generated
vendored
Normal file
269
node_modules/motion-dom/dist/es/layout/LayoutAnimationBuilder.mjs
generated
vendored
Normal file
@@ -0,0 +1,269 @@
|
||||
import { GroupAnimation } from '../animation/GroupAnimation.mjs';
|
||||
import { copyBoxInto } from '../projection/geometry/copy.mjs';
|
||||
import { createBox } from '../projection/geometry/models.mjs';
|
||||
import { HTMLProjectionNode } from '../projection/node/HTMLProjectionNode.mjs';
|
||||
import { HTMLVisualElement } from '../render/html/HTMLVisualElement.mjs';
|
||||
import { visualElementStore } from '../render/store.mjs';
|
||||
import { resolveElements } from '../utils/resolve-elements.mjs';
|
||||
import { frame } from '../frameloop/frame.mjs';
|
||||
|
||||
const layoutSelector = "[data-layout], [data-layout-id]";
|
||||
const noop = () => { };
|
||||
function snapshotFromTarget(projection) {
|
||||
const target = projection.targetWithTransforms || projection.target;
|
||||
if (!target)
|
||||
return undefined;
|
||||
const measuredBox = createBox();
|
||||
const layoutBox = createBox();
|
||||
copyBoxInto(measuredBox, target);
|
||||
copyBoxInto(layoutBox, target);
|
||||
return {
|
||||
animationId: projection.root?.animationId ?? 0,
|
||||
measuredBox,
|
||||
layoutBox,
|
||||
latestValues: projection.animationValues || projection.latestValues || {},
|
||||
source: projection.id,
|
||||
};
|
||||
}
|
||||
class LayoutAnimationBuilder {
|
||||
constructor(scope, updateDom, defaultOptions) {
|
||||
this.sharedTransitions = new Map();
|
||||
this.notifyReady = noop;
|
||||
this.rejectReady = noop;
|
||||
this.scope = scope;
|
||||
this.updateDom = updateDom;
|
||||
this.defaultOptions = defaultOptions;
|
||||
this.readyPromise = new Promise((resolve, reject) => {
|
||||
this.notifyReady = resolve;
|
||||
this.rejectReady = reject;
|
||||
});
|
||||
frame.postRender(() => {
|
||||
this.start().then(this.notifyReady).catch(this.rejectReady);
|
||||
});
|
||||
}
|
||||
shared(id, transition) {
|
||||
this.sharedTransitions.set(id, transition);
|
||||
return this;
|
||||
}
|
||||
then(resolve, reject) {
|
||||
return this.readyPromise.then(resolve, reject);
|
||||
}
|
||||
async start() {
|
||||
const beforeElements = collectLayoutElements(this.scope);
|
||||
const beforeRecords = this.buildRecords(beforeElements);
|
||||
beforeRecords.forEach(({ projection }) => {
|
||||
const hasCurrentAnimation = Boolean(projection.currentAnimation);
|
||||
const isSharedLayout = Boolean(projection.options.layoutId);
|
||||
if (hasCurrentAnimation && isSharedLayout) {
|
||||
const snapshot = snapshotFromTarget(projection);
|
||||
if (snapshot) {
|
||||
projection.snapshot = snapshot;
|
||||
}
|
||||
else if (projection.snapshot) {
|
||||
projection.snapshot = undefined;
|
||||
}
|
||||
}
|
||||
else if (projection.snapshot &&
|
||||
(projection.currentAnimation || projection.isProjecting())) {
|
||||
projection.snapshot = undefined;
|
||||
}
|
||||
projection.isPresent = true;
|
||||
projection.willUpdate();
|
||||
});
|
||||
await this.updateDom();
|
||||
const afterElements = collectLayoutElements(this.scope);
|
||||
const afterRecords = this.buildRecords(afterElements);
|
||||
this.handleExitingElements(beforeRecords, afterRecords);
|
||||
afterRecords.forEach(({ projection }) => {
|
||||
const instance = projection.instance;
|
||||
const resumeFromInstance = projection.resumeFrom
|
||||
?.instance;
|
||||
if (!instance || !resumeFromInstance)
|
||||
return;
|
||||
if (!("style" in instance))
|
||||
return;
|
||||
const currentTransform = instance.style.transform;
|
||||
const resumeFromTransform = resumeFromInstance.style.transform;
|
||||
if (currentTransform &&
|
||||
resumeFromTransform &&
|
||||
currentTransform === resumeFromTransform) {
|
||||
instance.style.transform = "";
|
||||
instance.style.transformOrigin = "";
|
||||
}
|
||||
});
|
||||
afterRecords.forEach(({ projection }) => {
|
||||
projection.isPresent = true;
|
||||
});
|
||||
const root = getProjectionRoot(afterRecords, beforeRecords);
|
||||
root?.didUpdate();
|
||||
await new Promise((resolve) => {
|
||||
frame.postRender(() => resolve());
|
||||
});
|
||||
const animations = collectAnimations(afterRecords);
|
||||
const animation = new GroupAnimation(animations);
|
||||
return animation;
|
||||
}
|
||||
buildRecords(elements) {
|
||||
const records = [];
|
||||
const recordMap = new Map();
|
||||
for (const element of elements) {
|
||||
const parentRecord = findParentRecord(element, recordMap, this.scope);
|
||||
const { layout, layoutId } = readLayoutAttributes(element);
|
||||
const override = layoutId
|
||||
? this.sharedTransitions.get(layoutId)
|
||||
: undefined;
|
||||
const transition = override || this.defaultOptions;
|
||||
const record = getOrCreateRecord(element, parentRecord?.projection, {
|
||||
layout,
|
||||
layoutId,
|
||||
animationType: typeof layout === "string" ? layout : "both",
|
||||
transition: transition,
|
||||
});
|
||||
recordMap.set(element, record);
|
||||
records.push(record);
|
||||
}
|
||||
return records;
|
||||
}
|
||||
handleExitingElements(beforeRecords, afterRecords) {
|
||||
const afterElementsSet = new Set(afterRecords.map((record) => record.element));
|
||||
beforeRecords.forEach((record) => {
|
||||
if (afterElementsSet.has(record.element))
|
||||
return;
|
||||
// For shared layout elements, relegate to set up resumeFrom
|
||||
// so the remaining element animates from this position
|
||||
if (record.projection.options.layoutId) {
|
||||
record.projection.isPresent = false;
|
||||
record.projection.relegate();
|
||||
}
|
||||
record.visualElement.unmount();
|
||||
visualElementStore.delete(record.element);
|
||||
});
|
||||
// Clear resumeFrom on EXISTING nodes that point to unmounted projections
|
||||
// This prevents crossfade animation when the source element was removed entirely
|
||||
// But preserve resumeFrom for NEW nodes so they can animate from the old position
|
||||
// Also preserve resumeFrom for lead nodes that were just promoted via relegate
|
||||
const beforeElementsSet = new Set(beforeRecords.map((record) => record.element));
|
||||
afterRecords.forEach(({ element, projection }) => {
|
||||
if (beforeElementsSet.has(element) &&
|
||||
projection.resumeFrom &&
|
||||
!projection.resumeFrom.instance &&
|
||||
!projection.isLead()) {
|
||||
projection.resumeFrom = undefined;
|
||||
projection.snapshot = undefined;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
function parseAnimateLayoutArgs(scopeOrUpdateDom, updateDomOrOptions, options) {
|
||||
// animateLayout(updateDom)
|
||||
if (typeof scopeOrUpdateDom === "function") {
|
||||
return {
|
||||
scope: document,
|
||||
updateDom: scopeOrUpdateDom,
|
||||
defaultOptions: updateDomOrOptions,
|
||||
};
|
||||
}
|
||||
// animateLayout(scope, updateDom, options?)
|
||||
const elements = resolveElements(scopeOrUpdateDom);
|
||||
const scope = elements[0] || document;
|
||||
return {
|
||||
scope,
|
||||
updateDom: updateDomOrOptions,
|
||||
defaultOptions: options,
|
||||
};
|
||||
}
|
||||
function collectLayoutElements(scope) {
|
||||
const elements = Array.from(scope.querySelectorAll(layoutSelector));
|
||||
if (scope instanceof Element && scope.matches(layoutSelector)) {
|
||||
if (!elements.includes(scope)) {
|
||||
elements.unshift(scope);
|
||||
}
|
||||
}
|
||||
return elements;
|
||||
}
|
||||
function readLayoutAttributes(element) {
|
||||
const layoutId = element.getAttribute("data-layout-id") || undefined;
|
||||
const rawLayout = element.getAttribute("data-layout");
|
||||
let layout;
|
||||
if (rawLayout === "" || rawLayout === "true") {
|
||||
layout = true;
|
||||
}
|
||||
else if (rawLayout) {
|
||||
layout = rawLayout;
|
||||
}
|
||||
return {
|
||||
layout,
|
||||
layoutId,
|
||||
};
|
||||
}
|
||||
function createVisualState() {
|
||||
return {
|
||||
latestValues: {},
|
||||
renderState: {
|
||||
transform: {},
|
||||
transformOrigin: {},
|
||||
style: {},
|
||||
vars: {},
|
||||
},
|
||||
};
|
||||
}
|
||||
function getOrCreateRecord(element, parentProjection, projectionOptions) {
|
||||
const existing = visualElementStore.get(element);
|
||||
const visualElement = existing ??
|
||||
new HTMLVisualElement({
|
||||
props: {},
|
||||
presenceContext: null,
|
||||
visualState: createVisualState(),
|
||||
}, { allowProjection: true });
|
||||
if (!existing || !visualElement.projection) {
|
||||
visualElement.projection = new HTMLProjectionNode(visualElement.latestValues, parentProjection);
|
||||
}
|
||||
visualElement.projection.setOptions({
|
||||
...projectionOptions,
|
||||
visualElement,
|
||||
});
|
||||
if (!visualElement.current) {
|
||||
visualElement.mount(element);
|
||||
}
|
||||
else if (!visualElement.projection.instance) {
|
||||
// Mount projection if VisualElement is already mounted but projection isn't
|
||||
// This happens when animate() was called before animateLayout()
|
||||
visualElement.projection.mount(element);
|
||||
}
|
||||
if (!existing) {
|
||||
visualElementStore.set(element, visualElement);
|
||||
}
|
||||
return {
|
||||
element,
|
||||
visualElement,
|
||||
projection: visualElement.projection,
|
||||
};
|
||||
}
|
||||
function findParentRecord(element, recordMap, scope) {
|
||||
let parent = element.parentElement;
|
||||
while (parent) {
|
||||
const record = recordMap.get(parent);
|
||||
if (record)
|
||||
return record;
|
||||
if (parent === scope)
|
||||
break;
|
||||
parent = parent.parentElement;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
function getProjectionRoot(afterRecords, beforeRecords) {
|
||||
const record = afterRecords[0] || beforeRecords[0];
|
||||
return record?.projection.root;
|
||||
}
|
||||
function collectAnimations(afterRecords) {
|
||||
const animations = new Set();
|
||||
afterRecords.forEach((record) => {
|
||||
const animation = record.projection.currentAnimation;
|
||||
if (animation)
|
||||
animations.add(animation);
|
||||
});
|
||||
return Array.from(animations);
|
||||
}
|
||||
|
||||
export { LayoutAnimationBuilder, parseAnimateLayoutArgs };
|
||||
//# sourceMappingURL=LayoutAnimationBuilder.mjs.map
|
||||
1
node_modules/motion-dom/dist/es/layout/LayoutAnimationBuilder.mjs.map
generated
vendored
Normal file
1
node_modules/motion-dom/dist/es/layout/LayoutAnimationBuilder.mjs.map
generated
vendored
Normal file
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user