PageSourceSearch

https://w4u.in/assets/ai-assistant/bw-section-effects.js?v=1784784114

js w4u.in collected 2026-10-01 15:55:45 UTC 22,508 bytes, 578 lines download raw bytes

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">&times;</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.