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 / Kinetic_Text_Hover / 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/Kinetic_Text_Hover/script.js

490 lines 16.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 "use strict";
2
3 (function ($) {
4 const clamp = (value, min, max) => Math.min(Math.max(value, min), max);
5
6 const parseOptions = (root) => {
7 if (!root || !root.dataset) {
8 return {};
9 }
10
11 try {
12 return JSON.parse(root.dataset.options || "{}");
13 } catch (e) {
14 return {};
15 }
16 };
17
18 const normalizeOptions = (raw = {}) => {
19 const letter = raw.letterDrift || {};
20 const magnetic = raw.magnetic || {};
21 const variableFont = raw.variableFont || {};
22 const reveal = raw.reveal || {};
23 const reducedMotion = raw.reducedMotion || {};
24
25 return {
26 preset: raw.preset || "split-underline",
27 trigger: raw.trigger || "hover",
28 mobileBehavior: raw.mobileBehavior || "disable",
29 intensity: clamp(Number(raw.intensity) || 35, 0, 100),
30 letterDrift: {
31 max: Math.max(Number(letter.max) || 0, 0),
32 randomness: clamp(Number(letter.randomness) || 0, 0, 100),
33 },
34 magnetic: {
35 enabled: magnetic.enabled === true || magnetic.enabled === "yes",
36 strength: clamp(Number(magnetic.strength) || 0, 0, 100),
37 radius: Math.max(Number(magnetic.radius) || 0, 0),
38 maxOffset: Math.max(Number(magnetic.maxOffset) || 0, 0),
39 smoothing: clamp(Number(magnetic.smoothing) || 0, 0, 1),
40 clamp: magnetic.clamp || "soft",
41 disableMobile: magnetic.disableMobile === true || magnetic.disableMobile === "yes",
42 },
43 variableFont: {
44 enabled: variableFont.enabled === true || variableFont.enabled === "yes",
45 wghtMin: Number(variableFont.wghtMin) || 400,
46 wghtMax: Number(variableFont.wghtMax) || 700,
47 wdthMin: Number(variableFont.wdthMin) || 100,
48 wdthMax: Number(variableFont.wdthMax) || 110,
49 widthSafe: variableFont.widthSafe === true || variableFont.widthSafe === "yes",
50 },
51 reveal: {
52 enabled: reveal.enabled === true || reveal.enabled === "yes",
53 type: reveal.type || "fade",
54 threshold: clamp(Number(reveal.threshold) || 0.2, 0, 1),
55 once: reveal.once === true || reveal.once === "yes",
56 },
57 reducedMotion: {
58 respect: reducedMotion.respect === true || reducedMotion.respect === "yes",
59 mode: reducedMotion.mode || "simplify",
60 },
61 editorPreview: raw.editorPreview === true || raw.editorPreview === "yes",
62 };
63 };
64
65 const splitText = (visual, text) => {
66 if (!visual) {
67 return [];
68 }
69
70 while (visual.firstChild) {
71 visual.removeChild(visual.firstChild);
72 }
73
74 const fragment = document.createDocumentFragment();
75 const chars = [];
76
77 for (let i = 0; i < text.length; i += 1) {
78 const char = text[i];
79
80 if (char === "\n") {
81 fragment.appendChild(document.createElement("br"));
82 continue;
83 }
84
85 const span = document.createElement("span");
86 span.className = "kng-kt-char";
87 span.setAttribute("aria-hidden", "true");
88
89 if (char === " ") {
90 span.classList.add("kng-kt-space");
91 span.innerHTML = "&nbsp;";
92 } else {
93 span.textContent = char;
94 }
95
96 fragment.appendChild(span);
97 chars.push(span);
98 }
99
100 visual.appendChild(fragment);
101 visual.dataset.kineticSplit = "yes";
102 return chars;
103 };
104
105 const getPointerSupport = () => {
106 if (!window.matchMedia) {
107 return { coarse: false };
108 }
109 const coarse = window.matchMedia("(hover: none), (pointer: coarse)").matches;
110 return { coarse };
111 };
112
113 const initInstance = (root) => {
114 if (!root || root.dataset.kineticInit === "yes") {
115 return;
116 }
117
118 root.dataset.kineticInit = "yes";
119
120 const options = normalizeOptions(parseOptions(root));
121 const visual = root.querySelector(".king-addons-kinetic-text-hover__visual");
122 if (!visual) {
123 return;
124 }
125
126 const originalText = visual.dataset.originalText || visual.textContent || "";
127 visual.dataset.originalText = originalText;
128 visual.dataset.text = originalText;
129
130 const charElements = splitText(visual, originalText);
131 const chars = charElements.map((el, index) => ({
132 el,
133 index,
134 randX: Math.random() * 2 - 1,
135 randY: Math.random() * 2 - 1,
136 driftX: 0,
137 driftY: 0,
138 magX: 0,
139 magY: 0,
140 centerX: 0,
141 centerY: 0,
142 }));
143
144 const intensity = clamp(options.intensity / 100, 0, 1);
145 const letterMax = options.letterDrift.max * intensity;
146 const letterRandomness = clamp(options.letterDrift.randomness / 100, 0, 1);
147
148 const pointerSupport = getPointerSupport();
149 const isCoarsePointer = pointerSupport.coarse;
150 const disableHover = isCoarsePointer && options.mobileBehavior === "disable";
151 const tapMode = isCoarsePointer && options.mobileBehavior === "tap";
152
153 if (disableHover) {
154 root.classList.add("is-hover-disabled");
155 }
156
157 const prefersReduced =
158 window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
159
160 let motionMode = "full";
161 if (options.reducedMotion.respect && prefersReduced) {
162 motionMode = options.reducedMotion.mode === "disable" ? "disable" : "simplify";
163 if (motionMode === "disable") {
164 root.classList.add("is-motion-disabled");
165 }
166 }
167
168 const canAnimate = motionMode !== "disable" && !disableHover;
169 const allowLetterMotion = motionMode === "full" && options.preset === "letter-drift";
170 const allowMagnetic =
171 motionMode === "full" &&
172 options.magnetic.enabled &&
173 !(options.magnetic.disableMobile && isCoarsePointer);
174
175 const setActive = (active) => {
176 if (disableHover) {
177 return;
178 }
179 if (!canAnimate && active) {
180 return;
181 }
182 root.classList.toggle("is-active", active);
183 if (options.variableFont.enabled) {
184 const useWidth = !options.variableFont.widthSafe;
185 const wght = active ? options.variableFont.wghtMax : options.variableFont.wghtMin;
186 const wdth = active && useWidth ? options.variableFont.wdthMax : options.variableFont.wdthMin;
187 visual.style.setProperty("--kng-kt-var-wght", `${wght}`);
188 visual.style.setProperty("--kng-kt-var-wdth", `${wdth}`);
189 }
190 };
191
192 if (options.variableFont.enabled) {
193 root.classList.add("is-varfont-enabled");
194 visual.style.setProperty("--kng-kt-var-wght", `${options.variableFont.wghtMin}`);
195 visual.style.setProperty("--kng-kt-var-wdth", `${options.variableFont.wdthMin}`);
196 }
197
198 if (options.trigger === "idle" && canAnimate) {
199 setActive(true);
200 }
201
202 if (options.reveal.enabled) {
203 if (options.editorPreview || !("IntersectionObserver" in window)) {
204 root.classList.add("is-visible");
205 } else {
206 const observer = new IntersectionObserver(
207 (entries) => {
208 entries.forEach((entry) => {
209 if (entry.isIntersecting) {
210 root.classList.add("is-visible");
211 if (options.reveal.once && observer) {
212 observer.unobserve(entry.target);
213 }
214 } else if (!options.reveal.once) {
215 root.classList.remove("is-visible");
216 }
217 });
218 },
219 { threshold: options.reveal.threshold }
220 );
221
222 observer.observe(root);
223 }
224 }
225
226 if (!canAnimate) {
227 return;
228 }
229
230 let active = options.trigger === "idle";
231 let pointerX = 0;
232 let pointerY = 0;
233 let driftRaf = 0;
234 let magneticRaf = 0;
235 let magneticActive = false;
236 let bounds = root.getBoundingClientRect();
237
238 const updateBounds = () => {
239 bounds = root.getBoundingClientRect();
240 chars.forEach((char) => {
241 const rect = char.el.getBoundingClientRect();
242 char.centerX = rect.left - bounds.left + rect.width / 2;
243 char.centerY = rect.top - bounds.top + rect.height / 2;
244 });
245 };
246
247 updateBounds();
248
249 const applyCharOffsets = (char) => {
250 const x = char.driftX + char.magX;
251 const y = char.driftY + char.magY;
252 char.el.style.setProperty("--kng-kt-char-x", `${x.toFixed(2)}px`);
253 char.el.style.setProperty("--kng-kt-char-y", `${y.toFixed(2)}px`);
254 };
255
256 const resetDrift = () => {
257 chars.forEach((char) => {
258 char.driftX = 0;
259 char.driftY = 0;
260 applyCharOffsets(char);
261 });
262 };
263
264 const updateDrift = (usePointer) => {
265 if (!allowLetterMotion) {
266 return;
267 }
268
269 const centerX = bounds.width / 2;
270 const centerY = bounds.height / 2;
271 const max = letterMax;
272
273 let baseX = 0;
274 let baseY = 0;
275 if (usePointer) {
276 const dx = (pointerX - centerX) / (bounds.width / 2 || 1);
277 const dy = (pointerY - centerY) / (bounds.height / 2 || 1);
278 baseX = clamp(dx, -1, 1) * max;
279 baseY = clamp(dy, -1, 1) * max;
280 }
281
282 chars.forEach((char) => {
283 const randomX = char.randX * max * letterRandomness;
284 const randomY = char.randY * max * letterRandomness;
285 char.driftX = clamp(baseX + randomX, -max, max);
286 char.driftY = clamp(baseY + randomY, -max, max);
287 applyCharOffsets(char);
288 });
289 };
290
291 const scheduleDrift = (usePointer) => {
292 if (driftRaf) {
293 return;
294 }
295 driftRaf = window.requestAnimationFrame(() => {
296 driftRaf = 0;
297 updateDrift(usePointer);
298 });
299 };
300
301 const animateMagnetic = () => {
302 if (!allowMagnetic) {
303 magneticRaf = 0;
304 return;
305 }
306
307 const strength = clamp(options.magnetic.strength / 100, 0, 1) * intensity;
308 const radius = options.magnetic.radius;
309 const maxOffset = options.magnetic.maxOffset * intensity;
310 const smoothing = clamp(options.magnetic.smoothing, 0, 1);
311 const lerp = clamp(1 - smoothing * 0.8, 0.05, 0.5);
312
313 let stillActive = false;
314
315 chars.forEach((char) => {
316 let targetX = 0;
317 let targetY = 0;
318
319 if (magneticActive && radius > 0 && strength > 0 && maxOffset > 0) {
320 const dx = pointerX - char.centerX;
321 const dy = pointerY - char.centerY;
322 const distance = Math.hypot(dx, dy) || 0;
323
324 if (distance < radius) {
325 const ratio = 1 - distance / radius;
326 let influence = ratio;
327 if (options.magnetic.clamp === "soft") {
328 influence = ratio * ratio;
329 } else if (options.magnetic.clamp === "hard") {
330 influence = ratio > 0 ? 1 : 0;
331 }
332
333 const normalizedX = distance > 0 ? dx / distance : 0;
334 const normalizedY = distance > 0 ? dy / distance : 0;
335 targetX = normalizedX * maxOffset * strength * influence;
336 targetY = normalizedY * maxOffset * strength * influence;
337 }
338 }
339
340 char.magX += (targetX - char.magX) * lerp;
341 char.magY += (targetY - char.magY) * lerp;
342 applyCharOffsets(char);
343
344 if (Math.abs(char.magX) > 0.05 || Math.abs(char.magY) > 0.05) {
345 stillActive = true;
346 }
347 });
348
349 if (magneticActive || stillActive) {
350 magneticRaf = window.requestAnimationFrame(animateMagnetic);
351 } else {
352 magneticRaf = 0;
353 }
354 };
355
356 const handlePointerMove = (event) => {
357 if (!active) {
358 return;
359 }
360 const rect = root.getBoundingClientRect();
361 pointerX = event.clientX - rect.left;
362 pointerY = event.clientY - rect.top;
363
364 root.style.setProperty("--kng-kt-pointer-x", `${(pointerX / rect.width) * 100}%`);
365 root.style.setProperty("--kng-kt-pointer-y", `${(pointerY / rect.height) * 100}%`);
366
367 if (allowLetterMotion && options.trigger === "hover-move") {
368 scheduleDrift(true);
369 }
370
371 if (allowMagnetic && !magneticRaf) {
372 magneticRaf = window.requestAnimationFrame(animateMagnetic);
373 }
374 };
375
376 const handlePointerEnter = (event) => {
377 if (tapMode) {
378 return;
379 }
380 active = true;
381 setActive(true);
382 if (allowLetterMotion) {
383 scheduleDrift(options.trigger === "hover-move");
384 }
385 if (allowMagnetic) {
386 magneticActive = true;
387 handlePointerMove(event);
388 }
389 };
390
391 const handlePointerLeave = () => {
392 if (options.trigger === "idle") {
393 return;
394 }
395 if (tapMode) {
396 return;
397 }
398 active = false;
399 setActive(false);
400 resetDrift();
401 magneticActive = false;
402 if (allowMagnetic && !magneticRaf) {
403 magneticRaf = window.requestAnimationFrame(animateMagnetic);
404 }
405 };
406
407 const handleFocusIn = () => {
408 if (disableHover) {
409 return;
410 }
411 active = true;
412 setActive(true);
413 if (allowLetterMotion) {
414 scheduleDrift(false);
415 }
416 };
417
418 const handleFocusOut = () => {
419 if (options.trigger === "idle") {
420 return;
421 }
422 if (tapMode) {
423 return;
424 }
425 active = false;
426 setActive(false);
427 resetDrift();
428 magneticActive = false;
429 if (allowMagnetic && !magneticRaf) {
430 magneticRaf = window.requestAnimationFrame(animateMagnetic);
431 }
432 };
433
434 const handleTap = (event) => {
435 if (!tapMode) {
436 return;
437 }
438 if (event.pointerType === "mouse") {
439 return;
440 }
441 active = !active;
442 setActive(active);
443 if (allowLetterMotion) {
444 scheduleDrift(false);
445 }
446 if (allowMagnetic) {
447 magneticActive = active;
448 if (active) {
449 handlePointerMove(event);
450 }
451 }
452 };
453
454 root.addEventListener("pointerenter", handlePointerEnter);
455 root.addEventListener("pointerleave", handlePointerLeave);
456 root.addEventListener("pointermove", handlePointerMove);
457 root.addEventListener("focusin", handleFocusIn);
458 root.addEventListener("focusout", handleFocusOut);
459 root.addEventListener("pointerdown", handleTap);
460
461 let resizeTimer = 0;
462 window.addEventListener("resize", () => {
463 window.clearTimeout(resizeTimer);
464 resizeTimer = window.setTimeout(() => {
465 updateBounds();
466 }, 150);
467 });
468
469 if (!allowMagnetic) {
470 magneticActive = false;
471 }
472 };
473
474 const initWidget = ($scope) => {
475 $scope.find(".king-addons-kinetic-text-hover").each(function () {
476 initInstance(this);
477 });
478 };
479
480 $(window).on("elementor/frontend/init", function () {
481 if (!window.elementorFrontend) {
482 return;
483 }
484 elementorFrontend.hooks.addAction(
485 "frontend/element_ready/king-addons-kinetic-text-hover.default",
486 initWidget
487 );
488 });
489 })(jQuery);
490