Introduction
Web animations have evolved significantly over the years, providing developers with powerful tools to create engaging and dynamic user experiences. One such captivating effect is the on-scroll fire transition, which leverages the power of WebGL and GSAP ScrollTrigger. This tutorial will guide you through the process of creating an impressive fire transition effect that animates as users scroll through a webpage.

Understanding the Components
Before diving into the code, it’s essential to understand the key components involved in creating this effect:
- HTML and CSS: These form the backbone of our webpage, providing structure and styling.
- WebGL: A JavaScript API used for rendering 2D and 3D graphics within any compatible web browser.
- GSAP (GreenSock Animation Platform): A powerful JavaScript library for creating high-performance animations.
- ScrollTrigger: A plugin for GSAP that allows you to create animations based on the user’s scroll position.
- Shaders: Programs written in GLSL (OpenGL Shading Language) that run on the GPU to perform rendering effects.
HTML Structure
The HTML structure for our fire transition effect is straightforward. We have a main container .page
, a header, content paragraphs, a canvas element for the WebGL rendering, and a scroll message to prompt users to scroll.
On-Scroll Fire Transition (WebGL + GSAP ScrollTrigger)
How it's done
The HTML content you're reading right now is overlaid with a full-screen <canvas> element. There is a fragment shader that defines opacity and color for each pixel of the <canvas>. Shader input values are scroll progress (aka animation progress), time, and resolution.
While time and window size (resolution) are super easy to gather, for animation progress I use GSAP ScrollTrigger plugin.
Once the inputs are prepared, we pass them as uniforms to the shader. The WebGL part of this demo is a basic JS boilerplate to render a fragment shader on the single full-screen plane. No extra libraries here.
The fragment shader is based on Fractal Brownian Motion (fBm) noise.
First, we create a semi-transparent mask to define a contour of burning paper. It is basically a low-scale fBm noise with scroll progress value used as a threshold. Taking the same fBm noise with different thresholds we can (a) darken parts of the paper so each pixel gets darker before turning transparent (b) define the stripe along the paper edge and use it as a mask for flames.
The fire is done as another two fBm based functions - one for shape and one for color. Both have a much higher scale and both are animated with time value instead of scroll progress.
Hello I Am Shobhit Dev 👋
scroll me
↓
CSS Styling
The CSS defines the visual appearance of our webpage and ensures the content is displayed correctly. It also handles animations for the scroll prompt.
body, html {
margin: 0;
padding: 0;
font-family: sans-serif;
font-size: 20px;
color: #3d3d3d;
}
a {
color: inherit;
}
.page {
width: 100%;
min-height: 180vh;
display: flex;
flex-direction: column;
align-items: center;
opacity: 0;
}
.page .header {
display: flex;
align-items: center;
justify-content: center;
font-size: 40px;
text-transform: uppercase;
width: 100vw;
margin-top: 20vh;
height: 25vh;
}
.page .content {
max-width: 800px;
padding: 10px;
}
.page .last-line {
text-align: right;
padding-top: 1em;
}
.page ::-moz-selection {
background: #F7C02D;
}
.page ::selection {
background: #F7C02D;
}
.scroll-msg {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
pointer-events: none;
padding-top: 2em;
}
.scroll-msg > div:nth-child(1) {
margin-top: -10vh;
padding-bottom: 1em;
text-transform: uppercase;
font-size: 2em;
}
canvas#fire-overlay {
position: fixed;
top: 0;
left: 0;
display: block;
width: 100%;
pointer-events: none;
}
.arrow-animated {
font-size: 1em;
animation: arrow-float 1s infinite;
}
@keyframes arrow-float {
0% {
transform: translateY(0);
animation-timing-function: ease-out;
}
60% {
transform: translateY(50%);
animation-timing-function: ease-in-out;
}
100% {
transform: translateY(0);
animation-timing-function: ease-out;
}
}
JavaScript for WebGL and GSAP ScrollTrigger
The JavaScript code brings our effect to life. It initializes the WebGL context, sets up shaders, and defines the animation logic using GSAP ScrollTrigger.
const canvasEl = document.querySelector("#fire-overlay");
const scrollMsgEl = document.querySelector(".scroll-msg");
const devicePixelRatio = Math.min(window.devicePixelRatio, 2);
const params = {
fireTime: .35,
fireTimeAddition: 0
}
let st, uniforms;
const gl = initShader();
st = gsap.timeline({
scrollTrigger: {
trigger: ".page",
start: "0% 0%",
end: "100% 100%",
scrub: true,
},
})
.to(scrollMsgEl, {
duration: .1,
opacity: 0
}, 0)
.to(params, {
fireTime: .63
}, 0)
window.addEventListener("resize", resizeCanvas);
resizeCanvas();
gsap.set(".page", {
opacity: 1
})
function initShader() {
const vsSource = document.getElementById("vertShader").innerHTML;
const fsSource = document.getElementById("fragShader").innerHTML;
const gl = canvasEl.getContext("webgl") || canvasEl.getContext("experimental-webgl");
if (!gl) {
alert("WebGL is not supported by your browser.");
}
function createShader(gl, sourceCode, type) {
const shader = gl.createShader(type);
gl.shaderSource(shader, sourceCode);
gl.compileShader(shader);
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
console.error("An error occurred compiling the shaders: " + gl.getShaderInfoLog(shader));
gl.deleteShader(shader);
return null;
}
return shader;
}
const vertexShader = createShader(gl, vsSource, gl.VERTEX_SHADER);
const fragmentShader = createShader(gl, fsSource, gl.FRAGMENT_SHADER);
function createShaderProgram(gl, vertexShader, fragmentShader) {
const program = gl.createProgram();
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
gl.linkProgram(program);
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
console.error("Unable to initialize the shader program: " + gl.getProgramInfoLog(program));
return null;
}
return program;
}
const shaderProgram = createShaderProgram(gl, vertexShader, fragmentShader);
uniforms = getUniforms(shaderProgram);
function getUniforms(program) {
let uniforms = [];
let uniformCount = gl.getProgramParameter(program, gl.ACTIVE_UNIFORMS);
for (let i = 0; i < uniformCount; i++) {
let uniformName = gl.getActiveUniform(program, i).name;
uniforms[uniformName] = gl.getUniformLocation(program, uniformName);
}
return uniforms;
}
const vertices = new Float32Array([-1., -1., 1., -1., -1., 1., 1., 1.]);
const vertexBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);
gl.useProgram(shaderProgram);
const positionLocation = gl.getAttribLocation(shaderProgram, "a_position");
gl.enableVertexAttribArray(positionLocation);
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
gl.vertexAttribPointer(positionLocation, 2, gl.FLOAT, false, 0, 0);
return gl;
}
function render() {
const currentTime = performance.now();
gl.uniform1f(uniforms.u_time, currentTime);
gl.uniform1f(uniforms.u_progress, params.fireTime + params.fireTimeAddition);
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
requestAnimationFrame(render);
}
function resizeCanvas() {
canvasEl.width = window.innerWidth * devicePixelRatio;
canvasEl.height = window.innerHeight * devicePixelRatio;
gl.viewport(0, 0, canvasEl.width, canvasEl.height);
gl.uniform2f(uniforms.u_resolution, canvasEl.width, canvasEl.height);
render();
}
Key Concepts
WebGL Shaders
Shaders are essential for rendering effects in WebGL. We use two types of shaders:
- Vertex Shader: Handles the processing of individual vertices.
- Fragment Shader: Computes the color and other attributes of each pixel.
In our example, the fragment shader is where the magic happens. It uses fractal Brownian motion (fBm) to create noise patterns that simulate the fire effect.
GSAP and ScrollTrigger
GSAP (GreenSock Animation Platform) is a powerful library for creating animations. ScrollTrigger, a plugin for GSAP, allows us to control animations based on the user’s scroll position. This is crucial for our on-scroll fire transition effect.
How It Works
- HTML Structure: The main content is overlaid with a full-screen
<canvas>
element, which will be used for rendering the fire effect. - CSS Styling: Ensures the content is styled correctly and adds animations for the scroll prompt.
- WebGL Initialization: Sets up the WebGL context, compiles shaders, and links them into a shader program.
- GSAP ScrollTrigger: Creates a timeline that updates the fire effect’s progress based on the user’s scroll position.
- Rendering Loop: Continuously updates the canvas with new frames, creating a smooth animation.
Conclusion
Creating an on-scroll fire transition effect using WebGL and GSAP ScrollTrigger is a fantastic way to enhance your website’s interactivity and visual appeal. By understanding the key components and following the provided code, you can implement this effect on your own site, providing users with a memorable and engaging experience.
This tutorial covers the basics, but there’s plenty of room for experimentation and customization. Feel free to tweak the shaders, adjust the animation timings, and add your unique touch to the effect. The possibilities with WebGL and GSAP are endless, so let your creativity run wild!