Skip to content

Instantly share code, notes, and snippets.

@halogenandtoast
Created December 30, 2023 01:20
Show Gist options
  • Select an option

  • Save halogenandtoast/35ac6897117087731c41599c71597180 to your computer and use it in GitHub Desktop.

Select an option

Save halogenandtoast/35ac6897117087731c41599c71597180 to your computer and use it in GitHub Desktop.
Vue SFC showcasing animating between lists
<script setup lang="ts">
import gsap from 'gsap'
import { ref } from 'vue'
const CardStates = ["Above", "Below"] as const
type CardState = typeof CardStates[number]
interface Card {
id: number
image: string
}
const animationMatrix = ref([])
const nextId = ref(4)
const above = ref([])
const below = ref([{ id: 1, color: "#FF0000" }, { id: 2, color: "#0000FF" }, { id: 3, color: "#00FF00" }])
function addCard() {
const randomColor = Math.floor(Math.random() * 16777215).toString(16);
const newCard = { id: nextId.value, color: `#${randomColor}` }
nextId.value = nextId.value + 1
below.value = [newCard, ...below.value]
}
function onBeforeEnter(el) {
el.style.opacity = 0
el.style.width = 0
}
function onEnter(el, done) {
const index = el.dataset.index
const finalRect = el.getBoundingClientRect()
if(!index) {
const width = window.getComputedStyle(el).width
gsap.to(el, { opacity: 1, width, onComplete: done })
return
}
const data = animationMatrix.value.find(([idx,]) => idx === index)
animationMatrix.value = animationMatrix.value.filter(([idx,]) => idx !== index)
if (!data) {
gsap.to(el, { startAt: { height: 0, marginLeft: 50, marginTop: 70 }, ease: "power4.out", opacity: 1, width: 100, height: 140, marginLeft: 0, marginTop: 0, onComplete: done })
return
}
const [,rect] = data
const startX = rect.left - finalRect.left
const startY = rect.top - finalRect.top
const c = el.cloneNode(true)
c.style.position = "fixed"
c.style.width = rect.width + "px"
el.parentNode.insertBefore(c, el)
const tl = gsap.timeline()
tl.
add("start").
to(el, {
startAt: { opacity: 0, width: 0 },
width: rect.width,
duration: 0.3
}, "start").
to(c, {
startAt: { x: startX, y: startY, opacity: 1 },
y: 0,
x: 0,
onComplete: () => {
c.remove()
el.style.opacity = "1"
done()
},
duration: 0.3
}, "start")
}
function onLeave(el, done) {
animationMatrix.value = [...animationMatrix.value, [el.dataset.index, el.getBoundingClientRect()]]
gsap.to(el, {
startAt: { opacity: 0 },
width: 0,
margin: 0,
onComplete: done,
duration: 0.3
})
}
function moveCard(card: string, moveTo: CardState) {
switch (moveTo) {
case 'Above':
above.value = [card, ...above.value]
below.value = below.value.filter((c) => c !== card)
return
case 'Below':
below.value = [card, ...below.value]
above.value = above.value.filter((c) => c !== card)
return
}
}
</script>
<template>
<div class="zones">
<div class="cards" key="above">
<transition-group @enter="onEnter" @leave="onLeave" @before-enter="onBeforeEnter">
<div :style="{ 'background-color': card.color }" @click.once="moveCard(card, 'Below')" class="card" v-for="card in above" :key="card.id" :data-index="card.id"></div>
</transition-group>
</div>
<div class="cards" key="below">
<transition-group @enter="onEnter" @leave="onLeave" @before-enter="onBeforeEnter">
<div :style="{ 'background-color': card.color }" @click.once="moveCard(card, 'Above')" class="card" v-for="card in below" :key="card.id" :data-index="card.id"></div>
</transition-group>
</div>
<button @click="addCard">Insert</button>
</div>
</template>
<style scoped>
.zones {
display: flex;
flex-direction: column;
gap: 100px;
width: 100%;
}
.cards {
display: flex;
height: 160px;
background: #AAA;
width: 100%;
padding: 10px;
box-sizing: border-box;
}
.card {
margin-right: 10px;
box-sizing: border-box;
width: 100px;
height: 140px;
border-radius: 4px;
background: #AAA;
}
</style>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment