PluginProbe
King Addons for Elementor – 80+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce, Mega Menu, Popup Builder / 51.1.49
King Addons for Elementor – 80+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce, Mega Menu, Popup Builder v51.1.49
51.1.83 51.1.82 51.1.81 51.1.79 51.1.78 51.1.77 51.1.76 51.1.74 51.1.75 51.1.65 51.1.64 51.1.63 trunk 51.1.14 51.1.2 51.1.35 51.1.36 51.1.37 51.1.38 51.1.39 51.1.44 51.1.45 51.1.46 51.1.47 51.1.49 All 37 releases
king-addons / includes / widgets / Interactive_Image_Sheen / script.js

script.js in King Addons for Elementor – 80+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce, Mega Menu, Popup Builder 51.1.49, at includes/widgets/Interactive_Image_Sheen/script.js

310 lines 9.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 "use strict";
2 /**
3 * Interactive Image Sheen Widget JavaScript.
4 *
5 * Handles follow cursor mode, mobile fallback, and accessibility.
6 * Pro features: cursor tracking with lerp smoothing, reveal on scroll.
7 *
8 * @package King_Addons
9 */
10 (($) => {
11 /**
12 * Parse JSON settings from data attribute.
13 *
14 * @param {HTMLElement} root - Root element.
15 * @returns {Object} Parsed settings.
16 */
17 const parseSettings = (root) => {
18 if (!root || !root.dataset) {
19 return {};
20 }
21
22 try {
23 return JSON.parse(root.dataset.settings || "{}");
24 } catch (e) {
25 return {};
26 }
27 };
28
29 /**
30 * Check if user prefers reduced motion.
31 *
32 * @returns {boolean}
33 */
34 const prefersReducedMotion = () =>
35 window.matchMedia &&
36 window.matchMedia("(prefers-reduced-motion: reduce)").matches;
37
38 /**
39 * Check if device is touch-only.
40 *
41 * @returns {boolean}
42 */
43 const isTouchDevice = () =>
44 window.matchMedia &&
45 window.matchMedia("(hover: none) and (pointer: coarse)").matches;
46
47 /**
48 * Linear interpolation.
49 *
50 * @param {number} start - Start value.
51 * @param {number} end - End value.
52 * @param {number} factor - Interpolation factor (0-1).
53 * @returns {number}
54 */
55 const lerp = (start, end, factor) => start + (end - start) * factor;
56
57 /**
58 * Clamp value between min and max.
59 *
60 * @param {number} value - Value to clamp.
61 * @param {number} min - Minimum.
62 * @param {number} max - Maximum.
63 * @returns {number}
64 */
65 const clamp = (value, min, max) => Math.min(Math.max(value, min), max);
66
67 /**
68 * Initialize widget instance.
69 *
70 * @param {jQuery} $scope - Widget scope.
71 */
72 const initWidget = ($scope) => {
73 const root = $scope.find(".kng-iis").first()[0];
74 if (!root || root.dataset.kngIisInit === "yes") {
75 return;
76 }
77
78 root.dataset.kngIisInit = "yes";
79 const options = parseSettings(root);
80
81 const isPro = root.classList.contains("kng-iis--pro");
82 const reducedMotion = prefersReducedMotion();
83 const isTouch = isTouchDevice();
84
85 // Pro settings
86 const followEnabled = isPro && options.followEnabled === true;
87 const followStrength = options.followStrength || 50;
88 const followSmoothing = options.followSmoothing || 0.1;
89 const followAngleShift = options.followAngleShift === true;
90 const mobileMode = options.mobileMode || "disable";
91 const focusTrigger = options.focusTrigger !== false;
92
93 // State for follow cursor
94 let targetX = 0.5;
95 let targetY = 0.5;
96 let currentX = 0.5;
97 let currentY = 0.5;
98 let isHovering = false;
99 let animationId = null;
100
101 const sheens = root.querySelectorAll(".kng-iis__sheen");
102
103 /**
104 * Update sheen position based on cursor.
105 */
106 const updateSheenPosition = () => {
107 if (!isHovering && Math.abs(currentX - 0.5) < 0.01 && Math.abs(currentY - 0.5) < 0.01) {
108 cancelAnimationFrame(animationId);
109 animationId = null;
110 return;
111 }
112
113 // Lerp towards target
114 currentX = lerp(currentX, isHovering ? targetX : 0.5, followSmoothing);
115 currentY = lerp(currentY, isHovering ? targetY : 0.5, followSmoothing);
116
117 // Calculate transform
118 const strength = followStrength / 100;
119 const translateX = (currentX - 0.5) * 200 * strength;
120 const translateY = (currentY - 0.5) * 50 * strength;
121
122 // Update sheen position
123 sheens.forEach((sheen, index) => {
124 const layerFactor = 1 - index * 0.15; // Slightly different for each layer
125 let transform = `translateX(${translateX * layerFactor}%) translateY(${translateY * layerFactor}%)`;
126
127 if (followAngleShift) {
128 const angle = (currentX - 0.5) * 30;
129 transform += ` rotate(${angle}deg)`;
130 }
131
132 sheen.style.transform = transform;
133 });
134
135 animationId = requestAnimationFrame(updateSheenPosition);
136 };
137
138 /**
139 * Setup follow cursor interaction.
140 */
141 const setupFollowCursor = () => {
142 if (!followEnabled || reducedMotion || isTouch) {
143 return;
144 }
145
146 root.addEventListener("mouseenter", () => {
147 isHovering = true;
148 root.classList.add("kng-iis--following");
149 if (!animationId) {
150 animationId = requestAnimationFrame(updateSheenPosition);
151 }
152 });
153
154 root.addEventListener("mouseleave", () => {
155 isHovering = false;
156 root.classList.remove("kng-iis--following");
157 // Animation continues to lerp back to center
158 if (!animationId) {
159 animationId = requestAnimationFrame(updateSheenPosition);
160 }
161 });
162
163 root.addEventListener("mousemove", (e) => {
164 const rect = root.getBoundingClientRect();
165 targetX = clamp((e.clientX - rect.left) / rect.width, 0, 1);
166 targetY = clamp((e.clientY - rect.top) / rect.height, 0, 1);
167 });
168 };
169
170 /**
171 * Setup mobile reveal on scroll (Pro).
172 */
173 const setupMobileReveal = () => {
174 if (mobileMode !== "reveal" || !isTouch) {
175 return;
176 }
177
178 if (!("IntersectionObserver" in window)) {
179 return;
180 }
181
182 const observer = new IntersectionObserver(
183 (entries) => {
184 entries.forEach((entry) => {
185 if (entry.isIntersecting) {
186 root.classList.add("kng-iis--in-view");
187 observer.unobserve(root);
188 }
189 });
190 },
191 {
192 threshold: 0.5,
193 }
194 );
195
196 observer.observe(root);
197 };
198
199 /**
200 * Setup focus trigger for accessibility.
201 */
202 const setupFocusTrigger = () => {
203 if (!focusTrigger) {
204 return;
205 }
206
207 const focusable = root.querySelectorAll("a, button, [tabindex]");
208 focusable.forEach((el) => {
209 el.addEventListener("focus", () => {
210 root.classList.add("kng-iis--focused");
211 });
212
213 el.addEventListener("blur", () => {
214 root.classList.remove("kng-iis--focused");
215 });
216 });
217 };
218
219 /**
220 * Handle reduced motion preference.
221 */
222 const setupReducedMotion = () => {
223 if (!reducedMotion) {
224 return;
225 }
226
227 // CSS handles the visual changes via media query
228 // We just ensure no JS animations run
229 if (animationId) {
230 cancelAnimationFrame(animationId);
231 animationId = null;
232 }
233 };
234
235 /**
236 * Parse sheen color to RGB for CSS calc.
237 * (Helper for advanced gradient calculations)
238 */
239 const setupSheenColor = () => {
240 const computedStyle = getComputedStyle(root);
241 const sheenColor = computedStyle.getPropertyValue("--kng-iis-sheen-color").trim();
242
243 // Convert hex to RGB for CSS calc
244 if (sheenColor.startsWith("#")) {
245 let r, g, b;
246 if (sheenColor.length === 4) {
247 r = parseInt(sheenColor[1] + sheenColor[1], 16);
248 g = parseInt(sheenColor[2] + sheenColor[2], 16);
249 b = parseInt(sheenColor[3] + sheenColor[3], 16);
250 } else {
251 r = parseInt(sheenColor.slice(1, 3), 16);
252 g = parseInt(sheenColor.slice(3, 5), 16);
253 b = parseInt(sheenColor.slice(5, 7), 16);
254 }
255 root.style.setProperty("--kng-iis-sheen-color-rgb", `${r}, ${g}, ${b}`);
256 }
257 };
258
259 // Initialize
260 setupSheenColor();
261 setupReducedMotion();
262
263 if (isPro) {
264 setupFollowCursor();
265 setupMobileReveal();
266 }
267
268 setupFocusTrigger();
269
270 // Cleanup function
271 root.kngIisCleanup = () => {
272 if (animationId) {
273 cancelAnimationFrame(animationId);
274 }
275 };
276 };
277
278 // Initialize on Elementor frontend ready
279 $(window).on("elementor/frontend/init", () => {
280 elementorFrontend.hooks.addAction(
281 "frontend/element_ready/king-addons-interactive-image-sheen.default",
282 ($scope) => {
283 elementorFrontend.elementsHandler.addHandler(
284 elementorModules.frontend.handlers.Base.extend({
285 onInit: function () {
286 initWidget(this.$element);
287 },
288 }),
289 { $element: $scope }
290 );
291 }
292 );
293
294 // Pro version
295 elementorFrontend.hooks.addAction(
296 "frontend/element_ready/king-addons-interactive-image-sheen-pro.default",
297 ($scope) => {
298 elementorFrontend.elementsHandler.addHandler(
299 elementorModules.frontend.handlers.Base.extend({
300 onInit: function () {
301 initWidget(this.$element);
302 },
303 }),
304 { $element: $scope }
305 );
306 }
307 );
308 });
309 })(jQuery);
310