Skip to main content

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

Function: initHeroEntrance() (lines 9-129)Orchestrates the staggered entrance animation for hero section elements on page load.Animation Sequence:
  1. Badge (0s delay): Scale up with bounce
  2. Words “Hola,” “soy” (0.2s): Slide in from left with rotation
  3. Name letters (0.5s): Drop from above with individual rotation and bounce
  4. Wave emoji (0.9s): Scale in with wiggle animation (5 repeats)
  5. Description (1.3s): Fade in from below
  6. Photo slider (1.0s): Fade and scale in
  7. 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);
    }
}

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: 40px;
    height: 40px;
    border-radius: 50%;
    pointer-events: none;
    z-index: 10000;
    background: rgba(255, 255, 255, 0.05);
    backdrop-filter: blur(10px);
    border: 1px solid rgba(255, 255, 255, 0.18);
    box-shadow: 0 8px 32px 0 rgba(173, 0, 0, 0.2);
    transition: width 0.3s ease, height 0.3s ease;
}

.cursor-outline.hover {
    width: 60px;
    height: 60px;
    background: rgba(255, 255, 255, 0.08);
}

.cursor-dot {
    position: fixed;
    width: 8px;
    height: 8px;
    border-radius: 50%;
    background: var(--color-primary);
    pointer-events: none;
    z-index: 10001;
    box-shadow: 0 0 10px 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 };

Performance Considerations

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

1

Feature per file

Each major feature gets its own file (animations, cursor, color transitions)
2

Safe initialization

Always check for DOM ready state and browser environment
3

Performance first

Use requestAnimationFrame, passive listeners, and conditional execution
4

Accessibility

Respect user preferences and provide fallbacks
5

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

Build docs developers (and LLMs) love