diff --git a/canvas.go b/canvas.go index 997afdf..ee30616 100644 --- a/canvas.go +++ b/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) diff --git a/earcut.go b/earcut.go new file mode 100644 index 0000000..e536556 --- /dev/null +++ b/earcut.go @@ -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 + } +} diff --git a/text.go b/text.go index cb11063..d9c0a60 100644 --- a/text.go +++ b/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