Holiday Script for SKYNET2.net
I previously had some simple snowfall and Christmas lights on there, but decided to ramp it up a bit because Im that extra type of nerd. So I ended up moving the holiday stuff out of the site's core script and made a new one. I also added some extra CSS to my custom CSS file that is loaded so I don't have to touch the template one.
Here is what I have, enjoy.
holiday.js
/* =========================================================
SKYNET2 Holiday Controller + Unified Falling Stuff Engine
- single canvas / single animation loop
- low-end friendly (caps, pauses when hidden)
- respects prefers-reduced-motion
========================================================= */
(() => {
"use strict";
// =========================================================
// DEBUG OVERRIDE (URL-driven, no file edits needed)
// Usage examples:
// ?debug=christmas-15
// ?debug=christmas-day
// ?debug=newyear-burst
// ?debug=2026-12-25
// ?debug=2026-12-25T00:05
// =========================================================
// Optional fallback preset if you want a default test when no URL is provided.
// Leave null for normal operation.
const DEBUG_PRESET = null; // e.g. "christmas-day"
const DEBUG_DATE = debugDateFromUrl() || debugDateFromPreset(DEBUG_PRESET) || null;
function debugDateFromUrl() {
const params = new URLSearchParams(window.location.search);
const v = params.get("debug");
if (!v) return null;
// 1) named presets
const preset = debugDateFromPreset(v);
if (preset) return preset;
// 2) ISO-ish support:
// - 2026-12-25
// - 2026-12-25T00:05
const iso = v.includes("T") ? v : `${v}T12:00`;
const d = new Date(iso);
return isNaN(d.getTime()) ? null : d;
}
function debugDateFromPreset(key) {
if (!key) return null;
// helpers
const atNoon = (y, m, d) => new Date(y, m, d, 12, 0);
const daysBefore = (y, m, d, n) => {
const t = atNoon(y, m, d);
t.setDate(t.getDate() - n);
return t;
};
// Pick a “test year” that keeps stuff sensible.
// (Any year works; it’s just used to compute dates.)
const Y = 2026;
// Map of ALL supported presets
const map = {
// --- New Year ---
"newyear-5": daysBefore(Y + 1, 0, 1, 5), // Dec 27
"newyear-day": atNoon(Y + 1, 0, 1), // Jan 1
"newyear-burst": new Date(Y + 1, 0, 1, 0, 5), // Jan 1 00:05
// --- Independence Day ---
"independence-15": daysBefore(Y, 6, 4, 15),
"independence-day": atNoon(Y, 6, 4),
// --- Birthdays (day only) ---
"allie-bday": atNoon(Y, 6, 10), // Jul 10
"jenny-bday": atNoon(Y, 7, 5), // Aug 5
// --- Halloween ---
"halloween-15": daysBefore(Y, 9, 31, 15),
"halloween-day": atNoon(Y, 9, 31),
// --- USMC Birthday ---
"usmc-9": daysBefore(Y, 10, 10, 9), // Nov 1 (USMC countdown works)
"usmc-day": atNoon(Y, 10, 10),
// --- Veterans Day (day only; your code has windowStartDays: 0) ---
"veterans-day": atNoon(Y, 10, 11),
// --- Thanksgiving (dynamic) ---
"thanksgiving-14": new Date(2026, 10, 12, 12, 0), // Nov 12
"thanksgiving-day": new Date(2026, 10, 26, 12, 0), // Nov 26, 2026
// --- Christmas ---
"christmas-15": daysBefore(Y, 11, 25, 15),
"christmas-day": atNoon(Y, 11, 25),
// --- Star Trek Day ---
"startrek-day": atNoon(Y, 8, 8),
// --- Star Wars Day ---
"starwars-day": atNoon(Y, 4, 4),
};
return map[key] || null;
}
function nowDate() {
return DEBUG_DATE ? new Date(DEBUG_DATE) : new Date();
}
// ---------- DOM targets ----------
const countdownWrap = document.getElementById("holiday-countdown");
const titleEl = document.getElementById("countdown-title");
const daysEl = document.getElementById("countdown-days");
// If the countdown isn't present, just bail quietly.
if (!countdownWrap || !titleEl || !daysEl) return;
// ---------- Motion preference ----------
const prefersReducedMotion = () =>
window.matchMedia &&
window.matchMedia("(prefers-reduced-motion: reduce)").matches;
// ---------- Date helpers ----------
const pad2 = (n) => String(n).padStart(2, "0");
// Midnight local time (browser local)
function atMidnight(d) {
return new Date(d.getFullYear(), d.getMonth(), d.getDate(), 0, 0, 0, 0);
}
// Days between "now" and targetDate (midnight-based)
function daysUntil(targetDate, baseNow = nowDate()) {
const now = atMidnight(baseNow);
const target = atMidnight(targetDate);
const diffMs = target - now;
return Math.floor(diffMs / 86400000);
}
// nth weekday of a month (0=Sun..6=Sat), month is 0-11
function nthWeekdayOfMonth(year, month, weekday, nth) {
const first = new Date(year, month, 1);
const firstWeekday = first.getDay();
const offset = (weekday - firstWeekday + 7) % 7;
const day = 1 + offset + (nth - 1) * 7;
return new Date(year, month, day);
}
function isSameYMD(a, b) {
return (
a.getFullYear() === b.getFullYear() &&
a.getMonth() === b.getMonth() &&
a.getDate() === b.getDate()
);
}
// Ordinal suffix: 1ST, 2ND, 3RD, 4TH...
function ordinal(n) {
const mod100 = n % 100;
if (mod100 >= 11 && mod100 <= 13) return `${n}TH`;
const mod10 = n % 10;
if (mod10 === 1) return `${n}ST`;
if (mod10 === 2) return `${n}ND`;
if (mod10 === 3) return `${n}RD`;
return `${n}TH`;
}
// ---------- Unified Falling Stuff Engine ----------
const FallingStuffEngine = (() => {
let canvas = null;
let ctx = null;
let rafId = null;
let running = false;
let paused = false;
let particles = [];
let config = null;
let lastFrame = 0;
const BASE_FONT =
'"Antonio", system-ui, -apple-system, "Segoe UI", Roboto, Arial, ' +
'"Apple Color Emoji", "Segoe UI Emoji", "Noto Color Emoji", sans-serif';
function ensureCanvas() {
if (canvas) return;
canvas = document.createElement("canvas");
canvas.id = "skynet2-holiday-canvas";
canvas.style.position = "fixed";
canvas.style.inset = "0";
canvas.style.pointerEvents = "none";
canvas.style.zIndex = "9999";
document.body.appendChild(canvas);
ctx = canvas.getContext("2d", {
alpha: true
});
resize(); // init
window.addEventListener("resize", resize, {
passive: true
});
window.addEventListener("pageshow", resize, {
passive: true
});
window.addEventListener("focus", resize, {
passive: true
});
document.addEventListener("visibilitychange", () => {
paused = document.hidden;
if (!document.hidden) {
// force a clean timing restart
lastFrame = 0;
resize();
}
});
}
function destroyCanvas() {
if (canvas) canvas.remove();
canvas = null;
ctx = null;
}
function resize() {
if (!canvas || !ctx) return;
const w = Math.max(document.documentElement.clientWidth, window.innerWidth || 0);
const h = Math.max(document.documentElement.clientHeight, window.innerHeight || 0);
if (w < 200 || h < 200) return;
const dpr = window.devicePixelRatio || 1;
canvas.style.width = w + "px";
canvas.style.height = h + "px";
canvas.width = Math.round(w * dpr);
canvas.height = Math.round(h * dpr);
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.scale(dpr, dpr);
}
function randomFrom(arr) {
return arr[Math.floor(Math.random() * arr.length)];
}
function spawnParticle() {
const rect = canvas.getBoundingClientRect();
const w = rect.width;
const h = rect.height;
const glyphArr = Array.isArray(config.glyphs) ? config.glyphs : [config.glyphs];
const size = config.sizeMin + Math.random() * (config.sizeMax - config.sizeMin);
const speed = config.speedMin + Math.random() * (config.speedMax - config.speedMin);
const swayAmp = config.sway ? (config.swayAmpMin + Math.random() * (config.swayAmpMax - config.swayAmpMin)) : 0;
const swayFreq = config.sway ? (config.swayFreqMin + Math.random() * (config.swayFreqMax - config.swayFreqMin)) : 0;
const color = Array.isArray(config.colors) ? randomFrom(config.colors) : (config.colors || "rgba(255,255,255,0.85)");
const x = Math.random() * w;
const y = Math.random() * h - h; // start above screen like your old code
return {
x,
y,
baseX: x,
size,
speed,
swayAmp,
swayFreq,
swayPhase: Math.random() * Math.PI * 2,
glyph: randomFrom(glyphArr),
color
};
}
function clear(rectW, rectH) {
ctx.clearRect(0, 0, rectW, rectH);
}
function drawParticle(p) {
ctx.font = `${p.size}px ${BASE_FONT}`;
ctx.fillStyle = p.color;
ctx.fillText(p.glyph, p.x, p.y);
}
function tick(ts) {
if (!running || !ctx || !config || !canvas) return;
if (paused) {
rafId = requestAnimationFrame(tick);
return;
}
const rect = canvas.getBoundingClientRect();
const w = rect.width;
const h = rect.height;
if (!lastFrame) lastFrame = ts;
const dt = Math.min(0.05, (ts - lastFrame) / 1000);
lastFrame = ts;
clear(w, h);
const killY = h + 20;
for (let i = 0; i < particles.length; i++) {
const p = particles[i];
p.y += p.speed * dt;
if (config.sway) {
const t = (ts / 1000) * p.swayFreq + p.swayPhase;
p.x = p.baseX + Math.sin(t) * p.swayAmp;
}
drawParticle(p);
// recycle like the old working code
if (p.y > killY) {
p.y = -20;
p.x = Math.random() * w;
p.baseX = p.x;
if (Array.isArray(config.glyphs)) p.glyph = randomFrom(config.glyphs);
if (Array.isArray(config.colors)) p.color = randomFrom(config.colors);
}
}
rafId = requestAnimationFrame(tick);
}
function start(newConfig) {
if (window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches) return;
ensureCanvas();
resize();
config = {
glyphs: "❄",
colors: "rgba(255,255,255,0.85)",
maxParticles: 120,
sizeMin: 16,
sizeMax: 34,
speedMin: 30,
speedMax: 80,
sway: true,
swayAmpMin: 2,
swayAmpMax: 12,
swayFreqMin: 0.6,
swayFreqMax: 1.6,
...newConfig
};
// fixed pool — no “spawn timing” at all
particles = [];
for (let i = 0; i < config.maxParticles; i++) {
particles.push(spawnParticle());
}
paused = document.hidden;
lastFrame = 0;
if (!running) {
running = true;
rafId = requestAnimationFrame(tick);
}
}
function stop() {
running = false;
config = null;
particles = [];
if (rafId) cancelAnimationFrame(rafId);
rafId = null;
lastFrame = 0;
destroyCanvas();
}
function isRunning() {
return running;
}
return {
start,
stop,
isRunning
};
})();
// ---------- Holiday definitions ----------
// windowStartDays: when countdown begins (days before holiday)
// if windowStartDays is 0, countdown never shows (day-of only)
// JS dates (specifically months) are 0-11 so the months are one off! - you arent crazy
const HOLIDAYS = [{
key: "newyear",
labelUntil: "DAYS UNTIL<br>🎉 NEW YEARS 🎉",
labelToday: "HAPPY NEW YEAR!",
dateForYear: (y) => new Date(y, 0, 1), // January 1st
windowStartDays: 30
},
{
key: "may4",
labelUntil: "DAYS UNTIL<br>⭐ MAY THE FOURTH ⭐", // ⭐
labelToday: "MAY THE FOURTH<br>BE WITH YOU!",
dateForYear: (y) => new Date(y, 4, 4), // May 4 (month=4)
windowStartDays: 0 // surprise mode
},
{
key: "independence",
labelUntil: "DAYS UNTIL<br>🇺🇸 JUL 4th 🇺🇸",
labelToday: "REBEL SCUM!",
dateForYear: (y) => new Date(y, 6, 4), // July 4th
windowStartDays: 30
},
{
key: "bdayallie",
labelUntil: "UNTIL ALLIE'S BIRTHDAY",
labelToday: "HAPPY BIRTHDAY!",
dateForYear: (y) => new Date(y, 6, 10), // July 10th
windowStartDays: 0
},
{
key: "bdayjenny",
labelUntil: "UNTIL JENNY'S BIRTHDAY",
labelToday: "HAPPY BIRTHDAY!",
dateForYear: (y) => new Date(y, 7, 5), // August 5th
windowStartDays: 0
},
{
key: "startrek",
labelUntil: "DAYS UNTIL<br>🖖 STAR TREK DAY 🖖", // 🖖
labelToday: "STAR TREK DAY!",
dateForYear: (y) => new Date(y, 8, 8), // Sep 8 (month=8)
windowStartDays: 0 // surprise mode
},
{
key: "halloween",
labelUntil: "DAYS UNTIL<br>🎃 HALLOWEEN 🎃",
labelToday: "HAPPY HALLOWEEN!",
dateForYear: (y) => new Date(y, 9, 31), // October 31st
// start Oct 1 (month theme), not 30 days
windowStartDays: 31 // effectively "all October" since Oct has 31 days
},
{
key: "usmc",
labelUntil: "DAYS UNTIL<br>🇺🇸 USMC BIRTHDAY 🇺🇸",
labelToday: "SEMPER FI!",
dateForYear: (y) => new Date(y, 10, 10), // November 10th
windowStartDays: 30,
specialToday: (now) => {
const n = now.getFullYear() - 1775;
return ordinal(n); // e.g. 250TH
}
},
{
key: "veterans",
labelUntil: "DAYS UNTIL<br>🇺🇸 VETERANS DAY 🇺🇸",
labelToday: "HAPPY VETERANS DAY!",
dateForYear: (y) => new Date(y, 10, 11), // November 11th
windowStartDays: 0 // day-of only to avoid fighting USMC
},
{
key: "thanksgiving",
labelUntil: "DAYS UNTIL<br>🦃 THANKSGIVING 🦃",
labelToday: "HAPPY<br>THANKSGIVING!",
dateForYear: (y) => nthWeekdayOfMonth(y, 10, 4, 4), // November, Thursday=4, 4th Thrusday of the month
windowStartDays: 14
},
{
key: "christmas",
labelUntil: "DAYS UNTIL<br>🎄 CHRISTMAS 🎄",
labelToday: "MERRY CHRISTMAS!",
dateForYear: (y) => new Date(y, 11, 25), // December 25th
windowStartDays: 30
}
];
// Seasonal window: Dec 1 -> Feb 2
function inWinterWindow(now) {
const y = now.getFullYear();
const dec1 = new Date(y, 11, 1);
const feb2 = new Date(y + 1, 1, 2); // Feb 2 next year
// If we're in Jan/Feb, the window started last year
const dec1Prev = new Date(y - 1, 11, 1);
const feb2This = new Date(y, 1, 2);
const t = now.getTime();
return (t >= dec1.getTime() && t < feb2.getTime()) ||
(t >= dec1Prev.getTime() && t < feb2This.getTime());
}
// Countdown day color per holiday (numbers only)
const HOLIDAY_DAY_COLORS = {
newyear: "var(--gold)",
independence: "var(--blue)",
halloween: "var(--orange)",
usmc: "var(--almond)",
veterans: "var(--bluey)",
thanksgiving: "var(--gold)",
christmas: "var(--christmas-green)",
};
// Holidays where you do NOT want blink behavior
const NO_BLINK_KEYS = new Set(["usmc", "veterans"]);
// helper: remove/add blink classes cleanly
function setBlinkClass(el, clsOrNull) {
el.classList.remove("blink-slow", "blink", "blink-fast");
if (clsOrNull) el.classList.add(clsOrNull);
}
function applyCountdownStyling(state) {
// Always start clean
daysEl.style.color = "";
setBlinkClass(daysEl, null);
// Only countdown gets holiday color + blink ladder
if (state.mode !== "countdown") return;
const key = state.holiday?.key;
// Color by holiday (numbers only)
const color = HOLIDAY_DAY_COLORS[key];
if (color) daysEl.style.color = color;
// Blink ladder (unless excluded)
if (NO_BLINK_KEYS.has(key)) return;
const d = state.days;
if (d <= 5) setBlinkClass(daysEl, "blink-fast");
else if (d <= 7) setBlinkClass(daysEl, "blink");
else if (d <= 10) setBlinkClass(daysEl, "blink-slow");
}
// ---------- Effect profiles (single engine) ----------
const HOLIDAY_EMOJI = {
newyear: "\u{1F942}", // 🥂
may4: "\u2B50", // ⭐
independence: "\u{1F1FA}\u{1F1F8}", // 🇺🇸 (flag = TWO codepoints)
bdayjenny: "\u{1F973}", // 🥳 party!
startrek: "\u{1F596}", // 🖖
bdayallie: "\u{1F973}", // 🥳 party!
halloween: "\u{1F47B}", // 👻
usmc: "\u2694\uFE0F", // ⚔️ (variation selector matters)
veterans: "\u{1F1FA}\u{1F1F8}", // 🇺🇸 flag
thanksgiving: "\u{1F983}", // 🦃
christmas: "\u{1F384}", // 🎄
};
// NOTE: keep emoji-heavy profiles lower maxParticles for weaker hardware.
const PROFILES = {
winterSnow: {
glyphs: ["\u2744", "\u2745", "\u2746", "\u2744"], // ❄ ❅ ❆ ❄️ (encoding-safe)
colors: "rgba(255,255,255,0.80)",
maxParticles: 120,
spawnEveryMs: 60,
sizeMin: 16,
sizeMax: 34,
speedMin: 30,
speedMax: 80,
sway: true
},
rwbConfetti: {
glyphs: ["\u25A0", "\u25AA", "\u25C6", "\u25CF", "\u{1F1FA}\u{1F1F8}"], // ■ ▪ ◆ ●
colors: ["rgba(255,60,60,0.85)", "rgba(255,255,255,0.85)", "rgba(80,140,255,0.85)"],
maxParticles: 90,
spawnEveryMs: 55,
sizeMin: 12,
sizeMax: 22,
speedMin: 70,
speedMax: 140,
sway: true,
swayAmpMin: 2,
swayAmpMax: 12
},
halloween: {
glyphs: ["\u{1F383}", "\u{1F47B}", "\u{1F36C}", "\u{1F987}", "\u{1F480}"], // 🎃👻🍬🦇💀
colors: "rgba(255,255,255,0.90)",
maxParticles: 35,
spawnEveryMs: 140,
sizeMin: 18,
sizeMax: 34,
speedMin: 55,
speedMax: 110,
sway: true
},
thanksgiving: {
glyphs: ["\u{1F983}", "\u{1F342}", "\u{1F341}", "\u{1F967}", "\u{1F33D}"], // 🎃🦃🍂🍁🥧🌽🍗
colors: "rgba(255,255,255,0.90)",
maxParticles: 35,
spawnEveryMs: 160,
sizeMin: 18,
sizeMax: 34,
speedMin: 60,
speedMax: 120,
sway: true
},
christmasDay: {
glyphs: ["\u{1F381}", "\u{1F385}", "\u{1F384}", "\u{26C4}", "\u{1F384}", "\u{1F9F8}", "\u{1F385}", "\u{26C4}", "\u{1F31F}"], // 🎁🎅🏻🎄⛄🎄🧸🎅🏻⛄⭐
colors: "rgba(255,255,255,0.92)",
maxParticles: 45,
spawnEveryMs: 120,
sizeMin: 18,
sizeMax: 34,
speedMin: 55,
speedMax: 115,
sway: true
},
newYearBurst: {
glyphs: ["\u2726", "\u273A", "\u2739", "\u2738", "\u{1F38A}"], // ✦ ✺ ✹ ✸ 🎊
colors: ["rgba(255,255,255,0.9)", "rgba(255,200,80,0.9)", "rgba(160,220,255,0.9)"],
maxParticles: 80,
spawnEveryMs: 45,
sizeMin: 12,
sizeMax: 22,
speedMin: 90,
speedMax: 180,
sway: true,
swayAmpMin: 0,
swayAmpMax: 8
},
birthday: {
glyphs: ["\u{1F382}", "\u{1F381}", "\u{1F389}", "\u2728", "\u{1F973}"], // 🎂 🎁 🎉 ✨ 🥳
colors: [
"rgba(255,180,220,0.92)",
"rgba(200,255,200,0.92)",
"rgba(180,210,255,0.92)",
"rgba(255,230,150,0.92)"
],
maxParticles: 45,
spawnEveryMs: 120,
sizeMin: 18,
sizeMax: 34,
speedMin: 55,
speedMax: 115,
sway: true
},
startrekStars: {
glyphs: ["\u2728", "\u2B50", "\u{1F30C}", "\u{1F680}", "\u{1F596}", "\u{1F6F0}\u{FE0F}"], // ✨ ⭐ 🌌 🚀 🖖 🛰️
colors: ["rgba(200,220,255,0.92)", "rgba(255,255,255,0.92)", "rgba(180,210,255,0.92)"],
maxParticles: 45,
sizeMin: 16,
sizeMax: 32,
speedMin: 55,
speedMax: 115,
sway: true
},
may4Stars: {
glyphs: ["\u2B50", "\u2728", "\u{1F30C}", "\u{1F680}", "\u{1F916}"], // ⭐ ✨ 🌌 🚀 🤖
colors: ["rgba(255,230,150,0.92)", "rgba(255,255,255,0.92)", "rgba(180,210,255,0.92)"],
maxParticles: 45,
sizeMin: 16,
sizeMax: 32,
speedMin: 55,
speedMax: 115,
sway: true
}
};
// ---------- Controller: choose current holiday + what to show ----------
function getNextHoliday(now) {
const y = now.getFullYear();
// Create a list of holiday occurrences for "this year" and "next year"
const candidates = [];
for (const h of HOLIDAYS) {
const dThis = h.dateForYear(y);
const dNext = h.dateForYear(y + 1);
candidates.push({
h,
date: dThis
});
candidates.push({
h,
date: dNext
});
}
// Filter to future or today, then pick soonest
candidates.sort((a, b) => a.date - b.date);
for (const c of candidates) {
// Accept today or future
const diff = daysUntil(c.date, now);
if (diff >= 0) return c;
}
return null;
}
function computeState(now) {
const today = atMidnight(now);
// Day-of holiday takes priority
for (const h of HOLIDAYS) {
const d = h.dateForYear(now.getFullYear());
if (isSameYMD(d, today)) {
return {
mode: "today",
holiday: h,
date: d
};
}
}
// Otherwise pick next holiday and see if we're in its countdown window
const next = getNextHoliday(now);
if (!next) return {
mode: "inactive"
};
const {
h,
date
} = next;
const du = daysUntil(date, now);
if (h.windowStartDays > 0 && du <= h.windowStartDays) {
return {
mode: "countdown",
holiday: h,
date,
days: du
};
}
return {
mode: "inactive"
};
}
// ---------- Apply UI state ----------
function hideCountdown() {
countdownWrap.style.display = "none";
}
function showCountdown() {
countdownWrap.style.display = "block";
}
function setBodyHolidayClass(key) {
// Clear prior holiday classes
document.body.classList.forEach((c) => {
if (c.startsWith("holiday--")) document.body.classList.remove(c);
});
if (key) document.body.classList.add(`holiday--${key}`);
}
// Decide what engine profile should run given the current time/state
function chooseProfile(now, state) {
// Reduced motion = no engine at all
if (prefersReducedMotion()) return null;
// Winter snow season (Dec 1 -> Feb 2) is the baseline
const winter = inWinterWindow(now);
// Day-of overrides
if (state.mode === "today") {
switch (state.holiday.key) {
case "christmas":
return PROFILES.christmasDay;
case "bdayjenny":
case "bdayallie":
return PROFILES.birthday;
case "startrek":
return PROFILES.startrekStars;
case "may4":
return PROFILES.may4Stars;
case "usmc":
case "veterans":
return PROFILES.rwbConfetti;
case "halloween":
return PROFILES.halloween;
case "thanksgiving":
return PROFILES.thanksgiving;
case "newyear": {
// 00:00–00:10 burst. If you want confetti after, we can schedule it later.
const hr = now.getHours();
const min = now.getMinutes();
if (hr === 0 && min < 10) return PROFILES.newYearBurst;
// Otherwise: winter snow if in window, else no engine
return winter ? PROFILES.winterSnow : null;
}
case "independence":
// Placeholder: use burst profile for now, or swap later for true fireworks
return PROFILES.rwbConfetti;
}
}
// Countdown (month vibes)
if (state.mode === "countdown") {
if (state.holiday.key === "halloween") {
// October: keep it light (emoji set but capped)
// If you’d rather do CSS-only webs for October, return null here.
return PROFILES.halloween;
}
// Otherwise: default winter snow in winter window only
return winter ? PROFILES.winterSnow : null;
}
// Inactive: still run winter snow season if applicable
return winter ? PROFILES.winterSnow : null;
}
function render(now) {
const state = computeState(now);
if (state.mode === "inactive") {
hideCountdown();
setBodyHolidayClass(null);
} else if (state.mode === "countdown") {
showCountdown();
setBodyHolidayClass(state.holiday.key);
titleEl.innerHTML = state.holiday.labelUntil;
daysEl.textContent = pad2(state.days);
} else if (state.mode === "today") {
showCountdown();
setBodyHolidayClass(state.holiday.key);
titleEl.innerHTML = state.holiday.labelToday;
if (typeof state.holiday.specialToday === "function") {
daysEl.textContent = state.holiday.specialToday(now);
} else {
daysEl.textContent = HOLIDAY_EMOJI[state.holiday.key] || "00";
}
}
// one call, always
applyCountdownStyling(state);
const profile = chooseProfile(now, state);
if (!profile) {
if (FallingStuffEngine.isRunning()) FallingStuffEngine.stop();
} else {
FallingStuffEngine.start(profile);
}
}
// ---------- Scheduling ----------
// Low-impact updates: every 10 minutes normally.
// If we're in the first 10 minutes of New Year, update every 10 seconds.
let timer = null;
function scheduleOnce() {
const now = nowDate();
const state = computeState(now);
let intervalMs = 10 * 60 * 1000;
if (state.mode === "today" && state.holiday.key === "newyear") {
if (now.getHours() === 0 && now.getMinutes() < 10) intervalMs = 10 * 1000;
}
setTimeout(() => {
render(nowDate());
scheduleOnce();
}, intervalMs);
}
// Pause engine hard when the tab is hidden (extra kiosk friendliness)
document.addEventListener("visibilitychange", () => {
if (!document.hidden) render(nowDate());
});
// Boot
document.addEventListener("DOMContentLoaded", () => {
render(nowDate());
scheduleOnce();
});
})();
The CSS
Just add this to your CSS file and call it good.
/* HOLIDAY CSS */
#holiday-countdown {
display: none; /* JS will flip it to block */
margin: 12px 0;
text-align: center;
}
#holiday-countdown .go-center {
display: block; /* make spans behave like full-width lines */
width: 100%;
white-space: nowrap; /* prevent wrapping */
}
#countdown-title {
font-size: clamp(1.4rem, 0.5vw + 1.2rem, 2.4rem);
color: #fba;
letter-spacing: 0.08em;
}
#countdown-days {
font-size: clamp(2.5rem, 2.5vw + 2rem, 7.5rem);
color: #89f;
font-variant-numeric: tabular-nums;
line-height: 1;
margin-bottom: 35px;
padding-bottom: 15px;
}