Skip to content
Merged
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.
2,511 changes: 1,565 additions & 946 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.
4 changes: 3 additions & 1 deletion xkcd-script/generator/pt4_additional_sources.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ def extract_symbol(arr, y0, y1, x0, x1, name, exclude=None):
'delta': ( 256, 304, 80, 136), # δ U+03B4
'theta': ( 334, 372, 80, 136), # θ U+03B8
'phi': ( 370, 440, 80, 136), # φ U+03C6 (y1 extended: clips on bottom)
'epsilon': ( 450, 476, 80, 136), # ε U+03B5
'lunate_epsilon': ( 450, 476, 80, 136), # ϵ U+03F5
'upsilon': ( 500, 534, 35, 86), # υ U+03C5 (left half of ν/υ pair row; x1 tightened: comma at col 87)
'nu': ( 500, 534, 88, 120), # ν U+03BD (right half of ν/υ pair row)
'mu': ( 562, 608, 65, 136), # μ U+03BC (x0 extended: clips on left)
Expand Down Expand Up @@ -159,6 +159,8 @@ def extract_symbol(arr, y0, y1, x0, x1, name, exclude=None):
('tau', '2520_symbols_2x__tau'), # τ U+03C4 source
('varsigma', '2586_greek_letters_2x__sigma'), # ς U+03C2 source
('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
]

print('Extracting hand-drawn extras...')
Expand Down
17 changes: 15 additions & 2 deletions xkcd-script/generator/pt5_svg_to_font.py
Original file line number Diff line number Diff line change
Expand Up @@ -402,6 +402,7 @@ def _import_comic_glyph(font, name, svg_path, target_top, weight_delta=0):
('delta', 0x03B4, 'b', True),
('theta', 0x03B8, 'b', True),
('phi', 0x03C6, 'a', True),
('lunate_epsilon', 0x03F5, 'a', True),
('epsilon', 0x03B5, 'a', True),
('upsilon', 0x03C5, 'a', True),
('nu', 0x03BD, 'a', True),
Expand Down Expand Up @@ -446,14 +447,15 @@ def _import_comic_glyph(font, name, svg_path, target_top, weight_delta=0):
# _scan_stroke_width measures bar *lengths* rather than stroke thickness,
# producing a grossly over-estimated value and a large negative delta that
# massively thins the letter. Skip stroke normalisation for these.
_GREEK_NO_STROKE_NORM = {'epsilon', 'Xi', 'Sigma'}
_GREEK_NO_STROKE_NORM = {'epsilon', 'lunate_epsilon', 'Xi', 'Sigma'}

# Per-letter changeWeight nudge applied after all positioning and stroke
# normalisation. Positive = thicker, negative = thinner.
_GREEK_WEIGHT_NUDGE = {
'Sigma': 15,
'mu': 15,
'epsilon': 15,
'lunate_epsilon': 15,
'epsilon': 20,
'psi': -15,
'lambda': 10,
}
Expand Down Expand Up @@ -523,6 +525,17 @@ def _import_comic_glyph(font, name, svg_path, target_top, weight_delta=0):
_ch.width = _g.width


# ¸ U+00B8 CEDILLA — hand-drawn hook shape from extras/cedilla.png.
_cedilla_svg = os.path.join(_COMIC_CHARS_DIR, 'cedilla.svg')
_cedilla_src = _import_comic_glyph(font, 'cedilla', _cedilla_svg,
target_top=font['comma'].boundingBox()[3])
_ch = font.createMappedChar(0x00B8)
_ch.clear()
for c in _cedilla_src.foreground:
_ch.foreground += c
_ch.width = _cedilla_src.width


# ß (U+00DF) and ẞ (U+1E9E) — hand-drawn source from extras/eszett.png.
# The same SVG is imported twice at different scales for the two case forms.
_eszett_svg = os.path.join(_COMIC_CHARS_DIR, 'eszett.svg')
Expand Down
118 changes: 103 additions & 15 deletions xkcd-script/generator/pt6_derived_chars.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,45 +130,49 @@ def _make_dstroke(font, cp, base_name, gap=5, dy_offset=0, width_extra=0):
return c


def _place_below(font, base_name, mark_name, gap=10):
"""Compute translation to place mark centered below base."""
def _place_below(font, base_name, mark_name, y_adj=10, x_adj=0):
"""Compute translation to place mark below base.

y_adj: positive = more space (mark lower), negative = closer/overlapping.
x_adj: positive shifts mark right, negative shifts left.
"""
base_bb = font[base_name].boundingBox()
mark_bb = font[mark_name].boundingBox()
base_cx = (base_bb[0] + base_bb[2]) / 2
mark_cx = (mark_bb[0] + mark_bb[2]) / 2
dx = base_cx - mark_cx
dy = base_bb[1] - gap - mark_bb[3]
dx = base_cx - mark_cx + x_adj
dy = base_bb[1] - y_adj - mark_bb[3]
return psMat.translate(dx, dy)


def _make_cedilla(font, cp, base_name, gap=8):
def _make_cedilla(font, cp, base_name, y_adj=8, mark='comma', x_adj=0):
if cp in _SKIP_CPS:
return
c = font.createMappedChar(cp)
c.clear()
c.addReference(base_name)
c.width = font[base_name].width
c.addReference('comma', _place_below(font, base_name, 'comma', gap))
c.addReference(base_name)
c.addReference(mark, _place_below(font, base_name, mark, y_adj, x_adj))


def _make_macron_below(font, cp, base_name, gap=15):
def _make_macron_below(font, cp, base_name, y_adj=15):
if cp in _SKIP_CPS:
return
c = font.createMappedChar(cp)
c.clear()
c.addReference(base_name)
c.width = font[base_name].width
c.addReference('_macron_below_mark', _place_below(font, base_name, '_macron_below_mark', gap))
c.addReference('_macron_below_mark', _place_below(font, base_name, '_macron_below_mark', y_adj))


def _make_dot_below(font, cp, base_name, gap=15):
def _make_dot_below(font, cp, base_name, y_adj=15):
if cp in _SKIP_CPS:
return
c = font.createMappedChar(cp)
c.clear()
c.addReference(base_name)
c.width = font[base_name].width
c.addReference('_dot_below_mark', _place_below(font, base_name, '_dot_below_mark', gap))
c.addReference('_dot_below_mark', _place_below(font, base_name, '_dot_below_mark', y_adj))


def _make_ogonek(font, cp, base_name):
Expand Down Expand Up @@ -268,6 +272,16 @@ def _accented(cp, base_name, mark_name, gap=20, x_adj=0):
_macron_below_mark.changeWeight(20)


# Hook cedilla: hand-drawn mark from the 2034 equations comic (extras/cedilla).
# Used for French/Turkish/Cameroonian characters (ç ş ţ); the comma-based cedilla
# is kept for Latvian (ģ ķ ļ ņ ŗ) where that shape is traditional.
_hook_cedilla_mark = make_mark(font, '_hook_cedilla_mark',
list(font['cedilla'].foreground))
_hook_cedilla_mark.changeWeight(30)
_hook_cedilla_mark.correctDirection()
_hook_cedilla_mark.removeOverlap()
_hook_cedilla_mark.transform(psMat.scale(1.2))

# Macron: topmost stroke from Ē, with weight correction.
_macron_mark = make_mark(font, '_macron_mark', extract_top_contours(font, 0x0112, 1))
_macron_mark.changeWeight(20)
Expand Down Expand Up @@ -398,6 +412,13 @@ def _accented(cp, base_name, mark_name, gap=20, x_adj=0):
_c0320.addReference('_macron_below_mark', psMat.translate(0, _descender_bottom // 2 - _combining_gap - _mb_bb[3]))
_c0320.width = 0

# U+0327 ◌̧ COMBINING CEDILLA — hook cedilla shape, positioned below descender.
_hc_bb = font['_hook_cedilla_mark'].boundingBox()
_c0327 = font.createMappedChar(0x0327)
_c0327.clear()
_c0327.addReference('_hook_cedilla_mark', psMat.translate(0, _descender_bottom - _combining_gap - _hc_bb[3]))
_c0327.width = 0


# ---------------------------------------------------------------------------
# Accented character tables
Expand Down Expand Up @@ -707,10 +728,22 @@ def _make_eng(font, cp, base_name, comma_name, x_frac=0.88, x_offset=0, y_offset
]:
_accented(cp, base, '_double_acute_mark')

# Cedilla: Ç Ş Ţ Ģ Ķ Ļ Ņ Ŗ / ç ş ţ ģ ķ ļ ņ ŗ
# Hook cedilla: Ç Ş Ţ / ç ş ţ — French/Turkish/Cameroonian
# C/c/S/s: curved base bottom leaves visual space — pull cedilla up to close the gap
_make_cedilla(font, 0x00C7, 'C', mark='_hook_cedilla_mark', y_adj=-15) # Ç
_make_cedilla(font, 0x00E7, 'c', mark='_hook_cedilla_mark', y_adj=-15) # ç
_make_cedilla(font, 0x015E, 'S', mark='_hook_cedilla_mark', y_adj=-15) # Ş
_make_cedilla(font, 0x015F, 's', mark='_hook_cedilla_mark', y_adj=-15) # ş
_make_cedilla(font, 0x0162, 'T', mark='_hook_cedilla_mark', y_adj=-10, x_adj=-37) # Ţ
_make_cedilla(font, 0x0163, 't', mark='_hook_cedilla_mark', y_adj=-15, x_adj=40) # ţ
# Ȩ/ȩ U+0228/U+0229 — E with cedilla (Cameroonian)
_make_cedilla(font, 0x0228, 'E', mark='_hook_cedilla_mark', y_adj=-25) # Ȩ
_make_cedilla(font, 0x0229, 'e', mark='_hook_cedilla_mark', y_adj=-15) # ȩ

# Comma cedilla: Ģ Ķ Ļ Ņ Ŗ / ģ ķ ļ ņ ŗ — Latvian
for cp, base in [
(0x00C7, 'C'), (0x015E, 'S'), (0x0162, 'T'), (0x0122, 'G'), (0x0136, 'K'), (0x013B, 'L'), (0x0145, 'N'), (0x0156, 'R'),
(0x00E7, 'c'), (0x015F, 's'), (0x0163, 't'), (0x0123, 'g'), (0x0137, 'k'), (0x013C, 'l'), (0x0146, 'n'), (0x0157, 'r'),
(0x0122, 'G'), (0x0136, 'K'), (0x013B, 'L'), (0x0145, 'N'), (0x0156, 'R'),
(0x0123, 'g'), (0x0137, 'k'), (0x013C, 'l'), (0x0146, 'n'), (0x0157, 'r'),
]:
_make_cedilla(font, cp, base)

Expand Down Expand Up @@ -750,7 +783,7 @@ def _make_eng(font, cp, base_name, comma_name, x_frac=0.88, x_offset=0, y_offset
(0x1E0F, 'd'), (0x1E3B, 'l'), (0x1E49, 'n'), (0x1E6F, 't'), (0x1E95, 'z'),
]:
_make_macron_below(font, cp, base)
_make_macron_below(font, 0x1E5F, 'r', gap=45) # ṟ — r sits low, needs extra gap
_make_macron_below(font, 0x1E5F, 'r', y_adj=45) # ṟ — r sits low, needs extra gap

# ij U+0133 / IJ U+0132: Dutch IJ digraph ligatures.
# Position so the ink edges have the same gap as adjacent letters would after kerning (~40 units).
Expand Down Expand Up @@ -814,9 +847,40 @@ def _make_eng(font, cp, base_name, comma_name, x_frac=0.88, x_offset=0, y_offset
_g.addReference(_name)
_g.width = font[_name].width

# ɛ U+025B LATIN SMALL LETTER OPEN E — same letterform as Greek ε, 20% larger but
# stroke-weight corrected back to match (scaling thickens strokes proportionally).
_eps = font[0x03B5]
_g = font.createMappedChar(0x025B)
_g.clear()
_layer = fontforge.layer()
for _c in _eps.foreground:
_layer += _c
_g.foreground = _layer
_g.transform(psMat.scale(1.2))
_g.correctDirection()
_g.removeOverlap()
_g.changeWeight(-15)
_bb = _g.boundingBox()
_g.width = int(round(_bb[2] + 20))

# Greek uppercase derived by scaling the corresponding lowercase to capital height.
_cap_height = font['A'].boundingBox()[3]

# Ɛ U+0190 LATIN CAPITAL LETTER OPEN E — ɛ (U+025B) scaled to capital height.
_eps_lc_bb = font[0x025B].boundingBox()
_g = font.createMappedChar(0x0190)
_g.clear()
_layer = fontforge.layer()
for _c in font[0x025B].foreground:
_layer += _c
_g.foreground = _layer
_g.transform(psMat.scale(_cap_height / _eps_lc_bb[3]))
_g.correctDirection()
_g.removeOverlap()
_g.changeWeight(-20)
_bb = _g.boundingBox()
_g.width = int(round(_bb[2] + 20))


def _greek_lc_to_uc(font, lc_cp, uc_cp, snap=True, weight_delta=0):
"""Copy a Greek lowercase glyph and scale it to capital height.
Expand Down Expand Up @@ -893,6 +957,30 @@ def _greek_lc_to_uc(font, lc_cp, uc_cp, snap=True, weight_delta=0):
_g.width = font['n'].width


# ---------------------------------------------------------------------------
# Greek monotonic accented forms (tonos = acute accent)
# Lowercase bases that have their own hand-drawn glyphs use those glyph names.
# Bases aliased to Latin letters reference the Latin glyph directly to avoid
# chained composites (Alpha → A → A).
# ---------------------------------------------------------------------------

_make_accented(font, 0x03AC, 'alpha', '_acute_mark') # ά
_make_accented(font, 0x03AD, 'epsilon', '_acute_mark') # έ
_make_accented(font, 0x03AE, 'eta', '_acute_mark') # ή
_make_accented(font, 0x03AF, 'dotlessi', '_acute_mark') # ί (dotless — iota has no dot)
_make_accented(font, 0x03CC, 'o', '_acute_mark') # ό (o for omicron)
_make_accented(font, 0x03CD, 'upsilon', '_acute_mark') # ύ
_make_accented(font, 0x03CE, 'omega', '_acute_mark') # ώ

_make_accented(font, 0x0386, 'A', '_acute_mark') # Ά
_make_accented(font, 0x0388, 'E', '_acute_mark') # Έ
_make_accented(font, 0x0389, 'H', '_acute_mark') # Ή
_make_accented(font, 0x038A, 'I', '_acute_mark') # Ί
_make_accented(font, 0x038C, 'O', '_acute_mark') # Ό
_make_accented(font, 0x038E, 'Y', '_acute_mark') # Ύ
_make_accented(font, 0x038F, font[0x03A9].glyphname, '_acute_mark') # Ώ


# ---------------------------------------------------------------------------
# Save
# ---------------------------------------------------------------------------
Expand Down
25 changes: 25 additions & 0 deletions xkcd-script/generator/pt7_font_properties.py
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,31 @@ def expand(chars, left_side):
glyph.addAnchorPoint('above', 'base', cx, bb[3] + _BASE_GAP)


# ---------------------------------------------------------------------------
# Mark-to-base GPOS: position combining cedilla below base glyphs
# ---------------------------------------------------------------------------

font.addLookup('below', 'gpos_mark2base', (), [['mark', [['latn', ['dflt']]]]])
font.addLookupSubtable('below', 'below_sub')
font.addAnchorClass('below_sub', 'below')

# Mark anchor at top-centre of the combining cedilla glyph.
_c0327 = font[0x0327]
_c0327_bb = _c0327.boundingBox()
_c0327.addAnchorPoint('below', 'mark', (_c0327_bb[0] + _c0327_bb[2]) / 2, _c0327_bb[3])

# Base anchors at bottom-centre of ɛ and Ɛ, pulled up by 15 units to match the
# y_adj=-15 overlap used in the precomposed cedilla glyphs (avoids a rendering gap).
_BELOW_BASES = {0x025B, 0x0190} # ɛ Ɛ
for glyph in font.glyphs():
if glyph.unicode not in _BELOW_BASES:
continue
bb = glyph.boundingBox()
if bb[2] <= bb[0]:
continue
glyph.addAnchorPoint('below', 'base', (bb[0] + bb[2]) / 2, bb[1] + 15)


# ---------------------------------------------------------------------------
# CFF hinting zones and OS/2 metrics
# ---------------------------------------------------------------------------
Expand Down
Binary file modified xkcd-script/samples/charmap_combining_diacritical_marks.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_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_b.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.
Loading