Overview
CV Staff Web uses a modular JavaScript architecture with separate files for different features. Scripts are organized in src/scripts/ and loaded through Astro’s build system for optimal performance.
Scripts Directory Structure
src/scripts/
├── animations.js # GSAP scroll animations (339 lines)
├── app.js # General application utilities (19 lines)
├── color-transition.js # Progressive scroll-based color transitions (50 lines)
└── glass-cursor.js # Custom glassmorphism cursor effect (107 lines)
Script Loading
Scripts are imported in the main layout file to ensure they run on every page.
File : src/layouts/Layout.astro:21-25
< body >
< a href = "#main" class = "skip-link" > Saltar al contenido </ a >
< slot />
< script >
import '../scripts/animations.js' ;
import '../scripts/color-transition.js' ;
import '../scripts/glass-cursor.js' ;
</ script >
</ body >
Astro’s <script> tags with imports are processed during build time and bundled efficiently with tree-shaking and code splitting.
Core Scripts
1. animations.js
The main animation orchestrator using GSAP and ScrollTrigger.
File : src/scripts/animations.js
Dependencies :
import gsap from 'gsap' ;
import { ScrollTrigger } from 'gsap/ScrollTrigger' ;
gsap . registerPlugin ( ScrollTrigger );
Animation Functions
Hero Entrance
Scroll Reveals
Works Reveal
Skills Reveal
Contact & Chatbot
Function : initHeroEntrance() (lines 9-129)Orchestrates the staggered entrance animation for hero section elements on page load. Animation Sequence :
Badge (0s delay): Scale up with bounce
Words “Hola,” “soy” (0.2s): Slide in from left with rotation
Name letters (0.5s): Drop from above with individual rotation and bounce
Wave emoji (0.9s): Scale in with wiggle animation (5 repeats)
Description (1.3s): Fade in from below
Photo slider (1.0s): Fade and scale in
CTA buttons (1.5s): Fade in from below
function initHeroEntrance () {
const hero = document . querySelector ( '.hero' );
if ( ! hero ) return ;
const badge = hero . querySelector ( '.badget' );
const nameLetters = hero . querySelectorAll ( '.name .letter' );
const wave = hero . querySelector ( '[data-animate="wave"]' );
const tl = gsap . timeline ({ delay: 0.1 });
// Badge animation
if ( badge ) {
gsap . set ( badge , { opacity: 0 , scale: 0.8 , y: - 10 });
tl . to ( badge , {
opacity: 1 ,
scale: 1 ,
y: 0 ,
duration: 0.4 ,
ease: 'back.out(2)' ,
}, 0 );
}
// Name letters with bounce
if ( nameLetters . length ) {
gsap . set ( nameLetters , {
opacity: 0 ,
y: - 80 ,
rotationZ : () => gsap . utils . random ( - 20 , 20 ),
scale: 0.5 ,
});
tl . to ( nameLetters , {
opacity: 1 ,
y: 0 ,
rotationZ: 0 ,
scale: 1 ,
duration: 0.6 ,
stagger: 0.04 ,
ease: 'bounce.out' ,
}, 0.5 );
}
// Wave wiggle
if ( wave ) {
tl . to ( wave , {
rotationZ: 20 ,
duration: 0.15 ,
ease: 'power1.inOut' ,
yoyo: true ,
repeat: 5 ,
}, 1.1 );
}
}
Function : initScrollReveal() (lines 134-152)Generic scroll-triggered reveal for sections with data-reveal attribute. function initScrollReveal () {
const sections = document . querySelectorAll ( '[data-reveal]' );
sections . forEach (( section ) => {
const inner = section . querySelector ( '[data-reveal-inner]' ) || section ;
gsap . from ( inner . children , {
opacity: 0 ,
y: 50 ,
duration: 0.8 ,
stagger: 0.1 ,
ease: 'power3.out' ,
scrollTrigger: {
trigger: section ,
start: 'top 80%' ,
toggleActions: 'play none none none' ,
},
});
});
}
Trigger Points :
Activates when section top reaches 80% down the viewport
Plays once and doesn’t reverse
Function : initWorksReveal() (lines 157-193)Specialized animation for the works/experience accordion section. function initWorksReveal () {
const section = document . querySelector ( '.works' );
if ( ! section ) return ;
const heading = section . querySelector ( '.heading' );
const items = section . querySelectorAll ( '.accordion-item' );
// Heading animation
gsap . from ( heading , {
opacity: 0 ,
y: 40 ,
duration: 0.7 ,
ease: 'power3.out' ,
scrollTrigger: {
trigger: section ,
start: 'top 80%' ,
},
});
// Accordion items with stagger
gsap . from ( items , {
opacity: 0 ,
y: 60 ,
duration: 0.7 ,
stagger: 0.15 ,
ease: 'power3.out' ,
scrollTrigger: {
trigger: items [ 0 ],
start: 'top 80%' ,
},
});
}
Function : initSkillsReveal() (lines 198-233)Animated reveal of skill tags with scale and bounce effect. function initSkillsReveal () {
const section = document . querySelector ( '.skills' );
if ( ! section ) return ;
const tags = section . querySelector ( '.tags' );
if ( ! tags ) return ;
gsap . set ( tags . children , {
opacity: 0 ,
y: 30 ,
scale: 0.8
});
gsap . to ( tags . children , {
opacity: 1 ,
y: 0 ,
scale: 1 ,
duration: 0.5 ,
stagger: 0.04 ,
ease: 'back.out(1.2)' ,
scrollTrigger: {
trigger: tags ,
start: 'top 85%' ,
},
});
}
Effect : Tags pop in with a bouncy scale animation, staggered by 40ms each.Functions :
initContactReveal() (lines 238-261)
initChatbotReveal() (lines 266-318)
function initContactReveal () {
const section = document . querySelector ( '.contact' );
const heading = section . querySelector ( '.heading' );
const subline = section . querySelector ( '.subline' );
const links = section . querySelectorAll ( '.link' );
const elements = [ heading , subline , ... links ]. filter ( Boolean );
gsap . from ( elements , {
opacity: 0 ,
y: 30 ,
duration: 0.6 ,
stagger: 0.1 ,
ease: 'power3.out' ,
scrollTrigger: {
trigger: section ,
start: 'top 80%' ,
},
});
}
function initChatbotReveal () {
const section = document . querySelector ( '.chatbot-section' );
const chatContainer = section . querySelector ( '.chat-container' );
gsap . from ( chatContainer , {
opacity: 0 ,
y: 50 ,
scale: 0.95 ,
duration: 0.8 ,
delay: 0.2 ,
ease: 'back.out(1.2)' ,
scrollTrigger: {
trigger: chatContainer ,
start: 'top 85%' ,
},
});
}
Initialization
function init () {
// Respect prefers-reduced-motion
if ( typeof window !== 'undefined' &&
window . matchMedia ( '(prefers-reduced-motion: reduce)' ). matches ) {
return ;
}
initHeroEntrance ();
initScrollReveal ();
initWorksReveal ();
initSkillsReveal ();
initChatbotReveal ();
initContactReveal ();
}
if ( typeof document !== 'undefined' ) {
if ( document . readyState === 'loading' ) {
document . addEventListener ( 'DOMContentLoaded' , init );
} else {
init ();
}
}
Animations respect the prefers-reduced-motion accessibility preference, disabling all animations if the user has requested reduced motion.
2. glass-cursor.js
Custom glassmorphism cursor effect for desktop users.
File : src/scripts/glass-cursor.js
Features :
Creates two cursor elements: outline (glass effect) and dot (center)
Smooth following animation with delay for outline
Hover state expansion for interactive elements
Respects prefers-reduced-motion
Disabled on mobile (< 1024px)
Implementation :
function initGlassCursor () {
// Check accessibility preference
if ( window . matchMedia ( '(prefers-reduced-motion: reduce)' ). matches ) {
return ;
}
// Desktop only
if ( window . innerWidth < 1024 ) {
return ;
}
// Create cursor elements
const cursorOutline = document . createElement ( 'div' );
cursorOutline . className = 'cursor-outline' ;
const cursorDot = document . createElement ( 'div' );
cursorDot . className = 'cursor-dot' ;
document . body . appendChild ( cursorOutline );
document . body . appendChild ( cursorDot );
let mouseX = window . innerWidth / 2 ;
let mouseY = window . innerHeight / 2 ;
let outlineX = mouseX ;
let outlineY = mouseY ;
// Instant dot movement
document . addEventListener ( 'mousemove' , ( e ) => {
mouseX = e . clientX ;
mouseY = e . clientY ;
cursorDot . style . left = ` ${ mouseX } px` ;
cursorDot . style . top = ` ${ mouseY } px` ;
});
// Smooth outline following
function animate () {
const delay = 0.12 ;
outlineX += ( mouseX - outlineX ) * delay ;
outlineY += ( mouseY - outlineY ) * delay ;
cursorOutline . style . left = ` ${ outlineX } px` ;
cursorOutline . style . top = ` ${ outlineY } px` ;
requestAnimationFrame ( animate );
}
animate ();
// Hover effects
const interactiveElements = document . querySelectorAll (
'a, button, .tag, .accordion-header, .q-btn, input, textarea'
);
interactiveElements . forEach ( el => {
el . addEventListener ( 'mouseenter' , () => {
cursorOutline . classList . add ( 'hover' );
cursorDot . classList . add ( 'hover' );
});
el . addEventListener ( 'mouseleave' , () => {
cursorOutline . classList . remove ( 'hover' );
cursorDot . classList . remove ( 'hover' );
});
});
// Hide default cursor
document . body . style . cursor = 'none' ;
}
Cursor Styles (defined in src/layouts/Layout.astro:152-220):
.cursor-outline {
position : fixed ;
width : 40 px ;
height : 40 px ;
border-radius : 50 % ;
pointer-events : none ;
z-index : 10000 ;
background : rgba ( 255 , 255 , 255 , 0.05 );
backdrop-filter : blur ( 10 px );
border : 1 px solid rgba ( 255 , 255 , 255 , 0.18 );
box-shadow : 0 8 px 32 px 0 rgba ( 173 , 0 , 0 , 0.2 );
transition : width 0.3 s ease , height 0.3 s ease ;
}
.cursor-outline.hover {
width : 60 px ;
height : 60 px ;
background : rgba ( 255 , 255 , 255 , 0.08 );
}
.cursor-dot {
position : fixed ;
width : 8 px ;
height : 8 px ;
border-radius : 50 % ;
background : var ( --color-primary );
pointer-events : none ;
z-index : 10001 ;
box-shadow : 0 0 10 px rgba ( 173 , 0 , 0 , 0.5 );
}
3. color-transition.js
Progressive color change effect as the user scrolls down the page.
File : src/scripts/color-transition.js
Feature : Smoothly transitions the primary color from red (#ad0000) to orange (rgb(227, 114, 1)) based on scroll progress.
Implementation :
function initColorTransition () {
const startColor = { r: 173 , g: 0 , b: 0 }; // #ad0000 (red)
const endColor = { r: 227 , g: 114 , b: 1 }; // Orange
let ticking = false ;
function lerp ( start , end , progress ) {
return Math . round ( start + ( end - start ) * progress );
}
function updateColor () {
const scrollHeight = document . documentElement . scrollHeight - window . innerHeight ;
const scrollProgress = Math . min ( window . scrollY / scrollHeight , 1 );
const r = lerp ( startColor . r , endColor . r , scrollProgress );
const g = lerp ( startColor . g , endColor . g , scrollProgress );
const b = lerp ( startColor . b , endColor . b , scrollProgress );
const newColor = `rgb( ${ r } , ${ g } , ${ b } )` ;
document . documentElement . style . setProperty ( '--color-primary' , newColor );
ticking = false ;
}
function handleScroll () {
if ( ! ticking ) {
window . requestAnimationFrame ( updateColor );
ticking = true ;
}
}
window . addEventListener ( 'scroll' , handleScroll , { passive: true });
updateColor (); // Initial
}
Performance Optimization :
Uses requestAnimationFrame for smooth 60fps updates
Throttles updates with ticking flag
Passive event listener for better scroll performance
The color transition creates a subtle visual progression as users explore the page, providing a sense of journey through the content.
4. app.js
General application utilities and initialization code.
File : src/scripts/app.js
// Variables
const variable1 = 'Hello' ;
let variable2 = 'World' ;
// Event Listeners
document . addEventListener ( 'DOMContentLoaded' , () => {
console . log ( 'DOM fully loaded and parsed' );
});
// Functions
function miFuncion () {
// Function code
}
// App initialization
( function init () {
// Initialization code
})();
This file currently contains boilerplate code and is ready for additional app-wide utilities.
Section-Specific Scripts
Some sections include their own inline <script> blocks for section-specific functionality.
Hero Swiper Script
Location : src/sections/hero.astro:248-323
import Swiper from 'swiper' ;
import { EffectCards , Pagination } from 'swiper/modules' ;
import 'swiper/css' ;
import 'swiper/css/effect-cards' ;
function initSwiper () {
const swiper = new Swiper ( '#heroSwiper' , {
modules: [ EffectCards , Pagination ],
effect: 'cards' ,
grabCursor: true ,
cardsEffect: {
slideShadows: true ,
perSlideOffset: 8 ,
perSlideRotate: 2 ,
},
pagination: {
el: '.swiper-pagination' ,
clickable: true ,
},
on: {
slideChange : function () {
// Pause all videos
document . querySelectorAll ( '.hero-video' ). forEach ( video => {
video . pause ();
});
// Play active video
const activeVideo = this . slides [ this . activeIndex ]
?. querySelector ( '.hero-video' );
if ( activeVideo ) {
activeVideo . play ();
}
}
}
});
}
JavaScript Patterns
Safe Initialization Pattern
All scripts use a safe initialization pattern:
function initFeature () {
// Feature code
}
if ( typeof document !== 'undefined' ) {
if ( document . readyState === 'loading' ) {
document . addEventListener ( 'DOMContentLoaded' , initFeature );
} else {
initFeature ();
}
}
Why?
Checks for browser environment (not SSR)
Handles both early and late script execution
Ensures DOM is ready before running
Export Pattern
Functions are exported for potential reuse:
export { initGlassCursor };
RequestAnimationFrame for animations
All visual updates use requestAnimationFrame for 60fps smoothness: function animate () {
// Update logic
requestAnimationFrame ( animate );
}
Scroll listeners use { passive: true } to improve scroll performance: window . addEventListener ( 'scroll' , handler , { passive: true });
Features check device capabilities before initializing: if ( window . innerWidth < 1024 ) return ; // Skip on mobile
Updates are throttled using flags: let ticking = false ;
if ( ! ticking ) {
requestAnimationFrame ( update );
ticking = true ;
}
Accessibility
Scripts respect user accessibility preferences:
if ( window . matchMedia ( '(prefers-reduced-motion: reduce)' ). matches ) {
return ; // Disable animations
}
Respects :
prefers-reduced-motion - Disables animations
Screen size - Desktop-only features disabled on mobile
Progressive enhancement - Site works without JavaScript
Best Practices
Feature per file
Each major feature gets its own file (animations, cursor, color transitions)
Safe initialization
Always check for DOM ready state and browser environment
Performance first
Use requestAnimationFrame, passive listeners, and conditional execution
Accessibility
Respect user preferences and provide fallbacks
Export functions
Make functions available for reuse or testing
Next Steps
GSAP Documentation Official GSAP and ScrollTrigger documentation
Swiper Documentation Learn about Swiper carousel configuration
Animation API Complete animations API reference
Glass Cursor API Glass cursor effect API documentation