From a1a598471ba8f6e2daba361e54b9e927ffb5faa8 Mon Sep 17 00:00:00 2001 From: Hajime Hoshi Date: Fri, 14 Oct 2022 19:28:01 +0900 Subject: [PATCH] vector: add LineJoin Updates #1843 --- examples/lines/main.go | 176 ++++++++++++++++++++++++++++++++++++++++ examples/vector/main.go | 4 + vector/path.go | 95 +++++++++++++++++++--- 3 files changed, 264 insertions(+), 11 deletions(-) create mode 100644 examples/lines/main.go diff --git a/examples/lines/main.go b/examples/lines/main.go new file mode 100644 index 000000000..5742e286a --- /dev/null +++ b/examples/lines/main.go @@ -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) + } +} diff --git a/examples/vector/main.go b/examples/vector/main.go index b168060d3..834357e0e 100644 --- a/examples/vector/main.go +++ b/examples/vector/main.go @@ -118,6 +118,7 @@ func drawEbitenText(screen *ebiten.Image, x, y int, scale float32, line bool) { if line { op := &vector.StrokeOptions{} op.Width = 5 + op.LineJoin = vector.LineJoinRound vs, is = path.AppendVerticesAndIndicesForStroke(nil, nil, op) } else { vs, is = path.AppendVerticesAndIndicesForFilling(nil, nil) @@ -170,6 +171,7 @@ func drawEbitenLogo(screen *ebiten.Image, x, y int, scale float32, line bool) { if line { op := &vector.StrokeOptions{} op.Width = 5 + op.LineJoin = vector.LineJoinRound vs, is = path.AppendVerticesAndIndicesForStroke(nil, nil, op) } else { vs, is = path.AppendVerticesAndIndicesForFilling(nil, nil) @@ -211,6 +213,7 @@ func drawArc(screen *ebiten.Image, count int, scale float32, line bool) { if line { op := &vector.StrokeOptions{} op.Width = 5 + op.LineJoin = vector.LineJoinRound vs, is = path.AppendVerticesAndIndicesForStroke(nil, nil, op) } else { vs, is = path.AppendVerticesAndIndicesForFilling(nil, nil) @@ -267,6 +270,7 @@ func drawWave(screen *ebiten.Image, counter int, scale float32, line bool) { if line { op := &vector.StrokeOptions{} op.Width = 5 + op.LineJoin = vector.LineJoinRound vs, is = path.AppendVerticesAndIndicesForStroke(nil, nil, op) } else { vs, is = path.AppendVerticesAndIndicesForFilling(nil, nil) diff --git a/vector/path.go b/vector/path.go index ed542d6f1..349558954 100644 --- a/vector/path.go +++ b/vector/path.go @@ -72,18 +72,35 @@ func (p *Path) QuadTo(x1, y1, x2, y2 float32) { 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. 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 := y1 - y0 - b := -(x1 - x0) - c := (x1-x0)*y0 - (y1-y0)*x0 + a, b, c := lineForTwoPoints(point{x: x0, y: y0}, point{x: x1, y: y1}) // The distance between a line ax+by+c=0 and (x0, y0) is // |ax0 + by0 + c| / √(a² + b²) 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) { if level > 10 { return @@ -313,10 +330,23 @@ func (p *Path) AppendVerticesAndIndicesForFilling(vertices []ebiten.Vertex, indi 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. type StrokeOptions struct { // Width is the stroke width in pixels. 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. @@ -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. 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 { if len(seg) < 2 { continue @@ -406,14 +440,53 @@ func (p *Path) AppendVerticesAndIndicesForStroke(vertices []ebiten.Vertex, indic continue } - 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) + switch op.LineJoin { + case LineJoinMiter: + // TODO: Enable to configure this. + const miterLimit = 10 + delta := math.Pi - da + 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