diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 8059106a..089e2186 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -1,15 +1,58 @@ name: Go on: [push] jobs: - test-arm: - name: Test (arm) + test-windows-x64: + name: Test (windows amd64) + runs-on: [self-hosted, windows, x64] + steps: + + - name: Set up Go 1.18 + uses: actions/setup-go@v1 + with: + go-version: 1.18 + id: go + + - name: Check out code into the Go module directory + uses: actions/checkout@v1 + + - name: Get dependencies + run: | + go get -v -t -d ./... + + - name: Test + run: ./test_examples.sh + shell: bash + + test-linux-arm: + name: Test (linux arm) runs-on: [self-hosted, linux, ARM64] steps: - - name: Set up Go 1.17 + - name: Set up Go 1.18 + uses: actions/setup-go@v1 + with: + go-version: 1.18 + id: go + + - name: Check out code into the Go module directory + uses: actions/checkout@v1 + + - name: Get dependencies + run: | + go get -v -t -d ./... + + - name: Test + run: ./test_examples.sh + + test-linux-x64: + name: Test (linux x64) + runs-on: [self-hosted, linux, x64] + steps: + + - name: Set up Go 1.18 uses: actions/setup-go@v1 with: - go-version: 1.17 + go-version: 1.18 id: go - name: Check out code into the Go module directory @@ -27,10 +70,10 @@ jobs: runs-on: ubuntu-latest steps: - - name: Set up Go 1.17 + - name: Set up Go 1.18 uses: actions/setup-go@v1 with: - go-version: 1.17 + go-version: 1.18 id: go - name: Check out code into the Go module directory diff --git a/.gitignore b/.gitignore index 39182116..1415900b 100644 --- a/.gitignore +++ b/.gitignore @@ -14,4 +14,7 @@ # Output of the go coverage tool, specifically when used with LiteIDE *.out -coverage.txt \ No newline at end of file +coverage.txt + +# Workspace configuration +.vscode \ No newline at end of file diff --git a/README.md b/README.md index 43c6944a..a87a843b 100644 --- a/README.md +++ b/README.md @@ -2,8 +2,8 @@ ## A Pure Go game engine -[![Go Reference](https://pkg.go.dev/badge/github.com/oakmound/oak/v3.svg)](https://pkg.go.dev/github.com/oakmound/oak/v3) -[![Code Coverage](https://codecov.io/gh/oakmound/oak/branch/develop/graph/badge.svg)](https://codecov.io/gh/oakmound/oak) +[![Go Reference](https://pkg.go.dev/badge/github.com/oakmound/oak/v4.svg)](https://pkg.go.dev/github.com/oakmound/oak/v4) +[![Code Coverage](https://codecov.io/gh/oakmound/oak/branch/master/graph/badge.svg)](https://codecov.io/gh/oakmound/oak) [![Mentioned in Awesome Go](https://awesome.re/mentioned-badge-flat.svg)](https://github.com/avelino/awesome-go) ## Table of Contents @@ -24,14 +24,14 @@ ## Installation -`go get -u github.com/oakmound/oak/v3` +`go get -u github.com/oakmound/oak/v4` ## Features and Systems 1. Window Management - Windows and key events forked from [shiny](https://pkg.go.dev/golang.org/x/exp/shiny) - Support for multiple windows running at the same time -1. [Image Rendering](https://pkg.go.dev/github.com/oakmound/oak/v3/render) +1. [Image Rendering](https://pkg.go.dev/github.com/oakmound/oak/v4/render) - Manipulation - `render.Modifiable` interface - Integrated with optimized image manipulation via [gift](https://github.com/disintegration/gift) @@ -40,22 +40,22 @@ - Primitive builders, `ColorBox`, `Line`, `Bezier` - History-tracking `Reverting` - Primarily 2D -1. [Particle System](https://pkg.go.dev/github.com/oakmound/oak/v3/render/particle) -1. [Mouse Handling](https://pkg.go.dev/github.com/oakmound/oak/v3/mouse) -1. [Joystick Support](https://pkg.go.dev/github.com/oakmound/oak/v3/joystick) -1. [Audio Support](https://pkg.go.dev/github.com/oakmound/oak/v3/audio) -1. [Collision](https://pkg.go.dev/github.com/oakmound/oak/v3/collision) +1. [Particle System](https://pkg.go.dev/github.com/oakmound/oak/v4/render/particle) +1. [Mouse Handling](https://pkg.go.dev/github.com/oakmound/oak/v4/mouse) +1. [Joystick Support](https://pkg.go.dev/github.com/oakmound/oak/v4/joystick) +1. [Audio Support](https://pkg.go.dev/github.com/oakmound/oak/v4/audio) +1. [Collision](https://pkg.go.dev/github.com/oakmound/oak/v4/collision) - Collision R-Tree forked from [rtreego](https://github.com/dhconnelly/rtreego) - - [2D Raycasting](https://pkg.go.dev/github.com/oakmound/oak/v3/collision/ray) + - [2D Raycasting](https://pkg.go.dev/github.com/oakmound/oak/v4/collision/ray) - Collision Spaces - Attachable to Objects - Auto React to collisions through events -1. [2D Physics System](https://pkg.go.dev/github.com/oakmound/oak/v3/physics) -1. [Event Handler](https://pkg.go.dev/github.com/oakmound/oak/v3/event) +1. [2D Physics System](https://pkg.go.dev/github.com/oakmound/oak/v4/physics) +1. [Event Handler](https://pkg.go.dev/github.com/oakmound/oak/v4/event) ## Support -For discussions not significant enough to be an Issue or PR, feel free to ping us in the #oak channel on the [gophers slack](https://invite.slack.golangbridge.org/). +For discussions not significant enough to be an Issue or PR, feel free to ping us in the #oak channel on the [gophers slack](https://invite.slack.golangbridge.org/). For insight into what is happening in oak see the [blog](https://200sc.dev/). ## Quick Start @@ -65,8 +65,8 @@ This is an example of the most basic oak program: package main import ( - "github.com/oakmound/oak/v3" - "github.com/oakmound/oak/v3/scene" + "github.com/oakmound/oak/v4" + "github.com/oakmound/oak/v4/scene" ) func main() { @@ -79,27 +79,28 @@ func main() { } ``` -See below or the [examples](examples) folder for longer demos, [godoc](https://pkg.go.dev/github.com/oakmound/oak/v3) for reference documentation, and the [wiki](https://github.com/oakmound/oak/wiki) for more guided tutorials and walkthroughs. +See below or navigate to the [examples](examples) folder for demos. For more examples and documentation checkout [godoc](https://pkg.go.dev/github.com/oakmound/oak/v4) for reference documentation, the [wiki](https://github.com/oakmound/oak/wiki), or our extended features in [grove](https://github.com/oakmound/grove). ## Examples | | | | |:-------------------------:|:-------------------------:|:-------------------------:| -| [Platformer](examples/platformer-tutorial) | [Top down shooter](examples/top-down-shooter-tutorial)| [Radar](examples/radar-demo) | -| [Slideshow](examples/slide) | [Bezier Curves](examples/bezier) | [Joysticks](examples/joystick-viz)| -| [Collision Demo](examples/collision-demo) | [Custom Mouse Cursor](examples/custom-cursor) | [Fallback Fonts](examples/fallback-font)| -| [Screen Options](examples/screenopts) | [Multi Window](examples/multi-window) | [Particle Demo](examples/particle-demo)| +| [Platformer](examples/platformer) | [Top down shooter](examples/top-down-shooter)| [Flappy Bird](examples/flappy-bird/) +| [Bezier Curves](examples/bezier) | [Joysticks](examples/joystick-viz)| [Piano](examples/piano)| +| [Screen Options](examples/screenopts) | [Multi Window](examples/multi-window) | [Particles](examples/particle-demo)| -## Games using Oak +## Games using Oak + +To kick off a larger game project you can get started with [game-template](https://github.com/oakmound/game-template). | | | |:-------------------------:|:-------------------------:| | [Agent Blue](https://oakmound.itch.io/agent-blue) | [Fantastic Doctor](https://github.com/oakmound/lowrez17) | [Hiring Now: Looters](https://oakmound.itch.io/cheststacker) | [Jeremy The Clam](https://github.com/200sc/jeremy) -| [Diamond Deck Championship](https://oakmound.itch.io/diamond-deck-championship) | +| [Diamond Deck Championship](https://oakmound.itch.io/diamond-deck-championship) | [SokoPic](https://oakmound.itch.io/sokopic) ## On Pure Go Oak has recently brought in dependencies that include C code, but we still describe the engine as a Pure Go engine, which at face value seems contradictory. Oak's goal is that, by default, a user can pull down the engine and create a fully functional game or GUI application on a machine with no C compiler installed, so when we say Pure Go we mean that, by default, the library is configured so no C compilation is required, and that no major features are locked behind C compliation. -We anticipate in the immediate future needing to introduce alternate drivers that include C dependencies for performance improvements in some scenarios, and currently we have no OSX solution that lacks objective C code. +We anticipate in the immediate future needing to introduce alternate drivers that include C dependencies for performance improvements in some scasenarios, and currently we have no OSX solution that lacks objective C code. diff --git a/alg/floatgeom/point.go b/alg/floatgeom/point.go index 7335f1ea..2edda851 100644 --- a/alg/floatgeom/point.go +++ b/alg/floatgeom/point.go @@ -3,7 +3,7 @@ package floatgeom import ( "math" - "github.com/oakmound/oak/v3/alg" + "github.com/oakmound/oak/v4/alg" ) // Point2 represents a 2D point on a plane. diff --git a/alg/floatgeom/point_test.go b/alg/floatgeom/point_test.go index 49617207..a0b0c976 100644 --- a/alg/floatgeom/point_test.go +++ b/alg/floatgeom/point_test.go @@ -6,7 +6,7 @@ import ( "testing" "time" - "github.com/oakmound/oak/v3/alg" + "github.com/oakmound/oak/v4/alg" ) func Seed() { diff --git a/alg/floatgeom/polygon.go b/alg/floatgeom/polygon.go index 92ccca3a..a257dcf9 100644 --- a/alg/floatgeom/polygon.go +++ b/alg/floatgeom/polygon.go @@ -1,7 +1,7 @@ package floatgeom import ( - "github.com/oakmound/oak/v3/alg" + "github.com/oakmound/oak/v4/alg" ) // A Polygon2 is a series of points in 2D space. diff --git a/alg/intgeom/point.go b/alg/intgeom/point.go index d8038baf..d95c07dc 100644 --- a/alg/intgeom/point.go +++ b/alg/intgeom/point.go @@ -3,7 +3,7 @@ package intgeom import ( "math" - "github.com/oakmound/oak/v3/alg" + "github.com/oakmound/oak/v4/alg" ) // Point2 represents a 2D point in space. diff --git a/alg/intgeom/point_test.go b/alg/intgeom/point_test.go index 6e28860d..a9869f40 100644 --- a/alg/intgeom/point_test.go +++ b/alg/intgeom/point_test.go @@ -6,7 +6,7 @@ import ( "testing" "time" - "github.com/oakmound/oak/v3/alg" + "github.com/oakmound/oak/v4/alg" ) func Seed() { diff --git a/alg/intgeom/rect.go b/alg/intgeom/rect.go index f7535205..43d0cd98 100644 --- a/alg/intgeom/rect.go +++ b/alg/intgeom/rect.go @@ -1,5 +1,11 @@ package intgeom +import ( + "math/rand" + + "github.com/oakmound/oak/v4/alg/span" +) + // A Rect2 represents a span from one point in 2D space to another. // If Min is less than max on any axis, it will return undefined results // for methods. @@ -326,3 +332,93 @@ func (r Rect2) Intersects(r2 Rect2) bool { return !((r2.Max.X() <= r.Min.X() || r.Max.X() <= r2.Min.X()) || (r2.Max.Y() <= r.Min.Y() || r.Max.Y() <= r2.Min.Y())) } + +// MulConst multiplies the boundary points of this rectangle by i. +func (r Rect2) MulConst(i int) Rect2 { + return Rect2{ + r.Min.MulConst(i), + r.Max.MulConst(i), + } +} + +// Poll returns a pseudorandom point from within this rectangle +func (r Rect2) Poll() Point2 { + return Point2{ + r.Min.X() + int(rand.Float64()*float64(r.W())), + r.Min.Y() + int(rand.Float64()*float64(r.H())), + } +} + +// Clamp returns a version of the provided point such that it is contained within r. If it was already contained in +// r, it will not be changed. +func (r Rect2) Clamp(pt Point2) Point2 { + for i := 0; i < r.MaxDimensions(); i++ { + if pt[i] < r.Min[i] { + pt[i] = r.Min[i] + } else if pt[i] > r.Max[i] { + pt[i] = r.Max[i] + } + } + return pt +} + +// Percentile returns a point within this rectangle along the vector from the top left to the bottom right of the +// rectangle, where for example, 0.0 will be r.Min, 1.0 will be r.Max, and 2.0 will be project the vector beyond r +// and return r.Min + {r.W()*2, r.H()*2} +func (r Rect2) Percentile(f float64) Point2 { + return Point2{ + r.Min.X() + int(f*float64(r.W())), + r.Min.Y() + int(f*float64(r.H())), + } +} + +// MulSpan returns this rectangle as a Point2 Span after multiplying the boundary points of the rectangle by f. +func (r Rect2) MulSpan(f float64) span.Span[Point2] { + return r.MulConst(int(f)) +} + +// MulConst multiplies the boundary points of this rectangle by i. +func (r Rect3) MulConst(i int) Rect3 { + return Rect3{ + r.Min.MulConst(i), + r.Max.MulConst(i), + } +} + +// Poll returns a pseudorandom point from within this rectangle +func (r Rect3) Poll() Point3 { + return Point3{ + r.Min.X() + int(rand.Float64()*float64(r.W())), + r.Min.Y() + int(rand.Float64()*float64(r.H())), + r.Min.Z() + int(rand.Float64()*float64(r.D())), + } +} + +// Clamp returns a version of the provided point such that it is contained within r. If it was already contained in +// r, it will not be changed. +func (r Rect3) Clamp(pt Point3) Point3 { + for i := 0; i < r.MaxDimensions(); i++ { + if pt[i] < r.Min[i] { + pt[i] = r.Min[i] + } else if pt[i] > r.Max[i] { + pt[i] = r.Max[i] + } + } + return pt +} + +// Percentile returns a point within this rectangle along the vector from the top left to the bottom right of the +// rectangle, where for example, 0.0 will be r.Min, 1.0 will be r.Max, and 2.0 will be project the vector beyond r +// and return r.Min + {r.W()*2, r.H()*2, r.D()*2} +func (r Rect3) Percentile(f float64) Point3 { + return Point3{ + r.Min.X() + int(f*float64(r.W())), + r.Min.Y() + int(f*float64(r.H())), + r.Min.Z() + int(f*float64(r.D())), + } +} + +// MulConst multiplies the boundary points of this rectangle by i. +func (r Rect3) MulSpan(f float64) span.Span[Point3] { + return r.MulConst(int(f)) +} diff --git a/alg/intgeom/rect_test.go b/alg/intgeom/rect_test.go index b6e12326..b3753f89 100644 --- a/alg/intgeom/rect_test.go +++ b/alg/intgeom/rect_test.go @@ -239,3 +239,67 @@ func TestRect3GreaterOf(t *testing.T) { } } } + +func TestRect2Span(t *testing.T) { + t.Run("Basic", func(t *testing.T) { + r := NewRect2WH(1, 1, 9, 9) + p1 := r.Percentile(1.0) + if p1 != r.Max { + t.Errorf("Percentile(1.0) did not return max point: got %v expected %v", p1, r.Max) + } + p2 := r.Percentile(0.0) + if p2 != r.Min { + t.Errorf("Percentile(0.0) did not return min point: got %v expected %v", p2, r.Min) + } + const pollTries = 100 + for i := 0; i < pollTries; i++ { + if !r.Contains(r.Poll()) { + t.Fatalf("polled point did not lie within the creating rectangle") + } + } + p3 := r.Clamp(Point2{0, 5}) + if p3 != (Point2{1, 5}) { + t.Errorf("Clamp(0,5) did not return {1,5}: got %v", p3) + } + p4 := r.Clamp(Point2{2, 11}) + if p4 != (Point2{2, 10}) { + t.Errorf("Clamp(2,11) did not return {2,10}: got %v", p4) + } + r2 := r.MulSpan(4) + if r2 != NewRect2(4, 4, 40, 40) { + t.Errorf("MulSpan did not return {4,4,40,40}: got %v", r2) + } + }) +} + +func TestRect3Span(t *testing.T) { + t.Run("Basic", func(t *testing.T) { + r := NewRect3WH(1, 1, 1, 9, 9, 9) + p1 := r.Percentile(1.0) + if p1 != r.Max { + t.Errorf("Percentile(1.0) did not return max point: got %v expected %v", p1, r.Max) + } + p2 := r.Percentile(0.0) + if p2 != r.Min { + t.Errorf("Percentile(0.0) did not return min point: got %v expected %v", p2, r.Min) + } + const pollTries = 100 + for i := 0; i < pollTries; i++ { + if !r.Contains(r.Poll()) { + t.Fatalf("polled point did not lie within the creating rectangle") + } + } + p3 := r.Clamp(Point3{0, -1, 5}) + if p3 != (Point3{1, 1, 5}) { + t.Errorf("Clamp(0,-1,5) did not return {1,1,5}: got %v", p3) + } + p4 := r.Clamp(Point3{20, 2, 11}) + if p4 != (Point3{10, 2, 10}) { + t.Errorf("Clamp(20, 2,11) did not return {10,2,10}: got %v", p4) + } + r2 := r.MulSpan(4) + if r2 != NewRect3(4, 4, 4, 40, 40, 40) { + t.Errorf("MulSpan did not return {4,4,4,40,40,40}: got %v", r2) + } + }) +} diff --git a/alg/range/colorrange/linear.go b/alg/range/colorrange/linear.go deleted file mode 100644 index 5dcc1940..00000000 --- a/alg/range/colorrange/linear.go +++ /dev/null @@ -1,57 +0,0 @@ -package colorrange - -import ( - "image/color" - - "github.com/oakmound/oak/v3/alg/range/intrange" -) - -// linear color ranges return colors on a linear distribution -type linear struct { - r, g, b, a intrange.Range -} - -// NewLinear returns a linear color distribution between min and maxColor -func NewLinear(minColor, maxColor color.Color) Range { - r, g, b, a := minColor.RGBA() - r2, g2, b2, a2 := maxColor.RGBA() - return linear{ - intrange.NewLinear(int(r), int(r2)), - intrange.NewLinear(int(g), int(g2)), - intrange.NewLinear(int(b), int(b2)), - intrange.NewLinear(int(a), int(a2)), - } -} - -// EnforceRange rounds the input color's components so that they fall in the -// given range. -func (l linear) EnforceRange(c color.Color) color.Color { - r3, g3, b3, a3 := c.RGBA() - r4 := l.r.EnforceRange(int(r3)) - g4 := l.g.EnforceRange(int(g3)) - b4 := l.b.EnforceRange(int(b3)) - a4 := l.a.EnforceRange(int(a3)) - return rgbaFromInts(r4, g4, b4, a4) -} - -// Poll returns a randomly chosen color in the bounds of this color range -func (l linear) Poll() color.Color { - r3 := l.r.Poll() - g3 := l.g.Poll() - b3 := l.b.Poll() - a3 := l.a.Poll() - return rgbaFromInts(r3, g3, b3, a3) -} - -// Percentile returns a color f percent along the color range -func (l linear) Percentile(f float64) color.Color { - r3 := l.r.Percentile(f) - g3 := l.g.Percentile(f) - b3 := l.b.Percentile(f) - a3 := l.a.Percentile(f) - return rgbaFromInts(r3, g3, b3, a3) -} - -func rgbaFromInts(r, g, b, a int) color.RGBA { - return color.RGBA{uint8(r / 257), uint8(g / 257), uint8(b / 257), uint8(a / 257)} -} diff --git a/alg/range/colorrange/linear_test.go b/alg/range/colorrange/linear_test.go deleted file mode 100644 index 561da66c..00000000 --- a/alg/range/colorrange/linear_test.go +++ /dev/null @@ -1,45 +0,0 @@ -package colorrange - -import ( - "image/color" - "math/rand" - "testing" -) - -func TestLinear(t *testing.T) { - rng := NewLinear(color.RGBA{255, 255, 255, 255}, color.RGBA{255, 255, 255, 255}) - if rng.Poll() != (color.RGBA{255, 255, 255, 255}) { - t.Fatal("false linear range did not return only possible value on Poll") - } - for i := 0; i < 100; i++ { - if rng.Percentile(rand.Float64()) != (color.RGBA{255, 255, 255, 255}) { - t.Fatal("false linear range did not return only possible value on Percentile") - } - } - rng = NewLinear(color.RGBA{0, 0, 0, 255}, color.RGBA{255, 255, 255, 255}) - for i := 0.0; i < 255; i++ { - p := i / 255 - uinti := uint8(i) - if rng.Percentile(p) != (color.RGBA{uinti, uinti, uinti, 255}) { - t.Fatal("linear color range did not return appropriate scaled color, bottom to top") - } - } - rng = NewLinear(color.RGBA{255, 255, 255, 255}, color.RGBA{0, 0, 0, 255}) - for i := 255.0; i > 0; i-- { - p := (255 - i) / 255 - uinti := uint8(i) - if rng.Percentile(p) != (color.RGBA{uinti, uinti, uinti, 255}) { - t.Fatal("linear color range did not return appropriate scaled color, top to bottom") - } - } - rng = NewLinear(color.RGBA{125, 125, 125, 125}, color.RGBA{200, 200, 200, 200}) - if rng.EnforceRange(color.RGBA{100, 100, 100, 100}) != (color.RGBA{125, 125, 125, 125}) { - t.Fatal("linear color range did not enforce minimum color") - } - if rng.EnforceRange(color.RGBA{225, 225, 225, 225}) != (color.RGBA{200, 200, 200, 200}) { - t.Fatal("linear color range did not enforce maximum color") - } - if rng.EnforceRange(color.RGBA{175, 175, 175, 175}) != (color.RGBA{175, 175, 175, 175}) { - t.Fatal("linear color range did not pass through value within range") - } -} diff --git a/alg/range/colorrange/range.go b/alg/range/colorrange/range.go deleted file mode 100644 index d609a7bd..00000000 --- a/alg/range/colorrange/range.go +++ /dev/null @@ -1,13 +0,0 @@ -// Package colorrange provides distributions that accept and return color.Colors. -package colorrange - -import ( - "image/color" -) - -// Range represents a range of colors -type Range interface { - Poll() color.Color - EnforceRange(color.Color) color.Color - Percentile(f float64) color.Color -} diff --git a/alg/range/floatrange/constant.go b/alg/range/floatrange/constant.go deleted file mode 100644 index a2514a48..00000000 --- a/alg/range/floatrange/constant.go +++ /dev/null @@ -1,30 +0,0 @@ -package floatrange - -// constant is a range that represents some constant float -type constant float64 - -// NewConstant returns a range that will always poll to return f -func NewConstant(f float64) Range { - return constant(f) -} - -// Poll returns the float behind the constant -func (c constant) Poll() float64 { - return float64(c) -} - -// Mult scales the constant by f -func (c constant) Mult(f float64) Range { - c = constant(float64(c) * f) - return c -} - -// EnforceRange returns the float behind the constant -func (c constant) EnforceRange(float64) float64 { - return float64(c) -} - -// Percentile returns the float behind the constant -func (c constant) Percentile(float64) float64 { - return float64(c) -} diff --git a/alg/range/floatrange/constant_test.go b/alg/range/floatrange/constant_test.go deleted file mode 100644 index cfeebc9c..00000000 --- a/alg/range/floatrange/constant_test.go +++ /dev/null @@ -1,32 +0,0 @@ -package floatrange - -import ( - "math/rand" - "testing" - "time" -) - -func TestConstant(t *testing.T) { - rand.Seed(time.Now().Unix()) - const testCount = 100 - const maxInt = 100000 - const minInt = -100000 - for i := 0; i < testCount; i++ { - val := rand.Float64()*(maxInt-minInt) + minInt - cons := NewConstant(val) - if cons.Poll() != val { - t.Fatal("Constant.Poll did not return initialized value") - } - magnitude := rand.Float64() - cons2 := cons.Mult(magnitude) - if cons2.Poll() != float64(val)*magnitude { - t.Fatal("Constant.Mult result did not match expected Poll") - } - if cons.EnforceRange(rand.Float64()*(maxInt-minInt)+minInt) != val { - t.Fatal("Constant.EnforceRange did not return initialized value") - } - if cons.Percentile(rand.Float64()) != val { - t.Fatal("Constant.Percentile did not return initialized value") - } - } -} diff --git a/alg/range/floatrange/infinite.go b/alg/range/floatrange/infinite.go deleted file mode 100644 index 725bf402..00000000 --- a/alg/range/floatrange/infinite.go +++ /dev/null @@ -1,31 +0,0 @@ -package floatrange - -import "math" - -// Infinite is an immutable range that will always return math.MaxFloat64 -type Infinite struct{} - -// NewInfinite returns an infinite. -func NewInfinite() Range { - return Infinite{} -} - -// Poll returns MaxFloat64 on an infinite -func (i Infinite) Poll() float64 { - return math.MaxFloat64 -} - -// Mult returns an infinite from an infinite. -func (i Infinite) Mult(f float64) Range { - return i -} - -// EnforceRange returns math.MaxFloat64 -func (i Infinite) EnforceRange(f float64) float64 { - return math.MaxFloat64 -} - -// Percentile returns the float behind the constant -func (i Infinite) Percentile(float64) float64 { - return math.MaxFloat64 -} diff --git a/alg/range/floatrange/infinite_test.go b/alg/range/floatrange/infinite_test.go deleted file mode 100644 index 0f7e81b2..00000000 --- a/alg/range/floatrange/infinite_test.go +++ /dev/null @@ -1,26 +0,0 @@ -package floatrange - -import ( - "math" - "math/rand" - "testing" - "time" -) - -func TestInfinite(t *testing.T) { - rand.Seed(time.Now().Unix()) - inf := NewInfinite() - if inf.Poll() != math.MaxFloat64 { - t.Fatal("infinite.Poll did not return math.MaxFloat64") - } - inf2 := inf.Mult(rand.Float64()) - if inf2 != inf { - t.Fatal("base infinite did not match multiplied infinite") - } - if inf.EnforceRange(rand.Float64()*10000) != math.MaxFloat64 { - t.Fatal("infinite.EnforceRange did not return math.MaxFloat64") - } - if inf.Percentile(rand.Float64()) != math.MaxFloat64 { - t.Fatal("infinite.Percentile did not return math.MaxFloat64") - } -} diff --git a/alg/range/floatrange/linear.go b/alg/range/floatrange/linear.go deleted file mode 100644 index ce24e1b4..00000000 --- a/alg/range/floatrange/linear.go +++ /dev/null @@ -1,73 +0,0 @@ -package floatrange - -import ( - "math/rand" - - "github.com/oakmound/oak/v3/alg/range/internal/random" -) - -// NewSpread returns a linear range from base-spread to base+spread -func NewSpread(base, spread float64) Range { - if spread == 0 { - return constant(base) - } - return linear{ - Min: base - spread, - Max: base + spread, - rng: random.Rand(), - } -} - -// NewLinear returns a linear range from min to max -func NewLinear(min, max float64) Range { - if max == min { - return constant(min) - } - flipped := false - if max < min { - max, min = min, max - flipped = true - } - return linear{ - Min: min, - Max: max, - rng: random.Rand(), - flipped: flipped, - } -} - -// linear is a range from min to max -type linear struct { - Max, Min float64 - rng *rand.Rand - flipped bool -} - -// Poll on a linear float range returns a float at uniform -// distribution in lfr's range -func (lfr linear) Poll() float64 { - return ((lfr.Max - lfr.Min) * lfr.rng.Float64()) + lfr.Min -} - -// Mult scales a Linear by f -func (lfr linear) Mult(f float64) Range { - lfr.Max *= f - lfr.Min *= f - return lfr -} - -// EnforceRange returns f, if is within the range, or the closest value -// in the range to f. -func (lfr linear) EnforceRange(f float64) float64 { - if f < lfr.Min { - return lfr.Min - } else if f > lfr.Max { - return lfr.Max - } - return f -} - -// Percentile returns the fth percentile value along this range -func (lfr linear) Percentile(f float64) float64 { - return ((lfr.Max - lfr.Min) * f) + lfr.Min -} diff --git a/alg/range/floatrange/linear_test.go b/alg/range/floatrange/linear_test.go deleted file mode 100644 index e881a7b6..00000000 --- a/alg/range/floatrange/linear_test.go +++ /dev/null @@ -1,69 +0,0 @@ -package floatrange - -import ( - "math/rand" - "testing" - "time" -) - -func TestNewLinear_Constant(t *testing.T) { - linear := NewLinear(1, 1) - if _, ok := linear.(constant); !ok { - t.Fatalf("NewLinear with no variance did not create constant") - } -} - -func TestNewSpread_Constant(t *testing.T) { - linear := NewSpread(1, 0) - if _, ok := linear.(constant); !ok { - t.Fatalf("NewSpread with no spread did not create constant") - } -} - -func TestNewSpread(t *testing.T) { - linear := NewSpread(10, -10).(linear) - if linear.flipped { - t.Fatalf("new spread should not produce flipped linear range") - } -} - -func TestLinear(t *testing.T) { - rand.Seed(time.Now().Unix()) - const testCount = 100 - const maxInt = 100000 - const minInt = -100000 - for i := 0; i < testCount; i++ { - min := rand.Float64()*(maxInt-minInt) + minInt - max := rand.Float64()*(maxInt-minInt) + minInt - linear := NewLinear(min, max) - if max < min { - min, max = max, min - } - poll := linear.Poll() - if poll < min || poll > max { - t.Fatal("Linear.Poll did not return a value in its range") - } - magnitude := rand.Float64() - linear2 := linear.Mult(magnitude) - poll2 := linear2.Poll() - if poll2 < float64(min)*magnitude || poll2 > float64(max)*magnitude { - t.Fatal("Linear.Mult result did not match expected Poll") - } - underMin := (rand.Float64()*(maxInt-minInt) + minInt) - (maxInt - minInt) - if linear.EnforceRange(underMin) != min { - t.Fatal("Linear.EnforceRange under min did not return min") - } - overMax := (rand.Float64()*(maxInt-minInt) + minInt) + (maxInt - minInt) - if linear.EnforceRange(overMax) != max { - t.Fatal("Linear.EnforceRange over max did not return max") - } - within := rand.Float64()*(max-min) + min - if linear.EnforceRange(within) != within { - t.Fatal("Linear.EnforceRange within range did not return input") - } - percent := rand.Float64() - if linear.Percentile(percent) != min+float64((max-min))*percent { - t.Fatal("Linear.Percentile did not return percentile value") - } - } -} diff --git a/alg/range/floatrange/range.go b/alg/range/floatrange/range.go deleted file mode 100644 index cafb2ae1..00000000 --- a/alg/range/floatrange/range.go +++ /dev/null @@ -1,10 +0,0 @@ -// Package floatrange provides distributions that accept and return float64s. -package floatrange - -// Range represents a range of floating point numbers -type Range interface { - Poll() float64 - Mult(f float64) Range - EnforceRange(f float64) float64 - Percentile(f float64) float64 -} diff --git a/alg/range/intrange/constant.go b/alg/range/intrange/constant.go deleted file mode 100644 index a0fcdc4c..00000000 --- a/alg/range/intrange/constant.go +++ /dev/null @@ -1,30 +0,0 @@ -package intrange - -// NewConstant returns a range which will always return the input constant -func NewConstant(i int) Range { - return constant(i) -} - -// constant implements Range as a poll -// which always returns the same integer. -type constant int - -// Poll returns c cast to an int -func (c constant) Poll() int { - return int(c) -} - -// Mult returns this range scaled by i -func (c constant) Mult(i float64) Range { - return constant(int(float64(int(c)) * i)) -} - -// EnforceRange on a constant must return the constant -func (c constant) EnforceRange(int) int { - return int(c) -} - -// Percentile can only return the constant itself -func (c constant) Percentile(float64) int { - return int(c) -} diff --git a/alg/range/intrange/constant_test.go b/alg/range/intrange/constant_test.go deleted file mode 100644 index 34c7f9d6..00000000 --- a/alg/range/intrange/constant_test.go +++ /dev/null @@ -1,32 +0,0 @@ -package intrange - -import ( - "math/rand" - "testing" - "time" -) - -func TestConstant(t *testing.T) { - rand.Seed(time.Now().Unix()) - const testCount = 100 - const maxInt = 100000 - const minInt = -100000 - for i := 0; i < testCount; i++ { - val := rand.Intn(maxInt-minInt) + minInt - cons := NewConstant(val) - if cons.Poll() != val { - t.Fatal("Constant.Poll did not return initialized value") - } - magnitude := rand.Float64() - cons2 := cons.Mult(magnitude) - if cons2.Poll() != int(float64(val)*magnitude) { - t.Fatal("Constant.Mult result did not match expected Poll") - } - if cons.EnforceRange(rand.Intn(maxInt)) != val { - t.Fatal("Constant.EnforceRange did not return initialized value") - } - if cons.Percentile(rand.Float64()) != val { - t.Fatal("Constant.Percentile did not return initialized value") - } - } -} diff --git a/alg/range/intrange/infinite.go b/alg/range/intrange/infinite.go deleted file mode 100644 index 7d494e10..00000000 --- a/alg/range/intrange/infinite.go +++ /dev/null @@ -1,34 +0,0 @@ -package intrange - -import ( - "math" -) - -// NewInfinite returns a range which will always return math.MaxInt32 and -// is unchangeable. -func NewInfinite() Range { - return Infinite{} -} - -// Infinite is a immutable range which always polls math.MaxInt32 -type Infinite struct{} - -// Poll returns math.MaxInt32 on Infinites. -func (inf Infinite) Poll() int { - return math.MaxInt32 -} - -// Mult does nothing to Infinites. -func (inf Infinite) Mult(i float64) Range { - return inf -} - -// EnforceRange for an Infinite returns Infinite -func (inf Infinite) EnforceRange(i int) int { - return math.MaxInt32 -} - -// Percentile can only return math.MaxInt32 -func (inf Infinite) Percentile(float64) int { - return math.MaxInt32 -} diff --git a/alg/range/intrange/infinite_test.go b/alg/range/intrange/infinite_test.go deleted file mode 100644 index 1d4eaefc..00000000 --- a/alg/range/intrange/infinite_test.go +++ /dev/null @@ -1,26 +0,0 @@ -package intrange - -import ( - "math" - "math/rand" - "testing" - "time" -) - -func TestInfinite(t *testing.T) { - rand.Seed(time.Now().Unix()) - inf := NewInfinite() - if inf.Poll() != math.MaxInt32 { - t.Fatal("infinite.Poll did not return math.MaxInt32") - } - inf2 := inf.Mult(rand.Float64()) - if inf2 != inf { - t.Fatal("base infinite did not match multiplied infinite") - } - if inf.EnforceRange(rand.Intn(10000)) != math.MaxInt32 { - t.Fatal("infinite.EnforceRange did not return math.MaxInt32") - } - if inf.Percentile(rand.Float64()) != math.MaxInt32 { - t.Fatal("infinite.Percentile did not return math.MaxInt32") - } -} diff --git a/alg/range/intrange/linear.go b/alg/range/intrange/linear.go deleted file mode 100644 index 367e23cd..00000000 --- a/alg/range/intrange/linear.go +++ /dev/null @@ -1,70 +0,0 @@ -package intrange - -import ( - "math/rand" - - "github.com/oakmound/oak/v3/alg/range/internal/random" -) - -// NewLinear returns a linear range between min and max -func NewLinear(min, max int) Range { - if max == min { - return constant(min) - } - flipped := false - if max < min { - max, min = min, max - flipped = true - } - return linear{ - min: min, - max: max, - rng: random.Rand(), - flipped: flipped, - } -} - -// NewSpread returns a linear range from base - s to base + s -func NewSpread(base, spread int) Range { - if spread == 0 { - return constant(base) - } - if spread < 0 { - spread *= -1 - } - return linear{base - spread, base + spread, random.Rand(), false} -} - -// linear polls on a linear scale between a minimum and a maximum -type linear struct { - min, max int - rng *rand.Rand - flipped bool -} - -func (lir linear) Poll() int { - return int(float64(lir.max-lir.min)*lir.rng.Float64()) + lir.min -} - -func (lir linear) Mult(i float64) Range { - lir.max = int(float64(lir.max) * i) - lir.min = int(float64(lir.min) * i) - return lir -} - -func (lir linear) EnforceRange(i int) int { - if i < lir.min { - return lir.min - } else if i > lir.max { - return lir.max - } - return i -} - -func (lir linear) Percentile(f float64) int { - diff := float64(lir.max-lir.min) * f // 0 - 255 * .1 = -25 + 255 = 230 // 255 - 0 * .1 = 25 - if lir.flipped { - return lir.max - int(diff) - } - return lir.min + int(diff) -} diff --git a/alg/range/intrange/range.go b/alg/range/intrange/range.go deleted file mode 100644 index 7cee8427..00000000 --- a/alg/range/intrange/range.go +++ /dev/null @@ -1,10 +0,0 @@ -// Package intrange provides distributions that return ints. -package intrange - -// Range represents a range of integer numbers -type Range interface { - Poll() int - Mult(float64) Range - EnforceRange(int) int - Percentile(float64) int -} diff --git a/alg/span/builtin.go b/alg/span/builtin.go new file mode 100644 index 00000000..b82ecd61 --- /dev/null +++ b/alg/span/builtin.go @@ -0,0 +1,98 @@ +package span + +import ( + "math/rand" + + "github.com/oakmound/oak/v4/alg/span/internal/random" + "golang.org/x/exp/constraints" +) + +// A Spanable must be usable in basic arithmetic-- addition, subtraction, and multiplication. +type Spanable interface { + constraints.Float | constraints.Integer +} + +// NewConstant returns a span where the minimum and maximum are both i. Poll, Percentile, and Clamp will always return i. +func NewConstant[T Spanable](i T) Span[T] { + return constant[T]{i} +} + +type constant[T Spanable] struct { + val T +} + +func (c constant[T]) Poll() T { + return c.val +} + +func (c constant[T]) MulSpan(i float64) Span[T] { + return constant[T]{T(float64(c.val) * i)} +} + +func (c constant[T]) Clamp(T) T { + return c.val +} + +func (c constant[T]) Percentile(float64) T { + return c.val +} + +// NewLinear returns a linear span between min and max. The linearity implies that no point in the span is preferred, +// and Percentile will scale in a constant fashion from min to max. +func NewLinear[T Spanable](min, max T) Span[T] { + if max == min { + return constant[T]{min} + } + flipped := false + if max < min { + max, min = min, max + flipped = true + } + return linear[T]{ + min: min, + max: max, + rng: random.Rand(), + flipped: flipped, + } +} + +// NewSpread returns a linear span from base-spread to base+spread. +func NewSpread[T Spanable](base, spread T) Span[T] { + if spread < 0 { + return NewLinear(base+spread, base-spread) + } + return NewLinear(base-spread, base+spread) +} + +type linear[T Spanable] struct { + min, max T + rng *rand.Rand + flipped bool +} + +func (lir linear[T]) Poll() T { + return T(float64(lir.max-lir.min)*lir.rng.Float64()) + lir.min +} + +func (lir linear[T]) MulSpan(i float64) Span[T] { + lir.max = T(float64(lir.max) * i) + lir.min = T(float64(lir.min) * i) + return lir +} + +func (lir linear[T]) Clamp(i T) T { + if i < lir.min { + return lir.min + } else if i > lir.max { + return lir.max + } + return i +} + +func (lir linear[T]) Percentile(f float64) T { + diff := float64(lir.max-lir.min) * f // 0 - 255 * .1 = -25 + 255 = 230 // 255 - 0 * .1 = 25 + if lir.flipped { + return lir.max - T(diff) + } + return lir.min + T(diff) +} diff --git a/alg/range/intrange/linear_test.go b/alg/span/builtin_test.go similarity index 58% rename from alg/range/intrange/linear_test.go rename to alg/span/builtin_test.go index 20c0fc87..ecb5be1e 100644 --- a/alg/range/intrange/linear_test.go +++ b/alg/span/builtin_test.go @@ -1,4 +1,4 @@ -package intrange +package span import ( "math/rand" @@ -8,20 +8,20 @@ import ( func TestNewLinear_Constant(t *testing.T) { linear := NewLinear(1, 1) - if _, ok := linear.(constant); !ok { + if _, ok := linear.(constant[int]); !ok { t.Fatalf("NewLinear with no variance did not create constant") } } func TestNewSpread_Constant(t *testing.T) { linear := NewSpread(1, 0) - if _, ok := linear.(constant); !ok { + if _, ok := linear.(constant[int]); !ok { t.Fatalf("NewSpread with no spread did not create constant") } } func TestNewSpread(t *testing.T) { - linear := NewSpread(10, -10).(linear) + linear := NewSpread[float32](10, -10).(linear[float32]) if linear.flipped { t.Fatalf("new spread should not produce flipped linear range") } @@ -46,22 +46,22 @@ func TestLinear(t *testing.T) { t.Fatal("Linear.Poll did not return a value in its range") } magnitude := rand.Float64() - linear2 := linear.Mult(magnitude) + linear2 := linear.MulSpan(magnitude) poll2 := linear2.Poll() if poll2 < int(float64(min)*magnitude) || poll2 > int(float64(max)*magnitude) { t.Fatal("Linear.Mult result did not match expected Poll") } underMin := (rand.Intn(maxInt-minInt) + minInt) - (maxInt - minInt) - if linear.EnforceRange(underMin) != min { - t.Fatal("Linear.EnforceRange under min did not return min") + if linear.Clamp(underMin) != min { + t.Fatal("Linear.Clamp under min did not return min") } overMax := (rand.Intn(maxInt-minInt) + minInt) + (maxInt - minInt) - if linear.EnforceRange(overMax) != max { - t.Fatal("Linear.EnforceRange over max did not return max") + if linear.Clamp(overMax) != max { + t.Fatal("Linear.Clamp over max did not return max") } within := rand.Intn(max-min) + min - if linear.EnforceRange(within) != within { - t.Fatal("Linear.EnforceRange within range did not return input") + if linear.Clamp(within) != within { + t.Fatal("Linear.Clamp within range did not return input") } percent := rand.Float64() if !flipped { @@ -75,3 +75,28 @@ func TestLinear(t *testing.T) { } } } + +func TestConstant(t *testing.T) { + rand.Seed(time.Now().Unix()) + const testCount = 100 + const maxInt = 100000 + const minInt = -100000 + for i := 0; i < testCount; i++ { + val := rand.Intn(maxInt-minInt) + minInt + cons := NewConstant(val) + if cons.Poll() != val { + t.Fatal("Constant.Poll did not return initialized value") + } + magnitude := rand.Float64() + cons2 := cons.MulSpan(magnitude) + if cons2.Poll() != int(float64(val)*magnitude) { + t.Fatal("Constant.Mult result did not match expected Poll") + } + if cons.Clamp(rand.Intn(maxInt)) != val { + t.Fatal("Constant.Clamp did not return initialized value") + } + if cons.Percentile(rand.Float64()) != val { + t.Fatal("Constant.Percentile did not return initialized value") + } + } +} diff --git a/alg/span/color.go b/alg/span/color.go new file mode 100644 index 00000000..0d670059 --- /dev/null +++ b/alg/span/color.go @@ -0,0 +1,57 @@ +package span + +import "image/color" + +type linearColor struct { + r, g, b, a Span[uint32] +} + +// NewLinearColor returns a linear color distribution between min and maxColor +func NewLinearColor(minColor, maxColor color.Color) Span[color.Color] { + r, g, b, a := minColor.RGBA() + r2, g2, b2, a2 := maxColor.RGBA() + return linearColor{ + NewLinear(r, r2), + NewLinear(g, g2), + NewLinear(b, b2), + NewLinear(a, a2), + } +} + +func (l linearColor) Clamp(c color.Color) color.Color { + r3, g3, b3, a3 := c.RGBA() + r4 := l.r.Clamp(r3) + g4 := l.g.Clamp(g3) + b4 := l.b.Clamp(b3) + a4 := l.a.Clamp(a3) + return rgbaFromInts(r4, g4, b4, a4) +} + +func (l linearColor) MulSpan(i float64) Span[color.Color] { + return linearColor{ + l.r.MulSpan(i), + l.g.MulSpan(i), + l.b.MulSpan(i), + l.a.MulSpan(i), + } +} + +func (l linearColor) Poll() color.Color { + r3 := l.r.Poll() + g3 := l.g.Poll() + b3 := l.b.Poll() + a3 := l.a.Poll() + return rgbaFromInts(r3, g3, b3, a3) +} + +func (l linearColor) Percentile(f float64) color.Color { + r3 := l.r.Percentile(f) + g3 := l.g.Percentile(f) + b3 := l.b.Percentile(f) + a3 := l.a.Percentile(f) + return rgbaFromInts(r3, g3, b3, a3) +} + +func rgbaFromInts(r, g, b, a uint32) color.RGBA { + return color.RGBA{uint8(r / 257), uint8(g / 257), uint8(b / 257), uint8(a / 257)} +} diff --git a/alg/span/color_test.go b/alg/span/color_test.go new file mode 100644 index 00000000..3b712cda --- /dev/null +++ b/alg/span/color_test.go @@ -0,0 +1,56 @@ +package span + +import ( + "image/color" + "math/rand" + "testing" +) + +func TestLinearColor(t *testing.T) { + rng := NewLinearColor(color.RGBA{255, 255, 255, 255}, color.RGBA{255, 255, 255, 255}) + if rng.Poll() != (color.RGBA{255, 255, 255, 255}) { + t.Fatal("false linear range did not return only possible value on Poll") + } + for i := 0; i < 100; i++ { + if rng.Percentile(rand.Float64()) != (color.RGBA{255, 255, 255, 255}) { + t.Fatal("false linear range did not return only possible value on Percentile") + } + } + rng = NewLinearColor(color.RGBA{0, 0, 0, 255}, color.RGBA{255, 255, 255, 255}) + for i := 0.0; i < 255; i++ { + p := i / 255 + uinti := uint8(i) + if rng.Percentile(p) != (color.RGBA{uinti, uinti, uinti, 255}) { + t.Fatal("linear color range did not return appropriate scaled color, bottom to top") + } + } + rng = NewLinearColor(color.RGBA{255, 255, 255, 255}, color.RGBA{0, 0, 0, 255}) + for i := 255.0; i > 0; i-- { + p := (255 - i) / 255 + uinti := uint8(i) + if rng.Percentile(p) != (color.RGBA{uinti, uinti, uinti, 255}) { + t.Fatal("linear color range did not return appropriate scaled color, top to bottom") + } + } + rng = NewLinearColor(color.RGBA{125, 125, 125, 125}, color.RGBA{200, 200, 200, 200}) + if rng.Clamp(color.RGBA{100, 100, 100, 100}) != (color.RGBA{125, 125, 125, 125}) { + t.Fatal("linear color range did not enforce minimum color") + } + if rng.Clamp(color.RGBA{225, 225, 225, 225}) != (color.RGBA{200, 200, 200, 200}) { + t.Fatal("linear color range did not enforce maximum color") + } + if rng.Clamp(color.RGBA{175, 175, 175, 175}) != (color.RGBA{175, 175, 175, 175}) { + t.Fatal("linear color range did not pass through value within range") + } + + rng = rng.MulSpan(1.1) + if rng.Clamp(color.RGBA{100, 100, 100, 100}) != (color.RGBA{137, 137, 137, 137}) { + t.Fatal("linear color range did not enforce minimum color") + } + if rng.Clamp(color.RGBA{225, 225, 225, 225}) != (color.RGBA{220, 220, 220, 220}) { + t.Fatal("linear color range did not enforce maximum color") + } + if rng.Clamp(color.RGBA{175, 175, 175, 175}) != (color.RGBA{175, 175, 175, 175}) { + t.Fatal("linear color range did not pass through value within range") + } +} diff --git a/alg/span/doc.go b/alg/span/doc.go new file mode 100644 index 00000000..efec8b41 --- /dev/null +++ b/alg/span/doc.go @@ -0,0 +1,2 @@ +// Package span provides helper constructs to represent ranges of values, to poll from or clamp to +package span diff --git a/alg/range/internal/random/rand.go b/alg/span/internal/random/rand.go similarity index 100% rename from alg/range/internal/random/rand.go rename to alg/span/internal/random/rand.go diff --git a/alg/span/span.go b/alg/span/span.go new file mode 100644 index 00000000..b339bee2 --- /dev/null +++ b/alg/span/span.go @@ -0,0 +1,19 @@ +package span + +// A Span represents some enumerable range. +type Span[T any] interface { + // Poll returns a pseudorandom value within this span. + Poll() T + // Clamp, if v lies within the boundary of this span, returns v. + // Otherwise, CLamp returns a modified version of v that is rounded to the closest value + // that does lie within the boundary of this span. + Clamp(v T) T + // Percentile returns the value along this span that is at the provided percentile through the span, + // e.g. providing .5 will return the middle of the span, providing 1 will return the maximum value in + // the span. Providing a value less than 0 or greater than 1 may extend the span by where it would theoretically + // progress, but should not be relied upon unless a given implementation specifies what it will do. If this span + // represents multiple degrees of freedom, this will pin all those degrees to the single provided percent. + Percentile(float64) T + // MulSpan returns this span with its entire range multiplied by the given constant. + MulSpan(float64) Span[T] +} diff --git a/audio/audio.go b/audio/audio.go deleted file mode 100644 index 535c7d31..00000000 --- a/audio/audio.go +++ /dev/null @@ -1,133 +0,0 @@ -package audio - -import ( - "fmt" - - "github.com/oakmound/oak/v3/audio/font" - "github.com/oakmound/oak/v3/audio/klang" - "github.com/oakmound/oak/v3/oakerr" -) - -// Audio is a struct of some audio data and the variables -// required to filter it through a sound font. -type Audio struct { - *font.Audio - toStop klang.Audio - X, Y *float64 - setVolume int32 -} - -// New returns an audio from a font, some audio data, and optional -// positional coordinates -func New(f *font.Font, d Data, coords ...*float64) *Audio { - a := new(Audio) - a.Audio = font.NewAudio(f, d) - if len(coords) > 0 { - a.X = coords[0] - if len(coords) > 1 { - a.Y = coords[1] - } - } - return a -} - -// SetVolume attempts to set the volume of the underlying OS audio. -func (a *Audio) SetVolume(v int32) error { - a.setVolume = v - if a.toStop != nil { - return a.toStop.SetVolume(v) - } - return nil -} - -// Play begin's an audio's playback -func (a *Audio) Play() <-chan error { - a2, err := a.Copy() - if err != nil { - return errChannel(err) - } - a3, err := a2.Filter(a.Font.Filters...) - if err != nil { - return errChannel(err) - } - a4, err := a3.(*Audio).FullAudio.Copy() - if err != nil { - return errChannel(err) - } - a.toStop = a4 - err = a.toStop.SetVolume(a.setVolume) - if err != nil { - return errChannel(err) - } - return a4.Play() -} - -func errChannel(err error) <-chan error { - ch := make(chan error) - go func() { - ch <- err - }() - return ch -} - -// Stop stops an audio's playback -func (a *Audio) Stop() error { - if a == nil || a.toStop == nil { - return oakerr.NilInput{InputName: "Audio"} - } - return a.toStop.Stop() -} - -// Copy returns a copy of the audio -func (a *Audio) Copy() (klang.Audio, error) { - a2, err := a.Audio.Copy() - if err != nil { - return nil, err - } - return New(a.Audio.Font, a2.(klang.FullAudio), a.X, a.Y), nil -} - -// MustCopy acts like Copy, but panics on an error. -func (a *Audio) MustCopy() klang.Audio { - return New(a.Audio.Font, a.Audio.MustCopy().(klang.FullAudio), a.X, a.Y) -} - -// Filter returns the audio with some set of filters applied to it. -func (a *Audio) Filter(fs ...klang.Filter) (klang.Audio, error) { - var ad klang.Audio = a - var err, consErr error - for _, f := range fs { - ad, err = f.Apply(ad) - if err != nil { - if consErr == nil { - consErr = err - } else { - consErr = fmt.Errorf("%w, %v", err, consErr) - } - } - } - return ad, consErr -} - -// MustFilter acts like Filter but ignores errors. -func (a *Audio) MustFilter(fs ...klang.Filter) klang.Audio { - ad, _ := a.Filter(fs...) - return ad -} - -// Xp returns a pointer to the x position of this audio, if it has one. -// It has no position, this returns nil. -func (a *Audio) Xp() *float64 { - return a.X -} - -// Yp returns a pointer to the y position of this audio, if it has one. -// It has no position, this returns nil. If This is not nil, Xp will not be nil. -func (a *Audio) Yp() *float64 { - return a.Y -} - -var ( - // Guarantee that Audio can have positional filters applied to it - _ SupportsPos = &Audio{} -) diff --git a/audio/audio_test.go b/audio/audio_test.go deleted file mode 100644 index 54ec7e61..00000000 --- a/audio/audio_test.go +++ /dev/null @@ -1,69 +0,0 @@ -package audio - -import ( - "testing" - "time" - - "github.com/oakmound/oak/v3/audio/klang/filter" - "github.com/oakmound/oak/v3/audio/synth" -) - -func TestAudioFuncs(t *testing.T) { - kla, err := synth.Int16.Sin() - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - a := New(DefaultFont, kla.(Data)) - err = a.SetVolume(0) - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - err = <-a.Play() - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - time.Sleep(a.PlayLength()) - // Assert audio is playing - <-a.Play() - err = a.Stop() - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - time.Sleep(a.PlayLength()) - // Assert audio is not playing - kla, err = a.Copy() - a = kla.(*Audio) - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - a.Play() - time.Sleep(a.PlayLength()) - // Assert audio is playing - a = a.MustCopy().(*Audio) - if a.Xp() != nil { - t.Fatalf("audio without position had x pointer") - } - if a.Yp() != nil { - t.Fatalf("audio without position had y pointer") - } - kla, err = a.Filter(filter.Volume(.5)) - a = kla.(*Audio) - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - a.Play() - time.Sleep(a.PlayLength()) - // Assert quieter audio is playing - a = a.MustFilter(filter.Volume(.5)).(*Audio) - a.Play() - time.Sleep(a.PlayLength()) - // Assert yet quieter audio is playing - err = a.SetVolume(-2000) - if err != nil { - t.Fatalf("unexpected error on set volume: %v", err) - } - a.Play() - time.Sleep(a.PlayLength()) - // Assert yet quieter audio is playing - -} diff --git a/audio/doc.go b/audio/doc.go deleted file mode 100644 index 2eb2fe27..00000000 --- a/audio/doc.go +++ /dev/null @@ -1,2 +0,0 @@ -// Package audio provides audio playing utilities. -package audio diff --git a/audio/pcm/driver.go b/audio/driver.go similarity index 59% rename from audio/pcm/driver.go rename to audio/driver.go index 7d11af01..b59f5e18 100644 --- a/audio/pcm/driver.go +++ b/audio/driver.go @@ -1,7 +1,6 @@ -package pcm +package audio -// A Driver defines the underlying interface that should be used for initializing PCM audio writers -// by this package. +// A Driver defines the underlying interface that should be used for initializing PCM audio writers. type Driver int const ( @@ -10,14 +9,16 @@ const ( DriverDefault Driver = iota DriverPulse DriverDirectSound + DriverALSA ) +var driverNames = map[Driver]string{ + DriverPulse: "pulseaudio", + DriverDirectSound: "directsound", + DriverDefault: "default", + DriverALSA: "alsa", +} + func (d Driver) String() string { - switch d { - case DriverPulse: - return "pulseaudio" - case DriverDirectSound: - return "directsound" - } - return "" + return driverNames[d] } diff --git a/audio/driver_test.go b/audio/driver_test.go new file mode 100644 index 00000000..befb7baf --- /dev/null +++ b/audio/driver_test.go @@ -0,0 +1,17 @@ +package audio + +import "testing" + +func TestDriver_String(t *testing.T) { + drivers := []Driver{ + DriverDefault, + DriverDirectSound, + DriverPulse, + DriverALSA, + } + for _, d := range drivers { + if d.String() == "" { + t.Errorf("driver %d had no defined string", d) + } + } +} diff --git a/audio/ears.go b/audio/ears.go deleted file mode 100644 index 9ab30ac9..00000000 --- a/audio/ears.go +++ /dev/null @@ -1,67 +0,0 @@ -package audio - -import ( - "github.com/oakmound/oak/v3/physics" -) - -// ScaleType should be moved to a different package that handles global -// scale varieties -type ScaleType int - -const ( - // LINEAR is the only ScaleType right now. - LINEAR ScaleType = iota -) - -// Ears are assisting variables and some position in the game world where -// audio should be 'heard' from, like the player character. Passing in that -// position's x and y as pointers then will allow for sounds further away from -// that point to be quieter and sounds to the left / right of that point to -// be panned left and right. -type Ears struct { - X *float64 - Y *float64 - PanWidth float64 - SilenceRadius float64 - // VolumeScale and PanScale are currently ignored because there is only - // one scale type - VolumeScale ScaleType - PanScale ScaleType -} - -// NewEars returns a new set of ears to hear pan/volume modified audio from -func NewEars(x, y *float64, panWidth float64, silentRadius float64) *Ears { - ears := new(Ears) - ears.X = x - ears.Y = y - ears.PanWidth = panWidth - ears.SilenceRadius = silentRadius - return ears -} - -// CalculatePan converts PanWidth and two x positions into a left / right pan -// value. -func (e *Ears) CalculatePan(x2 float64) float64 { - v := (x2 - *e.X) / e.PanWidth - if v < -1 { - return -1 - } else if v > 1 { - return 1 - } - return v -} - -// CalculateVolume converts two vector positions and SilenceRadius into a -// volume scale -func (e *Ears) CalculateVolume(v physics.Vector) float64 { - v2 := physics.NewVector(*e.X, *e.Y) - dist := v2.Distance(v) - - // Ignore scaling variable - lin := (e.SilenceRadius - dist) / e.SilenceRadius - if lin < 0 { - lin = 0 - } - - return lin -} diff --git a/audio/error_test.go b/audio/error_test.go deleted file mode 100644 index 5eaaea5b..00000000 --- a/audio/error_test.go +++ /dev/null @@ -1,15 +0,0 @@ -package audio - -import ( - "testing" - - "github.com/oakmound/oak/v3/oakerr" -) - -func TestErrorChannel(t *testing.T) { - err := oakerr.ExistingElement{} - err2 := <-errChannel(err) - if err != err2 { - t.Fatalf("err channel did not propagate error") - } -} diff --git a/audio/fade.go b/audio/fade.go new file mode 100644 index 00000000..43b94554 --- /dev/null +++ b/audio/fade.go @@ -0,0 +1,148 @@ +package audio + +import ( + "time" + + "github.com/oakmound/oak/v4/audio/pcm" +) + +// FadeIn wraps a reader such that it will linearly fade in over the given duration. +func FadeIn(dur time.Duration, in pcm.Reader) pcm.Reader { + perSec := in.PCMFormat().BytesPerSecond() + bytesToFadeIn := int((time.Duration(perSec) / 1000) * (dur / time.Millisecond)) + + return &fadeInReader{ + Reader: in, + toFadeIn: bytesToFadeIn, + totalToFadeIn: bytesToFadeIn, + } +} + +type fadeInReader struct { + pcm.Reader + toFadeIn, totalToFadeIn int +} + +func (fir *fadeInReader) ReadPCM(b []byte) (n int, err error) { + if fir.toFadeIn == 0 { + return fir.Reader.ReadPCM(b) + } + read, err := fir.Reader.ReadPCM(b) + if err != nil { + return read, err + } + format := fir.PCMFormat() + switch format.Bits { + case 8: + for i, byt := range b[:read] { + fadeInPercent := (float64(fir.totalToFadeIn) - float64(fir.toFadeIn)) / float64(fir.totalToFadeIn) + if fadeInPercent >= 1 { + fadeInPercent = 1 + } + b[i] = byte(int8(float64(int8(byt)) * fadeInPercent)) + fir.toFadeIn-- + } + case 16: + for i := 0; i+2 <= read; i += 2 { + fadeInPercent := (float64(fir.totalToFadeIn) - float64(fir.toFadeIn)) / float64(fir.totalToFadeIn) + if fadeInPercent >= 1 { + fadeInPercent = 1 + } + i16 := int16(b[i]) + (int16(b[i+1]) << 8) + new16 := int16(float64(i16) * fadeInPercent) + b[i] = byte(new16) + b[i+1] = byte(new16 >> 8) + fir.toFadeIn -= 2 + } + case 32: + for i := 0; i+4 <= read; i += 4 { + fadeInPercent := (float64(fir.totalToFadeIn) - float64(fir.toFadeIn)) / float64(fir.totalToFadeIn) + if fadeInPercent >= 1 { + fadeInPercent = 1 + } + i32 := int32(b[i]) + + (int32(b[i+1]) << 8) + + (int32(b[i+2]) << 16) + + (int32(b[i+3]) << 24) + new32 := int32(float64(i32) * fadeInPercent) + b[i] = byte(new32) + b[i+1] = byte(new32 >> 8) + b[i+2] = byte(new32 >> 16) + b[i+3] = byte(new32 >> 24) + fir.toFadeIn -= 4 + } + } + return read, nil +} + +// FadeOut wraps a reader such that it will linearly fade out over the given duration. +func FadeOut(dur time.Duration, in pcm.Reader) pcm.Reader { + perSec := in.PCMFormat().BytesPerSecond() + bytestoFadeOut := int((time.Duration(perSec) / 1000) * (dur / time.Millisecond)) + + return &fadeOutReader{ + Reader: in, + toFadeOut: bytestoFadeOut, + totaltoFadeOut: bytestoFadeOut, + } +} + +type fadeOutReader struct { + pcm.Reader + toFadeOut, totaltoFadeOut int +} + +func (fir *fadeOutReader) ReadPCM(b []byte) (n int, err error) { + if fir.toFadeOut == 0 { + return fir.Reader.ReadPCM(b) + } + read, err := fir.Reader.ReadPCM(b) + if err != nil { + return read, err + } + format := fir.PCMFormat() + switch format.Bits { + case 8: + for i, byt := range b[:read] { + fadeOutPercent := float64(fir.toFadeOut) / float64(fir.totaltoFadeOut) + if fadeOutPercent <= 0 { + fadeOutPercent = 0 + } + b[i] = byte(int8(float64(int8(byt)) * fadeOutPercent)) + fir.toFadeOut-- + } + case 16: + for i := 0; i+2 <= read; i += 2 { + fadeOutPercent := float64(fir.toFadeOut) / float64(fir.totaltoFadeOut) + if fadeOutPercent <= 0 { + fadeOutPercent = 0 + } + i16 := int16(b[i]) + (int16(b[i+1]) << 8) + new16 := int16(float64(i16) * fadeOutPercent) + b[i] = byte(new16) + b[i+1] = byte(new16 >> 8) + fir.toFadeOut -= 2 + } + case 32: + for i := 0; i+4 <= read; i += 4 { + fadeOutPercent := float64(fir.toFadeOut) / float64(fir.totaltoFadeOut) + if fadeOutPercent <= 0 { + fadeOutPercent = 0 + } + i32 := int32(b[i]) + + (int32(b[i+1]) << 8) + + (int32(b[i+2]) << 16) + + (int32(b[i+3]) << 24) + new32 := int32(float64(i32) * fadeOutPercent) + b[i] = byte(new32) + b[i+1] = byte(new32 >> 8) + b[i+2] = byte(new32 >> 16) + b[i+3] = byte(new32 >> 24) + fir.toFadeOut -= 4 + } + } + return read, nil +} + +var _ pcm.Reader = &fadeOutReader{} +var _ pcm.Reader = &fadeInReader{} diff --git a/audio/cache.go b/audio/file_cache.go similarity index 59% rename from audio/cache.go rename to audio/file_cache.go index f1e30a19..a6a021c5 100644 --- a/audio/cache.go +++ b/audio/file_cache.go @@ -3,6 +3,8 @@ package audio import ( "path/filepath" "sync" + + "github.com/oakmound/oak/v4/audio/pcm" ) // DefaultCache is the receiver for package level loading operations. @@ -11,20 +13,20 @@ var DefaultCache = NewCache() // Cache is a simple audio data cache type Cache struct { mu sync.RWMutex - data map[string]Data + data map[string]*BytesReader } // NewCache returns an empty Cache func NewCache() *Cache { return &Cache{ - data: make(map[string]Data), + data: make(map[string]*BytesReader), } } // ClearAll will remove all elements from a Cache func (c *Cache) ClearAll() { c.mu.Lock() - c.data = make(map[string]Data) + c.data = make(map[string]*BytesReader) c.mu.Unlock() } @@ -35,19 +37,22 @@ func (c *Cache) Clear(key string) { c.mu.Unlock() } -func (c *Cache) setLoaded(file string, data Data) { +func (c *Cache) setLoaded(file string, r pcm.Reader) { + // This ReadAll and .Copy() on Cache.Read ensure that multiple loads from the cache do not + // change the data that will be read on future reads. + br := ReadAll(r) c.mu.Lock() - c.data[file] = data - c.data[filepath.Base(file)] = data + c.data[file] = br + c.data[filepath.Base(file)] = br c.mu.Unlock() } // Load calls Load on the Default Cache. -func Load(file string) (Data, error) { +func Load(file string) (pcm.Reader, error) { return DefaultCache.Load(file) } // Get calls Get on the Default Cache. -func Get(file string) (Data, error) { +func Get(file string) (pcm.Reader, error) { return DefaultCache.Get(file) } diff --git a/audio/file_load.go b/audio/file_load.go new file mode 100644 index 00000000..640dedf5 --- /dev/null +++ b/audio/file_load.go @@ -0,0 +1,114 @@ +package audio + +import ( + "path/filepath" + "strings" + + "golang.org/x/sync/errgroup" + + "github.com/oakmound/oak/v4/audio/format" + "github.com/oakmound/oak/v4/audio/pcm" + "github.com/oakmound/oak/v4/dlog" + "github.com/oakmound/oak/v4/fileutil" + "github.com/oakmound/oak/v4/oakerr" +) + +// Get will read cached audio data from Load, or error if the given +// file is not in the cache. +func (c *Cache) Get(file string) (pcm.Reader, error) { + c.mu.RLock() + data, ok := c.data[file] + c.mu.RUnlock() + if !ok { + return nil, oakerr.NotFound{InputName: file} + } + return data.Copy(), nil +} + +// Load loads the given file and caches it by two keys: +// the full file name given and the final element of the file's +// path. If the file cannot be found or if its extension is not +// supported an error will be returned. +func (c *Cache) Load(file string) (pcm.Reader, error) { + dlog.Verb("Loading", file) + f, err := fileutil.Open(file) + if err != nil { + return nil, err + } + defer f.Close() + + ext := filepath.Ext(file) + ext = strings.ToLower(ext) + reader, ok := format.LoaderForExtension(ext) + if !ok { + // provide an error message suggesting a missing import for cases where we know about a + // common provider + knownFormats := map[string]string{ + ".mp3": "github.com/oakmound/oak/v4/audio/format/mp3", + ".flac": "github.com/oakmound/oak/v4/audio/format/flac", + ".wav": "github.com/oakmound/oak/v4/audio/format/wav", + } + if path, ok := knownFormats[ext]; ok { + dlog.Error("unable to parse audio format %v, did you mean to import %v?", ext, path) + } + return nil, oakerr.UnsupportedFormat{Format: ext} + } + r, err := reader(f) + if err != nil { + return nil, err + } + c.setLoaded(file, r) + return r, nil +} + +// BatchLoad attempts to load all audio files within a given directory +// should their file ending match a registered audio file parser +func BatchLoad(baseFolder string) error { + return batchLoad(baseFolder, false) +} + +// BlankBatchLoad acts like BatchLoad, but replaces all loaded assets +// with empty audio constructs. This is intended to reduce start-up +// times in development. +func BlankBatchLoad(baseFolder string) error { + return batchLoad(baseFolder, true) +} + +func batchLoad(baseFolder string, blankOut bool) error { + files, err := fileutil.ReadDir(baseFolder) + if err != nil { + return err + } + + var eg errgroup.Group + for _, file := range files { + if !file.IsDir() { + fileName := file.Name() + eg.Go(func() error { + if blankOut { + blankLoad(fileName) + } else { + _, err := DefaultCache.Load(filepath.Join(baseFolder, fileName)) + if err != nil { + return err + } + } + return nil + }) + } + } + err = eg.Wait() + return err +} + +func blankLoad(filename string) { + dlog.Verb("blank loading file %v", filename) + DefaultCache.setLoaded(filename, &BytesReader{ + Format: pcm.Format{ + SampleRate: 44000, + Bits: 16, + Channels: 2, + }, + Buffer: []byte{0, 0, 0, 0}, + }) +} diff --git a/audio/flac/flac.go b/audio/flac/flac.go deleted file mode 100644 index 757f3364..00000000 --- a/audio/flac/flac.go +++ /dev/null @@ -1,46 +0,0 @@ -// Package flac provides functionality to handle .flac files and .flac encoded data. -package flac - -import ( - "fmt" - "io" - - "github.com/eaburns/flac" - audio "github.com/oakmound/oak/v3/audio/klang" -) - -// def flac format -var format = audio.Format{ - SampleRate: 44100, - Bits: 16, - Channels: 2, -} - -// Load loads flac data from the incoming reader as an audio -func Load(r io.Reader) (audio.Audio, error) { - data, meta, err := flac.Decode(r) - if err != nil { - return nil, fmt.Errorf("failed to load flac: %w", err) - } - - fformat := audio.Format{ - SampleRate: uint32(meta.SampleRate), - Channels: uint16(meta.NChannels), - Bits: uint16(meta.BitsPerSample), - } - return audio.EncodeBytes( - audio.Encoding{ - Data: data, - Format: fformat, - }) -} - -// Save will eventually save an audio encoded as flac to the given writer -func Save(r io.ReadWriter, a audio.Audio) error { - return fmt.Errorf("unsupported Functionality") -} - -// Format returns the default flac formatting -func Format() audio.Format { - return format -} diff --git a/audio/font/audio.go b/audio/font/audio.go deleted file mode 100644 index 1a081566..00000000 --- a/audio/font/audio.go +++ /dev/null @@ -1,58 +0,0 @@ -package font - -import audio "github.com/oakmound/oak/v3/audio/klang" - -// Audio is an ease-of-use wrapper around an audio -// with an attached font, so that the audio can be played -// with .Play() but can take in the remotely variable -// font filter options. -// -// Note that it is a conscious choice for both Font and -// Audio to have a Filter(...Filter) function, so that when -// a FontAudio is in use the user needs to specify which -// element they want to apply a filter on. The alternative would -// be to have two similarly named functions, and its believed -// that fa.Font.Filter(...) and fa.Audio.Filter(...) is -// more or less equivalent to whatever those names would be. -type Audio struct { - *Font - audio.FullAudio - toStop audio.Audio -} - -// NewAudio returns a *FontAudio. -// For preparation against API changes, using NewAudio over Audio{} -// is recommended. -func NewAudio(f *Font, a audio.FullAudio) *Audio { - return &Audio{f, a, nil} -} - -// Play is equivalent to Audio.Font.Play(a.Audio) -func (ad *Audio) Play() <-chan error { - a2, err := ad.FullAudio.Copy() - if err != nil { - ch := make(chan error) - go func() { - ch <- err - }() - return ch - } - _, err = a2.Filter(ad.Font.Filters...) - if err != nil { - ch := make(chan error) - go func() { - ch <- err - }() - return ch - } - ad.toStop = a2 - return a2.Play() -} - -// Stop stops a font.Audio's playback -func (ad *Audio) Stop() error { - if ad.toStop != nil { - return ad.toStop.Stop() - } - return nil -} diff --git a/audio/font/font.go b/audio/font/font.go deleted file mode 100644 index 9d2f8d2a..00000000 --- a/audio/font/font.go +++ /dev/null @@ -1,45 +0,0 @@ -// Package font provides utilities to package together audio manipulations as -// a 'font'. -package font - -import audio "github.com/oakmound/oak/v3/audio/klang" - -// Font represents some group of settings which modify how an Audio -// should be played. The name is derived from the concept of a SoundFont -type Font struct { - Filters []audio.Filter -} - -// New returns a *Font. -// It is recommended for future API changes to avoid &Font{} and use NewFont instead -func New() *Font { - return &Font{} -} - -// Filter on a font is applied to all audios as they are played. -// Each call of Filter will completely reset a Font's filters -func (f *Font) Filter(fs ...audio.Filter) *Font { - f.Filters = fs - return f -} - -// Play on a font is equivalent to Audio.Copy().Filter(Font.GetFilters()).Play() -func (f *Font) Play(a audio.Audio) <-chan error { - a2, err := a.Copy() - if err != nil { - ch := make(chan error) - go func() { - ch <- err - }() - return ch - } - _, err = a2.Filter(f.Filters...) - if err != nil { - ch := make(chan error) - go func() { - ch <- err - }() - return ch - } - return a2.Play() -} diff --git a/audio/font/ceol/ceol.go b/audio/format/ceol/ceol.go similarity index 81% rename from audio/font/ceol/ceol.go rename to audio/format/ceol/ceol.go index 6afe8643..75f1d461 100644 --- a/audio/font/ceol/ceol.go +++ b/audio/format/ceol/ceol.go @@ -7,9 +7,6 @@ import ( "strconv" "strings" "time" - - "github.com/oakmound/oak/v3/audio/sequence" - "github.com/oakmound/oak/v3/audio/synth" ) // Raw Ceol types, holds all information in ceol file @@ -64,27 +61,6 @@ type Filter struct { LPFResonance int } -// ChordPattern converts a Ceol's patterns and arrangement into a playable chord -// pattern for sequences -func (c Ceol) ChordPattern() sequence.ChordPattern { - chp := sequence.ChordPattern{} - chp.Pitches = make([][]synth.Pitch, c.PatternLength*len(c.Arrangement)) - chp.Holds = make([][]time.Duration, c.PatternLength*len(c.Arrangement)) - for i, m := range c.Arrangement { - for _, p := range m { - if p != -1 { - for _, n := range c.Patterns[p].Notes { - chp.Pitches[n.Offset+i*c.PatternLength] = - append(chp.Pitches[n.Offset+i*c.PatternLength], synth.NoteFromIndex(n.PitchIndex)) - chp.Holds[n.Offset+i*c.PatternLength] = - append(chp.Holds[n.Offset+i*c.PatternLength], DurationFromQuarters(c.Bpm, n.Length)) - } - } - } - } - return chp -} - // DurationFromQuarters should not be here, should be in a package // managing bpm and time // Duration from quarters expects four quarters to occur per beat, diff --git a/audio/font/ceol/testdata/test.ceol b/audio/format/ceol/testdata/test.ceol similarity index 100% rename from audio/font/ceol/testdata/test.ceol rename to audio/format/ceol/testdata/test.ceol diff --git a/audio/font/dls/dls.go b/audio/format/dls/dls.go similarity index 98% rename from audio/font/dls/dls.go rename to audio/format/dls/dls.go index 67b73b63..28961920 100644 --- a/audio/font/dls/dls.go +++ b/audio/format/dls/dls.go @@ -1,7 +1,7 @@ // Package dls contains data structures for DLS (.dls) file types. package dls -import "github.com/oakmound/oak/v3/audio/font/riff" +import "github.com/oakmound/oak/v4/audio/format/riff" // The DLS is the major struct we care about in this package // DLS files contain instrument and wave sample information, and diff --git a/audio/font/dls/testdata/SanbikiSCC.dls b/audio/format/dls/testdata/SanbikiSCC.dls similarity index 100% rename from audio/font/dls/testdata/SanbikiSCC.dls rename to audio/format/dls/testdata/SanbikiSCC.dls diff --git a/audio/format/flac/flac.go b/audio/format/flac/flac.go new file mode 100644 index 00000000..d7899942 --- /dev/null +++ b/audio/format/flac/flac.go @@ -0,0 +1,66 @@ +// Package flac provides functionality to handle .flac files and .flac encoded data. +// +// +// This package may be imported solely to register flacs as a parseable file type within oak: +// +// import ( +// _ "github.com/oakmound/oak/v4/audio/format/flac" +// ) +// +package flac + +import ( + "fmt" + "io" + + "github.com/eaburns/flac" + "github.com/oakmound/oak/v4/audio/format" + "github.com/oakmound/oak/v4/audio/pcm" +) + +func init() { + format.Register(".flac", Load) +} + +// Load reads a FLAC header from a reader, parsing it's PCM format and returning +// a pcm Reader for the data following the header. It will error if the reader +// does not contain enough data to fill a FLAC header or if the header does not +// look like a FLAC header. +func Load(r io.Reader) (pcm.Reader, error) { + d, err := flac.NewDecoder(r) + if err != nil { + return nil, fmt.Errorf("failed to load flac: %w", err) + } + + return &pcm.IOReader{ + Format: pcm.Format{ + SampleRate: uint32(d.SampleRate), + Channels: uint16(d.NChannels), + Bits: uint16(d.BitsPerSample), + }, + Reader: &reader{d: d}, + }, nil +} + +type reader struct { + d *flac.Decoder + readAhead []byte +} + +func (r *reader) Read(data []byte) (int, error) { + if len(r.readAhead) == 0 { + read, err := r.d.Next() + if err != nil { + return 0, err + } + r.readAhead = read + } + copy(data, r.readAhead) + if len(r.readAhead) < len(data) { + n := len(r.readAhead) + r.readAhead = []byte{} + return n, nil + } + r.readAhead = r.readAhead[len(data):] + return len(data), nil +} diff --git a/audio/format/mp3/mp3.go b/audio/format/mp3/mp3.go new file mode 100644 index 00000000..14fb6ad7 --- /dev/null +++ b/audio/format/mp3/mp3.go @@ -0,0 +1,41 @@ +// Package mp3 provides functionality to handle .mp3 files and .mp3 encoded data. +// +// This package may be imported solely to register mp3s as a parseable file type within oak: +// +// import ( +// _ "github.com/oakmound/oak/v4/audio/format/mp3" +// ) +// +package mp3 + +import ( + "io" + + "github.com/oakmound/oak/v4/audio/format" + "github.com/oakmound/oak/v4/audio/pcm" + + "github.com/hajimehoshi/go-mp3" +) + +func init() { + format.Register(".mp3", Load) +} + +// Load reads MP3 data from a reader, parsing it's PCM format and returning +// a pcm Reader for the data contained within. It will error if the reader +// does not contain enough data to fill a file header. The resulting format +// will always be 16 bits and 2 channels. +func Load(r io.Reader) (pcm.Reader, error) { + d, err := mp3.NewDecoder(r) + if err != nil { + return nil, err + } + return &pcm.IOReader{ + Format: pcm.Format{ + SampleRate: uint32(d.SampleRate()), + Bits: 16, + Channels: 2, + }, + Reader: d, + }, nil +} diff --git a/audio/format/register.go b/audio/format/register.go new file mode 100644 index 00000000..1059030b --- /dev/null +++ b/audio/format/register.go @@ -0,0 +1,31 @@ +// Package format provides audio file and format parsers +package format + +import ( + "io" + "sync" + + "github.com/oakmound/oak/v4/audio/pcm" +) + +// A Loader can parse the data from an io.Reader and convert it into PCM encoded audio data with +// a known format. +type Loader func(r io.Reader) (pcm.Reader, error) + +var fileLoadersLock sync.RWMutex +var fileLoaders = map[string]func(r io.Reader) (pcm.Reader, error){} + +// Register registers a format by file extension (eg '.mp3') with its parsing function. +func Register(extension string, fn Loader) { + fileLoadersLock.Lock() + fileLoaders[extension] = fn + fileLoadersLock.Unlock() +} + +// LoaderForExtension returns a previously registered loader. +func LoaderForExtension(extension string) (Loader, bool) { + fileLoadersLock.RLock() + defer fileLoadersLock.RUnlock() + loader, ok := fileLoaders[extension] + return loader, ok +} diff --git a/audio/font/riff/info.go b/audio/format/riff/info.go similarity index 100% rename from audio/font/riff/info.go rename to audio/format/riff/info.go diff --git a/audio/font/riff/riff.go b/audio/format/riff/riff.go similarity index 100% rename from audio/font/riff/riff.go rename to audio/format/riff/riff.go diff --git a/audio/pcm/testdata/test.wav b/audio/format/wav/testdata/test.wav similarity index 100% rename from audio/pcm/testdata/test.wav rename to audio/format/wav/testdata/test.wav diff --git a/audio/wav/wav.go b/audio/format/wav/wav.go similarity index 56% rename from audio/wav/wav.go rename to audio/format/wav/wav.go index fa735469..e3168c59 100644 --- a/audio/wav/wav.go +++ b/audio/format/wav/wav.go @@ -1,56 +1,50 @@ // Package wav provides functionality to handle .wav files and .wav encoded data. +// +// This package may be imported solely to register wavs as a parseable file type within oak: +// +// import ( +// _ "github.com/oakmound/oak/v4/audio/format/wav" +// ) +// package wav import ( - "errors" "io" "encoding/binary" - audio "github.com/oakmound/oak/v3/audio/klang" + "github.com/oakmound/oak/v4/audio/format" + "github.com/oakmound/oak/v4/audio/pcm" ) -// Load loads wav data from the incoming reader as an audio -func Load(r io.Reader) (audio.Audio, error) { - wav, err := Read(r) - if err != nil { - return nil, err - } - return audio.EncodeBytes( - audio.Encoding{ - Data: wav.Data, - Format: audio.Format{ - SampleRate: wav.SampleRate, - Channels: wav.NumChannels, - Bits: wav.BitsPerSample, - }, - }) +func init() { + format.Register(".wav", Load) } -// Save will eventually save an audio encoded as a wav to the given writer -func Save(r io.ReadWriter, a audio.Audio) error { - return errors.New("Unsupported Functionality") -} - -// Read reads a WAV header from the provided reader, returning the PCM format and -// leaving the PCM data in the reader available for consumption. -func ReadFormat(r io.Reader) (audio.Format, error) { - data, err := ReadData(r) +// Load reads a WAV header from a reader, parsing it's PCM format and returning +// a pcm Reader for the data following the header. It will error if the reader +// does not contain enough data to fill a WAV header. It does not validate that the +// WAV header makes sense. +func Load(r io.Reader) (pcm.Reader, error) { + data, err := readData(r) if err != nil { - return audio.Format{}, err + return nil, err } - return audio.Format{ - SampleRate: data.SampleRate, - Channels: data.NumChannels, - Bits: data.BitsPerSample, + return &pcm.IOReader{ + Format: pcm.Format{ + SampleRate: data.SampleRate, + Channels: data.NumChannels, + Bits: data.BitsPerSample, + }, + Reader: r, }, nil } -// The following is a "fork" of verdverm's go-wav library +// The following is a fork of verdverm's go-wav library -// Data stores the raw information contained in a wav file -type Data struct { +// data stores the raw information contained in a wav file +type data struct { bChunkID [4]byte // B ChunkSize uint32 // L bFormat [4]byte // B @@ -70,8 +64,8 @@ type Data struct { Data []byte // L } -func ReadData(r io.Reader) (Data, error) { - data := Data{} +func readData(r io.Reader) (data, error) { + data := data{} err := binary.Read(r, binary.BigEndian, &data.bChunkID) if err != nil { @@ -129,16 +123,3 @@ func ReadData(r io.Reader) (Data, error) { } return data, nil } - -// Read returns raw wav data from an input reader -func Read(r io.Reader) (Data, error) { - data, err := ReadData(r) - if err != nil { - return data, err - } - - data.Data = make([]byte, data.Subchunk2Size) - err = binary.Read(r, binary.LittleEndian, &data.Data) - - return data, err -} diff --git a/audio/pcm/init_nix.go b/audio/init_darwin.go similarity index 70% rename from audio/pcm/init_nix.go rename to audio/init_darwin.go index 84853163..6b8512f9 100644 --- a/audio/pcm/init_nix.go +++ b/audio/init_darwin.go @@ -1,10 +1,13 @@ -//go:build linux || darwin +//go:build darwin -package pcm +package audio import ( + "fmt" + "github.com/jfreymuth/pulse" - "github.com/oakmound/oak/v3/oakerr" + "github.com/oakmound/oak/v4/audio/pcm" + "github.com/oakmound/oak/v4/oakerr" ) func initOS(driver Driver) error { @@ -26,6 +29,7 @@ func initOS(driver Driver) error { if err != nil { return err } + newWriter = newPulseWriter default: return oakerr.UnsupportedPlatform{ Operation: "pcm.Init:" + driver.String(), @@ -33,3 +37,7 @@ func initOS(driver Driver) error { } return nil } + +var newWriter = func(f pcm.Format) (pcm.Writer, error) { + return nil, fmt.Errorf("this package has not been initialized") +} diff --git a/audio/init_linux.go b/audio/init_linux.go new file mode 100644 index 00000000..4c562495 --- /dev/null +++ b/audio/init_linux.go @@ -0,0 +1,53 @@ +//go:build linux + +package audio + +import ( + "fmt" + "os" + + "github.com/jfreymuth/pulse" + "github.com/oakmound/oak/v4/audio/pcm" + "github.com/oakmound/oak/v4/oakerr" +) + +func initOS(driver Driver) error { + switch driver { + case DriverDefault: + fallthrough + case DriverPulse: + // Sanity check that pulse is installed and a sink is defined + client, err := pulse.NewClient() + if err != nil { + // osx: brew install pulseaudio + // linux: sudo apt install pulseaudio + return oakerr.UnsupportedPlatform{ + Operation: "pcm.Init:" + driver.String(), + } + } + defer client.Close() + _, err = client.DefaultSink() + if err != nil { + return err + } + newWriter = newPulseWriter + case DriverALSA: + //??? + newWriter = newALSAWriter + if skipDevices := os.Getenv("OAK_SKIP_AUDIO_DEVICES"); skipDevices != "" { + SkipDevicesContaining = skipDevices + } + default: + return oakerr.UnsupportedPlatform{ + Operation: "pcm.Init:" + driver.String(), + } + } + return nil +} + +var newWriter = func(f pcm.Format) (pcm.Writer, error) { + return nil, fmt.Errorf("this package has not been initialized") +} + +// TODO: do other drivers need this? Can we pick devices more intelligently? +var SkipDevicesContaining string = "HDMI" diff --git a/audio/wininternal/dsound.go b/audio/internal/dsound/dsound.go similarity index 73% rename from audio/wininternal/dsound.go rename to audio/internal/dsound/dsound.go index ba27ea22..1295a2da 100644 --- a/audio/wininternal/dsound.go +++ b/audio/internal/dsound/dsound.go @@ -1,18 +1,14 @@ //go:build windows -// Package wininternal defines common initialization steps for audio on windows. It must be -// a common library because windows will not allow the same application to initialize direct -// sound twice, but it must not be an `internal` directory because of how the audio packages -// are structured for backwards compatibility. TODO: oak v4: move to an internal package -package wininternal +package dsound import ( "strings" "sync" "syscall" - "github.com/oakmound/oak/v3/dlog" - "github.com/oakmound/oak/v3/oakerr" + "github.com/oakmound/oak/v4/dlog" + "github.com/oakmound/oak/v4/oakerr" "github.com/oov/directsound-go/dsound" ) @@ -27,7 +23,6 @@ var cfg Config var initLock sync.Mutex // Init initializes directsound or returns an already intialized direct sound instance. -// It may (but should probably not) be called outside of other oakmound/oak/* packages. func Init() (Config, error) { initLock.Lock() defer initLock.Unlock() diff --git a/audio/klang/audio.go b/audio/klang/audio.go deleted file mode 100644 index fcf34147..00000000 --- a/audio/klang/audio.go +++ /dev/null @@ -1,53 +0,0 @@ -// Package klang provides audio playing and encoding support -package klang - -import ( - "time" - - "github.com/oakmound/oak/v3/audio/klang/filter/supports" -) - -// Audio represents playable, filterable audio data. -type Audio interface { - // Play returns a channel that will signal when it finishes playing. - // Looping audio will never send on this channel! - // The value sent will always be true. - Play() <-chan error - // Filter will return an audio with some desired filters applied - Filter(...Filter) (Audio, error) - MustFilter(...Filter) Audio - // Stop will stop an ongoing audio - Stop() error - - // Implementing struct-- encoding - Copy() (Audio, error) - MustCopy() Audio - PlayLength() time.Duration - - // SetVolume sets the volume of an audio at an OS level, - // post filters. It multiplies with any volume filters. - // It takes a value from 0 to -10000, and can only reduce - // volume from the raw input. - SetVolume(int32) error -} - -// FullAudio supports all the built in filters -type FullAudio interface { - Audio - supports.Encoding - supports.Loop -} - -// Stream represents an audio stream. unlike Audio, the length of the -// stream is unknown. Copy is also not supported. -type Stream interface { - // Play returns a channel that will signal when it finishes playing. - // Looping audio will never send on this channel! - // The value sent will always be true. - Play() <-chan error - // Filter will return an audio with some desired filters applied - Filter(...Filter) (Audio, error) - MustFilter(...Filter) Audio - // Stop will stop an ongoing audio - Stop() error -} diff --git a/audio/klang/audio_windows.go b/audio/klang/audio_windows.go deleted file mode 100644 index 4268c3a0..00000000 --- a/audio/klang/audio_windows.go +++ /dev/null @@ -1,96 +0,0 @@ -//go:build windows -// +build windows - -package klang - -import ( - "errors" - - "github.com/oov/directsound-go/dsound" -) - -type dsAudio struct { - *Encoding - *dsound.IDirectSoundBuffer - flags dsound.BufferPlayFlag -} - -func (ds *dsAudio) Play() <-chan error { - ch := make(chan error) - if ds.Loop { - ds.flags = dsound.DSBPLAY_LOOPING - } - go func(dsbuff *dsound.IDirectSoundBuffer, flags dsound.BufferPlayFlag, ch chan error) { - err := dsbuff.SetCurrentPosition(0) - if err != nil { - select { - case ch <- err: - default: - } - } else { - err = dsbuff.Play(0, flags) - if err != nil { - select { - case ch <- err: - default: - } - } else { - select { - case ch <- nil: - default: - } - } - } - }(ds.IDirectSoundBuffer, ds.flags, ch) - return ch -} - -func (ds *dsAudio) Stop() error { - err := ds.IDirectSoundBuffer.Stop() - if err != nil { - return err - } - return ds.IDirectSoundBuffer.SetCurrentPosition(0) -} - -// SetVolume uses an underlying directsound command to set -// the volume of the audio. Applies multiplicatively with volume -// filters. Accepts int32s from -10000 to 0, 0 being the max and -// default volume. -func (ds *dsAudio) SetVolume(vol int32) error { - return ds.IDirectSoundBuffer.SetVolume(vol) -} - -func (ds *dsAudio) Filter(fs ...Filter) (Audio, error) { - var a Audio = ds - var err, consErr error - for _, f := range fs { - a, err = f.Apply(a) - if err != nil { - if consErr == nil { - consErr = err - } else { - consErr = errors.New(err.Error() + ":" + consErr.Error()) - } - } - } - // Consider: this is a significant amount - // of work to do just to make this an in-place filter. - // would it be worth it to offer both in place and non-inplace - // filter functions? - a2, err2 := EncodeBytes(*ds.Encoding) - if err2 != nil { - return nil, err2 - } - // reassign the contents of ds to be that of the - // new audio, so that this filters in place - *ds = *a2.(*dsAudio) - return ds, consErr -} - -// MustFilter acts like Filter, but ignores errors (it does not panic, -// as filter errors are expected to be non-fatal) -func (ds *dsAudio) MustFilter(fs ...Filter) Audio { - a, _ := ds.Filter(fs...) - return a -} diff --git a/audio/klang/encode_fallback.go b/audio/klang/encode_fallback.go deleted file mode 100644 index a3055e37..00000000 --- a/audio/klang/encode_fallback.go +++ /dev/null @@ -1,51 +0,0 @@ -//+build !linux -//+build !windows - -package klang - -import "errors" - -type darwinNopAudio struct { - Encoding -} - -func (dna *darwinNopAudio) Play() <-chan error { - ch := make(chan error) - go func() { - ch <- errors.New("Playback on Darwin is not supported") - }() - return ch -} - -func (dna *darwinNopAudio) Stop() error { - return errors.New("Playback on Darwin is not supported") -} - -func (dna *darwinNopAudio) SetVolume(int32) error { - return errors.New("SetVolume on Darwin is not supported") -} - -func (dna *darwinNopAudio) Filter(fs ...Filter) (Audio, error) { - var a Audio = dna - var err, consErr error - for _, f := range fs { - a, err = f.Apply(a) - if err != nil { - if consErr == nil { - consErr = err - } else { - consErr = errors.New(err.Error() + ":" + consErr.Error()) - } - } - } - return dna, consErr -} - -func (dna *darwinNopAudio) MustFilter(fs ...Filter) Audio { - a, _ := dna.Filter(fs...) - return a -} - -func EncodeBytes(enc Encoding) (Audio, error) { - return &darwinNopAudio{enc}, nil -} diff --git a/audio/klang/encode_linux.go b/audio/klang/encode_linux.go deleted file mode 100644 index d35e97b9..00000000 --- a/audio/klang/encode_linux.go +++ /dev/null @@ -1,216 +0,0 @@ -//+build linux - -package klang - -import ( - "errors" - "strings" - "sync" - - "github.com/oakmound/alsa" -) - -type alsaAudio struct { - *Encoding - *alsa.Device - playAmount int - playProgress int - stopCh chan struct{} - playing bool - playCh chan error - period int -} - -func (aa *alsaAudio) Play() <-chan error { - // If currently playing, restart - if aa.playing { - aa.playProgress = 0 - return aa.playCh - } - aa.playing = true - aa.playCh = make(chan error) - go func() { - for { - var data []byte - if len(aa.Encoding.Data)-aa.playProgress <= aa.playAmount { - data = aa.Encoding.Data[aa.playProgress:] - if aa.Loop { - delta := aa.playAmount - (len(aa.Encoding.Data) - aa.playProgress) - data = append(data, aa.Encoding.Data[:delta]...) - } - } else { - data = aa.Encoding.Data[aa.playProgress : aa.playProgress+aa.playAmount] - } - if len(data) != 0 { - err := aa.Device.Write(data, aa.period) - if err != nil { - select { - case aa.playCh <- err: - default: - } - break - } - } - aa.playProgress += aa.playAmount - if aa.playProgress > len(aa.Encoding.Data) { - if aa.Loop { - aa.playProgress %= len(aa.Encoding.Data) - } else { - select { - case aa.playCh <- nil: - default: - } - break - } - } - select { - case <-aa.stopCh: - select { - case aa.playCh <- nil: - default: - } - break - default: - } - } - aa.playing = false - aa.playProgress = 0 - }() - return aa.playCh -} - -func (aa *alsaAudio) Stop() error { - if aa.playing { - go func() { - aa.stopCh <- struct{}{} - }() - } else { - return errors.New("Audio not playing, cannot stop") - } - return nil -} - -func (aa *alsaAudio) SetVolume(int32) error { - return errors.New("SetVolume on Linux is not supported") -} - -func (aa *alsaAudio) Filter(fs ...Filter) (Audio, error) { - var a Audio = aa - var err, consErr error - for _, f := range fs { - a, err = f.Apply(a) - if err != nil { - if consErr == nil { - consErr = err - } else { - consErr = errors.New(err.Error() + ":" + consErr.Error()) - } - } - } - return aa, consErr -} - -// MustFilter acts like Filter, but ignores errors (it does not panic, -// as filter errors are expected to be non-fatal) -func (aa *alsaAudio) MustFilter(fs ...Filter) Audio { - a, _ := aa.Filter(fs...) - return a -} - -func EncodeBytes(enc Encoding) (Audio, error) { - handle, err := openDevice() - if err != nil { - return nil, err - } - // Todo: annotate these errors with more info - format, err := alsaFormat(enc.Bits) - if err != nil { - return nil, err - } - _, err = handle.NegotiateFormat(format) - if err != nil { - return nil, err - } - _, err = handle.NegotiateRate(int(enc.SampleRate)) - if err != nil { - return nil, err - } - _, err = handle.NegotiateChannels(int(enc.Channels)) - if err != nil { - return nil, err - } - // Default value at recommendation of library - period, err := handle.NegotiatePeriodSize(2048) - if err != nil { - return nil, err - } - _, err = handle.NegotiateBufferSize(4096) - if err != nil { - return nil, err - } - err = handle.Prepare() - if err != nil { - return nil, err - } - return &alsaAudio{ - playAmount: period * int(enc.Bits) / 4, - period: period, - Encoding: &enc, - Device: handle, - stopCh: make(chan struct{}), - }, nil -} - -var ( - // Todo: support more customized audio device usage - openDeviceLock sync.Mutex - openedDevice *alsa.Device -) - -func openDevice() (*alsa.Device, error) { - openDeviceLock.Lock() - defer openDeviceLock.Unlock() - - if openedDevice != nil { - return openedDevice, nil - } - cards, err := alsa.OpenCards() - if err != nil { - return nil, err - } - defer alsa.CloseCards(cards) - for i, c := range cards { - dvcs, err := c.Devices() - if err != nil { - continue - } - for _, d := range dvcs { - if d.Type != alsa.PCM || !d.Play { - continue - } - if strings.Contains(d.Title, SkipDevicesContaining) { - continue - } - d.Close() - err := d.Open() - if err != nil { - continue - } - // We've a found a device we can hypothetically use - cards = append(cards[:i], cards[i+1:]...) - openedDevice = d - return d, nil - } - } - return nil, errors.New("No valid device found") -} - -func alsaFormat(bits uint16) (alsa.FormatType, error) { - switch bits { - case 8: - return alsa.S8, nil - case 16: - return alsa.S16_LE, nil - } - return 0, errors.New("Undefined alsa format for encoding bits") -} diff --git a/audio/klang/encode_windows.go b/audio/klang/encode_windows.go deleted file mode 100644 index ea5fdeb5..00000000 --- a/audio/klang/encode_windows.go +++ /dev/null @@ -1,76 +0,0 @@ -//go:build windows -// +build windows - -package klang - -import ( - "syscall" - - "github.com/oakmound/oak/v3/audio/wininternal" - "github.com/oov/directsound-go/dsound" -) - -var ( - user32 = syscall.NewLazyDLL("user32") - getDesktopWindow = user32.NewProc("GetDesktopWindow") - dsoundInterface *dsound.IDirectSound - initErr error -) - -func init() { - cfg, err := wininternal.Init() - if err != nil { - initErr = err - return - } - dsoundInterface = cfg.Interface -} - -// EncodeBytes converts an encoding to Audio -func EncodeBytes(enc Encoding) (Audio, error) { - // An error here would be an error from init() - if initErr != nil { - return nil, initErr - } - - // Create the object which stores the wav data in a playable format - blockAlign := enc.Channels * enc.Bits / 8 - dsbuff, err := dsoundInterface.CreateSoundBuffer(&dsound.BufferDesc{ - // These flags cover everything we should ever want to do - Flags: dsound.DSBCAPS_GLOBALFOCUS | dsound.DSBCAPS_GETCURRENTPOSITION2 | dsound.DSBCAPS_CTRLVOLUME | dsound.DSBCAPS_CTRLPAN | dsound.DSBCAPS_CTRLFREQUENCY | dsound.DSBCAPS_LOCDEFER, - Format: &dsound.WaveFormatEx{ - FormatTag: dsound.WAVE_FORMAT_PCM, - Channels: enc.Channels, - SamplesPerSec: enc.SampleRate, - BitsPerSample: enc.Bits, - BlockAlign: blockAlign, - AvgBytesPerSec: enc.SampleRate * uint32(blockAlign), - ExtSize: 0, - }, - BufferBytes: uint32(len(enc.Data)), - }) - if err != nil { - return nil, err - } - - // Reserve some space in the sound buffer object to write to. - // The Lock function (and by extension LockBytes) actually - // reserves two spaces, but we ignore the second. - by1, by2, err := dsbuff.LockBytes(0, uint32(len(enc.Data)), 0) - if err != nil { - return nil, err - } - - // Write to the pointer we were given. - copy(by1, enc.Data) - - // Update the buffer object with the new data. - err = dsbuff.UnlockBytes(by1, by2) - if err != nil { - return nil, err - } - return &dsAudio{ - Encoding: &enc, - IDirectSoundBuffer: dsbuff, - }, nil -} diff --git a/audio/klang/encoding.go b/audio/klang/encoding.go deleted file mode 100644 index e579f70c..00000000 --- a/audio/klang/encoding.go +++ /dev/null @@ -1,53 +0,0 @@ -package klang - -import "time" - -// Encoding contains all information required to convert raw data -// (currently assumed PCM data but that may/will change) into playable Audio -type Encoding struct { - // Consider: non []byte data? - // Consider: should Data be a type just like Format and CanLoop? - Data []byte - Format - CanLoop -} - -// Copy returns an audio encoded from this encoding. -// Consider: Copy might be tied to HasEncoding -func (enc *Encoding) Copy() (Audio, error) { - return EncodeBytes(*enc.copy()) -} - -// MustCopy acts like Copy, but will panic if err != nil -func (enc *Encoding) MustCopy() Audio { - a, err := EncodeBytes(*enc.copy()) - if err != nil { - panic(err) - } - return a -} - -// GetData satisfies filter.SupportsData -func (enc *Encoding) GetData() *[]byte { - return &enc.Data -} - -// PlayLength returns how long this encoding will play its data for -func (enc *Encoding) PlayLength() time.Duration { - return time.Duration( - 1000000000*float64(len(enc.Data))/ - float64(enc.SampleRate)/ - float64(enc.Channels)/ - float64(enc.Bits/8)) * time.Nanosecond -} - -// copy for an encoding just copies the encoding data, -// it does not return an audio. -func (enc *Encoding) copy() *Encoding { - newEnc := new(Encoding) - newEnc.Format = enc.Format - newEnc.CanLoop = enc.CanLoop - newEnc.Data = make([]byte, len(enc.Data)) - copy(newEnc.Data, enc.Data) - return newEnc -} diff --git a/audio/klang/filter.go b/audio/klang/filter.go deleted file mode 100644 index 271191be..00000000 --- a/audio/klang/filter.go +++ /dev/null @@ -1,24 +0,0 @@ -package klang - -// A Filter takes an input audio and returns some new Audio from them. -// This usage implies that Audios can be copied, and that Audios have -// available information to be generically modified by a Filter. The -// functions for these capabilities are yet fleshed out. It's worth -// considering whether a Filter modifies in place. The answer is -// probably yes: -// a.Filter(fs) would modify a in place -// a.Copy().Filter(fs) would return a new audio -// Specific audio implementations could not follow this, however. -type Filter interface { - Apply(Audio) (Audio, error) -} - -// CanLoop offers composable looping -type CanLoop struct { - Loop bool -} - -// GetLoop allows CanLoop to satisfy the SupportsLoop interface -func (cl *CanLoop) GetLoop() *bool { - return &cl.Loop -} diff --git a/audio/klang/filter/data.go b/audio/klang/filter/data.go deleted file mode 100644 index 6dfadeee..00000000 --- a/audio/klang/filter/data.go +++ /dev/null @@ -1,19 +0,0 @@ -package filter - -import ( - "github.com/oakmound/oak/v3/audio/klang" - "github.com/oakmound/oak/v3/audio/klang/filter/supports" -) - -// Data filters are functions on []byte types -type Data func(*[]byte) - -// Apply checks that the given audio supports Data, filters if it -// can, then returns -func (df Data) Apply(a klang.Audio) (klang.Audio, error) { - if sd, ok := a.(supports.Data); ok { - df(sd.GetData()) - return a, nil - } - return a, supports.NewUnsupported([]string{"Data"}) -} diff --git a/audio/klang/filter/encoding.go b/audio/klang/filter/encoding.go deleted file mode 100644 index b25a68f1..00000000 --- a/audio/klang/filter/encoding.go +++ /dev/null @@ -1,62 +0,0 @@ -package filter - -import ( - "github.com/oakmound/oak/v3/audio/klang" - "github.com/oakmound/oak/v3/audio/klang/filter/supports" - "github.com/oakmound/oak/v3/audio/klang/internal/manip" -) - -// Encoding filters are functions on any combination of the values -// in an audio.Encoding -type Encoding func(supports.Encoding) - -// Apply checks that the given audio supports Encoding, filters if it -// can, then returns -func (enc Encoding) Apply(a klang.Audio) (klang.Audio, error) { - if senc, ok := a.(supports.Encoding); ok { - enc(senc) - return a, nil - } - return a, supports.NewUnsupported([]string{"Encoding"}) -} - -// AssertStereo does nothing to audio that has two channels, but will convert -// mono audio to two-channeled audio with the same data on both channels -func AssertStereo() Encoding { - return func(enc supports.Encoding) { - chs := enc.GetChannels() - if *chs > 1 { - // We can't really do this for non-mono audio - return - } - *chs = 2 - data := enc.GetData() - d := *data - newData := make([]byte, len(d)*2) - byteDepth := int(*enc.GetBitDepth() / 8) - for i := 0; i < len(d); i += 2 { - for j := 0; j < byteDepth; j++ { - newData[i*2+j] = d[i+j] - newData[i*2+j+byteDepth] = d[i+j] - } - } - *data = newData - } -} - -func mod(init, inc int, modFn func(float64) float64) Encoding { - return func(enc supports.Encoding) { - data := enc.GetData() - d := *data - byteDepth := int(*enc.GetBitDepth() / 8) - switch byteDepth { - case 2: - for i := byteDepth * init; i < len(d); i += byteDepth * inc { - manip.SetInt16(d, i, manip.Round(modFn(float64(manip.GetInt16(d, i))))) - } - default: - // log unsupported byte depth - } - *data = d - } -} diff --git a/audio/klang/filter/fourier.go b/audio/klang/filter/fourier.go deleted file mode 100644 index b39ba70a..00000000 --- a/audio/klang/filter/fourier.go +++ /dev/null @@ -1,94 +0,0 @@ -package filter - -// these fourier functions did not work for me. -// In case I can fix them, I leave them here. -// Credit Arnaud Gatouillat - -// fourier1 has a bad name -// fourier1 is a helper function that does some kind of fourier transform math -// What are nn and isign? -// func fourier1(data []float64, nn, isign int) { -// n := nn << 1 -// j := 1 -// for i := 1; i < n; i += 2 { -// if j > i { -// data[j], data[i] = data[i], data[j] -// data[j+1], data[i+1] = data[i+1], data[j+1] -// } -// m := n >> 1 -// for m >= 2 && j > m { -// j -= m -// m >>= 1 -// } -// j += m -// } -// mmax := 2 -// for n > mmax { -// stp := 2 * mmax -// theta := math.Pi * 2 / float64(isign*mmax) -// wpr, wpi := wprWpi(theta) -// wr := 1.0 -// wi := 0.0 -// for m := 1; m < mmax; m += 2 { -// for i := m; i <= n; i += stp { -// tr := wr*data[j] - wi*data[j+1] -// ti := wr*data[j+1] - wi*data[i] -// data[j] = data[i] - tr -// data[j+1] = data[i+1] - ti -// data[i] += tr -// data[i+1] += ti -// } -// wt := wr -// wr = wr*wpr - wi*wpi + wr -// wi = wi*wpr + wt*wpi + wi -// } -// mmax = stp -// } -// } - -// func RealFourierTransform(data []float64, n, isign int) { -// theta := math.Pi / float64(n) -// var c2 float64 -// if isign == 1 { -// c2 = -.5 -// fourier1(data, n, 1) -// } else { -// c2 = .5 -// theta *= -1 -// } -// wpr, wpi := wprWpi(theta) -// wr := 1.0 + wpr -// wi := wpi -// // Wow what a great name for this variable -// n2p3 := 2*n + 3 -// for i := 2; i <= n/2; i++ { -// i1 := i + i - 1 -// i2 := i1 + 1 -// i3 := n2p3 - i2 -// i4 := i3 + 1 -// h1r := .5 * (data[i1] + data[i3]) -// h1i := .5 * (data[i2] - data[i4]) -// h2r := -c2 * (data[i2] + data[i4]) -// h2i := c2 * (data[i1] - data[i3]) -// data[i1] = h1r + wr*h2r - wi*h2i -// data[i2] = h1i + wr*h2i + wi*h2r -// data[i3] = h1r - wr*h2r + wi*h2i -// data[i4] = -h1i + wr*h2i + wi*h2r -// wt := wr -// wr = wr*wpr - wi*wpi + wr -// wi = wi*wpr + wt*wpi + wi -// } -// if isign == 1 { -// data[1], data[2] = (data[1] + data[2]), (data[1] - data[2]) -// } else { -// data[1], data[2] = .5*(data[1]+data[2]), .5*(data[1]-data[2]) -// fourier1(data, n, -1) -// } -// } - -// func wprWpi(theta float64) (float64, float64) { -// w := math.Sin(0.5 * theta) -// wpr := -2 * math.Pow(w, 2) -// wpi := math.Sin(theta) -// return wpr, wpi -// } diff --git a/audio/klang/filter/guarantees.go b/audio/klang/filter/guarantees.go deleted file mode 100644 index 0ac71174..00000000 --- a/audio/klang/filter/guarantees.go +++ /dev/null @@ -1,16 +0,0 @@ -// Package filter provides various audio filters to be applied to audios through the -// Filter function. -package filter - -import ( - "github.com/oakmound/oak/v3/audio/klang" - "github.com/oakmound/oak/v3/audio/klang/filter/supports" -) - -// These declarations guarantee that the filters in this package satisfy the filter interface -var ( - _ klang.Filter = SampleRate(func(*uint32) {}) - _ klang.Filter = Data(func(*[]byte) {}) - _ klang.Filter = Loop(func(*bool) {}) - _ klang.Filter = Encoding(func(supports.Encoding) {}) -) diff --git a/audio/klang/filter/loop.go b/audio/klang/filter/loop.go deleted file mode 100644 index 9cf8d28c..00000000 --- a/audio/klang/filter/loop.go +++ /dev/null @@ -1,34 +0,0 @@ -package filter - -import ( - "github.com/oakmound/oak/v3/audio/klang" - "github.com/oakmound/oak/v3/audio/klang/filter/supports" -) - -// Loop functions modify a boolean, with the intention that that boolean -// is a loop variable -type Loop func(*bool) - -// Apply checks that the given audio supports Loop, filters if it -// can, then returns -func (lf Loop) Apply(a klang.Audio) (klang.Audio, error) { - if sl, ok := a.(supports.Loop); ok { - lf(sl.GetLoop()) - return a, nil - } - return a, supports.NewUnsupported([]string{"Loop"}) -} - -// LoopOn sets the loop to happen -func LoopOn() Loop { - return func(b *bool) { - *b = true - } -} - -// LoopOff sets the loop to not happen -func LoopOff() Loop { - return func(b *bool) { - *b = false - } -} diff --git a/audio/klang/filter/pan.go b/audio/klang/filter/pan.go deleted file mode 100644 index 8e6913bf..00000000 --- a/audio/klang/filter/pan.go +++ /dev/null @@ -1,76 +0,0 @@ -package filter - -import "github.com/oakmound/oak/v3/audio/klang/filter/supports" - -// LeftPan filters audio to only play on the left speaker -func LeftPan() Encoding { - return func(enc supports.Encoding) { - data := enc.GetData() - // Right/Left only makes sense for 2 channel - if *enc.GetChannels() != 2 { - return - } - // Zero out one channel - swtch := int((*enc.GetBitDepth()) / 8) - d := *data - for i := 0; i < len(d); i += (2 * swtch) { - for j := 0; j < swtch; j++ { - d[i+j] = byte((int(d[i+j]) + int(d[i+j+swtch])) / 2) - d[i+j+swtch] = 0 - } - } - *data = d - } -} - -// RightPan filters audio to only play on the right speaker -func RightPan() Encoding { - return func(enc supports.Encoding) { - data := enc.GetData() - // Right/Left only makes sense for 2 channel - if *enc.GetChannels() != 2 { - return - } - // Zero out one channel - swtch := int((*enc.GetBitDepth()) / 8) - d := *data - for i := 0; i < len(d); i += (2 * swtch) { - for j := 0; j < swtch; j++ { - d[i+j+swtch] = byte((int(d[i+j]) + int(d[i+j+swtch])) / 2) - d[i+j] = 0 - } - } - *data = d - } -} - -// Pan takes -1 <= f <= 1. -// An f of -1 represents a full pan to the left, a pan of 1 represents -// a full pan to the right. -func Pan(f float64) Encoding { - // Todo: test this is accurate - if f > 0 { - return VolumeBalance(1-f, 1) - } else if f < 0 { - return VolumeBalance(1, 1-(-1*f)) - } else { - return func(enc supports.Encoding) { - data := enc.GetData() - // Right/Left only makes sense for 2 channel - if *enc.GetChannels() != 2 { - return - } - // Zero out one channel - swtch := int((*enc.GetBitDepth()) / 8) - d := *data - for i := 0; i < len(d); i += (2 * swtch) { - for j := 0; j < swtch; j++ { - v := byte((int(d[i+j]) + int(d[i+j+swtch])) / 2) - d[i+j+swtch] = v - d[i+j] = v - } - } - *data = d - } - } -} diff --git a/audio/klang/filter/pitchshift.go b/audio/klang/filter/pitchshift.go deleted file mode 100644 index 67a41e72..00000000 --- a/audio/klang/filter/pitchshift.go +++ /dev/null @@ -1,297 +0,0 @@ -package filter - -import ( - "math" - - "github.com/oakmound/oak/v3/audio/klang/filter/supports" - "github.com/oakmound/oak/v3/audio/klang/internal/manip" -) - -/***************************************************************************** -* HOME URL: http://blogs.zynaptiq.com/bernsee -* KNOWN BUGS: none -* -* SYNOPSIS: Routine for doing pitch shifting while maintaining -* duration using the Short Time Fourier Transform. -* -* DESCRIPTION: The routine takes a pitchShift factor value which is between 0.5 -* (one octave down) and 2. (one octave up). A value of exactly 1 does not change -* the pitch. numSampsToProcess tells the routine how many samples in indata[0... -* numSampsToProcess-1] should be pitch shifted and moved to outdata[0 ... -* numSampsToProcess-1]. The two buffers can be identical (ie. it can process the -* data in-place). fftFrameSize defines the FFT frame size used for the -* processing. Typical values are 1024, 2048 and 4096. It may be any value <= -* MAX_FRAME_LENGTH but it MUST be a power of 2. osamp is the STFT -* oversampling factor which also determines the overlap between adjacent STFT -* frames. It should at least be 4 for moderate scaling ratios. A value of 32 is -* recommended for best quality. sampleRate takes the sample rate for the signal -* in unit Hz, ie. 44100 for 44.1 kHz audio. The data passed to the routine in -* indata[] should be in the range [-1.0, 1.0), which is also the output range -* for the data, make sure you scale the data accordingly (for 16bit signed integers -* you would have to divide (and multiply) by 32768). -* -* COPYRIGHT 1999-2015 Stephan M. Bernsee -* -* The Wide Open License (WOL) -* -* Permission to use, copy, modify, distribute and sell this software and its -* documentation for any purpose is hereby granted without fee, provided that -* the above copyright notice and this license appear in all source copies. -* THIS SOFTWARE IS PROVIDED "AS IS" WITHOUT EXPRESS OR IMPLIED WARRANTY OF -* ANY KIND. See http://www.dspguru.com/wol.htm for more information. -* -*****************************************************************************/ -// As is standard with translations of this code to other languages, -// Go translation copyright Patrick Stephen 2017 -// To be clear, the PitchShift function + FFT is what had to be translated - -// A PitchShifter has an encoding function that will shift -// a pitch up to an octave up or down (0.5 -> octave down, 2.0 -> octave up) -// these are for lower-level use, and a similar type that takes in steps to -// shift by (and eventually pitches to set to) will follow. -type PitchShifter interface { - PitchShift(float64) Encoding -} - -// FFTShifter holds buffers and settings for performing a pitch shift on PCM audio -type FFTShifter struct { - fftFrameSize int - oversampling int - step int - latency int - stack, frame []float64 - workBuffer []float64 - magnitudes, frequencies []float64 - synthMagnitudes, synthFrequencies []float64 - lastPhase, sumPhase []float64 - outAcc []float64 - expected float64 - window, windowFactors []float64 -} - -// These are built in shifters with some common inputs -var ( - LowQualityShifter, _ = NewFFTShifter(1024, 8) - HighQualityShifter, _ = NewFFTShifter(1024, 32) -) - -// NewFFTShifter returns a pitch shifter that uses fast fourier transforms -func NewFFTShifter(fftFrameSize int, oversampling int) (PitchShifter, error) { - // Todo: check that the frame size and oversampling rate make sense - ps := FFTShifter{} - ps.fftFrameSize = fftFrameSize - ps.oversampling = oversampling - ps.step = fftFrameSize / oversampling - ps.latency = fftFrameSize - ps.step - ps.stack = make([]float64, fftFrameSize) - ps.workBuffer = make([]float64, 2*fftFrameSize) - ps.magnitudes = make([]float64, fftFrameSize) - ps.frequencies = make([]float64, fftFrameSize) - ps.synthMagnitudes = make([]float64, fftFrameSize) - ps.synthFrequencies = make([]float64, fftFrameSize) - ps.lastPhase = make([]float64, fftFrameSize/2+1) - ps.sumPhase = make([]float64, fftFrameSize/2+1) - ps.outAcc = make([]float64, 2*fftFrameSize) - - ps.expected = 2 * math.Pi * float64(ps.step) / float64(fftFrameSize) - - ps.window = make([]float64, fftFrameSize) - ps.windowFactors = make([]float64, fftFrameSize) - t := 0.0 - for i := 0; i < fftFrameSize; i++ { - w := -0.5*math.Cos(t) + .5 - ps.window[i] = w - ps.windowFactors[i] = w * (2.0 / float64(fftFrameSize*oversampling)) - t += (math.Pi * 2) / float64(fftFrameSize) - } - - ps.frame = make([]float64, fftFrameSize) - return ps, nil -} - -// PitchShift modifies filtered audio by the input float, between 0.5 and 2.0, -// each end of the spectrum representing octave down and up respectively -func (ps FFTShifter) PitchShift(shiftBy float64) Encoding { - return func(senc supports.Encoding) { - data := *senc.GetData() - bitDepth := *senc.GetBitDepth() - byteDepth := bitDepth / 8 - sampleRate := *senc.GetSampleRate() - channels := *senc.GetChannels() - - // Jeeez - out := make([]byte, len(data)) - copy(out, data) - - freqPerBin := float64(sampleRate) / float64(ps.fftFrameSize) - frameIndex := ps.latency - - // End jeeeez - - // for each channel individually - for c := 0; c < int(channels); c++ { - // convert this to a channel-specific float64 buffer - f64in := manip.BytesToF64(data, channels, bitDepth, c) - f64out := f64in - - for i := 0; i < len(f64in); i++ { - // Get a frame - ps.frame[frameIndex] = f64in[i] - // Bug here for early i values: they'll all be 0! - f64out[i] = ps.stack[frameIndex-ps.latency] - frameIndex++ - - // A full frame has been obtained - if frameIndex >= ps.fftFrameSize { - frameIndex = ps.latency - - // Windowing - for k := 0; k < ps.fftFrameSize; k++ { - ps.workBuffer[2*k] = ps.frame[k] * ps.window[k] - ps.workBuffer[(2*k)+1] = 0 - } - - ShortTimeFourierTransform(ps.workBuffer, ps.fftFrameSize, -1) - - // Analysis - for k := 0; k <= ps.fftFrameSize/2; k++ { - real := ps.workBuffer[2*k] - imag := ps.workBuffer[(2*k)+1] - - magn := 2 * math.Sqrt(real*real+imag*imag) - ps.magnitudes[k] = magn - - phase := math.Atan2(imag, real) - - diff := phase - ps.lastPhase[k] - ps.lastPhase[k] = phase - - diff -= float64(k) * ps.expected - - deltaPhase := int(diff * (1 / math.Pi)) - if deltaPhase >= 0 { - deltaPhase += deltaPhase & 1 - } else { - deltaPhase -= deltaPhase & 1 - } - - diff -= math.Pi * float64(deltaPhase) - diff *= float64(ps.oversampling) / (math.Pi * 2) - diff = (float64(k) + diff) * freqPerBin - - ps.frequencies[k] = diff - } - - // Processing - for k := 0; k < ps.fftFrameSize; k++ { - ps.synthMagnitudes[k] = 0 - ps.synthFrequencies[k] = 0 - } - - for k := 0; k < ps.fftFrameSize/2; k++ { - l := int(float64(k) * shiftBy) - if l < ps.fftFrameSize/2 { - ps.synthMagnitudes[l] += ps.magnitudes[k] - ps.synthFrequencies[l] = ps.frequencies[k] * shiftBy - } - } - - // Synthesis - for k := 0; k <= ps.fftFrameSize/2; k++ { - magn := ps.synthMagnitudes[k] - tmp := ps.synthFrequencies[k] - tmp -= float64(k) * freqPerBin - tmp /= freqPerBin - tmp *= 2 * math.Pi / float64(ps.oversampling) - tmp += float64(k) * ps.expected - ps.sumPhase[k] += tmp - - ps.workBuffer[2*k] = magn * math.Cos(ps.sumPhase[k]) - ps.workBuffer[(2*k)+1] = magn * math.Sin(ps.sumPhase[k]) - } - - // Remove negative frequencies - // I don't get how we know these ones are negative - // also this looks like it's going to overflow the slice - for k := ps.fftFrameSize + 2; k < 2*ps.fftFrameSize; k++ { - ps.workBuffer[k] = 0.0 - } - - ShortTimeFourierTransform(ps.workBuffer, ps.fftFrameSize, 1) - - // Windowing - for k := 0; k < ps.fftFrameSize; k++ { - ps.outAcc[k] += ps.windowFactors[k] * ps.workBuffer[2*k] - } - for k := 0; k < ps.step; k++ { - ps.stack[k] = ps.outAcc[k] - } - - // Shift accumulator, shift frame - for k := 0; k < ps.fftFrameSize; k++ { - ps.outAcc[k] = ps.outAcc[k+ps.step] - } - - for k := 0; k < ps.latency; k++ { - ps.frame[k] = ps.frame[k+ps.step] - } - } - } - // remap this f64in to the output - for i := c * int(byteDepth); i < len(data); i += int(byteDepth * 2) { - manip.SetInt16F64(out, i, f64in[i/int(byteDepth*2)]) - } - } - datap := senc.GetData() - *datap = out - } -} - -// ShortTimeFourierTransform : FFT routine, (C)1996 S.M.Bernsee. Sign = -1 is FFT, 1 is iFFT (inverse) -// Fills fftBuffer[0...2*fftFrameSize-1] with the Fourier transform of the -// time domain data in fftBuffer[0...2*fftFrameSize-1]. The FFT array takes -// and returns the cosine and sine parts in an interleaved manner, ie. -// fftBuffer[0] = cosPart[0], fftBuffer[1] = sinPart[0], asf. fftFrameSize -// must be a power of 2. It expects a complex input signal (see footnote 2), -// ie. when working with 'common' audio signals our input signal has to be -// passed as {in[0],0.,in[1],0.,in[2],0.,...} asf. In that case, the transform -// of the frequencies of interest is in fftBuffer[0...fftFrameSize]. -func ShortTimeFourierTransform(data []float64, fftFrameSize, sign int) { - for i := 2; i < 2*(fftFrameSize-2); i += 2 { - j := 0 - for bitm := 2; bitm < 2*fftFrameSize; bitm <<= 1 { - if (i & bitm) != 0 { - j++ - } - j <<= 1 - } - if i < j { - data[j], data[i] = data[i], data[j] - data[j+1], data[i+1] = data[i+1], data[j+1] - } - } - max := int(math.Log(float64(fftFrameSize))/math.Log(2) + .5) - le := 2 - for k := 0; k < max; k++ { - le <<= 1 - le2 := le >> 1 - ur := 1.0 - ui := 0.0 - arg := math.Pi / float64(le2>>1) - wr := math.Cos(arg) - wi := float64(sign) * math.Sin(arg) - for j := 0; j < le2; j += 2 { - for i := j; i < 2*fftFrameSize; i += le { - tr := data[i+le2]*ur - data[i+le2+1]*ui - ti := data[i+le2]*ui + data[i+le2+1]*ur - data[i+le2] = data[i] - tr - data[i+le2+1] = data[i+1] - ti - data[i] += tr - data[i+1] += ti - } - tmp := ur*wr - ui*wi - ui = ur*wi + ui*wr - ur = tmp - } - } -} diff --git a/audio/klang/filter/resample.go b/audio/klang/filter/resample.go deleted file mode 100644 index 1dce410d..00000000 --- a/audio/klang/filter/resample.go +++ /dev/null @@ -1,23 +0,0 @@ -package filter - -import ( - "github.com/oakmound/oak/v3/audio/klang/filter/supports" -) - -// Speed modifies the filtered audio by a speed ratio, changing its sample rate -// in the process while maintaining pitch. -func Speed(ratio float64, pitchShifter PitchShifter) Encoding { - return func(senc supports.Encoding) { - r := ratio - for r < .5 { - r *= 2 - pitchShifter.PitchShift(.5)(senc) - } - for r > 2.0 { - r /= 2 - pitchShifter.PitchShift(2.0)(senc) - } - pitchShifter.PitchShift(1 / r)(senc) - ModSampleRate(ratio)(senc.GetSampleRate()) - } -} diff --git a/audio/klang/filter/sampleRate.go b/audio/klang/filter/sampleRate.go deleted file mode 100644 index 6400f528..00000000 --- a/audio/klang/filter/sampleRate.go +++ /dev/null @@ -1,27 +0,0 @@ -package filter - -import ( - "github.com/oakmound/oak/v3/audio/klang" - "github.com/oakmound/oak/v3/audio/klang/filter/supports" -) - -// A SampleRate is a function that takes in uint32 SampleRates -type SampleRate func(*uint32) - -// Apply checks that the given audio supports SampleRate, filters if it -// can, then returns -func (srf SampleRate) Apply(a klang.Audio) (klang.Audio, error) { - if ssr, ok := a.(supports.SampleRate); ok { - srf(ssr.GetSampleRate()) - return a, nil - } - return a, supports.NewUnsupported([]string{"SampleRate"}) -} - -// ModSampleRate might slow down or speed up a sample, but this will -// effect the perceived pitch of the sample. See Speed. -func ModSampleRate(mult float64) SampleRate { - return func(sr *uint32) { - *sr = uint32(float64(*sr) * mult) - } -} diff --git a/audio/klang/filter/supports/supports.go b/audio/klang/filter/supports/supports.go deleted file mode 100644 index 87221d32..00000000 --- a/audio/klang/filter/supports/supports.go +++ /dev/null @@ -1,39 +0,0 @@ -// Package supports holds interface types for filter supports. -package supports - -// Data types support filters that manipulate their raw audio data -type Data interface { - GetData() *[]byte -} - -// Loop types support filters that manipulate whether they loop -type Loop interface { - GetLoop() *bool -} - -// SampleRate types support filters that manipulate their SampleRate -type SampleRate interface { - GetSampleRate() *uint32 -} - -// BitDepth types support filters that manipulate bit depth. Probably -// only useful in combination as an encoding -type BitDepth interface { - GetBitDepth() *uint16 -} - -// Channels types support filters that manipulate channels. Probably -// only useful in combination as an encoding -type Channels interface { - GetChannels() *uint16 -} - -// Encoding types can get any variable on an audio.Encoding. They do -// not just return an audio.Encoding because that would be an import -// loop or another package to avoid said import loop. -type Encoding interface { - SampleRate - BitDepth - Data - Channels -} diff --git a/audio/klang/filter/supports/unsupported.go b/audio/klang/filter/supports/unsupported.go deleted file mode 100644 index 9fd2b875..00000000 --- a/audio/klang/filter/supports/unsupported.go +++ /dev/null @@ -1,20 +0,0 @@ -package supports - -// Unsupported is an error type reporting that a filter was not supported -// by the Audio type it was used on -type Unsupported struct { - filters []string -} - -// NewUnsupported returns an Unsupported error with the input filters -func NewUnsupported(filters []string) Unsupported { - return Unsupported{filters} -} - -func (un Unsupported) Error() string { - s := "Unsupported filters: " - for _, f := range un.filters { - s += f + " " - } - return s -} diff --git a/audio/klang/filter/volume.go b/audio/klang/filter/volume.go deleted file mode 100644 index 48ad79af..00000000 --- a/audio/klang/filter/volume.go +++ /dev/null @@ -1,66 +0,0 @@ -package filter - -import ( - "github.com/oakmound/oak/v3/audio/klang/filter/supports" - "github.com/oakmound/oak/v3/audio/klang/internal/manip" -) - -// Volume will magnify the data by mult, increasing or reducing the volume -// of the output sound. For mult <= 1 this should have no unexpected behavior, -// although for mult ~= 1 it might not have any effect. More importantly for -// mult > 1, values may result in the output data clipping over integer overflows, -// which is presumably not desired behavior. -func Volume(mult float64) Encoding { - return vol(0, 1, mult) -} - -// VolumeLeft acts like volume but reduces left channel volume only -func VolumeLeft(mult float64) Encoding { - return vol(0, 2, mult) -} - -// VolumeRight acts like volume but reduces left channel volume only -func VolumeRight(mult float64) Encoding { - return vol(1, 2, mult) -} - -func vol(init, inc int, mult float64) Encoding { - return mod(init, inc, func(f float64) float64 { - return f * mult - }) -} - -// VolumeBalance will filter audio on two channels such that the left channel -// is (l+r)/2 * lMult, and the right channel is (l+r)/2 * rMult -func VolumeBalance(lMult, rMult float64) Encoding { - return func(enc supports.Encoding) { - if *enc.GetChannels() != 2 { - return - } - data := enc.GetData() - d := *data - byteDepth := int(*enc.GetBitDepth() / 8) - switch byteDepth { - case 2: - for i := 0; i < len(d); i += (byteDepth * 2) { - var v int16 - var shift uint16 - for j := 0; j < byteDepth; j++ { - v += int16(int(d[i+j])+int(d[i+j+byteDepth])) / 2 << shift - shift += 8 - } - l := manip.Round(float64(v) * lMult) - r := manip.Round(float64(v) * rMult) - for j := 0; j < byteDepth; j++ { - d[i+j] = byte(l & 255) - d[i+j+byteDepth] = byte(r & 255) - l >>= 8 - r >>= 8 - } - } - default: - // log unsupported bit depth - } - *data = d - } -} diff --git a/audio/klang/format.go b/audio/klang/format.go deleted file mode 100644 index 04d1d271..00000000 --- a/audio/klang/format.go +++ /dev/null @@ -1,29 +0,0 @@ -package klang - -// Format stores the variables which are presumably -// constant for any given type of audio (wav / mp3 / flac ...) -type Format struct { - SampleRate uint32 - Channels uint16 - Bits uint16 -} - -// GetSampleRate satisfies supports.SampleRate -func (f *Format) GetSampleRate() *uint32 { - return &f.SampleRate -} - -// GetChannels satisfies supports.Channels -func (f *Format) GetChannels() *uint16 { - return &f.Channels -} - -// GetBitDepth satisfied supports.BitDepth -func (f *Format) GetBitDepth() *uint16 { - return &f.Bits -} - -// Wave takes in raw bytes and encodes them according to this format -func (f *Format) Wave(b []byte) (Audio, error) { - return EncodeBytes(Encoding{b, *f, CanLoop{}}) -} diff --git a/audio/klang/internal/manip/convert.go b/audio/klang/internal/manip/convert.go deleted file mode 100644 index 8fa912a1..00000000 --- a/audio/klang/internal/manip/convert.go +++ /dev/null @@ -1,10 +0,0 @@ -package manip - -func BytesToF64(data []byte, channels, bitRate uint16, channel int) []float64 { - byteDepth := bitRate / 8 - out := make([]float64, (len(data)/int(byteDepth*channels))+1) - for i := channel * int(byteDepth); i < len(data); i += int(byteDepth * channels) { - out[i/int(byteDepth*channels)] = GetFloat64(data, i, byteDepth) - } - return out -} diff --git a/audio/klang/internal/manip/math.go b/audio/klang/internal/manip/math.go deleted file mode 100644 index 54ba95e5..00000000 --- a/audio/klang/internal/manip/math.go +++ /dev/null @@ -1,38 +0,0 @@ -package manip - -func SetInt16(d []byte, i int, in int64) { - for j := 0; j < 2; j++ { - d[i+j] = byte(in & 255) - in >>= 8 - } -} - -func GetInt16(d []byte, i int) (out int16) { - var shift uint16 - for j := 0; j < 2; j++ { - out += int16(d[i+j]) << shift - shift += 8 - } - return -} - -func GetFloat64(d []byte, i int, byteDepth uint16) float64 { - switch byteDepth { - case 1: - return float64(int8(d[i])) / 128.0 - case 2: - return float64(GetInt16(d, i)) / 32768.0 - } - return 0.0 -} - -func SetInt16F64(d []byte, i int, in float64) { - SetInt16(d, i, int64(in*32768)) -} - -func Round(f float64) int64 { - if f < 0 { - return int64(f - .5) - } - return int64(f + .5) -} diff --git a/audio/klang/multi.go b/audio/klang/multi.go deleted file mode 100644 index 8f6726bd..00000000 --- a/audio/klang/multi.go +++ /dev/null @@ -1,108 +0,0 @@ -package klang - -import ( - "errors" - "time" -) - -// A Multi lets lists of audios be used simultaneously -type Multi struct { - Audios []Audio -} - -// NewMulti returns a new multi -func NewMulti(as ...Audio) *Multi { - return &Multi{Audios: as} -} - -// Play plays all audios in the Multi ASAP -func (m *Multi) Play() <-chan error { - extCh := make(chan error) - go func() { - // Todo: Propagating N errors? - for _, a := range m.Audios { - a.Play() - } - extCh <- nil - }() - return extCh -} - -// Filter applies all the given filters on everything in the Multi -func (m *Multi) Filter(fs ...Filter) (Audio, error) { - var err, consErr error - for i, a := range m.Audios { - m.Audios[i], err = a.Filter(fs...) - if err != nil { - consErr = errors.New(err.Error() + ":" + consErr.Error()) - } - } - return m, consErr -} - -// MustFilter acts like filter but ignores errors. -func (m *Multi) MustFilter(fs ...Filter) Audio { - a, _ := m.Filter(fs...) - return a -} - -func (m *Multi) SetVolume(vol int32) error { - for _, a := range m.Audios { - err := a.SetVolume(vol) - if err != nil { - return err - } - } - return nil -} - -// Stop stops all audios in the Multi. Any that fail will report an error. -func (m *Multi) Stop() error { - var err, consErr error - for _, a := range m.Audios { - err = a.Stop() - if err != nil { - if consErr == nil { - consErr = err - } else { - consErr = errors.New(err.Error() + ":" + consErr.Error()) - } - } - } - return consErr -} - -// Copy returns a copy of this Multi -func (m *Multi) Copy() (Audio, error) { - var err error - newAudios := make([]Audio, len(m.Audios)) - for i, a := range m.Audios { - newAudios[i], err = a.Copy() - if err != nil { - return nil, err - } - } - return &Multi{newAudios}, nil - -} - -// MustCopy acts like Copy but panics if error != nil -func (m *Multi) MustCopy() Audio { - m2, err := m.Copy() - if err != nil { - panic(err) - } - return m2 -} - -// PlayLength returns how long this audio will play for -func (m *Multi) PlayLength() time.Duration { - var d time.Duration - for _, a := range m.Audios { - d2 := a.PlayLength() - if d < d2 { - d = d2 - } - } - return d -} diff --git a/audio/klang/skip_devices.go b/audio/klang/skip_devices.go deleted file mode 100644 index b9008725..00000000 --- a/audio/klang/skip_devices.go +++ /dev/null @@ -1,19 +0,0 @@ -package klang - -import ( - "os" -) - -// SkipDevicesContaining is a environment variable controlled value -// which will cause audio devices containing the given string to be -// skipped when finding an audio device to play audio through. -// Currently only supported on linux. -// Todo: find a more elegant fix for bad audio devices being chosen -var SkipDevicesContaining = "HDMI" - -func init() { - skipDevices := os.Getenv("KGS_AUDIO_SKIP_DEVICES") - if skipDevices != "" { - SkipDevicesContaining = skipDevices - } -} diff --git a/audio/load.go b/audio/load.go deleted file mode 100644 index bc751599..00000000 --- a/audio/load.go +++ /dev/null @@ -1,122 +0,0 @@ -package audio - -import ( - "path/filepath" - - audio "github.com/oakmound/oak/v3/audio/klang" - "github.com/oakmound/oak/v3/audio/mp3" - "github.com/oakmound/oak/v3/audio/wav" - "golang.org/x/sync/errgroup" - - "github.com/oakmound/oak/v3/dlog" - "github.com/oakmound/oak/v3/fileutil" - "github.com/oakmound/oak/v3/oakerr" -) - -// Data is an alias for an interface supporting the built in filters in our -// underlying audio library -type Data audio.FullAudio - -// Get will read cached audio data from Load, or error if the given -// file is not in the cache. -func (c *Cache) Get(file string) (Data, error) { - c.mu.RLock() - data, ok := c.data[file] - c.mu.RUnlock() - if !ok { - return nil, oakerr.NotFound{InputName: file} - } - return data, nil -} - -// Load loads the given file and caches it by two keys: -// the full file name given and the final element of the file's -// path. If the file cannot be found or if its extension is not -// supported an error will be returned. -func (c *Cache) Load(file string) (Data, error) { - dlog.Verb("Loading", file) - f, err := fileutil.Open(file) - if err != nil { - return nil, err - } - defer f.Close() - var buffer audio.Audio - switch filepath.Ext(file) { - case ".wav": - buffer, err = wav.Load(f) - case ".mp3": - buffer, err = mp3.Load(f) - default: - return nil, oakerr.UnsupportedFormat{Format: filepath.Ext(file)} - } - if err != nil { - return nil, err - } - data := buffer.(audio.FullAudio) - c.setLoaded(file, data) - return data, nil -} - -// BatchLoad attempts to load all files within a given directory -// depending on their file ending -func BatchLoad(baseFolder string) error { - return batchLoad(baseFolder, false) -} - -// BlankBatchLoad acts like BatchLoad, but replaces all loaded assets -// with empty audio constructs. This is intended to reduce start-up -// times in development. -func BlankBatchLoad(baseFolder string) error { - return batchLoad(baseFolder, true) -} - -func batchLoad(baseFolder string, blankOut bool) error { - files, err := fileutil.ReadDir(baseFolder) - if err != nil { - return err - } - - var eg errgroup.Group - for _, file := range files { - if !file.IsDir() { - fileName := file.Name() - switch filepath.Ext(fileName) { - case ".wav", ".mp3": - eg.Go(func() error { - var err error - if blankOut { - dlog.Verb("blank loading file") - err = blankLoad(fileName) - } else { - _, err = DefaultCache.Load(filepath.Join(baseFolder, fileName)) - } - if err != nil { - return err - } - return nil - }) - } - } - } - err = eg.Wait() - return err -} - -func blankLoad(filename string) error { - mformat := audio.Format{ - SampleRate: 44000, - Bits: 16, - Channels: 2, - } - buffer, err := audio.EncodeBytes( - audio.Encoding{ - Format: mformat, - Data: []byte{0, 0, 0, 0}, - }) - if err != nil { - return err - } - data := buffer.(audio.FullAudio) - DefaultCache.setLoaded(filename, data) - return nil -} diff --git a/audio/load_test.go b/audio/load_test.go deleted file mode 100644 index 0b16434f..00000000 --- a/audio/load_test.go +++ /dev/null @@ -1,18 +0,0 @@ -package audio - -import "testing" - -func TestBatchLoad(t *testing.T) { - err := BatchLoad("testdata") - if err != nil { - t.Fatalf("expected batchload on valid path to succeed") - } - err = BlankBatchLoad("testdata") - if err != nil { - t.Fatalf("expected blank batchload on valid path to succeed: %v", err) - } - err = BatchLoad("GarbagePath") - if err == nil { - t.Fatalf("expected batchload on nonexistant path to fail") - } -} diff --git a/audio/mp3/mp3.go b/audio/mp3/mp3.go deleted file mode 100644 index 7e84b947..00000000 --- a/audio/mp3/mp3.go +++ /dev/null @@ -1,40 +0,0 @@ -// Package mp3 provides functionality to handle .mp3 files and .mp3 encoded data. -package mp3 - -import ( - "bytes" - "errors" - "io" - - audio "github.com/oakmound/oak/v3/audio/klang" - - haj "github.com/hajimehoshi/go-mp3" -) - -// Load loads an mp3-encoded reader into an audio -func Load(r io.ReadCloser) (audio.Audio, error) { - d, err := haj.NewDecoder(r) - if err != nil { - return nil, err - } - buf := bytes.NewBuffer(make([]byte, 0, d.Length())) - _, err = io.Copy(buf, d) - if err != nil { - return nil, err - } - mformat := audio.Format{ - SampleRate: uint32(d.SampleRate()), - Bits: 16, - Channels: 2, - } - return audio.EncodeBytes( - audio.Encoding{ - Data: buf.Bytes(), - Format: mformat, - }) -} - -// Save will eventually save an audio encoded as an MP3 to r -func Save(r io.ReadWriter, a audio.Audio) error { - return errors.New("Unsupported Functionality") -} diff --git a/audio/pcm/format.go b/audio/pcm/format.go deleted file mode 100644 index dd43e4f2..00000000 --- a/audio/pcm/format.go +++ /dev/null @@ -1,26 +0,0 @@ -package pcm - -// Format is a PCM format. Equivalent to klang.Format. -type Format struct { - SampleRate uint32 - Channels uint16 - Bits uint16 -} - -// PCMFormat returns this format. -func (f Format) PCMFormat() Format { - return f -} - -// The Formatted interface represents types that are aware of a PCM Format they expect or provide. -type Formatted interface { - // PCMFormat will return the Format used by an encoded audio or expected by an audio consumer. - // Implementations can embed a Format struct to simplify this. - PCMFormat() Format -} - -// BytesPerSecond returns how many bytes this format would be encoded into per second in an audio stream. -func (f Format) BytesPerSecond() uint32 { - blockAlign := f.Channels * f.Bits / 8 - return f.SampleRate * uint32(blockAlign) -} diff --git a/audio/pcm/interface.go b/audio/pcm/interface.go new file mode 100644 index 00000000..73ef5737 --- /dev/null +++ b/audio/pcm/interface.go @@ -0,0 +1,108 @@ +// Package pcm provides a interface for interacting with PCM audio streams +package pcm + +import ( + "fmt" + "io" +) + +var _ Reader = &IOReader{} + +// A Reader mimics io.Reader for pcm data. +type Reader interface { + Formatted + ReadPCM(b []byte) (n int, err error) +} + +// An IOReader converts an io.Reader into a pcm.Reader +type IOReader struct { + Format + io.Reader +} + +func (ior *IOReader) ReadPCM(p []byte) (n int, err error) { + return ior.Read(p) +} + +// A Writer can have PCM formatted audio data written to it. It mimics io.Writer. +type Writer interface { + io.Closer + Formatted + // WritePCM expects PCM bytes matching this Writer's format. + // WritePCM will block until all of the bytes are consumed. + WritePCM([]byte) (n int, err error) +} + +// The Formatted interface represents types that are aware of a PCM Format they expect or provide. +type Formatted interface { + // PCMFormat will return the Format used by an encoded audio or expected by an audio consumer. + // Implementations can embed a Format struct to simplify this. + PCMFormat() Format +} + +// Format is a PCM format; it defines how binary audio data should be converted into real audio. +type Format struct { + // SampleRate defines how many times per second a consumer should read a single value. An example + // of a common value for this is 44100 or 44.1khz. + SampleRate uint32 + // Channels defines how many concurrent audio channels are present in audio data. Common values are + // 1 for mono and 2 for stereo. + Channels uint16 + // Bits determines how many bits a single sample value takes up. 8, 16, and 32 are common values. + // TODO: Do we need LE vs BE, float vs int representation? + Bits uint16 +} + +// PCMFormat returns this format. +func (f Format) PCMFormat() Format { + return f +} + +// BytesPerSecond returns how many bytes this format would be encoded into per second in an audio stream. +func (f Format) BytesPerSecond() uint32 { + return f.SampleRate * uint32(f.SampleSize()) +} + +func (f Format) SampleSize() int { + return int(f.Channels) * int(f.Bits/8) +} + +// ReadFloat reads a single sample from an audio stream, respecting bits and channels: +// f.Bits / 8 bytes * f.Channels bytes will be read from b, and this count will be returned as 'read'. +// the length of values will be equal to f.Channels, if no error is returned. If an error is returned, +// it will be io.ErrUnexpectedEOF or ErrUnsupportedBits +func (f Format) SampleFloat(b []byte) (values []float64, read int, err error) { + values = make([]float64, 0, f.Channels) + read = f.SampleSize() + if len(b) < read { + return nil, 0, io.ErrUnexpectedEOF + } + _ = b[read-1] + switch f.Bits { + case 8: + for i := 0; i < int(f.Channels); i++ { + v := int8(b[i]) + values = append(values, float64(v)) + } + case 16: + for i := 0; i < int(f.Channels)*2; i += 2 { + v := int16(b[i]) + + int16(b[i+1])<<8 + values = append(values, float64(v)) + } + case 32: + for i := 0; i < int(f.Channels)*4; i += 4 { + v := int32(b[i]) + + int32(b[i+1])<<8 + + int32(b[i+2])<<16 + + int32(b[i+3])<<24 + values = append(values, float64(v)) + } + default: + return nil, read, ErrUnsupportedBits + } + return +} + +// ErrUnsupportedBits represents that the Bits value for a Format was not supported for some operation. +var ErrUnsupportedBits = fmt.Errorf("unsupported bits in pcm format") diff --git a/audio/pcm/pcm.go b/audio/pcm/pcm.go deleted file mode 100644 index ab78284e..00000000 --- a/audio/pcm/pcm.go +++ /dev/null @@ -1,114 +0,0 @@ -package pcm - -import ( - "context" - "errors" - "fmt" - "io" - "time" -) - -// WriterBufferLengthInSeconds defines how much data os-level writers provided by this package will rotate through -// in a theoretical circular buffer. -const WriterBufferLengthInSeconds = 1 - -// InitDefault calls Init with the following value by OS: -// windows: DriverDirectSound -// linux,osx: DriverPulse -func InitDefault() error { - return Init(DriverDefault) -} - -// Init initializes the pcm package to create writer objects with a specific audio driver. -func Init(d Driver) error { - return initOS(d) -} - -// A PlayOption sets some value on a PlayOptions struct. -type PlayOption func(*PlayOptions) - -// PlayOptions define ways to configure how playback of some audio proceeds -type PlayOptions struct { - // The span of data that should be copied from reader to writer - // at a time. If too low, may lose accuracy on windows. If too high, - // may require manual resets when changing audio sources. - // Defaults to 125 Milliseconds. - CopyIncrement time.Duration - // How many increments should make up the time between our read and write - // cursors-- i.e. the audio will be playing at X and we will be writing to - // X + ChaseIncrements * CopyIncrement. - // This must be at least 2 to avoid the read and write buffers clipping. - // Defaults to 2. - ChaseIncrements int - // If AllowMismatchedFormats is false, Play will error when a reader's PCM format - // disagrees with a writer's expected PCM format. Defaults to false. - AllowMismatchedFormats bool -} - -func defaultPlayOptions() PlayOptions { - return PlayOptions{ - CopyIncrement: 125 * time.Millisecond, - ChaseIncrements: 2, - } -} - -// ErrMismatchedPCMFormat will be returned by operations streaming from Readers to Writers where the PCM formats -// of those Readers and Writers are not equivalent. -var ErrMismatchedPCMFormat = fmt.Errorf("source and destination have differing PCM formats") - -// Play will copy data from the provided src to the provided dst until ctx is cancelled. This copy is not constant. -// The copy will occur in two phases: first, an initial population of the writer to give distance between the read -// cursor and write cursor; immediately upon this write, the writer should begin playback. Following this setup, a -// sub-second amount of data will streamed from src to dst after waiting that same duration. These wait times can -// be configured via PlayOptions. -func Play(ctx context.Context, dst Writer, src Reader, options ...PlayOption) error { - opts := defaultPlayOptions() - for _, o := range options { - o(&opts) - } - format := dst.PCMFormat() - if !opts.AllowMismatchedFormats { - if srcFormat := src.PCMFormat(); srcFormat != format { - return ErrMismatchedPCMFormat - } - } - defer dst.Reset() - buf := make([]byte, format.BytesPerSecond()/uint32(time.Second/opts.CopyIncrement)) - for i := 0; i < opts.ChaseIncrements; i++ { - // TODO: some formats may expect a minimum buffer size (synth waveforms expect a buffer size of - // at least bits / 8 * channels), and if the sample rate does not evenly divide that expected minimum, - // this can hang. - _, err := ReadFull(src, buf) - if errors.Is(err, io.EOF) { - return nil - } - if err != nil { - return fmt.Errorf("failed to read: %w", err) - } - _, err = dst.WritePCM(buf) - if err != nil { - return fmt.Errorf("failed to write: %w", err) - } - } - - tick := time.NewTicker(opts.CopyIncrement) - defer tick.Stop() - for { - select { - case <-ctx.Done(): - return nil - case <-tick.C: - _, err := ReadFull(src, buf) - if errors.Is(err, io.EOF) { - return nil - } - if err != nil { - return fmt.Errorf("failed to read: %w", err) - } - _, err = dst.WritePCM(buf) - if err != nil { - return fmt.Errorf("failed to write: %w", err) - } - } - } -} diff --git a/audio/pcm/pcm_test.go b/audio/pcm/pcm_test.go deleted file mode 100644 index 17598fae..00000000 --- a/audio/pcm/pcm_test.go +++ /dev/null @@ -1,111 +0,0 @@ -package pcm_test - -import ( - "context" - "fmt" - "math" - "os" - "path/filepath" - "testing" - "time" - - "github.com/oakmound/oak/v3/audio/pcm" - "github.com/oakmound/oak/v3/audio/synth" - "github.com/oakmound/oak/v3/audio/wav" -) - -func TestMain(m *testing.M) { - err := pcm.InitDefault() - if err != nil { - fmt.Println(err) - os.Exit(1) - } - os.Exit(m.Run()) -} - -func TestLoopingWav(t *testing.T) { - f, err := os.Open(filepath.Join("testdata", "test.wav")) - if err != nil { - t.Fatalf("failed to open test file: %v", err) - } - defer f.Close() - kfmt, err := wav.ReadFormat(f) - if err != nil { - t.Fatalf("failed to read wav header in file: %v", err) - } - format := pcm.Format{ - SampleRate: kfmt.SampleRate, - Channels: kfmt.Channels, - Bits: kfmt.Bits, - } - w, err := pcm.NewWriter(format) - if err != nil { - t.Fatalf("failed to create pcm writer: %v", err) - } - r := pcm.LoopReader(&pcm.IOReader{ - Format: format, - Reader: f, - }) - ctx, cancel := context.WithCancel(context.Background()) - go func() { - err = pcm.Play(ctx, w, r) - if err != nil { - t.Errorf("failed to play: %v", err) - } - }() - time.Sleep(10 * time.Second) - fmt.Println("stopping") - cancel() - time.Sleep(1 * time.Second) -} - -func TestLoopingSin(t *testing.T) { - format := pcm.Format{ - SampleRate: 44100, - Channels: 2, - Bits: 16, - } - w, err := pcm.NewWriter(format) - if err != nil { - t.Fatalf("failed to create pcm writer: %v", err) - } - - s := synth.Int16 - - s.Volume *= 65535 / 4 - wave := make([]int16, int(s.Seconds*float64(s.SampleRate))) - for i := 0; i < len(wave); i++ { - wave[i] = int16(s.Volume * math.Sin(s.Phase(i))) - } - b := bytesFromInts(wave, int(s.Channels)) - r := pcm.LoopReader(&pcm.BytesReader{ - Buffer: b, - Format: format, - }) - ctx, cancel := context.WithCancel(context.Background()) - go func() { - err = pcm.Play(ctx, w, r) - if err != nil { - t.Errorf("failed to play: %v", err) - } - }() - time.Sleep(10 * time.Second) - fmt.Println("stopping") - cancel() - time.Sleep(1 * time.Second) -} - -func bytesFromInts(is []int16, channels int) []byte { - var ratio = channels * 2 - wave := make([]byte, len(is)*ratio) - for i := 0; i < len(wave); i += ratio { - wave[i] = byte(is[i/ratio]) - wave[i+1] = byte(is[i/ratio] >> 8) - // duplicate the contents across all channels - for c := 1; c < channels; c++ { - wave[i+(2*c)] = wave[i] - wave[i+(2*c)+1] = wave[i+1] - } - } - return wave -} diff --git a/audio/pcm/writer.go b/audio/pcm/writer.go deleted file mode 100644 index 375570bc..00000000 --- a/audio/pcm/writer.go +++ /dev/null @@ -1,20 +0,0 @@ -package pcm - -import "io" - -// NewWriter returns a writer which can accept audio streamed matching the given format -func NewWriter(f Format) (Writer, error) { - return newWriter(f) -} - -// A Writer can have PCM formatted audio data written to it. It mimics io.Writer. -type Writer interface { - io.Closer - Formatted - // WritePCM expects PCM bytes matching the format this speaker was initialized with. - // WritePCM will block until all of the bytes are consumed. - WritePCM([]byte) (n int, err error) - // Reset must clear out any written data from buffers, without stopping playback - // TODO: do we need this? - Reset() error -} diff --git a/audio/play.go b/audio/play.go index f2ce1fb1..cbcd299a 100644 --- a/audio/play.go +++ b/audio/play.go @@ -1,28 +1,170 @@ +// Package audio provides utilities for playing or writing audio streams to OS consumers package audio import ( - "github.com/oakmound/oak/v3/audio/font" - "github.com/oakmound/oak/v3/dlog" + "context" + "errors" + "fmt" + "io" + "time" + + "github.com/oakmound/oak/v4/audio/pcm" ) -// DefaultFont is the font used for default functions. It can be publicly -// modified to apply a default font to generated audios through def -// methods. If it is not modified, it is a font of zero filters. -var DefaultFont = font.New() - -// Play is shorthand for Get followed by Play on the DefaultCache. -func Play(f *font.Font, filename string) error { - ad, err := DefaultCache.Get(filename) - if err == nil { - a := New(f, ad) - a.Play() - } else { - dlog.Error(err) +// WriterBufferLengthInSeconds defines how much data os-level writers provided by this package will rotate through +// in a theoretical circular buffer. +const WriterBufferLengthInSeconds float64 = .5 + +// InitDefault calls Init with the following value by OS: +// windows: DriverDirectSound +// linux,osx: DriverPulse +func InitDefault() error { + return Init(DriverDefault) +} + +// Init initializes the pcm package to create writer objects with a specific audio driver. +func Init(d Driver) error { + return initOS(d) +} + +// A PlayOption sets some value on a PlayOptions struct. +type PlayOption func(*PlayOptions) + +// PlayOptions define ways to configure how playback of some audio proceeds +type PlayOptions struct { + // If FadeOutOnStop is non-zero, when this play is stopped early it will fade out for this duration. + FadeOutOnStop time.Duration + + // If Destination is not provided, Play will create a new writer which will be + // closed after Play is complete. + Destination pcm.Writer + + // The span of data that should be copied from reader to writer + // at a time. If too low, may lose accuracy on windows. If too high, + // may require manual resets when changing audio sources. + // Defaults to 50 Milliseconds. + CopyIncrement time.Duration + // How many increments should make up the time between our read and write + // cursors-- i.e. the audio will be playing at X and we will be writing to + // X + ChaseIncrements * CopyIncrement. + // This must be at least 2 to avoid the read and write buffers clipping. + // Defaults to 2. + ChaseIncrements int + // If AllowMismatchedFormats is false, Play will error when a reader's PCM format + // disagrees with a writer's expected PCM format. Defaults to false. + AllowMismatchedFormats bool + + ClearBufferOnStop bool +} + +func defaultPlayOptions() PlayOptions { + return PlayOptions{ + CopyIncrement: 50 * time.Millisecond, + ChaseIncrements: 2, + FadeOutOnStop: 75 * time.Millisecond, + ClearBufferOnStop: true, } - return err } -// DefaultPlay is shorthand for Play(DefaultFont, filename) -func DefaultPlay(filename string) error { - return Play(DefaultFont, filename) +// ErrMismatchedPCMFormat will be returned by operations streaming from Readers to Writers where the PCM formats +// of those Readers and Writers are not equivalent. +var ErrMismatchedPCMFormat = fmt.Errorf("source and destination have differing PCM formats") + +// Play will copy data from the provided src to the provided dst until ctx is cancelled. This copy is not constant. +// The copy will occur in two phases: first, an initial population of the writer to give distance between the read +// cursor and write cursor; immediately upon this write, the writer should begin playback. Following this setup, a +// sub-second amount of data will streamed from src to dst after waiting that same duration. These wait times can +// be configured via PlayOptions. +func Play(ctx context.Context, src pcm.Reader, options ...PlayOption) error { + opts := defaultPlayOptions() + for _, o := range options { + o(&opts) + } + if opts.Destination == nil { + var err error + opts.Destination, err = NewWriter(src.PCMFormat()) + if err != nil { + return err + } + defer opts.Destination.Close() + } + format := opts.Destination.PCMFormat() + if !opts.AllowMismatchedFormats { + if srcFormat := src.PCMFormat(); srcFormat != format { + return ErrMismatchedPCMFormat + } + } + buf := make([]byte, format.BytesPerSecond()/uint32(time.Second/opts.CopyIncrement)) + for i := 0; i < opts.ChaseIncrements; i++ { + // TODO: some formats may expect a minimum buffer size (synth waveforms expect a buffer size of + // at least bits / 8 * channels), and if the sample rate does not evenly divide that expected minimum, + // this can hang. + _, err := ReadFull(src, buf) + if errors.Is(err, io.EOF) { + return nil + } + if err != nil { + return fmt.Errorf("failed to read: %w", err) + } + _, err = opts.Destination.WritePCM(buf) + if err != nil { + return fmt.Errorf("failed to write: %w", err) + } + } + + tick := time.NewTicker(opts.CopyIncrement) + defer tick.Stop() + // Once we're done, keep writing empty data until the buffer is cleared, unless told not to + // Do not clear this buffer immediately! You will clear audio data that is actively playing, which will clip! + if opts.ClearBufferOnStop { + defer func() { + buf = make([]byte, format.BytesPerSecond()/uint32(time.Second/opts.CopyIncrement)) + for totalDur := time.Duration(0); totalDur < time.Duration(float64(time.Second)*WriterBufferLengthInSeconds); totalDur += opts.CopyIncrement { + <-tick.C + opts.Destination.WritePCM(buf) + } + }() + } + for { + select { + case <-ctx.Done(): + if opts.FadeOutOnStop == 0 { + return nil + } else { + src = FadeOut(opts.FadeOutOnStop, src) + stopAt := time.NewTimer(opts.FadeOutOnStop * 2) + defer stopAt.Stop() + for { + select { + case <-stopAt.C: + return nil + case <-tick.C: + _, err := ReadFull(src, buf) + if errors.Is(err, io.EOF) { + return nil + } + if err != nil { + return fmt.Errorf("failed to read: %w", err) + } + _, err = opts.Destination.WritePCM(buf) + if err != nil { + return fmt.Errorf("failed to write: %w", err) + } + } + } + } + case <-tick.C: + _, err := ReadFull(src, buf) + if errors.Is(err, io.EOF) { + return nil + } + if err != nil { + return fmt.Errorf("failed to read: %w", err) + } + _, err = opts.Destination.WritePCM(buf) + if err != nil { + return fmt.Errorf("failed to write: %w", err) + } + } + } } diff --git a/audio/play_test.go b/audio/play_test.go index 46e53d6e..debd5d3d 100644 --- a/audio/play_test.go +++ b/audio/play_test.go @@ -1,38 +1,109 @@ -package audio +package audio_test import ( + "context" + "fmt" + "math" + "os" "path/filepath" "testing" "time" + + "github.com/oakmound/oak/v4/audio" + "github.com/oakmound/oak/v4/audio/format/wav" + "github.com/oakmound/oak/v4/audio/pcm" + "github.com/oakmound/oak/v4/audio/synth" ) -func TestPlayAndLoad(t *testing.T) { - _, err := Load(filepath.Join("testdata", "test.wav")) +func TestMain(m *testing.M) { + err := audio.InitDefault() if err != nil { - t.Fatalf("failed to load test.wav") - } - _, err = Load("badfile.wav") - if err == nil { - t.Fatalf("expected loading badfile to fail") - } - _, err = Load("play_test.go") - if err == nil { - t.Fatalf("expected loading non-wav file to fail") + fmt.Println(err) + os.Exit(1) } - err = Play(DefaultFont, "test.wav") + os.Exit(m.Run()) +} + +func TestLoopingWav(t *testing.T) { + f, err := os.Open(filepath.Join("testdata", "test.wav")) if err != nil { - t.Fatalf("failed to play test.wav (1)") + t.Fatalf("failed to open test file: %v", err) } - time.Sleep(1 * time.Second) - err = DefaultPlay("test.wav") + defer f.Close() + wavReader, err := wav.Load(f) if err != nil { - t.Fatalf("failed to play test.wav (2)") + t.Fatalf("failed to read wav header in file: %v", err) + } + ctx, cancel := context.WithCancel(context.Background()) + go func() { + err = audio.Play(ctx, audio.LoopReader(wavReader)) + if err != nil { + t.Errorf("failed to play: %v", err) + } + }() + if testing.Short() { + time.Sleep(100 * time.Millisecond) + } else { + time.Sleep(10 * time.Second) } + fmt.Println("stopping") + cancel() time.Sleep(1 * time.Second) - // Assert something was played twice - DefaultCache.Clear("test.wav") - err = Play(DefaultFont, "test.wav") - if err == nil { - t.Fatalf("expected playing unloaded test.wav to fail") +} + +func TestLoopingSin(t *testing.T) { + format := pcm.Format{ + SampleRate: 44100, + Channels: 2, + Bits: 16, + } + s := synth.Int16 + + s.Volume *= 65535 / 4 + wave := make([]int16, int(s.Seconds*float64(s.SampleRate))) + for i := 0; i < len(wave); i++ { + wave[i] = int16(s.Volume * math.Sin(s.Phase(i))) + } + b := bytesFromInts(wave, int(s.Channels)) + r := audio.LoopReader(&audio.BytesReader{ + Buffer: b, + Format: format, + }) + ctx, cancel := context.WithCancel(context.Background()) + done := make(chan struct{}) + go func() { + err := audio.Play(ctx, r) + if err != nil { + t.Errorf("failed to play: %v", err) + } + close(done) + }() + if testing.Short() { + time.Sleep(100 * time.Millisecond) + } else { + time.Sleep(10 * time.Second) + } + fmt.Println("stopping") + cancel() + select { + case <-done: + case <-time.After(1 * time.Second): + t.Errorf("play did not exit on cancel") + } + +} + +func bytesFromInts(is []int16, channels int) []byte { + var ratio = channels * 2 + wave := make([]byte, len(is)*ratio) + for i := 0; i < len(wave); i += ratio { + wave[i] = byte(is[i/ratio]) + wave[i+1] = byte(is[i/ratio] >> 8) + // duplicate the contents across all channels + for c := 1; c < channels; c++ { + wave[i+(2*c)] = wave[i] + wave[i+(2*c)+1] = wave[i+1] + } } + return wave } diff --git a/audio/posFilter.go b/audio/posFilter.go deleted file mode 100644 index 7efa15ed..00000000 --- a/audio/posFilter.go +++ /dev/null @@ -1,52 +0,0 @@ -package audio - -import ( - "github.com/oakmound/oak/v3/audio/klang" - "github.com/oakmound/oak/v3/audio/klang/filter" - "github.com/oakmound/oak/v3/audio/klang/filter/supports" - "github.com/oakmound/oak/v3/physics" -) - -// SupportsPos is a type used by filters to check that the audio they are given -// has a position. -type SupportsPos interface { - supports.Encoding - Xp() *float64 - Yp() *float64 -} - -var ( - _ klang.Filter = Pos(func(SupportsPos) {}) -) - -// Pos functions are filters that require a SupportsPos interface -type Pos func(SupportsPos) - -// Apply is a function allowing Pos to satisfy the audio.Filter interface. -// Pos applies itself to any audio it is given that supports it. -func (xp Pos) Apply(a klang.Audio) (klang.Audio, error) { - if sxp, ok := a.(SupportsPos); ok { - xp(sxp) - return a, nil - } - return a, nil //, supports.NewUnsupported([]string{"Pos"}) -} - -// PosFilter is the only Pos generating function right now. It takes in ears -// to listen from and changes incoming audio to be quiter and panned based -// on positional relation to those ears. -func PosFilter(e *Ears) Pos { - return func(sp SupportsPos) { - filter.AssertStereo()(sp) - x := sp.Xp() - if x != nil { - p := e.CalculatePan(*x) - filter.Pan(p)(sp) - y := sp.Yp() - if y != nil { - v := e.CalculateVolume(physics.NewVector(*x, *y)) - filter.Volume(v)(sp) - } - } - } -} diff --git a/audio/posFilter_test.go b/audio/posFilter_test.go deleted file mode 100644 index 05f49eeb..00000000 --- a/audio/posFilter_test.go +++ /dev/null @@ -1,60 +0,0 @@ -package audio - -import ( - "testing" - "time" - - "github.com/oakmound/oak/v3/audio/font" - "github.com/oakmound/oak/v3/audio/synth" -) - -func TestPosFilter(t *testing.T) { - kla, err := synth.Int16.Sin() - if err != nil { - t.Fatalf("expected sin wave creation to succeed") - } - x, y := new(float64), new(float64) - a := New(DefaultFont, kla.(Data), x, y) - x2 := 100.0 - y2 := 100.0 - DefaultFont.Filter(PosFilter(NewEars(&x2, &y2, 100, 300))) - err = <-a.Play() - if err != nil { - t.Fatalf("expected playing sin wave to succeed") - } - time.Sleep(a.PlayLength()) - // Assert left ear hears audio - x2 -= 200 - err = <-a.Play() - if err != nil { - t.Fatalf("expected playing sin wave (2) to succeed") - } - time.Sleep(a.PlayLength()) - // Assert right ear hears audio - y2 += 500 - err = <-a.Play() - if err != nil { - t.Fatalf("expected playing sin wave (3) to succeed") - } - time.Sleep(a.PlayLength()) - // Assert nothing is heard - *DefaultFont = *font.New() - DefaultFont.Filter(PosFilter(NewEars(&x2, &y2, 100, 2000))) - x2 -= 200 - err = <-a.Play() - if err != nil { - t.Fatalf("expected playing sin wave (4) to succeed") - } - time.Sleep(a.PlayLength()) - // Assert right ear hears audio - x2 += 1000 - err = <-a.Play() - if err != nil { - t.Fatalf("expected playing sin wave (5) to succeed") - } - time.Sleep(a.PlayLength()) - // Assert left ear hears audio - - _, _ = kla.Filter(PosFilter(NewEars(&x2, &y2, 0, 0))) - // assert.NotNil(t, err) -} diff --git a/audio/pcm/reader.go b/audio/reader.go similarity index 65% rename from audio/pcm/reader.go rename to audio/reader.go index be973946..ac460b31 100644 --- a/audio/pcm/reader.go +++ b/audio/reader.go @@ -1,22 +1,17 @@ -package pcm +package audio import ( "errors" "io" -) -var _ Reader = &LoopingReader{} -var _ Reader = &BytesReader{} -var _ Reader = &IOReader{} + "github.com/oakmound/oak/v4/audio/pcm" +) -// A Reader mimics io.Reader for pcm data. -type Reader interface { - Formatted - ReadPCM(b []byte) (n int, err error) -} +var _ pcm.Reader = &LoopingReader{} +var _ pcm.Reader = &BytesReader{} // LoopReader will cache read bytes as they are read and resend them after the reader returns EOF. -func LoopReader(r Reader) Reader { +func LoopReader(r pcm.Reader) pcm.Reader { return &LoopingReader{Reader: r} } @@ -24,7 +19,7 @@ func LoopReader(r Reader) Reader { // from the reader will be cached after read within the LoopingReader structure, potentially inflating memory // if provided a large stream. type LoopingReader struct { - Reader + pcm.Reader buffer []byte bufferPos int eofReached bool @@ -59,7 +54,7 @@ func (l *LoopingReader) ReadPCM(p []byte) (n int, err error) { // A BytesReader acts like a bytes.Buffer for converting raw []bytes into pcm Readers. type BytesReader struct { - Format + pcm.Format Buffer []byte Offset int } @@ -74,15 +69,49 @@ func (b *BytesReader) ReadPCM(p []byte) (n int, err error) { return len(p), nil } +func (b *BytesReader) Copy() *BytesReader { + copyBuff := make([]byte, len(b.Buffer)) + copy(b.Buffer, copyBuff) + return &BytesReader{ + Format: b.Format, + Buffer: b.Buffer, + Offset: b.Offset, + } +} + +// ReadAll will read all of the content within a reader and convert it into a BytesReader. Use carefully; use on +// a LoopingReader or reader which generates its data (e.g. synth types) will likely read until OOM. +func ReadAll(r pcm.Reader) *BytesReader { + b := make([]byte, 0, 512) + for { + if len(b) == cap(b) { + // Add more capacity (let append pick how much). + b = append(b, 0)[:len(b)] + } + n, err := r.ReadPCM(b[len(b):cap(b)]) + b = b[:len(b)+n] + if err != nil { + if err == io.EOF { + err = nil + } + break + } + } + return &BytesReader{ + Format: r.PCMFormat(), + Buffer: b, + } +} + // ReadFull acts like io.ReadFull with a pcm Reader. It will read until the provided buffer // is competely populated by the reader. -func ReadFull(r Reader, buf []byte) (n int, err error) { +func ReadFull(r pcm.Reader, buf []byte) (n int, err error) { return ReadAtLeast(r, buf, len(buf)) } // ReadAtLeast acts like io.ReadAtLeast with a pcm Reader. It will read until at least min // bytes have been read into the provided buffer. -func ReadAtLeast(r Reader, buf []byte, min int) (n int, err error) { +func ReadAtLeast(r pcm.Reader, buf []byte, min int) (n int, err error) { if len(buf) < min { return 0, io.ErrShortBuffer } @@ -98,13 +127,3 @@ func ReadAtLeast(r Reader, buf []byte, min int) (n int, err error) { } return } - -// An IOReader converts an io.Reader into a pcm.Reader -type IOReader struct { - Format - io.Reader -} - -func (ior *IOReader) ReadPCM(p []byte) (n int, err error) { - return ior.Read(p) -} diff --git a/audio/sequence/chordPattern.go b/audio/sequence/chordPattern.go deleted file mode 100644 index 900cdc25..00000000 --- a/audio/sequence/chordPattern.go +++ /dev/null @@ -1,36 +0,0 @@ -package sequence - -import ( - "time" - - "github.com/oakmound/oak/v3/audio/synth" -) - -// A ChordPattern represents the order of pitches and holds -// for each of those pitches over a sequence of (potential) -// chords. Todo: pitchPattern is a subset of this, should -// it even exist? -type ChordPattern struct { - Pitches [][]synth.Pitch - Holds [][]time.Duration -} - -// HasChords lets generators be built from chord Options -// if they have a pointer to a chord pattern -type HasChords interface { - GetChordPattern() *ChordPattern -} - -// GetChordPattern returns a pointer to a generator's chord pattern -func (cp *ChordPattern) GetChordPattern() *ChordPattern { - return cp -} - -// Chords sets the generator's chord pattern -func Chords(cp ChordPattern) Option { - return func(g Generator) { - if hcp, ok := g.(HasChords); ok { - *(hcp.GetChordPattern()) = cp - } - } -} diff --git a/audio/sequence/generator.go b/audio/sequence/generator.go deleted file mode 100644 index edbe635c..00000000 --- a/audio/sequence/generator.go +++ /dev/null @@ -1,20 +0,0 @@ -package sequence - -// A Generator stores settings to create a sequence -type Generator interface { - Generate() *Sequence -} - -// Option types are inserted into Constructors to create generators -type Option func(Generator) - -// And combines any number of options into a single option. -// And is a reminder that you can store combined settings to avoid -// having to rewrite them -func And(opts ...Option) Option { - return func(g Generator) { - for _, opt := range opts { - opt(g) - } - } -} diff --git a/audio/sequence/holdPattern.go b/audio/sequence/holdPattern.go deleted file mode 100644 index df0e02f7..00000000 --- a/audio/sequence/holdPattern.go +++ /dev/null @@ -1,76 +0,0 @@ -package sequence - -import "time" - -// A HoldPattern is a pattern that might loop on itself for how long notes -// should be held -type HoldPattern []time.Duration - -// HasHolds enables generators to be built from HoldPattern and use the -// related option functions -type HasHolds interface { - GetHoldPattern() *[]time.Duration -} - -// GetHoldPattern lets composing HoldPattern satisfy HasHolds -func (hp *HoldPattern) GetHoldPattern() *HoldPattern { - return hp -} - -// Holds sets the generator's Hold pattern -func Holds(vs ...time.Duration) Option { - return func(g Generator) { - if hhs, ok := g.(HasHolds); ok { - *hhs.GetHoldPattern() = vs - } - } -} - -// HoldAt sets the n'th value in the entire play sequence -// to be Hold p. This could involve duplicating a pattern -// until it is long enough to reach n. Meaningless if the -// Hold pattern has not been set yet. -func HoldAt(t time.Duration, n int) Option { - return func(g Generator) { - if hhs, ok := g.(HasHolds); ok { - if hl, ok := hhs.(HasLength); ok { - if hl.GetLength() < n { - hp := hhs.GetHoldPattern() - Holds := *hp - if len(Holds) == 0 { - return - } - // If the pattern is not long enough, there are two things - // we could do-- 1. Extend the pattern and replace the - // individual note, or 2. Replace the note that would be - // played at n and thus all earlier and later plays within - // the pattern as well. - // - // This uses approach 1. - for len(Holds) <= n { - Holds = append(Holds, Holds...) - } - Holds[n] = t - *hp = Holds - } - } - } - } -} - -// HoldPatternAt sets the n'th value in the Hold pattern -// to be Hold p. Meaningless if the Hold pattern has not -// been set yet. -func HoldPatternAt(t time.Duration, n int) Option { - return func(g Generator) { - if hhs, ok := g.(HasHolds); ok { - hp := hhs.GetHoldPattern() - Holds := *hp - if len(Holds) <= n { - return - } - Holds[n] = t - *hp = Holds - } - } -} diff --git a/audio/sequence/length.go b/audio/sequence/length.go deleted file mode 100644 index c923d6ed..00000000 --- a/audio/sequence/length.go +++ /dev/null @@ -1,24 +0,0 @@ -package sequence - -type Length int - -type HasLength interface { - GetLength() int - SetLength(int) -} - -func (l *Length) GetLength() int { - return int(*l) -} - -func (l *Length) SetLength(i int) { - *l = Length(i) -} - -func PlayLength(i int) Option { - return func(g Generator) { - if l, ok := g.(HasLength); ok { - l.SetLength(i) - } - } -} diff --git a/audio/sequence/loop.go b/audio/sequence/loop.go deleted file mode 100644 index 4c42eee0..00000000 --- a/audio/sequence/loop.go +++ /dev/null @@ -1,25 +0,0 @@ -package sequence - -type Loop bool - -type HasLoops interface { - GetLoop() bool - SetLoop(bool) -} - -func (l *Loop) GetLoop() bool { - return bool(*l) -} - -func (l *Loop) SetLoop(b bool) { - *l = Loop(b) -} - -// Loops sets the generator's Loop -func Loops(b bool) Option { - return func(g Generator) { - if ht, ok := g.(HasLoops); ok { - ht.SetLoop(b) - } - } -} diff --git a/audio/sequence/pitchPattern.go b/audio/sequence/pitchPattern.go deleted file mode 100644 index 2d6d8e15..00000000 --- a/audio/sequence/pitchPattern.go +++ /dev/null @@ -1,74 +0,0 @@ -package sequence - -import "github.com/oakmound/oak/v3/audio/synth" - -type PitchPattern []synth.Pitch - -type HasPitches interface { - GetPitchPattern() []synth.Pitch - SetPitchPattern([]synth.Pitch) -} - -func (pp *PitchPattern) GetPitchPattern() []synth.Pitch { - return *pp -} - -func (pp *PitchPattern) SetPitchPattern(ps []synth.Pitch) { - *pp = ps -} - -// Pitches sets the generator's pitch pattern -func Pitches(ps ...synth.Pitch) Option { - return func(g Generator) { - if hpp, ok := g.(HasPitches); ok { - hpp.SetPitchPattern(ps) - } - } -} - -// PitchAt sets the n'th value in the entire play sequence -// to be pitch p. This could involve duplicating a pattern -// until it is long enough to reach n. Meaningless if the -// pitch pattern has not been set yet. -func PitchAt(p synth.Pitch, n int) Option { - return func(g Generator) { - if hpp, ok := g.(HasPitches); ok { - if hl, ok := hpp.(HasLength); ok { - if hl.GetLength() < n { - pitches := hpp.GetPitchPattern() - if len(pitches) == 0 { - return - } - // If the pattern is not long enough, there are two things - // we could do-- 1. Extend the pattern and replace the - // individual note, or 2. Replace the note that would be - // played at n and thus all earlier and later plays within - // the pattern as well. - // - // This uses approach 1. - for len(pitches) < n { - pitches = append(pitches, pitches...) - } - pitches[n] = p - hpp.SetPitchPattern(pitches) - } - } - } - } -} - -// PitchPatternAt sets the n'th value in the pitch pattern -// to be pitch p. Meaningless if the pitch pattern has not -// been set yet. -func PitchPatternAt(p synth.Pitch, n int) Option { - return func(g Generator) { - if hpp, ok := g.(HasPitches); ok { - pitches := hpp.GetPitchPattern() - if len(pitches) < n { - return - } - pitches[n] = p - hpp.SetPitchPattern(pitches) - } - } -} diff --git a/audio/sequence/sequence.go b/audio/sequence/sequence.go deleted file mode 100644 index 4cb14b66..00000000 --- a/audio/sequence/sequence.go +++ /dev/null @@ -1,172 +0,0 @@ -// Package sequence provides generators and options for creating audio sequences. -package sequence - -import ( - "errors" - "time" - - audio "github.com/oakmound/oak/v3/audio/klang" -) - -// A Sequence is a timed pattern of simultaneously played audios. -type Sequence struct { - // Sequences play patterns of audio - // everything at Pattern[0] will be simultaneously Play()ed at - // Sequence.Play() - Pattern []*audio.Multi - patternIndex int - // Every tick, the next index in Pattern will be played by a Sequence - // until the pattern is over. - Ticker *time.Ticker - // needed to copy Ticker - // consider: replacing ticker with dynamic ticker - tickDuration time.Duration - stopCh chan error - loop bool -} - -// Play on a sequence plays the pattern encoded in the sequence until stopped -func (s *Sequence) Play() <-chan error { - ch := make(chan error) - go func() { - for { - s.patternIndex = 0 - for s.patternIndex < len(s.Pattern) { - s.Pattern[s.patternIndex].Play() - select { - case <-s.stopCh: - s.stopCh <- s.Pattern[s.patternIndex].Stop() - ch <- nil - return - case <-s.Ticker.C: - } - s.patternIndex++ - } - if !s.loop { - ch <- nil - return - } - } - }() - return ch -} - -// Filter for a sequence does nothing yet -func (s *Sequence) Filter(fs ...audio.Filter) (audio.Audio, error) { - // Filter on a sequence just applies the filter to all audios.. - // but it can't do that always, what if the filter is Loop? - // this implies two kinds of filters? - // this doesn't work because FIlter is not an interface - // for _, f := range fs { - // if _, ok := f.(audio.Loop); ok { - // s.loop = true - // } else if _, ok := f.(audio.NoLoop); ok { - // s.loop = false - // } else { - // for _, col := range s.Pattern { - // for _, a := range col { - // a.Filter(f) - // } - // } - // } - // } - return s, nil -} - -func (s *Sequence) SetVolume(int32) error { - return errors.New("unsupported") -} - -// MustFilter acts as filter, but does not respect errors. -func (s *Sequence) MustFilter(fs ...audio.Filter) audio.Audio { - a, _ := s.Filter(fs...) - return a -} - -// Stop stops a sequence -func (s *Sequence) Stop() error { - s.stopCh <- nil - return <-s.stopCh -} - -// Copy copies a sequence -func (s *Sequence) Copy() (audio.Audio, error) { - var err error - s2 := &Sequence{ - Pattern: make([]*audio.Multi, len(s.Pattern)), - Ticker: time.NewTicker(s.tickDuration), - tickDuration: s.tickDuration, - stopCh: make(chan error), - loop: s.loop, - } - for i := range s2.Pattern { - s2.Pattern[i] = new(audio.Multi) - s2.Pattern[i].Audios = make([]audio.Audio, len(s.Pattern[i].Audios)) - for j := range s2.Pattern[i].Audios { - // This could make a sequence that reuses the same - // audio use a lot more memory when copied-- a better route - // would involve identifying all unique audios - // and making a copy for each of those, but that - // requires producing unique IDs for each audio - // (which would probably be a hash of their encoding? - // but that raises issues for audios that don't want - // to follow real encoding rules (like this one!)) - s2.Pattern[i].Audios[j], err = s.Pattern[i].Audios[j].Copy() - if err != nil { - return nil, err - } - } - } - return s2, nil -} - -// MustCopy acts as copy but panics on errors -func (s *Sequence) MustCopy() audio.Audio { - a, err := s.Copy() - if err != nil { - panic(err) - } - return a -} - -// PlayLength returns how long this sequence will play before looping or stopping. -// This does not include how long the last note is held beyond the tick duration -func (s *Sequence) PlayLength() time.Duration { - return time.Duration(len(s.Pattern)) * s.tickDuration -} - -// Mix combines two sequences -func (s *Sequence) Mix(s2 *Sequence) (*Sequence, error) { - // Todo: we should be able to combine not-too-disparate - // sequences like one that ticks on .5 seconds and one that ticks - // on .25 seconds - if s.tickDuration != s2.tickDuration { - return nil, errors.New("Incompatible sequences") - } - seq, err := s.Copy() - if err != nil { - return nil, err - } - s3 := seq.(*Sequence) - for i, col := range s2.Pattern { - s3.Pattern[i].Audios = append(s3.Pattern[i].Audios, col.Audios...) - } - return s3, nil -} - -// Append creates a sequence by combining two sequences in order -func (s *Sequence) Append(s2 *Sequence) (*Sequence, error) { - // Todo: we should be able to combine not-too-disparate - // sequences like one that ticks on .5 seconds and one that ticks - // on .25 seconds - if s.tickDuration != s2.tickDuration { - return nil, errors.New("Incompatible sequences") - } - seq, err := s.Copy() - if err != nil { - return nil, err - } - s3 := seq.(*Sequence) - s3.Pattern = append(s3.Pattern, s2.Pattern...) - return s3, nil -} diff --git a/audio/sequence/tick.go b/audio/sequence/tick.go deleted file mode 100644 index a28a4e82..00000000 --- a/audio/sequence/tick.go +++ /dev/null @@ -1,27 +0,0 @@ -package sequence - -import "time" - -type Tick time.Duration - -type HasTicks interface { - GetTick() time.Duration - SetTick(time.Duration) -} - -func (vp *Tick) GetTick() time.Duration { - return time.Duration(*vp) -} - -func (vp *Tick) SetTick(vs time.Duration) { - *vp = Tick(vs) -} - -// Ticks sets the generator's Tick -func Ticks(t time.Duration) Option { - return func(g Generator) { - if ht, ok := g.(HasTicks); ok { - ht.SetTick(t) - } - } -} diff --git a/audio/sequence/volumePattern.go b/audio/sequence/volumePattern.go deleted file mode 100644 index adadd065..00000000 --- a/audio/sequence/volumePattern.go +++ /dev/null @@ -1,72 +0,0 @@ -package sequence - -type VolumePattern []float64 - -type HasVolumes interface { - GetVolumePattern() []float64 - SetVolumePattern([]float64) -} - -func (vp *VolumePattern) GetVolumePattern() []float64 { - return *vp -} - -func (vp *VolumePattern) SetVolumePattern(vs []float64) { - *vp = vs -} - -// Volumes sets the generator's Volume pattern -func Volumes(vs ...float64) Option { - return func(g Generator) { - if hvs, ok := g.(HasVolumes); ok { - hvs.SetVolumePattern(vs) - } - } -} - -// VolumeAt sets the n'th value in the entire play sequence -// to be Volume p. This could involve duplicating a pattern -// until it is long enough to reach n. Meaningless if the -// Volume pattern has not been set yet. -func VolumeAt(v float64, n int) Option { - return func(g Generator) { - if hvs, ok := g.(HasVolumes); ok { - if hl, ok := hvs.(HasLength); ok { - if hl.GetLength() < n { - volumes := hvs.GetVolumePattern() - if len(volumes) == 0 { - return - } - // If the pattern is not long enough, there are two things - // we could do-- 1. Extend the pattern and replace the - // individual note, or 2. Replace the note that would be - // played at n and thus all earlier and later plays within - // the pattern as well. - // - // This uses approach 1. - for len(volumes) < n { - volumes = append(volumes, volumes...) - } - volumes[n] = v - hvs.SetVolumePattern(volumes) - } - } - } - } -} - -// VolumePatternAt sets the n'th value in the Volume pattern -// to be Volume p. Meaningless if the Volume pattern has not -// been set yet. -func VolumePatternAt(v float64, n int) Option { - return func(g Generator) { - if hvs, ok := g.(HasVolumes); ok { - volumes := hvs.GetVolumePattern() - if len(volumes) < n { - return - } - volumes[n] = v - hvs.SetVolumePattern(volumes) - } - } -} diff --git a/audio/sequence/waveFunction.go b/audio/sequence/waveFunction.go deleted file mode 100644 index 0e76bac6..00000000 --- a/audio/sequence/waveFunction.go +++ /dev/null @@ -1,74 +0,0 @@ -package sequence - -import "github.com/oakmound/oak/v3/audio/synth" - -type WavePattern []synth.Wave - -type HasWaves interface { - GetWavePattern() []synth.Wave - SetWavePattern([]synth.Wave) -} - -func (wp *WavePattern) GetWavePattern() []synth.Wave { - return *wp -} - -func (wp *WavePattern) SetWavePattern(ws []synth.Wave) { - *wp = ws -} - -// Waves sets the generator's Wave pattern -func Waves(ws ...synth.Wave) Option { - return func(g Generator) { - if hw, ok := g.(HasWaves); ok { - hw.SetWavePattern(ws) - } - } -} - -// WaveAt sets the n'th value in the entire play sequence -// to be Wave p. This could involve duplicating a pattern -// until it is long enough to reach n. Meaningless if the -// Wave pattern has not been set yet. -func WaveAt(w synth.Wave, n int) Option { - return func(g Generator) { - if hw, ok := g.(HasWaves); ok { - if hl, ok := hw.(HasLength); ok { - if hl.GetLength() < n { - Waves := hw.GetWavePattern() - if len(Waves) == 0 { - return - } - // If the pattern is not long enough, there are two things - // we could do-- 1. Extend the pattern and replace the - // individual note, or 2. Replace the note that would be - // played at n and thus all earlier and later plays within - // the pattern as well. - // - // This uses approach 1. - for len(Waves) < n { - Waves = append(Waves, Waves...) - } - Waves[n] = w - hw.SetWavePattern(Waves) - } - } - } - } -} - -// WavePatternAt sets the n'th value in the Wave pattern -// to be Wave p. Meaningless if the Wave pattern has not -// been set yet. -func WavePatternAt(w synth.Wave, n int) Option { - return func(g Generator) { - if hw, ok := g.(HasWaves); ok { - Waves := hw.GetWavePattern() - if len(Waves) < n { - return - } - Waves[n] = w - hw.SetWavePattern(Waves) - } - } -} diff --git a/audio/sequence/waveGenerator.go b/audio/sequence/waveGenerator.go deleted file mode 100644 index d51d32fa..00000000 --- a/audio/sequence/waveGenerator.go +++ /dev/null @@ -1,93 +0,0 @@ -package sequence - -import ( - "time" - - audio "github.com/oakmound/oak/v3/audio/klang" - "github.com/oakmound/oak/v3/audio/synth" -) - -// A WaveGenerator composes sets of simple waveforms as a sequence -type WaveGenerator struct { - ChordPattern - PitchPattern - WavePattern - VolumePattern - HoldPattern - Length - Tick - Loop -} - -// NewWaveGenerator uses optional variadic syntax to enable -// any variant of a generator to be made -func NewWaveGenerator(opts ...Option) *WaveGenerator { - wg := &WaveGenerator{} - for _, opt := range opts { - opt(wg) - } - return wg -} - -// Generate generates a sequence from this wave generator -func (wg *WaveGenerator) Generate() *Sequence { - sq := &Sequence{} - sq.Ticker = time.NewTicker(time.Duration(wg.Tick)) - sq.tickDuration = time.Duration(wg.Tick) - sq.loop = bool(wg.Loop) - sq.stopCh = make(chan error) - if wg.Length == 0 { - if len(wg.PitchPattern) != 0 { - wg.Length = Length(len(wg.PitchPattern)) - } else if len(wg.ChordPattern.Pitches) != 0 { - wg.Length = Length(len(wg.ChordPattern.Pitches)) - } - // else whoops, there's no length - } - if len(wg.HoldPattern) == 0 { - wg.HoldPattern = []time.Duration{sq.tickDuration} - } - sq.Pattern = make([]*audio.Multi, wg.Length) - - volumeIndex := 0 - waveIndex := 0 - if len(wg.PitchPattern) != 0 { - pitchIndex := 0 - holdIndex := 0 - for i := range sq.Pattern { - p := wg.PitchPattern[pitchIndex] - if p != synth.Rest { - a, _ := wg.WavePattern[waveIndex]( - synth.AtPitch(p), - synth.Duration(wg.HoldPattern[holdIndex]), - synth.Volume(wg.VolumePattern[volumeIndex]), - ) - sq.Pattern[i] = audio.NewMulti(a) - } else { - sq.Pattern[i] = audio.NewMulti() - } - pitchIndex = (pitchIndex + 1) % len(wg.PitchPattern) - volumeIndex = (volumeIndex + 1) % len(wg.VolumePattern) - waveIndex = (waveIndex + 1) % len(wg.WavePattern) - holdIndex = (holdIndex + 1) % len(wg.HoldPattern) - } - } else if len(wg.ChordPattern.Pitches) != 0 { - chordIndex := 0 - for i := range sq.Pattern { - mult := audio.NewMulti() - for j, p := range wg.ChordPattern.Pitches[chordIndex] { - a, _ := wg.WavePattern[waveIndex]( - synth.AtPitch(p), - synth.Duration(wg.ChordPattern.Holds[chordIndex][j]), - synth.Volume(wg.VolumePattern[volumeIndex]), - ) - mult.Audios = append(mult.Audios, a) - } - sq.Pattern[i] = mult - waveIndex = (waveIndex + 1) % len(wg.WavePattern) - volumeIndex = (volumeIndex + 1) % len(wg.VolumePattern) - chordIndex = (chordIndex + 1) % len(wg.ChordPattern.Pitches) - } - } - return sq -} diff --git a/audio/synth/filter_test.go b/audio/synth/filter_test.go new file mode 100644 index 00000000..d760d921 --- /dev/null +++ b/audio/synth/filter_test.go @@ -0,0 +1,38 @@ +package synth + +import ( + "context" + "os" + "testing" + "time" + + "github.com/oakmound/oak/v4/audio" +) + +func TestMain(m *testing.M) { + err := audio.InitDefault() + if err != nil { + panic(err) + } + os.Exit(m.Run()) +} + +func TestFilters(t *testing.T) { + src := Int16 + // Todo: really gotta fix the sample rate evenness thing + src.SampleRate = 40000 + src.Volume = .07 + + fadeInFrames := time.Second + + unison := 4 + + for i := 0; i < unison; i++ { + go audio.Play(context.Background(), audio.FadeIn(fadeInFrames, audio.LoopReader(src.Saw()))) + go audio.Play(context.Background(), audio.FadeIn(fadeInFrames, audio.LoopReader(src.Saw(Detune(.04))))) + go audio.Play(context.Background(), audio.FadeIn(fadeInFrames, audio.LoopReader(src.Saw(Detune(-.05))))) + } + go audio.Play(context.Background(), audio.FadeIn(fadeInFrames, audio.LoopReader(src.Noise()))) + + time.Sleep(3 * time.Second) +} diff --git a/audio/synth/option.go b/audio/synth/option.go index 59300db7..c2e64dbe 100644 --- a/audio/synth/option.go +++ b/audio/synth/option.go @@ -16,12 +16,12 @@ func Duration(t time.Duration) Option { // Volume sets the volume of a generated waveform. It guarantees that 0 <= v <= 1 // (silent <= v <= max volume) func Volume(v float64) Option { + if v > 1.0 { + v = 1.0 + } else if v < 0 { + v = 0 + } return func(s Source) Source { - if v > 1.0 { - v = 1.0 - } else if v < 0 { - v = 0 - } s.Volume = v return s } @@ -35,7 +35,7 @@ func AtPitch(p Pitch) Option { } } -// Mono sets the format to play mono audio. +// Mono sets a synth source to play mono audio. func Mono() Option { return func(s Source) Source { s.Channels = 1 @@ -43,10 +43,31 @@ func Mono() Option { } } -// Stereo sets the format to play stereo audio. +// Stereo sets a synth source to play stereo audio. func Stereo() Option { return func(s Source) Source { s.Channels = 2 return s } } + +// Detune detunes between -1.0 and 1.0, 1.0 representing a half step up. +// Q: What is detuning? A: It's taking the pitch of the audio and adjusting it less than +// a single tone up or down. If you detune too far, you've just made the next pitch, +// but if you detune a little, you get a resonant sound. +func Detune(percent float64) Option { + return func(src Source) Source { + curPitch := src.Pitch + var nextPitch Pitch + if percent > 0 { + nextPitch = curPitch.Up(HalfStep) + } else { + nextPitch = curPitch.Down(HalfStep) + } + rawDelta := float64(int16(curPitch) - int16(nextPitch)) + delta := rawDelta * percent + // TODO: does pitch need to be a float? + src.Pitch = Pitch(float64(curPitch) + delta) + return src + } +} diff --git a/audio/synth/pitch.go b/audio/synth/pitch.go index f8b3e7a4..fbafa528 100644 --- a/audio/synth/pitch.go +++ b/audio/synth/pitch.go @@ -1,13 +1,18 @@ package synth -// A Pitch is a helper type for synth functions so -// a user can write A4 instead of a frequency value -// for a desired tone +import ( + "sort" + + "github.com/oakmound/oak/v4/audio/pcm" +) + +// A Pitch is a frequency value which represents how fast a wave should oscillate to produce a specific tone. type Pitch uint16 -// Pitch frequencies -// Values taken from http://peabody.sapp.org/class/st2/lab/notehz/ +// Pitch frequencies, taken from http://peabody.sapp.org/class/st2/lab/notehz/ +// These span octave 0 through octave 8, with sharps suffixed 's' and flats suffixed 'b' const ( + // 0 is reserved as representing a 'rest' for the purpose of composition Rest Pitch = 0 C0 Pitch = 16 C0s Pitch = 17 @@ -387,8 +392,124 @@ var ( A8s: 106, B8: 107, } + + pitchStrings = map[Pitch]string{ + Rest: "Rest", + C0: "C0", + C0s: "C0#", + D0: "D0", + D0s: "D0#", + E0: "E0", + F0: "F0", + F0s: "F0#", + G0: "G0", + G0s: "G0#", + A0: "A0", + A0s: "A0#", + B0: "B0", + C1: "C1", + C1s: "C1#", + D1: "D1", + D1s: "D1#", + E1: "E1", + F1: "F1", + F1s: "F1#", + G1: "G1", + G1s: "G1#", + A1: "A1", + A1s: "A1#", + B1: "B1", + C2: "C2", + C2s: "C2#", + D2: "D2", + D2s: "D2#", + E2: "E2", + F2: "F2", + F2s: "F2#", + G2: "G2", + G2s: "G2#", + A2: "A2", + A2s: "A2#", + B2: "B2", + C3: "C3", + C3s: "C3#", + D3: "D3", + D3s: "D3#", + E3: "E3", + F3: "F3", + F3s: "F3#", + G3: "G3", + G3s: "G3#", + A3: "A3", + A3s: "A3#", + B3: "B3", + C4: "C4", + C4s: "C4#", + D4: "D4", + D4s: "D4#", + E4: "E4", + F4: "F4", + F4s: "F4#", + G4: "G4", + G4s: "G4#", + A4: "A4", + A4s: "A4#", + B4: "B4", + C5: "C5", + C5s: "C5#", + D5: "D5", + D5s: "D5#", + E5: "E5", + F5: "F5", + F5s: "F5#", + G5: "G5", + G5s: "G5#", + A5: "A5", + A5s: "A5#", + B5: "B5", + C6: "C6", + C6s: "C6#", + D6: "D6", + D6s: "D6#", + E6: "E6", + F6: "F6", + F6s: "F6#", + G6: "G6", + G6s: "G6#", + A6: "A6", + A6s: "A6#", + B6: "B6", + C7: "C7", + C7s: "C7#", + D7: "D7", + D7s: "D7#", + E7: "E7", + F7: "F7", + F7s: "F7#", + G7: "G7", + G7s: "G7#", + A7: "A7", + A7s: "A7#", + B7: "B7", + C8: "C8", + C8s: "C8#", + D8: "D8", + D8s: "D8#", + E8: "E8", + F8: "F8", + F8s: "F8#", + G8: "G8", + G8s: "G8#", + A8: "A8", + A8s: "A8#", + B8: "B8", + } ) +func (p Pitch) String() string { + return pitchStrings[p] +} + var accidentals = map[Pitch]struct{}{ C0s: {}, D0s: {}, @@ -466,14 +587,97 @@ func (p Pitch) Down(s Step) Pitch { return allPitches[i-int(s)] } -// NoteFromIndex is a utility for pitch converters that for some reason have -// integers representing their notes to get a pitch from said integer -func NoteFromIndex(i int) Pitch { - return allPitches[i] -} - // IsAccidental reports true if this pitch is represented with a single sharp or a flat, usually. func (p Pitch) IsAccidental() bool { _, ok := accidentals[p] return ok } + +type PitchDetector struct { + pcm.Reader + + format pcm.Format + + // DetectedPitches and DetectedRawPitches store the calculated pitch values as this reader parses data. The length + // of these slices will be equal to this reader's format's channel count. Consumers should not modify these slices. + DetectedPitches []Pitch + DetectedRawPitches []float64 + + indices []int + lastValues []float64 + crossedZero []bool +} + +func NewPitchDetector(r pcm.Reader) *PitchDetector { + return &PitchDetector{ + Reader: r, + format: r.PCMFormat(), + DetectedPitches: make([]Pitch, r.PCMFormat().Channels), + DetectedRawPitches: make([]float64, r.PCMFormat().Channels), + indices: make([]int, r.PCMFormat().Channels), + lastValues: make([]float64, r.PCMFormat().Channels), + crossedZero: make([]bool, r.PCMFormat().Channels), + } +} + +func (pd *PitchDetector) ReadPCM(b []byte) (n int, err error) { + n, err = pd.Reader.ReadPCM(b) + if err != nil { + return n, err + } + var read int + sampleSize := pd.format.SampleSize() + for len(b[read:]) > sampleSize { + vals, valReadBytes, err := pd.format.SampleFloat(b[read:]) + if err != nil { + break + } + read += valReadBytes + for i, val := range vals { + pd.indices[i]++ + if pd.lastValues[i] < 0 && val > 0 || val < 0 && pd.lastValues[i] > 0 { + // we've crossed zero + if !pd.crossedZero[i] { + pd.crossedZero[i] = true + } else { + // assuming this is pitched audio (if it isn't we can't give a correct answer), + // pd.index is now the number of samples since the last time this audio + // stream crossed zero. The second last time this audio stream crossed zero defines how + // frequently this audio is cycling-- the speed the audio cycles at defines the pitch + // of the audio in hertz; our pitch constants above are also defined in hertz. + periodLength := pd.indices[i] * 2 + samplesPerSecond := pd.format.SampleRate + periodHz := 1 / (float64(periodLength) / float64(samplesPerSecond)) + pd.DetectedRawPitches[i] = periodHz + pd.DetectedPitches[i] = Pitch(periodHz).Round() + } + pd.indices[i] = 0 + } + pd.lastValues[i] = val + } + } + return +} + +// Round rounds a pitch value to the closest predefined pitch value in hertz: +// func main() { +// hz := synth.Pitch(1024) +// hz2 := hz.Round() +// fmt.Println(hz2, int(hz2))) // "C6", 1047 +// } +// +func (p Pitch) Round() Pitch { + // binary search + i := sort.Search(len(allPitches)-1, func(i int) bool { + return p < allPitches[i] + }) + // adjust for near matches + // we know hz < allPitches[i] + if i == 0 { + return allPitches[i] + } + if p-allPitches[i-1] < allPitches[i]-p { + return allPitches[i-1] + } + return allPitches[i] +} diff --git a/audio/synth/source.go b/audio/synth/source.go index 52777157..48ec4948 100644 --- a/audio/synth/source.go +++ b/audio/synth/source.go @@ -3,12 +3,12 @@ package synth import ( "time" - audio "github.com/oakmound/oak/v3/audio/klang" + "github.com/oakmound/oak/v4/audio/pcm" ) -// A Source stores necessary information for generating audio and waveform data +// A Source stores necessary information for generating waveform data type Source struct { - audio.Format + pcm.Format Pitch Pitch // Volume, between 0.0 -> 1.0 Volume float64 @@ -38,10 +38,11 @@ func (s Source) Update(opts ...Option) Source { var ( // Int16 is a default source for building 16-bit audio Int16 = Source{ - Format: audio.Format{ + Format: pcm.Format{ SampleRate: 44100, Channels: 2, - Bits: 16, + // within a source, if Bits is not specified, it'll default to 16. + Bits: 16, }, Pitch: A4, Volume: .25, diff --git a/audio/synth/waves.go b/audio/synth/waves.go index 7834457c..45695163 100644 --- a/audio/synth/waves.go +++ b/audio/synth/waves.go @@ -3,266 +3,181 @@ package synth import ( "math" + "math/rand" - audio "github.com/oakmound/oak/v3/audio/klang" - "github.com/oakmound/oak/v3/audio/pcm" - "github.com/oakmound/oak/v3/oakerr" + "github.com/oakmound/oak/v4/audio/pcm" ) // Wave functions take a set of options and return an audio -type Wave func(opts ...Option) (audio.Audio, error) +type Wave func(opts ...Option) pcm.Reader // Sourced from https://en.wikibooks.org/wiki/Sound_Synthesis_Theory/Oscillators_and_Wavetables func phase(freq Pitch, i int, sampleRate uint32) float64 { return float64(freq) * (float64(i) / float64(sampleRate)) * 2 * math.Pi } -func bytesFromInts(is []int16, channels int) []byte { - wave := make([]byte, len(is)*channels*2) - for i := 0; i < len(wave); i += channels * 2 { - wave[i] = byte(is[i/4] % 256) - wave[i+1] = byte(is[i/4] >> 8) - // duplicate the contents across all channels - for c := 1; c < channels; c++ { - wave[i+(2*c)] = wave[i] - wave[i+(2*c)+1] = wave[i+1] - } - } - wave = append(wave, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0) - return wave -} - // Sin produces a Sin wave -// __ -// -- -- -// / \ -//--__-- --__-- -func (s Source) Sin(opts ...Option) (audio.Audio, error) { - s = s.Update(opts...) - var b []byte - switch s.Bits { - case 16: - s.Volume *= math.MaxInt16 - wave := make([]int16, int(s.Seconds*float64(s.SampleRate))) - for i := 0; i < len(wave); i++ { - wave[i] = int16(s.sinAtIndex(i)) - } - b = bytesFromInts(wave, int(s.Channels)) - } - return s.Wave(b) +// __ +// -- -- +// / \ +// --__-- --__-- +func (s Source) Sin(opts ...Option) pcm.Reader { + return s.Wave(Source.SinWave, opts...) } -// SinPCM acts like Sin, but returns a PCM type instead of a klang type. -func (s Source) SinPCM(opts ...Option) (pcm.Reader, error) { - switch s.Bits { - case 16: - s.Volume *= math.MaxInt16 - return &Wave16Reader{ - Source: s.Update(opts...), - waveFunc: func(s Source, idx int) int16 { - return int16(s.sinAtIndex(idx)) - }, - }, nil - case 32: - s.Volume *= math.MaxInt32 - return &Wave32Reader{ - Source: s.Update(opts...), - waveFunc: func(s Source, idx int) int32 { - return int32(s.sinAtIndex(idx)) - }, - }, nil - } - return nil, oakerr.InvalidInput{InputName: "s.Bits"} +func (s Source) SinWave(idx int) float64 { + return s.Volume * math.Sin(s.modPhase(idx)) } -func (s Source) sinAtIndex(idx int) float64 { - return s.Volume * math.Sin(s.modPhase(idx)) +func (s Source) Square(opts ...Option) pcm.Reader { + return s.Pulse(2)(opts...) } // Pulse acts like Square when given a pulse of 2, when given any lesser // pulse the time up and down will change so that 1/pulse time the wave will // be up. // -// __ __ -// || || -// ____||____||____ -func (s Source) Pulse(pulse float64) Wave { - pulseSwitch := 1 - 2/pulse - return func(opts ...Option) (audio.Audio, error) { - s = s.Update(opts...) - - var b []byte - switch s.Bits { - case 16: - wave := make([]int16, int(s.Seconds*float64(s.SampleRate))) - for i := range wave { - // alternatively phase % 2pi - if math.Sin(s.Phase(i)) > pulseSwitch { - wave[i] = int16(s.Volume) - } else { - wave[i] = int16(-s.Volume) - } - } - b = bytesFromInts(wave, int(s.Channels)) - } - return s.Wave(b) +// __ __ +// || || +// ____||____||____ +func (s Source) Pulse(pulse float64) func(opts ...Option) pcm.Reader { + return func(opts ...Option) pcm.Reader { + return s.Wave(PulseWave(pulse), opts...) } } -// PulsePCM acts like Pulse, but returns a PCM type instead of a klang type. -func (s Source) PulsePCM(pulse float64) func(opts ...Option) (pcm.Reader, error) { - switch s.Bits { - case 16: - s.Volume *= math.MaxInt16 - case 32: - s.Volume *= math.MaxInt32 - } +func PulseWave(pulse float64) Waveform { pulseSwitch := 1 - 2/pulse - return func(opts ...Option) (pcm.Reader, error) { - switch s.Bits { - case 16: - return &Wave16Reader{ - Source: s.Update(opts...), - waveFunc: func(s Source, idx int) int16 { - if math.Sin(s.Phase(idx)) > pulseSwitch { - return int16(s.Volume) - } - return int16(-s.Volume) - }, - }, nil - case 32: - return &Wave32Reader{ - Source: s.Update(opts...), - waveFunc: func(s Source, idx int) int32 { - if math.Sin(s.Phase(idx)) > pulseSwitch { - return int32(s.Volume) - } - return int32(-s.Volume) - }, - }, nil + return func(s Source, idx int) float64 { + if math.Sin(s.Phase(idx)) > pulseSwitch { + return s.Volume } - return nil, oakerr.InvalidInput{InputName: "s.Bits"} + return -s.Volume } } -// Square produces a Square wave +// Saw produces a saw wave // -// _________ -// | | -// ______| |________ -func (s Source) Square(opts ...Option) (audio.Audio, error) { - return s.Pulse(2)(opts...) +// ^ ^ ^ +// / | / | / +// / |/ |/ +func (s Source) Saw(opts ...Option) pcm.Reader { + return s.Wave(Source.SawWave, opts...) } -// Saw produces a saw wave +func (s Source) SawWave(idx int) float64 { + return s.Volume - (s.Volume / math.Pi * math.Mod(s.Phase(idx), 2*math.Pi)) +} + +// Triangle produces a Triangle wave // -// ^ ^ ^ -// / | / | / -// / |/ |/ -func (s Source) Saw(opts ...Option) (audio.Audio, error) { - s = s.Update(opts...) +// ^ ^ +// / \ / \ +// v v v +func (s Source) Triangle(opts ...Option) pcm.Reader { + return s.Wave(Source.TriangleWave, opts...) +} - var b []byte - switch s.Bits { - case 16: - s.Volume *= math.MaxInt16 - wave := make([]int16, int(s.Seconds*float64(s.SampleRate))) - for i := range wave { - wave[i] = int16(s.Volume - (s.Volume / math.Pi * math.Mod(s.Phase(i), 2*math.Pi))) - } - b = bytesFromInts(wave, int(s.Channels)) +func (s Source) TriangleWave(idx int) float64 { + p := s.modPhase(idx) + m := p * (2 * s.Volume / math.Pi) + if math.Sin(p) > 0 { + return -s.Volume + m } - return s.Wave(b) + return 3*s.Volume - m +} + +// Noise produces random audio data. +func (s Source) Noise(opts ...Option) pcm.Reader { + return s.Wave(Source.NoiseWave, opts...) +} + +var _ Waveform = Source.NoiseWave + +// NoiseWave returns noise pcm data bounded by this source's volume. +func (s Source) NoiseWave(idx int) float64 { + return ((rand.Float64() * 2) - 1) * s.Volume } -// SawPCM acts like Saw, but returns a PCM type instead of a klang type. -func (s Source) SawPCM(opts ...Option) (pcm.Reader, error) { +func (s Source) modPhase(idx int) float64 { + return math.Mod(s.Phase(idx), 2*math.Pi) +} + +// A Waveform is a function that can report a point of audio data given some source parameters for generating the audio +// and an index of where in the generated waveform the requested point lies +type Waveform func(s Source, idx int) float64 + +// Wave converts a waveform function into a pcm.Reader +func (s Source) Wave(waveFn Waveform, opts ...Option) pcm.Reader { switch s.Bits { - case 16: - s.Volume *= math.MaxInt16 - return &Wave16Reader{ + case 8: + s.Volume *= math.MaxInt8 + return &wave8Reader{ Source: s.Update(opts...), - waveFunc: func(s Source, idx int) int16 { - return int16(s.Volume - (s.Volume / math.Pi * math.Mod(s.Phase(idx), 2*math.Pi))) + waveFunc: func(s Source, idx int) int8 { + return int8(waveFn(s, idx)) }, - }, nil + } case 32: s.Volume *= math.MaxInt32 - return &Wave32Reader{ + return &wave32Reader{ Source: s.Update(opts...), waveFunc: func(s Source, idx int) int32 { - return int32(s.Volume - (s.Volume / math.Pi * math.Mod(s.Phase(idx), 2*math.Pi))) + return int32(waveFn(s, idx)) }, - }, nil - } - return nil, oakerr.InvalidInput{InputName: "s.Bits"} -} - -// Triangle produces a Triangle wave -// -// ^ ^ -// / \ / \ -// v v v -func (s Source) Triangle(opts ...Option) (audio.Audio, error) { - s = s.Update(opts...) - var b []byte - switch s.Bits { - case 16: - s.Volume *= math.MaxInt16 - wave := make([]int16, int(s.Seconds*float64(s.SampleRate))) - for i := range wave { - wave[i] = int16(s.triangleAtIndex(i)) } - b = bytesFromInts(wave, int(s.Channels)) - } - return s.Wave(b) -} - -// TrianglePCM acts like Triangle, but returns a PCM type instead of a klang type. -func (s Source) TrianglePCM(opts ...Option) (pcm.Reader, error) { - switch s.Bits { case 16: + fallthrough + default: s.Volume *= math.MaxInt16 - return &Wave16Reader{ + return &wave16Reader{ Source: s.Update(opts...), waveFunc: func(s Source, idx int) int16 { - return int16(s.triangleAtIndex(idx)) - }, - }, nil - case 32: - s.Volume *= math.MaxInt32 - return &Wave32Reader{ - Source: s.Update(opts...), - waveFunc: func(s Source, idx int) int32 { - return int32(s.triangleAtIndex(idx)) + return int16(waveFn(s, idx)) }, - }, nil + } } - return nil, oakerr.InvalidInput{InputName: "s.Bits"} } -func (s Source) triangleAtIndex(idx int) float64 { - p := s.modPhase(idx) - m := p * (2 * s.Volume / math.Pi) - if math.Sin(p) > 0 { - return -s.Volume + m - } - return 3*s.Volume - m +// MultiWave converts a series of waveform functions into a combined reader, outputting the average +// of all of the source waveforms at any given index +func (s Source) MultiWave(waveFns []Waveform, opts ...Option) pcm.Reader { + return s.Wave(func(s Source, idx int) float64 { + var out float64 + for _, wv := range waveFns { + v := wv(s, idx) + out += v / float64(len(waveFns)) + } + return out + }, opts...) } -func (s Source) modPhase(idx int) float64 { - return math.Mod(s.Phase(idx), 2*math.Pi) +type wave8Reader struct { + Source + lastIndex int + waveFunc func(s Source, idx int) int8 } -// Could have pulse triangle +func (pr *wave8Reader) ReadPCM(b []byte) (n int, err error) { + bytesPerI8 := int(pr.Channels) + for i := 0; i+bytesPerI8 <= len(b); i += bytesPerI8 { + i8 := pr.waveFunc(pr.Source, pr.lastIndex) + pr.lastIndex++ + for c := 0; c < int(pr.Channels); c++ { + b[i+c] = byte(i8) + } + n += bytesPerI8 + } + return +} -type Wave16Reader struct { +type wave16Reader struct { Source lastIndex int waveFunc func(s Source, idx int) int16 } -func (pr *Wave16Reader) ReadPCM(b []byte) (n int, err error) { +func (pr *wave16Reader) ReadPCM(b []byte) (n int, err error) { bytesPerI16 := int(pr.Channels) * 2 for i := 0; i+bytesPerI16 <= len(b); i += bytesPerI16 { i16 := pr.waveFunc(pr.Source, pr.lastIndex) @@ -276,21 +191,13 @@ func (pr *Wave16Reader) ReadPCM(b []byte) (n int, err error) { return } -func (pr *Wave16Reader) PCMFormat() pcm.Format { - return pcm.Format{ - SampleRate: pr.SampleRate, - Channels: pr.Channels, - Bits: pr.Bits, - } -} - -type Wave32Reader struct { +type wave32Reader struct { Source lastIndex int waveFunc func(s Source, idx int) int32 } -func (pr *Wave32Reader) ReadPCM(b []byte) (n int, err error) { +func (pr *wave32Reader) ReadPCM(b []byte) (n int, err error) { bytesPerF32 := int(pr.Channels) * 4 for i := 0; i+bytesPerF32 <= len(b); i += bytesPerF32 { i32 := pr.waveFunc(pr.Source, pr.lastIndex) @@ -305,11 +212,3 @@ func (pr *Wave32Reader) ReadPCM(b []byte) (n int, err error) { } return } - -func (pr *Wave32Reader) PCMFormat() pcm.Format { - return pcm.Format{ - SampleRate: pr.SampleRate, - Channels: pr.Channels, - Bits: pr.Bits, - } -} diff --git a/audio/wav/testdata/test.wav b/audio/wav/testdata/test.wav deleted file mode 100644 index 85eabf65..00000000 Binary files a/audio/wav/testdata/test.wav and /dev/null differ diff --git a/audio/writer.go b/audio/writer.go new file mode 100644 index 00000000..07d07013 --- /dev/null +++ b/audio/writer.go @@ -0,0 +1,19 @@ +package audio + +import ( + "github.com/oakmound/oak/v4/audio/pcm" +) + +// NewWriter returns a writer which can accept audio streamed matching the given format +func NewWriter(f pcm.Format) (pcm.Writer, error) { + return newWriter(f) +} + +// MustNewWriter calls NewWriter and panics if an error is returned. +func MustNewWriter(f pcm.Format) pcm.Writer { + w, err := NewWriter(f) + if err != nil { + panic(err) + } + return w +} diff --git a/audio/writer_alsa.go b/audio/writer_alsa.go new file mode 100644 index 00000000..f26257a0 --- /dev/null +++ b/audio/writer_alsa.go @@ -0,0 +1,143 @@ +//go:build linux +// +build linux + +package audio + +import ( + "errors" + "strings" + "sync" + + "github.com/oakmound/alsa" + "github.com/oakmound/oak/v4/audio/pcm" +) + +func newALSAWriter(f pcm.Format) (pcm.Writer, error) { + handle, err := openDevice() + if err != nil { + return nil, err + } + // Todo: annotate these errors with more info + format, err := alsaFormat(f.Bits) + if err != nil { + return nil, err + } + _, err = handle.NegotiateFormat(format) + if err != nil { + return nil, err + } + _, err = handle.NegotiateRate(int(f.SampleRate)) + if err != nil { + return nil, err + } + _, err = handle.NegotiateChannels(int(f.Channels)) + if err != nil { + return nil, err + } + // Default value at recommendation of library + period, err := handle.NegotiatePeriodSize(2048) + if err != nil { + return nil, err + } + _, err = handle.NegotiateBufferSize(4096) + if err != nil { + return nil, err + } + err = handle.Prepare() + if err != nil { + return nil, err + } + return &alsaWriter{ + Format: f, + period: period, + Device: handle, + }, nil +} + +type alsaWriter struct { + sync.Mutex + pcm.Format + *alsa.Device + playing bool + period int +} + +var ( + // Todo: support more customized audio device usage + openDeviceLock sync.Mutex + openedDevice *alsa.Device +) + +func openDevice() (*alsa.Device, error) { + openDeviceLock.Lock() + defer openDeviceLock.Unlock() + + if openedDevice != nil { + return openedDevice, nil + } + cards, err := alsa.OpenCards() + if err != nil { + return nil, err + } + defer alsa.CloseCards(cards) + for i, c := range cards { + devices, err := c.Devices() + if err != nil { + continue + } + for _, d := range devices { + if d.Type != alsa.PCM || !d.Play { + continue + } + if strings.Contains(d.Title, SkipDevicesContaining) { + continue + } + d.Close() + err := d.Open() + if err != nil { + continue + } + // We've a found a device we can hypothetically use + // don't close this card + cards = append(cards[:i], cards[i+1:]...) + openedDevice = d + return d, nil + } + } + return nil, errors.New("No valid device found") +} + +func alsaFormat(bits uint16) (alsa.FormatType, error) { + switch bits { + case 8: + return alsa.S8, nil + case 16: + return alsa.S16_LE, nil + case 32: + return alsa.S32_LE, nil + } + return 0, errors.New("Undefined alsa format for encoding bits") +} + +func (aw *alsaWriter) Close() error { + aw.Lock() + defer aw.Unlock() + var err error + if aw.playing { + aw.playing = false + } + return err +} + +func (aw *alsaWriter) WritePCM(data []byte) (n int, err error) { + aw.Lock() + defer aw.Unlock() + err = aw.Device.Write(data, aw.period) + if err != nil { + return 0, err + } + if !aw.playing { + aw.playing = true + } + return len(data), err +} diff --git a/audio/pcm/writer_windows.go b/audio/writer_dsound.go similarity index 74% rename from audio/pcm/writer_windows.go rename to audio/writer_dsound.go index b7b1cff8..348d9d2a 100644 --- a/audio/pcm/writer_windows.go +++ b/audio/writer_dsound.go @@ -1,14 +1,15 @@ //go:build windows -package pcm +package audio import ( "fmt" "io" "sync" - "github.com/oakmound/oak/v3/audio/wininternal" - "github.com/oakmound/oak/v3/oakerr" + intdsound "github.com/oakmound/oak/v4/audio/internal/dsound" + "github.com/oakmound/oak/v4/audio/pcm" + "github.com/oakmound/oak/v4/oakerr" "github.com/oov/directsound-go/dsound" ) @@ -23,7 +24,7 @@ func initOS(driver Driver) error { Operation: "pcm.Init:" + driver.String(), } } - cfg, err := wininternal.Init() + cfg, err := intdsound.Init() if err != nil { return err } @@ -33,7 +34,7 @@ func initOS(driver Driver) error { var directSoundInterface *dsound.IDirectSound -func newWriter(f Format) (Writer, error) { +func newWriter(f pcm.Format) (pcm.Writer, error) { if directSoundInterface == nil { return nil, oakerr.NotFound{ InputName: "directSoundInterface", @@ -41,7 +42,7 @@ func newWriter(f Format) (Writer, error) { } blockAlign := f.Channels * f.Bits / 8 - bufferSize := f.BytesPerSecond() * WriterBufferLengthInSeconds + bufferSize := uint32(float64(f.BytesPerSecond()) * WriterBufferLengthInSeconds) dsbuff, err := directSoundInterface.CreateSoundBuffer(&dsound.BufferDesc{ // These flags cover everything we should ever want to do @@ -69,7 +70,7 @@ func newWriter(f Format) (Writer, error) { type directSoundWriter struct { sync.Mutex - Format + pcm.Format buff *dsound.IDirectSoundBuffer lockedOffset uint32 bufferSize uint32 @@ -82,7 +83,6 @@ func (dsw *directSoundWriter) Close() error { var err error if dsw.playing { - dsw.capOffAudio() err = dsw.buff.Stop() dsw.playing = false } @@ -90,41 +90,6 @@ func (dsw *directSoundWriter) Close() error { return err } -// this attempts to reduce the amount of noise incurred by stopping a playing audio -// it is not completely effective, a blip can still be heard -func (dsw *directSoundWriter) capOffAudio() { - // 10k zero bytes - cap := make([]byte, 10000) - a, b, err := dsw.buff.LockBytes(dsw.lockedOffset, 10000, 0) - if err != nil { - // should not happen, but if it does, we can't proceed - return - } - copy(a, cap) - if len(b) != 0 { - copy(b, cap[len(a):]) - } - dsw.buff.UnlockBytes(a, b) - dsw.buff.SetCurrentPosition(dsw.lockedOffset) -} - -func (dsw *directSoundWriter) Reset() error { - dsw.Lock() - defer dsw.Unlock() - emptyBuff := make([]byte, dsw.bufferSize) - a, b, err := dsw.buff.LockBytes(0, dsw.bufferSize, 0) - if err != nil { - return err - } - copy(a, emptyBuff) - if len(b) != 0 { - copy(b, emptyBuff) - } - err = dsw.buff.UnlockBytes(a, b) - dsw.Seek(0, io.SeekStart) - return err -} - func (dsw *directSoundWriter) Seek(offset int64, whence int) (position int64, err error) { switch whence { case io.SeekStart: diff --git a/audio/pcm/writer_other.go b/audio/writer_other.go similarity index 82% rename from audio/pcm/writer_other.go rename to audio/writer_other.go index adebd1fe..b38e8d24 100644 --- a/audio/pcm/writer_other.go +++ b/audio/writer_other.go @@ -1,8 +1,8 @@ //go:build !windows && !linux && !darwin -package pcm +package audio -import "github.com/oakmound/oak/v3/oakerr" +import "github.com/oakmound/oak/v4/oakerr" func initOS(driver Driver) error { return oakerr.UnsupportedPlatform{ diff --git a/audio/pcm/writer_pulse.go b/audio/writer_pulse.go similarity index 95% rename from audio/pcm/writer_pulse.go rename to audio/writer_pulse.go index 98355bc1..490b9e42 100644 --- a/audio/pcm/writer_pulse.go +++ b/audio/writer_pulse.go @@ -1,6 +1,6 @@ //go:build linux || darwin -package pcm +package audio import ( "bytes" @@ -10,12 +10,14 @@ import ( "github.com/jfreymuth/pulse" "github.com/jfreymuth/pulse/proto" + + "github.com/oakmound/oak/v4/audio/pcm" ) // This mutex may be unneeded var newWriterMutex sync.Mutex -func newWriter(f Format) (Writer, error) { +func newPulseWriter(f pcm.Format) (pcm.Writer, error) { newWriterMutex.Lock() defer newWriterMutex.Unlock() // TODO: @@ -89,7 +91,7 @@ func (m *eofFReader) Read(b []byte) (n int, err error) { type pulseWriter struct { sync.Mutex - Format + pcm.Format handOver *eofFReader playBack *pulse.PlaybackStream client *pulse.Client @@ -114,13 +116,6 @@ func (dsw *pulseWriter) Close() error { return err } -func (dsw *pulseWriter) Reset() error { - dsw.Lock() - defer dsw.Unlock() - // ??? - return nil -} - func (dsw *pulseWriter) WritePCM(data []byte) (n int, err error) { dsw.Lock() defer dsw.Unlock() diff --git a/collision/attachSpace.go b/collision/attachSpace.go index 7cf1612d..4955ea25 100644 --- a/collision/attachSpace.go +++ b/collision/attachSpace.go @@ -3,8 +3,8 @@ package collision import ( "errors" - "github.com/oakmound/oak/v3/event" - "github.com/oakmound/oak/v3/physics" + "github.com/oakmound/oak/v4/event" + "github.com/oakmound/oak/v4/physics" ) // An AttachSpace is a composable struct that provides attachment @@ -19,19 +19,32 @@ type AttachSpace struct { aSpace **Space tree *Tree offX, offY float64 + binding event.Binding } func (as *AttachSpace) getAttachSpace() *AttachSpace { return as } +func (as *AttachSpace) CID() event.CallerID { + return (*as.aSpace).CID +} + +var _ attachSpace = &AttachSpace{} + type attachSpace interface { + event.Caller getAttachSpace() *AttachSpace } // Attach attaches v to the given space with optional x,y offsets. See AttachSpace. func Attach(v physics.Vector, s *Space, tree *Tree, offsets ...float64) error { - if t, ok := s.CID.E().(attachSpace); ok { + return AttachWithBus(v, s, tree, event.DefaultBus, offsets...) +} + +func AttachWithBus(v physics.Vector, s *Space, tree *Tree, bus event.Handler, offsets ...float64) error { + en := bus.GetCallerMap().GetEntity(s.CID) + if t, ok := en.(attachSpace); ok { as := t.getAttachSpace() as.aSpace = &s as.follow = v @@ -39,7 +52,7 @@ func Attach(v physics.Vector, s *Space, tree *Tree, offsets ...float64) error { if as.tree == nil { as.tree = DefaultTree } - s.CID.Bind(event.Enter, attachSpaceEnter) + as.binding = event.Bind(bus, event.Enter, t, attachSpaceEnter) if len(offsets) > 0 { as.offX = offsets[0] if len(offsets) > 1 { @@ -54,30 +67,23 @@ func Attach(v physics.Vector, s *Space, tree *Tree, offsets ...float64) error { // Detach removes the attachSpaceEnter binding from an entity composed with // AttachSpace func Detach(s *Space) error { - en := s.CID.E() - if _, ok := en.(attachSpace); ok { - event.UnbindBindable( - event.UnbindOption{ - Event: event.Event{ - Name: event.Enter, - CallerID: s.CID, - }, - Fn: attachSpaceEnter, - }, - ) + return DetachWithBus(s, event.DefaultBus) +} + +func DetachWithBus(s *Space, bus event.Handler) error { + en := bus.GetCallerMap().GetEntity(s.CID) + if as, ok := en.(attachSpace); ok { + as.getAttachSpace().binding.Unbind() return nil } return errors.New("this space's entity is not composed of AttachSpace") } -func attachSpaceEnter(id event.CID, _ interface{}) int { - as := id.E().(attachSpace).getAttachSpace() +func attachSpaceEnter(asIface attachSpace, _ event.EnterPayload) event.Response { + as := asIface.(attachSpace).getAttachSpace() x, y := as.follow.X()+as.offX, as.follow.Y()+as.offY if x != (*as.aSpace).X() || y != (*as.aSpace).Y() { - - // If this was a nil pointer it would have already crashed but as of release 2.2.0 - // this could error from the space to delete not existing in the rtree. as.tree.UpdateSpace(x, y, (*as.aSpace).GetW(), (*as.aSpace).GetH(), *as.aSpace) } return 0 diff --git a/collision/attachSpace_test.go b/collision/attachSpace_test.go index 99d1f613..22a83a09 100644 --- a/collision/attachSpace_test.go +++ b/collision/attachSpace_test.go @@ -1,35 +1,34 @@ package collision import ( + "fmt" "testing" "time" - "github.com/oakmound/oak/v3/event" - "github.com/oakmound/oak/v3/physics" + "github.com/oakmound/oak/v4/event" + "github.com/oakmound/oak/v4/physics" ) type aspace struct { AttachSpace } -func (as *aspace) Init() event.CID { - return event.NextID(as) -} - func TestAttachSpace(t *testing.T) { Clear() - go event.ResolveChanges() + b := event.NewBus(event.NewCallerMap()) go func() { for { <-time.After(5 * time.Millisecond) - <-event.TriggerBack(event.Enter, nil) + <-event.TriggerOn(b, event.Enter, event.EnterPayload{}) } }() - as := aspace{} + as := &aspace{} + cid := b.GetCallerMap().Register(as) v := physics.NewVector(0, 0) - s := NewSpace(100, 100, 10, 10, as.Init()) + s := NewSpace(100, 100, 10, 10, cid) Add(s) - err := Attach(v, s, nil, 4, 4) + fmt.Println(s.CID) + err := AttachWithBus(v, s, nil, b, 4, 4) if err != nil { t.Fatalf("attach failed: %v", err) } @@ -42,7 +41,7 @@ func TestAttachSpace(t *testing.T) { t.Fatalf("expected attached space to have y of 9, was %v", s.Y()) } - err = Detach(s) + err = DetachWithBus(s, b) if err != nil { t.Fatalf("detach failed: %v", err) } @@ -60,10 +59,10 @@ func TestAttachSpace(t *testing.T) { s = NewUnassignedSpace(0, 0, 1, 1) err = Attach(v, s, nil) if err == nil { - t.Fatalf("unassinged space attach should have failed: %v", err) + t.Fatalf("unassigned space attach should have failed: %v", err) } err = Detach(s) if err == nil { - t.Fatalf("unassinged space detach should have failed: %v", err) + t.Fatalf("unassigned space detach should have failed: %v", err) } } diff --git a/collision/filter.go b/collision/filter.go index e7a07c3c..60beebf4 100644 --- a/collision/filter.go +++ b/collision/filter.go @@ -1,6 +1,6 @@ package collision -import "github.com/oakmound/oak/v3/event" +import "github.com/oakmound/oak/v4/event" // A Filter will take a set of collision spaces // and return the subset that match some requirement @@ -43,7 +43,7 @@ func Without(tossFn func(*Space) bool) Filter { } // WithoutCIDs will return no spaces with a CID in the input -func WithoutCIDs(cids ...event.CID) Filter { +func WithoutCIDs(cids ...event.CallerID) Filter { return Without(func(s *Space) bool { for _, c := range cids { if s.CID == c { diff --git a/collision/geom.go b/collision/geom.go index 62901050..7a39ca2a 100644 --- a/collision/geom.go +++ b/collision/geom.go @@ -7,7 +7,7 @@ package collision import ( "math" - "github.com/oakmound/oak/v3/alg/floatgeom" + "github.com/oakmound/oak/v4/alg/floatgeom" ) // minDist computes the square of the distance from a point to a rectangle. diff --git a/collision/onCollision.go b/collision/onCollision.go index 784aa90c..bd4782da 100644 --- a/collision/onCollision.go +++ b/collision/onCollision.go @@ -3,7 +3,7 @@ package collision import ( "errors" - "github.com/oakmound/oak/v3/event" + "github.com/oakmound/oak/v4/event" ) // A Phase is a struct that other structs who want to use PhaseCollision @@ -22,6 +22,10 @@ func (cp *Phase) getCollisionPhase() *Phase { return cp } +func (cp *Phase) CID() event.CallerID { + return cp.OnCollisionS.CID +} + type collisionPhase interface { getCollisionPhase() *Phase } @@ -31,13 +35,12 @@ type collisionPhase interface { // entities begin to collide or stop colliding with the space. // If tree is nil, it uses DefTree func PhaseCollision(s *Space, tree *Tree) error { - return PhaseCollisionWithBus(s, tree, event.DefaultBus, event.DefaultCallerMap) + return PhaseCollisionWithBus(s, tree, event.DefaultBus) } -// PhaseCollisionWithBus allows for a non-default bus and non-default entity mapping -// in a phase collision binding. -func PhaseCollisionWithBus(s *Space, tree *Tree, bus event.Handler, entities *event.CallerMap) error { - en := entities.GetEntity(s.CID) +// PhaseCollisionWithBus allows for a non-default bus in a phase collision binding. +func PhaseCollisionWithBus(s *Space, tree *Tree, bus event.Handler) error { + en := bus.GetCallerMap().GetEntity(s.CID) if cp, ok := en.(collisionPhase); ok { oc := cp.getCollisionPhase() oc.OnCollisionS = s @@ -46,46 +49,43 @@ func PhaseCollisionWithBus(s *Space, tree *Tree, bus event.Handler, entities *ev if oc.tree == nil { oc.tree = DefaultTree } - bus.Bind(event.Enter, s.CID, phaseCollisionEnter(entities)) + bus.UnsafeBind(event.Enter.UnsafeEventID, s.CID, phaseCollisionEnter) return nil } return errors.New("This space's entity does not implement collisionPhase") } // CollisionStart/Stop: when a PhaseCollision entity starts/stops touching some label. -// Payload: (Label) the label the entity has started/stopped touching -const ( - Start = "CollisionStart" - Stop = "CollisionStop" +var ( + Start = event.RegisterEvent[Label]() + Stop = event.RegisterEvent[Label]() ) -func phaseCollisionEnter(entities *event.CallerMap) func(id event.CID, nothing interface{}) int { - return func(id event.CID, nothing interface{}) int { - e := entities.GetEntity(id).(collisionPhase) - oc := e.getCollisionPhase() +func phaseCollisionEnter(id event.CallerID, handler event.Handler, _ interface{}) event.Response { + e := handler.GetCallerMap().GetEntity(id).(collisionPhase) + oc := e.getCollisionPhase() - // check hits - hits := oc.tree.Hits(oc.OnCollisionS) - newTouching := map[Label]bool{} + // check hits + hits := oc.tree.Hits(oc.OnCollisionS) + newTouching := map[Label]bool{} - // if any are new, trigger on collision - for _, h := range hits { - l := h.Label - if _, ok := oc.Touching[l]; !ok { - id.TriggerBus(Start, l, oc.bus) - } - newTouching[l] = true + // if any are new, trigger on collision + for _, h := range hits { + l := h.Label + if _, ok := oc.Touching[l]; !ok { + event.TriggerForCallerOn(oc.bus, id, Start, l) } + newTouching[l] = true + } - // if we lost any, trigger off collision - for l := range oc.Touching { - if _, ok := newTouching[l]; !ok { - id.TriggerBus(Stop, l, oc.bus) - } + // if we lost any, trigger off collision + for l := range oc.Touching { + if _, ok := newTouching[l]; !ok { + event.TriggerForCallerOn(handler, id, Stop, l) } + } - oc.Touching = newTouching + oc.Touching = newTouching - return 0 - } + return 0 } diff --git a/collision/onCollision_test.go b/collision/onCollision_test.go index 82777fbc..838e6b16 100644 --- a/collision/onCollision_test.go +++ b/collision/onCollision_test.go @@ -4,7 +4,7 @@ import ( "testing" "time" - "github.com/oakmound/oak/v3/event" + "github.com/oakmound/oak/v4/event" ) type cphase struct { @@ -12,55 +12,50 @@ type cphase struct { callers *event.CallerMap } -func (cp *cphase) Init() event.CID { - return cp.callers.NextID(cp) -} - func TestCollisionPhase(t *testing.T) { - callers := event.NewCallerMap() - bus := event.NewBus(callers) - go bus.ResolveChanges() + b := event.NewBus(event.NewCallerMap()) go func() { for { <-time.After(5 * time.Millisecond) - <-bus.TriggerBack(event.Enter, nil) + <-event.TriggerOn(b, event.Enter, event.EnterPayload{}) } }() - cp := cphase{ - callers: callers, - } - cid := cp.Init() + cp := &cphase{} + cid := b.GetCallerMap().Register(cp) s := NewSpace(10, 10, 10, 10, cid) tree := NewTree() - err := PhaseCollisionWithBus(s, tree, bus, callers) + err := PhaseCollisionWithBus(s, tree, b) if err != nil { t.Fatalf("phase collision failed: %v", err) } - var active bool - bus.Bind("CollisionStart", cid, func(event.CID, interface{}) int { - active = true + activeCh := make(chan bool, 5) + b1 := event.Bind(b, Start, cp, func(_ *cphase, _ Label) event.Response { + activeCh <- true return 0 }) - bus.Bind("CollisionStop", cid, func(event.CID, interface{}) int { - active = false + b2 := event.Bind(b, Stop, cp, func(_ *cphase, _ Label) event.Response { + activeCh <- false return 0 }) - + <-b1.Bound + <-b2.Bound s2 := NewLabeledSpace(15, 15, 10, 10, 5) tree.Add(s2) - time.Sleep(200 * time.Millisecond) - if !active { + if active := <-activeCh; !active { t.Fatalf("collision should be active") } tree.Remove(s2) time.Sleep(200 * time.Millisecond) - if active { + if active := <-activeCh; active { t.Fatalf("collision should be inactive") } +} +func TestPhaseCollision_Unembedded(t *testing.T) { + t.Parallel() s3 := NewSpace(10, 10, 10, 10, 5) - err = PhaseCollision(s3, nil) + err := PhaseCollision(s3, nil) if err == nil { t.Fatalf("phase collision should have failed") } diff --git a/collision/point.go b/collision/point.go index 168110da..b165d183 100644 --- a/collision/point.go +++ b/collision/point.go @@ -1,6 +1,6 @@ package collision -import "github.com/oakmound/oak/v3/alg/floatgeom" +import "github.com/oakmound/oak/v4/alg/floatgeom" // A Point is a specific point where // collision occurred and a zone to identify diff --git a/collision/ray/castFilter.go b/collision/ray/castFilter.go index 06103520..6f41a22e 100644 --- a/collision/ray/castFilter.go +++ b/collision/ray/castFilter.go @@ -1,8 +1,8 @@ package ray import ( - "github.com/oakmound/oak/v3/collision" - "github.com/oakmound/oak/v3/event" + "github.com/oakmound/oak/v4/collision" + "github.com/oakmound/oak/v4/event" ) // A CastFilter is a function that can be applied to a Caster @@ -46,7 +46,7 @@ func IgnoreLabels(ls ...collision.Label) CastOption { } // AcceptIDs is equivalent to AcceptLabels, but for CIDs. -func AcceptIDs(ids ...event.CID) CastOption { +func AcceptIDs(ids ...event.CallerID) CastOption { return AddFilter(func(s *collision.Space) bool { for _, id := range ids { if s.CID == id { @@ -58,7 +58,7 @@ func AcceptIDs(ids ...event.CID) CastOption { } // IgnoreIDs is equivalent to IgnoreLabels, but for CIDs. -func IgnoreIDs(ids ...event.CID) CastOption { +func IgnoreIDs(ids ...event.CallerID) CastOption { return AddFilter(func(s *collision.Space) bool { for _, id := range ids { if s.CID == id { diff --git a/collision/ray/castLimit.go b/collision/ray/castLimit.go index 5212958d..583249e4 100644 --- a/collision/ray/castLimit.go +++ b/collision/ray/castLimit.go @@ -1,8 +1,8 @@ package ray import ( - "github.com/oakmound/oak/v3/collision" - "github.com/oakmound/oak/v3/event" + "github.com/oakmound/oak/v4/collision" + "github.com/oakmound/oak/v4/event" ) // A CastLimit is a function that can be applied to @@ -47,7 +47,7 @@ func StopAtLabel(ls ...collision.Label) CastOption { // StopAtID will cause a caster to cease casting as soon as it // hits one of the input CIDs. -func StopAtID(ids ...event.CID) CastOption { +func StopAtID(ids ...event.CallerID) CastOption { return AddLimit(func(ps []collision.Point) bool { z := ps[len(ps)-1].Zone for _, id := range ids { diff --git a/collision/ray/caster.go b/collision/ray/caster.go index 90adcd5b..5d2ae1ac 100644 --- a/collision/ray/caster.go +++ b/collision/ray/caster.go @@ -3,8 +3,8 @@ package ray import ( "math" - "github.com/oakmound/oak/v3/alg/floatgeom" - "github.com/oakmound/oak/v3/collision" + "github.com/oakmound/oak/v4/alg/floatgeom" + "github.com/oakmound/oak/v4/collision" ) var ( diff --git a/collision/ray/caster_test.go b/collision/ray/caster_test.go index c8e7fd00..25e9384c 100644 --- a/collision/ray/caster_test.go +++ b/collision/ray/caster_test.go @@ -4,8 +4,8 @@ import ( "reflect" "testing" - "github.com/oakmound/oak/v3/alg/floatgeom" - "github.com/oakmound/oak/v3/collision" + "github.com/oakmound/oak/v4/alg/floatgeom" + "github.com/oakmound/oak/v4/collision" ) func TestCasterScene(t *testing.T) { diff --git a/collision/ray/coneCaster.go b/collision/ray/coneCaster.go index 2f935cd1..db7108a0 100644 --- a/collision/ray/coneCaster.go +++ b/collision/ray/coneCaster.go @@ -1,9 +1,9 @@ package ray import ( - "github.com/oakmound/oak/v3/alg" - "github.com/oakmound/oak/v3/alg/floatgeom" - "github.com/oakmound/oak/v3/collision" + "github.com/oakmound/oak/v4/alg" + "github.com/oakmound/oak/v4/alg/floatgeom" + "github.com/oakmound/oak/v4/collision" ) var ( diff --git a/collision/ray/coneCaster_test.go b/collision/ray/coneCaster_test.go index 2a73765b..b7d84ca3 100644 --- a/collision/ray/coneCaster_test.go +++ b/collision/ray/coneCaster_test.go @@ -7,9 +7,9 @@ import ( "testing" "time" - "github.com/oakmound/oak/v3/alg" - "github.com/oakmound/oak/v3/alg/floatgeom" - "github.com/oakmound/oak/v3/collision" + "github.com/oakmound/oak/v4/alg" + "github.com/oakmound/oak/v4/alg/floatgeom" + "github.com/oakmound/oak/v4/collision" ) func TestConeCasterSettings(t *testing.T) { diff --git a/collision/ray/raycast_test.go b/collision/ray/raycast_test.go index 2060d0fc..a1eae541 100644 --- a/collision/ray/raycast_test.go +++ b/collision/ray/raycast_test.go @@ -3,15 +3,15 @@ package ray import ( "testing" - "github.com/oakmound/oak/v3/alg/floatgeom" - "github.com/oakmound/oak/v3/alg/range/floatrange" - "github.com/oakmound/oak/v3/collision" + "github.com/oakmound/oak/v4/alg/floatgeom" + "github.com/oakmound/oak/v4/alg/span" + "github.com/oakmound/oak/v4/collision" ) func TestEmptyRaycasts(t *testing.T) { t.Skip() collision.DefaultTree.Clear() - vRange := floatrange.NewLinear(3, 359) + vRange := span.NewLinear(3.0, 359.0) tests := 100 for i := 0; i < tests; i++ { p1 := floatgeom.Point2{vRange.Poll(), vRange.Poll()} diff --git a/collision/rtree.go b/collision/rtree.go index 836ade53..d8e3a5d5 100644 --- a/collision/rtree.go +++ b/collision/rtree.go @@ -8,7 +8,7 @@ import ( "math" "sort" - "github.com/oakmound/oak/v3/alg/floatgeom" + "github.com/oakmound/oak/v4/alg/floatgeom" ) // Rtree represents an R-tree, a balanced search tree for storing and querying diff --git a/collision/rtree_test.go b/collision/rtree_test.go index e2ab9662..76dd50aa 100644 --- a/collision/rtree_test.go +++ b/collision/rtree_test.go @@ -10,7 +10,7 @@ import ( "strings" "testing" - "github.com/oakmound/oak/v3/alg/floatgeom" + "github.com/oakmound/oak/v4/alg/floatgeom" ) var ( diff --git a/collision/space.go b/collision/space.go index 1d9c07ac..7634abdc 100644 --- a/collision/space.go +++ b/collision/space.go @@ -1,9 +1,9 @@ package collision import ( - "github.com/oakmound/oak/v3/alg/floatgeom" - "github.com/oakmound/oak/v3/event" - "github.com/oakmound/oak/v3/physics" + "github.com/oakmound/oak/v4/alg/floatgeom" + "github.com/oakmound/oak/v4/event" + "github.com/oakmound/oak/v4/physics" ) // ID Types constant @@ -23,7 +23,7 @@ type Space struct { Label Label // A CID can be used to get the exact // entity which this rectangle belongs to. - CID event.CID + CID event.CallerID // Type represents which ID space the above ID // corresponds to. Type int @@ -197,7 +197,7 @@ func NewUnassignedSpace(x, y, w, h float64) *Space { } // NewSpace returns a space with an associated caller id -func NewSpace(x, y, w, h float64, cID event.CID) *Space { +func NewSpace(x, y, w, h float64, cID event.CallerID) *Space { return NewFullSpace(x, y, w, h, NilLabel, cID) } @@ -212,7 +212,7 @@ func NewLabeledSpace(x, y, w, h float64, l Label) *Space { } // NewFullSpace returns a space with both a label and a caller id -func NewFullSpace(x, y, w, h float64, l Label, cID event.CID) *Space { +func NewFullSpace(x, y, w, h float64, l Label, cID event.CallerID) *Space { rect := NewRect(x, y, w, h) return &Space{ rect, @@ -223,12 +223,12 @@ func NewFullSpace(x, y, w, h float64, l Label, cID event.CID) *Space { } // NewRect2Space returns a space with an associated caller id from a rect2 -func NewRect2Space(rect floatgeom.Rect2, cID event.CID) *Space { +func NewRect2Space(rect floatgeom.Rect2, cID event.CallerID) *Space { return NewSpace(rect.Min.X(), rect.Min.Y(), rect.W(), rect.H(), cID) } // NewRectSpace creates a colliison space with the specified 3D rectangle -func NewRectSpace(rect floatgeom.Rect3, l Label, cID event.CID) *Space { +func NewRectSpace(rect floatgeom.Rect3, l Label, cID event.CallerID) *Space { return &Space{ rect, l, diff --git a/collision/space_test.go b/collision/space_test.go index 8c41e398..3a4fe14b 100644 --- a/collision/space_test.go +++ b/collision/space_test.go @@ -3,9 +3,9 @@ package collision import ( "testing" - "github.com/oakmound/oak/v3/alg/floatgeom" + "github.com/oakmound/oak/v4/alg/floatgeom" - "github.com/oakmound/oak/v3/physics" + "github.com/oakmound/oak/v4/physics" ) func TestSpaceFuncs(t *testing.T) { diff --git a/collision/tree.go b/collision/tree.go index e32cd5b4..70069a5a 100644 --- a/collision/tree.go +++ b/collision/tree.go @@ -4,8 +4,8 @@ import ( "errors" "sync" - "github.com/oakmound/oak/v3/alg/floatgeom" - "github.com/oakmound/oak/v3/oakerr" + "github.com/oakmound/oak/v4/alg/floatgeom" + "github.com/oakmound/oak/v4/oakerr" ) // A Tree provides a space for managing collisions between rectangles diff --git a/collision/tree_test.go b/collision/tree_test.go index 26f025a9..8795ec8e 100644 --- a/collision/tree_test.go +++ b/collision/tree_test.go @@ -4,8 +4,8 @@ import ( "math/rand" "testing" - "github.com/oakmound/oak/v3/alg/floatgeom" - "github.com/oakmound/oak/v3/alg/range/floatrange" + "github.com/oakmound/oak/v4/alg/floatgeom" + "github.com/oakmound/oak/v4/alg/span" ) func TestNewTreeInvalidChildren(t *testing.T) { @@ -150,8 +150,8 @@ func randomSpace() *Space { } var ( - xRange = floatrange.NewLinear(0, 10000) - yRange = floatrange.NewLinear(0, 10000) - wRange = floatrange.NewLinear(1, 50) - hRange = floatrange.NewLinear(1, 50) + xRange = span.NewLinear(0.0, 10000.0) + yRange = span.NewLinear(0.0, 10000.0) + wRange = span.NewLinear(1.0, 50.0) + hRange = span.NewLinear(1.0, 50.0) ) diff --git a/config.go b/config.go index 37f26a13..d6a43b41 100644 --- a/config.go +++ b/config.go @@ -2,67 +2,45 @@ package oak import ( "encoding/json" - "errors" "io" - "time" - "github.com/oakmound/oak/v3/fileutil" - "github.com/oakmound/oak/v3/shiny/driver" + "github.com/oakmound/oak/v4/fileutil" + "github.com/oakmound/oak/v4/shiny/driver" ) -// Config stores initialization settings for oak. +// A Config defines the settings oak accepts on initialization. Some of these settings may be ignored depending +// on the target platform. type Config struct { - Driver Driver `json:"-"` - Assets Assets `json:"assets"` - Debug Debug `json:"debug"` - Screen Screen `json:"screen"` - BatchLoadOptions BatchLoadOptions `json:"batchLoadOptions"` - FrameRate int `json:"frameRate"` - DrawFrameRate int `json:"drawFrameRate"` - IdleDrawFrameRate int `json:"idleDrawFrameRate"` - Language string `json:"language"` - Title string `json:"title"` - EventRefreshRate Duration `json:"refreshRate"` - BatchLoad bool `json:"batchLoad"` - GestureSupport bool `json:"gestureSupport"` - LoadBuiltinCommands bool `json:"loadBuiltinCommands"` - TrackInputChanges bool `json:"trackInputChanges"` - EnableDebugConsole bool `json:"enableDebugConsole"` - TopMost bool `json:"topmost"` - Borderless bool `json:"borderless"` - Fullscreen bool `json:"fullscreen"` - SkipRNGSeed bool `json:"skip_rng_seed"` - UnlimitedDrawFrameRate bool `json:"unlimitedDrawFrameRate` -} - -// A Duration is a wrapper around time.Duration that allows for easier json formatting. -type Duration time.Duration - -// MarshalJSON writes a duration as json. -func (d Duration) MarshalJSON() ([]byte, error) { - return json.Marshal(time.Duration(d).String()) -} - -// UnmarshalJSON extracts a duration from a json byte slice. -func (d *Duration) UnmarshalJSON(b []byte) error { - var v interface{} - if err := json.Unmarshal(b, &v); err != nil { - return err - } - switch value := v.(type) { - case float64: - *d = Duration(time.Duration(value)) - return nil - case string: - tmp, err := time.ParseDuration(value) - if err != nil { - return err - } - *d = Duration(tmp) - return nil - default: - return errors.New("invalid duration type") - } + Driver Driver `json:"-"` + // Assets defines where assets should be loaded from by default. Defaults to + // 'assets/audio' and 'assets/images'. + Assets Assets `json:"assets"` + Debug Debug `json:"debug"` + Screen Screen `json:"screen"` + BatchLoadOptions BatchLoadOptions `json:"batchLoadOptions"` + // FrameRate, representing the rate enter frame events are triggered, defaults to 60. + FrameRate int `json:"frameRate"` + // DrawFrameRate is ignored on JS. It defaults to 60. + DrawFrameRate int `json:"drawFrameRate"` + // IdleDrawFrameRate defaults to 60. When a window goes out of focus, this setting can be lowered to + // reduce resource consumption by drawing. + IdleDrawFrameRate int `json:"idleDrawFrameRate"` + // Language defines the language oak logs are attempted to be translated to. Defaults to English. + Language string `json:"language"` + // Title defaults to 'Oak Window'. + Title string `json:"title"` + BatchLoad bool `json:"batchLoad"` + GestureSupport bool `json:"gestureSupport"` + LoadBuiltinCommands bool `json:"loadBuiltinCommands"` + TrackInputChanges bool `json:"trackInputChanges"` + // EnableDebugConsole is ignored on JS. + EnableDebugConsole bool `json:"enableDebugConsole"` + TopMost bool `json:"topmost"` + Borderless bool `json:"borderless"` + Fullscreen bool `json:"fullscreen"` + SkipRNGSeed bool `json:"skip_rng_seed"` + // UnlimitedDrawFrameRate is ignored on JS (it is effectively always true). + UnlimitedDrawFrameRate bool `json:"unlimitedDrawFrameRate"` } // NewConfig creates a config from a set of transformation options. @@ -98,7 +76,6 @@ func (c Config) setDefaults() Config { c.IdleDrawFrameRate = 60 c.Language = "English" c.Title = "Oak Window" - c.EventRefreshRate = Duration(50 * time.Millisecond) return c } @@ -121,16 +98,10 @@ type Screen struct { Height int `json:"height"` Width int `json:"width"` Scale float64 `json:"scale"` - // Target sets the expected dimensions of the monitor the game will be opened on, in pixels. - // If Fullscreen is false, then a scaling will be applied to correct the game screen size to be - // appropriate for the Target size. If no TargetWidth or Height is provided, scaling will not - // be adjusted. - TargetWidth int `json:"targetHeight"` - TargetHeight int `json:"targetWidth"` } // BatchLoadOptions is a json type storing customizations for batch loading. -// These settings do not take effect unless batch load is true. +// These settings do not take effect unless Config.BatchLoad is true. type BatchLoadOptions struct { BlankOutAudio bool `json:"blankOutAudio"` MaxImageFileSize int64 `json:"maxImageFileSize"` @@ -198,12 +169,6 @@ func (c Config) overwriteFrom(c2 Config) Config { if c2.Screen.Scale != 0 { c.Screen.Scale = c2.Screen.Scale } - if c2.Screen.TargetWidth != 0 { - c.Screen.TargetWidth = c2.Screen.TargetWidth - } - if c2.Screen.TargetHeight != 0 { - c.Screen.TargetHeight = c2.Screen.TargetHeight - } c.BatchLoadOptions.BlankOutAudio = c2.BatchLoadOptions.BlankOutAudio if c2.BatchLoadOptions.MaxImageFileSize != 0 { c.BatchLoadOptions.MaxImageFileSize = c2.BatchLoadOptions.MaxImageFileSize @@ -223,9 +188,6 @@ func (c Config) overwriteFrom(c2 Config) Config { if c2.Title != "" { c.Title = c2.Title } - if c2.EventRefreshRate != 0 { - c.EventRefreshRate = c2.EventRefreshRate - } // Booleans can be directly overwritten-- all booleans in a Config // default to false, if they were unset they will stay false. c.BatchLoad = c2.BatchLoad diff --git a/config_test.go b/config_test.go index 0bbf9a92..527da619 100644 --- a/config_test.go +++ b/config_test.go @@ -5,7 +5,8 @@ import ( "os" "path/filepath" "testing" - "time" + + "github.com/oakmound/oak/v4/shiny/screen" ) func TestDefaultConfigFileMatchesEmptyConfig(t *testing.T) { @@ -47,7 +48,6 @@ func configEquals(c1, c2 Config) bool { IdleDrawFrameRate int `json:"idleDrawFrameRate"` Language string `json:"language"` Title string `json:"title"` - EventRefreshRate Duration `json:"refreshRate"` BatchLoad bool `json:"batchLoad"` GestureSupport bool `json:"gestureSupport"` LoadBuiltinCommands bool `json:"loadBuiltinCommands"` @@ -57,7 +57,7 @@ func configEquals(c1, c2 Config) bool { Borderless bool `json:"borderless"` Fullscreen bool `json:"fullscreen"` SkipRNGSeed bool `json:"skip_rng_seed"` - UnlimitedDrawFrameRate bool `json:"unlimitedDrawFrameRate` + UnlimitedDrawFrameRate bool `json:"unlimitedDrawFrameRate"` } cc1 := comparableConfig{ Assets: c1.Assets, @@ -69,7 +69,6 @@ func configEquals(c1, c2 Config) bool { IdleDrawFrameRate: c1.IdleDrawFrameRate, Language: c1.Language, Title: c1.Title, - EventRefreshRate: c1.EventRefreshRate, BatchLoad: c1.BatchLoad, GestureSupport: c1.GestureSupport, LoadBuiltinCommands: c1.LoadBuiltinCommands, @@ -91,7 +90,6 @@ func configEquals(c1, c2 Config) bool { IdleDrawFrameRate: c2.IdleDrawFrameRate, Language: c2.Language, Title: c2.Title, - EventRefreshRate: c2.EventRefreshRate, BatchLoad: c2.BatchLoad, GestureSupport: c2.GestureSupport, LoadBuiltinCommands: c2.LoadBuiltinCommands, @@ -113,14 +111,13 @@ func TestConfig_overwriteFrom(t *testing.T) { Filter: "filter", }, Screen: Screen{ - X: 1, - Y: 1, - TargetWidth: 1, - TargetHeight: 1, + X: 1, + Y: 1, }, BatchLoadOptions: BatchLoadOptions{ MaxImageFileSize: 10000, }, + Driver: func(f func(screen.Screen)) { panic("fake") }, } c1 := Config{} c1.overwriteFrom(c2) @@ -144,59 +141,3 @@ func TestReaderConfigBadJSON(t *testing.T) { // This error is an stdlib error, not ours, so we don't care // about its type } - -func TestDuration_HappyPath(t *testing.T) { - d := Duration(time.Second) - marshalled, err := d.MarshalJSON() - if err != nil { - t.Fatalf("marshal duration failed: %v", err) - } - d2 := new(Duration) - err = d2.UnmarshalJSON(marshalled) - if err != nil { - t.Fatalf("unmarshal failed: %v", err) - } - marshalled2, err := d2.MarshalJSON() - if err != nil { - t.Fatalf("marshal duration 2 failed: %v", err) - } - if !bytes.Equal(marshalled, marshalled2) { - t.Fatalf("marshals not equal: %v vs %v", string(marshalled), string(marshalled2)) - } -} - -func TestDuration_UnmarshalJSON_Float(t *testing.T) { - f := []byte("10.0") - d2 := new(Duration) - err := d2.UnmarshalJSON(f) - if err != nil { - t.Fatalf("unmarshal failed: %v", err) - } -} - -func TestDuration_UnmarshalJSON_Boolean(t *testing.T) { - f := []byte("false") - d2 := new(Duration) - err := d2.UnmarshalJSON(f) - if err == nil { - t.Fatalf("expected failure in unmarshal") - } -} - -func TestDuration_UnmarshalJSON_BadString(t *testing.T) { - f := []byte("\"10mmmm\"") - d2 := new(Duration) - err := d2.UnmarshalJSON(f) - if err == nil { - t.Fatalf("expected failure in unmarshal") - } -} - -func TestDuration_UnmarshalJSON_BadJSON(t *testing.T) { - f := []byte("\"1mm") - d2 := new(Duration) - err := d2.UnmarshalJSON(f) - if err == nil { - t.Fatalf("expected failure in unmarshal") - } -} diff --git a/debugstream/commands.go b/debugstream/commands.go index 8ca6e69a..c37fd31f 100644 --- a/debugstream/commands.go +++ b/debugstream/commands.go @@ -10,7 +10,7 @@ import ( "strings" "sync" - "github.com/oakmound/oak/v3/oakerr" + "github.com/oakmound/oak/v4/oakerr" ) // ScopedCommands for the debug stream commands. diff --git a/debugstream/defaultcommands.go b/debugstream/defaultcommands.go index 3433e175..b189b731 100644 --- a/debugstream/defaultcommands.go +++ b/debugstream/defaultcommands.go @@ -5,7 +5,7 @@ import ( "io" "sync" - "github.com/oakmound/oak/v3/window" + "github.com/oakmound/oak/v4/window" ) var ( @@ -28,7 +28,7 @@ func AddCommand(c Command) error { } // AttachToStream if possible to start consuming the stream -// and executing commands per the stored infomraiton in the ScopeCommands. +// and executing commands per the stored information in the ScopeCommands. func AttachToStream(ctx context.Context, input io.Reader, output io.Writer) { checkOrCreateDefaults() DefaultCommands.AttachToStream(ctx, input, output) diff --git a/debugstream/scopeHelper.go b/debugstream/scopeHelper.go index b3e4183e..0dd9a6dd 100644 --- a/debugstream/scopeHelper.go +++ b/debugstream/scopeHelper.go @@ -5,15 +5,15 @@ import ( "strconv" "strings" - "github.com/oakmound/oak/v3/collision" - "github.com/oakmound/oak/v3/debugtools" - "github.com/oakmound/oak/v3/dlog" - "github.com/oakmound/oak/v3/event" - "github.com/oakmound/oak/v3/mouse" - "github.com/oakmound/oak/v3/oakerr" - "github.com/oakmound/oak/v3/render" - "github.com/oakmound/oak/v3/render/mod" - "github.com/oakmound/oak/v3/window" + "github.com/oakmound/oak/v4/collision" + "github.com/oakmound/oak/v4/debugtools" + "github.com/oakmound/oak/v4/dlog" + "github.com/oakmound/oak/v4/event" + "github.com/oakmound/oak/v4/mouse" + "github.com/oakmound/oak/v4/oakerr" + "github.com/oakmound/oak/v4/render" + "github.com/oakmound/oak/v4/render/mod" + "github.com/oakmound/oak/v4/window" ) // AddDefaultsForScope for debugging. @@ -39,8 +39,9 @@ func moveWindow(w window.Window) func([]string) string { InputName: "coordinates", }.Error() } - width := parseTokenAsInt(sub, 2, w.Width()) - height := parseTokenAsInt(sub, 3, w.Height()) + bds := w.Bounds() + width := parseTokenAsInt(sub, 2, bds.X()) + height := parseTokenAsInt(sub, 3, bds.Y()) v := w.Viewport() x := parseTokenAsInt(sub, 0, v.X()) y := parseTokenAsInt(sub, 1, v.Y()) @@ -69,14 +70,13 @@ const explainMouseDetails = "the mext mouse click on the given window will print func mouseCommands(w window.Window) func([]string) string { return func(tokenString []string) string { - w.EventHandler().GlobalBind("MouseRelease", mouseDetails(w)) + event.GlobalBind(w.EventHandler(), mouse.Release, mouseDetails(w)) return "" } } -func mouseDetails(w window.Window) func(event.CID, interface{}) int { - return func(nothing event.CID, mevent interface{}) int { - me := mevent.(mouse.Event) +func mouseDetails(w window.Window) func(*mouse.Event) event.Response { + return func(me *mouse.Event) event.Response { viewPos := w.Viewport() x := int(me.X()) + viewPos[0] y := int(me.Y()) + viewPos[1] @@ -86,17 +86,19 @@ func mouseDetails(w window.Window) func(event.CID, interface{}) int { if len(results) == 0 { results = mouse.Hits(loc) } + cm := w.EventHandler().GetCallerMap() + if len(results) > 0 { - i := int(results[0].CID) - if i > 0 && event.HasEntity(event.CID(i)) { - e := event.GetEntity(event.CID(i)) + i := results[0].CID + if i > 0 && cm.HasEntity(i) { + e := cm.HasEntity(i) fmt.Printf("%+v\n", e) } else { fmt.Println("No entity ", i) } } - return event.UnbindSingle + return event.ResponseUnbindThisBinding } } diff --git a/debugstream/scopeHelper_test.go b/debugstream/scopeHelper_test.go index 62ed8485..955589a1 100644 --- a/debugstream/scopeHelper_test.go +++ b/debugstream/scopeHelper_test.go @@ -6,11 +6,11 @@ import ( "testing" "time" - "github.com/oakmound/oak/v3/alg/intgeom" - "github.com/oakmound/oak/v3/debugtools" - "github.com/oakmound/oak/v3/event" - "github.com/oakmound/oak/v3/render" - "github.com/oakmound/oak/v3/window" + "github.com/oakmound/oak/v4/alg/intgeom" + "github.com/oakmound/oak/v4/debugtools" + "github.com/oakmound/oak/v4/event" + "github.com/oakmound/oak/v4/render" + "github.com/oakmound/oak/v4/window" ) type fakeWindow struct { @@ -45,12 +45,8 @@ func (f *fakeWindow) MoveWindow(x, y, w, h int) error { return nil } -func (f *fakeWindow) Width() int { - return 1 -} - -func (f *fakeWindow) Height() int { - return 1 +func (f *fakeWindow) Bounds() intgeom.Point2 { + return intgeom.Point2{1, 1} } func (f *fakeWindow) Viewport() intgeom.Point2 { diff --git a/debugtools/inputviz/joystick.go b/debugtools/inputviz/joystick.go index ba61d6a3..ec2e382f 100644 --- a/debugtools/inputviz/joystick.go +++ b/debugtools/inputviz/joystick.go @@ -9,14 +9,14 @@ import ( "math" "time" - "github.com/oakmound/oak/v3/alg/floatgeom" - "github.com/oakmound/oak/v3/dlog" - "github.com/oakmound/oak/v3/event" - "github.com/oakmound/oak/v3/joystick" - "github.com/oakmound/oak/v3/key" - "github.com/oakmound/oak/v3/render" - "github.com/oakmound/oak/v3/render/mod" - "github.com/oakmound/oak/v3/scene" + "github.com/oakmound/oak/v4/alg/floatgeom" + "github.com/oakmound/oak/v4/dlog" + "github.com/oakmound/oak/v4/event" + "github.com/oakmound/oak/v4/joystick" + "github.com/oakmound/oak/v4/key" + "github.com/oakmound/oak/v4/render" + "github.com/oakmound/oak/v4/render/mod" + "github.com/oakmound/oak/v4/scene" ) //go:embed controllerOutline.png @@ -47,7 +47,7 @@ type Joystick struct { BaseLayer int ctx *scene.Context - event.CID + event.CallerID joy *joystick.Joystick rs map[string]render.Modifiable lastState *joystick.State @@ -55,11 +55,12 @@ type Joystick struct { lStickCenter floatgeom.Point2 rStickCenter floatgeom.Point2 cancel func() + + bindings []event.Binding } -func (j *Joystick) Init() event.CID { - j.CID = j.ctx.CallerMap.NextID(j) - return j.CID +func (j *Joystick) CID() event.CallerID { + return j.CallerID } func (j *Joystick) RenderAndListen(ctx *scene.Context, joy *joystick.Joystick, layer int) error { @@ -76,7 +77,7 @@ func (j *Joystick) RenderAndListen(ctx *scene.Context, joy *joystick.Joystick, l j.rs = make(map[string]render.Modifiable) j.lastState = &joystick.State{} j.ctx = ctx - j.Init() + j.CallerID = ctx.CallerMap.Register(j) j.rs["Outline"] = outline j.rs["LtStick"] = render.NewCircle(color.RGBA{255, 255, 255, 255}, 15, 12) j.rs["RtStick"] = render.NewCircle(color.RGBA{255, 255, 255, 255}, 15, 12) @@ -180,19 +181,22 @@ func (j *Joystick) RenderAndListen(ctx *scene.Context, joy *joystick.Joystick, l joystick.InputRightShoulder, } - j.CheckedIDBind(joystick.Disconnected, func(rend *Joystick, _ uint32) { + b1 := event.Bind(ctx, joystick.Disconnected, j, func(rend *Joystick, _ uint32) event.Response { j.Destroy() + return 0 }) - j.CheckedBind(key.Down+key.Spacebar, func(rend *Joystick, st *joystick.State) { + // TODO: it is bad that you need to import two 'key' packages + b2 := event.Bind(ctx, key.Down(key.Spacebar), j, func(j *Joystick, _ key.Event) event.Response { j.joy.Vibrate(math.MaxUint16, math.MaxUint16) go func() { time.Sleep(1 * time.Second) j.joy.Vibrate(0, 0) }() + return 0 }) - j.CheckedBind(joystick.Change, func(rend *Joystick, st *joystick.State) { + b3 := event.Bind(ctx, joystick.Change, j, func(j *Joystick, st *joystick.State) event.Response { for _, inputB := range bts { b := string(inputB) r := j.rs[b] @@ -211,60 +215,36 @@ func (j *Joystick) RenderAndListen(ctx *scene.Context, joy *joystick.Joystick, l tgr = "RtTrigger" x = j.rs[tgr].X() j.rs[tgr].SetPos(x, j.triggerY+float64(st.TriggerR/16)) + return 0 }) - j.CheckedBind(joystick.LtStickChange, func(rend *Joystick, st *joystick.State) { + b4 := event.Bind(ctx, joystick.LtStickChange, j, func(j *Joystick, st *joystick.State) event.Response { pos := j.lStickCenter pos = pos.Add(floatgeom.Point2{ float64(st.StickLX / 2048), -float64(st.StickLY / 2048), }) j.rs["LtStick"].SetPos(pos.X(), pos.Y()) + return 0 }) - j.CheckedBind(joystick.RtStickChange, func(rend *Joystick, st *joystick.State) { + b5 := event.Bind(ctx, joystick.RtStickChange, j, func(j *Joystick, st *joystick.State) event.Response { pos := j.rStickCenter pos = pos.Add(floatgeom.Point2{ float64(st.StickRX / 2048), -float64(st.StickRY / 2048), }) j.rs["RtStick"].SetPos(pos.X(), pos.Y()) - }) - return nil -} - -func (j *Joystick) CheckedIDBind(ev string, f func(*Joystick, uint32)) { - j.Bind(ev, func(id event.CID, jid interface{}) int { - joy, ok := event.GetEntity(id).(*Joystick) - if !ok { - return 0 - } - n, ok := jid.(uint32) - if !ok { - return 0 - } - f(joy, n) - return 0 - }) -} - -func (j *Joystick) CheckedBind(ev string, f func(*Joystick, *joystick.State)) { - j.Bind(ev, func(id event.CID, state interface{}) int { - joy, ok := event.GetEntity(id).(*Joystick) - if !ok { - return 0 - } - st, ok := state.(*joystick.State) - if !ok { - return 0 - } - f(joy, st) return 0 }) + j.bindings = []event.Binding{b1, b2, b3, b4, b5} + return nil } func (j *Joystick) Destroy() { - j.UnbindAll() + for _, b := range j.bindings { + b.Unbind() + } for _, r := range j.rs { r.Undraw() } diff --git a/debugtools/inputviz/keyboard.go b/debugtools/inputviz/keyboard.go index 6f21712e..165271f7 100644 --- a/debugtools/inputviz/keyboard.go +++ b/debugtools/inputviz/keyboard.go @@ -3,15 +3,15 @@ package inputviz import ( "image/color" - "github.com/oakmound/oak/v3/alg/floatgeom" - "github.com/oakmound/oak/v3/event" - "github.com/oakmound/oak/v3/key" - "github.com/oakmound/oak/v3/render" - "github.com/oakmound/oak/v3/scene" + "github.com/oakmound/oak/v4/alg/floatgeom" + "github.com/oakmound/oak/v4/event" + "github.com/oakmound/oak/v4/key" + "github.com/oakmound/oak/v4/render" + "github.com/oakmound/oak/v4/scene" ) type KeyboardLayout interface { - KeyRect(k string) floatgeom.Rect2 + KeyRect(k key.Code) floatgeom.Rect2 } type LayoutKey interface { @@ -19,7 +19,7 @@ type LayoutKey interface { } type LayoutPosition struct { - Key string + Key key.Code Gap bool Width float64 Height float64 @@ -36,37 +36,37 @@ func (g gap) Pos() LayoutPosition { } } -type standardKey string +type standardKey key.Code func (s standardKey) Pos() LayoutPosition { return LayoutPosition{ - Key: string(s), + Key: key.Code(s), Width: 1, Height: 1, } } type wideKey struct { - k string + k key.Code w float64 } func (w wideKey) Pos() LayoutPosition { return LayoutPosition{ - Key: string(w.k), + Key: w.k, Width: w.w, Height: 1, } } type tallKey struct { - k string + k key.Code h float64 } func (h tallKey) Pos() LayoutPosition { return LayoutPosition{ - Key: string(h.k), + Key: h.k, Width: 1, Height: h.h, } @@ -74,7 +74,7 @@ func (h tallKey) Pos() LayoutPosition { type LayoutQWERTY struct { Bounds floatgeom.Rect2 - layoutMap map[string]LayoutPosition + layoutMap map[key.Code]LayoutPosition } func (l *LayoutQWERTY) init() { @@ -83,14 +83,14 @@ func (l *LayoutQWERTY) init() { } type sk = standardKey - l.layoutMap = make(map[string]LayoutPosition) + l.layoutMap = make(map[key.Code]LayoutPosition) qwertyRows := [][]LayoutKey{ {sk(key.Escape), gap(1), sk(key.F1), sk(key.F2), sk(key.F3), sk(key.F4), gap(.5), sk(key.F5), sk(key.F6), sk(key.F7), sk(key.F8), gap(.5), sk(key.F9), sk(key.F10), sk(key.F11), sk(key.F12), gap(2.1), sk(key.Pause)}, - {sk(key.GraveAccent), sk(key.One), sk(key.Two), sk(key.Three), sk(key.Four), sk(key.Five), sk(key.Six), sk(key.Seven), sk(key.Eight), sk(key.Nine), sk(key.Zero), sk(key.HyphenMinus), sk(key.EqualSign), wideKey{key.DeleteBackspace, 2.0}, gap(.1), sk(key.Insert), sk(key.Home), sk(key.PageUp), gap(.1), sk(key.KeypadNumLock), sk(key.KeypadSlash), sk(key.KeypadAsterisk), sk(key.KeypadHyphenMinus)}, + {sk(key.GraveAccent), sk(key.Num1), sk(key.Num2), sk(key.Num3), sk(key.Num4), sk(key.Num5), sk(key.Num6), sk(key.Num7), sk(key.Num8), sk(key.Num9), sk(key.Num0), sk(key.HyphenMinus), sk(key.EqualSign), wideKey{key.DeleteBackspace, 2.0}, gap(.1), sk(key.Insert), sk(key.Home), sk(key.PageUp), gap(.1), sk(key.KeypadNumLock), sk(key.KeypadSlash), sk(key.KeypadAsterisk), sk(key.KeypadHyphenMinus)}, {wideKey{key.Tab, 1.5}, sk(key.Q), sk(key.W), sk(key.E), sk(key.R), sk(key.T), sk(key.Y), sk(key.U), sk(key.I), sk(key.O), sk(key.P), sk(key.LeftSquareBracket), sk(key.RightSquareBracket), wideKey{key.Backslash, 1.5}, gap(.1), sk(key.DeleteForward), sk(key.End), sk(key.PageDown), gap(.1), sk(key.Keypad7), sk(key.Keypad8), sk(key.Keypad9), tallKey{key.KeypadPlusSign, 2}}, {wideKey{key.CapsLock, 1.5}, sk(key.A), sk(key.S), sk(key.D), sk(key.F), sk(key.G), sk(key.H), sk(key.J), sk(key.K), sk(key.L), sk(key.Semicolon), sk(key.Apostrophe), wideKey{key.ReturnEnter, 2.5}, gap(3.2), sk(key.Keypad4), sk(key.Keypad5), sk(key.Keypad6)}, {wideKey{key.LeftShift, 2.0}, sk(key.Z), sk(key.X), sk(key.C), sk(key.V), sk(key.B), sk(key.N), sk(key.M), sk(key.Comma), sk(key.FullStop), sk(key.Slash), wideKey{key.RightShift, 3.0}, gap(1.1), sk(key.UpArrow), gap(1.1), sk(key.Keypad1), sk(key.Keypad2), sk(key.Keypad3), tallKey{key.KeypadEnter, 2.0}}, - {wideKey{key.LeftControl, 1.5}, sk(key.LeftGUI), wideKey{key.LeftAlt, 1.5}, wideKey{key.Spacebar, 7.0}, wideKey{key.RightAlt, 1.5}, sk(key.RightGUI), wideKey{key.RightControl, 1.5}, gap(.1), sk(key.LeftArrow), sk(key.DownArrow), sk(key.RightArrow), gap(.1), wideKey{key.Keypad0, 2.0}, sk(key.KeypadPeriod)}, + {wideKey{key.LeftControl, 1.5}, sk(key.LeftGUI), wideKey{key.LeftAlt, 1.5}, wideKey{key.Spacebar, 7.0}, wideKey{key.RightAlt, 1.5}, sk(key.RightGUI), wideKey{key.RightControl, 1.5}, gap(.1), sk(key.LeftArrow), sk(key.DownArrow), sk(key.RightArrow), gap(.1), wideKey{key.Keypad0, 2.0}, sk(key.KeypadFullStop)}, } rowFloats := []float64{0.0, 1.1, 2.1, 3.1, 4.1, 5.1} for row, cols := range qwertyRows { @@ -98,7 +98,7 @@ func (l *LayoutQWERTY) init() { cf := 0.0 for _, v := range cols { ps := v.Pos() - if ps.Key != "" { + if ps.Key != 0 { l.layoutMap[ps.Key] = LayoutPosition{ Row: rf, Col: cf, @@ -111,7 +111,7 @@ func (l *LayoutQWERTY) init() { } } -func (l *LayoutQWERTY) KeyRect(k string) floatgeom.Rect2 { +func (l *LayoutQWERTY) KeyRect(k key.Code) floatgeom.Rect2 { l.init() pos, ok := l.layoutMap[k] @@ -137,31 +137,32 @@ func (l *LayoutQWERTY) KeyRect(k string) floatgeom.Rect2 { return floatgeom.NewRect2WH(x, y, keyWidth, keyHeight) } -var defaultColors = map[string]color.Color{} +var defaultColors = map[key.Code]color.Color{} type Keyboard struct { Rect floatgeom.Rect2 BaseLayer int - Colors map[string]color.Color + Colors map[key.Code]color.Color KeyboardLayout RenderCharacters bool Font *render.Font - event.CID + event.CallerID ctx *scene.Context - rs map[string]*render.Switch + rs map[key.Code]*render.Switch + + bindings []event.Binding } -func (k *Keyboard) Init() event.CID { - k.CID = k.ctx.CallerMap.NextID(k) - return k.CID +func (k *Keyboard) CID() event.CallerID { + return k.CallerID } func (k *Keyboard) RenderAndListen(ctx *scene.Context, layer int) error { k.ctx = ctx - k.Init() + k.CallerID = k.ctx.CallerMap.Register(k) if k.Rect.W() == 0 || k.Rect.H() == 0 { k.Rect.Max = k.Rect.Min.Add(floatgeom.Point2{320, 180}) @@ -178,9 +179,9 @@ func (k *Keyboard) RenderAndListen(ctx *scene.Context, layer int) error { k.Font = render.DefaultFont() } - k.rs = make(map[string]*render.Switch) + k.rs = make(map[key.Code]*render.Switch) - for kv := range key.AllKeys { + for kv, kstr := range key.AllKeys { rect := k.KeyboardLayout.KeyRect(kv) if rect == (floatgeom.Rect2{}) { continue @@ -198,7 +199,7 @@ func (k *Keyboard) RenderAndListen(ctx *scene.Context, layer int) error { k.rs[kv] = r if k.RenderCharacters { x, y := rect.Min.X(), rect.Min.Y() - txt := k.Font.NewText(kv, x, y) + txt := k.Font.NewText(kstr, x, y) tw, th := txt.GetDims() xBuffer := rect.W() - float64(tw) yBuffer := rect.H() - float64(th) @@ -220,30 +221,28 @@ func (k *Keyboard) RenderAndListen(ctx *scene.Context, layer int) error { } } - k.Bind(key.Down, key.Binding(func(id event.CID, ev key.Event) int { - kb, _ := k.ctx.CallerMap.GetEntity(id).(*Keyboard) - btn := ev.Code.String()[4:] - if kb.rs[btn] == nil { + b1 := event.Bind(ctx, key.AnyDown, k, func(kb *Keyboard, ev key.Event) event.Response { + if kb.rs[ev.Code] == nil { return 0 } - kb.rs[btn].Set("pressed") + kb.rs[ev.Code].Set("pressed") return 0 - })) - k.Bind(key.Up, key.Binding(func(id event.CID, ev key.Event) int { - kb, _ := k.ctx.CallerMap.GetEntity(id).(*Keyboard) - btn := ev.Code.String()[4:] - if kb.rs[btn] == nil { + }) + b2 := event.Bind(ctx, key.AnyUp, k, func(kb *Keyboard, ev key.Event) event.Response { + if kb.rs[ev.Code] == nil { return 0 } - kb.rs[btn].Set("released") + kb.rs[ev.Code].Set("released") return 0 - })) - + }) + k.bindings = []event.Binding{b1, b2} return nil } func (k *Keyboard) Destroy() { - k.UnbindAll() + for _, b := range k.bindings { + b.Unbind() + } for _, r := range k.rs { r.Undraw() } diff --git a/debugtools/inputviz/mouse.go b/debugtools/inputviz/mouse.go index 3356a7a0..65c81310 100644 --- a/debugtools/inputviz/mouse.go +++ b/debugtools/inputviz/mouse.go @@ -1,3 +1,4 @@ +// Package inputviz provides components that enable visualization of user input (e.g. mouse, keyboard) for debugging package inputviz import ( @@ -6,18 +7,18 @@ import ( "sync" "time" - "github.com/oakmound/oak/v3/alg/floatgeom" - "github.com/oakmound/oak/v3/event" - "github.com/oakmound/oak/v3/mouse" - "github.com/oakmound/oak/v3/render" - "github.com/oakmound/oak/v3/scene" + "github.com/oakmound/oak/v4/alg/floatgeom" + "github.com/oakmound/oak/v4/event" + "github.com/oakmound/oak/v4/mouse" + "github.com/oakmound/oak/v4/render" + "github.com/oakmound/oak/v4/scene" ) type Mouse struct { Rect floatgeom.Rect2 BaseLayer int - event.CID + event.CallerID ctx *scene.Context rs map[mouse.Button]*render.Switch @@ -27,16 +28,17 @@ type Mouse struct { stateIncLock sync.RWMutex stateInc map[mouse.Button]int + + bindings []event.Binding } -func (m *Mouse) Init() event.CID { - m.CID = m.ctx.CallerMap.NextID(m) - return m.CID +func (m *Mouse) CID() event.CallerID { + return m.CallerID } func (m *Mouse) RenderAndListen(ctx *scene.Context, layer int) error { m.ctx = ctx - m.Init() + m.CallerID = ctx.Register(m) if m.Rect.W() == 0 || m.Rect.H() == 0 { m.Rect.Max = m.Rect.Min.Add(floatgeom.Point2{60, 100}) @@ -98,24 +100,21 @@ func (m *Mouse) RenderAndListen(ctx *scene.Context, layer int) error { ctx.DrawStack.Draw(m.posText, m.BaseLayer, layer+2) } - m.Bind(mouse.Press, mouse.Binding(func(id event.CID, ev *mouse.Event) int { - m, _ := m.ctx.CallerMap.GetEntity(id).(*Mouse) + b1 := event.Bind(ctx, mouse.Press, m, func(m *Mouse, ev *mouse.Event) event.Response { m.rs[ev.Button].Set("pressed") m.stateIncLock.Lock() m.stateInc[ev.Button]++ m.stateIncLock.Unlock() return 0 - })) - m.Bind(mouse.Release, mouse.Binding(func(id event.CID, ev *mouse.Event) int { - m, _ := m.ctx.CallerMap.GetEntity(id).(*Mouse) + }) + b2 := event.Bind(ctx, mouse.Release, m, func(m *Mouse, ev *mouse.Event) event.Response { m.rs[ev.Button].Set("released") m.stateIncLock.Lock() m.stateInc[ev.Button]++ m.stateIncLock.Unlock() return 0 - })) - m.Bind(mouse.ScrollDown, mouse.Binding(func(id event.CID, e *mouse.Event) int { - m, _ := m.ctx.CallerMap.GetEntity(id).(*Mouse) + }) + b3 := event.Bind(ctx, mouse.ScrollDown, m, func(m *Mouse, ev *mouse.Event) event.Response { m.rs[mouse.ButtonMiddle].Set("scrolldown") m.stateIncLock.Lock() m.stateInc[mouse.ButtonMiddle]++ @@ -129,9 +128,8 @@ func (m *Mouse) RenderAndListen(ctx *scene.Context, layer int) error { m.stateIncLock.Unlock() }) return 0 - })) - m.Bind(mouse.ScrollUp, mouse.Binding(func(id event.CID, e *mouse.Event) int { - m, _ := m.ctx.CallerMap.GetEntity(id).(*Mouse) + }) + b4 := event.Bind(ctx, mouse.ScrollUp, m, func(m *Mouse, ev *mouse.Event) event.Response { m.rs[mouse.ButtonMiddle].Set("scrollup") m.stateIncLock.Lock() m.stateInc[mouse.ButtonMiddle]++ @@ -145,13 +143,12 @@ func (m *Mouse) RenderAndListen(ctx *scene.Context, layer int) error { m.stateIncLock.Unlock() }) return 0 - })) - m.Bind(mouse.Drag, mouse.Binding(func(id event.CID, e *mouse.Event) int { - m, _ := m.ctx.CallerMap.GetEntity(id).(*Mouse) - m.lastMousePos.Point2 = e.Point2 + }) + b5 := event.Bind(ctx, mouse.Drag, m, func(m *Mouse, ev *mouse.Event) event.Response { + m.lastMousePos.Point2 = ev.Point2 return 0 - })) - + }) + m.bindings = []event.Binding{b1, b2, b3, b4, b5} return nil } @@ -164,7 +161,10 @@ func (ps *posStringer) String() string { } func (m *Mouse) Destroy() { - m.UnbindAll() + // TODO: this is a lot of code to write to track and unbind all of an entity's bindings + for _, b := range m.bindings { + b.Unbind() + } for _, r := range m.rs { r.Undraw() } diff --git a/debugtools/mouse.go b/debugtools/mouse.go index 8aebcac2..eb8391ac 100644 --- a/debugtools/mouse.go +++ b/debugtools/mouse.go @@ -1,18 +1,18 @@ package debugtools import ( - "github.com/oakmound/oak/v3/dlog" - "github.com/oakmound/oak/v3/event" - "github.com/oakmound/oak/v3/mouse" - "github.com/oakmound/oak/v3/scene" + "github.com/oakmound/oak/v4/dlog" + "github.com/oakmound/oak/v4/event" + "github.com/oakmound/oak/v4/key" + "github.com/oakmound/oak/v4/mouse" + "github.com/oakmound/oak/v4/scene" ) // DebugMouseRelease will print the position and button pressed of the mouse when the mouse is released, if the given -// key is held down at the time. If no key is given, it will always be printed -func DebugMouseRelease(ctx *scene.Context, k string) { - ctx.EventHandler.GlobalBind(mouse.Release, func(_ event.CID, ev interface{}) int { - mev, _ := ev.(*mouse.Event) - if k == "" || ctx.KeyState.IsDown(k) { +// key is held down at the time. If 0 is given, it will always be printed +func DebugMouseRelease(ctx *scene.Context, k key.Code) { + event.GlobalBind(ctx, mouse.Release, func(mev *mouse.Event) event.Response { + if k == 0 || ctx.IsDown(k) { dlog.Info(mev) } return 0 diff --git a/debugtools/renderable.go b/debugtools/renderable.go index cdf18499..02371cd1 100644 --- a/debugtools/renderable.go +++ b/debugtools/renderable.go @@ -1,7 +1,7 @@ package debugtools import ( - "github.com/oakmound/oak/v3/render" + "github.com/oakmound/oak/v4/render" "golang.org/x/sync/syncmap" ) diff --git a/debugtools/tree.go b/debugtools/tree.go index 46a5a969..7340eeba 100644 --- a/debugtools/tree.go +++ b/debugtools/tree.go @@ -4,10 +4,10 @@ import ( "image/color" "image/draw" - "github.com/oakmound/oak/v3/render" - "github.com/oakmound/oak/v3/scene" + "github.com/oakmound/oak/v4/render" + "github.com/oakmound/oak/v4/scene" - "github.com/oakmound/oak/v3/collision" + "github.com/oakmound/oak/v4/collision" ) // NewRTree creates a wrapper around a tree that supports coloring the spaces @@ -46,7 +46,8 @@ type Rtree struct { // GetDims returns the total possible area to draw this on. func (r *Rtree) GetDims() (int, int) { - return r.Context.Window.Width(), r.Context.Window.Height() + bds := r.Context.Window.Bounds() + return bds.X(), bds.Y() } // Draw will draw the collision outlines @@ -55,12 +56,13 @@ func (r *Rtree) Draw(buff draw.Image, xOff, yOff float64) { return } vp := r.Context.Window.Viewport() + bds := r.Context.Window.Bounds() // Get all spaces on screen screen := collision.NewUnassignedSpace( float64(vp.X()), float64(vp.Y()), - float64(r.Context.Window.Width()+vp.X()), - float64(r.Context.Window.Height()+vp.Y())) + float64(bds.X()+vp.X()), + float64(bds.Y()+vp.Y())) hits := r.Tree.Hits(screen) // Draw spaces that are on screen (as outlines) for _, h := range hits { diff --git a/default.go b/default.go index 75384bbe..4b14c2f0 100644 --- a/default.go +++ b/default.go @@ -5,10 +5,11 @@ import ( "sync" "time" - "github.com/oakmound/oak/v3/alg/intgeom" - "github.com/oakmound/oak/v3/event" - "github.com/oakmound/oak/v3/render" - "github.com/oakmound/oak/v3/scene" + "github.com/oakmound/oak/v4/alg/intgeom" + "github.com/oakmound/oak/v4/event" + "github.com/oakmound/oak/v4/key" + "github.com/oakmound/oak/v4/render" + "github.com/oakmound/oak/v4/scene" ) var defaultWindow *Window @@ -37,27 +38,15 @@ func AddScene(name string, sc scene.Scene) error { } // IsDown calls IsDown on the default window. -func IsDown(key string) bool { +func IsDown(k key.Code) bool { initDefaultWindow() - return defaultWindow.IsDown(key) + return defaultWindow.IsDown(k) } // IsHeld calls IsHeld on the default window. -func IsHeld(key string) (bool, time.Duration) { +func IsHeld(k key.Code) (bool, time.Duration) { initDefaultWindow() - return defaultWindow.IsHeld(key) -} - -// SetUp calls SetUp on the default window. -func SetUp(key string) { - initDefaultWindow() - defaultWindow.SetUp(key) -} - -// SetDown calls SetDown on the default window. -func SetDown(key string) { - initDefaultWindow() - defaultWindow.SetDown(key) + return defaultWindow.IsHeld(k) } // SetViewportBounds calls SetViewportBounds on the default window. @@ -66,22 +55,16 @@ func SetViewportBounds(rect intgeom.Rect2) { defaultWindow.SetViewportBounds(rect) } -// ShiftScreen calls ShiftScreen on the default window. -func ShiftScreen(x, y int) { +// ShiftViewport calls ShiftViewport on the default window. +func ShiftViewport(pt intgeom.Point2) { initDefaultWindow() - defaultWindow.ShiftScreen(x, y) + defaultWindow.ShiftViewport(pt) } -// SetScreen calls SetScreen on the default window. -func SetScreen(x, y int) { +// SetViewport calls SetViewport on the default window. +func SetViewport(pt intgeom.Point2) { initDefaultWindow() - defaultWindow.SetScreen(x, y) -} - -// MoveWindow calls MoveWindow on the default window. -func MoveWindow(x, y, w, h int) error { - initDefaultWindow() - return defaultWindow.MoveWindow(x, y, w, h) + defaultWindow.SetViewport(pt) } // UpdateViewSize calls UpdateViewSize on the default window. @@ -90,42 +73,6 @@ func UpdateViewSize(w, h int) error { return defaultWindow.UpdateViewSize(w, h) } -// SetFullScreen calls SetFullScreen on the default window. -func SetFullScreen(fs bool) error { - initDefaultWindow() - return defaultWindow.SetFullScreen(fs) -} - -// SetBorderless calls SetBorderless on the default window. -func SetBorderless(bs bool) error { - initDefaultWindow() - return defaultWindow.SetBorderless(bs) -} - -// SetTopMost calls SetTopMost on the default window. -func SetTopMost(on bool) error { - initDefaultWindow() - return defaultWindow.SetTopMost(on) -} - -// SetTitle calls SetTitle on the default window. -func SetTitle(title string) error { - initDefaultWindow() - return defaultWindow.SetTitle(title) -} - -// SetTrayIcon calls SetTrayIcon on the default window. -func SetTrayIcon(icon string) error { - initDefaultWindow() - return defaultWindow.SetTrayIcon(icon) -} - -// ShowNotification calls ShowNotification on the default window. -func ShowNotification(title, msg string, icon bool) error { - initDefaultWindow() - return defaultWindow.ShowNotification(title, msg, icon) -} - // ScreenShot calls ScreenShot on the default window. func ScreenShot() *image.RGBA { initDefaultWindow() @@ -150,32 +97,8 @@ func SetColorBackground(img image.Image) { defaultWindow.SetColorBackground(img) } -// GetBackgroundImage calls GetBackgroundImage on the default window. -func GetBackgroundImage() image.Image { - initDefaultWindow() - return defaultWindow.GetBackgroundImage() -} - -// Width calls Width on the default window. -func Width() int { - initDefaultWindow() - return defaultWindow.Width() -} - -// Height calls Height on the default window. -func Height() int { - initDefaultWindow() - return defaultWindow.Height() -} - -// HideCursor calls HideCursor on the default window. -func HideCursor() error { - initDefaultWindow() - return defaultWindow.HideCursor() -} - -// GetCursorPosition calls GetCursorPosition on the default window. -func GetCursorPosition() (x, y float64, err error) { +// Bounds returns the default window's boundary. +func Bounds() intgeom.Point2 { initDefaultWindow() - return defaultWindow.GetCursorPosition() + return defaultWindow.Bounds() } diff --git a/default_desktop.go b/default_desktop.go new file mode 100644 index 00000000..d822ec8a --- /dev/null +++ b/default_desktop.go @@ -0,0 +1,59 @@ +//go:build (windows || linux || osx) && !js && !android && !nooswindow +// +build windows linux osx +// +build !js +// +build !android +// +build !nooswindow + +package oak + +import ( + "image" +) + +// MoveWindow calls MoveWindow on the default window. +func MoveWindow(x, y, w, h int) error { + initDefaultWindow() + return defaultWindow.MoveWindow(x, y, w, h) +} + +// SetFullScreen calls SetFullScreen on the default window. +func SetFullScreen(fs bool) error { + initDefaultWindow() + return defaultWindow.SetFullScreen(fs) +} + +// SetBorderless calls SetBorderless on the default window. +func SetBorderless(bs bool) error { + initDefaultWindow() + return defaultWindow.SetBorderless(bs) +} + +// SetTopMost calls SetTopMost on the default window. +func SetTopMost(on bool) error { + initDefaultWindow() + return defaultWindow.SetTopMost(on) +} + +// SetTitle calls SetTitle on the default window. +func SetTitle(title string) error { + initDefaultWindow() + return defaultWindow.SetTitle(title) +} + +// SetIcon calls SetIcon on the default window. +func SetIcon(icon image.Image) error { + initDefaultWindow() + return defaultWindow.SetIcon(icon) +} + +// HideCursor calls HideCursor on the default window. +func HideCursor() error { + initDefaultWindow() + return defaultWindow.HideCursor() +} + +// GetCursorPosition calls GetCursorPosition on the default window. +func GetCursorPosition() (x, y float64) { + initDefaultWindow() + return defaultWindow.GetCursorPosition() +} diff --git a/default_test.go b/default_test.go new file mode 100644 index 00000000..00a9feb4 --- /dev/null +++ b/default_test.go @@ -0,0 +1,32 @@ +package oak + +import ( + "testing" + + "github.com/oakmound/oak/v4/alg/intgeom" + "github.com/oakmound/oak/v4/key" + "github.com/oakmound/oak/v4/render" + "github.com/oakmound/oak/v4/scene" +) + +func TestDefaultFunctions(t *testing.T) { + t.Run("SuperficialCoverage", func(t *testing.T) { + IsDown(key.A) + IsHeld(key.A) + AddScene("test", scene.Scene{ + Start: func(ctx *scene.Context) { + ScreenShot() + ctx.Window.Quit() + }, + }) + SetViewportBounds(intgeom.NewRect2(0, 0, 1, 1)) + SetViewport(intgeom.Point2{}) + ShiftViewport(intgeom.Point2{}) + UpdateViewSize(10, 10) + Bounds() + SetLoadingRenderable(render.EmptyRenderable()) + SetColorBackground(nil) + SetBackground(render.EmptyRenderable()) + Init("test") + }) +} diff --git a/dlog/default.go b/dlog/default.go index e3e2a666..bd042a1e 100644 --- a/dlog/default.go +++ b/dlog/default.go @@ -10,7 +10,7 @@ import ( "strings" "sync" - "github.com/oakmound/oak/v3/oakerr" + "github.com/oakmound/oak/v4/oakerr" ) var ( @@ -109,7 +109,9 @@ func (l *logger) SetFilter(filter func(string) bool) { // will be printed. func (l *logger) SetLogLevel(level Level) error { if level < NONE || level > VERBOSE { - return oakerr.InvalidInput{} + return oakerr.InvalidInput{ + InputName: "level", + } } l.debugLevel = level return nil diff --git a/dlog/default_test.go b/dlog/default_test.go index 8d938658..8ad57f21 100644 --- a/dlog/default_test.go +++ b/dlog/default_test.go @@ -5,7 +5,7 @@ import ( "strings" "testing" - "github.com/oakmound/oak/v3/dlog" + "github.com/oakmound/oak/v4/dlog" ) func TestLogger(t *testing.T) { diff --git a/dlog/dlog_test.go b/dlog/dlog_test.go index 4ca38f1a..5791d1ff 100644 --- a/dlog/dlog_test.go +++ b/dlog/dlog_test.go @@ -6,7 +6,7 @@ import ( "os" "testing" - "github.com/oakmound/oak/v3/dlog" + "github.com/oakmound/oak/v4/dlog" ) func TestErrorCheck(t *testing.T) { diff --git a/dlog/levels.go b/dlog/levels.go index 01559a89..dde2c489 100644 --- a/dlog/levels.go +++ b/dlog/levels.go @@ -3,7 +3,7 @@ package dlog import ( "strings" - "github.com/oakmound/oak/v3/oakerr" + "github.com/oakmound/oak/v4/oakerr" ) // Level represents the levels a debug message can have diff --git a/dlog/levels_test.go b/dlog/levels_test.go index bd3cb585..95923ecf 100644 --- a/dlog/levels_test.go +++ b/dlog/levels_test.go @@ -3,7 +3,7 @@ package dlog_test import ( "testing" - "github.com/oakmound/oak/v3/dlog" + "github.com/oakmound/oak/v4/dlog" ) func TestLevelsString(t *testing.T) { diff --git a/dlog/strings.go b/dlog/strings.go index f0c19433..ee65f911 100644 --- a/dlog/strings.go +++ b/dlog/strings.go @@ -1,6 +1,6 @@ package dlog -import "github.com/oakmound/oak/v3/oakerr" +import "github.com/oakmound/oak/v4/oakerr" type logCode int diff --git a/doc.go b/doc.go deleted file mode 100644 index 572588d8..00000000 --- a/doc.go +++ /dev/null @@ -1,4 +0,0 @@ -// Package oak is a game engine. It provides scene control, control over windows -// and what is drawn to them, propagates regular events to evaluate game logic, -// and so on. -package oak diff --git a/doc_test.go b/doc_test.go deleted file mode 100644 index 4e03e9af..00000000 --- a/doc_test.go +++ /dev/null @@ -1,20 +0,0 @@ -package oak - -import ( - "image/color" - - "github.com/oakmound/oak/v3/entities" - "github.com/oakmound/oak/v3/render" - "github.com/oakmound/oak/v3/scene" -) - -// Use oak to display a scene with a single movable character -func Example() { - AddScene("basicScene", scene.Scene{Start: func(*scene.Context) { - char := entities.NewMoving(100, 100, 16, 32, - render.NewColorBox(16, 32, color.RGBA{255, 0, 0, 255}), - nil, 0, 0) - render.Draw(char.R) - }}) - Init("basicScene") -} diff --git a/drawLoop.go b/drawLoop.go index e251763d..23ff333e 100644 --- a/drawLoop.go +++ b/drawLoop.go @@ -57,8 +57,6 @@ func (w *Window) drawLoop() { loadingSelectUnlimited: for { select { - case <-w.ParentContext.Done(): - return case <-w.quitCh: return case <-w.drawCh: @@ -92,8 +90,6 @@ func (w *Window) drawLoop() { loadingSelect: for { select { - case <-w.ParentContext.Done(): - return case <-w.quitCh: return case <-w.drawCh: @@ -115,18 +111,18 @@ func (w *Window) drawLoop() { } func (w *Window) publish() { - w.prePublish(w, w.windowTextures[w.bufferIdx]) + w.prePublish(w.winBuffers[w.bufferIdx].RGBA()) w.windowTextures[w.bufferIdx].Upload(zeroPoint, w.winBuffers[w.bufferIdx], w.winBuffers[w.bufferIdx].Bounds()) - w.windowControl.Scale(w.windowRect, w.windowTextures[w.bufferIdx], w.windowTextures[w.bufferIdx].Bounds(), draw.Src) - w.windowControl.Publish() + w.Window.Scale(w.windowRect, w.windowTextures[w.bufferIdx], w.windowTextures[w.bufferIdx].Bounds(), draw.Src) + w.Window.Publish() // every frame, swap buffers. This enables drivers which might hold on to the rgba buffers we publish as if they // were immutable. w.bufferIdx = (w.bufferIdx + 1) % bufferCount } -// DoBetweenDraws will execute the given function in-between draw frames +// DoBetweenDraws will execute the given function in-between draw frames. It will prevent draws from happening until +// the provided function has terminated. DoBetweenDraws will block until the provided function is called within the +// draw loop's schedule, but will not wait for that function itself to terminate. func (w *Window) DoBetweenDraws(f func()) { - go func() { - w.betweenDrawCh <- f - }() + w.betweenDrawCh <- f } diff --git a/driver.go b/driver.go index 87f04f3c..94ff3afa 100644 --- a/driver.go +++ b/driver.go @@ -1,7 +1,7 @@ package oak import ( - "github.com/oakmound/oak/v3/shiny/screen" + "github.com/oakmound/oak/v4/shiny/screen" ) // A Driver is a function which can take in our lifecycle function diff --git a/entities/doc.go b/entities/doc.go index a56f92df..23d6bc46 100644 --- a/entities/doc.go +++ b/entities/doc.go @@ -1,2 +1,2 @@ -// Package entities stores common, useful object and entity combinations. +// Package entities provides common entity constructor functions package entities diff --git a/entities/doodad.go b/entities/doodad.go deleted file mode 100644 index 47c4beff..00000000 --- a/entities/doodad.go +++ /dev/null @@ -1,79 +0,0 @@ -package entities - -import ( - "github.com/oakmound/oak/v3/event" - "github.com/oakmound/oak/v3/render" -) - -// A Doodad is an entity composed of a position, a renderable, and a CallerID. -type Doodad struct { - Point - event.CID - R render.Renderable -} - -// NewDoodad returns a new doodad that is not drawn but is initialized. -// Passing a CID of 0 will initialize the entity as a Doodad. Passing -// any other CID will assume that the struct containing this doodad has -// already been initialized to the passed in CID. -// This applies to ALL NewX functions in entities which take in a CID. -func NewDoodad(x, y float64, r render.Renderable, cid event.CID) *Doodad { - if r != nil { - r.SetPos(x, y) - } - d := Doodad{} - d.Point = *NewPoint(x, y) - d.R = r - d.CID = cid.Parse(&d) - return &d -} - -// Init satisfies event.Entity -func (d *Doodad) Init() event.CID { - d.CID = event.NextID(d) - return d.CID -} - -// GetID returns this Doodad's CID -// Consider: are these getters needed? -func (d *Doodad) GetID() event.CID { - return d.CID -} - -// GetRenderable returns this Doodad's Renderable -func (d *Doodad) GetRenderable() render.Renderable { - return d.R -} - -// SetRenderable sets this Doodad's renderable, drawing it. -// Todo:this automatic drawing doesn't really work with our -// two tiers of draw layers -func (d *Doodad) SetRenderable(r render.Renderable) { - if d.R != nil { - d.R.Undraw() - } - d.R = r - render.Draw(d.R, d.R.GetLayer()) -} - -// Destroy cleans up the events, renderable and -// entity mapping for this Doodad -func (d *Doodad) Destroy() { - if d.R != nil { - d.R.Undraw() - } - d.CID.UnbindAll() - event.DestroyEntity(d.CID) -} - -// Overwrites - -// SetPos both Sets logical position and renderable position -// The need for this sort of function is lessened with the introduction -// of vector attachement. -func (d *Doodad) SetPos(x, y float64) { - d.SetLogicPos(x, y) - if d.R != nil { - d.R.SetPos(x, y) - } -} diff --git a/entities/entity.go b/entities/entity.go new file mode 100644 index 00000000..02e14106 --- /dev/null +++ b/entities/entity.go @@ -0,0 +1,273 @@ +package entities + +import ( + "image/color" + + "github.com/oakmound/oak/v4/alg/floatgeom" + "github.com/oakmound/oak/v4/collision" + "github.com/oakmound/oak/v4/dlog" + "github.com/oakmound/oak/v4/event" + "github.com/oakmound/oak/v4/render" + "github.com/oakmound/oak/v4/render/mod" + "github.com/oakmound/oak/v4/scene" +) + +type Generator struct { + Position floatgeom.Point2 + Dimensions floatgeom.Point2 + Speed floatgeom.Point2 + + Parent event.Caller + + Color color.Color + Renderable render.Renderable + + Mod mod.Mod + + Label collision.Label + + DrawLayers []int + + UseMouseTree bool + WithoutCollision bool + + Children [][]Option +} + +func And(opts ...Option) Option { + return func(g Generator) Generator { + for _, o := range opts { + g = o(g) + } + return g + } +} + +func WithChild(opts ...Option) Option { + return func(s Generator) Generator { + s.Children = append(s.Children, opts) + return s + } +} + +func WithRect(v floatgeom.Rect2) Option { + return func(s Generator) Generator { + s.Position = v.Min + s.Dimensions = v.Max.Sub(v.Min) + return s + } +} + +func WithOffset(p floatgeom.Point2) Option { + return func(g Generator) Generator { + g.Position = g.Position.Add(p) + return g + } +} + +var defaultGenerator = Generator{ + Dimensions: floatgeom.Point2{1, 1}, + DrawLayers: []int{0}, +} + +type Entity struct { + event.CallerID + + ctx *scene.Context + + Rect floatgeom.Rect2 + Speed floatgeom.Point2 + Delta floatgeom.Point2 + + Renderable render.Renderable + + collision.Phase + + Space *collision.Space + Tree *collision.Tree + + metadata map[string]string + + Children []*Entity +} + +func (e Entity) CID() event.CallerID { + return e.CallerID.CID() +} + +func (e Entity) X() float64 { + return e.Rect.Min.X() +} +func (e Entity) Y() float64 { + return e.Rect.Min.Y() +} +func (e Entity) W() float64 { + return e.Rect.W() +} +func (e Entity) H() float64 { + return e.Rect.H() +} +func (e Entity) Top() float64 { + return e.Y() +} +func (e Entity) Bottom() float64 { + return e.Y() + e.H() +} +func (e Entity) Left() float64 { + return e.X() +} +func (e Entity) Right() float64 { + return e.X() + e.W() +} + +func (e *Entity) ShiftDelta() { + e.Shift(e.Delta) +} + +func (e *Entity) Shift(delta floatgeom.Point2) { + // TODO: attachment? + // TODO: helper + e.Renderable.ShiftX(delta.X()) + e.Renderable.ShiftY(delta.Y()) + e.Rect = e.Rect.Shift(delta) + if e.Tree != nil { + e.Tree.UpdateSpace( + e.X(), e.Y(), e.W(), e.H(), e.Space, + ) + } +} + +func (e *Entity) SetX(x float64) { + e.ShiftX(x - e.X()) +} + +func (e *Entity) SetY(y float64) { + e.ShiftY(y - e.Y()) +} + +func (e *Entity) ShiftX(x float64) { + e.Renderable.ShiftX(x) + e.Rect = e.Rect.Shift(floatgeom.Point2{x, 0}) + if e.Tree != nil { + e.Tree.UpdateSpace( + e.X(), e.Y(), e.W(), e.H(), e.Space, + ) + } +} + +func (e *Entity) ShiftY(y float64) { + e.Renderable.ShiftY(y) + e.Rect = e.Rect.Shift(floatgeom.Point2{0, y}) + if e.Tree != nil { + e.Tree.UpdateSpace( + e.X(), e.Y(), e.W(), e.H(), e.Space, + ) + } +} + +func (e *Entity) SetPos(p floatgeom.Point2) { + w, h := e.W(), e.H() + e.Rect = floatgeom.NewRect2WH(p.X(), p.Y(), w, h) + e.Renderable.SetPos(p.X(), p.Y()) + if e.Tree != nil { + e.Tree.UpdateSpace( + e.X(), e.Y(), e.W(), e.H(), e.Space, + ) + } +} + +// TODO: take a point, not floats +func (e *Entity) ShiftPos(x, y float64) { + p := e.Rect.Min + e.SetPos(p.Add(floatgeom.Point2{x, y})) +} + +func (e *Entity) HitLabel(label collision.Label) *collision.Space { + return e.Tree.HitLabel(e.Space, label) +} + +func (e *Entity) Destroy() { + e.Renderable.Undraw() + e.Tree.Remove(e.Space) + e.ctx.UnbindAllFrom(e.CallerID) +} + +// SetMetadata sets the metadata for some key to some value. Empty value strings +// will not be stored. +func (e *Entity) SetMetadata(k, v string) { + if v == "" { + delete(e.metadata, k) + } else { + e.metadata[k] = v + } +} + +// Metadata accesses the value, and whether it existed, for a given metadata key +func (e *Entity) Metadata(k string) (v string, ok bool) { + v, ok = e.metadata[k] + return v, ok +} + +func New(ctx *scene.Context, opts ...Option) *Entity { + g := defaultGenerator + for _, o := range opts { + g = o(g) + } + + children := make([]*Entity, len(g.Children)) + for i, childOpts := range g.Children { + childOpts = append(childOpts, WithOffset(g.Position)) + children[i] = New(ctx, childOpts...) + } + + e := &Entity{ + ctx: ctx, + Rect: floatgeom.NewRect2WH( + g.Position[0], + g.Position[1], + g.Dimensions[0], + g.Dimensions[1], + ), + Renderable: g.Renderable, + Speed: g.Speed, + Children: children, + } + + if g.Renderable == nil && g.Color != nil { + e.Renderable = render.NewColorBox(int(e.W()), int(e.H()), g.Color) + } + + if m, isMod := e.Renderable.(render.Modifiable); g.Mod != nil && isMod { + e.Renderable = m.Modify(g.Mod) + } + + e.Renderable.SetPos(e.X(), e.Y()) + + if g.Parent == nil { + cid := ctx.CallerMap.Register(e) + e.CallerID = cid + } else { + e.CallerID = g.Parent.CID() + if e.CallerID == 0 { + dlog.Error("entity created with uninitialized parent caller ID") + } + } + + if !g.WithoutCollision { + e.Tree = ctx.CollisionTree + if g.UseMouseTree { + e.Tree = ctx.MouseTree + } + e.Space = collision.NewSpace( + e.X(), e.Y(), e.W(), e.H(), e.CallerID, + ) + e.Space.Label = g.Label + e.Tree.Add(e.Space) + } + + if len(g.DrawLayers) != 0 { + ctx.Draw(e.Renderable, g.DrawLayers...) + } + + return e +} diff --git a/entities/interactive.go b/entities/interactive.go deleted file mode 100644 index ffee9d0b..00000000 --- a/entities/interactive.go +++ /dev/null @@ -1,36 +0,0 @@ -package entities - -import ( - "github.com/oakmound/oak/v3/collision" - "github.com/oakmound/oak/v3/event" - "github.com/oakmound/oak/v3/physics" - "github.com/oakmound/oak/v3/render" -) - -// Interactive parallels Moving, but for Reactive instead of Solid -type Interactive struct { - Reactive - vMoving -} - -// NewInteractive returns a new Interactive -func NewInteractive(x, y, w, h float64, r render.Renderable, tree *collision.Tree, - cid event.CID, friction float64) *Interactive { - - i := Interactive{} - cid = cid.Parse(&i) - i.Reactive = *NewReactive(x, y, w, h, r, tree, cid) - i.vMoving = vMoving{ - Delta: physics.NewVector(0, 0), - Speed: physics.NewVector(0, 0), - Friction: friction, - } - return &i -} - -// Init satisfies event.Entity -func (iv *Interactive) Init() event.CID { - cID := event.NextID(iv) - iv.CID = cID - return cID -} diff --git a/entities/move.go b/entities/move.go new file mode 100644 index 00000000..05b71c2b --- /dev/null +++ b/entities/move.go @@ -0,0 +1,60 @@ +package entities + +import ( + "github.com/oakmound/oak/v4/alg/floatgeom" + "github.com/oakmound/oak/v4/alg/intgeom" + "github.com/oakmound/oak/v4/key" +) + +// WASD moves the given mover based on its speed as W,A,S, and D are pressed +func WASD(mvr *Entity) { + TopDown(mvr, key.W, key.S, key.A, key.D) +} + +// Arrows moves the given mover based on its speed as the arrow keys are pressed +func Arrows(mvr *Entity) { + TopDown(mvr, key.UpArrow, key.DownArrow, key.LeftArrow, key.RightAlt) +} + +// TopDown moves the given mover based on its speed as the given keys are pressed +func TopDown(mvr *Entity, up, down, left, right key.Code) { + mvr.Delta = floatgeom.Point2{} + if mvr.ctx.IsDown(up) { + mvr.Delta[1] -= mvr.Speed[1] + } + if mvr.ctx.IsDown(down) { + mvr.Delta[1] += mvr.Speed[1] + } + if mvr.ctx.IsDown(left) { + mvr.Delta[0] -= mvr.Speed[0] + } + if mvr.ctx.IsDown(right) { + mvr.Delta[0] += mvr.Speed[0] + } + mvr.ShiftDelta() +} + +// CenterScreenOn will cause the screen to center on the given mover, obeying +// viewport limits if they have been set previously +func CenterScreenOn(mvr *Entity) { + bds := mvr.ctx.Window.Bounds() + pos := intgeom.Point2{int(mvr.X()), int(mvr.Y())} + target := pos.Sub(bds).DivConst(2) + mvr.ctx.Window.SetViewport(target) +} + +// Limit restricts the movement of the mover to stay within a given rectangle +func Limit(mvr *Entity, rect floatgeom.Rect2) { + wf := mvr.W() + hf := mvr.H() + if mvr.X() < rect.Min.X() { + mvr.SetX(rect.Min.X()) + } else if mvr.X() > rect.Max.X()-wf { + mvr.SetX(rect.Max.X() - wf) + } + if mvr.Y() < rect.Min.Y() { + mvr.SetY(rect.Min.Y()) + } else if mvr.Y() > rect.Max.Y()-hf { + mvr.SetY(rect.Max.Y() - hf) + } +} diff --git a/entities/moving.go b/entities/moving.go deleted file mode 100644 index da37e3af..00000000 --- a/entities/moving.go +++ /dev/null @@ -1,72 +0,0 @@ -package entities - -import ( - "github.com/oakmound/oak/v3/collision" - "github.com/oakmound/oak/v3/event" - "github.com/oakmound/oak/v3/physics" - "github.com/oakmound/oak/v3/render" -) - -// A Moving is a Solid that also keeps track of a speed and a delta vector -type Moving struct { - Solid - vMoving -} - -// NewMoving returns a new Moving -func NewMoving(x, y, w, h float64, r render.Renderable, tree *collision.Tree, cid event.CID, friction float64) *Moving { - m := Moving{} - cid = cid.Parse(&m) - m.Solid = *NewSolid(x, y, w, h, r, tree, cid) - m.vMoving = vMoving{ - Delta: physics.NewVector(0, 0), - Speed: physics.NewVector(0, 0), - Friction: friction, - } - return &m -} - -// Init satisfies event.Entity -func (m *Moving) Init() event.CID { - m.CID = event.NextID(m) - return m.CID -} - -// ShiftVector probably shouldn't be on moving but it lets you -// ShiftPos by a given vector -func (m *Moving) ShiftVector(v physics.Vector) { - m.Solid.ShiftPos(v.X(), v.Y()) -} - -// ApplyFriction modifies a moving's delta by combining -// environmental friction with the moving's base friction -// and scaling down the delta by the combined result. -func (v *vMoving) ApplyFriction(outsideFriction float64) { - //Absolute friction is 1 - frictionScaler := 1 - (v.Friction * outsideFriction) - if frictionScaler > 1 { - frictionScaler = 1 - } else if frictionScaler < 0 { - frictionScaler = 0 - } - v.Delta.Scale(frictionScaler) - if v.Delta.Magnitude() < .01 { - v.Delta.Zero() - } -} - -type vMoving struct { - Delta physics.Vector - Speed physics.Vector - Friction float64 -} - -// GetDelta returns this moving's delta -func (v vMoving) GetDelta() physics.Vector { - return v.Delta -} - -// GetSpeed returns this moving's speed -func (v vMoving) GetSpeed() physics.Vector { - return v.Speed -} diff --git a/entities/opts_gen.go b/entities/opts_gen.go new file mode 100644 index 00000000..4be0dbfe --- /dev/null +++ b/entities/opts_gen.go @@ -0,0 +1,100 @@ +// Code generated by foptgen; DO NOT EDIT. + +package entities + +import ( + "image/color" + + "github.com/oakmound/oak/v4/alg/floatgeom" + "github.com/oakmound/oak/v4/collision" + "github.com/oakmound/oak/v4/event" + "github.com/oakmound/oak/v4/render" + "github.com/oakmound/oak/v4/render/mod" +) + +type Option func(Generator) Generator + +func WithPosition(v floatgeom.Point2) Option { + return func(s Generator) Generator { + s.Position = v + return s + } +} + +func WithDimensions(v floatgeom.Point2) Option { + return func(s Generator) Generator { + s.Dimensions = v + return s + } +} + +func WithSpeed(v floatgeom.Point2) Option { + return func(s Generator) Generator { + s.Speed = v + return s + } +} + +func WithParent(v event.Caller) Option { + return func(s Generator) Generator { + s.Parent = v + return s + } +} + +func WithColor(v color.Color) Option { + return func(s Generator) Generator { + s.Color = v + return s + } +} + +func WithRenderable(v render.Renderable) Option { + return func(s Generator) Generator { + s.Renderable = v + return s + } +} + +func WithMod(v mod.Mod) Option { + return func(s Generator) Generator { + s.Mod = v + return s + } +} + +func WithLabel(v collision.Label) Option { + return func(s Generator) Generator { + s.Label = v + return s + } +} + +func WithDrawLayers(v []int) Option { + return func(s Generator) Generator { + s.DrawLayers = v + return s + } +} + +func WithUseMouseTree(v bool) Option { + return func(s Generator) Generator { + s.UseMouseTree = v + return s + } +} + +func WithWithoutCollision(v bool) Option { + return func(s Generator) Generator { + s.WithoutCollision = v + return s + } +} + +func WithChildren(v [][]Option) Option { + return func(s Generator) Generator { + s.Children = v + return s + } +} + \ No newline at end of file diff --git a/entities/point.go b/entities/point.go deleted file mode 100644 index ec4a900e..00000000 --- a/entities/point.go +++ /dev/null @@ -1,42 +0,0 @@ -package entities - -import ( - "github.com/oakmound/oak/v3/physics" -) - -// A Point is a wrapper around a physics vector. -type Point struct { - physics.Vector -} - -// NewPoint returns a new point -func NewPoint(x, y float64) *Point { - return &Point{physics.NewVector(x, y)} -} - -// GetLogicPos returns the logical position of an entity. See SetLogicPos. -func (p *Point) GetLogicPos() (float64, float64) { - return p.X(), p.Y() -} - -// ShiftLogicPos shifts a point's underlying position by both x and y -func (p *Point) ShiftLogicPos(x, y float64) { - p.Vector.SetPos(p.X()+x, p.Y()+y) -} - -// SetLogicPos is an explicit declaration for setting just the logical -// position of an entity. On a Point there is no distinction as there is nothing -// but the logical position but this is important for other entity types -func (p *Point) SetLogicPos(x, y float64) { - p.Vector.SetPos(x, y) -} - -// DistanceTo returns the euclidean distance to (x,y) -func (p *Point) DistanceTo(x, y float64) float64 { - return p.Distance(physics.NewVector(x, y)) -} - -// DistanceToPoint returns the euclidean distance to p2.GetLogicPos() -func (p *Point) DistanceToPoint(p2 Point) float64 { - return p.Distance(p2.Vector) -} diff --git a/entities/reactive.go b/entities/reactive.go deleted file mode 100644 index 4acfa644..00000000 --- a/entities/reactive.go +++ /dev/null @@ -1,100 +0,0 @@ -package entities - -import ( - "github.com/oakmound/oak/v3/collision" - "github.com/oakmound/oak/v3/event" - "github.com/oakmound/oak/v3/render" -) - -// Reactive is parallel to Solid, but has a Reactive collision space instead of -// a regular collision space -type Reactive struct { - Doodad - W, H float64 - RSpace *collision.ReactiveSpace - Tree *collision.Tree -} - -// NewReactive returns a new Reactive struct. The added space will -// be added to the input tree, or DefTree if none is given. -func NewReactive(x, y, w, h float64, r render.Renderable, tree *collision.Tree, cid event.CID) *Reactive { - rct := Reactive{} - cid = cid.Parse(&rct) - rct.Doodad = *NewDoodad(x, y, r, cid) - rct.W = w - rct.H = h - rct.RSpace = collision.NewReactiveSpace(collision.NewSpace(x, y, w, h, cid), map[collision.Label]collision.OnHit{}) - if tree == nil { - tree = collision.DefaultTree - } - rct.RSpace.Tree = tree - rct.Tree = tree - rct.Tree.Add(rct.RSpace.Space) - return &rct -} - -// SetDim sets the dimensions of this reactive's space and it's logical dimensions -func (r *Reactive) SetDim(w, h float64) { - r.SetLogicDim(w, h) - r.RSpace.SetDim(w, h) -} - -// GetLogicDim returns this Reactive's width and height -// todo: move wh into their own struct to compose into solid and reactive -func (r *Reactive) GetLogicDim() (float64, float64) { - return r.W, r.H -} - -// SetLogicDim sets the logical width and height of this reactive -// without changing the real dimensions of its collision space -func (r *Reactive) SetLogicDim(w, h float64) { - r.W = w - r.H = h -} - -// SetSpace sets this reactive's collision space to the given reactive space, -// updating it's collision tree to include it. -func (r *Reactive) SetSpace(sp *collision.ReactiveSpace) { - r.Tree.Remove(r.RSpace.Space) - r.RSpace = sp - r.Tree.Add(r.RSpace.Space) -} - -// GetSpace returns this reactive's space underlying its RSpace -func (r *Reactive) GetSpace() *collision.Space { - return r.RSpace.Space -} - -// GetReactiveSpace returns this reactive's RSpace -func (r *Reactive) GetReactiveSpace() *collision.ReactiveSpace { - return r.RSpace -} - -// Overwrites - -// Init satisfies event.Entity -func (r *Reactive) Init() event.CID { - r.CID = event.NextID(r) - return r.CID -} - -// ShiftPos acts like SetPos if given r.X()+x, r.Y()+y -func (r *Reactive) ShiftPos(x, y float64) { - r.SetPos(r.X()+x, r.Y()+y) -} - -// SetPos sets this reactive's logical, renderable, and collision position to be x,y -func (r *Reactive) SetPos(x, y float64) { - r.SetLogicPos(x, y) - if r.R != nil { - r.R.SetPos(x, y) - } - r.Tree.UpdateSpace(r.X(), r.Y(), r.W, r.H, r.RSpace.Space) -} - -// Destroy destroys this reactive's doodad component and removes its space -// from it's collision tree -func (r *Reactive) Destroy() { - r.Tree.Remove(r.RSpace.Space) - r.Doodad.Destroy() -} diff --git a/entities/solid.go b/entities/solid.go deleted file mode 100644 index 01b7e0d2..00000000 --- a/entities/solid.go +++ /dev/null @@ -1,122 +0,0 @@ -package entities - -import ( - "github.com/oakmound/oak/v3/collision" - "github.com/oakmound/oak/v3/event" - "github.com/oakmound/oak/v3/render" -) - -// A Solid is a Doodad with a width, height, and collision space. -type Solid struct { - Doodad - W, H float64 - Space *collision.Space - Tree *collision.Tree -} - -// NewSolid returns an initialized Solid that is not drawn and whose space -// belongs to the given collision tree. If nil is given as the tree, it will -// belong to collision.DefTree -func NewSolid(x, y, w, h float64, r render.Renderable, tree *collision.Tree, cid event.CID) *Solid { - s := Solid{} - cid = cid.Parse(&s) - s.Doodad = *NewDoodad(x, y, r, cid) - s.W = w - s.H = h - if tree == nil { - tree = collision.DefaultTree - } - s.Tree = tree - s.Space = collision.NewSpace(x, y, w, h, cid) - s.Tree.Add(s.Space) - return &s -} - -// SetDim sets the logical dimensions of the solid and the real -// dimensions on the solid's space -func (s *Solid) SetDim(w, h float64) { - s.SetLogicDim(w, h) - s.Space.SetDim(w, h) -} - -// GetLogicDim will return the width and height of the Solid -func (s *Solid) GetLogicDim() (float64, float64) { - return s.W, s.H -} - -// SetLogicDim sets the width and height of the solid -func (s *Solid) SetLogicDim(w, h float64) { - s.W = w - s.H = h -} - -// SetSpace assigns a solid a collision space and puts it in this Solid's Tree -func (s *Solid) SetSpace(sp *collision.Space) { - s.Tree.Remove(s.Space) - s.Space = sp - s.Tree.Add(s.Space) -} - -// GetSpace returns a solid's collision space -func (s *Solid) GetSpace() *collision.Space { - return s.Space -} - -// ShiftX moves a solid by x along the x axis -func (s *Solid) ShiftX(x float64) { - s.SetPos(s.X()+x, s.Y()) -} - -// ShiftY moves a solid by y along the y axis -func (s *Solid) ShiftY(y float64) { - s.SetPos(s.X(), s.Y()+y) -} - -// ShiftPos moves a solid by (x,y) -func (s *Solid) ShiftPos(x, y float64) { - s.SetPos(s.X()+x, s.Y()+y) -} - -// UpdateLabel will update it's label in this solid's -// collision tree. -func (s *Solid) UpdateLabel(classtype collision.Label) { - s.Tree.UpdateLabel(classtype, s.Space) -} - -// ShiftSpace will shift this solid's collision space -// by (x,y) -func (s *Solid) ShiftSpace(x, y float64) { - s.Tree.UpdateSpace(s.X()+x, s.Y()+y, s.W, s.H, s.Space) -} - -// HitLabel will return the first space that this solid -// collides with matching the given label that it finds, -// or nil if it finds nothing. -func (s *Solid) HitLabel(classtype collision.Label) *collision.Space { - return s.Tree.HitLabel(s.Space, classtype) -} - -// Overwrites - -// Init satisfies event.Entity -func (s *Solid) Init() event.CID { - s.CID = event.NextID(s) - return s.CID -} - -// SetPos sets the position of the collision space, the logical position, -// and the renderable position of the solid. -func (s *Solid) SetPos(x float64, y float64) { - s.SetLogicPos(x, y) - if s.R != nil { - s.R.SetPos(x, y) - } - s.Tree.UpdateSpace(s.X(), s.Y(), s.W, s.H, s.Space) -} - -// Destroy removes this solid's collision space from it's Tree -// and destroys the doodad portion of the solid. -func (s *Solid) Destroy() { - s.Doodad.Destroy() - s.Tree.Remove(s.Space) -} diff --git a/entities/x/btn/box.go b/entities/x/btn/box.go deleted file mode 100644 index 54b3be7b..00000000 --- a/entities/x/btn/box.go +++ /dev/null @@ -1,60 +0,0 @@ -package btn - -import ( - "github.com/oakmound/oak/v3/entities" - "github.com/oakmound/oak/v3/event" - "github.com/oakmound/oak/v3/mouse" - "github.com/oakmound/oak/v3/render" -) - -// Box is a basic implementation of btn -type Box struct { - entities.Solid - mouse.CollisionPhase - metadata map[string]string -} - -// NewBox creates a new Box -func NewBox(cid event.CID, x, y, w, h float64, r render.Renderable, layers ...int) *Box { - b := Box{} - cid = cid.Parse(&b) - b.Solid = *entities.NewSolid(x, y, w, h, r, mouse.DefaultTree, cid) - if b.R != nil && len(layers) > 0 { - render.Draw(b.R, layers...) - } - b.metadata = make(map[string]string) - return &b -} - -// Init intializes the Box -func (b *Box) Init() event.CID { - b.CID = event.NextID(b) - return b.CID -} - -// GetRenderable returns the box's renderable -func (b *Box) GetRenderable() render.Renderable { - return b.R -} - -// SetMetadata sets the metadata for some key to some value. Empty value strings -// will not be stored. -func (b *Box) SetMetadata(k, v string) { - if v == "" { - delete(b.metadata, k) - } else { - b.metadata[k] = v - } -} - -// Metadata accesses the value, and whether it existed, for a given metadata key -func (b *Box) Metadata(k string) (v string, ok bool) { - v, ok = b.metadata[k] - return v, ok -} - -func (b *Box) Destroy() { - b.UnbindAll() - b.R.Undraw() - mouse.Remove(b.GetSpace()) -} \ No newline at end of file diff --git a/entities/x/btn/btn.go b/entities/x/btn/btn.go deleted file mode 100644 index 85a8fbcb..00000000 --- a/entities/x/btn/btn.go +++ /dev/null @@ -1,18 +0,0 @@ -package btn - -import ( - "github.com/oakmound/oak/v3/collision" - "github.com/oakmound/oak/v3/event" - "github.com/oakmound/oak/v3/render" -) - -// Btn defines a button for use in the UI -type Btn interface { - event.Caller - render.Positional - GetRenderable() render.Renderable - GetSpace() *collision.Space - SetMetadata(string, string) - Metadata(string) (string, bool) - Destroy() -} diff --git a/entities/x/btn/button.go b/entities/x/btn/button.go index 132de378..5f8b34a2 100644 --- a/entities/x/btn/button.go +++ b/entities/x/btn/button.go @@ -3,16 +3,16 @@ package btn import ( "fmt" "image/color" - "strconv" - "strings" - "github.com/oakmound/oak/v3/collision" - "github.com/oakmound/oak/v3/dlog" - "github.com/oakmound/oak/v3/event" - "github.com/oakmound/oak/v3/mouse" - "github.com/oakmound/oak/v3/render" - "github.com/oakmound/oak/v3/render/mod" - "github.com/oakmound/oak/v3/shape" + "github.com/oakmound/oak/v4/alg/floatgeom" + "github.com/oakmound/oak/v4/collision" + "github.com/oakmound/oak/v4/entities" + "github.com/oakmound/oak/v4/event" + "github.com/oakmound/oak/v4/mouse" + "github.com/oakmound/oak/v4/render" + "github.com/oakmound/oak/v4/render/mod" + "github.com/oakmound/oak/v4/scene" + "github.com/oakmound/oak/v4/shape" ) // A Generator defines the variables used to create buttons from optional arguments @@ -28,19 +28,15 @@ type Generator struct { R1 render.Modifiable R2 render.Modifiable RS []render.Modifiable - Cid event.CID + Cid event.CallerID Font *render.Font Layers []int Text string TextPtr *string TextStringer fmt.Stringer Children []Generator - Bindings map[string][]event.Bindable + Bindings []func(ctx *scene.Context, caller *entities.Entity) event.Binding Trigger string - Toggle *bool - ListChoice *int - Group *Group - AllowRevert bool Shape shape.Shape Label collision.Label } @@ -61,57 +57,18 @@ func defGenerator() Generator { R1: nil, R2: nil, - Children: []Generator{}, - Cid: 0, - Font: nil, - Layers: []int{0}, - Text: "Button", - Bindings: make(map[string][]event.Bindable), - Trigger: "MouseClickOn", - - Toggle: nil, + Font: nil, + Layers: []int{0}, + Text: "", + Trigger: "MouseClickOn", } } // Generate creates a Button from a generator. -func (g Generator) Generate() Btn { - return g.generate(nil) -} - -func (g Generator) generate(parent *Generator) Btn { +func (g Generator) Generate(ctx *scene.Context) *entities.Entity { var box render.Modifiable // handle different renderable options that could be passed to the generator switch { - case g.Toggle != nil: - //Handles checks and other toggle situations - start := "on" - if !(*g.Toggle) { - start = "off" - } - if _, ok := g.R1.(*render.Reverting); !ok { - g.R1 = render.NewReverting(g.R1) - } - if _, ok := g.R2.(*render.Reverting); !ok { - g.R2 = render.NewReverting(g.R2) - } - box = render.NewSwitch(start, map[string]render.Modifiable{ - "on": g.R1, - "off": g.R2, - }) - g.Bindings["MouseClickOn"] = append(g.Bindings["MouseClickOn"], toggleFxn(g)) - case g.ListChoice != nil: - - start := "list" + strconv.Itoa(*g.ListChoice) - mp := make(map[string]render.Modifiable) - for i, r := range g.RS { - if _, ok := r.(*render.Reverting); !ok { - r = render.NewReverting(r) - } - mp["list"+strconv.Itoa(i)] = r - } - box = render.NewSwitch(start, mp) - - g.Bindings["MouseClickOn"] = append(g.Bindings["MouseClickOn"], listFxn(g)) case g.R != nil: box = g.R case g.ProgressFunc != nil: @@ -126,94 +83,48 @@ func (g Generator) generate(parent *Generator) Btn { } } - if g.AllowRevert { - box = render.NewReverting(box) + entOpts := []entities.Option{ + entities.WithRenderable(box), + entities.WithMod(g.Mod), + entities.WithRect(floatgeom.NewRect2WH(g.X, g.Y, g.W, g.H)), + entities.WithLabel(g.Label), + entities.WithDrawLayers(g.Layers), + entities.WithUseMouseTree(true), } - if g.Mod != nil { - box.Modify(g.Mod) - } font := g.Font if font == nil { font = render.DefaultFont() } - var btn Btn + childLayers := make([]int, len(g.Layers)) + copy(childLayers, g.Layers) + if len(childLayers) != 0 { + childLayers[len(childLayers)-1]++ + } if g.Text != "" { - txtbx := NewTextBox(g.Cid, g.X, g.Y, g.W, g.H, g.TxtX, g.TxtY, font, box, g.Layers...) - txtbx.SetString(g.Text) - txtbx.Space.Label = g.Label - btn = txtbx + entOpts = append(entOpts, entities.WithChild( + entities.WithRenderable(font.NewText(g.Text, g.TxtX, g.TxtY)), + entities.WithDrawLayers(childLayers), + )) } else if g.TextPtr != nil { - txtbx := NewTextBox(g.Cid, g.X, g.Y, g.W, g.H, g.TxtX, g.TxtY, font, box, g.Layers...) - txtbx.SetStringPtr(g.TextPtr) - txtbx.Space.Label = g.Label - btn = txtbx + entOpts = append(entOpts, entities.WithChild( + entities.WithRenderable(font.NewStrPtrText(g.TextPtr, g.TxtX, g.TxtY)), + entities.WithDrawLayers(childLayers), + )) } else if g.TextStringer != nil { - txtbx := NewTextBox(g.Cid, g.X, g.Y, g.W, g.H, g.TxtX, g.TxtY, font, box, g.Layers...) - txtbx.SetStringer(g.TextStringer) - txtbx.Space.Label = g.Label - btn = txtbx - } else { - bx := NewBox(g.Cid, g.X, g.Y, g.W, g.H, box, g.Layers...) - bx.Space.Label = g.Label - btn = bx + entOpts = append(entOpts, entities.WithChild( + entities.WithRenderable(font.NewStringerText(g.TextStringer, g.TxtX, g.TxtY)), + entities.WithDrawLayers(childLayers), + )) } - // Update underlying mousecollision binding to only respect clicks in the shape. - // If a finer control is needed then it may make sense to use this as a starting off point - // instead of expanding this section. - if g.Shape != nil { + btn := entities.New(ctx, entOpts...) - // extract keys prior to loop as the map will be permuted by the following operations - keys := make([]string, 0, len(g.Bindings)) - for k := range g.Bindings { - // We only really care about mouse events. - // In some ways this is dangerous of an implementer has defined events that start with mouse... - // but in that case they might not use g.Shape anyways. - if !strings.HasPrefix(k, "Mouse") { - continue - } - keys = append(keys, k) - } - for _, k := range keys { - curBind := g.Bindings[k] - if curBind == nil { - continue - } - // This could cause issues with name collisions but its unlikely and documentation should help make it even more unlikely. - filteredK := "Filtered" + k - g.Bindings[filteredK] = g.Bindings[k] - g.Bindings[k] = []event.Bindable{ - func(id event.CID, button interface{}) int { - btn := id.E().(Btn) - mEvent, ok := button.(*mouse.Event) - // If the passed event is not a mouse event dont filter on location. - // Main current use case is for nil events passed via simulated clicks. - if !ok { - btn.Trigger(filteredK, button) - } - bSpace := btn.GetSpace().Bounds() - if g.Shape.In(int(mEvent.X()-bSpace.Min.X()), int(mEvent.Y()-bSpace.Min.Y()), int(bSpace.W()), int(bSpace.H())) { - btn.Trigger(filteredK, mEvent) - } - return 0 - }, - } - } + for _, binding := range g.Bindings { + binding(ctx, btn) } - for k, v := range g.Bindings { - for _, b := range v { - btn.Bind(k, b) - } - } - - err := mouse.PhaseCollision(btn.GetSpace()) - dlog.ErrorCheck(err) - - if g.Group != nil { - g.Group.members = append(g.Group.members, btn) - } + mouse.PhaseCollision(btn.Space, ctx.Handler) return btn } @@ -222,7 +133,7 @@ func (g Generator) generate(parent *Generator) Btn { type Option func(Generator) Generator // New creates a button with the given options and defaults for all variables not set. -func New(opts ...Option) Btn { +func New(ctx *scene.Context, opts ...Option) *entities.Entity { g := defGenerator() for _, opt := range opts { if opt == nil { @@ -230,66 +141,5 @@ func New(opts ...Option) Btn { } g = opt(g) } - return g.Generate() -} - -type switcher interface { - Get() string - Set(string) error -} - -// toggleFxn sets up the mouseclick binding for toggle buttons created for goreport cyclo decrease -func toggleFxn(g Generator) func(id event.CID, nothing interface{}) int { - return func(id event.CID, nothing interface{}) int { - btn := event.GetEntity(id).(Btn) - if btn.GetRenderable().(switcher).Get() == "on" { - if g.Group != nil && g.Group.active == btn { - g.Group.active = nil - } - btn.GetRenderable().(switcher).Set("off") - } else { - // We can pull this out to separate binding if group != nil - if g.Group != nil { - g.Group.active = btn - for _, b := range g.Group.members { - if b.GetRenderable().(switcher).Get() == "on" { - b.Trigger("MouseClickOn", nil) - } - } - } - btn.GetRenderable().(switcher).Set("on") - - } - *g.Toggle = !*g.Toggle - - return 0 - } -} - -// listFxn sets up the mouseclick binding for list buttons created for goreport cyclo reduction -func listFxn(g Generator) func(id event.CID, button interface{}) int { - return func(id event.CID, button interface{}) int { - btn := event.GetEntity(id).(Btn) - i := *g.ListChoice - mEvent := button.(*mouse.Event) - - if mEvent.Button == mouse.ButtonLeft { - i++ - if i == len(g.RS) { - i = 0 - } - - } else if mEvent.Button == mouse.ButtonRight { - i-- - if i < 0 { - i += len(g.RS) - } - } - - btn.GetRenderable().(*render.Switch).Set("list" + strconv.Itoa(i)) - - *g.ListChoice = i - - return 0 - } + return g.Generate(ctx) } diff --git a/entities/x/btn/grid/grid.go b/entities/x/btn/grid/grid.go index fdf7cba0..1a83c008 100644 --- a/entities/x/btn/grid/grid.go +++ b/entities/x/btn/grid/grid.go @@ -1,9 +1,14 @@ +// Package grid provides structures for aligning grids of buttons package grid -import "github.com/oakmound/oak/v3/entities/x/btn" +import ( + "github.com/oakmound/oak/v4/entities" + "github.com/oakmound/oak/v4/entities/x/btn" + "github.com/oakmound/oak/v4/scene" +) -// A Grid is a 2D slice of buttons -type Grid [][]btn.Btn +// A Grid is a 2D slice of entities +type Grid [][]*entities.Entity // A Generator defines the variables used to create grids from optional arguments type Generator struct { @@ -28,12 +33,12 @@ var ( ) // Generate creates a Grid from a Generator -func (g *Generator) Generate() Grid { - grid := make([][]btn.Btn, len(g.Content)) +func (g *Generator) Generate(ctx *scene.Context) Grid { + grid := make([][]*entities.Entity, len(g.Content)) for x := 0; x < len(g.Content); x++ { - grid[x] = make([]btn.Btn, len(g.Content[x])) + grid[x] = make([]*entities.Entity, len(g.Content[x])) for y := 0; y < len(g.Content[x]); y++ { - grid[x][y] = btn.New( + grid[x][y] = btn.New(ctx, g.Defaults, g.Content[x][y], btn.Offset(float64(x)*g.XGap, float64(y)*g.YGap), @@ -44,7 +49,7 @@ func (g *Generator) Generate() Grid { } // New creates a grid of buttons from a set of options -func New(opts ...Option) Grid { +func New(ctx *scene.Context, opts ...Option) Grid { g := defaultGenerator for _, opt := range opts { if opt == nil { @@ -52,5 +57,5 @@ func New(opts ...Option) Grid { } g = opt(g) } - return g.Generate() + return g.Generate(ctx) } diff --git a/entities/x/btn/grid/option.go b/entities/x/btn/grid/option.go index 47b02609..d044f5f0 100644 --- a/entities/x/btn/grid/option.go +++ b/entities/x/btn/grid/option.go @@ -1,8 +1,8 @@ package grid import ( - "github.com/oakmound/oak/v3/dlog" - "github.com/oakmound/oak/v3/entities/x/btn" + "github.com/oakmound/oak/v4/dlog" + "github.com/oakmound/oak/v4/entities/x/btn" ) // An Option modifies a generator prior to grid generation diff --git a/entities/x/btn/group.go b/entities/x/btn/group.go deleted file mode 100644 index e8bfcc24..00000000 --- a/entities/x/btn/group.go +++ /dev/null @@ -1,20 +0,0 @@ -package btn - -// Group links several btns together -type Group struct { - members []Btn - active Btn -} - -// GetActive returns the active btn from the group -func (g *Group) GetActive() Btn { - return g.active -} - -// ToggleGroup sets the group that this button is linked with -func ToggleGroup(gr *Group) Option { - return func(g Generator) Generator { - g.Group = gr - return g - } -} diff --git a/entities/x/btn/option.go b/entities/x/btn/option.go index b5b1b3cd..97e8e440 100644 --- a/entities/x/btn/option.go +++ b/entities/x/btn/option.go @@ -3,13 +3,14 @@ package btn import ( "image/color" - "github.com/oakmound/oak/v3/collision" - "github.com/oakmound/oak/v3/mouse" - "github.com/oakmound/oak/v3/shape" - - "github.com/oakmound/oak/v3/event" - "github.com/oakmound/oak/v3/render" - "github.com/oakmound/oak/v3/render/mod" + "github.com/oakmound/oak/v4/collision" + "github.com/oakmound/oak/v4/entities" + "github.com/oakmound/oak/v4/mouse" + "github.com/oakmound/oak/v4/scene" + + "github.com/oakmound/oak/v4/event" + "github.com/oakmound/oak/v4/render" + "github.com/oakmound/oak/v4/render/mod" ) // And combines a variadic number of options @@ -64,7 +65,7 @@ func Offset(x, y float64) Option { } //CID sets the starting CID of the button to be generated -func CID(c event.CID) Option { +func CID(c event.CallerID) Option { return func(g Generator) Generator { g.Cid = c return g @@ -127,60 +128,23 @@ func Renderable(r render.Modifiable) Option { } } -// Toggle sets that the type of the button toggles between two -// modifiables when it is clicked. The boolean behind isChecked -// is updated according to the state of the button. -// Todo: the copies here should be optional -func Toggle(r1, r2 render.Modifiable, isChecked *bool) Option { - return func(g Generator) Generator { - g.R1 = r1.Copy() - g.R2 = r2.Copy() - g.Toggle = isChecked - return g - } -} - -// ToggleList sets the togglable choices for a button -func ToggleList(chosen *int, rs ...render.Modifiable) Option { - return func(g Generator) Generator { - g.ListChoice = chosen - g.RS = rs - return g - } -} - // Binding appends a function to be called when a specific event // is triggered. -func Binding(s string, bnd event.Bindable) Option { +func Binding[Payload any](ev event.EventID[Payload], bnd event.Bindable[*entities.Entity, Payload]) Option { return func(g Generator) Generator { - g.Bindings[s] = append(g.Bindings[s], bnd) + g.Bindings = append(g.Bindings, func(ctx *scene.Context, caller *entities.Entity) event.Binding { + // TODO: not default + return event.Bind(ctx, ev, caller, bnd) + }) return g } } // Click appends a function to be called when the button is clicked on. -func Click(bnd event.Bindable) Option { +func Click(bnd event.Bindable[*entities.Entity, *mouse.Event]) Option { return Binding(mouse.ClickOn, bnd) } -// AllowRevert wraps a button in a Reverting renderable, enabling phase changes -// through modifications and reversion -func AllowRevert() Option { - return func(g Generator) Generator { - g.AllowRevert = true - return g - } -} - -// Shape sets the underlying mouse collision to only be respected if in shape. -// If color is responsible for arendering then it will be formed to this shape as well. -func Shape(s shape.Shape) Option { - return func(g Generator) Generator { - g.Shape = s - return g - } -} - func Label(l collision.Label) Option { return func(g Generator) Generator { g.Label = l diff --git a/entities/x/btn/revert.go b/entities/x/btn/revert.go deleted file mode 100644 index 23ff7540..00000000 --- a/entities/x/btn/revert.go +++ /dev/null @@ -1,19 +0,0 @@ -package btn - -import "errors" - -type reverting interface { - Revert(n int) -} - -// Revert will check that the given button's renderable -// can have modifications reverted, then revert the last -// n modifications. -func Revert(b Btn, n int) error { - r, ok := b.GetRenderable().(reverting) - if !ok { - return errors.New("Button's renderable does not implement revert functionality") - } - r.Revert(n) - return nil -} diff --git a/entities/x/btn/text.go b/entities/x/btn/text.go deleted file mode 100644 index c882eff9..00000000 --- a/entities/x/btn/text.go +++ /dev/null @@ -1,30 +0,0 @@ -package btn - -import ( - "fmt" - - "github.com/oakmound/oak/v3/entities" - - "github.com/oakmound/oak/v3/render" -) - -// NewText creates some uitext -func NewText(f *render.Font, str string, x, y float64, layers ...int) *entities.Doodad { - d := entities.NewDoodad(x, y, f.NewText(str, x, y), 0) - render.Draw(d.R, layers...) - return d -} - -// NewIntText creates some uitext from an integer -func NewIntText(f *render.Font, str *int, x, y float64, layers ...int) *entities.Doodad { - d := entities.NewDoodad(x, y, f.NewIntText(str, x, y), 0) - render.Draw(d.R, layers...) - return d -} - -// NewRawText creates some uitext from a stringer -func NewRawText(f *render.Font, str fmt.Stringer, x, y float64, layers ...int) *entities.Doodad { - d := entities.NewDoodad(x, y, f.NewStringerText(str, x, y), 0) - render.Draw(d.R, layers...) - return d -} diff --git a/entities/x/btn/textBox.go b/entities/x/btn/textBox.go deleted file mode 100644 index cbe4f442..00000000 --- a/entities/x/btn/textBox.go +++ /dev/null @@ -1,90 +0,0 @@ -package btn - -import ( - "github.com/oakmound/oak/v3/collision" - "github.com/oakmound/oak/v3/event" - "github.com/oakmound/oak/v3/mouse" - "github.com/oakmound/oak/v3/render" -) - -// TextBox is a Box with an associated text element -type TextBox struct { - Box - *render.Text -} - -// Init creates the CID -func (b *TextBox) Init() event.CID { - b.CID = event.NextID(b) - return b.CID -} - -// NewTextBox creates a textbox -func NewTextBox(cid event.CID, x, y, w, h, txtX, txtY float64, - f *render.Font, r render.Renderable, layers ...int) *TextBox { - - if f == nil { - f = render.DefaultFont() - } - - b := new(TextBox) - - cid = cid.Parse(b) - - b.Box = *NewBox(cid, x, y, w, h, r, layers...) - b.Text = f.NewText("Init", 0, 0) - b.Text.Attach(b.Box.Vector, txtX, txtY) - - // We dont want to modify the input's layers but we do want the text to show up on top of the base renderable. - txtLayers := make([]int, len(layers)) - copy(txtLayers, layers) - txtLayers[len(txtLayers)-1]++ - render.Draw(b.Text, txtLayers...) - return b -} - -// Y pulls the y of the composed Box (disambiguation with the y of the text component) -func (b *TextBox) Y() float64 { - return b.Box.Y() -} - -// X pulls the x of the composed Box (disambiguation with the x of the text component) -func (b *TextBox) X() float64 { - return b.Box.X() -} - -// ShiftX shifts the box by x. The associated text is attached and so will be moved along by default -func (b *TextBox) ShiftX(x float64) { - b.Box.ShiftX(x) -} - -// ShiftY shifts the box by y. The associated text is attached and so will be moved along by default -func (b *TextBox) ShiftY(y float64) { - b.Box.ShiftY(y) -} - -// SetSpace overwrites entities.Solid, -// pointing this button to use the mouse collision Rtree -// instead of the entity collision space. -func (b *TextBox) SetSpace(sp *collision.Space) { - mouse.Remove(b.Space) - b.Space = sp - mouse.Add(b.Space) -} - -// SetPos acts as SetSpace does, overwriting entities.Solid. -func (b *TextBox) SetPos(x, y float64) { - b.Box.SetPos(x, y) -} - -// SetOffsets changes the text position within the box -func (b *TextBox) SetOffsets(txtX, txtY float64) { - b.Text.Attach(b.Box.Vector, txtX, -txtY+b.H) -} - -func (b *TextBox) Destroy() { - if b.Text != nil { - b.Text.Undraw() - } - b.Box.Destroy() -} diff --git a/entities/x/btn/textOptions.go b/entities/x/btn/textOptions.go index e48e2683..c8ba4bfa 100644 --- a/entities/x/btn/textOptions.go +++ b/entities/x/btn/textOptions.go @@ -3,7 +3,7 @@ package btn import ( "fmt" - "github.com/oakmound/oak/v3/render" + "github.com/oakmound/oak/v4/render" ) //Text sets the text of the button to be generated diff --git a/entities/x/doc.go b/entities/x/doc.go new file mode 100644 index 00000000..530e9daf --- /dev/null +++ b/entities/x/doc.go @@ -0,0 +1,2 @@ +// Package x provides experimental utilities +package x diff --git a/entities/x/force/directionSpace.go b/entities/x/force/directionSpace.go deleted file mode 100644 index 83014cee..00000000 --- a/entities/x/force/directionSpace.go +++ /dev/null @@ -1,28 +0,0 @@ -package force - -import ( - "github.com/oakmound/oak/v3/collision" - "github.com/oakmound/oak/v3/event" - "github.com/oakmound/oak/v3/physics" -) - -// A DirectionSpace combines collision and a intended direction collision should imply -type DirectionSpace struct { - *collision.Space - physics.ForceVector -} - -// Init initializes the DirectionSpace as an entity -func (ds *DirectionSpace) Init() event.CID { - return event.NextID(ds) -} - -// NewDirectionSpace creates a DirectionSpace and initializes it as an entity. -func NewDirectionSpace(s *collision.Space, v physics.ForceVector) *DirectionSpace { - ds := &DirectionSpace{ - Space: s, - ForceVector: v, - } - s.CID = ds.Init() - return ds -} diff --git a/entities/x/force/hurtBox.go b/entities/x/force/hurtBox.go deleted file mode 100644 index 12af2bc7..00000000 --- a/entities/x/force/hurtBox.go +++ /dev/null @@ -1,52 +0,0 @@ -package force - -import ( - "image/color" - "time" - - "github.com/oakmound/oak/v3/collision" - "github.com/oakmound/oak/v3/physics" - "github.com/oakmound/oak/v3/render" - "github.com/oakmound/oak/v3/scene" -) - -type hurtBox struct { - *DirectionSpace -} - -// NewHurtBox creates a temporary collision space with a given force it should -// apply to objects it collides with -func NewHurtBox(ctx *scene.Context, x, y, w, h float64, duration time.Duration, l collision.Label, fv physics.ForceVector) { - hb := new(hurtBox) - hb.DirectionSpace = NewDirectionSpace(collision.NewLabeledSpace(x, y, w, h, l), fv) - collision.Add(hb.Space) - go ctx.DoAfter(duration, func() { - collision.Remove(hb.Space) - }) -} - -// NewHurtColor creates a temporary collision space with a given force it should -// apply to objects it collides with. The box is rendered as the given color -func NewHurtColor(ctx *scene.Context, x, y, w, h float64, duration time.Duration, l collision.Label, - fv physics.ForceVector, c color.Color, layers ...int) { - - cb := render.NewColorBox(int(w), int(h), c) - NewHurtDisplay(ctx, x, y, w, h, duration, l, fv, cb, layers...) -} - -// NewHurtDisplay creates a temporary collision space with a given force it should -// apply to objects it collides with. The box is rendered as the given renderable. -// The input renderable is not copied before it is drawn. -func NewHurtDisplay(ctx *scene.Context, x, y, w, h float64, duration time.Duration, l collision.Label, - fv physics.ForceVector, r render.Renderable, layers ...int) { - - hb := new(hurtBox) - hb.DirectionSpace = NewDirectionSpace(collision.NewLabeledSpace(x, y, w, h, l), fv) - collision.Add(hb.Space) - r.SetPos(x, y) - render.Draw(r, layers...) - go ctx.DoAfter(duration, func() { - collision.Remove(hb.Space) - r.Undraw() - }) -} diff --git a/entities/x/move/mover.go b/entities/x/move/mover.go deleted file mode 100644 index 7dd9cb9c..00000000 --- a/entities/x/move/mover.go +++ /dev/null @@ -1,18 +0,0 @@ -package move - -import ( - "github.com/oakmound/oak/v3/collision" - "github.com/oakmound/oak/v3/physics" - "github.com/oakmound/oak/v3/render" -) - -// A Mover can move its position, renderable, and space. Unless otherwise documented, -// functions effecting a mover move all of its logical position, renderable, and space -// simultaneously. -type Mover interface { - Vec() physics.Vector - GetRenderable() render.Renderable - GetDelta() physics.Vector - GetSpace() *collision.Space - GetSpeed() physics.Vector -} diff --git a/entities/x/move/shift.go b/entities/x/move/shift.go deleted file mode 100644 index cdfe6e7d..00000000 --- a/entities/x/move/shift.go +++ /dev/null @@ -1,21 +0,0 @@ -package move - -// ShiftX will ShiftX on the vector of the mover and -// set the renderable and space positions to that of the updated vector. -func ShiftX(mvr Mover, x float64) { - vec := mvr.Vec() - vec.ShiftX(x) - mvr.GetRenderable().SetPos(vec.X(), vec.Y()) - sp := mvr.GetSpace() - sp.Update(vec.X(), vec.Y(), sp.GetW(), sp.GetH()) -} - -// ShiftY will ShiftY on the vector of the mover and -// set the renderable and space positions to that of the updated vector. -func ShiftY(mvr Mover, y float64) { - vec := mvr.Vec() - vec.ShiftY(y) - mvr.GetRenderable().SetPos(vec.X(), vec.Y()) - sp := mvr.GetSpace() - sp.Update(vec.X(), vec.Y(), sp.GetW(), sp.GetH()) -} diff --git a/entities/x/move/topdown.go b/entities/x/move/topdown.go deleted file mode 100644 index 8d16bbd5..00000000 --- a/entities/x/move/topdown.go +++ /dev/null @@ -1,71 +0,0 @@ -package move - -import ( - oak "github.com/oakmound/oak/v3" - "github.com/oakmound/oak/v3/alg/floatgeom" - "github.com/oakmound/oak/v3/key" - "github.com/oakmound/oak/v3/physics" -) - -// WASD moves the given mover based on its speed as W,A,S, and D are pressed -func WASD(mvr Mover) { - TopDown(mvr, key.W, key.S, key.A, key.D) -} - -// Arrows moves the given mover based on its speed as the arrow keys are pressed -func Arrows(mvr Mover) { - TopDown(mvr, key.UpArrow, key.DownArrow, key.LeftArrow, key.RightAlt) -} - -// TopDown moves the given mover based on its speed as the given keys are pressed -func TopDown(mvr Mover, up, down, left, right string) { - delta := mvr.GetDelta() - vec := mvr.Vec() - spd := mvr.GetSpeed() - - delta.Zero() - if oak.IsDown(up) { - delta.Add(physics.NewVector(0, -spd.Y())) - } - if oak.IsDown(down) { - delta.Add(physics.NewVector(0, spd.Y())) - } - if oak.IsDown(left) { - delta.Add(physics.NewVector(-spd.X(), 0)) - } - if oak.IsDown(right) { - delta.Add(physics.NewVector(spd.X(), 0)) - } - vec.Add(delta) - mvr.GetRenderable().SetPos(vec.X(), vec.Y()) - sp := mvr.GetSpace() - sp.Update(vec.X(), vec.Y(), sp.GetW(), sp.GetH()) -} - -// CenterScreenOn will cause the screen to center on the given mover, obeying -// viewport limits if they have been set previously -func CenterScreenOn(mvr Mover) { - vec := mvr.Vec() - oak.SetScreen( - int(vec.X())-oak.Width()/2, - int(vec.Y())-oak.Height()/2, - ) -} - -// Limit restricts the movement of the mover to stay within a given rectangle -func Limit(mvr Mover, rect floatgeom.Rect2) { - vec := mvr.Vec() - w, h := mvr.GetRenderable().GetDims() - wf := float64(w) - hf := float64(h) - if vec.X() < rect.Min.X() { - vec.SetX(rect.Min.X()) - } else if vec.X() > rect.Max.X()-wf { - vec.SetX(rect.Max.X() - wf) - } - if vec.Y() < rect.Min.Y() { - vec.SetY(rect.Min.Y()) - } else if vec.Y() > rect.Max.Y()-hf { - vec.SetY(rect.Max.Y() - hf) - } -} diff --git a/entities/x/stat/default.go b/entities/x/stat/default.go deleted file mode 100644 index cd519ffd..00000000 --- a/entities/x/stat/default.go +++ /dev/null @@ -1,50 +0,0 @@ -package stat - -var ( - // DefStatistics is a base set of statistics used by package-level calls - // When using multiple statistics, avoid using overlapping event names - DefStatistics = NewStatistics() -) - -// Inc triggers an event, incrementing the given statistic by one -func Inc(eventName string) { - DefStatistics.Inc(eventName) -} - -// Trigger triggers the given event with a given increment to update a statistic -func Trigger(eventName string, inc int) { - DefStatistics.Trigger(eventName, inc) -} - -// TriggerOn triggers the given event, toggling it on -func TriggerOn(eventName string) { - DefStatistics.TriggerOn(eventName) -} - -// TriggerOff triggers the given event, toggling it off -func TriggerOff(eventName string) { - DefStatistics.TriggerOff(eventName) -} - -// TriggerTimed triggers the given event, toggling it on or off -func TriggerTimed(eventName string, on bool) { - DefStatistics.TriggerTimed(eventName, on) -} - -// TrackStats records a stat event to the Statistics map and creates the statistic if it does not already exist -func TrackStats(no int, data interface{}) int { - return DefStatistics.TrackStats(no, data) -} - -// TrackTimeStats acts like TrackStats, but tracks durations of events. If the -// event has not started, it logs a start time, and then when the event ends -// it will log the delta since the start. -func TrackTimeStats(no int, data interface{}) int { - return DefStatistics.TrackTimeStats(no, data) -} - -// IsTimedStat returns whether the given stat name is a part of this statistics' -// set of timed stats -func IsTimedStat(s string) bool { - return DefStatistics.IsTimedStat(s) -} diff --git a/entities/x/stat/statistic.go b/entities/x/stat/statistic.go deleted file mode 100644 index 970fa494..00000000 --- a/entities/x/stat/statistic.go +++ /dev/null @@ -1,113 +0,0 @@ -package stat - -import ( - "sync" - "time" - - "github.com/oakmound/oak/v3/dlog" - "github.com/oakmound/oak/v3/event" -) - -// Statistics stores the ongoing results of TrackStats and TrackTimeStats -type Statistics struct { - stats map[string]*History - statLock sync.Mutex - - statTimes map[string]time.Time - statTimeLock sync.Mutex -} - -// NewStatistics creates an empty statistics set -func NewStatistics() *Statistics { - return &Statistics{ - stats: make(map[string]*History), - statLock: sync.Mutex{}, - statTimes: make(map[string]time.Time), - statTimeLock: sync.Mutex{}, - } -} - -// A History keeps track of any recorded occurrences of this statstic and their magnitude -type History struct { - Name string - Events []Event -} - -// An Event ties a value to a timestamp -type Event struct { - Timestamp time.Time - Val int -} - -// NewHistory creates a stat -func NewHistory(statName string, time time.Time) *History { - return &History{Name: statName, Events: []Event{{time, 0}}} -} - -// track adds a tracked event to the stat's history -func (h *History) track(t time.Time, v int) *History { - if len(h.Events) > 0 { - v += h.Events[len(h.Events)-1].Val - } - h.Events = append(h.Events, Event{t, v}) - return h -} - -// Total takes a statistics history and finds the sum. -func (h *History) Total() int { - return h.Events[len(h.Events)-1].Val -} - -func (st *Statistics) trackStats(name string, val int) { - st.statLock.Lock() - stat, ok := st.stats[name] - if !ok { - stat = NewHistory(name, time.Now()) - st.stats[name] = stat - } - stat.track(time.Now(), val) - st.statLock.Unlock() -} - -// TrackStats records a stat event to the Statistics map and creates the statistic if it does not already exist -func (st *Statistics) TrackStats(no int, data interface{}) int { - stat, ok := data.(stat) - if !ok { - dlog.Error("TrackStats called with a non-stat payload") - return event.UnbindEvent - } - st.trackStats(stat.name, stat.inc) - return 0 -} - -// TrackTimeStats acts like TrackStats, but tracks durations of events. If the -// event has not started, it logs a start time, and then when the event ends -// it will log the delta since the start. -func (st *Statistics) TrackTimeStats(no int, data interface{}) int { - timed, ok := data.(timedStat) - if !ok { - dlog.Error("TrackTimeStats called with a non-timedStat payload") - return event.UnbindEvent - } - if timed.on { //Turning on a thing to time track - st.statTimeLock.Lock() - st.statTimes[timed.name] = time.Now() - st.statTimeLock.Unlock() - } else { - st.statTimeLock.Lock() - timeDiff := int(time.Since(st.statTimes[timed.name])) - st.statTimeLock.Unlock() - if timeDiff < 0 { - return 0 - } - st.trackStats(timed.name, timeDiff) - } - return 0 -} - -// IsTimedStat returns whether the given stat name is a part of this statistics' -// set of timed stats -func (st *Statistics) IsTimedStat(s string) bool { - _, ok := st.statTimes[s] - return ok -} diff --git a/entities/x/stat/stats.go b/entities/x/stat/stats.go deleted file mode 100644 index f7fe8358..00000000 --- a/entities/x/stat/stats.go +++ /dev/null @@ -1,63 +0,0 @@ -package stat - -import "github.com/oakmound/oak/v3/event" - -type timedStat struct { - name string - on bool -} -type stat struct { - name string - inc int -} - -// TimedOn returns a binding that will trigger toggling on the given event -func TimedOn(eventName string) event.Bindable { - return TimedBind(eventName, true) -} - -// TimedOff returns a binding that will trigger toggling off the given event -func TimedOff(eventName string) event.Bindable { - return TimedBind(eventName, false) -} - -// TimedBind returns a binding that will trigger toggling on or off the given event -func TimedBind(eventName string, on bool) event.Bindable { - return func(event.CID, interface{}) int { - event.Trigger(eventName, timedStat{eventName, on}) - return 0 - } -} - -// Bind returns a binding that will increment the given event by 'inc' -func Bind(eventName string, inc int) event.Bindable { - return func(event.CID, interface{}) int { - event.Trigger(eventName, stat{eventName, inc}) - return 0 - } -} - -// Inc triggers an event, incrementing the given statistic by one -func (st *Statistics) Inc(eventName string) { - st.Trigger(eventName, 1) -} - -// Trigger triggers the given event with a given increment to update a statistic -func (st *Statistics) Trigger(eventName string, inc int) { - event.Trigger(eventName, stat{eventName, inc}) -} - -// TriggerOn triggers the given event, toggling it on -func (st *Statistics) TriggerOn(eventName string) { - st.TriggerTimed(eventName, true) -} - -// TriggerOff triggers the given event, toggling it off -func (st *Statistics) TriggerOff(eventName string) { - st.TriggerTimed(eventName, false) -} - -// TriggerTimed triggers the given event, toggling it on or off -func (st *Statistics) TriggerTimed(eventName string, on bool) { - event.Trigger(eventName, timedStat{eventName, on}) -} diff --git a/event/bind.go b/event/bind.go index 4a0b654f..9e8276d2 100644 --- a/event/bind.go +++ b/event/bind.go @@ -1,71 +1,158 @@ package event -// Bind adds a function to the event bus tied to the given callerID -// to be called when the event name is triggered. It is equivalent to -// calling BindPriority with a zero Priority. -func (eb *Bus) Bind(name string, callerID CID, fn Bindable) { - eb.pendingMutex.Lock() - eb.binds = append(eb.binds, UnbindOption{ - Event: Event{ - Name: name, - CallerID: callerID, - }, Fn: fn}) - eb.pendingMutex.Unlock() +import ( + "sync/atomic" + + "github.com/oakmound/oak/v4/dlog" +) + +// Q: Why do Bind / Unbind / etc not immediately take effect? +// A: For concurrent safety, most operations on a bus lock the bus. Triggers acquire a read lock on the bus, +// as they iterate over internal bus components. Most logic within an event bus will happen from within +// a Trigger call-- when an entity is destroyed by some collision, for example, all of its bindings should +// be unregistered. If one were to call Unbind from within a call to Trigger, the trigger would never release +// its lock-- so the unbind would never be able to take the lock-- so the bus would be unrecoverably stuck. + +// Q: Why not trust users to call Bind / Unbind / etc with `go`, to allow the caller to decide when to use +// concurrency? +// A: It is almost never correct to not call such a function with `go`, and it is a bad user experience for +// the engine to deadlock unexpectedly because you forgot to begin some call with a goroutine. + +// A Binding, returned from calls to Bind, references the details of a binding and where that binding is +// stored within a handler. The common use case for this structure would involve a system that wanted to +// keep track of its bindings for later remote unbinding. This structure can also be used to construct +// and unbind a known reference. +type Binding struct { + Handler Handler + EventID UnsafeEventID + CallerID CallerID + BindID BindID + + busResetCount int64 + + // Bound is closed once the binding has been applied. Wait on this condition carefully; bindings + // will not take effect while an event is being triggered (e.g. in a event callback's returning thread) + Bound <-chan struct{} } -// PersistentBind acts like Bind, but persists the binding such that if the event -// bus is reset, the binding will still trigger. Thes bindings should likely be global -// bindings, using a CID of 0, or be tolerant to the CID bound not being present after -// such a clear. -func (eb *Bus) PersistentBind(name string, callerID CID, fn Bindable) { - eb.pendingMutex.Lock() - opt := UnbindOption{ - Event: Event{ - Name: name, - CallerID: callerID, - }, Fn: fn} - eb.binds = append(eb.binds, opt) - eb.persistentBinds = append(eb.persistentBinds, opt) - eb.pendingMutex.Unlock() +// Unbind unbinds the callback associated with this binding from it's own event handler. If this binding +// does not belong to its handler or has already been unbound, this will do nothing. +func (b Binding) Unbind() <-chan struct{} { + return b.Handler.Unbind(b) +} + +// A BindID is a unique identifier for a binding within a bus. +type BindID int64 + +// UnsafeBind registers a callback function to be called whenever the provided event is triggered +// against this bus. The binding is concurrently bound, and therefore may not be immediately +// available to be triggered. When Reset is called on a Bus, all prior bindings are unbound and any +// concurrent calls to UnsafeBind will not take effect. This call is 'unsafe' because UnsafeBindables +// use bare interface{} types. +func (bus *Bus) UnsafeBind(eventID UnsafeEventID, callerID CallerID, fn UnsafeBindable) Binding { + expectedResetCount := bus.resetCount + bindID := BindID(atomic.AddInt64(bus.nextBindID, 1)) + ch := make(chan struct{}) + go func() { + defer close(ch) + bus.mutex.Lock() + defer bus.mutex.Unlock() + if bus.resetCount != expectedResetCount { + // The event bus has reset while we we were waiting to bind this + return + } + bl := bus.getBindableList(eventID, callerID) + bl[bindID] = fn + }() + return Binding{ + Handler: bus, + EventID: eventID, + CallerID: callerID, + BindID: bindID, + Bound: ch, + busResetCount: bus.resetCount, + } } -// ClearPersistentBindings removes all persistent bindings. It will not unbind them -// from the bus, but they will not be bound following the next bus reset. -func (eb *Bus) ClearPersistentBindings() { - eb.pendingMutex.Lock() - eb.persistentBinds = []UnbindOption{} - eb.pendingMutex.Unlock() +// PersistentBind calls UnsafeBind, and causes UnsafeBind to be called with these inputs when a Bus is Reset, i.e. +// persisting the binding through bus resets. Unbinding this will not stop it from being rebound on the next +// Bus Reset-- ClearPersistentBindings will. If called concurrently during a bus Reset, the request may not be +// bound until the next bus Reset. +func (bus *Bus) PersistentBind(eventID UnsafeEventID, callerID CallerID, fn UnsafeBindable) Binding { + binding := bus.UnsafeBind(eventID, callerID, fn) + go func() { + bus.mutex.Lock() + bus.persistentBindings = append(bus.persistentBindings, persistentBinding{ + eventID: eventID, + callerID: callerID, + fn: fn, + }) + bus.mutex.Unlock() + }() + return binding } -// GlobalBind binds on the bus to the cid 0, a non entity. -func (eb *Bus) GlobalBind(name string, fn Bindable) { - eb.Bind(name, 0, fn) +// Unbind unregisters a binding from a bus concurrently. Once complete, triggers that would +// have previously caused the Bindable callback to execute will no longer do so. +func (bus *Bus) Unbind(loc Binding) <-chan struct{} { + ch := make(chan struct{}) + go func() { + bus.mutex.Lock() + defer bus.mutex.Unlock() + if bus.resetCount != loc.busResetCount { + // This binding is not valid for this bus (in this state) + return + } + l := bus.getBindableList(loc.EventID, loc.CallerID) + delete(l, loc.BindID) + close(ch) + }() + return ch } -// Empty is a helper to convert a func() into a Bindable function signature. -func Empty(f func()) Bindable { - return func(CID, interface{}) int { - f() - return 0 +// A Bindable is a strongly typed callback function to be executed on Trigger. It must be paired +// with an event registered via RegisterEvent. +type Bindable[C any, Payload any] func(C, Payload) Response + +// Bind will cause the function fn to be called whenever the event ev is triggered on the given event handler. The function +// will be called with the provided caller as its first argument, and will also be called when the provided event is specifically +// triggered on the caller's ID. +func Bind[C Caller, Payload any](h Handler, ev EventID[Payload], caller C, fn Bindable[C, Payload]) Binding { + if caller.CID() == 0 { + dlog.Error("Bind called with CallerID 0; is this entity registered and set?") } + return h.UnsafeBind(ev.UnsafeEventID, caller.CID(), func(cid CallerID, h Handler, payload interface{}) Response { + typedPayload := payload.(Payload) + ent := h.GetCallerMap().GetEntity(cid) + typedEntity := ent.(C) + return fn(typedEntity, typedPayload) + }) +} + +// A GlobalBindable is a bindable that is not bound to a specific caller. +type GlobalBindable[Payload any] func(Payload) Response + +// GlobalBind will cause the function fn to be called whenever the event ev is triggered on the given event handler. +func GlobalBind[Payload any](h Handler, ev EventID[Payload], fn GlobalBindable[Payload]) Binding { + return h.UnsafeBind(ev.UnsafeEventID, Global, func(cid CallerID, h Handler, payload interface{}) Response { + typedPayload := payload.(Payload) + return fn(typedPayload) + }) } -// WaitForEvent will return a single payload from the given event. This -// makes an internal binding, but that binding will clean itself up -// regardless of how this is used. This should be used in a select clause -// to ensure the signal is captured, if the signal comes and the output -// channel is not being waited on, the channel will be closed. -func (eb *Bus) WaitForEvent(name string) <-chan interface{} { - ch := make(chan interface{}) +// UnsafeBindable defines the underlying signature of all bindings. +type UnsafeBindable func(CallerID, Handler, interface{}) Response + +// UnbindAllFrom unbinds all bindings currently bound to the provided caller via ID. +func (bus *Bus) UnbindAllFrom(c CallerID) <-chan struct{} { + ch := make(chan struct{}) go func() { - eb.GlobalBind(name, func(c CID, i interface{}) int { - select { - case ch <- i: - default: - } - close(ch) - return UnbindSingle - }) + bus.mutex.Lock() + for _, callerMap := range bus.bindingMap { + delete(callerMap, c) + } + bus.mutex.Unlock() + close(ch) }() return ch } diff --git a/event/bind_test.go b/event/bind_test.go new file mode 100644 index 00000000..12d0414e --- /dev/null +++ b/event/bind_test.go @@ -0,0 +1,119 @@ +package event_test + +import ( + "sync/atomic" + "testing" + + "github.com/oakmound/oak/v4/event" +) + +func TestBus_UnsafeBind(t *testing.T) { + t.Run("ConcurrentReset", func(t *testing.T) { + b := event.NewBus(event.NewCallerMap()) + + var calls int32 + for i := 0; i < 1000; i++ { + b.UnsafeBind(1, 0, func(ci event.CallerID, h event.Handler, i interface{}) event.Response { + atomic.AddInt32(&calls, 1) + return 0 + }) + b.Reset() + // No matter what happens with thread scheduling above, this trigger should never increment calls + <-b.Trigger(1, nil) + } + if calls != 0 { + t.Fatal("a pre-reset binding was triggered after a bus reset") + } + }) +} + +func TestBus_Unbind(t *testing.T) { + t.Run("ConcurrentReset", func(t *testing.T) { + b := event.NewBus(event.NewCallerMap()) + + var goodCalls int32 + for i := 0; i < 1000; i++ { + b1 := b.UnsafeBind(1, 0, func(ci event.CallerID, h event.Handler, i interface{}) event.Response { + return 0 + }) + b.Unbind(b1) + b.Reset() + // b1 and b2 will share a bindID + b2 := b.UnsafeBind(1, 0, func(ci event.CallerID, h event.Handler, i interface{}) event.Response { + atomic.AddInt32(&goodCalls, 1) + return 0 + }) + <-b2.Bound + <-b.Trigger(1, nil) + <-b2.Unbind() + } + if goodCalls != 1000 { + t.Fatal("a pre-reset unbind unbound a post-reset binding", goodCalls) + } + }) +} + +func TestBind(t *testing.T) { + t.Run("SuperficialCoverage", func(t *testing.T) { + b := event.NewBus(event.NewCallerMap()) + var cid event.CallerID + id := b.GetCallerMap().Register(cid) + var calls int32 + b1 := event.Bind(b, event.Enter, id, func(event.CallerID, event.EnterPayload) event.Response { + atomic.AddInt32(&calls, 1) + return 0 + }) + <-b1.Bound + <-event.TriggerOn(b, event.Enter, event.EnterPayload{}) + if calls != 1 { + t.Fatal(expectedError("calls", 1, calls)) + } + }) +} + +func TestGlobalBind(t *testing.T) { + t.Run("SuperficialCoverage", func(t *testing.T) { + b := event.NewBus(event.NewCallerMap()) + var calls int32 + b1 := event.GlobalBind(b, event.Enter, func(event.EnterPayload) event.Response { + atomic.AddInt32(&calls, 1) + return 0 + }) + <-b1.Bound + <-event.TriggerOn(b, event.Enter, event.EnterPayload{}) + if calls != 1 { + t.Fatal(expectedError("calls", 1, calls)) + } + }) +} + +func TestBus_UnbindAllFrom(t *testing.T) { + t.Run("Basic", func(t *testing.T) { + b := event.NewBus(event.NewCallerMap()) + var cid event.CallerID + id := b.GetCallerMap().Register(cid) + var calls int32 + for i := 0; i < 5; i++ { + b1 := event.Bind(b, event.Enter, id, func(event.CallerID, event.EnterPayload) event.Response { + atomic.AddInt32(&calls, 1) + return 0 + }) + <-b1.Bound + } + id2 := b.GetCallerMap().Register(cid) + b1 := event.Bind(b, event.Enter, id2, func(event.CallerID, event.EnterPayload) event.Response { + atomic.AddInt32(&calls, 1) + return 0 + }) + <-b1.Bound + <-event.TriggerOn(b, event.Enter, event.EnterPayload{}) + if calls != 6 { + t.Fatal(expectedError("calls", 1, calls)) + } + <-b.UnbindAllFrom(id) + <-event.TriggerOn(b, event.Enter, event.EnterPayload{}) + if calls != 7 { + t.Fatal(expectedError("calls", 1, calls)) + } + }) +} diff --git a/event/bindingSet.go b/event/bindingSet.go deleted file mode 100644 index 23dc6a5c..00000000 --- a/event/bindingSet.go +++ /dev/null @@ -1,42 +0,0 @@ -package event - -// A Mapping stores a slice of event names and bindings -type Mapping struct { - eventNames []string - binds []Bindable -} - -// A BindingSet stores sets of event mappings bound to string names. -// The use case for a BindingSet is for a character that can exist in multiple states, -// so that they can swiftly switch between the event bindings that define those -// states. -type BindingSet map[string]Mapping - -// Set makes a new EventMapping for BindingSet -func (b BindingSet) Set(setName string, mappingSets ...map[string]Bindable) BindingSet { - - numMappings := 0 - for _, m := range mappingSets { - numMappings += len(m) - - } - bindings := make([]Bindable, numMappings) - events := make([]string, numMappings) - i := 0 - for _, m := range mappingSets { - for k, v := range m { - bindings[i] = v - events[i] = k - i++ - } - } - - b[setName] = Mapping{eventNames: events, binds: bindings} - return b -} - -// RebindMapping resets the entity controlling this cid to only have the bindings -// in the passed in event mapping -func (cid CID) RebindMapping(mapping Mapping) { - cid.UnbindAllAndRebind(mapping.binds, mapping.eventNames) -} diff --git a/event/bus.go b/event/bus.go index c95f1655..e47f7502 100644 --- a/event/bus.go +++ b/event/bus.go @@ -1,51 +1,33 @@ package event import ( - "reflect" "sync" "time" ) -// Bindable is a way of saying "Any function -// that takes a generic struct of data -// and returns an error can be bound". -type Bindable func(CID, interface{}) int +// A Bus stores bindables to be triggered by events. +type Bus struct { + // nextBindID is an atomically incrementing value to track bindings within this structure + nextBindID *int64 -// BindableList just stores other relevant data -// that a list of bindables needs to -// operate efficiently -type bindableList struct { - sl []Bindable - // We keep track of where the next nil - // element in our list is, so we - // can let bindings know where they - // are by index, (we don't shift to - // fill empty spaces) and so we can - // fill that slot next when a - // new binding comes in. - nextEmpty int -} + // resetCount increments every time the bus is reset. bindings and unbindings make sure that + // they are called on a bus with an unchanged reset count, and become NOPs if performed on + // a bus with a different reset count to ensure they do not interfere with a bus using different + // bind IDs. + resetCount int64 + bindingMap map[UnsafeEventID]map[CallerID]bindableList + persistentBindings []persistentBinding -// A Bus stores bindables to be triggered by events -type Bus struct { - bindingMap map[string]map[CID]*bindableList - doneCh chan struct{} - framesElapsed int - Ticker *time.Ticker - binds []UnbindOption - partUnbinds []Event - fullUnbinds []UnbindOption - unbinds []binding - unbindAllAndRebinds []UnbindAllOption - persistentBinds []UnbindOption - framerate int - refreshRate time.Duration - callerMap *CallerMap + callerMap *CallerMap - mutex sync.RWMutex - pendingMutex sync.Mutex + mutex sync.RWMutex +} - init sync.Once +// a persistentBinding is rebound every time the bus is reset. +type persistentBinding struct { + eventID UnsafeEventID + callerID CallerID + fn UnsafeBindable } // NewBus returns an empty event bus with an assigned caller map. If nil @@ -55,125 +37,78 @@ func NewBus(callerMap *CallerMap) *Bus { callerMap = DefaultCallerMap } return &Bus{ - bindingMap: make(map[string]map[CID]*bindableList), - doneCh: make(chan struct{}), + nextBindID: new(int64), + bindingMap: make(map[UnsafeEventID]map[CallerID]bindableList), callerMap: callerMap, } } // SetCallerMap updates a bus to use a specific set of callers. -func (b *Bus) SetCallerMap(cm *CallerMap) { - b.callerMap = cm +func (bus *Bus) SetCallerMap(cm *CallerMap) { + bus.callerMap = cm } -// An Event is an event name and an associated caller id -type Event struct { - Name string - CallerID CID +// GetCallerMap returns this bus's caller map. +func (b *Bus) GetCallerMap() *CallerMap { + return b.callerMap } -// UnbindOption stores information necessary -// to unbind a bindable -type UnbindOption struct { - Event - Fn Bindable -} - -// binding stores data necessary -// to trace back to a bindable function -// and remove it from a Bus. -type binding struct { - Event - index int -} - -// Reset empties out all transient portions of the bus. It will not stop -// an ongoing loop. -func (eb *Bus) Reset() { +// ClearPersistentBindings removes all persistent bindings. It will not unbind them +// from the bus, but they will not be bound following the next bus reset. +func (eb *Bus) ClearPersistentBindings() { eb.mutex.Lock() - eb.pendingMutex.Lock() - eb.bindingMap = make(map[string]map[CID]*bindableList) - eb.binds = []UnbindOption{} - eb.partUnbinds = []Event{} - eb.fullUnbinds = []UnbindOption{} - eb.unbinds = []binding{} - eb.unbindAllAndRebinds = []UnbindAllOption{} - for _, bindSet := range eb.persistentBinds { - list := eb.getBindableList(bindSet.Event) - list.storeBindable(bindSet.Fn) - } - eb.pendingMutex.Unlock() + eb.persistentBindings = eb.persistentBindings[:0] eb.mutex.Unlock() } -// UnbindAllOption stores information needed to unbind and rebind -type UnbindAllOption struct { - ub Event - bs []Event - bnds []Bindable -} - -// Store a bindable into a BindableList. -func (bl *bindableList) storeBindable(fn Bindable) int { - - i := bl.nextEmpty - if len(bl.sl) == i { - bl.sl = append(bl.sl, fn) - } else { - bl.sl[i] = fn +// Reset unbinds all present, non-persistent bindings on the bus. It will block until +// persistent bindings are in place. +func (bus *Bus) Reset() { + bus.mutex.Lock() + bus.resetCount++ + bus.bindingMap = make(map[UnsafeEventID]map[CallerID]bindableList) + repersist := make([]Binding, len(bus.persistentBindings)) + for i, pb := range bus.persistentBindings { + repersist[i] = bus.UnsafeBind(pb.eventID, pb.callerID, pb.fn) } - - // Find the next empty space - for len(bl.sl) != bl.nextEmpty && bl.sl[bl.nextEmpty] != nil { - bl.nextEmpty++ + bus.mutex.Unlock() + for _, bnd := range repersist { + <-bnd.Bound } - - return i } -// This scans linearly for the bindable -// This will cause an issue with closures! -// You can't unbind closures that don't have the -// same variable reference because this compares -// pointers! -// -// At all costs, this should be avoided, and -// returning UnbindSingle from the function -// itself is much safer! -func (bl *bindableList) removeBindable(fn Bindable) { - v := reflect.ValueOf(fn) - for i := 0; i < len(bl.sl); i++ { - v2 := reflect.ValueOf(bl.sl[i]) - if v2 == v { - bl.removeIndex(i) - return +// EnterLoop triggers Enter events at the specified rate until the returned cancel is called. +func EnterLoop(bus Handler, frameDelay time.Duration) (cancel func()) { + ch := make(chan struct{}) + go func() { + ticker := time.NewTicker(frameDelay) + frameDelayF64 := float64(frameDelay) + lastTick := time.Now() + framesElapsed := 0 + for { + select { + case now := <-ticker.C: + deltaTime := now.Sub(lastTick) + lastTick = now + <-bus.Trigger(Enter.UnsafeEventID, EnterPayload{ + FramesElapsed: framesElapsed, + SinceLastFrame: deltaTime, + TickPercent: float64(deltaTime) / frameDelayF64, + }) + framesElapsed++ + case <-ch: + ticker.Stop() + return + } } + }() + return func() { + // Q: why send here as well as close + // A: to ensure that no more ticks are sent, the above goroutine has to + // acknowledge that it should stop and return-- just closing would + // enable code following this cancel function to assume no enters were + // being triggered when they still are. + ch <- struct{}{} + close(ch) } } - -// Remove a bindable from a BindableList -func (bl *bindableList) removeBinding(b binding) { - bl.removeIndex(b.index) -} - -func (bl *bindableList) removeIndex(i int) { - if len(bl.sl) <= i { - return - } - - bl.sl[i] = nil - - if i < bl.nextEmpty { - bl.nextEmpty = i - } -} - -func (eb *Bus) getBindableList(opt Event) *bindableList { - if m := eb.bindingMap[opt.Name]; m == nil { - eb.bindingMap[opt.Name] = make(map[CID]*bindableList) - } - if m := eb.bindingMap[opt.Name][opt.CallerID]; m == nil { - eb.bindingMap[opt.Name][opt.CallerID] = new(bindableList) - } - return eb.bindingMap[opt.Name][opt.CallerID] -} diff --git a/event/bus_test.go b/event/bus_test.go index bde6a6d9..31441f7f 100644 --- a/event/bus_test.go +++ b/event/bus_test.go @@ -1,52 +1,99 @@ -package event +package event_test import ( - "fmt" + "math/rand" + "sync/atomic" "testing" "time" + + "github.com/oakmound/oak/v4/event" ) -func TestBusStop(t *testing.T) { - b := NewBus(nil) - b.Ticker = time.NewTicker(10000 * time.Second) - phase := 0 - wait := make(chan struct{}) - var topErr error - go func() { - if err := b.Stop(); err != nil { - topErr = fmt.Errorf("stop errored: %v", err) - } - if phase != 1 { - topErr = fmt.Errorf("expected phase %v, got %v", 1, phase) - } - wait <- struct{}{} - }() - phase = 1 +func TestNewBus(t *testing.T) { + t.Run("DefaultCallerMap", func(t *testing.T) { + b := event.NewBus(nil) + if b.GetCallerMap() != event.DefaultCallerMap { + t.Fatal("nil caller map not turned into default caller map") + } + }) + t.Run("Basic", func(t *testing.T) { + b := event.NewBus(event.NewCallerMap()) + if b == nil { + t.Fatal("NewBus created nil bus") + } + }) +} + +func TestBus_SetCallerMap(t *testing.T) { + t.Run("Basic", func(t *testing.T) { + cm1 := event.NewCallerMap() + b := event.NewBus(cm1) + c1 := event.CallerID(rand.Intn(10000)) + b.GetCallerMap().Register(c1) + cm2 := event.NewCallerMap() + b.SetCallerMap(cm2) + if b.GetCallerMap().HasEntity(c1) { + t.Fatal("event had old entity after changed caller map") + } + }) +} - <-b.doneCh - <-wait - if topErr != nil { - t.Fatal(topErr) - } +func TestBus_ClearPersistentBindings(t *testing.T) { + t.Run("Basic", func(t *testing.T) { + b := event.NewBus(event.NewCallerMap()) + var impersistentCalls int32 + var persistentCalls int32 + b1 := b.UnsafeBind(1, 0, func(ci event.CallerID, h event.Handler, i interface{}) event.Response { + atomic.AddInt32(&impersistentCalls, 1) + return 0 + }) + b2 := b.PersistentBind(1, 0, func(ci event.CallerID, h event.Handler, i interface{}) event.Response { + atomic.AddInt32(&persistentCalls, 1) + return 0 + }) + <-b1.Bound + <-b2.Bound + <-b.Trigger(1, nil) + if impersistentCalls != 1 { + t.Fatal(expectedError("impersistent calls", 1, impersistentCalls)) + } + if persistentCalls != 1 { + t.Fatal(expectedError("persistent calls", 1, persistentCalls)) + } + b.Reset() + <-b.Trigger(1, nil) + if impersistentCalls != 1 { + t.Fatal(expectedError("impersistent calls", 1, impersistentCalls)) + } + if persistentCalls != 2 { + t.Fatal(expectedError("persistent calls", 2, persistentCalls)) + } + b.ClearPersistentBindings() + b.Reset() + <-b.Trigger(1, nil) + if impersistentCalls != 1 { + t.Fatal(expectedError("impersistent calls", 1, impersistentCalls)) + } + if persistentCalls != 2 { + t.Fatal(expectedError("persistent calls", 2, persistentCalls)) + } + }) } -func TestBusPersistentBind(t *testing.T) { - t.Parallel() - b := NewBus(nil) - ev := "eventName" - calls := 0 - b.PersistentBind(ev, 0, func(c CID, i interface{}) int { - calls++ - return 0 +func TestBus_EnterLoop(t *testing.T) { + t.Run("Basic", func(t *testing.T) { + b := event.NewBus(event.NewCallerMap()) + var calls int32 + b1 := b.UnsafeBind(event.Enter.UnsafeEventID, 0, func(ci event.CallerID, h event.Handler, i interface{}) event.Response { + atomic.AddInt32(&calls, 1) + return 0 + }) + <-b1.Bound + cancel := event.EnterLoop(b, 50*time.Millisecond) + time.Sleep(1*time.Second + 15*time.Millisecond) + cancel() + if calls != 20 { + t.Fatal(expectedError("calls", 20, calls)) + } }) - b.Flush() - <-b.TriggerBack(ev, nil) - if calls != 1 { - t.Fatalf("expected binding to be called once, was called %d time(s)", calls) - } - b.Reset() - <-b.TriggerBack(ev, nil) - if calls != 2 { - t.Fatalf("expected binding to be called twice, was called %d time(s)", calls) - } } diff --git a/event/caller.go b/event/caller.go index b7515c40..880fd1a7 100644 --- a/event/caller.go +++ b/event/caller.go @@ -1,11 +1,94 @@ package event -// A Caller can bind, unbind and trigger events. +import ( + "sync" +) + +// A CallerID is a caller ID that Callers use to bind themselves to receive callback +// signals when given events are triggered +type CallerID int64 + +func (c CallerID) CID() CallerID { + return c +} + +// Global is the CallerID associated with global bindings. A caller must not be assigned +// this ID. Global may be used to manually create bindings scoped to no callers, but the GlobalBind function +// should be preferred when possible for type safety. +const Global CallerID = 0 + type Caller interface { - Trigger(string, interface{}) - Bind(string, Bindable) - UnbindAll() - UnbindAllAndRebind([]Bindable, []string) - E() interface{} - Parse(Entity) CID + CID() CallerID +} + +// A CallerMap tracks CallerID mappings to Entities. +// This is an alternative to passing in the entity via closure scoping, +// and allows for more general bindings as simple top level functions. +type CallerMap struct { + highestID CallerID + callersLock sync.RWMutex + callers map[CallerID]Caller +} + +// NewCallerMap creates a caller map. A CallerMap +// is not valid for use if not created via this function. +func NewCallerMap() *CallerMap { + return &CallerMap{ + callers: map[CallerID]Caller{}, + } +} + +// NextID finds the next available caller id +// and returns it, after adding the given entity to +// the caller map. +func (cm *CallerMap) Register(e Caller) CallerID { + cm.callersLock.Lock() + defer cm.callersLock.Unlock() + // Q: Why not use atomic? + // A: We're in a mutex and therefore it is not needed. + // A2: We need the mutex to safely assign to the map. + // A3: We cannot atomically increment outside of the map, consider: + // - GR1 calls Clear, waits on Lock + // - GR2 calls Register, gets id 100, waits on lock + // - GR1 claims lock, resets highestID to 0, exits + // - GR2 claims lock, inserts id 100 in the map + // - ... later, register silently overwrites entity 100, its + // bindings will now panic on a bad type assertion + // + // Increment before assigning to preserve Global == caller 0 + cm.highestID++ + cm.callers[cm.highestID] = e + return cm.highestID +} + +// Get returns the entity corresponding to the given ID within +// the caller map. If no entity is found, it returns nil. +func (cm *CallerMap) GetEntity(id CallerID) Caller { + cm.callersLock.RLock() + defer cm.callersLock.RUnlock() + return cm.callers[id] +} + +// Has returns whether the given caller id is an initialized entity +// within the caller map. +func (cm *CallerMap) HasEntity(id CallerID) bool { + cm.callersLock.RLock() + defer cm.callersLock.RUnlock() + _, ok := cm.callers[id] + return ok +} + +// Remove removes an entity from the caller map. +func (cm *CallerMap) RemoveEntity(id CallerID) { + cm.callersLock.Lock() + delete(cm.callers, id) + cm.callersLock.Unlock() +} + +// Clear clears the caller map to forget all registered callers. +func (cm *CallerMap) Clear() { + cm.callersLock.Lock() + cm.highestID = 0 + cm.callers = map[CallerID]Caller{} + cm.callersLock.Unlock() } diff --git a/event/callerMap.go b/event/callerMap.go deleted file mode 100644 index 49512f7a..00000000 --- a/event/callerMap.go +++ /dev/null @@ -1,104 +0,0 @@ -package event - -import ( - "sync" - "sync/atomic" -) - -// A CallerMap tracks CID mappings to Entities. Its intended use is -// to be a source of truth within event bindings for what entity the -// binding is triggering on: -// var cm *event.CallerMap -// func(cid event.CID, payload interface{}) int { -// ent := cm.GetEntity(cid) -// f, ok := ent.(*Foo) -// if !ok { -// // bound to an unexpected entity type! -// return event.UnbindSingle -// } -// // ... -// } -// This is an alternative to passing in the entity via closure scoping, -// and allows for more general bindings as simple top level functions. -type CallerMap struct { - highestID *int64 - callersLock sync.RWMutex - callers map[CID]Entity -} - -// NewCallerMap creates a caller map. A CallerMap -// is not valid for use if not created via this function. -func NewCallerMap() *CallerMap { - return &CallerMap{ - highestID: new(int64), - callers: map[CID]Entity{}, - } -} - -// DefaultCallerMap is the caller map used by all event package caller -// functions. -var DefaultCallerMap = NewCallerMap() - -// NextID finds the next available caller id -// and returns it, after adding the given entity to -// the caller map. -func (cm *CallerMap) NextID(e Entity) CID { - nextID := atomic.AddInt64(cm.highestID, 1) - cm.callersLock.Lock() - cm.callers[CID(nextID)] = e - cm.callersLock.Unlock() - return CID(nextID) -} - -// GetEntity returns the entity corresponding to the given ID within -// the caller map. If no entity is found, it returns nil. -func (cm *CallerMap) GetEntity(id CID) Entity { - cm.callersLock.RLock() - defer cm.callersLock.RUnlock() - return cm.callers[id] -} - -// HasEntity returns whether the given caller id is an initialized entity -// within the caller map. -func (cm *CallerMap) HasEntity(id CID) bool { - cm.callersLock.RLock() - defer cm.callersLock.RUnlock() - _, ok := cm.callers[id] - return ok -} - -// DestroyEntity removes an entity from the caller map. -func (cm *CallerMap) DestroyEntity(id CID) { - cm.callersLock.Lock() - delete(cm.callers, id) - cm.callersLock.Unlock() -} - -// NextID finds the next available caller id -// and returns it, after adding the given entity to -// the default caller map. -func NextID(e Entity) CID { - return DefaultCallerMap.NextID(e) -} - -// GetEntity returns the entity corresponding to the given ID within -// the default caller map. If no entity is found, it returns nil. -func GetEntity(id CID) Entity { - return DefaultCallerMap.GetEntity(id) -} - -// HasEntity returns whether the given caller id is an initialized entity -// within the default caller map. -func HasEntity(id CID) bool { - return DefaultCallerMap.HasEntity(id) -} - -// DestroyEntity removes an entity from the default caller map. -func DestroyEntity(id CID) { - DefaultCallerMap.DestroyEntity(id) -} - -// ResetCallerMap resets the DefaultCallerMap to be empty. -func ResetCallerMap() { - *DefaultCallerMap = *NewCallerMap() -} diff --git a/event/caller_test.go b/event/caller_test.go new file mode 100644 index 00000000..4011981a --- /dev/null +++ b/event/caller_test.go @@ -0,0 +1,67 @@ +package event_test + +import ( + "math/rand" + "testing" + + "github.com/oakmound/oak/v4/event" +) + +func TestCallerID_CID(t *testing.T) { + t.Run("Identity", func(t *testing.T) { + c := event.CallerID(rand.Intn(100000)) + if c != c.CID() { + t.Fatalf("callerID did not match itself: was %v, got %v", c, c.CID()) + } + }) +} + +func TestNewCallerMap(t *testing.T) { + t.Run("NotNil", func(t *testing.T) { + m := event.NewCallerMap() + if m == nil { + t.Fatalf("created caller map was nil") + } + }) +} + +func TestCallerMap_Register(t *testing.T) { + t.Run("Basic", func(t *testing.T) { + m := event.NewCallerMap() + c1 := event.CallerID(rand.Intn(10000)) + id := m.Register(c1) + c2 := m.GetEntity(id) + if c2 != c1 { + t.Fatalf("unable to retrieve registered caller") + } + if !m.HasEntity(id) { + t.Fatalf("caller map does not have registered caller") + } + }) + t.Run("Remove", func(t *testing.T) { + m := event.NewCallerMap() + c1 := event.CallerID(rand.Intn(10000)) + id := m.Register(c1) + m.RemoveEntity(id) + c3 := m.GetEntity(id) + if c3 != nil { + t.Fatalf("get entity had registered caller after remove") + } + if m.HasEntity(id) { + t.Fatalf("caller map has registered caller after remove") + } + }) + t.Run("Clear", func(t *testing.T) { + m := event.NewCallerMap() + c1 := event.CallerID(rand.Intn(10000)) + id := m.Register(c1) + m.Clear() + c3 := m.GetEntity(id) + if c3 != nil { + t.Fatalf("get entity had registered caller after clear") + } + if m.HasEntity(id) { + t.Fatalf("caller map has registered caller after clear") + } + }) +} diff --git a/event/cid.go b/event/cid.go deleted file mode 100644 index 14a2946b..00000000 --- a/event/cid.go +++ /dev/null @@ -1,21 +0,0 @@ -package event - -// A CID is a caller ID that entities use to trigger and bind functionality -type CID int - -// E is shorthand for GetEntity(int(cid)) -func (cid CID) E() interface{} { - return GetEntity(cid) -} - -// Parse returns the given cid, or the entity's cid -// if the given cid is 0. This way, multiple entities can be -// composed together by passing 0 down to lower tiered constructors, so that -// the topmost entity is stored once and bind functions will -// bind to the topmost entity. -func (cid CID) Parse(e Entity) CID { - if cid == 0 { - return e.Init() - } - return cid -} diff --git a/event/default.go b/event/default.go index c9c7a277..9f82cd56 100644 --- a/event/default.go +++ b/event/default.go @@ -1,124 +1,15 @@ package event -// As in collision and mouse, default.go lists functions that -// only operate on DefaultBus, a package global bus. +// DefaultBus is a global Bus. It uses the DefaultCallerMap internally. It should not be used unless your program is only +// using a single Bus. Preferably multi-bus programs would create their own buses and caller maps specific to each bus's +// use. +var DefaultBus *Bus -var ( - // DefaultBus is a bus that has additional operations for CIDs, and can - // be called via event.Call as opposed to bus.Call - DefaultBus = NewBus(DefaultCallerMap) -) - -// Trigger an event, but only for one ID, on the default bus -func (cid CID) Trigger(eventName string, data interface{}) { - go func(eventName string, data interface{}) { - DefaultBus.mutex.RLock() - if idMap, ok := DefaultBus.bindingMap[eventName]; ok { - if bs, ok := idMap[cid]; ok { - DefaultBus.triggerDefault(bs.sl, cid, eventName, data) - } - } - DefaultBus.mutex.RUnlock() - }(eventName, data) -} - -// TriggerBus triggers an event with some payload for this cid against the provided bus. -func (cid CID) TriggerBus(eventName string, data interface{}, bus Handler) chan struct{} { - return bus.TriggerCIDBack(cid, eventName, data) -} - -// Bind on a CID is shorthand for bus.Bind(name, cid, fn), on the default bus. -func (cid CID) Bind(name string, fn Bindable) { - DefaultBus.Bind(name, cid, fn) -} - -// UnbindAll removes all events with the given cid from the event bus -func (cid CID) UnbindAll() { - DefaultBus.UnbindAll(Event{ - Name: "", - CallerID: cid, - }) -} - -// UnbindAllAndRebind on a CID is equivalent to bus.UnbindAllAndRebind(..., cid) -func (cid CID) UnbindAllAndRebind(binds []Bindable, events []string) { - DefaultBus.UnbindAllAndRebind(Event{ - Name: "", - CallerID: cid, - }, binds, cid, events) -} - -// Trigger calls Trigger on the DefaultBus -func Trigger(eventName string, data interface{}) { - DefaultBus.Trigger(eventName, data) -} - -// TriggerBack calls TriggerBack on the DefaultBus -func TriggerBack(eventName string, data interface{}) chan struct{} { - return DefaultBus.TriggerBack(eventName, data) -} - -// GlobalBind calls GlobalBind on the DefaultBus -func GlobalBind(name string, fn Bindable) { - DefaultBus.GlobalBind(name, fn) -} - -// UnbindAll calls UnbindAll on the DefaultBus -func UnbindAll(opt Event) { - DefaultBus.UnbindAll(opt) -} - -// UnbindAllAndRebind calls UnbindAllAndRebind on the DefaultBus -func UnbindAllAndRebind(bo Event, binds []Bindable, cid CID, events []string) { - DefaultBus.UnbindAllAndRebind(bo, binds, cid, events) -} - -// UnbindBindable calls UnbindBindable on the DefaultBus -func UnbindBindable(opt UnbindOption) { - DefaultBus.UnbindBindable(opt) -} - -// Bind calls Bind on the DefaultBus -func Bind(name string, callerID CID, fn Bindable) { - DefaultBus.Bind(name, callerID, fn) -} - -// Flush calls Flush on the DefaultBus -func Flush() error { - return DefaultBus.Flush() -} - -// FramesElapsed calls FramesElapsed on the DefaultBus -func FramesElapsed() int { - return DefaultBus.FramesElapsed() -} - -// Reset calls Reset on the DefaultBus -func Reset() { - DefaultBus.Reset() -} - -// ResolveChanges calls ResolveChanges on the DefaultBus -func ResolveChanges() { - DefaultBus.ResolveChanges() -} - -// SetTick calls SetTick on the DefaultBus -func SetTick(framerate int) error { - return DefaultBus.SetTick(framerate) -} +// DefaultCallerMap is a global CallerMap. It should not be used unless your program is only using a single CallerMap, +// or in other words definitely only has one event bus running at a time. +var DefaultCallerMap *CallerMap -// Stop calls Stop on the DefaultBus -func Stop() error { - return DefaultBus.Stop() -} - -// Update calls Update on the DefaultBus -func Update() error { - return DefaultBus.Update() -} - -// UpdateLoop calls UpdateLoop on the DefaultBus -func UpdateLoop(framerate int, updateCh chan struct{}) error { - return DefaultBus.UpdateLoop(framerate, updateCh) +func init() { + DefaultCallerMap = NewCallerMap() + DefaultBus = NewBus(DefaultCallerMap) } diff --git a/event/doc.go b/event/doc.go index af897110..33069b21 100644 --- a/event/doc.go +++ b/event/doc.go @@ -1,2 +1,2 @@ -// Package event provides structures to propagate event occurences to subscribed system entities. +// Package event provides structures to propagate event occurrences to subscribed system entities. package event diff --git a/event/entity.go b/event/entity.go deleted file mode 100644 index 50345953..00000000 --- a/event/entity.go +++ /dev/null @@ -1,53 +0,0 @@ -package event - -// An Entity is an element which can be bound to, -// in that it has a CID. All Entities need to implement -// is an Init function which should call NextID(e) and -// return that id: -// func (f *Foo) Init() event.CID { -// f.CID = event.NextID(f) -// return f.CID -// } -// In a multi-window setup each window may have its own -// callerMap, in which case event.NextID should be replaced -// with a NextID call on the appropriate callerMap. -type Entity interface { - Init() CID -} - -// Q: Why does every entity need its own implementation -// of Init()? Why can't it get that method definition -// from struct embedding? -// -// A: Because the CallerMap will store whatever struct is -// passed in to NextID. In a naive implementation: -// type A struct { -// DefaultEntity -// } -// -// type DefaultEntity struct { -// event.CID -// } -// -// func (de *DefaultEntity) Init() event.CID { -// de.CID = event.NextID(de) -// return de.CID -// } -// -// func main() { -// ... -// a := &A{} -// cid := a.Init() -// ent := event.GetEntity(cid) -// _, ok := ent.(*A) -// // ok is false, ent is type *DefaultEntity -// } -// -// So to effectively do this you would need something like: -// func DefaultEntity(parent interface{}) *DefaultEntity {} -// ... where the structure would store and pass down the parent. -// This introduces empty interfaces, would make initialization -// more difficult, and would use slightly more memory. -// -// Feel free to use this idea in your own implementations, but -// this package will not provide this structure at this time. diff --git a/event/entity_test.go b/event/entity_test.go deleted file mode 100644 index 212a2aff..00000000 --- a/event/entity_test.go +++ /dev/null @@ -1,12 +0,0 @@ -package event - -import ( - "testing" -) - -func TestGetEntityFails(t *testing.T) { - entity := GetEntity(100) - if entity != nil { - t.Fatalf("expected nil entity, got %v", entity) - } -} diff --git a/event/event_test.go b/event/event_test.go deleted file mode 100644 index 1f689c98..00000000 --- a/event/event_test.go +++ /dev/null @@ -1,249 +0,0 @@ -package event - -import ( - "testing" - "time" -) - -func sleep() { - // this is effectively "sync", or wait for the previous - // goroutine job to get its job done (Trigger - // use channels for 'done' signals because we don't want - // to enable users to wait on triggers that won't actually - // happen -because they are waiting- within a call that is - // holding a lock) - time.Sleep(200 * time.Millisecond) -} - -func TestBus(t *testing.T) { - triggers := 0 - go ResolveChanges() - GlobalBind("T", Empty(func() { - triggers++ - })) - sleep() - <-TriggerBack("T", nil) - if triggers != 1 { - t.Fatalf("first trigger did not happen") - } - Trigger("T", nil) - sleep() - if triggers != 2 { - t.Fatalf("second trigger did not happen") - } -} - -func TestUnbind(t *testing.T) { - triggers := 0 - go ResolveChanges() - GlobalBind("T", func(CID, interface{}) int { - triggers++ - return UnbindSingle - }) - sleep() - <-TriggerBack("T", nil) - if triggers != 1 { - t.Fatalf("first trigger did not happen") - } - sleep() - Trigger("T", nil) - sleep() - if triggers != 1 { - t.Fatalf("second trigger after unbind happened") - } - GlobalBind("T", func(CID, interface{}) int { - triggers++ - return 0 - }) - GlobalBind("T", func(CID, interface{}) int { - triggers++ - return UnbindEvent - }) - sleep() - Trigger("T", nil) - sleep() - if triggers != 3 { - t.Fatalf("global triggers did not happen") - } - - Trigger("T", nil) - sleep() - if triggers != 3 { - t.Fatalf("global triggers happened after unbind") - } - - GlobalBind("T", func(CID, interface{}) int { - triggers++ - return 0 - }) - sleep() - Trigger("T", nil) - sleep() - if triggers != 4 { - t.Fatalf("global triggers did not happen") - } - - Reset() - - Trigger("T", nil) - sleep() - if triggers != 4 { - t.Fatalf("global triggers did not unbind after reset") - } -} - -type ent struct{} - -func (e ent) Init() CID { - return NextID(e) -} - -func TestCID(t *testing.T) { - triggers := 0 - go ResolveChanges() - cid := CID(0).Parse(ent{}) - cid.Bind("T", func(CID, interface{}) int { - triggers++ - return 0 - }) - sleep() - cid.Trigger("T", nil) - sleep() - if triggers != 1 { - t.Fatalf("first trigger did not happen") - } - - // UnbindAllAndRebind - cid.UnbindAllAndRebind([]Bindable{ - func(CID, interface{}) int { - triggers-- - return 0 - }, - }, []string{ - "T", - }) - - sleep() - cid.Trigger("T", nil) - sleep() - if triggers != 0 { - t.Fatalf("second trigger did not happen") - } - - // UnbindAll - cid.UnbindAll() - - sleep() - cid.Trigger("T", nil) - sleep() - if triggers != 0 { - t.Fatalf("second trigger did not unbind") - } - - cid.Bind("T", func(CID, interface{}) int { - panic("Should not have been triggered") - }) - - // ResetEntities, etc - ResetCallerMap() - - cid.Trigger("T", nil) - sleep() -} - -func TestEntity(t *testing.T) { - go ResolveChanges() - e := ent{} - cid := e.Init() - cid2 := cid.Parse(e) - if cid != cid2 { - t.Fatalf("expected id %v got %v", cid, cid2) - } - if _, ok := cid.E().(ent); !ok { - t.Fatalf("cid entity was not present") - } - DestroyEntity(cid) - if cid.E() != nil { - t.Fatalf("cid entity was not deleted") - } -} - -var ( - ubTriggers int -) - -func TestUnbindBindable(t *testing.T) { - go ResolveChanges() - GlobalBind("T", tBinding) - sleep() - Trigger("T", nil) - sleep() - if ubTriggers != 1 { - t.Fatalf("first trigger did not happen") - } - // Fix this syntax - UnbindBindable( - UnbindOption{ - Event: Event{ - Name: "T", - CallerID: 0, - }, - Fn: tBinding, - }, - ) - sleep() - Trigger("T", nil) - sleep() - if ubTriggers != 1 { - t.Fatalf("unbind call did not unbind trigger") - } -} - -func tBinding(CID, interface{}) int { - ubTriggers++ - return 0 -} - -func TestBindableList(t *testing.T) { - bl := new(bindableList) - bl.sl = make([]Bindable, 10) - bl.removeIndex(11) - bl.sl[2] = tBinding - bl.removeBindable(tBinding) - // Assert nothing panicked -} - -func TestUnbindAllAndRebind(t *testing.T) { - go ResolveChanges() - UnbindAllAndRebind( - Event{ - Name: "T", - CallerID: 0, - }, []Bindable{}, 0, []string{}) -} - -func TestBindingSet(t *testing.T) { - triggers := 0 - bs := BindingSet{} - bs.Set("one", map[string]Bindable{ - "T": func(CID, interface{}) int { - triggers++ - return 0 - }, - "P": func(CID, interface{}) int { - triggers *= 2 - return 0 - }, - }) - e := ent{} - cid := e.Init() - cid.RebindMapping(bs["one"]) - sleep() - cid.Trigger("T", nil) - sleep() - cid.Trigger("P", nil) - sleep() - if triggers != 2 { - t.Fatalf("triggers did not happen") - } -} diff --git a/event/events.go b/event/events.go new file mode 100644 index 00000000..4a90f2a2 --- /dev/null +++ b/event/events.go @@ -0,0 +1,39 @@ +package event + +import ( + "sync/atomic" + "time" +) + +// An UnsafeEventID is a non-typed eventID. EventIDs are just these, with type information attached. +type UnsafeEventID int64 + +// A EventID represents an event associated with a given payload type. +type EventID[T any] struct { + UnsafeEventID +} + +var ( + nextEventID int64 +) + +// RegisterEvent returns a unique ID to associate an event with. EventIDs not created through RegisterEvent are +// not valid for use in type-safe bindings. +func RegisterEvent[T any]() EventID[T] { + id := atomic.AddInt64(&nextEventID, 1) + return EventID[T]{ + UnsafeEventID: UnsafeEventID(id), + } +} + +// EnterPayload is the payload sent down to Enter bindings +type EnterPayload struct { + FramesElapsed int + SinceLastFrame time.Duration + TickPercent float64 +} + +var ( + // Enter: the beginning of every logical frame. + Enter = RegisterEvent[EnterPayload]() +) diff --git a/event/handler.go b/event/handler.go index 6088763a..6a28fd03 100644 --- a/event/handler.go +++ b/event/handler.go @@ -1,12 +1,5 @@ package event -import ( - "math" - "time" - - "github.com/oakmound/oak/v3/timing" -) - var ( _ Handler = &Bus{} ) @@ -15,162 +8,14 @@ var ( // for use in oak internally, and thus the functions that need to be replaced // by alternative event handlers. type Handler interface { - WaitForEvent(name string) <-chan interface{} - // - UpdateLoop(framerate int, updateCh chan struct{}) error - FramesElapsed() int - SetTick(framerate int) error - Update() error - Flush() error - Stop() error Reset() - SetRefreshRate(time.Duration) - // - Trigger(event string, data interface{}) - TriggerBack(event string, data interface{}) chan struct{} - TriggerCIDBack(cid CID, eventName string, data interface{}) chan struct{} - // - Pause() - Resume() - // - Bind(string, CID, Bindable) - GlobalBind(string, Bindable) - UnbindAll(Event) - UnbindAllAndRebind(Event, []Bindable, CID, []string) - UnbindBindable(UnbindOption) -} - -// A CallerMapper has an internal caller map that can be set. -type CallerMapper interface { + TriggerForCaller(cid CallerID, event UnsafeEventID, data interface{}) <-chan struct{} + Trigger(event UnsafeEventID, data interface{}) <-chan struct{} + UnsafeBind(UnsafeEventID, CallerID, UnsafeBindable) Binding + Unbind(Binding) <-chan struct{} + UnbindAllFrom(CallerID) <-chan struct{} SetCallerMap(*CallerMap) -} - -// A PersistentBinder can persist bindings through bus resets -type PersistentBinder interface { - PersistentBind(string, CID, Bindable) + GetCallerMap() *CallerMap + PersistentBind(eventID UnsafeEventID, callerID CallerID, fn UnsafeBindable) Binding ClearPersistentBindings() } - -// UpdateLoop is expected to internally call Update() -// or do something equivalent at the given frameRate, -// sending signals to the sceneCh after each Update(). -// Any flushing should be done as needed. This should -// not be called with `go`, if this requires goroutines -// it should create them itself. -// UpdateLoop is expected separately from Update() and -// Flush() because it will be more efficient for a Logical -// System to perform its own Updates outside of it’s exposed -// interface. -func (eb *Bus) UpdateLoop(framerate int, updateCh chan struct{}) error { - // The logical loop. - // In order, it waits on receiving a signal to begin a logical frame. - // It then runs any functions bound to when a frame begins. - // It then allows a scene to perform it's loop operation. - eb.framesElapsed = 0 - eb.framerate = framerate - frameDelay := timing.FPSToFrameDelay(framerate) - if eb.Ticker == nil { - eb.Ticker = time.NewTicker(frameDelay) - } - go eb.ResolveChanges() - go func() { - eb.Ticker.Reset(frameDelay) - frameDelayF64 := float64(frameDelay) - lastTick := time.Now() - for { - select { - case now := <-eb.Ticker.C: - deltaTime := now.Sub(lastTick) - lastTick = now - <-eb.TriggerBack(Enter, EnterPayload{ - FramesElapsed: eb.framesElapsed, - SinceLastFrame: deltaTime, - TickPercent: float64(deltaTime) / frameDelayF64, - }) - eb.framesElapsed++ - select { - case updateCh <- struct{}{}: - case <-eb.doneCh: - return - } - case <-eb.doneCh: - return - } - } - }() - return nil -} - -// EnterPayload is the payload sent down to Enter bindings -type EnterPayload struct { - FramesElapsed int - SinceLastFrame time.Duration - TickPercent float64 -} - -// Update updates all entities bound to this handler -func (eb *Bus) Update() error { - <-eb.TriggerBack(Enter, EnterPayload{ - FramesElapsed: eb.framesElapsed, - }) - return nil -} - -// Flush refreshes any changes to the Handler’s bindings. -func (eb *Bus) Flush() error { - if len(eb.unbindAllAndRebinds) > 0 { - eb.resolveUnbindAllAndRebinds() - } - // Specific unbinds - if len(eb.unbinds) > 0 { - eb.resolveUnbinds() - } - - // A full set of unbind settings - if len(eb.fullUnbinds) > 0 { - eb.resolveFullUnbinds() - } - - // A partial set of unbind settings - if len(eb.partUnbinds) > 0 { - eb.resolvePartialUnbinds() - } - - // Bindings - if len(eb.binds) > 0 { - eb.resolveBindings() - } - return nil -} - -// Stop ceases anything spawned by an ongoing UpdateLoop -func (eb *Bus) Stop() error { - if eb.Ticker != nil { - eb.Ticker.Stop() - } - eb.doneCh <- struct{}{} - return nil -} - -// Pause stops the event bus from running any further enter events -func (eb *Bus) Pause() { - eb.Ticker.Reset(math.MaxInt32 * time.Second) -} - -// Resume will resume emitting enter events -func (eb *Bus) Resume() { - eb.Ticker.Reset(timing.FPSToFrameDelay(eb.framerate)) -} - -// FramesElapsed returns how many frames have elapsed since UpdateLoop was last called. -func (eb *Bus) FramesElapsed() int { - return eb.framesElapsed -} - -// SetTick optionally updates the Logical System’s tick rate -// (while it is looping) to be frameRate. If this operation is not -// supported, it should return an error. -func (eb *Bus) SetTick(framerate int) error { - eb.Ticker.Reset(timing.FPSToFrameDelay(framerate)) - return nil -} diff --git a/event/handler_test.go b/event/handler_test.go deleted file mode 100644 index 9c5e0d1b..00000000 --- a/event/handler_test.go +++ /dev/null @@ -1,108 +0,0 @@ -package event - -import ( - "testing" - "time" -) - -func TestHandler(t *testing.T) { - updateCh := make(chan struct{}) - if UpdateLoop(60, updateCh) != nil { - t.Fatalf("UpdateLoop failed") - } - triggers := 0 - Bind(Enter, 0, func(CID, interface{}) int { - triggers++ - return 0 - }) - sleep() - if triggers != 1 { - t.Fatalf("expected update loop to increment triggers") - } - <-updateCh - sleep() - if triggers != 2 { - t.Fatalf("expected update loop to increment triggers") - } - if FramesElapsed() != 2 { - t.Fatalf("expected 2 update frames to have elapsed") - } - if SetTick(1) != nil { - t.Fatalf("SetTick failed") - } - <-updateCh - if Stop() != nil { - t.Fatalf("Stop failed") - } - sleep() - sleep() - select { - case <-updateCh: - t.Fatal("Handler should be closed") - default: - } - expectedTriggers := triggers + 1 - if Update() != nil { - t.Fatalf("Update failed") - } - sleep() - - if triggers != expectedTriggers { - t.Fatalf("expected update to increment triggers") - } - if Flush() != nil { - t.Fatalf("Flush failed") - } - - Flush() - sleep() - if Update() != nil { - t.Fatalf("final Update failed") - } - sleep() - sleep() - Reset() -} - -func BenchmarkHandler(b *testing.B) { - triggers := 0 - entities := 10 - go DefaultBus.ResolveChanges() - for i := 0; i < entities; i++ { - DefaultBus.GlobalBind(Enter, func(CID, interface{}) int { - triggers++ - return 0 - }) - } - b.ResetTimer() - for i := 0; i < b.N; i++ { - <-DefaultBus.TriggerBack(Enter, DefaultBus.framesElapsed) - } -} - -func TestPauseAndResume(t *testing.T) { - b := NewBus(nil) - b.ResolveChanges() - triggerCt := 0 - b.Bind("EnterFrame", 0, func(CID, interface{}) int { - triggerCt++ - return 0 - }) - ch := make(chan struct{}, 1000) - b.UpdateLoop(60, ch) - time.Sleep(1 * time.Second) - b.Pause() - time.Sleep(1 * time.Second) - oldCt := triggerCt - time.Sleep(1 * time.Second) - if oldCt != triggerCt { - t.Fatalf("pause did not stop enter frame from triggering: expected %v got %v", oldCt, triggerCt) - } - - b.Resume() - time.Sleep(1 * time.Second) - newCt := triggerCt - if newCt == oldCt { - t.Fatalf("resume did not resume enter frame triggering: expected %v got %v", oldCt, newCt) - } -} diff --git a/event/internal.go b/event/internal.go new file mode 100644 index 00000000..ea692834 --- /dev/null +++ b/event/internal.go @@ -0,0 +1,48 @@ +package event + +import ( + "sync" +) + +type bindableList map[BindID]UnsafeBindable + +func (eb *Bus) getBindableList(eventID UnsafeEventID, callerID CallerID) bindableList { + if m := eb.bindingMap[eventID]; m == nil { + eb.bindingMap[eventID] = make(map[CallerID]bindableList) + bl := make(bindableList) + eb.bindingMap[eventID][callerID] = bl + return bl + } + bl := eb.bindingMap[eventID][callerID] + if bl == nil { + bl = make(bindableList) + eb.bindingMap[eventID][callerID] = bl + } + return bl +} + +func (bus *Bus) trigger(binds bindableList, eventID UnsafeEventID, callerID CallerID, data interface{}) { + wg := &sync.WaitGroup{} + wg.Add(len(binds)) + for bindID, bnd := range binds { + bindID := bindID + bnd := bnd + go func() { + if callerID == Global || bus.callerMap.HasEntity(callerID) { + response := bnd(callerID, bus, data) + switch response { + case ResponseUnbindThisBinding: + // Q: Why does this call bus.Unbind when it already has the event index to delete? + // A: This goroutine does not own a write lock on the bus, and should therefore + // not modify its contents. We do not have a simple way of promoting our read lock + // to a write lock. + bus.Unbind(Binding{EventID: eventID, CallerID: callerID, BindID: bindID}) + case ResponseUnbindThisCaller: + bus.UnbindAllFrom(callerID) + } + } + wg.Done() + }() + } + wg.Wait() +} diff --git a/event/resolve.go b/event/resolve.go deleted file mode 100644 index 847d6d82..00000000 --- a/event/resolve.go +++ /dev/null @@ -1,135 +0,0 @@ -package event - -import "time" - -// ResolveChanges is a constant loop that tracks slices of bind or unbind calls -// and resolves them individually such that they don't break the bus. -// Each section of the loop waits for the predetermined refreshrate prior to attempting to flush. -// -// If you ask "Why does this not use select over channels, share memory by communicating", -// the answer is we tried, and it was cripplingly slow. -func (eb *Bus) ResolveChanges() { - eb.init.Do(func() { - go func() { - for { - time.Sleep(eb.refreshRate) - eb.Flush() - } - }() - }) -} - -// SetRefreshRate on the event bus detailing the time to wait per attempt to ResolveChanges. -func (eb *Bus) SetRefreshRate(refreshRate time.Duration) { - eb.refreshRate = refreshRate -} - -func (eb *Bus) resolveUnbindAllAndRebinds() { - eb.mutex.Lock() - eb.pendingMutex.Lock() - for _, ubaarb := range eb.unbindAllAndRebinds { - unbind := ubaarb.ub - orderedBindables := ubaarb.bnds - orderedBindOptions := ubaarb.bs - - var namekeys []string - // If we were given a name, - // we'll just iterate with that name. - if unbind.Name != "" { - namekeys = append(namekeys, unbind.Name) - // Otherwise, iterate through all events. - } else { - for k := range eb.bindingMap { - namekeys = append(namekeys, k) - } - } - - if unbind.CallerID != 0 { - for _, k := range namekeys { - delete(eb.bindingMap[k], unbind.CallerID) - } - } else { - for _, k := range namekeys { - delete(eb.bindingMap, k) - } - } - - // Bindings - for i := 0; i < len(orderedBindables); i++ { - fn := orderedBindables[i] - opt := orderedBindOptions[i] - list := eb.getBindableList(opt) - list.storeBindable(fn) - } - } - eb.unbindAllAndRebinds = []UnbindAllOption{} - eb.pendingMutex.Unlock() - eb.mutex.Unlock() -} - -func (eb *Bus) resolveUnbinds() { - eb.mutex.Lock() - eb.pendingMutex.Lock() - for _, bnd := range eb.unbinds { - eb.getBindableList(bnd.Event).removeBinding(bnd) - } - eb.unbinds = []binding{} - eb.pendingMutex.Unlock() - eb.mutex.Unlock() -} - -func (eb *Bus) resolveFullUnbinds() { - eb.mutex.Lock() - eb.pendingMutex.Lock() - for _, opt := range eb.fullUnbinds { - eb.getBindableList(opt.Event).removeBindable(opt.Fn) - } - eb.fullUnbinds = []UnbindOption{} - eb.pendingMutex.Unlock() - eb.mutex.Unlock() -} - -func (eb *Bus) resolvePartialUnbinds() { - eb.mutex.Lock() - eb.pendingMutex.Lock() - for _, opt := range eb.partUnbinds { - var namekeys []string - - // If we were given a name, - // we'll just iterate with that name. - if opt.Name != "" { - namekeys = append(namekeys, opt.Name) - - // Otherwise, iterate through all events. - } else { - for k := range eb.bindingMap { - namekeys = append(namekeys, k) - } - } - - if opt.CallerID != 0 { - for _, k := range namekeys { - delete(eb.bindingMap[k], opt.CallerID) - } - } else { - for _, k := range namekeys { - delete(eb.bindingMap, k) - } - } - } - eb.partUnbinds = []Event{} - eb.pendingMutex.Unlock() - eb.mutex.Unlock() -} - -func (eb *Bus) resolveBindings() { - eb.mutex.Lock() - eb.pendingMutex.Lock() - for _, bindSet := range eb.binds { - list := eb.getBindableList(bindSet.Event) - list.storeBindable(bindSet.Fn) - } - eb.binds = []UnbindOption{} - eb.pendingMutex.Unlock() - eb.mutex.Unlock() -} diff --git a/event/resolve_test.go b/event/resolve_test.go deleted file mode 100644 index 58cbc97c..00000000 --- a/event/resolve_test.go +++ /dev/null @@ -1,23 +0,0 @@ -package event - -import ( - "testing" - "time" -) - -func TestResolveChangesWithRefreshRate(t *testing.T) { - b := NewBus(nil) - b.SetRefreshRate(6 * time.Second) - b.ResolveChanges() - failed := false - b.Bind("EnterFrame", 0, func(CID, interface{}) int { - failed = true - return 0 - }) - ch := make(chan struct{}, 1000) - b.UpdateLoop(60, ch) - time.Sleep(3 * time.Second) - if failed { - t.Fatal("binding was called before refresh rate should have added binding") - } -} diff --git a/event/response.go b/event/response.go index 336cd389..a4391b4a 100644 --- a/event/response.go +++ b/event/response.go @@ -1,20 +1,16 @@ package event -// Response types from bindables -// reponses are not their own type because func(event.CID, interface{}) int -// is easier to write than func(event.CID, interface{}) event.Response. This may -// yet change. +type Response uint8 + +// Response types for bindables const ( - // NoResponse or 0, is returned by events that + // ResponseNone or 0, is returned by events that // don't want the event bus to do anything with // the event after they have been evaluated. This // is the usual behavior. - NoResponse = iota - // UnbindEvent unbinds everything for a specific - // event name from an entity at the bindable's - // priority. - UnbindEvent - // UnbindSingle just unbinds the one binding that - // it is returned from - UnbindSingle + ResponseNone Response = iota + // ResponseUnbindThisBinding unbinds the one binding that returns it. + ResponseUnbindThisBinding + // ResponseUnbindThisCaller unbinds all of a caller's bindings when returned from any binding. + ResponseUnbindThisCaller ) diff --git a/event/response_test.go b/event/response_test.go new file mode 100644 index 00000000..4d3616ec --- /dev/null +++ b/event/response_test.go @@ -0,0 +1,57 @@ +package event_test + +import ( + "sync/atomic" + "testing" + "time" + + "github.com/oakmound/oak/v4/event" +) + +func TestBindingResponses(t *testing.T) { + t.Run("UnbindThisBinding", func(t *testing.T) { + b := event.NewBus(event.NewCallerMap()) + + var calls int32 + b1 := b.UnsafeBind(1, 0, func(ci event.CallerID, h event.Handler, i interface{}) event.Response { + atomic.AddInt32(&calls, 1) + return event.ResponseUnbindThisBinding + }) + <-b1.Bound + <-b.Trigger(1, nil) + if calls != 1 { + t.Fatal(expectedError("calls", 1, calls)) + } + // we do not get a signal for when this unbinding is finished + time.Sleep(1 * time.Second) + <-b.Trigger(1, nil) + if calls != 1 { + t.Fatal(expectedError("calls", 1, calls)) + } + }) + t.Run("UNbindThisCaller", func(t *testing.T) { + b := event.NewBus(event.NewCallerMap()) + + var calls int32 + b1 := b.UnsafeBind(1, 0, func(ci event.CallerID, h event.Handler, i interface{}) event.Response { + atomic.AddInt32(&calls, 1) + return event.ResponseUnbindThisCaller + }) + <-b1.Bound + b2 := b.UnsafeBind(1, 0, func(ci event.CallerID, h event.Handler, i interface{}) event.Response { + atomic.AddInt32(&calls, 1) + return 0 + }) + <-b2.Bound + <-b.Trigger(1, nil) + if calls != 2 { + t.Fatal(expectedError("calls", 1, calls)) + } + // we do not get a signal for when this unbinding is finished + time.Sleep(1 * time.Second) + <-b.Trigger(1, nil) + if calls != 2 { + t.Fatal(expectedError("calls", 2, calls)) + } + }) +} diff --git a/event/strings.go b/event/strings.go deleted file mode 100644 index 0c4885fa..00000000 --- a/event/strings.go +++ /dev/null @@ -1,50 +0,0 @@ -package event - -// Oak uses the following built in events: -// -// - CollisionStart/Stop: when a PhaseCollision entity starts/stops touching some label. -// Payload: (collision.Label) the label the entity has started/stopped touching -// -// - MouseCollisionStart/Stop: as above, for mouse collision -// Payload: (*mouse.Event) -// -// - Mouse events: MousePress, MouseRelease, MouseScrollDown, MouseScrollUp, MouseDrag -// Payload: (*mouse.Event) details on the mouse event -// -// - KeyDown, KeyDown$a: when any key is pressed down, when key $a is pressed down. -// Payload: (key.Event) the key pressed -// -// - KeyUp, KeyUp$a: when any key is released, when key $a is released. -// Payload: (key.Event) the key released -// -// And the following: -const ( - // Enter : the beginning of every logical frame. - // Payload: (EnterPayload) details on the frame and time since last tick - Enter = "EnterFrame" - // AnimationEnd: Triggered on animations CIDs when they loop from the last to the first frame - // Payload: nil - AnimationEnd = "AnimationEnd" - // ViewportUpdate: Triggered when the position of of the viewport changes - // Payload: intgeom.Point2 - ViewportUpdate = "ViewportUpdate" - // OnStop: Triggered when the engine is stopped. - // Payload: nil - OnStop = "OnStop" - // FocusGain: Triggered when the window gains focus - // Payload: nil - FocusGain = "FocusGain" - // FocusLoss: Triggered when the window loses focus - // Payload: nil - FocusLoss = "FocusLoss" - // InputChange: triggered when the most recent input device changes (e.g. keyboard to joystick or vice versa) - // Payload: oak.InputType - InputChange = "InputChange" -) - -// -// Note all events built in to oak are CapitalizedCamelCase. Although our adding of new -// built in events is rare, we don't consider the addition of these events breaking -// changes for versioning. If a game has many events with generalized names, making -// them uncapitalizedCamelCase is perhaps the best approach to guarantee that builtin -// event names will never conflict with custom events. diff --git a/event/trigger.go b/event/trigger.go index b80184e3..cf842fec 100644 --- a/event/trigger.go +++ b/event/trigger.go @@ -1,99 +1,45 @@ package event -import ( - "sync" -) - -// TriggerBack is a version of Trigger which returns a channel that -// informs on when all bindables have been called and returned from -// the input event. It is dangerous to use this unless you have a -// very good idea how things will synchronize, as if a triggered -// bindable itself makes a TriggerBack call, this will cause the engine to freeze, -// as the function will never end because the first TriggerBack has control of -// the lock for the event bus, and the first TriggerBack won't give up that lock -// until the function ends. -// -// This inherently means that when you call Trigger, the event will almost -// almost never be immediately triggered but rather will be triggered sometime -// soon in the future. -// -// TriggerBack is right now used by the primary logic loop to dictate logical -// framerate, so EnterFrame events are called through TriggerBack. -func (eb *Bus) TriggerBack(eventName string, data interface{}) chan struct{} { +// TriggerForCaller acts like Trigger, but will only trigger for the given caller. +func (bus *Bus) TriggerForCaller(callerID CallerID, eventID UnsafeEventID, data interface{}) <-chan struct{} { + if callerID == Global { + return bus.Trigger(eventID, data) + } ch := make(chan struct{}) - go func(ch chan struct{}, eb *Bus, eventName string, data interface{}) { - eb.trigger(eventName, data) + go func() { + bus.mutex.RLock() + if idMap, ok := bus.bindingMap[eventID]; ok { + if bs, ok := idMap[callerID]; ok { + bus.trigger(bs, eventID, callerID, data) + } + } + bus.mutex.RUnlock() close(ch) - }(ch, eb, eventName, data) + }() return ch } // Trigger will scan through the event bus and call all bindables found attached // to the given event, with the passed in data. -func (eb *Bus) Trigger(eventName string, data interface{}) { - go func(eb *Bus, eventName string, data interface{}) { - eb.trigger(eventName, data) - }(eb, eventName, data) -} - -// TriggerCIDBack acts like trigger back, but triggers for a specific cid only. -func (eb *Bus) TriggerCIDBack(cid CID, eventName string, data interface{}) chan struct{} { +func (bus *Bus) Trigger(eventID UnsafeEventID, data interface{}) <-chan struct{} { ch := make(chan struct{}) go func() { - eb.mutex.RLock() - if idMap, ok := eb.bindingMap[eventName]; ok { - if bs, ok := idMap[cid]; ok { - eb.triggerDefault(bs.sl, cid, eventName, data) - } + bus.mutex.RLock() + for callerID, bs := range bus.bindingMap[eventID] { + bus.trigger(bs, eventID, callerID, data) } - eb.mutex.RUnlock() + bus.mutex.RUnlock() close(ch) }() return ch } -func (eb *Bus) trigger(eventName string, data interface{}) { - eb.mutex.RLock() - for id, bs := range eb.bindingMap[eventName] { - eb.triggerDefault(bs.sl, id, eventName, data) - } - eb.mutex.RUnlock() -} - -func (eb *Bus) triggerDefault(sl []Bindable, id CID, eventName string, data interface{}) { - prog := &sync.WaitGroup{} - prog.Add(len(sl)) - for i, bnd := range sl { - if bnd == nil { - prog.Done() - continue - } - go func(bnd Bindable, id CID, eventName string, data interface{}, prog *sync.WaitGroup, index int) { - eb.handleBindable(bnd, id, data, index, eventName) - prog.Done() - }(bnd, id, eventName, data, prog, i) - } - prog.Wait() +// TriggerOn calls Trigger with a strongly typed event. +func TriggerOn[T any](b Handler, ev EventID[T], data T) <-chan struct{} { + return b.Trigger(ev.UnsafeEventID, data) } -func (eb *Bus) handleBindable(bnd Bindable, id CID, data interface{}, index int, eventName string) { - if id == 0 || eb.callerMap.HasEntity(id) { - response := bnd(id, data) - switch response { - case UnbindEvent: - UnbindAll(Event{ - Name: eventName, - CallerID: id, - }) - case UnbindSingle: - bnd := binding{ - Event: Event{ - Name: eventName, - CallerID: id, - }, - index: index, - } - bnd.unbind(eb) - } - } +// TriggerForCallerOn calls TriggerForCaller with a strongly typed event. +func TriggerForCallerOn[T any](b Handler, cid CallerID, ev EventID[T], data T) <-chan struct{} { + return b.TriggerForCaller(cid, ev.UnsafeEventID, data) } diff --git a/event/trigger_test.go b/event/trigger_test.go new file mode 100644 index 00000000..aa233564 --- /dev/null +++ b/event/trigger_test.go @@ -0,0 +1,254 @@ +package event_test + +import ( + "fmt" + "math/rand" + "os" + "testing" + "time" + + "github.com/oakmound/oak/v4/event" +) + +func TestMain(m *testing.M) { + rand.Seed(time.Now().UnixNano()) + os.Exit(m.Run()) +} + +func TestBus_TriggerForCaller(t *testing.T) { + t.Run("NoBinding", func(t *testing.T) { + b := event.NewBus(event.NewCallerMap()) + id := event.UnsafeEventID(rand.Intn(100000)) + ch := b.TriggerForCaller(0, id, nil) + select { + case <-time.After(50 * time.Millisecond): + t.Fatal("timeout waiting for trigger to close channel") + case <-ch: + } + }) + t.Run("GlobalWithBinding", func(t *testing.T) { + b := event.NewBus(event.NewCallerMap()) + id := event.UnsafeEventID(rand.Intn(100000)) + errs := make(chan error) + binding := b.UnsafeBind(id, 0, func(ci event.CallerID, h event.Handler, i interface{}) event.Response { + defer close(errs) + if ci != 0 { + errs <- expectedError("callerID", 0, ci) + } + if h != b { + errs <- expectedError("bus", b, h) + } + if i != nil { + errs <- expectedError("payload", nil, i) + } + return 0 + }) + select { + case <-time.After(50 * time.Millisecond): + t.Fatal("timeout waiting for bind to close channel") + case <-binding.Bound: + } + ch := b.TriggerForCaller(0, id, nil) + select { + case <-time.After(50 * time.Millisecond): + t.Fatal("timeout waiting for trigger to close channel") + case <-ch: + } + for err := range errs { + t.Error(err) + } + }) + t.Run("WithMissingCallerID", func(t *testing.T) { + b := event.NewBus(event.NewCallerMap()) + id := event.UnsafeEventID(rand.Intn(100000)) + callerID := event.CallerID(rand.Intn(100000)) + errs := make(chan error) + binding := b.UnsafeBind(id, callerID, func(ci event.CallerID, h event.Handler, i interface{}) event.Response { + errs <- fmt.Errorf("binding should not be triggered") + return 0 + }) + _ = binding + select { + case <-time.After(50 * time.Millisecond): + t.Fatal("timeout waiting for bind to close channel") + case <-binding.Bound: + } + ch := b.TriggerForCaller(callerID, id, nil) + select { + case <-time.After(50 * time.Millisecond): + t.Fatal("timeout waiting for trigger to close channel") + case <-ch: + } + select { + case err := <-errs: + t.Error(err) + default: + } + }) + t.Run("WithValidCallerID", func(t *testing.T) { + b := event.NewBus(event.NewCallerMap()) + var cid event.CallerID + callerID := b.GetCallerMap().Register(cid) + id := event.UnsafeEventID(rand.Intn(100000)) + errs := make(chan error) + binding := b.UnsafeBind(id, callerID, func(ci event.CallerID, h event.Handler, i interface{}) event.Response { + defer close(errs) + if ci != callerID { + errs <- expectedError("callerID", callerID, ci) + } + if h != b { + errs <- expectedError("bus", b, h) + } + if i != nil { + errs <- expectedError("payload", nil, i) + } + return 0 + }) + _ = binding + select { + case <-time.After(50 * time.Millisecond): + t.Fatal("timeout waiting for bind to close channel") + case <-binding.Bound: + } + ch := b.TriggerForCaller(callerID, id, nil) + select { + case <-time.After(50 * time.Millisecond): + t.Fatal("timeout waiting for trigger to close channel") + case <-ch: + } + for err := range errs { + t.Error(err) + } + }) +} + +func TestBus_Trigger(t *testing.T) { + t.Run("NoBinding", func(t *testing.T) { + b := event.NewBus(event.NewCallerMap()) + id := event.UnsafeEventID(rand.Intn(100000)) + ch := b.Trigger(id, nil) + select { + case <-time.After(50 * time.Millisecond): + t.Fatal("timeout waiting for trigger to close channel") + case <-ch: + } + }) + t.Run("GlobalWithBinding", func(t *testing.T) { + b := event.NewBus(event.NewCallerMap()) + id := event.UnsafeEventID(rand.Intn(100000)) + errs := make(chan error) + binding := b.UnsafeBind(id, 0, func(ci event.CallerID, h event.Handler, i interface{}) event.Response { + defer close(errs) + if ci != 0 { + errs <- expectedError("callerID", 0, ci) + } + if h != b { + errs <- expectedError("bus", b, h) + } + if i != nil { + errs <- expectedError("payload", nil, i) + } + return 0 + }) + _ = binding + select { + case <-time.After(50 * time.Millisecond): + t.Fatal("timeout waiting for bind to close channel") + case <-binding.Bound: + } + ch := b.Trigger(id, nil) + select { + case <-time.After(50 * time.Millisecond): + t.Fatal("timeout waiting for trigger to close channel") + case <-ch: + } + for err := range errs { + t.Error(err) + } + }) + t.Run("WithMissingCallerID", func(t *testing.T) { + b := event.NewBus(event.NewCallerMap()) + id := event.UnsafeEventID(rand.Intn(100000)) + callerID := rand.Intn(100000) + errs := make(chan error) + binding := b.UnsafeBind(id, event.CallerID(callerID), func(ci event.CallerID, h event.Handler, i interface{}) event.Response { + errs <- fmt.Errorf("binding should not be triggered") + return 0 + }) + _ = binding + select { + case <-time.After(50 * time.Millisecond): + t.Fatal("timeout waiting for bind to close channel") + case <-binding.Bound: + } + ch := b.Trigger(id, nil) + select { + case <-time.After(50 * time.Millisecond): + t.Fatal("timeout waiting for trigger to close channel") + case <-ch: + } + select { + case err := <-errs: + t.Error(err) + default: + } + }) + t.Run("WithValidCallerID", func(t *testing.T) { + b := event.NewBus(event.NewCallerMap()) + var cid event.CallerID + callerID := b.GetCallerMap().Register(cid) + id := event.UnsafeEventID(rand.Intn(100000)) + errs := make(chan error) + binding := b.UnsafeBind(id, event.CallerID(callerID), func(ci event.CallerID, h event.Handler, i interface{}) event.Response { + defer close(errs) + if ci != callerID { + errs <- expectedError("callerID", callerID, ci) + } + if h != b { + errs <- expectedError("bus", b, h) + } + if i != nil { + errs <- expectedError("payload", nil, i) + } + return 0 + }) + _ = binding + select { + case <-time.After(50 * time.Millisecond): + t.Fatal("timeout waiting for bind to close channel") + case <-binding.Bound: + } + ch := b.Trigger(id, nil) + select { + case <-time.After(50 * time.Millisecond): + t.Fatal("timeout waiting for trigger to close channel") + case <-ch: + } + for err := range errs { + t.Error(err) + } + }) +} + +// TriggerOn and TriggerForCallerOn are simple wrappers of the tested methods above, so +// they are not tested thoroughly. + +func TestTriggerOn(t *testing.T) { + t.Run("SuperficialCoverage", func(t *testing.T) { + b := event.NewBus(event.NewCallerMap()) + eventID := event.RegisterEvent[struct{}]() + event.TriggerOn(b, eventID, struct{}{}) + }) +} + +func TestTriggerForCallerOn(t *testing.T) { + t.Run("SuperficialCoverage", func(t *testing.T) { + b := event.NewBus(event.NewCallerMap()) + eventID := event.RegisterEvent[struct{}]() + event.TriggerForCallerOn(b, 0, eventID, struct{}{}) + }) +} + +func expectedError(name string, expected, got interface{}) error { + return fmt.Errorf("expected %s to be %v, got %v", name, expected, got) +} diff --git a/event/unbind.go b/event/unbind.go deleted file mode 100644 index 636d465a..00000000 --- a/event/unbind.go +++ /dev/null @@ -1,50 +0,0 @@ -package event - -// Unbind on a binding is a rewriting of bus.Unbind(b) -func (b binding) unbind(eb *Bus) { - eb.unbind(b) -} - -func (eb *Bus) unbind(b binding) { - eb.pendingMutex.Lock() - eb.unbinds = append(eb.unbinds, b) - eb.pendingMutex.Unlock() -} - -// UnbindAllAndRebind is a way to reset the bindings on a CID efficiently, -// given a new set of equal length binding and event slices. This is equivalent -// to calling UnbindAll and then looping over Bind calls for the pairs of -// bindables and event names, but uses less mutex time. -func (eb *Bus) UnbindAllAndRebind(bo Event, binds []Bindable, cid CID, events []string) { - opts := make([]Event, len(events)) - for k, v := range events { - opts[k] = Event{ - Name: v, - CallerID: cid, - } - } - - eb.pendingMutex.Lock() - eb.unbindAllAndRebinds = append(eb.unbindAllAndRebinds, UnbindAllOption{ - ub: bo, - bs: opts, - bnds: binds, - }) - eb.pendingMutex.Unlock() -} - -// UnbindAll removes all events that match the given bindingOption from the -// default event bus -func (eb *Bus) UnbindAll(opt Event) { - eb.pendingMutex.Lock() - eb.partUnbinds = append(eb.partUnbinds, opt) - eb.pendingMutex.Unlock() -} - -// UnbindBindable is a manual way to unbind a function Bindable. Use of -// this with closures will result in undefined behavior. -func (eb *Bus) UnbindBindable(opt UnbindOption) { - eb.pendingMutex.Lock() - eb.fullUnbinds = append(eb.fullUnbinds, opt) - eb.pendingMutex.Unlock() -} diff --git a/examples/adventure/example.PNG b/examples/adventure/example.PNG new file mode 100644 index 00000000..e51c42a8 Binary files /dev/null and b/examples/adventure/example.PNG differ diff --git a/examples/adventure/main.go b/examples/adventure/main.go new file mode 100644 index 00000000..a4fb82e7 --- /dev/null +++ b/examples/adventure/main.go @@ -0,0 +1,255 @@ +package main + +import ( + "fmt" + "image/color" + "math/rand" + + "github.com/oakmound/oak/v4" + "github.com/oakmound/oak/v4/alg/floatgeom" + "github.com/oakmound/oak/v4/alg/intgeom" + "github.com/oakmound/oak/v4/collision" + "github.com/oakmound/oak/v4/entities" + "github.com/oakmound/oak/v4/event" + "github.com/oakmound/oak/v4/key" + "github.com/oakmound/oak/v4/render" + "github.com/oakmound/oak/v4/scene" +) + +// Rooms exercises shifting the camera in a zelda-esque fashion, +// moving the camera to center on even-sized rooms arranged in a grid +// once the player enters them. + +func isOffScreen(ctx *scene.Context, char *entities.Entity) (intgeom.Dir2, bool) { + x := int(char.X()) + y := int(char.Y()) + if x > ctx.Window.Viewport().X()+ctx.Window.Bounds().X() { + return intgeom.Right, true + } + if y > ctx.Window.Viewport().Y()+ctx.Window.Bounds().Y() { + return intgeom.Down, true + } + if int(char.Right()) < ctx.Window.Viewport().X() { + return intgeom.Left, true + } + if int(char.Bottom()) < ctx.Window.Viewport().Y() { + return intgeom.Up, true + } + return intgeom.Dir2{}, false +} + +const ( + transitionFrameCount = 25 +) + +const ( + LabelPlayer collision.Label = iota + LabelWall + LabelBox + LabelLock + LabelKey + LabelPot + LabelTreasure + LabelEnemy +) + +func main() { + + oak.AddScene("rooms", scene.Scene{Start: func(ctx *scene.Context) { + char := entities.New(ctx, + entities.WithRect(floatgeom.NewRect2WH(200, 200, 50, 50)), + entities.WithColor(color.RGBA{255, 255, 255, 255}), + entities.WithSpeed(floatgeom.Point2{5, 5}), + entities.WithDrawLayers([]int{1, 2}), + ) + var transitioning bool + var totalTransitionDelta intgeom.Point2 + var transitionDelta intgeom.Point2 + event.Bind(ctx, event.Enter, char, func(c *entities.Entity, ev event.EnterPayload) event.Response { + if !transitioning { + dir, ok := isOffScreen(ctx, char) + if ok { + transitioning = true + totalTransitionDelta = ctx.Window.Bounds().Mul(intgeom.Point2{dir.X(), dir.Y()}) + transitionDelta = totalTransitionDelta.DivConst(transitionFrameCount) + } + } + if transitioning { + // disable movement + // move camera one size towards the player + if totalTransitionDelta.X() != 0 || totalTransitionDelta.Y() != 0 { + oak.ShiftViewport(transitionDelta) + totalTransitionDelta = totalTransitionDelta.Sub(transitionDelta) + } else { + transitioning = false + } + } else { + char.Delta = floatgeom.Point2{} + if ctx.IsDown(key.W) { + char.Delta[1] -= char.Speed[1] + } + if ctx.IsDown(key.S) { + char.Delta[1] += char.Speed[1] + } + if ctx.IsDown(key.A) { + char.Delta[0] -= char.Speed[0] + } + if ctx.IsDown(key.D) { + char.Delta[0] += char.Speed[0] + } + char.Rect = char.Rect.Shift(char.Delta) + + char.Tree.UpdateSpace( + char.X(), char.Y(), char.W(), char.H(), char.Space, + ) + hitWall := false + hits := char.Tree.Hits(char.Space) + for _, h := range hits { + fmt.Println("hit label", h.Label) + switch h.Label { + case LabelWall: + if hitWall { + continue + } + hitWall = true + char.Delta = char.Delta.MulConst(-1) + char.Rect = char.Rect.Shift(char.Delta) + char.Tree.UpdateSpace( + char.X(), char.Y(), char.W(), char.H(), char.Space, + ) + } + } + char.Renderable.SetPos(char.X(), char.Y()) + } + + return 0 + }) + const tileWidth = 50 + const tileHeight = 50 + tileDims := entities.WithDimensions(floatgeom.Point2{tileWidth, tileHeight}) + x := 0.0 + y := float64(-tileHeight) // to accommodate for initial newline + for _, rn := range board { + if rn == '\n' { + x = 0 + y += tileHeight + continue + } + commonOpts := entities.And( + tileDims, + entities.WithPosition(floatgeom.Point2{x, y}), + entities.WithDrawLayers([]int{1}), + ) + switch Tile(rn) { + case Wall: + entities.New(ctx, commonOpts, + entities.WithColor(color.RGBA{50, 50, 50, 255}), + entities.WithLabel(LabelWall), + ) + x += tileWidth + continue + case Box: + entities.New(ctx, commonOpts, + tileDims, + entities.WithColor(color.RGBA{150, 150, 20, 255}), + entities.WithLabel(LabelBox), + ) + // TODO + case Player: + fmt.Println("placing character at", x, y) + char.SetPos(floatgeom.Point2{x, y}) + case Pot: + entities.New(ctx, commonOpts, + tileDims, + entities.WithColor(color.RGBA{100, 100, 100, 255}), + entities.WithLabel(LabelTreasure), + ) + // TODO + case Enemy: + // TODO + case Treasure: + entities.New(ctx, commonOpts, + tileDims, + entities.WithColor(color.RGBA{255, 255, 0, 255}), + entities.WithLabel(LabelTreasure), + ) + case Lock: + entities.New(ctx, commonOpts, + tileDims, + entities.WithColor(color.RGBA{200, 150, 0, 255}), + entities.WithLabel(LabelLock), + ) + case Key: + entities.New(ctx, commonOpts, + tileDims, + entities.WithColor(color.RGBA{50, 50, 200, 255}), + entities.WithLabel(LabelKey), + ) + case Empty: + } + + // ground beneath moving objects + r := uint8(rand.Intn(20) + 40) + g := uint8(rand.Intn(80) + 60) + b := uint8(rand.Intn(10) + 30) + cb := render.NewColorBoxR(tileWidth, tileHeight, color.RGBA{r, g, b, 255}) + cb.SetPos(float64(x), float64(y)) + render.Draw(cb, 0) + + x += tileWidth + } + }}) + + oak.Init("rooms", func(c oak.Config) (oak.Config, error) { + c.Screen.Width = 650 + c.Screen.Height = 500 + return c, nil + }) +} + +type Tile rune + +const ( + Wall Tile = 'W' + Empty Tile = ' ' + Box Tile = 'B' + Enemy Tile = 'E' + Player Tile = 'C' + Pot Tile = 'P' + Lock Tile = 'L' + Key Tile = 'K' + Treasure Tile = 'T' +) + +const board = ` +WWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWW +W WW WW W +W C WW WW W +W WW WWW W +W LLL T W +W WWW W +W WW WW W +W WW WW W +W WW P WWWWWWWWWWWWWW +WWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWW WW W +WWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWW WW B W +W WW WW BKB W +W WW WW E B W +W WWWWWWWWWWW WW E WW W +W WW WW WW E W +W WW WW W +W WW WW W +W WW W +W WW W +WWWWWW WWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWW +WWWWWW WWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWW +W B B P WW W +W P BB WW K W +W B B K WW E W +W BB P WW W +W WWWWWWWWWWWWWWWWWWWWWWWWWWWW W +W B E W +W E P E W +W B W +WWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWW +` diff --git a/examples/bezier/README.md b/examples/bezier/README.md new file mode 100644 index 00000000..8c055322 --- /dev/null +++ b/examples/bezier/README.md @@ -0,0 +1,5 @@ +# Bezier Rendering +Use a mouse or debug commands to create points. +The points will be used to draw bézier curve. + +![text](./example.PNG) \ No newline at end of file diff --git a/examples/bezier/main.go b/examples/bezier/main.go index f7b3b18a..7abe6ebe 100644 --- a/examples/bezier/main.go +++ b/examples/bezier/main.go @@ -5,13 +5,13 @@ import ( "image/color" "strconv" - oak "github.com/oakmound/oak/v3" - "github.com/oakmound/oak/v3/debugstream" - "github.com/oakmound/oak/v3/event" - "github.com/oakmound/oak/v3/mouse" - "github.com/oakmound/oak/v3/render" - "github.com/oakmound/oak/v3/scene" - "github.com/oakmound/oak/v3/shape" + oak "github.com/oakmound/oak/v4" + "github.com/oakmound/oak/v4/debugstream" + "github.com/oakmound/oak/v4/event" + "github.com/oakmound/oak/v4/mouse" + "github.com/oakmound/oak/v4/render" + "github.com/oakmound/oak/v4/scene" + "github.com/oakmound/oak/v4/shape" ) var ( @@ -53,23 +53,23 @@ func main() { return "" }}) - oak.AddScene("bezier", scene.Scene{Start: func(*scene.Context) { + oak.AddScene("bezier", scene.Scene{Start: func(ctx *scene.Context) { mouseFloats := []float64{} - event.GlobalBind(mouse.Press, func(_ event.CID, mouseEvent interface{}) int { - me := mouseEvent.(*mouse.Event) - // Left click to add a point to the curve - if me.Button == mouse.ButtonLeft { - mouseFloats = append(mouseFloats, float64(me.X()), float64(me.Y())) - renderCurve(mouseFloats) - // Perform any other click to reset the drawn curve - } else { - mouseFloats = []float64{} - if cmp != nil { - cmp.Undraw() + event.GlobalBind(ctx, + mouse.Press, func(me *mouse.Event) event.Response { + // Left click to add a point to the curve + if me.Button == mouse.ButtonLeft { + mouseFloats = append(mouseFloats, float64(me.X()), float64(me.Y())) + renderCurve(mouseFloats) + // Perform any other click to reset the drawn curve + } else { + mouseFloats = []float64{} + if cmp != nil { + cmp.Undraw() + } } - } - return 0 - }) + return 0 + }) }}) oak.Init("bezier", func(c oak.Config) (oak.Config, error) { c.EnableDebugConsole = true @@ -97,7 +97,7 @@ func bezierDrawRec(b shape.Bezier, list *render.CompositeM, alpha uint8) { bezierDrawRec(bzn.Right, list, uint8(float64(alpha)*.5)) case shape.BezierPoint: sp := render.NewColorBox(5, 5, color.RGBA{255, 255, 255, 255}) - sp.SetPos(bzn.X()-2, bzn.Y()-2) + sp.SetPos(bzn[0]-2, bzn[1]-2) list.Append(sp) } } diff --git a/examples/blank/README.md b/examples/blank/README.md new file mode 100644 index 00000000..8181d097 --- /dev/null +++ b/examples/blank/README.md @@ -0,0 +1,4 @@ +# Blank Scene +Starts a pprof server and sets up a blank scene. +Useful for benchmarking and as a minimal base to copy from. +For less minimalist copying point see [the basic game template](https://github.com/oakmound/game-template). \ No newline at end of file diff --git a/examples/blank/main.go b/examples/blank/main.go index 9e9fceee..6de09b14 100644 --- a/examples/blank/main.go +++ b/examples/blank/main.go @@ -5,9 +5,9 @@ import ( "net/http" _ "net/http/pprof" - "github.com/oakmound/oak/v3" - "github.com/oakmound/oak/v3/render" - "github.com/oakmound/oak/v3/scene" + "github.com/oakmound/oak/v4" + "github.com/oakmound/oak/v4/render" + "github.com/oakmound/oak/v4/scene" ) // This example is a blank, default scene with a pprof server. Useful for diff --git a/examples/click-propagation/main.go b/examples/click-propagation/main.go index c56e6fc0..a8533c58 100644 --- a/examples/click-propagation/main.go +++ b/examples/click-propagation/main.go @@ -1,110 +1,84 @@ package main import ( - "fmt" "image" "image/color" - "image/draw" - "github.com/oakmound/oak/v3" - "github.com/oakmound/oak/v3/collision" - "github.com/oakmound/oak/v3/event" - "github.com/oakmound/oak/v3/mouse" - "github.com/oakmound/oak/v3/render" - "github.com/oakmound/oak/v3/scene" + "github.com/oakmound/oak/v4" + "github.com/oakmound/oak/v4/collision" + "github.com/oakmound/oak/v4/event" + "github.com/oakmound/oak/v4/mouse" + "github.com/oakmound/oak/v4/render" + "github.com/oakmound/oak/v4/scene" ) -// This example demonstrates the use of the Propagated boolean on +// This example demonstrates the use of the StopPropagation boolean on // mouse event payloads to prevent mouse interactions from falling // through to lower UI elements after interacting with a higher layer func main() { oak.AddScene("click-propagation", scene.Scene{ Start: func(ctx *scene.Context) { - z := 0 - y := 400.0 - for x := 20.0; x < 400; x += 20 { - z++ - y -= 20 - newHoverButton(x, y, 35, 35, color.RGBA{200, 200, 200, 200}, z) + newHoverButton(ctx, 10, 10, 620, 460, color.RGBA{255, 255, 100, 255}, 1) + + newHoverButton(ctx, 30, 30, 190, 430, color.RGBA{255, 100, 100, 255}, 2) + newHoverButton(ctx, 240, 30, 370, 430, color.RGBA{255, 100, 255, 255}, 2) + + const gridW = 10 + for x := 50; x < 210-gridW; x += (gridW * 2) { + for y := 50; y < 450-gridW; y += (gridW * 2) { + newHoverButton(ctx, float64(x), float64(y), gridW, gridW, color.RGBA{100, 255, 255, 255}, 3) + } + } + + newHoverButton(ctx, 260, 50, 100, 390, color.RGBA{100, 100, 255, 255}, 3) + for y := 70; y < 440-gridW; y += (gridW * 2) { + newHoverButton(ctx, 270, float64(y), 80, gridW, color.RGBA{255, 255, 255, 255}, 4) } + newHoverButton(ctx, 380, 50, 200, 80, color.RGBA{100, 100, 100, 255}, 3) }, }) oak.Init("click-propagation") } type hoverButton struct { - id event.CID + id event.CallerID mouse.CollisionPhase - *changingColorBox + *render.ColorBoxR } -func (hb *hoverButton) Init() event.CID { - hb.id = event.NextID(hb) +func (hb *hoverButton) CID() event.CallerID { return hb.id } -func newHoverButton(x, y, w, h float64, clr color.RGBA, layer int) { +func newHoverButton(ctx *scene.Context, x, y, w, h float64, clr color.RGBA, layer int) { hb := &hoverButton{} - hb.Init() - hb.changingColorBox = newChangingColorBox(x, y, int(w), int(h), clr) + hb.id = ctx.Register(hb) + hb.ColorBoxR = render.NewColorBoxR(int(w), int(h), clr) + hb.ColorBoxR.SetPos(x, y) sp := collision.NewSpace(x, y, w, h, hb.id) sp.SetZLayer(float64(layer)) mouse.Add(sp) - mouse.PhaseCollision(sp) + mouse.PhaseCollision(sp, ctx.Handler) - render.Draw(hb.changingColorBox, 0, layer) - hb.id.Bind(mouse.ClickOn, func(c event.CID, i interface{}) int { - hb := event.GetEntity(c).(*hoverButton) - me := i.(*mouse.Event) - fmt.Println(c, me.Point2) - hb.changingColorBox.c = color.RGBA{128, 128, 128, 128} + render.Draw(hb.ColorBoxR, layer) + + event.Bind(ctx, mouse.ClickOn, hb, func(box *hoverButton, me *mouse.Event) event.Response { + box.ColorBoxR.Color = image.NewUniform(color.RGBA{128, 128, 128, 128}) me.StopPropagation = true return 0 }) - hb.id.Bind(mouse.Start, func(c event.CID, i interface{}) int { - fmt.Println("start") - hb := event.GetEntity(c).(*hoverButton) - me := i.(*mouse.Event) - hb.changingColorBox.c = color.RGBA{50, 50, 50, 50} + event.Bind(ctx, mouse.Start, hb, func(box *hoverButton, me *mouse.Event) event.Response { + box.ColorBoxR.Color = image.NewUniform(color.RGBA{50, 50, 50, 50}) me.StopPropagation = true return 0 }) - hb.id.Bind(mouse.Stop, func(c event.CID, i interface{}) int { - fmt.Println("stop") - hb := event.GetEntity(c).(*hoverButton) - me := i.(*mouse.Event) - hb.changingColorBox.c = clr + event.Bind(ctx, mouse.Stop, hb, func(box *hoverButton, me *mouse.Event) event.Response { + box.ColorBoxR.Color = image.NewUniform(clr) me.StopPropagation = true return 0 }) } - -type changingColorBox struct { - render.LayeredPoint - c color.RGBA - w, h int -} - -func newChangingColorBox(x, y float64, w, h int, c color.RGBA) *changingColorBox { - return &changingColorBox{ - LayeredPoint: render.NewLayeredPoint(x, y, 0), - c: c, - w: w, - h: h, - } -} - -func (ccb *changingColorBox) Draw(buff draw.Image, xOff, yOff float64) { - x := int(ccb.X() + xOff) - y := int(ccb.Y() + yOff) - rect := image.Rect(x, y, ccb.w+x, ccb.h+y) - draw.Draw(buff, rect, image.NewUniform(ccb.c), image.Point{int(ccb.X() + xOff), int(ccb.Y() + yOff)}, draw.Over) -} - -func (ccb *changingColorBox) GetDims() (int, int) { - return ccb.w, ccb.h -} diff --git a/examples/clipboard/go.mod b/examples/clipboard/go.mod deleted file mode 100644 index f55b9172..00000000 --- a/examples/clipboard/go.mod +++ /dev/null @@ -1,11 +0,0 @@ -module github.com/oakmound/oak/examples/clipboard - -go 1.16 - -require ( - github.com/atotto/clipboard v0.1.4 - github.com/oakmound/oak/v3 v3.0.0-alpha.1 - golang.org/x/mobile v0.0.0-20220112015953-858099ff7816 -) - -replace github.com/oakmound/oak/v3 => ../.. diff --git a/examples/clipboard/go.sum b/examples/clipboard/go.sum deleted file mode 100644 index a91dcd3d..00000000 --- a/examples/clipboard/go.sum +++ /dev/null @@ -1,73 +0,0 @@ -dmitri.shuralyov.com/gpu/mtl v0.0.0-20201218220906-28db891af037 h1:+PdD6GLKejR9DizMAKT5DpSAkKswvZrurk1/eEt9+pw= -dmitri.shuralyov.com/gpu/mtl v0.0.0-20201218220906-28db891af037/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= -github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= -github.com/BurntSushi/xgb v0.0.0-20210121224620-deaf085860bc h1:7D+Bh06CRPCJO3gr2F7h1sriovOZ8BMhca2Rg85c2nk= -github.com/BurntSushi/xgb v0.0.0-20210121224620-deaf085860bc/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= -github.com/BurntSushi/xgbutil v0.0.0-20190907113008-ad855c713046 h1:O/r2Sj+8QcMF7V5IcmiE2sMFV2q3J47BEirxbXJAdzA= -github.com/BurntSushi/xgbutil v0.0.0-20190907113008-ad855c713046/go.mod h1:uw9h2sd4WWHOPdJ13MQpwK5qYWKYDumDqxWWIknEQ+k= -github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= -github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= -github.com/disintegration/gift v1.2.0 h1:VMQeei2F+ZtsHjMgP6Sdt1kFjRhs2lGz8ljEOPeIR50= -github.com/disintegration/gift v1.2.0/go.mod h1:Jh2i7f7Q2BM7Ezno3PhfezbR1xpUg9dUg3/RlKGr4HI= -github.com/eaburns/bit v0.0.0-20131029213740-7bd5cd37375d/go.mod h1:CHkHWWZ4kbGY6jEy1+qlitDaCtRgNvCOQdakj/1Yl/Q= -github.com/eaburns/flac v0.0.0-20171003200620-9a6fb92396d1/go.mod h1:frG94byMNy+1CgGrQ25dZ+17tf98EN+OYBQL4Zh612M= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20210410170116-ea3d685f79fb h1:T6gaWBvRzJjuOrdCtg8fXXjKai2xSDqWTcKFUPuw8Tw= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20210410170116-ea3d685f79fb/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g= -github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= -github.com/hajimehoshi/go-mp3 v0.3.1 h1:pn/SKU1+/rfK8KaZXdGEC2G/KCB2aLRjbTCrwKcokao= -github.com/hajimehoshi/go-mp3 v0.3.1/go.mod h1:qMJj/CSDxx6CGHiZeCgbiq2DSUkbK0UbtXShQcnfyMM= -github.com/hajimehoshi/oto v0.6.1/go.mod h1:0QXGEkbuJRohbJaxr7ZQSxnju7hEhseiPx2hrh6raOI= -github.com/jfreymuth/pulse v0.1.0 h1:KN38/9hoF9PJvP5DpEVhMRKNuwnJUonc8c9ARorRXUA= -github.com/jfreymuth/pulse v0.1.0/go.mod h1:cpYspI6YljhkUf1WLXLLDmeaaPFc3CnGLjDZf9dZ4no= -github.com/oakmound/alsa v0.0.2 h1:JbOUckkJqVvhABth7qy2JgAjqsWuBPggyoYOk1L6eK0= -github.com/oakmound/alsa v0.0.2/go.mod h1:wx+ehwqFnNL7foTwxxu2bKQlaUmD2oXd4ka1UBSgWAo= -github.com/oakmound/libudev v0.2.1 h1:gaXuw7Pbt3RSRxbUakAjl0dSW6Wo3TZWpwS5aMq8+EA= -github.com/oakmound/libudev v0.2.1/go.mod h1:zYF5CkHY+UP6lzWbPR+XoVAscl/s+OncWA//qWjMLUs= -github.com/oakmound/w32 v2.1.0+incompatible h1:vIkC6eJVOaAnwTTOyiVCGh24GoryPRmcvWq3cekkG2U= -github.com/oakmound/w32 v2.1.0+incompatible/go.mod h1:lzloWlclSXIU4cDr67WF8qjFFDO8gHHBIk4Qqe90enQ= -github.com/oov/directsound-go v0.0.0-20141101201356-e53e59c700bf h1:od9gEl9UQ/QNHlgYlgsSaC5SZ+CGbvO2/PCIgserJc0= -github.com/oov/directsound-go v0.0.0-20141101201356-e53e59c700bf/go.mod h1:RBXkZ8n2vvtdJP6PO+TbU/N/DVuCDwUN53CU+C1pJOs= -github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56 h1:estk1glOnSVeJ9tdEZZc5mAMDZk5lNJNyJ6DvrBkTEU= -golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56/go.mod h1:JhuoJpWY28nO4Vef9tZUw9qufEGTyX1+7lmHxV5q5G4= -golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= -golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/image v0.0.0-20201208152932-35266b937fa6 h1:nfeHNc1nAqecKCy2FCy4HY+soOOe5sDLJ/gZLbx6GYI= -golang.org/x/image v0.0.0-20201208152932-35266b937fa6/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= -golang.org/x/mobile v0.0.0-20190415191353-3e0bab5405d6/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= -golang.org/x/mobile v0.0.0-20220112015953-858099ff7816 h1:jhDgkcu3yQ4tasBZ+1YwDmK7eFmuVf1w1k+NGGGxfmE= -golang.org/x/mobile v0.0.0-20220112015953-858099ff7816/go.mod h1:pe2sM7Uk+2Su1y7u/6Z8KJ24D7lepUjFZbhFOrmDfuQ= -golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= -golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ= -golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190429190828-d89cdac9e872/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220111092808-5a964db01320 h1:0jf+tOCoZ3LyutmCOWpVni1chK4VfFLhRsDK7MhqGRY= -golang.org/x/sys v0.0.0-20220111092808-5a964db01320/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.1.8-0.20211022200916-316ba0b74098/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/examples/clipboard/main.go b/examples/clipboard/main.go deleted file mode 100644 index 76fc8e5c..00000000 --- a/examples/clipboard/main.go +++ /dev/null @@ -1,77 +0,0 @@ -package main - -import ( - "fmt" - - gokey "golang.org/x/mobile/event/key" - - "github.com/oakmound/oak/v3" - "github.com/oakmound/oak/v3/entities/x/btn" - "github.com/oakmound/oak/v3/event" - "github.com/oakmound/oak/v3/key" - "github.com/oakmound/oak/v3/render" - "github.com/oakmound/oak/v3/scene" - - "github.com/atotto/clipboard" -) - -func main() { - oak.AddScene("clipboard-test", scene.Scene{ - Start: func(ctx *scene.Context) { - newClipboardCopyText("click-me-to-copy", 20, 20) - newClipboardCopyText("click-to-copy-me-too", 20, 50) - newClipboardPaster("click-or-ctrl+v-to-paste-here", 20, 200) - }, - }) - oak.Init("clipboard-test") -} - -func newClipboardCopyText(text string, x, y float64) { - btn.New( - btn.Font(render.DefaultFont()), - btn.Text(text), - btn.Pos(x, y), - btn.Height(20), - btn.FitText(20), - btn.Click(func(event.CID, interface{}) int { - err := clipboard.WriteAll(text) - if err != nil { - fmt.Println(err) - } - return 0 - }), - ) -} - -func newClipboardPaster(placeholder string, x, y float64) { - textPtr := new(string) - *textPtr = placeholder - btn.New( - btn.Font(render.DefaultFont()), - btn.TextPtr(textPtr), - btn.Pos(x, y), - btn.Height(20), - btn.FitText(20), - btn.Binding(key.Down+key.V, func(_ event.CID, payload interface{}) int { - kv := payload.(key.Event) - if kv.Modifiers&gokey.ModControl == gokey.ModControl { - got, err := clipboard.ReadAll() - if err != nil { - fmt.Println(err) - return 0 - } - *textPtr = got - } - return 0 - }), - btn.Click(func(event.CID, interface{}) int { - got, err := clipboard.ReadAll() - if err != nil { - fmt.Println(err) - return 0 - } - *textPtr = got - return 0 - }), - ) -} diff --git a/examples/collision-demo/example.PNG b/examples/collision-demo/example.PNG deleted file mode 100644 index 25bae737..00000000 Binary files a/examples/collision-demo/example.PNG and /dev/null differ diff --git a/examples/collision-demo/main.go b/examples/collision-demo/main.go deleted file mode 100644 index 41210b75..00000000 --- a/examples/collision-demo/main.go +++ /dev/null @@ -1,148 +0,0 @@ -package main - -import ( - "image/color" - - oak "github.com/oakmound/oak/v3" - "github.com/oakmound/oak/v3/collision" - "github.com/oakmound/oak/v3/entities" - "github.com/oakmound/oak/v3/event" - "github.com/oakmound/oak/v3/render" - "github.com/oakmound/oak/v3/scene" -) - -const ( - _ = iota - RED collision.Label = iota - GREEN - BLUE - TEAL -) - -func main() { - oak.AddScene("demo", scene.Scene{Start: func(*scene.Context) { - act := &AttachCollisionTest{} - act.Solid = entities.NewSolid(50, 50, 50, 50, render.NewColorBox(50, 50, color.RGBA{0, 0, 0, 255}), nil, act.Init()) - - collision.Attach(act.Vector, act.Space, nil, 0, 0) - - act.Bind(event.Enter, func(event.CID, interface{}) int { - if act.ShouldUpdate { - act.ShouldUpdate = false - act.R.Undraw() - act.R = act.nextR - render.Draw(act.R, 0, 1) - } - if oak.IsDown("A") { - // We could use attachment here to not have to shift both - // R and act but that is made more difficult by constantly - // changing the act's R - act.ShiftX(-3) - act.R.ShiftX(-3) - } else if oak.IsDown("D") { - act.ShiftX(3) - act.R.ShiftX(3) - } - if oak.IsDown("W") { - act.ShiftY(-3) - act.R.ShiftY(-3) - } else if oak.IsDown("S") { - act.ShiftY(3) - act.R.ShiftY(3) - } - return 0 - }) - - render.Draw(act.R, 0, 1) - - collision.PhaseCollision(act.Space, nil) - act.Bind(collision.Start, func(id event.CID, label interface{}) int { - l := label.(collision.Label) - switch l { - case RED: - act.r += 125 - act.UpdateR() - case GREEN: - act.g += 125 - act.UpdateR() - case BLUE: - act.b += 125 - act.UpdateR() - case TEAL: - act.b += 125 - act.g += 125 - act.UpdateR() - } - return 0 - }) - act.Bind(collision.Stop, func(id event.CID, label interface{}) int { - l := label.(collision.Label) - switch l { - case RED: - act.r -= 125 - act.UpdateR() - case GREEN: - act.g -= 125 - act.UpdateR() - case BLUE: - act.b -= 125 - act.UpdateR() - case TEAL: - act.b -= 125 - act.g -= 125 - act.UpdateR() - } - return 0 - }) - - upleft := entities.NewSolid(0, 0, 320, 240, render.NewColorBox(320, 240, color.RGBA{100, 0, 0, 100}), nil, 0) - upleft.Space.UpdateLabel(RED) - upleft.R.SetLayer(0) - render.Draw(upleft.R, 0, 0) - - upright := entities.NewSolid(320, 0, 320, 240, render.NewColorBox(320, 240, color.RGBA{0, 100, 0, 100}), nil, 0) - upright.Space.UpdateLabel(GREEN) - upright.R.SetLayer(0) - render.Draw(upright.R, 0, 0) - - botleft := entities.NewSolid(0, 240, 320, 240, render.NewColorBox(320, 240, color.RGBA{0, 0, 100, 100}), nil, 0) - botleft.Space.UpdateLabel(BLUE) - botleft.R.SetLayer(0) - render.Draw(botleft.R, 0, 0) - - botright := entities.NewSolid(320, 240, 320, 240, render.NewColorBox(320, 240, color.RGBA{0, 100, 100, 100}), nil, 0) - botright.Space.UpdateLabel(TEAL) - botright.R.SetLayer(0) - render.Draw(botright.R, 0, 0) - }}) - render.SetDrawStack( - render.NewDynamicHeap(), - ) - oak.Init("demo") -} - -type AttachCollisionTest struct { - *entities.Solid - // AttachSpace is a composable struct that allows - // spaces to be attached to vectors - collision.AttachSpace - // Phase is a composable struct that enables the call - // collision.CollisionPhase on this struct's space, - // which will start sending signals when that space - // starts and stops touching given labels - collision.Phase - r, g, b int - ShouldUpdate bool - nextR render.Renderable -} - -func (act *AttachCollisionTest) Init() event.CID { - return event.NextID(act) -} - -func (act *AttachCollisionTest) UpdateR() { - act.nextR = render.NewColorBox(50, 50, color.RGBA{uint8(act.r), uint8(act.g), uint8(act.b), 255}) - act.nextR.SetPos(act.X(), act.Y()) - act.nextR.SetLayer(1) - act.ShouldUpdate = true -} diff --git a/examples/collision/README.md b/examples/collision/README.md new file mode 100644 index 00000000..b06bb7b8 --- /dev/null +++ b/examples/collision/README.md @@ -0,0 +1,5 @@ +# Collision Demo + +Controllable box that colors itself based on what zones it collides with. + +![text](./example.PNG) \ No newline at end of file diff --git a/examples/collision/main.go b/examples/collision/main.go new file mode 100644 index 00000000..30998bfb --- /dev/null +++ b/examples/collision/main.go @@ -0,0 +1,164 @@ +package main + +import ( + "image/color" + "time" + + "github.com/oakmound/oak/v4" + "github.com/oakmound/oak/v4/alg/floatgeom" + "github.com/oakmound/oak/v4/collision" + "github.com/oakmound/oak/v4/entities" + "github.com/oakmound/oak/v4/event" + "github.com/oakmound/oak/v4/key" + "github.com/oakmound/oak/v4/render" + "github.com/oakmound/oak/v4/scene" + "github.com/oakmound/oak/v4/shake" +) + +const ( + _ = iota + RED collision.Label = iota + GREEN + BLUE + TEAL +) + +// if true, shake the screen on certain collisions +var demoShake bool = true + +func main() { + oak.AddScene("demo", scene.Scene{Start: func(ctx *scene.Context) { + act := &AttachCollisionTest{} + act.CallerID = ctx.Register(act) + act.Entity = entities.New(ctx, + entities.WithRect(floatgeom.NewRect2WH(50, 50, 50, 50)), + entities.WithColor(color.RGBA{0, 0, 0, 255}), + entities.WithDrawLayers([]int{0, 1}), + entities.WithParent(act), + ) + + event.Bind(ctx, event.Enter, act, func(act *AttachCollisionTest, ev event.EnterPayload) event.Response { + if act.ShouldUpdate { + act.ShouldUpdate = false + act.Renderable.Undraw() + act.Renderable = act.nextR + render.Draw(act.Renderable, 0, 1) + } + if oak.IsDown(key.A) { + act.ShiftX(-3) + } else if oak.IsDown(key.D) { + act.ShiftX(3) + } + if oak.IsDown(key.W) { + act.ShiftY(-3) + } else if oak.IsDown(key.S) { + act.ShiftY(3) + } + return 0 + }) + + collision.PhaseCollision(act.Space, ctx.CollisionTree) + + commonOpts := entities.And( + entities.WithDrawLayers([]int{0, 0}), + entities.WithDimensions(floatgeom.Point2{320, 240}), + ) + + upLeft := entities.New(ctx, commonOpts, + entities.WithColor(color.RGBA{100, 0, 0, 100}), + entities.WithLabel(RED), + ) + + upRight := entities.New(ctx, commonOpts, + entities.WithPosition(floatgeom.Point2{320, 0}), + entities.WithColor(color.RGBA{0, 100, 0, 100}), + entities.WithLabel(GREEN), + ) + _ = upRight + + botLeft := entities.New(ctx, commonOpts, + entities.WithPosition(floatgeom.Point2{0, 240}), + entities.WithColor(color.RGBA{0, 0, 100, 100}), + entities.WithLabel(BLUE), + ) + + botRight := entities.New(ctx, commonOpts, + entities.WithPosition(floatgeom.Point2{320, 240}), + entities.WithColor(color.RGBA{0, 100, 100, 100}), + entities.WithLabel(TEAL), + ) + + event.Bind(ctx, collision.Start, act, func(act *AttachCollisionTest, l collision.Label) event.Response { + switch l { + case RED: + act.r += 125 + act.UpdateR() + case GREEN: + act.g += 125 + act.UpdateR() + if demoShake { + shake.DefaultShaker.Shake(upLeft, time.Second) + shake.DefaultShaker.Shake(botLeft, time.Second) + shake.DefaultShaker.Shake(botRight, time.Second) + } + case BLUE: + act.b += 125 + act.UpdateR() + if demoShake { + shake.DefaultShaker.Shake(act, time.Second*2) + } + case TEAL: + act.b += 125 + act.g += 125 + act.UpdateR() + if demoShake { + shake.DefaultShaker.ShakeScreen(ctx, time.Second) + } + } + return 0 + }) + event.Bind(ctx, collision.Stop, act, func(act *AttachCollisionTest, l collision.Label) event.Response { + switch l { + case RED: + act.r -= 125 + act.UpdateR() + case GREEN: + act.g -= 125 + act.UpdateR() + case BLUE: + act.b -= 125 + act.UpdateR() + case TEAL: + act.b -= 125 + act.g -= 125 + act.UpdateR() + } + return 0 + }) + + }}) + render.SetDrawStack( + render.NewDynamicHeap(), + ) + oak.Init("demo") +} + +type AttachCollisionTest struct { + *entities.Entity + event.CallerID + r, g, b int + ShouldUpdate bool + nextR render.Renderable +} + +func (act *AttachCollisionTest) CID() event.CallerID { + return act.CallerID.CID() +} + +// UpdateR with the rgb set on the act. +func (act *AttachCollisionTest) UpdateR() { + act.nextR = render.NewColorBox(50, 50, color.RGBA{uint8(act.r), uint8(act.g), uint8(act.b), 255}) + act.nextR.SetPos(act.X(), act.Y()) + act.nextR.SetLayer(1) + act.ShouldUpdate = true +} diff --git a/examples/custom-cursor/example.PNG b/examples/custom-cursor/example.PNG deleted file mode 100644 index 6d18db5d..00000000 Binary files a/examples/custom-cursor/example.PNG and /dev/null differ diff --git a/examples/custom-cursor/main.go b/examples/custom-cursor/main.go deleted file mode 100644 index 863e0ae7..00000000 --- a/examples/custom-cursor/main.go +++ /dev/null @@ -1,48 +0,0 @@ -package main - -import ( - "fmt" - "image/color" - - oak "github.com/oakmound/oak/v3" - "github.com/oakmound/oak/v3/event" - "github.com/oakmound/oak/v3/mouse" - "github.com/oakmound/oak/v3/render" - "github.com/oakmound/oak/v3/scene" -) - -func main() { - oak.AddScene("customcursor", scene.Scene{ - Start: func(ctx *scene.Context) { - err := ctx.Window.HideCursor() - if err != nil { - fmt.Println(err) - } - - box := render.NewSequence(15, - render.NewColorBox(2, 2, color.RGBA{255, 255, 0, 255}), - render.NewColorBox(3, 3, color.RGBA{255, 235, 0, 255}), - render.NewColorBox(4, 4, color.RGBA{255, 215, 0, 255}), - render.NewColorBox(5, 5, color.RGBA{255, 195, 0, 255}), - render.NewColorBox(6, 6, color.RGBA{255, 175, 0, 255}), - render.NewColorBox(5, 5, color.RGBA{255, 155, 0, 255}), - render.NewColorBox(4, 4, color.RGBA{255, 135, 0, 255}), - render.NewColorBox(3, 3, color.RGBA{255, 115, 0, 255}), - render.NewColorBox(2, 2, color.RGBA{255, 95, 0, 255}), - render.NewColorBox(1, 1, color.RGBA{255, 75, 0, 255}), - render.EmptyRenderable(), - render.EmptyRenderable(), - render.EmptyRenderable(), - render.EmptyRenderable(), - ) - ctx.DrawStack.Draw(box) - - ctx.EventHandler.GlobalBind(mouse.Drag, func(_ event.CID, me interface{}) int { - mouseEvent := me.(*mouse.Event) - box.SetPos(mouseEvent.X(), mouseEvent.Y()) - return 0 - }) - }, - }) - oak.Init("customcursor") -} diff --git a/examples/error-scene/example.PNG b/examples/error-scene/example.PNG deleted file mode 100644 index b625a01a..00000000 Binary files a/examples/error-scene/example.PNG and /dev/null differ diff --git a/examples/error-scene/main.go b/examples/error-scene/main.go deleted file mode 100644 index 02de8666..00000000 --- a/examples/error-scene/main.go +++ /dev/null @@ -1,23 +0,0 @@ -package main - -import ( - "github.com/oakmound/oak/v3" - "github.com/oakmound/oak/v3/render" - "github.com/oakmound/oak/v3/scene" -) - -func main() { - controller := oak.NewWindow() - // If ErrorScene is set, the scene handler will - // fall back to this error scene if it is told to - // go to an unknown scene - controller.ErrorScene = "error" - controller.AddScene("typo", scene.Scene{Start: func(ctx *scene.Context) { - ctx.DrawStack.Draw(render.NewText("Real scene", 100, 100)) - }}) - controller.AddScene("error", scene.Scene{Start: func(ctx *scene.Context) { - ctx.DrawStack.Draw(render.NewText("Error scene", 100, 100)) - }}) - - controller.Init("typpo") -} diff --git a/examples/fallback-font/example.PNG b/examples/fallback-font/example.PNG deleted file mode 100644 index 3a475a17..00000000 Binary files a/examples/fallback-font/example.PNG and /dev/null differ diff --git a/examples/fallback-font/go.mod b/examples/fallback-font/go.mod deleted file mode 100644 index 6ae55a2d..00000000 --- a/examples/fallback-font/go.mod +++ /dev/null @@ -1,10 +0,0 @@ -module github.com/oakmound/oak/examples/fallback-font - -go 1.16 - -require ( - github.com/flopp/go-findfont v0.0.0-20201114153133-e7393a00c15b - github.com/oakmound/oak/v3 v3.0.0-alpha.1 -) - -replace github.com/oakmound/oak/v3 => ../.. diff --git a/examples/fallback-font/main.go b/examples/fallback-font/main.go deleted file mode 100644 index c32b8d26..00000000 --- a/examples/fallback-font/main.go +++ /dev/null @@ -1,74 +0,0 @@ -package main - -import ( - "fmt" - "image/color" - - "image" - - findfont "github.com/flopp/go-findfont" - oak "github.com/oakmound/oak/v3" - "github.com/oakmound/oak/v3/render" - "github.com/oakmound/oak/v3/scene" -) - -func main() { - oak.AddScene("demo", - scene.Scene{Start: func(*scene.Context) { - - const fontHeight = 16 - - fg := render.DefFontGenerator - fg.Color = image.NewUniform(color.RGBA{255, 0, 0, 255}) - fg.FontOptions.Size = fontHeight - font, _ := fg.Generate() - - fallbackFonts := []string{ - "Arial.ttf", - "Yumin.ttf", - // TODO: support multi-color glyphs - "Seguiemj.ttf", - } - - for _, fontname := range fallbackFonts { - fontPath, err := findfont.Find(fontname) - if err != nil { - fmt.Println("Do you have ", fontname, "installed?") - continue - } - fg := render.FontGenerator{ - File: fontPath, - Color: image.NewUniform(color.RGBA{255, 0, 0, 255}), - FontOptions: render.FontOptions{ - Size: fontHeight, - }, - } - fallbackFont, err := fg.Generate() - if err != nil { - panic(err) - } - font.Fallbacks = append(font.Fallbacks, fallbackFont) - } - - strs := []string{ - "Latin-lower: abcdefghijklmnopqrstuvwxyz", - "Latin-upper: ABCDEFGHIJKLMNOPQRSTUVWXYZ", - "Greek-lower: αβγδεζηθικλμνχοπρσςτυφψω", - "Greek-upper: ΑΒΓΔΕΖΗΘΙΚΛΜΝΧΟΠΡΣΤΥΦΨΩ", - "Japanese-kana: あいえおうかきけこくはひへほふさしせそすまみめもむ", - "Kanji: 茂僕私華花日本英雄の時", - "Emoji: 😀😃😄😁😆😅😂🤣🐶🐱🐭🐹🐰🦊🐻🐼", - } - - y := 0.0 - for _, str := range strs { - render.Draw(font.NewText(str, 10, y), 0) - y += fontHeight - } - }, - }) - render.SetDrawStack( - render.NewCompositeR(), - ) - oak.Init("demo") -} diff --git a/examples/flappy-bird/README.md b/examples/flappy-bird/README.md new file mode 100644 index 00000000..f837af86 --- /dev/null +++ b/examples/flappy-bird/README.md @@ -0,0 +1,4 @@ +# Flappy Bird +A simple implementation of Flappy Bird + +![text](./example.PNG) \ No newline at end of file diff --git a/examples/flappy-bird/example.PNG b/examples/flappy-bird/example.PNG deleted file mode 100644 index c8487a3c..00000000 Binary files a/examples/flappy-bird/example.PNG and /dev/null differ diff --git a/examples/flappy-bird/example.gif b/examples/flappy-bird/example.gif new file mode 100644 index 00000000..57797085 Binary files /dev/null and b/examples/flappy-bird/example.gif differ diff --git a/examples/flappy-bird/main.go b/examples/flappy-bird/main.go index 6f6adbb3..2cde1e34 100644 --- a/examples/flappy-bird/main.go +++ b/examples/flappy-bird/main.go @@ -4,44 +4,41 @@ import ( "image/color" "time" - "github.com/oakmound/oak/v3/alg/range/floatrange" - "github.com/oakmound/oak/v3/mouse" - - oak "github.com/oakmound/oak/v3" - "github.com/oakmound/oak/v3/collision" - "github.com/oakmound/oak/v3/entities" - "github.com/oakmound/oak/v3/event" - "github.com/oakmound/oak/v3/key" - "github.com/oakmound/oak/v3/render" - "github.com/oakmound/oak/v3/scene" + "github.com/oakmound/oak/v4/alg/floatgeom" + "github.com/oakmound/oak/v4/alg/span" + "github.com/oakmound/oak/v4/mouse" + + oak "github.com/oakmound/oak/v4" + "github.com/oakmound/oak/v4/collision" + "github.com/oakmound/oak/v4/entities" + "github.com/oakmound/oak/v4/event" + "github.com/oakmound/oak/v4/key" + "github.com/oakmound/oak/v4/render" + "github.com/oakmound/oak/v4/scene" ) var ( - pillarFreq = floatrange.NewLinear(1, 5) - gapPosition = floatrange.NewLinear(10, 370) - gapSpan = floatrange.NewLinear(100, 250) - playerHitPillar bool - score int + score int ) -// This const block is used for determining what type -// of entity is colliding with what +// label pillars with a known constant, so when we hit them, we can restart the scene const ( - player collision.Label = iota - pillar + pillar collision.Label = iota ) func main() { - oak.AddScene("bounce", scene.Scene{Start: func(ctx *scene.Context) { - render.Draw(render.NewDrawFPS(0.03, nil, 10, 10)) + oak.AddScene("flappy", scene.Scene{Start: func(ctx *scene.Context) { + render.Draw(render.NewDrawFPS(0, nil, 10, 10), 2, 0) + render.Draw(render.NewLogicFPS(0, nil, 10, 20), 2, 0) score = 0 // 1. Make Player - newFlappy(90, 140) + newFlappy(ctx, 90, 140) // 2. Make scrolling repeating pillars + pillarFreq := span.NewLinear(1.0, 5.0) var pillarLoop func() pillarLoop = func() { - newPillarPair() + newPillarPair(ctx) ctx.DoAfter(time.Duration(pillarFreq.Poll()*float64(time.Second)), pillarLoop) } go ctx.DoAfter(time.Duration(pillarFreq.Poll()*float64(time.Second)), pillarLoop) @@ -49,103 +46,57 @@ func main() { // 3. Make Score t := render.DefaultFont().NewIntText(&score, 200, 30) render.Draw(t, 0) - }, Loop: func() bool { - if playerHitPillar { - playerHitPillar = false - return false - } - return true - }, End: func() (string, *scene.Result) { - return "bounce", nil }}) - render.SetDrawStack( - render.NewDynamicHeap(), - ) - oak.Init("bounce") -} - -// A Flappy is on a journey to go to the right -type Flappy struct { - *entities.Interactive + oak.Init("flappy") } -// Init satisfies the event.Entity interface -func (f *Flappy) Init() event.CID { - return event.NextID(f) -} - -func newFlappy(x, y float64) *Flappy { - f := new(Flappy) - f.Interactive = entities.NewInteractive(x, y, 32, 32, render.NewColorBox(32, 32, color.RGBA{0, 255, 255, 255}), nil, f.Init(), 1) - - f.RSpace.Add(pillar, func(s1, s2 *collision.Space) { - playerHitPillar = true - }) - f.RSpace.Space.Label = player - collision.Add(f.RSpace.Space) - - f.R.SetLayer(1) - render.Draw(f.R, 0) +func newFlappy(ctx *scene.Context, x, y float64) { + f := entities.New(ctx, + entities.WithRect(floatgeom.NewRect2WH(x, y, 32, 32)), + entities.WithColor(color.RGBA{0, 255, 255, 255}), + entities.WithDrawLayers([]int{0, 1}), + ) - f.Bind(event.Enter, func(event.CID, interface{}) int { - f.ShiftPos(f.Delta.X(), f.Delta.Y()) - f.Add(f.Delta) + event.Bind(ctx, event.Enter, f, func(f *entities.Entity, ev event.EnterPayload) event.Response { + f.ShiftDelta() if f.Delta.Y() > 10 { - f.Delta.SetY(10) + f.Delta[1] = 10 } if f.Delta.Y() < -5 { - f.Delta.SetY(-5) + f.Delta[1] = -5 } // Gravity - f.Delta.ShiftY(.15) + f.Delta[1] += .15 - <-f.RSpace.CallOnHits() - if f.Y()+f.H > 480 { - playerHitPillar = true + if collision.HitLabel(f.Space, pillar) != nil { + ctx.Window.NextScene() + } + + if f.Bottom() > 480 { + ctx.Window.NextScene() } if f.Y() < 0 { - f.SetY(0) - f.Delta.SetY(0) + f.ShiftY(-f.Y()) + f.Delta[1] = 0 } return 0 }) - f.Bind(mouse.Press, func(event.CID, interface{}) int { - f.Delta.ShiftY(-4) + event.Bind(ctx, mouse.Press, f, func(f *entities.Entity, _ *mouse.Event) event.Response { + f.Delta[1] -= 4 return 0 }) - f.Bind(key.Down+key.W, func(event.CID, interface{}) int { - f.Delta.ShiftY(-4) + event.Bind(ctx, key.Down(key.W), f, func(f *entities.Entity, _ key.Event) event.Response { + f.Delta[1] -= 4 return 0 }) - return f -} - -// A Pillar blocks flappy from continuing forward -type Pillar struct { - *entities.Solid - hasScored bool -} - -// Init satisfies the event.Entity interface -func (p *Pillar) Init() event.CID { - return event.NextID(p) } -func newPillar(x, y, h float64, isAbove bool) { - p := new(Pillar) - p.Solid = entities.NewSolid(x, y, 64, h, render.NewColorBox(64, int(h), color.RGBA{0, 255, 0, 255}), nil, p.Init()) - p.Space.Label = pillar - collision.Add(p.Space) - p.Bind(event.Enter, enterPillar) - p.R.SetLayer(1) - render.Draw(p.R, 0) - // Don't score one out of each two pillars - if isAbove { - p.hasScored = true - } -} +var ( + gapPosition = span.NewLinear(10.0, 370.0) + gapSpan = span.NewLinear(100.0, 250.0) +) -func newPillarPair() { +func newPillarPair(ctx *scene.Context) { pos := gapPosition.Poll() span := gapSpan.Poll() if (pos + span) > 470 { @@ -155,19 +106,30 @@ func newPillarPair() { pos = 370 span = 100 } - newPillar(641, 0, pos, true) - newPillar(641, pos+span, 480-(pos+span), false) + newPillar(ctx, 641, 0, pos, true) + newPillar(ctx, 641, pos+span, 480-(pos+span), false) } -func enterPillar(id event.CID, nothing interface{}) int { - p := event.GetEntity(id).(*Pillar) - p.ShiftX(-2) - if p.X()+p.W < 0 { - p.Destroy() - } - if !p.hasScored && p.X()+p.W < 90 { - p.hasScored = true - score++ +func newPillar(ctx *scene.Context, x, y, h float64, isAbove bool) { + p := entities.New(ctx, + entities.WithRect(floatgeom.NewRect2WH(x, y, 64, h)), + entities.WithColor(color.RGBA{0, 255, 0, 255}), + entities.WithLabel(pillar), + entities.WithDrawLayers([]int{0, 1}), + ) + event.Bind(ctx, event.Enter, p, enterPillar(isAbove)) +} + +func enterPillar(isAbove bool) func(p *entities.Entity, ev event.EnterPayload) event.Response { + return func(p *entities.Entity, ev event.EnterPayload) event.Response { + p.ShiftX(-2) + if p.X()+p.W() < 0 { + // don't score one out of each two pillars + if isAbove { + score++ + } + p.Destroy() + } + return 0 } - return 0 } diff --git a/examples/joystick-viz/main.go b/examples/joystick-viz/main.go index 946da118..33edc802 100644 --- a/examples/joystick-viz/main.go +++ b/examples/joystick-viz/main.go @@ -4,15 +4,15 @@ import ( "fmt" "time" - "github.com/oakmound/oak/v3/debugtools/inputviz" - "github.com/oakmound/oak/v3/render" + "github.com/oakmound/oak/v4/debugtools/inputviz" + "github.com/oakmound/oak/v4/render" - "github.com/oakmound/oak/v3/alg/floatgeom" - "github.com/oakmound/oak/v3/event" + "github.com/oakmound/oak/v4/alg/floatgeom" + "github.com/oakmound/oak/v4/event" - oak "github.com/oakmound/oak/v3" - "github.com/oakmound/oak/v3/joystick" - "github.com/oakmound/oak/v3/scene" + oak "github.com/oakmound/oak/v4" + "github.com/oakmound/oak/v4/joystick" + "github.com/oakmound/oak/v4/scene" ) func main() { @@ -22,19 +22,21 @@ func main() { *latestInput = "Latest Input: Keyboard+Mouse" ctx.DrawStack.Draw(render.NewStrPtrText(latestInput, 10, 460), 4) ctx.DrawStack.Draw(render.NewText("Space to Vibrate", 10, 440), 4) - ctx.EventHandler.GlobalBind(event.InputChange, func(_ event.CID, payload interface{}) int { - input := payload.(oak.InputType) + + event.GlobalBind(ctx, oak.InputChange, func(input oak.InputType) event.Response { + switch input { case oak.InputJoystick: *latestInput = "Latest Input: Joystick" - case oak.InputKeyboardMouse: - *latestInput = "Latest Input: Keyboard+Mouse" + case oak.InputKeyboard: + *latestInput = "Latest Input: Keyboard" + case oak.InputMouse: + *latestInput = "Latest Input: Mouse" } return 0 }) go func() { - rWidth := float64(ctx.Window.Width()) / 2 - rHeight := float64(ctx.Window.Height()) / 2 + rBounds := ctx.Window.Bounds().DivConst(2) jCh, cancel := joystick.WaitForJoysticks(1 * time.Second) defer cancel() for joy := range jCh { @@ -44,15 +46,15 @@ func main() { case 0: // 0,0 case 1: - x = rWidth + x = float64(rBounds.X()) case 2: - y = rHeight + y = float64(rBounds.Y()) case 3: - x = rWidth - y = rHeight + x = float64(rBounds.X()) + y = float64(rBounds.Y()) } jrend := inputviz.Joystick{ - Rect: floatgeom.NewRect2WH(x, y, rWidth, rHeight), + Rect: floatgeom.NewRect2WH(x, y, float64(rBounds.X()), float64(rBounds.Y())), StickDeadzone: 4000, BaseLayer: -1, } diff --git a/examples/keyboard-viz/main.go b/examples/keyboard-viz/main.go index ce10b8cc..332b316b 100644 --- a/examples/keyboard-viz/main.go +++ b/examples/keyboard-viz/main.go @@ -5,23 +5,26 @@ import ( "image" "image/color" - "github.com/oakmound/oak/v3" - "github.com/oakmound/oak/v3/alg/floatgeom" - "github.com/oakmound/oak/v3/debugtools/inputviz" - "github.com/oakmound/oak/v3/render" - "github.com/oakmound/oak/v3/scene" + "github.com/oakmound/oak/v4" + "github.com/oakmound/oak/v4/alg/floatgeom" + "github.com/oakmound/oak/v4/debugtools/inputviz" + "github.com/oakmound/oak/v4/dlog" + "github.com/oakmound/oak/v4/render" + "github.com/oakmound/oak/v4/scene" ) func main() { oak.AddScene("keyviz", scene.Scene{ Start: func(ctx *scene.Context) { + fmt.Println("start") fnt, _ := render.DefFontGenerator.RegenerateWith(func(fg render.FontGenerator) render.FontGenerator { fg.Color = image.NewUniform(color.RGBA{0, 0, 0, 255}) fg.Size = 13 return fg }) + bds := ctx.Window.Bounds() m := inputviz.Keyboard{ - Rect: floatgeom.NewRect2(0, 0, float64(ctx.Window.Width()), float64(ctx.Window.Height())), + Rect: floatgeom.NewRect2(0, 0, float64(bds.X()), float64(bds.Y())), BaseLayer: -1, RenderCharacters: true, Font: fnt, @@ -30,6 +33,7 @@ func main() { }, }) err := oak.Init("keyviz", func(c oak.Config) (oak.Config, error) { + c.Debug.Level = dlog.VERBOSE.String() c.Screen.Width = 800 c.Screen.Height = 300 return c, nil diff --git a/examples/mouse-viz/main.go b/examples/mouse-viz/main.go index 370f572f..6c0871b5 100644 --- a/examples/mouse-viz/main.go +++ b/examples/mouse-viz/main.go @@ -1,17 +1,18 @@ package main import ( - "github.com/oakmound/oak/v3" - "github.com/oakmound/oak/v3/alg/floatgeom" - "github.com/oakmound/oak/v3/debugtools/inputviz" - "github.com/oakmound/oak/v3/scene" + "github.com/oakmound/oak/v4" + "github.com/oakmound/oak/v4/alg/floatgeom" + "github.com/oakmound/oak/v4/debugtools/inputviz" + "github.com/oakmound/oak/v4/scene" ) func main() { oak.AddScene("mouseviz", scene.Scene{ Start: func(ctx *scene.Context) { + bds := ctx.Window.Bounds() m := inputviz.Mouse{ - Rect: floatgeom.NewRect2(0, 0, float64(ctx.Window.Width()), float64(ctx.Window.Height())), + Rect: floatgeom.NewRect2(0, 0, float64(bds.X()), float64(bds.Y())), BaseLayer: -1, } m.RenderAndListen(ctx, 0) diff --git a/examples/multi-window/README.md b/examples/multi-window/README.md new file mode 100644 index 00000000..b3b932f5 --- /dev/null +++ b/examples/multi-window/README.md @@ -0,0 +1,4 @@ +# Multi Window +An example of managing multiple windows. + +![text](./example.PNG) \ No newline at end of file diff --git a/examples/multi-window/main.go b/examples/multi-window/main.go index e07cfa15..c263786e 100644 --- a/examples/multi-window/main.go +++ b/examples/multi-window/main.go @@ -4,11 +4,11 @@ import ( "fmt" "image/color" - "github.com/oakmound/oak/v3" - "github.com/oakmound/oak/v3/event" - "github.com/oakmound/oak/v3/mouse" - "github.com/oakmound/oak/v3/render" - "github.com/oakmound/oak/v3/scene" + "github.com/oakmound/oak/v4" + "github.com/oakmound/oak/v4/event" + "github.com/oakmound/oak/v4/mouse" + "github.com/oakmound/oak/v4/render" + "github.com/oakmound/oak/v4/scene" ) func main() { @@ -26,10 +26,10 @@ func main() { ctx.DrawStack.Draw(cb, 0) dFPS := render.NewDrawFPS(0.1, nil, 600, 10) ctx.DrawStack.Draw(dFPS, 1) - ctx.EventHandler.GlobalBind(mouse.Press, mouse.Binding(func(_ event.CID, me *mouse.Event) int { + event.GlobalBind(ctx, mouse.Press, func(me *mouse.Event) event.Response { cb.SetPos(me.X(), me.Y()) return 0 - })) + }) }, }) go func() { @@ -55,10 +55,10 @@ func main() { ctx.DrawStack.Draw(cb, 0) dFPS := render.NewDrawFPS(0.1, nil, 600, 10) ctx.DrawStack.Draw(dFPS, 1) - ctx.EventHandler.GlobalBind(mouse.Press, mouse.Binding(func(_ event.CID, me *mouse.Event) int { + event.GlobalBind(ctx, mouse.Press, func(me *mouse.Event) event.Response { cb.SetPos(me.X(), me.Y()) return 0 - })) + }) }, }) c2.Init("scene2", func(c oak.Config) (oak.Config, error) { diff --git a/examples/particle-demo/main.go b/examples/particle-demo/main.go index 7112d4b1..d74096ad 100644 --- a/examples/particle-demo/main.go +++ b/examples/particle-demo/main.go @@ -6,19 +6,18 @@ import ( "log" "strconv" - oak "github.com/oakmound/oak/v3" - "github.com/oakmound/oak/v3/alg" - "github.com/oakmound/oak/v3/alg/range/floatrange" - "github.com/oakmound/oak/v3/alg/range/intrange" - "github.com/oakmound/oak/v3/debugstream" - "github.com/oakmound/oak/v3/event" - "github.com/oakmound/oak/v3/mouse" - "github.com/oakmound/oak/v3/oakerr" - "github.com/oakmound/oak/v3/physics" - "github.com/oakmound/oak/v3/render" - pt "github.com/oakmound/oak/v3/render/particle" - "github.com/oakmound/oak/v3/scene" - "github.com/oakmound/oak/v3/shape" + oak "github.com/oakmound/oak/v4" + "github.com/oakmound/oak/v4/alg" + "github.com/oakmound/oak/v4/alg/span" + "github.com/oakmound/oak/v4/debugstream" + "github.com/oakmound/oak/v4/event" + "github.com/oakmound/oak/v4/mouse" + "github.com/oakmound/oak/v4/oakerr" + "github.com/oakmound/oak/v4/physics" + "github.com/oakmound/oak/v4/render" + pt "github.com/oakmound/oak/v4/render/particle" + "github.com/oakmound/oak/v4/scene" + "github.com/oakmound/oak/v4/shape" ) var ( @@ -56,7 +55,7 @@ func parseShape(args []string) shape.Shape { func main() { debugstream.AddCommand(debugstream.Command{Name: "followMouse", Operation: func(args []string) string { - event.GlobalBind(event.Enter, func(event.CID, interface{}) int { + event.GlobalBind(event.DefaultBus, event.Enter, func(ev event.EnterPayload) event.Response { // It'd be interesting to attach to the mouse position src.SetPos(float64(mouse.LastEvent.X()), float64(mouse.LastEvent.Y())) return 0 @@ -80,9 +79,9 @@ func main() { return oakerr.UnsupportedFormat{Format: err.Error()}.Error() } if !two { - src.Generator.(pt.Sizeable).SetSize(intrange.NewConstant(f1)) + src.Generator.(pt.Sizeable).SetSize(span.NewConstant(f1)) } else { - src.Generator.(pt.Sizeable).SetSize(intrange.NewLinear(f1, f2)) + src.Generator.(pt.Sizeable).SetSize(span.NewLinear(f1, f2)) } return "" @@ -94,9 +93,9 @@ func main() { return oakerr.UnsupportedFormat{Format: err.Error()}.Error() } if !two { - src.Generator.(pt.Sizeable).SetEndSize(intrange.NewConstant(f1)) + src.Generator.(pt.Sizeable).SetEndSize(span.NewConstant(f1)) } else { - src.Generator.(pt.Sizeable).SetEndSize(intrange.NewLinear(f1, f2)) + src.Generator.(pt.Sizeable).SetEndSize(span.NewLinear(f1, f2)) } return "" }}) @@ -107,9 +106,9 @@ func main() { return oakerr.UnsupportedFormat{Format: err.Error()}.Error() } if !two { - src.Generator.GetBaseGenerator().NewPerFrame = floatrange.NewConstant(npf) + src.Generator.GetBaseGenerator().NewPerFrame = span.NewConstant(npf) } else { - src.Generator.GetBaseGenerator().NewPerFrame = floatrange.NewLinear(npf, npf2) + src.Generator.GetBaseGenerator().NewPerFrame = span.NewLinear(npf, npf2) } return "" }}) @@ -120,9 +119,9 @@ func main() { return oakerr.UnsupportedFormat{Format: err.Error()}.Error() } if !two { - src.Generator.GetBaseGenerator().LifeSpan = floatrange.NewConstant(npf) + src.Generator.GetBaseGenerator().LifeSpan = span.NewConstant(npf) } else { - src.Generator.GetBaseGenerator().LifeSpan = floatrange.NewLinear(npf, npf2) + src.Generator.GetBaseGenerator().LifeSpan = span.NewLinear(npf, npf2) } return "" }}) @@ -133,9 +132,9 @@ func main() { return oakerr.UnsupportedFormat{Format: err.Error()}.Error() } if !two { - src.Generator.GetBaseGenerator().Rotation = floatrange.NewConstant(npf) + src.Generator.GetBaseGenerator().Rotation = span.NewConstant(npf) } else { - src.Generator.GetBaseGenerator().Rotation = floatrange.NewLinear(npf, npf2) + src.Generator.GetBaseGenerator().Rotation = span.NewLinear(npf, npf2) } return "" }}) @@ -146,9 +145,9 @@ func main() { return oakerr.UnsupportedFormat{Format: err.Error()}.Error() } if !two { - src.Generator.GetBaseGenerator().Angle = floatrange.NewConstant(npf * alg.DegToRad) + src.Generator.GetBaseGenerator().Angle = span.NewConstant(npf * alg.DegToRad) } else { - src.Generator.GetBaseGenerator().Angle = floatrange.NewLinear(npf*alg.DegToRad, npf2*alg.DegToRad) + src.Generator.GetBaseGenerator().Angle = span.NewLinear(npf*alg.DegToRad, npf2*alg.DegToRad) } return "" }}) @@ -159,9 +158,9 @@ func main() { return oakerr.UnsupportedFormat{Format: err.Error()}.Error() } if !two { - src.Generator.GetBaseGenerator().Speed = floatrange.NewConstant(npf) + src.Generator.GetBaseGenerator().Speed = span.NewConstant(npf) } else { - src.Generator.GetBaseGenerator().Speed = floatrange.NewLinear(npf, npf2) + src.Generator.GetBaseGenerator().Speed = span.NewLinear(npf, npf2) } return "" }}) @@ -271,11 +270,11 @@ func main() { render.Draw(render.NewDrawFPS(0, nil, 10, 10)) x := 320.0 y := 240.0 - newPf := floatrange.NewLinear(1, 2) - life := floatrange.NewLinear(100, 120) - angle := floatrange.NewLinear(0, 360) - speed := floatrange.NewLinear(1, 5) - size := intrange.NewConstant(1) + newPf := span.NewLinear(1.0, 2.0) + life := span.NewLinear(100.0, 120.0) + angle := span.NewLinear(0.0, 360.0) + speed := span.NewLinear(1.0, 5.0) + size := span.NewConstant(1) layerFn := func(v physics.Vector) int { return 1 } @@ -284,6 +283,7 @@ func main() { endColor = color.RGBA{255, 255, 255, 255} endColorRand = color.RGBA{0, 0, 0, 0} shape := shape.Square + src = pt.NewColorGenerator( pt.Pos(x, y), pt.Duration(pt.Inf), diff --git a/examples/piano/example.gif b/examples/piano/example.gif new file mode 100644 index 00000000..b1d4e270 Binary files /dev/null and b/examples/piano/example.gif differ diff --git a/examples/piano/main.go b/examples/piano/main.go index f2bbae6f..e746151f 100644 --- a/examples/piano/main.go +++ b/examples/piano/main.go @@ -7,19 +7,22 @@ import ( "image/draw" "math" "os" + "strconv" "sync" + "time" - "github.com/oakmound/oak/v3" - "github.com/oakmound/oak/v3/audio/klang" - "github.com/oakmound/oak/v3/audio/pcm" - "github.com/oakmound/oak/v3/audio/synth" - "github.com/oakmound/oak/v3/dlog" - "github.com/oakmound/oak/v3/entities" - "github.com/oakmound/oak/v3/event" - "github.com/oakmound/oak/v3/key" - "github.com/oakmound/oak/v3/mouse" - "github.com/oakmound/oak/v3/render" - "github.com/oakmound/oak/v3/scene" + "github.com/oakmound/oak/v4" + "github.com/oakmound/oak/v4/alg/floatgeom" + "github.com/oakmound/oak/v4/audio" + "github.com/oakmound/oak/v4/audio/pcm" + "github.com/oakmound/oak/v4/audio/synth" + "github.com/oakmound/oak/v4/dlog" + "github.com/oakmound/oak/v4/entities" + "github.com/oakmound/oak/v4/event" + "github.com/oakmound/oak/v4/key" + "github.com/oakmound/oak/v4/mouse" + "github.com/oakmound/oak/v4/render" + "github.com/oakmound/oak/v4/scene" ) const ( @@ -60,7 +63,7 @@ func (kc keyColor) Color() color.RGBA { return color.RGBA{255, 255, 255, 255} } -func newKey(note synth.Pitch, c keyColor, k string) *entities.Solid { +func newKey(ctx *scene.Context, note synth.Pitch, c keyColor, k key.Code) *entities.Entity { w := c.Width() h := c.Height() clr := c.Color() @@ -84,7 +87,11 @@ func newKey(note synth.Pitch, c keyColor, k string) *entities.Solid { render.NewLine(w, 0, 0, 0, color.RGBA{0, 0, 0, 255}), ).ToSprite(), }) - s := entities.NewSolid(0, 0, w, h, sw, mouse.DefaultTree, 0) + s := entities.New(ctx, + entities.WithUseMouseTree(true), + entities.WithDimensions(floatgeom.Point2{w, h}), + entities.WithRenderable(sw), + ) if c == keyColorBlack { s.Space.SetZLayer(1) s.Space.Label = labelBlackKey @@ -92,31 +99,30 @@ func newKey(note synth.Pitch, c keyColor, k string) *entities.Solid { s.Space.SetZLayer(2) s.Space.Label = labelWhiteKey } - mouse.UpdateSpace(s.X(), s.Y(), s.W, s.H, s.Space) - s.Bind(key.Down+k, func(c event.CID, i interface{}) int { - if oak.IsDown(key.LeftShift) || oak.IsDown(key.RightShift) { + event.GlobalBind(ctx, key.Down(k), func(ev key.Event) event.Response { + // TODO: add helper function for this? + if ev.Modifiers&key.ModShift == key.ModShift { return 0 } - playPitch(note) + playPitch(ctx, note) sw.Set("down") return 0 }) - s.Bind(key.Up+k, func(c event.CID, i interface{}) int { - if oak.IsDown(key.LeftShift) || oak.IsDown(key.RightShift) { + event.GlobalBind(ctx, key.Up(k), func(ev key.Event) event.Response { + if ev.Modifiers&key.ModShift == key.ModShift { return 0 } releasePitch(note) sw.Set("up") return 0 }) - s.Bind(mouse.PressOn, func(c event.CID, i interface{}) int { - playPitch(note) - me := i.(*mouse.Event) + event.Bind(ctx, mouse.PressOn, s, func(_ *entities.Entity, me *mouse.Event) event.Response { + playPitch(ctx, note) me.StopPropagation = true sw.Set("down") return 0 }) - s.Bind(mouse.Release, func(c event.CID, i interface{}) int { + event.Bind(ctx, mouse.Release, s, func(_ *entities.Entity, me *mouse.Event) event.Response { releasePitch(note) sw.Set("up") return 0 @@ -130,45 +136,30 @@ type keyDef struct { x float64 } -var keycharOrder = []string{ - "Z", "S", "X", "D", "C", - "V", "G", "B", "H", "N", "J", "M", - key.Comma, "L", key.Period, key.Semicolon, key.Slash, - "Q", "2", "W", "3", "E", "4", "R", - "T", "6", "Y", "7", "U", - "I", "9", "O", "0", "P", key.HyphenMinus, key.LeftSquareBracket, +var keycharOrder = []key.Code{ + key.Z, key.S, key.X, key.D, key.C, + key.V, key.G, key.B, key.H, key.N, key.J, key.M, + key.Comma, key.L, key.FullStop, key.Semicolon, key.Slash, + key.Q, key.Num2, key.W, key.Num3, key.E, key.Num4, key.R, + key.T, key.Num6, key.Y, key.Num7, key.U, + key.I, key.Num9, key.O, key.Num0, key.P, key.HyphenMinus, key.LeftSquareBracket, } var playLock sync.Mutex var cancelFuncs = map[synth.Pitch]func(){} -var synthKind func(...synth.Option) (pcm.Reader, error) +var makeSynth func(ctx context.Context, pitch synth.Pitch) -func playPitch(pitch synth.Pitch) { +func playPitch(ctx *scene.Context, pitch synth.Pitch) { playLock.Lock() defer playLock.Unlock() if cancel, ok := cancelFuncs[pitch]; ok { cancel() } - a, _ := synthKind(synth.AtPitch(pitch)) - toPlay := pcm.LoopReader(a) - format := toPlay.PCMFormat() - speaker, err := pcm.NewWriter(format) - if err != nil { - fmt.Println("new writer failed:", err) - return - } - monitor := newPCMMonitor(speaker) - monitor.SetPos(0, 0) - render.Draw(monitor) - ctx, cancel := context.WithCancel(context.Background()) + + gctx, cancel := context.WithCancel(ctx) go func() { - err = pcm.Play(ctx, monitor, toPlay) - if err != nil { - fmt.Println("play error:", err) - } - speaker.Close() - monitor.Undraw() + makeSynth(gctx, pitch) }() cancelFuncs[pitch] = cancel } @@ -182,8 +173,30 @@ func releasePitch(pitch synth.Pitch) { } } +type pitchText struct { + pitch *synth.Pitch +} + +func (pt *pitchText) String() string { + if pt.pitch == nil { + return "" + } + return pt.pitch.String() + " - " + strconv.Itoa(int(*pt.pitch)) +} + +type f64Text struct { + f64 *float64 +} + +func (ft *f64Text) String() string { + if ft.f64 == nil { + return "" + } + return fmt.Sprint(*ft.f64) +} + func main() { - err := pcm.InitDefault() + err := audio.InitDefault() if err != nil { fmt.Println("init failed:", err) os.Exit(1) @@ -191,26 +204,56 @@ func main() { oak.AddScene("piano", scene.Scene{ Start: func(ctx *scene.Context) { - src := synth.Int16 - src.Format = klang.Format{ - SampleRate: 40000, + var src = new(synth.Source) + *src = synth.Int16 + src.Format = pcm.Format{ + SampleRate: 80000, Channels: 2, Bits: 32, } - synthKind = src.SinPCM + pt := &pitchText{} + ft := &f64Text{} + playWithMonitor := func(gctx context.Context, r pcm.Reader) { + speaker, err := audio.NewWriter(r.PCMFormat()) + if err != nil { + fmt.Println("new writer failed:", err) + return + } + monitor := newPCMMonitor(ctx, speaker) + monitor.SetPos(0, 0) + render.Draw(monitor) + + pitchDetector := synth.NewPitchDetector(r) + pt.pitch = &pitchDetector.DetectedPitches[0] + ft.f64 = &pitchDetector.DetectedRawPitches[0] + + audio.Play(gctx, pitchDetector, func(po *audio.PlayOptions) { + po.Destination = monitor + }) + speaker.Close() + monitor.Undraw() + } + makeSynth = func(gctx context.Context, pitch synth.Pitch) { + toPlay := audio.LoopReader(src.Sin(synth.AtPitch(pitch))) + fadeIn := audio.FadeIn(100*time.Millisecond, toPlay) + playWithMonitor(gctx, fadeIn) + } + render.Draw(render.NewStringerText(pt, 10, 10)) + render.Draw(render.NewStringerText(ft, 10, 20)) + pitch := synth.C3 kc := keyColorWhite x := 20.0 y := 200.0 i := 0 - for i < len(keycharOrder) && x+kc.Width() < float64(ctx.Window.Width()-10) { - ky := newKey(pitch, kc, keycharOrder[i]) - ky.SetPos(x, y) + for i < len(keycharOrder) && x+kc.Width() < float64(ctx.Window.Bounds().X()-10) { + ky := newKey(ctx, pitch, kc, keycharOrder[i]) + ky.SetPos(floatgeom.Point2{x, y}) layer := 0 if kc == keyColorBlack { layer = 1 } - render.Draw(ky.R, layer) + render.Draw(ky.Renderable, layer) x += kc.Width() pitch = pitch.Up(synth.HalfStep) if pitch.IsAccidental() { @@ -223,36 +266,71 @@ func main() { i++ } // Consider: Adding volume control - event.GlobalBind(key.Down+key.S, func(c event.CID, i interface{}) int { - if oak.IsDown(key.LeftShift) || oak.IsDown(key.RightShift) { - synthKind = src.SinPCM - } - return 0 - }) - event.GlobalBind(key.Down+key.W, func(c event.CID, i interface{}) int { - if oak.IsDown(key.LeftShift) || oak.IsDown(key.RightShift) { - synthKind = src.SawPCM - } - return 0 - }) - event.GlobalBind(key.Down+key.T, func(c event.CID, i interface{}) int { - if oak.IsDown(key.LeftShift) || oak.IsDown(key.RightShift) { - synthKind = src.TrianglePCM - } - return 0 - }) - event.GlobalBind(key.Down+key.P, func(c event.CID, i interface{}) int { - if oak.IsDown(key.LeftShift) || oak.IsDown(key.RightShift) { - synthKind = src.PulsePCM(2) - } - return 0 - }) + codeKinds := map[key.Code]func(ctx context.Context, pitch synth.Pitch){ + key.S: func(gctx context.Context, pitch synth.Pitch) { + toPlay := audio.LoopReader(src.Sin(synth.AtPitch(pitch))) + fadeIn := audio.FadeIn(100*time.Millisecond, toPlay) + playWithMonitor(gctx, fadeIn) + }, + key.W: func(gctx context.Context, pitch synth.Pitch) { + toPlay := audio.LoopReader(src.Saw(synth.AtPitch(pitch))) + fadeIn := audio.FadeIn(100*time.Millisecond, toPlay) + playWithMonitor(gctx, fadeIn) + }, + key.Q: func(gctx context.Context, pitch synth.Pitch) { + // demonstrate adding waveforms to play in unison + unison := 4 + for i := 0; i < unison; i++ { + go playWithMonitor(gctx, audio.FadeIn(100*time.Millisecond, audio.LoopReader(src.Saw(synth.AtPitch(pitch))))) + go playWithMonitor(gctx, audio.FadeIn(100*time.Millisecond, audio.LoopReader(src.Saw(synth.AtPitch(pitch), synth.Detune(.04))))) + go playWithMonitor(gctx, audio.FadeIn(100*time.Millisecond, audio.LoopReader(src.Saw(synth.AtPitch(pitch), synth.Detune(-.05))))) + } + playWithMonitor(gctx, audio.FadeIn(100*time.Millisecond, audio.LoopReader(src.Saw(synth.AtPitch(pitch))))) + }, + key.T: func(gctx context.Context, pitch synth.Pitch) { + toPlay := audio.LoopReader(src.Triangle(synth.AtPitch(pitch))) + fadeIn := audio.FadeIn(100*time.Millisecond, toPlay) + playWithMonitor(gctx, fadeIn) + }, + key.P: func(gctx context.Context, pitch synth.Pitch) { + toPlay := audio.LoopReader(src.Pulse(2)(synth.AtPitch(pitch))) + fadeIn := audio.FadeIn(100*time.Millisecond, toPlay) + playWithMonitor(gctx, fadeIn) + }, + key.N: func(gctx context.Context, pitch synth.Pitch) { + toPlay := audio.LoopReader(src.Noise(synth.AtPitch(pitch))) + fadeIn := audio.FadeIn(100*time.Millisecond, toPlay) + playWithMonitor(gctx, fadeIn) + }, + key.X: func(gctx context.Context, pitch synth.Pitch) { + // demonstrate combining multiple wave forms in place + toPlay := src.MultiWave([]synth.Waveform{ + synth.Source.SinWave, + synth.Source.TriangleWave, + synth.PulseWave(2), + }, synth.AtPitch(pitch)) + fadeIn := audio.FadeIn(100*time.Millisecond, toPlay) + playWithMonitor(gctx, fadeIn) + + }, + } + for kc, synfn := range codeKinds { + synfn := synfn + kc := kc + event.GlobalBind(ctx, key.Down(kc), func(ev key.Event) event.Response { + if ev.Modifiers&key.ModShift == key.ModShift { + makeSynth = synfn + } + return 0 + }) + } + help1 := render.NewText("Shift+([S]in/[T]ri/[P]ulse/sa[W]) to change wave style", 10, 500) help2 := render.NewText("Keyboard / mouse to play", 10, 520) render.Draw(help1) render.Draw(help2) - event.GlobalBind(mouse.ScrollDown, func(c event.CID, i interface{}) int { + event.GlobalBind(ctx, mouse.ScrollDown, func(_ *mouse.Event) event.Response { mag := globalMagnification - 0.05 if mag < 1 { mag = 1 @@ -260,10 +338,23 @@ func main() { globalMagnification = mag return 0 }) - event.GlobalBind(mouse.ScrollUp, func(c event.CID, i interface{}) int { + event.GlobalBind(ctx, mouse.ScrollUp, func(_ *mouse.Event) event.Response { globalMagnification += 0.05 return 0 }) + event.GlobalBind(ctx, key.Down(key.Keypad0), func(_ key.Event) event.Response { + // TODO: synth all sound like pulse waves at 8 bit + src.Bits = 8 + return 0 + }) + event.GlobalBind(ctx, key.Down(key.Keypad1), func(_ key.Event) event.Response { + src.Bits = 16 + return 0 + }) + event.GlobalBind(ctx, key.Down(key.Keypad2), func(_ key.Event) event.Response { + src.Bits = 32 + return 0 + }) }, }) oak.Init("piano", func(c oak.Config) (oak.Config, error) { @@ -275,7 +366,7 @@ func main() { } type pcmMonitor struct { - event.CID + event.CallerID render.LayeredPoint pcm.Writer pcm.Format @@ -285,21 +376,19 @@ type pcmMonitor struct { var globalMagnification float64 = 1 -func newPCMMonitor(w pcm.Writer) *pcmMonitor { +func newPCMMonitor(ctx *scene.Context, w pcm.Writer) *pcmMonitor { fmt := w.PCMFormat() pm := &pcmMonitor{ Writer: w, Format: w.PCMFormat(), LayeredPoint: render.NewLayeredPoint(0, 0, 0), - written: make([]byte, fmt.BytesPerSecond()*pcm.WriterBufferLengthInSeconds), + written: make([]byte, int(float64(fmt.BytesPerSecond())*audio.WriterBufferLengthInSeconds)), } - pm.Init() return pm } -func (pm *pcmMonitor) Init() event.CID { - pm.CID = event.NextID(pm) - return pm.CID +func (pm *pcmMonitor) CID() event.CallerID { + return pm.CallerID } func (pm *pcmMonitor) PCMFormat() pcm.Format { @@ -327,6 +416,9 @@ func (pm *pcmMonitor) Draw(buf draw.Image, xOff, yOff float64) { var val int16 switch pm.Format.Bits { + case 8: + val8 := pm.written[wIndex] + val = int16(val8) << 8 case 16: wIndex -= wIndex % 2 val = int16(pm.written[wIndex+1])<<8 + diff --git a/examples/platformer-tutorial/1-start/start.go b/examples/platformer-tutorial/1-start/start.go deleted file mode 100644 index f7158df2..00000000 --- a/examples/platformer-tutorial/1-start/start.go +++ /dev/null @@ -1,21 +0,0 @@ -package main - -import ( - "image/color" - - oak "github.com/oakmound/oak/v3" - "github.com/oakmound/oak/v3/entities" - "github.com/oakmound/oak/v3/render" - "github.com/oakmound/oak/v3/scene" -) - -func main() { - oak.AddScene("platformer", scene.Scene{Start: func(*scene.Context) { - char := entities.NewMoving(100, 100, 16, 32, - render.NewColorBox(16, 32, color.RGBA{255, 0, 0, 255}), - nil, 0, 0) - - render.Draw(char.R) - }}) - oak.Init("platformer") -} diff --git a/examples/platformer-tutorial/2-moving/moving.go b/examples/platformer-tutorial/2-moving/moving.go deleted file mode 100644 index f50cfd82..00000000 --- a/examples/platformer-tutorial/2-moving/moving.go +++ /dev/null @@ -1,41 +0,0 @@ -package main - -import ( - "image/color" - - "github.com/oakmound/oak/v3/physics" - - "github.com/oakmound/oak/v3/event" - "github.com/oakmound/oak/v3/key" - - oak "github.com/oakmound/oak/v3" - "github.com/oakmound/oak/v3/entities" - "github.com/oakmound/oak/v3/render" - "github.com/oakmound/oak/v3/scene" -) - -func main() { - oak.AddScene("platformer", scene.Scene{Start: func(*scene.Context) { - - char := entities.NewMoving(100, 100, 16, 32, - render.NewColorBox(16, 32, color.RGBA{255, 0, 0, 255}), - nil, 0, 0) - - render.Draw(char.R) - - char.Speed = physics.NewVector(3, 3) - - char.Bind(event.Enter, func(id event.CID, nothing interface{}) int { - char := event.GetEntity(id).(*entities.Moving) - // Move left and right with A and D - if oak.IsDown(key.A) { - char.ShiftX(-char.Speed.X()) - } - if oak.IsDown(key.D) { - char.ShiftX(char.Speed.X()) - } - return 0 - }) - }}) - oak.Init("platformer") -} diff --git a/examples/platformer-tutorial/3-falling/falling.go b/examples/platformer-tutorial/3-falling/falling.go deleted file mode 100644 index cad71106..00000000 --- a/examples/platformer-tutorial/3-falling/falling.go +++ /dev/null @@ -1,68 +0,0 @@ -package main - -import ( - "image/color" - - "github.com/oakmound/oak/v3/collision" - - "github.com/oakmound/oak/v3/physics" - - "github.com/oakmound/oak/v3/event" - "github.com/oakmound/oak/v3/key" - - oak "github.com/oakmound/oak/v3" - "github.com/oakmound/oak/v3/entities" - "github.com/oakmound/oak/v3/render" - "github.com/oakmound/oak/v3/scene" -) - -// Collision labels -const ( - // The only collision label we need for this demo is 'ground', - // indicating something we shouldn't be able to fall or walk through - Ground collision.Label = 1 -) - -func main() { - oak.AddScene("platformer", scene.Scene{Start: func(*scene.Context) { - - char := entities.NewMoving(100, 100, 16, 32, - render.NewColorBox(16, 32, color.RGBA{255, 0, 0, 255}), - nil, 0, 0) - - render.Draw(char.R) - - char.Speed = physics.NewVector(3, 3) - - fallSpeed := .1 - - char.Bind(event.Enter, func(id event.CID, nothing interface{}) int { - char := event.GetEntity(id).(*entities.Moving) - // Move left and right with A and D - if oak.IsDown(key.A) { - char.ShiftX(-char.Speed.X()) - } - if oak.IsDown(key.D) { - char.ShiftX(char.Speed.X()) - } - hit := char.HitLabel(Ground) - if hit == nil { - // Fall if there's no ground - char.Delta.ShiftY(fallSpeed) - } else { - char.Delta.SetY(0) - } - char.ShiftY(char.Delta.Y()) - return 0 - }) - - ground := entities.NewSolid(0, 400, 500, 20, - render.NewColorBox(500, 20, color.RGBA{0, 0, 255, 255}), - nil, 0) - ground.UpdateLabel(Ground) - - render.Draw(ground.R) - - }}) - oak.Init("platformer") -} diff --git a/examples/platformer-tutorial/4-jumping/jumping.go b/examples/platformer-tutorial/4-jumping/jumping.go deleted file mode 100644 index ea83252a..00000000 --- a/examples/platformer-tutorial/4-jumping/jumping.go +++ /dev/null @@ -1,72 +0,0 @@ -package main - -import ( - "image/color" - - "github.com/oakmound/oak/v3/collision" - - "github.com/oakmound/oak/v3/physics" - - "github.com/oakmound/oak/v3/event" - "github.com/oakmound/oak/v3/key" - - oak "github.com/oakmound/oak/v3" - "github.com/oakmound/oak/v3/entities" - "github.com/oakmound/oak/v3/render" - "github.com/oakmound/oak/v3/scene" -) - -// Collision labels -const ( - // The only collision label we need for this demo is 'ground', - // indicating something we shouldn't be able to fall or walk through - Ground collision.Label = 1 -) - -func main() { - oak.AddScene("platformer", scene.Scene{Start: func(*scene.Context) { - - char := entities.NewMoving(100, 100, 16, 32, - render.NewColorBox(16, 32, color.RGBA{255, 0, 0, 255}), - nil, 0, 0) - - render.Draw(char.R) - - char.Speed = physics.NewVector(3, 3) - - fallSpeed := .1 - - char.Bind(event.Enter, func(id event.CID, nothing interface{}) int { - char := event.GetEntity(id).(*entities.Moving) - // Move left and right with A and D - if oak.IsDown(key.A) { - char.ShiftX(-char.Speed.X()) - } - if oak.IsDown(key.D) { - char.ShiftX(char.Speed.X()) - } - hit := collision.HitLabel(char.Space, Ground) - if hit == nil { - // Fall if there's no ground - char.Delta.ShiftY(fallSpeed) - } else { - char.Delta.SetY(0) - // Jump with Space - if oak.IsDown(key.Spacebar) { - char.Delta.ShiftY(-char.Speed.Y()) - } - } - char.ShiftY(char.Delta.Y()) - return 0 - }) - - ground := entities.NewSolid(0, 400, 500, 20, - render.NewColorBox(500, 20, color.RGBA{0, 0, 255, 255}), - nil, 0) - ground.UpdateLabel(Ground) - - render.Draw(ground.R) - - }}) - oak.Init("platformer") -} diff --git a/examples/platformer-tutorial/5-correct-jumping/correct-jumping.go b/examples/platformer-tutorial/5-correct-jumping/correct-jumping.go deleted file mode 100644 index 6691f3db..00000000 --- a/examples/platformer-tutorial/5-correct-jumping/correct-jumping.go +++ /dev/null @@ -1,78 +0,0 @@ -package main - -import ( - "image/color" - - "github.com/oakmound/oak/v3/collision" - - "github.com/oakmound/oak/v3/physics" - - "github.com/oakmound/oak/v3/event" - "github.com/oakmound/oak/v3/key" - - oak "github.com/oakmound/oak/v3" - "github.com/oakmound/oak/v3/entities" - "github.com/oakmound/oak/v3/render" - "github.com/oakmound/oak/v3/scene" -) - -// Collision labels -const ( - // The only collision label we need for this demo is 'ground', - // indicating something we shouldn't be able to fall or walk through - Ground collision.Label = 1 -) - -func main() { - oak.AddScene("platformer", scene.Scene{Start: func(*scene.Context) { - - char := entities.NewMoving(100, 100, 16, 32, - render.NewColorBox(16, 32, color.RGBA{255, 0, 0, 255}), - nil, 0, 0) - - render.Draw(char.R) - - char.Speed = physics.NewVector(3, 3) - - fallSpeed := .1 - - char.Bind(event.Enter, func(id event.CID, nothing interface{}) int { - char := event.GetEntity(id).(*entities.Moving) - // Move left and right with A and D - if oak.IsDown(key.A) { - char.ShiftX(-char.Speed.X()) - } - if oak.IsDown(key.D) { - char.ShiftX(char.Speed.X()) - } - oldY := char.Y() - char.ShiftY(char.Delta.Y()) - hit := collision.HitLabel(char.Space, Ground) - - // If we've moved in y value this frame and in the last frame, - // we were below what we're trying to hit, we are still falling - if hit != nil && !(oldY != char.Y() && oldY+char.H > hit.Y()) { - // Correct our y if we started falling into the ground - char.SetY(hit.Y() - char.H) - char.Delta.SetY(0) - // Jump with Space - if oak.IsDown(key.Spacebar) { - char.Delta.ShiftY(-char.Speed.Y()) - } - } else { - // Fall if there's no ground - char.Delta.ShiftY(fallSpeed) - } - return 0 - }) - - ground := entities.NewSolid(0, 400, 500, 20, - render.NewColorBox(500, 20, color.RGBA{0, 0, 255, 255}), - nil, 0) - ground.UpdateLabel(Ground) - - render.Draw(ground.R) - - }}) - oak.Init("platformer") -} diff --git a/examples/platformer-tutorial/README.md b/examples/platformer-tutorial/README.md deleted file mode 100644 index 3a8a0268..00000000 --- a/examples/platformer-tutorial/README.md +++ /dev/null @@ -1,17 +0,0 @@ -# Platformers -Check out how to make a simple 2d platformer. This has been broken into parts leading up to putting it all together. - -## start -Learn to set up an oak scene with a single potentially movable character. -## moving -Get your character to move left and right. -## falling -Implement basic collision and make the character fall if they are not on solid ground. -## jumping -Bind the ability to jump if we are on the ground. -## correct jumping -Update ground detection to only be able to jump on the ground not when falling through ground. -## complete -Put it all together! - -![Put it all together!](./6-complete/example.gif) \ No newline at end of file diff --git a/examples/platformer-tutorial/6-complete/example.gif b/examples/platformer/example.gif similarity index 100% rename from examples/platformer-tutorial/6-complete/example.gif rename to examples/platformer/example.gif diff --git a/examples/platformer-tutorial/6-complete/complete.go b/examples/platformer/main.go similarity index 60% rename from examples/platformer-tutorial/6-complete/complete.go rename to examples/platformer/main.go index 5decabf7..ce5b056c 100644 --- a/examples/platformer-tutorial/6-complete/complete.go +++ b/examples/platformer/main.go @@ -4,19 +4,16 @@ import ( "image/color" "math" - "github.com/oakmound/oak/v3/alg/floatgeom" + "github.com/oakmound/oak/v4/alg/floatgeom" - "github.com/oakmound/oak/v3/collision" + "github.com/oakmound/oak/v4/collision" - "github.com/oakmound/oak/v3/physics" + "github.com/oakmound/oak/v4/event" + "github.com/oakmound/oak/v4/key" - "github.com/oakmound/oak/v3/event" - "github.com/oakmound/oak/v3/key" - - oak "github.com/oakmound/oak/v3" - "github.com/oakmound/oak/v3/entities" - "github.com/oakmound/oak/v3/render" - "github.com/oakmound/oak/v3/scene" + oak "github.com/oakmound/oak/v4" + "github.com/oakmound/oak/v4/entities" + "github.com/oakmound/oak/v4/scene" ) const ( @@ -26,59 +23,55 @@ const ( ) func main() { - oak.AddScene("platformer", scene.Scene{Start: func(*scene.Context) { - - char := entities.NewMoving(100, 100, 16, 32, - render.NewColorBox(16, 32, color.RGBA{255, 0, 0, 255}), - nil, 0, 0) - - render.Draw(char.R) + oak.AddScene("platformer", scene.Scene{Start: func(ctx *scene.Context) { - char.Speed = physics.NewVector(3, 7) + char := entities.New(ctx, + entities.WithRect(floatgeom.NewRect2WH(100, 100, 16, 32)), + entities.WithColor(color.RGBA{255, 0, 0, 255}), + entities.WithSpeed(floatgeom.Point2{3, 7}), + ) - fallSpeed := .2 + const fallSpeed = .2 - char.Bind(event.Enter, func(id event.CID, nothing interface{}) int { - char := event.GetEntity(id).(*entities.Moving) + event.Bind(ctx, event.Enter, char, func(c *entities.Entity, ev event.EnterPayload) event.Response { // Move left and right with A and D if oak.IsDown(key.A) { - char.Delta.SetX(-char.Speed.X()) + char.Delta[0] = -char.Speed.X() } else if oak.IsDown(key.D) { - char.Delta.SetX(char.Speed.X()) + char.Delta[0] = char.Speed.X() } else { - char.Delta.SetX(0) + char.Delta[0] = (0) } - oldX, oldY := char.GetPos() - char.ShiftPos(char.Delta.X(), char.Delta.Y()) - + oldX, oldY := char.X(), char.Y() + char.ShiftDelta() aboveGround := false hit := collision.HitLabel(char.Space, Ground) // If we've moved in y value this frame and in the last frame, // we were below what we're trying to hit, we are still falling - if hit != nil && !(oldY != char.Y() && oldY+char.H > hit.Y()) { + if hit != nil && !(oldY != char.Y() && oldY+char.H() > hit.Y()) { // Correct our y if we started falling into the ground - char.SetY(hit.Y() - char.H) + char.SetY(hit.Y() - char.H()) // Stop falling - char.Delta.SetY(0) + char.Delta[1] = 0 // Jump with Space when on the ground if oak.IsDown(key.Spacebar) { - char.Delta.ShiftY(-char.Speed.Y()) + char.Delta[1] -= char.Speed.Y() } aboveGround = true } else { //Restart when is below ground if char.Y() > 500 { - char.Delta.SetY(0) + char.Delta[1] = 0 char.SetY(100) char.SetX(100) } // Fall if there's no ground - char.Delta.ShiftY(fallSpeed) + char.Delta[1] += fallSpeed } if hit != nil { @@ -103,7 +96,7 @@ func main() { } char.SetX(oldX + xbump) if char.Delta.Y() < 0 { - char.Delta.SetY(0) + char.Delta[1] = 0 } } @@ -113,7 +106,7 @@ func main() { if !aboveGround && math.Abs(xover) > 1 { // We add a buffer so this doesn't retrigger immediately char.SetY(oldY + 1) - char.Delta.SetY(fallSpeed) + char.Delta[1] = fallSpeed } } @@ -128,12 +121,11 @@ func main() { } for _, p := range platforms { - ground := entities.NewSolid(p.Min.X(), p.Min.Y(), p.W(), p.H(), - render.NewColorBox(int(p.W()), int(p.H()), color.RGBA{0, 0, 255, 255}), - nil, 0) - ground.UpdateLabel(Ground) - - render.Draw(ground.R) + entities.New(ctx, + entities.WithRect(p), + entities.WithColor(color.RGBA{0, 0, 255, 255}), + entities.WithLabel(Ground), + ) } }}) diff --git a/examples/pong/main.go b/examples/pong/main.go index ec8efbeb..b67aa0fb 100644 --- a/examples/pong/main.go +++ b/examples/pong/main.go @@ -5,13 +5,14 @@ import ( "math" "math/rand" - oak "github.com/oakmound/oak/v3" - "github.com/oakmound/oak/v3/collision" - "github.com/oakmound/oak/v3/entities" - "github.com/oakmound/oak/v3/event" - "github.com/oakmound/oak/v3/key" - "github.com/oakmound/oak/v3/render" - "github.com/oakmound/oak/v3/scene" + oak "github.com/oakmound/oak/v4" + "github.com/oakmound/oak/v4/alg/floatgeom" + "github.com/oakmound/oak/v4/collision" + "github.com/oakmound/oak/v4/entities" + "github.com/oakmound/oak/v4/event" + "github.com/oakmound/oak/v4/key" + "github.com/oakmound/oak/v4/render" + "github.com/oakmound/oak/v4/scene" ) var ( @@ -25,75 +26,74 @@ const ( func main() { oak.AddScene("pong", - scene.Scene{Start: func(*scene.Context) { - newPaddle(20, 200, 1) - newPaddle(600, 200, 2) - newBall(320, 240) - render.Draw(render.DefaultFont().NewIntText(&score2, 200, 20), 3) - render.Draw(render.DefaultFont().NewIntText(&score1, 400, 20), 3) + scene.Scene{Start: func(ctx *scene.Context) { + newPaddle(ctx, 20, 200, 1) + newPaddle(ctx, 600, 200, 2) + newBall(ctx, 320, 240) + ctx.Draw(render.NewIntText(&score2, 200, 20), 3) + ctx.Draw(render.NewIntText(&score1, 400, 20), 3) }}) - oak.Init("pong", func(c oak.Config) (oak.Config, error) { - c.DrawFrameRate = 120 - return c, nil - }) + oak.Init("pong") } -func newBall(x, y float64) { - b := entities.NewMoving(x, y, 10, 10, render.NewColorBoxR(10, 10, color.RGBA{255, 255, 255, 255}), nil, 0, 0) - render.Draw(b.R, 2) - b.Bind(event.Enter, func(id event.CID, nothing interface{}) int { - if b.Delta.X() == 0 && b.Delta.Y() == 0 { - b.Delta.SetY((rand.Float64() - 0.5) * 4) - b.Delta.SetX((rand.Float64() - 0.5) * 16) - if math.Abs(b.Delta.X()) < 0.1 { - b.Delta.SetX(8) - } - } - b.ShiftPos(b.Delta.X(), b.Delta.Y()) - if collision.HitLabel(b.Space, hitPaddle) != nil { - b.Delta.SetX(-1.1 * b.Delta.X()) - b.Delta.SetY(b.Delta.Y() + (rand.Float64()-0.5)*8) +func newBallDelta() floatgeom.Point2 { + d := floatgeom.Point2{(rand.Float64() - 0.5) * 4, (rand.Float64() - 0.5) * 16} + if math.Abs(d.X()) < 0.5 { + d[0] *= 5 + } + return d +} + +func newBall(ctx *scene.Context, x, y float64) { + ball := entities.New(ctx, + entities.WithRect(floatgeom.NewRect2WH(x, y, 10, 10)), + entities.WithColor(color.RGBA{255, 255, 255, 255}), + entities.WithDrawLayers([]int{2}), + ) + ball.Delta = newBallDelta() + event.Bind(ctx, event.Enter, ball, func(ball *entities.Entity, _ event.EnterPayload) event.Response { + ball.ShiftDelta() + if collision.HitLabel(ball.Space, hitPaddle) != nil { + ball.Delta[0] *= -1.1 + ball.Delta[1] += (rand.Float64() - 0.5) * 8 } - if b.X() < 0 || b.X() > 640 { - if b.X() < 0 { + if ball.X() < 0 || ball.X() > 640 { + if ball.X() < 0 { score1++ } else { score2++ } - b.Delta.SetX(0) - b.Delta.SetY(0) - b.SetPos(320, 240) - } else if b.Y() < 0 || b.Y() > 480-b.H { - b.Delta.SetY(-1 * b.Delta.Y()) + ball.Delta = newBallDelta() + ball.SetPos(floatgeom.Point2{320, 240}) + } else if ball.Y() < 0 || ball.Y() > 480-ball.H() { + ball.Delta[1] = -1 * ball.Delta.Y() } return 0 }) } -func newPaddle(x, y float64, player int) { - p := entities.NewMoving(x, y, 20, 100, render.NewColorBoxR(20, 100, color.RGBA{255, 255, 255, 255}), nil, 0, 0) - p.Speed.SetY(8) - render.Draw(p.R, 1) - p.Space.UpdateLabel(hitPaddle) - if player == 1 { - p.Bind(event.Enter, enterPaddle(key.UpArrow, key.DownArrow)) +func newPaddle(ctx *scene.Context, x, y float64, player int) { + paddle := entities.New(ctx, + entities.WithRect(floatgeom.NewRect2WH(x, y, 20, 100)), + entities.WithColor(color.RGBA{255, 255, 255, 255}), + entities.WithDrawLayers([]int{1}), + entities.WithLabel(hitPaddle), + ) + if player == 2 { + event.Bind(ctx, event.Enter, paddle, enterPaddle(key.UpArrow, key.DownArrow)) } else { - p.Bind(event.Enter, enterPaddle(key.W, key.S)) + event.Bind(ctx, event.Enter, paddle, enterPaddle(key.W, key.S)) } } -func enterPaddle(up, down string) func(event.CID, interface{}) int { - return func(id event.CID, nothing interface{}) int { - p := id.E().(*entities.Moving) - p.Delta.SetY(0) +func enterPaddle(up, down key.Code) func(*entities.Entity, event.EnterPayload) event.Response { + return func(p *entities.Entity, _ event.EnterPayload) event.Response { if oak.IsDown(up) { - p.Delta.SetY(-p.Speed.Y()) - } else if oak.IsDown(down) { - p.Delta.SetY(p.Speed.Y()) - } - p.ShiftY(p.Delta.Y()) - if p.Y() < 0 || p.Y() > (480-p.H) { - p.ShiftY(-p.Delta.Y()) + if p.Y() > 0 { + p.ShiftY(-8) + } + } else if oak.IsDown(down) && p.Y() < (480-p.H()) { + p.ShiftY(8) } return 0 } diff --git a/examples/radar-demo/README.md b/examples/radar-demo/README.md deleted file mode 100644 index f8fba78b..00000000 --- a/examples/radar-demo/README.md +++ /dev/null @@ -1,6 +0,0 @@ -# Radar Demo -Keep track of entities evern when they are off screen with a basic radar. - -Create a movable character, a randomly moving entities and draw them on the radar in the top right portion of the screen. - -![radar](./example.gif) \ No newline at end of file diff --git a/examples/radar-demo/example.gif b/examples/radar-demo/example.gif deleted file mode 100644 index 161b0950..00000000 Binary files a/examples/radar-demo/example.gif and /dev/null differ diff --git a/examples/radar-demo/main.go b/examples/radar-demo/main.go deleted file mode 100644 index 235fa48b..00000000 --- a/examples/radar-demo/main.go +++ /dev/null @@ -1,118 +0,0 @@ -package main - -import ( - "image/color" - "math" - "math/rand" - - "github.com/oakmound/oak/v3/alg/floatgeom" - "github.com/oakmound/oak/v3/alg/intgeom" - - oak "github.com/oakmound/oak/v3" - "github.com/oakmound/oak/v3/entities" - "github.com/oakmound/oak/v3/entities/x/move" - "github.com/oakmound/oak/v3/event" - "github.com/oakmound/oak/v3/examples/radar-demo/radar" - "github.com/oakmound/oak/v3/physics" - "github.com/oakmound/oak/v3/render" - "github.com/oakmound/oak/v3/scene" -) - -const ( - xLimit = 1000 - yLimit = 1000 -) - -// This example demonstrates making a basic radar or other custom renderable -// type. The radar here acts as a UI element, staying on screen, and follows -// around a player character. - -func main() { - oak.AddScene("demo", scene.Scene{Start: func(ctx *scene.Context) { - render.Draw(render.NewDrawFPS(0.03, nil, 10, 10)) - - char := entities.NewMoving(200, 200, 50, 50, render.NewColorBox(50, 50, color.RGBA{125, 125, 0, 255}), nil, 0, 1) - char.Speed = physics.NewVector(3, 3) - - oak.SetViewportBounds(intgeom.NewRect2(0, 0, xLimit, yLimit)) - moveRect := floatgeom.NewRect2(0, 0, xLimit, yLimit) - - char.Bind(event.Enter, func(event.CID, interface{}) int { - move.WASD(char) - move.Limit(char, moveRect) - move.CenterScreenOn(char) - return 0 - }) - render.Draw(char.R, 1, 2) - - // Create the Radar - center := radar.Point{X: char.Xp(), Y: char.Yp()} - points := make(map[radar.Point]color.Color) - w := 100 - h := 100 - r := radar.NewRadar(w, h, points, center, 10) - r.SetPos(float64(ctx.Window.Width()-w), 0) - - for i := 0; i < 5; i++ { - x, y := rand.Float64()*400, rand.Float64()*400 - enemy := newEnemyOnRadar(x, y) - enemy.CID.Bind(event.Enter, standardEnemyMove) - render.Draw(enemy.R, 1, 1) - r.AddPoint(radar.Point{X: enemy.Xp(), Y: enemy.Yp()}, color.RGBA{255, 255, 0, 255}) - } - - render.Draw(r, 2) - - for x := 0; x < xLimit; x += 64 { - for y := 0; y < yLimit; y += 64 { - r := uint8(rand.Intn(120)) - b := uint8(rand.Intn(120)) - cb := render.NewColorBox(64, 64, color.RGBA{r, 0, b, 255}) - cb.SetPos(float64(x), float64(y)) - render.Draw(cb, 0) - } - } - - }}) - - render.SetDrawStack( - render.NewCompositeR(), - render.NewDynamicHeap(), - render.NewStaticHeap(), - ) - oak.Init("demo") -} - -type enemyOnRadar struct { - *entities.Moving -} - -func (eor *enemyOnRadar) Init() event.CID { - return event.NextID(eor) -} -func newEnemyOnRadar(x, y float64) *enemyOnRadar { - eor := new(enemyOnRadar) - eor.Moving = entities.NewMoving(50, y, 50, 50, render.NewColorBox(25, 25, color.RGBA{0, 200, 0, 0}), nil, eor.Init(), 0) - eor.Speed = physics.NewVector(-1*(rand.Float64()*2+1), rand.Float64()*2-1) - eor.Delta = eor.Speed - return eor -} - -func standardEnemyMove(id event.CID, nothing interface{}) int { - eor := event.GetEntity(id).(*enemyOnRadar) - if eor.X() < 0 { - eor.Delta.SetPos(math.Abs(eor.Speed.X()), (eor.Speed.Y())) - } - if eor.X() > xLimit-eor.W { - eor.Delta.SetPos(-1*math.Abs(eor.Speed.X()), (eor.Speed.Y())) - } - if eor.Y() < 0 { - eor.Delta.SetPos(eor.Speed.X(), math.Abs(eor.Speed.Y())) - } - if eor.Y() > yLimit-eor.H { - eor.Delta.SetPos(eor.Speed.X(), -1*math.Abs(eor.Speed.Y())) - } - eor.ShiftX(eor.Delta.X()) - eor.ShiftY(eor.Delta.Y()) - return 0 -} diff --git a/examples/radar-demo/radar/radar.go b/examples/radar-demo/radar/radar.go deleted file mode 100644 index f755369c..00000000 --- a/examples/radar-demo/radar/radar.go +++ /dev/null @@ -1,81 +0,0 @@ -package radar - -import ( - "image" - "image/color" - "image/draw" - - "github.com/oakmound/oak/v3/render" -) - -// Point is a utility function for location -type Point struct { - X, Y *float64 -} - -// Radar helps store and present information around interesting entities on a radar map -type Radar struct { - render.LayeredPoint - points map[Point]color.Color - center Point - width, height int - r *image.RGBA - outline *render.Sprite - ratio float64 -} - -var ( - centerColor = color.RGBA{255, 255, 0, 255} -) - -// NewRadar creates a radar that will display at 0,0 with the given dimensions. -// The points given will be displayed on the radar relative to the center point, -// With the absolute distance reduced by the given ratio -func NewRadar(w, h int, points map[Point]color.Color, center Point, ratio float64) *Radar { - r := new(Radar) - r.LayeredPoint = render.NewLayeredPoint(0, 0, 0) - r.points = points - r.width = w - r.height = h - r.center = center - r.r = image.NewRGBA(image.Rect(0, 0, w, h)) - r.outline = render.NewColorBox(w, h, color.RGBA{0, 0, 125, 125}) - r.ratio = ratio - return r -} - -// SetPos sets the position of the radar on the screen -func (r *Radar) SetPos(x, y float64) { - r.LayeredPoint.SetPos(x, y) - r.outline.SetPos(x, y) -} - -// GetRGBA returns this radar's image -func (r *Radar) GetRGBA() *image.RGBA { - return r.r -} - -// Draw draws the radar at a given offset -func (r *Radar) Draw(buff draw.Image, xOff, yOff float64) { - // Draw each point p in r.points - // at r.X() + center.X() - p.X(), r.Y() + center.Y() - p.Y() - // IF that value is < r.width/2, > -r.width/2, < r.height/2, > -r.height/2 - for p, c := range r.points { - x := int((*p.X-*r.center.X)/r.ratio) + r.width/2 - y := int((*p.Y-*r.center.Y)/r.ratio) + r.height/2 - for x2 := x - 1; x2 < x+1; x2++ { - for y2 := y - 1; y2 < y+1; y2++ { - r.r.Set(x2, y2, c) - } - } - } - r.r.Set(r.width/2, r.height/2, centerColor) - render.DrawImage(buff, r.r, int(xOff+r.X()), int(yOff+r.Y())) - r.outline.Draw(buff, xOff, yOff) - r.r = image.NewRGBA(image.Rect(0, 0, r.width, r.height)) -} - -// AddPoint adds an additional point to the radar to be tracked -func (r *Radar) AddPoint(loc Point, c color.Color) { - r.points[loc] = c -} diff --git a/examples/rooms/main.go b/examples/rooms/main.go index 181e3c56..9fea3120 100644 --- a/examples/rooms/main.go +++ b/examples/rooms/main.go @@ -4,33 +4,32 @@ import ( "image/color" "math/rand" - oak "github.com/oakmound/oak/v3" - "github.com/oakmound/oak/v3/alg/intgeom" - "github.com/oakmound/oak/v3/entities" - "github.com/oakmound/oak/v3/entities/x/move" - "github.com/oakmound/oak/v3/event" - "github.com/oakmound/oak/v3/physics" - "github.com/oakmound/oak/v3/render" - "github.com/oakmound/oak/v3/scene" + "github.com/oakmound/oak/v4" + "github.com/oakmound/oak/v4/alg/floatgeom" + "github.com/oakmound/oak/v4/alg/intgeom" + "github.com/oakmound/oak/v4/entities" + "github.com/oakmound/oak/v4/event" + "github.com/oakmound/oak/v4/render" + "github.com/oakmound/oak/v4/scene" ) // Rooms exercises shifting the camera in a zelda-esque fashion, // moving the camera to center on even-sized rooms arranged in a grid // once the player enters them. -func isOffScreen(ctx *scene.Context, char *entities.Moving) (intgeom.Dir2, bool) { +func isOffScreen(ctx *scene.Context, char *entities.Entity) (intgeom.Dir2, bool) { x := int(char.X()) y := int(char.Y()) - if x > ctx.Window.Viewport().X()+ctx.Window.Width() { + if x > ctx.Window.Viewport().X()+ctx.Window.Bounds().X() { return intgeom.Right, true } - if y > ctx.Window.Viewport().Y()+ctx.Window.Height() { + if y > ctx.Window.Viewport().Y()+ctx.Window.Bounds().Y() { return intgeom.Down, true } - if x+int(char.W) < ctx.Window.Viewport().X() { + if int(char.Right()) < ctx.Window.Viewport().X() { return intgeom.Left, true } - if y+int(char.H) < ctx.Window.Viewport().Y() { + if int(char.Bottom()) < ctx.Window.Viewport().Y() { return intgeom.Up, true } return intgeom.Dir2{}, false @@ -43,36 +42,37 @@ const ( func main() { oak.AddScene("rooms", scene.Scene{Start: func(ctx *scene.Context) { - char := entities.NewMoving(200, 200, 50, 50, render.NewColorBox(50, 50, color.RGBA{255, 255, 255, 255}), nil, 0, 1) - char.Speed = physics.NewVector(3, 3) - + char := entities.New(ctx, + entities.WithRect(floatgeom.NewRect2WH(200, 200, 50, 50)), + entities.WithColor(color.RGBA{255, 255, 255, 255}), + entities.WithSpeed(floatgeom.Point2{3, 3}), + entities.WithDrawLayers([]int{1, 2}), + ) var transitioning bool var totalTransitionDelta intgeom.Point2 var transitionDelta intgeom.Point2 - char.Bind(event.Enter, func(event.CID, interface{}) int { + event.Bind(ctx, event.Enter, char, func(c *entities.Entity, ev event.EnterPayload) event.Response { dir, ok := isOffScreen(ctx, char) if !transitioning && ok { transitioning = true - totalTransitionDelta = intgeom.Point2{ctx.Window.Width(), ctx.Window.Height()}.Mul(intgeom.Point2{dir.X(), dir.Y()}) + totalTransitionDelta = ctx.Window.Bounds().Mul(intgeom.Point2{dir.X(), dir.Y()}) transitionDelta = totalTransitionDelta.DivConst(transitionFrameCount) } if transitioning { // disable movement // move camera one size towards the player if totalTransitionDelta.X() != 0 || totalTransitionDelta.Y() != 0 { - oak.ShiftScreen(transitionDelta.X(), transitionDelta.Y()) + oak.ShiftViewport(transitionDelta) totalTransitionDelta = totalTransitionDelta.Sub(transitionDelta) } else { transitioning = false } } else { - move.WASD(char) + entities.WASD(char) } return 0 }) - render.Draw(char.R, 1, 2) - for x := 0; x < 2000; x += 12 { for y := 0; y < 2000; y += 12 { r := uint8(rand.Intn(120)) @@ -82,7 +82,6 @@ func main() { render.Draw(cb, 0) } } - }}) oak.Init("rooms") diff --git a/examples/scenes/main.go b/examples/scenes/main.go new file mode 100644 index 00000000..2cbca333 --- /dev/null +++ b/examples/scenes/main.go @@ -0,0 +1,137 @@ +package main + +import ( + "image/color" + + oak "github.com/oakmound/oak/v4" + "github.com/oakmound/oak/v4/alg/floatgeom" + "github.com/oakmound/oak/v4/entities" + "github.com/oakmound/oak/v4/event" + "github.com/oakmound/oak/v4/key" + "github.com/oakmound/oak/v4/render" + "github.com/oakmound/oak/v4/scene" +) + +// Axes are the plural of axis +type Axes uint8 + +// This is an enum for what axes to center around +const ( + X Axes = iota + Y + Both +) + +func center(ctx *scene.Context, obj render.Renderable, ax Axes) { + objWidth, objHeight := obj.GetDims() + wbds := ctx.Window.Bounds() + switch ax { + case Both: + obj.SetPos(float64(wbds.X()/2-objWidth/2), + float64(wbds.Y()-objHeight)/2) //distributive property + case X: + obj.SetPos(float64(wbds.X()-objWidth)/2, obj.Y()) + case Y: + obj.SetPos(obj.X(), float64(wbds.Y()-objHeight)/2) + } +} + +func main() { + win := oak.NewWindow() + win.ErrorScene = "error" + win.AddScene("error", scene.Scene{Start: func(ctx *scene.Context) { + ctx.DrawStack.Draw(render.NewText("Bad input! Any key to return to title", 100, 100)) + event.GlobalBind(ctx, key.AnyDown, func(key.Event) event.Response { + ctx.Window.GoToScene("titlescreen") + return 0 + }) + }}) + + //make the scene for the titlescreen + win.AddScene("titlescreen", scene.Scene{Start: func(ctx *scene.Context) { + ctx.Window.(*oak.Window).ErrorScene = "error" + + //create text saying titlescreen in placeholder position + titleText := render.NewText("titlescreen", 0, 0) + + //center text along both axes + center(ctx, titleText, Both) + + //tell the draw loop to draw titleText + render.Draw(titleText) + + wbds := ctx.Window.Bounds() + + //do the same for the text with button instructions, but this time Y position is not a placeholder (X still is) + instructionText := render.NewText("press Enter to start, or press Q to quit", 0, float64(wbds.Y()*3/4)) + //this time we only center the X axis, otherwise it would overlap titleText + center(ctx, instructionText, X) + render.Draw(instructionText) + event.GlobalBind(ctx, key.Down(key.ReturnEnter), func(key.Event) event.Response { + // Go to the next scene if enter is pressed. Next scene is the game + ctx.Window.NextScene() + return 0 + }) + event.GlobalBind(ctx, key.Down(key.Q), func(key.Event) event.Response { + // exit the game if q is pressed + ctx.Window.Quit() + return 0 + }) + event.GlobalBind(ctx, key.AnyDown, func(k key.Event) event.Response { + if k.Code == key.Q || k.Code == key.ReturnEnter { + return 0 + } + ctx.Window.GoToScene("whatthe!") + return 0 + }) + + }, End: func() (string, *scene.Result) { + return "game", nil //set the next scene to "game" + }}) + + //define the "game" (it's just a square that can be moved with WASD) + win.AddScene("game", scene.Scene{Start: func(ctx *scene.Context) { + //create the player, a blue 32x32 square at 100,100 + player := entities.New(ctx, + entities.WithRect(floatgeom.NewRect2WH(100, 100, 32, 32)), + entities.WithColor(color.RGBA{0, 0, 255, 255}), + ) + + controlsText := render.NewText("WASD to move, ESC to return to titlescreen", 5, 20) + //we draw the text on layer 1 (instead of the default layer 0) + //because we want it to show up above the player + render.Draw(controlsText, 1) + event.GlobalBind(ctx, key.Down(key.Escape), func(key.Event) event.Response { + // Go to the next scene if escape is pressed. Next scene is titlescreen + ctx.Window.NextScene() + return 0 + }) + event.Bind(ctx, event.Enter, player, func(player *entities.Entity, _ event.EnterPayload) event.Response { + if ctx.IsDown(key.S) { + //if S is pressed, set the player's vertical speed to 2 (positive == down) + player.Delta[1] = 2 + } else if ctx.IsDown(key.W) { + player.Delta[1] = -2 + } else { + //if the now buttons are pressed for vertical movement, don't move vertically + player.Delta[1] = 0 + } + + //do the same thing as before, but horizontally + if ctx.IsDown(key.D) { + player.Delta[0] = 2 + } else if ctx.IsDown(key.A) { + player.Delta[0] = -2 + } else { + player.Delta[0] = 0 + } + //apply the player's speed to their position + player.ShiftDelta() + return 0 + }) + }, End: func() (string, *scene.Result) { + return "titlescreen", nil //set the next scene to be titlescreen + }}) + //start the game on the titlescreen + win.Init("titlescreen") +} diff --git a/examples/screenopts/main.go b/examples/screenopts/main.go index d1b0b9bd..f2a7850f 100644 --- a/examples/screenopts/main.go +++ b/examples/screenopts/main.go @@ -2,29 +2,63 @@ package main import ( "fmt" + "image" + "image/color" + "math/rand" + "strconv" - oak "github.com/oakmound/oak/v3" - "github.com/oakmound/oak/v3/event" - "github.com/oakmound/oak/v3/key" - "github.com/oakmound/oak/v3/render" - "github.com/oakmound/oak/v3/scene" -) - -const ( - borderlessAtStart = false - fullscreenAtStart = false + oak "github.com/oakmound/oak/v4" + "github.com/oakmound/oak/v4/event" + "github.com/oakmound/oak/v4/key" + "github.com/oakmound/oak/v4/mouse" + "github.com/oakmound/oak/v4/render" + "github.com/oakmound/oak/v4/scene" ) func main() { - oak.AddScene("demo", scene.Scene{Start: func(*scene.Context) { - txt := render.NewText("Press F to toggle fullscreen. Press B to toggle borderless.", 50, 50) + const ( + borderlessAtStart = false + fullscreenAtStart = false + topMostAtStart = false + ) + + oak.AddScene("demo", scene.Scene{Start: func(ctx *scene.Context) { + txt := render.NewText("Press F to toggle fullscreen. Press B to toggle borderless. Press T to toggle topmost / floating.", 50, 50) render.Draw(txt) + line2 := render.NewText("Press Q to change window title. Press C to change the window icon. Press H to replace the cursor.", 50, 70) + render.Draw(line2) borderless := borderlessAtStart fullscreen := fullscreenAtStart + topMost := topMostAtStart + + event.GlobalBind(ctx, key.Down(key.C), func(k key.Event) event.Response { + colors := []color.RGBA{ + {255, 255, 0, 255}, + {255, 0, 255, 255}, + {0, 255, 255, 255}, + {255, 0, 0, 255}, + {0, 255, 0, 255}, + {0, 0, 255, 255}, + } + c := colors[rand.Intn(len(colors))] + rgba := image.NewRGBA(image.Rect(0, 0, 32, 32)) + for x := 0; x < 32; x++ { + for y := 0; y < 32; y++ { + rgba.SetRGBA(x, y, c) + } + } + + err := ctx.Window.SetIcon(rgba) + if err != nil { + fmt.Println(err) + } + return 0 + }) - event.GlobalBind(key.Down+key.F, func(event.CID, interface{}) int { + event.GlobalBind(ctx, key.Down(key.F), func(k key.Event) event.Response { fullscreen = !fullscreen + fmt.Println("Setting fullscreen:", fullscreen) err := oak.SetFullScreen(fullscreen) if err != nil { fullscreen = !fullscreen @@ -32,8 +66,9 @@ func main() { } return 0 }) - event.GlobalBind(key.Down+key.B, func(event.CID, interface{}) int { + event.GlobalBind(ctx, key.Down(key.B), func(k key.Event) event.Response { borderless = !borderless + fmt.Println("Setting borderless:", borderless) err := oak.SetBorderless(borderless) if err != nil { borderless = !borderless @@ -41,10 +76,53 @@ func main() { } return 0 }) + event.GlobalBind(ctx, key.Down(key.T), func(k key.Event) event.Response { + topMost = !topMost + fmt.Println("Setting top most:", topMost) + err := oak.SetTopMost(topMost) + if err != nil { + topMost = !topMost + fmt.Println(err) + } + return 0 + }) + titleCt := 0 + event.GlobalBind(ctx, key.Down(key.Q), func(k key.Event) event.Response { + titleCt++ + oak.SetTitle("window title " + strconv.Itoa(titleCt)) + return 0 + }) + event.GlobalBind(ctx, key.Down(key.H), func(k key.Event) event.Response { + oak.HideCursor() + box := render.NewSequence(15, + render.NewColorBox(2, 2, color.RGBA{255, 255, 0, 255}), + render.NewColorBox(3, 3, color.RGBA{255, 235, 0, 255}), + render.NewColorBox(4, 4, color.RGBA{255, 215, 0, 255}), + render.NewColorBox(5, 5, color.RGBA{255, 195, 0, 255}), + render.NewColorBox(6, 6, color.RGBA{255, 175, 0, 255}), + render.NewColorBox(5, 5, color.RGBA{255, 155, 0, 255}), + render.NewColorBox(4, 4, color.RGBA{255, 135, 0, 255}), + render.NewColorBox(3, 3, color.RGBA{255, 115, 0, 255}), + render.NewColorBox(2, 2, color.RGBA{255, 95, 0, 255}), + render.NewColorBox(1, 1, color.RGBA{255, 75, 0, 255}), + render.EmptyRenderable(), + render.EmptyRenderable(), + render.EmptyRenderable(), + render.EmptyRenderable(), + ) + ctx.DrawStack.Draw(box) + event.GlobalBind(ctx, + mouse.Drag, func(mouseEvent *mouse.Event) event.Response { + box.SetPos(mouseEvent.X(), mouseEvent.Y()) + return 0 + }) + return event.ResponseUnbindThisBinding + }) }}) oak.Init("demo", func(c oak.Config) (oak.Config, error) { + c.TopMost = topMostAtStart // Both cannot be true at once! c.Borderless = borderlessAtStart c.Fullscreen = fullscreenAtStart diff --git a/examples/slide/README.md b/examples/slide/README.md deleted file mode 100644 index a939adde..00000000 --- a/examples/slide/README.md +++ /dev/null @@ -1,4 +0,0 @@ -# Example SlideShow -See how to make a slideshow using Oak! - -![example slideshow](./example.gif) \ No newline at end of file diff --git a/examples/slide/assets/font/expressway rg.ttf b/examples/slide/assets/font/expressway rg.ttf deleted file mode 100644 index 39e15794..00000000 Binary files a/examples/slide/assets/font/expressway rg.ttf and /dev/null differ diff --git a/examples/slide/assets/font/gnuolane rg.ttf b/examples/slide/assets/font/gnuolane rg.ttf deleted file mode 100644 index eb06ae90..00000000 Binary files a/examples/slide/assets/font/gnuolane rg.ttf and /dev/null differ diff --git a/examples/slide/assets/font/libel-suit-rg.ttf b/examples/slide/assets/font/libel-suit-rg.ttf deleted file mode 100644 index caf48fba..00000000 Binary files a/examples/slide/assets/font/libel-suit-rg.ttf and /dev/null differ diff --git a/examples/slide/assets/images/raw/AndPt.PNG b/examples/slide/assets/images/raw/AndPt.PNG deleted file mode 100644 index 50376e46..00000000 Binary files a/examples/slide/assets/images/raw/AndPt.PNG and /dev/null differ diff --git a/examples/slide/assets/images/raw/agent.PNG b/examples/slide/assets/images/raw/agent.PNG deleted file mode 100644 index a80932ae..00000000 Binary files a/examples/slide/assets/images/raw/agent.PNG and /dev/null differ diff --git a/examples/slide/assets/images/raw/agentAI.PNG b/examples/slide/assets/images/raw/agentAI.PNG deleted file mode 100644 index e49d434e..00000000 Binary files a/examples/slide/assets/images/raw/agentAI.PNG and /dev/null differ diff --git a/examples/slide/assets/images/raw/agentCharacter.PNG b/examples/slide/assets/images/raw/agentCharacter.PNG deleted file mode 100644 index 4cf227e6..00000000 Binary files a/examples/slide/assets/images/raw/agentCharacter.PNG and /dev/null differ diff --git a/examples/slide/assets/images/raw/agentDoodad.PNG b/examples/slide/assets/images/raw/agentDoodad.PNG deleted file mode 100644 index 03aa98c7..00000000 Binary files a/examples/slide/assets/images/raw/agentDoodad.PNG and /dev/null differ diff --git a/examples/slide/assets/images/raw/agentEnemy.PNG b/examples/slide/assets/images/raw/agentEnemy.PNG deleted file mode 100644 index 7f54bcf6..00000000 Binary files a/examples/slide/assets/images/raw/agentEnemy.PNG and /dev/null differ diff --git a/examples/slide/assets/images/raw/agentLevelGenerate.PNG b/examples/slide/assets/images/raw/agentLevelGenerate.PNG deleted file mode 100644 index a71e8a33..00000000 Binary files a/examples/slide/assets/images/raw/agentLevelGenerate.PNG and /dev/null differ diff --git a/examples/slide/assets/images/raw/agentLevelSelect.PNG b/examples/slide/assets/images/raw/agentLevelSelect.PNG deleted file mode 100644 index 48cb996d..00000000 Binary files a/examples/slide/assets/images/raw/agentLevelSelect.PNG and /dev/null differ diff --git a/examples/slide/assets/images/raw/agentRoom.PNG b/examples/slide/assets/images/raw/agentRoom.PNG deleted file mode 100644 index 2ff370ca..00000000 Binary files a/examples/slide/assets/images/raw/agentRoom.PNG and /dev/null differ diff --git a/examples/slide/assets/images/raw/agentTutorial.png b/examples/slide/assets/images/raw/agentTutorial.png deleted file mode 100644 index b2db2165..00000000 Binary files a/examples/slide/assets/images/raw/agentTutorial.png and /dev/null differ diff --git a/examples/slide/assets/images/raw/attachable.PNG b/examples/slide/assets/images/raw/attachable.PNG deleted file mode 100644 index 42fdd300..00000000 Binary files a/examples/slide/assets/images/raw/attachable.PNG and /dev/null differ diff --git a/examples/slide/assets/images/raw/audio.PNG b/examples/slide/assets/images/raw/audio.PNG deleted file mode 100644 index b4499104..00000000 Binary files a/examples/slide/assets/images/raw/audio.PNG and /dev/null differ diff --git a/examples/slide/assets/images/raw/botanist.PNG b/examples/slide/assets/images/raw/botanist.PNG deleted file mode 100644 index 793608f9..00000000 Binary files a/examples/slide/assets/images/raw/botanist.PNG and /dev/null differ diff --git a/examples/slide/assets/images/raw/chooseX.PNG b/examples/slide/assets/images/raw/chooseX.PNG deleted file mode 100644 index 35f99815..00000000 Binary files a/examples/slide/assets/images/raw/chooseX.PNG and /dev/null differ diff --git a/examples/slide/assets/images/raw/colorGen.PNG b/examples/slide/assets/images/raw/colorGen.PNG deleted file mode 100644 index 37500bf0..00000000 Binary files a/examples/slide/assets/images/raw/colorGen.PNG and /dev/null differ diff --git a/examples/slide/assets/images/raw/craftyParticle.PNG b/examples/slide/assets/images/raw/craftyParticle.PNG deleted file mode 100644 index aa8ecbe1..00000000 Binary files a/examples/slide/assets/images/raw/craftyParticle.PNG and /dev/null differ diff --git a/examples/slide/assets/images/raw/dataFilter.PNG b/examples/slide/assets/images/raw/dataFilter.PNG deleted file mode 100644 index 37542b5b..00000000 Binary files a/examples/slide/assets/images/raw/dataFilter.PNG and /dev/null differ diff --git a/examples/slide/assets/images/raw/degToRad.PNG b/examples/slide/assets/images/raw/degToRad.PNG deleted file mode 100644 index 022bf355..00000000 Binary files a/examples/slide/assets/images/raw/degToRad.PNG and /dev/null differ diff --git a/examples/slide/assets/images/raw/doctorBounce.PNG b/examples/slide/assets/images/raw/doctorBounce.PNG deleted file mode 100644 index a35ab8bf..00000000 Binary files a/examples/slide/assets/images/raw/doctorBounce.PNG and /dev/null differ diff --git a/examples/slide/assets/images/raw/doctorEntity.PNG b/examples/slide/assets/images/raw/doctorEntity.PNG deleted file mode 100644 index 15911cd4..00000000 Binary files a/examples/slide/assets/images/raw/doctorEntity.PNG and /dev/null differ diff --git a/examples/slide/assets/images/raw/doctorFile.png b/examples/slide/assets/images/raw/doctorFile.png deleted file mode 100644 index 054785b0..00000000 Binary files a/examples/slide/assets/images/raw/doctorFile.png and /dev/null differ diff --git a/examples/slide/assets/images/raw/doctorHasE.PNG b/examples/slide/assets/images/raw/doctorHasE.PNG deleted file mode 100644 index b5057933..00000000 Binary files a/examples/slide/assets/images/raw/doctorHasE.PNG and /dev/null differ diff --git a/examples/slide/assets/images/raw/doctorLevel.PNG b/examples/slide/assets/images/raw/doctorLevel.PNG deleted file mode 100644 index fc92a2dd..00000000 Binary files a/examples/slide/assets/images/raw/doctorLevel.PNG and /dev/null differ diff --git a/examples/slide/assets/images/raw/doctorLevelPlace.PNG b/examples/slide/assets/images/raw/doctorLevelPlace.PNG deleted file mode 100644 index dfc2f242..00000000 Binary files a/examples/slide/assets/images/raw/doctorLevelPlace.PNG and /dev/null differ diff --git a/examples/slide/assets/images/raw/dyscrasia.PNG b/examples/slide/assets/images/raw/dyscrasia.PNG deleted file mode 100644 index 1eda335b..00000000 Binary files a/examples/slide/assets/images/raw/dyscrasia.PNG and /dev/null differ diff --git a/examples/slide/assets/images/raw/entity.PNG b/examples/slide/assets/images/raw/entity.PNG deleted file mode 100644 index c87cd42d..00000000 Binary files a/examples/slide/assets/images/raw/entity.PNG and /dev/null differ diff --git a/examples/slide/assets/images/raw/esque.PNG b/examples/slide/assets/images/raw/esque.PNG deleted file mode 100644 index 0b243bd7..00000000 Binary files a/examples/slide/assets/images/raw/esque.PNG and /dev/null differ diff --git a/examples/slide/assets/images/raw/fantastic.PNG b/examples/slide/assets/images/raw/fantastic.PNG deleted file mode 100644 index 07e1ee89..00000000 Binary files a/examples/slide/assets/images/raw/fantastic.PNG and /dev/null differ diff --git a/examples/slide/assets/images/raw/filter.PNG b/examples/slide/assets/images/raw/filter.PNG deleted file mode 100644 index fdf11efe..00000000 Binary files a/examples/slide/assets/images/raw/filter.PNG and /dev/null differ diff --git a/examples/slide/assets/images/raw/flower.PNG b/examples/slide/assets/images/raw/flower.PNG deleted file mode 100644 index ee769296..00000000 Binary files a/examples/slide/assets/images/raw/flower.PNG and /dev/null differ diff --git a/examples/slide/assets/images/raw/jeremy.PNG b/examples/slide/assets/images/raw/jeremy.PNG deleted file mode 100644 index 85385440..00000000 Binary files a/examples/slide/assets/images/raw/jeremy.PNG and /dev/null differ diff --git a/examples/slide/assets/images/raw/jeremyFile.PNG b/examples/slide/assets/images/raw/jeremyFile.PNG deleted file mode 100644 index 9c601b3f..00000000 Binary files a/examples/slide/assets/images/raw/jeremyFile.PNG and /dev/null differ diff --git a/examples/slide/assets/images/raw/jeremyLevel.PNG b/examples/slide/assets/images/raw/jeremyLevel.PNG deleted file mode 100644 index 8e62c111..00000000 Binary files a/examples/slide/assets/images/raw/jeremyLevel.PNG and /dev/null differ diff --git a/examples/slide/assets/images/raw/jeremyTilePlace.PNG b/examples/slide/assets/images/raw/jeremyTilePlace.PNG deleted file mode 100644 index 20dbacb0..00000000 Binary files a/examples/slide/assets/images/raw/jeremyTilePlace.PNG and /dev/null differ diff --git a/examples/slide/assets/images/raw/jeremyTileTypes.PNG b/examples/slide/assets/images/raw/jeremyTileTypes.PNG deleted file mode 100644 index e5c6ccf1..00000000 Binary files a/examples/slide/assets/images/raw/jeremyTileTypes.PNG and /dev/null differ diff --git a/examples/slide/assets/images/raw/oakParticle.PNG b/examples/slide/assets/images/raw/oakParticle.PNG deleted file mode 100644 index 66431aaa..00000000 Binary files a/examples/slide/assets/images/raw/oakParticle.PNG and /dev/null differ diff --git a/examples/slide/assets/images/raw/particleOpts.PNG b/examples/slide/assets/images/raw/particleOpts.PNG deleted file mode 100644 index f2966499..00000000 Binary files a/examples/slide/assets/images/raw/particleOpts.PNG and /dev/null differ diff --git a/examples/slide/assets/images/raw/phase.PNG b/examples/slide/assets/images/raw/phase.PNG deleted file mode 100644 index f50dd89b..00000000 Binary files a/examples/slide/assets/images/raw/phase.PNG and /dev/null differ diff --git a/examples/slide/assets/images/raw/phaseCollision.PNG b/examples/slide/assets/images/raw/phaseCollision.PNG deleted file mode 100644 index 54f4036c..00000000 Binary files a/examples/slide/assets/images/raw/phaseCollision.PNG and /dev/null differ diff --git a/examples/slide/assets/images/raw/phaserParticle.PNG b/examples/slide/assets/images/raw/phaserParticle.PNG deleted file mode 100644 index 899af8a4..00000000 Binary files a/examples/slide/assets/images/raw/phaserParticle.PNG and /dev/null differ diff --git a/examples/slide/assets/images/raw/push.PNG b/examples/slide/assets/images/raw/push.PNG deleted file mode 100644 index 0c148a11..00000000 Binary files a/examples/slide/assets/images/raw/push.PNG and /dev/null differ diff --git a/examples/slide/assets/images/raw/slide.PNG b/examples/slide/assets/images/raw/slide.PNG deleted file mode 100644 index f92ac459..00000000 Binary files a/examples/slide/assets/images/raw/slide.PNG and /dev/null differ diff --git a/examples/slide/assets/images/raw/updateCode1.PNG b/examples/slide/assets/images/raw/updateCode1.PNG deleted file mode 100644 index 64b3b92e..00000000 Binary files a/examples/slide/assets/images/raw/updateCode1.PNG and /dev/null differ diff --git a/examples/slide/assets/images/raw/updateCode2.PNG b/examples/slide/assets/images/raw/updateCode2.PNG deleted file mode 100644 index 91c49b5c..00000000 Binary files a/examples/slide/assets/images/raw/updateCode2.PNG and /dev/null differ diff --git a/examples/slide/assets/images/raw/updateCode3.PNG b/examples/slide/assets/images/raw/updateCode3.PNG deleted file mode 100644 index 076400a7..00000000 Binary files a/examples/slide/assets/images/raw/updateCode3.PNG and /dev/null differ diff --git a/examples/slide/assets/images/raw/wolf.PNG b/examples/slide/assets/images/raw/wolf.PNG deleted file mode 100644 index 900ada31..00000000 Binary files a/examples/slide/assets/images/raw/wolf.PNG and /dev/null differ diff --git a/examples/slide/example.gif b/examples/slide/example.gif deleted file mode 100644 index 63e66d1a..00000000 Binary files a/examples/slide/example.gif and /dev/null differ diff --git a/examples/slide/main.go b/examples/slide/main.go deleted file mode 100644 index a41ce638..00000000 --- a/examples/slide/main.go +++ /dev/null @@ -1,670 +0,0 @@ -package main - -import ( - "embed" - "fmt" - "image/color" - "log" - - "github.com/oakmound/oak/v3/alg/range/floatrange" - "github.com/oakmound/oak/v3/alg/range/intrange" - "github.com/oakmound/oak/v3/render/mod" - "github.com/oakmound/oak/v3/render/particle" - - oak "github.com/oakmound/oak/v3" - "github.com/oakmound/oak/v3/examples/slide/show" - "github.com/oakmound/oak/v3/examples/slide/show/static" - "github.com/oakmound/oak/v3/render" - "github.com/oakmound/oak/v3/shape" - "golang.org/x/image/colornames" -) - -const ( - width = 1920 - height = 1080 -) - -func initFonts() (err error) { - err = show.InitFonts() - if err != nil { - return - } - Express28, err = show.Express.RegenerateWith(show.FontSize(28)) - if err != nil { - return - } - Gnuolane28, err = show.Gnuolane.RegenerateWith(show.FontSize(28)) - if err != nil { - return - } - Libel28, err = show.Libel.RegenerateWith(show.FontSize(28)) - if err != nil { - return - } - RLibel28, err = Libel28.RegenerateWith(show.FontColor(colornames.Blue)) - if err != nil { - return - } - Express44, err = show.Express.RegenerateWith(show.FontSize(44)) - if err != nil { - return - } - Gnuolane44, err = show.Gnuolane.RegenerateWith(show.FontSize(44)) - if err != nil { - return - } - Libel44, err = show.Libel.RegenerateWith(show.FontSize(44)) - if err != nil { - return - } - - Express72, err = show.Express.RegenerateWith(show.FontSize(72)) - if err != nil { - return - } - Gnuolane72, err = show.Gnuolane.RegenerateWith(show.FontSize(72)) - if err != nil { - return - } - Libel72, err = show.Libel.RegenerateWith(show.FontSize(72)) - if err != nil { - return - } - return nil -} - -var ( - Express28 *render.Font - Gnuolane28 *render.Font - Libel28 *render.Font - RLibel28 *render.Font - Express44 *render.Font - Gnuolane44 *render.Font - Libel44 *render.Font - Express72 *render.Font - Gnuolane72 *render.Font - Libel72 *render.Font -) - -//go:embed assets -var assets embed.FS - -func main() { - - show.SetDims(width, height) - - bz1, _ := shape.BezierCurve( - width/15, height/5, - width/15, height/15, - width/5, height/15) - - bz2, _ := shape.BezierCurve( - width-(width/15), height/5, - width-(width/15), height/15, - width-(width/5), height/15) - - bz3, _ := shape.BezierCurve( - width/15, height-(height/5), - width/15, height-(height/15), - width/5, height-(height/15)) - - bz4, _ := shape.BezierCurve( - width-(width/15), height-(height/5), - width-(width/15), height-(height/15), - width-(width/5), height-(height/15)) - - bkg := render.NewCompositeM( - render.NewColorBox(width, height, colornames.Seagreen), - render.BezierThickLine(bz1, colornames.White, 1), - render.BezierThickLine(bz2, colornames.White, 1), - render.BezierThickLine(bz3, colornames.White, 1), - render.BezierThickLine(bz4, colornames.White, 1), - ) - - oak.SetLoadingRenderable(bkg) - oak.SetFS(assets) - err := initFonts() - if err != nil { - log.Fatal(err) - } - show.SetTitleFont(Gnuolane72) - - setups := []slideSetup{ - intro, - why, - philo, - particles, - ai, - levels, - conclusion, - } - - total := 0 - - for _, setup := range setups { - total += setup.len - } - - fmt.Println("Total slides", total) - - sslides := static.NewSlideSet(total, - static.Background(bkg), - //static.Transition(scene.Fade(4, 12)), - ) - - nextStart := 0 - - for _, setup := range setups { - setup.add(nextStart, sslides) - nextStart += setup.len - } - - slides := make([]show.Slide, len(sslides)) - for i, s := range sslides { - slides[i] = s - } - show.AddNumberShortcuts(len(slides)) - show.Start(width, height, slides...) -} - -type slideSetup struct { - add func(int, []*static.Slide) - len int -} - -var ( - intro = slideSetup{ - addIntro, - 5, - } -) - -func addIntro(i int, sslides []*static.Slide) { - // Intro: three slides - // Title - sslides[i].Append( - show.Title("Applying Go to Game Programming"), - show.TxtAt(Gnuolane44, "Patrick Stephen", .5, .6), - ) - // Thanks everybody for coming to this talk. I'm going to be talking about - // design patterns, philosophies, and generally useful tricks for - // developing video games in Go. - - sslides[i+1].Append(show.Header("Who Am I")) - sslides[i+1].Append( - show.TxtSetAt(Gnuolane44, 0.5, 0.63, 0.0, 0.07, - "Graduate Student at University of Minnesota", - "Maintainer / Programmer of Oak", - "github.com/200sc github.com/oakmound/oak", - "patrick.d.stephen@gmail.com", - "oakmoundstudio@gmail.com", - )..., - ) - // My name is Patrick Stephen, I'm currently a Master's student at - // the University of Minnesota. I'm one of two primary maintainers - // of oak's source code, Oak being the game engine that we built - // to make our games with. - // If you have any questions that don't get answered in or after - // this talk, feel free to send those questions either to me - // personally or to our team's email, or if it applies, feel free - // to raise an issue on the repository. - - sslides[i+2].Append(show.Header("Games I Made")) - sslides[i+2].Append(show.TxtAt(Gnuolane28, "White = Me, Blue = Oakmound", .5, .24)) - sslides[i+2].Append( - show.ImageCaption("botanist.PNG", .67, .1, .5, Libel28, "Space Botanist"), - show.ImageCaption("agent.PNG", .1, .11, .85, RLibel28, "Agent Blue"), - show.ImageCaption("dyscrasia.PNG", .5, .65, .5, RLibel28, "Dyscrasia"), - show.ImageCaption("esque.PNG", .4, .37, .5, RLibel28, "Esque"), - show.ImageCaption("fantastic.PNG", .33, .65, .5, RLibel28, "A Fantastic Doctor"), - show.ImageCaption("flower.PNG", .7, .41, .75, Libel28, "Flower Son"), - show.ImageCaption("jeremy.PNG", .07, .5, .66, Libel28, "Jeremy The Clam"), - show.ImageCaption("wolf.PNG", .68, .71, .5, Libel28, "The Wolf Comes Out At 18:00"), - ) - // These are games that I've made in the past, most being made - // for game jams, built in somewhere between 2 days and 2 weeks. - // - // We'll mostly be focusing on these three games, which are those - // that we've been working on in Go-- Agent Blue, Jeremy the Clam, - // and A Fantastic Doctor. - - sslides[i+3].Append(show.Header("This Talk is Not About...")) - sslides[i+3].Append( - show.TxtSetFrom(Gnuolane44, .25, .35, 0, .07, - "- Optimizing Go", - "- 3D Graphics in Go", - "- Mobile Games in Go", - )..., - ) - - // And just to get this out of the way, as you will probably - // note from the games I just showed, we aren't going to be - // talking about 3D games here or really performance intensive - // games, or games for non-desktop platforms, just because, - // while we haven't ignored these things I don't have - // any revolutionary breakthroughs to share about them right now. - - sslides[i+4].Append(show.Header("Topics")) - sslides[i+4].Append( - show.TxtSetFrom(Gnuolane44, .25, .35, 0, .07, - "- Why Go", - "- Design Philosophy", - "- Particles", - "- AI with Interfaces", - "- Level Building with Interfaces", - "- Other Examples", - )..., - ) - - // What we will talk about, is why Go is particularly useful for - // developing games, the philosophy behind our engine and development - // strategy, and then some interesting use cases for applying - // design patterns that Go makes easy with particle generation, - // artificial intelligence, and level construction. -} - -var ( - why = slideSetup{ - addWhy, - 3, - } -) - -func addWhy(i int, sslides []*static.Slide) { - sslides[i].Append(show.Title("Why Go")) - sslides[i+1].Append(show.Header("Why Go")) - sslides[i+1].Append( - show.TxtSetFrom(Gnuolane44, .25, .35, 0, .07, - "- Execution Speed", - "- Concurrency", - "- Fast Development", - "- Scales Well", - "- Multiplatform Support", - )..., - ) - - // So Go is particularly nice for building games one the one hand - // for its speed-- If you're used to building games with javascript - // or pygame, you'll have way more cpu cycles than you know how to - // deal with, especially if you use concurrency well on machines with - // multiple CPUs, which is going to be most of your audience. - // - // More importantly, Go is just as fast to develop with as those slower - // languages but it scales so much better. A little effort into decoupling - // your components with interfaces, and your code becomes far easier to read - // and increment on. - - sslides[i+2].Append(show.Header("Why Not Go")) - sslides[i+2].Append( - show.TxtSetFrom(Gnuolane44, .25, .35, 0, .07, - "- Execution Speed", - "- Difficult to use Graphics Cards", - "- Difficult to vectorize instructions", - "- C is Unavoidable", - )..., - ) - - // But what I've said so far mostly applies to game jam style games-- - // how do you make a quick and dirty game in a few days without your - // code falling all over itself. If you're interested in doing something - // with heavy performance requirements, Go isn't the language to use. - // Go's speed isn't good enough for AAA games because it doesn't have easy - // access to things like OpenGL, Vulkan, or SIMD CPU instructions. - // What Go can do with these things is call out to C to do the work for it, - // but every C call in Go has overhead, and that overhead adds up if you're - // calling out to it thousands of times per second. - // - // There's other practical issues if you want to develop in Go even if you - // don't have high performance requirements-- depending on your platform - // you may need to install audio dependencies, usb dependencies, and so on, - // and for all of Go's benefits in cross compilation these dependencies - // completely break the hope of your game working the same on multiple - // platforms without you going in and testing it manually. -} - -var ( - philo = slideSetup{ - addPhilo, - 7, - } -) - -func addPhilo(i int, sslides []*static.Slide) { - // Philosophy, engine discussion - sslides[i].Append(show.Title("Design Philosophy")) - sslides[i+1].Append(show.Header("Design Philosophy")) - sslides[i+1].Append( - show.TxtSetFrom(Gnuolane44, .25, .35, 0, .07, - "- No non-Go dependencies", - "- Ease / Terseness of API", - "- If it's useful and generic, put it in the engine", - )..., - ) - - // That brings us to our design philosophy in Oak. - // First, if we have a non-Go dependency, we also have an issue to - // replace that non-Go dependency ASAP. Right now we have just one. - // - // The motivation for having few dependencies isn't just so we can - // feel confident that all of our platforms are supported, but also - // making the engine easy to use. Most libraries in Go can be used - // with 'go get', and we want the same thing here-- a developer - // should be able to go get oak and immediately start working with it. - // - // After that, we want our API to be easy to use and small. Part of our - // motivation to start building Oak was that other game engines at the - // time took 500 lines to draw a cube or 400 lines to write Pong. Maybe - // at their core, those problems do take that many lines, but a lot of that - // code we can write for you (and also for us, so we don't have to keep - // re-writing it). - // - // In line with this, we follow a rule where if we have to rewrite something - // more than once for a game or for a package of the engine, that probably - // means that should be its own package and feature the engine provides. - // This does go against the go proverbs-- we do not follow the idea that - // a little copying is better than a little dependency, so long as we - // treat that dependency as part of the larger, engine dependency. - - sslides[i+2].Append(show.Header("Update Loops and Functions")) - sslides[i+2].Append( - show.Image("updateCode1.PNG", .27, .4), - show.Image("updateCode3.PNG", .57, .4), - ) - // - // Some game engines model their exposed API as a loop-- - // stick all your logic inside update() - // - // In larger projects, this leads directly to an explicit splitting up of that - // loop into at least two parts-- update all entities, then - // draw all entities. - // - // The combining of these elements into one loop causes - // a major problem-- tying the rate at which entities update themselves - // to the rate at which entities are drawn. This leads to inflexible - // engines, and in large projects you'll have to do something to work around - // this, or if you hard lock your draw rate modders will post funny videos - // of your physics breaking when they try to fix your frame rate. - // - // Oak handles this loop for you, and splits it into two loops, one for - // drawing elements and one for logical frame updating. - // - sslides[i+3].Append(show.Header("Update Loops and Functions")) - sslides[i+3].Append( - show.Image("updateCode2.PNG", .27, .4), - show.Image("updateCode3.PNG", .57, .4), - ) - // - // Another pattern used, in parallel with the Update Loop, - // is the Update Function. Give every entity in your game the - // Upate() function, and then your game logic is handled by calling Update() - // on everything. At a glance, this works very well in Go because your entities - // all fit into this single-function interface, but in games with a lot of - // entities you'll end up with a lot of entities that don't need to do - // anything on each frame. - // - // The engine needs to provide a way to handle game objects that don't - // need to be updated as well as those that do, and separating these into - // two groups explicitly makes the engine less extensible. Oak uses an - // event handler for this instead, where each entity that wants to use - // an update function binds that function to their entity id once. - // - sslides[i+4].Append(show.Header("Useful Packages")) - sslides[i+4].Append( - show.TxtSetFrom(Gnuolane44, .25, .35, 0, .07, - "- oak/alg/intgeom, oak/alg/floatgeom", - "- oak/alg", - "- oak/physics", - "- oak/render/particle", - )..., - ) - // - // These are some of the less obvious useful packages we've taken - // from games or sub-packages and built into their own package-- - // - // intgeom and floatgeom should be self explanatory-- we and every - // other Go package continually redefine X,Y and X,Y,Z points of - // integers and floats, and we needed to stop redoing that work. - // - sslides[i+5].Append(show.Header("oak/alg")) - sslides[i+5].Append(show.ImageAt("degToRad.PNG", .3, .5, mod.Scale(1.25, 1.25))) - sslides[i+5].Append(show.ImageAt("chooseX.PNG", .6, .5, mod.Scale(1.25, 1.25))) - // - // in alg, we store things like rounding and selection algorithms. - // We found that we really needed to pick a random element from - // a list of weighted floats a lot, so we split it off here. - // - sslides[i+6].Append(show.Header("oak/physics")) - sslides[i+6].Append(show.ImageAt("push.PNG", .3, .5, mod.Scale(1.25, 1.25))) - sslides[i+6].Append(show.ImageAt("attachable.PNG", .7, .5, mod.Scale(1.25, 1.25))) - // - // Physics was built to store some physics primitives for handling - // propagation of forces, mass, friction, but was mostly built so - // we could attach entities to each other and stop having to move - // every sub-component in an entity when we moved the entity. - // - // And lastly, particle, where we figured being able to generate - // a lot of small images or colors in patterns was something that could easily - // spice up most games. -} - -var ( - particles = slideSetup{ - addParticles, - 5, - } -) - -func addParticles(i int, sslides []*static.Slide) { - sslides[i].Append(show.Title("Particles")) - sslides[i].OnClick = func() { - go particle.NewColorGenerator( - particle.Size(intrange.NewConstant(4)), - particle.EndSize(intrange.NewConstant(7)), - particle.Angle(floatrange.NewLinear(0, 359)), - particle.Pos(width/2, height/2), - particle.Speed(floatrange.NewSpread(5, 2)), - particle.NewPerFrame(floatrange.NewSpread(5, 5)), - particle.Color( - color.RGBA{0, 0, 0, 255}, color.RGBA{0, 0, 0, 0}, - color.RGBA{255, 255, 255, 255}, color.RGBA{0, 0, 0, 0}, - ), - ).Generate(0) - } - // - // Speaking of particles, that's our first example - // of applying some techniques Go provides for making this API something I - // would consider to be really special. - // - // A particle generator is something like what's showing on screen now-- - // a source of a bunch of colors or effects or images, and they're complex - // to implement only because of the vast number of options you can take in - // to a particle emitter. - sslides[i+1].Append(show.Header("Particles in Other Engines")) - sslides[i+1].Append( - show.ImageCaption("craftyParticle.PNG", .17, .3, 1.25, Libel28, "CraftyJS"), - show.ImageCaption("phaserParticle.PNG", .40, .3, 1.25, Libel28, "PhaserJS"), - ) - // - // For context, we'll look at how some other engines do their - // particle APIs. Before starting Oak we worked with CraftyJS, - // which has the nice feature that these giant blocks of settings - // can be stored and reused for new particles, but then you get - // giant settings blocks. - // - // Phaser uses the reverse approach-- you can't keep particle settings - // around but you don't need to set a bunch of settings you don't need. - // - // These examples aren't making the same particle emitter, by the way, - // they're just the first examples I found from the respective engine's - // documentation. - - sslides[i+2].Append(show.Header("Particle Generators in Oak")) - sslides[i+2].Append(show.Image("AndPt.PNG", .13, .59).Modify(mod.Scale(1.25, 1.25))) - sslides[i+2].Append(show.Image("colorGen.PNG", .13, .29).Modify(mod.Scale(1.25, 1.25))) - sslides[i+2].Append(show.Image("particleOpts.PNG", .53, .29).Modify(mod.Scale(1.25, 1.25))) - // - // We wanted to apply what crafty did with saving settings, but we wanted - // settings to not all be mandatory, so our functional pattern starts by - // setting a bunch of defaults, then applying all the options that are passed in. - // Because the Option type is an exported type, users can define their own settings, - // and one of the settings I like to define is the And helper, shown here. - // - sslides[i+3].Append(show.Header("Particle Generators in Oak")) - sslides[i+3].Append(show.Image("oakParticle.PNG", .27, .4).Modify(mod.Scale(1.25, 1.25))) - // - // This is what this code looks like in practice-- often - // particle effects will get thrown off to their own package in our games - // so we can use a shorter import for particle. - // - sslides[i+4].Append(show.Header("Aside: Filtering Audio with Functional Options")) - sslides[i+4].Append(show.Image("filter.PNG", .13, .29).Modify(mod.Scale(1.25, 1.25))) - sslides[i+4].Append(show.Image("audio.PNG", .53, .29).Modify(mod.Scale(1.25, 1.25))) - sslides[i+4].Append(show.Image("dataFilter.PNG", .13, .59).Modify(mod.Scale(1.25, 1.25))) - // - // On the implementation side, though, if you have multiple types of - // particle generators, it's really frustrating to have to define interfaces - // for them accepting a whole bunch of different kinds of settings or not. - // While we haven't refactored particles to use this approach yet, our - // audio library fixes this by defining all filters on audio as functions - // that define their own Apply function-- so the logic for whether or not - // a particle type supports a setting can be confined to the type of filter. -} - -var ( - ai = slideSetup{ - addAI, - 5, - } -) - -func addAI(i int, sslides []*static.Slide) { - sslides[i].Append(show.Title("Building AI with Interfaces")) - - sslides[i+1].Append(show.Header("When Your Interface is Massive")) - sslides[i+1].Append(show.ImageAt("agentAI.PNG", .4, .5)) - sslides[i+1].Append(show.ImageAt("agentCharacter.PNG", .7, .5)) - - // But now that you've stored all of your enemy types as themselves, - // if you've got a bunch of procedures that run on your AI for - // pathing, targeting, or attacking, you'll run into this issue - // where the interface that defines your AI needs to know a lot - // of different information for each of these different behaviors. - - sslides[i+2].Append(show.Header("Condensing Massive Interfaces")) - sslides[i+2].Append(show.ImageAt("agentEnemy.PNG", .5, .5)) - - // The solution to this is to implement this sort of interface, - // where you compose all of your entities with a struct that has - // a function to return itself (as a pointer). Define an interface - // of just that function and... - - sslides[i+3].Append(show.Header("Reusable AI")) - sslides[i+3].Append(show.ImageAt("doctorEntity.PNG", .39, .5, mod.Scale(1.25, 1.25))) - sslides[i+3].Append(show.ImageAt("doctorHasE.PNG", .15, .5, mod.Scale(1.25, 1.25))) - sslides[i+3].Append(show.ImageAt("doctorBounce.PNG", .75, .5, mod.Scale(1.25, 1.25))) - - // ... now you can store all of the things - // any AI entity needs in one embedded struct and run all of your - // entities on any AI procedure you have. - - sslides[i+4].Append(show.Header("Aside: Composition for Private Features")) - sslides[i+4].Append(show.ImageAt("phase.PNG", .3, .5)) - sslides[i+4].Append(show.ImageAt("phaseCollision.PNG", .7, .5)) -} - -var ( - levels = slideSetup{ - addLevels, - 8, - } -) - -func addLevels(i int, sslides []*static.Slide) { - sslides[i].Append(show.Title("Designing Levels with Interfaces")) - sslides[i+1].Append(show.Header("A Poor Approach")) - sslides[i+1].Append(show.ImageCaption("agentLevelGenerate.PNG", .3, .1, 1, Libel28, "Level Generation in Agent Blue")) - sslides[i+1].Append(show.ImageAt("agentRoom.PNG", .6, .5, mod.Scale(1.25, 1.25))) - // - // Our first approach to building levels didn't use interfaces. - // We're going to go through why this was a terrible idea. - // Agent Blue was the first game we started making in Go and so - // it also has all of our worst patterns in its code. - // ... - sslides[i+2].Append(show.Header("A Poor Approach")) - sslides[i+2].Append(show.ImageCaption("agentTutorial.png", .2, .4, 5, Libel28, "Agent Blue Tutorial Map")) - sslides[i+2].Append(show.ImageCaption("agentLevelSelect.PNG", .5, .3, 1.25, Libel28, "Agent Blue Level Select Room (demo)")) - // Level vs LevelSelect - sslides[i+3].Append(show.Header("A Poor Approach")) - sslides[i+3].Append(show.ImageAt("agentDoodad.PNG", .5, .5, mod.Scale(1.25, 1.25))) - // - // Doodads - // - sslides[i+4].Append(show.Header("Modular Tile Enumeration")) - sslides[i+4].Append(show.ImageAt("jeremyLevel.PNG", .4, .5)) - sslides[i+4].Append(show.ImageAt("jeremyTileTypes.PNG", .6, .5)) - // - // So we were restricted because our tiles had too limited functionality. - // In Jeremy the Clam I tried to adapt this out by giving tiles explicit - // types based on their value, and each tile built itself using a Place - // function. So where before we had a tile interface, now tiles are just - // integers, making shared functionality a lot easier. - // - sslides[i+5].Append(show.Header("Modular Tile Enumeration")) - sslides[i+5].Append(show.ImageAt("jeremyTilePlace.PNG", .5, .5)) - // - // However, the immediate problem caused from this was that we no longer - // could place multiple tiles in the same tile position. Before we could - // stack floor tiles below wall tiles or doodad tiles, and now we need - // tiles that are labeled as non-floors to place floors below them - // when they get inserted during the start of the level. - // - sslides[i+6].Append(show.Header("Level Interfaces")) - sslides[i+6].Append(show.ImageAt("doctorLevelPlace.PNG", .3, .5)) - sslides[i+6].Append(show.ImageAt("doctorLevel.PNG", .7, .5)) - // - // We addressed this in A Fantastic Doctor by moving entity creation - // out of levels themselves, but also by abstracting the concept of a - // level in the first place. In this case, all a level (or in this - // game, an Organ) needs to provide is a Place() function to initialize - // all of its components when it is entered. - // - // - // So while we didn't do this, that means that we can extend organ - // functionality by making organs with layers of tiles instead of - // just one 2d layer. - sslides[i+7].Append(show.Header("Level Files")) - sslides[i+7].Append(show.ImageCaption("jeremyFile.PNG", .2, .3, 1.0, Libel28, "A Jeremy Level File")) - sslides[i+7].Append(show.ImageCaption("doctorFile.png", .6, .3, 2.0, Libel28, "A Fantastic Doctor Level File")) -} - -var ( - conclusion = slideSetup{ - addConclusion, - 3, - } -) - -func addConclusion(i int, sslides []*static.Slide) { - sslides[i].Append(show.Header("Thanks To")) - sslides[i].Append( - show.TxtSetFrom(Gnuolane44, .25, .35, 0, .07, - "- Nate Fudenberg, John Ficklin", - "- Contributors on Github", - "- You, Audience", - )..., - ) - - // And I'll end by thanking the other people in Oakmound - // for working with me on our engine, those who've tried - // out the engine and raised issues or PRs, and all of you - // for listening. - sslides[i+1].Append(show.Header("Other GUI Programs")) - sslides[i+1].Append(show.ImageCaption("slide.PNG", .2, .4, 1.25, Libel28, "This Slideshow")) - - // As a final note, to demonstrate some kind of versatility, - // this slide show was written in Oak as well. - - sslides[i+2].Append(show.Title("Questions")) - - // ? -} diff --git a/examples/slide/show/fonts.go b/examples/slide/show/fonts.go deleted file mode 100644 index 53e95c5a..00000000 --- a/examples/slide/show/fonts.go +++ /dev/null @@ -1,60 +0,0 @@ -package show - -import ( - "image" - "image/color" - "path" - - "github.com/oakmound/oak/v3/render" -) - -func InitFonts() (err error) { - Express, err = (&render.FontGenerator{ - File: fpFilter("expressway rg.ttf"), - Color: image.NewUniform(color.RGBA{255, 255, 255, 255}), - }).Generate() - if err != nil { - return - } - Gnuolane, err = (&render.FontGenerator{ - File: fpFilter("gnuolane rg.ttf"), - Color: image.NewUniform(color.RGBA{255, 255, 255, 255}), - }).Generate() - if err != nil { - return - } - Libel, err = (&render.FontGenerator{ - File: fpFilter("libel-suit-rg.ttf"), - Color: image.NewUniform(color.RGBA{255, 255, 255, 255}), - }).Generate() - if err != nil { - return - } - return nil -} - -var ( - Express *render.Font - Gnuolane *render.Font - Libel *render.Font -) - -//FontSize sets size on a font -func FontSize(size float64) func(render.FontGenerator) render.FontGenerator { - return func(f render.FontGenerator) render.FontGenerator { - f.Size = size - return f - } -} - -//FontColor sets the color on a font -func FontColor(c color.Color) func(render.FontGenerator) render.FontGenerator { - return func(f render.FontGenerator) render.FontGenerator { - f.Color = image.NewUniform(c) - return f - } -} - -func fpFilter(file string) string { - return path.Join("assets", "font", file) -} diff --git a/examples/slide/show/helpers.go b/examples/slide/show/helpers.go deleted file mode 100644 index e15c6201..00000000 --- a/examples/slide/show/helpers.go +++ /dev/null @@ -1,168 +0,0 @@ -package show - -import ( - "fmt" - "path/filepath" - "strings" - - "github.com/oakmound/oak/v3/render/mod" - - "github.com/oakmound/oak/v3/render" -) - -var ( - width, height float64 -) - -// SetDims for the whole presentation global -func SetDims(w, h float64) { - width = w - height = h -} - -var ( - titleFont *render.Font -) - -// SetTitleFont on the presentation -func SetTitleFont(f *render.Font) { - if f == nil { - fmt.Println("title font nil") - } - titleFont = f -} - -// TxtSetAt creates text starting from a given location and advancing each text by an offset -func TxtSetAt(f *render.Font, xpos, ypos, xadv, yadv float64, txts ...string) []render.Renderable { - rs := make([]render.Renderable, len(txts)) - for i, txt := range txts { - rs[i] = TxtAt(f, txt, xpos, ypos) - xpos += xadv - ypos += yadv - } - return rs -} - -// TxtAt creates string on screen at a given location -func TxtAt(f *render.Font, txt string, xpos, ypos float64) render.Renderable { - return Pos(f.NewText(txt, 0, 0), xpos, ypos) -} - -// Title draws a string as the title of a slide -func Title(str string) render.Renderable { - return TxtAt(titleFont, str, .5, .4) -} - -// Header draws a header for the slide -func Header(str string) render.Renderable { - return TxtAt(titleFont, str, .5, .2) -} - -// TextSetFrom draws a series of text with offsets from top right not left -func TxtSetFrom(f *render.Font, xpos, ypos, xadv, yadv float64, txts ...string) []render.Renderable { - rs := make([]render.Renderable, len(txts)) - for i, txt := range txts { - rs[i] = TxtFrom(f, txt, xpos, ypos) - xpos += xadv - ypos += yadv - } - return rs -} - -// TxtFrom draws a new string starting from the right rather than the left -func TxtFrom(f *render.Font, txt string, xpos, ypos float64) render.Renderable { - return f.NewText(txt, width*xpos, height*ypos) -} - -// Pos sets the center x and y for a renderable -func Pos(r render.Renderable, xpos, ypos float64) render.Renderable { - XPos(r, xpos) - YPos(r, ypos) - return r -} - -// XPos sets the centered X pos of a renderable -func XPos(r render.Renderable, pos float64) render.Renderable { - w, _ := r.GetDims() - r.SetPos(width*pos-float64(w/2), r.Y()) - return r -} - -// YPos sets the centered Y pos of a renderable -func YPos(r render.Renderable, pos float64) render.Renderable { - _, h := r.GetDims() - r.SetPos(r.X(), height*pos-float64(h/2)) - return r -} - -// Image renders a static image at a location -func Image(file string, xpos, ypos float64) render.Modifiable { - s, err := render.LoadSprite(filepath.Join("assets", "images", "raw", file)) - if err != nil { - fmt.Println(err) - return nil - } - s.SetPos(width*xpos, height*ypos) - return s -} - -// ImageAt creates an image, centers it and applies modifications -func ImageAt(file string, xpos, ypos float64, mods ...mod.Mod) render.Modifiable { - m := Image(file, xpos, ypos) - m.Modify(mods...) - w, h := m.GetDims() - m.ShiftX(float64(-w / 2)) - m.ShiftY(float64(-h / 2)) - return m -} - -// ImageCaptionSize set the caption and its size -func ImageCaptionSize(file string, xpos, ypos float64, w, h float64, f *render.Font, cap string) render.Renderable { - r, err := render.LoadSprite(filepath.Join("assets", "images", "raw", file)) - if err != nil { - fmt.Println(err) - return nil - } - w2, h2 := r.GetDims() - w3 := float64(w2) / width - h3 := float64(h2) / height - wScale := w / w3 - hScale := h / h3 - if wScale > hScale { - wScale = hScale - } else { - hScale = wScale - } - r.Modify(mod.Scale(wScale, hScale)) - w4, h4 := r.GetDims() - r.SetPos(width*xpos, height*ypos) - r.ShiftX(float64(-w4 / 2)) - r.ShiftY(float64(-h4 / 2)) - - x := r.X() + float64(w4)/2 - y := r.Y() + float64(h4) + 42 - - caps := strings.Split(cap, "\n") - for i := 1; i < len(caps); i++ { - // remove whitespace - caps[i] = strings.TrimSpace(caps[i]) - } - s := TxtSetAt(f, float64(x)/width, float64(y)/height, 0, .04, caps...) - - return render.NewCompositeR(append(s, r)...) -} - -// ImageCaption creates caption text -func ImageCaption(file string, xpos, ypos float64, scale float64, f *render.Font, cap string) render.Renderable { - r := Image(file, xpos, ypos) - r.Modify(mod.Scale(scale, scale)) - w, h := r.GetDims() - - x := r.X() + float64(w)/2 - y := r.Y() + float64(h) + 28 - - s := f.NewText(cap, x, y) - s.Center() - - return render.NewCompositeR(r, s) -} diff --git a/examples/slide/show/slide.go b/examples/slide/show/slide.go deleted file mode 100644 index acf4fa7c..00000000 --- a/examples/slide/show/slide.go +++ /dev/null @@ -1,129 +0,0 @@ -package show - -import ( - "fmt" - "image" - "image/color" - "strconv" - - oak "github.com/oakmound/oak/v3" - "github.com/oakmound/oak/v3/debugstream" - "github.com/oakmound/oak/v3/event" - "github.com/oakmound/oak/v3/render" - "github.com/oakmound/oak/v3/scene" -) - -type Slide interface { - Init() - Continue() bool - Prev() bool - Transition() scene.Transition -} - -func slideResult(sl Slide) *scene.Result { - return &scene.Result{ - Transition: sl.Transition(), - } -} - -var ( - skip bool - skipTo string -) - -func AddNumberShortcuts(max int) { - debugstream.AddCommand(debugstream.Command{Name: "slide", Operation: func(args []string) string { - if len(args) < 2 { - return "" - } - v := args[1] - i, err := strconv.Atoi(v) - if err != nil { - fmt.Println(err) - return "" - } - if i < 0 { - skipTo = "0" - } else if i <= max { - skipTo = v - } else { - skipTo = strconv.Itoa(max) - } - skip = true - return "" - }}) -} - -func Start(width, height int, slides ...Slide) { - for i, sl := range slides { - i := i - sl := sl - oak.AddScene("slide"+strconv.Itoa(i), scene.Scene{ - Start: func(*scene.Context) { sl.Init() }, - Loop: func() bool { - cont := sl.Continue() && !skip - // This should be disable-able - if !cont { - oak.SetLoadingRenderable(render.NewSprite(0, 0, oak.ScreenShot())) - } - return cont - }, - End: func() (string, *scene.Result) { - fmt.Println("ending") - if skip { - skip = false - return "slide" + skipTo, slideResult(sl) - } - if sl.Prev() { - if i > 0 { - return "slide" + strconv.Itoa(i-1), slideResult(sl) - } - return "slide0", slideResult(sl) - } - fmt.Println("new slide", strconv.Itoa(i+1)) - return "slide" + strconv.Itoa(i+1), slideResult(sl) - }, - }) - } - - reset := false - - var oldBackground image.Image - - oak.AddScene("slide"+strconv.Itoa(len(slides)), - scene.Scene{ - Start: func(ctx *scene.Context) { - oldBackground = oak.GetBackgroundImage() - oak.SetColorBackground(image.NewUniform(color.RGBA{0, 0, 0, 255})) - render.Draw( - Express.NewText( - "Spacebar to restart show ...", - float64(ctx.Window.Width()/2), - float64(ctx.Window.Height()-50), - ), - ) - event.GlobalBind("KeyDownSpacebar", func(event.CID, interface{}) int { - reset = true - return 0 - }) - }, - Loop: func() bool { - return !reset - }, - End: func() (string, *scene.Result) { - oak.SetColorBackground(oldBackground) - reset = false - skip = false - return "slide0", nil - }, - }, - ) - oak.Init("slide0", func(c oak.Config) (oak.Config, error) { - c.Screen.Width = width - c.Screen.Height = height - c.FrameRate = 30 - c.DrawFrameRate = 30 - c.EnableDebugConsole = true - return c, nil - }) -} diff --git a/examples/slide/show/static/basicSlide.go b/examples/slide/show/static/basicSlide.go deleted file mode 100644 index b0a4e803..00000000 --- a/examples/slide/show/static/basicSlide.go +++ /dev/null @@ -1,110 +0,0 @@ -package static - -import ( - "fmt" - "os" - - oak "github.com/oakmound/oak/v3" - "github.com/oakmound/oak/v3/event" - "github.com/oakmound/oak/v3/render" - "github.com/oakmound/oak/v3/scene" -) - -type Slide struct { - Rs *render.CompositeR - ContinueKey string - PrevKey string - transition scene.Transition - cont bool - prev bool - OnClick func() -} - -func (ss *Slide) Init() { - oak.SetFullScreen(true) - render.Draw(ss.Rs, 0) - event.GlobalBind("KeyUp"+ss.ContinueKey, func(event.CID, interface{}) int { - fmt.Println("continue key pressed") - ss.cont = true - return 0 - }) - event.GlobalBind("KeyUp"+ss.PrevKey, func(event.CID, interface{}) int { - fmt.Println("prev key pressed") - ss.prev = true - return 0 - }) - event.GlobalBind("KeyUpEscape", func(event.CID, interface{}) int { - os.Exit(0) - return 0 - }) - if ss.OnClick != nil { - event.GlobalBind("MousePress", func(event.CID, interface{}) int { - ss.OnClick() - return 0 - }) - } -} - -func (ss *Slide) Continue() bool { - return !ss.cont && !ss.prev -} - -func (ss *Slide) Prev() bool { - ret := ss.prev - ss.prev = false - ss.cont = false - return ret -} - -func (ss *Slide) Append(rs ...render.Renderable) { - for _, r := range rs { - ss.Rs.Append(r) - } -} - -func (ss *Slide) Transition() scene.Transition { - return ss.transition -} - -func NewSlide(rs ...render.Renderable) *Slide { - return &Slide{ - Rs: render.NewCompositeR(rs...), - ContinueKey: "RightArrow", - PrevKey: "LeftArrow", - } -} - -func Transition(trans scene.Transition) SlideOption { - return func(s *Slide) *Slide { - s.transition = trans - return s - } -} - -func Background(r render.Modifiable) SlideOption { - return func(s *Slide) *Slide { - s.Rs.Prepend(r) - return s - } -} - -func ControlKeys(cont, prev string) SlideOption { - return func(s *Slide) *Slide { - s.ContinueKey = cont - s.PrevKey = prev - return s - } -} - -type SlideOption func(*Slide) *Slide - -func NewSlideSet(n int, opts ...SlideOption) []*Slide { - slides := make([]*Slide, n) - for i := range slides { - slides[i] = NewSlide() - for _, opt := range opts { - slides[i] = opt(slides[i]) - } - } - return slides -} diff --git a/examples/sprite-demo/README.md b/examples/sprite/README.md similarity index 100% rename from examples/sprite-demo/README.md rename to examples/sprite/README.md diff --git a/examples/sprite-demo/assets/images/raw/gopher11.png b/examples/sprite/assets/images/raw/gopher11.png similarity index 100% rename from examples/sprite-demo/assets/images/raw/gopher11.png rename to examples/sprite/assets/images/raw/gopher11.png diff --git a/examples/sprite-demo/main.go b/examples/sprite/main.go similarity index 62% rename from examples/sprite-demo/main.go rename to examples/sprite/main.go index a427072e..cbd7bff2 100644 --- a/examples/sprite-demo/main.go +++ b/examples/sprite/main.go @@ -7,12 +7,12 @@ import ( "math/rand" "path/filepath" - oak "github.com/oakmound/oak/v3" - "github.com/oakmound/oak/v3/entities" - "github.com/oakmound/oak/v3/event" - "github.com/oakmound/oak/v3/render" - "github.com/oakmound/oak/v3/render/mod" - "github.com/oakmound/oak/v3/scene" + oak "github.com/oakmound/oak/v4" + "github.com/oakmound/oak/v4/event" + "github.com/oakmound/oak/v4/key" + "github.com/oakmound/oak/v4/render" + "github.com/oakmound/oak/v4/render/mod" + "github.com/oakmound/oak/v4/scene" ) const ( @@ -27,7 +27,7 @@ var cache = [360]*image.RGBA{} func main() { oak.AddScene( "demo", - scene.Scene{Start: func(*scene.Context) { + scene.Scene{Start: func(ctx *scene.Context) { render.Draw(render.NewDrawFPS(0.03, nil, 10, 10)) render.Draw(render.NewLogicFPS(0.03, nil, 10, 20)) @@ -35,11 +35,11 @@ func main() { layerTxt := render.DefaultFont().NewIntText(&layer, 30, 20) layerTxt.SetLayer(100000000) render.Draw(layerTxt, 0) - NewGopher(layer) + NewGopher(ctx, layer) layer++ - event.GlobalBind(event.Enter, func(event.CID, interface{}) int { - if oak.IsDown("K") { - NewGopher(layer) + event.GlobalBind(ctx, event.Enter, func(ev event.EnterPayload) event.Response { + if oak.IsDown(key.K) { + NewGopher(ctx, layer) layer++ } return 0 @@ -71,40 +71,35 @@ var assets embed.FS // Gopher is a basic bouncing renderable type Gopher struct { - *entities.Doodad + *render.Switch + event.CallerID deltaX, deltaY float64 rotation int } -// Init sets up a gophers CID -func (g *Gopher) Init() event.CID { - return event.NextID(g) -} - // NewGopher creates a gopher sprite to bounce around -func NewGopher(layer int) { - goph := Gopher{} - goph.Doodad = entities.NewDoodad( +func NewGopher(ctx *scene.Context, layer int) { + goph := new(Gopher) + + goph.Switch = render.NewSwitch("goph", map[string]render.Modifiable{"goph": render.EmptyRenderable()}) + goph.Switch.SetLayer(layer) + goph.Switch.SetPos( rand.Float64()*576, rand.Float64()*416, - render.NewSwitch("goph", map[string]render.Modifiable{"goph": render.EmptyRenderable()}), - //render.NewReverting(render.LoadSprite(filepath.Join("raw", "gopher11.png"))), - goph.Init()) - goph.R.SetLayer(layer) - goph.Bind("EnterFrame", gophEnter) + ) + goph.CallerID = ctx.Register(goph) + event.Bind(ctx, event.Enter, goph, gophEnter) goph.deltaX = 4 * float64(rand.Intn(2)*2-1) goph.deltaY = 4 * float64(rand.Intn(2)*2-1) goph.rotation = rand.Intn(360) - render.Draw(goph.R, 0) + render.Draw(goph.Switch, 0) } -func gophEnter(cid event.CID, nothing interface{}) int { - goph := event.GetEntity(cid).(*Gopher) - +func gophEnter(goph *Gopher, ev event.EnterPayload) event.Response { // Compare against this version of rotation // (also swap the comments on lines in goph.Doodad's renderable) //goph.R.(*render.Reverting).RevertAndModify(1, render.Rotate(goph.rotation)) - goph.R.(*render.Switch).Add("goph", render.NewSprite(0, 0, cache[goph.rotation])) + goph.Switch.Add("goph", render.NewSprite(0, 0, cache[goph.rotation])) if goph.X() < minX || goph.X() > maxX { goph.deltaX *= -1 } diff --git a/examples/svg/assets/images/TestShapes.svg b/examples/svg/assets/images/TestShapes.svg deleted file mode 100644 index 26de7ec0..00000000 --- a/examples/svg/assets/images/TestShapes.svg +++ /dev/null @@ -1,37 +0,0 @@ - - Example triangle01- simple example of a 'path' - A path that draws a triangle - - - - - - - - - - - - - - - - - - - - - - diff --git a/examples/svg/go.mod b/examples/svg/go.mod deleted file mode 100644 index c85ce234..00000000 --- a/examples/svg/go.mod +++ /dev/null @@ -1,12 +0,0 @@ -module github.com/oakmound/oak/examples/svg - -go 1.16 - -require ( - github.com/oakmound/oak/v3 v3.0.0-alpha.1 - github.com/srwiley/oksvg v0.0.0-20210320200257-875f767ac39a - github.com/srwiley/rasterx v0.0.0-20200120212402-85cb7272f5e9 - golang.org/x/image v0.0.0-20210504121937-7319ad40d33e // indirect -) - -replace github.com/oakmound/oak/v3 => ../.. diff --git a/examples/svg/go.sum b/examples/svg/go.sum deleted file mode 100644 index e94799e5..00000000 --- a/examples/svg/go.sum +++ /dev/null @@ -1,78 +0,0 @@ -dmitri.shuralyov.com/gpu/mtl v0.0.0-20201218220906-28db891af037 h1:+PdD6GLKejR9DizMAKT5DpSAkKswvZrurk1/eEt9+pw= -dmitri.shuralyov.com/gpu/mtl v0.0.0-20201218220906-28db891af037/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= -github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= -github.com/BurntSushi/xgb v0.0.0-20210121224620-deaf085860bc h1:7D+Bh06CRPCJO3gr2F7h1sriovOZ8BMhca2Rg85c2nk= -github.com/BurntSushi/xgb v0.0.0-20210121224620-deaf085860bc/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= -github.com/BurntSushi/xgbutil v0.0.0-20190907113008-ad855c713046 h1:O/r2Sj+8QcMF7V5IcmiE2sMFV2q3J47BEirxbXJAdzA= -github.com/BurntSushi/xgbutil v0.0.0-20190907113008-ad855c713046/go.mod h1:uw9h2sd4WWHOPdJ13MQpwK5qYWKYDumDqxWWIknEQ+k= -github.com/disintegration/gift v1.2.0 h1:VMQeei2F+ZtsHjMgP6Sdt1kFjRhs2lGz8ljEOPeIR50= -github.com/disintegration/gift v1.2.0/go.mod h1:Jh2i7f7Q2BM7Ezno3PhfezbR1xpUg9dUg3/RlKGr4HI= -github.com/eaburns/bit v0.0.0-20131029213740-7bd5cd37375d/go.mod h1:CHkHWWZ4kbGY6jEy1+qlitDaCtRgNvCOQdakj/1Yl/Q= -github.com/eaburns/flac v0.0.0-20171003200620-9a6fb92396d1/go.mod h1:frG94byMNy+1CgGrQ25dZ+17tf98EN+OYBQL4Zh612M= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20210410170116-ea3d685f79fb h1:T6gaWBvRzJjuOrdCtg8fXXjKai2xSDqWTcKFUPuw8Tw= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20210410170116-ea3d685f79fb/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g= -github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= -github.com/hajimehoshi/go-mp3 v0.3.1 h1:pn/SKU1+/rfK8KaZXdGEC2G/KCB2aLRjbTCrwKcokao= -github.com/hajimehoshi/go-mp3 v0.3.1/go.mod h1:qMJj/CSDxx6CGHiZeCgbiq2DSUkbK0UbtXShQcnfyMM= -github.com/hajimehoshi/oto v0.6.1/go.mod h1:0QXGEkbuJRohbJaxr7ZQSxnju7hEhseiPx2hrh6raOI= -github.com/jfreymuth/pulse v0.1.0 h1:KN38/9hoF9PJvP5DpEVhMRKNuwnJUonc8c9ARorRXUA= -github.com/jfreymuth/pulse v0.1.0/go.mod h1:cpYspI6YljhkUf1WLXLLDmeaaPFc3CnGLjDZf9dZ4no= -github.com/oakmound/alsa v0.0.2 h1:JbOUckkJqVvhABth7qy2JgAjqsWuBPggyoYOk1L6eK0= -github.com/oakmound/alsa v0.0.2/go.mod h1:wx+ehwqFnNL7foTwxxu2bKQlaUmD2oXd4ka1UBSgWAo= -github.com/oakmound/libudev v0.2.1 h1:gaXuw7Pbt3RSRxbUakAjl0dSW6Wo3TZWpwS5aMq8+EA= -github.com/oakmound/libudev v0.2.1/go.mod h1:zYF5CkHY+UP6lzWbPR+XoVAscl/s+OncWA//qWjMLUs= -github.com/oakmound/w32 v2.1.0+incompatible h1:vIkC6eJVOaAnwTTOyiVCGh24GoryPRmcvWq3cekkG2U= -github.com/oakmound/w32 v2.1.0+incompatible/go.mod h1:lzloWlclSXIU4cDr67WF8qjFFDO8gHHBIk4Qqe90enQ= -github.com/oov/directsound-go v0.0.0-20141101201356-e53e59c700bf h1:od9gEl9UQ/QNHlgYlgsSaC5SZ+CGbvO2/PCIgserJc0= -github.com/oov/directsound-go v0.0.0-20141101201356-e53e59c700bf/go.mod h1:RBXkZ8n2vvtdJP6PO+TbU/N/DVuCDwUN53CU+C1pJOs= -github.com/srwiley/oksvg v0.0.0-20210320200257-875f767ac39a h1:Lhe6HPtH4ndWfV56fWc4/yQhOP3vEGlwl5nfPyBxUAg= -github.com/srwiley/oksvg v0.0.0-20210320200257-875f767ac39a/go.mod h1:afMbS0qvv1m5tfENCwnOdZGOF8RGR/FsZ7bvBxQGZG4= -github.com/srwiley/rasterx v0.0.0-20200120212402-85cb7272f5e9 h1:m59mIOBO4kfcNCEzJNy71UkeF4XIx2EVmL9KLwDQdmM= -github.com/srwiley/rasterx v0.0.0-20200120212402-85cb7272f5e9/go.mod h1:mvWM0+15UqyrFKqdRjY6LuAVJR0HOVhJlEgZ5JWtSWU= -github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56 h1:estk1glOnSVeJ9tdEZZc5mAMDZk5lNJNyJ6DvrBkTEU= -golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56/go.mod h1:JhuoJpWY28nO4Vef9tZUw9qufEGTyX1+7lmHxV5q5G4= -golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= -golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/image v0.0.0-20201208152932-35266b937fa6/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/image v0.0.0-20210504121937-7319ad40d33e h1:PzJMNfFQx+QO9hrC1GwZ4BoPGeNGhfeQEgcQFArEjPk= -golang.org/x/image v0.0.0-20210504121937-7319ad40d33e/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= -golang.org/x/mobile v0.0.0-20190415191353-3e0bab5405d6/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= -golang.org/x/mobile v0.0.0-20220112015953-858099ff7816 h1:jhDgkcu3yQ4tasBZ+1YwDmK7eFmuVf1w1k+NGGGxfmE= -golang.org/x/mobile v0.0.0-20220112015953-858099ff7816/go.mod h1:pe2sM7Uk+2Su1y7u/6Z8KJ24D7lepUjFZbhFOrmDfuQ= -golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= -golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d h1:20cMwl2fHAzkJMEA+8J4JgqBQcQGzbisXo31MIeenXI= -golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ= -golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190429190828-d89cdac9e872/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220111092808-5a964db01320 h1:0jf+tOCoZ3LyutmCOWpVni1chK4VfFLhRsDK7MhqGRY= -golang.org/x/sys v0.0.0-20220111092808-5a964db01320/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= -golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.1.8-0.20211022200916-316ba0b74098/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/examples/svg/main.go b/examples/svg/main.go deleted file mode 100644 index ceae36bd..00000000 --- a/examples/svg/main.go +++ /dev/null @@ -1,55 +0,0 @@ -package main - -import ( - "bytes" - _ "embed" - "fmt" - "image" - - "github.com/oakmound/oak/v3" - "github.com/oakmound/oak/v3/render" - "github.com/oakmound/oak/v3/scene" - "github.com/srwiley/oksvg" - "github.com/srwiley/rasterx" -) - -//go:embed assets/images/TestShapes.svg -var testShapes []byte - -func main() { - oak.AddScene("svg", scene.Scene{ - Start: func(*scene.Context) { - // load svg - // svg from oksvg testdata - icon, err := oksvg.ReadIconStream(bytes.NewBuffer(testShapes)) - if err != nil { - fmt.Println(err) - } - // put it in the thing - - inputW, inputH := icon.ViewBox.W, icon.ViewBox.H - iconAspect := inputW / inputH - const width = 640 - const height = 480 - - buff := image.NewRGBA(image.Rect(0, 0, width, height)) - - viewAspect := float64(width) / float64(height) - outputW, outputH := width, height - if viewAspect > iconAspect { - outputW = int(float64(height) * iconAspect) - } else if viewAspect < iconAspect { - outputH = int(float64(width) / iconAspect) - } - scanner := rasterx.NewScannerGV(int(inputW), int(inputH), buff, image.Rect(0, 0, width, height)) - scanner.SetBounds(10000, 10000) - dasher := rasterx.NewDasher(width, height, scanner) - icon.SetTarget(0, 0, float64(outputW), float64(outputH)) - icon.Draw(dasher, 1) - - render.Draw(render.NewSprite(0, 0, buff)) - }, - }) - - oak.Init("svg") -} diff --git a/examples/text-demo-1/README.md b/examples/text-demo-1/README.md deleted file mode 100644 index 14f8d568..00000000 --- a/examples/text-demo-1/README.md +++ /dev/null @@ -1,4 +0,0 @@ -# Text Creation -Draw text and update some of it to change its color and display the rgb value - -![text](./example.gif) \ No newline at end of file diff --git a/examples/text-demo-1/main.go b/examples/text-demo-1/main.go deleted file mode 100644 index 0b6c7a30..00000000 --- a/examples/text-demo-1/main.go +++ /dev/null @@ -1,88 +0,0 @@ -package main - -import ( - "embed" - "image/color" - "path" - "strconv" - - "github.com/oakmound/oak/v3/alg/range/floatrange" - "github.com/oakmound/oak/v3/dlog" - - "image" - - oak "github.com/oakmound/oak/v3" - "github.com/oakmound/oak/v3/render" - "github.com/oakmound/oak/v3/scene" -) - -var ( - font *render.Font - r, g, b float64 - diff = floatrange.NewSpread(0, 10) - limit = floatrange.NewLinear(0, 255) -) - -type floatStringer struct { - f *float64 -} - -func (fs floatStringer) String() string { - return strconv.Itoa(int(*fs.f)) -} - -func main() { - oak.AddScene("demo", - scene.Scene{Start: func(*scene.Context) { - render.Draw(render.NewDrawFPS(0.25, nil, 10, 10)) - fg := render.FontGenerator{ - File: path.Join("assets", "font", "luxisbi.ttf"), - Color: image.NewUniform(color.RGBA{255, 0, 0, 255}), - FontOptions: render.FontOptions{ - Size: 50, - DPI: 72, - }, - } - r = 255 - var err error - font, err = fg.Generate() - dlog.ErrorCheck(err) - font.Unsafe = true - txts := []*render.Text{ - font.NewText("Rainbow", 200, 200), - font.NewStringerText(floatStringer{&r}, 200, 260), - font.NewStringerText(floatStringer{&g}, 320, 260), - font.NewStringerText(floatStringer{&b}, 440, 260), - } - for _, txt := range txts { - render.Draw(txt, 0) - } - font2, _ := font.RegenerateWith(func(fg render.FontGenerator) render.FontGenerator { - fg.Color = image.NewUniform(color.RGBA{255, 255, 255, 255}) - return fg - }) - render.Draw(font2.NewText("r", 160, 260), 0) - render.Draw(font2.NewText("g", 280, 260), 0) - render.Draw(font2.NewText("b", 400, 260), 0) - }, - Loop: func() bool { - r = limit.EnforceRange(r + diff.Poll()) - g = limit.EnforceRange(g + diff.Poll()) - b = limit.EnforceRange(b + diff.Poll()) - font.Drawer.Src = image.NewUniform( - color.RGBA{ - uint8(r), - uint8(g), - uint8(b), - 255, - }, - ) - return true - }, - }) - oak.SetFS(assets) - oak.Init("demo") -} - -//go:embed assets -var assets embed.FS diff --git a/examples/text-demo-2/README.md b/examples/text-demo-2/README.md deleted file mode 100644 index aec48a90..00000000 --- a/examples/text-demo-2/README.md +++ /dev/null @@ -1,4 +0,0 @@ -# Text Creation -Continually draw text on screen with a random color - -![text](./example.gif) \ No newline at end of file diff --git a/examples/text-demo-2/main.go b/examples/text-demo-2/main.go deleted file mode 100644 index 235f5c10..00000000 --- a/examples/text-demo-2/main.go +++ /dev/null @@ -1,88 +0,0 @@ -package main - -import ( - "image/color" - "math/rand" - - "github.com/oakmound/oak/v3/alg/range/floatrange" - "github.com/oakmound/oak/v3/dlog" - - "image" - - oak "github.com/oakmound/oak/v3" - "github.com/oakmound/oak/v3/render" - "github.com/oakmound/oak/v3/scene" -) - -// ~60 fps draw rate with these examples in testing -const ( - strRangeTop = 128 - strlen = 250 - strSize = 6 -) - -var ( - font *render.Font - r, g, b float64 - diff = floatrange.NewSpread(0, 10) - limit = floatrange.NewLinear(0, 255) - strs []*render.Text -) - -func randomStr(chars int) string { - str := make([]rune, chars) - // ascii - for i := 0; i < chars; i++ { - str[i] = rune(rand.Intn(strRangeTop)) - } - return string(str) -} - -func main() { - oak.AddScene("demo", - scene.Scene{Start: func(*scene.Context) { - render.Draw(render.NewDrawFPS(.25, nil, 10, 10)) - - r = 255 - fg := render.DefFontGenerator - fg.Color = image.NewUniform(color.RGBA{255, 0, 0, 255}) - fg.FontOptions = render.FontOptions{ - Size: strSize, - } - - var err error - font, err = fg.Generate() - dlog.ErrorCheck(err) - font.Unsafe = true - - for y := 0.0; y <= 480; y += strSize { - str := randomStr(strlen) - strs = append(strs, font.NewText(str, 0, y)) - render.Draw(strs[len(strs)-1], 0) - } - }, - Loop: func() bool { - r = limit.EnforceRange(r + diff.Poll()) - g = limit.EnforceRange(g + diff.Poll()) - b = limit.EnforceRange(b + diff.Poll()) - // This should be a function in oak to just set color source - // (or texture source) - font.Drawer.Src = image.NewUniform( - color.RGBA{ - uint8(r), - uint8(g), - uint8(b), - 255, - }, - ) - for _, st := range strs { - st.SetString(randomStr(strlen)) - } - return true - }, - }) - render.SetDrawStack( - render.NewDynamicHeap(), - ) - oak.Init("demo") -} diff --git a/examples/text/README.md b/examples/text/README.md new file mode 100644 index 00000000..e90d0d0c --- /dev/null +++ b/examples/text/README.md @@ -0,0 +1,3 @@ +# Text Creation + +Examples of drawing text, supporting multiple fonts, and changing text color / content while on the screen. diff --git a/examples/text-demo-1/assets/font/luxisbi.ttf b/examples/text/assets/font/luxisbi.ttf similarity index 100% rename from examples/text-demo-1/assets/font/luxisbi.ttf rename to examples/text/assets/font/luxisbi.ttf diff --git a/examples/text/go.mod b/examples/text/go.mod new file mode 100644 index 00000000..1bef8311 --- /dev/null +++ b/examples/text/go.mod @@ -0,0 +1,30 @@ +module github.com/oakmound/oak/examples/text-demo + +go 1.18 + +require ( + github.com/flopp/go-findfont v0.0.0-20201114153133-e7393a00c15b + github.com/oakmound/oak/v4 v4.0.0-alpha.1 +) + +require ( + dmitri.shuralyov.com/gpu/mtl v0.0.0-20201218220906-28db891af037 // indirect + github.com/BurntSushi/xgb v0.0.0-20210121224620-deaf085860bc // indirect + github.com/BurntSushi/xgbutil v0.0.0-20190907113008-ad855c713046 // indirect + github.com/disintegration/gift v1.2.1 // indirect + github.com/go-gl/glfw/v3.3/glfw v0.0.0-20220320163800-277f93cfa958 // indirect + github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect + github.com/jfreymuth/pulse v0.1.0 // indirect + github.com/oakmound/alsa v0.0.2 // indirect + github.com/oakmound/libudev v0.2.1 // indirect + github.com/oakmound/w32 v2.1.0+incompatible // indirect + github.com/oov/directsound-go v0.0.0-20141101201356-e53e59c700bf // indirect + golang.org/x/exp v0.0.0-20220414153411-bcd21879b8fd // indirect + golang.org/x/exp/shiny v0.0.0-20220518171630-0b5c67f07fdf // indirect + golang.org/x/image v0.0.0-20220321031419-a8550c1d254a // indirect + golang.org/x/mobile v0.0.0-20220325161704-447654d348e3 // indirect + golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect + golang.org/x/sys v0.0.0-20220403205710-6acee93ad0eb // indirect +) + +replace github.com/oakmound/oak/v4 => ../.. diff --git a/examples/fallback-font/go.sum b/examples/text/go.sum similarity index 74% rename from examples/fallback-font/go.sum rename to examples/text/go.sum index fe44f8be..21d854ea 100644 --- a/examples/fallback-font/go.sum +++ b/examples/text/go.sum @@ -5,19 +5,14 @@ github.com/BurntSushi/xgb v0.0.0-20210121224620-deaf085860bc h1:7D+Bh06CRPCJO3gr github.com/BurntSushi/xgb v0.0.0-20210121224620-deaf085860bc/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/BurntSushi/xgbutil v0.0.0-20190907113008-ad855c713046 h1:O/r2Sj+8QcMF7V5IcmiE2sMFV2q3J47BEirxbXJAdzA= github.com/BurntSushi/xgbutil v0.0.0-20190907113008-ad855c713046/go.mod h1:uw9h2sd4WWHOPdJ13MQpwK5qYWKYDumDqxWWIknEQ+k= -github.com/disintegration/gift v1.2.0 h1:VMQeei2F+ZtsHjMgP6Sdt1kFjRhs2lGz8ljEOPeIR50= -github.com/disintegration/gift v1.2.0/go.mod h1:Jh2i7f7Q2BM7Ezno3PhfezbR1xpUg9dUg3/RlKGr4HI= -github.com/eaburns/bit v0.0.0-20131029213740-7bd5cd37375d/go.mod h1:CHkHWWZ4kbGY6jEy1+qlitDaCtRgNvCOQdakj/1Yl/Q= -github.com/eaburns/flac v0.0.0-20171003200620-9a6fb92396d1/go.mod h1:frG94byMNy+1CgGrQ25dZ+17tf98EN+OYBQL4Zh612M= +github.com/disintegration/gift v1.2.1 h1:Y005a1X4Z7Uc+0gLpSAsKhWi4qLtsdEcMIbbdvdZ6pc= +github.com/disintegration/gift v1.2.1/go.mod h1:Jh2i7f7Q2BM7Ezno3PhfezbR1xpUg9dUg3/RlKGr4HI= github.com/flopp/go-findfont v0.0.0-20201114153133-e7393a00c15b h1:/wqXgpZNTP8qV1dPEApjJXlDQd5N/F9U/WEvy5SawUI= github.com/flopp/go-findfont v0.0.0-20201114153133-e7393a00c15b/go.mod h1:wKKxRDjD024Rh7VMwoU90i6ikQRCr+JTHB5n4Ejkqvw= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20210410170116-ea3d685f79fb h1:T6gaWBvRzJjuOrdCtg8fXXjKai2xSDqWTcKFUPuw8Tw= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20210410170116-ea3d685f79fb/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20220320163800-277f93cfa958 h1:TL70PMkdPCt9cRhKTqsm+giRpgrd0IGEj763nNr2VFY= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20220320163800-277f93cfa958/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= -github.com/hajimehoshi/go-mp3 v0.3.1 h1:pn/SKU1+/rfK8KaZXdGEC2G/KCB2aLRjbTCrwKcokao= -github.com/hajimehoshi/go-mp3 v0.3.1/go.mod h1:qMJj/CSDxx6CGHiZeCgbiq2DSUkbK0UbtXShQcnfyMM= -github.com/hajimehoshi/oto v0.6.1/go.mod h1:0QXGEkbuJRohbJaxr7ZQSxnju7hEhseiPx2hrh6raOI= github.com/jfreymuth/pulse v0.1.0 h1:KN38/9hoF9PJvP5DpEVhMRKNuwnJUonc8c9ARorRXUA= github.com/jfreymuth/pulse v0.1.0/go.mod h1:cpYspI6YljhkUf1WLXLLDmeaaPFc3CnGLjDZf9dZ4no= github.com/oakmound/alsa v0.0.2 h1:JbOUckkJqVvhABth7qy2JgAjqsWuBPggyoYOk1L6eK0= @@ -32,17 +27,18 @@ github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56 h1:estk1glOnSVeJ9tdEZZc5mAMDZk5lNJNyJ6DvrBkTEU= golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56/go.mod h1:JhuoJpWY28nO4Vef9tZUw9qufEGTyX1+7lmHxV5q5G4= +golang.org/x/exp v0.0.0-20220414153411-bcd21879b8fd h1:zVFyTKZN/Q7mNRWSs1GOYnHM9NiFSJ54YVRsD0rNWT4= +golang.org/x/exp v0.0.0-20220414153411-bcd21879b8fd/go.mod h1:lgLbSvA5ygNOMpwM/9anMpWVlVJ7Z+cHWq/eFuinpGE= +golang.org/x/exp/shiny v0.0.0-20220518171630-0b5c67f07fdf h1:IbXVp9Gov7/6bw4sWq9M/u1cr+mr2NA8SJWvFCnu4is= +golang.org/x/exp/shiny v0.0.0-20220518171630-0b5c67f07fdf/go.mod h1:VjAR7z0ngyATZTELrBSkxOOHhhlnVUxDye4mcjx5h/8= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/image v0.0.0-20201208152932-35266b937fa6 h1:nfeHNc1nAqecKCy2FCy4HY+soOOe5sDLJ/gZLbx6GYI= -golang.org/x/image v0.0.0-20201208152932-35266b937fa6/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.0.0-20220321031419-a8550c1d254a h1:LnH9RNcpPv5Kzi15lXg42lYMPUf0x8CuPv1YnvBWZAg= +golang.org/x/image v0.0.0-20220321031419-a8550c1d254a/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM= golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= -golang.org/x/mobile v0.0.0-20190415191353-3e0bab5405d6/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= -golang.org/x/mobile v0.0.0-20220112015953-858099ff7816 h1:jhDgkcu3yQ4tasBZ+1YwDmK7eFmuVf1w1k+NGGGxfmE= -golang.org/x/mobile v0.0.0-20220112015953-858099ff7816/go.mod h1:pe2sM7Uk+2Su1y7u/6Z8KJ24D7lepUjFZbhFOrmDfuQ= +golang.org/x/mobile v0.0.0-20220325161704-447654d348e3 h1:ZDL7hDvJEQEcHVkoZawKmRUgbqn1pOIzb8EinBh5csU= +golang.org/x/mobile v0.0.0-20220325161704-447654d348e3/go.mod h1:pe2sM7Uk+2Su1y7u/6Z8KJ24D7lepUjFZbhFOrmDfuQ= golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -53,14 +49,12 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190429190828-d89cdac9e872/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220111092808-5a964db01320 h1:0jf+tOCoZ3LyutmCOWpVni1chK4VfFLhRsDK7MhqGRY= -golang.org/x/sys v0.0.0-20220111092808-5a964db01320/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220403205710-6acee93ad0eb h1:PVGECzEo9Y3uOidtkHGdd347NjLtITfJFO9BxFpmRoo= +golang.org/x/sys v0.0.0-20220403205710-6acee93ad0eb/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= diff --git a/examples/text/main.go b/examples/text/main.go new file mode 100644 index 00000000..64848756 --- /dev/null +++ b/examples/text/main.go @@ -0,0 +1,142 @@ +package main + +import ( + "embed" + "fmt" + "image/color" + "path" + "strconv" + + "image" + + findfont "github.com/flopp/go-findfont" + oak "github.com/oakmound/oak/v4" + "github.com/oakmound/oak/v4/alg/span" + "github.com/oakmound/oak/v4/render" + "github.com/oakmound/oak/v4/scene" +) + +//go:embed assets +var assets embed.FS + +type floatStringer struct { + f *float64 +} + +func (fs floatStringer) String() string { + return strconv.Itoa(int(*fs.f)) +} + +func main() { + oak.AddScene("demo", + scene.Scene{Start: func(ctx *scene.Context) { + render.Draw(render.NewDrawFPS(0.25, nil, 10, 10)) + drawFallbackFonts(ctx) + drawColorChangingText(ctx) + }, + }) + oak.SetFS(assets) + oak.Init("demo") +} + +func drawFallbackFonts(ctx *scene.Context) { + const fontHeight = 16 + + fg := render.DefFontGenerator + fg.Color = image.NewUniform(color.RGBA{255, 0, 0, 255}) + fg.FontOptions.Size = fontHeight + font, _ := fg.Generate() + + fallbackFonts := []string{ + "Arial.ttf", + "Yumin.ttf", + // TODO: support multi-color glyphs + "Seguiemj.ttf", + } + + for _, fontname := range fallbackFonts { + fontPath, err := findfont.Find(fontname) + if err != nil { + fmt.Println("Do you have ", fontname, "installed?") + continue + } + fg := render.FontGenerator{ + File: fontPath, + Color: image.NewUniform(color.RGBA{255, 0, 0, 255}), + FontOptions: render.FontOptions{ + Size: fontHeight, + }, + } + fallbackFont, err := fg.Generate() + if err != nil { + panic(err) + } + font.Fallbacks = append(font.Fallbacks, fallbackFont) + } + + strings := []string{ + "Latin-lower: abcdefghijklmnopqrstuvwxyz", + "Latin-upper: ABCDEFGHIJKLMNOPQRSTUVWXYZ", + "Greek-lower: αβγδεζηθικλμνχοπρσςτυφψω", + "Greek-upper: ΑΒΓΔΕΖΗΘΙΚΛΜΝΧΟΠΡΣΤΥΦΨΩ", + "Japanese-kana: あいえおうかきけこくはひへほふさしせそすまみめもむ", + "Kanji: 茂僕私華花日本英雄の時", + "Emoji: 😀😃😄😁😆😅😂🤣🐶🐱🐭🐹🐰🦊🐻🐼", + } + + y := 20.0 + for _, str := range strings { + render.Draw(font.NewText(str, 10, y), 0) + y += fontHeight + } +} + +func drawColorChangingText(ctx *scene.Context) { + var ( + r, g, b float64 + diff = span.NewSpread(0.0, 10.0) + limit = span.NewLinear(0.0, 255.0) + ) + + fg := render.FontGenerator{ + File: path.Join("assets", "font", "luxisbi.ttf"), + Color: image.NewUniform(color.RGBA{255, 0, 0, 255}), + FontOptions: render.FontOptions{ + Size: 50, + DPI: 72, + }, + } + r = 255 + font, _ := fg.Generate() + font.Unsafe = true + texts := []*render.Text{ + font.NewText("Color", 200, 200), + font.NewStringerText(floatStringer{&r}, 200, 260), + font.NewStringerText(floatStringer{&g}, 320, 260), + font.NewStringerText(floatStringer{&b}, 440, 260), + } + for _, txt := range texts { + render.Draw(txt, 0) + } + font2, _ := font.RegenerateWith(func(fg render.FontGenerator) render.FontGenerator { + fg.Color = image.NewUniform(color.RGBA{255, 255, 255, 255}) + return fg + }) + render.Draw(font2.NewText("r", 160, 260), 0) + render.Draw(font2.NewText("g", 280, 260), 0) + render.Draw(font2.NewText("b", 400, 260), 0) + + ctx.DoEachFrame(func() { + r = limit.Clamp(r + diff.Poll()) + g = limit.Clamp(g + diff.Poll()) + b = limit.Clamp(b + diff.Poll()) + font.Drawer.Src = image.NewUniform( + color.RGBA{ + uint8(r), + uint8(g), + uint8(b), + 255, + }, + ) + }) +} diff --git a/examples/titlescreen-demo/main.go b/examples/titlescreen-demo/main.go deleted file mode 100644 index 9cda7718..00000000 --- a/examples/titlescreen-demo/main.go +++ /dev/null @@ -1,119 +0,0 @@ -package main - -import ( - "image/color" - "os" - - oak "github.com/oakmound/oak/v3" - "github.com/oakmound/oak/v3/entities" - "github.com/oakmound/oak/v3/key" - "github.com/oakmound/oak/v3/render" - "github.com/oakmound/oak/v3/scene" -) - -// Axes are the plural of axis -type Axes uint8 - -// This is an enum for what axes to center around -const ( - X Axes = iota - Y - Both -) - -func center(ctx *scene.Context, obj render.Renderable, ax Axes) { - objWidth, objHeight := obj.GetDims() - - switch ax { - case Both: - obj.SetPos(float64(ctx.Window.Width()/2-objWidth/2), - float64(ctx.Window.Height()-objHeight)/2) //distributive property - case X: - obj.SetPos(float64(ctx.Window.Width()-objWidth)/2, obj.Y()) - case Y: - obj.SetPos(obj.X(), float64(ctx.Window.Height()-objHeight)/2) - } -} - -func main() { - //make the scene for the titlescreen - oak.AddScene("titlescreen", scene.Scene{Start: func(ctx *scene.Context) { - - //create text saying titlescreen in placeholder position - titleText := render.NewText("titlescreen", 0, 0) - - //center text along both axes - center(ctx, titleText, Both) - - //tell the draw loop to draw titleText - render.Draw(titleText) - - //do the same for the text with button instuctions, but this time Y position is not a placeholder (X still is) - instructionText := render.NewText("press Enter to start, or press Q to quit", 0, float64(ctx.Window.Height()*3/4)) - //this time we only center the X axis, otherwise it would overlap titleText - center(ctx, instructionText, X) - render.Draw(instructionText) - }, Loop: func() bool { - //if the enter key is pressed, go to the next scene - if oak.IsDown(key.Enter) { - return false - } - //exit the program if the q key is pressed - if oak.IsDown(key.Q) { - os.Exit(0) - } - return true - }, End: func() (string, *scene.Result) { - return "game", nil //set the next scene to "game" - }}) - - //we declare this here so it can be accesed by the scene start and scene loop - var player *entities.Moving - - //define the "game" (it's just a square that can be moved with WASD) - oak.AddScene("game", scene.Scene{Start: func(*scene.Context) { - //create the player, a blue 32x32 square at 100,100 - player = entities.NewMoving(100, 100, 32, 32, - render.NewColorBox(32, 32, color.RGBA{0, 0, 255, 255}), - nil, 0, 0) - //because the player is more than visuals (it has a hitbox, even though we don't use it), - //we have to get the visual part specificaly, and not the whole thing. - render.Draw(player.R) - - controlsText := render.NewText("WASD to move, ESC to return to titlescreen", 5, 20) - //we draw the text on layer 1 (instead of the default layer 0) - //because we want it to show up above the player - render.Draw(controlsText, 1) - }, Loop: func() bool { - //if escape is pressed, go to the next scene (titlescreen) - if oak.IsDown(key.Escape) { - return false - } - //controls - if oak.IsDown(key.S) { - //if S is pressed, set the player's vertical speed to 2 (positive == down) - player.Delta.SetY(2) - } else if oak.IsDown(key.W) { - player.Delta.SetY(-2) - } else { - //if the now buttons are pressed for vertical movement, don't move verticaly - player.Delta.SetY(0) - } - - //do the same thing as before, but horizontaly - if oak.IsDown(key.D) { - player.Delta.SetX(2) - } else if oak.IsDown(key.A) { - player.Delta.SetX(-2) - } else { - player.Delta.SetX(0) - } - //apply the player's speed to their position - player.ShiftPos(player.Delta.X(), player.Delta.Y()) - return true - }, End: func() (string, *scene.Result) { - return "titlescreen", nil //set the next scene to be titlescreen - }}) - //start the game on the titlescreen - oak.Init("titlescreen") -} diff --git a/examples/top-down-shooter-tutorial/1-start/start.go b/examples/top-down-shooter-tutorial/1-start/start.go deleted file mode 100644 index 8a1d6671..00000000 --- a/examples/top-down-shooter-tutorial/1-start/start.go +++ /dev/null @@ -1,62 +0,0 @@ -package main - -import ( - "image/color" - - oak "github.com/oakmound/oak/v3" - "github.com/oakmound/oak/v3/collision" - "github.com/oakmound/oak/v3/entities" - "github.com/oakmound/oak/v3/event" - "github.com/oakmound/oak/v3/key" - "github.com/oakmound/oak/v3/physics" - "github.com/oakmound/oak/v3/render" - "github.com/oakmound/oak/v3/scene" -) - -// Collision labels -const ( - Enemy collision.Label = 1 - Player collision.Label = 2 -) - -var ( - playerAlive = true -) - -func main() { - oak.AddScene("tds", scene.Scene{Start: func(*scene.Context) { - playerAlive = true - char := entities.NewMoving(100, 100, 32, 32, - render.NewColorBox(32, 32, color.RGBA{0, 255, 0, 255}), - nil, 0, 0) - - char.Speed = physics.NewVector(5, 5) - render.Draw(char.R) - - char.Bind(event.Enter, func(id event.CID, _ interface{}) int { - char := event.GetEntity(id).(*entities.Moving) - char.Delta.Zero() - if oak.IsDown(key.W) { - char.Delta.ShiftY(-char.Speed.Y()) - } - if oak.IsDown(key.A) { - char.Delta.ShiftX(-char.Speed.X()) - } - if oak.IsDown(key.S) { - char.Delta.ShiftY(char.Speed.Y()) - } - if oak.IsDown(key.D) { - char.Delta.ShiftX(char.Speed.X()) - } - char.ShiftPos(char.Delta.X(), char.Delta.Y()) - hit := char.HitLabel(Enemy) - if hit != nil { - playerAlive = false - } - - return 0 - }) - - }}) - oak.Init("tds") -} diff --git a/examples/top-down-shooter-tutorial/2-shooting/shooting.go b/examples/top-down-shooter-tutorial/2-shooting/shooting.go deleted file mode 100644 index 4d1e4493..00000000 --- a/examples/top-down-shooter-tutorial/2-shooting/shooting.go +++ /dev/null @@ -1,76 +0,0 @@ -package main - -import ( - "image/color" - "time" - - oak "github.com/oakmound/oak/v3" - "github.com/oakmound/oak/v3/collision" - "github.com/oakmound/oak/v3/entities" - "github.com/oakmound/oak/v3/event" - "github.com/oakmound/oak/v3/key" - "github.com/oakmound/oak/v3/mouse" - "github.com/oakmound/oak/v3/physics" - "github.com/oakmound/oak/v3/render" - "github.com/oakmound/oak/v3/scene" -) - -// Collision labels -const ( - Enemy collision.Label = 1 - Player collision.Label = 2 -) - -var ( - playerAlive = true -) - -func main() { - oak.AddScene("tds", scene.Scene{Start: func(ctx *scene.Context) { - playerAlive = true - char := entities.NewMoving(100, 100, 32, 32, - render.NewColorBox(32, 32, color.RGBA{0, 255, 0, 255}), - nil, 0, 0) - - char.Speed = physics.NewVector(5, 5) - render.Draw(char.R) - - char.Bind(event.Enter, func(id event.CID, _ interface{}) int { - char := event.GetEntity(id).(*entities.Moving) - char.Delta.Zero() - if oak.IsDown(key.W) { - char.Delta.ShiftY(-char.Speed.Y()) - } - if oak.IsDown(key.A) { - char.Delta.ShiftX(-char.Speed.X()) - } - if oak.IsDown(key.S) { - char.Delta.ShiftY(char.Speed.Y()) - } - if oak.IsDown(key.D) { - char.Delta.ShiftX(char.Speed.X()) - } - char.ShiftPos(char.Delta.X(), char.Delta.Y()) - hit := char.HitLabel(Enemy) - if hit != nil { - playerAlive = false - } - - return 0 - }) - - char.Bind(mouse.Press, func(id event.CID, me interface{}) int { - char := event.GetEntity(id).(*entities.Moving) - mevent := me.(*mouse.Event) - ctx.DrawForTime( - render.NewLine(char.X()+char.W/2, char.Y()+char.H/2, mevent.X(), mevent.Y(), color.RGBA{0, 128, 0, 128}), - time.Millisecond*50, - 1) - return 0 - }) - - }, Loop: func() bool { - return playerAlive - }}) - oak.Init("tds") -} diff --git a/examples/top-down-shooter-tutorial/3-enemies/enemies.go b/examples/top-down-shooter-tutorial/3-enemies/enemies.go deleted file mode 100644 index 65ce4577..00000000 --- a/examples/top-down-shooter-tutorial/3-enemies/enemies.go +++ /dev/null @@ -1,162 +0,0 @@ -package main - -import ( - "image/color" - "math/rand" - "time" - - oak "github.com/oakmound/oak/v3" - "github.com/oakmound/oak/v3/alg/floatgeom" - "github.com/oakmound/oak/v3/collision" - "github.com/oakmound/oak/v3/collision/ray" - "github.com/oakmound/oak/v3/entities" - "github.com/oakmound/oak/v3/event" - "github.com/oakmound/oak/v3/key" - "github.com/oakmound/oak/v3/mouse" - "github.com/oakmound/oak/v3/physics" - "github.com/oakmound/oak/v3/render" - "github.com/oakmound/oak/v3/scene" -) - -// Collision labels -const ( - Enemy collision.Label = 1 - Player collision.Label = 2 -) - -var ( - playerAlive = true - // Vectors are backed by pointers, - // so despite this not being a pointer, - // this does update according to the player's - // position so long as we don't reset - // the player's position vector - playerPos physics.Vector -) - -func main() { - oak.AddScene("tds", scene.Scene{Start: func(ctx *scene.Context) { - playerAlive = true - char := entities.NewMoving(100, 100, 32, 32, - render.NewColorBox(32, 32, color.RGBA{0, 255, 0, 255}), - nil, 0, 0) - - char.Speed = physics.NewVector(5, 5) - playerPos = char.Point.Vector - render.Draw(char.R) - - char.Bind(event.Enter, func(id event.CID, _ interface{}) int { - char := event.GetEntity(id).(*entities.Moving) - char.Delta.Zero() - if oak.IsDown(key.W) { - char.Delta.ShiftY(-char.Speed.Y()) - } - if oak.IsDown(key.A) { - char.Delta.ShiftX(-char.Speed.X()) - } - if oak.IsDown(key.S) { - char.Delta.ShiftY(char.Speed.Y()) - } - if oak.IsDown(key.D) { - char.Delta.ShiftX(char.Speed.X()) - } - char.ShiftPos(char.Delta.X(), char.Delta.Y()) - hit := char.HitLabel(Enemy) - if hit != nil { - playerAlive = false - } - - return 0 - }) - - char.Bind(mouse.Press, func(id event.CID, me interface{}) int { - char := event.GetEntity(id).(*entities.Moving) - mevent := me.(*mouse.Event) - x := char.X() + char.W/2 - y := char.Y() + char.H/2 - ray.DefaultCaster.CastDistance = floatgeom.Point2{x, y}.Sub(floatgeom.Point2{mevent.X(), mevent.Y()}).Magnitude() - hits := ray.CastTo(floatgeom.Point2{x, y}, floatgeom.Point2{mevent.X(), mevent.Y()}) - for _, hit := range hits { - hit.Zone.CID.Trigger("Destroy", nil) - } - ctx.DrawForTime( - render.NewLine(x, y, mevent.X(), mevent.Y(), color.RGBA{0, 128, 0, 128}), - time.Millisecond*50, - 1) - return 0 - }) - - event.GlobalBind(event.Enter, func(_ event.CID, frames interface{}) int { - enterPayload := frames.(event.EnterPayload) - if enterPayload.FramesElapsed%EnemyRefresh == 0 { - go NewEnemy(ctx) - } - return 0 - }) - - }, Loop: func() bool { - return playerAlive - }}) - oak.Init("tds") -} - -// Top down shooter consts -const ( - EnemyRefresh = 30 - EnemySpeed = 2 -) - -// NewEnemy creates an enemy for a top down shooter -func NewEnemy(ctx *scene.Context) { - x, y := enemyPos(ctx) - - enemy := entities.NewSolid(x, y, 16, 16, - render.NewColorBox(16, 16, color.RGBA{200, 0, 0, 200}), - nil, 0) - - render.Draw(enemy.R) - - enemy.UpdateLabel(Enemy) - - enemy.Bind(event.Enter, func(id event.CID, _ interface{}) int { - enemy := event.GetEntity(id).(*entities.Solid) - // move towards the player - x, y := enemy.GetPos() - pt := floatgeom.Point2{x, y} - pt2 := floatgeom.Point2{playerPos.X(), playerPos.Y()} - delta := pt2.Sub(pt).Normalize().MulConst(EnemySpeed) - enemy.ShiftPos(delta.X(), delta.Y()) - return 0 - }) - - enemy.Bind("Destroy", func(id event.CID, _ interface{}) int { - enemy := event.GetEntity(id).(*entities.Solid) - enemy.Destroy() - return 0 - }) -} - -func enemyPos(ctx *scene.Context) (float64, float64) { - w := ctx.Window.Width() - h := ctx.Window.Height() - // Spawn on the edge of the screen - perimeter := w*2 + h*2 - pos := int(rand.Float64() * float64(perimeter)) - // Top - if pos < w { - return float64(pos), 0 - } - pos -= w - // Right - if pos < h { - return float64(w), float64(pos) - } - // Bottom - pos -= h - if pos < w { - return float64(pos), float64(h) - } - pos -= w - // Left - return 0, float64(pos) -} diff --git a/examples/top-down-shooter-tutorial/4-sprites/sprites.go b/examples/top-down-shooter-tutorial/4-sprites/sprites.go deleted file mode 100644 index 26cbd871..00000000 --- a/examples/top-down-shooter-tutorial/4-sprites/sprites.go +++ /dev/null @@ -1,234 +0,0 @@ -package main - -import ( - "fmt" - "image/color" - "math/rand" - "time" - - "github.com/oakmound/oak/v3/render/mod" - - oak "github.com/oakmound/oak/v3" - "github.com/oakmound/oak/v3/alg/floatgeom" - "github.com/oakmound/oak/v3/collision" - "github.com/oakmound/oak/v3/collision/ray" - "github.com/oakmound/oak/v3/dlog" - "github.com/oakmound/oak/v3/entities" - "github.com/oakmound/oak/v3/event" - "github.com/oakmound/oak/v3/key" - "github.com/oakmound/oak/v3/mouse" - "github.com/oakmound/oak/v3/physics" - "github.com/oakmound/oak/v3/render" - "github.com/oakmound/oak/v3/scene" -) - -// Collision labels -const ( - Enemy collision.Label = 1 - Player collision.Label = 2 -) - -var ( - playerAlive = true - // Vectors are backed by pointers, - // so despite this not being a pointer, - // this does update according to the player's - // position so long as we don't reset - // the player's position vector - playerPos physics.Vector - - sheet [][]*render.Sprite -) - -func main() { - oak.AddScene("tds", scene.Scene{Start: func(ctx *scene.Context) { - // Initialization - playerAlive = true - sprites, err := render.GetSheet("sheet.png") - dlog.ErrorCheck(err) - sheet = sprites.ToSprites() - - // Player setup - eggplant, err := render.GetSprite("eggplant-fish.png") - playerR := render.NewSwitch("left", map[string]render.Modifiable{ - "left": eggplant, - // We must copy the sprite before we modify it, or "left" - // will also be flipped. - "right": eggplant.Copy().Modify(mod.FlipX), - }) - if err != nil { - fmt.Println(err) - } - char := entities.NewMoving(100, 100, 32, 32, - playerR, - nil, 0, 0) - - char.Speed = physics.NewVector(5, 5) - playerPos = char.Point.Vector - render.Draw(char.R, 2) - - char.Bind(event.Enter, func(id event.CID, _ interface{}) int { - char := event.GetEntity(id).(*entities.Moving) - char.Delta.Zero() - if oak.IsDown(key.W) { - char.Delta.ShiftY(-char.Speed.Y()) - } - if oak.IsDown(key.A) { - char.Delta.ShiftX(-char.Speed.X()) - } - if oak.IsDown(key.S) { - char.Delta.ShiftY(char.Speed.Y()) - } - if oak.IsDown(key.D) { - char.Delta.ShiftX(char.Speed.X()) - } - char.ShiftPos(char.Delta.X(), char.Delta.Y()) - hit := char.HitLabel(Enemy) - if hit != nil { - playerAlive = false - } - - // update animation - swtch := char.R.(*render.Switch) - if char.Delta.X() > 0 { - if swtch.Get() == "left" { - swtch.Set("right") - } - } else if char.Delta.X() < 0 { - if swtch.Get() == "right" { - swtch.Set("left") - } - } - - return 0 - }) - - char.Bind(mouse.Press, func(id event.CID, me interface{}) int { - char := event.GetEntity(id).(*entities.Moving) - mevent := me.(*mouse.Event) - x := char.X() + char.W/2 - y := char.Y() + char.H/2 - ray.DefaultCaster.CastDistance = floatgeom.Point2{x, y}.Sub(floatgeom.Point2{mevent.X(), mevent.Y()}).Magnitude() - hits := ray.CastTo(floatgeom.Point2{x, y}, floatgeom.Point2{mevent.X(), mevent.Y()}) - for _, hit := range hits { - hit.Zone.CID.Trigger("Destroy", nil) - } - ctx.DrawForTime( - render.NewLine(x, y, mevent.X(), mevent.Y(), color.RGBA{0, 128, 0, 128}), - time.Millisecond*50, - 2) - return 0 - }) - - // Create enemies periodically - event.GlobalBind(event.Enter, func(_ event.CID, frames interface{}) int { - enterPayload := frames.(event.EnterPayload) - if enterPayload.FramesElapsed%EnemyRefresh == 0 { - go NewEnemy(ctx) - } - return 0 - }) - - // Draw the background - for x := 0; x < ctx.Window.Width(); x += 16 { - for y := 0; y < ctx.Window.Height(); y += 16 { - i := rand.Intn(3) + 1 - // Get a random tile to draw in this position - sp := sheet[i/2][i%2].Copy() - sp.SetPos(float64(x), float64(y)) - render.Draw(sp, 1) - } - } - - }, Loop: func() bool { - return playerAlive - }}) - - oak.Init("tds", func(c oak.Config) (oak.Config, error) { - // This indicates to oak to automatically open and load image and audio - // files local to the project before starting any scene. - c.BatchLoad = true - c.Debug.Level = "Verbose" - c.Assets.ImagePath = "assets/images" - - return c, nil - }) -} - -// Top down shooter constsv -const ( - EnemyRefresh = 30 - EnemySpeed = 2 -) - -// NewEnemy creates an enemy for a top down shooter -func NewEnemy(ctx *scene.Context) { - x, y := enemyPos(ctx) - - enemyFrame := sheet[0][0].Copy() - enemyR := render.NewSwitch("left", map[string]render.Modifiable{ - "left": enemyFrame, - "right": enemyFrame.Copy().Modify(mod.FlipX), - }) - enemy := entities.NewSolid(x, y, 16, 16, - enemyR, - nil, 0) - - render.Draw(enemy.R, 2) - - enemy.UpdateLabel(Enemy) - - enemy.Bind(event.Enter, func(id event.CID, _ interface{}) int { - enemy := event.GetEntity(id).(*entities.Solid) - // move towards the player - x, y := enemy.GetPos() - pt := floatgeom.Point2{x, y} - pt2 := floatgeom.Point2{playerPos.X(), playerPos.Y()} - delta := pt2.Sub(pt).Normalize().MulConst(EnemySpeed) - enemy.ShiftPos(delta.X(), delta.Y()) - - // update animation - swtch := enemy.R.(*render.Switch) - if delta.X() > 0 { - if swtch.Get() == "left" { - swtch.Set("right") - } - } else if delta.X() < 0 { - if swtch.Get() == "right" { - swtch.Set("left") - } - } - return 0 - }) - - enemy.Bind("Destroy", func(id event.CID, _ interface{}) int { - enemy := event.GetEntity(id).(*entities.Solid) - enemy.Destroy() - return 0 - }) -} - -func enemyPos(ctx *scene.Context) (float64, float64) { - w := ctx.Window.Width() - h := ctx.Window.Height() - // Spawn on the edge of the screen - perimeter := w*2 + h*2 - pos := int(rand.Float64() * float64(perimeter)) - // Top - if pos < w { - return float64(pos), 0 - } - pos -= w - // Right - if pos < h { - return float64(w), float64(pos) - } - // Bottom - pos -= h - if pos < w { - return float64(pos), float64(h) - } - pos -= w - // Left - return 0, float64(pos) -} diff --git a/examples/top-down-shooter-tutorial/5-viewport/assets/images/16x16/sheet.png b/examples/top-down-shooter-tutorial/5-viewport/assets/images/16x16/sheet.png deleted file mode 100644 index fd1e7aa7..00000000 Binary files a/examples/top-down-shooter-tutorial/5-viewport/assets/images/16x16/sheet.png and /dev/null differ diff --git a/examples/top-down-shooter-tutorial/5-viewport/assets/images/character/eggplant-fish.png b/examples/top-down-shooter-tutorial/5-viewport/assets/images/character/eggplant-fish.png deleted file mode 100644 index ded61a2e..00000000 Binary files a/examples/top-down-shooter-tutorial/5-viewport/assets/images/character/eggplant-fish.png and /dev/null differ diff --git a/examples/top-down-shooter-tutorial/5-viewport/viewport.go b/examples/top-down-shooter-tutorial/5-viewport/viewport.go deleted file mode 100644 index f3ceab38..00000000 --- a/examples/top-down-shooter-tutorial/5-viewport/viewport.go +++ /dev/null @@ -1,254 +0,0 @@ -package main - -import ( - "fmt" - "image/color" - "math/rand" - "time" - - "github.com/oakmound/oak/v3/render/mod" - - oak "github.com/oakmound/oak/v3" - "github.com/oakmound/oak/v3/alg/floatgeom" - "github.com/oakmound/oak/v3/alg/intgeom" - "github.com/oakmound/oak/v3/collision" - "github.com/oakmound/oak/v3/collision/ray" - "github.com/oakmound/oak/v3/dlog" - "github.com/oakmound/oak/v3/entities" - "github.com/oakmound/oak/v3/event" - "github.com/oakmound/oak/v3/key" - "github.com/oakmound/oak/v3/mouse" - "github.com/oakmound/oak/v3/physics" - "github.com/oakmound/oak/v3/render" - "github.com/oakmound/oak/v3/scene" -) - -// Collision labels -const ( - Enemy collision.Label = 1 -) - -var ( - playerAlive = true - // Vectors are backed by pointers, - // so despite this not being a pointer, - // this does update according to the player's - // position so long as we don't reset - // the player's position vector - playerPos physics.Vector - - sheet [][]*render.Sprite -) - -const ( - fieldWidth = 1000 - fieldHeight = 1000 -) - -func main() { - - oak.AddScene("tds", scene.Scene{Start: func(ctx *scene.Context) { - // Initialization - playerAlive = true - sprites, err := render.GetSheet("sheet.png") - dlog.ErrorCheck(err) - sheet = sprites.ToSprites() - - oak.SetViewportBounds(intgeom.NewRect2(0, 0, fieldWidth, fieldHeight)) - - // Player setup - eggplant, err := render.GetSprite("eggplant-fish.png") - playerR := render.NewSwitch("left", map[string]render.Modifiable{ - "left": eggplant, - // We must copy the sprite before we modify it, or "left" - // will also be flipped. - "right": eggplant.Copy().Modify(mod.FlipX), - }) - if err != nil { - fmt.Println(err) - } - char := entities.NewMoving(100, 100, 32, 32, - playerR, - nil, 0, 0) - - char.Speed = physics.NewVector(5, 5) - playerPos = char.Point.Vector - render.Draw(char.R, 2) - - char.Bind(event.Enter, func(id event.CID, _ interface{}) int { - char := event.GetEntity(id).(*entities.Moving) - char.Delta.Zero() - if oak.IsDown(key.W) { - char.Delta.ShiftY(-char.Speed.Y()) - } - if oak.IsDown(key.A) { - char.Delta.ShiftX(-char.Speed.X()) - } - if oak.IsDown(key.S) { - char.Delta.ShiftY(char.Speed.Y()) - } - if oak.IsDown(key.D) { - char.Delta.ShiftX(char.Speed.X()) - } - char.ShiftPos(char.Delta.X(), char.Delta.Y()) - // Don't go out of bounds - if char.X() < 0 { - char.SetX(0) - } else if char.X() > fieldWidth-char.W { - char.SetX(fieldWidth - char.W) - } - if char.Y() < 0 { - char.SetY(0) - } else if char.Y() > fieldHeight-char.H { - char.SetY(fieldHeight - char.H) - } - oak.SetScreen( - int(char.R.X())-ctx.Window.Width()/2, - int(char.R.Y())-ctx.Window.Height()/2, - ) - hit := char.HitLabel(Enemy) - if hit != nil { - playerAlive = false - } - - // update animation - swtch := char.R.(*render.Switch) - if char.Delta.X() > 0 { - if swtch.Get() == "left" { - swtch.Set("right") - } - } else if char.Delta.X() < 0 { - if swtch.Get() == "right" { - swtch.Set("left") - } - } - - return 0 - }) - - char.Bind(mouse.Press, func(id event.CID, me interface{}) int { - char := event.GetEntity(id).(*entities.Moving) - mevent := me.(*mouse.Event) - x := char.X() + char.W/2 - y := char.Y() + char.H/2 - vp := ctx.Window.Viewport() - mx := mevent.X() + float64(vp.X()) - my := mevent.Y() + float64(vp.Y()) - ray.DefaultCaster.CastDistance = floatgeom.Point2{x, y}.Sub(floatgeom.Point2{mx, my}).Magnitude() - hits := ray.CastTo(floatgeom.Point2{x, y}, floatgeom.Point2{mx, my}) - for _, hit := range hits { - hit.Zone.CID.Trigger("Destroy", nil) - } - ctx.DrawForTime( - render.NewLine(x, y, mx, my, color.RGBA{0, 128, 0, 128}), - time.Millisecond*50, - 2) - return 0 - }) - - // Create enemies periodically - event.GlobalBind(event.Enter, func(_ event.CID, frames interface{}) int { - enterPayload := frames.(event.EnterPayload) - if enterPayload.FramesElapsed%EnemyRefresh == 0 { - go NewEnemy() - } - return 0 - }) - - // Draw the background - for x := 0; x < fieldWidth; x += 16 { - for y := 0; y < fieldHeight; y += 16 { - i := rand.Intn(3) + 1 - // Get a random tile to draw in this position - sp := sheet[i/2][i%2].Copy() - sp.SetPos(float64(x), float64(y)) - render.Draw(sp, 1) - } - } - - }, Loop: func() bool { - return playerAlive - }}) - - oak.Init("tds", func(c oak.Config) (oak.Config, error) { - c.BatchLoad = true - c.Assets.ImagePath = "assets/images" - - return c, nil - }) -} - -const ( - EnemyRefresh = 25 - EnemySpeed = 2 -) - -// NewEnemy creates an enemy for a top down shooter -func NewEnemy() { - x, y := enemyPos() - - enemyFrame := sheet[0][0].Copy() - enemyR := render.NewSwitch("left", map[string]render.Modifiable{ - "left": enemyFrame, - "right": enemyFrame.Copy().Modify(mod.FlipX), - }) - enemy := entities.NewSolid(x, y, 16, 16, - enemyR, - nil, 0) - - render.Draw(enemy.R, 2) - - enemy.UpdateLabel(Enemy) - - enemy.Bind(event.Enter, func(id event.CID, _ interface{}) int { - enemy := event.GetEntity(id).(*entities.Solid) - // move towards the player - x, y := enemy.GetPos() - pt := floatgeom.Point2{x, y} - pt2 := floatgeom.Point2{playerPos.X(), playerPos.Y()} - delta := pt2.Sub(pt).Normalize().MulConst(EnemySpeed) - enemy.ShiftPos(delta.X(), delta.Y()) - - // update animation - swtch := enemy.R.(*render.Switch) - if delta.X() > 0 { - if swtch.Get() == "left" { - swtch.Set("right") - } - } else if delta.X() < 0 { - if swtch.Get() == "right" { - swtch.Set("left") - } - } - return 0 - }) - - enemy.Bind("Destroy", func(id event.CID, _ interface{}) int { - enemy := event.GetEntity(id).(*entities.Solid) - enemy.Destroy() - return 0 - }) -} - -func enemyPos() (float64, float64) { - // Spawn on the edge of the screen - perimeter := fieldWidth*2 + fieldHeight*2 - pos := int(rand.Float64() * float64(perimeter)) - // Top - if pos < fieldWidth { - return float64(pos), 0 - } - pos -= fieldWidth - // Right - if pos < fieldHeight { - return float64(fieldWidth), float64(pos) - } - // Bottom - pos -= fieldHeight - if pos < fieldWidth { - return float64(pos), float64(fieldHeight) - } - pos -= fieldWidth - // Left - return 0, float64(pos) -} diff --git a/examples/top-down-shooter-tutorial/6-performance/assets/images/16x16/sheet.png b/examples/top-down-shooter-tutorial/6-performance/assets/images/16x16/sheet.png deleted file mode 100644 index fd1e7aa7..00000000 Binary files a/examples/top-down-shooter-tutorial/6-performance/assets/images/16x16/sheet.png and /dev/null differ diff --git a/examples/top-down-shooter-tutorial/6-performance/assets/images/character/eggplant-fish.png b/examples/top-down-shooter-tutorial/6-performance/assets/images/character/eggplant-fish.png deleted file mode 100644 index ded61a2e..00000000 Binary files a/examples/top-down-shooter-tutorial/6-performance/assets/images/character/eggplant-fish.png and /dev/null differ diff --git a/examples/top-down-shooter-tutorial/README.md b/examples/top-down-shooter-tutorial/README.md deleted file mode 100644 index ea84feac..00000000 --- a/examples/top-down-shooter-tutorial/README.md +++ /dev/null @@ -1,27 +0,0 @@ -# Simple Top Down Shooter - -## Start -Create a movable character. - - -## Shooting -Allow character to create short lived lines from the character to a location. - - -## Enemies -Create enemies that come towards the character and allow them to be destroyed by shots. - - -## Sprites -Add appropriate pictures to the game elements. - - -## Viewport -Give the character the ability to move the viewport and have a bigger play space. - - -## Performance -Clean it up! - - -![Together!](./6-performance/example.gif) \ No newline at end of file diff --git a/examples/top-down-shooter-tutorial/4-sprites/assets/images/16x16/sheet.png b/examples/top-down-shooter/assets/images/16x16/sheet.png similarity index 100% rename from examples/top-down-shooter-tutorial/4-sprites/assets/images/16x16/sheet.png rename to examples/top-down-shooter/assets/images/16x16/sheet.png diff --git a/examples/top-down-shooter-tutorial/4-sprites/assets/images/character/eggplant-fish.png b/examples/top-down-shooter/assets/images/character/eggplant-fish.png similarity index 100% rename from examples/top-down-shooter-tutorial/4-sprites/assets/images/character/eggplant-fish.png rename to examples/top-down-shooter/assets/images/character/eggplant-fish.png diff --git a/examples/top-down-shooter-tutorial/6-performance/example.gif b/examples/top-down-shooter/example.gif similarity index 100% rename from examples/top-down-shooter-tutorial/6-performance/example.gif rename to examples/top-down-shooter/example.gif diff --git a/examples/top-down-shooter-tutorial/6-performance/performance.go b/examples/top-down-shooter/main.go similarity index 56% rename from examples/top-down-shooter-tutorial/6-performance/performance.go rename to examples/top-down-shooter/main.go index 7cfaead4..17c0c915 100644 --- a/examples/top-down-shooter-tutorial/6-performance/performance.go +++ b/examples/top-down-shooter/main.go @@ -6,21 +6,20 @@ import ( "math/rand" "time" - "github.com/oakmound/oak/v3/render/mod" - - oak "github.com/oakmound/oak/v3" - "github.com/oakmound/oak/v3/alg/floatgeom" - "github.com/oakmound/oak/v3/alg/intgeom" - "github.com/oakmound/oak/v3/collision" - "github.com/oakmound/oak/v3/collision/ray" - "github.com/oakmound/oak/v3/dlog" - "github.com/oakmound/oak/v3/entities" - "github.com/oakmound/oak/v3/event" - "github.com/oakmound/oak/v3/key" - "github.com/oakmound/oak/v3/mouse" - "github.com/oakmound/oak/v3/physics" - "github.com/oakmound/oak/v3/render" - "github.com/oakmound/oak/v3/scene" + "github.com/oakmound/oak/v4/render/mod" + + oak "github.com/oakmound/oak/v4" + "github.com/oakmound/oak/v4/alg/floatgeom" + "github.com/oakmound/oak/v4/alg/intgeom" + "github.com/oakmound/oak/v4/collision" + "github.com/oakmound/oak/v4/collision/ray" + "github.com/oakmound/oak/v4/dlog" + "github.com/oakmound/oak/v4/entities" + "github.com/oakmound/oak/v4/event" + "github.com/oakmound/oak/v4/key" + "github.com/oakmound/oak/v4/mouse" + "github.com/oakmound/oak/v4/render" + "github.com/oakmound/oak/v4/scene" ) const ( @@ -28,13 +27,10 @@ const ( ) var ( - playerAlive = true - // Vectors are backed by pointers, - // so despite this not being a pointer, - // this does update according to the player's - // position so long as we don't reset - // the player's position vector - playerPos physics.Vector + playerX *float64 + playerY *float64 + + destroy = event.RegisterEvent[struct{}]() sheet [][]*render.Sprite ) @@ -52,7 +48,6 @@ func main() { // render.Draw(debugtools.NewThickRTree(ctx, collision.DefaultTree, 5), 2, 3) // Initialization - playerAlive = true sprites, err := render.GetSheet("sheet.png") dlog.ErrorCheck(err) sheet = sprites.ToSprites() @@ -68,62 +63,57 @@ func main() { // will also be flipped. "right": eggplant.Copy().Modify(mod.FlipX), }) - char := entities.NewMoving(100, 100, 32, 32, - playerR, - nil, 0, 0) - - char.Speed = physics.NewVector(3, 3) - playerPos = char.Point.Vector - render.Draw(char.R, 1, 2) + char := entities.New(ctx, + entities.WithRect(floatgeom.NewRect2WH(100, 100, 32, 32)), + entities.WithRenderable(playerR), + entities.WithSpeed(floatgeom.Point2{3, 3}), + entities.WithDrawLayers([]int{1, 2}), + ) - screenCenter := floatgeom.Point2{ - float64(ctx.Window.Width()) / 2, - float64(ctx.Window.Height()) / 2, - } + playerX = &char.Rect.Min[0] + playerY = &char.Rect.Min[1] - char.Bind(event.Enter, func(id event.CID, payload interface{}) int { - char := event.GetEntity(id).(*entities.Moving) + screenCenter := ctx.Window.Bounds().DivConst(2) - enterPayload := payload.(event.EnterPayload) + event.Bind(ctx, event.Enter, char, func(char *entities.Entity, ev event.EnterPayload) event.Response { if oak.IsDown(key.W) { - char.Delta.ShiftY(-char.Speed.Y() * enterPayload.TickPercent) + char.Delta[1] += (-char.Speed.Y() * ev.TickPercent) } if oak.IsDown(key.A) { - char.Delta.ShiftX(-char.Speed.X() * enterPayload.TickPercent) + char.Delta[0] += (-char.Speed.X() * ev.TickPercent) } if oak.IsDown(key.S) { - char.Delta.ShiftY(char.Speed.Y() * enterPayload.TickPercent) + char.Delta[1] += (char.Speed.Y() * ev.TickPercent) } if oak.IsDown(key.D) { - char.Delta.ShiftX(char.Speed.X() * enterPayload.TickPercent) + char.Delta[0] += (char.Speed.X() * ev.TickPercent) } ctx.Window.(*oak.Window).DoBetweenDraws(func() { - char.ShiftPos(char.Delta.X(), char.Delta.Y()) - oak.SetScreen( - int(char.R.X()-screenCenter.X()), - int(char.R.Y()-screenCenter.Y()), + char.ShiftDelta() + oak.SetViewport( + intgeom.Point2{int(char.X()), int(char.Y())}.Sub(screenCenter), ) - char.Delta.Zero() + char.Delta = floatgeom.Point2{} }) // Don't go out of bounds if char.X() < 0 { char.SetX(0) - } else if char.X() > fieldWidth-char.W { - char.SetX(fieldWidth - char.W) + } else if char.X() > fieldWidth-char.W() { + char.SetX(fieldWidth - char.W()) } if char.Y() < 0 { char.SetY(0) - } else if char.Y() > fieldHeight-char.H { - char.SetY(fieldHeight - char.H) + } else if char.Y() > fieldHeight-char.H() { + char.SetY(fieldHeight - char.H()) } hit := char.HitLabel(Enemy) if hit != nil { - playerAlive = false + ctx.Window.NextScene() } // update animation - swtch := char.R.(*render.Switch) + swtch := char.Renderable.(*render.Switch) if char.Delta.X() > 0 { if swtch.Get() == "left" { swtch.Set("right") @@ -137,18 +127,16 @@ func main() { return 0 }) - char.Bind(mouse.Press, func(id event.CID, me interface{}) int { - char := event.GetEntity(id).(*entities.Moving) - mevent := me.(*mouse.Event) - x := char.X() + char.W/2 - y := char.Y() + char.H/2 + event.Bind(ctx, mouse.Press, char, func(char *entities.Entity, mevent *mouse.Event) event.Response { + x := char.X() + char.W()/2 + y := char.Y() + char.H()/2 vp := ctx.Window.Viewport() mx := mevent.X() + float64(vp.X()) my := mevent.Y() + float64(vp.Y()) ray.DefaultCaster.CastDistance = floatgeom.Point2{x, y}.Sub(floatgeom.Point2{mx, my}).Magnitude() hits := ray.CastTo(floatgeom.Point2{x, y}, floatgeom.Point2{mx, my}) for _, hit := range hits { - hit.Zone.CID.Trigger("Destroy", nil) + event.TriggerForCallerOn(ctx, hit.Zone.CID, destroy, struct{}{}) } ctx.DrawForTime( render.NewLine(x, y, mx, my, color.RGBA{0, 128, 0, 128}), @@ -158,10 +146,9 @@ func main() { }) // Create enemies periodically - event.GlobalBind(event.Enter, func(_ event.CID, frames interface{}) int { - enterPayload := frames.(event.EnterPayload) + event.GlobalBind(ctx, event.Enter, func(enterPayload event.EnterPayload) event.Response { if enterPayload.FramesElapsed%EnemyRefresh == 0 { - go NewEnemy() + go NewEnemy(ctx) } return 0 }) @@ -177,8 +164,6 @@ func main() { } } - }, Loop: func() bool { - return playerAlive }}) render.SetDrawStack( @@ -206,7 +191,7 @@ const ( ) // NewEnemy creates an enemy for a top down shooter -func NewEnemy() { +func NewEnemy(ctx *scene.Context) { x, y := enemyPos() enemyFrame := sheet[0][0].Copy() @@ -214,26 +199,23 @@ func NewEnemy() { "left": enemyFrame, "right": enemyFrame.Copy().Modify(mod.FlipX), }) - enemy := entities.NewSolid(x, y, 16, 16, - enemyR, - nil, 0) - - render.Draw(enemy.R, 1, 2) - - enemy.UpdateLabel(Enemy) + enemy := entities.New(ctx, + entities.WithRect(floatgeom.NewRect2WH(x, y, 16, 16)), + entities.WithRenderable(enemyR), + entities.WithDrawLayers([]int{1, 2}), + entities.WithLabel(Enemy), + ) - enemy.Bind(event.Enter, func(id event.CID, payload interface{}) int { - enemy := event.GetEntity(id).(*entities.Solid) - enterPayload := payload.(event.EnterPayload) + event.Bind(ctx, event.Enter, enemy, func(e *entities.Entity, ev event.EnterPayload) event.Response { // move towards the player - x, y := enemy.GetPos() + x, y := enemy.X(), enemy.Y() pt := floatgeom.Point2{x, y} - pt2 := floatgeom.Point2{playerPos.X(), playerPos.Y()} - delta := pt2.Sub(pt).Normalize().MulConst(EnemySpeed * enterPayload.TickPercent) - enemy.ShiftPos(delta.X(), delta.Y()) + pt2 := floatgeom.Point2{*playerX, *playerY} + delta := pt2.Sub(pt).Normalize().MulConst(EnemySpeed * ev.TickPercent) + enemy.Shift(delta) // update animation - swtch := enemy.R.(*render.Switch) + swtch := enemy.Renderable.(*render.Switch) if delta.X() > 0 { if swtch.Get() == "left" { swtch.Set("right") @@ -246,9 +228,8 @@ func NewEnemy() { return 0 }) - enemy.Bind("Destroy", func(id event.CID, _ interface{}) int { - enemy := event.GetEntity(id).(*entities.Solid) - enemy.Destroy() + event.Bind(ctx, destroy, enemy, func(e *entities.Entity, nothing struct{}) event.Response { + e.Destroy() return 0 }) } diff --git a/examples/zooming/assets/mona-lisa.jpg b/examples/zooming/assets/mona-lisa.jpg deleted file mode 100644 index a68ca830..00000000 Binary files a/examples/zooming/assets/mona-lisa.jpg and /dev/null differ diff --git a/examples/zooming/main.go b/examples/zooming/main.go deleted file mode 100644 index c48ee22d..00000000 --- a/examples/zooming/main.go +++ /dev/null @@ -1,86 +0,0 @@ -package main - -import ( - "embed" - "image/color" - "image/draw" - "path/filepath" - - oak "github.com/oakmound/oak/v3" - "github.com/oakmound/oak/v3/dlog" - "github.com/oakmound/oak/v3/event" - "github.com/oakmound/oak/v3/key" - "github.com/oakmound/oak/v3/render" - "github.com/oakmound/oak/v3/scene" -) - -var ( - zoomOutFactorX = 1.0 - zoomOutFactorY = 1.0 -) - -func main() { - oak.AddScene("demo", scene.Scene{Start: func(*scene.Context) { - render.Draw(render.NewText("Controls: Arrow keys", 500, 440)) - - // Get an image that we will illustrate zooming with later - s, err := render.LoadSprite(filepath.Join("assets", "mona-lisa.jpg")) - dlog.ErrorCheck(err) - - // See the zoomR definition lower, wrap your renderable with a definition of how to zoom. - zoomer := &zoomR{ - Renderable: s, - SetFn: func(buff draw.Image, x, y int, c color.Color) { - x = int(float64(x) * zoomOutFactorX) - y = int(float64(y) * zoomOutFactorY) - buff.Set(x, y, c) - }, - } - render.Draw(zoomer) - - // To illustrate zooming allow for arrow keys to control the main zoomable renderable. - event.GlobalBind(event.Enter, func(i event.CID, _ interface{}) int { - if oak.IsDown(key.UpArrow) { - zoomOutFactorY *= .98 - } - if oak.IsDown(key.DownArrow) { - zoomOutFactorY *= 1.02 - } - if oak.IsDown(key.RightArrow) { - zoomOutFactorX *= 1.02 - } - if oak.IsDown(key.LeftArrow) { - zoomOutFactorX *= .98 - } - - return 0 - }) - - }}) - oak.SetFS(assets) - oak.Init("demo") -} - -//go:embed assets -var assets embed.FS - -// zoomR wraps a renderable with a function that details -type zoomR struct { - render.Renderable - SetFn func(buff draw.Image, x, y int, c color.Color) -} - -// Draw to draw the zoomR by creating a customImage and applying the set funcitonality. -func (z *zoomR) Draw(buff draw.Image, xOff, yOff float64) { - img := &customImage{buff, z.SetFn} - z.Renderable.Draw(img, xOff, yOff) -} - -type customImage struct { - draw.Image - SetFn func(buff draw.Image, x, y int, c color.Color) -} - -func (c *customImage) Set(x, y int, col color.Color) { - c.SetFn(c.Image, x, y, col) -} diff --git a/fileutil/open.go b/fileutil/open.go index 02140f32..7a155271 100644 --- a/fileutil/open.go +++ b/fileutil/open.go @@ -1,7 +1,6 @@ package fileutil import ( - "bytes" "io" "io/fs" "os" @@ -29,11 +28,11 @@ func Open(file string) (io.ReadCloser, error) { fixedPath := fixWindowsPath(file) f, readErr := FS.Open(fixedPath) if readErr != nil && OSFallback { - byt, err := os.ReadFile(file) + osFile, err := os.Open(file) if err != nil { return nil, err } - return io.NopCloser(bytes.NewReader(byt)), nil + return osFile, nil } return f, readErr } diff --git a/fileutil/open_test.go b/fileutil/open_test.go index 5472a371..c1e48974 100644 --- a/fileutil/open_test.go +++ b/fileutil/open_test.go @@ -2,7 +2,9 @@ package fileutil import ( "embed" + "errors" "io" + "os" "testing" ) @@ -10,36 +12,86 @@ import ( var testfs embed.FS func TestOpen(t *testing.T) { - FS = testfs - f, err := Open("testdata/test.txt") - if err != nil { - t.Fatalf("open failed: %v", err) - } - _, err = io.ReadAll(f) - if err != nil { - t.Fatalf("read all failed: %v", err) - } - err = f.Close() - if err != nil { - t.Fatalf("close failed: %v", err) - } + t.Run("Basic", func(t *testing.T) { + FS = testfs + f, err := Open("testdata/test.txt") + if err != nil { + t.Fatalf("open failed: %v", err) + } + _, err = io.ReadAll(f) + if err != nil { + t.Fatalf("read all failed: %v", err) + } + err = f.Close() + if err != nil { + t.Fatalf("close failed: %v", err) + } + }) + t.Run("NotFound", func(t *testing.T) { + FS = testfs + _, err := Open("testdata/notfound.txt") + perr := &os.PathError{} + if !errors.As(err, &perr) { + t.Fatalf("expected path error: %v", err) + } + }) + t.Run("OSFallback", func(t *testing.T) { + FS = testfs + f, err := os.CreateTemp(".", "test") + if err != nil { + t.Fatalf("failed to create temp file: %v", err) + } + defer os.Remove(f.Name()) + f.Close() + f2, err := Open(f.Name()) + if err != nil { + t.Fatalf("open failed: %v", err) + } + f2.Close() + }) } func TestReadFile(t *testing.T) { - FS = testfs - _, err := ReadFile("testdata/test.txt") - if err != nil { - t.Fatalf("read all failed: %v", err) - } + t.Run("Basic", func(t *testing.T) { + FS = testfs + _, err := ReadFile("testdata/test.txt") + if err != nil { + t.Fatalf("read all failed: %v", err) + } + }) + t.Run("NotFound", func(t *testing.T) { + FS = testfs + _, err := ReadFile("testdata/notfound.txt") + perr := &os.PathError{} + if !errors.As(err, &perr) { + t.Fatalf("expected path error: %v", err) + } + }) } func TestReadDir(t *testing.T) { - FS = testfs - ds, err := ReadDir("testdata") - if err != nil { - t.Fatalf("read dir failed: %v", err) - } - if len(ds) != 1 { - t.Fatalf("read dir had %v elements, expected 1", len(ds)) - } + t.Run("Basic", func(t *testing.T) { + FS = testfs + ds, err := ReadDir("testdata") + if err != nil { + t.Fatalf("read dir failed: %v", err) + } + if len(ds) != 1 { + t.Fatalf("read dir had %v elements, expected 1", len(ds)) + } + }) + t.Run("NoWindowsPaths", func(t *testing.T) { + FixWindowsPaths = false + defer func() { + FixWindowsPaths = true + }() + FS = testfs + ds, err := ReadDir("testdata") + if err != nil { + t.Fatalf("read dir failed: %v", err) + } + if len(ds) != 1 { + t.Fatalf("read dir had %v elements, expected 1", len(ds)) + } + }) } diff --git a/go.mod b/go.mod index 535122e7..710d2aac 100644 --- a/go.mod +++ b/go.mod @@ -1,24 +1,29 @@ -module github.com/oakmound/oak/v3 +module github.com/oakmound/oak/v4 -go 1.16 +go 1.18 require ( - dmitri.shuralyov.com/gpu/mtl v0.0.0-20201218220906-28db891af037 - github.com/BurntSushi/xgb v0.0.0-20210121224620-deaf085860bc - github.com/BurntSushi/xgbutil v0.0.0-20190907113008-ad855c713046 - github.com/disintegration/gift v1.2.0 - github.com/eaburns/bit v0.0.0-20131029213740-7bd5cd37375d // indirect + dmitri.shuralyov.com/gpu/mtl v0.0.0-20201218220906-28db891af037 // osx, shiny + github.com/BurntSushi/xgb v0.0.0-20210121224620-deaf085860bc // linux, shiny + github.com/BurntSushi/xgbutil v0.0.0-20190907113008-ad855c713046 // linux, shiny + github.com/disintegration/gift v1.2.1 // render github.com/eaburns/flac v0.0.0-20171003200620-9a6fb92396d1 - github.com/go-gl/glfw/v3.3/glfw v0.0.0-20210410170116-ea3d685f79fb + github.com/go-gl/glfw/v3.3/glfw v0.0.0-20220320163800-277f93cfa958 // osx, shiny github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 - github.com/hajimehoshi/go-mp3 v0.3.1 - github.com/jfreymuth/pulse v0.1.0 - github.com/oakmound/alsa v0.0.2 - github.com/oakmound/libudev v0.2.1 - github.com/oakmound/w32 v2.1.0+incompatible - github.com/oov/directsound-go v0.0.0-20141101201356-e53e59c700bf - golang.org/x/image v0.0.0-20201208152932-35266b937fa6 - golang.org/x/mobile v0.0.0-20220112015953-858099ff7816 + github.com/hajimehoshi/go-mp3 v0.3.2 + github.com/jfreymuth/pulse v0.1.0 // linux, audio + github.com/oakmound/alsa v0.0.2 // linux, audio + github.com/oakmound/libudev v0.2.1 // linux, joystick + github.com/oakmound/w32 v2.1.0+incompatible // windows, shiny + github.com/oov/directsound-go v0.0.0-20141101201356-e53e59c700bf // windows, audio + golang.org/x/exp v0.0.0-20220414153411-bcd21879b8fd + golang.org/x/image v0.0.0-20220321031419-a8550c1d254a + golang.org/x/mobile v0.0.0-20220325161704-447654d348e3 golang.org/x/sync v0.0.0-20210220032951-036812b2e83c - golang.org/x/sys v0.0.0-20220111092808-5a964db01320 + golang.org/x/sys v0.0.0-20220403205710-6acee93ad0eb +) + +require ( + github.com/eaburns/bit v0.0.0-20131029213740-7bd5cd37375d // indirect + golang.org/x/exp/shiny v0.0.0-20220518171630-0b5c67f07fdf // indirect ) diff --git a/go.sum b/go.sum index f8cf8ae7..42f641ca 100644 --- a/go.sum +++ b/go.sum @@ -5,18 +5,20 @@ github.com/BurntSushi/xgb v0.0.0-20210121224620-deaf085860bc h1:7D+Bh06CRPCJO3gr github.com/BurntSushi/xgb v0.0.0-20210121224620-deaf085860bc/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/BurntSushi/xgbutil v0.0.0-20190907113008-ad855c713046 h1:O/r2Sj+8QcMF7V5IcmiE2sMFV2q3J47BEirxbXJAdzA= github.com/BurntSushi/xgbutil v0.0.0-20190907113008-ad855c713046/go.mod h1:uw9h2sd4WWHOPdJ13MQpwK5qYWKYDumDqxWWIknEQ+k= -github.com/disintegration/gift v1.2.0 h1:VMQeei2F+ZtsHjMgP6Sdt1kFjRhs2lGz8ljEOPeIR50= -github.com/disintegration/gift v1.2.0/go.mod h1:Jh2i7f7Q2BM7Ezno3PhfezbR1xpUg9dUg3/RlKGr4HI= +github.com/disintegration/gift v1.2.1 h1:Y005a1X4Z7Uc+0gLpSAsKhWi4qLtsdEcMIbbdvdZ6pc= +github.com/disintegration/gift v1.2.1/go.mod h1:Jh2i7f7Q2BM7Ezno3PhfezbR1xpUg9dUg3/RlKGr4HI= github.com/eaburns/bit v0.0.0-20131029213740-7bd5cd37375d h1:HB5J9+f1xpkYLgWQ/RqEcbp3SEufyOIMYLoyKNKiG7E= github.com/eaburns/bit v0.0.0-20131029213740-7bd5cd37375d/go.mod h1:CHkHWWZ4kbGY6jEy1+qlitDaCtRgNvCOQdakj/1Yl/Q= github.com/eaburns/flac v0.0.0-20171003200620-9a6fb92396d1 h1:wl/ggSfTHqAy46hyzw1IlrUYwjqmXYvbJyPdH3rT7YE= github.com/eaburns/flac v0.0.0-20171003200620-9a6fb92396d1/go.mod h1:frG94byMNy+1CgGrQ25dZ+17tf98EN+OYBQL4Zh612M= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20210410170116-ea3d685f79fb h1:T6gaWBvRzJjuOrdCtg8fXXjKai2xSDqWTcKFUPuw8Tw= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20210410170116-ea3d685f79fb/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20220320163800-277f93cfa958 h1:TL70PMkdPCt9cRhKTqsm+giRpgrd0IGEj763nNr2VFY= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20220320163800-277f93cfa958/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g= +github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g= +github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= -github.com/hajimehoshi/go-mp3 v0.3.1 h1:pn/SKU1+/rfK8KaZXdGEC2G/KCB2aLRjbTCrwKcokao= -github.com/hajimehoshi/go-mp3 v0.3.1/go.mod h1:qMJj/CSDxx6CGHiZeCgbiq2DSUkbK0UbtXShQcnfyMM= +github.com/hajimehoshi/go-mp3 v0.3.2 h1:xSYNE2F3lxtOu9BRjCWHHceg7S91IHfXfXp5+LYQI7s= +github.com/hajimehoshi/go-mp3 v0.3.2/go.mod h1:qMJj/CSDxx6CGHiZeCgbiq2DSUkbK0UbtXShQcnfyMM= github.com/hajimehoshi/oto v0.6.1/go.mod h1:0QXGEkbuJRohbJaxr7ZQSxnju7hEhseiPx2hrh6raOI= github.com/jfreymuth/pulse v0.1.0 h1:KN38/9hoF9PJvP5DpEVhMRKNuwnJUonc8c9ARorRXUA= github.com/jfreymuth/pulse v0.1.0/go.mod h1:cpYspI6YljhkUf1WLXLLDmeaaPFc3CnGLjDZf9dZ4no= @@ -33,16 +35,19 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56 h1:estk1glOnSVeJ9tdEZZc5mAMDZk5lNJNyJ6DvrBkTEU= golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56/go.mod h1:JhuoJpWY28nO4Vef9tZUw9qufEGTyX1+7lmHxV5q5G4= +golang.org/x/exp v0.0.0-20220414153411-bcd21879b8fd h1:zVFyTKZN/Q7mNRWSs1GOYnHM9NiFSJ54YVRsD0rNWT4= +golang.org/x/exp v0.0.0-20220414153411-bcd21879b8fd/go.mod h1:lgLbSvA5ygNOMpwM/9anMpWVlVJ7Z+cHWq/eFuinpGE= +golang.org/x/exp/shiny v0.0.0-20220518171630-0b5c67f07fdf h1:IbXVp9Gov7/6bw4sWq9M/u1cr+mr2NA8SJWvFCnu4is= +golang.org/x/exp/shiny v0.0.0-20220518171630-0b5c67f07fdf/go.mod h1:VjAR7z0ngyATZTELrBSkxOOHhhlnVUxDye4mcjx5h/8= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/image v0.0.0-20201208152932-35266b937fa6 h1:nfeHNc1nAqecKCy2FCy4HY+soOOe5sDLJ/gZLbx6GYI= -golang.org/x/image v0.0.0-20201208152932-35266b937fa6/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.0.0-20220321031419-a8550c1d254a h1:LnH9RNcpPv5Kzi15lXg42lYMPUf0x8CuPv1YnvBWZAg= +golang.org/x/image v0.0.0-20220321031419-a8550c1d254a/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM= golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= golang.org/x/mobile v0.0.0-20190415191353-3e0bab5405d6/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= -golang.org/x/mobile v0.0.0-20220112015953-858099ff7816 h1:jhDgkcu3yQ4tasBZ+1YwDmK7eFmuVf1w1k+NGGGxfmE= -golang.org/x/mobile v0.0.0-20220112015953-858099ff7816/go.mod h1:pe2sM7Uk+2Su1y7u/6Z8KJ24D7lepUjFZbhFOrmDfuQ= +golang.org/x/mobile v0.0.0-20220325161704-447654d348e3 h1:ZDL7hDvJEQEcHVkoZawKmRUgbqn1pOIzb8EinBh5csU= +golang.org/x/mobile v0.0.0-20220325161704-447654d348e3/go.mod h1:pe2sM7Uk+2Su1y7u/6Z8KJ24D7lepUjFZbhFOrmDfuQ= golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -59,8 +64,8 @@ golang.org/x/sys v0.0.0-20190429190828-d89cdac9e872/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220111092808-5a964db01320 h1:0jf+tOCoZ3LyutmCOWpVni1chK4VfFLhRsDK7MhqGRY= -golang.org/x/sys v0.0.0-20220111092808-5a964db01320/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220403205710-6acee93ad0eb h1:PVGECzEo9Y3uOidtkHGdd347NjLtITfJFO9BxFpmRoo= +golang.org/x/sys v0.0.0-20220403205710-6acee93ad0eb/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= diff --git a/init.go b/init.go index f96ac657..f9a09dd1 100644 --- a/init.go +++ b/init.go @@ -8,9 +8,10 @@ import ( "strings" "time" - "github.com/oakmound/oak/v3/dlog" - "github.com/oakmound/oak/v3/oakerr" - "github.com/oakmound/oak/v3/timing" + "github.com/oakmound/oak/v4/dlog" + "github.com/oakmound/oak/v4/oakerr" + "github.com/oakmound/oak/v4/scene" + "github.com/oakmound/oak/v4/timing" ) var ( @@ -18,8 +19,11 @@ var ( ) // Init initializes the oak engine. -// It spawns off an event loop of several goroutines -// and loops through scenes after initialization. +// After the configuration options have been parsed and validated, this will run concurrent +// routines drawing to an OS window or app, forwarding OS inputs to this window's configured +// event handler, and running scenes: first the predefined 'loading' scene, then firstScene +// as provided here, then scenes following commands sent to the window or returned by ending +// scenes. func (w *Window) Init(firstScene string, configOptions ...ConfigOption) error { var err error @@ -28,13 +32,6 @@ func (w *Window) Init(firstScene string, configOptions ...ConfigOption) error { return fmt.Errorf("failed to create config: %w", err) } - // if c.config.Screen.TargetWidth != 0 && c.config.Screen.TargetHeight != 0 { - // w, h := driver.MonitorSize() - // if w != 0 || h != 0 { - // // Todo: Modify conf.Screen.Scale - // } - // } - lvl, err := dlog.ParseDebugLevel(w.config.Debug.Level) if err != nil { return fmt.Errorf("failed to parse debug config: %w", err) @@ -42,10 +39,8 @@ func (w *Window) Init(firstScene string, configOptions ...ConfigOption) error { dlog.SetFilter(func(msg string) bool { return strings.Contains(msg, w.config.Debug.Filter) }) - err = dlog.SetLogLevel(lvl) - if err != nil { - return err - } + // This error cannot happen as it would surface in Parse above + _ = dlog.SetLogLevel(lvl) err = oakerr.SetLanguageString(w.config.Language) if err != nil { return err @@ -65,9 +60,6 @@ func (w *Window) Init(firstScene string, configOptions ...ConfigOption) error { if w.config.TrackInputChanges { trackJoystickChanges(w.eventHandler) } - if w.config.EventRefreshRate != 0 { - w.eventHandler.SetRefreshRate(time.Duration(w.config.EventRefreshRate)) - } if !w.config.SkipRNGSeed { // seed math/rand with time.Now, useful for minimal examples @@ -77,14 +69,27 @@ func (w *Window) Init(firstScene string, configOptions ...ConfigOption) error { overrideInit(w) - go w.sceneLoop(firstScene, w.config.TrackInputChanges) - if w.config.BatchLoad { - w.startupLoading = true - go func() { - w.loadAssets(w.config.Assets.ImagePath, w.config.Assets.AudioPath) - w.endLoad() - }() + err = w.SceneMap.AddScene(oakLoadingScene, scene.Scene{ + Start: func(ctx *scene.Context) { + if w.config.BatchLoad { + go func() { + w.loadAssets(w.config.Assets.ImagePath, w.config.Assets.AudioPath) + w.endLoad() + }() + } else { + go w.endLoad() + } + }, + End: func() (string, *scene.Result) { + return w.firstScene, &scene.Result{ + NextSceneInput: w.FirstSceneInput, + } + }, + }) + if err != nil { + return err } + go w.sceneLoop(firstScene, w.config.TrackInputChanges) if w.config.EnableDebugConsole { go w.debugConsole(os.Stdin, os.Stdout) } diff --git a/init_override_js.go b/init_override_js.go index 1df10f17..3bfae168 100644 --- a/init_override_js.go +++ b/init_override_js.go @@ -4,7 +4,7 @@ package oak import ( - "github.com/oakmound/oak/v3/dlog" + "github.com/oakmound/oak/v4/dlog" "syscall/js" ) @@ -15,11 +15,11 @@ func overrideInit(w *Window) { } if w.config.EnableDebugConsole { dlog.Info("Debug console is not supported in JS") - w.config.EnableDebugConsole = false + w.config.EnableDebugConsole = false } if w.config.UnlimitedDrawFrameRate { dlog.Info("Unlimited draw frame rate is not supported in JS") - w.config.UnlimitedDrawFrameRate = false + w.config.UnlimitedDrawFrameRate = false } w.animationFrame = make(chan struct{}) js.Global().Call("requestAnimationFrame", js.FuncOf(w.requestFrame)) diff --git a/init_test.go b/init_test.go new file mode 100644 index 00000000..48135ded --- /dev/null +++ b/init_test.go @@ -0,0 +1,46 @@ +package oak + +import ( + "fmt" + "testing" +) + +func TestInitFailures(t *testing.T) { + t.Run("BadConfig", func(t *testing.T) { + c1 := NewWindow() + err := c1.Init("", func(c Config) (Config, error) { + return c, fmt.Errorf("whoops") + }) + if err == nil { + t.Fatal("expected error to cascade down from init") + } + }) + t.Run("ParseDebugLevel", func(t *testing.T) { + c1 := NewWindow() + err := c1.Init("", func(c Config) (Config, error) { + c.Debug.Level = "bogus" + return c, nil + }) + if err == nil { + t.Fatal("expected error parsing debug level") + } + }) + t.Run("SetLanguageString", func(t *testing.T) { + c1 := NewWindow() + err := c1.Init("", func(c Config) (Config, error) { + c.Language = "bogus" + return c, nil + }) + if err == nil { + t.Fatal("expected error parsing language string") + } + }) +} + +func TestInitDebugConsole(t *testing.T) { + c1 := NewWindow() + c1.Init("bad", func(c Config) (Config, error) { + c.EnableDebugConsole = true + return c, nil + }) +} diff --git a/inputLoop.go b/inputLoop.go index 1f7e814b..6137c188 100644 --- a/inputLoop.go +++ b/inputLoop.go @@ -1,47 +1,62 @@ package oak import ( - "github.com/oakmound/oak/v3/event" - "github.com/oakmound/oak/v3/timing" + "github.com/oakmound/oak/v4/alg/intgeom" + "github.com/oakmound/oak/v4/event" + "github.com/oakmound/oak/v4/timing" - "github.com/oakmound/oak/v3/dlog" - okey "github.com/oakmound/oak/v3/key" - omouse "github.com/oakmound/oak/v3/mouse" + "github.com/oakmound/oak/v4/dlog" + okey "github.com/oakmound/oak/v4/key" + omouse "github.com/oakmound/oak/v4/mouse" "golang.org/x/mobile/event/key" "golang.org/x/mobile/event/lifecycle" "golang.org/x/mobile/event/mouse" "golang.org/x/mobile/event/size" ) +// The following block defines events generated by oak during scene execution +var ( + // ViewportUpdate is triggered when the position of of the viewport changes + ViewportUpdate = event.RegisterEvent[intgeom.Point2]() + // OnStop is triggered when the engine is stopped, e.g. when a window's close + // button is clicked. + OnStop = event.RegisterEvent[struct{}]() + // FocusGain is triggered when a window gains focus + FocusGain = event.RegisterEvent[struct{}]() + // FocusLoss is triggered when a window loses focus + FocusLoss = event.RegisterEvent[struct{}]() + // InputChange is triggered when the most recent input device changes (e.g. keyboard to joystick or vice versa). It + // is only sent if Config.TrackInputChanges is true when Init is called. + InputChange = event.RegisterEvent[InputType]() +) + func (w *Window) inputLoop() { for { - switch e := w.windowControl.NextEvent().(type) { + switch e := w.Window.NextEvent().(type) { // We only currently respond to death lifecycle events. case lifecycle.Event: switch e.To { case lifecycle.StageDead: dlog.Info(dlog.WindowClosed) - // OnStop needs to be sent through TriggerBack, otherwise the - // program will close before the stop events get propagated. - <-w.eventHandler.TriggerBack(event.OnStop, nil) + <-event.TriggerOn(w.eventHandler, OnStop, struct{}{}) close(w.quitCh) return case lifecycle.StageFocused: w.inFocus = true // If you are in focused state, we don't care how you got there w.DrawTicker.Reset(timing.FPSToFrameDelay(w.DrawFrameRate)) - w.eventHandler.Trigger(event.FocusGain, nil) + event.TriggerOn(w.eventHandler, FocusGain, struct{}{}) case lifecycle.StageVisible: // If the last state was focused, this means the app is out of focus // otherwise, we're visible for the first time if e.From > e.To { w.inFocus = false w.DrawTicker.Reset(timing.FPSToFrameDelay(w.IdleDrawFrameRate)) - w.eventHandler.Trigger(event.FocusLoss, nil) + event.TriggerOn(w.eventHandler, FocusLoss, struct{}{}) } else { w.inFocus = true w.DrawTicker.Reset(timing.FPSToFrameDelay(w.DrawFrameRate)) - w.eventHandler.Trigger(event.FocusGain, nil) + event.TriggerOn(w.eventHandler, FocusGain, struct{}{}) } } // Send key events @@ -76,7 +91,7 @@ func (w *Window) inputLoop() { // Mouse events all receive an x, y, and button string. case mouse.Event: button := omouse.Button(e.Button) - eventName := omouse.GetEventName(e.Direction, e.Button) + ev := omouse.GetEvent(e.Direction, e.Button) // The event triggered for mouse events has the same scaling as the // render and collision space. I.e. if the viewport is at 0, the mouse's // position is exactly the same as the position of a visible entity @@ -85,7 +100,7 @@ func (w *Window) inputLoop() { float64((((e.X - float32(w.windowRect.Min.X)) / float32(w.windowRect.Max.X-w.windowRect.Min.X)) * float32(w.ScreenWidth))), float64((((e.Y - float32(w.windowRect.Min.Y)) / float32(w.windowRect.Max.Y-w.windowRect.Min.Y)) * float32(w.ScreenHeight))), button, - eventName, + ev, ) w.TriggerMouseEvent(mevent) @@ -102,10 +117,9 @@ func (w *Window) inputLoop() { // From the perspective of the event handler this is indistinguishable // from a real keypress. func (w *Window) TriggerKeyDown(e okey.Event) { - k := e.Code.String()[4:] - w.SetDown(k) - w.eventHandler.Trigger(okey.Down, e) - w.eventHandler.Trigger(okey.Down+k, e) + w.State.SetDown(e.Code) + event.TriggerOn(w.eventHandler, okey.AnyDown, e) + event.TriggerOn(w.eventHandler, okey.Down(e.Code), e) } // TriggerKeyUp triggers a software-emulated key release. @@ -113,10 +127,9 @@ func (w *Window) TriggerKeyDown(e okey.Event) { // From the perspective of the event handler this is indistinguishable // from a real key release. func (w *Window) TriggerKeyUp(e okey.Event) { - k := e.Code.String()[4:] - w.SetUp(k) - w.eventHandler.Trigger(okey.Up, e) - w.eventHandler.Trigger(okey.Up+k, e) + w.State.SetUp(e.Code) + event.TriggerOn(w.eventHandler, okey.AnyUp, e) + event.TriggerOn(w.eventHandler, okey.Up(e.Code), e) } // TriggerKeyHeld triggers a software-emulated key hold signal. @@ -124,9 +137,8 @@ func (w *Window) TriggerKeyUp(e okey.Event) { // From the perspective of the event handler this is indistinguishable // from a real key hold signal. func (w *Window) TriggerKeyHeld(e okey.Event) { - k := e.Code.String()[4:] - w.eventHandler.Trigger(okey.Held, e) - w.eventHandler.Trigger(okey.Held+k, e) + event.TriggerOn(w.eventHandler, okey.AnyHeld, e) + event.TriggerOn(w.eventHandler, okey.Held(e.Code), e) } // TriggerMouseEvent triggers a software-emulated mouse event. @@ -136,12 +148,21 @@ func (w *Window) TriggerKeyHeld(e okey.Event) { func (w *Window) TriggerMouseEvent(mevent omouse.Event) { w.LastMouseEvent = mevent omouse.LastEvent = mevent - w.Propagate(mevent.Event+"On", mevent) - w.eventHandler.Trigger(mevent.Event, &mevent) + on, onOk := omouse.EventOn(mevent.EventType) + if onOk { + w.Propagate(on, mevent) + } + event.TriggerOn(w.eventHandler, mevent.EventType, &mevent) - relativeEvent := mevent - relativeEvent.Point2[0] += float64(w.viewPos[0]) - relativeEvent.Point2[1] += float64(w.viewPos[1]) - w.LastRelativeMouseEvent = relativeEvent - w.Propagate(relativeEvent.Event+"OnRelative", relativeEvent) + if onOk { + rel, ok := omouse.EventRelative(on) + if ok { + relativeEvent := mevent + relativeEvent.Point2[0] += float64(w.viewPos[0]) + relativeEvent.Point2[1] += float64(w.viewPos[1]) + w.LastRelativeMouseEvent = relativeEvent + + w.Propagate(rel, relativeEvent) + } + } } diff --git a/inputLoop_test.go b/inputLoop_test.go index cc2093f4..2ac6d92b 100644 --- a/inputLoop_test.go +++ b/inputLoop_test.go @@ -4,25 +4,26 @@ import ( "testing" "time" - "github.com/oakmound/oak/v3/event" - okey "github.com/oakmound/oak/v3/key" + "github.com/oakmound/oak/v4/event" "golang.org/x/mobile/event/key" + "golang.org/x/mobile/event/mouse" ) func TestInputLoop(t *testing.T) { c1 := blankScene(t) c1.SetLogicHandler(event.NewBus(nil)) - c1.windowControl.Send(okey.Event{ + c1.Window.Send(key.Event{ Direction: key.DirPress, Code: key.Code0, }) - c1.windowControl.Send(okey.Event{ + c1.Window.Send(key.Event{ Direction: key.DirNone, Code: key.Code0, }) - c1.windowControl.Send(okey.Event{ + c1.Window.Send(key.Event{ Direction: key.DirRelease, Code: key.Code0, }) + c1.Window.Send(mouse.Event{}) time.Sleep(2 * time.Second) } diff --git a/inputTracker.go b/inputTracker.go index f8cbef27..5ee79c1c 100644 --- a/inputTracker.go +++ b/inputTracker.go @@ -4,41 +4,45 @@ import ( "sync/atomic" "time" - "github.com/oakmound/oak/v3/dlog" - "github.com/oakmound/oak/v3/event" - "github.com/oakmound/oak/v3/joystick" - "github.com/oakmound/oak/v3/key" - "github.com/oakmound/oak/v3/mouse" + "github.com/oakmound/oak/v4/dlog" + "github.com/oakmound/oak/v4/event" + "github.com/oakmound/oak/v4/joystick" + "github.com/oakmound/oak/v4/key" + "github.com/oakmound/oak/v4/mouse" ) // InputType expresses some form of input to the engine to represent a player -type InputType = int32 +type InputType int32 -// Supported Input Types +var trackingJoystickChange = event.RegisterEvent[struct{}]() + +// The following constants define valid types of input sent via the InputChange event. const ( - InputKeyboardMouse InputType = iota - InputJoystick InputType = iota + InputNone InputType = iota + InputKeyboard + InputMouse + InputJoystick ) func (w *Window) trackInputChanges() { - w.eventHandler.GlobalBind(key.Down, func(event.CID, interface{}) int { - old := atomic.SwapInt32(&w.mostRecentInput, InputKeyboardMouse) - if old != InputKeyboardMouse { - w.eventHandler.Trigger(event.InputChange, InputKeyboardMouse) + event.GlobalBind(w.eventHandler, key.AnyDown, func(key.Event) event.Response { + old := atomic.SwapInt32(&w.mostRecentInput, int32(InputKeyboard)) + if InputType(old) != InputKeyboard { + event.TriggerOn(w.eventHandler, InputChange, InputKeyboard) } return 0 }) - w.eventHandler.GlobalBind(mouse.Press, func(event.CID, interface{}) int { - old := atomic.SwapInt32(&w.mostRecentInput, InputKeyboardMouse) - if old != InputKeyboardMouse { - w.eventHandler.Trigger(event.InputChange, InputKeyboardMouse) + event.GlobalBind(w.eventHandler, mouse.Press, func(*mouse.Event) event.Response { + old := atomic.SwapInt32(&w.mostRecentInput, int32(InputMouse)) + if InputType(old) != InputMouse { + event.TriggerOn(w.eventHandler, InputChange, InputMouse) } return 0 }) - w.eventHandler.GlobalBind("Tracking"+joystick.Change, func(event.CID, interface{}) int { - old := atomic.SwapInt32(&w.mostRecentInput, InputJoystick) - if old != InputJoystick { - w.eventHandler.Trigger(event.InputChange, InputJoystick) + event.GlobalBind(w.eventHandler, trackingJoystickChange, func(struct{}) event.Response { + old := atomic.SwapInt32(&w.mostRecentInput, int32(InputMouse)) + if InputType(old) != InputJoystick { + event.TriggerOn(w.eventHandler, InputChange, InputJoystick) } return 0 }) @@ -48,8 +52,8 @@ type joyHandler struct { handler event.Handler } -func (jh *joyHandler) Trigger(ev string, state interface{}) { - jh.handler.Trigger("Tracking"+ev, state) +func (jh *joyHandler) Trigger(eventID event.UnsafeEventID, data interface{}) <-chan struct{} { + return event.TriggerOn(jh.handler, trackingJoystickChange, struct{}{}) } func trackJoystickChanges(handler event.Handler) { diff --git a/inputTracker_test.go b/inputTracker_test.go index b8730c6c..a3fd31b7 100644 --- a/inputTracker_test.go +++ b/inputTracker_test.go @@ -4,16 +4,17 @@ import ( "testing" "time" - "github.com/oakmound/oak/v3/event" - "github.com/oakmound/oak/v3/joystick" - "github.com/oakmound/oak/v3/key" - "github.com/oakmound/oak/v3/mouse" - "github.com/oakmound/oak/v3/scene" + "github.com/oakmound/oak/v4/event" + "github.com/oakmound/oak/v4/key" + "github.com/oakmound/oak/v4/mouse" + "github.com/oakmound/oak/v4/scene" ) func TestTrackInputChanges(t *testing.T) { + inputChangeFailed := make(chan bool) + c1 := NewWindow() - c1.SetLogicHandler(event.NewBus(nil)) + c1.SetLogicHandler(event.NewBus(event.NewCallerMap())) c1.AddScene("1", scene.Scene{}) go c1.Init("1", func(c Config) (Config, error) { c.TrackInputChanges = true @@ -21,36 +22,33 @@ func TestTrackInputChanges(t *testing.T) { }) time.Sleep(2 * time.Second) expectedType := new(InputType) - *expectedType = InputKeyboardMouse - failed := false - c1.eventHandler.GlobalBind(event.InputChange, func(_ event.CID, payload interface{}) int { - mode := payload.(InputType) - if mode != *expectedType { - failed = true - } + *expectedType = InputKeyboard + event.GlobalBind(c1.eventHandler, InputChange, func(mode InputType) event.Response { + inputChangeFailed <- mode != *expectedType return 0 }) c1.TriggerKeyDown(key.Event{}) - time.Sleep(2 * time.Second) - if failed { + if <-inputChangeFailed { t.Fatalf("keyboard change failed") } *expectedType = InputJoystick - c1.eventHandler.Trigger("Tracking"+joystick.Change, &joystick.State{}) - time.Sleep(2 * time.Second) - if failed { + event.TriggerOn(c1.eventHandler, trackingJoystickChange, struct{}{}) + if <-inputChangeFailed { t.Fatalf("joystick change failed") } - *expectedType = InputKeyboardMouse - c1.TriggerMouseEvent(mouse.Event{Event: mouse.Press}) - time.Sleep(2 * time.Second) - if failed { + c1.mostRecentInput = int32(InputJoystick) + *expectedType = InputMouse + c1.TriggerMouseEvent(mouse.Event{EventType: mouse.Press}) + if <-inputChangeFailed { t.Fatalf("mouse change failed") } - c1.mostRecentInput = InputJoystick + *expectedType = InputKeyboard + c1.mostRecentInput = int32(InputJoystick) c1.TriggerKeyDown(key.Event{}) - time.Sleep(2 * time.Second) - if failed { + if <-inputChangeFailed { t.Fatalf("keyboard change failed") } + if c1.MostRecentInput() != InputKeyboard { + t.Fatalf("most recent input getter failed") + } } diff --git a/joystick/driver_darwin.go b/joystick/driver_darwin.go index 396d1ceb..5bcb09ad 100644 --- a/joystick/driver_darwin.go +++ b/joystick/driver_darwin.go @@ -1,6 +1,6 @@ package joystick -import "github.com/oakmound/oak/v3/oakerr" +import "github.com/oakmound/oak/v4/oakerr" func osinit() error { return nil @@ -14,19 +14,19 @@ type osJoystick struct { } func (j *Joystick) prepare() error { - return oakerr.UnsupportedPlatform{Operation:"joystick"} + return oakerr.UnsupportedPlatform{Operation: "joystick"} } func (j *Joystick) getState() (*State, error) { - return nil, oakerr.UnsupportedPlatform{Operation:"joystick"} + return nil, oakerr.UnsupportedPlatform{Operation: "joystick"} } func (j *Joystick) vibrate(left, right uint16) error { - return oakerr.UnsupportedPlatform{Operation:"joystick"} + return oakerr.UnsupportedPlatform{Operation: "joystick"} } func (j *Joystick) close() error { - return oakerr.UnsupportedPlatform{Operation:"joystick"} + return oakerr.UnsupportedPlatform{Operation: "joystick"} } func getJoysticks() []*Joystick { diff --git a/joystick/driver_js.go b/joystick/driver_js.go index eb36b4e0..063c8ec5 100644 --- a/joystick/driver_js.go +++ b/joystick/driver_js.go @@ -1,17 +1,17 @@ package joystick import ( + "errors" "reflect" "syscall/js" - "errors" - "github.com/oakmound/oak/v3/event" - "github.com/oakmound/oak/v3/oakerr" - "github.com/oakmound/oak/v3/timing" + "github.com/oakmound/oak/v4/event" + "github.com/oakmound/oak/v4/oakerr" + "github.com/oakmound/oak/v4/timing" ) -func osinit() error { - // TODO: listen to joystick connected and joystick disconnected? We'd still need to +func osinit() error { + // TODO: listen to joystick connected and joystick disconnected? We'd still need to // list from getGamepads every frame, it seems, to get new button presses. return nil } @@ -63,19 +63,19 @@ type jsGamepadState struct { connected bool // osID string // index int - mapping string + mapping string } type jsButton struct { - value float64 + value float64 //touched bool pressed bool } type osJoystick struct { - cache State - jsState jsGamepadState - newJSState jsGamepadState + cache State + jsState jsGamepadState + newJSState jsGamepadState newButtons map[string]bool } diff --git a/joystick/driver_linux.go b/joystick/driver_linux.go index 4aed3e51..67e11c41 100644 --- a/joystick/driver_linux.go +++ b/joystick/driver_linux.go @@ -7,10 +7,10 @@ import ( "strconv" "sync" - "github.com/oakmound/oak/v3/dlog" - "github.com/oakmound/oak/v3/event" - "github.com/oakmound/oak/v3/oakerr" - "github.com/oakmound/oak/v3/timing" + "github.com/oakmound/oak/v4/dlog" + "github.com/oakmound/oak/v4/event" + "github.com/oakmound/oak/v4/oakerr" + "github.com/oakmound/oak/v4/timing" "encoding/binary" "path" diff --git a/joystick/driver_other.go b/joystick/driver_other.go index 34d4f06d..4f2abe8f 100644 --- a/joystick/driver_other.go +++ b/joystick/driver_other.go @@ -3,7 +3,7 @@ package joystick -import "github.com/oakmound/oak/v3/oakerr" +import "github.com/oakmound/oak/v4/oakerr" func newOsJoystick() osJoystick { return osJoystick{} diff --git a/joystick/driver_windows.go b/joystick/driver_windows.go index 7d64b1c6..4485699c 100644 --- a/joystick/driver_windows.go +++ b/joystick/driver_windows.go @@ -3,8 +3,8 @@ package joystick import ( "sync" - "github.com/oakmound/oak/v3/event" - "github.com/oakmound/oak/v3/timing" + "github.com/oakmound/oak/v4/event" + "github.com/oakmound/oak/v4/timing" "github.com/oakmound/w32" ) diff --git a/joystick/joystick.go b/joystick/joystick.go index 4632bb35..856381b7 100644 --- a/joystick/joystick.go +++ b/joystick/joystick.go @@ -4,9 +4,11 @@ package joystick import ( "math" + "sync" "time" - "github.com/oakmound/oak/v3/dlog" + "github.com/oakmound/oak/v4/dlog" + "github.com/oakmound/oak/v4/event" ) type Input string @@ -29,16 +31,17 @@ const ( InputRightStick Input = "RightStick" ) -// Events. All events include a *State payload. -const ( - Change = "JoystickChange" - ButtonDown = "ButtonDown" - ButtonUp = "ButtonUp" - RtTriggerChange = "RtTriggerChange" - LtTriggerChange = "LtTriggerChange" - RtStickChange = "RtStickChange" - LtStickChange = "LtStickChange" - Disconnected = "JoystickDisconnected" +// Events. All events but Disconnected include a *State payload. +var ( + Change = event.RegisterEvent[*State]() + ButtonDown = event.RegisterEvent[*State]() + ButtonUp = event.RegisterEvent[*State]() + RtTriggerChange = event.RegisterEvent[*State]() + LtTriggerChange = event.RegisterEvent[*State]() + RtStickChange = event.RegisterEvent[*State]() + LtStickChange = event.RegisterEvent[*State]() + // Disconnected includes the ID of the joystick that disconnected. + Disconnected = event.RegisterEvent[uint32]() ) // Init calls any os functions necessary to detect joysticks @@ -49,7 +52,7 @@ func Init() error { // A Triggerer can either be an event bus or event CID, allowing // joystick triggers to be listened to globally or sent to particular entities. type Triggerer interface { - Trigger(string, interface{}) + Trigger(eventID event.UnsafeEventID, data interface{}) <-chan struct{} } // A Joystick represents a (usually) physical controller connected to the machine. @@ -121,7 +124,7 @@ func (lo *ListenOptions) sendFn() func(Triggerer, *State, *State) { var fn func(Triggerer, *State, *State) if lo.JoystickChanges { fn = func(h Triggerer, cur, last *State) { - h.Trigger(Change, cur) + h.Trigger(Change.UnsafeEventID, cur) } } if lo.GenericButtonPresses { @@ -134,13 +137,13 @@ func (lo *ListenOptions) sendFn() func(Triggerer, *State, *State) { for k, v := range cur.Buttons { if v != last.Buttons[k] { if v && !downTriggered { - h.Trigger(ButtonDown, cur) + h.Trigger(ButtonDown.UnsafeEventID, cur) downTriggered = true if upTriggered { return } } else if !v && !upTriggered { - h.Trigger(ButtonUp, cur) + h.Trigger(ButtonUp.UnsafeEventID, cur) upTriggered = true if downTriggered { return @@ -156,13 +159,13 @@ func (lo *ListenOptions) sendFn() func(Triggerer, *State, *State) { for k, v := range cur.Buttons { if v != last.Buttons[k] { if v && !downTriggered { - h.Trigger(ButtonDown, cur) + h.Trigger(ButtonDown.UnsafeEventID, cur) downTriggered = true if upTriggered { return } } else if !v && !upTriggered { - h.Trigger(ButtonUp, cur) + h.Trigger(ButtonUp.UnsafeEventID, cur) upTriggered = true if downTriggered { return @@ -181,9 +184,9 @@ func (lo *ListenOptions) sendFn() func(Triggerer, *State, *State) { for k, v := range cur.Buttons { if v != last.Buttons[k] { if v { - h.Trigger(k+ButtonDown, cur) + h.Trigger(Down(k).UnsafeEventID, cur) } else { - h.Trigger(k+ButtonUp, cur) + h.Trigger(Up(k).UnsafeEventID, cur) } } } @@ -193,9 +196,9 @@ func (lo *ListenOptions) sendFn() func(Triggerer, *State, *State) { for k, v := range cur.Buttons { if v != last.Buttons[k] { if v { - h.Trigger(k+ButtonDown, cur) + h.Trigger(Down(k).UnsafeEventID, cur) } else { - h.Trigger(k+ButtonUp, cur) + h.Trigger(Up(k).UnsafeEventID, cur) } } } @@ -209,22 +212,22 @@ func (lo *ListenOptions) sendFn() func(Triggerer, *State, *State) { prevFn(h, cur, last) if deltaExceedsThreshold(cur.StickLX, last.StickLX, lo.StickDeadzoneLX) || deltaExceedsThreshold(cur.StickLY, last.StickLY, lo.StickDeadzoneLY) { - h.Trigger(LtStickChange, cur) + h.Trigger(LtStickChange.UnsafeEventID, cur) } if deltaExceedsThreshold(cur.StickRX, last.StickRX, lo.StickDeadzoneRX) || deltaExceedsThreshold(cur.StickRY, last.StickRY, lo.StickDeadzoneRY) { - h.Trigger(RtStickChange, cur) + h.Trigger(RtStickChange.UnsafeEventID, cur) } } } else { fn = func(h Triggerer, cur, last *State) { if deltaExceedsThreshold(cur.StickLX, last.StickLX, lo.StickDeadzoneLX) || deltaExceedsThreshold(cur.StickLY, last.StickLY, lo.StickDeadzoneLY) { - h.Trigger(LtStickChange, cur) + h.Trigger(LtStickChange.UnsafeEventID, cur) } if deltaExceedsThreshold(cur.StickRX, last.StickRX, lo.StickDeadzoneRX) || deltaExceedsThreshold(cur.StickRY, last.StickRY, lo.StickDeadzoneRY) { - h.Trigger(RtStickChange, cur) + h.Trigger(RtStickChange.UnsafeEventID, cur) } } } @@ -235,19 +238,19 @@ func (lo *ListenOptions) sendFn() func(Triggerer, *State, *State) { fn = func(h Triggerer, cur, last *State) { prevFn(h, cur, last) if cur.TriggerL != last.TriggerL { - h.Trigger(LtTriggerChange, cur) + h.Trigger(LtTriggerChange.UnsafeEventID, cur) } if cur.TriggerR != last.TriggerR { - h.Trigger(RtTriggerChange, cur) + h.Trigger(RtTriggerChange.UnsafeEventID, cur) } } } else { fn = func(h Triggerer, cur, last *State) { if cur.TriggerL != last.TriggerL { - h.Trigger(LtTriggerChange, cur) + h.Trigger(LtTriggerChange.UnsafeEventID, cur) } if cur.TriggerR != last.TriggerR { - h.Trigger(RtTriggerChange, cur) + h.Trigger(RtTriggerChange.UnsafeEventID, cur) } } } @@ -255,6 +258,34 @@ func (lo *ListenOptions) sendFn() func(Triggerer, *State, *State) { return fn } +var upEventsLock sync.Mutex +var upEvents = map[string]event.EventID[*State]{} + +func Up(s string) event.EventID[*State] { + upEventsLock.Lock() + defer upEventsLock.Unlock() + if ev, ok := upEvents[s]; ok { + return ev + } + ev := event.RegisterEvent[*State]() + upEvents[s] = ev + return ev +} + +var downEventsLock sync.Mutex +var downEvents = map[string]event.EventID[*State]{} + +func Down(s string) event.EventID[*State] { + downEventsLock.Lock() + defer downEventsLock.Unlock() + if ev, ok := downEvents[s]; ok { + return ev + } + ev := event.RegisterEvent[*State]() + downEvents[s] = ev + return ev +} + func deltaExceedsThreshold(old, new, threshold int16) bool { return intAbs(old-new) > threshold } @@ -306,7 +337,7 @@ func (j *Joystick) Listen(opts *ListenOptions) (cancel func()) { } state, err := j.GetState() if err != nil { - j.Handler.Trigger(Disconnected, j.id) + j.Handler.Trigger(Disconnected.UnsafeEventID, j.id) dlog.Error(err) t.Stop() j.Close() diff --git a/key/events.go b/key/events.go index ec96bd36..143261f2 100644 --- a/key/events.go +++ b/key/events.go @@ -1,38 +1,83 @@ package key import ( - "github.com/oakmound/oak/v3/event" + "sync" + + "github.com/oakmound/oak/v4/event" "golang.org/x/mobile/event/key" ) -const ( +var ( // Down is sent when a key is pressed. It is sent both as // Down, and as Down + the key name. - Down = "KeyDown" + AnyDown = event.RegisterEvent[Event]() // Up is sent when a key is released. It is sent both as // Up, and as Up + the key name. - Up = "KeyUp" + AnyUp = event.RegisterEvent[Event]() // Held is sent when a key is held down. It is sent both as // Held, and as Held + the key name. - Held = "KeyHeld" + AnyHeld = event.RegisterEvent[Event]() ) // An Event is sent as the payload for all key bindings. -type Event = key.Event - -// A code is a unique integer code for a given common key -type Code = key.Code - -// Binding will convert a function that accepts a typecast key.Event into a generic event binding -// -// Example: -// bus.Bind(key.Down, key.Binding(keyHandler)) -func Binding(fn func(event.CID, Event) int) func(event.CID, interface{}) int { - return func(cid event.CID, iface interface{}) int { - ke, ok := iface.(Event) - if !ok { - return event.UnbindSingle - } - return fn(cid, ke) +type Event key.Event + +type Modifiers = key.Modifiers + +const ( + ModShift Modifiers = 1 << 0 + ModControl Modifiers = 1 << 1 + ModAlt Modifiers = 1 << 2 + ModMeta Modifiers = 1 << 3 // called "Command" on OS X +) + +type Direction = key.Direction + +const ( + DirNone Direction = 0 + DirPress Direction = 1 + DirRelease Direction = 2 +) + +var upEventsLock sync.Mutex +var upEvents = map[Code]event.EventID[Event]{} + +// Up checks +func Up(code Code) event.EventID[Event] { + upEventsLock.Lock() + defer upEventsLock.Unlock() + if ev, ok := upEvents[code]; ok { + return ev + } + ev := event.RegisterEvent[Event]() + upEvents[code] = ev + return ev +} + +var downEventsLock sync.Mutex +var downEvents = map[Code]event.EventID[Event]{} + +func Down(code Code) event.EventID[Event] { + downEventsLock.Lock() + defer downEventsLock.Unlock() + if ev, ok := downEvents[code]; ok { + return ev + } + ev := event.RegisterEvent[Event]() + downEvents[code] = ev + return ev +} + +var heldEventsLock sync.Mutex +var heldEvents = map[Code]event.EventID[Event]{} + +func Held(code Code) event.EventID[Event] { + heldEventsLock.Lock() + defer heldEventsLock.Unlock() + if ev, ok := heldEvents[code]; ok { + return ev } + ev := event.RegisterEvent[Event]() + heldEvents[code] = ev + return ev } diff --git a/key/keycodes.go b/key/keycodes.go new file mode 100644 index 00000000..4c06915a --- /dev/null +++ b/key/keycodes.go @@ -0,0 +1,141 @@ +package key + +import "golang.org/x/mobile/event/key" + +// Code is the identity of a key relative to a notional "standard" keyboard. +// It is a straight copy of mobile package's key codes cleaned up for ease of binding in oak. +// See AllKeys for string mappers. +type Code = key.Code + +const ( + Unknown Code = 0 + A Code = 4 + B Code = 5 + C Code = 6 + D Code = 7 + E Code = 8 + F Code = 9 + G Code = 10 + H Code = 11 + I Code = 12 + J Code = 13 + K Code = 14 + L Code = 15 + M Code = 16 + N Code = 17 + O Code = 18 + P Code = 19 + Q Code = 20 + R Code = 21 + S Code = 22 + T Code = 23 + U Code = 24 + V Code = 25 + W Code = 26 + X Code = 27 + Y Code = 28 + Z Code = 29 + + Num1 Code = 30 + Num2 Code = 31 + Num3 Code = 32 + Num4 Code = 33 + Num5 Code = 34 + Num6 Code = 35 + Num7 Code = 36 + Num8 Code = 37 + Num9 Code = 38 + Num0 Code = 39 + + ReturnEnter Code = 40 + Escape Code = 41 + DeleteBackspace Code = 42 + Tab Code = 43 + Spacebar Code = 44 + HyphenMinus Code = 45 + EqualSign Code = 46 + LeftSquareBracket Code = 47 + RightSquareBracket Code = 48 + Backslash Code = 49 + Semicolon Code = 51 + Apostrophe Code = 52 + GraveAccent Code = 53 + Comma Code = 54 + FullStop Code = 55 + Slash Code = 56 + CapsLock Code = 57 + + F1 Code = 58 + F2 Code = 59 + F3 Code = 60 + F4 Code = 61 + F5 Code = 62 + F6 Code = 63 + F7 Code = 64 + F8 Code = 65 + F9 Code = 66 + F10 Code = 67 + F11 Code = 68 + F12 Code = 69 + + Pause Code = 72 + Insert Code = 73 + Home Code = 74 + PageUp Code = 75 + DeleteForward Code = 76 + End Code = 77 + PageDown Code = 78 + + RightArrow Code = 79 + LeftArrow Code = 80 + DownArrow Code = 81 + UpArrow Code = 82 + + KeypadNumLock Code = 83 + KeypadSlash Code = 84 + KeypadAsterisk Code = 85 + KeypadHyphenMinus Code = 86 + KeypadPlusSign Code = 87 + KeypadEnter Code = 88 + Keypad1 Code = 89 + Keypad2 Code = 90 + Keypad3 Code = 91 + Keypad4 Code = 92 + Keypad5 Code = 93 + Keypad6 Code = 94 + Keypad7 Code = 95 + Keypad8 Code = 96 + Keypad9 Code = 97 + Keypad0 Code = 98 + KeypadFullStop Code = 99 + KeypadEqualSign Code = 103 + + F13 Code = 104 + F14 Code = 105 + F15 Code = 106 + F16 Code = 107 + F17 Code = 108 + F18 Code = 109 + F19 Code = 110 + F20 Code = 111 + F21 Code = 112 + F22 Code = 113 + F23 Code = 114 + F24 Code = 115 + + Help Code = 117 + + Mute Code = 127 + VolumeUp Code = 128 + VolumeDown Code = 129 + + LeftControl Code = 224 + LeftShift Code = 225 + LeftAlt Code = 226 + LeftGUI Code = 227 + RightControl Code = 228 + RightShift Code = 229 + RightAlt Code = 230 + RightGUI Code = 231 + Compose Code = 0x10000 +) diff --git a/key/keys.go b/key/keys.go index 1cb73fc9..64bb3d95 100644 --- a/key/keys.go +++ b/key/keys.go @@ -1,277 +1,135 @@ package key -// This lists the keys sent through oak's input events. -// This list is not used internally by oak, but was generated from -// the expected output from x/mobile/key. -// -// These strings are sent as payloads to Key.Down and Key.Up events, -// and through "KeyDown"+$a, "KeyUp"+$a for any $a in the const. -const ( - Unknown = "Unknown" - - A = "A" - B = "B" - C = "C" - D = "D" - E = "E" - F = "F" - G = "G" - H = "H" - I = "I" - J = "J" - K = "K" - L = "L" - M = "M" - N = "N" - O = "O" - P = "P" - Q = "Q" - R = "R" - S = "S" - T = "T" - U = "U" - V = "V" - W = "W" - X = "X" - Y = "Y" - Z = "Z" - - One = "1" - Two = "2" - Three = "3" - Four = "4" - Five = "5" - Six = "6" - Seven = "7" - Eight = "8" - Nine = "9" - Zero = "0" - - ReturnEnter = "ReturnEnter" - Enter = ReturnEnter - Escape = "Escape" - DeleteBackspace = "DeleteBackspace" - Tab = "Tab" - Spacebar = "Spacebar" - HyphenMinus = "HyphenMinus" //- - EqualSign = "EqualSign" //= - LeftSquareBracket = "LeftSquareBracket" //[ - RightSquareBracket = "RightSquareBracket" //] - Backslash = "Backslash" //\ - Semicolon = "Semicolon" //; - Apostrophe = "Apostrophe" //' - GraveAccent = "GraveAccent" //` - Comma = "Comma" //, - FullStop = "FullStop" //. - Period = FullStop - Slash = "Slash" /// - CapsLock = "CapsLock" - - F1 = "F1" - F2 = "F2" - F3 = "F3" - F4 = "F4" - F5 = "F5" - F6 = "F6" - F7 = "F7" - F8 = "F8" - F9 = "F9" - F10 = "F10" - F11 = "F11" - F12 = "F12" - - Pause = "Pause" - Insert = "Insert" - Home = "Home" - PageUp = "PageUp" - DeleteForward = "DeleteForward" - End = "End" - PageDown = "PageDown" - - RightArrow = "RightArrow" - LeftArrow = "LeftArrow" - DownArrow = "DownArrow" - UpArrow = "UpArrow" - - KeypadNumLock = "KeypadNumLock" - KeypadSlash = "KeypadSlash" /// - KeypadAsterisk = "KeypadAsterisk" //* - KeypadHyphenMinus = "KeypadHyphenMinus" //- - KeypadPlusSign = "KeypadPlusSign" //+ - KeypadEnter = "KeypadEnter" - Keypad1 = "Keypad1" - Keypad2 = "Keypad2" - Keypad3 = "Keypad3" - Keypad4 = "Keypad4" - Keypad5 = "Keypad5" - Keypad6 = "Keypad6" - Keypad7 = "Keypad7" - Keypad8 = "Keypad8" - Keypad9 = "Keypad9" - Keypad0 = "Keypad0" - KeypadFullStop = "KeypadFullStop" //. - KeypadPeriod = KeypadFullStop - KeypadEqualSign = "KeypadEqualSign" //= - - F13 = "F13" - F14 = "F14" - F15 = "F15" - F16 = "F16" - F17 = "F17" - F18 = "F18" - F19 = "F19" - F20 = "F20" - F21 = "F21" - F22 = "F22" - F23 = "F23" - F24 = "F24" - - Help = "Help" - - Mute = "Mute" - VolumeUp = "VolumeUp" - VolumeDown = "VolumeDown" - - LeftControl = "LeftControl" - LeftShift = "LeftShift" - LeftAlt = "LeftAlt" - LeftGUI = "LeftGUI" - RightControl = "RightControl" - RightShift = "RightShift" - RightAlt = "RightAlt" - RightGUI = "RightGUI" -) - // AllKeys is the set of all defined key codes -var AllKeys = map[string]struct{}{ - Unknown: {}, - - A: {}, - B: {}, - C: {}, - D: {}, - E: {}, - F: {}, - G: {}, - H: {}, - I: {}, - J: {}, - K: {}, - L: {}, - M: {}, - N: {}, - O: {}, - P: {}, - Q: {}, - R: {}, - S: {}, - T: {}, - U: {}, - V: {}, - W: {}, - X: {}, - Y: {}, - Z: {}, - - One: {}, - Two: {}, - Three: {}, - Four: {}, - Five: {}, - Six: {}, - Seven: {}, - Eight: {}, - Nine: {}, - Zero: {}, - - ReturnEnter: {}, - Escape: {}, - DeleteBackspace: {}, - Tab: {}, - Spacebar: {}, - HyphenMinus: {}, - EqualSign: {}, - LeftSquareBracket: {}, - RightSquareBracket: {}, - Backslash: {}, - Semicolon: {}, - Apostrophe: {}, - GraveAccent: {}, - Comma: {}, - FullStop: {}, - Slash: {}, - CapsLock: {}, - - F1: {}, - F2: {}, - F3: {}, - F4: {}, - F5: {}, - F6: {}, - F7: {}, - F8: {}, - F9: {}, - F10: {}, - F11: {}, - F12: {}, - - Pause: {}, - Insert: {}, - Home: {}, - PageUp: {}, - DeleteForward: {}, - End: {}, - PageDown: {}, - - RightArrow: {}, - LeftArrow: {}, - DownArrow: {}, - UpArrow: {}, - - KeypadNumLock: {}, - KeypadSlash: {}, - KeypadAsterisk: {}, - KeypadHyphenMinus: {}, - KeypadPlusSign: {}, - KeypadEnter: {}, - Keypad1: {}, - Keypad2: {}, - Keypad3: {}, - Keypad4: {}, - Keypad5: {}, - Keypad6: {}, - Keypad7: {}, - Keypad8: {}, - Keypad9: {}, - Keypad0: {}, - KeypadFullStop: {}, - KeypadEqualSign: {}, - - F13: {}, - F14: {}, - F15: {}, - F16: {}, - F17: {}, - F18: {}, - F19: {}, - F20: {}, - F21: {}, - F22: {}, - F23: {}, - F24: {}, - - Help: {}, - - Mute: {}, - VolumeUp: {}, - VolumeDown: {}, - - LeftControl: {}, - LeftShift: {}, - LeftAlt: {}, - LeftGUI: {}, - RightControl: {}, - RightShift: {}, - RightAlt: {}, - RightGUI: {}, +var AllKeys = map[Code]string{ + Unknown: "Unknown", + + A: "A", + B: "B", + C: "C", + D: "D", + E: "E", + F: "F", + G: "G", + H: "H", + I: "I", + J: "J", + K: "K", + L: "L", + M: "M", + N: "N", + O: "O", + P: "P", + Q: "Q", + R: "R", + S: "S", + T: "T", + U: "U", + V: "V", + W: "W", + X: "X", + Y: "Y", + Z: "Z", + + Num1: "1", + Num2: "2", + Num3: "3", + Num4: "4", + Num5: "5", + Num6: "6", + Num7: "7", + Num8: "8", + Num9: "9", + Num0: "0", + + ReturnEnter: "ReturnEnter", + Escape: "Escape", + DeleteBackspace: "DeleteBackspace", + Tab: "Tab", + Spacebar: "Spacebar", + HyphenMinus: "HyphenMinus", + EqualSign: "EqualSign", + LeftSquareBracket: "LeftSquareBracket", + RightSquareBracket: "RightSquareBracket", + Backslash: "Backslash", + Semicolon: "Semicolon", + Apostrophe: "Apostrophe", + GraveAccent: "GraveAccent", + Comma: "Comma", + FullStop: "FullStop", + Slash: "Slash", + CapsLock: "CapsLock", + + F1: "F1", + F2: "F2", + F3: "F3", + F4: "F4", + F5: "F5", + F6: "F6", + F7: "F7", + F8: "F8", + F9: "F9", + F10: "F10", + F11: "F11", + F12: "F12", + + Pause: "Pause", + Insert: "Insert", + Home: "Home", + PageUp: "PageUp", + DeleteForward: "DeleteForward", + End: "End", + PageDown: "PageDown", + + RightArrow: "RightArrow", + LeftArrow: "LeftArrow", + DownArrow: "DownArrow", + UpArrow: "UpArrow", + + KeypadNumLock: "KeypadNumLock", + KeypadSlash: "KeypadSlash", + KeypadAsterisk: "KeypadAsterisk", + KeypadHyphenMinus: "KeypadHyphenMinus", + KeypadPlusSign: "KeypadPlusSign", + KeypadEnter: "KeypadEnter", + Keypad1: "Keypad1", + Keypad2: "Keypad2", + Keypad3: "Keypad3", + Keypad4: "Keypad4", + Keypad5: "Keypad5", + Keypad6: "Keypad6", + Keypad7: "Keypad7", + Keypad8: "Keypad8", + Keypad9: "Keypad9", + Keypad0: "Keypad0", + KeypadFullStop: "KeypadFullStop", + KeypadEqualSign: "KeypadEqualSign", + + F13: "F13", + F14: "F14", + F15: "F15", + F16: "F16", + F17: "F17", + F18: "F18", + F19: "F19", + F20: "F20", + F21: "F21", + F22: "F22", + F23: "F23", + F24: "F24", + + Help: "Help", + + Mute: "Mute", + VolumeUp: "VolumeUp", + VolumeDown: "VolumeDown", + + LeftControl: "LeftControl", + LeftShift: "LeftShift", + LeftAlt: "LeftAlt", + LeftGUI: "LeftGUI", + RightControl: "RightControl", + RightShift: "RightShift", + RightAlt: "RightAlt", + RightGUI: "RightGUI", } diff --git a/key/state.go b/key/state.go index 428d955c..0165b0fc 100644 --- a/key/state.go +++ b/key/state.go @@ -8,8 +8,8 @@ import ( // A State tracks what keys of a keyboard are currently pressed and for how long they have been // pressed if they are held down. type State struct { - state map[string]bool - durations map[string]time.Time + state map[Code]bool + durations map[Code]time.Time stateLock sync.RWMutex durationLock sync.RWMutex } @@ -17,8 +17,8 @@ type State struct { // NewState creates a state object for tracking keyboard state. func NewState() State { return State{ - state: make(map[string]bool), - durations: make(map[string]time.Time), + state: make(map[Code]bool), + durations: make(map[Code]time.Time), } } @@ -27,7 +27,7 @@ func NewState() State { // events are sent from the real keyboard and mouse. // Calling this can interrupt real input or cause // unintended behavior and should be done cautiously. -func (ks *State) SetUp(key string) { +func (ks *State) SetUp(key Code) { ks.stateLock.Lock() ks.durationLock.Lock() delete(ks.state, key) @@ -41,7 +41,7 @@ func (ks *State) SetUp(key string) { // events are sent from the real keyboard and mouse. // Calling this can interrupt real input or cause // unintended behavior and should be done cautiously. -func (ks *State) SetDown(key string) { +func (ks *State) SetDown(key Code) { ks.stateLock.Lock() ks.state[key] = true ks.durations[key] = time.Now() @@ -49,7 +49,7 @@ func (ks *State) SetDown(key string) { } // IsDown returns whether a key is held down -func (ks *State) IsDown(key string) (k bool) { +func (ks *State) IsDown(key Code) (k bool) { ks.stateLock.RLock() k = ks.state[key] ks.stateLock.RUnlock() @@ -58,7 +58,7 @@ func (ks *State) IsDown(key string) (k bool) { // IsHeld returns whether a key is held down, and for how long // it has been held. -func (ks *State) IsHeld(key string) (k bool, d time.Duration) { +func (ks *State) IsHeld(key Code) (k bool, d time.Duration) { ks.stateLock.RLock() k = ks.state[key] ks.stateLock.RUnlock() diff --git a/key/state_test.go b/key/state_test.go index 82b11cbb..a9d5cc70 100644 --- a/key/state_test.go +++ b/key/state_test.go @@ -7,29 +7,29 @@ import ( func TestState(t *testing.T) { ks := NewState() - ks.SetDown("Test") - if !ks.IsDown("Test") { - t.Fatalf("test was not set down") + ks.SetDown(A) + if !ks.IsDown(A) { + t.Fatalf("a was not set down") } - ks.SetUp("Test") - if ks.IsDown("Test") { - t.Fatalf("test was not set up") + ks.SetUp(A) + if ks.IsDown(A) { + t.Fatalf("a was not set up") } - ks.SetDown("Test") + ks.SetDown(A) time.Sleep(2 * time.Second) - ok, d := ks.IsHeld("Test") + ok, d := ks.IsHeld(A) if !ok { - t.Fatalf("test was not held down") + t.Fatalf("a was not held down") } if d < 2000*time.Millisecond { - t.Fatalf("test was not held down for sleep length") + t.Fatalf("a was not held down for sleep length") } - ks.SetUp("Test") - ok, d = ks.IsHeld("Test") + ks.SetUp(A) + ok, d = ks.IsHeld(A) if ok { - t.Fatalf("test was not released") + t.Fatalf("a was not released") } if d != 0 { - t.Fatalf("test hold was not reset") + t.Fatalf("a hold was not reset") } } diff --git a/lifecycle.go b/lifecycle.go index 795a0860..b202e8d8 100644 --- a/lifecycle.go +++ b/lifecycle.go @@ -4,11 +4,11 @@ import ( "image" "image/draw" - "github.com/oakmound/oak/v3/alg" - "github.com/oakmound/oak/v3/debugstream" + "github.com/oakmound/oak/v4/alg" + "github.com/oakmound/oak/v4/debugstream" "golang.org/x/mobile/event/lifecycle" - "github.com/oakmound/oak/v3/shiny/screen" + "github.com/oakmound/oak/v4/shiny/screen" ) func (w *Window) lifecycleLoop(s screen.Screen) { @@ -23,8 +23,8 @@ func (w *Window) lifecycleLoop(s screen.Screen) { // Apply that factor to the scale err = w.newWindow( - int32(w.config.Screen.X), - int32(w.config.Screen.Y), + w.config.Screen.X, + w.config.Screen.Y, int(float64(w.ScreenWidth)*w.config.Screen.Scale), int(float64(w.ScreenHeight)*w.config.Screen.Scale), ) @@ -37,7 +37,7 @@ func (w *Window) lifecycleLoop(s screen.Screen) { go w.inputLoop() <-w.quitCh - w.windowControl.Release() + w.Window.Release() } // Quit sends a signal to the window to close itself, closing the window and @@ -45,24 +45,24 @@ func (w *Window) lifecycleLoop(s screen.Screen) { // it must not be called again. func (w *Window) Quit() { // We could have hit this before the window was created - if w.windowControl == nil { + if w.Window == nil { close(w.quitCh) } else { - w.windowControl.Send(lifecycle.Event{To: lifecycle.StageDead}) + w.Window.Send(lifecycle.Event{To: lifecycle.StageDead}) } if w.config.EnableDebugConsole { debugstream.DefaultCommands.RemoveScope(w.ControllerID) } } -func (w *Window) newWindow(x, y int32, width, height int) error { +func (w *Window) newWindow(x, y, width, height int) error { // The window controller handles incoming hardware or platform events and // publishes image data to the screen. wC, err := w.windowController(w.screenControl, x, y, width, height) if err != nil { return err } - w.windowControl = wC + w.Window = wC return w.ChangeWindow(width, height) } @@ -77,13 +77,11 @@ func (w *Window) SetAspectRatio(xToY float64) { // ChangeWindow sets the width and height of the game window. Although exported, // calling it without a size event will probably not act as expected. func (w *Window) ChangeWindow(width, height int) error { - // Draw a black frame to cover up smears - // Todo: could restrict the black to -just- the area not covered by the - // scaled screen buffer + // Draw the background to cover up smears buff, err := w.screenControl.NewImage(image.Point{width, height}) if err == nil { draw.Draw(buff.RGBA(), buff.Bounds(), w.bkgFn(), zeroPoint, draw.Src) - w.windowControl.Upload(zeroPoint, buff, buff.Bounds()) + w.Window.Upload(zeroPoint, buff, buff.Bounds()) } else { return err } @@ -104,10 +102,15 @@ func (w *Window) ChangeWindow(width, height int) error { return nil } -// UpdateViewSize updates the size of this window's viewport. +// UpdateViewSize updates the size of this window's viewport. If the window has yet +// to be initialized, it will update ScreenWidth and ScreenHeight, and then exit. func (w *Window) UpdateViewSize(width, height int) error { w.ScreenWidth = width w.ScreenHeight = height + // this is being called before Init + if w.screenControl == nil { + return nil + } for i := 0; i < bufferCount; i++ { newBuffer, err := w.screenControl.NewImage(image.Point{width, height}) if err != nil { diff --git a/loading.go b/loading.go index 7293da61..e0d75271 100644 --- a/loading.go +++ b/loading.go @@ -3,10 +3,10 @@ package oak import ( "io/fs" - "github.com/oakmound/oak/v3/audio" - "github.com/oakmound/oak/v3/dlog" - "github.com/oakmound/oak/v3/fileutil" - "github.com/oakmound/oak/v3/render" + "github.com/oakmound/oak/v4/audio" + "github.com/oakmound/oak/v4/dlog" + "github.com/oakmound/oak/v4/fileutil" + "github.com/oakmound/oak/v4/render" "golang.org/x/sync/errgroup" ) @@ -27,18 +27,15 @@ func (w *Window) loadAssets(imageDir, audioDir string) { } else { err = audio.BatchLoad(audioDir) } - if err != nil { - return err - } dlog.Verb("Done Loading Audio") - return nil + return err }) dlog.ErrorCheck(eg.Wait()) } func (w *Window) endLoad() { dlog.Verb("Done Loading") - w.startupLoading = false + w.NextScene() } // SetFS updates all calls oak or oak's subpackages will make to read from the given filesystem. diff --git a/loading_test.go b/loading_test.go index 3d03e82b..ba242a69 100644 --- a/loading_test.go +++ b/loading_test.go @@ -4,7 +4,7 @@ import ( "os" "testing" - "github.com/oakmound/oak/v3/scene" + "github.com/oakmound/oak/v4/scene" ) func TestBatchLoad_HappyPath(t *testing.T) { diff --git a/mouse/bindings.go b/mouse/bindings.go deleted file mode 100644 index cedfe8dd..00000000 --- a/mouse/bindings.go +++ /dev/null @@ -1,17 +0,0 @@ -package mouse - -import "github.com/oakmound/oak/v3/event" - -// Binding will convert a function that accepts a typecast *mouse.Event into a generic event binding -// -// Example: -// bus.Bind(mouse.ClickOn, mouse.Binding(clickHandler)) -func Binding(fn func(event.CID, *Event) int) func(event.CID, interface{}) int { - return func(cid event.CID, iface interface{}) int { - me, ok := iface.(*Event) - if !ok { - return event.UnbindSingle - } - return fn(cid, me) - } -} diff --git a/mouse/default.go b/mouse/default.go index 0200513f..afda470e 100644 --- a/mouse/default.go +++ b/mouse/default.go @@ -1,6 +1,6 @@ package mouse -import "github.com/oakmound/oak/v3/collision" +import "github.com/oakmound/oak/v4/collision" // DefaultTree is a collision tree intended to be used by default if no other // is instantiated. Methods on a collision tree are duplicated as functions diff --git a/mouse/default_test.go b/mouse/default_test.go index d44fa0d9..64684588 100644 --- a/mouse/default_test.go +++ b/mouse/default_test.go @@ -3,7 +3,7 @@ package mouse import ( "testing" - "github.com/oakmound/oak/v3/collision" + "github.com/oakmound/oak/v4/collision" ) func TestDefaultFunctions(t *testing.T) { diff --git a/mouse/event.go b/mouse/event.go index c185bc7d..7ffd5309 100644 --- a/mouse/event.go +++ b/mouse/event.go @@ -1,19 +1,19 @@ package mouse import ( - "github.com/oakmound/oak/v3/alg/floatgeom" - "github.com/oakmound/oak/v3/collision" + "github.com/oakmound/oak/v4/alg/floatgeom" + "github.com/oakmound/oak/v4/collision" + "github.com/oakmound/oak/v4/event" ) var ( // LastEvent is the last triggered mouse event, // tracked for continuous mouse responsiveness on events // that don't take in a mouse event - LastEvent = NewZeroEvent(0, 0) + LastEvent = Event{} // LastPress is the last triggered mouse event, // where the mouse event was a press. - // If TrackMouseClicks is set to false then this will not be tracked - LastPress = NewZeroEvent(0, 0) + LastPress = Event{} ) // An Event is passed in through all Mouse related event bindings to @@ -22,7 +22,7 @@ var ( type Event struct { floatgeom.Point2 Button - Event string + EventType event.EventID[*Event] // Set StopPropagation on a mouse event to prevent it from triggering on // lower layers of mouse collision spaces while in flight @@ -30,19 +30,14 @@ type Event struct { } // NewEvent creates an event. -func NewEvent(x, y float64, button Button, event string) Event { +func NewEvent(x, y float64, button Button, ev event.EventID[*Event]) Event { return Event{ - Point2: floatgeom.Point2{x, y}, - Button: button, - Event: event, + Point2: floatgeom.Point2{x, y}, + Button: button, + EventType: ev, } } -// NewZeroEvent creates an event with no button or event name. -func NewZeroEvent(x, y float64) Event { - return NewEvent(x, y, ButtonNone, "") -} - // ToSpace converts a mouse event into a collision space func (e Event) ToSpace() *collision.Space { sp := collision.NewUnassignedSpace(e.X(), e.Y(), 0.1, 0.1) diff --git a/mouse/event_test.go b/mouse/event_test.go index 145bfcd7..97df2d0c 100644 --- a/mouse/event_test.go +++ b/mouse/event_test.go @@ -3,11 +3,11 @@ package mouse import ( "testing" - "github.com/oakmound/oak/v3/collision" + "github.com/oakmound/oak/v4/collision" ) func TestEventConversions(t *testing.T) { - me := NewZeroEvent(1.0, 1.0) + me := NewEvent(1.0, 1.0, ButtonLeft, Drag) s := me.ToSpace() Add(collision.NewUnassignedSpace(1.0, 1.0, .1, .1)) if len(Hits(s)) == 0 { diff --git a/mouse/events.go b/mouse/events.go new file mode 100644 index 00000000..628e4926 --- /dev/null +++ b/mouse/events.go @@ -0,0 +1,77 @@ +package mouse + +import "github.com/oakmound/oak/v4/event" + +var ( + // Press is triggered when a mouse key is pressed down + Press = event.RegisterEvent[*Event]() + // Release is triggered when a mouse key, pressed, is released + Release = event.RegisterEvent[*Event]() + // ScrollDown is triggered when a mouse's scroll wheel scrolls downward + ScrollDown = event.RegisterEvent[*Event]() + // ScrollUp is triggered when a mouse's scroll wheel scrolls upward + ScrollUp = event.RegisterEvent[*Event]() + // Click is triggered when a Release follows a press for the same mouse key without + // other mouse key presses intertwining. + Click = event.RegisterEvent[*Event]() + // Drag is triggered when the mouse is moved. + Drag = event.RegisterEvent[*Event]() + + // The 'On' Variants of all mouse events are triggered when a mouse event occurs on + // a specific entity in a mouse collision tree. + PressOn = event.RegisterEvent[*Event]() + ReleaseOn = event.RegisterEvent[*Event]() + ScrollDownOn = event.RegisterEvent[*Event]() + ScrollUpOn = event.RegisterEvent[*Event]() + ClickOn = event.RegisterEvent[*Event]() + DragOn = event.RegisterEvent[*Event]() + + // Relative variants are like 'On' variants, but their mouse position data is relative to + // the window's current viewport. E.g. if the viewport is at 100,100 and a click happens at + // 100,100 on the window-- Relative will report 100,100, and non-relative will report 200,200. + // TODO: re-evaluate relative vs non-relative mouse events + RelativePressOn = event.RegisterEvent[*Event]() + RelativeReleaseOn = event.RegisterEvent[*Event]() + RelativeScrollDownOn = event.RegisterEvent[*Event]() + RelativeScrollUpOn = event.RegisterEvent[*Event]() + RelativeClickOn = event.RegisterEvent[*Event]() + RelativeDragOn = event.RegisterEvent[*Event]() +) + +// EventOn converts a generic positioned mouse event into its variant indicating +// it occurred on a CallerID targetted entity +func EventOn(ev event.EventID[*Event]) (event.EventID[*Event], bool) { + switch ev { + case Press: + return PressOn, true + case Release: + return ReleaseOn, true + case ScrollDown: + return ScrollDownOn, true + case ScrollUp: + return ScrollUpOn, true + case Click: + return ClickOn, true + case Drag: + return DragOn, true + } + return event.EventID[*Event]{}, false +} + +func EventRelative(ev event.EventID[*Event]) (event.EventID[*Event], bool) { + switch ev { + case PressOn: + return RelativePressOn, true + case ReleaseOn: + return RelativeReleaseOn, true + case ScrollDownOn: + return RelativeScrollDownOn, true + case ScrollUpOn: + return RelativeScrollUpOn, true + case ClickOn: + return RelativeClickOn, true + case DragOn: + return RelativeDragOn, true + } + return event.EventID[*Event]{}, false +} diff --git a/mouse/events_test.go b/mouse/events_test.go new file mode 100644 index 00000000..112b328b --- /dev/null +++ b/mouse/events_test.go @@ -0,0 +1,67 @@ +package mouse + +import ( + "testing" + + "github.com/oakmound/oak/v4/event" +) + +func TestEventOn(t *testing.T) { + t.Run("AllEvents", func(t *testing.T) { + if ev2, ok := EventOn(Press); !ok || ev2 != PressOn { + t.Error("Press was not matched to PressOn") + } + if ev2, ok := EventOn(Release); !ok || ev2 != ReleaseOn { + t.Error("Release was not matched to ReleaseOn") + } + if ev2, ok := EventOn(ScrollDown); !ok || ev2 != ScrollDownOn { + t.Error("ScrollDown was not matched to ScrollDownOn") + } + if ev2, ok := EventOn(ScrollUp); !ok || ev2 != ScrollUpOn { + t.Error("ScrollUp was not matched to ScrollUpOn") + } + if ev2, ok := EventOn(Click); !ok || ev2 != ClickOn { + t.Error("Click was not matched to ClickOn") + } + if ev2, ok := EventOn(Drag); !ok || ev2 != DragOn { + t.Error("Drag was not matched to DragOn") + } + }) + t.Run("Unknown", func(t *testing.T) { + ev := event.RegisterEvent[*Event]() + _, ok := EventOn(ev) + if ok { + t.Error("EventOn should have returned false for an unknown event") + } + }) +} + +func TestEventRelative(t *testing.T) { + t.Run("AllEvents", func(t *testing.T) { + if ev2, ok := EventRelative(PressOn); !ok || ev2 != RelativePressOn { + t.Error("PressOn was not matched to RelativePressOn") + } + if ev2, ok := EventRelative(ReleaseOn); !ok || ev2 != RelativeReleaseOn { + t.Error("ReleaseOn was not matched to RelativeReleaseOn") + } + if ev2, ok := EventRelative(ScrollDownOn); !ok || ev2 != RelativeScrollDownOn { + t.Error("ScrollDownOn was not matched to RelativeScrollDownOn") + } + if ev2, ok := EventRelative(ScrollUpOn); !ok || ev2 != RelativeScrollUpOn { + t.Error("ScrollUpOn was not matched to RelativeScrollUpOn") + } + if ev2, ok := EventRelative(ClickOn); !ok || ev2 != RelativeClickOn { + t.Error("ClickOn was not matched to RelativeClickOn") + } + if ev2, ok := EventRelative(DragOn); !ok || ev2 != RelativeDragOn { + t.Error("DragOn was not matched to RelativeDragOn") + } + }) + t.Run("Unknown", func(t *testing.T) { + ev := event.RegisterEvent[*Event]() + _, ok := EventRelative(ev) + if ok { + t.Error("EventRelative should have returned false for an unknown event") + } + }) +} diff --git a/mouse/mouse.go b/mouse/mouse.go index 7fcd47c1..a6f93cb3 100644 --- a/mouse/mouse.go +++ b/mouse/mouse.go @@ -1,6 +1,7 @@ package mouse import ( + "github.com/oakmound/oak/v4/event" "golang.org/x/mobile/event/mouse" ) @@ -21,7 +22,7 @@ const ( ) // GetEventName returns a string event name given some mobile/mouse information -func GetEventName(d mouse.Direction, b mouse.Button) string { +func GetEvent(d mouse.Direction, b mouse.Button) event.EventID[*Event] { switch d { case mouse.DirPress: return Press diff --git a/mouse/mouse_test.go b/mouse/mouse_test.go index 8d04a33e..ac0686d3 100644 --- a/mouse/mouse_test.go +++ b/mouse/mouse_test.go @@ -7,19 +7,19 @@ import ( ) func TestEventNameIdentity(t *testing.T) { - if GetEventName(mouse.DirPress, 0) != "MousePress" { - t.Fatalf("event name mismatch for event %v, expected %v", mouse.DirPress, "MousePress") + if GetEvent(mouse.DirPress, 0) != Press { + t.Fatalf("event mismatch for event %v, expected %v", mouse.DirPress, "MousePress") } - if GetEventName(mouse.DirRelease, 0) != "MouseRelease" { - t.Fatalf("event name mismatch for event %v, expected %v", mouse.DirRelease, "MouseRelease") + if GetEvent(mouse.DirRelease, 0) != Release { + t.Fatalf("event mismatch for event %v, expected %v", mouse.DirRelease, "MouseRelease") } - if GetEventName(mouse.DirNone, -2) != "MouseScrollDown" { - t.Fatalf("event name mismatch for event %v, expected %v", mouse.DirNone, "MouseScrollDown") + if GetEvent(mouse.DirNone, -2) != ScrollDown { + t.Fatalf("event mismatch for event %v, expected %v", mouse.DirNone, "MouseScrollDown") } - if GetEventName(mouse.DirNone, -1) != "MouseScrollUp" { - t.Fatalf("event name mismatch for event %v, expected %v", mouse.DirNone, "MouseScrollUp") + if GetEvent(mouse.DirNone, -1) != ScrollUp { + t.Fatalf("event mismatch for event %v, expected %v", mouse.DirNone, "MouseScrollUp") } - if GetEventName(mouse.DirNone, 0) != "MouseDrag" { - t.Fatalf("event name mismatch for event %v, expected %v", mouse.DirNone, "MouseDrag") + if GetEvent(mouse.DirNone, 0) != Drag { + t.Fatalf("event mismatch for event %v, expected %v", mouse.DirNone, "MouseDrag") } } diff --git a/mouse/onCollision.go b/mouse/onCollision.go index 31af0bde..36fbb469 100644 --- a/mouse/onCollision.go +++ b/mouse/onCollision.go @@ -3,14 +3,15 @@ package mouse import ( "errors" - "github.com/oakmound/oak/v3/collision" - "github.com/oakmound/oak/v3/event" + "github.com/oakmound/oak/v4/collision" + "github.com/oakmound/oak/v4/event" ) // CollisionPhase is a component that can be placed into another struct to // enable PhaseCollision on the struct. See PhaseCollision. type CollisionPhase struct { OnCollisionS *collision.Space + CallerMap *event.CallerMap LastEvent *Event wasTouching bool @@ -20,6 +21,10 @@ func (cp *CollisionPhase) getCollisionPhase() *CollisionPhase { return cp } +func (cp *CollisionPhase) CID() event.CallerID { + return cp.OnCollisionS.CID +} + type collisionPhase interface { getCollisionPhase() *CollisionPhase } @@ -27,32 +32,35 @@ type collisionPhase interface { // PhaseCollision binds to the entity behind the space's CID so that it will // receive MouseCollisionStart and MouseCollisionStop events, appropriately when // the mouse begins to hover or stops hovering over the input space. -func PhaseCollision(s *collision.Space) error { - en := s.CID.E() +func PhaseCollision(s *collision.Space, handler event.Handler) error { + en := handler.GetCallerMap().GetEntity(s.CID) if cp, ok := en.(collisionPhase); ok { oc := cp.getCollisionPhase() oc.OnCollisionS = s - s.CID.Bind(event.Enter, phaseCollisionEnter) + oc.CallerMap = handler.GetCallerMap() + handler.UnsafeBind(event.Enter.UnsafeEventID, s.CID, phaseCollisionEnter) return nil } return errors.New("This space's entity does not implement collisionPhase") } // MouseCollisionStart/Stop: see collision Start/Stop, for mouse collision -// Payload: (*mouse.Event) -const ( - Start = "MouseCollisionStart" - Stop = "MouseCollisionStop" +var ( + Start = event.RegisterEvent[*Event]() + Stop = event.RegisterEvent[*Event]() ) -func phaseCollisionEnter(id event.CID, nothing interface{}) int { - e := id.E().(collisionPhase) +func phaseCollisionEnter(id event.CallerID, handler event.Handler, _ interface{}) event.Response { + e, ok := handler.GetCallerMap().GetEntity(id).(collisionPhase) + if !ok { + return event.ResponseUnbindThisBinding + } oc := e.getCollisionPhase() if oc == nil || oc.OnCollisionS == nil { return 0 } - // TODO: think about how this can more cleanly work with multiple controllers + // TODO: think about how this can more cleanly work with multiple windows ev := oc.LastEvent if ev == nil { ev = &LastEvent @@ -63,12 +71,12 @@ func phaseCollisionEnter(id event.CID, nothing interface{}) int { if oc.OnCollisionS.Contains(ev.ToSpace()) { if !oc.wasTouching { - id.Trigger(Start, ev) + event.TriggerForCallerOn(handler, id, Start, ev) oc.wasTouching = true } } else { if oc.wasTouching { - id.Trigger(Stop, ev) + event.TriggerForCallerOn(handler, id, Stop, ev) oc.wasTouching = false } } diff --git a/mouse/onCollision_test.go b/mouse/onCollision_test.go index fbff918d..2c4fdd89 100644 --- a/mouse/onCollision_test.go +++ b/mouse/onCollision_test.go @@ -4,59 +4,63 @@ import ( "testing" "time" - "github.com/oakmound/oak/v3/alg/floatgeom" - "github.com/oakmound/oak/v3/collision" - "github.com/oakmound/oak/v3/event" + "github.com/oakmound/oak/v4/alg/floatgeom" + "github.com/oakmound/oak/v4/collision" + "github.com/oakmound/oak/v4/event" ) type cphase struct { CollisionPhase -} - -func (cp *cphase) Init() event.CID { - return event.NextID(cp) + callers *event.CallerMap } func TestCollisionPhase(t *testing.T) { - go event.ResolveChanges() + b := event.NewBus(event.NewCallerMap()) go func() { for { <-time.After(5 * time.Millisecond) - <-event.TriggerBack(event.Enter, nil) + <-event.TriggerOn(b, event.Enter, event.EnterPayload{}) } }() - cp := cphase{} - cid := cp.Init() + cp := &cphase{} + cid := b.GetCallerMap().Register(cp) s := collision.NewSpace(10, 10, 10, 10, cid) - if PhaseCollision(s) != nil { - t.Fatalf("phase collision errored") + err := PhaseCollision(s, b) + if err != nil { + t.Fatalf("phase collision failed: %v", err) } - var active bool - cid.Bind("MouseCollisionStart", func(event.CID, interface{}) int { - active = true + activeCh := make(chan bool, 5) + b1 := event.Bind(b, Start, cp, func(_ *cphase, _ *Event) event.Response { + activeCh <- true return 0 }) - cid.Bind("MouseCollisionStop", func(event.CID, interface{}) int { - active = false + b2 := event.Bind(b, Stop, cp, func(_ *cphase, _ *Event) event.Response { + activeCh <- false return 0 }) - time.Sleep(200 * time.Millisecond) + <-b1.Bound + <-b2.Bound LastEvent = Event{ Point2: floatgeom.Point2{10, 10}, } - time.Sleep(200 * time.Millisecond) - if !active { - t.Fatalf("phase collision did not trigger") + if active := <-activeCh; !active { + t.Fatalf("collision should be active") } + LastEvent = Event{ Point2: floatgeom.Point2{21, 21}, } time.Sleep(200 * time.Millisecond) - if active { - t.Fatalf("phase collision triggered innapropriately") + if active := <-activeCh; active { + t.Fatalf("collision should be inactive") } - s = collision.NewSpace(10, 10, 10, 10, 5) - if PhaseCollision(s) == nil { - t.Fatalf("phase collision did not error on invalid space") +} + +func TestPhaseCollision_Unembedded(t *testing.T) { + t.Parallel() + s3 := collision.NewSpace(10, 10, 10, 10, 5) + err := PhaseCollision(s3, event.DefaultBus) + if err == nil { + t.Fatalf("phase collision should have failed") } } diff --git a/mouse/strings.go b/mouse/strings.go deleted file mode 100644 index cdc1c24e..00000000 --- a/mouse/strings.go +++ /dev/null @@ -1,19 +0,0 @@ -package mouse - -// Mouse events: MousePress, MouseRelease, MouseScrollDown, MouseScrollUp, MouseDrag -// Payload: (*mouse.Event) details of the mouse event -const ( - Press = "MousePress" - Release = "MouseRelease" - ScrollDown = "MouseScrollDown" - ScrollUp = "MouseScrollUp" - Click = "MouseClick" - Drag = "MouseDrag" - // - PressOn = Press + "On" - ReleaseOn = Release + "On" - ScrollDownOn = ScrollDown + "On" - ScrollUpOn = ScrollUp + "On" - ClickOn = Click + "On" - DragOn = Drag + "On" -) diff --git a/physics/force.go b/physics/force.go index a0b8f2f7..de316caf 100644 --- a/physics/force.go +++ b/physics/force.go @@ -1,7 +1,7 @@ package physics import ( - "github.com/oakmound/oak/v3/oakerr" + "github.com/oakmound/oak/v4/oakerr" ) const frozen = -64 diff --git a/physics/vector.go b/physics/vector.go index a19a55e7..b36a225e 100644 --- a/physics/vector.go +++ b/physics/vector.go @@ -3,7 +3,7 @@ package physics import ( "math" - "github.com/oakmound/oak/v3/alg" + "github.com/oakmound/oak/v4/alg" ) // A Vector is a two-dimensional point or vector used throughout oak diff --git a/physics/vector_test.go b/physics/vector_test.go index 2ad81e81..cb727dbb 100644 --- a/physics/vector_test.go +++ b/physics/vector_test.go @@ -3,7 +3,7 @@ package physics import ( "testing" - "github.com/oakmound/oak/v3/alg" + "github.com/oakmound/oak/v4/alg" ) func TestVectorFuncs(t *testing.T) { diff --git a/render/bachload_test.go b/render/bachload_test.go index 09e4697c..2d67bf2e 100644 --- a/render/bachload_test.go +++ b/render/bachload_test.go @@ -4,7 +4,7 @@ import ( "errors" "testing" - "github.com/oakmound/oak/v3/oakerr" + "github.com/oakmound/oak/v4/oakerr" ) func TestBlankBatchLoad_BadBaseFolder(t *testing.T) { diff --git a/render/batchload.go b/render/batchload.go index 27f8165b..8400de4c 100644 --- a/render/batchload.go +++ b/render/batchload.go @@ -8,10 +8,10 @@ import ( "strconv" "sync" - "github.com/oakmound/oak/v3/alg/intgeom" - "github.com/oakmound/oak/v3/dlog" - "github.com/oakmound/oak/v3/fileutil" - "github.com/oakmound/oak/v3/oakerr" + "github.com/oakmound/oak/v4/alg/intgeom" + "github.com/oakmound/oak/v4/dlog" + "github.com/oakmound/oak/v4/fileutil" + "github.com/oakmound/oak/v4/oakerr" ) // BatchLoad loads subdirectories from the given base folder and imports all files, diff --git a/render/bezier.go b/render/bezier.go index 1e0c3974..49ce88ec 100644 --- a/render/bezier.go +++ b/render/bezier.go @@ -4,9 +4,9 @@ import ( "image" "image/color" - "github.com/oakmound/oak/v3/alg" - "github.com/oakmound/oak/v3/alg/intgeom" - "github.com/oakmound/oak/v3/shape" + "github.com/oakmound/oak/v4/alg" + "github.com/oakmound/oak/v4/alg/intgeom" + "github.com/oakmound/oak/v4/shape" ) // BezierLine converts a bezier into a line sprite. diff --git a/render/bezier_test.go b/render/bezier_test.go index 8172c12a..4daa242b 100644 --- a/render/bezier_test.go +++ b/render/bezier_test.go @@ -4,7 +4,7 @@ import ( "image/color" "testing" - "github.com/oakmound/oak/v3/shape" + "github.com/oakmound/oak/v4/shape" ) func TestSimpleBezierLine(t *testing.T) { diff --git a/render/cache.go b/render/cache.go index d3bca480..7a1a2e32 100644 --- a/render/cache.go +++ b/render/cache.go @@ -5,7 +5,7 @@ import ( "sync" "github.com/golang/freetype/truetype" - "github.com/oakmound/oak/v3/alg/intgeom" + "github.com/oakmound/oak/v4/alg/intgeom" ) // DefaultCache is the receiver for package level sprites, sheets, and font loading operations. diff --git a/render/cache_test.go b/render/cache_test.go index 2782c4a3..c194eed0 100644 --- a/render/cache_test.go +++ b/render/cache_test.go @@ -17,4 +17,14 @@ func TestCache_Clear(t *testing.T) { if err == nil { t.Fatal("get jeremy should have failed post-Clear") } + file = "testdata/assets/fonts/luxisr.ttf" + _, err = LoadFont(file) + if err != nil { + t.Fatalf("load luxisr should have succeeded: %v", err) + } + DefaultCache.Clear(file) + _, err = GetFont(file) + if err == nil { + t.Fatal("get luxisr should have failed post-Clear") + } } diff --git a/render/colorbox.go b/render/colorbox.go index 4eac32a5..b496b5b3 100644 --- a/render/colorbox.go +++ b/render/colorbox.go @@ -5,7 +5,7 @@ import ( "image/color" "image/draw" - "github.com/oakmound/oak/v3/alg/intgeom" + "github.com/oakmound/oak/v4/alg/intgeom" ) // NewColorBox returns a Sprite full of a given color with the given dimensions diff --git a/render/compositeM.go b/render/compositeM.go index 0438f006..17ec5c9d 100644 --- a/render/compositeM.go +++ b/render/compositeM.go @@ -4,8 +4,8 @@ import ( "image" "image/draw" - "github.com/oakmound/oak/v3/alg/floatgeom" - "github.com/oakmound/oak/v3/render/mod" + "github.com/oakmound/oak/v4/alg/floatgeom" + "github.com/oakmound/oak/v4/render/mod" ) // CompositeM Types display all of their parts at the same time, diff --git a/render/compositeM_test.go b/render/compositeM_test.go index ee8c827d..a8001dc5 100644 --- a/render/compositeM_test.go +++ b/render/compositeM_test.go @@ -6,8 +6,8 @@ import ( "reflect" "testing" - "github.com/oakmound/oak/v3/alg/floatgeom" - "github.com/oakmound/oak/v3/render/mod" + "github.com/oakmound/oak/v4/alg/floatgeom" + "github.com/oakmound/oak/v4/render/mod" ) func TestComposite(t *testing.T) { diff --git a/render/compositeR.go b/render/compositeR.go index 3c63cf77..ba9220c1 100644 --- a/render/compositeR.go +++ b/render/compositeR.go @@ -5,8 +5,8 @@ import ( "image/draw" "sync" - "github.com/oakmound/oak/v3/alg/floatgeom" - "github.com/oakmound/oak/v3/alg/intgeom" + "github.com/oakmound/oak/v4/alg/floatgeom" + "github.com/oakmound/oak/v4/alg/intgeom" ) // A CompositeR is equivalent to a CompositeM for Renderables instead of diff --git a/render/compositeR_test.go b/render/compositeR_test.go index 3beb08fd..20cdf0b0 100644 --- a/render/compositeR_test.go +++ b/render/compositeR_test.go @@ -6,8 +6,8 @@ import ( "reflect" "testing" - "github.com/oakmound/oak/v3/alg/floatgeom" - "github.com/oakmound/oak/v3/alg/intgeom" + "github.com/oakmound/oak/v4/alg/floatgeom" + "github.com/oakmound/oak/v4/alg/intgeom" ) func TestCompositeR(t *testing.T) { diff --git a/render/curve.go b/render/curve.go index 8406113c..8fe72c8b 100644 --- a/render/curve.go +++ b/render/curve.go @@ -5,8 +5,8 @@ import ( "image/color" "math" - "github.com/oakmound/oak/v3/alg" - "github.com/oakmound/oak/v3/alg/floatgeom" + "github.com/oakmound/oak/v4/alg" + "github.com/oakmound/oak/v4/alg/floatgeom" ) // NewCircle creates a sprite and draws a circle onto it diff --git a/render/decoder.go b/render/decoder.go index 81a17755..b93ce20f 100644 --- a/render/decoder.go +++ b/render/decoder.go @@ -2,14 +2,9 @@ package render import ( "image" - "image/gif" - "image/jpeg" - "image/png" "io" - "github.com/oakmound/oak/v3/oakerr" - - "golang.org/x/image/bmp" + "github.com/oakmound/oak/v4/oakerr" ) // Decoder functions convert arbitrary readers to images. @@ -22,20 +17,8 @@ type Decoder func(io.Reader) (image.Image, error) type CfgDecoder func(io.Reader) (image.Config, error) var ( - fileDecoders = map[string]Decoder{ - ".jpeg": jpeg.Decode, - ".jpg": jpeg.Decode, - ".gif": gif.Decode, - ".png": png.Decode, - ".bmp": bmp.Decode, - } - cfgDecoders = map[string]CfgDecoder{ - ".jpeg": jpeg.DecodeConfig, - ".jpg": jpeg.DecodeConfig, - ".gif": gif.DecodeConfig, - ".png": png.DecodeConfig, - ".bmp": bmp.DecodeConfig, - } + fileDecoders = map[string]Decoder{} + cfgDecoders = map[string]CfgDecoder{} ) // RegisterDecoder adds a decoder to the set of image decoders diff --git a/render/default_decoders.go b/render/default_decoders.go new file mode 100644 index 00000000..ea0c1ca3 --- /dev/null +++ b/render/default_decoders.go @@ -0,0 +1,26 @@ +//go:build !noimages +// +build !noimages + +package render + +import ( + "image/gif" + "image/jpeg" + "image/png" + + "golang.org/x/image/bmp" +) + +func init() { + // Register standard image decoders. If provided with the build tag 'noimages', this is skipped. + RegisterDecoder(".jpeg", jpeg.Decode) + RegisterDecoder(".jpg", jpeg.Decode) + RegisterDecoder(".gif", gif.Decode) + RegisterDecoder(".png", png.Decode) + RegisterDecoder(".bmp", bmp.Decode) + RegisterCfgDecoder(".jpeg", jpeg.DecodeConfig) + RegisterCfgDecoder(".jpg", jpeg.DecodeConfig) + RegisterCfgDecoder(".gif", gif.DecodeConfig) + RegisterCfgDecoder(".png", png.DecodeConfig) + RegisterCfgDecoder(".bmp", bmp.DecodeConfig) +} diff --git a/render/draw.go b/render/draw.go index 81287f14..17648e53 100644 --- a/render/draw.go +++ b/render/draw.go @@ -10,8 +10,7 @@ var ( emptyRenderable = NewColorBox(1, 1, color.RGBA{0, 0, 0, 0}) ) -// EmptyRenderable returns a minimal, 1-width and height pseudo-nil -// Renderable (and Modifiable) +// EmptyRenderable returns a minimal, 1-width and height pseudo-nil Renderable func EmptyRenderable() Modifiable { return emptyRenderable.Copy() } diff --git a/render/drawHeap.go b/render/drawHeap.go index 23cf4ec5..da56eb61 100644 --- a/render/drawHeap.go +++ b/render/drawHeap.go @@ -4,7 +4,7 @@ import ( "image/draw" "sync" - "github.com/oakmound/oak/v3/alg/intgeom" + "github.com/oakmound/oak/v4/alg/intgeom" ) // A RenderableHeap manages a set of renderables to be drawn in explicit layered diff --git a/render/drawHeap_test.go b/render/drawHeap_test.go index 6012e5fb..dd981726 100644 --- a/render/drawHeap_test.go +++ b/render/drawHeap_test.go @@ -5,7 +5,7 @@ import ( "image/color" "testing" - "github.com/oakmound/oak/v3/alg/intgeom" + "github.com/oakmound/oak/v4/alg/intgeom" ) const heapLoops = 2000 diff --git a/render/drawStack.go b/render/drawStack.go index dd268c89..2b3188c5 100644 --- a/render/drawStack.go +++ b/render/drawStack.go @@ -3,8 +3,8 @@ package render import ( "image/draw" - "github.com/oakmound/oak/v3/alg/intgeom" - "github.com/oakmound/oak/v3/oakerr" + "github.com/oakmound/oak/v4/alg/intgeom" + "github.com/oakmound/oak/v4/oakerr" ) var ( diff --git a/render/drawStack_test.go b/render/drawStack_test.go index 8ff14adc..9da7c7fb 100644 --- a/render/drawStack_test.go +++ b/render/drawStack_test.go @@ -6,7 +6,7 @@ import ( "reflect" "testing" - "github.com/oakmound/oak/v3/alg/intgeom" + "github.com/oakmound/oak/v4/alg/intgeom" ) func TestDrawStack(t *testing.T) { @@ -26,6 +26,10 @@ func TestDrawStack(t *testing.T) { if len(GlobalDrawStack.as) != 1 { t.Fatalf("global draw stack did not have one length after pop") } + cp := GlobalDrawStack.Copy() + if len(cp.toPush) != len(GlobalDrawStack.toPush) { + t.Fatalf("copy failed to copy push length") + } } func TestDrawStack_Draw(t *testing.T) { diff --git a/render/font.go b/render/font.go index cbaf84f1..a95dd7cf 100644 --- a/render/font.go +++ b/render/font.go @@ -12,9 +12,9 @@ import ( "golang.org/x/image/font" "golang.org/x/image/math/fixed" - "github.com/oakmound/oak/v3/alg/intgeom" - "github.com/oakmound/oak/v3/fileutil" - "github.com/oakmound/oak/v3/oakerr" + "github.com/oakmound/oak/v4/alg/intgeom" + "github.com/oakmound/oak/v4/fileutil" + "github.com/oakmound/oak/v4/oakerr" ) var ( diff --git a/render/font_test.go b/render/font_test.go index 625c3117..12f86913 100644 --- a/render/font_test.go +++ b/render/font_test.go @@ -7,7 +7,7 @@ import ( "math/rand" "testing" - "github.com/oakmound/oak/v3/oakerr" + "github.com/oakmound/oak/v4/oakerr" "golang.org/x/image/colornames" ) @@ -57,6 +57,37 @@ func TestFontGenerator_validate(t *testing.T) { } } +func TestFontGenerator_Generate_Failure(t *testing.T) { + t.Run("BadRawFile", func(t *testing.T) { + fg := FontGenerator{ + RawFile: []byte("notafontfile"), + Color: image.NewUniform(color.RGBA{255, 0, 0, 255}), + FontOptions: FontOptions{ + Size: 13.0, + DPI: 44.0, + }, + } + _, err := fg.Generate() + if err == nil { + t.Fatalf("generate should have failed") + } + }) + t.Run("BadLoadFont", func(t *testing.T) { + fg := FontGenerator{ + File: "file that does not exist", + Color: image.NewUniform(color.RGBA{255, 0, 0, 255}), + FontOptions: FontOptions{ + Size: 13.0, + DPI: 44.0, + }, + } + _, err := fg.Generate() + if err == nil { + t.Fatalf("generate should have failed") + } + }) +} + func TestFontGenerator_Generate_Success(t *testing.T) { fg := FontGenerator{ File: "testdata/assets/fonts/luxisr.ttf", @@ -71,3 +102,109 @@ func TestFontGenerator_Generate_Success(t *testing.T) { t.Fatalf("generate failed: %v", err) } } + +func TestFont_Height(t *testing.T) { + ht := rand.Float64() * 10 + fg := FontGenerator{ + File: "testdata/assets/fonts/luxisr.ttf", + Color: image.NewUniform(color.RGBA{255, 0, 0, 255}), + FontOptions: FontOptions{ + Size: ht, + DPI: 44.0, + }, + } + f, err := fg.Generate() + if err != nil { + t.Fatalf("generate failed: %v", err) + } + if f.Height() != ht { + t.Fatalf("size did not match height: got %v expected %v", f.Height(), ht) + } +} + +func TestFont_RegenerateWith(t *testing.T) { + fg := FontGenerator{ + File: "testdata/assets/fonts/luxisr.ttf", + Color: image.NewUniform(color.RGBA{255, 0, 0, 255}), + FontOptions: FontOptions{ + Size: 13.0, + DPI: 44.0, + }, + } + f, err := fg.Generate() + if err != nil { + t.Fatalf("generate failed: %v", err) + } + f2, err := f.RegenerateWith(func(fg FontGenerator) FontGenerator { + fg.Size = 100 + return fg + }) + if err != nil { + t.Fatalf("regenerate failed: %v", err) + } + if f2.Height() != 100 { + t.Fatalf("size did not match height: got %v expected %v", f.Height(), 100) + } +} + +func TestCache_LoadFont(t *testing.T) { + t.Run("NotExists", func(t *testing.T) { + c := NewCache() + _, err := c.LoadFont("bogusfilepath") + if err == nil { + t.Fatal("expected error loading bad file") + } + }) + t.Run("NotFontFile", func(t *testing.T) { + c := NewCache() + _, err := c.LoadFont("testdata/assets/images/devfile.pdn") + if err == nil { + t.Fatal("expected error loading non-font") + } + }) + t.Run("GetCached", func(t *testing.T) { + c := NewCache() + _, err := c.LoadFont("testdata/assets/fonts/luxisr.ttf") + if err != nil { + t.Fatal("failed to load font into cache") + } + _, err = c.GetFont("luxisr.ttf") + if err != nil { + t.Fatalf("failed to get cached font: %v", err) + } + }) + t.Run("GetUncached", func(t *testing.T) { + c := NewCache() + _, err := c.GetFont("luxisr.ttf") + if err == nil { + t.Fatalf("expected error getting uncached font") + } + }) +} + +func TestFont_Fallback(t *testing.T) { + fg := FontGenerator{ + File: "testdata/assets/fonts/luxisr.ttf", + Color: image.NewUniform(color.RGBA{255, 0, 0, 255}), + FontOptions: FontOptions{ + Size: 13.0, + DPI: 44.0, + }, + } + f, err := fg.Generate() + if err != nil { + t.Fatalf("generate failed: %v", err) + } + + fg.File = "testdata/assets/fonts/seguiemj.ttf" + emjfont, err := fg.Generate() + if err != nil { + t.Fatalf("generate failed: %v", err) + } + + f.Fallbacks = append(f.Fallbacks, emjfont) + + f.MeasureString("a😀b😃c😄d😁e本") + txt := f.NewText("a😀b😃c😄d😁e本", 0, 0) + txt.Draw(image.NewRGBA(image.Rect(0, 0, 200, 200)), 0, 0) +} diff --git a/render/fps.go b/render/fps.go index 9b10a0a4..d7992b25 100644 --- a/render/fps.go +++ b/render/fps.go @@ -4,7 +4,7 @@ import ( "image/draw" "time" - "github.com/oakmound/oak/v3/timing" + "github.com/oakmound/oak/v4/timing" ) const ( diff --git a/render/interfaceFeatures.go b/render/interfaceFeatures.go index 14bdb756..803a88d4 100644 --- a/render/interfaceFeatures.go +++ b/render/interfaceFeatures.go @@ -1,6 +1,6 @@ package render -import "github.com/oakmound/oak/v3/event" +import "github.com/oakmound/oak/v4/event" // NonStatic types are not always static. If something is not NonStatic, // it is equivalent to having IsStatic always return true. @@ -11,7 +11,7 @@ type NonStatic interface { // Triggerable types can have an ID set so when their animations finish, // they trigger AnimationEnd on that ID. type Triggerable interface { - SetTriggerID(event.CID) + SetTriggerID(event.CallerID) } type updates interface { diff --git a/render/layered.go b/render/layered.go index 97031f8a..2c2024db 100644 --- a/render/layered.go +++ b/render/layered.go @@ -1,7 +1,7 @@ package render import ( - "github.com/oakmound/oak/v3/physics" + "github.com/oakmound/oak/v4/physics" ) const ( diff --git a/render/line.go b/render/line.go index 09c8e217..f37ba9d8 100644 --- a/render/line.go +++ b/render/line.go @@ -5,7 +5,7 @@ import ( "image/color" "math" - "github.com/oakmound/oak/v3/alg/range/colorrange" + "github.com/oakmound/oak/v4/alg/span" ) // Todo: @@ -30,7 +30,7 @@ func NewThickLine(x1, y1, x2, y2 float64, c color.Color, thickness int) *Sprite // NewGradientLine returns a Line that has some value of thickness along with a start and end color func NewGradientLine(x1, y1, x2, y2 float64, c1, c2 color.Color, thickness int) *Sprite { - colorer := colorrange.NewLinear(c1, c2).Percentile + colorer := span.NewLinearColor(c1, c2).Percentile return NewColoredLine(x1, y1, x2, y2, colorer, thickness) } @@ -57,7 +57,7 @@ func DrawThickLine(rgba *image.RGBA, x1, y1, x2, y2 int, c color.Color, thicknes //DrawGradientLine acts like DrawThickLine but also applies a gradient to the line func DrawGradientLine(rgba *image.RGBA, x1, y1, x2, y2 int, c1, c2 color.Color, thickness int) { - colorer := colorrange.NewLinear(c1, c2).Percentile + colorer := span.NewLinearColor(c1, c2).Percentile DrawLineColored(rgba, x1, y1, x2, y2, thickness, colorer) } diff --git a/render/loadsheet.go b/render/loadsheet.go index cd513dc4..1db88a37 100644 --- a/render/loadsheet.go +++ b/render/loadsheet.go @@ -4,8 +4,8 @@ import ( "image" "path/filepath" - "github.com/oakmound/oak/v3/alg/intgeom" - "github.com/oakmound/oak/v3/oakerr" + "github.com/oakmound/oak/v4/alg/intgeom" + "github.com/oakmound/oak/v4/oakerr" ) // LoadSheet loads a file in some directory with sheets of (w,h) sized sprites. diff --git a/render/loadsheet_test.go b/render/loadsheet_test.go index 8d9e05f2..1a079522 100644 --- a/render/loadsheet_test.go +++ b/render/loadsheet_test.go @@ -6,8 +6,8 @@ import ( "path/filepath" "testing" - "github.com/oakmound/oak/v3/alg/intgeom" - "github.com/oakmound/oak/v3/oakerr" + "github.com/oakmound/oak/v4/alg/intgeom" + "github.com/oakmound/oak/v4/oakerr" ) var ( diff --git a/render/loadsprite.go b/render/loadsprite.go index 21c38be7..34172bc8 100644 --- a/render/loadsprite.go +++ b/render/loadsprite.go @@ -8,8 +8,8 @@ import ( "path/filepath" "strings" - "github.com/oakmound/oak/v3/fileutil" - "github.com/oakmound/oak/v3/oakerr" + "github.com/oakmound/oak/v4/fileutil" + "github.com/oakmound/oak/v4/oakerr" ) func loadSpriteNoCache(file string, maxFileSize int64) (*image.RGBA, error) { diff --git a/render/logicfps.go b/render/logicfps.go index 8c06d81a..f6dd71ac 100644 --- a/render/logicfps.go +++ b/render/logicfps.go @@ -3,25 +3,22 @@ package render import ( "time" - "github.com/oakmound/oak/v3/event" - "github.com/oakmound/oak/v3/timing" + "github.com/oakmound/oak/v4/event" + "github.com/oakmound/oak/v4/timing" ) // LogicFPS is a Stackable that will draw the logical fps onto the screen when a part // of the draw stack. type LogicFPS struct { - event.CID + event.CallerID *Text fps int lastTime time.Time Smoothing float64 } -// Init satisfies event.Entity -func (lf *LogicFPS) Init() event.CID { - id := event.NextID(lf) - lf.CID = id - return id +func (lf LogicFPS) CID() event.CallerID { + return lf.CallerID } // NewLogicFPS returns a LogicFPS, which will render a counter of how fast it receives event.Enter events. @@ -38,14 +35,14 @@ func NewLogicFPS(smoothing float64, font *Font, x, y float64) *LogicFPS { lastTime: time.Now(), } lf.Text = font.NewIntText(&lf.fps, x, y) - lf.Init() - lf.Bind(event.Enter, logicFPSBind) + lf.CallerID = event.DefaultCallerMap.Register(lf) + // TODO: not default bus + event.Bind(event.DefaultBus, event.Enter, lf, logicFPSBind) return lf } -func logicFPSBind(id event.CID, nothing interface{}) int { - lf := event.GetEntity(id).(*LogicFPS) +func logicFPSBind(lf *LogicFPS, _ event.EnterPayload) event.Response { t := time.Now() lf.fps = int((timing.FPS(lf.lastTime, t) * lf.Smoothing) + (float64(lf.fps) * (1 - lf.Smoothing))) lf.lastTime = t diff --git a/render/logicfps_test.go b/render/logicfps_test.go index c6c86e6a..981c2c71 100644 --- a/render/logicfps_test.go +++ b/render/logicfps_test.go @@ -3,12 +3,14 @@ package render import ( "image" "testing" + + "github.com/oakmound/oak/v4/event" ) func TestLogicFPS(t *testing.T) { lfps := NewLogicFPS(0, nil, 0, 0) lfps.Draw(image.NewRGBA(image.Rect(0, 0, 100, 100)), 10, 10) - logicFPSBind(lfps.CID, nil) + logicFPSBind(lfps, event.EnterPayload{}) if lfps.fps == 0 { t.Fatalf("fps not set by binding") } diff --git a/render/mod/cut.go b/render/mod/cut.go index 8d1f01fc..4363fc78 100644 --- a/render/mod/cut.go +++ b/render/mod/cut.go @@ -4,9 +4,9 @@ import ( "image" "image/color" - "github.com/oakmound/oak/v3/alg" - "github.com/oakmound/oak/v3/alg/floatgeom" - "github.com/oakmound/oak/v3/shape" + "github.com/oakmound/oak/v4/alg" + "github.com/oakmound/oak/v4/alg/floatgeom" + "github.com/oakmound/oak/v4/shape" ) // CutRound rounds the edges of the Modifiable with Bezier curves. diff --git a/render/mod/filter.go b/render/mod/filter.go index f578cfbd..eba448d4 100644 --- a/render/mod/filter.go +++ b/render/mod/filter.go @@ -20,9 +20,9 @@ func AndFilter(fs ...Filter) Filter { } } -// ConformToPallete is not a modification, but acts like ConformToPallete +// ConformToPalette( is not a modification, but acts like ConformToPalette( // without allocating a new *image.RGBA -func ConformToPallete(p color.Model) Filter { +func ConformToPalette(p color.Model) Filter { return func(rgba *image.RGBA) { bounds := rgba.Bounds() w := bounds.Max.X diff --git a/render/mod/gift.go b/render/mod/gift.go index 36a20815..6423bb0b 100644 --- a/render/mod/gift.go +++ b/render/mod/gift.go @@ -1,8 +1,12 @@ +//go:build !nogift +// +build !nogift + package mod import ( "image" "image/color" + "math" "github.com/disintegration/gift" ) @@ -124,3 +128,17 @@ var Transpose = GiftTransform(gift.Transpose()) // Transverse flips vertically and rotates 90 degrees counter clockwise. var Transverse = GiftTransform(gift.Transverse()) + +// Scale returns a scaled rgba. +func Scale(xRatio, yRatio float64) Mod { + return func(rgba image.Image) *image.RGBA { + bounds := rgba.Bounds() + w := int(math.Floor(float64(bounds.Max.X) * xRatio)) + h := int(math.Floor(float64(bounds.Max.Y) * yRatio)) + filter := gift.New( + gift.Resize(w, h, gift.CubicResampling)) + dst := image.NewRGBA(filter.Bounds(rgba.Bounds())) + filter.Draw(dst, rgba) + return dst + } +} diff --git a/entities/x/mods/highlight.go b/render/mod/highlight.go similarity index 87% rename from entities/x/mods/highlight.go rename to render/mod/highlight.go index 8033b571..d02939f6 100644 --- a/entities/x/mods/highlight.go +++ b/render/mod/highlight.go @@ -1,14 +1,13 @@ -package mods +package mod import ( "image" "image/color" - "github.com/oakmound/oak/v3/alg/intgeom" - "github.com/oakmound/oak/v3/render/mod" + "github.com/oakmound/oak/v4/alg/intgeom" ) -func HighlightOff(c color.Color, thickness, xOff, yOff int) mod.Mod { +func HighlightOff(c color.Color, thickness, xOff, yOff int) Mod { return func(img image.Image) *image.RGBA { bds := img.Bounds() @@ -48,7 +47,7 @@ func HighlightOff(c color.Color, thickness, xOff, yOff int) mod.Mod { } } -func InnerHighlightOff(c color.Color, thickness, xOff, yOff int) mod.Mod { +func InnerHighlightOff(c color.Color, thickness, xOff, yOff int) Mod { return func(img image.Image) *image.RGBA { bds := img.Bounds() @@ -88,17 +87,17 @@ func InnerHighlightOff(c color.Color, thickness, xOff, yOff int) mod.Mod { } } -func InnerHighlight(c color.Color, thickness int) mod.Mod { +func InnerHighlight(c color.Color, thickness int) Mod { return InnerHighlightOff(c, thickness, 0, 0) } -func Highlight(c color.Color, thickness int) mod.Mod { +func Highlight(c color.Color, thickness int) Mod { return HighlightOff(c, thickness, 0, 0) } -type Filter func(color.Color) color.Color +type InsetFilter func(color.Color) color.Color -func Inset(fn Filter, dir intgeom.Dir2) mod.Mod { +func Inset(fn InsetFilter, dir intgeom.Dir2) Mod { return func(img image.Image) *image.RGBA { bds := img.Bounds() @@ -161,8 +160,8 @@ func Lighter(c color.Color, f float64) color.Color { return color.RGBA64{uint16(r), uint16(g), uint16(b), uint16(a)} } -// Fade produces a color with more transparency by f percentage (0 to 1) -func Fade(c color.Color, f float64) color.Color { +// FadeColor produces a color with more transparency by f percentage (0 to 1) +func FadeColor(c color.Color, f float64) color.Color { r, g, b, a := c.RGBA() diff := uint32(65535 * f) r -= diff diff --git a/render/mod/mod.go b/render/mod/mod.go index 1ff484a6..295f3d4f 100644 --- a/render/mod/mod.go +++ b/render/mod/mod.go @@ -3,9 +3,6 @@ package mod import ( "image" "image/color" - "math" - - "github.com/disintegration/gift" ) // A Mod takes an image and returns that image transformed in some way. @@ -44,20 +41,6 @@ func SafeAnd(ms ...Mod) Mod { return And(ms...) } -// Scale returns a scaled rgba. -func Scale(xRatio, yRatio float64) Mod { - return func(rgba image.Image) *image.RGBA { - bounds := rgba.Bounds() - w := int(math.Floor(float64(bounds.Max.X) * xRatio)) - h := int(math.Floor(float64(bounds.Max.Y) * yRatio)) - filter := gift.New( - gift.Resize(w, h, gift.CubicResampling)) - dst := image.NewRGBA(filter.Bounds(rgba.Bounds())) - filter.Draw(dst, rgba) - return dst - } -} - // TrimColor will trim inputs so that any rows or columns where each pixel is // less than or equal to the input color are removed. This will change the dimensions // of the image. diff --git a/render/mod/mod_test.go b/render/mod/mod_test.go index a32f13bc..ec2795df 100644 --- a/render/mod/mod_test.go +++ b/render/mod/mod_test.go @@ -7,7 +7,7 @@ import ( "testing" "github.com/disintegration/gift" - "github.com/oakmound/oak/v3/shape" + "github.com/oakmound/oak/v4/shape" ) func TestComposedModifications(t *testing.T) { @@ -70,7 +70,7 @@ func TestAllModifications(t *testing.T) { *image.RGBA } filterList := []filterCase{{ - ConformToPallete(color.Palette{color.RGBA{64, 0, 0, 128}}), + ConformToPalette(color.Palette{color.RGBA{64, 0, 0, 128}}), setAll(newrgba(3, 3), color.RGBA{64, 0, 0, 128}), }, { Fade(10), diff --git a/render/modifiable.go b/render/modifiable.go index 4446fd65..46ab9684 100644 --- a/render/modifiable.go +++ b/render/modifiable.go @@ -3,7 +3,7 @@ package render import ( "image" - "github.com/oakmound/oak/v3/render/mod" + "github.com/oakmound/oak/v4/render/mod" ) // A Modifiable is a Renderable that has functions to change its diff --git a/render/noopStackable.go b/render/noopStackable.go index ad695ccb..de665c50 100644 --- a/render/noopStackable.go +++ b/render/noopStackable.go @@ -3,7 +3,7 @@ package render import ( "image/draw" - "github.com/oakmound/oak/v3/alg/intgeom" + "github.com/oakmound/oak/v4/alg/intgeom" ) // NoopStackable is a Stackable element where all methods are no-ops. diff --git a/render/noopStackable_test.go b/render/noopStackable_test.go index 5357c611..d986adcb 100644 --- a/render/noopStackable_test.go +++ b/render/noopStackable_test.go @@ -3,7 +3,7 @@ package render import ( "testing" - "github.com/oakmound/oak/v3/alg/intgeom" + "github.com/oakmound/oak/v4/alg/intgeom" ) func TestNoopStackable(t *testing.T) { @@ -20,4 +20,5 @@ func TestNoopStackable(t *testing.T) { if noop2 != noop { t.Fatalf("expected equal noop stackables") } + noop.Clear() } diff --git a/render/particle/allocator.go b/render/particle/allocator.go index 1d6f80e9..13a7f9aa 100644 --- a/render/particle/allocator.go +++ b/render/particle/allocator.go @@ -1,7 +1,7 @@ package particle import ( - "github.com/oakmound/oak/v3/event" + "github.com/oakmound/oak/v4/event" ) const ( @@ -10,24 +10,24 @@ const ( // An Allocator can allocate ids for particles type Allocator struct { - particleBlocks map[int]event.CID + particleBlocks map[int]event.CallerID nextOpenCh chan int freeCh chan int - allocCh chan event.CID + allocCh chan event.CallerID requestCh chan int - responseCh chan event.CID + responseCh chan event.CallerID stopCh chan struct{} } // NewAllocator creates a new allocator func NewAllocator() *Allocator { return &Allocator{ - particleBlocks: make(map[int]event.CID), + particleBlocks: make(map[int]event.CallerID), nextOpenCh: make(chan int), freeCh: make(chan int), - allocCh: make(chan event.CID), + allocCh: make(chan event.CallerID), requestCh: make(chan int), - responseCh: make(chan event.CID), + responseCh: make(chan event.CallerID), stopCh: make(chan struct{}), } } @@ -82,7 +82,7 @@ func (a *Allocator) freereceive(i int) int { } // Allocate requests a new block in the particle space for the given cid -func (a *Allocator) Allocate(id event.CID) int { +func (a *Allocator) Allocate(id event.CallerID) int { nextOpen := <-a.nextOpenCh a.allocCh <- id return nextOpen @@ -97,7 +97,8 @@ func (a *Allocator) Deallocate(block int) { func (a *Allocator) LookupSource(id int) *Source { a.requestCh <- id owner := <-a.responseCh - return event.GetEntity(owner).(*Source) + // TODO: not default? + return event.DefaultCallerMap.GetEntity(owner).(*Source) } // Lookup requests a specific particle in the particle space diff --git a/render/particle/allocator_test.go b/render/particle/allocator_test.go index e11c945f..546d1567 100644 --- a/render/particle/allocator_test.go +++ b/render/particle/allocator_test.go @@ -3,14 +3,14 @@ package particle import ( "testing" - "github.com/oakmound/oak/v3/event" + "github.com/oakmound/oak/v4/event" ) func TestAllocate(t *testing.T) { a := NewAllocator() go a.Run() for i := 0; i < 100; i++ { - if a.Allocate(event.CID(i)) != i { + if a.Allocate(event.CallerID(i)) != i { t.Fatalf("expected allocation of id %d to match id", i) } } @@ -32,8 +32,8 @@ func TestAllocatorLookup(t *testing.T) { a := NewAllocator() go a.Run() - src := NewSource(NewColorGenerator(), 0) - cid := src.CID + src := NewDefaultSource(NewColorGenerator(), 0) + cid := src.CID() pidBlock := a.Allocate(cid) src2 := a.LookupSource(pidBlock * blockSize) if src != src2 { diff --git a/render/particle/collisionParticle.go b/render/particle/collisionParticle.go index c88371ed..a8d534ba 100644 --- a/render/particle/collisionParticle.go +++ b/render/particle/collisionParticle.go @@ -3,7 +3,7 @@ package particle import ( "image/draw" - "github.com/oakmound/oak/v3/collision" + "github.com/oakmound/oak/v4/collision" ) // A CollisionParticle is a wrapper around other particles that also diff --git a/render/particle/collision_test.go b/render/particle/collision_test.go index 40d2dba9..a1b3a71a 100644 --- a/render/particle/collision_test.go +++ b/render/particle/collision_test.go @@ -4,7 +4,7 @@ import ( "image" "testing" - "github.com/oakmound/oak/v3/collision" + "github.com/oakmound/oak/v4/collision" ) func TestCollisionParticle(t *testing.T) { diff --git a/render/particle/collisonGenerator.go b/render/particle/collisonGenerator.go index 983e30de..4e2d4460 100644 --- a/render/particle/collisonGenerator.go +++ b/render/particle/collisonGenerator.go @@ -1,8 +1,8 @@ package particle import ( - "github.com/oakmound/oak/v3/collision" - "github.com/oakmound/oak/v3/event" + "github.com/oakmound/oak/v4/collision" + "github.com/oakmound/oak/v4/event" ) // A CollisionGenerator generates collision particles @@ -49,7 +49,7 @@ func (cg *CollisionGenerator) GenerateParticle(bp *baseParticle) Particle { pos := p.GetPos() return &CollisionParticle{ p, - collision.NewReactiveSpace(collision.NewFullSpace(pos.X(), pos.Y(), w, h, 0, event.CID(bp.pID)), cg.HitMap), + collision.NewReactiveSpace(collision.NewFullSpace(pos.X(), pos.Y(), w, h, 0, event.CallerID(bp.pID)), cg.HitMap), } } diff --git a/render/particle/colorGenerator.go b/render/particle/colorGenerator.go index 4a7a7c32..a7478ba4 100644 --- a/render/particle/colorGenerator.go +++ b/render/particle/colorGenerator.go @@ -3,10 +3,10 @@ package particle import ( "image/color" - "github.com/oakmound/oak/v3/alg" - "github.com/oakmound/oak/v3/shape" + "github.com/oakmound/oak/v4/alg" + "github.com/oakmound/oak/v4/shape" - "github.com/oakmound/oak/v3/alg/range/intrange" + "github.com/oakmound/oak/v4/alg/span" ) // A ColorGenerator generates ColorParticles @@ -15,8 +15,8 @@ type ColorGenerator struct { StartColor, StartColorRand color.Color EndColor, EndColorRand color.Color // The size, in pixel radius, of spawned particles - Size intrange.Range - EndSize intrange.Range + Size span.Span[int] + EndSize span.Span[int] // // Some sort of particle type, for rendering triangles or squares or circles... Shape shape.Shape @@ -40,8 +40,8 @@ func (cg *ColorGenerator) setDefaults() { cg.StartColorRand = color.RGBA{0, 0, 0, 0} cg.EndColor = color.RGBA{0, 0, 0, 0} cg.EndColorRand = color.RGBA{0, 0, 0, 0} - cg.Size = intrange.NewConstant(1) - cg.EndSize = intrange.NewConstant(1) + cg.Size = span.NewConstant(1) + cg.EndSize = span.NewConstant(1) cg.Shape = shape.Square } @@ -49,9 +49,9 @@ func (cg *ColorGenerator) setDefaults() { func (cg *ColorGenerator) Generate(layer int) *Source { // Convert rotation from degrees to radians if cg.Rotation != nil { - cg.Rotation = cg.Rotation.Mult(alg.DegToRad) + cg.Rotation = cg.Rotation.MulSpan(alg.DegToRad) } - return NewSource(cg, layer) + return NewDefaultSource(cg, layer) } // GenerateParticle creates a particle from a generator @@ -66,7 +66,7 @@ func (cg *ColorGenerator) GenerateParticle(bp *baseParticle) Particle { } // GetParticleSize on a color generator returns that the particles -// are per-particle specificially sized +// are per-particle specifically sized func (cg *ColorGenerator) GetParticleSize() (w float64, h float64, perParticle bool) { return 0, 0, true } @@ -92,12 +92,12 @@ func (cg *ColorGenerator) SetEndColor(ec, ecr color.Color) { // A Sizeable is a generator that can have some size set to it type Sizeable interface { - SetSize(i intrange.Range) - SetEndSize(i intrange.Range) + SetSize(i span.Span[int]) + SetEndSize(i span.Span[int]) } // Size is an option to set a Sizeable size -func Size(i intrange.Range) func(Generator) { +func Size(i span.Span[int]) func(Generator) { return func(g Generator) { if g2, ok := g.(Sizeable); ok { g2.SetSize(i) @@ -106,7 +106,7 @@ func Size(i intrange.Range) func(Generator) { } // EndSize sets the end size of a Sizeable -func EndSize(i intrange.Range) func(Generator) { +func EndSize(i span.Span[int]) func(Generator) { return func(g Generator) { if g2, ok := g.(Sizeable); ok { g2.SetEndSize(i) @@ -115,12 +115,12 @@ func EndSize(i intrange.Range) func(Generator) { } // SetSize satisfies Sizeable -func (cg *ColorGenerator) SetSize(i intrange.Range) { +func (cg *ColorGenerator) SetSize(i span.Span[int]) { cg.Size = i } // SetEndSize stasfies Sizeable -func (cg *ColorGenerator) SetEndSize(i intrange.Range) { +func (cg *ColorGenerator) SetEndSize(i span.Span[int]) { cg.EndSize = i } diff --git a/render/particle/colorParticle.go b/render/particle/colorParticle.go index 2c6e7a68..6fcb7220 100644 --- a/render/particle/colorParticle.go +++ b/render/particle/colorParticle.go @@ -4,8 +4,8 @@ import ( "image/color" "image/draw" - "github.com/oakmound/oak/v3/physics" - "github.com/oakmound/oak/v3/render" + "github.com/oakmound/oak/v4/physics" + "github.com/oakmound/oak/v4/render" ) // A ColorParticle is a particle with a defined color and size diff --git a/render/particle/color_test.go b/render/particle/color_test.go index 0ad2dcde..9c3275f5 100644 --- a/render/particle/color_test.go +++ b/render/particle/color_test.go @@ -5,18 +5,17 @@ import ( "image/color" "testing" - "github.com/oakmound/oak/v3/alg/range/floatrange" - "github.com/oakmound/oak/v3/alg/range/intrange" - "github.com/oakmound/oak/v3/render" - "github.com/oakmound/oak/v3/shape" + "github.com/oakmound/oak/v4/alg/span" + "github.com/oakmound/oak/v4/render" + "github.com/oakmound/oak/v4/shape" ) func TestColorParticle(t *testing.T) { g := NewColorGenerator( - Rotation(floatrange.NewConstant(1)), + Rotation(span.NewConstant(1.0)), Color(color.RGBA{255, 0, 0, 255}, color.RGBA{255, 0, 0, 255}, color.RGBA{255, 0, 0, 255}, color.RGBA{255, 0, 0, 255}), - Size(intrange.NewConstant(5)), - EndSize(intrange.NewConstant(10)), + Size(span.NewConstant(5)), + EndSize(span.NewConstant(10)), Shape(shape.Heart), ) src := g.Generate(0) diff --git a/render/particle/generator.go b/render/particle/generator.go index 53299897..ffb84c57 100644 --- a/render/particle/generator.go +++ b/render/particle/generator.go @@ -1,14 +1,15 @@ package particle import ( - "github.com/oakmound/oak/v3/alg/range/floatrange" - "github.com/oakmound/oak/v3/alg/range/intrange" - "github.com/oakmound/oak/v3/physics" + "math" + + "github.com/oakmound/oak/v4/alg/span" + "github.com/oakmound/oak/v4/physics" ) var ( // Inf represents Infinite duration - Inf = intrange.NewInfinite() + Inf = span.NewConstant(math.MaxInt32) ) // A Generator holds settings for generating particles @@ -33,14 +34,14 @@ type BaseGenerator struct { // to something along the lines of 'new per 30 frames', // or allow low fractional values to be meaningful, // so that more fine-tuned particle generation speeds are possible. - NewPerFrame floatrange.Range + NewPerFrame span.Span[float64] // The number of frames each particle should persist // before being removed. - LifeSpan floatrange.Range + LifeSpan span.Span[float64] // 0 - between quadrant 1 and 4 // 90 - between quadrant 2 and 1 - Angle floatrange.Range - Speed floatrange.Range + Angle span.Span[float64] + Speed span.Span[float64] Spread physics.Vector // Duration in milliseconds for the particle source. // After this many milliseconds have passed, it will @@ -48,9 +49,9 @@ type BaseGenerator struct { // not be removed until their individual lifespans run // out. // A duration of -1 represents never stopping. - Duration intrange.Range + Duration span.Span[int] // Rotational acceleration, to change angle over time - Rotation floatrange.Range + Rotation span.Span[float64] // Gravity X() and Gravity Y() represent particle acceleration per frame. Gravity physics.Vector SpeedDecay physics.Vector @@ -67,10 +68,10 @@ func (bg *BaseGenerator) GetBaseGenerator() *BaseGenerator { func (bg *BaseGenerator) setDefaults() { *bg = BaseGenerator{ Vector: physics.NewVector(0, 0), - NewPerFrame: floatrange.NewConstant(1), - LifeSpan: floatrange.NewConstant(60), - Angle: floatrange.NewConstant(0), - Speed: floatrange.NewConstant(1), + NewPerFrame: span.NewConstant(1.0), + LifeSpan: span.NewConstant(60.0), + Angle: span.NewConstant(0.0), + Speed: span.NewConstant(1.0), Spread: physics.NewVector(0, 0), Duration: Inf, Rotation: nil, diff --git a/render/particle/gradientGenerator.go b/render/particle/gradientGenerator.go index d2a947d7..eb221518 100644 --- a/render/particle/gradientGenerator.go +++ b/render/particle/gradientGenerator.go @@ -3,8 +3,8 @@ package particle import ( "image/color" - "github.com/oakmound/oak/v3/alg" - "github.com/oakmound/oak/v3/render" + "github.com/oakmound/oak/v4/alg" + "github.com/oakmound/oak/v4/render" ) // A GradientGenerator is a ColorGenerator with a patterned gradient @@ -43,9 +43,9 @@ func (gg *GradientGenerator) setDefaults() { func (gg *GradientGenerator) Generate(layer int) *Source { // Convert rotation from degrees to radians if gg.Rotation != nil { - gg.Rotation = gg.Rotation.Mult(alg.DegToRad) + gg.Rotation = gg.Rotation.MulSpan(alg.DegToRad) } - return NewSource(gg, layer) + return NewDefaultSource(gg, layer) } // GenerateParticle creates a particle from a generator diff --git a/render/particle/gradientParticle.go b/render/particle/gradientParticle.go index 47b1fd5f..a1e51a25 100644 --- a/render/particle/gradientParticle.go +++ b/render/particle/gradientParticle.go @@ -4,7 +4,7 @@ import ( "image/color" "image/draw" - "github.com/oakmound/oak/v3/render" + "github.com/oakmound/oak/v4/render" ) // A GradientParticle has a gradient from one color to another diff --git a/render/particle/gradient_test.go b/render/particle/gradient_test.go index 244d9aa3..1fd70431 100644 --- a/render/particle/gradient_test.go +++ b/render/particle/gradient_test.go @@ -5,31 +5,30 @@ import ( "image/color" "testing" - "github.com/oakmound/oak/v3/alg/range/floatrange" - "github.com/oakmound/oak/v3/alg/range/intrange" - "github.com/oakmound/oak/v3/physics" - "github.com/oakmound/oak/v3/render" - "github.com/oakmound/oak/v3/shape" + "github.com/oakmound/oak/v4/alg/span" + "github.com/oakmound/oak/v4/physics" + "github.com/oakmound/oak/v4/render" + "github.com/oakmound/oak/v4/shape" ) func TestGradientParticle(t *testing.T) { g := NewGradientGenerator( - Rotation(floatrange.NewConstant(1)), + Rotation(span.NewConstant(1.0)), Color(color.RGBA{255, 0, 0, 255}, color.RGBA{255, 0, 0, 255}, color.RGBA{255, 0, 0, 255}, color.RGBA{255, 0, 0, 255}), Color2(color.RGBA{255, 0, 0, 255}, color.RGBA{255, 0, 0, 255}, color.RGBA{255, 0, 0, 255}, color.RGBA{255, 0, 0, 255}), - Size(intrange.NewConstant(5)), - EndSize(intrange.NewConstant(10)), + Size(span.NewConstant(5)), + EndSize(span.NewConstant(10)), Shape(shape.Heart), Progress(render.HorizontalProgress), And( - NewPerFrame(floatrange.NewConstant(20)), + NewPerFrame(span.NewConstant(20.0)), ), Pos(20, 20), - LifeSpan(floatrange.NewConstant(10)), - Angle(floatrange.NewConstant(0)), - Speed(floatrange.NewConstant(0)), + LifeSpan(span.NewConstant(10.0)), + Angle(span.NewConstant(0.0)), + Speed(span.NewConstant(0.0)), Spread(10, 10), - Duration(intrange.NewConstant(10)), + Duration(span.NewConstant(10)), Gravity(10, 10), SpeedDecay(1, 1), End(func(_ Particle) {}), diff --git a/render/particle/math.go b/render/particle/math.go index 80fc2be6..d890368d 100644 --- a/render/particle/math.go +++ b/render/particle/math.go @@ -5,7 +5,7 @@ import ( "math" "math/rand" - "github.com/oakmound/oak/v3/alg" + "github.com/oakmound/oak/v4/alg" ) // floatFromSpread returns a random value between diff --git a/render/particle/options.go b/render/particle/options.go index 3b53f59e..d822f134 100644 --- a/render/particle/options.go +++ b/render/particle/options.go @@ -1,10 +1,11 @@ package particle import ( - "github.com/oakmound/oak/v3/alg" - "github.com/oakmound/oak/v3/alg/range/floatrange" - "github.com/oakmound/oak/v3/alg/range/intrange" - "github.com/oakmound/oak/v3/physics" + "math" + + "github.com/oakmound/oak/v4/alg" + "github.com/oakmound/oak/v4/alg/span" + "github.com/oakmound/oak/v4/physics" ) // And chains together particle options into a single option @@ -18,7 +19,7 @@ func And(as ...func(Generator)) func(Generator) { } // NewPerFrame sets how many particles should be produced per frame -func NewPerFrame(npf floatrange.Range) func(Generator) { +func NewPerFrame(npf span.Span[float64]) func(Generator) { return func(g Generator) { g.GetBaseGenerator().NewPerFrame = npf } @@ -32,7 +33,7 @@ func Pos(x, y float64) func(Generator) { } // LifeSpan sets how long a particle should last before dying -func LifeSpan(ls floatrange.Range) func(Generator) { +func LifeSpan(ls span.Span[float64]) func(Generator) { return func(g Generator) { g.GetBaseGenerator().LifeSpan = ls } @@ -41,19 +42,19 @@ func LifeSpan(ls floatrange.Range) func(Generator) { // InfiniteLifeSpan will set particles to never die over time. func InfiniteLifeSpan() func(Generator) { return func(g Generator) { - g.GetBaseGenerator().LifeSpan = floatrange.NewInfinite() + g.GetBaseGenerator().LifeSpan = span.NewConstant(math.MaxFloat64) } } // Angle sets the initial angle of a particle in degrees -func Angle(a floatrange.Range) func(Generator) { +func Angle(a span.Span[float64]) func(Generator) { return func(g Generator) { - g.GetBaseGenerator().Angle = a.Mult(alg.DegToRad) + g.GetBaseGenerator().Angle = a.MulSpan(alg.DegToRad) } } // Speed sets the initial speed of a particle -func Speed(s floatrange.Range) func(Generator) { +func Speed(s span.Span[float64]) func(Generator) { return func(g Generator) { g.GetBaseGenerator().Speed = s } @@ -67,14 +68,14 @@ func Spread(x, y float64) func(Generator) { } // Duration sets how long a generator should produce particles for -func Duration(i intrange.Range) func(Generator) { +func Duration(i span.Span[int]) func(Generator) { return func(g Generator) { g.GetBaseGenerator().Duration = i } } // Rotation rotates particles by a variable amount per frame -func Rotation(a floatrange.Range) func(Generator) { +func Rotation(a span.Span[float64]) func(Generator) { return func(g Generator) { g.GetBaseGenerator().Rotation = a } diff --git a/render/particle/particle.go b/render/particle/particle.go index 91f6c571..a089026c 100644 --- a/render/particle/particle.go +++ b/render/particle/particle.go @@ -5,8 +5,8 @@ package particle import ( "image/draw" - "github.com/oakmound/oak/v3/physics" - "github.com/oakmound/oak/v3/render" + "github.com/oakmound/oak/v4/physics" + "github.com/oakmound/oak/v4/render" ) // A Particle is a renderable that is spawned by a generator, usually very fast, diff --git a/render/particle/particle_test.go b/render/particle/particle_test.go index 748f9fbc..578b2033 100644 --- a/render/particle/particle_test.go +++ b/render/particle/particle_test.go @@ -3,7 +3,7 @@ package particle import ( "testing" - "github.com/oakmound/oak/v3/render" + "github.com/oakmound/oak/v4/render" ) func TestParticle(t *testing.T) { diff --git a/render/particle/shape.go b/render/particle/shape.go index 3121d705..e1317275 100644 --- a/render/particle/shape.go +++ b/render/particle/shape.go @@ -1,6 +1,6 @@ package particle -import "github.com/oakmound/oak/v3/shape" +import "github.com/oakmound/oak/v4/shape" // Shapeable generators can have the Shape option called on them type Shapeable interface { diff --git a/render/particle/source.go b/render/particle/source.go index e1abe60d..601e6cd7 100644 --- a/render/particle/source.go +++ b/render/particle/source.go @@ -4,9 +4,9 @@ import ( "math" "time" - "github.com/oakmound/oak/v3/event" - "github.com/oakmound/oak/v3/physics" - "github.com/oakmound/oak/v3/render" + "github.com/oakmound/oak/v4/event" + "github.com/oakmound/oak/v4/physics" + "github.com/oakmound/oak/v4/render" ) const ( @@ -19,9 +19,11 @@ type Source struct { Generator Generator *Allocator - particles [blockSize]Particle - nextPID int - CID event.CID + rotateBinding event.Binding + + particles [blockSize]Particle + nextPID int + event.CallerID pIDBlock int stackLevel int EndFunc func() @@ -31,25 +33,31 @@ type Source struct { stopped bool } -// NewSource creates a new source -func NewSource(g Generator, stackLevel int) *Source { +// NewDefaultSource creates a new sourceattached to the default event bus. +func NewDefaultSource(g Generator, stackLevel int) *Source { + return NewSource(event.DefaultBus, g, stackLevel) +} + +// NewSource for particles constructed from a generator with specifications on how the particles should be handled. +func NewSource(handler event.Handler, g Generator, stackLevel int) *Source { ps := new(Source) ps.Generator = g ps.stackLevel = stackLevel ps.Allocator = DefaultAllocator - ps.Init() + cid := handler.GetCallerMap().Register(ps) + ps.stopRotateAt = time.Now().Add( + time.Duration(ps.Generator.GetBaseGenerator().Duration.Poll()) * time.Millisecond) + + ps.CallerID = cid // cid must be set before the following bind call + ps.rotateBinding = event.Bind(handler, event.Enter, ps, rotateParticles) + + ps.pIDBlock = ps.Allocate(ps.CallerID) return ps } -// Init allows a source to be considered as an entity, and initializes it -func (ps *Source) Init() event.CID { - CID := event.NextID(ps) - ps.stopRotateAt = time.Now().Add( - time.Duration(ps.Generator.GetBaseGenerator().Duration.Poll()) * time.Millisecond) - CID.Bind(event.Enter, rotateParticles) - ps.CID = CID - ps.pIDBlock = ps.Allocate(ps.CID) - return CID +// CID of our particle source +func (ps *Source) CID() event.CallerID { + return ps.CallerID } func (ps *Source) cycleParticles() bool { @@ -176,8 +184,7 @@ func (ps *Source) addParticles() { // rotateParticles updates particles over time as long // as a Source is active. -func rotateParticles(id event.CID, payload interface{}) int { - ps := id.E().(*Source) +func rotateParticles(ps *Source, _ event.EnterPayload) event.Response { if ps.stopped { return 0 } @@ -197,33 +204,31 @@ func rotateParticles(id event.CID, payload interface{}) int { // clearParticles is used after a Source has been stopped // to continue moving old particles for as long as they exist. -func clearParticles(id event.CID, nothing interface{}) int { - if ps, ok := id.E().(*Source); ok { - if !ps.paused { - if ps.cycleParticles() { - } else { - if ps.EndFunc != nil { - ps.EndFunc() - } - event.DestroyEntity(id) - ps.Deallocate(ps.pIDBlock) - return event.UnbindEvent +func clearParticles(ps *Source, _ event.EnterPayload) event.Response { + if !ps.paused { + if ps.cycleParticles() { + } else { + if ps.EndFunc != nil { + ps.EndFunc() } + // TODO: not default + event.DefaultCallerMap.RemoveEntity(ps.CID()) + ps.Deallocate(ps.pIDBlock) + return event.ResponseUnbindThisBinding } - - return 0 } - return event.UnbindEvent + return 0 } // Stop manually stops a Source, if its duration is infinite -// or if it should be stopped before expriring naturally. +// or if it should be stopped before expiring naturally. func (ps *Source) Stop() { if ps == nil { return } ps.stopped = true - ps.CID.UnbindAllAndRebind([]event.Bindable{clearParticles}, []string{event.Enter}) + ps.rotateBinding.Unbind() + event.Bind(event.DefaultBus, event.Enter, ps, clearParticles) } // Pause on a Source just stops the repetition diff --git a/render/particle/source_test.go b/render/particle/source_test.go index c70c1f76..01f1fc13 100644 --- a/render/particle/source_test.go +++ b/render/particle/source_test.go @@ -4,35 +4,34 @@ import ( "image/color" "testing" - "github.com/oakmound/oak/v3/alg/range/floatrange" - "github.com/oakmound/oak/v3/alg/range/intrange" - "github.com/oakmound/oak/v3/event" - "github.com/oakmound/oak/v3/physics" - "github.com/oakmound/oak/v3/render" - "github.com/oakmound/oak/v3/shape" + "github.com/oakmound/oak/v4/alg/span" + "github.com/oakmound/oak/v4/event" + "github.com/oakmound/oak/v4/physics" + "github.com/oakmound/oak/v4/render" + "github.com/oakmound/oak/v4/shape" ) func TestSource(t *testing.T) { g := NewGradientGenerator( - Rotation(floatrange.NewConstant(1)), + Rotation(span.NewConstant(1.0)), Color(color.RGBA{255, 0, 0, 255}, color.RGBA{255, 0, 0, 255}, color.RGBA{255, 0, 0, 255}, color.RGBA{255, 0, 0, 255}), Color2(color.RGBA{255, 0, 0, 255}, color.RGBA{255, 0, 0, 255}, color.RGBA{255, 0, 0, 255}, color.RGBA{255, 0, 0, 255}), - Size(intrange.NewConstant(5)), - EndSize(intrange.NewConstant(10)), + Size(span.NewConstant(5)), + EndSize(span.NewConstant(10)), Shape(shape.Heart), Progress(render.HorizontalProgress), And( - NewPerFrame(floatrange.NewConstant(200)), + NewPerFrame(span.NewConstant(200.0)), ), Pos(20, 20), - LifeSpan(floatrange.NewConstant(10)), + LifeSpan(span.NewConstant(10.0)), Limit(2047), - Angle(floatrange.NewConstant(0)), - Speed(floatrange.NewConstant(0)), + Angle(span.NewConstant(0.0)), + Speed(span.NewConstant(0.0)), Spread(10, 10), - Duration(intrange.NewConstant(10)), + Duration(span.NewConstant(10)), Gravity(10, 10), SpeedDecay(1, 1), End(func(_ Particle) {}), @@ -48,9 +47,9 @@ func TestSource(t *testing.T) { } for i := 0; i < 1000; i++ { - rotateParticles(src.CID, nil) + rotateParticles(src, event.EnterPayload{}) } - for clearParticles(src.CID, nil) != event.UnbindEvent { + for clearParticles(src, event.EnterPayload{}) != event.ResponseUnbindThisBinding { } if !ended { @@ -87,14 +86,3 @@ func TestSource(t *testing.T) { var src2 *Source src2.Stop() } - -func TestClearParticles(t *testing.T) { - t.Parallel() - t.Run("BadTypeBinding", func(t *testing.T) { - t.Parallel() - result := clearParticles(10000, nil) - if result != event.UnbindEvent { - t.Fatalf("expected UnbindEvent result, got %v", result) - } - }) -} diff --git a/render/particle/spriteGenerator.go b/render/particle/spriteGenerator.go index 1cd5c760..b91856b9 100644 --- a/render/particle/spriteGenerator.go +++ b/render/particle/spriteGenerator.go @@ -1,16 +1,16 @@ package particle import ( - "github.com/oakmound/oak/v3/alg/range/floatrange" + "github.com/oakmound/oak/v4/alg/span" - "github.com/oakmound/oak/v3/alg" - "github.com/oakmound/oak/v3/render" + "github.com/oakmound/oak/v4/alg" + "github.com/oakmound/oak/v4/render" ) // A SpriteGenerator generate SpriteParticles type SpriteGenerator struct { BaseGenerator - SpriteRotation floatrange.Range + SpriteRotation span.Span[float64] Base *render.Sprite } @@ -28,16 +28,16 @@ func NewSpriteGenerator(options ...func(Generator)) Generator { func (sg *SpriteGenerator) setDefaults() { sg.BaseGenerator.setDefaults() - sg.SpriteRotation = floatrange.NewConstant(0) + sg.SpriteRotation = span.NewConstant(0.0) } // Generate creates a source using this generator func (sg *SpriteGenerator) Generate(layer int) *Source { // Convert rotation from degrees to radians if sg.Rotation != nil { - sg.Rotation = sg.Rotation.Mult(alg.DegToRad) + sg.Rotation = sg.Rotation.MulSpan(alg.DegToRad) } - return NewSource(sg, layer) + return NewDefaultSource(sg, layer) } // GenerateParticle creates a particle from a generator @@ -51,7 +51,7 @@ func (sg *SpriteGenerator) GenerateParticle(bp *baseParticle) Particle { // A Sprited can have a sprite set to it type Sprited interface { SetSprite(*render.Sprite) - SetSpriteRotation(f floatrange.Range) + SetSpriteRotation(f span.Span[float64]) } // Sprite sets a Sprited's sprite @@ -68,14 +68,14 @@ func (sg *SpriteGenerator) SetSprite(s *render.Sprite) { } // SpriteRotation sets a Sprited's rotation -func SpriteRotation(f floatrange.Range) func(Generator) { +func SpriteRotation(f span.Span[float64]) func(Generator) { return func(g Generator) { g.(Sprited).SetSpriteRotation(f) } } // SetSpriteRotation satisfied Sprited for SpriteGenerators -func (sg *SpriteGenerator) SetSpriteRotation(f floatrange.Range) { +func (sg *SpriteGenerator) SetSpriteRotation(f span.Span[float64]) { sg.SpriteRotation = f } diff --git a/render/particle/spriteParticle.go b/render/particle/spriteParticle.go index 317ba791..84682f53 100644 --- a/render/particle/spriteParticle.go +++ b/render/particle/spriteParticle.go @@ -3,8 +3,8 @@ package particle import ( "image/draw" - "github.com/oakmound/oak/v3/render" - "github.com/oakmound/oak/v3/render/mod" + "github.com/oakmound/oak/v4/render" + "github.com/oakmound/oak/v4/render/mod" ) // A SpriteParticle is a particle that has an amount of sprite rotation diff --git a/render/particle/sprite_test.go b/render/particle/sprite_test.go index e03940d5..bf6e67ed 100644 --- a/render/particle/sprite_test.go +++ b/render/particle/sprite_test.go @@ -5,17 +5,16 @@ import ( "image/color" "testing" - "github.com/oakmound/oak/v3/alg/range/floatrange" - - "github.com/oakmound/oak/v3/render" + "github.com/oakmound/oak/v4/alg/span" + "github.com/oakmound/oak/v4/render" ) func TestSpriteParticle(t *testing.T) { s := render.NewColorBox(10, 10, color.RGBA{255, 0, 0, 255}) g := NewSpriteGenerator( Sprite(s), - Rotation(floatrange.NewConstant(1)), - SpriteRotation(floatrange.NewConstant(1)), + Rotation(span.NewConstant(1.0)), + SpriteRotation(span.NewConstant(1.0)), ) src := g.Generate(0) src.addParticles() diff --git a/render/polygon.go b/render/polygon.go index c2086a18..e1908da4 100644 --- a/render/polygon.go +++ b/render/polygon.go @@ -5,8 +5,8 @@ import ( "image/color" "math" - "github.com/oakmound/oak/v3/alg/floatgeom" - "github.com/oakmound/oak/v3/alg/range/colorrange" + "github.com/oakmound/oak/v4/alg/floatgeom" + "github.com/oakmound/oak/v4/alg/span" ) // A Polygon is a renderable that is represented by a set of in order points @@ -45,7 +45,7 @@ func (pg *Polygon) GetThickOutline(c color.Color, thickness int) *CompositeM { // GetGradientOutline returns a set of lines of the given color along this polygon's outline, // at the given thickness, ranging from c1 to c2 in color func (pg *Polygon) GetGradientOutline(c1, c2 color.Color, thickness int) *CompositeM { - return pg.GetColoredOutline(colorrange.NewLinear(c1, c2).Percentile, thickness) + return pg.GetColoredOutline(span.NewLinearColor(c1, c2).Percentile, thickness) } // GetColoredOutline returns a set of lines of the given color along this polygon's outline diff --git a/render/polygon_test.go b/render/polygon_test.go index 9a705abc..7ae4709f 100644 --- a/render/polygon_test.go +++ b/render/polygon_test.go @@ -4,7 +4,7 @@ import ( "image/color" "testing" - "github.com/oakmound/oak/v3/alg/floatgeom" + "github.com/oakmound/oak/v4/alg/floatgeom" ) func TestContains(t *testing.T) { diff --git a/render/renderable.go b/render/renderable.go index 5a56e19f..ce2c3fee 100644 --- a/render/renderable.go +++ b/render/renderable.go @@ -3,7 +3,7 @@ package render import ( "image/draw" - "github.com/oakmound/oak/v3/physics" + "github.com/oakmound/oak/v4/physics" ) // A Renderable is anything which can diff --git a/render/reverting.go b/render/reverting.go index c93d3823..3b38162d 100644 --- a/render/reverting.go +++ b/render/reverting.go @@ -1,8 +1,8 @@ package render import ( - "github.com/oakmound/oak/v3/event" - "github.com/oakmound/oak/v3/render/mod" + "github.com/oakmound/oak/v4/event" + "github.com/oakmound/oak/v4/render/mod" ) // The Reverting structure lets modifications be made to a Modifiable and then @@ -119,7 +119,7 @@ func (rv *Reverting) update() { } // SetTriggerID sets the ID AnimationEnd will trigger on for animating subtypes. -func (rv *Reverting) SetTriggerID(cid event.CID) { +func (rv *Reverting) SetTriggerID(cid event.CallerID) { if t, ok := rv.Modifiable.(Triggerable); ok { t.SetTriggerID(cid) } diff --git a/render/reverting_test.go b/render/reverting_test.go index d4d324e1..e8a64497 100644 --- a/render/reverting_test.go +++ b/render/reverting_test.go @@ -6,7 +6,7 @@ import ( "reflect" "testing" - "github.com/oakmound/oak/v3/render/mod" + "github.com/oakmound/oak/v4/render/mod" ) var ( @@ -157,7 +157,7 @@ func TestRevertingCascadeFns(t *testing.T) { t.Fatalf("reverting unpause did not resume underlying sequence") } rv.SetTriggerID(1) - if sq.cID != 1 { + if sq.CallerID != 1 { t.Fatalf("reverting cID did not set underlying squence cID") } rv.update() diff --git a/render/sequence.go b/render/sequence.go index d1a9bc9f..9fd3c5c5 100644 --- a/render/sequence.go +++ b/render/sequence.go @@ -5,9 +5,9 @@ import ( "image/draw" "time" - "github.com/oakmound/oak/v3/event" - "github.com/oakmound/oak/v3/render/mod" - "github.com/oakmound/oak/v3/timing" + "github.com/oakmound/oak/v4/event" + "github.com/oakmound/oak/v4/render/mod" + "github.com/oakmound/oak/v4/timing" ) // A Sequence is a series of modifiables drawn as an animation. It is more @@ -20,7 +20,7 @@ type Sequence struct { lastChange time.Time sheetPos int frameTime int64 - cID event.CID + event.CallerID } // NewSequence returns a new sequence from the input modifiables, playing at @@ -68,18 +68,21 @@ func (sq *Sequence) Copy() Modifiable { return newSq } +var AnimationEnd = event.RegisterEvent[struct{}]() + // SetTriggerID sets the ID that AnimationEnd will be triggered on when this // sequence loops over from its last frame to its first -func (sq *Sequence) SetTriggerID(id event.CID) { - sq.cID = id +func (sq *Sequence) SetTriggerID(id event.CallerID) { + sq.CallerID = id } func (sq *Sequence) update() { if sq.playing && time.Since(sq.lastChange).Nanoseconds() > sq.frameTime { sq.lastChange = time.Now() sq.sheetPos = (sq.sheetPos + 1) % len(sq.rs) - if sq.sheetPos == (len(sq.rs)-1) && sq.cID != 0 { - sq.cID.Trigger(event.AnimationEnd, nil) + if sq.sheetPos == (len(sq.rs)-1) && sq.CallerID != 0 { + // TODO: not default bus + event.TriggerForCallerOn(event.DefaultBus, sq.CallerID, AnimationEnd, struct{}{}) } } } diff --git a/render/sequence_test.go b/render/sequence_test.go index 1a0c4ae4..5fe92ebf 100644 --- a/render/sequence_test.go +++ b/render/sequence_test.go @@ -8,35 +8,23 @@ import ( "testing" "time" - "github.com/oakmound/oak/v3/event" - "github.com/oakmound/oak/v3/render/mod" + "github.com/oakmound/oak/v4/event" + "github.com/oakmound/oak/v4/render/mod" ) -type Dummy struct{} - -func (d Dummy) Init() event.CID { - return event.NextID(d) +type Dummy struct { + event.CallerID } func TestSequenceTrigger(t *testing.T) { sq := NewSequence(5, NewColorBox(10, 10, color.RGBA{255, 0, 0, 255}), NewColorBox(10, 10, color.RGBA{0, 255, 0, 255})) - go event.ResolveChanges() - cid := Dummy{}.Init() - sq.SetTriggerID(cid) + d := Dummy{} + d.CallerID = event.DefaultCallerMap.Register(d) + sq.SetTriggerID(d.CallerID) triggerCh := make(chan struct{}) - cid.Bind(event.AnimationEnd, func(event.CID, interface{}) int { - // This is a bad idea in real code, this will lock up - // unbindings because the function that triggered this owns - // the lock on the event bus until this function exits. - // It is for this reason that all triggers, bindings, - // and unbindings do nothing when they are called, just put - // off work to be done-- to make sure no one is expecting a - // result from one of those functions, from within a triggered - // function, causing a deadlock. - // - // For this test this is the easiest way to do this though + event.Bind(event.DefaultBus, AnimationEnd, d, func(_ Dummy, _ struct{}) event.Response { triggerCh <- struct{}{} return 0 }) diff --git a/render/sheet.go b/render/sheet.go index 7818c075..c537eaad 100644 --- a/render/sheet.go +++ b/render/sheet.go @@ -3,7 +3,7 @@ package render import ( "image" - "github.com/oakmound/oak/v3/oakerr" + "github.com/oakmound/oak/v4/oakerr" ) // Sheet is a 2D array of image rgbas diff --git a/render/sheet_test.go b/render/sheet_test.go index 5b279b21..8f9f8dcf 100644 --- a/render/sheet_test.go +++ b/render/sheet_test.go @@ -5,8 +5,8 @@ import ( "os" "testing" - "github.com/oakmound/oak/v3/alg/intgeom" - "github.com/oakmound/oak/v3/fileutil" + "github.com/oakmound/oak/v4/alg/intgeom" + "github.com/oakmound/oak/v4/fileutil" ) //go:embed testdata/assets/* diff --git a/render/sprite.go b/render/sprite.go index 91409abf..df041be7 100644 --- a/render/sprite.go +++ b/render/sprite.go @@ -5,7 +5,7 @@ import ( "image/color" "image/draw" - "github.com/oakmound/oak/v3/render/mod" + "github.com/oakmound/oak/v4/render/mod" ) // A Sprite is a basic wrapper around image data and a point. The most basic Renderable. diff --git a/render/sprite_test.go b/render/sprite_test.go index d6abed87..2bb1dbfe 100644 --- a/render/sprite_test.go +++ b/render/sprite_test.go @@ -6,18 +6,17 @@ import ( "reflect" "testing" - "github.com/oakmound/oak/v3/alg/range/colorrange" - "github.com/oakmound/oak/v3/alg/range/intrange" - "github.com/oakmound/oak/v3/render/mod" + "github.com/oakmound/oak/v4/alg/span" + "github.com/oakmound/oak/v4/render/mod" ) var ( // this is excessive for a lot of tests // but it takes away some decision making // and could reveal problems that probably aren't there - widths = intrange.NewLinear(1, 10) - heights = intrange.NewLinear(1, 10) - colors = colorrange.NewLinear(color.RGBA{0, 0, 0, 0}, color.RGBA{255, 255, 255, 255}) + widths = span.NewLinear(1, 10) + heights = span.NewLinear(1, 10) + colors = span.NewLinearColor(color.RGBA{0, 0, 0, 0}, color.RGBA{255, 255, 255, 255}) ) const ( diff --git a/render/switch.go b/render/switch.go index 9387205f..6a38aa53 100644 --- a/render/switch.go +++ b/render/switch.go @@ -5,10 +5,10 @@ import ( "image/draw" "sync" - "github.com/oakmound/oak/v3/event" - "github.com/oakmound/oak/v3/oakerr" - "github.com/oakmound/oak/v3/physics" - "github.com/oakmound/oak/v3/render/mod" + "github.com/oakmound/oak/v4/event" + "github.com/oakmound/oak/v4/oakerr" + "github.com/oakmound/oak/v4/physics" + "github.com/oakmound/oak/v4/render/mod" ) // The Switch type is intended for use to easily swap between multiple @@ -187,7 +187,7 @@ func (c *Switch) IsStatic() bool { // Todo: standardize this with the other interface Set functions so that it // also only acts on the current subRenderable, or the other way around, or // somehow offer both options -func (c *Switch) SetTriggerID(cid event.CID) { +func (c *Switch) SetTriggerID(cid event.CallerID) { c.lock.RLock() for _, r := range c.subRenderables { if t, ok := r.(Triggerable); ok { diff --git a/render/switch_test.go b/render/switch_test.go index e9ff6585..ca286ab8 100644 --- a/render/switch_test.go +++ b/render/switch_test.go @@ -6,8 +6,8 @@ import ( "reflect" "testing" - "github.com/oakmound/oak/v3/physics" - "github.com/oakmound/oak/v3/render/mod" + "github.com/oakmound/oak/v4/physics" + "github.com/oakmound/oak/v4/render/mod" ) func TestCompoundFuncs(t *testing.T) { diff --git a/render/testdata/assets/fonts/seguiemj.ttf b/render/testdata/assets/fonts/seguiemj.ttf new file mode 100644 index 00000000..77497bf7 Binary files /dev/null and b/render/testdata/assets/fonts/seguiemj.ttf differ diff --git a/render/text.go b/render/text.go index 9b3a00d8..88131260 100644 --- a/render/text.go +++ b/render/text.go @@ -5,7 +5,7 @@ import ( "image/draw" "strconv" - "github.com/oakmound/oak/v3/alg" + "github.com/oakmound/oak/v4/alg" "golang.org/x/image/math/fixed" ) diff --git a/scene.go b/scene.go index a59310cf..a6ce44e8 100644 --- a/scene.go +++ b/scene.go @@ -3,11 +3,11 @@ package oak import ( "time" - "github.com/oakmound/oak/v3/scene" - "github.com/oakmound/oak/v3/timing" + "github.com/oakmound/oak/v4/scene" + "github.com/oakmound/oak/v4/timing" ) -// AddScene is shorthand for c.SceneMap.AddScene +// AddScene is shorthand for w.SceneMap.AddScene func (w *Window) AddScene(name string, s scene.Scene) error { return w.SceneMap.AddScene(name, s) } diff --git a/scene/context.go b/scene/context.go index 4121b2b9..ef337127 100644 --- a/scene/context.go +++ b/scene/context.go @@ -3,31 +3,37 @@ package scene import ( "context" - "github.com/oakmound/oak/v3/collision" - "github.com/oakmound/oak/v3/event" - "github.com/oakmound/oak/v3/key" - "github.com/oakmound/oak/v3/render" - "github.com/oakmound/oak/v3/window" + "github.com/oakmound/oak/v4/collision" + "github.com/oakmound/oak/v4/event" + "github.com/oakmound/oak/v4/key" + "github.com/oakmound/oak/v4/render" ) // A Context contains all transient engine components used in a scene, including // the draw stack, event bus, known event callers, collision trees, keyboard state, // and a reference to the OS window itself. When a scene ends, modifications made // to these structures will be reset, excluding window modifications. -// TODO oak v4: consider embedding these system objects on the context to change -// ctx.DrawStack.Draw to ctx.Draw and ctx.EventHandler.Bind to ctx.Bind type Context struct { // This context will be canceled when the scene ends context.Context PreviousScene string SceneInput interface{} - Window window.Window + Window Window + + *event.CallerMap + event.Handler + *render.DrawStack + *key.State - DrawStack *render.DrawStack - EventHandler event.Handler - CallerMap *event.CallerMap MouseTree *collision.Tree CollisionTree *collision.Tree - KeyState *key.State +} + +// DoEachFrame is a helper method to call a function on each frame for the duration of this scene. +func (ctx *Context) DoEachFrame(f func()) { + event.GlobalBind(ctx, event.Enter, func(_ event.EnterPayload) event.Response { + f() + return 0 + }) } diff --git a/scene/context_desktop.go b/scene/context_desktop.go new file mode 100644 index 00000000..7777bdb6 --- /dev/null +++ b/scene/context_desktop.go @@ -0,0 +1,8 @@ +//go:build !js && !android && !nooswindow +// +build !js,!android,!nooswindow + +package scene + +import "github.com/oakmound/oak/v4/window" + +type Window = window.Window diff --git a/scene/context_other.go b/scene/context_other.go new file mode 100644 index 00000000..153661f3 --- /dev/null +++ b/scene/context_other.go @@ -0,0 +1,8 @@ +//go:build js || android || nooswindow +// +build js android nooswindow + +package scene + +import "github.com/oakmound/oak/v4/window" + +type Window = window.App diff --git a/scene/delay.go b/scene/delay.go index 524418c8..fd6682ea 100644 --- a/scene/delay.go +++ b/scene/delay.go @@ -4,7 +4,7 @@ import ( "context" "time" - "github.com/oakmound/oak/v3/render" + "github.com/oakmound/oak/v4/render" ) // DoAfter will execute the given function after some duration. When the scene diff --git a/scene/delay_test.go b/scene/delay_test.go index 96b48aed..518e9326 100644 --- a/scene/delay_test.go +++ b/scene/delay_test.go @@ -6,7 +6,7 @@ import ( "testing" "time" - "github.com/oakmound/oak/v3/render" + "github.com/oakmound/oak/v4/render" ) func TestDoAfterCancels(t *testing.T) { diff --git a/scene/example_test.go b/scene/example_test.go index a75894fc..dbeeb830 100644 --- a/scene/example_test.go +++ b/scene/example_test.go @@ -3,7 +3,7 @@ package scene_test import ( "fmt" - "github.com/oakmound/oak/v3/scene" + "github.com/oakmound/oak/v4/scene" ) func ExampleMap_GetCurrent() { diff --git a/scene/map.go b/scene/map.go index 6687d260..346f6463 100644 --- a/scene/map.go +++ b/scene/map.go @@ -3,7 +3,7 @@ package scene import ( "sync" - "github.com/oakmound/oak/v3/oakerr" + "github.com/oakmound/oak/v4/oakerr" ) // A Map lets scenes be accessed via associated names. @@ -41,16 +41,12 @@ func (m *Map) GetCurrent() (Scene, bool) { // conflict with an existing name in the map, and then adds it to the map. // If a conflict occurs, the scene will not be overwritten. // Checks if the Scene's start is nil, sets to noop if so. -// Checks if the Scene's loop is nil, sets to infinite if so. // Checks if the Scene's end is nil, sets to loop to this scene if so. func (m *Map) AddScene(name string, s Scene) error { if s.Start == nil { s.Start = func(*Context) {} } - if s.Loop == nil { - s.Loop = func() bool { return true } - } if s.End == nil { s.End = GoTo(name) } diff --git a/scene/map_test.go b/scene/map_test.go index 91e4c424..bf029400 100644 --- a/scene/map_test.go +++ b/scene/map_test.go @@ -5,7 +5,7 @@ import ( "image" "testing" - "github.com/oakmound/oak/v3/oakerr" + "github.com/oakmound/oak/v4/oakerr" ) func TestMap(t *testing.T) { @@ -77,9 +77,6 @@ func TestAddScene(t *testing.T) { t.Fatalf("getting test scene failed") } - if !test1.Loop() { - t.Fatalf("test loop failed") - } eStr, _ := test1.End() if eStr != "test1" { t.Fatalf("looping test end did not return test1, got %v", eStr) diff --git a/scene/scene.go b/scene/scene.go index 2e7d7420..93554e3f 100644 --- a/scene/scene.go +++ b/scene/scene.go @@ -1,20 +1,17 @@ package scene import ( - "github.com/oakmound/oak/v3/dlog" - "github.com/oakmound/oak/v3/oakerr" + "github.com/oakmound/oak/v4/dlog" + "github.com/oakmound/oak/v4/oakerr" ) // A Scene is a set of functions defining what needs to happen when a scene -// starts, loops, and ends. +// starts and ends. type Scene struct { // Start is called when a scene begins, including contextual information like // what scene came before this one and a direct reference to clean data structures // for event handling and rendering Start func(ctx *Context) - // If Loop returns true, the scene will continue - // If Loop returns false, End will be called to determine the next scene - Loop func() (cont bool) // End is a function returning the next scene and a SceneResult of // input settings for the next scene. End func() (nextScene string, result *Result) @@ -27,19 +24,6 @@ type Result struct { Transition } -// BooleanLoop returns a Loop function that will end a scene as soon as the -// input boolean is false, resetting it to true in the process for the -// next scene -func BooleanLoop(b *bool) func() (cont bool) { - return func() bool { - if !(*b) { - *b = true - return false - } - return true - } -} - // GoTo returns an End function that, without any other customization possible, // will change to the input next scene. func GoTo(nextScene string) func() (nextScene string, result *Result) { diff --git a/scene/scene_test.go b/scene/scene_test.go index 5d4946c7..683f240e 100644 --- a/scene/scene_test.go +++ b/scene/scene_test.go @@ -5,21 +5,6 @@ import ( "testing" ) -func TestBooleanLoop(t *testing.T) { - v := true - l := BooleanLoop(&v) - if !l() { - t.Fatalf("boolean loop should loop when true") - } - v = false - if l() { - t.Fatalf("boolean loop should not loop when false") - } - if !l() { - t.Fatalf("boolean loop should resume looping after returning false") - } -} - func randString() string { length := rand.Intn(100) data := make([]byte, length) diff --git a/scene/transition.go b/scene/transition.go index 94818c1a..03bf73a3 100644 --- a/scene/transition.go +++ b/scene/transition.go @@ -4,39 +4,21 @@ import ( "image" "image/draw" - "github.com/oakmound/oak/v3/render/mod" -) - -var ( - zeroPoint = image.Point{X: 0, Y: 0} + "github.com/oakmound/oak/v4/render/mod" ) // Transition functions can be set to occur at the end of a scene. type Transition func(*image.RGBA, int) bool -// Fade is a scene transition that fades to black at a given rate for -// a total of 'frames' frames -func Fade(rate float32, frames int) func(*image.RGBA, int) bool { - rate *= -1 - return func(buf *image.RGBA, frame int) bool { - if frame > frames { - return false - } - i := float32(frame) - mod.Brighten(rate * i)(buf) - return true - } -} - // Zoom transitions by performing a simplistic zoom each frame towards some -// percentange-based part of the screen. -func Zoom(xPerc, yPerc float64, frames int, zoomRate float64) func(*image.RGBA, int) bool { +// percentage-based part of the screen. +func Zoom(xPerc, yPerc float64, frames int, zoomRate float64) Transition { return func(buf *image.RGBA, frame int) bool { if frame > frames { return false } z := mod.Zoom(xPerc, yPerc, 1+zoomRate*float64(frame)) - draw.Draw(buf, buf.Bounds(), z(buf), zeroPoint, draw.Src) + draw.Draw(buf, buf.Bounds(), z(buf), image.ZP, draw.Src) return true } } diff --git a/scene/transition_gift.go b/scene/transition_gift.go new file mode 100644 index 00000000..3c58631d --- /dev/null +++ b/scene/transition_gift.go @@ -0,0 +1,24 @@ +//go:build !nogift +// +build !nogift + +package scene + +import ( + "image" + + "github.com/oakmound/oak/v4/render/mod" +) + +// Fade is a scene transition that fades to black at a given rate for +// a total of 'frames' frames +func Fade(rate float32, frames int) Transition { + rate *= -1 + return func(buf *image.RGBA, frame int) bool { + if frame > frames { + return false + } + i := float32(frame) + mod.Brighten(rate * i)(buf) + return true + } +} diff --git a/sceneLoop.go b/sceneLoop.go index e4a31e8c..3fc9fc74 100644 --- a/sceneLoop.go +++ b/sceneLoop.go @@ -3,11 +3,12 @@ package oak import ( "context" - "github.com/oakmound/oak/v3/alg/intgeom" - "github.com/oakmound/oak/v3/dlog" - "github.com/oakmound/oak/v3/event" - "github.com/oakmound/oak/v3/oakerr" - "github.com/oakmound/oak/v3/scene" + "github.com/oakmound/oak/v4/alg/intgeom" + "github.com/oakmound/oak/v4/dlog" + "github.com/oakmound/oak/v4/event" + "github.com/oakmound/oak/v4/oakerr" + "github.com/oakmound/oak/v4/scene" + "github.com/oakmound/oak/v4/timing" ) // the oak loading scene is a reserved scene @@ -15,21 +16,11 @@ import ( const oakLoadingScene = "oak:loading" func (w *Window) sceneLoop(first string, trackingInputs bool) { - w.SceneMap.AddScene(oakLoadingScene, scene.Scene{ - Loop: func() bool { - return w.startupLoading - }, - End: func() (string, *scene.Result) { - return w.firstScene, &scene.Result{ - NextSceneInput: w.FirstSceneInput, - } - }, - }) - var prevScene string result := new(scene.Result) + // kick start the draw loop w.drawCh <- struct{}{} w.drawCh <- struct{}{} @@ -38,7 +29,7 @@ func (w *Window) sceneLoop(first string, trackingInputs bool) { w.SceneMap.CurrentScene = oakLoadingScene for { - w.setViewport(intgeom.Point2{0, 0}) + w.SetViewport(intgeom.Point2{0, 0}) w.RemoveViewportBounds() dlog.Info(dlog.SceneStarting, w.SceneMap.CurrentScene) @@ -67,18 +58,17 @@ func (w *Window) sceneLoop(first string, trackingInputs bool) { PreviousScene: prevScene, SceneInput: result.NextSceneInput, DrawStack: w.DrawStack, - EventHandler: w.eventHandler, + Handler: w.eventHandler, CallerMap: w.CallerMap, MouseTree: w.MouseTree, CollisionTree: w.CollisionTree, Window: w, - KeyState: &w.State, + State: &w.State, }) w.transitionCh <- struct{}{} }() w.sceneTransition(result) - // Post transition, begin loading animation w.drawCh <- struct{}{} <-w.transitionCh @@ -86,29 +76,25 @@ func (w *Window) sceneLoop(first string, trackingInputs bool) { w.drawCh <- struct{}{} dlog.Info(dlog.SceneLooping) - cont := true - - dlog.ErrorCheck(w.eventHandler.UpdateLoop(w.FrameRate, w.sceneCh)) + enterCancel := event.EnterLoop(w.eventHandler, timing.FPSToFrameDelay(w.FrameRate)) nextSceneOverride := "" - for cont { - select { - case <-w.ParentContext.Done(): - case <-w.quitCh: - cancel() - return - case <-w.sceneCh: - cont = scen.Loop() - case nextSceneOverride = <-w.skipSceneCh: - cont = false - } + select { + case <-w.ParentContext.Done(): + w.Quit() + cancel() + return + case <-w.quitCh: + cancel() + return + case nextSceneOverride = <-w.skipSceneCh: } cancel() dlog.Info(dlog.SceneEnding, w.SceneMap.CurrentScene) // We don't want enterFrames going off between scenes - dlog.ErrorCheck(w.eventHandler.Stop()) + enterCancel() prevScene = w.SceneMap.CurrentScene // Send a signal to stop drawing @@ -124,15 +110,8 @@ func (w *Window) sceneLoop(first string, trackingInputs bool) { // be triggered and attempt to access an entity w.CollisionTree.Clear() w.MouseTree.Clear() - if w.CallerMap == event.DefaultCallerMap { - event.ResetCallerMap() - w.CallerMap = event.DefaultCallerMap - } else { - w.CallerMap = event.NewCallerMap() - } - if cmr, ok := w.eventHandler.(event.CallerMapper); ok { - cmr.SetCallerMap(w.CallerMap) - } + w.CallerMap.Clear() + w.eventHandler.SetCallerMap(w.CallerMap) w.DrawStack.Clear() w.DrawStack.PreDraw() diff --git a/sceneLoop_test.go b/sceneLoop_test.go index fb48c25b..89d6e8b0 100644 --- a/sceneLoop_test.go +++ b/sceneLoop_test.go @@ -4,7 +4,7 @@ import ( "testing" "time" - "github.com/oakmound/oak/v3/scene" + "github.com/oakmound/oak/v4/scene" ) func TestSceneLoopUnknownScene(t *testing.T) { diff --git a/scene_test.go b/scene_test.go index c92b7431..5b6f205d 100644 --- a/scene_test.go +++ b/scene_test.go @@ -1,9 +1,12 @@ package oak import ( + "context" + "errors" "testing" - "github.com/oakmound/oak/v3/scene" + "github.com/oakmound/oak/v4/oakerr" + "github.com/oakmound/oak/v4/scene" ) func TestSceneTransition(t *testing.T) { @@ -25,3 +28,35 @@ func TestSceneTransition(t *testing.T) { }) c1.Init("1") } + +func TestLoadingSceneClaimed(t *testing.T) { + c1 := NewWindow() + c1.AddScene(oakLoadingScene, scene.Scene{}) + err := c1.Init("1") + var wantErr oakerr.ExistingElement + if !errors.As(err, &wantErr) { + t.Fatalf("expected existing element error, got %v", err) + } +} + +func TestSceneGoTo(t *testing.T) { + c1 := NewWindow() + var cancel func() + c1.ParentContext, cancel = context.WithCancel(c1.ParentContext) + c1.AddScene("1", scene.Scene{ + Start: func(context *scene.Context) { + context.Window.GoToScene("good") + }, + End: func() (nextScene string, result *scene.Result) { + return "bad", &scene.Result{ + Transition: scene.Fade(1, 10), + } + }, + }) + c1.AddScene("good", scene.Scene{ + Start: func(ctx *scene.Context) { + cancel() + }, + }) + c1.Init("1") +} diff --git a/screenFilter.go b/screenFilter.go index 1e56aee3..b9789432 100644 --- a/screenFilter.go +++ b/screenFilter.go @@ -1,28 +1,27 @@ package oak import ( + "image" "image/color" - "github.com/oakmound/oak/v3/shiny/screen" - - "github.com/oakmound/oak/v3/render/mod" + "github.com/oakmound/oak/v4/render/mod" ) // SetPalette tells oak to conform the screen to the input color palette before drawing. func (w *Window) SetPalette(palette color.Palette) { - w.SetScreenFilter(mod.ConformToPallete(palette)) + w.SetDrawFilter(mod.ConformToPalette(palette)) } -// SetScreenFilter will filter the screen by the given modification function prior +// SetDrawFilter will filter the screen by the given modification function prior // to publishing the screen's rgba to be displayed. -func (w *Window) SetScreenFilter(screenFilter mod.Filter) { - w.prePublish = func(w *Window, tx screen.Texture) { - screenFilter(w.winBuffers[w.bufferIdx].RGBA()) +func (w *Window) SetDrawFilter(screenFilter mod.Filter) { + w.prePublish = func(buf *image.RGBA) { + screenFilter(buf) } } // ClearScreenFilter resets the draw function to no longer filter the screen before // publishing it to the window. func (w *Window) ClearScreenFilter() { - w.prePublish = func(*Window, screen.Texture) {} + w.prePublish = func(buf *image.RGBA) {} } diff --git a/screenFilter_test.go b/screenFilter_test.go new file mode 100644 index 00000000..d8d96992 --- /dev/null +++ b/screenFilter_test.go @@ -0,0 +1,18 @@ +package oak + +import ( + "image" + "image/color" + "testing" +) + +func TestScreenFilter(t *testing.T) { + c1 := NewWindow() + blackAndWhite := color.Palette{ + color.RGBA{0, 0, 0, 255}, + color.RGBA{255, 255, 255, 255}, + } + c1.SetPalette(blackAndWhite) + buf := image.NewRGBA(image.Rect(0, 0, 1, 1)) + c1.prePublish(buf) +} diff --git a/screenOpts.go b/screenOpts.go deleted file mode 100644 index 9e76788e..00000000 --- a/screenOpts.go +++ /dev/null @@ -1,135 +0,0 @@ -package oak - -import "github.com/oakmound/oak/v3/oakerr" - -type fullScreenable interface { - SetFullScreen(bool) error -} - -// SetFullScreen attempts to set the local oak window to be full screen. -// If the window does not support this functionality, it will report as such. -func (w *Window) SetFullScreen(on bool) error { - if fs, ok := w.windowControl.(fullScreenable); ok { - return fs.SetFullScreen(on) - } - return oakerr.UnsupportedPlatform{ - Operation: "SetFullScreen", - } -} - -type movableWindow interface { - MoveWindow(x, y, w, h int32) error -} - -// MoveWindow sets the position of a window to be x,y and it's dimensions to w,h -// If the window does not support being positioned, it will report as such. -func (w *Window) MoveWindow(x, y, wd, h int) error { - if mw, ok := w.windowControl.(movableWindow); ok { - return mw.MoveWindow(int32(x), int32(y), int32(wd), int32(h)) - } - return oakerr.UnsupportedPlatform{ - Operation: "MoveWindow", - } -} - -type borderlesser interface { - SetBorderless(bool) error -} - -// SetBorderless attempts to set the local oak window to have no border. -// If the window does not support this functionaltiy, it will report as such. -func (w *Window) SetBorderless(on bool) error { - if bs, ok := w.windowControl.(borderlesser); ok { - return bs.SetBorderless(on) - } - return oakerr.UnsupportedPlatform{ - Operation: "SetBorderless", - } -} - -type topMoster interface { - SetTopMost(bool) error -} - -// SetTopMost attempts to set the local oak window to stay on top of other windows. -// If the window does not support this functionality, it will report as such. -func (w *Window) SetTopMost(on bool) error { - if tm, ok := w.windowControl.(topMoster); ok { - return tm.SetTopMost(on) - } - return oakerr.UnsupportedPlatform{ - Operation: "SetTopMost", - } -} - -type titler interface { - SetTitle(string) error -} - -// SetTitle sets this window's title. -func (w *Window) SetTitle(title string) error { - if t, ok := w.windowControl.(titler); ok { - return t.SetTitle(title) - } - return oakerr.UnsupportedPlatform{ - Operation: "SetTitle", - } -} - -type trayIconer interface { - SetTrayIcon(string) error -} - -// SetTrayIcon sets a application tray icon for this program. -func (w *Window) SetTrayIcon(icon string) error { - if t, ok := w.windowControl.(trayIconer); ok { - return t.SetTrayIcon(icon) - } - return oakerr.UnsupportedPlatform{ - Operation: "SetTrayIcon", - } -} - -type trayNotifier interface { - ShowNotification(title, msg string, icon bool) error -} - -// ShowNotification shows a text notification, optionally using a previously set -// tray icon. -func (w *Window) ShowNotification(title, msg string, icon bool) error { - if t, ok := w.windowControl.(trayNotifier); ok { - return t.ShowNotification(title, msg, icon) - } - return oakerr.UnsupportedPlatform{ - Operation: "ShowNotification", - } -} - -type cursorHider interface { - HideCursor() error -} - -// HideCursor disables showing the cursor when it is over this window. -func (w *Window) HideCursor() error { - if t, ok := w.windowControl.(cursorHider); ok { - return t.HideCursor() - } - return oakerr.UnsupportedPlatform{ - Operation: "HideCursor", - } -} - -type getCursorPositioner interface { - GetCursorPosition() (x, y float64) -} - -// GetCursorPosition returns the cusor position relative to the top left corner of this window. -func (w *Window) GetCursorPosition() (x, y float64, err error) { - if wp, ok := w.windowControl.(getCursorPositioner); ok { - x, y := wp.GetCursorPosition() - return x, y, nil - } - return 0, 0, oakerr.UnsupportedPlatform{ - Operation: "GetCursorPosition", - } -} diff --git a/screenOpts_test.go b/screenOpts_test.go deleted file mode 100644 index be8dca28..00000000 --- a/screenOpts_test.go +++ /dev/null @@ -1,22 +0,0 @@ -package oak - -import "testing" - -func TestScreenOpts(t *testing.T) { - // What these functions do (and error presence) depends on the operating - // system / build tags, which we can't configure at test time without - // making a new driver just for this test. - c1 := blankScene(t) - c1.SetFullScreen(true) - c1.SetFullScreen(false) - c1.MoveWindow(10, 10, 20, 20) - c1.SetBorderless(true) - c1.SetBorderless(false) - c1.SetTopMost(true) - c1.SetTopMost(false) - c1.SetTitle("testScreenOpts") - c1.SetTrayIcon("icon.ico") - c1.ShowNotification("testnotification", "testmessge", true) - c1.ShowNotification("testnotification", "testmessge", false) - c1.HideCursor() -} diff --git a/screenshot.go b/screenshot.go index 32105885..f0ca1b55 100644 --- a/screenshot.go +++ b/screenshot.go @@ -6,8 +6,6 @@ import ( "image/draw" "image/gif" "time" - - "github.com/oakmound/oak/v3/shiny/screen" ) // ScreenShot takes a snap shot of the window's image content. @@ -17,9 +15,8 @@ func (w *Window) ScreenShot() *image.RGBA { shotCh := make(chan *image.RGBA) // We need to take the shot when the screen is not being redrawn // We know the screen has everything drawn on it when it is published - w.prePublish = func(w *Window, tx screen.Texture) { + w.prePublish = func(rgba *image.RGBA) { // Copy the buffer - rgba := w.winBuffers[w.bufferIdx].RGBA() bds := rgba.Bounds() copy := image.NewRGBA(bds) for x := bds.Min.X; x < bds.Max.X; x++ { @@ -39,9 +36,8 @@ func (w *Window) gifShot() *image.Paletted { shotCh := make(chan *image.Paletted) // We need to take the shot when the screen is not being redrawn // We know the screen has everything drawn on it when it is published - w.prePublish = func(w *Window, tx screen.Texture) { + w.prePublish = func(rgba *image.RGBA) { // Copy the buffer - rgba := w.winBuffers[w.bufferIdx].RGBA() bds := rgba.Bounds() copy := image.NewPaletted(bds, palette.Plan9) draw.Draw(copy, bds, rgba, zeroPoint, draw.Src) diff --git a/screenshot_test.go b/screenshot_test.go index 7c38ae9d..412c6b08 100644 --- a/screenshot_test.go +++ b/screenshot_test.go @@ -7,7 +7,7 @@ import ( "testing" "time" - "github.com/oakmound/oak/v3/scene" + "github.com/oakmound/oak/v4/scene" ) func blankScene(t *testing.T) *Window { diff --git a/entities/x/shake/shake.go b/shake/shake.go similarity index 88% rename from entities/x/shake/shake.go rename to shake/shake.go index e05d3adb..b03b89e0 100644 --- a/entities/x/shake/shake.go +++ b/shake/shake.go @@ -1,3 +1,4 @@ +// Package shake provides methods for rapidly shifting graphical components' positions package shake import ( @@ -5,9 +6,10 @@ import ( "math/rand" "time" - "github.com/oakmound/oak/v3/alg/floatgeom" - "github.com/oakmound/oak/v3/scene" - "github.com/oakmound/oak/v3/window" + "github.com/oakmound/oak/v4/alg/floatgeom" + "github.com/oakmound/oak/v4/alg/intgeom" + "github.com/oakmound/oak/v4/scene" + "github.com/oakmound/oak/v4/window" ) // A Shaker knows how to shake something by a (or up to a) given magnitude. @@ -24,7 +26,7 @@ type Shaker struct { } var ( - // DefaultShaker is the global default shaker, used when ShakeScreen is called. + // DefaultShaker is the global default shaker, used when shake.Screen or shake.Shake are called. DefaultShaker = &Shaker{ Random: false, Magnitude: floatgeom.Point2{3.0, 3.0}, @@ -110,7 +112,7 @@ type screenToPoser struct { } func (stp screenToPoser) ShiftPos(x, y float64) { - stp.ShiftScreen(int(x), int(y)) + stp.ShiftViewport(intgeom.Point2{int(x), int(y)}) } // Screen shakes the screen that the context controls for the given duration. diff --git a/shape/bezier.go b/shape/bezier.go index c7cce062..30b44c8a 100644 --- a/shape/bezier.go +++ b/shape/bezier.go @@ -1,8 +1,8 @@ package shape import ( - "github.com/oakmound/oak/v3/alg/floatgeom" - "github.com/oakmound/oak/v3/oakerr" + "github.com/oakmound/oak/v4/alg/floatgeom" + "github.com/oakmound/oak/v4/oakerr" ) // BezierCurve will form a Bezier on the given coordinates, expected in (x,y) @@ -64,13 +64,3 @@ type BezierPoint floatgeom.Point2 func (bp BezierPoint) Pos(float64) (x, y float64) { return bp[0], bp[1] } - -// X returns bp's value on the X axis. -func (bp BezierPoint) X() float64 { - return bp[0] -} - -// Y returns bp's value on the Y axis. -func (bp BezierPoint) Y() float64 { - return bp[1] -} diff --git a/shape/bezier_test.go b/shape/bezier_test.go index 6fb9cec3..d5b16a73 100644 --- a/shape/bezier_test.go +++ b/shape/bezier_test.go @@ -6,7 +6,7 @@ import ( "testing" "time" - "github.com/oakmound/oak/v3/oakerr" + "github.com/oakmound/oak/v4/oakerr" ) const randTestCt = 100 diff --git a/shape/condense.go b/shape/condense.go index 1d55a782..f9fd8aaf 100644 --- a/shape/condense.go +++ b/shape/condense.go @@ -1,6 +1,6 @@ package shape -import "github.com/oakmound/oak/v3/alg/intgeom" +import "github.com/oakmound/oak/v4/alg/intgeom" // Condense finds a set of rectangles that covers the shape. // Used to return a minimal set of rectangles in an appropriate time. diff --git a/shape/condense_test.go b/shape/condense_test.go index 0564b2f3..25bf2101 100644 --- a/shape/condense_test.go +++ b/shape/condense_test.go @@ -5,7 +5,7 @@ import ( "sort" "testing" - "github.com/oakmound/oak/v3/alg/intgeom" + "github.com/oakmound/oak/v4/alg/intgeom" ) func TestCondense(t *testing.T) { diff --git a/shape/holes.go b/shape/holes.go index 33629a28..805b5741 100644 --- a/shape/holes.go +++ b/shape/holes.go @@ -1,7 +1,7 @@ package shape import ( - "github.com/oakmound/oak/v3/alg/intgeom" + "github.com/oakmound/oak/v4/alg/intgeom" ) // GetHoles finds sets of points which are not In this shape that diff --git a/shape/holes_test.go b/shape/holes_test.go index 8d36c804..9c6d6cb5 100644 --- a/shape/holes_test.go +++ b/shape/holes_test.go @@ -3,7 +3,7 @@ package shape import ( "testing" - "github.com/oakmound/oak/v3/alg/intgeom" + "github.com/oakmound/oak/v4/alg/intgeom" ) func TestHoles(t *testing.T) { diff --git a/shape/in.go b/shape/in.go index caee4df8..f5658f53 100644 --- a/shape/in.go +++ b/shape/in.go @@ -3,7 +3,7 @@ package shape import ( "math" - "github.com/oakmound/oak/v3/alg/intgeom" + "github.com/oakmound/oak/v4/alg/intgeom" ) // In functions return whether the given coordinate lies diff --git a/shape/outline.go b/shape/outline.go index 4bbde964..c884b6f0 100644 --- a/shape/outline.go +++ b/shape/outline.go @@ -4,7 +4,7 @@ import ( "errors" "math" - "github.com/oakmound/oak/v3/alg/intgeom" + "github.com/oakmound/oak/v4/alg/intgeom" ) const ( diff --git a/shape/points.go b/shape/points.go index bccea8e8..1e31a377 100644 --- a/shape/points.go +++ b/shape/points.go @@ -1,7 +1,7 @@ package shape import ( - "github.com/oakmound/oak/v3/alg/intgeom" + "github.com/oakmound/oak/v4/alg/intgeom" ) // Points is a shape defined by a set of points. diff --git a/shape/points_test.go b/shape/points_test.go index c9afc875..b72e50d2 100644 --- a/shape/points_test.go +++ b/shape/points_test.go @@ -3,7 +3,7 @@ package shape import ( "testing" - "github.com/oakmound/oak/v3/alg/intgeom" + "github.com/oakmound/oak/v4/alg/intgeom" ) var ( diff --git a/shape/rect.go b/shape/rect.go index f2110fa7..17b56e40 100644 --- a/shape/rect.go +++ b/shape/rect.go @@ -1,7 +1,7 @@ package shape import ( - "github.com/oakmound/oak/v3/alg/intgeom" + "github.com/oakmound/oak/v4/alg/intgeom" ) // A Rect is a function that returns a 2d boolean array diff --git a/shape/shape.go b/shape/shape.go index 706d5403..4c9de64b 100644 --- a/shape/shape.go +++ b/shape/shape.go @@ -1,6 +1,6 @@ package shape -import "github.com/oakmound/oak/v3/alg/intgeom" +import "github.com/oakmound/oak/v4/alg/intgeom" // A Shape represents a rectangle of width/height size // where for each x,y coordinate, either that value lies diff --git a/shiny/doc.go b/shiny/doc.go new file mode 100644 index 00000000..9f0d5f99 --- /dev/null +++ b/shiny/doc.go @@ -0,0 +1,2 @@ +// Package shiny provides interfaces and drivers for instantiating and managing application windows +package shiny diff --git a/shiny/driver/androiddriver/image.go b/shiny/driver/androiddriver/image.go index e5d58786..c8fa2134 100644 --- a/shiny/driver/androiddriver/image.go +++ b/shiny/driver/androiddriver/image.go @@ -11,7 +11,7 @@ import ( ) type imageImpl struct { - screen *screenImpl + screen *Screen size image.Point img *glutil.Image deadLock sync.Mutex diff --git a/shiny/driver/androiddriver/main.go b/shiny/driver/androiddriver/main.go index 3f2e8aaf..597ffa93 100644 --- a/shiny/driver/androiddriver/main.go +++ b/shiny/driver/androiddriver/main.go @@ -6,7 +6,7 @@ package androiddriver import ( "sync" - "github.com/oakmound/oak/v3/shiny/screen" + "github.com/oakmound/oak/v4/shiny/screen" "golang.org/x/mobile/app" "golang.org/x/mobile/event/lifecycle" @@ -20,7 +20,7 @@ import ( func Main(f func(screen.Screen)) { app.Main(func(a app.App) { - s := &screenImpl{ + s := &Screen{ app: a, } screenOnce := sync.Once{} diff --git a/shiny/driver/androiddriver/screen.go b/shiny/driver/androiddriver/screen.go index 8ab94970..fe1bfb56 100644 --- a/shiny/driver/androiddriver/screen.go +++ b/shiny/driver/androiddriver/screen.go @@ -1,16 +1,15 @@ //go:build android // +build android +// Package androiddriver provides a Android driver for accessing a screen. package androiddriver import ( "image" - "image/color" - "github.com/oakmound/oak/v3/shiny/driver/internal/event" - "github.com/oakmound/oak/v3/shiny/screen" + "github.com/oakmound/oak/v4/shiny/driver/internal/event" + "github.com/oakmound/oak/v4/shiny/screen" "golang.org/x/image/draw" - "golang.org/x/image/math/f64" "golang.org/x/mobile/app" "golang.org/x/mobile/event/size" "golang.org/x/mobile/exp/gl/glutil" @@ -18,9 +17,9 @@ import ( "golang.org/x/mobile/gl" ) -var _ screen.Screen = &screenImpl{} +var _ screen.Screen = &Screen{} -type screenImpl struct { +type Screen struct { event.Deque app app.App @@ -32,7 +31,7 @@ type screenImpl struct { lastSz size.Event } -func (s *screenImpl) NewImage(size image.Point) (screen.Image, error) { +func (s *Screen) NewImage(size image.Point) (screen.Image, error) { img := &imageImpl{ screen: s, size: size, @@ -42,28 +41,22 @@ func (s *screenImpl) NewImage(size image.Point) (screen.Image, error) { return img, nil } -func (s *screenImpl) NewTexture(size image.Point) (screen.Texture, error) { +func (s *Screen) NewTexture(size image.Point) (screen.Texture, error) { return NewTexture(s, size), nil } -var _ screen.Window = &screenImpl{} +var _ screen.Window = &Screen{} -func (s *screenImpl) NewWindow(opts screen.WindowGenerator) (screen.Window, error) { +func (s *Screen) NewWindow(opts screen.WindowGenerator) (screen.Window, error) { // android does not support multiple windows return s, nil } -func (w *screenImpl) Publish() screen.PublishResult { - return screen.PublishResult{} -} +func (w *Screen) Publish() {} -func (w *screenImpl) Release() {} -func (w *screenImpl) Upload(dp image.Point, src screen.Image, sr image.Rectangle) {} -func (w *screenImpl) Fill(dr image.Rectangle, src color.Color, op draw.Op) {} -func (w *screenImpl) Draw(src2dst f64.Aff3, src screen.Texture, sr image.Rectangle, op draw.Op) {} -func (w *screenImpl) DrawUniform(src2dst f64.Aff3, src color.Color, sr image.Rectangle, op draw.Op) {} -func (w *screenImpl) Copy(dp image.Point, src screen.Texture, sr image.Rectangle, op draw.Op) {} -func (w *screenImpl) Scale(dr image.Rectangle, src screen.Texture, sr image.Rectangle, op draw.Op) { +func (w *Screen) Release() {} +func (w *Screen) Upload(dp image.Point, src screen.Image, sr image.Rectangle) {} +func (w *Screen) Scale(dr image.Rectangle, src screen.Texture, sr image.Rectangle, op draw.Op) { t := src.(*textureImpl) t.img.img.Draw( w.lastSz, diff --git a/shiny/driver/androiddriver/texture.go b/shiny/driver/androiddriver/texture.go index 25748d6c..6056eae0 100644 --- a/shiny/driver/androiddriver/texture.go +++ b/shiny/driver/androiddriver/texture.go @@ -8,16 +8,16 @@ import ( "image/color" "image/draw" - "github.com/oakmound/oak/v3/shiny/screen" + "github.com/oakmound/oak/v4/shiny/screen" ) type textureImpl struct { - screen *screenImpl + screen *Screen size image.Point img *imageImpl } -func NewTexture(s *screenImpl, size image.Point) *textureImpl { +func NewTexture(s *Screen, size image.Point) *textureImpl { return &textureImpl{ screen: s, size: size, diff --git a/shiny/driver/driver.go b/shiny/driver/driver.go index f4ac6122..7b74b4a7 100644 --- a/shiny/driver/driver.go +++ b/shiny/driver/driver.go @@ -11,7 +11,7 @@ package driver // or OpenGL library. import ( - "github.com/oakmound/oak/v3/shiny/screen" + "github.com/oakmound/oak/v4/shiny/screen" ) // Main is called by the program's main function to run the graphical @@ -23,8 +23,3 @@ import ( func Main(f func(screen.Screen)) { main(f) } - -// MonitorSize reports the size in pixels of the primary monitor. -func MonitorSize() (width int, height int) { - return monitorSize() -} diff --git a/shiny/driver/driver_android.go b/shiny/driver/driver_android.go index 1645acc5..2431fe9d 100644 --- a/shiny/driver/driver_android.go +++ b/shiny/driver/driver_android.go @@ -10,15 +10,12 @@ package driver import ( - "github.com/oakmound/oak/v3/shiny/driver/androiddriver" - "github.com/oakmound/oak/v3/shiny/screen" + "github.com/oakmound/oak/v4/shiny/driver/androiddriver" + "github.com/oakmound/oak/v4/shiny/screen" ) func main(f func(screen.Screen)) { androiddriver.Main(f) } -func monitorSize() (int, int) { - // GetSystemMetrics syscall - return 0, 0 -} +type Window = androiddriver.Screen diff --git a/shiny/driver/driver_darwin.go b/shiny/driver/driver_darwin.go deleted file mode 100644 index b561f207..00000000 --- a/shiny/driver/driver_darwin.go +++ /dev/null @@ -1,56 +0,0 @@ -// Copyright 2015 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -//go:build darwin && darwingl && !nooswindow && !android -// +build darwin,darwingl,!nooswindow,!android - -package driver - -import ( - "bytes" - "fmt" - "os/exec" - "regexp" - "strconv" - - "github.com/oakmound/oak/v3/shiny/driver/gldriver" - "github.com/oakmound/oak/v3/shiny/screen" -) - -func main(f func(screen.Screen)) { - gldriver.Main(f) -} - -var ( - sysProfRegex = regexp.MustCompile(`Resolution: (\d)* x (\d)*`) -) - -func monitorSize() (int, int) { - out, err := exec.Command("system_profiler", "SPDisplaysDataType").CombinedOutput() - if err != nil { - return 0, 0 - } - found := sysProfRegex.FindAll(out, -1) - if len(found) == 0 { - return 0, 0 - } - if len(found) != 1 { - fmt.Println("Found multiple screens", len(found)) - } - first := found[0] - first = bytes.TrimPrefix(first, []byte("Resolution: ")) - dims := bytes.Split(first, []byte(" x ")) - if len(dims) != 2 { - return 0, 0 - } - w, err := strconv.Atoi(string(dims[0])) - if err != nil { - return 0, 0 - } - h, err := strconv.Atoi(string(dims[1])) - if err != nil { - return 0, 0 - } - return w, h -} diff --git a/shiny/driver/driver_fallback.go b/shiny/driver/driver_fallback.go index c086af73..c9462f59 100644 --- a/shiny/driver/driver_fallback.go +++ b/shiny/driver/driver_fallback.go @@ -10,14 +10,12 @@ package driver import ( "errors" - "github.com/oakmound/oak/v3/shiny/driver/internal/errscreen" - "github.com/oakmound/oak/v3/shiny/screen" + "github.com/oakmound/oak/v4/shiny/driver/internal/errscreen" + "github.com/oakmound/oak/v4/shiny/screen" ) func main(f func(screen.Screen)) { f(errscreen.Stub(errors.New("no driver for accessing a screen"))) } -func monitorSize() (int, int) { - return 0, 0 -} +type Window = struct{} diff --git a/shiny/driver/driver_js.go b/shiny/driver/driver_js.go index 53e73e30..1f4808a4 100644 --- a/shiny/driver/driver_js.go +++ b/shiny/driver/driver_js.go @@ -1,17 +1,15 @@ //go:build js && !nooswindow && !windows && !darwin && !linux -// +build js,!nooswindow,!windows,!darwin,!linux,!android +// +build js,!nooswindow,!windows,!darwin,!linux package driver import ( - "github.com/oakmound/oak/v3/shiny/driver/jsdriver" - "github.com/oakmound/oak/v3/shiny/screen" + "github.com/oakmound/oak/v4/shiny/driver/jsdriver" + "github.com/oakmound/oak/v4/shiny/screen" ) func main(f func(screen.Screen)) { jsdriver.Main(f) } -func monitorSize() (int, int) { - return 0, 0 -} +type Window = jsdriver.Window diff --git a/shiny/driver/driver_noop.go b/shiny/driver/driver_noop.go index 38516784..1a5cf8c1 100644 --- a/shiny/driver/driver_noop.go +++ b/shiny/driver/driver_noop.go @@ -1,16 +1,15 @@ +//go:build nooswindow // +build nooswindow package driver import ( - "github.com/oakmound/oak/v3/shiny/driver/noop" - "github.com/oakmound/oak/v3/shiny/screen" + "github.com/oakmound/oak/v4/shiny/driver/noop" + "github.com/oakmound/oak/v4/shiny/screen" ) func main(f func(screen.Screen)) { noop.Main(f) } -func monitorSize() (int, int) { - return 0, 0 -} +type Window = noop.Window diff --git a/shiny/driver/driver_windows.go b/shiny/driver/driver_windows.go index 15cd3e5e..6c855053 100644 --- a/shiny/driver/driver_windows.go +++ b/shiny/driver/driver_windows.go @@ -8,15 +8,12 @@ package driver import ( - "github.com/oakmound/oak/v3/shiny/driver/windriver" - "github.com/oakmound/oak/v3/shiny/screen" + "github.com/oakmound/oak/v4/shiny/driver/windriver" + "github.com/oakmound/oak/v4/shiny/screen" ) func main(f func(screen.Screen)) { windriver.Main(f) } -func monitorSize() (int, int) { - // GetSystemMetrics syscall - return 0, 0 -} +type Window = windriver.Window diff --git a/shiny/driver/driver_x11.go b/shiny/driver/driver_x11.go index c2169238..cfdad0b2 100644 --- a/shiny/driver/driver_x11.go +++ b/shiny/driver/driver_x11.go @@ -2,20 +2,19 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. +//go:build ((linux && !android) || dragonfly || openbsd) && !nooswindow // +build linux,!android dragonfly openbsd // +build !nooswindow package driver import ( - "github.com/oakmound/oak/v3/shiny/driver/x11driver" - "github.com/oakmound/oak/v3/shiny/screen" + "github.com/oakmound/oak/v4/shiny/driver/x11driver" + "github.com/oakmound/oak/v4/shiny/screen" ) func main(f func(screen.Screen)) { x11driver.Main(f) } -func monitorSize() (int, int) { - return 0, 0 -} +type Window = x11driver.Window diff --git a/shiny/driver/gldriver/buffer.go b/shiny/driver/gldriver/buffer.go deleted file mode 100644 index 38df52a0..00000000 --- a/shiny/driver/gldriver/buffer.go +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright 2015 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package gldriver - -import "image" - -type bufferImpl struct { - // buf should always be equal to (i.e. the same ptr, len, cap as) rgba.Pix. - // It is a separate, redundant field in order to detect modifications to - // the rgba field that are invalid as per the screen.Image documentation. - buf []byte - rgba image.RGBA - size image.Point -} - -func (b *bufferImpl) Release() {} -func (b *bufferImpl) Size() image.Point { return b.size } -func (b *bufferImpl) Bounds() image.Rectangle { return image.Rectangle{Max: b.size} } -func (b *bufferImpl) RGBA() *image.RGBA { return &b.rgba } - -func (b *bufferImpl) preUpload() { - // Check that the program hasn't tried to modify the rgba field via the - // pointer returned by the bufferImpl.RGBA method. This check doesn't catch - // 100% of all cases; it simply tries to detect some invalid uses of a - // screen.Image such as: - // *buffer.RGBA() = anotherImageRGBA - if len(b.buf) != 0 && len(b.rgba.Pix) != 0 && &b.buf[0] != &b.rgba.Pix[0] { - panic("gldriver: invalid Buffer.RGBA modification") - } -} diff --git a/shiny/driver/gldriver/cocoa.go b/shiny/driver/gldriver/cocoa.go deleted file mode 100644 index fb7e82f3..00000000 --- a/shiny/driver/gldriver/cocoa.go +++ /dev/null @@ -1,676 +0,0 @@ -// Copyright 2015 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -// +build darwin -// +build 386 amd64 -// +build !ios - -package gldriver - -/* -#cgo CFLAGS: -x objective-c -DGL_SILENCE_DEPRECATION -#cgo LDFLAGS: -framework Cocoa -framework OpenGL -#include -#import // for HIToolbox/Events.h -#import -#include -#include -#include - -void startDriver(); -void stopDriver(); -void makeCurrentContext(uintptr_t ctx); -void flushContext(uintptr_t ctx); -uintptr_t doNewWindow(int width, int height, char* title); -void doShowWindow(uintptr_t id); -void doCloseWindow(uintptr_t id); -uint64_t threadID(); -*/ -import "C" - -import ( - "errors" - "fmt" - "log" - "runtime" - "unsafe" - - "github.com/oakmound/oak/v3/shiny/driver/internal/lifecycler" - "github.com/oakmound/oak/v3/shiny/screen" - "golang.org/x/mobile/event/key" - "golang.org/x/mobile/event/mouse" - "golang.org/x/mobile/event/paint" - "golang.org/x/mobile/event/size" - "golang.org/x/mobile/geom" - "golang.org/x/mobile/gl" -) - -const useLifecycler = true - -// TODO: change this to true, after manual testing on OS X. -const handleSizeEventsAtChannelReceive = false - -var initThreadID C.uint64_t - -func init() { - // Lock the goroutine responsible for initialization to an OS thread. - // This means the goroutine running main (and calling startDriver below) - // is locked to the OS thread that started the program. This is - // necessary for the correct delivery of Cocoa events to the process. - // - // A discussion on this topic: - // https://groups.google.com/forum/#!msg/golang-nuts/IiWZ2hUuLDA/SNKYYZBelsYJ - runtime.LockOSThread() - initThreadID = C.threadID() -} - -func newWindow(opts screen.WindowGenerator) (uintptr, error) { - width, height := optsSize(opts) - - title := C.CString(opts.Title) - defer C.free(unsafe.Pointer(title)) - - return uintptr(C.doNewWindow(C.int(width), C.int(height), title)), nil -} - -func moveWindow(w *windowImpl, opts screen.WindowGenerator) error { - // todo - return nil -} - -func initWindow(w *windowImpl) { - w.glctx, w.worker = gl.NewContext() -} - -func showWindow(w *windowImpl) { - C.doShowWindow(C.uintptr_t(w.id)) -} - -//export preparedOpenGL -func preparedOpenGL(id, ctx, vba uintptr) { - theScreen.mu.Lock() - w := theScreen.windows[id] - theScreen.mu.Unlock() - - w.ctx = ctx - go drawLoop(w, vba) -} - -func closeWindow(id uintptr) { - C.doCloseWindow(C.uintptr_t(id)) -} - -var mainCallback func(screen.Screen) - -func main(f func(screen.Screen)) error { - if tid := C.threadID(); tid != initThreadID { - log.Fatalf("gldriver.Main called on thread %d, but gldriver.init ran on %d", tid, initThreadID) - } - - mainCallback = f - C.startDriver() - return nil -} - -//export driverStarted -func driverStarted() { - go func() { - mainCallback(theScreen) - C.stopDriver() - }() -} - -//export drawgl -func drawgl(id uintptr) { - theScreen.mu.Lock() - w := theScreen.windows[id] - theScreen.mu.Unlock() - - if w == nil { - return // closing window - } - - // TODO: is this necessary? - w.lifecycler.SetVisible(true) - w.lifecycler.SendEvent(w, w.glctx) - - w.Send(paint.Event{External: true}) - <-w.drawDone -} - -// drawLoop is the primary drawing loop. -// -// After Cocoa has created an NSWindow and called prepareOpenGL, -// it starts drawLoop on a locked goroutine to handle OpenGL calls. -// -// The screen is drawn every time a paint.Event is received, which can be -// triggered either by the user or by Cocoa via drawgl (for example, when -// the window is resized). -func drawLoop(w *windowImpl, vba uintptr) { - runtime.LockOSThread() - C.makeCurrentContext(C.uintptr_t(w.ctx.(uintptr))) - - // Starting in OS X 10.11 (El Capitan), the vertex array is - // occasionally getting unbound when the context changes threads. - // - // Avoid this by binding it again. - C.glBindVertexArray(C.GLuint(vba)) - if errno := C.glGetError(); errno != 0 { - panic(fmt.Sprintf("gldriver: glBindVertexArray failed: %d", errno)) - } - - workAvailable := w.worker.WorkAvailable() - - // TODO(crawshaw): exit this goroutine on Release. - for { - select { - case <-workAvailable: - w.worker.DoWork() - case <-w.publish: - loop: - for { - select { - case <-workAvailable: - w.worker.DoWork() - default: - break loop - } - } - C.flushContext(C.uintptr_t(w.ctx.(uintptr))) - w.publishDone <- screen.PublishResult{} - } - } -} - -//export setGeom -func setGeom(id uintptr, ppp float32, widthPx, heightPx int) { - theScreen.mu.Lock() - w := theScreen.windows[id] - theScreen.mu.Unlock() - - if w == nil { - return // closing window - } - - sz := size.Event{ - WidthPx: widthPx, - HeightPx: heightPx, - WidthPt: geom.Pt(float32(widthPx) / ppp), - HeightPt: geom.Pt(float32(heightPx) / ppp), - PixelsPerPt: ppp, - } - - if !handleSizeEventsAtChannelReceive { - w.szMu.Lock() - w.sz = sz - w.szMu.Unlock() - } - - w.Send(sz) -} - -//export windowClosing -func windowClosing(id uintptr) { - sendLifecycle(id, (*lifecycler.State).SetDead, true) -} - -func sendWindowEvent(id uintptr, e interface{}) { - theScreen.mu.Lock() - w := theScreen.windows[id] - theScreen.mu.Unlock() - - if w == nil { - return // closing window - } - w.Send(e) -} - -var mods = [...]struct { - flags uint32 - code uint16 - mod key.Modifiers -}{ - // Left and right variants of modifier keys have their own masks, - // but they are not documented. These were determined empirically. - {1<<17 | 0x102, C.kVK_Shift, key.ModShift}, - {1<<17 | 0x104, C.kVK_RightShift, key.ModShift}, - {1<<18 | 0x101, C.kVK_Control, key.ModControl}, - {33<<13 | 0x100, C.kVK_RightControl, key.ModControl}, - {1<<19 | 0x120, C.kVK_Option, key.ModAlt}, - {1<<19 | 0x140, C.kVK_RightOption, key.ModAlt}, - {1<<20 | 0x108, C.kVK_Command, key.ModMeta}, - {1<<20 | 0x110, 0x36 /* kVK_RightCommand */, key.ModMeta}, -} - -func cocoaMods(flags uint32) (m key.Modifiers) { - for _, mod := range mods { - if flags&mod.flags == mod.flags { - m |= mod.mod - } - } - return m -} - -func cocoaMouseDir(ty int32) mouse.Direction { - switch ty { - case C.NSLeftMouseDown, C.NSRightMouseDown, C.NSOtherMouseDown: - return mouse.DirPress - case C.NSLeftMouseUp, C.NSRightMouseUp, C.NSOtherMouseUp: - return mouse.DirRelease - default: // dragged - return mouse.DirNone - } -} - -func cocoaMouseButton(button int32) mouse.Button { - switch button { - case 0: - return mouse.ButtonLeft - case 1: - return mouse.ButtonRight - case 2: - return mouse.ButtonMiddle - default: - return mouse.ButtonNone - } -} - -//export mouseEvent -func mouseEvent(id uintptr, x, y, dx, dy float32, ty, button int32, flags uint32) { - cmButton := mouse.ButtonNone - switch ty { - default: - cmButton = cocoaMouseButton(button) - case C.NSMouseMoved, C.NSLeftMouseDragged, C.NSRightMouseDragged, C.NSOtherMouseDragged: - // No-op. - case C.NSScrollWheel: - // Note that the direction of scrolling is inverted by default - // on OS X by the "natural scrolling" setting. At the Cocoa - // level this inversion is applied to trackpads and mice behind - // the scenes, and the value of dy goes in the direction the OS - // wants scrolling to go. - // - // This means the same trackpad/mouse motion on OS X and Linux - // can produce wheel events in opposite directions, but the - // direction matches what other programs on the OS do. - // - // If we wanted to expose the physical device motion in the - // event we could use [NSEvent isDirectionInvertedFromDevice] - // to know if "natural scrolling" is enabled. - // - // TODO: On a trackpad, a scroll can be a drawn-out affair with a - // distinct beginning and end. Should the intermediate events be - // DirNone? - // - // TODO: handle horizontal scrolling - button := mouse.ButtonWheelUp - if dy < 0 { - dy = -dy - button = mouse.ButtonWheelDown - } - e := mouse.Event{ - X: x, - Y: y, - Button: button, - Direction: mouse.DirStep, - Modifiers: cocoaMods(flags), - } - for delta := int(dy); delta != 0; delta-- { - sendWindowEvent(id, e) - } - return - } - sendWindowEvent(id, mouse.Event{ - X: x, - Y: y, - Button: cmButton, - Direction: cocoaMouseDir(ty), - Modifiers: cocoaMods(flags), - }) -} - -//export keyEvent -func keyEvent(id uintptr, runeVal rune, dir uint8, code uint16, flags uint32) { - sendWindowEvent(id, key.Event{ - Rune: cocoaRune(runeVal), - Direction: key.Direction(dir), - Code: cocoaKeyCode(code), - Modifiers: cocoaMods(flags), - }) -} - -//export flagEvent -func flagEvent(id uintptr, flags uint32) { - for _, mod := range mods { - if flags&mod.flags == mod.flags && lastFlags&mod.flags != mod.flags { - keyEvent(id, -1, C.NSKeyDown, mod.code, flags) - } - if lastFlags&mod.flags == mod.flags && flags&mod.flags != mod.flags { - keyEvent(id, -1, C.NSKeyUp, mod.code, flags) - } - } - lastFlags = flags -} - -var lastFlags uint32 - -func sendLifecycle(id uintptr, setter func(*lifecycler.State, bool), val bool) { - theScreen.mu.Lock() - w := theScreen.windows[id] - theScreen.mu.Unlock() - - if w == nil { - return - } - setter(&w.lifecycler, val) - w.lifecycler.SendEvent(w, w.glctx) -} - -func sendLifecycleAll(dead bool) { - windows := []*windowImpl{} - - theScreen.mu.Lock() - for _, w := range theScreen.windows { - windows = append(windows, w) - } - theScreen.mu.Unlock() - - for _, w := range windows { - w.lifecycler.SetFocused(false) - w.lifecycler.SetVisible(false) - if dead { - w.lifecycler.SetDead(true) - } - w.lifecycler.SendEvent(w, w.glctx) - } -} - -//export lifecycleDeadAll -func lifecycleDeadAll() { sendLifecycleAll(true) } - -//export lifecycleHideAll -func lifecycleHideAll() { sendLifecycleAll(false) } - -//export lifecycleVisible -func lifecycleVisible(id uintptr, val bool) { - sendLifecycle(id, (*lifecycler.State).SetVisible, val) -} - -//export lifecycleFocused -func lifecycleFocused(id uintptr, val bool) { - sendLifecycle(id, (*lifecycler.State).SetFocused, val) -} - -// cocoaRune marks the Carbon/Cocoa private-range unicode rune representing -// a non-unicode key event to -1, used for Rune in the key package. -// -// http://www.unicode.org/Public/MAPPINGS/VENDORS/APPLE/CORPCHAR.TXT -func cocoaRune(r rune) rune { - if '\uE000' <= r && r <= '\uF8FF' { - return -1 - } - return r -} - -// cocoaKeyCode converts a Carbon/Cocoa virtual key code number -// into the standard keycodes used by the key package. -// -// To get a sense of the key map, see the diagram on -// http://boredzo.org/blog/archives/2007-05-22/virtual-key-codes -func cocoaKeyCode(vkcode uint16) key.Code { - switch vkcode { - case C.kVK_ANSI_A: - return key.CodeA - case C.kVK_ANSI_B: - return key.CodeB - case C.kVK_ANSI_C: - return key.CodeC - case C.kVK_ANSI_D: - return key.CodeD - case C.kVK_ANSI_E: - return key.CodeE - case C.kVK_ANSI_F: - return key.CodeF - case C.kVK_ANSI_G: - return key.CodeG - case C.kVK_ANSI_H: - return key.CodeH - case C.kVK_ANSI_I: - return key.CodeI - case C.kVK_ANSI_J: - return key.CodeJ - case C.kVK_ANSI_K: - return key.CodeK - case C.kVK_ANSI_L: - return key.CodeL - case C.kVK_ANSI_M: - return key.CodeM - case C.kVK_ANSI_N: - return key.CodeN - case C.kVK_ANSI_O: - return key.CodeO - case C.kVK_ANSI_P: - return key.CodeP - case C.kVK_ANSI_Q: - return key.CodeQ - case C.kVK_ANSI_R: - return key.CodeR - case C.kVK_ANSI_S: - return key.CodeS - case C.kVK_ANSI_T: - return key.CodeT - case C.kVK_ANSI_U: - return key.CodeU - case C.kVK_ANSI_V: - return key.CodeV - case C.kVK_ANSI_W: - return key.CodeW - case C.kVK_ANSI_X: - return key.CodeX - case C.kVK_ANSI_Y: - return key.CodeY - case C.kVK_ANSI_Z: - return key.CodeZ - case C.kVK_ANSI_1: - return key.Code1 - case C.kVK_ANSI_2: - return key.Code2 - case C.kVK_ANSI_3: - return key.Code3 - case C.kVK_ANSI_4: - return key.Code4 - case C.kVK_ANSI_5: - return key.Code5 - case C.kVK_ANSI_6: - return key.Code6 - case C.kVK_ANSI_7: - return key.Code7 - case C.kVK_ANSI_8: - return key.Code8 - case C.kVK_ANSI_9: - return key.Code9 - case C.kVK_ANSI_0: - return key.Code0 - // TODO: move the rest of these codes to constants in key.go - // if we are happy with them. - case C.kVK_Return: - return key.CodeReturnEnter - case C.kVK_Escape: - return key.CodeEscape - case C.kVK_Delete: - return key.CodeDeleteBackspace - case C.kVK_Tab: - return key.CodeTab - case C.kVK_Space: - return key.CodeSpacebar - case C.kVK_ANSI_Minus: - return key.CodeHyphenMinus - case C.kVK_ANSI_Equal: - return key.CodeEqualSign - case C.kVK_ANSI_LeftBracket: - return key.CodeLeftSquareBracket - case C.kVK_ANSI_RightBracket: - return key.CodeRightSquareBracket - case C.kVK_ANSI_Backslash: - return key.CodeBackslash - // 50: Keyboard Non-US "#" and ~ - case C.kVK_ANSI_Semicolon: - return key.CodeSemicolon - case C.kVK_ANSI_Quote: - return key.CodeApostrophe - case C.kVK_ANSI_Grave: - return key.CodeGraveAccent - case C.kVK_ANSI_Comma: - return key.CodeComma - case C.kVK_ANSI_Period: - return key.CodeFullStop - case C.kVK_ANSI_Slash: - return key.CodeSlash - case C.kVK_CapsLock: - return key.CodeCapsLock - case C.kVK_F1: - return key.CodeF1 - case C.kVK_F2: - return key.CodeF2 - case C.kVK_F3: - return key.CodeF3 - case C.kVK_F4: - return key.CodeF4 - case C.kVK_F5: - return key.CodeF5 - case C.kVK_F6: - return key.CodeF6 - case C.kVK_F7: - return key.CodeF7 - case C.kVK_F8: - return key.CodeF8 - case C.kVK_F9: - return key.CodeF9 - case C.kVK_F10: - return key.CodeF10 - case C.kVK_F11: - return key.CodeF11 - case C.kVK_F12: - return key.CodeF12 - // 70: PrintScreen - // 71: Scroll Lock - // 72: Pause - // 73: Insert - case C.kVK_Home: - return key.CodeHome - case C.kVK_PageUp: - return key.CodePageUp - case C.kVK_ForwardDelete: - return key.CodeDeleteForward - case C.kVK_End: - return key.CodeEnd - case C.kVK_PageDown: - return key.CodePageDown - case C.kVK_RightArrow: - return key.CodeRightArrow - case C.kVK_LeftArrow: - return key.CodeLeftArrow - case C.kVK_DownArrow: - return key.CodeDownArrow - case C.kVK_UpArrow: - return key.CodeUpArrow - case C.kVK_ANSI_KeypadClear: - return key.CodeKeypadNumLock - case C.kVK_ANSI_KeypadDivide: - return key.CodeKeypadSlash - case C.kVK_ANSI_KeypadMultiply: - return key.CodeKeypadAsterisk - case C.kVK_ANSI_KeypadMinus: - return key.CodeKeypadHyphenMinus - case C.kVK_ANSI_KeypadPlus: - return key.CodeKeypadPlusSign - case C.kVK_ANSI_KeypadEnter: - return key.CodeKeypadEnter - case C.kVK_ANSI_Keypad1: - return key.CodeKeypad1 - case C.kVK_ANSI_Keypad2: - return key.CodeKeypad2 - case C.kVK_ANSI_Keypad3: - return key.CodeKeypad3 - case C.kVK_ANSI_Keypad4: - return key.CodeKeypad4 - case C.kVK_ANSI_Keypad5: - return key.CodeKeypad5 - case C.kVK_ANSI_Keypad6: - return key.CodeKeypad6 - case C.kVK_ANSI_Keypad7: - return key.CodeKeypad7 - case C.kVK_ANSI_Keypad8: - return key.CodeKeypad8 - case C.kVK_ANSI_Keypad9: - return key.CodeKeypad9 - case C.kVK_ANSI_Keypad0: - return key.CodeKeypad0 - case C.kVK_ANSI_KeypadDecimal: - return key.CodeKeypadFullStop - case C.kVK_ANSI_KeypadEquals: - return key.CodeKeypadEqualSign - case C.kVK_F13: - return key.CodeF13 - case C.kVK_F14: - return key.CodeF14 - case C.kVK_F15: - return key.CodeF15 - case C.kVK_F16: - return key.CodeF16 - case C.kVK_F17: - return key.CodeF17 - case C.kVK_F18: - return key.CodeF18 - case C.kVK_F19: - return key.CodeF19 - case C.kVK_F20: - return key.CodeF20 - // 116: Keyboard Execute - case C.kVK_Help: - return key.CodeHelp - // 118: Keyboard Menu - // 119: Keyboard Select - // 120: Keyboard Stop - // 121: Keyboard Again - // 122: Keyboard Undo - // 123: Keyboard Cut - // 124: Keyboard Copy - // 125: Keyboard Paste - // 126: Keyboard Find - case C.kVK_Mute: - return key.CodeMute - case C.kVK_VolumeUp: - return key.CodeVolumeUp - case C.kVK_VolumeDown: - return key.CodeVolumeDown - // 130: Keyboard Locking Caps Lock - // 131: Keyboard Locking Num Lock - // 132: Keyboard Locking Scroll Lock - // 133: Keyboard Comma - // 134: Keyboard Equal Sign - // ...: Bunch of stuff - case C.kVK_Control: - return key.CodeLeftControl - case C.kVK_Shift: - return key.CodeLeftShift - case C.kVK_Option: - return key.CodeLeftAlt - case C.kVK_Command: - return key.CodeLeftGUI - case C.kVK_RightControl: - return key.CodeRightControl - case C.kVK_RightShift: - return key.CodeRightShift - case C.kVK_RightOption: - return key.CodeRightAlt - // TODO key.CodeRightGUI - default: - return key.CodeUnknown - } -} - -func surfaceCreate() error { - return errors.New("gldriver: surface creation not implemented on darwin") -} diff --git a/shiny/driver/gldriver/cocoa.m b/shiny/driver/gldriver/cocoa.m deleted file mode 100644 index 8f9f7a3b..00000000 --- a/shiny/driver/gldriver/cocoa.m +++ /dev/null @@ -1,325 +0,0 @@ -// Copyright 2014 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -// +build darwin -// +build 386 amd64 -// +build !ios - -#include "_cgo_export.h" -#include -#include - -#import -#import -#import - -// The variables did not exist on older OS X releases, -// we use the old variables deprecated on macOS to define them. -#if __MAC_OS_X_VERSION_MAX_ALLOWED < 101200 -enum -{ - NSEventTypeScrollWheel = NSScrollWheel, - NSEventTypeKeyDown = NSKeyDown -}; -enum -{ - NSWindowStyleMaskTitled = NSTitledWindowMask, - NSWindowStyleMaskResizable = NSResizableWindowMask, - NSWindowStyleMaskMiniaturizable = NSMiniaturizableWindowMask, - NSWindowStyleMaskClosable = NSClosableWindowMask -}; -#endif - -void makeCurrentContext(uintptr_t context) { - NSOpenGLContext* ctx = (NSOpenGLContext*)context; - [ctx makeCurrentContext]; - [ctx update]; -} - -void flushContext(uintptr_t context) { - NSOpenGLContext* ctx = (NSOpenGLContext*)context; - [ctx flushBuffer]; -} - -uint64 threadID() { - uint64 id; - if (pthread_threadid_np(pthread_self(), &id)) { - abort(); - } - return id; -} - -@interface ScreenGLView : NSOpenGLView -{ -} -@end - -@implementation ScreenGLView -- (void)prepareOpenGL { - [self setWantsBestResolutionOpenGLSurface:YES]; - GLint swapInt = 1; - NSOpenGLContext *ctx = [self openGLContext]; - [ctx setValues:&swapInt forParameter:NSOpenGLCPSwapInterval]; - - // Using attribute arrays in OpenGL 3.3 requires the use of a VBA. - // But VBAs don't exist in ES 2. So we bind a default one. - GLuint vba; - glGenVertexArrays(1, &vba); - glBindVertexArray(vba); - - preparedOpenGL((GoUintptr)self, (GoUintptr)ctx, (GoUintptr)vba); -} - -- (void)callSetGeom { - // Calculate screen PPI. - // - // Note that the backingScaleFactor converts from logical - // pixels to actual pixels, but both of these units vary - // independently from real world size. E.g. - // - // 13" Retina Macbook Pro, 2560x1600, 227ppi, backingScaleFactor=2, scale=3.15 - // 15" Retina Macbook Pro, 2880x1800, 220ppi, backingScaleFactor=2, scale=3.06 - // 27" iMac, 2560x1440, 109ppi, backingScaleFactor=1, scale=1.51 - // 27" Retina iMac, 5120x2880, 218ppi, backingScaleFactor=2, scale=3.03 - NSScreen *screen = self.window.screen; - double screenPixW = [screen frame].size.width * [screen backingScaleFactor]; - - CGDirectDisplayID display = (CGDirectDisplayID)[[[screen deviceDescription] valueForKey:@"NSScreenNumber"] intValue]; - CGSize screenSizeMM = CGDisplayScreenSize(display); // in millimeters - float ppi = 25.4 * screenPixW / screenSizeMM.width; - float pixelsPerPt = ppi/72.0; - - // The width and height reported to the geom package are the - // bounds of the OpenGL view. Several steps are necessary. - // First, [self bounds] gives us the number of logical pixels - // in the view. Multiplying this by the backingScaleFactor - // gives us the number of actual pixels. - NSRect r = [self bounds]; - int w = r.size.width * [screen backingScaleFactor]; - int h = r.size.height * [screen backingScaleFactor]; - - setGeom((GoUintptr)self, pixelsPerPt, w, h); -} - -- (void)reshape { - [super reshape]; - [self callSetGeom]; -} - -- (void)drawRect:(NSRect)theRect { - // Called during resize. Do an extra draw if we are visible. - // This gets rid of flicker when resizing. - drawgl((GoUintptr)self); -} - -- (void)mouseEventNS:(NSEvent *)theEvent { - NSPoint p = [theEvent locationInWindow]; - double h = self.frame.size.height; - - // Both h and p are measured in Cocoa pixels, which are a fraction of - // physical pixels, so we multiply by backingScaleFactor. - double scale = [self.window.screen backingScaleFactor]; - - double x = p.x * scale; - double y = (h - p.y) * scale - 1; // flip origin from bottom-left to top-left. - - double dx, dy; - if (theEvent.type == NSEventTypeScrollWheel) { - dx = theEvent.scrollingDeltaX; - dy = theEvent.scrollingDeltaY; - } - - mouseEvent((GoUintptr)self, x, y, dx, dy, theEvent.type, theEvent.buttonNumber, theEvent.modifierFlags); -} - -- (void)mouseMoved:(NSEvent *)theEvent { [self mouseEventNS:theEvent]; } -- (void)mouseDown:(NSEvent *)theEvent { [self mouseEventNS:theEvent]; } -- (void)mouseUp:(NSEvent *)theEvent { [self mouseEventNS:theEvent]; } -- (void)mouseDragged:(NSEvent *)theEvent { [self mouseEventNS:theEvent]; } -- (void)rightMouseDown:(NSEvent *)theEvent { [self mouseEventNS:theEvent]; } -- (void)rightMouseUp:(NSEvent *)theEvent { [self mouseEventNS:theEvent]; } -- (void)rightMouseDragged:(NSEvent *)theEvent { [self mouseEventNS:theEvent]; } -- (void)otherMouseDown:(NSEvent *)theEvent { [self mouseEventNS:theEvent]; } -- (void)otherMouseUp:(NSEvent *)theEvent { [self mouseEventNS:theEvent]; } -- (void)otherMouseDragged:(NSEvent *)theEvent { [self mouseEventNS:theEvent]; } -- (void)scrollWheel:(NSEvent *)theEvent { [self mouseEventNS:theEvent]; } - -// raw modifier key presses -- (void)flagsChanged:(NSEvent *)theEvent { - flagEvent((GoUintptr)self, theEvent.modifierFlags); -} - -// overrides special handling of escape and tab -- (BOOL)performKeyEquivalent:(NSEvent *)theEvent { - [self key:theEvent]; - return YES; -} - -- (void)keyDown:(NSEvent *)theEvent { [self key:theEvent]; } -- (void)keyUp:(NSEvent *)theEvent { [self key:theEvent]; } - -- (void)key:(NSEvent *)theEvent { - NSRange range = [theEvent.characters rangeOfComposedCharacterSequenceAtIndex:0]; - - uint8_t buf[4] = {0, 0, 0, 0}; - if (![theEvent.characters getBytes:buf - maxLength:4 - usedLength:nil - encoding:NSUTF32LittleEndianStringEncoding - options:NSStringEncodingConversionAllowLossy - range:range - remainingRange:nil]) { - NSLog(@"failed to read key event %@", theEvent); - return; - } - - uint32_t rune = (uint32_t)buf[0]<<0 | (uint32_t)buf[1]<<8 | (uint32_t)buf[2]<<16 | (uint32_t)buf[3]<<24; - - uint8_t direction; - if ([theEvent isARepeat]) { - direction = 0; - } else if (theEvent.type == NSEventTypeKeyDown) { - direction = 1; - } else { - direction = 2; - } - keyEvent((GoUintptr)self, (int32_t)rune, direction, theEvent.keyCode, theEvent.modifierFlags); -} - -- (void)windowDidChangeScreenProfile:(NSNotification *)notification { - [self callSetGeom]; -} - -// TODO: catch windowDidMiniaturize? - -- (void)windowDidExpose:(NSNotification *)notification { - lifecycleVisible((GoUintptr)self, true); -} - -- (void)windowDidBecomeKey:(NSNotification *)notification { - lifecycleFocused((GoUintptr)self, true); -} - -- (void)windowDidResignKey:(NSNotification *)notification { - lifecycleFocused((GoUintptr)self, false); - if ([NSApp isHidden]) { - lifecycleVisible((GoUintptr)self, false); - } -} - -- (void)windowWillClose:(NSNotification *)notification { - windowClosing((GoUintptr)self); - - if (self.window.nextResponder != NULL) { - [self.window.nextResponder release]; - self.window.nextResponder = NULL; - } -} -@end - -@interface AppDelegate : NSObject -{ -} -@end - -@implementation AppDelegate -- (void)applicationDidFinishLaunching:(NSNotification *)aNotification { - driverStarted(); - [[NSRunningApplication currentApplication] activateWithOptions:(NSApplicationActivateAllWindows | NSApplicationActivateIgnoringOtherApps)]; -} - -- (void)applicationWillTerminate:(NSNotification *)aNotification { - lifecycleDeadAll(); -} - -- (void)applicationWillHide:(NSNotification *)aNotification { - lifecycleHideAll(); -} -@end - -uintptr_t doNewWindow(int width, int height, char* title) { - NSScreen *screen = [NSScreen mainScreen]; - double w = (double)width / [screen backingScaleFactor]; - double h = (double)height / [screen backingScaleFactor]; - __block ScreenGLView* view = NULL; - - dispatch_sync(dispatch_get_main_queue(), ^{ - id menuBar = [NSMenu new]; - id menuItem = [NSMenuItem new]; - [menuBar addItem:menuItem]; - [NSApp setMainMenu:menuBar]; - - id menu = [NSMenu new]; - NSString* name = [[NSString alloc] initWithUTF8String:title]; - - id hideMenuItem = [[NSMenuItem alloc] initWithTitle:@"Hide" - action:@selector(hide:) keyEquivalent:@"h"]; - [menu addItem:hideMenuItem]; - - id quitMenuItem = [[NSMenuItem alloc] initWithTitle:@"Quit" - action:@selector(terminate:) keyEquivalent:@"q"]; - [menu addItem:quitMenuItem]; - [menuItem setSubmenu:menu]; - - NSRect rect = NSMakeRect(0, 0, w, h); - - NSWindow* window = [[NSWindow alloc] initWithContentRect:rect - styleMask:NSWindowStyleMaskTitled - backing:NSBackingStoreBuffered - defer:NO]; - window.styleMask |= NSWindowStyleMaskResizable; - window.styleMask |= NSWindowStyleMaskMiniaturizable; - window.styleMask |= NSWindowStyleMaskClosable; - window.title = name; - window.displaysWhenScreenProfileChanges = YES; - [window cascadeTopLeftFromPoint:NSMakePoint(20,20)]; - [window setAcceptsMouseMovedEvents:YES]; - - NSOpenGLPixelFormatAttribute attr[] = { - NSOpenGLPFAOpenGLProfile, NSOpenGLProfileVersion3_2Core, - NSOpenGLPFAColorSize, 24, - NSOpenGLPFAAlphaSize, 8, - NSOpenGLPFADepthSize, 16, - NSOpenGLPFADoubleBuffer, - NSOpenGLPFAAllowOfflineRenderers, - 0 - }; - id pixFormat = [[NSOpenGLPixelFormat alloc] initWithAttributes:attr]; - view = [[ScreenGLView alloc] initWithFrame:rect pixelFormat:pixFormat]; - [window setContentView:view]; - [window setDelegate:view]; - [window makeFirstResponder:view]; - }); - - return (uintptr_t)view; -} - -void doShowWindow(uintptr_t viewID) { - ScreenGLView* view = (ScreenGLView*)viewID; - dispatch_async(dispatch_get_main_queue(), ^{ - [view.window makeKeyAndOrderFront:view.window]; - }); -} - -void doCloseWindow(uintptr_t viewID) { - ScreenGLView* view = (ScreenGLView*)viewID; - dispatch_sync(dispatch_get_main_queue(), ^{ - [view.window performClose:view]; - }); -} - -void startDriver() { - [NSAutoreleasePool new]; - [NSApplication sharedApplication]; - [NSApp setActivationPolicy:NSApplicationActivationPolicyRegular]; - AppDelegate* delegate = [[AppDelegate alloc] init]; - [NSApp setDelegate:delegate]; - [NSApp run]; -} - -void stopDriver() { - dispatch_async(dispatch_get_main_queue(), ^{ - [NSApp terminate:nil]; - }); -} diff --git a/shiny/driver/gldriver/context.go b/shiny/driver/gldriver/context.go deleted file mode 100644 index 197be350..00000000 --- a/shiny/driver/gldriver/context.go +++ /dev/null @@ -1,37 +0,0 @@ -// Copyright 2016 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -// +build !android - -package gldriver - -import ( - "runtime" - - "golang.org/x/mobile/gl" -) - -// NewContext creates an OpenGL ES context with a dedicated processing thread. -func NewContext() (gl.Context, error) { - glctx, worker := gl.NewContext() - - errCh := make(chan error) - workAvailable := worker.WorkAvailable() - go func() { - runtime.LockOSThread() - err := surfaceCreate() - errCh <- err - if err != nil { - return - } - - for range workAvailable { - worker.DoWork() - } - }() - if err := <-errCh; err != nil { - return nil, err - } - return glctx, nil -} diff --git a/shiny/driver/gldriver/egl.go b/shiny/driver/gldriver/egl.go deleted file mode 100644 index 6f5d3d7b..00000000 --- a/shiny/driver/gldriver/egl.go +++ /dev/null @@ -1,106 +0,0 @@ -// Copyright 2015 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package gldriver - -// These constants match the values found in the EGL 1.4 headers, -// egl.h, eglext.h, and eglplatform.h. -const ( - _EGL_DONT_CARE = -1 - - _EGL_NO_SURFACE = 0 - _EGL_NO_CONTEXT = 0 - _EGL_NO_DISPLAY = 0 - - _EGL_OPENGL_ES2_BIT = 0x04 // EGL_RENDERABLE_TYPE mask - _EGL_WINDOW_BIT = 0x04 // EGL_SURFACE_TYPE mask - - _EGL_OPENGL_ES_API = 0x30A0 - _EGL_RENDERABLE_TYPE = 0x3040 - _EGL_SURFACE_TYPE = 0x3033 - _EGL_BUFFER_SIZE = 0x3020 - _EGL_ALPHA_SIZE = 0x3021 - _EGL_BLUE_SIZE = 0x3022 - _EGL_GREEN_SIZE = 0x3023 - _EGL_RED_SIZE = 0x3024 - _EGL_DEPTH_SIZE = 0x3025 - _EGL_STENCIL_SIZE = 0x3026 - _EGL_SAMPLE_BUFFERS = 0x3032 - _EGL_CONFIG_CAVEAT = 0x3027 - _EGL_NONE = 0x3038 - - _EGL_CONTEXT_CLIENT_VERSION = 0x3098 -) - -// ANGLE specific options found in eglext.h -const ( - _EGL_PLATFORM_ANGLE_ANGLE = 0x3202 - _EGL_PLATFORM_ANGLE_TYPE_ANGLE = 0x3203 - _EGL_PLATFORM_ANGLE_MAX_VERSION_MAJOR_ANGLE = 0x3204 - _EGL_PLATFORM_ANGLE_MAX_VERSION_MINOR_ANGLE = 0x3205 - _EGL_PLATFORM_ANGLE_TYPE_DEFAULT_ANGLE = 0x3206 - - _EGL_PLATFORM_ANGLE_TYPE_D3D9_ANGLE = 0x3207 - _EGL_PLATFORM_ANGLE_TYPE_D3D11_ANGLE = 0x3208 - _EGL_PLATFORM_ANGLE_DEVICE_TYPE_ANGLE = 0x3209 - _EGL_PLATFORM_ANGLE_DEVICE_TYPE_HARDWARE_ANGLE = 0x320A - _EGL_PLATFORM_ANGLE_DEVICE_TYPE_WARP_ANGLE = 0x320B - - _EGL_PLATFORM_ANGLE_TYPE_OPENGL_ANGLE = 0x320D - _EGL_PLATFORM_ANGLE_TYPE_OPENGLES_ANGLE = 0x320E -) - -const ( - _EGL_SUCCESS = 0x3000 - _EGL_NOT_INITIALIZED = 0x3001 - _EGL_BAD_ACCESS = 0x3002 - _EGL_BAD_ALLOC = 0x3003 - _EGL_BAD_ATTRIBUTE = 0x3004 - _EGL_BAD_CONFIG = 0x3005 - _EGL_BAD_CONTEXT = 0x3006 - _EGL_BAD_CURRENT_SURFACE = 0x3007 - _EGL_BAD_DISPLAY = 0x3008 - _EGL_BAD_MATCH = 0x3009 - _EGL_BAD_NATIVE_PIXMAP = 0x300A - _EGL_BAD_NATIVE_WINDOW = 0x300B - _EGL_BAD_PARAMETER = 0x300C - _EGL_BAD_SURFACE = 0x300D - _EGL_CONTEXT_LOST = 0x300E -) - -func eglErrString(errno uintptr) string { - switch errno { - case _EGL_SUCCESS: - return "EGL_SUCCESS" - case _EGL_NOT_INITIALIZED: - return "EGL_NOT_INITIALIZED" - case _EGL_BAD_ACCESS: - return "EGL_BAD_ACCESS" - case _EGL_BAD_ALLOC: - return "EGL_BAD_ALLOC" - case _EGL_BAD_ATTRIBUTE: - return "EGL_BAD_ATTRIBUTE" - case _EGL_BAD_CONFIG: - return "EGL_BAD_CONFIG" - case _EGL_BAD_CONTEXT: - return "EGL_BAD_CONTEXT" - case _EGL_BAD_CURRENT_SURFACE: - return "EGL_BAD_CURRENT_SURFACE" - case _EGL_BAD_DISPLAY: - return "EGL_BAD_DISPLAY" - case _EGL_BAD_MATCH: - return "EGL_BAD_MATCH" - case _EGL_BAD_NATIVE_PIXMAP: - return "EGL_BAD_NATIVE_PIXMAP" - case _EGL_BAD_NATIVE_WINDOW: - return "EGL_BAD_NATIVE_WINDOW" - case _EGL_BAD_PARAMETER: - return "EGL_BAD_PARAMETER" - case _EGL_BAD_SURFACE: - return "EGL_BAD_SURFACE" - case _EGL_CONTEXT_LOST: - return "EGL_CONTEXT_LOST" - } - return "EGL: unknown error" -} diff --git a/shiny/driver/gldriver/gldriver.go b/shiny/driver/gldriver/gldriver.go deleted file mode 100644 index f9b0b27f..00000000 --- a/shiny/driver/gldriver/gldriver.go +++ /dev/null @@ -1,121 +0,0 @@ -// Copyright 2015 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -// Package gldriver provides an OpenGL driver for accessing a screen. -package gldriver - -import ( - "encoding/binary" - "fmt" - "math" - - "github.com/oakmound/oak/v3/shiny/driver/internal/errscreen" - "github.com/oakmound/oak/v3/shiny/screen" - "golang.org/x/image/math/f64" - "golang.org/x/mobile/gl" -) - -// Main is called by the program's main function to run the graphical -// application. -// -// It calls f on the Screen, possibly in a separate goroutine, as some OS- -// specific libraries require being on 'the main thread'. It returns when f -// returns. -func Main(f func(screen.Screen)) { - if err := main(f); err != nil { - f(errscreen.Stub(err)) - } -} - -// writeAff3 must only be called while holding windowImpl.glctxMu. -func writeAff3(glctx gl.Context, u gl.Uniform, a f64.Aff3) { - var m [9]float32 - m[0*3+0] = float32(a[0*3+0]) - m[0*3+1] = float32(a[1*3+0]) - m[0*3+2] = 0 - m[1*3+0] = float32(a[0*3+1]) - m[1*3+1] = float32(a[1*3+1]) - m[1*3+2] = 0 - m[2*3+0] = float32(a[0*3+2]) - m[2*3+1] = float32(a[1*3+2]) - m[2*3+2] = 1 - glctx.UniformMatrix3fv(u, m[:]) -} - -// f32Bytes returns the byte representation of float32 values in the given byte -// order. byteOrder must be either binary.BigEndian or binary.LittleEndian. -func f32Bytes(byteOrder binary.ByteOrder, values ...float32) []byte { - le := false - switch byteOrder { - case binary.BigEndian: - case binary.LittleEndian: - le = true - default: - panic(fmt.Sprintf("invalid byte order %v", byteOrder)) - } - - b := make([]byte, 4*len(values)) - for i, v := range values { - u := math.Float32bits(v) - if le { - b[4*i+0] = byte(u >> 0) - b[4*i+1] = byte(u >> 8) - b[4*i+2] = byte(u >> 16) - b[4*i+3] = byte(u >> 24) - } else { - b[4*i+0] = byte(u >> 24) - b[4*i+1] = byte(u >> 16) - b[4*i+2] = byte(u >> 8) - b[4*i+3] = byte(u >> 0) - } - } - return b -} - -// compileProgram must only be called while holding windowImpl.glctxMu. -func compileProgram(glctx gl.Context, vSrc, fSrc string) (gl.Program, error) { - program := glctx.CreateProgram() - if program.Value == 0 { - return gl.Program{}, fmt.Errorf("gldriver: no programs available") - } - - vertexShader, err := compileShader(glctx, gl.VERTEX_SHADER, vSrc) - if err != nil { - return gl.Program{}, err - } - fragmentShader, err := compileShader(glctx, gl.FRAGMENT_SHADER, fSrc) - if err != nil { - glctx.DeleteShader(vertexShader) - return gl.Program{}, err - } - - glctx.AttachShader(program, vertexShader) - glctx.AttachShader(program, fragmentShader) - glctx.LinkProgram(program) - - // Flag shaders for deletion when program is unlinked. - glctx.DeleteShader(vertexShader) - glctx.DeleteShader(fragmentShader) - - if glctx.GetProgrami(program, gl.LINK_STATUS) == 0 { - defer glctx.DeleteProgram(program) - return gl.Program{}, fmt.Errorf("gldriver: program compile: %s", glctx.GetProgramInfoLog(program)) - } - return program, nil -} - -// compileShader must only be called while holding windowImpl.glctxMu. -func compileShader(glctx gl.Context, shaderType gl.Enum, src string) (gl.Shader, error) { - shader := glctx.CreateShader(shaderType) - if shader.Value == 0 { - return gl.Shader{}, fmt.Errorf("gldriver: could not create shader (type %v)", shaderType) - } - glctx.ShaderSource(shader, src) - glctx.CompileShader(shader) - if glctx.GetShaderi(shader, gl.COMPILE_STATUS) == 0 { - defer glctx.DeleteShader(shader) - return gl.Shader{}, fmt.Errorf("gldriver: shader compile: %s", glctx.GetShaderInfoLog(shader)) - } - return shader, nil -} diff --git a/shiny/driver/gldriver/other.go b/shiny/driver/gldriver/other.go deleted file mode 100644 index 538feb36..00000000 --- a/shiny/driver/gldriver/other.go +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright 2015 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -// +build !darwin !386,!amd64 ios -// +build !linux android -// +build !windows -// +build !openbsd - -package gldriver - -import ( - "fmt" - "runtime" - - "github.com/oakmound/oak/v3/shiny/screen" -) - -func newWindow(opts screen.WindowGenerator) (uintptr, error) { return 0, nil } - -func moveWindow(w *windowImpl, opts screen.WindowGenerator) error { return nil } - -const useLifecycler = true -const handleSizeEventsAtChannelReceive = true - -func initWindow(id *windowImpl) {} -func showWindow(id *windowImpl) {} -func closeWindow(id uintptr) {} -func drawLoop(w *windowImpl) {} - -func main(f func(screen.Screen)) error { - return fmt.Errorf("gldriver: unsupported GOOS/GOARCH %s/%s", runtime.GOOS, runtime.GOARCH) -} diff --git a/shiny/driver/gldriver/screen.go b/shiny/driver/gldriver/screen.go deleted file mode 100644 index 4e01ac7d..00000000 --- a/shiny/driver/gldriver/screen.go +++ /dev/null @@ -1,138 +0,0 @@ -// Copyright 2015 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package gldriver - -import ( - "fmt" - "image" - "sync" - - "github.com/oakmound/oak/v3/shiny/screen" - "golang.org/x/mobile/gl" -) - -var theScreen = &screenImpl{ - windows: make(map[uintptr]*windowImpl), -} - -type screenImpl struct { - texture struct { - program gl.Program - pos gl.Attrib - mvp gl.Uniform - uvp gl.Uniform - inUV gl.Attrib - sample gl.Uniform - quad gl.Buffer - } - fill struct { - program gl.Program - pos gl.Attrib - mvp gl.Uniform - color gl.Uniform - quad gl.Buffer - } - - mu sync.Mutex - windows map[uintptr]*windowImpl -} - -func (s *screenImpl) NewImage(size image.Point) (retBuf screen.Image, retErr error) { - m := image.NewRGBA(image.Rectangle{Max: size}) - return &bufferImpl{ - buf: m.Pix, - rgba: *m, - size: size, - }, nil -} - -func (s *screenImpl) NewTexture(size image.Point) (screen.Texture, error) { - // TODO: can we compile these programs eagerly instead of lazily? - - // Find a GL context for this texture. - // TODO: this might be correct. Some GL objects can be shared - // across contexts. But this needs a review of the spec to make - // sure it's correct, and some testing would be nice. - var w *windowImpl - - s.mu.Lock() - for _, window := range s.windows { - w = window - break - } - s.mu.Unlock() - - if w == nil { - return nil, fmt.Errorf("gldriver: no window available") - } - - w.glctxMu.Lock() - defer w.glctxMu.Unlock() - glctx := w.glctx - if glctx == nil { - return nil, fmt.Errorf("gldriver: no GL context available") - } - - if !glctx.IsProgram(s.texture.program) { - p, err := compileProgram(glctx, textureVertexSrc, textureFragmentSrc) - if err != nil { - return nil, err - } - s.texture.program = p - s.texture.pos = glctx.GetAttribLocation(p, "pos") - s.texture.mvp = glctx.GetUniformLocation(p, "mvp") - s.texture.uvp = glctx.GetUniformLocation(p, "uvp") - s.texture.inUV = glctx.GetAttribLocation(p, "inUV") - s.texture.sample = glctx.GetUniformLocation(p, "sample") - s.texture.quad = glctx.CreateBuffer() - - glctx.BindBuffer(gl.ARRAY_BUFFER, s.texture.quad) - glctx.BufferData(gl.ARRAY_BUFFER, quadCoords, gl.STATIC_DRAW) - } - - t := &textureImpl{ - w: w, - id: glctx.CreateTexture(), - size: size, - } - - glctx.BindTexture(gl.TEXTURE_2D, t.id) - glctx.TexImage2D(gl.TEXTURE_2D, 0, gl.RGBA, size.X, size.Y, gl.RGBA, gl.UNSIGNED_BYTE, nil) - glctx.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR) - glctx.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR) - glctx.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE) - glctx.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE) - - return t, nil -} - -func (s *screenImpl) NewWindow(opts screen.WindowGenerator) (screen.Window, error) { - id, err := newWindow(opts) - if err != nil { - return nil, err - } - w := &windowImpl{ - s: s, - id: id, - publish: make(chan struct{}), - publishDone: make(chan screen.PublishResult), - drawDone: make(chan struct{}), - } - initWindow(w) - - s.mu.Lock() - s.windows[id] = w - s.mu.Unlock() - - if useLifecycler { - w.lifecycler.SendEvent(w, nil) - } - - showWindow(w) - - moveWindow(w, opts) - - return w, nil -} diff --git a/shiny/driver/gldriver/texture.go b/shiny/driver/gldriver/texture.go deleted file mode 100644 index 21b734ea..00000000 --- a/shiny/driver/gldriver/texture.go +++ /dev/null @@ -1,160 +0,0 @@ -// Copyright 2015 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package gldriver - -import ( - "encoding/binary" - "image" - "image/color" - "image/draw" - - "github.com/oakmound/oak/v3/shiny/screen" - "golang.org/x/mobile/gl" -) - -type textureImpl struct { - w *windowImpl - id gl.Texture - fb gl.Framebuffer - size image.Point -} - -func (t *textureImpl) Size() image.Point { return t.size } -func (t *textureImpl) Bounds() image.Rectangle { return image.Rectangle{Max: t.size} } - -func (t *textureImpl) Release() { - t.w.glctxMu.Lock() - defer t.w.glctxMu.Unlock() - - if t.fb.Value != 0 { - t.w.glctx.DeleteFramebuffer(t.fb) - t.fb = gl.Framebuffer{} - } - t.w.glctx.DeleteTexture(t.id) - t.id = gl.Texture{} -} - -func (t *textureImpl) Upload(dp image.Point, src screen.Image, sr image.Rectangle) { - buf := src.(*bufferImpl) - buf.preUpload() - - // src2dst is added to convert from the src coordinate space to the dst - // coordinate space. It is subtracted to convert the other way. - src2dst := dp.Sub(sr.Min) - - // Clip to the source. - sr = sr.Intersect(buf.Bounds()) - - // Clip to the destination. - dr := sr.Add(src2dst) - dr = dr.Intersect(t.Bounds()) - if dr.Empty() { - return - } - - // Bring dr.Min in dst-space back to src-space to get the pixel buffer offset. - pix := buf.rgba.Pix[buf.rgba.PixOffset(dr.Min.X-src2dst.X, dr.Min.Y-src2dst.Y):] - - t.w.glctxMu.Lock() - defer t.w.glctxMu.Unlock() - - t.w.glctx.BindTexture(gl.TEXTURE_2D, t.id) - - width := dr.Dx() - if width*4 == buf.rgba.Stride { - t.w.glctx.TexSubImage2D(gl.TEXTURE_2D, 0, dr.Min.X, dr.Min.Y, width, dr.Dy(), gl.RGBA, gl.UNSIGNED_BYTE, pix) - return - } - // TODO: can we use GL_UNPACK_ROW_LENGTH with glPixelStorei for stride in - // ES 3.0, instead of uploading the pixels row-by-row? - for y, p := dr.Min.Y, 0; y < dr.Max.Y; y++ { - t.w.glctx.TexSubImage2D(gl.TEXTURE_2D, 0, dr.Min.X, y, width, 1, gl.RGBA, gl.UNSIGNED_BYTE, pix[p:]) - p += buf.rgba.Stride - } -} - -func (t *textureImpl) Fill(dr image.Rectangle, src color.Color, op draw.Op) { - minX := float64(dr.Min.X) - minY := float64(dr.Min.Y) - maxX := float64(dr.Max.X) - maxY := float64(dr.Max.Y) - mvp := calcMVP( - t.size.X, t.size.Y, - minX, minY, - maxX, minY, - minX, maxY, - ) - - glctx := t.w.glctx - - t.w.glctxMu.Lock() - defer t.w.glctxMu.Unlock() - - create := t.fb.Value == 0 - if create { - t.fb = glctx.CreateFramebuffer() - } - glctx.BindFramebuffer(gl.FRAMEBUFFER, t.fb) - if create { - glctx.FramebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, t.id, 0) - } - - glctx.Viewport(0, 0, t.size.X, t.size.Y) - doFill(t.w.s, t.w.glctx, mvp, src, op) - - // We can't restore the GL state (i.e. bind the back buffer, also known as - // gl.Framebuffer{Value: 0}) right away, since we don't necessarily know - // the right viewport size yet. It is valid to call textureImpl.Fill before - // we've gotten our first size.Event. We bind it lazily instead. - t.w.backBufferBound = false -} - -var quadCoords = f32Bytes(binary.LittleEndian, - 0, 0, // top left - 1, 0, // top right - 0, 1, // bottom left - 1, 1, // bottom right -) - -const textureVertexSrc = `#version 100 -uniform mat3 mvp; -uniform mat3 uvp; -attribute vec3 pos; -attribute vec2 inUV; -varying vec2 uv; -void main() { - vec3 p = pos; - p.z = 1.0; - gl_Position = vec4(mvp * p, 1); - uv = (uvp * vec3(inUV, 1)).xy; -} -` - -const textureFragmentSrc = `#version 100 -precision mediump float; -varying vec2 uv; -uniform sampler2D sample; -void main() { - gl_FragColor = texture2D(sample, uv); -} -` - -const fillVertexSrc = `#version 100 -uniform mat3 mvp; -attribute vec3 pos; -void main() { - vec3 p = pos; - p.z = 1.0; - gl_Position = vec4(mvp * p, 1); -} -` - -const fillFragmentSrc = `#version 100 -precision mediump float; -uniform vec4 color; -void main() { - gl_FragColor = color; -} -` diff --git a/shiny/driver/gldriver/win32.go b/shiny/driver/gldriver/win32.go deleted file mode 100644 index d26fe143..00000000 --- a/shiny/driver/gldriver/win32.go +++ /dev/null @@ -1,365 +0,0 @@ -// Copyright 2015 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -// +build windows - -package gldriver - -import ( - "errors" - "fmt" - "runtime" - "unsafe" - - "github.com/oakmound/oak/v3/shiny/driver/internal/win32" - "github.com/oakmound/oak/v3/shiny/screen" - "golang.org/x/mobile/event/key" - "golang.org/x/mobile/event/lifecycle" - "golang.org/x/mobile/event/mouse" - "golang.org/x/mobile/event/paint" - "golang.org/x/mobile/event/size" - "golang.org/x/mobile/gl" -) - -const useLifecycler = true -const handleSizeEventsAtChannelReceive = true - -var screenHWND win32.HWND - -func main(f func(screen.Screen)) error { - var err error - screenHWND, err = win32.NewScreen() - if err != nil { - return err - } - return win32.Main(screenHWND, func() { f(theScreen) }) -} - -var ( - eglGetPlatformDisplayEXT = gl.LibEGL.NewProc("eglGetPlatformDisplayEXT") - eglInitialize = gl.LibEGL.NewProc("eglInitialize") - eglChooseConfig = gl.LibEGL.NewProc("eglChooseConfig") - eglGetError = gl.LibEGL.NewProc("eglGetError") - eglBindAPI = gl.LibEGL.NewProc("eglBindAPI") - eglCreateWindowSurface = gl.LibEGL.NewProc("eglCreateWindowSurface") - eglCreateContext = gl.LibEGL.NewProc("eglCreateContext") - eglMakeCurrent = gl.LibEGL.NewProc("eglMakeCurrent") - eglSwapInterval = gl.LibEGL.NewProc("eglSwapInterval") - eglDestroySurface = gl.LibEGL.NewProc("eglDestroySurface") - eglSwapBuffers = gl.LibEGL.NewProc("eglSwapBuffers") -) - -type eglConfig uintptr // void* - -type eglInt int32 - -var rgb888 = [...]eglInt{ - _EGL_RENDERABLE_TYPE, _EGL_OPENGL_ES2_BIT, - _EGL_SURFACE_TYPE, _EGL_WINDOW_BIT, - _EGL_BLUE_SIZE, 8, - _EGL_GREEN_SIZE, 8, - _EGL_RED_SIZE, 8, - _EGL_DEPTH_SIZE, 16, - _EGL_STENCIL_SIZE, 8, - _EGL_NONE, -} - -type ctxWin32 struct { - ctx uintptr - display uintptr // EGLDisplay - surface uintptr // EGLSurface -} - -func newWindow(opts screen.WindowGenerator) (uintptr, error) { - w, err := win32.NewWindow(screenHWND, opts) - if err != nil { - return 0, err - } - - return uintptr(w), nil -} - -func moveWindow(w *windowImpl, opts screen.WindowGenerator) error { - return win32.ResizeClientRect(win32.HWND(w.id), opts) -} - -func initWindow(w *windowImpl) { - w.glctx, w.worker = gl.NewContext() -} - -func showWindow(w *windowImpl) { - // Show makes an initial call to sizeEvent (via win32.SizeEvent), where - // we setup the EGL surface and GL context. - win32.Show(win32.HWND(w.id)) -} - -func closeWindow(id uintptr) {} // TODO - -func drawLoop(w *windowImpl) { - runtime.LockOSThread() - - display := w.ctx.(ctxWin32).display - surface := w.ctx.(ctxWin32).surface - ctx := w.ctx.(ctxWin32).ctx - - if ret, _, _ := eglMakeCurrent.Call(display, surface, surface, ctx); ret == 0 { - panic(fmt.Sprintf("eglMakeCurrent failed: %v", eglErr())) - } - - // TODO(crawshaw): exit this goroutine on Release. - workAvailable := w.worker.WorkAvailable() - for { - select { - case <-workAvailable: - w.worker.DoWork() - case <-w.publish: - loop: - for { - select { - case <-workAvailable: - w.worker.DoWork() - default: - break loop - } - } - if ret, _, _ := eglSwapBuffers.Call(display, surface); ret == 0 { - panic(fmt.Sprintf("eglSwapBuffers failed: %v", eglErr())) - } - w.publishDone <- screen.PublishResult{} - } - } -} - -func init() { - win32.SizeEvent = sizeEvent - win32.PaintEvent = paintEvent - win32.MouseEvent = mouseEvent - win32.KeyEvent = keyEvent - win32.LifecycleEvent = lifecycleEvent -} - -func lifecycleEvent(hwnd win32.HWND, to lifecycle.Stage) { - theScreen.mu.Lock() - w := theScreen.windows[uintptr(hwnd)] - theScreen.mu.Unlock() - - if w.lifecycleStage == to { - return - } - w.Send(lifecycle.Event{ - From: w.lifecycleStage, - To: to, - DrawContext: w.glctx, - }) - w.lifecycleStage = to -} - -func mouseEvent(hwnd win32.HWND, e mouse.Event) { - theScreen.mu.Lock() - w := theScreen.windows[uintptr(hwnd)] - theScreen.mu.Unlock() - - w.Send(e) -} - -func keyEvent(hwnd win32.HWND, e key.Event) { - theScreen.mu.Lock() - w := theScreen.windows[uintptr(hwnd)] - theScreen.mu.Unlock() - - w.Send(e) -} - -func paintEvent(hwnd win32.HWND, e paint.Event) { - theScreen.mu.Lock() - w := theScreen.windows[uintptr(hwnd)] - theScreen.mu.Unlock() - - if w.ctx == nil { - // Sometimes a paint event comes in before initial - // window size is set. Ignore it. - return - } - - // TODO: the paint.Event should have External: true. - w.Send(paint.Event{}) -} - -func sizeEvent(hwnd win32.HWND, e size.Event) { - theScreen.mu.Lock() - w := theScreen.windows[uintptr(hwnd)] - theScreen.mu.Unlock() - - if w.ctx == nil { - // This is the initial size event on window creation. - // Create an EGL surface and spin up a GL context. - if err := createEGLSurface(hwnd, w); err != nil { - panic(err) - } - go drawLoop(w) - } - - if !handleSizeEventsAtChannelReceive { - w.szMu.Lock() - w.sz = e - w.szMu.Unlock() - } - - w.Send(e) - - if handleSizeEventsAtChannelReceive { - return - } - - // Screen is dirty, generate a paint event. - // - // The sizeEvent function is called on the goroutine responsible for - // calling the GL worker.DoWork. When compiling with -tags gldebug, - // these GL calls are blocking (so we can read the error message), so - // to make progress they need to happen on another goroutine. - go func() { - // TODO: this call to Viewport is not right, but is very hard to - // do correctly with our async events channel model. We want - // the call to Viewport to be made the instant before the - // paint.Event is received. - w.glctxMu.Lock() - w.glctx.Viewport(0, 0, e.WidthPx, e.HeightPx) - w.glctx.ClearColor(0, 0, 0, 1) - w.glctx.Clear(gl.COLOR_BUFFER_BIT) - w.glctxMu.Unlock() - - w.Send(paint.Event{}) - }() -} - -func eglErr() error { - if ret, _, _ := eglGetError.Call(); ret != _EGL_SUCCESS { - return errors.New(eglErrString(ret)) - } - return nil -} - -func createEGLSurface(hwnd win32.HWND, w *windowImpl) error { - var displayAttribPlatforms = [][]eglInt{ - // Default - { - _EGL_PLATFORM_ANGLE_TYPE_ANGLE, - _EGL_PLATFORM_ANGLE_TYPE_DEFAULT_ANGLE, - _EGL_PLATFORM_ANGLE_MAX_VERSION_MAJOR_ANGLE, _EGL_DONT_CARE, - _EGL_PLATFORM_ANGLE_MAX_VERSION_MINOR_ANGLE, _EGL_DONT_CARE, - _EGL_NONE, - }, - // Direct3D 11 - { - _EGL_PLATFORM_ANGLE_TYPE_ANGLE, - _EGL_PLATFORM_ANGLE_TYPE_D3D11_ANGLE, - _EGL_PLATFORM_ANGLE_MAX_VERSION_MAJOR_ANGLE, _EGL_DONT_CARE, - _EGL_PLATFORM_ANGLE_MAX_VERSION_MINOR_ANGLE, _EGL_DONT_CARE, - _EGL_NONE, - }, - // Direct3D 9 - { - _EGL_PLATFORM_ANGLE_TYPE_ANGLE, - _EGL_PLATFORM_ANGLE_TYPE_D3D9_ANGLE, - _EGL_PLATFORM_ANGLE_MAX_VERSION_MAJOR_ANGLE, _EGL_DONT_CARE, - _EGL_PLATFORM_ANGLE_MAX_VERSION_MINOR_ANGLE, _EGL_DONT_CARE, - _EGL_NONE, - }, - // Direct3D 11 with WARP - // https://msdn.microsoft.com/en-us/library/windows/desktop/gg615082.aspx - { - _EGL_PLATFORM_ANGLE_TYPE_ANGLE, - _EGL_PLATFORM_ANGLE_TYPE_D3D11_ANGLE, - _EGL_PLATFORM_ANGLE_DEVICE_TYPE_ANGLE, - _EGL_PLATFORM_ANGLE_DEVICE_TYPE_WARP_ANGLE, - _EGL_PLATFORM_ANGLE_MAX_VERSION_MAJOR_ANGLE, _EGL_DONT_CARE, - _EGL_PLATFORM_ANGLE_MAX_VERSION_MINOR_ANGLE, _EGL_DONT_CARE, - _EGL_NONE, - }, - } - - dc, err := win32.GetDC(hwnd) - if err != nil { - return fmt.Errorf("win32.GetDC failed: %v", err) - } - - var display uintptr = _EGL_NO_DISPLAY - for i, displayAttrib := range displayAttribPlatforms { - lastTry := i == len(displayAttribPlatforms)-1 - - display, _, _ = eglGetPlatformDisplayEXT.Call( - _EGL_PLATFORM_ANGLE_ANGLE, - uintptr(dc), - uintptr(unsafe.Pointer(&displayAttrib[0])), - ) - - if display == _EGL_NO_DISPLAY { - if !lastTry { - continue - } - return fmt.Errorf("eglGetPlatformDisplayEXT failed: %v", eglErr()) - } - - if ret, _, _ := eglInitialize.Call(display, 0, 0); ret == 0 { - if !lastTry { - continue - } - return fmt.Errorf("eglInitialize failed: %v", eglErr()) - } - } - - eglBindAPI.Call(_EGL_OPENGL_ES_API) - if err := eglErr(); err != nil { - return err - } - - var numConfigs eglInt - var config eglConfig - ret, _, _ := eglChooseConfig.Call( - display, - uintptr(unsafe.Pointer(&rgb888[0])), - uintptr(unsafe.Pointer(&config)), - 1, - uintptr(unsafe.Pointer(&numConfigs)), - ) - if ret == 0 { - return fmt.Errorf("eglChooseConfig failed: %v", eglErr()) - } - if numConfigs <= 0 { - return errors.New("eglChooseConfig found no valid config") - } - - surface, _, _ := eglCreateWindowSurface.Call(display, uintptr(config), uintptr(hwnd), 0, 0) - if surface == _EGL_NO_SURFACE { - return fmt.Errorf("eglCreateWindowSurface failed: %v", eglErr()) - } - - contextAttribs := [...]eglInt{ - _EGL_CONTEXT_CLIENT_VERSION, 2, - _EGL_NONE, - } - context, _, _ := eglCreateContext.Call( - display, - uintptr(config), - _EGL_NO_CONTEXT, - uintptr(unsafe.Pointer(&contextAttribs[0])), - ) - if context == _EGL_NO_CONTEXT { - return fmt.Errorf("eglCreateContext failed: %v", eglErr()) - } - - eglSwapInterval.Call(display, 1) - - w.ctx = ctxWin32{ - ctx: context, - display: display, - surface: surface, - } - - return nil -} - -func surfaceCreate() error { - return errors.New("gldriver: surface creation not implemented on windows") -} diff --git a/shiny/driver/gldriver/window.go b/shiny/driver/gldriver/window.go deleted file mode 100644 index ea614f88..00000000 --- a/shiny/driver/gldriver/window.go +++ /dev/null @@ -1,398 +0,0 @@ -// Copyright 2015 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package gldriver - -import ( - "image" - "image/color" - "image/draw" - "sync" - - "github.com/oakmound/oak/v3/shiny/driver/internal/drawer" - "github.com/oakmound/oak/v3/shiny/driver/internal/event" - "github.com/oakmound/oak/v3/shiny/driver/internal/lifecycler" - "github.com/oakmound/oak/v3/shiny/screen" - "golang.org/x/image/math/f64" - "golang.org/x/mobile/event/lifecycle" - "golang.org/x/mobile/event/size" - "golang.org/x/mobile/gl" -) - -type windowImpl struct { - s *screenImpl - - // id is an OS-specific data structure for the window. - // - Cocoa: ScreenGLView* - // - X11: Window - // - Windows: win32.HWND - id uintptr - - // ctx is a C data structure for the GL context. - // - Cocoa: uintptr holding a NSOpenGLContext*. - // - X11: uintptr holding an EGLSurface. - // - Windows: ctxWin32 - ctx interface{} - - lifecycler lifecycler.State - // TODO: Delete the field below (and the useLifecycler constant), and use - // the field above for cocoa and win32. - lifecycleStage lifecycle.Stage // current stage - - event.Deque - publish chan struct{} - publishDone chan screen.PublishResult - drawDone chan struct{} - - // glctxMu is a mutex that enforces the atomicity of methods like - // Texture.Upload or Window.Draw that are conceptually one operation - // but are implemented by multiple OpenGL calls. OpenGL is a stateful - // API, so interleaving OpenGL calls from separate higher-level - // operations causes inconsistencies. - glctxMu sync.Mutex - glctx gl.Context - worker gl.Worker - // backBufferBound is whether the default Framebuffer, with ID 0, also - // known as the back buffer or the window's Framebuffer, is bound and its - // viewport is known to equal the window size. It can become false when we - // bind to a texture's Framebuffer or when the window size changes. - backBufferBound bool - - // szMu protects only sz. If you need to hold both glctxMu and szMu, the - // lock ordering is to lock glctxMu first (and unlock it last). - szMu sync.Mutex - sz size.Event -} - -// NextEvent implements the screen.EventDeque interface. -func (w *windowImpl) NextEvent() interface{} { - e := w.Deque.NextEvent() - if handleSizeEventsAtChannelReceive { - if sz, ok := e.(size.Event); ok { - w.glctxMu.Lock() - w.backBufferBound = false - w.szMu.Lock() - w.sz = sz - w.szMu.Unlock() - w.glctxMu.Unlock() - } - } - return e -} - -func (w *windowImpl) Release() { - // There are two ways a window can be closed: the Operating System or - // Desktop Environment can initiate (e.g. in response to a user clicking a - // red button), or the Go app can programatically close the window (by - // calling Window.Release). - // - // When the OS closes a window: - // - Cocoa: Obj-C's windowWillClose calls Go's windowClosing. - // - X11: the X11 server sends a WM_DELETE_WINDOW message. - // - Windows: TODO: implement and document this. - // - // This should send a lifecycle event (To: StageDead) to the Go app's event - // loop, which should respond by calling Window.Release (this method). - // Window.Release is where system resources are actually cleaned up. - // - // When Window.Release is called, the closeWindow call below: - // - Cocoa: calls Obj-C's performClose, which emulates the red button - // being clicked. (TODO: document how this actually cleans up - // resources??) - // - X11: calls C's XDestroyWindow. - // - Windows: TODO: implement and document this. - // - // On Cocoa, if these two approaches race, experiments suggest that the - // race is won by performClose (which is called serially on the main - // thread). Even if that isn't true, the windowWillClose handler is - // idempotent. - - theScreen.mu.Lock() - delete(theScreen.windows, w.id) - theScreen.mu.Unlock() - - closeWindow(w.id) -} - -func (w *windowImpl) Upload(dp image.Point, src screen.Image, sr image.Rectangle) { - originalSRMin := sr.Min - sr = sr.Intersect(src.Bounds()) - if sr.Empty() { - return - } - dp = dp.Add(sr.Min.Sub(originalSRMin)) - // TODO: keep a texture around for this purpose? - t, err := w.s.NewTexture(sr.Size()) - if err != nil { - panic(err) - } - t.Upload(image.Point{}, src, sr) - w.Draw(f64.Aff3{ - 1, 0, float64(dp.X), - 0, 1, float64(dp.Y), - }, t, t.Bounds(), draw.Src) - t.Release() -} - -func useOp(glctx gl.Context, op draw.Op) { - if op == draw.Over { - glctx.Enable(gl.BLEND) - glctx.BlendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA) - } else { - glctx.Disable(gl.BLEND) - } -} - -func (w *windowImpl) bindBackBuffer() { - w.szMu.Lock() - sz := w.sz - w.szMu.Unlock() - - w.backBufferBound = true - w.glctx.BindFramebuffer(gl.FRAMEBUFFER, gl.Framebuffer{Value: 0}) - w.glctx.Viewport(0, 0, sz.WidthPx, sz.HeightPx) -} - -func (w *windowImpl) fill(mvp f64.Aff3, src color.Color, op draw.Op) { - w.glctxMu.Lock() - defer w.glctxMu.Unlock() - - if !w.backBufferBound { - w.bindBackBuffer() - } - - doFill(w.s, w.glctx, mvp, src, op) -} - -func doFill(s *screenImpl, glctx gl.Context, mvp f64.Aff3, src color.Color, op draw.Op) { - useOp(glctx, op) - if !glctx.IsProgram(s.fill.program) { - p, err := compileProgram(glctx, fillVertexSrc, fillFragmentSrc) - if err != nil { - // TODO: initialize this somewhere else we can better handle the error. - panic(err.Error()) - } - s.fill.program = p - s.fill.pos = glctx.GetAttribLocation(p, "pos") - s.fill.mvp = glctx.GetUniformLocation(p, "mvp") - s.fill.color = glctx.GetUniformLocation(p, "color") - s.fill.quad = glctx.CreateBuffer() - - glctx.BindBuffer(gl.ARRAY_BUFFER, s.fill.quad) - glctx.BufferData(gl.ARRAY_BUFFER, quadCoords, gl.STATIC_DRAW) - } - glctx.UseProgram(s.fill.program) - - writeAff3(glctx, s.fill.mvp, mvp) - - r, g, b, a := src.RGBA() - glctx.Uniform4f( - s.fill.color, - float32(r)/65535, - float32(g)/65535, - float32(b)/65535, - float32(a)/65535, - ) - - glctx.BindBuffer(gl.ARRAY_BUFFER, s.fill.quad) - glctx.EnableVertexAttribArray(s.fill.pos) - glctx.VertexAttribPointer(s.fill.pos, 2, gl.FLOAT, false, 0, 0) - - glctx.DrawArrays(gl.TRIANGLE_STRIP, 0, 4) - - glctx.DisableVertexAttribArray(s.fill.pos) -} - -func (w *windowImpl) Fill(dr image.Rectangle, src color.Color, op draw.Op) { - minX := float64(dr.Min.X) - minY := float64(dr.Min.Y) - maxX := float64(dr.Max.X) - maxY := float64(dr.Max.Y) - w.fill(w.mvp( - minX, minY, - maxX, minY, - minX, maxY, - ), src, op) -} - -func (w *windowImpl) DrawUniform(src2dst f64.Aff3, src color.Color, sr image.Rectangle, op draw.Op) { - minX := float64(sr.Min.X) - minY := float64(sr.Min.Y) - maxX := float64(sr.Max.X) - maxY := float64(sr.Max.Y) - w.fill(w.mvp( - src2dst[0]*minX+src2dst[1]*minY+src2dst[2], - src2dst[3]*minX+src2dst[4]*minY+src2dst[5], - src2dst[0]*maxX+src2dst[1]*minY+src2dst[2], - src2dst[3]*maxX+src2dst[4]*minY+src2dst[5], - src2dst[0]*minX+src2dst[1]*maxY+src2dst[2], - src2dst[3]*minX+src2dst[4]*maxY+src2dst[5], - ), src, op) -} - -func (w *windowImpl) Draw(src2dst f64.Aff3, src screen.Texture, sr image.Rectangle, op draw.Op) { - t := src.(*textureImpl) - sr = sr.Intersect(t.Bounds()) - if sr.Empty() { - return - } - - w.glctxMu.Lock() - defer w.glctxMu.Unlock() - - if !w.backBufferBound { - w.bindBackBuffer() - } - - useOp(w.glctx, op) - w.glctx.UseProgram(w.s.texture.program) - - // Start with src-space left, top, right and bottom. - srcL := float64(sr.Min.X) - srcT := float64(sr.Min.Y) - srcR := float64(sr.Max.X) - srcB := float64(sr.Max.Y) - // Transform to dst-space via the src2dst matrix, then to a MVP matrix. - writeAff3(w.glctx, w.s.texture.mvp, w.mvp( - src2dst[0]*srcL+src2dst[1]*srcT+src2dst[2], - src2dst[3]*srcL+src2dst[4]*srcT+src2dst[5], - src2dst[0]*srcR+src2dst[1]*srcT+src2dst[2], - src2dst[3]*srcR+src2dst[4]*srcT+src2dst[5], - src2dst[0]*srcL+src2dst[1]*srcB+src2dst[2], - src2dst[3]*srcL+src2dst[4]*srcB+src2dst[5], - )) - - // OpenGL's fragment shaders' UV coordinates run from (0,0)-(1,1), - // unlike vertex shaders' XY coordinates running from (-1,+1)-(+1,-1). - // - // We are drawing a rectangle PQRS, defined by two of its - // corners, onto the entire texture. The two quads may actually - // be equal, but in the general case, PQRS can be smaller. - // - // (0,0) +---------------+ (1,0) - // | P +-----+ Q | - // | | | | - // | S +-----+ R | - // (0,1) +---------------+ (1,1) - // - // The PQRS quad is always axis-aligned. First of all, convert - // from pixel space to texture space. - tw := float64(t.size.X) - th := float64(t.size.Y) - px := float64(sr.Min.X-0) / tw - py := float64(sr.Min.Y-0) / th - qx := float64(sr.Max.X-0) / tw - sy := float64(sr.Max.Y-0) / th - // Due to axis alignment, qy = py and sx = px. - // - // The simultaneous equations are: - // 0 + 0 + a02 = px - // 0 + 0 + a12 = py - // a00 + 0 + a02 = qx - // a10 + 0 + a12 = qy = py - // 0 + a01 + a02 = sx = px - // 0 + a11 + a12 = sy - writeAff3(w.glctx, w.s.texture.uvp, f64.Aff3{ - qx - px, 0, px, - 0, sy - py, py, - }) - - w.glctx.ActiveTexture(gl.TEXTURE0) - w.glctx.BindTexture(gl.TEXTURE_2D, t.id) - w.glctx.Uniform1i(w.s.texture.sample, 0) - - w.glctx.BindBuffer(gl.ARRAY_BUFFER, w.s.texture.quad) - w.glctx.EnableVertexAttribArray(w.s.texture.pos) - w.glctx.VertexAttribPointer(w.s.texture.pos, 2, gl.FLOAT, false, 0, 0) - - w.glctx.BindBuffer(gl.ARRAY_BUFFER, w.s.texture.quad) - w.glctx.EnableVertexAttribArray(w.s.texture.inUV) - w.glctx.VertexAttribPointer(w.s.texture.inUV, 2, gl.FLOAT, false, 0, 0) - - w.glctx.DrawArrays(gl.TRIANGLE_STRIP, 0, 4) - - w.glctx.DisableVertexAttribArray(w.s.texture.pos) - w.glctx.DisableVertexAttribArray(w.s.texture.inUV) -} - -func (w *windowImpl) Copy(dp image.Point, src screen.Texture, sr image.Rectangle, op draw.Op) { - drawer.Copy(w, dp, src, sr, op) -} - -func (w *windowImpl) Scale(dr image.Rectangle, src screen.Texture, sr image.Rectangle, op draw.Op) { - drawer.Scale(w, dr, src, sr, op) -} - -func (w *windowImpl) mvp(tlx, tly, trx, try, blx, bly float64) f64.Aff3 { - w.szMu.Lock() - sz := w.sz - w.szMu.Unlock() - - return calcMVP(sz.WidthPx, sz.HeightPx, tlx, tly, trx, try, blx, bly) -} - -// calcMVP returns the Model View Projection matrix that maps the quadCoords -// unit square, (0, 0) to (1, 1), to a quad QV, such that QV in vertex shader -// space corresponds to the quad QP in pixel space, where QP is defined by -// three of its four corners - the arguments to this function. The three -// corners are nominally the top-left, top-right and bottom-left, but there is -// no constraint that e.g. tlx < trx. -// -// In pixel space, the window ranges from (0, 0) to (widthPx, heightPx). The -// Y-axis points downwards. -// -// In vertex shader space, the window ranges from (-1, +1) to (+1, -1), which -// is a 2-unit by 2-unit square. The Y-axis points upwards. -func calcMVP(widthPx, heightPx int, tlx, tly, trx, try, blx, bly float64) f64.Aff3 { - // Convert from pixel coords to vertex shader coords. - invHalfWidth := +2 / float64(widthPx) - invHalfHeight := -2 / float64(heightPx) - tlx = tlx*invHalfWidth - 1 - tly = tly*invHalfHeight + 1 - trx = trx*invHalfWidth - 1 - try = try*invHalfHeight + 1 - blx = blx*invHalfWidth - 1 - bly = bly*invHalfHeight + 1 - - // The resultant affine matrix: - // - maps (0, 0) to (tlx, tly). - // - maps (1, 0) to (trx, try). - // - maps (0, 1) to (blx, bly). - return f64.Aff3{ - trx - tlx, blx - tlx, tlx, - try - tly, bly - tly, tly, - } -} - -func (w *windowImpl) Publish() screen.PublishResult { - // gl.Flush is a lightweight (on modern GL drivers) blocking call - // that ensures all GL functions pending in the gl package have - // been passed onto the GL driver before the app package attempts - // to swap the screen buffer. - // - // This enforces that the final receive (for this paint cycle) on - // gl.WorkAvailable happens before the send on publish. - w.glctxMu.Lock() - w.glctx.Flush() - w.glctxMu.Unlock() - - w.publish <- struct{}{} - res := <-w.publishDone - - select { - case w.drawDone <- struct{}{}: - default: - } - - return res -} - -func (w *windowImpl) MoveWindow(x, y, width, height int32) error { - return moveWindow(w, screen.WindowGenerator{ - X: x, - Y: y, - Width: int(width), - Height: int(height), - }) -} diff --git a/shiny/driver/gldriver/x11.c b/shiny/driver/gldriver/x11.c deleted file mode 100644 index 4edd6640..00000000 --- a/shiny/driver/gldriver/x11.c +++ /dev/null @@ -1,337 +0,0 @@ -// Copyright 2015 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -// +build linux,!android openbsd - -#include "_cgo_export.h" -#include -#include -#include -#include - -Atom net_wm_name; -Atom utf8_string; -Atom wm_delete_window; -Atom wm_protocols; -Atom wm_take_focus; - -EGLConfig e_config; -EGLContext e_ctx; -EGLDisplay e_dpy; -Colormap x_colormap; -Display *x_dpy; -XVisualInfo *x_visual_info; -Window x_root; - -// TODO: share code with eglErrString -char * -eglGetErrorStr() { - switch (eglGetError()) { - case EGL_SUCCESS: - return "EGL_SUCCESS"; - case EGL_NOT_INITIALIZED: - return "EGL_NOT_INITIALIZED"; - case EGL_BAD_ACCESS: - return "EGL_BAD_ACCESS"; - case EGL_BAD_ALLOC: - return "EGL_BAD_ALLOC"; - case EGL_BAD_ATTRIBUTE: - return "EGL_BAD_ATTRIBUTE"; - case EGL_BAD_CONFIG: - return "EGL_BAD_CONFIG"; - case EGL_BAD_CONTEXT: - return "EGL_BAD_CONTEXT"; - case EGL_BAD_CURRENT_SURFACE: - return "EGL_BAD_CURRENT_SURFACE"; - case EGL_BAD_DISPLAY: - return "EGL_BAD_DISPLAY"; - case EGL_BAD_MATCH: - return "EGL_BAD_MATCH"; - case EGL_BAD_NATIVE_PIXMAP: - return "EGL_BAD_NATIVE_PIXMAP"; - case EGL_BAD_NATIVE_WINDOW: - return "EGL_BAD_NATIVE_WINDOW"; - case EGL_BAD_PARAMETER: - return "EGL_BAD_PARAMETER"; - case EGL_BAD_SURFACE: - return "EGL_BAD_SURFACE"; - case EGL_CONTEXT_LOST: - return "EGL_CONTEXT_LOST"; - } - return "unknown EGL error"; -} - -void -startDriver() { - x_dpy = XOpenDisplay(NULL); - if (!x_dpy) { - fprintf(stderr, "XOpenDisplay failed\n"); - exit(1); - } - e_dpy = eglGetDisplay(x_dpy); - if (!e_dpy) { - fprintf(stderr, "eglGetDisplay failed: %s\n", eglGetErrorStr()); - exit(1); - } - EGLint e_major, e_minor; - if (!eglInitialize(e_dpy, &e_major, &e_minor)) { - fprintf(stderr, "eglInitialize failed: %s\n", eglGetErrorStr()); - exit(1); - } - if (!eglBindAPI(EGL_OPENGL_ES_API)) { - fprintf(stderr, "eglBindAPI failed: %s\n", eglGetErrorStr()); - exit(1); - } - - static const EGLint attribs[] = { - EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT, - EGL_SURFACE_TYPE, EGL_WINDOW_BIT, - EGL_BLUE_SIZE, 8, - EGL_GREEN_SIZE, 8, - EGL_RED_SIZE, 8, - EGL_DEPTH_SIZE, 16, - EGL_CONFIG_CAVEAT, EGL_NONE, - EGL_NONE - }; - EGLint num_configs; - if (!eglChooseConfig(e_dpy, attribs, &e_config, 1, &num_configs)) { - fprintf(stderr, "eglChooseConfig failed: %s\n", eglGetErrorStr()); - exit(1); - } - EGLint vid; - if (!eglGetConfigAttrib(e_dpy, e_config, EGL_NATIVE_VISUAL_ID, &vid)) { - fprintf(stderr, "eglGetConfigAttrib failed: %s\n", eglGetErrorStr()); - exit(1); - } - - XVisualInfo visTemplate; - visTemplate.visualid = vid; - int num_visuals; - x_visual_info = XGetVisualInfo(x_dpy, VisualIDMask, &visTemplate, &num_visuals); - if (!x_visual_info) { - fprintf(stderr, "XGetVisualInfo failed\n"); - exit(1); - } - - x_root = RootWindow(x_dpy, DefaultScreen(x_dpy)); - x_colormap = XCreateColormap(x_dpy, x_root, x_visual_info->visual, AllocNone); - if (!x_colormap) { - fprintf(stderr, "XCreateColormap failed\n"); - exit(1); - } - - static const EGLint ctx_attribs[] = { - EGL_CONTEXT_CLIENT_VERSION, 3, - EGL_NONE - }; - e_ctx = eglCreateContext(e_dpy, e_config, EGL_NO_CONTEXT, ctx_attribs); - if (!e_ctx) { - fprintf(stderr, "eglCreateContext failed: %s\n", eglGetErrorStr()); - exit(1); - } - - net_wm_name = XInternAtom(x_dpy, "_NET_WM_NAME", False); - utf8_string = XInternAtom(x_dpy, "UTF8_STRING", False); - wm_delete_window = XInternAtom(x_dpy, "WM_DELETE_WINDOW", False); - wm_protocols = XInternAtom(x_dpy, "WM_PROTOCOLS", False); - wm_take_focus = XInternAtom(x_dpy, "WM_TAKE_FOCUS", False); - - const int key_lo = 8; - const int key_hi = 255; - int keysyms_per_keycode; - KeySym *keysyms = XGetKeyboardMapping(x_dpy, key_lo, key_hi-key_lo+1, &keysyms_per_keycode); - if (keysyms_per_keycode < 2) { - fprintf(stderr, "XGetKeyboardMapping returned too few keysyms per keycode: %d\n", keysyms_per_keycode); - exit(1); - } - int k; - for (k = key_lo; k <= key_hi; k++) { - onKeysym(k, - keysyms[(k-key_lo)*keysyms_per_keycode + 0], - keysyms[(k-key_lo)*keysyms_per_keycode + 1]); - } - //TODO: use GetModifierMapping to figure out which modifier is the numlock modifier. -} - -void -processEvents() { - while (XPending(x_dpy)) { - XEvent ev; - XNextEvent(x_dpy, &ev); - switch (ev.type) { - case KeyPress: - case KeyRelease: - onKey(ev.xkey.window, ev.xkey.state, ev.xkey.keycode, ev.type == KeyPress ? 1 : 2); - break; - case ButtonPress: - case ButtonRelease: - onMouse(ev.xbutton.window, ev.xbutton.x, ev.xbutton.y, ev.xbutton.state, ev.xbutton.button, - ev.type == ButtonPress ? 1 : 2); - break; - case MotionNotify: - onMouse(ev.xmotion.window, ev.xmotion.x, ev.xmotion.y, ev.xmotion.state, 0, 0); - break; - case FocusIn: - case FocusOut: - onFocus(ev.xmotion.window, ev.type == FocusIn); - break; - case Expose: - // A non-zero Count means that there are more expose events coming. For - // example, a non-rectangular exposure (e.g. from a partially overlapped - // window) will result in multiple expose events whose dirty rectangles - // combine to define the dirty region. Go's paint events do not provide - // dirty regions, so we only pass on the final X11 expose event. - if (ev.xexpose.count == 0) { - onExpose(ev.xexpose.window); - } - break; - case ConfigureNotify: - onConfigure(ev.xconfigure.window, ev.xconfigure.x, ev.xconfigure.y, - ev.xconfigure.width, ev.xconfigure.height, - DisplayWidth(x_dpy, DefaultScreen(x_dpy)), - DisplayWidthMM(x_dpy, DefaultScreen(x_dpy))); - break; - case ClientMessage: - if ((ev.xclient.message_type != wm_protocols) || (ev.xclient.format != 32)) { - break; - } - Atom a = ev.xclient.data.l[0]; - if (a == wm_delete_window) { - onDeleteWindow(ev.xclient.window); - } else if (a == wm_take_focus) { - XSetInputFocus(x_dpy, ev.xclient.window, RevertToParent, ev.xclient.data.l[1]); - } - break; - } - } -} - -void -makeCurrent(uintptr_t surface) { - EGLSurface surf = (EGLSurface)(surface); - if (!eglMakeCurrent(e_dpy, surf, surf, e_ctx)) { - fprintf(stderr, "eglMakeCurrent failed: %s\n", eglGetErrorStr()); - exit(1); - } -} - -void -swapBuffers(uintptr_t surface) { - EGLSurface surf = (EGLSurface)(surface); - if (!eglSwapBuffers(e_dpy, surf)) { - fprintf(stderr, "eglSwapBuffers failed: %s\n", eglGetErrorStr()); - exit(1); - } -} - -void -doCloseWindow(uintptr_t id) { - Window win = (Window)(id); - XDestroyWindow(x_dpy, win); -} - -uintptr_t -doNewWindow(int x, int y, int width, int height, char* title, int title_len) { - XSetWindowAttributes attr; - attr.colormap = x_colormap; - attr.event_mask = - KeyPressMask | - KeyReleaseMask | - ButtonPressMask | - ButtonReleaseMask | - PointerMotionMask | - ExposureMask | - StructureNotifyMask | - FocusChangeMask; - - Window win = XCreateWindow( - x_dpy, x_root, x, y, width, height, 0, x_visual_info->depth, InputOutput, - x_visual_info->visual, CWColormap | CWEventMask, &attr); - - XSizeHints sizehints; - sizehints.width = width; - sizehints.height = height; - sizehints.flags = USSize; - XSetNormalHints(x_dpy, win, &sizehints); - - Atom atoms[2]; - atoms[0] = wm_delete_window; - atoms[1] = wm_take_focus; - XSetWMProtocols(x_dpy, win, atoms, 2); - - XSetStandardProperties(x_dpy, win, "", "App", None, (char **)NULL, 0, &sizehints); - XChangeProperty(x_dpy, win, net_wm_name, utf8_string, 8, PropModeReplace, title, title_len); - - return win; -} - -void -doConfigureWindow(uintptr_t id, int x, int y, int width, int height) { - Window win = (Window)(id); - unsigned int mask = CWX | CWY | CWWidth | CWHeight; - XWindowChanges values = {x,y,width,height,0,0,0}; - - XConfigureWindow(x_dpy, win, mask, &values); -} - -uintptr_t -doShowWindow(uintptr_t id) { - Window win = (Window)(id); - XMapWindow(x_dpy, win); - EGLSurface surf = eglCreateWindowSurface(e_dpy, e_config, win, NULL); - if (!surf) { - fprintf(stderr, "eglCreateWindowSurface failed: %s\n", eglGetErrorStr()); - exit(1); - } - return (uintptr_t)(surf); -} - -uintptr_t -surfaceCreate() { - static const EGLint ctx_attribs[] = { - EGL_CONTEXT_CLIENT_VERSION, 3, - EGL_NONE - }; - EGLContext ctx = eglCreateContext(e_dpy, e_config, EGL_NO_CONTEXT, ctx_attribs); - if (!ctx) { - fprintf(stderr, "surface eglCreateContext failed: %s\n", eglGetErrorStr()); - return 0; - } - - static const EGLint cfg_attribs[] = { - EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT, - EGL_SURFACE_TYPE, EGL_PBUFFER_BIT, - EGL_BLUE_SIZE, 8, - EGL_GREEN_SIZE, 8, - EGL_RED_SIZE, 8, - EGL_DEPTH_SIZE, 16, - EGL_CONFIG_CAVEAT, EGL_NONE, - EGL_NONE - }; - EGLConfig cfg; - EGLint num_configs; - if (!eglChooseConfig(e_dpy, cfg_attribs, &cfg, 1, &num_configs)) { - fprintf(stderr, "gldriver: surface eglChooseConfig failed: %s\n", eglGetErrorStr()); - return 0; - } - - // TODO: use the size of the monitor as a bound for texture size. - static const EGLint attribs[] = { - EGL_WIDTH, 4096, - EGL_HEIGHT, 3072, - EGL_NONE - }; - EGLSurface surface = eglCreatePbufferSurface(e_dpy, cfg, attribs); - if (!surface) { - fprintf(stderr, "gldriver: surface eglCreatePbufferSurface failed: %s\n", eglGetErrorStr()); - return 0; - } - - if (!eglMakeCurrent(e_dpy, surface, surface, ctx)) { - fprintf(stderr, "gldriver: surface eglMakeCurrent failed: %s\n", eglGetErrorStr()); - return 0; - } - - return (uintptr_t)surface; -} diff --git a/shiny/driver/gldriver/x11.go b/shiny/driver/gldriver/x11.go deleted file mode 100644 index 23e6cc81..00000000 --- a/shiny/driver/gldriver/x11.go +++ /dev/null @@ -1,322 +0,0 @@ -// Copyright 2015 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -// +build linux,!android openbsd - -package gldriver - -/* -#cgo LDFLAGS: -lEGL -lGLESv2 -lX11 - -#include -#include -#include - -char *eglGetErrorStr(); -void startDriver(); -void processEvents(); -void makeCurrent(uintptr_t ctx); -void swapBuffers(uintptr_t ctx); -void doCloseWindow(uintptr_t id); -void doConfigureWindow(uintptr_t id, int x, int y, int width, int height); -uintptr_t doNewWindow(int x, int y, int width, int height, char* title, int title_len); -uintptr_t doShowWindow(uintptr_t id); -uintptr_t surfaceCreate(); -*/ -import "C" -import ( - "errors" - "runtime" - "time" - "unsafe" - - "github.com/oakmound/oak/v3/shiny/driver/internal/x11key" - "github.com/oakmound/oak/v3/shiny/screen" - "golang.org/x/mobile/event/key" - "golang.org/x/mobile/event/mouse" - "golang.org/x/mobile/event/paint" - "golang.org/x/mobile/event/size" - "golang.org/x/mobile/geom" - "golang.org/x/mobile/gl" -) - -const useLifecycler = true - -const handleSizeEventsAtChannelReceive = true - -var theKeysyms x11key.KeysymTable - -func init() { - // It might not be necessary, but it probably doesn't hurt to try to make - // 'the main thread' be 'the X11 / OpenGL thread'. - runtime.LockOSThread() -} - -func newWindow(opts screen.WindowGenerator) (uintptr, error) { - width, height := optsSize(opts) - - title := opts.Title - ctitle := C.CString(title) - defer C.free(unsafe.Pointer(ctitle)) - - retc := make(chan uintptr) - uic <- uiClosure{ - f: func() uintptr { - return uintptr(C.doNewWindow(C.int(opts.X), C.int(opts.Y), C.int(width), C.int(height), ctitle, C.int(len(title)))) - }, - retc: retc, - } - return <-retc, nil -} - -func moveWindow(w *windowImpl, opts screen.WindowGenerator) error { - width, height := optsSize(opts) - C.doConfigureWindow(C.uintptr_t(w.id), C.int(opts.X), C.int(opts.Y), C.int(width), C.int(height)) - return nil -} - -func initWindow(w *windowImpl) { - w.glctx, w.worker = glctx, worker -} - -func showWindow(w *windowImpl) { - retc := make(chan uintptr) - uic <- uiClosure{ - f: func() uintptr { - return uintptr(C.doShowWindow(C.uintptr_t(w.id))) - }, - retc: retc, - } - w.ctx = <-retc - go drawLoop(w) -} - -func closeWindow(id uintptr) { - uic <- uiClosure{ - f: func() uintptr { - C.doCloseWindow(C.uintptr_t(id)) - return 0 - }, - } -} - -func drawLoop(w *windowImpl) { - glcontextc <- w.ctx.(uintptr) - go func() { - for range w.publish { - publishc <- w - } - }() -} - -var ( - glcontextc = make(chan uintptr) - publishc = make(chan *windowImpl) - uic = make(chan uiClosure) - - // TODO: don't assume that there is only one window, and hence only - // one (global) GL context. - // - // TODO: should we be able to make a shiny.Texture before having a - // shiny.Window's GL context? Should something like gl.IsProgram be a - // method instead of a function, and have each shiny.Window have its own - // gl.Context? - glctx gl.Context - worker gl.Worker -) - -// uiClosure is a closure to be run on C's UI thread. -type uiClosure struct { - f func() uintptr - retc chan uintptr -} - -func main(f func(screen.Screen)) error { - if gl.Version() == "GL_ES_2_0" { - return errors.New("gldriver: ES 3 required on X11") - } - C.startDriver() - glctx, worker = gl.NewContext() - - closec := make(chan struct{}) - go func() { - f(theScreen) - close(closec) - }() - - // heartbeat is a channel that, at regular intervals, directs the select - // below to also consider X11 events, not just Go events (channel - // communications). - // - // TODO: select instead of poll. Note that knowing whether to call - // C.processEvents needs to select on a file descriptor, and the other - // cases below select on Go channels. - heartbeat := time.NewTicker(time.Second / 60) - workAvailable := worker.WorkAvailable() - - for { - select { - case <-closec: - return nil - case ctx := <-glcontextc: - // TODO: do we need to synchronize with seeing a size event for - // this window's context before or after calling makeCurrent? - // Otherwise, are we racing with the gl.Viewport call? I've - // occasionally seen a stale viewport, if the window manager sets - // the window width and height to something other than that - // requested by XCreateWindow, but it's not easily reproducible. - C.makeCurrent(C.uintptr_t(ctx)) - case w := <-publishc: - C.swapBuffers(C.uintptr_t(w.ctx.(uintptr))) - w.publishDone <- screen.PublishResult{} - case req := <-uic: - ret := req.f() - if req.retc != nil { - req.retc <- ret - } - case <-heartbeat.C: - C.processEvents() - case <-workAvailable: - worker.DoWork() - } - } -} - -//export onExpose -func onExpose(id uintptr) { - theScreen.mu.Lock() - w := theScreen.windows[id] - theScreen.mu.Unlock() - - if w == nil { - return - } - - w.Send(paint.Event{External: true}) -} - -//export onKeysym -func onKeysym(k, unshifted, shifted uint32) { - theKeysyms[k][0] = unshifted - theKeysyms[k][1] = shifted -} - -//export onKey -func onKey(id uintptr, state uint16, detail, dir uint8) { - theScreen.mu.Lock() - w := theScreen.windows[id] - theScreen.mu.Unlock() - - if w == nil { - return - } - - r, c := theKeysyms.Lookup(detail, state, 0) - w.Send(key.Event{ - Rune: r, - Code: c, - Modifiers: x11key.KeyModifiers(state), - Direction: key.Direction(dir), - }) -} - -//export onMouse -func onMouse(id uintptr, x, y int32, state uint16, button, dir uint8) { - theScreen.mu.Lock() - w := theScreen.windows[id] - theScreen.mu.Unlock() - - if w == nil { - return - } - - // TODO: should a mouse.Event have a separate MouseModifiers field, for - // which buttons are pressed during a mouse move? - btn := mouse.Button(button) - switch btn { - case 4: - btn = mouse.ButtonWheelUp - case 5: - btn = mouse.ButtonWheelDown - case 6: - btn = mouse.ButtonWheelLeft - case 7: - btn = mouse.ButtonWheelRight - } - if btn.IsWheel() { - if dir != uint8(mouse.DirPress) { - return - } - dir = uint8(mouse.DirStep) - } - w.Send(mouse.Event{ - X: float32(x), - Y: float32(y), - Button: btn, - Modifiers: x11key.KeyModifiers(state), - Direction: mouse.Direction(dir), - }) -} - -//export onFocus -func onFocus(id uintptr, focused bool) { - theScreen.mu.Lock() - w := theScreen.windows[id] - theScreen.mu.Unlock() - - if w == nil { - return - } - - w.lifecycler.SetFocused(focused) - w.lifecycler.SendEvent(w, w.glctx) -} - -//export onConfigure -func onConfigure(id uintptr, x, y, width, height, displayWidth, displayWidthMM int32) { - theScreen.mu.Lock() - w := theScreen.windows[id] - theScreen.mu.Unlock() - - if w == nil { - return - } - - w.lifecycler.SetVisible(x+width > 0 && y+height > 0) - w.lifecycler.SendEvent(w, w.glctx) - - const ( - mmPerInch = 25.4 - ptPerInch = 72 - ) - pixelsPerMM := float32(displayWidth) / float32(displayWidthMM) - w.Send(size.Event{ - WidthPx: int(width), - HeightPx: int(height), - WidthPt: geom.Pt(width), - HeightPt: geom.Pt(height), - PixelsPerPt: pixelsPerMM * mmPerInch / ptPerInch, - }) -} - -//export onDeleteWindow -func onDeleteWindow(id uintptr) { - theScreen.mu.Lock() - w := theScreen.windows[id] - theScreen.mu.Unlock() - - if w == nil { - return - } - - w.lifecycler.SetDead(true) - w.lifecycler.SendEvent(w, w.glctx) -} - -func surfaceCreate() error { - if C.surfaceCreate() == 0 { - return errors.New("gldriver: surface creation failed") - } - return nil -} diff --git a/shiny/driver/internal/drawer/drawer.go b/shiny/driver/internal/drawer/drawer.go index e981eb3f..75094b5d 100644 --- a/shiny/driver/internal/drawer/drawer.go +++ b/shiny/driver/internal/drawer/drawer.go @@ -9,13 +9,13 @@ import ( "image" "image/draw" - "github.com/oakmound/oak/v3/shiny/screen" + "github.com/oakmound/oak/v4/shiny/screen" "golang.org/x/image/math/f64" ) // Copy implements the Copy method of the screen.Drawer interface by calling // the Draw method of that same interface. -func Copy(dst screen.Drawer, dp image.Point, src screen.Texture, sr image.Rectangle, op draw.Op) { +func Copy(dst screen.SimpleDrawer, dp image.Point, src screen.Texture, sr image.Rectangle, op draw.Op) { dst.Draw(f64.Aff3{ 1, 0, float64(dp.X - sr.Min.X), 0, 1, float64(dp.Y - sr.Min.Y), @@ -24,7 +24,7 @@ func Copy(dst screen.Drawer, dp image.Point, src screen.Texture, sr image.Rectan // Scale implements the Scale method of the screen.Drawer interface by calling // the Draw method of that same interface. -func Scale(dst screen.Drawer, dr image.Rectangle, src screen.Texture, sr image.Rectangle, op draw.Op) { +func Scale(dst screen.SimpleDrawer, dr image.Rectangle, src screen.Texture, sr image.Rectangle, op draw.Op) { rx := float64(dr.Dx()) / float64(sr.Dx()) ry := float64(dr.Dy()) / float64(sr.Dy()) dst.Draw(f64.Aff3{ diff --git a/shiny/driver/internal/errscreen/errscreen.go b/shiny/driver/internal/errscreen/errscreen.go index 333a4835..612de6ea 100644 --- a/shiny/driver/internal/errscreen/errscreen.go +++ b/shiny/driver/internal/errscreen/errscreen.go @@ -8,7 +8,7 @@ package errscreen import ( "image" - "github.com/oakmound/oak/v3/shiny/screen" + "github.com/oakmound/oak/v4/shiny/screen" ) // Stub returns a Screen whose methods all return the given error. diff --git a/shiny/driver/internal/event/event.go b/shiny/driver/internal/event/event.go index 8573730b..85fcbedd 100644 --- a/shiny/driver/internal/event/event.go +++ b/shiny/driver/internal/event/event.go @@ -57,12 +57,3 @@ func (q *Deque) Send(event interface{}) { q.back = append(q.back, event) q.cond.Signal() } - -// SendFirst implements the screen.EventDeque interface. -func (q *Deque) SendFirst(event interface{}) { - q.lockAndInit() - defer q.mu.Unlock() - - q.front = append(q.front, event) - q.cond.Signal() -} diff --git a/shiny/driver/internal/win32/cursor.go b/shiny/driver/internal/win32/cursor.go index ecf0ef1e..61e75628 100644 --- a/shiny/driver/internal/win32/cursor.go +++ b/shiny/driver/internal/win32/cursor.go @@ -1,3 +1,6 @@ +//go:build windows +// +build windows + package win32 import "sync" diff --git a/shiny/driver/internal/win32/win32.go b/shiny/driver/internal/win32/win32.go index 9dfa55ec..ebf1ffb3 100644 --- a/shiny/driver/internal/win32/win32.go +++ b/shiny/driver/internal/win32/win32.go @@ -20,7 +20,7 @@ import ( "syscall" "unsafe" - "github.com/oakmound/oak/v3/shiny/screen" + "github.com/oakmound/oak/v4/shiny/screen" "golang.org/x/mobile/event/key" "golang.org/x/mobile/event/lifecycle" "golang.org/x/mobile/event/mouse" @@ -126,11 +126,11 @@ func ResizeClientRect(hwnd HWND, opts screen.WindowGenerator) error { h := (wr.Bottom - wr.Top) - (cr.Bottom - int32(opts.Height)) x := wr.Left if opts.X != 0 { - x = opts.X + x = int32(opts.X) } y := wr.Top if opts.Y != 0 { - y = opts.Y + y = int32(opts.Y) } return MoveWindow(hwnd, x, y, w, h, false) } diff --git a/shiny/driver/internal/win32/zsyscall_windows.go b/shiny/driver/internal/win32/zsyscall_windows.go index 26814887..9f2d2e4d 100644 --- a/shiny/driver/internal/win32/zsyscall_windows.go +++ b/shiny/driver/internal/win32/zsyscall_windows.go @@ -42,7 +42,7 @@ var ( ) var ( - procShell_NotifyIconW = modshell32.NewProc("Shell_NotifyIconW") + procShell_NotifyIconW = modshell32.NewProc("Shell_NotifyIconW") procRegisterClass = moduser32.NewProc("RegisterClassW") procIsZoomed = moduser32.NewProc("IsZoomed") procLoadIcon = moduser32.NewProc("LoadIconW") @@ -60,6 +60,7 @@ var ( procPostMessage = moduser32.NewProc("PostMessageW") procSetWindowText = moduser32.NewProc("SetWindowTextW") procGetWindowRect = moduser32.NewProc("GetWindowRect") + procGetWindow = moduser32.NewProc("GetWindow") procMoveWindow = moduser32.NewProc("MoveWindow") procScreenToClient = moduser32.NewProc("ScreenToClient") procSetWindowLong = moduser32.NewProc("SetWindowLongW") @@ -180,6 +181,11 @@ func DestroyWindow(hwnd HWND) bool { return ret != 0 } +const ( + ICON_BIG = 1 + ICON_SMALL = 0 +) + func DefWindowProc(hwnd HWND, msg uint32, wParam, lParam uintptr) (uintptr, error) { ret, _, err := procDefWindowProc.Call( uintptr(hwnd), diff --git a/shiny/driver/internal/x11/x11.go b/shiny/driver/internal/x11/x11.go index 3584f00b..b85f17c0 100644 --- a/shiny/driver/internal/x11/x11.go +++ b/shiny/driver/internal/x11/x11.go @@ -11,7 +11,7 @@ import ( "github.com/BurntSushi/xgbutil/motif" ) -func MoveWindow(xc *xgb.Conn, xw xproto.Window, x, y, width, height int32) (int32, int32, int32, int32) { +func MoveWindow(xc *xgb.Conn, xw xproto.Window, x, y, width, height int) (int, int, int, int) { vals := []uint32{} flags := xproto.ConfigWindowHeight | @@ -51,10 +51,22 @@ func SetFullScreen(xutil *xgbutil.XUtil, win xproto.Window, fullscreen bool) err return ewmh.WmStateReq(xutil, win, action, "_NET_WM_STATE_FULLSCREEN") } +func ToggleTopMost(xutil *xgbutil.XUtil, win xproto.Window) error { + return ewmh.WmStateReq(xutil, win, ewmh.StateToggle, "_NET_WM_STATE_ABOVE") +} + +func SetTopMost(xutil *xgbutil.XUtil, win xproto.Window, topMost bool) error { + action := ewmh.StateRemove + if topMost { + action = ewmh.StateAdd + } + return ewmh.WmStateReq(xutil, win, action, "_NET_WM_STATE_ABOVE") +} + func SetBorderless(xutil *xgbutil.XUtil, win xproto.Window, borderless bool) error { hints := &motif.Hints{ - Flags: motif.HintDecorations, - Decoration: motif.DecorationNone, + Flags: motif.HintDecorations, + Decoration: motif.DecorationNone, } if !borderless { hints.Decoration = motif.DecorationAll diff --git a/shiny/driver/jsdriver/screen.go b/shiny/driver/jsdriver/screen.go index 806cfaaf..70472f30 100644 --- a/shiny/driver/jsdriver/screen.go +++ b/shiny/driver/jsdriver/screen.go @@ -1,6 +1,7 @@ //go:build js // +build js +// Package jsdriver provides a WASM/JS driver for accessing a screen. package jsdriver import ( @@ -8,7 +9,7 @@ import ( "image" "syscall/js" - "github.com/oakmound/oak/v3/shiny/screen" + "github.com/oakmound/oak/v4/shiny/screen" "golang.org/x/mobile/event/key" "golang.org/x/mobile/event/mouse" ) @@ -18,7 +19,7 @@ func Main(f func(screen.Screen)) { } type screenImpl struct { - windows []*windowImpl + windows []*Window } func (s *screenImpl) NewImage(size image.Point) (screen.Image, error) { @@ -42,7 +43,7 @@ func (s *screenImpl) NewWindow(opts screen.WindowGenerator) (screen.Window, erro } cvs := NewCanvas2d(opts.Width, opts.Height) - w := &windowImpl{ + w := &Window{ cvs: cvs, screen: s, } diff --git a/shiny/driver/jsdriver/texture.go b/shiny/driver/jsdriver/texture.go index 2dc94061..c9e8f801 100644 --- a/shiny/driver/jsdriver/texture.go +++ b/shiny/driver/jsdriver/texture.go @@ -8,7 +8,7 @@ import ( "image/color" "image/draw" - "github.com/oakmound/oak/v3/shiny/screen" + "github.com/oakmound/oak/v4/shiny/screen" ) type textureImpl struct { diff --git a/shiny/driver/jsdriver/window.go b/shiny/driver/jsdriver/window.go index 111f24fc..c6af3cd0 100644 --- a/shiny/driver/jsdriver/window.go +++ b/shiny/driver/jsdriver/window.go @@ -5,41 +5,33 @@ package jsdriver import ( "image" - "image/color" "image/draw" "syscall/js" - "github.com/oakmound/oak/v3/shiny/driver/internal/event" - "github.com/oakmound/oak/v3/shiny/screen" - "golang.org/x/image/math/f64" + "github.com/oakmound/oak/v4/shiny/driver/internal/event" + "github.com/oakmound/oak/v4/shiny/screen" "golang.org/x/mobile/event/key" "golang.org/x/mobile/event/mouse" ) -type windowImpl struct { +type Window struct { screen *screenImpl cvs *Canvas2D event.Deque } -func (w *windowImpl) Release() {} -func (w *windowImpl) Draw(src2dst f64.Aff3, src screen.Texture, sr image.Rectangle, op draw.Op) {} -func (w *windowImpl) DrawUniform(src2dst f64.Aff3, src color.Color, sr image.Rectangle, op draw.Op) {} -func (w *windowImpl) Copy(dp image.Point, src screen.Texture, sr image.Rectangle, op draw.Op) {} -func (w *windowImpl) Scale(dr image.Rectangle, src screen.Texture, sr image.Rectangle, op draw.Op) { +func (w *Window) Release() {} +func (w *Window) Scale(dr image.Rectangle, src screen.Texture, sr image.Rectangle, op draw.Op) { rgba := src.(*textureImpl).rgba js.CopyBytesToJS(w.cvs.copybuff, rgba.Pix) w.cvs.imgData.Get("data").Call("set", w.cvs.copybuff) w.cvs.ctx.Call("putImageData", w.cvs.imgData, 0, 0) } -func (w *windowImpl) Upload(dp image.Point, src screen.Image, sr image.Rectangle) {} -func (w *windowImpl) Fill(dr image.Rectangle, src color.Color, op draw.Op) {} +func (w *Window) Upload(dp image.Point, src screen.Image, sr image.Rectangle) {} -func (w *windowImpl) Publish() screen.PublishResult { - return screen.PublishResult{} -} +func (w *Window) Publish() {} -func (w *windowImpl) sendMouseEvent(mouseEvent js.Value, dir mouse.Direction) { +func (w *Window) sendMouseEvent(mouseEvent js.Value, dir mouse.Direction) { x, y := mouseEvent.Get("offsetX"), mouseEvent.Get("offsetY") button := mouseEvent.Get("button") var mButton mouse.Button @@ -61,7 +53,7 @@ func (w *windowImpl) sendMouseEvent(mouseEvent js.Value, dir mouse.Direction) { }) } -func (w *windowImpl) sendKeyEvent(keyEvent js.Value, dir key.Direction) { +func (w *Window) sendKeyEvent(keyEvent js.Value, dir key.Direction) { var mods key.Modifiers if keyEvent.Get("shiftKey").Bool() { mods |= key.ModShift diff --git a/shiny/driver/mtldriver/internal/appkit/appkit.go b/shiny/driver/mtldriver/internal/appkit/appkit.go index 4a408bab..93515791 100644 --- a/shiny/driver/mtldriver/internal/appkit/appkit.go +++ b/shiny/driver/mtldriver/internal/appkit/appkit.go @@ -18,7 +18,7 @@ package appkit import ( "unsafe" - "github.com/oakmound/oak/v3/shiny/driver/mtldriver/internal/coreanim" + "github.com/oakmound/oak/v4/shiny/driver/mtldriver/internal/coreanim" ) /* diff --git a/shiny/driver/mtldriver/mtldriver.go b/shiny/driver/mtldriver/mtldriver.go index 68cebe79..b866eb2a 100644 --- a/shiny/driver/mtldriver/mtldriver.go +++ b/shiny/driver/mtldriver/mtldriver.go @@ -21,10 +21,10 @@ import ( "dmitri.shuralyov.com/gpu/mtl" "github.com/go-gl/glfw/v3.3/glfw" - "github.com/oakmound/oak/v3/shiny/driver/internal/errscreen" - "github.com/oakmound/oak/v3/shiny/driver/mtldriver/internal/appkit" - "github.com/oakmound/oak/v3/shiny/driver/mtldriver/internal/coreanim" - "github.com/oakmound/oak/v3/shiny/screen" + "github.com/oakmound/oak/v4/shiny/driver/internal/errscreen" + "github.com/oakmound/oak/v4/shiny/driver/mtldriver/internal/appkit" + "github.com/oakmound/oak/v4/shiny/driver/mtldriver/internal/coreanim" + "github.com/oakmound/oak/v4/shiny/screen" "golang.org/x/mobile/event/key" "golang.org/x/mobile/event/mouse" "golang.org/x/mobile/event/paint" @@ -106,10 +106,15 @@ func main(f func(screen.Screen)) error { case req := <-glfwChans.updateCh: // this is not functionalized to prevent methods from accidentally // calling this outside of the main thread + if req.title != nil { + req.window.SetTitle(*req.title) + } + for _, atr := range req.attribs { + req.window.SetAttrib(atr.key, atr.val) + } if req.setPos { req.window.SetPos(int(req.x), int(req.y)) req.window.SetSize(int(req.width), int(req.height)) - req.respCh <- struct{}{} } if req.setBorderless != nil { if *req.setBorderless { @@ -162,6 +167,8 @@ type updateWindowReq struct { setBorderless *bool setPos bool x, y, width, height int + title *string + attribs []attribPair respCh chan struct{} } @@ -199,7 +206,7 @@ func newWindow(device mtl.Device, chans windowRequestChannels, opts screen.Windo window.SetAttrib(glfw.Decorated, 0) } - w := &windowImpl{ + w := &Window{ device: device, window: window, chans: chans, diff --git a/shiny/driver/mtldriver/screen.go b/shiny/driver/mtldriver/screen.go index f269f3a3..86b6101b 100644 --- a/shiny/driver/mtldriver/screen.go +++ b/shiny/driver/mtldriver/screen.go @@ -11,7 +11,7 @@ import ( "image" "github.com/go-gl/glfw/v3.3/glfw" - "github.com/oakmound/oak/v3/shiny/screen" + "github.com/oakmound/oak/v4/shiny/screen" ) // screenImpl implements screen.Screen. diff --git a/shiny/driver/mtldriver/texture.go b/shiny/driver/mtldriver/texture.go index 7f6179ce..32f94225 100644 --- a/shiny/driver/mtldriver/texture.go +++ b/shiny/driver/mtldriver/texture.go @@ -11,7 +11,7 @@ import ( "image" "image/color" - "github.com/oakmound/oak/v3/shiny/screen" + "github.com/oakmound/oak/v4/shiny/screen" "golang.org/x/image/draw" ) diff --git a/shiny/driver/mtldriver/window.go b/shiny/driver/mtldriver/window.go index 30121c9e..2f26dde5 100644 --- a/shiny/driver/mtldriver/window.go +++ b/shiny/driver/mtldriver/window.go @@ -13,15 +13,14 @@ import ( "dmitri.shuralyov.com/gpu/mtl" "github.com/go-gl/glfw/v3.3/glfw" - "github.com/oakmound/oak/v3/shiny/driver/internal/event" - "github.com/oakmound/oak/v3/shiny/driver/internal/lifecycler" - "github.com/oakmound/oak/v3/shiny/driver/mtldriver/internal/coreanim" - "github.com/oakmound/oak/v3/shiny/screen" + "github.com/oakmound/oak/v4/shiny/driver/internal/event" + "github.com/oakmound/oak/v4/shiny/driver/internal/lifecycler" + "github.com/oakmound/oak/v4/shiny/driver/mtldriver/internal/coreanim" "golang.org/x/mobile/event/size" ) -// windowImpl implements screen.Window. -type windowImpl struct { +// Window implements screen.Window. +type Window struct { device mtl.Device window *glfw.Window chans windowRequestChannels @@ -42,12 +41,12 @@ type windowImpl struct { x, y int } -func (w *windowImpl) HideCursor() error { +func (w *Window) HideCursor() error { w.window.SetInputMode(glfw.CursorMode, glfw.CursorHidden) return nil } -func (w *windowImpl) SetBorderless(borderless bool) error { +func (w *Window) SetBorderless(borderless bool) error { if w.borderless == borderless { return nil } @@ -67,7 +66,7 @@ func (w *windowImpl) SetBorderless(borderless bool) error { return nil } -func (w *windowImpl) SetFullScreen(full bool) error { +func (w *Window) SetFullScreen(full bool) error { if w.fullscreen == full { return nil } @@ -90,12 +89,12 @@ func (w *windowImpl) SetFullScreen(full bool) error { return nil } -func (w *windowImpl) MoveWindow(x, y, width, height int32) error { +func (w *Window) MoveWindow(x, y, width, height int) error { respCh := make(chan struct{}) - w.x = int(x) - w.y = int(y) - w.w = int(width) - w.h = int(height) + w.x = x + w.y = y + w.w = width + w.h = height w.chans.updateCh <- updateWindowReq{ window: w.window, setPos: true, @@ -110,11 +109,11 @@ func (w *windowImpl) MoveWindow(x, y, width, height int32) error { return nil } -func (w *windowImpl) GetCursorPosition() (x, y float64) { +func (w *Window) GetCursorPosition() (x, y float64) { return w.window.GetCursorPos() } -func (w *windowImpl) Release() { +func (w *Window) Release() { respCh := make(chan struct{}) w.chans.releaseCh <- releaseWindowReq{ window: w.window, @@ -124,7 +123,49 @@ func (w *windowImpl) Release() { <-respCh } -func (w *windowImpl) NextEvent() interface{} { +func (w *Window) SetTitle(title string) error { + respCh := make(chan struct{}) + w.chans.updateCh <- updateWindowReq{ + window: w.window, + title: &title, + respCh: respCh, + } + glfw.PostEmptyEvent() // Break main loop out of glfw.WaitEvents so it can receive on releaseWindowCh. + <-respCh + return nil +} + +type attribPair struct { + key glfw.Hint + val int +} + +func (w *Window) SetTopMost(topMost bool) error { + respCh := make(chan struct{}) + val := glfw.True + if !topMost { + val = glfw.False + } + w.chans.updateCh <- updateWindowReq{ + window: w.window, + attribs: []attribPair{{ + key: glfw.Floating, + val: val, + }}, + respCh: respCh, + } + glfw.PostEmptyEvent() // Break main loop out of glfw.WaitEvents so it can receive on releaseWindowCh. + <-respCh + return nil +} + +// BUG: this doesn't work, and it doesn't error either +func (w *Window) SetIcon(img image.Image) error { + w.window.SetIcon([]image.Image{img}) + return nil +} + +func (w *Window) NextEvent() interface{} { e := w.Deque.NextEvent() if sz, ok := e.(size.Event); ok { // TODO(dmitshur): this is the best place/time/frequency to do this @@ -143,7 +184,7 @@ func (w *windowImpl) NextEvent() interface{} { return e } -func (w *windowImpl) Publish() screen.PublishResult { +func (w *Window) Publish() { // Copy w.rgba pixels into a texture. region := mtl.RegionMake2D(0, 0, w.texture.Width, w.texture.Height) bytesPerRow := 4 * w.texture.Width @@ -152,7 +193,7 @@ func (w *windowImpl) Publish() screen.PublishResult { drawable, err := w.ml.NextDrawable() if err != nil { log.Println("Window.Publish: couldn't get the next drawable:", err) - return screen.PublishResult{} + return } cb := w.cq.MakeCommandBuffer() @@ -171,5 +212,5 @@ func (w *windowImpl) Publish() screen.PublishResult { cb.PresentDrawable(drawable) cb.Commit() - return screen.PublishResult{} + return } diff --git a/shiny/driver/mtldriver/window_amd64.go b/shiny/driver/mtldriver/window_amd64.go index f8dafad9..9e6e0f9d 100644 --- a/shiny/driver/mtldriver/window_amd64.go +++ b/shiny/driver/mtldriver/window_amd64.go @@ -5,35 +5,22 @@ package mtldriver import ( "image" - "image/color" - "github.com/oakmound/oak/v3/shiny/driver/internal/drawer" - "github.com/oakmound/oak/v3/shiny/screen" + "github.com/oakmound/oak/v4/shiny/driver/internal/drawer" + "github.com/oakmound/oak/v4/shiny/screen" "golang.org/x/image/draw" "golang.org/x/image/math/f64" ) -func (w *windowImpl) Upload(dp image.Point, src screen.Image, sr image.Rectangle) { +func (w *Window) Upload(dp image.Point, src screen.Image, sr image.Rectangle) { draw.Draw(w.bgra, sr.Sub(sr.Min).Add(dp), src.RGBA(), sr.Min, draw.Src) } -func (w *windowImpl) Fill(dr image.Rectangle, src color.Color, op draw.Op) { - draw.Draw(w.bgra, dr, &image.Uniform{src}, image.Point{}, op) -} - -func (w *windowImpl) Draw(src2dst f64.Aff3, src screen.Texture, sr image.Rectangle, op draw.Op) { +func (w *Window) Draw(src2dst f64.Aff3, src screen.Texture, sr image.Rectangle, op draw.Op) { draw.NearestNeighbor.Transform(w.bgra, src2dst, src.(*textureImpl).rgba, sr, op, nil) } -func (w *windowImpl) DrawUniform(src2dst f64.Aff3, src color.Color, sr image.Rectangle, op draw.Op) { - draw.NearestNeighbor.Transform(w.bgra, src2dst, &image.Uniform{src}, sr, op, nil) -} - -func (w *windowImpl) Copy(dp image.Point, src screen.Texture, sr image.Rectangle, op draw.Op) { - drawer.Copy(w, dp, src, sr, op) -} - -func (w *windowImpl) Scale(dr image.Rectangle, src screen.Texture, sr image.Rectangle, op draw.Op) { +func (w *Window) Scale(dr image.Rectangle, src screen.Texture, sr image.Rectangle, op draw.Op) { drawer.Scale(w, dr, src, sr, op) } diff --git a/shiny/driver/mtldriver/window_arm64.go b/shiny/driver/mtldriver/window_arm64.go index ea5818dc..56c242c3 100644 --- a/shiny/driver/mtldriver/window_arm64.go +++ b/shiny/driver/mtldriver/window_arm64.go @@ -5,15 +5,14 @@ package mtldriver import ( "image" - "image/color" - "github.com/oakmound/oak/v3/shiny/driver/internal/drawer" - "github.com/oakmound/oak/v3/shiny/screen" + "github.com/oakmound/oak/v4/shiny/driver/internal/drawer" + "github.com/oakmound/oak/v4/shiny/screen" "golang.org/x/image/draw" "golang.org/x/image/math/f64" ) -func (w *windowImpl) Upload(dp image.Point, srcImg screen.Image, sr image.Rectangle) { +func (w *Window) Upload(dp image.Point, srcImg screen.Image, sr image.Rectangle) { dst := w.bgra r := sr.Sub(sr.Min).Add(dp) src := srcImg.RGBA() @@ -45,22 +44,10 @@ func (w *windowImpl) Upload(dp image.Point, srcImg screen.Image, sr image.Rectan } } -func (w *windowImpl) Fill(dr image.Rectangle, src color.Color, op draw.Op) { - // Unimplemented -} - -func (w *windowImpl) Draw(src2dst f64.Aff3, src screen.Texture, sr image.Rectangle, op draw.Op) { +func (w *Window) Draw(src2dst f64.Aff3, src screen.Texture, sr image.Rectangle, op draw.Op) { nnInterpolator{}.Transform(w.bgra, src2dst, src.(*textureImpl).rgba, sr) } -func (w *windowImpl) DrawUniform(src2dst f64.Aff3, src color.Color, sr image.Rectangle, op draw.Op) { - // Unimplemented -} - -func (w *windowImpl) Copy(dp image.Point, src screen.Texture, sr image.Rectangle, op draw.Op) { - drawer.Copy(w, dp, src, sr, draw.Over) -} - -func (w *windowImpl) Scale(dr image.Rectangle, src screen.Texture, sr image.Rectangle, op draw.Op) { +func (w *Window) Scale(dr image.Rectangle, src screen.Texture, sr image.Rectangle, op draw.Op) { drawer.Scale(w, dr, src, sr, draw.Over) } diff --git a/shiny/driver/mtldriver_darwin.go b/shiny/driver/mtldriver_darwin.go index 59618ed7..5eaf56f9 100644 --- a/shiny/driver/mtldriver_darwin.go +++ b/shiny/driver/mtldriver_darwin.go @@ -2,55 +2,18 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -// +build darwin -// +build !noop +//go:build darwin && !noop +// +build darwin,!noop package driver import ( - "bytes" - "fmt" - "os/exec" - "regexp" - "strconv" - - "github.com/oakmound/oak/v3/shiny/driver/mtldriver" - "github.com/oakmound/oak/v3/shiny/screen" + "github.com/oakmound/oak/v4/shiny/driver/mtldriver" + "github.com/oakmound/oak/v4/shiny/screen" ) func main(f func(screen.Screen)) { mtldriver.Main(f) } -var ( - sysProfRegex = regexp.MustCompile(`Resolution: (\d)* x (\d)*`) -) - -func monitorSize() (int, int) { - out, err := exec.Command("system_profiler", "SPDisplaysDataType").CombinedOutput() - if err != nil { - return 0, 0 - } - found := sysProfRegex.FindAll(out, -1) - if len(found) == 0 { - return 0, 0 - } - if len(found) != 1 { - fmt.Println("Found multiple screens", len(found)) - } - first := found[0] - first = bytes.TrimPrefix(first, []byte("Resolution: ")) - dims := bytes.Split(first, []byte(" x ")) - if len(dims) != 2 { - return 0, 0 - } - w, err := strconv.Atoi(string(dims[0])) - if err != nil { - return 0, 0 - } - h, err := strconv.Atoi(string(dims[1])) - if err != nil { - return 0, 0 - } - return w, h -} +type Window = mtldriver.Window diff --git a/shiny/driver/noop/noop.go b/shiny/driver/noop/noop.go index 145e0e30..521de1b3 100644 --- a/shiny/driver/noop/noop.go +++ b/shiny/driver/noop/noop.go @@ -6,9 +6,8 @@ import ( "image/color" "image/draw" - "github.com/oakmound/oak/v3/shiny/driver/internal/event" - "github.com/oakmound/oak/v3/shiny/screen" - "golang.org/x/image/math/f64" + "github.com/oakmound/oak/v4/shiny/driver/internal/event" + "github.com/oakmound/oak/v4/shiny/screen" ) func Main(f func(screen.Screen)) { @@ -31,7 +30,7 @@ func (screenImpl) NewTexture(size image.Point) (screen.Texture, error) { } func (screenImpl) NewWindow(opts screen.WindowGenerator) (screen.Window, error) { - return &windowImpl{}, nil + return &Window{}, nil } type imageImpl struct { @@ -69,18 +68,12 @@ func (textureImpl) Upload(dp image.Point, src screen.Image, sr image.Rectangle) func (textureImpl) Fill(dr image.Rectangle, src color.Color, op draw.Op) {} func (textureImpl) Release() {} -type windowImpl struct { +type Window struct { event.Deque } -func (*windowImpl) Release() {} -func (*windowImpl) Draw(src2dst f64.Aff3, src screen.Texture, sr image.Rectangle, op draw.Op) {} -func (*windowImpl) DrawUniform(src2dst f64.Aff3, src color.Color, sr image.Rectangle, op draw.Op) {} -func (*windowImpl) Copy(dp image.Point, src screen.Texture, sr image.Rectangle, op draw.Op) {} -func (*windowImpl) Scale(dr image.Rectangle, src screen.Texture, sr image.Rectangle, op draw.Op) {} -func (*windowImpl) Upload(dp image.Point, src screen.Image, sr image.Rectangle) {} -func (*windowImpl) Fill(dr image.Rectangle, src color.Color, op draw.Op) {} +func (*Window) Release() {} +func (*Window) Scale(dr image.Rectangle, src screen.Texture, sr image.Rectangle, op draw.Op) {} +func (*Window) Upload(dp image.Point, src screen.Image, sr image.Rectangle) {} -func (*windowImpl) Publish() screen.PublishResult { - return screen.PublishResult{} -} +func (*Window) Publish() {} diff --git a/shiny/driver/windriver/buffer.go b/shiny/driver/windriver/buffer.go index a5cea54e..4fc6aa23 100644 --- a/shiny/driver/windriver/buffer.go +++ b/shiny/driver/windriver/buffer.go @@ -2,6 +2,7 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. +//go:build windows // +build windows package windriver @@ -12,8 +13,8 @@ import ( "sync" "syscall" - "github.com/oakmound/oak/v3/shiny/driver/internal/swizzle" - "github.com/oakmound/oak/v3/shiny/driver/internal/win32" + "github.com/oakmound/oak/v4/shiny/driver/internal/swizzle" + "github.com/oakmound/oak/v4/shiny/driver/internal/win32" ) type bufferImpl struct { diff --git a/shiny/driver/windriver/ico.go b/shiny/driver/windriver/ico.go new file mode 100644 index 00000000..f6f4d38a --- /dev/null +++ b/shiny/driver/windriver/ico.go @@ -0,0 +1,74 @@ +package windriver + +import ( + "bufio" + "bytes" + "encoding/binary" + "image" + "image/draw" + "image/png" + "io" +) + +// adapted from https://github.com/Kodeworks/golang-image-ico + +type icondir struct { + reserved uint16 + imageType uint16 + numImages uint16 +} + +type icondirentry struct { + imageWidth uint8 + imageHeight uint8 + numColors uint8 + reserved uint8 + colorPlanes uint16 + bitsPerPixel uint16 + sizeInBytes uint32 + offset uint32 +} + +func newIcondir() icondir { + var id icondir + id.imageType = 1 + id.numImages = 1 + return id +} + +func newIcondirentry() icondirentry { + var ide icondirentry + ide.colorPlanes = 1 // windows is supposed to not mind 0 or 1, but other icon files seem to have 1 here + ide.bitsPerPixel = 32 // can be 24 for bitmap or 24/32 for png. Set to 32 for now + ide.offset = 22 //6 icondir + 16 icondirentry, next image will be this image size + 16 icondirentry, etc + return ide +} + +func encodeIco(w io.Writer, im image.Image) error { + b := im.Bounds() + m := image.NewRGBA(b) + draw.Draw(m, b, im, b.Min, draw.Src) + + id := newIcondir() + ide := newIcondirentry() + + pngbb := new(bytes.Buffer) + pngwriter := bufio.NewWriter(pngbb) + png.Encode(pngwriter, m) + pngwriter.Flush() + ide.sizeInBytes = uint32(len(pngbb.Bytes())) + + bounds := m.Bounds() + ide.imageWidth = uint8(bounds.Dx()) + ide.imageHeight = uint8(bounds.Dy()) + bb := new(bytes.Buffer) + + var e error + binary.Write(bb, binary.LittleEndian, id) + binary.Write(bb, binary.LittleEndian, ide) + + w.Write(bb.Bytes()) + w.Write(pngbb.Bytes()) + + return e +} diff --git a/shiny/driver/windriver/other.go b/shiny/driver/windriver/other.go index 72e8c2e2..2197033d 100644 --- a/shiny/driver/windriver/other.go +++ b/shiny/driver/windriver/other.go @@ -2,6 +2,7 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. +//go:build !windows // +build !windows package windriver @@ -10,8 +11,8 @@ import ( "fmt" "runtime" - "github.com/oakmound/oak/v3/shiny/driver/internal/errscreen" - "github.com/oakmound/oak/v3/shiny/screen" + "github.com/oakmound/oak/v4/shiny/driver/internal/errscreen" + "github.com/oakmound/oak/v4/shiny/screen" ) // Main is called by the program's main function to run the graphical diff --git a/shiny/driver/windriver/screen.go b/shiny/driver/windriver/screen.go index e4be68be..0e916055 100644 --- a/shiny/driver/windriver/screen.go +++ b/shiny/driver/windriver/screen.go @@ -2,6 +2,7 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. +//go:build windows // +build windows package windriver @@ -11,8 +12,8 @@ import ( "image" "unsafe" - "github.com/oakmound/oak/v3/shiny/driver/internal/win32" - "github.com/oakmound/oak/v3/shiny/screen" + "github.com/oakmound/oak/v4/shiny/driver/internal/win32" + "github.com/oakmound/oak/v4/shiny/screen" ) type screenImpl struct { @@ -61,7 +62,7 @@ func (s *screenImpl) NewTexture(size image.Point) (screen.Texture, error) { } func (s *screenImpl) NewWindow(opts screen.WindowGenerator) (screen.Window, error) { - w := &windowImpl{} + w := &Window{} var err error w.hwnd, err = win32.NewWindow(s.screenHWND, opts) @@ -69,6 +70,7 @@ func (s *screenImpl) NewWindow(opts screen.WindowGenerator) (screen.Window, erro w.exStyle = win32.WS_EX_WINDOWEDGE if opts.TopMost { w.exStyle |= win32.WS_EX_TOPMOST + w.topMost = true } if err != nil { diff --git a/shiny/driver/windriver/texture.go b/shiny/driver/windriver/texture.go index 727e17d4..36edce5b 100644 --- a/shiny/driver/windriver/texture.go +++ b/shiny/driver/windriver/texture.go @@ -2,6 +2,7 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. +//go:build windows // +build windows package windriver @@ -15,8 +16,8 @@ import ( "syscall" "unsafe" - "github.com/oakmound/oak/v3/shiny/driver/internal/win32" - "github.com/oakmound/oak/v3/shiny/screen" + "github.com/oakmound/oak/v4/shiny/driver/internal/win32" + "github.com/oakmound/oak/v4/shiny/screen" ) type textureImpl struct { diff --git a/shiny/driver/windriver/window.go b/shiny/driver/windriver/window.go index 0062213a..a864882f 100644 --- a/shiny/driver/windriver/window.go +++ b/shiny/driver/windriver/window.go @@ -2,6 +2,7 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. +//go:build windows // +build windows package windriver @@ -15,14 +16,18 @@ import ( "image/color" "image/draw" "math" + "math/rand" + "os" + "path/filepath" + "strconv" "sync" "syscall" "unsafe" - "github.com/oakmound/oak/v3/shiny/driver/internal/drawer" - "github.com/oakmound/oak/v3/shiny/driver/internal/event" - "github.com/oakmound/oak/v3/shiny/driver/internal/win32" - "github.com/oakmound/oak/v3/shiny/screen" + "github.com/oakmound/oak/v4/shiny/driver/internal/drawer" + "github.com/oakmound/oak/v4/shiny/driver/internal/event" + "github.com/oakmound/oak/v4/shiny/driver/internal/win32" + "github.com/oakmound/oak/v4/shiny/screen" "golang.org/x/image/math/f64" "golang.org/x/mobile/event/key" "golang.org/x/mobile/event/lifecycle" @@ -34,12 +39,14 @@ import ( var ( windowLock sync.RWMutex - allWindows = make(map[win32.HWND]*windowImpl) + allWindows = make(map[win32.HWND]*Window) ) -type windowImpl struct { +type Window struct { hwnd win32.HWND + changeLock sync.RWMutex + event.Deque sz size.Event @@ -50,6 +57,7 @@ type windowImpl struct { fullscreen bool borderless bool maximized bool + topMost bool windowRect *win32.RECT clientRect *win32.RECT @@ -59,7 +67,7 @@ type windowImpl struct { trayGUID *win32.GUID } -func (w *windowImpl) Release() { +func (w *Window) Release() { if w.trayGUID != nil { iconData := win32.NOTIFYICONDATA{} iconData.CbSize = uint32(unsafe.Sizeof(iconData)) @@ -71,7 +79,7 @@ func (w *windowImpl) Release() { win32.Release(win32.HWND(w.hwnd)) } -func (w *windowImpl) Upload(dp image.Point, src screen.Image, sr image.Rectangle) { +func (w *Window) Upload(dp image.Point, src screen.Image, sr image.Rectangle) { src.(*bufferImpl).preUpload() defer src.(*bufferImpl).postUpload() @@ -83,16 +91,7 @@ func (w *windowImpl) Upload(dp image.Point, src screen.Image, sr image.Rectangle }) } -func (w *windowImpl) Fill(dr image.Rectangle, src color.Color, op draw.Op) { - w.execCmd(&cmd{ - id: cmdFill, - dr: dr, - color: src, - op: op, - }) -} - -func (w *windowImpl) Draw(src2dst f64.Aff3, src screen.Texture, sr image.Rectangle, op draw.Op) { +func (w *Window) Draw(src2dst f64.Aff3, src screen.Texture, sr image.Rectangle, op draw.Op) { if op != draw.Src && op != draw.Over { // TODO: return @@ -106,25 +105,12 @@ func (w *windowImpl) Draw(src2dst f64.Aff3, src screen.Texture, sr image.Rectang }) } -func (w *windowImpl) DrawUniform(src2dst f64.Aff3, src color.Color, sr image.Rectangle, op draw.Op) { - if op != draw.Src && op != draw.Over { - return - } - w.execCmd(&cmd{ - id: cmdDrawUniform, - src2dst: src2dst, - color: src, - sr: sr, - op: op, - }) -} - -func (w *windowImpl) SetTitle(title string) error { +func (w *Window) SetTitle(title string) error { win32.SetWindowText(w.hwnd, title) return nil } -func (w *windowImpl) SetBorderless(borderless bool) error { +func (w *Window) SetBorderless(borderless bool) error { // Don't set borderless if currently fullscreen. if !w.fullscreen && borderless != w.borderless { if !w.borderless { @@ -169,7 +155,7 @@ func (w *windowImpl) SetBorderless(borderless bool) error { return nil } -func (w *windowImpl) SetFullScreen(fullscreen bool) error { +func (w *Window) SetFullScreen(fullscreen bool) error { if w.borderless { return errors.New("cannot combine borderless and fullscreen") } @@ -232,7 +218,7 @@ func (w *windowImpl) SetFullScreen(fullscreen bool) error { } // HideCursor turns the OS cursor into a 1x1 transparent image. -func (w *windowImpl) HideCursor() error { +func (w *Window) HideCursor() error { emptyCursor := win32.GetEmptyCursor() success := win32.SetClassLongPtr(w.hwnd, win32.GCLP_HCURSOR, uintptr(emptyCursor)) if !success { @@ -241,74 +227,67 @@ func (w *windowImpl) HideCursor() error { return nil } -func (w *windowImpl) SetTrayIcon(iconPath string) error { - if w.trayGUID == nil { - if err := w.createTrayItem(); err != nil { - return err - } +// SetIcon sets this window's taskbar (and top left corner) icon +func (w *Window) SetIcon(icon image.Image) error { + // windows supports four modes of setting icons: + // 1. loading internal resources embedded into binaries in a windows-specific fashion + // 2. loading from file + // 3. using windows-os built in icons like question marks + // 4. hand crafting black and white icons via combining AND and XOR masks + // + // note, none of these are 'use an icon held in application memory' + // + // 1 is not an option for a multiplatform app. + // 3 is not an option because icons are usually not built in windows icons. + // 4 is not an option because icons are usually colorful. + // + // so we're left with 2: take the image given, write it as an icon to a temporary + // file, load that file, set it as the icon, delete that file. + iconPath := filepath.Join(os.TempDir(), "oakicon"+strconv.Itoa(rand.Int())+".ico") + f, err := os.Create(iconPath) + if err != nil { + return fmt.Errorf("failed to create icon: %w", err) } - iconData := win32.NOTIFYICONDATA{} - iconData.CbSize = uint32(unsafe.Sizeof(iconData)) - iconData.UFlags = win32.NIF_GUID | win32.NIF_MESSAGE - iconData.HWnd = w.hwnd - iconData.GUIDItem = *w.trayGUID - iconData.UFlags = win32.NIF_GUID | win32.NIF_ICON - var err error - iconData.HIcon, err = win32.LoadImage( + defer os.Remove(iconPath) + err = encodeIco(f, icon) + if err != nil { + return err + } + f.Close() + + hicon, err := win32.LoadImage( 0, windows.StringToUTF16Ptr(iconPath), win32.IMAGE_ICON, 0, 0, win32.LR_DEFAULTSIZE|win32.LR_LOADFROMFILE) if err != nil { - return fmt.Errorf("failed to load icon: %w", err) - } - if !win32.Shell_NotifyIcon(win32.NIM_MODIFY, &iconData) { - return fmt.Errorf("failed to create notification icon") - } - return nil -} - -func (w *windowImpl) ShowNotification(title, msg string, icon bool) error { - if w.trayGUID == nil { - if err := w.createTrayItem(); err != nil { - return err + if isWindowsSuccessError(err) { + return fmt.Errorf("failed to reload image") } - } - iconData := win32.NOTIFYICONDATA{} - iconData.CbSize = uint32(unsafe.Sizeof(iconData)) - iconData.UFlags = win32.NIF_GUID | win32.NIF_INFO - iconData.HWnd = w.hwnd - iconData.GUIDItem = *w.trayGUID - copy(iconData.SzInfoTitle[:], windows.StringToUTF16(title)) - copy(iconData.SzInfo[:], windows.StringToUTF16(msg)) - if icon { - iconData.DwInfoFlags = win32.NIIF_USER | win32.NIIF_LARGE_ICON + return fmt.Errorf("failed to reload image: %v", err) } - if !win32.Shell_NotifyIcon(win32.NIM_MODIFY, &iconData) { - return fmt.Errorf("failed to create notification icon") - } + win32.SendMessage(w.hwnd, win32.WM_SETICON, win32.ICON_SMALL, hicon) + win32.SendMessage(w.hwnd, win32.WM_SETICON, win32.ICON_BIG, hicon) return nil } -func (w *windowImpl) createTrayItem() error { - w.trayGUID = new(win32.GUID) - *w.trayGUID = win32.MakeGUID(w.guid) - iconData := win32.NOTIFYICONDATA{} - iconData.CbSize = uint32(unsafe.Sizeof(iconData)) - iconData.UFlags = win32.NIF_GUID | win32.NIF_MESSAGE - iconData.HWnd = w.hwnd - iconData.GUIDItem = *w.trayGUID - iconData.UCallbackMessage = win32.WM_APP + 1 - if !win32.Shell_NotifyIcon(win32.NIM_ADD, &iconData) { - return fmt.Errorf("failed to create notification") +func isWindowsSuccessError(err error) bool { + var errno syscall.Errno + if errors.As(err, &errno) { + if errno == 0 { + // we got a confusing 'this operation completed successfully' + // no, this does not actually mean the operation necessarily succeeded + // no, win32.GetLastError will not necessarily return a real error to clarify things + return true + } } - return nil + return false } -func (w *windowImpl) MoveWindow(x, y, wd, ht int32) error { - return win32.MoveWindow(w.hwnd, x, y, wd, ht, true) +func (w *Window) MoveWindow(x, y, wd, ht int) error { + return win32.MoveWindow(w.hwnd, int32(x), int32(y), int32(wd), int32(ht), true) } func drawWindow(dc win32.HDC, src2dst f64.Aff3, src interface{}, sr image.Rectangle, op draw.Op) (retErr error) { @@ -377,18 +356,11 @@ func drawWindow(dc win32.HDC, src2dst f64.Aff3, src interface{}, sr image.Rectan return fmt.Errorf("unsupported type %T", src) } -func (w *windowImpl) Copy(dp image.Point, src screen.Texture, sr image.Rectangle, op draw.Op) { - drawer.Copy(w, dp, src, sr, op) -} - -func (w *windowImpl) Scale(dr image.Rectangle, src screen.Texture, sr image.Rectangle, op draw.Op) { +func (w *Window) Scale(dr image.Rectangle, src screen.Texture, sr image.Rectangle, op draw.Op) { drawer.Scale(w, dr, src, sr, op) } -func (w *windowImpl) Publish() screen.PublishResult { - // TODO - return screen.PublishResult{} -} +func (w *Window) Publish() {} func init() { send := func(hwnd win32.HWND, e interface{}) { @@ -462,7 +434,7 @@ const ( // msgCmd is the stored value for our handleCmd function for syscalls. var msgCmd = win32.AddWindowMsg(handleCmd) -func (w *windowImpl) execCmd(c *cmd) { +func (w *Window) execCmd(c *cmd) { win32.SendMessage(win32.HWND(w.hwnd), msgCmd, 0, uintptr(unsafe.Pointer(c))) if c.err != nil { panic(fmt.Sprintf("execCmd faild for cmd.id=%d: %v", c.id, c.err)) // TODO handle errors @@ -495,8 +467,37 @@ func handleCmd(hwnd win32.HWND, uMsg uint32, wParam, lParam uintptr) { } } -func (w *windowImpl) GetCursorPosition() (x, y float64) { +func (w *Window) GetCursorPosition() (x, y float64) { + w.changeLock.RLock() + defer w.changeLock.RUnlock() + w.windowRect, _ = win32.GetWindowRect(w.hwnd) xint, yint, _ := win32.GetCursorPos() return float64(xint) - float64(w.windowRect.Left), float64(yint) - float64(w.windowRect.Top) } + +func (w *Window) SetTopMost(topMost bool) error { + w.changeLock.Lock() + defer w.changeLock.Unlock() + + if w.topMost == topMost { + return nil + } + + // Note: although you can change a window's ex style to include EX_TOPMOST + // this will not work after window creation. The following is what you need to + // do instead. + + var ok bool + if topMost { + ok = win32.SetWindowPos(w.hwnd, win32.HWND_TOPMOST, 0, 0, 0, 0, win32.SWP_NOMOVE|win32.SWP_NOSIZE) + } else { + ok = win32.SetWindowPos(w.hwnd, win32.HWND_NOTOPMOST, 0, 0, 0, 0, win32.SWP_NOMOVE|win32.SWP_NOSIZE) + } + if !ok { + // TODO: extract and parse os error + return fmt.Errorf("failed to set top most") + } + w.topMost = topMost + return nil +} diff --git a/shiny/driver/windriver/windraw.go b/shiny/driver/windriver/windraw.go index a412bf7b..8d7f1ca2 100644 --- a/shiny/driver/windriver/windraw.go +++ b/shiny/driver/windriver/windraw.go @@ -2,6 +2,7 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. +//go:build windows // +build windows package windriver @@ -14,7 +15,7 @@ import ( "syscall" "unsafe" - "github.com/oakmound/oak/v3/shiny/driver/internal/win32" + "github.com/oakmound/oak/v4/shiny/driver/internal/win32" ) func mkbitmap(size image.Point) (syscall.Handle, *byte, error) { diff --git a/shiny/driver/windriver/windriver.go b/shiny/driver/windriver/windriver.go index 5e29e862..cf339cd9 100644 --- a/shiny/driver/windriver/windriver.go +++ b/shiny/driver/windriver/windriver.go @@ -2,14 +2,15 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. +//go:build windows // +build windows package windriver import ( - "github.com/oakmound/oak/v3/shiny/driver/internal/errscreen" - "github.com/oakmound/oak/v3/shiny/driver/internal/win32" - "github.com/oakmound/oak/v3/shiny/screen" + "github.com/oakmound/oak/v4/shiny/driver/internal/errscreen" + "github.com/oakmound/oak/v4/shiny/driver/internal/win32" + "github.com/oakmound/oak/v4/shiny/screen" ) // Main is called by the program's main function to run the graphical diff --git a/shiny/driver/x11driver/buffer.go b/shiny/driver/x11driver/buffer.go index 603d0003..4cb09e37 100644 --- a/shiny/driver/x11driver/buffer.go +++ b/shiny/driver/x11driver/buffer.go @@ -17,7 +17,7 @@ import ( "github.com/BurntSushi/xgb/shm" "github.com/BurntSushi/xgb/xproto" - "github.com/oakmound/oak/v3/shiny/driver/internal/swizzle" + "github.com/oakmound/oak/v4/shiny/driver/internal/swizzle" ) type bufferImpl struct { diff --git a/shiny/driver/x11driver/screen.go b/shiny/driver/x11driver/screen.go index fa15a388..56d5c448 100644 --- a/shiny/driver/x11driver/screen.go +++ b/shiny/driver/x11driver/screen.go @@ -21,8 +21,8 @@ import ( "github.com/BurntSushi/xgb/shm" "github.com/BurntSushi/xgb/xproto" - "github.com/oakmound/oak/v3/shiny/driver/internal/x11key" - "github.com/oakmound/oak/v3/shiny/screen" + "github.com/oakmound/oak/v4/shiny/driver/internal/x11key" + "github.com/oakmound/oak/v4/shiny/screen" "golang.org/x/image/math/f64" "golang.org/x/mobile/event/key" "golang.org/x/mobile/event/mouse" @@ -57,7 +57,7 @@ type screenImpl struct { mu sync.Mutex buffers map[shm.Seg]*bufferImpl uploads map[uint16]chan struct{} - windows map[xproto.Window]*windowImpl + windows map[xproto.Window]*Window nPendingUploads int completionKeys []uint16 } @@ -69,6 +69,7 @@ var ( "WM_DELETE_WINDOW", "WM_PROTOCOLS", "WM_TAKE_FOCUS", + "_NET_WM_ICON", } ) @@ -85,7 +86,7 @@ func newScreenImpl(xutil *xgbutil.XUtil) (s *screenImpl, err error) { xsi: xutil.Setup().DefaultScreen(xutil.Conn()), buffers: map[shm.Seg]*bufferImpl{}, uploads: map[uint16]chan struct{}{}, - windows: map[xproto.Window]*windowImpl{}, + windows: map[xproto.Window]*Window{}, } for _, atom := range initialAtoms { s.atoms[atom], err = xprop.Atm(s.XUtil, atom) @@ -294,7 +295,7 @@ func (s *screenImpl) findBuffer(key shm.Seg) *bufferImpl { return b } -func (s *screenImpl) findWindow(key xproto.Window) *windowImpl { +func (s *screenImpl) findWindow(key xproto.Window) *Window { s.mu.Lock() w := s.windows[key] s.mu.Unlock() @@ -447,7 +448,7 @@ func (s *screenImpl) NewWindow(opts screen.WindowGenerator) (screen.Window, erro pictformat = s.pictformat32 } - w := &windowImpl{ + w := &Window{ s: s, xw: xw, xg: xg, @@ -489,7 +490,7 @@ func (s *screenImpl) NewWindow(opts screen.WindowGenerator) (screen.Window, erro render.CreatePicture(s.xc, xp, xproto.Drawable(xw), pictformat, 0, nil) xproto.MapWindow(s.xc, xw) - err = w.MoveWindow(opts.X, opts.Y, int32(width), int32(height)) + err = w.MoveWindow(opts.X, opts.Y, width, height) if opts.Fullscreen { err = w.SetFullScreen(true) if err != nil { diff --git a/shiny/driver/x11driver/texture.go b/shiny/driver/x11driver/texture.go index 31d52391..5c753a6f 100644 --- a/shiny/driver/x11driver/texture.go +++ b/shiny/driver/x11driver/texture.go @@ -14,7 +14,7 @@ import ( "github.com/BurntSushi/xgb/render" "github.com/BurntSushi/xgb/xproto" - "github.com/oakmound/oak/v3/shiny/screen" + "github.com/oakmound/oak/v4/shiny/screen" "golang.org/x/image/math/f64" ) diff --git a/shiny/driver/x11driver/window.go b/shiny/driver/x11driver/window.go index 84e6c4da..4756e917 100644 --- a/shiny/driver/x11driver/window.go +++ b/shiny/driver/x11driver/window.go @@ -16,12 +16,12 @@ import ( "github.com/BurntSushi/xgb/render" "github.com/BurntSushi/xgb/xproto" - "github.com/oakmound/oak/v3/shiny/driver/internal/drawer" - "github.com/oakmound/oak/v3/shiny/driver/internal/event" - "github.com/oakmound/oak/v3/shiny/driver/internal/lifecycler" - "github.com/oakmound/oak/v3/shiny/driver/internal/x11" - "github.com/oakmound/oak/v3/shiny/driver/internal/x11key" - "github.com/oakmound/oak/v3/shiny/screen" + "github.com/oakmound/oak/v4/shiny/driver/internal/drawer" + "github.com/oakmound/oak/v4/shiny/driver/internal/event" + "github.com/oakmound/oak/v4/shiny/driver/internal/lifecycler" + "github.com/oakmound/oak/v4/shiny/driver/internal/x11" + "github.com/oakmound/oak/v4/shiny/driver/internal/x11key" + "github.com/oakmound/oak/v4/shiny/screen" "golang.org/x/image/math/f64" "golang.org/x/mobile/event/key" "golang.org/x/mobile/event/mouse" @@ -30,7 +30,7 @@ import ( "golang.org/x/mobile/geom" ) -type windowImpl struct { +type Window struct { s *screenImpl xw xproto.Window @@ -48,11 +48,13 @@ type windowImpl struct { mu sync.Mutex + lastMouseX, lastMouseY int16 + x, y uint32 released bool } -func (w *windowImpl) Release() { +func (w *Window) Release() { w.mu.Lock() released := w.released w.released = true @@ -69,31 +71,31 @@ func (w *windowImpl) Release() { xproto.DestroyWindow(w.s.xc, w.xw) } -func (w *windowImpl) Upload(dp image.Point, src screen.Image, sr image.Rectangle) { +func (w *Window) Upload(dp image.Point, src screen.Image, sr image.Rectangle) { src.(*bufferImpl).upload(xproto.Drawable(w.xw), w.xg, w.s.xsi.RootDepth, dp, sr) } -func (w *windowImpl) Fill(dr image.Rectangle, src color.Color, op draw.Op) { +func (w *Window) Fill(dr image.Rectangle, src color.Color, op draw.Op) { fill(w.s.xc, w.xp, dr, src, op) } -func (w *windowImpl) DrawUniform(src2dst f64.Aff3, src color.Color, sr image.Rectangle, op draw.Op) { +func (w *Window) DrawUniform(src2dst f64.Aff3, src color.Color, sr image.Rectangle, op draw.Op) { w.s.drawUniform(w.xp, &src2dst, src, sr, op) } -func (w *windowImpl) Draw(src2dst f64.Aff3, src screen.Texture, sr image.Rectangle, op draw.Op) { +func (w *Window) Draw(src2dst f64.Aff3, src screen.Texture, sr image.Rectangle, op draw.Op) { src.(*textureImpl).draw(w.xp, &src2dst, sr, op) } -func (w *windowImpl) Copy(dp image.Point, src screen.Texture, sr image.Rectangle, op draw.Op) { +func (w *Window) Copy(dp image.Point, src screen.Texture, sr image.Rectangle, op draw.Op) { drawer.Copy(w, dp, src, sr, op) } -func (w *windowImpl) Scale(dr image.Rectangle, src screen.Texture, sr image.Rectangle, op draw.Op) { +func (w *Window) Scale(dr image.Rectangle, src screen.Texture, sr image.Rectangle, op draw.Op) { drawer.Scale(w, dr, src, sr, op) } -func (w *windowImpl) Publish() screen.PublishResult { +func (w *Window) Publish() { // TODO: implement a back buffer, and copy or flip that here to the front // buffer. @@ -105,19 +107,17 @@ func (w *windowImpl) Publish() screen.PublishResult { // client could easily end up sending work at a faster rate than the X11 // server can serve. w.s.xc.Sync() - - return screen.PublishResult{} } -func (w *windowImpl) SetFullScreen(fullscreen bool) error { +func (w *Window) SetFullScreen(fullscreen bool) error { return x11.SetFullScreen(w.s.XUtil, w.xw, fullscreen) } -func (w *windowImpl) SetBorderless(borderless bool) error { +func (w *Window) SetBorderless(borderless bool) error { return x11.SetBorderless(w.s.XUtil, w.xw, borderless) } -func (w *windowImpl) handleConfigureNotify(ev xproto.ConfigureNotifyEvent) { +func (w *Window) handleConfigureNotify(ev xproto.ConfigureNotifyEvent) { // TODO: does the order of these lifecycle and size events matter? Should // they really be a single, atomic event? w.lifecycler.SetVisible((int(ev.X)+int(ev.Width)) > 0 && (int(ev.Y)+int(ev.Height)) > 0) @@ -137,11 +137,11 @@ func (w *windowImpl) handleConfigureNotify(ev xproto.ConfigureNotifyEvent) { }) } -func (w *windowImpl) handleExpose() { +func (w *Window) handleExpose() { w.Send(paint.Event{}) } -func (w *windowImpl) handleKey(detail xproto.Keycode, state uint16, dir key.Direction) { +func (w *Window) handleKey(detail xproto.Keycode, state uint16, dir key.Direction) { r, c := w.s.keysyms.Lookup(uint8(detail), state, w.s.numLockMod) w.Send(key.Event{ Rune: r, @@ -151,7 +151,9 @@ func (w *windowImpl) handleKey(detail xproto.Keycode, state uint16, dir key.Dire }) } -func (w *windowImpl) handleMouse(x, y int16, b xproto.Button, state uint16, dir mouse.Direction) { +func (w *Window) handleMouse(x, y int16, b xproto.Button, state uint16, dir mouse.Direction) { + w.lastMouseX = x + w.lastMouseY = y // TODO: should a mouse.Event have a separate MouseModifiers field, for // which buttons are pressed during a mouse move? btn := mouse.Button(b) @@ -180,7 +182,7 @@ func (w *windowImpl) handleMouse(x, y int16, b xproto.Button, state uint16, dir }) } -func (w *windowImpl) MoveWindow(x, y, width, height int32) error { +func (w *Window) MoveWindow(x, y, width, height int) error { newX, newY, newW, newH := x11.MoveWindow(w.s.xc, w.xw, x, y, width, height) w.x = uint32(newX) w.y = uint32(newY) @@ -195,3 +197,95 @@ func (w *windowImpl) MoveWindow(x, y, width, height int32) error { }) return nil } + +func (w *Window) SetTitle(title string) error { + xproto.ChangeProperty(w.s.xc, xproto.PropModeReplace, w.xw, + w.s.atoms["_NET_WM_NAME"], w.s.atoms["UTF8_STRING"], + 8, uint32(len(title)), []byte(title)) + return nil +} + +func (w *Window) SetTopMost(topMost bool) error { + return x11.SetTopMost(w.s.XUtil, w.xw, topMost) +} + +func (w *Window) HideCursor() error { + // ask X for a pixmap id + px, err := xproto.NewPixmapId(w.s.xc) + if err != nil { + return err + } + + // Create a 1x1 pixmap with that pixmap id + // depth has to be 1, otherwise you get BadMatch + // the drawable has to be this root window. I don't know why. + // You can't make a pixmap with less than 1x1 dimensions. + // I don't even know if this pixmap is black or transparent + xproto.CreatePixmap(w.s.xc, 1, px, xproto.Drawable(w.s.XUtil.RootWin()), 1, 1) + + // ask X for a cursor id + cursorId, err := xproto.NewCursorId(w.s.xc) + if err != nil { + return err + } + + // create a cursor from the pixmap with that cursor id. + // the zeros are colors (r,g,b,r,g,b) and the hotspot of the cursor (x,y) + // the second px is a mask which we ignore. + xproto.CreateCursor(w.s.xc, cursorId, px, px, 0, 0, 0, 0, 0, 0, 0, 0) + + // change the cursor of the window to be the created cursor. + xproto.ChangeWindowAttributes(w.s.xc, w.xw, + xproto.CwBackPixel|xproto.CwCursor, []uint32{0xffffffff, uint32(cursorId)}) + + // free the things we created + // it does not make sense to me that we can free these, and still persist our created + // cursor, but it works + xproto.FreeCursor(w.s.xc, cursorId) + xproto.FreePixmap(w.s.xc, px) + + return nil +} + +func (w *Window) SetIcon(icon image.Image) error { + bds := icon.Bounds() + wd := bds.Max.X + h := bds.Max.Y + u32w := uint32(wd) + u32h := uint32(h) + // 4 bytes, b/g/r/a, per pixel + bgra := make([]byte, 8, 8+wd*h*4) + // prepend width and height + bgra[0] = byte(u32w) + bgra[1] = byte(u32w >> 8) + bgra[2] = byte(u32w >> 16) + bgra[3] = byte(u32w >> 24) + bgra[4] = byte(u32h) + bgra[5] = byte(u32h >> 8) + bgra[6] = byte(u32h >> 16) + bgra[7] = byte(u32h >> 24) + for x := 0; x < wd; x++ { + for y := 0; y < h; y++ { + c := icon.At(x, (h-1)-y) + r, g, b, a := c.RGBA() + bgra = append(bgra, byte(b>>8)) + bgra = append(bgra, byte(g>>8)) + bgra = append(bgra, byte(r>>8)) + bgra = append(bgra, byte(a>>8)) + } + } + const XA_CARDINAL = 6 + // 32 here is the bit size of a cardinal, which is a bgra pixel + // we divide our length by 4 because we're sending a byte slice, + // not a cardinal slice + xproto.ChangeProperty(w.s.xc, xproto.PropModeReplace, w.xw, + w.s.atoms["_NET_WM_ICON"], XA_CARDINAL, + 32, uint32(len(bgra))/4, bgra) + return nil +} + +func (w *Window) GetCursorPosition() (x, y float64) { + // it's really not easy to do this with X + // we're just caching the last values we got + return float64(w.lastMouseX), float64(w.lastMouseY) +} diff --git a/shiny/driver/x11driver/x11driver.go b/shiny/driver/x11driver/x11driver.go index fa1f63db..ac7614f9 100644 --- a/shiny/driver/x11driver/x11driver.go +++ b/shiny/driver/x11driver/x11driver.go @@ -19,8 +19,8 @@ import ( "github.com/BurntSushi/xgbutil" "github.com/BurntSushi/xgbutil/xevent" - "github.com/oakmound/oak/v3/shiny/driver/internal/errscreen" - "github.com/oakmound/oak/v3/shiny/screen" + "github.com/oakmound/oak/v4/shiny/driver/internal/errscreen" + "github.com/oakmound/oak/v4/shiny/screen" ) // Main is called by the program's main function to run the graphical diff --git a/shiny/gesture/gesture.go b/shiny/gesture/gesture.go deleted file mode 100644 index fb441d73..00000000 --- a/shiny/gesture/gesture.go +++ /dev/null @@ -1,326 +0,0 @@ -// Copyright 2016 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -// Package gesture provides gesture events such as long presses and drags. -// These are higher level than underlying mouse and touch events. -package gesture - -import ( - "fmt" - "time" - - "github.com/oakmound/oak/v3/shiny/screen" - "golang.org/x/mobile/event/mouse" -) - -// TODO: handle touch events, not just mouse events. -// -// TODO: multi-button / multi-touch gestures such as pinch, rotate and tilt? - -const ( - // TODO: use a resolution-independent unit such as DIPs or Millimetres? - dragThreshold = 10 // Pixels. - - doublePressThreshold = 300 * time.Millisecond - longPressThreshold = 500 * time.Millisecond -) - -// Type describes the type of a touch event. -type Type uint8 - -const ( - // TypeStart and TypeEnd are the start and end of a gesture. A gesture - // spans multiple events. - TypeStart Type = 0 - TypeEnd Type = 1 - - // TypeIsXxx is when the gesture is recognized as a long press, double - // press or drag. For example, a mouse button press won't generate a - // TypeIsLongPress immediately, but if a threshold duration passes without - // the corresponding mouse button release, a TypeIsLongPress event is sent. - // - // Once a TypeIsXxx event is sent, the corresponding Event.Xxx bool field - // is set for this and subsequent events. For example, a TypeTap event by - // itself doesn't say whether or not it is a single tap or the first tap of - // a double tap. If the app needs to distinguish these two sorts of taps, - // it can wait until a TypeEnd or TypeIsDoublePress event is seen. If a - // TypeEnd is seen before TypeIsDoublePress, or equivalently, if the - // TypeEnd event's DoublePress field is false, the gesture is a single tap. - // - // These attributes aren't exclusive. A long press drag is perfectly valid. - // - // The uncommon "double press" instead of "double tap" terminology is - // because, in this package, taps are associated with button releases, not - // button presses. Note also that "double" really means "at least two". - TypeIsLongPress Type = 10 - TypeIsDoublePress Type = 11 - TypeIsDrag Type = 12 - - // TypeTap and TypeDrag are tap and drag events. - // - // For 'flinging' drags, to simulate inertia, look to the Velocity field of - // the TypeEnd event. - // - // TODO: implement velocity. - TypeTap Type = 20 - TypeDrag Type = 21 - - // All internal types are >= typeInternal. - typeInternal Type = 100 - - // The typeXxxSchedule and typeXxxResolve constants are used for the two - // step process for sending an event after a timeout, in a separate - // goroutine. There are two steps so that the spawned goroutine is - // guaranteed to execute only after any other EventDeque.SendFirst calls - // are made for the one underlying mouse or touch event. - - typeDoublePressSchedule Type = 100 - typeDoublePressResolve Type = 101 - - typeLongPressSchedule Type = 110 - typeLongPressResolve Type = 111 -) - -func (t Type) String() string { - switch t { - case TypeStart: - return "Start" - case TypeEnd: - return "End" - case TypeIsLongPress: - return "IsLongPress" - case TypeIsDoublePress: - return "IsDoublePress" - case TypeIsDrag: - return "IsDrag" - case TypeTap: - return "Tap" - case TypeDrag: - return "Drag" - default: - return fmt.Sprintf("gesture.Type(%d)", t) - } -} - -// Point is a mouse or touch location, in pixels. -type Point struct { - X, Y float32 -} - -// Event is a gesture event. -type Event struct { - // Type is the gesture type. - Type Type - - // Drag, LongPress and DoublePress are set when the gesture is recognized as - // a drag, etc. - // - // Note that these status fields can be lost during a gesture's events over - // time: LongPress can be set for the first press of a double press, but - // unset on the second press. - Drag bool - LongPress bool - DoublePress bool - - // InitialPos is the initial position of the button press or touch that - // started this gesture. - InitialPos Point - - // CurrentPos is the current position of the button or touch event. - CurrentPos Point - - // TODO: a "Velocity Point" field. See - // - frameworks/native/libs/input/VelocityTracker.cpp in AOSP, or - // - https://chromium.googlesource.com/chromium/src/+/master/ui/events/gesture_detection/velocity_tracker.cc in Chromium, - // for some velocity tracking implementations. - - // Time is the event's time. - Time time.Time - - // TODO: include the mouse Button and key Modifiers? -} - -type internalEvent struct { - eventFilter *EventFilter - - typ Type - x, y float32 - - // pressCounter is the EventFilter.pressCounter value at the time this - // internal event was scheduled to be delivered after a timeout. It detects - // whether there have been other button presses and releases during that - // timeout, and hence whether this internalEvent is obsolete. - pressCounter uint32 -} - -// EventFilter generates gesture events from lower level mouse and touch -// events. -type EventFilter struct { - EventDeque screen.EventDeque - - inProgress bool - drag bool - longPress bool - doublePress bool - - // initialPos is the initial position of the button press or touch that - // started this gesture. - initialPos Point - - // pressButton is the initial button that started this gesture. If - // button.None, no gesture is in progress. - pressButton mouse.Button - - // pressCounter is incremented on every button press and release. - pressCounter uint32 -} - -func (f *EventFilter) sendFirst(t Type, x, y float32, now time.Time) { - if t >= typeInternal { - f.EventDeque.SendFirst(internalEvent{ - eventFilter: f, - typ: t, - x: x, - y: y, - pressCounter: f.pressCounter, - }) - return - } - f.EventDeque.SendFirst(Event{ - Type: t, - Drag: f.drag, - LongPress: f.longPress, - DoublePress: f.doublePress, - InitialPos: f.initialPos, - CurrentPos: Point{ - X: x, - Y: y, - }, - // TODO: Velocity. - Time: now, - }) -} - -func (f *EventFilter) sendAfter(e internalEvent, sleep time.Duration) { - time.Sleep(sleep) - f.EventDeque.SendFirst(e) -} - -func (f *EventFilter) end(x, y float32, now time.Time) { - f.sendFirst(TypeEnd, x, y, now) - f.inProgress = false - f.drag = false - f.longPress = false - f.doublePress = false - f.initialPos = Point{} - f.pressButton = mouse.ButtonNone -} - -// Filter filters the event. It can return e, a different event, or nil to -// consume the event. It can also trigger side effects such as pushing new -// events onto its EventDeque. -func (f *EventFilter) Filter(e interface{}) interface{} { - switch e := e.(type) { - case internalEvent: - if e.eventFilter != f { - break - } - - now := time.Now() - - switch e.typ { - case typeDoublePressSchedule: - e.typ = typeDoublePressResolve - go f.sendAfter(e, doublePressThreshold) - - case typeDoublePressResolve: - if e.pressCounter == f.pressCounter { - // It's a single press only. - f.end(e.x, e.y, now) - } - - case typeLongPressSchedule: - e.typ = typeLongPressResolve - go f.sendAfter(e, longPressThreshold) - - case typeLongPressResolve: - if e.pressCounter == f.pressCounter && !f.drag { - f.longPress = true - f.sendFirst(TypeIsLongPress, e.x, e.y, now) - } - } - return nil - - case mouse.Event: - now := time.Now() - - switch e.Direction { - case mouse.DirNone: - if f.pressButton == mouse.ButtonNone { - break - } - startDrag := false - if !f.drag && - (abs(e.X-f.initialPos.X) > dragThreshold || abs(e.Y-f.initialPos.Y) > dragThreshold) { - f.drag = true - startDrag = true - } - if f.drag { - f.sendFirst(TypeDrag, e.X, e.Y, now) - } - if startDrag { - f.sendFirst(TypeIsDrag, e.X, e.Y, now) - } - - case mouse.DirPress: - if f.pressButton != mouse.ButtonNone { - break - } - - oldInProgress := f.inProgress - oldDoublePress := f.doublePress - - f.drag = false - f.longPress = false - f.doublePress = f.inProgress - f.initialPos = Point{e.X, e.Y} - f.pressButton = e.Button - f.pressCounter++ - - f.inProgress = true - - f.sendFirst(typeLongPressSchedule, e.X, e.Y, now) - if !oldDoublePress && f.doublePress { - f.sendFirst(TypeIsDoublePress, e.X, e.Y, now) - } - if !oldInProgress { - f.sendFirst(TypeStart, e.X, e.Y, now) - } - - case mouse.DirRelease: - if f.pressButton != e.Button { - break - } - f.pressButton = mouse.ButtonNone - f.pressCounter++ - - if f.drag { - f.end(e.X, e.Y, now) - break - } - f.sendFirst(typeDoublePressSchedule, e.X, e.Y, now) - f.sendFirst(TypeTap, e.X, e.Y, now) - } - } - return e -} - -func abs(x float32) float32 { - if x < 0 { - return -x - } else if x == 0 { - return 0 // Handle floating point negative zero. - } - return x -} diff --git a/shiny/screen/event.go b/shiny/screen/event.go index 76d944fb..ef44685d 100644 --- a/shiny/screen/event.go +++ b/shiny/screen/event.go @@ -6,10 +6,6 @@ type EventDeque interface { // NextEvent in FIFO order. Send(event interface{}) - // SendFirst adds an event to the start of the deque. They are returned by - // NextEvent in LIFO order, and have priority over events sent via Send. - SendFirst(event interface{}) - // NextEvent returns the next event in the deque. It blocks until such an // event has been sent. // @@ -21,6 +17,6 @@ type EventDeque interface { // - mouse.Event // - touch.Event // from the golang.org/x/mobile/event/... packages. Other packages may send - // events, of those types above or of other types, via Send or SendFirst. + // events, of those types above or of other types, via Send. NextEvent() interface{} } diff --git a/shiny/screen/options.go b/shiny/screen/options.go index 4702241b..47ba2603 100644 --- a/shiny/screen/options.go +++ b/shiny/screen/options.go @@ -30,7 +30,7 @@ type WindowGenerator struct { // X and Y determine the location the new window should be created at. If // either are zero, a driver-dependant default will be used for each zero // value. If Fullscreen is true, these values will be ignored. - X, Y int32 + X, Y int } // A WindowOption is any function that sets up a WindowGenerator. @@ -54,7 +54,7 @@ func Dimensions(w, h int) WindowOption { } // Position sets the starting position of the new window -func Position(x, y int32) WindowOption { +func Position(x, y int) WindowOption { return func(g *WindowGenerator) { g.X = x g.Y = y diff --git a/shiny/screen/screen.go b/shiny/screen/screen.go index 248bc40f..417bfe47 100644 --- a/shiny/screen/screen.go +++ b/shiny/screen/screen.go @@ -15,8 +15,8 @@ // package main // // import ( -// "github.com/oakmound/oak/v3/shiny/driver" -// "github.com/oakmound/oak/v3/shiny/screen" +// "github.com/oakmound/oak/v4/shiny/driver" +// "github.com/oakmound/oak/v4/shiny/screen" // "golang.org/x/mobile/event/lifecycle" // ) // @@ -54,6 +54,9 @@ package screen import ( "image" + "image/draw" + + "golang.org/x/image/math/f64" ) // Screen creates Images, Textures and Windows. @@ -65,8 +68,6 @@ type Screen interface { NewTexture(size image.Point) (Texture, error) // NewWindow returns a new Window for this screen. - // - // A nil opts is valid and means to use the default option values. NewWindow(opts WindowGenerator) (Window, error) } @@ -80,16 +81,44 @@ type Window interface { EventDeque - Drawer + // Scale scales the sub-Texture defined by src and sr to the destination + // (the method receiver), such that sr in src-space is mapped to dr in + // dst-space. + Scale(dr image.Rectangle, src Texture, sr image.Rectangle, op draw.Op) + + // Upload uploads the sub-Buffer defined by src and sr to the destination + // (the method receiver), such that sr.Min in src-space aligns with dp in + // dst-space. The destination's contents are overwritten; the draw operator + // is implicitly draw.Src. + // + // It is valid to upload a Buffer while another upload of the same Buffer + // is in progress, but a Buffer's image.RGBA pixel contents should not be + // accessed while it is uploading. A Buffer is re-usable, in that its pixel + // contents can be further modified, once all outstanding calls to Upload + // have returned. + // + // TODO: make it optional that a Buffer's contents is preserved after + // Upload? Undoing a swizzle is a non-trivial amount of work, and can be + // redundant if the next paint cycle starts by clearing the buffer. + // + // When uploading to a Window, there will not be any visible effect until + // Publish is called. + Upload(dp image.Point, src Image, sr image.Rectangle) // Publish flushes any pending Upload and Draw calls to the window, and // swaps the back buffer to the front. - Publish() PublishResult + Publish() } -// PublishResult is the result of an Window.Publish call. -type PublishResult struct { - // BackBufferPreserved is whether the contents of the back buffer was - // preserved. If false, the contents are undefined. - BackBufferPreserved bool +type SimpleDrawer interface { + // Draw draws the sub-Texture defined by src and sr to the destination (the + // method receiver). src2dst defines how to transform src coordinates to + // dst coordinates. For example, if src2dst is the matrix + // + // m00 m01 m02 + // m10 m11 m12 + // + // then the src-space point (sx, sy) maps to the dst-space point + // (m00*sx + m01*sy + m02, m10*sx + m11*sy + m12). + Draw(src2dst f64.Aff3, src Texture, sr image.Rectangle, op draw.Op) } diff --git a/shiny/screen/screen_test.go b/shiny/screen/utf_test.go similarity index 100% rename from shiny/screen/screen_test.go rename to shiny/screen/utf_test.go diff --git a/test_coverage.sh b/test_coverage.sh index 06aa1ff3..a0b230ca 100755 --- a/test_coverage.sh +++ b/test_coverage.sh @@ -18,6 +18,11 @@ if [ -f profile.out ]; then cat profile.out >> coverage.txt rm profile.out fi +go test -coverprofile=profile.out -covermode=atomic ./alg/span +if [ -f profile.out ]; then + cat profile.out >> coverage.txt + rm profile.out +fi go test -coverprofile=profile.out -covermode=atomic ./collision if [ -f profile.out ]; then cat profile.out >> coverage.txt diff --git a/test_examples.sh b/test_examples.sh index 8d3bce7a..636211a5 100755 --- a/test_examples.sh +++ b/test_examples.sh @@ -1,6 +1,7 @@ #!/usr/bin/env bash examples=$(find ./examples | grep main.go$) +root=$(pwd) for ex in $examples do echo $ex @@ -12,5 +13,5 @@ do if [ $retVal -ne 124 ]; then exit 1 fi - cd ../.. + cd $root done \ No newline at end of file diff --git a/testdata/default.config b/testdata/default.config index bdab51fe..61b8427d 100644 --- a/testdata/default.config +++ b/testdata/default.config @@ -22,6 +22,5 @@ "language": "English", "title": "Oak Window", "batchLoad": false, - "gestureSupport": false, - "refreshRate": "50ms" + "gestureSupport": false } \ No newline at end of file diff --git a/tidy_all.sh b/tidy_all.sh new file mode 100644 index 00000000..66988be7 --- /dev/null +++ b/tidy_all.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash + +go mod tidy + +cd examples/text +go mod tidy \ No newline at end of file diff --git a/timing/doc.go b/timing/doc.go deleted file mode 100644 index 3d18ba1d..00000000 --- a/timing/doc.go +++ /dev/null @@ -1,2 +0,0 @@ -// Package timing provides utilities for time. -package timing diff --git a/timing/fps.go b/timing/fps.go index 6cc03eb6..8f924f17 100644 --- a/timing/fps.go +++ b/timing/fps.go @@ -1,3 +1,4 @@ +// Package timing provides utilities for time. package timing import ( @@ -16,7 +17,7 @@ const ( func FPS(lastTime, now time.Time) float64 { fps := 1 / now.Sub(lastTime).Seconds() // This indicates that we recorded two times within - // the innacuracy of the OS's system clock, so the values + // the inaccuracy of the OS's system clock, so the values // were the same. 1200 is chosen because on windows, // fps will be 1200 instead of a negative value. if int(fps) < 0 { diff --git a/viewport.go b/viewport.go index 0cf386f6..b405f474 100644 --- a/viewport.go +++ b/viewport.go @@ -1,21 +1,23 @@ package oak import ( - "github.com/oakmound/oak/v3/alg/intgeom" - "github.com/oakmound/oak/v3/event" + "github.com/oakmound/oak/v4/alg/intgeom" + "github.com/oakmound/oak/v4/event" ) -// SetScreen positions the viewport to be at x,y -func (w *Window) SetScreen(x, y int) { - w.setViewport(intgeom.Point2{x, y}) +type Viewport struct { + Position intgeom.Point2 + Bounds intgeom.Rect2 + BoundsEnforced bool } -// ShiftScreen shifts the viewport by x,y -func (w *Window) ShiftScreen(x, y int) { - w.setViewport(w.viewPos.Add(intgeom.Point2{x, y})) +// ShiftViewport shifts the viewport by x,y +func (w *Window) ShiftViewport(delta intgeom.Point2) { + w.SetViewport(w.viewPos.Add(delta)) } -func (w *Window) setViewport(pt intgeom.Point2) { +// SetViewport positions the viewport to be at x,y +func (w *Window) SetViewport(pt intgeom.Point2) { if w.useViewBounds { if w.viewBounds.Min.X() <= pt.X() && w.viewBounds.Max.X() >= pt.X()+w.ScreenWidth { w.viewPos[0] = pt.X() @@ -34,11 +36,14 @@ func (w *Window) setViewport(pt intgeom.Point2) { } else { w.viewPos = pt } - w.eventHandler.Trigger(event.ViewportUpdate, w.viewPos) + event.TriggerOn(w.eventHandler, ViewportUpdate, w.viewPos) } -// GetViewportBounds reports what bounds the viewport has been set to, if any. -func (w *Window) GetViewportBounds() (rect intgeom.Rect2, ok bool) { +// ViewportBounds returns the boundary of this window's viewport, or the rectangle +// that the viewport is not allowed to exit as it moves around. It often represents +// the total size of the world within a given scene. If bounds are not enforced, ok will +// be false. +func (w *Window) ViewportBounds() (rect intgeom.Rect2, ok bool) { return w.viewBounds, w.useViewBounds } @@ -60,20 +65,14 @@ func (w *Window) SetViewportBounds(rect intgeom.Rect2) { w.useViewBounds = true w.viewBounds = rect - newViewX := w.viewPos.X() - newViewY := w.viewPos.Y() - if newViewX < rect.Min[0] { - newViewX = rect.Min[0] - } else if newViewX > rect.Max[0] { - newViewX = rect.Max[0] - } - if newViewY < rect.Min[1] { - newViewY = rect.Min[1] - } else if newViewY > rect.Max[1] { - newViewY = rect.Max[1] + newView := rect.Clamp(w.viewPos) + if newView != w.viewPos { + w.SetViewport(newView) } +} - if newViewX != w.viewPos.X() || newViewY != w.viewPos.Y() { - w.setViewport(intgeom.Point2{newViewX, newViewY}) - } +// Viewport returns the viewport's position. Its width and height are the window's +// width and height. This position plus width/height cannot exceed ViewportBounds. +func (w *Window) Viewport() intgeom.Point2 { + return w.viewPos } diff --git a/viewport_test.go b/viewport_test.go index 96a3d113..7f44f1ed 100644 --- a/viewport_test.go +++ b/viewport_test.go @@ -4,8 +4,8 @@ import ( "testing" "time" - "github.com/oakmound/oak/v3/alg/intgeom" - "github.com/oakmound/oak/v3/scene" + "github.com/oakmound/oak/v4/alg/intgeom" + "github.com/oakmound/oak/v4/scene" ) func sleep() { @@ -21,57 +21,58 @@ func TestViewport(t *testing.T) { } go c1.Init("blank") time.Sleep(2 * time.Second) - if (c1.viewPos) != (intgeom.Point2{0, 0}) { - t.Fatalf("expected %v got %v", c1.viewPos, intgeom.Point2{0, 0}) + if (c1.Viewport()) != (intgeom.Point2{0, 0}) { + t.Fatalf("expected %v got %v", c1.Viewport(), intgeom.Point2{0, 0}) } - c1.SetScreen(5, 5) - if (c1.viewPos) != (intgeom.Point2{5, 5}) { - t.Fatalf("expected %v got %v", c1.viewPos, intgeom.Point2{5, 5}) + c1.SetViewport(intgeom.Point2{5, 5}) + if (c1.Viewport()) != (intgeom.Point2{5, 5}) { + t.Fatalf("expected %v got %v", c1.Viewport(), intgeom.Point2{5, 5}) } - _, ok := c1.GetViewportBounds() + _, ok := c1.ViewportBounds() if ok { t.Fatalf("viewport bounds should not be set on scene start") } c1.SetViewportBounds(intgeom.NewRect2(0, 0, 4, 4)) - if (c1.viewPos) != (intgeom.Point2{5, 5}) { - t.Fatalf("expected %v got %v", c1.viewPos, intgeom.Point2{5, 5}) + if (c1.Viewport()) != (intgeom.Point2{5, 5}) { + t.Fatalf("expected %v got %v", c1.Viewport(), intgeom.Point2{5, 5}) } - c1.SetScreen(-1, -1) - if (c1.viewPos) != (intgeom.Point2{0, 0}) { - t.Fatalf("expected %v got %v", c1.viewPos, intgeom.Point2{0, 0}) + c1.SetViewport(intgeom.Point2{-1, -1}) + if (c1.Viewport()) != (intgeom.Point2{0, 0}) { + t.Fatalf("expected %v got %v", c1.Viewport(), intgeom.Point2{0, 0}) } - c1.SetScreen(6, 6) - if (c1.viewPos) != (intgeom.Point2{0, 0}) { - t.Fatalf("expected %v got %v", c1.viewPos, intgeom.Point2{0, 0}) + c1.SetViewport(intgeom.Point2{6, 6}) + if (c1.Viewport()) != (intgeom.Point2{0, 0}) { + t.Fatalf("expected %v got %v", c1.Viewport(), intgeom.Point2{0, 0}) } c1.SetViewportBounds(intgeom.NewRect2(0, 0, 1000, 1000)) - c1.SetScreen(20, 20) - if (c1.viewPos) != (intgeom.Point2{20, 20}) { - t.Fatalf("expected %v got %v", c1.viewPos, intgeom.Point2{20, 20}) + c1.SetViewport(intgeom.Point2{20, 20}) + if (c1.Viewport()) != (intgeom.Point2{20, 20}) { + t.Fatalf("expected %v got %v", c1.Viewport(), intgeom.Point2{20, 20}) } - c1.ShiftScreen(-1, -1) - if (c1.viewPos) != (intgeom.Point2{19, 19}) { - t.Fatalf("expected %v got %v", c1.viewPos, intgeom.Point2{19, 19}) + c1.ShiftViewport(intgeom.Point2{-1, -1}) + if (c1.Viewport()) != (intgeom.Point2{19, 19}) { + t.Fatalf("expected %v got %v", c1.Viewport(), intgeom.Point2{19, 19}) } c1.SetViewportBounds(intgeom.NewRect2(21, 21, 2000, 2000)) - if (c1.viewPos) != (intgeom.Point2{21, 21}) { - t.Fatalf("expected %v got %v", c1.viewPos, intgeom.Point2{21, 21}) + if (c1.Viewport()) != (intgeom.Point2{21, 21}) { + t.Fatalf("expected %v got %v", c1.Viewport(), intgeom.Point2{21, 21}) } - c1.SetScreen(1000, 1000) + c1.SetViewport(intgeom.Point2{1000, 1000}) c1.SetViewportBounds(intgeom.NewRect2(0, 0, 900, 900)) - bds, ok := c1.GetViewportBounds() + bds, ok := c1.ViewportBounds() if !ok { t.Fatalf("viewport bounds were not enabled") } if bds != intgeom.NewRect2(0, 0, 900, 900) { t.Fatalf("viewport bounds were not set: expected %v got %v", intgeom.NewRect2(0, 0, 900, 900), bds) } - if (c1.viewPos) != (intgeom.Point2{900 - c1.Width(), 900 - c1.Height()}) { - t.Fatalf("expected %v got %v", c1.viewPos, intgeom.Point2{900 - c1.Width(), 900 - c1.Height()}) + mx := intgeom.Point2{900, 900} + if (c1.Viewport()) != mx.Sub(c1.Bounds()) { + t.Fatalf("expected %v got %v", c1.Viewport(), mx.Sub(c1.Bounds())) } c1.RemoveViewportBounds() - _, ok = c1.GetViewportBounds() + _, ok = c1.ViewportBounds() if ok { t.Fatalf("viewport bounds were enabled after clear") } @@ -81,11 +82,11 @@ func TestViewport(t *testing.T) { sleep() - if (c1.viewPos) != (intgeom.Point2{0, 0}) { - t.Fatalf("expected %v got %v", c1.viewPos, intgeom.Point2{0, 0}) + if (c1.Viewport()) != (intgeom.Point2{0, 0}) { + t.Fatalf("expected %v got %v", c1.Viewport(), intgeom.Point2{0, 0}) } - _, ok = c1.GetViewportBounds() + _, ok = c1.ViewportBounds() if ok { t.Fatalf("viewport bounds should not be set on scene start") } diff --git a/window.go b/window.go index 93c38069..6df3db61 100644 --- a/window.go +++ b/window.go @@ -1,3 +1,15 @@ +// Package oak is a game engine. It provides scene control, control over windows +// and what is drawn to them, propagates regular events to evaluate game logic, +// and so on. +// +// A minimal oak app follows: +// +// func main() { +// oak.AddScene("myApp", scene.Scene{Start: func(ctx *scene.Context) { +// // ... ctx.Draw(...), event.Bind(ctx, ...) +// }}) +// oak.Init("myApp") +// } package oak import ( @@ -8,23 +20,23 @@ import ( "sync/atomic" "time" - "github.com/oakmound/oak/v3/alg/intgeom" - "github.com/oakmound/oak/v3/collision" - "github.com/oakmound/oak/v3/debugstream" - "github.com/oakmound/oak/v3/event" - "github.com/oakmound/oak/v3/key" - "github.com/oakmound/oak/v3/mouse" - "github.com/oakmound/oak/v3/render" - "github.com/oakmound/oak/v3/scene" - "github.com/oakmound/oak/v3/shiny/driver" - "github.com/oakmound/oak/v3/shiny/screen" - "github.com/oakmound/oak/v3/window" + "github.com/oakmound/oak/v4/alg/intgeom" + "github.com/oakmound/oak/v4/collision" + "github.com/oakmound/oak/v4/debugstream" + "github.com/oakmound/oak/v4/event" + "github.com/oakmound/oak/v4/key" + "github.com/oakmound/oak/v4/mouse" + "github.com/oakmound/oak/v4/render" + "github.com/oakmound/oak/v4/scene" + "github.com/oakmound/oak/v4/shiny/driver" + "github.com/oakmound/oak/v4/shiny/screen" + "github.com/oakmound/oak/v4/window" ) -var _ window.Window = &Window{} +var _ window.App = &Window{} -func (w *Window) windowController(s screen.Screen, x, y int32, width, height int) (screen.Window, error) { - return s.NewWindow(screen.NewWindowGenerator( +func (w *Window) windowController(s screen.Screen, x, y, width, height int) (*driver.Window, error) { + dwin, err := s.NewWindow(screen.NewWindowGenerator( screen.Dimensions(width, height), screen.Title(w.config.Title), screen.Position(x, y), @@ -32,22 +44,23 @@ func (w *Window) windowController(s screen.Screen, x, y int32, width, height int screen.Borderless(w.config.Borderless), screen.TopMost(w.config.TopMost), )) + return dwin.(*driver.Window), err } // the number of rgba buffers oak's draw loop swaps between const bufferCount = 2 type Window struct { + // The keyboard state this window is aware of. key.State + // the driver.Window embedded in this window exposes at compile time the OS level + // options one has to manipulate this. + *driver.Window + // TODO: most of these channels are not closed cleanly transitionCh chan struct{} - // The Scene channel receives a signal - // when a scene's .loop() function should - // be called. - sceneCh chan struct{} - // The skip scene channel receives a debug // signal to forcibly go to the next // scene. @@ -61,6 +74,8 @@ type Window struct { // drawing should cease (or resume) drawCh chan struct{} + // The between draw channel receives a signal when + // a function is provided to Window.DoBetweenDraws. betweenDrawCh chan func() // ScreenWidth is the width of the screen @@ -84,9 +99,9 @@ type Window struct { // The window buffer represents the subsection of the world which is available to // be shown in a window. - winBuffers [bufferCount]screen.Image - screenControl screen.Screen - windowControl screen.Window + winBuffers [bufferCount]screen.Image + screenControl screen.Screen + windowTextures [bufferCount]screen.Texture bufferIdx uint8 @@ -114,8 +129,8 @@ type Window struct { // Driver is the driver oak will call during initialization Driver Driver - // prePublish is a function called each draw frame prior to - prePublish func(w *Window, tx screen.Texture) + // prePublish is a function called each draw frame prior to publishing frames to the OS + prePublish func(*image.RGBA) // LoadingR is a renderable that is displayed during loading screens. LoadingR render.Renderable @@ -146,20 +161,16 @@ type Window struct { FirstSceneInput interface{} - commands map[string]func([]string) - ControllerID int32 config Config - mostRecentInput InputType + mostRecentInput int32 exitError error ParentContext context.Context - TrackMouseClicks bool - startupLoading bool - useViewBounds bool + useViewBounds bool // UseAspectRatio determines whether new window changes will distort or // maintain the relative width to height ratio of the screen buffer. UseAspectRatio bool @@ -173,84 +184,78 @@ var ( // NewWindow creates a window with default settings. func NewWindow() *Window { - c := &Window{ + return &Window{ State: key.NewState(), transitionCh: make(chan struct{}), - sceneCh: make(chan struct{}), skipSceneCh: make(chan string), quitCh: make(chan struct{}), drawCh: make(chan struct{}), betweenDrawCh: make(chan func()), + SceneMap: scene.NewMap(), + Driver: driver.Main, + prePublish: func(*image.RGBA) {}, + bkgFn: func() image.Image { + return image.Black + }, + eventHandler: event.DefaultBus, + MouseTree: mouse.DefaultTree, + CollisionTree: collision.DefaultTree, + CallerMap: event.DefaultCallerMap, + DrawStack: render.GlobalDrawStack, + ControllerID: atomic.AddInt32(nextControllerID, 1), + ParentContext: context.Background(), } - - c.SceneMap = scene.NewMap() - c.Driver = driver.Main - c.prePublish = func(*Window, screen.Texture) {} - c.bkgFn = func() image.Image { - return image.Black - } - c.eventHandler = event.DefaultBus - c.MouseTree = mouse.DefaultTree - c.CollisionTree = collision.DefaultTree - c.CallerMap = event.DefaultCallerMap - c.DrawStack = render.GlobalDrawStack - c.TrackMouseClicks = true - c.commands = make(map[string]func([]string)) - c.ControllerID = atomic.AddInt32(nextControllerID, 1) - c.ParentContext = context.Background() - return c } // Propagate triggers direct mouse events on entities which are clicked -func (w *Window) Propagate(eventName string, me mouse.Event) { +func (w *Window) Propagate(ev event.EventID[*mouse.Event], me mouse.Event) { hits := w.MouseTree.SearchIntersect(me.ToSpace().Bounds()) sort.Slice(hits, func(i, j int) bool { - return hits[i].Location.Min.Z() < hits[i].Location.Max.Z() + return hits[i].Location.Min.Z() > hits[j].Location.Max.Z() }) for _, sp := range hits { - <-sp.CID.TriggerBus(eventName, &me, w.eventHandler) + <-event.TriggerForCallerOn(w.eventHandler, sp.CID, ev, &me) if me.StopPropagation { break } } me.StopPropagation = false - if w.TrackMouseClicks { - if eventName == mouse.PressOn+"Relative" { - w.lastRelativePress = me - } else if eventName == mouse.PressOn { - w.LastMousePress = me - } else if eventName == mouse.ReleaseOn { - if me.Button == w.LastMousePress.Button { - pressHits := w.MouseTree.SearchIntersect(w.LastMousePress.ToSpace().Bounds()) - sort.Slice(pressHits, func(i, j int) bool { - return pressHits[i].Location.Min.Z() < pressHits[i].Location.Max.Z() - }) - for _, sp1 := range pressHits { - for _, sp2 := range hits { - if sp1.CID == sp2.CID { - w.eventHandler.Trigger(mouse.Click, &me) - <-sp1.CID.TriggerBus(mouse.ClickOn, &me, w.eventHandler) - if me.StopPropagation { - return - } + if ev == mouse.RelativePressOn { + w.lastRelativePress = me + } else if ev == mouse.PressOn { + w.LastMousePress = me + } else if ev == mouse.ReleaseOn { + if me.Button == w.LastMousePress.Button { + event.TriggerOn(w.eventHandler, mouse.Click, &me) + + pressHits := w.MouseTree.SearchIntersect(w.LastMousePress.ToSpace().Bounds()) + sort.Slice(pressHits, func(i, j int) bool { + return pressHits[i].Location.Min.Z() > pressHits[j].Location.Max.Z() + }) + for _, sp1 := range pressHits { + for _, sp2 := range hits { + if sp1.CID == sp2.CID { + <-event.TriggerForCallerOn(w.eventHandler, sp1.CID, mouse.ClickOn, &me) + if me.StopPropagation { + return } } } } - } else if eventName == mouse.ReleaseOn+"Relative" { - if me.Button == w.lastRelativePress.Button { - pressHits := w.MouseTree.SearchIntersect(w.lastRelativePress.ToSpace().Bounds()) - sort.Slice(pressHits, func(i, j int) bool { - return pressHits[i].Location.Min.Z() < pressHits[i].Location.Max.Z() - }) - for _, sp1 := range pressHits { - for _, sp2 := range hits { - if sp1.CID == sp2.CID { - sp1.CID.Trigger(mouse.ClickOn+"Relative", &me) - if me.StopPropagation { - return - } + } + } else if ev == mouse.RelativeReleaseOn { + if me.Button == w.lastRelativePress.Button { + pressHits := w.MouseTree.SearchIntersect(w.lastRelativePress.ToSpace().Bounds()) + sort.Slice(pressHits, func(i, j int) bool { + return pressHits[i].Location.Min.Z() > pressHits[j].Location.Max.Z() + }) + for _, sp1 := range pressHits { + for _, sp2 := range hits { + if sp1.CID == sp2.CID { + <-event.TriggerForCallerOn(w.eventHandler, sp1.CID, mouse.RelativeClickOn, &me) + if me.StopPropagation { + return } } } @@ -259,27 +264,10 @@ func (w *Window) Propagate(eventName string, me mouse.Event) { } } -// Width returns the absolute width of the window in pixels. -func (w *Window) Width() int { - return w.ScreenWidth -} - -// Height returns the absolute height of the window in pixels. -func (w *Window) Height() int { - return w.ScreenHeight -} - -// Viewport returns the viewport's position. Its width and height are the window's -// width and height. This position plus width/height cannot exceed ViewportBounds. -func (w *Window) Viewport() intgeom.Point2 { - return w.viewPos -} - -// ViewportBounds returns the boundary of this window's viewport, or the rectangle -// that the viewport is not allowed to exit as it moves around. It often represents -// the total size of the world within a given scene. -func (w *Window) ViewportBounds() intgeom.Rect2 { - return w.viewBounds +// Width returns the absolute bounds of a window in pixels. It does not include window elements outside +// of the client area (OS provided title bars). +func (w *Window) Bounds() intgeom.Point2 { + return intgeom.Point2{w.ScreenWidth, w.ScreenHeight} } // SetLoadingRenderable sets what renderable should display between scenes @@ -295,7 +283,7 @@ func (w *Window) SetBackground(b Background) { } } -// SetColorBackground sets this window's background to be a standar image.Image, +// SetColorBackground sets this window's background to be a standard image.Image, // commonly a uniform color. func (w *Window) SetColorBackground(img image.Image) { w.bkgFn = func() image.Image { @@ -316,9 +304,7 @@ func (w *Window) SetLogicHandler(h event.Handler) { // NextScene causes this window to immediately end the current scene. func (w *Window) NextScene() { - go func() { - w.skipSceneCh <- "" - }() + w.GoToScene("") } // GoToScene causes this window to skip directly to the given scene. @@ -333,22 +319,16 @@ func (w *Window) InFocus() bool { return w.inFocus } -// CollisionTrees helps access the mouse and collision trees from the controller. -// These trees together detail how a controller can drive mouse and entity interactions. -func (w *Window) CollisionTrees() (mouseTree, collisionTree *collision.Tree) { - return w.MouseTree, w.CollisionTree -} - // EventHandler returns this window's event handler. func (w *Window) EventHandler() event.Handler { return w.eventHandler } // MostRecentInput returns the most recent input type (e.g keyboard/mouse or joystick) -// recognized by the window. This value will only change if the controller's Config is +// recognized by the window. This value will only change if the window is // set to TrackInputChanges func (w *Window) MostRecentInput() InputType { - return w.mostRecentInput + return InputType(w.mostRecentInput) } func (w *Window) exitWithError(err error) { diff --git a/window/window.go b/window/window.go index 8d13bae4..6b512db5 100644 --- a/window/window.go +++ b/window/window.go @@ -2,34 +2,69 @@ package window import ( - "github.com/oakmound/oak/v3/alg/intgeom" - "github.com/oakmound/oak/v3/event" + "image" + + "github.com/oakmound/oak/v4/alg/intgeom" + "github.com/oakmound/oak/v4/event" ) -// Window is an interface of methods on an oak.Window used -// to avoid circular imports +// Window is an interface of methods on an oak.Window available on platforms which have distinct app windows +// (osx, linux, windows). It is not available on other platforms (js, android) type Window interface { + App + + // SetFullscreen causes a window to expand and fill a display. SetFullScreen(bool) error + // SetBorderless causes a window to lose its OS-provided border definitions, e.g. window title, close button. SetBorderless(bool) error + // SetTopMost causes a window to remain above other windows even when it is clicked out of. SetTopMost(bool) error + // SetTitle changes the title of this window, usually displayed in the top left of the window next to the icon. SetTitle(string) error - SetTrayIcon(string) error - ShowNotification(title, msg string, icon bool) error + // SetIcon changes the icon of this window, usually displayed both in the top left of the window and in a taskbar + // component. + SetIcon(image.Image) error + // MoveWindow moves a window to the given x,y coordinates with the given dimensions. + // TODO v4: intgeom.Rect2? MoveWindow(x, y, w, h int) error + // HideCursor will cause the mouse cursor to not display when it lies within this window. HideCursor() error +} + +// App is an interface of methods available to all oak programs. +type App interface { + // Bounds returns the boundaries of the application client area measured in pixels. This is not the size + // of the window or app on the operating system necessarily; it is the area able to be rendered to within oak. + // On some platforms these two concepts will usually be equal (js); on some they will have a built in scaling factor + // (osx, for retina displays), and if a window is manually scaled by a user and oak is not instructed to resize to + // match the scale, this area will be unchanged and the view will be stretched to fit the window. + Bounds() intgeom.Point2 - Width() int - Height() int + // Viewport relates Bounds() to the entire content available for display. Viewport returns where the top left corner + // of the application client area is. Viewport() intgeom.Point2 + // SetViewportBounds defines the limits of where the viewport may be positioned. In other words, the total viewable + // content of a scene. Unless impossible, the rectangle (viewport, viewport+bounds) will never leave the area defined + // by SetViewportBounds. SetViewportBounds(intgeom.Rect2) + // ShiftViewport is a helper method calling a.SetViewport(a.Viewport()+delta) + ShiftViewport(delta intgeom.Point2) + // SetViewport changes where the viewport position. If the resulting rectangle (viewport, viewport+bounds) would + // exceed the boundary set by SetViewportBounds, viewport will be clamped to the edges of that boundary. + SetViewport(intgeom.Point2) + // NextScene causes the End function to be triggered for the current scene. NextScene() + // GoToScene causes the End function to be triggered for the current scene, overriding the next scene to start. GoToScene(string) + // InFocus returns whether the application is currently focused on, by whatever definition the OS has for an + // application being in focus. For example, on linux/osx/windows a window is in focus once it is clicked on + // and out of focus after another window is clicked on. InFocus() bool - ShiftScreen(int, int) - SetScreen(int, int) + // Quit causes the app to cleanly exit. The current scene will not call it's End function. Quit() + // EventHandler returns this app's active event handler. EventHandler() event.Handler } diff --git a/window_test.go b/window_test.go index 23c15f79..cd61f47b 100644 --- a/window_test.go +++ b/window_test.go @@ -1,79 +1,185 @@ package oak import ( + "image" + "os" "testing" "time" - "github.com/oakmound/oak/v3/collision" - "github.com/oakmound/oak/v3/event" - "github.com/oakmound/oak/v3/mouse" + "github.com/oakmound/oak/v4/alg/floatgeom" + "github.com/oakmound/oak/v4/collision" + "github.com/oakmound/oak/v4/event" + "github.com/oakmound/oak/v4/mouse" + "github.com/oakmound/oak/v4/render" ) func TestMouseClicks(t *testing.T) { c1 := NewWindow() - sp := collision.NewFullSpace(0, 0, 100, 100, 1, 0) - var triggered bool - go event.ResolveChanges() - event.GlobalBind(mouse.Click, func(event.CID, interface{}) int { - triggered = true + c1.MouseTree = collision.NewTree() + ch := make(chan struct{}) + c1.eventHandler = event.NewBus(event.NewCallerMap()) + bnd := event.GlobalBind(c1.eventHandler, mouse.Click, func(_ *mouse.Event) event.Response { + close(ch) return 0 }) - time.Sleep(2 * time.Second) - mouse.DefaultTree.Add(sp) + select { + case <-time.After(2 * time.Second): + t.Fatalf("click binding never bound") + case <-bnd.Bound: + } + sp := collision.NewFullSpace(0, 0, 100, 100, 1, 0) + c1.MouseTree.Add(sp) c1.Propagate(mouse.PressOn, mouse.NewEvent(5, 5, mouse.ButtonLeft, mouse.PressOn)) c1.Propagate(mouse.ReleaseOn, mouse.NewEvent(5, 5, mouse.ButtonLeft, mouse.ReleaseOn)) - time.Sleep(2 * time.Second) - if !triggered { + select { + case <-time.After(2 * time.Second): t.Fatalf("propagation failed to trigger click binding") + case <-ch: } } func TestMouseClicksRelative(t *testing.T) { c1 := NewWindow() - sp := collision.NewFullSpace(0, 0, 100, 100, 1, 0) - var triggered bool - go c1.eventHandler.(*event.Bus).ResolveChanges() - c1.eventHandler.GlobalBind(mouse.ClickOn+"Relative", func(event.CID, interface{}) int { - triggered = true + c1.MouseTree = collision.NewTree() + ch := make(chan struct{}) + c1.eventHandler = event.NewBus(event.NewCallerMap()) + bnd := event.GlobalBind(c1.eventHandler, mouse.RelativeClickOn, func(_ *mouse.Event) event.Response { + close(ch) return 0 }) - time.Sleep(2 * time.Second) + select { + case <-time.After(2 * time.Second): + t.Fatalf("click binding never bound") + case <-bnd.Bound: + } + sp := collision.NewFullSpace(0, 0, 100, 100, 1, 0) c1.MouseTree.Add(sp) - c1.Propagate(mouse.PressOn+"Relative", mouse.NewEvent(5, 5, mouse.ButtonLeft, mouse.PressOn)) - c1.Propagate(mouse.ReleaseOn+"Relative", mouse.NewEvent(5, 5, mouse.ButtonLeft, mouse.ReleaseOn)) - time.Sleep(3 * time.Second) - if !triggered { + defer c1.MouseTree.Clear() + c1.Propagate(mouse.RelativePressOn, mouse.NewEvent(5, 5, mouse.ButtonLeft, mouse.PressOn)) + c1.Propagate(mouse.RelativeReleaseOn, mouse.NewEvent(5, 5, mouse.ButtonLeft, mouse.ReleaseOn)) + select { + case <-time.After(2 * time.Second): t.Fatalf("propagation failed to trigger click binding") + case <-ch: } } -type ent struct{} - -func (e ent) Init() event.CID { - return 0 +type ent struct { + event.CallerID } func TestPropagate(t *testing.T) { c1 := NewWindow() - go event.ResolveChanges() - var triggered bool - cid := event.CID(0).Parse(ent{}) - s := collision.NewSpace(10, 10, 10, 10, cid) - s.CID.Bind("MouseDownOn", func(event.CID, interface{}) int { - triggered = true + c1.eventHandler = event.NewBus(event.NewCallerMap()) + + thisEnt := ent{} + thisEnt.CallerID = c1.eventHandler.GetCallerMap().Register(thisEnt) + ch := make(chan struct{}) + s := collision.NewSpace(10, 10, 10, 10, thisEnt.CallerID) + event.Bind(c1.eventHandler, mouse.PressOn, thisEnt, func(ent, *mouse.Event) event.Response { + close(ch) + return 0 + }) + c1.MouseTree = collision.NewTree() + c1.MouseTree.Add(s) + c1.Propagate(mouse.ReleaseOn, mouse.NewEvent(15, 15, mouse.ButtonLeft, mouse.Release)) + select { + case <-ch: + t.Fatalf("release propagation triggered press binding") + case <-time.After(1 * time.Second): + } + c1.Propagate(mouse.PressOn, mouse.NewEvent(15, 15, mouse.ButtonLeft, mouse.Press)) + select { + case <-time.After(2 * time.Second): + t.Fatalf("propagation failed to trigger press binding") + case <-ch: + } +} + +func TestPropagate_StopPropagation(t *testing.T) { + c1 := NewWindow() + c1.eventHandler = event.NewBus(event.NewCallerMap()) + c1.MouseTree = collision.NewTree() + + e1 := ent{} + e1.CallerID = c1.eventHandler.GetCallerMap().Register(e1) + e2 := ent{} + e2.CallerID = c1.eventHandler.GetCallerMap().Register(e2) + + s1 := collision.NewSpace(10, 10, 10, 10, e1.CallerID) + s1.SetZLayer(10) + c1.MouseTree.Insert(s1) + s2 := collision.NewSpace(10, 10, 10, 10, e2.CallerID) + s2.SetZLayer(1) + c1.MouseTree.Insert(s2) + var failed bool + <-event.Bind(c1.eventHandler, mouse.PressOn, e1, func(_ ent, ev *mouse.Event) event.Response { + ev.StopPropagation = true + return 0 + }).Bound + <-event.Bind(c1.eventHandler, mouse.PressOn, e2, func(_ ent, ev *mouse.Event) event.Response { + failed = true + return 0 + }).Bound + <-event.Bind(c1.eventHandler, mouse.ClickOn, e1, func(_ ent, ev *mouse.Event) event.Response { + ev.StopPropagation = true return 0 + }).Bound + <-event.Bind(c1.eventHandler, mouse.ClickOn, e2, func(_ ent, ev *mouse.Event) event.Response { + failed = true + return 0 + }).Bound + <-event.Bind(c1.eventHandler, mouse.RelativeClickOn, e1, func(_ ent, ev *mouse.Event) event.Response { + ev.StopPropagation = true + return 0 + }).Bound + <-event.Bind(c1.eventHandler, mouse.RelativeClickOn, e2, func(_ ent, ev *mouse.Event) event.Response { + failed = true + return 0 + }).Bound + c1.TriggerMouseEvent(mouse.Event{ + Point2: floatgeom.Point2{ + 15, 15, + }, + Button: mouse.ButtonLeft, + EventType: mouse.Press, + }) + c1.TriggerMouseEvent(mouse.Event{ + Point2: floatgeom.Point2{ + 15, 15, + }, + Button: mouse.ButtonLeft, + EventType: mouse.Release, }) - mouse.Add(s) - time.Sleep(200 * time.Millisecond) - c1.Propagate("MouseUpOn", mouse.NewEvent(15, 15, mouse.ButtonLeft, "MouseUp")) - time.Sleep(200 * time.Millisecond) - if triggered { - t.Fatalf("mouse up triggered binding") + if failed { + t.Fatal("stop propagation failed") + } +} + +func TestWindowGetters(t *testing.T) { + c1 := NewWindow() + c1.debugConsole(os.Stdin, os.Stdout) + if c1.InFocus() { + t.Errorf("new windows should not be in focus") + } + if c1.EventHandler() != event.DefaultBus { + t.Errorf("new windows should have the default event bus") + } + if c1.GetBackgroundImage() != image.Black { + t.Errorf("new windows should have a black background") + } + c1.SetColorBackground(image.White) + if c1.GetBackgroundImage() != image.White { + t.Errorf("set color background failed") + } + rend := render.EmptyRenderable() + c1.SetLoadingRenderable(rend) + if c1.LoadingR != rend { + t.Errorf("Set loading renderable failed") } - time.Sleep(200 * time.Millisecond) - c1.Propagate("MouseDownOn", mouse.NewEvent(15, 15, mouse.ButtonLeft, "MouseDown")) - time.Sleep(200 * time.Millisecond) - if !triggered { - t.Fatalf("mouse down failed to trigger binding") + c1.SetBackground(rend) + r, g, b, a := c1.bkgFn().At(0, 0).RGBA() + if r != 0 || g != 0 || b != 0 || a != 0 { + t.Errorf("background was not set to empty renderable") } }