|
|
|
@ -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:
|
|
|
|
|