diff --git a/components/sections/HeroDotCanvas.tsx b/components/sections/HeroDotCanvas.tsx index e8a938f..108f127 100644 --- a/components/sections/HeroDotCanvas.tsx +++ b/components/sections/HeroDotCanvas.tsx @@ -105,6 +105,8 @@ export function HeroDotCanvas({ sectionRef }: HeroDotCanvasProps) { spotCtx = spotCanvas.getContext("2d"); } + + // Bottom curved cutout path from line 1145 of hero_back.svg (covers bottom corners) const bottomPath = typeof Path2D !== "undefined" @@ -116,8 +118,9 @@ export function HeroDotCanvas({ sectionRef }: HeroDotCanvasProps) { const draw = () => { ctx.clearRect(0, 0, width, height); - // 1. Draw all dots with their dynamic radius, using ONLY the single standard dot fill - // No color change, no alpha change: only the scale changes! + // 1. Single-pass dot rendering: + // Every dot is drawn EXACTLY ONCE with its dynamic radius r + // Eliminates any duplicate dot layers completely! ctx.fillStyle = "rgba(255, 255, 255, 0.18)"; ctx.beginPath(); for (let i = 0; i < dots.length; i++) { @@ -140,7 +143,7 @@ export function HeroDotCanvas({ sectionRef }: HeroDotCanvasProps) { ctx.fill(); // 2. Apply vertical fade gradient mask matching the SVG's dotFadeMask - // Invisible dots on top stay 100% invisible; transparent dots stay transparent! + // Invisible dots on top stay invisible; transparent dots stay transparent! ctx.globalCompositeOperation = "destination-in"; const fadeGrad = ctx.createLinearGradient(0, 45 * scale, 0, 566 * scale); fadeGrad.addColorStop(0, "rgba(255, 255, 255, 0)"); @@ -150,8 +153,8 @@ export function HeroDotCanvas({ sectionRef }: HeroDotCanvasProps) { ctx.fillStyle = fadeGrad; 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 invisible at rest + // 3. Cut out the bottom curved shape matching line 1145 of hero_back.svg: + // Preserves the authentic curved arch of the dots pattern (matching previous commits) if (bottomPath) { ctx.globalCompositeOperation = "destination-out"; ctx.save(); @@ -163,49 +166,21 @@ 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(); - } - } - } - } + // 4. Subtle hover illumination on dots: + // Uses source-atop so it ONLY gently highlights the already-drawn & masked dots, + // strictly guaranteeing that NO dots outside the bottom curve are ever illuminated! + if (DOT_HOVER_BRIGHTNESS > 0.001 && currentStrength > 0.005) { + ctx.globalCompositeOperation = "source-atop"; + const scX = currentX * scale + offsetX; + const scY = currentY * scale; + const pRad = maxRadius * scale; + const dotLight = ctx.createRadialGradient(scX, scY, 0, scX, scY, pRad); + dotLight.addColorStop(0, `rgba(255, 255, 255, ${(DOT_HOVER_BRIGHTNESS * 6 * currentStrength).toFixed(3)})`); + dotLight.addColorStop(0.6, `rgba(255, 255, 255, ${(DOT_HOVER_BRIGHTNESS * 2 * currentStrength).toFixed(3)})`); + dotLight.addColorStop(1, "rgba(255, 255, 255, 0)"); + ctx.fillStyle = dotLight; + ctx.fillRect(scX - pRad, scY - pRad, pRad * 2, pRad * 2); + ctx.globalCompositeOperation = "source-over"; } // 5. Pixel-precise radial spotlight for the TeamBy logo patterns: diff --git a/public/assets/images/hero_back.svg b/public/assets/images/hero_back.svg index 82cff53..b504266 100644 --- a/public/assets/images/hero_back.svg +++ b/public/assets/images/hero_back.svg @@ -1030,7 +1030,6 @@ - @@ -1181,9 +1180,6 @@ - - - @@ -1209,6 +1205,5 @@ -