updated ebiten version from 2.7.9 to 2.9.9
This commit is contained in:
+204
@@ -0,0 +1,204 @@
|
||||
// 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 (
|
||||
"image"
|
||||
"math"
|
||||
"slices"
|
||||
|
||||
"github.com/hajimehoshi/ebiten/v2"
|
||||
)
|
||||
|
||||
type atlasRegion struct {
|
||||
pathIndex int
|
||||
imageIndex int
|
||||
imageBounds image.Rectangle
|
||||
}
|
||||
|
||||
type atlas struct {
|
||||
pathRenderingBounds []image.Rectangle
|
||||
atlasRegions []atlasRegion
|
||||
pathIndexToAtlasRegionIndex map[int]int
|
||||
atlasSizes []image.Point
|
||||
atlasImages []*ebiten.Image
|
||||
}
|
||||
|
||||
func roundUpAtlasSize(size int) int {
|
||||
if size < 16 {
|
||||
return 16
|
||||
}
|
||||
return int(math.Ceil(math.Pow(1.5, math.Ceil(math.Log(float64(size))/math.Log(1.5)))))
|
||||
}
|
||||
|
||||
func roundUp16(x int) int {
|
||||
return (x + 15) &^ 15
|
||||
}
|
||||
|
||||
func (a *atlas) setPaths(dstBounds image.Rectangle, paths []*Path, antialias bool) {
|
||||
// Reset the members.
|
||||
a.pathRenderingBounds = slices.Delete(a.pathRenderingBounds, 0, len(a.pathRenderingBounds))
|
||||
a.atlasRegions = slices.Delete(a.atlasRegions, 0, len(a.atlasRegions))
|
||||
clear(a.pathIndexToAtlasRegionIndex)
|
||||
a.atlasSizes = slices.Delete(a.atlasSizes, 0, len(a.atlasSizes))
|
||||
|
||||
if len(paths) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
a.pathRenderingBounds = slices.Grow(a.pathRenderingBounds, len(paths))[:len(paths)]
|
||||
for i, p := range paths {
|
||||
b := p.Bounds().Intersect(dstBounds)
|
||||
// Round up the size to 16px in order to encourage reusing sub image cache.
|
||||
a.pathRenderingBounds[i] = image.Rectangle{
|
||||
Min: b.Min,
|
||||
Max: b.Min.Add(image.Pt(roundUp16(b.Dx()), roundUp16(b.Dy()))),
|
||||
}
|
||||
a.atlasRegions = append(a.atlasRegions, atlasRegion{
|
||||
pathIndex: i,
|
||||
})
|
||||
}
|
||||
|
||||
slices.SortFunc(a.atlasRegions, func(ra, rb atlasRegion) int {
|
||||
ba := a.pathRenderingBounds[ra.pathIndex]
|
||||
bb := a.pathRenderingBounds[rb.pathIndex]
|
||||
if ba.Dy() != bb.Dy() {
|
||||
return bb.Dy() - ba.Dy()
|
||||
}
|
||||
if ba.Dx() != bb.Dx() {
|
||||
return ba.Dx() - bb.Dx()
|
||||
}
|
||||
return ra.pathIndex - rb.pathIndex
|
||||
})
|
||||
|
||||
if a.pathIndexToAtlasRegionIndex == nil {
|
||||
a.pathIndexToAtlasRegionIndex = make(map[int]int, len(a.atlasRegions))
|
||||
}
|
||||
for i, r := range a.atlasRegions {
|
||||
a.pathIndexToAtlasRegionIndex[r.pathIndex] = i
|
||||
}
|
||||
|
||||
w, h := dstBounds.Dx(), dstBounds.Dy()
|
||||
// For antialiasing, doubled regions in the X direction are used.
|
||||
if antialias {
|
||||
w *= 2
|
||||
}
|
||||
// Use 2^n - 1, as a region in internal/atlas has 1px padding.
|
||||
maxImageSize := max(4093, w, h)
|
||||
|
||||
// Pack the regions into an atlas with a very simple algorithm:
|
||||
// Order the regions by height and then place them in a row.
|
||||
var atlasImageCount int
|
||||
{
|
||||
a.atlasSizes = append(a.atlasSizes, image.Point{})
|
||||
|
||||
var atlasImageIndex int
|
||||
var currentRowHeight int
|
||||
var currentPosition image.Point
|
||||
for i := range a.atlasRegions {
|
||||
pb := a.pathRenderingBounds[a.atlasRegions[i].pathIndex]
|
||||
// TODO: What if s already exceeds maxImageSize (#3357)?
|
||||
s := pb.Size()
|
||||
// An additional image for antialiasing must be on the same atlas,
|
||||
// so extend the width and use it as a sub image.
|
||||
if antialias {
|
||||
s.X *= 2
|
||||
}
|
||||
if i == 0 {
|
||||
currentRowHeight = s.Y
|
||||
} else if currentPosition.X+s.X > maxImageSize {
|
||||
// Try the next row.
|
||||
currentPosition.X = 0
|
||||
currentPosition.Y += currentRowHeight
|
||||
if currentPosition.Y+s.Y > maxImageSize {
|
||||
atlasImageIndex++
|
||||
a.atlasSizes = append(a.atlasSizes, image.Point{})
|
||||
currentPosition.Y = 0
|
||||
currentRowHeight = s.Y
|
||||
} else {
|
||||
currentRowHeight = max(currentRowHeight, s.Y)
|
||||
}
|
||||
}
|
||||
a.atlasRegions[i].imageIndex = atlasImageIndex
|
||||
a.atlasRegions[i].imageBounds = image.Rectangle{
|
||||
Min: currentPosition,
|
||||
Max: currentPosition.Add(s),
|
||||
}
|
||||
a.atlasSizes[atlasImageIndex] = image.Point{
|
||||
X: max(a.atlasSizes[atlasImageIndex].X, a.atlasRegions[i].imageBounds.Max.X),
|
||||
Y: max(a.atlasSizes[atlasImageIndex].Y, a.atlasRegions[i].imageBounds.Max.Y),
|
||||
}
|
||||
currentPosition.X += s.X
|
||||
}
|
||||
atlasImageCount = atlasImageIndex + 1
|
||||
}
|
||||
|
||||
a.atlasImages = slices.Grow(a.atlasImages, atlasImageCount)[:atlasImageCount]
|
||||
for i := range a.atlasImages {
|
||||
s := a.atlasSizes[i]
|
||||
var origWidth, origHeight int
|
||||
if a.atlasImages[i] != nil {
|
||||
origWidth = a.atlasImages[i].Bounds().Dx()
|
||||
origHeight = a.atlasImages[i].Bounds().Dy()
|
||||
if origWidth < s.X || origHeight < s.Y {
|
||||
a.atlasImages[i].Deallocate()
|
||||
a.atlasImages[i] = nil
|
||||
}
|
||||
}
|
||||
if a.atlasImages[i] != nil {
|
||||
a.atlasImages[i].Clear()
|
||||
} else {
|
||||
// Extend the bounds a little bit by roundUpAtlasSize to avoid creating an image too often.
|
||||
w := min(maxImageSize, max(roundUpAtlasSize(s.X), origWidth))
|
||||
h := min(maxImageSize, max(roundUpAtlasSize(s.Y), origHeight))
|
||||
a.atlasImages[i] = ebiten.NewImage(w, h)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (a *atlas) stencilBufferImageAt(i int, antialias bool, antialiasIndex int) *ebiten.Image {
|
||||
idx, ok := a.pathIndexToAtlasRegionIndex[i]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
ar := a.atlasRegions[idx]
|
||||
if ar.imageBounds.Empty() {
|
||||
return nil
|
||||
}
|
||||
|
||||
atlas := a.atlasImages[ar.imageIndex]
|
||||
b := ar.imageBounds
|
||||
if antialias {
|
||||
switch antialiasIndex {
|
||||
case 0:
|
||||
b = image.Rectangle{
|
||||
Min: b.Min,
|
||||
Max: image.Pt(b.Min.X+b.Dx()/2, b.Max.Y),
|
||||
}
|
||||
case 1:
|
||||
b = image.Rectangle{
|
||||
Min: image.Pt(b.Min.X+b.Dx()/2, b.Min.Y),
|
||||
Max: b.Max,
|
||||
}
|
||||
default:
|
||||
panic("not reached")
|
||||
}
|
||||
}
|
||||
return atlas.SubImage(b).(*ebiten.Image)
|
||||
}
|
||||
|
||||
func (a *atlas) pathRenderingPositionAt(i int) image.Point {
|
||||
return a.pathRenderingBounds[i].Min
|
||||
}
|
||||
+494
@@ -0,0 +1,494 @@
|
||||
// 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 (
|
||||
"fmt"
|
||||
"image"
|
||||
"slices"
|
||||
|
||||
"github.com/hajimehoshi/ebiten/v2"
|
||||
)
|
||||
|
||||
type offsetAndColor struct {
|
||||
offsetX float32
|
||||
offsetY float32
|
||||
colorR float32
|
||||
colorG float32
|
||||
colorB float32
|
||||
colorA float32
|
||||
imageIndex int
|
||||
}
|
||||
|
||||
var (
|
||||
offsetAndColorsNonAA = []offsetAndColor{
|
||||
{
|
||||
offsetX: 0,
|
||||
offsetY: 0,
|
||||
colorR: 1,
|
||||
colorG: 0,
|
||||
colorB: 0,
|
||||
colorA: 0,
|
||||
},
|
||||
}
|
||||
|
||||
// https://learn.microsoft.com/en-us/windows/win32/api/d3d11/ne-d3d11-d3d11_standard_multisample_quality_levels
|
||||
offsetAndColorsAA = []offsetAndColor{
|
||||
{
|
||||
offsetX: 1.0 / 16.0,
|
||||
offsetY: -3.0 / 16.0,
|
||||
colorR: 1,
|
||||
colorG: 0,
|
||||
colorB: 0,
|
||||
colorA: 0,
|
||||
imageIndex: 0,
|
||||
},
|
||||
{
|
||||
offsetX: -1.0 / 16.0,
|
||||
offsetY: 3.0 / 16.0,
|
||||
colorR: 0,
|
||||
colorG: 1,
|
||||
colorB: 0,
|
||||
colorA: 0,
|
||||
imageIndex: 0,
|
||||
},
|
||||
{
|
||||
offsetX: 5.0 / 16.0,
|
||||
offsetY: 1.0 / 16.0,
|
||||
colorR: 0,
|
||||
colorG: 0,
|
||||
colorB: 1,
|
||||
colorA: 0,
|
||||
imageIndex: 0,
|
||||
},
|
||||
{
|
||||
offsetX: -3.0 / 16.0,
|
||||
offsetY: -5.0 / 16.0,
|
||||
colorR: 0,
|
||||
colorG: 0,
|
||||
colorB: 0,
|
||||
colorA: 1,
|
||||
imageIndex: 0,
|
||||
},
|
||||
{
|
||||
offsetX: -5.0 / 16.0,
|
||||
offsetY: 5.0 / 16.0,
|
||||
colorR: 1,
|
||||
colorG: 0,
|
||||
colorB: 0,
|
||||
colorA: 0,
|
||||
imageIndex: 1,
|
||||
},
|
||||
{
|
||||
offsetX: -7.0 / 16.0,
|
||||
offsetY: -1.0 / 16.0,
|
||||
colorR: 0,
|
||||
colorG: 1,
|
||||
colorB: 0,
|
||||
colorA: 0,
|
||||
imageIndex: 1,
|
||||
},
|
||||
{
|
||||
offsetX: 3.0 / 16.0,
|
||||
offsetY: 7.0 / 16.0,
|
||||
colorR: 0,
|
||||
colorG: 0,
|
||||
colorB: 1,
|
||||
colorA: 0,
|
||||
imageIndex: 1,
|
||||
},
|
||||
{
|
||||
offsetX: 7.0 / 16.0,
|
||||
offsetY: -7.0 / 16.0,
|
||||
colorR: 0,
|
||||
colorG: 0,
|
||||
colorB: 0,
|
||||
colorA: 1,
|
||||
imageIndex: 1,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
// theAtlas manages the atlas for stencil buffer images.
|
||||
// theAtlas is a singleton to avoid unnecessary texture allocations.
|
||||
//
|
||||
// theAtlas methods are used only at fillPathsState.fillPaths, and should be protected by theFillPathM.
|
||||
var theAtlas atlas
|
||||
|
||||
type fillPathsState struct {
|
||||
paths []*Path
|
||||
colors []ebiten.ColorScale
|
||||
bounds []image.Rectangle
|
||||
|
||||
vertices []ebiten.Vertex
|
||||
indices []uint32
|
||||
|
||||
antialias bool
|
||||
blend ebiten.Blend
|
||||
fillRule FillRule
|
||||
}
|
||||
|
||||
func (f *fillPathsState) reset() {
|
||||
for _, p := range f.paths {
|
||||
p.Reset()
|
||||
}
|
||||
f.paths = f.paths[:0]
|
||||
f.bounds = f.bounds[:0]
|
||||
f.colors = slices.Delete(f.colors, 0, len(f.colors))
|
||||
}
|
||||
|
||||
func (f *fillPathsState) addPath(path *Path, bounds image.Rectangle, clr ebiten.ColorScale) {
|
||||
if path == nil {
|
||||
return
|
||||
}
|
||||
|
||||
f.paths = slices.Grow(f.paths, 1)[:len(f.paths)+1]
|
||||
if f.paths[len(f.paths)-1] == nil {
|
||||
f.paths[len(f.paths)-1] = &Path{}
|
||||
}
|
||||
dst := f.paths[len(f.paths)-1]
|
||||
dst.addSubPaths(len(path.subPaths))
|
||||
for i, subPath := range path.subPaths {
|
||||
dst.subPaths[i].start = subPath.start
|
||||
dst.subPaths[i].closed = subPath.closed
|
||||
dst.subPaths[i].ops = slices.Grow(dst.subPaths[i].ops, len(subPath.ops))[:len(subPath.ops)]
|
||||
copy(dst.subPaths[i].ops, subPath.ops)
|
||||
}
|
||||
f.bounds = append(f.bounds, bounds)
|
||||
f.colors = append(f.colors, clr)
|
||||
}
|
||||
|
||||
// fillPaths fills the specified path with the specified color.
|
||||
//
|
||||
// fillPaths callers must be protected by theFillPathM.
|
||||
func (f *fillPathsState) fillPaths(dst *ebiten.Image) {
|
||||
if len(f.paths) != len(f.colors) {
|
||||
panic("vector: the number of paths and colors must be the same")
|
||||
}
|
||||
|
||||
vs := f.vertices[:0]
|
||||
is := f.indices[:0]
|
||||
defer func() {
|
||||
f.vertices = vs
|
||||
f.indices = is
|
||||
}()
|
||||
|
||||
theAtlas.setPaths(dst.Bounds(), f.paths, f.antialias)
|
||||
|
||||
offsetAndColors := offsetAndColorsNonAA
|
||||
if f.antialias {
|
||||
offsetAndColors = offsetAndColorsAA
|
||||
}
|
||||
|
||||
// First, render the polygons roughly.
|
||||
for i, path := range f.paths {
|
||||
if path == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, oac := range offsetAndColors {
|
||||
vs = vs[:0]
|
||||
is = is[:0]
|
||||
|
||||
stencilBufferImage := theAtlas.stencilBufferImageAt(i, f.antialias, oac.imageIndex)
|
||||
if stencilBufferImage == nil {
|
||||
continue
|
||||
}
|
||||
pp := theAtlas.pathRenderingPositionAt(i)
|
||||
dstOffsetX := float32(-pp.X + stencilBufferImage.Bounds().Min.X - max(0, dst.Bounds().Min.X-pp.X))
|
||||
dstOffsetY := float32(-pp.Y + stencilBufferImage.Bounds().Min.Y - max(0, dst.Bounds().Min.Y-pp.Y))
|
||||
|
||||
for i := range path.subPaths {
|
||||
subPath := &path.subPaths[i]
|
||||
if !subPath.isValid() {
|
||||
continue
|
||||
}
|
||||
|
||||
// Add an origin point. Any position works in theory.
|
||||
// Use the sub-path's start point. Using one of the sub-path's points can reduce triangles.
|
||||
// Also, this point should be close to the other points and then triangle overlaps are reduced.
|
||||
// TODO: Use a better position like the center of the sub-path.
|
||||
originIdx := uint32(len(vs))
|
||||
cur := subPath.start
|
||||
vs = append(vs, ebiten.Vertex{
|
||||
DstX: cur.x + oac.offsetX + dstOffsetX,
|
||||
DstY: cur.y + oac.offsetY + dstOffsetY,
|
||||
ColorR: oac.colorR,
|
||||
ColorG: oac.colorG,
|
||||
ColorB: oac.colorB,
|
||||
ColorA: oac.colorA,
|
||||
})
|
||||
|
||||
for _, op := range subPath.ops {
|
||||
switch op.typ {
|
||||
case opTypeLineTo:
|
||||
idx := uint32(len(vs))
|
||||
vs = append(vs,
|
||||
ebiten.Vertex{
|
||||
DstX: cur.x + oac.offsetX + dstOffsetX,
|
||||
DstY: cur.y + oac.offsetY + dstOffsetY,
|
||||
ColorR: oac.colorR,
|
||||
ColorG: oac.colorG,
|
||||
ColorB: oac.colorB,
|
||||
ColorA: oac.colorA,
|
||||
},
|
||||
ebiten.Vertex{
|
||||
DstX: op.p1.x + oac.offsetX + dstOffsetX,
|
||||
DstY: op.p1.y + oac.offsetY + dstOffsetY,
|
||||
ColorR: oac.colorR,
|
||||
ColorG: oac.colorG,
|
||||
ColorB: oac.colorB,
|
||||
ColorA: oac.colorA,
|
||||
})
|
||||
is = append(is, idx, originIdx, idx+1)
|
||||
cur = op.p1
|
||||
case opTypeQuadTo:
|
||||
idx := uint32(len(vs))
|
||||
vs = append(vs,
|
||||
ebiten.Vertex{
|
||||
DstX: cur.x + oac.offsetX + dstOffsetX,
|
||||
DstY: cur.y + oac.offsetY + dstOffsetY,
|
||||
ColorR: oac.colorR,
|
||||
ColorG: oac.colorG,
|
||||
ColorB: oac.colorB,
|
||||
ColorA: oac.colorA,
|
||||
},
|
||||
ebiten.Vertex{
|
||||
DstX: op.p2.x + oac.offsetX + dstOffsetX,
|
||||
DstY: op.p2.y + oac.offsetY + dstOffsetY,
|
||||
ColorR: oac.colorR,
|
||||
ColorG: oac.colorG,
|
||||
ColorB: oac.colorB,
|
||||
ColorA: oac.colorA,
|
||||
})
|
||||
is = append(is, idx, originIdx, idx+1)
|
||||
cur = op.p2
|
||||
}
|
||||
}
|
||||
// If the sub-path is not closed, add a supplementary line.
|
||||
if !subPath.closed {
|
||||
idx := uint32(len(vs))
|
||||
vs = append(vs,
|
||||
ebiten.Vertex{
|
||||
DstX: cur.x + oac.offsetX + dstOffsetX,
|
||||
DstY: cur.y + oac.offsetY + dstOffsetY,
|
||||
ColorR: oac.colorR,
|
||||
ColorG: oac.colorG,
|
||||
ColorB: oac.colorB,
|
||||
ColorA: oac.colorA,
|
||||
},
|
||||
ebiten.Vertex{
|
||||
DstX: subPath.start.x + oac.offsetX + dstOffsetX,
|
||||
DstY: subPath.start.y + oac.offsetY + dstOffsetY,
|
||||
ColorR: oac.colorR,
|
||||
ColorG: oac.colorG,
|
||||
ColorB: oac.colorB,
|
||||
ColorA: oac.colorA,
|
||||
})
|
||||
is = append(is, idx, originIdx, idx+1)
|
||||
}
|
||||
}
|
||||
op := &ebiten.DrawTrianglesShaderOptions{}
|
||||
op.Blend = ebiten.BlendLighter
|
||||
shader, err := ensureStencilBufferShaders()
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("vector: failed to create stencil buffer shader: %v", err))
|
||||
}
|
||||
stencilBufferImage.DrawTrianglesShader32(vs, is, shader, op)
|
||||
}
|
||||
}
|
||||
|
||||
// Second, render the bezier curves.
|
||||
for i, path := range f.paths {
|
||||
if path == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, oac := range offsetAndColors {
|
||||
vs = vs[:0]
|
||||
is = is[:0]
|
||||
|
||||
stencilBufferImage := theAtlas.stencilBufferImageAt(i, f.antialias, oac.imageIndex)
|
||||
if stencilBufferImage == nil {
|
||||
continue
|
||||
}
|
||||
pp := theAtlas.pathRenderingPositionAt(i)
|
||||
dstOffsetX := float32(-pp.X + stencilBufferImage.Bounds().Min.X - max(0, dst.Bounds().Min.X-pp.X))
|
||||
dstOffsetY := float32(-pp.Y + stencilBufferImage.Bounds().Min.Y - max(0, dst.Bounds().Min.Y-pp.Y))
|
||||
for i := range path.subPaths {
|
||||
subPath := &path.subPaths[i]
|
||||
if !subPath.isValid() {
|
||||
continue
|
||||
}
|
||||
|
||||
cur := subPath.start
|
||||
for _, op := range subPath.ops {
|
||||
switch op.typ {
|
||||
case opTypeLineTo:
|
||||
cur = op.p1
|
||||
case opTypeQuadTo:
|
||||
idx := uint32(len(vs))
|
||||
vs = append(vs,
|
||||
ebiten.Vertex{
|
||||
DstX: cur.x + oac.offsetX + dstOffsetX,
|
||||
DstY: cur.y + oac.offsetY + dstOffsetY,
|
||||
ColorR: oac.colorR,
|
||||
ColorG: oac.colorG,
|
||||
ColorB: oac.colorB,
|
||||
ColorA: oac.colorA,
|
||||
Custom0: 0, // u for Loop-Blinn algorithm
|
||||
Custom1: 0, // v for Loop-Blinn algorithm
|
||||
},
|
||||
ebiten.Vertex{
|
||||
DstX: op.p1.x + oac.offsetX + dstOffsetX,
|
||||
DstY: op.p1.y + oac.offsetY + dstOffsetY,
|
||||
ColorR: oac.colorR,
|
||||
ColorG: oac.colorG,
|
||||
ColorB: oac.colorB,
|
||||
ColorA: oac.colorA,
|
||||
Custom0: 0.5,
|
||||
Custom1: 0,
|
||||
},
|
||||
ebiten.Vertex{
|
||||
DstX: op.p2.x + oac.offsetX + dstOffsetX,
|
||||
DstY: op.p2.y + oac.offsetY + dstOffsetY,
|
||||
ColorR: oac.colorR,
|
||||
ColorG: oac.colorG,
|
||||
ColorB: oac.colorB,
|
||||
ColorA: oac.colorA,
|
||||
Custom0: 1,
|
||||
Custom1: 1,
|
||||
})
|
||||
is = append(is, idx, idx+1, idx+2)
|
||||
cur = op.p2
|
||||
}
|
||||
}
|
||||
}
|
||||
op := &ebiten.DrawTrianglesShaderOptions{}
|
||||
op.Blend = ebiten.BlendLighter
|
||||
shader, err := ensureStencilBufferBezierShader()
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("vector: failed to create stencil buffer bezier shader: %v", err))
|
||||
}
|
||||
stencilBufferImage.DrawTrianglesShader32(vs, is, shader, op)
|
||||
}
|
||||
}
|
||||
|
||||
// Render the stencil buffer with the specified color.
|
||||
for i, path := range f.paths {
|
||||
if path == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
stencilImage := theAtlas.stencilBufferImageAt(i, f.antialias, 0)
|
||||
if stencilImage == nil {
|
||||
continue
|
||||
}
|
||||
srcRegion := stencilImage.Bounds()
|
||||
|
||||
var offsetX, offsetY float32
|
||||
if f.antialias {
|
||||
stencilImage1 := theAtlas.stencilBufferImageAt(i, f.antialias, 1)
|
||||
offsetX = float32(stencilImage1.Bounds().Min.X - stencilImage.Bounds().Min.X)
|
||||
offsetY = float32(stencilImage1.Bounds().Min.Y - stencilImage.Bounds().Min.Y)
|
||||
}
|
||||
|
||||
pp := theAtlas.pathRenderingPositionAt(i)
|
||||
|
||||
vs = vs[:0]
|
||||
is = is[:0]
|
||||
dstOffsetX := max(0, dst.Bounds().Min.X-pp.X)
|
||||
dstOffsetY := max(0, dst.Bounds().Min.Y-pp.Y)
|
||||
var clrR, clrG, clrB, clrA float32
|
||||
clrR = f.colors[i].R()
|
||||
clrG = f.colors[i].G()
|
||||
clrB = f.colors[i].B()
|
||||
clrA = f.colors[i].A()
|
||||
vs = append(vs,
|
||||
ebiten.Vertex{
|
||||
DstX: float32(pp.X + dstOffsetX),
|
||||
DstY: float32(pp.Y + dstOffsetY),
|
||||
SrcX: float32(srcRegion.Min.X),
|
||||
SrcY: float32(srcRegion.Min.Y),
|
||||
ColorR: clrR,
|
||||
ColorG: clrG,
|
||||
ColorB: clrB,
|
||||
ColorA: clrA,
|
||||
Custom0: offsetX,
|
||||
Custom1: offsetY,
|
||||
},
|
||||
ebiten.Vertex{
|
||||
DstX: float32(pp.X + srcRegion.Dx() + dstOffsetX),
|
||||
DstY: float32(pp.Y + dstOffsetY),
|
||||
SrcX: float32(srcRegion.Max.X),
|
||||
SrcY: float32(srcRegion.Min.Y),
|
||||
ColorR: clrR,
|
||||
ColorG: clrG,
|
||||
ColorB: clrB,
|
||||
ColorA: clrA,
|
||||
Custom0: offsetX,
|
||||
Custom1: offsetY,
|
||||
},
|
||||
ebiten.Vertex{
|
||||
DstX: float32(pp.X + dstOffsetX),
|
||||
DstY: float32(pp.Y + srcRegion.Dy() + dstOffsetY),
|
||||
SrcX: float32(srcRegion.Min.X),
|
||||
SrcY: float32(srcRegion.Max.Y),
|
||||
ColorR: clrR,
|
||||
ColorG: clrG,
|
||||
ColorB: clrB,
|
||||
ColorA: clrA,
|
||||
Custom0: offsetX,
|
||||
Custom1: offsetY,
|
||||
},
|
||||
ebiten.Vertex{
|
||||
DstX: float32(pp.X + srcRegion.Dx() + dstOffsetX),
|
||||
DstY: float32(pp.Y + srcRegion.Dy() + dstOffsetY),
|
||||
SrcX: float32(srcRegion.Max.X),
|
||||
SrcY: float32(srcRegion.Max.Y),
|
||||
ColorR: clrR,
|
||||
ColorG: clrG,
|
||||
ColorB: clrB,
|
||||
ColorA: clrA,
|
||||
Custom0: offsetX,
|
||||
Custom1: offsetY,
|
||||
})
|
||||
is = append(is, 0, 1, 2, 1, 2, 3)
|
||||
|
||||
op := &ebiten.DrawTrianglesShaderOptions{}
|
||||
op.Blend = f.blend
|
||||
op.Images[0] = stencilImage
|
||||
var shader *ebiten.Shader
|
||||
switch f.fillRule {
|
||||
case FillRuleNonZero:
|
||||
var err error
|
||||
shader, err = ensureStencilBufferNonZeroShader(f.antialias)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("vector: failed to create stencil buffer non-zero shader: %v", err))
|
||||
}
|
||||
case FillRuleEvenOdd:
|
||||
var err error
|
||||
shader, err = ensureStencilBufferEvenOddShader(f.antialias)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("vector: failed to create stencil buffer even-odd shader: %v", err))
|
||||
}
|
||||
}
|
||||
dst2 := dst
|
||||
if dst.Bounds() != f.bounds[i] {
|
||||
dst2 = dst.SubImage(f.bounds[i]).(*ebiten.Image)
|
||||
}
|
||||
dst2.DrawTrianglesShader32(vs, is, shader, op)
|
||||
}
|
||||
}
|
||||
+742
-246
File diff suppressed because it is too large
Load Diff
+236
@@ -0,0 +1,236 @@
|
||||
// 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 (
|
||||
"sync"
|
||||
|
||||
"github.com/hajimehoshi/ebiten/v2"
|
||||
)
|
||||
|
||||
// The implementation is based on the following article:
|
||||
// https://medium.com/@evanwallace/easy-scalable-text-rendering-on-the-gpu-c3f4d782c5ac
|
||||
|
||||
// These values are protected by cacheM.
|
||||
|
||||
var (
|
||||
stencilBufferFillShader *ebiten.Shader
|
||||
stencilBufferBezierShader *ebiten.Shader
|
||||
stencilBufferNonZeroShader *ebiten.Shader
|
||||
stencilBufferNonZeroAAShader *ebiten.Shader
|
||||
stencilBufferEvenOddShader *ebiten.Shader
|
||||
stencilBufferEvenOddAAShader *ebiten.Shader
|
||||
|
||||
stencilBufferM sync.Mutex
|
||||
)
|
||||
|
||||
func ensureStencilBufferShaders() (*ebiten.Shader, error) {
|
||||
stencilBufferM.Lock()
|
||||
defer stencilBufferM.Unlock()
|
||||
|
||||
if stencilBufferFillShader != nil {
|
||||
return stencilBufferFillShader, nil
|
||||
}
|
||||
s, err := ebiten.NewShader([]byte(stencilBufferFillShaderSrc))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
stencilBufferFillShader = s
|
||||
return stencilBufferFillShader, err
|
||||
}
|
||||
|
||||
func ensureStencilBufferBezierShader() (*ebiten.Shader, error) {
|
||||
stencilBufferM.Lock()
|
||||
defer stencilBufferM.Unlock()
|
||||
|
||||
if stencilBufferBezierShader != nil {
|
||||
return stencilBufferBezierShader, nil
|
||||
}
|
||||
s, err := ebiten.NewShader([]byte(stencilBufferBezierShaderSrc))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
stencilBufferBezierShader = s
|
||||
return stencilBufferBezierShader, nil
|
||||
}
|
||||
|
||||
func ensureStencilBufferNonZeroShader(antialias bool) (*ebiten.Shader, error) {
|
||||
stencilBufferM.Lock()
|
||||
defer stencilBufferM.Unlock()
|
||||
|
||||
if antialias {
|
||||
if stencilBufferNonZeroAAShader != nil {
|
||||
return stencilBufferNonZeroAAShader, nil
|
||||
}
|
||||
s, err := ebiten.NewShader([]byte(stencilBufferNonZeroAAShaderSrc))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
stencilBufferNonZeroAAShader = s
|
||||
return stencilBufferNonZeroAAShader, nil
|
||||
}
|
||||
|
||||
if stencilBufferNonZeroShader != nil {
|
||||
return stencilBufferNonZeroShader, nil
|
||||
}
|
||||
s, err := ebiten.NewShader([]byte(stencilBufferNonZeroShaderSrc))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
stencilBufferNonZeroShader = s
|
||||
return stencilBufferNonZeroShader, nil
|
||||
}
|
||||
|
||||
func ensureStencilBufferEvenOddShader(antialias bool) (*ebiten.Shader, error) {
|
||||
stencilBufferM.Lock()
|
||||
defer stencilBufferM.Unlock()
|
||||
|
||||
if antialias {
|
||||
if stencilBufferEvenOddAAShader != nil {
|
||||
return stencilBufferEvenOddAAShader, nil
|
||||
}
|
||||
s, err := ebiten.NewShader([]byte(stencilBufferEvenOddAAShaderSrc))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
stencilBufferEvenOddAAShader = s
|
||||
return stencilBufferEvenOddAAShader, nil
|
||||
}
|
||||
|
||||
if stencilBufferEvenOddShader != nil {
|
||||
return stencilBufferEvenOddShader, nil
|
||||
}
|
||||
s, err := ebiten.NewShader([]byte(stencilBufferEvenOddShaderSrc))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
stencilBufferEvenOddShader = s
|
||||
return stencilBufferEvenOddShader, nil
|
||||
}
|
||||
|
||||
//ebitengine:shadersource
|
||||
const stencilBufferFillShaderSrc = `//kage:unit pixels
|
||||
|
||||
package main
|
||||
|
||||
func Fragment(dstPos vec4, srcPos vec2, color vec4, custom vec4) vec4 {
|
||||
v := 1.0 / 255.0
|
||||
if frontfacing() {
|
||||
v *= 16
|
||||
}
|
||||
return v * color
|
||||
}
|
||||
`
|
||||
|
||||
//ebitengine:shadersource
|
||||
const stencilBufferBezierShaderSrc = `//kage:unit pixels
|
||||
|
||||
package main
|
||||
|
||||
func Fragment(dstPos vec4, srcPos vec2, color vec4, custom vec4) vec4 {
|
||||
// Loop-Blinn algorithm.
|
||||
// https://developer.nvidia.com/gpugems/gpugems3/part-iv-image-effects/chapter-25-rendering-vector-art-gpu
|
||||
uv := custom.xy
|
||||
v := clamp(-sign(uv.x * uv.x - uv.y), 0, 1) * 1.0/255.0
|
||||
// This is opposite to the fill shader, especially for the non-zero fill rule.
|
||||
if !frontfacing() {
|
||||
v *= 16
|
||||
}
|
||||
return v * color
|
||||
}
|
||||
`
|
||||
|
||||
//ebitengine:shadersource
|
||||
const stencilBufferNonZeroShaderSrc = `//kage:unit pixels
|
||||
|
||||
package main
|
||||
|
||||
func round(x float) float {
|
||||
return floor(x + 0.5)
|
||||
}
|
||||
|
||||
func Fragment(dstPos vec4, srcPos vec2, color vec4) vec4 {
|
||||
c := imageSrc0UnsafeAt(srcPos)
|
||||
r := int(round(c.r*255))
|
||||
w := abs((r >> 4) - (r & 0x0F))
|
||||
v := min(float(w), 1)
|
||||
return v * color
|
||||
}
|
||||
`
|
||||
|
||||
//ebitengine:shadersource
|
||||
const stencilBufferNonZeroAAShaderSrc = `//kage:unit pixels
|
||||
|
||||
package main
|
||||
|
||||
func round(x vec4) vec4 {
|
||||
return floor(x + 0.5)
|
||||
}
|
||||
|
||||
func Fragment(dstPos vec4, srcPos vec2, color vec4, custom vec4) vec4 {
|
||||
c0 := imageSrc0UnsafeAt(srcPos)
|
||||
// imageSrc1UnsafeAt uses the offset info, which would prevent batching.
|
||||
// Use a custom offset instead.
|
||||
c1 := imageSrc0UnsafeAt(srcPos + custom.xy)
|
||||
ci0 := ivec4(round(c0*255))
|
||||
ci1 := ivec4(round(c1*255))
|
||||
w0 := abs((ci0 >> 4) - (ci0 & 0x0F))
|
||||
w1 := abs((ci1 >> 4) - (ci1 & 0x0F))
|
||||
v0 := min(vec4(w0), 1)
|
||||
v1 := min(vec4(w1), 1)
|
||||
return (dot(v0, vec4(1.0/8.0)) + dot(v1, vec4(1.0/8.0))) * color
|
||||
}
|
||||
`
|
||||
|
||||
//ebitengine:shadersource
|
||||
const stencilBufferEvenOddShaderSrc = `//kage:unit pixels
|
||||
|
||||
package main
|
||||
|
||||
func round(x float) float {
|
||||
return floor(x + 0.5)
|
||||
}
|
||||
|
||||
func Fragment(dstPos vec4, srcPos vec2, color vec4) vec4 {
|
||||
c := imageSrc0UnsafeAt(srcPos)
|
||||
r := int(round(c.r*255))
|
||||
v := abs((r >> 4) - (r & 0x0F))
|
||||
return float(v % 2) * color
|
||||
}
|
||||
`
|
||||
|
||||
//ebitengine:shadersource
|
||||
const stencilBufferEvenOddAAShaderSrc = `//kage:unit pixels
|
||||
|
||||
package main
|
||||
|
||||
func round(x vec4) vec4 {
|
||||
return floor(x + 0.5)
|
||||
}
|
||||
|
||||
func Fragment(dstPos vec4, srcPos vec2, color vec4, custom vec4) vec4 {
|
||||
c0 := imageSrc0UnsafeAt(srcPos)
|
||||
// imageSrc1UnsafeAt uses the offset info, which would prevent batching.
|
||||
// Use a custom offset instead.
|
||||
c1 := imageSrc0UnsafeAt(srcPos + custom.xy)
|
||||
ci0 := ivec4(round(c0*255))
|
||||
ci1 := ivec4(round(c1*255))
|
||||
w0 := abs((ci0 >> 4) - (ci0 & 0x0F))
|
||||
w1 := abs((ci1 >> 4) - (ci1 & 0x0F))
|
||||
v0 := vec4(w0 % 2)
|
||||
v1 := vec4(w1 % 2)
|
||||
return (dot(v0, vec4(1.0/8.0)) + dot(v1, vec4(1.0/8.0))) * color
|
||||
}
|
||||
`
|
||||
+426
@@ -0,0 +1,426 @@
|
||||
// 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"
|
||||
)
|
||||
|
||||
// LineCap represents the way in which how the ends of the stroke are rendered.
|
||||
type LineCap int
|
||||
|
||||
const (
|
||||
LineCapButt LineCap = iota
|
||||
LineCapRound
|
||||
LineCapSquare
|
||||
)
|
||||
|
||||
// LineJoin represents the way in which how two segments are joined.
|
||||
type LineJoin int
|
||||
|
||||
const (
|
||||
LineJoinMiter LineJoin = iota
|
||||
LineJoinBevel
|
||||
LineJoinRound
|
||||
)
|
||||
|
||||
// StrokeOptions is options to render a stroke.
|
||||
type StrokeOptions struct {
|
||||
// Width is the stroke width in pixels.
|
||||
//
|
||||
// The default (zero) value is 0.
|
||||
Width float32
|
||||
|
||||
// LineCap is the way in which how the ends of the stroke are rendered.
|
||||
// Line caps are not rendered when the sub-path is marked as closed.
|
||||
//
|
||||
// The default (zero) value is [LineCapButt].
|
||||
LineCap LineCap
|
||||
|
||||
// LineJoin is the way in which how two segments are joined.
|
||||
//
|
||||
// The default (zero) value is [LineJoinMiter].
|
||||
LineJoin LineJoin
|
||||
|
||||
// MiterLimit is the miter limit for [LineJoinMiter].
|
||||
// For details, see https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/stroke-miterlimit.
|
||||
//
|
||||
// The default (zero) value is 0.
|
||||
MiterLimit float32
|
||||
}
|
||||
|
||||
// AddStrokeOptions is options for [Path.AddStroke].
|
||||
type AddStrokeOptions struct {
|
||||
// StrokeOptions is options for the stroke.
|
||||
StrokeOptions
|
||||
|
||||
// GeoM is a geometry matrix to apply to the path.
|
||||
//
|
||||
// The default (zero) value is an identity matrix.
|
||||
GeoM ebiten.GeoM
|
||||
}
|
||||
|
||||
// AddStroke adds a stroke path to the path p.
|
||||
//
|
||||
// The added stroke path must be rendered with FileRuleNonZero.
|
||||
func (p *Path) AddStroke(src *Path, options *AddStrokeOptions) {
|
||||
if options == nil {
|
||||
return
|
||||
}
|
||||
if options.Width <= 0 {
|
||||
return
|
||||
}
|
||||
|
||||
// Normalize the source path to simplify the logic to generate a stroke path.
|
||||
src.normalize()
|
||||
|
||||
origN := len(p.subPaths)
|
||||
// p might be the same as src. Use srcN to avoid modifying the overlapped region.
|
||||
srcN := len(src.subPaths)
|
||||
for _, subPath := range src.subPaths[:srcN] {
|
||||
_, sp1, sp2, sp3, sp4 := strokeStartControlPositions(&subPath, options.Width/2)
|
||||
p.MoveTo(sp4.x, sp4.y)
|
||||
|
||||
appendParalleledPathFromSubPath(p, &subPath, &options.StrokeOptions)
|
||||
_, ep1, ep2, ep3, ep4 := strokeEndControlPositions(&subPath, options.Width/2)
|
||||
if subPath.closed {
|
||||
p.Close()
|
||||
p.MoveTo(ep4.x, ep4.y)
|
||||
} else {
|
||||
switch options.LineCap {
|
||||
case LineCapButt:
|
||||
p.LineTo(ep4.x, ep4.y)
|
||||
case LineCapRound:
|
||||
p.ArcTo(ep1.x, ep1.y, ep2.x, ep2.y, options.Width/2)
|
||||
p.ArcTo(ep3.x, ep3.y, ep4.x, ep4.y, options.Width/2)
|
||||
case LineCapSquare:
|
||||
p.LineTo(ep1.x, ep1.y)
|
||||
p.LineTo(ep3.x, ep3.y)
|
||||
p.LineTo(ep4.x, ep4.y)
|
||||
}
|
||||
}
|
||||
appendParalleledPathFromSubPathReversed(p, &subPath, &options.StrokeOptions)
|
||||
if !subPath.closed {
|
||||
switch options.LineCap {
|
||||
case LineCapButt:
|
||||
p.LineTo(sp4.x, sp4.y)
|
||||
case LineCapRound:
|
||||
p.ArcTo(sp1.x, sp1.y, sp2.x, sp2.y, options.Width/2)
|
||||
p.ArcTo(sp3.x, sp3.y, sp4.x, sp4.y, options.Width/2)
|
||||
case LineCapSquare:
|
||||
p.LineTo(sp1.x, sp1.y)
|
||||
p.LineTo(sp3.x, sp3.y)
|
||||
p.LineTo(sp4.x, sp4.y)
|
||||
}
|
||||
}
|
||||
p.Close()
|
||||
}
|
||||
|
||||
if options.GeoM != (ebiten.GeoM{}) {
|
||||
for i, subPath := range p.subPaths[origN:] {
|
||||
x, y := options.GeoM.Apply(float64(subPath.start.x), float64(subPath.start.y))
|
||||
p.subPaths[origN+i].start = point{x: float32(x), y: float32(y)}
|
||||
for j, op := range subPath.ops {
|
||||
switch op.typ {
|
||||
case opTypeLineTo:
|
||||
x1, y1 := options.GeoM.Apply(float64(op.p1.x), float64(op.p1.y))
|
||||
p.subPaths[origN+i].ops[j].p1 = point{x: float32(x1), y: float32(y1)}
|
||||
case opTypeQuadTo:
|
||||
x1, y1 := options.GeoM.Apply(float64(op.p1.x), float64(op.p1.y))
|
||||
x2, y2 := options.GeoM.Apply(float64(op.p2.x), float64(op.p2.y))
|
||||
p.subPaths[origN+i].ops[j].p1 = point{x: float32(x1), y: float32(y1)}
|
||||
p.subPaths[origN+i].ops[j].p2 = point{x: float32(x2), y: float32(y2)}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func strokeStartControlPositions(subPath *subPath, dist float32) (point, point, point, point, point) {
|
||||
p := subPath.startAtOp(0)
|
||||
dir := subPath.startDir(0).inv().norm().mul(dist)
|
||||
dirPerp := dir.perp()
|
||||
// TODO: These values are a little tricky. Refactor this.
|
||||
return p.add(dirPerp), p.add(dir).add(dirPerp), p.add(dir), p.add(dir).add(dirPerp.inv()), p.add(dirPerp.inv())
|
||||
}
|
||||
|
||||
func strokeEndControlPositions(subPath *subPath, dist float32) (point, point, point, point, point) {
|
||||
p := subPath.endAtOp(len(subPath.ops) - 1)
|
||||
dir := subPath.endDir(len(subPath.ops) - 1).norm().mul(dist)
|
||||
dirPerp := dir.perp()
|
||||
// TODO: These values are a little tricky. Refactor this.
|
||||
return p.add(dirPerp), p.add(dir).add(dirPerp), p.add(dir), p.add(dir).add(dirPerp.inv()), p.add(dirPerp.inv())
|
||||
}
|
||||
|
||||
func appendParalleledPathFromSubPath(strokePath *Path, subPath *subPath, options *StrokeOptions) {
|
||||
if len(subPath.ops) == 0 {
|
||||
panic("not reached")
|
||||
}
|
||||
|
||||
// As the source path is normalized, every operation is guaranteed to be valid.
|
||||
// A line operation must have a different point from the start point.
|
||||
// A quadratic curve operation must have create a curve, not a line.
|
||||
|
||||
cur := subPath.start
|
||||
|
||||
for i, op := range subPath.ops {
|
||||
switch op.typ {
|
||||
case opTypeLineTo:
|
||||
appendParalleledLine(strokePath, cur, op.p1, options.Width/2)
|
||||
cur = op.p1
|
||||
case opTypeQuadTo:
|
||||
appendParalleledQuad(strokePath, cur, op.p1, op.p2, options.Width/2)
|
||||
cur = op.p2
|
||||
}
|
||||
addJoint(strokePath, subPath, i, false, options)
|
||||
}
|
||||
}
|
||||
|
||||
func appendParalleledPathFromSubPathReversed(strokePath *Path, subPath *subPath, options *StrokeOptions) {
|
||||
if len(subPath.ops) == 0 {
|
||||
panic("not reached")
|
||||
}
|
||||
|
||||
// As the source path is normalized, every operation is guaranteed to be valid.
|
||||
// A line operation must have a different point from the start point.
|
||||
// A quadratic curve operation must have create a curve, not a line.
|
||||
|
||||
for i := len(subPath.ops) - 1; i >= 0; i-- {
|
||||
op := subPath.ops[i]
|
||||
nextP := subPath.startAtOp(i)
|
||||
switch op.typ {
|
||||
case opTypeLineTo:
|
||||
appendParalleledLine(strokePath, op.p1, nextP, options.Width/2)
|
||||
case opTypeQuadTo:
|
||||
appendParalleledQuad(strokePath, op.p2, op.p1, nextP, options.Width/2)
|
||||
}
|
||||
addJoint(strokePath, subPath, i, true, options)
|
||||
}
|
||||
}
|
||||
|
||||
func appendParalleledLine(path *Path, p0, p1 point, dist float32) {
|
||||
if p0 == p1 {
|
||||
panic("not reached")
|
||||
}
|
||||
|
||||
dir := vec2{x: p1.x - p0.x, y: p1.y - p0.y}
|
||||
v := dir.perp().norm().mul(dist)
|
||||
pp1 := p1.add(v)
|
||||
path.LineTo(pp1.x, pp1.y)
|
||||
}
|
||||
|
||||
// appendParalleledLineForQuadIfNeeded appends a paralleled line for a quadratic curve if the quadratic curve is just a line.
|
||||
func appendParalleledLineForQuadIfNeeded(path *Path, p0, p1, p2 point, dist float32) bool {
|
||||
if p0 == p1 && p0 == p2 {
|
||||
panic("not reached")
|
||||
}
|
||||
// This curve is empty as the start and the end points are the same.
|
||||
if p0 == p2 {
|
||||
return true
|
||||
}
|
||||
// This curve is a line as the control point is the same as the start point.
|
||||
if p0 == p1 || p1 == p2 {
|
||||
appendParalleledLine(path, p0, p2, dist)
|
||||
return true
|
||||
}
|
||||
// This curve is a line as p0, p1, and p2 are on the same line.
|
||||
if (p1.x-p0.x)*(p2.y-p0.y)-(p2.x-p0.x)*(p1.y-p0.y) == 0 {
|
||||
appendParalleledLine(path, p0, p2, dist)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func appendParalleledQuad(path *Path, p0, p1, p2 point, dist float32) {
|
||||
if appendParalleledLineForQuadIfNeeded(path, p0, p1, p2, dist) {
|
||||
return
|
||||
}
|
||||
doAppendParalleledQuad(path, p0, p1, p2, dist, 0)
|
||||
}
|
||||
|
||||
func doAppendParalleledQuad(path *Path, p0, p1, p2 point, dist float32, level int) {
|
||||
if p0 == p1 && p0 == p2 {
|
||||
return
|
||||
}
|
||||
if appendParalleledLineForQuadIfNeeded(path, p0, p1, p2, dist) {
|
||||
return
|
||||
}
|
||||
|
||||
// B(t) = (1-t)*(1-t)*p0 + 2*(1-t)*t*p1 + t*t*p2
|
||||
// B'(t) = 2*(1-t)*(p1-p0) + 2*t*(p2-p1)
|
||||
// B'(0) = 2*(p1-p0)
|
||||
// B'(0.5) = p2-p0
|
||||
// B'(1) = 2*(p2-p1)
|
||||
// B''(t) = 2*(p0 - 2*p1 + p2)
|
||||
|
||||
// t = 0
|
||||
dir0 := vec2{x: p1.x - p0.x, y: p1.y - p0.y}
|
||||
v0 := dir0.perp().norm().mul(dist)
|
||||
pp0 := p0.add(v0)
|
||||
|
||||
// t = 1
|
||||
dir2 := vec2{x: p2.x - p1.x, y: p2.y - p1.y}
|
||||
v2 := dir2.perp().norm().mul(dist)
|
||||
pp2 := p2.add(v2)
|
||||
|
||||
// t = 0.5
|
||||
dir1 := vec2{x: p2.x - p0.x, y: p2.y - p0.y}
|
||||
v1 := dir1.perp().norm().mul(dist)
|
||||
mid := point{
|
||||
x: 0.25*p0.x + 0.5*p1.x + 0.25*p2.x,
|
||||
y: 0.25*p0.y + 0.5*p1.y + 0.25*p2.y,
|
||||
}.add(v1)
|
||||
// Calculate the control point P1 from B(0.5).
|
||||
pp1 := point{
|
||||
x: 2*mid.x - 0.5*(pp0.x+pp2.x),
|
||||
y: 2*mid.y - 0.5*(pp0.y+pp2.y),
|
||||
}
|
||||
|
||||
if level > 5 {
|
||||
path.QuadTo(pp1.x, pp1.y, pp2.x, pp2.y)
|
||||
return
|
||||
}
|
||||
|
||||
// If any of the points is not a regular float32, do not call this function recursively.
|
||||
if !isRegularF32(pp0.x) || !isRegularF32(pp0.y) || !isRegularF32(pp1.x) || !isRegularF32(pp1.y) || !isRegularF32(pp2.x) || !isRegularF32(pp2.y) {
|
||||
path.QuadTo(pp1.x, pp1.y, pp2.x, pp2.y)
|
||||
return
|
||||
}
|
||||
|
||||
minAllowance := max(dist*63/64, 0)
|
||||
maxAllowance := dist * 65 / 64
|
||||
|
||||
var needSplit bool
|
||||
for _, t := range []float32{0.25, 0.75} {
|
||||
gotP := point{
|
||||
x: (1-t)*(1-t)*pp0.x + 2*(1-t)*t*pp1.x + t*t*pp2.x,
|
||||
y: (1-t)*(1-t)*pp0.y + 2*(1-t)*t*pp1.y + t*t*pp2.y,
|
||||
}
|
||||
|
||||
dir := vec2{
|
||||
x: (1-t)*(p1.x-p0.x) + t*(p2.x-p1.x),
|
||||
y: (1-t)*(p1.y-p0.y) + t*(p2.y-p1.y),
|
||||
}
|
||||
v := dir.perp().norm().mul(dist)
|
||||
p := point{
|
||||
x: (1-t)*(1-t)*p0.x + 2*(1-t)*t*p1.x + t*t*p2.x + v.x,
|
||||
y: (1-t)*(1-t)*p0.y + 2*(1-t)*t*p1.y + t*t*p2.y + v.y,
|
||||
}
|
||||
expectedP := p.add(v)
|
||||
|
||||
if !arePointsInRange(gotP, expectedP, minAllowance, maxAllowance) {
|
||||
needSplit = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !needSplit {
|
||||
path.QuadTo(pp1.x, pp1.y, pp2.x, pp2.y)
|
||||
return
|
||||
}
|
||||
|
||||
// Split a quadratic curve into two quadratic curves by De Casteljau algorithm.
|
||||
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,
|
||||
}
|
||||
doAppendParalleledQuad(path, p0, p01, p012, dist, level+1)
|
||||
doAppendParalleledQuad(path, p012, p12, p2, dist, level+1)
|
||||
}
|
||||
|
||||
func addJoint(strokePath *Path, subPath *subPath, opIndex int, reverse bool, options *StrokeOptions) {
|
||||
var p point
|
||||
var dir0, dir1 vec2
|
||||
if !reverse {
|
||||
nextOpIdx := opIndex + 1
|
||||
if nextOpIdx == len(subPath.ops) {
|
||||
if !subPath.closed {
|
||||
return
|
||||
}
|
||||
nextOpIdx = 0
|
||||
}
|
||||
p = subPath.endAtOp(opIndex)
|
||||
dir0 = subPath.endDir(opIndex).norm()
|
||||
dir1 = subPath.startDir(nextOpIdx).norm()
|
||||
} else {
|
||||
nextOpIdx := opIndex - 1
|
||||
if nextOpIdx == -1 {
|
||||
if !subPath.closed {
|
||||
return
|
||||
}
|
||||
nextOpIdx = len(subPath.ops) - 1
|
||||
}
|
||||
p = subPath.startAtOp(opIndex)
|
||||
dir0 = subPath.startDir(opIndex).inv().norm()
|
||||
dir1 = subPath.endDir(nextOpIdx).inv().norm()
|
||||
}
|
||||
|
||||
if dir0 == dir1 {
|
||||
return
|
||||
}
|
||||
|
||||
v1 := dir1.perp().mul(options.Width / 2)
|
||||
p1 := p.add(v1)
|
||||
|
||||
// If the joint is an internal angle (< 180 degrees), the joint is not rendered. Just connect the two segments.
|
||||
// [vec2.cross] has a precision issue. Use a comparison instead.
|
||||
if dir0.x*dir1.y > dir0.y*dir1.x {
|
||||
strokePath.LineTo(p1.x, p1.y)
|
||||
return
|
||||
}
|
||||
|
||||
v0 := dir0.perp().mul(options.Width / 2)
|
||||
p0 := p.add(v0)
|
||||
|
||||
// Add a joint.
|
||||
switch options.LineJoin {
|
||||
case LineJoinMiter:
|
||||
theta := math.Acos(float64(dir0.x*(-dir1.x) + dir0.y*(-dir1.y)))
|
||||
exceed := float32(math.Abs(1/math.Sin(float64(theta/2)))) > options.MiterLimit
|
||||
if !exceed {
|
||||
cp := crossingPointForTwoLines(p0, p0.add(dir0), p1, p1.add(dir1))
|
||||
if isRegularF32(cp.x) && isRegularF32(cp.y) {
|
||||
strokePath.LineTo(cp.x, cp.y)
|
||||
}
|
||||
}
|
||||
strokePath.LineTo(p1.x, p1.y)
|
||||
case LineJoinBevel:
|
||||
strokePath.LineTo(p1.x, p1.y)
|
||||
case LineJoinRound:
|
||||
dir := vec2{
|
||||
x: dir0.x - dir1.x,
|
||||
y: dir0.y - dir1.y,
|
||||
}.norm()
|
||||
cp := p.add(dir.mul(options.Width / 2))
|
||||
cp0 := crossingPointForTwoLines(p0, p0.add(dir0), cp, cp.add(dir.perp()))
|
||||
cp1 := crossingPointForTwoLines(p1, p1.add(dir1), cp, cp.add(dir.perp()))
|
||||
if isRegularF32(cp.x) && isRegularF32(cp.y) && isRegularF32(cp0.x) && isRegularF32(cp0.y) && isRegularF32(cp1.x) && isRegularF32(cp1.y) {
|
||||
strokePath.ArcTo(cp0.x, cp0.y, cp.x, cp.y, options.Width/2)
|
||||
strokePath.ArcTo(cp1.x, cp1.y, p1.x, p1.y, options.Width/2)
|
||||
} else {
|
||||
strokePath.LineTo(p1.x, p1.y)
|
||||
}
|
||||
}
|
||||
}
|
||||
+358
-59
@@ -18,6 +18,8 @@ import (
|
||||
"image"
|
||||
"image/color"
|
||||
"math"
|
||||
"sync"
|
||||
_ "unsafe"
|
||||
|
||||
"github.com/hajimehoshi/ebiten/v2"
|
||||
)
|
||||
@@ -27,6 +29,18 @@ var (
|
||||
whiteSubImage = whiteImage.SubImage(image.Rect(1, 1, 2, 2)).(*ebiten.Image)
|
||||
)
|
||||
|
||||
var (
|
||||
theCachedVerticesForUtil []ebiten.Vertex
|
||||
theCachedIndicesForUtil []uint32
|
||||
theCacheForUtilM sync.Mutex
|
||||
)
|
||||
|
||||
func useCachedVerticesAndIndicesForUtil(fn func([]ebiten.Vertex, []uint32) (vs []ebiten.Vertex, is []uint32)) {
|
||||
theCacheForUtilM.Lock()
|
||||
defer theCacheForUtilM.Unlock()
|
||||
theCachedVerticesForUtil, theCachedIndicesForUtil = fn(theCachedVerticesForUtil[:0], theCachedIndicesForUtil[:0])
|
||||
}
|
||||
|
||||
func init() {
|
||||
b := whiteImage.Bounds()
|
||||
pix := make([]byte, 4*b.Dx()*b.Dy())
|
||||
@@ -37,88 +51,373 @@ func init() {
|
||||
whiteImage.WritePixels(pix)
|
||||
}
|
||||
|
||||
func drawVerticesForUtil(dst *ebiten.Image, vs []ebiten.Vertex, is []uint16, clr color.Color, antialias bool) {
|
||||
r, g, b, a := clr.RGBA()
|
||||
for i := range vs {
|
||||
vs[i].SrcX = 1
|
||||
vs[i].SrcY = 1
|
||||
vs[i].ColorR = float32(r) / 0xffff
|
||||
vs[i].ColorG = float32(g) / 0xffff
|
||||
vs[i].ColorB = float32(b) / 0xffff
|
||||
vs[i].ColorA = float32(a) / 0xffff
|
||||
// StrokeLine strokes a line (x0, y0)-(x1, y1) with the specified width and color.
|
||||
func StrokeLine(dst *ebiten.Image, x0, y0, x1, y1 float32, strokeWidth float32, clr color.Color, antialias bool) {
|
||||
if antialias {
|
||||
var path Path
|
||||
path.MoveTo(x0, y0)
|
||||
path.LineTo(x1, y1)
|
||||
strokeOp := &StrokeOptions{}
|
||||
strokeOp.Width = strokeWidth
|
||||
drawOp := &DrawPathOptions{}
|
||||
drawOp.AntiAlias = true
|
||||
drawOp.ColorScale.ScaleWithColor(clr)
|
||||
StrokePath(dst, &path, strokeOp, drawOp)
|
||||
return
|
||||
}
|
||||
|
||||
op := &ebiten.DrawTrianglesOptions{}
|
||||
op.ColorScaleMode = ebiten.ColorScaleModePremultipliedAlpha
|
||||
op.AntiAlias = antialias
|
||||
dst.DrawTriangles(vs, is, whiteSubImage, op)
|
||||
// Use a regular DrawImage for batching.
|
||||
op := &ebiten.DrawImageOptions{}
|
||||
op.GeoM.Scale(math.Hypot(float64(x1-x0), float64(y1-y0)), float64(strokeWidth))
|
||||
op.GeoM.Translate(0, -float64(strokeWidth)/2)
|
||||
op.GeoM.Rotate(math.Atan2(float64(y1-y0), float64(x1-x0)))
|
||||
op.GeoM.Translate(float64(x0), float64(y0))
|
||||
op.ColorScale.ScaleWithColor(clr)
|
||||
dst.DrawImage(whiteSubImage, op)
|
||||
}
|
||||
|
||||
// StrokeLine strokes a line (x0, y0)-(x1, y1) with the specified width and color.
|
||||
//
|
||||
// clr has be to be a solid (non-transparent) color.
|
||||
func StrokeLine(dst *ebiten.Image, x0, y0, x1, y1 float32, strokeWidth float32, clr color.Color, antialias bool) {
|
||||
var path Path
|
||||
path.MoveTo(x0, y0)
|
||||
path.LineTo(x1, y1)
|
||||
strokeOp := &StrokeOptions{}
|
||||
strokeOp.Width = strokeWidth
|
||||
vs, is := path.AppendVerticesAndIndicesForStroke(nil, nil, strokeOp)
|
||||
// FillRect fills a rectangle with the specified width and color.
|
||||
func FillRect(dst *ebiten.Image, x, y, width, height float32, clr color.Color, antialias bool) {
|
||||
if antialias {
|
||||
var path Path
|
||||
path.MoveTo(x, y)
|
||||
path.LineTo(x, y+height)
|
||||
path.LineTo(x+width, y+height)
|
||||
path.LineTo(x+width, y)
|
||||
drawOp := &DrawPathOptions{}
|
||||
drawOp.AntiAlias = true
|
||||
drawOp.ColorScale.ScaleWithColor(clr)
|
||||
FillPath(dst, &path, nil, drawOp)
|
||||
return
|
||||
}
|
||||
|
||||
drawVerticesForUtil(dst, vs, is, clr, antialias)
|
||||
// Use a regular DrawImage for batching.
|
||||
op := &ebiten.DrawImageOptions{}
|
||||
op.GeoM.Scale(float64(width), float64(height))
|
||||
op.GeoM.Translate(float64(x), float64(y))
|
||||
op.ColorScale.ScaleWithColor(clr)
|
||||
dst.DrawImage(whiteSubImage, op)
|
||||
}
|
||||
|
||||
// DrawFilledRect fills a rectangle with the specified width and color.
|
||||
//
|
||||
// Deprecated: as of v2.9. Use [FillRect] instead.
|
||||
func DrawFilledRect(dst *ebiten.Image, x, y, width, height float32, clr color.Color, antialias bool) {
|
||||
var path Path
|
||||
path.MoveTo(x, y)
|
||||
path.LineTo(x, y+height)
|
||||
path.LineTo(x+width, y+height)
|
||||
path.LineTo(x+width, y)
|
||||
vs, is := path.AppendVerticesAndIndicesForFilling(nil, nil)
|
||||
|
||||
drawVerticesForUtil(dst, vs, is, clr, antialias)
|
||||
FillRect(dst, x, y, width, height, clr, antialias)
|
||||
}
|
||||
|
||||
// StrokeRect strokes a rectangle with the specified width and color.
|
||||
//
|
||||
// clr has be to be a solid (non-transparent) color.
|
||||
func StrokeRect(dst *ebiten.Image, x, y, width, height float32, strokeWidth float32, clr color.Color, antialias bool) {
|
||||
var path Path
|
||||
path.MoveTo(x, y)
|
||||
path.LineTo(x, y+height)
|
||||
path.LineTo(x+width, y+height)
|
||||
path.LineTo(x+width, y)
|
||||
path.Close()
|
||||
if antialias {
|
||||
var path Path
|
||||
path.MoveTo(x, y)
|
||||
path.LineTo(x, y+height)
|
||||
path.LineTo(x+width, y+height)
|
||||
path.LineTo(x+width, y)
|
||||
path.Close()
|
||||
strokeOp := &StrokeOptions{}
|
||||
strokeOp.Width = strokeWidth
|
||||
strokeOp.MiterLimit = 10
|
||||
drawOp := &DrawPathOptions{}
|
||||
drawOp.AntiAlias = true
|
||||
drawOp.ColorScale.ScaleWithColor(clr)
|
||||
StrokePath(dst, &path, strokeOp, drawOp)
|
||||
return
|
||||
}
|
||||
|
||||
strokeOp := &StrokeOptions{}
|
||||
strokeOp.Width = strokeWidth
|
||||
strokeOp.MiterLimit = 10
|
||||
vs, is := path.AppendVerticesAndIndicesForStroke(nil, nil, strokeOp)
|
||||
if strokeWidth <= 0 {
|
||||
return
|
||||
}
|
||||
|
||||
drawVerticesForUtil(dst, vs, is, clr, antialias)
|
||||
if strokeWidth >= width || strokeWidth >= height {
|
||||
FillRect(dst, x-strokeWidth/2, y-strokeWidth/2, width+strokeWidth, height+strokeWidth, clr, false)
|
||||
return
|
||||
}
|
||||
|
||||
// Use a regular DrawImage for batching.
|
||||
{
|
||||
// Render the top side.
|
||||
op := &ebiten.DrawImageOptions{}
|
||||
op.GeoM.Scale(float64(width+strokeWidth), float64(strokeWidth))
|
||||
op.GeoM.Translate(float64(x-strokeWidth/2), float64(y-strokeWidth/2))
|
||||
op.ColorScale.ScaleWithColor(clr)
|
||||
dst.DrawImage(whiteSubImage, op)
|
||||
}
|
||||
{
|
||||
// Render the left side.
|
||||
op := &ebiten.DrawImageOptions{}
|
||||
op.GeoM.Scale(float64(strokeWidth), float64(height-strokeWidth))
|
||||
op.GeoM.Translate(float64(x-strokeWidth/2), float64(y+strokeWidth/2))
|
||||
op.ColorScale.ScaleWithColor(clr)
|
||||
dst.DrawImage(whiteSubImage, op)
|
||||
}
|
||||
{
|
||||
// Render the right side.
|
||||
op := &ebiten.DrawImageOptions{}
|
||||
op.GeoM.Scale(float64(strokeWidth), float64(height-strokeWidth))
|
||||
op.GeoM.Translate(float64(x+width-strokeWidth/2), float64(y+strokeWidth/2))
|
||||
op.ColorScale.ScaleWithColor(clr)
|
||||
dst.DrawImage(whiteSubImage, op)
|
||||
}
|
||||
{
|
||||
// Render the bottom side.
|
||||
op := &ebiten.DrawImageOptions{}
|
||||
op.GeoM.Scale(float64(width+strokeWidth), float64(strokeWidth))
|
||||
op.GeoM.Translate(float64(x-strokeWidth/2), float64(y+height-strokeWidth/2))
|
||||
op.ColorScale.ScaleWithColor(clr)
|
||||
dst.DrawImage(whiteSubImage, op)
|
||||
}
|
||||
}
|
||||
|
||||
// FillCircle fills a circle with the specified center position (cx, cy), the radius (r), width and color.
|
||||
func FillCircle(dst *ebiten.Image, cx, cy, r float32, clr color.Color, antialias bool) {
|
||||
if antialias {
|
||||
var path Path
|
||||
path.Arc(cx, cy, r, 0, 2*math.Pi, Clockwise)
|
||||
drawOp := &DrawPathOptions{}
|
||||
drawOp.AntiAlias = true
|
||||
drawOp.ColorScale.ScaleWithColor(clr)
|
||||
FillPath(dst, &path, nil, drawOp)
|
||||
return
|
||||
}
|
||||
|
||||
// Use a regular DrawTriangles32 for batching.
|
||||
cr, cg, cb, ca := clr.RGBA()
|
||||
crf := float32(cr) / 0xffff
|
||||
cgf := float32(cg) / 0xffff
|
||||
cbf := float32(cb) / 0xffff
|
||||
caf := float32(ca) / 0xffff
|
||||
useCachedVerticesAndIndicesForUtil(func(vs []ebiten.Vertex, is []uint32) ([]ebiten.Vertex, []uint32) {
|
||||
count := int(math.Ceil(math.Pi * float64(r)))
|
||||
for i := range count {
|
||||
angle := float64(i) * (2 * math.Pi / float64(count))
|
||||
sin, cos := math.Sincos(angle)
|
||||
x := cx + r*float32(cos)
|
||||
y := cy + r*float32(sin)
|
||||
vs = append(vs, ebiten.Vertex{
|
||||
DstX: x,
|
||||
DstY: y,
|
||||
SrcX: 1,
|
||||
SrcY: 1,
|
||||
ColorR: crf,
|
||||
ColorG: cgf,
|
||||
ColorB: cbf,
|
||||
ColorA: caf,
|
||||
})
|
||||
if i > 1 {
|
||||
idx := uint32(len(vs))
|
||||
is = append(is, 0, idx-1, idx-2)
|
||||
}
|
||||
}
|
||||
op := &ebiten.DrawTrianglesOptions{}
|
||||
op.ColorScaleMode = ebiten.ColorScaleModePremultipliedAlpha
|
||||
dst.DrawTriangles32(vs, is, whiteSubImage, op)
|
||||
return vs, is
|
||||
})
|
||||
}
|
||||
|
||||
// DrawFilledCircle fills a circle with the specified center position (cx, cy), the radius (r), width and color.
|
||||
//
|
||||
// Deprecated: as of v2.9. Use [FillCircle] instead.
|
||||
func DrawFilledCircle(dst *ebiten.Image, cx, cy, r float32, clr color.Color, antialias bool) {
|
||||
var path Path
|
||||
path.Arc(cx, cy, r, 0, 2*math.Pi, Clockwise)
|
||||
vs, is := path.AppendVerticesAndIndicesForFilling(nil, nil)
|
||||
|
||||
drawVerticesForUtil(dst, vs, is, clr, antialias)
|
||||
FillCircle(dst, cx, cy, r, clr, antialias)
|
||||
}
|
||||
|
||||
// StrokeCircle strokes a circle with the specified center position (cx, cy), the radius (r), width and color.
|
||||
//
|
||||
// clr has be to be a solid (non-transparent) color.
|
||||
func StrokeCircle(dst *ebiten.Image, cx, cy, r float32, strokeWidth float32, clr color.Color, antialias bool) {
|
||||
var path Path
|
||||
path.Arc(cx, cy, r, 0, 2*math.Pi, Clockwise)
|
||||
path.Close()
|
||||
if antialias {
|
||||
var path Path
|
||||
path.Arc(cx, cy, r, 0, 2*math.Pi, Clockwise)
|
||||
path.Close()
|
||||
strokeOp := &StrokeOptions{}
|
||||
strokeOp.Width = strokeWidth
|
||||
strokeOp.LineJoin = LineJoinRound
|
||||
drawOp := &DrawPathOptions{}
|
||||
drawOp.AntiAlias = true
|
||||
drawOp.ColorScale.ScaleWithColor(clr)
|
||||
StrokePath(dst, &path, strokeOp, drawOp)
|
||||
return
|
||||
}
|
||||
|
||||
strokeOp := &StrokeOptions{}
|
||||
strokeOp.Width = strokeWidth
|
||||
vs, is := path.AppendVerticesAndIndicesForStroke(nil, nil, strokeOp)
|
||||
if strokeWidth <= 0 {
|
||||
return
|
||||
}
|
||||
|
||||
drawVerticesForUtil(dst, vs, is, clr, antialias)
|
||||
if strokeWidth >= r {
|
||||
FillCircle(dst, cx, cy, r+strokeWidth/2, clr, false)
|
||||
return
|
||||
}
|
||||
|
||||
// Use a regular DrawTriangles32 for batching.
|
||||
cr, cg, cb, ca := clr.RGBA()
|
||||
crf := float32(cr) / 0xffff
|
||||
cgf := float32(cg) / 0xffff
|
||||
cbf := float32(cb) / 0xffff
|
||||
caf := float32(ca) / 0xffff
|
||||
useCachedVerticesAndIndicesForUtil(func(vs []ebiten.Vertex, is []uint32) ([]ebiten.Vertex, []uint32) {
|
||||
count := int(math.Ceil(math.Pi * float64(r+strokeWidth/2)))
|
||||
for i := range count {
|
||||
angle := float64(i) * (2 * math.Pi / float64(count))
|
||||
sin, cos := math.Sincos(angle)
|
||||
x0 := cx + (r+strokeWidth/2)*float32(cos)
|
||||
y0 := cy + (r+strokeWidth/2)*float32(sin)
|
||||
vs = append(vs, ebiten.Vertex{
|
||||
DstX: x0,
|
||||
DstY: y0,
|
||||
SrcX: 1,
|
||||
SrcY: 1,
|
||||
ColorR: crf,
|
||||
ColorG: cgf,
|
||||
ColorB: cbf,
|
||||
ColorA: caf,
|
||||
})
|
||||
x1 := cx + (r-strokeWidth/2)*float32(cos)
|
||||
y1 := cy + (r-strokeWidth/2)*float32(sin)
|
||||
vs = append(vs, ebiten.Vertex{
|
||||
DstX: x1,
|
||||
DstY: y1,
|
||||
SrcX: 1,
|
||||
SrcY: 1,
|
||||
ColorR: crf,
|
||||
ColorG: cgf,
|
||||
ColorB: cbf,
|
||||
ColorA: caf,
|
||||
})
|
||||
idx := uint32(2 * i)
|
||||
total := uint32(2 * count)
|
||||
is = append(is, idx, idx+1, (idx+2)%total, idx+1, (idx+2)%total, (idx+3)%total)
|
||||
}
|
||||
op := &ebiten.DrawTrianglesOptions{}
|
||||
op.ColorScaleMode = ebiten.ColorScaleModePremultipliedAlpha
|
||||
dst.DrawTriangles32(vs, is, whiteSubImage, op)
|
||||
return vs, is
|
||||
})
|
||||
}
|
||||
|
||||
// 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
|
||||
op := &AddStrokeOptions{}
|
||||
op.StrokeOptions = *strokeOptions
|
||||
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)
|
||||
|
||||
Reference in New Issue
Block a user