gsapcustom-scrolling

How to Get Half Circle Scrolling Effect in Javascript


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.


Solution

  • 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>