I've been working on cloning suno.ai for learning purposes, and I'm facing two challenges. Firstly, I've successfully implemented a play song on scroll functionality using GSAP scroll trigger, but I'm struggling to figure out how to rotate these elements in a half circle as seen on suno.ai.
You can check my live site here.
If anyone has insights or guidance on achieving both the play song on scroll and the rotating elements in a half circle effect, I would greatly appreciate your help!
Thank you.
I've tried placing elements around a circle and rotating a div on scroll, but it won't work as expected.
I had seen one tutorial of this, I don't remember the channel but here is what I created. Check this out
const songsContainer = document.querySelector('#songs')
const songs = songsContainer.querySelectorAll('span')
const skip = songsContainer.querySelectorAll('span.skip').length
const perSongRotationPercentage = 100 / (songs.length - skip)
const totalRotation = 180
const onScroll = () => {
const maxScrollY = songsContainer.clientHeight - window.innerHeight
const scrollPercentage = window.scrollY / maxScrollY * 100
for (let i = 0; i < songs.length; i++) {
const rotationPercentage = i * perSongRotationPercentage
const rotation = rotationPercentage / 100 * totalRotation;
const scrollRotation = scrollPercentage / 100 * totalRotation;
const skipRotation = skip * perSongRotationPercentage / 100 * totalRotation;
songs[i].style.transform = `rotate(${rotation - scrollRotation - skipRotation}deg)`
songs[i].classList.remove('active');
}
const nSongs = songs.length - skip;
const activeSongIndex = skip + Math.round(scrollPercentage / 100 * nSongs)
songs[activeSongIndex]?.classList.add('active');
}
addEventListener("scroll", onScroll)
onScroll()
for (let i = skip; i < songs.length; i++) {
songs[i].addEventListener('click', () => {
const percentage = perSongRotationPercentage * (i - skip);
window.scrollTo({ top: (percentage / 100) * (songsContainer.clientHeight - window.innerHeight), behavior: 'smooth' })
})
}
html,
body {
margin: 0;
padding: 0;
font-family: 'Inter', sans-serif;
overscroll-behavior: none;
overflow-x: clip;
background: #050510;
color: white;
}
html::-webkit-scrollbar {
display: none;
}
#songs {
height: 600vh;
position: relative;
}
.follow-screen {
height: 100vh;
position: sticky;
top: 0;
display: flex;
align-items: center;
margin-left: -30vw;
}
#songs span {
position: absolute;
transform-origin: left;
padding-left: 40vw;
white-space: nowrap;
user-select: none;
transition: transform 200ms ease-out , opacity 300ms ease-in-out;
font-size: 4.5vw;
display: flex;
align-items: center;
}
#songs span.active {
--flag-scale: .7;
}
#songs span:not(.active):not(.skip) {
opacity: 0.5;
}
#songs span.skip {
pointer-events: none;
opacity: .2;
}
#songs span::before {
content: var(--flag);
font-family: 'Noto Color Emoji';
position: absolute;
left: 35vw;
scale: var(--flag-scale, 0);
transition: scale 200ms ease-in-out;
}
#songs span:not(.skip):not(.active):hover {
opacity: .7;
cursor: pointer;
}
/* Additional content: could be anything, this is just to show how the songs would fade out of the screen */
.next-trip {
height: 100vh;
width: 100%;
margin-top: -50vh;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
& img {
width: 100%;
height: 100%;
object-fit: cover;
position: absolute;
--mask-image: linear-gradient(to bottom, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 1) 100%);
mask-image: var(--mask-image);
-webkit-mask-image: var(--mask-image);
}
& span {
z-index: 1;
text-shadow: 0px 0px 4px white;
user-select: none;
text-align: center;
}
& .small-text {
font-size: 2em;
}
& .big-text {
font-size: 6em;
}
}
<head>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@700&family=Noto+Color+Emoji&display=swap" rel="stylesheet">
</head>
<body>
<div id="songs">
<div class="follow-screen">
<span class="skip">Celtic magic in Wales</span>
<span class="skip">Cuisine captivates France's essence</span>
<span class="skip">Siesta echoes through Spain</span>
<span class="skip">Chocolate indulgence, Belgium's pride</span>
<span class="skip">Home sweet home</span>
<span class="skip">Luxe in Luxembourg's lap</span>
<span class="skip">Oktoberfest joy, Germany's celebration</span>
<span class="skip">Fjords define Norway's beauty</span>
<span class="skip">Nordic serenity, Sweden's calm</span>
<span class="skip">Alpine vistas grace Switzerland</span>
<span class="skip">Vienna waltz, Austria's melody</span>
<span style="--flag: 'đŠđ°'">Hygge embraces Denmark's streets</span>
<span style="--flag: 'đ¨đą'">Andes echo in Chile</span>
<span style="--flag: 'đ§đˇ'">Samba heartbeats, Brazil's rhythm</span>
<span style="--flag: 'đŚđˇ'">Tango thrives in Argentina</span>
<span style="--flag: 'đ§đ´'">Andean peaks define Bolivia</span>
<span style="--flag: 'đľđš'">Fado serenades Portugal</span>
<span style="--flag: 'đ´ó §ó ˘ó Ľó Žó §ó ż'">London's charm enchants England</span>
<span style="--flag: 'đ´ó §ó ˘ó łó Łó ´ó ż'">Highlands whisper Scotland's tales</span>
<span style="--flag: 'đ´ó §ó ˘ó ˇó Źó łó ż'">Celtic magic in Wales</span>
<span style="--flag: 'đŤđˇ'">Cuisine captivates France's essence</span>
<span style="--flag: 'đŞđ¸'">Siesta echoes through Spain</span>
<span style="--flag: 'đ§đŞ'">Chocolate indulgence, Belgium's pride</span>
<span style="--flag: 'đłđą'">Home sweet home</span>
<span style="--flag: 'đąđş'">Luxe in Luxembourg's lap</span>
<span style="--flag: 'đŠđŞ'">Bratwurst, beer, Oktoberfest cheer</span>
<span style="--flag: 'đłđ´'">Fjords define Norway's beauty</span>
<span style="--flag: 'đ¸đŞ'">Nordic serenity, Sweden's calm</span>
<span style="--flag: 'đ¨đ'">Alpine vistas grace Switzerland</span>
<span style="--flag: 'đŚđš'">Vienna waltz, Austria's melody</span>
</div>
</div>
<div class="next-trip">
<span class="small-text">Next trip</span>
<span class="big-text">Costa Rica</span>
</div>
</body>