<script src="https://kit.fontawesome.com/48764efa36.js" crossorigin="anonymous"></script>
        <link rel="preconnect" href="https://fonts.gstatic.com">
        <link href="https://fonts.googleapis.com/css2?family=Source+Sans+Pro:wght@300;400;600&display=swap" rel="stylesheet">

    <h1>Select Your Path</h1>
    <div id="album-rotator">
        <div id="album-rotator-holder">
            <a target="_top" class="album-item" href="https://twitter.com/smpnjn">
            </a>
        </div>
    </div>
<script id="noise" type="x-shader/x-fragment">
  #define NUM_OCTAVES 5

vec3 mod289(vec3 x) { return x - floor(x * (1.0 / 289.0)) * 289.0; }
vec2 mod289(vec2 x) { return x - floor(x * (1.0 / 289.0)) * 289.0; }
vec3 permute(vec3 x) { return mod289(((x*34.0)+1.0)*x); }

float rand(float n){return fract(sin(n) * 43758.5453123);}
float rand(vec2 n) { 
    return fract(sin(dot(n, vec2(12.9898, 4.1414))) * 43758.5453);
}
float noise(float p){
    float fl = floor(p);
  float fc = fract(p);
    return mix(rand(fl), rand(fl + 1.0), fc);
}
float noise(vec2 n) {
    const vec2 d = vec2(0.0, 1.0);
  vec2 b = floor(n), f = smoothstep(vec2(0.0), vec2(1.0), fract(n));
    return mix(mix(rand(b), rand(b + d.yx), f.x), mix(rand(b + d.xy), rand(b + d.yy), f.x), f.y);
}

float snoise(vec2 v) {
    const vec4 C = vec4(0.211324865405187,  // (3.0-sqrt(3.0))/6.0
                        0.366025403784439,  // 0.5*(sqrt(3.0)-1.0)
                        -0.577350269189626,  // -1.0 + 2.0 * C.x
                        0.024390243902439); // 1.0 / 41.0
    vec2 i  = floor(v + dot(v, C.yy) );
    vec2 x0 = v -   i + dot(i, C.xx);
    vec2 i1;
    i1 = (x0.x > x0.y) ? vec2(1.0, 0.0) : vec2(0.0, 1.0);
    vec4 x12 = x0.xyxy + C.xxzz;
    x12.xy -= i1;
    i = mod289(i); // Avoid truncation effects in permutation
    vec3 p = permute( permute( i.y + vec3(0.0, i1.y, 1.0 ))
        + i.x + vec3(0.0, i1.x, 1.0 ));

    vec3 m = max(0.5 - vec3(dot(x0,x0), dot(x12.xy,x12.xy), dot(x12.zw,x12.zw)), 0.0);
    m = m*m ;
    m = m*m ;
    vec3 x = 2.0 * fract(p * C.www) - 1.0;
    vec3 h = abs(x) - 0.5;
    vec3 ox = floor(x + 0.5);
    vec3 a0 = x - ox;
    m *= 1.79284291400159 - 0.85373472095314 * ( a0*a0 + h*h );
    vec3 g;
    g.x  = a0.x  * x0.x  + h.x  * x0.y;
    g.yz = a0.yz * x12.xz + h.yz * x12.yw;
    return 130.0 * dot(m, g);
}
const mat2 m2 = mat2(0.8,-0.6,0.6,0.8);

#define NB_OCTAVES 8
#define LACUNARITY 10.0
#define GAIN 0.5

float fbm(in vec2 p) {
    float total = 0.0,
        frequency = 1.0,
        amplitude = 1.0;
    
    for (int i = 0; i < NB_OCTAVES; i++) {
        total += snoise(p * frequency) * amplitude;
        frequency *= LACUNARITY;
        amplitude *= GAIN;
    }    
    return total;
}

</script>
<script id="vertex" type="x-shader/x-fragment">
  uniform float u_time;
uniform float u_height;
uniform vec2 u_rand;

float xDistortion;
float yDistortion;

varying float vDistortion;
varying vec2 vUv;

void main() {
    vUv = uv;
    vDistortion = snoise(vUv.xx * 3. - vec2(u_time / u_rand.x, u_time / u_rand.x) + cos(vUv.yy) * u_rand.y) * u_height;
    xDistortion = snoise(vUv.xx * 1.) * u_height * u_rand.x / 10.;
    vec3 pos = position;
    pos.z += (vDistortion * 55.);
    pos.x += (xDistortion * 55.);
    pos.y += (sin(vUv.y) * 55.);
    
    gl_Position = projectionMatrix * modelViewMatrix * vec4(pos, 1.0);
}

</script>
<script id="fragment" type="x-shader/x-fragment">
  vec3 rgb(float r, float g, float b) {
    return vec3(r / 255., g / 255., b / 255.);
}
vec3 rgb(float c) {
    return vec3(c / 255., c / 255., c / 255.);
}

uniform vec3 u_lowColor;
uniform vec3 u_highColor;
uniform float u_time;

varying vec2 vUv;
varying float vDistortion;
varying float xDistortion;

void main() {
    vec3 highColor = rgb(u_highColor.r, u_highColor.g, u_highColor.b);
    
    vec3 colorMap = rgb(u_lowColor.r, u_lowColor.g, u_lowColor.b);

    colorMap = mix(colorMap, highColor, vDistortion);
    
    gl_FragColor = vec4(colorMap, 1.);
}

  </script>
body {
    padding: 0;
    margin: 0;
    background: radial-gradient(circle at top left, #161231, #161231);
    font-family: "Source Sans Pro", -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
    padding: 4rem;
}

#album-rotator {
    width: 1000px;
    float: left;
    white-space: nowrap;
    overflow: hidden;
    position: relative;
    word-break: keep-all;
    padding: 2rem 0;
}
#album-rotator-holder {
    position: relative;
    user-select: none;
    will-change: transform;
}

.album-item {
    word-break: keep-all;
    background: black;
    box-shadow: 0 2px 30px rgba(0,0,0,0.5);
    position: relative;
    width: 500px;
    user-drag: none;
    height: 1000px;
    user-select: none;
    border-radius: 10px;
    white-space: nowrap;
    display: inline-block;
    margin: 0 3rem 0 0;
    transition: all 0.2s ease-out;
    transform: scale(1);
}

.subtext {
    font-size: 1.1167rem;
    position: absolute;
    bottom: 0;
    left: 0;
    padding: 1.25rem 1rem;
    width: 100%;
    white-space: initial;
    letter-spacing: 0;
    box-sizing: border-box;
    font-weight: 400;
}
.icon {
    font-size: 1rem;
    line-height: 1rem;
    display: block;
    padding: 0 0 0.5rem 0;
}
canvas {
    position: absolute;
    top: 0;
    left: 0;
}
h1 {
    font-size: 2.5rem;
    color: white;
    margin: 0;
    font-weight: 600;
    text-shadow: 0 2px 10px rgba(0,0,0,0.2);
}
import * as THREE from 'https://cdn.skypack.dev/three@v0.122.0';

// Helper functions
const rgb = function(r, g, b) {
    return new THREE.Vector3(r, g, b);
}
const loader = function(path, texture) {
    return new Promise((resolve, reject) => {
        let loader = new THREE.FileLoader();
        if(typeof texture !== "undefined") {
            loader = new THREE.TextureLoader();
        }
        loader.load(path, (item) => resolve(item));
    })
}
const randomInteger = function(min, max) {
    return Math.floor(Math.random() * (max - min + 1)) + min;
}
// -- End Helper Functions

const config = {
    individualItem: '.album-item', // class of individual item
    carouselWidth: 1000, // in px
    carouselId: '#album-rotator', // carousel selector
    carouselHolderId: '#album-rotator-holder', // carousel should be <div id="carouselId"><div id="carouselHolderId">{items}</div></div>
    colors: [
        // Define colors for each item. If more items than colors, then first color will be used as default
        // Format { low: rgb(), high: rgb() for each color }
        { low: rgb(0, 114, 255), high: rgb(48, 0, 255) },
        { low: rgb(236, 166, 15), high: rgb(233, 104, 0) },
        { low: rgb(43, 75, 235), high: rgb(213, 51, 248) },
        { low: rgb(175, 49, 49), high: rgb(123, 16, 16) }
    ]
}

// Async function for generating webGL waves
const createWave = async function(selector, colors) {      
    if(document.querySelectorAll(selector) !== null && document.querySelectorAll(selector).length > 0) {
        // Import all the fragment and vertex shaders
        const noise = document.getElementById('noise').textContent;
        const fragment = document.getElementById('fragment').textContent
        const vertex =document.getElementById('vertex').textContent
        let i = 0;
        // For each of the selector elements
        document.querySelectorAll(selector).forEach(function(item) {
            // Create a renderer
            
            const newCanvas = document.createElement('canvas');
            newCanvas.id = `canvas-${i}`;
            item.appendChild(newCanvas);
            
            const renderer = new THREE.WebGLRenderer({
                powerPreference: "high-performance",
                antialias: true, 
                alpha: true,
                canvas: document.getElementById(`canvas-${i}`)
            });


            // Get el width and height
            const elWidth = parseFloat(window.getComputedStyle(item).width);
            const elHeight = parseFloat(window.getComputedStyle(item).height);

            // Set sizes and set scene/camera
            renderer.setSize( elWidth, elHeight );
            renderer.setPixelRatio( window.devicePixelRatio );

            const scene = new THREE.Scene();
            const camera = new THREE.PerspectiveCamera( 75, elWidth / elHeight, 0.1, 1000 );

            // Check on colors to use
            let high = colors[0].high; 
            let low = colors[0].low;
            if(typeof colors[i] !== "undefined") {
                high = colors[i].high;
                low = colors[i].low;
                ++i;
            }

            // And use the high color for the subtext.
            if(item.querySelector('.subtext') !== null) {
                item.querySelector('.subtext').style.background = `rgba(${high.x},${high.y},${high.z},0.75)`;
            }

            // Create a plane, and pass that through to our shaders
            let geometry = new THREE.PlaneGeometry(600, 600, 100, 100);
            let material = new THREE.ShaderMaterial({
                uniforms: {
                    u_lowColor: {type: 'v3', value: low },
                    u_highColor: {type: 'v3', value: high },
                    u_time: {type: 'f', value: 0},
                    u_height: {type: 'f', value: 1},
                    u_rand: {type: 'f', value: new THREE.Vector2(randomInteger(6, 10), randomInteger(8, 10)) }
                },
                fragmentShader: noise + fragment,
                vertexShader: noise + vertex,
            });

            // Create the mesh and position appropriately
            let mesh = new THREE.Mesh(geometry, material);
            mesh.position.set(0, 0, -300);
            mesh.material.needsUpdate = true;
            scene.add(mesh);
            
            // On hover effects for each item
            let enterTimer, exitTimer;
            item.addEventListener('mouseenter', function(e) {
                if(typeof exitTimer !== "undefined") {
                    clearTimeout(exitTimer);
                }
                enterTimer = setInterval(function() {
                    if(mesh.material.uniforms.u_height.value >= 0.5) {
                        mesh.material.uniforms.u_height.value -= 0.05;
                    } else {
                        clearTimeout(enterTimer);
                    }
                }, 10);
            });
            item.addEventListener('mouseleave', function(e) {
                if(typeof enterTimer !== "undefined") {
                    clearTimeout(enterTimer);
                }
                exitTimer = setInterval(function() {
                    if(mesh.material.uniforms.u_height.value < 1) {
                        mesh.material.uniforms.u_height.value += 0.05;
                    } else {
                        clearTimeout(exitTimer);
                    }
                }, 10);
            });

            // Render
            renderer.render( scene, camera );
            let t = 0;

            // Animate
            const animate = function () {
              requestAnimationFrame( animate );
                renderer.render( scene, camera );
                mesh.material.uniforms.u_time.value = t;
                t = t + 0.02;
            };
            animate();
        });
    }
}

document.addEventListener("DOMContentLoaded", function(e) {
    createWave(config.individualItem, config.colors);

    // Get items
    const el = document.querySelector(config.individualItem);
    const elWidth = parseFloat(window.getComputedStyle(el).width) + parseFloat(window.getComputedStyle(el).marginLeft) + parseFloat(window.getComputedStyle(el).marginRight);
    
    // Track carousel
    let mousedown = false;
    let movement = false;
    let initialPosition = 0;
    let selectedItem;
    let currentDelta = 0;

    document.querySelectorAll(config.carouselId).forEach(function(item) { 
        item.style.width = `${config.carouselWidth}px`;
    });
    
    document.querySelectorAll(config.carouselId).forEach(function(item) {
        item.addEventListener('pointerdown', function(e) {
            mousedown = true;
            selectedItem = item;
            initialPosition = e.pageX;
            currentDelta = parseFloat(item.querySelector(config.carouselHolderId).style.transform.split('translateX(')[1]) || 0;
        }); 
    });
    
    const scrollCarousel = function(change, currentDelta, selectedItem) {
        let numberThatFit = Math.floor(config.carouselWidth / elWidth);
        let newDelta = currentDelta + change;
        let elLength = selectedItem.querySelectorAll(config.individualItem).length - numberThatFit;
        if(newDelta <= 0 && newDelta >= -elWidth * elLength) {
            selectedItem.querySelector(config.carouselHolderId).style.transform = `translateX(${newDelta}px)`;
        } else {
            if(newDelta <= -elWidth * elLength) {
                selectedItem.querySelector(config.carouselHolderId).style.transform = `translateX(${-elWidth * elLength}px)`;
            } else if(newDelta >= 0) {
                selectedItem.querySelector(config.carouselHolderId).style.transform = `translateX(0px)`;
            }
        }
    }

    document.body.addEventListener('pointermove', function(e) {
        if(mousedown == true && typeof selectedItem !== "undefined") {
            let change = -(initialPosition - e.pageX);
            scrollCarousel(change, currentDelta, document.body);
            document.querySelectorAll(`${config.carouselId} a`).forEach(function(item) {
                item.style.pointerEvents = 'none';
            });
            movement = true;
        }
    });
    
    ['pointerup', 'mouseleave'].forEach(function(item) {
        document.body.addEventListener(item, function(e) {
            selectedItem = undefined;
            movement = false;
            document.querySelectorAll(`${config.carouselId} a`).forEach(function(item) {
                item.style.pointerEvents = 'all';
            });
        });
    });

    document.querySelectorAll(config.carouselId).forEach(function(item) {
        let trigger = 0;
        item.addEventListener('wheel', function(e) {
            if(trigger !== 1) {
                ++trigger;
            } else {
                let change = e.deltaX * -3;
                let currentDelta = parseFloat(item.querySelector(config.carouselHolderId).style.transform.split('translateX(')[1]) || 0;
                scrollCarousel(change, currentDelta, item);
                trigger = 0;
            }
            e.preventDefault();
            e.stopImmediatePropagation();
            return false;
        });
    });
});

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

This Pen doesn't use any external JavaScript resources.