Christmas Snowfall Effect
I decided to add some snowfall to my sites, and figured a light JS and some CSS would help with that, there is probably a better way to do this, but here is what I came up with.
Javascript
/* ---- Optimized Seasonal Snow Effect (Canvas) ---- */
(function() {
// 1. Seasonal & Motion Checks
const now = new Date();
if (now.getMonth() !== 11) return;
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return;
// 2. Configuration
const MAX_FLAKES = 100; // Reduced slightly for safety, usually handles 500+ easily
const SNOWFLAKES = ['\u2744', '\u2745', '\u2746']; // Unicode flakes
// 3. Setup Canvas
const canvas = document.createElement('canvas');
canvas.style.position = 'fixed';
canvas.style.top = '0';
canvas.style.left = '0';
canvas.style.width = '100%';
canvas.style.height = '100%';
canvas.style.pointerEvents = 'none'; // CRITICAL: Lets clicks pass through to site
canvas.style.zIndex = '9999';
canvas.id = 'skynet2-snow-canvas';
document.body.appendChild(canvas);
const ctx = canvas.getContext('2d');
let width = window.innerWidth;
let height = window.innerHeight;
// Handle Resize
function resize() {
width = window.innerWidth;
height = window.innerHeight;
canvas.width = width;
canvas.height = height;
}
window.addEventListener('resize', resize);
resize(); // Init size
// 4. Particle System
const flakes = [];
function createFlake() {
return {
x: Math.random() * width,
y: Math.random() * height - height, // Start above screen
r: 10 + Math.random() * 15, // Size
d: Math.random() * MAX_FLAKES, // Density/Seed for sway
s: 1 + Math.random() * 2, // Fall speed
txt: SNOWFLAKES[Math.floor(Math.random() * SNOWFLAKES.length)]
};
}
// Populate initial array
for (let i = 0; i < MAX_FLAKES; i++) {
flakes.push(createFlake());
}
// 5. The Render Loop
let animationFrameId;
function draw() {
ctx.clearRect(0, 0, width, height); // Clear previous frame
// Style
ctx.fillStyle = "rgba(255, 255, 255, 0.8)";
ctx.font = "20px sans-serif"; // Base font size
for (let i = 0; i < MAX_FLAKES; i++) {
const f = flakes[i];
// Update positions
f.y += f.s;
f.x += Math.sin(f.d) * 0.5; // Sway math
f.d += 0.01; // Increment sway angle
// Draw
ctx.font = `${f.r}px sans-serif`;
ctx.fillText(f.txt, f.x, f.y);
// Loop: If it falls off the bottom, send to top
if (f.y > height) {
flakes[i] = createFlake();
flakes[i].y = -20; // Reset to just above top
}
}
animationFrameId = requestAnimationFrame(draw);
}
// Start
draw();
})();
CSS
/* === SKYNET2 winter snow effect ====================== */
#skynet2-snow {
position: fixed;
inset: 0;
pointer-events: none;
overflow: hidden;
z-index: 9999;
}
.skynet2-snowflake {
position: absolute;
top: -2rem;
color: rgba(255, 255, 255, 0.9);
font-size: 0.8rem;
user-select: none;
opacity: 0;
/* Names must match the JS animation string */
animation-name: skynet2-snow-fall, skynet2-snow-sway;
animation-timing-function: linear, ease-in-out;
animation-iteration-count: 1, infinite;
}
/* Vertical Fall (GPU accelerated) */
@keyframes skynet2-snow-fall {
0% {
transform: translate3d(0, -10vh, 0);
opacity: 0;
}
10% {
opacity: 1;
}
100% {
transform: translate3d(0, 110vh, 0);
opacity: 0;
}
}
/* Horizontal Sway (Margin-based to avoid conflict) */
/* Logic: Center -> Right -> Left -> Center */
@keyframes skynet2-snow-sway {
0% {
margin-left: 0;
}
25% {
margin-left: 20px;
}
75% {
margin-left: -20px;
}
100% {
margin-left: 0;
}
}
@media (prefers-reduced-motion: reduce) {
#skynet2-snow {
display: none !important;
}
}