Files
Websites/Profice WebSite/scripts/hex-background.js
2026-03-20 12:44:48 +01:00

282 lines
9.6 KiB
JavaScript

/**
* Hexagonal Magnetism Background - OPTIMIZED
* Only renders visible hexagons, uses viewport culling
*/
(function() {
'use strict';
// Configuration
const CONFIG = {
hexSize: 30,
lineWidth: 1,
maxLineWidth: 2.5,
baseOpacity: 0.15,
maxOpacity: 0.6,
magnetRadius: 280,
maxDisplacement: 12,
returnSpeed: 0.4,
// Pre-calculated colors
baseR: 119, baseG: 119, baseB: 100,
tealR: 38, tealG: 166, tealB: 154,
orangeR: 245, orangeG: 124, orangeB: 0
};
// Pre-calculate hex geometry
const SQRT3 = Math.sqrt(3);
const horizSpacing = CONFIG.hexSize * SQRT3;
const vertSpacing = CONFIG.hexSize * 1.5;
// Pre-calculate hex vertices angles (pointy-top)
const HEX_ANGLES = [];
for (let i = 0; i < 6; i++) {
const angle = (Math.PI / 3) * i - Math.PI / 2;
HEX_ANGLES.push({ cos: Math.cos(angle), sin: Math.sin(angle) });
}
// State
let canvas, ctx;
let mouse = { x: -1000, y: -1000 };
let scrollY = 0;
let animationId;
let isInitialized = false;
let canvasWidth = 0;
let canvasHeight = 0;
// Active hexagons (only those being animated)
let activeHexagons = new Map();
// Easing function
function easeOutCubic(t) {
return 1 - (1 - t) * (1 - t) * (1 - t);
}
// Get hex key from grid position
function getHexKey(col, row) {
return `${col},${row}`;
}
// Get hex origin from grid position
function getHexOrigin(col, row) {
const xOffset = (row % 2 === 1) ? horizSpacing / 2 : 0;
return {
x: col * horizSpacing + xOffset,
y: row * vertSpacing
};
}
// Draw a single hexagon
function drawHex(x, y, opacity, lineWidth, colorInfluence) {
ctx.beginPath();
ctx.moveTo(
x + CONFIG.hexSize * HEX_ANGLES[0].cos,
y + CONFIG.hexSize * HEX_ANGLES[0].sin
);
for (let i = 1; i < 6; i++) {
ctx.lineTo(
x + CONFIG.hexSize * HEX_ANGLES[i].cos,
y + CONFIG.hexSize * HEX_ANGLES[i].sin
);
}
ctx.closePath();
// Color calculation
let r, g, b;
if (colorInfluence < 0.01) {
r = CONFIG.baseR; g = CONFIG.baseG; b = CONFIG.baseB;
} else if (colorInfluence < 0.6) {
const t = colorInfluence / 0.6;
r = CONFIG.baseR + (CONFIG.tealR - CONFIG.baseR) * t | 0;
g = CONFIG.baseG + (CONFIG.tealG - CONFIG.baseG) * t | 0;
b = CONFIG.baseB + (CONFIG.tealB - CONFIG.baseB) * t | 0;
} else {
const t = (colorInfluence - 0.6) / 0.4;
r = CONFIG.tealR + (CONFIG.orangeR - CONFIG.tealR) * t | 0;
g = CONFIG.tealG + (CONFIG.orangeG - CONFIG.tealG) * t | 0;
b = CONFIG.tealB + (CONFIG.orangeB - CONFIG.tealB) * t | 0;
}
ctx.strokeStyle = `rgba(${r},${g},${b},${opacity})`;
ctx.lineWidth = lineWidth;
ctx.stroke();
}
// Initialize
function init() {
// Skip animation on mobile/touch devices for performance
if (window.innerWidth <= 768 || ('ontouchstart' in window)) return;
canvas = document.getElementById('hexCanvas');
if (!canvas) return;
ctx = canvas.getContext('2d', { alpha: true });
resize();
window.addEventListener('resize', debounce(resize, 200));
document.addEventListener('mousemove', handleMouseMove, { passive: true });
document.addEventListener('mouseleave', handleMouseLeave);
window.addEventListener('scroll', handleScroll, { passive: true });
isInitialized = true;
animate();
}
function resize() {
const dpr = Math.min(window.devicePixelRatio || 1, 2); // Cap DPR at 2
canvasWidth = window.innerWidth;
canvasHeight = window.innerHeight;
canvas.width = canvasWidth * dpr;
canvas.height = canvasHeight * dpr;
canvas.style.width = canvasWidth + 'px';
canvas.style.height = canvasHeight + 'px';
canvas.style.position = 'fixed';
canvas.style.top = '0';
canvas.style.left = '0';
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.scale(dpr, dpr);
}
function handleMouseMove(e) {
mouse.x = e.clientX;
mouse.y = e.clientY;
}
function handleMouseLeave() {
mouse.x = -1000;
mouse.y = -1000;
}
function handleScroll() {
scrollY = window.scrollY;
}
// Main animation loop - optimized
function animate() {
if (!isInitialized) return;
ctx.clearRect(0, 0, canvasWidth, canvasHeight);
// Calculate visible grid range (viewport only)
const viewTop = scrollY - CONFIG.hexSize;
const viewBottom = scrollY + canvasHeight + CONFIG.hexSize;
const viewLeft = -CONFIG.hexSize;
const viewRight = canvasWidth + CONFIG.hexSize;
const startRow = Math.floor(viewTop / vertSpacing) - 1;
const endRow = Math.ceil(viewBottom / vertSpacing) + 1;
const startCol = Math.floor(viewLeft / horizSpacing) - 1;
const endCol = Math.ceil(viewRight / horizSpacing) + 1;
// Mouse position in world coordinates
const mouseWorldY = mouse.y + scrollY;
const magnetRadiusSq = CONFIG.magnetRadius * CONFIG.magnetRadius;
// Update active hexagons
for (const [key, hex] of activeHexagons) {
const dx = mouse.x - hex.originX;
const dy = mouseWorldY - hex.originY;
const distSq = dx * dx + dy * dy;
if (distSq < magnetRadiusSq && mouse.x > -500) {
const distance = Math.sqrt(distSq);
const influence = 1 - (distance / CONFIG.magnetRadius);
const easedInfluence = easeOutCubic(influence);
const angle = Math.atan2(dy, dx);
const displacement = CONFIG.maxDisplacement * easedInfluence;
hex.currentX += (hex.originX + Math.cos(angle) * displacement - hex.currentX) * 0.15;
hex.currentY += (hex.originY + Math.sin(angle) * displacement - hex.currentY) * 0.15;
hex.opacity = CONFIG.baseOpacity + (CONFIG.maxOpacity - CONFIG.baseOpacity) * easedInfluence;
hex.lineWidth = CONFIG.lineWidth + (CONFIG.maxLineWidth - CONFIG.lineWidth) * easedInfluence;
hex.colorInfluence = easedInfluence;
} else {
// Return to origin
hex.currentX += (hex.originX - hex.currentX) * CONFIG.returnSpeed;
hex.currentY += (hex.originY - hex.currentY) * CONFIG.returnSpeed;
hex.opacity += (CONFIG.baseOpacity - hex.opacity) * CONFIG.returnSpeed;
hex.lineWidth += (CONFIG.lineWidth - hex.lineWidth) * CONFIG.returnSpeed;
hex.colorInfluence *= (1 - CONFIG.returnSpeed);
// Remove if fully reset
if (hex.colorInfluence < 0.001 &&
Math.abs(hex.currentX - hex.originX) < 0.1 &&
Math.abs(hex.currentY - hex.originY) < 0.1) {
activeHexagons.delete(key);
}
}
}
// Draw visible hexagons
for (let row = startRow; row <= endRow; row++) {
for (let col = startCol; col <= endCol; col++) {
const origin = getHexOrigin(col, row);
const screenY = origin.y - scrollY;
// Skip if outside viewport
if (screenY < -CONFIG.hexSize || screenY > canvasHeight + CONFIG.hexSize) continue;
const key = getHexKey(col, row);
const activeHex = activeHexagons.get(key);
if (activeHex) {
// Draw animated hex
drawHex(
activeHex.currentX,
activeHex.currentY - scrollY,
activeHex.opacity,
activeHex.lineWidth,
activeHex.colorInfluence
);
} else {
// Check if should become active
const dx = mouse.x - origin.x;
const dy = mouseWorldY - origin.y;
const distSq = dx * dx + dy * dy;
if (distSq < magnetRadiusSq && mouse.x > -500) {
// Add to active hexagons
activeHexagons.set(key, {
originX: origin.x,
originY: origin.y,
currentX: origin.x,
currentY: origin.y,
opacity: CONFIG.baseOpacity,
lineWidth: CONFIG.lineWidth,
colorInfluence: 0
});
}
// Draw static hex
drawHex(origin.x, screenY, CONFIG.baseOpacity, CONFIG.lineWidth, 0);
}
}
}
animationId = requestAnimationFrame(animate);
}
function debounce(func, wait) {
let timeout;
return function(...args) {
clearTimeout(timeout);
timeout = setTimeout(() => func(...args), wait);
};
}
function destroy() {
if (animationId) cancelAnimationFrame(animationId);
isInitialized = false;
activeHexagons.clear();
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
window.HexBackground = { init, destroy, config: CONFIG };
})();