|
|
@ -25,21 +25,17 @@ export function HeroDotCanvas({ sectionRef }: HeroDotCanvasProps) { |
|
|
const minY = 3; // Grid extends to the top of the card
|
|
|
const minY = 3; // Grid extends to the top of the card
|
|
|
const maxY = 555; |
|
|
const maxY = 555; |
|
|
|
|
|
|
|
|
// Scaling constants per user reference:
|
|
|
|
|
|
// dotmin: default radius of the dots (1.5px)
|
|
|
|
|
|
// dotsizebase: peak radius directly under the cursor (2.5px - smaller and more refined)
|
|
|
|
|
|
// decay: size drop-off per grid unit (0.32) -> ~38px tight interaction radius
|
|
|
|
|
|
|
|
|
// Scaling constants per original reference:
|
|
|
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)
|
|
|
|
|
|
|
|
|
const DOT_HOVER_BRIGHTNESS = 0.03; // Extra brightness boost when hovered
|
|
|
|
|
|
const LOGO_HOVER_BRIGHTNESS = 0.28; // Peak brightness multiplier for logos
|
|
|
|
|
|
|
|
|
// Reaction delay & trailing lag constants:
|
|
|
// Reaction delay & trailing lag constants:
|
|
|
// Lower values = smoother delay and trailing inertia
|
|
|
|
|
|
const LERP_LAG = 0.07; // Delay in tracking cursor movement
|
|
|
|
|
|
const FADE_IN_LAG = 0.07; // Delay to smoothly bloom up
|
|
|
|
|
|
const FADE_OUT_LAG = 0.04; // Lingering delay before returning to rest
|
|
|
|
|
|
|
|
|
const LERP_LAG = 0.07; |
|
|
|
|
|
const FADE_IN_LAG = 0.07; |
|
|
|
|
|
const FADE_OUT_LAG = 0.04; |
|
|
|
|
|
|
|
|
// Pre-calculate dot grid coordinates
|
|
|
// Pre-calculate dot grid coordinates
|
|
|
const dots: { x: number; y: number }[] = []; |
|
|
const dots: { x: number; y: number }[] = []; |
|
|
@ -49,8 +45,6 @@ export function HeroDotCanvas({ sectionRef }: HeroDotCanvasProps) { |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
const numCols = Math.round((maxX - minX) / rowsize) + 1; // 155
|
|
|
|
|
|
const numRows = Math.round((maxY - minY) / rowsize) + 1; // 43
|
|
|
|
|
|
const maxRadius = ((dotsizebase - dotmin) / decay) * rowsize; // ~120px
|
|
|
const maxRadius = ((dotsizebase - dotmin) / decay) * rowsize; // ~120px
|
|
|
|
|
|
|
|
|
// Cursor tracking state
|
|
|
// Cursor tracking state
|
|
|
@ -84,8 +78,6 @@ export function HeroDotCanvas({ sectionRef }: HeroDotCanvasProps) { |
|
|
// Sizing matches Next.js Image object-cover object-top
|
|
|
// Sizing matches Next.js Image object-cover object-top
|
|
|
scale = height / SVG_HEIGHT; |
|
|
scale = height / SVG_HEIGHT; |
|
|
offsetX = (width - SVG_WIDTH * scale) / 2; |
|
|
offsetX = (width - SVG_WIDTH * scale) / 2; |
|
|
|
|
|
|
|
|
draw(); |
|
|
|
|
|
}; |
|
|
}; |
|
|
|
|
|
|
|
|
// Load the illuminated bright logo patterns for cursor reaction
|
|
|
// Load the illuminated bright logo patterns for cursor reaction
|
|
|
@ -93,7 +85,6 @@ export function HeroDotCanvas({ sectionRef }: HeroDotCanvasProps) { |
|
|
let brightLogoLoaded = false; |
|
|
let brightLogoLoaded = false; |
|
|
brightLogoImg.onload = () => { |
|
|
brightLogoImg.onload = () => { |
|
|
brightLogoLoaded = true; |
|
|
brightLogoLoaded = true; |
|
|
draw(); |
|
|
|
|
|
}; |
|
|
}; |
|
|
brightLogoImg.src = "/assets/images/hero_logo_patterns.svg"; |
|
|
brightLogoImg.src = "/assets/images/hero_logo_patterns.svg"; |
|
|
|
|
|
|
|
|
@ -105,9 +96,7 @@ export function HeroDotCanvas({ sectionRef }: HeroDotCanvasProps) { |
|
|
spotCtx = spotCanvas.getContext("2d"); |
|
|
spotCtx = spotCanvas.getContext("2d"); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Bottom curved cutout path from line 1145 of hero_back.svg (covers bottom corners)
|
|
|
|
|
|
|
|
|
// Bottom curved cutout path from line 1144 of hero_back.svg (covers bottom corners)
|
|
|
const bottomPath = |
|
|
const bottomPath = |
|
|
typeof Path2D !== "undefined" |
|
|
typeof Path2D !== "undefined" |
|
|
? new Path2D( |
|
|
? new Path2D( |
|
|
@ -115,16 +104,18 @@ export function HeroDotCanvas({ sectionRef }: HeroDotCanvasProps) { |
|
|
) |
|
|
) |
|
|
: null; |
|
|
: null; |
|
|
|
|
|
|
|
|
const draw = () => { |
|
|
|
|
|
|
|
|
const draw = (t: number) => { |
|
|
ctx.clearRect(0, 0, width, height); |
|
|
ctx.clearRect(0, 0, width, height); |
|
|
|
|
|
|
|
|
// 1. Single-pass dot rendering:
|
|
|
// 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)"; |
|
|
|
|
|
|
|
|
// Preserves original look, with dynamic, fast acoustic sound-wave undulation
|
|
|
|
|
|
ctx.fillStyle = "rgba(255, 255, 255, 0.22)"; |
|
|
ctx.beginPath(); |
|
|
ctx.beginPath(); |
|
|
|
|
|
|
|
|
for (let i = 0; i < dots.length; i++) { |
|
|
for (let i = 0; i < dots.length; i++) { |
|
|
const dot = dots[i]; |
|
|
const dot = dots[i]; |
|
|
|
|
|
|
|
|
|
|
|
// Static dot grid (no wave animation on dots per user request)
|
|
|
const screenX = dot.x * scale + offsetX; |
|
|
const screenX = dot.x * scale + offsetX; |
|
|
const screenY = dot.y * scale; |
|
|
const screenY = dot.y * scale; |
|
|
|
|
|
|
|
|
@ -132,9 +123,12 @@ export function HeroDotCanvas({ sectionRef }: HeroDotCanvasProps) { |
|
|
|
|
|
|
|
|
let r = dotmin; |
|
|
let r = dotmin; |
|
|
if (currentStrength > 0.005) { |
|
|
if (currentStrength > 0.005) { |
|
|
const scaler = Math.hypot((currentX - dot.x) / rowsize, (currentY - dot.y) / rowsize); |
|
|
|
|
|
const addedSize = Math.max(0, (dotsizebase - dotmin) - scaler * decay); |
|
|
|
|
|
r = dotmin + addedSize * currentStrength; |
|
|
|
|
|
|
|
|
const scaler = Math.hypot( |
|
|
|
|
|
(currentX - dot.x) / rowsize, |
|
|
|
|
|
(currentY - dot.y) / rowsize |
|
|
|
|
|
); |
|
|
|
|
|
const addedSize = Math.max(0, dotsizebase - dotmin - scaler * decay); |
|
|
|
|
|
r += addedSize * currentStrength; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
ctx.moveTo(screenX + r, screenY); |
|
|
ctx.moveTo(screenX + r, screenY); |
|
|
@ -142,8 +136,7 @@ export function HeroDotCanvas({ sectionRef }: HeroDotCanvasProps) { |
|
|
} |
|
|
} |
|
|
ctx.fill(); |
|
|
ctx.fill(); |
|
|
|
|
|
|
|
|
// 2. Apply vertical fade gradient mask matching the SVG's dotFadeMask
|
|
|
|
|
|
// Invisible dots on top stay invisible; transparent dots stay transparent!
|
|
|
|
|
|
|
|
|
// 2. Apply vertical fade gradient mask matching original dotFadeMask
|
|
|
ctx.globalCompositeOperation = "destination-in"; |
|
|
ctx.globalCompositeOperation = "destination-in"; |
|
|
const fadeGrad = ctx.createLinearGradient(0, 45 * scale, 0, 566 * scale); |
|
|
const fadeGrad = ctx.createLinearGradient(0, 45 * scale, 0, 566 * scale); |
|
|
fadeGrad.addColorStop(0, "rgba(255, 255, 255, 0)"); |
|
|
fadeGrad.addColorStop(0, "rgba(255, 255, 255, 0)"); |
|
|
@ -153,8 +146,7 @@ export function HeroDotCanvas({ sectionRef }: HeroDotCanvasProps) { |
|
|
ctx.fillStyle = fadeGrad; |
|
|
ctx.fillStyle = fadeGrad; |
|
|
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:
|
|
|
|
|
|
// Preserves the authentic curved arch of the dots pattern (matching previous commits)
|
|
|
|
|
|
|
|
|
// 3. Cut out the bottom curved shape matching line 1144 of hero_back.svg
|
|
|
if (bottomPath) { |
|
|
if (bottomPath) { |
|
|
ctx.globalCompositeOperation = "destination-out"; |
|
|
ctx.globalCompositeOperation = "destination-out"; |
|
|
ctx.save(); |
|
|
ctx.save(); |
|
|
@ -167,28 +159,29 @@ export function HeroDotCanvas({ sectionRef }: HeroDotCanvasProps) { |
|
|
ctx.globalCompositeOperation = "source-over"; |
|
|
ctx.globalCompositeOperation = "source-over"; |
|
|
|
|
|
|
|
|
// 4. Subtle hover illumination on dots:
|
|
|
// 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) { |
|
|
if (DOT_HOVER_BRIGHTNESS > 0.001 && currentStrength > 0.005) { |
|
|
ctx.globalCompositeOperation = "source-atop"; |
|
|
ctx.globalCompositeOperation = "source-atop"; |
|
|
const scX = currentX * scale + offsetX; |
|
|
const scX = currentX * scale + offsetX; |
|
|
const scY = currentY * scale; |
|
|
const scY = currentY * scale; |
|
|
const pRad = maxRadius * scale; |
|
|
const pRad = maxRadius * scale; |
|
|
const dotLight = ctx.createRadialGradient(scX, scY, 0, scX, scY, pRad); |
|
|
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( |
|
|
|
|
|
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)"); |
|
|
dotLight.addColorStop(1, "rgba(255, 255, 255, 0)"); |
|
|
ctx.fillStyle = dotLight; |
|
|
ctx.fillStyle = dotLight; |
|
|
ctx.fillRect(scX - pRad, scY - pRad, pRad * 2, pRad * 2); |
|
|
ctx.fillRect(scX - pRad, scY - pRad, pRad * 2, pRad * 2); |
|
|
ctx.globalCompositeOperation = "source-over"; |
|
|
ctx.globalCompositeOperation = "source-over"; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
// 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!
|
|
|
|
|
|
|
|
|
// 5. Pixel-precise radial spotlight for TeamBy logo patterns:
|
|
|
if (currentStrength > 0.005 && brightLogoLoaded && spotCanvas && spotCtx) { |
|
|
if (currentStrength > 0.005 && brightLogoLoaded && spotCanvas && spotCtx) { |
|
|
const spotRadiusSvg = maxRadius; // 120px in SVG units matching cursor reach
|
|
|
|
|
|
|
|
|
const spotRadiusSvg = maxRadius; |
|
|
const spotRadiusScreen = spotRadiusSvg * scale; |
|
|
const spotRadiusScreen = spotRadiusSvg * scale; |
|
|
const spotSize = Math.ceil(spotRadiusScreen * 2); |
|
|
const spotSize = Math.ceil(spotRadiusScreen * 2); |
|
|
|
|
|
|
|
|
@ -205,19 +198,18 @@ export function HeroDotCanvas({ sectionRef }: HeroDotCanvasProps) { |
|
|
const scX = currentX * scale + offsetX; |
|
|
const scX = currentX * scale + offsetX; |
|
|
const scY = currentY * scale; |
|
|
const scY = currentY * scale; |
|
|
|
|
|
|
|
|
// Clear offscreen spotlight canvas
|
|
|
|
|
|
spotCtx.clearRect(0, 0, spotSize, spotSize); |
|
|
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.save(); |
|
|
spotCtx.translate(spotRadiusScreen - scX, spotRadiusScreen - scY); |
|
|
spotCtx.translate(spotRadiusScreen - scX, spotRadiusScreen - scY); |
|
|
spotCtx.drawImage(brightLogoImg, offsetX, 0, SVG_WIDTH * scale, SVG_HEIGHT * scale); |
|
|
|
|
|
|
|
|
spotCtx.drawImage( |
|
|
|
|
|
brightLogoImg, |
|
|
|
|
|
offsetX, |
|
|
|
|
|
0, |
|
|
|
|
|
SVG_WIDTH * scale, |
|
|
|
|
|
SVG_HEIGHT * scale |
|
|
|
|
|
); |
|
|
spotCtx.restore(); |
|
|
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 dLeft = Math.hypot((currentX - 531) / 220, (currentY - 194) / 180); |
|
|
const dRight = Math.hypot((currentX - 1325) / 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 orbLight = Math.max(0, 1 - Math.min(dLeft, dRight)); |
|
|
@ -226,52 +218,62 @@ export function HeroDotCanvas({ sectionRef }: HeroDotCanvasProps) { |
|
|
const topFade = tTop * tTop * (3 - 2 * tTop); |
|
|
const topFade = tTop * tTop * (3 - 2 * tTop); |
|
|
|
|
|
|
|
|
const defaultBrightness = (0.2 + 0.8 * orbLight) * topFade; |
|
|
const defaultBrightness = (0.2 + 0.8 * orbLight) * topFade; |
|
|
const peakBoost = LOGO_HOVER_BRIGHTNESS * (0.2 + 0.8 * defaultBrightness) * topFade * currentStrength; |
|
|
|
|
|
|
|
|
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"; |
|
|
spotCtx.globalCompositeOperation = "destination-in"; |
|
|
const rad = spotCtx.createRadialGradient( |
|
|
const rad = spotCtx.createRadialGradient( |
|
|
spotRadiusScreen, spotRadiusScreen, 0, |
|
|
|
|
|
spotRadiusScreen, spotRadiusScreen, spotRadiusScreen |
|
|
|
|
|
|
|
|
spotRadiusScreen, |
|
|
|
|
|
spotRadiusScreen, |
|
|
|
|
|
0, |
|
|
|
|
|
spotRadiusScreen, |
|
|
|
|
|
spotRadiusScreen, |
|
|
|
|
|
spotRadiusScreen |
|
|
); |
|
|
); |
|
|
rad.addColorStop(0, `rgba(255, 255, 255, ${peakBoost.toFixed(3)})`); |
|
|
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( |
|
|
|
|
|
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)"); |
|
|
rad.addColorStop(1, "rgba(255, 255, 255, 0)"); |
|
|
|
|
|
|
|
|
spotCtx.fillStyle = rad; |
|
|
spotCtx.fillStyle = rad; |
|
|
spotCtx.fillRect(0, 0, spotSize, spotSize); |
|
|
spotCtx.fillRect(0, 0, spotSize, spotSize); |
|
|
spotCtx.globalCompositeOperation = "source-over"; |
|
|
spotCtx.globalCompositeOperation = "source-over"; |
|
|
|
|
|
|
|
|
// Blit the illuminated logo spotlight onto the main canvas
|
|
|
|
|
|
ctx.drawImage(spotCanvas, scX - spotRadiusScreen, scY - spotRadiusScreen, spotSize, spotSize); |
|
|
|
|
|
|
|
|
ctx.drawImage( |
|
|
|
|
|
spotCanvas, |
|
|
|
|
|
scX - spotRadiusScreen, |
|
|
|
|
|
scY - spotRadiusScreen, |
|
|
|
|
|
spotSize, |
|
|
|
|
|
spotSize |
|
|
|
|
|
); |
|
|
} |
|
|
} |
|
|
}; |
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
// Smooth animation loop for subtle acoustic dot waviness and hover reaction
|
|
|
const loop = () => { |
|
|
const loop = () => { |
|
|
if (isHovering) { |
|
|
if (isHovering) { |
|
|
currentX += (targetX - currentX) * LERP_LAG; |
|
|
currentX += (targetX - currentX) * LERP_LAG; |
|
|
currentY += (targetY - currentY) * LERP_LAG; |
|
|
currentY += (targetY - currentY) * LERP_LAG; |
|
|
currentStrength += (targetStrength - currentStrength) * FADE_IN_LAG; |
|
|
currentStrength += (targetStrength - currentStrength) * FADE_IN_LAG; |
|
|
} else { |
|
|
|
|
|
|
|
|
} else if (currentStrength > 0.001) { |
|
|
currentStrength += (0 - currentStrength) * FADE_OUT_LAG; |
|
|
currentStrength += (0 - currentStrength) * FADE_OUT_LAG; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
draw(); |
|
|
|
|
|
|
|
|
|
|
|
if (isHovering || currentStrength > 0.005) { |
|
|
|
|
|
animFrameId = requestAnimationFrame(loop); |
|
|
|
|
|
} else { |
|
|
} else { |
|
|
currentStrength = 0; |
|
|
currentStrength = 0; |
|
|
draw(); |
|
|
|
|
|
animFrameId = null; |
|
|
|
|
|
} |
|
|
} |
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
const startLoop = () => { |
|
|
|
|
|
if (!animFrameId) { |
|
|
|
|
|
animFrameId = requestAnimationFrame(loop); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
const t = performance.now() * 0.001; |
|
|
|
|
|
draw(t); |
|
|
|
|
|
|
|
|
|
|
|
animFrameId = requestAnimationFrame(loop); |
|
|
}; |
|
|
}; |
|
|
|
|
|
|
|
|
const handleMouseMove = (e: MouseEvent) => { |
|
|
const handleMouseMove = (e: MouseEvent) => { |
|
|
@ -279,7 +281,6 @@ export function HeroDotCanvas({ sectionRef }: HeroDotCanvasProps) { |
|
|
const targetArea = section || canvas; |
|
|
const targetArea = section || canvas; |
|
|
const sRect = targetArea.getBoundingClientRect(); |
|
|
const sRect = targetArea.getBoundingClientRect(); |
|
|
|
|
|
|
|
|
// Check if cursor is anywhere within the hero section
|
|
|
|
|
|
if ( |
|
|
if ( |
|
|
e.clientX >= sRect.left && |
|
|
e.clientX >= sRect.left && |
|
|
e.clientX <= sRect.right && |
|
|
e.clientX <= sRect.right && |
|
|
@ -287,7 +288,6 @@ export function HeroDotCanvas({ sectionRef }: HeroDotCanvasProps) { |
|
|
e.clientY <= sRect.bottom |
|
|
e.clientY <= sRect.bottom |
|
|
) { |
|
|
) { |
|
|
const cRect = canvas.getBoundingClientRect(); |
|
|
const cRect = canvas.getBoundingClientRect(); |
|
|
// Convert client coordinates to SVG grid coordinate space
|
|
|
|
|
|
targetX = (e.clientX - cRect.left - offsetX) / scale; |
|
|
targetX = (e.clientX - cRect.left - offsetX) / scale; |
|
|
targetY = (e.clientY - cRect.top) / scale; |
|
|
targetY = (e.clientY - cRect.top) / scale; |
|
|
targetStrength = 1; |
|
|
targetStrength = 1; |
|
|
@ -297,25 +297,23 @@ export function HeroDotCanvas({ sectionRef }: HeroDotCanvasProps) { |
|
|
currentX = targetX; |
|
|
currentX = targetX; |
|
|
currentY = targetY; |
|
|
currentY = targetY; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
startLoop(); |
|
|
|
|
|
} else if (isHovering) { |
|
|
} else if (isHovering) { |
|
|
targetStrength = 0; |
|
|
targetStrength = 0; |
|
|
isHovering = false; |
|
|
isHovering = false; |
|
|
startLoop(); |
|
|
|
|
|
} |
|
|
} |
|
|
}; |
|
|
}; |
|
|
|
|
|
|
|
|
const handleMouseLeave = () => { |
|
|
const handleMouseLeave = () => { |
|
|
targetStrength = 0; |
|
|
targetStrength = 0; |
|
|
isHovering = false; |
|
|
isHovering = false; |
|
|
startLoop(); |
|
|
|
|
|
}; |
|
|
}; |
|
|
|
|
|
|
|
|
resize(); |
|
|
resize(); |
|
|
const ro = new ResizeObserver(() => resize()); |
|
|
const ro = new ResizeObserver(() => resize()); |
|
|
ro.observe(canvas); |
|
|
ro.observe(canvas); |
|
|
|
|
|
|
|
|
|
|
|
animFrameId = requestAnimationFrame(loop); |
|
|
|
|
|
|
|
|
window.addEventListener("mousemove", handleMouseMove, { passive: true }); |
|
|
window.addEventListener("mousemove", handleMouseMove, { passive: true }); |
|
|
window.addEventListener("mouseleave", handleMouseLeave); |
|
|
window.addEventListener("mouseleave", handleMouseLeave); |
|
|
|
|
|
|
|
|
|