Teks yang melingkar terlihat seperti trik menggambar, padahal itu tata letak teks biasa dengan satu perbedaan: alih-alih bergerak di garis dasar yang lurus, setiap huruf bergerak di sepanjang busur dan berputar menghadap ke luar. Type on a Path Tool di Vector Editor pada aplikasi desktop osFoundry melakukan hal ini, begitu juga <textPath> di SVG. Di bawah ini tata letak yang sama ditulis dengan Python memakai fontTools, numpy, dan Pillow (sekitar 150 baris termasuk bagian menggambar), lalu dijalankan pada stempel yang dibuat di aplikasi (KUMO COFFEE ROASTERS di atas lingkaran 450.6 px, EST. 2026 di bagian bawah, teks tebal 40 px). Ada tiga hal yang diukur: bagaimana jarak antarhuruf dibandingkan dengan memutar huruf dengan sudut yang sama, kenapa teks rata tengah muncul di sisi berlawanan dari titik yang diklik, dan di mana baris bawah berakhir setelah Flip dan Ascender.
Teks di aplikasi memakai Source Sans 3. Skripnya memakai Arial Bold bawaan macOS, karena itu font tebal yang tersedia, jadi lebar hurufnya berbeda dari aplikasi; geometrinya tidak.
Lebar advance, bukan sudut
Font memberi setiap glyph lebar advance (advance width), yaitu jarak pena bergeser setelah menggambar glyph itu. Teks lurus disusun dengan menjumlahkan nilai-nilai itu. Di lingkaran caranya persis sama, dan total berjalannya diperlakukan sebagai jarak di sepanjang busur.
from fontTools.ttLib import TTFont
font = TTFont('/System/Library/Fonts/Supplemental/Arial Bold.ttf')
cmap, hmtx, glyf = font.getBestCmap(), font['hmtx'], font['glyf']
k = 40.0 / font['head'].unitsPerEm # 40 px text
R = 225.3 # the app's circle: 450.6 px wide
def layout(text):
s, out = 0.0, []
for ch in text:
name = cmap[ord(ch)]
adv = hmtx[name][0] * k
g = glyf[name]
box = (g.xMin * k, g.xMax * k) if g.numberOfContours else None # ink, None for a space
out.append((ch, s, adv, box))
s += adv
return out, s
Glyph yang titik tengahnya berada pada jarak s di sepanjang path digambar pada sudut s / R radian, dan diputar sebesar sudut yang sama. Untuk KUMO COFFEE ROASTERS totalnya 528.9 px path, atau 134.5 derajat dari lingkaran berjari-jari 225.3 px.
Cara yang biasa dipakai saat mengerjakan manual berbeda: setiap karakter diputar dengan langkah yang sama. Supaya perbandingannya adil, versi sudut sama saya beri 134.5 derajat yang sama, dibagi menjadi 20 slot sama besar, satu per karakter (termasuk spasi), lalu saya ukur celah antara tinta huruf-huruf yang bersebelahan di sepanjang garis dasar:
| huruf | berdasarkan lebar advance | berdasarkan sudut sama |
|---|---|---|
| K U | 2.9 px | 0.5 px |
| M O | 4.6 px | -1.2 px |
| O C (spasi antarkata) | 14.6 px | 26.4 px |
| F F | 4.8 px | 6.8 px |
| E R (spasi antarkata) | 16.0 px | 30.0 px |
| R O | 2.0 px | -1.6 px |
| O A | 1.6 px | -2.0 px |
Di dalam kata, tata letak berdasarkan lebar advance menghasilkan celah 1.6 sampai 6.0 px, yang tidak lain adalah jarak yang dirancang oleh desainer huruf Arial. Dengan sudut sama hasilnya -2.0 sampai 6.8 px: celah negatif berarti huruf saling bersentuhan atau bertumpuk, dan itu terjadi di sebelah huruf lebar M, O, dan A. Spasi antarkata hampir dua kali lipat, dari 14.6 dan 16.0 px menjadi 26.4 dan 30.0 px, karena spasi itu sempit tetapi mendapat satu slot penuh. Itulah tampilan tidak rata dari huruf yang diputar manual, dan sumbernya adalah aturannya sendiri, bukan satu huruf tertentu.
Kenapa teks rata tengah melompat ke sisi seberang
Pada path terbuka, teks rata tengah berada di tengah path. Lingkaran adalah path tertutup, jadi "tengah" bergantung pada titik awalnya. Di aplikasi, path dimulai di titik yang diklik, sehingga tengahnya berada setengah keliling dari situ:
C = 2 * math.pi * R # 1415.6 px
click = 180.0 # the bottom of the circle, degrees clockwise from the top
mid = (click + math.degrees((C / 2) / R)) % 360 # 0: the top
Teks lalu ditempatkan dari mid - width / 2 sampai mid + width / 2. Saya memeriksanya di aplikasi dengan mengukur piksel gelap pada screenshot di sekitar pusat lingkaran (screenshot-nya selebar 2880 px, zoom 66.57 % di layar 2x, jadi satu piksel dokumen sama dengan 1.33 piksel screenshot). Nama toko, yang diketik setelah mengklik bagian bawah, membentang dari -55.8 sampai +56.0 derajat dari arah atas, jadi titik tengahnya hanya meleset 0.1 derajat dari tegak lurus ke atas. Baris kedua, yang diketik setelah mengklik bagian atas, berpusat 0.1 derajat dari tegak lurus ke bawah. Itulah sebabnya di langkah tutorial, baris atas dibuat dengan mengklik bagian bawah, dan baris bawah dengan mengklik bagian atas.
Flip, dan selisih yang ditinggalkan Ascender
Searah jarum jam, garis singgung di bagian bawah lingkaran menunjuk ke kiri, jadi teks yang mengikuti path di situ terbaca dari kanan ke kiri, terbalik. Flip membalik arah dan memindahkan glyph ke sisi lain path. Di kode, itu berarti arah negatif dan normal yang mengarah ke dalam:
phi = phi0 + direction * (s + adv / 2) / radius # glyph centre angle
rot = phi if direction > 0 else phi - math.pi # flipped glyphs turn 180 degrees
tx, ty = math.cos(rot), math.sin(rot) # along the text
ox, oy = math.sin(phi), -math.cos(phi) # outward from the centre (y points down)
# each outline point (gx, gy) in glyph space, centred on the glyph:
px = cx + radius * ox + gx * tx + n * gy * ox # n = +1 outward, -1 inward
py = cy + radius * oy + gx * ty + n * gy * oy
Jika garis dasarnya tetap di lingkaran, huruf yang dibalik tumbuh ke arah pusat, masuk ke dalam lingkaran. Align to Path: Ascender menggeser garis dasar ke luar sejauh ascender font, sehingga puncak ascender menyentuh lingkaran dan huruf berada di luarnya. Di skrip, itu adalah radius = R + ascender.
Jebakannya, ascender tidak sama dengan tinggi huruf kapital. Arial Bold pada 40 px punya ascender 36.2 px dan huruf kapital setinggi 28.6 px. Huruf kapital baris atas menempati jari-jari 225.3 sampai 253.9; baris bawah menempati 232.9 sampai 261.5. Keduanya bergeser 7.6 px, yaitu selisih kedua ukuran itu.
Screenshot aplikasi menunjukkan hal yang sama dengan font miliknya sendiri. Tinta baris atas berada di jari-jari 225.8 sampai 251.7 px dan baris bawah di 239.2 sampai 265.1 px, selisih sekitar 13 px. Kedua baris masih muat dengan lega di antara dua cincin stempel jadi, jadi terbaca sebagai satu pita, tetapi kalau cincinnya rapat, baris bawahlah yang lebih dulu menyentuh cincin luar. Agar pas persis, garis dasar baris bawah seharusnya di R + cap_height (253.9 px dengan Arial), bukan di R + ascender.
Lengkungan meregangkan satu sisi setiap huruf
Garis dasar huruf berada di lingkaran, tetapi puncaknya ada di jari-jari yang berbeda, sehingga sudut yang sama menempuh panjang yang berbeda di sana. Pada baris atas, puncak huruf kapital berada di lingkaran yang 28.6 px lebih besar, dan jarak antarhuruf setinggi huruf kapital menjadi 1.127 kali jarak di garis dasar. Pada baris bawah yang dibalik, huruf kapital menghadap ke pusat, dan puncaknya hanya mendapat 0.891 kali jarak garis dasar. Jadi pada ukuran dan tracking yang sama, baris bawah terlihat lebih rapat dari baris atas, dengan selisih yang bisa dihitung sebelum menggambar apa pun.
print((R + CAP) / R) # 1.127 top line, tops of the capitals
print((R + ASC - CAP) / (R + ASC)) # 0.891 bottom line after Flip + Ascender
Menjalankannya
Skrip lengkapnya, yang juga menggambar gambar di atas, adalah circle_text.py. Jalankan dengan uv run --with fonttools --with numpy --with pillow python circle_text.py; bagian menggambarnya mengubah outline setiap glyph menjadi poligon dengan pen fontTools kecil dan mengisinya dengan aturan even-odd, supaya lubang di O dan A tetap terbuka. Untuk satu logo saja, tata letak yang sama cukup satu klik dan satu dialog di Vector Editor osFoundry, yang gratis dari halaman unduhan.

Top comments (0)