char/iocgn-mono
iosevka variant inspired by Simple Köln-Bonn
git clone https://git.t4t.associates/char/iocgn-mono
a3bd267
main
1import { CombineGeometry , ContourSetGeometry , ReferenceGeometry , SimplifyGeometry } from '@iosevka/geometry' ; 2import { Anchor } from '@iosevka/geometry/anchor' ; 3import { Point } from '@iosevka/geometry/point' ; 4import { Transform } from '@iosevka/geometry/transform' ; 5import { Glyph } from '@iosevka/glyph' ; 6import { Dotless } from '@iosevka/glyph/relation' ; 7import { GlyphStore } from '@iosevka/glyph/store' ; 8import { refineArrows } from './iocgn-arrows.mjs' ; 9import config from './iocgn-config.mjs' ; 10import { bounds , roundCorners } from './iocgn-round.mjs' ; 11 12const CELL = 500 , MARGIN = 14 ; 13const MARKS = [ 0x300 , 0x301 , 0x302 , 0x303 , 0x304 , 0x306 , 0x307 , 0x308 , 0x30A , 0x30B , 0x30C , 0x327 , 0x328 ]; 14const range = ( first , last ) => Array . from ({ length :last - first + 1 }, ( _ , i ) => first + i ); 15const codes = string => [ ...string ]. map ( char => char . codePointAt ( 0 )); 16const ARROWS = new Set ( codes ( config . terminal . arrows )); 17const CELL_GRAPHICS = new Set ( config . terminal . ranges . flatMap (([ first , last ]) => range ( first , last ))); 18 19// Geometry widths are independent of the exported 500-unit advances. 20function geometryWidth ( cp ) { 21if ( cp === 0x2C6 ) return 540 ; 22if ( CELL_GRAPHICS . has ( cp ) || ( MARKS . includes ( cp ) && cp !== 0x327 )) return 500 ; 23const char = String . fromCodePoint ( cp ); 24if ( cp < 384 && ! config . symbols . characters . includes ( char )) { 25const base = char . normalize ( 'NFD' )[ 0 ]; 26for ( const width of [ 540 , 500 ]) if ( config . geometry [ width ]. includes ( base )) return width ; 27} 28return 660 ; 29} 30 31const transformed = ( contours , tfm ) => contours . map ( c => c . map ( z => Point . transformed ( tfm , z ))); 32const translated = ( contours , x , y ) => transformed ( contours , Transform . Translate ( x , y )); 33 34function cellTransform ( contours , width , scale = 1 , nudge = 0 ) { 35const box = bounds ( contours ); 36let dx = ( CELL - width * scale ) / 2 ; 37if ( box && scale * ( box . xMax - box . xMin ) <= CELL - 2 * MARGIN ) { 38dx = Math . max ( MARGIN - scale * box . xMin , Math . min ( dx , CELL - MARGIN - scale * box . xMax )); 39} 40return new Transform ( scale , 0 , 0 , 1 , dx + nudge , 0 ); 41} 42 43export function buildNative ( para , buildGlyphs ) { 44const base = buildGlyphs ( para . createFork ( argv => { argv . shape . width = 660 ; }), undefined , true ); 45const { executor} = base ; 46const forks = new Map ([[ 660 , base ]]); 47const candidates = [ ...range ( 32 , 383 ), 0x2C6 , ...MARKS , ...CELL_GRAPHICS ]; 48for ( const width of [ 540 , 500 ]) { 49const codes = candidates . filter ( cp => geometryWidth ( cp ) === width ); 50const forkPara = para . createFork ( argv => { argv . shape . width = width ; }); 51forkPara . koelnCompactM = width === 500 ; 52const roots = codes . map ( cp => base . glyphStore . queryByUnicodeEnsured ( cp )); 53// Enclosure registration also resolves its mosaic ingredients eagerly. 54if ( codes . includes ( 0xA9 )) { 55roots . push ( base . glyphStore . queryByUnicodeEnsured ( 0x2714 ), base . glyphStore . queryByUnicodeEnsured ( 0x21B2 )); 56} 57// Miniature ingredients belong to another executor's dependency graph. 58if ( codes . includes ( 0xAE )) roots . push ( base . glyphStore . queryByNameEnsured ( 'combRingCap/adwsN' )); 59const filter = executor . dependencyManager . traverseDependencies ( roots ); 60// Derived glyph names depend on geometry, so only filter whole blocks. 61filter . glyphIsNeeded = () => true ; 62const blockIsNeeded = filter . blockIsNeeded . bind ( filter ); 63const rootBlocks = new Set ( roots . map ( glyph => executor . dependencyManager . glyphToBlock . get ( glyph ))); 64filter . blockIsNeeded = id => id === 'AutoBuild-Accents' || 65( / ^autobuild- / i . test ( id ) ?rootBlocks . has ( id ) :blockIsNeeded ( id )); 66filter . koeln = true ; 67filter . koelnCodes = new Set ( codes ); 68forks . set ( width , buildGlyphs ( forkPara , filter , true )); 69} 70refineArrows ( base . glyphStore ); 71return { glyphStore :assemble ( forks , base . fontMetrics ), fontMetrics :base . fontMetrics }; 72} 73 74// Outlines are simplified, rounded and placed here; Iosevka exports them verbatim. 75function assemble ( forks , fontMetrics ) { 76const xHeight = fontMetrics . os2 . sxHeight , capHeight = fontMetrics . os2 . sCapHeight ; 77const ascent = fontMetrics . os2 . sTypoAscender , descent = fontMetrics . os2 . sTypoDescender ; 78// hhea mirrors the typographic line box. 79fontMetrics . hhea . descender = descent ; 80fontMetrics . hhea . lineGap = fontMetrics . os2 . sTypoLineGap ; 81fontMetrics . os2 . xAvgCharWidth = CELL ; 82 83const sourceOf = ( cp , width = geometryWidth ( cp )) => forks . get ( width ). glyphStore . queryByUnicodeEnsured ( cp ); 84const radius = width => { 85const stroke = forks . get ( width ). metrics . Stroke ; 86return Math . max ( stroke * .16 , stroke * .5 - 21.5 ); 87}; 88// Terminal symbols and cell graphics keep their sharp corners. 89const radiusOf = cp => cp < 0x2100 ?radius ( geometryWidth ( cp )) :0 ; 90 91const outlines = new Map (); 92// Like TrueType components, referenced glyphs are simplified and rounded on their own. 93function outline ( glyph , r ) { 94if ( ! outlines . has ( glyph )) outlines . set ( glyph , new Map ()); 95const memo = outlines . get ( glyph ); 96if ( memo . has ( r )) return memo . get ( r ); 97const refs = glyph . geometry . toReferences (); 98let contours ; 99if ( refs ) contours = refs . flatMap ( ref => translated ( outline ( ref . glyph , r ), ref . x , ref . y )); 100// Simplifying these polygons makes their point topology vary across weights. 101else if ( glyph . koelnPreserveTopology ) contours = glyph . geometry . toContours (); 102else { 103contours = SimplifyGeometry . wrapWithGizmo ( glyph . geometry , glyph . gizmo ). toContours (); 104if ( r ) contours = roundCorners ( contours , r ); 105} 106memo . set ( r , contours ); 107return contours ; 108} 109 110const store = new GlyphStore (); 111function addGlyph ( name , source , geometry , anchorTransform , advance ) { 112if ( store . queryByName ( name )) throw new Error ( `Duplicate glyph name ${ name } ` ); 113const glyph = new Glyph ( name ); 114// Other relations name glyphs outside this store. 115if ( Dotless . get ( source )) Dotless . set ( glyph , Dotless . get ( source )); 116glyph . cloneRankFromGlyph ( source ); 117if ( anchorTransform ) { 118for ( const kind of [ 'baseAnchors' , 'markAnchors' ]) { 119for ( const [ key , anchor ] of Object . entries ( source [ kind ])) { 120glyph [ kind ][ key ] = Anchor . transform ( anchorTransform , anchor ); 121} 122} 123} 124glyph . geometry = geometry ; 125glyph . koelnFinal = true ; 126glyph . advanceWidth = advance ; 127store . addGlyph ( name , glyph ); 128return glyph ; 129} 130 131const placed = new Map (); 132function emit ( cp , contours , anchorTransform , encode = true ) { 133const fork = forks . get ( geometryWidth ( cp )). glyphStore ; 134const source = fork . queryByUnicodeEnsured ( cp ); 135placed . set ( cp , contours ); 136const glyph = addGlyph ( fork . queryNameOf ( source ), source , new ContourSetGeometry ( contours ), 137anchorTransform , MARKS . includes ( cp ) ?0 :CELL ); 138if ( encode ) store . encodeGlyph ( cp , glyph ); 139return glyph ; 140} 141 142const notdef = forks . get ( 660 ). glyphStore . queryByNameEnsured ( '.notdef' ); 143const notdefContours = outline ( notdef , 0 ); 144const notdefTransform = cellTransform ( notdefContours , 660 ); 145addGlyph ( '.notdef' , notdef , new ContourSetGeometry ( transformed ( notdefContours , notdefTransform )), notdefTransform , CELL ); 146 147const offsets = new Map (); 148for ( const cp of range ( 32 , 126 )) { 149const contours = outline ( sourceOf ( cp ), radiusOf ( cp )); 150const tfm = cellTransform ( contours , geometryWidth ( cp ), 1 , config . nudge [ String . fromCodePoint ( cp )] ?? 0 ); 151let result = transformed ( contours , tfm ); 152if ( cp === 0x69 || cp === 0x6A ) { 153// Keep the approved tittles independent of accent metrics. 154const ys = result . flat (). filter ( z => z . y > xHeight ). map ( z => z . y ); 155const shift = Math . round ( xHeight + 150 - ( Math . min ( ...ys ) + Math . max ( ...ys )) / 2 ); 156result = result . map ( c => c . map ( z => z . y > xHeight ?Point . translated ( z , 0 , shift ) :z )); 157} 158offsets . set ( cp , tfm . tx ); 159emit ( cp , result , tfm ); 160} 161const dotless = new Map (); 162for ( const [ cp , letter ] of [[ 0x131 , 0x69 ], [ 0x237 , 0x6A ]]) { 163const tfm = Transform . Translate ( offsets . get ( letter ), 0 ); 164dotless . set ( letter , emit ( cp , transformed ( outline ( sourceOf ( cp ), radiusOf ( cp )), tfm ), tfm , cp === 0x131 )); 165} 166 167const decompositions = new Map (); 168for ( const cp of range ( 160 , 383 )) { 169const parts = codes ( String . fromCodePoint ( cp ). normalize ( 'NFD' )); 170if ( parts . length === 2 ) decompositions . set ( cp , parts ); 171else if ( cp === 0xA1 || cp === 0xBF ) { 172const upright = placed . get ( cp === 0xA1 ?0x21 :0x3F ); 173emit ( cp , transformed ( upright , new Transform ( - 1 , 0 , 0 , - 1 , CELL , xHeight )), null ); 174} else if ( cp === 0xB1 ) { 175emit ( cp , [ ...translated ( placed . get ( 0x2B ), 0 , 150 ), ...translated ( placed . get ( 0x2D ), 0 , - 308 )], null ); 176} else if ( cp !== 0x131 ) { 177const contours = outline ( sourceOf ( cp ), radiusOf ( cp )), box = bounds ( contours ); 178const scale = box ?Math . min ( 1 , ( CELL - 2 * MARGIN ) / ( box . xMax - box . xMin )) :1 ; 179const tfm = cellTransform ( contours , geometryWidth ( cp ), scale , config . nudge [ String . fromCodePoint ( cp )] ?? 0 ); 180emit ( cp , transformed ( contours , tfm ), tfm ); 181} 182} 183 184for ( const cp of MARKS ) { 185const tfm = Transform . Translate (( CELL + geometryWidth ( cp )) / 2 , 0 ); 186emit ( cp , transformed ( outline ( sourceOf ( cp ), radiusOf ( cp )), tfm ), tfm ); 187} 188 189for ( const [ cp , [ base , mark ]] of decompositions ) { 190const width = geometryWidth ( base ), dx = offsets . get ( base ), r = radius ( width ); 191// A lone reference is a merged letter-and-mark design, which Iosevka would export as one outline. 192const source = sourceOf ( cp ), refs = source . geometry . toReferences (); 193let accent ; 194if ( mark === 0x30B ) { 195// Wider native marks merge the two acute strokes at heavy weights. 196const upper = String . fromCodePoint ( base ) !== String . fromCodePoint ( base ). toLowerCase (); 197accent = translated ( outline ( sourceOf ( mark ), radiusOf ( mark )), CELL , upper ?capHeight - xHeight :0 ); 198} else if ( width === 540 && mark === 0x302 ) { 199// Preserve the spacing accent's unsplit outline on the narrow w. 200accent = translated ( outline ( sourceOf ( 0x2C6 ), r ), dx , 0 ); 201} else if ( refs ?. length > 1 ) { 202// Apostrophe-carons use reverse component order upstream. 203const baseIndex = [ 0x10F , 0x13D , 0x13E , 0x165 ]. includes ( cp ) ?refs . length - 1 :0 ; 204accent = refs . filter (( _ , i ) => i !== baseIndex ) 205. flatMap ( ref => translated ( outline ( ref . glyph , r ), ref . x + dx , ref . y )); 206} else { 207const markWidth = mark === 0x328 && width === 660 ?660 :geometryWidth ( mark ); 208let x = width + dx ; 209if ( mark === 0x328 ) x += ( bounds ( outline ( sourceOf ( base ), r )). xMax - width / 2 ) * .65 ; 210accent = translated ( outline ( sourceOf ( mark , markWidth ), radius ( markWidth )), x , 0 ); 211} 212const box = bounds ( accent ); 213if ( box ) { 214// Preserve d's bowl rather than narrowing it for the apostrophe-caron. 215const shiftY = cp === 0x10F ?Math . max ( 0 , bounds ( placed . get ( base )). yMax + 40 - box . yMin ) :0 ; 216accent = translated ( accent , Math . max ( MARGIN - box . xMin , Math . min ( 0 , CELL - MARGIN - box . xMax )), shiftY ); 217} 218const name = forks . get ( width ). glyphStore . queryNameOf ( source ); 219const accentGlyph = addGlyph ( `accent. ${ name } ` , source , new ContourSetGeometry ( accent ), null , 0 ); 220const baseGlyph = dotless . has ( base ) && mark < 0x320 ?dotless . get ( base ) :store . queryByUnicode ( base ); 221const composite = addGlyph ( name , source , new CombineGeometry ([ 222new ReferenceGeometry ( baseGlyph , 0 , 0 ), new ReferenceGeometry ( accentGlyph , 0 , 0 ), 223]), Transform . Translate ( dx , 0 ), CELL ); 224store . encodeGlyph ( cp , composite ); 225} 226 227for ( const cp of [ ...CELL_GRAPHICS , ...ARROWS ]) emit ( cp , outline ( sourceOf ( cp ), 0 ), Transform . Id ()); 228for ( const cp of codes ( config . terminal . characters )) { 229if ( cp === 0xFFFD ) { 230// A stable diamond/question cutout avoids upstream contour collapse. 231const diamond = [[ 250 , 800 ], [ 485 , 340 ], [ 250 , - 120 ], [ 15 , 340 ]]. map (([ x , y ]) => Point . corner ( x , y )); 232const question = transformed ( placed . get ( 0x3F ), new Transform ( .46 , 0 , 0 , .46 , 135 , 171 )); 233emit ( cp , [ diamond , ...question . map ( c => [ ...c ]. reverse ())], null ); 234continue ; 235} 236const contours = outline ( sourceOf ( cp ), radiusOf ( cp )), box = bounds ( contours ); 237const scale = Math . min ( 1 , ( cp === 0x2013 ?400 :CELL - 2 * MARGIN ) / ( box . xMax - box . xMin )); 238// Geometric indicators should remain circles and squares, not ellipses. 239const vertical = Math . min ( 0x25A0 <= cp && cp <= 0x2718 ?scale :1 , ( ascent - descent ) / ( box . yMax - box . yMin )); 240const dy = Math . max ( descent - vertical * box . yMin , 241Math . min (( 1 - vertical ) * ( ascent + descent ) / 2 , ascent - vertical * box . yMax )); 242const tfm = new Transform ( scale , 0 , 0 , vertical , CELL / 2 - scale * ( box . xMin + box . xMax ) / 2 , dy ); 243emit ( cp , transformed ( contours , tfm ), tfm ); 244} 245 246for ( const [ cp , contours ] of placed ) { 247const box = bounds ( contours ); 248if ( ! box || ! store . queryByUnicode ( cp )) continue ; 249const exact = CELL_GRAPHICS . has ( cp ) || ARROWS . has ( cp ); 250const margin = exact ?( 0x2571 <= cp && cp <= 0x2573 ?- 64 :0 ) :MARGIN - 1 ; 251if ( Math . round ( box . xMin ) < margin || Math . round ( box . xMax ) > CELL - margin ) { 252throw new Error ( `U+ ${ cp . toString ( 16 ). toUpperCase ()} ink ${ box . xMin } .. ${ box . xMax } leaves its cell` ); 253} 254} 255return store ; 256}