Let’s create an On-Scroll Fire Transition effect using HTML, CSS, JavaScript, WebGL, and GSAP’s ScrollTrigger. This project will create a fiery transition effect that activates as the user scrolls down the page, providing an engaging and dynamic user experience.
We’ll use HTML for the structure, CSS for basic styling, JavaScript to integrate WebGL for the fire effect, and GSAP’s ScrollTrigger to control the animations based on the scroll position.
Let’s dive into building the On-Scroll Fire Transition effect. Whether you’re a beginner or an experienced developer, this project offers a challenging and rewarding way to enhance your web development skills and create an immersive user experience. Let’s ignite some creativity and make scrolling more exciting!
HTML :
This HTML code creates a web page with a dynamic, full-screen canvas that displays a fire transition effect when scrolling. The canvas uses WebGL and fragment shaders to control pixel colors and opacities based on inputs like scroll position, time, and screen dimensions. Perlin Noise is utilized to create smooth, organic patterns for the fire effect. The GSAP ScrollTrigger library is used to manage the scrolling animations. Additionally, links to Anime.js and other resources are provided for further reference. The overall setup involves minimal JavaScript for rendering and doesn’t rely on additional libraries for the core functionality.
<!DOCTYPE html> <html lang="en" > <head> <meta charset="UTF-8"> <title>On-Scroll Fire Transition (WebGL + GSAP ScrollTrigger)</title> <link rel="stylesheet" href="./style.css"> </head> <body> <!-- partial:index.partial.html --> <div class="page"> <div class="header"> The Magic Behind It </div> <div class="content"> <p> This HTML page you're viewing is enhanced with a full-screen <b><canvas></b> element. A fragment shader runs on this canvas, determining the color and opacity of each pixel. The shader takes several inputs: <b>scroll position (or animation state)</b>, <b>time</b>, and <b>screen dimensions</b>. </p> <p> Gathering <b>time</b> and <b>screen dimensions</b> is straightforward, but for <b>animation state</b> we leverage the <a href="https://animejs.com/documentation/" target="_blank">Anime.js</a> library. </p> <p> With these inputs ready, we feed them into the shader as uniforms. The WebGL component of this demo uses a minimal JS setup to render the fragment shader on a single, full-screen plane. No additional libraries are required. </p> <p> The shader utilizes <a href="https://thebookofshaders.com/12/" target="_blank">Perlin Noise</a> to create dynamic visual effects. </p> <p> Initially, we generate a semi-transparent mask to shape a glowing effect. This mask is created with low-scale Perlin Noise and uses the <b>scroll position</b> as a threshold. By adjusting the thresholds on the same Perlin Noise, we can <br> (a) gradually dim parts of the screen, darkening pixels before they become transparent <br> (b) create a border effect along the edges, which we use to apply a glowing outline </p> <p> The glow itself is produced using two Perlin Noise functions - one for the shape and one for the color. Both functions operate on a larger scale and are animated with the <b>time</b> input instead of the <b>scroll position</b>. </p> <p class="last-line"> <a href="https://www.linkedin.com/in/johndoe/" target="_blank">LinkedIn</a> | <a href="https://codepen.io/johndoe" target="_blank">CodePen</a> | <a href="https://twitter.com/johndoe" target="_top">Twitter (X)</a> </p> </div> </div> <canvas id="fire-overlay"></canvas> <div class="scroll-msg"> <div>Hello 👋</div> <div>scroll me</div> <div class="arrow-animated">↓</div> </div> <script type="x-shader/x-fragment" id="vertShader"> precision mediump float; varying vec2 vUv; attribute vec2 a_position; void main() { vUv = a_position; gl_Position = vec4(a_position, 0.0, 1.0); } </script> <script type="x-shader/x-fragment" id="fragShader"> precision mediump float; varying vec2 vUv; uniform vec2 u_resolution; uniform float u_progress; uniform float u_time; float rand(vec2 n) { return fract(cos(dot(n, vec2(12.9898, 4.1414))) * 43758.5453); } float noise(vec2 n) { const vec2 d = vec2(0., 1.); 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 fbm(vec2 n) { float total = 0.0, amplitude = .4; for (int i = 0; i < 4; i++) { total += noise(n) * amplitude; n += n; amplitude *= 0.6; } return total; } void main() { vec2 uv = vUv; uv.x *= min(1., u_resolution.x / u_resolution.y); uv.y *= min(1., u_resolution.y / u_resolution.x); float t = u_progress; vec3 color = vec3(1., 1., .95); float main_noise = 1. - fbm(.75 * uv + 10. - vec2(.3, .9 * t)); float paper_darkness = smoothstep(main_noise - .1, main_noise, t); color -= vec3(.99, .95, .99) * paper_darkness; vec3 fire_color = fbm(6. * uv - vec2(0., .005 * u_time)) * vec3(6., 1.4, .0); float show_fire = smoothstep(.4, .9, fbm(10. * uv + 2. - vec2(0., .005 * u_time))); show_fire += smoothstep(.7, .8, fbm(.5 * uv + 5. - vec2(0., .001 * u_time))); float fire_border = .02 * show_fire; float fire_edge = smoothstep(main_noise - fire_border, main_noise - .5 * fire_border, t); fire_edge *= (1. - smoothstep(main_noise - .5 * fire_border, main_noise, t)); color += fire_color * fire_edge; float opacity = 1. - smoothstep(main_noise - .0005, main_noise, t); gl_FragColor = vec4(color, opacity); } </script> <!-- partial --> <script src='https://unpkg.com/gsap@3/dist/gsap.min.js'></script> <script src='https://unpkg.com/gsap@3/dist/ScrollTrigger.min.js'></script><script src="./script.js"></script> </body> </html>
CSS :
This CSS styles an HTML page with a full-screen fire effect. The main components include the body, page, scroll message, and canvas. The body and html elements are reset to have no margins or padding, and a sans-serif font is used. Links inherit the default color.
The .page
class centers its content and covers the entire viewport height with a minimum height of 180vh. The header is centered with uppercase text and a large font size. The content section is limited to 800px in width with padding for spacing. Selected text has a custom highlight color.
The .scroll-msg
section is fixed and centered on the screen, showing a scroll prompt. The canvas with ID fire-overlay
is fixed, covering the full screen to display the fire effect. The animated arrow below the scroll message bounces up and down continuously.
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:
This JavaScript code animates a fire effect on a WebGL canvas element based on scrolling using GSAP. It sets up the canvas and shaders, adjusts the fire effect parameters as the user scrolls, and ensures the canvas resizes correctly with the window. The animation is smoothly rendered in real-time by continuously updating the canvas with requestAnimationFrame
.
const canvasEl = document.querySelector("#fire-overlay"); const scrollMsgEl = document.querySelector(".scroll-msg"); const devicePixelRatio = Math.min(window.devicePixelRatio, 2); // const devicePixelRatio = 1; const params = { fireTime: .35, fireTimeAddition: 0 } let st, uniforms; const gl = initShader(); st = gsap.timeline({ scrollTrigger: { trigger: ".page", start: "0% 0%", end: "100% 100%", // markers: true, scrub: true, onLeaveBack: () => { // params.fireTimeAddition = Math.min(params.fireTimeAddition, .3); // gsap.to(params, { // duration: .75, // fireTimeAddition: 0 // }) }, }, }) .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); // if (st.scrollTrigger.isActive && st.scrollTrigger.direction === 1) { // params.fireTimeAddition += .001; // } 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(); }
In conclusion, creating an On-Scroll Fire Transition using HTML, CSS, JavaScript, WebGL, and GSAP’s ScrollTrigger has been a thrilling and educational project. By combining these technologies, we’ve crafted a dynamic and visually striking effect that enhances user engagement as they scroll through the page. This project showcases the power of WebGL and GSAP in creating advanced animations and interactions for modern web designs.
If your project has problems, don’t worry. Just click to download the source code and face your coding challenges with excitement. Have fun coding!