|
|
@ -22,7 +22,7 @@ export function HeroDotCanvas({ sectionRef }: HeroDotCanvasProps) { |
|
|
const rowsize = 12; |
|
|
const rowsize = 12; |
|
|
const minX = 6; |
|
|
const minX = 6; |
|
|
const maxX = 1854; |
|
|
const maxX = 1854; |
|
|
const minY = 51; |
|
|
|
|
|
|
|
|
const minY = 3; // Grid extends to the top of the card
|
|
|
const maxY = 555; |
|
|
const maxY = 555; |
|
|
|
|
|
|
|
|
// Scaling constants per user reference:
|
|
|
// Scaling constants per user reference:
|
|
|
@ -32,6 +32,8 @@ export function HeroDotCanvas({ sectionRef }: HeroDotCanvasProps) { |
|
|
const dotmin = 1.5; |
|
|
const dotmin = 1.5; |
|
|
const dotsizebase = 4.5; |
|
|
const dotsizebase = 4.5; |
|
|
const decay = 0.3; |
|
|
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:
|
|
|
// Reaction delay & trailing lag constants:
|
|
|
// Lower values = smoother delay and trailing inertia
|
|
|
// Lower values = smoother delay and trailing inertia
|
|
|
@ -86,6 +88,23 @@ export function HeroDotCanvas({ sectionRef }: HeroDotCanvasProps) { |
|
|
draw(); |
|
|
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)
|
|
|
// Bottom curved cutout path from line 1145 of hero_back.svg (covers bottom corners)
|
|
|
const bottomPath = |
|
|
const bottomPath = |
|
|
typeof Path2D !== "undefined" |
|
|
typeof Path2D !== "undefined" |
|
|
@ -132,7 +151,7 @@ export function HeroDotCanvas({ sectionRef }: HeroDotCanvasProps) { |
|
|
ctx.fillRect(0, 0, width, height); |
|
|
ctx.fillRect(0, 0, width, height); |
|
|
|
|
|
|
|
|
// 3. Cut out the bottom curved shape matching line 1145 of hero_back.svg
|
|
|
// 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) { |
|
|
if (bottomPath) { |
|
|
ctx.globalCompositeOperation = "destination-out"; |
|
|
ctx.globalCompositeOperation = "destination-out"; |
|
|
ctx.save(); |
|
|
ctx.save(); |
|
|
@ -143,6 +162,115 @@ export function HeroDotCanvas({ sectionRef }: HeroDotCanvasProps) { |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
ctx.globalCompositeOperation = "source-over"; |
|
|
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 = () => { |
|
|
const loop = () => { |
|
|
|