291 lines
7.1 KiB
Go
291 lines
7.1 KiB
Go
package canvas
|
|
|
|
import (
|
|
"errors"
|
|
"image"
|
|
"image/draw"
|
|
"io/ioutil"
|
|
"unsafe"
|
|
|
|
"github.com/golang/freetype"
|
|
"github.com/golang/freetype/truetype"
|
|
"golang.org/x/image/font"
|
|
"golang.org/x/image/math/fixed"
|
|
)
|
|
|
|
var fontRenderingContext = newFRContext()
|
|
|
|
// Font is a loaded font that can be passed to the
|
|
// SetFont method
|
|
type Font struct {
|
|
font *truetype.Font
|
|
}
|
|
|
|
var fonts = make(map[string]*Font)
|
|
var zeroes [alphaTexSize]byte
|
|
var textImage *image.Alpha
|
|
|
|
var defaultFont *Font
|
|
|
|
// LoadFont loads a font and returns the result. The font
|
|
// can be a file name or a byte slice in TTF format
|
|
func LoadFont(src interface{}) (*Font, error) {
|
|
var f *Font
|
|
switch v := src.(type) {
|
|
case *truetype.Font:
|
|
f = &Font{font: v}
|
|
case string:
|
|
data, err := ioutil.ReadFile(v)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
font, err := freetype.ParseFont(data)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
f = &Font{font: font}
|
|
case []byte:
|
|
font, err := freetype.ParseFont(v)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
f = &Font{font: font}
|
|
default:
|
|
return nil, errors.New("Unsupported source type")
|
|
}
|
|
if defaultFont == nil {
|
|
defaultFont = f
|
|
}
|
|
return f, nil
|
|
}
|
|
|
|
// FillText draws the given string at the given coordinates
|
|
// using the currently set font and font height
|
|
func (cv *Canvas) FillText(str string, x, y float64) {
|
|
cv.activate()
|
|
|
|
if cv.state.font == nil {
|
|
return
|
|
}
|
|
|
|
frc := fontRenderingContext
|
|
frc.setFont(cv.state.font.font)
|
|
frc.setFontSize(float64(cv.state.fontSize))
|
|
fnt := cv.state.font.font
|
|
|
|
curX := x
|
|
var p fixed.Point26_6
|
|
|
|
strFrom, strTo := 0, len(str)
|
|
curInside := false
|
|
|
|
var textOffset image.Point
|
|
var strWidth, strHeight int
|
|
for i, rn := range str {
|
|
idx := fnt.Index(rn)
|
|
if idx == 0 {
|
|
idx = fnt.Index(' ')
|
|
}
|
|
advance, bounds, err := frc.glyphMeasure(idx, p)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
|
|
p0 := cv.tf(vec{float64(bounds.Min.X) + curX, float64(bounds.Min.Y) + y})
|
|
p1 := cv.tf(vec{float64(bounds.Min.X) + curX, float64(bounds.Max.Y) + y})
|
|
p2 := cv.tf(vec{float64(bounds.Max.X) + curX, float64(bounds.Max.Y) + y})
|
|
p3 := cv.tf(vec{float64(bounds.Max.X) + curX, float64(bounds.Min.Y) + y})
|
|
inside := (p0[0] >= 0 || p1[0] >= 0 || p2[0] >= 0 || p3[0] >= 0) &&
|
|
(p0[1] >= 0 || p1[1] >= 0 || p2[1] >= 0 || p3[1] >= 0) &&
|
|
(p0[0] < cv.fw || p1[0] < cv.fw || p2[0] < cv.fw || p3[0] < cv.fw) &&
|
|
(p0[1] < cv.fh || p1[1] < cv.fh || p2[1] < cv.fh || p3[1] < cv.fh)
|
|
|
|
if !curInside && inside {
|
|
curInside = true
|
|
strFrom = i
|
|
} else if curInside && !inside {
|
|
strTo = i
|
|
break
|
|
}
|
|
|
|
if i == 0 {
|
|
textOffset.X = bounds.Min.X
|
|
}
|
|
if bounds.Min.Y < textOffset.Y {
|
|
textOffset.Y = bounds.Min.Y
|
|
}
|
|
if h := bounds.Dy(); h > strHeight {
|
|
strHeight = h
|
|
}
|
|
p.X += advance
|
|
}
|
|
strWidth = p.X.Ceil() - textOffset.X
|
|
|
|
if strFrom == strTo || strWidth == 0 || strHeight == 0 {
|
|
return
|
|
}
|
|
|
|
if textImage == nil || textImage.Bounds().Dx() < strWidth || textImage.Bounds().Dy() < strHeight {
|
|
var size int
|
|
for size = 2; size < alphaTexSize; size *= 2 {
|
|
if size >= strWidth && size >= strHeight {
|
|
break
|
|
}
|
|
}
|
|
if size > alphaTexSize {
|
|
size = alphaTexSize
|
|
}
|
|
textImage = image.NewAlpha(image.Rect(0, 0, size, size))
|
|
}
|
|
|
|
curX = x
|
|
p = fixed.Point26_6{}
|
|
|
|
for y := 0; y < strHeight; y++ {
|
|
off := textImage.PixOffset(0, y)
|
|
line := textImage.Pix[off : off+strWidth]
|
|
for i := range line {
|
|
line[i] = 0
|
|
}
|
|
}
|
|
|
|
prev, hasPrev := truetype.Index(0), false
|
|
for _, rn := range str[strFrom:strTo] {
|
|
idx := fnt.Index(rn)
|
|
if idx == 0 {
|
|
prev = 0
|
|
hasPrev = false
|
|
continue
|
|
}
|
|
if hasPrev {
|
|
kern := fnt.Kern(frc.scale, prev, idx)
|
|
if frc.hinting != font.HintingNone {
|
|
kern = (kern + 32) &^ 63
|
|
}
|
|
curX += float64(kern) / 64
|
|
}
|
|
advance, mask, offset, err := frc.glyph(idx, p)
|
|
if err != nil {
|
|
prev = 0
|
|
hasPrev = false
|
|
continue
|
|
}
|
|
p.X += advance
|
|
|
|
draw.Draw(textImage, mask.Bounds().Add(offset).Sub(textOffset), mask, image.ZP, draw.Over)
|
|
|
|
curX += float64(advance) / 64
|
|
}
|
|
|
|
if cv.state.textAlign == Center {
|
|
x -= float64(strWidth) * 0.5
|
|
} else if cv.state.textAlign == Right || cv.state.textAlign == End {
|
|
x -= float64(strWidth)
|
|
}
|
|
|
|
gli.ActiveTexture(gl_TEXTURE1)
|
|
gli.BindTexture(gl_TEXTURE_2D, alphaTex)
|
|
for y := 0; y < strHeight; y++ {
|
|
off := y * textImage.Stride
|
|
gli.TexSubImage2D(gl_TEXTURE_2D, 0, 0, int32(alphaTexSize-1-y), int32(strWidth), 1, gl_ALPHA, gl_UNSIGNED_BYTE, gli.Ptr(&textImage.Pix[off]))
|
|
}
|
|
|
|
cv.drawTextShadow(textOffset, strWidth, strHeight, x, y)
|
|
|
|
gli.StencilFunc(gl_EQUAL, 0, 0xFF)
|
|
|
|
gli.BindBuffer(gl_ARRAY_BUFFER, buf)
|
|
|
|
vertex, alphaTexCoord := cv.useAlphaShader(&cv.state.fill, 1)
|
|
|
|
gli.EnableVertexAttribArray(vertex)
|
|
gli.EnableVertexAttribArray(alphaTexCoord)
|
|
|
|
p0 := cv.tf(vec{float64(textOffset.X) + x, float64(textOffset.Y) + y})
|
|
p1 := cv.tf(vec{float64(textOffset.X) + x, float64(textOffset.Y+strHeight) + y})
|
|
p2 := cv.tf(vec{float64(textOffset.X+strWidth) + x, float64(textOffset.Y+strHeight) + y})
|
|
p3 := cv.tf(vec{float64(textOffset.X+strWidth) + x, float64(textOffset.Y) + y})
|
|
|
|
tw := float64(strWidth) / alphaTexSize
|
|
th := float64(strHeight) / alphaTexSize
|
|
data := [16]float32{float32(p0[0]), float32(p0[1]), float32(p1[0]), float32(p1[1]), float32(p2[0]), float32(p2[1]), float32(p3[0]), float32(p3[1]),
|
|
0, 1, 0, float32(1 - th), float32(tw), float32(1 - th), float32(tw), 1}
|
|
gli.BufferData(gl_ARRAY_BUFFER, len(data)*4, unsafe.Pointer(&data[0]), gl_STREAM_DRAW)
|
|
|
|
gli.VertexAttribPointer(vertex, 2, gl_FLOAT, false, 0, 0)
|
|
gli.VertexAttribPointer(alphaTexCoord, 2, gl_FLOAT, false, 0, 8*4)
|
|
gli.DrawArrays(gl_TRIANGLE_FAN, 0, 4)
|
|
|
|
gli.DisableVertexAttribArray(vertex)
|
|
gli.DisableVertexAttribArray(alphaTexCoord)
|
|
|
|
gli.ActiveTexture(gl_TEXTURE0)
|
|
|
|
gli.StencilFunc(gl_ALWAYS, 0, 0xFF)
|
|
|
|
for y := 0; y < strHeight; y++ {
|
|
gli.TexSubImage2D(gl_TEXTURE_2D, 0, 0, int32(alphaTexSize-1-y), int32(strWidth), 1, gl_ALPHA, gl_UNSIGNED_BYTE, gli.Ptr(&zeroes[0]))
|
|
}
|
|
}
|
|
|
|
// TextMetrics is the result of a MeasureText call
|
|
type TextMetrics struct {
|
|
Width float64
|
|
ActualBoundingBoxAscent float64
|
|
ActualBoundingBoxDescent float64
|
|
}
|
|
|
|
// MeasureText measures the given string using the
|
|
// current font and font height
|
|
func (cv *Canvas) MeasureText(str string) TextMetrics {
|
|
if cv.state.font == nil {
|
|
return TextMetrics{}
|
|
}
|
|
|
|
frc := fontRenderingContext
|
|
frc.setFont(cv.state.font.font)
|
|
frc.setFontSize(float64(cv.state.fontSize))
|
|
fnt := cv.state.font.font
|
|
|
|
var p fixed.Point26_6
|
|
var x float64
|
|
var minY float64
|
|
var maxY float64
|
|
prev, hasPrev := truetype.Index(0), false
|
|
for _, rn := range str {
|
|
idx := fnt.Index(rn)
|
|
if idx == 0 {
|
|
prev = 0
|
|
hasPrev = false
|
|
continue
|
|
}
|
|
if hasPrev {
|
|
kern := fnt.Kern(frc.scale, prev, idx)
|
|
if frc.hinting != font.HintingNone {
|
|
kern = (kern + 32) &^ 63
|
|
}
|
|
x += float64(kern) / 64
|
|
}
|
|
|
|
advance, glyphBounds, err := frc.glyphMeasure(idx, p)
|
|
if err != nil {
|
|
prev = 0
|
|
hasPrev = false
|
|
continue
|
|
}
|
|
if glyphMinY := float64(glyphBounds.Min.Y); glyphMinY < minY {
|
|
minY = glyphMinY
|
|
}
|
|
if glyphMaxY := float64(glyphBounds.Max.Y); glyphMaxY > maxY {
|
|
maxY = glyphMaxY
|
|
}
|
|
x += float64(advance) / 64
|
|
p.X += advance
|
|
}
|
|
|
|
return TextMetrics{
|
|
Width: x,
|
|
ActualBoundingBoxAscent: -minY,
|
|
ActualBoundingBoxDescent: +maxY,
|
|
}
|
|
}
|