Merge pull request #1187 from jonnybarnes/1185-add-some-snow

Add some snow ❄️
This commit is contained in:
Jonny Barnes 2023-12-17 14:36:20 +00:00 committed by GitHub
commit 4123e73ee0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 496 additions and 2 deletions

28
package-lock.json generated
View file

@ -8,6 +8,10 @@
"name": "jbuk-frontend", "name": "jbuk-frontend",
"version": "0.0.1", "version": "0.0.1",
"license": "CC0-1.0", "license": "CC0-1.0",
"dependencies": {
"@11ty/is-land": "^4.0.0",
"@zachleat/snow-fall": "^1.0.2"
},
"devDependencies": { "devDependencies": {
"@babel/core": "^7.23.6", "@babel/core": "^7.23.6",
"@babel/preset-env": "^7.23.6", "@babel/preset-env": "^7.23.6",
@ -35,6 +39,15 @@
"webpack-cli": "^5.1.4" "webpack-cli": "^5.1.4"
} }
}, },
"node_modules/@11ty/is-land": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/@11ty/is-land/-/is-land-4.0.0.tgz",
"integrity": "sha512-RxbjF2+FzSu3rerHrWLRsvsPX2YM47RwXpdWCCzLhwRSsz5sJe9TnK7mphEld1gZnp2GeD5ByvhqjIc4CqidsQ==",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/11ty"
}
},
"node_modules/@aashutoshrathi/word-wrap": { "node_modules/@aashutoshrathi/word-wrap": {
"version": "1.2.6", "version": "1.2.6",
"resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz",
@ -2591,6 +2604,11 @@
"integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==",
"dev": true "dev": true
}, },
"node_modules/@zachleat/snow-fall": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@zachleat/snow-fall/-/snow-fall-1.0.2.tgz",
"integrity": "sha512-nyNeliowryq+roSMktyV3o14DduSuU4BvBzruVSPV6e8U8Eid2zNzSj1AzCQByPId7Q4MrIP2QWL2UHeHGfmcA=="
},
"node_modules/acorn": { "node_modules/acorn": {
"version": "8.9.0", "version": "8.9.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.9.0.tgz", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.9.0.tgz",
@ -7566,6 +7584,11 @@
} }
}, },
"dependencies": { "dependencies": {
"@11ty/is-land": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/@11ty/is-land/-/is-land-4.0.0.tgz",
"integrity": "sha512-RxbjF2+FzSu3rerHrWLRsvsPX2YM47RwXpdWCCzLhwRSsz5sJe9TnK7mphEld1gZnp2GeD5ByvhqjIc4CqidsQ=="
},
"@aashutoshrathi/word-wrap": { "@aashutoshrathi/word-wrap": {
"version": "1.2.6", "version": "1.2.6",
"resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz",
@ -9353,6 +9376,11 @@
"integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==",
"dev": true "dev": true
}, },
"@zachleat/snow-fall": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@zachleat/snow-fall/-/snow-fall-1.0.2.tgz",
"integrity": "sha512-nyNeliowryq+roSMktyV3o14DduSuU4BvBzruVSPV6e8U8Eid2zNzSj1AzCQByPId7Q4MrIP2QWL2UHeHGfmcA=="
},
"acorn": { "acorn": {
"version": "8.9.0", "version": "8.9.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.9.0.tgz", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.9.0.tgz",

View file

@ -39,5 +39,9 @@
"> 1%", "> 1%",
"not IE 11", "not IE 11",
"not IE_Mob 11" "not IE_Mob 11"
] ],
"dependencies": {
"@11ty/is-land": "^4.0.0",
"@zachleat/snow-fall": "^1.0.2"
}
} }

File diff suppressed because one or more lines are too long

Binary file not shown.

View file

@ -0,0 +1,338 @@
class Island extends HTMLElement {
static tagName = "is-land";
static prefix = "is-land--";
static attr = {
template: "data-island",
ready: "ready",
defer: "defer-hydration",
};
static onceCache = new Map();
static onReady = new Map();
static fallback = {
":not(is-land,:defined,[defer-hydration])": (readyPromise, node, prefix) => {
// remove from document to prevent web component init
let cloned = document.createElement(prefix + node.localName);
for(let attr of node.getAttributeNames()) {
cloned.setAttribute(attr, node.getAttribute(attr));
}
// Declarative Shadow DOM (with polyfill)
let shadowroot = node.shadowRoot;
if(!shadowroot) {
let tmpl = node.querySelector(":scope > template:is([shadowrootmode], [shadowroot])");
if(tmpl) {
let mode = tmpl.getAttribute("shadowrootmode") || tmpl.getAttribute("shadowroot") || "closed";
shadowroot = node.attachShadow({ mode }); // default is closed
shadowroot.appendChild(tmpl.content.cloneNode(true));
}
}
// Cheers to https://gist.github.com/developit/45c85e9be01e8c3f1a0ec073d600d01e
if(shadowroot) {
cloned.attachShadow({ mode: shadowroot.mode }).append(...shadowroot.childNodes);
}
// Keep *same* child nodes to preserve state of children (e.g. details->summary)
cloned.append(...node.childNodes);
node.replaceWith(cloned);
return readyPromise.then(() => {
// Restore original children and shadow DOM
if(cloned.shadowRoot) {
node.shadowRoot.append(...cloned.shadowRoot.childNodes);
}
node.append(...cloned.childNodes);
cloned.replaceWith(node);
});
}
}
constructor() {
super();
// Internal promises
this.ready = new Promise(resolve => {
this.readyResolve = resolve;
});
}
// any parents of `el` that are <is-land> (with conditions)
static getParents(el, stopAt = false) {
let nodes = [];
while(el) {
if(el.matches && el.matches(Island.tagName)) {
if(stopAt && el === stopAt) {
break;
}
if(Conditions.hasConditions(el)) {
nodes.push(el);
}
}
el = el.parentNode;
}
return nodes;
}
static async ready(el, parents) {
if(!parents) {
parents = Island.getParents(el);
}
if(parents.length === 0) {
return;
}
let imports = await Promise.all(parents.map(p => p.wait()));
// return innermost module import
if(imports.length) {
return imports[0];
}
}
forceFallback() {
if(window.Island) {
Object.assign(Island.fallback, window.Island.fallback);
}
for(let selector in Island.fallback) {
// Reverse here as a cheap way to get the deepest nodes first
let components = Array.from(this.querySelectorAll(selector)).reverse();
// with thanks to https://gist.github.com/cowboy/938767
for(let node of components) {
if(!node.isConnected) {
continue;
}
let parents = Island.getParents(node);
// must be in a leaf island (not nested deep)
if(parents.length === 1) {
let p = Island.ready(node, parents);
Island.fallback[selector](p, node, Island.prefix);
}
}
}
}
wait() {
return this.ready;
}
async connectedCallback() {
// Only use fallback content with loading conditions
if(Conditions.hasConditions(this)) {
// Keep fallback content without initializing the components
this.forceFallback();
}
await this.hydrate();
}
getTemplates() {
return this.querySelectorAll(`template[${Island.attr.template}]`);
}
replaceTemplates(templates) {
// replace <template> with the live content
for(let node of templates) {
// if the template is nested inside another child <is-land> inside, skip
if(Island.getParents(node, this).length > 0) {
continue;
}
let value = node.getAttribute(Island.attr.template);
// get rid of the rest of the content on the island
if(value === "replace") {
let children = Array.from(this.childNodes);
for(let child of children) {
this.removeChild(child);
}
this.appendChild(node.content);
break;
} else {
let html = node.innerHTML;
if(value === "once" && html) {
if(Island.onceCache.has(html)) {
node.remove();
return;
}
Island.onceCache.set(html, true);
}
node.replaceWith(node.content);
}
}
}
async hydrate() {
let conditions = [];
if(this.parentNode) {
// wait for all parents before hydrating
conditions.push(Island.ready(this.parentNode));
}
let attrs = Conditions.getConditions(this);
for(let condition in attrs) {
if(Conditions.map[condition]) {
conditions.push(Conditions.map[condition](attrs[condition], this));
}
}
// Loading conditions must finish before dependencies are loaded
await Promise.all(conditions);
this.replaceTemplates(this.getTemplates());
for(let fn of Island.onReady.values()) {
await fn.call(this, Island);
}
this.readyResolve();
this.setAttribute(Island.attr.ready, "");
// Remove [defer-hydration]
this.querySelectorAll(`[${Island.attr.defer}]`).forEach(node => node.removeAttribute(Island.attr.defer));
}
}
class Conditions {
static map = {
visible: Conditions.visible,
idle: Conditions.idle,
interaction: Conditions.interaction,
media: Conditions.media,
"save-data": Conditions.saveData,
};
static hasConditions(node) {
return Object.keys(Conditions.getConditions(node)).length > 0;
}
static getConditions(node) {
let map = {};
for(let key of Object.keys(Conditions.map)) {
if(node.hasAttribute(`on:${key}`)) {
map[key] = node.getAttribute(`on:${key}`);
}
}
return map;
}
static visible(noop, el) {
if(!('IntersectionObserver' in window)) {
// runs immediately
return;
}
return new Promise(resolve => {
let observer = new IntersectionObserver(entries => {
let [entry] = entries;
if(entry.isIntersecting) {
observer.unobserve(entry.target);
resolve();
}
});
observer.observe(el);
});
}
// Warning: on:idle is not very useful with other conditions as it may resolve long before.
static idle() {
let onload = new Promise(resolve => {
if(document.readyState !== "complete") {
window.addEventListener("load", () => resolve(), { once: true });
} else {
resolve();
}
});
if(!("requestIdleCallback" in window)) {
// run immediately
return onload;
}
// both idle and onload
return Promise.all([
new Promise(resolve => {
requestIdleCallback(() => {
resolve();
});
}),
onload,
]);
}
static interaction(eventOverrides, el) {
let events = ["click", "touchstart"];
// event overrides e.g. on:interaction="mouseenter"
if(eventOverrides) {
events = (eventOverrides || "").split(",").map(entry => entry.trim());
}
return new Promise(resolve => {
function resolveFn(e) {
resolve();
// cleanup the other event handlers
for(let name of events) {
el.removeEventListener(name, resolveFn);
}
}
for(let name of events) {
el.addEventListener(name, resolveFn, { once: true });
}
});
}
static media(query) {
let mm = {
matches: true
};
if(query && ("matchMedia" in window)) {
mm = window.matchMedia(query);
}
if(mm.matches) {
return;
}
return new Promise(resolve => {
mm.addListener(e => {
if(e.matches) {
resolve();
}
});
});
}
static saveData(expects) {
// return early if API does not exist
if(!("connection" in navigator) || navigator.connection.saveData === (expects !== "false")) {
return;
}
// dangly promise
return new Promise(() => {});
}
}
// Should this auto define? Folks can redefine later using { component } export
if("customElements" in window) {
window.customElements.define(Island.tagName, Island);
window.Island = Island;
}
export {
Island,
Island as component, // Backwards compat only: recommend `Island` export
};
// TODO remove in 4.0
export const ready = Island.ready; // Backwards compat only: recommend `Island` export

Binary file not shown.

View file

@ -0,0 +1,117 @@
class Snow extends HTMLElement {
static random(min, max) {
return min + Math.floor(Math.random() * (max - min) + 1);
}
static attrs = {
count: "count", // default: 100
mode: "mode",
}
generateCss(mode, count) {
let css = [];
css.push(`
:host([mode="element"]) {
display: block;
position: relative;
overflow: hidden;
}
:host([mode="page"]) {
position: fixed;
top: 0;
left: 0;
right: 0;
}
:host([mode="page"]),
:host([mode="element"]) > * {
pointer-events: none;
}
:host([mode="element"]) ::slotted(*) {
pointer-events: all;
}
* {
position: absolute;
width: var(--snow-fall-size, 10px);
height: var(--snow-fall-size, 10px);
background: var(--snow-fall-color, rgba(255,255,255,.5));
border-radius: 50%;
}
`);
// using vw units (max 100)
let dimensions = { width: 100, height: 100 };
let units = { x: "vw", y: "vh"};
if(mode === "element") {
dimensions = {
width: this.firstElementChild.clientWidth,
height: this.firstElementChild.clientHeight,
};
units = { x: "px", y: "px"};
}
// Thank you @alphardex: https://codepen.io/alphardex/pen/dyPorwJ
for(let j = 1; j<= count; j++ ) {
let x = Snow.random(1, 100) * dimensions.width/100; // vw
let offset = Snow.random(-10, 10) * dimensions.width/100; // vw
let yoyo = Math.round(Snow.random(30, 100)); // % time
let yStart = yoyo * dimensions.height/100; // vh
let yEnd = dimensions.height; // vh
let scale = Snow.random(1, 10000) * .0001;
let duration = Snow.random(10, 30);
let delay = Snow.random(0, 30) * -1;
css.push(`
:nth-child(${j}) {
opacity: ${Snow.random(0, 1000) * 0.001};
transform: translate(${x}${units.x}, -10px) scale(${scale});
animation: fall-${j} ${duration}s ${delay}s linear infinite;
}
@keyframes fall-${j} {
${yoyo}% {
transform: translate(${x + offset}${units.x}, ${yStart}${units.y}) scale(${scale});
}
to {
transform: translate(${x + offset / 2}${units.x}, ${yEnd}${units.y}) scale(${scale});
}
}`)
}
return css.join("\n");
}
connectedCallback() {
// https://caniuse.com/mdn-api_cssstylesheet_replacesync
if(this.shadowRoot || !("replaceSync" in CSSStyleSheet.prototype)) {
return;
}
let count = parseInt(this.getAttribute(Snow.attrs.count)) || 100;
let mode;
if(this.hasAttribute(Snow.attrs.mode)) {
mode = this.getAttribute(Snow.attrs.mode);
} else {
mode = this.firstElementChild ? "element" : "page";
this.setAttribute(Snow.attrs.mode, mode);
}
let sheet = new CSSStyleSheet();
sheet.replaceSync(this.generateCss(mode, count));
let shadowroot = this.attachShadow({ mode: "open" });
shadowroot.adoptedStyleSheets = [sheet];
let d = document.createElement("div");
for(let j = 0, k = count; j<k; j++) {
shadowroot.appendChild(d.cloneNode());
}
shadowroot.appendChild(document.createElement("slot"));
}
}
customElements.define("snow-fall", Snow);

Binary file not shown.

View file

@ -30,6 +30,11 @@
<link rel="pgpkey" href="/assets/jonnybarnes-public-key-ecc.asc"> <link rel="pgpkey" href="/assets/jonnybarnes-public-key-ecc.asc">
</head> </head>
<body class="grid"> <body class="grid">
<is-land on:media="(prefers-reduced-motion: no-preference)">
<snow-fall
style="--snow-fall-color: oklch(36.14% 0.224 277.28)"
></snow-fall>
</is-land>
<header id="site-header"> <header id="site-header">
<h1> <h1>
<a rel="author" href="/">{{ config('user.display_name') }}</a> <a rel="author" href="/">{{ config('user.display_name') }}</a>
@ -73,6 +78,8 @@
<!--scripts go here when needed--> <!--scripts go here when needed-->
@section('scripts') @section('scripts')
<script type="module" src="/assets/frontent/is-land.js"></script>
<script type="module" src="/assets/frontend/snow-fall.js"></script>
<script src="/assets/app.js"></script> <script src="/assets/app.js"></script>
@show @show
</body> </body>