Files
CleanMM/scripts/generate_icon.py
zhukang 96168fa24f feat: update README assets, refine feature views and design tokens
Replace incorrect README icon with actual Atlas app icon, add new
screenshots (about, settings, privilege), improve feature view
responsiveness and empty states, adjust design system brand tokens
and localization strings, add icon generation script.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 16:40:28 +08:00

230 lines
8.3 KiB
Python

#!/usr/bin/env python3
"""
Atlas for Mac — App Icon Generator
Brand: Calm Authority
Concept: A stylized globe with meridian lines overlaid on a deep-teal-to-emerald
gradient, with a darker premium backdrop and a refined mint accent arc representing the "atlas" mapping metaphor.
Generates all required macOS app icon sizes from a programmatic SVG.
"""
import subprocess
import os
import json
import tempfile
ICON_DIR = os.path.join(
os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
"Apps", "AtlasApp", "Sources", "AtlasApp", "Assets.xcassets", "AppIcon.appiconset"
)
# macOS icon sizes needed
SIZES = [16, 32, 64, 128, 256, 512, 1024]
def generate_svg(size=1024):
"""Generate the Atlas app icon as SVG."""
return f'''<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {size} {size}" width="{size}" height="{size}">
<defs>
<!-- Brand gradient: darker premium teal to deep emerald -->
<linearGradient id="bgGrad" x1="0" y1="0" x2="1" y2="1">
<stop offset="0%" stop-color="#031B1A"/>
<stop offset="50%" stop-color="#0A5C56"/>
<stop offset="100%" stop-color="#073936"/>
</linearGradient>
<!-- Inner glow from top-left -->
<radialGradient id="innerGlow" cx="0.3" cy="0.25" r="0.7">
<stop offset="0%" stop-color="#D1FAE5" stop-opacity="0.16"/>
<stop offset="100%" stop-color="white" stop-opacity="0"/>
</radialGradient>
<!-- Globe gradient -->
<radialGradient id="globeGrad" cx="0.4" cy="0.35" r="0.55">
<stop offset="0%" stop-color="#A7F3D0" stop-opacity="0.38"/>
<stop offset="60%" stop-color="#5EEAD4" stop-opacity="0.22"/>
<stop offset="100%" stop-color="#0A5C56" stop-opacity="0.10"/>
</radialGradient>
<!-- Mint accent gradient -->
<linearGradient id="mintGrad" x1="0" y1="0" x2="1" y2="0.5">
<stop offset="0%" stop-color="#D1FAE5" stop-opacity="0.98"/>
<stop offset="100%" stop-color="#6EE7B7" stop-opacity="0.82"/>
</linearGradient>
<!-- Clip to rounded square -->
<clipPath id="roundClip">
<rect x="0" y="0" width="{size}" height="{size}" rx="{int(size * 0.22)}" ry="{int(size * 0.22)}"/>
</clipPath>
</defs>
<g clip-path="url(#roundClip)">
<!-- Background -->
<rect width="{size}" height="{size}" fill="url(#bgGrad)"/>
<rect width="{size}" height="{size}" fill="url(#innerGlow)"/>
<!-- Globe circle -->
<circle cx="{size//2}" cy="{size//2}" r="{int(size * 0.32)}"
fill="url(#globeGrad)" stroke="#CCFBF1" stroke-width="{max(1, size//256)}" stroke-opacity="0.24"/>
<!-- Meridian lines (longitude) -->
<g fill="none" stroke="#CCFBF1" stroke-width="{max(1, size//512)}" stroke-opacity="0.24">
<!-- Vertical center line -->
<line x1="{size//2}" y1="{int(size*0.18)}" x2="{size//2}" y2="{int(size*0.82)}"/>
<!-- Elliptical meridians -->
<ellipse cx="{size//2}" cy="{size//2}" rx="{int(size*0.12)}" ry="{int(size*0.32)}"/>
<ellipse cx="{size//2}" cy="{size//2}" rx="{int(size*0.24)}" ry="{int(size*0.32)}"/>
</g>
<!-- Latitude lines (horizontal) -->
<g fill="none" stroke="#CCFBF1" stroke-width="{max(1, size//512)}" stroke-opacity="0.18">
<line x1="{int(size*0.18)}" y1="{size//2}" x2="{int(size*0.82)}" y2="{size//2}"/>
<ellipse cx="{size//2}" cy="{size//2}" rx="{int(size*0.32)}" ry="{int(size*0.12)}"/>
<ellipse cx="{size//2}" cy="{size//2}" rx="{int(size*0.32)}" ry="{int(size*0.22)}"/>
</g>
<!-- Mint accent arc — the "mapping" highlight -->
<path d="M {int(size*0.28)} {int(size*0.58)}
Q {int(size*0.5)} {int(size*0.35)}, {int(size*0.72)} {int(size*0.42)}"
fill="none" stroke="url(#mintGrad)" stroke-width="{max(2, int(size*0.018))}"
stroke-linecap="round" stroke-opacity="0.92"/>
<!-- Small mint dot at arc start -->
<circle cx="{int(size*0.28)}" cy="{int(size*0.58)}" r="{max(2, int(size*0.009))}"
fill="#A7F3D0" opacity="0.95"/>
<!-- Small mint dot at arc end -->
<circle cx="{int(size*0.72)}" cy="{int(size*0.42)}" r="{max(2, int(size*0.009))}"
fill="#A7F3D0" opacity="0.95"/>
<!-- Subtle sparkle at top-right of globe -->
<g transform="translate({int(size*0.62)}, {int(size*0.28)})" opacity="0.5">
<line x1="0" y1="-{int(size*0.02)}" x2="0" y2="{int(size*0.02)}"
stroke="white" stroke-width="{max(1, size//512)}" stroke-linecap="round"/>
<line x1="-{int(size*0.02)}" y1="0" x2="{int(size*0.02)}" y2="0"
stroke="white" stroke-width="{max(1, size//512)}" stroke-linecap="round"/>
</g>
<!-- Bottom subtle reflection -->
<rect x="0" y="{int(size*0.75)}" width="{size}" height="{int(size*0.25)}"
fill="url(#bgGrad)" opacity="0.3"/>
</g>
</svg>'''
def main():
os.makedirs(ICON_DIR, exist_ok=True)
# Write SVG
svg_content = generate_svg(1024)
svg_path = os.path.join(ICON_DIR, "icon_1024.svg")
with open(svg_path, "w") as f:
f.write(svg_content)
print(f"SVG written to {svg_path}")
# Try to convert to PNG using sips (built-in macOS tool) via a temp file
# First, check if we have rsvg-convert or cairosvg
converters = []
# Check for rsvg-convert (from librsvg)
if subprocess.run(["which", "rsvg-convert"], capture_output=True).returncode == 0:
converters.append("rsvg-convert")
# Check for python cairosvg
try:
import cairosvg
converters.append("cairosvg")
except ImportError:
pass
# Check for Inkscape
if subprocess.run(["which", "inkscape"], capture_output=True).returncode == 0:
converters.append("inkscape")
images = {}
if "rsvg-convert" in converters:
print("Using rsvg-convert for PNG generation...")
for s in SIZES:
out = os.path.join(ICON_DIR, f"icon_{s}x{s}.png")
subprocess.run([
"rsvg-convert", "-w", str(s), "-h", str(s),
svg_path, "-o", out
], check=True)
images[f"icon_{s}x{s}.png"] = s
print(f" Generated {s}x{s}")
elif "cairosvg" in converters:
print("Using cairosvg for PNG generation...")
import cairosvg
for s in SIZES:
out = os.path.join(ICON_DIR, f"icon_{s}x{s}.png")
cairosvg.svg2png(
bytestring=svg_content.encode(),
write_to=out,
output_width=s,
output_height=s
)
images[f"icon_{s}x{s}.png"] = s
print(f" Generated {s}x{s}")
elif "inkscape" in converters:
print("Using Inkscape for PNG generation...")
for s in SIZES:
out = os.path.join(ICON_DIR, f"icon_{s}x{s}.png")
subprocess.run([
"inkscape", svg_path,
"--export-type=png",
f"--export-filename={out}",
f"--export-width={s}",
f"--export-height={s}"
], check=True, capture_output=True)
images[f"icon_{s}x{s}.png"] = s
print(f" Generated {s}x{s}")
else:
print("WARNING: No SVG-to-PNG converter found.")
print("Install one of: librsvg (brew install librsvg), cairosvg (pip install cairosvg), or Inkscape")
print(f"Then run: cd {ICON_DIR} && rsvg-convert -w 1024 -h 1024 icon_1024.svg -o icon_1024x1024.png")
print("SVG file is ready for manual conversion.")
# Still write Contents.json with expected filenames
for s in SIZES:
images[f"icon_{s}x{s}.png"] = s
# Write Contents.json for Xcode
icon_images = []
for s in [16, 32, 128, 256, 512]:
# 1x
icon_images.append({
"filename": f"icon_{s}x{s}.png",
"idiom": "mac",
"scale": "1x",
"size": f"{s}x{s}"
})
# 2x
icon_images.append({
"filename": f"icon_{s*2}x{s*2}.png",
"idiom": "mac",
"scale": "2x",
"size": f"{s}x{s}"
})
contents = {
"images": icon_images,
"info": {
"author": "atlas-icon-generator",
"version": 1
}
}
contents_path = os.path.join(ICON_DIR, "Contents.json")
with open(contents_path, "w") as f:
json.dump(contents, f, indent=2)
print(f"Contents.json written to {contents_path}")
print("Done!")
if __name__ == "__main__":
main()