mirror of
https://github.com/hajimehoshi/ebiten.git
synced 2024-12-24 10:48:53 +01:00
parent
f5ae18d6f6
commit
a1a598471b
176
examples/lines/main.go
Normal file
176
examples/lines/main.go
Normal 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)
|
||||
}
|
||||
}
|
@ -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)
|
||||
|
@ -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,6 +440,44 @@ func (p *Path) AppendVerticesAndIndicesForStroke(vertices []ebiten.Vertex, indic
|
||||
continue
|
||||
}
|
||||
|
||||
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 {
|
||||
@ -416,5 +488,6 @@ func (p *Path) AppendVerticesAndIndicesForStroke(vertices []ebiten.Vertex, indic
|
||||
vertices, indices = arc.AppendVerticesAndIndicesForFilling(vertices, indices)
|
||||
}
|
||||
}
|
||||
}
|
||||
return vertices, indices
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user