Celebrations
Year-round seasonal effects - eight celebrations, each with traits you can switch off or thin out.
Preview
In season, a sparkle disc on every page of this site turns the day's celebration on and off. Here you can run any of them, any day. Pick one, set how much of it you want, and switch individual traits off - Santa's sleigh, the steam train, the witches. Changes apply straight away.
Pick a celebration to see what it draws.
<div class="celebrate-demo">
<div class="celebrate-controls">
<label class="celebrate-control">Celebration
<select class="form-select form-select-sm" data-model="theme.value" data-on-change="themeChanged">
<option value="auto">Auto - today's, if any</option>
<option value="christmas">๐ Christmas</option>
<option value="halloween">๐ Halloween</option>
<option value="valentines">๐ Valentine's</option>
<option value="guy-fawkes">๐ Guy Fawkes</option>
<option value="st-patricks">โ๏ธ St Patrick's</option>
<option value="st-andrews">๐ด๓ ง๓ ข๓ ณ๓ ฃ๓ ด๓ ฟ St Andrew's</option>
<option value="st-davids">๐ด๓ ง๓ ข๓ ท๓ ฌ๓ ณ๓ ฟ St David's</option>
<option value="st-georges">๐ด๓ ง๓ ข๓ ฅ๓ ฎ๓ ง๓ ฟ St George's</option>
</select>
</label>
<div class="btn-group btn-group-sm" role="group" aria-label="Intensity">
<button type="button" class="btn btn-sm" data-bind-class="intensity.value === 'light' ? 'btn-primary' : 'btn-secondary'" data-on-click="setIntensity('light')">Light</button>
<button type="button" class="btn btn-sm" data-bind-class="intensity.value === 'medium' ? 'btn-primary' : 'btn-secondary'" data-on-click="setIntensity('medium')">Medium</button>
<button type="button" class="btn btn-sm" data-bind-class="intensity.value === 'heavy' ? 'btn-primary' : 'btn-secondary'" data-on-click="setIntensity('heavy')">Heavy</button>
</div>
<button type="button" class="btn btn-sm" data-bind-class="running.value ? 'btn-danger' : 'btn-success'" data-on-click="toggle" data-bind-text="running.value ? 'Stop' : 'Start'">Start</button>
</div>
<div class="celebrate-stats">
<div class="celebrate-stat"><span data-icon="calendar"></span><div><small>Today</small><strong data-bind-text="today.value">-</strong></div></div>
<div class="celebrate-stat"><span data-icon="sparkles"></span><div><small>Showing</small><strong data-bind-text="showing.value">None</strong></div></div>
<div class="celebrate-stat"><span data-icon="activity"></span><div><small>Status</small><strong data-bind-class="running.value ? 'text-success' : 'text-danger'" data-bind-text="running.value ? 'Running' : 'Stopped'">Stopped</strong></div></div>
<div class="celebrate-stat"><span data-icon="box"></span><div><small>Particles</small><strong data-bind-text="particles.value">0</strong></div></div>
</div>
<div class="card celebrate-traits-card">
<div class="card-header flex items-center justify-between">
<strong>What appears</strong>
<span class="btn-group btn-group-sm">
<button type="button" class="btn btn-secondary btn-sm" data-on-click="allTraits(true)">All on</button>
<button type="button" class="btn btn-secondary btn-sm" data-on-click="allTraits(false)">All off</button>
</span>
</div>
<div class="card-body">
<p class="text-muted" data-if="traits.value.length === 0">Pick a celebration to see what it draws.</p>
<div class="celebrate-traits" data-each="traits.value key=name">
<label><input type="checkbox" data-bind-checked="enabled" data-on-change="$root.setTrait($data)"> <span data-bind-text="label"></span></label>
</div>
</div>
</div>
<div class="celebrate-cards" data-each="themes key=id">
<button type="button" class="celebrate-card" data-bind-class="$root.theme.value === id && 'active'" data-on-click="$root.show($data)">
<span class="celebrate-emoji" data-bind-text="emoji"></span>
<strong data-bind-text="name"></strong>
<small class="celebrate-dates" data-bind-text="dates"></small>
<small class="celebrate-draws" data-bind-text="draws"></small>
</button>
</div>
</div>/**
* Preview every celebration - from public/examples/celebrations, rebuilt on bindings.
*
* The celebrations are the domma-celebrate package; this site serves its build from
* the dommajs-site plugin. A preview runs whatever the date, and even for a visitor
* who asked for less motion - they came here to see it.
*/
const CELEBRATE = '/plugins/dommajs-site/public/vendor/celebrate/domma-celebrate.esm.js';
const THEMES = [
{id: 'christmas', emoji: '๐', name: 'Christmas', dates: '1 Dec - 1 Jan', draws: "Snowflakes, Santa's sleigh and reindeer, trees, wreaths, toy trains, robins, elves"},
{id: 'halloween', emoji: '๐', name: 'Halloween', dates: '26-31 Oct', draws: 'Pumpkins, spiders, witches, bats, gravestones, jack-o-lanterns'},
{id: 'valentines', emoji: '๐', name: "Valentine's Day", dates: '9-14 Feb', draws: 'Hearts, rose petals, cupid, heart garlands, sparkles'},
{id: 'guy-fawkes', emoji: '๐', name: 'Guy Fawkes', dates: '1-5 Nov', draws: 'Sparks, rockets, bonfires, Catherine wheels, Roman candles'},
{id: 'st-patricks', emoji: 'โ๏ธ', name: "St Patrick's", dates: '12-17 Mar ยท Ireland', draws: 'Shamrocks, gold coins, leprechauns, banshees, pots of gold, rainbows'},
{id: 'st-andrews', emoji: '๐ด๓ ง๓ ข๓ ณ๓ ฃ๓ ด๓ ฟ', name: "St Andrew's", dates: '25-30 Nov ยท Scotland', draws: 'Thistles, snowflakes, a bagpiper, saltire flags, tartan'},
{id: 'st-davids', emoji: '๐ด๓ ง๓ ข๓ ท๓ ฌ๓ ณ๓ ฟ', name: "St David's", dates: '24 Feb - 1 Mar ยท Wales', draws: 'Daffodils, leeks, dragons, daffodil fields'},
{id: 'st-georges', emoji: '๐ด๓ ง๓ ข๓ ฅ๓ ฎ๓ ง๓ ฟ', name: "St George's", dates: '18-23 Apr ยท England', draws: 'Rose petals, knights, dragons, St George flags'}
];
export async function mount(root) {
const {Celebrations} = await import(CELEBRATE);
const known = Celebrations.getThemes();
const theme = M.observable('auto');
const intensity = M.observable('medium');
const running = M.observable(false);
const showing = M.observable('None');
const particles = M.observable(0);
const today = M.observable(D().format('dddd D MMMM YYYY'));
const traits = M.observable([]);
let chosenTraits = {}; // kept across celebrations: each theme reads only its own names
let effect = null;
const nameOf = (id) => (id && known[id] ? `${known[id].emoji} ${known[id].displayName}` : 'None');
const refresh = () => {
showing.value = nameOf(theme.value === 'auto' ? Celebrations.getCurrentTheme() : theme.value);
particles.value = effect?.particles ? effect.particles.length + (effect.specialParticles?.length || 0) : 0;
const list = effect ? effect.getTraits() : {};
traits.value = Object.entries(list).map(([name, t]) => ({name, label: t.label, enabled: !!t.enabled}));
};
async function load() {
effect?.destroy();
effect = new Celebrations({theme: theme.value, intensity: intensity.value, enabled: false,
traits: chosenTraits, respectMotionPreference: false});
await effect.init();
if (running.value) effect.start();
refresh();
}
const state = {
theme, intensity, running, showing, particles, today, traits, themes: THEMES,
async themeChanged() {
if (effect) await effect.setTheme(theme.value); else await load();
refresh();
},
setIntensity(level) {
intensity.value = level;
effect?.setIntensity(level);
},
toggle() {
if (!effect) return;
running.value = !running.value;
if (running.value) effect.start();
else { effect.pause(); effect.canvasManager?.clear(); }
refresh();
},
async show(card) {
theme.value = card.id;
await state.themeChanged();
if (!running.value) state.toggle();
},
setTrait(trait, event) {
chosenTraits[trait.name] = event.target.checked;
effect?.setTrait(trait.name, event.target.checked);
refresh();
},
allTraits(on) {
if (!effect) return;
const settings = Object.fromEntries(Object.keys(effect.getTraits()).map(n => [n, on]));
chosenTraits = {...chosenTraits, ...settings};
effect.setTraits(settings);
refresh();
}
};
const bindings = M.applyBindings(state, root);
await load();
const ticker = setInterval(() => {
particles.value = effect?.particles ? effect.particles.length + (effect.specialParticles?.length || 0) : 0;
}, 1000);
return () => {
clearInterval(ticker);
effect?.destroy();
bindings?.dispose?.();
};
}.celebrate-controls { display: flex; flex-wrap: wrap; align-items: center; gap: 1rem; margin-bottom: 1rem; }
.celebrate-control { display: flex; align-items: center; gap: .5rem; margin: 0; }
.celebrate-stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: .75rem; margin-bottom: 1rem; }
.celebrate-stat { display: flex; align-items: center; gap: .75rem; padding: .75rem 1rem; border: 1px solid var(--dm-border); border-radius: 10px; background: var(--dm-surface-raised); }
.celebrate-stat small { display: block; color: var(--dm-text-muted); }
.celebrate-traits-card { margin-bottom: 1rem; }
.celebrate-traits { display: grid; grid-template-columns: repeat(auto-fill, minmax(170px, 1fr)); gap: .5rem; }
.celebrate-traits label { display: flex; align-items: center; gap: .5rem; cursor: pointer; }
.celebrate-cards { display: grid; grid-template-columns: repeat(auto-fill, minmax(190px, 1fr)); gap: .75rem; }
.celebrate-card { display: flex; flex-direction: column; align-items: center; gap: .25rem; padding: 1rem; border: 1px solid var(--dm-border); border-radius: 12px; background: var(--dm-surface-raised); color: var(--dm-text); text-align: center; cursor: pointer; transition: transform .2s ease, border-color .2s ease; }
.celebrate-card:hover { transform: translateY(-2px); border-color: var(--dm-primary); }
.celebrate-card.active { border-color: var(--dm-primary); box-shadow: 0 0 0 2px color-mix(in oklab, var(--dm-primary), transparent 60%); }
.celebrate-emoji { font-size: 2rem; line-height: 1; }
.celebrate-dates { color: var(--dm-text-secondary); }
.celebrate-draws { color: var(--dm-text-muted); font-size: .75rem; }The package
Celebrations are their own package, domma-celebrate, so a site with no Domma in it can use them. Domma ships the build at dist/celebrate/.
Manual control
import {Celebrations} from './dist/celebrate/domma-celebrate.esm.js';
const celebrations = new Celebrations({
theme: 'christmas', // or 'auto' for today's, if any
intensity: 'medium', // 'light', 'medium' or 'heavy'
enabled: true
});
// false when there is nothing to show - no celebration in season, or a visitor
// who asked for reduced motion. It never throws.
await celebrations.init();
await celebrations.setTheme('halloween');
celebrations.setIntensity('heavy');
celebrations.pause();
celebrations.start();
celebrations.destroy();
Turning individual traits off
A theme is not one switch. You may want the snow and the trees but not a steam train crossing your header:
const celebrations = new Celebrations({
theme: 'christmas',
traits: {
train: false, // no steam train
elf: false, // no elves
tree: 0.5, // half as many trees
firework: 2 // twice as many fireworks
}
});
celebrations.setTrait('sleigh', false);
celebrations.setTraits({robin: false, wreath: 0.5});
celebrations.getTraits(); // what this theme can switch
false is off and a number is a density multiplier - 0 is off, 1 is untouched, 2 is twice as many. The panel in the preview is built from getTraits().
The celebrations
| Theme | Draws | Dates |
|---|---|---|
christmas |
Snowflakes, Santa's sleigh, reindeer, trees, wreaths, robins, toy trains, elves | 1 Dec - 1 Jan |
halloween |
Pumpkins, bats, ghosts, witches, gravestones, spiders | 26-31 Oct |
valentines |
Hearts, rose petals, cupid, love letters | 9-14 Feb |
guy-fawkes |
Fireworks, bonfires, rockets, Catherine wheels, Roman candles | 1-5 Nov |
st-patricks |
Shamrocks, gold coins, leprechauns, pots of gold, rainbows | 12-17 Mar |
st-andrews |
Thistles, the saltire, a bagpiper, heather | 25-30 Nov |
st-davids |
Daffodils, leeks, the Welsh dragon, flags | 24 Feb - 1 Mar |
st-georges |
Rose petals, knights, dragons, the St George flag | 18-23 Apr |
Intensity
- light - fewer particles, subtle, easiest on the processor
- medium - balanced; the default
- heavy - every particle and decoration
Without Domma
The same package works on any site from one script tag - it mounts the canvas and a control disc of its own, and remembers whether the visitor turned it off:
<script src="domma-celebrate.min.js" data-celebrate></script>
npm install domma-celebrate.
Automatic detection
With theme: 'auto' it reads today's date, finds the celebration in season and starts it - or does nothing (getCurrentTheme() is null) when there is none.