vector: refactoring: organize files

This commit is contained in:
Hajime Hoshi
2025-12-27 15:30:15 +09:00
parent f17459ca94
commit d12fd25017
6 changed files with 635 additions and 611 deletions
+121
View File
@@ -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
View File
@@ -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)))
}
+101
View File
@@ -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)
}
}
}
+413
View File
@@ -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
View File
@@ -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)
-99
View File
@@ -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{