Skip to content

Commit 7079bb7

Browse files
committed
Refine arrows and triangles
1 parent 87a37ce commit 7079bb7

10 files changed

Lines changed: 4684 additions & 957 deletions

xkcd-script/font/xkcd-script.otf

82.2 KB
Binary file not shown.

xkcd-script/font/xkcd-script.sfd

Lines changed: 4514 additions & 941 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

xkcd-script/font/xkcd-script.ttf

22.9 KB
Binary file not shown.

xkcd-script/font/xkcd-script.woff

26.4 KB
Binary file not shown.

xkcd-script/generator/pt6_derived_chars.py

Lines changed: 162 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -212,25 +212,50 @@ def _accented(cp, base_name, mark_name, gap=20, x_adj=0):
212212
# Glyph aliases and re-uses
213213
# ---------------------------------------------------------------------------
214214

215-
# U+20DE COMBINING ENCLOSING SQUARE — zero-width mark sized and positioned to
216-
# enclose '?' with a small margin. GPOS would be needed for full generality.
215+
# U+20DE COMBINING ENCLOSING SQUARE / U+20E4 COMBINING ENCLOSING UPWARD
216+
# POINTING TRIANGLE — zero-width marks spanning the full font EM.
217+
# Centred over 'e' width ('e' renders correctly; narrower letters like 'f'
218+
# will be slightly off — unavoidable without GPOS anchors).
217219
_sq = font[0x25A1]
218220
_sq_bb = _sq.boundingBox()
219221
_sq_cx = (_sq_bb[0] + _sq_bb[2]) / 2
220-
_sq_h = _sq_bb[3] - _sq_bb[1] # sq_bb[1] == 0 after baseline snap
221-
_q_bb = font[ord('?')].boundingBox()
222-
_q_adv = font[ord('?')].width
223-
_margin = 20
224-
_scale_y = (_q_bb[3] - _q_bb[1] + 2 * _margin) / _sq_h
225-
_x_offset = -_q_adv / 2 - _sq_cx
226-
_y_offset = _q_bb[1] - _margin
222+
_sq_h = _sq_bb[3] - _sq_bb[1]
223+
_e_adv = font['e'].width
224+
# Both combining marks share _comb_top so their tops are aligned.
225+
# 1.3× factor ensures they visually enclose tall capitals; the bottom
226+
# hangs 0.3 × _comb_top below the baseline on both marks.
227+
_comb_top = font.ascent + font.descent // 2
228+
229+
_sq_s = 1.3 * _comb_top / _sq_h
230+
_sq_dy = _comb_top - _sq_h * _sq_s
227231
c = font.createMappedChar(0x20DE)
228232
c.addReference(_sq.glyphname, psMat.compose(
229-
psMat.scale(1, _scale_y),
230-
psMat.translate(_x_offset, _y_offset),
233+
psMat.scale(_sq_s),
234+
psMat.translate(-_e_adv / 2 - _sq_cx * _sq_s, _sq_dy),
231235
))
232236
c.width = 0
233237

238+
# Triangle: outlines copied (not a reference) so stroke weight can be
239+
# controlled independently of the regular △. Top at _comb_top;
240+
# 1.3× _comb_top tall so bottom hangs below baseline.
241+
_tri_g = font[0x25B3]
242+
_tri_bb = _tri_g.boundingBox()
243+
_tri_cx = (_tri_bb[0] + _tri_bb[2]) / 2
244+
_tri_h = _tri_bb[3] - _tri_bb[1]
245+
_tri_comb_s = 1.3 * _comb_top / _tri_h
246+
_tri_dy = _comb_top - _tri_h * _tri_comb_s
247+
c = font.createMappedChar(0x20E4)
248+
c.clear()
249+
_layer = fontforge.layer()
250+
for _cont in _tri_g.foreground:
251+
_layer += _cont
252+
c.foreground = _layer
253+
c.transform(psMat.scale(_tri_comb_s))
254+
c.transform(psMat.translate(-_e_adv / 2 - _tri_cx * _tri_comb_s, _tri_dy))
255+
c.changeWeight(-40)
256+
c.correctDirection()
257+
c.width = 0
258+
234259
# Vertical pipe: re-use the I glyph (same stroke, same weight).
235260
c = font.createChar(-1, 'I.sansserif')
236261
c.addReference('I')
@@ -996,6 +1021,132 @@ def _greek_lc_to_uc(font, lc_cp, uc_cp, snap=True, weight_delta=0):
9961021
_make_accented(font, 0x038F, font[0x03A9].glyphname, '_acute_mark') # Ώ
9971022

9981023

1024+
# ---------------------------------------------------------------------------
1025+
# Arrow mirrors and rotations
1026+
# ---------------------------------------------------------------------------
1027+
1028+
# Left-pointing arrows: horizontal flip of right-pointing.
1029+
for _src_cp, _dst_cp in [
1030+
(0x2192, 0x2190), # → ← LEFTWARDS ARROW
1031+
(0x21D2, 0x21D0), # ⇒ ⇐ LEFTWARDS DOUBLE ARROW
1032+
(0x21C0, 0x21BC), # ⇀ ↼ LEFTWARDS HARPOON WITH BARB UPWARDS
1033+
]:
1034+
_src = font[_src_cp]
1035+
_g = font.createMappedChar(_dst_cp)
1036+
_g.clear()
1037+
_layer = fontforge.layer()
1038+
for _c in _src.foreground:
1039+
_layer += _c
1040+
_g.foreground = _layer
1041+
_g.transform(psMat.scale(-1, 1))
1042+
_g.correctDirection()
1043+
_bb = _g.boundingBox()
1044+
_g.transform(psMat.translate(-_bb[0] + 20, 0))
1045+
_g.width = _src.width
1046+
1047+
# Up/down arrows: 90° CCW rotation of each right-pointing arrow family,
1048+
# scaled to the ascent height; down is a vertical flip of up.
1049+
# → ↑ ↓ U+2192 U+2191 U+2193 RIGHTWARDS / UPWARDS / DOWNWARDS ARROW
1050+
# ⇒ ⇑ ⇓ U+21D2 U+21D1 U+21D3 RIGHTWARDS / UPWARDS / DOWNWARDS DOUBLE ARROW
1051+
# ⇀ ↿ ⇃ U+21C0 U+21BF U+21C3 RIGHTWARDS / UPWARDS / DOWNWARDS HARPOON
1052+
for _src_cp, _up_cp, _dn_cp in [
1053+
(0x2192, 0x2191, 0x2193),
1054+
(0x21D2, 0x21D1, 0x21D3),
1055+
(0x21C0, 0x21BF, 0x21C3),
1056+
]:
1057+
_g = font.createMappedChar(_up_cp)
1058+
_g.clear()
1059+
_layer = fontforge.layer()
1060+
for _c in font[_src_cp].foreground:
1061+
_layer += _c
1062+
_g.foreground = _layer
1063+
_g.transform(psMat.rotate(math.radians(90)))
1064+
_bb = _g.boundingBox()
1065+
_s = font.ascent / (_bb[3] - _bb[1])
1066+
_g.transform(psMat.scale(_s))
1067+
_bb = _g.boundingBox()
1068+
_g.transform(psMat.translate(-_bb[0] + 20, -_bb[1]))
1069+
_g.width = int(round(_g.boundingBox()[2] + 20))
1070+
1071+
_g = font.createMappedChar(_dn_cp)
1072+
_g.clear()
1073+
_layer = fontforge.layer()
1074+
for _c in font[_up_cp].foreground:
1075+
_layer += _c
1076+
_g.foreground = _layer
1077+
_up_bb = font[_up_cp].boundingBox()
1078+
_g.transform(psMat.compose(psMat.scale(1, -1), psMat.translate(0, _up_bb[1] + _up_bb[3])))
1079+
_g.correctDirection()
1080+
_g.width = font[_up_cp].width
1081+
1082+
# ⇋ U+21CB LEFTWARDS HARPOON OVER RIGHTWARDS HARPOON
1083+
# ⇌ U+21CC RIGHTWARDS HARPOON OVER LEFTWARDS HARPOON
1084+
# Packed tightly: top harpoon shifted up, bottom harpoon flipped vertically and
1085+
# shifted down so its barb faces outward (away from centre).
1086+
_harp_bb = font[0x21C0].boundingBox()
1087+
_harp_cy = (_harp_bb[1] + _harp_bb[3]) / 2
1088+
_harp_sep = (_harp_bb[3] - _harp_bb[1]) // 2 + 20
1089+
for _dst_cp, _top_cp, _bot_cp in [
1090+
(0x21CB, 0x21BC, 0x21C0), # ⇋: ↼ over ⇀
1091+
(0x21CC, 0x21C0, 0x21BC), # ⇌: ⇀ over ↼
1092+
]:
1093+
c = font.createMappedChar(_dst_cp)
1094+
c.clear()
1095+
c.addReference(font[_top_cp].glyphname, psMat.translate(0, _harp_sep))
1096+
c.addReference(font[_bot_cp].glyphname, psMat.compose(
1097+
psMat.scale(1, -1),
1098+
psMat.translate(0, 2 * _harp_cy - _harp_sep),
1099+
))
1100+
c.width = font[_top_cp].width
1101+
1102+
# 45° diagonal arrows: each right-pointing source rotated to NE/NW/SW/SE and
1103+
# scaled to the same ascent height as the 90° arrows.
1104+
# → ↗ ↖ ↙ ↘ U+2192 U+2197 U+2196 U+2199 U+2198
1105+
# ⇒ ⇗ ⇖ ⇙ ⇘ U+21D2 U+21D7 U+21D6 U+21D9 U+21D8
1106+
for _src_cp, _diag_cps in [
1107+
(0x2192, [(0x2197, 45), (0x2196, 135), (0x2199, 225), (0x2198, -45)]),
1108+
(0x21D2, [(0x21D7, 45), (0x21D6, 135), (0x21D9, 225), (0x21D8, -45)]),
1109+
]:
1110+
_src = font[_src_cp]
1111+
for _diag_cp, _angle in _diag_cps:
1112+
_g = font.createMappedChar(_diag_cp)
1113+
_g.clear()
1114+
_layer = fontforge.layer()
1115+
for _c in _src.foreground:
1116+
_layer += _c
1117+
_g.foreground = _layer
1118+
_g.transform(psMat.rotate(math.radians(_angle)))
1119+
_bb = _g.boundingBox()
1120+
_s = font.ascent / (_bb[3] - _bb[1])
1121+
_g.transform(psMat.scale(_s))
1122+
_bb = _g.boundingBox()
1123+
_g.transform(psMat.translate(-_bb[0] + 20, -_bb[1]))
1124+
_g.width = int(round(_g.boundingBox()[2] + 20))
1125+
1126+
1127+
# ---------------------------------------------------------------------------
1128+
# Triangles
1129+
# ---------------------------------------------------------------------------
1130+
1131+
# ▽ U+25BD WHITE DOWN-POINTING TRIANGLE — △ flipped vertically, point at baseline.
1132+
_tri_up_bb = font[0x25B3].boundingBox()
1133+
_g = font.createMappedChar(0x25BD)
1134+
_g.clear()
1135+
_layer = fontforge.layer()
1136+
for _c in font[0x25B3].foreground:
1137+
_layer += _c
1138+
_g.foreground = _layer
1139+
_g.transform(psMat.compose(psMat.scale(1, -1), psMat.translate(0, _tri_up_bb[1] + _tri_up_bb[3])))
1140+
_g.correctDirection()
1141+
_g.width = font[0x25B3].width
1142+
1143+
# ∇ U+2207 NABLA (del / gradient / curl operator) — same letterform as ▽.
1144+
_g = font.createMappedChar(0x2207)
1145+
_g.clear()
1146+
_g.addReference(font[0x25BD].glyphname)
1147+
_g.width = font[0x25BD].width
1148+
1149+
9991150
# ---------------------------------------------------------------------------
10001151
# Save
10011152
# ---------------------------------------------------------------------------
7.05 KB
Loading
85 Bytes
Loading
1.65 KB
Loading
2.19 KB
Loading

xkcd-script/samples/gen_charmap.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -61,10 +61,13 @@
6161
# Use None to reserve a slot (renders as a blank cell) so that removing a
6262
# character doesn't shift subsequent entries and cause a noisy table diff.
6363
EXTRAS_ORDER = [
64-
0x025B, # ɛ LATIN SMALL LETTER OPEN E
64+
0x025B, # ɛ LATIN SMALL LETTER OPEN E
6565
0x1F382, # 🎂 BIRTHDAY CAKE
66-
0x20DE, # ⃞ COMBINING ENCLOSING SQUARE
67-
0x25A1, # □ WHITE SQUARE
66+
0x20DE, # ⃞ COMBINING ENCLOSING SQUARE
67+
0x25A1, # □ WHITE SQUARE
68+
0x20E4, # b⃤ COMBINING ENCLOSING UPWARD POINTING TRIANGLE
69+
0x25B3, # △ WHITE UP-POINTING TRIANGLE
70+
0x25BD, # ▽ WHITE DOWN-POINTING TRIANGLE
6871
]
6972

7073
block_covered = set()
@@ -80,7 +83,7 @@ def _is_invisible(cp):
8083

8184
extras_in_block = [cp for cp in EXTRAS_ORDER if cp is not None and cp in block_covered]
8285
if extras_in_block:
83-
lines = [f" U+{cp:04X} {unicodedata.name(chr(cp), '(unknown)')}" for cp in extras_in_block]
86+
lines = [f" 0x{cp:04X}, # chr(cp) {unicodedata.name(chr(cp), '(unknown)')}" for cp in extras_in_block]
8487
raise ValueError(
8588
"EXTRAS_ORDER contains codepoints already covered by a named block:\n"
8689
+ "\n".join(lines)
@@ -89,7 +92,7 @@ def _is_invisible(cp):
8992
extras_cps = set(cp for cp in EXTRAS_ORDER if cp is not None)
9093
uncovered = sorted(cp for cp in present if cp not in block_covered and cp not in extras_cps and not _is_invisible(cp))
9194
if uncovered:
92-
lines = [f" U+{cp:04X} {unicodedata.name(chr(cp), '(unknown)')}" for cp in uncovered]
95+
lines = [f" 0x{cp:04X}, # {chr(cp)} {unicodedata.name(chr(cp), '(unknown)')}" for cp in uncovered]
9396
raise ValueError(
9497
"Font contains codepoints not in any named block or EXTRAS_ORDER.\n"
9598
"Add them to EXTRAS_ORDER in gen_charmap.py:\n" + "\n".join(lines)

0 commit comments

Comments
 (0)