All checks were successful
Deploy to SFTP Server / build (push) Successful in 21m44s
214 lines
9.6 KiB
Plaintext
214 lines
9.6 KiB
Plaintext
---
|
|
import LanguagePicker from "../components/LanguagePicker.astro";
|
|
import { AstroFont } from "astro-font";
|
|
import Nav from "../components/Nav.astro";
|
|
import { useTranslations } from "@i18n/utils";
|
|
interface Props {
|
|
title: string;
|
|
width?: "full" | "compact";
|
|
}
|
|
const t = useTranslations(Astro.url);
|
|
|
|
const { title } = Astro.props;
|
|
import "./theme.css";
|
|
import "./global.css";
|
|
---
|
|
|
|
<!doctype html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8" />
|
|
<meta name="description" content="Astro description" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
<meta name="props" content={JSON.stringify(Astro.props)} />
|
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
|
<script defer src="https://umami.max-richter.dev/script.js" data-website-id="bf418035-fd77-48c6-bc0c-f7f5e2bc6522"></script>
|
|
<AstroFont
|
|
config={[
|
|
{
|
|
name: "Nunito Sans",
|
|
src: [
|
|
{
|
|
style: "light",
|
|
weight: "300",
|
|
path: "/fonts/nunito-v26-latin-300.woff2",
|
|
},
|
|
],
|
|
preload: true,
|
|
display: "swap",
|
|
selector: "body",
|
|
fallback: "sans-serif",
|
|
},
|
|
{
|
|
name: "Roboto",
|
|
src: [
|
|
{
|
|
weight: "300",
|
|
style: "normal",
|
|
path: "/fonts/roboto-v30-latin-300.woff2",
|
|
},
|
|
],
|
|
preload: true,
|
|
display: "swap",
|
|
selector: "body > span",
|
|
fallback: "serif",
|
|
},
|
|
]}
|
|
/>
|
|
<meta name="generator" content={Astro.generator} />
|
|
<!-- <meta http-equiv="refresh" content="0;url=/" /> -->
|
|
<title>{title}</title>
|
|
<script is:inline>
|
|
(function () {
|
|
try {
|
|
var mode = localStorage.getItem("theme");
|
|
var supportDarkMode =
|
|
window.matchMedia("(prefers-color-scheme: dark)").matches === true;
|
|
if (!mode && supportDarkMode)
|
|
document.documentElement.classList.add("dark");
|
|
if (!mode) return;
|
|
document.documentElement.classList.add(mode);
|
|
} catch (e) {}
|
|
})();
|
|
</script>
|
|
<style>
|
|
body {
|
|
max-width: 700px;
|
|
margin: 0 auto;
|
|
position: relative;
|
|
}
|
|
main,
|
|
header,
|
|
footer {
|
|
background: var(--background-dark);
|
|
padding: 0.5rem 3rem;
|
|
}
|
|
|
|
body::after {
|
|
content: "";
|
|
position: fixed;
|
|
height: 100vh;
|
|
width: 200px;
|
|
top: 0;
|
|
transform: translateX(-100%);
|
|
background: red;
|
|
background: linear-gradient(
|
|
to left,
|
|
var(--background-dark),
|
|
transparent
|
|
);
|
|
}
|
|
|
|
main::before {
|
|
content: "";
|
|
position: absolute;
|
|
top: -200px;
|
|
right: -200px;
|
|
bottom: -100px;
|
|
width: 200px;
|
|
background: linear-gradient(
|
|
to right,
|
|
var(--background-dark),
|
|
transparent
|
|
);
|
|
}
|
|
|
|
@media (max-width: 700px) {
|
|
body > * {
|
|
padding: 0.5rem 1rem;
|
|
}
|
|
}
|
|
</style>
|
|
</head>
|
|
<body class="text-neutral flex flex-col">
|
|
<header class="sticky top-0 z-2">
|
|
<Nav />
|
|
</header>
|
|
<main id="main-content" class="relative flex flex-col gap-6">
|
|
<slot />
|
|
</main>
|
|
<footer class="mx-8 mb-4 mt-2 flex gap-8">
|
|
<LanguagePicker />
|
|
<a
|
|
href="https://git.max-richter.dev/max/website"
|
|
class="flex gap-2 items-center">
|
|
<span class="i-tabler-brand-git"></span>{t("website-source")}
|
|
</a>
|
|
</footer>
|
|
<script is:inline>
|
|
(function () {
|
|
// prettier-ignore
|
|
function rgbaToDataURL(w, h, rgba) { let row = w * 4 + 1; let idat = 6 + h * (5 + row); let bytes = [ 137, 80, 78, 71, 13, 10, 26, 10, 0, 0, 0, 13, 73, 72, 68, 82, 0, 0, w >> 8, w & 255, 0, 0, h >> 8, h & 255, 8, 6, 0, 0, 0, 0, 0, 0, 0, idat >>> 24, (idat >> 16) & 255, (idat >> 8) & 255, idat & 255, 73, 68, 65, 84, 120, 1, ]; let table = [ 0, 498536548, 997073096, 651767980, 1994146192, 1802195444, 1303535960, 1342533948, -306674912, -267414716, -690576408, -882789492, -1687895376, -2032938284, -1609899400, -1111625188, ]; let a = 1, b = 0; for (let y = 0, i = 0, end = row - 1; y < h; y++, end += row - 1) { bytes.push( y + 1 < h ? 0 : 1, row & 255, row >> 8, ~row & 255, (row >> 8) ^ 255, 0,); for (b = (b + a) % 65521; i < end; i++) { let u = rgba[i] & 255; bytes.push(u); a = (a + u) % 65521; b = (b + a) % 65521; } } bytes.push( b >> 8, b & 255, a >> 8, a & 255, 0, 0, 0, 0, 0, 0, 0, 0, 73, 69, 78, 68, 174, 66, 96, 130,); for (let [start, end] of [ [12, 29], [37, 41 + idat], ]) { let c = ~0; for (let i = start; i < end; i++) { c ^= bytes[i]; c = (c >>> 4) ^ table[c & 15]; c = (c >>> 4) ^ table[c & 15]; } c = ~c; bytes[end++] = c >>> 24; bytes[end++] = (c >> 16) & 255; bytes[end++] = (c >> 8) & 255; bytes[end++] = c & 255; } return "data:image/png;base64," + window.btoa(String.fromCharCode(...bytes)); }
|
|
// prettier-ignore
|
|
function thumbHashToApproximateAspectRatio(hash) { let header = hash[3]; let hasAlpha = hash[2] & 0x80; let isLandscape = hash[4] & 0x80; let lx = isLandscape ? (hasAlpha ? 5 : 7) : header & 7; let ly = isLandscape ? header & 7 : hasAlpha ? 5 : 7; return lx / ly; }
|
|
// prettier-ignore
|
|
function thumbHashToRGBA(hash) { let { PI, min, max, cos, round } = Math; let header24 = hash[0] | (hash[1] << 8) | (hash[2] << 16); let header16 = hash[3] | (hash[4] << 8); let l_dc = (header24 & 63) / 63; let p_dc = ((header24 >> 6) & 63) / 31.5 - 1; let q_dc = ((header24 >> 12) & 63) / 31.5 - 1; let l_scale = ((header24 >> 18) & 31) / 31; let hasAlpha = header24 >> 23; let p_scale = ((header16 >> 3) & 63) / 63; let q_scale = ((header16 >> 9) & 63) / 63; let isLandscape = header16 >> 15; let lx = max(3, isLandscape ? (hasAlpha ? 5 : 7) : header16 & 7); let ly = max(3, isLandscape ? header16 & 7 : hasAlpha ? 5 : 7); let a_dc = hasAlpha ? (hash[5] & 15) / 15 : 1; let a_scale = (hash[5] >> 4) / 15; let ac_start = hasAlpha ? 6 : 5; let ac_index = 0; let decodeChannel = (nx, ny, scale) => { let ac = []; for (let cy = 0; cy < ny; cy++) for (let cx = cy ? 0 : 1; cx * ny < nx * (ny - cy); cx++) ac.push( (((hash[ac_start + (ac_index >> 1)] >> ((ac_index++ & 1) << 2)) & 15) / 7.5 - 1) * scale,); return ac; }; let l_ac = decodeChannel(lx, ly, l_scale); let p_ac = decodeChannel(3, 3, p_scale * 1.25); let q_ac = decodeChannel(3, 3, q_scale * 1.25); let a_ac = hasAlpha && decodeChannel(5, 5, a_scale); let ratio = thumbHashToApproximateAspectRatio(hash); let w = round(ratio > 1 ? 32 : 32 * ratio); let h = round(ratio > 1 ? 32 / ratio : 32); let rgba = new Uint8Array(w * h * 4), fx = [], fy = []; for (let y = 0, i = 0; y < h; y++) { for (let x = 0; x < w; x++, i += 4) { let l = l_dc, p = p_dc, q = q_dc, a = a_dc; for (let cx = 0, n = max(lx, hasAlpha ? 5 : 3); cx < n; cx++) fx[cx] = cos((PI / w) * (x + 0.5) * cx); for (let cy = 0, n = max(ly, hasAlpha ? 5 : 3); cy < n; cy++) fy[cy] = cos((PI / h) * (y + 0.5) * cy); for (let cy = 0, j = 0; cy < ly; cy++) for ( let cx = cy ? 0 : 1, fy2 = fy[cy] * 2; cx * ly < lx * (ly - cy); cx++, j++) l += l_ac[j] * fx[cx] * fy2; for (let cy = 0, j = 0; cy < 3; cy++) { for ( let cx = cy ? 0 : 1, fy2 = fy[cy] * 2; cx < 3 - cy; cx++, j++) { let f = fx[cx] * fy2; p += p_ac[j] * f; q += q_ac[j] * f; } } if (hasAlpha) for (let cy = 0, j = 0; cy < 5; cy++) for ( let cx = cy ? 0 : 1, fy2 = fy[cy] * 2; cx < 5 - cy; cx++, j++) a += a_ac[j] * fx[cx] * fy2; let b = l - (2 / 3) * p; let r = (3 * l - b + q) / 2; let g = r - q; rgba[i] = max(0, 255 * min(1, r)); rgba[i + 1] = max(0, 255 * min(1, g)); rgba[i + 2] = max(0, 255 * min(1, b)); rgba[i + 3] = max(0, 255 * min(1, a)); } } return { w, h, rgba }; }
|
|
|
|
function show(img) {
|
|
img.style.opacity = "1";
|
|
img.style.filter = "blur(0px)";
|
|
setTimeout(() => {
|
|
if (img.parentNode) {
|
|
img.parentNode.style.background = "";
|
|
img.parentNode.classList.remove("thumb-loading");
|
|
}
|
|
}, 600);
|
|
}
|
|
|
|
document.querySelectorAll("[data-thumbhash]").forEach((entry) => {
|
|
const parent = entry?.parentNode;
|
|
const img = entry;
|
|
|
|
if (parent?.nodeName !== "PICTURE") return;
|
|
|
|
const hash = img.getAttribute("data-thumbhash");
|
|
if (!hash) return;
|
|
|
|
if (img.complete || parent.complete) return;
|
|
|
|
// its only browser, why you have to be mad
|
|
const decodedString = window.atob(hash);
|
|
|
|
// Create Uint8Array from decoded string
|
|
const buffer = new Uint8Array(decodedString.length);
|
|
for (let i = 0; i < decodedString.length; i++) {
|
|
buffer[i] = decodedString.charCodeAt(i);
|
|
}
|
|
|
|
const image = thumbHashToRGBA(buffer);
|
|
const dataURL = rgbaToDataURL(image.w, image.h, image.rgba);
|
|
|
|
parent.classList.add("thumb-loading");
|
|
parent.style.background = `url(${dataURL})`;
|
|
parent.style.backgroundSize = "cover";
|
|
|
|
let count = 0;
|
|
const interval = setInterval(() => {
|
|
count++;
|
|
if (count > 20) {
|
|
clearInterval(interval);
|
|
show(img);
|
|
}
|
|
if (img.complete || parent.complete) {
|
|
clearInterval(interval);
|
|
show(img);
|
|
}
|
|
}, 100);
|
|
|
|
img.addEventListener(
|
|
"load",
|
|
() => {
|
|
clearInterval(interval);
|
|
show(img);
|
|
},
|
|
{ once: true },
|
|
);
|
|
|
|
img.style.opacity = "0";
|
|
img.style.transition = "opacity 0.6s ease, filter 0.8s ease";
|
|
});
|
|
})();
|
|
</script>
|
|
</body>
|
|
</html>
|