Compare commits
7 Commits
8dea0d1da1
...
68f67f5839
| Author | SHA1 | Date | |
|---|---|---|---|
| 68f67f5839 | |||
| 090dacdeba | |||
| f757c2ad7d | |||
| 31fe00f4ca | |||
| d45465219a | |||
| 52b0c22d59 | |||
| 655fd7d652 |
@@ -6,10 +6,6 @@
|
|||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<script>
|
|
||||||
const settingsModal = document.getElementById("siteSettings");
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div id="siteSettingsContainer">
|
<div id="siteSettingsContainer">
|
||||||
<button onclick="siteSettings.showModal();">Site Settings</button>
|
<button onclick="siteSettings.showModal();">Site Settings</button>
|
||||||
<dialog id="siteSettings">
|
<dialog id="siteSettings">
|
||||||
|
|||||||
245
content/blog/let-it-snow.md
Normal file
245
content/blog/let-it-snow.md
Normal file
@@ -0,0 +1,245 @@
|
|||||||
|
---
|
||||||
|
title: "Let it Snow: Adding a Falling-Snow Effect to Your Eleventy Website"
|
||||||
|
description: As you can see, I'm getting my priorities in order. Here's how I added a falling-snow effect to my website.
|
||||||
|
date: 2025-11-10
|
||||||
|
tags:
|
||||||
|
- Site Updates
|
||||||
|
- Eleventy
|
||||||
|
synopsis: As you can see, I'm getting my priorities in order. Here's how I added a falling-snow effect to my website.
|
||||||
|
mastodon_id: "115528575840719665"
|
||||||
|
---
|
||||||
|
I'm pretty happy with the look and feel of my website but, looking around the indieweb, I see so many creative and fun websites with neat animations and interactive features. Reader, I was jealous; I wanted my website to be more fun. The [flying toasters](/special/flying-toasters) just weren't enough anymore. My first thought was to add a midi-player. I spent a few hours in a hyperfocus-hole digging up all sorts of fun midi tracks, from Kate Bush to Rammstein. I was *excited.* But reality hit me like a truck when I learned that HTML5 dropped support for midi files. This meant that it was either going to be a monumental pain in the arse to implement my midi payer, or I was going to have to rely on some pretty heavy [dependencies](https://github.com/cifkao/html-midi-player?tab=readme-ov-file#installing-from-npm). And look, I know typing `npm install blah` doesn't seem like a big deal to some folk but, where I can, I would really rather avoid summoning from the ether giant directories full of code that I don't understand for my little website. To add to that, provided you don't want your midi files played by some dead-simple synth sound, there's the business of soundfonts: gigabytes of audio samples from mysterious origins which you have to host yourself if you don't want [Google's servers](https://github.com/cifkao/html-midi-player?tab=readme-ov-file#soundfonts) tracking all of your visitors. At least one popular soundfont also seems to be a bit of a mystery; where does SGM Plus come from? No one seems to know. How is it licensed? I sure couldn't find an answer.
|
||||||
|
|
||||||
|
So, yea, I gave up on that idea and decided to implement a falling-snow effect instead. Here's how I did it.
|
||||||
|
|
||||||
|
## Humble beginnings
|
||||||
|
I set out to look for an implementation with as little JavaScript as possible. I have nothing against JavaScript, but I figure it's best to try trimming your toenails with clippers before reaching for a chainsaw. The search led me to a codepen with [this HTML and CSS-only solution](https://codepen.io/codeconvey/pen/xRzQay). I tidied up the formatting, stripped out anything unnecessary, and put together my include, `_includes/weather.njk`:
|
||||||
|
|
||||||
|
{% raw %}
|
||||||
|
``` html
|
||||||
|
<!-- weather -->
|
||||||
|
<!-- Based on https://codepen.io/codeconvey/pen/xRzQay -->
|
||||||
|
{# This include causes a symbol (text, emoji, et cetera; from metadata.weatherSymbol) to fall from the top of the viewport like snow. #}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.fallingObject {
|
||||||
|
color: #fff;
|
||||||
|
font-size: 1em;
|
||||||
|
font-family: Arial;
|
||||||
|
pointer-events: none;
|
||||||
|
text-shadow: 0 0 1px #000;
|
||||||
|
}
|
||||||
|
@keyframes fallingObjects-fall {
|
||||||
|
0% {
|
||||||
|
top: -10%;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
top: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@keyframes fallingObjects-shake {
|
||||||
|
0% {
|
||||||
|
transform: translateX(0px);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: translateX(80px);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: translateX(0px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.fallingObject {
|
||||||
|
position: fixed;
|
||||||
|
top: -10%;
|
||||||
|
z-index: 9999;
|
||||||
|
user-select: none;
|
||||||
|
cursor: default;
|
||||||
|
animation-name: fallingObjects-fall, fallingObjects-shake;
|
||||||
|
animation-duration: 10s, 3s;
|
||||||
|
animation-timing-function: linear, ease-in-out;
|
||||||
|
animation-iteration-count: infinite, infinite;
|
||||||
|
animation-play-state: running, running;
|
||||||
|
}
|
||||||
|
.fallingObject:nth-of-type(0) {
|
||||||
|
left: 1%;
|
||||||
|
animation-delay: 0s, 0s;
|
||||||
|
& > div {
|
||||||
|
transform: rotate(45deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.fallingObject:nth-of-type(1) {
|
||||||
|
left: 10%;
|
||||||
|
animation-delay: 1s, 1s;
|
||||||
|
& > div {
|
||||||
|
transform: rotate(10deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.fallingObject:nth-of-type(2) {
|
||||||
|
left: 20%;
|
||||||
|
animation-delay: 6s, 0.5s;
|
||||||
|
& > div {
|
||||||
|
transform: rotate(60deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.fallingObject:nth-of-type(3) {
|
||||||
|
left: 30%;
|
||||||
|
animation-delay: 4s, 2s;
|
||||||
|
& > div {
|
||||||
|
transform: rotate(84deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.fallingObject:nth-of-type(4) {
|
||||||
|
left: 40%;
|
||||||
|
animation-delay: 2s, 2s;
|
||||||
|
& > div {
|
||||||
|
transform: rotate(267deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.fallingObject:nth-of-type(5) {
|
||||||
|
left: 50%;
|
||||||
|
animation-delay: 8s, 3s;
|
||||||
|
& > div {
|
||||||
|
transform: rotate(200deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.fallingObject:nth-of-type(6) {
|
||||||
|
left: 60%;
|
||||||
|
animation-delay: 6s, 2s;
|
||||||
|
& > div {
|
||||||
|
transform: rotate(20deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.fallingObject:nth-of-type(7) {
|
||||||
|
left: 70%;
|
||||||
|
animation-delay: 2.5s, 1s;
|
||||||
|
& > div {
|
||||||
|
transform: rotate(78deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.fallingObject:nth-of-type(8) {
|
||||||
|
left: 80%;
|
||||||
|
animation-delay: 1s, 0s;
|
||||||
|
& > div {
|
||||||
|
transform: rotate(3120deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.fallingObject:nth-of-type(9) {
|
||||||
|
left: 90%;
|
||||||
|
animation-delay: 3s, 1.5s;
|
||||||
|
& > div {
|
||||||
|
transform: rotate(123deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<div class="fallingObjects" id="weather" aria-hidden="true">
|
||||||
|
<div class="fallingObject">
|
||||||
|
<div>{{ metadata.weatherSymbol }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="fallingObject">
|
||||||
|
<div>{{ metadata.weatherSymbol }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="fallingObject">
|
||||||
|
<div>{{ metadata.weatherSymbol }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="fallingObject">
|
||||||
|
<div>{{ metadata.weatherSymbol }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="fallingObject">
|
||||||
|
<div>{{ metadata.weatherSymbol }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="fallingObject">
|
||||||
|
<div>{{ metadata.weatherSymbol }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="fallingObject">
|
||||||
|
<div>{{ metadata.weatherSymbol }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="fallingObject">
|
||||||
|
<div>{{ metadata.weatherSymbol }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="fallingObject">
|
||||||
|
<div>{{ metadata.weatherSymbol }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="fallingObject">
|
||||||
|
<div>{{ metadata.weatherSymbol }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- /weather -->
|
||||||
|
```
|
||||||
|
{% endraw %}
|
||||||
|
I added this include in my base layout, after the footer, just before the the closing `</body>` tag. Beyond cleanup, I made the following changes:
|
||||||
|
* I replaced the snowflakes in the example with {% raw %}`{{ metadata.weatherSymbol }}`{% endraw %} so that I can easily change the symbol that falls (that's right folks, this'll do more than just snowflakes!).
|
||||||
|
* I added a random amount of rotation to each object.
|
||||||
|
* I changed the class names from anything snowflake related because I'm a pedant.
|
||||||
|
|
||||||
|
Now all we need to do is make sure {% raw %}`{{ metadata.weatherSymbol }}`{% endraw %} exists and we should be cooking with gas. To `_data/metadata.js` I added: `weatherSymbol: "🍁",`; a falling leaf for autumn.
|
||||||
|
|
||||||
|
## Settings
|
||||||
|
Now we have our ~~falling-snow~~ falling leaf effect working but, as with anything fun, there are going to be at least a few crabbit souls who are going to hate this. For them, let's implement a toggle. First, the toggle itself, in `_includes/weatherController.njk`:
|
||||||
|
|
||||||
|
``` html
|
||||||
|
<form id="weatherController">
|
||||||
|
<input type="checkbox" id="weatherToggle" checked />
|
||||||
|
<label for="weatherToggle">Show weather?</label>
|
||||||
|
</form>
|
||||||
|
```
|
||||||
|
|
||||||
|
Second, a settings modal to hold the toggle, in `_includes/siteSettings.njk` (if the feature happens to be turned on at the moment you should be able to scroll down to the bottom of the page to see this in action):
|
||||||
|
{% raw %}
|
||||||
|
``` html
|
||||||
|
<div id="siteSettingsContainer">
|
||||||
|
<button onclick="siteSettings.showModal();">Site Settings</button>
|
||||||
|
<dialog id="siteSettings">
|
||||||
|
<h2>Site Settings</h2>
|
||||||
|
{% include "weatherController.njk" %}
|
||||||
|
<button id="settingsDone" onclick="siteSettings.close();">Done</button>
|
||||||
|
</dialog>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
{% endraw %}
|
||||||
|
Alright, now we just need to pop our site settings include into the site footer and wire everything up together.
|
||||||
|
|
||||||
|
## Wiring it all up
|
||||||
|
Let's first add a quick rule to our CSS:
|
||||||
|
``` CSS
|
||||||
|
.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Then we can work on our script. Let's add it to the bottom of our weather include, `_includes/weather.njk`, as it's positioned right before the closing `</body>` tag.
|
||||||
|
|
||||||
|
First, we'll check local storage to see if the user has set a preference before; if so, we'll add/remove our `.hidden` CSS rule to our weather element and update the weather-controller checkbox accordingly:
|
||||||
|
|
||||||
|
``` javascript
|
||||||
|
<script>
|
||||||
|
const weather = document.getElementById("weather");
|
||||||
|
const weatherToggle = document.getElementById("weatherToggle");
|
||||||
|
const weatherPreference = localStorage.getItem("weather");
|
||||||
|
|
||||||
|
// Initial weather preference check on page load
|
||||||
|
if (weatherPreference == 0) {
|
||||||
|
weather.classList.add("hidden");
|
||||||
|
weatherToggle.checked = false;
|
||||||
|
} else {
|
||||||
|
weather.classList.remove("hidden");
|
||||||
|
weatherToggle.checked = true;
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
Then, we'll create an event listener on the checkbox, which will add a value into local storage to save our visitor's preference and add/remove that CSS rule whenever a change to the checkbox value is detected:
|
||||||
|
|
||||||
|
``` javascript
|
||||||
|
// Handle weather setting toggle
|
||||||
|
weatherToggle.addEventListener('change', function() {
|
||||||
|
if (this.checked) {
|
||||||
|
localStorage.setItem("weather", 1);
|
||||||
|
weather.classList.remove("hidden");
|
||||||
|
} else {
|
||||||
|
localStorage.setItem("weather", 0);
|
||||||
|
weather.classList.add("hidden");
|
||||||
|
};
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
## Done!
|
||||||
|
With that, we're done! We now have a "falling-snow" effect that can take any emoji (or arbitrary text) set in `_data/metadata.js`, and can be toggled on and off by the visitor whose preference is retained in local storage across sessions.
|
||||||
@@ -16,7 +16,7 @@ An old-school links page. Click on one or two!
|
|||||||
* [GNU](https://gnu.org)
|
* [GNU](https://gnu.org)
|
||||||
* [KDE](https://kde.org)
|
* [KDE](https://kde.org)
|
||||||
|
|
||||||
## Friends
|
## Friends (Not linked elsewhere on this site)
|
||||||
* [Davey Dynamite](https://daveydynamite.neocities.org/)
|
* [Davey Dynamite](https://daveydynamite.neocities.org/)
|
||||||
* [Mercury Retro](https://calam.us/)
|
* [Mercury Retro](https://calam.us/)
|
||||||
|
|
||||||
@@ -33,7 +33,7 @@ An old-school links page. Click on one or two!
|
|||||||
* [searchmysite.net—Search real content by real people from their personal websites](https://searchmysite.net/)
|
* [searchmysite.net—Search real content by real people from their personal websites](https://searchmysite.net/)
|
||||||
|
|
||||||
## Indieweb resources
|
## Indieweb resources
|
||||||
* [90s Cursor Efffects](https://tholman.com/cursor-effects/)
|
* [90s Cursor Effects](https://tholman.com/cursor-effects/)
|
||||||
* [blinkies.cafe—blinkie maker](https://blinkies.cafe/)
|
* [blinkies.cafe—blinkie maker](https://blinkies.cafe/)
|
||||||
* [GIF Printer 2000](https://melonking.net/frames/pixelsea)
|
* [GIF Printer 2000](https://melonking.net/frames/pixelsea)
|
||||||
* [GifCities—The Geocities Animated Gif Search from Internet Archive](https://gifcities.org/)
|
* [GifCities—The Geocities Animated Gif Search from Internet Archive](https://gifcities.org/)
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
title: Brunei Kinam
|
||||||
|
manufacturer: Ensar Oud
|
||||||
|
date: 2025-11-10 11:54:00
|
||||||
|
time: 11:54 AM
|
||||||
|
---
|
||||||
|
Carrying on with the agarwood theme by putting a chip (sliver) on the heater.
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
title: Jinko Daikoboku
|
||||||
|
manufacturer: Seikado
|
||||||
|
date: 2025-11-10 9:21:00
|
||||||
|
time: 9:21 AM
|
||||||
|
---
|
||||||
|
The falling snow outside has put me in the mood for a spicy agarwood stick; this is just the ticket!
|
||||||
@@ -195,12 +195,7 @@ export default async function (eleventyConfig) {
|
|||||||
// Passthrough
|
// Passthrough
|
||||||
eleventyConfig
|
eleventyConfig
|
||||||
.addPassthroughCopy({ "./public/": "/" })
|
.addPassthroughCopy({ "./public/": "/" })
|
||||||
.addPassthroughCopy("./content/special/")
|
.addPassthroughCopy("./content/special/");
|
||||||
.addPassthroughCopy("./node_modules/@magenta/music/es6/core.js")
|
|
||||||
.addPassthroughCopy(
|
|
||||||
"./node_modules/html-midi-player/dist/midi-player.min.js",
|
|
||||||
)
|
|
||||||
.addPassthroughCopy("./node_modules/tone/build/Tone.js");
|
|
||||||
|
|
||||||
// Get the first `n` elements of a collection.
|
// Get the first `n` elements of a collection.
|
||||||
eleventyConfig.addFilter("head", (array, n) => {
|
eleventyConfig.addFilter("head", (array, n) => {
|
||||||
|
|||||||
@@ -604,9 +604,6 @@ pre:not([class*="language-"]) {
|
|||||||
word-spacing: normal;
|
word-spacing: normal;
|
||||||
word-break: normal;
|
word-break: normal;
|
||||||
}
|
}
|
||||||
code {
|
|
||||||
word-break: break-all;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Footnotes */
|
/* Footnotes */
|
||||||
.footnotes-list {
|
.footnotes-list {
|
||||||
|
|||||||
Reference in New Issue
Block a user