From 3af674aebad9e3792fbf13b3cbda7b1691b1f4f3 Mon Sep 17 00:00:00 2001 From: Paul Buetow Date: Sun, 22 Jun 2025 22:06:43 +0300 Subject: Add 50 new experimental HTML themes for Gemtexter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Generated 50 unique themes with creative layouts and color schemes - Each theme includes: - Custom CSS with W3C validated styles - Example HTML preview page - Font files with proper licensing - Theme configuration file - License documentation - Layout types include: centered, wide, magazine, card, brutalist, terminal, book, hero, sidebar, and more creative designs - All themes support both light and dark color schemes - Fixed CSS validation issues for W3C compliance - Created theme gallery page at extras/html/themes/index.html - Added screenshot previews for all themes - Utility scripts included: - generate_50_themes.py - Main theme generator - fix_css_validation.py - CSS validator/fixer - create_theme_previews.py - Screenshot generator - Theme gallery with filtering at index.html 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- extras/html/themes/generate_random_themes.py | 1008 ++++++++++++++++++++++++++ 1 file changed, 1008 insertions(+) create mode 100644 extras/html/themes/generate_random_themes.py (limited to 'extras/html/themes/generate_random_themes.py') diff --git a/extras/html/themes/generate_random_themes.py b/extras/html/themes/generate_random_themes.py new file mode 100644 index 0000000..8da7d24 --- /dev/null +++ b/extras/html/themes/generate_random_themes.py @@ -0,0 +1,1008 @@ +#!/usr/bin/env python3 + +import os +import json +import random +import requests +import concurrent.futures +import colorsys +from pathlib import Path +import zipfile +import io + +# Google Fonts API - Popular fonts with clear licenses +GOOGLE_FONTS = [ + {"family": "Roboto", "variants": ["300", "regular", "700"], "category": "sans-serif", "license": "Apache License 2.0"}, + {"family": "Open Sans", "variants": ["300", "regular", "700"], "category": "sans-serif", "license": "Apache License 2.0"}, + {"family": "Lato", "variants": ["300", "regular", "700"], "category": "sans-serif", "license": "OFL"}, + {"family": "Montserrat", "variants": ["300", "regular", "700"], "category": "sans-serif", "license": "OFL"}, + {"family": "Poppins", "variants": ["300", "regular", "700"], "category": "sans-serif", "license": "OFL"}, + {"family": "Raleway", "variants": ["300", "regular", "700"], "category": "sans-serif", "license": "OFL"}, + {"family": "Inter", "variants": ["300", "regular", "700"], "category": "sans-serif", "license": "OFL"}, + {"family": "Nunito", "variants": ["300", "regular", "700"], "category": "sans-serif", "license": "OFL"}, + {"family": "Work Sans", "variants": ["300", "regular", "700"], "category": "sans-serif", "license": "OFL"}, + {"family": "Quicksand", "variants": ["300", "regular", "700"], "category": "sans-serif", "license": "OFL"}, + + {"family": "Playfair Display", "variants": ["regular", "700"], "category": "serif", "license": "OFL"}, + {"family": "Merriweather", "variants": ["300", "regular", "700"], "category": "serif", "license": "OFL"}, + {"family": "Lora", "variants": ["regular", "700"], "category": "serif", "license": "OFL"}, + {"family": "Source Serif Pro", "variants": ["regular", "700"], "category": "serif", "license": "OFL"}, + {"family": "Crimson Text", "variants": ["regular", "700"], "category": "serif", "license": "OFL"}, + {"family": "Libre Baskerville", "variants": ["regular", "700"], "category": "serif", "license": "OFL"}, + {"family": "EB Garamond", "variants": ["regular", "700"], "category": "serif", "license": "OFL"}, + {"family": "Cormorant", "variants": ["300", "regular", "700"], "category": "serif", "license": "OFL"}, + + {"family": "Space Mono", "variants": ["regular", "700"], "category": "monospace", "license": "OFL"}, + {"family": "Fira Code", "variants": ["regular", "700"], "category": "monospace", "license": "OFL"}, + {"family": "JetBrains Mono", "variants": ["regular", "700"], "category": "monospace", "license": "OFL"}, + {"family": "Source Code Pro", "variants": ["regular", "700"], "category": "monospace", "license": "OFL"}, + {"family": "Ubuntu Mono", "variants": ["regular", "700"], "category": "monospace", "license": "Ubuntu Font License"}, + + {"family": "Dancing Script", "variants": ["regular", "700"], "category": "handwriting", "license": "OFL"}, + {"family": "Pacifico", "variants": ["regular"], "category": "handwriting", "license": "OFL"}, + {"family": "Caveat", "variants": ["regular", "700"], "category": "handwriting", "license": "OFL"}, + {"family": "Satisfy", "variants": ["regular"], "category": "handwriting", "license": "OFL"}, + {"family": "Great Vibes", "variants": ["regular"], "category": "handwriting", "license": "OFL"}, + + {"family": "Bebas Neue", "variants": ["regular"], "category": "display", "license": "OFL"}, + {"family": "Righteous", "variants": ["regular"], "category": "display", "license": "OFL"}, + {"family": "Fredoka One", "variants": ["regular"], "category": "display", "license": "OFL"}, + {"family": "Rubik", "variants": ["300", "regular", "700"], "category": "sans-serif", "license": "OFL"}, + {"family": "Oswald", "variants": ["300", "regular", "700"], "category": "sans-serif", "license": "OFL"}, + {"family": "Barlow", "variants": ["300", "regular", "700"], "category": "sans-serif", "license": "OFL"}, + {"family": "Archivo", "variants": ["regular", "700"], "category": "sans-serif", "license": "OFL"}, + {"family": "Exo 2", "variants": ["300", "regular", "700"], "category": "sans-serif", "license": "OFL"}, + {"family": "Karla", "variants": ["300", "regular", "700"], "category": "sans-serif", "license": "OFL"}, + {"family": "Bitter", "variants": ["regular", "700"], "category": "serif", "license": "OFL"}, + {"family": "Josefin Sans", "variants": ["300", "regular", "700"], "category": "sans-serif", "license": "OFL"}, + {"family": "Abril Fatface", "variants": ["regular"], "category": "display", "license": "OFL"}, + {"family": "Anton", "variants": ["regular"], "category": "sans-serif", "license": "OFL"}, + {"family": "Comfortaa", "variants": ["300", "regular", "700"], "category": "display", "license": "OFL"}, + {"family": "Lexend", "variants": ["300", "regular", "700"], "category": "sans-serif", "license": "OFL"}, + {"family": "DM Sans", "variants": ["regular", "700"], "category": "sans-serif", "license": "OFL"}, + {"family": "Space Grotesk", "variants": ["300", "regular", "700"], "category": "sans-serif", "license": "OFL"}, + {"family": "Sora", "variants": ["300", "regular", "700"], "category": "sans-serif", "license": "OFL"}, + {"family": "Manrope", "variants": ["300", "regular", "700"], "category": "sans-serif", "license": "OFL"}, + {"family": "Figtree", "variants": ["300", "regular", "700"], "category": "sans-serif", "license": "OFL"} +] + +# Color palette generators +def generate_complementary_palette(): + base_hue = random.random() + base_saturation = random.uniform(0.3, 0.9) + base_lightness = random.uniform(0.3, 0.7) + + # Base color + base_rgb = colorsys.hls_to_rgb(base_hue, base_lightness, base_saturation) + base_color = '#{:02x}{:02x}{:02x}'.format(int(base_rgb[0]*255), int(base_rgb[1]*255), int(base_rgb[2]*255)) + + # Complementary color + comp_hue = (base_hue + 0.5) % 1.0 + comp_rgb = colorsys.hls_to_rgb(comp_hue, base_lightness, base_saturation * 0.8) + comp_color = '#{:02x}{:02x}{:02x}'.format(int(comp_rgb[0]*255), int(comp_rgb[1]*255), int(comp_rgb[2]*255)) + + # Background - very light or very dark + if random.random() > 0.5: # Light theme + bg_lightness = random.uniform(0.92, 0.98) + text_lightness = random.uniform(0.1, 0.2) + else: # Dark theme + bg_lightness = random.uniform(0.05, 0.15) + text_lightness = random.uniform(0.85, 0.95) + + bg_rgb = colorsys.hls_to_rgb(base_hue, bg_lightness, 0.1) + bg_color = '#{:02x}{:02x}{:02x}'.format(int(bg_rgb[0]*255), int(bg_rgb[1]*255), int(bg_rgb[2]*255)) + + text_rgb = colorsys.hls_to_rgb(0, text_lightness, 0) + text_color = '#{:02x}{:02x}{:02x}'.format(int(text_rgb[0]*255), int(text_rgb[1]*255), int(text_rgb[2]*255)) + + return { + "primary": base_color, + "secondary": comp_color, + "background": bg_color, + "text": text_color, + "is_dark": bg_lightness < 0.5 + } + +def generate_triadic_palette(): + base_hue = random.random() + base_saturation = random.uniform(0.4, 0.8) + base_lightness = random.uniform(0.4, 0.6) + + colors = [] + for i in range(3): + hue = (base_hue + i * 0.333) % 1.0 + rgb = colorsys.hls_to_rgb(hue, base_lightness + random.uniform(-0.1, 0.1), base_saturation) + color = '#{:02x}{:02x}{:02x}'.format(int(rgb[0]*255), int(rgb[1]*255), int(rgb[2]*255)) + colors.append(color) + + # Background + if random.random() > 0.5: # Light theme + bg_lightness = random.uniform(0.94, 0.99) + text_lightness = random.uniform(0.05, 0.15) + else: # Dark theme + bg_lightness = random.uniform(0.02, 0.12) + text_lightness = random.uniform(0.88, 0.98) + + bg_rgb = colorsys.hls_to_rgb(base_hue, bg_lightness, 0.05) + bg_color = '#{:02x}{:02x}{:02x}'.format(int(bg_rgb[0]*255), int(bg_rgb[1]*255), int(bg_rgb[2]*255)) + + text_rgb = colorsys.hls_to_rgb(0, text_lightness, 0) + text_color = '#{:02x}{:02x}{:02x}'.format(int(text_rgb[0]*255), int(text_rgb[1]*255), int(text_rgb[2]*255)) + + return { + "primary": colors[0], + "secondary": colors[1], + "accent": colors[2], + "background": bg_color, + "text": text_color, + "is_dark": bg_lightness < 0.5 + } + +def generate_analogous_palette(): + base_hue = random.random() + hue_shift = random.uniform(0.05, 0.12) + base_saturation = random.uniform(0.3, 0.8) + base_lightness = random.uniform(0.35, 0.65) + + colors = [] + for i in range(-2, 3): + hue = (base_hue + i * hue_shift) % 1.0 + saturation = base_saturation + random.uniform(-0.1, 0.1) + lightness = base_lightness + random.uniform(-0.1, 0.1) + rgb = colorsys.hls_to_rgb(hue, lightness, saturation) + color = '#{:02x}{:02x}{:02x}'.format(int(rgb[0]*255), int(rgb[1]*255), int(rgb[2]*255)) + colors.append(color) + + # Background + if random.random() > 0.5: # Light theme + bg_lightness = random.uniform(0.95, 0.99) + text_lightness = random.uniform(0.08, 0.18) + else: # Dark theme + bg_lightness = random.uniform(0.03, 0.10) + text_lightness = random.uniform(0.90, 0.97) + + bg_rgb = colorsys.hls_to_rgb(base_hue, bg_lightness, 0.08) + bg_color = '#{:02x}{:02x}{:02x}'.format(int(bg_rgb[0]*255), int(bg_rgb[1]*255), int(bg_rgb[2]*255)) + + text_rgb = colorsys.hls_to_rgb(0, text_lightness, 0) + text_color = '#{:02x}{:02x}{:02x}'.format(int(text_rgb[0]*255), int(text_rgb[1]*255), int(text_rgb[2]*255)) + + return { + "primary": colors[2], + "secondary": colors[1], + "accent": colors[3], + "background": bg_color, + "text": text_color, + "is_dark": bg_lightness < 0.5 + } + +# Layout generators +LAYOUTS = [ + "centered", + "wide", + "magazine", + "card", + "asymmetric", + "minimal_grid", + "brutalist", + "newspaper", + "terminal", + "book", + "sidebar", + "hero", + "masonry", + "split", + "overlap" +] + +def generate_layout_css(layout_type, colors): + base_css = f""" +body {{ + background-color: {colors['background']}; + color: {colors['text']}; + margin: 0; + padding: 0; + min-height: 100vh; +}} + +h1, h2, h3 {{ + color: {colors['primary']}; + margin-top: 1.5em; + margin-bottom: 0.5em; +}} + +a {{ + color: {colors['secondary']}; + text-decoration: none; + transition: all 0.3s ease; +}} + +a:hover {{ + opacity: 0.8; + text-decoration: underline; +}} + +.quote {{ + border-left: 4px solid {colors['secondary']}; + padding: 1em 1.5em; + margin: 1.5em 0; + background-color: {colors['primary']}11; + font-style: italic; +}} + +pre {{ + background-color: {colors['text']}11; + border: 1px solid {colors['text']}22; + padding: 1em; + overflow-x: auto; + border-radius: 4px; +}} + +.inlinecode {{ + background-color: {colors['text']}11; + padding: 0.2em 0.4em; + border-radius: 3px; + font-family: var(--font-mono); +}} +""" + + if layout_type == "centered": + layout_css = """ +body { + max-width: 65ch; + margin: 0 auto; + padding: 2em 1em; + line-height: 1.6; +} + +.header { + text-align: center; + margin-bottom: 3em; + padding-bottom: 2em; + border-bottom: 1px solid currentColor; +} +""" + elif layout_type == "wide": + layout_css = """ +body { + max-width: 90%; + margin: 0 auto; + padding: 2em; + line-height: 1.6; +} + +.header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 3em; + padding-bottom: 1em; + border-bottom: 2px solid currentColor; +} + +p { + max-width: 75ch; +} +""" + elif layout_type == "magazine": + layout_css = """ +body { + max-width: 1200px; + margin: 0 auto; + padding: 2em; + display: grid; + grid-template-columns: 2fr 1fr; + gap: 3em; + line-height: 1.6; +} + +.header { + grid-column: 1 / -1; + text-align: center; + margin-bottom: 2em; + padding: 2em 0; + background: linear-gradient(45deg, transparent 30%, currentColor 30%, currentColor 70%, transparent 70%); + background-size: 10px 10px; +} + +.header h1 { + background-color: var(--bg-color); + padding: 0.5em 1em; + display: inline-block; +} + +h1, h2, h3 { + grid-column: 1 / -1; +} + +p, ul, pre, .quote { + grid-column: 1; +} +""" + elif layout_type == "card": + layout_css = f""" +body {{ + padding: 2em; + line-height: 1.6; + background-color: {colors['text']}08; +}} + +.header {{ + background-color: {colors['background']}; + padding: 3em; + margin: -2em -2em 2em -2em; + box-shadow: 0 4px 20px rgba(0,0,0,0.1); +}} + +h1, h2, h3 {{ + background-color: {colors['background']}; + padding: 1em; + margin-left: -1em; + margin-right: -1em; + box-shadow: 0 2px 10px rgba(0,0,0,0.05); +}} + +p, ul, pre, .quote {{ + background-color: {colors['background']}; + padding: 1.5em; + margin: 1em 0; + border-radius: 8px; + box-shadow: 0 2px 10px rgba(0,0,0,0.05); +}} +""" + elif layout_type == "asymmetric": + layout_css = """ +body { + max-width: 1000px; + margin: 0 auto; + padding: 2em; + line-height: 1.6; +} + +.header { + margin-left: 20%; + margin-bottom: 3em; +} + +h1 { + margin-left: -5%; + font-size: 3em; +} + +h2 { + margin-left: 10%; +} + +h3 { + margin-left: 25%; +} + +p:nth-child(odd) { + margin-left: 5%; + margin-right: 15%; +} + +p:nth-child(even) { + margin-left: 15%; + margin-right: 5%; +} +""" + elif layout_type == "minimal_grid": + layout_css = """ +body { + display: grid; + grid-template-columns: repeat(12, 1fr); + gap: 1em; + padding: 2em; + line-height: 1.8; +} + +.header { + grid-column: 3 / 11; + text-align: center; + margin-bottom: 2em; +} + +h1 { + grid-column: 2 / 12; +} + +h2 { + grid-column: 3 / 11; +} + +h3 { + grid-column: 4 / 10; +} + +p, ul, pre, .quote { + grid-column: 3 / 10; +} +""" + elif layout_type == "brutalist": + layout_css = f""" +body {{ + padding: 0; + line-height: 1.4; +}} + +.header {{ + background-color: {colors['primary']}; + color: {colors['background']}; + padding: 2em; + transform: skewY(-2deg); + margin-bottom: 3em; +}} + +.header h1 {{ + color: {colors['background']}; + font-size: 4em; + margin: 0; +}} + +h1, h2, h3 {{ + text-transform: uppercase; + letter-spacing: 0.1em; +}} + +p, ul, pre, .quote {{ + margin: 2em; + padding: 1em; + border: 4px solid {colors['text']}; +}} + +a {{ + background-color: {colors['secondary']}; + color: {colors['background']}; + padding: 0.2em 0.5em; +}} +""" + elif layout_type == "newspaper": + layout_css = """ +body { + max-width: 1200px; + margin: 0 auto; + padding: 1em; + column-count: 3; + column-gap: 2em; + column-rule: 1px solid currentColor; + line-height: 1.5; +} + +.header { + column-span: all; + text-align: center; + border-top: 4px double currentColor; + border-bottom: 4px double currentColor; + padding: 1em 0; + margin-bottom: 2em; +} + +h1, h2 { + column-span: all; + text-align: center; +} + +h3 { + break-after: avoid; +} + +p { + text-align: justify; + hyphens: auto; +} +""" + elif layout_type == "terminal": + layout_css = f""" +body {{ + background-color: #000; + color: #0f0; + padding: 1em; + font-family: var(--font-mono); + line-height: 1.4; +}} + +.header {{ + border: 1px solid #0f0; + padding: 1em; + margin-bottom: 2em; +}} + +.header::before {{ + content: "$ "; +}} + +h1, h2, h3 {{ + color: #0f0; +}} + +h1::before {{ + content: "### "; +}} + +h2::before {{ + content: "## "; +}} + +h3::before {{ + content: "# "; +}} + +a {{ + color: #0ff; +}} + +.quote {{ + border-left-color: #0f0; + background-color: #0f01; +}} + +pre {{ + border-color: #0f0; + background-color: #0f01; +}} +""" + elif layout_type == "book": + layout_css = """ +body { + max-width: 40em; + margin: 4em auto; + padding: 2em; + line-height: 1.8; + text-align: justify; + hyphens: auto; +} + +.header { + text-align: center; + margin-bottom: 4em; + page-break-after: always; +} + +h1 { + text-align: center; + margin: 3em 0 2em 0; +} + +h2 { + margin-top: 2em; +} + +p { + text-indent: 1.5em; +} + +p:first-of-type { + text-indent: 0; +} + +p:first-of-type::first-letter { + font-size: 3em; + line-height: 1; + float: left; + margin-right: 0.1em; +} +""" + elif layout_type == "sidebar": + layout_css = """ +body { + display: grid; + grid-template-columns: 250px 1fr; + min-height: 100vh; + margin: 0; +} + +.header { + grid-column: 1; + position: sticky; + top: 0; + height: 100vh; + padding: 2em; + border-right: 2px solid currentColor; +} + +h1, h2, h3, p, ul, pre, .quote { + grid-column: 2; + margin-left: 2em; + margin-right: 2em; + max-width: 65ch; +} +""" + elif layout_type == "hero": + layout_css = f""" +body {{ + margin: 0; + padding: 0; +}} + +.header {{ + min-height: 70vh; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + background: linear-gradient(135deg, {colors['primary']}22, {colors['secondary']}22); + margin-bottom: 3em; +}} + +.header h1 {{ + font-size: 4em; + margin: 0; +}} + +h1, h2, h3, p, ul, pre, .quote {{ + max-width: 65ch; + margin-left: auto; + margin-right: auto; + padding: 0 1em; +}} +""" + elif layout_type == "masonry": + layout_css = """ +body { + padding: 2em; + columns: 300px auto; + column-gap: 2em; +} + +.header { + column-span: all; + text-align: center; + margin-bottom: 3em; +} + +h1, h2 { + column-span: all; +} + +p, ul, pre, .quote, h3 { + break-inside: avoid; + margin-bottom: 2em; +} +""" + elif layout_type == "split": + layout_css = f""" +body {{ + display: grid; + grid-template-columns: 1fr 1fr; + min-height: 100vh; + margin: 0; +}} + +.header {{ + grid-column: 1; + background-color: {colors['primary']}; + color: {colors['background']}; + padding: 3em; + position: sticky; + top: 0; + height: 100vh; + display: flex; + flex-direction: column; + justify-content: center; +}} + +.header h1 {{ + color: {colors['background']}; +}} + +h1, h2, h3, p, ul, pre, .quote {{ + grid-column: 2; + padding: 0 3em; +}} +""" + elif layout_type == "overlap": + layout_css = f""" +body {{ + padding: 2em; + position: relative; +}} + +.header {{ + position: relative; + z-index: 10; + background-color: {colors['background']}; + padding: 2em; + margin-bottom: -2em; + box-shadow: 0 10px 30px rgba(0,0,0,0.1); +}} + +h1 {{ + font-size: 3em; + margin-top: 1em; + position: relative; + z-index: 5; +}} + +h2 {{ + margin-left: 2em; + position: relative; + z-index: 4; +}} + +h3 {{ + margin-left: 4em; + position: relative; + z-index: 3; +}} + +p, ul, pre, .quote {{ + position: relative; + background-color: {colors['background']}; + padding: 1.5em; + margin: 1em 0 1em 3em; + box-shadow: 0 2px 10px rgba(0,0,0,0.05); +}} + +p:nth-child(even) {{ + margin-left: 0; + margin-right: 3em; +}} +""" + + return base_css + "\n" + layout_css + +def download_font(font_info): + """Download font files from Google Fonts""" + font_dir = Path("fonts_cache") + font_dir.mkdir(exist_ok=True) + + downloaded_files = [] + + for variant in font_info['variants']: + # Construct Google Fonts download URL + family_param = font_info['family'].replace(' ', '+') + weight = '400' if variant == 'regular' else variant + + # Try to download from Google Fonts API + url = f"https://fonts.google.com/download?family={family_param}" + + try: + response = requests.get(url, timeout=10) + if response.status_code == 200: + # Extract font files from zip + with zipfile.ZipFile(io.BytesIO(response.content)) as z: + for file in z.namelist(): + if file.endswith('.ttf') and weight in file: + font_data = z.read(file) + filename = f"{font_info['family'].replace(' ', '_')}_{weight}.ttf" + filepath = font_dir / filename + with open(filepath, 'wb') as f: + f.write(font_data) + downloaded_files.append(filepath) + break + except Exception as e: + print(f"Error downloading {font_info['family']} {variant}: {e}") + + return downloaded_files + +def create_theme(theme_name, fonts, colors, layout): + """Create a single theme with fonts, colors, and layout""" + theme_dir = Path(theme_name) + theme_dir.mkdir(exist_ok=True) + + # Copy font files + theme_fonts_dir = theme_dir / "fonts" + theme_fonts_dir.mkdir(exist_ok=True) + + font_paths = {} + for font_type, font_info in fonts.items(): + if font_info['files']: + # Copy first available font file + src = font_info['files'][0] + dst = theme_fonts_dir / src.name + os.system(f"cp '{src}' '{dst}'") + font_paths[font_type] = f"./fonts/{src.name}" + + # Create theme.conf + theme_conf = f"""#!/bin/bash +# Font configuration for {theme_name} theme +# Font licenses: +# - {fonts['heading']['family']}: {fonts['heading']['license']} +# - {fonts['body']['family']}: {fonts['body']['license']} + +declare -xr HTML_WEBFONT_HEADING="{font_paths.get('heading', './fonts/default.ttf')}" +declare -xr HTML_WEBFONT_TEXT="{font_paths.get('body', './fonts/default.ttf')}" +""" + + with open(theme_dir / "theme.conf", "w") as f: + f.write(theme_conf) + + # Create LICENSE file with font information + license_content = f"""Theme: {theme_name} +Generated: {os.popen('date').read().strip()} + +Font Licenses: +============== + +Heading Font: {fonts['heading']['family']} +License: {fonts['heading']['license']} +Category: {fonts['heading']['category']} + +Body Font: {fonts['body']['family']} +License: {fonts['body']['license']} +Category: {fonts['body']['category']} + +Color Scheme: +============= +Primary: {colors['primary']} +Secondary: {colors['secondary']} +Background: {colors['background']} +Text: {colors['text']} +Theme Type: {'Dark' if colors['is_dark'] else 'Light'} + +Layout: {layout} +""" + + with open(theme_dir / "LICENSE", "w") as f: + f.write(license_content) + + # Create style.css + css_content = f"""/* {theme_name} theme - {layout} layout */ +@font-face {{ + font-family: 'HeadingFont'; + src: url('{font_paths.get('heading', './fonts/default.ttf')}') format('truetype'); + font-weight: normal; + font-style: normal; +}} + +@font-face {{ + font-family: 'BodyFont'; + src: url('{font_paths.get('body', './fonts/default.ttf')}') format('truetype'); + font-weight: normal; + font-style: normal; +}} + +:root {{ + --font-heading: 'HeadingFont', serif; + --font-body: 'BodyFont', sans-serif; + --font-mono: 'Courier New', monospace; + --bg-color: {colors['background']}; +}} + +body {{ + font-family: var(--font-body); +}} + +h1, h2, h3, h4, h5, h6 {{ + font-family: var(--font-heading); + font-weight: normal; +}} + +{generate_layout_css(layout, colors)} +""" + + with open(theme_dir / "style.css", "w") as f: + f.write(css_content) + + # Create example.html + example_html = f""" + + + + + {theme_name.replace('_', ' ').title()} Theme - Gemtexter + + + +
+

{theme_name.replace('_', ' ').title()} Theme

+

A unique combination of {fonts['heading']['family']} and {fonts['body']['family']}

+
+ +

Welcome to {theme_name.replace('_', ' ').title()}

+

This theme features a {layout.replace('_', ' ')} layout with a {'dark' if colors['is_dark'] else 'light'} color scheme. The typography combines {fonts['heading']['family']} for headings with {fonts['body']['family']} for body text, creating a unique reading experience.

+ +

Color Palette

+

The carefully selected colors work together to create visual harmony. Primary color {colors['primary']} pairs beautifully with secondary {colors['secondary']}.

+ +

Typography Showcase

+

The {fonts['body']['family']} font family ({fonts['body']['category']}) provides excellent readability for body text, while {fonts['heading']['family']} ({fonts['heading']['category']}) adds character to headings.

+ +

Interactive Elements

+

Links like this example link use the secondary color for clear visual distinction. Longer text links demonstrate the theme's approach to navigation:

+ Explore more about this theme's unique design choices and color combinations + +

Content Examples

+
+ "Good typography is invisible; great typography is unforgettable." This theme strives to balance both principles with its careful font selection and layout design. +
+ +

Code Display

+

Inline code like theme.configure() stands out clearly. Larger code blocks maintain readability:

+
const theme = {{
+    name: "{theme_name}",
+    layout: "{layout}",
+    fonts: {{
+        heading: "{fonts['heading']['family']}",
+        body: "{fonts['body']['family']}"
+    }},
+    isDark: {str(colors['is_dark']).lower()}
+}};
+ +

Lists and Structure

+

This theme handles various content types elegantly:

+ + +

Final Thoughts

+

Every element of this theme has been crafted to work in harmony. From the {layout.replace('_', ' ')} layout to the font pairing of {fonts['heading']['family']} and {fonts['body']['family']}, each choice enhances the reading experience.

+ +

The color scheme creates the perfect backdrop for your content, ensuring that whether you're writing technical documentation, creative prose, or anything in between, your words will shine through with clarity and style.

+ +""" + + with open(theme_dir / "example.html", "w") as f: + f.write(example_html) + + return theme_name + +def generate_random_theme(): + """Generate a complete random theme""" + # Random theme name + adjectives = ["cosmic", "serene", "vibrant", "minimal", "bold", "elegant", "modern", "classic", "dynamic", "subtle", "refined", "crisp", "warm", "cool", "fresh", "clean", "sharp", "smooth", "bright", "deep", "light", "rich", "soft", "strong", "pure", "clear", "radiant", "muted", "vivid", "gentle"] + nouns = ["wave", "sky", "forest", "ocean", "mountain", "valley", "desert", "river", "lake", "field", "garden", "meadow", "storm", "breeze", "mist", "frost", "flame", "spark", "glow", "shadow", "light", "dawn", "dusk", "night", "day", "season", "horizon", "vista", "peak", "flow"] + + theme_name = f"{random.choice(adjectives)}_{random.choice(nouns)}" + + # Random fonts + heading_font = random.choice([f for f in GOOGLE_FONTS if f['category'] in ['serif', 'display', 'sans-serif']]) + body_font = random.choice([f for f in GOOGLE_FONTS if f['category'] in ['serif', 'sans-serif']]) + + # Download fonts + heading_files = download_font(heading_font) + body_files = download_font(body_font) + + fonts = { + 'heading': { + 'family': heading_font['family'], + 'license': heading_font['license'], + 'category': heading_font['category'], + 'files': heading_files + }, + 'body': { + 'family': body_font['family'], + 'license': body_font['license'], + 'category': body_font['category'], + 'files': body_files + } + } + + # Random color palette + palette_generators = [generate_complementary_palette, generate_triadic_palette, generate_analogous_palette] + colors = random.choice(palette_generators)() + + # Random layout + layout = random.choice(LAYOUTS) + + return create_theme(theme_name, fonts, colors, layout) + +def main(): + print("Generating 50 random themes with web fonts...") + + # Generate themes in parallel + with concurrent.futures.ThreadPoolExecutor(max_workers=10) as executor: + futures = [executor.submit(generate_random_theme) for _ in range(50)] + + themes = [] + for i, future in enumerate(concurrent.futures.as_completed(futures)): + try: + theme_name = future.result() + themes.append(theme_name) + print(f"Created theme {i+1}/50: {theme_name}") + except Exception as e: + print(f"Error creating theme {i+1}: {e}") + + print(f"\nSuccessfully created {len(themes)} themes!") + + # Clean up font cache + os.system("rm -rf fonts_cache") + + return themes + +if __name__ == "__main__": + main() \ No newline at end of file -- cgit v1.2.3