|
| 1 | +// Copyright ©2021 The Gonum Authors. All rights reserved. |
| 2 | +// Use of this source code is governed by a BSD-style |
| 3 | +// license that can be found in the LICENSE file. |
| 4 | + |
| 5 | +package text |
| 6 | + |
| 7 | +import ( |
| 8 | + "image/color" |
| 9 | + "math" |
| 10 | + |
| 11 | + "gonum.org/v1/plot/vg" |
| 12 | +) |
| 13 | + |
| 14 | +// Handler parses, formats and renders text. |
| 15 | +type Handler interface { |
| 16 | + // Lines splits a given block of text into separate lines. |
| 17 | + Lines(txt string) []string |
| 18 | + |
| 19 | + // Box returns the bounding box of the given non-multiline text where: |
| 20 | + // - width is the horizontal space from the origin. |
| 21 | + // - height is the vertical space above the baseline. |
| 22 | + // - depth is the vertical space below the baseline, a positive number. |
| 23 | + Box(txt string, fnt vg.Font) (width, height, depth vg.Length) |
| 24 | + |
| 25 | + // Draw renders the given text with the provided style and position |
| 26 | + // on the canvas. |
| 27 | + Draw(c vg.Canvas, txt string, sty TextStyle, pt vg.Point) |
| 28 | +} |
| 29 | + |
| 30 | +// XAlignment specifies text alignment in the X direction. Three preset |
| 31 | +// options are available, but an arbitrary alignment |
| 32 | +// can also be specified using XAlignment(desired number). |
| 33 | +type XAlignment float64 |
| 34 | + |
| 35 | +const ( |
| 36 | + // XLeft aligns the left edge of the text with the specified location. |
| 37 | + XLeft XAlignment = 0 |
| 38 | + // XCenter aligns the horizontal center of the text with the specified location. |
| 39 | + XCenter XAlignment = -0.5 |
| 40 | + // XRight aligns the right edge of the text with the specified location. |
| 41 | + XRight XAlignment = -1 |
| 42 | +) |
| 43 | + |
| 44 | +// YAlignment specifies text alignment in the Y direction. Three preset |
| 45 | +// options are available, but an arbitrary alignment |
| 46 | +// can also be specified using YAlignment(desired number). |
| 47 | +type YAlignment float64 |
| 48 | + |
| 49 | +const ( |
| 50 | + // YTop aligns the top of of the text with the specified location. |
| 51 | + YTop YAlignment = -1 |
| 52 | + // YCenter aligns the vertical center of the text with the specified location. |
| 53 | + YCenter YAlignment = -0.5 |
| 54 | + // YBottom aligns the bottom of the text with the specified location. |
| 55 | + YBottom YAlignment = 0 |
| 56 | +) |
| 57 | + |
| 58 | +// Position specifies the text position. |
| 59 | +const ( |
| 60 | + PosLeft = -1 |
| 61 | + PosBottom = -1 |
| 62 | + PosCenter = 0 |
| 63 | + PosTop = +1 |
| 64 | + PosRight = +1 |
| 65 | +) |
| 66 | + |
| 67 | +// TextStyle describes what text will look like. |
| 68 | +type TextStyle struct { |
| 69 | + // Color is the text color. |
| 70 | + Color color.Color |
| 71 | + |
| 72 | + // Font is the font description. |
| 73 | + Font vg.Font |
| 74 | + |
| 75 | + // Rotation is the text rotation in radians, performed around the axis |
| 76 | + // defined by XAlign and YAlign. |
| 77 | + Rotation float64 |
| 78 | + |
| 79 | + // XAlign and YAlign specify the alignment of the text. |
| 80 | + XAlign XAlignment |
| 81 | + YAlign YAlignment |
| 82 | + |
| 83 | + // Handler parses and formats text according to a given |
| 84 | + // dialect (Markdown, LaTeX, plain, ...) |
| 85 | + // The default is a plain text handler. |
| 86 | + Handler Handler |
| 87 | +} |
| 88 | + |
| 89 | +// Width returns the width of lines of text |
| 90 | +// when using the given font before any text rotation is applied. |
| 91 | +func (sty TextStyle) Width(txt string) (max vg.Length) { |
| 92 | + w, _ := sty.box(txt) |
| 93 | + return w |
| 94 | +} |
| 95 | + |
| 96 | +// Height returns the height of the text when using |
| 97 | +// the given font before any text rotation is applied. |
| 98 | +func (sty TextStyle) Height(txt string) vg.Length { |
| 99 | + _, h := sty.box(txt) |
| 100 | + return h |
| 101 | +} |
| 102 | + |
| 103 | +// box returns the bounding box of a possibly multi-line text. |
| 104 | +func (sty TextStyle) box(txt string) (w, h vg.Length) { |
| 105 | + var ( |
| 106 | + lines = sty.Handler.Lines(txt) |
| 107 | + e = sty.Font.Extents() |
| 108 | + linegap = (e.Height - e.Ascent - e.Descent) |
| 109 | + ) |
| 110 | + for i, line := range lines { |
| 111 | + ww, hh, dd := sty.Handler.Box(line, sty.Font) |
| 112 | + if ww > w { |
| 113 | + w = ww |
| 114 | + } |
| 115 | + h += hh + dd |
| 116 | + if i > 0 { |
| 117 | + h += linegap |
| 118 | + } |
| 119 | + } |
| 120 | + |
| 121 | + return w, h |
| 122 | +} |
| 123 | + |
| 124 | +// Rectangle returns a rectangle giving the bounds of |
| 125 | +// this text assuming that it is drawn at (0, 0). |
| 126 | +func (sty TextStyle) Rectangle(txt string) vg.Rectangle { |
| 127 | + e := sty.Font.Extents() |
| 128 | + w, h := sty.box(txt) |
| 129 | + desc := vg.Length(e.Height - e.Ascent) // descent + linegap |
| 130 | + xoff := vg.Length(sty.XAlign) * w |
| 131 | + yoff := vg.Length(sty.YAlign)*h - desc |
| 132 | + |
| 133 | + // lower left corner |
| 134 | + p1 := rotatePoint(sty.Rotation, vg.Point{X: xoff, Y: yoff}) |
| 135 | + // upper left corner |
| 136 | + p2 := rotatePoint(sty.Rotation, vg.Point{X: xoff, Y: h + yoff}) |
| 137 | + // lower right corner |
| 138 | + p3 := rotatePoint(sty.Rotation, vg.Point{X: w + xoff, Y: yoff}) |
| 139 | + // upper right corner |
| 140 | + p4 := rotatePoint(sty.Rotation, vg.Point{X: w + xoff, Y: h + yoff}) |
| 141 | + |
| 142 | + return vg.Rectangle{ |
| 143 | + Max: vg.Point{ |
| 144 | + X: max(p1.X, p2.X, p3.X, p4.X), |
| 145 | + Y: max(p1.Y, p2.Y, p3.Y, p4.Y), |
| 146 | + }, |
| 147 | + Min: vg.Point{ |
| 148 | + X: min(p1.X, p2.X, p3.X, p4.X), |
| 149 | + Y: min(p1.Y, p2.Y, p3.Y, p4.Y), |
| 150 | + }, |
| 151 | + } |
| 152 | +} |
| 153 | + |
| 154 | +// rotatePoint applies rotation theta (in radians) about the origin to point p. |
| 155 | +func rotatePoint(theta float64, p vg.Point) vg.Point { |
| 156 | + if theta == 0 { |
| 157 | + return p |
| 158 | + } |
| 159 | + x := float64(p.X) |
| 160 | + y := float64(p.Y) |
| 161 | + |
| 162 | + sin, cos := math.Sincos(theta) |
| 163 | + |
| 164 | + return vg.Point{ |
| 165 | + X: vg.Length(x*cos - y*sin), |
| 166 | + Y: vg.Length(y*cos + x*sin), |
| 167 | + } |
| 168 | +} |
| 169 | + |
| 170 | +func max(d ...vg.Length) vg.Length { |
| 171 | + o := vg.Length(math.Inf(-1)) |
| 172 | + for _, dd := range d { |
| 173 | + if dd > o { |
| 174 | + o = dd |
| 175 | + } |
| 176 | + } |
| 177 | + return o |
| 178 | +} |
| 179 | + |
| 180 | +func min(d ...vg.Length) vg.Length { |
| 181 | + o := vg.Length(math.Inf(1)) |
| 182 | + for _, dd := range d { |
| 183 | + if dd < o { |
| 184 | + o = dd |
| 185 | + } |
| 186 | + } |
| 187 | + return o |
| 188 | +} |
0 commit comments