# king-addons/51.1.86/includes/widgets/Spotlight_Reveal/script.js

King Addons for Elementor – 100+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce Builder, Mega Menu, Popup Builder, version 51.1.86. 333 lines.

- Page: https://pluginprobe.com/plugins/king-addons/51.1.86/code/includes/widgets/Spotlight_Reveal/script.js
- Raw: https://pluginprobe.com/plugins/king-addons/51.1.86/raw/includes/widgets/Spotlight_Reveal/script.js
- Modified: 2026-09-23T19:18:30+00:00

Line numbers below start at 1. Link to a line or a range by appending a fragment to the
page URL, for example `https://pluginprobe.com/plugins/king-addons/51.1.86/code/includes/widgets/Spotlight_Reveal/script.js#L10-L20`.

```javascript
"use strict";

(function ($) {
    const clamp = (value, min, max) => Math.min(Math.max(value, min), max);
    const globalPointerHandlers = new Set();
    let globalPointerActive = false;

    const onGlobalPointerMove = (event) => {
        globalPointerHandlers.forEach((handler) => handler(event));
    };

    const registerGlobalPointer = (handler) => {
        globalPointerHandlers.add(handler);
        if (!globalPointerActive) {
            window.addEventListener("pointermove", onGlobalPointerMove);
            globalPointerActive = true;
        }
    };

    const parseOptions = (root) => {
        if (!root || !root.dataset) {
            return {};
        }

        try {
            return JSON.parse(root.dataset.options || "{}");
        } catch (e) {
            return {};
        }
    };

    const getUnit = (value) => {
        if (!value) {
            return "";
        }
        const match = String(value).trim().match(/[a-z%]+$/i);
        return match ? match[0] : "";
    };

    const toPixels = (value, root, rect) => {
        if (!value) {
            return 0;
        }

        const number = parseFloat(value);
        if (Number.isNaN(number)) {
            return 0;
        }

        const unit = getUnit(value);
        if (unit === "vw") {
            return (window.innerWidth * number) / 100;
        }

        if (unit === "vh") {
            return (window.innerHeight * number) / 100;
        }

        if (unit === "%") {
            const basis = rect ? Math.min(rect.width, rect.height) : 0;
            return (basis * number) / 100;
        }

        if (unit === "rem") {
            const rootSize = parseFloat(getComputedStyle(document.documentElement).fontSize) || 16;
            return number * rootSize;
        }

        if (unit === "em") {
            const rootSize = parseFloat(getComputedStyle(root).fontSize) || 16;
            return number * rootSize;
        }

        return number;
    };

    const getMaskRadius = (root, rect) => {
        const style = window.getComputedStyle(root);
        const sizeValue = style.getPropertyValue("--ka-spotlight-size").trim();
        const sizePx = toPixels(sizeValue, root, rect);
        return sizePx / 2;
    };

    const initSpotlightReveal = (root) => {
        if (!root || root.dataset.spotlightInit === "yes") {
            return;
        }

        root.dataset.spotlightInit = "yes";

        const options = parseOptions(root);
        const trigger = options.trigger === "scroll" ? "scroll" : "cursor";
        const hoverOnly = !(options.hoverOnly === false || options.hoverOnly === "no" || options.hoverOnly === 0);
        const constrain = !(options.constrain === false || options.constrain === "no" || options.constrain === 0);
        const smoothingInput = clamp(Number(options.smoothing) || 0, 0, 1);
        const speed = 1 - smoothingInput * 0.9;
        const startPercentX = clamp(Number(options.startX) || 50, 0, 100);
        const startPercentY = clamp(Number(options.startY) || 50, 0, 100);
        const scrollStart = clamp(Number(options.scrollStart) || 0, 0, 100);
        const scrollEnd = clamp(Number(options.scrollEnd) || 0, 0, 100);

        const isEditor =
            window.elementorFrontend &&
            elementorFrontend.isEditMode &&
            elementorFrontend.isEditMode();
        const activeTrigger = isEditor && trigger === "scroll" ? "cursor" : trigger;

        let bounds = root.getBoundingClientRect();
        let maskRadius = getMaskRadius(root, bounds);
        let startPosition = {
            x: (bounds.width * startPercentX) / 100,
            y: (bounds.height * startPercentY) / 100,
        };

        let currentX = startPosition.x;
        let currentY = startPosition.y;
        let targetX = startPosition.x;
        let targetY = startPosition.y;
        let rafId = 0;
        let lastFrame = 0;
        let isHovering = false;
        let inView = true;
        const frameInterval = isEditor ? 1000 / 30 : 0;

        const prefersReduced =
            window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches;

        const applyPosition = () => {
            root.style.setProperty("--ka-spotlight-x", `${currentX}px`);
            root.style.setProperty("--ka-spotlight-y", `${currentY}px`);
        };

        const refreshMetrics = () => {
            bounds = root.getBoundingClientRect();
            maskRadius = getMaskRadius(root, bounds);
            startPosition = {
                x: (bounds.width * startPercentX) / 100,
                y: (bounds.height * startPercentY) / 100,
            };
        };

        const clampPosition = (x, y) => {
            if (!constrain) {
                return { x, y };
            }

            let clampedX = x;
            let clampedY = y;
            const maxX = bounds.width - maskRadius;
            const maxY = bounds.height - maskRadius;

            if (maxX <= maskRadius) {
                clampedX = bounds.width / 2;
            } else {
                clampedX = clamp(x, maskRadius, maxX);
            }

            if (maxY <= maskRadius) {
                clampedY = bounds.height / 2;
            } else {
                clampedY = clamp(y, maskRadius, maxY);
            }

            return { x: clampedX, y: clampedY };
        };

        const updateTarget = (x, y) => {
            const clamped = clampPosition(x, y);
            targetX = clamped.x;
            targetY = clamped.y;
        };

        const animate = (timestamp) => {
            if (frameInterval && timestamp - lastFrame < frameInterval) {
                rafId = window.requestAnimationFrame(animate);
                return;
            }

            lastFrame = timestamp;

            currentX += (targetX - currentX) * speed;
            currentY += (targetY - currentY) * speed;

            if (Math.abs(targetX - currentX) < 0.2) {
                currentX = targetX;
            }

            if (Math.abs(targetY - currentY) < 0.2) {
                currentY = targetY;
            }

            applyPosition();

            if (currentX === targetX && currentY === targetY) {
                rafId = 0;
                return;
            }

            rafId = window.requestAnimationFrame(animate);
        };

        const startAnimation = () => {
            if (!rafId) {
                rafId = window.requestAnimationFrame(animate);
            }
        };

        const resetToStart = () => {
            updateTarget(startPosition.x, startPosition.y);
            startAnimation();
        };

        const updateTargetFromPointer = (clientX, clientY) => {
            const x = clientX - bounds.left;
            const y = clientY - bounds.top;
            updateTarget(x, y);
            startAnimation();
        };

        const updateTargetFromScroll = () => {
            if (!inView) {
                return;
            }

            const rect = root.getBoundingClientRect();
            bounds = rect;
            const viewportHeight = window.innerHeight || document.documentElement.clientHeight || 0;
            const startOffset = (viewportHeight * scrollStart) / 100;
            const endOffset = (viewportHeight * scrollEnd) / 100;
            const total = rect.height + viewportHeight - startOffset - endOffset;
            const progress = total > 0 ? (viewportHeight - rect.top - startOffset) / total : 0;
            const clamped = clamp(progress, 0, 1);

            updateTarget(rect.width * 0.5, rect.height * clamped);
            startAnimation();
        };

        const onResize = () => {
            refreshMetrics();
            if (activeTrigger === "scroll") {
                updateTargetFromScroll();
            } else if (!isHovering) {
                resetToStart();
            }
        };

        refreshMetrics();
        applyPosition();

        if (prefersReduced) {
            root.classList.add("king-addons-spotlight-reveal--active");
            return;
        }

        if (activeTrigger === "cursor") {
            const onPointerEnter = (event) => {
                if (event.pointerType === "touch") {
                    return;
                }
                isHovering = true;
                refreshMetrics();
                updateTargetFromPointer(event.clientX, event.clientY);
            };

            const onPointerMove = (event) => {
                if (event.pointerType === "touch") {
                    return;
                }
                if (hoverOnly && !isHovering) {
                    return;
                }
                updateTargetFromPointer(event.clientX, event.clientY);
            };

            const onPointerLeave = () => {
                isHovering = false;
                if (hoverOnly) {
                    resetToStart();
                }
            };

            root.addEventListener("pointerenter", onPointerEnter);
            if (hoverOnly) {
                root.addEventListener("pointermove", onPointerMove);
                root.addEventListener("pointerleave", onPointerLeave);
            } else {
                registerGlobalPointer(onPointerMove);
            }
        }

        if (activeTrigger === "scroll") {
            if (typeof IntersectionObserver !== "undefined") {
                const observer = new IntersectionObserver((entries) => {
                    entries.forEach((entry) => {
                        if (entry.target !== root) {
                            return;
                        }
                        inView = entry.isIntersecting;
                        if (inView) {
                            updateTargetFromScroll();
                        }
                    });
                });

                observer.observe(root);
            } else {
                inView = true;
            }

            window.addEventListener("scroll", updateTargetFromScroll, { passive: true });
            updateTargetFromScroll();
        }

        window.addEventListener("resize", onResize, { passive: true });
        root.classList.add("king-addons-spotlight-reveal--active");
    };

    const initSpotlightReveals = ($scope) => {
        $scope.find(".king-addons-spotlight-reveal").each(function () {
            initSpotlightReveal(this);
        });
    };

    $(window).on("elementor/frontend/init", function () {
        elementorFrontend.hooks.addAction(
            "frontend/element_ready/king-addons-spotlight-reveal.default",
            function ($scope) {
                initSpotlightReveals($scope);
            }
        );
    });
})(jQuery);

```
