diff --git a/public/css/sub-pages.css b/public/css/sub-pages.css index 1630bbaa1..3feffd4d3 100644 --- a/public/css/sub-pages.css +++ b/public/css/sub-pages.css @@ -997,16 +997,14 @@ main#main { .split-comparison { position: relative; width: 100%; - max-width: 560px; - margin: 0 auto clamp(2rem, 4vw, 3rem); + max-width: 500px; + margin: 0 0 clamp(2rem, 4vw, 3rem); } .split-container { position: relative; width: 100%; - max-width: 500px; height: 360px; - margin: 0 auto; border-radius: 12px; overflow: hidden; background: var(--color-cream); @@ -1060,7 +1058,7 @@ main#main { display: flex; justify-content: space-between; align-items: center; - margin-top: var(--spacing-sm); + margin-top: 10px; font-family: var(--font-mono); font-size: 0.6875rem; font-weight: 600; @@ -1078,11 +1076,11 @@ main#main { } .skill-demo-caption { - text-align: center; font-size: 0.875rem; color: var(--color-charcoal); margin-top: var(--spacing-md); font-style: italic; + max-width: 500px; } .skill-demo-eyebrow { @@ -1092,7 +1090,6 @@ main#main { text-transform: uppercase; letter-spacing: 0.14em; color: var(--color-ash); - text-align: center; margin-bottom: var(--spacing-sm); } diff --git a/scripts/build-sub-pages.js b/scripts/build-sub-pages.js index 530244f8c..4d5e61db5 100644 --- a/scripts/build-sub-pages.js +++ b/scripts/build-sub-pages.js @@ -39,7 +39,7 @@ function renderSkillDemo(skill) { const { before, after, caption } = skill.demo; return `
-

Drag to compare

+

Drag or hover to compare

diff --git a/scripts/lib/render-page.js b/scripts/lib/render-page.js index 038eed930..cdb6cd0e3 100644 --- a/scripts/lib/render-page.js +++ b/scripts/lib/render-page.js @@ -115,19 +115,20 @@ ${bodyHtml} }).catch(() => {}); }); - // Lightweight split-compare interaction for before/after demos. - // Drag or hover horizontally over .split-container to sweep the - // divider. No lerp, no ResizeObserver — good enough for docs. + // Before/after split-compare: drag on touch, hover OR drag on mouse. + // On mouse-leave the divider eases back to the default position. (function initSplitCompare() { const containers = document.querySelectorAll('.split-container'); if (containers.length === 0) return; + const hasHover = matchMedia('(hover: hover)').matches; + const DEFAULT_POSITION = 50; + for (const container of containers) { const splitAfter = container.querySelector('.split-after'); const splitDivider = container.querySelector('.split-divider'); if (!splitAfter || !splitDivider) continue; - const skewAngle = 10 * Math.PI / 180; - const tanAngle = Math.tan(skewAngle); + const tanAngle = Math.tan(10 * Math.PI / 180); let skewOffset = 8; const recalcSkew = () => { const r = container.getBoundingClientRect(); @@ -138,36 +139,78 @@ ${bodyHtml} recalcSkew(); window.addEventListener('resize', recalcSkew, { passive: true }); - const update = (pct) => { + let targetX = DEFAULT_POSITION; + let currentX = DEFAULT_POSITION; + let rafId = null; + + const paint = (pct) => { const x = Math.max(-skewOffset, Math.min(100 + skewOffset, pct)); - splitAfter.style.clipPath = \`polygon(\${x + skewOffset}% 0%, 100% 0%, 100% 100%, \${x - skewOffset}% 100%)\`; + splitAfter.style.clipPath = + \`polygon(\${x + skewOffset}% 0%, 100% 0%, 100% 100%, \${x - skewOffset}% 100%)\`; splitDivider.style.left = \`\${x}%\`; }; - update(50); - let tracking = false; - const onMove = (clientX) => { + const step = () => { + currentX += (targetX - currentX) * 0.2; + if (Math.abs(targetX - currentX) < 0.1) { + currentX = targetX; + rafId = null; + } else { + rafId = requestAnimationFrame(step); + } + paint(currentX); + }; + + const setTarget = (pct) => { + targetX = pct; + if (rafId === null) rafId = requestAnimationFrame(step); + }; + + paint(DEFAULT_POSITION); + + const pctFromClientX = (clientX) => { const rect = container.getBoundingClientRect(); - const pct = ((clientX - rect.left) / rect.width) * 100; - update(pct); + return ((clientX - rect.left) / rect.width) * 100; }; + + let hovering = false; + let dragging = false; + + container.addEventListener('pointerenter', (e) => { + if (hasHover && e.pointerType === 'mouse') { + hovering = true; + } + }); + container.addEventListener('pointerdown', (e) => { - tracking = true; + dragging = true; container.setPointerCapture(e.pointerId); - onMove(e.clientX); + setTarget(pctFromClientX(e.clientX)); }); + container.addEventListener('pointermove', (e) => { - if (!tracking) return; - onMove(e.clientX); + if (dragging || hovering) { + setTarget(pctFromClientX(e.clientX)); + } }); - const stop = (e) => { - if (!tracking) return; - tracking = false; - try { container.releasePointerCapture(e.pointerId); } catch {} + + const endDrag = (e) => { + if (dragging) { + dragging = false; + try { container.releasePointerCapture(e.pointerId); } catch {} + } }; - container.addEventListener('pointerup', stop); - container.addEventListener('pointercancel', stop); - container.addEventListener('pointerleave', stop); + + container.addEventListener('pointerup', endDrag); + container.addEventListener('pointercancel', endDrag); + + container.addEventListener('pointerleave', (e) => { + endDrag(e); + if (hovering) { + hovering = false; + setTarget(DEFAULT_POSITION); + } + }); } })();