201 lines
5.1 KiB
Go
201 lines
5.1 KiB
Go
// Copyright ©2021 The Gonum Authors. All rights reserved.
|
|
// Use of this source code is governed by a BSD-style
|
|
// license that can be found in the LICENSE file.
|
|
|
|
package text
|
|
|
|
import (
|
|
"image/color"
|
|
"math"
|
|
|
|
"gonum.org/v1/plot/font"
|
|
"gonum.org/v1/plot/vg"
|
|
)
|
|
|
|
// Handler parses, formats and renders text.
|
|
type Handler interface {
|
|
// Cache returns the cache of fonts used by the text handler.
|
|
Cache() *font.Cache
|
|
|
|
// Extents returns the Extents of a font.
|
|
Extents(fnt font.Font) font.Extents
|
|
|
|
// Lines splits a given block of text into separate lines.
|
|
Lines(txt string) []string
|
|
|
|
// Box returns the bounding box of the given non-multiline text where:
|
|
// - width is the horizontal space from the origin.
|
|
// - height is the vertical space above the baseline.
|
|
// - depth is the vertical space below the baseline, a positive number.
|
|
Box(txt string, fnt font.Font) (width, height, depth vg.Length)
|
|
|
|
// Draw renders the given text with the provided style and position
|
|
// on the canvas.
|
|
Draw(c vg.Canvas, txt string, sty Style, pt vg.Point)
|
|
}
|
|
|
|
// XAlignment specifies text alignment in the X direction. Three preset
|
|
// options are available, but an arbitrary alignment
|
|
// can also be specified using XAlignment(desired number).
|
|
type XAlignment float64
|
|
|
|
const (
|
|
// XLeft aligns the left edge of the text with the specified location.
|
|
XLeft XAlignment = 0
|
|
// XCenter aligns the horizontal center of the text with the specified location.
|
|
XCenter XAlignment = -0.5
|
|
// XRight aligns the right edge of the text with the specified location.
|
|
XRight XAlignment = -1
|
|
)
|
|
|
|
// YAlignment specifies text alignment in the Y direction. Three preset
|
|
// options are available, but an arbitrary alignment
|
|
// can also be specified using YAlignment(desired number).
|
|
type YAlignment float64
|
|
|
|
const (
|
|
// YTop aligns the top of of the text with the specified location.
|
|
YTop YAlignment = -1
|
|
// YCenter aligns the vertical center of the text with the specified location.
|
|
YCenter YAlignment = -0.5
|
|
// YBottom aligns the bottom of the text with the specified location.
|
|
YBottom YAlignment = 0
|
|
)
|
|
|
|
// Position specifies the text position.
|
|
const (
|
|
PosLeft = -1
|
|
PosBottom = -1
|
|
PosCenter = 0
|
|
PosTop = +1
|
|
PosRight = +1
|
|
)
|
|
|
|
// Style describes what text will look like.
|
|
type Style struct {
|
|
// Color is the text color.
|
|
Color color.Color
|
|
|
|
// Font is the font description.
|
|
Font font.Font
|
|
|
|
// Rotation is the text rotation in radians, performed around the axis
|
|
// defined by XAlign and YAlign.
|
|
Rotation float64
|
|
|
|
// XAlign and YAlign specify the alignment of the text.
|
|
XAlign XAlignment
|
|
YAlign YAlignment
|
|
|
|
// Handler parses and formats text according to a given
|
|
// dialect (Markdown, LaTeX, plain, ...)
|
|
// The default is a plain text handler.
|
|
Handler Handler
|
|
}
|
|
|
|
// FontExtents returns the extents of this Style's font.
|
|
func (s Style) FontExtents() font.Extents {
|
|
return s.Handler.Extents(s.Font)
|
|
}
|
|
|
|
// Width returns the width of lines of text
|
|
// when using the given font before any text rotation is applied.
|
|
func (s Style) Width(txt string) (max vg.Length) {
|
|
w, _ := s.box(txt)
|
|
return w
|
|
}
|
|
|
|
// Height returns the height of the text when using
|
|
// the given font before any text rotation is applied.
|
|
func (s Style) Height(txt string) vg.Length {
|
|
_, h := s.box(txt)
|
|
return h
|
|
}
|
|
|
|
// box returns the bounding box of a possibly multi-line text.
|
|
func (s Style) box(txt string) (w, h vg.Length) {
|
|
var (
|
|
lines = s.Handler.Lines(txt)
|
|
e = s.FontExtents()
|
|
linegap = (e.Height - e.Ascent - e.Descent)
|
|
)
|
|
for i, line := range lines {
|
|
ww, hh, dd := s.Handler.Box(line, s.Font)
|
|
if ww > w {
|
|
w = ww
|
|
}
|
|
h += hh + dd
|
|
if i > 0 {
|
|
h += linegap
|
|
}
|
|
}
|
|
|
|
return w, h
|
|
}
|
|
|
|
// Rectangle returns a rectangle giving the bounds of
|
|
// this text assuming that it is drawn at (0, 0).
|
|
func (s Style) Rectangle(txt string) vg.Rectangle {
|
|
e := s.Handler.Extents(s.Font)
|
|
w, h := s.box(txt)
|
|
desc := vg.Length(e.Height - e.Ascent) // descent + linegap
|
|
xoff := vg.Length(s.XAlign) * w
|
|
yoff := vg.Length(s.YAlign)*h - desc
|
|
|
|
// lower left corner
|
|
p1 := rotatePoint(s.Rotation, vg.Point{X: xoff, Y: yoff})
|
|
// upper left corner
|
|
p2 := rotatePoint(s.Rotation, vg.Point{X: xoff, Y: h + yoff})
|
|
// lower right corner
|
|
p3 := rotatePoint(s.Rotation, vg.Point{X: w + xoff, Y: yoff})
|
|
// upper right corner
|
|
p4 := rotatePoint(s.Rotation, vg.Point{X: w + xoff, Y: h + yoff})
|
|
|
|
return vg.Rectangle{
|
|
Max: vg.Point{
|
|
X: max(p1.X, p2.X, p3.X, p4.X),
|
|
Y: max(p1.Y, p2.Y, p3.Y, p4.Y),
|
|
},
|
|
Min: vg.Point{
|
|
X: min(p1.X, p2.X, p3.X, p4.X),
|
|
Y: min(p1.Y, p2.Y, p3.Y, p4.Y),
|
|
},
|
|
}
|
|
}
|
|
|
|
// rotatePoint applies rotation theta (in radians) about the origin to point p.
|
|
func rotatePoint(theta float64, p vg.Point) vg.Point {
|
|
if theta == 0 {
|
|
return p
|
|
}
|
|
x := float64(p.X)
|
|
y := float64(p.Y)
|
|
|
|
sin, cos := math.Sincos(theta)
|
|
|
|
return vg.Point{
|
|
X: vg.Length(x*cos - y*sin),
|
|
Y: vg.Length(y*cos + x*sin),
|
|
}
|
|
}
|
|
|
|
func max(d ...vg.Length) vg.Length {
|
|
o := vg.Length(math.Inf(-1))
|
|
for _, dd := range d {
|
|
if dd > o {
|
|
o = dd
|
|
}
|
|
}
|
|
return o
|
|
}
|
|
|
|
func min(d ...vg.Length) vg.Length {
|
|
o := vg.Length(math.Inf(1))
|
|
for _, dd := range d {
|
|
if dd < o {
|
|
o = dd
|
|
}
|
|
}
|
|
return o
|
|
}
|