
Favicon Generator
- 15 installs
- 28 repo stars
- Updated December 5, 2025
- chongdashu/cc-skills
Generate professional favicon suites with layered effects, curated templates, and Lucide icons in ICO, SVG, and PNG formats.
About
Produces polished favicons using a multi-layer effects engine, design templates, and framework integration in proper ICO, SVG, and PNG formats. Used when a developer needs app icons or browser tab icons matching a brand.
- Multi-layer effects with shadows, glows, and gradients
- 8 templates, 18 Lucide icons, full format suite
Favicon Generator by the numbers
- 15 all-time installs (skills.sh)
- Ranked #1,401 of 1,880 Design & UI/UX skills by installs in the Skillselion catalog
- Data as of Jul 28, 2026 (Skillselion catalog sync)
npx skills add https://github.com/chongdashu/cc-skills --skill favicon-generatorAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 15 |
|---|---|
| repo stars | ★ 28 |
| Last updated | December 5, 2025 |
| Repository | chongdashu/cc-skills ↗ |
What it does
Generate professional favicon suites with layered effects, curated templates, and Lucide icons in ICO, SVG, and PNG formats.
Files
Pro-Grade Favicon Generator
Create stunning, professional-quality favicons that stand alongside icons from Linear, Notion, Figma, and other polished apps.
Philosophy: Favicons Are Miniature Design Artifacts
The difference between a mediocre favicon and a great one isn't complexity—it's polish. Great favicons have:
- Depth: Subtle shadows that lift the icon off the surface
- Lighting: Highlights and gradients that create dimensionality
- Texture: Optional noise/grain that adds organic feel
- Precision: Optical centering, proper padding, crisp edges
Before generating, ask yourself: 1. What's the app's personality? (Playful, professional, technical, creative) 2. What colors define the brand? (Extract from tailwind config, CSS, or ask) 3. What level of polish is needed? (Quick prototype vs. production launch)
---
Workflow: Discover Existing Icons First
CRITICAL: Before generating a favicon, always check what icons are already used in the codebase. The favicon should match your existing brand identity.
Step 1: Search for Icon Usage
Search the codebase for icon imports and usage:
# Find lucide-react imports
rg "from.*lucide-react" --type tsx --type ts
# Find icon component usage
rg "PackagePlus|Package|Icon" --type tsx --type ts
# Check Header/Nav components (common icon locations)
rg "Header|Nav|Logo" --type tsxStep 2: Identify Primary Brand Icons
Look for:
- Logo icons: Used in Header, navigation, or branding components
- Most frequently used icons: Appear in multiple places
- Icon libraries: lucide-react, react-icons, custom SVG components
Example discovery:
Found in Header.tsx: PackagePlus from lucide-react
Found in HomePage.tsx: PackagePlus, Package
Primary brand icon: PackagePlus (used in logo/branding)Step 3: Extract Icon Paths
If using lucide-react or similar libraries:
1. Locate icon definition:
cat node_modules/lucide-react/dist/esm/icons/package-plus.js2. Extract SVG paths from the icon definition:
- Lucide icons use 24x24 viewBox
- Paths are defined as arrays:
["path", { d: "M..." }] - Copy the exact
dattributes from each path
3. Use cairosvg for accurate rendering (recommended):
pip install cairosvg
brew install cairo # macOS - required native libraryWhy cairosvg? Pillow cannot render SVG bezier curves and arcs. Lucide icons use arc commands (a2 2 0 0 0...) that only a proper SVG renderer can draw.
Step 4: Match Favicon to Brand Icon
- Same icon: Use the exact icon from your brand (e.g., PackagePlus → PackagePlus favicon)
- Same colors: Extract brand colors from Tailwind config or CSS variables
- Same style: Match the visual style (minimal, vibrant, etc.)
Example: PackagePlus Favicon with cairosvg
import cairosvg
from PIL import Image
from io import BytesIO
# Actual Lucide PackagePlus paths (from node_modules/lucide-react/dist/esm/icons/package-plus.js)
SVG_TEMPLATE = """<svg xmlns="http://www.w3.org/2000/svg" width="{size}" height="{size}" viewBox="0 0 {size} {size}">
<defs>
<linearGradient id="bg" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#f97316"/>
<stop offset="100%" stop-color="#ef4444"/>
</linearGradient>
</defs>
<rect width="{size}" height="{size}" rx="{radius}" fill="url(#bg)"/>
<g transform="translate({offset}, {offset}) scale({scale})"
stroke="#ffffff" stroke-width="2" fill="none"
stroke-linecap="round" stroke-linejoin="round">
<!-- Exact Lucide PackagePlus paths -->
<path d="M16 16h6"/>
<path d="M19 13v6"/>
<path d="M21 10V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l2-1.14"/>
<path d="m7.5 4.27 9 5.15"/>
<polyline points="3.29 7 12 12 20.71 7"/>
<line x1="12" x2="12" y1="22" y2="12"/>
</g>
</svg>"""
def render_lucide_icon(size):
# Lucide uses 24x24, scale to fit in favicon with padding
scale = (size * 0.7) / 24
offset = size * 0.15
radius = int(size * 0.22)
svg = SVG_TEMPLATE.format(size=size, scale=scale, offset=offset, radius=radius)
png_data = cairosvg.svg2png(bytestring=svg.encode('utf-8'))
return Image.open(BytesIO(png_data)).convert('RGBA')Why This Matters
- Consistency: Favicon matches your app's visual identity
- Brand recognition: Users recognize your icon across contexts
- Professionalism: Shows attention to detail and design coherence
- Avoids mismatch: Prevents generating a favicon that doesn't match your actual logo
---
The Effects Stack
Professional favicons are built in layers, not drawn flat:
┌─────────────────────────────────────┐
│ Layer 6: Content (letter/icon) │ ← With its own shadow
│ Layer 5: Noise texture │ ← Subtle grain for organic feel
│ Layer 4: Highlight gradient │ ← Top-lit shine effect
│ Layer 3: Inner glow │ ← Ambient light/shadow
│ Layer 2: Background │ ← Gradient or solid
│ Layer 1: Drop shadow │ ← Depth and lift
└─────────────────────────────────────┘Each layer is subtle. Combined, they create polish that's felt rather than seen.
---
Generation Tools
This skill provides two complementary tools:
Tool 1: Interactive HTML Generator
File: scripts/generate_favicon_pro.html
Open in browser for real-time preview and customization:
- 8 professional design templates
- 18 Lucide icons + letter/emoji modes
- Live effect adjustment (shadow, glow, highlight, noise)
- All sizes preview (16px to 512px)
- Context preview (browser tab, bookmarks)
- Bulk download
Best for: Quick iteration, visual exploration, client demos
Tool 2: Python CLI Pipeline
File: scripts/generate_favicon.py
Command-line generation with Pillow:
# Using a template
python generate_favicon.py --letter A --style vibrant --output ./public/
# Custom colors
python generate_favicon.py --letter T --bg "#22c55e" --bg2 "#14b8a6" --output ./favicons/
# Full control
python generate_favicon.py --letter N --bg "#0f172a" --fg "#22d3ee" \
--shadow 0.6 --glow 0.5 --noise 0.04 --output ./icons/Best for: CI/CD integration, batch generation, precise control
---
Design Templates
Choose a template that matches the app's personality:
| Template | Colors | Character | Best For |
|---|---|---|---|
| Modern | Indigo → Purple | Clean, trustworthy | SaaS, productivity |
| Vibrant | Pink → Orange | Energetic, bold | Consumer apps, social |
| Minimal | Near-black | Understated, technical | Dev tools, utilities |
| Glass | Blue → Cyan | Airy, modern | Dashboards, analytics |
| Neon | Dark + Cyan glow | Futuristic, edgy | Gaming, creative tools |
| Warm | Amber → Red | Friendly, approachable | Food, lifestyle, community |
| Forest | Green → Teal | Natural, sustainable | Health, environment, finance |
| Mono | White + Black | Minimal, adaptable | Any (works in any context) |
Template Selection Guide
App personality assessment:
├── Professional/Enterprise → Minimal, Modern, Mono
├── Consumer/Fun → Vibrant, Warm, Neon
├── Technical/Developer → Minimal, Glass, Neon
├── Health/Wellness → Forest, Warm
└── Creative/Design → Vibrant, Glass, Modern---
Content Types
1. Letter/Monogram (Default)
Single letter or two-letter combination from app name.
"TaskFlow" → "T" or "TF"
"Acme Corp" → "A" or "AC"Typography considerations:
- Single letters work better at small sizes
- Choose distinctive letters (avoid O, I which lack character)
- Font weight matters—bold reads better at 16px
2. Icons (Lucide Integration)
18 curated Lucide icons for common app types:
| Icon | Use Case |
|---|---|
rocket | Startups, launch, speed |
zap | Performance, automation |
star | Favorites, ratings, premium |
heart | Health, favorites, social |
code | Developer tools, IDEs |
box | Packages, containers, storage |
compass | Navigation, exploration |
flame | Trending, hot, energy |
globe | International, web, browser |
layers | Design, stacks, organization |
music | Audio, media, entertainment |
send | Messaging, communication |
shield | Security, protection, trust |
sparkles | AI, magic, premium |
sun | Light mode, energy, positivity |
target | Goals, focus, precision |
terminal | CLI, developer, technical |
wand | Magic, automation, creative |
3. Emoji
Native emoji for playful, informal apps.
🚀 → Launch, speed, startups
💡 → Ideas, innovation
🔥 → Trending, hot
✨ → Premium, magicNote: Emoji rendering varies by OS—test on multiple platforms.
---
Effects Reference
Drop Shadow
Creates depth and lift. Essential for polished look.
| Intensity | Effect | Use When |
|---|---|---|
| 0.2–0.3 | Subtle | Minimal designs, light backgrounds |
| 0.4–0.5 | Balanced | Most apps (default) |
| 0.6+ | Strong | Dark backgrounds, high contrast |
Highlight
Top-lit gradient that adds dimensionality.
| Intensity | Effect | Use When |
|---|---|---|
| 0.15–0.25 | Gentle | Subtle polish |
| 0.3–0.4 | Pronounced | Glass, vibrant styles |
| 0.5+ | Strong | Glossy, skeuomorphic look |
Inner Glow
Radial lighting from center, creates depth.
| Intensity | Effect | Use When |
|---|---|---|
| 0.2–0.3 | Soft ambient | Glass style |
| 0.4–0.5 | Noticeable | Neon, futuristic |
| 0.6+ | Strong | Glowing effect |
Noise/Grain
Subtle texture that prevents banding and adds organic feel.
| Intensity | Effect | Use When |
|---|---|---|
| 0.03–0.05 | Barely visible | Anti-banding only |
| 0.06–0.08 | Subtle texture | Organic, natural feel |
| 0.1+ | Visible grain | Vintage, film aesthetic |
Corner Radius
Shape of the icon background.
| Value | Shape | Platform |
|---|---|---|
| 0.15–0.18 | Squircle | iOS-like |
| 0.20–0.24 | Rounded | Modern default |
| 0.30+ | Very round | Playful, bubble |
| 0.50 | Circle | Circular icons |
---
Output Structure
Standard Suite (Default)
public/
├── favicon.ico # Legacy (16+32 combined)
├── favicon.svg # Modern browsers (scalable)
├── favicon-16x16.png # Browser tabs
├── favicon-32x32.png # Browser tabs (retina)
├── favicon-48x48.png # Windows tiles
├── favicon-64x64.png # Windows tiles
├── favicon-128x128.png # Chrome Web Store
├── apple-touch-icon.png # iOS home screen (180x180)
├── favicon-192x192.png # Android Chrome
└── favicon-512x512.png # PWA, AndroidFramework Integration
Next.js (App Router)
// app/layout.tsx
import type { Metadata } from 'next'
export const metadata: Metadata = {
icons: {
icon: [
{ url: '/favicon.ico', sizes: 'any' },
{ url: '/favicon.svg', type: 'image/svg+xml' },
{ url: '/favicon-16x16.png', sizes: '16x16', type: 'image/png' },
{ url: '/favicon-32x32.png', sizes: '32x32', type: 'image/png' },
],
apple: [
{ url: '/apple-touch-icon.png', sizes: '180x180' },
],
},
}Standard HTML
<link rel="icon" href="/favicon.ico" sizes="any">
<link rel="icon" href="/favicon.svg" type="image/svg+xml">
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">PWA Manifest
{
"icons": [
{ "src": "/favicon-192x192.png", "sizes": "192x192", "type": "image/png" },
{ "src": "/favicon-512x512.png", "sizes": "512x512", "type": "image/png" }
]
}---
Workflow Examples
Example 1: New SaaS Product
User: "Create a favicon for my project management app called Flowboard"
Claude:
1. [Discovers existing icons]
- Searches codebase: rg "from.*lucide-react"
- Finds: Header.tsx uses PackagePlus icon
- Checks: No existing favicon, but brand uses PackagePlus
2. [Analyzes codebase]
- tailwind.config.ts: primary = "#6366f1"
- Package name: "flowboard"
- Brand icon: PackagePlus (from lucide-react)
3. [Selects approach]
- Option A: Match brand icon (PackagePlus) → Extract paths from lucide-react
- Option B: Use letter "F" (distinctive, good at small sizes)
- Decision: Use PackagePlus to match brand identity
- Style: "Modern" (professional SaaS)
- Colors: Use brand indigo
4. [Extracts icon paths]
- Reads: node_modules/lucide-react/dist/esm/icons/package-plus.js
- Extracts SVG paths and converts 24x24 → 32x32
5. [Generates with Python script]
python generate_packageplus_favicon.py --output ./public/
6. [Integrates with Next.js]
Updates app/layout.tsx with metadata.icons
7. [Delivers]
"Created favicon suite matching your PackagePlus brand icon.
The favicon now matches your Header logo exactly—check the browser tab preview."Example 2: Developer Tool
User: "Make a favicon for my CLI tool"
Claude:
1. [Discovers existing icons]
- Searches: rg "Icon|Logo" --type tsx
- Finds: No existing brand icons, new project
2. [Considers personality]
- Developer audience → technical, minimal
- CLI context → terminal icon fits well
3. [Selects approach]
- Content: Terminal icon from Lucide (extract actual paths)
- Style: "Minimal" (dark, understated)
- Effects: Subtle shadow, slight noise
4. [Extracts Terminal icon paths]
- Reads: node_modules/lucide-react/dist/esm/icons/terminal.js
- Converts paths to favicon coordinate system
5. [Opens HTML generator for preview]
Shows user the terminal icon in minimal style
6. [Adjusts based on feedback]
User: "Can we make it more techy?"
→ Switches to "Neon" style with cyan glow
7. [Generates final suite]
Downloads all sizes, integrates with projectExample 3: Playful Consumer App
User: "I need a fun favicon with a rocket for my startup"
Claude:
1. [Selects approach]
- Content: Rocket icon (not emoji for consistency)
- Style: "Vibrant" (pink→orange, energetic)
- Effects: Strong shadow, highlight, no noise
2. [Previews in context]
Shows browser tab mockup, bookmark bar
3. [Generates]
Full suite with all sizes
4. [Delivers with context]
"Here's your rocket favicon in vibrant colors. The icon
stays crisp even at 16px. I've included the apple-touch-icon
for when users add to their phone home screen."---
Anti-Patterns
❌ Flat, shadowless designs
Problem: Icon looks pasted on, no depth
Fix: Add at least 0.3 shadow intensity❌ Over-complicated at small sizes
Problem: 16px version is unrecognizable mush
Fix: Test at actual 16px—simplify if needed❌ Generic blue gradient
Problem: Looks like every other AI-generated icon
Fix: Use brand colors, vary the template❌ Ignoring the effects stack
Problem: Just background + letter, looks amateur
Fix: Apply shadow + highlight at minimum❌ Wrong template for context
Problem: Neon style for a healthcare app
Fix: Match template to brand personality❌ Skipping the preview step
Problem: Looks good at 512px, bad at 16px
Fix: Always check size previews before finalizing❌ Not testing in context
Problem: Colors clash with browser chrome
Fix: Use context preview (tab, bookmarks)---
Variation Guidance
CRITICAL: Each favicon should feel custom, not templated.
Vary by app type:
- Dev tools → Terminal icon, Minimal/Neon style, dark colors
- Consumer apps → Vibrant icons, warm colors, playful shapes
- Enterprise → Letter monogram, Modern/Mono style, brand colors
- Creative tools → Abstract shapes, Glass style, unique gradients
Vary the effects:
- Don't always use the same shadow intensity
- Try different corner radii
- Experiment with inner glow for certain styles
- Add noise for organic apps, skip for technical ones
Vary the content:
- Not every app needs a letter
- Icons can be more memorable than letters
- Consider the app's core action (send, shield, target)
---
Quick Reference
Python CLI
# Basic
python generate_favicon.py --letter A --output ./public/
# With template
python generate_favicon.py --letter T --style vibrant --output ./public/
# Custom colors
python generate_favicon.py --letter N --bg "#0f172a" --bg2 "#1e293b" \
--fg "#22d3ee" --output ./public/
# Full control
python generate_favicon.py --letter M \
--bg "#ec4899" --bg2 "#f97316" --fg "#ffffff" \
--shadow 0.5 --highlight 0.3 --glow 0.2 --noise 0.05 \
--radius 0.24 --output ./public/Available Templates
modern, vibrant, minimal, glass, neon, warm, forest, mono
Available Icons
rocket, zap, star, heart, code, box, compass, flame, globe, layers, music, send, shield, sparkles, sun, target, terminal, wand
Effect Ranges
- Shadow: 0.0–1.0 (default: 0.4)
- Highlight: 0.0–1.0 (default: 0.25)
- Inner Glow: 0.0–1.0 (default: 0.0)
- Noise: 0.0–1.0 (default: 0.0)
- Corner Radius: 0.0–0.5 (default: 0.22)
---
Remember
Great favicons are felt, not analyzed. Users don't consciously notice the drop shadow or the highlight gradient—they just sense that the icon feels professional and polished.
The difference between amateur and professional is: 1. Layered effects vs. flat rendering 2. Considered templates vs. random colors 3. Size-appropriate detail vs. complexity that muddies 4. Tested in context vs. only viewed at full size
Use the tools to handle the technical complexity. Focus your energy on choosing the right personality, colors, and content for the specific app.
Visual Effects Technical Guide
Deep technical reference for the favicon effects engine.
The Perception of Quality
Why do some icons look "professional" and others look "amateur"? The answer lies in subtle visual cues that mimic real-world lighting and depth.
Real-World Analogies
| Effect | Real-World Equivalent | Why It Works |
|---|---|---|
| Drop shadow | Object lifted from surface | Creates depth hierarchy |
| Highlight | Light source from above | Matches natural lighting |
| Inner glow | Ambient light reflection | Adds dimensionality |
| Noise/grain | Material texture | Prevents "digital" flatness |
Effect Implementation Details
Drop Shadow
The shadow creates the illusion that the icon floats above the background.
Parameters:
- Color: Black with 20-40% opacity
- Offset: 3-8% of icon size, typically downward
- Blur: 10-20% of icon size
Canvas Implementation:
ctx.shadowColor = 'rgba(0, 0, 0, 0.25)';
ctx.shadowBlur = size * 0.12;
ctx.shadowOffsetY = size * 0.04;
// Draw shape - shadow appears automaticallyPillow Implementation:
# Create shadow layer from alpha channel
shadow = Image.new('RGBA', size, (0,0,0,0))
shadow_alpha = original.split()[3]
shadow_color = Image.new('RGBA', size, (0,0,0,int(255*0.3)))
shadow.paste(shadow_color, (0, offset), shadow_alpha)
shadow = shadow.filter(ImageFilter.GaussianBlur(blur))
# Composite behind original
result = Image.alpha_composite(shadow, original)Highlight Gradient
Simulates top-down lighting, making the icon appear three-dimensional.
Parameters:
- Top: White with 20-50% opacity
- Middle: Transparent
- Bottom: Black with 10-20% opacity
Canvas Implementation:
const gradient = ctx.createLinearGradient(0, 0, 0, size);
gradient.addColorStop(0, 'rgba(255,255,255,0.4)');
gradient.addColorStop(0.5, 'rgba(255,255,255,0)');
gradient.addColorStop(1, 'rgba(0,0,0,0.15)');
ctx.globalCompositeOperation = 'overlay';
ctx.fillStyle = gradient;
ctx.fill();Inner Glow
Creates depth within the shape, as if light is reflecting inside.
Parameters:
- Center: Bright (white with low opacity)
- Edges: Dark (black with low opacity)
- Center offset: Slightly above geometric center (top lighting)
Canvas Implementation:
const glowGradient = ctx.createRadialGradient(
size/2, size*0.4, 0, // Center slightly above middle
size/2, size/2, size*0.6 // Extends to edges
);
glowGradient.addColorStop(0, 'rgba(255,255,255,0.3)');
glowGradient.addColorStop(0.6, 'rgba(255,255,255,0)');
glowGradient.addColorStop(1, 'rgba(0,0,0,0.15)');Noise/Grain
Adds subtle texture that prevents color banding and digital flatness.
Parameters:
- Intensity: 3-10% of full range
- Alpha: Very low (10-30 out of 255)
- Blend mode: Overlay or soft-light
Canvas Implementation:
const noiseData = ctx.createImageData(size, size);
for (let i = 0; i < noiseData.data.length; i += 4) {
const noise = (Math.random() - 0.5) * 255 * intensity;
noiseData.data[i] = 128 + noise; // R
noiseData.data[i+1] = 128 + noise; // G
noiseData.data[i+2] = 128 + noise; // B
noiseData.data[i+3] = 25; // Very low alpha
}
ctx.globalCompositeOperation = 'overlay';
ctx.putImageData(noiseData, 0, 0);Multi-Stop Gradients
Professional gradients use 3+ color stops for smoother transitions.
Basic (2-stop):
gradient.addColorStop(0, color1);
gradient.addColorStop(1, color2);Professional (4-stop):
gradient.addColorStop(0, color1);
gradient.addColorStop(0.3, blendColors(color1, color2, 0.3));
gradient.addColorStop(0.7, blendColors(color1, color2, 0.7));
gradient.addColorStop(1, color2);The extra stops create smoother perceptual transitions.
Optical Centering
Letters aren't mathematically centered—they're optically adjusted.
Problem: Mathematical center places "T" too low visually.
Solution: Shift text up by 1-3% of icon size.
// Get text metrics
const metrics = ctx.measureText(letter);
const textHeight = metrics.actualBoundingBoxAscent + metrics.actualBoundingBoxDescent;
// Mathematical center
const mathY = size / 2;
// Optical center (shift up slightly)
const opticalY = mathY - (size * 0.02);
ctx.fillText(letter, size/2, opticalY);Size-Specific Adjustments
Effects should scale appropriately—or be disabled at small sizes.
| Size | Shadow | Highlight | Inner Glow | Noise |
|---|---|---|---|---|
| 16px | Off | Off | Off | Off |
| 32px | Light | Light | Off | Off |
| 64px | Full | Full | Light | Off |
| 128px+ | Full | Full | Full | Full |
Implementation:
function getEffectsForSize(size) {
if (size < 32) {
return { shadow: 0, highlight: 0, innerGlow: 0, noise: 0 };
}
if (size < 64) {
return { shadow: 0.5, highlight: 0.5, innerGlow: 0, noise: 0 };
}
return { shadow: 1, highlight: 1, innerGlow: 1, noise: 1 };
}Color Space Considerations
Perceptual Uniformity
HSL is better than RGB for color manipulation because it matches human perception.
// Lighten a color perceptually
function lighten(hex, amount) {
const hsl = hexToHsl(hex);
hsl.l = Math.min(1, hsl.l + amount);
return hslToHex(hsl);
}Gradient Color Transitions
RGB gradients can produce muddy intermediate colors. For better results:
1. Convert to HSL 2. Interpolate hue, saturation, lightness separately 3. Convert back to RGB
function blendColorsHsl(color1, color2, ratio) {
const hsl1 = hexToHsl(color1);
const hsl2 = hexToHsl(color2);
return hslToHex({
h: hsl1.h + (hsl2.h - hsl1.h) * ratio,
s: hsl1.s + (hsl2.s - hsl1.s) * ratio,
l: hsl1.l + (hsl2.l - hsl1.l) * ratio
});
}Composite Operations
Understanding blend modes is key to layered effects.
| Mode | Effect | Use For |
|---|---|---|
source-over | Normal stacking | Default |
overlay | Preserves shadows/highlights | Noise, texture |
multiply | Darkens | Shadows |
screen | Lightens | Highlights |
soft-light | Subtle overlay | Gentle effects |
ctx.globalCompositeOperation = 'overlay';
// Draw highlight/noise layer
ctx.globalCompositeOperation = 'source-over'; // ResetAnti-Aliasing Techniques
High-Resolution Rendering
Render at 2-4x target size, then downscale:
const scale = 4;
const largeCanvas = createCanvas(size * scale, size * scale);
// Draw at large size
const result = downscale(largeCanvas, size);Lanczos Resampling
For best downscaling quality:
# Pillow
result = large_image.resize((size, size), Image.Resampling.LANCZOS)Performance Optimization
Canvas Performance
- Batch operations: Group draws before any state changes
- Avoid recreating gradients: Cache gradient objects
- Use offscreen canvas: For complex compositions
// Cache the gradient
const cachedGradient = createGradient(size, colors);
// Use for multiple icons
sizes.forEach(s => {
draw(cachedGradient.resize(s));
});Pillow Performance
- Use numpy for noise: Much faster than pixel loops
- Composite fewer layers: Merge where possible
import numpy as np
def fast_noise(size, intensity):
noise = np.random.random((size, size)) * 255 * intensity
noise = noise.astype(np.uint8)
return Image.fromarray(noise, mode='L')Debugging Visual Issues
Banding in Gradients
Symptom: Visible steps in gradient instead of smooth transition. Cause: 8-bit color depth, large uniform areas. Fix: Add subtle noise (3-5%) to break up bands.
Muddy Colors
Symptom: Gradient intermediate colors look gray/brown. Cause: RGB interpolation through gray zone. Fix: Use HSL interpolation, or choose colors on similar hue.
Fuzzy Edges
Symptom: Icon edges look blurry/soft. Cause: Anti-aliasing without proper scaling. Fix: Render at 4x, use Lanczos downscaling.
Shadow Cutoff
Symptom: Shadow appears clipped at edges. Cause: Shadow extends beyond canvas bounds. Fix: Add padding, or offset shadow inward.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Favicon Atelier · Icon Design Studio</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Playfair+Display:ital,wght@0,400;0,700;1,400&family=Karla:wght@300;400;500;600&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
<style>
:root {
--cream: #FAF8F6;
--cream-dark: #F1EDE8;
--champagne: #C9A961;
--champagne-dark: #A88D4F;
--navy: #1A1D29;
--navy-light: #2C3040;
--sage: #9CAF88;
--terracotta: #D4876F;
--stone: #8B8680;
--stone-light: #C5C0BA;
--shadow: rgba(26, 29, 41, 0.08);
--shadow-strong: rgba(26, 29, 41, 0.15);
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(30px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes shimmer {
0% { background-position: -200% center; }
100% { background-position: 200% center; }
}
@keyframes float {
0%, 100% { transform: translateY(0px); }
50% { transform: translateY(-8px); }
}
body {
font-family: 'Karla', -apple-system, sans-serif;
background: linear-gradient(135deg, var(--cream) 0%, var(--cream-dark) 100%);
color: var(--navy);
min-height: 100vh;
padding: 60px 40px;
position: relative;
overflow-x: hidden;
}
body::before {
content: '';
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-image:
radial-gradient(circle at 20% 30%, rgba(201, 169, 97, 0.03) 0%, transparent 50%),
radial-gradient(circle at 80% 70%, rgba(156, 175, 136, 0.04) 0%, transparent 50%);
pointer-events: none;
z-index: 0;
}
.container {
max-width: 1600px;
margin: 0 auto;
position: relative;
z-index: 1;
}
/* Header */
header {
margin-bottom: 80px;
animation: fadeInUp 0.8s ease-out;
}
.header-content {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 40px;
flex-wrap: wrap;
}
.header-left {
flex: 1;
min-width: 300px;
}
.studio-label {
font-size: 11px;
letter-spacing: 3px;
text-transform: uppercase;
color: var(--champagne);
font-weight: 600;
margin-bottom: 16px;
display: inline-block;
}
h1 {
font-family: 'Playfair Display', serif;
font-size: 64px;
font-weight: 700;
line-height: 1.1;
color: var(--navy);
margin-bottom: 20px;
position: relative;
}
h1 em {
font-style: italic;
color: var(--champagne);
}
.subtitle {
font-size: 18px;
color: var(--stone);
line-height: 1.6;
max-width: 500px;
font-weight: 300;
}
.header-ornament {
width: 120px;
height: 2px;
background: linear-gradient(90deg, var(--champagne) 0%, transparent 100%);
margin-top: 32px;
}
/* Main Layout */
.workspace {
display: grid;
grid-template-columns: 420px 1fr;
gap: 48px;
align-items: start;
}
@media (max-width: 1200px) {
.workspace {
grid-template-columns: 1fr;
}
}
/* Panels */
.panel {
background: rgba(255, 255, 255, 0.6);
backdrop-filter: blur(20px);
border: 1px solid rgba(201, 169, 97, 0.15);
border-radius: 24px;
padding: 36px;
box-shadow:
0 4px 24px var(--shadow),
0 1px 0 rgba(255, 255, 255, 0.8) inset;
position: relative;
overflow: hidden;
animation: fadeInUp 0.8s ease-out;
animation-fill-mode: both;
}
.panel::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 1px;
background: linear-gradient(90deg,
transparent 0%,
var(--champagne) 50%,
transparent 100%);
opacity: 0.3;
}
.controls-column .panel:nth-child(1) { animation-delay: 0.1s; }
.controls-column .panel:nth-child(2) { animation-delay: 0.2s; }
.controls-column .panel:nth-child(3) { animation-delay: 0.3s; }
.controls-column .panel:nth-child(4) { animation-delay: 0.4s; }
.preview-section .panel:nth-child(1) { animation-delay: 0.2s; }
.preview-section .panel:nth-child(2) { animation-delay: 0.3s; }
.preview-section .panel:nth-child(3) { animation-delay: 0.4s; }
.preview-section .panel:nth-child(4) { animation-delay: 0.5s; }
.panel-title {
font-family: 'Playfair Display', serif;
font-size: 24px;
font-weight: 700;
color: var(--navy);
margin-bottom: 28px;
display: flex;
align-items: center;
gap: 12px;
position: relative;
}
.panel-title::after {
content: '';
flex: 1;
height: 1px;
background: linear-gradient(90deg, var(--champagne-dark) 0%, transparent 100%);
opacity: 0.2;
}
/* Template Grid */
.template-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 12px;
}
.template-btn {
background: rgba(255, 255, 255, 0.8);
border: 2px solid rgba(201, 169, 97, 0.15);
border-radius: 16px;
padding: 20px 16px;
cursor: pointer;
transition: all 0.35s cubic-bezier(0.4, 0, 0.2, 1);
position: relative;
overflow: hidden;
}
.template-btn::before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg,
transparent 0%,
rgba(201, 169, 97, 0.1) 50%,
transparent 100%);
transition: left 0.6s;
}
.template-btn:hover::before {
left: 100%;
}
.template-btn:hover {
border-color: var(--champagne);
transform: translateY(-2px);
box-shadow: 0 8px 24px var(--shadow);
}
.template-btn.active {
border-color: var(--champagne);
background: rgba(201, 169, 97, 0.08);
box-shadow: 0 4px 16px rgba(201, 169, 97, 0.2);
}
.template-icon {
width: 48px;
height: 48px;
margin: 0 auto 12px;
border-radius: 12px;
border: 1px solid rgba(201, 169, 97, 0.2);
box-shadow: 0 2px 8px var(--shadow);
}
.template-name {
font-size: 14px;
font-weight: 500;
color: var(--navy);
text-align: center;
letter-spacing: 0.3px;
}
/* Inputs */
.control-section {
margin-bottom: 28px;
}
.control-section:last-child {
margin-bottom: 0;
}
.control-label {
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 1.5px;
color: var(--champagne-dark);
margin-bottom: 14px;
display: block;
}
.input-row {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
gap: 14px;
}
.input-group {
display: flex;
flex-direction: column;
gap: 8px;
}
.input-group label {
font-size: 13px;
color: var(--stone);
font-weight: 500;
}
input[type="text"],
select {
width: 100%;
background: rgba(255, 255, 255, 0.9);
border: 1px solid rgba(139, 134, 128, 0.2);
border-radius: 10px;
padding: 12px 16px;
font-size: 15px;
font-family: 'Karla', sans-serif;
color: var(--navy);
transition: all 0.3s ease;
}
input[type="text"]:focus,
select:focus {
outline: none;
border-color: var(--champagne);
box-shadow: 0 0 0 3px rgba(201, 169, 97, 0.1);
background: white;
}
input[type="color"] {
width: 100%;
height: 52px;
background: white;
border: 1px solid rgba(139, 134, 128, 0.2);
border-radius: 10px;
padding: 6px;
cursor: pointer;
transition: all 0.3s ease;
}
input[type="color"]:hover {
border-color: var(--champagne);
box-shadow: 0 4px 12px var(--shadow);
}
/* Icon Grid */
.icon-grid {
display: grid;
grid-template-columns: repeat(6, 1fr);
gap: 8px;
max-height: 220px;
overflow-y: auto;
padding: 4px;
scrollbar-width: thin;
scrollbar-color: var(--champagne) transparent;
}
.icon-grid::-webkit-scrollbar {
width: 6px;
}
.icon-grid::-webkit-scrollbar-track {
background: rgba(201, 169, 97, 0.05);
border-radius: 3px;
}
.icon-grid::-webkit-scrollbar-thumb {
background: var(--champagne);
border-radius: 3px;
}
.icon-btn {
aspect-ratio: 1;
background: rgba(255, 255, 255, 0.8);
border: 1px solid rgba(139, 134, 128, 0.2);
border-radius: 10px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s ease;
color: var(--stone);
}
.icon-btn:hover {
border-color: var(--champagne);
color: var(--champagne-dark);
transform: scale(1.05);
}
.icon-btn.active {
border-color: var(--champagne);
background: rgba(201, 169, 97, 0.12);
color: var(--champagne-dark);
box-shadow: 0 2px 8px rgba(201, 169, 97, 0.3);
}
.icon-btn svg {
width: 22px;
height: 22px;
stroke-width: 1.5;
}
/* Range Inputs */
input[type="range"] {
width: 100%;
height: 6px;
background: linear-gradient(90deg,
rgba(201, 169, 97, 0.2) 0%,
rgba(201, 169, 97, 0.08) 100%);
border-radius: 3px;
outline: none;
-webkit-appearance: none;
}
input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
width: 18px;
height: 18px;
background: linear-gradient(135deg, var(--champagne) 0%, var(--champagne-dark) 100%);
border-radius: 50%;
cursor: pointer;
box-shadow: 0 2px 8px var(--shadow);
transition: all 0.2s;
}
input[type="range"]::-webkit-slider-thumb:hover {
transform: scale(1.2);
box-shadow: 0 4px 12px rgba(201, 169, 97, 0.4);
}
input[type="range"]::-moz-range-thumb {
width: 18px;
height: 18px;
background: linear-gradient(135deg, var(--champagne) 0%, var(--champagne-dark) 100%);
border-radius: 50%;
cursor: pointer;
border: none;
box-shadow: 0 2px 8px var(--shadow);
}
.range-value {
font-size: 12px;
font-family: 'JetBrains Mono', monospace;
color: var(--champagne-dark);
text-align: right;
margin-top: 6px;
font-weight: 500;
}
/* Checkbox */
.checkbox-row {
display: flex;
align-items: center;
gap: 12px;
margin-top: 16px;
}
.checkbox-row input[type="checkbox"] {
width: 20px;
height: 20px;
accent-color: var(--champagne);
cursor: pointer;
}
.checkbox-row label {
font-size: 14px;
color: var(--stone);
cursor: pointer;
font-weight: 400;
}
/* Effects */
.effects-grid {
display: flex;
flex-direction: column;
gap: 20px;
}
.effect-control {
background: rgba(255, 255, 255, 0.5);
border-radius: 14px;
padding: 18px;
border: 1px solid rgba(201, 169, 97, 0.1);
}
.effect-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
}
.effect-name {
font-size: 14px;
font-weight: 500;
color: var(--navy);
letter-spacing: 0.3px;
}
.effect-toggle {
width: 44px;
height: 24px;
background: rgba(139, 134, 128, 0.2);
border-radius: 12px;
position: relative;
cursor: pointer;
transition: all 0.3s ease;
}
.effect-toggle.active {
background: linear-gradient(135deg, var(--champagne) 0%, var(--champagne-dark) 100%);
}
.effect-toggle::after {
content: '';
position: absolute;
width: 20px;
height: 20px;
background: white;
border-radius: 50%;
top: 2px;
left: 2px;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
box-shadow: 0 2px 6px var(--shadow);
}
.effect-toggle.active::after {
transform: translateX(20px);
}
/* Generate Button */
.generate-btn {
width: 100%;
background: linear-gradient(135deg, var(--navy) 0%, var(--navy-light) 100%);
border: none;
border-radius: 16px;
padding: 20px;
font-size: 16px;
font-weight: 600;
color: var(--cream);
cursor: pointer;
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
margin-top: 32px;
position: relative;
overflow: hidden;
letter-spacing: 0.5px;
box-shadow:
0 8px 24px rgba(26, 29, 41, 0.2),
0 2px 8px rgba(26, 29, 41, 0.15);
}
.generate-btn::before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg,
transparent 0%,
rgba(255, 255, 255, 0.15) 50%,
transparent 100%);
transition: left 0.6s;
}
.generate-btn:hover::before {
left: 100%;
}
.generate-btn:hover {
transform: translateY(-3px);
box-shadow:
0 12px 32px rgba(26, 29, 41, 0.25),
0 4px 16px rgba(26, 29, 41, 0.2);
}
.generate-btn:active {
transform: translateY(-1px);
}
/* Preview Section */
.preview-section {
display: flex;
flex-direction: column;
gap: 32px;
}
.main-preview {
display: flex;
align-items: center;
justify-content: center;
min-height: 400px;
background:
radial-gradient(circle at 30% 40%, rgba(201, 169, 97, 0.08) 0%, transparent 50%),
linear-gradient(135deg, rgba(255, 255, 255, 0.8) 0%, rgba(241, 237, 232, 0.6) 100%);
border-radius: 20px;
border: 1px solid rgba(201, 169, 97, 0.2);
position: relative;
overflow: hidden;
box-shadow: 0 2px 16px var(--shadow) inset;
}
.main-preview::before {
content: '';
position: absolute;
top: -50%;
left: -50%;
width: 200%;
height: 200%;
background: radial-gradient(circle, rgba(201, 169, 97, 0.03) 1px, transparent 1px);
background-size: 20px 20px;
opacity: 0.5;
}
.main-preview canvas {
border-radius: 28px;
box-shadow:
0 20px 60px rgba(26, 29, 41, 0.15),
0 8px 24px rgba(26, 29, 41, 0.1),
0 0 0 1px rgba(201, 169, 97, 0.1);
position: relative;
z-index: 1;
animation: float 4s ease-in-out infinite;
}
/* Size Previews */
.size-previews {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(100px, 1fr));
gap: 16px;
}
.size-preview {
display: flex;
flex-direction: column;
align-items: center;
gap: 10px;
padding: 20px;
background: rgba(255, 255, 255, 0.7);
border-radius: 14px;
border: 1px solid rgba(201, 169, 97, 0.12);
transition: all 0.3s ease;
}
.size-preview:hover {
transform: translateY(-4px);
box-shadow: 0 8px 20px var(--shadow);
border-color: var(--champagne);
}
.size-preview canvas {
image-rendering: pixelated;
border-radius: 6px;
box-shadow: 0 2px 8px var(--shadow);
}
.size-label {
font-size: 11px;
font-family: 'JetBrains Mono', monospace;
color: var(--champagne-dark);
font-weight: 500;
letter-spacing: 0.5px;
}
/* Context Previews */
.context-previews {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 20px;
}
.context-preview {
background: rgba(255, 255, 255, 0.8);
border-radius: 16px;
border: 1px solid rgba(201, 169, 97, 0.15);
overflow: hidden;
box-shadow: 0 4px 16px var(--shadow);
transition: all 0.3s ease;
}
.context-preview:hover {
transform: translateY(-2px);
box-shadow: 0 8px 24px var(--shadow-strong);
}
.context-header {
padding: 14px 20px;
border-bottom: 1px solid rgba(201, 169, 97, 0.15);
font-size: 11px;
font-weight: 600;
color: var(--champagne-dark);
text-transform: uppercase;
letter-spacing: 1.2px;
background: rgba(201, 169, 97, 0.05);
}
.browser-tab {
display: flex;
align-items: center;
gap: 10px;
padding: 14px 20px;
background: #e8e6e3;
}
.browser-tab canvas {
border-radius: 3px;
box-shadow: 0 1px 3px rgba(26, 29, 41, 0.2);
}
.browser-tab .tab-title {
font-size: 13px;
color: var(--navy-light);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.bookmark-item {
display: flex;
align-items: center;
gap: 12px;
padding: 14px 20px;
}
.bookmark-item canvas {
border-radius: 4px;
box-shadow: 0 2px 6px var(--shadow);
}
.bookmark-item .bookmark-name {
font-size: 14px;
color: var(--navy);
font-weight: 500;
}
/* Downloads */
.download-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
gap: 12px;
}
.download-btn {
background: rgba(255, 255, 255, 0.8);
border: 1px solid rgba(201, 169, 97, 0.2);
border-radius: 14px;
padding: 16px;
cursor: pointer;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
text-align: center;
position: relative;
overflow: hidden;
}
.download-btn::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(135deg, var(--champagne) 0%, var(--champagne-dark) 100%);
opacity: 0;
transition: opacity 0.3s;
}
.download-btn:hover {
border-color: var(--champagne);
transform: translateY(-3px);
box-shadow: 0 8px 20px rgba(201, 169, 97, 0.25);
}
.download-btn:hover::before {
opacity: 0.08;
}
.download-btn .format {
font-size: 14px;
font-weight: 600;
color: var(--navy);
margin-bottom: 4px;
position: relative;
z-index: 1;
}
.download-btn .size {
font-size: 11px;
font-family: 'JetBrains Mono', monospace;
color: var(--stone);
position: relative;
z-index: 1;
}
.download-all-btn {
grid-column: 1 / -1;
background: linear-gradient(135deg, var(--sage) 0%, #88A074 100%);
border: none;
color: white;
font-weight: 600;
padding: 20px;
font-size: 15px;
letter-spacing: 0.5px;
box-shadow: 0 6px 20px rgba(156, 175, 136, 0.3);
}
.download-all-btn:hover {
transform: translateY(-3px);
box-shadow: 0 10px 28px rgba(156, 175, 136, 0.4);
}
.download-all-btn::before {
display: none;
}
/* Responsive */
@media (max-width: 768px) {
body {
padding: 40px 20px;
}
h1 {
font-size: 42px;
}
.workspace {
gap: 32px;
}
.panel {
padding: 28px;
}
.template-grid {
grid-template-columns: repeat(2, 1fr);
gap: 10px;
}
.icon-grid {
grid-template-columns: repeat(5, 1fr);
}
}
</style>
</head>
<body>
<div class="container">
<header>
<div class="header-content">
<div class="header-left">
<div class="studio-label">Design Studio</div>
<h1>Favicon <em>Atelier</em></h1>
<p class="subtitle">
Craft distinctive, professional-grade icons with advanced layering effects,
curated templates, and studio-quality rendering.
</p>
<div class="header-ornament"></div>
</div>
</div>
</header>
<div class="workspace">
<!-- Controls Column -->
<div class="controls-column">
<div class="panel">
<div class="panel-title">Templates</div>
<div class="template-grid" id="templateGrid"></div>
</div>
<div class="panel" style="margin-top: 24px;">
<div class="panel-title">Content</div>
<div class="control-section">
<div class="control-label">Type</div>
<div class="input-row">
<div class="input-group">
<select id="contentType" onchange="updateContentType()">
<option value="letter">Letter / Monogram</option>
<option value="icon">Icon</option>
<option value="emoji">Emoji</option>
</select>
</div>
</div>
</div>
<div class="control-section" id="letterSection">
<div class="input-row">
<div class="input-group">
<label>Letter</label>
<input type="text" id="letter" value="A" maxlength="2" oninput="generate()">
</div>
<div class="input-group">
<label>Font</label>
<select id="fontFamily" onchange="generate()">
<option value="Playfair Display">Playfair Display</option>
<option value="Karla">Karla</option>
<option value="JetBrains Mono">JetBrains Mono</option>
<option value="system-ui">System</option>
</select>
</div>
</div>
</div>
<div class="control-section" id="iconSection" style="display:none;">
<div class="control-label">Choose Icon</div>
<div class="icon-grid" id="iconGrid"></div>
</div>
<div class="control-section" id="emojiSection" style="display:none;">
<div class="input-group">
<label>Emoji</label>
<input type="text" id="emoji" value="🚀" maxlength="2" oninput="generate()">
</div>
</div>
</div>
<div class="panel" style="margin-top: 24px;">
<div class="panel-title">Colors</div>
<div class="input-row">
<div class="input-group">
<label>Background</label>
<input type="color" id="bgColor" value="#6366f1" oninput="generate()">
</div>
<div class="input-group">
<label>Gradient End</label>
<input type="color" id="bgColor2" value="#8b5cf6" oninput="generate()">
</div>
</div>
<div class="input-row" style="margin-top: 14px;">
<div class="input-group">
<label>Icon / Text</label>
<input type="color" id="fgColor" value="#ffffff" oninput="generate()">
</div>
</div>
<div class="checkbox-row">
<input type="checkbox" id="useGradient" checked onchange="generate()">
<label for="useGradient">Use gradient background</label>
</div>
</div>
<div class="panel" style="margin-top: 24px;">
<div class="panel-title">Effects</div>
<div class="effects-grid">
<div class="effect-control">
<div class="effect-header">
<span class="effect-name">Drop Shadow</span>
<div class="effect-toggle active" id="shadowToggle" onclick="toggleEffect('shadow')"></div>
</div>
<input type="range" id="shadowIntensity" min="0" max="100" value="40" oninput="generate()">
<div class="range-value" id="shadowValue">40%</div>
</div>
<div class="effect-control">
<div class="effect-header">
<span class="effect-name">Inner Glow</span>
<div class="effect-toggle" id="innerGlowToggle" onclick="toggleEffect('innerGlow')"></div>
</div>
<input type="range" id="innerGlowIntensity" min="0" max="100" value="30" oninput="generate()">
<div class="range-value" id="innerGlowValue">30%</div>
</div>
<div class="effect-control">
<div class="effect-header">
<span class="effect-name">Highlight</span>
<div class="effect-toggle active" id="highlightToggle" onclick="toggleEffect('highlight')"></div>
</div>
<input type="range" id="highlightIntensity" min="0" max="100" value="25" oninput="generate()">
<div class="range-value" id="highlightValue">25%</div>
</div>
<div class="effect-control">
<div class="effect-header">
<span class="effect-name">Noise / Grain</span>
<div class="effect-toggle" id="noiseToggle" onclick="toggleEffect('noise')"></div>
</div>
<input type="range" id="noiseIntensity" min="0" max="100" value="8" oninput="generate()">
<div class="range-value" id="noiseValue">8%</div>
</div>
<div class="effect-control">
<div class="effect-header">
<span class="effect-name">Corner Radius</span>
</div>
<input type="range" id="cornerRadius" min="0" max="50" value="22" oninput="generate()">
<div class="range-value" id="cornerValue">22%</div>
</div>
</div>
</div>
<button class="generate-btn" onclick="generate()">
Generate Favicon
</button>
</div>
<!-- Preview Section -->
<div class="preview-section">
<div class="panel">
<div class="panel-title">Preview</div>
<div class="main-preview">
<canvas id="mainCanvas" width="256" height="256"></canvas>
</div>
</div>
<div class="panel">
<div class="panel-title">All Sizes</div>
<div class="size-previews" id="sizePreviewsContainer"></div>
</div>
<div class="panel">
<div class="panel-title">In Context</div>
<div class="context-previews">
<div class="context-preview">
<div class="context-header">Browser Tab</div>
<div class="browser-tab">
<canvas id="tabCanvas" width="16" height="16"></canvas>
<span class="tab-title">My Awesome App</span>
</div>
</div>
<div class="context-preview">
<div class="context-header">Bookmarks Bar</div>
<div class="bookmark-item">
<canvas id="bookmarkCanvas" width="16" height="16"></canvas>
<span class="bookmark-name">My Awesome App</span>
</div>
</div>
</div>
</div>
<div class="panel">
<div class="panel-title">Download</div>
<div class="download-grid" id="downloadGrid"></div>
</div>
</div>
</div>
</div>
<script>
// =====================================================
// DESIGN TEMPLATES
// =====================================================
const TEMPLATES = [
{
id: 'modern',
name: 'Modern',
preview: 'linear-gradient(135deg, #6366f1, #8b5cf6)',
settings: {
bgColor: '#6366f1',
bgColor2: '#8b5cf6',
fgColor: '#ffffff',
useGradient: true,
shadow: true,
shadowIntensity: 40,
highlight: true,
highlightIntensity: 25,
innerGlow: false,
noise: false,
cornerRadius: 22
}
},
{
id: 'vibrant',
name: 'Vibrant',
preview: 'linear-gradient(135deg, #ec4899, #f97316)',
settings: {
bgColor: '#ec4899',
bgColor2: '#f97316',
fgColor: '#ffffff',
useGradient: true,
shadow: true,
shadowIntensity: 50,
highlight: true,
highlightIntensity: 30,
innerGlow: true,
innerGlowIntensity: 20,
noise: false,
cornerRadius: 22
}
},
{
id: 'minimal',
name: 'Minimal',
preview: '#18181b',
settings: {
bgColor: '#18181b',
bgColor2: '#27272a',
fgColor: '#fafafa',
useGradient: false,
shadow: true,
shadowIntensity: 30,
highlight: false,
innerGlow: false,
noise: true,
noiseIntensity: 5,
cornerRadius: 18
}
},
{
id: 'glass',
name: 'Glass',
preview: 'linear-gradient(135deg, rgba(59, 130, 246, 0.8), rgba(6, 182, 212, 0.8))',
settings: {
bgColor: '#3b82f6',
bgColor2: '#06b6d4',
fgColor: '#ffffff',
useGradient: true,
shadow: true,
shadowIntensity: 35,
highlight: true,
highlightIntensity: 50,
innerGlow: true,
innerGlowIntensity: 40,
noise: true,
noiseIntensity: 3,
cornerRadius: 24
}
},
{
id: 'neon',
name: 'Neon',
preview: 'linear-gradient(135deg, #0f172a, #1e293b)',
settings: {
bgColor: '#0f172a',
bgColor2: '#1e293b',
fgColor: '#22d3ee',
useGradient: true,
shadow: true,
shadowIntensity: 60,
highlight: false,
innerGlow: true,
innerGlowIntensity: 60,
noise: true,
noiseIntensity: 4,
cornerRadius: 20
}
},
{
id: 'warm',
name: 'Warm',
preview: 'linear-gradient(135deg, #f59e0b, #ef4444)',
settings: {
bgColor: '#f59e0b',
bgColor2: '#ef4444',
fgColor: '#ffffff',
useGradient: true,
shadow: true,
shadowIntensity: 45,
highlight: true,
highlightIntensity: 35,
innerGlow: false,
noise: false,
cornerRadius: 22
}
},
{
id: 'forest',
name: 'Forest',
preview: 'linear-gradient(135deg, #22c55e, #14b8a6)',
settings: {
bgColor: '#22c55e',
bgColor2: '#14b8a6',
fgColor: '#ffffff',
useGradient: true,
shadow: true,
shadowIntensity: 40,
highlight: true,
highlightIntensity: 25,
innerGlow: false,
noise: true,
noiseIntensity: 6,
cornerRadius: 22
}
},
{
id: 'mono',
name: 'Mono',
preview: '#ffffff',
settings: {
bgColor: '#ffffff',
bgColor2: '#f4f4f5',
fgColor: '#18181b',
useGradient: false,
shadow: true,
shadowIntensity: 25,
highlight: false,
innerGlow: false,
noise: false,
cornerRadius: 18
}
}
];
// =====================================================
// LUCIDE ICONS (subset)
// =====================================================
const ICONS = {
rocket: '<path d="M4.5 16.5c-1.5 1.26-2 5-2 5s3.74-.5 5-2c.71-.84.7-2.13-.09-2.91a2.18 2.18 0 0 0-2.91-.09z"/><path d="m12 15-3-3a22 22 0 0 1 2-3.95A12.88 12.88 0 0 1 22 2c0 2.72-.78 7.5-6 11a22.35 22.35 0 0 1-4 2z"/><path d="M9 12H4s.55-3.03 2-4c1.62-1.08 5 0 5 0"/><path d="M12 15v5s3.03-.55 4-2c1.08-1.62 0-5 0-5"/>',
zap: '<polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/>',
star: '<polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/>',
heart: '<path d="M19 14c1.49-1.46 3-3.21 3-5.5A5.5 5.5 0 0 0 16.5 3c-1.76 0-3 .5-4.5 2-1.5-1.5-2.74-2-4.5-2A5.5 5.5 0 0 0 2 8.5c0 2.3 1.5 4.05 3 5.5l7 7Z"/>',
code: '<polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/>',
box: '<path d="M21 8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16Z"/><path d="m3.3 7 8.7 5 8.7-5"/><path d="M12 22V12"/>',
compass: '<circle cx="12" cy="12" r="10"/><polygon points="16.24 7.76 14.12 14.12 7.76 16.24 9.88 9.88 16.24 7.76"/>',
flame: '<path d="M8.5 14.5A2.5 2.5 0 0 0 11 12c0-1.38-.5-2-1-3-1.072-2.143-.224-4.054 2-6 .5 2.5 2 4.9 4 6.5 2 1.6 3 3.5 3 5.5a7 7 0 1 1-14 0c0-1.153.433-2.294 1-3a2.5 2.5 0 0 0 2.5 2.5z"/>',
globe: '<circle cx="12" cy="12" r="10"/><path d="M12 2a14.5 14.5 0 0 0 0 20 14.5 14.5 0 0 0 0-20"/><path d="M2 12h20"/>',
layers: '<polygon points="12 2 2 7 12 12 22 7 12 2"/><polyline points="2 17 12 22 22 17"/><polyline points="2 12 12 17 22 12"/>',
music: '<path d="M9 18V5l12-2v13"/><circle cx="6" cy="18" r="3"/><circle cx="18" cy="16" r="3"/>',
send: '<path d="m22 2-7 20-4-9-9-4Z"/><path d="M22 2 11 13"/>',
shield: '<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10"/>',
sparkles: '<path d="m12 3-1.912 5.813a2 2 0 0 1-1.275 1.275L3 12l5.813 1.912a2 2 0 0 1 1.275 1.275L12 21l1.912-5.813a2 2 0 0 1 1.275-1.275L21 12l-5.813-1.912a2 2 0 0 1-1.275-1.275L12 3Z"/><path d="M5 3v4"/><path d="M19 17v4"/><path d="M3 5h4"/><path d="M17 19h4"/>',
sun: '<circle cx="12" cy="12" r="4"/><path d="M12 2v2"/><path d="M12 20v2"/><path d="m4.93 4.93 1.41 1.41"/><path d="m17.66 17.66 1.41 1.41"/><path d="M2 12h2"/><path d="M20 12h2"/><path d="m6.34 17.66-1.41 1.41"/><path d="m19.07 4.93-1.41 1.41"/>',
target: '<circle cx="12" cy="12" r="10"/><circle cx="12" cy="12" r="6"/><circle cx="12" cy="12" r="2"/>',
terminal: '<polyline points="4 17 10 11 4 5"/><line x1="12" x2="20" y1="19" y2="19"/>',
wand: '<path d="M15 4V2"/><path d="M15 16v-2"/><path d="M8 9h2"/><path d="M20 9h2"/><path d="M17.8 11.8 19 13"/><path d="M15 9h0"/><path d="M17.8 6.2 19 5"/><path d="m3 21 9-9"/><path d="M12.2 6.2 11 5"/>'
};
// =====================================================
// STATE
// =====================================================
let currentTemplate = 'modern';
let selectedIcon = 'rocket';
let effects = {
shadow: true,
innerGlow: false,
highlight: true,
noise: false
};
const SIZES = [16, 32, 48, 64, 128, 180, 192, 512];
// =====================================================
// INITIALIZATION
// =====================================================
function init() {
renderTemplates();
renderIcons();
renderDownloadButtons();
renderSizePreviews();
applyTemplate('modern');
generate();
}
function renderTemplates() {
const grid = document.getElementById('templateGrid');
grid.innerHTML = TEMPLATES.map(t => `
<div class="template-btn ${t.id === currentTemplate ? 'active' : ''}"
onclick="applyTemplate('${t.id}')"
data-template="${t.id}">
<div class="template-icon" style="background: ${t.preview};"></div>
<div class="template-name">${t.name}</div>
</div>
`).join('');
}
function renderIcons() {
const grid = document.getElementById('iconGrid');
grid.innerHTML = Object.entries(ICONS).map(([name, path]) => `
<div class="icon-btn ${name === selectedIcon ? 'active' : ''}"
onclick="selectIcon('${name}')"
data-icon="${name}">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
${path}
</svg>
</div>
`).join('');
}
function renderSizePreviews() {
const container = document.getElementById('sizePreviewsContainer');
container.innerHTML = SIZES.map(size => `
<div class="size-preview">
<canvas id="preview_${size}" width="${size}" height="${size}"
style="width: ${Math.min(size, 64)}px; height: ${Math.min(size, 64)}px;"></canvas>
<span class="size-label">${size}×${size}</span>
</div>
`).join('');
}
function renderDownloadButtons() {
const grid = document.getElementById('downloadGrid');
const buttons = SIZES.map(size => {
const label = size === 180 ? 'Apple Touch' : `${size}×${size}`;
return `
<div class="download-btn" onclick="downloadSize(${size})">
<div class="format">${label}</div>
<div class="size">.png</div>
</div>
`;
});
buttons.push(`
<div class="download-btn" onclick="downloadSVG()">
<div class="format">Vector</div>
<div class="size">.svg</div>
</div>
`);
buttons.push(`
<div class="download-btn download-all-btn" onclick="downloadAll()">
📦 Download All Files
</div>
`);
grid.innerHTML = buttons.join('');
}
// =====================================================
// TEMPLATE APPLICATION
// =====================================================
function applyTemplate(templateId) {
currentTemplate = templateId;
const template = TEMPLATES.find(t => t.id === templateId);
if (!template) return;
const s = template.settings;
document.getElementById('bgColor').value = s.bgColor;
document.getElementById('bgColor2').value = s.bgColor2;
document.getElementById('fgColor').value = s.fgColor;
document.getElementById('useGradient').checked = s.useGradient;
// Effects
effects.shadow = s.shadow ?? true;
effects.innerGlow = s.innerGlow ?? false;
effects.highlight = s.highlight ?? true;
effects.noise = s.noise ?? false;
document.getElementById('shadowToggle').classList.toggle('active', effects.shadow);
document.getElementById('innerGlowToggle').classList.toggle('active', effects.innerGlow);
document.getElementById('highlightToggle').classList.toggle('active', effects.highlight);
document.getElementById('noiseToggle').classList.toggle('active', effects.noise);
document.getElementById('shadowIntensity').value = s.shadowIntensity ?? 40;
document.getElementById('innerGlowIntensity').value = s.innerGlowIntensity ?? 30;
document.getElementById('highlightIntensity').value = s.highlightIntensity ?? 25;
document.getElementById('noiseIntensity').value = s.noiseIntensity ?? 8;
document.getElementById('cornerRadius').value = s.cornerRadius ?? 22;
updateRangeValues();
// Update template buttons
document.querySelectorAll('.template-btn').forEach(btn => {
btn.classList.toggle('active', btn.dataset.template === templateId);
});
generate();
}
function updateRangeValues() {
document.getElementById('shadowValue').textContent = document.getElementById('shadowIntensity').value + '%';
document.getElementById('innerGlowValue').textContent = document.getElementById('innerGlowIntensity').value + '%';
document.getElementById('highlightValue').textContent = document.getElementById('highlightIntensity').value + '%';
document.getElementById('noiseValue').textContent = document.getElementById('noiseIntensity').value + '%';
document.getElementById('cornerValue').textContent = document.getElementById('cornerRadius').value + '%';
}
// =====================================================
// UI INTERACTIONS
// =====================================================
function toggleEffect(effectName) {
effects[effectName] = !effects[effectName];
document.getElementById(effectName + 'Toggle').classList.toggle('active', effects[effectName]);
generate();
}
function updateContentType() {
const type = document.getElementById('contentType').value;
document.getElementById('letterSection').style.display = type === 'letter' ? 'block' : 'none';
document.getElementById('iconSection').style.display = type === 'icon' ? 'block' : 'none';
document.getElementById('emojiSection').style.display = type === 'emoji' ? 'block' : 'none';
generate();
}
function selectIcon(iconName) {
selectedIcon = iconName;
document.querySelectorAll('.icon-btn').forEach(btn => {
btn.classList.toggle('active', btn.dataset.icon === iconName);
});
generate();
}
// =====================================================
// GENERATION ENGINE (keeping original functionality)
// =====================================================
function generate() {
updateRangeValues();
// Generate all sizes
SIZES.forEach(size => {
const canvas = document.getElementById(`preview_${size}`);
if (canvas) generateFavicon(canvas, size);
});
// Main preview
generateFavicon(document.getElementById('mainCanvas'), 256);
// Context previews
generateFavicon(document.getElementById('tabCanvas'), 16);
generateFavicon(document.getElementById('bookmarkCanvas'), 16);
}
function generateFavicon(canvas, size) {
const ctx = canvas.getContext('2d');
ctx.clearRect(0, 0, size, size);
// Settings
const bgColor = document.getElementById('bgColor').value;
const bgColor2 = document.getElementById('bgColor2').value;
const fgColor = document.getElementById('fgColor').value;
const useGradient = document.getElementById('useGradient').checked;
const cornerRadius = (parseInt(document.getElementById('cornerRadius').value) / 100) * size * 0.5;
const shadowIntensity = parseInt(document.getElementById('shadowIntensity').value) / 100;
const highlightIntensity = parseInt(document.getElementById('highlightIntensity').value) / 100;
const innerGlowIntensity = parseInt(document.getElementById('innerGlowIntensity').value) / 100;
const noiseIntensity = parseInt(document.getElementById('noiseIntensity').value) / 100;
// Enable high quality rendering
ctx.imageSmoothingEnabled = true;
ctx.imageSmoothingQuality = 'high';
// ===== LAYER 1: DROP SHADOW =====
if (effects.shadow && size >= 32) {
ctx.save();
ctx.shadowColor = 'rgba(0, 0, 0, ' + (0.3 * shadowIntensity) + ')';
ctx.shadowBlur = size * 0.15 * shadowIntensity;
ctx.shadowOffsetY = size * 0.05 * shadowIntensity;
ctx.fillStyle = bgColor;
drawRoundedRect(ctx, size * 0.05, size * 0.05, size * 0.9, size * 0.9, cornerRadius * 0.9);
ctx.fill();
ctx.restore();
}
// ===== LAYER 2: BACKGROUND =====
ctx.save();
if (useGradient) {
const gradient = ctx.createLinearGradient(0, 0, size, size);
gradient.addColorStop(0, bgColor);
gradient.addColorStop(0.5, blendColors(bgColor, bgColor2, 0.5));
gradient.addColorStop(1, bgColor2);
ctx.fillStyle = gradient;
} else {
ctx.fillStyle = bgColor;
}
drawRoundedRect(ctx, 0, 0, size, size, cornerRadius);
ctx.fill();
ctx.restore();
// ===== LAYER 3: INNER GLOW =====
if (effects.innerGlow && size >= 32) {
ctx.save();
ctx.globalCompositeOperation = 'source-atop';
const glowGradient = ctx.createRadialGradient(
size * 0.5, size * 0.3, 0,
size * 0.5, size * 0.5, size * 0.7
);
glowGradient.addColorStop(0, 'rgba(255, 255, 255, ' + (0.4 * innerGlowIntensity) + ')');
glowGradient.addColorStop(0.5, 'rgba(255, 255, 255, ' + (0.1 * innerGlowIntensity) + ')');
glowGradient.addColorStop(1, 'rgba(0, 0, 0, ' + (0.2 * innerGlowIntensity) + ')');
ctx.fillStyle = glowGradient;
drawRoundedRect(ctx, 0, 0, size, size, cornerRadius);
ctx.fill();
ctx.restore();
}
// ===== LAYER 4: HIGHLIGHT =====
if (effects.highlight && size >= 32) {
ctx.save();
ctx.globalCompositeOperation = 'overlay';
const highlightGradient = ctx.createLinearGradient(0, 0, 0, size);
highlightGradient.addColorStop(0, 'rgba(255, 255, 255, ' + (0.5 * highlightIntensity) + ')');
highlightGradient.addColorStop(0.5, 'rgba(255, 255, 255, 0)');
highlightGradient.addColorStop(1, 'rgba(0, 0, 0, ' + (0.2 * highlightIntensity) + ')');
ctx.fillStyle = highlightGradient;
drawRoundedRect(ctx, 0, 0, size, size, cornerRadius);
ctx.fill();
ctx.restore();
}
// ===== LAYER 5: NOISE/GRAIN =====
if (effects.noise && size >= 64) {
ctx.save();
ctx.globalCompositeOperation = 'overlay';
const noiseCanvas = generateNoise(size, noiseIntensity);
ctx.drawImage(noiseCanvas, 0, 0);
ctx.restore();
}
// ===== LAYER 6: CONTENT =====
const contentType = document.getElementById('contentType').value;
if (contentType === 'letter') {
drawLetter(ctx, size, fgColor);
} else if (contentType === 'icon') {
drawIcon(ctx, size, fgColor);
} else if (contentType === 'emoji') {
drawEmoji(ctx, size);
}
}
function drawRoundedRect(ctx, x, y, width, height, radius) {
ctx.beginPath();
ctx.roundRect(x, y, width, height, radius);
}
function drawLetter(ctx, size, color) {
const letter = document.getElementById('letter').value || 'A';
const fontFamily = document.getElementById('fontFamily').value;
ctx.save();
if (size >= 32 && effects.shadow) {
ctx.shadowColor = 'rgba(0, 0, 0, 0.3)';
ctx.shadowBlur = size * 0.04;
ctx.shadowOffsetY = size * 0.02;
}
ctx.fillStyle = color;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
const fontSize = letter.length > 1 ? size * 0.4 : size * 0.55;
ctx.font = `bold ${fontSize}px "${fontFamily}", sans-serif`;
const metrics = ctx.measureText(letter.toUpperCase());
const opticalOffset = size * 0.02;
ctx.fillText(letter.toUpperCase(), size / 2, size / 2 - opticalOffset);
ctx.restore();
}
function drawIcon(ctx, size, color) {
const iconPath = ICONS[selectedIcon];
if (!iconPath) return;
ctx.save();
if (size >= 32 && effects.shadow) {
ctx.shadowColor = 'rgba(0, 0, 0, 0.3)';
ctx.shadowBlur = size * 0.04;
ctx.shadowOffsetY = size * 0.02;
}
const iconSize = size * 0.55;
const offset = (size - iconSize) / 2;
ctx.translate(offset, offset);
ctx.scale(iconSize / 24, iconSize / 24);
ctx.strokeStyle = color;
ctx.lineWidth = 2;
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
ctx.fillStyle = 'none';
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
svg.innerHTML = iconPath;
svg.querySelectorAll('path, polyline, polygon, circle, line, rect').forEach(el => {
const path = new Path2D();
if (el.tagName === 'path') {
path.addPath(new Path2D(el.getAttribute('d')));
} else if (el.tagName === 'circle') {
const cx = parseFloat(el.getAttribute('cx'));
const cy = parseFloat(el.getAttribute('cy'));
const r = parseFloat(el.getAttribute('r'));
path.arc(cx, cy, r, 0, Math.PI * 2);
} else if (el.tagName === 'polyline' || el.tagName === 'polygon') {
const points = el.getAttribute('points').trim().split(/[\s,]+/).map(Number);
for (let i = 0; i < points.length; i += 2) {
if (i === 0) path.moveTo(points[i], points[i + 1]);
else path.lineTo(points[i], points[i + 1]);
}
if (el.tagName === 'polygon') path.closePath();
} else if (el.tagName === 'line') {
path.moveTo(parseFloat(el.getAttribute('x1')), parseFloat(el.getAttribute('y1')));
path.lineTo(parseFloat(el.getAttribute('x2')), parseFloat(el.getAttribute('y2')));
}
ctx.stroke(path);
});
ctx.restore();
}
function drawEmoji(ctx, size) {
const emoji = document.getElementById('emoji').value || '🚀';
ctx.save();
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.font = `${size * 0.6}px "Apple Color Emoji", "Segoe UI Emoji", sans-serif`;
ctx.fillText(emoji, size / 2, size / 2 + size * 0.03);
ctx.restore();
}
function generateNoise(size, intensity) {
const noiseCanvas = document.createElement('canvas');
noiseCanvas.width = size;
noiseCanvas.height = size;
const noiseCtx = noiseCanvas.getContext('2d');
const imageData = noiseCtx.createImageData(size, size);
const data = imageData.data;
for (let i = 0; i < data.length; i += 4) {
const noise = (Math.random() - 0.5) * 255 * intensity;
data[i] = 128 + noise;
data[i + 1] = 128 + noise;
data[i + 2] = 128 + noise;
data[i + 3] = 30 * intensity;
}
noiseCtx.putImageData(imageData, 0, 0);
return noiseCanvas;
}
function blendColors(color1, color2, ratio) {
const hex = (c) => parseInt(c.slice(1), 16);
const r1 = (hex(color1) >> 16) & 255;
const g1 = (hex(color1) >> 8) & 255;
const b1 = hex(color1) & 255;
const r2 = (hex(color2) >> 16) & 255;
const g2 = (hex(color2) >> 8) & 255;
const b2 = hex(color2) & 255;
const r = Math.round(r1 + (r2 - r1) * ratio);
const g = Math.round(g1 + (g2 - g1) * ratio);
const b = Math.round(b1 + (b2 - b1) * ratio);
return `#${((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1)}`;
}
// =====================================================
// DOWNLOAD FUNCTIONS (keeping original)
// =====================================================
function downloadSize(size) {
const canvas = document.createElement('canvas');
canvas.width = size;
canvas.height = size;
generateFavicon(canvas, size);
canvas.toBlob(blob => {
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = size === 180 ? 'apple-touch-icon.png' : `favicon-${size}x${size}.png`;
a.click();
URL.revokeObjectURL(url);
}, 'image/png');
}
function downloadSVG() {
const letter = document.getElementById('letter').value || 'A';
const bgColor = document.getElementById('bgColor').value;
const bgColor2 = document.getElementById('bgColor2').value;
const fgColor = document.getElementById('fgColor').value;
const useGradient = document.getElementById('useGradient').checked;
const cornerRadius = parseInt(document.getElementById('cornerRadius').value);
const rx = (cornerRadius / 100) * 16;
const fontFamily = document.getElementById('fontFamily').value;
let bgFill = bgColor;
let defs = '';
if (useGradient) {
defs = `
<defs>
<linearGradient id="bg" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="${bgColor}"/>
<stop offset="100%" stop-color="${bgColor2}"/>
</linearGradient>
</defs>`;
bgFill = 'url(#bg)';
}
const svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">${defs}
<rect width="32" height="32" rx="${rx}" fill="${bgFill}"/>
<text x="16" y="16" font-family="${fontFamily}, sans-serif" font-size="18" font-weight="bold" text-anchor="middle" dominant-baseline="central" fill="${fgColor}">${letter.toUpperCase()}</text>
</svg>`;
const blob = new Blob([svg], { type: 'image/svg+xml' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'favicon.svg';
a.click();
URL.revokeObjectURL(url);
}
function downloadAll() {
SIZES.forEach((size, index) => {
setTimeout(() => downloadSize(size), index * 150);
});
setTimeout(() => downloadSVG(), SIZES.length * 150);
}
// Initialize
init();
</script>
</body>
</html>
#!/usr/bin/env python3
"""
Pro-Grade Favicon Generator
Generate professional-quality favicons with advanced visual effects using Pillow.
Supports drop shadows, inner glows, highlights, noise, gradients, and more.
For Lucide icons, uses cairosvg to render actual SVG paths with bezier curves.
Usage:
python generate_favicon.py --letter A --bg "#6366f1" --output ./favicons/
python generate_favicon.py --letter T --bg "#ec4899" --bg2 "#f97316" --style vibrant
python generate_favicon.py --lucide package-plus --bg "#f97316" --bg2 "#ef4444" --output ./public/
"""
import argparse
import colorsys
import math
import os
import struct
import zlib
from io import BytesIO
from pathlib import Path
from typing import Literal
try:
from PIL import Image, ImageDraw, ImageFilter, ImageFont
except ImportError:
print("Error: Pillow is required. Install with: pip install Pillow")
exit(1)
# Try to import cairosvg for Lucide icon rendering
try:
import cairosvg
HAS_CAIROSVG = True
except ImportError:
HAS_CAIROSVG = False
# ============================================================================
# DESIGN TEMPLATES
# ============================================================================
TEMPLATES = {
"modern": {
"bg": "#6366f1",
"bg2": "#8b5cf6",
"fg": "#ffffff",
"gradient": True,
"shadow": 0.4,
"highlight": 0.25,
"inner_glow": 0.0,
"noise": 0.0,
"corner_radius": 0.22,
},
"vibrant": {
"bg": "#ec4899",
"bg2": "#f97316",
"fg": "#ffffff",
"gradient": True,
"shadow": 0.5,
"highlight": 0.3,
"inner_glow": 0.2,
"noise": 0.0,
"corner_radius": 0.22,
},
"minimal": {
"bg": "#18181b",
"bg2": "#27272a",
"fg": "#fafafa",
"gradient": False,
"shadow": 0.3,
"highlight": 0.0,
"inner_glow": 0.0,
"noise": 0.05,
"corner_radius": 0.18,
},
"glass": {
"bg": "#3b82f6",
"bg2": "#06b6d4",
"fg": "#ffffff",
"gradient": True,
"shadow": 0.35,
"highlight": 0.5,
"inner_glow": 0.4,
"noise": 0.03,
"corner_radius": 0.24,
},
"neon": {
"bg": "#0f172a",
"bg2": "#1e293b",
"fg": "#22d3ee",
"gradient": True,
"shadow": 0.6,
"highlight": 0.0,
"inner_glow": 0.6,
"noise": 0.04,
"corner_radius": 0.20,
},
"warm": {
"bg": "#f59e0b",
"bg2": "#ef4444",
"fg": "#ffffff",
"gradient": True,
"shadow": 0.45,
"highlight": 0.35,
"inner_glow": 0.0,
"noise": 0.0,
"corner_radius": 0.22,
},
"forest": {
"bg": "#22c55e",
"bg2": "#14b8a6",
"fg": "#ffffff",
"gradient": True,
"shadow": 0.4,
"highlight": 0.25,
"inner_glow": 0.0,
"noise": 0.06,
"corner_radius": 0.22,
},
"mono": {
"bg": "#ffffff",
"bg2": "#f4f4f5",
"fg": "#18181b",
"gradient": False,
"shadow": 0.25,
"highlight": 0.0,
"inner_glow": 0.0,
"noise": 0.0,
"corner_radius": 0.18,
},
}
# Standard favicon sizes
SIZES = [16, 32, 48, 64, 128, 180, 192, 512]
# ============================================================================
# LUCIDE ICON SVG PATHS
# Extract from node_modules/lucide-react/dist/esm/icons/[icon-name].js
# ============================================================================
LUCIDE_ICONS = {
"package-plus": [
'<path d="M16 16h6"/>',
'<path d="M19 13v6"/>',
'<path d="M21 10V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l2-1.14"/>',
'<path d="m7.5 4.27 9 5.15"/>',
'<polyline points="3.29 7 12 12 20.71 7"/>',
'<line x1="12" x2="12" y1="22" y2="12"/>',
],
"rocket": [
'<path d="M4.5 16.5c-1.5 1.26-2 5-2 5s3.74-.5 5-2c.71-.84.7-2.13-.09-2.91a2.18 2.18 0 0 0-2.91-.09z"/>',
'<path d="m12 15-3-3a22 22 0 0 1 2-3.95A12.88 12.88 0 0 1 22 2c0 2.72-.78 7.5-6 11a22.35 22.35 0 0 1-4 2z"/>',
'<path d="M9 12H4s.55-3.03 2-4c1.62-1.08 5 0 5 0"/>',
'<path d="M12 15v5s3.03-.55 4-2c1.08-1.62 0-5 0-5"/>',
],
"zap": [
'<path d="M4 14a1 1 0 0 1-.78-1.63l9.9-10.2a.5.5 0 0 1 .86.46l-1.92 6.02A1 1 0 0 0 13 10h7a1 1 0 0 1 .78 1.63l-9.9 10.2a.5.5 0 0 1-.86-.46l1.92-6.02A1 1 0 0 0 11 14z"/>',
],
"star": [
'<path d="M11.525 2.295a.53.53 0 0 1 .95 0l2.31 4.679a2.123 2.123 0 0 0 1.595 1.16l5.166.756a.53.53 0 0 1 .294.904l-3.736 3.638a2.123 2.123 0 0 0-.611 1.878l.882 5.14a.53.53 0 0 1-.771.56l-4.618-2.428a2.122 2.122 0 0 0-1.973 0L6.396 21.01a.53.53 0 0 1-.77-.56l.881-5.139a2.122 2.122 0 0 0-.611-1.879L2.16 9.795a.53.53 0 0 1 .294-.906l5.165-.755a2.122 2.122 0 0 0 1.597-1.16z"/>',
],
"terminal": [
'<polyline points="4 17 10 11 4 5"/>',
'<line x1="12" x2="20" y1="19" y2="19"/>',
],
"code": [
'<polyline points="16 18 22 12 16 6"/>',
'<polyline points="8 6 2 12 8 18"/>',
],
"sparkles": [
'<path d="M9.937 15.5A2 2 0 0 0 8.5 14.063l-6.135-1.582a.5.5 0 0 1 0-.962L8.5 9.936A2 2 0 0 0 9.937 8.5l1.582-6.135a.5.5 0 0 1 .963 0L14.063 8.5A2 2 0 0 0 15.5 9.937l6.135 1.581a.5.5 0 0 1 0 .964L15.5 14.063a2 2 0 0 0-1.437 1.437l-1.582 6.135a.5.5 0 0 1-.963 0z"/>',
'<path d="M20 3v4"/>',
'<path d="M22 5h-4"/>',
'<path d="M4 17v2"/>',
'<path d="M5 18H3"/>',
],
"heart": [
'<path d="M19 14c1.49-1.46 3-3.21 3-5.5A5.5 5.5 0 0 0 16.5 3c-1.76 0-3 .5-4.5 2-1.5-1.5-2.74-2-4.5-2A5.5 5.5 0 0 0 2 8.5c0 2.3 1.5 4.05 3 5.5l7 7Z"/>',
],
"shield": [
'<path d="M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z"/>',
],
"flame": [
'<path d="M8.5 14.5A2.5 2.5 0 0 0 11 12c0-1.38-.5-2-1-3-1.072-2.143-.224-4.054 2-6 .5 2.5 2 4.9 4 6.5 2 1.6 3 3.5 3 5.5a7 7 0 1 1-14 0c0-1.153.433-2.294 1-3a2.5 2.5 0 0 0 2.5 2.5z"/>',
],
"globe": [
'<circle cx="12" cy="12" r="10"/>',
'<path d="M12 2a14.5 14.5 0 0 0 0 20 14.5 14.5 0 0 0 0-20"/>',
'<path d="M2 12h20"/>',
],
"send": [
'<path d="M14.536 21.686a.5.5 0 0 0 .937-.024l6.5-19a.496.496 0 0 0-.635-.635l-19 6.5a.5.5 0 0 0-.024.937l7.93 3.18a2 2 0 0 1 1.112 1.11z"/>',
'<path d="m21.854 2.147-10.94 10.939"/>',
],
"box": [
'<path d="M21 8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16Z"/>',
'<path d="m3.3 7 8.7 5 8.7-5"/>',
'<path d="M12 22V12"/>',
],
}
def render_lucide_icon(
icon_name: str,
size: int,
bg_color: str = "#6366f1",
bg_color2: str | None = None,
fg_color: str = "#ffffff",
corner_radius: float = 0.22,
) -> Image.Image | None:
"""
Render a Lucide icon using cairosvg.
Args:
icon_name: Name of the Lucide icon (e.g., 'package-plus', 'rocket')
size: Output size in pixels
bg_color: Background color (hex)
bg_color2: Gradient end color (hex), None for solid
fg_color: Icon stroke color (hex)
corner_radius: Corner radius as fraction (0-0.5)
Returns:
PIL Image with the rendered icon, or None if cairosvg not available
"""
if not HAS_CAIROSVG:
print(f"Warning: cairosvg not available. Install with: pip install cairosvg")
print(f" Also need native cairo: brew install cairo (macOS)")
return None
if icon_name not in LUCIDE_ICONS:
print(f"Warning: Unknown icon '{icon_name}'. Available: {', '.join(LUCIDE_ICONS.keys())}")
return None
# Build SVG with icon paths
icon_paths = "\n ".join(LUCIDE_ICONS[icon_name])
# Calculate scaling: Lucide uses 24x24 viewBox
padding_ratio = 0.15
icon_area = size * (1 - 2 * padding_ratio)
scale = icon_area / 24
offset = size * padding_ratio
radius = int(size * corner_radius)
# Build gradient or solid fill
if bg_color2 and bg_color2 != bg_color:
gradient_def = f'''<defs>
<linearGradient id="bg" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="{bg_color}"/>
<stop offset="100%" stop-color="{bg_color2}"/>
</linearGradient>
</defs>'''
fill = "url(#bg)"
else:
gradient_def = ""
fill = bg_color
svg_content = f'''<svg xmlns="http://www.w3.org/2000/svg" width="{size}" height="{size}" viewBox="0 0 {size} {size}">
{gradient_def}
<rect width="{size}" height="{size}" rx="{radius}" fill="{fill}"/>
<g transform="translate({offset}, {offset}) scale({scale})"
stroke="{fg_color}" stroke-width="2" fill="none"
stroke-linecap="round" stroke-linejoin="round">
{icon_paths}
</g>
</svg>'''
# Render SVG to PNG
png_data = cairosvg.svg2png(bytestring=svg_content.encode('utf-8'))
return Image.open(BytesIO(png_data)).convert('RGBA')
# ============================================================================
# COLOR UTILITIES
# ============================================================================
def hex_to_rgb(hex_color: str) -> tuple[int, int, int]:
"""Convert hex color to RGB tuple."""
hex_color = hex_color.lstrip("#")
return tuple(int(hex_color[i : i + 2], 16) for i in (0, 2, 4))
def rgb_to_hex(rgb: tuple[int, int, int]) -> str:
"""Convert RGB tuple to hex string."""
return "#{:02x}{:02x}{:02x}".format(*rgb)
def blend_colors(
color1: tuple[int, int, int], color2: tuple[int, int, int], ratio: float
) -> tuple[int, int, int]:
"""Blend two colors by ratio (0.0 = color1, 1.0 = color2)."""
return tuple(int(c1 + (c2 - c1) * ratio) for c1, c2 in zip(color1, color2))
def adjust_brightness(
color: tuple[int, int, int], factor: float
) -> tuple[int, int, int]:
"""Adjust color brightness (factor > 1 = lighter, < 1 = darker)."""
h, l, s = colorsys.rgb_to_hls(color[0] / 255, color[1] / 255, color[2] / 255)
l = max(0, min(1, l * factor))
r, g, b = colorsys.hls_to_rgb(h, l, s)
return (int(r * 255), int(g * 255), int(b * 255))
# ============================================================================
# DRAWING UTILITIES
# ============================================================================
def create_rounded_mask(size: int, radius: float) -> Image.Image:
"""
Create an anti-aliased rounded rectangle mask.
Args:
size: Image size in pixels
radius: Corner radius as fraction of size (0-0.5)
Returns:
Grayscale mask image
"""
# Create at 4x resolution for anti-aliasing
scale = 4
large_size = size * scale
large_radius = int(radius * size * scale)
mask = Image.new("L", (large_size, large_size), 0)
draw = ImageDraw.Draw(mask)
draw.rounded_rectangle(
[(0, 0), (large_size - 1, large_size - 1)],
radius=large_radius,
fill=255,
)
# Downscale with anti-aliasing
return mask.resize((size, size), Image.Resampling.LANCZOS)
def create_gradient(
size: int,
color1: tuple[int, int, int],
color2: tuple[int, int, int],
direction: Literal["diagonal", "vertical", "horizontal"] = "diagonal",
) -> Image.Image:
"""
Create a smooth gradient image.
Args:
size: Image size in pixels
color1: Starting color (RGB)
color2: Ending color (RGB)
direction: Gradient direction
Returns:
RGBA gradient image
"""
img = Image.new("RGBA", (size, size))
pixels = img.load()
for y in range(size):
for x in range(size):
if direction == "diagonal":
ratio = (x + y) / (2 * size - 2)
elif direction == "vertical":
ratio = y / (size - 1)
else: # horizontal
ratio = x / (size - 1)
color = blend_colors(color1, color2, ratio)
pixels[x, y] = (*color, 255)
return img
def add_noise(img: Image.Image, intensity: float) -> Image.Image:
"""
Add subtle noise/grain texture to an image.
Args:
img: Source image
intensity: Noise intensity (0-1)
Returns:
Image with noise applied
"""
import random
if intensity <= 0:
return img
width, height = img.size
noise_img = Image.new("RGBA", (width, height))
pixels = noise_img.load()
for y in range(height):
for x in range(width):
noise = int((random.random() - 0.5) * 255 * intensity)
gray = 128 + noise
alpha = int(30 * intensity)
pixels[x, y] = (gray, gray, gray, alpha)
return Image.alpha_composite(img, noise_img)
# ============================================================================
# EFFECT LAYERS
# ============================================================================
def apply_drop_shadow(
img: Image.Image, intensity: float, offset: float = 0.05, blur: float = 0.15
) -> Image.Image:
"""
Apply drop shadow effect to the image.
Args:
img: Source image with transparency
intensity: Shadow opacity (0-1)
offset: Shadow offset as fraction of size
blur: Shadow blur as fraction of size
Returns:
Image with drop shadow
"""
if intensity <= 0:
return img
size = img.size[0]
shadow_offset = int(size * offset * intensity)
shadow_blur = int(size * blur * intensity)
# Create shadow from alpha channel
shadow = Image.new("RGBA", img.size, (0, 0, 0, 0))
shadow_alpha = img.split()[3]
# Offset and blur the shadow
shadow_layer = Image.new("RGBA", img.size, (0, 0, 0, 0))
shadow_color = Image.new("RGBA", img.size, (0, 0, 0, int(255 * 0.3 * intensity)))
shadow_layer.paste(shadow_color, (0, shadow_offset), shadow_alpha)
if shadow_blur > 0:
shadow_layer = shadow_layer.filter(ImageFilter.GaussianBlur(shadow_blur))
# Composite: shadow behind original
result = Image.alpha_composite(shadow_layer, img)
return result
def apply_highlight(img: Image.Image, intensity: float) -> Image.Image:
"""
Apply top highlight gradient effect.
Args:
img: Source image
intensity: Highlight opacity (0-1)
Returns:
Image with highlight overlay
"""
if intensity <= 0:
return img
size = img.size[0]
# Create highlight gradient (bright at top, dark at bottom)
highlight = Image.new("RGBA", (size, size), (0, 0, 0, 0))
pixels = highlight.load()
for y in range(size):
for x in range(size):
ratio = y / (size - 1)
if ratio < 0.5:
# Top half: white highlight
alpha = int((1 - ratio * 2) * 80 * intensity)
pixels[x, y] = (255, 255, 255, alpha)
else:
# Bottom half: slight darken
alpha = int((ratio - 0.5) * 2 * 40 * intensity)
pixels[x, y] = (0, 0, 0, alpha)
return Image.alpha_composite(img, highlight)
def apply_inner_glow(img: Image.Image, intensity: float, mask: Image.Image) -> Image.Image:
"""
Apply inner glow/ambient occlusion effect.
Args:
img: Source image
intensity: Glow intensity (0-1)
mask: Rounded rectangle mask
Returns:
Image with inner glow
"""
if intensity <= 0:
return img
size = img.size[0]
# Create radial gradient (bright center, dark edges)
glow = Image.new("RGBA", (size, size), (0, 0, 0, 0))
pixels = glow.load()
center_x, center_y = size // 2, int(size * 0.4) # Slightly above center
for y in range(size):
for x in range(size):
# Distance from center (normalized)
dx = (x - center_x) / (size * 0.5)
dy = (y - center_y) / (size * 0.5)
distance = math.sqrt(dx * dx + dy * dy)
if distance < 1.0:
# Inner bright area
alpha = int((1 - distance) * 60 * intensity)
pixels[x, y] = (255, 255, 255, alpha)
else:
# Outer dark area
alpha = int(min(1, distance - 1) * 40 * intensity)
pixels[x, y] = (0, 0, 0, alpha)
# Apply mask
glow.putalpha(
Image.composite(glow.split()[3], Image.new("L", (size, size), 0), mask)
)
return Image.alpha_composite(img, glow)
# ============================================================================
# CONTENT RENDERING
# ============================================================================
def get_system_font(size: int, bold: bool = True) -> ImageFont.FreeTypeFont | None:
"""
Get a system font for text rendering.
Args:
size: Font size in pixels
bold: Whether to use bold weight
Returns:
Font object or None if not found
"""
# Common font paths by OS
font_paths = [
# macOS
"/System/Library/Fonts/SFNSDisplay.ttf",
"/System/Library/Fonts/Helvetica.ttc",
"/Library/Fonts/Arial Bold.ttf" if bold else "/Library/Fonts/Arial.ttf",
# Linux
"/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf",
"/usr/share/fonts/truetype/liberation/LiberationSans-Bold.ttf",
# Windows
"C:/Windows/Fonts/arialbd.ttf" if bold else "C:/Windows/Fonts/arial.ttf",
"C:/Windows/Fonts/segoeui.ttf",
]
for path in font_paths:
if os.path.exists(path):
try:
return ImageFont.truetype(path, size)
except (IOError, OSError):
continue
# Fallback to default
return ImageFont.load_default()
def render_letter(
img: Image.Image,
letter: str,
color: tuple[int, int, int],
shadow_intensity: float = 0.3,
) -> Image.Image:
"""
Render a letter/monogram on the favicon.
Args:
img: Background image
letter: Letter(s) to render
color: Text color (RGB)
shadow_intensity: Text shadow intensity
Returns:
Image with letter rendered
"""
size = img.size[0]
result = img.copy()
draw = ImageDraw.Draw(result)
# Font size based on letter count
font_size = int(size * (0.4 if len(letter) > 1 else 0.55))
font = get_system_font(font_size)
# Get text bounding box for centering
bbox = draw.textbbox((0, 0), letter.upper(), font=font)
text_width = bbox[2] - bbox[0]
text_height = bbox[3] - bbox[1]
# Center position with optical adjustment
x = (size - text_width) // 2 - bbox[0]
y = (size - text_height) // 2 - bbox[1] - int(size * 0.02)
# Draw shadow first
if shadow_intensity > 0 and size >= 32:
shadow_offset = max(1, int(size * 0.02))
shadow_color = (0, 0, 0, int(255 * 0.3 * shadow_intensity))
# Create shadow layer
shadow_layer = Image.new("RGBA", (size, size), (0, 0, 0, 0))
shadow_draw = ImageDraw.Draw(shadow_layer)
shadow_draw.text(
(x, y + shadow_offset),
letter.upper(),
font=font,
fill=shadow_color,
)
# Blur shadow
shadow_layer = shadow_layer.filter(ImageFilter.GaussianBlur(size * 0.02))
result = Image.alpha_composite(result, shadow_layer)
draw = ImageDraw.Draw(result)
# Draw main text
draw.text((x, y), letter.upper(), font=font, fill=(*color, 255))
return result
# ============================================================================
# MAIN GENERATION
# ============================================================================
def generate_favicon(
size: int,
letter: str = "A",
bg_color: str = "#6366f1",
bg_color2: str | None = None,
fg_color: str = "#ffffff",
use_gradient: bool = True,
shadow_intensity: float = 0.4,
highlight_intensity: float = 0.25,
inner_glow_intensity: float = 0.0,
noise_intensity: float = 0.0,
corner_radius: float = 0.22,
) -> Image.Image:
"""
Generate a professional-quality favicon.
Args:
size: Output size in pixels
letter: Letter/monogram to display
bg_color: Background color (hex)
bg_color2: Gradient end color (hex), None for solid
fg_color: Foreground/text color (hex)
use_gradient: Whether to use gradient background
shadow_intensity: Drop shadow intensity (0-1)
highlight_intensity: Top highlight intensity (0-1)
inner_glow_intensity: Inner glow intensity (0-1)
noise_intensity: Noise/grain intensity (0-1)
corner_radius: Corner radius as fraction (0-0.5)
Returns:
Generated favicon as RGBA Image
"""
bg_rgb = hex_to_rgb(bg_color)
bg2_rgb = hex_to_rgb(bg_color2) if bg_color2 else bg_rgb
fg_rgb = hex_to_rgb(fg_color)
# Create base with transparency
img = Image.new("RGBA", (size, size), (0, 0, 0, 0))
# Create rounded mask
mask = create_rounded_mask(size, corner_radius)
# Create background (gradient or solid)
if use_gradient and bg_color2:
background = create_gradient(size, bg_rgb, bg2_rgb, "diagonal")
else:
background = Image.new("RGBA", (size, size), (*bg_rgb, 255))
# Apply mask to background
background.putalpha(mask)
# Apply effects in order
img = Image.alpha_composite(img, background)
# Inner glow
if inner_glow_intensity > 0 and size >= 32:
img = apply_inner_glow(img, inner_glow_intensity, mask)
# Highlight
if highlight_intensity > 0 and size >= 32:
highlight_layer = apply_highlight(
Image.new("RGBA", (size, size), (0, 0, 0, 0)), highlight_intensity
)
highlight_layer.putalpha(
Image.composite(highlight_layer.split()[3], Image.new("L", (size, size), 0), mask)
)
img = Image.alpha_composite(img, highlight_layer)
# Noise
if noise_intensity > 0 and size >= 64:
img = add_noise(img, noise_intensity)
# Render letter
img = render_letter(img, letter, fg_rgb, shadow_intensity)
# Drop shadow (applied last, affects whole icon)
if shadow_intensity > 0 and size >= 32:
# Create a version with shadow
shadow_canvas = Image.new("RGBA", (size, size), (0, 0, 0, 0))
img = apply_drop_shadow(img, shadow_intensity * 0.5)
return img
def generate_favicon_suite(
output_dir: str,
letter: str = "A",
style: str | None = None,
**kwargs,
) -> list[str]:
"""
Generate a complete favicon suite with all standard sizes.
Args:
output_dir: Directory to save files
letter: Letter/monogram to display
style: Template name to use (overrides other settings)
**kwargs: Override settings (bg_color, fg_color, etc.)
Returns:
List of generated file paths
"""
# Apply template if specified
settings = {}
if style and style in TEMPLATES:
template = TEMPLATES[style]
settings = {
"bg_color": template["bg"],
"bg_color2": template["bg2"],
"fg_color": template["fg"],
"use_gradient": template["gradient"],
"shadow_intensity": template["shadow"],
"highlight_intensity": template["highlight"],
"inner_glow_intensity": template["inner_glow"],
"noise_intensity": template["noise"],
"corner_radius": template["corner_radius"],
}
# Override with provided kwargs
settings.update({k: v for k, v in kwargs.items() if v is not None})
# Create output directory
Path(output_dir).mkdir(parents=True, exist_ok=True)
generated_files = []
# Generate each size
for size in SIZES:
favicon = generate_favicon(size, letter, **settings)
if size == 180:
filename = "apple-touch-icon.png"
else:
filename = f"favicon-{size}x{size}.png"
filepath = os.path.join(output_dir, filename)
favicon.save(filepath, "PNG")
generated_files.append(filepath)
print(f" ✓ Generated {filename}")
# Generate ICO file (16x16 + 32x32)
ico_path = os.path.join(output_dir, "favicon.ico")
create_ico_file(
ico_path,
[
generate_favicon(16, letter, **settings),
generate_favicon(32, letter, **settings),
],
)
generated_files.append(ico_path)
print(f" ✓ Generated favicon.ico")
# Generate SVG
svg_path = os.path.join(output_dir, "favicon.svg")
create_svg_file(svg_path, letter, settings)
generated_files.append(svg_path)
print(f" ✓ Generated favicon.svg")
return generated_files
def generate_lucide_favicon_suite(
output_dir: str,
icon_name: str,
style: str | None = None,
**kwargs,
) -> list[str]:
"""
Generate a complete favicon suite using a Lucide icon.
Args:
output_dir: Directory to save files
icon_name: Lucide icon name (e.g., 'package-plus', 'rocket')
style: Template name to use (overrides other settings)
**kwargs: Override settings (bg_color, fg_color, etc.)
Returns:
List of generated file paths
"""
# Apply template if specified
settings = {}
if style and style in TEMPLATES:
template = TEMPLATES[style]
settings = {
"bg_color": template["bg"],
"bg_color2": template["bg2"],
"fg_color": template["fg"],
"corner_radius": template["corner_radius"],
}
# Override with provided kwargs
settings.update({k: v for k, v in kwargs.items() if v is not None})
# Create output directory
Path(output_dir).mkdir(parents=True, exist_ok=True)
generated_files = []
# Generate each size
for size in SIZES:
favicon = render_lucide_icon(icon_name, size, **settings)
if favicon is None:
print(f" ✗ Failed to generate {size}x{size} (cairosvg error)")
continue
if size == 180:
filename = "apple-touch-icon.png"
else:
filename = f"favicon-{size}x{size}.png"
filepath = os.path.join(output_dir, filename)
favicon.save(filepath, "PNG")
generated_files.append(filepath)
print(f" ✓ Generated {filename}")
# Generate ICO file (16x16 + 32x32)
ico_path = os.path.join(output_dir, "favicon.ico")
ico_images = [
render_lucide_icon(icon_name, 16, **settings),
render_lucide_icon(icon_name, 32, **settings),
]
if all(ico_images):
create_ico_file(ico_path, ico_images)
generated_files.append(ico_path)
print(f" ✓ Generated favicon.ico")
# Generate SVG with actual Lucide paths
svg_path = os.path.join(output_dir, "favicon.svg")
create_lucide_svg_file(svg_path, icon_name, settings)
generated_files.append(svg_path)
print(f" ✓ Generated favicon.svg")
return generated_files
def create_lucide_svg_file(filepath: str, icon_name: str, settings: dict) -> None:
"""
Create an SVG favicon file using Lucide icon paths.
Args:
filepath: Output SVG file path
icon_name: Lucide icon name
settings: Style settings dict
"""
if icon_name not in LUCIDE_ICONS:
return
bg_color = settings.get("bg_color", "#6366f1")
bg_color2 = settings.get("bg_color2", bg_color)
fg_color = settings.get("fg_color", "#ffffff")
corner_radius = settings.get("corner_radius", 0.22)
rx = corner_radius * 32
icon_paths = "\n ".join(LUCIDE_ICONS[icon_name])
# Scale: 24x24 Lucide → 32x32 favicon with padding
# offset = 32 * 0.15 = 4.8, scale = (32 * 0.7) / 24 = 0.933
scale = 0.933
offset = 4.8
if bg_color2 and bg_color2 != bg_color:
gradient_def = f'''<defs>
<linearGradient id="bg" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="{bg_color}"/>
<stop offset="100%" stop-color="{bg_color2}"/>
</linearGradient>
</defs>'''
fill = "url(#bg)"
else:
gradient_def = ""
fill = bg_color
svg = f'''<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
{gradient_def}
<rect width="32" height="32" rx="{rx:.1f}" fill="{fill}"/>
<g transform="translate({offset}, {offset}) scale({scale})"
stroke="{fg_color}" stroke-width="2" fill="none"
stroke-linecap="round" stroke-linejoin="round">
{icon_paths}
</g>
</svg>'''
with open(filepath, "w") as f:
f.write(svg)
def create_ico_file(filepath: str, images: list[Image.Image]) -> None:
"""
Create an ICO file from multiple PNG images.
Args:
filepath: Output ICO file path
images: List of PIL Images (should include 16x16 and 32x32)
"""
# ICO format implementation
icon_dir = BytesIO()
# ICONDIR header
icon_dir.write(struct.pack("<HHH", 0, 1, len(images)))
image_data = []
offset = 6 + len(images) * 16 # Header + entries
for img in images:
# Convert to RGBA if needed
if img.mode != "RGBA":
img = img.convert("RGBA")
# Save as PNG
png_data = BytesIO()
img.save(png_data, "PNG")
png_bytes = png_data.getvalue()
image_data.append(png_bytes)
# ICONDIRENTRY
width = img.size[0] if img.size[0] < 256 else 0
height = img.size[1] if img.size[1] < 256 else 0
icon_dir.write(
struct.pack(
"<BBBBHHII",
width, # Width
height, # Height
0, # Color palette
0, # Reserved
1, # Color planes
32, # Bits per pixel
len(png_bytes), # Size of image data
offset, # Offset to image data
)
)
offset += len(png_bytes)
# Write image data
for data in image_data:
icon_dir.write(data)
with open(filepath, "wb") as f:
f.write(icon_dir.getvalue())
def create_svg_file(filepath: str, letter: str, settings: dict) -> None:
"""
Create an SVG favicon file.
Args:
filepath: Output SVG file path
letter: Letter to display
settings: Style settings dict
"""
bg_color = settings.get("bg_color", "#6366f1")
bg_color2 = settings.get("bg_color2", bg_color)
fg_color = settings.get("fg_color", "#ffffff")
use_gradient = settings.get("use_gradient", True)
corner_radius = settings.get("corner_radius", 0.22)
rx = corner_radius * 16 # Based on 32x32 viewBox
gradient_def = ""
fill = bg_color
if use_gradient and bg_color2 != bg_color:
gradient_def = f"""
<defs>
<linearGradient id="bg" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="{bg_color}"/>
<stop offset="100%" stop-color="{bg_color2}"/>
</linearGradient>
</defs>"""
fill = "url(#bg)"
svg = f"""<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">{gradient_def}
<rect width="32" height="32" rx="{rx:.1f}" fill="{fill}"/>
<text x="16" y="16" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif" font-size="18" font-weight="bold" text-anchor="middle" dominant-baseline="central" fill="{fg_color}">{letter.upper()}</text>
</svg>"""
with open(filepath, "w") as f:
f.write(svg)
# ============================================================================
# CLI
# ============================================================================
def main():
"""Command-line interface for favicon generation."""
parser = argparse.ArgumentParser(
description="Generate professional-quality favicons with advanced effects",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
%(prog)s --letter A --bg "#6366f1" --output ./favicons/
%(prog)s --letter T --style vibrant --output ./public/
%(prog)s --lucide package-plus --bg "#f97316" --bg2 "#ef4444" --output ./public/
%(prog)s --lucide rocket --style vibrant --output ./icons/
Available styles: modern, vibrant, minimal, glass, neon, warm, forest, mono
Available icons: """ + ", ".join(LUCIDE_ICONS.keys()) + """
""",
)
parser.add_argument(
"--letter", "-l", help="Letter or monogram to display"
)
parser.add_argument(
"--lucide", "-i",
choices=list(LUCIDE_ICONS.keys()),
help="Use a Lucide icon (requires cairosvg: pip install cairosvg)"
)
parser.add_argument(
"--bg", default="#6366f1", help="Background color in hex (default: #6366f1)"
)
parser.add_argument(
"--bg2", help="Gradient end color in hex (default: same as --bg)"
)
parser.add_argument(
"--fg", default="#ffffff", help="Foreground/text color (default: #ffffff)"
)
parser.add_argument(
"--style",
"-s",
choices=list(TEMPLATES.keys()),
help="Use a predefined style template",
)
parser.add_argument(
"--output", "-o", default="./favicons", help="Output directory (default: ./favicons)"
)
parser.add_argument(
"--no-gradient", action="store_true", help="Disable gradient background"
)
parser.add_argument(
"--shadow", type=float, default=0.4, help="Shadow intensity 0-1 (default: 0.4)"
)
parser.add_argument(
"--highlight", type=float, default=0.25, help="Highlight intensity 0-1 (default: 0.25)"
)
parser.add_argument(
"--glow", type=float, default=0.0, help="Inner glow intensity 0-1 (default: 0)"
)
parser.add_argument(
"--noise", type=float, default=0.0, help="Noise intensity 0-1 (default: 0)"
)
parser.add_argument(
"--radius", type=float, default=0.22, help="Corner radius 0-0.5 (default: 0.22)"
)
args = parser.parse_args()
# Validate: must have either --letter or --lucide
if not args.letter and not args.lucide:
args.letter = "A" # Default
print(f"\n🎨 Pro Favicon Generator")
print(f"{'=' * 40}")
if args.lucide:
print(f"Lucide Icon: {args.lucide}")
if not HAS_CAIROSVG:
print("\n⚠️ Warning: cairosvg not installed!")
print(" Install with: pip install cairosvg")
print(" Also need native cairo: brew install cairo (macOS)")
print(" Falling back to letter mode...\n")
args.letter = args.lucide[0].upper()
args.lucide = None
else:
print(f"Letter: {args.letter.upper()}")
if args.style:
print(f"Style: {args.style}")
else:
print(f"Background: {args.bg}" + (f" → {args.bg2}" if args.bg2 else ""))
print(f"Foreground: {args.fg}")
print(f"Output: {args.output}")
print(f"{'=' * 40}\n")
if args.lucide and HAS_CAIROSVG:
# Generate using Lucide icon with cairosvg
files = generate_lucide_favicon_suite(
output_dir=args.output,
icon_name=args.lucide,
style=args.style,
bg_color=args.bg if not args.style else None,
bg_color2=args.bg2 if not args.style else None,
fg_color=args.fg if not args.style else None,
corner_radius=args.radius if not args.style else None,
)
else:
# Generate using letter
files = generate_favicon_suite(
output_dir=args.output,
letter=args.letter,
style=args.style,
bg_color=args.bg if not args.style else None,
bg_color2=args.bg2 if not args.style else None,
fg_color=args.fg if not args.style else None,
use_gradient=not args.no_gradient if not args.style else None,
shadow_intensity=args.shadow if not args.style else None,
highlight_intensity=args.highlight if not args.style else None,
inner_glow_intensity=args.glow if not args.style else None,
noise_intensity=args.noise if not args.style else None,
corner_radius=args.radius if not args.style else None,
)
print(f"\n✅ Generated {len(files)} files in {args.output}/")
print("\nNext steps:")
print(" 1. Copy files to your project's public/ directory")
print(" 2. Add favicon links to your HTML <head>")
print(" 3. Update manifest.json if using PWA")
if __name__ == "__main__":
main()