Compare commits

42 Commits

Author SHA1 Message Date
Max Richter
151f7ae3d0 fix: some stuff
Some checks are pending
Deploy to SFTP Server / build (push) Waiting to run
2025-10-25 13:55:48 +02:00
Max Richter
c96a13c734 feat: trying to optimize builds
All checks were successful
Deploy to SFTP Server / build (push) Successful in 20m53s
2025-10-25 13:34:18 +02:00
Max Richter
22781de3eb fix: revert some of the last changes
All checks were successful
Deploy to SFTP Server / build (push) Successful in 16m52s
2025-10-24 19:44:42 +02:00
Max Richter
71f778671d feat: trying to make image generation faster 2025-10-24 19:39:31 +02:00
Max Richter
9eadefdb51 feat: add external link to articles
All checks were successful
Deploy to SFTP Server / build (push) Successful in 26m59s
2025-10-24 19:12:09 +02:00
Max Richter
83c099873d feat: add external link to articles
Some checks failed
Deploy to SFTP Server / build (push) Failing after 3m13s
2025-10-24 19:08:03 +02:00
Max Richter
cf336da3c4 fix: some stuff
All checks were successful
Deploy to SFTP Server / build (push) Successful in 32m9s
2025-10-24 16:06:16 +02:00
Max Richter
c33691c23c fix: some stuff
Some checks failed
Deploy to SFTP Server / build (push) Failing after 6m46s
2025-10-24 15:53:52 +02:00
Max Richter
1ca044fe13 fix: some stuff
Some checks failed
Deploy to SFTP Server / build (push) Failing after 8m19s
2025-10-24 15:33:41 +02:00
Max Richter
467dcbfd32 fix: some stuff
Some checks failed
Deploy to SFTP Server / build (push) Failing after 14m20s
2025-10-24 15:09:39 +02:00
Max Richter
190edd0915 fix: some stuff
Some checks failed
Deploy to SFTP Server / build (push) Failing after 8m37s
2025-10-24 15:00:08 +02:00
Max Richter
5c1df62fd2 fix: should not error when fetch fails
Some checks failed
Deploy to SFTP Server / build (push) Failing after 6m44s
2025-10-24 14:01:35 +02:00
Max Richter
f52da7f3aa fix: some image loading
Some checks failed
Deploy to SFTP Server / build (push) Failing after 6m29s
2025-10-24 13:46:35 +02:00
Max Richter
6230126d38 fix: some image loading
Some checks failed
Deploy to SFTP Server / build (push) Failing after 1m55s
2025-10-24 13:43:34 +02:00
Max Richter
231bdb09a3 feat: add movies and series to resources
Some checks failed
Deploy to SFTP Server / build (push) Failing after 2m2s
2025-10-24 12:44:46 +02:00
Max Richter
6aa48cc60e fix: remove wrong urls
All checks were successful
Deploy to SFTP Server / build (push) Successful in 15m25s
2025-10-22 22:45:25 +02:00
Max Richter
7d0ccac81f feat: add picos de europa post
Some checks failed
Deploy to SFTP Server / build (push) Failing after 2m23s
2025-10-22 22:32:34 +02:00
Max Richter
c542408f6a fix: handle empty markdown
All checks were successful
Deploy to SFTP Server / build (push) Successful in 10m50s
2025-10-22 18:15:38 +02:00
Max Richter
3b583eade5 fix: handle empty markdown
Some checks failed
Deploy to SFTP Server / build (push) Failing after 4m50s
2025-10-22 17:55:05 +02:00
Max Richter
342f83c783 feat: display articles
Some checks failed
Deploy to SFTP Server / build (push) Failing after 3m59s
2025-10-22 17:43:04 +02:00
Max Richter
ae266dbdc5 feat: display articles
Some checks failed
Deploy to SFTP Server / build (push) Failing after 1m38s
2025-10-22 17:17:34 +02:00
Max Richter
3a120e32fc fix: show date on herocard closes #2 2025-10-22 16:39:23 +02:00
Max Richter
d9a2f63865 feat: show recipe image on page
Some checks failed
Deploy to SFTP Server / build (push) Failing after 1m31s
2025-10-22 16:24:04 +02:00
Max Richter
24a66940e9 fix: images in remotes
All checks were successful
Deploy to SFTP Server / build (push) Successful in 9m22s
2025-10-22 16:00:21 +02:00
Max Richter
2446629515 chore: remove .zone files
Some checks failed
Deploy to SFTP Server / build (push) Has been cancelled
2025-10-22 16:00:03 +02:00
Max Richter
7048db9d76 fix: make image loading more robust
All checks were successful
Deploy to SFTP Server / build (push) Successful in 6m38s
2025-10-22 14:30:49 +02:00
Max Richter
0db489269b fix: image loading stuff
Some checks failed
Deploy to SFTP Server / build (push) Failing after 3m50s
2025-10-22 14:20:34 +02:00
Max Richter
1c3f136f57 fix: make some changes
All checks were successful
Deploy to SFTP Server / build (push) Successful in 17m9s
2025-10-22 13:48:16 +02:00
Max Richter
038e78f27d fix: handle empty recipeInstructions
Some checks failed
Deploy to SFTP Server / build (push) Failing after 3m4s
2025-10-22 13:42:27 +02:00
Max Richter
93c00e1c7e fix: handle of markdownTextTs receives null
Some checks failed
Deploy to SFTP Server / build (push) Failing after 3m7s
2025-10-22 13:37:29 +02:00
Max Richter
c914ee6719 fix: add some debug logs to git lfs pull
Some checks failed
Deploy to SFTP Server / build (push) Failing after 4m21s
2025-10-22 13:31:43 +02:00
Max Richter
9ca190550d fix: add some debug logs to git lfs pull
Some checks are pending
Deploy to SFTP Server / build (push) Waiting to run
2025-10-22 13:17:58 +02:00
Max Richter
edecf0bf75 fix: some stuff
Some checks failed
Deploy to SFTP Server / build (push) Has been cancelled
2025-10-22 13:14:42 +02:00
Max Richter
a27e9046c0 fix: some stuff
Some checks failed
Deploy to SFTP Server / build (push) Has been cancelled
2025-10-22 12:58:18 +02:00
Max Richter
5ba54fee6e fix: make some changes
Some checks failed
Deploy to SFTP Server / build (push) Failing after 8m2s
2025-10-21 19:38:33 +02:00
Max Richter
06e5126fe0 refactor: use more generic resource system
Some checks failed
Deploy to SFTP Server / build (push) Failing after 7m2s
2025-10-06 00:30:44 +02:00
Max Richter
61251e2c85 update
Some checks failed
Deploy to SFTP Server / build (push) Has been cancelled
2025-10-04 14:07:46 +02:00
Max Richter
a1b8eb22e5 fix: some type errors
Some checks failed
Deploy to SFTP Server / build (push) Failing after 6m48s
2025-10-04 13:39:06 +02:00
Max Richter
246fc3ae44 feat: add some stuff
Some checks failed
Deploy to SFTP Server / build (push) Failing after 7m38s
2025-10-04 13:19:19 +02:00
Max Richter
48f451ceb0 Merge branch 'feat/memorium-go' 2025-10-04 13:07:45 +02:00
Max Richter
fba2337b9c feat: load data from marka 2025-10-04 13:07:11 +02:00
546b36f44f Merge pull request 'feat/memorium-go' (#3) from feat/memorium-go into main
All checks were successful
Deploy to SFTP Server / build (push) Successful in 26m2s
Reviewed-on: max/website#3
2025-07-20 14:17:06 +02:00
113 changed files with 5481 additions and 1952 deletions

View File

@@ -20,15 +20,13 @@ jobs:
- name: 🔄 Checkout code
uses: actions/checkout@v3
- name: 🔢 Calculate cache IDs
- name: 🔢 Prepare cache keys
run: |
# Calculate cache IDs for Git LFS and PNPM
# Calculate cache IDs for Git LFS
git lfs ls-files -l | cut -d' ' -f1 | sort > .lfs-assets-id
LFS_CACHE_ID=$(cat .lfs-assets-id | md5sum)-v1
PNPM_CACHE_ID=$(cat pnpm-lock.yaml | md5sum)-v1
echo "LFS_CACHE_ID=$LFS_CACHE_ID" >> $GITHUB_ENV
echo "PNPM_STORE_PATH=$(pnpm store path)" >> $GITHUB_ENV
echo "PNPM_CACHE_ID=$PNPM_CACHE_ID" >> $GITHUB_ENV
- name: 🗄️ Cache Git LFS objects
uses: actions/cache@v4
@@ -41,12 +39,16 @@ jobs:
with:
path: ${{ env.PNPM_STORE_PATH }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
- name: 📷 Cache Astro Images
uses: actions/cache@v4
with:
path: node_modules/.astro
key: ${{ runner.os }}-astro-v1
key: ${{ runner.os }}-astro-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-astro-
- name: 🔄 Pull Git LFS files
run: git lfs pull
@@ -70,8 +72,8 @@ jobs:
- name: 🚀 Deploy Changed Files via rclone
run: |
echo "Uploading _astro assets"
rclone sync --update -v --progress --size-only --stats 2s --stats-one-line ./dist/_astro sftp-remote:${REMOTE_DIR}/_astro --transfers 4
rclone sync --update -v --progress --size-only --fast-list --stats 2s --stats-one-line ./dist/_astro sftp-remote:${REMOTE_DIR}/_astro --transfers 4
echo "Uploading the rest"
rclone sync --update -v --progress --exclude _astro/** --stats 2s --stats-one-line ./dist/ sftp-remote:${REMOTE_DIR} --transfers 4
rclone sync --update -v --progress --exclude _astro/** --fast-list --stats 2s --stats-one-line ./dist/ sftp-remote:${REMOTE_DIR} --transfers 4
env:
REMOTE_DIR: ${{ vars.REMOTE_DIR }}

View File

@@ -1,12 +1,12 @@
import { defineConfig } from 'astro/config';
import { i18n, filterSitemapByDefaultLocale } from "astro-i18n-aut/integration";
import { defineConfig } from "astro/config";
import { filterSitemapByDefaultLocale, i18n } from "astro-i18n-aut/integration";
import sitemap from "@astrojs/sitemap";
import Icons from 'unplugin-icons/vite'
import mdx from '@astrojs/mdx';
import glsl from 'vite-plugin-glsl';
import Icons from "unplugin-icons/vite";
import mdx from "@astrojs/mdx";
import glsl from "vite-plugin-glsl";
import svelte from "@astrojs/svelte";
import UnoCSS from 'unocss/astro'
import UnoCSS from "unocss/astro";
const defaultLocale = "de";
const locales = {
@@ -14,7 +14,7 @@ const locales = {
de: "de",
};
const DEFAULT_LAYOUT = '@layouts/Post.astro';
const DEFAULT_LAYOUT = "@layouts/Post.astro";
function setDefaultLayout() {
return function(_, file) {
@@ -27,6 +27,9 @@ export default defineConfig({
site: "https://max-richter.dev",
trailingSlash: "never",
prefetch: true,
image: {
remotePatterns: [{ protocol: "https" }],
},
build: {
format: "file",
},
@@ -34,30 +37,30 @@ export default defineConfig({
plugins: [
glsl(),
Icons({
compiler: 'svelte',
compiler: "svelte",
}),
],
server: {
watch: {
// Customize watch behavior to reduce file watchers
ignored: ['**/node_modules/**', '**/dist/**', '**/.git/**'],
usePolling: process.env.NODE_ENV === 'production',
ignored: ["**/node_modules/**", "**/dist/**", "**/.git/**"],
usePolling: process.env.NODE_ENV === "production",
},
},
},
markdown: {
remarkPlugins: [setDefaultLayout]
remarkPlugins: [setDefaultLayout],
},
integrations: [
i18n({
exclude: ["pages/**/*.json.ts", "pages/api/**/*",],
exclude: ["pages/**/*.json.ts", "pages/api/**/*"],
locales,
defaultLocale,
}),
mdx(),
svelte(),
UnoCSS({
injectReset: true
injectReset: true,
}),
sitemap({
i18n: {
@@ -66,5 +69,5 @@ export default defineConfig({
},
filter: filterSitemapByDefaultLocale({ defaultLocale }),
}),
]
],
});

View File

@@ -10,33 +10,33 @@
"astro": "astro"
},
"dependencies": {
"@astrojs/check": "^0.9.4",
"@astrojs/mdx": "^4.3.1",
"@astrojs/svelte": "^7.1.0",
"@astrojs/check": "^0.9.5",
"@astrojs/mdx": "^4.3.7",
"@astrojs/svelte": "^7.2.0",
"@astrojs/tailwind": "^6.0.2",
"astro": "^5.12.0",
"astro": "^5.14.8",
"astro-i18n-aut": "^0.7.3",
"exifreader": "^4.31.1",
"svelte": "^5.36.10",
"svelte-gestures": "^5.1.4",
"tailwindcss": "^4.1.11",
"exifreader": "^4.32.0",
"svelte": "^5.39.8",
"svelte-gestures": "^5.2.2",
"tailwindcss": "^4.1.14",
"thumbhash": "^0.1.1",
"typescript": "^5.8.3"
"typescript": "^5.9.3"
},
"devDependencies": {
"@astrojs/sitemap": "^3.4.1",
"@iconify-json/tabler": "^1.2.19",
"@astrojs/sitemap": "^3.6.0",
"@iconify-json/tabler": "^1.2.23",
"@types/markdown-it": "^14.1.2",
"@unocss/preset-icons": "^66.3.3",
"@unocss/reset": "^66.3.3",
"@unocss/preset-icons": "^66.5.2",
"@unocss/reset": "^66.5.2",
"astro-font": "^1.1.0",
"markdown-it": "^14.1.0",
"ogl": "^1.0.11",
"prettier": "^3.6.2",
"prettier-plugin-astro": "^0.14.1",
"sharp": "^0.34.3",
"unocss": "^66.3.3",
"unplugin-icons": "^22.1.0",
"vite-plugin-glsl": "^1.5.1"
"sharp": "^0.34.4",
"unocss": "^66.5.2",
"unplugin-icons": "^22.4.2",
"vite-plugin-glsl": "^1.5.4"
}
}

2556
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,5 @@
---
import markdownToText from "@helpers/markdownToText";
import { markdownToText, readDuration } from "@helpers/markdown";
import { Card } from "./card";
import { useTranslatedPath, useTranslations } from "@i18n/utils";
import Image from "@components/Image.astro";
@@ -15,7 +15,7 @@ interface Props {
}
const {
data: { title, cover, icon },
data: { title, cover, icon, date, rating, author },
collection,
body,
id,
@@ -30,14 +30,24 @@ const link = translatePath(`/${collection}/${id.split("/")[0]}`);
<Card
classes={`grid gradient border-1 border-neutral overflow-hidden ${cover ? "grid-rows-[200px_1fr] xs:grid-rows-none xs:grid-cols-[1fr_200px]" : ""}`}>
<Card.Content classes="px-8 py-7 order-last xs:order-first">
{
(date || body || rating !== undefined) && (
<Card.Metadata
date={date}
readDuration={readDuration(body)}
rating={rating}
author={author}
/>
)
}
<Card.Title classes="text-4xl flex items-center gap-2">
{
icon &&
(
icon?.length > 5
? <img class="h-6 w-6" src={icon} />
: <span class="p-r-4 text-md">{icon}</span>
)
icon &&
(icon?.length > 5 ? (
<img class="h-6 w-6" src={icon} />
) : (
<span class="p-r-4 text-md">{icon}</span>
))
}
{title}
</Card.Title>
@@ -55,6 +65,7 @@ const link = translatePath(`/${collection}/${id.split("/")[0]}`);
src={cover as ImageMetadata}
alt={"cover for " + title}
class="right-0 h-full object-cover object-center rounded-none border-l border-neutral"
thumbnail
/>
</a>
)

View File

@@ -1,9 +1,10 @@
---
import type { ImageMetadata } from "astro";
import { Picture as AstroImage } from "astro:assets";
import { generateThumbHash, getExifData } from "@helpers/image";
import { inferRemoteSize } from "astro/assets/utils";
import { getProcessedImage } from "@helpers/image";
interface Props {
src: ImageMetadata & { fsPath?: string };
src: ImageMetadata & { fsPath?: string; src?: string };
alt: string;
pictureClass?: string;
class?: string;
@@ -11,6 +12,27 @@ interface Props {
hash?: boolean;
loader?: boolean;
maxWidth?: number;
thumbnail?: boolean;
}
async function checkImage(image: ImageMetadata) {
const src = typeof image === "string" ? image : image.src;
if (!src) return false;
try {
if (src.startsWith("/@fs") || src.startsWith("/_astro")) return true;
const res = await inferRemoteSize(src);
if (res.format) {
image.format = res.format;
return true;
} else {
console.log("Failed to load: ", src);
}
return false;
} catch (err) {
console.log("\n");
console.log("Failed to fetch: ", src);
return false;
}
}
const {
@@ -20,13 +42,16 @@ const {
hash = true,
alt,
maxWidth,
thumbnail = false,
} = Astro.props;
let thumbhash = hash && image.fsPath ? await generateThumbHash(image) : "";
const imageOk = await checkImage(image);
let exif = await getExifData(image);
const { thumbhash, exif } = imageOk
? await getProcessedImage(image)
: { thumbhash: undefined, exif: undefined };
const sizes = [
const definedSizes = [
{
width: 240,
media: "(max-width: 360px)",
@@ -42,21 +67,30 @@ const sizes = [
{
width: image.width,
},
].filter((size) => !maxWidth || size.width <= maxWidth);
];
const sizes = thumbnail
? [definedSizes[0]]
: definedSizes.filter((size) => !maxWidth || size.width <= maxWidth);
---
<AstroImage
src={image}
alt={alt}
data-thumbhash={thumbhash}
data-exif={JSON.stringify(exif)}
pictureAttributes={{
class: `${hash ? "block h-full relative" : ""} ${loader ? "thumb" : ""} ${pictureClass}`,
}}
class={Astro.props.class}
widths={sizes.map((size) => size.width)}
sizes={sizes
.map((size) => `${size.media || "100vw"} ${size.width}px`)
.join(", ")}>
<slot />
</AstroImage>
{
imageOk ? (
<AstroImage
src={image}
alt={alt}
data-thumbhash={thumbhash}
data-exif={JSON.stringify(exif)}
inferSize={true}
pictureAttributes={{
class: `${hash ? "block h-full relative" : ""} ${loader ? "thumb" : ""} ${pictureClass}`,
}}
class={Astro.props.class}
widths={sizes.map((size) => size.width)}
sizes={sizes
.map((size) => `${size.media || "100vw"} ${size.width}px`)
.join(", ")}>
<slot />
</AstroImage>
) : undefined
}

View File

@@ -16,7 +16,7 @@
let progress: number[] = [];
let currentIndex = -1;
const maxZoom = 5;
import { swipe } from "svelte-gestures";
import { useSwipe } from "svelte-gestures";
const mod = (a: number, b: number) => ((a % b) + b) % b;
@@ -232,10 +232,9 @@
{#if currentIndex > -1}
<div
{...useSwipe(handleSwipe)}
class="image"
use:swipe
role="dialog"
on:swipe={handleSwipe}
on:wheel|passive={handleScroll}
on:mousemove={handleMouseMove}
on:pointermove={handlePointerMove}>

View File

@@ -14,14 +14,13 @@
function show(img: HTMLPictureElement) {
img.classList.add("active");
const _img = img.querySelector("img");
const _img = img.querySelector("img") || img;
if (!_img) return;
_img.addEventListener("load", () => {
img.classList.remove("thumb-loading");
_img.style.opacity = "1";
});
if (_img?.alt) altText = _img.alt;
else altText = "";
altText = _img["alt"] ?? _img.getAttribute("alt") ?? "";
height = _img.getBoundingClientRect().height;
setTimeout(() => {
height = _img.getBoundingClientRect().height;
@@ -58,25 +57,21 @@
class:title
class:not-loaded={!loaded}
class:loaded
style={`--height:${height}px`}
>
style={`--height:${height}px`}>
{#if title}
<div class="flex items-center p-x-4 p-y-6 bg justify-between">
<h3>{title}</h3>
<div
class="overflow-hidden rounded-md bg-light gap-2 flex p-2 border border-light"
>
class="overflow-hidden rounded-md bg-light gap-2 flex p-2 border border-light">
<button
class="flex-1 i-tabler-arrow-left"
aria-label="previous image"
on:click={() => setIndex(index - 1)}
/>
on:click={() => setIndex(index - 1)} />
<button
class="flex-1 i-tabler-arrow-right"
aria-label="next image"
on:click={() => setIndex(index + 1)}
/>
on:click={() => setIndex(index + 1)} />
</div>
</div>
{/if}

View File

@@ -1,13 +1,20 @@
---
interface Props {
title: string;
cover: string;
cover?: string;
description?: string;
}
const {
title,
cover,
description = "A personal blog and portfolio by Max Richter.",
} = Astro.props;
---
<meta property="og:title" content={Astro.props.title} />
<meta property="og:title" content={title} />
<meta property="og:type" content="website" />
<meta property="og:url" content={Astro.url} />
<meta property="og:image" content={Astro.props.cover} />
<meta property="og:description" content="Max Richters personal blog" />
{cover && <meta property="og:image" content={cover} />}
<meta property="og:description" content={description} />
<meta property="og:site_name" content="Max Richter" />

View File

@@ -1,5 +1,5 @@
---
import markdownToText from "@helpers/markdownToText";
import { markdownToText } from "@helpers/markdown";
import { useTranslatedPath } from "@i18n/utils";
import type { InferEntrySchema } from "astro:content";

View File

@@ -0,0 +1,61 @@
<script lang="ts">
export let date: string | Date;
export let readDuration: number | undefined;
export let rating: string | number | undefined;
export let author: string | undefined;
const toDate = (d: string | Date) =>
typeof d === "string" ? new Date(d) : d;
const iso = (d: string | Date) => {
if(!d) return ""
const v = toDate(d);
if(!v?.getTime) return ""
return isNaN(v.getTime()) ? "" : v.toISOString();
};
function formatRating(rating: string | number) {
if (typeof rating === "number") {
return "⭐".repeat(rating);
}
return rating;
}
const formatDate = (d: string | Date) => {
try {
return new Intl.DateTimeFormat("de-DE", {
day: "2-digit",
month: "long",
year: "numeric",
}).format(toDate(d));
} catch (err) {
return "";
}
};
</script>
<div class="flex gap-3 wrapper">
{#if rating}
<div class="text-sm bg-light">{formatRating(rating)}</div>
{/if}
{#if date}
<time class="text-sm bg-light" datetime={iso(date)}
>{formatDate(date)}</time>
{/if}
{#if readDuration > 1}
<div class="text-sm bg-light">{readDuration} mins read</div>
{/if}
{#if author}
<div class="text-sm bg-light">{author}</div>
{/if}
</div>
<style>
.wrapper > * {
padding: 2px 11px;
border-radius: 14px;
font-size: 11px;
opacity: 0.7;
}
</style>

View File

@@ -1,19 +1,22 @@
import Wrapper from './Wrapper.svelte';
import Image from './Image.svelte';
import Content from './Content.svelte';
import Title from './Title.svelte';
import Description from './Description.svelte';
import ReadMoreButton from './ReadMoreButton.svelte';
import Wrapper from "./Wrapper.svelte";
import Image from "./Image.svelte";
import Content from "./Content.svelte";
import Title from "./Title.svelte";
import Description from "./Description.svelte";
import ReadMoreButton from "./ReadMoreButton.svelte";
import Metadata from "./Metadata.svelte";
const Card = Wrapper as typeof Wrapper & {
Image: typeof Image;
Metadata: typeof Metadata;
Content: typeof Content;
Title: typeof Title;
Description: typeof Description;
ReadMoreButton: typeof ReadMoreButton;
}
};
Card.Image = Image;
Card.Metadata = Metadata;
Card.Content = Content;
Card.Title = Title;
Card.Description = Description;

View File

@@ -0,0 +1,24 @@
---
import * as memorium from "@helpers/memorium";
import { markdownToHtml } from "@helpers/markdown";
import Image from "@components/Image.astro";
import type { ImageMetadata } from "astro";
const { resource } = Astro.props;
---
<h1 class="text-4xl">{resource?.content?.headline}</h1>
<div>
{
resource?.content?.image && (
<Image
hash
src={{ src: memorium.getImageUrl(resource.content.image) } as ImageMetadata}
alt="Cover for {resource?.content?.name}"
class="rounded-2xl overflow-hidden"
pictureClass="rounded-2xl"
/>
)
}
</div>
<div set:html={markdownToHtml(resource?.content?.articleBody)} />

View File

@@ -0,0 +1,25 @@
---
import { Code } from 'astro:components';
import Recipe from "./Recipe.astro";
import Article from "./Article.astro";
import Review from "./Review.astro";
const { resource } = Astro.props;
const type = resource?.content?._type ?? "unknown";
---
{type === "Recipe" && <Recipe resource={resource} />}
{type === "Article" && <Article resource={resource} />}
{type === "Review" && <Review resource={resource} />}
{
type === "unknown" && (
<div>
<h3>Unknown resource type</h3>
</div>
)
}
<details >
<summary class="flex"><span class="i-tabler-code w-6 h-6 inline"/></summary>
<Code code={JSON.stringify(resource??"{}", null, 2)} lang="json" theme="dark-plus" />
</details>

View File

@@ -0,0 +1,33 @@
---
import * as memorium from "@helpers/memorium";
import { markdownToHtml } from "@helpers/markdown";
import Image from "@components/Image.astro";
import type { ImageMetadata } from "astro";
const { resource } = Astro.props
const ingredients = resource?.content?.recipeIngredient || [];
const instructions = resource?.content?.recipeInstructions || [];
---
<h1 class="text-4xl">{resource?.content?.name}</h1>
<div>
{resource?.content?.image && <Image hash src={{src: memorium.getImageUrl(resource.content.image)} as ImageMetadata} alt="Cover for {resource?.content?.name}" class="rounded-2xl overflow-hidden" pictureClass="rounded-2xl" />}
</div>
<p>{resource?.content?.description}</p>
<h2 class="text-2xl">Ingredients</h2>
<ul>
{
ingredients.map((ingredient: string) => (
<li set:html={markdownToHtml(ingredient)}/>
))
}
</ul>
<h2 class="text-2xl">Steps</h2>
<ol>
{
instructions.map((ingredient: string) => (
<li set:html={markdownToHtml(ingredient)}/>
))
}
</ol>

View File

@@ -0,0 +1,28 @@
---
import * as memorium from "@helpers/memorium";
import { markdownToHtml } from "@helpers/markdown";
import Image from "@components/Image.astro";
import type { ImageMetadata } from "astro";
const { resource } = Astro.props;
---
<div>
{
resource?.content?.image && (
<Image
hash
src={
{ src: memorium.getImageUrl(resource.content.image) } as ImageMetadata
}
alt="Cover for {resource?.content?.name}"
class="rounded-2xl overflow-hidden"
pictureClass="rounded-2xl w-1/2 mr-4 mb-4 float-left"
/>
)
}
<h1 class="text-4xl mb-4">{resource?.content?.itemReviewed?.name || "Unknown Name"}</h1>
{ resource?.content?.reviewRating?.ratingValue !== undefined && <div>{resource?.content?.reviewRating?.ratingValue}</div>}
<div set:html={markdownToHtml(resource?.content?.reviewBody)} />
</div>

View File

@@ -1,33 +1,41 @@
import { glob } from 'astro/loaders';
import { defineCollection, z, type ImageFunction } from 'astro:content';
const defaultSchema = ({ image }: { image: ImageFunction }) => z.object({
title: z.string(),
date: z.date(),
cover: image().optional(),
links: z.array(z.array(z.string())).optional(),
coverAlt: z.string().optional(),
toc: z.boolean().optional(),
description: z.string().optional(),
icon: z.string().optional(),
draft: z.boolean().optional(),
featured: z.boolean().optional(),
tags: z.array(z.string()).optional(),
_layout: z.enum(['normal', 'transparent']).optional(),
})
import { glob } from "astro/loaders";
import { defineCollection, type ImageFunction, z } from "astro:content";
const defaultSchema = ({ image }: { image: ImageFunction }) =>
z.object({
title: z.string(),
date: z.date(),
cover: image().optional(),
links: z.array(z.array(z.string())).optional(),
rating: z.union([z.string(), z.number()]).optional(),
coverAlt: z.string().optional(),
toc: z.boolean().optional(),
description: z.string().optional(),
icon: z.string().optional(),
author: z.string().optional(),
draft: z.boolean().optional(),
featured: z.boolean().optional(),
tags: z.array(z.string()).optional(),
_layout: z.enum(["normal", "transparent"]).optional(),
});
export const collections = {
'blog': defineCollection({
loader: glob({ pattern: '**/[^_]*.{md,mdx}', base: "./src/content/blog" }),
"blog": defineCollection({
loader: glob({ pattern: "**/[^_]*.{md,mdx}", base: "./src/content/blog" }),
schema: defaultSchema,
}),
"projects": defineCollection({
loader: glob({ pattern: '**/[^_]*.{md,mdx}', base: "./src/content/projects" }),
loader: glob({
pattern: "**/[^_]*.{md,mdx}",
base: "./src/content/projects",
}),
schema: defaultSchema,
}),
"photos": defineCollection({
loader: glob({ pattern: '**/[^_]*.{md,mdx}', base: "./src/content/photos" }),
loader: glob({
pattern: "**/[^_]*.{md,mdx}",
base: "./src/content/photos",
}),
schema: defaultSchema,
})
}),
};

View File

@@ -1,4 +0,0 @@
[ZoneTransfer]
ZoneId=3
ReferrerUrl=https://ezgif.com/gif-to-webm/ezgif-1-af9e72fcc6.gif
HostUrl=https://ezgif.com/save/ezgif-1-78ce3365b7.webm

View File

@@ -1,4 +0,0 @@
[ZoneTransfer]
ZoneId=3
ReferrerUrl=https://ezgif.com/gif-to-webm/ezgif-1-74cf771d87.gif
HostUrl=https://ezgif.com/save/ezgif-1-93f790072e.webm

View File

@@ -1,4 +0,0 @@
[ZoneTransfer]
ZoneId=3
ReferrerUrl=https://ezgif.com/gif-to-webm/ezgif-1-70f1c50104.gif
HostUrl=https://ezgif.com/save/ezgif-1-28f4d917d4.webm

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 MiB

View File

@@ -0,0 +1,131 @@
---
title: Picos de Europa
date: 2025-10-25
license: "CC-BY-SA:4.0"
comments: true
icon: 🏔️
cover: ./images/20250527_125228.jpg
tags: ["picos-de-europa","spain","hiking", "travel"]
---
Unsorted pictures from our hike through the Picos de Europa.
import Image from "@components/Image.astro";
import ImageGallery from "@components/ImageGallery.svelte"
import ImageSlider from "@components/ImageSlider.svelte"
<ImageGallery client:load/>
## May 27th
First day, arrival at the sea.
import image1 from "images/20250527_125228.jpg"
import image16 from "images/PXL_20250527_101057540.MP.jpg"
import image17 from "images/PXL_20250527_100728883.jpg"
<ImageSlider title="Hike" client:load>
<Image src={image1} alt="A cow on a meadow in front of the sea"/>
<Image src={image17} alt="A person on flysch rock in front of a cave at the sea"/>
<Image src={image16} alt="A person in a cave from which you can see the sea"/>
</ImageSlider>
## May 28th
First day of hiking
import image15 from "images/PXL_20250528_121633744.MP.jpg"
import image19 from "images/20250528_164715.jpg"
<ImageSlider title="Hike" client:load>
<Image src={image15} alt="Us after the first 300 meters"/>
<Image src={image19} alt="Many mountain goats that like to lick the salt from the skin"/>
</ImageSlider>
## May 29th
Hard ascent
import videoUrl1 from "images/PXL_20250529_125633973_small_x265.mp4"
import image12 from "images/PXL_20250530_082919731.jpg"
import image13 from "images/PXL_20250529_201559403.jpg"
<video src={videoUrl1} controls alt=""/>
<Image alt="" src={image12} alt="Finally arrived at the Refugio"/>
## May 30-31
Thunderstorm and rest day
import image11 from "images/PXL_20250530_110041174.jpg"
import image10 from "images/PXL_20250530_135631186.MP.jpg"
import image8 from "images/PXL_20250530_170114907.MP.jpg"
import image9 from "images/PXL_20250530_141432767.jpg"
import image18 from "images/IMG-20250531-WA0020.jpeg"
<ImageSlider title="Thunderstorm and rest day" client:load>
<Image src={image11} alt="Departure to the mountains"/>
<Image src={image10} alt="Surprisingly saved under the tarp after a thunderstorm"/>
<Image src={image9} alt="Second departure to the mountains"/>
<Image src={image8} alt="Waiting for the second thunderstorm"/>
<Image src={image18} alt="Arrival at the Refugio"/>
</ImageSlider>
## June 1st
We continue as a pair.
import videoUrl2 from "images/PXL_20250601_044050514_small_x265.mp4"
import image3 from "images/PXL_20250601_124002445.jpg"
import image4 from "images/PXL_20250601_095307910.jpg"
import image5 from "images/PXL_20250601_082528934.jpg"
import image2 from "images/PXL_20250601_132646197.jpg"
<video src={videoUrl2} controls alt="14"/>
<ImageSlider title="Over the peaks" client:load>
<Image src={image4} alt="First ascent-done, about 1000 meters in altitude"/>
<Image src={image5} alt="In the middle of the mountain basin"/>
<Image src={image3} alt="On the descent"/>
<Image src={image2} alt="Immersed in the fog"/>
</ImageSlider>
## June 2nd
Tarping in the forest and wet-hiking then drying out in the hotel room
import image21 from "images/PXL_20250602_064221551.jpg"
import videoUrl3 from "images/PXL_20250602_064236132_small_x265.mp4"
<Image alt="" src={image21} alt="20"/>
<video src={videoUrl3} controls alt=""/>
## June 3rd
Ascent to lofty heights
import image22 from "images/PXL_20250603_083830771.jpg"
import image23 from "images/PXL_20250603_093606665.jpg"
import image25 from "images/PXL_20250603_194635918.jpg"
import image26 from "images/PXL_20250603_194636284.jpg"
<ImageSlider title="Over the peaks" client:load>
<Image src={image22} alt="The ascent was completely in the fog"/>
<Image src={image23} alt="Then came the first snowfields"/>
<Image src={image25} alt="Arrived at the Refugio"/>
<Image src={image26} alt="Our camp for the night"/>
</ImageSlider>
## June 4th
Overnight stay at Refugio Los Cabrones
## June 5th
Final-descent
import image27 from "images/PXL_20250605_100650266.jpg"
<Image alt="" src={image27} alt="27"/>

View File

@@ -0,0 +1,130 @@
---
title: Picos de Europa
date: 2025-10-25
license: "CC-BY-SA:4.0"
comments: true
icon: 🏔️
cover: ./images/20250527_125228.jpg
tags: ["picos-de-europa","spain","hiking", "travel"]
---
Unsortierte Bilder aus unserer Wanderung durch die Picos de Europa.
import Image from "@components/Image.astro";
import ImageGallery from "@components/ImageGallery.svelte"
import ImageSlider from "@components/ImageSlider.svelte"
<ImageGallery client:load/>
## 27. May
Erster Tag, Ankunft am Meer.
import image1 from "images/20250527_125228.jpg"
import image16 from "images/PXL_20250527_101057540.MP.jpg"
import image17 from "images/PXL_20250527_100728883.jpg"
<ImageSlider title="Wanderung" client:load>
<Image src={image1} alt="Bild einer Kuh auf einer Wiese vor dem Meer"/>
<Image src={image17} alt="Person auf flysch gestein vor einer Höhle am Meer"/>
<Image src={image16} alt="Bild von Person in einer Höhle aus der man das Meer sieht"/>
</ImageSlider>
## 28. May
Erster Wandertag
import image15 from "images/PXL_20250528_121633744.MP.jpg"
import image19 from "images/20250528_164715.jpg"
<ImageSlider title="Wanderung" client:load>
<Image src={image15} alt="Wir nach den ersten 300 Metern"/>
<Image src={image19} alt="Viele Bergziegen die gerne das Salz von der Haut lecken"/>
</ImageSlider>
## 29. May
Harter Aufstieg
import videoUrl1 from "images/PXL_20250529_125633973_small_x265.mp4"
import image12 from "images/PXL_20250530_082919731.jpg"
import image13 from "images/PXL_20250529_201559403.jpg"
<video src={videoUrl1} controls alt=""/>
<Image alt="" src={image12} alt="Endlich am Refugio angekommen"/>
## 30-31. May
Gewitter und Restday
import image11 from "images/PXL_20250530_110041174.jpg"
import image10 from "images/PXL_20250530_135631186.MP.jpg"
import image8 from "images/PXL_20250530_170114907.MP.jpg"
import image9 from "images/PXL_20250530_141432767.jpg"
import image18 from "images/IMG-20250531-WA0020.jpeg"
<ImageSlider title="Gewitter und Restday" client:load>
<Image src={image11} alt="Aufbruch in die Berge"/>
<Image src={image10} alt="Überrascht nach einem Gewitter unters Tarp gerettet"/>
<Image src={image9} alt="Zweiter Aufbruch in die Berge"/>
<Image src={image8} alt="Zweites Gewitter abwarten"/>
<Image src={image18} alt="Ankommen am Refugio"/>
</ImageSlider>
## 1. Juni
Es geht zu zweit weiter.
import videoUrl2 from "images/PXL_20250601_044050514_small_x265.mp4"
import image3 from "images/PXL_20250601_124002445.jpg"
import image4 from "images/PXL_20250601_095307910.jpg"
import image5 from "images/PXL_20250601_082528934.jpg"
import image2 from "images/PXL_20250601_132646197.jpg"
<video src={videoUrl2} controls alt="14"/>
<ImageSlider title="Über die Gipfel" client:load>
<Image src={image4} alt="Erster Aufstieg geschafft, circa 1000 Höhenmeter"/>
<Image src={image5} alt="Mittem im Bergkessel"/>
<Image src={image3} alt="Auf dem Abstieg"/>
<Image src={image2} alt="Eingetaucht in den Nebel"/>
</ImageSlider>
## 2. Juni
Tarpen im Wald und nass-wandern dann Austrocken im Hotelzimmer
import image21 from "images/PXL_20250602_064221551.jpg"
import videoUrl3 from "images/PXL_20250602_064236132_small_x265.mp4"
<Image alt="" src={image21} alt="20"/>
<video src={videoUrl3} controls alt=""/>
## 3. Juni
Aufstieg in lichte Höhen
import image22 from "images/PXL_20250603_083830771.jpg"
import image23 from "images/PXL_20250603_093606665.jpg"
import image25 from "images/PXL_20250603_194635918.jpg"
import image26 from "images/PXL_20250603_194636284.jpg"
<ImageSlider title="Über die Gipfel" client:load>
<Image src={image22} alt="Der Aufstieg war komplett im Nebel"/>
<Image src={image23} alt="Dann kamen die ersten Schneefelder"/>
<Image src={image25} alt="Angekommen am Refugio"/>
<Image src={image26} alt="Unser Camp für die Nacht"/>
</ImageSlider>
## 4. Juni
Übernachten im Refugio Los Cabrones
## 5. Juni
Finaler Abstieg
import image27 from "images/PXL_20250605_100650266.jpg"
<Image alt="" src={image27} alt="27"/>

View File

@@ -22,8 +22,6 @@ import ImageSlider from '@components/ImageSlider.svelte'
import Image from '@components/Image.astro'
# Einleitung
In meiner Freizeit übernehme ich gerne kleinere Aufträge und erledige Botengänge, Aufbauten und Abholungen für andere.
Ein unvermeidlicher Bestandteil dieser Tätigkeiten ist das Erstellen von Rechnungen im PDF-Format. Anfangs habe ich mich dem manuellen Prozess hingegeben und die ersten Rechnungen in Figma erstellt. Doch wie es unter Programmierer*innen oft heißt:

View File

@@ -26,8 +26,6 @@ import ImageGallery from "@components/ImageGallery.svelte"
> K.A.R.L ist eine WebApp die einem dabei hilft 360Grad Panoramas in Sektionen einzuteilen, (Himmel, Boden, Bäume usw...) und dann den Anteil der einzelnen Sektionen am Gesamtbild festzustellen.
# Einleitung
Das Projekt ist aus der Zusammenarbeit mit zwei Freunden entstanden. Der eine steckt gerade mitten in der Konzeptionsphase seiner Bachelorarbeit (Geographie), die sich mit der Auswirkung von Vegetation auf das Stadtklima beschäftigt. Dazu hat er an verschiedenen Orten in Köln Albedo Messungen vorgenommen, also quasi "wieviel Licht kommt vom Himmel, und wieviel davon wird vom Boden reflektiert". Um diese Messungen in den richtigen Kontext zu setzen hat er von jedem Messort 360 Panoramas angelegt, diese sehen ungefähr so aus:
<Image src={Crosswalk} alt="Image of a crosswalk" caption="bild von hdrihaven.com" />

View File

@@ -1,3 +0,0 @@
[ZoneTransfer]
ZoneId=3
HostUrl=about:internet

View File

@@ -10,8 +10,6 @@ draft: false
toc: true
---
# Einführung
Plantarium ist die Schnittmenge zwischen zwei Dingen die ich sehr faszinierend finde, Pflanzen und 3D Modellierung.
Es ist eine WebApp die es Nutzern ermöglicht Pflanzen zu erstellen und zu exportieren.
Die User legen dabei über ein Node-System fest wie die Pflanze aussieht und Plantarium generiert daraus ein 3D Modell.

View File

@@ -0,0 +1,944 @@
<div class="wrapper">
<div class="invert-x">
<svg
width="262"
height="261"
viewBox="0 0 262 261"
fill="none"
xmlns="http://www.w3.org/2000/svg">
<g id="mask">
<path
id="Vector 28"
d="M35 165C39.5 160 42 160.5 44 162C46 163.5 46 170 41.5 169C40.6667 168.833 39.1 168.1 39.5 166.5"
stroke-linecap="round" />
<path
id="Vector 29"
d="M44 153.032C48.5 148.032 52.7137 149.856 54 152C57 157 52.5 161 50.5 161C48.5 161 46.5 160 46.5 157.5C46.5 155 50.4 153.5 50 157.5"
stroke-linecap="round" />
<path
id="Vector 30"
d="M51 150C57 138.5 64.7137 143.356 66 145.5C69 150.5 63.5 154.171 61.5 154.171C59.5 154.171 57.5 153.171 57.5 150.671C57.5 148.171 61.5 146.5 61.5 150"
stroke-linecap="round" />
<path
id="Vector 31"
d="M65 143.939C72.1803 129.985 81.4115 135.878 82.9507 138.479C86.5409 144.546 79.9589 149 77.5655 149C75.1721 149 72.7787 147.787 72.7787 144.753C72.7787 141.72 77.5655 139.692 77.5655 143.939"
stroke-linecap="round" />
<path
id="Vector 32"
d="M83 139.216C90.1803 123.269 99.4115 130.003 100.951 132.976C104.541 139.91 97.9589 145 95.5655 145C93.1721 145 90.7787 143.613 90.7787 140.146C90.7787 136.68 95.5655 134.363 95.5655 139.216"
stroke-linecap="round" />
<path
id="Vector 33"
d="M101 133.578C107.424 118.627 115.684 124.941 117.061 127.728C120.273 134.228 114.384 139 112.243 139C110.101 139 107.96 137.7 107.96 134.45C107.96 131.2 112.243 129.028 112.243 133.578"
stroke-linecap="round" />
<path
id="Vector 34"
d="M117 127.216C124.18 111.269 133.411 118.003 134.951 120.976C138.541 127.91 131.959 133 129.566 133C127.172 133 124.779 131.613 124.779 128.146C124.779 124.68 129.566 122.363 129.566 127.216"
stroke-linecap="round" />
<path
id="Vector 35"
d="M135 121.216C142.18 105.269 151.411 112.003 152.951 114.976C156.541 121.91 149.959 127 147.566 127C145.172 127 142.779 125.613 142.779 122.146C142.779 118.68 147.566 116.363 147.566 121.216"
stroke-linecap="round" />
<path
id="Vector 36"
d="M154.5 127C168.5 108 181 107 186 115.5C189.959 122.23 183.5 130 179.5 130.5C175.5 131 171.5 129.354 171.5 124.5C171.5 119.646 178 117.147 178 122"
stroke-linecap="round" />
<path
id="Vector 37"
d="M164 131.5C166.5 129.5 176.172 126.515 179 135C181 141 176 146.5 172 147C168 147.5 165.5 144.392 165.5 141C165.5 137.608 171.5 135.147 171.5 140"
stroke-linecap="round" />
<path
id="Vector 38"
d="M148.5 155C151.5 145.5 164.172 142.515 167 151C169 157 165.5 160.5 161.5 161C157.5 161.5 156 159 156 155.5C156 152 161.5 151 161.5 155"
stroke-linecap="round" />
<path
id="Vector 39"
d="M143.5 167.973C146 162 153.035 159.682 154.666 165.317C155.819 169.301 153.801 171.626 151.495 171.958C149.189 172.29 148 171.324 148 169C148 166.676 151.495 165.317 151.495 167.973"
stroke-linecap="round" />
<path
id="Vector 15"
d="M84 192.5C101.167 193.5 137.6 188.8 146 162C154.4 135.2 166.5 128.167 171.5 128"
stroke-linecap="round" />
<path
id="Vector 16"
d="M85 192.5C103.5 194 147.287 189.088 171 155C195 120.5 192.5 113 193 109.5"
stroke-linecap="round" />
<path
id="Vector 17"
d="M83 193C108.667 193.667 160.089 186.414 177.5 163C192 143.5 197 127.5 193 109.5"
stroke-linecap="round" />
<path id="Vector 19" d="M10.5 138.5L1 128.5" stroke-linecap="round" />
<path id="Vector 20" d="M13 134.5L1 118.5" stroke-linecap="round" />
<path
id="Vector 21"
d="M18 128.5C17.8333 124.5 16 116.4 10 116C8.5 110 5 108 1 107.5"
stroke-linecap="round" />
<path
id="Vector 11"
d="M29 230C25.1667 207.5 29.5 160 77.5 150C137.5 137.5 135.5 124 172 128"
stroke-linecap="round" />
<path
id="Vector 12"
d="M28.9999 230C25.1666 207.5 30.5 158 96 158C130.5 158 148 130 172 128"
stroke-linecap="round" />
<path
id="Vector 1"
d="M1 252.5C1.33333 247.333 2.5 240.5 7.5 241.5C12.5 242.5 11 250 8 250C5 250 4.5 245.5 7.5 246.5"
stroke-linecap="round" />
<path
id="Vector 2"
d="M20 242.5C10.5 224.167 -3.60001 176.9 24 138.5C58.5 90.5 103 97 145 84.5C189 71.4048 236 59.5 261 1"
stroke-linecap="round" />
<path
id="Vector 3"
d="M25 236.5C16.3333 217.833 3.99998 166.2 30 131"
stroke-linecap="round" />
<path
id="Vector 6"
d="M193.5 131C215.667 130.833 259.8 104.2 261 1"
stroke-linecap="round" />
<path
id="Form 1"
d="M1 260C1 260 1 223.1 1 200C1 176.9 5.8 138.4 41.5 115.5"
stroke-miterlimit="100"
stroke-linecap="round" />
<path
id="Form 3"
d="M1 260C9 260 29.4 230.5 35.5 223.3C40.9 217 51.3 211.3 59 221C70.1 235 44 247.2 43.3 233.6C42.8 224.3 52.6 227.4 51 231"
stroke-miterlimit="100"
stroke-linecap="round" />
<path
id="Form 3 copy"
d="M59.5 234.5C59.5 234.5 65.4 227.7 71.5 220.5C76.9 214.2 87.3 208.5 95 218.2C106.1 232.2 80 244.4 79.3 230.8C78.8 221.5 88.6 224.6 87 228.2"
stroke-miterlimit="100"
stroke-linecap="round" />
<path
id="Form 3 copy_2"
d="M94.5 232.5C94.5 232.5 98.9366 226.392 104.04 220.322C108.559 215.012 117.26 210.207 123.703 218.384C132.99 230.185 111.152 240.47 110.567 229.005C110.148 221.165 118.348 223.779 117.009 226.813"
stroke-miterlimit="100"
stroke-linecap="round" />
<path
id="Vector 4"
d="M29 231.5C26 209.5 23.3 160.6 38.5 143C57.5 121 80 116.5 107.5 110C135 103.5 228 93.5 261 1"
stroke-linecap="round" />
<path
id="Vector 5"
d="M29 231.5C26 209.5 24.8 175.1 40 157.5C59 135.5 78.6302 132.623 107.5 123C137.5 113 147.5 103.5 193 109.5"
stroke-linecap="round" />
<path
id="Vector 7"
d="M194.5 125.5C223 118.5 230.5 103 241 87.5"
stroke-linecap="round" />
<path
id="Vector 8"
d="M194.5 121C214.833 115.833 253.5 89 261 1"
stroke-linecap="round" />
<path
id="Vector 9"
d="M188.5 104C203.667 101 240.6 73.8 261 1"
stroke-linecap="round" />
<path
id="Vector 69"
d="M177 104.5C184.667 102 192 99.5 207 88.5"
stroke-linecap="round" />
<path
id="Vector 10"
d="M194 112.5C232.5 93.5 249.5 66.5 261 1"
stroke-linecap="round" />
<path
id="Vector 68"
d="M194.5 116.5C205.5 113 217 104 230 90.5"
stroke-linecap="round" />
<path
id="Vector 13"
d="M30.5 203C32.1667 198.833 39.7 190.7 56.5 191.5C77.5 192.5 122 201 135 145.5"
stroke-linecap="round" />
<path
id="Vector 14"
d="M78 192.5C96.6667 195 135.3 188.5 140.5 142.5"
stroke-linecap="round" />
<path
id="Vector 18"
d="M1 154.5C6.83333 142.5 23.8 116.5 41 110.5"
stroke-linecap="round" />
<path
id="Vector 21_2"
d="M27.5 119C26 108.5 11.5 105 1 94.5"
stroke-linecap="round" />
<path
id="Vector 22"
d="M28.5 118C27 105 11.5 100.5 1 87.5"
stroke-linecap="round" />
<path
id="Vector 23"
d="M29.5 117C27.5 99 9.5 94.5 1 80.5"
stroke-linecap="round" />
<path
id="Vector 24"
d="M36 112.5C34.5 84.5 8 92.5 1 55"
stroke-linecap="round" />
<path
id="Vector 25"
d="M41.5 119C43.5 77 11 84 1 43"
stroke-linecap="round" />
<path
id="Vector 26"
d="M129.5 115C131 110 141.5 95 157 108"
stroke-linecap="round" />
<path
id="Vector 27"
d="M154.5 106C160.5 101.5 171.5 95.5 179.5 108"
stroke-linecap="round" />
<path
id="Vector 40"
d="M123 179.5L140.5 172.5"
stroke-linecap="round" />
<path id="Vector 41" d="M131.5 169.5L146 162" stroke-linecap="round" />
<path
id="Vector 42"
d="M136.5 159.5L149.5 152.5"
stroke-linecap="round" />
<path id="Vector 43" d="M140 148L155.5 140.5" stroke-linecap="round" />
<path
id="Vector 44"
d="M63.5 163.5C69 162 69.5 152.5 74.5 151"
stroke-linecap="round" />
<path
id="Vector 45"
d="M81.5 159C86 158.5 85 148.5 89.5 147.5"
stroke-linecap="round" />
<path
id="Vector 46"
d="M96 158C99 157.5 100 145 104 143.5"
stroke-linecap="round" />
<path
id="Vector 47"
d="M107.5 157C112.5 155.5 112.5 141.5 120 138"
stroke-linecap="round" />
<path
id="Vector 48"
d="M122 152.5C127.5 150 126 136 133 133"
stroke-linecap="round" />
<path
id="Vector 49"
d="M136.5 144.5C140.5 142 140.5 131 147.5 128.5"
stroke-linecap="round" />
<path
id="Vector 50"
d="M180.5 108C181.5 105.5 189.5 99.5 193 109.5"
stroke-linecap="round" />
<path
id="Vector 51"
d="M36.5 162.5C39 159 35.5 148.5 40 141.5"
stroke-linecap="round" />
<path
id="Vector 52"
d="M42 155.5C45.5 151 43.5 138 48.5 133"
stroke-linecap="round" />
<path
id="Vector 53"
d="M49 148.5C53 145.5 50 132.5 52 130.5"
stroke-linecap="round" />
<path id="Vector 54" d="M1 81V67" stroke-linecap="round" />
<path id="Vector 55" d="M6 87L7 76.5" stroke-linecap="round" />
<path id="Vector 56" d="M13 93L14 85.5" stroke-linecap="round" />
<path id="Vector 57" d="M19.5 99L21 91" stroke-linecap="round" />
<path id="Vector 58" d="M24 104L28 98" stroke-linecap="round" />
<path id="Vector 59" d="M27.5 109L31.5 104.5" stroke-linecap="round" />
<path id="Vector 60" d="M29 114L34 110.5" stroke-linecap="round" />
<path
id="Vector 61"
d="M52 130.5C53.5 129.5 62.5 139 66.5 137"
stroke-linecap="round" />
<path
id="Vector 62"
d="M55.5 128C61 125 78 132.5 84 130.5"
stroke-linecap="round" />
<path
id="Vector 63"
d="M64 123.5C72 120 92.5 127.5 99.5 125.5"
stroke-linecap="round" />
<path
id="Vector 64"
d="M74.5 119C82.5 116.5 112 121.5 120 118.5"
stroke-linecap="round" />
<path
id="Vector 65"
d="M89 114.5C98.5 112 122.5 111 129.5 114.5"
stroke-linecap="round" />
<path
id="Vector 66"
d="M172.5 101.5C195.5 91 238.5 74 260.5 5"
stroke-linecap="round" />
<path
id="Vector 67"
d="M166.5 100.5C177 95.6667 182.2 93.8 189 89"
stroke-linecap="round" />
<path
id="Vector 70"
d="M194.5 112C195 110.333 196 105.9 196 101.5"
stroke-linecap="round" />
<path
id="Vector 71"
d="M199 110C201.4 109 199.6 100.5 202 98"
stroke-linecap="round" />
<path
id="Vector 72"
d="M206 106C208.5 104.5 206.5 96 211 91"
stroke-linecap="round" />
<path
id="Vector 73"
d="M212 102C216 99 213 89.5 216 86"
stroke-linecap="round" />
<path
id="Vector 74"
d="M220.5 95C223 93 219 84.5 221.5 80.5"
stroke-linecap="round" />
<path
id="Vector 75"
d="M227.5 88C230.5 85 226 76.5 229 71"
stroke-linecap="round" />
<path
id="Vector 76"
d="M235 78.5C238 74.5 232.5 67 234.5 62.5"
stroke-linecap="round" />
<path
id="Vector 77"
d="M241.5 67.5C243.5 64 237 59.5 238.5 56"
stroke-linecap="round" />
<path
id="Vector 78"
d="M247 55C248 52 242 50.5 243.5 47"
stroke-linecap="round" />
<path
id="Vector 79"
d="M250.5 44C251.5 41.5 247.7 39.3 248.5 36.5"
stroke-linecap="round" />
<path
id="Vector 80"
d="M191.5 138.5C194.5 135.833 200.7 130.4 201.5 130"
stroke-linecap="round" />
<path
id="Vector 81"
d="M188.5 146C192.5 144 204.5 135.5 211 126.5"
stroke-linecap="round" />
<path
id="Vector 82"
d="M41.5 115.5C43.1667 113.167 46.9 106.1 46.5 92.5"
stroke-linecap="round" />
<path
id="Vector 83"
d="M34.5 90.5C34.8333 87.5 35.3 80.1 34.5 74.5"
stroke-linecap="round" />
<path
id="Vector 84"
d="M18 73C19.1667 72.1667 21.6 68.5 22 60.5"
stroke-linecap="round" />
<path
id="Vector 85"
d="M38.5 97C39.1667 92.6667 42.1 83.2 46.5 80"
stroke-linecap="round" />
<path
id="Vector 90"
d="M56 108.5C56 97.5 50.6933 83.2 46 80"
stroke-linecap="round" />
<path
id="Vector 86"
d="M26 80.5C26.6667 76.1667 29 68.5 34.5 65"
stroke-linecap="round" />
<path
id="Vector 89"
d="M43 84C42.2941 79.6667 39.8235 68.5 34 65"
stroke-linecap="round" />
<path
id="Vector 87"
d="M13 67.5C13.6667 63.1667 16 53.5 21.5 50"
stroke-linecap="round" />
<path
id="Vector 88"
d="M30.5 69C29.7941 64.7905 26.8235 53.4 21 50"
stroke-linecap="round" />
<path
id="Vector 91"
d="M183 155C194.5 150.5 206.5 147.5 219 122"
stroke-linecap="round" />
<path
id="Vector 92"
d="M176.5 165C189.167 164.833 217.5 154.2 229.5 113"
stroke-linecap="round" />
<path
id="Vector 93"
d="M246 87.5C251 89.8333 261.1 97.8 261.5 111C261.9 124.2 250 131.833 244 134"
stroke-linecap="round" />
<path
id="Vector 94"
d="M28 216.5C32 215.5 42 202 49 202C56 202 64 214.5 72.5 214.5C81 214.5 92 203.5 100 204C108 204.5 116.5 217.5 127.5 215.5C138.5 213.5 145.5 203.5 153.5 202.5C161.5 201.5 175 210.5 184.5 207.5C192.1 205.1 195 198.5 195.5 195.5"
stroke-linecap="round" />
<path
id="Vector 95"
d="M30.5 204C32 204 45.5 214 49.5 214C53.5 214 63 204 72 204C81 204 87.5 215 99 215.5C110.5 216 116.5 204 126.5 205C136.5 206 144 214 154 212C164 210 174.5 199 178.5 197C182.5 195 192 197.5 195 196C198 194.5 206 189.5 209 179.5"
stroke-linecap="round" />
<path
id="Vector 96"
d="M29.5 205C31 202 36.5 196.5 55.5 197.5C74.5 198.5 95 202.5 112 201.5C129 200.5 145.5 198.5 155.5 194.5C165.5 190.5 178.5 179.5 184 172"
stroke-linecap="round" />
<path
id="Vector 97"
d="M123 201C134.667 200.333 160.4 198.2 170 195C179.6 191.8 190.167 182.333 193.5 178.5"
stroke-linecap="round" />
<path
id="Vector 98"
d="M165.5 174C173.333 174 193.7 168.4 200.5 160C207.3 151.6 213.667 144.167 216 141.5"
stroke-linecap="round" />
<path
id="Vector 99"
d="M183 170.5C186 174.833 197.5 182.5 215 178"
stroke-linecap="round" />
<path
id="Vector 100"
d="M204 173C207.333 177.333 217.6 183.4 230 169"
stroke-linecap="round" />
<path
id="Vector 101"
d="M221 167.5C224.667 169.667 233.6 171.4 240 161C242.8 156.2 244.333 152 244.5 151"
stroke-linecap="round" />
<path
id="Vector 102"
d="M234.5 153C240.833 154 253.4 150.6 253 129"
stroke-linecap="round" />
<path
id="Vector 103"
d="M204.5 155.5C208.5 151.5 214.937 148.517 219 152C222.5 155 226 162.5 220 167.5C214.664 171.947 210.187 167.904 209.5 165.5C208.214 161 213 157 215 161"
stroke-linecap="round" />
<path
id="Vector 104"
d="M214 144C218 140 226.437 137.054 230.5 140.537C234 143.537 237.5 151.037 231.5 156.037C226.164 160.484 221.687 156.441 221 154.037C219.714 149.537 224 146 226.5 149.537"
stroke-linecap="round" />
<path
id="Vector 105"
d="M223.5 129.5C227.5 125.5 234.437 121.517 238.5 125C242 128 246.5 135.037 240.5 140.037C235.164 144.484 230.687 140.441 230 138.037C228.714 133.537 233.5 130 235.5 133.537"
stroke-linecap="round" />
<path
id="Vector 106"
d="M229.5 113C233.5 109 240.623 102.5 246.5 107.537C250 110.537 253.5 118.037 247.5 123.037C242.164 127.484 237.687 123.441 237 121.037C235.714 116.537 240.5 113 242.5 116.537"
stroke-linecap="round" />
<path
id="Vector 107"
d="M224.5 169C223.833 170.667 220.1 173.7 210.5 172.5"
stroke-linecap="round" />
<path
id="Vector 108"
d="M238.5 153C238.333 155.5 235.8 160.9 227 162.5"
stroke-linecap="round" />
<path
id="Vector 109"
d="M248.5 132C248.5 135.167 246.8 142.6 240 147"
stroke-linecap="round" />
<path
id="Vector 110"
d="M242 95C247 95.3333 256.5 100.5 254.5 118.5"
stroke-linecap="round" />
<path
id="Vector 111"
d="M200.5 160C198.833 162.333 196.5 169.5 200.5 172C205.067 174.854 209 170.5 209 168C209 165.5 205.82 160.5 202.5 165.5"
stroke-linecap="round" />
<path
id="Vector 112"
d="M207 176C205.167 177 200.4 177.7 192 172.5"
stroke-linecap="round" />
</g>
</svg>
</div>
<div class="">
<svg
width="262"
height="261"
viewBox="0 0 262 261"
fill="none"
xmlns="http://www.w3.org/2000/svg">
<g id="mask">
<path
id="Vector 28"
d="M35 165C39.5 160 42 160.5 44 162C46 163.5 46 170 41.5 169C40.6667 168.833 39.1 168.1 39.5 166.5"
stroke-linecap="round" />
<path
id="Vector 29"
d="M44 153.032C48.5 148.032 52.7137 149.856 54 152C57 157 52.5 161 50.5 161C48.5 161 46.5 160 46.5 157.5C46.5 155 50.4 153.5 50 157.5"
stroke-linecap="round" />
<path
id="Vector 30"
d="M51 150C57 138.5 64.7137 143.356 66 145.5C69 150.5 63.5 154.171 61.5 154.171C59.5 154.171 57.5 153.171 57.5 150.671C57.5 148.171 61.5 146.5 61.5 150"
stroke-linecap="round" />
<path
id="Vector 31"
d="M65 143.939C72.1803 129.985 81.4115 135.878 82.9507 138.479C86.5409 144.546 79.9589 149 77.5655 149C75.1721 149 72.7787 147.787 72.7787 144.753C72.7787 141.72 77.5655 139.692 77.5655 143.939"
stroke-linecap="round" />
<path
id="Vector 32"
d="M83 139.216C90.1803 123.269 99.4115 130.003 100.951 132.976C104.541 139.91 97.9589 145 95.5655 145C93.1721 145 90.7787 143.613 90.7787 140.146C90.7787 136.68 95.5655 134.363 95.5655 139.216"
stroke-linecap="round" />
<path
id="Vector 33"
d="M101 133.578C107.424 118.627 115.684 124.941 117.061 127.728C120.273 134.228 114.384 139 112.243 139C110.101 139 107.96 137.7 107.96 134.45C107.96 131.2 112.243 129.028 112.243 133.578"
stroke-linecap="round" />
<path
id="Vector 34"
d="M117 127.216C124.18 111.269 133.411 118.003 134.951 120.976C138.541 127.91 131.959 133 129.566 133C127.172 133 124.779 131.613 124.779 128.146C124.779 124.68 129.566 122.363 129.566 127.216"
stroke-linecap="round" />
<path
id="Vector 35"
d="M135 121.216C142.18 105.269 151.411 112.003 152.951 114.976C156.541 121.91 149.959 127 147.566 127C145.172 127 142.779 125.613 142.779 122.146C142.779 118.68 147.566 116.363 147.566 121.216"
stroke-linecap="round" />
<path
id="Vector 36"
d="M154.5 127C168.5 108 181 107 186 115.5C189.959 122.23 183.5 130 179.5 130.5C175.5 131 171.5 129.354 171.5 124.5C171.5 119.646 178 117.147 178 122"
stroke-linecap="round" />
<path
id="Vector 37"
d="M164 131.5C166.5 129.5 176.172 126.515 179 135C181 141 176 146.5 172 147C168 147.5 165.5 144.392 165.5 141C165.5 137.608 171.5 135.147 171.5 140"
stroke-linecap="round" />
<path
id="Vector 38"
d="M148.5 155C151.5 145.5 164.172 142.515 167 151C169 157 165.5 160.5 161.5 161C157.5 161.5 156 159 156 155.5C156 152 161.5 151 161.5 155"
stroke-linecap="round" />
<path
id="Vector 39"
d="M143.5 167.973C146 162 153.035 159.682 154.666 165.317C155.819 169.301 153.801 171.626 151.495 171.958C149.189 172.29 148 171.324 148 169C148 166.676 151.495 165.317 151.495 167.973"
stroke-linecap="round" />
<path
id="Vector 15"
d="M84 192.5C101.167 193.5 137.6 188.8 146 162C154.4 135.2 166.5 128.167 171.5 128"
stroke-linecap="round" />
<path
id="Vector 16"
d="M85 192.5C103.5 194 147.287 189.088 171 155C195 120.5 192.5 113 193 109.5"
stroke-linecap="round" />
<path
id="Vector 17"
d="M83 193C108.667 193.667 160.089 186.414 177.5 163C192 143.5 197 127.5 193 109.5"
stroke-linecap="round" />
<path id="Vector 19" d="M10.5 138.5L1 128.5" stroke-linecap="round" />
<path id="Vector 20" d="M13 134.5L1 118.5" stroke-linecap="round" />
<path
id="Vector 21"
d="M18 128.5C17.8333 124.5 16 116.4 10 116C8.5 110 5 108 1 107.5"
stroke-linecap="round" />
<path
id="Vector 11"
d="M29 230C25.1667 207.5 29.5 160 77.5 150C137.5 137.5 135.5 124 172 128"
stroke-linecap="round" />
<path
id="Vector 12"
d="M28.9999 230C25.1666 207.5 30.5 158 96 158C130.5 158 148 130 172 128"
stroke-linecap="round" />
<path
id="Vector 1"
d="M1 252.5C1.33333 247.333 2.5 240.5 7.5 241.5C12.5 242.5 11 250 8 250C5 250 4.5 245.5 7.5 246.5"
stroke-linecap="round" />
<path
id="Vector 2"
d="M20 242.5C10.5 224.167 -3.60001 176.9 24 138.5C58.5 90.5 103 97 145 84.5C189 71.4048 236 59.5 261 1"
stroke-linecap="round" />
<path
id="Vector 3"
d="M25 236.5C16.3333 217.833 3.99998 166.2 30 131"
stroke-linecap="round" />
<path
id="Vector 6"
d="M193.5 131C215.667 130.833 259.8 104.2 261 1"
stroke-linecap="round" />
<path
id="Form 1"
d="M1 260C1 260 1 223.1 1 200C1 176.9 5.8 138.4 41.5 115.5"
stroke-miterlimit="100"
stroke-linecap="round" />
<path
id="Form 3"
d="M1 260C9 260 29.4 230.5 35.5 223.3C40.9 217 51.3 211.3 59 221C70.1 235 44 247.2 43.3 233.6C42.8 224.3 52.6 227.4 51 231"
stroke-miterlimit="100"
stroke-linecap="round" />
<path
id="Form 3 copy"
d="M59.5 234.5C59.5 234.5 65.4 227.7 71.5 220.5C76.9 214.2 87.3 208.5 95 218.2C106.1 232.2 80 244.4 79.3 230.8C78.8 221.5 88.6 224.6 87 228.2"
stroke-miterlimit="100"
stroke-linecap="round" />
<path
id="Form 3 copy_2"
d="M94.5 232.5C94.5 232.5 98.9366 226.392 104.04 220.322C108.559 215.012 117.26 210.207 123.703 218.384C132.99 230.185 111.152 240.47 110.567 229.005C110.148 221.165 118.348 223.779 117.009 226.813"
stroke-miterlimit="100"
stroke-linecap="round" />
<path
id="Vector 4"
d="M29 231.5C26 209.5 23.3 160.6 38.5 143C57.5 121 80 116.5 107.5 110C135 103.5 228 93.5 261 1"
stroke-linecap="round" />
<path
id="Vector 5"
d="M29 231.5C26 209.5 24.8 175.1 40 157.5C59 135.5 78.6302 132.623 107.5 123C137.5 113 147.5 103.5 193 109.5"
stroke-linecap="round" />
<path
id="Vector 7"
d="M194.5 125.5C223 118.5 230.5 103 241 87.5"
stroke-linecap="round" />
<path
id="Vector 8"
d="M194.5 121C214.833 115.833 253.5 89 261 1"
stroke-linecap="round" />
<path
id="Vector 9"
d="M188.5 104C203.667 101 240.6 73.8 261 1"
stroke-linecap="round" />
<path
id="Vector 69"
d="M177 104.5C184.667 102 192 99.5 207 88.5"
stroke-linecap="round" />
<path
id="Vector 10"
d="M194 112.5C232.5 93.5 249.5 66.5 261 1"
stroke-linecap="round" />
<path
id="Vector 68"
d="M194.5 116.5C205.5 113 217 104 230 90.5"
stroke-linecap="round" />
<path
id="Vector 13"
d="M30.5 203C32.1667 198.833 39.7 190.7 56.5 191.5C77.5 192.5 122 201 135 145.5"
stroke-linecap="round" />
<path
id="Vector 14"
d="M78 192.5C96.6667 195 135.3 188.5 140.5 142.5"
stroke-linecap="round" />
<path
id="Vector 18"
d="M1 154.5C6.83333 142.5 23.8 116.5 41 110.5"
stroke-linecap="round" />
<path
id="Vector 21_2"
d="M27.5 119C26 108.5 11.5 105 1 94.5"
stroke-linecap="round" />
<path
id="Vector 22"
d="M28.5 118C27 105 11.5 100.5 1 87.5"
stroke-linecap="round" />
<path
id="Vector 23"
d="M29.5 117C27.5 99 9.5 94.5 1 80.5"
stroke-linecap="round" />
<path
id="Vector 24"
d="M36 112.5C34.5 84.5 8 92.5 1 55"
stroke-linecap="round" />
<path
id="Vector 25"
d="M41.5 119C43.5 77 11 84 1 43"
stroke-linecap="round" />
<path
id="Vector 26"
d="M129.5 115C131 110 141.5 95 157 108"
stroke-linecap="round" />
<path
id="Vector 27"
d="M154.5 106C160.5 101.5 171.5 95.5 179.5 108"
stroke-linecap="round" />
<path
id="Vector 40"
d="M123 179.5L140.5 172.5"
stroke-linecap="round" />
<path id="Vector 41" d="M131.5 169.5L146 162" stroke-linecap="round" />
<path
id="Vector 42"
d="M136.5 159.5L149.5 152.5"
stroke-linecap="round" />
<path id="Vector 43" d="M140 148L155.5 140.5" stroke-linecap="round" />
<path
id="Vector 44"
d="M63.5 163.5C69 162 69.5 152.5 74.5 151"
stroke-linecap="round" />
<path
id="Vector 45"
d="M81.5 159C86 158.5 85 148.5 89.5 147.5"
stroke-linecap="round" />
<path
id="Vector 46"
d="M96 158C99 157.5 100 145 104 143.5"
stroke-linecap="round" />
<path
id="Vector 47"
d="M107.5 157C112.5 155.5 112.5 141.5 120 138"
stroke-linecap="round" />
<path
id="Vector 48"
d="M122 152.5C127.5 150 126 136 133 133"
stroke-linecap="round" />
<path
id="Vector 49"
d="M136.5 144.5C140.5 142 140.5 131 147.5 128.5"
stroke-linecap="round" />
<path
id="Vector 50"
d="M180.5 108C181.5 105.5 189.5 99.5 193 109.5"
stroke-linecap="round" />
<path
id="Vector 51"
d="M36.5 162.5C39 159 35.5 148.5 40 141.5"
stroke-linecap="round" />
<path
id="Vector 52"
d="M42 155.5C45.5 151 43.5 138 48.5 133"
stroke-linecap="round" />
<path
id="Vector 53"
d="M49 148.5C53 145.5 50 132.5 52 130.5"
stroke-linecap="round" />
<path id="Vector 54" d="M1 81V67" stroke-linecap="round" />
<path id="Vector 55" d="M6 87L7 76.5" stroke-linecap="round" />
<path id="Vector 56" d="M13 93L14 85.5" stroke-linecap="round" />
<path id="Vector 57" d="M19.5 99L21 91" stroke-linecap="round" />
<path id="Vector 58" d="M24 104L28 98" stroke-linecap="round" />
<path id="Vector 59" d="M27.5 109L31.5 104.5" stroke-linecap="round" />
<path id="Vector 60" d="M29 114L34 110.5" stroke-linecap="round" />
<path
id="Vector 61"
d="M52 130.5C53.5 129.5 62.5 139 66.5 137"
stroke-linecap="round" />
<path
id="Vector 62"
d="M55.5 128C61 125 78 132.5 84 130.5"
stroke-linecap="round" />
<path
id="Vector 63"
d="M64 123.5C72 120 92.5 127.5 99.5 125.5"
stroke-linecap="round" />
<path
id="Vector 64"
d="M74.5 119C82.5 116.5 112 121.5 120 118.5"
stroke-linecap="round" />
<path
id="Vector 65"
d="M89 114.5C98.5 112 122.5 111 129.5 114.5"
stroke-linecap="round" />
<path
id="Vector 66"
d="M172.5 101.5C195.5 91 238.5 74 260.5 5"
stroke-linecap="round" />
<path
id="Vector 67"
d="M166.5 100.5C177 95.6667 182.2 93.8 189 89"
stroke-linecap="round" />
<path
id="Vector 70"
d="M194.5 112C195 110.333 196 105.9 196 101.5"
stroke-linecap="round" />
<path
id="Vector 71"
d="M199 110C201.4 109 199.6 100.5 202 98"
stroke-linecap="round" />
<path
id="Vector 72"
d="M206 106C208.5 104.5 206.5 96 211 91"
stroke-linecap="round" />
<path
id="Vector 73"
d="M212 102C216 99 213 89.5 216 86"
stroke-linecap="round" />
<path
id="Vector 74"
d="M220.5 95C223 93 219 84.5 221.5 80.5"
stroke-linecap="round" />
<path
id="Vector 75"
d="M227.5 88C230.5 85 226 76.5 229 71"
stroke-linecap="round" />
<path
id="Vector 76"
d="M235 78.5C238 74.5 232.5 67 234.5 62.5"
stroke-linecap="round" />
<path
id="Vector 77"
d="M241.5 67.5C243.5 64 237 59.5 238.5 56"
stroke-linecap="round" />
<path
id="Vector 78"
d="M247 55C248 52 242 50.5 243.5 47"
stroke-linecap="round" />
<path
id="Vector 79"
d="M250.5 44C251.5 41.5 247.7 39.3 248.5 36.5"
stroke-linecap="round" />
<path
id="Vector 80"
d="M191.5 138.5C194.5 135.833 200.7 130.4 201.5 130"
stroke-linecap="round" />
<path
id="Vector 81"
d="M188.5 146C192.5 144 204.5 135.5 211 126.5"
stroke-linecap="round" />
<path
id="Vector 82"
d="M41.5 115.5C43.1667 113.167 46.9 106.1 46.5 92.5"
stroke-linecap="round" />
<path
id="Vector 83"
d="M34.5 90.5C34.8333 87.5 35.3 80.1 34.5 74.5"
stroke-linecap="round" />
<path
id="Vector 84"
d="M18 73C19.1667 72.1667 21.6 68.5 22 60.5"
stroke-linecap="round" />
<path
id="Vector 85"
d="M38.5 97C39.1667 92.6667 42.1 83.2 46.5 80"
stroke-linecap="round" />
<path
id="Vector 90"
d="M56 108.5C56 97.5 50.6933 83.2 46 80"
stroke-linecap="round" />
<path
id="Vector 86"
d="M26 80.5C26.6667 76.1667 29 68.5 34.5 65"
stroke-linecap="round" />
<path
id="Vector 89"
d="M43 84C42.2941 79.6667 39.8235 68.5 34 65"
stroke-linecap="round" />
<path
id="Vector 87"
d="M13 67.5C13.6667 63.1667 16 53.5 21.5 50"
stroke-linecap="round" />
<path
id="Vector 88"
d="M30.5 69C29.7941 64.7905 26.8235 53.4 21 50"
stroke-linecap="round" />
<path
id="Vector 91"
d="M183 155C194.5 150.5 206.5 147.5 219 122"
stroke-linecap="round" />
<path
id="Vector 92"
d="M176.5 165C189.167 164.833 217.5 154.2 229.5 113"
stroke-linecap="round" />
<path
id="Vector 93"
d="M246 87.5C251 89.8333 261.1 97.8 261.5 111C261.9 124.2 250 131.833 244 134"
stroke-linecap="round" />
<path
id="Vector 94"
d="M28 216.5C32 215.5 42 202 49 202C56 202 64 214.5 72.5 214.5C81 214.5 92 203.5 100 204C108 204.5 116.5 217.5 127.5 215.5C138.5 213.5 145.5 203.5 153.5 202.5C161.5 201.5 175 210.5 184.5 207.5C192.1 205.1 195 198.5 195.5 195.5"
stroke-linecap="round" />
<path
id="Vector 95"
d="M30.5 204C32 204 45.5 214 49.5 214C53.5 214 63 204 72 204C81 204 87.5 215 99 215.5C110.5 216 116.5 204 126.5 205C136.5 206 144 214 154 212C164 210 174.5 199 178.5 197C182.5 195 192 197.5 195 196C198 194.5 206 189.5 209 179.5"
stroke-linecap="round" />
<path
id="Vector 96"
d="M29.5 205C31 202 36.5 196.5 55.5 197.5C74.5 198.5 95 202.5 112 201.5C129 200.5 145.5 198.5 155.5 194.5C165.5 190.5 178.5 179.5 184 172"
stroke-linecap="round" />
<path
id="Vector 97"
d="M123 201C134.667 200.333 160.4 198.2 170 195C179.6 191.8 190.167 182.333 193.5 178.5"
stroke-linecap="round" />
<path
id="Vector 98"
d="M165.5 174C173.333 174 193.7 168.4 200.5 160C207.3 151.6 213.667 144.167 216 141.5"
stroke-linecap="round" />
<path
id="Vector 99"
d="M183 170.5C186 174.833 197.5 182.5 215 178"
stroke-linecap="round" />
<path
id="Vector 100"
d="M204 173C207.333 177.333 217.6 183.4 230 169"
stroke-linecap="round" />
<path
id="Vector 101"
d="M221 167.5C224.667 169.667 233.6 171.4 240 161C242.8 156.2 244.333 152 244.5 151"
stroke-linecap="round" />
<path
id="Vector 102"
d="M234.5 153C240.833 154 253.4 150.6 253 129"
stroke-linecap="round" />
<path
id="Vector 103"
d="M204.5 155.5C208.5 151.5 214.937 148.517 219 152C222.5 155 226 162.5 220 167.5C214.664 171.947 210.187 167.904 209.5 165.5C208.214 161 213 157 215 161"
stroke-linecap="round" />
<path
id="Vector 104"
d="M214 144C218 140 226.437 137.054 230.5 140.537C234 143.537 237.5 151.037 231.5 156.037C226.164 160.484 221.687 156.441 221 154.037C219.714 149.537 224 146 226.5 149.537"
stroke-linecap="round" />
<path
id="Vector 105"
d="M223.5 129.5C227.5 125.5 234.437 121.517 238.5 125C242 128 246.5 135.037 240.5 140.037C235.164 144.484 230.687 140.441 230 138.037C228.714 133.537 233.5 130 235.5 133.537"
stroke-linecap="round" />
<path
id="Vector 106"
d="M229.5 113C233.5 109 240.623 102.5 246.5 107.537C250 110.537 253.5 118.037 247.5 123.037C242.164 127.484 237.687 123.441 237 121.037C235.714 116.537 240.5 113 242.5 116.537"
stroke-linecap="round" />
<path
id="Vector 107"
d="M224.5 169C223.833 170.667 220.1 173.7 210.5 172.5"
stroke-linecap="round" />
<path
id="Vector 108"
d="M238.5 153C238.333 155.5 235.8 160.9 227 162.5"
stroke-linecap="round" />
<path
id="Vector 109"
d="M248.5 132C248.5 135.167 246.8 142.6 240 147"
stroke-linecap="round" />
<path
id="Vector 110"
d="M242 95C247 95.3333 256.5 100.5 254.5 118.5"
stroke-linecap="round" />
<path
id="Vector 111"
d="M200.5 160C198.833 162.333 196.5 169.5 200.5 172C205.067 174.854 209 170.5 209 168C209 165.5 205.82 160.5 202.5 165.5"
stroke-linecap="round" />
<path
id="Vector 112"
d="M207 176C205.167 177 200.4 177.7 192 172.5"
stroke-linecap="round" />
</g>
</svg>
</div>
</div>
<style use:global>
@import "./mask.css";
.wrapper {
padding: 20px;
filter: drop-shadow(0px 0px 40px #be8630aa) drop-shadow(0px 0px 5px black)
drop-shadow(0px 0px 5px black) drop-shadow(0px 0px 5px black);
position: relative;
z-index: 5;
width: 100%;
display: flex;
justify-content: center;
height: 50%;
max-height: 50vh;
}
.wrapper > div {
max-width: 40vw;
}
.wrapper > div > :global(svg) {
overflow: visible;
max-width: 100%;
will-change: contents;
transform: translateZ(1px);
height: 100%;
width: auto;
}
.wrapper > div :global(path) {
animation-duration: 8s !important;
}
svg path {
stroke: #ceba51 !important;
}
.invert-x {
transform: scaleX(-1) translateX(-2.5px);
}
</style>

View File

@@ -0,0 +1,121 @@
<svg width="262" height="261" viewBox="0 0 262 261" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="mask">
<path id="Vector 28" d="M35 165C39.5 160 42 160.5 44 162C46 163.5 46 170 41.5 169C40.6667 168.833 39.1 168.1 39.5 166.5" stroke="#FF1717" stroke-linecap="round"/>
<path id="Vector 29" d="M44 153.032C48.5 148.032 52.7137 149.856 54 152C57 157 52.5 161 50.5 161C48.5 161 46.5 160 46.5 157.5C46.5 155 50.4 153.5 50 157.5" stroke="#FF1717" stroke-linecap="round"/>
<path id="Vector 30" d="M51 150C57 138.5 64.7137 143.356 66 145.5C69 150.5 63.5 154.171 61.5 154.171C59.5 154.171 57.5 153.171 57.5 150.671C57.5 148.171 61.5 146.5 61.5 150" stroke="#FF1717" stroke-linecap="round"/>
<path id="Vector 31" d="M65 143.939C72.1803 129.985 81.4115 135.878 82.9507 138.479C86.5409 144.546 79.9589 149 77.5655 149C75.1721 149 72.7787 147.787 72.7787 144.753C72.7787 141.72 77.5655 139.692 77.5655 143.939" stroke="#FF1717" stroke-linecap="round"/>
<path id="Vector 32" d="M83 139.216C90.1803 123.269 99.4115 130.003 100.951 132.976C104.541 139.91 97.9589 145 95.5655 145C93.1721 145 90.7787 143.613 90.7787 140.146C90.7787 136.68 95.5655 134.363 95.5655 139.216" stroke="#FF1717" stroke-linecap="round"/>
<path id="Vector 33" d="M101 133.578C107.424 118.627 115.684 124.941 117.061 127.728C120.273 134.228 114.384 139 112.243 139C110.101 139 107.96 137.7 107.96 134.45C107.96 131.2 112.243 129.028 112.243 133.578" stroke="#FF1717" stroke-linecap="round"/>
<path id="Vector 34" d="M117 127.216C124.18 111.269 133.411 118.003 134.951 120.976C138.541 127.91 131.959 133 129.566 133C127.172 133 124.779 131.613 124.779 128.146C124.779 124.68 129.566 122.363 129.566 127.216" stroke="#FF1717" stroke-linecap="round"/>
<path id="Vector 35" d="M135 121.216C142.18 105.269 151.411 112.003 152.951 114.976C156.541 121.91 149.959 127 147.566 127C145.172 127 142.779 125.613 142.779 122.146C142.779 118.68 147.566 116.363 147.566 121.216" stroke="#FF1717" stroke-linecap="round"/>
<path id="Vector 36" d="M154.5 127C168.5 108 181 107 186 115.5C189.959 122.23 183.5 130 179.5 130.5C175.5 131 171.5 129.354 171.5 124.5C171.5 119.646 178 117.147 178 122" stroke="#FF1717" stroke-linecap="round"/>
<path id="Vector 37" d="M164 131.5C166.5 129.5 176.172 126.515 179 135C181 141 176 146.5 172 147C168 147.5 165.5 144.392 165.5 141C165.5 137.608 171.5 135.147 171.5 140" stroke="#FF1717" stroke-linecap="round"/>
<path id="Vector 38" d="M148.5 155C151.5 145.5 164.172 142.515 167 151C169 157 165.5 160.5 161.5 161C157.5 161.5 156 159 156 155.5C156 152 161.5 151 161.5 155" stroke="#FF1717" stroke-linecap="round"/>
<path id="Vector 39" d="M143.5 167.973C146 162 153.035 159.682 154.666 165.317C155.819 169.301 153.801 171.626 151.495 171.958C149.189 172.29 148 171.324 148 169C148 166.676 151.495 165.317 151.495 167.973" stroke="#FF1717" stroke-linecap="round"/>
<path id="Vector 15" d="M84 192.5C101.167 193.5 137.6 188.8 146 162C154.4 135.2 166.5 128.167 171.5 128" stroke="#FF1717" stroke-linecap="round"/>
<path id="Vector 16" d="M85 192.5C103.5 194 147.287 189.088 171 155C195 120.5 192.5 113 193 109.5" stroke="#FF1717" stroke-linecap="round"/>
<path id="Vector 17" d="M83 193C108.667 193.667 160.089 186.414 177.5 163C192 143.5 197 127.5 193 109.5" stroke="#FF1717" stroke-linecap="round"/>
<path id="Vector 19" d="M10.5 138.5L1 128.5" stroke="#FF1717" stroke-linecap="round"/>
<path id="Vector 20" d="M13 134.5L1 118.5" stroke="#FF1717" stroke-linecap="round"/>
<path id="Vector 21" d="M18 128.5C17.8333 124.5 16 116.4 10 116C8.5 110 5 108 1 107.5" stroke="#FF1717" stroke-linecap="round"/>
<path id="Vector 11" d="M29 230C25.1667 207.5 29.5 160 77.5 150C137.5 137.5 135.5 124 172 128" stroke="#FF1717" stroke-linecap="round"/>
<path id="Vector 12" d="M28.9999 230C25.1666 207.5 30.5 158 96 158C130.5 158 148 130 172 128" stroke="#FF1717" stroke-linecap="round"/>
<path id="Vector 1" d="M1 252.5C1.33333 247.333 2.5 240.5 7.5 241.5C12.5 242.5 11 250 8 250C5 250 4.5 245.5 7.5 246.5" stroke="#FF1717" stroke-linecap="round"/>
<path id="Vector 2" d="M20 242.5C10.5 224.167 -3.60001 176.9 24 138.5C58.5 90.5 103 97 145 84.5C189 71.4048 236 59.5 261 1" stroke="#FF1717" stroke-linecap="round"/>
<path id="Vector 3" d="M25 236.5C16.3333 217.833 3.99998 166.2 30 131" stroke="#FF1717" stroke-linecap="round"/>
<path id="Vector 6" d="M193.5 131C215.667 130.833 259.8 104.2 261 1" stroke="#FF1717" stroke-linecap="round"/>
<path id="Form 1" d="M1 260C1 260 1 223.1 1 200C1 176.9 5.8 138.4 41.5 115.5" stroke="#FF1717" stroke-miterlimit="100" stroke-linecap="round"/>
<path id="Form 3" d="M1 260C9 260 29.4 230.5 35.5 223.3C40.9 217 51.3 211.3 59 221C70.1 235 44 247.2 43.3 233.6C42.8 224.3 52.6 227.4 51 231" stroke="#FF1717" stroke-miterlimit="100" stroke-linecap="round"/>
<path id="Form 3 copy" d="M59.5 234.5C59.5 234.5 65.4 227.7 71.5 220.5C76.9 214.2 87.3 208.5 95 218.2C106.1 232.2 80 244.4 79.3 230.8C78.8 221.5 88.6 224.6 87 228.2" stroke="#FF1717" stroke-miterlimit="100" stroke-linecap="round"/>
<path id="Form 3 copy_2" d="M94.5 232.5C94.5 232.5 98.9366 226.392 104.04 220.322C108.559 215.012 117.26 210.207 123.703 218.384C132.99 230.185 111.152 240.47 110.567 229.005C110.148 221.165 118.348 223.779 117.009 226.813" stroke="#FF1717" stroke-miterlimit="100" stroke-linecap="round"/>
<path id="Vector 4" d="M29 231.5C26 209.5 23.3 160.6 38.5 143C57.5 121 80 116.5 107.5 110C135 103.5 228 93.5 261 1" stroke="#FF1717" stroke-linecap="round"/>
<path id="Vector 5" d="M29 231.5C26 209.5 24.8 175.1 40 157.5C59 135.5 78.6302 132.623 107.5 123C137.5 113 147.5 103.5 193 109.5" stroke="#FF1717" stroke-linecap="round"/>
<path id="Vector 7" d="M194.5 125.5C223 118.5 230.5 103 241 87.5" stroke="#FF1717" stroke-linecap="round"/>
<path id="Vector 8" d="M194.5 121C214.833 115.833 253.5 89 261 1" stroke="#FF1717" stroke-linecap="round"/>
<path id="Vector 9" d="M188.5 104C203.667 101 240.6 73.8 261 1" stroke="#FF1717" stroke-linecap="round"/>
<path id="Vector 69" d="M177 104.5C184.667 102 192 99.5 207 88.5" stroke="#FF1717" stroke-linecap="round"/>
<path id="Vector 10" d="M194 112.5C232.5 93.5 249.5 66.5 261 1" stroke="#FF1717" stroke-linecap="round"/>
<path id="Vector 68" d="M194.5 116.5C205.5 113 217 104 230 90.5" stroke="#FF1717" stroke-linecap="round"/>
<path id="Vector 13" d="M30.5 203C32.1667 198.833 39.7 190.7 56.5 191.5C77.5 192.5 122 201 135 145.5" stroke="#FF1717" stroke-linecap="round"/>
<path id="Vector 14" d="M78 192.5C96.6667 195 135.3 188.5 140.5 142.5" stroke="#FF1717" stroke-linecap="round"/>
<path id="Vector 18" d="M1 154.5C6.83333 142.5 23.8 116.5 41 110.5" stroke="#FF1717" stroke-linecap="round"/>
<path id="Vector 21_2" d="M27.5 119C26 108.5 11.5 105 1 94.5" stroke="#FF1717" stroke-linecap="round"/>
<path id="Vector 22" d="M28.5 118C27 105 11.5 100.5 1 87.5" stroke="#FF1717" stroke-linecap="round"/>
<path id="Vector 23" d="M29.5 117C27.5 99 9.5 94.5 1 80.5" stroke="#FF1717" stroke-linecap="round"/>
<path id="Vector 24" d="M36 112.5C34.5 84.5 8 92.5 1 55" stroke="#FF1717" stroke-linecap="round"/>
<path id="Vector 25" d="M41.5 119C43.5 77 11 84 1 43" stroke="#FF1717" stroke-linecap="round"/>
<path id="Vector 26" d="M129.5 115C131 110 141.5 95 157 108" stroke="#FF1717" stroke-linecap="round"/>
<path id="Vector 27" d="M154.5 106C160.5 101.5 171.5 95.5 179.5 108" stroke="#FF1717" stroke-linecap="round"/>
<path id="Vector 40" d="M123 179.5L140.5 172.5" stroke="#FF1717" stroke-linecap="round"/>
<path id="Vector 41" d="M131.5 169.5L146 162" stroke="#FF1717" stroke-linecap="round"/>
<path id="Vector 42" d="M136.5 159.5L149.5 152.5" stroke="#FF1717" stroke-linecap="round"/>
<path id="Vector 43" d="M140 148L155.5 140.5" stroke="#FF1717" stroke-linecap="round"/>
<path id="Vector 44" d="M63.5 163.5C69 162 69.5 152.5 74.5 151" stroke="#FF1717" stroke-linecap="round"/>
<path id="Vector 45" d="M81.5 159C86 158.5 85 148.5 89.5 147.5" stroke="#FF1717" stroke-linecap="round"/>
<path id="Vector 46" d="M96 158C99 157.5 100 145 104 143.5" stroke="#FF1717" stroke-linecap="round"/>
<path id="Vector 47" d="M107.5 157C112.5 155.5 112.5 141.5 120 138" stroke="#FF1717" stroke-linecap="round"/>
<path id="Vector 48" d="M122 152.5C127.5 150 126 136 133 133" stroke="#FF1717" stroke-linecap="round"/>
<path id="Vector 49" d="M136.5 144.5C140.5 142 140.5 131 147.5 128.5" stroke="#FF1717" stroke-linecap="round"/>
<path id="Vector 50" d="M180.5 108C181.5 105.5 189.5 99.5 193 109.5" stroke="#FF1717" stroke-linecap="round"/>
<path id="Vector 51" d="M36.5 162.5C39 159 35.5 148.5 40 141.5" stroke="#FF1717" stroke-linecap="round"/>
<path id="Vector 52" d="M42 155.5C45.5 151 43.5 138 48.5 133" stroke="#FF1717" stroke-linecap="round"/>
<path id="Vector 53" d="M49 148.5C53 145.5 50 132.5 52 130.5" stroke="#FF1717" stroke-linecap="round"/>
<path id="Vector 54" d="M1 81V67" stroke="#FF1717" stroke-linecap="round"/>
<path id="Vector 55" d="M6 87L7 76.5" stroke="#FF1717" stroke-linecap="round"/>
<path id="Vector 56" d="M13 93L14 85.5" stroke="#FF1717" stroke-linecap="round"/>
<path id="Vector 57" d="M19.5 99L21 91" stroke="#FF1717" stroke-linecap="round"/>
<path id="Vector 58" d="M24 104L28 98" stroke="#FF1717" stroke-linecap="round"/>
<path id="Vector 59" d="M27.5 109L31.5 104.5" stroke="#FF1717" stroke-linecap="round"/>
<path id="Vector 60" d="M29 114L34 110.5" stroke="#FF1717" stroke-linecap="round"/>
<path id="Vector 61" d="M52 130.5C53.5 129.5 62.5 139 66.5 137" stroke="#FF1717" stroke-linecap="round"/>
<path id="Vector 62" d="M55.5 128C61 125 78 132.5 84 130.5" stroke="#FF1717" stroke-linecap="round"/>
<path id="Vector 63" d="M64 123.5C72 120 92.5 127.5 99.5 125.5" stroke="#FF1717" stroke-linecap="round"/>
<path id="Vector 64" d="M74.5 119C82.5 116.5 112 121.5 120 118.5" stroke="#FF1717" stroke-linecap="round"/>
<path id="Vector 65" d="M89 114.5C98.5 112 122.5 111 129.5 114.5" stroke="#FF1717" stroke-linecap="round"/>
<path id="Vector 66" d="M172.5 101.5C195.5 91 238.5 74 260.5 5" stroke="#FF1717" stroke-linecap="round"/>
<path id="Vector 67" d="M166.5 100.5C177 95.6667 182.2 93.8 189 89" stroke="#FF1717" stroke-linecap="round"/>
<path id="Vector 70" d="M194.5 112C195 110.333 196 105.9 196 101.5" stroke="#FF1717" stroke-linecap="round"/>
<path id="Vector 71" d="M199 110C201.4 109 199.6 100.5 202 98" stroke="#FF1717" stroke-linecap="round"/>
<path id="Vector 72" d="M206 106C208.5 104.5 206.5 96 211 91" stroke="#FF1717" stroke-linecap="round"/>
<path id="Vector 73" d="M212 102C216 99 213 89.5 216 86" stroke="#FF1717" stroke-linecap="round"/>
<path id="Vector 74" d="M220.5 95C223 93 219 84.5 221.5 80.5" stroke="#FF1717" stroke-linecap="round"/>
<path id="Vector 75" d="M227.5 88C230.5 85 226 76.5 229 71" stroke="#FF1717" stroke-linecap="round"/>
<path id="Vector 76" d="M235 78.5C238 74.5 232.5 67 234.5 62.5" stroke="#FF1717" stroke-linecap="round"/>
<path id="Vector 77" d="M241.5 67.5C243.5 64 237 59.5 238.5 56" stroke="#FF1717" stroke-linecap="round"/>
<path id="Vector 78" d="M247 55C248 52 242 50.5 243.5 47" stroke="#FF1717" stroke-linecap="round"/>
<path id="Vector 79" d="M250.5 44C251.5 41.5 247.7 39.3 248.5 36.5" stroke="#FF1717" stroke-linecap="round"/>
<path id="Vector 80" d="M191.5 138.5C194.5 135.833 200.7 130.4 201.5 130" stroke="#FF1717" stroke-linecap="round"/>
<path id="Vector 81" d="M188.5 146C192.5 144 204.5 135.5 211 126.5" stroke="#FF1717" stroke-linecap="round"/>
<path id="Vector 82" d="M41.5 115.5C43.1667 113.167 46.9 106.1 46.5 92.5" stroke="#FF1717" stroke-linecap="round"/>
<path id="Vector 83" d="M34.5 90.5C34.8333 87.5 35.3 80.1 34.5 74.5" stroke="#FF1717" stroke-linecap="round"/>
<path id="Vector 84" d="M18 73C19.1667 72.1667 21.6 68.5 22 60.5" stroke="#FF1717" stroke-linecap="round"/>
<path id="Vector 85" d="M38.5 97C39.1667 92.6667 42.1 83.2 46.5 80" stroke="#FF1717" stroke-linecap="round"/>
<path id="Vector 90" d="M56 108.5C56 97.5 50.6933 83.2 46 80" stroke="#FF1717" stroke-linecap="round"/>
<path id="Vector 86" d="M26 80.5C26.6667 76.1667 29 68.5 34.5 65" stroke="#FF1717" stroke-linecap="round"/>
<path id="Vector 89" d="M43 84C42.2941 79.6667 39.8235 68.5 34 65" stroke="#FF1717" stroke-linecap="round"/>
<path id="Vector 87" d="M13 67.5C13.6667 63.1667 16 53.5 21.5 50" stroke="#FF1717" stroke-linecap="round"/>
<path id="Vector 88" d="M30.5 69C29.7941 64.7905 26.8235 53.4 21 50" stroke="#FF1717" stroke-linecap="round"/>
<path id="Vector 91" d="M183 155C194.5 150.5 206.5 147.5 219 122" stroke="#FF1717" stroke-linecap="round"/>
<path id="Vector 92" d="M176.5 165C189.167 164.833 217.5 154.2 229.5 113" stroke="#FF1717" stroke-linecap="round"/>
<path id="Vector 93" d="M246 87.5C251 89.8333 261.1 97.8 261.5 111C261.9 124.2 250 131.833 244 134" stroke="#FF1717" stroke-linecap="round"/>
<path id="Vector 94" d="M28 216.5C32 215.5 42 202 49 202C56 202 64 214.5 72.5 214.5C81 214.5 92 203.5 100 204C108 204.5 116.5 217.5 127.5 215.5C138.5 213.5 145.5 203.5 153.5 202.5C161.5 201.5 175 210.5 184.5 207.5C192.1 205.1 195 198.5 195.5 195.5" stroke="#FF1717" stroke-linecap="round"/>
<path id="Vector 95" d="M30.5 204C32 204 45.5 214 49.5 214C53.5 214 63 204 72 204C81 204 87.5 215 99 215.5C110.5 216 116.5 204 126.5 205C136.5 206 144 214 154 212C164 210 174.5 199 178.5 197C182.5 195 192 197.5 195 196C198 194.5 206 189.5 209 179.5" stroke="#FF1717" stroke-linecap="round"/>
<path id="Vector 96" d="M29.5 205C31 202 36.5 196.5 55.5 197.5C74.5 198.5 95 202.5 112 201.5C129 200.5 145.5 198.5 155.5 194.5C165.5 190.5 178.5 179.5 184 172" stroke="#FF1717" stroke-linecap="round"/>
<path id="Vector 97" d="M123 201C134.667 200.333 160.4 198.2 170 195C179.6 191.8 190.167 182.333 193.5 178.5" stroke="#FF1717" stroke-linecap="round"/>
<path id="Vector 98" d="M165.5 174C173.333 174 193.7 168.4 200.5 160C207.3 151.6 213.667 144.167 216 141.5" stroke="#FF1717" stroke-linecap="round"/>
<path id="Vector 99" d="M183 170.5C186 174.833 197.5 182.5 215 178" stroke="#FF1717" stroke-linecap="round"/>
<path id="Vector 100" d="M204 173C207.333 177.333 217.6 183.4 230 169" stroke="#FF1717" stroke-linecap="round"/>
<path id="Vector 101" d="M221 167.5C224.667 169.667 233.6 171.4 240 161C242.8 156.2 244.333 152 244.5 151" stroke="#FF1717" stroke-linecap="round"/>
<path id="Vector 102" d="M234.5 153C240.833 154 253.4 150.6 253 129" stroke="#FF1717" stroke-linecap="round"/>
<path id="Vector 103" d="M204.5 155.5C208.5 151.5 214.937 148.517 219 152C222.5 155 226 162.5 220 167.5C214.664 171.947 210.187 167.904 209.5 165.5C208.214 161 213 157 215 161" stroke="#FF1717" stroke-linecap="round"/>
<path id="Vector 104" d="M214 144C218 140 226.437 137.054 230.5 140.537C234 143.537 237.5 151.037 231.5 156.037C226.164 160.484 221.687 156.441 221 154.037C219.714 149.537 224 146 226.5 149.537" stroke="#FF1717" stroke-linecap="round"/>
<path id="Vector 105" d="M223.5 129.5C227.5 125.5 234.437 121.517 238.5 125C242 128 246.5 135.037 240.5 140.037C235.164 144.484 230.687 140.441 230 138.037C228.714 133.537 233.5 130 235.5 133.537" stroke="#FF1717" stroke-linecap="round"/>
<path id="Vector 106" d="M229.5 113C233.5 109 240.623 102.5 246.5 107.537C250 110.537 253.5 118.037 247.5 123.037C242.164 127.484 237.687 123.441 237 121.037C235.714 116.537 240.5 113 242.5 116.537" stroke="#FF1717" stroke-linecap="round"/>
<path id="Vector 107" d="M224.5 169C223.833 170.667 220.1 173.7 210.5 172.5" stroke="#FF1717" stroke-linecap="round"/>
<path id="Vector 108" d="M238.5 153C238.333 155.5 235.8 160.9 227 162.5" stroke="#FF1717" stroke-linecap="round"/>
<path id="Vector 109" d="M248.5 132C248.5 135.167 246.8 142.6 240 147" stroke="#FF1717" stroke-linecap="round"/>
<path id="Vector 110" d="M242 95C247 95.3333 256.5 100.5 254.5 118.5" stroke="#FF1717" stroke-linecap="round"/>
<path id="Vector 111" d="M200.5 160C198.833 162.333 196.5 169.5 200.5 172C205.067 174.854 209 170.5 209 168C209 165.5 205.82 160.5 202.5 165.5" stroke="#FF1717" stroke-linecap="round"/>
<path id="Vector 112" d="M207 176C205.167 177 200.4 177.7 192 172.5" stroke="#FF1717" stroke-linecap="round"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 16 KiB

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 1007 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 339 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 142 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 175 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 151 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 664 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 701 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 743 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 646 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 584 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 376 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 322 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 287 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 844 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

View File

@@ -0,0 +1,117 @@
---
date: 2025-10-25
title: "New Year's Eve Parties"
draft: false
cover: ./images/cover.png
description: "An overview of our annual New Year's Eve parties, from Gatsby to Occult."
tags: ["event", "webdev", "party", "design"]
---
import Image from "@components/Image.astro"
import ImageSlider from "@components/ImageSlider.svelte"
import ImageGallery from "@components/ImageGallery.svelte"
<ImageGallery client:load/>
Since 2019, my flat-share has been hosting a New Year's Eve party. Every year, we chose a special theme that determined the costumes and decorations.
I always took the parties as an opportunity to be creative and designed digital invitation cards for each party, sometimes simpler, sometimes more complex.
### 2019-2020: The Great Gatsby
For the "The Great Gatsby" party, a visualizer was projected onto the wall with a beamer. Instead of a complex website, there was only a static invitation page, supplemented by printed invitation cards. The tech stack was rather unusual with MongoDB, ExpressJs and SocketIO. The frontend was quite complex, but was developed completely without a framework. The project escalated and a chat and a party management system were implemented. In addition, an extensive Google Sheet was created to track expenses, number of guests and paid contributions.
import gg1 from "./images/gg-party-1.jpg"
import gg2 from "./images/gg-party-2.jpg"
import gg3 from "./images/gg-party-3.jpg"
import gg4 from "./images/gg-2.jpg"
import gg5 from "./images/gg-icons.jpg"
<ImageSlider title="The Great Gatsby" client:load>
<Image src={gg1} alt="Photo of our buffet" />
<Image src={gg2} alt="Photo of our buffet" />
<Image src={gg3} alt="Photo of the cocktail party" />
<Image src={gg4} alt="Photo of the Gatsby party" />
<Image src={gg5} alt="Icons for the screen design" />
</ImageSlider>
### [2022-2023: Harry Potter](https://s23.app.max-richter.dev)
After no party could take place in recent years due to Covid, we decided on the motto "Harry Potter" as my flatmate is a big fan. This time there was a more complex invitation website with an animated WebGL "Talking Hat" that assigned a house to each guest. After registration, the guests were automatically stored in a Google Sheet. There was an admin interface where we could award points to the individual houses, and an interactive quiz that guests could play on the mobile website. The frontend was implemented with SvelteKit, tRPC, Prisma and SQLite.
import hp1 from "./images/hp-1-start.png"
import hp2 from "./images/hp-2-start.png"
import hp3 from "./images/hp-3-need-name.png"
import hp4 from "./images/hp-4-which-house.png"
import hp5 from "./images/hp-5-select-house.png"
import hp6 from "./images/hp-6-attendance-probability.png"
import hp7 from "./images/hp-7-invitation-card.png"
<ImageSlider title="Harry Potter and the Talking Hat" client:load>
<Image src={hp1} alt="Harry Potter Party Start" />
<Image src={hp2} alt="Harry Potter Party Start 2" />
<Image src={hp3} alt="Harry Potter Party Need Name" />
<Image src={hp4} alt="Harry Potter Party Which House" />
<Image src={hp5} alt="Harry Potter Party Select House" />
<Image src={hp6} alt="Harry Potter Party Attendance Probability" />
<Image src={hp7} alt="Harry Potter Party Invitation Card" />
</ImageSlider>
### [2023-2024: Venetian Masked Ball](https://s24.app.max-richter.dev)
There was also an animated invitation website for the "Venetian Masked Ball". Guests could enter their name, from which a suitable title of nobility was then generated using an LLM. A portrait was then generated from the name and some information about their appearance, which was displayed in a kind of ancestral gallery. Technically, SvelteKit was used again, but this time with a PocketBase backend.
import venice1 from "./images/venice-1-start.png"
import venice2 from "./images/venice-2-mask.png"
import venice3 from "./images/venice-3-invitation-test.png"
import venice4 from "./images/venice-4-generate-portrait.png"
import venice5 from "./images/venice-5-portrait.png"
import venice6 from "./images/venice-6-gallery.png"
import venice7 from "./images/venice-1.jpg"
import Mask from "./components/Mask.svelte"
<ImageSlider title="Venetian Masked Ball" client:load>
<Image src={venice1} alt="Venice Party Start" />
<Image src={venice2} alt="Venice Party Mask" />
<picture alt="Animiertes SVG Maske" description="asd">
<Mask client:load/>
</picture>
<Image src={venice3} alt="Venice Party Invitation Test" />
<Image src={venice4} alt="Venice Party Generate Portrait" />
<Image src={venice5} alt="Venice Party Portrait" />
<Image src={venice6} alt="Venice Party Gallery" />
<Image src={venice7} alt="Photo of the Venetian Masked Ball Party" />
</ImageSlider>
### [2024-2025: Everything is Fashion](https://s25.app.max-richter.dev)
For "Everything is Fashion" there was again an animated invitation website, but without interactive elements at the party itself. In keeping with the theme "everything is fashion", I wanted to shoot a teaser that refers to physical materials.
import eif1 from "./images/eif-party-1.jpg"
import eif2 from "./images/eif-video.mp4?url"
import videoUrl from "./images/eif-teaser.mp4?url"
<ImageSlider title="Everything is Fashion" client:load>
<picture>
<video src={videoUrl} controls alt="Fashion Party Teaser Video" />
</picture>
<picture>
<video src={eif2} controls alt="Fashion Party Teaser Video" />
</picture>
<Image src={eif1} alt="Photo of the Fashion Party" />
</ImageSlider>
### 2025-2026: Occult
For the upcoming New Year's Eve party 2025-2026, we are planning an "Occult" theme that will create a mystical and mysterious atmosphere.
import occult1 from "./images/occult-1.png"
import occult2 from "./images/occult-2.png"
import occult3 from "./images/occult-3.png"
import occult4 from "./images/occult-4.png"
<ImageSlider title="Everything is Fashion" client:load>
<Image src={occult1} alt="Occult Party" />
<Image src={occult2} alt="Occult Party" />
<Image src={occult3} alt="Occult Party" />
<Image src={occult4} alt="Occult Party" />
</ImageSlider>

View File

@@ -0,0 +1,117 @@
---
date: 2025-10-25
title: Silvester-Partys
draft: false
cover: ./images/cover.png
description: "Eine Übersicht über unsere jährlichen Silvester-Partys, von Gatsby bis Okkult."
tags: ["event", "webdev", "party", "design"]
---
import Image from "@components/Image.astro"
import ImageSlider from "@components/ImageSlider.svelte"
import ImageGallery from "@components/ImageGallery.svelte"
<ImageGallery client:load/>
Seit 2019 veranstaltet meine WG eine Silvester Party. Dafür haben wir jedes Jahr ein besonderes Motto ausgewählt welches dann die Kostüme und die Dekoration bestimmt.
Ich hab die Parties immer zum Anlass genommen um mich einmal kreativ auszuleben und habe für jede Party mal einfachere mal komplexere Digital Einladungskarten gestaltet.
### 2019-2020: The Great Gatsby
Für die "The Great Gatsby" Party wurde ein Visualizer mit einem Beamer an die Wand projiziert. Anstelle einer komplexen Website gab es nur eine statische Einladungsseite, ergänzt durch gedruckte Einladungskarten. Der Tech-Stack war mit MongoDB, ExpressJs und SocketIO eher ungewöhnlich. Das Frontend war recht komplex, wurde aber komplett ohne Framework entwickelt. Das Projekt eskalierte und es wurden ein Chat sowie ein Party-Management-System implementiert. Zusätzlich wurde ein umfangreiches Google Sheet erstellt, um Ausgaben, Gästeanzahl und bezahlte Beiträge zu verfolgen.
import gg1 from "./images/gg-party-1.jpg"
import gg2 from "./images/gg-party-2.jpg"
import gg3 from "./images/gg-party-3.jpg"
import gg4 from "./images/gg-2.jpg"
import gg5 from "./images/gg-icons.jpg"
<ImageSlider title="The Great Gatsby" client:load>
<Image src={gg1} alt="Foto von unserem Buffet" />
<Image src={gg2} alt="Foto von unserem Buffet" />
<Image src={gg3} alt="Foto der Cocktail Party" />
<Image src={gg4} alt="Foto der Gatsby Party" />
<Image src={gg5} alt="Icons fürs Screendesign" />
</ImageSlider>
### [2022-2023: Harry Potter](https://s23.app.max-richter.dev)
Nachdem die letzten Jahre wegen Covid keine Party stattfinden konnte, entschieden wir uns für das Motto "Harry Potter", da meine Mitbewohnerin ein großer Fan ist. Diesmal gab es eine komplexere Einladungswebsite mit einem animierten WebGL "Talking Hat", der jedem Gast ein Haus zuordnete. Nach der Anmeldung wurden die Gäste automatisch in einem Google Sheet hinterlegt. Es gab ein Admin-Interface, in dem wir Punkte an die einzelnen Häuser vergeben konnten, und ein interaktives Quiz, das die Gäste auf der mobilen Website mitspielen konnten. Das Frontend wurde mit SvelteKit, tRPC, Prisma und SQLite umgesetzt.
import hp1 from "./images/hp-1-start.png"
import hp2 from "./images/hp-2-start.png"
import hp3 from "./images/hp-3-need-name.png"
import hp4 from "./images/hp-4-which-house.png"
import hp5 from "./images/hp-5-select-house.png"
import hp6 from "./images/hp-6-attendance-probability.png"
import hp7 from "./images/hp-7-invitation-card.png"
<ImageSlider title="Harry Potter und der sprechende Hut" client:load>
<Image src={hp1} alt="Harry Potter Party Start" />
<Image src={hp2} alt="Harry Potter Party Start 2" />
<Image src={hp3} alt="Harry Potter Party Need Name" />
<Image src={hp4} alt="Harry Potter Party Which House" />
<Image src={hp5} alt="Harry Potter Party Select House" />
<Image src={hp6} alt="Harry Potter Party Attendance Probability" />
<Image src={hp7} alt="Harry Potter Party Invitation Card" />
</ImageSlider>
### [2023-2024: Venezianischer Maskenball](https://s24.app.max-richter.dev)
Auch für den "Venezianischen Maskenball" gab es wieder eine animierte Einladungswebsite. Die Gäste konnten ihren Namen eingeben, aus dem dann mittels eines LLM ein passender Adelstitel generiert wurde. Aus dem Namen und einigen Angaben zum Aussehen wurde anschließend ein Porträt generiert, das in einer Art Ahnengalerie angezeigt wurde. Technisch wurde wieder auf SvelteKit gesetzt, diesmal jedoch mit einem PocketBase-Backend.
import venice1 from "./images/venice-1-start.png"
import venice2 from "./images/venice-2-mask.png"
import venice3 from "./images/venice-3-invitation-test.png"
import venice4 from "./images/venice-4-generate-portrait.png"
import venice5 from "./images/venice-5-portrait.png"
import venice6 from "./images/venice-6-gallery.png"
import venice7 from "./images/venice-1.jpg"
import Mask from "./components/Mask.svelte"
<ImageSlider title="Venezianischer Maskenball" client:load>
<Image src={venice1} alt="Venice Party Start" />
<Image src={venice2} alt="Venice Party Mask" />
<picture alt="Animiertes SVG Maske" description="asd">
<Mask client:load/>
</picture>
<Image src={venice3} alt="Venice Party Invitation Test" />
<Image src={venice4} alt="Venice Party Generate Portrait" />
<Image src={venice5} alt="Venice Party Portrait" />
<Image src={venice6} alt="Venice Party Gallery" />
<Image src={venice7} alt="Foto der Venezianischer Maskenball Party" />
</ImageSlider>
### [2024-2025: Everything is Fashion](https://s25.app.max-richter.dev)
Für "Everything is Fashion" gab es wieder eine animierte Einladungswebsite, allerdings ohne interaktive Elemente auf der Party selbst. Passend zum Thema "everything is fashion" wollte ich einen Teaser drehen, der sich auf physische Materialien bezieht.
import eif1 from "./images/eif-party-1.jpg"
import eif2 from "./images/eif-video.mp4?url"
import videoUrl from "./images/eif-teaser.mp4?url"
<ImageSlider title="Everything is Fashion" client:load>
<picture>
<video src={videoUrl} controls alt="Fashion Party Teaser Video" />
</picture>
<picture>
<video src={eif2} controls alt="Fashion Party Teaser Video" />
</picture>
<Image src={eif1} alt="Foto der Fashion Party" />
</ImageSlider>
### 2025-2026: Okkult
Für die kommende Silvesterparty 2025-2026 planen wir ein "Okkult"-Thema, das eine mystische und geheimnisvolle Atmosphäre schaffen wird.
import occult1 from "./images/occult-1.png"
import occult2 from "./images/occult-2.png"
import occult3 from "./images/occult-3.png"
import occult4 from "./images/occult-4.png"
<ImageSlider title="Everything is Fashion" client:load>
<Image src={occult1} alt="Okkult Party" />
<Image src={occult2} alt="Okkult Party" />
<Image src={occult3} alt="Okkult Party" />
<Image src={occult4} alt="Okkult Party" />
</ImageSlider>

View File

@@ -1,39 +1,33 @@
import { rgbaToThumbHash } from "thumbhash";
import ExifReader from 'exifreader';
import ExifReader from "exifreader";
import type { ImageMetadata } from "astro";
import { readFile } from "node:fs/promises";
import sharp from "sharp";
import { createHash } from "node:crypto";
import { promises as fs } from "node:fs";
import path from "node:path";
let s: typeof import("sharp") | undefined;
async function getSharp(): Promise<typeof import("sharp") | undefined> {
if (s) return s;
s = (await import("sharp")).default;
return s;
}
export async function generateThumbHash(
buffer: ArrayBuffer,
): Promise<string | undefined> {
if (!buffer) return;
export async function generateThumbHash(image: ImageMetadata & { fsPath?: string }) {
const sp = sharp(buffer);
const sharp = await getSharp();
if (!sharp) return;
const meta = await sp.metadata();
const scaleFactor = 100 / Math.max(meta.width, meta.height);
const smallWidth = Math.floor(meta.width * scaleFactor);
const smallHeight = Math.floor(meta.height * scaleFactor);
const scaleFactor = 100 / Math.max(image.width, image.height);
const smallWidth = Math.floor(image.width * scaleFactor);
const smallHeight = Math.floor(image.height * scaleFactor);
try {
const smallImg = await sharp(image.fsPath)
.resize(smallWidth, smallHeight)
.withMetadata()
.raw()
.ensureAlpha()
.toBuffer();
const buffer = rgbaToThumbHash(smallWidth, smallHeight, smallImg);
return Buffer.from(buffer).toString("base64");
} catch (error) {
console.log(`Could not generate thumbhash for ${image.fsPath}`, error)
return ""
}
const smallImg = await sp
.resize(smallWidth, smallHeight)
.withMetadata()
.raw()
.ensureAlpha()
.toBuffer();
const hashBuffer = rgbaToThumbHash(smallWidth, smallHeight, smallImg);
return Buffer.from(hashBuffer).toString("base64");
}
const allowedExif = [
@@ -52,31 +46,74 @@ const allowedExif = [
"Model",
];
export async function getExifData(image: ImageMetadata) {
if (image.format === "svg") return undefined; // SVGs don't have EXIF data")
const sharp = await getSharp();
if (!sharp) return;
const imagePath = (image as ImageMetadata & { fsPath: string }).fsPath;
export async function getImageBuffer(
image: ImageMetadata,
): Promise<ArrayBuffer> {
if (image.format === "svg") return undefined; // SVGs don't have EXIF data
const imagePath = (image as ImageMetadata & { fsPath: string }).fsPath ??
image.src;
if (!imagePath) return undefined;
try {
const buffer = await sharp(imagePath).toBuffer();
const tags = await ExifReader.load(buffer, { async: true });
const out: Record<string, any> = {};
let hasExif = false;
for (const key of allowedExif) {
if (!tags[key]) continue;
hasExif = true;
out[key] = tags[key]?.description;
if (imagePath.startsWith("https://") || imagePath.startsWith("http://")) {
const res = await fetch(imagePath, { signal: AbortSignal.timeout(5000) });
return await res.arrayBuffer();
} else {
const b = await readFile(imagePath);
return b.buffer as ArrayBuffer;
}
} catch (err) {
return undefined;
}
}
return hasExif ? out : undefined;
} catch (error) {
export async function getExifData(buffer: ArrayBuffer) {
if (!buffer) return undefined;
console.log(`Error reading EXIF data from ${imagePath}`, error);
return undefined
const tags = await ExifReader.load(buffer, { async: true });
const out: Record<string, unknown> = {};
let hasExif = false;
for (const key of allowedExif) {
if (!tags[key]) continue;
hasExif = true;
out[key] = tags[key]?.description;
}
return hasExif ? out : undefined;
}
const CACHE_DIR = path.join(process.cwd(), 'node_modules', '.astro', 'image-cache');
export async function getProcessedImage(image: ImageMetadata) {
const buffer = await getImageBuffer(image);
if (!buffer) {
return { thumbhash: undefined, exif: undefined };
}
const hash = createHash('sha256').update(new Uint8Array(buffer)).digest('hex');
const cacheFile = path.join(CACHE_DIR, `${hash}.json`);
try {
const cachedData = await fs.readFile(cacheFile, 'utf-8');
return JSON.parse(cachedData);
} catch (e) {
if (e.code !== 'ENOENT') {
console.error("Failed to read from image cache:", e);
}
}
const thumbhash = await generateThumbHash(buffer);
const exif = await getExifData(buffer);
try {
await fs.mkdir(CACHE_DIR, { recursive: true });
await fs.writeFile(cacheFile, JSON.stringify({ thumbhash, exif }), 'utf-8');
} catch (writeError) {
console.error("Failed to write to image cache:", writeError);
}
return { thumbhash, exif };
}

48
src/helpers/markdown.ts Normal file
View File

@@ -0,0 +1,48 @@
import MarkdownIt from "markdown-it";
const parser = new MarkdownIt();
export function readDuration(markdown: string): number | undefined {
if (!markdown) return;
const words = markdown.split(" ")?.filter(Boolean)?.length;
if (!words) return;
return words && Math.round(words / 250);
}
export function markdownToHtml(markdown: string): string {
if (!markdown) return "";
const md = new MarkdownIt({
html: false, // set to true only if you trust the source
linkify: true,
typographer: true,
breaks: true,
});
// Convert -> sanitize
const unsafeHtml = md.render(markdown);
return unsafeHtml;
}
export function markdownToText(markdown: string): string {
if (!markdown) return "";
return parser
.render(markdown)
.split("\n")
.map((str) => str.trim())
.map((str) => {
return str.replace(/<\/?[^>]+(>|$)/g, "").split("\n");
})
.flat()
.filter((str) =>
!str.startsWith("import") &&
!str.startsWith("export") &&
!str.startsWith("#") &&
!str.startsWith("const") &&
!str.startsWith("function") &&
!str.startsWith("export") &&
!str.startsWith("import") &&
!str.startsWith("&lt;") &&
!str.startsWith("let") &&
str.length > 0
)
.join(" ");
}

View File

@@ -1,25 +0,0 @@
import MarkdownIt from 'markdown-it';
const parser = new MarkdownIt();
export default function markdownToText(markdown: string): string {
return parser
.render(markdown)
.split('\n')
.map((str) => str.trim())
.map((str) => {
return str.replace(/<\/?[^>]+(>|$)/g, '').split('\n');
})
.flat()
.filter((str) => !str.startsWith("import")
&& !str.startsWith("export")
&& !str.startsWith("#")
&& !str.startsWith("const")
&& !str.startsWith("function")
&& !str.startsWith("export")
&& !str.startsWith("import")
&& !str.startsWith("&lt;")
&& !str.startsWith("let")
&& str.length > 0
)
.join(' ');
}

View File

@@ -1,10 +1,61 @@
export async function listResource(id: string): Promise<any[]> {
export type MemoriumFile = {
type: "file";
name: string;
path: string;
modTime: string;
mime: string;
size: string;
content: any;
};
export type MemoriumDir = {
type: "dir";
name: string;
path: string;
modTime: string;
mime: string;
size: string;
content: MemoriumEntry[];
};
export type MemoriumEntry = MemoriumFile | MemoriumDir;
const SERVER_URL = "https://marka.max-richter.dev";
//const SERVER_URL = "http://localhost:8080";
export async function listResource(
id: string,
): Promise<MemoriumEntry | undefined> {
const url = `${SERVER_URL}/resources/${id}`;
try {
const response = await fetch(
`http://localhost:8080/resources?name=${id}`,
);
return await response.json();
} catch (error) {
return []
const response = await fetch(url, { signal: AbortSignal.timeout(5000) });
if (response.ok) {
const json = await response.json();
if (json.type == "dir") {
return {
...json,
content: json.content.filter((res: MemoriumEntry) =>
res.mime === "application/markdown"
),
};
}
return json;
}
} catch (_e) {
console.log("Failed to get: ", url);
return;
}
}
export function getImageUrl(input: string): string {
if (!input) {
return;
}
if (input.startsWith("https://") || input.startsWith("http://")) {
return input;
}
if (input.startsWith("/")) {
return `${SERVER_URL}${input}`;
}
return `${SERVER_URL}/${input}`;
}

View File

@@ -1,10 +1,9 @@
export const languages = {
en: 'English',
de: 'Deutsch',
en: "English",
de: "Deutsch",
};
export const defaultLang = 'de';
export const defaultLang = "de";
export const ui = {
en: {
@@ -15,15 +14,24 @@ export const ui = {
"read-more": "Read More",
"latest-posts": "Latest Posts",
"latest-projects": "Latest Projects",
'home.title': 'Hi, Im Max :)',
'website-source': 'source',
'home.subtitle': 'Trained Media Designer, Blender Nerd, Developer and Hardware Tinkerer.',
'nav.blog': 'Blog',
'nav.projects': 'Projects',
'nav.resources': 'Resources',
'nav.photos': 'Photos',
'toc.title': 'Table of Contents',
"home.title": "Hi, Im Max :)",
"website-source": "source",
"home.subtitle":
"Trained Media Designer, Blender Nerd, Developer and Hardware Tinkerer.",
"nav.blog": "Blog",
"nav.projects": "Projects",
"nav.resources": "Resources",
"nav.photos": "Photos",
"toc.title": "Table of Contents",
"resume": "Resume",
"articles": "📰 Articles",
"articles.description": "Random articles that impressed me while reading.",
"movies": "🎥 Movies",
"movies.description": "Watched, not watched, found good, found bad.",
"recipes": "🍲 Recipes",
"recipes.description": "Things I have cooked or want to cook.",
"series": "📺 Series",
"series.description": "Series I have watched or plan to watch.",
},
de: {
"en": "English",
@@ -33,15 +41,26 @@ export const ui = {
"latest-posts": "Neueste Posts",
"latest-projects": "Neueste Projekte",
"read-more": "weiterlesen",
'home.title': 'Hi, ich bin Max :)',
'website-source': 'Sourcecode',
'home.subtitle': 'Ausgebildeter Mediengestalter, Blender Nerd, Entwickler und Hardware Bastler.',
'nav.blog': 'Blog',
'nav.projects': 'Projekte',
'nav.resources': 'Resources',
'nav.photos': 'Fotos',
"home.title": "Hi, ich bin Max :)",
"website-source": "Sourcecode",
"home.subtitle":
"Ausgebildeter Mediengestalter, Blender Nerd, Entwickler und Hardware Bastler.",
"nav.blog": "Blog",
"nav.projects": "Projekte",
"nav.resources": "Resourcen",
"nav.photos": "Fotos",
"resume": "Lebenslauf",
'toc.title': 'Inhaltsverzeichnis',
"toc.title": "Inhaltsverzeichnis",
"articles": "📰 Artikel",
"articles.description":
"Random Artikel die mich beim Lesen beindruckt haben",
"movies": "🎥 Filme",
"movies.description":
"Gesehen, nicht gesehen, für gut befunden, für schlecht befunden.",
"recipes": "🍲 Rezepte",
"recipes.description": "Sachen die ich gekocht habe oder kochen will",
"series": "📺 Serien",
"series.description": "Serien die ich angeguckt habe",
},
} as const;

View File

@@ -3,6 +3,7 @@ import { getCollection, render } from "astro:content";
import { getLocale } from "astro-i18n-aut";
import { filterCollection, parseSlug } from "@i18n/utils";
import MetaTags from "@components/MetaTags.astro";
import { markdownToText } from "@helpers/markdown";
const locale = getLocale(Astro.url);
@@ -32,5 +33,9 @@ if (!page) {
const { Content } = await render(page);
---
<MetaTags title={page.data.title} cover={page.data.cover?.src} />
<MetaTags
title={page.data.title}
cover={page.data.cover?.src}
description={page.data.description || markdownToText(page.body).slice(0, 200)}
/>
<Content />

View File

@@ -3,6 +3,7 @@ import { getCollection, render } from "astro:content";
import { getLocale } from "astro-i18n-aut";
import { filterCollection, parseSlug } from "@i18n/utils";
import MetaTags from "@components/MetaTags.astro";
import { markdownToText } from "@helpers/markdown";
const locale = getLocale(Astro.url);
@@ -32,5 +33,9 @@ if (!page) {
const { Content } = await render(page);
---
<MetaTags title={page.data.title} cover={page.data.cover?.src} />
<MetaTags
title={page.data.title}
cover={page.data.cover?.src}
description={page.data.description || markdownToText(page.body).slice(0, 200)}
/>
<Content />

Some files were not shown because too many files have changed in this diff Show More