PluginProbe
King Addons for Elementor – 80+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce, Mega Menu, Popup Builder / 51.1.78
King Addons for Elementor – 80+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce, Mega Menu, Popup Builder v51.1.78
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 / Holographic_Card / script.js

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

257 lines 8.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * King Addons - Holographic Card Widget Script
3 * 3D tilt effect with pointer tracking and lerp animation
4 */
5 (function () {
6 'use strict';
7
8 function initHolographicCard(stage) {
9 if (!stage) return;
10
11 const card = stage.querySelector('.king-addons-holo-card');
12 if (!card) return;
13
14 // Read data attributes
15 const maxTilt = parseFloat(stage.dataset.maxTilt) || 12;
16 const kIn = parseFloat(stage.dataset.smoothnessIn) || 0.16;
17 const kHover = parseFloat(stage.dataset.smoothnessHover) || 0.08;
18 const kOut = parseFloat(stage.dataset.smoothnessOut) || 0.12;
19 const parallaxStrength = parseFloat(stage.dataset.parallaxStrength) || 0.18;
20 const disableMotion = stage.dataset.disableMotion === 'true';
21 const respectReducedMotion = stage.dataset.respectReducedMotion === 'true';
22
23 // Check for reduced motion preference
24 if (respectReducedMotion && window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
25 return;
26 }
27
28 // Skip if motion disabled
29 if (disableMotion) {
30 return;
31 }
32
33 // Utility functions
34 const clamp = (v, a, b) => Math.min(b, Math.max(a, v));
35
36 // Current values (animated)
37 const cur = { rx: 0, ry: 0, mx: 50, my: 50, p: 0 };
38 // Target values (from pointer)
39 const tgt = { rx: 0, ry: 0, mx: 50, my: 50, p: 0 };
40
41 let rect = null;
42 let isInside = false;
43 let isEntering = false; // true only during initial hover transition
44 let rafId = 0;
45
46 function readCssVarsIntoCur() {
47 const s = getComputedStyle(stage);
48
49 const rx = parseFloat(s.getPropertyValue('--rx')) || 0;
50 const ry = parseFloat(s.getPropertyValue('--ry')) || 0;
51 const mx = parseFloat(s.getPropertyValue('--mx')) || 50;
52 const my = parseFloat(s.getPropertyValue('--my')) || 50;
53 const p = parseFloat(s.getPropertyValue('--p')) || 0;
54
55 cur.rx = rx;
56 cur.ry = ry;
57 cur.mx = mx;
58 cur.my = my;
59 cur.p = p;
60 }
61
62 function updateRect() {
63 rect = stage.getBoundingClientRect();
64 }
65
66 function setTargetFromEvent(ev) {
67 if (!rect) updateRect();
68
69 const x = (ev.clientX - rect.left) / rect.width;
70 const y = (ev.clientY - rect.top) / rect.height;
71
72 const px = clamp(x, 0, 1);
73 const py = clamp(y, 0, 1);
74
75 tgt.ry = (px - 0.5) * (maxTilt * 2);
76 tgt.rx = (0.5 - py) * (maxTilt * 2);
77 tgt.mx = px * 100;
78 tgt.my = py * 100;
79
80 const dx = px - 0.5;
81 const dy = py - 0.5;
82 tgt.p = clamp(Math.sqrt(dx * dx + dy * dy) * 1.6, 0, 1);
83 }
84
85 function setTargetNeutral() {
86 tgt.rx = 0;
87 tgt.ry = 0;
88 tgt.mx = 50;
89 tgt.my = 50;
90 tgt.p = 0;
91 }
92
93 function applyCur() {
94 stage.style.setProperty('--rx', cur.rx.toFixed(2) + 'deg');
95 stage.style.setProperty('--ry', cur.ry.toFixed(2) + 'deg');
96 stage.style.setProperty('--mx', cur.mx.toFixed(2) + '%');
97 stage.style.setProperty('--my', cur.my.toFixed(2) + '%');
98 stage.style.setProperty('--p', cur.p.toFixed(3));
99 }
100
101 function nearly(a, b, eps) {
102 return Math.abs(a - b) <= eps;
103 }
104
105 function tick() {
106 // Smoothing factor: kIn = when entering card, kOut = when leaving card
107 // When inside but not entering, use a base smooth value (0.08) for fluid motion
108 let k;
109 if (isEntering) {
110 k = kIn;
111 // Check if we've reached close to target - then switch off entering mode
112 const enteredEnough =
113 nearly(cur.rx, tgt.rx, 1) &&
114 nearly(cur.ry, tgt.ry, 1) &&
115 nearly(cur.mx, tgt.mx, 5) &&
116 nearly(cur.my, tgt.my, 5);
117 if (enteredEnough) {
118 isEntering = false;
119 }
120 } else if (isInside) {
121 k = kHover; // smooth movement while hovering inside
122 } else {
123 k = kOut;
124 }
125
126 cur.rx += (tgt.rx - cur.rx) * k;
127 cur.ry += (tgt.ry - cur.ry) * k;
128 cur.mx += (tgt.mx - cur.mx) * k;
129 cur.my += (tgt.my - cur.my) * k;
130 cur.p += (tgt.p - cur.p) * k;
131
132 applyCur();
133
134 const done =
135 nearly(cur.rx, tgt.rx, 0.05) &&
136 nearly(cur.ry, tgt.ry, 0.05) &&
137 nearly(cur.mx, tgt.mx, 0.08) &&
138 nearly(cur.my, tgt.my, 0.08) &&
139 nearly(cur.p, tgt.p, 0.004);
140
141 if (!isInside && done) {
142 rafId = 0;
143 card.classList.remove('is-active');
144 return;
145 }
146
147 rafId = requestAnimationFrame(tick);
148 }
149
150 function ensureRaf() {
151 if (rafId) return;
152 rafId = requestAnimationFrame(tick);
153 }
154
155 function onEnter(ev) {
156 isInside = true;
157 isEntering = true; // activate kIn smoothness only during entry
158 card.classList.add('is-active');
159
160 updateRect();
161 // Start from current state to avoid jump
162 readCssVarsIntoCur();
163
164 setTargetFromEvent(ev);
165 ensureRaf();
166 }
167
168 function onMove(ev) {
169 if (!isInside) return;
170 setTargetFromEvent(ev);
171 }
172
173 function onLeave() {
174 isInside = false;
175 isEntering = false;
176 // If Auto-Sway is active, don't reset to neutral - let Pro script handle it
177 if (stage.dataset.autoSway === 'true') {
178 // Just stop the base animation, Pro will take over
179 if (rafId) {
180 cancelAnimationFrame(rafId);
181 rafId = 0;
182 }
183 card.classList.remove('is-active');
184 return;
185 }
186 setTargetNeutral();
187 ensureRaf();
188 }
189
190 // Event listeners
191 stage.addEventListener('pointerenter', onEnter);
192 stage.addEventListener('pointermove', onMove);
193 stage.addEventListener('pointerleave', onLeave);
194
195 window.addEventListener('resize', updateRect, { passive: true });
196 window.addEventListener('scroll', updateRect, { passive: true });
197
198 // Initial state
199 setTargetNeutral();
200 applyCur();
201
202 // Mark as initialized
203 stage.dataset.initialized = 'true';
204 }
205
206 function initAllCards() {
207 const stages = document.querySelectorAll('.king-addons-holo-card-stage:not([data-initialized="true"])');
208 stages.forEach(initHolographicCard);
209 }
210
211 // Initialize on DOM ready
212 if (document.readyState === 'loading') {
213 document.addEventListener('DOMContentLoaded', initAllCards);
214 } else {
215 initAllCards();
216 }
217
218 // Elementor editor support
219 if (typeof window.elementorFrontend !== 'undefined') {
220 window.addEventListener('elementor/frontend/init', function () {
221 if (window.elementorFrontend.hooks) {
222 window.elementorFrontend.hooks.addAction('frontend/element_ready/king-addons-holographic-card.default', function ($scope) {
223 const stage = $scope[0].querySelector('.king-addons-holo-card-stage');
224 if (stage) {
225 stage.removeAttribute('data-initialized');
226 initHolographicCard(stage);
227 }
228 });
229 }
230 });
231 }
232
233 // MutationObserver for dynamic content
234 const observer = new MutationObserver(function (mutations) {
235 mutations.forEach(function (mutation) {
236 if (mutation.addedNodes.length) {
237 mutation.addedNodes.forEach(function (node) {
238 if (node.nodeType === 1) {
239 if (node.classList && node.classList.contains('king-addons-holo-card-stage')) {
240 initHolographicCard(node);
241 }
242 const stages = node.querySelectorAll && node.querySelectorAll('.king-addons-holo-card-stage:not([data-initialized="true"])');
243 if (stages) {
244 stages.forEach(initHolographicCard);
245 }
246 }
247 });
248 }
249 });
250 });
251
252 observer.observe(document.body, {
253 childList: true,
254 subtree: true
255 });
256 })();
257