animaciones-gsap — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited animaciones-gsap (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
defer.app.js. Nunca en archivos separados.gsap.context() para agrupar animaciones y facilitar cleanup.gsap.registerPlugin(ScrollTrigger);power2.out o power3.out.prefers-reduced-motion: desactiva animaciones si el usuario lo pide.document.addEventListener('DOMContentLoaded', () => {
gsap.registerPlugin(ScrollTrigger);
const ctx = gsap.context(() => {
// Aquí van todas las animaciones
initNavbar();
initHeroAnimations();
initScrollAnimations();
initInteractions();
});
// Cleanup si se necesita
// ctx.revert();
});const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
if (!prefersReducedMotion) {
// Inicializar animaciones
}Navbar tipo píldora flotante que cambia de transparente a sólida al hacer scroll.
function initNavbar() {
const navbar = document.querySelector('.navbar');
ScrollTrigger.create({
start: 'top -80',
onUpdate: (self) => {
if (self.direction === 1 && self.scroll() > 80) {
navbar.classList.add('navbar-scrolled');
}
if (self.scroll() < 80) {
navbar.classList.remove('navbar-scrolled');
}
}
});
}CSS necesario en styles.css:
.navbar {
position: fixed;
top: 1rem;
left: 50%;
transform: translateX(-50%);
z-index: 1000;
padding: 0.75rem 2rem;
border-radius: 9999px;
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
background: transparent;
}
.navbar-scrolled {
background: rgba(255, 255, 255, 0.85);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
box-shadow: 0 4px 30px rgba(0, 0, 0, 0.08);
border: 1px solid rgba(255, 255, 255, 0.3);
}Aparición escalonada de elementos del hero de abajo hacia arriba.
function initHeroAnimations() {
const heroElements = gsap.utils.toArray('.hero-animate');
gsap.from(heroElements, {
y: 60,
opacity: 0,
duration: 0.8,
stagger: 0.15,
ease: 'power3.out',
delay: 0.3
});
}HTML: añadir clase hero-animate a cada elemento que debe aparecer en secuencia.
<h1 class="hero-animate">Titular principal</h1>
<p class="hero-animate">Subtítulo descriptivo</p>
<div class="hero-animate">
<a href="#" class="btn-cta">CTA Principal</a>
<a href="#" class="btn-secondary">CTA Secundario</a>
</div>Elementos que aparecen suavemente al entrar en el viewport.
function initScrollAnimations() {
// Fade up general
gsap.utils.toArray('.animate-on-scroll').forEach(el => {
gsap.from(el, {
y: 40,
opacity: 0,
duration: 0.7,
ease: 'power2.out',
scrollTrigger: {
trigger: el,
start: 'top 85%',
toggleActions: 'play none none none'
}
});
});
// Entrada desde izquierda
gsap.utils.toArray('.animate-from-left').forEach(el => {
gsap.from(el, {
x: -60,
opacity: 0,
duration: 0.7,
ease: 'power2.out',
scrollTrigger: {
trigger: el,
start: 'top 85%',
toggleActions: 'play none none none'
}
});
});
// Entrada desde derecha
gsap.utils.toArray('.animate-from-right').forEach(el => {
gsap.from(el, {
x: 60,
opacity: 0,
duration: 0.7,
ease: 'power2.out',
scrollTrigger: {
trigger: el,
start: 'top 85%',
toggleActions: 'play none none none'
}
});
});
// Escalonado para grupos (cards, features, timeline steps)
gsap.utils.toArray('.animate-stagger-group').forEach(group => {
const items = group.querySelectorAll('.stagger-item');
gsap.from(items, {
y: 40,
opacity: 0,
duration: 0.6,
stagger: 0.12,
ease: 'power2.out',
scrollTrigger: {
trigger: group,
start: 'top 80%',
toggleActions: 'play none none none'
}
});
});
}Para secciones de manifiesto o titulares dramáticos.
function initSplitText(selector) {
const elements = document.querySelectorAll(selector);
elements.forEach(el => {
const text = el.textContent;
const words = text.split(' ');
el.innerHTML = words.map(word =>
`<span class="inline-block overflow-hidden"><span class="split-word inline-block">${word}</span></span>`
).join(' ');
gsap.from(el.querySelectorAll('.split-word'), {
y: '100%',
opacity: 0,
duration: 0.6,
stagger: 0.05,
ease: 'power3.out',
scrollTrigger: {
trigger: el,
start: 'top 80%',
toggleActions: 'play none none none'
}
});
});
}Efecto de terminal/telemetría con cursor intermitente.
function initTypewriter(selector, messages, speed = 50) {
const el = document.querySelector(selector);
let messageIndex = 0;
let charIndex = 0;
let isDeleting = false;
function type() {
const currentMessage = messages[messageIndex];
if (isDeleting) {
el.textContent = currentMessage.substring(0, charIndex - 1);
charIndex--;
} else {
el.textContent = currentMessage.substring(0, charIndex + 1);
charIndex++;
}
let delay = isDeleting ? speed / 2 : speed;
if (!isDeleting && charIndex === currentMessage.length) {
delay = 2000; // Pausa al completar
isDeleting = true;
} else if (isDeleting && charIndex === 0) {
isDeleting = false;
messageIndex = (messageIndex + 1) % messages.length;
delay = 500;
}
setTimeout(type, delay);
}
type();
}
// Uso:
// initTypewriter('.typewriter-text', [
// 'Optimizando tu embudo…',
// 'Generando landing…',
// 'Ajustando conversiones…'
// ]);CSS para el cursor:
.typewriter-container::after {
content: '|';
animation: blink 0.8s infinite;
color: var(--color-accent);
font-weight: 300;
}
@keyframes blink {
0%, 50% { opacity: 1; }
51%, 100% { opacity: 0; }
}Acordeón con transición suave de altura.
function initAccordion() {
const items = document.querySelectorAll('.accordion-item');
items.forEach(item => {
const trigger = item.querySelector('.accordion-trigger');
const content = item.querySelector('.accordion-content');
const icon = item.querySelector('.accordion-icon');
// Iniciar cerrado
gsap.set(content, { height: 0, overflow: 'hidden' });
trigger.addEventListener('click', () => {
const isOpen = item.classList.contains('is-open');
// Cerrar todos los demás
items.forEach(other => {
if (other !== item && other.classList.contains('is-open')) {
other.classList.remove('is-open');
gsap.to(other.querySelector('.accordion-content'), {
height: 0,
duration: 0.4,
ease: 'power2.inOut'
});
gsap.to(other.querySelector('.accordion-icon'), {
rotation: 0,
duration: 0.3
});
}
});
// Toggle actual
if (isOpen) {
item.classList.remove('is-open');
gsap.to(content, { height: 0, duration: 0.4, ease: 'power2.inOut' });
gsap.to(icon, { rotation: 0, duration: 0.3 });
} else {
item.classList.add('is-open');
gsap.to(content, { height: 'auto', duration: 0.4, ease: 'power2.inOut' });
gsap.to(icon, { rotation: 45, duration: 0.3 });
}
});
});
}Movimiento sutil del fondo al hacer scroll para dar profundidad.
function initParallax() {
gsap.utils.toArray('.parallax-bg').forEach(bg => {
gsap.to(bg, {
yPercent: -20,
ease: 'none',
scrollTrigger: {
trigger: bg.parentElement,
start: 'top bottom',
end: 'bottom top',
scrub: 1
}
});
});
}HTML necesario:
<section class="relative overflow-hidden">
<div class="parallax-bg absolute inset-0 -top-[20%] -bottom-[20%]">
<img src="imagen.jpg" class="w-full h-full object-cover" alt="Descripción">
</div>
<div class="relative z-10">
<!-- Contenido de la sección -->
</div>
</section>Para secciones de métricas o prueba social.
function initCounters() {
gsap.utils.toArray('.counter').forEach(counter => {
const target = parseInt(counter.getAttribute('data-target'));
const suffix = counter.getAttribute('data-suffix') || '';
gsap.from(counter, {
textContent: 0,
duration: 2,
ease: 'power1.out',
snap: { textContent: 1 },
scrollTrigger: {
trigger: counter,
start: 'top 85%',
toggleActions: 'play none none none'
},
onUpdate: function() {
counter.textContent = Math.ceil(parseFloat(counter.textContent)) + suffix;
}
});
});
}HTML:
<span class="counter" data-target="147" data-suffix="+">0</span>Toggle de menú en móvil con animación.
function initMobileMenu() {
const menuBtn = document.querySelector('.menu-toggle');
const mobileMenu = document.querySelector('.mobile-menu');
const menuLinks = mobileMenu.querySelectorAll('a');
let isOpen = false;
menuBtn.addEventListener('click', () => {
isOpen = !isOpen;
menuBtn.classList.toggle('is-active');
if (isOpen) {
gsap.to(mobileMenu, {
clipPath: 'circle(150% at top right)',
duration: 0.5,
ease: 'power3.inOut'
});
gsap.from(menuLinks, {
y: 20,
opacity: 0,
stagger: 0.08,
duration: 0.4,
delay: 0.2
});
} else {
gsap.to(mobileMenu, {
clipPath: 'circle(0% at top right)',
duration: 0.4,
ease: 'power2.in'
});
}
});
// Cerrar al hacer click en un link
menuLinks.forEach(link => {
link.addEventListener('click', () => {
isOpen = false;
menuBtn.classList.remove('is-active');
gsap.to(mobileMenu, {
clipPath: 'circle(0% at top right)',
duration: 0.4,
ease: 'power2.in'
});
});
});
}CSS inicial del menú:
.mobile-menu {
clip-path: circle(0% at top right);
position: fixed;
inset: 0;
z-index: 999;
}Indicador de estado tipo "sistema activo".
.pulse-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background-color: #22c55e;
position: relative;
display: inline-block;
}
.pulse-dot::before {
content: '';
position: absolute;
inset: -3px;
border-radius: 50%;
background-color: #22c55e;
animation: pulse-ring 2s ease-out infinite;
}
@keyframes pulse-ring {
0% { transform: scale(1); opacity: 0.5; }
100% { transform: scale(2.5); opacity: 0; }
}HTML:
<span class="pulse-dot"></span> Sistema activo~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.