
why this text is not wrapping - oh it is now
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.









