
Wavy 3D Blob Animation
Step-by-Step Setup Guide
This tutorial will walk you through creating a mesmerizing 3D animated blob that responds to scroll events. The blob uses custom WebGL shaders to create smooth distortion effects.
What You'll Learn
- How to implement Three.js in Webflow
 - Creating custom shader materials
 - Scroll-triggered animations
 - WebGL rendering optimization
 
Prerequisites
- Basic understanding of Webflow
 - Familiarity with custom code embedding
 
Step 1: Add the Required Libraries
&<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>&
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/gsap.min.js"></script>
First, you need to add the Three.js library to your project. In your Webflow project settings:
- Go to Project Settings > Custom Code
 - Add this to the Head Code section:
 
```js
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/gsap.min.js"></script>
```
Step 2: Create the HTML Structure
copy paste this elemnet into your webflow designer
[c-gsap-dots]
Step 3: Add the CSS Styles
&<style>&
    .blob-component { 
        width: 100%; 
        height: 100vh; 
        position: relative; 
        overflow: hidden; 
    }
    .blob {
        width: 100%;
        height: 100%;
        position: relative;
        z-index: 1;
    }
    canvas { 
        position: absolute; 
        top: 0; 
        left: 0; 
        width: 100%; 
        height: 100%; 
    }
</style>
Add this CSS to your Project Settings > Custom Code > Head Code:
```CSS
<style>
    .blob-component { 
        width: 100%; 
        height: 100vh; 
        position: relative; 
        overflow: hidden; 
    }
    .blob {
        width: 100%;
        height: 100%;
        position: relative;
        z-index: 1;
    }
    canvas { 
        position: absolute; 
        top: 0; 
        left: 0; 
        width: 100%; 
        height: 100%; 
    }
</style>
```
Step 4: Add the JavaScript Code
&const vertexShader = `  &
varying vec2 vUv;  
varying float vDistortion;  
uniform float uTime;  
uniform float uFrequency;  
uniform float uAmplitude;  
uniform float uSpeed;  
void main() {    
    vUv = uv;    
    vec3 pos = position;    
    float distortion = sin(pos.x * uFrequency + uTime * uSpeed) *                       
                      sin(pos.y * uFrequency + uTime * uSpeed) *                       
                      sin(pos.z * uFrequency + uTime * uSpeed) *                       
                      uAmplitude;    
    pos += normal * distortion;    
    vDistortion = distortion;    
    gl_Position = projectionMatrix * modelViewMatrix * vec4(pos, 1.0);  
}`;
const fragmentShader = `  
varying vec2 vUv;  
varying float vDistortion;  
uniform vec3 uLowColor;  
uniform vec3 uHighColor;  
void main() {    
    vec3 color = mix(uLowColor, uHighColor, vDistortion * 0.5);    
    gl_FragColor = vec4(color, 1.0);  
}`;
let scene, camera, renderer, blob, blobMaterial;
function handleScroll() {  
    if (!blob || !blobMaterial) return;    
    const scrollTrigger = document.querySelector('.blob-component');  
    if (!scrollTrigger) return;  
    
    const rect = scrollTrigger.getBoundingClientRect();  
    const windowHeight = window.innerHeight;    
    let scrollProgress = 0;    
    
    if (rect.top <= windowHeight && rect.bottom >= 0) {    
        scrollProgress = (windowHeight - rect.top) / (windowHeight + rect.height);    
        scrollProgress = Math.min(Math.max(scrollProgress, 0), 1);  
    } else if (rect.top > windowHeight) {    
        scrollProgress = 0;  
    } else if (rect.bottom < 0) {    
        scrollProgress = 1;  
    }    
    
    blobMaterial.uniforms.uFrequency.value = 0.3 + (scrollProgress * 0.7);  
    blobMaterial.uniforms.uAmplitude.value = 0.5 + (scrollProgress * 2.5);  
    blobMaterial.uniforms.uSpeed.value = 0.5 + (scrollProgress * 2.0);
}
function handleResize() {  
    if (!camera || !renderer) return;    
    const container = document.querySelector('.blob');  
    if (!container) return;  
    
    camera.aspect = container.clientWidth / container.clientHeight;  
    camera.updateProjectionMatrix();  
    renderer.setSize(container.clientWidth, container.clientHeight);
}
function animate() {  
    if (!scene || !camera || !renderer || !blobMaterial || !blob) return;    
    requestAnimationFrame(animate);  
    
    blobMaterial.uniforms.uTime.value += 0.01;  
    blob.rotation.y = window.scrollY * 0.001;  
    renderer.render(scene, camera);
}
function cleanup() {  
    if (scene) {    
        window.removeEventListener('scroll', handleScroll);    
        window.removeEventListener('resize', handleResize);    
        scene.remove(blob);    
        blob.geometry.dispose();    
        blobMaterial.dispose();    
        renderer.dispose();    
        scene = null;    
        camera = null;    
        renderer = null;    
        blob = null;    
        blobMaterial = null;  
    }
}
function initBlob() {  
    if (scene) return;    
    const container = document.querySelector('.blob');  
    if (!container) return;  
    
    container.style.position = 'relative';  
    container.style.zIndex = '1';  
    container.style.overflow = 'hidden';  
    
    scene = new THREE.Scene();  
    camera = new THREE.PerspectiveCamera(75, container.clientWidth / container.clientHeight, 0.1, 1000);  
    renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });    
    renderer.setSize(container.clientWidth, container.clientHeight);  
    container.appendChild(renderer.domElement);  
    
    blobMaterial = new THREE.ShaderMaterial({    
        uniforms: {      
            uTime: { value: 0 },      
            uFrequency: { value: 0.3 },      
            uAmplitude: { value: 0.5 },      
            uSpeed: { value: 0.5 },      
            uLowColor: { value: new THREE.Color('#AC70F3') },      
            uHighColor: { value: new THREE.Color('#CDA4FF') }    
        },    
        vertexShader,    
        fragmentShader  
    });  
    
    const geometry = new THREE.SphereGeometry(4, 128, 128);  
    blob = new THREE.Mesh(geometry, blobMaterial);  
    scene.add(blob);  
    camera.position.z = 10;  
    
    window.addEventListener('scroll', handleScroll);  
    window.addEventListener('resize', handleResize);  
    window.addEventListener('beforeunload', cleanup);  
    animate();
}
document.addEventListener('DOMContentLoaded', initBlob);
Add this JavaScript code to your Project Settings > Custom Code > Before </body> tag:
```js
const vertexShader = `  
varying vec2 vUv;  
varying float vDistortion;  
uniform float uTime;  
uniform float uFrequency;  
uniform float uAmplitude;  
uniform float uSpeed;  
void main() {    
    vUv = uv;    
    vec3 pos = position;    
    float distortion = sin(pos.x * uFrequency + uTime * uSpeed) *                       
                      sin(pos.y * uFrequency + uTime * uSpeed) *                       
                      sin(pos.z * uFrequency + uTime * uSpeed) *                       
                      uAmplitude;    
    pos += normal * distortion;    
    vDistortion = distortion;    
    gl_Position = projectionMatrix * modelViewMatrix * vec4(pos, 1.0);  
}`;
const fragmentShader = `  
varying vec2 vUv;  
varying float vDistortion;  
uniform vec3 uLowColor;  
uniform vec3 uHighColor;  
void main() {    
    vec3 color = mix(uLowColor, uHighColor, vDistortion * 0.5);    
    gl_FragColor = vec4(color, 1.0);  
}`;
let scene, camera, renderer, blob, blobMaterial;
function handleScroll() {  
    if (!blob || !blobMaterial) return;    
    const scrollTrigger = document.querySelector('.blob-component');  
    if (!scrollTrigger) return;  
    
    const rect = scrollTrigger.getBoundingClientRect();  
    const windowHeight = window.innerHeight;    
    let scrollProgress = 0;    
    
    if (rect.top <= windowHeight && rect.bottom >= 0) {    
        scrollProgress = (windowHeight - rect.top) / (windowHeight + rect.height);    
        scrollProgress = Math.min(Math.max(scrollProgress, 0), 1);  
    } else if (rect.top > windowHeight) {    
        scrollProgress = 0;  
    } else if (rect.bottom < 0) {    
        scrollProgress = 1;  
    }    
    
    blobMaterial.uniforms.uFrequency.value = 0.3 + (scrollProgress * 0.7);  
    blobMaterial.uniforms.uAmplitude.value = 0.5 + (scrollProgress * 2.5);  
    blobMaterial.uniforms.uSpeed.value = 0.5 + (scrollProgress * 2.0);
}
function handleResize() {  
    if (!camera || !renderer) return;    
    const container = document.querySelector('.blob');  
    if (!container) return;  
    
    camera.aspect = container.clientWidth / container.clientHeight;  
    camera.updateProjectionMatrix();  
    renderer.setSize(container.clientWidth, container.clientHeight);
}
function animate() {  
    if (!scene || !camera || !renderer || !blobMaterial || !blob) return;    
    requestAnimationFrame(animate);  
    
    blobMaterial.uniforms.uTime.value += 0.01;  
    blob.rotation.y = window.scrollY * 0.001;  
    renderer.render(scene, camera);
}
function cleanup() {  
    if (scene) {    
        window.removeEventListener('scroll', handleScroll);    
        window.removeEventListener('resize', handleResize);    
        scene.remove(blob);    
        blob.geometry.dispose();    
        blobMaterial.dispose();    
        renderer.dispose();    
        scene = null;    
        camera = null;    
        renderer = null;    
        blob = null;    
        blobMaterial = null;  
    }
}
function initBlob() {  
    if (scene) return;    
    const container = document.querySelector('.blob');  
    if (!container) return;  
    
    container.style.position = 'relative';  
    container.style.zIndex = '1';  
    container.style.overflow = 'hidden';  
    
    scene = new THREE.Scene();  
    camera = new THREE.PerspectiveCamera(75, container.clientWidth / container.clientHeight, 0.1, 1000);  
    renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });    
    renderer.setSize(container.clientWidth, container.clientHeight);  
    container.appendChild(renderer.domElement);  
    
    blobMaterial = new THREE.ShaderMaterial({    
        uniforms: {      
            uTime: { value: 0 },      
            uFrequency: { value: 0.3 },      
            uAmplitude: { value: 0.5 },      
            uSpeed: { value: 0.5 },      
            uLowColor: { value: new THREE.Color('#AC70F3') },      
            uHighColor: { value: new THREE.Color('#CDA4FF') }    
        },    
        vertexShader,    
        fragmentShader  
    });  
    
    const geometry = new THREE.SphereGeometry(4, 128, 128);  
    blob = new THREE.Mesh(geometry, blobMaterial);  
    scene.add(blob);  
    camera.position.z = 10;  
    
    window.addEventListener('scroll', handleScroll);  
    window.addEventListener('resize', handleResize);  
    window.addEventListener('beforeunload', cleanup);  
    animate();
}
document.addEventListener('DOMContentLoaded', initBlob);
```
You can copy-paste this component directly into your Webflow project and style it however you want.
[ag-0-d-This is the description]
[ag-0-a-name = value]
[ag-0-a-name = value]- This is the description[li-a-name = value]Click to Copy[li-a-name = value]
 - This is the description[li-a-name = value][li-a-name = value]
 - This is the description[li-a-name = value][li-a-name = value]
 
Quick Setup:
You can copy-paste this component directly into your Webflow project and style it however you want.
[c-Final - First Bento Grid]
Required Attributes:
- this is the head. should it be the first item or should it be the first heading before the list?
 - data-flow = card: Defines the card element. The glow is inside this div.
 - data-flow-color-border = #your-color: Sets the border glow color. Accepts CSS variables, functions, or static color values.
 - data-flow-color-card = #your-color: Sets the inner glow color. Accepts CSS variables, functions, or static color values.
 
Optional Settings:
Use variables and the color-mix function. You can set the card’s inner glow to something like: [a-data-flow-color-card = "color-mix(in srgb, var(--pink) 20%, transparent)"]. Here we are using a color variable, which can be the same as the border  [a-data-flow-color = "transparent)"] color, but set it to 20% opacity with the color-mix function.
In the code, there are two default values that you can change if you want.









