vector: add LineJoin

Updates #1843
This commit is contained in:
Hajime Hoshi 2022-10-14 19:28:01 +09:00
parent f5ae18d6f6
commit a1a598471b
3 changed files with 264 additions and 11 deletions

176
examples/lines/main.go Normal file
View File

@ -0,0 +1,176 @@
// Copyright 2022 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 main
import (
"image"
"image/color"
"log"
"math"
"github.com/hajimehoshi/ebiten/v2"
"github.com/hajimehoshi/ebiten/v2/ebitenutil"
"github.com/hajimehoshi/ebiten/v2/inpututil"
"github.com/hajimehoshi/ebiten/v2/vector"
)
func min(x, y int) int {
if x < y {
return x
}
return y
}
var (
emptyImage = ebiten.NewImage(3, 3)
// emptySubImage is an internal sub image of emptyImage.
// Use emptySubImage at DrawTriangles instead of emptyImage in order to avoid bleeding edges.
emptySubImage = emptyImage.SubImage(image.Rect(1, 1, 2, 2)).(*ebiten.Image)
)
func init() {
emptyImage.Fill(color.White)
}
const (
screenWidth = 640
screenHeight = 480
)
type Game struct {
counter int
vertices []ebiten.Vertex
indices []uint16
offscreen *ebiten.Image
aa bool
showCenter bool
}
func (g *Game) Update() error {
g.counter++
if inpututil.IsKeyJustPressed(ebiten.KeyA) {
g.aa = !g.aa
}
if inpututil.IsKeyJustPressed(ebiten.KeyC) {
g.showCenter = !g.showCenter
}
return nil
}
func (g *Game) Draw(screen *ebiten.Image) {
target := screen
if g.aa {
// Prepare the double-sized offscreen.
// This is for anti-aliasing by a pseudo MSAA (multisample anti-aliasing).
if g.offscreen != nil {
sw, sh := screen.Size()
ow, oh := g.offscreen.Size()
if ow != sw*2 || oh != sh*2 {
g.offscreen.Dispose()
g.offscreen = nil
}
}
if g.offscreen == nil {
sw, sh := screen.Size()
g.offscreen = ebiten.NewImage(sw*2, sh*2)
}
g.offscreen.Clear()
target = g.offscreen
}
ow, oh := target.Size()
size := min(ow/4, oh/4)
offsetX, offsetY := (ow-size*3)/2, (oh-size*3)/2
// Render the lines on the target.
for j := 0; j < 3; j++ {
for i, join := range []vector.LineJoin{vector.LineJoinMiter, vector.LineJoinBevel, vector.LineJoinRound} {
r := image.Rect(i*size+offsetX, j*size+offsetY, (i+1)*size+offsetX, (j+1)*size+offsetY)
g.drawLine(target, r, join)
}
}
if g.aa {
// Render the offscreen to the screen.
op := &ebiten.DrawImageOptions{}
op.GeoM.Scale(0.5, 0.5)
op.Filter = ebiten.FilterLinear
screen.DrawImage(g.offscreen, op)
}
msg := `Press A to switch anti-aliasing.
Press C to switch to draw the center lines.`
ebitenutil.DebugPrint(screen, msg)
}
func (g *Game) drawLine(screen *ebiten.Image, region image.Rectangle, join vector.LineJoin) {
c0x := float64(region.Min.X + region.Dx()/4)
c0y := float64(region.Min.Y + region.Dy()/4)
c1x := float64(region.Max.X - region.Dx()/4)
c1y := float64(region.Max.Y - region.Dy()/4)
r := float64(min(region.Dx(), region.Dy()) / 4)
a := 2 * math.Pi * float64(g.counter) / (10 * ebiten.DefaultTPS)
var path vector.Path
sin, cos := math.Sincos(a)
path.MoveTo(float32(r*cos+c0x), float32(r*sin+c0y))
path.LineTo(float32(-r*cos+c0x), float32(-r*sin+c0y))
path.LineTo(float32(r*cos+c1x), float32(r*sin+c1y))
path.LineTo(float32(-r*cos+c1x), float32(-r*sin+c1y))
// Draw the main line in white.
op := &vector.StrokeOptions{}
op.LineJoin = join
op.Width = float32(r / 2)
vs, is := path.AppendVerticesAndIndicesForStroke(g.vertices[:0], g.indices[:0], op)
for i := range vs {
vs[i].SrcX = 1
vs[i].SrcY = 1
}
screen.DrawTriangles(vs, is, emptySubImage, nil)
// Draw the center line in red.
if g.showCenter {
op.Width = 1
vs, is := path.AppendVerticesAndIndicesForStroke(g.vertices[:0], g.indices[:0], op)
for i := range vs {
vs[i].SrcX = 1
vs[i].SrcY = 1
vs[i].SrcX = 1
vs[i].SrcY = 1
vs[i].ColorR = 1
vs[i].ColorG = 0
vs[i].ColorB = 0
}
screen.DrawTriangles(vs, is, emptySubImage, nil)
}
}
func (g *Game) Layout(outsideWidth, outsideHeight int) (int, int) {
return screenWidth, screenHeight
}
func main() {
var g Game
ebiten.SetWindowSize(screenWidth, screenHeight)
ebiten.SetWindowTitle("Lines (Ebitengine Demo)")
if err := ebiten.RunGame(&g); err != nil {
log.Fatal(err)
}
}

View File

@ -118,6 +118,7 @@ func drawEbitenText(screen *ebiten.Image, x, y int, scale float32, line bool) {
if line { if line {
op := &vector.StrokeOptions{} op := &vector.StrokeOptions{}
op.Width = 5 op.Width = 5
op.LineJoin = vector.LineJoinRound
vs, is = path.AppendVerticesAndIndicesForStroke(nil, nil, op) vs, is = path.AppendVerticesAndIndicesForStroke(nil, nil, op)
} else { } else {
vs, is = path.AppendVerticesAndIndicesForFilling(nil, nil) vs, is = path.AppendVerticesAndIndicesForFilling(nil, nil)
@ -170,6 +171,7 @@ func drawEbitenLogo(screen *ebiten.Image, x, y int, scale float32, line bool) {
if line { if line {
op := &vector.StrokeOptions{} op := &vector.StrokeOptions{}
op.Width = 5 op.Width = 5
op.LineJoin = vector.LineJoinRound
vs, is = path.AppendVerticesAndIndicesForStroke(nil, nil, op) vs, is = path.AppendVerticesAndIndicesForStroke(nil, nil, op)
} else { } else {
vs, is = path.AppendVerticesAndIndicesForFilling(nil, nil) vs, is = path.AppendVerticesAndIndicesForFilling(nil, nil)
@ -211,6 +213,7 @@ func drawArc(screen *ebiten.Image, count int, scale float32, line bool) {
if line { if line {
op := &vector.StrokeOptions{} op := &vector.StrokeOptions{}
op.Width = 5 op.Width = 5
op.LineJoin = vector.LineJoinRound
vs, is = path.AppendVerticesAndIndicesForStroke(nil, nil, op) vs, is = path.AppendVerticesAndIndicesForStroke(nil, nil, op)
} else { } else {
vs, is = path.AppendVerticesAndIndicesForFilling(nil, nil) vs, is = path.AppendVerticesAndIndicesForFilling(nil, nil)
@ -267,6 +270,7 @@ func drawWave(screen *ebiten.Image, counter int, scale float32, line bool) {
if line { if line {
op := &vector.StrokeOptions{} op := &vector.StrokeOptions{}
op.Width = 5 op.Width = 5
op.LineJoin = vector.LineJoinRound
vs, is = path.AppendVerticesAndIndicesForStroke(nil, nil, op) vs, is = path.AppendVerticesAndIndicesForStroke(nil, nil, op)
} else { } else {
vs, is = path.AppendVerticesAndIndicesForFilling(nil, nil) vs, is = path.AppendVerticesAndIndicesForFilling(nil, nil)

View File

@ -72,18 +72,35 @@ func (p *Path) QuadTo(x1, y1, x2, y2 float32) {
p.quadTo(x1, y1, x2, y2, 0) p.quadTo(x1, y1, x2, y2, 0)
} }
// 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
a = p1.y - p0.y
b = -(p1.x - p0.x)
c = (p1.x-p0.x)*p0.y - (p1.y-p0.y)*p0.x
return
}
// isPointCloseToSegment detects the distance between a segment (x0, y0)-(x1, y1) and a point (x, y) is less than allow. // isPointCloseToSegment detects the distance between a segment (x0, y0)-(x1, y1) and a point (x, y) is less than allow.
func isPointCloseToSegment(x, y, x0, y0, x1, y1 float32, allow float32) bool { func isPointCloseToSegment(x, y, x0, y0, x1, y1 float32, allow float32) bool {
// Line passing through (x0, y0) and (x1, y1) in the form of ax + by + c = 0 a, b, c := lineForTwoPoints(point{x: x0, y: y0}, point{x: x1, y: y1})
a := y1 - y0
b := -(x1 - x0)
c := (x1-x0)*y0 - (y1-y0)*x0
// The distance between a line ax+by+c=0 and (x0, y0) is // The distance between a line ax+by+c=0 and (x0, y0) is
// |ax0 + by0 + c| / √(a² + b²) // |ax0 + by0 + c| / √(a² + b²)
return allow*allow*(a*a+b*b) > (a*x+b*y+c)*(a*x+b*y+c) return allow*allow*(a*a+b*b) > (a*x+b*y+c)*(a*x+b*y+c)
} }
// crossingPointForTwoLines returns a crossing point for two lines.
func crossingPointForTwoLines(p00, p01, p10, p11 point) point {
a0, b0, c0 := lineForTwoPoints(p00, p01)
a1, b1, c1 := lineForTwoPoints(p10, p11)
det := a0*b1 - a1*b0
return point{
x: (b0*c1 - b1*c0) / det,
y: (a1*c0 - a0*c1) / det,
}
}
func (p *Path) quadTo(x1, y1, x2, y2 float32, level int) { func (p *Path) quadTo(x1, y1, x2, y2 float32, level int) {
if level > 10 { if level > 10 {
return return
@ -313,10 +330,23 @@ func (p *Path) AppendVerticesAndIndicesForFilling(vertices []ebiten.Vertex, indi
return vertices, indices return vertices, indices
} }
// LineJoin represents the way in which how two segments are joined.
type LineJoin int
const (
LineJoinMiter LineJoin = iota
LineJoinBevel
LineJoinRound
)
// StokeOptions is options to render a stroke. // StokeOptions is options to render a stroke.
type StrokeOptions struct { type StrokeOptions struct {
// Width is the stroke width in pixels. // Width is the stroke width in pixels.
Width float32 Width float32
// LineJoin is the way in which how two segments are joined.
// The default (zero) value is LineJoiMiter.
LineJoin LineJoin
} }
// AppendVerticesAndIndicesForStroke appends vertices and indices to render a stroke of this path and returns them. // AppendVerticesAndIndicesForStroke appends vertices and indices to render a stroke of this path and returns them.
@ -327,6 +357,10 @@ type StrokeOptions struct {
// //
// The returned values are intended to be passed to DrawTriangles or DrawTrianglesShader with FillAll fill mode, not EvenOdd fill mode. // The returned values are intended to be passed to DrawTriangles or DrawTrianglesShader with FillAll fill mode, not EvenOdd fill mode.
func (p *Path) AppendVerticesAndIndicesForStroke(vertices []ebiten.Vertex, indices []uint16, op *StrokeOptions) ([]ebiten.Vertex, []uint16) { func (p *Path) AppendVerticesAndIndicesForStroke(vertices []ebiten.Vertex, indices []uint16, op *StrokeOptions) ([]ebiten.Vertex, []uint16) {
if op == nil {
return vertices, indices
}
for _, seg := range p.segs { for _, seg := range p.segs {
if len(seg) < 2 { if len(seg) < 2 {
continue continue
@ -406,14 +440,53 @@ func (p *Path) AppendVerticesAndIndicesForStroke(vertices []ebiten.Vertex, indic
continue continue
} }
var arc Path switch op.LineJoin {
arc.MoveTo(c.x, c.y) case LineJoinMiter:
if da < math.Pi { // TODO: Enable to configure this.
arc.Arc(c.x, c.y, op.Width/2, a0, a1, Clockwise) const miterLimit = 10
} else { delta := math.Pi - da
arc.Arc(c.x, c.y, op.Width/2, a0+math.Pi, a1+math.Pi, CounterClockwise) exceed := math.Abs(1/math.Sin(float64(delta/2))) > miterLimit
var quad Path
quad.MoveTo(c.x, c.y)
if da < math.Pi {
quad.LineTo(rect[1].x, rect[1].y)
if !exceed {
pt := crossingPointForTwoLines(rect[0], rect[1], nextRect[0], nextRect[1])
quad.LineTo(pt.x, pt.y)
}
quad.LineTo(nextRect[0].x, nextRect[0].y)
} else {
quad.LineTo(rect[3].x, rect[3].y)
if !exceed {
pt := crossingPointForTwoLines(rect[2], rect[3], nextRect[2], nextRect[3])
quad.LineTo(pt.x, pt.y)
}
quad.LineTo(nextRect[2].x, nextRect[2].y)
}
vertices, indices = quad.AppendVerticesAndIndicesForFilling(vertices, indices)
case LineJoinBevel:
var tri Path
tri.MoveTo(c.x, c.y)
if da < math.Pi {
tri.LineTo(rect[1].x, rect[1].y)
tri.LineTo(nextRect[0].x, nextRect[0].y)
} else {
tri.LineTo(rect[3].x, rect[3].y)
tri.LineTo(nextRect[2].x, nextRect[2].y)
}
vertices, indices = tri.AppendVerticesAndIndicesForFilling(vertices, indices)
case LineJoinRound:
var arc Path
arc.MoveTo(c.x, c.y)
if da < math.Pi {
arc.Arc(c.x, c.y, op.Width/2, a0, a1, Clockwise)
} else {
arc.Arc(c.x, c.y, op.Width/2, a0+math.Pi, a1+math.Pi, CounterClockwise)
}
vertices, indices = arc.AppendVerticesAndIndicesForFilling(vertices, indices)
} }
vertices, indices = arc.AppendVerticesAndIndicesForFilling(vertices, indices)
} }
} }
return vertices, indices return vertices, indices