...

Source file src/oss.terrastruct.com/d2/lib/textmeasure/atlas.go

Documentation: oss.terrastruct.com/d2/lib/textmeasure

     1  package textmeasure
     2  
     3  import (
     4  	"sort"
     5  	"unicode"
     6  
     7  	"golang.org/x/image/font"
     8  	"golang.org/x/image/math/fixed"
     9  
    10  	"oss.terrastruct.com/d2/lib/geo"
    11  )
    12  
    13  // glyph describes one glyph in an atlas.
    14  type glyph struct {
    15  	dot     *geo.Point
    16  	frame   *rect
    17  	advance float64
    18  }
    19  
    20  // atlas is a set of pre-drawn glyphs of a fixed set of runes. This allows for efficient text drawing.
    21  type atlas struct {
    22  	face       font.Face
    23  	mapping    map[rune]glyph
    24  	ascent     float64
    25  	descent    float64
    26  	lineHeight float64
    27  }
    28  
    29  // NewAtlas creates a new atlas containing glyphs of the union of the given sets of runes (plus
    30  // unicode.ReplacementChar) from the given font face.
    31  //
    32  // Creating an atlas is rather expensive, do not create a new atlas each frame.
    33  //
    34  // Do not destroy or close the font.Face after creating the atlas. atlas still uses it.
    35  func NewAtlas(face font.Face, runeSets ...[]rune) *atlas {
    36  	seen := make(map[rune]bool)
    37  	runes := []rune{unicode.ReplacementChar}
    38  	for _, set := range runeSets {
    39  		for _, r := range set {
    40  			if !seen[r] {
    41  				runes = append(runes, r)
    42  				seen[r] = true
    43  			}
    44  		}
    45  	}
    46  
    47  	fixedMapping, fixedBounds := makeSquareMapping(face, runes, fixed.I(2))
    48  
    49  	bounds := &rect{
    50  		tl: geo.NewPoint(
    51  			i2f(fixedBounds.Min.X),
    52  			i2f(fixedBounds.Min.Y),
    53  		),
    54  		br: geo.NewPoint(
    55  			i2f(fixedBounds.Max.X),
    56  			i2f(fixedBounds.Max.Y),
    57  		),
    58  	}
    59  
    60  	mapping := make(map[rune]glyph)
    61  	for r, fg := range fixedMapping {
    62  		mapping[r] = glyph{
    63  			dot: geo.NewPoint(
    64  				i2f(fg.dot.X),
    65  				bounds.br.Y-(i2f(fg.dot.Y)-bounds.tl.Y),
    66  			),
    67  			frame: rect{
    68  				tl: geo.NewPoint(
    69  					i2f(fg.frame.Min.X),
    70  					bounds.br.Y-(i2f(fg.frame.Min.Y)-bounds.tl.Y),
    71  				),
    72  				br: geo.NewPoint(
    73  					i2f(fg.frame.Max.X),
    74  					bounds.br.Y-(i2f(fg.frame.Max.Y)-bounds.tl.Y),
    75  				),
    76  			}.norm(),
    77  			advance: i2f(fg.advance),
    78  		}
    79  	}
    80  
    81  	return &atlas{
    82  		face:       face,
    83  		mapping:    mapping,
    84  		ascent:     i2f(face.Metrics().Ascent),
    85  		descent:    i2f(face.Metrics().Descent),
    86  		lineHeight: i2f(face.Metrics().Height),
    87  	}
    88  }
    89  
    90  func (a *atlas) contains(r rune) bool {
    91  	_, ok := a.mapping[r]
    92  	return ok
    93  }
    94  
    95  // glyph returns the description of r within the atlas.
    96  func (a *atlas) glyph(r rune) glyph {
    97  	return a.mapping[r]
    98  }
    99  
   100  // Kern returns the kerning distance between runes r0 and r1. Positive distance means that the
   101  // glyphs should be further apart.
   102  func (a *atlas) Kern(r0, r1 rune) float64 {
   103  	return i2f(a.face.Kern(r0, r1))
   104  }
   105  
   106  // Ascent returns the distance from the top of the line to the baseline.
   107  func (a *atlas) Ascent() float64 {
   108  	return a.ascent
   109  }
   110  
   111  // Descent returns the distance from the baseline to the bottom of the line.
   112  func (a *atlas) Descent() float64 {
   113  	return a.descent
   114  }
   115  
   116  // DrawRune returns parameters necessary for drawing a rune glyph.
   117  //
   118  // Rect is a rectangle where the glyph should be positioned. frame is the glyph frame inside the
   119  // atlas's Picture. NewDot is the new position of the dot.
   120  func (a *atlas) DrawRune(prevR, r rune, dot *geo.Point) (rect2, frame, bounds *rect, newDot *geo.Point) {
   121  	if !a.contains(r) {
   122  		r = unicode.ReplacementChar
   123  	}
   124  	if !a.contains(unicode.ReplacementChar) {
   125  		return newRect(), newRect(), newRect(), dot
   126  	}
   127  	if !a.contains(prevR) {
   128  		prevR = unicode.ReplacementChar
   129  	}
   130  
   131  	if prevR >= 0 {
   132  		dot.X += a.Kern(prevR, r)
   133  	}
   134  
   135  	glyph := a.glyph(r)
   136  
   137  	subbed := geo.NewPoint(
   138  		dot.X-glyph.dot.X,
   139  		dot.Y-glyph.dot.Y,
   140  	)
   141  
   142  	rect2 = &rect{
   143  		tl: geo.NewPoint(
   144  			glyph.frame.tl.X+subbed.X,
   145  			glyph.frame.tl.Y+subbed.Y,
   146  		),
   147  		br: geo.NewPoint(
   148  			glyph.frame.br.X+subbed.X,
   149  			glyph.frame.br.Y+subbed.Y,
   150  		),
   151  	}
   152  	bounds = rect2
   153  
   154  	if bounds.w()*bounds.h() != 0 {
   155  		bounds = &rect{
   156  			tl: geo.NewPoint(
   157  				bounds.tl.X,
   158  				dot.Y-a.Descent(),
   159  			),
   160  			br: geo.NewPoint(
   161  				bounds.br.X,
   162  				dot.Y+a.Ascent(),
   163  			),
   164  		}
   165  	}
   166  
   167  	dot.X += glyph.advance
   168  
   169  	return rect2, glyph.frame, bounds, dot
   170  }
   171  
   172  type fixedGlyph struct {
   173  	dot     fixed.Point26_6
   174  	frame   fixed.Rectangle26_6
   175  	advance fixed.Int26_6
   176  }
   177  
   178  // makeSquareMapping finds an optimal glyph arrangement of the given runes, so that their common
   179  // bounding box is as square as possible.
   180  func makeSquareMapping(face font.Face, runes []rune, padding fixed.Int26_6) (map[rune]fixedGlyph, fixed.Rectangle26_6) {
   181  	width := sort.Search(int(fixed.I(1024*1024)), func(i int) bool {
   182  		width := fixed.Int26_6(i)
   183  		_, bounds := makeMapping(face, runes, padding, width)
   184  		return bounds.Max.X-bounds.Min.X >= bounds.Max.Y-bounds.Min.Y
   185  	})
   186  	return makeMapping(face, runes, padding, fixed.Int26_6(width))
   187  }
   188  
   189  // makeMapping arranges glyphs of the given runes into rows in such a way, that no glyph is located
   190  // fully to the right of the specified width. Specifically, it places glyphs in a row one by one and
   191  // once it reaches the specified width, it starts a new row.
   192  func makeMapping(face font.Face, runes []rune, padding, width fixed.Int26_6) (map[rune]fixedGlyph, fixed.Rectangle26_6) {
   193  	mapping := make(map[rune]fixedGlyph)
   194  	bounds := fixed.Rectangle26_6{}
   195  
   196  	dot := fixed.P(0, 0)
   197  
   198  	for _, r := range runes {
   199  		b, advance, ok := face.GlyphBounds(r)
   200  		if !ok {
   201  			continue
   202  		}
   203  
   204  		// this is important for drawing, artifacts arise otherwise
   205  		frame := fixed.Rectangle26_6{
   206  			Min: fixed.P(b.Min.X.Floor(), b.Min.Y.Floor()),
   207  			Max: fixed.P(b.Max.X.Ceil(), b.Max.Y.Ceil()),
   208  		}
   209  
   210  		dot.X -= frame.Min.X
   211  		frame = frame.Add(dot)
   212  
   213  		mapping[r] = fixedGlyph{
   214  			dot:     dot,
   215  			frame:   frame,
   216  			advance: advance,
   217  		}
   218  		bounds = bounds.Union(frame)
   219  
   220  		dot.X = frame.Max.X
   221  
   222  		// padding + align to integer
   223  		dot.X += padding
   224  		dot.X = fixed.I(dot.X.Ceil())
   225  
   226  		// width exceeded, new row
   227  		if frame.Max.X >= width {
   228  			dot.X = 0
   229  			dot.Y += face.Metrics().Ascent + face.Metrics().Descent
   230  
   231  			// padding + align to integer
   232  			dot.Y += padding
   233  			dot.Y = fixed.I(dot.Y.Ceil())
   234  		}
   235  	}
   236  
   237  	return mapping, bounds
   238  }
   239  
   240  func i2f(i fixed.Int26_6) float64 {
   241  	return float64(i) / (1 << 6)
   242  }
   243  

View as plain text