Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
275 changes: 275 additions & 0 deletions dev/html/public/animate-layout/global-collector.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,275 @@
<html>
<head>
<style>
body {
padding: 20px;
margin: 0;
font-family: system-ui, sans-serif;
}

.container {
display: flex;
gap: 20px;
margin-bottom: 20px;
}

.box {
width: 100px;
height: 100px;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-weight: bold;
}

#boxA {
background-color: #00cc88;
}

#boxB {
background-color: #0088cc;
}

#boxC {
background-color: #cc8800;
}

/* Moved states */
#boxA.moved {
transform: translateX(150px);
}

#boxB.moved {
transform: translateY(50px);
}

#boxC.moved {
transform: translate(100px, 100px);
}

.controls {
margin-top: 20px;
}

button {
padding: 10px 20px;
margin-right: 10px;
cursor: pointer;
}

.status {
margin-top: 20px;
padding: 10px;
background: #f0f0f0;
font-family: monospace;
white-space: pre-wrap;
}

[data-layout-correct="false"] {
background: #dd1144 !important;
opacity: 0.5;
}
</style>
</head>
<body>
<h1>Global Layout Animation Collector</h1>
<p>Test the layoutAnimation.add() and layoutAnimation.play() API</p>

<div class="container">
<div id="boxA" class="box" data-layout>A</div>
<div id="boxB" class="box" data-layout>B</div>
<div id="boxC" class="box" data-layout>C</div>
</div>

<div class="controls">
<button id="btnRegister">1. Register Elements</button>
<button id="btnMove">2. Move Elements</button>
<button id="btnPlay">3. Play Animation</button>
<button id="btnReset">Reset All</button>
</div>

<div id="status" class="status">Ready. Click buttons in order: Register → Move → Play</div>

<script type="module" src="/src/imports/animate-layout.js"></script>
<script type="module">
const { layoutAnimation, frame } = window.AnimateLayout

const boxA = document.getElementById("boxA")
const boxB = document.getElementById("boxB")
const boxC = document.getElementById("boxC")
const status = document.getElementById("status")

let currentControls = null

function log(message) {
status.textContent = message
console.log(message)
}

// Button 1: Register all elements
document.getElementById("btnRegister").addEventListener("click", () => {
// Multiple add() calls - should batch into one snapshot
layoutAnimation.add(boxA, { duration: 0.5, ease: "easeOut" })
layoutAnimation.add(boxB, { duration: 0.8, ease: "easeInOut" })
layoutAnimation.add(boxC) // uses defaults

log("Registered all 3 elements with different durations.\nNow click 'Move Elements'.")
})

// Button 2: Apply DOM changes
document.getElementById("btnMove").addEventListener("click", () => {
boxA.classList.add("moved")
boxB.classList.add("moved")
boxC.classList.add("moved")

log("DOM changes applied (transforms added).\nNow click 'Play Animation'.")
})

// Button 3: Trigger the animation
document.getElementById("btnPlay").addEventListener("click", async () => {
log("Playing animation...")

const controls = await layoutAnimation.play()
currentControls = controls

// Test that calling play() again returns same reference
const sameControls = await layoutAnimation.play()
const isSameReference = controls === sameControls

log(`Animation playing!\n` +
`Duration: ${controls.duration}s\n` +
`play() idempotent: ${isSameReference ? "✓ YES" : "✗ NO"}`)

controls.finished.then(() => {
log("Animation complete!")
})
})

// Reset button
document.getElementById("btnReset").addEventListener("click", () => {
// Stop any running animation
if (currentControls) {
currentControls.stop()
currentControls = null
}

// Reset the collector
layoutAnimation.reset()

// Reset DOM
boxA.classList.remove("moved")
boxB.classList.remove("moved")
boxC.classList.remove("moved")

log("Reset complete. Click buttons in order: Register → Move → Play")
})

// Auto-run test
async function runAutoTest() {
log("Running automated test...")

// Register
layoutAnimation.add(boxA, { duration: 0.3 })
layoutAnimation.add(boxB, { duration: 0.3 })
layoutAnimation.add(boxC, { duration: 0.3 })

// Wait for snapshot to be taken
await new Promise(resolve => frame.postRender(resolve))

// Apply DOM changes
boxA.classList.add("moved")
boxB.classList.add("moved")
boxC.classList.add("moved")

// Play
const controls = await layoutAnimation.play()

// Verify play() returns same reference
const controls2 = await layoutAnimation.play()
if (controls !== controls2) {
log("ERROR: play() should return same reference!")
return
}

// Pause at midpoint to verify animation is working
controls.pause()
controls.time = controls.duration * 0.5

await new Promise(resolve => frame.postRender(resolve))

log(`Auto-test passed!\n` +
`Animation duration: ${controls.duration}s\n` +
`Animation count: ${controls.animations.length}\n` +
`play() idempotent: ✓ YES`)

// Resume the animation
setTimeout(() => {
controls.play()
}, 1000)
}

// Test interruption: can start new animation without reset()
async function runInterruptionTest() {
log("Running interruption test...")

// First animation
layoutAnimation.add(boxA, { duration: 0.5 })
layoutAnimation.add(boxB, { duration: 0.5 })

await new Promise(resolve => frame.postRender(resolve))

boxA.classList.add("moved")
boxB.classList.add("moved")

const controls1 = await layoutAnimation.play()

// Wait a bit (animation still running)
await new Promise(resolve => setTimeout(resolve, 100))

// Start new animation WITHOUT calling reset()
layoutAnimation.add(boxA, { duration: 0.3 })
layoutAnimation.add(boxB, { duration: 0.3 })

await new Promise(resolve => frame.postRender(resolve))

// Move back
boxA.classList.remove("moved")
boxB.classList.remove("moved")

const controls2 = await layoutAnimation.play()

// Should be a NEW animation, not the same reference
if (controls1 === controls2) {
log("ERROR: New animation should have different controls!")
return false
}

await controls2.finished

log(`Interruption test passed!\n` +
`First animation had ${controls1.animations.length} animations\n` +
`Second animation had ${controls2.animations.length} animations\n` +
`No reset() required: ✓ YES`)

return true
}

// Run auto-test on page load
runAutoTest()
.then(() => new Promise(resolve => setTimeout(resolve, 2000)))
.then(() => {
// Reset state for interruption test
boxA.classList.remove("moved")
boxB.classList.remove("moved")
boxC.classList.remove("moved")
layoutAnimation.reset()
return runInterruptionTest()
})
.catch(err => {
log(`ERROR: ${err.message}`)
console.error(err)
})
</script>
</body>
</html>
2 changes: 2 additions & 0 deletions dev/html/src/imports/animate-layout.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
frame,
parseAnimateLayoutArgs,
animate,
layoutAnimation,
} from "framer-motion/dom"

export function unstable_animateLayout(
Expand All @@ -22,6 +23,7 @@ export function unstable_animateLayout(
window.AnimateLayout = {
animateLayout: unstable_animateLayout,
LayoutAnimationBuilder,
layoutAnimation,
frame,
animate,
}
Original file line number Diff line number Diff line change
@@ -1 +1 @@
["app-store-a-b-a.html","basic-position-change.html","interrupt-animation.html","modal-open-after-animate.html","modal-open-close-interrupt.html","modal-open-close-open-interrupt.html","modal-open-close-open.html","modal-open-close.html","modal-open-opacity.html","modal-open.html","repeat-animation.html","scale-correction.html","scope-with-data-layout.html","shared-element-a-ab-a.html","shared-element-a-b-a-replace.html","shared-element-a-b-a-reuse.html","shared-element-basic.html","shared-element-configured.html","shared-element-crossfade.html","shared-element-nested-children-bottom.html","shared-element-nested-children.html","shared-element-no-crossfade.html","shared-multiple-elements.html"]
["app-store-a-b-a.html","basic-position-change.html","global-collector.html","interrupt-animation.html","modal-open-after-animate.html","modal-open-close-interrupt.html","modal-open-close-open-interrupt.html","modal-open-close-open.html","modal-open-close.html","modal-open-opacity.html","modal-open.html","repeat-animation.html","scale-correction.html","scope-with-data-layout.html","shared-element-a-ab-a.html","shared-element-a-b-a-replace.html","shared-element-a-b-a-reuse.html","shared-element-basic.html","shared-element-configured.html","shared-element-crossfade.html","shared-element-nested-children-bottom.html","shared-element-nested-children.html","shared-element-no-crossfade.html","shared-multiple-elements.html"]
1 change: 1 addition & 0 deletions packages/motion-dom/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,7 @@ export {
LayoutAnimationBuilder,
parseAnimateLayoutArgs,
} from "./layout/LayoutAnimationBuilder"
export { layoutAnimation } from "./layout/layout-animation"

/**
* Deprecated
Expand Down
Loading