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

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

502 lines 17.3 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 parseColor = (value) => {
7 if (!value) {
8 return null;
9 }
10 const input = String(value).trim();
11 const rgbMatch = input.match(/^rgba?\(\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)(?:\s*,\s*([\d.]+))?\s*\)$/i);
12 if (rgbMatch) {
13 return {
14 r: parseFloat(rgbMatch[1]),
15 g: parseFloat(rgbMatch[2]),
16 b: parseFloat(rgbMatch[3]),
17 a: rgbMatch[4] !== undefined ? parseFloat(rgbMatch[4]) : 1,
18 };
19 }
20 const hexMatch = input.match(/^#([0-9a-f]{3}|[0-9a-f]{6})$/i);
21 if (hexMatch) {
22 let hex = hexMatch[1];
23 if (hex.length === 3) {
24 hex = hex.split("").map((char) => char + char).join("");
25 }
26 return {
27 r: parseInt(hex.slice(0, 2), 16),
28 g: parseInt(hex.slice(2, 4), 16),
29 b: parseInt(hex.slice(4, 6), 16),
30 a: 1,
31 };
32 }
33 return null;
34 };
35
36 const toRgba = (rgb, alpha) => {
37 const r = Math.round(clamp(rgb.r ?? 255, 0, 255));
38 const g = Math.round(clamp(rgb.g ?? 255, 0, 255));
39 const b = Math.round(clamp(rgb.b ?? 255, 0, 255));
40 const a = clamp(alpha, 0, 1);
41 return `rgba(${r}, ${g}, ${b}, ${a})`;
42 };
43
44 const buildRgba = (value, intensity) => {
45 const parsed = parseColor(value);
46 if (!parsed) {
47 return {
48 color: value || "#ffffff",
49 fade: "rgba(255, 255, 255, 0)",
50 };
51 }
52 const baseAlpha = Number.isFinite(parsed.a) ? parsed.a : 1;
53 const power = Number.isFinite(intensity) ? intensity : 1;
54 const alpha = clamp(baseAlpha * power, 0, 1);
55 return {
56 color: toRgba(parsed, alpha),
57 fade: toRgba(parsed, 0),
58 };
59 };
60
61 const supportsCssVars = () => {
62 return typeof CSS !== "undefined" && CSS.supports("color", "var(--kng-test)");
63 };
64
65 const supportsGradients = () => {
66 return (
67 typeof CSS !== "undefined" &&
68 CSS.supports("background-image", "radial-gradient(circle at 10% 10%, #fff 0%, transparent 60%)")
69 );
70 };
71
72 const supportsCanvas = () => {
73 try {
74 const canvas = document.createElement("canvas");
75 return !!canvas.getContext && !!canvas.getContext("2d");
76 } catch (error) {
77 return false;
78 }
79 };
80
81 const parsePayload = (wrapper) => {
82 const raw = wrapper.dataset.mesh || "";
83 if (!raw) {
84 return null;
85 }
86 try {
87 return JSON.parse(raw);
88 } catch (error) {
89 return null;
90 }
91 };
92
93 const getSpeed = (wrapper, payload) => {
94 const cssValue = getComputedStyle(wrapper).getPropertyValue("--kng-mesh-speed");
95 const parsed = parseFloat(cssValue);
96 if (Number.isFinite(parsed)) {
97 return parsed;
98 }
99 return Number.isFinite(payload.speed) ? payload.speed : 40;
100 };
101
102 const getPerformanceFps = (payload, isMobile) => {
103 const mode = payload.performance?.mode || "auto";
104 if (mode === "performance") {
105 return 20;
106 }
107 if (mode === "balanced") {
108 return 30;
109 }
110 if (mode === "quality") {
111 return 60;
112 }
113 return isMobile ? 24 : 36;
114 };
115
116 const applyFallback = (wrapper, payload) => {
117 wrapper.classList.add("is-fallback");
118 if (payload.fallback?.color) {
119 wrapper.style.backgroundColor = payload.fallback.color;
120 }
121 if (payload.fallback?.image) {
122 wrapper.style.backgroundImage = `url(${payload.fallback.image})`;
123 wrapper.style.backgroundSize = "cover";
124 wrapper.style.backgroundPosition = "center";
125 }
126 };
127
128 const resolveEngine = (payload) => {
129 const requested = payload.engine || "auto";
130 if (requested === "canvas") {
131 return supportsCanvas() ? "canvas" : "css";
132 }
133 if (requested === "auto") {
134 return supportsCanvas() ? "canvas" : "css";
135 }
136 return "css";
137 };
138
139 const buildPointState = (points) => {
140 return points.map((point, index) => {
141 const intensity = point.intensity ?? 0.8;
142 const colors = buildRgba(point.color || "#ffffff", intensity);
143 return {
144 index,
145 baseX: point.x ?? 50,
146 baseY: point.y ?? 50,
147 radius: point.radius ?? 220,
148 intensity,
149 feather: point.feather ?? 60,
150 color: colors.color,
151 fade: colors.fade,
152 blend: point.blend || "screen",
153 motion: point.motion || "drift",
154 amplitude: point.amplitude ?? 18,
155 frequency: point.frequency ?? 0.25,
156 phase: (point.phase ?? 0) * (Math.PI / 180),
157 direction: (point.direction ?? 0) * (Math.PI / 180),
158 speed: point.speed ?? 1,
159 };
160 });
161 };
162
163 const rotateVector = (x, y, angle) => {
164 if (!angle) {
165 return { x, y };
166 }
167 const cos = Math.cos(angle);
168 const sin = Math.sin(angle);
169 return {
170 x: x * cos - y * sin,
171 y: x * sin + y * cos,
172 };
173 };
174
175 const getScrollProgress = (wrapper, payload, state) => {
176 const mode = payload.scroll?.mode || "off";
177 if (mode === "off") {
178 return 0;
179 }
180
181 if (payload.scroll?.reduceMobile && state.isMobile) {
182 return 0;
183 }
184
185 const direction = payload.scroll?.direction === "reverse" ? -1 : 1;
186 const startPct = payload.scroll?.start ?? 0;
187 const endPct = payload.scroll?.end ?? 100;
188 const offset = payload.scroll?.offset ?? 0;
189 let progress = 0;
190
191 if (mode === "page") {
192 const scrollTop = window.scrollY || window.pageYOffset || 0;
193 const docHeight = document.documentElement.scrollHeight - window.innerHeight;
194 progress = docHeight > 0 ? scrollTop / docHeight : 0;
195 } else {
196 const rect = wrapper.getBoundingClientRect();
197 const scrollTop = window.scrollY || window.pageYOffset || 0;
198 const elementTop = scrollTop + rect.top;
199 const start = elementTop - window.innerHeight * (startPct / 100) - offset;
200 const end = elementTop + rect.height - window.innerHeight * (endPct / 100) - offset;
201 progress = end > start ? (scrollTop - start) / (end - start) : 0;
202 }
203
204 progress = clamp(progress, 0, 1);
205 if (direction < 0) {
206 progress = 1 - progress;
207 }
208
209 const easing = payload.scroll?.easing || "linear";
210 if (easing === "ease-out") {
211 progress = 1 - Math.pow(1 - progress, 2);
212 } else if (easing === "ease-in") {
213 progress = Math.pow(progress, 2);
214 } else if (easing === "ease-in-out") {
215 progress = progress < 0.5 ? 2 * progress * progress : 1 - Math.pow(-2 * progress + 2, 2) / 2;
216 }
217
218 return progress;
219 };
220
221 const setPointVars = (wrapper, points, dims, t) => {
222 points.forEach((point) => {
223 let dx = 0;
224 let dy = 0;
225 const motion = point.motion;
226 const localT = t * point.speed;
227
228 if (motion !== "static") {
229 const angle = localT * point.frequency * Math.PI * 2 + point.phase;
230 if (motion === "orbit") {
231 dx = Math.cos(angle) * point.amplitude;
232 dy = Math.sin(angle) * point.amplitude;
233 } else if (motion === "noise") {
234 dx = Math.sin(angle) * point.amplitude;
235 dy = Math.sin(angle * 1.3 + point.phase * 0.4) * point.amplitude;
236 } else {
237 dx = Math.sin(angle) * point.amplitude;
238 dy = Math.cos(angle) * point.amplitude;
239 }
240
241 const rotated = rotateVector(dx, dy, point.direction);
242 dx = rotated.x;
243 dy = rotated.y;
244 }
245
246 const xPct = clamp(point.baseX + (dx / dims.width) * 100, 0, 100);
247 const yPct = clamp(point.baseY + (dy / dims.height) * 100, 0, 100);
248 wrapper.style.setProperty(`--kng-mesh-p${point.index + 1}-x`, `${xPct}%`);
249 wrapper.style.setProperty(`--kng-mesh-p${point.index + 1}-y`, `${yPct}%`);
250 });
251 };
252
253 const drawCanvas = (ctx, points, dims, t) => {
254 ctx.clearRect(0, 0, dims.width, dims.height);
255 points.forEach((point) => {
256 let dx = 0;
257 let dy = 0;
258 const motion = point.motion;
259 const localT = t * point.speed;
260
261 if (motion !== "static") {
262 const angle = localT * point.frequency * Math.PI * 2 + point.phase;
263 if (motion === "orbit") {
264 dx = Math.cos(angle) * point.amplitude;
265 dy = Math.sin(angle) * point.amplitude;
266 } else if (motion === "noise") {
267 dx = Math.sin(angle) * point.amplitude;
268 dy = Math.sin(angle * 1.3 + point.phase * 0.4) * point.amplitude;
269 } else {
270 dx = Math.sin(angle) * point.amplitude;
271 dy = Math.cos(angle) * point.amplitude;
272 }
273
274 const rotated = rotateVector(dx, dy, point.direction);
275 dx = rotated.x;
276 dy = rotated.y;
277 }
278
279 const x = (point.baseX / 100) * dims.width + dx;
280 const y = (point.baseY / 100) * dims.height + dy;
281 const radius = Math.max(point.radius, 1);
282 const feather = clamp(point.feather / 100, 0, 1);
283 const solidRadius = radius - radius * feather;
284 const stop = clamp(solidRadius / radius, 0, 1);
285
286 const gradient = ctx.createRadialGradient(x, y, 0, x, y, radius);
287 gradient.addColorStop(0, point.color);
288 gradient.addColorStop(stop, point.color);
289 gradient.addColorStop(1, point.fade);
290
291 ctx.globalCompositeOperation = point.blend === "normal" ? "source-over" : point.blend;
292 ctx.fillStyle = gradient;
293 ctx.fillRect(0, 0, dims.width, dims.height);
294 });
295 };
296
297 const initExportCopy = (wrapper) => {
298 const exportPanel = wrapper.querySelector(".king-addons-gradient-mesh__export");
299 if (!exportPanel) {
300 return;
301 }
302 const button = exportPanel.querySelector(".king-addons-gradient-mesh__export-copy");
303 const textarea = exportPanel.querySelector(".king-addons-gradient-mesh__export-text");
304 if (!button || !textarea) {
305 return;
306 }
307 button.addEventListener("click", () => {
308 textarea.select();
309 try {
310 navigator.clipboard?.writeText(textarea.value);
311 } catch (error) {
312 document.execCommand("copy");
313 }
314 button.textContent = "Copied";
315 setTimeout(() => {
316 button.textContent = "Copy CSS";
317 }, 1200);
318 });
319 };
320
321 const initMesh = ($scope) => {
322 const wrapper = $scope[0]?.querySelector(".king-addons-gradient-mesh");
323 if (!wrapper) {
324 return;
325 }
326
327 const payload = parsePayload(wrapper);
328 if (!payload) {
329 return;
330 }
331
332 initExportCopy(wrapper);
333
334 const isEditMode = window.elementorFrontend?.isEditMode && elementorFrontend.isEditMode();
335 if (isEditMode) {
336 wrapper.classList.add("is-editor");
337 }
338
339 wrapper.dataset.motion = payload.motion || "drift";
340
341 const engine = resolveEngine(payload);
342 if (engine === "canvas") {
343 wrapper.classList.add("is-canvas");
344 } else {
345 wrapper.classList.remove("is-canvas");
346 }
347
348 if (engine === "css" && (!supportsCssVars() || !supportsGradients())) {
349 applyFallback(wrapper, payload);
350 return;
351 }
352
353 const points = buildPointState(payload.points || []);
354
355 const prefersReduced = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
356 const animate = payload.animate && !prefersReduced;
357 if (!animate) {
358 wrapper.classList.remove("is-animated");
359 if (engine === "canvas" && points.length) {
360 const canvas = wrapper.querySelector(".king-addons-gradient-mesh__canvas");
361 const ctx = canvas ? canvas.getContext("2d") : null;
362 if (ctx) {
363 const drawStatic = () => {
364 const dims = { width: wrapper.clientWidth, height: wrapper.clientHeight };
365 canvas.width = dims.width;
366 canvas.height = dims.height;
367 drawCanvas(ctx, points, dims, 0);
368 };
369 drawStatic();
370 window.addEventListener("resize", drawStatic);
371 }
372 }
373 return;
374 }
375
376 const hasMotion = points.some((point) => point.motion !== "static");
377 const scrollMode = payload.scroll?.mode || "off";
378 const needsLoop = scrollMode !== "off" || hasMotion;
379
380 if (!needsLoop) {
381 if (engine === "canvas" && points.length) {
382 const canvas = wrapper.querySelector(".king-addons-gradient-mesh__canvas");
383 const ctx = canvas ? canvas.getContext("2d") : null;
384 if (ctx) {
385 const drawStatic = () => {
386 const dims = { width: wrapper.clientWidth, height: wrapper.clientHeight };
387 canvas.width = dims.width;
388 canvas.height = dims.height;
389 drawCanvas(ctx, points, dims, 0);
390 };
391 drawStatic();
392 window.addEventListener("resize", drawStatic);
393 }
394 }
395 return;
396 }
397
398 const canvas = wrapper.querySelector(".king-addons-gradient-mesh__canvas");
399 const ctx = engine === "canvas" && canvas ? canvas.getContext("2d") : null;
400 const dims = { width: wrapper.clientWidth, height: wrapper.clientHeight };
401
402 const updateCanvasSize = () => {
403 dims.width = wrapper.clientWidth;
404 dims.height = wrapper.clientHeight;
405 if (canvas) {
406 canvas.width = dims.width;
407 canvas.height = dims.height;
408 }
409 };
410
411 updateCanvasSize();
412
413 const isMobile = window.matchMedia("(max-width: 767px)").matches;
414 const fps = getPerformanceFps(payload, isMobile);
415 const frameInterval = 1000 / fps;
416 let lastTime = 0;
417 let rafId = 0;
418 let inView = true;
419 let scrollProgress = 0;
420
421 const updateScroll = () => {
422 scrollProgress = getScrollProgress(wrapper, payload, { isMobile });
423 };
424
425 if (scrollMode !== "off") {
426 window.addEventListener("scroll", () => {
427 window.requestAnimationFrame(updateScroll);
428 });
429 updateScroll();
430 }
431
432 const observer = "IntersectionObserver" in window
433 ? new IntersectionObserver((entries) => {
434 entries.forEach((entry) => {
435 inView = entry.isIntersecting;
436 });
437 })
438 : null;
439
440 if (observer) {
441 observer.observe(wrapper);
442 }
443
444 const animateFrame = (time) => {
445 if (!inView) {
446 rafId = window.requestAnimationFrame(animateFrame);
447 return;
448 }
449
450 if (time - lastTime < frameInterval) {
451 rafId = window.requestAnimationFrame(animateFrame);
452 return;
453 }
454
455 lastTime = time;
456
457 const speed = getSpeed(wrapper, payload);
458 const speedScale = 0.15 + (speed / 100) * 0.85;
459 let t = (time / 1000) * speedScale;
460
461 if (scrollMode !== "off") {
462 if (scrollMode === "hybrid") {
463 t = t + scrollProgress * 6;
464 } else {
465 t = scrollProgress * 8;
466 }
467 }
468
469 if (engine === "canvas" && ctx) {
470 drawCanvas(ctx, points, dims, t);
471 } else {
472 setPointVars(wrapper, points, dims, t);
473 }
474
475 rafId = window.requestAnimationFrame(animateFrame);
476 };
477
478 rafId = window.requestAnimationFrame(animateFrame);
479 window.addEventListener("resize", () => {
480 updateCanvasSize();
481 });
482
483 if (isEditMode) {
484 wrapper.addEventListener("mouseleave", () => {
485 if (rafId) {
486 cancelAnimationFrame(rafId);
487 rafId = window.requestAnimationFrame(animateFrame);
488 }
489 });
490 }
491 };
492
493 $(window).on("elementor/frontend/init", function () {
494 elementorFrontend.hooks.addAction(
495 "frontend/element_ready/king-addons-interactive-gradient-mesh.default",
496 function ($scope) {
497 initMesh($scope);
498 }
499 );
500 });
501 })(jQuery);
502