@@ -2212,44 +2212,100 @@ const BLEND_MODES: Record<BlendMode, (backdrop: number, source: number) => numbe
22122212
22132213/**
22142214 * Grows a coverage mask by a radius, using the distance of every pixel to the glyphs (an exact euclidean distance
2215- * transform, so the cost doesn't depend on the radius)
2215+ * transform, so the cost doesn't depend on the radius). Near the outline's edge, the distance goes to the glyph edge
2216+ * within the pixels around the nearest covered one (estimated from their coverage and its gradient, like Gustavson's
2217+ * antialiased distance transform), so the outline follows the antialiased edge instead of the pixels
22162218 * @return the outline's coverage (0..1) per pixel, including the area under the glyphs
22172219 */
2220+ // covered pixels around the nearest one (on either side) whose edge points the outline's edge is measured to
2221+ const OUTLINE_WINDOW = 1 ;
2222+
22182223function outline ( coverage : Uint8Array , width : number , height : number , radius : number ) : Float32Array {
22192224 const count = width * height ;
2220- // squared distance to the nearest pixel that is at least half covered
2225+ // squared distance to the nearest pixel that is at least half covered, and that pixel's coordinates
22212226 const distance = new Float64Array ( count ) ;
22222227 for ( let i = 0 ; i < count ; i ++ ) distance [ i ] = coverage [ i ] >= 128 ? 0 : Infinity ;
2228+ const nearestY = new Int32Array ( count ) , nearestX = new Int32Array ( count ) ;
22232229
22242230 const size = Math . max ( width , height ) ;
2225- const line = new Float64Array ( size ) , result = new Float64Array ( size ) ;
2231+ const line = new Float64Array ( size ) , result = new Float64Array ( size ) , nearest = new Int32Array ( size ) ;
22262232 const parabolas = new Int32Array ( size ) , bounds = new Float64Array ( size + 1 ) ;
22272233
22282234 for ( let x = 0 ; x < width ; x ++ ) {
22292235 for ( let y = 0 ; y < height ; y ++ ) line [ y ] = distance [ y * width + x ] ;
2230- distanceTransform ( line , height , result , parabolas , bounds ) ;
2231- for ( let y = 0 ; y < height ; y ++ ) distance [ y * width + x ] = result [ y ] ;
2236+ distanceTransform ( line , height , result , parabolas , bounds , nearest ) ;
2237+ for ( let y = 0 ; y < height ; y ++ ) {
2238+ distance [ y * width + x ] = result [ y ] ;
2239+ nearestY [ y * width + x ] = nearest [ y ] ;
2240+ }
22322241 }
22332242
2243+ // distances beyond the outline don't matter: leaving them out of the rows saves most of their work
2244+ const far = ( radius + 2 ) ** 2 ;
22342245 for ( let y = 0 ; y < height ; y ++ ) {
22352246 const row = y * width ;
2236- for ( let x = 0 ; x < width ; x ++ ) line [ x ] = distance [ row + x ] ;
2237- distanceTransform ( line , width , result , parabolas , bounds ) ;
2238- for ( let x = 0 ; x < width ; x ++ ) distance [ row + x ] = result [ x ] ;
2247+ for ( let x = 0 ; x < width ; x ++ ) line [ x ] = distance [ row + x ] < far ? distance [ row + x ] : Infinity ;
2248+ distanceTransform ( line , width , result , parabolas , bounds , nearest ) ;
2249+ for ( let x = 0 ; x < width ; x ++ ) {
2250+ distance [ row + x ] = result [ x ] ;
2251+ nearestX [ row + x ] = nearest [ x ] ;
2252+ }
22392253 }
22402254
2255+ // where the glyph edge crosses an edge pixel (partly covered, or covered next to an uncovered one): from its
2256+ // center along the outward normal (the coverage gradient) to where a straight edge would cover it as much. The
2257+ // coverage has an empty border (the outline's room), so covered pixels have all their neighbors
2258+ const pointX = new Float32Array ( count ) , pointY = new Float32Array ( count ) , isEdge = new Uint8Array ( count ) ;
2259+ for ( let y = 1 ; y < height - 1 ; y ++ )
2260+ for ( let x = 1 , e = y * width + 1 ; x < width - 1 ; x ++ , e ++ ) {
2261+ const c = coverage [ e ] ;
2262+ if ( ! c || c === 255 && coverage [ e - 1 ] >= 128 && coverage [ e + 1 ] >= 128 && coverage [ e - width ] >= 128 && coverage [ e + width ] >= 128 ) continue ;
2263+ // Sobel gradient, pointing inwards
2264+ const above = e - width , below = e + width ;
2265+ const gx = coverage [ above + 1 ] + 2 * coverage [ e + 1 ] + coverage [ below + 1 ] - coverage [ above - 1 ] - 2 * coverage [ e - 1 ] - coverage [ below - 1 ] ;
2266+ const gy = coverage [ below - 1 ] + 2 * coverage [ below ] + coverage [ below + 1 ] - coverage [ above - 1 ] - 2 * coverage [ above ] - coverage [ above + 1 ] ;
2267+ const length = Math . sqrt ( gx * gx + gy * gy ) ;
2268+ if ( ! length ) continue ;
2269+ // the center lies 1/2 - c outside the edge
2270+ const outside = 0.5 - c / 255 ;
2271+ pointX [ e ] = x + outside * gx / length ;
2272+ pointY [ e ] = y + outside * gy / length ;
2273+ isEdge [ e ] = 1 ;
2274+ }
2275+
2276+ // the distance to the nearest covered pixel's center is within a pixel of the distance to the edge: outside of that
2277+ // band around the outline's edge, a pixel is fully covered or not at all
2278+ // (pixels next to the glyph edge are covered by the glyph's own antialiasing)
22412279 const out = new Float32Array ( count ) ;
2242- for ( let i = 0 ; i < count ; i ++ ) {
2243- // pixels next to the glyph edge are covered by the glyph's own antialiasing
2244- const d = Math . sqrt ( distance [ i ] ) ;
2245- out [ i ] = Math . max ( coverage [ i ] / 255 , Math . min ( 1 , Math . max ( 0 , radius + 0.5 - d ) ) ) ;
2246- }
2280+ const inner = radius - 1.5 , near = inner > 0 ? inner * inner : - 1 ;
2281+ for ( let y = 0 , i = 0 ; y < height ; y ++ )
2282+ for ( let x = 0 ; x < width ; x ++ , i ++ ) {
2283+ const d2 = distance [ i ] ;
2284+ if ( d2 <= near ) out [ i ] = 1 ;
2285+ if ( d2 <= near || d2 >= far ) {
2286+ if ( d2 >= far ) out [ i ] = coverage [ i ] / 255 ;
2287+ continue ;
2288+ }
2289+
2290+ // the closest edge point around the nearest covered pixel
2291+ const qx = nearestX [ i ] , qy = nearestY [ y * width + qx ] ;
2292+ let closest = Infinity ;
2293+ for ( let wy = Math . max ( 0 , qy - OUTLINE_WINDOW ) ; wy <= Math . min ( height - 1 , qy + OUTLINE_WINDOW ) ; wy ++ )
2294+ for ( let wx = Math . max ( 0 , qx - OUTLINE_WINDOW ) , e = wy * width + wx ; wx <= Math . min ( width - 1 , qx + OUTLINE_WINDOW ) ; wx ++ , e ++ ) {
2295+ if ( ! isEdge [ e ] ) continue ;
2296+ const ex = x - pointX [ e ] , ey = y - pointY [ e ] ;
2297+ closest = Math . min ( closest , ex * ex + ey * ey ) ;
2298+ }
2299+ closest = closest === Infinity ? Math . sqrt ( d2 ) - 0.5 : Math . sqrt ( closest ) ;
2300+ out [ i ] = Math . max ( coverage [ i ] / 255 , Math . min ( 1 , Math . max ( 0 , radius + 0.5 - closest ) ) ) ;
2301+ }
22472302
22482303 return out ;
22492304}
22502305
2251- // 1D squared euclidean distance transform (Felzenszwalb & Huttenlocher): lower envelope of parabolas
2252- function distanceTransform ( f : Float64Array , n : number , d : Float64Array , v : Int32Array , z : Float64Array ) : void {
2306+ // 1D squared euclidean distance transform (Felzenszwalb & Huttenlocher): lower envelope of parabolas, also the index
2307+ // of the nearest sample
2308+ function distanceTransform ( f : Float64Array , n : number , d : Float64Array , v : Int32Array , z : Float64Array , nearest : Int32Array ) : void {
22532309 let k = - 1 ;
22542310 for ( let q = 0 ; q < n ; q ++ ) {
22552311 if ( f [ q ] === Infinity ) continue ;
@@ -2273,5 +2329,6 @@ function distanceTransform(f: Float64Array, n: number, d: Float64Array, v: Int32
22732329 for ( let q = 0 , j = 0 ; q < n ; q ++ ) {
22742330 while ( z [ j + 1 ] < q ) j ++ ;
22752331 d [ q ] = ( q - v [ j ] ) ** 2 + f [ v [ j ] ] ;
2332+ nearest [ q ] = v [ j ] ;
22762333 }
22772334}
0 commit comments