converted earcut.hpp library to Go and used it to triangulate fonts
This commit is contained in:
parent
1d5a02b1d6
commit
c4d3130770
3 changed files with 966 additions and 26 deletions
39
canvas.go
39
canvas.go
|
@ -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
801
earcut.go
Normal 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
152
text.go
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue