
Svg Art
- 417 installs
- 14 repo stars
- Updated July 30, 2026
- kv0906/cc-skills
svg-art is a Claude Code skill that generates clean, scalable SVG illustrations and icons for sites, docs, and UI chrome—optimized paths, viewBox, and theme-aware palettes.
About
svg-art is a kv0906/cc-skills agent skill that creates vector graphics through eight executable Python scripts outputting valid SVG to stdout or files via -o. Scripts cover generate_grid.py for grid patterns, generate_radial.py for spirals and sunbursts, generate_fractal.py for tree, Koch, and Sierpinski fractals, generate_wave.py for audio visualizations, generate_particles.py for constellations, generate_chart.py for bar, line, pie, and donut charts, generate_icon.py with 40+ UI icons, and optimize_svg.py for minification. Common CLI flags include --fill, --stroke, --stroke-width, --seed for reproducibility, and -o for file output with default fill #3B82F6. Developers reach for svg-art when they need parameterized icons, diagrams, or generative art for documentation and UI without manual Figma or Illustrator work.
- Hand-authored SVG output
- Icon and illustration patterns
- Optimized paths and viewBox
- Theme-friendly color tokens
- Embeddable React/HTML usage
Svg Art by the numbers
- 417 all-time installs (skills.sh)
- +13 installs in the week ending Aug 2, 2026 (Skillselion tracking)
- Ranked #431 of 1,335 Generative Media skills by installs in the Skillselion catalog
- Data as of Aug 2, 2026 (Skillselion catalog sync)
npx skills add https://github.com/kv0906/cc-skills --skill svg-artAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 417 |
|---|---|
| repo stars | ★ 14 |
| Last updated | July 30, 2026 |
| Repository | kv0906/cc-skills ↗ |
How do you generate SVG icons and charts programmatically?
Generate clean, scalable SVG illustrations and icons for sites, docs, and UI chrome—optimized paths, viewBox, and theme-aware palettes.
Who is it for?
Frontend developers and technical writers who need reproducible SVG icons, charts, fractals, or diagrams from CLI Python scripts without a design tool.
Skip if: Designers needing raster photo editing, complex manual Bézier illustration, or brand systems that require proprietary Figma component libraries only.
When should I use this skill?
A developer asks to create SVG icons, logos, data charts, fractal art, grid patterns, or optimize existing SVG path output for web or docs.
What you get
Valid SVG files or stdout output for icons, charts, fractals, patterns, and minified vector assets with reproducible --seed parameters.
- SVG icon files
- chart diagrams
- optimized vector assets
By the numbers
- Bundles 8 Python SVG generation scripts
- generate_icon.py includes 40+ UI icons
Files
SVG Art: Programmatic Generation
Generate high-quality SVG graphics using Python scripts. All scripts output valid SVG to stdout (or file with -o).
Available Scripts
| Script | Purpose | Key Options |
|---|---|---|
generate_grid.py | Grid patterns | --cols, --rows, --shape, --vary-* |
generate_radial.py | Radial/spiral/sunburst | --spiral, --concentric, --sunburst |
generate_fractal.py | Fractals (tree, koch, sierpinski) | --tree, --koch, --sierpinski, --depth |
generate_wave.py | Waves and audio viz | --layers, --noise, --bars |
generate_particles.py | Scatter/cluster/constellation | --cluster, --gradient, --constellation |
generate_chart.py | Data visualization | --bar, --line, --pie, --donut |
generate_icon.py | Common UI icons | --icon NAME, --list, --filled |
optimize_svg.py | Minify/optimize SVG | --aggressive, --stats |
Quick Examples
# Grid with size variation
python scripts/generate_grid.py -c 6 -r 6 --vary-size --vary-opacity -o grid.svg
# Spiral pattern
python scripts/generate_radial.py --spiral -n 60 --turns 4 -o spiral.svg
# Fractal tree
python scripts/generate_fractal.py --tree --depth 8 --vary-angle -o tree.svg
# Layered waves with fill
python scripts/generate_wave.py --layers 5 --fill -o waves.svg
# Constellation network
python scripts/generate_particles.py --constellation -n 30 --connect-distance 25 -o network.svg
# Bar chart
python scripts/generate_chart.py --bar --data "30,50,80,45,90" --labels "A,B,C,D,E" -o chart.svg
# Heart icon
python scripts/generate_icon.py --icon heart --filled --stroke "#E11D48" -o heart.svg
# Optimize existing SVG
python scripts/optimize_svg.py input.svg --aggressive -o output.svgScript Usage Patterns
Grid Patterns
python scripts/generate_grid.py \
-c 8 -r 8 # columns and rows
-s 10 -g 2 # size and gap
--shape circle # rect, circle, or diamond
--vary-size # random size variation
--vary-opacity # random opacity
--vary-hue # color variation
--seed 42 # reproducible randomnessRadial Patterns
# Concentric rings
python scripts/generate_radial.py --concentric --rings 5 --vary-hue
# Sunburst rays
python scripts/generate_radial.py --sunburst -n 24 --vary-lengthFractals
# Koch snowflake
python scripts/generate_fractal.py --koch --depth 4 --fill "#3B82F6"
# Sierpinski triangle
python scripts/generate_fractal.py --sierpinski --depth 5Charts
# Line chart with points
python scripts/generate_chart.py --line --data "10,30,20,50" --show-points --smooth
# Donut chart
python scripts/generate_chart.py --donut --data "40,30,20,10" --labels "A,B,C,D"Icons
# List all available icons
python scripts/generate_icon.py --list
# Common icons: check, x, plus, menu, search, home, user, settings,
# mail, heart, star, play, file, download, edit, share, sun, moon, etc.Common Options (All Scripts)
--fill COLOR: Fill color (default: #3B82F6)--stroke COLOR: Stroke color--stroke-width N: Stroke width--seed N: Random seed for reproducibility-o FILE: Output to file instead of stdout
Piping and Composition
Scripts can be piped together:
# Generate and optimize
python scripts/generate_grid.py -c 10 -r 10 | python scripts/optimize_svg.py --aggressive
# Check optimization stats
python scripts/generate_fractal.py --tree --depth 10 | python scripts/optimize_svg.py --statsSVG Fundamentals Reference
See references/svg-fundamentals.md for:
- Core SVG structure and viewBox
- Element types (rect, circle, path, etc.)
- Path command syntax
- Gradients and patterns
- Accessibility requirements
SVG Fundamentals Reference
Quick reference for SVG structure, elements, and best practices.
Core Structure
<svg xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 100 100"
width="100" height="100"
role="img" aria-labelledby="title desc">
<title id="title">Descriptive title</title>
<desc id="desc">Detailed description</desc>
<!-- Content -->
</svg>Critical: Always use viewBox for proper scaling. Coordinate system: origin top-left, +x right, +y down.
Elements
| Element | Attributes | Example |
|---|---|---|
<rect> | x, y, width, height, rx, ry | <rect x="10" y="10" width="80" height="40" rx="5"/> |
<circle> | cx, cy, r | <circle cx="50" cy="50" r="25"/> |
<ellipse> | cx, cy, rx, ry | <ellipse cx="50" cy="50" rx="40" ry="20"/> |
<line> | x1, y1, x2, y2 | <line x1="0" y1="0" x2="100" y2="100"/> |
<polyline> | points | <polyline points="10,10 50,50 90,10"/> |
<polygon> | points | <polygon points="50,10 90,90 10,90"/> |
<path> | d | <path d="M10 10 L90 90"/> |
<text> | x, y, font-* | <text x="50" y="50">Hello</text> |
<g> | transform, id | <g transform="translate(10,10)">...</g> |
Path Commands
| Cmd | Name | Parameters | Example |
|---|---|---|---|
| M/m | Move | x y | M10 10 |
| L/l | Line | x y | L90 90 |
| H/h | Horizontal | x | H90 |
| V/v | Vertical | y | V90 |
| C/c | Cubic Bézier | x1 y1 x2 y2 x y | C20 20 80 20 90 90 |
| S/s | Smooth cubic | x2 y2 x y | S80 80 90 90 |
| Q/q | Quadratic | x1 y1 x y | Q50 0 90 90 |
| T/t | Smooth quad | x y | T90 90 |
| A/a | Arc | rx ry rot large sweep x y | A25 25 0 0 1 90 90 |
| Z/z | Close | — | Z |
Uppercase = absolute, lowercase = relative
Styling
<!-- Inline -->
<rect fill="#3B82F6" stroke="#1E40AF" stroke-width="2" opacity="0.8"/>
<!-- CSS -->
<style>
.primary { fill: #3B82F6; stroke: none; }
</style>
<rect class="primary"/>Colors: HEX (#RGB, #RRGGBB), HSL (hsl(210, 70%, 50%)), currentColor
Transforms
<g transform="translate(50, 50) rotate(45) scale(1.5)">
<!-- transforms apply right-to-left -->
</g>| Transform | Syntax |
|---|---|
| Translate | translate(x, y) |
| Rotate | rotate(angle) or rotate(angle, cx, cy) |
| Scale | scale(s) or scale(sx, sy) |
| Skew | skewX(angle), skewY(angle) |
Gradients
<defs>
<linearGradient id="grad" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" stop-color="#3B82F6"/>
<stop offset="100%" stop-color="#8B5CF6"/>
</linearGradient>
<radialGradient id="radial" cx="50%" cy="50%" r="50%">
<stop offset="0%" stop-color="#FFF"/>
<stop offset="100%" stop-color="#3B82F6"/>
</radialGradient>
</defs>
<rect fill="url(#grad)"/>Reusable Elements
<defs>
<symbol id="icon" viewBox="0 0 24 24">
<path d="M12 2l3 6 6 1-4 4 1 6-6-3-6 3 1-6-4-4 6-1z"/>
</symbol>
</defs>
<use href="#icon" x="10" y="10" width="24" height="24"/>
<use href="#icon" x="50" y="10" width="24" height="24"/>Clipping & Masking
<defs>
<clipPath id="clip">
<circle cx="50" cy="50" r="40"/>
</clipPath>
</defs>
<rect clip-path="url(#clip)" width="100" height="100" fill="#3B82F6"/>Accessibility
<!-- Informative SVG -->
<svg role="img" aria-labelledby="title desc">
<title id="title">Chart Title</title>
<desc id="desc">Description of the graphic</desc>
</svg>
<!-- Decorative SVG -->
<svg aria-hidden="true">...</svg>Optimization Checklist
1. ✓ Use viewBox for scalability 2. ✓ Round coordinates to reduce precision 3. ✓ Use relative path commands when shorter 4. ✓ Remove default attributes (x="0", opacity="1") 5. ✓ Shorten hex colors (#RRGGBB → #RGB when possible) 6. ✓ Use <use> for repeated elements 7. ✓ Remove unnecessary IDs and comments 8. ✓ Combine paths with same styling
#!/usr/bin/env python3
"""Generate SVG charts and data visualizations.
Examples:
# Bar chart
python generate_chart.py --bar --data "30,50,80,45,90" --labels "A,B,C,D,E"
# Line chart
python generate_chart.py --line --data "20,45,30,60,50,80" -o line.svg
# Pie chart
python generate_chart.py --pie --data "30,25,20,15,10" --labels "A,B,C,D,E"
# Donut chart
python generate_chart.py --donut --data "40,30,20,10"
# Area chart
python generate_chart.py --area --data "10,30,20,50,40,60"
"""
import argparse
import math
import sys
def parse_data(data_str: str) -> list:
"""Parse comma-separated data string into list of floats."""
return [float(x.strip()) for x in data_str.split(",")]
def parse_labels(labels_str: str) -> list:
"""Parse comma-separated labels string."""
if not labels_str:
return []
return [x.strip() for x in labels_str.split(",")]
def get_color(index: int, base_hue: int, total: int) -> str:
"""Generate color for data point."""
hue = (base_hue + (index * 360 / total)) % 360
return f"hsl({hue:.0f}, 70%, 55%)"
def generate_bar_chart(
data: list,
labels: list,
width: float,
height: float,
base_hue: int,
show_values: bool,
horizontal: bool,
) -> list:
"""Generate bar chart."""
elements = []
# Chart area with margins
margin = {"top": 20, "right": 20, "bottom": 40, "left": 50}
chart_width = width - margin["left"] - margin["right"]
chart_height = height - margin["top"] - margin["bottom"]
max_val = max(data)
n = len(data)
# Bar dimensions
gap_ratio = 0.2
if horizontal:
bar_height = chart_height / n * (1 - gap_ratio)
gap = chart_height / n * gap_ratio
else:
bar_width = chart_width / n * (1 - gap_ratio)
gap = chart_width / n * gap_ratio
# Axes
elements.append(f'<line x1="{margin["left"]}" y1="{margin["top"]}" '
f'x2="{margin["left"]}" y2="{height - margin["bottom"]}" '
f'stroke="#E5E7EB" stroke-width="1"/>')
elements.append(f'<line x1="{margin["left"]}" y1="{height - margin["bottom"]}" '
f'x2="{width - margin["right"]}" y2="{height - margin["bottom"]}" '
f'stroke="#E5E7EB" stroke-width="1"/>')
# Grid lines
for i in range(5):
y = margin["top"] + (chart_height * i / 4)
elements.append(f'<line x1="{margin["left"]}" y1="{y:.1f}" '
f'x2="{width - margin["right"]}" y2="{y:.1f}" '
f'stroke="#F3F4F6" stroke-width="1"/>')
# Bars
for i, val in enumerate(data):
color = get_color(i, base_hue, n)
if horizontal:
bar_w = (val / max_val) * chart_width
x = margin["left"]
y = margin["top"] + i * (bar_height + gap) + gap / 2
elements.append(f'<rect x="{x:.1f}" y="{y:.1f}" width="{bar_w:.1f}" '
f'height="{bar_height:.1f}" fill="{color}" rx="2"/>')
if labels and i < len(labels):
elements.append(f'<text x="{margin["left"] - 5}" y="{y + bar_height / 2:.1f}" '
f'text-anchor="end" dominant-baseline="middle" '
f'font-size="10" fill="#6B7280">{labels[i]}</text>')
else:
bar_h = (val / max_val) * chart_height
x = margin["left"] + i * (bar_width + gap) + gap / 2
y = height - margin["bottom"] - bar_h
elements.append(f'<rect x="{x:.1f}" y="{y:.1f}" width="{bar_width:.1f}" '
f'height="{bar_h:.1f}" fill="{color}" rx="2"/>')
# Labels
if labels and i < len(labels):
label_x = x + bar_width / 2
label_y = height - margin["bottom"] + 15
elements.append(f'<text x="{label_x:.1f}" y="{label_y:.1f}" '
f'text-anchor="middle" font-size="10" fill="#6B7280">{labels[i]}</text>')
# Values
if show_values:
val_y = y - 5
elements.append(f'<text x="{x + bar_width / 2:.1f}" y="{val_y:.1f}" '
f'text-anchor="middle" font-size="9" fill="#374151">{val:.0f}</text>')
return elements
def generate_line_chart(
data: list,
labels: list,
width: float,
height: float,
stroke: str,
stroke_width: float,
show_points: bool,
show_area: bool,
smooth: bool,
) -> list:
"""Generate line chart."""
elements = []
margin = {"top": 20, "right": 20, "bottom": 40, "left": 50}
chart_width = width - margin["left"] - margin["right"]
chart_height = height - margin["top"] - margin["bottom"]
max_val = max(data)
min_val = min(data)
val_range = max_val - min_val or 1
n = len(data)
# Axes
elements.append(f'<line x1="{margin["left"]}" y1="{margin["top"]}" '
f'x2="{margin["left"]}" y2="{height - margin["bottom"]}" '
f'stroke="#E5E7EB" stroke-width="1"/>')
elements.append(f'<line x1="{margin["left"]}" y1="{height - margin["bottom"]}" '
f'x2="{width - margin["right"]}" y2="{height - margin["bottom"]}" '
f'stroke="#E5E7EB" stroke-width="1"/>')
# Calculate points
points = []
for i, val in enumerate(data):
x = margin["left"] + (i / (n - 1)) * chart_width if n > 1 else margin["left"] + chart_width / 2
y = margin["top"] + chart_height - ((val - min_val) / val_range) * chart_height
points.append((x, y))
# Build path
if smooth and len(points) > 2:
# Catmull-Rom to Bezier approximation
path_d = f"M{points[0][0]:.1f},{points[0][1]:.1f}"
for i in range(1, len(points)):
p0 = points[max(0, i - 2)]
p1 = points[max(0, i - 1)]
p2 = points[i]
p3 = points[min(len(points) - 1, i + 1)]
cp1x = p1[0] + (p2[0] - p0[0]) / 6
cp1y = p1[1] + (p2[1] - p0[1]) / 6
cp2x = p2[0] - (p3[0] - p1[0]) / 6
cp2y = p2[1] - (p3[1] - p1[1]) / 6
path_d += f" C{cp1x:.1f},{cp1y:.1f} {cp2x:.1f},{cp2y:.1f} {p2[0]:.1f},{p2[1]:.1f}"
else:
path_d = " ".join([f"{'M' if i == 0 else 'L'}{p[0]:.1f},{p[1]:.1f}" for i, p in enumerate(points)])
# Area fill
if show_area:
area_d = path_d + f" L{points[-1][0]:.1f},{height - margin['bottom']:.1f} L{points[0][0]:.1f},{height - margin['bottom']:.1f} Z"
elements.append(f'<path d="{area_d}" fill="{stroke}" fill-opacity="0.2"/>')
# Line
elements.append(f'<path d="{path_d}" fill="none" stroke="{stroke}" '
f'stroke-width="{stroke_width}" stroke-linecap="round" stroke-linejoin="round"/>')
# Points
if show_points:
for i, (x, y) in enumerate(points):
elements.append(f'<circle cx="{x:.1f}" cy="{y:.1f}" r="4" fill="white" stroke="{stroke}" stroke-width="2"/>')
# Labels
for i, (x, y) in enumerate(points):
if labels and i < len(labels):
elements.append(f'<text x="{x:.1f}" y="{height - margin["bottom"] + 15:.1f}" '
f'text-anchor="middle" font-size="10" fill="#6B7280">{labels[i]}</text>')
return elements
def generate_pie_chart(
data: list,
labels: list,
cx: float,
cy: float,
radius: float,
base_hue: int,
donut: bool,
inner_radius_ratio: float,
) -> list:
"""Generate pie or donut chart."""
elements = []
total = sum(data)
n = len(data)
start_angle = -90 # Start from top
inner_radius = radius * inner_radius_ratio if donut else 0
for i, val in enumerate(data):
# Calculate angles
sweep = (val / total) * 360
end_angle = start_angle + sweep
# Convert to radians
start_rad = math.radians(start_angle)
end_rad = math.radians(end_angle)
# Calculate arc points
x1 = cx + radius * math.cos(start_rad)
y1 = cy + radius * math.sin(start_rad)
x2 = cx + radius * math.cos(end_rad)
y2 = cy + radius * math.sin(end_rad)
large_arc = 1 if sweep > 180 else 0
color = get_color(i, base_hue, n)
if donut:
# Inner arc points
ix1 = cx + inner_radius * math.cos(start_rad)
iy1 = cy + inner_radius * math.sin(start_rad)
ix2 = cx + inner_radius * math.cos(end_rad)
iy2 = cy + inner_radius * math.sin(end_rad)
path_d = (f"M{x1:.1f},{y1:.1f} "
f"A{radius},{radius} 0 {large_arc} 1 {x2:.1f},{y2:.1f} "
f"L{ix2:.1f},{iy2:.1f} "
f"A{inner_radius},{inner_radius} 0 {large_arc} 0 {ix1:.1f},{iy1:.1f} Z")
else:
path_d = (f"M{cx},{cy} "
f"L{x1:.1f},{y1:.1f} "
f"A{radius},{radius} 0 {large_arc} 1 {x2:.1f},{y2:.1f} Z")
elements.append(f'<path d="{path_d}" fill="{color}" stroke="white" stroke-width="1"/>')
# Label
if labels and i < len(labels):
mid_angle = math.radians(start_angle + sweep / 2)
label_radius = radius * (0.7 if not donut else (1 + inner_radius_ratio) / 2)
label_x = cx + label_radius * math.cos(mid_angle)
label_y = cy + label_radius * math.sin(mid_angle)
elements.append(f'<text x="{label_x:.1f}" y="{label_y:.1f}" '
f'text-anchor="middle" dominant-baseline="middle" '
f'font-size="10" fill="white" font-weight="bold">{labels[i]}</text>')
start_angle = end_angle
return elements
def main():
parser = argparse.ArgumentParser(description="Generate SVG charts")
# Chart type
parser.add_argument("--bar", action="store_true", help="Generate bar chart")
parser.add_argument("--line", action="store_true", help="Generate line chart")
parser.add_argument("--pie", action="store_true", help="Generate pie chart")
parser.add_argument("--donut", action="store_true", help="Generate donut chart")
parser.add_argument("--area", action="store_true", help="Generate area chart")
# Data
parser.add_argument("--data", required=True, help="Comma-separated data values")
parser.add_argument("--labels", default="", help="Comma-separated labels")
# Dimensions
parser.add_argument("--width", type=float, default=300, help="Chart width")
parser.add_argument("--height", type=float, default=200, help="Chart height")
# Styling
parser.add_argument("--base-hue", type=int, default=217, help="Base hue for colors")
parser.add_argument("--stroke", default="#3B82F6", help="Line stroke color")
parser.add_argument("--stroke-width", type=float, default=2, help="Line stroke width")
# Options
parser.add_argument("--show-values", action="store_true", help="Show values on bars")
parser.add_argument("--show-points", action="store_true", help="Show points on line")
parser.add_argument("--horizontal", action="store_true", help="Horizontal bar chart")
parser.add_argument("--smooth", action="store_true", help="Smooth line curves")
parser.add_argument("--inner-radius", type=float, default=0.5, help="Donut inner radius ratio")
parser.add_argument("-o", "--output", help="Output file")
args = parser.parse_args()
data = parse_data(args.data)
labels = parse_labels(args.labels)
if args.pie or args.donut:
cx, cy = args.width / 2, args.height / 2
radius = min(args.width, args.height) / 2 - 20
elements = generate_pie_chart(
data=data,
labels=labels,
cx=cx,
cy=cy,
radius=radius,
base_hue=args.base_hue,
donut=args.donut,
inner_radius_ratio=args.inner_radius,
)
title = "Donut Chart" if args.donut else "Pie Chart"
elif args.line or args.area:
elements = generate_line_chart(
data=data,
labels=labels,
width=args.width,
height=args.height,
stroke=args.stroke,
stroke_width=args.stroke_width,
show_points=args.show_points,
show_area=args.area,
smooth=args.smooth,
)
title = "Area Chart" if args.area else "Line Chart"
else: # Default to bar
elements = generate_bar_chart(
data=data,
labels=labels,
width=args.width,
height=args.height,
base_hue=args.base_hue,
show_values=args.show_values,
horizontal=args.horizontal,
)
title = "Bar Chart"
desc = f"Data visualization with {len(data)} data points"
svg = f'''<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {args.width} {args.height}" width="{args.width:.0f}" height="{args.height:.0f}" role="img" aria-labelledby="title desc">
<title id="title">{title}</title>
<desc id="desc">{desc}</desc>
<style>text {{ font-family: system-ui, -apple-system, sans-serif; }}</style>
{chr(10).join(" " + e for e in elements)}
</svg>'''
if args.output:
with open(args.output, "w") as f:
f.write(svg)
print(f"Generated: {args.output}", file=sys.stderr)
else:
print(svg)
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""Generate SVG fractal patterns (trees, snowflakes, sierpinski, etc.).
Examples:
# Fractal tree
python generate_fractal.py --tree --depth 8 -o tree.svg
# Koch snowflake
python generate_fractal.py --koch --depth 4 -o snowflake.svg
# Sierpinski triangle
python generate_fractal.py --sierpinski --depth 5 -o sierpinski.svg
# Recursive circles
python generate_fractal.py --circles --depth 4 -o circles.svg
"""
import argparse
import math
import sys
def seeded_random(seed: int) -> float:
"""Deterministic pseudo-random number generator."""
x = math.sin(seed * 9999) * 10000
return x - math.floor(x)
def generate_tree(
x: float,
y: float,
length: float,
angle: float,
depth: int,
branch_angle: float,
length_ratio: float,
stroke: str,
vary_angle: bool,
seed: int,
) -> list:
"""Generate fractal tree branches recursively."""
if depth == 0 or length < 1:
return []
x2 = x + length * math.cos(angle)
y2 = y + length * math.sin(angle)
stroke_width = max(0.5, depth * 0.8)
line = f'<line x1="{x:.1f}" y1="{y:.1f}" x2="{x2:.1f}" y2="{y2:.1f}" stroke="{stroke}" stroke-width="{stroke_width:.1f}" stroke-linecap="round"/>'
elements = [line]
# Branch variations
r = seeded_random(seed + depth * 100)
left_angle = branch_angle * (0.8 + r * 0.4) if vary_angle else branch_angle
right_angle = branch_angle * (0.8 + seeded_random(seed + depth * 100 + 1) * 0.4) if vary_angle else branch_angle
# Left branch
elements.extend(generate_tree(
x2, y2, length * length_ratio,
angle - left_angle,
depth - 1, branch_angle, length_ratio, stroke, vary_angle, seed + 1
))
# Right branch
elements.extend(generate_tree(
x2, y2, length * length_ratio,
angle + right_angle,
depth - 1, branch_angle, length_ratio, stroke, vary_angle, seed + 2
))
return elements
def koch_segment(x1: float, y1: float, x2: float, y2: float, depth: int) -> str:
"""Generate Koch curve segment recursively."""
if depth == 0:
return f"L{x2:.1f},{y2:.1f}"
dx = x2 - x1
dy = y2 - y1
# Divide into thirds
ax = x1 + dx / 3
ay = y1 + dy / 3
bx = x1 + dx * 2 / 3
by = y1 + dy * 2 / 3
# Peak point (equilateral triangle)
px = (ax + bx) / 2 - dy * math.sqrt(3) / 6
py = (ay + by) / 2 + dx * math.sqrt(3) / 6
return (
koch_segment(x1, y1, ax, ay, depth - 1) +
koch_segment(ax, ay, px, py, depth - 1) +
koch_segment(px, py, bx, by, depth - 1) +
koch_segment(bx, by, x2, y2, depth - 1)
)
def generate_koch_snowflake(
cx: float,
cy: float,
size: float,
depth: int,
stroke: str,
stroke_width: float,
fill: str,
) -> list:
"""Generate Koch snowflake."""
# Start with equilateral triangle
h = size * math.sqrt(3) / 2
p1 = (cx, cy - h * 2 / 3)
p2 = (cx - size / 2, cy + h / 3)
p3 = (cx + size / 2, cy + h / 3)
path_d = f"M{p1[0]:.1f},{p1[1]:.1f}"
path_d += koch_segment(p1[0], p1[1], p2[0], p2[1], depth)
path_d += koch_segment(p2[0], p2[1], p3[0], p3[1], depth)
path_d += koch_segment(p3[0], p3[1], p1[0], p1[1], depth)
path_d += " Z"
fill_attr = f'fill="{fill}"' if fill != "none" else 'fill="none"'
return [f'<path d="{path_d}" {fill_attr} stroke="{stroke}" stroke-width="{stroke_width}"/>']
def generate_sierpinski(
x: float,
y: float,
size: float,
depth: int,
fill: str,
stroke: str,
stroke_width: float,
) -> list:
"""Generate Sierpinski triangle recursively."""
h = size * math.sqrt(3) / 2
if depth == 0:
points = f"{x:.1f},{y + h:.1f} {x + size / 2:.1f},{y:.1f} {x + size:.1f},{y + h:.1f}"
stroke_attr = f' stroke="{stroke}" stroke-width="{stroke_width}"' if stroke != "none" else ""
return [f'<polygon points="{points}" fill="{fill}"{stroke_attr}/>']
half = size / 2
h_half = half * math.sqrt(3) / 2
elements = []
# Bottom-left triangle
elements.extend(generate_sierpinski(x, y + h_half, half, depth - 1, fill, stroke, stroke_width))
# Top triangle
elements.extend(generate_sierpinski(x + half / 2, y, half, depth - 1, fill, stroke, stroke_width))
# Bottom-right triangle
elements.extend(generate_sierpinski(x + half, y + h_half, half, depth - 1, fill, stroke, stroke_width))
return elements
def generate_recursive_circles(
cx: float,
cy: float,
radius: float,
depth: int,
fill: str,
stroke: str,
stroke_width: float,
ratio: float,
) -> list:
"""Generate recursively nested circles."""
if depth == 0 or radius < 1:
return []
stroke_attr = f' stroke="{stroke}" stroke-width="{stroke_width}"' if stroke != "none" else ""
elements = [f'<circle cx="{cx:.1f}" cy="{cy:.1f}" r="{radius:.1f}" fill="{fill}" fill-opacity="0.3"{stroke_attr}/>']
# Four smaller circles at cardinal points
new_radius = radius * ratio
offset = radius - new_radius
for angle in [0, math.pi / 2, math.pi, 3 * math.pi / 2]:
nx = cx + offset * math.cos(angle)
ny = cy + offset * math.sin(angle)
elements.extend(generate_recursive_circles(nx, ny, new_radius, depth - 1, fill, stroke, stroke_width, ratio))
return elements
def main():
parser = argparse.ArgumentParser(description="Generate SVG fractal patterns")
# Pattern type
parser.add_argument("--tree", action="store_true", help="Generate fractal tree")
parser.add_argument("--koch", action="store_true", help="Generate Koch snowflake")
parser.add_argument("--sierpinski", action="store_true", help="Generate Sierpinski triangle")
parser.add_argument("--circles", action="store_true", help="Generate recursive circles")
# Common options
parser.add_argument("--depth", type=int, default=5, help="Recursion depth")
parser.add_argument("--size", type=float, default=80, help="Base size")
parser.add_argument("--fill", default="#3B82F6", help="Fill color")
parser.add_argument("--stroke", default="#1E40AF", help="Stroke color")
parser.add_argument("--stroke-width", type=float, default=1, help="Stroke width")
parser.add_argument("--viewbox", type=int, default=100, help="ViewBox size")
# Tree options
parser.add_argument("--branch-angle", type=float, default=0.5, help="Branch angle in radians")
parser.add_argument("--length-ratio", type=float, default=0.7, help="Length reduction ratio")
parser.add_argument("--vary-angle", action="store_true", help="Add random variation to angles")
# Circle options
parser.add_argument("--ratio", type=float, default=0.45, help="Size ratio for recursive circles")
parser.add_argument("--seed", type=int, default=42, help="Random seed")
parser.add_argument("-o", "--output", help="Output file (default: stdout)")
args = parser.parse_args()
vb = args.viewbox
cx, cy = vb / 2, vb / 2
if args.tree:
# Tree grows upward from bottom center
elements = generate_tree(
x=cx,
y=vb - 10,
length=args.size * 0.3,
angle=-math.pi / 2, # Point upward
depth=args.depth,
branch_angle=args.branch_angle,
length_ratio=args.length_ratio,
stroke=args.stroke,
vary_angle=args.vary_angle,
seed=args.seed,
)
title = "Fractal Tree"
desc = f"A fractal tree with depth {args.depth}"
elif args.koch:
elements = generate_koch_snowflake(
cx=cx,
cy=cy,
size=args.size,
depth=args.depth,
stroke=args.stroke,
stroke_width=args.stroke_width,
fill=args.fill,
)
title = "Koch Snowflake"
desc = f"A Koch snowflake with depth {args.depth}"
elif args.sierpinski:
# Center the triangle
h = args.size * math.sqrt(3) / 2
start_x = cx - args.size / 2
start_y = cy - h / 2
elements = generate_sierpinski(
x=start_x,
y=start_y,
size=args.size,
depth=args.depth,
fill=args.fill,
stroke=args.stroke,
stroke_width=args.stroke_width,
)
title = "Sierpinski Triangle"
desc = f"A Sierpinski triangle with depth {args.depth}"
elif args.circles:
elements = generate_recursive_circles(
cx=cx,
cy=cy,
radius=args.size / 2,
depth=args.depth,
fill=args.fill,
stroke=args.stroke,
stroke_width=args.stroke_width,
ratio=args.ratio,
)
title = "Recursive Circles"
desc = f"Recursive circles with depth {args.depth}"
else:
# Default to tree
elements = generate_tree(
x=cx, y=vb - 10, length=args.size * 0.3, angle=-math.pi / 2,
depth=args.depth, branch_angle=args.branch_angle,
length_ratio=args.length_ratio, stroke=args.stroke,
vary_angle=args.vary_angle, seed=args.seed,
)
title = "Fractal Tree"
desc = f"A fractal tree with depth {args.depth}"
svg = f'''<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {vb} {vb}" width="{vb}" height="{vb}">
<title>{title}</title>
<desc>{desc}</desc>
{chr(10).join(" " + e for e in elements)}
</svg>'''
if args.output:
with open(args.output, "w") as f:
f.write(svg)
print(f"Generated: {args.output}", file=sys.stderr)
else:
print(svg)
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""Generate SVG grid patterns.
Examples:
# Basic grid
python generate_grid.py -c 6 -r 6 -o grid.svg
# Grid with variations
python generate_grid.py -c 8 -r 8 --vary-size --vary-opacity -o varied.svg
# Diamond grid with hue variation
python generate_grid.py -c 5 -r 5 --shape diamond --vary-hue -o diamonds.svg
# Circles with custom colors
python generate_grid.py -c 10 -r 10 --shape circle --fill "#E11D48" -o circles.svg
"""
import argparse
import math
import sys
def seeded_random(seed: int) -> float:
"""Deterministic pseudo-random number generator."""
x = math.sin(seed * 9999) * 10000
return x - math.floor(x)
def generate_rect(x: float, y: float, size: float, fill: str, opacity: float) -> str:
"""Generate rectangle element."""
return f'<rect x="{x:.1f}" y="{y:.1f}" width="{size:.1f}" height="{size:.1f}" fill="{fill}" opacity="{opacity:.2f}"/>'
def generate_circle(x: float, y: float, size: float, fill: str, opacity: float) -> str:
"""Generate circle element."""
r = size / 2
cx = x + r
cy = y + r
return f'<circle cx="{cx:.1f}" cy="{cy:.1f}" r="{r:.1f}" fill="{fill}" opacity="{opacity:.2f}"/>'
def generate_diamond(x: float, y: float, size: float, fill: str, opacity: float) -> str:
"""Generate diamond (rotated square) element."""
cx = x + size / 2
cy = y + size / 2
half = size / 2
points = f"{cx},{cy - half} {cx + half},{cy} {cx},{cy + half} {cx - half},{cy}"
return f'<polygon points="{points}" fill="{fill}" opacity="{opacity:.2f}"/>'
def hsl_to_hex(h: float, s: float, l: float) -> str:
"""Convert HSL to hex color."""
c = (1 - abs(2 * l - 1)) * s
x = c * (1 - abs((h / 60) % 2 - 1))
m = l - c / 2
if h < 60:
r, g, b = c, x, 0
elif h < 120:
r, g, b = x, c, 0
elif h < 180:
r, g, b = 0, c, x
elif h < 240:
r, g, b = 0, x, c
elif h < 300:
r, g, b = x, 0, c
else:
r, g, b = c, 0, x
r, g, b = int((r + m) * 255), int((g + m) * 255), int((b + m) * 255)
return f"#{r:02x}{g:02x}{b:02x}"
def generate_grid(
cols: int,
rows: int,
size: float,
gap: float,
shape: str,
fill: str,
base_hue: int,
vary_size: bool,
vary_opacity: bool,
vary_hue: bool,
seed: int,
) -> str:
"""Generate complete SVG grid."""
width = cols * (size + gap) - gap
height = rows * (size + gap) - gap
shape_funcs = {
"rect": generate_rect,
"circle": generate_circle,
"diamond": generate_diamond,
}
shape_func = shape_funcs.get(shape, generate_rect)
elements = []
rand_idx = seed
for row in range(rows):
for col in range(cols):
x = col * (size + gap)
y = row * (size + gap)
# Apply variations
elem_size = size
opacity = 1.0
elem_fill = fill
if vary_size:
rand_idx += 1
scale = 0.5 + seeded_random(rand_idx) * 0.5
elem_size = size * scale
x += (size - elem_size) / 2
y += (size - elem_size) / 2
if vary_opacity:
rand_idx += 1
opacity = 0.3 + seeded_random(rand_idx) * 0.7
if vary_hue:
rand_idx += 1
hue = (base_hue + seeded_random(rand_idx) * 60 - 30) % 360
elem_fill = hsl_to_hex(hue, 0.7, 0.55)
elements.append(shape_func(x, y, elem_size, elem_fill, opacity))
svg = f'''<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {width:.1f} {height:.1f}" width="{width:.0f}" height="{height:.0f}">
<title>Grid Pattern</title>
{chr(10).join(" " + e for e in elements)}
</svg>'''
return svg
def main():
parser = argparse.ArgumentParser(description="Generate SVG grid patterns")
parser.add_argument("-c", "--cols", type=int, default=6, help="Number of columns")
parser.add_argument("-r", "--rows", type=int, default=6, help="Number of rows")
parser.add_argument("-s", "--size", type=float, default=10, help="Element size")
parser.add_argument("-g", "--gap", type=float, default=2, help="Gap between elements")
parser.add_argument("--shape", choices=["rect", "circle", "diamond"], default="rect", help="Shape type")
parser.add_argument("--fill", default="#3B82F6", help="Fill color")
parser.add_argument("--base-hue", type=int, default=210, help="Base hue for variations")
parser.add_argument("--vary-size", action="store_true", help="Randomize sizes")
parser.add_argument("--vary-opacity", action="store_true", help="Randomize opacity")
parser.add_argument("--vary-hue", action="store_true", help="Randomize hue")
parser.add_argument("--seed", type=int, default=42, help="Random seed")
parser.add_argument("-o", "--output", help="Output file (default: stdout)")
args = parser.parse_args()
svg = generate_grid(
cols=args.cols,
rows=args.rows,
size=args.size,
gap=args.gap,
shape=args.shape,
fill=args.fill,
base_hue=args.base_hue,
vary_size=args.vary_size,
vary_opacity=args.vary_opacity,
vary_hue=args.vary_hue,
seed=args.seed,
)
if args.output:
with open(args.output, "w") as f:
f.write(svg)
else:
print(svg)
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""Generate common SVG icons.
Examples:
# List available icons
python generate_icon.py --list
# Generate specific icon
python generate_icon.py --icon check -o check.svg
# Generate with custom size and color
python generate_icon.py --icon heart --size 32 --stroke "#E11D48"
# Generate filled variant
python generate_icon.py --icon star --filled
"""
import argparse
import sys
# Icon definitions: path data for 24x24 viewBox
# Format: {name: {"path": d, "filled_path": d (optional)}}
ICONS = {
# UI Icons
"check": {
"path": "M5 12l5 5 9-9",
},
"x": {
"path": "M6 6l12 12M18 6l-12 12",
},
"plus": {
"path": "M12 5v14M5 12h14",
},
"minus": {
"path": "M5 12h14",
},
"chevron-right": {
"path": "M9 6l6 6-6 6",
},
"chevron-left": {
"path": "M15 6l-6 6 6 6",
},
"chevron-down": {
"path": "M6 9l6 6 6-6",
},
"chevron-up": {
"path": "M6 15l6-6 6 6",
},
"arrow-right": {
"path": "M5 12h14M12 5l7 7-7 7",
},
"arrow-left": {
"path": "M19 12H5M12 5l-7 7 7 7",
},
"menu": {
"path": "M3 6h18M3 12h18M3 18h18",
},
"more-horizontal": {
"path": "M12 12h.01M6 12h.01M18 12h.01",
"stroke_width": 3,
},
"more-vertical": {
"path": "M12 12h.01M12 6h.01M12 18h.01",
"stroke_width": 3,
},
# Common Icons
"search": {
"path": "M21 21l-5-5m2-5a7 7 0 11-14 0 7 7 0 0114 0z",
},
"home": {
"path": "M3 12l9-9 9 9M5 10v10a1 1 0 001 1h3a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1h3a1 1 0 001-1V10",
},
"user": {
"path": "M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z",
},
"settings": {
"path": "M12 15a3 3 0 100-6 3 3 0 000 6z",
"extra": "M19.4 15a1.65 1.65 0 00.33 1.82l.06.06a2 2 0 010 2.83 2 2 0 01-2.83 0l-.06-.06a1.65 1.65 0 00-1.82-.33 1.65 1.65 0 00-1 1.51V21a2 2 0 01-2 2 2 2 0 01-2-2v-.09A1.65 1.65 0 009 19.4a1.65 1.65 0 00-1.82.33l-.06.06a2 2 0 01-2.83 0 2 2 0 010-2.83l.06-.06a1.65 1.65 0 00.33-1.82 1.65 1.65 0 00-1.51-1H3a2 2 0 01-2-2 2 2 0 012-2h.09A1.65 1.65 0 004.6 9a1.65 1.65 0 00-.33-1.82l-.06-.06a2 2 0 010-2.83 2 2 0 012.83 0l.06.06a1.65 1.65 0 001.82.33H9a1.65 1.65 0 001-1.51V3a2 2 0 012-2 2 2 0 012 2v.09a1.65 1.65 0 001 1.51 1.65 1.65 0 001.82-.33l.06-.06a2 2 0 012.83 0 2 2 0 010 2.83l-.06.06a1.65 1.65 0 00-.33 1.82V9a1.65 1.65 0 001.51 1H21a2 2 0 012 2 2 2 0 01-2 2h-.09a1.65 1.65 0 00-1.51 1z",
},
"mail": {
"path": "M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z",
},
"phone": {
"path": "M22 16.92v3a2 2 0 01-2.18 2 19.79 19.79 0 01-8.63-3.07 19.5 19.5 0 01-6-6 19.79 19.79 0 01-3.07-8.67A2 2 0 014.11 2h3a2 2 0 012 1.72 12.84 12.84 0 00.7 2.81 2 2 0 01-.45 2.11L8.09 9.91a16 16 0 006 6l1.27-1.27a2 2 0 012.11-.45 12.84 12.84 0 002.81.7A2 2 0 0122 16.92z",
},
"calendar": {
"path": "M19 4H5a2 2 0 00-2 2v14a2 2 0 002 2h14a2 2 0 002-2V6a2 2 0 00-2-2zM16 2v4M8 2v4M3 10h18",
},
"clock": {
"path": "M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2 2 6.477 2 12s4.477 10 10 10zM12 6v6l4 2",
},
"bell": {
"path": "M18 8A6 6 0 006 8c0 7-3 9-3 9h18s-3-2-3-9M13.73 21a2 2 0 01-3.46 0",
},
# Status Icons
"heart": {
"path": "M20.84 4.61a5.5 5.5 0 00-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 00-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 000-7.78z",
"filled_path": "M20.84 4.61a5.5 5.5 0 00-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 00-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 000-7.78z",
},
"star": {
"path": "M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z",
"filled_path": "M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z",
},
"thumbs-up": {
"path": "M14 9V5a3 3 0 00-3-3l-4 9v11h11.28a2 2 0 002-1.7l1.38-9a2 2 0 00-2-2.3zM7 22H4a2 2 0 01-2-2v-7a2 2 0 012-2h3",
},
"alert-circle": {
"path": "M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2 2 6.477 2 12s4.477 10 10 10zM12 8v4M12 16h.01",
},
"check-circle": {
"path": "M22 11.08V12a10 10 0 11-5.93-9.14M22 4L12 14.01l-3-3",
},
"info": {
"path": "M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2 2 6.477 2 12s4.477 10 10 10zM12 16v-4M12 8h.01",
},
# Media Icons
"play": {
"path": "M5 3l14 9-14 9V3z",
"filled_path": "M5 3l14 9-14 9V3z",
},
"pause": {
"path": "M6 4h4v16H6zM14 4h4v16h-4z",
},
"skip-forward": {
"path": "M5 4l10 8-10 8V4zM19 5v14",
},
"volume": {
"path": "M11 5L6 9H2v6h4l5 4V5zM19.07 4.93a10 10 0 010 14.14M15.54 8.46a5 5 0 010 7.07",
},
"image": {
"path": "M19 3H5a2 2 0 00-2 2v14a2 2 0 002 2h14a2 2 0 002-2V5a2 2 0 00-2-2zM8.5 10a1.5 1.5 0 100-3 1.5 1.5 0 000 3zM21 15l-5-5L5 21",
},
"camera": {
"path": "M23 19a2 2 0 01-2 2H3a2 2 0 01-2-2V8a2 2 0 012-2h4l2-3h6l2 3h4a2 2 0 012 2zM12 17a4 4 0 100-8 4 4 0 000 8z",
},
# File Icons
"file": {
"path": "M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8zM14 2v6h6M16 13H8M16 17H8M10 9H8",
},
"folder": {
"path": "M22 19a2 2 0 01-2 2H4a2 2 0 01-2-2V5a2 2 0 012-2h5l2 3h9a2 2 0 012 2z",
},
"download": {
"path": "M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4M7 10l5 5 5-5M12 15V3",
},
"upload": {
"path": "M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4M17 8l-5-5-5 5M12 3v12",
},
"trash": {
"path": "M3 6h18M19 6v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2M10 11v6M14 11v6",
},
"edit": {
"path": "M11 4H4a2 2 0 00-2 2v14a2 2 0 002 2h14a2 2 0 002-2v-7M18.5 2.5a2.121 2.121 0 013 3L12 15l-4 1 1-4 9.5-9.5z",
},
"copy": {
"path": "M20 9h-9a2 2 0 00-2 2v9a2 2 0 002 2h9a2 2 0 002-2v-9a2 2 0 00-2-2zM5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1",
},
# Social Icons
"share": {
"path": "M18 8a3 3 0 100-6 3 3 0 000 6zM6 15a3 3 0 100-6 3 3 0 000 6zM18 22a3 3 0 100-6 3 3 0 000 6zM8.59 13.51l6.83 3.98M15.41 6.51l-6.82 3.98",
},
"link": {
"path": "M10 13a5 5 0 007.54.54l3-3a5 5 0 00-7.07-7.07l-1.72 1.71M14 11a5 5 0 00-7.54-.54l-3 3a5 5 0 007.07 7.07l1.71-1.71",
},
"external-link": {
"path": "M18 13v6a2 2 0 01-2 2H5a2 2 0 01-2-2V8a2 2 0 012-2h6M15 3h6v6M10 14L21 3",
},
# Misc
"loader": {
"path": "M12 2v4M12 18v4M4.93 4.93l2.83 2.83M16.24 16.24l2.83 2.83M2 12h4M18 12h4M4.93 19.07l2.83-2.83M16.24 7.76l2.83-2.83",
},
"refresh": {
"path": "M23 4v6h-6M1 20v-6h6M3.51 9a9 9 0 0114.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0020.49 15",
},
"eye": {
"path": "M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z",
"extra": "M12 15a3 3 0 100-6 3 3 0 000 6z",
},
"lock": {
"path": "M19 11H5a2 2 0 00-2 2v7a2 2 0 002 2h14a2 2 0 002-2v-7a2 2 0 00-2-2zM7 11V7a5 5 0 0110 0v4",
},
"unlock": {
"path": "M19 11H5a2 2 0 00-2 2v7a2 2 0 002 2h14a2 2 0 002-2v-7a2 2 0 00-2-2zM7 11V7a5 5 0 019.9-1",
},
"sun": {
"path": "M12 17a5 5 0 100-10 5 5 0 000 10zM12 1v2M12 21v2M4.22 4.22l1.42 1.42M18.36 18.36l1.42 1.42M1 12h2M21 12h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42",
},
"moon": {
"path": "M21 12.79A9 9 0 1111.21 3 7 7 0 0021 12.79z",
},
}
def generate_icon(
icon_name: str,
size: int,
stroke: str,
stroke_width: float,
filled: bool,
include_background: bool,
bg_color: str,
) -> str:
"""Generate SVG icon."""
if icon_name not in ICONS:
available = ", ".join(sorted(ICONS.keys()))
raise ValueError(f"Unknown icon: {icon_name}. Available: {available}")
icon = ICONS[icon_name]
custom_stroke_width = icon.get("stroke_width", stroke_width)
# Use filled path if available and requested
if filled and "filled_path" in icon:
path_d = icon["filled_path"]
fill_attr = f'fill="{stroke}"'
stroke_attr = 'stroke="none"'
else:
path_d = icon["path"]
fill_attr = 'fill="none"'
stroke_attr = f'stroke="{stroke}" stroke-width="{custom_stroke_width}"'
elements = []
# Background
if include_background:
elements.append(f'<rect width="24" height="24" rx="4" fill="{bg_color}"/>')
# Main path
elements.append(f'<path d="{path_d}" {fill_attr} {stroke_attr} stroke-linecap="round" stroke-linejoin="round"/>')
# Extra paths (for complex icons)
if "extra" in icon:
elements.append(f'<path d="{icon["extra"]}" {fill_attr} {stroke_attr} stroke-linecap="round" stroke-linejoin="round"/>')
svg = f'''<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="{size}" height="{size}">
<title>{icon_name.replace("-", " ").title()} Icon</title>
{chr(10).join(" " + e for e in elements)}
</svg>'''
return svg
def list_icons():
"""Print available icons."""
print("Available icons:")
print("-" * 40)
categories = {
"UI": ["check", "x", "plus", "minus", "chevron-right", "chevron-left",
"chevron-down", "chevron-up", "arrow-right", "arrow-left",
"menu", "more-horizontal", "more-vertical"],
"Common": ["search", "home", "user", "settings", "mail", "phone",
"calendar", "clock", "bell"],
"Status": ["heart", "star", "thumbs-up", "alert-circle", "check-circle", "info"],
"Media": ["play", "pause", "skip-forward", "volume", "image", "camera"],
"File": ["file", "folder", "download", "upload", "trash", "edit", "copy"],
"Social": ["share", "link", "external-link"],
"Misc": ["loader", "refresh", "eye", "lock", "unlock", "sun", "moon"],
}
for cat, icons in categories.items():
print(f"\n{cat}:")
print(" " + ", ".join(icons))
def main():
parser = argparse.ArgumentParser(description="Generate SVG icons")
parser.add_argument("--icon", help="Icon name to generate")
parser.add_argument("--list", action="store_true", help="List available icons")
parser.add_argument("--size", type=int, default=24, help="Icon size in pixels")
parser.add_argument("--stroke", default="currentColor", help="Stroke color")
parser.add_argument("--stroke-width", type=float, default=2, help="Stroke width")
parser.add_argument("--filled", action="store_true", help="Use filled variant if available")
parser.add_argument("--background", action="store_true", help="Include background")
parser.add_argument("--bg-color", default="#F3F4F6", help="Background color")
parser.add_argument("-o", "--output", help="Output file")
args = parser.parse_args()
if args.list:
list_icons()
return
if not args.icon:
parser.error("--icon is required (or use --list to see available icons)")
try:
svg = generate_icon(
icon_name=args.icon,
size=args.size,
stroke=args.stroke,
stroke_width=args.stroke_width,
filled=args.filled,
include_background=args.background,
bg_color=args.bg_color,
)
if args.output:
with open(args.output, "w") as f:
f.write(svg)
print(f"Generated: {args.output}", file=sys.stderr)
else:
print(svg)
except ValueError as e:
print(f"Error: {e}", file=sys.stderr)
sys.exit(1)
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""Generate SVG particle patterns (scatter, clusters, constellations).
Examples:
# Random scatter
python generate_particles.py -n 50 -o scatter.svg
# Clustered particles
python generate_particles.py --cluster -n 100 -o cluster.svg
# Constellation with connections
python generate_particles.py --constellation -n 30 --connect-distance 25 -o network.svg
# Gradient particles
python generate_particles.py --gradient -n 80 -o gradient.svg
"""
import argparse
import math
import sys
def seeded_random(seed: int) -> float:
"""Deterministic pseudo-random number generator."""
x = math.sin(seed * 9999) * 10000
return x - math.floor(x)
def hsl_to_hex(h: float, s: float, l: float) -> str:
"""Convert HSL to hex color."""
c = (1 - abs(2 * l - 1)) * s
x = c * (1 - abs((h / 60) % 2 - 1))
m = l - c / 2
if h < 60:
r, g, b = c, x, 0
elif h < 120:
r, g, b = x, c, 0
elif h < 180:
r, g, b = 0, c, x
elif h < 240:
r, g, b = 0, x, c
elif h < 300:
r, g, b = x, 0, c
else:
r, g, b = c, 0, x
r, g, b = int((r + m) * 255), int((g + m) * 255), int((b + m) * 255)
return f"#{r:02x}{g:02x}{b:02x}"
def generate_particles(
n: int,
width: float,
height: float,
fill: str,
min_size: float,
max_size: float,
seed: int,
) -> list:
"""Generate random scatter particles."""
particles = []
rand_idx = seed
for _ in range(n):
rand_idx += 1
x = seeded_random(rand_idx) * width
rand_idx += 1
y = seeded_random(rand_idx) * height
rand_idx += 1
size = min_size + seeded_random(rand_idx) * (max_size - min_size)
rand_idx += 1
opacity = 0.3 + seeded_random(rand_idx) * 0.7
particles.append((x, y, size, opacity))
elements = []
for x, y, size, opacity in particles:
elements.append(
f'<circle cx="{x:.1f}" cy="{y:.1f}" r="{size:.1f}" fill="{fill}" opacity="{opacity:.2f}"/>'
)
return elements, particles
def generate_cluster(
n: int,
width: float,
height: float,
clusters: int,
fill: str,
base_hue: int,
vary_hue: bool,
seed: int,
) -> list:
"""Generate clustered particles."""
elements = []
rand_idx = seed
# Generate cluster centers
centers = []
for _ in range(clusters):
rand_idx += 1
cx = 10 + seeded_random(rand_idx) * (width - 20)
rand_idx += 1
cy = 10 + seeded_random(rand_idx) * (height - 20)
centers.append((cx, cy))
# Generate particles around centers
for i in range(n):
rand_idx += 1
center = centers[int(seeded_random(rand_idx) * len(centers))]
rand_idx += 1
angle = seeded_random(rand_idx) * 2 * math.pi
rand_idx += 1
distance = seeded_random(rand_idx) ** 0.5 * 15 # Square root for uniform distribution
x = center[0] + distance * math.cos(angle)
y = center[1] + distance * math.sin(angle)
rand_idx += 1
size = 1 + seeded_random(rand_idx) * 2
rand_idx += 1
opacity = 0.4 + seeded_random(rand_idx) * 0.6
particle_fill = fill
if vary_hue:
hue = (base_hue + i * (60 / n)) % 360
particle_fill = hsl_to_hex(hue, 0.7, 0.55)
elements.append(
f'<circle cx="{x:.1f}" cy="{y:.1f}" r="{size:.1f}" fill="{particle_fill}" opacity="{opacity:.2f}"/>'
)
return elements
def generate_constellation(
n: int,
width: float,
height: float,
connect_distance: float,
fill: str,
stroke: str,
seed: int,
) -> list:
"""Generate constellation pattern with connected particles."""
elements = []
rand_idx = seed
# Generate points
points = []
for _ in range(n):
rand_idx += 1
x = 5 + seeded_random(rand_idx) * (width - 10)
rand_idx += 1
y = 5 + seeded_random(rand_idx) * (height - 10)
points.append((x, y))
# Generate connections (lines first, so dots appear on top)
for i, p1 in enumerate(points):
for j, p2 in enumerate(points):
if i >= j:
continue
dist = math.sqrt((p1[0] - p2[0]) ** 2 + (p1[1] - p2[1]) ** 2)
if dist < connect_distance:
opacity = 1 - (dist / connect_distance)
elements.append(
f'<line x1="{p1[0]:.1f}" y1="{p1[1]:.1f}" x2="{p2[0]:.1f}" y2="{p2[1]:.1f}" '
f'stroke="{stroke}" stroke-width="0.5" opacity="{opacity:.2f}"/>'
)
# Generate dots
for x, y in points:
elements.append(f'<circle cx="{x:.1f}" cy="{y:.1f}" r="2" fill="{fill}"/>')
return elements
def generate_gradient_particles(
n: int,
width: float,
height: float,
base_hue: int,
seed: int,
) -> list:
"""Generate particles with gradient coloring based on position."""
elements = []
rand_idx = seed
for _ in range(n):
rand_idx += 1
x = seeded_random(rand_idx) * width
rand_idx += 1
y = seeded_random(rand_idx) * height
rand_idx += 1
size = 1 + seeded_random(rand_idx) * 3
# Color based on position
hue = (base_hue + (x / width) * 60 + (y / height) * 30) % 360
fill = hsl_to_hex(hue, 0.7, 0.55)
rand_idx += 1
opacity = 0.5 + seeded_random(rand_idx) * 0.5
elements.append(
f'<circle cx="{x:.1f}" cy="{y:.1f}" r="{size:.1f}" fill="{fill}" opacity="{opacity:.2f}"/>'
)
return elements
def main():
parser = argparse.ArgumentParser(description="Generate SVG particle patterns")
parser.add_argument("-n", "--count", type=int, default=50, help="Number of particles")
parser.add_argument("--cluster", action="store_true", help="Generate clustered particles")
parser.add_argument("--clusters", type=int, default=3, help="Number of clusters")
parser.add_argument("--constellation", action="store_true", help="Generate constellation")
parser.add_argument("--connect-distance", type=float, default=20, help="Max connection distance")
parser.add_argument("--gradient", action="store_true", help="Position-based gradient coloring")
parser.add_argument("--fill", default="#3B82F6", help="Fill color")
parser.add_argument("--stroke", default="#3B82F6", help="Stroke color for connections")
parser.add_argument("--min-size", type=float, default=1, help="Minimum particle size")
parser.add_argument("--max-size", type=float, default=4, help="Maximum particle size")
parser.add_argument("--base-hue", type=int, default=210, help="Base hue")
parser.add_argument("--vary-hue", action="store_true", help="Vary hue")
parser.add_argument("--seed", type=int, default=42, help="Random seed")
parser.add_argument("-o", "--output", help="Output file")
args = parser.parse_args()
width, height = 100, 100
if args.cluster:
elements = generate_cluster(
n=args.count,
width=width,
height=height,
clusters=args.clusters,
fill=args.fill,
base_hue=args.base_hue,
vary_hue=args.vary_hue,
seed=args.seed,
)
elif args.constellation:
elements = generate_constellation(
n=args.count,
width=width,
height=height,
connect_distance=args.connect_distance,
fill=args.fill,
stroke=args.stroke,
seed=args.seed,
)
elif args.gradient:
elements = generate_gradient_particles(
n=args.count,
width=width,
height=height,
base_hue=args.base_hue,
seed=args.seed,
)
else:
elements, _ = generate_particles(
n=args.count,
width=width,
height=height,
fill=args.fill,
min_size=args.min_size,
max_size=args.max_size,
seed=args.seed,
)
svg = f'''<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {width} {height}" width="{width}" height="{height}">
<title>Particle Pattern</title>
{chr(10).join(" " + e for e in elements)}
</svg>'''
if args.output:
with open(args.output, "w") as f:
f.write(svg)
else:
print(svg)
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""Generate SVG radial and spiral patterns.
Examples:
# Simple radial distribution
python generate_radial.py -n 12 -r 40 --element-size 8 -o radial.svg
# Spiral pattern
python generate_radial.py --spiral -n 50 --start-radius 10 --end-radius 45 --turns 3
# Concentric rings
python generate_radial.py --concentric --rings 5 --elements-per-ring 8
# Sunburst pattern
python generate_radial.py --sunburst -n 24 -r 40
"""
import argparse
import math
import sys
def seeded_random(seed: int) -> float:
"""Deterministic pseudo-random number generator."""
x = math.sin(seed * 9999) * 10000
return x - math.floor(x)
def generate_radial(
count: int,
radius: float,
center_x: float,
center_y: float,
element_size: float,
fill: str,
stroke: str,
stroke_width: float,
shape: str,
start_angle: float,
vary_size: bool,
vary_opacity: bool,
seed: int,
) -> list:
"""Generate elements in a radial pattern."""
elements = []
for i in range(count):
angle = math.radians(start_angle) + (2 * math.pi * i) / count
x = center_x + radius * math.cos(angle)
y = center_y + radius * math.sin(angle)
r = seeded_random(seed + i)
size = element_size * (0.5 + r * 0.5) if vary_size else element_size
opacity = 0.4 + seeded_random(seed + i + 1000) * 0.6 if vary_opacity else 1.0
opacity_attr = f' opacity="{opacity:.2f}"' if opacity < 1 else ""
stroke_attr = f' stroke="{stroke}" stroke-width="{stroke_width}"' if stroke != "none" else ""
if shape == "circle":
elem = f'<circle cx="{x:.1f}" cy="{y:.1f}" r="{size:.1f}" fill="{fill}"{stroke_attr}{opacity_attr}/>'
elif shape == "rect":
elem = f'<rect x="{x - size:.1f}" y="{y - size:.1f}" width="{size * 2:.1f}" height="{size * 2:.1f}" fill="{fill}"{stroke_attr}{opacity_attr} transform="rotate({math.degrees(angle):.1f} {x:.1f} {y:.1f})"/>'
else: # line (for sunburst)
x2 = center_x + (radius + size * 3) * math.cos(angle)
y2 = center_y + (radius + size * 3) * math.sin(angle)
elem = f'<line x1="{x:.1f}" y1="{y:.1f}" x2="{x2:.1f}" y2="{y2:.1f}" stroke="{fill}" stroke-width="{stroke_width}"{opacity_attr}/>'
elements.append(elem)
return elements
def generate_spiral(
count: int,
start_radius: float,
end_radius: float,
turns: float,
center_x: float,
center_y: float,
element_size: float,
fill: str,
stroke: str,
stroke_width: float,
vary_size: bool,
seed: int,
) -> list:
"""Generate elements in a spiral pattern."""
elements = []
for i in range(count):
t = i / (count - 1) if count > 1 else 0
angle = turns * 2 * math.pi * t - math.pi / 2
radius = start_radius + (end_radius - start_radius) * t
x = center_x + radius * math.cos(angle)
y = center_y + radius * math.sin(angle)
r = seeded_random(seed + i)
size = element_size * (0.5 + t * 0.5) if vary_size else element_size
stroke_attr = f' stroke="{stroke}" stroke-width="{stroke_width}"' if stroke != "none" else ""
elem = f'<circle cx="{x:.1f}" cy="{y:.1f}" r="{size:.1f}" fill="{fill}"{stroke_attr}/>'
elements.append(elem)
return elements
def generate_concentric(
rings: int,
elements_per_ring: int,
min_radius: float,
max_radius: float,
center_x: float,
center_y: float,
element_size: float,
fill: str,
stroke: str,
stroke_width: float,
vary_hue: bool,
base_hue: int,
seed: int,
) -> list:
"""Generate concentric rings of elements."""
elements = []
for ring in range(rings):
t = ring / (rings - 1) if rings > 1 else 0
radius = min_radius + (max_radius - min_radius) * t
count = elements_per_ring + ring * 4 # More elements in outer rings
if vary_hue:
hue = (base_hue + ring * 30) % 360
ring_fill = f"hsl({hue}, 70%, 55%)"
else:
ring_fill = fill
for i in range(count):
angle = (2 * math.pi * i) / count + (ring * 0.2) # Offset each ring
x = center_x + radius * math.cos(angle)
y = center_y + radius * math.sin(angle)
size = element_size * (0.6 + t * 0.4)
stroke_attr = f' stroke="{stroke}" stroke-width="{stroke_width}"' if stroke != "none" else ""
elem = f'<circle cx="{x:.1f}" cy="{y:.1f}" r="{size:.1f}" fill="{ring_fill}"{stroke_attr}/>'
elements.append(elem)
return elements
def generate_sunburst(
count: int,
inner_radius: float,
outer_radius: float,
center_x: float,
center_y: float,
fill: str,
stroke_width: float,
vary_length: bool,
seed: int,
) -> list:
"""Generate sunburst/ray pattern."""
elements = []
for i in range(count):
angle = (2 * math.pi * i) / count - math.pi / 2
r = seeded_random(seed + i)
length = outer_radius * (0.6 + r * 0.4) if vary_length else outer_radius
x1 = center_x + inner_radius * math.cos(angle)
y1 = center_y + inner_radius * math.sin(angle)
x2 = center_x + length * math.cos(angle)
y2 = center_y + length * math.sin(angle)
elem = f'<line x1="{x1:.1f}" y1="{y1:.1f}" x2="{x2:.1f}" y2="{y2:.1f}" stroke="{fill}" stroke-width="{stroke_width}" stroke-linecap="round"/>'
elements.append(elem)
return elements
def main():
parser = argparse.ArgumentParser(description="Generate SVG radial patterns")
# Pattern type
parser.add_argument("--spiral", action="store_true", help="Generate spiral pattern")
parser.add_argument("--concentric", action="store_true", help="Generate concentric rings")
parser.add_argument("--sunburst", action="store_true", help="Generate sunburst pattern")
# Common options
parser.add_argument("-n", "--count", type=int, default=12, help="Number of elements")
parser.add_argument("-r", "--radius", type=float, default=40, help="Radius")
parser.add_argument("--element-size", type=float, default=5, help="Size of each element")
parser.add_argument("--fill", default="#3B82F6", help="Fill color")
parser.add_argument("--stroke", default="none", help="Stroke color")
parser.add_argument("--stroke-width", type=float, default=2, help="Stroke width")
parser.add_argument("--shape", choices=["circle", "rect", "line"], default="circle", help="Shape type")
parser.add_argument("--start-angle", type=float, default=-90, help="Start angle in degrees")
parser.add_argument("--viewbox", type=int, default=100, help="ViewBox size")
# Spiral options
parser.add_argument("--start-radius", type=float, default=10, help="Spiral start radius")
parser.add_argument("--end-radius", type=float, default=45, help="Spiral end radius")
parser.add_argument("--turns", type=float, default=3, help="Number of spiral turns")
# Concentric options
parser.add_argument("--rings", type=int, default=4, help="Number of concentric rings")
parser.add_argument("--elements-per-ring", type=int, default=6, help="Elements in innermost ring")
# Variation options
parser.add_argument("--vary-size", action="store_true", help="Vary element sizes")
parser.add_argument("--vary-opacity", action="store_true", help="Vary element opacity")
parser.add_argument("--vary-hue", action="store_true", help="Vary hue across rings")
parser.add_argument("--vary-length", action="store_true", help="Vary ray length (sunburst)")
parser.add_argument("--base-hue", type=int, default=217, help="Base hue (0-360)")
parser.add_argument("--seed", type=int, default=42, help="Random seed")
parser.add_argument("-o", "--output", help="Output file (default: stdout)")
args = parser.parse_args()
vb = args.viewbox
cx, cy = vb / 2, vb / 2
if args.spiral:
elements = generate_spiral(
count=args.count,
start_radius=args.start_radius,
end_radius=args.end_radius,
turns=args.turns,
center_x=cx,
center_y=cy,
element_size=args.element_size,
fill=args.fill,
stroke=args.stroke,
stroke_width=args.stroke_width,
vary_size=args.vary_size,
seed=args.seed,
)
desc = f"A spiral pattern with {args.count} elements"
elif args.concentric:
elements = generate_concentric(
rings=args.rings,
elements_per_ring=args.elements_per_ring,
min_radius=args.element_size * 3,
max_radius=args.radius,
center_x=cx,
center_y=cy,
element_size=args.element_size,
fill=args.fill,
stroke=args.stroke,
stroke_width=args.stroke_width,
vary_hue=args.vary_hue,
base_hue=args.base_hue,
seed=args.seed,
)
desc = f"Concentric rings with {args.rings} layers"
elif args.sunburst:
elements = generate_sunburst(
count=args.count,
inner_radius=args.element_size * 2,
outer_radius=args.radius,
center_x=cx,
center_y=cy,
fill=args.fill,
stroke_width=args.stroke_width,
vary_length=args.vary_length,
seed=args.seed,
)
desc = f"A sunburst pattern with {args.count} rays"
else:
elements = generate_radial(
count=args.count,
radius=args.radius,
center_x=cx,
center_y=cy,
element_size=args.element_size,
fill=args.fill,
stroke=args.stroke,
stroke_width=args.stroke_width,
shape=args.shape,
start_angle=args.start_angle,
vary_size=args.vary_size,
vary_opacity=args.vary_opacity,
seed=args.seed,
)
desc = f"A radial pattern with {args.count} elements"
svg = f'''<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {vb} {vb}" width="{vb}" height="{vb}">
<title>Radial Pattern</title>
<desc>{desc}</desc>
{chr(10).join(" " + e for e in elements)}
</svg>'''
if args.output:
with open(args.output, "w") as f:
f.write(svg)
print(f"Generated: {args.output}", file=sys.stderr)
else:
print(svg)
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""Generate SVG wave and organic patterns.
Examples:
# Simple sine wave
python generate_wave.py -o wave.svg
# Multiple layered waves
python generate_wave.py --layers 5 --fill
# Noise-based organic wave
python generate_wave.py --noise --amplitude 20
# Sound wave / audio visualization style
python generate_wave.py --bars --count 50
"""
import argparse
import math
import sys
def seeded_random(seed: int) -> float:
"""Deterministic pseudo-random number generator."""
x = math.sin(seed * 9999) * 10000
return x - math.floor(x)
def smooth_noise(x: float, seed: int = 0) -> float:
"""Simple smooth noise function."""
x0 = int(math.floor(x))
x1 = x0 + 1
t = x - x0
smooth = t * t * (3 - 2 * t) # smoothstep
n0 = seeded_random(x0 + seed * 1000)
n1 = seeded_random(x1 + seed * 1000)
return n0 + smooth * (n1 - n0)
def generate_wave_path(
width: float,
amplitude: float,
frequency: float,
phase: float,
y_offset: float,
steps: int,
noise_amount: float,
seed: int,
) -> str:
"""Generate a wave path."""
points = []
for i in range(steps + 1):
t = i / steps
x = t * width
# Base sine wave
y = y_offset + math.sin(t * frequency * math.pi * 2 + phase) * amplitude
# Add noise if specified
if noise_amount > 0:
noise = (smooth_noise(t * 10, seed) - 0.5) * 2 * noise_amount
y += noise
if i == 0:
points.append(f"M{x:.1f},{y:.1f}")
else:
points.append(f"L{x:.1f},{y:.1f}")
return " ".join(points)
def generate_filled_wave(
width: float,
height: float,
amplitude: float,
frequency: float,
phase: float,
y_offset: float,
steps: int,
fill_to_bottom: bool,
) -> str:
"""Generate a filled wave area."""
points = []
for i in range(steps + 1):
t = i / steps
x = t * width
y = y_offset + math.sin(t * frequency * math.pi * 2 + phase) * amplitude
if i == 0:
points.append(f"M{x:.1f},{y:.1f}")
else:
points.append(f"L{x:.1f},{y:.1f}")
if fill_to_bottom:
points.append(f"L{width:.1f},{height:.1f}")
points.append(f"L0,{height:.1f}")
points.append("Z")
return " ".join(points)
def generate_layered_waves(
layers: int,
width: float,
height: float,
base_amplitude: float,
base_frequency: float,
base_hue: int,
fill: bool,
seed: int,
) -> list:
"""Generate multiple layered waves."""
elements = []
for i in range(layers):
t = i / (layers - 1) if layers > 1 else 0
# Each layer has different parameters
y_offset = height * 0.3 + t * height * 0.5
amplitude = base_amplitude * (1 - t * 0.3)
frequency = base_frequency + i * 0.5
phase = i * 0.5
# Color gradient
hue = (base_hue + i * 15) % 360
lightness = 50 + i * 5
opacity = 0.3 + t * 0.4
color = f"hsl({hue}, 70%, {lightness}%)"
if fill:
path_d = generate_filled_wave(
width, height, amplitude, frequency, phase, y_offset, 100, True
)
elements.append(f'<path d="{path_d}" fill="{color}" fill-opacity="{opacity:.2f}"/>')
else:
path_d = generate_wave_path(
width, amplitude, frequency, phase, y_offset, 100, 0, seed + i
)
elements.append(f'<path d="{path_d}" fill="none" stroke="{color}" stroke-width="2" opacity="{opacity:.2f}"/>')
return elements
def generate_noise_wave(
width: float,
height: float,
amplitude: float,
noise_scale: float,
y_offset: float,
steps: int,
stroke: str,
stroke_width: float,
seed: int,
) -> list:
"""Generate organic noise-based wave."""
points = []
for i in range(steps + 1):
t = i / steps
x = t * width
# Layer multiple noise frequencies
noise = 0
noise += (smooth_noise(t * noise_scale, seed) - 0.5) * 2
noise += (smooth_noise(t * noise_scale * 2, seed + 100) - 0.5) * 1
noise += (smooth_noise(t * noise_scale * 4, seed + 200) - 0.5) * 0.5
y = y_offset + noise * amplitude
if i == 0:
points.append(f"M{x:.1f},{y:.1f}")
else:
points.append(f"L{x:.1f},{y:.1f}")
path_d = " ".join(points)
return [f'<path d="{path_d}" fill="none" stroke="{stroke}" stroke-width="{stroke_width}" stroke-linecap="round"/>']
def generate_bar_wave(
count: int,
width: float,
height: float,
max_bar_height: float,
bar_width: float,
gap: float,
fill: str,
vary_height: bool,
seed: int,
) -> list:
"""Generate bar/equalizer style wave visualization."""
elements = []
total_width = count * bar_width + (count - 1) * gap
start_x = (width - total_width) / 2
center_y = height / 2
for i in range(count):
x = start_x + i * (bar_width + gap)
if vary_height:
# Use noise for smooth variation
t = i / count
noise = smooth_noise(t * 5, seed)
bar_h = max_bar_height * (0.2 + noise * 0.8)
else:
# Sine wave pattern
t = i / count
bar_h = max_bar_height * (0.3 + math.sin(t * math.pi * 4) * 0.5 + 0.2)
y = center_y - bar_h / 2
elements.append(
f'<rect x="{x:.1f}" y="{y:.1f}" width="{bar_width:.1f}" height="{bar_h:.1f}" '
f'fill="{fill}" rx="{bar_width / 2:.1f}"/>'
)
return elements
def main():
parser = argparse.ArgumentParser(description="Generate SVG wave patterns")
# Pattern type
parser.add_argument("--layers", type=int, default=0, help="Number of layered waves (0 for single)")
parser.add_argument("--noise", action="store_true", help="Generate noise-based organic wave")
parser.add_argument("--bars", action="store_true", help="Generate bar/equalizer style")
# Wave options
parser.add_argument("--amplitude", type=float, default=15, help="Wave amplitude")
parser.add_argument("--frequency", type=float, default=2, help="Wave frequency")
parser.add_argument("--phase", type=float, default=0, help="Phase offset")
parser.add_argument("--fill", action="store_true", help="Fill waves to bottom")
# Bar options
parser.add_argument("--count", type=int, default=30, help="Number of bars")
parser.add_argument("--bar-width", type=float, default=3, help="Width of each bar")
parser.add_argument("--bar-gap", type=float, default=2, help="Gap between bars")
# Noise options
parser.add_argument("--noise-scale", type=float, default=8, help="Noise scale")
# Common options
parser.add_argument("--width", type=float, default=200, help="SVG width")
parser.add_argument("--height", type=float, default=100, help="SVG height")
parser.add_argument("--stroke", default="#3B82F6", help="Stroke color")
parser.add_argument("--stroke-width", type=float, default=2, help="Stroke width")
parser.add_argument("--base-hue", type=int, default=217, help="Base hue for colors")
parser.add_argument("--seed", type=int, default=42, help="Random seed")
parser.add_argument("-o", "--output", help="Output file (default: stdout)")
args = parser.parse_args()
if args.bars:
elements = generate_bar_wave(
count=args.count,
width=args.width,
height=args.height,
max_bar_height=args.height * 0.7,
bar_width=args.bar_width,
gap=args.bar_gap,
fill=args.stroke,
vary_height=True,
seed=args.seed,
)
title = "Bar Wave"
desc = f"Audio visualization with {args.count} bars"
elif args.noise:
elements = generate_noise_wave(
width=args.width,
height=args.height,
amplitude=args.amplitude,
noise_scale=args.noise_scale,
y_offset=args.height / 2,
steps=200,
stroke=args.stroke,
stroke_width=args.stroke_width,
seed=args.seed,
)
title = "Noise Wave"
desc = "Organic noise-based wave pattern"
elif args.layers > 0:
elements = generate_layered_waves(
layers=args.layers,
width=args.width,
height=args.height,
base_amplitude=args.amplitude,
base_frequency=args.frequency,
base_hue=args.base_hue,
fill=args.fill,
seed=args.seed,
)
title = "Layered Waves"
desc = f"Multiple layered wave pattern with {args.layers} layers"
else:
path_d = generate_wave_path(
width=args.width,
amplitude=args.amplitude,
frequency=args.frequency,
phase=args.phase,
y_offset=args.height / 2,
steps=100,
noise_amount=0,
seed=args.seed,
)
elements = [f'<path d="{path_d}" fill="none" stroke="{args.stroke}" stroke-width="{args.stroke_width}"/>']
title = "Sine Wave"
desc = "Simple sine wave pattern"
svg = f'''<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {args.width} {args.height}" width="{args.width:.0f}" height="{args.height:.0f}">
<title>{title}</title>
<desc>{desc}</desc>
{chr(10).join(" " + e for e in elements)}
</svg>'''
if args.output:
with open(args.output, "w") as f:
f.write(svg)
print(f"Generated: {args.output}", file=sys.stderr)
else:
print(svg)
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""Optimize SVG files for smaller size and better performance.
Examples:
# Basic optimization
python optimize_svg.py input.svg -o output.svg
# Aggressive optimization
python optimize_svg.py input.svg --aggressive
# Optimize and show stats
python optimize_svg.py input.svg --stats
# Process from stdin
cat input.svg | python optimize_svg.py
"""
import argparse
import re
import sys
from typing import Tuple
def remove_comments(svg: str) -> str:
"""Remove XML comments."""
return re.sub(r'<!--[\s\S]*?-->', '', svg)
def remove_metadata(svg: str) -> str:
"""Remove metadata and editor-specific elements."""
# Remove metadata tags
svg = re.sub(r'<metadata[\s\S]*?</metadata>', '', svg, flags=re.IGNORECASE)
# Remove sodipodi, inkscape namespaces
svg = re.sub(r'<sodipodi:[^>]*/?>''', '', svg)
svg = re.sub(r'<inkscape:[^>]*/?>''', '', svg)
# Remove namespace declarations
svg = re.sub(r'\s+xmlns:(sodipodi|inkscape|dc|cc|rdf)="[^"]*"', '', svg)
return svg
def remove_empty_groups(svg: str) -> str:
"""Remove empty <g> elements."""
# Iteratively remove empty groups
prev = ""
while prev != svg:
prev = svg
svg = re.sub(r'<g[^>]*>\s*</g>', '', svg)
return svg
def collapse_whitespace(svg: str) -> str:
"""Collapse excessive whitespace."""
# Collapse multiple spaces/newlines
svg = re.sub(r'\s+', ' ', svg)
# Remove space before closing tags
svg = re.sub(r'\s+/>', '/>', svg)
svg = re.sub(r'\s+>', '>', svg)
# Remove space after opening bracket
svg = re.sub(r'<\s+', '<', svg)
return svg
def minify_whitespace(svg: str) -> str:
"""More aggressive whitespace removal."""
# Remove newlines and excess spaces
svg = re.sub(r'>\s+<', '><', svg)
svg = re.sub(r'\s+', ' ', svg)
return svg.strip()
def round_numbers(svg: str, precision: int = 2) -> str:
"""Round floating point numbers to reduce precision."""
def round_match(match):
num = float(match.group())
if precision == 0:
return str(int(round(num)))
rounded = round(num, precision)
# Remove trailing zeros
result = f"{rounded:.{precision}f}".rstrip('0').rstrip('.')
return result
# Match floating point numbers (including negative)
svg = re.sub(r'-?\d+\.\d+', round_match, svg)
return svg
def shorten_hex_colors(svg: str) -> str:
"""Convert 6-digit hex to 3-digit where possible."""
def shorten(match):
color = match.group(1)
if len(color) == 6:
if color[0] == color[1] and color[2] == color[3] and color[4] == color[5]:
return f"#{color[0]}{color[2]}{color[4]}"
return f"#{color}"
return re.sub(r'#([0-9A-Fa-f]{6})\b', shorten, svg)
def remove_default_attributes(svg: str) -> str:
"""Remove attributes that are set to their default values."""
defaults = [
(r'\s+fill-opacity="1"', ''),
(r'\s+stroke-opacity="1"', ''),
(r'\s+opacity="1"', ''),
(r'\s+stroke="none"', ''),
(r'\s+stroke-width="1"', ''),
(r'\s+fill-rule="nonzero"', ''),
(r'\s+clip-rule="nonzero"', ''),
(r'\s+font-style="normal"', ''),
(r'\s+font-weight="normal"', ''),
(r'\s+x="0"(?=[\s/>])', ''),
(r'\s+y="0"(?=[\s/>])', ''),
(r'\s+cx="0"(?=[\s/>])', ''),
(r'\s+cy="0"(?=[\s/>])', ''),
(r'\s+rx="0"(?=[\s/>])', ''),
(r'\s+ry="0"(?=[\s/>])', ''),
(r'\s+transform="translate\(0[,\s]+0\)"', ''),
(r'\s+transform="translate\(0\)"', ''),
(r'\s+transform="rotate\(0\)"', ''),
(r'\s+transform="scale\(1\)"', ''),
(r'\s+transform="scale\(1[,\s]+1\)"', ''),
]
for pattern, replacement in defaults:
svg = re.sub(pattern, replacement, svg)
return svg
def convert_colors_to_hex(svg: str) -> str:
"""Convert rgb() colors to hex."""
def rgb_to_hex(match):
r, g, b = int(match.group(1)), int(match.group(2)), int(match.group(3))
return f"#{r:02x}{g:02x}{b:02x}"
svg = re.sub(r'rgb\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)', rgb_to_hex, svg)
return svg
def simplify_path_commands(svg: str) -> str:
"""Simplify path data commands."""
def simplify_path(match):
d = match.group(1)
# Remove unnecessary spaces in path commands
d = re.sub(r'([MLHVCSQTAZmlhvcsqtaz])\s+', r'\1', d)
d = re.sub(r'\s+([MLHVCSQTAZmlhvcsqtaz])', r'\1', d)
# Use space instead of comma between numbers
d = re.sub(r',', ' ', d)
# Remove space before negative numbers (they have implicit separator)
d = re.sub(r'\s+(-)', r'\1', d)
# Collapse multiple spaces
d = re.sub(r'\s+', ' ', d)
return f'd="{d.strip()}"'
svg = re.sub(r'd="([^"]*)"', simplify_path, svg)
return svg
def remove_unnecessary_ids(svg: str) -> str:
"""Remove IDs that aren't referenced anywhere."""
# Find all ID definitions
ids = re.findall(r'\bid="([^"]+)"', svg)
# Check each ID for references
for id_val in ids:
# Check for url(#id), href="#id", xlink:href="#id"
patterns = [
f'url\\(#{re.escape(id_val)}\\)',
f'href="#{re.escape(id_val)}"',
f'xlink:href="#{re.escape(id_val)}"',
]
referenced = any(re.search(p, svg) for p in patterns)
if not referenced:
# Remove the id attribute (but keep the element)
svg = re.sub(f'\\s+id="{re.escape(id_val)}"', '', svg)
return svg
def optimize_svg(
svg: str,
aggressive: bool = False,
precision: int = 2,
keep_ids: bool = False,
) -> str:
"""Apply all optimizations to SVG."""
# Basic optimizations (always applied)
svg = remove_comments(svg)
svg = remove_metadata(svg)
svg = remove_empty_groups(svg)
svg = remove_default_attributes(svg)
svg = convert_colors_to_hex(svg)
svg = shorten_hex_colors(svg)
svg = collapse_whitespace(svg)
if not keep_ids:
svg = remove_unnecessary_ids(svg)
# Aggressive optimizations
if aggressive:
svg = round_numbers(svg, precision)
svg = simplify_path_commands(svg)
svg = minify_whitespace(svg)
return svg
def get_size_stats(original: str, optimized: str) -> Tuple[int, int, float]:
"""Calculate size statistics."""
orig_size = len(original.encode('utf-8'))
opt_size = len(optimized.encode('utf-8'))
reduction = (1 - opt_size / orig_size) * 100 if orig_size > 0 else 0
return orig_size, opt_size, reduction
def format_size(size: int) -> str:
"""Format byte size for display."""
if size < 1024:
return f"{size} B"
elif size < 1024 * 1024:
return f"{size / 1024:.1f} KB"
else:
return f"{size / (1024 * 1024):.1f} MB"
def main():
parser = argparse.ArgumentParser(description="Optimize SVG files")
parser.add_argument("input", nargs="?", help="Input SVG file (or stdin if omitted)")
parser.add_argument("-o", "--output", help="Output file (default: stdout)")
parser.add_argument("--aggressive", action="store_true",
help="Apply aggressive optimizations (minification, path simplification)")
parser.add_argument("--precision", type=int, default=2,
help="Decimal precision for numbers (default: 2)")
parser.add_argument("--keep-ids", action="store_true",
help="Keep all IDs even if unreferenced")
parser.add_argument("--stats", action="store_true",
help="Show size statistics")
args = parser.parse_args()
# Read input
if args.input:
with open(args.input, 'r') as f:
svg = f.read()
else:
svg = sys.stdin.read()
# Optimize
optimized = optimize_svg(
svg,
aggressive=args.aggressive,
precision=args.precision,
keep_ids=args.keep_ids,
)
# Output
if args.output:
with open(args.output, 'w') as f:
f.write(optimized)
print(f"Optimized: {args.output}", file=sys.stderr)
else:
if not args.stats:
print(optimized)
# Stats
if args.stats:
orig_size, opt_size, reduction = get_size_stats(svg, optimized)
print(f"\nOptimization Statistics:", file=sys.stderr)
print(f" Original: {format_size(orig_size)}", file=sys.stderr)
print(f" Optimized: {format_size(opt_size)}", file=sys.stderr)
print(f" Reduction: {reduction:.1f}%", file=sys.stderr)
if __name__ == "__main__":
main()
Related skills
How it compares
Pick svg-art over manual SVG editing when developers need CLI-generated icons, charts, or fractals with reproducible seeds and built-in optimization.
FAQ
What scripts does svg-art include?
svg-art bundles eight Python scripts: generate_grid, generate_radial, generate_fractal, generate_wave, generate_particles, generate_chart, generate_icon, and optimize_svg. Each outputs valid SVG to stdout or a file with the -o flag.
How many icons does svg-art provide?
svg-art's generate_icon.py ships 40+ common UI icons accessible via --icon NAME and --list flags. Icons support --filled styling and standard --fill and --stroke color options.
Can svg-art output be reproducible?
svg-art scripts accept a --seed N flag for deterministic random variation across fractals, particles, and pattern generators. Default fill color is #3B82F6 and output pipes to optimize_svg.py for minification.