diff --git a/astro.config.ts b/astro.config.ts index 2c601bd..ff25041 100644 --- a/astro.config.ts +++ b/astro.config.ts @@ -1,17 +1,16 @@ +import mdx from "@astrojs/mdx"; +import sitemap from "@astrojs/sitemap"; import { defineConfig } from "astro/config"; import UnoCSS from "unocss/astro"; -import sitemap from '@astrojs/sitemap'; - -import mdx from "@astrojs/mdx"; // https://astro.build/config export default defineConfig({ - integrations: [ - UnoCSS(), - mdx({ - extendMarkdownConfig: true, - }), - sitemap() - ], - site: "https://twofortyeight.net", + integrations: [ + UnoCSS(), + mdx({ + extendMarkdownConfig: true, + }), + sitemap(), + ], + site: "https://twofortyeight.net", }); diff --git a/public/site.webmanifest b/public/site.webmanifest index 45dc8a2..9591150 100644 --- a/public/site.webmanifest +++ b/public/site.webmanifest @@ -1 +1,19 @@ -{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"} \ No newline at end of file +{ + "name": "", + "short_name": "", + "icons": [ + { + "src": "/android-chrome-192x192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "/android-chrome-512x512.png", + "sizes": "512x512", + "type": "image/png" + } + ], + "theme_color": "#ffffff", + "background_color": "#ffffff", + "display": "standalone" +} diff --git a/src/content.config.ts b/src/content.config.ts index 21d807f..9877aea 100644 --- a/src/content.config.ts +++ b/src/content.config.ts @@ -3,66 +3,66 @@ import { defineCollection, z } from "astro:content"; import { glob } from "astro/loaders"; export function toSlug(text: string) { - return text - .trim() - .replace(/[^\w\s-]/g, "") - .replace(/[\s_-]+/g, "-") - .replace(/^-+|-+$/g, ""); + return text + .trim() + .replace(/[^\w\s-]/g, "") + .replace(/[\s_-]+/g, "-") + .replace(/^-+|-+$/g, ""); } function dateToEST(date: Date, ctx: z.RefinementCtx) { - const estOffsetMs = -5 * 60 * 60 * 1000; // est offset (-5 hours) - return new Date(date.getTime() - estOffsetMs); + const estOffsetMs = -5 * 60 * 60 * 1000; // est offset (-5 hours) + return new Date(date.getTime() - estOffsetMs); } const blog = defineCollection({ - loader: glob({ base: "./src/posts", pattern: "**/*.{md,mdx}" }), - schema: z - .object({ - title: z.string(), - slug: z.string().optional(), - description: z.string(), - date: z.date().transform(dateToEST), - tags: z.array(z.string()).default([]), - image: z.string().optional(), - }) - .transform((object) => { - return { - ...object, - slug: object.slug || toSlug(object.title), - }; - }), + loader: glob({ base: "./src/posts", pattern: "**/*.{md,mdx}" }), + schema: z + .object({ + title: z.string(), + slug: z.string().optional(), + description: z.string(), + date: z.date().transform(dateToEST), + tags: z.array(z.string()).default([]), + image: z.string().optional(), + }) + .transform((object) => { + return { + ...object, + slug: object.slug || toSlug(object.title), + }; + }), }); const projects = defineCollection({ - loader: glob({ base: "./src/projects", pattern: "**/*.{md,mdx}" }), - schema: z - .object({ - title: z.string(), - slug: z.string().optional(), - url: z.string().optional(), - description: z.string(), - tags: z.array(z.string()).default([]), - image: z.string().optional(), - startDate: z.date().transform(dateToEST), - endDate: z.date().transform(dateToEST).optional(), - }) - .transform((object) => { - const slug = object.slug || toSlug(object.title); - const url = object.url || `/projects/${slug}`; - const startYear = object.startDate.getFullYear(); - const endYear = object.endDate?.getFullYear(); - const date = - startYear === endYear || startYear === new Date().getFullYear() - ? startYear - : `${startYear} – ${endYear ?? "now"}`; - return { - ...object, - slug, - date, - url, - }; - }), + loader: glob({ base: "./src/projects", pattern: "**/*.{md,mdx}" }), + schema: z + .object({ + title: z.string(), + slug: z.string().optional(), + url: z.string().optional(), + description: z.string(), + tags: z.array(z.string()).default([]), + image: z.string().optional(), + startDate: z.date().transform(dateToEST), + endDate: z.date().transform(dateToEST).optional(), + }) + .transform((object) => { + const slug = object.slug || toSlug(object.title); + const url = object.url || `/projects/${slug}`; + const startYear = object.startDate.getFullYear(); + const endYear = object.endDate?.getFullYear(); + const date = + startYear === endYear || startYear === new Date().getFullYear() + ? startYear + : `${startYear} – ${endYear ?? "now"}`; + return { + ...object, + slug, + date, + url, + }; + }), }); export const collections = { blog, projects }; diff --git a/src/lib/colors.ts b/src/lib/colors.ts index f940d6b..e08efd1 100644 --- a/src/lib/colors.ts +++ b/src/lib/colors.ts @@ -1,156 +1,156 @@ -import type { Rgb, Oklab } from "culori/fn"; -import { convertRgbToOklab, convertOklabToRgb } from "culori/fn"; +import type { Oklab, Rgb } from "culori/fn"; +import { convertOklabToRgb, convertRgbToOklab } from "culori/fn"; // palette of bipolar - beige export const defaultPalette: Rgb[] = [ - { r: 189, g: 149, b: 93, mode: "rgb" }, - { r: 114, g: 113, b: 168, mode: "rgb" }, - { r: 127, g: 174, b: 98, mode: "rgb" }, - { r: 57, g: 82, b: 38, mode: "rgb" }, + { r: 189, g: 149, b: 93, mode: "rgb" }, + { r: 114, g: 113, b: 168, mode: "rgb" }, + { r: 127, g: 174, b: 98, mode: "rgb" }, + { r: 57, g: 82, b: 38, mode: "rgb" }, ]; export type RGBArray = [number, number, number]; export type CompletePalette = { - palette: string[]; - dominant: string; - textColor: string; - secondaryTextColor: string; + palette: string[]; + dominant: string; + textColor: string; + secondaryTextColor: string; }; function oklabSaturation(color: Oklab) { - return Math.sqrt(color.a ** 2 + color.b ** 2); + return Math.sqrt(color.a ** 2 + color.b ** 2); } function oklabHue(color: Oklab) { - const rawDeg = Math.atan2(color.b, color.a) * (180 / Math.PI); - return (rawDeg + 360) % 360; + const rawDeg = Math.atan2(color.b, color.a) * (180 / Math.PI); + return (rawDeg + 360) % 360; } function oklabBrownCheck(color: Oklab) { - const hue = oklabHue(color); // degrees - const chroma = Math.sqrt(color.a ** 2 + color.b ** 2); + const hue = oklabHue(color); // degrees + const chroma = Math.sqrt(color.a ** 2 + color.b ** 2); - const isOrange = hue > 40 && hue < 80; - const isDark = color.l / 100 < 0.6; - const isDull = chroma / 100 < 0.03; + const isOrange = hue > 40 && hue < 80; + const isDark = color.l / 100 < 0.6; + const isDull = chroma / 100 < 0.03; - return isOrange && isDark && isDull; // its prolly brown + return isOrange && isDark && isDull; // its prolly brown } export function getPalette(colorThiefPalette: RGBArray[]): CompletePalette { - let rawPalette: Oklab[] = (colorThiefPalette || defaultPalette).map( - ([r, g, b]) => convertRgbToOklab({ r, g, b }), // manually convert to proper RGB type - ); - - // biome-ignore lint/style/noNonNullAssertion: bro shut up i JUST checked it - const rawDominant = rawPalette.shift()!; - - if (rawPalette.length < 3) { - console.log("not enough colors"); - - // if not enough colors, make everything else white/black - const fillerColor = - rawDominant.l / 100 > 0.6 ? { l: 0, b: 0, a: 0 } : { l: 100, b: 0, 1: 0 }; - - // keep original dominant color and whatever other colors we have - rawPalette = [ - ...rawPalette, - ...Array(Math.max(0, 4 - rawPalette.length)).fill(fillerColor), - ].slice(0, 4); - } - - // change secondary color to the most saturated form of itself that we received - // first we gotta get the hue of the color - const trueSecondaryHue = oklabHue(rawPalette[0]); - - // now we should check if all but one color is roughly achromatic. if not, then we shouldnt care about browns - const shouldBrownCheck = !( - rawPalette.filter((color) => Math.sqrt(color.a ** 2 + color.b ** 2) > 0.03) - .length < 2 && - oklabBrownCheck( - rawPalette.toSorted((a, b) => oklabSaturation(b) - oklabSaturation(a))[0], - ) - ); - - // then we can find colors with a hue difference of less than 15 deg.. (and theyre not brown) - const secondaryCandidates = rawPalette.filter( - (color) => - Math.abs(oklabHue(color) - trueSecondaryHue) <= 15 && - (shouldBrownCheck ? !oklabBrownCheck(color) : true), - ); - - // and then we can get the most saturated one! - let newSecondary = secondaryCandidates.sort( - (a, b) => oklabSaturation(b) - oklabSaturation(a), - )[0]; - - // if we found a better secondary choice, use that - // otherwise just sort by saturation - const saturationSort = (a: Oklab, b: Oklab) => - oklabSaturation(b) - - oklabSaturation(a) - - (shouldBrownCheck && oklabBrownCheck(b) ? 100 : 0); // harsh penalty for being brown - const palette = newSecondary - ? [ - newSecondary, - ...rawPalette.filter((color) => color !== newSecondary), - ].filter(Boolean) - : rawPalette.sort(saturationSort); - - if (Math.abs(palette[0].l - rawDominant.l) < 40) { - if (rawDominant.l > 50) { - palette[0].l = rawDominant.l - 30; - } else { - palette[0].l = rawDominant.l + 30; - } - } - // calculate text color from dominant color luminance - const textColor: Rgb = - rawDominant.l / 100 > 0.6 - ? { r: 0, g: 0, b: 0, mode: "rgb" } - : { r: 255, g: 255, b: 255, mode: "rgb" }; - - const secondaryTextColor: Rgb = - palette[0].l / 100 > 0.5 - ? { r: 0, g: 0, b: 0, mode: "rgb" } - : { r: 255, g: 255, b: 255, mode: "rgb" }; - - const result = { - palette: palette - .map(convertOklabToRgb) - .map(({ r, g, b }) => rgbToString({ r, g, b, a: 1 })), - textColor: rgbToString({ ...textColor, a: 1 }), - dominant: oklabToRgbString(rawDominant), - secondaryTextColor: rgbToString({ ...secondaryTextColor, a: 1 }), - }; - return result; + let rawPalette: Oklab[] = (colorThiefPalette || defaultPalette).map( + ([r, g, b]) => convertRgbToOklab({ r, g, b }), // manually convert to proper RGB type + ); + + // biome-ignore lint/style/noNonNullAssertion: bro shut up i JUST checked it + const rawDominant = rawPalette.shift()!; + + if (rawPalette.length < 3) { + console.log("not enough colors"); + + // if not enough colors, make everything else white/black + const fillerColor = + rawDominant.l / 100 > 0.6 ? { l: 0, b: 0, a: 0 } : { l: 100, b: 0, 1: 0 }; + + // keep original dominant color and whatever other colors we have + rawPalette = [ + ...rawPalette, + ...Array(Math.max(0, 4 - rawPalette.length)).fill(fillerColor), + ].slice(0, 4); + } + + // change secondary color to the most saturated form of itself that we received + // first we gotta get the hue of the color + const trueSecondaryHue = oklabHue(rawPalette[0]); + + // now we should check if all but one color is roughly achromatic. if not, then we shouldnt care about browns + const shouldBrownCheck = !( + rawPalette.filter((color) => Math.sqrt(color.a ** 2 + color.b ** 2) > 0.03) + .length < 2 && + oklabBrownCheck( + rawPalette.toSorted((a, b) => oklabSaturation(b) - oklabSaturation(a))[0], + ) + ); + + // then we can find colors with a hue difference of less than 15 deg.. (and theyre not brown) + const secondaryCandidates = rawPalette.filter( + (color) => + Math.abs(oklabHue(color) - trueSecondaryHue) <= 15 && + (shouldBrownCheck ? !oklabBrownCheck(color) : true), + ); + + // and then we can get the most saturated one! + const newSecondary = secondaryCandidates.sort( + (a, b) => oklabSaturation(b) - oklabSaturation(a), + )[0]; + + // if we found a better secondary choice, use that + // otherwise just sort by saturation + const saturationSort = (a: Oklab, b: Oklab) => + oklabSaturation(b) - + oklabSaturation(a) - + (shouldBrownCheck && oklabBrownCheck(b) ? 100 : 0); // harsh penalty for being brown + const palette = newSecondary + ? [ + newSecondary, + ...rawPalette.filter((color) => color !== newSecondary), + ].filter(Boolean) + : rawPalette.sort(saturationSort); + + if (Math.abs(palette[0].l - rawDominant.l) < 40) { + if (rawDominant.l > 50) { + palette[0].l = rawDominant.l - 30; + } else { + palette[0].l = rawDominant.l + 30; + } + } + // calculate text color from dominant color luminance + const textColor: Rgb = + rawDominant.l / 100 > 0.6 + ? { r: 0, g: 0, b: 0, mode: "rgb" } + : { r: 255, g: 255, b: 255, mode: "rgb" }; + + const secondaryTextColor: Rgb = + palette[0].l / 100 > 0.5 + ? { r: 0, g: 0, b: 0, mode: "rgb" } + : { r: 255, g: 255, b: 255, mode: "rgb" }; + + const result = { + palette: palette + .map(convertOklabToRgb) + .map(({ r, g, b }) => rgbToString({ r, g, b, a: 1 })), + textColor: rgbToString({ ...textColor, a: 1 }), + dominant: oklabToRgbString(rawDominant), + secondaryTextColor: rgbToString({ ...secondaryTextColor, a: 1 }), + }; + return result; } function rgbToString({ - r, - g, - b, - a, + r, + g, + b, + a, }: { - r: number; - g: number; - b: number; - a: number; + r: number; + g: number; + b: number; + a: number; }) { - if (a > 0) { - const colors = [r, g, b, a].join(","); - return `rgba(${colors})`; - } - const colors = [r, g, b].join(","); - return `rgb(${colors})`; + if (a > 0) { + const colors = [r, g, b, a].join(","); + return `rgba(${colors})`; + } + const colors = [r, g, b].join(","); + return `rgb(${colors})`; } function oklabToRgbString(color: Oklab) { - const { r, g, b }: Rgb = convertOklabToRgb(color); - return rgbToString({ - r: Math.round(r), - g: Math.round(g), - b: Math.round(b), - a: color.alpha || 1, - }); + const { r, g, b }: Rgb = convertOklabToRgb(color); + return rgbToString({ + r: Math.round(r), + g: Math.round(g), + b: Math.round(b), + a: color.alpha || 1, + }); } diff --git a/src/lib/garden.ts b/src/lib/garden.ts index 4197197..d69a5d0 100644 --- a/src/lib/garden.ts +++ b/src/lib/garden.ts @@ -1,9 +1,9 @@ -import { getCollection, type CollectionEntry } from "astro:content"; +import { type CollectionEntry, getCollection } from "astro:content"; export const posts: CollectionEntry<"blog">[] = ( - await getCollection("blog") + await getCollection("blog") ).sort((a, b) => b.data.date.valueOf() - a.data.date.valueOf()); export function getPosts(): CollectionEntry<"blog">[] { - return posts; + return posts; } diff --git a/src/lib/musicData.ts b/src/lib/musicData.ts index 38f6c61..d7acf7f 100644 --- a/src/lib/musicData.ts +++ b/src/lib/musicData.ts @@ -1,110 +1,110 @@ const ALBUM_MBID = "f6e2def5-3283-42ea-9352-d2f9c5cd36c8"; type MusicBrainzLookup = { - "cover-art-archive": { - front: boolean; - }; - title: string; - id: string; - "artist-credit": [ - { - name: string; - artist: { - name: string; - id: string; - "sort-name": string; - }; - joinphrase: string; - }, - ]; + "cover-art-archive": { + front: boolean; + }; + title: string; + id: string; + "artist-credit": [ + { + name: string; + artist: { + name: string; + id: string; + "sort-name": string; + }; + joinphrase: string; + }, + ]; }; type ListenBrainzStatsRes = { - payload: { - count: number; - releases: { - artist_mbids?: string[]; - artist_name: string; - artists: - | { - artist_credit_name: string; - artist_mbid: string; - join_phrase: string; - }[] - | null; - caa_id: string | null; - release_mbid?: string | null; - release_name: string; - }[]; - }; + payload: { + count: number; + releases: { + artist_mbids?: string[]; + artist_name: string; + artists: + | { + artist_credit_name: string; + artist_mbid: string; + join_phrase: string; + }[] + | null; + caa_id: string | null; + release_mbid?: string | null; + release_name: string; + }[]; + }; }; type MinimalAlbum = { - title: string; - artists: { name: string; mbid: string; joinPhrase: string }[]; - mbid: string; + title: string; + artists: { name: string; mbid: string; joinPhrase: string }[]; + mbid: string; }; let topAlbum: MinimalAlbum | undefined; let topAlbumColors: [number, number, number][]; export async function getAlbum(): Promise { - if (topAlbum) return topAlbum; - const mbid = ALBUM_MBID; + if (topAlbum) return topAlbum; + const mbid = ALBUM_MBID; - if (mbid !== undefined) { - let res = await fetch( - `https://musicbrainz.org/ws/2/release/${mbid}?fmt=json&inc=artists`, - { headers: { "User-Agent": "thrzl/xyz v0.1.0" } }, - ); - const results: MusicBrainzLookup = await res.json(); - topAlbum = { - title: results.title, - artists: results["artist-credit"].map((artist) => ({ - name: artist.name, - joinPhrase: artist.joinphrase, - mbid: artist.artist.id, - })), - mbid: results.id, - }; - return topAlbum; - } - // https://open.spotify.com/oembed?url=https://open.spotify.com/album/40XTuBTvlOkvkT2LqF2AE7 - let res = await fetch( - `https://api.listenbrainz.org/1/stats/user/thrizzle/releases?range=this_week`, - ); - let result: ListenBrainzStatsRes = await res.json(); - let topResult = result.payload.releases.find( - (release) => release.caa_id !== null, - ); - topAlbum = - topResult !== undefined - ? { - title: topResult.release_name, - artists: (topResult.artists || []).map((artist) => ({ - name: artist.artist_credit_name, - joinPhrase: artist.join_phrase, - mbid: artist.artist_mbid, - })), - mbid: topResult.release_mbid!, - } - : undefined; - return topAlbum; + if (mbid !== undefined) { + const res = await fetch( + `https://musicbrainz.org/ws/2/release/${mbid}?fmt=json&inc=artists`, + { headers: { "User-Agent": "thrzl/xyz v0.1.0" } }, + ); + const results: MusicBrainzLookup = await res.json(); + topAlbum = { + title: results.title, + artists: results["artist-credit"].map((artist) => ({ + name: artist.name, + joinPhrase: artist.joinphrase, + mbid: artist.artist.id, + })), + mbid: results.id, + }; + return topAlbum; + } + // https://open.spotify.com/oembed?url=https://open.spotify.com/album/40XTuBTvlOkvkT2LqF2AE7 + const res = await fetch( + `https://api.listenbrainz.org/1/stats/user/thrizzle/releases?range=this_week`, + ); + const result: ListenBrainzStatsRes = await res.json(); + const topResult = result.payload.releases.find( + (release) => release.caa_id !== null, + ); + topAlbum = + topResult !== undefined + ? { + title: topResult.release_name, + artists: (topResult.artists || []).map((artist) => ({ + name: artist.artist_credit_name, + joinPhrase: artist.join_phrase, + mbid: artist.artist_mbid, + })), + mbid: topResult.release_mbid!, + } + : undefined; + return topAlbum; } export async function getColors( - album: MinimalAlbum | undefined, + album: MinimalAlbum | undefined, ): Promise<[number, number, number][]> { - if (!album) return []; - if (topAlbumColors) return topAlbumColors; - const paletteRes = await fetch( - `https://calore.twofortyeight.net/?image=https://coverartarchive.org/release/${album?.mbid}/front`, - ); + if (!album) return []; + if (topAlbumColors) return topAlbumColors; + const paletteRes = await fetch( + `https://calore.twofortyeight.net/?image=https://coverartarchive.org/release/${album?.mbid}/front`, + ); - const { palette }: { palette: [number, number, number][] } = - await paletteRes.json(); + const { palette }: { palette: [number, number, number][] } = + await paletteRes.json(); - topAlbumColors = palette; + topAlbumColors = palette; - return palette; + return palette; } diff --git a/src/pages/palette.json.ts b/src/pages/palette.json.ts index 3592ef1..0da3568 100644 --- a/src/pages/palette.json.ts +++ b/src/pages/palette.json.ts @@ -2,6 +2,6 @@ import { getAlbum, getColors } from "@lib/musicData"; import type { APIRoute } from "astro"; export const GET: APIRoute = async (context) => { - const colors = await getColors(await getAlbum()); - return Response.json({ palette: colors }); + const colors = await getColors(await getAlbum()); + return Response.json({ palette: colors }); }; diff --git a/src/styles/markdown.css b/src/styles/markdown.css index d410699..4fa8302 100644 --- a/src/styles/markdown.css +++ b/src/styles/markdown.css @@ -1,32 +1,32 @@ img { - width: 100%; - height: auto; + width: 100%; + height: auto; } h2#footnote-label { - text-transform: lowercase; + text-transform: lowercase; } blockquote { - padding: 0.1rem 1rem; - background-color: var(--accent, blue); - margin: 0; - border-left: 2px solid var(--accent, blue); + padding: 0.1rem 1rem; + background-color: var(--accent, blue); + margin: 0; + border-left: 2px solid var(--accent, blue); } blockquote * { - color: var(--secondaryTextColor, white); + color: var(--secondaryTextColor, white); } pre { - padding: 1rem 1.5rem; - border-radius: 10px; + padding: 1rem 1.5rem; + border-radius: 10px; } pre, code { - font-family: "Geist Mono Variable", monospace; + font-family: "Geist Mono Variable", monospace; } hr { - border: none; - border-bottom: var(--accent) dotted 5px; - width: 25%; - /*border-style: dotted; + border: none; + border-bottom: var(--accent) dotted 5px; + width: 25%; + /*border-style: dotted; border-width: 3px;*/ } diff --git a/tsconfig.json b/tsconfig.json index 65c0083..b8ec1db 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,14 +1,14 @@ { - "extends": "astro/tsconfigs/strict", - "include": [".astro/types.d.ts", "**/*"], - "exclude": ["dist"], - "compilerOptions": { - "paths": { - "@layouts/*": ["./src/layouts/*"], - "@components/*": ["./src/components/*"], - "@lib/*": ["./src/lib/*"], - "@assets/*": ["./src/assets/*"], - "@styles/*": ["./src/styles/*"], - }, - }, + "extends": "astro/tsconfigs/strict", + "include": [".astro/types.d.ts", "**/*"], + "exclude": ["dist"], + "compilerOptions": { + "paths": { + "@layouts/*": ["./src/layouts/*"], + "@components/*": ["./src/components/*"], + "@lib/*": ["./src/lib/*"], + "@assets/*": ["./src/assets/*"], + "@styles/*": ["./src/styles/*"] + } + } } diff --git a/uno.config.ts b/uno.config.ts index 516aa7c..0764261 100644 --- a/uno.config.ts +++ b/uno.config.ts @@ -1,12 +1,12 @@ import { defineConfig } from "unocss"; export default defineConfig({ - theme: { - colors: { - dominant: "var(--dominant, #e9e7d7)", - "text-color": "var(--textColor, black)", - "secondary-text-color": "var(--secondaryTextColor, white)", - accent: "var(--accent, blue)", - }, - }, + theme: { + colors: { + dominant: "var(--dominant, #e9e7d7)", + "text-color": "var(--textColor, black)", + "secondary-text-color": "var(--secondaryTextColor, white)", + accent: "var(--accent, blue)", + }, + }, });