From 9a63ffa890e64b09014a634084142f1d1c6336ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Garc=C3=ADa=20Santamar=C3=ADa?= <69808296+PGSCOM@users.noreply.github.com> Date: Sun, 31 May 2026 15:13:45 +0200 Subject: [PATCH 01/49] Initial commit --- src/pages/_styles.css | 29 +++++++++++++++++++++++ src/pages/index.astro | 11 +++++++++ src/scripts/motion-path-hero.js | 41 +++++++++++++++++++++++++++++++++ 3 files changed, 81 insertions(+) create mode 100644 src/scripts/motion-path-hero.js diff --git a/src/pages/_styles.css b/src/pages/_styles.css index 6c5bbcf..8a300cd 100644 --- a/src/pages/_styles.css +++ b/src/pages/_styles.css @@ -95,6 +95,35 @@ z-index: -2; } +.hero-motion-layer { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: 5; + pointer-events: none; +} + +.motion-path-svg { + position: absolute; + inset: 0; + width: 100%; + height: 100%; + pointer-events: none; +} + +.hero-dot { + position: absolute; + width: 40px; + height: 40px; + border-radius: 50%; + background: linear-gradient(135deg, #367e78, #0ea5e9); + box-shadow: 0 0 20px rgba(14, 165, 233, 0.4); + z-index: 4; + pointer-events: none; +} + /* Eliminamos el overlay global */ /* Contenido principal - lado izquierdo */ diff --git a/src/pages/index.astro b/src/pages/index.astro index 795dfba..a72e502 100644 --- a/src/pages/index.astro +++ b/src/pages/index.astro @@ -24,6 +24,12 @@ import contactoData from '../data/contacto.json'; Logo PGSCOM +
+ + + +
+
@@ -564,6 +570,11 @@ import contactoData from '../data/contacto.json'; import '../scripts/galaxia-scroll.js'; + + + + + + + + + - - - - + - - - -
- + + + +
-
- - - - - - +
+ {featured.map((proyecto, i) => { + const l = heroLayout[i]; + return ( + + + + {proyecto.titulo} + + {proyecto.titulo} + {proyecto.aptitudTitulo} + + + + + ); + })}
+
+

Desarrollo web · Audiovisual · Animación 3D

PGSCOM

+

Portfolio de Pablo García Santamaría — proyectos de código, cine y diseño.

+
+ +
Scroll
@@ -206,7 +288,7 @@ import contactoData from '../data/contacto.json';
-
+
@@ -461,9 +543,11 @@ import contactoData from '../data/contacto.json'; import '../scripts/galaxia-scroll.js'; - + diff --git a/src/scripts/hero.js b/src/scripts/hero.js new file mode 100644 index 0000000..9760ee6 --- /dev/null +++ b/src/scripts/hero.js @@ -0,0 +1,144 @@ +import gsap from 'gsap'; +import { ScrollTrigger } from 'gsap/ScrollTrigger'; +gsap.registerPlugin(ScrollTrigger); + +// ── Hero: cards de proyectos flotando alrededor del logo ───────────────────── +// Tres capas por card para que las animaciones no se pisen el transform: +// .hero-card → posición, entrada, parallax de ratón y scroll (GSAP) +// .hero-card-tilt → rotación base + flotación orgánica (GSAP) +// .hero-card-media → hover scale (CSS) + +const reduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches; + +function initHero() { + const cards = gsap.utils.toArray('.hero-card'); + const pEl = document.getElementById('letter-p'); + const gscomWrap = document.getElementById('letters-gscom-wrap'); + const gscomText = document.getElementById('letters-gscom'); + if (!pEl || !gscomWrap) return; + + const fadeEls = ['.hero-kicker', '.hero-sub', '.hero-actions', '.hero-scroll-hint'] + .map(s => document.querySelector(s)) + .filter(Boolean); + + // ── Estados iniciales (el preloader cubre la pantalla mientras tanto) ──── + gsap.set(cards, { xPercent: -50, yPercent: -50, autoAlpha: 0, scale: 0.55 }); + cards.forEach(card => { + const tilt = card.querySelector('.hero-card-tilt'); + gsap.set(tilt, { rotation: parseFloat(card.dataset.rot) || 0 }); + }); + gsap.set(gscomWrap, { width: 0, opacity: 0 }); + gsap.set(pEl, { scale: 0.4, autoAlpha: 0 }); + gsap.set(fadeEls, { autoAlpha: 0, y: 26 }); + + let entered = false; + + function startIdleFloat() { + if (reduced) return; + cards.forEach(card => { + const tilt = card.querySelector('.hero-card-tilt'); + const d = parseFloat(card.dataset.depth) || 1; + gsap.to(tilt, { + y: gsap.utils.random(10, 20) * (Math.random() < 0.5 ? -1 : 1) * d, + rotation: `+=${gsap.utils.random(-3, 3)}`, + duration: gsap.utils.random(3, 5), + yoyo: true, repeat: -1, ease: 'sine.inOut', + delay: gsap.utils.random(0, 1.5), + }); + }); + } + + function entrance() { + if (entered) return; + entered = true; + + // Medir GSCOM con la fuente ya cargada (el preloader espera fonts.ready) + gsap.set(gscomWrap, { width: 'auto' }); + const gscomWidth = gscomWrap.offsetWidth + 2; + gsap.set(gscomWrap, { width: 0 }); + + if (reduced) { + gsap.set([pEl, ...fadeEls], { autoAlpha: 1, scale: 1, y: 0, clearProps: 'transform' }); + gsap.set(gscomWrap, { width: 'auto', opacity: 1, overflow: 'visible' }); + gsap.set(cards, { autoAlpha: 1, scale: 1 }); + return; + } + + const tl = gsap.timeline({ defaults: { ease: 'power3.out' } }); + tl.to(pEl, { scale: 1, autoAlpha: 1, duration: 0.7, ease: 'back.out(1.8)' }) + .to(gscomWrap, { + width: gscomWidth, opacity: 1, duration: 0.9, ease: 'power3.inOut', + onComplete() { + gsap.set(gscomWrap, { width: 'auto', overflow: 'visible' }); + const grad = 'pgscom-intro-scroll 2s linear forwards, pgscom-loop-scroll 4s linear 2s infinite'; + pEl.style.animation = grad; + if (gscomText) gscomText.style.animation = grad; + }, + }, '-=0.35') + .to(cards, { + autoAlpha: 1, scale: 1, duration: 1, + ease: 'back.out(1.4)', + stagger: { each: 0.08, from: 'random' }, + }, 0.2); + + fadeEls.forEach((el, i) => { + tl.to(el, { autoAlpha: 1, y: 0, duration: 0.55 }, 0.55 + i * 0.12); + }); + + tl.call(startIdleFloat); + } + + // ── Parallax de ratón (solo puntero fino, fuera del hero se pausa) ─────── + if (!reduced && window.matchMedia('(pointer: fine)').matches && cards.length) { + let heroVisible = true; + const setters = cards.map(card => ({ + x: gsap.quickTo(card, 'x', { duration: 0.9, ease: 'power3' }), + y: gsap.quickTo(card, 'y', { duration: 0.9, ease: 'power3' }), + d: parseFloat(card.dataset.depth) || 1, + })); + + window.addEventListener('pointermove', (e) => { + if (!heroVisible || !entered) return; + const nx = e.clientX / window.innerWidth - 0.5; + const ny = e.clientY / window.innerHeight - 0.5; + for (const s of setters) { s.x(nx * 46 * s.d); s.y(ny * 28 * s.d); } + }, { passive: true }); + + ScrollTrigger.create({ + trigger: '.main-container', + start: 'top top', end: 'bottom top', + onToggle: self => { heroVisible = self.isActive; }, + }); + } + + // ── Salida con scroll: cada profundidad sube a distinta velocidad ──────── + if (!reduced) { + const exit = gsap.timeline({ + scrollTrigger: { + trigger: '.main-container', + start: 'top top', end: 'bottom top', + scrub: 0.8, + }, + }); + cards.forEach(card => { + const d = parseFloat(card.dataset.depth) || 1; + exit.to(card, { yPercent: -(50 + 55 * d), ease: 'none' }, 0); + }); + exit.to('.content', { yPercent: -22, autoAlpha: 0.15, ease: 'none' }, 0); + exit.to('.hero-scroll-hint', { autoAlpha: 0, ease: 'none', duration: 0.25 }, 0); + } + + // El preloader dispara la entrada; fallback por si ya no existe + if (!document.getElementById('preloader')) { + entrance(); + } else { + window.addEventListener('loading-done', entrance, { once: true }); + setTimeout(entrance, 9000); // red de seguridad si el preloader falla + } +} + +if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', initHero); +} else { + initHero(); +} diff --git a/src/scripts/motion-path-hero.js b/src/scripts/motion-path-hero.js deleted file mode 100644 index 5126593..0000000 --- a/src/scripts/motion-path-hero.js +++ /dev/null @@ -1,370 +0,0 @@ -import gsap from 'gsap'; -import { MotionPathPlugin } from 'gsap/MotionPathPlugin'; -import { ScrollTrigger } from 'gsap/ScrollTrigger'; -gsap.registerPlugin(MotionPathPlugin, ScrollTrigger); - -// ── Estado compartido entre fases de animación ──────────────────────────────── -let scrollTl = null; -let wrappers = []; -let dots = []; -let orbitTween = null; -let orbitState = { rot: 0 }; -let rPx = 0; -let count = 0; - -// ── Utilidades ──────────────────────────────────────────────────────────────── - -// Clona el ícono de una proyecto-card para usarlo dentro del orb -function cloneOrbIcon(card) { - const inner = document.createElement('div'); - inner.className = 'proyecto-card-inner'; - const img = card.querySelector('.proyecto-icon img'); - if (img) { - const wrap = document.createElement('div'); - wrap.className = 'proyecto-icon'; - const clone = img.cloneNode(true); - clone.removeAttribute('srcset'); - wrap.appendChild(clone); - inner.appendChild(wrap); - } - return inner; -} - -// Posición de pantalla donde cada orb aterriza en la fase de navegación -function getCornerPos(i, W, H, isMobile) { - const peaks = isMobile - ? [{ x: 0.15, y: 0.15 }, { x: 0.85, y: 0.25 }, { x: 0.20, y: 0.45 }, - { x: 0.80, y: 0.55 }, { x: 0.25, y: 0.70 }, { x: 0.75, y: 0.85 }] - : [{ x: 0.60, y: 0.20 }, { x: 0.85, y: 0.30 }, { x: 0.65, y: 0.45 }, - { x: 0.90, y: 0.55 }, { x: 0.55, y: 0.70 }, { x: 0.80, y: 0.80 }]; - const p = peaks[i] ?? { x: 0.5, y: 0.5 }; - return { x: p.x * W, y: p.y * H }; -} - -// Trayectoria cúbica en S para la re-entrada de los orbs al hacer scroll -function makeSwoopPath(W, destX, destY, i) { - const sweep = i % 2 === 0 ? 1 : -1; - const sx = W / 2 + sweep * (W * 0.4 + i * 30); - const sy = -200 - i * 60; - const dx = destX - sx; - const dy = destY - sy; - const c = 0.6 * sweep; - // Control points cruzados (perpendiculares al vector director) para la curva S - const cp1x = sx + dx * 0.1 + dy * c, cp1y = sy + dy * 0.1 - dx * c; - const cp2x = sx + dx * 0.9 + dy * c, cp2y = sy + dy * 0.9 - dx * c; - return `M ${sx},${sy} C ${cp1x},${cp1y} ${cp2x},${cp2y} ${destX},${destY}`; -} - -// ── Construcción de la escena ───────────────────────────────────────────────── - -function initHeroMotion() { - const layer = document.querySelector('.hero-motion-layer'); - const svg = document.querySelector('.motion-path-svg'); - if (!layer || !svg) return; - - const W = document.documentElement.clientWidth; - const H = window.innerHeight; - const isMobile = W < 768; - const cx = W / 2; - const cy = H / 2; - - svg.setAttribute('viewBox', `0 0 ${W} ${H}`); - - const cards = [...document.querySelectorAll('.proyecto-card')]; - count = cards.length; - if (!count) return; - - // Radio del anillo de carga y radio de la órbita en la sección de proyectos - const loadingR = isMobile ? 80 : 140; - rPx = Math.min(isMobile ? 130 : 220, W * (isMobile ? 0.35 : 0.22)); - - // ── Logo: medir y centrar antes de la animación ─────────────────────────── - const pEl = document.getElementById('letter-p'); - const gscomWrap = document.getElementById('letters-gscom-wrap'); - const gscomText = document.getElementById('letters-gscom'); - const logoWrap = document.getElementById('logo-wrapper'); - - // Medir el ancho natural de "GSCOM" antes de ocultarlo - gsap.set(gscomWrap, { width: 'auto' }); - const gscomWidth = gscomWrap.offsetWidth + 2; - gsap.set(gscomWrap, { width: 0, opacity: 0 }); - - // Desplazar el logo-wrapper para que la "P" quede centrada en pantalla. - // Los offsets ópticos compensan que el bounding-box del texto no coincide - // exactamente con el centro visual del glifo (métricas del tipo). - const OPTICAL_X = 0.14; // ajuste horizontal por el kerning/espacio lateral - const OPTICAL_Y = -0.12; // la "P" visual está más arriba que el centro del bbox - const pRect = pEl.getBoundingClientRect(); - const logoOffsetX = cx - (pRect.left + pRect.width / 2) + pRect.width * OPTICAL_X; - const logoOffsetY = cy - (pRect.top + pRect.height / 2) + pRect.height * OPTICAL_Y; - - gsap.set(logoWrap, { x: logoOffsetX, y: logoOffsetY }); - gsap.set(pEl, { scale: 0, opacity: 0 }); - - // ── Crear los orbs (punto de carga gris + ícono real) ───────────────────── - const grayDots = []; - const iconClones = []; - const floatFns = []; // se ejecutan al terminar la intro, no antes - - cards.forEach((card, i) => { - const ang = -Math.PI / 2 + (i / count) * Math.PI * 2; - const orbX = cx + Math.cos(ang) * rPx; // posición destino en la órbita de proyectos - const orbY = cy + Math.sin(ang) * rPx; - - // Capa de posicionamiento — la mueven los scroll timelines - const wrapper = document.createElement('div'); - wrapper.className = 'hero-dot-wrapper'; - Object.assign(wrapper.style, { - position: 'absolute', width: '100px', height: '100px', - left: '-50px', top: '-50px', zIndex: '4', pointerEvents: 'none', - }); - - // Capa de flotación orgánica — independiente del scroll - const floatEl = document.createElement('div'); - floatEl.className = 'hero-float-wrapper'; - Object.assign(floatEl.style, { position: 'absolute', width: '100%', height: '100%' }); - - const gray = document.createElement('div'); - gray.className = 'gray-loading-dot'; - - const icon = cloneOrbIcon(card); - icon.classList.add('orb-content-clone'); - - const dot = document.createElement('div'); - dot.className = 'hero-dot proyecto-orb'; - dot.appendChild(gray); - dot.appendChild(icon); - - floatEl.appendChild(dot); - wrapper.appendChild(floatEl); - layer.appendChild(wrapper); - - wrappers.push(wrapper); - dots.push(dot); - grayDots.push(gray); - iconClones.push(icon); - - gsap.set(wrapper, { x: cx, y: cy, scale: 0, opacity: 0 }); - - // Trayectoria SVG para la re-entrada de los orbs al hacer scroll - const path = document.createElementNS('http://www.w3.org/2000/svg', 'path'); - path.id = `scroll-path-${i}`; - path.setAttribute('d', makeSwoopPath(W, orbX, orbY, i)); - path.setAttribute('fill', 'none'); - path.setAttribute('stroke', 'transparent'); - svg.appendChild(path); - - // Guardamos la función de flotación para dispararla al terminar la intro - const dirY = Math.random() > 0.5 ? 18 : -18; - const dirX = Math.random() > 0.5 ? 12 : -12; - floatFns.push(() => gsap.to(floatEl, { - y: `+=${dirY}`, x: `+=${dirX}`, rotation: Math.random() * 8 - 4, - duration: 2.5 + Math.random() * 2, - yoyo: true, repeat: -1, ease: 'sine.inOut', delay: Math.random() * 2, - })); - }); - - // ── TIMELINE MAESTRO ────────────────────────────────────────────────────── - // Fase 1 — la "P" nace en el centro de la pantalla - // Fase 2 — los puntos de carga aparecen en anillo y orbitan - // Fase 3 — explosión: el logo ocupa su lugar, los orbs vuelan a esquinas - - const masterTl = gsap.timeline(); - const orbitProxy = { angle: 0 }; - - masterTl - // La P aparece y se hincha - .to(pEl, { scale: 1, opacity: 1, duration: 0.6, ease: 'back.out(2)' }) - .to(pEl, { scale: 1.4, duration: 0.6, ease: 'power2.inOut' }, '+=0.2') - - // Los puntos de carga aparecen en el anillo y dan media vuelta - .to(wrappers, { - scale: 1, opacity: 1, rotation: '+=180', - duration: 0.4, stagger: 0.05, ease: 'back.out(2)', - onStart() { - wrappers.forEach((w, i) => { - const a = -Math.PI / 2 + (i / count) * Math.PI * 2; - gsap.set(w, { x: cx + Math.cos(a) * loadingR, y: cy + Math.sin(a) * loadingR }); - }); - }, - }, '<0.1') - - // El anillo orbita media vuelta para dar la sensación de carga - .to(orbitProxy, { - angle: Math.PI, duration: 1.2, ease: 'power1.inOut', - onUpdate() { - wrappers.forEach((w, i) => { - const a = -Math.PI / 2 + (i / count) * Math.PI * 2 + orbitProxy.angle; - gsap.set(w, { x: cx + Math.cos(a) * loadingR, y: cy + Math.sin(a) * loadingR }); - }); - }, - }, '<') - - // La P vuelve a su tamaño normal antes de la explosión - .to(pEl, { scale: 1, duration: 0.6, ease: 'power2.inOut' }, '+=0.2') - - // ── EXPLOSIÓN — todo ocurre en paralelo desde aquí ── - .add('explode') - - // El logo-wrapper retorna a su posición natural en el layout - .to(logoWrap, { - x: 0, y: 0, duration: 1.2, ease: 'power3.inOut', - onComplete() { - document.documentElement.style.overflow = ''; - gsap.set(logoWrap, { clearProps: 'all' }); - gsap.set(gscomWrap, { width: 'auto', overflow: 'visible' }); - - const gradAnim = 'pgscom-intro-scroll 2s linear forwards, pgscom-loop-scroll 4s linear 2s infinite'; - pEl.style.animation = gradAnim; - gscomText.style.animation = gradAnim; - - // Solo ahora que los orbs están en esquinas arrancamos la flotación - floatFns.forEach(fn => fn()); - - window.dispatchEvent(new CustomEvent('loading-done')); - }, - }, 'explode') - - // GSCOM se despliega horizontalmente - .to(gscomWrap, { width: gscomWidth, opacity: 1, duration: 1.2, ease: 'power3.inOut' }, 'explode') - - // Los puntos grises mutan a iconos reales - .to(grayDots, { opacity: 0, scale: 0, duration: 0.4, ease: 'power2.in' }, 'explode') - .to(iconClones, { opacity: 1, scale: 1, duration: 0.8, ease: 'back.out(1.5)' }, 'explode+=0.2'); - - // Los orbs vuelan en paralelo a sus posiciones de esquina - wrappers.forEach((w, i) => { - const { x, y } = getCornerPos(i, W, H, isMobile); - masterTl.to(w, { x, y, duration: 1.2 + (i % 2) * 0.1, ease: 'power3.out' }, 'explode'); - }); - - // ── SALIDA POR SCROLL (hero → proyectos) ───────────────────────────────── - const exitTl = gsap.timeline({ - scrollTrigger: { - trigger: '.main-container', - start: () => H * 0.45, - end: () => H * 0.85, - scrub: 1, - }, - }); - wrappers.forEach((w, i) => { - exitTl.to(w, { - y: `-=${H * 0.5}`, - x: `+=${(i % 2 === 0 ? -1 : 1) * 200}`, - opacity: 0, scale: 0.5, - rotation: i % 2 === 0 ? -45 : 45, - duration: 0.8, ease: 'power2.in', - }, i * 0.05); - }); - - // ── RE-ENTRADA EN PROYECTOS (los orbs vuelven en curva al hacer scroll) ── - scrollTl = gsap.timeline({ - scrollTrigger: { - trigger: '.main-container', - start: () => 2 * H, - endTrigger: '.proyectos-logo', - end: 'center center', - scrub: 1.5, - onLeave: startOrbit, - onEnterBack: stopOrbit, - }, - }); - wrappers.forEach((w, i) => { - scrollTl.fromTo(w, - { opacity: 0, scale: 0.2, rotation: -180 }, - { - opacity: 1, scale: 1, rotation: 360, - duration: 1.2, ease: 'power2.inOut', - motionPath: { path: `#scroll-path-${i}`, align: `#scroll-path-${i}`, alignOrigin: [0.5, 0.5] }, - }, - i * 0.08, - ); - }); -} - -// ── Órbita continua en la sección de proyectos ─────────────────────────────── - -function startOrbit() { - const layer = document.querySelector('.hero-motion-layer'); - - if (scrollTl?.scrollTrigger) { - const { end } = scrollTl.scrollTrigger; - if (layer) { layer.style.position = 'absolute'; layer.style.top = `${end}px`; } - if (scrollTl.progress() < 1) scrollTl.progress(1); - } - - orbitTween?.kill(); - orbitState.rot = 0; - - orbitTween = gsap.to(orbitState, { - rot: Math.PI * 2, - duration: 30, ease: 'none', repeat: -1, - onUpdate() { - dots.forEach((dot, i) => { - const base = -Math.PI / 2 + (i / count) * Math.PI * 2; - // Desplazamiento relativo al wrapper (que ya está en posición de órbita) - gsap.set(dot, { - x: (Math.cos(base + orbitState.rot) - Math.cos(base)) * rPx, - y: (Math.sin(base + orbitState.rot) - Math.sin(base)) * rPx, - }); - }); - }, - }); -} - -function stopOrbit() { - const layer = document.querySelector('.hero-motion-layer'); - if (layer) { layer.style.position = 'fixed'; layer.style.top = '0'; } - - orbitTween?.kill(); - orbitTween = null; - - dots.forEach(dot => gsap.to(dot, { x: 0, y: 0, duration: 0.8, ease: 'power2.out', overwrite: 'auto' })); -} - -// ── Bootstrap ───────────────────────────────────────────────────────────────── - -async function safeInit() { - if ('scrollRestoration' in history) history.scrollRestoration = 'manual'; - window.scrollTo(0, 0); - document.documentElement.style.overflow = 'hidden'; - const logoWrap = document.getElementById('logo-wrapper'); - if (logoWrap) logoWrap.style.opacity = '0'; - - // Forzar que PGSText esté descargada antes de medir el layout. - // El timeout de 3s es el fallback por si el woff2 no responde. - try { - await Promise.race([ - document.fonts.load('400 5rem "PGSText"'), - new Promise(r => setTimeout(r, 3000)), - ]); - } catch {} - - if (logoWrap) logoWrap.style.opacity = ''; - initHeroMotion(); -} - -function cleanup() { - ScrollTrigger.getAll().forEach(st => st.kill()); - orbitTween?.kill(); - orbitTween = null; - scrollTl = null; - wrappers.forEach(w => w.remove()); - wrappers = []; - dots = []; - document.querySelector('.motion-path-svg')?.replaceChildren(); -} - -// Bfcache: el navegador restaura la página del caché sin re-ejecutar scripts. -// GSAP queda en estado inconsistente → limpiamos todo y arrancamos de cero. -window.addEventListener('pageshow', (e) => { - if (!e.persisted) return; - cleanup(); - safeInit(); -}); - -if (document.readyState === 'loading') { - document.addEventListener('DOMContentLoaded', safeInit); -} else { - safeInit(); -} \ No newline at end of file diff --git a/src/scripts/preloader.js b/src/scripts/preloader.js new file mode 100644 index 0000000..8ee27db --- /dev/null +++ b/src/scripts/preloader.js @@ -0,0 +1,80 @@ +import gsap from 'gsap'; + +// ── Preloader: la P de carga.svg como máscara con collage de proyectos ──────── +// El overlay viene renderizado desde el servidor (index.astro) para que cubra +// la pantalla desde el primer paint. Este script solo lo anima y lo retira. + +const TRACK_STEP = 364; // alto de cada imagen dentro del clip (unidades del viewBox) + +function dispatchDone() { + document.documentElement.style.overflow = ''; + window.dispatchEvent(new CustomEvent('loading-done')); +} + +function initPreloader() { + const pre = document.getElementById('preloader'); + if (!pre) { dispatchDone(); return; } + + const svg = pre.querySelector('.pl-svg'); + const track = pre.querySelector('.pl-track'); + const count = pre.querySelector('.pl-count'); + const meta = pre.querySelector('.pl-meta'); + + if ('scrollRestoration' in history) history.scrollRestoration = 'manual'; + window.scrollTo(0, 0); + document.documentElement.style.overflow = 'hidden'; + + const reduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches; + if (reduced) { + count.textContent = '100'; + dispatchDone(); + gsap.to(pre, { autoAlpha: 0, duration: 0.4, delay: 0.3, onComplete: () => pre.remove() }); + return; + } + + const images = track.querySelectorAll('image'); + const distance = TRACK_STEP * (images.length - 1); + const state = { p: 0 }; + + const render = () => { + count.textContent = String(Math.round(state.p)); + gsap.set(track, { y: -(state.p / 100) * distance }); + }; + + // Avanza hasta 90 mientras carga de verdad; el 100 llega con el load real + const crawl = gsap.to(state, { p: 90, duration: 2.6, ease: 'power1.inOut', onUpdate: render }); + + const ready = Promise.all([ + document.readyState === 'complete' + ? Promise.resolve() + : new Promise(r => window.addEventListener('load', r, { once: true })), + document.fonts?.ready ?? Promise.resolve(), + new Promise(r => setTimeout(r, 1700)), // duración mínima para que la intro respire + ]); + + ready.then(() => { + crawl.kill(); + gsap.timeline() + .to(state, { p: 100, duration: 0.5, ease: 'power2.out', onUpdate: render }) + .to(meta, { autoAlpha: 0, y: -24, duration: 0.4, ease: 'power2.in' }, '<') + // El hero arranca su entrada debajo mientras la P hace zoom + .add(dispatchDone, '+=0.1') + // Zoom a través de la P: transform compositado sobre el propio + .to(svg, { scale: 30, duration: 1.1, ease: 'power3.in' }) + .to(pre, { autoAlpha: 0, duration: 0.4, ease: 'power1.out' }, '-=0.4') + .call(() => pre.remove()); + }); +} + +// Bfcache: si el navegador restaura la página, el preloader ya no pinta nada +window.addEventListener('pageshow', (e) => { + if (!e.persisted) return; + document.getElementById('preloader')?.remove(); + document.documentElement.style.overflow = ''; +}); + +if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', initPreloader); +} else { + initPreloader(); +} diff --git a/src/scripts/proyectos-orbit.js b/src/scripts/proyectos-orbit.js new file mode 100644 index 0000000..b19c229 --- /dev/null +++ b/src/scripts/proyectos-orbit.js @@ -0,0 +1,65 @@ +import gsap from 'gsap'; +import { ScrollTrigger } from 'gsap/ScrollTrigger'; +gsap.registerPlugin(ScrollTrigger); + +// ── Órbita de categorías alrededor del logo en la sección de proyectos ─────── +// Usa las .proyecto-card reales (enlaces a cada bloque), sin clones. +// El tween solo corre mientras la sección está en viewport. + +function initOrbit() { + const ring = document.querySelector('.proyectos-grid'); + if (!ring) return; + const cards = gsap.utils.toArray('.proyecto-card', ring); + if (!cards.length) return; + + const reduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches; + const proxy = { a: 0 }; + let radius = 0; + + const measure = () => { + const rect = ring.getBoundingClientRect(); + const card = cards[0].getBoundingClientRect(); + radius = Math.min(rect.width, rect.height) / 2 - card.width / 2 - 8; + }; + + const place = () => { + cards.forEach((card, i) => { + const ang = -Math.PI / 2 + (i / cards.length) * Math.PI * 2 + proxy.a; + gsap.set(card, { x: Math.cos(ang) * radius, y: Math.sin(ang) * radius }); + }); + }; + + gsap.set(cards, { xPercent: -50, yPercent: -50 }); + measure(); + place(); + + gsap.from(cards, { + scale: 0, autoAlpha: 0, duration: 0.7, ease: 'back.out(1.6)', + stagger: 0.06, + scrollTrigger: { trigger: '.proyectos-hero', start: 'top 75%' }, + }); + + if (!reduced) { + const spin = gsap.to(proxy, { + a: Math.PI * 2, duration: 48, repeat: -1, ease: 'none', + onUpdate: place, paused: true, + }); + ScrollTrigger.create({ + trigger: '.proyectos-hero', + start: 'top bottom', end: 'bottom top', + onToggle: self => (self.isActive ? spin.play() : spin.pause()), + }); + } + + let resizeRaf = 0; + window.addEventListener('resize', () => { + if (resizeRaf) return; + resizeRaf = requestAnimationFrame(() => { resizeRaf = 0; measure(); place(); }); + }); +} + +if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', initOrbit); +} else { + initOrbit(); +} From 657b7b2b7ffd089bb8daac505fc326133ba7d7c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Garc=C3=ADa=20Santamar=C3=ADa?= <69808296+PGSCOM@users.noreply.github.com> Date: Wed, 10 Jun 2026 21:27:03 +0200 Subject: [PATCH 21/49] Modo claro --- src/layouts/Layout.astro | 2 -- src/pages/_styles.css | 66 +++++++++++++++++++--------------------- src/pages/index.astro | 2 +- 3 files changed, 33 insertions(+), 37 deletions(-) diff --git a/src/layouts/Layout.astro b/src/layouts/Layout.astro index 1870881..40769c3 100644 --- a/src/layouts/Layout.astro +++ b/src/layouts/Layout.astro @@ -51,7 +51,5 @@ font-family: 'Montserrat', sans-serif; overflow-x: clip; /* Usar clip en lugar de hidden para evitar conflictos con sticky */ /* overflow-y: auto; - Se permite por defecto */ - background: #05060a; /* tema oscuro: sin flash blanco antes del preloader */ - color-scheme: dark; } diff --git a/src/pages/_styles.css b/src/pages/_styles.css index 573e4b8..d3d2dcb 100644 --- a/src/pages/_styles.css +++ b/src/pages/_styles.css @@ -13,7 +13,7 @@ position: fixed; inset: 0; z-index: 9999; - background: #05060a; + background: #f5f5f7; display: grid; place-items: center; overflow: hidden; @@ -29,7 +29,7 @@ .pl-outline { fill: none; - stroke: rgba(255, 255, 255, 0.14); + stroke: rgba(0, 0, 0, 0.12); stroke-width: 2; } @@ -44,14 +44,14 @@ } .pl-word { - color: rgba(255, 255, 255, 0.55); + color: rgba(0, 0, 0, 0.5); font-size: clamp(0.7rem, 1.2vw, 0.85rem); letter-spacing: 0.35em; text-transform: uppercase; } .pl-count { - color: #fff; + color: #111; font-size: clamp(3.5rem, 9vw, 7rem); font-weight: 700; line-height: 1; @@ -61,7 +61,7 @@ .pl-count::after { content: '%'; font-size: 0.35em; - color: rgba(255, 255, 255, 0.45); + color: rgba(0, 0, 0, 0.35); margin-left: 0.15em; } @@ -78,10 +78,10 @@ align-items: center; justify-content: center; background: - radial-gradient(60vw 45vh at 72% 18%, rgba(37, 99, 235, 0.12), transparent 60%), - radial-gradient(50vw 40vh at 18% 78%, rgba(255, 90, 205, 0.08), transparent 60%), - radial-gradient(45vw 35vh at 50% 50%, rgba(54, 126, 120, 0.10), transparent 65%), - #06070d; + radial-gradient(60vw 45vh at 72% 18%, rgba(37, 99, 235, 0.06), transparent 60%), + radial-gradient(50vw 40vh at 18% 78%, rgba(255, 90, 205, 0.04), transparent 60%), + radial-gradient(45vw 35vh at 50% 50%, rgba(54, 126, 120, 0.05), transparent 65%), + #f5f7fa; } /* Retícula de puntos sutil, desvanecida hacia los bordes */ @@ -89,7 +89,7 @@ content: ''; position: absolute; inset: 0; - background-image: radial-gradient(rgba(255, 255, 255, 0.07) 1px, transparent 1.5px); + background-image: radial-gradient(rgba(0, 0, 0, 0.05) 1px, transparent 1.5px); background-size: 34px 34px; -webkit-mask-image: radial-gradient(ellipse at center, black 25%, transparent 78%); mask-image: radial-gradient(ellipse at center, black 25%, transparent 78%); @@ -121,13 +121,13 @@ font-size: clamp(0.7rem, 1.1vw, 0.85rem); letter-spacing: 0.32em; text-transform: uppercase; - color: rgba(255, 255, 255, 0.55); + color: rgba(0, 0, 0, 0.5); } .hero-sub { font-family: 'Chakra Petch', sans-serif; font-size: clamp(0.9rem, 1.4vw, 1.1rem); - color: rgba(255, 255, 255, 0.65); + color: rgba(0, 0, 0, 0.6); max-width: 560px; line-height: 1.6; } @@ -145,9 +145,9 @@ font-size: 0.9rem; font-weight: 600; letter-spacing: 0.06em; - color: #06070d; - background: #fff; - border: 1px solid #fff; + color: #fff; + background: #1a1a2e; + border: 1px solid #1a1a2e; border-radius: 999px; padding: 13px 30px; text-decoration: none; @@ -156,18 +156,18 @@ .hero-cta:hover { transform: translateY(-2px); - box-shadow: 0 10px 35px rgba(255, 255, 255, 0.18); + box-shadow: 0 10px 35px rgba(0, 0, 0, 0.15); } .hero-cta--ghost { background: transparent; - color: rgba(255, 255, 255, 0.85); - border-color: rgba(255, 255, 255, 0.25); + color: rgba(0, 0, 0, 0.8); + border-color: rgba(0, 0, 0, 0.25); } .hero-cta--ghost:hover { - border-color: rgba(255, 255, 255, 0.6); - box-shadow: 0 10px 35px rgba(255, 255, 255, 0.08); + border-color: rgba(0, 0, 0, 0.5); + box-shadow: 0 10px 35px rgba(0, 0, 0, 0.06); } /* Indicador de scroll */ @@ -185,7 +185,7 @@ font-size: 0.65rem; letter-spacing: 0.3em; text-transform: uppercase; - color: rgba(255, 255, 255, 0.45); + color: rgba(0, 0, 0, 0.4); pointer-events: none; } @@ -193,7 +193,7 @@ content: ''; width: 1px; height: 34px; - background: linear-gradient(to bottom, transparent, rgba(255, 255, 255, 0.6)); + background: linear-gradient(to bottom, transparent, rgba(0, 0, 0, 0.4)); animation: scroll-hint-drop 2s ease-in-out infinite; } @@ -230,7 +230,7 @@ } .hero-card.is-far img { - filter: brightness(0.7) saturate(0.85); + filter: brightness(0.9) saturate(0.85); } .hero-card-tilt { @@ -243,19 +243,19 @@ display: block; border-radius: 14px; overflow: hidden; - background: #10131c; + background: #e8ecf0; box-shadow: - 0 24px 60px rgba(0, 0, 0, 0.55), - 0 0 0 1px rgba(255, 255, 255, 0.08); + 0 24px 60px rgba(0, 0, 0, 0.12), + 0 0 0 1px rgba(0, 0, 0, 0.06); transition: transform 0.35s cubic-bezier(0.22, 1, 0.36, 1), box-shadow 0.35s ease; } .hero-card:hover .hero-card-media { transform: scale(1.05); box-shadow: - 0 30px 70px rgba(0, 0, 0, 0.6), - 0 0 0 1px rgba(255, 255, 255, 0.2), - 0 0 45px rgba(147, 197, 253, 0.12); + 0 30px 70px rgba(0, 0, 0, 0.15), + 0 0 0 1px rgba(0, 0, 0, 0.1), + 0 0 45px rgba(37, 99, 235, 0.1); } .hero-card-media img { @@ -272,7 +272,7 @@ flex-direction: column; gap: 2px; padding: 24px 12px 10px; - background: linear-gradient(to top, rgba(4, 5, 10, 0.92), transparent); + background: linear-gradient(to top, rgba(255, 255, 255, 0.95), transparent); opacity: 0; translate: 0 8px; transition: opacity 0.3s ease, translate 0.3s ease; @@ -286,7 +286,7 @@ .hero-card-label strong { font-family: 'Chakra Petch', sans-serif; font-size: 0.8rem; - color: #fff; + color: #111; line-height: 1.2; } @@ -296,7 +296,7 @@ font-size: 0.65rem; letter-spacing: 0.12em; text-transform: uppercase; - color: rgba(255, 255, 255, 0.55); + color: rgba(0, 0, 0, 0.5); } /* ============================================ @@ -343,8 +343,6 @@ -webkit-background-clip: text; background-clip: text; color: transparent; - /* Sobre fondo oscuro, las letras parten de un tono claro del degradado */ - background-position: 35% 50%; } #letter-p { diff --git a/src/pages/index.astro b/src/pages/index.astro index ad1ae23..e24ad56 100644 --- a/src/pages/index.astro +++ b/src/pages/index.astro @@ -75,7 +75,7 @@ const heroLayout = [ /> ))} - + From 4e5c42f3bd611b618bfcf2bb88e48913e27e213d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Garc=C3=ADa=20Santamar=C3=ADa?= <69808296+PGSCOM@users.noreply.github.com> Date: Wed, 10 Jun 2026 21:29:32 +0200 Subject: [PATCH 22/49] =?UTF-8?q?Fix=20tama=C3=B1o=20m=C3=A1scara?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/scripts/galaxia-scroll.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/scripts/galaxia-scroll.js b/src/scripts/galaxia-scroll.js index 3d448a6..02b9f89 100644 --- a/src/scripts/galaxia-scroll.js +++ b/src/scripts/galaxia-scroll.js @@ -27,7 +27,7 @@ function init() { onUpdate() { const p = this.progress(); const scale = 1 + 24 * p * p * p; - const s = 32.3 * scale - 8; + const s = 34 * scale - 8; maskEl.style.webkitMaskSize = `${s}vh ${s}vh`; maskEl.style.maskSize = `${s}vh ${s}vh`; } From 9544b493787143a0c8089acc4f20fe25f325c2d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Garc=C3=ADa=20Santamar=C3=ADa?= <69808296+PGSCOM@users.noreply.github.com> Date: Wed, 10 Jun 2026 21:29:49 +0200 Subject: [PATCH 23/49] Fix degradado a blanco de parte 2 --- src/layouts/Layout.astro | 1 + src/pages/_styles.css | 20 ++++++++++++++++---- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/src/layouts/Layout.astro b/src/layouts/Layout.astro index 40769c3..0bca5e1 100644 --- a/src/layouts/Layout.astro +++ b/src/layouts/Layout.astro @@ -51,5 +51,6 @@ font-family: 'Montserrat', sans-serif; overflow-x: clip; /* Usar clip en lugar de hidden para evitar conflictos con sticky */ /* overflow-y: auto; - Se permite por defecto */ + background: #f5f7fa; } diff --git a/src/pages/_styles.css b/src/pages/_styles.css index d3d2dcb..9d59fa1 100644 --- a/src/pages/_styles.css +++ b/src/pages/_styles.css @@ -78,10 +78,9 @@ align-items: center; justify-content: center; background: - radial-gradient(60vw 45vh at 72% 18%, rgba(37, 99, 235, 0.06), transparent 60%), - radial-gradient(50vw 40vh at 18% 78%, rgba(255, 90, 205, 0.04), transparent 60%), - radial-gradient(45vw 35vh at 50% 50%, rgba(54, 126, 120, 0.05), transparent 65%), - #f5f7fa; + radial-gradient(60vw 45vh at 72% 18%, rgba(37, 99, 235, 0.20), transparent 60%), + radial-gradient(50vw 40vh at 18% 78%, rgba(255, 90, 205, 0.15), transparent 60%), + radial-gradient(45vw 35vh at 50% 50%, rgba(54, 126, 120, 0.18), transparent 65%); } /* Retícula de puntos sutil, desvanecida hacia los bordes */ @@ -96,6 +95,18 @@ pointer-events: none; } +/* Fundido inferior: de transparente al color de parte2-wrapper */ +.main-container::after { + content: ''; + position: absolute; + bottom: 0; + left: 0; + width: 100%; + height: 30vh; + background: linear-gradient(to bottom, transparent, #f5f7fa); + pointer-events: none; +} + /* Contenido central */ .content { position: relative; @@ -410,6 +421,7 @@ display: block; width: 100%; z-index: 50; + background: #f5f7fa; } .parte2 { From efc95320189328dbbad551efa5f29ee3e6f77bc5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Garc=C3=ADa=20Santamar=C3=ADa?= <69808296+PGSCOM@users.noreply.github.com> Date: Wed, 10 Jun 2026 21:29:57 +0200 Subject: [PATCH 24/49] Fix color logo scroll --- public/assets/animlogo/logo.svg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/assets/animlogo/logo.svg b/public/assets/animlogo/logo.svg index ab845fa..fd6fb0e 100644 --- a/public/assets/animlogo/logo.svg +++ b/public/assets/animlogo/logo.svg @@ -63,7 +63,7 @@ transform="translate(23.0118, 50.3065)"> From 7a2ec5ac1b3ccadb5909b97319932f62f1a391ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Garc=C3=ADa=20Santamar=C3=ADa?= <69808296+PGSCOM@users.noreply.github.com> Date: Wed, 10 Jun 2026 22:30:11 +0200 Subject: [PATCH 25/49] Logo redondeado --- public/assets/animlogo/logo.svg | 39 ++------------------------------- src/scripts/galaxia-scroll.js | 2 +- 2 files changed, 3 insertions(+), 38 deletions(-) diff --git a/public/assets/animlogo/logo.svg b/public/assets/animlogo/logo.svg index fd6fb0e..e111f88 100644 --- a/public/assets/animlogo/logo.svg +++ b/public/assets/animlogo/logo.svg @@ -5,44 +5,11 @@ viewBox="0 0 192.35219 192.35229" version="1.1" id="svg8" - inkscape:version="1.4.2 (f4327f4, 2025-05-13)" - sodipodi:docname="logo.svg" - inkscape:export-filename="C:\Users\pablo\Google Drive\Logo svg\Logocircular.png" - inkscape:export-xdpi="333.27728" - inkscape:export-ydpi="333.27728" - xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" - xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/"> - diff --git a/src/scripts/galaxia-scroll.js b/src/scripts/galaxia-scroll.js index 02b9f89..87d4595 100644 --- a/src/scripts/galaxia-scroll.js +++ b/src/scripts/galaxia-scroll.js @@ -27,7 +27,7 @@ function init() { onUpdate() { const p = this.progress(); const scale = 1 + 24 * p * p * p; - const s = 34 * scale - 8; + const s = 33 * scale - 8; maskEl.style.webkitMaskSize = `${s}vh ${s}vh`; maskEl.style.maskSize = `${s}vh ${s}vh`; } From 1aa10da374c5464ea272e823c22d3b3c29ab803f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Garc=C3=ADa=20Santamar=C3=ADa?= <69808296+PGSCOM@users.noreply.github.com> Date: Wed, 10 Jun 2026 22:57:38 +0200 Subject: [PATCH 26/49] Cambio pantalla de carga --- src/pages/_styles.css | 22 ++++++++++++++++++++-- src/pages/index.astro | 30 +++--------------------------- src/scripts/preloader.js | 37 +++++++++++++++++++++++-------------- 3 files changed, 46 insertions(+), 43 deletions(-) diff --git a/src/pages/_styles.css b/src/pages/_styles.css index 9d59fa1..6cebf8e 100644 --- a/src/pages/_styles.css +++ b/src/pages/_styles.css @@ -22,17 +22,35 @@ .pl-svg { width: min(56vmin, 420px); height: min(56vmin, 420px); - /* Origen del zoom: interior del lóbulo de la P (350/600, 214/600) */ - transform-origin: 58.3% 35.7%; + /* Origen del zoom: centro real del glifo de la P. + BBox de la P en el viewBox 600: x[185,427] y[93,456] → centro (306,275) + → (306/600, 275/600) = (51%, 45.8%). Así crece desde su propio centro. */ + transform-origin: 51% 45.8%; will-change: transform; } +/* Guía tenue de la forma completa, siempre visible bajo el trazo */ .pl-outline { fill: none; stroke: rgba(0, 0, 0, 0.12); stroke-width: 2; } +/* Trazo que construye la P (DrawSVG, ligado al progreso de carga). + Oculto hasta que el JS fija drawSVG:0% para evitar ver el trazo completo. */ +.pl-draw { + fill: none; + stroke-width: 7; + stroke-linecap: round; + stroke-linejoin: round; + visibility: hidden; +} + +/* Relleno con degradado que aparece al completar la carga */ +.pl-fill { + opacity: 0; +} + .pl-meta { position: absolute; left: clamp(20px, 5vw, 56px); diff --git a/src/pages/index.astro b/src/pages/index.astro index e24ad56..51af3ef 100644 --- a/src/pages/index.astro +++ b/src/pages/index.astro @@ -12,13 +12,6 @@ import contactoData from '../data/contacto.json'; // Forma de la P extraída de public/assets/animlogo/carga.svg (viewBox 600x600) const pPath = 'M 185.06066,92.957646 V 214.07196 456.30058 H 306.17677 V 335.19165 c 66.88974,0 121.11609,-54.22597 121.11609,-121.11609 0,-66.88986 -54.22635,-121.114324 -121.11609,-121.114324 v -0.004 z'; -// Imágenes del collage del preloader (deben existir en /public) -const preloaderImgs = [ - '/assets/proyectos/portfolio.png', - '/assets/proyectos/Macula.png', - '/assets/proyectos/UI.png', -]; - // Proyectos destacados que flotan en el hero (con imagen existente) const featuredIds = ['portfolio-pgscom', 'premio-macula-2024', 'diseño-web', 'portafolio-cine', 'python-tools', 'blog-personal']; const featured = []; @@ -48,36 +41,19 @@ const heroLayout = [ - + ); })}
@@ -724,6 +726,12 @@ const heroLayout = [ } }); + document.querySelectorAll('.hero-card').forEach(card => { + card.addEventListener('click', () => { + openModal(card.dataset.proyectoId); + }); + }); + document.querySelectorAll('.proyecto-detail-card').forEach(card => { card.addEventListener('click', (e) => { if (e.target.closest('.card-btn-link-float')) return; From 3ae93825ae63dbdfcdf356c2214f9957e7c7ad53 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Garc=C3=ADa=20Santamar=C3=ADa?= <69808296+PGSCOM@users.noreply.github.com> Date: Wed, 10 Jun 2026 23:08:24 +0200 Subject: [PATCH 29/49] =?UTF-8?q?Bot=C3=B3n=20contacto=20eliminado?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/index.astro | 1 - 1 file changed, 1 deletion(-) diff --git a/src/pages/index.astro b/src/pages/index.astro index 083a2a0..49e41f6 100644 --- a/src/pages/index.astro +++ b/src/pages/index.astro @@ -96,7 +96,6 @@ const heroLayout = [

Portfolio de Pablo García Santamaría — proyectos de código, cine y diseño.

From 8fdd3020a78ebc02901a48cf1d5936d70f4b11a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Garc=C3=ADa=20Santamar=C3=ADa?= <69808296+PGSCOM@users.noreply.github.com> Date: Wed, 10 Jun 2026 23:21:04 +0200 Subject: [PATCH 30/49] =?UTF-8?q?Animaci=C3=B3n=20scroll=20al=20pulsar=20"?= =?UTF-8?q?Ver=20Proyecetos"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/scripts/hero.js | 51 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/src/scripts/hero.js b/src/scripts/hero.js index 9760ee6..0474fff 100644 --- a/src/scripts/hero.js +++ b/src/scripts/hero.js @@ -128,6 +128,57 @@ function initHero() { exit.to('.hero-scroll-hint', { autoAlpha: 0, ease: 'none', duration: 0.25 }, 0); } + // ── Botón "Ver proyectos": scroll suave para no saltarse las animaciones ── + // En vez de saltar directo a #proyectos (lo que se salta el zoom de galaxia y + // la salida de las cards), recorremos toda la página con un scroll animado por + // tiempo para que se vean todas las animaciones de scroll antes de llegar. + const cta = document.querySelector('.hero-cta'); + if (cta) { + cta.addEventListener('click', (e) => { + const target = document.querySelector('#proyectos'); + if (!target) return; + e.preventDefault(); + + if (reduced) { + target.scrollIntoView(); + return; + } + + const startY = window.scrollY || document.documentElement.scrollTop; + const endY = target.getBoundingClientRect().top + startY; + const dist = endY - startY; + if (Math.abs(dist) < 4) return; + + // Duración proporcional a la distancia para que el ritmo sea agradable + const duration = Math.min(7000, Math.max(7000, Math.abs(dist) * 1.5)); + const startT = performance.now(); + let cancelled = false; + + // Si el usuario interactúa, le devolvemos el control inmediatamente + const cancel = () => { cancelled = true; cleanup(); }; + const cleanup = () => { + window.removeEventListener('wheel', cancel); + window.removeEventListener('touchstart', cancel); + window.removeEventListener('keydown', cancel); + }; + window.addEventListener('wheel', cancel, { passive: true }); + window.addEventListener('touchstart', cancel, { passive: true }); + window.addEventListener('keydown', cancel); + + // easeInOutCubic + const ease = (t) => t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2; + + function step(now) { + if (cancelled) return; + const p = Math.min(1, (now - startT) / duration); + window.scrollTo(0, startY + dist * ease(p)); + if (p < 1) requestAnimationFrame(step); + else cleanup(); + } + requestAnimationFrame(step); + }); + } + // El preloader dispara la entrada; fallback por si ya no existe if (!document.getElementById('preloader')) { entrance(); From 472c750c122b31ac1ef9f888c628fda04ba3de79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Garc=C3=ADa=20Santamar=C3=ADa?= <69808296+PGSCOM@users.noreply.github.com> Date: Thu, 11 Jun 2026 00:52:09 +0200 Subject: [PATCH 31/49] =?UTF-8?q?Navegaci=C3=B3n=20de=20proyectos?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- astro.config.mjs | 1 + public/robots.txt | 4 + src/layouts/Layout.astro | 76 ++++- src/pages/index.astro | 223 +++---------- src/pages/proyectos/[id].astro | 196 ++++++++++++ src/pages/sitemap.xml.ts | 32 ++ src/scripts/preloader.js | 2 + src/styles/proyecto-detalle.css | 546 ++++++++++++++++++++++++++++++++ src/styles/proyectos.css | 6 +- 9 files changed, 909 insertions(+), 177 deletions(-) create mode 100644 public/robots.txt create mode 100644 src/pages/proyectos/[id].astro create mode 100644 src/pages/sitemap.xml.ts create mode 100644 src/styles/proyecto-detalle.css diff --git a/astro.config.mjs b/astro.config.mjs index 53e49de..b02f7da 100644 --- a/astro.config.mjs +++ b/astro.config.mjs @@ -5,5 +5,6 @@ import react from '@astrojs/react'; // https://astro.build/config export default defineConfig({ + site: 'https://pgscom.es', integrations: [react()] }); \ No newline at end of file diff --git a/public/robots.txt b/public/robots.txt new file mode 100644 index 0000000..1eb3610 --- /dev/null +++ b/public/robots.txt @@ -0,0 +1,4 @@ +User-agent: * +Allow: / + +Sitemap: https://pgscom.es/sitemap.xml diff --git a/src/layouts/Layout.astro b/src/layouts/Layout.astro index 0bca5e1..be73731 100644 --- a/src/layouts/Layout.astro +++ b/src/layouts/Layout.astro @@ -1,3 +1,16 @@ +--- +const { + title = 'PGSCOM — Pablo García Santamaría', + description = 'Portfolio de Pablo García Santamaría: proyectos de desarrollo web, producción audiovisual, animación 3D, programación, diseño gráfico y fotografía.', + image = '/assets/proyectos/portfolio.png', + type = 'website', + noindex = false, +} = Astro.props; + +const canonical = new URL(Astro.url.pathname, Astro.site ?? Astro.url.origin).href; +const ogImage = new URL(image, Astro.site ?? Astro.url.origin).href; +--- + @@ -5,8 +18,67 @@ - PGSCOM - + + + {title} + + + {noindex && } + + + + + + + + + + + + + + + + + + + diff --git a/src/pages/index.astro b/src/pages/index.astro index 49e41f6..a6207a9 100644 --- a/src/pages/index.astro +++ b/src/pages/index.astro @@ -58,19 +58,34 @@ const heroLayout = [
+ + +
{featured.map((proyecto, i) => { const l = heroLayout[i]; return ( -
@@ -81,7 +96,7 @@ const heroLayout = [ -
+ ); })}
@@ -162,21 +177,21 @@ const heroLayout = [
{aptitud.proyectos && aptitud.proyectos.map((proyecto) => ( -
{proyecto.titulo} - + {proyecto.link && ( - + + )}
@@ -199,7 +214,7 @@ const heroLayout = [
)}
- + ))} @@ -207,59 +222,6 @@ const heroLayout = [ - -
-
- - - - - -
-
@@ -506,9 +468,14 @@ const heroLayout = [ animatedText.classList.add('loaded'); // Esperar a que la carga termine para empezar a rotar fuentes - window.addEventListener('loading-done', () => { + // (si ya terminó —p. ej. con ?sincarga— arranca directamente) + if (window.__loadingDone) { setInterval(changeFont, delay); - }, { once: true }); + } else { + window.addEventListener('loading-done', () => { + setInterval(changeFont, delay); + }, { once: true }); + } }); @@ -637,114 +604,24 @@ const heroLayout = [ }); - - \ No newline at end of file diff --git a/src/pages/proyectos/[id].astro b/src/pages/proyectos/[id].astro new file mode 100644 index 0000000..7c0b95d --- /dev/null +++ b/src/pages/proyectos/[id].astro @@ -0,0 +1,196 @@ +--- +import Layout from '../../layouts/Layout.astro'; +import '../../styles/proyecto-detalle.css'; +import proyectosData from '../../data/proyectos.json'; + +// Genera una ruta estática por cada proyecto de cada aptitud. +// Cada página es HTML real y rastreable por los bots de Google. +export function getStaticPaths() { + const paths = []; + for (const aptitud of proyectosData) { + for (const proyecto of aptitud.proyectos ?? []) { + const relacionados = aptitud.proyectos.filter((p) => p.id !== proyecto.id); + paths.push({ + params: { id: proyecto.id }, + props: { proyecto, aptitud, relacionados }, + }); + } + } + return paths; +} + +const { proyecto, aptitud, relacionados } = Astro.props; + +const tituloPagina = `${proyecto.titulo.trim()} · ${aptitud.titulo} — PGSCOM`; +const descripcionPagina = proyecto.descripcion || `${proyecto.titulo} — proyecto de ${aptitud.titulo} de Pablo García Santamaría.`; + +// Datos estructurados (Schema.org) para enriquecer el resultado en Google +const jsonLd = { + '@context': 'https://schema.org', + '@type': 'CreativeWork', + name: proyecto.titulo.trim(), + description: descripcionPagina, + url: new URL(`/proyectos/${proyecto.id}`, Astro.site).href, + image: new URL(proyecto.imagen, Astro.site).href, + genre: aptitud.titulo, + keywords: (proyecto.tecnologias ?? []).join(', '), + creator: { + '@type': 'Person', + name: 'Pablo García Santamaría', + url: Astro.site?.href, + }, + ...(proyecto.premio && { + award: proyecto.premio, + }), + ...(proyecto.link && { + sameAs: proyecto.link, + }), +}; + +const breadcrumbLd = { + '@context': 'https://schema.org', + '@type': 'BreadcrumbList', + itemListElement: [ + { '@type': 'ListItem', position: 1, name: 'Inicio', item: Astro.site?.href }, + { '@type': 'ListItem', position: 2, name: 'Proyectos', item: new URL('/#proyectos', Astro.site).href }, + { '@type': 'ListItem', position: 3, name: proyecto.titulo.trim(), item: new URL(`/proyectos/${proyecto.id}`, Astro.site).href }, + ], +}; +--- + + + + diff --git a/src/pages/sitemap.xml.ts b/src/pages/sitemap.xml.ts new file mode 100644 index 0000000..751a457 --- /dev/null +++ b/src/pages/sitemap.xml.ts @@ -0,0 +1,32 @@ +import type { APIRoute } from 'astro'; +import proyectosData from '../data/proyectos.json'; + +// Sitemap generado proceduralmente: portada + una URL por cada proyecto. +export const GET: APIRoute = ({ site }) => { + const base = (site?.href ?? 'https://pgscom.es/').replace(/\/$/, ''); + + const urls: string[] = [`${base}/`]; + for (const aptitud of proyectosData) { + for (const proyecto of aptitud.proyectos ?? []) { + urls.push(`${base}/proyectos/${proyecto.id}`); + } + } + + const body = ` + +${urls + .map( + (url) => ` + ${url} + monthly + ${url.endsWith('/') ? '1.0' : '0.8'} + `, + ) + .join('\n')} + +`; + + return new Response(body, { + headers: { 'Content-Type': 'application/xml; charset=utf-8' }, + }); +}; diff --git a/src/scripts/preloader.js b/src/scripts/preloader.js index 950781c..408f9a3 100644 --- a/src/scripts/preloader.js +++ b/src/scripts/preloader.js @@ -9,6 +9,8 @@ gsap.registerPlugin(DrawSVGPlugin); function dispatchDone() { document.documentElement.style.overflow = ''; + // Flag para listeners que se registran tarde (p. ej. tras un await) + window.__loadingDone = true; window.dispatchEvent(new CustomEvent('loading-done')); } diff --git a/src/styles/proyecto-detalle.css b/src/styles/proyecto-detalle.css new file mode 100644 index 0000000..ff81ce0 --- /dev/null +++ b/src/styles/proyecto-detalle.css @@ -0,0 +1,546 @@ +/* ============================================ + FICHA DE PROYECTO — Página individual + Estilo cinematográfico, hermano del modal de proyectos + ============================================ */ + +:root { + --pd-azul: #93c5fd; + --pd-bg: #05070d; +} + +body:has(.pd-page) { + background: var(--pd-bg); +} + +.pd-page { + position: relative; + min-height: 100vh; + color: rgba(255, 255, 255, 0.85); + font-family: 'Chakra Petch', 'Montserrat', sans-serif; + overflow: clip; + isolation: isolate; +} + +/* ── Fondo cinematográfico ───────────────────────────── */ +.pd-backdrop { + position: fixed; + inset: 0; + z-index: -2; + overflow: hidden; +} + +.pd-backdrop-img { + width: 100%; + height: 70vh; + min-height: 480px; + object-fit: cover; + object-position: center top; + filter: brightness(0.45) saturate(1.05); + transform: scale(1.05); +} + +.pd-backdrop::after { + content: ''; + position: absolute; + inset: 0; + background: linear-gradient( + 180deg, + rgba(5, 7, 13, 0.35) 0%, + rgba(5, 7, 13, 0.55) 35%, + rgba(5, 7, 13, 0.9) 70%, + var(--pd-bg) 100% + ); +} + +/* ── Botón volver flotante ───────────────────────────── */ +.pd-back { + position: fixed; + top: 24px; + left: 24px; + z-index: 50; + display: inline-flex; + align-items: center; + gap: 8px; + padding: 10px 18px 10px 14px; + border-radius: 999px; + background: rgba(8, 10, 18, 0.6); + backdrop-filter: blur(12px); + -webkit-backdrop-filter: blur(12px); + border: 1px solid rgba(255, 255, 255, 0.15); + color: rgba(255, 255, 255, 0.9); + text-decoration: none; + font-size: 0.9rem; + font-weight: 600; + transition: transform 0.3s ease, background 0.3s ease, border-color 0.3s ease; +} + +.pd-back:hover { + background: rgba(255, 255, 255, 0.12); + border-color: rgba(147, 197, 253, 0.5); + transform: translateX(-3px); +} + +/* ── Contenedor ──────────────────────────────────────── */ +.pd-wrapper { + position: relative; + z-index: 1; + max-width: 1100px; + margin: 0 auto; + padding: 0 32px 12vh; +} + +/* ── Migas de pan ────────────────────────────────────── */ +.pd-breadcrumb { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 8px; + padding-top: 96px; + font-size: 0.82rem; + color: rgba(255, 255, 255, 0.55); +} + +.pd-breadcrumb a { + color: rgba(255, 255, 255, 0.7); + text-decoration: none; + transition: color 0.25s ease; +} + +.pd-breadcrumb a:hover { + color: var(--pd-azul); +} + +.pd-breadcrumb-current { + color: rgba(255, 255, 255, 0.45); +} + +/* ── Hero ────────────────────────────────────────────── */ +.pd-hero { + display: flex; + align-items: flex-end; + gap: 36px; + padding: 26vh 0 36px; + min-height: 50vh; +} + +.pd-poster-container { + flex-shrink: 0; + width: 260px; + height: 260px; +} + +.pd-poster { + width: 100%; + height: 100%; + border-radius: 14px; + object-fit: cover; + box-shadow: + 0 24px 60px rgba(0, 0, 0, 0.65), + 0 0 0 1px rgba(255, 255, 255, 0.1); +} + +.pd-hero-info { + flex: 1; + min-width: 0; + padding-bottom: 8px; +} + +.pd-categoria { + display: inline-flex; + align-items: center; + gap: 10px; + margin: 0 0 14px; + padding: 6px 14px 6px 10px; + background: rgba(147, 197, 253, 0.12); + border: 1px solid rgba(147, 197, 253, 0.25); + border-radius: 999px; + font-size: 0.85rem; + font-weight: 600; + color: rgba(200, 220, 255, 0.95); +} + +.pd-categoria img { + width: 20px; + height: 20px; + object-fit: contain; + filter: drop-shadow(0 1px 4px rgba(0, 0, 0, 0.4)); +} + +.pd-titulo { + font-size: clamp(2.1rem, 5.5vw, 3.6rem); + font-weight: 700; + color: #fff; + margin: 0 0 18px; + line-height: 1.08; + letter-spacing: -0.02em; + text-shadow: 0 4px 24px rgba(0, 0, 0, 0.7); +} + +.pd-meta { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 14px; + margin-bottom: 18px; +} + +.pd-meta-item { + font-size: 0.9rem; + color: rgba(255, 255, 255, 0.7); +} + +.pd-meta-badge { + padding: 5px 12px; + background: rgba(255, 255, 255, 0.15); + border: 1px solid rgba(255, 255, 255, 0.22); + border-radius: 6px; + font-size: 0.82rem; + font-weight: 600; + color: #fff; +} + +.pd-tagline { + font-size: 1.1rem; + font-style: italic; + color: rgba(255, 255, 255, 0.85); + margin: 0 0 24px; + line-height: 1.55; + max-width: 640px; +} + +/* ── Acciones ────────────────────────────────────────── */ +.pd-actions { + display: flex; + align-items: center; + gap: 12px; + flex-wrap: wrap; +} + +.pd-btn-primary { + display: inline-flex; + align-items: center; + gap: 10px; + padding: 14px 30px; + background: #fff; + border: none; + border-radius: 8px; + color: #0a0a14; + font-family: inherit; + font-weight: 700; + font-size: 1rem; + text-decoration: none; + cursor: pointer; + transition: transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1), box-shadow 0.3s ease; +} + +.pd-btn-primary:hover { + transform: scale(1.05); + box-shadow: 0 8px 30px rgba(255, 255, 255, 0.25); +} + +.pd-btn-secondary { + position: relative; + display: inline-flex; + align-items: center; + justify-content: center; + width: 50px; + height: 50px; + background: rgba(255, 255, 255, 0.1); + backdrop-filter: blur(10px); + -webkit-backdrop-filter: blur(10px); + border: 1px solid rgba(255, 255, 255, 0.2); + border-radius: 50%; + color: #fff; + cursor: pointer; + transition: transform 0.3s ease, background 0.3s ease, border-color 0.3s ease; +} + +.pd-btn-secondary:hover { + background: rgba(255, 255, 255, 0.2); + border-color: rgba(255, 255, 255, 0.4); + transform: scale(1.1); +} + +.pd-btn-secondary.pd-copied::after { + content: '¡Copiado!'; + position: absolute; + bottom: calc(100% + 8px); + left: 50%; + transform: translateX(-50%); + white-space: nowrap; + font-size: 0.75rem; + font-weight: 600; + padding: 4px 10px; + border-radius: 6px; + background: rgba(147, 197, 253, 0.9); + color: #0a0a14; +} + +/* ── Cuerpo ──────────────────────────────────────────── */ +.pd-body { + position: relative; +} + +.pd-tecnologias { + display: flex; + flex-wrap: wrap; + gap: 10px; + margin-bottom: 8px; +} + +.pd-tech-tag { + font-size: 0.82rem; + font-weight: 500; + padding: 8px 16px; + background: rgba(147, 197, 253, 0.15); + border: 1px solid rgba(147, 197, 253, 0.3); + border-radius: 20px; + color: rgba(200, 220, 255, 0.95); + letter-spacing: 0.02em; + transition: transform 0.3s ease, background 0.3s ease, border-color 0.3s ease; +} + +.pd-tech-tag:hover { + background: rgba(147, 197, 253, 0.25); + border-color: rgba(147, 197, 253, 0.5); + transform: translateY(-2px); +} + +.pd-tech-tag[data-tech="React"], +.pd-tech-tag[data-tech="Next.js"] { background: rgba(97, 218, 251, 0.18); color: #61dafb; border-color: rgba(97, 218, 251, 0.35); } +.pd-tech-tag[data-tech="Blender"] { background: rgba(234, 106, 32, 0.18); color: #f08a4b; border-color: rgba(234, 106, 32, 0.35); } +.pd-tech-tag[data-tech="Python"] { background: rgba(55, 118, 171, 0.22); color: #7cb3e8; border-color: rgba(55, 118, 171, 0.4); } +.pd-tech-tag[data-tech="TypeScript"] { background: rgba(49, 120, 198, 0.22); color: #6fa8e8; border-color: rgba(49, 120, 198, 0.4); } +.pd-tech-tag[data-tech="Astro"] { background: rgba(255, 90, 40, 0.18); color: #ff8a66; border-color: rgba(255, 90, 40, 0.35); } + +/* ── Contenido HTML ──────────────────────────────────── */ +.pd-contenido { + color: rgba(255, 255, 255, 0.85); + line-height: 1.7; + margin-top: 32px; + padding-top: 32px; + border-top: 1px solid rgba(255, 255, 255, 0.1); + max-width: 800px; +} + +.pd-contenido h3 { + font-size: 1.4rem; + font-weight: 700; + color: #fff; + margin: 0 0 16px; +} + +.pd-contenido h4 { + font-size: 1.1rem; + font-weight: 600; + color: rgba(200, 220, 255, 1); + margin: 26px 0 12px; +} + +.pd-contenido p { + margin: 0 0 16px; + color: rgba(255, 255, 255, 0.8); +} + +.pd-contenido ul, +.pd-contenido ol { + margin: 0 0 16px; + padding-left: 24px; +} + +.pd-contenido li { + margin-bottom: 8px; + color: rgba(255, 255, 255, 0.8); +} + +.pd-contenido strong { + color: var(--pd-azul); + font-weight: 600; +} + +.pd-contenido blockquote { + margin: 22px 0; + padding: 16px 20px; + background: linear-gradient(135deg, rgba(147, 197, 253, 0.1), rgba(196, 181, 253, 0.08)); + border-left: 4px solid rgba(147, 197, 253, 0.6); + border-radius: 0 12px 12px 0; + font-style: italic; + color: rgba(255, 255, 255, 0.9); +} + +.pd-contenido code { + font-family: 'JetBrains Mono', monospace; + font-size: 0.85em; + background: rgba(0, 0, 0, 0.4); + padding: 2px 8px; + border-radius: 4px; + color: rgba(196, 181, 253, 1); +} + +.pd-contenido img { + max-width: 100%; + border-radius: 12px; + margin: 16px 0; + box-shadow: 0 8px 30px rgba(0, 0, 0, 0.4); +} + +/* ── Relacionados ────────────────────────────────────── */ +.pd-relacionados { + margin-top: 64px; + padding-top: 40px; + border-top: 1px solid rgba(255, 255, 255, 0.1); +} + +.pd-relacionados-titulo { + font-size: 1.5rem; + font-weight: 700; + color: #fff; + margin: 0 0 28px; +} + +.pd-relacionados-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); + gap: 24px; +} + +.pd-rel-card { + display: flex; + flex-direction: column; + background: linear-gradient(135deg, rgba(30, 35, 55, 0.7), rgba(15, 20, 35, 0.8)); + backdrop-filter: blur(20px); + -webkit-backdrop-filter: blur(20px); + border-radius: 18px; + border: 1px solid rgba(255, 255, 255, 0.1); + text-decoration: none; + overflow: hidden; + transition: transform 0.4s cubic-bezier(0.34, 1.56, 0.64, 1), border-color 0.4s ease, box-shadow 0.4s ease; +} + +.pd-rel-card:hover { + transform: translateY(-8px); + border-color: rgba(147, 197, 253, 0.5); + box-shadow: 0 20px 40px rgba(0, 0, 0, 0.5), 0 0 30px rgba(147, 197, 253, 0.15); +} + +.pd-rel-image { + width: 100%; + height: 170px; + overflow: hidden; + border-bottom: 1px solid rgba(255, 255, 255, 0.05); +} + +.pd-rel-image img { + width: 100%; + height: 100%; + object-fit: cover; + transition: transform 0.6s cubic-bezier(0.34, 1.56, 0.64, 1); +} + +.pd-rel-card:hover .pd-rel-image img { + transform: scale(1.08); +} + +.pd-rel-info { + padding: 16px; +} + +.pd-rel-titulo { + font-size: 1.05rem; + font-weight: 600; + color: #fff; + margin: 0 0 8px; + transition: color 0.3s ease; +} + +.pd-rel-card:hover .pd-rel-titulo { + color: var(--pd-azul); +} + +.pd-rel-desc { + font-size: 0.82rem; + color: rgba(255, 255, 255, 0.75); + margin: 0; + line-height: 1.45; + display: -webkit-box; + -webkit-line-clamp: 2; + line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; +} + +/* ── Responsive ──────────────────────────────────────── */ +@media (max-width: 768px) { + .pd-wrapper { + padding: 0 20px 10vh; + } + + .pd-backdrop-img { + height: 60vh; + } + + .pd-hero { + flex-direction: column; + align-items: center; + text-align: center; + padding: 18vh 0 28px; + gap: 22px; + min-height: auto; + } + + .pd-poster-container { + width: 190px; + height: 190px; + } + + .pd-hero-info { + display: flex; + flex-direction: column; + align-items: center; + } + + .pd-titulo { + font-size: 1.9rem; + } + + .pd-meta, + .pd-actions { + justify-content: center; + } + + .pd-tagline { + font-size: 1rem; + } + + .pd-back span { + display: none; + } + + .pd-back { + padding: 10px; + } +} + +@media (max-width: 480px) { + .pd-poster-container { + width: 150px; + height: 150px; + } + + .pd-titulo { + font-size: 1.6rem; + } + + .pd-relacionados-grid { + grid-template-columns: 1fr; + } +} + +@media (prefers-reduced-motion: reduce) { + .pd-rel-card, + .pd-rel-image img, + .pd-btn-primary, + .pd-back { + transition: none; + } +} diff --git a/src/styles/proyectos.css b/src/styles/proyectos.css index 58db96f..b28fb4c 100644 --- a/src/styles/proyectos.css +++ b/src/styles/proyectos.css @@ -335,8 +335,10 @@ .proyecto-detail-card { display: flex; flex-direction: column; - background: linear-gradient(135deg, - rgba(30, 35, 55, 0.7), + text-decoration: none; + color: inherit; + background: linear-gradient(135deg, + rgba(30, 35, 55, 0.7), rgba(15, 20, 35, 0.8)); backdrop-filter: blur(20px); -webkit-backdrop-filter: blur(20px); From 5ab5774074071d808dd6d26a575b2f5071e30d91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Garc=C3=ADa=20Santamar=C3=ADa?= <69808296+PGSCOM@users.noreply.github.com> Date: Thu, 11 Jun 2026 12:21:14 +0200 Subject: [PATCH 32/49] Refactor project styles for category orbit and bubble map layout; enhance responsiveness and visual effects --- package-lock.json | 213 +++++- package.json | 7 +- src/data/proyectos.json | 18 + src/pages/index.astro | 127 ++-- src/scripts/proyectos-burbujas.js | 458 ++++++++++++ src/styles/proyectos.css | 1120 +++++------------------------ 6 files changed, 909 insertions(+), 1034 deletions(-) create mode 100644 src/scripts/proyectos-burbujas.js diff --git a/package-lock.json b/package-lock.json index efb3ae7..a8e1cf2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,12 @@ "@types/react": "^19.1.16", "@types/react-dom": "^19.1.9", "astro": "^5.15.1", - "gsap": "^3.12.7", + "d3-drag": "^3.0.0", + "d3-force": "^3.0.0", + "d3-scale": "^4.0.2", + "d3-selection": "^3.0.0", + "d3-transition": "^3.0.1", + "gsap": "^3.15.0", "liquid-glass-react": "^1.1.1", "react": "^19.1.1", "react-dom": "^19.1.1" @@ -140,7 +145,6 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.4.tgz", "integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==", "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", @@ -1801,7 +1805,6 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.16.tgz", "integrity": "sha512-WBM/nDbEZmDUORKnh5i1bTnAz6vTohUf9b8esSMu+b24+srbaxa04UbJgWx78CVfNXA20sNu0odEIluZDFdCog==", "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.0.2" } @@ -1811,7 +1814,6 @@ "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.9.tgz", "integrity": "sha512-qXRuZaOsAdXKFyOhRBg6Lqqc0yay13vN7KrIg4L7N4aaHN68ma9OK3NE1BoDFgFOTfM7zg+3/8+2n8rLUH3OKQ==", "license": "MIT", - "peer": true, "peerDependencies": { "@types/react": "^19.0.0" } @@ -2151,7 +2153,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.3", "caniuse-lite": "^1.0.30001741", @@ -2452,6 +2453,179 @@ "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", "license": "MIT" }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dispatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-drag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", + "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-selection": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-force": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-3.0.0.tgz", + "integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-quadtree": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz", + "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-quadtree": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz", + "integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "license": "ISC", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-selection": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "license": "ISC", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-transition": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", + "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3", + "d3-dispatch": "1 - 3", + "d3-ease": "1 - 3", + "d3-interpolate": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "d3-selection": "2 - 3" + } + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -2837,10 +3011,10 @@ "license": "ISC" }, "node_modules/gsap": { - "version": "3.12.7", - "resolved": "https://registry.npmjs.org/gsap/-/gsap-3.12.7.tgz", - "integrity": "sha512-V4GsyVamhmKefvcAKaoy0h6si0xX7ogwBoBSs2CTJwt7luW0oZzC0LhdkyuKV8PJAXr7Yaj8pMjCKD4GJ+eEMg==", - "license": "Standard 'no charge' license: https://gsap.com/standard-license. Club GSAP members get more: https://gsap.com/licensing/. Why GreenSock doesn't employ an MIT license: https://gsap.com/why-license/" + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/gsap/-/gsap-3.15.0.tgz", + "integrity": "sha512-dMW4CWBTUK1AEEDeZc1g4xpPGIrSf9fJF960qbTZmN/QwZIWY5wgliS6JWl9/25fpTGJrMRtSjGtOmPnfjZB+A==", + "license": "Standard 'no charge' license: https://gsap.com/standard-license." }, "node_modules/h3": { "version": "1.15.5", @@ -3068,6 +3242,15 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/iron-webcrypto": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/iron-webcrypto/-/iron-webcrypto-1.2.1.tgz", @@ -4355,7 +4538,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.1.1.tgz", "integrity": "sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -4365,7 +4547,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.1.tgz", "integrity": "sha512-Dlq/5LAZgF0Gaz6yiqZCf6VCcZs1ghAJyrsu84Q/GT0gV+mCxbfmKNoGRKBYMJ8IEdGPqu49YWXD02GCknEDkw==", "license": "MIT", - "peer": true, "dependencies": { "scheduler": "^0.26.0" }, @@ -4627,7 +4808,6 @@ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.52.3.tgz", "integrity": "sha512-RIDh866U8agLgiIcdpB+COKnlCreHJLfIhWC3LVflku5YHfpnsIKigRZeFfMfCc4dVcqNVfQQ5gO/afOck064A==", "license": "MIT", - "peer": true, "dependencies": { "@types/estree": "1.0.8" }, @@ -4980,13 +5160,6 @@ "integrity": "sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==", "license": "MIT" }, - "node_modules/undici-types": { - "version": "7.8.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.8.0.tgz", - "integrity": "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==", - "license": "MIT", - "optional": true - }, "node_modules/unified": { "version": "11.0.5", "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", @@ -5313,7 +5486,6 @@ "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz", "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", @@ -5518,7 +5690,6 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/package.json b/package.json index 2916fe2..40f2b98 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,12 @@ "@types/react": "^19.1.16", "@types/react-dom": "^19.1.9", "astro": "^5.15.1", - "gsap": "^3.12.7", + "d3-drag": "^3.0.0", + "d3-force": "^3.0.0", + "d3-scale": "^4.0.2", + "d3-selection": "^3.0.0", + "d3-transition": "^3.0.1", + "gsap": "^3.15.0", "liquid-glass-react": "^1.1.1", "react": "^19.1.1", "react-dom": "^19.1.1" diff --git a/src/data/proyectos.json b/src/data/proyectos.json index d674531..492fc4f 100644 --- a/src/data/proyectos.json +++ b/src/data/proyectos.json @@ -8,6 +8,7 @@ { "id": "portfolio-pgscom", "titulo": "Portfolio PGSCOM", + "peso": 90, "imagen": "/assets/proyectos/portfolio.png", "descripcion": "Sitio web personal desarrollado con Astro, React y animaciones con AnimeJS.", "tecnologias": ["Astro", "React", "TypeScript", "AnimeJS", "CSS3"], @@ -17,6 +18,7 @@ { "id": "blog-personal", "titulo": "Blog Personal", + "peso": 45, "imagen": "/assets/proyectos/Blog.png", "descripcion": "Blog técnico sobre audiovisual, tecnología y programación.", "tecnologias": ["HTML", "CSS", "JavaScript"], @@ -26,6 +28,7 @@ { "id": "portafolio-cine", "titulo": "Portafolio Cinematográfico", + "peso": 60, "imagen": "/assets/proyectos/PortfolioCortos.png", "descripcion": "Galería de proyectos audiovisuales con reproductor de video integrado.", "tecnologias": ["HTML5", "CSS3", "JavaScript", "Video.js"], @@ -43,6 +46,7 @@ { "id": "premio-macula-2024", "titulo": "Premio Mácula 2024 ", + "peso": 80, "imagen": "/assets/proyectos/Macula.png", "descripcion": "Segundo Premio Mácula 2024 - Cortometraje documental galardonado.", "tecnologias": ["DaVinci Resolve", "Premiere Pro", "After Effects"], @@ -52,6 +56,7 @@ { "id": "premio-macula-2023", "titulo": "Premio Mácula 2023 ", + "peso": 70, "imagen": "/assets/proyectos/Macula.png", "descripcion": "Segundo Premio Mácula 2023 - Proyecto audiovisual experimental.", "tecnologias": ["DaVinci Resolve", "Premiere Pro"], @@ -61,6 +66,7 @@ { "id": "cortometrajes", "titulo": "Cortometrajes Varios", + "peso": 60, "imagen": "/assets/proyectos/PortfolioCortos.png", "descripcion": "Colección de cortometrajes de ficción y documental.", "tecnologias": ["DaVinci Resolve", "After Effects", "Audition"], @@ -77,6 +83,7 @@ { "id": "logo-animado-pgscom", "titulo": "Logo Animado PGSCOM", + "peso": 50, "imagen": "/assets/proyectos/logo-anim.jpg", "descripcion": "Animación 3D del logo personal con partículas y efectos visuales.", "tecnologias": ["Blender", "Cycles", "After Effects"], @@ -85,6 +92,7 @@ { "id": "visuales-unreal", "titulo": "Entornos Virtuales", + "peso": 70, "imagen": "/assets/proyectos/unreal.jpg", "descripcion": "Creación de entornos virtuales fotorrealistas en Unreal Engine.", "tecnologias": ["Unreal Engine", "Blender", "Substance Painter"], @@ -93,6 +101,7 @@ { "id": "motion-graphics", "titulo": "Motion Graphics", + "peso": 55, "imagen": "/assets/proyectos/motion.jpg", "descripcion": "Piezas de motion graphics para redes sociales y proyectos comerciales.", "tecnologias": ["Blender", "After Effects", "Illustrator"], @@ -109,6 +118,7 @@ { "id": "github-contributions", "titulo": "Contribuciones Open Source", + "peso": 65, "imagen": "/assets/proyectos/Contribuciones.png", "descripcion": "Más de 4 años de experiencia con Git/GitHub.", "tecnologias": ["Git", "GitHub", "Python", "JavaScript"], @@ -118,6 +128,7 @@ { "id": "python-tools", "titulo": "Herramientas Python", + "peso": 75, "imagen": "/assets/proyectos/Herramientas.png", "descripcion": "Scripts de automatización para proyectos audiovisuales. +6 años.", "tecnologias": ["Python", "NumPy", "OpenCV", "FFmpeg"], @@ -126,6 +137,7 @@ { "id": "cpp-learning", "titulo": "Aprendiendo C++", + "peso": 40, "imagen": "/assets/proyectos/cpp.jpg", "descripcion": "Proyectos académicos y personales en C++.", "tecnologias": ["C++", "CMake", "Visual Studio"], @@ -142,6 +154,7 @@ { "id": "identidad-pgscom", "titulo": "Identidad PGSCOM", + "peso": 55, "imagen": "/assets/proyectos/branding.jpg", "descripcion": "Diseño de identidad visual completa.", "tecnologias": ["Illustrator", "Photoshop", "Figma"], @@ -150,6 +163,7 @@ { "id": "carteleria", "titulo": "Cartelería de Eventos", + "peso": 45, "imagen": "/assets/proyectos/posters.jpg", "descripcion": "Diseño de carteles para eventos culturales.", "tecnologias": ["Photoshop", "Illustrator", "InDesign"], @@ -158,6 +172,7 @@ { "id": "diseño-web", "titulo": "UI/UX Design", + "peso": 60, "imagen": "/assets/proyectos/UI.png", "descripcion": "Diseño de interfaces para aplicaciones web y móviles.", "tecnologias": ["Figma", "Adobe XD", "Sketch"], @@ -174,6 +189,7 @@ { "id": "fotografia-eventos", "titulo": "Fotografía de Eventos", + "peso": 55, "imagen": "/assets/proyectos/eventos.jpg", "descripcion": "Cobertura fotográfica de eventos culturales.", "tecnologias": ["Lightroom", "Photoshop", "Capture One"], @@ -182,6 +198,7 @@ { "id": "retratos", "titulo": "Retratos", + "peso": 40, "imagen": "/assets/proyectos/retratos.jpg", "descripcion": "Sesiones de retrato profesional.", "tecnologias": ["Lightroom", "Photoshop"], @@ -190,6 +207,7 @@ { "id": "fotografia-producto", "titulo": "Fotografía de Producto", + "peso": 50, "imagen": "/assets/proyectos/producto.jpg", "descripcion": "Fotografía comercial de productos.", "tecnologias": ["Photoshop", "Lightroom", "Capture One"], diff --git a/src/pages/index.astro b/src/pages/index.astro index a6207a9..64f042a 100644 --- a/src/pages/index.astro +++ b/src/pages/index.astro @@ -33,6 +33,38 @@ const heroLayout = [ { left: '30%', top: '87%', depth: 0.55, rot: 5, size: 'sm', far: true }, { left: '67%', top: '89%', depth: 0.8, rot: -5, size: 'sm', far: true }, ]; + +// Color de cada categoría: lo comparten la órbita, las pills y el mapa de burbujas +const catColors = { + 'desarrollo-web': '#4da3ff', + 'video-produccion': '#ff7a4d', + 'animacion-3d': '#9a8cff', + 'programacion': '#34d39b', + 'diseño-grafico': '#ff6fb3', + 'fotografia': '#ffc94d', +}; + +// Datos planos para el mapa de burbujas (peso = envergadura → tamaño) +const burbujasData = { + cats: proyectosData.map((a) => ({ + id: a.id, + titulo: a.titulo, + color: catColors[a.id] ?? '#9bb4d0', + logo: a.logo, + })), + projects: proyectosData.flatMap((a) => + (a.proyectos ?? []).map((p) => ({ + id: p.id, + titulo: p.titulo.trim(), + cat: a.id, + peso: p.peso ?? 50, + imagen: p.imagen, + descripcion: p.descripcion ?? '', + tecnologias: p.tecnologias ?? [], + premio: p.premio ?? null, + })) + ), +}; --- @@ -133,14 +165,21 @@ const heroLayout = [

Proyectos

-

Explora mis trabajos por categoría

+

+ Un mapa interactivo de mis trabajos: cada burbuja es un proyecto y su tamaño refleja la envergadura. Filtra por categoría o arrastra las burbujas. +

{proyectosData.map((aptitud) => ( - +
{aptitud.titulo} @@ -163,63 +202,36 @@ const heroLayout = [
-
- {proyectosData.map((aptitud) => ( -
-
-
- {aptitud.titulo} -
-
-

{aptitud.titulo}

-

{aptitud.descripcion}

-
-
-
- {aptitud.proyectos && aptitud.proyectos.map((proyecto) => ( - -
- {proyecto.titulo} - - {proyecto.link && ( - - )} -
+
+ +

+ Tamaño = envergadura del proyecto · Pasa el cursor para ver detalles · Pulsa una burbuja para abrir el proyecto +

+ + +
    + {proyectosData.map((aptitud) => ( +
  • + {aptitud.titulo} + +
  • + ))} +
+ + - + diff --git a/src/scripts/proyectos-burbujas.js b/src/scripts/proyectos-burbujas.js new file mode 100644 index 0000000..64b4b70 --- /dev/null +++ b/src/scripts/proyectos-burbujas.js @@ -0,0 +1,458 @@ +import { forceSimulation, forceCollide, forceManyBody, forceX, forceY } from 'd3-force'; +import { select } from 'd3-selection'; +import { scaleSqrt } from 'd3-scale'; +import { drag } from 'd3-drag'; +import 'd3-transition'; +import gsap from 'gsap'; + +// ── Mapa de proyectos: burbujas-squircle con simulación de física ───────────── +// Cada proyecto es un "squircle" (forma de icono de iOS) cuyo tamaño viene de +// la propiedad `peso` del JSON. Las burbujas se agrupan por categoría en zonas +// y se pueden filtrar desde las pills o desde la órbita de categorías. + +function initBurbujas() { + const dataEl = document.getElementById('burbujas-data'); + const wrap = document.getElementById('mapaWrap'); + const tip = document.getElementById('mapaTip'); + const pillsEl = document.getElementById('mapaPills'); + if (!dataEl || !wrap || !tip || !pillsEl) return; + + const { cats, projects } = JSON.parse(dataEl.textContent); + const catById = Object.fromEntries(cats.map((c) => [c.id, c])); + const reduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches; + + const svg = select('#mapaChart'); + if (svg.empty()) return; + + let curCat = 'all'; + let entered = false; // ya se reprodujo la animación de entrada + let narrow = isNarrow(); + let sim = null; + let nodes = []; + let bubs = null; + let W = 0; + let H = 0; + let zones = {}; + + function isNarrow() { + return (wrap.clientWidth || window.innerWidth) < 620; + } + + // Superellipse |x/r|^n + |y/r|^n = 1 → la forma "casi cuadrada" de los + // iconos de iOS. n controla cuánto se acerca al cuadrado. + function squircle(r, n = 4.5) { + const STEPS = 64; + let d = ''; + for (let i = 0; i < STEPS; i++) { + const t = (i / STEPS) * Math.PI * 2; + const c = Math.cos(t); + const s = Math.sin(t); + const x = r * Math.sign(c) * Math.abs(c) ** (2 / n); + const y = r * Math.sign(s) * Math.abs(s) ** (2 / n); + d += (i === 0 ? 'M' : 'L') + x.toFixed(2) + ',' + y.toFixed(2); + } + return d + 'Z'; + } + + // La esquina del squircle sobresale del círculo inscrito (~4%) + const EXTENT = Math.SQRT2 * 0.5 ** (2 / 4.5); + + function wrapTitle(t, maxChars) { + const words = t.trim().split(/\s+/); + const lines = []; + let cur = ''; + for (const w of words) { + if (!cur) cur = w; + else if ((cur + ' ' + w).length <= maxChars) cur += ' ' + w; + else { lines.push(cur); cur = w; } + } + if (cur) lines.push(cur); + if (lines.length > 2) { + lines.length = 2; + lines[1] = lines[1].slice(0, Math.max(1, maxChars - 1)) + '…'; + } + return lines; + } + + function target(d) { + return curCat === 'all' ? zones[d.cat] : [W / 2, H / 2]; + } + + function clampX(d) { return Math.max(d.r * EXTENT + 2, Math.min(W - d.r * EXTENT - 2, d.x)); } + function clampY(d) { return Math.max(d.r * EXTENT + 2, Math.min(H - d.r * EXTENT - 2, d.y)); } + + function redraw() { + if (!bubs) return; + bubs.attr('transform', (d) => { + d.x = clampX(d); + d.y = clampY(d); + return `translate(${d.x},${d.y}) scale(${d.k * d.h})`; + }); + } + + function build() { + narrow = isNarrow(); + const cols = narrow ? 2 : 3; + const rows = Math.ceil(cats.length / cols); + W = narrow ? 640 : 1000; + H = narrow ? 980 : 640; + const cellW = W / cols; + const cellH = H / rows; + + zones = {}; + cats.forEach((c, i) => { + const col = i % cols; + const row = Math.floor(i / cols); + // Desplazado hacia abajo para dejar aire a la etiqueta de zona + zones[c.id] = [(col + 0.5) * cellW, (row + 0.5) * cellH + 18]; + }); + + const rScale = scaleSqrt().domain([0, 100]).range([0, narrow ? 62 : 86]); + const keepK = entered || reduced ? 1 : 0; + nodes = projects.map((p, i) => { + const [zx, zy] = zones[p.cat] || [W / 2, H / 2]; + return { + ...p, + r: Math.max(narrow ? 26 : 32, rScale(p.peso ?? 50)), + x: zx + Math.cos(i * 2.4) * 30, + y: zy + Math.sin(i * 2.4) * 30, + k: keepK, // escala de entrada + h: 1, // escala de hover + }; + }); + + if (sim) sim.stop(); + svg.selectAll('*').remove(); + svg.attr('viewBox', `0 0 ${W} ${H}`); + + // ── Defs: degradado de legibilidad + clip squircle por burbuja ── + const defs = svg.append('defs'); + const grad = defs.append('linearGradient') + .attr('id', 'bub-fade').attr('x1', 0).attr('y1', 0).attr('x2', 0).attr('y2', 1); + grad.append('stop').attr('offset', '0.3').attr('stop-color', 'rgba(2,8,23,0)'); + grad.append('stop').attr('offset', '1').attr('stop-color', 'rgba(2,8,23,0.86)'); + + nodes.forEach((d) => { + defs.append('clipPath').attr('id', `bclip-${d.id}`) + .append('path').attr('d', squircle(d.r)); + }); + + // ── Divisores y etiquetas de zona ── + const divG = svg.append('g').attr('class', 'mapa-divs'); + for (let c = 1; c < cols; c++) { + divG.append('line') + .attr('x1', c * cellW).attr('y1', 16).attr('x2', c * cellW).attr('y2', H - 16) + .attr('stroke', 'rgba(255,255,255,0.13)').attr('stroke-width', 1).attr('stroke-dasharray', '3,7'); + } + for (let r = 1; r < rows; r++) { + divG.append('line') + .attr('x1', 16).attr('y1', r * cellH).attr('x2', W - 16).attr('y2', r * cellH) + .attr('stroke', 'rgba(255,255,255,0.13)').attr('stroke-width', 1).attr('stroke-dasharray', '3,7'); + } + + const zlG = svg.append('g').attr('class', 'mapa-zlabels'); + cats.forEach((c, i) => { + const col = i % cols; + const row = Math.floor(i / cols); + zlG.append('text') + .attr('x', (col + 0.5) * cellW) + .attr('y', row * cellH + 22) + .attr('text-anchor', 'middle') + .attr('font-family', "'Chakra Petch', sans-serif") + .attr('font-size', narrow ? 12 : 13) + .attr('font-weight', 600) + .attr('letter-spacing', '0.12em') + .attr('fill', c.color) + .attr('opacity', 0.85) + .text(c.titulo.toUpperCase()); + }); + + // ── Burbujas ── + bubs = svg.append('g').attr('class', 'mapa-bubs') + .selectAll('g') + .data(nodes) + .enter() + .append('g') + .attr('class', 'bub') + .attr('tabindex', 0) + .attr('role', 'link') + .attr('aria-label', (d) => `${d.titulo} — ${catById[d.cat]?.titulo ?? ''}`); + + // Fondo (por si la imagen tarda) + imagen recortada al squircle + bubs.append('path') + .attr('d', (d) => squircle(d.r)) + .attr('fill', 'rgba(13,18,38,0.95)'); + + bubs.append('image') + .attr('href', (d) => d.imagen) + .attr('x', (d) => -d.r).attr('y', (d) => -d.r) + .attr('width', (d) => d.r * 2).attr('height', (d) => d.r * 2) + .attr('preserveAspectRatio', 'xMidYMid slice') + .attr('clip-path', (d) => `url(#bclip-${d.id})`) + .each(function (d) { + // Si la imagen no existe: fondo tintado del color de la categoría + su icono + this.addEventListener('error', () => { + const g = select(this.parentNode); + const color = catById[d.cat]?.color ?? '#9bb4d0'; + this.remove(); + g.insert('path', '.bub-ring') + .attr('d', squircle(d.r)) + .attr('fill', color) + .attr('fill-opacity', 0.16); + const logo = catById[d.cat]?.logo; + if (logo) { + const s = d.r * 0.62; + g.insert('image', '.bub-ring') + .attr('href', logo) + .attr('x', -s / 2) + .attr('y', -s / 2 - d.r * 0.14) + .attr('width', s) + .attr('height', s) + .attr('opacity', 0.9) + .attr('pointer-events', 'none'); + } + }, { once: true }); + }); + + bubs.append('path') + .attr('d', (d) => squircle(d.r)) + .attr('fill', 'url(#bub-fade)'); + + bubs.append('path') + .attr('class', 'bub-ring') + .attr('d', (d) => squircle(d.r)) + .attr('fill', 'none') + .attr('stroke', (d) => catById[d.cat]?.color ?? '#9bb4d0') + .attr('stroke-width', 1.6) + .attr('opacity', 0.9); + + // Título dentro de la burbuja (solo si cabe) + bubs.each(function (d) { + if (d.r < 40) return; + const g = select(this); + const fs = Math.max(11, Math.min(17, d.r * 0.24)); + const maxChars = Math.max(7, Math.floor((d.r * 1.55) / (fs * 0.56))); + const lines = wrapTitle(d.titulo, maxChars); + const lh = fs + 2; + const baseY = d.r * 0.66; + lines.forEach((line, i) => { + g.append('text') + .attr('class', 'bub-titulo') + .attr('text-anchor', 'middle') + .attr('x', 0) + .attr('y', baseY - (lines.length - 1 - i) * lh) + .attr('font-family', "'Chakra Petch', sans-serif") + .attr('font-size', fs) + .attr('font-weight', 600) + .attr('fill', '#fff') + .attr('stroke', 'rgba(0,8,20,0.55)') + .attr('stroke-width', 2.5) + .attr('paint-order', 'stroke') + .attr('pointer-events', 'none') + .text(line); + }); + if (d.premio) { + g.append('text') + .attr('text-anchor', 'middle') + .attr('x', 0) + .attr('y', -d.r * 0.58) + .attr('font-size', Math.max(13, d.r * 0.24)) + .attr('pointer-events', 'none') + .text('🏆'); + } + }); + + // ── Física ── + sim = forceSimulation(nodes) + .force('charge', forceManyBody().strength(-14)) + .force('collide', forceCollide((d) => d.r * 1.05 + 4).strength(0.95)) + .force('x', forceX((d) => target(d)[0]).strength(curCat === 'all' ? 0.07 : 0.12)) + .force('y', forceY((d) => target(d)[1]).strength(curCat === 'all' ? 0.07 : 0.12)) + .on('tick', redraw) + .stop(); + + // ── Arrastrar burbujas ── + bubs.call( + drag() + .clickDistance(6) + .on('start', (event, d) => { + if (!event.active) sim.alphaTarget(0.25).restart(); + d.fx = d.x; + d.fy = d.y; + }) + .on('drag', (event, d) => { d.fx = event.x; d.fy = event.y; }) + .on('end', (event, d) => { + if (!event.active) sim.alphaTarget(0); + d.fx = null; + d.fy = null; + }) + ); + + // ── Interacción: tooltip, click y teclado ── + bubs + .on('click', (event, d) => { + if (event.defaultPrevented) return; // venía de un drag + window.location.href = `/proyectos/${d.id}`; + }) + .on('keydown', (event, d) => { + if (event.key === 'Enter' || event.key === ' ') { + event.preventDefault(); + window.location.href = `/proyectos/${d.id}`; + } + }) + .on('mouseenter', function (event, d) { + select(this).raise(); + select(this).select('.bub-ring').attr('stroke-width', 3).attr('opacity', 1); + gsap.to(d, { h: 1.06, duration: 0.25, ease: 'power2.out', onUpdate: redraw }); + showTip(d); + moveTip(event); + }) + .on('mousemove', (event) => moveTip(event)) + .on('mouseleave', function (event, d) { + select(this).select('.bub-ring').attr('stroke-width', 1.6).attr('opacity', 0.9); + gsap.to(d, { h: 1, duration: 0.25, ease: 'power2.out', onUpdate: redraw }); + tip.style.display = 'none'; + }); + + applyFilterStyles(0); + redraw(); + + if (entered || reduced) { + sim.alpha(0.9).restart(); + } + } + + // ── Tooltip ── + function showTip(d) { + const cat = catById[d.cat]; + const techs = (d.tecnologias || []) + .map((t) => `${t}`) + .join(''); + tip.innerHTML = ` +
${d.titulo}
+
${cat?.titulo ?? ''}
+ ${d.premio ? `
🏆 ${d.premio}
` : ''} + ${d.descripcion ? `
${d.descripcion}
` : ''} +
+ Envergadura + +
+ ${techs ? `
${techs}
` : ''} + `; + tip.style.display = 'block'; + } + + function moveTip(event) { + const r = wrap.getBoundingClientRect(); + const x = event.clientX - r.left; + const y = event.clientY - r.top; + const tw = tip.offsetWidth || 220; + tip.style.left = (x + 16 + tw < r.width ? x + 16 : Math.max(0, x - tw - 16)) + 'px'; + tip.style.top = Math.max(0, Math.min(y - 12, r.height - tip.offsetHeight - 8)) + 'px'; + } + + // ── Filtro por categoría ── + function applyFilterStyles(dur) { + if (!bubs) return; + const visible = (d) => curCat === 'all' || d.cat === curCat; + bubs + .style('pointer-events', (d) => (visible(d) ? 'auto' : 'none')) + .attr('tabindex', (d) => (visible(d) ? 0 : -1)) + .transition().duration(dur) + .style('opacity', (d) => (visible(d) ? 1 : 0.07)); + svg.select('.mapa-divs').transition().duration(dur).style('opacity', curCat === 'all' ? 1 : 0); + svg.select('.mapa-zlabels').transition().duration(dur).style('opacity', curCat === 'all' ? 1 : 0); + } + + function setFilter(cat) { + curCat = cat; + const s = cat === 'all' ? 0.07 : 0.12; + sim.force('x', forceX((d) => target(d)[0]).strength(s)); + sim.force('y', forceY((d) => target(d)[1]).strength(s)); + sim.alpha(0.6).restart(); + applyFilterStyles(350); + tip.style.display = 'none'; + + pillsEl.querySelectorAll('.mapa-pill').forEach((p) => { + p.classList.toggle('act', p.dataset.cat === cat); + p.setAttribute('aria-pressed', p.dataset.cat === cat ? 'true' : 'false'); + }); + document.querySelectorAll('.proyecto-card').forEach((card) => { + card.classList.toggle('is-active', card.dataset.aptitudId === cat); + }); + } + + // ── Pills de filtro ── + [{ id: 'all', titulo: 'Todos', color: null }, ...cats].forEach((c) => { + const el = document.createElement('button'); + el.type = 'button'; + el.className = 'mapa-pill' + (c.id === 'all' ? ' act' : ''); + el.dataset.cat = c.id; + el.setAttribute('aria-pressed', c.id === 'all' ? 'true' : 'false'); + if (c.color) { + const dot = document.createElement('span'); + dot.className = 'mapa-pill-dot'; + dot.style.background = c.color; + el.appendChild(dot); + } + el.appendChild(document.createTextNode(c.titulo)); + el.addEventListener('click', () => setFilter(curCat === c.id && c.id !== 'all' ? 'all' : c.id)); + pillsEl.appendChild(el); + }); + + // ── La órbita de categorías también filtra el mapa ── + document.querySelectorAll('.proyecto-card').forEach((card) => { + card.addEventListener('click', (e) => { + e.preventDefault(); + const id = card.dataset.aptitudId; + if (!id) return; + setFilter(curCat === id ? 'all' : id); + document.getElementById('proyectos-mapa')?.scrollIntoView({ + behavior: reduced ? 'auto' : 'smooth', + block: 'nearest', + }); + }); + }); + + build(); + + // ── Entrada: las burbujas brotan al entrar en viewport ── + const io = new IntersectionObserver( + (entries) => { + if (!entries.some((e) => e.isIntersecting) || entered) return; + entered = true; + io.disconnect(); + sim.alpha(0.9).restart(); + if (reduced) { + nodes.forEach((d) => (d.k = 1)); + redraw(); + } else { + gsap.to(nodes, { + k: 1, + duration: 0.8, + ease: 'back.out(1.7)', + stagger: 0.045, + onUpdate: redraw, + }); + } + }, + { threshold: 0.15 } + ); + io.observe(wrap); + + // ── Cambio de layout (3×2 ↔ 2×3) al redimensionar ── + let resizeRaf = 0; + window.addEventListener('resize', () => { + if (resizeRaf) return; + resizeRaf = requestAnimationFrame(() => { + resizeRaf = 0; + if (isNarrow() !== narrow) build(); + }); + }); +} + +if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', initBurbujas); +} else { + initBurbujas(); +} diff --git a/src/styles/proyectos.css b/src/styles/proyectos.css index b28fb4c..85afb1e 100644 --- a/src/styles/proyectos.css +++ b/src/styles/proyectos.css @@ -1,4 +1,4 @@ -/* Estilos para la sección de proyectos - Layout legible y fluido */ +/* Estilos para la sección de proyectos - Órbita de categorías + mapa de burbujas */ /* ============================================ ANIMACIONES PERSONALIZADAS @@ -56,7 +56,8 @@ } /* ============================================ - HERO DECORATIVO - Órbita sin scroll-hijack + HERO DECORATIVO - Órbita de categorías + (las tarjetas en órbita filtran el mapa) ============================================ */ .proyectos-hero { @@ -66,15 +67,14 @@ justify-content: center; position: relative; padding: 20px; - margin-top: 5vh; - margin-bottom: 10vh; + margin-top: 2vh; + margin-bottom: 4vh; } .proyectos-hero-inner { position: relative; width: 100%; - /*max-width: 400px;*/ - height: 600px; + height: 520px; display: flex; align-items: center; justify-content: center; @@ -142,7 +142,7 @@ overflow: hidden; transition: all 0.4s cubic-bezier(0.34, 1.56, 0.64, 1); will-change: transform; - box-shadow: + box-shadow: 0 8px 20px rgba(0, 0, 0, 0.2), 0 0 20px rgba(147, 197, 253, 0.1); } @@ -176,7 +176,7 @@ .proyecto-card:hover .proyecto-card-inner { transform: translateY(-8px) scale(1.08); - box-shadow: + box-shadow: 0 25px 50px rgba(0, 0, 0, 0.35), 0 0 40px rgba(147, 197, 253, 0.3), 0 0 60px rgba(147, 197, 253, 0.15); @@ -187,6 +187,18 @@ rgba(255, 255, 255, 0.08)); } +/* Categoría activa como filtro del mapa: aro en su color */ +.proyecto-card.is-active { + filter: brightness(1.15) saturate(1.2); +} + +.proyecto-card.is-active .proyecto-card-inner { + border-color: var(--cat-color, rgba(147, 197, 253, 0.8)); + box-shadow: + 0 8px 24px rgba(0, 0, 0, 0.35), + 0 0 26px color-mix(in srgb, var(--cat-color, #93c5fd) 55%, transparent); +} + .proyecto-icon { width: 100%; height: auto; @@ -235,698 +247,193 @@ } /* ============================================ - CONTENIDO DE PROYECTOS - Layout por Aptitud + MAPA DE BURBUJAS - Proyectos con física ============================================ */ -.proyectos-content { - max-width: 1200px; +.proyectos-mapa { + max-width: 1160px; margin: 0 auto; padding: 0 24px 8vh; - display: flex; - flex-direction: column; - gap: 10vh; -} - -.aptitud-bloque { - position: relative; -} - -.aptitud-header { - display: flex; - align-items: center; - gap: 20px; - margin-bottom: 32px; - padding: 24px; - background: linear-gradient(135deg, - rgba(255, 255, 255, 0.08), - rgba(147, 197, 253, 0.05)); - backdrop-filter: blur(16px); - -webkit-backdrop-filter: blur(16px); - border-radius: 20px; - border: 1px solid rgba(255, 255, 255, 0.12); - box-shadow: - 0 8px 32px rgba(0, 0, 0, 0.2), - inset 0 1px 0 rgba(255, 255, 255, 0.1); - transition: all 0.4s ease; -} - -.aptitud-header:hover { - border-color: rgba(147, 197, 253, 0.25); - box-shadow: - 0 12px 40px rgba(0, 0, 0, 0.3), - 0 0 40px rgba(147, 197, 253, 0.1), - inset 0 1px 0 rgba(255, 255, 255, 0.15); } -.aptitud-icono { - width: 70px; - height: 70px; - border-radius: 50%; - background: linear-gradient(145deg, - rgba(255, 255, 255, 0.15), - rgba(147, 197, 253, 0.1)); - border: 2px solid rgba(255, 255, 255, 0.2); +/* Pills de filtro por categoría */ +.mapa-pills { display: flex; - align-items: center; + flex-wrap: wrap; justify-content: center; - flex-shrink: 0; - box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2); -} - -.aptitud-icono img { - width: 36px; - height: 36px; - object-fit: contain; - filter: drop-shadow(0 2px 8px rgba(0, 0, 0, 0.3)); -} - -.aptitud-info { - flex: 1; - min-width: 0; -} - -.aptitud-titulo { - font-family: 'Chakra Petch', sans-serif; - font-size: 1.5rem; - font-weight: 700; - color: white; - margin: 0 0 8px 0; - text-shadow: 0 2px 8px rgba(0,0,0,0.4); - line-height: 1.2; -} - -.aptitud-descripcion { - font-family: 'Chakra Petch', sans-serif; - font-size: 0.95rem; - color: rgba(255, 255, 255, 0.7); - margin: 0; - line-height: 1.5; - max-width: 700px; -} - -/* Grid de proyectos - Más espacio para tarjetas grandes */ -.proyectos-grid-list { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); - gap: 32px; -} - -/* Tarjeta de proyecto individual - Formato Vertical */ -.proyecto-detail-card { - display: flex; - flex-direction: column; - text-decoration: none; - color: inherit; - background: linear-gradient(135deg, - rgba(30, 35, 55, 0.7), - rgba(15, 20, 35, 0.8)); - backdrop-filter: blur(20px); - -webkit-backdrop-filter: blur(20px); - border-radius: 20px; - border: 1px solid rgba(255, 255, 255, 0.1); - cursor: pointer; - transition: all 0.4s cubic-bezier(0.34, 1.56, 0.64, 1); - position: relative; - overflow: hidden; - height: 100%; - will-change: transform; -} - -.proyecto-detail-card:hover { - transform: translateY(-8px); - border-color: rgba(147, 197, 253, 0.5); - box-shadow: - 0 20px 40px rgba(0, 0, 0, 0.5), - 0 0 30px rgba(147, 197, 253, 0.15); -} - -/* Imagen del proyecto (Ahora ocupa el 50% superior) */ -.proyecto-image-container { - width: 100%; - height: 200px; - overflow: hidden; - position: relative; - border-bottom: 1px solid rgba(255, 255, 255, 0.05); -} - -.proyecto-image { - width: 100%; - height: 100%; - object-fit: cover; - transition: transform 0.6s cubic-bezier(0.34, 1.56, 0.64, 1); - will-change: transform; -} - -.proyecto-detail-card:hover .proyecto-image { - transform: scale(1.08); + gap: 10px; + margin-bottom: 22px; } -/* Botón flotante para enlace externo */ -.card-btn-link-float { - position: absolute; - top: 16px; - right: 16px; - width: 40px; - height: 40px; - background: rgba(10, 10, 20, 0.7); - backdrop-filter: blur(8px); - border: 1px solid rgba(255, 255, 255, 0.2); - border-radius: 50%; - display: flex; +.mapa-pill { + display: inline-flex; align-items: center; - justify-content: center; - color: white; - opacity: 0; - transform: translateY(-10px); - transition: all 0.3s ease; - z-index: 2; -} - -.proyecto-detail-card:hover .card-btn-link-float { - opacity: 1; - transform: translateY(0); -} - -.card-btn-link-float:hover { - background: rgba(34, 197, 94, 0.8); - border-color: #22c55e; - transform: scale(1.1) !important; -} - -/* Contenedor de la info */ -.proyecto-info-wrapper { - padding: 16px; - position: static; - transform: none; -} - -.proyecto-info { - margin-bottom: 0; -} - -.proyecto-detail-titulo { + gap: 8px; + padding: 8px 18px; + border-radius: 999px; font-family: 'Chakra Petch', sans-serif; - font-size: 1.1rem; - font-weight: 600; - color: white; - margin: 0 0 8px 0; - line-height: 1.2; - transition: color 0.3s ease; -} - -.proyecto-detail-card:hover .proyecto-detail-titulo { - color: #93c5fd; -} - -.proyecto-detail-desc { - font-family: 'Chakra Petch', sans-serif; - font-size: 0.82rem; - color: rgba(255, 255, 255, 0.75); - margin: 0 0 10px 0; - line-height: 1.45; - display: -webkit-box; - -webkit-line-clamp: 2; - line-clamp: 2; - -webkit-box-orient: vertical; - overflow: hidden; -} - -/* Tags de tecnologías */ -.proyecto-tecnologias { - display: flex; - flex-wrap: wrap; - gap: 6px; -} - -.tech-tag { - font-family: 'Chakra Petch', sans-serif; - font-size: 0.75rem; - padding: 4px 12px; - background: rgba(255,255,255,0.1); - border: 1px solid rgba(147, 197, 253, 0.2); - border-radius: 8px; - color: #bfdbfe; -} - -.tech-tag[data-tech="React"], -.tech-tag[data-tech="Next.js"] { background: rgba(97,218,251,0.2); color: #61dafb; } -.tech-tag[data-tech="Blender"] { background: rgba(234,106,32,0.2); color: #ea6a20; } -.tech-tag[data-tech="Python"] { background: rgba(55,118,171,0.2); color: #3776ab; } -.tech-tag[data-tech="TypeScript"] { background: rgba(49,120,198,0.2); color: #3178c6; } -.tech-tag[data-tech="Astro"] { background: rgba(255,90,40,0.2); color: #ff5a28; } - -.tech-tag-more { - background: transparent; - border-color: rgba(255, 255, 255, 0.2); - color: rgba(255, 255, 255, 0.6); -} - -/* ============================================ - MODAL DE PROYECTO - Estilo Cinematográfico - ============================================ */ - -.proyecto-modal-overlay { - position: fixed; - inset: 0; - backdrop-filter: blur(5px) saturate(120%); - -webkit-backdrop-filter: blur(5px) saturate(120%); - z-index: 10000; - display: flex; - align-items: center; - justify-content: center; - padding: 24px; - opacity: 0; - visibility: hidden; - transition: all 0.36s cubic-bezier(0.16, 1, 0.3, 1); - overflow-y: auto; -} - -.proyecto-modal-overlay.active { - opacity: 1; - visibility: visible; -} - -.proyecto-modal { - width: 100%; - max-width: 1400px; - max-height: 90vh; - overflow: hidden; - display: flex; - flex-direction: column; - position: relative; - transform: scale(0.95) translateY(20px); - transition: transform 0.44s cubic-bezier(0.16, 1, 0.3, 1); - background: rgba(12, 14, 20, 0.48); - backdrop-filter: blur(6px) saturate(120%); - -webkit-backdrop-filter: blur(6px) saturate(120%); - border-radius: 18px; - border: 1px solid rgba(255, 255, 255, 0.06); - box-shadow: - 0 30px 90px rgba(0, 0, 0, 0.75), - 0 0 0 1px rgba(255, 255, 255, 0.02); -} - -.proyecto-modal-overlay.active .proyecto-modal { - transform: scale(1) translateY(0); -} - -/* Imagen de fondo hero */ -.modal-backdrop { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 70%; - overflow: hidden; - z-index: 0; - border-radius: 20px 20px 0 0; -} - -.modal-backdrop-img { - width: 100%; - height: 100%; - object-fit: cover; - object-position: center top; - filter: brightness(0.6); - transition: transform 0.6s ease; -} - -.proyecto-modal:hover .modal-backdrop-img { - transform: scale(1.02); -} - -/* Gradiente sobre la imagen */ -.modal-backdrop::after { - content: ''; - position: absolute; - inset: 0; - background: linear-gradient(180deg, - transparent 0%, - transparent 20%, - rgba(10, 10, 20, 0.6) 50%, - rgba(10, 10, 20, 0.95) 80%, - rgba(10, 10, 20, 1) 100%); - pointer-events: none; -} - -/* Botón cerrar */ -.modal-close { - position: absolute; - top: 16px; - right: 16px; - width: 48px; - height: 48px; - border-radius: 50%; - background: rgba(0, 0, 0, 0.7); + font-size: 0.85rem; + font-weight: 500; + color: rgba(255, 255, 255, 0.8); + background: rgba(30, 35, 55, 0.6); + border: 1px solid rgba(255, 255, 255, 0.15); backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px); - border: 2px solid rgba(255, 255, 255, 0.2); - color: rgba(255, 255, 255, 0.9); cursor: pointer; - display: flex; - align-items: center; - justify-content: center; transition: all 0.3s ease; - z-index: 100; } -.modal-close:hover { - background: rgba(255, 255, 255, 0.15); - border-color: rgba(255, 255, 255, 0.3); +.mapa-pill:hover { + border-color: rgba(147, 197, 253, 0.5); color: white; - transform: scale(1.1); -} - -/* Contenido principal del modal */ -.modal-content-wrapper { - position: relative; - z-index: 10; - display: flex; - flex-direction: column; - height: 100%; - padding: 0; - overflow-y: auto; - scrollbar-width: thin; - scrollbar-color: rgba(147, 197, 253, 0.3) transparent; -} - -.modal-content-wrapper::-webkit-scrollbar { - width: 6px; + transform: translateY(-2px); } -.modal-content-wrapper::-webkit-scrollbar-track { - background: transparent; +.mapa-pill.act { + background: white; + color: #0a0e1c; + border-color: white; + box-shadow: 0 4px 20px rgba(255, 255, 255, 0.2); } -.modal-content-wrapper::-webkit-scrollbar-thumb { - background: rgba(147, 197, 253, 0.3); - border-radius: 3px; +.mapa-pill-dot { + width: 9px; + height: 9px; + border-radius: 50%; + flex-shrink: 0; } -/* Sección hero con poster y título */ -.modal-hero { - display: flex; - align-items: flex-end; - gap: 30px; - padding: 40vh 48px 28px 48px; - min-height: 55vh; +/* Panel de cristal que contiene el chart */ +.mapa-panel { + background: linear-gradient(135deg, + rgba(30, 35, 55, 0.55), + rgba(12, 16, 32, 0.7)); + backdrop-filter: blur(16px); + -webkit-backdrop-filter: blur(16px); + border: 1px solid rgba(255, 255, 255, 0.12); + border-radius: 28px; + padding: 14px; + box-shadow: + 0 12px 48px rgba(0, 0, 0, 0.35), + inset 0 1px 0 rgba(255, 255, 255, 0.08); } -/* Poster del proyecto - Cuadrado */ -.modal-poster-container { - flex-shrink: 0; - width: 240px; - height: 240px; +.mapa-wrap { position: relative; } -.modal-poster { +#mapaChart { width: 100%; - height: 100%; - border-radius: 12px; - object-fit: cover; - box-shadow: - 0 20px 50px rgba(0, 0, 0, 0.6), - 0 0 0 1px rgba(255, 255, 255, 0.1); - transition: transform 0.4s ease, box-shadow 0.4s ease; + height: auto; + display: block; } -.modal-poster:hover { - transform: scale(1.03); - box-shadow: - 0 25px 60px rgba(0, 0, 0, 0.7), - 0 0 0 1px rgba(255, 255, 255, 0.2); +/* Burbujas: squircles clicables */ +.bub { + cursor: pointer; + outline: none; } -/* Info del héroe */ -.modal-hero-info { - flex: 1; - min-width: 0; - padding-bottom: 10px; +.bub:focus-visible .bub-ring { + stroke-width: 4; + opacity: 1; } -/* Título grande estilo película */ -.modal-titulo { +/* Tooltip */ +.mapa-tip { + position: absolute; + display: none; + pointer-events: none; + z-index: 20; + width: 236px; + padding: 14px 16px; + border-radius: 14px; + background: rgba(10, 14, 28, 0.92); + backdrop-filter: blur(12px); + -webkit-backdrop-filter: blur(12px); + border: 1px solid rgba(255, 255, 255, 0.15); + box-shadow: 0 14px 40px rgba(0, 0, 0, 0.55); font-family: 'Chakra Petch', sans-serif; - font-size: clamp(2rem, 5vw, 3.5rem); +} + +.mapa-tip-titulo { + font-size: 0.95rem; font-weight: 700; color: white; - margin: 0 0 16px 0; - line-height: 1.1; - text-shadow: - 0 4px 20px rgba(0, 0, 0, 0.8), - 0 2px 4px rgba(0, 0, 0, 0.5); - letter-spacing: -0.02em; + margin-bottom: 2px; + line-height: 1.25; } -/* Metadatos estilo streaming */ -.modal-meta { - display: flex; - align-items: center; - gap: 16px; - margin-bottom: 20px; - flex-wrap: wrap; +.mapa-tip-cat { + font-size: 0.72rem; + font-weight: 600; + letter-spacing: 0.06em; + text-transform: uppercase; + margin-bottom: 8px; } -.modal-meta-item { - font-family: 'Chakra Petch', sans-serif; - font-size: 0.9rem; - color: rgba(255, 255, 255, 0.7); - display: flex; - align-items: center; - gap: 6px; +.mapa-tip-premio { + font-size: 0.78rem; + color: #ffd97a; + margin-bottom: 6px; } -.modal-meta-badge { - padding: 4px 10px; - background: rgba(255, 255, 255, 0.15); - border-radius: 4px; +.mapa-tip-desc { font-size: 0.8rem; - font-weight: 600; - color: white; - border: 1px solid rgba(255, 255, 255, 0.2); -} - -/* Descripción corta / tagline */ -.modal-tagline { - font-family: 'Chakra Petch', sans-serif; - font-size: 1.1rem; - font-style: italic; - color: rgba(255, 255, 255, 0.85); - margin: 0 0 16px 0; - line-height: 1.5; + color: rgba(255, 255, 255, 0.78); + line-height: 1.45; + margin-bottom: 10px; } -/* Acciones principales */ -.modal-actions { +.mapa-tip-peso { display: flex; align-items: center; - gap: 12px; - margin-top: 24px; - flex-wrap: wrap; -} - -.modal-btn-primary { - display: inline-flex; - align-items: center; - gap: 10px; - padding: 14px 32px; - background: white; - border: none; - border-radius: 8px; - color: #0a0a14; - font-family: 'Chakra Petch', sans-serif; - font-weight: 700; - font-size: 1rem; - text-decoration: none; - cursor: pointer; - transition: all 0.3s cubic-bezier(0.34, 1.56, 0.64, 1); -} - -.modal-btn-primary:hover { - transform: scale(1.05); - box-shadow: 0 8px 30px rgba(255, 255, 255, 0.3); -} - -.modal-btn-primary svg { - width: 20px; - height: 20px; -} - -.modal-btn-secondary { - display: inline-flex; - align-items: center; - justify-content: center; - width: 48px; - height: 48px; - background: rgba(255, 255, 255, 0.1); - backdrop-filter: blur(10px); - -webkit-backdrop-filter: blur(10px); - border: 1px solid rgba(255, 255, 255, 0.2); - border-radius: 50%; - color: white; - cursor: pointer; - transition: all 0.3s ease; -} - -.modal-btn-secondary:hover { - background: rgba(255, 255, 255, 0.2); - border-color: rgba(255, 255, 255, 0.4); - transform: scale(1.1); -} - -.modal-btn-secondary svg { - width: 22px; - height: 22px; + gap: 8px; + font-size: 0.68rem; + color: rgba(255, 255, 255, 0.55); + text-transform: uppercase; + letter-spacing: 0.05em; + margin-bottom: 10px; } -/* Cuerpo del modal con detalles */ -.modal-body { +.mapa-tip-barra { flex: 1; - padding: 0 48px 40px 48px; - background: rgba(10, 10, 20, 1); -} - -/* Descripción del proyecto */ -.modal-description { - max-width: 800px; - margin-bottom: 24px; -} - -.modal-description:empty { - display: none; - margin-bottom: 0; + height: 4px; + border-radius: 2px; + background: rgba(255, 255, 255, 0.12); + overflow: hidden; } -.modal-description-text { - font-family: 'Chakra Petch', sans-serif; - font-size: 1rem; - color: rgba(255, 255, 255, 0.85); - line-height: 1.7; - margin: 0; +.mapa-tip-barra i { + display: block; + height: 100%; + border-radius: 2px; } -/* Tecnologías / Tags */ -.modal-tecnologias { +.mapa-tip-techs { display: flex; flex-wrap: wrap; - gap: 10px; - margin-top: 20px; -} - -.modal-tecnologias:empty { - display: none; - margin-top: 0; -} - -.modal-tech-tag { - font-family: 'Chakra Petch', sans-serif; - font-size: 0.8rem; - font-weight: 500; - padding: 8px 16px; - background: rgba(147, 197, 253, 0.15); - border: 1px solid rgba(147, 197, 253, 0.3); - border-radius: 20px; - color: rgba(200, 220, 255, 0.95); - letter-spacing: 0.02em; - transition: all 0.3s ease; -} - -.modal-tech-tag:hover { - background: rgba(147, 197, 253, 0.25); - border-color: rgba(147, 197, 253, 0.5); - transform: translateY(-2px); -} - -/* Contenido HTML del proyecto */ -.modal-contenido { - font-family: 'Chakra Petch', sans-serif; - color: rgba(255, 255, 255, 0.85); - line-height: 1.7; - margin-top: 28px; - padding-top: 28px; - border-top: 1px solid rgba(255, 255, 255, 0.1); -} - -.modal-contenido:empty { - display: none; - margin-top: 0; - padding-top: 0; - border-top: none; -} - -.modal-contenido h3 { - font-size: 1.3rem; - font-weight: 700; - color: white; - margin: 0 0 16px 0; -} - -.modal-contenido h4 { - font-size: 1.05rem; - font-weight: 600; - color: rgba(200, 220, 255, 1); - margin: 24px 0 12px 0; -} - -.modal-contenido p { - margin: 0 0 16px 0; - color: rgba(255, 255, 255, 0.8); -} - -.modal-contenido ul, .modal-contenido ol { - margin: 0 0 16px 0; - padding-left: 24px; -} - -.modal-contenido li { - margin-bottom: 8px; - color: rgba(255, 255, 255, 0.8); -} - -.modal-contenido strong { - color: rgba(147, 197, 253, 1); - font-weight: 600; + gap: 4px; } -.modal-contenido code { - font-family: 'JetBrains Mono', monospace; - font-size: 0.85em; - background: rgba(0, 0, 0, 0.4); +.mapa-tip-tech { + font-size: 0.68rem; padding: 2px 8px; - border-radius: 4px; - color: rgba(196, 181, 253, 1); -} - -.modal-contenido pre { - background: rgba(0, 0, 0, 0.5); - border: 1px solid rgba(255, 255, 255, 0.1); - border-radius: 12px; - padding: 16px; - overflow-x: auto; - margin: 16px 0; -} - -.modal-contenido pre code { - background: none; - padding: 0; -} - -.modal-contenido blockquote { - margin: 20px 0; - padding: 16px 20px; - background: linear-gradient(135deg, rgba(147, 197, 253, 0.1), rgba(196, 181, 253, 0.08)); - border-left: 4px solid rgba(147, 197, 253, 0.6); - border-radius: 0 12px 12px 0; - font-style: italic; - color: rgba(255, 255, 255, 0.9); + border-radius: 6px; + border: 1px solid rgba(147, 197, 253, 0.25); + background: rgba(147, 197, 253, 0.08); + color: #bfdbfe; } -.modal-contenido img { - max-width: 100%; - border-radius: 12px; - margin: 16px 0; - box-shadow: 0 8px 30px rgba(0, 0, 0, 0.3); +/* Leyenda bajo el mapa */ +.mapa-leyenda { + font-family: 'Chakra Petch', sans-serif; + font-size: 0.75rem; + color: rgba(255, 255, 255, 0.45); + text-align: center; + margin: 14px auto 0; + max-width: 600px; + line-height: 1.5; } /* ============================================ @@ -962,10 +469,6 @@ .proyecto-titulo { font-size: 0.65rem; } - - .proyectos-grid-list { - grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); - } } @media (max-width: 1024px) { @@ -993,43 +496,6 @@ width: 38px; height: 38px; } - - .aptitud-header { - padding: 20px; - gap: 16px; - } - - .aptitud-icono { - width: 60px; - height: 60px; - } - - .aptitud-titulo { - font-size: 1.3rem; - } - - .aptitud-descripcion { - font-size: 0.85rem; - } - - .modal-backdrop { - height: 58%; - } - - .modal-hero { - padding: 30vh 32px 24px 32px; - gap: 24px; - min-height: 50vh; - } - - .modal-poster-container { - width: 200px; - height: 200px; - } - - .modal-body { - padding: 0 32px 32px 32px; - } } @media (max-width: 768px) { @@ -1072,165 +538,23 @@ max-width: 70px; } - .proyectos-content { - padding: 0 16px 6vh; - gap: 8vh; - } - - .proyectos-grid-list { - grid-template-columns: 1fr; - gap: 16px; - } - - .aptitud-header { - flex-direction: column; - text-align: center; - padding: 20px; - gap: 14px; + .proyectos-mapa { + padding: 0 14px 6vh; } - .aptitud-icono { - width: 55px; - height: 55px; - } - - .aptitud-icono img { - width: 28px; - height: 28px; - } - - .aptitud-titulo { - font-size: 1.2rem; - } - - .proyecto-detail-card { - flex-direction: column; - } - - .proyecto-image-container { - height: 160px; - } - - .proyecto-info-wrapper { - padding: 16px; - } - - .proyecto-detail-titulo { - font-size: 1.1rem; - } - - .proyecto-detail-desc { - font-size: 0.85rem; - } - - /* Modal responsive */ - .proyecto-modal-overlay { - padding: 12px; - align-items: flex-start; - padding-top: 40px; - } - - .proyecto-modal { - max-height: 88vh; - border-radius: 16px; - } - - .modal-backdrop { - border-radius: 16px 16px 0 0; - height: 54%; - } - - .modal-hero { - flex-direction: column; - align-items: center; - text-align: center; - padding: 8vh 20px 14px 20px; - gap: 16px; - min-height: auto; - } - - .modal-poster-container { - width: 160px; - height: 160px; - } - - .modal-hero-info { - display: flex; - flex-direction: column; - align-items: center; - } - - .modal-titulo { - font-size: 1.6rem; - text-align: center; - line-height: 1.2; - margin-bottom: 12px; - } - - .modal-meta { - justify-content: center; - font-size: 0.85rem; - gap: 12px; - } - - .modal-meta-badge { - font-size: 0.75rem; - padding: 3px 8px; - } - - .modal-tagline { - text-align: center; - font-size: 0.95rem; - line-height: 1.4; - } - - .modal-actions { - justify-content: center; - margin-top: 16px; - } - - .modal-btn-primary { - padding: 12px 24px; - font-size: 0.9rem; - } - - .modal-btn-secondary { - width: 44px; - height: 44px; - } - - .modal-body { - padding: 0 20px 24px 20px; - } - - .modal-description { - text-align: left; - } - - .modal-description-text { - font-size: 0.9rem; - line-height: 1.6; - } - - .modal-tecnologias { - justify-content: center; - gap: 8px; - } - - .modal-tech-tag { - font-size: 0.75rem; - padding: 6px 12px; + .mapa-panel { + border-radius: 20px; + padding: 10px; } - .modal-contenido { - font-size: 0.9rem; + .mapa-pill { + padding: 7px 14px; + font-size: 0.78rem; } - .modal-close { - top: 10px; - right: 10px; - width: 40px; - height: 40px; + .mapa-tip { + width: 200px; + padding: 12px 14px; } } @@ -1272,140 +596,26 @@ display: none; } - .proyectos-content { - padding: 0 12px 5vh; - gap: 6vh; - } - - .aptitud-icono { - width: 48px; - height: 48px; - } - - .aptitud-icono img { - width: 24px; - height: 24px; - } - - .aptitud-titulo { - font-size: 1.05rem; - } - - .aptitud-descripcion { - font-size: 0.8rem; - } - - .proyecto-detail-card { - flex-direction: column; - border-radius: 14px; + .proyectos-mapa { + padding: 0 10px 5vh; } - .proyecto-image-container { - height: 140px; + .mapa-pills { + gap: 7px; + margin-bottom: 16px; } - .proyecto-info-wrapper { - padding: 14px; - } - - .proyecto-detail-titulo { - font-size: 0.95rem; - } - - .proyecto-detail-desc { - font-size: 0.8rem; + .mapa-pill { + padding: 6px 12px; + font-size: 0.72rem; } - /* Modal extra small */ - .proyecto-modal-overlay { + .mapa-panel { + border-radius: 16px; padding: 8px; - padding-top: 30px; - } - - .proyecto-modal { - max-height: 90vh; - border-radius: 12px; - } - - .modal-backdrop { - border-radius: 12px 12px 0 0; - height: 40%; - } - - .modal-hero { - padding: 15vh 16px 16px 16px; - gap: 12px; - } - - .modal-poster-container { - width: 140px; - height: 140px; - } - - .modal-titulo { - font-size: 1.35rem; - margin-bottom: 8px; - } - - .modal-meta { - font-size: 0.8rem; - gap: 10px; - } - - .modal-meta-badge { - font-size: 0.7rem; - padding: 2px 6px; - } - - .modal-tagline { - font-size: 0.85rem; - line-height: 1.4; - } - - .modal-actions { - margin-top: 12px; - gap: 10px; - } - - .modal-btn-primary { - padding: 10px 20px; - font-size: 0.85rem; - } - - .modal-btn-secondary { - width: 40px; - height: 40px; - } - - .modal-body { - padding: 0 16px 20px 16px; - } - - .modal-description-text { - font-size: 0.85rem; - } - - .modal-tech-tag { - font-size: 0.7rem; - padding: 5px 10px; - } - - .modal-contenido { - font-size: 0.85rem; - } - - .modal-contenido h3 { - font-size: 1.1rem; - } - - .modal-contenido h4 { - font-size: 0.95rem; } - .modal-close { - width: 36px; - height: 36px; - top: 8px; - right: 8px; + .mapa-leyenda { + font-size: 0.68rem; } } From be479338238a7fbdf2c4f66c29aa668dc93199fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Garc=C3=ADa=20Santamar=C3=ADa?= <69808296+PGSCOM@users.noreply.github.com> Date: Thu, 11 Jun 2026 12:21:52 +0200 Subject: [PATCH 33/49] =?UTF-8?q?Cambio=20en=20la=20secci=C3=B3n=20de=20pr?= =?UTF-8?q?oyectos?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package-lock.json | 177 ------------ package.json | 4 - src/pages/index.astro | 14 +- src/pages/proyectos/[id].astro | 6 +- src/scripts/hero.js | 4 +- src/scripts/preloader.js | 2 +- src/scripts/proyectos-burbujas.js | 458 ------------------------------ src/scripts/proyectos-panal.js | 446 +++++++++++++++++++++++++++++ src/styles/proyectos.css | 12 +- 9 files changed, 467 insertions(+), 656 deletions(-) delete mode 100644 src/scripts/proyectos-burbujas.js create mode 100644 src/scripts/proyectos-panal.js diff --git a/package-lock.json b/package-lock.json index a8e1cf2..b409b69 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,11 +12,7 @@ "@types/react": "^19.1.16", "@types/react-dom": "^19.1.9", "astro": "^5.15.1", - "d3-drag": "^3.0.0", - "d3-force": "^3.0.0", - "d3-scale": "^4.0.2", "d3-selection": "^3.0.0", - "d3-transition": "^3.0.1", "gsap": "^3.15.0", "liquid-glass-react": "^1.1.1", "react": "^19.1.1", @@ -2453,118 +2449,6 @@ "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", "license": "MIT" }, - "node_modules/d3-array": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", - "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", - "license": "ISC", - "dependencies": { - "internmap": "1 - 2" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-color": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", - "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-dispatch": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", - "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-drag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", - "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", - "license": "ISC", - "dependencies": { - "d3-dispatch": "1 - 3", - "d3-selection": "3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-ease": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", - "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", - "license": "BSD-3-Clause", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-force": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-3.0.0.tgz", - "integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==", - "license": "ISC", - "dependencies": { - "d3-dispatch": "1 - 3", - "d3-quadtree": "1 - 3", - "d3-timer": "1 - 3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-format": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz", - "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-interpolate": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", - "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", - "license": "ISC", - "dependencies": { - "d3-color": "1 - 3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-quadtree": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz", - "integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-scale": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", - "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", - "license": "ISC", - "dependencies": { - "d3-array": "2.10.0 - 3", - "d3-format": "1 - 3", - "d3-interpolate": "1.2.0 - 3", - "d3-time": "2.1.1 - 3", - "d3-time-format": "2 - 4" - }, - "engines": { - "node": ">=12" - } - }, "node_modules/d3-selection": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", @@ -2574,58 +2458,6 @@ "node": ">=12" } }, - "node_modules/d3-time": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", - "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", - "license": "ISC", - "dependencies": { - "d3-array": "2 - 3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-time-format": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", - "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", - "license": "ISC", - "dependencies": { - "d3-time": "1 - 3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-timer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", - "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-transition": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", - "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", - "license": "ISC", - "dependencies": { - "d3-color": "1 - 3", - "d3-dispatch": "1 - 3", - "d3-ease": "1 - 3", - "d3-interpolate": "1 - 3", - "d3-timer": "1 - 3" - }, - "engines": { - "node": ">=12" - }, - "peerDependencies": { - "d3-selection": "2 - 3" - } - }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -3242,15 +3074,6 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/internmap": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", - "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, "node_modules/iron-webcrypto": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/iron-webcrypto/-/iron-webcrypto-1.2.1.tgz", diff --git a/package.json b/package.json index 40f2b98..f23e3dd 100644 --- a/package.json +++ b/package.json @@ -13,11 +13,7 @@ "@types/react": "^19.1.16", "@types/react-dom": "^19.1.9", "astro": "^5.15.1", - "d3-drag": "^3.0.0", - "d3-force": "^3.0.0", - "d3-scale": "^4.0.2", "d3-selection": "^3.0.0", - "d3-transition": "^3.0.1", "gsap": "^3.15.0", "liquid-glass-react": "^1.1.1", "react": "^19.1.1", diff --git a/src/pages/index.astro b/src/pages/index.astro index 64f042a..0e417e1 100644 --- a/src/pages/index.astro +++ b/src/pages/index.astro @@ -142,7 +142,7 @@ const burbujasData = {

Portfolio de Pablo García Santamaría — proyectos de código, cine y diseño.

@@ -166,7 +166,7 @@ const burbujasData = {

Proyectos

- Un mapa interactivo de mis trabajos: cada burbuja es un proyecto y su tamaño refleja la envergadura. Filtra por categoría o arrastra las burbujas. + Todos mis proyectos de un vistazo. Cada icono es un proyecto, su tamaño refleja la envergadura y el color indica la categoría.

@@ -203,17 +203,17 @@ const burbujasData = {
-
+
- +

- Tamaño = envergadura del proyecto · Pasa el cursor para ver detalles · Pulsa una burbuja para abrir el proyecto + Tamaño = envergadura · Mueve el cursor para ampliar · Pulsa un icono para abrir el proyecto

@@ -496,12 +496,12 @@ const burbujasData = { import '../scripts/galaxia-scroll.js'; - + diff --git a/src/pages/proyectos/[id].astro b/src/pages/proyectos/[id].astro index 7c0b95d..aff4a3a 100644 --- a/src/pages/proyectos/[id].astro +++ b/src/pages/proyectos/[id].astro @@ -52,7 +52,7 @@ const breadcrumbLd = { '@type': 'BreadcrumbList', itemListElement: [ { '@type': 'ListItem', position: 1, name: 'Inicio', item: Astro.site?.href }, - { '@type': 'ListItem', position: 2, name: 'Proyectos', item: new URL('/#proyectos', Astro.site).href }, + { '@type': 'ListItem', position: 2, name: 'Proyectos', item: new URL('/#proyectos-mapa', Astro.site).href }, { '@type': 'ListItem', position: 3, name: proyecto.titulo.trim(), item: new URL(`/proyectos/${proyecto.id}`, Astro.site).href }, ], }; @@ -73,7 +73,7 @@ const breadcrumbLd = {
- + @@ -86,7 +86,7 @@ const breadcrumbLd = { - Proyectos + Proyectos {aptitud.titulo} diff --git a/src/scripts/hero.js b/src/scripts/hero.js index 0474fff..cb8e4e7 100644 --- a/src/scripts/hero.js +++ b/src/scripts/hero.js @@ -129,13 +129,13 @@ function initHero() { } // ── Botón "Ver proyectos": scroll suave para no saltarse las animaciones ── - // En vez de saltar directo a #proyectos (lo que se salta el zoom de galaxia y + // En vez de saltar directo al panal (lo que se salta el zoom de galaxia y // la salida de las cards), recorremos toda la página con un scroll animado por // tiempo para que se vean todas las animaciones de scroll antes de llegar. const cta = document.querySelector('.hero-cta'); if (cta) { cta.addEventListener('click', (e) => { - const target = document.querySelector('#proyectos'); + const target = document.querySelector('#proyectos-mapa') || document.querySelector('#proyectos'); if (!target) return; e.preventDefault(); diff --git a/src/scripts/preloader.js b/src/scripts/preloader.js index 408f9a3..3ed6497 100644 --- a/src/scripts/preloader.js +++ b/src/scripts/preloader.js @@ -1,5 +1,5 @@ import gsap from 'gsap'; -import { DrawSVGPlugin } from 'gsap/DrawSVGPlugin'; +import { DrawSVGPlugin } from 'gsap/DrawSVGPlugin.js'; gsap.registerPlugin(DrawSVGPlugin); // ── Preloader: la P de carga.svg se construye trazándose con DrawSVG ───────── diff --git a/src/scripts/proyectos-burbujas.js b/src/scripts/proyectos-burbujas.js deleted file mode 100644 index 64b4b70..0000000 --- a/src/scripts/proyectos-burbujas.js +++ /dev/null @@ -1,458 +0,0 @@ -import { forceSimulation, forceCollide, forceManyBody, forceX, forceY } from 'd3-force'; -import { select } from 'd3-selection'; -import { scaleSqrt } from 'd3-scale'; -import { drag } from 'd3-drag'; -import 'd3-transition'; -import gsap from 'gsap'; - -// ── Mapa de proyectos: burbujas-squircle con simulación de física ───────────── -// Cada proyecto es un "squircle" (forma de icono de iOS) cuyo tamaño viene de -// la propiedad `peso` del JSON. Las burbujas se agrupan por categoría en zonas -// y se pueden filtrar desde las pills o desde la órbita de categorías. - -function initBurbujas() { - const dataEl = document.getElementById('burbujas-data'); - const wrap = document.getElementById('mapaWrap'); - const tip = document.getElementById('mapaTip'); - const pillsEl = document.getElementById('mapaPills'); - if (!dataEl || !wrap || !tip || !pillsEl) return; - - const { cats, projects } = JSON.parse(dataEl.textContent); - const catById = Object.fromEntries(cats.map((c) => [c.id, c])); - const reduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches; - - const svg = select('#mapaChart'); - if (svg.empty()) return; - - let curCat = 'all'; - let entered = false; // ya se reprodujo la animación de entrada - let narrow = isNarrow(); - let sim = null; - let nodes = []; - let bubs = null; - let W = 0; - let H = 0; - let zones = {}; - - function isNarrow() { - return (wrap.clientWidth || window.innerWidth) < 620; - } - - // Superellipse |x/r|^n + |y/r|^n = 1 → la forma "casi cuadrada" de los - // iconos de iOS. n controla cuánto se acerca al cuadrado. - function squircle(r, n = 4.5) { - const STEPS = 64; - let d = ''; - for (let i = 0; i < STEPS; i++) { - const t = (i / STEPS) * Math.PI * 2; - const c = Math.cos(t); - const s = Math.sin(t); - const x = r * Math.sign(c) * Math.abs(c) ** (2 / n); - const y = r * Math.sign(s) * Math.abs(s) ** (2 / n); - d += (i === 0 ? 'M' : 'L') + x.toFixed(2) + ',' + y.toFixed(2); - } - return d + 'Z'; - } - - // La esquina del squircle sobresale del círculo inscrito (~4%) - const EXTENT = Math.SQRT2 * 0.5 ** (2 / 4.5); - - function wrapTitle(t, maxChars) { - const words = t.trim().split(/\s+/); - const lines = []; - let cur = ''; - for (const w of words) { - if (!cur) cur = w; - else if ((cur + ' ' + w).length <= maxChars) cur += ' ' + w; - else { lines.push(cur); cur = w; } - } - if (cur) lines.push(cur); - if (lines.length > 2) { - lines.length = 2; - lines[1] = lines[1].slice(0, Math.max(1, maxChars - 1)) + '…'; - } - return lines; - } - - function target(d) { - return curCat === 'all' ? zones[d.cat] : [W / 2, H / 2]; - } - - function clampX(d) { return Math.max(d.r * EXTENT + 2, Math.min(W - d.r * EXTENT - 2, d.x)); } - function clampY(d) { return Math.max(d.r * EXTENT + 2, Math.min(H - d.r * EXTENT - 2, d.y)); } - - function redraw() { - if (!bubs) return; - bubs.attr('transform', (d) => { - d.x = clampX(d); - d.y = clampY(d); - return `translate(${d.x},${d.y}) scale(${d.k * d.h})`; - }); - } - - function build() { - narrow = isNarrow(); - const cols = narrow ? 2 : 3; - const rows = Math.ceil(cats.length / cols); - W = narrow ? 640 : 1000; - H = narrow ? 980 : 640; - const cellW = W / cols; - const cellH = H / rows; - - zones = {}; - cats.forEach((c, i) => { - const col = i % cols; - const row = Math.floor(i / cols); - // Desplazado hacia abajo para dejar aire a la etiqueta de zona - zones[c.id] = [(col + 0.5) * cellW, (row + 0.5) * cellH + 18]; - }); - - const rScale = scaleSqrt().domain([0, 100]).range([0, narrow ? 62 : 86]); - const keepK = entered || reduced ? 1 : 0; - nodes = projects.map((p, i) => { - const [zx, zy] = zones[p.cat] || [W / 2, H / 2]; - return { - ...p, - r: Math.max(narrow ? 26 : 32, rScale(p.peso ?? 50)), - x: zx + Math.cos(i * 2.4) * 30, - y: zy + Math.sin(i * 2.4) * 30, - k: keepK, // escala de entrada - h: 1, // escala de hover - }; - }); - - if (sim) sim.stop(); - svg.selectAll('*').remove(); - svg.attr('viewBox', `0 0 ${W} ${H}`); - - // ── Defs: degradado de legibilidad + clip squircle por burbuja ── - const defs = svg.append('defs'); - const grad = defs.append('linearGradient') - .attr('id', 'bub-fade').attr('x1', 0).attr('y1', 0).attr('x2', 0).attr('y2', 1); - grad.append('stop').attr('offset', '0.3').attr('stop-color', 'rgba(2,8,23,0)'); - grad.append('stop').attr('offset', '1').attr('stop-color', 'rgba(2,8,23,0.86)'); - - nodes.forEach((d) => { - defs.append('clipPath').attr('id', `bclip-${d.id}`) - .append('path').attr('d', squircle(d.r)); - }); - - // ── Divisores y etiquetas de zona ── - const divG = svg.append('g').attr('class', 'mapa-divs'); - for (let c = 1; c < cols; c++) { - divG.append('line') - .attr('x1', c * cellW).attr('y1', 16).attr('x2', c * cellW).attr('y2', H - 16) - .attr('stroke', 'rgba(255,255,255,0.13)').attr('stroke-width', 1).attr('stroke-dasharray', '3,7'); - } - for (let r = 1; r < rows; r++) { - divG.append('line') - .attr('x1', 16).attr('y1', r * cellH).attr('x2', W - 16).attr('y2', r * cellH) - .attr('stroke', 'rgba(255,255,255,0.13)').attr('stroke-width', 1).attr('stroke-dasharray', '3,7'); - } - - const zlG = svg.append('g').attr('class', 'mapa-zlabels'); - cats.forEach((c, i) => { - const col = i % cols; - const row = Math.floor(i / cols); - zlG.append('text') - .attr('x', (col + 0.5) * cellW) - .attr('y', row * cellH + 22) - .attr('text-anchor', 'middle') - .attr('font-family', "'Chakra Petch', sans-serif") - .attr('font-size', narrow ? 12 : 13) - .attr('font-weight', 600) - .attr('letter-spacing', '0.12em') - .attr('fill', c.color) - .attr('opacity', 0.85) - .text(c.titulo.toUpperCase()); - }); - - // ── Burbujas ── - bubs = svg.append('g').attr('class', 'mapa-bubs') - .selectAll('g') - .data(nodes) - .enter() - .append('g') - .attr('class', 'bub') - .attr('tabindex', 0) - .attr('role', 'link') - .attr('aria-label', (d) => `${d.titulo} — ${catById[d.cat]?.titulo ?? ''}`); - - // Fondo (por si la imagen tarda) + imagen recortada al squircle - bubs.append('path') - .attr('d', (d) => squircle(d.r)) - .attr('fill', 'rgba(13,18,38,0.95)'); - - bubs.append('image') - .attr('href', (d) => d.imagen) - .attr('x', (d) => -d.r).attr('y', (d) => -d.r) - .attr('width', (d) => d.r * 2).attr('height', (d) => d.r * 2) - .attr('preserveAspectRatio', 'xMidYMid slice') - .attr('clip-path', (d) => `url(#bclip-${d.id})`) - .each(function (d) { - // Si la imagen no existe: fondo tintado del color de la categoría + su icono - this.addEventListener('error', () => { - const g = select(this.parentNode); - const color = catById[d.cat]?.color ?? '#9bb4d0'; - this.remove(); - g.insert('path', '.bub-ring') - .attr('d', squircle(d.r)) - .attr('fill', color) - .attr('fill-opacity', 0.16); - const logo = catById[d.cat]?.logo; - if (logo) { - const s = d.r * 0.62; - g.insert('image', '.bub-ring') - .attr('href', logo) - .attr('x', -s / 2) - .attr('y', -s / 2 - d.r * 0.14) - .attr('width', s) - .attr('height', s) - .attr('opacity', 0.9) - .attr('pointer-events', 'none'); - } - }, { once: true }); - }); - - bubs.append('path') - .attr('d', (d) => squircle(d.r)) - .attr('fill', 'url(#bub-fade)'); - - bubs.append('path') - .attr('class', 'bub-ring') - .attr('d', (d) => squircle(d.r)) - .attr('fill', 'none') - .attr('stroke', (d) => catById[d.cat]?.color ?? '#9bb4d0') - .attr('stroke-width', 1.6) - .attr('opacity', 0.9); - - // Título dentro de la burbuja (solo si cabe) - bubs.each(function (d) { - if (d.r < 40) return; - const g = select(this); - const fs = Math.max(11, Math.min(17, d.r * 0.24)); - const maxChars = Math.max(7, Math.floor((d.r * 1.55) / (fs * 0.56))); - const lines = wrapTitle(d.titulo, maxChars); - const lh = fs + 2; - const baseY = d.r * 0.66; - lines.forEach((line, i) => { - g.append('text') - .attr('class', 'bub-titulo') - .attr('text-anchor', 'middle') - .attr('x', 0) - .attr('y', baseY - (lines.length - 1 - i) * lh) - .attr('font-family', "'Chakra Petch', sans-serif") - .attr('font-size', fs) - .attr('font-weight', 600) - .attr('fill', '#fff') - .attr('stroke', 'rgba(0,8,20,0.55)') - .attr('stroke-width', 2.5) - .attr('paint-order', 'stroke') - .attr('pointer-events', 'none') - .text(line); - }); - if (d.premio) { - g.append('text') - .attr('text-anchor', 'middle') - .attr('x', 0) - .attr('y', -d.r * 0.58) - .attr('font-size', Math.max(13, d.r * 0.24)) - .attr('pointer-events', 'none') - .text('🏆'); - } - }); - - // ── Física ── - sim = forceSimulation(nodes) - .force('charge', forceManyBody().strength(-14)) - .force('collide', forceCollide((d) => d.r * 1.05 + 4).strength(0.95)) - .force('x', forceX((d) => target(d)[0]).strength(curCat === 'all' ? 0.07 : 0.12)) - .force('y', forceY((d) => target(d)[1]).strength(curCat === 'all' ? 0.07 : 0.12)) - .on('tick', redraw) - .stop(); - - // ── Arrastrar burbujas ── - bubs.call( - drag() - .clickDistance(6) - .on('start', (event, d) => { - if (!event.active) sim.alphaTarget(0.25).restart(); - d.fx = d.x; - d.fy = d.y; - }) - .on('drag', (event, d) => { d.fx = event.x; d.fy = event.y; }) - .on('end', (event, d) => { - if (!event.active) sim.alphaTarget(0); - d.fx = null; - d.fy = null; - }) - ); - - // ── Interacción: tooltip, click y teclado ── - bubs - .on('click', (event, d) => { - if (event.defaultPrevented) return; // venía de un drag - window.location.href = `/proyectos/${d.id}`; - }) - .on('keydown', (event, d) => { - if (event.key === 'Enter' || event.key === ' ') { - event.preventDefault(); - window.location.href = `/proyectos/${d.id}`; - } - }) - .on('mouseenter', function (event, d) { - select(this).raise(); - select(this).select('.bub-ring').attr('stroke-width', 3).attr('opacity', 1); - gsap.to(d, { h: 1.06, duration: 0.25, ease: 'power2.out', onUpdate: redraw }); - showTip(d); - moveTip(event); - }) - .on('mousemove', (event) => moveTip(event)) - .on('mouseleave', function (event, d) { - select(this).select('.bub-ring').attr('stroke-width', 1.6).attr('opacity', 0.9); - gsap.to(d, { h: 1, duration: 0.25, ease: 'power2.out', onUpdate: redraw }); - tip.style.display = 'none'; - }); - - applyFilterStyles(0); - redraw(); - - if (entered || reduced) { - sim.alpha(0.9).restart(); - } - } - - // ── Tooltip ── - function showTip(d) { - const cat = catById[d.cat]; - const techs = (d.tecnologias || []) - .map((t) => `${t}`) - .join(''); - tip.innerHTML = ` -
${d.titulo}
-
${cat?.titulo ?? ''}
- ${d.premio ? `
🏆 ${d.premio}
` : ''} - ${d.descripcion ? `
${d.descripcion}
` : ''} -
- Envergadura - -
- ${techs ? `
${techs}
` : ''} - `; - tip.style.display = 'block'; - } - - function moveTip(event) { - const r = wrap.getBoundingClientRect(); - const x = event.clientX - r.left; - const y = event.clientY - r.top; - const tw = tip.offsetWidth || 220; - tip.style.left = (x + 16 + tw < r.width ? x + 16 : Math.max(0, x - tw - 16)) + 'px'; - tip.style.top = Math.max(0, Math.min(y - 12, r.height - tip.offsetHeight - 8)) + 'px'; - } - - // ── Filtro por categoría ── - function applyFilterStyles(dur) { - if (!bubs) return; - const visible = (d) => curCat === 'all' || d.cat === curCat; - bubs - .style('pointer-events', (d) => (visible(d) ? 'auto' : 'none')) - .attr('tabindex', (d) => (visible(d) ? 0 : -1)) - .transition().duration(dur) - .style('opacity', (d) => (visible(d) ? 1 : 0.07)); - svg.select('.mapa-divs').transition().duration(dur).style('opacity', curCat === 'all' ? 1 : 0); - svg.select('.mapa-zlabels').transition().duration(dur).style('opacity', curCat === 'all' ? 1 : 0); - } - - function setFilter(cat) { - curCat = cat; - const s = cat === 'all' ? 0.07 : 0.12; - sim.force('x', forceX((d) => target(d)[0]).strength(s)); - sim.force('y', forceY((d) => target(d)[1]).strength(s)); - sim.alpha(0.6).restart(); - applyFilterStyles(350); - tip.style.display = 'none'; - - pillsEl.querySelectorAll('.mapa-pill').forEach((p) => { - p.classList.toggle('act', p.dataset.cat === cat); - p.setAttribute('aria-pressed', p.dataset.cat === cat ? 'true' : 'false'); - }); - document.querySelectorAll('.proyecto-card').forEach((card) => { - card.classList.toggle('is-active', card.dataset.aptitudId === cat); - }); - } - - // ── Pills de filtro ── - [{ id: 'all', titulo: 'Todos', color: null }, ...cats].forEach((c) => { - const el = document.createElement('button'); - el.type = 'button'; - el.className = 'mapa-pill' + (c.id === 'all' ? ' act' : ''); - el.dataset.cat = c.id; - el.setAttribute('aria-pressed', c.id === 'all' ? 'true' : 'false'); - if (c.color) { - const dot = document.createElement('span'); - dot.className = 'mapa-pill-dot'; - dot.style.background = c.color; - el.appendChild(dot); - } - el.appendChild(document.createTextNode(c.titulo)); - el.addEventListener('click', () => setFilter(curCat === c.id && c.id !== 'all' ? 'all' : c.id)); - pillsEl.appendChild(el); - }); - - // ── La órbita de categorías también filtra el mapa ── - document.querySelectorAll('.proyecto-card').forEach((card) => { - card.addEventListener('click', (e) => { - e.preventDefault(); - const id = card.dataset.aptitudId; - if (!id) return; - setFilter(curCat === id ? 'all' : id); - document.getElementById('proyectos-mapa')?.scrollIntoView({ - behavior: reduced ? 'auto' : 'smooth', - block: 'nearest', - }); - }); - }); - - build(); - - // ── Entrada: las burbujas brotan al entrar en viewport ── - const io = new IntersectionObserver( - (entries) => { - if (!entries.some((e) => e.isIntersecting) || entered) return; - entered = true; - io.disconnect(); - sim.alpha(0.9).restart(); - if (reduced) { - nodes.forEach((d) => (d.k = 1)); - redraw(); - } else { - gsap.to(nodes, { - k: 1, - duration: 0.8, - ease: 'back.out(1.7)', - stagger: 0.045, - onUpdate: redraw, - }); - } - }, - { threshold: 0.15 } - ); - io.observe(wrap); - - // ── Cambio de layout (3×2 ↔ 2×3) al redimensionar ── - let resizeRaf = 0; - window.addEventListener('resize', () => { - if (resizeRaf) return; - resizeRaf = requestAnimationFrame(() => { - resizeRaf = 0; - if (isNarrow() !== narrow) build(); - }); - }); -} - -if (document.readyState === 'loading') { - document.addEventListener('DOMContentLoaded', initBurbujas); -} else { - initBurbujas(); -} diff --git a/src/scripts/proyectos-panal.js b/src/scripts/proyectos-panal.js new file mode 100644 index 0000000..f1404fb --- /dev/null +++ b/src/scripts/proyectos-panal.js @@ -0,0 +1,446 @@ +import { select } from 'd3-selection'; +import gsap from 'gsap'; + +// ── Panal de proyectos: springboard estilo Apple ────────────────────────────── +// Todos los proyectos visibles a la vez como iconos-squircle en una rejilla +// hexagonal, con su nombre debajo. El tamaño del icono viene de `peso`, el aro +// de color indica la categoría, y al mover el cursor una "lupa" tipo Apple +// Watch magnifica los iconos cercanos. Las chips de leyenda resaltan una +// categoría sin ocultar el resto. Escala solo: más proyectos → más filas. + +function initPanal() { + const dataEl = document.getElementById('burbujas-data'); + const wrap = document.getElementById('mapaWrap'); + const tip = document.getElementById('mapaTip'); + const chipsEl = document.getElementById('mapaPills'); + if (!dataEl || !wrap || !tip || !chipsEl) return; + + const { cats, projects } = JSON.parse(dataEl.textContent); + const catById = Object.fromEntries(cats.map((c) => [c.id, c])); + const reduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches; + const finePointer = window.matchMedia('(pointer: fine)').matches; + + const svg = select('#mapaChart'); + if (svg.empty()) return; + + let entered = false; // animación de entrada ya reproducida + let hlCat = null; // categoría resaltada de forma fija (chips / órbita) + let narrow = isNarrow(); + let nodes = []; + let icons = null; + let W = 0; + let H = 0; + let fisheyeOn = false; + let rafId = 0; + + function isNarrow() { + return (wrap.clientWidth || window.innerWidth) < 620; + } + + // Superellipse |x/r|^n + |y/r|^n = 1 → forma de icono de iOS + function squircle(r, n = 4.5) { + const STEPS = 64; + let d = ''; + for (let i = 0; i < STEPS; i++) { + const t = (i / STEPS) * Math.PI * 2; + const c = Math.cos(t); + const s = Math.sin(t); + const x = r * Math.sign(c) * Math.abs(c) ** (2 / n); + const y = r * Math.sign(s) * Math.abs(s) ** (2 / n); + d += (i === 0 ? 'M' : 'L') + x.toFixed(2) + ',' + y.toFixed(2); + } + return d + 'Z'; + } + + function wrapTitle(t, maxChars) { + const words = t.trim().split(/\s+/); + const lines = []; + let cur = ''; + for (const w of words) { + if (!cur) cur = w; + else if ((cur + ' ' + w).length <= maxChars) cur += ' ' + w; + else { lines.push(cur); cur = w; } + } + if (cur) lines.push(cur); + if (lines.length > 2) { + lines.length = 2; + lines[1] = lines[1].slice(0, Math.max(1, maxChars - 1)) + '…'; + } + return lines; + } + + function render() { + if (!icons) return; + icons.attr('transform', (d) => + `translate(${d.x + d.ox},${d.y + d.oy}) scale(${d.k * d.s})`); + } + + function build() { + narrow = isNarrow(); + W = narrow ? 400 : 1000; + const side = narrow ? 14 : 30; + const cols = narrow ? 3 : 6; + const dx = (W - side * 2) / cols; + const rMin = narrow ? 27 : 36; + const rMax = narrow ? 41 : 54; + const labelFs = narrow ? 11.5 : 13; + const dy = rMax * 2 + labelFs * 2 + (narrow ? 14 : 18); + + // ── Posiciones en panal: filas equilibradas (sin huérfanos) y centradas, + // con desplazamiento alterno para el aspecto hexagonal ── + const n = projects.length; + const numRows = Math.max(1, Math.ceil(n / cols)); + const base = Math.floor(n / numRows); + const extra = n % numRows; + const slots = []; + let row = 0; + for (let r = 0; r < numRows; r++) { + const inRow = base + (r < extra ? 1 : 0); + const rowW = inRow * dx; + const shift = narrow ? 0 : (r % 2 === 0 ? -1 : 1) * dx / 4; + const startX = (W - rowW) / 2 + dx / 2 + shift; + for (let c = 0; c < inRow; c++) { + slots.push([startX + c * dx, side + rMax + 6 + r * dy]); + } + row++; + } + H = side + row * dy + labelFs * 2; + + const keepK = entered || reduced ? 1 : 0; + nodes = projects.map((p, i) => ({ + ...p, + r: rMin + (rMax - rMin) * Math.sqrt((p.peso ?? 50) / 100), + x: slots[i][0], + y: slots[i][1], + k: keepK, // escala de entrada + s: 1, // escala de lupa (actual) + ox: 0, // desplazamiento de lupa (actual) + oy: 0, + ts: 1, // objetivos hacia los que se interpola + tox: 0, + toy: 0, + })); + + svg.selectAll('*').remove(); + svg.attr('viewBox', `0 0 ${W} ${H}`); + + // ── Defs: degradado de legibilidad + clip squircle por icono ── + const defs = svg.append('defs'); + const grad = defs.append('linearGradient') + .attr('id', 'bub-fade').attr('x1', 0).attr('y1', 0).attr('x2', 0).attr('y2', 1); + grad.append('stop').attr('offset', '0.45').attr('stop-color', 'rgba(2,8,23,0)'); + grad.append('stop').attr('offset', '1').attr('stop-color', 'rgba(2,8,23,0.55)'); + + nodes.forEach((d) => { + defs.append('clipPath').attr('id', `bclip-${d.id}`) + .append('path').attr('d', squircle(d.r)); + }); + + // ── Iconos: enlaces SVG reales ── + icons = svg.append('g').attr('class', 'mapa-icons') + .selectAll('a') + .data(nodes) + .enter() + .append('a') + .attr('class', 'picon') + .attr('href', (d) => `/proyectos/${d.id}`) + .attr('aria-label', (d) => `${d.titulo} — ${catById[d.cat]?.titulo ?? ''}`); + + icons.append('path') + .attr('d', (d) => squircle(d.r)) + .attr('fill', 'rgba(13,18,38,0.95)'); + + icons.append('image') + .attr('href', (d) => d.imagen) + .attr('x', (d) => -d.r).attr('y', (d) => -d.r) + .attr('width', (d) => d.r * 2).attr('height', (d) => d.r * 2) + .attr('preserveAspectRatio', 'xMidYMid slice') + .attr('clip-path', (d) => `url(#bclip-${d.id})`) + .each(function (d) { + // Si la imagen no existe: fondo tintado de la categoría + su icono + this.addEventListener('error', () => { + const g = select(this.parentNode); + const color = catById[d.cat]?.color ?? '#9bb4d0'; + this.remove(); + g.insert('path', '.picon-ring') + .attr('d', squircle(d.r)) + .attr('fill', color) + .attr('fill-opacity', 0.16); + const logo = catById[d.cat]?.logo; + if (logo) { + const s = d.r * 0.78; + g.insert('image', '.picon-ring') + .attr('href', logo) + .attr('x', -s / 2) + .attr('y', -s / 2) + .attr('width', s) + .attr('height', s) + .attr('opacity', 0.9) + .attr('pointer-events', 'none'); + } + }, { once: true }); + }); + + icons.append('path') + .attr('d', (d) => squircle(d.r)) + .attr('fill', 'url(#bub-fade)'); + + icons.append('path') + .attr('class', 'picon-ring') + .attr('d', (d) => squircle(d.r)) + .attr('fill', 'none') + .attr('stroke', (d) => catById[d.cat]?.color ?? '#9bb4d0') + .attr('stroke-width', 1.6); + + // Insignia de premio + icons.filter((d) => d.premio) + .append('text') + .attr('x', (d) => d.r * 0.72) + .attr('y', (d) => -d.r * 0.62) + .attr('text-anchor', 'middle') + .attr('font-size', (d) => Math.max(13, d.r * 0.34)) + .attr('pointer-events', 'none') + .text('🏆'); + + // Nombre bajo el icono, como en una pantalla de inicio + icons.each(function (d) { + const g = select(this); + const maxChars = Math.max(10, Math.floor((narrow ? 15 : 17) * (d.r / rMax + 0.35))); + const lines = wrapTitle(d.titulo, maxChars); + lines.forEach((line, i) => { + g.append('text') + .attr('class', 'picon-label') + .attr('text-anchor', 'middle') + .attr('x', 0) + .attr('y', d.r + labelFs + 4 + i * (labelFs + 2)) + .attr('font-family', "'Chakra Petch', sans-serif") + .attr('font-size', labelFs) + .attr('font-weight', 500) + .attr('fill', 'rgba(255,255,255,0.88)') + .attr('stroke', 'rgba(0,8,20,0.6)') + .attr('stroke-width', 2.5) + .attr('paint-order', 'stroke') + .attr('pointer-events', 'none') + .text(line); + }); + }); + + // ── Tooltip y resaltado por icono ── + icons + .on('click', (event, d) => { + // Marca la imagen para el morph de view-transition hacia la ficha + const img = event.currentTarget.querySelector('image'); + if (img) img.style.viewTransitionName = 'project-hero'; + }) + .on('mouseenter', function (event, d) { + select(this).raise(); + select(this).select('.picon-ring').attr('stroke-width', 3); + showTip(d); + moveTip(event); + }) + .on('mousemove', (event) => moveTip(event)) + .on('mouseleave', function () { + select(this).select('.picon-ring').attr('stroke-width', 1.6); + tip.style.display = 'none'; + }) + .on('focus', function (event, d) { + showTip(d); + const r = wrap.getBoundingClientRect(); + const b = this.getBoundingClientRect(); + positionTip(b.right - r.left + 10, b.top - r.top); + }) + .on('blur', () => { tip.style.display = 'none'; }); + + applyHighlight(); + render(); + + // ── Lupa tipo Apple Watch ── + fisheyeOn = finePointer && !reduced && !narrow; + if (fisheyeOn) { + const sigma = dx * 0.85; + const AMP = 0.5; + const PUSH = dx * 0.16; + + const els = icons.nodes(); + let raised = null; + const setTargets = (mx, my) => { + let nearest = -1; + let nearestDist = Infinity; + nodes.forEach((d, i) => { + const ddx = d.x - mx; + const ddy = d.y - my; + const dist = Math.hypot(ddx, ddy) || 1; + const f = Math.exp(-(dist * dist) / (2 * sigma * sigma)); + d.ts = 1 + AMP * f; + d.tox = (ddx / dist) * PUSH * f; + d.toy = (ddy / dist) * PUSH * f; + if (dist < nearestDist) { nearestDist = dist; nearest = i; } + }); + // El icono más cercano al cursor pasa al frente para que su + // nombre no quede tapado por la fila siguiente + if (nearest >= 0 && els[nearest] !== raised) { + raised = els[nearest]; + raised.parentNode.appendChild(raised); + } + startLoop(); + }; + + const clearTargets = () => { + for (const d of nodes) { d.ts = 1; d.tox = 0; d.toy = 0; } + startLoop(); + }; + + svg.node().addEventListener('pointermove', (e) => { + const rect = svg.node().getBoundingClientRect(); + setTargets( + ((e.clientX - rect.left) / rect.width) * W, + ((e.clientY - rect.top) / rect.height) * H + ); + }); + svg.node().addEventListener('pointerleave', clearTargets); + } + } + + // Interpola suavemente escala/desplazamiento hacia sus objetivos + function startLoop() { + if (rafId) return; + const step = () => { + let busy = false; + for (const d of nodes) { + d.s += (d.ts - d.s) * 0.22; + d.ox += (d.tox - d.ox) * 0.22; + d.oy += (d.toy - d.oy) * 0.22; + if (Math.abs(d.ts - d.s) > 0.002 || Math.abs(d.tox - d.ox) > 0.1 || Math.abs(d.toy - d.oy) > 0.1) { + busy = true; + } + } + render(); + rafId = busy ? requestAnimationFrame(step) : 0; + }; + rafId = requestAnimationFrame(step); + } + + // ── Tooltip ── + function showTip(d) { + const cat = catById[d.cat]; + const techs = (d.tecnologias || []) + .map((t) => `${t}`) + .join(''); + tip.innerHTML = ` +
${d.titulo}
+
${cat?.titulo ?? ''}
+ ${d.premio ? `
🏆 ${d.premio}
` : ''} + ${d.descripcion ? `
${d.descripcion}
` : ''} +
+ Envergadura + +
+ ${techs ? `
${techs}
` : ''} + `; + tip.style.display = 'block'; + } + + function positionTip(x, y) { + const r = wrap.getBoundingClientRect(); + const tw = tip.offsetWidth || 220; + tip.style.left = (x + tw < r.width ? x : Math.max(0, x - tw - 28)) + 'px'; + tip.style.top = Math.max(0, Math.min(y, r.height - tip.offsetHeight - 8)) + 'px'; + } + + function moveTip(event) { + const r = wrap.getBoundingClientRect(); + positionTip(event.clientX - r.left + 16, event.clientY - r.top - 12); + } + + // ── Resaltado por categoría (no oculta nada, solo atenúa el resto) ── + function applyHighlight(previewCat) { + const cat = previewCat !== undefined ? previewCat : hlCat; + if (!icons) return; + icons.classed('dim', (d) => cat !== null && d.cat !== cat); + chipsEl.querySelectorAll('.mapa-pill').forEach((p) => { + p.classList.toggle('act', p.dataset.cat === hlCat); + p.setAttribute('aria-pressed', p.dataset.cat === hlCat ? 'true' : 'false'); + }); + document.querySelectorAll('.proyecto-card').forEach((card) => { + card.classList.toggle('is-active', card.dataset.aptitudId === hlCat); + }); + } + + function setHighlight(cat) { + hlCat = cat; + applyHighlight(); + } + + // ── Chips de leyenda ── + cats.forEach((c) => { + const el = document.createElement('button'); + el.type = 'button'; + el.className = 'mapa-pill'; + el.dataset.cat = c.id; + el.setAttribute('aria-pressed', 'false'); + const dot = document.createElement('span'); + dot.className = 'mapa-pill-dot'; + dot.style.background = c.color; + el.appendChild(dot); + el.appendChild(document.createTextNode(c.titulo)); + el.addEventListener('click', () => setHighlight(hlCat === c.id ? null : c.id)); + el.addEventListener('mouseenter', () => applyHighlight(c.id)); + el.addEventListener('mouseleave', () => applyHighlight()); + chipsEl.appendChild(el); + }); + + // ── La órbita de categorías lleva al panal y resalta su categoría ── + document.querySelectorAll('.proyecto-card').forEach((card) => { + card.addEventListener('click', (e) => { + e.preventDefault(); + const id = card.dataset.aptitudId; + if (!id) return; + setHighlight(hlCat === id ? null : id); + document.getElementById('proyectos-mapa')?.scrollIntoView({ + behavior: reduced ? 'auto' : 'smooth', + block: 'start', + }); + }); + }); + + build(); + + // ── Entrada: los iconos brotan al entrar en viewport ── + const io = new IntersectionObserver( + (entries) => { + if (!entries.some((e) => e.isIntersecting) || entered) return; + entered = true; + io.disconnect(); + if (reduced) { + nodes.forEach((d) => (d.k = 1)); + render(); + } else { + gsap.to(nodes, { + k: 1, + duration: 0.7, + ease: 'back.out(1.7)', + stagger: 0.035, + onUpdate: render, + }); + } + }, + { threshold: 0.1 } + ); + io.observe(wrap); + + // ── Cambio de layout (6 ↔ 3 columnas) al redimensionar ── + let resizeRaf = 0; + window.addEventListener('resize', () => { + if (resizeRaf) return; + resizeRaf = requestAnimationFrame(() => { + resizeRaf = 0; + if (isNarrow() !== narrow) build(); + }); + }); +} + +if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', initPanal); +} else { + initPanal(); +} diff --git a/src/styles/proyectos.css b/src/styles/proyectos.css index 85afb1e..d3420ba 100644 --- a/src/styles/proyectos.css +++ b/src/styles/proyectos.css @@ -328,15 +328,19 @@ display: block; } -/* Burbujas: squircles clicables */ -.bub { +/* Iconos del panal: squircles-enlace estilo springboard */ +.picon { cursor: pointer; outline: none; + transition: opacity 0.3s ease; } -.bub:focus-visible .bub-ring { +.picon.dim { + opacity: 0.16; +} + +.picon:focus-visible .picon-ring { stroke-width: 4; - opacity: 1; } /* Tooltip */ From f6fad2c19ce6ed34fd863af7b9e8d695ee7f6187 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Garc=C3=ADa=20Santamar=C3=ADa?= <69808296+PGSCOM@users.noreply.github.com> Date: Thu, 11 Jun 2026 12:48:23 +0200 Subject: [PATCH 34/49] =?UTF-8?q?Redise=C3=B1o=20secci=C3=B3n=20de=20proye?= =?UTF-8?q?ctos?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package-lock.json | 10 - package.json | 1 - src/pages/index.astro | 117 ++++---- src/scripts/proyectos-panal.js | 446 ------------------------------- src/scripts/proyectos-ruta.js | 159 +++++++++++ src/styles/proyectos.css | 475 ++++++++++++++++++++------------- 6 files changed, 518 insertions(+), 690 deletions(-) delete mode 100644 src/scripts/proyectos-panal.js create mode 100644 src/scripts/proyectos-ruta.js diff --git a/package-lock.json b/package-lock.json index b409b69..160188f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,6 @@ "@types/react": "^19.1.16", "@types/react-dom": "^19.1.9", "astro": "^5.15.1", - "d3-selection": "^3.0.0", "gsap": "^3.15.0", "liquid-glass-react": "^1.1.1", "react": "^19.1.1", @@ -2449,15 +2448,6 @@ "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", "license": "MIT" }, - "node_modules/d3-selection": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", - "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", diff --git a/package.json b/package.json index f23e3dd..5a76347 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,6 @@ "@types/react": "^19.1.16", "@types/react-dom": "^19.1.9", "astro": "^5.15.1", - "d3-selection": "^3.0.0", "gsap": "^3.15.0", "liquid-glass-react": "^1.1.1", "react": "^19.1.1", diff --git a/src/pages/index.astro b/src/pages/index.astro index 0e417e1..a7b8c03 100644 --- a/src/pages/index.astro +++ b/src/pages/index.astro @@ -44,27 +44,28 @@ const catColors = { 'fotografia': '#ffc94d', }; -// Datos planos para el mapa de burbujas (peso = envergadura → tamaño) -const burbujasData = { - cats: proyectosData.map((a) => ({ - id: a.id, - titulo: a.titulo, - color: catColors[a.id] ?? '#9bb4d0', - logo: a.logo, - })), - projects: proyectosData.flatMap((a) => - (a.proyectos ?? []).map((p) => ({ - id: p.id, - titulo: p.titulo.trim(), - cat: a.id, - peso: p.peso ?? 50, - imagen: p.imagen, - descripcion: p.descripcion ?? '', - tecnologias: p.tecnologias ?? [], - premio: p.premio ?? null, - })) - ), -}; +// Ruta de proyectos: tarjetas grandes a lo largo del scroll, una estación por +// categoría. peso → tamaño de tarjeta; lado e inset alternan para que la línea +// serpentee de forma orgánica. +let rutaIndice = 0; +const rutaData = proyectosData.map((a) => ({ + id: a.id, + titulo: a.titulo, + descripcion: a.descripcion, + logo: a.logo, + color: catColors[a.id] ?? '#9bb4d0', + items: (a.proyectos ?? []).map((p) => { + const i = rutaIndice++; + const peso = p.peso ?? 50; + return { + ...p, + num: String(i + 1).padStart(2, '0'), + lado: i % 2 === 0 ? 'izq' : 'der', + inset: ['0%', '7%', '3%'][i % 3], + w: peso >= 75 ? 580 : peso >= 55 ? 500 : 420, + }; + }), +})); --- @@ -166,7 +167,7 @@ const burbujasData = {

Proyectos

- Todos mis proyectos de un vistazo. Cada icono es un proyecto, su tamaño refleja la envergadura y el color indica la categoría. + Un recorrido por todos mis proyectos: baja siguiendo la línea y párate en los que te llamen. El tamaño de cada parada refleja la envergadura del proyecto.

@@ -175,7 +176,7 @@ const burbujasData = {
{proyectosData.map((aptitud) => ( - - - + diff --git a/src/scripts/proyectos-panal.js b/src/scripts/proyectos-panal.js deleted file mode 100644 index f1404fb..0000000 --- a/src/scripts/proyectos-panal.js +++ /dev/null @@ -1,446 +0,0 @@ -import { select } from 'd3-selection'; -import gsap from 'gsap'; - -// ── Panal de proyectos: springboard estilo Apple ────────────────────────────── -// Todos los proyectos visibles a la vez como iconos-squircle en una rejilla -// hexagonal, con su nombre debajo. El tamaño del icono viene de `peso`, el aro -// de color indica la categoría, y al mover el cursor una "lupa" tipo Apple -// Watch magnifica los iconos cercanos. Las chips de leyenda resaltan una -// categoría sin ocultar el resto. Escala solo: más proyectos → más filas. - -function initPanal() { - const dataEl = document.getElementById('burbujas-data'); - const wrap = document.getElementById('mapaWrap'); - const tip = document.getElementById('mapaTip'); - const chipsEl = document.getElementById('mapaPills'); - if (!dataEl || !wrap || !tip || !chipsEl) return; - - const { cats, projects } = JSON.parse(dataEl.textContent); - const catById = Object.fromEntries(cats.map((c) => [c.id, c])); - const reduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches; - const finePointer = window.matchMedia('(pointer: fine)').matches; - - const svg = select('#mapaChart'); - if (svg.empty()) return; - - let entered = false; // animación de entrada ya reproducida - let hlCat = null; // categoría resaltada de forma fija (chips / órbita) - let narrow = isNarrow(); - let nodes = []; - let icons = null; - let W = 0; - let H = 0; - let fisheyeOn = false; - let rafId = 0; - - function isNarrow() { - return (wrap.clientWidth || window.innerWidth) < 620; - } - - // Superellipse |x/r|^n + |y/r|^n = 1 → forma de icono de iOS - function squircle(r, n = 4.5) { - const STEPS = 64; - let d = ''; - for (let i = 0; i < STEPS; i++) { - const t = (i / STEPS) * Math.PI * 2; - const c = Math.cos(t); - const s = Math.sin(t); - const x = r * Math.sign(c) * Math.abs(c) ** (2 / n); - const y = r * Math.sign(s) * Math.abs(s) ** (2 / n); - d += (i === 0 ? 'M' : 'L') + x.toFixed(2) + ',' + y.toFixed(2); - } - return d + 'Z'; - } - - function wrapTitle(t, maxChars) { - const words = t.trim().split(/\s+/); - const lines = []; - let cur = ''; - for (const w of words) { - if (!cur) cur = w; - else if ((cur + ' ' + w).length <= maxChars) cur += ' ' + w; - else { lines.push(cur); cur = w; } - } - if (cur) lines.push(cur); - if (lines.length > 2) { - lines.length = 2; - lines[1] = lines[1].slice(0, Math.max(1, maxChars - 1)) + '…'; - } - return lines; - } - - function render() { - if (!icons) return; - icons.attr('transform', (d) => - `translate(${d.x + d.ox},${d.y + d.oy}) scale(${d.k * d.s})`); - } - - function build() { - narrow = isNarrow(); - W = narrow ? 400 : 1000; - const side = narrow ? 14 : 30; - const cols = narrow ? 3 : 6; - const dx = (W - side * 2) / cols; - const rMin = narrow ? 27 : 36; - const rMax = narrow ? 41 : 54; - const labelFs = narrow ? 11.5 : 13; - const dy = rMax * 2 + labelFs * 2 + (narrow ? 14 : 18); - - // ── Posiciones en panal: filas equilibradas (sin huérfanos) y centradas, - // con desplazamiento alterno para el aspecto hexagonal ── - const n = projects.length; - const numRows = Math.max(1, Math.ceil(n / cols)); - const base = Math.floor(n / numRows); - const extra = n % numRows; - const slots = []; - let row = 0; - for (let r = 0; r < numRows; r++) { - const inRow = base + (r < extra ? 1 : 0); - const rowW = inRow * dx; - const shift = narrow ? 0 : (r % 2 === 0 ? -1 : 1) * dx / 4; - const startX = (W - rowW) / 2 + dx / 2 + shift; - for (let c = 0; c < inRow; c++) { - slots.push([startX + c * dx, side + rMax + 6 + r * dy]); - } - row++; - } - H = side + row * dy + labelFs * 2; - - const keepK = entered || reduced ? 1 : 0; - nodes = projects.map((p, i) => ({ - ...p, - r: rMin + (rMax - rMin) * Math.sqrt((p.peso ?? 50) / 100), - x: slots[i][0], - y: slots[i][1], - k: keepK, // escala de entrada - s: 1, // escala de lupa (actual) - ox: 0, // desplazamiento de lupa (actual) - oy: 0, - ts: 1, // objetivos hacia los que se interpola - tox: 0, - toy: 0, - })); - - svg.selectAll('*').remove(); - svg.attr('viewBox', `0 0 ${W} ${H}`); - - // ── Defs: degradado de legibilidad + clip squircle por icono ── - const defs = svg.append('defs'); - const grad = defs.append('linearGradient') - .attr('id', 'bub-fade').attr('x1', 0).attr('y1', 0).attr('x2', 0).attr('y2', 1); - grad.append('stop').attr('offset', '0.45').attr('stop-color', 'rgba(2,8,23,0)'); - grad.append('stop').attr('offset', '1').attr('stop-color', 'rgba(2,8,23,0.55)'); - - nodes.forEach((d) => { - defs.append('clipPath').attr('id', `bclip-${d.id}`) - .append('path').attr('d', squircle(d.r)); - }); - - // ── Iconos: enlaces SVG reales ── - icons = svg.append('g').attr('class', 'mapa-icons') - .selectAll('a') - .data(nodes) - .enter() - .append('a') - .attr('class', 'picon') - .attr('href', (d) => `/proyectos/${d.id}`) - .attr('aria-label', (d) => `${d.titulo} — ${catById[d.cat]?.titulo ?? ''}`); - - icons.append('path') - .attr('d', (d) => squircle(d.r)) - .attr('fill', 'rgba(13,18,38,0.95)'); - - icons.append('image') - .attr('href', (d) => d.imagen) - .attr('x', (d) => -d.r).attr('y', (d) => -d.r) - .attr('width', (d) => d.r * 2).attr('height', (d) => d.r * 2) - .attr('preserveAspectRatio', 'xMidYMid slice') - .attr('clip-path', (d) => `url(#bclip-${d.id})`) - .each(function (d) { - // Si la imagen no existe: fondo tintado de la categoría + su icono - this.addEventListener('error', () => { - const g = select(this.parentNode); - const color = catById[d.cat]?.color ?? '#9bb4d0'; - this.remove(); - g.insert('path', '.picon-ring') - .attr('d', squircle(d.r)) - .attr('fill', color) - .attr('fill-opacity', 0.16); - const logo = catById[d.cat]?.logo; - if (logo) { - const s = d.r * 0.78; - g.insert('image', '.picon-ring') - .attr('href', logo) - .attr('x', -s / 2) - .attr('y', -s / 2) - .attr('width', s) - .attr('height', s) - .attr('opacity', 0.9) - .attr('pointer-events', 'none'); - } - }, { once: true }); - }); - - icons.append('path') - .attr('d', (d) => squircle(d.r)) - .attr('fill', 'url(#bub-fade)'); - - icons.append('path') - .attr('class', 'picon-ring') - .attr('d', (d) => squircle(d.r)) - .attr('fill', 'none') - .attr('stroke', (d) => catById[d.cat]?.color ?? '#9bb4d0') - .attr('stroke-width', 1.6); - - // Insignia de premio - icons.filter((d) => d.premio) - .append('text') - .attr('x', (d) => d.r * 0.72) - .attr('y', (d) => -d.r * 0.62) - .attr('text-anchor', 'middle') - .attr('font-size', (d) => Math.max(13, d.r * 0.34)) - .attr('pointer-events', 'none') - .text('🏆'); - - // Nombre bajo el icono, como en una pantalla de inicio - icons.each(function (d) { - const g = select(this); - const maxChars = Math.max(10, Math.floor((narrow ? 15 : 17) * (d.r / rMax + 0.35))); - const lines = wrapTitle(d.titulo, maxChars); - lines.forEach((line, i) => { - g.append('text') - .attr('class', 'picon-label') - .attr('text-anchor', 'middle') - .attr('x', 0) - .attr('y', d.r + labelFs + 4 + i * (labelFs + 2)) - .attr('font-family', "'Chakra Petch', sans-serif") - .attr('font-size', labelFs) - .attr('font-weight', 500) - .attr('fill', 'rgba(255,255,255,0.88)') - .attr('stroke', 'rgba(0,8,20,0.6)') - .attr('stroke-width', 2.5) - .attr('paint-order', 'stroke') - .attr('pointer-events', 'none') - .text(line); - }); - }); - - // ── Tooltip y resaltado por icono ── - icons - .on('click', (event, d) => { - // Marca la imagen para el morph de view-transition hacia la ficha - const img = event.currentTarget.querySelector('image'); - if (img) img.style.viewTransitionName = 'project-hero'; - }) - .on('mouseenter', function (event, d) { - select(this).raise(); - select(this).select('.picon-ring').attr('stroke-width', 3); - showTip(d); - moveTip(event); - }) - .on('mousemove', (event) => moveTip(event)) - .on('mouseleave', function () { - select(this).select('.picon-ring').attr('stroke-width', 1.6); - tip.style.display = 'none'; - }) - .on('focus', function (event, d) { - showTip(d); - const r = wrap.getBoundingClientRect(); - const b = this.getBoundingClientRect(); - positionTip(b.right - r.left + 10, b.top - r.top); - }) - .on('blur', () => { tip.style.display = 'none'; }); - - applyHighlight(); - render(); - - // ── Lupa tipo Apple Watch ── - fisheyeOn = finePointer && !reduced && !narrow; - if (fisheyeOn) { - const sigma = dx * 0.85; - const AMP = 0.5; - const PUSH = dx * 0.16; - - const els = icons.nodes(); - let raised = null; - const setTargets = (mx, my) => { - let nearest = -1; - let nearestDist = Infinity; - nodes.forEach((d, i) => { - const ddx = d.x - mx; - const ddy = d.y - my; - const dist = Math.hypot(ddx, ddy) || 1; - const f = Math.exp(-(dist * dist) / (2 * sigma * sigma)); - d.ts = 1 + AMP * f; - d.tox = (ddx / dist) * PUSH * f; - d.toy = (ddy / dist) * PUSH * f; - if (dist < nearestDist) { nearestDist = dist; nearest = i; } - }); - // El icono más cercano al cursor pasa al frente para que su - // nombre no quede tapado por la fila siguiente - if (nearest >= 0 && els[nearest] !== raised) { - raised = els[nearest]; - raised.parentNode.appendChild(raised); - } - startLoop(); - }; - - const clearTargets = () => { - for (const d of nodes) { d.ts = 1; d.tox = 0; d.toy = 0; } - startLoop(); - }; - - svg.node().addEventListener('pointermove', (e) => { - const rect = svg.node().getBoundingClientRect(); - setTargets( - ((e.clientX - rect.left) / rect.width) * W, - ((e.clientY - rect.top) / rect.height) * H - ); - }); - svg.node().addEventListener('pointerleave', clearTargets); - } - } - - // Interpola suavemente escala/desplazamiento hacia sus objetivos - function startLoop() { - if (rafId) return; - const step = () => { - let busy = false; - for (const d of nodes) { - d.s += (d.ts - d.s) * 0.22; - d.ox += (d.tox - d.ox) * 0.22; - d.oy += (d.toy - d.oy) * 0.22; - if (Math.abs(d.ts - d.s) > 0.002 || Math.abs(d.tox - d.ox) > 0.1 || Math.abs(d.toy - d.oy) > 0.1) { - busy = true; - } - } - render(); - rafId = busy ? requestAnimationFrame(step) : 0; - }; - rafId = requestAnimationFrame(step); - } - - // ── Tooltip ── - function showTip(d) { - const cat = catById[d.cat]; - const techs = (d.tecnologias || []) - .map((t) => `${t}`) - .join(''); - tip.innerHTML = ` -
${d.titulo}
-
${cat?.titulo ?? ''}
- ${d.premio ? `
🏆 ${d.premio}
` : ''} - ${d.descripcion ? `
${d.descripcion}
` : ''} -
- Envergadura - -
- ${techs ? `
${techs}
` : ''} - `; - tip.style.display = 'block'; - } - - function positionTip(x, y) { - const r = wrap.getBoundingClientRect(); - const tw = tip.offsetWidth || 220; - tip.style.left = (x + tw < r.width ? x : Math.max(0, x - tw - 28)) + 'px'; - tip.style.top = Math.max(0, Math.min(y, r.height - tip.offsetHeight - 8)) + 'px'; - } - - function moveTip(event) { - const r = wrap.getBoundingClientRect(); - positionTip(event.clientX - r.left + 16, event.clientY - r.top - 12); - } - - // ── Resaltado por categoría (no oculta nada, solo atenúa el resto) ── - function applyHighlight(previewCat) { - const cat = previewCat !== undefined ? previewCat : hlCat; - if (!icons) return; - icons.classed('dim', (d) => cat !== null && d.cat !== cat); - chipsEl.querySelectorAll('.mapa-pill').forEach((p) => { - p.classList.toggle('act', p.dataset.cat === hlCat); - p.setAttribute('aria-pressed', p.dataset.cat === hlCat ? 'true' : 'false'); - }); - document.querySelectorAll('.proyecto-card').forEach((card) => { - card.classList.toggle('is-active', card.dataset.aptitudId === hlCat); - }); - } - - function setHighlight(cat) { - hlCat = cat; - applyHighlight(); - } - - // ── Chips de leyenda ── - cats.forEach((c) => { - const el = document.createElement('button'); - el.type = 'button'; - el.className = 'mapa-pill'; - el.dataset.cat = c.id; - el.setAttribute('aria-pressed', 'false'); - const dot = document.createElement('span'); - dot.className = 'mapa-pill-dot'; - dot.style.background = c.color; - el.appendChild(dot); - el.appendChild(document.createTextNode(c.titulo)); - el.addEventListener('click', () => setHighlight(hlCat === c.id ? null : c.id)); - el.addEventListener('mouseenter', () => applyHighlight(c.id)); - el.addEventListener('mouseleave', () => applyHighlight()); - chipsEl.appendChild(el); - }); - - // ── La órbita de categorías lleva al panal y resalta su categoría ── - document.querySelectorAll('.proyecto-card').forEach((card) => { - card.addEventListener('click', (e) => { - e.preventDefault(); - const id = card.dataset.aptitudId; - if (!id) return; - setHighlight(hlCat === id ? null : id); - document.getElementById('proyectos-mapa')?.scrollIntoView({ - behavior: reduced ? 'auto' : 'smooth', - block: 'start', - }); - }); - }); - - build(); - - // ── Entrada: los iconos brotan al entrar en viewport ── - const io = new IntersectionObserver( - (entries) => { - if (!entries.some((e) => e.isIntersecting) || entered) return; - entered = true; - io.disconnect(); - if (reduced) { - nodes.forEach((d) => (d.k = 1)); - render(); - } else { - gsap.to(nodes, { - k: 1, - duration: 0.7, - ease: 'back.out(1.7)', - stagger: 0.035, - onUpdate: render, - }); - } - }, - { threshold: 0.1 } - ); - io.observe(wrap); - - // ── Cambio de layout (6 ↔ 3 columnas) al redimensionar ── - let resizeRaf = 0; - window.addEventListener('resize', () => { - if (resizeRaf) return; - resizeRaf = requestAnimationFrame(() => { - resizeRaf = 0; - if (isNarrow() !== narrow) build(); - }); - }); -} - -if (document.readyState === 'loading') { - document.addEventListener('DOMContentLoaded', initPanal); -} else { - initPanal(); -} diff --git a/src/scripts/proyectos-ruta.js b/src/scripts/proyectos-ruta.js new file mode 100644 index 0000000..5293b09 --- /dev/null +++ b/src/scripts/proyectos-ruta.js @@ -0,0 +1,159 @@ +import gsap from 'gsap'; +import { ScrollTrigger } from 'gsap/ScrollTrigger'; +gsap.registerPlugin(ScrollTrigger); + +// ── Ruta de proyectos ───────────────────────────────────────────────────────── +// Una línea luminosa serpentea por la sección pasando por cada estación de +// categoría y cada tarjeta de proyecto, y se va trazando a medida que se hace +// scroll. Las tarjetas (HTML estático) entran animadas al llegar a ellas. + +function initRuta() { + const cont = document.getElementById('proyectos-mapa'); + const svg = cont?.querySelector('.ruta-linea'); + if (!cont || !svg) return; + + const reduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches; + const NS = 'http://www.w3.org/2000/svg'; + const glow = document.createElementNS(NS, 'path'); + const trazo = document.createElementNS(NS, 'path'); + glow.setAttribute('class', 'ruta-linea-glow'); + trazo.setAttribute('class', 'ruta-linea-trazo'); + svg.append(glow, trazo); + + let drawTween = null; + + // Puntos de paso: el dot de cada estación y el centro de cada tarjeta. + // En pantallas estrechas la línea baja recta por el margen izquierdo. + function puntos() { + const base = cont.getBoundingClientRect(); + const narrow = base.width < 760; + const els = cont.querySelectorAll('.ruta-estacion-dot, .ruta-item'); + return [...els].map((el) => { + const b = el.getBoundingClientRect(); + return { + x: narrow ? 18 : b.left - base.left + b.width / 2, + y: b.top - base.top + b.height / 2, + }; + }); + } + + function build() { + const w = cont.clientWidth; + const h = cont.scrollHeight; + svg.setAttribute('width', w); + svg.setAttribute('height', h); + svg.setAttribute('viewBox', `0 0 ${w} ${h}`); + + const ps = puntos(); + if (ps.length < 2) return; + + // Curvas con tangente vertical en cada punto → la línea "cae" en S + let d = `M ${ps[0].x.toFixed(1)} ${ps[0].y.toFixed(1)}`; + for (let i = 1; i < ps.length; i++) { + const a = ps[i - 1]; + const b = ps[i]; + const k = (b.y - a.y) * 0.5; + d += ` C ${a.x.toFixed(1)} ${(a.y + k).toFixed(1)}, ${b.x.toFixed(1)} ${(b.y - k).toFixed(1)}, ${b.x.toFixed(1)} ${b.y.toFixed(1)}`; + } + glow.setAttribute('d', d); + trazo.setAttribute('d', d); + + const L = trazo.getTotalLength(); + drawTween?.scrollTrigger?.kill(); + drawTween?.kill(); + + if (reduced) { + gsap.set([glow, trazo], { strokeDasharray: 'none', strokeDashoffset: 0 }); + return; + } + gsap.set([glow, trazo], { strokeDasharray: L, strokeDashoffset: L }); + drawTween = gsap.to([glow, trazo], { + strokeDashoffset: 0, + ease: 'none', + scrollTrigger: { + trigger: cont, + start: 'top 65%', + end: 'bottom 85%', + scrub: 0.5, + }, + }); + } + + // ── Entradas al hacer scroll ── + if (!reduced) { + gsap.utils.toArray('.ruta-estacion', cont).forEach((el) => { + gsap.from(el, { + autoAlpha: 0, + y: 30, + duration: 0.7, + ease: 'power3.out', + scrollTrigger: { trigger: el, start: 'top 88%' }, + }); + }); + gsap.utils.toArray('.ruta-item', cont).forEach((el, i) => { + gsap.from(el, { + autoAlpha: 0, + y: 56, + rotation: i % 2 === 0 ? -1.4 : 1.4, + duration: 0.9, + ease: 'power3.out', + scrollTrigger: { trigger: el, start: 'top 88%' }, + }); + }); + } + + // La estación se "enciende" cuando la ruta pasa por ella + gsap.utils.toArray('.ruta-estacion', cont).forEach((el) => { + ScrollTrigger.create({ + trigger: el, + start: 'top 75%', + onEnter: () => el.classList.add('on'), + onLeaveBack: () => el.classList.remove('on'), + }); + }); + + // ── Imágenes que no existen: fondo tintado + icono de la categoría ── + cont.querySelectorAll('.ruta-img').forEach((img) => { + const fallar = () => img.closest('.ruta-item')?.classList.add('sin-imagen'); + if (img.complete && img.naturalWidth === 0) fallar(); + else img.addEventListener('error', fallar, { once: true }); + }); + + // ── La órbita de categorías navega a su estación con scroll suave ── + document.querySelectorAll('.proyecto-card').forEach((card) => { + card.addEventListener('click', (e) => { + const id = card.dataset.aptitudId; + const target = id && document.getElementById(`ruta-${id}`); + if (!target) return; + e.preventDefault(); + target.scrollIntoView({ behavior: reduced ? 'auto' : 'smooth', block: 'start' }); + }); + }); + + build(); + // Las imágenes tienen aspect-ratio fijo, pero las fuentes pueden mover el + // layout: se recalcula la línea al cargar todo y al redimensionar. + window.addEventListener('load', () => { + ScrollTrigger.refresh(); + build(); + }); + + let resizeRaf = 0; + let lastW = cont.clientWidth; + window.addEventListener('resize', () => { + if (resizeRaf) return; + resizeRaf = requestAnimationFrame(() => { + resizeRaf = 0; + if (cont.clientWidth !== lastW) { + lastW = cont.clientWidth; + build(); + } + }); + }); +} + +if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', initRuta); +} else { + initRuta(); +} diff --git a/src/styles/proyectos.css b/src/styles/proyectos.css index d3420ba..03d9cf8 100644 --- a/src/styles/proyectos.css +++ b/src/styles/proyectos.css @@ -1,4 +1,4 @@ -/* Estilos para la sección de proyectos - Órbita de categorías + mapa de burbujas */ +/* Estilos para la sección de proyectos - Órbita de categorías + ruta de proyectos */ /* ============================================ ANIMACIONES PERSONALIZADAS @@ -57,7 +57,7 @@ /* ============================================ HERO DECORATIVO - Órbita de categorías - (las tarjetas en órbita filtran el mapa) + (cada tarjeta en órbita navega a su estación) ============================================ */ .proyectos-hero { @@ -180,25 +180,13 @@ 0 25px 50px rgba(0, 0, 0, 0.35), 0 0 40px rgba(147, 197, 253, 0.3), 0 0 60px rgba(147, 197, 253, 0.15); - border-color: rgba(147, 197, 253, 0.5); + border-color: var(--cat-color, rgba(147, 197, 253, 0.5)); background: linear-gradient(145deg, rgba(255, 255, 255, 0.2), rgba(147, 197, 253, 0.12), rgba(255, 255, 255, 0.08)); } -/* Categoría activa como filtro del mapa: aro en su color */ -.proyecto-card.is-active { - filter: brightness(1.15) saturate(1.2); -} - -.proyecto-card.is-active .proyecto-card-inner { - border-color: var(--cat-color, rgba(147, 197, 253, 0.8)); - box-shadow: - 0 8px 24px rgba(0, 0, 0, 0.35), - 0 0 26px color-mix(in srgb, var(--cat-color, #93c5fd) 55%, transparent); -} - .proyecto-icon { width: 100%; height: auto; @@ -247,199 +235,294 @@ } /* ============================================ - MAPA DE BURBUJAS - Proyectos con física + RUTA DE PROYECTOS - Línea + paradas con scroll ============================================ */ .proyectos-mapa { - max-width: 1160px; + position: relative; + max-width: 1200px; margin: 0 auto; - padding: 0 24px 8vh; + padding: 4vh 24px 12vh; + scroll-margin-top: 8vh; } -/* Pills de filtro por categoría */ -.mapa-pills { - display: flex; - flex-wrap: wrap; - justify-content: center; - gap: 10px; - margin-bottom: 22px; +/* La línea que serpentea (la dibuja el JS) */ +.ruta-linea { + position: absolute; + top: 0; + left: 0; + pointer-events: none; + z-index: 0; + overflow: visible; } -.mapa-pill { - display: inline-flex; - align-items: center; - gap: 8px; - padding: 8px 18px; - border-radius: 999px; - font-family: 'Chakra Petch', sans-serif; - font-size: 0.85rem; - font-weight: 500; - color: rgba(255, 255, 255, 0.8); - background: rgba(30, 35, 55, 0.6); - border: 1px solid rgba(255, 255, 255, 0.15); - backdrop-filter: blur(10px); - -webkit-backdrop-filter: blur(10px); - cursor: pointer; - transition: all 0.3s ease; +.ruta-linea-glow { + fill: none; + stroke: rgba(147, 197, 253, 0.45); + stroke-width: 7; + stroke-linecap: round; + filter: blur(7px); } -.mapa-pill:hover { - border-color: rgba(147, 197, 253, 0.5); - color: white; - transform: translateY(-2px); +.ruta-linea-trazo { + fill: none; + stroke: rgba(196, 218, 255, 0.85); + stroke-width: 2; + stroke-linecap: round; } -.mapa-pill.act { - background: white; - color: #0a0e1c; - border-color: white; - box-shadow: 0 4px 20px rgba(255, 255, 255, 0.2); +/* Estación de categoría */ +.ruta-estacion { + position: relative; + z-index: 1; + display: flex; + flex-direction: column; + align-items: center; + gap: 8px; + text-align: center; + max-width: 560px; + margin: 9vh auto 8vh; + scroll-margin-top: 14vh; } -.mapa-pill-dot { - width: 9px; - height: 9px; +.ruta-estacion-dot { + width: 20px; + height: 20px; border-radius: 50%; - flex-shrink: 0; -} - -/* Panel de cristal que contiene el chart */ -.mapa-panel { - background: linear-gradient(135deg, - rgba(30, 35, 55, 0.55), - rgba(12, 16, 32, 0.7)); - backdrop-filter: blur(16px); - -webkit-backdrop-filter: blur(16px); - border: 1px solid rgba(255, 255, 255, 0.12); - border-radius: 28px; - padding: 14px; + background: var(--cat-color, #93c5fd); + box-shadow: 0 0 0 5px color-mix(in srgb, var(--cat-color, #93c5fd) 22%, transparent); + transition: transform 0.4s cubic-bezier(0.34, 1.56, 0.64, 1), box-shadow 0.4s ease; +} + +.ruta-estacion.on .ruta-estacion-dot { + transform: scale(1.3); box-shadow: - 0 12px 48px rgba(0, 0, 0, 0.35), - inset 0 1px 0 rgba(255, 255, 255, 0.08); + 0 0 0 6px color-mix(in srgb, var(--cat-color, #93c5fd) 28%, transparent), + 0 0 28px var(--cat-color, #93c5fd); } -.mapa-wrap { - position: relative; +.ruta-estacion-titulo { + font-family: 'Chakra Petch', sans-serif; + font-size: clamp(1.25rem, 2.4vw, 1.6rem); + font-weight: 700; + color: white; + margin: 0; + text-shadow: 0 2px 8px rgba(0, 0, 0, 0.4); } -#mapaChart { - width: 100%; - height: auto; - display: block; +.ruta-estacion-desc { + font-family: 'Chakra Petch', sans-serif; + font-size: 0.9rem; + color: rgba(255, 255, 255, 0.6); + margin: 0; + line-height: 1.5; } -/* Iconos del panal: squircles-enlace estilo springboard */ -.picon { - cursor: pointer; +/* Parada de proyecto: imagen grande protagonista */ +.ruta-item { + position: relative; + z-index: 1; + display: block; + width: min(46%, var(--w, 500px)); + margin-bottom: 10vh; + text-decoration: none; outline: none; - transition: opacity 0.3s ease; } -.picon.dim { - opacity: 0.16; +.ruta-item--izq { + margin-right: auto; + margin-left: var(--inset, 0%); } -.picon:focus-visible .picon-ring { - stroke-width: 4; +.ruta-item--der { + margin-left: auto; + margin-right: var(--inset, 0%); } -/* Tooltip */ -.mapa-tip { +/* Número de parada, como un hito del recorrido */ +.ruta-num { position: absolute; - display: none; - pointer-events: none; - z-index: 20; - width: 236px; - padding: 14px 16px; - border-radius: 14px; - background: rgba(10, 14, 28, 0.92); - backdrop-filter: blur(12px); - -webkit-backdrop-filter: blur(12px); - border: 1px solid rgba(255, 255, 255, 0.15); - box-shadow: 0 14px 40px rgba(0, 0, 0, 0.55); + top: -0.52em; + z-index: 2; font-family: 'Chakra Petch', sans-serif; + font-weight: 700; + font-size: clamp(2.4rem, 4.5vw, 3.6rem); + line-height: 1; + color: transparent; + -webkit-text-stroke: 1.5px color-mix(in srgb, var(--cat-color, #93c5fd) 75%, white 15%); + opacity: 0.85; + pointer-events: none; } -.mapa-tip-titulo { - font-size: 0.95rem; - font-weight: 700; - color: white; - margin-bottom: 2px; - line-height: 1.25; +.ruta-item--izq .ruta-num { right: 14px; } +.ruta-item--der .ruta-num { left: 14px; } + +.ruta-media { + position: relative; + aspect-ratio: 16 / 10; + border-radius: 26px; + overflow: hidden; + border: 1px solid rgba(255, 255, 255, 0.14); + background: linear-gradient(135deg, rgba(20, 26, 48, 0.9), rgba(8, 12, 26, 0.95)); + box-shadow: 0 16px 44px rgba(0, 0, 0, 0.45); + transition: + transform 0.45s cubic-bezier(0.34, 1.56, 0.64, 1), + border-color 0.35s ease, + box-shadow 0.45s ease; + will-change: transform; } -.mapa-tip-cat { - font-size: 0.72rem; - font-weight: 600; - letter-spacing: 0.06em; - text-transform: uppercase; - margin-bottom: 8px; +.ruta-item:hover .ruta-media, +.ruta-item:focus-visible .ruta-media { + transform: translateY(-8px); + border-color: color-mix(in srgb, var(--cat-color, #93c5fd) 75%, transparent); + box-shadow: + 0 26px 64px rgba(0, 0, 0, 0.55), + 0 0 38px color-mix(in srgb, var(--cat-color, #93c5fd) 30%, transparent); } -.mapa-tip-premio { - font-size: 0.78rem; - color: #ffd97a; - margin-bottom: 6px; +.ruta-item:focus-visible .ruta-media { + outline: 2px solid var(--cat-color, #93c5fd); + outline-offset: 3px; } -.mapa-tip-desc { - font-size: 0.8rem; - color: rgba(255, 255, 255, 0.78); - line-height: 1.45; - margin-bottom: 10px; +.ruta-img { + position: absolute; + inset: 0; + width: 100%; + height: 100%; + object-fit: cover; + transition: transform 0.6s cubic-bezier(0.34, 1.56, 0.64, 1); +} + +.ruta-item:hover .ruta-img { + transform: scale(1.05); +} + +/* Fallback cuando la imagen del proyecto no existe */ +.ruta-fallback { + display: none; +} + +.ruta-item.sin-imagen .ruta-img { + display: none; +} + +.ruta-item.sin-imagen .ruta-media { + background: + radial-gradient(ellipse at 50% 35%, color-mix(in srgb, var(--cat-color, #93c5fd) 22%, transparent), transparent 70%), + linear-gradient(135deg, rgba(20, 26, 48, 0.95), rgba(8, 12, 26, 0.98)); +} + +.ruta-item.sin-imagen .ruta-fallback { + display: block; + position: absolute; + left: 50%; + top: 38%; + transform: translate(-50%, -50%); + width: 22%; + opacity: 0.9; + filter: drop-shadow(0 4px 16px rgba(0, 0, 0, 0.5)); } -.mapa-tip-peso { +/* Insignia de premio */ +.ruta-premio { + position: absolute; + top: 14px; + left: 14px; + z-index: 2; + padding: 6px 12px; + border-radius: 999px; + background: rgba(8, 10, 22, 0.72); + backdrop-filter: blur(8px); + -webkit-backdrop-filter: blur(8px); + border: 1px solid rgba(255, 217, 122, 0.45); + color: #ffd97a; + font-family: 'Chakra Petch', sans-serif; + font-size: 0.72rem; + font-weight: 600; +} + +/* Info sobre la imagen: título siempre, detalle al pasar el cursor */ +.ruta-info { + position: absolute; + left: 0; + right: 0; + bottom: 0; + z-index: 1; display: flex; - align-items: center; - gap: 8px; + flex-direction: column; + gap: 5px; + padding: 56px 20px 16px; + background: linear-gradient(180deg, + transparent 0%, + rgba(2, 8, 23, 0.5) 40%, + rgba(2, 8, 23, 0.92) 100%); +} + +.ruta-cat { + font-family: 'Chakra Petch', sans-serif; font-size: 0.68rem; - color: rgba(255, 255, 255, 0.55); + font-weight: 600; + letter-spacing: 0.12em; text-transform: uppercase; - letter-spacing: 0.05em; - margin-bottom: 10px; + color: var(--cat-color, #93c5fd); } -.mapa-tip-barra { - flex: 1; - height: 4px; - border-radius: 2px; - background: rgba(255, 255, 255, 0.12); - overflow: hidden; +.ruta-titulo { + font-family: 'Chakra Petch', sans-serif; + font-size: clamp(1.05rem, 1.7vw, 1.4rem); + font-weight: 700; + color: white; + margin: 0; + line-height: 1.2; + text-shadow: 0 2px 8px rgba(0, 0, 0, 0.6); } -.mapa-tip-barra i { - display: block; - height: 100%; - border-radius: 2px; +.ruta-desc { + font-family: 'Chakra Petch', sans-serif; + font-size: 0.85rem; + color: rgba(255, 255, 255, 0.82); + line-height: 1.45; + margin: 0; + max-height: 0; + opacity: 0; + overflow: hidden; + transition: max-height 0.45s ease, opacity 0.35s ease; } -.mapa-tip-techs { +.ruta-techs { display: flex; flex-wrap: wrap; - gap: 4px; + gap: 5px; + max-height: 0; + opacity: 0; + overflow: hidden; + transition: max-height 0.45s ease, opacity 0.35s ease; +} + +.ruta-item:hover .ruta-desc, +.ruta-item:focus-visible .ruta-desc { + max-height: 5.5em; + opacity: 1; +} + +.ruta-item:hover .ruta-techs, +.ruta-item:focus-visible .ruta-techs { + max-height: 4em; + opacity: 1; } -.mapa-tip-tech { +.ruta-tech { + font-family: 'Chakra Petch', sans-serif; font-size: 0.68rem; - padding: 2px 8px; + padding: 3px 9px; border-radius: 6px; border: 1px solid rgba(147, 197, 253, 0.25); - background: rgba(147, 197, 253, 0.08); + background: rgba(8, 12, 26, 0.55); color: #bfdbfe; } -/* Leyenda bajo el mapa */ -.mapa-leyenda { - font-family: 'Chakra Petch', sans-serif; - font-size: 0.75rem; - color: rgba(255, 255, 255, 0.45); - text-align: center; - margin: 14px auto 0; - max-width: 600px; - line-height: 1.5; -} - /* ============================================ RESPONSIVE ============================================ */ @@ -500,6 +583,62 @@ width: 38px; height: 38px; } + + .ruta-item { + width: min(48%, var(--w, 500px)); + } +} + +@media (max-width: 760px) { + /* La ruta pasa a línea de tiempo: línea recta a la izquierda */ + .proyectos-mapa { + padding: 3vh 16px 8vh 46px; + } + + .ruta-item, + .ruta-item--izq, + .ruta-item--der { + width: 100%; + margin-left: 0; + margin-right: 0; + margin-bottom: 7vh; + } + + .ruta-estacion { + align-items: flex-start; + text-align: left; + margin: 7vh 0 5vh; + } + + .ruta-estacion-dot { + position: absolute; + left: -38px; + top: 4px; + width: 18px; + height: 18px; + } + + .ruta-num { + font-size: 2rem; + top: -0.5em; + } + + .ruta-item--izq .ruta-num, + .ruta-item--der .ruta-num { + left: auto; + right: 12px; + } + + /* Sin hover en táctil: la descripción y las techs se muestran siempre */ + .ruta-desc, + .ruta-techs { + max-height: none; + opacity: 1; + } + + .ruta-info { + padding: 44px 14px 13px; + } } @media (max-width: 768px) { @@ -541,25 +680,6 @@ font-size: 0.6rem; max-width: 70px; } - - .proyectos-mapa { - padding: 0 14px 6vh; - } - - .mapa-panel { - border-radius: 20px; - padding: 10px; - } - - .mapa-pill { - padding: 7px 14px; - font-size: 0.78rem; - } - - .mapa-tip { - width: 200px; - padding: 12px 14px; - } } @media (max-width: 480px) { @@ -601,25 +721,18 @@ } .proyectos-mapa { - padding: 0 10px 5vh; - } - - .mapa-pills { - gap: 7px; - margin-bottom: 16px; + padding: 2vh 12px 7vh 40px; } - .mapa-pill { - padding: 6px 12px; - font-size: 0.72rem; + .ruta-estacion-dot { + left: -32px; } - .mapa-panel { - border-radius: 16px; - padding: 8px; + .ruta-titulo { + font-size: 1rem; } - .mapa-leyenda { - font-size: 0.68rem; + .ruta-desc { + font-size: 0.8rem; } } From 7136aa5b3d70ac64a3dd5cd864ee16f3e792a43e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Garc=C3=ADa=20Santamar=C3=ADa?= <69808296+PGSCOM@users.noreply.github.com> Date: Thu, 11 Jun 2026 12:56:01 +0200 Subject: [PATCH 35/49] Fix texto al cambiar mueve toda la landing page --- src/pages/_styles.css | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/pages/_styles.css b/src/pages/_styles.css index 81d6b8e..f24cccc 100644 --- a/src/pages/_styles.css +++ b/src/pages/_styles.css @@ -306,6 +306,7 @@ justify-content: center; align-items: center; will-change: transform, opacity; + min-height: 13vw; } .logo-text { @@ -320,7 +321,6 @@ align-items: center; font-family: 'Montserrat', sans-serif; width: 100%; - min-height: 1.5em; padding: 0 10px; box-sizing: border-box; animation: none !important; @@ -508,6 +508,10 @@ letter-spacing: 0.2rem; } + #logo-wrapper { + min-height: 26vw; + } + .hero-card { width: clamp(105px, 26vw, 150px); } From 3592d4d00239f510ebf54b479296d56df9844f1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Garc=C3=ADa=20Santamar=C3=ADa?= <69808296+PGSCOM@users.noreply.github.com> Date: Thu, 11 Jun 2026 13:15:10 +0200 Subject: [PATCH 36/49] =?UTF-8?q?A=C3=B1adido=20filtrado=20de=20competenci?= =?UTF-8?q?as=20al=20portfolio?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/data/proyectos.json | 484 ++++++++++++++++++--------------- src/pages/index.astro | 207 +++++++++----- src/pages/proyectos/[id].astro | 39 ++- src/pages/sitemap.xml.ts | 6 +- src/scripts/proyectos-ruta.js | 111 +++++--- src/styles/proyectos.css | 444 +++++++++++++++++++++++------- 6 files changed, 861 insertions(+), 430 deletions(-) diff --git a/src/data/proyectos.json b/src/data/proyectos.json index 492fc4f..6590af2 100644 --- a/src/data/proyectos.json +++ b/src/data/proyectos.json @@ -1,218 +1,266 @@ -[ - { - "id": "desarrollo-web", - "titulo": "Desarrollo Web", - "logo": "/icons/web.svg", - "descripcion": "Desarrollo de sitios y aplicaciones web con tecnologías modernas", - "proyectos": [ - { - "id": "portfolio-pgscom", - "titulo": "Portfolio PGSCOM", - "peso": 90, - "imagen": "/assets/proyectos/portfolio.png", - "descripcion": "Sitio web personal desarrollado con Astro, React y animaciones con AnimeJS.", - "tecnologias": ["Astro", "React", "TypeScript", "AnimeJS", "CSS3"], - "link": "https://pgscom.es", - "contenido": "

Portfolio Personal PGSCOM

Este proyecto representa mi carta de presentación digital, desarrollado completamente desde cero con las tecnologías más modernas.

Características principales:

  • Astro 4.0 - Framework ultra-rápido
  • React - Componentes interactivos
  • AnimeJS - Animaciones fluidas
  • Diseño Responsivo
" - }, - { - "id": "blog-personal", - "titulo": "Blog Personal", - "peso": 45, - "imagen": "/assets/proyectos/Blog.png", - "descripcion": "Blog técnico sobre audiovisual, tecnología y programación.", - "tecnologias": ["HTML", "CSS", "JavaScript"], - "link": "https://blog.pgscom.es", - "contenido": "

Blog Técnico Personal

Espacio dedicado a compartir conocimientos sobre producción audiovisual y programación.

Temáticas:

  • Tutoriales de edición
  • Guías de programación
  • Consejos de diseño
" - }, - { - "id": "portafolio-cine", - "titulo": "Portafolio Cinematográfico", - "peso": 60, - "imagen": "/assets/proyectos/PortfolioCortos.png", - "descripcion": "Galería de proyectos audiovisuales con reproductor de video integrado.", - "tecnologias": ["HTML5", "CSS3", "JavaScript", "Video.js"], - "link": "https://cine.pgscom.es", - "contenido": "

Portafolio Cinematográfico

Galería especializada para mostrar mis proyectos audiovisuales.

Funcionalidades:

  • Reproductor personalizado
  • Sistema de categorías
  • Galería de stills
" - } - ] - }, - { - "id": "video-produccion", - "titulo": "Producción Audiovisual", - "logo": "/icons/video.svg", - "descripcion": "Cortometrajes, documentales y proyectos cinematográficos", - "proyectos": [ - { - "id": "premio-macula-2024", - "titulo": "Premio Mácula 2024 ", - "peso": 80, - "imagen": "/assets/proyectos/Macula.png", - "descripcion": "Segundo Premio Mácula 2024 - Cortometraje documental galardonado.", - "tecnologias": ["DaVinci Resolve", "Premiere Pro", "After Effects"], - "premio": "2º Premio Mácula 2024", - "contenido": "

Segundo Premio Mácula 2024

Cortometraje documental galardonado con el Segundo Premio.

Ficha técnica:

  • Formato: Documental
  • Duración: 12 minutos
  • Postproducción: DaVinci Resolve
\"Trabajo excepcional\" - Jurado Festival Mácula
" - }, - { - "id": "premio-macula-2023", - "titulo": "Premio Mácula 2023 ", - "peso": 70, - "imagen": "/assets/proyectos/Macula.png", - "descripcion": "Segundo Premio Mácula 2023 - Proyecto audiovisual experimental.", - "tecnologias": ["DaVinci Resolve", "Premiere Pro"], - "premio": "2º Premio Mácula 2023", - "contenido": "

Segundo Premio Mácula 2023

Proyecto audiovisual experimental con nuevas formas narrativas.

Características:

  • Montaje experimental
  • Paleta expresionista
  • Banda sonora original
" - }, - { - "id": "cortometrajes", - "titulo": "Cortometrajes Varios", - "peso": 60, - "imagen": "/assets/proyectos/PortfolioCortos.png", - "descripcion": "Colección de cortometrajes de ficción y documental.", - "tecnologias": ["DaVinci Resolve", "After Effects", "Audition"], - "contenido": "

Colección de Cortometrajes

Recopilación de proyectos audiovisuales en ficción y documental.

Roles:

  • Dirección de fotografía
  • Montaje
  • Corrección de color
" - } - ] - }, - { - "id": "animacion-3d", - "titulo": "Animación 3D", - "logo": "/icons/animation.svg", - "descripcion": "Modelado, animación y renders con Blender y Unreal Engine", - "proyectos": [ - { - "id": "logo-animado-pgscom", - "titulo": "Logo Animado PGSCOM", - "peso": 50, - "imagen": "/assets/proyectos/logo-anim.jpg", - "descripcion": "Animación 3D del logo personal con partículas y efectos visuales.", - "tecnologias": ["Blender", "Cycles", "After Effects"], - "contenido": "

Logo Animado PGSCOM

Animación 3D con sistema de partículas.

Especificaciones:

  • Render: Cycles GPU
  • Resolución: 4K
  • Frames: 120+
" - }, - { - "id": "visuales-unreal", - "titulo": "Entornos Virtuales", - "peso": 70, - "imagen": "/assets/proyectos/unreal.jpg", - "descripcion": "Creación de entornos virtuales fotorrealistas en Unreal Engine.", - "tecnologias": ["Unreal Engine", "Blender", "Substance Painter"], - "contenido": "

Entornos Virtuales

Escenarios fotorrealistas en Unreal Engine 5.

Tecnologías:

  • Lumen y Nanite
  • Texturizado PBR
  • Modelado en Blender
" - }, - { - "id": "motion-graphics", - "titulo": "Motion Graphics", - "peso": 55, - "imagen": "/assets/proyectos/motion.jpg", - "descripcion": "Piezas de motion graphics para redes sociales y proyectos comerciales.", - "tecnologias": ["Blender", "After Effects", "Illustrator"], - "contenido": "

Motion Graphics

Piezas animadas para redes y publicidad.

Servicios:

  • Contenido social
  • Intros/outros
  • Infografías animadas
" - } - ] - }, - { - "id": "programacion", - "titulo": "Proyectos de Programación", - "logo": "/icons/code.svg", - "descripcion": "Aplicaciones y scripts en Python, C++ y más", - "proyectos": [ - { - "id": "github-contributions", - "titulo": "Contribuciones Open Source", - "peso": 65, - "imagen": "/assets/proyectos/Contribuciones.png", - "descripcion": "Más de 4 años de experiencia con Git/GitHub.", - "tecnologias": ["Git", "GitHub", "Python", "JavaScript"], - "link": "https://github.com/PGSCOM", - "contenido": "

Open Source

Participación activa en código abierto.

Estadísticas:

  • 20+ repositorios
  • 50+ estrellas
  • 30+ pull requests
" - }, - { - "id": "python-tools", - "titulo": "Herramientas Python", - "peso": 75, - "imagen": "/assets/proyectos/Herramientas.png", - "descripcion": "Scripts de automatización para proyectos audiovisuales. +6 años.", - "tecnologias": ["Python", "NumPy", "OpenCV", "FFmpeg"], - "contenido": "

Herramientas Python

Scripts de automatización audiovisual.

Proyectos:

  • Automatización FFmpeg
  • Procesamiento de metadatos
  • Batch de imágenes
" - }, - { - "id": "cpp-learning", - "titulo": "Aprendiendo C++", - "peso": 40, - "imagen": "/assets/proyectos/cpp.jpg", - "descripcion": "Proyectos académicos y personales en C++.", - "tecnologias": ["C++", "CMake", "Visual Studio"], - "contenido": "

Aprendiendo C++

Formación en C++ moderno.

Áreas:

  • C++17/20
  • Algoritmos
  • Desarrollo de juegos
" - } - ] - }, - { - "id": "diseño-grafico", - "titulo": "Diseño Gráfico", - "logo": "/icons/design.svg", - "descripcion": "Identidad visual, carteles y diseño digital", - "proyectos": [ - { - "id": "identidad-pgscom", - "titulo": "Identidad PGSCOM", - "peso": 55, - "imagen": "/assets/proyectos/branding.jpg", - "descripcion": "Diseño de identidad visual completa.", - "tecnologias": ["Illustrator", "Photoshop", "Figma"], - "contenido": "

Identidad PGSCOM

Identidad de marca completa.

Elementos:

  • Logo vectorial
  • Paleta de colores
  • Tipografía
  • Assets sociales
" - }, - { - "id": "carteleria", - "titulo": "Cartelería de Eventos", - "peso": 45, - "imagen": "/assets/proyectos/posters.jpg", - "descripcion": "Diseño de carteles para eventos culturales.", - "tecnologias": ["Photoshop", "Illustrator", "InDesign"], - "contenido": "

Cartelería

Material gráfico promocional.

Tipos:

  • Estrenos cinematográficos
  • Eventos musicales
  • Portadas
" - }, - { - "id": "diseño-web", - "titulo": "UI/UX Design", - "peso": 60, - "imagen": "/assets/proyectos/UI.png", - "descripcion": "Diseño de interfaces para aplicaciones web y móviles.", - "tecnologias": ["Figma", "Adobe XD", "Sketch"], - "contenido": "

UI/UX Design

Interfaces centradas en experiencia.

Proceso:

  1. Investigación
  2. Wireframing
  3. Diseño visual
  4. Testing
" - } - ] - }, - { - "id": "fotografia", - "titulo": "Fotografía", - "logo": "/icons/photo.svg", - "descripcion": "Fotografía digital y retoque profesional", - "proyectos": [ - { - "id": "fotografia-eventos", - "titulo": "Fotografía de Eventos", - "peso": 55, - "imagen": "/assets/proyectos/eventos.jpg", - "descripcion": "Cobertura fotográfica de eventos culturales.", - "tecnologias": ["Lightroom", "Photoshop", "Capture One"], - "contenido": "

Fotografía de Eventos

Cobertura profesional.

Servicios:

  • Eventos culturales
  • Corporativos
  • Premieres
" - }, - { - "id": "retratos", - "titulo": "Retratos", - "peso": 40, - "imagen": "/assets/proyectos/retratos.jpg", - "descripcion": "Sesiones de retrato profesional.", - "tecnologias": ["Lightroom", "Photoshop"], - "contenido": "

Retratos

Retrato profesional.

Tipos:

  • Corporativos
  • Headshots
  • Familiares
" - }, - { - "id": "fotografia-producto", - "titulo": "Fotografía de Producto", - "peso": 50, - "imagen": "/assets/proyectos/producto.jpg", - "descripcion": "Fotografía comercial de productos.", - "tecnologias": ["Photoshop", "Lightroom", "Capture One"], - "contenido": "

Fotografía de Producto

Fotografía comercial para e-commerce.

Servicios:

  • E-commerce
  • Catálogos
  • Gastronómica
" - } - ] - } -] +{ + "categorias": [ + { + "id": "desarrollo-web", + "titulo": "Desarrollo Web", + "logo": "/icons/web.svg", + "descripcion": "Desarrollo de sitios y aplicaciones web con tecnologías modernas" + }, + { + "id": "video-produccion", + "titulo": "Producción Audiovisual", + "logo": "/icons/video.svg", + "descripcion": "Cortometrajes, documentales y proyectos cinematográficos" + }, + { + "id": "animacion-3d", + "titulo": "Animación 3D", + "logo": "/icons/animation.svg", + "descripcion": "Modelado, animación y renders con Blender y Unreal Engine" + }, + { + "id": "programacion", + "titulo": "Programación", + "logo": "/icons/code.svg", + "descripcion": "Aplicaciones y scripts en Python, C++ y más" + }, + { + "id": "diseño-grafico", + "titulo": "Diseño Gráfico", + "logo": "/icons/design.svg", + "descripcion": "Identidad visual, carteles y diseño digital" + }, + { + "id": "fotografia", + "titulo": "Fotografía", + "logo": "/icons/photo.svg", + "descripcion": "Fotografía digital y retoque profesional" + } + ], + "proyectos": [ + { + "id": "portfolio-pgscom", + "titulo": "Portfolio PGSCOM", + "fecha": "2025-03", + "categorias": ["desarrollo-web"], + "peso": 90, + "imagen": "/assets/proyectos/portfolio.png", + "descripcion": "Sitio web personal desarrollado con Astro, React y animaciones con AnimeJS.", + "tecnologias": ["Astro", "React", "TypeScript", "AnimeJS", "CSS3"], + "link": "https://pgscom.es", + "contenido": "

Portfolio Personal PGSCOM

Este proyecto representa mi carta de presentación digital, desarrollado completamente desde cero con las tecnologías más modernas.

Características principales:

  • Astro 4.0 - Framework ultra-rápido
  • React - Componentes interactivos
  • AnimeJS - Animaciones fluidas
  • Diseño Responsivo
" + }, + { + "id": "cpp-learning", + "titulo": "Aprendiendo C++", + "fecha": "2024-10", + "categorias": ["programacion"], + "peso": 40, + "imagen": "/assets/proyectos/cpp.jpg", + "descripcion": "Proyectos académicos y personales en C++.", + "tecnologias": ["C++", "CMake", "Visual Studio"], + "contenido": "

Aprendiendo C++

Formación en C++ moderno.

Áreas:

  • C++17/20
  • Algoritmos
  • Desarrollo de juegos
" + }, + { + "id": "visuales-unreal", + "titulo": "Entornos Virtuales", + "fecha": "2024-08", + "categorias": ["animacion-3d"], + "peso": 70, + "imagen": "/assets/proyectos/unreal.jpg", + "descripcion": "Creación de entornos virtuales fotorrealistas en Unreal Engine.", + "tecnologias": ["Unreal Engine", "Blender", "Substance Painter"], + "contenido": "

Entornos Virtuales

Escenarios fotorrealistas en Unreal Engine 5.

Tecnologías:

  • Lumen y Nanite
  • Texturizado PBR
  • Modelado en Blender
" + }, + { + "id": "diseño-web", + "titulo": "UI/UX Design", + "fecha": "2024-06", + "categorias": ["diseño-grafico", "desarrollo-web"], + "peso": 60, + "imagen": "/assets/proyectos/UI.png", + "descripcion": "Diseño de interfaces para aplicaciones web y móviles.", + "tecnologias": ["Figma", "Adobe XD", "Sketch"], + "contenido": "

UI/UX Design

Interfaces centradas en experiencia.

Proceso:

  1. Investigación
  2. Wireframing
  3. Diseño visual
  4. Testing
" + }, + { + "id": "premio-macula-2024", + "titulo": "Premio Mácula 2024", + "fecha": "2024-05", + "categorias": ["video-produccion", "programacion"], + "peso": 80, + "imagen": "/assets/proyectos/Macula.png", + "descripcion": "Segundo Premio Mácula 2024 - Cortometraje documental galardonado.", + "tecnologias": ["DaVinci Resolve", "Premiere Pro", "After Effects"], + "premio": "2º Premio Mácula 2024", + "sub": [ + { + "titulo": "Sistema de validación de entradas", + "descripcion": "Aplicación web para validar las entradas del festival en la puerta.", + "categoria": "programacion", + "fecha": "2024-04" + }, + { + "titulo": "Cartelería del festival", + "descripcion": "Diseño del cartel y material promocional de la edición.", + "categoria": "diseño-grafico" + } + ], + "contenido": "

Segundo Premio Mácula 2024

Cortometraje documental galardonado con el Segundo Premio.

Ficha técnica:

  • Formato: Documental
  • Duración: 12 minutos
  • Postproducción: DaVinci Resolve
\"Trabajo excepcional\" - Jurado Festival Mácula
" + }, + { + "id": "portafolio-cine", + "titulo": "Portafolio Cinematográfico", + "fecha": "2024-02", + "categorias": ["desarrollo-web", "video-produccion"], + "peso": 60, + "imagen": "/assets/proyectos/PortfolioCortos.png", + "descripcion": "Galería de proyectos audiovisuales con reproductor de video integrado.", + "tecnologias": ["HTML5", "CSS3", "JavaScript", "Video.js"], + "link": "https://cine.pgscom.es", + "contenido": "

Portafolio Cinematográfico

Galería especializada para mostrar mis proyectos audiovisuales.

Funcionalidades:

  • Reproductor personalizado
  • Sistema de categorías
  • Galería de stills
" + }, + { + "id": "fotografia-producto", + "titulo": "Fotografía de Producto", + "fecha": "2024-01", + "categorias": ["fotografia"], + "peso": 50, + "imagen": "/assets/proyectos/producto.jpg", + "descripcion": "Fotografía comercial de productos.", + "tecnologias": ["Photoshop", "Lightroom", "Capture One"], + "contenido": "

Fotografía de Producto

Fotografía comercial para e-commerce.

Servicios:

  • E-commerce
  • Catálogos
  • Gastronómica
" + }, + { + "id": "logo-animado-pgscom", + "titulo": "Logo Animado PGSCOM", + "fecha": "2023-11", + "categorias": ["animacion-3d"], + "peso": 50, + "imagen": "/assets/proyectos/logo-anim.jpg", + "descripcion": "Animación 3D del logo personal con partículas y efectos visuales.", + "tecnologias": ["Blender", "Cycles", "After Effects"], + "contenido": "

Logo Animado PGSCOM

Animación 3D con sistema de partículas.

Especificaciones:

  • Render: Cycles GPU
  • Resolución: 4K
  • Frames: 120+
" + }, + { + "id": "blog-personal", + "titulo": "Blog Personal", + "fecha": "2023-09", + "categorias": ["desarrollo-web"], + "peso": 45, + "imagen": "/assets/proyectos/Blog.png", + "descripcion": "Blog técnico sobre audiovisual, tecnología y programación.", + "tecnologias": ["HTML", "CSS", "JavaScript"], + "link": "https://blog.pgscom.es", + "contenido": "

Blog Técnico Personal

Espacio dedicado a compartir conocimientos sobre producción audiovisual y programación.

Temáticas:

  • Tutoriales de edición
  • Guías de programación
  • Consejos de diseño
" + }, + { + "id": "fotografia-eventos", + "titulo": "Fotografía de Eventos", + "fecha": "2023-07", + "categorias": ["fotografia"], + "peso": 55, + "imagen": "/assets/proyectos/eventos.jpg", + "descripcion": "Cobertura fotográfica de eventos culturales.", + "tecnologias": ["Lightroom", "Photoshop", "Capture One"], + "contenido": "

Fotografía de Eventos

Cobertura profesional.

Servicios:

  • Eventos culturales
  • Corporativos
  • Premieres
" + }, + { + "id": "premio-macula-2023", + "titulo": "Premio Mácula 2023", + "fecha": "2023-05", + "categorias": ["video-produccion", "programacion"], + "peso": 70, + "imagen": "/assets/proyectos/Macula.png", + "descripcion": "Segundo Premio Mácula 2023 - Proyecto audiovisual experimental.", + "tecnologias": ["DaVinci Resolve", "Premiere Pro"], + "premio": "2º Premio Mácula 2023", + "sub": [ + { + "titulo": "Sistema de validación de entradas", + "descripcion": "Primera versión del validador de entradas del festival.", + "categoria": "programacion" + } + ], + "contenido": "

Segundo Premio Mácula 2023

Proyecto audiovisual experimental con nuevas formas narrativas.

Características:

  • Montaje experimental
  • Paleta expresionista
  • Banda sonora original
" + }, + { + "id": "motion-graphics", + "titulo": "Motion Graphics", + "fecha": "2023-04", + "categorias": ["animacion-3d", "diseño-grafico"], + "peso": 55, + "imagen": "/assets/proyectos/motion.jpg", + "descripcion": "Piezas de motion graphics para redes sociales y proyectos comerciales.", + "tecnologias": ["Blender", "After Effects", "Illustrator"], + "contenido": "

Motion Graphics

Piezas animadas para redes y publicidad.

Servicios:

  • Contenido social
  • Intros/outros
  • Infografías animadas
" + }, + { + "id": "carteleria", + "titulo": "Cartelería de Eventos", + "fecha": "2023-02", + "categorias": ["diseño-grafico"], + "peso": 45, + "imagen": "/assets/proyectos/posters.jpg", + "descripcion": "Diseño de carteles para eventos culturales.", + "tecnologias": ["Photoshop", "Illustrator", "InDesign"], + "contenido": "

Cartelería

Material gráfico promocional.

Tipos:

  • Estrenos cinematográficos
  • Eventos musicales
  • Portadas
" + }, + { + "id": "retratos", + "titulo": "Retratos", + "fecha": "2022-09", + "categorias": ["fotografia"], + "peso": 40, + "imagen": "/assets/proyectos/retratos.jpg", + "descripcion": "Sesiones de retrato profesional.", + "tecnologias": ["Lightroom", "Photoshop"], + "contenido": "

Retratos

Retrato profesional.

Tipos:

  • Corporativos
  • Headshots
  • Familiares
" + }, + { + "id": "python-tools", + "titulo": "Herramientas Python", + "fecha": "2022-06", + "categorias": ["programacion"], + "peso": 75, + "imagen": "/assets/proyectos/Herramientas.png", + "descripcion": "Scripts de automatización para proyectos audiovisuales. +6 años.", + "tecnologias": ["Python", "NumPy", "OpenCV", "FFmpeg"], + "contenido": "

Herramientas Python

Scripts de automatización audiovisual.

Proyectos:

  • Automatización FFmpeg
  • Procesamiento de metadatos
  • Batch de imágenes
" + }, + { + "id": "identidad-pgscom", + "titulo": "Identidad PGSCOM", + "fecha": "2022-03", + "categorias": ["diseño-grafico"], + "peso": 55, + "imagen": "/assets/proyectos/branding.jpg", + "descripcion": "Diseño de identidad visual completa.", + "tecnologias": ["Illustrator", "Photoshop", "Figma"], + "contenido": "

Identidad PGSCOM

Identidad de marca completa.

Elementos:

  • Logo vectorial
  • Paleta de colores
  • Tipografía
  • Assets sociales
" + }, + { + "id": "cortometrajes", + "titulo": "Cortometrajes Varios", + "fecha": "2022", + "categorias": ["video-produccion"], + "peso": 60, + "imagen": "/assets/proyectos/PortfolioCortos.png", + "descripcion": "Colección de cortometrajes de ficción y documental.", + "tecnologias": ["DaVinci Resolve", "After Effects", "Audition"], + "contenido": "

Colección de Cortometrajes

Recopilación de proyectos audiovisuales en ficción y documental.

Roles:

  • Dirección de fotografía
  • Montaje
  • Corrección de color
" + }, + { + "id": "github-contributions", + "titulo": "Contribuciones Open Source", + "fecha": "2021", + "categorias": ["programacion"], + "peso": 65, + "imagen": "/assets/proyectos/Contribuciones.png", + "descripcion": "Más de 4 años de experiencia con Git/GitHub.", + "tecnologias": ["Git", "GitHub", "Python", "JavaScript"], + "link": "https://github.com/PGSCOM", + "contenido": "

Open Source

Participación activa en código abierto.

Estadísticas:

  • 20+ repositorios
  • 50+ estrellas
  • 30+ pull requests
" + } + ] +} diff --git a/src/pages/index.astro b/src/pages/index.astro index a7b8c03..8e8a024 100644 --- a/src/pages/index.astro +++ b/src/pages/index.astro @@ -12,17 +12,20 @@ import contactoData from '../data/contacto.json'; // Forma de la P extraída de public/assets/animlogo/carga.svg (viewBox 600x600) const pPath = 'M 185.06066,92.957646 V 214.07196 456.30058 H 306.17677 V 335.19165 c 66.88974,0 121.11609,-54.22597 121.11609,-121.11609 0,-66.88986 -54.22635,-121.114324 -121.11609,-121.114324 v -0.004 z'; +const categorias = proyectosData.categorias; +const proyectos = proyectosData.proyectos; +const catById = Object.fromEntries(categorias.map((c) => [c.id, c])); + // Proyectos destacados que flotan en el hero (con imagen existente) const featuredIds = ['portfolio-pgscom', 'premio-macula-2024', 'diseño-web', 'portafolio-cine', 'python-tools', 'blog-personal']; -const featured = []; -for (const aptitud of proyectosData) { - for (const proyecto of aptitud.proyectos) { - if (featuredIds.includes(proyecto.id)) { - featured.push({ ...proyecto, aptitudId: aptitud.id, aptitudTitulo: aptitud.titulo }); - } - } -} -featured.sort((a, b) => featuredIds.indexOf(a.id) - featuredIds.indexOf(b.id)); +const featured = featuredIds + .map((id) => proyectos.find((p) => p.id === id)) + .filter(Boolean) + .map((p) => ({ + ...p, + aptitudId: p.categorias?.[0], + aptitudTitulo: catById[p.categorias?.[0]]?.titulo ?? '', + })); // Posición, profundidad (parallax) y rotación de cada card del hero const heroLayout = [ @@ -44,28 +47,53 @@ const catColors = { 'fotografia': '#ffc94d', }; -// Ruta de proyectos: tarjetas grandes a lo largo del scroll, una estación por -// categoría. peso → tamaño de tarjeta; lado e inset alternan para que la línea -// serpentee de forma orgánica. -let rutaIndice = 0; -const rutaData = proyectosData.map((a) => ({ - id: a.id, - titulo: a.titulo, - descripcion: a.descripcion, - logo: a.logo, - color: catColors[a.id] ?? '#9bb4d0', - items: (a.proyectos ?? []).map((p) => { - const i = rutaIndice++; - const peso = p.peso ?? 50; - return { - ...p, - num: String(i + 1).padStart(2, '0'), - lado: i % 2 === 0 ? 'izq' : 'der', - inset: ['0%', '7%', '3%'][i % 3], - w: peso >= 75 ? 580 : peso >= 55 ? 500 : 420, - }; - }), -})); +// ── Ruta cronológica (formato currículum: lo más reciente arriba) ── +// fecha admite "YYYY", "YYYY-MM" o "YYYY-MM-DD"; se muestra con la precisión +// que tenga. Las categorías son etiquetas del proyecto (puede tener varias) y +// las subtarjetas (`sub`) muestran trabajos concretos dentro del proyecto. +const MESES = ['enero', 'febrero', 'marzo', 'abril', 'mayo', 'junio', 'julio', 'agosto', 'septiembre', 'octubre', 'noviembre', 'diciembre']; +function formatFecha(f) { + if (!f) return ''; + const [y, m, d] = String(f).split('-'); + if (!m) return y; + const mes = MESES[Number(m) - 1] ?? ''; + if (d) return `${Number(d)} de ${mes} de ${y}`; + return `${mes.charAt(0).toUpperCase()}${mes.slice(1)} ${y}`; +} + +const cronologia = [...proyectos].sort((a, b) => + String(b.fecha ?? '').localeCompare(String(a.fecha ?? '')) +); + +let añoAnterior = null; +const rutaEntries = cronologia.map((p, i) => { + const año = String(p.fecha ?? '').slice(0, 4) || 'Otros'; + const nuevoAño = año !== añoAnterior; + añoAnterior = año; + const peso = p.peso ?? 50; + const catsProyecto = p.categorias ?? []; + const subs = (p.sub ?? []).map((s) => ({ + ...s, + cat: s.categoria ? catById[s.categoria] : null, + color: s.categoria ? (catColors[s.categoria] ?? '#9bb4d0') : null, + fechaTexto: formatFecha(s.fecha), + })); + // Para el filtro cuentan las categorías del proyecto y las de sus subtarjetas + const todasCats = [...new Set([...catsProyecto, ...subs.map((s) => s.categoria).filter(Boolean)])]; + return { + ...p, + año, + nuevoAño, + fechaTexto: formatFecha(p.fecha), + lado: i % 2 === 0 ? 'izq' : 'der', + inset: ['0%', '5%', '2%'][i % 3], + w: peso >= 75 ? 580 : peso >= 55 ? 500 : 430, + color: catColors[catsProyecto[0]] ?? '#9bb4d0', + cats: catsProyecto.map((id) => catById[id]).filter(Boolean), + todasCats, + subs, + }; +}); --- @@ -167,16 +195,16 @@ const rutaData = proyectosData.map((a) => ({

Proyectos

- Un recorrido por todos mis proyectos: baja siguiendo la línea y párate en los que te llamen. El tamaño de cada parada refleja la envergadura del proyecto. + Mi trayectoria en orden cronológico, de lo más reciente a lo más antiguo. Filtra por disciplina para ver solo lo que te interesa.

diff --git a/src/pages/proyectos/[id].astro b/src/pages/proyectos/[id].astro index aff4a3a..b4e971d 100644 --- a/src/pages/proyectos/[id].astro +++ b/src/pages/proyectos/[id].astro @@ -3,24 +3,38 @@ import Layout from '../../layouts/Layout.astro'; import '../../styles/proyecto-detalle.css'; import proyectosData from '../../data/proyectos.json'; -// Genera una ruta estática por cada proyecto de cada aptitud. +// Genera una ruta estática por cada proyecto. // Cada página es HTML real y rastreable por los bots de Google. export function getStaticPaths() { - const paths = []; - for (const aptitud of proyectosData) { - for (const proyecto of aptitud.proyectos ?? []) { - const relacionados = aptitud.proyectos.filter((p) => p.id !== proyecto.id); - paths.push({ - params: { id: proyecto.id }, - props: { proyecto, aptitud, relacionados }, - }); - } - } - return paths; + const cats = proyectosData.categorias; + return proyectosData.proyectos.map((proyecto) => { + const catIds = proyecto.categorias ?? []; + // La primera categoría del proyecto actúa como principal + const aptitud = cats.find((c) => c.id === catIds[0]) ?? cats[0]; + const relacionados = proyectosData.proyectos + .filter((p) => p.id !== proyecto.id && (p.categorias ?? []).some((c) => catIds.includes(c))) + .slice(0, 3); + return { + params: { id: proyecto.id }, + props: { proyecto, aptitud, relacionados }, + }; + }); } const { proyecto, aptitud, relacionados } = Astro.props; +// Fecha con precisión variable: "YYYY", "YYYY-MM" o "YYYY-MM-DD" +const MESES = ['enero', 'febrero', 'marzo', 'abril', 'mayo', 'junio', 'julio', 'agosto', 'septiembre', 'octubre', 'noviembre', 'diciembre']; +function formatFecha(f) { + if (!f) return ''; + const [y, m, d] = String(f).split('-'); + if (!m) return y; + const mes = MESES[Number(m) - 1] ?? ''; + if (d) return `${Number(d)} de ${mes} de ${y}`; + return `${mes.charAt(0).toUpperCase()}${mes.slice(1)} ${y}`; +} +const fechaTexto = formatFecha(proyecto.fecha); + const tituloPagina = `${proyecto.titulo.trim()} · ${aptitud.titulo} — PGSCOM`; const descripcionPagina = proyecto.descripcion || `${proyecto.titulo} — proyecto de ${aptitud.titulo} de Pablo García Santamaría.`; @@ -110,6 +124,7 @@ const breadcrumbLd = {

{proyecto.titulo.trim()}

+ {fechaTexto && 📅 {fechaTexto}} {proyecto.premio && 🏆 {proyecto.premio}} {proyecto.tecnologias?.length && ( {proyecto.tecnologias.length} tecnologías diff --git a/src/pages/sitemap.xml.ts b/src/pages/sitemap.xml.ts index 751a457..b108602 100644 --- a/src/pages/sitemap.xml.ts +++ b/src/pages/sitemap.xml.ts @@ -6,10 +6,8 @@ export const GET: APIRoute = ({ site }) => { const base = (site?.href ?? 'https://pgscom.es/').replace(/\/$/, ''); const urls: string[] = [`${base}/`]; - for (const aptitud of proyectosData) { - for (const proyecto of aptitud.proyectos ?? []) { - urls.push(`${base}/proyectos/${proyecto.id}`); - } + for (const proyecto of proyectosData.proyectos ?? []) { + urls.push(`${base}/proyectos/${proyecto.id}`); } const body = ` diff --git a/src/scripts/proyectos-ruta.js b/src/scripts/proyectos-ruta.js index 5293b09..ae08d2a 100644 --- a/src/scripts/proyectos-ruta.js +++ b/src/scripts/proyectos-ruta.js @@ -2,15 +2,17 @@ import gsap from 'gsap'; import { ScrollTrigger } from 'gsap/ScrollTrigger'; gsap.registerPlugin(ScrollTrigger); -// ── Ruta de proyectos ───────────────────────────────────────────────────────── -// Una línea luminosa serpentea por la sección pasando por cada estación de -// categoría y cada tarjeta de proyecto, y se va trazando a medida que se hace -// scroll. Las tarjetas (HTML estático) entran animadas al llegar a ellas. +// ── Ruta cronológica de proyectos ───────────────────────────────────────────── +// Una línea luminosa baja por la cronología (lo más reciente arriba) pasando +// por cada marcador de año y cada tarjeta, y se traza con el scroll. Las +// subtarjetas aparecen "conectándose" a su tarjeta principal. Los filtros por +// disciplina atenúan y desaturan lo que no encaja, sin ocultarlo. function initRuta() { - const cont = document.getElementById('proyectos-mapa'); - const svg = cont?.querySelector('.ruta-linea'); - if (!cont || !svg) return; + const mapa = document.getElementById('proyectos-mapa'); + const cuerpo = mapa?.querySelector('.ruta-cuerpo'); + const svg = mapa?.querySelector('.ruta-linea'); + if (!mapa || !cuerpo || !svg) return; const reduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches; const NS = 'http://www.w3.org/2000/svg'; @@ -22,24 +24,24 @@ function initRuta() { let drawTween = null; - // Puntos de paso: el dot de cada estación y el centro de cada tarjeta. + // Puntos de paso: el punto de cada año y el centro de cada tarjeta principal. // En pantallas estrechas la línea baja recta por el margen izquierdo. function puntos() { - const base = cont.getBoundingClientRect(); + const base = cuerpo.getBoundingClientRect(); const narrow = base.width < 760; - const els = cont.querySelectorAll('.ruta-estacion-dot, .ruta-item'); + const els = cuerpo.querySelectorAll('.ruta-año-dot, .ruta-item'); return [...els].map((el) => { const b = el.getBoundingClientRect(); return { - x: narrow ? 18 : b.left - base.left + b.width / 2, + x: narrow ? 16 : b.left - base.left + b.width / 2, y: b.top - base.top + b.height / 2, }; }); } function build() { - const w = cont.clientWidth; - const h = cont.scrollHeight; + const w = cuerpo.clientWidth; + const h = cuerpo.scrollHeight; svg.setAttribute('width', w); svg.setAttribute('height', h); svg.setAttribute('viewBox', `0 0 ${w} ${h}`); @@ -71,7 +73,7 @@ function initRuta() { strokeDashoffset: 0, ease: 'none', scrollTrigger: { - trigger: cont, + trigger: cuerpo, start: 'top 65%', end: 'bottom 85%', scrub: 0.5, @@ -81,29 +83,46 @@ function initRuta() { // ── Entradas al hacer scroll ── if (!reduced) { - gsap.utils.toArray('.ruta-estacion', cont).forEach((el) => { + gsap.utils.toArray('.ruta-año', cuerpo).forEach((el) => { gsap.from(el, { autoAlpha: 0, - y: 30, - duration: 0.7, - ease: 'power3.out', + scale: 0.7, + duration: 0.6, + ease: 'back.out(1.6)', scrollTrigger: { trigger: el, start: 'top 88%' }, }); }); - gsap.utils.toArray('.ruta-item', cont).forEach((el, i) => { - gsap.from(el, { + + gsap.utils.toArray('.ruta-entry', cuerpo).forEach((entry) => { + const der = entry.classList.contains('ruta-entry--der'); + const item = entry.querySelector('.ruta-item'); + const subs = entry.querySelectorAll('.ruta-sub'); + + gsap.from(item, { autoAlpha: 0, y: 56, - rotation: i % 2 === 0 ? -1.4 : 1.4, + rotation: der ? 1.4 : -1.4, duration: 0.9, ease: 'power3.out', - scrollTrigger: { trigger: el, start: 'top 88%' }, + scrollTrigger: { trigger: entry, start: 'top 86%' }, }); + + // Las subtarjetas brotan desde la tarjeta principal, en cascada + if (subs.length) { + gsap.from(subs, { + autoAlpha: 0, + x: der ? 36 : -36, + duration: 0.7, + ease: 'power3.out', + stagger: 0.16, + scrollTrigger: { trigger: entry, start: 'top 70%' }, + }); + } }); } - // La estación se "enciende" cuando la ruta pasa por ella - gsap.utils.toArray('.ruta-estacion', cont).forEach((el) => { + // El marcador de año se "enciende" cuando la ruta pasa por él + gsap.utils.toArray('.ruta-año', cuerpo).forEach((el) => { ScrollTrigger.create({ trigger: el, start: 'top 75%', @@ -113,20 +132,48 @@ function initRuta() { }); // ── Imágenes que no existen: fondo tintado + icono de la categoría ── - cont.querySelectorAll('.ruta-img').forEach((img) => { + cuerpo.querySelectorAll('.ruta-img').forEach((img) => { const fallar = () => img.closest('.ruta-item')?.classList.add('sin-imagen'); if (img.complete && img.naturalWidth === 0) fallar(); else img.addEventListener('error', fallar, { once: true }); }); - // ── La órbita de categorías navega a su estación con scroll suave ── + // ── Filtro por disciplina: atenúa y desatura, no oculta ── + const filtros = mapa.querySelectorAll('.ruta-filtro'); + const entries = cuerpo.querySelectorAll('.ruta-entry'); + let filtroActual = 'all'; + + function setFiltro(cat) { + filtroActual = cat; + entries.forEach((entry) => { + const cats = (entry.dataset.cats || '').split(','); + entry.classList.toggle('fuera', cat !== 'all' && !cats.includes(cat)); + }); + filtros.forEach((f) => { + const act = f.dataset.cat === cat; + f.classList.toggle('act', act); + f.setAttribute('aria-pressed', act ? 'true' : 'false'); + }); + document.querySelectorAll('.proyecto-card').forEach((card) => { + card.classList.toggle('is-active', card.dataset.aptitudId === cat); + }); + } + + filtros.forEach((f) => { + f.addEventListener('click', () => { + const cat = f.dataset.cat; + setFiltro(cat !== 'all' && filtroActual === cat ? 'all' : cat); + }); + }); + + // La órbita de categorías también filtra y baja hasta la cronología document.querySelectorAll('.proyecto-card').forEach((card) => { card.addEventListener('click', (e) => { const id = card.dataset.aptitudId; - const target = id && document.getElementById(`ruta-${id}`); - if (!target) return; + if (!id) return; e.preventDefault(); - target.scrollIntoView({ behavior: reduced ? 'auto' : 'smooth', block: 'start' }); + setFiltro(filtroActual === id ? 'all' : id); + mapa.scrollIntoView({ behavior: reduced ? 'auto' : 'smooth', block: 'start' }); }); }); @@ -139,13 +186,13 @@ function initRuta() { }); let resizeRaf = 0; - let lastW = cont.clientWidth; + let lastW = cuerpo.clientWidth; window.addEventListener('resize', () => { if (resizeRaf) return; resizeRaf = requestAnimationFrame(() => { resizeRaf = 0; - if (cont.clientWidth !== lastW) { - lastW = cont.clientWidth; + if (cuerpo.clientWidth !== lastW) { + lastW = cuerpo.clientWidth; build(); } }); diff --git a/src/styles/proyectos.css b/src/styles/proyectos.css index 03d9cf8..1af13ba 100644 --- a/src/styles/proyectos.css +++ b/src/styles/proyectos.css @@ -1,4 +1,4 @@ -/* Estilos para la sección de proyectos - Órbita de categorías + ruta de proyectos */ +/* Estilos para la sección de proyectos - Órbita de categorías + ruta cronológica */ /* ============================================ ANIMACIONES PERSONALIZADAS @@ -57,7 +57,7 @@ /* ============================================ HERO DECORATIVO - Órbita de categorías - (cada tarjeta en órbita navega a su estación) + (cada tarjeta en órbita filtra la cronología) ============================================ */ .proyectos-hero { @@ -187,6 +187,18 @@ rgba(255, 255, 255, 0.08)); } +/* Categoría activa como filtro: aro en su color */ +.proyecto-card.is-active { + filter: brightness(1.15) saturate(1.2); +} + +.proyecto-card.is-active .proyecto-card-inner { + border-color: var(--cat-color, rgba(147, 197, 253, 0.8)); + box-shadow: + 0 8px 24px rgba(0, 0, 0, 0.35), + 0 0 26px color-mix(in srgb, var(--cat-color, #93c5fd) 55%, transparent); +} + .proyecto-icon { width: 100%; height: auto; @@ -235,17 +247,81 @@ } /* ============================================ - RUTA DE PROYECTOS - Línea + paradas con scroll + RUTA CRONOLÓGICA - Currículum con scroll ============================================ */ .proyectos-mapa { position: relative; - max-width: 1200px; + max-width: 1320px; margin: 0 auto; - padding: 4vh 24px 12vh; + padding: 2vh 24px 12vh; scroll-margin-top: 8vh; } +/* Filtros por disciplina (atenúan, no ocultan) */ +.ruta-filtros { + position: sticky; + top: 14px; + z-index: 30; + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 8px; + width: fit-content; + max-width: 100%; + margin: 0 auto 7vh; + padding: 10px 14px; + border-radius: 22px; + background: rgba(5, 8, 18, 0.72); + border: 1px solid rgba(255, 255, 255, 0.12); + backdrop-filter: blur(14px); + -webkit-backdrop-filter: blur(14px); + box-shadow: 0 10px 36px rgba(0, 0, 0, 0.4); +} + +.ruta-filtro { + display: inline-flex; + align-items: center; + gap: 7px; + padding: 7px 14px; + border-radius: 999px; + font-family: 'Chakra Petch', sans-serif; + font-size: 0.8rem; + font-weight: 500; + color: rgba(255, 255, 255, 0.78); + background: rgba(255, 255, 255, 0.05); + border: 1px solid rgba(255, 255, 255, 0.14); + cursor: pointer; + transition: all 0.3s ease; +} + +.ruta-filtro img { + width: 14px; + height: 14px; +} + +.ruta-filtro:hover { + color: white; + border-color: color-mix(in srgb, var(--cat-color, #93c5fd) 60%, transparent); + transform: translateY(-1px); +} + +.ruta-filtro.act { + color: white; + background: color-mix(in srgb, var(--cat-color, #ffffff) 24%, transparent); + border-color: var(--cat-color, #ffffff); +} + +.ruta-filtro[data-cat="all"].act { + background: white; + color: #0a0e1c; +} + +/* Cuerpo de la cronología (sobre él se dibuja la línea) */ +.ruta-cuerpo { + position: relative; +} + /* La línea que serpentea (la dibuja el JS) */ .ruta-linea { position: absolute; @@ -271,91 +347,114 @@ stroke-linecap: round; } -/* Estación de categoría */ -.ruta-estacion { +/* Marcador de año en la línea */ +.ruta-año { position: relative; z-index: 1; display: flex; flex-direction: column; align-items: center; - gap: 8px; - text-align: center; - max-width: 560px; - margin: 9vh auto 8vh; - scroll-margin-top: 14vh; + gap: 12px; + width: fit-content; + margin: 2vh auto 7vh; } -.ruta-estacion-dot { - width: 20px; - height: 20px; +.ruta-año-dot { + width: 16px; + height: 16px; border-radius: 50%; - background: var(--cat-color, #93c5fd); - box-shadow: 0 0 0 5px color-mix(in srgb, var(--cat-color, #93c5fd) 22%, transparent); - transition: transform 0.4s cubic-bezier(0.34, 1.56, 0.64, 1), box-shadow 0.4s ease; + background: rgba(196, 218, 255, 0.95); + box-shadow: 0 0 0 5px rgba(147, 197, 253, 0.18); + transition: box-shadow 0.4s ease, transform 0.4s cubic-bezier(0.34, 1.56, 0.64, 1); } -.ruta-estacion.on .ruta-estacion-dot { - transform: scale(1.3); +.ruta-año.on .ruta-año-dot { + transform: scale(1.25); box-shadow: - 0 0 0 6px color-mix(in srgb, var(--cat-color, #93c5fd) 28%, transparent), - 0 0 28px var(--cat-color, #93c5fd); + 0 0 0 6px rgba(147, 197, 253, 0.25), + 0 0 26px rgba(147, 197, 253, 0.85); } -.ruta-estacion-titulo { +.ruta-año-num { font-family: 'Chakra Petch', sans-serif; - font-size: clamp(1.25rem, 2.4vw, 1.6rem); font-weight: 700; - color: white; - margin: 0; - text-shadow: 0 2px 8px rgba(0, 0, 0, 0.4); + font-size: clamp(1.4rem, 2.6vw, 1.9rem); + letter-spacing: 0.08em; + color: rgba(255, 255, 255, 0.95); + padding: 4px 20px; + border-radius: 999px; + border: 1px solid rgba(255, 255, 255, 0.16); + background: rgba(6, 9, 20, 0.6); + backdrop-filter: blur(8px); + -webkit-backdrop-filter: blur(8px); + text-shadow: 0 2px 14px rgba(0, 0, 0, 0.7); + box-shadow: 0 6px 24px rgba(0, 0, 0, 0.35); } -.ruta-estacion-desc { - font-family: 'Chakra Petch', sans-serif; - font-size: 0.9rem; - color: rgba(255, 255, 255, 0.6); - margin: 0; - line-height: 1.5; +/* Entrada de la cronología: tarjeta principal + subtarjetas conectadas */ +.ruta-entry { + position: relative; + z-index: 1; + display: flex; + align-items: center; + gap: 34px; + margin-bottom: 14vh; + transition: opacity 0.5s ease, filter 0.5s ease; } -/* Parada de proyecto: imagen grande protagonista */ +.ruta-entry--izq { + justify-content: flex-start; + padding-left: var(--inset, 0%); +} + +.ruta-entry--der { + flex-direction: row-reverse; + justify-content: flex-start; + padding-right: var(--inset, 0%); +} + +/* Filtrado: fuera de la disciplina elegida → gris y atenuado */ +.ruta-entry.fuera { + opacity: 0.32; + filter: grayscale(1) brightness(0.85); +} + +/* Tarjeta principal */ .ruta-item { position: relative; - z-index: 1; display: block; - width: min(46%, var(--w, 500px)); - margin-bottom: 10vh; + flex: 0 1 var(--w, 500px); + min-width: 280px; text-decoration: none; outline: none; } -.ruta-item--izq { - margin-right: auto; - margin-left: var(--inset, 0%); -} - -.ruta-item--der { - margin-left: auto; - margin-right: var(--inset, 0%); -} - -/* Número de parada, como un hito del recorrido */ -.ruta-num { +/* Fecha del proyecto: chip legible sobre la esquina de la tarjeta */ +.ruta-fecha { position: absolute; - top: -0.52em; - z-index: 2; + top: -15px; + left: 18px; + z-index: 3; font-family: 'Chakra Petch', sans-serif; font-weight: 700; - font-size: clamp(2.4rem, 4.5vw, 3.6rem); - line-height: 1; - color: transparent; - -webkit-text-stroke: 1.5px color-mix(in srgb, var(--cat-color, #93c5fd) 75%, white 15%); - opacity: 0.85; - pointer-events: none; + font-size: 0.8rem; + color: white; + white-space: nowrap; + padding: 7px 14px; + border-radius: 999px; + background: rgba(6, 9, 20, 0.9); + border: 1px solid color-mix(in srgb, var(--cat-color, #93c5fd) 60%, transparent); + backdrop-filter: blur(8px); + -webkit-backdrop-filter: blur(8px); + box-shadow: + 0 4px 18px rgba(0, 0, 0, 0.45), + 0 0 14px color-mix(in srgb, var(--cat-color, #93c5fd) 18%, transparent); } -.ruta-item--izq .ruta-num { right: 14px; } -.ruta-item--der .ruta-num { left: 14px; } +.ruta-entry--der .ruta-fecha { + left: auto; + right: 18px; +} .ruta-media { position: relative; @@ -429,7 +528,7 @@ .ruta-premio { position: absolute; top: 14px; - left: 14px; + right: 14px; z-index: 2; padding: 6px 12px; border-radius: 999px; @@ -443,7 +542,7 @@ font-weight: 600; } -/* Info sobre la imagen: título siempre, detalle al pasar el cursor */ +/* Info sobre la imagen: chips de categoría + título; detalle al pasar el cursor */ .ruta-info { position: absolute; left: 0; @@ -452,7 +551,7 @@ z-index: 1; display: flex; flex-direction: column; - gap: 5px; + gap: 6px; padding: 56px 20px 16px; background: linear-gradient(180deg, transparent 0%, @@ -460,13 +559,32 @@ rgba(2, 8, 23, 0.92) 100%); } -.ruta-cat { +/* Distintivo de categoría: color + icono (puede haber varias) */ +.ruta-cats { + display: flex; + flex-wrap: wrap; + gap: 6px; +} + +.ruta-cat-chip { + display: inline-flex; + align-items: center; + gap: 5px; font-family: 'Chakra Petch', sans-serif; - font-size: 0.68rem; + font-size: 0.62rem; font-weight: 600; - letter-spacing: 0.12em; + letter-spacing: 0.08em; text-transform: uppercase; color: var(--cat-color, #93c5fd); + padding: 3px 9px; + border-radius: 999px; + border: 1px solid color-mix(in srgb, var(--cat-color, #93c5fd) 45%, transparent); + background: rgba(6, 9, 20, 0.55); +} + +.ruta-cat-chip img { + width: 12px; + height: 12px; } .ruta-titulo { @@ -523,6 +641,88 @@ color: #bfdbfe; } +/* Subtarjetas: trabajos concretos dentro del proyecto, conectadas al lado */ +.ruta-subs { + flex: 0 1 320px; + min-width: 240px; + display: flex; + flex-direction: column; + gap: 16px; +} + +.ruta-sub { + position: relative; + display: flex; + gap: 10px; + align-items: flex-start; + padding: 13px 15px; + border-radius: 14px; + background: rgba(10, 14, 28, 0.78); + border: 1px solid rgba(255, 255, 255, 0.12); + border-left: 3px solid var(--cat-color, rgba(147, 197, 253, 0.55)); + backdrop-filter: blur(8px); + -webkit-backdrop-filter: blur(8px); + box-shadow: 0 8px 26px rgba(0, 0, 0, 0.35); +} + +/* Conector hacia la tarjeta principal */ +.ruta-sub::before { + content: ''; + position: absolute; + top: 50%; + left: -35px; + width: 34px; + height: 2px; + transform: translateY(-50%); + background: linear-gradient(90deg, + color-mix(in srgb, var(--cat-color, #93c5fd) 70%, transparent), + color-mix(in srgb, var(--cat-color, #93c5fd) 22%, transparent)); +} + +.ruta-entry--der .ruta-sub::before { + left: auto; + right: -35px; + background: linear-gradient(270deg, + color-mix(in srgb, var(--cat-color, #93c5fd) 70%, transparent), + color-mix(in srgb, var(--cat-color, #93c5fd) 22%, transparent)); +} + +.ruta-sub-icon { + width: 22px; + height: 22px; + flex-shrink: 0; + margin-top: 2px; + filter: drop-shadow(0 1px 4px rgba(0, 0, 0, 0.5)); +} + +.ruta-sub-titulo { + display: block; + font-family: 'Chakra Petch', sans-serif; + font-size: 0.85rem; + font-weight: 600; + color: white; + line-height: 1.3; +} + +.ruta-sub-desc { + font-family: 'Chakra Petch', sans-serif; + font-size: 0.76rem; + color: rgba(255, 255, 255, 0.7); + line-height: 1.45; + margin: 4px 0 0; +} + +.ruta-sub-fecha { + display: inline-block; + font-family: 'Chakra Petch', sans-serif; + font-size: 0.65rem; + font-weight: 600; + letter-spacing: 0.08em; + text-transform: uppercase; + color: var(--cat-color, #93c5fd); + margin-top: 6px; +} + /* ============================================ RESPONSIVE ============================================ */ @@ -584,49 +784,98 @@ height: 38px; } - .ruta-item { - width: min(48%, var(--w, 500px)); + .ruta-entry { + gap: 26px; + } + + .ruta-subs { + flex-basis: 280px; + } +} + +/* Las subtarjetas pasan debajo de la tarjeta cuando no caben al lado */ +@media (max-width: 980px) { + .ruta-entry { + flex-wrap: wrap; + row-gap: 22px; + } + + .ruta-subs { + flex: 1 1 100%; + max-width: var(--w, 500px); + } + + .ruta-entry--izq .ruta-subs { + margin-right: auto; + } + + .ruta-entry--der .ruta-subs { + margin-left: auto; + } + + .ruta-sub::before, + .ruta-entry--der .ruta-sub::before { + display: none; } } @media (max-width: 760px) { - /* La ruta pasa a línea de tiempo: línea recta a la izquierda */ + /* La cronología pasa a línea de tiempo: línea recta a la izquierda */ .proyectos-mapa { - padding: 3vh 16px 8vh 46px; + padding: 2vh 16px 8vh 44px; + } + + /* En táctil ocupa varias filas: mejor estática para no tapar contenido */ + .ruta-filtros { + position: static; + padding: 8px 10px; + gap: 6px; + margin-bottom: 5vh; } - .ruta-item, - .ruta-item--izq, - .ruta-item--der { + .ruta-filtro { + padding: 6px 10px; + font-size: 0.72rem; + } + + .ruta-entry, + .ruta-entry--der { + flex-direction: column; + align-items: stretch; + gap: 18px; + padding-left: 0; + padding-right: 0; + margin-bottom: 9vh; + } + + .ruta-item { + flex-basis: auto; + min-width: 0; width: 100%; - margin-left: 0; - margin-right: 0; - margin-bottom: 7vh; } - .ruta-estacion { - align-items: flex-start; - text-align: left; - margin: 7vh 0 5vh; + .ruta-entry--der .ruta-fecha { + right: auto; + left: 14px; } - .ruta-estacion-dot { - position: absolute; - left: -38px; - top: 4px; - width: 18px; - height: 18px; + .ruta-subs { + max-width: none; + gap: 12px; } - .ruta-num { - font-size: 2rem; - top: -0.5em; + .ruta-año { + position: relative; + flex-direction: row; + align-items: center; + margin: 1vh 0 5vh; } - .ruta-item--izq .ruta-num, - .ruta-item--der .ruta-num { - left: auto; - right: 12px; + .ruta-año-dot { + position: absolute; + left: -36px; + width: 16px; + height: 16px; } /* Sin hover en táctil: la descripción y las techs se muestran siempre */ @@ -721,11 +970,11 @@ } .proyectos-mapa { - padding: 2vh 12px 7vh 40px; + padding: 1vh 12px 7vh 38px; } - .ruta-estacion-dot { - left: -32px; + .ruta-año-dot { + left: -30px; } .ruta-titulo { @@ -735,4 +984,9 @@ .ruta-desc { font-size: 0.8rem; } + + .ruta-fecha { + font-size: 0.72rem; + padding: 6px 11px; + } } From 37eeffabb5c39ead316d70d32c29b36a3204464f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Garc=C3=ADa=20Santamar=C3=ADa?= <69808296+PGSCOM@users.noreply.github.com> Date: Thu, 11 Jun 2026 15:20:25 +0200 Subject: [PATCH 37/49] =?UTF-8?q?Fix=20dise=C3=B1o=20linea=20en=20filtro?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/scripts/proyectos-ruta.js | 57 ++++++++++++++++++++++++++++++++++- 1 file changed, 56 insertions(+), 1 deletion(-) diff --git a/src/scripts/proyectos-ruta.js b/src/scripts/proyectos-ruta.js index ae08d2a..6d44c8b 100644 --- a/src/scripts/proyectos-ruta.js +++ b/src/scripts/proyectos-ruta.js @@ -20,7 +20,53 @@ function initRuta() { const trazo = document.createElementNS(NS, 'path'); glow.setAttribute('class', 'ruta-linea-glow'); trazo.setAttribute('class', 'ruta-linea-trazo'); - svg.append(glow, trazo); + + // ── Máscara: la línea se oculta al pasar por detrás de cada tarjeta ── + // Siempre activa (con o sin filtro): la línea parece "colarse" entre las + // tarjetas. Los bordes se difuminan para que el corte sea suave. El + // desenfoque se aplica una sola vez a todo el grupo de recortes (más barato + // en móvil) y la máscara solo se recalcula cuando cambia el layout. + const defs = document.createElementNS(NS, 'defs'); + const blur = document.createElementNS(NS, 'filter'); + blur.setAttribute('id', 'ruta-mascara-blur'); + blur.setAttribute('x', '-20%'); + blur.setAttribute('y', '-20%'); + blur.setAttribute('width', '140%'); + blur.setAttribute('height', '140%'); + const feBlur = document.createElementNS(NS, 'feGaussianBlur'); + feBlur.setAttribute('stdDeviation', '16'); + blur.append(feBlur); + const mask = document.createElementNS(NS, 'mask'); + mask.setAttribute('id', 'ruta-mascara'); + mask.setAttribute('maskUnits', 'userSpaceOnUse'); + const maskBg = document.createElementNS(NS, 'rect'); // blanco = visible + maskBg.setAttribute('x', '0'); + maskBg.setAttribute('y', '0'); + maskBg.setAttribute('fill', '#fff'); + const recortes = document.createElementNS(NS, 'g'); // negro = oculto + recortes.setAttribute('filter', 'url(#ruta-mascara-blur)'); + mask.append(maskBg, recortes); + defs.append(blur, mask); + svg.append(defs, glow, trazo); + glow.setAttribute('mask', 'url(#ruta-mascara)'); + trazo.setAttribute('mask', 'url(#ruta-mascara)'); + + function construirMascara() { + recortes.replaceChildren(); + const base = cuerpo.getBoundingClientRect(); + cuerpo.querySelectorAll('.ruta-item').forEach((el) => { + const b = el.getBoundingClientRect(); + const pad = 22; + const r = document.createElementNS(NS, 'rect'); + r.setAttribute('x', (b.left - base.left - pad).toFixed(1)); + r.setAttribute('y', (b.top - base.top - pad).toFixed(1)); + r.setAttribute('width', (b.width + pad * 2).toFixed(1)); + r.setAttribute('height', (b.height + pad * 2).toFixed(1)); + r.setAttribute('rx', '36'); + r.setAttribute('fill', '#000'); + recortes.append(r); + }); + } let drawTween = null; @@ -46,6 +92,14 @@ function initRuta() { svg.setAttribute('height', h); svg.setAttribute('viewBox', `0 0 ${w} ${h}`); + mask.setAttribute('x', '0'); + mask.setAttribute('y', '0'); + mask.setAttribute('width', w); + mask.setAttribute('height', h); + maskBg.setAttribute('width', w); + maskBg.setAttribute('height', h); + construirMascara(); + const ps = puntos(); if (ps.length < 2) return; @@ -68,6 +122,7 @@ function initRuta() { gsap.set([glow, trazo], { strokeDasharray: 'none', strokeDashoffset: 0 }); return; } + gsap.set([glow, trazo], { strokeDasharray: L, strokeDashoffset: L }); drawTween = gsap.to([glow, trazo], { strokeDashoffset: 0, From 813d95971605c4fa3255ed0551f9a6007a893d6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Garc=C3=ADa=20Santamar=C3=ADa?= <69808296+PGSCOM@users.noreply.github.com> Date: Thu, 11 Jun 2026 15:34:10 +0200 Subject: [PATCH 38/49] =?UTF-8?q?A=C3=B1adir=20imagen=20en=20subproyectos?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/data/proyectos.json | 6 ++++-- src/pages/index.astro | 25 +++++++++++++---------- src/styles/proyectos.css | 43 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 62 insertions(+), 12 deletions(-) diff --git a/src/data/proyectos.json b/src/data/proyectos.json index 6590af2..646bb12 100644 --- a/src/data/proyectos.json +++ b/src/data/proyectos.json @@ -98,12 +98,14 @@ "titulo": "Sistema de validación de entradas", "descripcion": "Aplicación web para validar las entradas del festival en la puerta.", "categoria": "programacion", - "fecha": "2024-04" + "fecha": "2024-04", + "imagen": "/assets/proyectos/Herramientas.png" }, { "titulo": "Cartelería del festival", "descripcion": "Diseño del cartel y material promocional de la edición.", - "categoria": "diseño-grafico" + "categoria": "diseño-grafico", + "imagen": "/assets/proyectos/posters.jpg" } ], "contenido": "

Segundo Premio Mácula 2024

Cortometraje documental galardonado con el Segundo Premio.

Ficha técnica:

  • Formato: Documental
  • Duración: 12 minutos
  • Postproducción: DaVinci Resolve
\"Trabajo excepcional\" - Jurado Festival Mácula
" diff --git a/src/pages/index.astro b/src/pages/index.astro index 8e8a024..f3f4715 100644 --- a/src/pages/index.astro +++ b/src/pages/index.astro @@ -297,16 +297,21 @@ const rutaEntries = cronologia.map((p, i) => { {proyecto.subs.length > 0 && (
- {proyecto.subs.map((s) => ( -
- {s.cat && {s.cat.titulo}} -
- {s.titulo} - {s.descripcion &&

{s.descripcion}

} - {s.fechaTexto && {s.fechaTexto}} -
-
- ))} + {proyecto.subs.map((s) => ( +
+ {s.cat && {s.cat.titulo}} + {s.imagen && ( +
+ {s.titulo} +
+ )} +
+ {s.titulo} + {s.descripcion &&

{s.descripcion}

} + {s.fechaTexto && {s.fechaTexto}} +
+
+ ))}
)}
diff --git a/src/styles/proyectos.css b/src/styles/proyectos.css index 1af13ba..63ff178 100644 --- a/src/styles/proyectos.css +++ b/src/styles/proyectos.css @@ -723,6 +723,49 @@ margin-top: 6px; } +/* Subtarjetas con imagen - thumbnail horizontal compactor */ +.ruta-sub--con-img { + padding: 10px 12px; + gap: 12px; + align-items: center; +} + +.ruta-sub--con-img .ruta-sub-media { + width: 86px; + height: 54px; + border-radius: 8px; + overflow: hidden; + flex-shrink: 0; + border: 1px solid rgba(255, 255, 255, 0.1); + box-shadow: 0 3px 10px rgba(0, 0, 0, 0.3); +} + +.ruta-sub--con-img .ruta-sub-media img { + width: 100%; + height: 100%; + object-fit: cover; + display: block; + transition: transform 0.35s ease; +} + +.ruta-sub--con-img:hover .ruta-sub-media img { + transform: scale(1.06); +} + +.ruta-sub--con-img .ruta-sub-body { + flex: 1; + min-width: 0; + display: flex; + flex-direction: column; + gap: 3px; +} + +.ruta-sub--con-img .ruta-sub-icon { + width: 18px; + height: 18px; + margin-top: 1px; +} + /* ============================================ RESPONSIVE ============================================ */ From ffad477eb46fe152ae31ee038ba2d78e7ce2cf57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Garc=C3=ADa=20Santamar=C3=ADa?= <69808296+PGSCOM@users.noreply.github.com> Date: Thu, 11 Jun 2026 16:46:04 +0200 Subject: [PATCH 39/49] =?UTF-8?q?A=C3=B1adir=20link=20en=20subproyectos?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/data/proyectos.json | 6 ++++-- src/pages/index.astro | 43 +++++++++++++++++++++++----------------- src/styles/proyectos.css | 23 +++++++++++++++++++++ 3 files changed, 52 insertions(+), 20 deletions(-) diff --git a/src/data/proyectos.json b/src/data/proyectos.json index 646bb12..2e265d5 100644 --- a/src/data/proyectos.json +++ b/src/data/proyectos.json @@ -99,13 +99,15 @@ "descripcion": "Aplicación web para validar las entradas del festival en la puerta.", "categoria": "programacion", "fecha": "2024-04", - "imagen": "/assets/proyectos/Herramientas.png" + "imagen": "/assets/proyectos/Herramientas.png", + "link": "/proyectos/premio-macula-2024#sistema-validacion" }, { "titulo": "Cartelería del festival", "descripcion": "Diseño del cartel y material promocional de la edición.", "categoria": "diseño-grafico", - "imagen": "/assets/proyectos/posters.jpg" + "imagen": "/assets/proyectos/posters.jpg", + "link": "/proyectos/premio-macula-2024#carteleria" } ], "contenido": "

Segundo Premio Mácula 2024

Cortometraje documental galardonado con el Segundo Premio.

Ficha técnica:

  • Formato: Documental
  • Duración: 12 minutos
  • Postproducción: DaVinci Resolve
\"Trabajo excepcional\" - Jurado Festival Mácula
" diff --git a/src/pages/index.astro b/src/pages/index.astro index f3f4715..3f35fca 100644 --- a/src/pages/index.astro +++ b/src/pages/index.astro @@ -295,25 +295,32 @@ const rutaEntries = cronologia.map((p, i) => { - {proyecto.subs.length > 0 && ( -
- {proyecto.subs.map((s) => ( -
- {s.cat && {s.cat.titulo}} - {s.imagen && ( -
- {s.titulo} + {proyecto.subs.length > 0 && ( +
+ {proyecto.subs.map((s) => { + const Tag = s.link ? 'a' : 'div'; + return ( + + {s.cat && {s.cat.titulo}} + {s.imagen && ( +
+ {s.titulo} +
+ )} +
+ {s.titulo} + {s.descripcion &&

{s.descripcion}

} + {s.fechaTexto && {s.fechaTexto}}
- )} -
- {s.titulo} - {s.descripcion &&

{s.descripcion}

} - {s.fechaTexto && {s.fechaTexto}} -
-
- ))} -
- )} + + ); + })} +
+ )}
))} diff --git a/src/styles/proyectos.css b/src/styles/proyectos.css index 63ff178..ea4088a 100644 --- a/src/styles/proyectos.css +++ b/src/styles/proyectos.css @@ -663,6 +663,29 @@ backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px); box-shadow: 0 8px 26px rgba(0, 0, 0, 0.35); + transition: transform 0.3s ease, border-color 0.3s ease, box-shadow 0.3s ease; + will-change: transform; + cursor: default; +} + +/* Subtarjetas clickeables */ +.ruta-sub[href] { + cursor: pointer; + text-decoration: none; +} + +.ruta-sub[href]:hover, +.ruta-sub[href]:focus-visible { + transform: translateY(-4px); + border-color: color-mix(in srgb, var(--cat-color, #93c5fd) 60%, transparent); + box-shadow: + 0 12px 32px rgba(0, 0, 0, 0.45), + 0 0 24px color-mix(in srgb, var(--cat-color, #93c5fd) 20%, transparent); +} + +.ruta-sub[href]:focus-visible { + outline: 2px solid var(--cat-color, #93c5fd); + outline-offset: 3px; } /* Conector hacia la tarjeta principal */ From 37cf0106171da3325fedb39aabe89b4706ab2d89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Garc=C3=ADa=20Santamar=C3=ADa?= <69808296+PGSCOM@users.noreply.github.com> Date: Thu, 11 Jun 2026 16:46:32 +0200 Subject: [PATCH 40/49] =?UTF-8?q?Dise=C3=B1o=20proyectos=20(A=C3=B1adido?= =?UTF-8?q?=20lineas=20para=20que=20se=20vea=20bien=20que=20los=20subproye?= =?UTF-8?q?ctos=20son=20parte=20de=20un=20proyecto?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/styles/proyectos.css | 72 +++++++++++++++++++++++++++++++++------- 1 file changed, 60 insertions(+), 12 deletions(-) diff --git a/src/styles/proyectos.css b/src/styles/proyectos.css index ea4088a..6fff739 100644 --- a/src/styles/proyectos.css +++ b/src/styles/proyectos.css @@ -643,6 +643,7 @@ /* Subtarjetas: trabajos concretos dentro del proyecto, conectadas al lado */ .ruta-subs { + position: relative; flex: 0 1 320px; min-width: 240px; display: flex; @@ -650,6 +651,55 @@ gap: 16px; } +/* Espina vertical que agrupa todas las subtarjetas bajo el proyecto padre. + Va en el CENTRO del hueco (gap 34px → -17px) para que nunca quede + tapada por la tarjeta padre ni se funda con su borde. */ +.ruta-subs::before { + content: ''; + position: absolute; + top: 14px; + bottom: 14px; + left: -17px; + width: 3px; + border-radius: 3px; + transform: translateX(-50%); + background: linear-gradient( + 180deg, + transparent 0%, + var(--cat-color, #93c5fd) 12%, + var(--cat-color, #93c5fd) 88%, + transparent 100% + ); + box-shadow: 0 0 9px color-mix(in srgb, var(--cat-color, #93c5fd) 55%, transparent); +} + +/* Nodo de unión: punto del que nacen las ramas, a la altura del padre */ +.ruta-subs::after { + content: ''; + position: absolute; + top: 50%; + left: -17px; + width: 13px; + height: 13px; + transform: translate(-50%, -50%); + border-radius: 50%; + background: var(--cat-color, #93c5fd); + border: 2px solid rgba(5, 8, 18, 0.92); + box-shadow: 0 0 14px color-mix(in srgb, var(--cat-color, #93c5fd) 80%, transparent); +} + +.ruta-entry--der .ruta-subs::before { + left: auto; + right: -17px; + transform: translateX(50%); +} + +.ruta-entry--der .ruta-subs::after { + left: auto; + right: -17px; + transform: translate(50%, -50%); +} + .ruta-sub { position: relative; display: flex; @@ -688,26 +738,23 @@ outline-offset: 3px; } -/* Conector hacia la tarjeta principal */ +/* Conector horizontal desde la espina hacia cada subtarjeta */ .ruta-sub::before { content: ''; position: absolute; top: 50%; - left: -35px; - width: 34px; - height: 2px; + left: -17px; + width: 17px; + height: 2.5px; transform: translateY(-50%); - background: linear-gradient(90deg, - color-mix(in srgb, var(--cat-color, #93c5fd) 70%, transparent), - color-mix(in srgb, var(--cat-color, #93c5fd) 22%, transparent)); + border-radius: 2px; + background: var(--cat-color, #93c5fd); + box-shadow: 0 0 6px color-mix(in srgb, var(--cat-color, #93c5fd) 45%, transparent); } .ruta-entry--der .ruta-sub::before { left: auto; - right: -35px; - background: linear-gradient(270deg, - color-mix(in srgb, var(--cat-color, #93c5fd) 70%, transparent), - color-mix(in srgb, var(--cat-color, #93c5fd) 22%, transparent)); + right: -17px; } .ruta-sub-icon { @@ -880,7 +927,8 @@ } .ruta-sub::before, - .ruta-entry--der .ruta-sub::before { + .ruta-entry--der .ruta-sub::before, + .ruta-subs::before { display: none; } } From f551f7132547be4a4209b721087b21cb124a7c81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Garc=C3=ADa=20Santamar=C3=ADa?= <69808296+PGSCOM@users.noreply.github.com> Date: Thu, 11 Jun 2026 17:16:27 +0200 Subject: [PATCH 41/49] Animaciones siempre activas --- src/scripts/hero.js | 3 ++- src/scripts/preloader.js | 3 ++- src/scripts/proyectos-orbit.js | 3 ++- src/scripts/proyectos-ruta.js | 3 ++- 4 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/scripts/hero.js b/src/scripts/hero.js index cb8e4e7..affac9c 100644 --- a/src/scripts/hero.js +++ b/src/scripts/hero.js @@ -8,7 +8,8 @@ gsap.registerPlugin(ScrollTrigger); // .hero-card-tilt → rotación base + flotación orgánica (GSAP) // .hero-card-media → hover scale (CSS) -const reduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches; +// Animaciones siempre activas: ignoramos deliberadamente prefers-reduced-motion +const reduced = false; function initHero() { const cards = gsap.utils.toArray('.hero-card'); diff --git a/src/scripts/preloader.js b/src/scripts/preloader.js index 3ed6497..f101a8d 100644 --- a/src/scripts/preloader.js +++ b/src/scripts/preloader.js @@ -27,7 +27,8 @@ function initPreloader() { window.scrollTo(0, 0); document.documentElement.style.overflow = 'hidden'; - const reduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches; + // Animaciones siempre activas: ignoramos deliberadamente prefers-reduced-motion + const reduced = false; if (reduced) { gsap.set(fill, { opacity: 1 }); gsap.set(draw, { drawSVG: '100%', visibility: 'visible' }); diff --git a/src/scripts/proyectos-orbit.js b/src/scripts/proyectos-orbit.js index b19c229..80c11ba 100644 --- a/src/scripts/proyectos-orbit.js +++ b/src/scripts/proyectos-orbit.js @@ -12,7 +12,8 @@ function initOrbit() { const cards = gsap.utils.toArray('.proyecto-card', ring); if (!cards.length) return; - const reduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches; + // Animaciones siempre activas: ignoramos deliberadamente prefers-reduced-motion + const reduced = false; const proxy = { a: 0 }; let radius = 0; diff --git a/src/scripts/proyectos-ruta.js b/src/scripts/proyectos-ruta.js index 6d44c8b..7797ad8 100644 --- a/src/scripts/proyectos-ruta.js +++ b/src/scripts/proyectos-ruta.js @@ -14,7 +14,8 @@ function initRuta() { const svg = mapa?.querySelector('.ruta-linea'); if (!mapa || !cuerpo || !svg) return; - const reduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches; + // Animaciones siempre activas: ignoramos deliberadamente prefers-reduced-motion + const reduced = false; const NS = 'http://www.w3.org/2000/svg'; const glow = document.createElementNS(NS, 'path'); const trazo = document.createElementNS(NS, 'path'); From 5fe4344171c6f5cc23c72b06cc80b5b5d3b00d23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Garc=C3=ADa=20Santamar=C3=ADa?= <69808296+PGSCOM@users.noreply.github.com> Date: Thu, 11 Jun 2026 18:12:39 +0200 Subject: [PATCH 42/49] =?UTF-8?q?Mejorar=20dise=C3=B1o=20y=20funcionalidad?= =?UTF-8?q?=20de=20subtarjetas=20en=20la=20secci=C3=B3n=20de=20proyectos?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/index.astro | 55 +++---- src/scripts/proyectos-ruta.js | 6 + src/styles/proyectos.css | 283 ++++++++++++++++++++-------------- 3 files changed, 199 insertions(+), 145 deletions(-) diff --git a/src/pages/index.astro b/src/pages/index.astro index 3f35fca..b0dadc6 100644 --- a/src/pages/index.astro +++ b/src/pages/index.astro @@ -295,32 +295,35 @@ const rutaEntries = cronologia.map((p, i) => { - {proyecto.subs.length > 0 && ( -
- {proyecto.subs.map((s) => { - const Tag = s.link ? 'a' : 'div'; - return ( - - {s.cat && {s.cat.titulo}} - {s.imagen && ( -
- {s.titulo} -
- )} -
- {s.titulo} - {s.descripcion &&

{s.descripcion}

} - {s.fechaTexto && {s.fechaTexto}} -
-
- ); - })} -
- )} + {proyecto.subs.length > 0 && ( +
+ {proyecto.subs.map((s) => { + const Tag = s.link ? 'a' : 'div'; + return ( + +
+ {s.cat && } + {s.imagen && } + {s.imagen && s.cat && ( + + {s.cat.titulo} + + )} +
+
+ {s.fechaTexto && {s.fechaTexto}} + {s.titulo} + {s.descripcion &&

{s.descripcion}

} +
+
+ ); + })} +
+ )} ))} diff --git a/src/scripts/proyectos-ruta.js b/src/scripts/proyectos-ruta.js index 7797ad8..d75535f 100644 --- a/src/scripts/proyectos-ruta.js +++ b/src/scripts/proyectos-ruta.js @@ -194,6 +194,12 @@ function initRuta() { else img.addEventListener('error', fallar, { once: true }); }); + cuerpo.querySelectorAll('.ruta-sub-img').forEach((img) => { + const fallar = () => img.closest('.ruta-sub')?.classList.add('sin-imagen'); + if (img.complete && img.naturalWidth === 0) fallar(); + else img.addEventListener('error', fallar, { once: true }); + }); + // ── Filtro por disciplina: atenúa y desatura, no oculta ── const filtros = mapa.querySelectorAll('.ruta-filtro'); const entries = cuerpo.querySelectorAll('.ruta-entry'); diff --git a/src/styles/proyectos.css b/src/styles/proyectos.css index 6fff739..1b004b0 100644 --- a/src/styles/proyectos.css +++ b/src/styles/proyectos.css @@ -641,188 +641,210 @@ color: #bfdbfe; } -/* Subtarjetas: trabajos concretos dentro del proyecto, conectadas al lado */ +/* ── Subtarjetas: trabajos concretos que cuelgan del proyecto ───────────── + Columna conectada al lateral de la tarjeta padre por un raíl vertical en + el color del proyecto. Cada subtarjeta es una "estación" del raíl: un + punto en su color de categoría y un ramal corto hasta la tarjeta. El + hueco del raíl lo reserva el padding lateral de .ruta-subs. */ .ruta-subs { position: relative; - flex: 0 1 320px; - min-width: 240px; + flex: 0 1 340px; + min-width: 250px; display: flex; flex-direction: column; - gap: 16px; + gap: 14px; + padding-left: 26px; } -/* Espina vertical que agrupa todas las subtarjetas bajo el proyecto padre. - Va en el CENTRO del hueco (gap 34px → -17px) para que nunca quede - tapada por la tarjeta padre ni se funda con su borde. */ +.ruta-entry--der .ruta-subs { + padding-left: 0; + padding-right: 26px; +} + +/* Raíl vertical en el color del proyecto padre, desvanecido en los extremos */ .ruta-subs::before { content: ''; position: absolute; - top: 14px; - bottom: 14px; - left: -17px; - width: 3px; - border-radius: 3px; - transform: translateX(-50%); + top: 8px; + bottom: 8px; + left: 5px; + width: 2px; + border-radius: 2px; background: linear-gradient( 180deg, transparent 0%, - var(--cat-color, #93c5fd) 12%, - var(--cat-color, #93c5fd) 88%, + color-mix(in srgb, var(--cat-color, #93c5fd) 80%, transparent) 16%, + color-mix(in srgb, var(--cat-color, #93c5fd) 80%, transparent) 84%, transparent 100% ); - box-shadow: 0 0 9px color-mix(in srgb, var(--cat-color, #93c5fd) 55%, transparent); + box-shadow: 0 0 10px color-mix(in srgb, var(--cat-color, #93c5fd) 40%, transparent); +} + +.ruta-entry--der .ruta-subs::before { + left: auto; + right: 5px; } -/* Nodo de unión: punto del que nacen las ramas, a la altura del padre */ +/* Nodo de unión con el padre: rombo a su altura (la entrada centra en vertical) */ .ruta-subs::after { content: ''; position: absolute; top: 50%; - left: -17px; - width: 13px; - height: 13px; - transform: translate(-50%, -50%); - border-radius: 50%; + left: 6px; + width: 11px; + height: 11px; + transform: translate(-50%, -50%) rotate(45deg); + border-radius: 2px; background: var(--cat-color, #93c5fd); border: 2px solid rgba(5, 8, 18, 0.92); - box-shadow: 0 0 14px color-mix(in srgb, var(--cat-color, #93c5fd) 80%, transparent); -} - -.ruta-entry--der .ruta-subs::before { - left: auto; - right: -17px; - transform: translateX(50%); + box-shadow: 0 0 14px color-mix(in srgb, var(--cat-color, #93c5fd) 75%, transparent); } .ruta-entry--der .ruta-subs::after { left: auto; - right: -17px; - transform: translate(50%, -50%); + right: 6px; + transform: translate(50%, -50%) rotate(45deg); } .ruta-sub { position: relative; display: flex; - gap: 10px; - align-items: flex-start; - padding: 13px 15px; - border-radius: 14px; - background: rgba(10, 14, 28, 0.78); - border: 1px solid rgba(255, 255, 255, 0.12); - border-left: 3px solid var(--cat-color, rgba(147, 197, 253, 0.55)); + align-items: center; + gap: 12px; + padding: 12px; + border-radius: 16px; + background: linear-gradient(160deg, rgba(16, 22, 42, 0.85), rgba(8, 12, 26, 0.92)); + border: 1px solid rgba(255, 255, 255, 0.1); backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px); - box-shadow: 0 8px 26px rgba(0, 0, 0, 0.35); - transition: transform 0.3s ease, border-color 0.3s ease, box-shadow 0.3s ease; - will-change: transform; + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.35); + transition: border-color 0.3s ease, box-shadow 0.3s ease, background 0.3s ease; cursor: default; } -/* Subtarjetas clickeables */ -.ruta-sub[href] { - cursor: pointer; - text-decoration: none; -} - -.ruta-sub[href]:hover, -.ruta-sub[href]:focus-visible { - transform: translateY(-4px); - border-color: color-mix(in srgb, var(--cat-color, #93c5fd) 60%, transparent); - box-shadow: - 0 12px 32px rgba(0, 0, 0, 0.45), - 0 0 24px color-mix(in srgb, var(--cat-color, #93c5fd) 20%, transparent); -} - -.ruta-sub[href]:focus-visible { - outline: 2px solid var(--cat-color, #93c5fd); - outline-offset: 3px; -} - -/* Conector horizontal desde la espina hacia cada subtarjeta */ +/* Ramal desde el raíl hasta la subtarjeta, en el color de su categoría */ .ruta-sub::before { content: ''; position: absolute; top: 50%; - left: -17px; - width: 17px; - height: 2.5px; + left: -20px; + width: 20px; + height: 2px; transform: translateY(-50%); border-radius: 2px; + background: color-mix(in srgb, var(--cat-color, #93c5fd) 80%, transparent); +} + +/* Estación: punto sobre el raíl a la altura de la subtarjeta */ +.ruta-sub::after { + content: ''; + position: absolute; + top: 50%; + left: -20px; + width: 8px; + height: 8px; + transform: translate(-50%, -50%); + border-radius: 50%; background: var(--cat-color, #93c5fd); - box-shadow: 0 0 6px color-mix(in srgb, var(--cat-color, #93c5fd) 45%, transparent); + border: 2px solid rgba(5, 8, 18, 0.92); + box-shadow: 0 0 10px color-mix(in srgb, var(--cat-color, #93c5fd) 60%, transparent); } .ruta-entry--der .ruta-sub::before { left: auto; - right: -17px; -} - -.ruta-sub-icon { - width: 22px; - height: 22px; - flex-shrink: 0; - margin-top: 2px; - filter: drop-shadow(0 1px 4px rgba(0, 0, 0, 0.5)); + right: -20px; } -.ruta-sub-titulo { - display: block; - font-family: 'Chakra Petch', sans-serif; - font-size: 0.85rem; - font-weight: 600; - color: white; - line-height: 1.3; +.ruta-entry--der .ruta-sub::after { + left: auto; + right: -20px; + transform: translate(50%, -50%); } -.ruta-sub-desc { - font-family: 'Chakra Petch', sans-serif; - font-size: 0.76rem; - color: rgba(255, 255, 255, 0.7); - line-height: 1.45; - margin: 4px 0 0; +/* Subtarjetas clickeables: sin transform, para que la estación no se + despegue del raíl al hacer hover */ +.ruta-sub[href] { + cursor: pointer; + text-decoration: none; } -.ruta-sub-fecha { - display: inline-block; - font-family: 'Chakra Petch', sans-serif; - font-size: 0.65rem; - font-weight: 600; - letter-spacing: 0.08em; - text-transform: uppercase; - color: var(--cat-color, #93c5fd); - margin-top: 6px; +.ruta-sub[href]:hover, +.ruta-sub[href]:focus-visible { + border-color: color-mix(in srgb, var(--cat-color, #93c5fd) 60%, transparent); + background: linear-gradient(160deg, rgba(22, 30, 56, 0.9), rgba(10, 15, 32, 0.95)); + box-shadow: + 0 12px 34px rgba(0, 0, 0, 0.45), + 0 0 22px color-mix(in srgb, var(--cat-color, #93c5fd) 22%, transparent); } -/* Subtarjetas con imagen - thumbnail horizontal compactor */ -.ruta-sub--con-img { - padding: 10px 12px; - gap: 12px; - align-items: center; +.ruta-sub[href]:focus-visible { + outline: 2px solid var(--cat-color, #93c5fd); + outline-offset: 3px; } -.ruta-sub--con-img .ruta-sub-media { - width: 86px; - height: 54px; - border-radius: 8px; - overflow: hidden; +/* Miniatura cuadrada; si no hay imagen (o falla) muestra el icono de la + categoría sobre un fondo tintado, como las tarjetas principales */ +.ruta-sub-media { + position: relative; + width: 64px; + height: 64px; flex-shrink: 0; + border-radius: 12px; + overflow: hidden; border: 1px solid rgba(255, 255, 255, 0.1); - box-shadow: 0 3px 10px rgba(0, 0, 0, 0.3); + background: + radial-gradient(ellipse at 50% 40%, color-mix(in srgb, var(--cat-color, #93c5fd) 26%, transparent), transparent 75%), + linear-gradient(135deg, rgba(20, 26, 48, 0.95), rgba(8, 12, 26, 0.98)); } -.ruta-sub--con-img .ruta-sub-media img { +.ruta-sub-fallback { + position: absolute; + left: 50%; + top: 50%; + transform: translate(-50%, -50%); + width: 55%; + opacity: 0.9; + filter: drop-shadow(0 2px 8px rgba(0, 0, 0, 0.5)); +} + +.ruta-sub-img { + position: absolute; + inset: 0; width: 100%; height: 100%; object-fit: cover; - display: block; transition: transform 0.35s ease; } -.ruta-sub--con-img:hover .ruta-sub-media img { - transform: scale(1.06); +.ruta-sub[href]:hover .ruta-sub-img { + transform: scale(1.07); +} + +.ruta-sub.sin-imagen .ruta-sub-img, +.ruta-sub.sin-imagen .ruta-sub-cat { + display: none; } -.ruta-sub--con-img .ruta-sub-body { +/* Insignia de categoría sobre la esquina de la miniatura */ +.ruta-sub-cat { + position: absolute; + right: 3px; + bottom: 3px; + display: grid; + place-items: center; + width: 20px; + height: 20px; + border-radius: 7px; + background: rgba(5, 8, 18, 0.85); + border: 1px solid color-mix(in srgb, var(--cat-color, #93c5fd) 55%, transparent); +} + +.ruta-sub-cat img { + width: 12px; + height: 12px; +} + +.ruta-sub-body { flex: 1; min-width: 0; display: flex; @@ -830,10 +852,29 @@ gap: 3px; } -.ruta-sub--con-img .ruta-sub-icon { - width: 18px; - height: 18px; - margin-top: 1px; +.ruta-sub-fecha { + font-family: 'Chakra Petch', sans-serif; + font-size: 0.6rem; + font-weight: 700; + letter-spacing: 0.1em; + text-transform: uppercase; + color: var(--cat-color, #93c5fd); +} + +.ruta-sub-titulo { + font-family: 'Chakra Petch', sans-serif; + font-size: 0.88rem; + font-weight: 600; + color: white; + line-height: 1.3; +} + +.ruta-sub-desc { + font-family: 'Chakra Petch', sans-serif; + font-size: 0.74rem; + color: rgba(255, 255, 255, 0.68); + line-height: 1.45; + margin: 0; } /* ============================================ @@ -916,6 +957,8 @@ .ruta-subs { flex: 1 1 100%; max-width: var(--w, 500px); + padding-left: 0; + padding-right: 0; } .ruta-entry--izq .ruta-subs { @@ -926,9 +969,11 @@ margin-left: auto; } + /* Sin raíl ni estaciones cuando las subtarjetas pasan debajo */ + .ruta-subs::before, + .ruta-subs::after, .ruta-sub::before, - .ruta-entry--der .ruta-sub::before, - .ruta-subs::before { + .ruta-sub::after { display: none; } } From 5f4068f38c9062406b84eebd5710a9ff109401e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Garc=C3=ADa=20Santamar=C3=ADa?= <69808296+PGSCOM@users.noreply.github.com> Date: Thu, 11 Jun 2026 18:15:40 +0200 Subject: [PATCH 43/49] Smooth scroll --- package-lock.json | 32 ++++++++++++++++++++++++++++++++ package.json | 1 + src/pages/_styles.css | 1 + src/pages/index.astro | 1 + src/scripts/proyectos-ruta.js | 3 ++- src/scripts/smooth-scroll.js | 28 ++++++++++++++++++++++++++++ 6 files changed, 65 insertions(+), 1 deletion(-) create mode 100644 src/scripts/smooth-scroll.js diff --git a/package-lock.json b/package-lock.json index 160188f..640c3c7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "@types/react-dom": "^19.1.9", "astro": "^5.15.1", "gsap": "^3.15.0", + "lenis": "^1.3.23", "liquid-glass-react": "^1.1.1", "react": "^19.1.1", "react-dom": "^19.1.1" @@ -3184,6 +3185,37 @@ "node": ">=6" } }, + "node_modules/lenis": { + "version": "1.3.23", + "resolved": "https://registry.npmjs.org/lenis/-/lenis-1.3.23.tgz", + "integrity": "sha512-YxYq3TJqj9sJNv0V9SkyQHejt14xwyIwgDaaMK89Uf9SxQfIszu+gTQSSphh6BWlLTNVKvvXAGkg+Zf+oFIevg==", + "license": "MIT", + "workspaces": [ + "packages/*", + "playground", + "playground/*" + ], + "funding": { + "type": "github", + "url": "https://github.com/sponsors/darkroomengineering" + }, + "peerDependencies": { + "@nuxt/kit": ">=3.0.0", + "react": ">=17.0.0", + "vue": ">=3.0.0" + }, + "peerDependenciesMeta": { + "@nuxt/kit": { + "optional": true + }, + "react": { + "optional": true + }, + "vue": { + "optional": true + } + } + }, "node_modules/liquid-glass-react": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/liquid-glass-react/-/liquid-glass-react-1.1.1.tgz", diff --git a/package.json b/package.json index 5a76347..d090d86 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "@types/react-dom": "^19.1.9", "astro": "^5.15.1", "gsap": "^3.15.0", + "lenis": "^1.3.23", "liquid-glass-react": "^1.1.1", "react": "^19.1.1", "react-dom": "^19.1.1" diff --git a/src/pages/_styles.css b/src/pages/_styles.css index f24cccc..b7a056e 100644 --- a/src/pages/_styles.css +++ b/src/pages/_styles.css @@ -1,4 +1,5 @@ /* Reset y estilos base */ + * { margin: 0; padding: 0; diff --git a/src/pages/index.astro b/src/pages/index.astro index b0dadc6..a3209e5 100644 --- a/src/pages/index.astro +++ b/src/pages/index.astro @@ -595,6 +595,7 @@ const rutaEntries = cronologia.map((p, i) => {