char/iocgn-mono
iosevka variant inspired by Simple Köln-Bonn
git clone https://git.t4t.associates/char/iocgn-mono
2ba32a9
main
1import { Point } from '@iosevka/geometry/point' ; 2 3// Segments are [start, end] lines or [start, control, end] quadratics of plain {x, y} points. 4 5const lerp = ( a , b , t ) => ({ x :a . x + ( b . x - a . x ) * t , y :a . y + ( b . y - a . y ) * t }); 6const sub = ( a , b ) => ({ x :a . x - b . x , y :a . y - b . y }); 7const dot = ( a , b ) => a . x * b . x + a . y * b . y ; 8const cross = ( a , b ) => a . x * b . y - a . y * b . x ; 9const norm = v => Math . hypot ( v . x , v . y ); 10 11function split ( segment , t ) { 12const a = lerp ( segment [ 0 ], segment [ 1 ], t ); 13if ( segment . length === 2 ) return [[ segment [ 0 ], a ], [ a , segment [ 1 ]]]; 14const b = lerp ( segment [ 1 ], segment [ 2 ], t ), m = lerp ( a , b , t ); 15return [[ segment [ 0 ], a , m ], [ m , b , segment [ 2 ]]]; 16} 17 18// Closed form from fontTools' calcQuadraticArcLength. 19function length ( segment ) { 20if ( segment . length === 2 ) return norm ( sub ( segment [ 1 ], segment [ 0 ])); 21const [ p0 , p1 , p2 ] = segment ; 22const d0 = sub ( p1 , p0 ), d1 = sub ( p2 , p1 ), d = sub ( d1 , d0 ); 23const n = { x :- d . y , y :d . x }, scale = norm ( n ); 24if ( scale === 0 ) return norm ( sub ( p2 , p0 )); 25const origin = dot ( n , d0 ); 26if ( Math . abs ( origin ) < 1e-10 ) { 27if ( dot ( d0 , d1 ) >= 0 ) return norm ( sub ( p2 , p0 )); 28const a = norm ( d0 ), b = norm ( d1 ); 29return ( a * a + b * b ) / ( a + b ); 30} 31const integral = x => x * Math . sqrt ( x * x + 1 ) / 2 + Math . asinh ( x ) / 2 ; 32const x0 = dot ( d , d0 ) / origin , x1 = dot ( d , d1 ) / origin ; 33return Math . abs ( 2 * ( integral ( x1 ) - integral ( x0 )) * origin / ( scale * ( x1 - x0 ))); 34} 35 36function parameterAtDistance ( segment , distance ) { 37let lo = 0 , hi = 1 ; 38for ( let i = 0 ; i < 18 ; i ++ ) { 39const t = ( lo + hi ) / 2 ; 40if ( length ( split ( segment , t )[ 0 ]) < distance ) lo = t ; 41else hi = t ; 42} 43return ( lo + hi ) / 2 ; 44} 45 46function segmentsOf ( contour ) { 47const on = z => z . type !== Point . Type . Quadratic ; 48const first = contour . findIndex ( on ); 49// TrueType implies an on-curve point between consecutive off-curve points. 50const points = first < 0 51 ?[{ ...lerp ( contour . at ( - 1 ), contour [ 0 ], .5 ), type :Point . Type . Corner }, ...contour ] 52 :[ ...contour . slice ( first ), ...contour . slice ( 0 , first )]; 53const segments = []; 54let current = { x :points [ 0 ]. x , y :points [ 0 ]. y }, control = null ; 55for ( const z of [ ...points . slice ( 1 ), points [ 0 ]]) { 56const p = { x :z . x , y :z . y }; 57if ( ! on ( z )) { 58if ( control ) { 59const implied = lerp ( control , p , .5 ); 60segments . push ([ current , control , implied ]); 61current = implied ; 62} 63control = p ; 64continue ; 65} 66if ( control ) segments . push ([ current , control , p ]); 67else if ( p . x !== current . x || p . y !== current . y ) segments . push ([ current , p ]); 68current = p ; 69control = null ; 70} 71return segments ; 72} 73 74function pointsOf ( segments ) { 75const points = [ Point . corner ( segments [ 0 ][ 0 ]. x , segments [ 0 ][ 0 ]. y )]; 76for ( const segment of segments ) { 77if ( segment . length === 3 ) points . push ( new Point ( Point . Type . Quadratic , segment [ 1 ]. x , segment [ 1 ]. y )); 78points . push ( Point . corner ( segment . at ( - 1 ). x , segment . at ( - 1 ). y )); 79} 80const last = points . at ( - 1 ); 81if ( last . x === points [ 0 ]. x && last . y === points [ 0 ]. y ) points . pop (); 82return points ; 83} 84 85function segmentBounds ( segment ) { 86const values = axis => { 87const v = segment . map ( p => p [ axis ]); 88if ( v . length === 3 ) { 89const t = ( v [ 0 ] - v [ 1 ]) / ( v [ 0 ] - 2 * v [ 1 ] + v [ 2 ]); 90if ( t > 0 && t < 1 ) v . push (( 1 - t ) * ( 1 - t ) * v [ 0 ] + 2 * t * ( 1 - t ) * v [ 1 ] + t * t * v [ 2 ]); 91v . splice ( 1 , 1 ); 92} 93return v ; 94}; 95const xs = values ( 'x' ), ys = values ( 'y' ); 96return { xMin :Math . min ( ...xs ), yMin :Math . min ( ...ys ), xMax :Math . max ( ...xs ), yMax :Math . max ( ...ys ) }; 97} 98 99function boundsOfSegments ( segments ) { 100const boxes = segments . map ( segmentBounds ); 101if ( ! boxes . length ) return null ; 102return { 103xMin :Math . min ( ...boxes . map ( b => b . xMin )), yMin :Math . min ( ...boxes . map ( b => b . yMin )), 104xMax :Math . max ( ...boxes . map ( b => b . xMax )), yMax :Math . max ( ...boxes . map ( b => b . yMax )), 105}; 106} 107 108export function bounds ( contours ) { 109return boundsOfSegments ( contours . flatMap ( segmentsOf )); 110} 111 112function area ( contours ) { 113let total = 0 ; 114for ( const segment of contours . flat ()) { 115const [ p0 , p1 ] = [ segment [ 0 ], segment . at ( - 1 )]; 116if ( segment . length === 3 ) total -= cross ( sub ( p1 , p0 ), sub ( segment [ 1 ], p0 )) / 3 ; 117total -= ( p1 . x - p0 . x ) * ( p1 . y + p0 . y ) / 2 ; 118} 119return total ; 120} 121 122function roundedContour ( segments , radius ) { 123if ( segments . length < 2 || radius < .25 ) return segments ; 124const n = segments . length ; 125const corners = new Map (); 126segments . forEach (( prev , i ) => { 127const next = segments [( i + 1 ) % n ]; 128const incoming = sub ( prev . at ( - 1 ), prev . at ( - 2 )), outgoing = sub ( next [ 1 ], next [ 0 ]); 129const scale = norm ( incoming ) * norm ( outgoing ); 130if ( norm ( incoming ) < 1e-6 || norm ( outgoing ) < 1e-6 ) return ; 131const turn = { cos :dot ( incoming , outgoing ) / scale , sin :cross ( incoming , outgoing ) / scale }; 132if ( Math . abs ( turn . sin ) > .15 || turn . cos < 0 ) corners . set (( i + 1 ) % n , turn ); 133}); 134if ( ! corners . size ) return segments ; 135 136// Work across smooth segment boundaries: a terminal may start with a tiny 137// straight stub that must not constrain the radius of the whole corner. 138const starts = [ ...corners . keys ()]. sort (( a , b ) => a - b ); 139const runs = starts . map (( start , i ) => { 140const count = (( starts [( i + 1 ) % starts . length ] - start ) % n + n ) % n || n ; 141return Array . from ({ length :count }, ( _ , j ) => segments [( start + j ) % n ]); 142}); 143const lengths = runs . map ( run => run . reduce (( sum , s ) => sum + length ( s ), 0 )); 144const trims = starts . map (( start , i ) => { 145const turn = corners . get ( start ); 146// TrueType filled contours run clockwise; preserve concave joins. 147if ( turn . sin >= - .15 ) return 0 ; 148const angle = Math . acos ( Math . max ( - 1 , Math . min ( 1 , turn . cos ))); 149return Math . min ( radius * Math . tan ( angle / 2 ), lengths . at ( i - 1 ) * .4 , lengths [ i ] * .4 ); 150}); 151 152const pieces = runs . map (( run , i ) => { 153const lo = trims [ i ], hi = lengths [ i ] - trims [( i + 1 ) % runs . length ]; 154const kept = []; 155let cursor = 0 ; 156for ( const segment of run ) { 157const size = length ( segment ); 158const a = Math . max ( 0 , lo - cursor ), b = Math . min ( size , hi - cursor ); 159cursor += size ; 160if ( b <= a ) continue ; 161const t0 = a ?parameterAtDistance ( segment , a ) :0 ; 162const t1 = b < size ?parameterAtDistance ( segment , b ) :1 ; 163const piece = t1 < 1 ?split ( segment , t1 )[ 0 ] :segment ; 164kept . push ( t0 ?split ( piece , t0 / t1 )[ 1 ] :piece ); 165} 166return kept ; 167}); 168 169const result = []; 170for ( const [ i , run ] of pieces . entries ()) { 171result . push ( ...run ); 172if ( ! trims [( i + 1 ) % runs . length ]) continue ; 173const prev = run . at ( - 1 ), next = pieces [( i + 1 ) % pieces . length ][ 0 ]; 174const a = prev . at ( - 1 ), b = next [ 0 ]; 175const u = sub ( a , prev . at ( - 2 )), v = sub ( next [ 1 ], b ); 176const det = cross ( u , v ); 177// Tight curved features can turn past the tangent intersection. 178// Back off rather than introducing a loop or a reversed fillet. 179if ( det >= - 1e-9 ) return roundedContour ( segments , radius / 2 ); 180const k = cross ( sub ( b , a ), v ) / det ; 181const control = { x :a . x + u . x * k , y :a . y + u . y * k }; 182if ( dot ( sub ( control , a ), u ) <= 0 || dot ( sub ( b , control ), v ) <= 0 ) return roundedContour ( segments , radius / 2 ); 183result . push ([ a , control , b ]); 184} 185const before = boundsOfSegments ( segments ), after = boundsOfSegments ( result ); 186if ( after . xMin < before . xMin - .01 || after . yMin < before . yMin - .01 || 187after . xMax > before . xMax + .01 || after . yMax > before . yMax + .01 ) { 188return roundedContour ( segments , radius / 2 ); 189} 190return result ; 191} 192 193// Round convex corners without rounding counters or terminal widths. 194export function roundCorners ( contours , radius ) { 195const original = contours . map ( segmentsOf ). filter ( segments => segments . length ); 196const minimum = Math . abs ( area ( original )) * .9 ; 197for ( let trial = radius ; trial >= .25 ; trial /= 2 ) { 198const rounded = original . map ( segments => roundedContour ( segments , trial )); 199if ( Math . abs ( area ( rounded )) >= minimum ) return rounded . map ( pointsOf ); 200} 201return contours ; 202}