Plugin Directory

Changeset 3441664


Ignore:
Timestamp:
01/17/2026 06:24:54 PM (5 weeks ago)
Author:
svisciano
Message:

Release 0.1.1

Location:
svisciano-snowfall-effect
Files:
29 added
5 edited

Legend:

Unmodified
Added
Removed
  • svisciano-snowfall-effect/trunk/assets/js/svis-snow.js

    r3436116 r3441664  
    11if (SVIS_SNOW_DATA.enabled === "1") {
    2     const DEBUG_MODE = false;
    3     const startTime = performance.now();
    4     const TABLE_SIZE = 4096;
    5     const TWO_PI = Math.PI * 2;
    6     const INV_TWO_PI = 1 / TWO_PI;
    7     const sinTable = new Float32Array(TABLE_SIZE);
    8     const cosTable = new Float32Array(TABLE_SIZE);
    9     for (let i = 0; i < TABLE_SIZE; i++) {
    10         const a = i / TABLE_SIZE * TWO_PI;
    11         sinTable[i] = Math.sin(a);
    12         cosTable[i] = Math.cos(a);
    13     }
    14     let isScrollable = SVIS_SNOW_DATA.scroll_with_page === "1";
    15     let currentWidth = document.documentElement.clientWidth;
    16     let currentHeight = isScrollable ? document.documentElement.scrollHeight : document.documentElement.clientHeight;
    17     let deviceRatio = window.devicePixelRatio || 1;
    18     const isMobile = navigator.hardwareConcurrency <= 2 || /Mobile|Android/i.test(navigator.userAgent);
    19     const canvas = document.createElement("canvas");
    20     const ctx = canvas.getContext("2d", {
    21         alpha: true
     2    document.addEventListener("DOMContentLoaded", function() {
     3        const DEBUG_MODE = false;
     4        const startTime = performance.now();
     5        const TABLE_SIZE = 4096;
     6        const TWO_PI = Math.PI * 2;
     7        const INV_TWO_PI = 1 / TWO_PI;
     8        const sinTable = new Float32Array(TABLE_SIZE);
     9        const cosTable = new Float32Array(TABLE_SIZE);
     10        for (let i = 0; i < TABLE_SIZE; i++) {
     11            const a = i / TABLE_SIZE * TWO_PI;
     12            sinTable[i] = Math.sin(a);
     13            cosTable[i] = Math.cos(a);
     14        }
     15        let isScrollable = SVIS_SNOW_DATA.scroll_with_page === "1";
     16        let currentWidth = document.documentElement.clientWidth;
     17        let currentHeight = isScrollable ? document.documentElement.scrollHeight : document.documentElement.clientHeight;
     18        let deviceRatio = window.devicePixelRatio || 1;
     19        const isMobile = navigator.hardwareConcurrency <= 2 || /Mobile|Android/i.test(navigator.userAgent);
     20        const canvas = document.createElement("canvas");
     21        const ctx = canvas.getContext("2d", {
     22            alpha: true
     23        });
     24        canvas.style.position = isScrollable ? "absolute" : "fixed";
     25        canvas.style.top = "0";
     26        canvas.style.left = "0";
     27        canvas.style.pointerEvents = "none";
     28        canvas.style.zIndex = SVIS_SNOW_DATA.z_index || 9999;
     29        let VERTICAL_AREA = 0;
     30        let MAX_FLAKES = 0;
     31        const flakes = [];
     32        function parseVerticalArea(value) {
     33            const totalHeight = currentHeight;
     34            if (!value) return totalHeight;
     35            value = value.trim();
     36            if (value.endsWith("%")) {
     37                let num = parseFloat(value.replace("%", ""));
     38                if (isNaN(num)) return totalHeight;
     39                num = Math.min(Math.max(num, 0), 100);
     40                return totalHeight * (num / 100);
     41            }
     42            if (value.endsWith("px")) {
     43                let num = parseFloat(value.replace("px", ""));
     44                return isNaN(num) ? totalHeight : num;
     45            }
     46            let num = parseFloat(value);
     47            return isNaN(num) ? totalHeight : num;
     48        }
     49        const QUALITY_MULTIPLIER = isMobile ? 1 : 1.2;
     50        const flakeCache = Object.create(null);
     51        function getFlakeCanvas(size, color, char) {
     52            const dpr = window.devicePixelRatio || 1;
     53            const q = dpr * QUALITY_MULTIPLIER;
     54            const key = `${size}_${color}_${char}_${q}`;
     55            if (flakeCache[key]) return flakeCache[key];
     56            const c = document.createElement("canvas");
     57            const s = Math.ceil(size * 2);
     58            c.width = s * q;
     59            c.height = s * q;
     60            c.style.width = s + "px";
     61            c.style.height = s + "px";
     62            const cctx = c.getContext("2d");
     63            cctx.scale(q, q);
     64            cctx.fillStyle = color;
     65            cctx.textAlign = "center";
     66            cctx.textBaseline = "middle";
     67            cctx.font = `${size}px serif`;
     68            cctx.fillText(char, s / 2, s / 2);
     69            flakeCache[key] = c;
     70            return c;
     71        }
     72        function rand(min, max) {
     73            min = parseFloat(min);
     74            max = parseFloat(max);
     75            if (isNaN(min) || isNaN(max)) return 0;
     76            return Math.random() * (max - min) + min;
     77        }
     78        const clamp = (value, fieldId) => {
     79            const meta = SVIS_SNOW_DATA._field_meta?.[fieldId];
     80            if (!meta) return value;
     81            let result = value;
     82            if (meta.min !== null) result = Math.max(meta.min, result);
     83            if (meta.max !== null) result = Math.min(meta.max, result);
     84            return result;
     85        };
     86        function angleToIndex(angle) {
     87            let idx = Math.floor(angle % TWO_PI * INV_TWO_PI * TABLE_SIZE);
     88            if (idx < 0) idx += TABLE_SIZE;
     89            return idx;
     90        }
     91        let globalGustIntensity = clamp(parseFloat(SVIS_SNOW_DATA.global_gust_strength) || 0, "global_gust_strength");
     92        let globalGustX = 0;
     93        let globalGustY = 0;
     94        let globalGustTimer = 0;
     95        let nextGlobalGust = rand(2, 5);
     96        function updateGlobalGusts(delta) {
     97            globalGustIntensity = clamp(parseFloat(SVIS_SNOW_DATA.global_gust_strength) || 0, "global_gust_strength") * .2;
     98            if (globalGustIntensity <= 0) {
     99                globalGustX = 0;
     100                globalGustY = 0;
     101                globalGustTimer = 0;
     102                return;
     103            }
     104            globalGustTimer += delta;
     105            if (globalGustTimer > nextGlobalGust) {
     106                globalGustX = globalGustIntensity / 10 * rand(-3, 3);
     107                globalGustY = globalGustIntensity / 10 * rand(-1.5, 1.5);
     108                const maxInterval = 15;
     109                const minInterval = 4;
     110                const gustFrequency = maxInterval - globalGustIntensity / 10 * (maxInterval - minInterval);
     111                nextGlobalGust = globalGustTimer + rand(gustFrequency * .8, gustFrequency * 1.2) * 3;
     112            }
     113            globalGustX *= .993;
     114            globalGustY *= .993;
     115        }
     116        class Snowflake {
     117            isAlive=false;
     118            constructor(firstSpawn = false, spawnIndexY = 0, spawnIndexX = 0, totalSpawning = 1) {
     119                this.spawn(firstSpawn, spawnIndexY, spawnIndexX, totalSpawning);
     120            }
     121            spawn(firstSpawn = false, spawnIndexY = 0, spawnIndexX = 0, totalSpawning = 1) {
     122                this.size = Math.round(clamp(rand(SVIS_SNOW_DATA.flake_min_size, SVIS_SNOW_DATA.flake_max_size), "flake_min_size")) * 1.5;
     123                this.img = getFlakeCanvas(this.size, SVIS_SNOW_DATA.flake_color, SVIS_SNOW_DATA.flake_type);
     124                if (firstSpawn && totalSpawning > 1) {
     125                    const sliceHeight = VERTICAL_AREA / totalSpawning;
     126                    const sliceStartY = spawnIndexY * sliceHeight;
     127                    this.y = rand(sliceStartY, sliceStartY + sliceHeight);
     128                    const sliceWidth = currentWidth / totalSpawning;
     129                    const sliceStartX = spawnIndexX * sliceWidth;
     130                    this.x = rand(sliceStartX, sliceStartX + sliceWidth);
     131                } else {
     132                    this.x = rand(0, currentWidth);
     133                    this.y = firstSpawn ? rand(0, VERTICAL_AREA) : -this.size * rand(1, 2);
     134                }
     135                this.verticalWrapLimit = -this.size * 2 * rand(1, 2);
     136                this.baseWindForce = clamp(rand(SVIS_SNOW_DATA.min_wind_speed, SVIS_SNOW_DATA.max_wind_speed), "min_wind_speed") * .15;
     137                this.gravityForce = clamp(rand(SVIS_SNOW_DATA.min_fall_speed, SVIS_SNOW_DATA.max_fall_speed), "min_fall_speed") * .1;
     138                this.vx = this.baseWindForce;
     139                this.vy = this.gravityForce;
     140                this.rotation = rand(0, TWO_PI);
     141                this.rotationSpeed = clamp(rand(SVIS_SNOW_DATA.min_rotation_speed, SVIS_SNOW_DATA.max_rotation_speed) * .3, "min_rotation_speed");
     142                const depthMin = SVIS_SNOW_DATA._field_meta?.flake_depth?.min ?? 0;
     143                const depthMax = SVIS_SNOW_DATA._field_meta?.flake_depth?.max ?? 10;
     144                let depthValue = clamp(parseFloat(SVIS_SNOW_DATA.flake_depth) || 0, "flake_depth");
     145                depthValue = Math.max(depthMin, Math.min(depthMax, depthValue));
     146                const depthNormalized = (depthValue - depthMin) / (depthMax - depthMin);
     147                const opacityMin = .1;
     148                const opacityMax = 1;
     149                const lowerLimit = opacityMin + (1 - depthNormalized) * (opacityMax - opacityMin);
     150                this.initialOpacity = rand(lowerLimit, opacityMax);
     151                this.opacity = this.initialOpacity;
     152                this.fadeTop = VERTICAL_AREA + this.size / 2 + rand(0, 0);
     153                this.fadeDistance = this.size / 2 + rand(0, this.size);
     154                this.fadeBottom = this.fadeTop + this.fadeDistance;
     155                this.turbulenceLevel = clamp(parseFloat(SVIS_SNOW_DATA.turbulence) || 0, "turbulence") * .4;
     156                if (this.turbulenceLevel > 0) {
     157                    this.turbulenceTimer = rand(0, 10);
     158                    this.turbulenceOffset = rand(0, TWO_PI);
     159                    this.turbulenceFreqX = rand(1.5, 3.5);
     160                    this.turbulenceFreqY = rand(1, 2.5);
     161                    this.nextTurbulenceChange = rand(1, 3);
     162                    this.currentGustX = 0;
     163                    this.targetGustX = 0;
     164                    this.currentGustY = 0;
     165                    this.targetGustY = 0;
     166                }
     167                if (globalGustIntensity > 0) {
     168                    this.currentGlobalGustX = 0;
     169                    this.currentGlobalGustY = 0;
     170                    const maxPossibleSize = SVIS_SNOW_DATA?._field_meta?.flake_max_size?.max || 100;
     171                    const normalizedSize = Math.min(1, this.size / (maxPossibleSize * 1.5));
     172                    this.globalGustSensitivity = rand(.8, 1.5) * (1 - Math.pow(normalizedSize, 1.5));
     173                }
     174            }
     175            update(delta) {
     176                let targetVx = this.baseWindForce;
     177                let targetVy = this.gravityForce;
     178                if (this.turbulenceLevel > 0) {
     179                    this.turbulenceTimer += delta;
     180                    if (this.turbulenceTimer > this.nextTurbulenceChange) {
     181                        this.targetGustX = this.turbulenceLevel / 10 * rand(-2, 2);
     182                        this.targetGustY = this.turbulenceLevel / 10 * rand(-1, 1);
     183                        const gustFrequency = Math.max(.5, 5 - this.turbulenceLevel / 10);
     184                        this.nextTurbulenceChange = this.turbulenceTimer + rand(gustFrequency * .8, gustFrequency * 1.5);
     185                    }
     186                    const lerpSpeed = 4 * delta;
     187                    this.currentGustX += (this.targetGustX - this.currentGustX) * lerpSpeed;
     188                    this.currentGustY += (this.targetGustY - this.currentGustY) * lerpSpeed;
     189                    const baseVariation = this.turbulenceLevel / 10 * 2.5;
     190                    const waveX = Math.sin(this.turbulenceTimer * this.turbulenceFreqX + this.turbulenceOffset) * baseVariation;
     191                    const waveY = Math.cos(this.turbulenceTimer * this.turbulenceFreqY + this.turbulenceOffset * .7) * baseVariation * .3;
     192                    targetVx += this.currentGustX + waveX;
     193                    targetVy += this.currentGustY + waveY;
     194                }
     195                if (globalGustIntensity > 0) {
     196                    const globalLerpSpeed = .5 * delta;
     197                    this.currentGlobalGustX += (globalGustX - this.currentGlobalGustX) * globalLerpSpeed;
     198                    this.currentGlobalGustY += (globalGustY - this.currentGlobalGustY) * globalLerpSpeed;
     199                    const globalInfluence = 4e4;
     200                    targetVx += this.currentGlobalGustX * globalInfluence * this.globalGustSensitivity * delta;
     201                    targetVy += this.currentGlobalGustY * globalInfluence * this.globalGustSensitivity * delta;
     202                }
     203                this.vx = targetVx;
     204                this.vy = targetVy;
     205                const TARGET_FPS = 60;
     206                const FRAME_TIME = 1 / TARGET_FPS;
     207                const deltaNorm = Math.min(3, delta / FRAME_TIME);
     208                this.x += this.vx * deltaNorm;
     209                this.y += this.vy * deltaNorm;
     210                this.rotation += this.rotationSpeed * delta;
     211                const w = currentWidth;
     212                const h = VERTICAL_AREA;
     213                if (this.x < -this.size) {
     214                    this.x = w + this.size;
     215                    this.y = Math.random() * h;
     216                }
     217                if (this.x > w + this.size) {
     218                    this.x = -this.size;
     219                    this.y = Math.random() * h;
     220                }
     221                if (this.y < this.verticalWrapLimit) {
     222                    this.x = Math.random() * w;
     223                    this.y = this.fadeBottom - .1;
     224                }
     225                let t = (this.y - this.fadeTop) / (this.fadeBottom - this.fadeTop);
     226                t = Math.max(0, Math.min(1, t));
     227                this.opacity = (1 - t) * this.initialOpacity;
     228                this.isAlive = this.opacity > 0;
     229            }
     230            draw() {
     231                ctx.globalAlpha = this.opacity;
     232                if (this.rotation !== 0 && this.size > 8) {
     233                    const ridx = angleToIndex(this.rotation);
     234                    const cos = cosTable[ridx];
     235                    const sin = sinTable[ridx];
     236                    const d = deviceRatio;
     237                    ctx.setTransform(cos * d, sin * d, -sin * d, cos * d, this.x * d, this.y * d);
     238                    ctx.drawImage(this.img, -this.size, -this.size, this.size * 2, this.size * 2);
     239                } else {
     240                    const d = deviceRatio;
     241                    ctx.setTransform(d, 0, 0, d, (this.x - this.size) * d, (this.y - this.size) * d);
     242                    ctx.drawImage(this.img, 0, 0, this.size * 2, this.size * 2);
     243                }
     244                ctx.globalAlpha = 1;
     245            }
     246        }
     247        function resizeCanvas() {
     248            deviceRatio = window.devicePixelRatio || 1;
     249            const isScrollable = SVIS_SNOW_DATA.scroll_with_page === "1";
     250            currentWidth = document.documentElement.clientWidth;
     251            currentHeight = isScrollable ? document.documentElement.scrollHeight : document.documentElement.clientHeight;
     252            canvas.width = Math.round(currentWidth * deviceRatio);
     253            canvas.height = Math.round(currentHeight * deviceRatio);
     254            canvas.style.width = currentWidth + "px";
     255            canvas.style.height = currentHeight + "px";
     256            VERTICAL_AREA = parseVerticalArea(SVIS_SNOW_DATA.vertical_area);
     257            const screenArea = currentWidth * currentHeight;
     258            const performanceMultiplier = isMobile ? .7 : 1;
     259            const newMaxFlakes = Math.max(5, Math.round(screenArea / 4e5 * parseInt(SVIS_SNOW_DATA.flakes_density) * performanceMultiplier));
     260            if (newMaxFlakes > MAX_FLAKES) {
     261                const toAdd = newMaxFlakes - MAX_FLAKES;
     262                const isInitialSpawn = flakes.length === 0;
     263                const xIndices = Array.from({
     264                    length: toAdd
     265                }, (_, i) => i);
     266                if (isInitialSpawn) {
     267                    for (let i = xIndices.length - 1; i > 0; i--) {
     268                        const j = Math.floor(Math.random() * (i + 1));
     269                        [xIndices[i], xIndices[j]] = [ xIndices[j], xIndices[i] ];
     270                    }
     271                }
     272                for (let i = 0; i < toAdd; i++) {
     273                    flakes.push(new Snowflake(isInitialSpawn, i, xIndices[i], toAdd));
     274                }
     275            }
     276            if (newMaxFlakes < MAX_FLAKES) {
     277                const aliveFlakes = flakes.filter(f => f.isAlive);
     278                const toDeactivate = aliveFlakes.length - newMaxFlakes;
     279                if (toDeactivate > 0) {
     280                    aliveFlakes.sort((a, b) => b.y - a.y).slice(0, toDeactivate).forEach(f => {
     281                        f.isFadingOut = true;
     282                    });
     283                }
     284            }
     285            MAX_FLAKES = newMaxFlakes;
     286        }
     287        resizeCanvas();
     288        window.addEventListener("resize", resizeCanvas);
     289        document.body.appendChild(canvas);
     290        let frameCount = 0;
     291        let fpsHistory = [];
     292        let lastFpsCheck = performance.now();
     293        function updatePerformanceLevel(currentFps) {
     294            fpsHistory.push(currentFps);
     295            if (fpsHistory.length > 20) fpsHistory.shift();
     296            const avgFps = Math.min(60, Math.max(0, fpsHistory.reduce((a, b) => a + b) / fpsHistory.length));
     297            if (avgFps < 30) {
     298                const aliveFlakes = flakes.filter(f => f.isAlive).length;
     299                if (aliveFlakes > MAX_FLAKES * .3) {
     300                    let toDeactivate = Math.min(2, aliveFlakes - Math.round(MAX_FLAKES * .3));
     301                    const sortedFlakes = flakes.map((flake, index) => ({
     302                        flake: flake,
     303                        index: index
     304                    })).filter(item => item.flake.isAlive).sort((a, b) => a.flake.y - b.flake.y);
     305                    for (let i = 0; i < toDeactivate && i < sortedFlakes.length; i++) {
     306                        sortedFlakes[i].flake.isFadingOut = true;
     307                    }
     308                }
     309            }
     310        }
     311        let lastTime = performance.now();
     312        function animate(now) {
     313            let delta = (now - lastTime) / 1e3 || 0;
     314            delta = Math.min(delta, .05);
     315            lastTime = now;
     316            frameCount++;
     317            if (now - lastFpsCheck > 1e3) {
     318                const currentFps = frameCount;
     319                updatePerformanceLevel(currentFps);
     320                frameCount = 0;
     321                lastFpsCheck = now;
     322            }
     323            ctx.setTransform(deviceRatio, 0, 0, deviceRatio, 0, 0);
     324            ctx.clearRect(0, 0, currentWidth, currentHeight);
     325            updateGlobalGusts(delta);
     326            for (let i = flakes.length - 1; i >= 0; i--) {
     327                const fl = flakes[i];
     328                fl.update(delta);
     329                if (!fl.isAlive) {
     330                    flakes.splice(i, 1);
     331                    continue;
     332                }
     333                fl.draw();
     334            }
     335            const aliveCount = flakes.filter(f => f.isAlive).length;
     336            if (aliveCount < MAX_FLAKES) {
     337                const toAdd = MAX_FLAKES - aliveCount;
     338                for (let i = 0; i < toAdd; i++) {
     339                    flakes.push(new Snowflake);
     340                }
     341            }
     342            drawDebugInfo();
     343            requestAnimationFrame(animate);
     344        }
     345        let outOfBoundsCount = 0;
     346        function drawDebugInfo() {
     347            if (!DEBUG_MODE) return;
     348            const scrollYOffset = isScrollable ? window.scrollY : 0;
     349            const now = performance.now();
     350            let ms = now - startTime;
     351            const hours = Math.floor(ms / 36e5);
     352            ms %= 36e5;
     353            const minutes = Math.floor(ms / 6e4);
     354            ms %= 6e4;
     355            const seconds = Math.floor(ms / 1e3);
     356            const deci = Math.floor(ms % 1e3 / 100);
     357            const uptime = String(hours).padStart(2, "0") + ":" + String(minutes).padStart(2, "0") + ":" + String(seconds).padStart(2, "0") + "." + deci;
     358            const avgFps = fpsHistory.length > 0 ? Math.round(fpsHistory.reduce((a, b) => a + b) / fpsHistory.length) : 0;
     359            const totalFlakes = flakes.length;
     360            const aliveFlakes = flakes.filter(f => f.isAlive).length;
     361            const renderEfficiency = Math.round(aliveFlakes / totalFlakes * 100);
     362            const debugLines = [ `Uptime: ${uptime}`, ``, `FPS: ${avgFps}`, `Flakes: ${totalFlakes}/${MAX_FLAKES} (alive: ${aliveFlakes})`, `Render efficiency: ${renderEfficiency}%`, `Screen: ${currentWidth}x${currentHeight}`, `Canvas: ${canvas.width}x${canvas.height}`, `Device ratio: ${deviceRatio}`, `Vertical Area: ${VERTICAL_AREA}px`, `Mobile device: ${isMobile ? "Yes" : "No"}`, ``, `Out of bounds (SafeArea): ${outOfBoundsCount}` ];
     363            const padding = 40;
     364            const lineHeight = 18;
     365            const boxWidth = 300;
     366            const boxHeight = debugLines.length * lineHeight + 8;
     367            ctx.save();
     368            ctx.setTransform(1, 0, 0, 1, 0, 0);
     369            ctx.scale(deviceRatio, deviceRatio);
     370            ctx.globalAlpha = 1;
     371            ctx.fillStyle = "rgba(0, 0, 0, 0.8)";
     372            ctx.fillRect(padding / 2, padding + scrollYOffset, boxWidth, boxHeight);
     373            debugLines.forEach((line, i) => {
     374                if (line.startsWith("Out of bounds") && outOfBoundsCount > 0) {
     375                    ctx.fillStyle = "red";
     376                    ctx.font = "bold 14px monospace";
     377                } else {
     378                    ctx.fillStyle = "#00ff00";
     379                    ctx.font = "14px monospace";
     380                }
     381                ctx.fillText(line, padding, padding + lineHeight * (i + 1) + scrollYOffset);
     382            });
     383            ctx.strokeStyle = "yellow";
     384            ctx.lineWidth = 1;
     385            ctx.setLineDash([ 5, 5 ]);
     386            ctx.beginPath();
     387            ctx.moveTo(0, VERTICAL_AREA);
     388            ctx.lineTo(currentWidth, VERTICAL_AREA);
     389            ctx.stroke();
     390            ctx.fillStyle = "yellow";
     391            ctx.font = "12px monospace";
     392            ctx.fillText("VERTICAL_AREA", 5, VERTICAL_AREA - 5);
     393            ctx.restore();
     394        }
     395        function checkSafeArea(flakeArray, canvasWidth, canvasHeight) {
     396            const safe = {
     397                minX: -canvasWidth * .5,
     398                maxX: canvasWidth * 1.5,
     399                minY: -canvasHeight * .5,
     400                maxY: canvasHeight * 1.5
     401            };
     402            let removedCount = 0;
     403            for (let i = flakeArray.length - 1; i >= 0; i--) {
     404                const f = flakeArray[i];
     405                if (f.x < safe.minX || f.x > safe.maxX || f.y < safe.minY || f.y > safe.maxY) {
     406                    flakeArray.splice(i, 1);
     407                    removedCount++;
     408                }
     409            }
     410            outOfBoundsCount += removedCount;
     411        }
     412        setInterval(() => {
     413            checkSafeArea(flakes, currentWidth, VERTICAL_AREA);
     414        }, 1e4);
     415        requestAnimationFrame(animate);
    22416    });
    23     canvas.style.position = isScrollable ? "absolute" : "fixed";
    24     canvas.style.top = "0";
    25     canvas.style.left = "0";
    26     canvas.style.pointerEvents = "none";
    27     canvas.style.zIndex = SVIS_SNOW_DATA.z_index || 9999;
    28     let VERTICAL_AREA = 0;
    29     let MAX_FLAKES = 0;
    30     const flakes = [];
    31     function parseVerticalArea(value) {
    32         const totalHeight = currentHeight;
    33         if (!value) return totalHeight;
    34         value = value.trim();
    35         if (value.endsWith("%")) {
    36             let num = parseFloat(value.replace("%", ""));
    37             if (isNaN(num)) return totalHeight;
    38             num = Math.min(Math.max(num, 0), 100);
    39             return totalHeight * (num / 100);
    40         }
    41         if (value.endsWith("px")) {
    42             let num = parseFloat(value.replace("px", ""));
    43             return isNaN(num) ? totalHeight : num;
    44         }
    45         let num = parseFloat(value);
    46         return isNaN(num) ? totalHeight : num;
    47     }
    48     const flakeCache = Object.create(null);
    49     function getFlakeCanvas(size, color, char) {
    50         const key = `${size}_${color}_${char}`;
    51         if (flakeCache[key]) return flakeCache[key];
    52         const c = document.createElement("canvas");
    53         const s = Math.ceil(size * 2);
    54         c.width = s;
    55         c.height = s;
    56         const cctx = c.getContext("2d");
    57         cctx.fillStyle = color;
    58         cctx.textAlign = "center";
    59         cctx.textBaseline = "middle";
    60         cctx.font = `${size}px serif`;
    61         cctx.fillText(char, s / 2, s / 2);
    62         flakeCache[key] = c;
    63         return c;
    64     }
    65     function rand(min, max) {
    66         min = parseFloat(min);
    67         max = parseFloat(max);
    68         if (isNaN(min) || isNaN(max)) return 0;
    69         return Math.random() * (max - min) + min;
    70     }
    71     const clamp = (value, fieldId) => {
    72         const meta = SVIS_SNOW_DATA._field_meta?.[fieldId];
    73         if (!meta) return value;
    74         let result = value;
    75         if (meta.min !== null) result = Math.max(meta.min, result);
    76         if (meta.max !== null) result = Math.min(meta.max, result);
    77         return result;
    78     };
    79     function angleToIndex(angle) {
    80         let idx = Math.floor(angle % TWO_PI * INV_TWO_PI * TABLE_SIZE);
    81         if (idx < 0) idx += TABLE_SIZE;
    82         return idx;
    83     }
    84     let globalGustIntensity = clamp(parseFloat(SVIS_SNOW_DATA.global_gust_strength) || 0, "global_gust_strength");
    85     let globalGustX = 0;
    86     let globalGustY = 0;
    87     let globalGustTimer = 0;
    88     let nextGlobalGust = rand(2, 5);
    89     function updateGlobalGusts(delta) {
    90         globalGustIntensity = clamp(parseFloat(SVIS_SNOW_DATA.global_gust_strength) || 0, "global_gust_strength");
    91         if (globalGustIntensity <= 0) {
    92             globalGustX = 0;
    93             globalGustY = 0;
    94             globalGustTimer = 0;
    95             return;
    96         }
    97         globalGustTimer += delta;
    98         if (globalGustTimer > nextGlobalGust) {
    99             globalGustX = globalGustIntensity / 10 * rand(-3, 3);
    100             globalGustY = globalGustIntensity / 10 * rand(-1.5, 1.5);
    101             const maxInterval = 15;
    102             const minInterval = 4;
    103             const gustFrequency = maxInterval - globalGustIntensity / 10 * (maxInterval - minInterval);
    104             nextGlobalGust = globalGustTimer + rand(gustFrequency * .8, gustFrequency * 1.2) * 3;
    105         }
    106         globalGustX *= .993;
    107         globalGustY *= .993;
    108     }
    109     class Snowflake {
    110         isAlive=false;
    111         constructor(firstSpawn = false, spawnIndexY = 0, spawnIndexX = 0, totalSpawning = 1) {
    112             this.spawn(firstSpawn, spawnIndexY, spawnIndexX, totalSpawning);
    113         }
    114         spawn(firstSpawn = false, spawnIndexY = 0, spawnIndexX = 0, totalSpawning = 1) {
    115             this.size = Math.round(clamp(rand(SVIS_SNOW_DATA.flake_min_size, SVIS_SNOW_DATA.flake_max_size), "flake_min_size")) * 1.5;
    116             this.img = getFlakeCanvas(this.size, SVIS_SNOW_DATA.flake_color, SVIS_SNOW_DATA.flake_type);
    117             if (firstSpawn && totalSpawning > 1) {
    118                 const sliceHeight = VERTICAL_AREA / totalSpawning;
    119                 const sliceStartY = spawnIndexY * sliceHeight;
    120                 this.y = rand(sliceStartY, sliceStartY + sliceHeight);
    121                 const sliceWidth = currentWidth / totalSpawning;
    122                 const sliceStartX = spawnIndexX * sliceWidth;
    123                 this.x = rand(sliceStartX, sliceStartX + sliceWidth);
    124             } else {
    125                 this.x = rand(0, currentWidth);
    126                 this.y = firstSpawn ? rand(0, VERTICAL_AREA) : -this.size * rand(1, 2);
    127             }
    128             this.verticalWrapLimit = -this.size * 2 * rand(1, 2);
    129             this.baseWindForce = clamp(rand(SVIS_SNOW_DATA.min_wind_speed, SVIS_SNOW_DATA.max_wind_speed), "min_wind_speed") * 1.5;
    130             this.gravityForce = clamp(rand(SVIS_SNOW_DATA.min_fall_speed, SVIS_SNOW_DATA.max_fall_speed), "min_fall_speed") * 1;
    131             this.vx = this.baseWindForce;
    132             this.vy = this.gravityForce;
    133             this.rotation = rand(0, TWO_PI);
    134             this.rotationSpeed = clamp(rand(SVIS_SNOW_DATA.min_rotation_speed, SVIS_SNOW_DATA.max_rotation_speed) * .1, "min_rotation_speed");
    135             const depthMin = SVIS_SNOW_DATA._field_meta?.flake_depth?.min ?? 0;
    136             const depthMax = SVIS_SNOW_DATA._field_meta?.flake_depth?.max ?? 10;
    137             let depthValue = clamp(parseFloat(SVIS_SNOW_DATA.flake_depth) || 0, "flake_depth");
    138             depthValue = Math.max(depthMin, Math.min(depthMax, depthValue));
    139             const depthNormalized = (depthValue - depthMin) / (depthMax - depthMin);
    140             const opacityMin = .1;
    141             const opacityMax = 1;
    142             const lowerLimit = opacityMin + (1 - depthNormalized) * (opacityMax - opacityMin);
    143             this.initialOpacity = rand(lowerLimit, opacityMax);
    144             this.opacity = this.initialOpacity;
    145             this.fadeTop = VERTICAL_AREA + this.size / 2 + rand(0, 0);
    146             this.fadeDistance = this.size / 2 + rand(0, this.size);
    147             this.fadeBottom = this.fadeTop + this.fadeDistance;
    148             this.turbulenceLevel = clamp(parseFloat(SVIS_SNOW_DATA.turbulence) || 0, "turbulence") * 4;
    149             if (this.turbulenceLevel > 0) {
    150                 this.turbulenceTimer = rand(0, 10);
    151                 this.turbulenceOffset = rand(0, TWO_PI);
    152                 this.turbulenceFreqX = rand(1.5, 3.5);
    153                 this.turbulenceFreqY = rand(1, 2.5);
    154                 this.nextTurbulenceChange = rand(1, 3);
    155                 this.currentGustX = 0;
    156                 this.targetGustX = 0;
    157                 this.currentGustY = 0;
    158                 this.targetGustY = 0;
    159             }
    160             if (globalGustIntensity > 0) {
    161                 this.currentGlobalGustX = 0;
    162                 this.currentGlobalGustY = 0;
    163                 const maxPossibleSize = SVIS_SNOW_DATA?._field_meta?.flake_max_size?.max || 100;
    164                 const normalizedSize = Math.min(1, this.size / (maxPossibleSize * 1.5));
    165                 this.globalGustSensitivity = rand(.8, 1.5) * (1 - Math.pow(normalizedSize, 1.5));
    166             }
    167         }
    168         update(delta) {
    169             let targetVx = this.baseWindForce;
    170             let targetVy = this.gravityForce;
    171             if (this.turbulenceLevel > 0) {
    172                 this.turbulenceTimer += delta;
    173                 if (this.turbulenceTimer > this.nextTurbulenceChange) {
    174                     this.targetGustX = this.turbulenceLevel / 10 * rand(-2, 2);
    175                     this.targetGustY = this.turbulenceLevel / 10 * rand(-1, 1);
    176                     const gustFrequency = Math.max(.5, 5 - this.turbulenceLevel / 10);
    177                     this.nextTurbulenceChange = this.turbulenceTimer + rand(gustFrequency * .8, gustFrequency * 1.5);
    178                 }
    179                 const lerpSpeed = 4 * delta;
    180                 this.currentGustX += (this.targetGustX - this.currentGustX) * lerpSpeed;
    181                 this.currentGustY += (this.targetGustY - this.currentGustY) * lerpSpeed;
    182                 const baseVariation = this.turbulenceLevel / 10 * 2.5;
    183                 const waveX = Math.sin(this.turbulenceTimer * this.turbulenceFreqX + this.turbulenceOffset) * baseVariation;
    184                 const waveY = Math.cos(this.turbulenceTimer * this.turbulenceFreqY + this.turbulenceOffset * .7) * baseVariation * .3;
    185                 targetVx += this.currentGustX + waveX;
    186                 targetVy += this.currentGustY + waveY;
    187             }
    188             if (globalGustIntensity > 0) {
    189                 const globalLerpSpeed = .5 * delta;
    190                 this.currentGlobalGustX += (globalGustX - this.currentGlobalGustX) * globalLerpSpeed;
    191                 this.currentGlobalGustY += (globalGustY - this.currentGlobalGustY) * globalLerpSpeed;
    192                 const globalInfluence = 4e4;
    193                 targetVx += this.currentGlobalGustX * globalInfluence * this.globalGustSensitivity * delta;
    194                 targetVy += this.currentGlobalGustY * globalInfluence * this.globalGustSensitivity * delta;
    195             }
    196             this.vx = targetVx;
    197             this.vy = targetVy;
    198             this.x += this.vx * delta * 6;
    199             this.y += this.vy * delta * 6;
    200             this.rotation += this.rotationSpeed * delta;
    201             const w = currentWidth;
    202             const h = VERTICAL_AREA;
    203             if (this.x < -this.size) {
    204                 this.x = w + this.size;
    205                 this.y = Math.random() * h;
    206             }
    207             if (this.x > w + this.size) {
    208                 this.x = -this.size;
    209                 this.y = Math.random() * h;
    210             }
    211             if (this.y < this.verticalWrapLimit) {
    212                 this.x = Math.random() * w;
    213                 this.y = this.fadeBottom - .1;
    214             }
    215             let t = (this.y - this.fadeTop) / (this.fadeBottom - this.fadeTop);
    216             t = Math.max(0, Math.min(1, t));
    217             this.opacity = (1 - t) * this.initialOpacity;
    218             this.isAlive = this.opacity > 0;
    219         }
    220         draw() {
    221             ctx.globalAlpha = this.opacity;
    222             if (this.rotation !== 0) {
    223                 const ridx = angleToIndex(this.rotation);
    224                 const cos = cosTable[ridx];
    225                 const sin = sinTable[ridx];
    226                 const d = deviceRatio;
    227                 ctx.setTransform(cos * d, sin * d, -sin * d, cos * d, this.x * d, this.y * d);
    228                 ctx.drawImage(this.img, -this.size, -this.size, this.size * 2, this.size * 2);
    229             } else {
    230                 const d = deviceRatio;
    231                 ctx.setTransform(d, 0, 0, d, (this.x - this.size) * d, (this.y - this.size) * d);
    232                 ctx.drawImage(this.img, 0, 0, this.size * 2, this.size * 2);
    233             }
    234             ctx.globalAlpha = 1;
    235         }
    236     }
    237     function resizeCanvas() {
    238         deviceRatio = window.devicePixelRatio || 1;
    239         const isScrollable = SVIS_SNOW_DATA.scroll_with_page === "1";
    240         currentWidth = document.documentElement.clientWidth;
    241         currentHeight = isScrollable ? document.documentElement.scrollHeight : document.documentElement.clientHeight;
    242         canvas.width = Math.round(currentWidth * deviceRatio);
    243         canvas.height = Math.round(currentHeight * deviceRatio);
    244         canvas.style.width = currentWidth + "px";
    245         canvas.style.height = currentHeight + "px";
    246         VERTICAL_AREA = parseVerticalArea(SVIS_SNOW_DATA.vertical_area);
    247         const screenArea = currentWidth * currentHeight;
    248         const performanceMultiplier = isMobile ? .7 : 1;
    249         const newMaxFlakes = Math.max(5, Math.round(screenArea / 4e5 * parseInt(SVIS_SNOW_DATA.flakes_density) * performanceMultiplier));
    250         if (newMaxFlakes > MAX_FLAKES) {
    251             const toAdd = newMaxFlakes - MAX_FLAKES;
    252             const isInitialSpawn = flakes.length === 0;
    253             const xIndices = Array.from({
    254                 length: toAdd
    255             }, (_, i) => i);
    256             if (isInitialSpawn) {
    257                 for (let i = xIndices.length - 1; i > 0; i--) {
    258                     const j = Math.floor(Math.random() * (i + 1));
    259                     [xIndices[i], xIndices[j]] = [ xIndices[j], xIndices[i] ];
    260                 }
    261             }
    262             for (let i = 0; i < toAdd; i++) {
    263                 flakes.push(new Snowflake(isInitialSpawn, i, xIndices[i], toAdd));
    264             }
    265         }
    266         if (newMaxFlakes < MAX_FLAKES) {
    267             const aliveFlakes = flakes.filter(f => f.isAlive);
    268             const toDeactivate = aliveFlakes.length - newMaxFlakes;
    269             if (toDeactivate > 0) {
    270                 aliveFlakes.sort((a, b) => b.y - a.y).slice(0, toDeactivate).forEach(f => {
    271                     f.isFadingOut = true;
    272                 });
    273             }
    274         }
    275         MAX_FLAKES = newMaxFlakes;
    276     }
    277     resizeCanvas();
    278     window.addEventListener("resize", resizeCanvas);
    279     document.body.appendChild(canvas);
    280     let frameCount = 0;
    281     let fpsHistory = [];
    282     let lastFpsCheck = performance.now();
    283     function updatePerformanceLevel(currentFps) {
    284         fpsHistory.push(currentFps);
    285         if (fpsHistory.length > 20) fpsHistory.shift();
    286         const avgFps = Math.min(60, Math.max(0, fpsHistory.reduce((a, b) => a + b) / fpsHistory.length));
    287         if (avgFps < 30) {
    288             const aliveFlakes = flakes.filter(f => f.isAlive).length;
    289             if (aliveFlakes > MAX_FLAKES * .3) {
    290                 let toDeactivate = Math.min(2, aliveFlakes - Math.round(MAX_FLAKES * .3));
    291                 const sortedFlakes = flakes.map((flake, index) => ({
    292                     flake: flake,
    293                     index: index
    294                 })).filter(item => item.flake.isAlive).sort((a, b) => a.flake.y - b.flake.y);
    295                 for (let i = 0; i < toDeactivate && i < sortedFlakes.length; i++) {
    296                     sortedFlakes[i].flake.isFadingOut = true;
    297                 }
    298             }
    299         }
    300     }
    301     let lastTime = performance.now();
    302     function animate(now) {
    303         let delta = (now - lastTime) / 1e3 || 0;
    304         delta = Math.min(delta, .05);
    305         lastTime = now;
    306         frameCount++;
    307         if (now - lastFpsCheck > 1e3) {
    308             const currentFps = frameCount;
    309             updatePerformanceLevel(currentFps);
    310             frameCount = 0;
    311             lastFpsCheck = now;
    312         }
    313         ctx.setTransform(deviceRatio, 0, 0, deviceRatio, 0, 0);
    314         ctx.clearRect(0, 0, currentWidth, currentHeight);
    315         updateGlobalGusts(delta);
    316         for (let i = flakes.length - 1; i >= 0; i--) {
    317             const fl = flakes[i];
    318             fl.update(delta);
    319             if (!fl.isAlive) {
    320                 flakes.splice(i, 1);
    321                 continue;
    322             }
    323             fl.draw();
    324         }
    325         const aliveCount = flakes.filter(f => f.isAlive).length;
    326         if (aliveCount < MAX_FLAKES) {
    327             const toAdd = MAX_FLAKES - aliveCount;
    328             for (let i = 0; i < toAdd; i++) {
    329                 flakes.push(new Snowflake);
    330             }
    331         }
    332         drawDebugInfo();
    333         requestAnimationFrame(animate);
    334     }
    335     let outOfBoundsCount = 0;
    336     function drawDebugInfo() {
    337         if (!DEBUG_MODE) return;
    338         const scrollYOffset = isScrollable ? window.scrollY : 0;
    339         const now = performance.now();
    340         let ms = now - startTime;
    341         const hours = Math.floor(ms / 36e5);
    342         ms %= 36e5;
    343         const minutes = Math.floor(ms / 6e4);
    344         ms %= 6e4;
    345         const seconds = Math.floor(ms / 1e3);
    346         const deci = Math.floor(ms % 1e3 / 100);
    347         const uptime = String(hours).padStart(2, "0") + ":" + String(minutes).padStart(2, "0") + ":" + String(seconds).padStart(2, "0") + "." + deci;
    348         const avgFps = fpsHistory.length > 0 ? Math.round(fpsHistory.reduce((a, b) => a + b) / fpsHistory.length) : 0;
    349         const totalFlakes = flakes.length;
    350         const aliveFlakes = flakes.filter(f => f.isAlive).length;
    351         const renderEfficiency = Math.round(aliveFlakes / totalFlakes * 100);
    352         const debugLines = [ `Uptime: ${uptime}`, ``, `FPS: ${avgFps}`, `Flakes: ${totalFlakes}/${MAX_FLAKES} (alive: ${aliveFlakes})`, `Render efficiency: ${renderEfficiency}%`, `Screen: ${currentWidth}x${currentHeight}`, `Canvas: ${canvas.width}x${canvas.height}`, `Device ratio: ${deviceRatio}`, `Vertical Area: ${VERTICAL_AREA}px`, `Mobile device: ${isMobile ? "Yes" : "No"}`, ``, `Out of bounds (SafeArea): ${outOfBoundsCount}` ];
    353         const padding = 40;
    354         const lineHeight = 18;
    355         const boxWidth = 300;
    356         const boxHeight = debugLines.length * lineHeight + 8;
    357         ctx.save();
    358         ctx.setTransform(1, 0, 0, 1, 0, 0);
    359         ctx.scale(deviceRatio, deviceRatio);
    360         ctx.globalAlpha = 1;
    361         ctx.fillStyle = "rgba(0, 0, 0, 0.8)";
    362         ctx.fillRect(padding / 2, padding + scrollYOffset, boxWidth, boxHeight);
    363         debugLines.forEach((line, i) => {
    364             if (line.startsWith("Out of bounds") && outOfBoundsCount > 0) {
    365                 ctx.fillStyle = "red";
    366                 ctx.font = "bold 14px monospace";
    367             } else {
    368                 ctx.fillStyle = "#00ff00";
    369                 ctx.font = "14px monospace";
    370             }
    371             ctx.fillText(line, padding, padding + lineHeight * (i + 1) + scrollYOffset);
    372         });
    373         ctx.strokeStyle = "yellow";
    374         ctx.lineWidth = 1;
    375         ctx.setLineDash([ 5, 5 ]);
    376         ctx.beginPath();
    377         ctx.moveTo(0, VERTICAL_AREA);
    378         ctx.lineTo(currentWidth, VERTICAL_AREA);
    379         ctx.stroke();
    380         ctx.fillStyle = "yellow";
    381         ctx.font = "12px monospace";
    382         ctx.fillText("VERTICAL_AREA", 5, VERTICAL_AREA - 5);
    383         ctx.restore();
    384     }
    385     function checkSafeArea(flakeArray, canvasWidth, canvasHeight) {
    386         const safe = {
    387             minX: -canvasWidth * .5,
    388             maxX: canvasWidth * 1.5,
    389             minY: -canvasHeight * .5,
    390             maxY: canvasHeight * 1.5
    391         };
    392         let removedCount = 0;
    393         for (let i = flakeArray.length - 1; i >= 0; i--) {
    394             const f = flakeArray[i];
    395             if (f.x < safe.minX || f.x > safe.maxX || f.y < safe.minY || f.y > safe.maxY) {
    396                 flakeArray.splice(i, 1);
    397                 removedCount++;
    398             }
    399         }
    400         outOfBoundsCount += removedCount;
    401     }
    402     setInterval(() => {
    403         checkSafeArea(flakes, currentWidth, VERTICAL_AREA);
    404     }, 1e4);
    405     requestAnimationFrame(animate);
    406417}
  • svisciano-snowfall-effect/trunk/assets/js/svis-snow.min.js

    r3436116 r3441664  
    1 if("1"===SVIS_SNOW_DATA.enabled){const t=!1,e=performance.now(),i=4096,s=2*Math.PI,n=1/s,a=new Float32Array(i),r=new Float32Array(i);for(let O=0;O<i;O++){const W=O/i*s;a[O]=Math.sin(W),r[O]=Math.cos(W)}let l="1"===SVIS_SNOW_DATA.scroll_with_page,o=document.documentElement.clientWidth,h=l?document.documentElement.scrollHeight:document.documentElement.clientHeight,c=window.devicePixelRatio||1;const u=navigator.hardwareConcurrency<=2||/Mobile|Android/i.test(navigator.userAgent),d=document.createElement("canvas"),f=d.getContext("2d",{alpha:!0});d.style.position=l?"absolute":"fixed",d.style.top="0",d.style.left="0",d.style.pointerEvents="none",d.style.zIndex=SVIS_SNOW_DATA.z_index||9999;let m=0,_=0;const S=[];function parseVerticalArea(t){const e=h;if(!t)return e;if((t=t.trim()).endsWith("%")){let i=parseFloat(t.replace("%",""));return isNaN(i)?e:(i=Math.min(Math.max(i,0),100),e*(i/100))}if(t.endsWith("px")){let i=parseFloat(t.replace("px",""));return isNaN(i)?e:i}let i=parseFloat(t);return isNaN(i)?e:i}const p=Object.create(null);function getFlakeCanvas(t,e,i){const s=`${t}_${e}_${i}`;if(p[s])return p[s];const n=document.createElement("canvas"),a=Math.ceil(2*t);n.width=a,n.height=a;const r=n.getContext("2d");return r.fillStyle=e,r.textAlign="center",r.textBaseline="middle",r.font=`${t}px serif`,r.fillText(i,a/2,a/2),p[s]=n,n}function rand(t,e){return t=parseFloat(t),e=parseFloat(e),isNaN(t)||isNaN(e)?0:Math.random()*(e-t)+t}const g=(t,e)=>{const i=SVIS_SNOW_DATA._field_meta?.[e];if(!i)return t;let s=t;return null!==i.min&&(s=Math.max(i.min,s)),null!==i.max&&(s=Math.min(i.max,s)),s};function angleToIndex(t){let e=Math.floor(t%s*n*i);return e<0&&(e+=i),e}let A=g(parseFloat(SVIS_SNOW_DATA.global_gust_strength)||0,"global_gust_strength"),b=0,x=0,y=0,v=rand(2,5);function updateGlobalGusts(t){if(A=g(parseFloat(SVIS_SNOW_DATA.global_gust_strength)||0,"global_gust_strength"),A<=0)return b=0,x=0,void(y=0);if(y+=t,y>v){b=A/10*rand(-3,3),x=A/10*rand(-1.5,1.5);const t=15,e=t-A/10*(t-4);v=y+3*rand(.8*e,1.2*e)}b*=.993,x*=.993}class T{isAlive=!1;constructor(t=!1,e=0,i=0,s=1){this.spawn(t,e,i,s)}spawn(t=!1,e=0,i=0,n=1){if(this.size=1.5*Math.round(g(rand(SVIS_SNOW_DATA.flake_min_size,SVIS_SNOW_DATA.flake_max_size),"flake_min_size")),this.img=getFlakeCanvas(this.size,SVIS_SNOW_DATA.flake_color,SVIS_SNOW_DATA.flake_type),t&&n>1){const t=m/n,s=e*t;this.y=rand(s,s+t);const a=o/n,r=i*a;this.x=rand(r,r+a)}else this.x=rand(0,o),this.y=t?rand(0,m):-this.size*rand(1,2);this.verticalWrapLimit=2*-this.size*rand(1,2),this.baseWindForce=1.5*g(rand(SVIS_SNOW_DATA.min_wind_speed,SVIS_SNOW_DATA.max_wind_speed),"min_wind_speed"),this.gravityForce=1*g(rand(SVIS_SNOW_DATA.min_fall_speed,SVIS_SNOW_DATA.max_fall_speed),"min_fall_speed"),this.vx=this.baseWindForce,this.vy=this.gravityForce,this.rotation=rand(0,s),this.rotationSpeed=g(.1*rand(SVIS_SNOW_DATA.min_rotation_speed,SVIS_SNOW_DATA.max_rotation_speed),"min_rotation_speed");const a=SVIS_SNOW_DATA._field_meta?.flake_depth?.min??0,r=SVIS_SNOW_DATA._field_meta?.flake_depth?.max??10;let l=g(parseFloat(SVIS_SNOW_DATA.flake_depth)||0,"flake_depth");l=Math.max(a,Math.min(r,l));const h=.1+.9*(1-(l-a)/(r-a));if(this.initialOpacity=rand(h,1),this.opacity=this.initialOpacity,this.fadeTop=m+this.size/2+rand(0,0),this.fadeDistance=this.size/2+rand(0,this.size),this.fadeBottom=this.fadeTop+this.fadeDistance,this.turbulenceLevel=4*g(parseFloat(SVIS_SNOW_DATA.turbulence)||0,"turbulence"),this.turbulenceLevel>0&&(this.turbulenceTimer=rand(0,10),this.turbulenceOffset=rand(0,s),this.turbulenceFreqX=rand(1.5,3.5),this.turbulenceFreqY=rand(1,2.5),this.nextTurbulenceChange=rand(1,3),this.currentGustX=0,this.targetGustX=0,this.currentGustY=0,this.targetGustY=0),A>0){this.currentGlobalGustX=0,this.currentGlobalGustY=0;const t=SVIS_SNOW_DATA?._field_meta?.flake_max_size?.max||100,e=Math.min(1,this.size/(1.5*t));this.globalGustSensitivity=rand(.8,1.5)*(1-Math.pow(e,1.5))}}update(t){let e=this.baseWindForce,i=this.gravityForce;if(this.turbulenceLevel>0){if(this.turbulenceTimer+=t,this.turbulenceTimer>this.nextTurbulenceChange){this.targetGustX=this.turbulenceLevel/10*rand(-2,2),this.targetGustY=this.turbulenceLevel/10*rand(-1,1);const t=Math.max(.5,5-this.turbulenceLevel/10);this.nextTurbulenceChange=this.turbulenceTimer+rand(.8*t,1.5*t)}const s=4*t;this.currentGustX+=(this.targetGustX-this.currentGustX)*s,this.currentGustY+=(this.targetGustY-this.currentGustY)*s;const n=this.turbulenceLevel/10*2.5,a=Math.sin(this.turbulenceTimer*this.turbulenceFreqX+this.turbulenceOffset)*n,r=Math.cos(this.turbulenceTimer*this.turbulenceFreqY+.7*this.turbulenceOffset)*n*.3;e+=this.currentGustX+a,i+=this.currentGustY+r}if(A>0){const s=.5*t;this.currentGlobalGustX+=(b-this.currentGlobalGustX)*s,this.currentGlobalGustY+=(x-this.currentGlobalGustY)*s;const n=4e4;e+=this.currentGlobalGustX*n*this.globalGustSensitivity*t,i+=this.currentGlobalGustY*n*this.globalGustSensitivity*t}this.vx=e,this.vy=i,this.x+=this.vx*t*6,this.y+=this.vy*t*6,this.rotation+=this.rotationSpeed*t;const s=o,n=m;this.x<-this.size&&(this.x=s+this.size,this.y=Math.random()*n),this.x>s+this.size&&(this.x=-this.size,this.y=Math.random()*n),this.y<this.verticalWrapLimit&&(this.x=Math.random()*s,this.y=this.fadeBottom-.1);let a=(this.y-this.fadeTop)/(this.fadeBottom-this.fadeTop);a=Math.max(0,Math.min(1,a)),this.opacity=(1-a)*this.initialOpacity,this.isAlive=this.opacity>0}draw(){if(f.globalAlpha=this.opacity,0!==this.rotation){const t=angleToIndex(this.rotation),e=r[t],i=a[t],s=c;f.setTransform(e*s,i*s,-i*s,e*s,this.x*s,this.y*s),f.drawImage(this.img,-this.size,-this.size,2*this.size,2*this.size)}else{const t=c;f.setTransform(t,0,0,t,(this.x-this.size)*t,(this.y-this.size)*t),f.drawImage(this.img,0,0,2*this.size,2*this.size)}f.globalAlpha=1}}function resizeCanvas(){c=window.devicePixelRatio||1;const t="1"===SVIS_SNOW_DATA.scroll_with_page;o=document.documentElement.clientWidth,h=t?document.documentElement.scrollHeight:document.documentElement.clientHeight,d.width=Math.round(o*c),d.height=Math.round(h*c),d.style.width=o+"px",d.style.height=h+"px",m=parseVerticalArea(SVIS_SNOW_DATA.vertical_area);const e=o*h,i=u?.7:1,s=Math.max(5,Math.round(e/4e5*parseInt(SVIS_SNOW_DATA.flakes_density)*i));if(s>_){const t=s-_,e=0===S.length,i=Array.from({length:t},(t,e)=>e);if(e)for(let t=i.length-1;t>0;t--){const e=Math.floor(Math.random()*(t+1));[i[t],i[e]]=[i[e],i[t]]}for(let s=0;s<t;s++)S.push(new T(e,s,i[s],t))}if(s<_){const t=S.filter(t=>t.isAlive),e=t.length-s;e>0&&t.sort((t,e)=>e.y-t.y).slice(0,e).forEach(t=>{t.isFadingOut=!0})}_=s}resizeCanvas(),window.addEventListener("resize",resizeCanvas),document.body.appendChild(d);let M=0,w=[],G=performance.now();function updatePerformanceLevel(t){w.push(t),w.length>20&&w.shift();if(Math.min(60,Math.max(0,w.reduce((t,e)=>t+e)/w.length))<30){const t=S.filter(t=>t.isAlive).length;if(t>.3*_){let e=Math.min(2,t-Math.round(.3*_));const i=S.map((t,e)=>({flake:t,index:e})).filter(t=>t.flake.isAlive).sort((t,e)=>t.flake.y-e.flake.y);for(let t=0;t<e&&t<i.length;t++)i[t].flake.isFadingOut=!0}}}let I=performance.now();function animate(t){let e=(t-I)/1e3||0;if(e=Math.min(e,.05),I=t,M++,t-G>1e3){updatePerformanceLevel(M),M=0,G=t}f.setTransform(c,0,0,c,0,0),f.clearRect(0,0,o,h),updateGlobalGusts(e);for(let t=S.length-1;t>=0;t--){const i=S[t];i.update(e),i.isAlive?i.draw():S.splice(t,1)}const i=S.filter(t=>t.isAlive).length;if(i<_){const t=_-i;for(let e=0;e<t;e++)S.push(new T)}drawDebugInfo(),requestAnimationFrame(animate)}let N=0;function drawDebugInfo(){if(!t)return;const i=l?window.scrollY:0;let s=performance.now()-e;const n=Math.floor(s/36e5);s%=36e5;const a=Math.floor(s/6e4);s%=6e4;const r=Math.floor(s/1e3),p=Math.floor(s%1e3/100),g=String(n).padStart(2,"0")+":"+String(a).padStart(2,"0")+":"+String(r).padStart(2,"0")+"."+p,A=w.length>0?Math.round(w.reduce((t,e)=>t+e)/w.length):0,b=S.length,x=S.filter(t=>t.isAlive).length,y=Math.round(x/b*100),v=[`Uptime: ${g}`,"",`FPS: ${A}`,`Flakes: ${b}/${_} (alive: ${x})`,`Render efficiency: ${y}%`,`Screen: ${o}x${h}`,`Canvas: ${d.width}x${d.height}`,`Device ratio: ${c}`,`Vertical Area: ${m}px`,"Mobile device: "+(u?"Yes":"No"),"",`Out of bounds (SafeArea): ${N}`],T=18*v.length+8;f.save(),f.setTransform(1,0,0,1,0,0),f.scale(c,c),f.globalAlpha=1,f.fillStyle="rgba(0, 0, 0, 0.8)",f.fillRect(20,40+i,300,T),v.forEach((t,e)=>{t.startsWith("Out of bounds")&&N>0?(f.fillStyle="red",f.font="bold 14px monospace"):(f.fillStyle="#00ff00",f.font="14px monospace"),f.fillText(t,40,40+18*(e+1)+i)}),f.strokeStyle="yellow",f.lineWidth=1,f.setLineDash([5,5]),f.beginPath(),f.moveTo(0,m),f.lineTo(o,m),f.stroke(),f.fillStyle="yellow",f.font="12px monospace",f.fillText("VERTICAL_AREA",5,m-5),f.restore()}function checkSafeArea(t,e,i){const s=.5*-e,n=1.5*e,a=.5*-i,r=1.5*i;let l=0;for(let e=t.length-1;e>=0;e--){const i=t[e];(i.x<s||i.x>n||i.y<a||i.y>r)&&(t.splice(e,1),l++)}N+=l}setInterval(()=>{checkSafeArea(S,o,m)},1e4),requestAnimationFrame(animate)}
     1"1"===SVIS_SNOW_DATA.enabled&&document.addEventListener("DOMContentLoaded",function(){performance.now();const t=4096,e=2*Math.PI,i=1/e,s=new Float32Array(t),n=new Float32Array(t);for(let i=0;i<t;i++){const a=i/t*e;s[i]=Math.sin(a),n[i]=Math.cos(a)}let a="1"===SVIS_SNOW_DATA.scroll_with_page,l=document.documentElement.clientWidth,h=a?document.documentElement.scrollHeight:document.documentElement.clientHeight,r=window.devicePixelRatio||1;const o=navigator.hardwareConcurrency<=2||/Mobile|Android/i.test(navigator.userAgent),c=document.createElement("canvas"),u=c.getContext("2d",{alpha:!0});c.style.position=a?"absolute":"fixed",c.style.top="0",c.style.left="0",c.style.pointerEvents="none",c.style.zIndex=SVIS_SNOW_DATA.z_index||9999;let d=0,_=0;const m=[];const f=o?1:1.2,S=Object.create(null);function p(t,e){return t=parseFloat(t),e=parseFloat(e),isNaN(t)||isNaN(e)?0:Math.random()*(e-t)+t}const A=(t,e)=>{const i=SVIS_SNOW_DATA._field_meta?.[e];if(!i)return t;let s=t;return null!==i.min&&(s=Math.max(i.min,s)),null!==i.max&&(s=Math.min(i.max,s)),s};let g=A(parseFloat(SVIS_SNOW_DATA.global_gust_strength)||0,"global_gust_strength"),x=0,b=0,y=0,v=p(2,5);class T{isAlive=!1;constructor(t=!1,e=0,i=0,s=1){this.spawn(t,e,i,s)}spawn(t=!1,i=0,s=0,n=1){if(this.size=1.5*Math.round(A(p(SVIS_SNOW_DATA.flake_min_size,SVIS_SNOW_DATA.flake_max_size),"flake_min_size")),this.img=function(t,e,i){const s=(window.devicePixelRatio||1)*f,n=`${t}_${e}_${i}_${s}`;if(S[n])return S[n];const a=document.createElement("canvas"),l=Math.ceil(2*t);a.width=l*s,a.height=l*s,a.style.width=l+"px",a.style.height=l+"px";const h=a.getContext("2d");return h.scale(s,s),h.fillStyle=e,h.textAlign="center",h.textBaseline="middle",h.font=`${t}px serif`,h.fillText(i,l/2,l/2),S[n]=a,a}(this.size,SVIS_SNOW_DATA.flake_color,SVIS_SNOW_DATA.flake_type),t&&n>1){const t=d/n,e=i*t;this.y=p(e,e+t);const a=l/n,h=s*a;this.x=p(h,h+a)}else this.x=p(0,l),this.y=t?p(0,d):-this.size*p(1,2);this.verticalWrapLimit=2*-this.size*p(1,2),this.baseWindForce=.15*A(p(SVIS_SNOW_DATA.min_wind_speed,SVIS_SNOW_DATA.max_wind_speed),"min_wind_speed"),this.gravityForce=.1*A(p(SVIS_SNOW_DATA.min_fall_speed,SVIS_SNOW_DATA.max_fall_speed),"min_fall_speed"),this.vx=this.baseWindForce,this.vy=this.gravityForce,this.rotation=p(0,e),this.rotationSpeed=A(.3*p(SVIS_SNOW_DATA.min_rotation_speed,SVIS_SNOW_DATA.max_rotation_speed),"min_rotation_speed");const a=SVIS_SNOW_DATA._field_meta?.flake_depth?.min??0,h=SVIS_SNOW_DATA._field_meta?.flake_depth?.max??10;let r=A(parseFloat(SVIS_SNOW_DATA.flake_depth)||0,"flake_depth");r=Math.max(a,Math.min(h,r));const o=.1+.9*(1-(r-a)/(h-a));if(this.initialOpacity=p(o,1),this.opacity=this.initialOpacity,this.fadeTop=d+this.size/2+p(0,0),this.fadeDistance=this.size/2+p(0,this.size),this.fadeBottom=this.fadeTop+this.fadeDistance,this.turbulenceLevel=.4*A(parseFloat(SVIS_SNOW_DATA.turbulence)||0,"turbulence"),this.turbulenceLevel>0&&(this.turbulenceTimer=p(0,10),this.turbulenceOffset=p(0,e),this.turbulenceFreqX=p(1.5,3.5),this.turbulenceFreqY=p(1,2.5),this.nextTurbulenceChange=p(1,3),this.currentGustX=0,this.targetGustX=0,this.currentGustY=0,this.targetGustY=0),g>0){this.currentGlobalGustX=0,this.currentGlobalGustY=0;const t=SVIS_SNOW_DATA?._field_meta?.flake_max_size?.max||100,e=Math.min(1,this.size/(1.5*t));this.globalGustSensitivity=p(.8,1.5)*(1-Math.pow(e,1.5))}}update(t){let e=this.baseWindForce,i=this.gravityForce;if(this.turbulenceLevel>0){if(this.turbulenceTimer+=t,this.turbulenceTimer>this.nextTurbulenceChange){this.targetGustX=this.turbulenceLevel/10*p(-2,2),this.targetGustY=this.turbulenceLevel/10*p(-1,1);const t=Math.max(.5,5-this.turbulenceLevel/10);this.nextTurbulenceChange=this.turbulenceTimer+p(.8*t,1.5*t)}const s=4*t;this.currentGustX+=(this.targetGustX-this.currentGustX)*s,this.currentGustY+=(this.targetGustY-this.currentGustY)*s;const n=this.turbulenceLevel/10*2.5,a=Math.sin(this.turbulenceTimer*this.turbulenceFreqX+this.turbulenceOffset)*n,l=Math.cos(this.turbulenceTimer*this.turbulenceFreqY+.7*this.turbulenceOffset)*n*.3;e+=this.currentGustX+a,i+=this.currentGustY+l}if(g>0){const s=.5*t;this.currentGlobalGustX+=(x-this.currentGlobalGustX)*s,this.currentGlobalGustY+=(b-this.currentGlobalGustY)*s;const n=4e4;e+=this.currentGlobalGustX*n*this.globalGustSensitivity*t,i+=this.currentGlobalGustY*n*this.globalGustSensitivity*t}this.vx=e,this.vy=i;const s=1/60,n=Math.min(3,t/s);this.x+=this.vx*n,this.y+=this.vy*n,this.rotation+=this.rotationSpeed*t;const a=l,h=d;this.x<-this.size&&(this.x=a+this.size,this.y=Math.random()*h),this.x>a+this.size&&(this.x=-this.size,this.y=Math.random()*h),this.y<this.verticalWrapLimit&&(this.x=Math.random()*a,this.y=this.fadeBottom-.1);let r=(this.y-this.fadeTop)/(this.fadeBottom-this.fadeTop);r=Math.max(0,Math.min(1,r)),this.opacity=(1-r)*this.initialOpacity,this.isAlive=this.opacity>0}draw(){if(u.globalAlpha=this.opacity,0!==this.rotation&&this.size>8){const a=function(s){let n=Math.floor(s%e*i*t);return n<0&&(n+=t),n}(this.rotation),l=n[a],h=s[a],o=r;u.setTransform(l*o,h*o,-h*o,l*o,this.x*o,this.y*o),u.drawImage(this.img,-this.size,-this.size,2*this.size,2*this.size)}else{const t=r;u.setTransform(t,0,0,t,(this.x-this.size)*t,(this.y-this.size)*t),u.drawImage(this.img,0,0,2*this.size,2*this.size)}u.globalAlpha=1}}function M(){r=window.devicePixelRatio||1;const t="1"===SVIS_SNOW_DATA.scroll_with_page;l=document.documentElement.clientWidth,h=t?document.documentElement.scrollHeight:document.documentElement.clientHeight,c.width=Math.round(l*r),c.height=Math.round(h*r),c.style.width=l+"px",c.style.height=h+"px",d=function(t){const e=h;if(!t)return e;if((t=t.trim()).endsWith("%")){let i=parseFloat(t.replace("%",""));return isNaN(i)?e:(i=Math.min(Math.max(i,0),100),e*(i/100))}if(t.endsWith("px")){let i=parseFloat(t.replace("px",""));return isNaN(i)?e:i}let i=parseFloat(t);return isNaN(i)?e:i}(SVIS_SNOW_DATA.vertical_area);const e=l*h,i=o?.7:1,s=Math.max(5,Math.round(e/4e5*parseInt(SVIS_SNOW_DATA.flakes_density)*i));if(s>_){const t=s-_,e=0===m.length,i=Array.from({length:t},(t,e)=>e);if(e)for(let t=i.length-1;t>0;t--){const e=Math.floor(Math.random()*(t+1));[i[t],i[e]]=[i[e],i[t]]}for(let s=0;s<t;s++)m.push(new T(e,s,i[s],t))}if(s<_){const t=m.filter(t=>t.isAlive),e=t.length-s;e>0&&t.sort((t,e)=>e.y-t.y).slice(0,e).forEach(t=>{t.isFadingOut=!0})}_=s}M(),window.addEventListener("resize",M),document.body.appendChild(c);let N=0,O=[],G=performance.now();let W=performance.now();let w=0;setInterval(()=>{!function(t,e,i){const s=.5*-e,n=1.5*e,a=.5*-i,l=1.5*i;let h=0;for(let e=t.length-1;e>=0;e--){const i=t[e];(i.x<s||i.x>n||i.y<a||i.y>l)&&(t.splice(e,1),h++)}w+=h}(m,l,d)},1e4),requestAnimationFrame(function t(e){let i=(e-W)/1e3||0;if(i=Math.min(i,.05),W=e,N++,e-G>1e3){!function(t){if(O.push(t),O.length>20&&O.shift(),Math.min(60,Math.max(0,O.reduce((t,e)=>t+e)/O.length))<30){const t=m.filter(t=>t.isAlive).length;if(t>.3*_){let e=Math.min(2,t-Math.round(.3*_));const i=m.map((t,e)=>({flake:t,index:e})).filter(t=>t.flake.isAlive).sort((t,e)=>t.flake.y-e.flake.y);for(let t=0;t<e&&t<i.length;t++)i[t].flake.isFadingOut=!0}}}(N),N=0,G=e}u.setTransform(r,0,0,r,0,0),u.clearRect(0,0,l,h),function(t){if(g=.2*A(parseFloat(SVIS_SNOW_DATA.global_gust_strength)||0,"global_gust_strength"),g<=0)return x=0,b=0,void(y=0);if(y+=t,y>v){x=g/10*p(-3,3),b=g/10*p(-1.5,1.5);const t=15,e=t-g/10*(t-4);v=y+3*p(.8*e,1.2*e)}x*=.993,b*=.993}(i);for(let t=m.length-1;t>=0;t--){const e=m[t];e.update(i),e.isAlive?e.draw():m.splice(t,1)}const s=m.filter(t=>t.isAlive).length;if(s<_){const t=_-s;for(let e=0;e<t;e++)m.push(new T)}requestAnimationFrame(t)})});
  • svisciano-snowfall-effect/trunk/config/settings-presets.php

    r3436116 r3441664  
    1717        'min_wind_speed' => -1,
    1818        'max_wind_speed' => 1,
    19         'min_rotation_speed' => -3,
    20         'max_rotation_speed' => 3,
     19        'min_rotation_speed' => -1,
     20        'max_rotation_speed' => 1,
    2121        'flakes_density' => 1,
    2222        'flake_depth' => 1,
     
    3333        'min_wind_speed' => 0,
    3434        'max_wind_speed' => 2,
    35         'min_rotation_speed' => -5,
    36         'max_rotation_speed' => 5,
     35        'min_rotation_speed' => -2,
     36        'max_rotation_speed' => 2,
    3737        'flakes_density' => 2,
    3838        'flake_depth' => 3,
     
    4949        'min_wind_speed' => 2,
    5050        'max_wind_speed' => 4,
    51         'min_rotation_speed' => -5,
    52         'max_rotation_speed' => 5,
     51        'min_rotation_speed' => -3,
     52        'max_rotation_speed' => 3,
    5353        'flakes_density' => 3,
    5454        'flake_depth' => 3,
  • svisciano-snowfall-effect/trunk/readme.txt

    r3436116 r3441664  
    11=== SVisciano - Snowfall Effect ===
    2 Contributors: SVisciano
     2Contributors: svisciano
    33Donate link: https://buymeacoffee.com/svisciano
    44Tags: snow, animation, winter, effect, christmas
     
    66Tested up to: 6.9
    77Requires PHP: 7.0
    8 Stable tag: 0.1.0
     8Stable tag: 0.1.1
    99License: GPLv2 or later
    1010License URI: https://www.gnu.org/licenses/gpl-2.0.html
     
    47472. Heavy snowfall effect with different settings.
    48483. Storm snowfall preset with increased density, gusts, and visual impact.
    49 4. Snow effect correctly layered using z-index, staying behind headers and Vertical Snow Area set to 30% .
     494. Snow effect correctly layered using z-index, staying behind headers and Vertical Snow Area set to 30%.
    50505. Plugin settings page inside the WordPress admin panel.
    5151
    52 == Development ==
    53 This plugin uses a simple Node.js build process to generate minified assets.
    54 Original (non-minified) JavaScript and CSS source files are included in the same path of minified files.
    55 Minified files (.min.js / .min.css) are generated using:
    56 - terser
    57 - postcss + cssnano
     52== Changelog ==
     53= 0.1.1 =
     54* Enhanced stability and performance optimizations
     55* Minor internal improvements and bug fixes
    5856
    59 == Changelog ==
    6057= 0.1.0 =
    6158* Initial release
    6259
    6360== Upgrade Notice ==
     61= 0.1.1 =
     62This update enhances stability and performance optimizations, with minor internal improvements and bug fixes.
     63
    6464= 0.1.0 =
    6565Initial release.
     66
  • svisciano-snowfall-effect/trunk/svisciano-snowfall-effect.php

    r3436116 r3441664  
    44 * Plugin Name: SVisciano - Snowfall Effect
    55 * Description: Add a festive touch to your WordPress site with realistic falling snow animation.
    6  * Version: 0.1.0
     6 * Version: 0.1.1
    77 * Author: Simone Visciano
    88 * Author URI: https://www.svisciano.it
Note: See TracChangeset for help on using the changeset viewer.