# king-addons/51.1.83/includes/widgets/Product_360_Viewer/script.js

King Addons for Elementor – 80+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce, Mega Menu, Popup Builder, version 51.1.83. 374 lines.

- Page: https://pluginprobe.com/plugins/king-addons/51.1.83/code/includes/widgets/Product_360_Viewer/script.js
- Raw: https://pluginprobe.com/plugins/king-addons/51.1.83/raw/includes/widgets/Product_360_Viewer/script.js
- Modified: 2026-09-16T13:52:08+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.83/code/includes/widgets/Product_360_Viewer/script.js#L10-L20`.

```javascript
"use strict";

(() => {
    const clamp = (value, min, max) => Math.min(Math.max(value, min), max);

    const parseJSON = (value, fallback) => {
        try {
            return JSON.parse(value);
        } catch (e) {
            return fallback;
        }
    };

    class KingAddonsProduct360Viewer {
        constructor(root) {
            this.root = root;
            this.canvas = root.querySelector(".king-addons-product-360-viewer__canvas");
            this.image = root.querySelector(".king-addons-product-360-viewer__frame");
            this.loader = root.querySelector(".king-addons-product-360-viewer__loader");
            this.progress = root.querySelector(".king-addons-product-360-viewer__progress");
            this.progressValue = root.querySelector(".king-addons-product-360-viewer__progress-value");
            this.zoomButton = root.querySelector(".king-addons-product-360-viewer__zoom");
            this.hotspotsLayer = root.querySelector(".king-addons-product-360-viewer__hotspots");

            this.frames = parseJSON(root.dataset.frames || "[]", []);
            this.hotspots = parseJSON(root.dataset.hotspots || "[]", []);

            this.settings = {
                isPro: root.dataset.isPro === "true",
                autoplay: (root.dataset.autoplay || "no") === "yes",
                autoplaySpeed: parseInt(root.dataset.autoplaySpeed || "90", 10),
                autoplayMode: root.dataset.autoplayMode || "loop",
                dragSensitivity: parseFloat(root.dataset.dragSensitivity || "8"),
                invertDrag: (root.dataset.invertDrag || "no") === "yes",
                dragAxis: root.dataset.dragAxis || "horizontal",
                enableScroll: (root.dataset.enableScroll || "no") === "yes",
                inertia: parseFloat(root.dataset.inertia || "0"),
                showProgress: (root.dataset.showProgress || "yes") === "yes",
                showZoom: (root.dataset.showZoom || "no") === "yes",
                zoomScale: parseFloat(root.dataset.zoomScale || "1.35"),
                startFrame: clamp(parseInt(root.dataset.startFrame || "1", 10) - 1, 0, this.frames.length - 1),
            };

            this.state = {
                currentFrame: this.settings.startFrame,
                autoplayTimer: null,
                pointerDown: false,
                lastPointer: { x: 0, y: 0 },
                inertiaFrame: null,
                inertiaAccumulator: 0,
                velocity: 0,
                autoplayWasRunning: false,
            };
        }

        init() {
            if (!this.frames.length || !this.canvas || !this.image) {
                return;
            }

            this.setFrame(this.state.currentFrame, false);
            this.buildHotspots();
            this.bindEvents();
            this.preloadFrames();

            if (this.settings.autoplay) {
                this.startAutoplay();
            }
        }

        bindEvents() {
            this.canvas.addEventListener("pointerdown", (event) => this.handlePointerDown(event));
            if (this.settings.enableScroll) {
                this.canvas.addEventListener(
                    "wheel",
                    (event) => this.handleWheel(event),
                    { passive: false }
                );
            }

            if (this.zoomButton) {
                this.zoomButton.addEventListener("click", () => this.toggleZoom());
            }
        }

        handlePointerDown(event) {
            event.preventDefault();
            this.state.pointerDown = true;
            this.state.lastPointer = { x: event.clientX, y: event.clientY };
            this.state.velocity = 0;
            this.state.inertiaAccumulator = 0;

            if (this.settings.autoplay) {
                this.state.autoplayWasRunning = true;
                this.stopAutoplay();
            } else {
                this.state.autoplayWasRunning = false;
            }

            const moveHandler = (moveEvent) => this.handlePointerMove(moveEvent);
            const upHandler = (upEvent) => this.handlePointerUp(upEvent, moveHandler, upHandler);

            document.addEventListener("pointermove", moveHandler);
            document.addEventListener("pointerup", upHandler);
            document.addEventListener("pointercancel", upHandler);
        }

        handlePointerMove(event) {
            if (!this.state.pointerDown) {
                return;
            }

            const current = { x: event.clientX, y: event.clientY };
            const deltaX = current.x - this.state.lastPointer.x;
            const deltaY = current.y - this.state.lastPointer.y;

            let delta = deltaX;
            if (this.settings.dragAxis === "vertical") {
                delta = deltaY * -1;
            } else if (this.settings.dragAxis === "both") {
                delta = Math.abs(deltaY) > Math.abs(deltaX) ? deltaY * -1 : deltaX;
            }

            const sensitivity = Math.max(1, this.settings.dragSensitivity);
            const steps = Math.trunc(delta / sensitivity);

            if (steps !== 0) {
                const direction = this.settings.invertDrag ? -steps : steps;
                this.rotateBy(direction);
                this.state.velocity = direction;
                this.state.lastPointer = current;
            }
        }

        handlePointerUp(event, moveHandler, upHandler) {
            event.preventDefault();
            this.state.pointerDown = false;

            document.removeEventListener("pointermove", moveHandler);
            document.removeEventListener("pointerup", upHandler);
            document.removeEventListener("pointercancel", upHandler);

            if (this.settings.inertia > 0 && Math.abs(this.state.velocity) > 0) {
                this.startInertia(this.state.velocity);
            }

            if (this.state.autoplayWasRunning) {
                this.startAutoplay();
            }
        }

        handleWheel(event) {
            event.preventDefault();
            const direction = event.deltaY > 0 ? 1 : -1;
            this.rotateBy(direction);
        }

        rotateBy(step) {
            if (!Number.isFinite(step) || this.frames.length === 0) {
                return;
            }

            this.setFrame(this.state.currentFrame + step, true);
        }

        setFrame(targetIndex, updateProgress = true) {
            const length = this.frames.length;
            const nextIndex = ((targetIndex % length) + length) % length;
            const nextFrame = this.frames[nextIndex];

            if (!nextFrame) {
                return;
            }

            this.state.currentFrame = nextIndex;
            this.image.src = nextFrame.url;
            this.image.alt = nextFrame.alt || nextFrame.label || "";

            if (updateProgress) {
                this.updateProgress();
            }

            this.updateHotspots();
        }

        updateProgress() {
            if (!this.progressValue || !this.settings.showProgress) {
                return;
            }
            const humanIndex = this.state.currentFrame + 1;
            this.progressValue.textContent = `${humanIndex} / ${this.frames.length}`;
        }

        startAutoplay() {
            this.stopAutoplay();
            if (this.frames.length <= 1) {
                return;
            }

            const speed = clamp(this.settings.autoplaySpeed, 10, 2000);
            let direction = 1;

            this.state.autoplayTimer = window.setInterval(() => {
                if (this.settings.autoplayMode === "pingpong") {
                    const nextIndex = this.state.currentFrame + direction;
                    if (nextIndex >= this.frames.length || nextIndex < 0) {
                        direction *= -1;
                    }
                    this.setFrame(this.state.currentFrame + direction);
                    return;
                }

                this.setFrame(this.state.currentFrame + 1);
            }, speed);
        }

        stopAutoplay() {
            if (this.state.autoplayTimer) {
                window.clearInterval(this.state.autoplayTimer);
                this.state.autoplayTimer = null;
            }
        }

        startInertia(initialVelocity) {
            if (this.state.inertiaFrame) {
                window.cancelAnimationFrame(this.state.inertiaFrame);
            }

            const friction = clamp(this.settings.inertia, 0, 1);
            let velocity = initialVelocity;
            this.state.inertiaAccumulator = 0;

            const tick = () => {
                velocity *= 1 - friction;
                this.state.inertiaAccumulator += velocity;

                const step = Math.trunc(this.state.inertiaAccumulator);
                if (step !== 0) {
                    this.state.inertiaAccumulator -= step;
                    this.rotateBy(step);
                }

                if (Math.abs(velocity) > 0.05 || Math.abs(this.state.inertiaAccumulator) > 0.05) {
                    this.state.inertiaFrame = window.requestAnimationFrame(tick);
                }
            };

            this.state.inertiaFrame = window.requestAnimationFrame(tick);
        }

        preloadFrames() {
            if (!this.loader) {
                return;
            }

            let loaded = 0;
            const total = this.frames.length;

            const handleLoaded = () => {
                loaded += 1;
                if (loaded >= Math.min(total, 2)) {
                    this.root.classList.remove("king-addons-product-360-viewer--loading");
                }
            };

            this.frames.forEach((frame) => {
                const img = new Image();
                img.onload = handleLoaded;
                img.onerror = handleLoaded;
                img.src = frame.url;
            });
        }

        toggleZoom() {
            if (!this.settings.showZoom) {
                return;
            }

            const isActive = this.root.classList.toggle("king-addons-product-360-viewer--zoomed");
            if (this.zoomButton) {
                this.zoomButton.setAttribute("aria-pressed", isActive ? "true" : "false");
            }
            this.image.style.setProperty("--ka-360-zoom-scale", this.settings.zoomScale || 1.35);
        }

        buildHotspots() {
            if (!this.hotspotsLayer || !this.hotspots.length) {
                return;
            }

            this.hotspotsLayer.querySelectorAll(".king-addons-product-360-viewer__hotspot").forEach((node) => {
                node.addEventListener("click", (event) => this.handleHotspotClick(event.currentTarget));
            });

            this.updateHotspots();
        }

        handleHotspotClick(target) {
            const url = target.dataset.link;
            if (!url) {
                return;
            }

            const anchor = document.createElement("a");
            anchor.href = url;
            anchor.target = target.dataset.newTab === "true" ? "_blank" : "_self";

            const rel = [];
            if (target.dataset.nofollow === "true") {
                rel.push("nofollow");
            }
            if (anchor.target === "_blank") {
                rel.push("noopener", "noreferrer");
            }
            anchor.rel = rel.join(" ");

            anchor.style.display = "none";
            document.body.appendChild(anchor);
            anchor.click();
            anchor.remove();
        }

        updateHotspots() {
            if (!this.hotspotsLayer) {
                return;
            }

            const activeFrame = this.state.currentFrame + 1;
            this.hotspotsLayer.querySelectorAll(".king-addons-product-360-viewer__hotspot").forEach((hotspot) => {
                const frameMatch = Number(hotspot.dataset.frame) === activeFrame;
                hotspot.classList.toggle("is-active", frameMatch);
                hotspot.setAttribute("aria-hidden", frameMatch ? "false" : "true");
            });
        }
    }

    const initScope = (scope) => {
        const roots = scope.querySelectorAll(".king-addons-product-360-viewer");
        roots.forEach((root) => {
            if (root.dataset.ka360Init === "true") {
                return;
            }
            root.dataset.ka360Init = "true";

            const instance = new KingAddonsProduct360Viewer(root);
            instance.init();
        });
    };

    const onDocumentReady = () => initScope(document);

    if (window.elementorFrontend && window.elementorFrontend.hooks) {
        window.elementorFrontend.hooks.addAction(
            "frontend/element_ready/king-addons-product-360-viewer.default",
            (scope) => {
                const target = Array.isArray(scope) ? scope[0] : scope;
                if (target) {
                    initScope(target);
                }
            }
        );
    }

    if (document.readyState === "complete" || document.readyState === "interactive") {
        onDocumentReady();
    } else {
        document.addEventListener("DOMContentLoaded", onDocumentReady);
    }
})();





```
