Skip to main content
This guide covers all animation systems in the NILVER T.I Portfolio, including particle backgrounds, SVG character animations, skill progress circles, and interactive effects.

Particle Animation System

The background particle effect is powered by js/fondo.js, which creates animated red particles using HTML5 Canvas.

Understanding the System

The particle system (js/fondo.js:1-44) renders 100 moving particles on a canvas:
js/fondo.js
const canvas = document.getElementById('particlesCanvas');
const ctx = canvas.getContext('2d');

let particles = [];

function resizeCanvas() {
    canvas.width = window.innerWidth;
    canvas.height = document.getElementById('home').offsetHeight;
}

window.addEventListener('resize', resizeCanvas);
resizeCanvas();

// Create 100 particles
for (let i = 0; i < 100; i++) {
    particles.push({
        x: Math.random() * canvas.width,
        y: Math.random() * canvas.height,
        radius: Math.random() * 2 + 1,
        speedX: Math.random() * 0.6 - 0.3,
        speedY: Math.random() * 0.6 - 0.3
    });
}

function animateParticles() {
    ctx.clearRect(0, 0, canvas.width, canvas.height);

    for (let p of particles) {
        p.x += p.speedX;
        p.y += p.speedY;

        if (p.x < 0 || p.x > canvas.width) p.speedX *= -1;
        if (p.y < 0 || p.y > canvas.height) p.speedY *= -1;

        ctx.beginPath();
        ctx.arc(p.x, p.y, p.radius, 0, 2 * Math.PI);
        ctx.fillStyle = 'rgba(229, 9, 20, 0.4)';
        ctx.fill();
    }

    requestAnimationFrame(animateParticles);
}

animateParticles();
1
Step 1: Adjust Particle Count
2
Change the number of particles in js/fondo.js:14:
3
Default (100)
for (let i = 0; i < 100; i++) {
    particles.push({...});
}
More Particles (200)
for (let i = 0; i < 200; i++) {
    particles.push({...});
}
More particles = more visual density but may impact performance
Fewer Particles (50)
for (let i = 0; i < 50; i++) {
    particles.push({...});
}
Fewer particles create a cleaner, more minimalist look
4
Step 2: Modify Particle Size
5
Adjust the radius calculation in js/fondo.js:18:
6
// Current: radius between 1-3 pixels
radius: Math.random() * 2 + 1

// Larger particles (2-5 pixels)
radius: Math.random() * 3 + 2

// Smaller particles (0.5-1.5 pixels)
radius: Math.random() * 1 + 0.5

// Uniform size (exactly 2 pixels)
radius: 2
7
Step 3: Change Particle Speed
8
Modify movement speed in js/fondo.js:19-20:
9
// Current: slow movement
speedX: Math.random() * 0.6 - 0.3  // Range: -0.3 to 0.3
speedY: Math.random() * 0.6 - 0.3

// Faster movement
speedX: Math.random() * 1.2 - 0.6  // Range: -0.6 to 0.6
speedY: Math.random() * 1.2 - 0.6

// Slower movement
speedX: Math.random() * 0.3 - 0.15 // Range: -0.15 to 0.15
speedY: Math.random() * 0.3 - 0.15

// Upward floating effect
speedX: Math.random() * 0.4 - 0.2
speedY: -Math.random() * 0.5  // Negative = upward
10
Step 4: Customize Particle Color
11
Change the particle color in js/fondo.js:36:
12
Red (Current)
ctx.fillStyle = 'rgba(229, 9, 20, 0.4)'; // Red with 40% opacity
Blue Theme
ctx.fillStyle = 'rgba(14, 165, 233, 0.4)'; // Sky blue
Green Theme
ctx.fillStyle = 'rgba(16, 185, 129, 0.4)'; // Emerald green
Purple Theme
ctx.fillStyle = 'rgba(168, 85, 247, 0.4)'; // Purple
Multi-Color
// Random color per particle
const colors = [
    'rgba(229, 9, 20, 0.4)',
    'rgba(14, 165, 233, 0.4)',
    'rgba(16, 185, 129, 0.4)'
];
ctx.fillStyle = colors[Math.floor(Math.random() * colors.length)];
13
Step 5: Add Particle Trails
14
Create a trailing effect by modifying js/fondo.js:25:
15
function animateParticles() {
    // Instead of clearing completely, add a fade effect
    ctx.fillStyle = 'rgba(15, 15, 15, 0.1)'; // Semi-transparent background
    ctx.fillRect(0, 0, canvas.width, canvas.height);

    // Rest of the animation code...
}
The canvas automatically resizes to match the home section height (js/fondo.js:6-8). If you modify the home section layout, the particle area will adjust accordingly.

SVG Character Animation

The animated SVG face responds to mouse movements (js/main.js:52-76).

SVG Structure

The SVG is defined in index.html:87-96:
<svg id="profileSvg" width="400" height="400" viewBox="0 0 200 200">
    <circle cx="100" cy="100" r="80" fill="none" stroke="#333" stroke-width="2" />
    <circle cx="100" cy="100" r="60" fill="none" stroke="#333" stroke-width="2" />
    <circle cx="100" cy="100" r="40" fill="none" stroke="#333" stroke-width="2" />
    <path id="faceOutline" fill="none" stroke="#E50914" stroke-width="3" stroke-linecap="round"
        d="M60,100 Q100,150 140,100" />
    <circle id="leftEye" cx="80" cy="80" r="10" fill="#E50914" />
    <circle id="rightEye" cx="120" cy="80" r="10" fill="#E50914" />
</svg>

Mouse Interaction Code

js/main.js:52-76
const profileSvg = document.getElementById('profileSvg');
if (profileSvg) {
    const leftEye = document.getElementById('leftEye');
    const rightEye = document.getElementById('rightEye');
    const faceOutline = document.getElementById('faceOutline');

    profileSvg.addEventListener('mousemove', e => {
        const rect = profileSvg.getBoundingClientRect();
        const x = (e.clientX - rect.left) / rect.width * 200;
        const y = (e.clientY - rect.top) / rect.height * 200;
        
        // Eyes follow mouse (5% movement)
        leftEye.setAttribute('cx', 80 + (x - 80) * 0.05);
        leftEye.setAttribute('cy', 80 + (y - 80) * 0.05);
        rightEye.setAttribute('cx', 120 + (x - 120) * 0.05);
        rightEye.setAttribute('cy', 80 + (y - 80) * 0.05);
        
        // Mouth changes shape (20% movement)
        faceOutline.setAttribute('d', `M60,100 Q100,${130 + (y - 100) * 0.2} 140,100`);
    });

    profileSvg.addEventListener('mouseleave', () => {
        // Reset to default positions
        leftEye.setAttribute('cx', '80');
        leftEye.setAttribute('cy', '80');
        rightEye.setAttribute('cx', '120');
        rightEye.setAttribute('cy', '80');
        faceOutline.setAttribute('d', 'M60,100 Q100,150 140,100');
    });
}
1
Increase Eye Movement Range
2
Modify the multiplier in js/main.js:63-66:
3
// Current: 5% movement (subtle)
leftEye.setAttribute('cx', 80 + (x - 80) * 0.05);

// More dramatic: 15% movement
leftEye.setAttribute('cx', 80 + (x - 80) * 0.15);

// Exaggerated: 30% movement
leftEye.setAttribute('cx', 80 + (x - 80) * 0.30);
4
Change Eye/Mouth Colors
5
Update the SVG fill colors in index.html:91-94:
6
<!-- Red theme (current) -->
<path id="faceOutline" stroke="#E50914" />
<circle id="leftEye" fill="#E50914" />
<circle id="rightEye" fill="#E50914" />

<!-- Blue theme -->
<path id="faceOutline" stroke="#0EA5E9" />
<circle id="leftEye" fill="#0EA5E9" />
<circle id="rightEye" fill="#0EA5E9" />
7
Add More Interactive Elements
8
Add eyebrows or other SVG elements:
9
<!-- Add animated eyebrows -->
<line id="leftBrow" x1="70" y1="65" x2="90" y2="65" 
      stroke="#E50914" stroke-width="2" stroke-linecap="round" />
<line id="rightBrow" x1="110" y1="65" x2="130" y2="65" 
      stroke="#E50914" stroke-width="2" stroke-linecap="round" />
10
Then animate them in JavaScript:
11
const leftBrow = document.getElementById('leftBrow');
const rightBrow = document.getElementById('rightBrow');

leftBrow.setAttribute('y1', 65 - (y - 80) * 0.1);
leftBrow.setAttribute('y2', 65 - (y - 80) * 0.1);
rightBrow.setAttribute('y1', 65 - (y - 80) * 0.1);
rightBrow.setAttribute('y2', 65 - (y - 80) * 0.1);
The SVG uses a viewBox="0 0 200 200" coordinate system. All positions are relative to this 200×200 grid, making it resolution-independent.

Skill Circle Animations

Circular skill indicators animate on page load using CSS keyframes (css/estilos.css:30-43).

Animation Mechanics

css/estilos.css:30-43
.skill-circle {
    stroke: #E50914;
    stroke-width: 10;
    fill: transparent;
    stroke-dasharray: 314;
    stroke-dashoffset: 314; /* Starts fully hidden */
    animation: fillCircle 1.5s forwards;
}

@keyframes fillCircle {
    to {
        stroke-dashoffset: var(--dash-offset); /* Animates to target percentage */
    }
}
Each skill circle has a --dash-offset CSS variable set inline:
index.html:127
<circle class="skill-circle" cx="60" cy="60" r="50" 
        style="--dash-offset: 94.2;" data-value="70" />
1
Step 1: Change Animation Duration
2
Modify the animation timing in css/estilos.css:36:
3
/* Current: 1.5 seconds */
animation: fillCircle 1.5s forwards;

/* Faster: 0.8 seconds */
animation: fillCircle 0.8s forwards;

/* Slower: 3 seconds */
animation: fillCircle 3s forwards;

/* With delay: starts after 0.5s */
animation: fillCircle 1.5s 0.5s forwards;
4
Step 2: Add Easing Functions
5
Create smoother or bouncier animations:
6
/* Current: linear */
animation: fillCircle 1.5s forwards;

/* Ease-in-out (smooth) */
animation: fillCircle 1.5s ease-in-out forwards;

/* Bounce effect */
animation: fillCircle 1.5s cubic-bezier(0.68, -0.55, 0.265, 1.55) forwards;

/* Fast start, slow end */
animation: fillCircle 1.5s ease-out forwards;
7
Step 3: Change Circle Colors
8
Update the stroke color in css/estilos.css:31:
9
.skill-circle {
    stroke: #E50914; /* Change to your accent color */
}
10
Or create gradient strokes:
11
<defs>
    <linearGradient id="skillGradient" x1="0%" y1="0%" x2="100%" y2="100%">
        <stop offset="0%" style="stop-color:#E50914;stop-opacity:1" />
        <stop offset="100%" style="stop-color:#FF6B6B;stop-opacity:1" />
    </linearGradient>
</defs>
<circle class="skill-circle" stroke="url(#skillGradient)" ... />
12
Step 4: Stagger Animations
13
Make circles animate sequentially:
14
.skill-item:nth-child(1) .skill-circle {
    animation: fillCircle 1.5s 0s forwards;
}

.skill-item:nth-child(2) .skill-circle {
    animation: fillCircle 1.5s 0.2s forwards;
}

.skill-item:nth-child(3) .skill-circle {
    animation: fillCircle 1.5s 0.4s forwards;
}

.skill-item:nth-child(4) .skill-circle {
    animation: fillCircle 1.5s 0.6s forwards;
}

.skill-item:nth-child(5) .skill-circle {
    animation: fillCircle 1.5s 0.8s forwards;
}
The stroke-dasharray: 314 value represents the circle’s circumference (2πr = 2 × 3.14 × 50 = 314). The stroke-dashoffset animates from 314 (empty) to the calculated percentage value (filled).

Canvas Drawing Feature

The interactive drawing canvas allows users to draw on a small canvas (js/main.js:79-112).

Drawing Implementation

js/main.js:80-112
const canvas = document.getElementById('drawingCanvas');
if (canvas) {
    const ctx = canvas.getContext('2d');
    let isDrawing = false;

    canvas.addEventListener('mousedown', e => { isDrawing = true; draw(e); });
    canvas.addEventListener('mousemove', draw);
    canvas.addEventListener('mouseup', () => isDrawing = false);
    canvas.addEventListener('mouseout', () => isDrawing = false);

    function draw(e) {
        if (!isDrawing) return;
        ctx.lineWidth = 2;
        ctx.lineCap = 'round';
        ctx.strokeStyle = '#E50914';
        const rect = canvas.getBoundingClientRect();
        const x = e.clientX - rect.left;
        const y = e.clientY - rect.top;
        if (ctx.lastX && ctx.lastY) {
            ctx.beginPath();
            ctx.moveTo(ctx.lastX, ctx.lastY);
            ctx.lineTo(x, y);
            ctx.stroke();
        }
        ctx.lastX = x;
        ctx.lastY = y;
    }

    document.getElementById('clearCanvas')?.addEventListener('click', () => {
        ctx.clearRect(0, 0, canvas.width, canvas.height);
    });
}
1
Customize Brush Size
2
// Current: 2px brush
ctx.lineWidth = 2;

// Thicker brush (5px)
ctx.lineWidth = 5;

// Thin brush (1px)
ctx.lineWidth = 1;

// Dynamic brush size based on speed
const speed = Math.sqrt(Math.pow(x - ctx.lastX, 2) + Math.pow(y - ctx.lastY, 2));
ctx.lineWidth = Math.max(1, 5 - speed * 0.1);
3
Change Brush Color
4
// Current: Red
ctx.strokeStyle = '#E50914';

// Blue
ctx.strokeStyle = '#0EA5E9';

// Rainbow effect (color changes over time)
const hue = (Date.now() / 10) % 360;
ctx.strokeStyle = `hsl(${hue}, 70%, 50%)`;
5
Add Touch Support
6
Enable drawing on mobile devices:
7
// Add touch event listeners
canvas.addEventListener('touchstart', e => {
    e.preventDefault();
    isDrawing = true;
    const touch = e.touches[0];
    draw(touch);
});

canvas.addEventListener('touchmove', e => {
    e.preventDefault();
    const touch = e.touches[0];
    draw(touch);
});

canvas.addEventListener('touchend', () => {
    isDrawing = false;
});

Hover Effects

The navigation links have an animated underline (css/estilos.css:83-96):
.nav-link::after {
    content: '';
    position: absolute;
    width: 0;
    height: 2px;
    bottom: -5px;
    left: 0;
    background-color: #E50914;
    transition: width 0.3s ease;
}

.nav-link:hover::after {
    width: 100%;
}
transition: width 0.15s ease; /* 150ms instead of 300ms */

Button Scale Effect

Buttons scale up on hover (css/estilos.css:22-28):
.hover-scale {
    transition: transform 0.3s ease;
}

.hover-scale:hover {
    transform: scale(1.05); /* 5% larger */
}
Customize the scale:
/* Subtle (2% larger) */
transform: scale(1.02);

/* Dramatic (15% larger) */
transform: scale(1.15);

/* With rotation */
transform: scale(1.05) rotate(2deg);

Project Card Hover

Project cards lift and glow on hover (css/estilos.css:51-54):
.project-card:hover {
    transform: translateY(-10px);
    box-shadow: 0 10px 20px rgba(229, 9, 20, 0.2);
}
1
Change Lift Distance
2
/* Higher lift (20px) */
transform: translateY(-20px);

/* Subtle lift (5px) */
transform: translateY(-5px);

/* Tilt effect */
transform: translateY(-10px) rotateX(5deg);
3
Add Glow Effect
4
.project-card:hover {
    transform: translateY(-10px);
    box-shadow: 
        0 10px 20px rgba(229, 9, 20, 0.2),
        0 0 40px rgba(229, 9, 20, 0.3),  /* Outer glow */
        inset 0 0 20px rgba(229, 9, 20, 0.1); /* Inner glow */
}

Project Card Expand/Collapse

The “Ver más” button smoothly expands project details (js/main.js:156-176).

Toggle Animation

js/main.js:156-169
function toggleDetails(button) {
    const container = button.nextElementSibling;
    const isOpen = container.classList.contains('open');

    // Close all other containers
    document.querySelectorAll('.details-container').forEach(el => {
        el.classList.remove('open');
        el.style.maxHeight = null;
    });

    // Open clicked container
    if (!isOpen) {
        container.classList.add('open');
        container.style.maxHeight = container.scrollHeight + 'px';
    }
}
The CSS transition is defined in css/estilos.css:123-130:
.details-container {
    overflow: hidden;
    max-height: 0;
    transition: max-height 0.5s ease-in-out;
}

.details-container.open {
    max-height: 1000px;
}
1
Adjust Animation Speed
2
/* Faster (250ms) */
transition: max-height 0.25s ease-in-out;

/* Slower (1s) */
transition: max-height 1s ease-in-out;
3
Change Easing
4
/* Smooth acceleration */
transition: max-height 0.5s ease-out;

/* Bounce effect */
transition: max-height 0.5s cubic-bezier(0.68, -0.55, 0.265, 1.55);

Floating Background Animation

There’s a floating animation defined in css/estilos.css:61-77 (currently unused in HTML):
.floating-shape {
    position: absolute;
    opacity: 0.2;
    animation: float 15s infinite ease-in-out;
}

@keyframes float {
    0%, 100% {
        transform: translateY(0) rotate(0deg);
    }
    50% {
        transform: translateY(-20px) rotate(5deg);
    }
}
To use it, add floating shapes:
index.html (inside any section)
<div class="floating-shape" style="width: 100px; height: 100px; background: rgba(229,9,20,0.1); border-radius: 50%; top: 10%; left: 5%;"></div>
<div class="floating-shape" style="width: 150px; height: 150px; background: rgba(229,9,20,0.05); border-radius: 30%; top: 60%; right: 10%;"></div>

Performance Tips

Animations can impact performance on lower-end devices. Consider these optimizations:

Reduce Particle Count

Lower for (let i = 0; i < 100; i++) to 50 or fewer particles in js/fondo.js:14

Use CSS Transform

Prefer transform over top/left for smoother animations (GPU-accelerated)

Throttle Events

Add debouncing to mouse move events for SVG animations

Prefers Reduced Motion

Respect user preferences:
@media (prefers-reduced-motion: reduce) {
  * { animation-duration: 0.01ms !important; }
}

Animation Summary Reference

  • Count: Line 14 (for (let i = 0; i < 100; i++))
  • Size: Line 18 (radius: Math.random() * 2 + 1)
  • Speed: Lines 19-20 (speedX/speedY)
  • Color: Line 36 (ctx.fillStyle)
  • Eye Movement: Lines 63-66 (multiplier * 0.05)
  • Mouth Shape: Line 67 (multiplier * 0.2)
  • Colors: index.html:91-94 (stroke/fill)
  • Duration: Line 36 (animation: fillCircle 1.5s)
  • Color: Line 31 (stroke: #E50914)
  • Dash Offset: index.html inline styles
  • Nav Underline: Lines 83-96
  • Button Scale: Lines 22-28
  • Project Cards: Lines 51-54

Next Steps

Styling Customization

Customize colors, fonts, spacing, and visual design

Content Customization

Update personal information, skills, projects, and text

Build docs developers (and LLMs) love