Changeset 3441664
- Timestamp:
- 01/17/2026 06:24:54 PM (5 weeks ago)
- Location:
- svisciano-snowfall-effect
- Files:
-
- 29 added
- 5 edited
-
tags/0.1.1 (added)
-
tags/0.1.1/admin (added)
-
tags/0.1.1/admin/index.php (added)
-
tags/0.1.1/admin/settings-page.php (added)
-
tags/0.1.1/assets (added)
-
tags/0.1.1/assets/css (added)
-
tags/0.1.1/assets/css/index.php (added)
-
tags/0.1.1/assets/css/settings-page.css (added)
-
tags/0.1.1/assets/css/settings-page.min.css (added)
-
tags/0.1.1/assets/index.php (added)
-
tags/0.1.1/assets/js (added)
-
tags/0.1.1/assets/js/color-picker.js (added)
-
tags/0.1.1/assets/js/color-picker.min.js (added)
-
tags/0.1.1/assets/js/index.php (added)
-
tags/0.1.1/assets/js/settings-page.js (added)
-
tags/0.1.1/assets/js/settings-page.min.js (added)
-
tags/0.1.1/assets/js/svis-snow.js (added)
-
tags/0.1.1/assets/js/svis-snow.min.js (added)
-
tags/0.1.1/config (added)
-
tags/0.1.1/config/index.php (added)
-
tags/0.1.1/config/settings-fields.php (added)
-
tags/0.1.1/config/settings-presets.php (added)
-
tags/0.1.1/includes (added)
-
tags/0.1.1/includes/index.php (added)
-
tags/0.1.1/includes/svis-framework.php (added)
-
tags/0.1.1/languages (added)
-
tags/0.1.1/languages/index.php (added)
-
tags/0.1.1/readme.txt (added)
-
tags/0.1.1/svisciano-snowfall-effect.php (added)
-
trunk/assets/js/svis-snow.js (modified) (1 diff)
-
trunk/assets/js/svis-snow.min.js (modified) (1 diff)
-
trunk/config/settings-presets.php (modified) (3 diffs)
-
trunk/readme.txt (modified) (3 diffs)
-
trunk/svisciano-snowfall-effect.php (modified) (1 diff)
Legend:
- Unmodified
- Added
- Removed
-
svisciano-snowfall-effect/trunk/assets/js/svis-snow.js
r3436116 r3441664 1 1 if (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); 22 416 }); 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: toAdd255 }, (_, 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: index294 })).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.5391 };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);406 417 } -
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 17 17 'min_wind_speed' => -1, 18 18 'max_wind_speed' => 1, 19 'min_rotation_speed' => - 3,20 'max_rotation_speed' => 3,19 'min_rotation_speed' => -1, 20 'max_rotation_speed' => 1, 21 21 'flakes_density' => 1, 22 22 'flake_depth' => 1, … … 33 33 'min_wind_speed' => 0, 34 34 'max_wind_speed' => 2, 35 'min_rotation_speed' => - 5,36 'max_rotation_speed' => 5,35 'min_rotation_speed' => -2, 36 'max_rotation_speed' => 2, 37 37 'flakes_density' => 2, 38 38 'flake_depth' => 3, … … 49 49 'min_wind_speed' => 2, 50 50 'max_wind_speed' => 4, 51 'min_rotation_speed' => - 5,52 'max_rotation_speed' => 5,51 'min_rotation_speed' => -3, 52 'max_rotation_speed' => 3, 53 53 'flakes_density' => 3, 54 54 'flake_depth' => 3, -
svisciano-snowfall-effect/trunk/readme.txt
r3436116 r3441664 1 1 === SVisciano - Snowfall Effect === 2 Contributors: SVisciano2 Contributors: svisciano 3 3 Donate link: https://buymeacoffee.com/svisciano 4 4 Tags: snow, animation, winter, effect, christmas … … 6 6 Tested up to: 6.9 7 7 Requires PHP: 7.0 8 Stable tag: 0.1. 08 Stable tag: 0.1.1 9 9 License: GPLv2 or later 10 10 License URI: https://www.gnu.org/licenses/gpl-2.0.html … … 47 47 2. Heavy snowfall effect with different settings. 48 48 3. 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% .49 4. Snow effect correctly layered using z-index, staying behind headers and Vertical Snow Area set to 30%. 50 50 5. Plugin settings page inside the WordPress admin panel. 51 51 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 58 56 59 == Changelog ==60 57 = 0.1.0 = 61 58 * Initial release 62 59 63 60 == Upgrade Notice == 61 = 0.1.1 = 62 This update enhances stability and performance optimizations, with minor internal improvements and bug fixes. 63 64 64 = 0.1.0 = 65 65 Initial release. 66 -
svisciano-snowfall-effect/trunk/svisciano-snowfall-effect.php
r3436116 r3441664 4 4 * Plugin Name: SVisciano - Snowfall Effect 5 5 * Description: Add a festive touch to your WordPress site with realistic falling snow animation. 6 * Version: 0.1. 06 * Version: 0.1.1 7 7 * Author: Simone Visciano 8 8 * Author URI: https://www.svisciano.it
Note: See TracChangeset
for help on using the changeset viewer.