├── .gitignore ├── Brewfile ├── LICENSE.md ├── Readme.md ├── deploy_site.sh ├── out └── .gitkeep ├── patcher.py ├── requirements.txt ├── site ├── .gitignore ├── 404.html ├── Gemfile ├── Gemfile.lock ├── _config.yml ├── _data │ └── versions.yml ├── downloads │ └── .gitkeep ├── fonts │ └── .gitkeep └── index.html └── test.html /.gitignore: -------------------------------------------------------------------------------- 1 | out/*.otf 2 | out/*.ttf 3 | out/*.zip 4 | out/*.woff2 5 | mods.fea 6 | fonts/* 7 | 8 | site/fonts/*.woff2 9 | site/downloads/*.zip 10 | site/_data/manifest.json 11 | -------------------------------------------------------------------------------- /Brewfile: -------------------------------------------------------------------------------- 1 | brew "fontforge" 2 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright 2019 Tristan Hume and others 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # Numderline 2 | 3 | Numderline is a hacky font patcher that takes a font and converts it into one that underlines alternating groups of 3 digits starting from the right. It can also do other similar tricks. 4 | 5 | It was inspired by [my job](https://www.janestreet.com/technology/) involving a lot of staring at numbers in nanoseconds and trying to pick out the milliseconds or microseconds. 6 | 7 | A blog post about this and a web page to see and download pre-patched fonts should hopefully be coming soon™. 8 | 9 | ## Features 10 | 11 | - Patch any font to underline alternating groups of 3 digits in numbers 12 | - For use in proportional contexts, can also just insert fake commas with shaping 13 | - Alternatively it can squish digits and group them closer together in threes 14 | - Or some other variants, including small monospace mini-commas! 15 | 16 | ### Usage 17 | 18 | 1. Clone the repo 19 | 1. Install the FontForge Python API, on macOS I used Homebrew to do this with `brew install fontforge`, or you can use `brew bundle` 20 | 1. Install the fonttools API, I used `pip3 install fonttools` to install it in my Homebrew python3, or you can use `pip3 install -r requirements.txt` 21 | 1. Run `python3 patcher.py FONT_FILE_TO_PATCH` and look in the `out` folder 22 | 23 | You can also run `python3 patcher.py --help` and read the source to see other options. 24 | -------------------------------------------------------------------------------- /deploy_site.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # Only works on Tristan's computer, ask him if you want the site deployed 3 | cp -r site/_site/* ~/Box/Sites/thume/numderline/ 4 | -------------------------------------------------------------------------------- /out/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trishume/numderline/e09528699a1bca52ce5e2d22a019dfdd034808ca/out/.gitkeep -------------------------------------------------------------------------------- /patcher.py: -------------------------------------------------------------------------------- 1 | # Based on https://github.com/powerline/fontpatcher/blob/develop/scripts/powerline-fontpatcher 2 | # Used under the MIT license 3 | 4 | import argparse 5 | import sys 6 | import re 7 | import subprocess 8 | import os.path 9 | import json 10 | import shutil 11 | import zipfile 12 | 13 | from itertools import chain 14 | from collections import defaultdict 15 | 16 | try: 17 | import fontforge 18 | import psMat 19 | # from fontTools.misc.py23 import * 20 | from fontTools.ttLib import TTFont 21 | from fontTools.feaLib.builder import addOpenTypeFeatures, Builder 22 | except ImportError: 23 | sys.stderr.write('The required FontForge and fonttools modules could not be loaded.\n\n') 24 | sys.stderr.write('You need FontForge with Python bindings for this script to work.\n') 25 | sys.exit(1) 26 | 27 | 28 | def get_argparser(ArgumentParser=argparse.ArgumentParser): 29 | parser = ArgumentParser( 30 | description=('Font patcher for Numderline. ' 31 | 'Requires FontForge with Python bindings. ' 32 | 'Stores the patched font as a new, renamed font file by default.') 33 | ) 34 | parser.add_argument('target_fonts', help='font files to patch', metavar='font', 35 | nargs='*', type=argparse.FileType('rb')) 36 | parser.add_argument('--group', 37 | help='group squished digits in threes, shorthand for --no-underline --shift-amount 100 --squish 0.85 --squish-all', 38 | default=False, action='store_true') 39 | parser.add_argument('--no-rename', 40 | help='don\'t add " with Numderline" to the font name', 41 | default=True, action='store_false', dest='rename_font') 42 | parser.add_argument('--no-underline', 43 | help='don\'t add underlines', 44 | default=True, action='store_false', dest='add_underlines') 45 | parser.add_argument('--no-decimals', 46 | help='don\'t touch digits after the decimal point', 47 | default=True, action='store_false', dest='do_decimals') 48 | parser.add_argument('--add-commas', 49 | help='add commas', 50 | default=False, action='store_true') 51 | parser.add_argument('--shift-amount', help='amount to shift digits to group them together, try 100', type=int, default=0) 52 | parser.add_argument('--squish', help='horizontal scale to apply to the digits to maybe make them more readable when shifted', type=float, default=1.0) 53 | parser.add_argument('--squish-all', 54 | help='squish all numbers, including decimals and ones less than 4 digits, use with --squish flag', 55 | default=False, action='store_true') 56 | parser.add_argument('--sub-font', help='substitute alternating groups of 3 with this font', type=argparse.FileType('rb')) 57 | parser.add_argument('--spaceless-commas', 58 | help='manipulate commas to not change the spacing, for monospace fonts, use with --add-commas', 59 | default=False, action='store_true') 60 | parser.add_argument('--debug-annotate', 61 | help='annotate glyph copies with debug digits', 62 | default=False, action='store_true') 63 | parser.add_argument('--build-release', 64 | help='build the default release of Numderline', 65 | default=False, action='store_true') 66 | return parser 67 | 68 | 69 | FONT_NAME_RE = re.compile(r'^([^-]*)(?:(-.*))?$') 70 | NUM_DIGIT_COPIES = 7 71 | 72 | def gen_feature(digit_names, underscore_name, dot_name, do_decimals): 73 | if do_decimals: 74 | decimal_sub = """ 75 | sub {dot_name} @digits' by @nd2; 76 | sub @nd2 @digits' by @nd1; 77 | sub @nd1 @digits' by @nd6; 78 | sub @nd6 @digits' by @nd5; 79 | sub @nd5 @digits' by @nd4; 80 | sub @nd4 @digits' by @nd3; 81 | sub @nd3 @digits' by @nd2; 82 | """ 83 | else: 84 | decimal_sub = """ 85 | ignore sub {dot_name} @digits'; 86 | sub @digits @digits' by @digits; 87 | """ 88 | 89 | decimal_sub = decimal_sub.format(dot_name=dot_name) 90 | 91 | feature = """ 92 | languagesystem DFLT dflt; 93 | languagesystem latn dflt; 94 | languagesystem cyrl dflt; 95 | languagesystem grek dflt; 96 | languagesystem kana dflt; 97 | @digits=[{digit_names}]; 98 | {nds} 99 | 100 | feature calt {{ 101 | {decimal_sub} 102 | 103 | sub @digits' @digits @digits @digits by @nd0; 104 | sub @nd0 @digits' by @nd0; 105 | 106 | reversesub @nd0' @nd0 by @nd1; 107 | reversesub @nd0' @nd1 by @nd2; 108 | reversesub @nd0' @nd2 by @nd3; 109 | reversesub @nd0' @nd3 by @nd4; 110 | reversesub @nd0' @nd4 by @nd5; 111 | reversesub @nd0' @nd5 by @nd6; 112 | reversesub @nd0' @nd6 by @nd1; 113 | }} calt; 114 | """[1:] 115 | 116 | nds = [' '.join(['nd{}.{}'.format(i,j) for j in range(10)]) for i in range(NUM_DIGIT_COPIES)] 117 | nds = ['@nd{}=[{}];'.format(i,nds[i]) for i in range(NUM_DIGIT_COPIES)] 118 | nds = "\n".join(nds) 119 | feature = feature.format(digit_names=' '.join(digit_names), 120 | nds=nds, underscore_name=underscore_name, decimal_sub=decimal_sub) 121 | with open('mods.fea', 'w') as f: 122 | f.write(feature) 123 | 124 | def shift_layer(layer, shift): 125 | layer = layer.dup() 126 | mat = psMat.translate(shift, 0) 127 | layer.transform(mat) 128 | return layer 129 | 130 | def squish_layer(layer, squish): 131 | layer = layer.dup() 132 | mat = psMat.scale(squish, 1.0) 133 | layer.transform(mat) 134 | return layer 135 | 136 | def add_comma_to(glyph, comma_glyph, spaceless): 137 | comma_layer = comma_glyph.layers[1].dup() 138 | x_shift = glyph.width 139 | y_shift = 0 140 | if spaceless: 141 | mat = psMat.scale(0.8, 0.8) 142 | comma_layer.transform(mat) 143 | x_shift -= comma_glyph.width / 2 144 | # y_shift = -200 145 | mat = psMat.translate(x_shift, y_shift) 146 | comma_layer.transform(mat) 147 | glyph.layers[1] += comma_layer 148 | if not spaceless: 149 | glyph.width += comma_glyph.width 150 | 151 | def annotate_glyph(glyph, extra_glyph): 152 | layer = extra_glyph.layers[1].dup() 153 | mat = psMat.translate(-(extra_glyph.width/2), 0) 154 | layer.transform(mat) 155 | mat = psMat.scale(0.3, 0.3) 156 | layer.transform(mat) 157 | mat = psMat.translate((extra_glyph.width/2), 0) 158 | layer.transform(mat) 159 | mat = psMat.translate(0, -600) 160 | layer.transform(mat) 161 | glyph.layers[1] += layer 162 | 163 | def out_path(name): 164 | return 'out/{0}.ttf'.format(name) 165 | 166 | def patch_one_font(font, rename_font, add_underlines, shift_amount, squish, squish_all, add_commas, spaceless_commas, debug_annotate, do_decimals, group, sub_font): 167 | font.encoding = 'ISO10646' 168 | 169 | if group: 170 | add_underlines = False 171 | shift_amount = 100 172 | squish = 0.85 173 | squish_all = True 174 | 175 | mod_name = 'N' 176 | if add_commas: 177 | if spaceless_commas: 178 | mod_name += 'onoCommas' 179 | else: 180 | mod_name += 'ommas' 181 | if add_underlines: 182 | mod_name += 'umderline' 183 | if sub_font is not None: 184 | mod_name += 'Sub' 185 | # Cleaner name for what I expect to be a common combination 186 | if shift_amount == 100 and squish == 0.85 and squish_all: 187 | mod_name += 'Group' 188 | else: 189 | if shift_amount != 0: 190 | mod_name += 'Shift{}'.format(shift_amount) 191 | if squish != 1.0: 192 | squish_s = '{}'.format(squish) 193 | mod_name += 'Squish{}'.format(squish_s.replace('.','p')) 194 | if squish_all: 195 | mod_name += 'All' 196 | if debug_annotate: 197 | mod_name += 'Debug' 198 | if not do_decimals: 199 | mod_name += 'NoDecimals' 200 | 201 | # Rename font 202 | if rename_font: 203 | font.familyname += ' with '+mod_name 204 | font.fullname += ' with '+mod_name 205 | fontname, style = FONT_NAME_RE.match(font.fontname).groups() 206 | font.fontname = fontname + 'With' + mod_name 207 | if style is not None: 208 | font.fontname += style 209 | font.appendSFNTName( 210 | 'English (US)', 'Preferred Family', font.familyname) 211 | font.appendSFNTName( 212 | 'English (US)', 'Compatible Full', font.fullname) 213 | 214 | digit_names = [font[code].glyphname for code in range(ord('0'),ord('9')+1)] 215 | test_names = [font[code].glyphname for code in range(ord('A'),ord('J')+1)] 216 | underscore_name = font[ord('_')].glyphname 217 | dot_name = font[ord('.')].glyphname 218 | # print(digit_names) 219 | 220 | if sub_font is not None: 221 | sub_font = fontforge.open(sub_font.name) 222 | 223 | underscore_layer = font[underscore_name].layers[1] 224 | 225 | # 0xE900 starts an area spanning until 0xF000 that as far as I can tell nothing 226 | # popular uses. I checked the Apple glyph browser and Nerd Font. 227 | # Uses an array because of python closure capture semantics 228 | encoding_alloc = [0xE900] 229 | def make_copy(src_font, loc, to_name, add_underscore, add_comma, shift, squish, annotate_with): 230 | encoding = encoding_alloc[0] 231 | src_font.selection.select(loc) 232 | src_font.copy() 233 | font.selection.select(encoding) 234 | font.paste() 235 | glyph = font[encoding] 236 | glyph.glyphname = to_name 237 | if squish != 1.0: 238 | glyph.layers[1] = squish_layer(glyph.layers[1], squish) 239 | if shift != 0: 240 | glyph.layers[1] = shift_layer(glyph.layers[1], shift) 241 | if add_underscore: 242 | glyph.layers[1] += underscore_layer 243 | if add_comma: 244 | add_comma_to(glyph, font[ord(',')], spaceless_commas) 245 | if annotate_with is not None: 246 | annotate_glyph(glyph, annotate_with) 247 | encoding_alloc[0] += 1 248 | 249 | for copy_i in range(0,NUM_DIGIT_COPIES): 250 | for digit_i in range(0,10): 251 | shift = 0 252 | if copy_i % 3 == 0: 253 | shift = -shift_amount 254 | elif copy_i % 3 == 2: 255 | shift = shift_amount 256 | in_alternating_group = (copy_i >= 3 and copy_i < 6) 257 | add_underscore = add_underlines and in_alternating_group 258 | add_comma = add_commas and (copy_i == 3 or copy_i == 6) 259 | annotate_with = font[digit_names[copy_i]] if debug_annotate else None 260 | use_sub_font = (sub_font is not None) and in_alternating_group 261 | src_font = sub_font if use_sub_font else font 262 | make_copy(src_font, digit_names[digit_i], 'nd{}.{}'.format(copy_i,digit_i), add_underscore, add_comma, shift, squish, annotate_with) 263 | 264 | if squish_all and squish != 1.0: 265 | for digit in digit_names: 266 | glyph = font[digit] 267 | glyph.layers[1] = squish_layer(glyph.layers[1], squish) 268 | 269 | gen_feature(digit_names, underscore_name, dot_name, do_decimals) 270 | 271 | font.generate('out/tmp.ttf') 272 | ft_font = TTFont('out/tmp.ttf') 273 | addOpenTypeFeatures(ft_font, 'mods.fea', tables=['GSUB']) 274 | # replacement to comply with SIL Open Font License 275 | out_name = font.fullname.replace('Source ', 'Sauce ') 276 | ft_font.save(out_path(out_name)) 277 | print("> Created '{}'".format(out_name)) 278 | 279 | if sub_font is not None: 280 | sub_font.close() 281 | 282 | return out_name 283 | 284 | 285 | def patch_fonts(target_files, *args): 286 | res = None 287 | for target_file in target_files: 288 | target_font = fontforge.open(target_file.name) 289 | try: 290 | res = patch_one_font(target_font, *args) 291 | finally: 292 | target_font.close() 293 | return res 294 | 295 | def source_font_path(weight, is_italic): 296 | suffix = weight 297 | if is_italic and weight == 'Regular': 298 | suffix = '' 299 | if is_italic: 300 | suffix += 'It' 301 | return "fonts/source-code-pro/SourceCodePro-{}.ttf".format(suffix) 302 | 303 | def build_release(): 304 | infos = { 305 | 'DejaVuSansMono' : {'prefix': 'fonts/dejavu/DejaVuSansMono', 'suffixes': ['', '-Bold', '-Oblique', '-BoldOblique']}, 306 | 'DejaVuSans' : {'prefix': 'fonts/dejavu/DejaVuSans', 'suffixes': ['', '-Bold', '-Oblique', '-BoldOblique']}, 307 | 'UbuntuMono' : {'prefix': 'fonts/ubuntu/UbuntuMono', 'suffixes': ['-R', '-B', '-RI', '-BI']}, 308 | 'Hack' : {'prefix': 'fonts/hack/Hack', 'suffixes': ['-Regular', '-Bold', '-Italic', '-BoldItalic']}, 309 | 'SauceCode' : {'prefix': 'fonts/source-code-pro/SourceCodePro', 'suffixes': ['-Regular', '-Light', '-Bold', '-It', '-BoldIt']}, 310 | } 311 | 312 | to_build = [ 313 | ('debug', 'DejaVuSansMono', ['fonts/dejavu/DejaVuSansMono.ttf', '--no-underline', '--debug-annotate']) 314 | # ['fonts/source-code-pro/SourceCodePro-Light.ttf', '--no-underline', '--sub-font', 'fonts/source-code-pro/SourceCodePro-Regular.ttf'] 315 | ] 316 | 317 | def for_all_weights(name, set_name, opts): 318 | info = infos[name] 319 | for suffix in info['suffixes']: 320 | to_build.append((set_name, name, ['{}{}.ttf'.format(info['prefix'], suffix)]+opts)) 321 | 322 | for name in ['DejaVuSansMono', 'UbuntuMono', 'SauceCode']: 323 | for_all_weights(name, 'numderline', []) 324 | 325 | for_all_weights('DejaVuSans', 'nommas', ['--no-underline', '--add-commas', '--no-decimals']) 326 | 327 | all_monospace = ['DejaVuSansMono', 'UbuntuMono', 'Hack', 'SauceCode'] 328 | for name in all_monospace: 329 | for_all_weights(name, 'ngroup', ['--no-underline', '--group']) 330 | for name in all_monospace: 331 | for_all_weights(name, 'monocommas', ['--no-underline', '--group', '--add-commas', '--spaceless-commas', '--no-decimals']) 332 | 333 | source_sub_series = ['Regular', 'Semibold', 'Bold', 'Black'] 334 | for i, weight in enumerate(source_sub_series[:-1]): 335 | for it in [False, True]: 336 | bolder = source_sub_series[i+1] 337 | to_build.append(('nbold','SauceCode',[source_font_path(weight, it), '--no-underline', '--sub-font', source_font_path(bolder, it)])) 338 | 339 | 340 | manifest = defaultdict(lambda: defaultdict(lambda: [])) 341 | for set_name, font_id, args in to_build: 342 | out_name = main(args) 343 | if out_name is None: 344 | raise Exception("Build failed") 345 | manifest[set_name][font_id].append(out_name) 346 | 347 | print(manifest) 348 | 349 | for variant, fonts in manifest.items(): 350 | for font, weights in fonts.items(): 351 | archive_name = "{}-{}".format(font, variant) 352 | with zipfile.ZipFile("site/downloads/{}.zip".format(archive_name), 'w', compression=zipfile.ZIP_DEFLATED) as myzip: 353 | for weight in weights: 354 | myzip.write(out_path(weight), '{}/{}.ttf'.format(archive_name, weight)) 355 | 356 | with open('site/_data/manifest.json','w') as f: 357 | f.write(json.dumps(manifest)) 358 | 359 | for fonts in manifest.values(): 360 | for weights in fonts.values(): 361 | subprocess.run(['woff2_compress',out_path(weights[0])]) 362 | shutil.copyfile("out/{}.woff2".format(weights[0]), "site/fonts/{}.woff2".format(weights[0])) 363 | 364 | 365 | def main(argv): 366 | args = get_argparser().parse_args(argv) 367 | 368 | if args.build_release: 369 | build_release() 370 | return 371 | 372 | return patch_fonts(args.target_fonts, args.rename_font, args.add_underlines, args.shift_amount, args.squish, args.squish_all, 373 | args.add_commas, args.spaceless_commas, args.debug_annotate, args.do_decimals, args.group, args.sub_font) 374 | 375 | 376 | main(sys.argv[1:]) 377 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | fonttools>4.0,<4.1 2 | -------------------------------------------------------------------------------- /site/.gitignore: -------------------------------------------------------------------------------- 1 | _site 2 | .sass-cache 3 | .jekyll-cache 4 | .jekyll-metadata 5 | vendor 6 | -------------------------------------------------------------------------------- /site/404.html: -------------------------------------------------------------------------------- 1 | --- 2 | permalink: /404.html 3 | layout: default 4 | --- 5 | 6 | 19 | 20 |
21 |

404

22 | 23 |

Page not found :(

24 |

The requested page could not be found.

25 |
26 | -------------------------------------------------------------------------------- /site/Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | gem "jekyll", "~> 4.0.0" 3 | 4 | -------------------------------------------------------------------------------- /site/Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | addressable (2.7.0) 5 | public_suffix (>= 2.0.2, < 5.0) 6 | colorator (1.1.0) 7 | concurrent-ruby (1.1.5) 8 | em-websocket (0.5.1) 9 | eventmachine (>= 0.12.9) 10 | http_parser.rb (~> 0.6.0) 11 | eventmachine (1.2.7) 12 | ffi (1.11.1) 13 | forwardable-extended (2.6.0) 14 | http_parser.rb (0.6.0) 15 | i18n (1.7.0) 16 | concurrent-ruby (~> 1.0) 17 | jekyll (4.0.0) 18 | addressable (~> 2.4) 19 | colorator (~> 1.0) 20 | em-websocket (~> 0.5) 21 | i18n (>= 0.9.5, < 2) 22 | jekyll-sass-converter (~> 2.0) 23 | jekyll-watch (~> 2.0) 24 | kramdown (~> 2.1) 25 | kramdown-parser-gfm (~> 1.0) 26 | liquid (~> 4.0) 27 | mercenary (~> 0.3.3) 28 | pathutil (~> 0.9) 29 | rouge (~> 3.0) 30 | safe_yaml (~> 1.0) 31 | terminal-table (~> 1.8) 32 | jekyll-sass-converter (2.0.1) 33 | sassc (> 2.0.1, < 3.0) 34 | jekyll-watch (2.2.1) 35 | listen (~> 3.0) 36 | kramdown (2.1.0) 37 | kramdown-parser-gfm (1.1.0) 38 | kramdown (~> 2.0) 39 | liquid (4.0.3) 40 | listen (3.2.0) 41 | rb-fsevent (~> 0.10, >= 0.10.3) 42 | rb-inotify (~> 0.9, >= 0.9.10) 43 | mercenary (0.3.6) 44 | pathutil (0.16.2) 45 | forwardable-extended (~> 2.6) 46 | public_suffix (4.0.1) 47 | rb-fsevent (0.10.3) 48 | rb-inotify (0.10.0) 49 | ffi (~> 1.0) 50 | rouge (3.11.1) 51 | safe_yaml (1.0.5) 52 | sassc (2.2.1) 53 | ffi (~> 1.9) 54 | terminal-table (1.8.0) 55 | unicode-display_width (~> 1.1, >= 1.1.1) 56 | unicode-display_width (1.6.0) 57 | 58 | PLATFORMS 59 | ruby 60 | 61 | DEPENDENCIES 62 | jekyll (~> 4.0.0) 63 | 64 | BUNDLED WITH 65 | 2.0.2 66 | -------------------------------------------------------------------------------- /site/_config.yml: -------------------------------------------------------------------------------- 1 | title: Numderline 2 | email: tristan@thume.ca 3 | baseurl: "" # the subpath of your site, e.g. /blog 4 | url: "" # the base hostname & protocol for your site, e.g. http://example.com 5 | -------------------------------------------------------------------------------- /site/_data/versions.yml: -------------------------------------------------------------------------------- 1 | - title: Underlining 2 | set_name: numderline 3 | desc: The flagship "Numderline" variant underlines alternating groups of 3 digits. It's what I use at work. 4 | - title: Grouping 5 | set_name: ngroup 6 | desc: This variant squishes digits together into groups of 3. It's nice looking and works for most fonts but is annoying for editable text since it jumps around while typing. 7 | - title: Bolding 8 | set_name: nbold 9 | desc: For fonts with lots of weights, making alternating groups slightly bolder is an alternative to underlines. 10 | - title: Commas 11 | set_name: nommas 12 | desc: Inserting fake commas is a good option in proportional contexts where changing the width doesn't matter. 13 | - title: Monospace Commas 14 | set_name: monocommas 15 | desc: By combining the grouping variant with tiny commas this variant adds commas while remaining monospaced. 16 | - title: Debug 17 | set_name: debug 18 | desc: The debug version shows how the font uses shaping tricks to replace digits with alternate versions that track the position modulo 7. 19 | -------------------------------------------------------------------------------- /site/downloads/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trishume/numderline/e09528699a1bca52ce5e2d22a019dfdd034808ca/site/downloads/.gitkeep -------------------------------------------------------------------------------- /site/fonts/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trishume/numderline/e09528699a1bca52ce5e2d22a019dfdd034808ca/site/fonts/.gitkeep -------------------------------------------------------------------------------- /site/index.html: -------------------------------------------------------------------------------- 1 | --- 2 | --- 3 | 4 | 5 | Numderline Test 6 | 21 | 93 | 98 | 99 | 100 |

Numderline

101 | 102 |

103 | Numderline is a font patcher that uses OpenType font shaping trickery to make it easier to visually parse large numbers. It has multiple variants for different preferences, fonts and contexts. 104 |

105 |

106 | I (Tristan Hume) was inspired to build this by my job at Jane Street involving staring at a lot of large numbers, mostly latencies in nanoseconds, and wanting to pick out larger quantities like milliseconds and microseconds. 107 |

108 |

109 | See the code on Github to contribute or patch fonts of your choice. 110 |

111 | {% for version in site.data.versions %} 112 |

{{ version.title }}

113 |

{{ version.desc }}

114 |
115 | {% if version.set_name == 'nommas' %} 116 | 119 | {% else %} 120 | 130 | {% endif %} 131 |
132 | 133 | Download (hover to preview): 134 | {% assign faces = site.data.manifest[version.set_name] %} 135 | {% for face in faces %} 136 | {{face[0]}} 137 | {% endfor %} 138 | 139 | {% endfor %} 140 |

Notes

141 | 148 | 149 | 150 | -------------------------------------------------------------------------------- /test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Numderline Test 4 | 62 | 63 | 64 | 68 |
 69 | the quick brown fox jumps over the lazy dog
 70 | this <= that && why == zeven === five >>= lol
 71 | hamburgefont
 72 | 1
 73 | 12
 74 | 123
 75 | 1234
 76 | 12345
 77 | 123456
 78 | 1234567
 79 | 12345678
 80 | 123456789
 81 | 1234567890
 82 | 12345678901
 83 | 123456789012
 84 | 1234567890123
 85 | 12345678901234
 86 | 123456789012345
 87 | 1234567890123456
 88 | 12345678901234567
 89 | 123456789012345678
 90 | 1234567890123456789
 91 | 12345678901234567890
 92 | 
 93 | 1234.1
 94 | 1234.12
 95 | 1234.123
 96 | 1234.1234
 97 | 1234.12345
 98 | 1234.123456
 99 | 1234.1234567
100 | 1234.12345678
101 | 1234.123456789
102 | 1234.1234567890
103 | 
104 | 111111
105 | 222222
106 | 777777
107 | 
108 | 123456 12345678 12345678
109 | 
110 | aoeu1234125otnuehnth
111 | 12543,1243152,12543523
112 |
113 | the quick brown fox jumps over the lazy dog
114 | 1234567890 123
115 | 1234567 123423,124356.0000000
116 |
117 | the quick brown fox jumps over the lazy dog
118 | 1234567890 123
119 | 1234567 123423,124356.0000000
120 |
121 | the quick brown fox jumps over the lazy dog
122 | 1234567890 123
123 | 1234567 123423,124356.0000000
124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 |
BenchmarkNanoseconds
blah_foo_bar17892384923
blah_baz116892323
blah_baz214772243
blah_baz316423444
blah_baz416978123
blah_qux17923
blah_lorem234923
134 |
135 | time                       system      flow_type   latency_ns  epoch             ack_latency_ns  transit_delay_ns
136 | -------------------------  ----------  ----------  ----------  ----------------  --------------  ----------------
137 | 2019-10-02 22:11:59 -0400  test        Order            31439  1570068719.15762          814364        9045303407
138 | 2019-10-02 22:11:59 -0400  test        Cancel           65354  1570068719.15764          863851        8572002092
139 | 2019-10-02 22:11:59 -0400  test        Cancel           74405  1570068719.15765          837488        2089369225
140 | 2019-10-02 22:11:59 -0400  test        Cancel            5279  1570068719.15767          223318        5853333820
141 | 2019-10-02 22:11:59 -0400  test        Cancel           88937  1570068719.15768           49391       31165277747
142 | 2019-10-02 22:11:59 -0400  test        Cancel           17868  1570068719.15769          456572       11704916307
143 | 2019-10-02 22:11:59 -0400  test        Cancel            6392  1570068719.15771          733550       38885924077
144 | 2019-10-02 22:11:59 -0400  test        Order            18447  1570068719.15772          407225       43493701983
145 | 2019-10-02 22:11:59 -0400  test        Cancel            4202  1570068719.15773           66013       33465308851
146 | 2019-10-02 22:11:59 -0400  test        Cancel           49780  1570068719.15774          516106       16016644531
147 | 2019-10-02 22:11:59 -0400  test        Order            97926  1570068719.15775          265912       19644235826
148 | 2019-10-02 22:11:59 -0400  test        Cancel           19296  1570068719.15777           10634       15924194457
149 | 2019-10-02 22:11:59 -0400  test        Order            12823  1570068719.15778          535120       23514150184
150 | 2019-10-02 22:11:59 -0400  test        Cancel           68016  1570068719.15779           67837       47286090933
151 | 2019-10-02 22:11:59 -0400  test        Cancel           78940  1570068719.15780          602530       27100187791
152 | 2019-10-02 22:11:59 -0400  test        Cancel           35906  1570068719.15782          803041       39733642501
153 | 2019-10-02 22:11:59 -0400  test        Cancel           55789  1570068719.15783          989714       40888487353
154 | 2019-10-02 22:11:59 -0400  test        Cancel           50383  1570068719.15784          862812        2585391372
155 | 2019-10-02 22:11:59 -0400  test        Cancel             288  1570068719.15785          826173        6026064740
156 | 2019-10-02 22:11:59 -0400  test        Order            89229  1570068719.15786          562273        3911931282
157 | 2019-10-02 22:11:59 -0400  test        Order            12094  1570068719.15788          871769       32250236519
158 | 2019-10-02 22:11:59 -0400  test        Order            32439  1570068719.15789          291946        7990450518
159 | 2019-10-02 22:11:59 -0400  test        Order            65752  1570068719.15790          677662       48878932453
160 | 2019-10-02 22:11:59 -0400  test        Cancel           73670  1570068719.15791          456673       41427748520
161 | 2019-10-02 22:11:59 -0400  test        Cancel           56356  1570068719.15792          613750       14742523916
162 | 2019-10-02 22:11:59 -0400  test        Order            76657  1570068719.15794          594455       26118260407
163 | 2019-10-02 22:11:59 -0400  test        Order            51382  1570068719.15795          766570       48838516772
164 | 2019-10-02 22:11:59 -0400  test        Order            46687  1570068719.15796          332451       43413161771
165 | 2019-10-02 22:11:59 -0400  test        Cancel           67096  1570068719.15797           25554       33673299015
166 | 2019-10-02 22:11:59 -0400  test        Cancel           11056  1570068719.15798          239809       38505070571
167 | 2019-10-02 22:11:59 -0400  test        Order            27827  1570068719.15800          736729       21898159224
168 | 2019-10-02 22:11:59 -0400  test        Cancel           97811  1570068719.15801          632739       30448521554
169 | 2019-10-02 22:11:59 -0400  test        Order            10496  1570068719.15802           97349       40958374404
170 | 2019-10-02 22:11:59 -0400  test        Order            35046  1570068719.15803          241790        9916338303
171 | 2019-10-02 22:11:59 -0400  test        Order            96075  1570068719.15804          678424       24873810307
172 | 2019-10-02 22:11:59 -0400  test        Order            17361  1570068719.15806          126723       10457495989
173 | 2019-10-02 22:11:59 -0400  test        Order            35683  1570068719.15807          989790        8931144993
174 | 2019-10-02 22:11:59 -0400  test        Order            21781  1570068719.15808          472297       14425326102
175 | 2019-10-02 22:11:59 -0400  test        Cancel           49254  1570068719.15809          965808       26247909055
176 | 2019-10-02 22:11:59 -0400  test        Cancel           56166  1570068719.15810          664022       48883316442
177 | 2019-10-02 22:11:59 -0400  test        Cancel           41950  1570068719.15812          383750       36802874991
178 | 2019-10-02 22:11:59 -0400  test        Order            93157  1570068719.15813          732659       46854420709
179 | 2019-10-02 22:11:59 -0400  test        Order             8183  1570068719.15819          778452       16661995101
180 | 2019-10-02 22:11:59 -0400  test        Cancel           57488  1570068719.15821          887350       12134193056
181 | 2019-10-02 22:11:59 -0400  test        Order            29980  1570068719.15823          517741       36356083776
182 | 2019-10-02 22:11:59 -0400  test        Order             5203  1570068719.1583            23081       16638430992
183 | 2019-10-02 22:11:59 -0400  test        Order            54527  1570068719.15832          296844       39210574584
184 | 2019-10-02 22:11:59 -0400  test        Order            38379  1570068719.15833          449316       25962337037
185 | 2019-10-02 22:11:59 -0400  test        Order            57342  1570068719.15835          950924         404072538
186 | 2019-10-02 22:11:59 -0400  test        Cancel           35902  1570068719.15836          411195       29564962362
187 |     
188 | 189 | 190 | --------------------------------------------------------------------------------