diff --git a/aifont.py b/aifont.py deleted file mode 100755 index 5898faa..0000000 --- a/aifont.py +++ /dev/null @@ -1,74 +0,0 @@ -#!/usr/bin/env python3 -import subprocess, sys, os - -# download MesloLGS NF -meslo = "/tmp/MesloLGS NF Regular.ttf" -if not os.path.exists(meslo): - meslo_url = "https://github.com/ryanoasis/nerd-fonts/releases/latest/download/Meslo.tar.xz" - subprocess.run(["curl", "-sL", "-o", "/tmp/Meslo.tar.xz", meslo_url], check=True) - subprocess.run(["tar", "xf", "/tmp/Meslo.tar.xz", "-C", "/tmp", "MesloLGSNerdFont-Regular.ttf"], check=True) - os.rename("/tmp/MesloLGSNerdFont-Regular.ttf", meslo) - -# download SVG icons -base = "https://git.syui.ai/ai/app/raw/branch/main/icon" -for name in ["ai.svg", "syui.svg", "bluesky.svg"]: - subprocess.run(["curl", "-sL", "-o", f"/tmp/{name}", f"{base}/{name}"], check=True) - -subprocess.run(["fontforge", "-script", "/dev/stdin"], input=b""" -import fontforge - -icons = fontforge.font() -icons.em = 1024 - -scale = 200 - -glyph = icons.createChar(0xE001, "ai") -glyph.importOutlines("/tmp/ai.svg") -bb = glyph.boundingBox() -glyph.transform((scale/100.0, 0, 0, scale/100.0, 0, 0)) -glyph.width = 1024 - -glyph = icons.createChar(0xE002, "syui") -glyph.importOutlines("/tmp/syui.svg") -glyph.transform((scale/100.0, 0, 0, scale/100.0, 0, 0)) -glyph.width = 1024 - -glyph = icons.createChar(0xE003, "bluesky") -glyph.importOutlines("/tmp/bluesky.svg") -# center vertically: move to baseline -bb = glyph.boundingBox() -yshift = -bb[1] # move bottom to y=0 -glyph.transform((1, 0, 0, 1, 0, yshift)) -s = scale / 100.0 -glyph.transform((s, 0, 0, s, 0, 0)) -glyph.width = 1024 - -icons.generate("/tmp/icons.ttf") -icons.close() - -font = fontforge.open("/tmp/MesloLGS NF Regular.ttf") -font.selection.select(0xE001) -font.clear() -font.selection.select(0xE002) -font.clear() -font.selection.select(0xE003) -font.clear() -font.selection.none() - -font.mergeFonts("/tmp/icons.ttf") - -font.fontname = "aifont" -font.familyname = "aifont" -font.fullname = "aifont" -font.appendSFNTName("English (US)", "Family", "aifont") -font.appendSFNTName("English (US)", "SubFamily", "Regular") -font.appendSFNTName("English (US)", "UniqueID", "aifont-regular") -font.appendSFNTName("English (US)", "Fullname", "aifont") -font.appendSFNTName("English (US)", "PostScriptName", "aifont") -font.appendSFNTName("English (US)", "Preferred Family", "aifont") -font.appendSFNTName("English (US)", "Preferred Styles", "Regular") - -font.generate("/tmp/aifont.ttf") -font.close() -print("done: /tmp/aifont.ttf") -""", check=True) diff --git a/aifont.ttf b/aifont.ttf index 2b3a5e7..c69e14b 100644 Binary files a/aifont.ttf and b/aifont.ttf differ diff --git a/py/arch_cut.py b/py/arch_cut.py new file mode 100644 index 0000000..65669f7 --- /dev/null +++ b/py/arch_cut.py @@ -0,0 +1,128 @@ +#!/usr/bin/env fontforge +""" +Apply arch cut to uppercase A-Z and digit 2-9. +Arch = planet rising from below. The arc curves UPWARD into the bottom of the glyph. +The cut area is removed (transparent), not filled. +""" +import fontforge +import os +import math + +SRC_DIR = "/tmp/aifont_meslo" +DST_DIR = "/tmp/aifont_arch" +os.makedirs(DST_DIR, exist_ok=True) + +chars = [] +for c in range(ord('A'), ord('Z')+1): + chars.append(chr(c)) +for c in range(ord('0'), ord('9')+1): + chars.append(chr(c)) + +# Skip arch for round-bottom characters +skip_arch = set() + +WIDTH = 602 # Meslo monospace width + +workfont = fontforge.font() +workfont.em = 1000 +workfont.encoding = "UnicodeFull" + +for g in chars: + cp = ord(g) + src = os.path.join(SRC_DIR, g + ".svg") + if not os.path.exists(src): + continue + + glyph = workfont.createChar(cp, g) + glyph.importOutlines(src) + glyph.width = WIDTH + + if g not in skip_arch: + bb = glyph.boundingBox() + xmin, ymin, xmax, ymax = bb + h = ymax - ymin + w = xmax - xmin + + # How much of the glyph bottom to cut (15%) + cut_depth = h * 0.07 + + # Large arc radius - the "planet" rising from below + # Larger radius = gentler curve + radius = w * 2.5 + + # The arc center is below the glyph + cx = (xmin + xmax) / 2.0 + # Position so the top of the arc reaches cut_depth into the glyph + cy = ymin - radius + cut_depth + + # We need to intersect the glyph with the area ABOVE the arc. + # Strategy: create a large clockwise (additive) shape representing + # "everything above the arc line", then intersect with the glyph. + + # Step 1: Clear glyph, we'll rebuild via intersection + # Save current contours by using a temp approach + + # Better strategy using fontforge: + # 1. Create a second glyph with a huge filled area that has the arc as its bottom edge + # 2. Use intersect to keep only where glyph and mask overlap + + # Create mask glyph: a large rectangle with arc-shaped bottom + mask_cp = 0xE900 + mask = workfont.createChar(mask_cp, "mask") + mask.clear() + + margin = 200 + left = xmin - margin + right = xmax + margin + top = ymax + margin + + # The arc: we need points along the arc from left to right + # Arc equation: (x - cx)^2 + (y - cy)^2 = radius^2 + # y = cy + sqrt(radius^2 - (x-cx)^2) (upper half of circle) + + # Calculate arc endpoints + def arc_y(x): + dx = x - cx + if abs(dx) > radius: + return ymin + return cy + math.sqrt(radius * radius - dx * dx) + + # Generate points along the arc + n_points = 40 + arc_points = [] + for idx in range(n_points + 1): + x = left + (right - left) * idx / n_points + y = arc_y(x) + arc_points.append((x, y)) + + pen = mask.glyphPen() + # Draw clockwise: top-left -> top-right -> arc from right to left -> close + pen.moveTo((left, top)) + pen.lineTo((right, top)) + # Go down right side to arc start + pen.lineTo((right, arc_points[-1][1])) + # Follow arc from right to left (reverse order) + for idx in range(n_points - 1, -1, -1): + pen.lineTo(arc_points[idx]) + pen.closePath() + pen = None + + # Now intersect: keep only the part of glyph inside the mask + glyph.correctDirection() + mask.correctDirection() + + # Copy mask into glyph's layer and intersect + workfont.selection.select(mask_cp) + workfont.copy() + workfont.selection.select(cp) + workfont.pasteInto() + glyph.intersect() + + # Clean up mask + mask.clear() + + dst = os.path.join(DST_DIR, g + ".svg") + glyph.export(dst) + print("Done: " + g) + +print("\nAll done! Results: " + DST_DIR) diff --git a/py/build_font.py b/py/build_font.py new file mode 100644 index 0000000..2c92f61 --- /dev/null +++ b/py/build_font.py @@ -0,0 +1,156 @@ +#!/usr/bin/env fontforge +""" +Build aifont.ttf: +- Base: Meslo LGS NF Regular +- Replace A-Z with arch-cut versions +- Replace 0-9 with arch-cut versions +- Replace a, i, o, s with custom ai-world designs +- Keep everything else (other lowercase, symbols, nerd font icons) +""" +import fontforge +import os + +BASE_FONT = "/tmp/MesloLGS NF Regular.ttf" +UPPER_DIR = "/tmp/aifont_build/upper" +DIGIT_DIR = "/tmp/aifont_build/digit" +LOWER_DIR = "/tmp/aifont_build/lower" +OUTPUT = "/tmp/aifont.ttf" + +font = fontforge.open(BASE_FONT) + +# Replace uppercase A-Z +for c in range(ord('A'), ord('Z')+1): + g = chr(c) + svg = os.path.join(UPPER_DIR, g + ".svg") + if os.path.exists(svg): + glyph = font[c] + w = glyph.width + glyph.clear() + glyph.importOutlines(svg) + glyph.width = w + glyph.correctDirection() + print("Upper: " + g) + +# Replace digits 0-9 +for c in range(ord('0'), ord('9')+1): + g = chr(c) + svg = os.path.join(DIGIT_DIR, g + ".svg") + if os.path.exists(svg): + glyph = font[c] + w = glyph.width + glyph.clear() + glyph.importOutlines(svg) + glyph.width = w + glyph.correctDirection() + print("Digit: " + g) + +# Replace lowercase a, i, o, s +# These have different coordinate systems, so center them after import +import psMat + +for g in ['a', 'i']: + c = ord(g) + svg = os.path.join(LOWER_DIR, g + ".svg") + if os.path.exists(svg): + glyph = font[c] + target_w = glyph.width + glyph.clear() + glyph.importOutlines(svg) + glyph.correctDirection() + + # Get bounding box and center horizontally + bb = glyph.boundingBox() + xmin, ymin, xmax, ymax = bb + glyph_w = xmax - xmin + glyph_h = ymax - ymin + + # Scale per glyph + if g == 'i': + target_h = 1500 + elif g == 'a': + target_h = 1400 + else: + target_h = 1200 + if glyph_h > 0: + scale = target_h / glyph_h + glyph.transform(psMat.scale(scale)) + bb = glyph.boundingBox() + xmin, ymin, xmax, ymax = bb + glyph_w = xmax - xmin + + # Center horizontally within the glyph width + x_offset = (target_w - glyph_w) / 2.0 - xmin + # Align baseline + y_offset = -ymin + glyph.transform(psMat.translate(x_offset, y_offset)) + glyph.width = target_w + + # Thin down a, o, s (not i) - scale horizontally to 75% + if g in ['a', 'o', 's']: + bb = glyph.boundingBox() + xmin, ymin, xmax, ymax = bb + cx = (xmin + xmax) / 2.0 + # Scale X to 75%, keep Y + glyph.transform(psMat.compose( + psMat.translate(-cx, 0), + psMat.compose( + psMat.scale(0.75, 1.0), + psMat.translate(cx, 0) + ) + )) + # Re-center + bb = glyph.boundingBox() + xmin, ymin, xmax, ymax = bb + glyph_w = xmax - xmin + x_offset = (target_w - glyph_w) / 2.0 - xmin + glyph.transform(psMat.translate(x_offset, 0)) + glyph.width = target_w + + print("Lower: " + g) + +# Re-import custom icons at E001-E003 (same approach as original aifont.py) +# Build icon font separately, then merge +ICON_DIR = "/tmp/aifont_build" +scale = 200 # same as original + +icons_font = fontforge.font() +icons_font.em = 1024 + +for cp, name, has_yshift in [(0xE001, "ai.svg", False), (0xE002, "syui.svg", False), (0xE003, "bluesky.svg", True)]: + svg = os.path.join(ICON_DIR, name) + if os.path.exists(svg): + glyph = icons_font.createChar(cp, name.replace(".svg", "")) + glyph.importOutlines(svg) + if has_yshift: + bb = glyph.boundingBox() + yshift = -bb[1] + glyph.transform((1, 0, 0, 1, 0, yshift)) + s = scale / 100.0 + glyph.transform((s, 0, 0, s, 0, 0)) + glyph.width = 1024 + print("Icon: " + name) + +icons_font.generate("/tmp/aifont_icons.ttf") +icons_font.close() + +# Clear existing icon slots and merge +for cp in [0xE001, 0xE002, 0xE003]: + font.selection.select(cp) + font.clear() +font.selection.none() +font.mergeFonts("/tmp/aifont_icons.ttf") + +# Set font metadata +font.fontname = "aifont" +font.familyname = "aifont" +font.fullname = "aifont" +font.appendSFNTName("English (US)", "Family", "aifont") +font.appendSFNTName("English (US)", "SubFamily", "Regular") +font.appendSFNTName("English (US)", "UniqueID", "aifont-regular") +font.appendSFNTName("English (US)", "Fullname", "aifont") +font.appendSFNTName("English (US)", "PostScriptName", "aifont") +font.appendSFNTName("English (US)", "Preferred Family", "aifont") +font.appendSFNTName("English (US)", "Preferred Styles", "Regular") + +font.generate(OUTPUT) +print("\nDone: " + OUTPUT) diff --git a/svg/digit/0.svg b/svg/digit/0.svg new file mode 100644 index 0000000..bb0585b --- /dev/null +++ b/svg/digit/0.svg @@ -0,0 +1,10 @@ + + + + + diff --git a/svg/digit/1.svg b/svg/digit/1.svg new file mode 100644 index 0000000..7c3fc4e --- /dev/null +++ b/svg/digit/1.svg @@ -0,0 +1,7 @@ + + + + + diff --git a/svg/digit/2.svg b/svg/digit/2.svg new file mode 100644 index 0000000..20fe9e8 --- /dev/null +++ b/svg/digit/2.svg @@ -0,0 +1,10 @@ + + + + + diff --git a/svg/digit/3.svg b/svg/digit/3.svg new file mode 100644 index 0000000..db41e16 --- /dev/null +++ b/svg/digit/3.svg @@ -0,0 +1,11 @@ + + + + + diff --git a/svg/digit/4.svg b/svg/digit/4.svg new file mode 100644 index 0000000..1b50021 --- /dev/null +++ b/svg/digit/4.svg @@ -0,0 +1,6 @@ + + + + + diff --git a/svg/digit/5.svg b/svg/digit/5.svg new file mode 100644 index 0000000..c2fbd33 --- /dev/null +++ b/svg/digit/5.svg @@ -0,0 +1,10 @@ + + + + + diff --git a/svg/digit/6.svg b/svg/digit/6.svg new file mode 100644 index 0000000..0eaab68 --- /dev/null +++ b/svg/digit/6.svg @@ -0,0 +1,11 @@ + + + + + diff --git a/svg/digit/7.svg b/svg/digit/7.svg new file mode 100644 index 0000000..a893510 --- /dev/null +++ b/svg/digit/7.svg @@ -0,0 +1,6 @@ + + + + + diff --git a/svg/digit/8.svg b/svg/digit/8.svg new file mode 100644 index 0000000..5ee31c1 --- /dev/null +++ b/svg/digit/8.svg @@ -0,0 +1,12 @@ + + + + + diff --git a/svg/digit/9.svg b/svg/digit/9.svg new file mode 100644 index 0000000..550be1a --- /dev/null +++ b/svg/digit/9.svg @@ -0,0 +1,10 @@ + + + + + diff --git a/svg/digit/Q.svg b/svg/digit/Q.svg new file mode 100644 index 0000000..e032853 --- /dev/null +++ b/svg/digit/Q.svg @@ -0,0 +1,9 @@ + + + + + diff --git a/svg/digit/U.svg b/svg/digit/U.svg new file mode 100644 index 0000000..0327b37 --- /dev/null +++ b/svg/digit/U.svg @@ -0,0 +1,9 @@ + + + + + diff --git a/svg/lower/a.svg b/svg/lower/a.svg new file mode 100644 index 0000000..ebdc0b7 --- /dev/null +++ b/svg/lower/a.svg @@ -0,0 +1,19 @@ + + + diff --git a/svg/lower/i.svg b/svg/lower/i.svg new file mode 100644 index 0000000..586ee07 --- /dev/null +++ b/svg/lower/i.svg @@ -0,0 +1,20 @@ + + + + + + + + diff --git a/svg/lower/o.svg b/svg/lower/o.svg new file mode 100644 index 0000000..b8d3da6 --- /dev/null +++ b/svg/lower/o.svg @@ -0,0 +1,17 @@ + + + + diff --git a/svg/lower/s.svg b/svg/lower/s.svg new file mode 100644 index 0000000..0e92b81 --- /dev/null +++ b/svg/lower/s.svg @@ -0,0 +1,32 @@ + + + + + + + + diff --git a/svg/upper/0.svg b/svg/upper/0.svg new file mode 100644 index 0000000..bb0585b --- /dev/null +++ b/svg/upper/0.svg @@ -0,0 +1,10 @@ + + + + + diff --git a/svg/upper/6.svg b/svg/upper/6.svg new file mode 100644 index 0000000..0eaab68 --- /dev/null +++ b/svg/upper/6.svg @@ -0,0 +1,11 @@ + + + + + diff --git a/svg/upper/8.svg b/svg/upper/8.svg new file mode 100644 index 0000000..5ee31c1 --- /dev/null +++ b/svg/upper/8.svg @@ -0,0 +1,12 @@ + + + + + diff --git a/svg/upper/9.svg b/svg/upper/9.svg new file mode 100644 index 0000000..550be1a --- /dev/null +++ b/svg/upper/9.svg @@ -0,0 +1,10 @@ + + + + + diff --git a/svg/upper/A.svg b/svg/upper/A.svg new file mode 100644 index 0000000..a839f20 --- /dev/null +++ b/svg/upper/A.svg @@ -0,0 +1,7 @@ + + + + + diff --git a/svg/upper/B.svg b/svg/upper/B.svg new file mode 100644 index 0000000..1159e11 --- /dev/null +++ b/svg/upper/B.svg @@ -0,0 +1,10 @@ + + + + + diff --git a/svg/upper/C.svg b/svg/upper/C.svg new file mode 100644 index 0000000..5873f4f --- /dev/null +++ b/svg/upper/C.svg @@ -0,0 +1,9 @@ + + + + + diff --git a/svg/upper/D.svg b/svg/upper/D.svg new file mode 100644 index 0000000..44c085a --- /dev/null +++ b/svg/upper/D.svg @@ -0,0 +1,8 @@ + + + + + diff --git a/svg/upper/E.svg b/svg/upper/E.svg new file mode 100644 index 0000000..1c58b88 --- /dev/null +++ b/svg/upper/E.svg @@ -0,0 +1,8 @@ + + + + + diff --git a/svg/upper/F.svg b/svg/upper/F.svg new file mode 100644 index 0000000..ea2c9d1 --- /dev/null +++ b/svg/upper/F.svg @@ -0,0 +1,6 @@ + + + + + diff --git a/svg/upper/G.svg b/svg/upper/G.svg new file mode 100644 index 0000000..9d4b1c9 --- /dev/null +++ b/svg/upper/G.svg @@ -0,0 +1,9 @@ + + + + + diff --git a/svg/upper/H.svg b/svg/upper/H.svg new file mode 100644 index 0000000..964cf6d --- /dev/null +++ b/svg/upper/H.svg @@ -0,0 +1,7 @@ + + + + + diff --git a/svg/upper/I.svg b/svg/upper/I.svg new file mode 100644 index 0000000..3c12064 --- /dev/null +++ b/svg/upper/I.svg @@ -0,0 +1,8 @@ + + + + + diff --git a/svg/upper/J.svg b/svg/upper/J.svg new file mode 100644 index 0000000..ad586f1 --- /dev/null +++ b/svg/upper/J.svg @@ -0,0 +1,8 @@ + + + + + diff --git a/svg/upper/K.svg b/svg/upper/K.svg new file mode 100644 index 0000000..3d78fe8 --- /dev/null +++ b/svg/upper/K.svg @@ -0,0 +1,7 @@ + + + + + diff --git a/svg/upper/L.svg b/svg/upper/L.svg new file mode 100644 index 0000000..4c17464 --- /dev/null +++ b/svg/upper/L.svg @@ -0,0 +1,7 @@ + + + + + diff --git a/svg/upper/M.svg b/svg/upper/M.svg new file mode 100644 index 0000000..f12b362 --- /dev/null +++ b/svg/upper/M.svg @@ -0,0 +1,7 @@ + + + + + diff --git a/svg/upper/N.svg b/svg/upper/N.svg new file mode 100644 index 0000000..dcb8576 --- /dev/null +++ b/svg/upper/N.svg @@ -0,0 +1,7 @@ + + + + + diff --git a/svg/upper/O.svg b/svg/upper/O.svg new file mode 100644 index 0000000..7d5ae41 --- /dev/null +++ b/svg/upper/O.svg @@ -0,0 +1,9 @@ + + + + + diff --git a/svg/upper/P.svg b/svg/upper/P.svg new file mode 100644 index 0000000..b066463 --- /dev/null +++ b/svg/upper/P.svg @@ -0,0 +1,7 @@ + + + + + diff --git a/svg/upper/Q.svg b/svg/upper/Q.svg new file mode 100644 index 0000000..e032853 --- /dev/null +++ b/svg/upper/Q.svg @@ -0,0 +1,9 @@ + + + + + diff --git a/svg/upper/R.svg b/svg/upper/R.svg new file mode 100644 index 0000000..5740ab8 --- /dev/null +++ b/svg/upper/R.svg @@ -0,0 +1,9 @@ + + + + + diff --git a/svg/upper/S.svg b/svg/upper/S.svg new file mode 100644 index 0000000..da4512d --- /dev/null +++ b/svg/upper/S.svg @@ -0,0 +1,11 @@ + + + + + diff --git a/svg/upper/T.svg b/svg/upper/T.svg new file mode 100644 index 0000000..69a5444 --- /dev/null +++ b/svg/upper/T.svg @@ -0,0 +1,6 @@ + + + + + diff --git a/svg/upper/U.svg b/svg/upper/U.svg new file mode 100644 index 0000000..0327b37 --- /dev/null +++ b/svg/upper/U.svg @@ -0,0 +1,9 @@ + + + + + diff --git a/svg/upper/V.svg b/svg/upper/V.svg new file mode 100644 index 0000000..adc96e2 --- /dev/null +++ b/svg/upper/V.svg @@ -0,0 +1,6 @@ + + + + + diff --git a/svg/upper/W.svg b/svg/upper/W.svg new file mode 100644 index 0000000..35f0d75 --- /dev/null +++ b/svg/upper/W.svg @@ -0,0 +1,7 @@ + + + + + diff --git a/svg/upper/X.svg b/svg/upper/X.svg new file mode 100644 index 0000000..10d1866 --- /dev/null +++ b/svg/upper/X.svg @@ -0,0 +1,7 @@ + + + + + diff --git a/svg/upper/Y.svg b/svg/upper/Y.svg new file mode 100644 index 0000000..279acb6 --- /dev/null +++ b/svg/upper/Y.svg @@ -0,0 +1,6 @@ + + + + + diff --git a/svg/upper/Z.svg b/svg/upper/Z.svg new file mode 100644 index 0000000..dfa6a59 --- /dev/null +++ b/svg/upper/Z.svg @@ -0,0 +1,8 @@ + + + + +