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.
1,927 changes: 1,227 additions & 700 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.
17 changes: 17 additions & 0 deletions xkcd-script/generator/pt5_svg_to_font.py
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,14 @@ def charname(char):
('x',): psMat.translate(0, 20),
}

# Per-character weight nudge applied after the per-line scale correction.
# Positive = thicker, negative = thinner. Mirrors _GREEK_WEIGHT_NUDGE for the
# main alphabet — used when an individual glyph's hand-drawn stroke is
# noticeably heavier or lighter than its neighbours.
_per_char_weight_nudge = {
('z',): -15, # z's diagonal strokes are heavier than the rest of the alphabet
}

# Pick out particular glyphs that are more pleasant than their latter alternatives.
special_choices = {('C', ): dict(line=4),
('G',): dict(line=4),
Expand Down Expand Up @@ -387,6 +395,15 @@ def charname(char):
c.transform(psMat.scale(_restore))
c.width = int(round(c.width * _restore))

# Per-character weight nudge: applied to source glyphs whose stroke weight
# is noticeably off from the rest of the alphabet.
_weight_nudge = _per_char_weight_nudge.get(chars)
if _weight_nudge:
c.correctDirection()
c.removeOverlap()
c.changeWeight(_weight_nudge)
c.simplify()

# Per-character size adjustments: scale about the baseline (origin) to reduce
# overall size while preserving stroke weight gained from changeWeight above.
_operation_matrix = _per_char_operation.get(chars)
Expand Down
151 changes: 151 additions & 0 deletions xkcd-script/generator/pt6_derived_chars.py
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,26 @@ def _make_ogonek(font, cp, base_name):
c.addReference('comma', psMat.compose(psMat.scale(-1, -1), psMat.translate(dx, dy)))


def _make_spacing_modifier(font, cp, mark_name, y_bottom=None, lsb=40, rsb=40):
"""Spacing modifier letter: reference to mark glyph, repositioned and given advance width.

Translates the mark so its bottom sits at y_bottom and its left ink edge is
at lsb. Default y_bottom is 20 units above the x-height of 'a', placing
the modifier at a natural reading height as if hovering over a lowercase letter.
lsb/rsb: left/right side bearings of the resulting glyph.
"""
if y_bottom is None:
y_bottom = int(round(font['a'].boundingBox()[3] + 20))
g = font.createMappedChar(cp)
g.clear()
mark_bb = font[mark_name].boundingBox()
dx = lsb - mark_bb[0]
dy = y_bottom - mark_bb[1]
g.addReference(mark_name, psMat.translate(dx, dy))
g.width = int(round(mark_bb[2] + dx + rsb))
return g


# Codepoints that already exist as hand-drawn glyphs; skip to avoid overwriting.
_SKIP_CPS = frozenset({
0x00DC, # Ü — hand-drawn
Expand Down Expand Up @@ -276,18 +296,44 @@ def _accented(cp, base_name, mark_name, gap=20, x_adj=0):
(0x2033, 'quotedbl'), # doubleprime
(0x2035, 'quoteleft'), # backprime
# Dash aliases
(0x2010, 'hyphen'), # Unicode hyphen
(0x2011, 'hyphen'), # non-breaking hyphen
(0x00AD, 'hyphen'), # soft hyphen
(0x2212, 'endash'), # minus sign
(0x2012, 'endash'), # figure dash
(0x2015, 'emdash'), # horizontal bar
# Modifier letter apostrophes (Polynesian, transliteration)
(0x02BB, 'quoteleft'), # modifier letter turned comma ʻ
(0x02BC, 'quoteright'), # modifier letter apostrophe ʼ
]
for codepoint, source_name in ref_aliases:
c = font.createMappedChar(codepoint)
c.addReference(source_name)
c.width = font[source_name].width


# ---------------------------------------------------------------------------
# Inverted punctuation
# ---------------------------------------------------------------------------

# U+00A1 ¡ INVERTED EXCLAMATION MARK — ! flipped vertically.
# U+00BF ¿ INVERTED QUESTION MARK — ? flipped vertically.
# scale(1,-1) reflects about y=0; the translate re-maps [ymin,ymax] back to the
# same vertical range so the glyph stays above the baseline.
for _cp, _gname in [(0x00A1, 'exclam'), (0x00BF, 'question')]:
_src = font[_gname]
_src_bb = _src.boundingBox()
_g = font.createMappedChar(_cp)
_g.clear()
_layer = fontforge.layer()
for _c in _src.foreground:
_layer += _c
_g.foreground = _layer
_g.transform(psMat.compose(psMat.scale(1, -1), psMat.translate(0, _src_bb[1] + _src_bb[3])))
_g.correctDirection()
_g.width = _src.width


# ---------------------------------------------------------------------------
# Combining mark glyphs
# ---------------------------------------------------------------------------
Expand Down Expand Up @@ -467,6 +513,24 @@ def _accented(cp, base_name, mark_name, gap=20, x_adj=0):
_c0327.width = 0


# ---------------------------------------------------------------------------
# Spacing modifier letters (U+00A8, U+00AF, U+02C6–U+02DD)
# Each is a reference to the corresponding combining mark, repositioned so its
# bottom sits just above the x-height and given an advance width.
# ---------------------------------------------------------------------------

_sm_y = int(round(font['a'].boundingBox()[3] + 20))

_make_spacing_modifier(font, 0x00A8, '_diaeresis_mark', _sm_y) # ¨ spacing diaeresis
_make_spacing_modifier(font, 0x00AF, '_macron_mark', _sm_y) # ¯ spacing macron
_make_spacing_modifier(font, 0x02C6, '_circumflex_mark', _sm_y) # ˆ modifier letter circumflex accent
_make_spacing_modifier(font, 0x02C7, '_caron_mark', _sm_y) # ˇ modifier letter caron
_make_spacing_modifier(font, 0x02D8, '_breve_mark', _sm_y) # ˘ modifier letter breve
_make_spacing_modifier(font, 0x02D9, '_dot_above_mark', _sm_y) # ˙ modifier letter dot above
_make_spacing_modifier(font, 0x02DC, '_tilde_mark', _sm_y) # ˜ modifier letter small tilde
_make_spacing_modifier(font, 0x02DD, '_double_acute_mark', _sm_y) # ˝ modifier letter double acute accent


# ---------------------------------------------------------------------------
# Accented character tables
# ---------------------------------------------------------------------------
Expand Down Expand Up @@ -1147,6 +1211,93 @@ def _greek_lc_to_uc(font, lc_cp, uc_cp, snap=True, weight_delta=0):
_g.width = font[0x25BD].width


# ---------------------------------------------------------------------------
# Additional composites
# ---------------------------------------------------------------------------

# U with diaeresis + tone mark: Ǖ Ǘ Ǚ Ǜ / ǖ ǘ ǚ ǜ (Pinyin)
# Ü / ü are used as bases so the new mark floats above the existing dots.
_Udi = font[0x00DC].glyphname # Ü hand-drawn
_udi = font[0x00FC].glyphname # ü derived
_accented(0x01D5, _Udi, '_macron_mark') # Ǖ
_accented(0x01D6, _udi, '_macron_mark', gap=8) # ǖ
_accented(0x01D7, _Udi, '_acute_mark') # Ǘ
_accented(0x01D8, _udi, '_acute_mark', gap=8) # ǘ
_accented(0x01D9, _Udi, '_caron_mark') # Ǚ
_accented(0x01DA, _udi, '_caron_mark', gap=8) # ǚ
_accented(0x01DB, _Udi, '_grave_mark') # Ǜ
_accented(0x01DC, _udi, '_grave_mark', gap=8) # ǜ

# A/a with diaeresis + macron: Ǟ ǟ (Livonian, Skolt Sámi)
# gap=30/20: the base's bb top already includes the diaeresis dots; extra
# clearance prevents the macron from pressing against the dots.
_Adi = font[0x00C4].glyphname # Ä
_adi = font[0x00E4].glyphname # ä
_accented(0x01DE, _Adi, '_macron_mark', gap=30) # Ǟ
_accented(0x01DF, _adi, '_macron_mark', gap=20) # ǟ

# AE/ae + macron: Ǣ ǣ (Old Norse transliteration)
# x_adj shifts the macron rightward: Æ/æ's geometric centre sits left of the
# visual centre (E side is heavier than A side).
_Ae_name = font[0x00C6].glyphname # Æ
_ae_name = font[0x00E6].glyphname # æ
_accented(0x01E2, _Ae_name, '_macron_mark', x_adj=100) # Ǣ
_accented(0x01E3, _ae_name, '_macron_mark', gap=25, x_adj=50) # ǣ

# G/g + caron: Ǧ ǧ (Skolt Sámi, some transliteration systems)
# x_adj for ǧ: g's full bounding box includes a wide descender that pulls
# its geometric centre left of the bowl centre.
_accented(0x01E6, 'G', '_caron_mark') # Ǧ
_accented(0x01E7, 'g', '_caron_mark', gap=8, x_adj=80) # ǧ

# K/k + caron: Ǩ ǩ (Skolt Sámi)
_accented(0x01E8, 'K', '_caron_mark') # Ǩ
_accented(0x01E9, 'k', '_caron_mark', gap=8) # ǩ

# G/g + acute: Ǵ ǵ (Upper Sorbian)
# x_adj for ǵ: same descender-bowl offset as ǧ.
_accented(0x01F4, 'G', '_acute_mark') # Ǵ
_accented(0x01F5, 'g', '_acute_mark', gap=20, x_adj=80) # ǵ

# A-ring + acute: Ǻ ǻ (Northern Sámi)
# The acute floats above the existing ring; base bb already includes the ring.
_Aring_name = font[0x00C5].glyphname # Å
_aring_name = font[0x00E5].glyphname # å
_accented(0x01FA, _Aring_name, '_acute_mark') # Ǻ
_accented(0x01FB, _aring_name, '_acute_mark', gap=8) # ǻ

# AE/ae + acute: Ǽ ǽ (Faroese, Danish orthography)
_accented(0x01FC, _Ae_name, '_acute_mark') # Ǽ
_accented(0x01FD, _ae_name, '_acute_mark', gap=8) # ǽ

# O-stroke + acute: Ǿ ǿ (Faroese, Danish orthography)
# Ø/ø are derived earlier in this script; their bounding boxes include the stroke.
_Ost_name = font[0x00D8].glyphname # Ø
_ost_name = font[0x00F8].glyphname # ø
_accented(0x01FE, _Ost_name, '_acute_mark') # Ǿ
_accented(0x01FF, _ost_name, '_acute_mark', gap=8) # ǿ

# DZ/dz digraph ligatures: DZ Dz dz and DŽ/Dž/dž: DŽ Dž dž
# Letters are placed ink-edge to ink-edge with a small gap, like IJ above.
_Zcaron_name = font[0x017D].glyphname # Ž
_zcaron_name = font[0x017E].glyphname # ž
_dz_gap = 20
for _cp, _left, _right in [
(0x01F1, 'D', 'Z'), # DZ capital DZ
(0x01F2, 'D', 'z'), # Dz titlecase Dz
(0x01F3, 'd', 'z'), # dz lowercase dz
(0x01C4, 'D', _Zcaron_name), # DŽ capital DŽ
(0x01C5, 'D', _zcaron_name), # Dž titlecase Dž
(0x01C6, 'd', _zcaron_name), # dž lowercase dž
]:
_g = font.createMappedChar(_cp)
_g.clear()
_g.addReference(_left)
_dx = int(round(font[_left].boundingBox()[2] + _dz_gap - font[_right].boundingBox()[0]))
_g.addReference(_right, psMat.translate(_dx, 0))
_g.width = _dx + font[_right].width


# ---------------------------------------------------------------------------
# Save
# ---------------------------------------------------------------------------
Expand Down
Binary file modified xkcd-script/samples/charmap_basic_latin.png
Copy link
Copy Markdown
Member Author

@pelson pelson May 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Jitter caused by the change in the yMax height, introduced by Ǘ. The handwriting sample is more insightful to the real change here.

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_general_punctuation.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_latin_1_supplement.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_latin_extended_a.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_latin_extended_additional.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_latin_extended_b.png
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.
1 change: 1 addition & 0 deletions xkcd-script/samples/gen_charmap.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
(0x00A0, 0x0100, "Latin-1 Supplement"),
(0x0100, 0x0180, "Latin Extended-A"),
(0x0180, 0x0250, "Latin Extended-B"),
(0x02B0, 0x0300, "Spacing Modifier Letters"),
(0x0300, 0x0370, "Combining Diacritical Marks"),
(0x0370, 0x0400, "Greek and Coptic"),
(0x1E00, 0x1F00, "Latin Extended Additional"),
Expand Down
Binary file modified xkcd-script/samples/handwriting.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/kerning.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading