feat(site): add anime.js v4 magic background, remove Three.js

Merge anime.js magic background changes to trigger deployment to GitHub Pages and hatch.surf server.
This commit is contained in:
Naranmandakh Enkhbat 2026-06-24 15:21:14 +08:00 committed by GitHub
commit c6552da7e6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 876 additions and 249 deletions

View File

@ -1,6 +1,6 @@
# Hatch Static Site # Hatch Static Site
This directory contains the static site for [hatch.sh](https://hatch.sh). This directory contains the static site for [hatch.surf](https://hatch.surf).
## Structure ## Structure
@ -29,11 +29,11 @@ The site is automatically deployed to GitHub Pages via the `deploy-site.yml` wor
1. Go to repository Settings → Pages 1. Go to repository Settings → Pages
2. Source: Deploy from a branch 2. Source: Deploy from a branch
3. Branch: `main`, folder: `/site` 3. Branch: `main`, folder: `/site`
4. Custom domain: `hatch.sh` 4. Custom domain: `hatch.surf`
### DNS Configuration ### DNS Configuration
To point `hatch.sh` to GitHub Pages: To point `hatch.surf` to GitHub Pages:
1. Add these DNS records: 1. Add these DNS records:
- Type: A, Name: @, Value: 185.199.108.153 - Type: A, Name: @, Value: 185.199.108.153

View File

@ -9,21 +9,38 @@
<!-- Open Graph --> <!-- Open Graph -->
<meta property="og:title" content="Blog — Hatch"> <meta property="og:title" content="Blog — Hatch">
<meta property="og:description" content="Blog posts about Hatch, a self-hostable HTTP request inspector and mocker."> <meta property="og:description" content="Blog posts about Hatch, a self-hostable HTTP request inspector and mocker.">
<meta property="og:image" content="https://hatch.sh/brand/og/default.png"> <meta property="og:image" content="https://hatch.surf/brand/og/default.png">
<meta property="og:url" content="https://hatch.sh/blog/"> <meta property="og:url" content="https://hatch.surf/blog/">
<meta property="og:type" content="website"> <meta property="og:type" content="website">
<!-- Twitter Card --> <!-- Twitter Card -->
<meta name="twitter:card" content="summary_large_image"> <meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="Blog — Hatch"> <meta name="twitter:title" content="Blog — Hatch">
<meta name="twitter:description" content="Blog posts about Hatch, a self-hostable HTTP request inspector and mocker."> <meta name="twitter:description" content="Blog posts about Hatch, a self-hostable HTTP request inspector and mocker.">
<meta name="twitter:image" content="https://hatch.sh/brand/og/default.png"> <meta name="twitter:image" content="https://hatch.surf/brand/og/default.png">
<link rel="canonical" href="https://hatch.sh/blog/"> <link rel="canonical" href="https://hatch.surf/blog/">
<link rel="icon" type="image/png" href="/brand/favicon/favicon.png"> <link rel="icon" type="image/png" href="/brand/favicon/favicon.png">
<link rel="stylesheet" href="/style.css"> <link rel="stylesheet" href="/style.css">
</head> </head>
<body> <body>
<!-- Anime.js magical background layers -->
<div id="magic-bg" aria-hidden="true">
<div class="magic-aurora"></div>
<div class="magic-aurora magic-aurora--2"></div>
<div class="magic-aurora magic-aurora--3"></div>
<svg class="magic-runes" viewBox="0 0 800 800" xmlns="http://www.w3.org/2000/svg">
<circle class="rune rune-outer" cx="400" cy="400" r="350" />
<circle class="rune rune-mid" cx="400" cy="400" r="260" />
<circle class="rune rune-inner" cx="400" cy="400" r="170" />
<circle class="rune rune-core" cx="400" cy="400" r="80" />
</svg>
<div class="magic-orbs" id="magic-orbs"></div>
<svg class="magic-connections" id="magic-connections"></svg>
<div class="magic-sparkles" id="magic-sparkles"></div>
<div class="magic-cursor-glow" id="magic-cursor-glow"></div>
</div>
<header> <header>
<div class="container"> <div class="container">
<a href="/" class="logo">Hatch</a> <a href="/" class="logo">Hatch</a>
@ -56,5 +73,9 @@
<p>&copy; 2026 El Foundation. <a href="https://github.com/elfoundation/hatch">Hatch</a> is released under the <a href="https://github.com/elfoundation/hatch/blob/main/LICENSE">MIT License</a>.</p> <p>&copy; 2026 El Foundation. <a href="https://github.com/elfoundation/hatch">Hatch</a> is released under the <a href="https://github.com/elfoundation/hatch/blob/main/LICENSE">MIT License</a>.</p>
</div> </div>
</footer> </footer>
<!-- Anime.js v4 (UMD) -->
<script src="https://cdn.jsdelivr.net/npm/animejs@4/dist/bundles/anime.umd.min.js"></script>
<script src="/main.js" defer></script>
</body> </body>
</html> </html>

View File

@ -9,8 +9,8 @@
<!-- Open Graph --> <!-- Open Graph -->
<meta property="og:title" content="Why we are building Hatch"> <meta property="og:title" content="Why we are building Hatch">
<meta property="og:description" content="Hatch is a self-hostable HTTP request inspector and mocker. One Go binary, SQLite, no SaaS, no per-request fee. Open source, MIT."> <meta property="og:description" content="Hatch is a self-hostable HTTP request inspector and mocker. One Go binary, SQLite, no SaaS, no per-request fee. Open source, MIT.">
<meta property="og:image" content="https://hatch.sh/brand/og/default.png"> <meta property="og:image" content="https://hatch.surf/brand/og/default.png">
<meta property="og:url" content="https://hatch.sh/blog/why-we-are-building-hatch"> <meta property="og:url" content="https://hatch.surf/blog/why-we-are-building-hatch">
<meta property="og:type" content="article"> <meta property="og:type" content="article">
<meta property="article:published_time" content="2026-06-23"> <meta property="article:published_time" content="2026-06-23">
<meta property="article:author" content="El Foundation"> <meta property="article:author" content="El Foundation">
@ -19,13 +19,30 @@
<meta name="twitter:card" content="summary_large_image"> <meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="Why we are building Hatch"> <meta name="twitter:title" content="Why we are building Hatch">
<meta name="twitter:description" content="Hatch is a self-hostable HTTP request inspector and mocker. One Go binary, SQLite, no SaaS, no per-request fee. Open source, MIT."> <meta name="twitter:description" content="Hatch is a self-hostable HTTP request inspector and mocker. One Go binary, SQLite, no SaaS, no per-request fee. Open source, MIT.">
<meta name="twitter:image" content="https://hatch.sh/brand/og/default.png"> <meta name="twitter:image" content="https://hatch.surf/brand/og/default.png">
<link rel="canonical" href="https://hatch.sh/blog/why-we-are-building-hatch"> <link rel="canonical" href="https://hatch.surf/blog/why-we-are-building-hatch">
<link rel="icon" type="image/png" href="/brand/favicon/favicon.png"> <link rel="icon" type="image/png" href="/brand/favicon/favicon.png">
<link rel="stylesheet" href="/style.css"> <link rel="stylesheet" href="/style.css">
</head> </head>
<body> <body>
<!-- Anime.js magical background layers -->
<div id="magic-bg" aria-hidden="true">
<div class="magic-aurora"></div>
<div class="magic-aurora magic-aurora--2"></div>
<div class="magic-aurora magic-aurora--3"></div>
<svg class="magic-runes" viewBox="0 0 800 800" xmlns="http://www.w3.org/2000/svg">
<circle class="rune rune-outer" cx="400" cy="400" r="350" />
<circle class="rune rune-mid" cx="400" cy="400" r="260" />
<circle class="rune rune-inner" cx="400" cy="400" r="170" />
<circle class="rune rune-core" cx="400" cy="400" r="80" />
</svg>
<div class="magic-orbs" id="magic-orbs"></div>
<svg class="magic-connections" id="magic-connections"></svg>
<div class="magic-sparkles" id="magic-sparkles"></div>
<div class="magic-cursor-glow" id="magic-cursor-glow"></div>
</div>
<header> <header>
<div class="container"> <div class="container">
<a href="/" class="logo">Hatch</a> <a href="/" class="logo">Hatch</a>
@ -116,5 +133,9 @@ docker compose up</code></pre>
<p>&copy; 2026 El Foundation. <a href="https://github.com/elfoundation/hatch">Hatch</a> is released under the <a href="https://github.com/elfoundation/hatch/blob/main/LICENSE">MIT License</a>.</p> <p>&copy; 2026 El Foundation. <a href="https://github.com/elfoundation/hatch">Hatch</a> is released under the <a href="https://github.com/elfoundation/hatch/blob/main/LICENSE">MIT License</a>.</p>
</div> </div>
</footer> </footer>
<!-- Anime.js v4 (UMD) -->
<script src="https://cdn.jsdelivr.net/npm/animejs@4/dist/bundles/anime.umd.min.js"></script>
<script src="/main.js" defer></script>
</body> </body>
</html> </html>

View File

@ -30,8 +30,22 @@
<link rel="stylesheet" href="/style.css"> <link rel="stylesheet" href="/style.css">
</head> </head>
<body> <body>
<!-- Three.js canvas for animated particle background --> <!-- Anime.js magical background layers -->
<canvas id="particle-canvas" aria-hidden="true"></canvas> <div id="magic-bg" aria-hidden="true">
<div class="magic-aurora"></div>
<div class="magic-aurora magic-aurora--2"></div>
<div class="magic-aurora magic-aurora--3"></div>
<svg class="magic-runes" viewBox="0 0 800 800" xmlns="http://www.w3.org/2000/svg">
<circle class="rune rune-outer" cx="400" cy="400" r="350" />
<circle class="rune rune-mid" cx="400" cy="400" r="260" />
<circle class="rune rune-inner" cx="400" cy="400" r="170" />
<circle class="rune rune-core" cx="400" cy="400" r="80" />
</svg>
<div class="magic-orbs" id="magic-orbs"></div>
<svg class="magic-connections" id="magic-connections"></svg>
<div class="magic-sparkles" id="magic-sparkles"></div>
<div class="magic-cursor-glow" id="magic-cursor-glow"></div>
</div>
<header> <header>
<div class="container"> <div class="container">
@ -162,9 +176,7 @@
</div> </div>
</footer> </footer>
<!-- Libraries --> <script src="https://cdn.jsdelivr.net/npm/animejs@4/dist/bundles/anime.umd.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script> <script src="/main.js" defer></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/animejs/3.2.2/anime.min.js"></script>
<script src="/main.js"></script>
</body> </body>
</html> </html>

View File

@ -1,266 +1,440 @@
/** /**
* Hatch Homepage v2 Three.js particle network + anime.js entrance animations * Hatch Homepage Anime.js Magic Background + Entrance Animations
*
* Multi-layered anime.js-powered magical background:
* 1. Aurora blobs soft gradient spheres that drift and shift color
* 2. Rune circles SVG rings rotating at different speeds
* 3. Floating orbs luminous dots with organic drift
* 4. Energy connections lines between nearby orbs that pulse
* 5. Sparkle particles twinkling dots with staggered opacity
* 6. Cursor glow radial glow that follows the mouse
* *
* Particle network: nodes float gently, connected by proximity lines.
* Represents HTTP requests flowing through the network.
* Entrance: elements with [data-animate] fade up on scroll. * Entrance: elements with [data-animate] fade up on scroll.
* Performance: orb tick throttled to ~30fps, connections drawn every 3rd frame.
*/ */
(function () { (function () {
'use strict'; 'use strict';
var prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
var isMobile = window.innerWidth < 768;
// ============================================================ // ============================================================
// Three.js Particle Network // 1. Aurora Blobs — slow drift + color pulse
// ============================================================ // ============================================================
const canvas = document.getElementById('particle-canvas'); function initAurora(animate, random) {
if (!canvas) return; var blobs = document.querySelectorAll('.magic-aurora');
blobs.forEach(function (blob, i) {
const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches; animate(blob, {
translateX: [
if (!prefersReducedMotion && typeof THREE !== 'undefined') { { to: random(-40, 40), duration: random(8000, 12000) },
initParticleNetwork(); { to: random(-30, 30), duration: random(8000, 12000) },
{ to: 0, duration: random(8000, 12000) }
],
translateY: [
{ to: random(-30, 30), duration: random(10000, 14000) },
{ to: random(-40, 40), duration: random(10000, 14000) },
{ to: 0, duration: random(10000, 14000) }
],
scale: [
{ to: random(0.85, 1.15), duration: random(6000, 10000) },
{ to: random(0.9, 1.1), duration: random(6000, 10000) },
{ to: 1, duration: random(6000, 10000) }
],
opacity: [
{ to: random(0.06, 0.18), duration: random(5000, 8000) },
{ to: random(0.08, 0.14), duration: random(5000, 8000) },
{ to: i === 0 ? 0.12 : i === 1 ? 0.1 : 0.08, duration: random(5000, 8000) }
],
loop: true,
alternate: true,
ease: 'easeInOutSine'
});
});
} }
function initParticleNetwork() { // ============================================================
const scene = new THREE.Scene(); // 2. Rune Circles — continuous rotation at different speeds
const camera = new THREE.PerspectiveCamera( // ============================================================
60,
window.innerWidth / window.innerHeight,
0.1,
1000
);
camera.position.z = 50;
const renderer = new THREE.WebGLRenderer({ function initRunes(animate, random) {
canvas: canvas, var runeData = [
alpha: true, { sel: '.rune-outer', speed: 120, dir: 1 },
antialias: false, { sel: '.rune-mid', speed: 80, dir: -1 },
}); { sel: '.rune-inner', speed: 60, dir: 1 },
renderer.setSize(window.innerWidth, window.innerHeight); { sel: '.rune-core', speed: 40, dir: -1 }
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); ];
// Particle parameters runeData.forEach(function (r) {
const PARTICLE_COUNT = 80; var el = document.querySelector(r.sel);
const CONNECTION_DISTANCE = 12; if (!el) return;
const PARTICLE_SIZE = 0.15;
// Create particles animate(el, {
const particlesGeometry = new THREE.BufferGeometry(); opacity: parseFloat(getComputedStyle(el).opacity) || 0.06,
const positions = new Float32Array(PARTICLE_COUNT * 3); duration: 2000,
const velocities = []; delay: random(500, 1500),
const opacities = new Float32Array(PARTICLE_COUNT); ease: 'outQuad'
for (let i = 0; i < PARTICLE_COUNT; i++) {
positions[i * 3] = (Math.random() - 0.5) * 100;
positions[i * 3 + 1] = (Math.random() - 0.5) * 60;
positions[i * 3 + 2] = (Math.random() - 0.5) * 30;
velocities.push({
x: (Math.random() - 0.5) * 0.02,
y: (Math.random() - 0.5) * 0.02,
z: (Math.random() - 0.5) * 0.01,
}); });
opacities[i] = 0.3 + Math.random() * 0.5; animate(el, {
rotate: r.dir > 0 ? '1turn' : '-1turn',
duration: r.speed * 1000,
loop: true,
ease: 'linear'
});
});
var core = document.querySelector('.rune-core');
if (core) {
animate(core, {
scale: [
{ to: 1.05, duration: 3000 },
{ to: 0.95, duration: 3000 },
{ to: 1, duration: 3000 }
],
loop: true,
ease: 'easeInOutSine'
});
} }
}
particlesGeometry.setAttribute('position', new THREE.BufferAttribute(positions, 3)); // ============================================================
// 3. Floating Orbs — luminous dots with organic drift
// ============================================================
const particlesMaterial = new THREE.PointsMaterial({ var orbState = [];
color: 0x3b82f6,
size: PARTICLE_SIZE,
transparent: true,
opacity: 0.6,
sizeAttenuation: true,
blending: THREE.AdditiveBlending,
depthWrite: false,
});
const particles = new THREE.Points(particlesGeometry, particlesMaterial); function createOrbs(count, animate, random) {
scene.add(particles); var container = document.getElementById('magic-orbs');
if (!container) return;
// Connection lines var colors = [
const lineMaterial = new THREE.LineBasicMaterial({ 'rgba(59, 130, 246, VAR)',
color: 0x3b82f6, 'rgba(168, 85, 247, VAR)',
transparent: true, 'rgba(6, 182, 212, VAR)',
opacity: 0.08, 'rgba(99, 102, 241, VAR)',
blending: THREE.AdditiveBlending, 'rgba(14, 165, 233, VAR)'
depthWrite: false, ];
});
const linesGroup = new THREE.Group(); for (var i = 0; i < count; i++) {
scene.add(linesGroup); var size = random(3, 8);
var opacity = random(0.15, 0.5);
var colorIdx = Math.min(Math.floor(random(0, colors.length)), colors.length - 1);
var glowIdx = Math.min(Math.floor(random(0, colors.length)), colors.length - 1);
var color = colors[colorIdx].replace('VAR', opacity.toFixed(2));
var glowColor = colors[glowIdx].replace('VAR', (opacity * 0.4).toFixed(2));
// Mouse interaction var el = document.createElement('div');
let mouseX = 0; el.className = 'magic-orb';
let mouseY = 0; el.style.width = size + 'px';
document.addEventListener('mousemove', (e) => { el.style.height = size + 'px';
mouseX = (e.clientX / window.innerWidth - 0.5) * 2; el.style.background = color;
mouseY = -(e.clientY / window.innerHeight - 0.5) * 2; el.style.boxShadow = '0 0 ' + (size * 3) + 'px ' + glowColor;
}); container.appendChild(el);
// Resize orbState.push({
function onResize() { x: random(0, 100),
camera.aspect = window.innerWidth / window.innerHeight; y: random(0, 100),
camera.updateProjectionMatrix(); vx: random(-0.015, 0.015),
renderer.setSize(window.innerWidth, window.innerHeight); vy: random(-0.015, 0.015),
el: el,
size: size
});
el.style.left = orbState[i].x + '%';
el.style.top = orbState[i].y + '%';
animate(el, {
opacity: [0, 1],
scale: [0, 1],
duration: random(800, 1600),
delay: random(200, 2000),
ease: 'outExpo'
});
} }
window.addEventListener('resize', onResize); }
// Animate // Throttled orb tick — runs at ~30fps for performance
function animate() { var lastOrbTick = 0;
requestAnimationFrame(animate); var orbTickInterval = 33; // ~30fps
var frameCount = 0;
const posArray = particlesGeometry.attributes.position.array; function tickOrbs(timestamp) {
requestAnimationFrame(tickOrbs);
// Update particle positions if (timestamp - lastOrbTick < orbTickInterval) return;
for (let i = 0; i < PARTICLE_COUNT; i++) { lastOrbTick = timestamp;
posArray[i * 3] += velocities[i].x; frameCount++;
posArray[i * 3 + 1] += velocities[i].y;
posArray[i * 3 + 2] += velocities[i].z;
// Wrap around edges orbState.forEach(function (orb) {
if (posArray[i * 3] > 55) posArray[i * 3] = -55; orb.x += orb.vx;
if (posArray[i * 3] < -55) posArray[i * 3] = 55; orb.y += orb.vy;
if (posArray[i * 3 + 1] > 35) posArray[i * 3 + 1] = -35;
if (posArray[i * 3 + 1] < -35) posArray[i * 3 + 1] = 35;
if (posArray[i * 3 + 2] > 20) posArray[i * 3 + 2] = -20;
if (posArray[i * 3 + 2] < -20) posArray[i * 3 + 2] = 20;
}
particlesGeometry.attributes.position.needsUpdate = true; if (orb.x < -2 || orb.x > 102) orb.vx *= -1;
if (orb.y < -2 || orb.y > 102) orb.vy *= -1;
// Update connection lines orb.x = Math.max(-2, Math.min(102, orb.x));
while (linesGroup.children.length > 0) { orb.y = Math.max(-2, Math.min(102, orb.y));
const child = linesGroup.children[0];
child.geometry.dispose();
child.material.dispose();
linesGroup.remove(child);
}
for (let i = 0; i < PARTICLE_COUNT; i++) { orb.vx += (Math.random() - 0.5) * 0.0005;
for (let j = i + 1; j < PARTICLE_COUNT; j++) { orb.vy += (Math.random() - 0.5) * 0.0005;
const dx = posArray[i * 3] - posArray[j * 3];
const dy = posArray[i * 3 + 1] - posArray[j * 3 + 1];
const dz = posArray[i * 3 + 2] - posArray[j * 3 + 2];
const dist = Math.sqrt(dx * dx + dy * dy + dz * dz);
if (dist < CONNECTION_DISTANCE) { orb.vx = Math.max(-0.03, Math.min(0.03, orb.vx));
const lineGeometry = new THREE.BufferGeometry(); orb.vy = Math.max(-0.03, Math.min(0.03, orb.vy));
const linePositions = new Float32Array([
posArray[i * 3], posArray[i * 3 + 1], posArray[i * 3 + 2],
posArray[j * 3], posArray[j * 3 + 1], posArray[j * 3 + 2],
]);
lineGeometry.setAttribute('position', new THREE.BufferAttribute(linePositions, 3));
const lineMat = lineMaterial.clone(); orb.el.style.left = orb.x + '%';
lineMat.opacity = 0.06 * (1 - dist / CONNECTION_DISTANCE); orb.el.style.top = orb.y + '%';
linesGroup.add(new THREE.Line(lineGeometry, lineMat)); });
}
// Draw connections every 3rd frame for performance
if (frameCount % 3 === 0) {
drawConnections();
}
}
// ============================================================
// 4. Energy Connections — lines between nearby orbs
// ============================================================
function drawConnections() {
var svg = document.getElementById('magic-connections');
if (!svg) return;
var w = window.innerWidth;
var h = window.innerHeight;
var threshold = isMobile ? 15 : 20;
var lines = '';
for (var i = 0; i < orbState.length; i++) {
for (var j = i + 1; j < orbState.length; j++) {
var dx = orbState[i].x - orbState[j].x;
var dy = orbState[i].y - orbState[j].y;
var dist = Math.sqrt(dx * dx + dy * dy);
if (dist < threshold) {
var opacity = (1 - dist / threshold) * 0.12;
var x1 = (orbState[i].x / 100) * w;
var y1 = (orbState[i].y / 100) * h;
var x2 = (orbState[j].x / 100) * w;
var y2 = (orbState[j].y / 100) * h;
lines += '<line x1="' + x1 + '" y1="' + y1 + '" x2="' + x2 + '" y2="' + y2 + '" stroke="rgba(59,130,246,' + opacity.toFixed(3) + ')" stroke-width="0.5" />';
} }
} }
// Subtle camera follow mouse
camera.position.x += (mouseX * 3 - camera.position.x) * 0.02;
camera.position.y += (mouseY * 2 - camera.position.y) * 0.02;
camera.lookAt(scene.position);
renderer.render(scene, camera);
} }
svg.innerHTML = lines;
animate();
} }
// ============================================================ // ============================================================
// Anime.js Entrance Animations // 5. Sparkle Particles — twinkling dots
// ============================================================ // ============================================================
if (prefersReducedMotion || typeof anime === 'undefined') { function initSparkles(animate, random) {
// Make everything visible immediately var container = document.getElementById('magic-sparkles');
document.querySelectorAll('[data-animate]').forEach(function (el) { if (!container) return;
el.style.opacity = '1';
el.style.transform = 'none'; var count = isMobile ? 15 : 25;
}); for (var i = 0; i < count; i++) {
return; var dot = document.createElement('div');
dot.className = 'magic-sparkle';
dot.style.left = random(5, 95) + '%';
dot.style.top = random(5, 95) + '%';
container.appendChild(dot);
animate(dot, {
opacity: [
{ to: random(0, 0.8), duration: random(800, 2000) },
{ to: 0, duration: random(800, 2000) }
],
scale: [
{ to: random(0.5, 2), duration: random(800, 2000) },
{ to: 0.5, duration: random(800, 2000) }
],
delay: random(0, 4000),
loop: true,
ease: 'easeInOutSine'
});
}
} }
// Hero elements animate immediately on load (above the fold) // ============================================================
var heroElements = document.querySelectorAll('.hero [data-animate]'); // 6. Cursor Glow — follows mouse
heroElements.forEach(function (el) { // ============================================================
var delay = parseInt(el.getAttribute('data-delay') || '0', 10);
anime({ function initCursorGlow(animate) {
targets: el, var glow = document.getElementById('magic-cursor-glow');
opacity: [0, 1], if (!glow) return;
translateY: [24, 0],
duration: 700, // Skip on touch devices — no cursor to follow
delay: delay + 200, // small base offset for page load if ('ontouchstart' in window) return;
easing: 'easeOutQuart',
var mouseX = window.innerWidth / 2;
var mouseY = window.innerHeight / 2;
var glowX = mouseX;
var glowY = mouseY;
document.addEventListener('mousemove', function (e) {
mouseX = e.clientX;
mouseY = e.clientY;
if (glow.style.opacity === '0' || glow.style.opacity === '') {
animate(glow, { opacity: 1, duration: 600, ease: 'outQuad' });
}
}); });
});
// Below-fold elements: Intersection Observer with immediate fallback function updateGlow() {
var belowFold = document.querySelectorAll('[data-animate]:not(.hero [data-animate])'); glowX += (mouseX - glowX) * 0.08;
glowY += (mouseY - glowY) * 0.08;
glow.style.left = glowX + 'px';
glow.style.top = glowY + 'px';
requestAnimationFrame(updateGlow);
}
updateGlow();
// Fallback: ensure all elements become visible after 1.5s max document.addEventListener('mouseleave', function () {
// This covers headless/screenshot scenarios where scroll never fires animate(glow, { opacity: 0, duration: 400, ease: 'outQuad' });
setTimeout(function () { });
belowFold.forEach(function (el) { }
if (el.style.opacity === '0' || el.style.opacity === '') {
// ============================================================
// Master background init
// ============================================================
function initMagicBackground(animate, createTimeline, stagger, random) {
initAurora(animate, random);
initRunes(animate, random);
var orbCount = isMobile ? 20 : 30;
createOrbs(orbCount, animate, random);
requestAnimationFrame(tickOrbs);
initSparkles(animate, random);
initCursorGlow(animate);
}
// ============================================================
// Entrance Animations (anime.js for [data-animate])
// ============================================================
function initEntranceAnimations(animate, stagger) {
if (prefersReducedMotion || typeof animate === 'undefined') {
document.querySelectorAll('[data-animate]').forEach(function (el) {
el.style.opacity = '1'; el.style.opacity = '1';
el.style.transform = 'none'; el.style.transform = 'none';
} });
return;
}
// Hero elements animate immediately on load
var heroElements = document.querySelectorAll('.hero [data-animate]');
heroElements.forEach(function (el) {
var delay = parseInt(el.getAttribute('data-delay') || '0', 10);
animate(el, {
opacity: [0, 1],
translateY: [24, 0],
duration: 700,
delay: delay + 200,
ease: 'easeOutQuart'
});
}); });
}, 1500);
if (typeof IntersectionObserver !== 'undefined') { // Below-fold elements: Intersection Observer
var observer = new IntersectionObserver( var belowFold = document.querySelectorAll('[data-animate]:not(.hero [data-animate])');
function (entries) {
entries.forEach(function (entry) {
if (entry.isIntersecting) {
var el = entry.target;
var delay = parseInt(el.getAttribute('data-delay') || '0', 10);
observer.unobserve(el);
anime({ // Fallback: ensure all elements become visible after 1.5s
targets: el, // Covers headless/screenshot scenarios where scroll never fires
opacity: [0, 1], setTimeout(function () {
translateY: [24, 0], belowFold.forEach(function (el) {
duration: 700, if (el.style.opacity === '0' || el.style.opacity === '') {
delay: delay, el.style.opacity = '1';
easing: 'easeOutQuart', el.style.transform = 'none';
}); }
} });
}); }, 1500);
},
{
threshold: 0.1,
rootMargin: '0px 0px -40px 0px',
}
);
belowFold.forEach(function (el) { if (typeof IntersectionObserver !== 'undefined') {
observer.observe(el); var observer = new IntersectionObserver(
}); function (entries) {
entries.forEach(function (entry) {
if (entry.isIntersecting) {
var el = entry.target;
var delay = parseInt(el.getAttribute('data-delay') || '0', 10);
observer.unobserve(el);
animate(el, {
opacity: [0, 1],
translateY: [24, 0],
duration: 700,
delay: delay,
ease: 'easeOutQuart'
});
}
});
},
{ threshold: 0.1, rootMargin: '0px 0px -40px 0px' }
);
belowFold.forEach(function (el) {
observer.observe(el);
});
}
} }
// ============================================================ // ============================================================
// Terminal glow pulse (subtle) // Terminal glow pulse
// ============================================================ // ============================================================
var terminalEl = document.querySelector('.hero-terminal'); function initTerminalGlow(animate) {
if (terminalEl && !prefersReducedMotion && typeof anime !== 'undefined') { if (prefersReducedMotion || typeof animate === 'undefined') return;
var terminalEl = document.querySelector('.hero-terminal');
if (!terminalEl) return;
setTimeout(function () { setTimeout(function () {
anime({ animate('.hero-terminal', {
targets: '.hero-terminal',
boxShadow: [ boxShadow: [
'0 0 0 0 rgba(59, 130, 246, 0)', '0 0 0 0 rgba(59, 130, 246, 0)',
'0 0 40px -8px rgba(59, 130, 246, 0.15)', '0 0 40px -8px rgba(59, 130, 246, 0.15)',
'0 0 0 0 rgba(59, 130, 246, 0)', '0 0 0 0 rgba(59, 130, 246, 0)'
], ],
duration: 2000, duration: 2000,
easing: 'easeInOutQuad', easing: 'easeInOutQuad'
}); });
}, 1200); }, 1200);
} }
// ============================================================
// Boot
// ============================================================
function boot(animeLib) {
var animate = animeLib.animate;
var createTimeline = animeLib.createTimeline;
var stagger = animeLib.stagger;
var random = animeLib.random;
if (!prefersReducedMotion) {
initMagicBackground(animate, createTimeline, stagger, random);
}
initEntranceAnimations(animate, stagger);
initTerminalGlow(animate);
}
// Defer boot by one frame so anime.js engine is ready
if (window.anime) {
requestAnimationFrame(function () { boot(window.anime); });
} else {
var tries = 0;
var poll = setInterval(function () {
tries++;
if (window.anime) {
clearInterval(poll);
requestAnimationFrame(function () { boot(window.anime); });
}
if (tries > 50) clearInterval(poll);
}, 100);
}
})(); })();

View File

@ -1,6 +1,6 @@
/* ============================================================ /* ============================================================
Hatch Homepage v2 Dark Theme Hatch Homepage Dark Theme
Design system: OKLCH-inspired tokens, Inter + JetBrains Mono Design tokens, Inter + JetBrains Mono, anime.js magic background
============================================================ */ ============================================================ */
/* --- Reset --- */ /* --- Reset --- */
@ -13,22 +13,22 @@
/* --- Tokens --- */ /* --- Tokens --- */
:root { :root {
/* Surface */ /* Surface */
--surface-0: #09090b; /* deepest bg */ --surface-0: #09090b;
--surface-1: #111114; /* cards, sections */ --surface-1: #111114;
--surface-2: #1a1a1f; /* elevated surfaces */ --surface-2: #1a1a1f;
--surface-3: #232328; /* borders, subtle dividers */ --surface-3: #232328;
/* Brand */ /* Brand */
--brand: #3b82f6; /* blue-500 */ --brand: #3b82f6;
--brand-dim: #2563eb; /* blue-600 hover */ --brand-dim: #2563eb;
--brand-glow: rgba(59, 130, 246, 0.15); --brand-glow: rgba(59, 130, 246, 0.15);
--brand-glow-strong: rgba(59, 130, 246, 0.25); --brand-glow-strong: rgba(59, 130, 246, 0.25);
/* Text */ /* Text */
--text-primary: #fafafa; /* near-white */ --text-primary: #fafafa;
--text-secondary: #a1a1aa; /* zinc-400 */ --text-secondary: #a1a1aa;
--text-tertiary: #71717a; /* zinc-500 */ --text-tertiary: #71717a;
--text-code: #e879f9; /* fuchsia-400 for code highlights */ --text-code: #e879f9;
/* Code */ /* Code */
--code-bg: #18181b; --code-bg: #18181b;
@ -39,18 +39,18 @@
--font-mono: 'JetBrains Mono', 'SFMono-Regular', Consolas, monospace; --font-mono: 'JetBrains Mono', 'SFMono-Regular', Consolas, monospace;
/* Spacing scale (4px base) */ /* Spacing scale (4px base) */
--sp-1: 0.25rem; /* 4 */ --sp-1: 0.25rem;
--sp-2: 0.5rem; /* 8 */ --sp-2: 0.5rem;
--sp-3: 0.75rem; /* 12 */ --sp-3: 0.75rem;
--sp-4: 1rem; /* 16 */ --sp-4: 1rem;
--sp-5: 1.25rem; /* 20 */ --sp-5: 1.25rem;
--sp-6: 1.5rem; /* 24 */ --sp-6: 1.5rem;
--sp-8: 2rem; /* 32 */ --sp-8: 2rem;
--sp-10: 2.5rem; /* 40 */ --sp-10: 2.5rem;
--sp-12: 3rem; /* 48 */ --sp-12: 3rem;
--sp-16: 4rem; /* 64 */ --sp-16: 4rem;
--sp-20: 5rem; /* 80 */ --sp-20: 5rem;
--sp-24: 6rem; /* 96 */ --sp-24: 6rem;
/* Radii */ /* Radii */
--radius-sm: 6px; --radius-sm: 6px;
@ -98,8 +98,37 @@ code {
color: var(--text-code); color: var(--text-code);
} }
/* --- Particle canvas --- */ /* --- Accessibility: skip link --- */
#particle-canvas { .skip-to-content {
position: absolute;
top: -50px;
left: var(--sp-4);
background: var(--brand);
color: #fff;
padding: var(--sp-2) var(--sp-4);
z-index: 200;
font-weight: 600;
font-size: 0.9rem;
border-radius: 0 0 var(--radius-sm) var(--radius-sm);
transition: top 0.2s ease;
}
.skip-to-content:focus {
top: 0;
outline: 2px solid #fff;
outline-offset: 2px;
}
/* --- Accessibility: focus-visible --- */
a:focus-visible,
.btn:focus-visible {
outline: 2px solid var(--brand);
outline-offset: 3px;
border-radius: var(--radius-sm);
}
/* --- Magic background layers --- */
#magic-bg {
position: fixed; position: fixed;
top: 0; top: 0;
left: 0; left: 0;
@ -107,6 +136,138 @@ code {
height: 100%; height: 100%;
z-index: 0; z-index: 0;
pointer-events: none; pointer-events: none;
overflow: hidden;
}
/* Aurora gradient blobs */
.magic-aurora {
position: absolute;
width: 600px;
height: 600px;
border-radius: 50%;
filter: blur(120px);
opacity: 0.12;
background: radial-gradient(circle, var(--brand) 0%, transparent 70%);
top: -10%;
left: -5%;
will-change: transform, opacity;
}
.magic-aurora--2 {
background: radial-gradient(circle, #a855f7 0%, transparent 70%);
top: 40%;
right: -15%;
left: auto;
width: 500px;
height: 500px;
opacity: 0.1;
}
.magic-aurora--3 {
background: radial-gradient(circle, #06b6d4 0%, transparent 70%);
bottom: -10%;
left: 30%;
top: auto;
width: 450px;
height: 450px;
opacity: 0.08;
}
/* SVG rune circles */
.magic-runes {
position: absolute;
top: 50%;
left: 50%;
width: 900px;
height: 900px;
transform: translate(-50%, -50%);
will-change: transform;
}
.rune {
fill: none;
stroke-width: 1;
opacity: 0;
}
.rune-outer {
stroke: var(--brand);
stroke-dasharray: 8 24;
opacity: 0.08;
}
.rune-mid {
stroke: #a855f7;
stroke-dasharray: 4 16;
opacity: 0.06;
}
.rune-inner {
stroke: #06b6d4;
stroke-dasharray: 12 8;
opacity: 0.05;
}
.rune-core {
stroke: var(--brand);
stroke-dasharray: 2 12;
opacity: 0.04;
stroke-width: 1.5;
}
/* Floating orbs container */
.magic-orbs {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.magic-orb {
position: absolute;
border-radius: 50%;
will-change: transform, opacity;
}
/* Connection lines SVG */
.magic-connections {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
/* Sparkle particles */
.magic-sparkles {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.magic-sparkle {
position: absolute;
width: 2px;
height: 2px;
border-radius: 50%;
background: #fff;
will-change: opacity;
}
/* Cursor glow follower */
.magic-cursor-glow {
position: absolute;
width: 400px;
height: 400px;
border-radius: 50%;
background: radial-gradient(circle, rgba(59, 130, 246, 0.08) 0%, transparent 70%);
pointer-events: none;
transform: translate(-50%, -50%);
will-change: transform;
opacity: 0;
} }
/* --- Container --- */ /* --- Container --- */
@ -175,7 +336,7 @@ nav {
.hero { .hero {
position: relative; position: relative;
z-index: 1; z-index: 1;
padding: calc(var(--sp-24) + 2rem) 0 var(--sp-20); padding: calc(var(--sp-24) + 2rem) 0 var(--sp-16);
text-align: center; text-align: center;
} }
@ -233,6 +394,8 @@ nav {
cursor: pointer; cursor: pointer;
border: none; border: none;
white-space: nowrap; white-space: nowrap;
position: relative;
overflow: hidden;
} }
.btn-primary { .btn-primary {
@ -304,7 +467,7 @@ nav {
} }
.hero-terminal pre { .hero-terminal pre {
padding: var(--sp-5) var(--sp-5); padding: var(--sp-5);
overflow-x: auto; overflow-x: auto;
font-size: 0.82rem; font-size: 0.82rem;
line-height: 1.8; line-height: 1.8;
@ -326,7 +489,7 @@ nav {
.features { .features {
position: relative; position: relative;
z-index: 1; z-index: 1;
padding: var(--sp-24) 0; padding: var(--sp-20) 0;
} }
.features h2 { .features h2 {
@ -349,13 +512,14 @@ nav {
background: var(--surface-1); background: var(--surface-1);
border: 1px solid var(--surface-3); border: 1px solid var(--surface-3);
border-radius: var(--radius-lg); border-radius: var(--radius-lg);
transition: border-color var(--duration-normal) var(--ease-out), transition: all var(--duration-normal) var(--ease-out);
box-shadow var(--duration-normal) var(--ease-out); transform: translateY(0);
} }
.feature:hover { .feature:hover {
border-color: rgba(59, 130, 246, 0.3); border-color: rgba(59, 130, 246, 0.3);
box-shadow: 0 0 32px -8px var(--brand-glow); box-shadow: 0 0 32px -8px var(--brand-glow);
transform: translateY(-4px);
} }
.feature-icon { .feature-icon {
@ -384,11 +548,49 @@ nav {
line-height: 1.65; line-height: 1.65;
} }
/* --- Tech strip --- */
.tech-strip {
position: relative;
z-index: 1;
padding: var(--sp-12) 0;
border-top: 1px solid var(--surface-3);
border-bottom: 1px solid var(--surface-3);
}
.tech-items {
display: flex;
justify-content: center;
gap: var(--sp-10);
flex-wrap: wrap;
}
.tech-item {
display: flex;
flex-direction: column;
align-items: center;
gap: var(--sp-1);
}
.tech-label {
font-size: 0.75rem;
color: var(--text-tertiary);
text-transform: uppercase;
letter-spacing: 0.06em;
font-weight: 500;
}
.tech-value {
font-family: var(--font-mono);
font-size: 1rem;
font-weight: 600;
color: var(--text-primary);
}
/* --- Why --- */ /* --- Why --- */
.why { .why {
position: relative; position: relative;
z-index: 1; z-index: 1;
padding: var(--sp-24) 0; padding: var(--sp-20) 0;
background: var(--surface-1); background: var(--surface-1);
border-top: 1px solid var(--surface-3); border-top: 1px solid var(--surface-3);
border-bottom: 1px solid var(--surface-3); border-bottom: 1px solid var(--surface-3);
@ -444,7 +646,7 @@ nav {
.final-cta { .final-cta {
position: relative; position: relative;
z-index: 1; z-index: 1;
padding: var(--sp-24) 0; padding: var(--sp-20) 0;
text-align: center; text-align: center;
} }
@ -501,6 +703,149 @@ footer a:hover {
transform: translateY(20px); transform: translateY(20px);
} }
/* --- Blog --- */
.blog-index {
position: relative;
z-index: 1;
padding: calc(var(--sp-24) + 2rem) 0 var(--sp-20);
}
.blog-index h1 {
font-size: clamp(2rem, 4vw, 2.5rem);
font-weight: 800;
letter-spacing: -0.03em;
margin-bottom: var(--sp-10);
color: var(--text-primary);
}
.post-preview {
padding: var(--sp-8);
background: var(--surface-1);
border: 1px solid var(--surface-3);
border-radius: var(--radius-lg);
transition: border-color var(--duration-normal) var(--ease-out),
box-shadow var(--duration-normal) var(--ease-out);
}
.post-preview:hover {
border-color: rgba(59, 130, 246, 0.3);
box-shadow: 0 0 32px -8px var(--brand-glow);
}
.post-preview h2 {
font-size: 1.4rem;
font-weight: 700;
margin-bottom: var(--sp-2);
letter-spacing: -0.02em;
}
.post-preview h2 a {
color: var(--text-primary);
}
.post-preview h2 a:hover {
color: var(--brand);
}
.post-preview time {
font-size: 0.85rem;
color: var(--text-tertiary);
font-family: var(--font-mono);
}
.post-preview p {
color: var(--text-secondary);
font-size: 0.95rem;
line-height: 1.7;
margin: var(--sp-4) 0;
}
.read-more {
font-weight: 600;
font-size: 0.9rem;
}
/* Blog post */
.blog-post {
position: relative;
z-index: 1;
padding: calc(var(--sp-24) + 2rem) 0 var(--sp-20);
}
.post-header {
margin-bottom: var(--sp-10);
padding-bottom: var(--sp-6);
border-bottom: 1px solid var(--surface-3);
}
.post-header h1 {
font-size: clamp(1.75rem, 4vw, 2.5rem);
font-weight: 800;
line-height: 1.15;
letter-spacing: -0.03em;
text-wrap: balance;
margin-bottom: var(--sp-3);
color: var(--text-primary);
}
.post-header time {
font-size: 0.85rem;
color: var(--text-tertiary);
font-family: var(--font-mono);
}
.post-content {
max-width: 680px;
}
.post-content p {
color: var(--text-secondary);
font-size: 1.05rem;
line-height: 1.8;
margin-bottom: var(--sp-6);
}
.post-content h2 {
font-size: 1.4rem;
font-weight: 700;
letter-spacing: -0.02em;
margin: var(--sp-10) 0 var(--sp-4);
color: var(--text-primary);
}
.post-content pre {
background: var(--code-bg);
border: 1px solid var(--code-border);
border-radius: var(--radius-md);
padding: var(--sp-5);
overflow-x: auto;
margin-bottom: var(--sp-6);
}
.post-content pre code {
background: none;
border: none;
padding: 0;
font-size: 0.85rem;
line-height: 1.8;
color: var(--text-secondary);
}
.post-content strong {
color: var(--text-primary);
font-weight: 600;
}
@media (max-width: 768px) {
.post-preview {
padding: var(--sp-6);
}
.post-content p {
font-size: 1rem;
}
}
/* --- Reduced motion --- */ /* --- Reduced motion --- */
@media (prefers-reduced-motion: reduce) { @media (prefers-reduced-motion: reduce) {
[data-animate] { [data-animate] {
@ -512,19 +857,23 @@ footer a:hover {
transform: none; transform: none;
} }
#particle-canvas { .feature:hover {
transform: none;
}
#magic-bg {
display: none; display: none;
} }
} }
/* --- Responsive --- */ /* --- Responsive: tablet --- */
@media (max-width: 768px) { @media (max-width: 768px) {
.container { .container {
padding: 0 var(--sp-4); padding: 0 var(--sp-4);
} }
.hero { .hero {
padding: calc(var(--sp-20) + 1rem) 0 var(--sp-16); padding: calc(var(--sp-20) + 1rem) 0 var(--sp-12);
} }
.hero h1 { .hero h1 {
@ -567,8 +916,13 @@ footer a:hover {
font-size: 0.75rem; font-size: 0.75rem;
padding: var(--sp-4); padding: var(--sp-4);
} }
.tech-items {
gap: var(--sp-6);
}
} }
/* --- Responsive: mobile --- */
@media (max-width: 480px) { @media (max-width: 480px) {
.hero-cta { .hero-cta {
flex-direction: column; flex-direction: column;
@ -578,4 +932,49 @@ footer a:hover {
.btn { .btn {
justify-content: center; justify-content: center;
} }
/* Terminal horizontal scroll with fade indicator */
.hero-terminal {
position: relative;
overflow: hidden;
}
.hero-terminal::after {
content: '';
position: absolute;
right: 0;
top: 0;
bottom: 0;
width: 40px;
background: linear-gradient(to right, transparent, var(--code-bg));
pointer-events: none;
}
.hero-terminal pre {
overflow-x: auto;
-webkit-overflow-scrolling: touch;
scrollbar-width: thin;
scrollbar-color: var(--surface-3) transparent;
}
.hero-terminal pre::-webkit-scrollbar {
height: 6px;
}
.hero-terminal pre::-webkit-scrollbar-track {
background: transparent;
}
.hero-terminal pre::-webkit-scrollbar-thumb {
background: var(--surface-3);
border-radius: 3px;
}
.tech-items {
gap: var(--sp-4);
}
.tech-item {
min-width: calc(50% - var(--sp-4));
}
} }