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:
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 ();
Step 1: Adjust Particle Count
Change the number of particles in js/fondo.js:14:
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
Step 2: Modify Particle Size
Adjust the radius calculation in js/fondo.js:18:
// 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
Step 3: Change Particle Speed
Modify movement speed in js/fondo.js:19-20:
// 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
Step 4: Customize Particle Color
Change the particle color in js/fondo.js:36:
ctx . fillStyle = 'rgba(229, 9, 20, 0.4)' ; // Red with 40% opacity
ctx . fillStyle = 'rgba(14, 165, 233, 0.4)' ; // Sky blue
ctx . fillStyle = 'rgba(16, 185, 129, 0.4)' ; // Emerald green
ctx . fillStyle = 'rgba(168, 85, 247, 0.4)' ; // Purple
// 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 )];
Step 5: Add Particle Trails
Create a trailing effect by modifying js/fondo.js:25:
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
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' );
});
}
Increase Eye Movement Range
Modify the multiplier in js/main.js:63-66:
// 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 );
Update the SVG fill colors in index.html:91-94:
<!-- 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" />
Add More Interactive Elements
Add eyebrows or other SVG elements:
<!-- 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" />
Then animate them in JavaScript:
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
.skill-circle {
stroke : #E50914 ;
stroke-width : 10 ;
fill : transparent ;
stroke-dasharray : 314 ;
stroke-dashoffset : 314 ; /* Starts fully hidden */
animation : fillCircle 1.5 s forwards ;
}
@keyframes fillCircle {
to {
stroke-dashoffset : var ( --dash-offset ); /* Animates to target percentage */
}
}
Each skill circle has a --dash-offset CSS variable set inline:
< circle class = "skill-circle" cx = "60" cy = "60" r = "50"
style = "--dash-offset: 94.2;" data-value = "70" />
Step 1: Change Animation Duration
Modify the animation timing in css/estilos.css:36:
/* 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;
Step 2: Add Easing Functions
Create smoother or bouncier animations:
/* 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;
Step 3: Change Circle Colors
Update the stroke color in css/estilos.css:31:
.skill-circle {
stroke : #E50914 ; /* Change to your accent color */
}
Or create gradient strokes:
< 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)" ... />
Step 4: Stagger Animations
Make circles animate sequentially:
.skill-item:nth-child ( 1 ) .skill-circle {
animation : fillCircle 1.5 s 0 s forwards ;
}
.skill-item:nth-child ( 2 ) .skill-circle {
animation : fillCircle 1.5 s 0.2 s forwards ;
}
.skill-item:nth-child ( 3 ) .skill-circle {
animation : fillCircle 1.5 s 0.4 s forwards ;
}
.skill-item:nth-child ( 4 ) .skill-circle {
animation : fillCircle 1.5 s 0.6 s forwards ;
}
.skill-item:nth-child ( 5 ) .skill-circle {
animation : fillCircle 1.5 s 0.8 s 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
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 );
});
}
// 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 );
// 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%)` ;
Enable drawing on mobile devices:
// 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
Navigation Underline
The navigation links have an animated underline (css/estilos.css:83-96):
.nav-link::after {
content : '' ;
position : absolute ;
width : 0 ;
height : 2 px ;
bottom : -5 px ;
left : 0 ;
background-color : #E50914 ;
transition : width 0.3 s ease ;
}
.nav-link:hover::after {
width : 100 % ;
}
Faster Animation
Thicker Underline
Bounce Effect
transition: width 0 .15s ease; /* 150ms instead of 300ms */
height: 3px; /* 3px instead of 2px */
transition: width 0 .3s cubic-bezier(0 .68 , -0 .55 , 0 .265 , 1 .55 );
Buttons scale up on hover (css/estilos.css:22-28):
.hover-scale {
transition : transform 0.3 s 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 ( -10 px );
box-shadow : 0 10 px 20 px rgba ( 229 , 9 , 20 , 0.2 );
}
/* Higher lift (20px) */
transform: translateY(-20px);
/* Subtle lift (5px) */
transform: translateY(-5px);
/* Tilt effect */
transform: translateY(-10px) rotateX(5deg);
.project-card:hover {
transform : translateY ( -10 px );
box-shadow :
0 10 px 20 px rgba ( 229 , 9 , 20 , 0.2 ),
0 0 40 px rgba ( 229 , 9 , 20 , 0.3 ), /* Outer glow */
inset 0 0 20 px 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
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.5 s ease-in-out ;
}
.details-container.open {
max-height : 1000 px ;
}
/* Faster (250ms) */
transition: max-height 0 .25s ease-in-out ;
/* Slower (1s) */
transition: max-height 1s ease-in-out ;
/* 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 15 s infinite ease-in-out ;
}
@keyframes float {
0% , 100% {
transform : translateY ( 0 ) rotate ( 0 deg );
}
50% {
transform : translateY ( -20 px ) rotate ( 5 deg );
}
}
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 >
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.01 ms !important ; }
}
Animation Summary Reference
Particle System (js/fondo.js)
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)
SVG Animation (js/main.js:52-76)
Eye Movement : Lines 63-66 (multiplier * 0.05)
Mouth Shape : Line 67 (multiplier * 0.2)
Colors : index.html:91-94 (stroke/fill)
Skill Circles (css/estilos.css:30-43)
Duration : Line 36 (animation: fillCircle 1.5s)
Color : Line 31 (stroke: #E50914)
Dash Offset : index.html inline styles
Hover Effects (css/estilos.css)
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