mirror of
https://github.com/hajimehoshi/ebiten.git
synced 2026-04-23 00:07:15 +08:00
vector: refactoring: organize files
This commit is contained in:
+121
@@ -18,10 +18,131 @@ import (
|
||||
"fmt"
|
||||
"image"
|
||||
"slices"
|
||||
"sync"
|
||||
_ "unsafe"
|
||||
|
||||
"github.com/hajimehoshi/ebiten/v2"
|
||||
)
|
||||
|
||||
// FillRule is the rule whether an overlapped region is rendered or not.
|
||||
type FillRule int
|
||||
|
||||
const (
|
||||
// FillRuleNonZero means that triangles are rendered based on the non-zero rule.
|
||||
// If and only if the number of overlaps is not 0, the region is rendered.
|
||||
FillRuleNonZero FillRule = iota
|
||||
|
||||
// FillRuleEvenOdd means that triangles are rendered based on the even-odd rule.
|
||||
// If and only if the number of overlaps is odd, the region is rendered.
|
||||
FillRuleEvenOdd
|
||||
)
|
||||
|
||||
var (
|
||||
theCallbackTokens = map[*ebiten.Image]int64{}
|
||||
theFillPathsStates = map[*ebiten.Image]*fillPathsState{}
|
||||
theFillPathsStatesPool = sync.Pool{
|
||||
New: func() any {
|
||||
return &fillPathsState{}
|
||||
},
|
||||
}
|
||||
theFillPathM sync.Mutex
|
||||
)
|
||||
|
||||
// FillOptions is options to fill a path.
|
||||
type FillOptions struct {
|
||||
// FillRule is the rule whether an overlapped region is rendered or not.
|
||||
// The default (zero) value is FillRuleNonZero.
|
||||
FillRule FillRule
|
||||
}
|
||||
|
||||
// DrawPathOptions is options to draw a path.
|
||||
type DrawPathOptions struct {
|
||||
// AntiAlias is whether the path is drawn with anti-aliasing.
|
||||
// The default (zero) value is false.
|
||||
AntiAlias bool
|
||||
|
||||
// ColorScale is the color scale to apply to the path.
|
||||
// The default (zero) value is identity, which is (1, 1, 1, 1) (white).
|
||||
ColorScale ebiten.ColorScale
|
||||
|
||||
// Blend is the blend mode to apply to the path.
|
||||
// The default (zero) value is ebiten.BlendSourceOver.
|
||||
Blend ebiten.Blend
|
||||
}
|
||||
|
||||
// FillPath fills the specified path with the specified options.
|
||||
func FillPath(dst *ebiten.Image, path *Path, fillOptions *FillOptions, drawPathOptions *DrawPathOptions) {
|
||||
if drawPathOptions == nil {
|
||||
drawPathOptions = &DrawPathOptions{}
|
||||
}
|
||||
if fillOptions == nil {
|
||||
fillOptions = &FillOptions{}
|
||||
}
|
||||
|
||||
bounds := dst.Bounds()
|
||||
|
||||
// Get the original image if dst is a sub-image to integrate the callbacks.
|
||||
dst = originalImage(dst)
|
||||
|
||||
theFillPathM.Lock()
|
||||
defer theFillPathM.Unlock()
|
||||
|
||||
// Remove the previous registered callbacks.
|
||||
if token, ok := theCallbackTokens[dst]; ok {
|
||||
removeUsageCallback(dst, token)
|
||||
}
|
||||
delete(theCallbackTokens, dst)
|
||||
|
||||
if _, ok := theFillPathsStates[dst]; !ok {
|
||||
theFillPathsStates[dst] = theFillPathsStatesPool.Get().(*fillPathsState)
|
||||
}
|
||||
s := theFillPathsStates[dst]
|
||||
if s.antialias != drawPathOptions.AntiAlias || s.blend != drawPathOptions.Blend || s.fillRule != fillOptions.FillRule {
|
||||
s.fillPaths(dst)
|
||||
s.reset()
|
||||
}
|
||||
s.antialias = drawPathOptions.AntiAlias
|
||||
s.blend = drawPathOptions.Blend
|
||||
s.fillRule = fillOptions.FillRule
|
||||
s.addPath(path, bounds, drawPathOptions.ColorScale)
|
||||
|
||||
// Use an independent callback function to avoid unexpected captures.
|
||||
theCallbackTokens[dst] = addUsageCallback(dst, fillPathCallback)
|
||||
}
|
||||
|
||||
func fillPathCallback(dst *ebiten.Image) {
|
||||
if originalImage(dst) != dst {
|
||||
panic("vector: dst must be the original image")
|
||||
}
|
||||
|
||||
theFillPathM.Lock()
|
||||
defer theFillPathM.Unlock()
|
||||
|
||||
// Remove the callback not to call this twice.
|
||||
if token, ok := theCallbackTokens[dst]; ok {
|
||||
removeUsageCallback(dst, token)
|
||||
}
|
||||
delete(theCallbackTokens, dst)
|
||||
|
||||
s, ok := theFillPathsStates[dst]
|
||||
if !ok {
|
||||
panic("vector: fillPathsState must exist here")
|
||||
}
|
||||
s.fillPaths(dst)
|
||||
s.reset()
|
||||
delete(theFillPathsStates, dst)
|
||||
theFillPathsStatesPool.Put(s)
|
||||
}
|
||||
|
||||
//go:linkname originalImage github.com/hajimehoshi/ebiten/v2.originalImage
|
||||
func originalImage(img *ebiten.Image) *ebiten.Image
|
||||
|
||||
//go:linkname addUsageCallback github.com/hajimehoshi/ebiten/v2.addUsageCallback
|
||||
func addUsageCallback(img *ebiten.Image, fn func(img *ebiten.Image)) int64
|
||||
|
||||
//go:linkname removeUsageCallback github.com/hajimehoshi/ebiten/v2.removeUsageCallback
|
||||
func removeUsageCallback(img *ebiten.Image, token int64)
|
||||
|
||||
type offsetAndColor struct {
|
||||
offsetX float32
|
||||
offsetY float32
|
||||
|
||||
-392
@@ -177,48 +177,6 @@ func (s *subPath) endDir(index int) vec2 {
|
||||
panic("not reached")
|
||||
}
|
||||
|
||||
// flatPath is a flattened sub-path of a path.
|
||||
// A flatPath consists of points for line segments.
|
||||
type flatPath struct {
|
||||
points []point
|
||||
closed bool
|
||||
}
|
||||
|
||||
// reset resets the flatPath.
|
||||
// reset doesn't release the allocated memory so that the memory can be reused.
|
||||
func (f *flatPath) reset() {
|
||||
f.points = f.points[:0]
|
||||
f.closed = false
|
||||
}
|
||||
|
||||
func (f flatPath) pointCount() int {
|
||||
return len(f.points)
|
||||
}
|
||||
|
||||
func (f flatPath) lastPoint() point {
|
||||
return f.points[len(f.points)-1]
|
||||
}
|
||||
|
||||
func (f *flatPath) appendPoint(pt point) {
|
||||
if f.closed {
|
||||
panic("vector: a closed flatPath cannot append a new point")
|
||||
}
|
||||
|
||||
if len(f.points) > 0 {
|
||||
// Do not add a too close point to the last point.
|
||||
// This can cause unexpected rendering results.
|
||||
if lp := f.lastPoint(); abs(lp.x-pt.x) < 1e-2 && abs(lp.y-pt.y) < 1e-2 {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
f.points = append(f.points, pt)
|
||||
}
|
||||
|
||||
func (f *flatPath) close() {
|
||||
f.closed = true
|
||||
}
|
||||
|
||||
// Path represents a collection of vector graphics operations.
|
||||
type Path struct {
|
||||
subPaths []subPath
|
||||
@@ -242,13 +200,6 @@ func (p *Path) resetSubPaths() {
|
||||
p.subPaths = p.subPaths[:0]
|
||||
}
|
||||
|
||||
func (p *Path) resetFlatPaths() {
|
||||
for _, fp := range p.flatPaths {
|
||||
fp.reset()
|
||||
}
|
||||
p.flatPaths = p.flatPaths[:0]
|
||||
}
|
||||
|
||||
func (p *Path) resetLastSubPathCacheStates() {
|
||||
if len(p.subPaths) == 0 {
|
||||
return
|
||||
@@ -258,45 +209,6 @@ func (p *Path) resetLastSubPathCacheStates() {
|
||||
s.isCachedValidValid = false
|
||||
}
|
||||
|
||||
func (p *Path) appendNewFlatPath(pt point) {
|
||||
if cap(p.flatPaths) > len(p.flatPaths) {
|
||||
// Reuse the last flat path since the last flat path might have an already allocated slice.
|
||||
p.flatPaths = p.flatPaths[:len(p.flatPaths)+1]
|
||||
p.flatPaths[len(p.flatPaths)-1].reset()
|
||||
p.flatPaths[len(p.flatPaths)-1].appendPoint(pt)
|
||||
return
|
||||
}
|
||||
p.flatPaths = append(p.flatPaths, flatPath{
|
||||
points: []point{pt},
|
||||
})
|
||||
}
|
||||
|
||||
func (p *Path) ensureFlatPaths() []flatPath {
|
||||
if len(p.flatPaths) > 0 || len(p.subPaths) == 0 {
|
||||
return p.flatPaths
|
||||
}
|
||||
|
||||
for _, subPath := range p.subPaths {
|
||||
p.appendNewFlatPath(subPath.start)
|
||||
cur := subPath.start
|
||||
for _, op := range subPath.ops {
|
||||
switch op.typ {
|
||||
case opTypeLineTo:
|
||||
p.appendFlatPathPointsForLine(op.p1)
|
||||
cur = op.p1
|
||||
case opTypeQuadTo:
|
||||
p.appendFlatPathPointsForQuad(cur, op.p1, op.p2, 0)
|
||||
cur = op.p2
|
||||
}
|
||||
}
|
||||
if subPath.closed {
|
||||
p.closeFlatPath()
|
||||
}
|
||||
}
|
||||
|
||||
return p.flatPaths
|
||||
}
|
||||
|
||||
func (p *Path) addSubPaths(n int) {
|
||||
// Use slices.Grow instead of append to reuse the underlying sub path object.
|
||||
p.subPaths = slices.Grow(p.subPaths, n)[:len(p.subPaths)+n]
|
||||
@@ -478,14 +390,6 @@ func (p *Path) Close() {
|
||||
p.subPaths[len(p.subPaths)-1].closed = true
|
||||
}
|
||||
|
||||
func (p *Path) appendFlatPathPointsForLine(pt point) {
|
||||
if len(p.flatPaths) == 0 || p.flatPaths[len(p.flatPaths)-1].closed {
|
||||
p.appendNewFlatPath(pt)
|
||||
return
|
||||
}
|
||||
p.flatPaths[len(p.flatPaths)-1].appendPoint(pt)
|
||||
}
|
||||
|
||||
// lineForTwoPoints returns parameters for a line passing through p0 and p1.
|
||||
func lineForTwoPoints(p0, p1 point) (a, b, c float32) {
|
||||
// Line passing through p0 and p1 in the form of ax + by + c = 0
|
||||
@@ -529,32 +433,6 @@ func crossingPointForTwoLines(p00, p01, p10, p11 point) point {
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Path) appendFlatPathPointsForQuad(p0, p1, p2 point, level int) {
|
||||
if level > 10 {
|
||||
return
|
||||
}
|
||||
|
||||
if isPointCloseToSegment(p1, p0, p2, 0.5) {
|
||||
p.appendFlatPathPointsForLine(p2)
|
||||
return
|
||||
}
|
||||
|
||||
p01 := point{
|
||||
x: (p0.x + p1.x) / 2,
|
||||
y: (p0.y + p1.y) / 2,
|
||||
}
|
||||
p12 := point{
|
||||
x: (p1.x + p2.x) / 2,
|
||||
y: (p1.y + p2.y) / 2,
|
||||
}
|
||||
p012 := point{
|
||||
x: (p01.x + p12.x) / 2,
|
||||
y: (p01.y + p12.y) / 2,
|
||||
}
|
||||
p.appendFlatPathPointsForQuad(p0, p01, p012, level+1)
|
||||
p.appendFlatPathPointsForQuad(p012, p12, p2, level+1)
|
||||
}
|
||||
|
||||
func (p *Path) currentPosition() (point, bool) {
|
||||
if len(p.subPaths) == 0 {
|
||||
return point{}, false
|
||||
@@ -729,54 +607,6 @@ func (p *Path) Arc(x, y, radius, startAngle, endAngle float32, dir Direction) {
|
||||
p.CubicTo(cx0, cy0, cx1, cy1, x1, y1)
|
||||
}
|
||||
|
||||
func (p *Path) closeFlatPath() {
|
||||
if len(p.flatPaths) == 0 {
|
||||
return
|
||||
}
|
||||
p.flatPaths[len(p.flatPaths)-1].close()
|
||||
}
|
||||
|
||||
// AppendVerticesAndIndicesForFilling appends vertices and indices to fill this path and returns them.
|
||||
//
|
||||
// AppendVerticesAndIndicesForFilling works in a similar way to the built-in append function.
|
||||
// If the arguments are nils, AppendVerticesAndIndicesForFilling returns new slices.
|
||||
//
|
||||
// The returned vertice's SrcX and SrcY are 0, and ColorR, ColorG, ColorB, and ColorA are 1.
|
||||
//
|
||||
// The returned values are intended to be passed to DrawTriangles or DrawTrianglesShader with FileRuleNonZero or FillRuleEvenOdd
|
||||
// in order to render a complex polygon like a concave polygon, a polygon with holes, or a self-intersecting polygon.
|
||||
//
|
||||
// The returned vertices and indices should be rendered with a solid (non-transparent) color with the default Blend (source-over).
|
||||
// Otherwise, there is no guarantee about the rendering result.
|
||||
//
|
||||
// Deprecated: as of v2.9. Use [FillPath] instead.
|
||||
func (p *Path) AppendVerticesAndIndicesForFilling(vertices []ebiten.Vertex, indices []uint16) ([]ebiten.Vertex, []uint16) {
|
||||
base := uint16(len(vertices))
|
||||
for _, flatPath := range p.ensureFlatPaths() {
|
||||
if flatPath.pointCount() < 3 {
|
||||
continue
|
||||
}
|
||||
for i, pt := range flatPath.points {
|
||||
vertices = append(vertices, ebiten.Vertex{
|
||||
DstX: pt.x,
|
||||
DstY: pt.y,
|
||||
SrcX: 0,
|
||||
SrcY: 0,
|
||||
ColorR: 1,
|
||||
ColorG: 1,
|
||||
ColorB: 1,
|
||||
ColorA: 1,
|
||||
})
|
||||
if i < 2 {
|
||||
continue
|
||||
}
|
||||
indices = append(indices, base, base+uint16(i-1), base+uint16(i))
|
||||
}
|
||||
base += uint16(flatPath.pointCount())
|
||||
}
|
||||
return vertices, indices
|
||||
}
|
||||
|
||||
// AddPathOptions is options for [Path.AddPath].
|
||||
type AddPathOptions struct {
|
||||
// GeoM is a geometry matrix to apply to the path.
|
||||
@@ -876,228 +706,6 @@ func (p *Path) normalize() {
|
||||
p.subPaths = p.subPaths[:n]
|
||||
}
|
||||
|
||||
// AppendVerticesAndIndicesForStroke appends vertices and indices to render a stroke of this path and returns them.
|
||||
// AppendVerticesAndIndicesForStroke works in a similar way to the built-in append function.
|
||||
// If the arguments are nils, AppendVerticesAndIndicesForStroke returns new slices.
|
||||
//
|
||||
// The returned vertice's SrcX and SrcY are 0, and ColorR, ColorG, ColorB, and ColorA are 1.
|
||||
//
|
||||
// The returned values are intended to be passed to DrawTriangles or DrawTrianglesShader with a solid (non-transparent) color
|
||||
// with FillRuleFillAll or FillRuleNonZero, not FileRuleEvenOdd.
|
||||
//
|
||||
// Deprecated: as of v2.9. Use [StrokePath] or [Path.AddStroke] instead.
|
||||
func (p *Path) AppendVerticesAndIndicesForStroke(vertices []ebiten.Vertex, indices []uint16, op *StrokeOptions) ([]ebiten.Vertex, []uint16) {
|
||||
if op == nil {
|
||||
return vertices, indices
|
||||
}
|
||||
|
||||
var rects [][4]point
|
||||
var tmpPath Path
|
||||
for _, flatPath := range p.ensureFlatPaths() {
|
||||
if flatPath.pointCount() < 2 {
|
||||
continue
|
||||
}
|
||||
|
||||
rects = rects[:0]
|
||||
for i := 0; i < flatPath.pointCount()-1; i++ {
|
||||
pt := flatPath.points[i]
|
||||
|
||||
nextPt := flatPath.points[i+1]
|
||||
dx := nextPt.x - pt.x
|
||||
dy := nextPt.y - pt.y
|
||||
dist := float32(math.Sqrt(float64(dx*dx + dy*dy)))
|
||||
extX := (dy) * op.Width / 2 / dist
|
||||
extY := (-dx) * op.Width / 2 / dist
|
||||
|
||||
rects = append(rects, [4]point{
|
||||
{
|
||||
x: pt.x + extX,
|
||||
y: pt.y + extY,
|
||||
},
|
||||
{
|
||||
x: nextPt.x + extX,
|
||||
y: nextPt.y + extY,
|
||||
},
|
||||
{
|
||||
x: pt.x - extX,
|
||||
y: pt.y - extY,
|
||||
},
|
||||
{
|
||||
x: nextPt.x - extX,
|
||||
y: nextPt.y - extY,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
for i, rect := range rects {
|
||||
idx := uint16(len(vertices))
|
||||
for _, pt := range rect {
|
||||
vertices = append(vertices, ebiten.Vertex{
|
||||
DstX: pt.x,
|
||||
DstY: pt.y,
|
||||
SrcX: 0,
|
||||
SrcY: 0,
|
||||
ColorR: 1,
|
||||
ColorG: 1,
|
||||
ColorB: 1,
|
||||
ColorA: 1,
|
||||
})
|
||||
}
|
||||
// All the triangles are rendered in clockwise order to enable FillRuleNonZero (#2833).
|
||||
indices = append(indices, idx, idx+1, idx+2, idx+1, idx+3, idx+2)
|
||||
|
||||
// Add line joints.
|
||||
var nextRect [4]point
|
||||
if i < len(rects)-1 {
|
||||
nextRect = rects[i+1]
|
||||
} else if flatPath.closed {
|
||||
nextRect = rects[0]
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
|
||||
// c is the center of the 'end' edge of the current rect (= the second point of the segment).
|
||||
c := point{
|
||||
x: (rect[1].x + rect[3].x) / 2,
|
||||
y: (rect[1].y + rect[3].y) / 2,
|
||||
}
|
||||
|
||||
// Note that the Y direction and the angle direction are opposite from math's.
|
||||
a0 := float32(math.Atan2(float64(rect[1].y-c.y), float64(rect[1].x-c.x)))
|
||||
a1 := float32(math.Atan2(float64(nextRect[0].y-c.y), float64(nextRect[0].x-c.x)))
|
||||
da := a1 - a0
|
||||
for da < 0 {
|
||||
da += 2 * math.Pi
|
||||
}
|
||||
if da == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
switch op.LineJoin {
|
||||
case LineJoinMiter:
|
||||
delta := math.Pi - da
|
||||
exceed := float32(math.Abs(1/math.Sin(float64(delta/2)))) > op.MiterLimit
|
||||
|
||||
// Quadrilateral
|
||||
tmpPath.Reset()
|
||||
tmpPath.MoveTo(c.x, c.y)
|
||||
if da < math.Pi {
|
||||
tmpPath.LineTo(rect[1].x, rect[1].y)
|
||||
if !exceed {
|
||||
pt := crossingPointForTwoLines(rect[0], rect[1], nextRect[0], nextRect[1])
|
||||
tmpPath.LineTo(pt.x, pt.y)
|
||||
}
|
||||
tmpPath.LineTo(nextRect[0].x, nextRect[0].y)
|
||||
} else {
|
||||
tmpPath.LineTo(rect[3].x, rect[3].y)
|
||||
if !exceed {
|
||||
pt := crossingPointForTwoLines(rect[2], rect[3], nextRect[2], nextRect[3])
|
||||
tmpPath.LineTo(pt.x, pt.y)
|
||||
}
|
||||
tmpPath.LineTo(nextRect[2].x, nextRect[2].y)
|
||||
}
|
||||
vertices, indices = tmpPath.AppendVerticesAndIndicesForFilling(vertices, indices)
|
||||
|
||||
case LineJoinBevel:
|
||||
// Triangle
|
||||
tmpPath.Reset()
|
||||
tmpPath.MoveTo(c.x, c.y)
|
||||
if da < math.Pi {
|
||||
tmpPath.LineTo(rect[1].x, rect[1].y)
|
||||
tmpPath.LineTo(nextRect[0].x, nextRect[0].y)
|
||||
} else {
|
||||
tmpPath.LineTo(rect[3].x, rect[3].y)
|
||||
tmpPath.LineTo(nextRect[2].x, nextRect[2].y)
|
||||
}
|
||||
vertices, indices = tmpPath.AppendVerticesAndIndicesForFilling(vertices, indices)
|
||||
|
||||
case LineJoinRound:
|
||||
// Arc
|
||||
tmpPath.Reset()
|
||||
tmpPath.MoveTo(c.x, c.y)
|
||||
if da < math.Pi {
|
||||
tmpPath.Arc(c.x, c.y, op.Width/2, a0, a1, Clockwise)
|
||||
} else {
|
||||
tmpPath.Arc(c.x, c.y, op.Width/2, a0+math.Pi, a1+math.Pi, CounterClockwise)
|
||||
}
|
||||
vertices, indices = tmpPath.AppendVerticesAndIndicesForFilling(vertices, indices)
|
||||
}
|
||||
}
|
||||
|
||||
if len(rects) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
// If the flat path is closed, do not render line caps.
|
||||
if flatPath.closed {
|
||||
continue
|
||||
}
|
||||
|
||||
switch op.LineCap {
|
||||
case LineCapButt:
|
||||
// Do nothing.
|
||||
|
||||
case LineCapRound:
|
||||
startR, endR := rects[0], rects[len(rects)-1]
|
||||
{
|
||||
c := point{
|
||||
x: (startR[0].x + startR[2].x) / 2,
|
||||
y: (startR[0].y + startR[2].y) / 2,
|
||||
}
|
||||
a := float32(math.Atan2(float64(startR[0].y-startR[2].y), float64(startR[0].x-startR[2].x)))
|
||||
// Arc
|
||||
tmpPath.Reset()
|
||||
tmpPath.MoveTo(startR[0].x, startR[0].y)
|
||||
tmpPath.Arc(c.x, c.y, op.Width/2, a, a+math.Pi, CounterClockwise)
|
||||
vertices, indices = tmpPath.AppendVerticesAndIndicesForFilling(vertices, indices)
|
||||
}
|
||||
{
|
||||
c := point{
|
||||
x: (endR[1].x + endR[3].x) / 2,
|
||||
y: (endR[1].y + endR[3].y) / 2,
|
||||
}
|
||||
a := float32(math.Atan2(float64(endR[1].y-endR[3].y), float64(endR[1].x-endR[3].x)))
|
||||
// Arc
|
||||
tmpPath.Reset()
|
||||
tmpPath.MoveTo(endR[1].x, endR[1].y)
|
||||
tmpPath.Arc(c.x, c.y, op.Width/2, a, a+math.Pi, Clockwise)
|
||||
vertices, indices = tmpPath.AppendVerticesAndIndicesForFilling(vertices, indices)
|
||||
}
|
||||
|
||||
case LineCapSquare:
|
||||
startR, endR := rects[0], rects[len(rects)-1]
|
||||
{
|
||||
a := math.Atan2(float64(startR[0].y-startR[1].y), float64(startR[0].x-startR[1].x))
|
||||
s, c := math.Sincos(a)
|
||||
dx, dy := float32(c)*op.Width/2, float32(s)*op.Width/2
|
||||
|
||||
// Quadrilateral
|
||||
tmpPath.Reset()
|
||||
tmpPath.MoveTo(startR[0].x, startR[0].y)
|
||||
tmpPath.LineTo(startR[0].x+dx, startR[0].y+dy)
|
||||
tmpPath.LineTo(startR[2].x+dx, startR[2].y+dy)
|
||||
tmpPath.LineTo(startR[2].x, startR[2].y)
|
||||
vertices, indices = tmpPath.AppendVerticesAndIndicesForFilling(vertices, indices)
|
||||
}
|
||||
{
|
||||
a := math.Atan2(float64(endR[1].y-endR[0].y), float64(endR[1].x-endR[0].x))
|
||||
s, c := math.Sincos(a)
|
||||
dx, dy := float32(c)*op.Width/2, float32(s)*op.Width/2
|
||||
|
||||
// Quadrilateral
|
||||
tmpPath.Reset()
|
||||
tmpPath.MoveTo(endR[1].x, endR[1].y)
|
||||
tmpPath.LineTo(endR[1].x+dx, endR[1].y+dy)
|
||||
tmpPath.LineTo(endR[3].x+dx, endR[3].y+dy)
|
||||
tmpPath.LineTo(endR[3].x, endR[3].y)
|
||||
vertices, indices = tmpPath.AppendVerticesAndIndicesForFilling(vertices, indices)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return vertices, indices
|
||||
}
|
||||
|
||||
func floor(x float32) int {
|
||||
return int(math.Floor(float64(x)))
|
||||
}
|
||||
|
||||
@@ -15,7 +15,10 @@
|
||||
package vector_test
|
||||
|
||||
import (
|
||||
"image"
|
||||
"image/color"
|
||||
"runtime"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/hajimehoshi/ebiten/v2"
|
||||
@@ -292,3 +295,101 @@ func TestArcAndGeoM(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Issue #3330
|
||||
func TestFillPathSubImage(t *testing.T) {
|
||||
dst := ebiten.NewImage(16, 16)
|
||||
|
||||
dst2 := dst.SubImage(image.Rect(0, 0, 8, 8)).(*ebiten.Image)
|
||||
var p vector.Path
|
||||
p.MoveTo(0, 0)
|
||||
p.LineTo(8, 0)
|
||||
p.LineTo(8, 8)
|
||||
p.LineTo(0, 8)
|
||||
p.Close()
|
||||
op := &vector.DrawPathOptions{}
|
||||
op.ColorScale.ScaleWithColor(color.White)
|
||||
op.AntiAlias = true
|
||||
vector.FillPath(dst2, &p, nil, op)
|
||||
if got, want := dst.At(5, 5), (color.RGBA{0xff, 0xff, 0xff, 0xff}); got != want {
|
||||
t.Errorf("got: %v, want: %v", got, want)
|
||||
}
|
||||
if got, want := dst2.At(5, 5), (color.RGBA{0xff, 0xff, 0xff, 0xff}); got != want {
|
||||
t.Errorf("got: %v, want: %v", got, want)
|
||||
}
|
||||
|
||||
dst3 := dst2.SubImage(image.Rect(4, 4, 8, 8)).(*ebiten.Image)
|
||||
var p2 vector.Path
|
||||
p2.MoveTo(4, 4)
|
||||
p2.LineTo(8, 4)
|
||||
p2.LineTo(8, 8)
|
||||
p2.LineTo(4, 8)
|
||||
p2.Close()
|
||||
op.ColorScale.Reset()
|
||||
op.ColorScale.ScaleWithColor(color.Black)
|
||||
vector.FillPath(dst3, &p2, nil, op)
|
||||
if got, want := dst.At(5, 5), (color.RGBA{0x00, 0x00, 0x00, 0xff}); got != want {
|
||||
t.Errorf("got: %v, want: %v", got, want)
|
||||
}
|
||||
if got, want := dst2.At(5, 5), (color.RGBA{0x00, 0x00, 0x00, 0xff}); got != want {
|
||||
t.Errorf("got: %v, want: %v", got, want)
|
||||
}
|
||||
if got, want := dst3.At(5, 5), (color.RGBA{0x00, 0x00, 0x00, 0xff}); got != want {
|
||||
t.Errorf("got: %v, want: %v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRaceConditionWithSubImage(t *testing.T) {
|
||||
const w, h = 16, 16
|
||||
src := ebiten.NewImage(w, h)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
for i := range h {
|
||||
for j := range w {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
subImg := src.SubImage(image.Rect(i, j, i+1, j+1)).(*ebiten.Image)
|
||||
var p vector.Path
|
||||
p.MoveTo(0, 0)
|
||||
p.LineTo(w, 0)
|
||||
p.LineTo(w, h)
|
||||
p.LineTo(0, h)
|
||||
p.Close()
|
||||
op := &vector.DrawPathOptions{}
|
||||
op.ColorScale.ScaleWithColor(color.White)
|
||||
op.AntiAlias = true
|
||||
vector.FillPath(subImg, &p, nil, op)
|
||||
dst := ebiten.NewImage(w, h)
|
||||
dst.DrawImage(subImg, nil)
|
||||
wg.Done()
|
||||
}()
|
||||
}
|
||||
}
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
// Issue #3355
|
||||
func TestFillPathSubImageAndImage(t *testing.T) {
|
||||
dst := ebiten.NewImage(200, 200)
|
||||
defer dst.Deallocate()
|
||||
for i := range 100 {
|
||||
var path vector.Path
|
||||
path.LineTo(0, 0)
|
||||
path.LineTo(0, 100)
|
||||
path.LineTo(100, 100)
|
||||
path.LineTo(100, 0)
|
||||
path.LineTo(0, 0)
|
||||
path.Close()
|
||||
drawOp := &vector.DrawPathOptions{}
|
||||
drawOp.ColorScale.ScaleWithColor(color.RGBA{255, 0, 0, 255})
|
||||
subDst := dst.SubImage(image.Rect(0, 0, 100, 100)).(*ebiten.Image)
|
||||
vector.FillPath(subDst, &path, nil, drawOp)
|
||||
drawOp.ColorScale.Reset()
|
||||
drawOp.ColorScale.ScaleWithColor(color.RGBA{0, 255, 0, 255})
|
||||
vector.FillPath(dst, &path, nil, drawOp)
|
||||
|
||||
if got, want := dst.At(50, 50), (color.RGBA{0, 255, 0, 255}); got != want {
|
||||
t.Errorf("%d: got: %v, want: %v", i, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,413 @@
|
||||
// Copyright 2025 The Ebitengine Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package vector
|
||||
|
||||
import (
|
||||
"math"
|
||||
|
||||
"github.com/hajimehoshi/ebiten/v2"
|
||||
)
|
||||
|
||||
// AppendVerticesAndIndicesForFilling appends vertices and indices to fill this path and returns them.
|
||||
//
|
||||
// AppendVerticesAndIndicesForFilling works in a similar way to the built-in append function.
|
||||
// If the arguments are nils, AppendVerticesAndIndicesForFilling returns new slices.
|
||||
//
|
||||
// The returned vertice's SrcX and SrcY are 0, and ColorR, ColorG, ColorB, and ColorA are 1.
|
||||
//
|
||||
// The returned values are intended to be passed to DrawTriangles or DrawTrianglesShader with FileRuleNonZero or FillRuleEvenOdd
|
||||
// in order to render a complex polygon like a concave polygon, a polygon with holes, or a self-intersecting polygon.
|
||||
//
|
||||
// The returned vertices and indices should be rendered with a solid (non-transparent) color with the default Blend (source-over).
|
||||
// Otherwise, there is no guarantee about the rendering result.
|
||||
//
|
||||
// Deprecated: as of v2.9. Use [FillPath] instead.
|
||||
func (p *Path) AppendVerticesAndIndicesForFilling(vertices []ebiten.Vertex, indices []uint16) ([]ebiten.Vertex, []uint16) {
|
||||
base := uint16(len(vertices))
|
||||
for _, flatPath := range p.ensureFlatPaths() {
|
||||
if flatPath.pointCount() < 3 {
|
||||
continue
|
||||
}
|
||||
for i, pt := range flatPath.points {
|
||||
vertices = append(vertices, ebiten.Vertex{
|
||||
DstX: pt.x,
|
||||
DstY: pt.y,
|
||||
SrcX: 0,
|
||||
SrcY: 0,
|
||||
ColorR: 1,
|
||||
ColorG: 1,
|
||||
ColorB: 1,
|
||||
ColorA: 1,
|
||||
})
|
||||
if i < 2 {
|
||||
continue
|
||||
}
|
||||
indices = append(indices, base, base+uint16(i-1), base+uint16(i))
|
||||
}
|
||||
base += uint16(flatPath.pointCount())
|
||||
}
|
||||
return vertices, indices
|
||||
}
|
||||
|
||||
// AppendVerticesAndIndicesForStroke appends vertices and indices to render a stroke of this path and returns them.
|
||||
// AppendVerticesAndIndicesForStroke works in a similar way to the built-in append function.
|
||||
// If the arguments are nils, AppendVerticesAndIndicesForStroke returns new slices.
|
||||
//
|
||||
// The returned vertice's SrcX and SrcY are 0, and ColorR, ColorG, ColorB, and ColorA are 1.
|
||||
//
|
||||
// The returned values are intended to be passed to DrawTriangles or DrawTrianglesShader with a solid (non-transparent) color
|
||||
// with FillRuleFillAll or FillRuleNonZero, not FileRuleEvenOdd.
|
||||
//
|
||||
// Deprecated: as of v2.9. Use [StrokePath] or [Path.AddStroke] instead.
|
||||
func (p *Path) AppendVerticesAndIndicesForStroke(vertices []ebiten.Vertex, indices []uint16, op *StrokeOptions) ([]ebiten.Vertex, []uint16) {
|
||||
if op == nil {
|
||||
return vertices, indices
|
||||
}
|
||||
|
||||
var rects [][4]point
|
||||
var tmpPath Path
|
||||
for _, flatPath := range p.ensureFlatPaths() {
|
||||
if flatPath.pointCount() < 2 {
|
||||
continue
|
||||
}
|
||||
|
||||
rects = rects[:0]
|
||||
for i := 0; i < flatPath.pointCount()-1; i++ {
|
||||
pt := flatPath.points[i]
|
||||
|
||||
nextPt := flatPath.points[i+1]
|
||||
dx := nextPt.x - pt.x
|
||||
dy := nextPt.y - pt.y
|
||||
dist := float32(math.Sqrt(float64(dx*dx + dy*dy)))
|
||||
extX := (dy) * op.Width / 2 / dist
|
||||
extY := (-dx) * op.Width / 2 / dist
|
||||
|
||||
rects = append(rects, [4]point{
|
||||
{
|
||||
x: pt.x + extX,
|
||||
y: pt.y + extY,
|
||||
},
|
||||
{
|
||||
x: nextPt.x + extX,
|
||||
y: nextPt.y + extY,
|
||||
},
|
||||
{
|
||||
x: pt.x - extX,
|
||||
y: pt.y - extY,
|
||||
},
|
||||
{
|
||||
x: nextPt.x - extX,
|
||||
y: nextPt.y - extY,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
for i, rect := range rects {
|
||||
idx := uint16(len(vertices))
|
||||
for _, pt := range rect {
|
||||
vertices = append(vertices, ebiten.Vertex{
|
||||
DstX: pt.x,
|
||||
DstY: pt.y,
|
||||
SrcX: 0,
|
||||
SrcY: 0,
|
||||
ColorR: 1,
|
||||
ColorG: 1,
|
||||
ColorB: 1,
|
||||
ColorA: 1,
|
||||
})
|
||||
}
|
||||
// All the triangles are rendered in clockwise order to enable FillRuleNonZero (#2833).
|
||||
indices = append(indices, idx, idx+1, idx+2, idx+1, idx+3, idx+2)
|
||||
|
||||
// Add line joints.
|
||||
var nextRect [4]point
|
||||
if i < len(rects)-1 {
|
||||
nextRect = rects[i+1]
|
||||
} else if flatPath.closed {
|
||||
nextRect = rects[0]
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
|
||||
// c is the center of the 'end' edge of the current rect (= the second point of the segment).
|
||||
c := point{
|
||||
x: (rect[1].x + rect[3].x) / 2,
|
||||
y: (rect[1].y + rect[3].y) / 2,
|
||||
}
|
||||
|
||||
// Note that the Y direction and the angle direction are opposite from math's.
|
||||
a0 := float32(math.Atan2(float64(rect[1].y-c.y), float64(rect[1].x-c.x)))
|
||||
a1 := float32(math.Atan2(float64(nextRect[0].y-c.y), float64(nextRect[0].x-c.x)))
|
||||
da := a1 - a0
|
||||
for da < 0 {
|
||||
da += 2 * math.Pi
|
||||
}
|
||||
if da == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
switch op.LineJoin {
|
||||
case LineJoinMiter:
|
||||
delta := math.Pi - da
|
||||
exceed := float32(math.Abs(1/math.Sin(float64(delta/2)))) > op.MiterLimit
|
||||
|
||||
// Quadrilateral
|
||||
tmpPath.Reset()
|
||||
tmpPath.MoveTo(c.x, c.y)
|
||||
if da < math.Pi {
|
||||
tmpPath.LineTo(rect[1].x, rect[1].y)
|
||||
if !exceed {
|
||||
pt := crossingPointForTwoLines(rect[0], rect[1], nextRect[0], nextRect[1])
|
||||
tmpPath.LineTo(pt.x, pt.y)
|
||||
}
|
||||
tmpPath.LineTo(nextRect[0].x, nextRect[0].y)
|
||||
} else {
|
||||
tmpPath.LineTo(rect[3].x, rect[3].y)
|
||||
if !exceed {
|
||||
pt := crossingPointForTwoLines(rect[2], rect[3], nextRect[2], nextRect[3])
|
||||
tmpPath.LineTo(pt.x, pt.y)
|
||||
}
|
||||
tmpPath.LineTo(nextRect[2].x, nextRect[2].y)
|
||||
}
|
||||
vertices, indices = tmpPath.AppendVerticesAndIndicesForFilling(vertices, indices)
|
||||
|
||||
case LineJoinBevel:
|
||||
// Triangle
|
||||
tmpPath.Reset()
|
||||
tmpPath.MoveTo(c.x, c.y)
|
||||
if da < math.Pi {
|
||||
tmpPath.LineTo(rect[1].x, rect[1].y)
|
||||
tmpPath.LineTo(nextRect[0].x, nextRect[0].y)
|
||||
} else {
|
||||
tmpPath.LineTo(rect[3].x, rect[3].y)
|
||||
tmpPath.LineTo(nextRect[2].x, nextRect[2].y)
|
||||
}
|
||||
vertices, indices = tmpPath.AppendVerticesAndIndicesForFilling(vertices, indices)
|
||||
|
||||
case LineJoinRound:
|
||||
// Arc
|
||||
tmpPath.Reset()
|
||||
tmpPath.MoveTo(c.x, c.y)
|
||||
if da < math.Pi {
|
||||
tmpPath.Arc(c.x, c.y, op.Width/2, a0, a1, Clockwise)
|
||||
} else {
|
||||
tmpPath.Arc(c.x, c.y, op.Width/2, a0+math.Pi, a1+math.Pi, CounterClockwise)
|
||||
}
|
||||
vertices, indices = tmpPath.AppendVerticesAndIndicesForFilling(vertices, indices)
|
||||
}
|
||||
}
|
||||
|
||||
if len(rects) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
// If the flat path is closed, do not render line caps.
|
||||
if flatPath.closed {
|
||||
continue
|
||||
}
|
||||
|
||||
switch op.LineCap {
|
||||
case LineCapButt:
|
||||
// Do nothing.
|
||||
|
||||
case LineCapRound:
|
||||
startR, endR := rects[0], rects[len(rects)-1]
|
||||
{
|
||||
c := point{
|
||||
x: (startR[0].x + startR[2].x) / 2,
|
||||
y: (startR[0].y + startR[2].y) / 2,
|
||||
}
|
||||
a := float32(math.Atan2(float64(startR[0].y-startR[2].y), float64(startR[0].x-startR[2].x)))
|
||||
// Arc
|
||||
tmpPath.Reset()
|
||||
tmpPath.MoveTo(startR[0].x, startR[0].y)
|
||||
tmpPath.Arc(c.x, c.y, op.Width/2, a, a+math.Pi, CounterClockwise)
|
||||
vertices, indices = tmpPath.AppendVerticesAndIndicesForFilling(vertices, indices)
|
||||
}
|
||||
{
|
||||
c := point{
|
||||
x: (endR[1].x + endR[3].x) / 2,
|
||||
y: (endR[1].y + endR[3].y) / 2,
|
||||
}
|
||||
a := float32(math.Atan2(float64(endR[1].y-endR[3].y), float64(endR[1].x-endR[3].x)))
|
||||
// Arc
|
||||
tmpPath.Reset()
|
||||
tmpPath.MoveTo(endR[1].x, endR[1].y)
|
||||
tmpPath.Arc(c.x, c.y, op.Width/2, a, a+math.Pi, Clockwise)
|
||||
vertices, indices = tmpPath.AppendVerticesAndIndicesForFilling(vertices, indices)
|
||||
}
|
||||
|
||||
case LineCapSquare:
|
||||
startR, endR := rects[0], rects[len(rects)-1]
|
||||
{
|
||||
a := math.Atan2(float64(startR[0].y-startR[1].y), float64(startR[0].x-startR[1].x))
|
||||
s, c := math.Sincos(a)
|
||||
dx, dy := float32(c)*op.Width/2, float32(s)*op.Width/2
|
||||
|
||||
// Quadrilateral
|
||||
tmpPath.Reset()
|
||||
tmpPath.MoveTo(startR[0].x, startR[0].y)
|
||||
tmpPath.LineTo(startR[0].x+dx, startR[0].y+dy)
|
||||
tmpPath.LineTo(startR[2].x+dx, startR[2].y+dy)
|
||||
tmpPath.LineTo(startR[2].x, startR[2].y)
|
||||
vertices, indices = tmpPath.AppendVerticesAndIndicesForFilling(vertices, indices)
|
||||
}
|
||||
{
|
||||
a := math.Atan2(float64(endR[1].y-endR[0].y), float64(endR[1].x-endR[0].x))
|
||||
s, c := math.Sincos(a)
|
||||
dx, dy := float32(c)*op.Width/2, float32(s)*op.Width/2
|
||||
|
||||
// Quadrilateral
|
||||
tmpPath.Reset()
|
||||
tmpPath.MoveTo(endR[1].x, endR[1].y)
|
||||
tmpPath.LineTo(endR[1].x+dx, endR[1].y+dy)
|
||||
tmpPath.LineTo(endR[3].x+dx, endR[3].y+dy)
|
||||
tmpPath.LineTo(endR[3].x, endR[3].y)
|
||||
vertices, indices = tmpPath.AppendVerticesAndIndicesForFilling(vertices, indices)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return vertices, indices
|
||||
}
|
||||
|
||||
// flatPath is a flattened sub-path of a path.
|
||||
// A flatPath consists of points for line segments.
|
||||
type flatPath struct {
|
||||
points []point
|
||||
closed bool
|
||||
}
|
||||
|
||||
// reset resets the flatPath.
|
||||
// reset doesn't release the allocated memory so that the memory can be reused.
|
||||
func (f *flatPath) reset() {
|
||||
f.points = f.points[:0]
|
||||
f.closed = false
|
||||
}
|
||||
|
||||
func (f flatPath) pointCount() int {
|
||||
return len(f.points)
|
||||
}
|
||||
|
||||
func (f flatPath) lastPoint() point {
|
||||
return f.points[len(f.points)-1]
|
||||
}
|
||||
|
||||
func (f *flatPath) appendPoint(pt point) {
|
||||
if f.closed {
|
||||
panic("vector: a closed flatPath cannot append a new point")
|
||||
}
|
||||
|
||||
if len(f.points) > 0 {
|
||||
// Do not add a too close point to the last point.
|
||||
// This can cause unexpected rendering results.
|
||||
if lp := f.lastPoint(); abs(lp.x-pt.x) < 1e-2 && abs(lp.y-pt.y) < 1e-2 {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
f.points = append(f.points, pt)
|
||||
}
|
||||
|
||||
func (f *flatPath) close() {
|
||||
f.closed = true
|
||||
}
|
||||
|
||||
func (p *Path) resetFlatPaths() {
|
||||
for _, fp := range p.flatPaths {
|
||||
fp.reset()
|
||||
}
|
||||
p.flatPaths = p.flatPaths[:0]
|
||||
}
|
||||
|
||||
func (p *Path) ensureFlatPaths() []flatPath {
|
||||
if len(p.flatPaths) > 0 || len(p.subPaths) == 0 {
|
||||
return p.flatPaths
|
||||
}
|
||||
|
||||
for _, subPath := range p.subPaths {
|
||||
p.appendNewFlatPath(subPath.start)
|
||||
cur := subPath.start
|
||||
for _, op := range subPath.ops {
|
||||
switch op.typ {
|
||||
case opTypeLineTo:
|
||||
p.appendFlatPathPointsForLine(op.p1)
|
||||
cur = op.p1
|
||||
case opTypeQuadTo:
|
||||
p.appendFlatPathPointsForQuad(cur, op.p1, op.p2, 0)
|
||||
cur = op.p2
|
||||
}
|
||||
}
|
||||
if subPath.closed {
|
||||
p.closeFlatPath()
|
||||
}
|
||||
}
|
||||
|
||||
return p.flatPaths
|
||||
}
|
||||
|
||||
func (p *Path) appendNewFlatPath(pt point) {
|
||||
if cap(p.flatPaths) > len(p.flatPaths) {
|
||||
// Reuse the last flat path since the last flat path might have an already allocated slice.
|
||||
p.flatPaths = p.flatPaths[:len(p.flatPaths)+1]
|
||||
p.flatPaths[len(p.flatPaths)-1].reset()
|
||||
p.flatPaths[len(p.flatPaths)-1].appendPoint(pt)
|
||||
return
|
||||
}
|
||||
p.flatPaths = append(p.flatPaths, flatPath{
|
||||
points: []point{pt},
|
||||
})
|
||||
}
|
||||
|
||||
func (p *Path) appendFlatPathPointsForLine(pt point) {
|
||||
if len(p.flatPaths) == 0 || p.flatPaths[len(p.flatPaths)-1].closed {
|
||||
p.appendNewFlatPath(pt)
|
||||
return
|
||||
}
|
||||
p.flatPaths[len(p.flatPaths)-1].appendPoint(pt)
|
||||
}
|
||||
|
||||
func (p *Path) appendFlatPathPointsForQuad(p0, p1, p2 point, level int) {
|
||||
if level > 10 {
|
||||
return
|
||||
}
|
||||
|
||||
if isPointCloseToSegment(p1, p0, p2, 0.5) {
|
||||
p.appendFlatPathPointsForLine(p2)
|
||||
return
|
||||
}
|
||||
|
||||
p01 := point{
|
||||
x: (p0.x + p1.x) / 2,
|
||||
y: (p0.y + p1.y) / 2,
|
||||
}
|
||||
p12 := point{
|
||||
x: (p1.x + p2.x) / 2,
|
||||
y: (p1.y + p2.y) / 2,
|
||||
}
|
||||
p012 := point{
|
||||
x: (p01.x + p12.x) / 2,
|
||||
y: (p01.y + p12.y) / 2,
|
||||
}
|
||||
p.appendFlatPathPointsForQuad(p0, p01, p012, level+1)
|
||||
p.appendFlatPathPointsForQuad(p012, p12, p2, level+1)
|
||||
}
|
||||
|
||||
func (p *Path) closeFlatPath() {
|
||||
if len(p.flatPaths) == 0 {
|
||||
return
|
||||
}
|
||||
p.flatPaths[len(p.flatPaths)-1].close()
|
||||
}
|
||||
-120
@@ -19,7 +19,6 @@ import (
|
||||
"image/color"
|
||||
"math"
|
||||
"sync"
|
||||
_ "unsafe"
|
||||
|
||||
"github.com/hajimehoshi/ebiten/v2"
|
||||
)
|
||||
@@ -294,116 +293,6 @@ func StrokeCircle(dst *ebiten.Image, cx, cy, r float32, strokeWidth float32, clr
|
||||
})
|
||||
}
|
||||
|
||||
// FillRule is the rule whether an overlapped region is rendered or not.
|
||||
type FillRule int
|
||||
|
||||
const (
|
||||
// FillRuleNonZero means that triangles are rendered based on the non-zero rule.
|
||||
// If and only if the number of overlaps is not 0, the region is rendered.
|
||||
FillRuleNonZero FillRule = iota
|
||||
|
||||
// FillRuleEvenOdd means that triangles are rendered based on the even-odd rule.
|
||||
// If and only if the number of overlaps is odd, the region is rendered.
|
||||
FillRuleEvenOdd
|
||||
)
|
||||
|
||||
var (
|
||||
theCallbackTokens = map[*ebiten.Image]int64{}
|
||||
theFillPathsStates = map[*ebiten.Image]*fillPathsState{}
|
||||
theFillPathsStatesPool = sync.Pool{
|
||||
New: func() any {
|
||||
return &fillPathsState{}
|
||||
},
|
||||
}
|
||||
theFillPathM sync.Mutex
|
||||
)
|
||||
|
||||
// FillOptions is options to fill a path.
|
||||
type FillOptions struct {
|
||||
// FillRule is the rule whether an overlapped region is rendered or not.
|
||||
// The default (zero) value is FillRuleNonZero.
|
||||
FillRule FillRule
|
||||
}
|
||||
|
||||
// DrawPathOptions is options to draw a path.
|
||||
type DrawPathOptions struct {
|
||||
// AntiAlias is whether the path is drawn with anti-aliasing.
|
||||
// The default (zero) value is false.
|
||||
AntiAlias bool
|
||||
|
||||
// ColorScale is the color scale to apply to the path.
|
||||
// The default (zero) value is identity, which is (1, 1, 1, 1) (white).
|
||||
ColorScale ebiten.ColorScale
|
||||
|
||||
// Blend is the blend mode to apply to the path.
|
||||
// The default (zero) value is ebiten.BlendSourceOver.
|
||||
Blend ebiten.Blend
|
||||
}
|
||||
|
||||
// FillPath fills the specified path with the specified options.
|
||||
func FillPath(dst *ebiten.Image, path *Path, fillOptions *FillOptions, drawPathOptions *DrawPathOptions) {
|
||||
if drawPathOptions == nil {
|
||||
drawPathOptions = &DrawPathOptions{}
|
||||
}
|
||||
if fillOptions == nil {
|
||||
fillOptions = &FillOptions{}
|
||||
}
|
||||
|
||||
bounds := dst.Bounds()
|
||||
|
||||
// Get the original image if dst is a sub-image to integrate the callbacks.
|
||||
dst = originalImage(dst)
|
||||
|
||||
theFillPathM.Lock()
|
||||
defer theFillPathM.Unlock()
|
||||
|
||||
// Remove the previous registered callbacks.
|
||||
if token, ok := theCallbackTokens[dst]; ok {
|
||||
removeUsageCallback(dst, token)
|
||||
}
|
||||
delete(theCallbackTokens, dst)
|
||||
|
||||
if _, ok := theFillPathsStates[dst]; !ok {
|
||||
theFillPathsStates[dst] = theFillPathsStatesPool.Get().(*fillPathsState)
|
||||
}
|
||||
s := theFillPathsStates[dst]
|
||||
if s.antialias != drawPathOptions.AntiAlias || s.blend != drawPathOptions.Blend || s.fillRule != fillOptions.FillRule {
|
||||
s.fillPaths(dst)
|
||||
s.reset()
|
||||
}
|
||||
s.antialias = drawPathOptions.AntiAlias
|
||||
s.blend = drawPathOptions.Blend
|
||||
s.fillRule = fillOptions.FillRule
|
||||
s.addPath(path, bounds, drawPathOptions.ColorScale)
|
||||
|
||||
// Use an independent callback function to avoid unexpected captures.
|
||||
theCallbackTokens[dst] = addUsageCallback(dst, fillPathCallback)
|
||||
}
|
||||
|
||||
func fillPathCallback(dst *ebiten.Image) {
|
||||
if originalImage(dst) != dst {
|
||||
panic("vector: dst must be the original image")
|
||||
}
|
||||
|
||||
theFillPathM.Lock()
|
||||
defer theFillPathM.Unlock()
|
||||
|
||||
// Remove the callback not to call this twice.
|
||||
if token, ok := theCallbackTokens[dst]; ok {
|
||||
removeUsageCallback(dst, token)
|
||||
}
|
||||
delete(theCallbackTokens, dst)
|
||||
|
||||
s, ok := theFillPathsStates[dst]
|
||||
if !ok {
|
||||
panic("vector: fillPathsState must exist here")
|
||||
}
|
||||
s.fillPaths(dst)
|
||||
s.reset()
|
||||
delete(theFillPathsStates, dst)
|
||||
theFillPathsStatesPool.Put(s)
|
||||
}
|
||||
|
||||
// StrokePath strokes the specified path with the specified options.
|
||||
func StrokePath(dst *ebiten.Image, path *Path, strokeOptions *StrokeOptions, drawPathOptions *DrawPathOptions) {
|
||||
var stroke Path
|
||||
@@ -412,12 +301,3 @@ func StrokePath(dst *ebiten.Image, path *Path, strokeOptions *StrokeOptions, dra
|
||||
stroke.AddStroke(path, op)
|
||||
FillPath(dst, &stroke, nil, drawPathOptions)
|
||||
}
|
||||
|
||||
//go:linkname originalImage github.com/hajimehoshi/ebiten/v2.originalImage
|
||||
func originalImage(img *ebiten.Image) *ebiten.Image
|
||||
|
||||
//go:linkname addUsageCallback github.com/hajimehoshi/ebiten/v2.addUsageCallback
|
||||
func addUsageCallback(img *ebiten.Image, fn func(img *ebiten.Image)) int64
|
||||
|
||||
//go:linkname removeUsageCallback github.com/hajimehoshi/ebiten/v2.removeUsageCallback
|
||||
func removeUsageCallback(img *ebiten.Image, token int64)
|
||||
|
||||
@@ -17,7 +17,6 @@ package vector_test
|
||||
import (
|
||||
"image"
|
||||
"image/color"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/hajimehoshi/ebiten/v2"
|
||||
@@ -99,104 +98,6 @@ func TestFillCircleSubImage(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// Issue #3330
|
||||
func TestFillPathSubImage(t *testing.T) {
|
||||
dst := ebiten.NewImage(16, 16)
|
||||
|
||||
dst2 := dst.SubImage(image.Rect(0, 0, 8, 8)).(*ebiten.Image)
|
||||
var p vector.Path
|
||||
p.MoveTo(0, 0)
|
||||
p.LineTo(8, 0)
|
||||
p.LineTo(8, 8)
|
||||
p.LineTo(0, 8)
|
||||
p.Close()
|
||||
op := &vector.DrawPathOptions{}
|
||||
op.ColorScale.ScaleWithColor(color.White)
|
||||
op.AntiAlias = true
|
||||
vector.FillPath(dst2, &p, nil, op)
|
||||
if got, want := dst.At(5, 5), (color.RGBA{0xff, 0xff, 0xff, 0xff}); got != want {
|
||||
t.Errorf("got: %v, want: %v", got, want)
|
||||
}
|
||||
if got, want := dst2.At(5, 5), (color.RGBA{0xff, 0xff, 0xff, 0xff}); got != want {
|
||||
t.Errorf("got: %v, want: %v", got, want)
|
||||
}
|
||||
|
||||
dst3 := dst2.SubImage(image.Rect(4, 4, 8, 8)).(*ebiten.Image)
|
||||
var p2 vector.Path
|
||||
p2.MoveTo(4, 4)
|
||||
p2.LineTo(8, 4)
|
||||
p2.LineTo(8, 8)
|
||||
p2.LineTo(4, 8)
|
||||
p2.Close()
|
||||
op.ColorScale.Reset()
|
||||
op.ColorScale.ScaleWithColor(color.Black)
|
||||
vector.FillPath(dst3, &p2, nil, op)
|
||||
if got, want := dst.At(5, 5), (color.RGBA{0x00, 0x00, 0x00, 0xff}); got != want {
|
||||
t.Errorf("got: %v, want: %v", got, want)
|
||||
}
|
||||
if got, want := dst2.At(5, 5), (color.RGBA{0x00, 0x00, 0x00, 0xff}); got != want {
|
||||
t.Errorf("got: %v, want: %v", got, want)
|
||||
}
|
||||
if got, want := dst3.At(5, 5), (color.RGBA{0x00, 0x00, 0x00, 0xff}); got != want {
|
||||
t.Errorf("got: %v, want: %v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRaceConditionWithSubImage(t *testing.T) {
|
||||
const w, h = 16, 16
|
||||
src := ebiten.NewImage(w, h)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
for i := range h {
|
||||
for j := range w {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
subImg := src.SubImage(image.Rect(i, j, i+1, j+1)).(*ebiten.Image)
|
||||
var p vector.Path
|
||||
p.MoveTo(0, 0)
|
||||
p.LineTo(w, 0)
|
||||
p.LineTo(w, h)
|
||||
p.LineTo(0, h)
|
||||
p.Close()
|
||||
op := &vector.DrawPathOptions{}
|
||||
op.ColorScale.ScaleWithColor(color.White)
|
||||
op.AntiAlias = true
|
||||
vector.FillPath(subImg, &p, nil, op)
|
||||
dst := ebiten.NewImage(w, h)
|
||||
dst.DrawImage(subImg, nil)
|
||||
wg.Done()
|
||||
}()
|
||||
}
|
||||
}
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
// Issue #3355
|
||||
func TestFillPathSubImageAndImage(t *testing.T) {
|
||||
dst := ebiten.NewImage(200, 200)
|
||||
defer dst.Deallocate()
|
||||
for i := range 100 {
|
||||
var path vector.Path
|
||||
path.LineTo(0, 0)
|
||||
path.LineTo(0, 100)
|
||||
path.LineTo(100, 100)
|
||||
path.LineTo(100, 0)
|
||||
path.LineTo(0, 0)
|
||||
path.Close()
|
||||
drawOp := &vector.DrawPathOptions{}
|
||||
drawOp.ColorScale.ScaleWithColor(color.RGBA{255, 0, 0, 255})
|
||||
subDst := dst.SubImage(image.Rect(0, 0, 100, 100)).(*ebiten.Image)
|
||||
vector.FillPath(subDst, &path, nil, drawOp)
|
||||
drawOp.ColorScale.Reset()
|
||||
drawOp.ColorScale.ScaleWithColor(color.RGBA{0, 255, 0, 255})
|
||||
vector.FillPath(dst, &path, nil, drawOp)
|
||||
|
||||
if got, want := dst.At(50, 50), (color.RGBA{0, 255, 0, 255}); got != want {
|
||||
t.Errorf("%d: got: %v, want: %v", i, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Issue #3357
|
||||
func TestFillRects(t *testing.T) {
|
||||
dsts := []*ebiten.Image{
|
||||
|
||||
Reference in New Issue
Block a user