initial commit

This commit is contained in:
Zacharias-Brohn
2026-01-14 10:46:21 +01:00
commit 5c123db557
32 changed files with 7430 additions and 0 deletions
+235
View File
@@ -0,0 +1,235 @@
// Helper functions for color interpolation and palette generation
export type RGB = { r: number; g: number; b: number };
export function hexToRgb(hex: string): RGB | null {
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
return result
? {
r: parseInt(result[1], 16),
g: parseInt(result[2], 16),
b: parseInt(result[3], 16),
}
: null;
}
export function rgbToHex(r: number, g: number, b: number): string {
return (
"#" + [r, g, b].map((x) => x.toString(16).padStart(2, "0")).join("")
);
}
// Parse rgba() string to RGB + alpha
export function parseRgba(
rgba: string,
): { r: number; g: number; b: number; a: number } | null {
const match = rgba.match(
/rgba?\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*(?:,\s*([\d.]+))?\s*\)/,
);
if (!match) return null;
return {
r: parseInt(match[1], 10),
g: parseInt(match[2], 10),
b: parseInt(match[3], 10),
a: match[4] ? parseFloat(match[4]) : 1,
};
}
// Convert RGB to HSL
export function rgbToHsl(
r: number,
g: number,
b: number,
): { h: number; s: number; l: number } {
r /= 255;
g /= 255;
b /= 255;
const max = Math.max(r, g, b);
const min = Math.min(r, g, b);
const l = (max + min) / 2;
let h = 0;
let s = 0;
if (max !== min) {
const d = max - min;
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
switch (max) {
case r:
h = ((g - b) / d + (g < b ? 6 : 0)) / 6;
break;
case g:
h = ((b - r) / d + 2) / 6;
break;
case b:
h = ((r - g) / d + 4) / 6;
break;
}
}
return { h: h * 360, s: s * 100, l: l * 100 };
}
// Convert HSL to RGB
export function hslToRgb(
h: number,
s: number,
l: number,
): { r: number; g: number; b: number } {
h /= 360;
s /= 100;
l /= 100;
let r: number, g: number, b: number;
if (s === 0) {
r = g = b = l;
} else {
const hue2rgb = (p: number, q: number, t: number) => {
if (t < 0) t += 1;
if (t > 1) t -= 1;
if (t < 1 / 6) return p + (q - p) * 6 * t;
if (t < 1 / 2) return q;
if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
return p;
};
const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
const p = 2 * l - q;
r = hue2rgb(p, q, h + 1 / 3);
g = hue2rgb(p, q, h);
b = hue2rgb(p, q, h - 1 / 3);
}
return {
r: Math.round(r * 255),
g: Math.round(g * 255),
b: Math.round(b * 255),
};
}
// Interpolate between two colors
export function lerpColor(color1: RGB, color2: RGB, t: number): RGB {
return {
r: Math.round(color1.r + (color2.r - color1.r) * t),
g: Math.round(color1.g + (color2.g - color1.g) * t),
b: Math.round(color1.b + (color2.b - color1.b) * t),
};
}
// Generate a color at a specific position along a gradient
function getGradientColor(color1: RGB, color2: RGB, position: number): RGB {
return lerpColor(color1, color2, position);
}
// Adjust lightness of a color
function adjustLightness(color: RGB, amount: number): RGB {
const hsl = rgbToHsl(color.r, color.g, color.b);
hsl.l = Math.max(0, Math.min(100, hsl.l + amount));
return hslToRgb(hsl.h, hsl.s, hsl.l);
}
// Adjust saturation of a color
function adjustSaturation(color: RGB, amount: number): RGB {
const hsl = rgbToHsl(color.r, color.g, color.b);
hsl.s = Math.max(0, Math.min(100, hsl.s + amount));
return hslToRgb(hsl.h, hsl.s, hsl.l);
}
// Format RGB as rgba string
function toRgba(color: RGB, alpha: number): string {
return `rgba(${color.r}, ${color.g}, ${color.b}, ${alpha})`;
}
// Color palette type for the animated background
export type ColorPalette = {
keyword: string;
string: string;
function: string;
variable: string;
comment: string;
punctuation: string;
number: string;
};
// Default palette for when no gradient colors are provided
export const defaultPalette: ColorPalette = {
keyword: "rgba(168, 85, 247, 0.7)", // Purple
string: "rgba(34, 197, 94, 0.7)", // Green
function: "rgba(59, 130, 246, 0.7)", // Blue
variable: "rgba(239, 68, 68, 0.7)", // Red
comment: "rgba(156, 163, 175, 0.6)", // Gray
punctuation: "rgba(107, 114, 128, 0.5)", // Darker gray
number: "rgba(249, 115, 22, 0.7)", // Orange
};
// Generate a code color palette from two gradient colors
export function generatePaletteFromGradient(
color1: RGB,
color2: RGB,
): ColorPalette {
// Create variations by interpolating between the two colors
// and adjusting lightness/saturation
// Primary color (closer to color1)
const primary = getGradientColor(color1, color2, 0.2);
// Secondary color (closer to color2)
const secondary = getGradientColor(color1, color2, 0.8);
// Middle blend
const middle = getGradientColor(color1, color2, 0.5);
// Lighter variation for highlights
const light = adjustLightness(getGradientColor(color1, color2, 0.3), 15);
// Darker variation for comments
const dark = adjustLightness(middle, -20);
// Desaturated for punctuation
const desaturated = adjustSaturation(
adjustLightness(middle, -10),
-30,
);
// Accent (shift towards color2 with more saturation)
const accent = adjustSaturation(getGradientColor(color1, color2, 0.7), 10);
return {
keyword: toRgba(primary, 0.8),
string: toRgba(secondary, 0.75),
function: toRgba(middle, 0.75),
variable: toRgba(light, 0.7),
comment: toRgba(dark, 0.5),
punctuation: toRgba(desaturated, 0.5),
number: toRgba(accent, 0.8),
};
}
// Parse gradient string and extract the two colors
export function parseGradientColors(
gradient: string,
): { color1: RGB; color2: RGB } | null {
// Match rgba colors in the gradient string
const rgbaPattern =
/rgba?\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*(?:,\s*[\d.]+)?\s*\)/g;
const matches = [...gradient.matchAll(rgbaPattern)];
if (matches.length >= 2) {
return {
color1: {
r: parseInt(matches[0][1], 10),
g: parseInt(matches[0][2], 10),
b: parseInt(matches[0][3], 10),
},
color2: {
r: parseInt(matches[1][1], 10),
g: parseInt(matches[1][2], 10),
b: parseInt(matches[1][3], 10),
},
};
}
return null;
}
+14
View File
@@ -0,0 +1,14 @@
export { stripHtmlFromMarkdown } from "./markdown";
export {
hexToRgb,
rgbToHex,
parseRgba,
rgbToHsl,
hslToRgb,
lerpColor,
generatePaletteFromGradient,
parseGradientColors,
defaultPalette,
type RGB,
type ColorPalette,
} from "./color";
+39
View File
@@ -0,0 +1,39 @@
// Strip HTML tags from markdown content, preserving code blocks
export function stripHtmlFromMarkdown(markdown: string): string {
// First, normalize smart quotes (but NOT backticks - they're fine as-is)
let result = markdown
.replace(/[""]/g, '"') // Smart quotes to regular quotes
.replace(/['']/g, "'"); // Smart single quotes
// Placeholder for code blocks
const codeBlocks: string[] = [];
// Replace fenced code blocks (```...```) with placeholders
result = result.replace(/```[\s\S]*?```/g, (match) => {
codeBlocks.push(match);
return `__CODE_BLOCK_${codeBlocks.length - 1}__`;
});
// Replace inline code (`...`) with placeholders
const inlineCode: string[] = [];
result = result.replace(/`[^`]+`/g, (match) => {
inlineCode.push(match);
return `__INLINE_CODE_${inlineCode.length - 1}__`;
});
// Remove HTML tags (but not their content for simple tags)
// This handles both self-closing and paired tags
result = result.replace(/<[^>]+>/g, "");
// Restore inline code
inlineCode.forEach((code, i) => {
result = result.replace(`__INLINE_CODE_${i}__`, code);
});
// Restore code blocks
codeBlocks.forEach((block, i) => {
result = result.replace(`__CODE_BLOCK_${i}__`, block);
});
return result;
}