From 40c67c99f3f96374482a715f0b97b1920bf178e3 Mon Sep 17 00:00:00 2001 From: Ihor_Zhekov Date: Fri, 6 Feb 2026 14:03:59 +0100 Subject: [PATCH] 765 --- Profice WebSite/scripts/hex-background.js | 405 ++++++++++------------ Profice WebSite/style/design.css | 14 +- Profice WebSite/style/tech-onepager.css | 90 +++++ 3 files changed, 284 insertions(+), 225 deletions(-) diff --git a/Profice WebSite/scripts/hex-background.js b/Profice WebSite/scripts/hex-background.js index 809a480..980f138 100644 --- a/Profice WebSite/scripts/hex-background.js +++ b/Profice WebSite/scripts/hex-background.js @@ -1,7 +1,6 @@ /** - * Hexagonal Magnetism Background - * Rigid hexagon grid with mouse-based magnetism effect - * Hexagons move as whole units - no vertex deformation + * Hexagonal Magnetism Background - OPTIMIZED + * Only renders visible hexagons, uses viewport culling */ (function() { @@ -14,210 +13,128 @@ maxLineWidth: 2.5, baseOpacity: 0.15, maxOpacity: 0.6, - magnetRadius: 350, // Larger radius for 16:9/16:10 monitors - maxDisplacement: 15, - returnSpeed: 0.35, // Faster return to reset colors quickly - // Base color (neutral gray) + magnetRadius: 280, + maxDisplacement: 12, + returnSpeed: 0.4, + // Pre-calculated colors baseR: 119, baseG: 119, baseB: 100, - // Colors for proximity effect (teal to orange) 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 hexagons = []; 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(); - // 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 && mouseX > -500 && mouseY > -500) { - // 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 - use faster speed for reset - const resetSpeed = CONFIG.returnSpeed * 1.5; - this.currentX += (this.originX - this.currentX) * resetSpeed; - this.currentY += (this.originY - this.currentY) * resetSpeed; - - // Fade back to default - faster color reset - this.opacity += (CONFIG.baseOpacity - this.opacity) * resetSpeed; - this.lineWidth += (CONFIG.lineWidth - this.lineWidth) * resetSpeed; - this.colorInfluence += (0 - this.colorInfluence) * resetSpeed; - - // Force reset if very close to default - if (this.colorInfluence < 0.005) this.colorInfluence = 0; - if (Math.abs(this.opacity - CONFIG.baseOpacity) < 0.005) this.opacity = CONFIG.baseOpacity; - if (Math.abs(this.lineWidth - CONFIG.lineWidth) < 0.01) this.lineWidth = CONFIG.lineWidth; - } - } - - 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(); - - // Smooth color interpolation: gray -> teal -> orange based on proximity - let r, g, b; - if (this.colorInfluence < 0.01) { - // Base gray color - r = CONFIG.baseR; - g = CONFIG.baseG; - b = CONFIG.baseB; - } else if (this.colorInfluence < 0.6) { - // Gray to teal (smooth transition) - const t = this.colorInfluence / 0.6; - r = Math.round(CONFIG.baseR + (CONFIG.tealR - CONFIG.baseR) * t); - g = Math.round(CONFIG.baseG + (CONFIG.tealG - CONFIG.baseG) * t); - b = Math.round(CONFIG.baseB + (CONFIG.tealB - CONFIG.baseB) * t); - } else { - // Teal to orange (only very close to cursor) - const t = (this.colorInfluence - 0.6) / 0.4; - 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})`; - ctx.lineWidth = this.lineWidth; - ctx.stroke(); - } - } - - // Easing function for smooth animations + // Easing function function easeOutCubic(t) { - return 1 - Math.pow(1 - t, 3); + return 1 - (1 - t) * (1 - t) * (1 - t); } - // Initialize canvas + // 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() { canvas = document.getElementById('hexCanvas'); - if (!canvas) { - console.warn('hexCanvas element not found'); - return; - } + if (!canvas) return; - ctx = canvas.getContext('2d'); + ctx = canvas.getContext('2d', { alpha: true }); resize(); - generateHexagons(); - // Event listeners - window.addEventListener('resize', debounce(handleResize, 150)); - document.addEventListener('mousemove', handleMouseMove); + 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(); } - // 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; - - // Use full screen dimensions - const width = Math.max(window.innerWidth, document.documentElement.clientWidth); - const height = Math.max(window.innerHeight, document.documentElement.clientHeight); - - const cols = Math.ceil(width / horizSpacing) + 4; - const rows = Math.ceil(height / vertSpacing) + 4; - - 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; - // Use full document dimensions to cover entire scrollable page - const width = Math.max(window.innerWidth, document.documentElement.clientWidth); - const height = Math.max(window.innerHeight, document.documentElement.clientHeight); + const dpr = Math.min(window.devicePixelRatio || 1, 2); // Cap DPR at 2 + canvasWidth = window.innerWidth; + canvasHeight = window.innerHeight; - canvas.width = width * dpr; - canvas.height = height * dpr; - canvas.style.width = width + 'px'; - canvas.style.height = height + 'px'; + 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'; - // Reset transform and apply new scale ctx.setTransform(1, 0, 0, 1, 0, 0); ctx.scale(dpr, dpr); } - // Mouse handlers function handleMouseMove(e) { mouse.x = e.clientX; mouse.y = e.clientY; @@ -228,74 +145,134 @@ mouse.y = -1000; } - // Animation loop + function handleScroll() { + scrollY = window.scrollY; + } + + // Main animation loop - optimized function animate() { if (!isInitialized) return; - ctx.clearRect(0, 0, canvas.width, canvas.height); + ctx.clearRect(0, 0, canvasWidth, canvasHeight); - const updateRadius = CONFIG.magnetRadius + CONFIG.hexSize * 2; + // 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; - for (let i = 0; i < hexagons.length; i++) { - const hex = hexagons[i]; + 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; - // Check if hexagon needs updating (near mouse or returning to origin) + // 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 = mouse.y - hex.originY; - const distance = Math.sqrt(dx * dx + dy * dy); - - // Check if position OR color needs to return to default - const isReturning = Math.abs(hex.currentX - hex.originX) > 0.05 || - Math.abs(hex.currentY - hex.originY) > 0.05 || - hex.colorInfluence > 0.001 || - hex.opacity > CONFIG.baseOpacity + 0.001; + const dy = mouseWorldY - hex.originY; + const distSq = dx * dx + dy * dy; - if (distance < updateRadius || isReturning) { - hex.update(mouse.x, mouse.y); + 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); + } } + } - hex.draw(ctx); + // 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); } - // Debounce utility function debounce(func, wait) { let timeout; - return function executedFunction(...args) { - const later = () => { - clearTimeout(timeout); - func(...args); - }; + return function(...args) { clearTimeout(timeout); - timeout = setTimeout(later, wait); + timeout = setTimeout(() => func(...args), wait); }; } - // Cleanup function destroy() { - if (animationId) { - cancelAnimationFrame(animationId); - } - window.removeEventListener('resize', handleResize); - document.removeEventListener('mousemove', handleMouseMove); - document.removeEventListener('mouseleave', handleMouseLeave); + if (animationId) cancelAnimationFrame(animationId); isInitialized = false; + activeHexagons.clear(); } - // 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 - }; - + window.HexBackground = { init, destroy, config: CONFIG }; })(); diff --git a/Profice WebSite/style/design.css b/Profice WebSite/style/design.css index d0c9905..ff4ae5c 100644 --- a/Profice WebSite/style/design.css +++ b/Profice WebSite/style/design.css @@ -18,6 +18,8 @@ body { background-color: var(--primary-light); color: var(--primary-dark); min-height: 100vh; + position: relative; + overflow-x: hidden; } /* Hexagonal Canvas Background */ @@ -56,12 +58,8 @@ body { background-color 0.4s cubic-bezier(0.25, 0.46, 0.45, 0.94); /* Performance optimizations */ - will-change: height, padding, transform; transform: translateZ(0); - backface-visibility: hidden; -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; - contain: layout style paint; /* Prevent sub-pixel rendering issues */ box-shadow: 0 0 0 rgba(0,0,0,0); @@ -128,8 +126,7 @@ body { .logo { height: 50px; width: auto; - transition: height 0.4s cubic-bezier(0.25, 0.46, 0.45, 0.94); - will-change: height; + transition: height 0.3s ease-out; } .top-banner.scrolled .logo { @@ -149,7 +146,6 @@ body { justify-content: center; align-items: center; transition: all 0.4s cubic-bezier(0.25, 0.46, 0.45, 0.94); - will-change: width, height; } .top-banner.scrolled .menu-toggle, @@ -166,7 +162,6 @@ body { border-radius: 25px; transition: font-size 0.4s cubic-bezier(0.25, 0.46, 0.45, 0.94), padding 0.4s cubic-bezier(0.25, 0.46, 0.45, 0.94); - will-change: font-size, padding; } .top-banner.scrolled .opening-hours { @@ -354,7 +349,6 @@ body { padding: 10px 20px; border-radius: 25px; transition: font-size 0.25s cubic-bezier(0.4, 0, 0.2, 1), padding 0.25s cubic-bezier(0.4, 0, 0.2, 1); - will-change: font-size, padding; } .menu-toggle { @@ -371,7 +365,6 @@ body { justify-content: center; gap: 5px; transition: background 0.3s ease, color 0.3s ease, width 0.25s cubic-bezier(0.4, 0, 0.2, 1), height 0.25s cubic-bezier(0.4, 0, 0.2, 1); - will-change: width, height; } #cursorToggle { @@ -387,7 +380,6 @@ body { justify-content: center; padding: 0; transition: width 0.25s cubic-bezier(0.4, 0, 0.2, 1), height 0.25s cubic-bezier(0.4, 0, 0.2, 1), background 0.3s ease, border-color 0.3s ease; - will-change: width, height; } .cursor-icon { diff --git a/Profice WebSite/style/tech-onepager.css b/Profice WebSite/style/tech-onepager.css index 7f67c0f..bcbf906 100644 --- a/Profice WebSite/style/tech-onepager.css +++ b/Profice WebSite/style/tech-onepager.css @@ -2105,6 +2105,96 @@ } /* ===== RESPONSIVE DESIGN ===== */ + +/* Ultra-wide monitors (16:9, 16:10 - 1920px+) */ +@media (min-width: 1920px) { + .hero-container { + max-width: 1600px; + gap: 100px; + } + + .hero-headline { + font-size: 4rem; + } + + .hero-subline, + .hero-proof { + font-size: 1.4rem; + } + + .hero-right { + height: 600px; + } + + .system-graphic { + transform: scale(1.1); + } + + .node { + min-width: 140px; + padding: 20px; + } + + .central-node { + min-width: 160px; + max-width: 180px; + padding: 12px 24px; + } + + .section-headline { + font-size: 3rem; + } + + .systeme-grid { + gap: 50px; + max-width: 1800px; + } + + .system-card { + padding: 50px; + } + + .results-grid { + max-width: 1400px; + } + + .data-card { + padding: 45px; + } + + .card-metric { + font-size: 4rem; + } +} + +/* Extra large screens (2560px+) */ +@media (min-width: 2560px) { + .hero-container { + max-width: 2000px; + gap: 120px; + } + + .hero-headline { + font-size: 4.5rem; + } + + .hero-right { + height: 700px; + } + + .system-graphic { + transform: scale(1.2); + } + + .container { + max-width: 1800px; + } + + .systeme-grid { + max-width: 2200px; + } +} + @media (max-width: 1024px) { .hero-container { grid-template-columns: 1fr;