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 @@
-
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")
}
}