From 3d2e2e8e5843af931a01802a6869b79a93af61ff Mon Sep 17 00:00:00 2001 From: sina_sajjadi Date: Thu, 3 Sep 2026 16:26:51 +0330 Subject: [PATCH] feat: add HeroDotCanvas component with interactive mouse-following dot grid animation --- components/sections/HeroDotCanvas.tsx | 132 ++- components/sections/HeroSection.tsx | 4 +- public/assets/images/hero_back.svg | 5 +- public/assets/images/hero_logo_patterns.svg | 1013 +++++++++++++++++ .../assets/images/hero_logo_patterns_dark.svg | 1013 +++++++++++++++++ 5 files changed, 2162 insertions(+), 5 deletions(-) create mode 100644 public/assets/images/hero_logo_patterns.svg create mode 100644 public/assets/images/hero_logo_patterns_dark.svg diff --git a/components/sections/HeroDotCanvas.tsx b/components/sections/HeroDotCanvas.tsx index 3aa97b0..e8a938f 100644 --- a/components/sections/HeroDotCanvas.tsx +++ b/components/sections/HeroDotCanvas.tsx @@ -22,7 +22,7 @@ export function HeroDotCanvas({ sectionRef }: HeroDotCanvasProps) { const rowsize = 12; const minX = 6; const maxX = 1854; - const minY = 51; + const minY = 3; // Grid extends to the top of the card const maxY = 555; // Scaling constants per user reference: @@ -32,6 +32,8 @@ export function HeroDotCanvas({ sectionRef }: HeroDotCanvasProps) { const dotmin = 1.5; const dotsizebase = 4.5; const decay = 0.3; + const DOT_HOVER_BRIGHTNESS = 0.03; // Extra brightness boost when hovered (0.13 = subtle/soft, increase for brighter dots) + const LOGO_HOVER_BRIGHTNESS = 0.28; // Peak brightness multiplier for logos (scales based on default brightness) // Reaction delay & trailing lag constants: // Lower values = smoother delay and trailing inertia @@ -86,6 +88,23 @@ export function HeroDotCanvas({ sectionRef }: HeroDotCanvasProps) { draw(); }; + // Load the illuminated bright logo patterns for cursor reaction + const brightLogoImg = new Image(); + let brightLogoLoaded = false; + brightLogoImg.onload = () => { + brightLogoLoaded = true; + draw(); + }; + brightLogoImg.src = "/assets/images/hero_logo_patterns.svg"; + + // Dedicated offscreen canvas for pixel-precise radial spotlight on logos + let spotCanvas: HTMLCanvasElement | null = null; + let spotCtx: CanvasRenderingContext2D | null = null; + if (typeof document !== "undefined") { + spotCanvas = document.createElement("canvas"); + spotCtx = spotCanvas.getContext("2d"); + } + // Bottom curved cutout path from line 1145 of hero_back.svg (covers bottom corners) const bottomPath = typeof Path2D !== "undefined" @@ -132,7 +151,7 @@ export function HeroDotCanvas({ sectionRef }: HeroDotCanvasProps) { ctx.fillRect(0, 0, width, height); // 3. Cut out the bottom curved shape matching line 1145 of hero_back.svg - // Dots on bottom right and left with black background stay 100% invisible! + // Dots on bottom right and left with black background stay invisible at rest if (bottomPath) { ctx.globalCompositeOperation = "destination-out"; ctx.save(); @@ -143,6 +162,115 @@ export function HeroDotCanvas({ sectionRef }: HeroDotCanvasProps) { } ctx.globalCompositeOperation = "source-over"; + + // 4. Subtle hover interaction: + // When hovered, scaled dots receive a gentle, soft brightness boost (~0.13 * ratio): + // - If a dot is invisible as default (on top or in dark corners): it softly emerges at ~0.13 alpha (never fully white!) + // - If a dot is already visible (0.18): it gently brightens to ~0.29 alpha + // - When cursor leaves: it smoothly fades back to its resting state + if (currentStrength > 0.005) { + const minCol = Math.max(0, Math.floor((currentX - maxRadius - minX) / rowsize)); + const maxCol = Math.min(numCols - 1, Math.ceil((currentX + maxRadius - minX) / rowsize)); + const minRow = Math.max(0, Math.floor((currentY - maxRadius - minY) / rowsize)); + const maxRow = Math.min(numRows - 1, Math.ceil((currentY + maxRadius - minY) / rowsize)); + + for (let r = minRow; r <= maxRow; r++) { + const dotY = minY + r * rowsize; + const screenY = dotY * scale; + + for (let c = minCol; c <= maxCol; c++) { + const dotX = minX + c * rowsize; + const screenX = dotX * scale + offsetX; + + if (screenX < -10 || screenX > width + 10) continue; + + const scaler = Math.hypot((currentX - dotX) / rowsize, (currentY - dotY) / rowsize); + const addedSize = Math.max(0, (dotsizebase - dotmin) - scaler * decay); + + if (addedSize > 0) { + const radius = dotmin + addedSize * currentStrength; + const ratio = (addedSize / (dotsizebase - dotmin)) * currentStrength; + + // Smooth top atmospheric fade: tapers smoothly from y=150 down to y=15 + // ensuring dots never hit a sharp ceiling or sudden cut off + const t = Math.max(0, Math.min(1, (dotY - 15) / 135)); + const topFade = t * t * (3 - 2 * t); + + const alpha = DOT_HOVER_BRIGHTNESS * ratio * topFade; + if (alpha > 0.002) { + ctx.fillStyle = `rgba(255, 255, 255, ${alpha.toFixed(3)})`; + ctx.beginPath(); + ctx.arc(screenX, screenY, radius, 0, Math.PI * 2); + ctx.fill(); + } + } + } + } + } + + // 5. Pixel-precise radial spotlight for the TeamBy logo patterns: + // - Logos DO NOT scale (preserve exact geometric dimensions) + // - Radial gradient centered at cursor provides continuous falloff + // - Any icon at the edge of the radius is partially illuminated only where the radius reaches! + if (currentStrength > 0.005 && brightLogoLoaded && spotCanvas && spotCtx) { + const spotRadiusSvg = maxRadius; // 120px in SVG units matching cursor reach + const spotRadiusScreen = spotRadiusSvg * scale; + const spotSize = Math.ceil(spotRadiusScreen * 2); + + const dpr = window.devicePixelRatio || 1; + const targetW = Math.round(spotSize * dpr); + const targetH = Math.round(spotSize * dpr); + + if (spotCanvas.width !== targetW || spotCanvas.height !== targetH) { + spotCanvas.width = targetW; + spotCanvas.height = targetH; + spotCtx.setTransform(dpr, 0, 0, dpr, 0, 0); + } + + const scX = currentX * scale + offsetX; + const scY = currentY * scale; + + // Clear offscreen spotlight canvas + spotCtx.clearRect(0, 0, spotSize, spotSize); + + // Position the full bright logo SVG so that cursor point (scX, scY) lands at (spotRadiusScreen, spotRadiusScreen) + spotCtx.save(); + spotCtx.translate(spotRadiusScreen - scX, spotRadiusScreen - scY); + spotCtx.drawImage(brightLogoImg, offsetX, 0, SVG_WIDTH * scale, SVG_HEIGHT * scale); + spotCtx.restore(); + + // Calculate the logo's intrinsic default brightness at this cursor position in hero_back.svg: + // - Logos under the glowing light orbs at (1325, 194) and (531, 194) are naturally bright + // - Logos in darker areas are naturally dark + // - Logos near the top taper to 0 via top atmospheric fade + const dLeft = Math.hypot((currentX - 531) / 220, (currentY - 194) / 180); + const dRight = Math.hypot((currentX - 1325) / 220, (currentY - 194) / 180); + const orbLight = Math.max(0, 1 - Math.min(dLeft, dRight)); + + const tTop = Math.max(0, Math.min(1, (currentY - 20) / 140)); + const topFade = tTop * tTop * (3 - 2 * tTop); + + const defaultBrightness = (0.2 + 0.8 * orbLight) * topFade; + const peakBoost = LOGO_HOVER_BRIGHTNESS * (0.2 + 0.8 * defaultBrightness) * topFade * currentStrength; + + // Feather with smooth radial gradient centered at (spotRadiusScreen, spotRadiusScreen): + spotCtx.globalCompositeOperation = "destination-in"; + const rad = spotCtx.createRadialGradient( + spotRadiusScreen, spotRadiusScreen, 0, + spotRadiusScreen, spotRadiusScreen, spotRadiusScreen + ); + rad.addColorStop(0, `rgba(255, 255, 255, ${peakBoost.toFixed(3)})`); + rad.addColorStop(0.5, `rgba(255, 255, 255, ${(peakBoost * 0.45).toFixed(3)})`); + rad.addColorStop(0.85, `rgba(255, 255, 255, ${(peakBoost * 0.12).toFixed(3)})`); + rad.addColorStop(1, "rgba(255, 255, 255, 0)"); + + spotCtx.fillStyle = rad; + spotCtx.fillRect(0, 0, spotSize, spotSize); + spotCtx.globalCompositeOperation = "source-over"; + + // Blit the illuminated logo spotlight onto the main canvas + ctx.drawImage(spotCanvas, scX - spotRadiusScreen, scY - spotRadiusScreen, spotSize, spotSize); + } }; const loop = () => { diff --git a/components/sections/HeroSection.tsx b/components/sections/HeroSection.tsx index 0f5a9dd..1d13a16 100644 --- a/components/sections/HeroSection.tsx +++ b/components/sections/HeroSection.tsx @@ -51,7 +51,7 @@ export function HeroSection() { {/* Left Floating 3D Shape (Object_4178_61355: x: 479, y: 436, w: 56, h: 56) */} -
+
{/* Right Floating 3D Shape (Object_4178_61358: x: 1397, y: 458, w: 48, h: 48) */} -
+
+ @@ -1180,7 +1181,9 @@ - + + + diff --git a/public/assets/images/hero_logo_patterns.svg b/public/assets/images/hero_logo_patterns.svg new file mode 100644 index 0000000..316d59e --- /dev/null +++ b/public/assets/images/hero_logo_patterns.svg @@ -0,0 +1,1013 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/public/assets/images/hero_logo_patterns_dark.svg b/public/assets/images/hero_logo_patterns_dark.svg new file mode 100644 index 0000000..d424eec --- /dev/null +++ b/public/assets/images/hero_logo_patterns_dark.svg @@ -0,0 +1,1013 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file