🔥 Description
A smooth scroll-triggered text reveal effect built using GSAP, combined with a hover-activated clip-path animation for an interactive, modern UI. Perfect for portfolio websites, hero sections, and eye-catching text animations.
🧠 How the Component Works
🔹 HTML Explanation
The HTML contains multiple heading elements wrapped inside a container.
Each heading has a span inside it — this span is used for the hover reveal animation.
External fonts, stylesheets, and GSAP libraries are included through CDN links, with a clean document structure.
🔹 CSS Explanation
The CSS styles create a full-screen vertical layout with large text.
The text is initially transparent but has a background gradient clipped to the text, giving a reveal effect when animated.
Each span is positioned on top of the text using absolute positioning and clipped using clip-path, which expands on hover to reveal a colored overlay.
🔹 JavaScript Explanation
The JavaScript uses GSAP’s ScrollTrigger plugin to animate the background clipping as the user scrolls. Each text element increases its background size based on scroll position. The animation is smooth, synchronized with scroll, and triggers individually for each text block.
1<!DOCTYPE html>
2<html lang="en">
3
4 <head>
5 <meta charset="UTF-8">
6 <title>three.js + HexaVision Pass</title>
7
8
9 </head>
10
11 <body>
12 <html lang="en">
13 <head>
14 <meta charset="UTF-8" />
15 <title>three.js + HexaVision Pass</title>
16 <style>
17 body {
18 margin: 0;
19 overflow: hidden;
20 background: radial-gradient(#000, #111);
21 user-select: none;
22 -webkit-user-select: none;
23 -moz-user-select: none;
24 -ms-user-select: none;
25 }
26 canvas {
27 display: block;
28 }
29 .note {
30 position: absolute;
31 left: 12px;
32 top: 12px;
33 color: #ddd;
34 font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial;
35 font-size: 13px;
36 background: rgba(0, 0, 0, 0.5);
37 padding: 8px 10px;
38 border-radius: 6px;
39 backdrop-filter: blur(4px);
40 z-index: 10;
41 }
42 #notification {
43 position: fixed;
44 bottom: -100px;
45 left: 50%;
46 transform: translateX(-50%);
47 background: rgba(0, 0, 0, 0.7);
48 color: #fff;
49 font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial;
50 font-size: 13px;
51 padding: 12px 18px;
52 border-radius: 8px;
53 backdrop-filter: blur(6px);
54 display: flex;
55 align-items: center;
56 gap: 12px;
57 z-index: 999;
58 }
59 #notification a {
60 color: orange;
61 text-decoration: none;
62 font-weight: 500;
63 transition: color 0.3s ease;
64 }
65
66 #notification a:hover {
67 color: #ffb347; /* un orange plus clair au survol */
68 }
69
70 #notification button {
71 background: none;
72 border: none;
73 color: gray;
74 font-size: 16px;
75 cursor: pointer;
76 }
77
78 #notification button:hover {
79 background: none;
80 border: none;
81 color: white;
82 font-size: 16px;
83 cursor: pointer;
84 }
85
86 </style>
87 <script src="https://cdnjs.cloudflare.com/ajax/libs/dat-gui/0.7.9/dat.gui.min.js"></script>
88 </head>
89 <body>
90
91 <div id="app">
92 <div class="note">three.js + HexaVision Pass</div>
93 <div id="notification" class="slide-up">
94 <span>Curious for more? Check out my other creations <a href="https://codepen.io/vainsan" target="_blank" rel="noopener">here</a>
95 </span>
96 <button id="closeBtn">✕</button>
97 </div>
98 </div>
99
100 <!--
101 V2: https://codepen.io/vainsan/pen/LEGKGrY
102 -->
103
104 <script type="module">
105 import * as THREE from 'https://cdn.skypack.dev/three@0.136.0';
106 import { EffectComposer } from 'https://cdn.skypack.dev/three@0.136.0/examples/jsm/postprocessing/EffectComposer.js';
107 import { RenderPass } from 'https://cdn.skypack.dev/three@0.136.0/examples/jsm/postprocessing/RenderPass.js';
108 import { UnrealBloomPass } from 'https://cdn.skypack.dev/three@0.136.0/examples/jsm/postprocessing/UnrealBloomPass.js';
109 import { ShaderPass } from 'https://cdn.skypack.dev/three@0.136.0/examples/jsm/postprocessing/ShaderPass.js';
110 import { OrbitControls } from 'https://cdn.skypack.dev/three@0.136.0/examples/jsm/controls/OrbitControls.js';
111 import { TrackballControls } from 'https://cdn.skypack.dev/three@0.136.0/examples/jsm/controls/TrackballControls.js';
112
113 function scrambleText(selector, options = {}) {
114 const {
115 speed = 40,
116 duration = 1200,
117 characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()_+-=[]{}<>?',
118 delayBetween = 20
119 } = options;
120
121 const element = document.querySelector(selector);
122 if (!element) return;
123
124 const originalText = element.textContent;
125 const output = Array(originalText.length).fill('');
126 const resolveFrames = Array.from({ length: originalText.length }, (_, i) =>
127 Math.floor(Math.random() * (duration / speed)) + i * (delayBetween / speed)
128 );
129
130 // Masquer le texte initial
131 element.textContent = '';
132
133 let frame = 0;
134 const interval = setInterval(() => {
135 for (let i = 0; i < originalText.length; i++) {
136 if (frame >= resolveFrames[i]) {
137 output[i] = originalText[i];
138 } else {
139 output[i] = characters.charAt(Math.floor(Math.random() * characters.length));
140 }
141 }
142
143 element.textContent = output.join('');
144 frame++;
145
146 if (frame > Math.max(...resolveFrames)) {
147 clearInterval(interval);
148 element.textContent = originalText;
149 }
150 }, speed);
151 }
152
153
154 scrambleText('.note', {
155 speed: 30,
156 duration: 1500,
157 delayBetween: 30
158 });
159
160 const effectConfig = {
161 enableFlyEffect: true, // État initial
162 toggleFlyEffect: function() {
163 // Basculer l'activation du pass
164 this.enableFlyEffect = !this.enableFlyEffect;
165 if (this.enableFlyEffect) {
166 composer.addPass(flyPass);
167 } else {
168 composer.removePass(flyPass);
169 }
170 flyPass.enabled = this.enableFlyEffect;
171 }
172 };
173
174 const scene = new THREE.Scene();
175 const camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 100);
176 camera.position.z = 6;
177 const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
178 renderer.setSize(window.innerWidth, window.innerHeight);
179 renderer.setPixelRatio(window.devicePixelRatio);
180 document.body.appendChild(renderer.domElement);
181 const controls = new OrbitControls(camera, renderer.domElement);
182 controls.enableDamping = true;
183 controls.dampingFactor = 0.02;
184 controls.maxDistance = 20;
185 controls.minDistance = 0.1;
186 controls.panSpeed = 0.02;
187 controls.rotateSpeed = 0.5;
188 controls.zoomSpeed = 1;
189 controls.enableZoom = false;
190
191 const controls2 = new TrackballControls(camera, renderer.domElement);
192 controls2.noRotate = true;
193 controls2.noPan = true;
194 controls2.noZoom = false;
195 controls2.zoomSpeed = 1.5;
196
197
198 const geometry = new THREE.IcosahedronGeometry(1, 30);
199 // ShaderMaterial for the icosahedron
200 const material = new THREE.ShaderMaterial({
201 uniforms: {
202 uTime: { value: 0 },
203 uScale: { value: 6.0 },
204 uDepth: { value: 1 },
205 uSharpness: { value: 3.0 },
206 uSpeed: { value: 0 },
207 uColor: { value: new THREE.Color('#00ff00') },
208 uNoiseScale: { value: 1.5 },
209 uNoiseStrength: { value: 1.4 },
210 uOutlineWidth: { value: 0.5 },
211 uOutlineColor: { value: new THREE.Color('#0a0060') },
212 uSecondaryColor: { value: new THREE.Color('#ff005f') },
213 uDisplacementStrength: { value: 1 }
214 },
215 vertexShader: `
216 uniform float uTime;
217 uniform float uScale;
218 uniform float uSharpness;
219 uniform float uSpeed;
220 uniform float uNoiseScale;
221 uniform float uNoiseStrength;
222 uniform float uDisplacementStrength;
223 varying vec3 vNormal;
224 varying vec3 v3Position;
225 varying float vShellPattern;
226 float hash(vec2 p) {
227 return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453);
228 }
229 float noise(vec2 uv, float timeOffset) {
230 vec2 i = floor(uv);
231 vec2 f = fract(uv);
232 float a = hash(i + vec2(timeOffset));
233 float b = hash(i + vec2(1.0, 0.0) + vec2(timeOffset));
234 float c = hash(i + vec2(0.0, 1.0) + vec2(timeOffset));
235 float d = hash(i + vec2(1.0, 1.0) + vec2(timeOffset));
236 vec2 u = f * f * (3.0 - 2.0 * f);
237 return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);
238 }
239 float voronoi(vec2 uv, float t) {
240 vec2 g = floor(uv);
241 vec2 f = fract(uv);
242 float minDist1 = 1.0;
243 float secondMinDist1 = 1.0;
244 float minDist2 = 1.0;
245 float secondMinDist2 = 1.0;
246 float t0 = t;
247 float t1 = t + 1.0;
248 float a = smoothstep(0.0, 1.0, fract(t));
249 for (int y = -1; y <= 1; y++) {
250 for (int x = -1; x <= 1; x++) {
251 vec2 lattice = vec2(x, y);
252 vec2 perturbed_lattice = lattice + uNoiseStrength * (noise((g + lattice) * uNoiseScale, t0) * 2.0 - 1.0);
253 vec2 point = hash(g + perturbed_lattice) + perturbed_lattice - f;
254 float dist = length(point);
255 if (dist < minDist1) {
256 secondMinDist1 = minDist1;
257 minDist1 = dist;
258 } else if (dist < secondMinDist1) {
259 secondMinDist1 = dist;
260 }
261 }
262 }
263 for (int y = -1; y <= 1; y++) {
264 for (int x = -1; x <= 1; x++) {
265 vec2 lattice = vec2(x, y);
266 vec2 perturbed_lattice = lattice + uNoiseStrength * (noise((g + lattice) * uNoiseScale, t1) * 2.0 - 1.0);
267 vec2 point = hash(g + perturbed_lattice) + perturbed_lattice - f;
268 float dist = length(point);
269 if (dist < minDist2) {
270 secondMinDist2 = minDist2;
271 minDist2 = dist;
272 } else if (dist < secondMinDist2) {
273 secondMinDist2 = dist;
274 }
275 }
276 }
277 float pattern1 = secondMinDist1 - minDist1;
278 float pattern2 = secondMinDist2 - minDist2;
279 return mix(pattern1, pattern2, a);
280 }
281 float triplanar(vec3 p, vec3 normal, float t) {
282 vec3 blending = abs(normal);
283 blending = normalize(max(blending, 0.00001));
284 blending /= (blending.x + blending.y + blending.z);
285 float x = voronoi(p.yz * uScale, t);
286 float y = voronoi(p.xz * uScale, t);
287 float z = voronoi(p.xy * uScale, t);
288 return (x * blending.x + y * blending.y + z * blending.z);
289 }
290 void main() {
291 vec3 transformedNormal = normalize(normalMatrix * normal);
292 vec3 displacedPosition = position;
293 float time = uTime * uSpeed;
294 float patternValue = triplanar(position, normal, time);
295 vShellPattern = patternValue;
296 float softPattern = smoothstep(0.2, 0.8, patternValue);
297 float displacementFactor = softPattern * uDisplacementStrength;
298 displacedPosition += normal * displacementFactor;
299 vNormal = transformedNormal;
300 v3Position = position;
301 gl_Position = projectionMatrix * modelViewMatrix * vec4(displacedPosition, 1.0);
302 }
303 `,
304 fragmentShader: `
305 uniform float uTime;
306 uniform float uDepth;
307 uniform vec3 uColor;
308 uniform float uOutlineWidth;
309 uniform vec3 uOutlineColor;
310 uniform vec3 uSecondaryColor;
311 varying vec3 vNormal;
312 varying vec3 v3Position;
313 varying float vShellPattern;
314 void main() {
315 float steppedPattern = smoothstep(uOutlineWidth, uOutlineWidth + 0.2, vShellPattern);
316 vec3 lightDirection = normalize(vec3(0.5, 0.5, 1.0));
317 float lighting = dot(vNormal, lightDirection) * 0.5 + 0.5;
318 vec3 baseColor = mix(uOutlineColor, uSecondaryColor, steppedPattern);
319 float highlightIntensity = smoothstep(0.0, 0.5, vShellPattern);
320 vec3 finalColor = baseColor + uColor * highlightIntensity * uDepth * lighting;
321 gl_FragColor = vec4(finalColor * lighting, 1.0);
322 }
323 `,
324 wireframe: true,
325 });
326 const mesh = new THREE.Mesh(geometry, material);
327 scene.add(mesh);
328 const light = new THREE.DirectionalLight(0xffffff, 1);
329 light.position.set(5, 5, 5);
330 scene.add(light);
331 const composer = new EffectComposer(renderer);
332 composer.addPass(new RenderPass(scene, camera));
333 const bloomPass = new UnrealBloomPass(new THREE.Vector2(window.innerWidth, window.innerHeight), 1.3, 0.4, 0.0);
334
335 const flyShader = {
336 uniforms: {
337 tDiffuse: { value: null },
338 resolution: { value: new THREE.Vector2(window.innerWidth * window.devicePixelRatio, window.innerHeight * window.devicePixelRatio) },
339 time: { value: 0 },
340 ommatidiaSize: { value: 4.0 },
341 },
342 vertexShader: `
343 varying vec2 vUv;
344 void main() {
345 vUv = uv;
346 gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
347 }
348 `,
349 fragmentShader: `
350 precision highp float;
351
352 varying vec2 vUv;
353 uniform sampler2D tDiffuse;
354 uniform vec2 resolution;
355 uniform float ommatidiaSize;
356
357 // Grille hexagonale orientée verticalement (pointes en haut/bas)
358 vec2 hexCoord(vec2 uv, float size) {
359 vec2 r = resolution / size;
360 uv *= r;
361
362 // Décalage horizontal sur une ligne sur deux
363 float row = floor(uv.y);
364 float col = floor(uv.x - mod(row, 2.0) * 0.5);
365
366 vec2 hexUV = vec2(col + 0.5 * mod(row, 2.0), row);
367 hexUV /= r;
368
369 return hexUV;
370 }
371
372
373 // Masque hexagonal orienté verticalement
374 float hexMask(vec2 uv, float size) {
375 vec2 p = uv * resolution / size;
376 p = fract(p) - 0.5;
377
378 // Transformation pour hexagone vertical
379 p.y *= 1.0;
380 p.x *= 0.57735; // sqrt(3)/3
381
382 p = abs(p);
383 float a = max(p.y * 0.866025 + p.x, p.x * 2.0); // 0.866 = cos(30°)
384 return step(a, 0.5);
385 }
386
387 void main() {
388 vec2 hexUV = hexCoord(vUv, ommatidiaSize);
389 vec4 color = texture2D(tDiffuse, hexUV);
390
391 float mask = hexMask(vUv, ommatidiaSize);
392 gl_FragColor = color * mask;
393 }
394
395 `
396 };
397 const flyPass = new ShaderPass(flyShader);
398 flyPass.renderToScreen = true;
399 composer.addPass(flyPass);
400 composer.addPass(bloomPass);
401 const gui = new dat.GUI();
402 gui.close();
403
404 function getRandomColor() {
405 const letters = '~#{[|`\^@]@^---\`[|{[##@~@^{{~#}]}|--<>]]}';
406 let color = '#';
407 for (let i = 0; i < 6; i++) {
408 color += letters[Math.floor(Math.random() * 16)];
409 }
410 return color;
411 }
412
413 const randomizer = {
414 randomize: () => {
415 material.uniforms.uScale.value = 1 + Math.random() * 19;
416 material.uniforms.uDepth.value = Math.random() * 2;
417 material.uniforms.uSharpness.value = 1 + Math.random() * 9;
418 material.uniforms.uColor.value.set(getRandomColor());
419 material.uniforms.uNoiseScale.value = 0.1 + Math.random() * 4.9;
420 material.uniforms.uNoiseStrength.value = Math.random() * 1;
421 material.uniforms.uOutlineWidth.value = 0.01 + Math.random() * 0.99;
422 material.uniforms.uOutlineColor.value.set(getRandomColor());
423 material.uniforms.uSecondaryColor.value.set(getRandomColor());
424 material.uniforms.uDisplacementStrength.value = -0.5 + Math.random() * 5.5;
425 for (let i in gui.__folders) {
426 for (let j in gui.__folders[i].__controllers) {
427 gui.__folders[i].__controllers[j].updateDisplay();
428 }
429 }
430 }
431 };
432 renderer.domElement.addEventListener('dblclick', () => {
433 randomizer.randomize();
434 });
435 const flyFolder = gui.addFolder('ShaderPass');
436 flyFolder.add(flyPass, 'enabled').name('HexaVision');
437 flyFolder.add(flyPass.material.uniforms.ommatidiaSize, 'value', 2.0, 200).step(1).name('Ommatidia Size');
438 flyFolder.add(randomizer, 'randomize').name('Randomize All (Double Click)');
439 const guiControls = { wireframe: true };
440 flyFolder.add(guiControls, 'wireframe').name('Toggle Wireframe').onChange(value => {
441 material.wireframe = value;
442 });
443 flyFolder.open();
444
445
446 const patternFolder = gui.addFolder('Shader Pattern');
447 patternFolder.add(material.uniforms.uScale, 'value', 1, 20).name('Pattern Scale');
448 patternFolder.add(material.uniforms.uDepth, 'value', 0, 2).name('Highlight Depth');
449 patternFolder.add(material.uniforms.uSharpness, 'value', 1, 10).name('Pattern Sharpness');
450 // patternFolder.add(material.uniforms.uSpeed, 'value', 0.00000, 1.00000).step(0.00001).name('Pattern Speed');
451 patternFolder.addColor({ color: '#ff00f1' }, 'color').onChange(val => {
452 material.uniforms.uColor.value.set(val);
453 }).name('Highlight Color');
454 // patternFolder.open();
455
456 const displacementFolder = gui.addFolder('Displacement');
457 displacementFolder.add(material.uniforms.uNoiseScale, 'value', 0.1, 5.0).name('Noise Scale');
458 displacementFolder.add(material.uniforms.uNoiseStrength, 'value', 0.0, 1.0).name('Noise Strength');
459 displacementFolder.add(material.uniforms.uOutlineWidth, 'value', 0.01, 1).name('Outline Width');
460 displacementFolder.addColor({ outline: '#0a0060' }, 'outline').onChange(val => {
461 material.uniforms.uOutlineColor.value.set(val);
462 }).name('Outline Color');
463 displacementFolder.addColor({ secondary: '#ff005f' }, 'secondary').onChange(val => {
464 material.uniforms.uSecondaryColor.value.set(val);
465 }).name('Scale Fill Color');
466 displacementFolder.add(material.uniforms.uDisplacementStrength, 'value', -0.5, 5).name('Displacement Strength');
467 // displacementFolder.open();
468 const bloomFolder = gui.addFolder('Bloom');
469 bloomFolder.add(bloomPass, 'strength', 0.0, 3.0).name('Strength');
470 bloomFolder.add(bloomPass, 'radius', 0.0, 1.0).name('Radius');
471 bloomFolder.add(bloomPass, 'threshold', 0.0, 1.0).name('Threshold');
472 // bloomFolder.open();
473
474 window.addEventListener('resize', () => {
475 camera.aspect = window.innerWidth / window.innerHeight;
476 camera.updateProjectionMatrix();
477 renderer.setSize(window.innerWidth, window.innerHeight);
478 composer.setSize(window.innerWidth, window.innerHeight);
479 flyPass.material.uniforms.resolution.value.set(window.innerWidth * window.devicePixelRatio, window.innerHeight * window.devicePixelRatio);
480 });
481 function animate(time) {
482 material.uniforms.uTime.value = time * 0.001;
483 flyPass.material.uniforms.time.value = time * 0.001;
484 mesh.rotation.set(
485 mesh.rotation.x + 0.0002,
486 mesh.rotation.y + 0.001,
487 mesh.rotation.z + 0.0002
488 );
489
490 const target = controls.target;
491 controls2.target.set(target.x, target.y, target.z);
492 controls.update();
493 controls2.update();
494
495 composer.render();
496 requestAnimationFrame(animate);
497 }
498 animate();
499
500 import anime from 'https://cdn.skypack.dev/animejs@3.2.1';
501
502 const notif = document.getElementById('notification');
503 const closeBtn = document.getElementById('closeBtn');
504
505 // Slide in from bottom
506 anime({
507 targets: notif,
508 bottom: '24px',
509 easing: 'easeOutExpo',
510 duration: 800,
511 delay: 1000
512 });
513
514 // Slide out to bottom on close
515 closeBtn.addEventListener('click', () => {
516 anime({
517 targets: notif,
518 bottom: '-100px',
519 easing: 'easeInExpo',
520 duration: 600
521 });
522 });
523
524 </script>
525 </body>
526</html>
527
528 </body>
529
530</html>
531