src/lib/demo/effects/spotlight-effect.ts

Total Symbols
2
Lines of Code
112
Avg Complexity
4.0
Symbol Types
1

File Relationships

graph LR applySpotlightEffect["applySpotlightEffect"] injectStyles["injectStyles"] applySpotlightEffect -->|calls| injectStyles click applySpotlightEffect "../symbols/ed098e44a2649922.html" click injectStyles "../symbols/e97e8d96c98fbc38.html"

Symbols by Kind

function 2

All Symbols

Name Kind Visibility Status Lines Signature
injectStyles function - 10-32 injectStyles(): : void
applySpotlightEffect function exported- 35-111 applySpotlightEffect( target: Element | null, config: SpotlightEffect, ): : Promise<void>

Full Source

/**
 * Spotlight effect — dims the page and highlights the target element.
 */

import type { SpotlightEffect } from "./effect.types";

const OVERLAY_ID = "fill-all-spotlight-overlay";
const HOLE_ID = "fill-all-spotlight-hole";

function injectStyles(): void {
  if (document.getElementById("fill-all-effect-spotlight-styles")) return;
  const style = document.createElement("style");
  style.id = "fill-all-effect-spotlight-styles";
  style.textContent = `
    #${OVERLAY_ID} {
      position: fixed;
      inset: 0;
      z-index: 2147483644;
      pointer-events: none;
      transition: opacity 300ms ease;
    }
    #${HOLE_ID} {
      position: fixed;
      z-index: 2147483645;
      pointer-events: none;
      border-radius: 8px;
      box-shadow: 0 0 0 4px rgba(255,255,255,0.6), 0 0 0 8px rgba(255,255,255,0.15);
      transition: all 300ms ease;
    }
  `;
  document.head.appendChild(style);
}

/** Dims the page and spotlights the target element for `duration` ms. */
export function applySpotlightEffect(
  target: Element | null,
  config: SpotlightEffect,
): Promise<void> {
  return new Promise((resolve) => {
    if (!target) {
      resolve();
      return;
    }

    injectStyles();

    const opacity = config.opacity ?? 0.6;
    const duration = config.duration ?? 2000;

    const rect = target.getBoundingClientRect();
    const padding = 8;

    // Dark overlay with clip-path to create a hole
    const overlay = document.createElement("div");
    overlay.id = OVERLAY_ID;
    overlay.style.backgroundColor = `rgba(0,0,0,${opacity})`;
    overlay.style.opacity = "0";

    // Create a clip-path polygon that covers everything except the target area
    const x1 = rect.left - padding;
    const y1 = rect.top - padding;
    const x2 = rect.right + padding;
    const y2 = rect.bottom + padding;
    const vpWidth = window.innerWidth;
    const vpHeight = window.innerHeight;

    overlay.style.clipPath = `polygon(
      0% 0%,
      100% 0%,
      100% 100%,
      0% 100%,
      0% 0%,
      ${x1}px ${y1}px,
      ${x1}px ${y2}px,
      ${x2}px ${y2}px,
      ${x2}px ${y1}px,
      ${x1}px ${y1}px
    )`;

    document.body.appendChild(overlay);

    // Bright hole around the target for visual effect
    const hole = document.createElement("div");
    hole.id = HOLE_ID;
    hole.style.top = `${rect.top - padding}px`;
    hole.style.left = `${rect.left - padding}px`;
    hole.style.width = `${rect.width + padding * 2}px`;
    hole.style.height = `${rect.height + padding * 2}px`;
    hole.style.background = "transparent";
    document.body.appendChild(hole);

    requestAnimationFrame(() => {
      overlay.style.opacity = "1";
    });

    const cleanup = () => {
      overlay.style.opacity = "0";
      setTimeout(() => {
        overlay.remove();
        hole.remove();
        resolve();
      }, 320);
    };

    if (duration > 0) {
      setTimeout(cleanup, duration);
    } else {
      resolve();
    }
  });
}