origin font
This commit is contained in:
128
py/arch_cut.py
Normal file
128
py/arch_cut.py
Normal file
@@ -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)
|
||||
156
py/build_font.py
Normal file
156
py/build_font.py
Normal file
@@ -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)
|
||||
Reference in New Issue
Block a user