Browse Source

feat: implement HeroDotCanvas with responsive grid and cursor-reactive lighting effects

master
sina_sajjadi 2 weeks ago
parent
commit
66586fe292
  1. 71
      components/sections/HeroDotCanvas.tsx
  2. 5
      public/assets/images/hero_back.svg

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

5
public/assets/images/hero_back.svg
File diff suppressed because it is too large
View File

Loading…
Cancel
Save