converted earcut.hpp library to Go and used it to triangulate fonts

This commit is contained in:
Thomas Friedel 2020-03-28 16:08:58 +01:00
parent 1d5a02b1d6
commit c4d3130770
3 changed files with 966 additions and 26 deletions

View file

@ -27,10 +27,11 @@ type Canvas struct {
state drawState
stateStack []drawState
images map[interface{}]*Image
fonts map[interface{}]*Font
fontCtxs map[fontKey]*frCache
fontTriCache map[*Font]*fontTriCache
images map[interface{}]*Image
fonts map[interface{}]*Font
fontCtxs map[fontKey]*frCache
fontPathCache map[*Font]*fontPathCache
fontTriCache map[*Font]*fontTriCache
shadowBuf []backendbase.Vec
}
@ -139,12 +140,13 @@ var Performance = struct {
// coordinates given here also use the bottom left as origin
func New(backend backendbase.Backend) *Canvas {
cv := &Canvas{
b: backend,
stateStack: make([]drawState, 0, 20),
images: make(map[interface{}]*Image),
fonts: make(map[interface{}]*Font),
fontCtxs: make(map[fontKey]*frCache),
fontTriCache: make(map[*Font]*fontTriCache),
b: backend,
stateStack: make([]drawState, 0, 20),
images: make(map[interface{}]*Image),
fonts: make(map[interface{}]*Font),
fontCtxs: make(map[fontKey]*frCache),
fontPathCache: make(map[*Font]*fontPathCache),
fontTriCache: make(map[*Font]*fontTriCache),
}
cv.state.lineWidth = 1
cv.state.lineAlpha = 1
@ -482,6 +484,7 @@ func (cv *Canvas) reduceCache(keepSize, rec int) {
oldest := time.Now()
var oldestFontKey fontKey
var oldestFontKey2 *Font
var oldestFontKey3 *Font
var oldestImageKey interface{}
for src, img := range cv.images {
w, h := img.img.Size()
@ -499,7 +502,7 @@ func (cv *Canvas) reduceCache(keepSize, rec int) {
oldestImageKey = nil
}
}
for fnt, cache := range cv.fontTriCache {
for fnt, cache := range cv.fontPathCache {
total += cache.size()
if cache.lastUsed.Before(oldest) {
oldest = cache.lastUsed
@ -508,6 +511,16 @@ func (cv *Canvas) reduceCache(keepSize, rec int) {
oldestImageKey = nil
}
}
for fnt, cache := range cv.fontTriCache {
total += cache.size()
if cache.lastUsed.Before(oldest) {
oldest = cache.lastUsed
oldestFontKey3 = fnt
oldestFontKey2 = nil
oldestFontKey = fontKey{}
oldestImageKey = nil
}
}
if total <= keepSize {
return
}
@ -516,7 +529,9 @@ func (cv *Canvas) reduceCache(keepSize, rec int) {
cv.images[oldestImageKey].Delete()
delete(cv.images, oldestImageKey)
} else if oldestFontKey2 != nil {
delete(cv.fontTriCache, oldestFontKey2)
delete(cv.fontPathCache, oldestFontKey2)
} else if oldestFontKey3 != nil {
delete(cv.fontTriCache, oldestFontKey3)
} else {
cv.fontCtxs[oldestFontKey].ctx = nil
delete(cv.fontCtxs, oldestFontKey)

801
earcut.go Normal file
View file

@ -0,0 +1,801 @@
package canvas
import (
"math"
"sort"
"github.com/tfriedel6/canvas/backend/backendbase"
)
// Go port of https://github.com/mapbox/earcut.hpp
type node struct {
i int
x, y float64
// previous and next vertice nodes in a polygon ring
prev *node
next *node
// z-order curve value
z int
// previous and next nodes in z-order
prevZ *node
nextZ *node
// indicates whether this is a steiner point
steiner bool
}
type earcut struct {
indices []int
vertices int
hashing bool
minX, minY float64
maxX, maxY float64
invSize float64
nodes []node
}
func (ec *earcut) run(points [][]backendbase.Vec) {
if len(points) == 0 {
return
}
var x, y float64
threshold := 80
ln := 0
for i := 0; threshold >= 0 && i < len(points); i++ {
threshold -= len(points[i])
ln += len(points[i])
}
//estimate size of nodes and indices
ec.nodes = make([]node, 0, ln*3/2)
ec.indices = make([]int, 0, ln+len(points[0]))
ec.vertices = 0
outerNode := ec.linkedList(points[0], true)
if outerNode == nil || outerNode.prev == outerNode.next {
return
}
if len(points) > 1 {
outerNode = ec.eliminateHoles(points, outerNode)
}
// if the shape is not too simple, we'll use z-order curve hash later; calculate polygon bbox
ec.hashing = threshold < 0
if ec.hashing {
p := outerNode.next
ec.minX, ec.maxX = outerNode.x, outerNode.x
ec.minY, ec.maxY = outerNode.y, outerNode.y
for {
x = p.x
y = p.y
ec.minX = math.Min(ec.minX, x)
ec.minY = math.Min(ec.minY, y)
ec.maxX = math.Min(ec.maxX, x)
ec.maxY = math.Min(ec.maxY, y)
p = p.next
if p != outerNode {
break
}
}
// minX, minY and size are later used to transform coords into integers for z-order calculation
ec.invSize = math.Max(ec.maxX-ec.minX, ec.maxY-ec.minY)
if ec.invSize != 0 {
ec.invSize = 1 / ec.invSize
}
}
ec.earcutLinked(outerNode, 0)
ec.nodes = ec.nodes[:0]
}
// create a circular doubly linked list from polygon points in the specified winding order
func (ec *earcut) linkedList(points []backendbase.Vec, clockwise bool) *node {
var sum float64
ln := len(points)
var i, j int
var last *node
// calculate original winding order of a polygon ring
if ln > 0 {
j = ln - 1
}
for i < ln {
p1 := points[i]
p2 := points[j]
p20 := p2[0]
p10 := p1[0]
p11 := p1[1]
p21 := p2[1]
sum += (p20 - p10) * (p11 + p21)
j = i
i++
}
// link points into circular doubly-linked list in the specified winding order
if clockwise == (sum > 0) {
for i := 0; i < ln; i++ {
last = ec.insertNode(ec.vertices+i, points[i], last)
}
} else {
for i = ln - 1; i >= 0; i-- {
last = ec.insertNode(ec.vertices+i, points[i], last)
}
}
if last != nil && ec.equals(last, last.next) {
ec.removeNode(last)
last = last.next
}
ec.vertices += ln
return last
}
// eliminate colinear or duplicate points
func (ec *earcut) filterPoints(start, end *node) *node {
if end == nil {
end = start
}
p := start
var again bool
for {
again = false
if !p.steiner && (ec.equals(p, p.next) || ec.area(p.prev, p, p.next) == 0) {
ec.removeNode(p)
p, end = p.prev, p.prev
if p == p.next {
break
}
again = true
} else {
p = p.next
}
if !again && p == end {
break
}
}
return end
}
// main ear slicing loop which triangulates a polygon (given as a linked list)
func (ec *earcut) earcutLinked(ear *node, pass int) {
if ear == nil {
return
}
// interlink polygon nodes in z-order
if pass == 0 && ec.hashing {
ec.indexCurve(ear)
}
stop := ear
var prev, next *node
iterations := 0
// iterate through ears, slicing them one by one
for ear.prev != ear.next {
iterations++
prev = ear.prev
next = ear.next
var e bool
if ec.hashing {
e = ec.isEarHashed(ear)
} else {
e = ec.isEar(ear)
}
if e {
// cut off the triangle
ec.indices = append(ec.indices, prev.i, ear.i, next.i)
ec.removeNode(ear)
// skipping the next vertice leads to less sliver triangles
ear = next.next
stop = next.next
continue
}
ear = next
// if we looped through the whole remaining polygon and can't find any more ears
if ear == stop {
// try filtering points and slicing again
if pass == 0 {
ec.earcutLinked(ec.filterPoints(ear, nil), 1)
} else if pass == 1 {
// if this didn't work, try curing all small self-intersections locally
ear = ec.cureLocalIntersections(ec.filterPoints(ear, nil))
ec.earcutLinked(ear, 2)
} else if pass == 2 {
// as a last resort, try splitting the remaining polygon into two
ec.splitEarcut(ear)
}
break
}
}
}
// check whether a polygon node forms a valid ear with adjacent nodes
func (ec *earcut) isEar(ear *node) bool {
a := ear.prev
b := ear
c := ear.next
if ec.area(a, b, c) >= 0 {
return false // reflex, can't be an ear
}
// now make sure we don't have other points inside the potential ear
p := ear.next.next
for p != ear.prev {
if ec.pointInTriangle(a.x, a.y, b.x, b.y, c.x, c.y, p.x, p.y) &&
ec.area(p.prev, p, p.next) >= 0 {
return false
}
p = p.next
}
return true
}
func (ec *earcut) isEarHashed(ear *node) bool {
a := ear.prev
b := ear
c := ear.next
if ec.area(a, b, c) >= 0 {
return false // reflex, can't be an ear
}
// triangle bbox; min & max are calculated like this for speed
minTX := math.Min(a.x, math.Min(b.x, c.x))
minTY := math.Min(a.y, math.Min(b.y, c.y))
maxTX := math.Max(a.x, math.Max(b.x, c.x))
maxTY := math.Max(a.y, math.Max(b.y, c.y))
// z-order range for the current triangle bbox;
minZ := ec.zOrder(minTX, minTY)
maxZ := ec.zOrder(maxTX, maxTY)
// first look for points inside the triangle in increasing z-order
p := ear.nextZ
for p != nil && p.z <= maxZ {
if p != ear.prev && p != ear.next &&
ec.pointInTriangle(a.x, a.y, b.x, b.y, c.x, c.y, p.x, p.y) &&
ec.area(p.prev, p, p.next) >= 0 {
return false
}
p = p.nextZ
}
// then look for points in decreasing z-order
p = ear.prevZ
for p != nil && p.z >= minZ {
if p != ear.prev && p != ear.next &&
ec.pointInTriangle(a.x, a.y, b.x, b.y, c.x, c.y, p.x, p.y) &&
ec.area(p.prev, p, p.next) >= 0 {
return false
}
p = p.prevZ
}
return true
}
// go through all polygon nodes and cure small local self-intersections
func (ec *earcut) cureLocalIntersections(start *node) *node {
p := start
for {
a := p.prev
b := p.next.next
// a self-intersection where edge (v[i-1],v[i]) intersects (v[i+1],v[i+2])
if !ec.equals(a, b) && ec.intersects(a, p, p.next, b) && ec.locallyInside(a, b) && ec.locallyInside(b, a) {
ec.indices = append(ec.indices, a.i, p.i, b.i)
// remove two nodes involved
ec.removeNode(p)
ec.removeNode(p.next)
p, start = b, b
}
p = p.next
if p == start {
break
}
}
return ec.filterPoints(p, nil)
}
// try splitting polygon into two and triangulate them independently
func (ec *earcut) splitEarcut(start *node) {
// look for a valid diagonal that divides the polygon into two
a := start
for {
b := a.next.next
for b != a.prev {
if a.i != b.i && ec.isValidDiagonal(a, b) {
// split the polygon in two by the diagonal
c := ec.splitPolygon(a, b)
// filter colinear points around the cuts
a = ec.filterPoints(a, a.next)
c = ec.filterPoints(c, c.next)
// run earcut on each half
ec.earcutLinked(a, 0)
ec.earcutLinked(c, 0)
return
}
b = b.next
}
a = a.next
if a == start {
break
}
}
}
// link every hole into the outer loop, producing a single-ring polygon without holes
func (ec *earcut) eliminateHoles(points [][]backendbase.Vec, outerNode *node) *node {
ln := len(points)
queue := make([]*node, 0, ln)
for i := 1; i < ln; i++ {
list := ec.linkedList(points[i], false)
if list != nil {
if list == list.next {
list.steiner = true
}
queue = append(queue, ec.getLeftmost(list))
}
}
sort.Slice(queue, func(a, b int) bool {
return queue[a].x < queue[b].x
})
// process holes from left to right
for i := 0; i < len(queue); i++ {
ec.eliminateHole(queue[i], outerNode)
outerNode = ec.filterPoints(outerNode, outerNode.next)
}
return outerNode
}
// find a bridge between vertices that connects hole with an outer ring and and link it
func (ec *earcut) eliminateHole(hole, outerNode *node) {
outerNode = ec.findHoleBridge(hole, outerNode)
if outerNode != nil {
b := ec.splitPolygon(outerNode, hole)
// filter out colinear points around cuts
ec.filterPoints(outerNode, outerNode.next)
ec.filterPoints(b, b.next)
}
}
// David Eberly's algorithm for finding a bridge between hole and outer polygon
func (ec *earcut) findHoleBridge(hole, outerNode *node) *node {
p := outerNode
hx := hole.x
hy := hole.y
qx := math.Inf(-1)
var m *node
// find a segment intersected by a ray from the hole's leftmost Vertex to the left;
// segment's endpoint with lesser x will be potential connection Vertex
for {
if hy <= p.y && hy >= p.next.y && p.next.y != p.y {
x := p.x + (hy-p.y)*(p.next.x-p.x)/(p.next.y-p.y)
if x <= hx && x > qx {
qx = x
if x == hx {
if hy == p.y {
return p
}
if hy == p.next.y {
return p.next
}
}
if p.x < p.next.x {
m = p
} else {
m = p.next
}
}
}
p = p.next
if p == outerNode {
break
}
}
if m == nil {
return nil
}
if hx == qx {
return m // hole touches outer segment; pick leftmost endpoint
}
// look for points inside the triangle of hole Vertex, segment intersection and endpoint;
// if there are no points found, we have a valid connection;
// otherwise choose the Vertex of the minimum angle with the ray as connection Vertex
stop := m
tanMin := math.Inf(1)
tanCur := 0.0
p = m
mx := m.x
my := m.y
for {
var pt1, pt2 float64
if hy < my {
pt1 = hx
pt2 = qx
} else {
pt1 = qx
pt2 = hx
}
if hx >= p.x && p.x >= mx && hx != p.x &&
ec.pointInTriangle(pt1, hy, mx, my, pt2, hy, p.x, p.y) {
tanCur = math.Abs(hy-p.y) / (hx - p.x) // tangential
if ec.locallyInside(p, hole) &&
(tanCur < tanMin || (tanCur == tanMin && (p.x > m.x || ec.sectorContainsSector(m, p)))) {
m = p
tanMin = tanCur
}
}
p = p.next
if p == stop {
break
}
}
return m
}
// whether sector in vertex m contains sector in vertex p in the same coordinates
func (ec *earcut) sectorContainsSector(m, p *node) bool {
return ec.area(m.prev, m, p.prev) < 0 && ec.area(p.next, m, m.next) < 0
}
// interlink polygon nodes in z-order
func (ec *earcut) indexCurve(start *node) {
if start == nil {
panic("start must not be nil")
}
p := start
for {
if p.z <= 0 {
p.z = ec.zOrder(p.x, p.y)
}
p.prevZ = p.prev
p.nextZ = p.next
p = p.next
if p == start {
break
}
}
p.prevZ.nextZ = nil
p.prevZ = nil
ec.sortLinked(p)
}
// Simon Tatham's linked list merge sort algorithm
// http://www.chiark.greenend.org.uk/~sgtatham/algorithms/listsort.html
func (ec *earcut) sortLinked(list *node) *node {
if list == nil {
panic("list must not be nil")
}
var p, q, e, tail *node
var i, numMerges, pSize, qSize int
inSize := 1
for {
p = list
list = nil
tail = nil
numMerges = 0
for p != nil {
numMerges++
q = p
pSize = 0
for i = 0; i < inSize; i++ {
pSize++
q = q.nextZ
if q == nil {
break
}
}
qSize = inSize
for pSize > 0 || (qSize > 0 && q != nil) {
if pSize == 0 {
e = q
q = q.nextZ
qSize--
} else if qSize == 0 || q == nil {
e = p
p = p.nextZ
pSize--
} else if p.z <= q.z {
e = p
p = p.nextZ
pSize--
} else {
e = q
q = q.nextZ
qSize--
}
if tail != nil {
tail.nextZ = e
} else {
list = e
}
e.prevZ = tail
tail = e
}
p = q
}
tail.nextZ = nil
if numMerges <= 1 {
return list
}
inSize *= 2
}
}
// z-order of a Vertex given coords and size of the data bounding box
func (ec *earcut) zOrder(x, y float64) int {
// coords are transformed into non-negative 15-bit integer range
x2 := int(32767.0 * (x - ec.minX) * ec.invSize)
y2 := int(32767.0 * (y - ec.minY) * ec.invSize)
x2 = (x2 | (x2 << 8)) & 0x00FF00FF
x2 = (x2 | (x2 << 4)) & 0x0F0F0F0F
x2 = (x2 | (x2 << 2)) & 0x33333333
x2 = (x2 | (x2 << 1)) & 0x55555555
y2 = (y2 | (y2 << 8)) & 0x00FF00FF
y2 = (y2 | (y2 << 4)) & 0x0F0F0F0F
y2 = (y2 | (y2 << 2)) & 0x33333333
y2 = (y2 | (y2 << 1)) & 0x55555555
return x2 | (y2 << 1)
}
// find the leftmost node of a polygon ring
func (ec *earcut) getLeftmost(start *node) *node {
p := start
leftmost := start
for {
if p.x < leftmost.x || (p.x == leftmost.x && p.y < leftmost.y) {
leftmost = p
}
p = p.next
if p == start {
break
}
}
return leftmost
}
// check if a point lies within a convex triangle
func (ec *earcut) pointInTriangle(ax, ay, bx, by, cx, cy, px, py float64) bool {
return (cx-px)*(ay-py)-(ax-px)*(cy-py) >= 0 &&
(ax-px)*(by-py)-(bx-px)*(ay-py) >= 0 &&
(bx-px)*(cy-py)-(cx-px)*(by-py) >= 0
}
// check if a diagonal between two polygon nodes is valid (lies in polygon interior)
func (ec *earcut) isValidDiagonal(a, b *node) bool {
return a.next.i != b.i && a.prev.i != b.i && !ec.intersectsPolygon(a, b) && // dones't intersect other edges
((ec.locallyInside(a, b) && ec.locallyInside(b, a) && ec.middleInside(a, b) && // locally visible
(ec.area(a.prev, a, b.prev) != 0.0 || ec.area(a, b.prev, b) != 0.0)) || // does not create opposite-facing sectors
(ec.equals(a, b) && ec.area(a.prev, a, a.next) > 0 && ec.area(b.prev, b, b.next) > 0)) // special zero-length case
}
// signed area of a triangle
func (ec *earcut) area(p, q, r *node) float64 {
return (q.y-p.y)*(r.x-q.x) - (q.x-p.x)*(r.y-q.y)
}
// check if two points are equal
func (ec *earcut) equals(p1, p2 *node) bool {
return p1.x == p2.x && p1.y == p2.y
}
// check if two segments intersect
func (ec *earcut) intersects(p1, q1, p2, q2 *node) bool {
o1 := ec.sign(ec.area(p1, q1, p2))
o2 := ec.sign(ec.area(p1, q1, q2))
o3 := ec.sign(ec.area(p2, q2, p1))
o4 := ec.sign(ec.area(p2, q2, q1))
if o1 != o2 && o3 != o4 {
return true // general case
}
if o1 == 0 && ec.onSegment(p1, p2, q1) {
// p1, q1 and p2 are collinear and p2 lies on p1q1
return true
}
if o2 == 0 && ec.onSegment(p1, q2, q1) {
// p1, q1 and q2 are collinear and q2 lies on p1q1
return true
}
if o3 == 0 && ec.onSegment(p2, p1, q2) {
// p2, q2 and p1 are collinear and p1 lies on p2q2
return true
}
if o4 == 0 && ec.onSegment(p2, q1, q2) {
// p2, q2 and q1 are collinear and q1 lies on p2q2
return true
}
return false
}
// for collinear points p, q, r, check if point q lies on segment pr
func (ec *earcut) onSegment(p, q, r *node) bool {
return q.x <= math.Max(p.x, r.x) &&
q.x >= math.Min(p.x, r.x) &&
q.y <= math.Max(p.y, r.y) &&
q.y >= math.Min(p.y, r.y)
}
func (ec *earcut) sign(val float64) int {
if val < 0 {
return -1
} else if val > 0 {
return 1
}
return 0
}
// check if a polygon diagonal intersects any polygon segments
func (ec *earcut) intersectsPolygon(a, b *node) bool {
p := a
for {
if p.i != a.i && p.next.i != a.i && p.i != b.i && p.next.i != b.i &&
ec.intersects(p, p.next, a, b) {
return true
}
p = p.next
if p == a {
break
}
}
return false
}
// check if a polygon diagonal is locally inside the polygon
func (ec *earcut) locallyInside(a, b *node) bool {
if ec.area(a.prev, a, a.next) < 0 {
return ec.area(a, b, a.next) >= 0 && ec.area(a, a.prev, b) >= 0
}
return ec.area(a, b, a.prev) < 0 || ec.area(a, a.next, b) < 0
}
// check if the middle Vertex of a polygon diagonal is inside the polygon
func (ec *earcut) middleInside(a, b *node) bool {
p := a
inside := false
px := (a.x + b.x) / 2
py := (a.y + b.y) / 2
for {
if ((p.y > py) != (p.next.y > py)) && p.next.y != p.y &&
(px < (p.next.x-p.x)*(py-p.y)/(p.next.y-p.y)+p.x) {
inside = !inside
}
p = p.next
if p == a {
break
}
}
return inside
}
// link two polygon vertices with a bridge; if the vertices belong to the same ring, it splits
// polygon into two; if one belongs to the outer ring and another to a hole, it merges it into a
// single ring
func (ec *earcut) splitPolygon(a, b *node) *node {
ec.nodes = append(ec.nodes, node{i: a.i, x: a.x, y: a.y})
a2 := &ec.nodes[len(ec.nodes)-1]
ec.nodes = append(ec.nodes, node{i: b.i, x: b.x, y: b.y})
b2 := &ec.nodes[len(ec.nodes)-1]
an := a.next
bp := b.prev
a.next = b
b.prev = a
a2.next = an
an.prev = a2
b2.next = a2
a2.prev = b2
bp.next = b2
b2.prev = bp
return b2
}
// create a node and util::optionally link it with previous one (in a circular doubly linked list)
func (ec *earcut) insertNode(i int, pt backendbase.Vec, last *node) *node {
ec.nodes = append(ec.nodes, node{i: i, x: pt[0], y: pt[1]})
p := &ec.nodes[len(ec.nodes)-1]
if last == nil {
p.prev = p
p.next = p
} else {
if last == nil {
panic("last must not be nil")
}
p.next = last.next
p.prev = last
last.next.prev = p
last.next = p
}
return p
}
func (ec *earcut) removeNode(p *node) {
p.next.prev = p.prev
p.prev.next = p.next
if p.prevZ != nil {
p.prevZ.nextZ = p.nextZ
}
if p.nextZ != nil {
p.nextZ.prevZ = p.prevZ
}
}

152
text.go
View file

@ -34,16 +34,29 @@ type frCache struct {
lastUsed time.Time
}
type fontTriCache struct {
type fontPathCache struct {
cache map[truetype.Index]*Path2D
lastUsed time.Time
}
func (fpc *fontPathCache) size() int {
size := 0
pps := int(unsafe.Sizeof(pathPoint{}))
for _, p := range fpc.cache {
size += len(p.p) * pps
}
return size
}
type fontTriCache struct {
cache map[truetype.Index][]backendbase.Vec
lastUsed time.Time
}
func (ftc *fontTriCache) size() int {
size := 0
pps := int(unsafe.Sizeof(pathPoint{}))
for _, p := range ftc.cache {
size += len(p.p) * pps
size += len(p) * 16
}
return size
}
@ -140,11 +153,10 @@ func (cv *Canvas) FillText(str string, x, y float64) {
scale := (scaleX + scaleY) * 0.5
fontSize := fixed.Int26_6(math.Round(float64(cv.state.fontSize) * scale))
// if fontSize > fixed.I(30) {
// if time.Now().Second()%2 == 0 {
// cv.fillText2(str, x, y)
// return
// }
if fontSize > fixed.I(25) {
cv.fillText2(str, x, y)
return
}
frc := cv.getFRContext(cv.state.font, fontSize)
fnt := cv.state.font.font
@ -258,9 +270,11 @@ func (cv *Canvas) fillText2(str string, x, y float64) {
continue
}
path := cv.runePath(rn)
tris := cv.runeTris(rn)
tf := scaleMat.Mul(backendbase.MatTranslate(backendbase.Vec{x, y})).Mul(cv.state.transform)
cv.fillPath(path, tf)
cv.drawShadow(tris, nil, false)
stl := cv.backendFillStyle(&cv.state.fill, 1)
cv.b.Fill(&stl, tris, tf, false)
x += float64(advance) / 64
}
@ -483,7 +497,7 @@ func (cv *Canvas) runePath(rn rune) *Path2D {
idx = cv.state.font.font.Index(' ')
}
if cache, ok := cv.fontTriCache[cv.state.font]; ok {
if cache, ok := cv.fontPathCache[cv.state.font]; ok {
if path, ok := cache.cache[idx]; ok {
cache.lastUsed = time.Now()
return path
@ -553,10 +567,10 @@ func (cv *Canvas) runePath(rn rune) *Path2D {
from = to
}
cache, ok := cv.fontTriCache[cv.state.font]
cache, ok := cv.fontPathCache[cv.state.font]
if !ok {
cache = &fontTriCache{cache: make(map[truetype.Index]*Path2D, 1024)}
cv.fontTriCache[cv.state.font] = cache
cache = &fontPathCache{cache: make(map[truetype.Index]*Path2D, 1024)}
cv.fontPathCache[cv.state.font] = cache
}
cache.lastUsed = time.Now()
cache.cache[idx] = path
@ -564,6 +578,116 @@ func (cv *Canvas) runePath(rn rune) *Path2D {
return path
}
func (cv *Canvas) runeTris(rn rune) []backendbase.Vec {
idx := cv.state.font.font.Index(rn)
if idx == 0 {
idx = cv.state.font.font.Index(' ')
}
if cache, ok := cv.fontTriCache[cv.state.font]; ok {
if tris, ok := cache.cache[idx]; ok {
cache.lastUsed = time.Now()
return tris
}
}
const scale = 1.0 / 64.0
var gb truetype.GlyphBuf
gb.Load(cv.state.font.font, baseFontSize, idx, font.HintingFull)
polyWithHoles := make([][]backendbase.Vec, 0, len(gb.Ends))
from := 0
for _, to := range gb.Ends {
ps := gb.Points[from:to]
start := ps[0]
others := []truetype.Point(nil)
if ps[0].Flags&0x01 != 0 {
others = ps[1:]
} else {
last := ps[len(ps)-1]
if ps[len(ps)-1].Flags&0x01 != 0 {
start = last
others = ps[:len(ps)-1]
} else {
start = truetype.Point{
X: (start.X + last.X) / 2,
Y: (start.Y + last.Y) / 2,
}
others = ps
}
}
p0, on0 := start, true
path := &Path2D{cv: cv, p: make([]pathPoint, 0, 50), standalone: true, noSelfIntersection: true}
path.MoveTo(float64(p0.X)*scale, -float64(p0.Y)*scale)
for _, p := range others {
on := p.Flags&0x01 != 0
if on {
if on0 {
path.LineTo(float64(p.X)*scale, -float64(p.Y)*scale)
} else {
path.QuadraticCurveTo(float64(p0.X)*scale, -float64(p0.Y)*scale, float64(p.X)*scale, -float64(p.Y)*scale)
}
} else {
if on0 {
// No-op.
} else {
mid := fixed.Point26_6{
X: (p0.X + p.X) / 2,
Y: (p0.Y + p.Y) / 2,
}
path.QuadraticCurveTo(float64(p0.X)*scale, -float64(p0.Y)*scale, float64(mid.X)*scale, -float64(mid.Y)*scale)
}
}
p0, on0 = p, on
}
if on0 {
path.LineTo(float64(start.X)*scale, -float64(start.Y)*scale)
} else {
path.QuadraticCurveTo(float64(p0.X)*scale, -float64(p0.Y)*scale, float64(start.X)*scale, -float64(start.Y)*scale)
}
path.ClosePath()
poly := make([]backendbase.Vec, len(path.p))
for i, pt := range path.p {
poly[i] = pt.pos
}
polyWithHoles = append(polyWithHoles, poly)
from = to
}
var ec earcut
ec.run(polyWithHoles)
tris := make([]backendbase.Vec, len(ec.indices))
for i, idx := range ec.indices {
pidx := 0
poly := polyWithHoles[pidx]
for idx >= len(poly) {
idx -= len(poly)
pidx++
poly = polyWithHoles[pidx]
}
tris[i] = poly[idx]
}
cache, ok := cv.fontTriCache[cv.state.font]
if !ok {
cache = &fontTriCache{cache: make(map[truetype.Index][]backendbase.Vec, 1024)}
cv.fontTriCache[cv.state.font] = cache
}
cache.lastUsed = time.Now()
cache.cache[idx] = tris
return tris
}
// TextMetrics is the result of a MeasureText call
type TextMetrics struct {
Width float64