1/** 2 * Per-section particle effects (editor + live). Modal UI + canvas engine. 3 * Inspired by particles.js presets: https://vincentgarreau.com/particles.js/ 4 */ 5(function (global) { 6 'use strict'; 7 8 var PRESETS = { 9 default: { 10 label: 'Default (network)', 11 variant: 'network', 12 count: 70, 13 speed: 1, 14 size: 2.5, 15 opacity: 0.45, 16 linkDistance: 110, 17 linkOpacity: 0.14, 18 lines: true 19 }, 20 nasa: { 21 label: 'NASA (stars)', 22 variant: 'stars', 23 count: 140, 24 speed: 0.35, 25 size: 1.8, 26 opacity: 0.7, 27 linkDistance: 0, 28 linkOpacity: 0, 29 lines: false 30 }, 31 bubble: { 32 label: 'Bubble', 33 variant: 'bubbles', 34 count: 40, 35 speed: 0.6, 36 size: 5, 37 opacity: 0.35, 38 linkDistance: 0, 39 linkOpacity: 0, 40 lines: false 41 }, 42 snow: { 43 label: 'Snow', 44 variant: 'snow', 45 count: 90, 46 speed: 0.8, 47 size: 2.2, 48 opacity: 0.65, 49 linkDistance: 0, 50 linkOpacity: 0, 51 lines: false 52 }, 53 orbs: { 54 label: 'Soft orbs', 55 variant: 'orbs', 56 count: 35, 57 speed: 0.5, 58 size: 4, 59 opacity: 0.4, 60 linkDistance: 0, 61 linkOpacity: 0, 62 lines: false 63 } 64 }; 65 66 var $modal = null; 67 var $targetSection = null; 68 var previewRunning = false; 69 70 function hexToRgb(hex) { 71 hex = String(hex || '#4766e6').replace('#', ''); 72 if (hex.length === 3) { 73 hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2]; 74 } 75 var n = parseInt(hex, 16); 76 if (isNaN(n)) { 77 return { r: 71, g: 102, b: 230 }; 78 } 79 return { r: (n >> 16) & 255, g: (n >> 8) & 255, b: n & 255 }; 80 } 81 82 function brandColors() { 83 var s = global.getComputedStyle(document.documentElement); 84 return { 85 c1: (s.getPropertyValue('--c1') || '').trim() || '#4766e6', 86 c2: (s.getPropertyValue('--c2') || '').trim() || '#064e39' 87 }; 88 } 89 90 function defaultSettings() { 91 var b = brandColors(); 92 return { 93 preset: 'default', 94 c1: b.c1, 95 c2: b.c2, 96 count: PRESETS.default.count, 97 speed: PRESETS.default.speed, 98 size: PRESETS.default.size, 99 opacity: PRESETS.default.opacity, 100 linkDistance: PRESETS.default.linkDistance, 101 linkOpacity: PRESETS.default.linkOpacity, 102 lines: PRESETS.default.lines 103 }; 104 } 105 106 function presetConfig(key) { 107 return PRESETS[key] || PRESETS.default; 108 } 109 110 function readSettingsFromSection($sec) { 111 var $wrap = $sec.find('.bw-ai-particles').first(); 112 if (!$wrap.length) { 113 return null; 114 } 115 var s = defaultSettings(); 116 var preset = $wrap.attr('data-bw-particle-preset') || ''; 117 if (preset && PRESETS[preset]) { 118 s.preset = preset; 119 } else { 120 var v = $wrap.attr('data-bw-particle-variant') || 'network'; 121 Object.keys(PRESETS).forEach(function (k) { 122 if (PRESETS[k].variant === v) { 123 s.preset = k; 124 } 125 }); 126 } 127 s.c1 = $wrap.attr('data-bw-particle-c1') || s.c1; 128 s.c2 = $wrap.attr('data-bw-particle-c2') || s.c2; 129 s.count = parseInt($wrap.attr('data-bw-particle-count'), 10) || presetConfig(s.preset).count; 130 s.speed = parseFloat($wrap.attr('data-bw-particle-speed')) || presetConfig(s.preset).speed; 131 s.size = parseFloat($wrap.attr('data-bw-particle-size')) || presetConfig(s.preset).size; 132 s.opacity = parseFloat($wrap.attr('data-bw-particle-opacity')) || presetConfig(s.preset).opacity; 133 s.linkDistance = parseInt($wrap.attr('data-bw-particle-link-dist'), 10); 134 if (isNaN(s.linkDistance)) { 135 s.linkDistance = presetConfig(s.preset).linkDistance; 136 } 137 s.lines = $wrap.attr('data-bw-particle-lines') !== '0'; 138 return s; 139 } 140
141 function buildOverlayHtml(settings) { 142 var p = presetConfig(settings.preset); 143 var variant = p.variant; 144 return '<div class="is-overlay bw-ai-particles" data-bw-particle-preset="' + settings.preset + '"' 145 + ' data-bw-particle-variant="' + variant + '"' 146 + ' data-bw-particle-c1="' + settings.c1 + '"' 147 + ' data-bw-particle-c2="' + settings.c2 + '"' 148 + ' data-bw-particle-count="' + settings.count + '"' 149 + ' data-bw-particle-speed="' + settings.speed + '"' 150 + ' data-bw-particle-size="' + settings.size + '"' 151 + ' data-bw-particle-opacity="' + settings.opacity + '"' 152 + ' data-bw-particle-link-dist="' + settings.linkDistance + '"' 153 + ' data-bw-particle-lines="' + (settings.lines ? '1' : '0') + '"' 154 + '><canvas class="bw-particle-canvas"></canvas></div>'; 155 } 156 157 function ensureSectionLayout($sec) { 158 /* ContentBox box.css handles .is-section.is-box + .is-overlay stacking â no inline position */ 159 } 160 161 /** Insert particles after photo .is-overlay-bg, before .is-boxes (canvas must paint above bg). */ 162 function insertParticlesOverlay($sec, html) { 163 var $particle = global.jQuery(html); 164 var $boxes = $sec.children('.is-boxes').first(); 165 if ($boxes.length) { 166 $boxes.before($particle); 167 return; 168 } 169 var $bgOverlay = $sec.children('.is-overlay').not('.bw-ai-particles').filter(function () { 170 return global.jQuery(this).find('.is-overlay-bg').length > 0; 171 }).last(); 172 if ($bgOverlay.length) { 173 $bgOverlay.after($particle); 174 } else { 175 $sec.prepend($particle); 176 } 177 } 178 179 function stopCanvas(canvas) { 180 if (!canvas) { 181 return; 182 } 183 canvas._bwParticleRunning = false; 184 if (canvas._bwParticleRaf) { 185 cancelAnimationFrame(canvas._bwParticleRaf); 186 canvas._bwParticleRaf = 0; 187 } 188 } 189 190 function startCanvas(canvas, settings) { 191 if (!canvas) { 192 return; 193 } 194 stopCanvas(canvas); 195 canvas._bwParticleRunning = true; 196 var ctx = canvas.getContext('2d'); 197 if (!ctx) { 198 return; 199 } 200 var p = presetConfig(settings.preset); 201 var variant = p.variant; 202 var rgb1 = hexToRgb(settings.c1); 203 var rgb2 = hexToRgb(settings.c2); 204 var count = Math.max(10, Math.min(200, parseInt(settings.count, 10) || 70)); 205 var speedMul = Math.max(0.1, Math.min(3, parseFloat(settings.speed) || 1)); 206 var sizeMul = Math.max(0.5, Math.min(8, parseFloat(settings.size) || 2.5)); 207 var opacityMul = Math.max(0.1, Math.min(1, parseFloat(settings.opacity) || 0.45)); 208 var linkDist = Math.max(0, parseInt(settings.linkDistance, 10) || 0); 209 var drawLines = settings.lines && variant === 'network' && linkDist > 0; 210 var particles = []; 211 var w = 0; 212 var h = 0; 213 214 function resize() { 215 var par = canvas.parentElement; 216 w = (par && par.clientWidth) ? par.clientWidth : 800; 217 h = (par && par.clientHeight) ? par.clientHeight : 400; 218 if (h < 100) { 219 h = 100; 220 } 221 canvas.width = w; 222 canvas.height = h; 223 } 224 225 function seed() { 226 particles = []; 227 var i; 228 for (i = 0; i < count; i += 1) { 229 particles.push({ 230 x: Math.random() * w, 231 y: Math.random() * h, 232 vx: (Math.random() - 0.5) * speedMul * (variant === 'snow' ? 0.3 : 0.9), 233 vy: (Math.random() - 0.5) * speedMul * (variant === 'snow' ? 0.2 : 0.9), 234 r: (Math.random() * 1.5 + 0.8) * sizeMul, 235 a: (Math.random() * 0.35 + 0.2) * opacityMul, 236 useAlt: Math.random() > 0.5 237 }); 238 if (variant === 'snow') { 239 particles[i].vy = Math.abs(particles[i].vy) + 0.3 * speedMul; 240 } 241 if (variant === 'bubbles') { 242 particles[i].vy = -Math.abs(particles[i].vy) - 0.2 * speedMul; 243 } 244 } 245 } 246 247 function step() { 248 if (!canvas._bwParticleRunning) { 249 return; 250 } 251 ctx.clearRect(0, 0, w, h); 252 var i; 253 var j; 254 var pt; 255 var q; 256 var col; 257 for (i = 0; i < particles.length; i += 1) { 258 pt = particles[i]; 259 pt.x += pt.vx; 260 pt.y += pt.vy; 261 if (variant === 'snow') { 262 pt.y += 0.4 * speedMul; 263 } 264 if (variant === 'bubbles') { 265 pt.y += pt.vy; 266 } 267 if (pt.x < 0) { 268 pt.x = w; 269 } 270 if (pt.x > w) { 271 pt.x = 0; 272 } 273 if (pt.y < 0) { 274 pt.y = variant === 'snow' ? h : 0; 275 } 276 if (pt.y > h) { 277 pt.y = variant === 'snow' ? 0 : h; 278 } 279 col = pt.useAlt ? rgb2 : rgb1; 280 ctx.beginPath(); 281 if (variant === 'orbs' || variant === 'bubbles') { 282 var g = ctx.createRadialGradient(pt.x, pt.y, 0, pt.x, pt.y, pt.r * 3); 283 g.addColorStop(0, 'rgba(' + col.r + ',' + col.g + ',' + col.b + ',' + pt.a + ')'); 284 g.addColorStop(1, 'rgba(' + col.r + ',' + col.g + ',' + col.b + ',0)'); 285 ctx.fillStyle = g; 286 ctx.arc(pt.x, pt.y, pt.r * 3, 0, Math.PI * 2); 287 } else { 288 ctx.fillStyle = 'rgba(' + col.r + ',' + col.g + ',' + col.b + ',' + pt.a + ')'; 289 ctx.arc(pt.x, pt.y, pt.r, 0, Math.PI * 2); 290 } 291 ctx.fill(); 292 if (drawLines) { 293 for (j = i + 1; j < particles.length; j += 1) { 294 q = particles[j]; 295 var dx = pt.x - q.x; 296 var dy = pt.y - q.y; 297 var dist = Math.sqrt(dx * dx + dy * dy); 298 if (dist < linkDist) { 299 var lo = (settings.linkOpacity || 0.14) * (1 - dist / linkDist); 300 ctx.strokeStyle = 'rgba(' + rgb1.r + ',' + rgb1.g + ',' + rgb1.b + ',' + lo + ')'; 301 ctx.lineWidth = 1; 302 ctx.beginPath(); 303 ctx.moveTo(pt.x, pt.y); 304 ctx.lineTo(q.x, q.y); 305 ctx.stroke(); 306 } 307 } 308 } 309 } 310 canvas._bwParticleRaf = requestAnimationFrame(step); 311 } 312 313 resize(); 314 seed(); 315 step(); 316 canvas._bwResizeHandler = function () { 317 if (!canvas._bwParticleRunning) { 318 return; 319 } 320 resize(); 321 seed(); 322 }; 323 window.addEventListener('resize', canvas._bwResizeHandler); 324 } 325 326 function initParticles(root) { 327 root = root || document; 328 var $root = root.jquery ? root : (global.jQuery ? global.jQuery(root) : null); 329 if (!$root) { 330 return; 331 } 332 $root.find('.bw-ai-particles').each(function () { 333 var $wrap = global.jQuery(this); 334 var canvas = $wrap.find('canvas.bw-particle-canvas')[0]; 335 if (canvas && canvas._bwParticleRunning) { 336 return; 337 } 338 var $sec = $wrap.closest('.is-section'); 339 var settings = $sec.length ? readSettingsFromSection($sec) : null; 340 if (!settings) { 341 settings = defaultSettings(); 342 } 343 if (!canvas) { 344 $wrap.append('<canvas class="bw-particle-canvas"></canvas>'); 345 canvas = $wrap.find('canvas.bw-particle-canvas')[0]; 346 } 347 $wrap.attr('data-bw-particle-ready', '1'); 348 startCanvas(canvas, settings); 349 }); 350 } 351 352 function removeFromSection($sec) { 353 if (!$sec || !$sec.length) { 354 return; 355 } 356 $sec.find('.bw-ai-particles').each(function () { 357 var c = this.querySelector('canvas.bw-particle-canvas'); 358 stopCanvas(c); 359 this.parentNode.removeChild(this); 360 }); 361 triggerChange(); 362 } 363 364 function applyToSection($sec, settings) { 365 if (!$sec || !$sec.length) { 366 return; 367 } 368 removeFromSection($sec); 369 ensureSectionLayout($sec);
370 insertParticlesOverlay($sec, buildOverlayHtml(settings)); 371 initParticles($sec); 372 triggerChange(); 373 } 374 375 function triggerChange() { 376 var $w = global.jQuery ? global.jQuery('.is-wrapper') : null; 377 if ($w && $w.length) { 378 var cb = $w.data('contentbox'); 379 if (cb && cb.settings && typeof cb.settings.onChange === 'function') { 380 cb.settings.onChange(); 381 } 382 } 383 } 384 385 function collectModalSettings() { 386 var preset = $modal.find('#bw-fx-preset').val() || 'default'; 387 var base = presetConfig(preset); 388 return { 389 preset: preset, 390 c1: $modal.find('#bw-fx-c1').val() || brandColors().c1, 391 c2: $modal.find('#bw-fx-c2').val() || brandColors().c2, 392 count: parseInt($modal.find('#bw-fx-count').val(), 10) || base.count, 393 speed: parseFloat($modal.find('#bw-fx-speed').val()) || base.speed, 394 size: parseFloat($modal.find('#bw-fx-size').val()) || base.size, 395 opacity: parseFloat($modal.find('#bw-fx-opacity').val()) || base.opacity, 396 linkDistance: parseInt($modal.find('#bw-fx-link').val(), 10), 397 lines: $modal.find('#bw-fx-lines').is(':checked') 398 }; 399 } 400 401 function fillModal(settings) { 402 settings = settings || defaultSettings(); 403 $modal.find('#bw-fx-preset').val(settings.preset); 404 $modal.find('#bw-fx-c1').val(settings.c1); 405 $modal.find('#bw-fx-c2').val(settings.c2); 406 $modal.find('#bw-fx-count').val(settings.count); 407 $modal.find('#bw-fx-speed').val(settings.speed); 408 $modal.find('#bw-fx-size').val(settings.size); 409 $modal.find('#bw-fx-opacity').val(settings.opacity); 410 $modal.find('#bw-fx-link').val(settings.linkDistance); 411 $modal.find('#bw-fx-lines').prop('checked', settings.lines); 412 toggleLinkRow(); 413 } 414 415 function toggleLinkRow() { 416 var preset = $modal.find('#bw-fx-preset').val(); 417 var isNet = presetConfig(preset).variant === 'network'; 418 $modal.find('.bw-fx-link-row').toggle(isNet); 419 } 420 421 function applyPresetToForm(presetKey) { 422 var p = presetConfig(presetKey); 423 $modal.find('#bw-fx-count').val(p.count); 424 $modal.find('#bw-fx-speed').val(p.speed); 425 $modal.find('#bw-fx-size').val(p.size); 426 $modal.find('#bw-fx-opacity').val(p.opacity); 427 $modal.find('#bw-fx-link').val(p.linkDistance); 428 $modal.find('#bw-fx-lines').prop('checked', p.lines); 429 toggleLinkRow(); 430 if (previewRunning && $targetSection && $targetSection.length) { 431 applyToSection($targetSection, collectModalSettings()); 432 } 433 } 434 435 function ensureModal() { 436 if ($modal && $modal.length) { 437 return; 438 } 439 if (!global.jQuery) { 440 return; 441 } 442 var $ = global.jQuery; 443 var presetOpts = ''; 444 Object.keys(PRESETS).forEach(function (k) { 445 presetOpts += '<option value="' + k + '">' + PRESETS[k].label + '</option>'; 446 }); 447 var html = '<div id="bw-fx-modal" class="bw-fx-modal" role="dialog" aria-labelledby="bw-fx-title" hidden>' 448 + '<div class="bw-fx-dialog">' 449 + '<header class="bw-fx-head"><h2 id="bw-fx-title">Section effects</h2>' 450 + '<button type="button" class="bw-fx-close" aria-label="Close">×</button></header>' 451 + '<div class="bw-fx-body">' 452 + '<label class="bw-fx-field"><span>Preset</span><select id="bw-fx-preset">' + presetOpts + '</select></label>' 453 + '<div class="bw-fx-colors"><label class="bw-fx-field"><span>Color 1</span><input type="color" id="bw-fx-c1"></label>' 454 + '<label class="bw-fx-field"><span>Color 2</span><input type="color" id="bw-fx-c2"></label></div>' 455 + '<label class="bw-fx-field"><span>Particle count <output id="bw-fx-count-out"></output></span>' 456 + '<input type="range" id="bw-fx-count" min="15" max="180" step="5"></label>' 457 + '<label class="bw-fx-field"><span>Speed <output id="bw-fx-speed-out"></output></span>' 458 + '<input type="range" id="bw-fx-speed" min="0.2" max="2.5" step="0.1"></label>' 459 + '<label class="bw-fx-field"><span>Size <output id="bw-fx-size-out"></output></span>' 460 + '<input type="range" id="bw-fx-size" min="1" max="8" step="0.2"></label>' 461 + '<label class="bw-fx-field"><span>Opacity <output id="bw-fx-opacity-out"></output></span>' 462 + '<input type="range" id="bw-fx-opacity" min="0.15" max="1" step="0.05"></label>' 463 + '<label class="bw-fx-field bw-fx-link-row"><span>Link distance <output id="bw-fx-link-out"></output></span>' 464 + '<input type="range" id="bw-fx-link" min="0" max="180" step="5"></label>' 465 + '<label class="bw-fx-check bw-fx-link-row"><input type="checkbox" id="bw-fx-lines" checked> Connect particles with lines</label>' 466 + '<label class="bw-fx-check">
466<input type="checkbox" id="bw-fx-preview" checked> Live preview while adjusting</label>' 467 + '</div>' 468 + '<footer class="bw-fx-foot">' 469 + '<button type="button" class="bw-fx-btn bw-fx-btn-danger" id="bw-fx-remove">Remove effects</button>' 470 + '<button type="button" class="bw-fx-btn" id="bw-fx-cancel">Cancel</button>' 471 + '<button type="button" class="bw-fx-btn bw-fx-btn-primary" id="bw-fx-apply">Apply</button>' 472 + '</footer></div></div>'; 473 $('body').append(html); 474 $modal = $('#bw-fx-modal'); 475 476 function syncOutputs() { 477 $modal.find('#bw-fx-count-out').text($modal.find('#bw-fx-count').val()); 478 $modal.find('#bw-fx-speed-out').text($modal.find('#bw-fx-speed').val()); 479 $modal.find('#bw-fx-size-out').text($modal.find('#bw-fx-size').val()); 480 $modal.find('#bw-fx-opacity-out').text($modal.find('#bw-fx-opacity').val()); 481 $modal.find('#bw-fx-link-out').text($modal.find('#bw-fx-link').val()); 482 } 483 484 $modal.on('change', '#bw-fx-preset', function () { 485 applyPresetToForm($(this).val()); 486 syncOutputs(); 487 }); 488 $modal.on('input', '#bw-fx-count, #bw-fx-speed, #bw-fx-size, #bw-fx-opacity, #bw-fx-link, #bw-fx-c1, #bw-fx-c2', function () { 489 syncOutputs(); 490 previewRunning = $modal.find('#bw-fx-preview').is(':checked'); 491 if (previewRunning && $targetSection && $targetSection.length) { 492 applyToSection($targetSection, collectModalSettings()); 493 } 494 }); 495 $modal.on('change', '#bw-fx-lines, #bw-fx-preview', function () { 496 previewRunning = $modal.find('#bw-fx-preview').is(':checked'); 497 if (previewRunning && $targetSection && $targetSection.length) { 498 applyToSection($targetSection, collectModalSettings()); 499 } 500 }); 501 $modal.on('click', '.bw-fx-close, #bw-fx-cancel', closeModal); 502 $modal.on('click', '#bw-fx-apply', function () { 503 if ($targetSection && $targetSection.length) { 504 applyToSection($targetSection, collectModalSettings()); 505 } 506 closeModal(); 507 }); 508 $modal.on('click', '#bw-fx-remove', function () { 509 if ($targetSection && $targetSection.length) { 510 removeFromSection($targetSection); 511 } 512 closeModal(); 513 }); 514 } 515 516 function openModal($sec) { 517 if (!global.jQuery) { 518 return; 519 } 520 ensureModal(); 521 $targetSection = $sec; 522 var existing = readSettingsFromSection($sec); 523 fillModal(existing || defaultSettings()); 524 previewRunning = true; 525 $modal.removeAttr('hidden').addClass('is-open'); 526 global.jQuery('body').addClass('bw-fx-modal-open'); 527 if (!$sec.find('.bw-ai-particles').length && $modal.find('#bw-fx-preview').is(':checked')) { 528 applyToSection($sec, collectModalSettings()); 529 } 530 } 531 532 function closeModal() { 533 previewRunning = false; 534 if ($modal) { 535 $modal.removeClass('is-open').attr('hidden', 'hidden'); 536 } 537 if (global.jQuery) { 538 global.jQuery('body').removeClass('bw-fx-modal-open'); 539 } 540 $targetSection = null; 541 } 542 543 global.BwSectionEffects = { 544 PRESETS: PRESETS, 545 init: initParticles, 546 apply: applyToSection, 547 remove: removeFromSection, 548 openModal: openModal, 549 readSettings: readSettingsFromSection, 550 defaultSettings: defaultSettings 551 }; 552 553 function bootLiveParticles() { 554 if (!global.jQuery) { 555 return; 556 } 557 initParticles(document); 558 } 559 560 function scheduleLiveBoot() { 561 if (global.jQuery) { 562 global.jQuery(bootLiveParticles); 563 global.jQuery(global).on('load', function () { 564 setTimeout(bootLiveParticles, 80); 565 }); 566 return; 567 } 568 if (document.readyState === 'loading') { 569 document.addEventListener('DOMContentLoaded', scheduleLiveBoot); 570 } else { 571 setTimeout(scheduleLiveBoot, 50); 572 } 573 } 574 575 if (typeof document !== 'undefined') { 576 scheduleLiveBoot(); 577 } 578})(typeof window !== 'undefined' ? window : this);
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.