/**
* 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
*
* Entrance: elements with [data-animate] fade up on scroll.
* Performance: orb tick throttled to ~30fps, connections drawn every 3rd frame.
*/
(function () {
'use strict';
var prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
var isMobile = window.innerWidth < 768;
// ============================================================
// 1. Aurora Blobs — slow drift + color pulse
// ============================================================
function initAurora(animate, random) {
var blobs = document.querySelectorAll('.magic-aurora');
blobs.forEach(function (blob, i) {
animate(blob, {
translateX: [
{ to: random(-40, 40), duration: random(8000, 12000) },
{ 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'
});
});
}
// ============================================================
// 2. Rune Circles — continuous rotation at different speeds
// ============================================================
function initRunes(animate, random) {
var runeData = [
{ sel: '.rune-outer', speed: 120, dir: 1 },
{ sel: '.rune-mid', speed: 80, dir: -1 },
{ sel: '.rune-inner', speed: 60, dir: 1 },
{ sel: '.rune-core', speed: 40, dir: -1 }
];
runeData.forEach(function (r) {
var el = document.querySelector(r.sel);
if (!el) return;
animate(el, {
opacity: parseFloat(getComputedStyle(el).opacity) || 0.06,
duration: 2000,
delay: random(500, 1500),
ease: 'outQuad'
});
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'
});
}
}
// ============================================================
// 3. Floating Orbs — luminous dots with organic drift
// ============================================================
var orbState = [];
function createOrbs(count, animate, random) {
var container = document.getElementById('magic-orbs');
if (!container) return;
var colors = [
'rgba(59, 130, 246, VAR)',
'rgba(168, 85, 247, VAR)',
'rgba(6, 182, 212, VAR)',
'rgba(99, 102, 241, VAR)',
'rgba(14, 165, 233, VAR)'
];
for (var i = 0; i < count; i++) {
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));
var el = document.createElement('div');
el.className = 'magic-orb';
el.style.width = size + 'px';
el.style.height = size + 'px';
el.style.background = color;
el.style.boxShadow = '0 0 ' + (size * 3) + 'px ' + glowColor;
container.appendChild(el);
orbState.push({
x: random(0, 100),
y: random(0, 100),
vx: random(-0.015, 0.015),
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'
});
}
}
// Throttled orb tick — runs at ~30fps for performance
var lastOrbTick = 0;
var orbTickInterval = 33; // ~30fps
var frameCount = 0;
function tickOrbs(timestamp) {
requestAnimationFrame(tickOrbs);
if (timestamp - lastOrbTick < orbTickInterval) return;
lastOrbTick = timestamp;
frameCount++;
orbState.forEach(function (orb) {
orb.x += orb.vx;
orb.y += orb.vy;
if (orb.x < -2 || orb.x > 102) orb.vx *= -1;
if (orb.y < -2 || orb.y > 102) orb.vy *= -1;
orb.x = Math.max(-2, Math.min(102, orb.x));
orb.y = Math.max(-2, Math.min(102, orb.y));
orb.vx += (Math.random() - 0.5) * 0.0005;
orb.vy += (Math.random() - 0.5) * 0.0005;
orb.vx = Math.max(-0.03, Math.min(0.03, orb.vx));
orb.vy = Math.max(-0.03, Math.min(0.03, orb.vy));
orb.el.style.left = orb.x + '%';
orb.el.style.top = orb.y + '%';
});
// 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 += '
s every 3.5s with a 700ms opacity crossfade // (handled in CSS via .hero-tagline-item transition). Honors // prefers-reduced-motion: skips the timer entirely so the default // #1 stays visible without any animation. // ============================================================ function initRotatingTagline() { var container = document.querySelector('.hero-tagline'); if (!container) return; var items = container.querySelectorAll('.hero-tagline-item'); if (items.length < 2) return; if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return; var idx = 0; window.setInterval(function () { items[idx].classList.remove('is-active'); items[idx].setAttribute('aria-hidden', 'true'); idx = (idx + 1) % items.length; items[idx].classList.add('is-active'); items[idx].removeAttribute('aria-hidden'); }, 3500); } // 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); } })();