278 lines
9.2 KiB
JavaScript
278 lines
9.2 KiB
JavaScript
/**
|
|
* Hexagonal Magnetism Background
|
|
* Rigid hexagon grid with mouse-based magnetism effect
|
|
* Hexagons move as whole units - no vertex deformation
|
|
*/
|
|
|
|
(function() {
|
|
'use strict';
|
|
|
|
// Configuration
|
|
const CONFIG = {
|
|
hexSize: 30,
|
|
lineWidth: 1,
|
|
maxLineWidth: 2.5,
|
|
baseOpacity: 0.2,
|
|
maxOpacity: 0.7,
|
|
magnetRadius: 180,
|
|
maxDisplacement: 12,
|
|
returnSpeed: 0.1,
|
|
// Colors (teal to orange gradient based on proximity)
|
|
tealR: 38, tealG: 166, tealB: 154,
|
|
orangeR: 245, orangeG: 124, orangeB: 0
|
|
};
|
|
|
|
// State
|
|
let canvas, ctx;
|
|
let hexagons = [];
|
|
let mouse = { x: -1000, y: -1000 };
|
|
let animationId;
|
|
let isInitialized = false;
|
|
|
|
// Hexagon class - rigid shape, no vertex warping
|
|
class Hexagon {
|
|
constructor(originX, originY, size) {
|
|
this.originX = originX;
|
|
this.originY = originY;
|
|
this.currentX = originX;
|
|
this.currentY = originY;
|
|
this.size = size;
|
|
this.opacity = CONFIG.baseOpacity;
|
|
this.lineWidth = CONFIG.lineWidth;
|
|
this.colorInfluence = 0;
|
|
}
|
|
|
|
// Calculate vertices relative to current center position
|
|
getVertices() {
|
|
const vertices = [];
|
|
for (let i = 0; i < 6; i++) {
|
|
// Pointy-top hexagon: first vertex at top
|
|
const angle = (Math.PI / 3) * i - Math.PI / 2;
|
|
vertices.push({
|
|
x: this.currentX + this.size * Math.cos(angle),
|
|
y: this.currentY + this.size * Math.sin(angle)
|
|
});
|
|
}
|
|
return vertices;
|
|
}
|
|
|
|
update(mouseX, mouseY) {
|
|
const dx = mouseX - this.originX;
|
|
const dy = mouseY - this.originY;
|
|
const distance = Math.sqrt(dx * dx + dy * dy);
|
|
|
|
if (distance < CONFIG.magnetRadius && distance > 0) {
|
|
// Calculate influence (stronger when closer)
|
|
const influence = 1 - (distance / CONFIG.magnetRadius);
|
|
const easedInfluence = easeOutCubic(influence);
|
|
|
|
// Calculate displacement toward mouse (limited)
|
|
const angle = Math.atan2(dy, dx);
|
|
const displacement = CONFIG.maxDisplacement * easedInfluence;
|
|
|
|
// Target position (pulled toward mouse)
|
|
const targetX = this.originX + Math.cos(angle) * displacement;
|
|
const targetY = this.originY + Math.sin(angle) * displacement;
|
|
|
|
// Smooth interpolation to target
|
|
this.currentX += (targetX - this.currentX) * 0.15;
|
|
this.currentY += (targetY - this.currentY) * 0.15;
|
|
|
|
// Update visual properties
|
|
this.opacity = CONFIG.baseOpacity + (CONFIG.maxOpacity - CONFIG.baseOpacity) * easedInfluence;
|
|
this.lineWidth = CONFIG.lineWidth + (CONFIG.maxLineWidth - CONFIG.lineWidth) * easedInfluence;
|
|
this.colorInfluence = easedInfluence;
|
|
} else {
|
|
// Return to origin with spring effect
|
|
this.currentX += (this.originX - this.currentX) * CONFIG.returnSpeed;
|
|
this.currentY += (this.originY - this.currentY) * CONFIG.returnSpeed;
|
|
|
|
// Fade back to default
|
|
this.opacity += (CONFIG.baseOpacity - this.opacity) * CONFIG.returnSpeed;
|
|
this.lineWidth += (CONFIG.lineWidth - this.lineWidth) * CONFIG.returnSpeed;
|
|
this.colorInfluence += (0 - this.colorInfluence) * CONFIG.returnSpeed;
|
|
}
|
|
}
|
|
|
|
draw(ctx) {
|
|
const vertices = this.getVertices();
|
|
|
|
ctx.beginPath();
|
|
ctx.moveTo(vertices[0].x, vertices[0].y);
|
|
for (let i = 1; i < vertices.length; i++) {
|
|
ctx.lineTo(vertices[i].x, vertices[i].y);
|
|
}
|
|
ctx.closePath();
|
|
|
|
// Interpolate color: base gray -> teal -> orange
|
|
if (this.colorInfluence > 0.01) {
|
|
let r, g, b;
|
|
if (this.colorInfluence < 0.5) {
|
|
// Gray to teal
|
|
const t = this.colorInfluence * 2;
|
|
r = Math.round(119 + (CONFIG.tealR - 119) * t);
|
|
g = Math.round(119 + (CONFIG.tealG - 119) * t);
|
|
b = Math.round(100 + (CONFIG.tealB - 100) * t);
|
|
} else {
|
|
// Teal to orange
|
|
const t = (this.colorInfluence - 0.5) * 2;
|
|
r = Math.round(CONFIG.tealR + (CONFIG.orangeR - CONFIG.tealR) * t);
|
|
g = Math.round(CONFIG.tealG + (CONFIG.orangeG - CONFIG.tealG) * t);
|
|
b = Math.round(CONFIG.tealB + (CONFIG.orangeB - CONFIG.tealB) * t);
|
|
}
|
|
ctx.strokeStyle = `rgba(${r}, ${g}, ${b}, ${this.opacity})`;
|
|
} else {
|
|
ctx.strokeStyle = `rgba(119, 119, 100, ${this.opacity})`;
|
|
}
|
|
|
|
ctx.lineWidth = this.lineWidth;
|
|
ctx.stroke();
|
|
}
|
|
}
|
|
|
|
// Easing function for smooth animations
|
|
function easeOutCubic(t) {
|
|
return 1 - Math.pow(1 - t, 3);
|
|
}
|
|
|
|
// Initialize canvas
|
|
function init() {
|
|
canvas = document.getElementById('hexCanvas');
|
|
if (!canvas) {
|
|
console.warn('hexCanvas element not found');
|
|
return;
|
|
}
|
|
|
|
ctx = canvas.getContext('2d');
|
|
resize();
|
|
generateHexagons();
|
|
|
|
// Event listeners
|
|
window.addEventListener('resize', debounce(handleResize, 150));
|
|
document.addEventListener('mousemove', handleMouseMove);
|
|
document.addEventListener('mouseleave', handleMouseLeave);
|
|
|
|
isInitialized = true;
|
|
animate();
|
|
}
|
|
|
|
// Generate hexagon grid with proper pointy-top tessellation
|
|
function generateHexagons() {
|
|
hexagons = [];
|
|
|
|
// Pointy-top hexagon math:
|
|
// horizontal spacing = size * sqrt(3)
|
|
// vertical spacing = size * 1.5
|
|
// every odd row offset by horizontal_spacing / 2
|
|
const horizSpacing = CONFIG.hexSize * Math.sqrt(3);
|
|
const vertSpacing = CONFIG.hexSize * 1.5;
|
|
|
|
const cols = Math.ceil(window.innerWidth / horizSpacing) + 3;
|
|
const rows = Math.ceil(window.innerHeight / vertSpacing) + 3;
|
|
|
|
for (let row = -1; row < rows; row++) {
|
|
for (let col = -1; col < cols; col++) {
|
|
// Offset every odd row by half horizontal spacing
|
|
const xOffset = (row % 2 === 1) ? horizSpacing / 2 : 0;
|
|
const x = col * horizSpacing + xOffset;
|
|
const y = row * vertSpacing;
|
|
|
|
hexagons.push(new Hexagon(x, y, CONFIG.hexSize));
|
|
}
|
|
}
|
|
}
|
|
|
|
// Resize handler
|
|
function handleResize() {
|
|
resize();
|
|
generateHexagons();
|
|
}
|
|
|
|
function resize() {
|
|
const dpr = window.devicePixelRatio || 1;
|
|
canvas.width = window.innerWidth * dpr;
|
|
canvas.height = window.innerHeight * dpr;
|
|
canvas.style.width = window.innerWidth + 'px';
|
|
canvas.style.height = window.innerHeight + 'px';
|
|
ctx.scale(dpr, dpr);
|
|
}
|
|
|
|
// Mouse handlers
|
|
function handleMouseMove(e) {
|
|
mouse.x = e.clientX;
|
|
mouse.y = e.clientY;
|
|
}
|
|
|
|
function handleMouseLeave() {
|
|
mouse.x = -1000;
|
|
mouse.y = -1000;
|
|
}
|
|
|
|
// Animation loop
|
|
function animate() {
|
|
if (!isInitialized) return;
|
|
|
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
|
|
const updateRadius = CONFIG.magnetRadius + CONFIG.hexSize * 2;
|
|
|
|
for (let i = 0; i < hexagons.length; i++) {
|
|
const hex = hexagons[i];
|
|
|
|
// Check if hexagon needs updating (near mouse or returning to origin)
|
|
const dx = mouse.x - hex.originX;
|
|
const dy = mouse.y - hex.originY;
|
|
const distance = Math.sqrt(dx * dx + dy * dy);
|
|
const isReturning = Math.abs(hex.currentX - hex.originX) > 0.1 ||
|
|
Math.abs(hex.currentY - hex.originY) > 0.1;
|
|
|
|
if (distance < updateRadius || isReturning) {
|
|
hex.update(mouse.x, mouse.y);
|
|
}
|
|
|
|
hex.draw(ctx);
|
|
}
|
|
|
|
animationId = requestAnimationFrame(animate);
|
|
}
|
|
|
|
// Debounce utility
|
|
function debounce(func, wait) {
|
|
let timeout;
|
|
return function executedFunction(...args) {
|
|
const later = () => {
|
|
clearTimeout(timeout);
|
|
func(...args);
|
|
};
|
|
clearTimeout(timeout);
|
|
timeout = setTimeout(later, wait);
|
|
};
|
|
}
|
|
|
|
// Cleanup
|
|
function destroy() {
|
|
if (animationId) {
|
|
cancelAnimationFrame(animationId);
|
|
}
|
|
window.removeEventListener('resize', handleResize);
|
|
document.removeEventListener('mousemove', handleMouseMove);
|
|
document.removeEventListener('mouseleave', handleMouseLeave);
|
|
isInitialized = false;
|
|
}
|
|
|
|
// Initialize on DOM ready
|
|
if (document.readyState === 'loading') {
|
|
document.addEventListener('DOMContentLoaded', init);
|
|
} else {
|
|
init();
|
|
}
|
|
|
|
// Expose for potential external control
|
|
window.HexBackground = {
|
|
init,
|
|
destroy,
|
|
config: CONFIG
|
|
};
|
|
|
|
})();
|