Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file modified xkcd-script/font/xkcd-script.otf
Binary file not shown.
5,398 changes: 4,457 additions & 941 deletions xkcd-script/font/xkcd-script.sfd

Large diffs are not rendered by default.

Binary file modified xkcd-script/font/xkcd-script.ttf
Binary file not shown.
Binary file modified xkcd-script/font/xkcd-script.woff
Binary file not shown.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
7 changes: 7 additions & 0 deletions xkcd-script/generator/pt4_additional_sources.py
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,13 @@ def extract_symbol(arr, y0, y1, x0, x1, name, exclude=None):
('AElig', '2763_linguistics_gossip_2x__AE'), # Æ U+00C6 source
('cedilla', '2034_equations_2x__cedilla.hand-tweaked'), # hook cedilla mark source
('epsilon', '2034_equations_2x__epsilon'), # ε U+03B5 source
('Lambda', '2034_equations_2x__Lambda'), # Λ U+039B source
('rounded_d', '2520_symbols_2x__rounded_d'), # ∂ U+2202 source
('infinity', '2343_mathematical_symbol_fight_2x__infinity'), # ∞ U+221E source
('right_double_arrow', '2343_mathematical_symbol_fight_2x__right_double_arrow'), # ⇒ U+21D2 source
('right_half_arrow', '2343_mathematical_symbol_fight_2x__right_half_arrow'), # ⇀ U+21C0 source
('right_lim_arrow', '2343_mathematical_symbol_fight_2x__right_lim_arrow'), # → U+2192 source
('triangle', '2343_mathematical_symbol_fight_2x__triangle'), # △ U+25B3 source
]

print('Extracting hand-drawn extras...')
Expand Down
68 changes: 68 additions & 0 deletions xkcd-script/generator/pt5_svg_to_font.py
Original file line number Diff line number Diff line change
Expand Up @@ -445,6 +445,7 @@ def _import_comic_glyph(font, name, svg_path, target_top, weight_delta=0):
('lambda', 0x03BB, 'b', True),
('tau', 0x03C4, 'a', True),
('varsigma', 0x03C2, 'a', True),
('Lambda', 0x039B, 'A', True),
]

# Letters with genuine descenders: fraction of the crop height that is the
Expand Down Expand Up @@ -631,6 +632,73 @@ def _import_comic_glyph(font, name, svg_path, target_top, weight_delta=0):
_ch.width = _square_src.width


# ∂ U+2202 PARTIAL DIFFERENTIAL — hand-drawn "rounded d" from xkcd #2520.
# Sits on the baseline and reaches ascender height like 'b'.
_rounded_d_svg = os.path.join(_COMIC_CHARS_DIR, 'rounded_d.svg')
_rounded_d_src = _import_comic_glyph(font, 'rounded_d', _rounded_d_svg,
target_top=font['b'].boundingBox()[3], weight_delta=-35)
_bb = _rounded_d_src.boundingBox()
if _bb[1] != 0:
_rounded_d_src.transform(psMat.translate(0, -_bb[1]))
_bb = _rounded_d_src.boundingBox()
if _bb[3] > 0:
_rounded_d_src.transform(psMat.scale(font['b'].boundingBox()[3] / _bb[3]))
_rounded_d_src.width = int(round(_rounded_d_src.boundingBox()[2] + 20))
_ch = font.createMappedChar(0x2202)
_ch.clear()
for _cont in _rounded_d_src.foreground:
_ch.foreground += _cont
_ch.width = _rounded_d_src.width


# ---------------------------------------------------------------------------
# Math symbols from xkcd #2343 "Mathematical Symbol Fight"
# Horizontally-oriented symbols (∞, arrows) are centred at the math axis
# (x-height / 2). The triangle sits on the baseline at cap height.
# Arrow height fractions are initial estimates and may need tuning.
# ---------------------------------------------------------------------------

_xh = font['a'].boundingBox()[3]
_cap_h = font['A'].boundingBox()[3]
_math_axis = _xh / 2


def _import_math_centered(name, cp, target_top, weight_delta=0):
"""Import a math symbol SVG, then centre it at the math axis."""
svg = os.path.join(_COMIC_CHARS_DIR, f'{name}.svg')
g = _import_comic_glyph(font, name, svg, target_top=target_top, weight_delta=weight_delta)
bb = g.boundingBox()
g.transform(psMat.translate(0, _math_axis - (bb[1] + bb[3]) / 2))
ch = font.createMappedChar(cp)
ch.clear()
for cont in g.foreground:
ch.foreground += cont
ch.width = g.width
return ch


_import_math_centered('infinity', 0x221E, _xh, weight_delta=30) # ∞
_import_math_centered('right_lim_arrow', 0x2192, _xh, weight_delta=20) # →
_import_math_centered('right_double_arrow', 0x21D2, _xh, weight_delta=20) # ⇒
_import_math_centered('right_half_arrow', 0x21C0, _xh, weight_delta=20) # ⇀

# △ U+25B3 WHITE UP-POINTING TRIANGLE — baseline to cap height.
_tri_svg = os.path.join(_COMIC_CHARS_DIR, 'triangle.svg')
_tri_src = _import_comic_glyph(font, 'triangle', _tri_svg, target_top=_cap_h, weight_delta=30)
_bb = _tri_src.boundingBox()
if _bb[1] != 0:
_tri_src.transform(psMat.translate(0, -_bb[1]))
_bb = _tri_src.boundingBox()
if _bb[3] > 0:
_tri_src.transform(psMat.scale(_cap_h / _bb[3]))
_tri_src.width = int(round(_tri_src.boundingBox()[2] + 20))
_ch = font.createMappedChar(0x25B3)
_ch.clear()
for _cont in _tri_src.foreground:
_ch.foreground += _cont
_ch.width = _tri_src.width


# ---------------------------------------------------------------------------
# Save
# ---------------------------------------------------------------------------
Expand Down
180 changes: 162 additions & 18 deletions xkcd-script/generator/pt6_derived_chars.py
Original file line number Diff line number Diff line change
Expand Up @@ -212,25 +212,50 @@ def _accented(cp, base_name, mark_name, gap=20, x_adj=0):
# Glyph aliases and re-uses
# ---------------------------------------------------------------------------

# U+20DE COMBINING ENCLOSING SQUARE — zero-width mark sized and positioned to
# enclose '?' with a small margin. GPOS would be needed for full generality.
# U+20DE COMBINING ENCLOSING SQUARE / U+20E4 COMBINING ENCLOSING UPWARD
# POINTING TRIANGLE — zero-width marks spanning the full font EM.
# Centred over 'e' width ('e' renders correctly; narrower letters like 'f'
# will be slightly off — unavoidable without GPOS anchors).
_sq = font[0x25A1]
_sq_bb = _sq.boundingBox()
_sq_cx = (_sq_bb[0] + _sq_bb[2]) / 2
_sq_h = _sq_bb[3] - _sq_bb[1] # sq_bb[1] == 0 after baseline snap
_q_bb = font[ord('?')].boundingBox()
_q_adv = font[ord('?')].width
_margin = 20
_scale_y = (_q_bb[3] - _q_bb[1] + 2 * _margin) / _sq_h
_x_offset = -_q_adv / 2 - _sq_cx
_y_offset = _q_bb[1] - _margin
_sq_h = _sq_bb[3] - _sq_bb[1]
_e_adv = font['e'].width
# Both combining marks share _comb_top so their tops are aligned.
# 1.3× factor ensures they visually enclose tall capitals; the bottom
# hangs 0.3 × _comb_top below the baseline on both marks.
_comb_top = font.ascent + font.descent // 2

_sq_s = 1.3 * _comb_top / _sq_h
_sq_dy = _comb_top - _sq_h * _sq_s
c = font.createMappedChar(0x20DE)
c.addReference(_sq.glyphname, psMat.compose(
psMat.scale(1, _scale_y),
psMat.translate(_x_offset, _y_offset),
psMat.scale(_sq_s),
psMat.translate(-_e_adv / 2 - _sq_cx * _sq_s, _sq_dy),
))
c.width = 0

# Triangle: outlines copied (not a reference) so stroke weight can be
# controlled independently of the regular △. Top at _comb_top;
# 1.3× _comb_top tall so bottom hangs below baseline.
_tri_g = font[0x25B3]
_tri_bb = _tri_g.boundingBox()
_tri_cx = (_tri_bb[0] + _tri_bb[2]) / 2
_tri_h = _tri_bb[3] - _tri_bb[1]
_tri_comb_s = 1.3 * _comb_top / _tri_h
_tri_dy = _comb_top - _tri_h * _tri_comb_s
c = font.createMappedChar(0x20E4)
c.clear()
_layer = fontforge.layer()
for _cont in _tri_g.foreground:
_layer += _cont
c.foreground = _layer
c.transform(psMat.scale(_tri_comb_s))
c.transform(psMat.translate(-_e_adv / 2 - _tri_cx * _tri_comb_s, _tri_dy))
c.changeWeight(-40)
c.correctDirection()
c.width = 0

# Vertical pipe: re-use the I glyph (same stroke, same weight).
c = font.createChar(-1, 'I.sansserif')
c.addReference('I')
Expand Down Expand Up @@ -952,13 +977,6 @@ def _greek_lc_to_uc(font, lc_cp, uc_cp, snap=True, weight_delta=0):
_g.addReference('L', psMat.compose(psMat.scale(1, -1), psMat.translate(0, _L_bb[3])))
_g.width = font['L'].width

# Λ (U+039B): V flipped vertically — two diagonals meeting at the top.
_V_bb = font['V'].boundingBox()
_g = font.createMappedChar(0x039B)
_g.clear()
_g.addReference('V', psMat.compose(psMat.scale(1, -1), psMat.translate(0, _V_bb[3])))
_g.width = font['V'].width

# η (eta, U+03B7): n with a straight vertical descender on the right leg.
# A rotated hyphen-bar gives a stroke of matching weight and shape.
# x-scaled to 90% of original thickness so it reads slightly thinner than n's strokes.
Expand Down Expand Up @@ -1003,6 +1021,132 @@ def _greek_lc_to_uc(font, lc_cp, uc_cp, snap=True, weight_delta=0):
_make_accented(font, 0x038F, font[0x03A9].glyphname, '_acute_mark') # Ώ


# ---------------------------------------------------------------------------
# Arrow mirrors and rotations
# ---------------------------------------------------------------------------

# Left-pointing arrows: horizontal flip of right-pointing.
for _src_cp, _dst_cp in [
(0x2192, 0x2190), # → ← LEFTWARDS ARROW
(0x21D2, 0x21D0), # ⇒ ⇐ LEFTWARDS DOUBLE ARROW
(0x21C0, 0x21BC), # ⇀ ↼ LEFTWARDS HARPOON WITH BARB UPWARDS
]:
_src = font[_src_cp]
_g = font.createMappedChar(_dst_cp)
_g.clear()
_layer = fontforge.layer()
for _c in _src.foreground:
_layer += _c
_g.foreground = _layer
_g.transform(psMat.scale(-1, 1))
_g.correctDirection()
_bb = _g.boundingBox()
_g.transform(psMat.translate(-_bb[0] + 20, 0))
_g.width = _src.width

# Up/down arrows: 90° CCW rotation of each right-pointing arrow family,
# scaled to the ascent height; down is a vertical flip of up.
# → ↑ ↓ U+2192 U+2191 U+2193 RIGHTWARDS / UPWARDS / DOWNWARDS ARROW
# ⇒ ⇑ ⇓ U+21D2 U+21D1 U+21D3 RIGHTWARDS / UPWARDS / DOWNWARDS DOUBLE ARROW
# ⇀ ↿ ⇃ U+21C0 U+21BF U+21C3 RIGHTWARDS / UPWARDS / DOWNWARDS HARPOON
for _src_cp, _up_cp, _dn_cp in [
(0x2192, 0x2191, 0x2193),
(0x21D2, 0x21D1, 0x21D3),
(0x21C0, 0x21BF, 0x21C3),
]:
_g = font.createMappedChar(_up_cp)
_g.clear()
_layer = fontforge.layer()
for _c in font[_src_cp].foreground:
_layer += _c
_g.foreground = _layer
_g.transform(psMat.rotate(math.radians(90)))
_bb = _g.boundingBox()
_s = font.ascent / (_bb[3] - _bb[1])
_g.transform(psMat.scale(_s))
_bb = _g.boundingBox()
_g.transform(psMat.translate(-_bb[0] + 20, -_bb[1]))
_g.width = int(round(_g.boundingBox()[2] + 20))

_g = font.createMappedChar(_dn_cp)
_g.clear()
_layer = fontforge.layer()
for _c in font[_up_cp].foreground:
_layer += _c
_g.foreground = _layer
_up_bb = font[_up_cp].boundingBox()
_g.transform(psMat.compose(psMat.scale(1, -1), psMat.translate(0, _up_bb[1] + _up_bb[3])))
_g.correctDirection()
_g.width = font[_up_cp].width

# ⇋ U+21CB LEFTWARDS HARPOON OVER RIGHTWARDS HARPOON
# ⇌ U+21CC RIGHTWARDS HARPOON OVER LEFTWARDS HARPOON
# Packed tightly: top harpoon shifted up, bottom harpoon flipped vertically and
# shifted down so its barb faces outward (away from centre).
_harp_bb = font[0x21C0].boundingBox()
_harp_cy = (_harp_bb[1] + _harp_bb[3]) / 2
_harp_sep = (_harp_bb[3] - _harp_bb[1]) // 2 + 20
for _dst_cp, _top_cp, _bot_cp in [
(0x21CB, 0x21BC, 0x21C0), # ⇋: ↼ over ⇀
(0x21CC, 0x21C0, 0x21BC), # ⇌: ⇀ over ↼
]:
c = font.createMappedChar(_dst_cp)
c.clear()
c.addReference(font[_top_cp].glyphname, psMat.translate(0, _harp_sep))
c.addReference(font[_bot_cp].glyphname, psMat.compose(
psMat.scale(1, -1),
psMat.translate(0, 2 * _harp_cy - _harp_sep),
))
c.width = font[_top_cp].width

# 45° diagonal arrows: each right-pointing source rotated to NE/NW/SW/SE and
# scaled to the same ascent height as the 90° arrows.
# → ↗ ↖ ↙ ↘ U+2192 U+2197 U+2196 U+2199 U+2198
# ⇒ ⇗ ⇖ ⇙ ⇘ U+21D2 U+21D7 U+21D6 U+21D9 U+21D8
for _src_cp, _diag_cps in [
(0x2192, [(0x2197, 45), (0x2196, 135), (0x2199, 225), (0x2198, -45)]),
(0x21D2, [(0x21D7, 45), (0x21D6, 135), (0x21D9, 225), (0x21D8, -45)]),
]:
_src = font[_src_cp]
for _diag_cp, _angle in _diag_cps:
_g = font.createMappedChar(_diag_cp)
_g.clear()
_layer = fontforge.layer()
for _c in _src.foreground:
_layer += _c
_g.foreground = _layer
_g.transform(psMat.rotate(math.radians(_angle)))
_bb = _g.boundingBox()
_s = font.ascent / (_bb[3] - _bb[1])
_g.transform(psMat.scale(_s))
_bb = _g.boundingBox()
_g.transform(psMat.translate(-_bb[0] + 20, -_bb[1]))
_g.width = int(round(_g.boundingBox()[2] + 20))


# ---------------------------------------------------------------------------
# Triangles
# ---------------------------------------------------------------------------

# ▽ U+25BD WHITE DOWN-POINTING TRIANGLE — △ flipped vertically, point at baseline.
_tri_up_bb = font[0x25B3].boundingBox()
_g = font.createMappedChar(0x25BD)
_g.clear()
_layer = fontforge.layer()
for _c in font[0x25B3].foreground:
_layer += _c
_g.foreground = _layer
_g.transform(psMat.compose(psMat.scale(1, -1), psMat.translate(0, _tri_up_bb[1] + _tri_up_bb[3])))
_g.correctDirection()
_g.width = font[0x25B3].width

# ∇ U+2207 NABLA (del / gradient / curl operator) — same letterform as ▽.
_g = font.createMappedChar(0x2207)
_g.clear()
_g.addReference(font[0x25BD].glyphname)
_g.width = font[0x25BD].width


# ---------------------------------------------------------------------------
# Save
# ---------------------------------------------------------------------------
Expand Down
Binary file modified xkcd-script/samples/charmap_arrows.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified xkcd-script/samples/charmap_greek_and_coptic.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified xkcd-script/samples/charmap_mathematical_operators.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified xkcd-script/samples/charmap_non_latin_other.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
7 changes: 5 additions & 2 deletions xkcd-script/samples/gen_charmap.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,11 @@
EXTRAS_ORDER = [
0x025B, # ɛ LATIN SMALL LETTER OPEN E
0x1F382, # 🎂 BIRTHDAY CAKE
0x20DE, # ⃞ COMBINING ENCLOSING SQUARE
0x25A1, # □ WHITE SQUARE
0x20DE, # ⃞ COMBINING ENCLOSING SQUARE
0x25A1, # □ WHITE SQUARE
0x20E4, # COMBINING ENCLOSING UPWARD POINTING TRIANGLE
0x25B3, # WHITE UP-POINTING TRIANGLE
0x25BD, # WHITE DOWN-POINTING TRIANGLE
]

block_covered = set()
Expand Down
Loading