├── .gitignore
├── .travis.yml
├── CHANGELOG.md
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── batch.go
├── circle.go
├── circle_test.go
├── color.go
├── color_test.go
├── compose.go
├── data.go
├── data_test.go
├── doc.go
├── drawer.go
├── drawer_test.go
├── go.mod
├── go.sum
├── imdraw
├── imdraw.go
└── imdraw_test.go
├── interface.go
├── line_test.go
├── logo
├── LOGOTYPE-HORIZONTAL-BLACK.png
├── LOGOTYPE-HORIZONTAL-BLUE.png
├── LOGOTYPE-HORIZONTAL-MAROON.png
├── LOGOTYPE-HORIZONTAL-PURPLE.png
├── LOGOTYPE-HORIZONTAL-RED.png
└── LOGOTYPE-HORIZONTAL-WHITE.png
├── math.go
├── math_test.go
├── matrix.go
├── matrix_test.go
├── pixel_test.go
├── pixelgl
├── canvas.go
├── doc.go
├── glframe.go
├── glpicture.go
├── glshader.go
├── gltriangles.go
├── input.go
├── joystick.go
├── monitor.go
├── run.go
├── util.go
└── window.go
├── rectangle.go
├── rectangle_test.go
├── sprite.go
├── text
├── atlas.go
├── atlas_test.go
├── doc.go
├── text.go
└── text_test.go
├── vector.go
└── vector_test.go
/.gitignore:
--------------------------------------------------------------------------------
1 | test
2 | .vscode
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: go
2 | # https://github.com/golang/go/issues/31293
3 | dist: xenial
4 | sudo: false
5 | addons:
6 | apt:
7 | packages:
8 | - xorg-dev
9 | - libx11-dev
10 | - libxrandr-dev
11 | - libxinerama-dev
12 | - libxcursor-dev
13 | - libxi-dev
14 | - libopenal-dev
15 | - libasound2-dev
16 | - libgl1-mesa-dev
17 |
18 | services:
19 | - xvfb
20 |
21 | env:
22 | - GO111MODULE=on
23 |
24 | go:
25 | - tip
26 | - 1.12.x
27 |
28 | install:
29 | - # Do nothing. This is needed to prevent the default install action
30 | # "go get -t -v ./..." from happening here because we want it to happen
31 | # inside script step.
32 |
33 | script:
34 | - go test -v -race -mod=readonly ./...
35 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 | All notable changes to this project will be documented in this file.
3 |
4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6 |
7 | ## [Unreleased]
8 | - Add AnchorPos struct and functions #252
9 | - Add Clipboard Support
10 | - Fix SIGSEGV on text.NewAtlas if glyph absent
11 | - Use slice for range in Drawer.Dirty(), to improve performance
12 | - GLTriangle's fragment shader is used when rendered by the Canvas.
13 | - Add MSAA support
14 |
15 | ## [v0.10.0] 2020-08-22
16 | - Add AnchorPos struct and functions
17 | - Gamepad API added
18 | - Support setting an initial window position
19 | - Support hiding the window initially
20 | - Support creating maximized windows
21 | - Support waiting for events to reduce CPU load
22 | - Adding clipping rectangle support in GLTriangles
23 |
24 | ## [v0.10.0-beta] 2020-05-10
25 | - Add `WindowConfig.TransparentFramebuffer` option to support window transparency onto the background
26 | - Fixed Line intersects failing on lines passing through (0, 0)
27 |
28 | ## [v0.10.0-alpha] 2020-05-08
29 | - Upgrade to GLFW 3.3! :tada:
30 | - Closes https://github.com/faiface/pixel/issues/137
31 | - Add support for glfw's DisableCursor
32 | - Closes https://github.com/faiface/pixel/issues/213
33 |
34 | ## [v0.9.0] - 2020-05-02
35 | - Added feature from https://github.com/faiface/pixel/pull/219
36 | - Exposing Window.SwapBuffers so buffers can be swapped without polling input
37 | - Add more examples
38 | - Add position as out variable from vertex shader
39 | - Add experimental joystick support
40 | - Add mouse cursor operations
41 | - Add `Vec.Floor(…)` function
42 | - Add circle geometry
43 | - Fix `Matrix.Unproject(…)` for rotated matrix
44 | - Add 2D Line geometry
45 | - Add floating point round error correction
46 | - Performance improvements
47 | - Fix race condition in `NewGLTriangles(…)`
48 | - Add `TriangleData` benchmarks and improvements
49 | - Add zero rectangle variable for utility and consistency
50 | - Add support for Go Modules
51 | - Add `NoIconify` and `AlwaysOnTop` window hints
52 |
53 |
54 | ## [v0.8.0] - 2018-10-10
55 | Changelog for this and older versions can be found on the corresponding [GitHub
56 | releases](https://github.com/faiface/pixel/releases).
57 |
58 | [Unreleased]: https://github.com/faiface/pixel/compare/v0.10.0...HEAD
59 | [v0.10.0]: https://github.com/faiface/pixel/compare/v0.10.0-beta...v0.10.0
60 | [v0.10.0-beta]: https://github.com/faiface/pixel/compare/v0.10.0-alpha...v0.10.0-beta
61 | [v0.10.0-alpha]: https://github.com/faiface/pixel/compare/v0.9.0...v0.10.0-alpha
62 | [v0.9.0]: https://github.com/faiface/pixel/compare/v0.8.0...v0.9.0
63 | [v0.8.0]: https://github.com/faiface/pixel/releases/tag/v0.8.0
64 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing to Pixel
2 |
3 | :tada: Hi! I'm really glad you're considering contributing to Pixel! :tada:
4 |
5 | ## Here are a few ways you can contribute
6 |
7 | 1. **Make a community example** and place it inside the `community` folder of the [examples repository][examples].
8 | 2. **Add tests**. There only few tests in Pixel at the moment. Take a look at them and make some similar.
9 | 3. **Add a small feature or an improvement**. Feel like some small feature is missing? Just make a PR. Be ready that I might reject it, though, if I don't find it particularly appealing.
10 | 4. **Join the big development** by joining the discussion on our [Discord Server](https://discord.gg/q2DK4MP), where we can discuss bigger changes and implement them after that.
11 |
12 | ## How to make a pull request
13 |
14 | Go gives you a nice surprise when attempting to make a PR on Github. The thing is, that when user _xyz_ forks Pixel on Github, it ends up in _github.com/xyz/pixel_, which fucks up your import paths. Here's how you deal with that: https://www.reddit.com/r/golang/comments/2jdcw1/how_do_you_deal_with_github_forking/.
15 |
16 | [examples]: https://github.com/faiface/pixel-examples/tree/master/community
17 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2016 Michal Štrba
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # \*\*\*\*\*NOTICE\*\*\*\*\*
2 |
3 | This repo is not under active development anymore and has been archived. Continued development has been migrated to [Pixel2](https://github.com/gopxl/pixel). A big thank you to [faiface](https://github.com/faiface) for creating this awesome library and for all the hard work put into it. We encourage old and new users to check out the new repo and contribute to it.
4 |
5 |
6 |
7 |

8 |
9 | # Pixel [](https://travis-ci.org/faiface/pixel) [](https://godoc.org/github.com/faiface/pixel) [](https://goreportcard.com/report/github.com/faiface/pixel) [](https://gitter.im/pixellib/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [](https://discord.gg/q2DK4MP)
10 |
11 | A hand-crafted 2D game library in Go. Take a look into the [features](#features) to see what it can
12 | do.
13 |
14 | ```
15 | go get github.com/faiface/pixel
16 | ```
17 |
18 | If you are using Modules (Go 1.11 or higher) and want a mutable copy of the source code:
19 |
20 | ```
21 | git clone https://github.com/faiface/pixel # clone outside of $GOPATH
22 | cd pixel
23 | go install ./...
24 | ```
25 |
26 | See [requirements](#requirements) for the list of libraries necessary for compilation.
27 |
28 | All significant changes are documented in [CHANGELOG.md](CHANGELOG.md).
29 |
30 | ## Tutorial
31 |
32 | The [Wiki of this repo](https://github.com/faiface/pixel/wiki) contains an extensive tutorial
33 | covering several topics of Pixel. Here's the content of the tutorial parts so far:
34 |
35 | - [Creating a Window](https://github.com/faiface/pixel/wiki/Creating-a-Window)
36 | - [Drawing a Sprite](https://github.com/faiface/pixel/wiki/Drawing-a-Sprite)
37 | - [Moving, scaling and rotating with Matrix](https://github.com/faiface/pixel/wiki/Moving,-scaling-and-rotating-with-Matrix)
38 | - [Pressing keys and clicking mouse](https://github.com/faiface/pixel/wiki/Pressing-keys-and-clicking-mouse)
39 | - [Drawing efficiently with Batch](https://github.com/faiface/pixel/wiki/Drawing-efficiently-with-Batch)
40 | - [Drawing shapes with IMDraw](https://github.com/faiface/pixel/wiki/Drawing-shapes-with-IMDraw)
41 | - [Typing text on the screen](https://github.com/faiface/pixel/wiki/Typing-text-on-the-screen)
42 | - [Using a custom fragment shader](https://github.com/faiface/pixel/wiki/Using-a-custom-fragment-shader)
43 |
44 | ## [Examples](https://github.com/faiface/pixel-examples)
45 |
46 | The [examples](https://github.com/faiface/pixel-examples) repository contains a few
47 | examples demonstrating Pixel's functionality.
48 |
49 | **To run an example**, navigate to it's directory, then `go run` the `main.go` file. For example:
50 |
51 | ```
52 | $ cd pixel-examples/platformer
53 | $ go run main.go
54 | ```
55 |
56 | Here are some screenshots from the examples!
57 |
58 | | [Lights](https://github.com/faiface/pixel-examples/blob/master/lights) | [Platformer](https://github.com/faiface/pixel-examples/blob/master/platformer) |
59 | | --- | --- |
60 | |  |  |
61 |
62 | | [Smoke](https://github.com/faiface/pixel-examples/blob/master/smoke) | [Typewriter](https://github.com/faiface/pixel-examples/blob/master/typewriter) |
63 | | --- | --- |
64 | |  |  |
65 |
66 | | [Raycaster](https://github.com/faiface/pixel-examples/blob/master/community/raycaster) | [Gizmo](https://github.com/Lallassu/gizmo) |
67 | | --- | --- |
68 | |  |  |
69 |
70 | ## Features
71 |
72 | Here's the list of the main features in Pixel. Although Pixel is still under heavy development,
73 | **there should be no major breakage in the API.** This is not a 100% guarantee, though.
74 |
75 | - Fast 2D graphics
76 | - Sprites
77 | - Primitive shapes with immediate mode style
78 | [IMDraw](https://github.com/faiface/pixel/wiki/Drawing-shapes-with-IMDraw) (circles, rectangles,
79 | lines, ...)
80 | - Optimized drawing with [Batch](https://github.com/faiface/pixel/wiki/Drawing-efficiently-with-Batch)
81 | - Text drawing with [text](https://godoc.org/github.com/faiface/pixel/text) package
82 | - Audio through a separate [Beep](https://github.com/faiface/beep) library.
83 | - Simple and convenient API
84 | - Drawing a sprite to a window is as simple as `sprite.Draw(window, matrix)`
85 | - Wanna know where the center of a window is? `window.Bounds().Center()`
86 | - [...](https://godoc.org/github.com/faiface/pixel)
87 | - Full documentation and tutorial
88 | - Works on Linux, macOS and Windows
89 | - Window creation and manipulation (resizing, fullscreen, multiple windows, ...)
90 | - Keyboard (key presses, text input) and mouse input without events
91 | - Well integrated with the Go standard library
92 | - Use `"image"` package for loading pictures
93 | - Use `"time"` package for measuring delta time and FPS
94 | - Use `"image/color"` for colors, or use Pixel's own `color.Color` format, which supports easy
95 | multiplication and a few more features
96 | - Pixel uses `float64` throughout the library, compatible with `"math"` package
97 | - Geometry transformations with
98 | [Matrix](https://github.com/faiface/pixel/wiki/Moving,-scaling-and-rotating-with-Matrix)
99 | - Moving, scaling, rotating
100 | - Easy camera implementation
101 | - Off-screen drawing to Canvas or any other target (Batch, IMDraw, ...)
102 | - Fully garbage collected, no `Close` or `Dispose` methods
103 | - Full [Porter-Duff](http://ssp.impulsetrain.com/porterduff.html) composition, which enables
104 | - 2D lighting
105 | - Cutting holes into objects
106 | - Much more...
107 | - Pixel let's you draw stuff and do your job, it doesn't impose any particular style or paradigm
108 | - Platform and backend independent [core](https://godoc.org/github.com/faiface/pixel)
109 | - Core Target/Triangles/Picture pattern makes it easy to create new drawing targets that do
110 | arbitrarily crazy stuff (e.g. graphical effects)
111 | - Small codebase, ~5K lines of code, including the backend [glhf](https://github.com/faiface/glhf)
112 | package
113 |
114 | ## Related repositories
115 |
116 | Here are some packages which use Pixel:
117 | - [TilePix](https://github.com/bcvery1/tilepix) Makes handling TMX files built with [Tiled](https://www.mapeditor.org/) trivially easy to work with using Pixel.
118 | - [spriteplus](https://github.com/cebarks/spriteplus) Basic `SpriteSheet` and `Animation` implementations
119 | - [PixelUI](https://github.com/dusk125/pixelui) Imgui-based GUIs for Pixel
120 | - [pixelutils](https://github.com/dusk125/pixelutils) Variety of game related utilities (sprite packer, id generator, ticker, sprite loader, voronoia diagrams)
121 |
122 | ## Missing features
123 |
124 | Pixel is in development and still missing few critical features. Here're the most critical ones.
125 |
126 | - ~~Audio~~
127 | - ~~Drawing text~~
128 | - Antialiasing (filtering is supported, though)
129 | - ~~Advanced window manipulation (cursor hiding, window icon, ...)~~
130 | - Better support for Hi-DPI displays
131 | - Mobile (and perhaps HTML5?) backend
132 | - ~~More advanced graphical effects (e.g. blur)~~ (solved with the addition of GLSL effects)
133 | - Tests and benchmarks
134 | - Vulkan support
135 |
136 | **Implementing these features will get us to the 1.0 release.** Contribute, so that it's as soon as
137 | possible!
138 |
139 | ## Requirements
140 |
141 | If you're using Windows and having trouble building Pixel, please check [this
142 | guide](https://github.com/faiface/pixel/wiki/Building-Pixel-on-Windows) on the
143 | [wiki](https://github.com/faiface/pixel/wiki).
144 |
145 | [PixelGL](https://godoc.org/github.com/faiface/pixel/pixelgl) backend uses OpenGL to render
146 | graphics. Because of that, OpenGL development libraries are needed for compilation. The dependencies
147 | are same as for [GLFW](https://github.com/go-gl/glfw).
148 |
149 | The OpenGL version used is **OpenGL 3.3**.
150 |
151 | - On macOS, you need Xcode or Command Line Tools for Xcode (`xcode-select --install`) for required
152 | headers and libraries.
153 | - On Ubuntu/Debian-like Linux distributions, you need `libgl1-mesa-dev` and `xorg-dev` packages.
154 | - On CentOS/Fedora-like Linux distributions, you need `libX11-devel libXcursor-devel libXrandr-devel
155 | libXinerama-devel mesa-libGL-devel libXi-devel libXxf86vm-devel` packages.
156 | - See [here](http://www.glfw.org/docs/latest/compile.html#compile_deps) for full details.
157 |
158 | **The combination of Go 1.8, macOS and latest XCode seems to be problematic** as mentioned in issue
159 | [#7](https://github.com/faiface/pixel/issues/7). This issue is probably not related to Pixel.
160 | **Upgrading to Go 1.8.1 fixes the issue.**
161 |
162 | ## Contributing
163 |
164 | Join us in the [Discord Chat!](https://discord.gg/q2DK4MP)
165 |
166 | Pixel is in, let's say, mid-stage of development. Many of the important features are here, some are
167 | missing. That's why **contributions are very important and welcome!** All alone, I will be able to
168 | finish the library, but it'll take a lot of time. With your help, it'll take much less. I encourage
169 | everyone to contribute, even with just an idea. Especially welcome are **issues** and **pull
170 | requests**.
171 |
172 | **However, I won't accept everything. Pixel is being developed with thought and care.** Each
173 | component was designed and re-designed multiple times. Code and API quality is very important here.
174 | API is focused on simplicity and expressiveness.
175 |
176 | When contributing, keep these goals in mind. It doesn't mean that I'll only accept perfect pull
177 | requests. It just means that I might not like your idea. Or that your pull requests could need some
178 | rewriting. That's perfectly fine, don't let it put you off. In the end, we'll just end up with a
179 | better result.
180 |
181 | Take a look at [CONTRIBUTING.md](CONTRIBUTING.md) for further information.
182 |
183 | ## License
184 |
185 | [MIT](LICENSE)
186 |
--------------------------------------------------------------------------------
/batch.go:
--------------------------------------------------------------------------------
1 | package pixel
2 |
3 | import (
4 | "fmt"
5 | "image/color"
6 | )
7 |
8 | // Batch is a Target that allows for efficient drawing of many objects with the same Picture.
9 | //
10 | // To put an object into a Batch, just draw it onto it:
11 | // object.Draw(batch)
12 | type Batch struct {
13 | cont Drawer
14 |
15 | mat Matrix
16 | col RGBA
17 | }
18 |
19 | var _ BasicTarget = (*Batch)(nil)
20 |
21 | // NewBatch creates an empty Batch with the specified Picture and container.
22 | //
23 | // The container is where objects get accumulated. Batch will support precisely those Triangles
24 | // properties, that the supplied container supports. If you retain access to the container and
25 | // change it, call Dirty to notify Batch about the change.
26 | //
27 | // Note, that if the container does not support TrianglesColor, color masking will not work.
28 | func NewBatch(container Triangles, pic Picture) *Batch {
29 | b := &Batch{cont: Drawer{Triangles: container, Picture: pic, Cached: true}}
30 | b.SetMatrix(IM)
31 | b.SetColorMask(Alpha(1))
32 | return b
33 | }
34 |
35 | // Dirty notifies Batch about an external modification of it's container. If you retain access to
36 | // the Batch's container and change it, call Dirty to notify Batch about the change.
37 | //
38 | // container := &pixel.TrianglesData{}
39 | // batch := pixel.NewBatch(container, nil)
40 | // container.SetLen(10) // container changed from outside of Batch
41 | // batch.Dirty() // notify Batch about the change
42 | func (b *Batch) Dirty() {
43 | b.cont.Dirty()
44 | }
45 |
46 | // Clear removes all objects from the Batch.
47 | func (b *Batch) Clear() {
48 | b.cont.Triangles.SetLen(0)
49 | b.cont.Dirty()
50 | }
51 |
52 | // Draw draws all objects that are currently in the Batch onto another Target.
53 | func (b *Batch) Draw(t Target) {
54 | b.cont.Draw(t)
55 | }
56 |
57 | // SetMatrix sets a Matrix that every point will be projected by.
58 | func (b *Batch) SetMatrix(m Matrix) {
59 | b.mat = m
60 | }
61 |
62 | // SetColorMask sets a mask color used in the following draws onto the Batch.
63 | func (b *Batch) SetColorMask(c color.Color) {
64 | if c == nil {
65 | b.col = Alpha(1)
66 | return
67 | }
68 | b.col = ToRGBA(c)
69 | }
70 |
71 | // MakeTriangles returns a specialized copy of the provided Triangles that draws onto this Batch.
72 | func (b *Batch) MakeTriangles(t Triangles) TargetTriangles {
73 | bt := &batchTriangles{
74 | tri: t.Copy(),
75 | tmp: MakeTrianglesData(t.Len()),
76 | dst: b,
77 | }
78 | return bt
79 | }
80 |
81 | // MakePicture returns a specialized copy of the provided Picture that draws onto this Batch.
82 | func (b *Batch) MakePicture(p Picture) TargetPicture {
83 | if p != b.cont.Picture {
84 | panic(fmt.Errorf("(%T).MakePicture: Picture is not the Batch's Picture", b))
85 | }
86 | bp := &batchPicture{
87 | pic: p,
88 | dst: b,
89 | }
90 | return bp
91 | }
92 |
93 | type batchTriangles struct {
94 | tri Triangles
95 | tmp *TrianglesData
96 | dst *Batch
97 | }
98 |
99 | func (bt *batchTriangles) Len() int {
100 | return bt.tri.Len()
101 | }
102 |
103 | func (bt *batchTriangles) SetLen(len int) {
104 | bt.tri.SetLen(len)
105 | bt.tmp.SetLen(len)
106 | }
107 |
108 | func (bt *batchTriangles) Slice(i, j int) Triangles {
109 | return &batchTriangles{
110 | tri: bt.tri.Slice(i, j),
111 | tmp: bt.tmp.Slice(i, j).(*TrianglesData),
112 | dst: bt.dst,
113 | }
114 | }
115 |
116 | func (bt *batchTriangles) Update(t Triangles) {
117 | bt.tri.Update(t)
118 | }
119 |
120 | func (bt *batchTriangles) Copy() Triangles {
121 | return &batchTriangles{
122 | tri: bt.tri.Copy(),
123 | tmp: bt.tmp.Copy().(*TrianglesData),
124 | dst: bt.dst,
125 | }
126 | }
127 |
128 | func (bt *batchTriangles) draw(bp *batchPicture) {
129 | bt.tmp.Update(bt.tri)
130 |
131 | for i := range *bt.tmp {
132 | (*bt.tmp)[i].Position = bt.dst.mat.Project((*bt.tmp)[i].Position)
133 | (*bt.tmp)[i].Color = bt.dst.col.Mul((*bt.tmp)[i].Color)
134 | }
135 |
136 | cont := bt.dst.cont.Triangles
137 | cont.SetLen(cont.Len() + bt.tri.Len())
138 | added := cont.Slice(cont.Len()-bt.tri.Len(), cont.Len())
139 | added.Update(bt.tri)
140 | added.Update(bt.tmp)
141 | bt.dst.cont.Dirty()
142 | }
143 |
144 | func (bt *batchTriangles) Draw() {
145 | bt.draw(nil)
146 | }
147 |
148 | type batchPicture struct {
149 | pic Picture
150 | dst *Batch
151 | }
152 |
153 | func (bp *batchPicture) Bounds() Rect {
154 | return bp.pic.Bounds()
155 | }
156 |
157 | func (bp *batchPicture) Draw(t TargetTriangles) {
158 | bt := t.(*batchTriangles)
159 | if bp.dst != bt.dst {
160 | panic(fmt.Errorf("(%T).Draw: TargetTriangles generated by different Batch", bp))
161 | }
162 | bt.draw(bp)
163 | }
164 |
--------------------------------------------------------------------------------
/circle.go:
--------------------------------------------------------------------------------
1 | package pixel
2 |
3 | import (
4 | "fmt"
5 | "math"
6 | )
7 |
8 | // Circle is a 2D circle. It is defined by two properties:
9 | // - Center vector
10 | // - Radius float64
11 | type Circle struct {
12 | Center Vec
13 | Radius float64
14 | }
15 |
16 | // C returns a new Circle with the given radius and center coordinates.
17 | //
18 | // Note that a negative radius is valid.
19 | func C(center Vec, radius float64) Circle {
20 | return Circle{
21 | Center: center,
22 | Radius: radius,
23 | }
24 | }
25 |
26 | // String returns the string representation of the Circle.
27 | //
28 | // c := pixel.C(10.1234, pixel.ZV)
29 | // c.String() // returns "Circle(10.12, Vec(0, 0))"
30 | // fmt.Println(c) // Circle(10.12, Vec(0, 0))
31 | func (c Circle) String() string {
32 | return fmt.Sprintf("Circle(%s, %.2f)", c.Center, c.Radius)
33 | }
34 |
35 | // Norm returns the Circle in normalized form - this sets the radius to its absolute value.
36 | //
37 | // c := pixel.C(-10, pixel.ZV)
38 | // c.Norm() // returns pixel.Circle{pixel.Vec{0, 0}, 10}
39 | func (c Circle) Norm() Circle {
40 | return Circle{
41 | Center: c.Center,
42 | Radius: math.Abs(c.Radius),
43 | }
44 | }
45 |
46 | // Area returns the area of the Circle.
47 | func (c Circle) Area() float64 {
48 | return math.Pi * math.Pow(c.Radius, 2)
49 | }
50 |
51 | // Moved returns the Circle moved by the given vector delta.
52 | func (c Circle) Moved(delta Vec) Circle {
53 | return Circle{
54 | Center: c.Center.Add(delta),
55 | Radius: c.Radius,
56 | }
57 | }
58 |
59 | // Resized returns the Circle resized by the given delta. The Circles center is use as the anchor.
60 | //
61 | // c := pixel.C(pixel.ZV, 10)
62 | // c.Resized(-5) // returns pixel.Circle{pixel.Vec{0, 0}, 5}
63 | // c.Resized(25) // returns pixel.Circle{pixel.Vec{0, 0}, 35}
64 | func (c Circle) Resized(radiusDelta float64) Circle {
65 | return Circle{
66 | Center: c.Center,
67 | Radius: c.Radius + radiusDelta,
68 | }
69 | }
70 |
71 | // Contains checks whether a vector `u` is contained within this Circle (including it's perimeter).
72 | func (c Circle) Contains(u Vec) bool {
73 | toCenter := c.Center.To(u)
74 | return c.Radius >= toCenter.Len()
75 | }
76 |
77 | // Formula returns the values of h and k, for the equation of the circle: (x-h)^2 + (y-k)^2 = r^2
78 | // where r is the radius of the circle.
79 | func (c Circle) Formula() (h, k float64) {
80 | return c.Center.X, c.Center.Y
81 | }
82 |
83 | // maxCircle will return the larger circle based on the radius.
84 | func maxCircle(c, d Circle) Circle {
85 | if c.Radius < d.Radius {
86 | return d
87 | }
88 | return c
89 | }
90 |
91 | // minCircle will return the smaller circle based on the radius.
92 | func minCircle(c, d Circle) Circle {
93 | if c.Radius < d.Radius {
94 | return c
95 | }
96 | return d
97 | }
98 |
99 | // Union returns the minimal Circle which covers both `c` and `d`.
100 | func (c Circle) Union(d Circle) Circle {
101 | biggerC := maxCircle(c.Norm(), d.Norm())
102 | smallerC := minCircle(c.Norm(), d.Norm())
103 |
104 | // Get distance between centers
105 | dist := c.Center.To(d.Center).Len()
106 |
107 | // If the bigger Circle encompasses the smaller one, we have the result
108 | if dist+smallerC.Radius <= biggerC.Radius {
109 | return biggerC
110 | }
111 |
112 | // Calculate radius for encompassing Circle
113 | r := (dist + biggerC.Radius + smallerC.Radius) / 2
114 |
115 | // Calculate center for encompassing Circle
116 | theta := .5 + (biggerC.Radius-smallerC.Radius)/(2*dist)
117 | center := Lerp(smallerC.Center, biggerC.Center, theta)
118 |
119 | return Circle{
120 | Center: center,
121 | Radius: r,
122 | }
123 | }
124 |
125 | // Intersect returns the maximal Circle which is covered by both `c` and `d`.
126 | //
127 | // If `c` and `d` don't overlap, this function returns a zero-sized circle at the centerpoint between the two Circle's
128 | // centers.
129 | func (c Circle) Intersect(d Circle) Circle {
130 | // Check if one of the circles encompasses the other; if so, return that one
131 | biggerC := maxCircle(c.Norm(), d.Norm())
132 | smallerC := minCircle(c.Norm(), d.Norm())
133 |
134 | if biggerC.Radius >= biggerC.Center.To(smallerC.Center).Len()+smallerC.Radius {
135 | return biggerC
136 | }
137 |
138 | // Calculate the midpoint between the two radii
139 | // Distance between centers
140 | dist := c.Center.To(d.Center).Len()
141 | // Difference between radii
142 | diff := dist - (c.Radius + d.Radius)
143 | // Distance from c.Center to the weighted midpoint
144 | distToMidpoint := c.Radius + 0.5*diff
145 | // Weighted midpoint
146 | center := Lerp(c.Center, d.Center, distToMidpoint/dist)
147 |
148 | // No need to calculate radius if the circles do not overlap
149 | if c.Center.To(d.Center).Len() >= c.Radius+d.Radius {
150 | return C(center, 0)
151 | }
152 |
153 | radius := c.Center.To(d.Center).Len() - (c.Radius + d.Radius)
154 |
155 | return Circle{
156 | Center: center,
157 | Radius: math.Abs(radius),
158 | }
159 | }
160 |
161 | // IntersectLine will return the shortest Vec such that if the Circle is moved by the Vec returned, the Line and Rect no
162 | // longer intersect.
163 | func (c Circle) IntersectLine(l Line) Vec {
164 | return l.IntersectCircle(c).Scaled(-1)
165 | }
166 |
167 | // IntersectRect returns a minimal required Vector, such that moving the circle by that vector would stop the Circle
168 | // and the Rect intersecting. This function returns a zero-vector if the Circle and Rect do not overlap, and if only
169 | // the perimeters touch.
170 | //
171 | // This function will return a non-zero vector if:
172 | // - The Rect contains the Circle, partially or fully
173 | // - The Circle contains the Rect, partially of fully
174 | func (c Circle) IntersectRect(r Rect) Vec {
175 | // Checks if the c.Center is not in the diagonal quadrants of the rectangle
176 | if (r.Min.X <= c.Center.X && c.Center.X <= r.Max.X) || (r.Min.Y <= c.Center.Y && c.Center.Y <= r.Max.Y) {
177 | // 'grow' the Rect by c.Radius in each orthagonal
178 | grown := Rect{Min: r.Min.Sub(V(c.Radius, c.Radius)), Max: r.Max.Add(V(c.Radius, c.Radius))}
179 | if !grown.Contains(c.Center) {
180 | // c.Center not close enough to overlap, return zero-vector
181 | return ZV
182 | }
183 |
184 | // Get minimum distance to travel out of Rect
185 | rToC := r.Center().To(c.Center)
186 | h := c.Radius - math.Abs(rToC.X) + (r.W() / 2)
187 | v := c.Radius - math.Abs(rToC.Y) + (r.H() / 2)
188 |
189 | if rToC.X < 0 {
190 | h = -h
191 | }
192 | if rToC.Y < 0 {
193 | v = -v
194 | }
195 |
196 | // No intersect
197 | if h == 0 && v == 0 {
198 | return ZV
199 | }
200 |
201 | if math.Abs(h) > math.Abs(v) {
202 | // Vertical distance shorter
203 | return V(0, v)
204 | }
205 | return V(h, 0)
206 | } else {
207 | // The center is in the diagonal quadrants
208 |
209 | // Helper points to make code below easy to read.
210 | rectTopLeft := V(r.Min.X, r.Max.Y)
211 | rectBottomRight := V(r.Max.X, r.Min.Y)
212 |
213 | // Check for overlap.
214 | if !(c.Contains(r.Min) || c.Contains(r.Max) || c.Contains(rectTopLeft) || c.Contains(rectBottomRight)) {
215 | // No overlap.
216 | return ZV
217 | }
218 |
219 | var centerToCorner Vec
220 | if c.Center.To(r.Min).Len() <= c.Radius {
221 | // Closest to bottom-left
222 | centerToCorner = c.Center.To(r.Min)
223 | }
224 | if c.Center.To(r.Max).Len() <= c.Radius {
225 | // Closest to top-right
226 | centerToCorner = c.Center.To(r.Max)
227 | }
228 | if c.Center.To(rectTopLeft).Len() <= c.Radius {
229 | // Closest to top-left
230 | centerToCorner = c.Center.To(rectTopLeft)
231 | }
232 | if c.Center.To(rectBottomRight).Len() <= c.Radius {
233 | // Closest to bottom-right
234 | centerToCorner = c.Center.To(rectBottomRight)
235 | }
236 |
237 | cornerToCircumferenceLen := c.Radius - centerToCorner.Len()
238 |
239 | return centerToCorner.Unit().Scaled(cornerToCircumferenceLen)
240 | }
241 | }
242 |
243 | // IntersectionPoints returns all the points where the Circle intersects with the line provided. This can be zero, one or
244 | // two points, depending on the location of the shapes. The points of intersection will be returned in order of
245 | // closest-to-l.A to closest-to-l.B.
246 | func (c Circle) IntersectionPoints(l Line) []Vec {
247 | cContainsA := c.Contains(l.A)
248 | cContainsB := c.Contains(l.B)
249 |
250 | // Special case for both endpoint being contained within the circle
251 | if cContainsA && cContainsB {
252 | return []Vec{}
253 | }
254 |
255 | // Get closest point on the line to this circles' center
256 | closestToCenter := l.Closest(c.Center)
257 |
258 | // If the distance to the closest point is greater than the radius, there are no points of intersection
259 | if closestToCenter.To(c.Center).Len() > c.Radius {
260 | return []Vec{}
261 | }
262 |
263 | // If the distance to the closest point is equal to the radius, the line is tangent and the closest point is the
264 | // point at which it touches the circle.
265 | if closestToCenter.To(c.Center).Len() == c.Radius {
266 | return []Vec{closestToCenter}
267 | }
268 |
269 | // Special case for endpoint being on the circles' center
270 | if c.Center == l.A || c.Center == l.B {
271 | otherEnd := l.B
272 | if c.Center == l.B {
273 | otherEnd = l.A
274 | }
275 | intersect := c.Center.Add(c.Center.To(otherEnd).Unit().Scaled(c.Radius))
276 | return []Vec{intersect}
277 | }
278 |
279 | // This means the distance to the closest point is less than the radius, so there is at least one intersection,
280 | // possibly two.
281 |
282 | // If one of the end points exists within the circle, there is only one intersection
283 | if cContainsA || cContainsB {
284 | containedPoint := l.A
285 | otherEnd := l.B
286 | if cContainsB {
287 | containedPoint = l.B
288 | otherEnd = l.A
289 | }
290 |
291 | // Use trigonometry to get the length of the line between the contained point and the intersection point.
292 | // The following is used to describe the triangle formed:
293 | // - a is the side between contained point and circle center
294 | // - b is the side between the center and the intersection point (radius)
295 | // - c is the side between the contained point and the intersection point
296 | // The captials of these letters are used as the angles opposite the respective sides.
297 | // a and b are known
298 | a := containedPoint.To(c.Center).Len()
299 | b := c.Radius
300 | // B can be calculated by subtracting the angle of b (to the x-axis) from the angle of c (to the x-axis)
301 | B := containedPoint.To(c.Center).Angle() - containedPoint.To(otherEnd).Angle()
302 | // Using the Sin rule we can get A
303 | A := math.Asin((a * math.Sin(B)) / b)
304 | // Using the rule that there are 180 degrees (or Pi radians) in a triangle, we can now get C
305 | C := math.Pi - A + B
306 | // If C is zero, the line segment is in-line with the center-intersect line.
307 | var c float64
308 | if C == 0 {
309 | c = b - a
310 | } else {
311 | // Using the Sine rule again, we can now get c
312 | c = (a * math.Sin(C)) / math.Sin(A)
313 | }
314 | // Travelling from the contained point to the other end by length of a will provide the intersection point.
315 | return []Vec{
316 | containedPoint.Add(containedPoint.To(otherEnd).Unit().Scaled(c)),
317 | }
318 | }
319 |
320 | // Otherwise the endpoints exist outside of the circle, and the line segment intersects in two locations.
321 | // The vector formed by going from the closest point to the center of the circle will be perpendicular to the line;
322 | // this forms a right-angled triangle with the intersection points, with the radius as the hypotenuse.
323 | // Calculate the other triangles' sides' length.
324 | a := math.Sqrt(math.Pow(c.Radius, 2) - math.Pow(closestToCenter.To(c.Center).Len(), 2))
325 |
326 | // Travelling in both directions from the closest point by length of a will provide the two intersection points.
327 | first := closestToCenter.Add(closestToCenter.To(l.A).Unit().Scaled(a))
328 | second := closestToCenter.Add(closestToCenter.To(l.B).Unit().Scaled(a))
329 |
330 | if first.To(l.A).Len() < second.To(l.A).Len() {
331 | return []Vec{first, second}
332 | }
333 | return []Vec{second, first}
334 | }
335 |
--------------------------------------------------------------------------------
/circle_test.go:
--------------------------------------------------------------------------------
1 | package pixel_test
2 |
3 | import (
4 | "math"
5 | "reflect"
6 | "testing"
7 |
8 | "github.com/faiface/pixel"
9 | )
10 |
11 | func TestC(t *testing.T) {
12 | type args struct {
13 | radius float64
14 | center pixel.Vec
15 | }
16 | tests := []struct {
17 | name string
18 | args args
19 | want pixel.Circle
20 | }{
21 | {
22 | name: "C(): positive radius",
23 | args: args{radius: 10, center: pixel.ZV},
24 | want: pixel.Circle{Radius: 10, Center: pixel.ZV},
25 | },
26 | {
27 | name: "C(): zero radius",
28 | args: args{radius: 0, center: pixel.ZV},
29 | want: pixel.Circle{Radius: 0, Center: pixel.ZV},
30 | },
31 | {
32 | name: "C(): negative radius",
33 | args: args{radius: -5, center: pixel.ZV},
34 | want: pixel.Circle{Radius: -5, Center: pixel.ZV},
35 | },
36 | }
37 | for _, tt := range tests {
38 | t.Run(tt.name, func(t *testing.T) {
39 | if got := pixel.C(tt.args.center, tt.args.radius); !reflect.DeepEqual(got, tt.want) {
40 | t.Errorf("C() = %v, want %v", got, tt.want)
41 | }
42 | })
43 | }
44 | }
45 |
46 | func TestCircle_String(t *testing.T) {
47 | type fields struct {
48 | radius float64
49 | center pixel.Vec
50 | }
51 | tests := []struct {
52 | name string
53 | fields fields
54 | want string
55 | }{
56 | {
57 | name: "Circle.String(): positive radius",
58 | fields: fields{radius: 10, center: pixel.ZV},
59 | want: "Circle(Vec(0, 0), 10.00)",
60 | },
61 | {
62 | name: "Circle.String(): zero radius",
63 | fields: fields{radius: 0, center: pixel.ZV},
64 | want: "Circle(Vec(0, 0), 0.00)",
65 | },
66 | {
67 | name: "Circle.String(): negative radius",
68 | fields: fields{radius: -5, center: pixel.ZV},
69 | want: "Circle(Vec(0, 0), -5.00)",
70 | },
71 | {
72 | name: "Circle.String(): irrational radius",
73 | fields: fields{radius: math.Pi, center: pixel.ZV},
74 | want: "Circle(Vec(0, 0), 3.14)",
75 | },
76 | }
77 | for _, tt := range tests {
78 | t.Run(tt.name, func(t *testing.T) {
79 | c := pixel.C(tt.fields.center, tt.fields.radius)
80 | if got := c.String(); got != tt.want {
81 | t.Errorf("Circle.String() = %v, want %v", got, tt.want)
82 | }
83 | })
84 | }
85 | }
86 |
87 | func TestCircle_Norm(t *testing.T) {
88 | type fields struct {
89 | radius float64
90 | center pixel.Vec
91 | }
92 | tests := []struct {
93 | name string
94 | fields fields
95 | want pixel.Circle
96 | }{
97 | {
98 | name: "Circle.Norm(): positive radius",
99 | fields: fields{radius: 10, center: pixel.ZV},
100 | want: pixel.C(pixel.ZV, 10),
101 | },
102 | {
103 | name: "Circle.Norm(): zero radius",
104 | fields: fields{radius: 0, center: pixel.ZV},
105 | want: pixel.C(pixel.ZV, 0),
106 | },
107 | {
108 | name: "Circle.Norm(): negative radius",
109 | fields: fields{radius: -5, center: pixel.ZV},
110 | want: pixel.C(pixel.ZV, 5),
111 | },
112 | }
113 | for _, tt := range tests {
114 | t.Run(tt.name, func(t *testing.T) {
115 | c := pixel.C(tt.fields.center, tt.fields.radius)
116 | if got := c.Norm(); !reflect.DeepEqual(got, tt.want) {
117 | t.Errorf("Circle.Norm() = %v, want %v", got, tt.want)
118 | }
119 | })
120 | }
121 | }
122 |
123 | func TestCircle_Area(t *testing.T) {
124 | type fields struct {
125 | radius float64
126 | center pixel.Vec
127 | }
128 | tests := []struct {
129 | name string
130 | fields fields
131 | want float64
132 | }{
133 | {
134 | name: "Circle.Area(): positive radius",
135 | fields: fields{radius: 10, center: pixel.ZV},
136 | want: 100 * math.Pi,
137 | },
138 | {
139 | name: "Circle.Area(): zero radius",
140 | fields: fields{radius: 0, center: pixel.ZV},
141 | want: 0,
142 | },
143 | {
144 | name: "Circle.Area(): negative radius",
145 | fields: fields{radius: -5, center: pixel.ZV},
146 | want: 25 * math.Pi,
147 | },
148 | }
149 | for _, tt := range tests {
150 | t.Run(tt.name, func(t *testing.T) {
151 | c := pixel.C(tt.fields.center, tt.fields.radius)
152 | if got := c.Area(); got != tt.want {
153 | t.Errorf("Circle.Area() = %v, want %v", got, tt.want)
154 | }
155 | })
156 | }
157 | }
158 |
159 | func TestCircle_Moved(t *testing.T) {
160 | type fields struct {
161 | radius float64
162 | center pixel.Vec
163 | }
164 | type args struct {
165 | delta pixel.Vec
166 | }
167 | tests := []struct {
168 | name string
169 | fields fields
170 | args args
171 | want pixel.Circle
172 | }{
173 | {
174 | name: "Circle.Moved(): positive movement",
175 | fields: fields{radius: 10, center: pixel.ZV},
176 | args: args{delta: pixel.V(10, 20)},
177 | want: pixel.C(pixel.V(10, 20), 10),
178 | },
179 | {
180 | name: "Circle.Moved(): zero movement",
181 | fields: fields{radius: 10, center: pixel.ZV},
182 | args: args{delta: pixel.ZV},
183 | want: pixel.C(pixel.V(0, 0), 10),
184 | },
185 | {
186 | name: "Circle.Moved(): negative movement",
187 | fields: fields{radius: 10, center: pixel.ZV},
188 | args: args{delta: pixel.V(-5, -10)},
189 | want: pixel.C(pixel.V(-5, -10), 10),
190 | },
191 | }
192 | for _, tt := range tests {
193 | t.Run(tt.name, func(t *testing.T) {
194 | c := pixel.C(tt.fields.center, tt.fields.radius)
195 | if got := c.Moved(tt.args.delta); !reflect.DeepEqual(got, tt.want) {
196 | t.Errorf("Circle.Moved() = %v, want %v", got, tt.want)
197 | }
198 | })
199 | }
200 | }
201 |
202 | func TestCircle_Resized(t *testing.T) {
203 | type fields struct {
204 | radius float64
205 | center pixel.Vec
206 | }
207 | type args struct {
208 | radiusDelta float64
209 | }
210 | tests := []struct {
211 | name string
212 | fields fields
213 | args args
214 | want pixel.Circle
215 | }{
216 | {
217 | name: "Circle.Resized(): positive delta",
218 | fields: fields{radius: 10, center: pixel.ZV},
219 | args: args{radiusDelta: 5},
220 | want: pixel.C(pixel.V(0, 0), 15),
221 | },
222 | {
223 | name: "Circle.Resized(): zero delta",
224 | fields: fields{radius: 10, center: pixel.ZV},
225 | args: args{radiusDelta: 0},
226 | want: pixel.C(pixel.V(0, 0), 10),
227 | },
228 | {
229 | name: "Circle.Resized(): negative delta",
230 | fields: fields{radius: 10, center: pixel.ZV},
231 | args: args{radiusDelta: -5},
232 | want: pixel.C(pixel.V(0, 0), 5),
233 | },
234 | }
235 | for _, tt := range tests {
236 | t.Run(tt.name, func(t *testing.T) {
237 | c := pixel.C(tt.fields.center, tt.fields.radius)
238 | if got := c.Resized(tt.args.radiusDelta); !reflect.DeepEqual(got, tt.want) {
239 | t.Errorf("Circle.Resized() = %v, want %v", got, tt.want)
240 | }
241 | })
242 | }
243 | }
244 |
245 | func TestCircle_Contains(t *testing.T) {
246 | type fields struct {
247 | radius float64
248 | center pixel.Vec
249 | }
250 | type args struct {
251 | u pixel.Vec
252 | }
253 | tests := []struct {
254 | name string
255 | fields fields
256 | args args
257 | want bool
258 | }{
259 | {
260 | name: "Circle.Contains(): point on cicles' center",
261 | fields: fields{radius: 10, center: pixel.ZV},
262 | args: args{u: pixel.ZV},
263 | want: true,
264 | },
265 | {
266 | name: "Circle.Contains(): point offcenter",
267 | fields: fields{radius: 10, center: pixel.V(5, 0)},
268 | args: args{u: pixel.ZV},
269 | want: true,
270 | },
271 | {
272 | name: "Circle.Contains(): point on circumference",
273 | fields: fields{radius: 10, center: pixel.V(10, 0)},
274 | args: args{u: pixel.ZV},
275 | want: true,
276 | },
277 | {
278 | name: "Circle.Contains(): point outside circle",
279 | fields: fields{radius: 10, center: pixel.V(15, 0)},
280 | args: args{u: pixel.ZV},
281 | want: false,
282 | },
283 | }
284 | for _, tt := range tests {
285 | t.Run(tt.name, func(t *testing.T) {
286 | c := pixel.C(tt.fields.center, tt.fields.radius)
287 | if got := c.Contains(tt.args.u); got != tt.want {
288 | t.Errorf("Circle.Contains() = %v, want %v", got, tt.want)
289 | }
290 | })
291 | }
292 | }
293 |
294 | func TestCircle_Union(t *testing.T) {
295 | type fields struct {
296 | radius float64
297 | center pixel.Vec
298 | }
299 | type args struct {
300 | d pixel.Circle
301 | }
302 | tests := []struct {
303 | name string
304 | fields fields
305 | args args
306 | want pixel.Circle
307 | }{
308 | {
309 | name: "Circle.Union(): overlapping circles",
310 | fields: fields{radius: 5, center: pixel.ZV},
311 | args: args{d: pixel.C(pixel.ZV, 5)},
312 | want: pixel.C(pixel.ZV, 5),
313 | },
314 | {
315 | name: "Circle.Union(): separate circles",
316 | fields: fields{radius: 1, center: pixel.ZV},
317 | args: args{d: pixel.C(pixel.V(0, 2), 1)},
318 | want: pixel.C(pixel.V(0, 1), 2),
319 | },
320 | }
321 | for _, tt := range tests {
322 | t.Run(tt.name, func(t *testing.T) {
323 | c := pixel.C(tt.fields.center, tt.fields.radius)
324 | if got := c.Union(tt.args.d); !reflect.DeepEqual(got, tt.want) {
325 | t.Errorf("Circle.Union() = %v, want %v", got, tt.want)
326 | }
327 | })
328 | }
329 | }
330 |
331 | func TestCircle_Intersect(t *testing.T) {
332 | type fields struct {
333 | radius float64
334 | center pixel.Vec
335 | }
336 | type args struct {
337 | d pixel.Circle
338 | }
339 | tests := []struct {
340 | name string
341 | fields fields
342 | args args
343 | want pixel.Circle
344 | }{
345 | {
346 | name: "Circle.Intersect(): intersecting circles",
347 | fields: fields{radius: 1, center: pixel.ZV},
348 | args: args{d: pixel.C(pixel.V(1, 0), 1)},
349 | want: pixel.C(pixel.V(0.5, 0), 1),
350 | },
351 | {
352 | name: "Circle.Intersect(): non-intersecting circles",
353 | fields: fields{radius: 1, center: pixel.ZV},
354 | args: args{d: pixel.C(pixel.V(3, 3), 1)},
355 | want: pixel.C(pixel.V(1.5, 1.5), 0),
356 | },
357 | {
358 | name: "Circle.Intersect(): first circle encompassing second",
359 | fields: fields{radius: 10, center: pixel.ZV},
360 | args: args{d: pixel.C(pixel.V(3, 3), 1)},
361 | want: pixel.C(pixel.ZV, 10),
362 | },
363 | {
364 | name: "Circle.Intersect(): second circle encompassing first",
365 | fields: fields{radius: 1, center: pixel.V(-1, -4)},
366 | args: args{d: pixel.C(pixel.ZV, 10)},
367 | want: pixel.C(pixel.ZV, 10),
368 | },
369 | {
370 | name: "Circle.Intersect(): matching circles",
371 | fields: fields{radius: 1, center: pixel.ZV},
372 | args: args{d: pixel.C(pixel.ZV, 1)},
373 | want: pixel.C(pixel.ZV, 1),
374 | },
375 | }
376 | for _, tt := range tests {
377 | t.Run(tt.name, func(t *testing.T) {
378 | c := pixel.C(
379 | tt.fields.center,
380 | tt.fields.radius,
381 | )
382 | if got := c.Intersect(tt.args.d); !reflect.DeepEqual(got, tt.want) {
383 | t.Errorf("Circle.Intersect() = %v, want %v", got, tt.want)
384 | }
385 | })
386 | }
387 | }
388 |
389 | func TestCircle_IntersectPoints(t *testing.T) {
390 | type fields struct {
391 | Center pixel.Vec
392 | Radius float64
393 | }
394 | type args struct {
395 | l pixel.Line
396 | }
397 | tests := []struct {
398 | name string
399 | fields fields
400 | args args
401 | want []pixel.Vec
402 | }{
403 | {
404 | name: "Line intersects circle at two points",
405 | fields: fields{Center: pixel.V(2, 2), Radius: 1},
406 | args: args{pixel.L(pixel.V(0, 0), pixel.V(10, 10))},
407 | want: []pixel.Vec{pixel.V(1.292, 1.292), pixel.V(2.707, 2.707)},
408 | },
409 | {
410 | name: "Line intersects circle at one point",
411 | fields: fields{Center: pixel.V(-0.5, -0.5), Radius: 1},
412 | args: args{pixel.L(pixel.V(0, 0), pixel.V(10, 10))},
413 | want: []pixel.Vec{pixel.V(0.207, 0.207)},
414 | },
415 | {
416 | name: "Line endpoint is circle center",
417 | fields: fields{Center: pixel.V(0, 0), Radius: 1},
418 | args: args{pixel.L(pixel.V(0, 0), pixel.V(10, 10))},
419 | want: []pixel.Vec{pixel.V(0.707, 0.707)},
420 | },
421 | {
422 | name: "Both line endpoints within circle",
423 | fields: fields{Center: pixel.V(0, 0), Radius: 1},
424 | args: args{pixel.L(pixel.V(0.2, 0.2), pixel.V(0.5, 0.5))},
425 | want: []pixel.Vec{},
426 | },
427 | {
428 | name: "Line does not intersect circle",
429 | fields: fields{Center: pixel.V(10, 0), Radius: 1},
430 | args: args{pixel.L(pixel.V(0, 0), pixel.V(10, 10))},
431 | want: []pixel.Vec{},
432 | },
433 | {
434 | name: "Horizontal line intersects circle at two points",
435 | fields: fields{Center: pixel.V(5, 5), Radius: 1},
436 | args: args{pixel.L(pixel.V(0, 5), pixel.V(10, 5))},
437 | want: []pixel.Vec{pixel.V(4, 5), pixel.V(6, 5)},
438 | },
439 | {
440 | name: "Vertical line intersects circle at two points",
441 | fields: fields{Center: pixel.V(5, 5), Radius: 1},
442 | args: args{pixel.L(pixel.V(5, 0), pixel.V(5, 10))},
443 | want: []pixel.Vec{pixel.V(5, 4), pixel.V(5, 6)},
444 | },
445 | {
446 | name: "Left and down line intersects circle at two points",
447 | fields: fields{Center: pixel.V(5, 5), Radius: 1},
448 | args: args{pixel.L(pixel.V(10, 10), pixel.V(0, 0))},
449 | want: []pixel.Vec{pixel.V(5.707, 5.707), pixel.V(4.292, 4.292)},
450 | },
451 | }
452 | for _, tt := range tests {
453 | t.Run(tt.name, func(t *testing.T) {
454 | c := pixel.Circle{
455 | Center: tt.fields.Center,
456 | Radius: tt.fields.Radius,
457 | }
458 | got := c.IntersectionPoints(tt.args.l)
459 | for i, v := range got {
460 | if !closeEnough(v.X, tt.want[i].X, 2) || !closeEnough(v.Y, tt.want[i].Y, 2) {
461 | t.Errorf("Circle.IntersectPoints() = %v, want %v", v, tt.want[i])
462 | }
463 | }
464 | })
465 | }
466 | }
467 |
--------------------------------------------------------------------------------
/color.go:
--------------------------------------------------------------------------------
1 | package pixel
2 |
3 | import "image/color"
4 |
5 | // RGBA represents an alpha-premultiplied RGBA color with components within range [0, 1].
6 | //
7 | // The difference between color.RGBA is that the value range is [0, 1] and the values are floats.
8 | type RGBA struct {
9 | R, G, B, A float64
10 | }
11 |
12 | // RGB returns a fully opaque RGBA color with the given RGB values.
13 | //
14 | // A common way to construct a transparent color is to create one with RGB constructor, then
15 | // multiply it by a color obtained from the Alpha constructor.
16 | func RGB(r, g, b float64) RGBA {
17 | return RGBA{r, g, b, 1}
18 | }
19 |
20 | // Alpha returns a white RGBA color with the given alpha component.
21 | func Alpha(a float64) RGBA {
22 | return RGBA{a, a, a, a}
23 | }
24 |
25 | // Add adds color d to color c component-wise and returns the result (the components are not
26 | // clamped).
27 | func (c RGBA) Add(d RGBA) RGBA {
28 | return RGBA{
29 | R: c.R + d.R,
30 | G: c.G + d.G,
31 | B: c.B + d.B,
32 | A: c.A + d.A,
33 | }
34 | }
35 |
36 | // Sub subtracts color d from color c component-wise and returns the result (the components
37 | // are not clamped).
38 | func (c RGBA) Sub(d RGBA) RGBA {
39 | return RGBA{
40 | R: c.R - d.R,
41 | G: c.G - d.G,
42 | B: c.B - d.B,
43 | A: c.A - d.A,
44 | }
45 | }
46 |
47 | // Mul multiplies color c by color d component-wise (the components are not clamped).
48 | func (c RGBA) Mul(d RGBA) RGBA {
49 | return RGBA{
50 | R: c.R * d.R,
51 | G: c.G * d.G,
52 | B: c.B * d.B,
53 | A: c.A * d.A,
54 | }
55 | }
56 |
57 | // Scaled multiplies each component of color c by scale and returns the result (the components
58 | // are not clamped).
59 | func (c RGBA) Scaled(scale float64) RGBA {
60 | return RGBA{
61 | R: c.R * scale,
62 | G: c.G * scale,
63 | B: c.B * scale,
64 | A: c.A * scale,
65 | }
66 | }
67 |
68 | // RGBA returns alpha-premultiplied red, green, blue and alpha components of the RGBA color.
69 | func (c RGBA) RGBA() (r, g, b, a uint32) {
70 | r = uint32(0xffff * c.R)
71 | g = uint32(0xffff * c.G)
72 | b = uint32(0xffff * c.B)
73 | a = uint32(0xffff * c.A)
74 | return
75 | }
76 |
77 | // ToRGBA converts a color to RGBA format. Using this function is preferred to using RGBAModel, for
78 | // performance (using RGBAModel introduces additional unnecessary allocations).
79 | func ToRGBA(c color.Color) RGBA {
80 | if c, ok := c.(RGBA); ok {
81 | return c
82 | }
83 | r, g, b, a := c.RGBA()
84 | return RGBA{
85 | float64(r) / 0xffff,
86 | float64(g) / 0xffff,
87 | float64(b) / 0xffff,
88 | float64(a) / 0xffff,
89 | }
90 | }
91 |
92 | // RGBAModel converts colors to RGBA format.
93 | var RGBAModel = color.ModelFunc(rgbaModel)
94 |
95 | func rgbaModel(c color.Color) color.Color {
96 | return ToRGBA(c)
97 | }
98 |
--------------------------------------------------------------------------------
/color_test.go:
--------------------------------------------------------------------------------
1 | package pixel_test
2 |
3 | import (
4 | "fmt"
5 | "image/color"
6 | "testing"
7 |
8 | "github.com/faiface/pixel"
9 | )
10 |
11 | func BenchmarkColorToRGBA(b *testing.B) {
12 | types := []color.Color{
13 | color.NRGBA{R: 124, G: 14, B: 230, A: 42}, // slowest
14 | color.RGBA{R: 62, G: 32, B: 14, A: 63}, // faster
15 | pixel.RGB(0.8, 0.2, 0.5).Scaled(0.712), // fastest
16 | }
17 | for _, col := range types {
18 | b.Run(fmt.Sprintf("From %T", col), func(b *testing.B) {
19 | for i := 0; i < b.N; i++ {
20 | _ = pixel.ToRGBA(col)
21 | }
22 | })
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/compose.go:
--------------------------------------------------------------------------------
1 | package pixel
2 |
3 | import "errors"
4 |
5 | // ComposeTarget is a BasicTarget capable of Porter-Duff composition.
6 | type ComposeTarget interface {
7 | BasicTarget
8 |
9 | // SetComposeMethod sets a Porter-Duff composition method to be used.
10 | SetComposeMethod(ComposeMethod)
11 | }
12 |
13 | // ComposeMethod is a Porter-Duff composition method.
14 | type ComposeMethod int
15 |
16 | // Here's the list of all available Porter-Duff composition methods. Use ComposeOver for the basic
17 | // alpha blending.
18 | const (
19 | ComposeOver ComposeMethod = iota
20 | ComposeIn
21 | ComposeOut
22 | ComposeAtop
23 | ComposeRover
24 | ComposeRin
25 | ComposeRout
26 | ComposeRatop
27 | ComposeXor
28 | ComposePlus
29 | ComposeCopy
30 | )
31 |
32 | // Compose composes two colors together according to the ComposeMethod. A is the foreground, B is
33 | // the background.
34 | func (cm ComposeMethod) Compose(a, b RGBA) RGBA {
35 | var fa, fb float64
36 |
37 | switch cm {
38 | case ComposeOver:
39 | fa, fb = 1, 1-a.A
40 | case ComposeIn:
41 | fa, fb = b.A, 0
42 | case ComposeOut:
43 | fa, fb = 1-b.A, 0
44 | case ComposeAtop:
45 | fa, fb = b.A, 1-a.A
46 | case ComposeRover:
47 | fa, fb = 1-b.A, 1
48 | case ComposeRin:
49 | fa, fb = 0, a.A
50 | case ComposeRout:
51 | fa, fb = 0, 1-a.A
52 | case ComposeRatop:
53 | fa, fb = 1-b.A, a.A
54 | case ComposeXor:
55 | fa, fb = 1-b.A, 1-a.A
56 | case ComposePlus:
57 | fa, fb = 1, 1
58 | case ComposeCopy:
59 | fa, fb = 1, 0
60 | default:
61 | panic(errors.New("Compose: invalid ComposeMethod"))
62 | }
63 |
64 | return a.Mul(Alpha(fa)).Add(b.Mul(Alpha(fb)))
65 | }
66 |
--------------------------------------------------------------------------------
/data.go:
--------------------------------------------------------------------------------
1 | package pixel
2 |
3 | import (
4 | "fmt"
5 | "image"
6 | "image/color"
7 | "image/draw"
8 | "math"
9 | )
10 |
11 | // zeroValueTriangleData is the default value of a TriangleData element
12 | var zeroValueTriangleData = struct {
13 | Position Vec
14 | Color RGBA
15 | Picture Vec
16 | Intensity float64
17 | ClipRect Rect
18 | IsClipped bool
19 | }{Color: RGBA{1, 1, 1, 1}}
20 |
21 | // TrianglesData specifies a list of Triangles vertices with three common properties:
22 | // TrianglesPosition, TrianglesColor and TrianglesPicture.
23 | type TrianglesData []struct {
24 | Position Vec
25 | Color RGBA
26 | Picture Vec
27 | Intensity float64
28 | ClipRect Rect
29 | IsClipped bool
30 | }
31 |
32 | // MakeTrianglesData creates TrianglesData of length len initialized with default property values.
33 | //
34 | // Prefer this function to make(TrianglesData, len), because make zeros them, while this function
35 | // does the correct intialization.
36 | func MakeTrianglesData(len int) *TrianglesData {
37 | td := make(TrianglesData, len)
38 | for i := 0; i < len; i++ {
39 | td[i] = zeroValueTriangleData
40 | }
41 | return &td
42 | }
43 |
44 | // Len returns the number of vertices in TrianglesData.
45 | func (td *TrianglesData) Len() int {
46 | return len(*td)
47 | }
48 |
49 | // SetLen resizes TrianglesData to len, while keeping the original content.
50 | //
51 | // If len is greater than TrianglesData's current length, the new data is filled with default
52 | // values ((0, 0), white, (0, 0), 0).
53 | func (td *TrianglesData) SetLen(len int) {
54 | if len > td.Len() {
55 | needAppend := len - td.Len()
56 | for i := 0; i < needAppend; i++ {
57 | *td = append(*td, zeroValueTriangleData)
58 | }
59 | }
60 | if len < td.Len() {
61 | *td = (*td)[:len]
62 | }
63 | }
64 |
65 | // Slice returns a sub-Triangles of this TrianglesData.
66 | func (td *TrianglesData) Slice(i, j int) Triangles {
67 | s := TrianglesData((*td)[i:j])
68 | return &s
69 | }
70 |
71 | func (td *TrianglesData) updateData(t Triangles) {
72 | // fast path optimization
73 | if t, ok := t.(*TrianglesData); ok {
74 | copy(*td, *t)
75 | return
76 | }
77 |
78 | // slow path manual copy
79 | if t, ok := t.(TrianglesPosition); ok {
80 | for i := range *td {
81 | (*td)[i].Position = t.Position(i)
82 | }
83 | }
84 | if t, ok := t.(TrianglesColor); ok {
85 | for i := range *td {
86 | (*td)[i].Color = t.Color(i)
87 | }
88 | }
89 | if t, ok := t.(TrianglesPicture); ok {
90 | for i := range *td {
91 | (*td)[i].Picture, (*td)[i].Intensity = t.Picture(i)
92 | }
93 | }
94 | if t, ok := t.(TrianglesClipped); ok {
95 | for i := range *td {
96 | (*td)[i].ClipRect, (*td)[i].IsClipped = t.ClipRect(i)
97 | }
98 | }
99 | }
100 |
101 | // Update copies vertex properties from the supplied Triangles into this TrianglesData.
102 | //
103 | // TrianglesPosition, TrianglesColor and TrianglesTexture are supported.
104 | func (td *TrianglesData) Update(t Triangles) {
105 | if td.Len() != t.Len() {
106 | panic(fmt.Errorf("(%T).Update: invalid triangles length", td))
107 | }
108 | td.updateData(t)
109 | }
110 |
111 | // Copy returns an exact independent copy of this TrianglesData.
112 | func (td *TrianglesData) Copy() Triangles {
113 | copyTd := MakeTrianglesData(td.Len())
114 | copyTd.Update(td)
115 | return copyTd
116 | }
117 |
118 | // Position returns the position property of i-th vertex.
119 | func (td *TrianglesData) Position(i int) Vec {
120 | return (*td)[i].Position
121 | }
122 |
123 | // Color returns the color property of i-th vertex.
124 | func (td *TrianglesData) Color(i int) RGBA {
125 | return (*td)[i].Color
126 | }
127 |
128 | // Picture returns the picture property of i-th vertex.
129 | func (td *TrianglesData) Picture(i int) (pic Vec, intensity float64) {
130 | return (*td)[i].Picture, (*td)[i].Intensity
131 | }
132 |
133 | // ClipRect returns the clipping rectangle property of the i-th vertex.
134 | func (td *TrianglesData) ClipRect(i int) (rect Rect, has bool) {
135 | return (*td)[i].ClipRect, (*td)[i].IsClipped
136 | }
137 |
138 | // PictureData specifies an in-memory rectangular area of pixels and implements Picture and
139 | // PictureColor.
140 | //
141 | // Pixels are small rectangles of unit size of form (x, y, x+1, y+1), where x and y are integers.
142 | // PictureData contains and assigns a color to all pixels that are at least partially contained
143 | // within it's Bounds (Rect).
144 | //
145 | // The struct's innards are exposed for convenience, manual modification is at your own risk.
146 | //
147 | // The format of the pixels is color.RGBA and not pixel.RGBA for a very serious reason:
148 | // pixel.RGBA takes up 8x more memory than color.RGBA.
149 | type PictureData struct {
150 | Pix []color.RGBA
151 | Stride int
152 | Rect Rect
153 | }
154 |
155 | // MakePictureData creates a zero-initialized PictureData covering the given rectangle.
156 | func MakePictureData(rect Rect) *PictureData {
157 | w := int(math.Ceil(rect.Max.X)) - int(math.Floor(rect.Min.X))
158 | h := int(math.Ceil(rect.Max.Y)) - int(math.Floor(rect.Min.Y))
159 | pd := &PictureData{
160 | Stride: w,
161 | Rect: rect,
162 | }
163 | pd.Pix = make([]color.RGBA, w*h)
164 | return pd
165 | }
166 |
167 | func verticalFlip(rgba *image.RGBA) {
168 | bounds := rgba.Bounds()
169 | width := bounds.Dx()
170 |
171 | tmpRow := make([]uint8, width*4)
172 | for i, j := 0, bounds.Dy()-1; i < j; i, j = i+1, j-1 {
173 | iRow := rgba.Pix[i*rgba.Stride : i*rgba.Stride+width*4]
174 | jRow := rgba.Pix[j*rgba.Stride : j*rgba.Stride+width*4]
175 |
176 | copy(tmpRow, iRow)
177 | copy(iRow, jRow)
178 | copy(jRow, tmpRow)
179 | }
180 | }
181 |
182 | // PictureDataFromImage converts an image.Image into PictureData.
183 | //
184 | // The resulting PictureData's Bounds will be the equivalent of the supplied image.Image's Bounds.
185 | func PictureDataFromImage(img image.Image) *PictureData {
186 | rgba := image.NewRGBA(img.Bounds())
187 | draw.Draw(rgba, rgba.Bounds(), img, img.Bounds().Min, draw.Src)
188 |
189 | verticalFlip(rgba)
190 |
191 | pd := MakePictureData(R(
192 | float64(rgba.Bounds().Min.X),
193 | float64(rgba.Bounds().Min.Y),
194 | float64(rgba.Bounds().Max.X),
195 | float64(rgba.Bounds().Max.Y),
196 | ))
197 |
198 | for i := range pd.Pix {
199 | pd.Pix[i].R = rgba.Pix[i*4+0]
200 | pd.Pix[i].G = rgba.Pix[i*4+1]
201 | pd.Pix[i].B = rgba.Pix[i*4+2]
202 | pd.Pix[i].A = rgba.Pix[i*4+3]
203 | }
204 |
205 | return pd
206 | }
207 |
208 | // PictureDataFromPicture converts an arbitrary Picture into PictureData (the conversion may be
209 | // lossy, because PictureData works with unit-sized pixels).
210 | //
211 | // Bounds are preserved.
212 | func PictureDataFromPicture(pic Picture) *PictureData {
213 | if pd, ok := pic.(*PictureData); ok {
214 | return pd
215 | }
216 |
217 | bounds := pic.Bounds()
218 | pd := MakePictureData(bounds)
219 |
220 | if pic, ok := pic.(PictureColor); ok {
221 | for y := math.Floor(bounds.Min.Y); y < bounds.Max.Y; y++ {
222 | for x := math.Floor(bounds.Min.X); x < bounds.Max.X; x++ {
223 | // this together with the Floor is a trick to get all of the pixels
224 | at := V(
225 | math.Max(x, bounds.Min.X),
226 | math.Max(y, bounds.Min.Y),
227 | )
228 | col := pic.Color(at)
229 | pd.Pix[pd.Index(at)] = color.RGBA{
230 | R: uint8(col.R * 255),
231 | G: uint8(col.G * 255),
232 | B: uint8(col.B * 255),
233 | A: uint8(col.A * 255),
234 | }
235 | }
236 | }
237 | }
238 |
239 | return pd
240 | }
241 |
242 | // Image converts PictureData into an image.RGBA.
243 | //
244 | // The resulting image.RGBA's Bounds will be equivalent of the PictureData's Bounds.
245 | func (pd *PictureData) Image() *image.RGBA {
246 | bounds := image.Rect(
247 | int(math.Floor(pd.Rect.Min.X)),
248 | int(math.Floor(pd.Rect.Min.Y)),
249 | int(math.Ceil(pd.Rect.Max.X)),
250 | int(math.Ceil(pd.Rect.Max.Y)),
251 | )
252 | rgba := image.NewRGBA(bounds)
253 |
254 | i := 0
255 | for y := bounds.Min.Y; y < bounds.Max.Y; y++ {
256 | for x := bounds.Min.X; x < bounds.Max.X; x++ {
257 | off := pd.Index(V(float64(x), float64(y)))
258 | rgba.Pix[i*4+0] = pd.Pix[off].R
259 | rgba.Pix[i*4+1] = pd.Pix[off].G
260 | rgba.Pix[i*4+2] = pd.Pix[off].B
261 | rgba.Pix[i*4+3] = pd.Pix[off].A
262 | i++
263 | }
264 | }
265 |
266 | verticalFlip(rgba)
267 |
268 | return rgba
269 | }
270 |
271 | // Index returns the index of the pixel at the specified position inside the Pix slice.
272 | func (pd *PictureData) Index(at Vec) int {
273 | at = at.Sub(pd.Rect.Min.Map(math.Floor))
274 | x, y := int(at.X), int(at.Y)
275 | return y*pd.Stride + x
276 | }
277 |
278 | // Bounds returns the bounds of this PictureData.
279 | func (pd *PictureData) Bounds() Rect {
280 | return pd.Rect
281 | }
282 |
283 | // Color returns the color located at the given position.
284 | func (pd *PictureData) Color(at Vec) RGBA {
285 | if !pd.Rect.Contains(at) {
286 | return RGBA{0, 0, 0, 0}
287 | }
288 | return ToRGBA(pd.Pix[pd.Index(at)])
289 | }
290 |
--------------------------------------------------------------------------------
/data_test.go:
--------------------------------------------------------------------------------
1 | package pixel_test
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/faiface/pixel"
7 | )
8 |
9 | func BenchmarkMakeTrianglesData(b *testing.B) {
10 | tests := []struct {
11 | name string
12 | len int
13 | }{
14 | {
15 | name: "Small slice",
16 | len: 10,
17 | },
18 | {
19 | name: "Large slice",
20 | len: 10000,
21 | },
22 | }
23 |
24 | for _, tt := range tests {
25 | b.Run(tt.name, func(b *testing.B) {
26 | for i := 0; i < b.N; i++ {
27 | _ = pixel.MakeTrianglesData(tt.len)
28 | }
29 | })
30 | }
31 | }
32 |
33 | func BenchmarkTrianglesData_Len(b *testing.B) {
34 | tests := []struct {
35 | name string
36 | tData *pixel.TrianglesData
37 | }{
38 | {
39 | name: "Small slice",
40 | tData: pixel.MakeTrianglesData(10),
41 | },
42 | {
43 | name: "Large slice",
44 | tData: pixel.MakeTrianglesData(10000),
45 | },
46 | }
47 |
48 | for _, tt := range tests {
49 | b.Run(tt.name, func(b *testing.B) {
50 | for i := 0; i < b.N; i++ {
51 | _ = tt.tData.Len()
52 | }
53 | })
54 | }
55 | }
56 |
57 | func BenchmarkTrianglesData_SetLen(b *testing.B) {
58 | tests := []struct {
59 | name string
60 | tData *pixel.TrianglesData
61 | nextLenFunc func(int, int) (int, int)
62 | }{
63 | {
64 | name: "Stay same size",
65 | tData: pixel.MakeTrianglesData(50),
66 | nextLenFunc: func(i, j int) (int, int) { return 50, 0 },
67 | },
68 | {
69 | name: "Change size",
70 | tData: pixel.MakeTrianglesData(50),
71 | nextLenFunc: func(i, j int) (int, int) {
72 | // 0 is shrink
73 | if j == 0 {
74 | next := i - 1
75 | if next < 1 {
76 | return 2, 1
77 | }
78 | return next, 0
79 | }
80 |
81 | // other than 0 is grow
82 | next := i + 1
83 | if next == 100 {
84 | return next, 0
85 | }
86 | return next, 1
87 | },
88 | },
89 | }
90 |
91 | for _, tt := range tests {
92 | b.Run(tt.name, func(b *testing.B) {
93 | var newLen int
94 | var c int
95 | for i := 0; i < b.N; i++ {
96 | newLen, c = tt.nextLenFunc(newLen, c)
97 | tt.tData.SetLen(newLen)
98 | }
99 | })
100 | }
101 | }
102 |
103 | func BenchmarkTrianglesData_Slice(b *testing.B) {
104 | tests := []struct {
105 | name string
106 | tData *pixel.TrianglesData
107 | }{
108 | {
109 | name: "Basic slice",
110 | tData: pixel.MakeTrianglesData(100),
111 | },
112 | }
113 |
114 | for _, tt := range tests {
115 | b.Run(tt.name, func(b *testing.B) {
116 | for i := 0; i < b.N; i++ {
117 | _ = tt.tData.Slice(25, 50)
118 | }
119 | })
120 | }
121 | }
122 |
123 | func BenchmarkTrianglesData_Update(b *testing.B) {
124 | tests := []struct {
125 | name string
126 | tData *pixel.TrianglesData
127 | t pixel.Triangles
128 | }{
129 | {
130 | name: "Small Triangles",
131 | tData: pixel.MakeTrianglesData(20),
132 | t: pixel.MakeTrianglesData(20),
133 | },
134 | {
135 | name: "Large Triangles",
136 | tData: pixel.MakeTrianglesData(10000),
137 | t: pixel.MakeTrianglesData(10000),
138 | },
139 | }
140 |
141 | for _, tt := range tests {
142 | b.Run(tt.name, func(b *testing.B) {
143 | for i := 0; i < b.N; i++ {
144 | tt.tData.Update(tt.t)
145 | }
146 | })
147 | }
148 | }
149 |
150 | func BenchmarkTrianglesData_Copy(b *testing.B) {
151 | tests := []struct {
152 | name string
153 | tData *pixel.TrianglesData
154 | }{
155 | {
156 | name: "Small copy",
157 | tData: pixel.MakeTrianglesData(20),
158 | },
159 | {
160 | name: "Large copy",
161 | tData: pixel.MakeTrianglesData(10000),
162 | },
163 | }
164 |
165 | for _, tt := range tests {
166 | b.Run(tt.name, func(b *testing.B) {
167 | for i := 0; i < b.N; i++ {
168 | _ = tt.tData.Copy()
169 | }
170 | })
171 | }
172 | }
173 |
174 | func BenchmarkTrianglesData_Position(b *testing.B) {
175 | tests := []struct {
176 | name string
177 | tData *pixel.TrianglesData
178 | position int
179 | }{
180 | {
181 | name: "Getting beginning position",
182 | tData: pixel.MakeTrianglesData(1000),
183 | position: 2,
184 | },
185 | {
186 | name: "Getting middle position",
187 | tData: pixel.MakeTrianglesData(1000),
188 | position: 500,
189 | },
190 | {
191 | name: "Getting end position",
192 | tData: pixel.MakeTrianglesData(1000),
193 | position: 999,
194 | },
195 | }
196 |
197 | for _, tt := range tests {
198 | b.Run(tt.name, func(b *testing.B) {
199 | for i := 0; i < b.N; i++ {
200 | _ = tt.tData.Position(tt.position)
201 | }
202 | })
203 | }
204 | }
205 |
206 | func BenchmarkTrianglesData_Color(b *testing.B) {
207 | tests := []struct {
208 | name string
209 | tData *pixel.TrianglesData
210 | position int
211 | }{
212 | {
213 | name: "Getting beginning position",
214 | tData: pixel.MakeTrianglesData(1000),
215 | position: 2,
216 | },
217 | {
218 | name: "Getting middle position",
219 | tData: pixel.MakeTrianglesData(1000),
220 | position: 500,
221 | },
222 | {
223 | name: "Getting end position",
224 | tData: pixel.MakeTrianglesData(1000),
225 | position: 999,
226 | },
227 | }
228 |
229 | for _, tt := range tests {
230 | b.Run(tt.name, func(b *testing.B) {
231 | for i := 0; i < b.N; i++ {
232 | _ = tt.tData.Color(tt.position)
233 | }
234 | })
235 | }
236 | }
237 |
238 | func BenchmarkTrianglesData_Picture(b *testing.B) {
239 | tests := []struct {
240 | name string
241 | tData *pixel.TrianglesData
242 | position int
243 | }{
244 | {
245 | name: "Getting beginning position",
246 | tData: pixel.MakeTrianglesData(1000),
247 | position: 2,
248 | },
249 | {
250 | name: "Getting middle position",
251 | tData: pixel.MakeTrianglesData(1000),
252 | position: 500,
253 | },
254 | {
255 | name: "Getting end position",
256 | tData: pixel.MakeTrianglesData(1000),
257 | position: 999,
258 | },
259 | }
260 |
261 | for _, tt := range tests {
262 | b.Run(tt.name, func(b *testing.B) {
263 | for i := 0; i < b.N; i++ {
264 | _, _ = tt.tData.Picture(tt.position)
265 | }
266 | })
267 | }
268 | }
269 |
270 | func BenchmarkTrianglesData_ClipRect(b *testing.B) {
271 | tests := []struct {
272 | name string
273 | tData *pixel.TrianglesData
274 | position int
275 | }{
276 | {
277 | name: "Getting beginning position",
278 | tData: pixel.MakeTrianglesData(1000),
279 | position: 2,
280 | },
281 | {
282 | name: "Getting middle position",
283 | tData: pixel.MakeTrianglesData(1000),
284 | position: 500,
285 | },
286 | {
287 | name: "Getting end position",
288 | tData: pixel.MakeTrianglesData(1000),
289 | position: 999,
290 | },
291 | }
292 |
293 | for _, tt := range tests {
294 | b.Run(tt.name, func(b *testing.B) {
295 | for i := 0; i < b.N; i++ {
296 | _, _ = tt.tData.ClipRect(tt.position)
297 | }
298 | })
299 | }
300 | }
301 |
--------------------------------------------------------------------------------
/doc.go:
--------------------------------------------------------------------------------
1 | // Package pixel implements platform and backend agnostic core of the Pixel game development
2 | // library.
3 | //
4 | // It specifies the core Target, Triangles, Picture pattern and implements standard elements, such
5 | // as Sprite, Batch, Vec, Matrix and RGBA in addition to the basic Triangles and Picture
6 | // implementations: TrianglesData and PictureData.
7 | package pixel
8 |
--------------------------------------------------------------------------------
/drawer.go:
--------------------------------------------------------------------------------
1 | package pixel
2 |
3 | // Drawer glues all the fundamental interfaces (Target, Triangles, Picture) into a coherent and the
4 | // only intended usage pattern.
5 | //
6 | // Drawer makes it possible to draw any combination of Triangles and Picture onto any Target
7 | // efficiently.
8 | //
9 | // To create a Drawer, just assign it's Triangles and Picture fields:
10 | //
11 | // d := pixel.Drawer{Triangles: t, Picture: p}
12 | //
13 | // If Triangles is nil, nothing will be drawn. If Picture is nil, Triangles will be drawn without a
14 | // Picture.
15 | //
16 | // Whenever you change the Triangles, call Dirty to notify Drawer that Triangles changed. You don't
17 | // need to notify Drawer about a change of the Picture.
18 | //
19 | // Note, that Drawer caches the results of MakePicture from Targets it's drawn to for each Picture
20 | // it's set to. What it means is that using a Drawer with an unbounded number of Pictures leads to a
21 | // memory leak, since Drawer caches them and never forgets. In such a situation, create a new Drawer
22 | // for each Picture.
23 | type Drawer struct {
24 | Triangles Triangles
25 | Picture Picture
26 | Cached bool
27 |
28 | targets map[Target]*drawerTarget
29 | allTargets []*drawerTarget
30 | inited bool
31 | }
32 |
33 | type drawerTarget struct {
34 | tris TargetTriangles
35 | pics map[Picture]TargetPicture
36 | clean bool
37 | }
38 |
39 | func (d *Drawer) lazyInit() {
40 | if !d.inited {
41 | d.targets = make(map[Target]*drawerTarget)
42 | d.inited = true
43 | }
44 | }
45 |
46 | // Dirty marks the Triangles of this Drawer as changed. If not called, changes will not be visible
47 | // when drawing.
48 | func (d *Drawer) Dirty() {
49 | d.lazyInit()
50 |
51 | for _, t := range d.allTargets {
52 | t.clean = false
53 | }
54 | }
55 |
56 | // Draw efficiently draws Triangles with Picture onto the provided Target.
57 | //
58 | // If Triangles is nil, nothing will be drawn. If Picture is nil, Triangles will be drawn without a
59 | // Picture.
60 | func (d *Drawer) Draw(t Target) {
61 | d.lazyInit()
62 |
63 | if d.Triangles == nil {
64 | return
65 | }
66 |
67 | dt := d.targets[t]
68 | if dt == nil {
69 | dt = &drawerTarget{
70 | pics: make(map[Picture]TargetPicture),
71 | }
72 | d.targets[t] = dt
73 | d.allTargets = append(d.allTargets, dt)
74 | }
75 |
76 | if dt.tris == nil {
77 | dt.tris = t.MakeTriangles(d.Triangles)
78 | dt.clean = true
79 | }
80 |
81 | if !dt.clean {
82 | dt.tris.SetLen(d.Triangles.Len())
83 | dt.tris.Update(d.Triangles)
84 | dt.clean = true
85 | }
86 |
87 | if d.Picture == nil {
88 | dt.tris.Draw()
89 | return
90 | }
91 |
92 | pic := dt.pics[d.Picture]
93 | if pic == nil {
94 | pic = t.MakePicture(d.Picture)
95 |
96 | if d.Cached {
97 | dt.pics[d.Picture] = pic
98 | }
99 | }
100 |
101 | pic.Draw(dt.tris)
102 | }
103 |
--------------------------------------------------------------------------------
/drawer_test.go:
--------------------------------------------------------------------------------
1 | package pixel_test
2 |
3 | import (
4 | "image"
5 | "testing"
6 |
7 | "github.com/faiface/pixel"
8 | )
9 |
10 | func BenchmarkSpriteDrawBatch(b *testing.B) {
11 | img := image.NewRGBA(image.Rect(0, 0, 64, 64))
12 | pic := pixel.PictureDataFromImage(img)
13 | sprite := pixel.NewSprite(pic, pixel.R(0, 0, 64, 64))
14 | batch := pixel.NewBatch(&pixel.TrianglesData{}, pic)
15 | for i := 0; i < b.N; i++ {
16 | sprite.Draw(batch, pixel.IM)
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/faiface/pixel
2 |
3 | go 1.12
4 |
5 | require (
6 | github.com/faiface/glhf v0.0.0-20211013000516-57b20770c369
7 | github.com/faiface/mainthread v0.0.0-20171120011319-8b78f0a41ae3
8 | github.com/go-gl/gl v0.0.0-20210905235341-f7a045908259
9 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20210727001814-0db043d8d5be
10 | github.com/go-gl/mathgl v1.0.0
11 | github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0
12 | github.com/pkg/errors v0.9.1
13 | github.com/stretchr/testify v1.7.0
14 | golang.org/x/image v0.5.0
15 | )
16 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
2 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
3 | github.com/faiface/glhf v0.0.0-20211013000516-57b20770c369 h1:gv4BgP50atccdK/1tZHDyP6rMwiiutR2HPreR/OyLzI=
4 | github.com/faiface/glhf v0.0.0-20211013000516-57b20770c369/go.mod h1:dDdUO+G9ZnJ9sc8nIUvhLkE45k8PEKW6+A3TdWsfpV0=
5 | github.com/faiface/mainthread v0.0.0-20171120011319-8b78f0a41ae3 h1:baVdMKlASEHrj19iqjARrPbaRisD7EuZEVJj6ZMLl1Q=
6 | github.com/faiface/mainthread v0.0.0-20171120011319-8b78f0a41ae3/go.mod h1:VEPNJUlxl5KdWjDvz6Q1l+rJlxF2i6xqDeGuGAxa87M=
7 | github.com/go-gl/gl v0.0.0-20210905235341-f7a045908259 h1:8q7+xl2D2qHPLTII1t4vSMNP2VKwDcn+Avf2WXvdB1A=
8 | github.com/go-gl/gl v0.0.0-20210905235341-f7a045908259/go.mod h1:wjpnOv6ONl2SuJSxqCPVaPZibGFdSci9HFocT9qtVYM=
9 | github.com/go-gl/glfw v0.0.0-20210727001814-0db043d8d5be h1:UVW91pfMB1GRQfVwC7//RGVbqX6Ea8jURmJhlANak1M=
10 | github.com/go-gl/glfw v0.0.0-20210727001814-0db043d8d5be/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
11 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20210727001814-0db043d8d5be h1:vEIVIuBApEBQTEJt19GfhoU+zFSV+sNTa9E9FdnRYfk=
12 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20210727001814-0db043d8d5be/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
13 | github.com/go-gl/mathgl v1.0.0 h1:t9DznWJlXxxjeeKLIdovCOVJQk/GzDEL7h/h+Ro2B68=
14 | github.com/go-gl/mathgl v1.0.0/go.mod h1:yhpkQzEiH9yPyxDUGzkmgScbaBVlhC06qodikEM0ZwQ=
15 | github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
16 | github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
17 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
18 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
19 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
20 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
21 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
22 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
23 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
24 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
25 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
26 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
27 | golang.org/x/image v0.0.0-20190321063152-3fc05d484e9f/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
28 | golang.org/x/image v0.5.0 h1:5JMiNunQeQw++mMOz48/ISeNu3Iweh/JaZU8ZLqHRrI=
29 | golang.org/x/image v0.5.0/go.mod h1:FVC7BI/5Ym8R25iw5OLsgshdUBbT1h5jZTpA+mvAdZ4=
30 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
31 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
32 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
33 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
34 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
35 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
36 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
37 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
38 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
39 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
40 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
41 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
42 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
43 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
44 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
45 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
46 | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
47 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
48 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
49 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
50 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
51 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
52 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
53 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
54 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
55 |
--------------------------------------------------------------------------------
/imdraw/imdraw_test.go:
--------------------------------------------------------------------------------
1 | package imdraw_test
2 |
3 | import (
4 | "fmt"
5 | "math/rand"
6 | "testing"
7 |
8 | "github.com/faiface/pixel"
9 | "github.com/faiface/pixel/imdraw"
10 | )
11 |
12 | func BenchmarkPush(b *testing.B) {
13 | imd := imdraw.New(nil)
14 | for i := 0; i < b.N; i++ {
15 | imd.Push(pixel.V(123.1, 99.4))
16 | }
17 | }
18 |
19 | func pointLists(counts ...int) [][]pixel.Vec {
20 | lists := make([][]pixel.Vec, len(counts))
21 | for i := range lists {
22 | lists[i] = make([]pixel.Vec, counts[i])
23 | for j := range lists[i] {
24 | lists[i][j] = pixel.V(
25 | rand.Float64()*5000-2500,
26 | rand.Float64()*5000-2500,
27 | )
28 | }
29 | }
30 | return lists
31 | }
32 |
33 | func BenchmarkLine(b *testing.B) {
34 | lists := pointLists(2, 5, 10, 100, 1000)
35 | for _, pts := range lists {
36 | b.Run(fmt.Sprintf("%d", len(pts)), func(b *testing.B) {
37 | imd := imdraw.New(nil)
38 | for i := 0; i < b.N; i++ {
39 | imd.Push(pts...)
40 | imd.Line(1)
41 | }
42 | })
43 | }
44 | }
45 |
46 | func BenchmarkRectangle(b *testing.B) {
47 | lists := pointLists(2, 10, 100, 1000)
48 | for _, pts := range lists {
49 | b.Run(fmt.Sprintf("%d", len(pts)), func(b *testing.B) {
50 | imd := imdraw.New(nil)
51 | for i := 0; i < b.N; i++ {
52 | imd.Push(pts...)
53 | imd.Rectangle(0)
54 | }
55 | })
56 | }
57 | }
58 |
59 | func BenchmarkPolygon(b *testing.B) {
60 | lists := pointLists(3, 10, 100, 1000)
61 | for _, pts := range lists {
62 | b.Run(fmt.Sprintf("%d", len(pts)), func(b *testing.B) {
63 | imd := imdraw.New(nil)
64 | for i := 0; i < b.N; i++ {
65 | imd.Push(pts...)
66 | imd.Polygon(0)
67 | }
68 | })
69 | }
70 | }
71 |
72 | func BenchmarkEllipseFill(b *testing.B) {
73 | lists := pointLists(1, 10, 100, 1000)
74 | for _, pts := range lists {
75 | b.Run(fmt.Sprintf("%d", len(pts)), func(b *testing.B) {
76 | imd := imdraw.New(nil)
77 | for i := 0; i < b.N; i++ {
78 | imd.Push(pts...)
79 | imd.Ellipse(pixel.V(50, 100), 0)
80 | }
81 | })
82 | }
83 | }
84 |
85 | func BenchmarkEllipseOutline(b *testing.B) {
86 | lists := pointLists(1, 10, 100, 1000)
87 | for _, pts := range lists {
88 | b.Run(fmt.Sprintf("%d", len(pts)), func(b *testing.B) {
89 | imd := imdraw.New(nil)
90 | for i := 0; i < b.N; i++ {
91 | imd.Push(pts...)
92 | imd.Ellipse(pixel.V(50, 100), 1)
93 | }
94 | })
95 | }
96 | }
97 |
--------------------------------------------------------------------------------
/interface.go:
--------------------------------------------------------------------------------
1 | package pixel
2 |
3 | import "image/color"
4 |
5 | // Target is something that can be drawn onto, such as a window, a canvas, and so on.
6 | //
7 | // You can notice, that there are no "drawing" methods in a Target. That's because all drawing
8 | // happens indirectly through Triangles and Picture instances generated via MakeTriangles and
9 | // MakePicture method.
10 | type Target interface {
11 | // MakeTriangles generates a specialized copy of the provided Triangles.
12 | //
13 | // When calling Draw method on the returned TargetTriangles, the TargetTriangles will be
14 | // drawn onto the Target that generated them.
15 | //
16 | // Note, that not every Target has to recognize all possible types of Triangles. Some may
17 | // only recognize TrianglesPosition and TrianglesColor and ignore all other properties (if
18 | // present) when making new TargetTriangles. This varies from Target to Target.
19 | MakeTriangles(Triangles) TargetTriangles
20 |
21 | // MakePicture generates a specialized copy of the provided Picture.
22 | //
23 | // When calling Draw method on the returned TargetPicture, the TargetPicture will be drawn
24 | // onto the Target that generated it together with the TargetTriangles supplied to the Draw
25 | // method.
26 | MakePicture(Picture) TargetPicture
27 | }
28 |
29 | // BasicTarget is a Target with additional basic adjustment methods.
30 | type BasicTarget interface {
31 | Target
32 |
33 | // SetMatrix sets a Matrix that every point will be projected by.
34 | SetMatrix(Matrix)
35 |
36 | // SetColorMask sets a color that will be multiplied with the TrianglesColor property of all
37 | // Triangles.
38 | SetColorMask(color.Color)
39 | }
40 |
41 | // Triangles represents a list of vertices, where each three vertices form a triangle. (First,
42 | // second and third is the first triangle, fourth, fifth and sixth is the second triangle, etc.)
43 | type Triangles interface {
44 | // Len returns the number of vertices. The number of triangles is the number of vertices
45 | // divided by 3.
46 | Len() int
47 |
48 | // SetLen resizes Triangles to len vertices. If Triangles B were obtained by calling Slice
49 | // method on Triangles A, the relationship between A and B is undefined after calling SetLen
50 | // on either one of them.
51 | SetLen(len int)
52 |
53 | // Slice returns a sub-Triangles of this Triangles, covering vertices in range [i, j).
54 | //
55 | // If Triangles B were obtained by calling Slice(4, 9) on Triangles A, then A and B must
56 | // share the same underlying data. Modifying B must change the contents of A in range
57 | // [4, 9). The vertex with index 0 at B is the vertex with index 4 in A, and so on.
58 | //
59 | // Returned Triangles must have the same underlying type.
60 | Slice(i, j int) Triangles
61 |
62 | // Update copies vertex properties from the supplied Triangles into this Triangles.
63 | //
64 | // Properies not supported by these Triangles should be ignored. Properties not supported by
65 | // the supplied Triangles should be left untouched.
66 | //
67 | // The two Triangles must have the same Len.
68 | Update(Triangles)
69 |
70 | // Copy creates an exact independent copy of this Triangles (with the same underlying type).
71 | Copy() Triangles
72 | }
73 |
74 | // TargetTriangles are Triangles generated by a Target with MakeTriangles method. They can be drawn
75 | // onto that (no other) Target.
76 | type TargetTriangles interface {
77 | Triangles
78 |
79 | // Draw draws Triangles onto an associated Target.
80 | Draw()
81 | }
82 |
83 | // TrianglesPosition specifies Triangles with Position property.
84 | type TrianglesPosition interface {
85 | Triangles
86 | Position(i int) Vec
87 | }
88 |
89 | // TrianglesColor specifies Triangles with Color property.
90 | type TrianglesColor interface {
91 | Triangles
92 | Color(i int) RGBA
93 | }
94 |
95 | // TrianglesPicture specifies Triangles with Picture property.
96 | //
97 | // The first value returned from Picture method is Picture coordinates. The second one specifies the
98 | // weight of the Picture. Value of 0 means, that Picture should be completely ignored, 1 means that
99 | // is should be fully included and anything in between means anything in between.
100 | type TrianglesPicture interface {
101 | Triangles
102 | Picture(i int) (pic Vec, intensity float64)
103 | }
104 |
105 | // TrianglesClipped specifies Triangles with Clipping Rectangle property.
106 | //
107 | // The first value returned from ClipRect method is the clipping rectangle. The second one specifies
108 | // if the triangle is clipped.
109 | type TrianglesClipped interface {
110 | Triangles
111 | ClipRect(i int) (rect Rect, is bool)
112 | }
113 |
114 | // Picture represents a rectangular area of raster data, such as a color. It has Bounds which
115 | // specify the rectangle where data is located.
116 | type Picture interface {
117 | // Bounds returns the rectangle of the Picture. All data is located within this rectangle.
118 | // Querying properties outside the rectangle should return default value of that property.
119 | Bounds() Rect
120 | }
121 |
122 | // TargetPicture is a Picture generated by a Target using MakePicture method. This Picture can be drawn onto
123 | // that (no other) Target together with a TargetTriangles generated by the same Target.
124 | //
125 | // The TargetTriangles specify where, shape and how the Picture should be drawn.
126 | type TargetPicture interface {
127 | Picture
128 |
129 | // Draw draws the supplied TargetTriangles (which must be generated by the same Target as
130 | // this TargetPicture) with this TargetPicture. The TargetTriangles should utilize the data
131 | // from this TargetPicture in some way.
132 | Draw(TargetTriangles)
133 | }
134 |
135 | // PictureColor specifies Picture with Color property, so that every position inside the Picture's
136 | // Bounds has a color.
137 | //
138 | // Positions outside the Picture's Bounds must return full transparent (Alpha(0)).
139 | type PictureColor interface {
140 | Picture
141 | Color(at Vec) RGBA
142 | }
143 |
--------------------------------------------------------------------------------
/logo/LOGOTYPE-HORIZONTAL-BLACK.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/faiface/pixel/0a251bc08bfcfd68720fc237ee6a6268fa3a12e6/logo/LOGOTYPE-HORIZONTAL-BLACK.png
--------------------------------------------------------------------------------
/logo/LOGOTYPE-HORIZONTAL-BLUE.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/faiface/pixel/0a251bc08bfcfd68720fc237ee6a6268fa3a12e6/logo/LOGOTYPE-HORIZONTAL-BLUE.png
--------------------------------------------------------------------------------
/logo/LOGOTYPE-HORIZONTAL-MAROON.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/faiface/pixel/0a251bc08bfcfd68720fc237ee6a6268fa3a12e6/logo/LOGOTYPE-HORIZONTAL-MAROON.png
--------------------------------------------------------------------------------
/logo/LOGOTYPE-HORIZONTAL-PURPLE.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/faiface/pixel/0a251bc08bfcfd68720fc237ee6a6268fa3a12e6/logo/LOGOTYPE-HORIZONTAL-PURPLE.png
--------------------------------------------------------------------------------
/logo/LOGOTYPE-HORIZONTAL-RED.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/faiface/pixel/0a251bc08bfcfd68720fc237ee6a6268fa3a12e6/logo/LOGOTYPE-HORIZONTAL-RED.png
--------------------------------------------------------------------------------
/logo/LOGOTYPE-HORIZONTAL-WHITE.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/faiface/pixel/0a251bc08bfcfd68720fc237ee6a6268fa3a12e6/logo/LOGOTYPE-HORIZONTAL-WHITE.png
--------------------------------------------------------------------------------
/math.go:
--------------------------------------------------------------------------------
1 | package pixel
2 |
3 | // Clamp returns x clamped to the interval [min, max].
4 | //
5 | // If x is less than min, min is returned. If x is more than max, max is returned. Otherwise, x is
6 | // returned.
7 | func Clamp(x, min, max float64) float64 {
8 | if x < min {
9 | return min
10 | }
11 | if x > max {
12 | return max
13 | }
14 | return x
15 | }
16 |
--------------------------------------------------------------------------------
/math_test.go:
--------------------------------------------------------------------------------
1 | package pixel_test
2 |
3 | import (
4 | "fmt"
5 | "math"
6 | "testing"
7 |
8 | "github.com/faiface/pixel"
9 | )
10 |
11 | // closeEnough will shift the decimal point by the accuracy required, truncates the results and compares them.
12 | // Effectively this compares two floats to a given decimal point.
13 | // Example:
14 | // closeEnough(100.125342432, 100.125, 2) == true
15 | // closeEnough(math.Pi, 3.14, 2) == true
16 | // closeEnough(0.1234, 0.1245, 3) == false
17 | func closeEnough(got, expected float64, decimalAccuracy int) bool {
18 | gotShifted := got * math.Pow10(decimalAccuracy)
19 | expectedShifted := expected * math.Pow10(decimalAccuracy)
20 |
21 | return math.Trunc(gotShifted) == math.Trunc(expectedShifted)
22 | }
23 |
24 | type clampTest struct {
25 | number float64
26 | min float64
27 | max float64
28 | expected float64
29 | }
30 |
31 | func TestClamp(t *testing.T) {
32 | tests := []clampTest{
33 | {number: 1, min: 0, max: 5, expected: 1},
34 | {number: 2, min: 0, max: 5, expected: 2},
35 | {number: 8, min: 0, max: 5, expected: 5},
36 | {number: -5, min: 0, max: 5, expected: 0},
37 | {number: -5, min: -4, max: 5, expected: -4},
38 | }
39 |
40 | for _, tc := range tests {
41 | result := pixel.Clamp(tc.number, tc.min, tc.max)
42 | if result != tc.expected {
43 | t.Error(fmt.Sprintf("Clamping %v with min %v and max %v should have given %v, but gave %v", tc.number, tc.min, tc.max, tc.expected, result))
44 | }
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/matrix.go:
--------------------------------------------------------------------------------
1 | package pixel
2 |
3 | import (
4 | "fmt"
5 | "math"
6 | )
7 |
8 | // Matrix is a 2x3 affine matrix that can be used for all kinds of spatial transforms, such
9 | // as movement, scaling and rotations.
10 | //
11 | // Matrix has a handful of useful methods, each of which adds a transformation to the matrix. For
12 | // example:
13 | //
14 | // pixel.IM.Moved(pixel.V(100, 200)).Rotated(pixel.ZV, math.Pi/2)
15 | //
16 | // This code creates a Matrix that first moves everything by 100 units horizontally and 200 units
17 | // vertically and then rotates everything by 90 degrees around the origin.
18 | //
19 | // Layout is:
20 | // [0] [2] [4]
21 | // [1] [3] [5]
22 | // 0 0 1 (implicit row)
23 | type Matrix [6]float64
24 |
25 | // IM stands for identity matrix. Does nothing, no transformation.
26 | var IM = Matrix{1, 0, 0, 1, 0, 0}
27 |
28 | // String returns a string representation of the Matrix.
29 | //
30 | // m := pixel.IM
31 | // fmt.Println(m) // Matrix(1 0 0 | 0 1 0)
32 | func (m Matrix) String() string {
33 | return fmt.Sprintf(
34 | "Matrix(%v %v %v | %v %v %v)",
35 | m[0], m[2], m[4],
36 | m[1], m[3], m[5],
37 | )
38 | }
39 |
40 | // Moved moves everything by the delta vector.
41 | func (m Matrix) Moved(delta Vec) Matrix {
42 | m[4], m[5] = m[4]+delta.X, m[5]+delta.Y
43 | return m
44 | }
45 |
46 | // ScaledXY scales everything around a given point by the scale factor in each axis respectively.
47 | func (m Matrix) ScaledXY(around Vec, scale Vec) Matrix {
48 | m[4], m[5] = m[4]-around.X, m[5]-around.Y
49 | m[0], m[2], m[4] = m[0]*scale.X, m[2]*scale.X, m[4]*scale.X
50 | m[1], m[3], m[5] = m[1]*scale.Y, m[3]*scale.Y, m[5]*scale.Y
51 | m[4], m[5] = m[4]+around.X, m[5]+around.Y
52 | return m
53 | }
54 |
55 | // Scaled scales everything around a given point by the scale factor.
56 | func (m Matrix) Scaled(around Vec, scale float64) Matrix {
57 | return m.ScaledXY(around, V(scale, scale))
58 | }
59 |
60 | // Rotated rotates everything around a given point by the given angle in radians.
61 | func (m Matrix) Rotated(around Vec, angle float64) Matrix {
62 | sint, cost := math.Sincos(angle)
63 | m[4], m[5] = m[4]-around.X, m[5]-around.Y
64 | m = m.Chained(Matrix{cost, sint, -sint, cost, 0, 0})
65 | m[4], m[5] = m[4]+around.X, m[5]+around.Y
66 | return m
67 | }
68 |
69 | // Chained adds another Matrix to this one. All tranformations by the next Matrix will be applied
70 | // after the transformations of this Matrix.
71 | func (m Matrix) Chained(next Matrix) Matrix {
72 | return Matrix{
73 | next[0]*m[0] + next[2]*m[1],
74 | next[1]*m[0] + next[3]*m[1],
75 | next[0]*m[2] + next[2]*m[3],
76 | next[1]*m[2] + next[3]*m[3],
77 | next[0]*m[4] + next[2]*m[5] + next[4],
78 | next[1]*m[4] + next[3]*m[5] + next[5],
79 | }
80 | }
81 |
82 | // Project applies all transformations added to the Matrix to a vector u and returns the result.
83 | //
84 | // Time complexity is O(1).
85 | func (m Matrix) Project(u Vec) Vec {
86 | return Vec{m[0]*u.X + m[2]*u.Y + m[4], m[1]*u.X + m[3]*u.Y + m[5]}
87 | }
88 |
89 | // Unproject does the inverse operation to Project.
90 | //
91 | // Time complexity is O(1).
92 | func (m Matrix) Unproject(u Vec) Vec {
93 | det := m[0]*m[3] - m[2]*m[1]
94 | return Vec{
95 | (m[3]*(u.X-m[4]) - m[2]*(u.Y-m[5])) / det,
96 | (-m[1]*(u.X-m[4]) + m[0]*(u.Y-m[5])) / det,
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/matrix_test.go:
--------------------------------------------------------------------------------
1 | package pixel_test
2 |
3 | import (
4 | "fmt"
5 | "math"
6 | "math/rand"
7 | "testing"
8 |
9 | "github.com/faiface/pixel"
10 | "github.com/stretchr/testify/assert"
11 | )
12 |
13 | func BenchmarkMatrix(b *testing.B) {
14 | b.Run("Moved", func(b *testing.B) {
15 | m := pixel.IM
16 | for i := 0; i < b.N; i++ {
17 | m = m.Moved(pixel.V(4.217, -132.99))
18 | }
19 | })
20 | b.Run("ScaledXY", func(b *testing.B) {
21 | m := pixel.IM
22 | for i := 0; i < b.N; i++ {
23 | m = m.ScaledXY(pixel.V(-5.1, 9.3), pixel.V(2.1, 0.98))
24 | }
25 | })
26 | b.Run("Rotated", func(b *testing.B) {
27 | m := pixel.IM
28 | for i := 0; i < b.N; i++ {
29 | m = m.Rotated(pixel.V(-5.1, 9.3), 1.4)
30 | }
31 | })
32 | b.Run("Chained", func(b *testing.B) {
33 | var m1, m2 pixel.Matrix
34 | for i := range m1 {
35 | m1[i] = rand.Float64()
36 | m2[i] = rand.Float64()
37 | }
38 | for i := 0; i < b.N; i++ {
39 | m1 = m1.Chained(m2)
40 | }
41 | })
42 | b.Run("Project", func(b *testing.B) {
43 | var m pixel.Matrix
44 | for i := range m {
45 | m[i] = rand.Float64()
46 | }
47 | u := pixel.V(1, 1)
48 | for i := 0; i < b.N; i++ {
49 | u = m.Project(u)
50 | }
51 | })
52 | b.Run("Unproject", func(b *testing.B) {
53 | again:
54 | var m pixel.Matrix
55 | for i := range m {
56 | m[i] = rand.Float64()
57 | }
58 | if (m[0]*m[3])-(m[1]*m[2]) == 0 { // zero determinant, not invertible
59 | goto again
60 | }
61 | u := pixel.V(1, 1)
62 | for i := 0; i < b.N; i++ {
63 | u = m.Unproject(u)
64 | }
65 | })
66 | }
67 |
68 | func TestMatrix_Unproject(t *testing.T) {
69 | const delta = 1e-15
70 | t.Run("for rotated matrix", func(t *testing.T) {
71 | matrix := pixel.IM.
72 | Rotated(pixel.ZV, math.Pi/2)
73 | unprojected := matrix.Unproject(pixel.V(0, 1))
74 | assert.InDelta(t, unprojected.X, 1, delta)
75 | assert.InDelta(t, unprojected.Y, 0, delta)
76 | })
77 | t.Run("for moved matrix", func(t *testing.T) {
78 | matrix := pixel.IM.
79 | Moved(pixel.V(1, 2))
80 | unprojected := matrix.Unproject(pixel.V(2, 5))
81 | assert.InDelta(t, unprojected.X, 1, delta)
82 | assert.InDelta(t, unprojected.Y, 3, delta)
83 | })
84 | t.Run("for scaled matrix", func(t *testing.T) {
85 | matrix := pixel.IM.
86 | Scaled(pixel.ZV, 2)
87 | unprojected := matrix.Unproject(pixel.V(2, 4))
88 | assert.InDelta(t, unprojected.X, 1, delta)
89 | assert.InDelta(t, unprojected.Y, 2, delta)
90 | })
91 | t.Run("for scaled, rotated and moved matrix", func(t *testing.T) {
92 | matrix := pixel.IM.
93 | Scaled(pixel.ZV, 2).
94 | Rotated(pixel.ZV, math.Pi/2).
95 | Moved(pixel.V(2, 2))
96 | unprojected := matrix.Unproject(pixel.V(-2, 6))
97 | assert.InDelta(t, unprojected.X, 2, delta)
98 | assert.InDelta(t, unprojected.Y, 2, delta)
99 | })
100 | t.Run("for rotated and moved matrix", func(t *testing.T) {
101 | matrix := pixel.IM.
102 | Rotated(pixel.ZV, math.Pi/2).
103 | Moved(pixel.V(1, 1))
104 | unprojected := matrix.Unproject(pixel.V(1, 2))
105 | assert.InDelta(t, unprojected.X, 1, delta)
106 | assert.InDelta(t, unprojected.Y, 0, delta)
107 | })
108 | t.Run("for projected vertices using all kinds of matrices", func(t *testing.T) {
109 | namedMatrices := map[string]pixel.Matrix{
110 | "IM": pixel.IM,
111 | "Scaled": pixel.IM.Scaled(pixel.ZV, 0.5),
112 | "Scaled x 2": pixel.IM.Scaled(pixel.ZV, 2),
113 | "Rotated": pixel.IM.Rotated(pixel.ZV, math.Pi/4),
114 | "Moved": pixel.IM.Moved(pixel.V(0.5, 1)),
115 | "Moved 2": pixel.IM.Moved(pixel.V(-1, -0.5)),
116 | "Scaled and Rotated": pixel.IM.Scaled(pixel.ZV, 0.5).Rotated(pixel.ZV, math.Pi/4),
117 | "Scaled, Rotated and Moved": pixel.IM.Scaled(pixel.ZV, 0.5).Rotated(pixel.ZV, math.Pi/4).Moved(pixel.V(1, 2)),
118 | "Rotated and Moved": pixel.IM.Rotated(pixel.ZV, math.Pi/4).Moved(pixel.V(1, 2)),
119 | }
120 | vertices := [...]pixel.Vec{
121 | pixel.V(0, 0),
122 | pixel.V(5, 0),
123 | pixel.V(5, 10),
124 | pixel.V(0, 10),
125 | pixel.V(-5, 10),
126 | pixel.V(-5, 0),
127 | pixel.V(-5, -10),
128 | pixel.V(0, -10),
129 | pixel.V(5, -10),
130 | }
131 | for matrixName, matrix := range namedMatrices {
132 | for _, vertex := range vertices {
133 | testCase := fmt.Sprintf("for matrix %s and vertex %v", matrixName, vertex)
134 | t.Run(testCase, func(t *testing.T) {
135 | projected := matrix.Project(vertex)
136 | unprojected := matrix.Unproject(projected)
137 | assert.InDelta(t, vertex.X, unprojected.X, delta)
138 | assert.InDelta(t, vertex.Y, unprojected.Y, delta)
139 | })
140 | }
141 | }
142 | })
143 | t.Run("for singular matrix", func(t *testing.T) {
144 | matrix := pixel.Matrix{0, 0, 0, 0, 0, 0}
145 | unprojected := matrix.Unproject(pixel.ZV)
146 | assert.True(t, math.IsNaN(unprojected.X))
147 | assert.True(t, math.IsNaN(unprojected.Y))
148 | })
149 | }
150 |
--------------------------------------------------------------------------------
/pixel_test.go:
--------------------------------------------------------------------------------
1 | package pixel_test
2 |
3 | import (
4 | "bytes"
5 | "image"
6 | "os"
7 | "testing"
8 |
9 | _ "image/png"
10 |
11 | "github.com/faiface/pixel"
12 | "github.com/faiface/pixel/pixelgl"
13 | )
14 |
15 | // onePixelImage is the byte representation of a 1x1 solid white png file
16 | var onePixelImage = []byte{
17 | 137, 80, 78, 71, 13, 10, 26, 10, 0, 0, 0, 13, 73, 72, 68, 82, 0, 0, 0, 1, 0, 0, 0, 1, 8, 2,
18 | 0, 0, 0, 144, 119, 83, 222, 0, 0, 1, 130, 105, 67, 67, 80, 73, 67, 67, 32, 112, 114, 111, 102, 105, 108, 101, 0,
19 | 0, 40, 145, 125, 145, 59, 72, 3, 65, 20, 69, 143, 73, 68, 17, 37, 133, 41, 68, 44, 182, 80, 43, 5, 81, 17, 75,
20 | 141, 66, 16, 34, 132, 168, 96, 212, 194, 221, 141, 137, 66, 118, 13, 187, 9, 54, 150, 130, 109, 192, 194, 79,
21 | 227, 175, 176, 177, 214, 214, 194, 86, 16, 4, 63, 32, 54, 182, 86, 138, 54, 18, 214, 55, 73, 32, 65, 140, 3,
22 | 195, 28, 238, 188, 123, 121, 243, 6, 124, 71, 25, 211, 114, 3, 3, 96, 217, 57, 39, 30, 9, 107, 243, 137, 5, 173,
23 | 233, 149, 0, 65, 90, 104, 0, 221, 116, 179, 227, 177, 88, 148, 186, 235, 235, 94, 213, 193, 93, 191, 202, 170,
24 | 95, 247, 231, 106, 75, 174, 184, 38, 52, 104, 194, 99, 102, 214, 201, 9, 47, 11, 143, 108, 228, 178, 138, 247,
25 | 132, 67, 230, 170, 158, 20, 62, 23, 238, 115, 164, 65, 225, 71, 165, 27, 101, 126, 83, 156, 46, 177, 79, 101,
26 | 134, 156, 217, 248, 132, 112, 72, 88, 75, 215, 176, 81, 195, 230, 170, 99, 9, 15, 11, 119, 39, 45, 91, 242, 125,
27 | 243, 101, 78, 42, 222, 84, 108, 101, 242, 102, 165, 79, 245, 194, 214, 21, 123, 110, 70, 233, 178, 187, 136, 48,
28 | 197, 52, 49, 52, 12, 242, 172, 145, 33, 71, 191, 156, 182, 40, 46, 113, 185, 15, 215, 241, 119, 150, 252, 49,
29 | 113, 25, 226, 90, 195, 20, 199, 36, 235, 88, 232, 37, 63, 234, 15, 126, 207, 214, 77, 13, 13, 150, 147, 90, 195,
30 | 208, 248, 226, 121, 31, 61, 208, 180, 3, 197, 130, 231, 125, 31, 123, 94, 241, 4, 252, 207, 112, 101, 87, 253,
31 | 235, 71, 48, 250, 41, 122, 161, 170, 117, 31, 66, 112, 11, 46, 174, 171, 154, 177, 11, 151, 219, 208, 241, 148,
32 | 213, 29, 189, 36, 249, 101, 251, 82, 41, 120, 63, 147, 111, 74, 64, 251, 45, 180, 44, 150, 231, 86, 185, 231,
33 | 244, 1, 102, 101, 86, 209, 27, 216, 63, 128, 222, 180, 100, 47, 213, 121, 119, 115, 237, 220, 254, 173, 169,
34 | 204, 239, 7, 178, 211, 114, 90, 10, 150, 157, 65, 0, 0, 0, 9, 112, 72, 89, 115, 0, 0, 46, 35, 0, 0, 46, 35, 1,
35 | 120, 165, 63, 118, 0, 0, 0, 7, 116, 73, 77, 69, 7, 227, 4, 15, 10, 5, 36, 189, 4, 224, 88, 0, 0, 0, 25, 116, 69,
36 | 88, 116, 67, 111, 109, 109, 101, 110, 116, 0, 67, 114, 101, 97, 116, 101, 100, 32, 119, 105, 116, 104, 32, 71,
37 | 73, 77, 80, 87, 129, 14, 23, 0, 0, 0, 12, 73, 68, 65, 84, 8, 215, 99, 120, 241, 226, 61, 0, 5, 123, 2, 192, 194,
38 | 77, 211, 95, 0, 0, 0, 0, 73, 69, 78, 68, 174, 66, 96, 130,
39 | }
40 |
41 | func TestMain(m *testing.M) {
42 | pixelgl.Run(func() {
43 | os.Exit(m.Run())
44 | })
45 | }
46 |
47 | func TestSprite_Draw(t *testing.T) {
48 | img, _, err := image.Decode(bytes.NewReader(onePixelImage))
49 | if err != nil {
50 | t.Fatalf("Could not decode image: %v", err)
51 | }
52 | pic := pixel.PictureDataFromImage(img)
53 |
54 | sprite := pixel.NewSprite(pic, pic.Bounds())
55 |
56 | cfg := pixelgl.WindowConfig{
57 | Title: "testing",
58 | Bounds: pixel.R(0, 0, 150, 150),
59 | Invisible: true,
60 | }
61 |
62 | win, err := pixelgl.NewWindow(cfg)
63 | if err != nil {
64 | t.Fatalf("Could not create window: %v", err)
65 | }
66 |
67 | sprite.Draw(win, pixel.IM)
68 | }
69 |
--------------------------------------------------------------------------------
/pixelgl/canvas.go:
--------------------------------------------------------------------------------
1 | package pixelgl
2 |
3 | import (
4 | "fmt"
5 | "image/color"
6 |
7 | "github.com/faiface/glhf"
8 | "github.com/faiface/mainthread"
9 | "github.com/faiface/pixel"
10 | "github.com/go-gl/mathgl/mgl32"
11 | "github.com/pkg/errors"
12 | )
13 |
14 | // Canvas is an off-screen rectangular BasicTarget and Picture at the same time, that you can draw
15 | // onto.
16 | //
17 | // It supports TrianglesPosition, TrianglesColor, TrianglesPicture and PictureColor.
18 | type Canvas struct {
19 | gf *GLFrame
20 | shader *GLShader
21 |
22 | cmp pixel.ComposeMethod
23 | mat mgl32.Mat3
24 | col mgl32.Vec4
25 | smooth bool
26 |
27 | sprite *pixel.Sprite
28 | }
29 |
30 | var _ pixel.ComposeTarget = (*Canvas)(nil)
31 |
32 | // NewCanvas creates a new empty, fully transparent Canvas with given bounds.
33 | func NewCanvas(bounds pixel.Rect) *Canvas {
34 | c := &Canvas{
35 | gf: NewGLFrame(bounds),
36 | mat: mgl32.Ident3(),
37 | col: mgl32.Vec4{1, 1, 1, 1},
38 | }
39 |
40 | c.shader = NewGLShader(baseCanvasFragmentShader)
41 | c.SetBounds(bounds)
42 | return c
43 | }
44 |
45 | // SetUniform will update the named uniform with the value of any supported underlying
46 | // attribute variable. If the uniform already exists, including defaults, they will be reassigned
47 | // to the new value. The value can be a pointer.
48 | func (c *Canvas) SetUniform(name string, value interface{}) {
49 | c.shader.SetUniform(name, value)
50 | }
51 |
52 | // SetFragmentShader allows you to set a new fragment shader on the underlying
53 | // framebuffer. Argument "src" is the GLSL source, not a filename.
54 | func (c *Canvas) SetFragmentShader(src string) {
55 | c.shader.fs = src
56 | c.shader.Update()
57 | }
58 |
59 | // MakeTriangles creates a specialized copy of the supplied Triangles that draws onto this Canvas.
60 | //
61 | // TrianglesPosition, TrianglesColor and TrianglesPicture are supported.
62 | func (c *Canvas) MakeTriangles(t pixel.Triangles) pixel.TargetTriangles {
63 | if gt, ok := t.(*GLTriangles); ok {
64 | return &canvasTriangles{
65 | GLTriangles: gt,
66 | dst: c,
67 | }
68 | }
69 | return &canvasTriangles{
70 | GLTriangles: NewGLTriangles(c.shader, t),
71 | dst: c,
72 | }
73 | }
74 |
75 | // MakePicture create a specialized copy of the supplied Picture that draws onto this Canvas.
76 | //
77 | // PictureColor is supported.
78 | func (c *Canvas) MakePicture(p pixel.Picture) pixel.TargetPicture {
79 | if cp, ok := p.(*canvasPicture); ok {
80 | return &canvasPicture{
81 | GLPicture: cp.GLPicture,
82 | dst: c,
83 | }
84 | }
85 | if gp, ok := p.(GLPicture); ok {
86 | return &canvasPicture{
87 | GLPicture: gp,
88 | dst: c,
89 | }
90 | }
91 | return &canvasPicture{
92 | GLPicture: NewGLPicture(p),
93 | dst: c,
94 | }
95 | }
96 |
97 | // SetMatrix sets a Matrix that every point will be projected by.
98 | func (c *Canvas) SetMatrix(m pixel.Matrix) {
99 | // pixel.Matrix is 3x2 with an implicit 0, 0, 1 row after it. So
100 | // [0] [2] [4] [0] [3] [6]
101 | // [1] [3] [5] => [1] [4] [7]
102 | // 0 0 1 0 0 1
103 | // since all matrix ops are affine, the last row never changes, and we don't need to copy it
104 | for i, j := range [...]int{0, 1, 3, 4, 6, 7} {
105 | c.mat[j] = float32(m[i])
106 | }
107 | }
108 |
109 | // SetColorMask sets a color that every color in triangles or a picture will be multiplied by.
110 | func (c *Canvas) SetColorMask(col color.Color) {
111 | rgba := pixel.Alpha(1)
112 | if col != nil {
113 | rgba = pixel.ToRGBA(col)
114 | }
115 | c.col = mgl32.Vec4{
116 | float32(rgba.R),
117 | float32(rgba.G),
118 | float32(rgba.B),
119 | float32(rgba.A),
120 | }
121 | }
122 |
123 | // SetComposeMethod sets a Porter-Duff composition method to be used in the following draws onto
124 | // this Canvas.
125 | func (c *Canvas) SetComposeMethod(cmp pixel.ComposeMethod) {
126 | c.cmp = cmp
127 | }
128 |
129 | // SetBounds resizes the Canvas to the new bounds. Old content will be preserved.
130 | func (c *Canvas) SetBounds(bounds pixel.Rect) {
131 | c.gf.SetBounds(bounds)
132 | if c.sprite == nil {
133 | c.sprite = pixel.NewSprite(nil, pixel.Rect{})
134 | }
135 | c.sprite.Set(c, c.Bounds())
136 | // c.sprite.SetMatrix(pixel.IM.Moved(c.Bounds().Center()))
137 | }
138 |
139 | // Bounds returns the rectangular bounds of the Canvas.
140 | func (c *Canvas) Bounds() pixel.Rect {
141 | return c.gf.Bounds()
142 | }
143 |
144 | // SetSmooth sets whether stretched Pictures drawn onto this Canvas should be drawn smooth or
145 | // pixely.
146 | func (c *Canvas) SetSmooth(smooth bool) {
147 | c.smooth = smooth
148 | }
149 |
150 | // Smooth returns whether stretched Pictures drawn onto this Canvas are set to be drawn smooth or
151 | // pixely.
152 | func (c *Canvas) Smooth() bool {
153 | return c.smooth
154 | }
155 |
156 | // must be manually called inside mainthread
157 | func (c *Canvas) setGlhfBounds() {
158 | _, _, bw, bh := intBounds(c.gf.Bounds())
159 | glhf.Bounds(0, 0, bw, bh)
160 | }
161 |
162 | // must be manually called inside mainthread
163 | func setBlendFunc(cmp pixel.ComposeMethod) {
164 | switch cmp {
165 | case pixel.ComposeOver:
166 | glhf.BlendFunc(glhf.One, glhf.OneMinusSrcAlpha)
167 | case pixel.ComposeIn:
168 | glhf.BlendFunc(glhf.DstAlpha, glhf.Zero)
169 | case pixel.ComposeOut:
170 | glhf.BlendFunc(glhf.OneMinusDstAlpha, glhf.Zero)
171 | case pixel.ComposeAtop:
172 | glhf.BlendFunc(glhf.DstAlpha, glhf.OneMinusSrcAlpha)
173 | case pixel.ComposeRover:
174 | glhf.BlendFunc(glhf.OneMinusDstAlpha, glhf.One)
175 | case pixel.ComposeRin:
176 | glhf.BlendFunc(glhf.Zero, glhf.SrcAlpha)
177 | case pixel.ComposeRout:
178 | glhf.BlendFunc(glhf.Zero, glhf.OneMinusSrcAlpha)
179 | case pixel.ComposeRatop:
180 | glhf.BlendFunc(glhf.OneMinusDstAlpha, glhf.SrcAlpha)
181 | case pixel.ComposeXor:
182 | glhf.BlendFunc(glhf.OneMinusDstAlpha, glhf.OneMinusSrcAlpha)
183 | case pixel.ComposePlus:
184 | glhf.BlendFunc(glhf.One, glhf.One)
185 | case pixel.ComposeCopy:
186 | glhf.BlendFunc(glhf.One, glhf.Zero)
187 | default:
188 | panic(errors.New("Canvas: invalid compose method"))
189 | }
190 | }
191 |
192 | // Clear fills the whole Canvas with a single color.
193 | func (c *Canvas) Clear(color color.Color) {
194 | c.gf.Dirty()
195 |
196 | rgba := pixel.ToRGBA(color)
197 |
198 | // color masking
199 | rgba = rgba.Mul(pixel.RGBA{
200 | R: float64(c.col[0]),
201 | G: float64(c.col[1]),
202 | B: float64(c.col[2]),
203 | A: float64(c.col[3]),
204 | })
205 |
206 | mainthread.CallNonBlock(func() {
207 | c.setGlhfBounds()
208 | c.gf.Frame().Begin()
209 | glhf.Clear(
210 | float32(rgba.R),
211 | float32(rgba.G),
212 | float32(rgba.B),
213 | float32(rgba.A),
214 | )
215 | c.gf.Frame().End()
216 | })
217 | }
218 |
219 | // Color returns the color of the pixel over the given position inside the Canvas.
220 | func (c *Canvas) Color(at pixel.Vec) pixel.RGBA {
221 | return c.gf.Color(at)
222 | }
223 |
224 | // Texture returns the underlying OpenGL Texture of this Canvas.
225 | //
226 | // Implements GLPicture interface.
227 | func (c *Canvas) Texture() *glhf.Texture {
228 | return c.gf.Texture()
229 | }
230 |
231 | // Frame returns the underlying OpenGL Frame of this Canvas.
232 | func (c *Canvas) Frame() *glhf.Frame {
233 | return c.gf.frame
234 | }
235 |
236 | // SetPixels replaces the content of the Canvas with the provided pixels. The provided slice must be
237 | // an alpha-premultiplied RGBA sequence of correct length (4 * width * height).
238 | func (c *Canvas) SetPixels(pixels []uint8) {
239 | c.gf.Dirty()
240 |
241 | mainthread.Call(func() {
242 | tex := c.Texture()
243 | tex.Begin()
244 | tex.SetPixels(0, 0, tex.Width(), tex.Height(), pixels)
245 | tex.End()
246 | })
247 | }
248 |
249 | // Pixels returns an alpha-premultiplied RGBA sequence of the content of the Canvas.
250 | func (c *Canvas) Pixels() []uint8 {
251 | var pixels []uint8
252 |
253 | mainthread.Call(func() {
254 | tex := c.Texture()
255 | tex.Begin()
256 | pixels = tex.Pixels(0, 0, tex.Width(), tex.Height())
257 | tex.End()
258 | })
259 |
260 | return pixels
261 | }
262 |
263 | // Draw draws the content of the Canvas onto another Target, transformed by the given Matrix, just
264 | // like if it was a Sprite containing the whole Canvas.
265 | func (c *Canvas) Draw(t pixel.Target, matrix pixel.Matrix) {
266 | c.sprite.Draw(t, matrix)
267 | }
268 |
269 | // DrawColorMask draws the content of the Canvas onto another Target, transformed by the given
270 | // Matrix and multiplied by the given mask, just like if it was a Sprite containing the whole Canvas.
271 | //
272 | // If the color mask is nil, a fully opaque white mask will be used causing no effect.
273 | func (c *Canvas) DrawColorMask(t pixel.Target, matrix pixel.Matrix, mask color.Color) {
274 | c.sprite.DrawColorMask(t, matrix, mask)
275 | }
276 |
277 | type canvasTriangles struct {
278 | *GLTriangles
279 | dst *Canvas
280 | }
281 |
282 | func (ct *canvasTriangles) draw(tex *glhf.Texture, bounds pixel.Rect) {
283 | ct.dst.gf.Dirty()
284 |
285 | // save the current state vars to avoid race condition
286 | cmp := ct.dst.cmp
287 | smt := ct.dst.smooth
288 | mat := ct.dst.mat
289 | col := ct.dst.col
290 |
291 | mainthread.CallNonBlock(func() {
292 | ct.dst.setGlhfBounds()
293 | setBlendFunc(cmp)
294 |
295 | frame := ct.dst.gf.Frame()
296 | shader := ct.shader.s
297 |
298 | frame.Begin()
299 | shader.Begin()
300 |
301 | ct.shader.uniformDefaults.transform = mat
302 | ct.shader.uniformDefaults.colormask = col
303 | dstBounds := ct.dst.Bounds()
304 | ct.shader.uniformDefaults.bounds = mgl32.Vec4{
305 | float32(dstBounds.Min.X),
306 | float32(dstBounds.Min.Y),
307 | float32(dstBounds.W()),
308 | float32(dstBounds.H()),
309 | }
310 |
311 | bx, by, bw, bh := intBounds(bounds)
312 | ct.shader.uniformDefaults.texbounds = mgl32.Vec4{
313 | float32(bx),
314 | float32(by),
315 | float32(bw),
316 | float32(bh),
317 | }
318 |
319 | for loc, u := range ct.shader.uniforms {
320 | ct.shader.s.SetUniformAttr(loc, u.Value())
321 | }
322 |
323 | if tex == nil {
324 | ct.vs.Begin()
325 | ct.vs.Draw()
326 | ct.vs.End()
327 | } else {
328 | tex.Begin()
329 |
330 | if tex.Smooth() != smt {
331 | tex.SetSmooth(smt)
332 | }
333 |
334 | ct.vs.Begin()
335 | ct.vs.Draw()
336 | ct.vs.End()
337 |
338 | tex.End()
339 | }
340 |
341 | shader.End()
342 | frame.End()
343 | })
344 | }
345 |
346 | func (ct *canvasTriangles) Draw() {
347 | ct.draw(nil, pixel.Rect{})
348 | }
349 |
350 | type canvasPicture struct {
351 | GLPicture
352 | dst *Canvas
353 | }
354 |
355 | func (cp *canvasPicture) Draw(t pixel.TargetTriangles) {
356 | ct := t.(*canvasTriangles)
357 | if cp.dst != ct.dst {
358 | panic(fmt.Errorf("(%T).Draw: TargetTriangles generated by different Canvas", cp))
359 | }
360 | ct.draw(cp.GLPicture.Texture(), cp.GLPicture.Bounds())
361 | }
362 |
--------------------------------------------------------------------------------
/pixelgl/doc.go:
--------------------------------------------------------------------------------
1 | // Package pixelgl implements efficient OpenGL targets and utilities for the Pixel game development
2 | // library, specifically Window and Canvas.
3 | //
4 | // It also contains a few additional utilities to help extend Pixel with OpenGL graphical effects.
5 | package pixelgl
6 |
--------------------------------------------------------------------------------
/pixelgl/glframe.go:
--------------------------------------------------------------------------------
1 | package pixelgl
2 |
3 | import (
4 | "github.com/faiface/glhf"
5 | "github.com/faiface/mainthread"
6 | "github.com/faiface/pixel"
7 | )
8 |
9 | // GLFrame is a type that helps implementing OpenGL Targets. It implements most common methods to
10 | // avoid code redundancy. It contains an glhf.Frame that you can draw on.
11 | type GLFrame struct {
12 | frame *glhf.Frame
13 | bounds pixel.Rect
14 | pixels []uint8
15 | dirty bool
16 | }
17 |
18 | // NewGLFrame creates a new GLFrame with the given bounds.
19 | func NewGLFrame(bounds pixel.Rect) *GLFrame {
20 | gf := new(GLFrame)
21 | gf.SetBounds(bounds)
22 | return gf
23 | }
24 |
25 | // SetBounds resizes the GLFrame to the new bounds.
26 | func (gf *GLFrame) SetBounds(bounds pixel.Rect) {
27 | if bounds == gf.Bounds() {
28 | return
29 | }
30 |
31 | mainthread.Call(func() {
32 | oldF := gf.frame
33 |
34 | _, _, w, h := intBounds(bounds)
35 | if w <= 0 {
36 | w = 1
37 | }
38 | if h <= 0 {
39 | h = 1
40 | }
41 | gf.frame = glhf.NewFrame(w, h, false)
42 |
43 | // preserve old content
44 | if oldF != nil {
45 | ox, oy, ow, oh := intBounds(bounds)
46 | oldF.Blit(
47 | gf.frame,
48 | ox, oy, ox+ow, oy+oh,
49 | ox, oy, ox+ow, oy+oh,
50 | )
51 | }
52 | })
53 |
54 | gf.bounds = bounds
55 | gf.pixels = nil
56 | gf.dirty = true
57 | }
58 |
59 | // Bounds returns the current GLFrame's bounds.
60 | func (gf *GLFrame) Bounds() pixel.Rect {
61 | return gf.bounds
62 | }
63 |
64 | // Color returns the color of the pixel under the specified position.
65 | func (gf *GLFrame) Color(at pixel.Vec) pixel.RGBA {
66 | if gf.dirty {
67 | mainthread.Call(func() {
68 | tex := gf.frame.Texture()
69 | tex.Begin()
70 | gf.pixels = tex.Pixels(0, 0, tex.Width(), tex.Height())
71 | tex.End()
72 | })
73 | gf.dirty = false
74 | }
75 | if !gf.bounds.Contains(at) {
76 | return pixel.Alpha(0)
77 | }
78 | bx, by, bw, _ := intBounds(gf.bounds)
79 | x, y := int(at.X)-bx, int(at.Y)-by
80 | off := y*bw + x
81 | return pixel.RGBA{
82 | R: float64(gf.pixels[off*4+0]) / 255,
83 | G: float64(gf.pixels[off*4+1]) / 255,
84 | B: float64(gf.pixels[off*4+2]) / 255,
85 | A: float64(gf.pixels[off*4+3]) / 255,
86 | }
87 | }
88 |
89 | // Frame returns the GLFrame's Frame that you can draw on.
90 | func (gf *GLFrame) Frame() *glhf.Frame {
91 | return gf.frame
92 | }
93 |
94 | // Texture returns the underlying Texture of the GLFrame's Frame.
95 | //
96 | // Implements GLPicture interface.
97 | func (gf *GLFrame) Texture() *glhf.Texture {
98 | return gf.frame.Texture()
99 | }
100 |
101 | // Dirty marks the GLFrame as changed. Always call this method when you draw onto the GLFrame's
102 | // Frame.
103 | func (gf *GLFrame) Dirty() {
104 | gf.dirty = true
105 | }
106 |
--------------------------------------------------------------------------------
/pixelgl/glpicture.go:
--------------------------------------------------------------------------------
1 | package pixelgl
2 |
3 | import (
4 | "math"
5 |
6 | "github.com/faiface/glhf"
7 | "github.com/faiface/mainthread"
8 | "github.com/faiface/pixel"
9 | )
10 |
11 | // GLPicture is a pixel.PictureColor with a Texture. All OpenGL Targets should implement and accept
12 | // this interface, because it enables seamless drawing of one to another.
13 | //
14 | // Implementing this interface on an OpenGL Target enables other OpenGL Targets to efficiently draw
15 | // that Target onto them.
16 | type GLPicture interface {
17 | pixel.PictureColor
18 | Texture() *glhf.Texture
19 | }
20 |
21 | // NewGLPicture creates a new GLPicture with it's own static OpenGL texture. This function always
22 | // allocates a new texture that cannot (shouldn't) be further modified.
23 | func NewGLPicture(p pixel.Picture) GLPicture {
24 | bounds := p.Bounds()
25 | bx, by, bw, bh := intBounds(bounds)
26 |
27 | pixels := make([]uint8, 4*bw*bh)
28 |
29 | if pd, ok := p.(*pixel.PictureData); ok {
30 | // PictureData short path
31 | for y := 0; y < bh; y++ {
32 | for x := 0; x < bw; x++ {
33 | rgba := pd.Pix[y*pd.Stride+x]
34 | off := (y*bw + x) * 4
35 | pixels[off+0] = rgba.R
36 | pixels[off+1] = rgba.G
37 | pixels[off+2] = rgba.B
38 | pixels[off+3] = rgba.A
39 | }
40 | }
41 | } else if p, ok := p.(pixel.PictureColor); ok {
42 | for y := 0; y < bh; y++ {
43 | for x := 0; x < bw; x++ {
44 | at := pixel.V(
45 | math.Max(float64(bx+x), bounds.Min.X),
46 | math.Max(float64(by+y), bounds.Min.Y),
47 | )
48 | color := p.Color(at)
49 | off := (y*bw + x) * 4
50 | pixels[off+0] = uint8(color.R * 255)
51 | pixels[off+1] = uint8(color.G * 255)
52 | pixels[off+2] = uint8(color.B * 255)
53 | pixels[off+3] = uint8(color.A * 255)
54 | }
55 | }
56 | }
57 |
58 | var tex *glhf.Texture
59 | mainthread.Call(func() {
60 | tex = glhf.NewTexture(bw, bh, false, pixels)
61 | })
62 |
63 | gp := &glPicture{
64 | bounds: bounds,
65 | tex: tex,
66 | pixels: pixels,
67 | }
68 | return gp
69 | }
70 |
71 | type glPicture struct {
72 | bounds pixel.Rect
73 | tex *glhf.Texture
74 | pixels []uint8
75 | }
76 |
77 | func (gp *glPicture) Bounds() pixel.Rect {
78 | return gp.bounds
79 | }
80 |
81 | func (gp *glPicture) Texture() *glhf.Texture {
82 | return gp.tex
83 | }
84 |
85 | func (gp *glPicture) Color(at pixel.Vec) pixel.RGBA {
86 | if !gp.bounds.Contains(at) {
87 | return pixel.Alpha(0)
88 | }
89 | bx, by, bw, _ := intBounds(gp.bounds)
90 | x, y := int(at.X)-bx, int(at.Y)-by
91 | off := y*bw + x
92 | return pixel.RGBA{
93 | R: float64(gp.pixels[off*4+0]) / 255,
94 | G: float64(gp.pixels[off*4+1]) / 255,
95 | B: float64(gp.pixels[off*4+2]) / 255,
96 | A: float64(gp.pixels[off*4+3]) / 255,
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/pixelgl/glshader.go:
--------------------------------------------------------------------------------
1 | package pixelgl
2 |
3 | import (
4 | "github.com/faiface/glhf"
5 | "github.com/faiface/mainthread"
6 | "github.com/go-gl/mathgl/mgl32"
7 | "github.com/pkg/errors"
8 | )
9 |
10 | // GLShader is a type to assist with managing a canvas's underlying
11 | // shader configuration. This allows for customization of shaders on
12 | // a per canvas basis.
13 | type GLShader struct {
14 | s *glhf.Shader
15 | vf, uf glhf.AttrFormat
16 | vs, fs string
17 |
18 | uniforms []gsUniformAttr
19 |
20 | uniformDefaults struct {
21 | transform mgl32.Mat3
22 | colormask mgl32.Vec4
23 | bounds mgl32.Vec4
24 | texbounds mgl32.Vec4
25 | cliprect mgl32.Vec4
26 | }
27 | }
28 |
29 | type gsUniformAttr struct {
30 | Name string
31 | Type glhf.AttrType
32 | value interface{}
33 | ispointer bool
34 | }
35 |
36 | const (
37 | canvasPosition int = iota
38 | canvasColor
39 | canvasTexCoords
40 | canvasIntensity
41 | canvasClip
42 | )
43 |
44 | var defaultCanvasVertexFormat = glhf.AttrFormat{
45 | canvasPosition: glhf.Attr{Name: "aPosition", Type: glhf.Vec2},
46 | canvasColor: glhf.Attr{Name: "aColor", Type: glhf.Vec4},
47 | canvasTexCoords: glhf.Attr{Name: "aTexCoords", Type: glhf.Vec2},
48 | canvasIntensity: glhf.Attr{Name: "aIntensity", Type: glhf.Float},
49 | canvasClip: glhf.Attr{Name: "aClipRect", Type: glhf.Vec4},
50 | }
51 |
52 | // Sets up a base shader with everything needed for a Pixel
53 | // canvas to render correctly. The defaults can be overridden
54 | // by simply using the SetUniform function.
55 | func NewGLShader(fragmentShader string) *GLShader {
56 | gs := &GLShader{
57 | vf: defaultCanvasVertexFormat,
58 | vs: baseCanvasVertexShader,
59 | fs: fragmentShader,
60 | }
61 |
62 | gs.SetUniform("uTransform", &gs.uniformDefaults.transform)
63 | gs.SetUniform("uColorMask", &gs.uniformDefaults.colormask)
64 | gs.SetUniform("uBounds", &gs.uniformDefaults.bounds)
65 | gs.SetUniform("uTexBounds", &gs.uniformDefaults.texbounds)
66 |
67 | gs.Update()
68 |
69 | return gs
70 | }
71 |
72 | // Update reinitialize GLShader data and recompile the underlying gl shader object
73 | func (gs *GLShader) Update() {
74 | gs.uf = make([]glhf.Attr, len(gs.uniforms))
75 | for idx := range gs.uniforms {
76 | gs.uf[idx] = glhf.Attr{
77 | Name: gs.uniforms[idx].Name,
78 | Type: gs.uniforms[idx].Type,
79 | }
80 | }
81 |
82 | var shader *glhf.Shader
83 | mainthread.Call(func() {
84 | var err error
85 | shader, err = glhf.NewShader(
86 | gs.vf,
87 | gs.uf,
88 | gs.vs,
89 | gs.fs,
90 | )
91 | if err != nil {
92 | panic(errors.Wrap(err, "failed to create Canvas, there's a bug in the shader"))
93 | }
94 | })
95 |
96 | gs.s = shader
97 | }
98 |
99 | // gets the uniform index from GLShader
100 | func (gs *GLShader) getUniform(Name string) int {
101 | for i, u := range gs.uniforms {
102 | if u.Name == Name {
103 | return i
104 | }
105 | }
106 | return -1
107 | }
108 |
109 | // SetUniform appends a custom uniform name and value to the shader.
110 | // if the uniform already exists, it will simply be overwritten.
111 | //
112 | // example:
113 | //
114 | // utime := float32(time.Since(starttime)).Seconds())
115 | // mycanvas.shader.AddUniform("u_time", &utime)
116 | func (gs *GLShader) SetUniform(name string, value interface{}) {
117 | t, p := getAttrType(value)
118 | if loc := gs.getUniform(name); loc > -1 {
119 | gs.uniforms[loc].Name = name
120 | gs.uniforms[loc].Type = t
121 | gs.uniforms[loc].ispointer = p
122 | gs.uniforms[loc].value = value
123 | return
124 | }
125 | gs.uniforms = append(gs.uniforms, gsUniformAttr{
126 | Name: name,
127 | Type: t,
128 | ispointer: p,
129 | value: value,
130 | })
131 | }
132 |
133 | // Value returns the attribute's concrete value. If the stored value
134 | // is a pointer, we return the dereferenced value.
135 | func (gu *gsUniformAttr) Value() interface{} {
136 | if !gu.ispointer {
137 | return gu.value
138 | }
139 | switch gu.Type {
140 | case glhf.Vec2:
141 | return *gu.value.(*mgl32.Vec2)
142 | case glhf.Vec3:
143 | return *gu.value.(*mgl32.Vec3)
144 | case glhf.Vec4:
145 | return *gu.value.(*mgl32.Vec4)
146 | case glhf.Mat2:
147 | return *gu.value.(*mgl32.Mat2)
148 | case glhf.Mat23:
149 | return *gu.value.(*mgl32.Mat2x3)
150 | case glhf.Mat24:
151 | return *gu.value.(*mgl32.Mat2x4)
152 | case glhf.Mat3:
153 | return *gu.value.(*mgl32.Mat3)
154 | case glhf.Mat32:
155 | return *gu.value.(*mgl32.Mat3x2)
156 | case glhf.Mat34:
157 | return *gu.value.(*mgl32.Mat3x4)
158 | case glhf.Mat4:
159 | return *gu.value.(*mgl32.Mat4)
160 | case glhf.Mat42:
161 | return *gu.value.(*mgl32.Mat4x2)
162 | case glhf.Mat43:
163 | return *gu.value.(*mgl32.Mat4x3)
164 | case glhf.Int:
165 | return *gu.value.(*int32)
166 | case glhf.Float:
167 | return *gu.value.(*float32)
168 | default:
169 | panic("invalid attrtype")
170 | }
171 | }
172 |
173 | // Returns the type identifier for any (supported) attribute variable type
174 | // and whether or not it is a pointer of that type.
175 | func getAttrType(v interface{}) (glhf.AttrType, bool) {
176 | switch v.(type) {
177 | case int32:
178 | return glhf.Int, false
179 | case float32:
180 | return glhf.Float, false
181 | case mgl32.Vec2:
182 | return glhf.Vec2, false
183 | case mgl32.Vec3:
184 | return glhf.Vec3, false
185 | case mgl32.Vec4:
186 | return glhf.Vec4, false
187 | case mgl32.Mat2:
188 | return glhf.Mat2, false
189 | case mgl32.Mat2x3:
190 | return glhf.Mat23, false
191 | case mgl32.Mat2x4:
192 | return glhf.Mat24, false
193 | case mgl32.Mat3:
194 | return glhf.Mat3, false
195 | case mgl32.Mat3x2:
196 | return glhf.Mat32, false
197 | case mgl32.Mat3x4:
198 | return glhf.Mat34, false
199 | case mgl32.Mat4:
200 | return glhf.Mat4, false
201 | case mgl32.Mat4x2:
202 | return glhf.Mat42, false
203 | case mgl32.Mat4x3:
204 | return glhf.Mat43, false
205 | case *mgl32.Vec2:
206 | return glhf.Vec2, true
207 | case *mgl32.Vec3:
208 | return glhf.Vec3, true
209 | case *mgl32.Vec4:
210 | return glhf.Vec4, true
211 | case *mgl32.Mat2:
212 | return glhf.Mat2, true
213 | case *mgl32.Mat2x3:
214 | return glhf.Mat23, true
215 | case *mgl32.Mat2x4:
216 | return glhf.Mat24, true
217 | case *mgl32.Mat3:
218 | return glhf.Mat3, true
219 | case *mgl32.Mat3x2:
220 | return glhf.Mat32, true
221 | case *mgl32.Mat3x4:
222 | return glhf.Mat34, true
223 | case *mgl32.Mat4:
224 | return glhf.Mat4, true
225 | case *mgl32.Mat4x2:
226 | return glhf.Mat42, true
227 | case *mgl32.Mat4x3:
228 | return glhf.Mat43, true
229 | case *int32:
230 | return glhf.Int, true
231 | case *float32:
232 | return glhf.Float, true
233 | default:
234 | panic("invalid AttrType")
235 | }
236 | }
237 |
238 | var baseCanvasVertexShader = `
239 | #version 330 core
240 |
241 | in vec2 aPosition;
242 | in vec4 aColor;
243 | in vec2 aTexCoords;
244 | in float aIntensity;
245 | in vec4 aClipRect;
246 | in float aIsClipped;
247 |
248 | out vec4 vColor;
249 | out vec2 vTexCoords;
250 | out float vIntensity;
251 | out vec2 vPosition;
252 | out vec4 vClipRect;
253 |
254 | uniform mat3 uTransform;
255 | uniform vec4 uBounds;
256 |
257 | void main() {
258 | vec2 transPos = (uTransform * vec3(aPosition, 1.0)).xy;
259 | vec2 normPos = (transPos - uBounds.xy) / uBounds.zw * 2 - vec2(1, 1);
260 | gl_Position = vec4(normPos, 0.0, 1.0);
261 |
262 | vColor = aColor;
263 | vPosition = aPosition;
264 | vTexCoords = aTexCoords;
265 | vIntensity = aIntensity;
266 | vClipRect = aClipRect;
267 | }
268 | `
269 |
270 | var baseCanvasFragmentShader = `
271 | #version 330 core
272 |
273 | in vec4 vColor;
274 | in vec2 vTexCoords;
275 | in float vIntensity;
276 | in vec4 vClipRect;
277 |
278 | out vec4 fragColor;
279 |
280 | uniform vec4 uColorMask;
281 | uniform vec4 uTexBounds;
282 | uniform sampler2D uTexture;
283 |
284 | void main() {
285 | if ((vClipRect != vec4(0,0,0,0)) && (gl_FragCoord.x < vClipRect.x || gl_FragCoord.y < vClipRect.y || gl_FragCoord.x > vClipRect.z || gl_FragCoord.y > vClipRect.w))
286 | discard;
287 |
288 | if (vIntensity == 0) {
289 | fragColor = uColorMask * vColor;
290 | } else {
291 | fragColor = vec4(0, 0, 0, 0);
292 | fragColor += (1 - vIntensity) * vColor;
293 | vec2 t = (vTexCoords - uTexBounds.xy) / uTexBounds.zw;
294 | fragColor += vIntensity * vColor * texture(uTexture, t);
295 | fragColor *= uColorMask;
296 | }
297 | }
298 | `
299 |
--------------------------------------------------------------------------------
/pixelgl/gltriangles.go:
--------------------------------------------------------------------------------
1 | package pixelgl
2 |
3 | import (
4 | "fmt"
5 |
6 | "github.com/faiface/glhf"
7 | "github.com/faiface/mainthread"
8 | "github.com/faiface/pixel"
9 | )
10 |
11 | // GLTriangles are OpenGL triangles implemented using glhf.VertexSlice.
12 | //
13 | // Triangles returned from this function support TrianglesPosition, TrianglesColor and
14 | // TrianglesPicture. If you need to support more, you can "override" SetLen and Update methods.
15 | type GLTriangles struct {
16 | vs *glhf.VertexSlice
17 | data []float32
18 | shader *GLShader
19 | }
20 |
21 | var (
22 | _ pixel.TrianglesPosition = (*GLTriangles)(nil)
23 | _ pixel.TrianglesColor = (*GLTriangles)(nil)
24 | _ pixel.TrianglesPicture = (*GLTriangles)(nil)
25 | _ pixel.TrianglesClipped = (*GLTriangles)(nil)
26 | )
27 |
28 | // The following is a helper so that the indices of
29 | // each of these items is easier to see/debug.
30 | const (
31 | triPosX = iota
32 | triPosY
33 | triColorR
34 | triColorG
35 | triColorB
36 | triColorA
37 | triPicX
38 | triPicY
39 | triIntensity
40 | triClipMinX
41 | triClipMinY
42 | triClipMaxX
43 | triClipMaxY
44 | trisAttrLen
45 | )
46 |
47 | // NewGLTriangles returns GLTriangles initialized with the data from the supplied Triangles.
48 | //
49 | // Only draw the Triangles using the provided Shader.
50 | func NewGLTriangles(shader *GLShader, t pixel.Triangles) *GLTriangles {
51 | var gt *GLTriangles
52 | mainthread.Call(func() {
53 | gt = &GLTriangles{
54 | vs: glhf.MakeVertexSlice(shader.s, 0, t.Len()),
55 | shader: shader,
56 | }
57 | })
58 | gt.SetLen(t.Len())
59 | gt.Update(t)
60 | return gt
61 | }
62 |
63 | // VertexSlice returns the VertexSlice of this GLTriangles.
64 | //
65 | // You can use it to draw them.
66 | func (gt *GLTriangles) VertexSlice() *glhf.VertexSlice {
67 | return gt.vs
68 | }
69 |
70 | // Shader returns the GLTriangles's associated shader.
71 | func (gt *GLTriangles) Shader() *GLShader {
72 | return gt.shader
73 | }
74 |
75 | // Len returns the number of vertices.
76 | func (gt *GLTriangles) Len() int {
77 | return len(gt.data) / gt.vs.Stride()
78 | }
79 |
80 | // SetLen efficiently resizes GLTriangles to len.
81 | //
82 | // Time complexity is amortized O(1).
83 | func (gt *GLTriangles) SetLen(length int) {
84 | switch {
85 | case length > gt.Len():
86 | needAppend := length - gt.Len()
87 | for i := 0; i < needAppend; i++ {
88 | gt.data = append(gt.data,
89 | 0, 0,
90 | 1, 1, 1, 1,
91 | 0, 0,
92 | 0,
93 | 0, 0, 0, 0,
94 | )
95 | }
96 | case length < gt.Len():
97 | gt.data = gt.data[:length*gt.vs.Stride()]
98 | default:
99 | return
100 | }
101 | mainthread.Call(func() {
102 | gt.vs.Begin()
103 | gt.vs.SetLen(length)
104 | gt.vs.End()
105 | })
106 | }
107 |
108 | // Slice returns a sub-Triangles of this GLTriangles in range [i, j).
109 | func (gt *GLTriangles) Slice(i, j int) pixel.Triangles {
110 | return &GLTriangles{
111 | vs: gt.vs.Slice(i, j),
112 | data: gt.data[i*gt.vs.Stride() : j*gt.vs.Stride()],
113 | shader: gt.shader,
114 | }
115 | }
116 |
117 | func (gt *GLTriangles) updateData(t pixel.Triangles) {
118 | // glTriangles short path
119 | if t, ok := t.(*GLTriangles); ok {
120 | copy(gt.data, t.data)
121 | return
122 | }
123 |
124 | // TrianglesData short path
125 | stride := gt.vs.Stride()
126 | length := gt.Len()
127 | if t, ok := t.(*pixel.TrianglesData); ok {
128 | for i := 0; i < length; i++ {
129 | var (
130 | px, py = (*t)[i].Position.XY()
131 | col = (*t)[i].Color
132 | tx, ty = (*t)[i].Picture.XY()
133 | in = (*t)[i].Intensity
134 | rec = (*t)[i].ClipRect
135 | )
136 | d := gt.data[i*stride : i*stride+trisAttrLen]
137 | d[triPosX] = float32(px)
138 | d[triPosY] = float32(py)
139 | d[triColorR] = float32(col.R)
140 | d[triColorG] = float32(col.G)
141 | d[triColorB] = float32(col.B)
142 | d[triColorA] = float32(col.A)
143 | d[triPicX] = float32(tx)
144 | d[triPicY] = float32(ty)
145 | d[triIntensity] = float32(in)
146 | d[triClipMinX] = float32(rec.Min.X)
147 | d[triClipMinY] = float32(rec.Min.Y)
148 | d[triClipMaxX] = float32(rec.Max.X)
149 | d[triClipMaxY] = float32(rec.Max.Y)
150 | }
151 | return
152 | }
153 |
154 | if t, ok := t.(pixel.TrianglesPosition); ok {
155 | for i := 0; i < length; i++ {
156 | px, py := t.Position(i).XY()
157 | gt.data[i*stride+triPosX] = float32(px)
158 | gt.data[i*stride+triPosY] = float32(py)
159 | }
160 | }
161 | if t, ok := t.(pixel.TrianglesColor); ok {
162 | for i := 0; i < length; i++ {
163 | col := t.Color(i)
164 | gt.data[i*stride+triColorR] = float32(col.R)
165 | gt.data[i*stride+triColorG] = float32(col.G)
166 | gt.data[i*stride+triColorB] = float32(col.B)
167 | gt.data[i*stride+triColorA] = float32(col.A)
168 | }
169 | }
170 | if t, ok := t.(pixel.TrianglesPicture); ok {
171 | for i := 0; i < length; i++ {
172 | pic, intensity := t.Picture(i)
173 | gt.data[i*stride+triPicX] = float32(pic.X)
174 | gt.data[i*stride+triPicY] = float32(pic.Y)
175 | gt.data[i*stride+triIntensity] = float32(intensity)
176 | }
177 | }
178 | if t, ok := t.(pixel.TrianglesClipped); ok {
179 | for i := 0; i < length; i++ {
180 | rect, _ := t.ClipRect(i)
181 | gt.data[i*stride+triClipMinX] = float32(rect.Min.X)
182 | gt.data[i*stride+triClipMinY] = float32(rect.Min.Y)
183 | gt.data[i*stride+triClipMaxX] = float32(rect.Max.X)
184 | gt.data[i*stride+triClipMaxY] = float32(rect.Max.Y)
185 | }
186 | }
187 | }
188 |
189 | // Update copies vertex properties from the supplied Triangles into this GLTriangles.
190 | //
191 | // The two Triangles (gt and t) must be of the same len.
192 | func (gt *GLTriangles) Update(t pixel.Triangles) {
193 | if gt.Len() != t.Len() {
194 | panic(fmt.Errorf("(%T).Update: invalid triangles len", gt))
195 | }
196 | gt.updateData(t)
197 |
198 | // Copy the verteces down to the glhf.VertexData
199 | gt.CopyVertices()
200 | }
201 |
202 | // CopyVertices copies the GLTriangle data down to the vertex data.
203 | func (gt *GLTriangles) CopyVertices() {
204 | // this code is supposed to copy the vertex data and CallNonBlock the update if
205 | // the data is small enough, otherwise it'll block and not copy the data
206 | if len(gt.data) < 256 { // arbitrary heurestic constant
207 | data := append([]float32{}, gt.data...)
208 | mainthread.CallNonBlock(func() {
209 | gt.vs.Begin()
210 | gt.vs.SetVertexData(data)
211 | gt.vs.End()
212 | })
213 | } else {
214 | mainthread.Call(func() {
215 | gt.vs.Begin()
216 | gt.vs.SetVertexData(gt.data)
217 | gt.vs.End()
218 | })
219 | }
220 | }
221 |
222 | // Copy returns an independent copy of this GLTriangles.
223 | //
224 | // The returned Triangles are *GLTriangles as the underlying type.
225 | func (gt *GLTriangles) Copy() pixel.Triangles {
226 | return NewGLTriangles(gt.shader, gt)
227 | }
228 |
229 | // index is a helper function that returns the index in the data
230 | // slice given the i-th vertex and the item index.
231 | func (gt *GLTriangles) index(i, idx int) int {
232 | return i*gt.vs.Stride() + idx
233 | }
234 |
235 | // Position returns the Position property of the i-th vertex.
236 | func (gt *GLTriangles) Position(i int) pixel.Vec {
237 | px := gt.data[gt.index(i, triPosX)]
238 | py := gt.data[gt.index(i, triPosY)]
239 | return pixel.V(float64(px), float64(py))
240 | }
241 |
242 | // SetPosition sets the position property of the i-th vertex.
243 | func (gt *GLTriangles) SetPosition(i int, p pixel.Vec) {
244 | gt.data[gt.index(i, triPosX)] = float32(p.X)
245 | gt.data[gt.index(i, triPosY)] = float32(p.Y)
246 | }
247 |
248 | // Color returns the Color property of the i-th vertex.
249 | func (gt *GLTriangles) Color(i int) pixel.RGBA {
250 | r := gt.data[gt.index(i, triColorR)]
251 | g := gt.data[gt.index(i, triColorG)]
252 | b := gt.data[gt.index(i, triColorB)]
253 | a := gt.data[gt.index(i, triColorA)]
254 | return pixel.RGBA{
255 | R: float64(r),
256 | G: float64(g),
257 | B: float64(b),
258 | A: float64(a),
259 | }
260 | }
261 |
262 | // SetColor sets the color property of the i-th vertex.
263 | func (gt *GLTriangles) SetColor(i int, c pixel.RGBA) {
264 | gt.data[gt.index(i, triColorR)] = float32(c.R)
265 | gt.data[gt.index(i, triColorG)] = float32(c.G)
266 | gt.data[gt.index(i, triColorB)] = float32(c.B)
267 | gt.data[gt.index(i, triColorA)] = float32(c.A)
268 | }
269 |
270 | // Picture returns the Picture property of the i-th vertex.
271 | func (gt *GLTriangles) Picture(i int) (pic pixel.Vec, intensity float64) {
272 | tx := gt.data[gt.index(i, triPicX)]
273 | ty := gt.data[gt.index(i, triPicY)]
274 | intensity = float64(gt.data[gt.index(i, triIntensity)])
275 | return pixel.V(float64(tx), float64(ty)), intensity
276 | }
277 |
278 | // SetPicture sets the picture property of the i-th vertex.
279 | func (gt *GLTriangles) SetPicture(i int, pic pixel.Vec, intensity float64) {
280 | gt.data[gt.index(i, triPicX)] = float32(pic.X)
281 | gt.data[gt.index(i, triPicY)] = float32(pic.Y)
282 | gt.data[gt.index(i, triIntensity)] = float32(intensity)
283 | }
284 |
285 | // ClipRect returns the Clipping rectangle property of the i-th vertex.
286 | func (gt *GLTriangles) ClipRect(i int) (rect pixel.Rect, is bool) {
287 | mx := gt.data[gt.index(i, triClipMinX)]
288 | my := gt.data[gt.index(i, triClipMinY)]
289 | ax := gt.data[gt.index(i, triClipMaxX)]
290 | ay := gt.data[gt.index(i, triClipMaxY)]
291 | rect = pixel.R(float64(mx), float64(my), float64(ax), float64(ay))
292 | is = rect.Area() != 0.0
293 | return
294 | }
295 |
296 | // SetClipRect sets the Clipping rectangle property of the i-th vertex.
297 | func (gt *GLTriangles) SetClipRect(i int, rect pixel.Rect) {
298 | gt.data[gt.index(i, triClipMinX)] = float32(rect.Min.X)
299 | gt.data[gt.index(i, triClipMinY)] = float32(rect.Min.Y)
300 | gt.data[gt.index(i, triClipMaxX)] = float32(rect.Max.X)
301 | gt.data[gt.index(i, triClipMaxY)] = float32(rect.Max.Y)
302 | }
303 |
--------------------------------------------------------------------------------
/pixelgl/input.go:
--------------------------------------------------------------------------------
1 | package pixelgl
2 |
3 | import (
4 | "time"
5 |
6 | "github.com/faiface/mainthread"
7 | "github.com/faiface/pixel"
8 | "github.com/go-gl/glfw/v3.3/glfw"
9 | )
10 |
11 | // Pressed returns whether the Button is currently pressed down.
12 | func (w *Window) Pressed(button Button) bool {
13 | return w.currInp.buttons[button]
14 | }
15 |
16 | // JustPressed returns whether the Button has been pressed in the last frame.
17 | func (w *Window) JustPressed(button Button) bool {
18 | return w.pressEvents[button]
19 | }
20 |
21 | // JustReleased returns whether the Button has been released in the last frame.
22 | func (w *Window) JustReleased(button Button) bool {
23 | return w.releaseEvents[button]
24 | }
25 |
26 | // Repeated returns whether a repeat event has been triggered on button.
27 | //
28 | // Repeat event occurs repeatedly when a button is held down for some time.
29 | func (w *Window) Repeated(button Button) bool {
30 | return w.currInp.repeat[button]
31 | }
32 |
33 | // MousePosition returns the current mouse position in the Window's Bounds.
34 | func (w *Window) MousePosition() pixel.Vec {
35 | return w.currInp.mouse
36 | }
37 |
38 | // MousePreviousPosition returns the previous mouse position in the Window's Bounds.
39 | func (w *Window) MousePreviousPosition() pixel.Vec {
40 | return w.prevInp.mouse
41 | }
42 |
43 | // SetMousePosition positions the mouse cursor anywhere within the Window's Bounds.
44 | func (w *Window) SetMousePosition(v pixel.Vec) {
45 | mainthread.Call(func() {
46 | if (v.X >= 0 && v.X <= w.bounds.W()) &&
47 | (v.Y >= 0 && v.Y <= w.bounds.H()) {
48 | w.window.SetCursorPos(
49 | v.X+w.bounds.Min.X,
50 | (w.bounds.H()-v.Y)+w.bounds.Min.Y,
51 | )
52 | w.prevInp.mouse = v
53 | w.currInp.mouse = v
54 | w.tempInp.mouse = v
55 | }
56 | })
57 | }
58 |
59 | // MouseInsideWindow returns true if the mouse position is within the Window's Bounds.
60 | func (w *Window) MouseInsideWindow() bool {
61 | return w.cursorInsideWindow
62 | }
63 |
64 | // MouseScroll returns the mouse scroll amount (in both axes) since the last call to Window.Update.
65 | func (w *Window) MouseScroll() pixel.Vec {
66 | return w.currInp.scroll
67 | }
68 |
69 | // Typed returns the text typed on the keyboard since the last call to Window.Update.
70 | func (w *Window) Typed() string {
71 | return w.currInp.typed
72 | }
73 |
74 | // Button is a keyboard or mouse button. Why distinguish?
75 | type Button int
76 |
77 | // List of all mouse buttons.
78 | const (
79 | MouseButton1 = Button(glfw.MouseButton1)
80 | MouseButton2 = Button(glfw.MouseButton2)
81 | MouseButton3 = Button(glfw.MouseButton3)
82 | MouseButton4 = Button(glfw.MouseButton4)
83 | MouseButton5 = Button(glfw.MouseButton5)
84 | MouseButton6 = Button(glfw.MouseButton6)
85 | MouseButton7 = Button(glfw.MouseButton7)
86 | MouseButton8 = Button(glfw.MouseButton8)
87 | MouseButtonLast = Button(glfw.MouseButtonLast)
88 | MouseButtonLeft = Button(glfw.MouseButtonLeft)
89 | MouseButtonRight = Button(glfw.MouseButtonRight)
90 | MouseButtonMiddle = Button(glfw.MouseButtonMiddle)
91 | )
92 |
93 | // List of all keyboard buttons.
94 | const (
95 | KeyUnknown = Button(glfw.KeyUnknown)
96 | KeySpace = Button(glfw.KeySpace)
97 | KeyApostrophe = Button(glfw.KeyApostrophe)
98 | KeyComma = Button(glfw.KeyComma)
99 | KeyMinus = Button(glfw.KeyMinus)
100 | KeyPeriod = Button(glfw.KeyPeriod)
101 | KeySlash = Button(glfw.KeySlash)
102 | Key0 = Button(glfw.Key0)
103 | Key1 = Button(glfw.Key1)
104 | Key2 = Button(glfw.Key2)
105 | Key3 = Button(glfw.Key3)
106 | Key4 = Button(glfw.Key4)
107 | Key5 = Button(glfw.Key5)
108 | Key6 = Button(glfw.Key6)
109 | Key7 = Button(glfw.Key7)
110 | Key8 = Button(glfw.Key8)
111 | Key9 = Button(glfw.Key9)
112 | KeySemicolon = Button(glfw.KeySemicolon)
113 | KeyEqual = Button(glfw.KeyEqual)
114 | KeyA = Button(glfw.KeyA)
115 | KeyB = Button(glfw.KeyB)
116 | KeyC = Button(glfw.KeyC)
117 | KeyD = Button(glfw.KeyD)
118 | KeyE = Button(glfw.KeyE)
119 | KeyF = Button(glfw.KeyF)
120 | KeyG = Button(glfw.KeyG)
121 | KeyH = Button(glfw.KeyH)
122 | KeyI = Button(glfw.KeyI)
123 | KeyJ = Button(glfw.KeyJ)
124 | KeyK = Button(glfw.KeyK)
125 | KeyL = Button(glfw.KeyL)
126 | KeyM = Button(glfw.KeyM)
127 | KeyN = Button(glfw.KeyN)
128 | KeyO = Button(glfw.KeyO)
129 | KeyP = Button(glfw.KeyP)
130 | KeyQ = Button(glfw.KeyQ)
131 | KeyR = Button(glfw.KeyR)
132 | KeyS = Button(glfw.KeyS)
133 | KeyT = Button(glfw.KeyT)
134 | KeyU = Button(glfw.KeyU)
135 | KeyV = Button(glfw.KeyV)
136 | KeyW = Button(glfw.KeyW)
137 | KeyX = Button(glfw.KeyX)
138 | KeyY = Button(glfw.KeyY)
139 | KeyZ = Button(glfw.KeyZ)
140 | KeyLeftBracket = Button(glfw.KeyLeftBracket)
141 | KeyBackslash = Button(glfw.KeyBackslash)
142 | KeyRightBracket = Button(glfw.KeyRightBracket)
143 | KeyGraveAccent = Button(glfw.KeyGraveAccent)
144 | KeyWorld1 = Button(glfw.KeyWorld1)
145 | KeyWorld2 = Button(glfw.KeyWorld2)
146 | KeyEscape = Button(glfw.KeyEscape)
147 | KeyEnter = Button(glfw.KeyEnter)
148 | KeyTab = Button(glfw.KeyTab)
149 | KeyBackspace = Button(glfw.KeyBackspace)
150 | KeyInsert = Button(glfw.KeyInsert)
151 | KeyDelete = Button(glfw.KeyDelete)
152 | KeyRight = Button(glfw.KeyRight)
153 | KeyLeft = Button(glfw.KeyLeft)
154 | KeyDown = Button(glfw.KeyDown)
155 | KeyUp = Button(glfw.KeyUp)
156 | KeyPageUp = Button(glfw.KeyPageUp)
157 | KeyPageDown = Button(glfw.KeyPageDown)
158 | KeyHome = Button(glfw.KeyHome)
159 | KeyEnd = Button(glfw.KeyEnd)
160 | KeyCapsLock = Button(glfw.KeyCapsLock)
161 | KeyScrollLock = Button(glfw.KeyScrollLock)
162 | KeyNumLock = Button(glfw.KeyNumLock)
163 | KeyPrintScreen = Button(glfw.KeyPrintScreen)
164 | KeyPause = Button(glfw.KeyPause)
165 | KeyF1 = Button(glfw.KeyF1)
166 | KeyF2 = Button(glfw.KeyF2)
167 | KeyF3 = Button(glfw.KeyF3)
168 | KeyF4 = Button(glfw.KeyF4)
169 | KeyF5 = Button(glfw.KeyF5)
170 | KeyF6 = Button(glfw.KeyF6)
171 | KeyF7 = Button(glfw.KeyF7)
172 | KeyF8 = Button(glfw.KeyF8)
173 | KeyF9 = Button(glfw.KeyF9)
174 | KeyF10 = Button(glfw.KeyF10)
175 | KeyF11 = Button(glfw.KeyF11)
176 | KeyF12 = Button(glfw.KeyF12)
177 | KeyF13 = Button(glfw.KeyF13)
178 | KeyF14 = Button(glfw.KeyF14)
179 | KeyF15 = Button(glfw.KeyF15)
180 | KeyF16 = Button(glfw.KeyF16)
181 | KeyF17 = Button(glfw.KeyF17)
182 | KeyF18 = Button(glfw.KeyF18)
183 | KeyF19 = Button(glfw.KeyF19)
184 | KeyF20 = Button(glfw.KeyF20)
185 | KeyF21 = Button(glfw.KeyF21)
186 | KeyF22 = Button(glfw.KeyF22)
187 | KeyF23 = Button(glfw.KeyF23)
188 | KeyF24 = Button(glfw.KeyF24)
189 | KeyF25 = Button(glfw.KeyF25)
190 | KeyKP0 = Button(glfw.KeyKP0)
191 | KeyKP1 = Button(glfw.KeyKP1)
192 | KeyKP2 = Button(glfw.KeyKP2)
193 | KeyKP3 = Button(glfw.KeyKP3)
194 | KeyKP4 = Button(glfw.KeyKP4)
195 | KeyKP5 = Button(glfw.KeyKP5)
196 | KeyKP6 = Button(glfw.KeyKP6)
197 | KeyKP7 = Button(glfw.KeyKP7)
198 | KeyKP8 = Button(glfw.KeyKP8)
199 | KeyKP9 = Button(glfw.KeyKP9)
200 | KeyKPDecimal = Button(glfw.KeyKPDecimal)
201 | KeyKPDivide = Button(glfw.KeyKPDivide)
202 | KeyKPMultiply = Button(glfw.KeyKPMultiply)
203 | KeyKPSubtract = Button(glfw.KeyKPSubtract)
204 | KeyKPAdd = Button(glfw.KeyKPAdd)
205 | KeyKPEnter = Button(glfw.KeyKPEnter)
206 | KeyKPEqual = Button(glfw.KeyKPEqual)
207 | KeyLeftShift = Button(glfw.KeyLeftShift)
208 | KeyLeftControl = Button(glfw.KeyLeftControl)
209 | KeyLeftAlt = Button(glfw.KeyLeftAlt)
210 | KeyLeftSuper = Button(glfw.KeyLeftSuper)
211 | KeyRightShift = Button(glfw.KeyRightShift)
212 | KeyRightControl = Button(glfw.KeyRightControl)
213 | KeyRightAlt = Button(glfw.KeyRightAlt)
214 | KeyRightSuper = Button(glfw.KeyRightSuper)
215 | KeyMenu = Button(glfw.KeyMenu)
216 | KeyLast = Button(glfw.KeyLast)
217 | )
218 |
219 | // String returns a human-readable string describing the Button.
220 | func (b Button) String() string {
221 | name, ok := buttonNames[b]
222 | if !ok {
223 | return "Invalid"
224 | }
225 | return name
226 | }
227 |
228 | var buttonNames = map[Button]string{
229 | MouseButton4: "MouseButton4",
230 | MouseButton5: "MouseButton5",
231 | MouseButton6: "MouseButton6",
232 | MouseButton7: "MouseButton7",
233 | MouseButton8: "MouseButton8",
234 | MouseButtonLeft: "MouseButtonLeft",
235 | MouseButtonRight: "MouseButtonRight",
236 | MouseButtonMiddle: "MouseButtonMiddle",
237 | KeyUnknown: "Unknown",
238 | KeySpace: "Space",
239 | KeyApostrophe: "Apostrophe",
240 | KeyComma: "Comma",
241 | KeyMinus: "Minus",
242 | KeyPeriod: "Period",
243 | KeySlash: "Slash",
244 | Key0: "0",
245 | Key1: "1",
246 | Key2: "2",
247 | Key3: "3",
248 | Key4: "4",
249 | Key5: "5",
250 | Key6: "6",
251 | Key7: "7",
252 | Key8: "8",
253 | Key9: "9",
254 | KeySemicolon: "Semicolon",
255 | KeyEqual: "Equal",
256 | KeyA: "A",
257 | KeyB: "B",
258 | KeyC: "C",
259 | KeyD: "D",
260 | KeyE: "E",
261 | KeyF: "F",
262 | KeyG: "G",
263 | KeyH: "H",
264 | KeyI: "I",
265 | KeyJ: "J",
266 | KeyK: "K",
267 | KeyL: "L",
268 | KeyM: "M",
269 | KeyN: "N",
270 | KeyO: "O",
271 | KeyP: "P",
272 | KeyQ: "Q",
273 | KeyR: "R",
274 | KeyS: "S",
275 | KeyT: "T",
276 | KeyU: "U",
277 | KeyV: "V",
278 | KeyW: "W",
279 | KeyX: "X",
280 | KeyY: "Y",
281 | KeyZ: "Z",
282 | KeyLeftBracket: "LeftBracket",
283 | KeyBackslash: "Backslash",
284 | KeyRightBracket: "RightBracket",
285 | KeyGraveAccent: "GraveAccent",
286 | KeyWorld1: "World1",
287 | KeyWorld2: "World2",
288 | KeyEscape: "Escape",
289 | KeyEnter: "Enter",
290 | KeyTab: "Tab",
291 | KeyBackspace: "Backspace",
292 | KeyInsert: "Insert",
293 | KeyDelete: "Delete",
294 | KeyRight: "Right",
295 | KeyLeft: "Left",
296 | KeyDown: "Down",
297 | KeyUp: "Up",
298 | KeyPageUp: "PageUp",
299 | KeyPageDown: "PageDown",
300 | KeyHome: "Home",
301 | KeyEnd: "End",
302 | KeyCapsLock: "CapsLock",
303 | KeyScrollLock: "ScrollLock",
304 | KeyNumLock: "NumLock",
305 | KeyPrintScreen: "PrintScreen",
306 | KeyPause: "Pause",
307 | KeyF1: "F1",
308 | KeyF2: "F2",
309 | KeyF3: "F3",
310 | KeyF4: "F4",
311 | KeyF5: "F5",
312 | KeyF6: "F6",
313 | KeyF7: "F7",
314 | KeyF8: "F8",
315 | KeyF9: "F9",
316 | KeyF10: "F10",
317 | KeyF11: "F11",
318 | KeyF12: "F12",
319 | KeyF13: "F13",
320 | KeyF14: "F14",
321 | KeyF15: "F15",
322 | KeyF16: "F16",
323 | KeyF17: "F17",
324 | KeyF18: "F18",
325 | KeyF19: "F19",
326 | KeyF20: "F20",
327 | KeyF21: "F21",
328 | KeyF22: "F22",
329 | KeyF23: "F23",
330 | KeyF24: "F24",
331 | KeyF25: "F25",
332 | KeyKP0: "KP0",
333 | KeyKP1: "KP1",
334 | KeyKP2: "KP2",
335 | KeyKP3: "KP3",
336 | KeyKP4: "KP4",
337 | KeyKP5: "KP5",
338 | KeyKP6: "KP6",
339 | KeyKP7: "KP7",
340 | KeyKP8: "KP8",
341 | KeyKP9: "KP9",
342 | KeyKPDecimal: "KPDecimal",
343 | KeyKPDivide: "KPDivide",
344 | KeyKPMultiply: "KPMultiply",
345 | KeyKPSubtract: "KPSubtract",
346 | KeyKPAdd: "KPAdd",
347 | KeyKPEnter: "KPEnter",
348 | KeyKPEqual: "KPEqual",
349 | KeyLeftShift: "LeftShift",
350 | KeyLeftControl: "LeftControl",
351 | KeyLeftAlt: "LeftAlt",
352 | KeyLeftSuper: "LeftSuper",
353 | KeyRightShift: "RightShift",
354 | KeyRightControl: "RightControl",
355 | KeyRightAlt: "RightAlt",
356 | KeyRightSuper: "RightSuper",
357 | KeyMenu: "Menu",
358 | }
359 |
360 | func (w *Window) initInput() {
361 | mainthread.Call(func() {
362 | w.window.SetMouseButtonCallback(func(_ *glfw.Window, button glfw.MouseButton, action glfw.Action, mod glfw.ModifierKey) {
363 | switch action {
364 | case glfw.Press:
365 | w.tempPressEvents[Button(button)] = true
366 | w.tempInp.buttons[Button(button)] = true
367 | case glfw.Release:
368 | w.tempReleaseEvents[Button(button)] = true
369 | w.tempInp.buttons[Button(button)] = false
370 | }
371 | })
372 |
373 | w.window.SetKeyCallback(func(_ *glfw.Window, key glfw.Key, scancode int, action glfw.Action, mods glfw.ModifierKey) {
374 | if key == glfw.KeyUnknown {
375 | return
376 | }
377 | switch action {
378 | case glfw.Press:
379 | w.tempPressEvents[Button(key)] = true
380 | w.tempInp.buttons[Button(key)] = true
381 | case glfw.Release:
382 | w.tempReleaseEvents[Button(key)] = true
383 | w.tempInp.buttons[Button(key)] = false
384 | case glfw.Repeat:
385 | w.tempInp.repeat[Button(key)] = true
386 | }
387 | })
388 |
389 | w.window.SetCursorEnterCallback(func(_ *glfw.Window, entered bool) {
390 | w.cursorInsideWindow = entered
391 | })
392 |
393 | w.window.SetCursorPosCallback(func(_ *glfw.Window, x, y float64) {
394 | w.tempInp.mouse = pixel.V(
395 | x+w.bounds.Min.X,
396 | (w.bounds.H()-y)+w.bounds.Min.Y,
397 | )
398 | })
399 |
400 | w.window.SetScrollCallback(func(_ *glfw.Window, xoff, yoff float64) {
401 | w.tempInp.scroll.X += xoff
402 | w.tempInp.scroll.Y += yoff
403 | })
404 |
405 | w.window.SetCharCallback(func(_ *glfw.Window, r rune) {
406 | w.tempInp.typed += string(r)
407 | })
408 | })
409 | }
410 |
411 | // UpdateInput polls window events. Call this function to poll window events
412 | // without swapping buffers. Note that the Update method invokes UpdateInput.
413 | func (w *Window) UpdateInput() {
414 | mainthread.Call(func() {
415 | glfw.PollEvents()
416 | })
417 | w.doUpdateInput()
418 | }
419 |
420 | // UpdateInputWait blocks until an event is received or a timeout. If timeout is 0
421 | // then it will wait indefinitely
422 | func (w *Window) UpdateInputWait(timeout time.Duration) {
423 | mainthread.Call(func() {
424 | if timeout <= 0 {
425 | glfw.WaitEvents()
426 | } else {
427 | glfw.WaitEventsTimeout(timeout.Seconds())
428 | }
429 | })
430 | w.doUpdateInput()
431 | }
432 |
433 | // internal input bookkeeping
434 | func (w *Window) doUpdateInput() {
435 | w.prevInp = w.currInp
436 | w.currInp = w.tempInp
437 |
438 | w.pressEvents = w.tempPressEvents
439 | w.releaseEvents = w.tempReleaseEvents
440 |
441 | // Clear last frame's temporary status
442 | w.tempPressEvents = [KeyLast + 1]bool{}
443 | w.tempReleaseEvents = [KeyLast + 1]bool{}
444 | w.tempInp.repeat = [KeyLast + 1]bool{}
445 | w.tempInp.scroll = pixel.ZV
446 | w.tempInp.typed = ""
447 |
448 | w.updateJoystickInput()
449 | }
450 |
--------------------------------------------------------------------------------
/pixelgl/joystick.go:
--------------------------------------------------------------------------------
1 | package pixelgl
2 |
3 | import (
4 | "github.com/go-gl/glfw/v3.3/glfw"
5 | )
6 |
7 | // Joystick is a joystick or controller (gamepad).
8 | type Joystick int
9 |
10 | // List all of the joysticks.
11 | const (
12 | Joystick1 = Joystick(glfw.Joystick1)
13 | Joystick2 = Joystick(glfw.Joystick2)
14 | Joystick3 = Joystick(glfw.Joystick3)
15 | Joystick4 = Joystick(glfw.Joystick4)
16 | Joystick5 = Joystick(glfw.Joystick5)
17 | Joystick6 = Joystick(glfw.Joystick6)
18 | Joystick7 = Joystick(glfw.Joystick7)
19 | Joystick8 = Joystick(glfw.Joystick8)
20 | Joystick9 = Joystick(glfw.Joystick9)
21 | Joystick10 = Joystick(glfw.Joystick10)
22 | Joystick11 = Joystick(glfw.Joystick11)
23 | Joystick12 = Joystick(glfw.Joystick12)
24 | Joystick13 = Joystick(glfw.Joystick13)
25 | Joystick14 = Joystick(glfw.Joystick14)
26 | Joystick15 = Joystick(glfw.Joystick15)
27 | Joystick16 = Joystick(glfw.Joystick16)
28 |
29 | JoystickLast = Joystick(glfw.JoystickLast)
30 | )
31 |
32 | // GamepadAxis corresponds to a gamepad axis.
33 | type GamepadAxis int
34 |
35 | // Gamepad axis IDs.
36 | const (
37 | AxisLeftX = GamepadAxis(glfw.AxisLeftX)
38 | AxisLeftY = GamepadAxis(glfw.AxisLeftY)
39 | AxisRightX = GamepadAxis(glfw.AxisRightX)
40 | AxisRightY = GamepadAxis(glfw.AxisRightY)
41 | AxisLeftTrigger = GamepadAxis(glfw.AxisLeftTrigger)
42 | AxisRightTrigger = GamepadAxis(glfw.AxisRightTrigger)
43 | AxisLast = GamepadAxis(glfw.AxisLast)
44 | )
45 |
46 | // GamepadButton corresponds to a gamepad button.
47 | type GamepadButton int
48 |
49 | // Gamepad button IDs.
50 | const (
51 | ButtonA = GamepadButton(glfw.ButtonA)
52 | ButtonB = GamepadButton(glfw.ButtonB)
53 | ButtonX = GamepadButton(glfw.ButtonX)
54 | ButtonY = GamepadButton(glfw.ButtonY)
55 | ButtonLeftBumper = GamepadButton(glfw.ButtonLeftBumper)
56 | ButtonRightBumper = GamepadButton(glfw.ButtonRightBumper)
57 | ButtonBack = GamepadButton(glfw.ButtonBack)
58 | ButtonStart = GamepadButton(glfw.ButtonStart)
59 | ButtonGuide = GamepadButton(glfw.ButtonGuide)
60 | ButtonLeftThumb = GamepadButton(glfw.ButtonLeftThumb)
61 | ButtonRightThumb = GamepadButton(glfw.ButtonRightThumb)
62 | ButtonDpadUp = GamepadButton(glfw.ButtonDpadUp)
63 | ButtonDpadRight = GamepadButton(glfw.ButtonDpadRight)
64 | ButtonDpadDown = GamepadButton(glfw.ButtonDpadDown)
65 | ButtonDpadLeft = GamepadButton(glfw.ButtonDpadLeft)
66 | ButtonLast = GamepadButton(glfw.ButtonLast)
67 | ButtonCross = GamepadButton(glfw.ButtonCross)
68 | ButtonCircle = GamepadButton(glfw.ButtonCircle)
69 | ButtonSquare = GamepadButton(glfw.ButtonSquare)
70 | ButtonTriangle = GamepadButton(glfw.ButtonTriangle)
71 | )
72 |
73 | // JoystickPresent returns if the joystick is currently connected.
74 | //
75 | // This API is experimental.
76 | func (w *Window) JoystickPresent(js Joystick) bool {
77 | return w.currJoy.connected[js]
78 | }
79 |
80 | // JoystickName returns the name of the joystick. A disconnected joystick will return an
81 | // empty string.
82 | //
83 | // This API is experimental.
84 | func (w *Window) JoystickName(js Joystick) string {
85 | return w.currJoy.name[js]
86 | }
87 |
88 | // JoystickButtonCount returns the number of buttons a connected joystick has.
89 | //
90 | // This API is experimental.
91 | func (w *Window) JoystickButtonCount(js Joystick) int {
92 | return len(w.currJoy.buttons[js])
93 | }
94 |
95 | // JoystickAxisCount returns the number of axes a connected joystick has.
96 | //
97 | // This API is experimental.
98 | func (w *Window) JoystickAxisCount(js Joystick) int {
99 | return len(w.currJoy.axis[js])
100 | }
101 |
102 | // JoystickPressed returns whether the joystick Button is currently pressed down.
103 | // If the button index is out of range, this will return false.
104 | //
105 | // This API is experimental.
106 | func (w *Window) JoystickPressed(js Joystick, button GamepadButton) bool {
107 | return w.currJoy.getButton(js, int(button))
108 | }
109 |
110 | // JoystickJustPressed returns whether the joystick Button has just been pressed down.
111 | // If the button index is out of range, this will return false.
112 | //
113 | // This API is experimental.
114 | func (w *Window) JoystickJustPressed(js Joystick, button GamepadButton) bool {
115 | return w.currJoy.getButton(js, int(button)) && !w.prevJoy.getButton(js, int(button))
116 | }
117 |
118 | // JoystickJustReleased returns whether the joystick Button has just been released up.
119 | // If the button index is out of range, this will return false.
120 | //
121 | // This API is experimental.
122 | func (w *Window) JoystickJustReleased(js Joystick, button GamepadButton) bool {
123 | return !w.currJoy.getButton(js, int(button)) && w.prevJoy.getButton(js, int(button))
124 | }
125 |
126 | // JoystickAxis returns the value of a joystick axis at the last call to Window.Update.
127 | // If the axis index is out of range, this will return 0.
128 | //
129 | // This API is experimental.
130 | func (w *Window) JoystickAxis(js Joystick, axis GamepadAxis) float64 {
131 | return w.currJoy.getAxis(js, int(axis))
132 | }
133 |
134 | // Used internally during Window.UpdateInput to update the state of the joysticks.
135 | func (w *Window) updateJoystickInput() {
136 | for js := Joystick1; js <= JoystickLast; js++ {
137 | // Determine and store if the joystick was connected
138 | joystickPresent := glfw.Joystick(js).Present()
139 | w.tempJoy.connected[js] = joystickPresent
140 |
141 | if joystickPresent {
142 | if glfw.Joystick(js).IsGamepad() {
143 | gamepadInputs := glfw.Joystick(js).GetGamepadState()
144 |
145 | w.tempJoy.buttons[js] = gamepadInputs.Buttons[:]
146 | w.tempJoy.axis[js] = gamepadInputs.Axes[:]
147 | } else {
148 | w.tempJoy.buttons[js] = glfw.Joystick(js).GetButtons()
149 | w.tempJoy.axis[js] = glfw.Joystick(js).GetAxes()
150 | }
151 |
152 | if !w.currJoy.connected[js] {
153 | // The joystick was recently connected, we get the name
154 | w.tempJoy.name[js] = glfw.Joystick(js).GetName()
155 | } else {
156 | // Use the name from the previous one
157 | w.tempJoy.name[js] = w.currJoy.name[js]
158 | }
159 | } else {
160 | w.tempJoy.buttons[js] = []glfw.Action{}
161 | w.tempJoy.axis[js] = []float32{}
162 | w.tempJoy.name[js] = ""
163 | }
164 | }
165 |
166 | w.prevJoy = w.currJoy
167 | w.currJoy = w.tempJoy
168 | }
169 |
170 | type joystickState struct {
171 | connected [JoystickLast + 1]bool
172 | name [JoystickLast + 1]string
173 | buttons [JoystickLast + 1][]glfw.Action
174 | axis [JoystickLast + 1][]float32
175 | }
176 |
177 | // Returns if a button on a joystick is down, returning false if the button or joystick is invalid.
178 | func (js *joystickState) getButton(joystick Joystick, button int) bool {
179 | // Check that the joystick and button is valid, return false by default
180 | if js.buttons[joystick] == nil || button >= len(js.buttons[joystick]) || button < 0 {
181 | return false
182 | }
183 | return js.buttons[joystick][byte(button)] == glfw.Press
184 | }
185 |
186 | // Returns the value of a joystick axis, returning 0 if the button or joystick is invalid.
187 | func (js *joystickState) getAxis(joystick Joystick, axis int) float64 {
188 | // Check that the joystick and axis is valid, return 0 by default.
189 | if js.axis[joystick] == nil || axis >= len(js.axis[joystick]) || axis < 0 {
190 | return 0
191 | }
192 | return float64(js.axis[joystick][axis])
193 | }
194 |
--------------------------------------------------------------------------------
/pixelgl/monitor.go:
--------------------------------------------------------------------------------
1 | package pixelgl
2 |
3 | import (
4 | "github.com/faiface/mainthread"
5 | "github.com/go-gl/glfw/v3.3/glfw"
6 | )
7 |
8 | // Monitor represents a physical display attached to your computer.
9 | type Monitor struct {
10 | monitor *glfw.Monitor
11 | }
12 |
13 | // VideoMode represents all properties of a video mode and is
14 | // associated with a monitor if it is used in fullscreen mode.
15 | type VideoMode struct {
16 | // Width is the width of the vide mode in pixels.
17 | Width int
18 | // Height is the height of the video mode in pixels.
19 | Height int
20 | // RefreshRate holds the refresh rate of the associated monitor in Hz.
21 | RefreshRate int
22 | }
23 |
24 | // PrimaryMonitor returns the main monitor (usually the one with the taskbar and stuff).
25 | func PrimaryMonitor() *Monitor {
26 | var monitor *glfw.Monitor
27 | mainthread.Call(func() {
28 | monitor = glfw.GetPrimaryMonitor()
29 | })
30 | return &Monitor{
31 | monitor: monitor,
32 | }
33 | }
34 |
35 | // Monitors returns a slice of all currently available monitors.
36 | func Monitors() []*Monitor {
37 | var monitors []*Monitor
38 | mainthread.Call(func() {
39 | for _, monitor := range glfw.GetMonitors() {
40 | monitors = append(monitors, &Monitor{monitor: monitor})
41 | }
42 | })
43 | return monitors
44 | }
45 |
46 | // Name returns a human-readable name of the Monitor.
47 | func (m *Monitor) Name() string {
48 | var name string
49 | mainthread.Call(func() {
50 | name = m.monitor.GetName()
51 | })
52 | return name
53 | }
54 |
55 | // PhysicalSize returns the size of the display area of the Monitor in millimeters.
56 | func (m *Monitor) PhysicalSize() (width, height float64) {
57 | var wi, hi int
58 | mainthread.Call(func() {
59 | wi, hi = m.monitor.GetPhysicalSize()
60 | })
61 | width = float64(wi)
62 | height = float64(hi)
63 | return
64 | }
65 |
66 | // Position returns the position of the upper-left corner of the Monitor in screen coordinates.
67 | func (m *Monitor) Position() (x, y float64) {
68 | var xi, yi int
69 | mainthread.Call(func() {
70 | xi, yi = m.monitor.GetPos()
71 | })
72 | x = float64(xi)
73 | y = float64(yi)
74 | return
75 | }
76 |
77 | // Size returns the resolution of the Monitor in pixels.
78 | func (m *Monitor) Size() (width, height float64) {
79 | var mode *glfw.VidMode
80 | mainthread.Call(func() {
81 | mode = m.monitor.GetVideoMode()
82 | })
83 | width = float64(mode.Width)
84 | height = float64(mode.Height)
85 | return
86 | }
87 |
88 | // BitDepth returns the number of bits per color of the Monitor.
89 | func (m *Monitor) BitDepth() (red, green, blue int) {
90 | var mode *glfw.VidMode
91 | mainthread.Call(func() {
92 | mode = m.monitor.GetVideoMode()
93 | })
94 | red = mode.RedBits
95 | green = mode.GreenBits
96 | blue = mode.BlueBits
97 | return
98 | }
99 |
100 | // RefreshRate returns the refresh frequency of the Monitor in Hz (refreshes/second).
101 | func (m *Monitor) RefreshRate() (rate float64) {
102 | var mode *glfw.VidMode
103 | mainthread.Call(func() {
104 | mode = m.monitor.GetVideoMode()
105 | })
106 | rate = float64(mode.RefreshRate)
107 | return
108 | }
109 |
110 | // VideoModes returns all available video modes for the monitor.
111 | func (m *Monitor) VideoModes() (vmodes []VideoMode) {
112 | var modes []*glfw.VidMode
113 | mainthread.Call(func() {
114 | modes = m.monitor.GetVideoModes()
115 | })
116 | for _, mode := range modes {
117 | vmodes = append(vmodes, VideoMode{
118 | Width: mode.Width,
119 | Height: mode.Height,
120 | RefreshRate: mode.RefreshRate,
121 | })
122 | }
123 | return
124 | }
125 |
--------------------------------------------------------------------------------
/pixelgl/run.go:
--------------------------------------------------------------------------------
1 | package pixelgl
2 |
3 | import (
4 | "github.com/faiface/mainthread"
5 | "github.com/go-gl/glfw/v3.3/glfw"
6 | "github.com/pkg/errors"
7 | )
8 |
9 | // Run is essentially the main function of PixelGL. It exists mainly due to the technical
10 | // limitations of OpenGL and operating systems. In short, all graphics and window manipulating calls
11 | // must be done from the main thread. Run makes this possible.
12 | //
13 | // Call this function from the main function of your application. This is necessary, so that Run
14 | // runs on the main thread.
15 | //
16 | // func run() {
17 | // // interact with Pixel and PixelGL from here (even concurrently)
18 | // }
19 | //
20 | // func main() {
21 | // pixel.Run(run)
22 | // }
23 | //
24 | // You can spawn any number of goroutines from your run function and interact with PixelGL
25 | // concurrently. The only condition is that the Run function is called from your main function.
26 | func Run(run func()) {
27 | err := glfw.Init()
28 | if err != nil {
29 | panic(errors.Wrap(err, "failed to initialize GLFW"))
30 | }
31 | defer glfw.Terminate()
32 | mainthread.Run(run)
33 | }
34 |
--------------------------------------------------------------------------------
/pixelgl/util.go:
--------------------------------------------------------------------------------
1 | package pixelgl
2 |
3 | import (
4 | "math"
5 |
6 | "github.com/faiface/pixel"
7 | )
8 |
9 | func intBounds(bounds pixel.Rect) (x, y, w, h int) {
10 | x0 := int(math.Floor(bounds.Min.X))
11 | y0 := int(math.Floor(bounds.Min.Y))
12 | x1 := int(math.Ceil(bounds.Max.X))
13 | y1 := int(math.Ceil(bounds.Max.Y))
14 | return x0, y0, x1 - x0, y1 - y0
15 | }
16 |
--------------------------------------------------------------------------------
/rectangle.go:
--------------------------------------------------------------------------------
1 | package pixel
2 |
3 | import (
4 | "fmt"
5 | "math"
6 | )
7 |
8 | // Rect is a 2D rectangle aligned with the axes of the coordinate system. It is defined by two
9 | // points, Min and Max.
10 | //
11 | // The invariant should hold, that Max's components are greater or equal than Min's components
12 | // respectively.
13 | type Rect struct {
14 | Min, Max Vec
15 | }
16 |
17 | // ZR is a zero rectangle.
18 | var ZR = Rect{Min: ZV, Max: ZV}
19 |
20 | // R returns a new Rect with given the Min and Max coordinates.
21 | //
22 | // Note that the returned rectangle is not automatically normalized.
23 | func R(minX, minY, maxX, maxY float64) Rect {
24 | return Rect{
25 | Min: Vec{minX, minY},
26 | Max: Vec{maxX, maxY},
27 | }
28 | }
29 |
30 | // String returns the string representation of the Rect.
31 | //
32 | // r := pixel.R(100, 50, 200, 300)
33 | // r.String() // returns "Rect(100, 50, 200, 300)"
34 | // fmt.Println(r) // Rect(100, 50, 200, 300)
35 | func (r Rect) String() string {
36 | return fmt.Sprintf("Rect(%v, %v, %v, %v)", r.Min.X, r.Min.Y, r.Max.X, r.Max.Y)
37 | }
38 |
39 | // Norm returns the Rect in normal form, such that Max is component-wise greater or equal than Min.
40 | func (r Rect) Norm() Rect {
41 | return Rect{
42 | Min: Vec{
43 | math.Min(r.Min.X, r.Max.X),
44 | math.Min(r.Min.Y, r.Max.Y),
45 | },
46 | Max: Vec{
47 | math.Max(r.Min.X, r.Max.X),
48 | math.Max(r.Min.Y, r.Max.Y),
49 | },
50 | }
51 | }
52 |
53 | // W returns the width of the Rect.
54 | func (r Rect) W() float64 {
55 | return r.Max.X - r.Min.X
56 | }
57 |
58 | // H returns the height of the Rect.
59 | func (r Rect) H() float64 {
60 | return r.Max.Y - r.Min.Y
61 | }
62 |
63 | // Size returns the vector of width and height of the Rect.
64 | func (r Rect) Size() Vec {
65 | return V(r.W(), r.H())
66 | }
67 |
68 | // Area returns the area of r. If r is not normalized, area may be negative.
69 | func (r Rect) Area() float64 {
70 | return r.W() * r.H()
71 | }
72 |
73 | // Edges will return the four lines which make up the edges of the rectangle.
74 | func (r Rect) Edges() [4]Line {
75 | corners := r.Vertices()
76 |
77 | return [4]Line{
78 | {A: corners[0], B: corners[1]},
79 | {A: corners[1], B: corners[2]},
80 | {A: corners[2], B: corners[3]},
81 | {A: corners[3], B: corners[0]},
82 | }
83 | }
84 |
85 | // Anchor is a vector used to define anchors, such as `Center`, `Top`, `TopRight`, etc.
86 | type Anchor Vec
87 |
88 | var (
89 | Center = Anchor{0.5, 0.5}
90 | Top = Anchor{0.5, 0}
91 | TopRight = Anchor{0, 0}
92 | Right = Anchor{0, 0.5}
93 | BottomRight = Anchor{0, 1}
94 | Bottom = Anchor{0.5, 1}
95 | BottomLeft = Anchor{1, 1}
96 | Left = Anchor{1, 0.5}
97 | TopLeft = Anchor{1, 0}
98 | )
99 |
100 | var anchorStrings map[Anchor]string = map[Anchor]string{
101 | Center: "center",
102 | Top: "top",
103 | TopRight: "top-right",
104 | Right: "right",
105 | BottomRight: "bottom-right",
106 | Bottom: "bottom",
107 | BottomLeft: "bottom-left",
108 | Left: "left",
109 | TopLeft: "top-left",
110 | }
111 |
112 | // String returns the string representation of an anchor.
113 | func (anchor Anchor) String() string {
114 | return anchorStrings[anchor]
115 | }
116 |
117 | var oppositeAnchors map[Anchor]Anchor = map[Anchor]Anchor{
118 | Center: Center,
119 | Top: Bottom,
120 | Bottom: Top,
121 | Right: Left,
122 | Left: Right,
123 | TopRight: BottomLeft,
124 | BottomLeft: TopRight,
125 | BottomRight: TopLeft,
126 | TopLeft: BottomRight,
127 | }
128 |
129 | // Opposite returns the opposite position of the anchor (ie. Top -> Bottom; BottomLeft -> TopRight, etc.).
130 | func (anchor Anchor) Opposite() Anchor {
131 | return oppositeAnchors[anchor]
132 | }
133 |
134 | // AnchorPos returns the relative position of the given anchor.
135 | func (r Rect) AnchorPos(anchor Anchor) Vec {
136 | return r.Size().ScaledXY(V(0, 0).Sub(Vec(anchor)))
137 | }
138 |
139 | // AlignedTo returns the rect moved by the given anchor.
140 | func (rect Rect) AlignedTo(anchor Anchor) Rect {
141 | return rect.Moved(rect.AnchorPos(anchor))
142 | }
143 |
144 | // Center returns the position of the center of the Rect.
145 | // `rect.Center()` is equivalent to `rect.Anchor(pixel.Anchor.Center)`
146 | func (r Rect) Center() Vec {
147 | return Lerp(r.Min, r.Max, 0.5)
148 | }
149 |
150 | // Moved returns the Rect moved (both Min and Max) by the given vector delta.
151 | func (r Rect) Moved(delta Vec) Rect {
152 | return Rect{
153 | Min: r.Min.Add(delta),
154 | Max: r.Max.Add(delta),
155 | }
156 | }
157 |
158 | // Resized returns the Rect resized to the given size while keeping the position of the given
159 | // anchor.
160 | //
161 | // r.Resized(r.Min, size) // resizes while keeping the position of the lower-left corner
162 | // r.Resized(r.Max, size) // same with the top-right corner
163 | // r.Resized(r.Center(), size) // resizes around the center
164 | //
165 | // This function does not make sense for resizing a rectangle of zero area and will panic. Use
166 | // ResizedMin in the case of zero area.
167 | func (r Rect) Resized(anchor, size Vec) Rect {
168 | if r.W()*r.H() == 0 {
169 | panic(fmt.Errorf("(%T).Resize: zero area", r))
170 | }
171 | fraction := Vec{size.X / r.W(), size.Y / r.H()}
172 | return Rect{
173 | Min: anchor.Add(r.Min.Sub(anchor).ScaledXY(fraction)),
174 | Max: anchor.Add(r.Max.Sub(anchor).ScaledXY(fraction)),
175 | }
176 | }
177 |
178 | // ResizedMin returns the Rect resized to the given size while keeping the position of the Rect's
179 | // Min.
180 | //
181 | // Sizes of zero area are safe here.
182 | func (r Rect) ResizedMin(size Vec) Rect {
183 | return Rect{
184 | Min: r.Min,
185 | Max: r.Min.Add(size),
186 | }
187 | }
188 |
189 | // Contains checks whether a vector u is contained within this Rect (including it's borders).
190 | func (r Rect) Contains(u Vec) bool {
191 | return r.Min.X <= u.X && u.X <= r.Max.X && r.Min.Y <= u.Y && u.Y <= r.Max.Y
192 | }
193 |
194 | // Union returns the minimal Rect which covers both r and s. Rects r and s must be normalized.
195 | func (r Rect) Union(s Rect) Rect {
196 | return R(
197 | math.Min(r.Min.X, s.Min.X),
198 | math.Min(r.Min.Y, s.Min.Y),
199 | math.Max(r.Max.X, s.Max.X),
200 | math.Max(r.Max.Y, s.Max.Y),
201 | )
202 | }
203 |
204 | // Intersect returns the maximal Rect which is covered by both r and s. Rects r and s must be normalized.
205 | //
206 | // If r and s don't overlap, this function returns a zero-rectangle.
207 | func (r Rect) Intersect(s Rect) Rect {
208 | t := R(
209 | math.Max(r.Min.X, s.Min.X),
210 | math.Max(r.Min.Y, s.Min.Y),
211 | math.Min(r.Max.X, s.Max.X),
212 | math.Min(r.Max.Y, s.Max.Y),
213 | )
214 | if t.Min.X >= t.Max.X || t.Min.Y >= t.Max.Y {
215 | return ZR
216 | }
217 | return t
218 | }
219 |
220 | // Intersects returns whether or not the given Rect intersects at any point with this Rect.
221 | //
222 | // This function is overall about 5x faster than Intersect, so it is better
223 | // to use if you have no need for the returned Rect from Intersect.
224 | func (r Rect) Intersects(s Rect) bool {
225 | return !(s.Max.X <= r.Min.X ||
226 | s.Min.X >= r.Max.X ||
227 | s.Max.Y <= r.Min.Y ||
228 | s.Min.Y >= r.Max.Y)
229 | }
230 |
231 | // IntersectCircle returns a minimal required Vector, such that moving the rect by that vector would stop the Circle
232 | // and the Rect intersecting. This function returns a zero-vector if the Circle and Rect do not overlap, and if only
233 | // the perimeters touch.
234 | //
235 | // This function will return a non-zero vector if:
236 | // - The Rect contains the Circle, partially or fully
237 | // - The Circle contains the Rect, partially of fully
238 | func (r Rect) IntersectCircle(c Circle) Vec {
239 | return c.IntersectRect(r).Scaled(-1)
240 | }
241 |
242 | // IntersectLine will return the shortest Vec such that if the Rect is moved by the Vec returned, the Line and Rect no
243 | // longer intersect.
244 | func (r Rect) IntersectLine(l Line) Vec {
245 | return l.IntersectRect(r).Scaled(-1)
246 | }
247 |
248 | // IntersectionPoints returns all the points where the Rect intersects with the line provided. This can be zero, one or
249 | // two points, depending on the location of the shapes. The points of intersection will be returned in order of
250 | // closest-to-l.A to closest-to-l.B.
251 | func (r Rect) IntersectionPoints(l Line) []Vec {
252 | // Use map keys to ensure unique points
253 | pointMap := make(map[Vec]struct{})
254 |
255 | for _, edge := range r.Edges() {
256 | if intersect, ok := l.Intersect(edge); ok {
257 | pointMap[intersect] = struct{}{}
258 | }
259 | }
260 |
261 | points := make([]Vec, 0, len(pointMap))
262 | for point := range pointMap {
263 | points = append(points, point)
264 | }
265 |
266 | // Order the points
267 | if len(points) == 2 {
268 | if points[1].To(l.A).Len() < points[0].To(l.A).Len() {
269 | return []Vec{points[1], points[0]}
270 | }
271 | }
272 |
273 | return points
274 | }
275 |
276 | // Vertices returns a slice of the four corners which make up the rectangle.
277 | func (r Rect) Vertices() [4]Vec {
278 | return [4]Vec{
279 | r.Min,
280 | V(r.Min.X, r.Max.Y),
281 | r.Max,
282 | V(r.Max.X, r.Min.Y),
283 | }
284 | }
285 |
--------------------------------------------------------------------------------
/rectangle_test.go:
--------------------------------------------------------------------------------
1 | package pixel_test
2 |
3 | import (
4 | "fmt"
5 | "reflect"
6 | "testing"
7 |
8 | "github.com/faiface/pixel"
9 | )
10 |
11 | func TestRect_Resize(t *testing.T) {
12 | type rectTestTransform struct {
13 | name string
14 | f func(pixel.Rect) pixel.Rect
15 | }
16 |
17 | // rectangles
18 | squareAroundOrigin := pixel.R(-10, -10, 10, 10)
19 | squareAround2020 := pixel.R(10, 10, 30, 30)
20 | rectangleAroundOrigin := pixel.R(-20, -10, 20, 10)
21 | rectangleAround2020 := pixel.R(0, 10, 40, 30)
22 |
23 | // resize transformations
24 | resizeByHalfAroundCenter := rectTestTransform{"by half around center", func(rect pixel.Rect) pixel.Rect {
25 | return rect.Resized(rect.Center(), rect.Size().Scaled(0.5))
26 | }}
27 | resizeByHalfAroundMin := rectTestTransform{"by half around Min", func(rect pixel.Rect) pixel.Rect {
28 | return rect.Resized(rect.Min, rect.Size().Scaled(0.5))
29 | }}
30 | resizeByHalfAroundMax := rectTestTransform{"by half around Max", func(rect pixel.Rect) pixel.Rect {
31 | return rect.Resized(rect.Max, rect.Size().Scaled(0.5))
32 | }}
33 | resizeByHalfAroundMiddleOfLeftSide := rectTestTransform{"by half around middle of left side", func(rect pixel.Rect) pixel.Rect {
34 | return rect.Resized(pixel.V(rect.Min.X, rect.Center().Y), rect.Size().Scaled(0.5))
35 | }}
36 | resizeByHalfAroundOrigin := rectTestTransform{"by half around the origin", func(rect pixel.Rect) pixel.Rect {
37 | return rect.Resized(pixel.ZV, rect.Size().Scaled(0.5))
38 | }}
39 |
40 | testCases := []struct {
41 | input pixel.Rect
42 | transform rectTestTransform
43 | answer pixel.Rect
44 | }{
45 | {squareAroundOrigin, resizeByHalfAroundCenter, pixel.R(-5, -5, 5, 5)},
46 | {squareAround2020, resizeByHalfAroundCenter, pixel.R(15, 15, 25, 25)},
47 | {rectangleAroundOrigin, resizeByHalfAroundCenter, pixel.R(-10, -5, 10, 5)},
48 | {rectangleAround2020, resizeByHalfAroundCenter, pixel.R(10, 15, 30, 25)},
49 |
50 | {squareAroundOrigin, resizeByHalfAroundMin, pixel.R(-10, -10, 0, 0)},
51 | {squareAround2020, resizeByHalfAroundMin, pixel.R(10, 10, 20, 20)},
52 | {rectangleAroundOrigin, resizeByHalfAroundMin, pixel.R(-20, -10, 0, 0)},
53 | {rectangleAround2020, resizeByHalfAroundMin, pixel.R(0, 10, 20, 20)},
54 |
55 | {squareAroundOrigin, resizeByHalfAroundMax, pixel.R(0, 0, 10, 10)},
56 | {squareAround2020, resizeByHalfAroundMax, pixel.R(20, 20, 30, 30)},
57 | {rectangleAroundOrigin, resizeByHalfAroundMax, pixel.R(0, 0, 20, 10)},
58 | {rectangleAround2020, resizeByHalfAroundMax, pixel.R(20, 20, 40, 30)},
59 |
60 | {squareAroundOrigin, resizeByHalfAroundMiddleOfLeftSide, pixel.R(-10, -5, 0, 5)},
61 | {squareAround2020, resizeByHalfAroundMiddleOfLeftSide, pixel.R(10, 15, 20, 25)},
62 | {rectangleAroundOrigin, resizeByHalfAroundMiddleOfLeftSide, pixel.R(-20, -5, 0, 5)},
63 | {rectangleAround2020, resizeByHalfAroundMiddleOfLeftSide, pixel.R(0, 15, 20, 25)},
64 |
65 | {squareAroundOrigin, resizeByHalfAroundOrigin, pixel.R(-5, -5, 5, 5)},
66 | {squareAround2020, resizeByHalfAroundOrigin, pixel.R(5, 5, 15, 15)},
67 | {rectangleAroundOrigin, resizeByHalfAroundOrigin, pixel.R(-10, -5, 10, 5)},
68 | {rectangleAround2020, resizeByHalfAroundOrigin, pixel.R(0, 5, 20, 15)},
69 | }
70 |
71 | for _, testCase := range testCases {
72 | t.Run(fmt.Sprintf("Resize %v %s", testCase.input, testCase.transform.name), func(t *testing.T) {
73 | testResult := testCase.transform.f(testCase.input)
74 | if testResult != testCase.answer {
75 | t.Errorf("Got: %v, wanted: %v\n", testResult, testCase.answer)
76 | }
77 | })
78 | }
79 | }
80 |
81 | func TestRect_Edges(t *testing.T) {
82 | type fields struct {
83 | Min pixel.Vec
84 | Max pixel.Vec
85 | }
86 | tests := []struct {
87 | name string
88 | fields fields
89 | want [4]pixel.Line
90 | }{
91 | {
92 | name: "Get edges",
93 | fields: fields{Min: pixel.V(0, 0), Max: pixel.V(10, 10)},
94 | want: [4]pixel.Line{
95 | pixel.L(pixel.V(0, 0), pixel.V(0, 10)),
96 | pixel.L(pixel.V(0, 10), pixel.V(10, 10)),
97 | pixel.L(pixel.V(10, 10), pixel.V(10, 0)),
98 | pixel.L(pixel.V(10, 0), pixel.V(0, 0)),
99 | },
100 | },
101 | }
102 | for _, tt := range tests {
103 | t.Run(tt.name, func(t *testing.T) {
104 | r := pixel.Rect{
105 | Min: tt.fields.Min,
106 | Max: tt.fields.Max,
107 | }
108 | if got := r.Edges(); !reflect.DeepEqual(got, tt.want) {
109 | t.Errorf("Rect.Edges() = %v, want %v", got, tt.want)
110 | }
111 | })
112 | }
113 | }
114 |
115 | func TestRect_Vertices(t *testing.T) {
116 | type fields struct {
117 | Min pixel.Vec
118 | Max pixel.Vec
119 | }
120 | tests := []struct {
121 | name string
122 | fields fields
123 | want [4]pixel.Vec
124 | }{
125 | {
126 | name: "Get corners",
127 | fields: fields{Min: pixel.V(0, 0), Max: pixel.V(10, 10)},
128 | want: [4]pixel.Vec{
129 | pixel.V(0, 0),
130 | pixel.V(0, 10),
131 | pixel.V(10, 10),
132 | pixel.V(10, 0),
133 | },
134 | },
135 | }
136 | for _, tt := range tests {
137 | t.Run(tt.name, func(t *testing.T) {
138 | r := pixel.Rect{
139 | Min: tt.fields.Min,
140 | Max: tt.fields.Max,
141 | }
142 | if got := r.Vertices(); !reflect.DeepEqual(got, tt.want) {
143 | t.Errorf("Rect.Vertices() = %v, want %v", got, tt.want)
144 | }
145 | })
146 | }
147 | }
148 |
149 | func TestRect_IntersectCircle(t *testing.T) {
150 | type fields struct {
151 | Min pixel.Vec
152 | Max pixel.Vec
153 | }
154 | type args struct {
155 | c pixel.Circle
156 | }
157 | tests := []struct {
158 | name string
159 | fields fields
160 | args args
161 | want pixel.Vec
162 | }{
163 | {
164 | name: "Rect.IntersectCircle(): no overlap",
165 | fields: fields{Min: pixel.ZV, Max: pixel.V(10, 10)},
166 | args: args{c: pixel.C(pixel.V(50, 50), 1)},
167 | want: pixel.ZV,
168 | },
169 | {
170 | name: "Rect.IntersectCircle(): circle contains rect",
171 | fields: fields{Min: pixel.ZV, Max: pixel.V(10, 10)},
172 | args: args{c: pixel.C(pixel.V(5, 5), 10)},
173 | want: pixel.V(-15, 0),
174 | },
175 | {
176 | name: "Rect.IntersectCircle(): rect contains circle",
177 | fields: fields{Min: pixel.ZV, Max: pixel.V(10, 10)},
178 | args: args{c: pixel.C(pixel.V(5, 5), 1)},
179 | want: pixel.V(-6, 0),
180 | },
181 | {
182 | name: "Rect.IntersectCircle(): circle overlaps bottom-left corner",
183 | fields: fields{Min: pixel.ZV, Max: pixel.V(10, 10)},
184 | args: args{c: pixel.C(pixel.V(-0.5, -0.5), 1)},
185 | want: pixel.V(-0.2, -0.2),
186 | },
187 | {
188 | name: "Rect.IntersectCircle(): circle overlaps top-left corner",
189 | fields: fields{Min: pixel.ZV, Max: pixel.V(10, 10)},
190 | args: args{c: pixel.C(pixel.V(-0.5, 10.5), 1)},
191 | want: pixel.V(-0.2, 0.2),
192 | },
193 | {
194 | name: "Rect.IntersectCircle(): circle overlaps bottom-right corner",
195 | fields: fields{Min: pixel.ZV, Max: pixel.V(10, 10)},
196 | args: args{c: pixel.C(pixel.V(10.5, -0.5), 1)},
197 | want: pixel.V(0.2, -0.2),
198 | },
199 | {
200 | name: "Rect.IntersectCircle(): circle overlaps top-right corner",
201 | fields: fields{Min: pixel.ZV, Max: pixel.V(10, 10)},
202 | args: args{c: pixel.C(pixel.V(10.5, 10.5), 1)},
203 | want: pixel.V(0.2, 0.2),
204 | },
205 | {
206 | name: "Rect.IntersectCircle(): circle overlaps two corners",
207 | fields: fields{Min: pixel.ZV, Max: pixel.V(10, 10)},
208 | args: args{c: pixel.C(pixel.V(0, 5), 6)},
209 | want: pixel.V(6, 0),
210 | },
211 | {
212 | name: "Rect.IntersectCircle(): circle overlaps left edge",
213 | fields: fields{Min: pixel.ZV, Max: pixel.V(10, 10)},
214 | args: args{c: pixel.C(pixel.V(0, 5), 1)},
215 | want: pixel.V(1, 0),
216 | },
217 | {
218 | name: "Rect.IntersectCircle(): circle overlaps bottom edge",
219 | fields: fields{Min: pixel.ZV, Max: pixel.V(10, 10)},
220 | args: args{c: pixel.C(pixel.V(5, 0), 1)},
221 | want: pixel.V(0, 1),
222 | },
223 | {
224 | name: "Rect.IntersectCircle(): circle overlaps right edge",
225 | fields: fields{Min: pixel.ZV, Max: pixel.V(10, 10)},
226 | args: args{c: pixel.C(pixel.V(10, 5), 1)},
227 | want: pixel.V(-1, 0),
228 | },
229 | {
230 | name: "Rect.IntersectCircle(): circle overlaps top edge",
231 | fields: fields{Min: pixel.ZV, Max: pixel.V(10, 10)},
232 | args: args{c: pixel.C(pixel.V(5, 10), 1)},
233 | want: pixel.V(0, -1),
234 | },
235 | {
236 | name: "Rect.IntersectCircle(): edge is tangent of left side",
237 | fields: fields{Min: pixel.ZV, Max: pixel.V(10, 10)},
238 | args: args{c: pixel.C(pixel.V(-1, 5), 1)},
239 | want: pixel.ZV,
240 | },
241 | {
242 | name: "Rect.IntersectCircle(): edge is tangent of top side",
243 | fields: fields{Min: pixel.ZV, Max: pixel.V(10, 10)},
244 | args: args{c: pixel.C(pixel.V(5, -1), 1)},
245 | want: pixel.ZV,
246 | },
247 | {
248 | name: "Rect.IntersectCircle(): circle above rectangle",
249 | fields: fields{Min: pixel.ZV, Max: pixel.V(10, 10)},
250 | args: args{c: pixel.C(pixel.V(5, 12), 1)},
251 | want: pixel.ZV,
252 | },
253 | {
254 | name: "Rect.IntersectCircle(): circle below rectangle",
255 | fields: fields{Min: pixel.ZV, Max: pixel.V(10, 10)},
256 | args: args{c: pixel.C(pixel.V(5, -2), 1)},
257 | want: pixel.ZV,
258 | },
259 | {
260 | name: "Rect.IntersectCircle(): circle left of rectangle",
261 | fields: fields{Min: pixel.ZV, Max: pixel.V(10, 10)},
262 | args: args{c: pixel.C(pixel.V(-1, 5), 1)},
263 | want: pixel.ZV,
264 | },
265 | {
266 | name: "Rect.IntersectCircle(): circle right of rectangle",
267 | fields: fields{Min: pixel.ZV, Max: pixel.V(10, 10)},
268 | args: args{c: pixel.C(pixel.V(11, 5), 1)},
269 | want: pixel.ZV,
270 | },
271 | }
272 | for _, tt := range tests {
273 | t.Run(tt.name, func(t *testing.T) {
274 | r := pixel.Rect{
275 | Min: tt.fields.Min,
276 | Max: tt.fields.Max,
277 | }
278 | got := r.IntersectCircle(tt.args.c)
279 | if !closeEnough(got.X, tt.want.X, 2) || !closeEnough(got.Y, tt.want.Y, 2) {
280 | t.Errorf("Rect.IntersectCircle() = %v, want %v", got, tt.want)
281 | }
282 | })
283 | }
284 | }
285 |
286 | func TestRect_IntersectionPoints(t *testing.T) {
287 | type fields struct {
288 | Min pixel.Vec
289 | Max pixel.Vec
290 | }
291 | type args struct {
292 | l pixel.Line
293 | }
294 | tests := []struct {
295 | name string
296 | fields fields
297 | args args
298 | want []pixel.Vec
299 | }{
300 | {
301 | name: "No intersection points",
302 | fields: fields{Min: pixel.V(1, 1), Max: pixel.V(5, 5)},
303 | args: args{l: pixel.L(pixel.V(-5, 0), pixel.V(-2, 2))},
304 | want: []pixel.Vec{},
305 | },
306 | {
307 | name: "One intersection point",
308 | fields: fields{Min: pixel.V(1, 1), Max: pixel.V(5, 5)},
309 | args: args{l: pixel.L(pixel.V(2, 0), pixel.V(2, 3))},
310 | want: []pixel.Vec{pixel.V(2, 1)},
311 | },
312 | {
313 | name: "Two intersection points",
314 | fields: fields{Min: pixel.V(1, 1), Max: pixel.V(5, 5)},
315 | args: args{l: pixel.L(pixel.V(0, 2), pixel.V(6, 2))},
316 | want: []pixel.Vec{pixel.V(1, 2), pixel.V(5, 2)},
317 | },
318 | }
319 | for _, tt := range tests {
320 | t.Run(tt.name, func(t *testing.T) {
321 | r := pixel.Rect{
322 | Min: tt.fields.Min,
323 | Max: tt.fields.Max,
324 | }
325 | if got := r.IntersectionPoints(tt.args.l); !reflect.DeepEqual(got, tt.want) {
326 | t.Errorf("Rect.IntersectPoints() = %v, want %v", got, tt.want)
327 | }
328 | })
329 | }
330 | }
331 |
332 | var rectIntTests = []struct {
333 | name string
334 | r1, r2 pixel.Rect
335 | want pixel.Rect
336 | intersect bool
337 | }{
338 | {
339 | name: "Nothing touching",
340 | r1: pixel.R(0, 0, 10, 10),
341 | r2: pixel.R(21, 21, 40, 40),
342 | want: pixel.ZR,
343 | },
344 | {
345 | name: "Edge touching",
346 | r1: pixel.R(0, 0, 10, 10),
347 | r2: pixel.R(10, 10, 20, 20),
348 | want: pixel.ZR,
349 | },
350 | {
351 | name: "Bit of overlap",
352 | r1: pixel.R(0, 0, 10, 10),
353 | r2: pixel.R(0, 9, 20, 20),
354 | want: pixel.R(0, 9, 10, 10),
355 | intersect: true,
356 | },
357 | {
358 | name: "Fully overlapped",
359 | r1: pixel.R(0, 0, 10, 10),
360 | r2: pixel.R(0, 0, 10, 10),
361 | want: pixel.R(0, 0, 10, 10),
362 | intersect: true,
363 | },
364 | }
365 |
366 | func TestRect_Intersect(t *testing.T) {
367 | for _, tt := range rectIntTests {
368 | t.Run(tt.name, func(t *testing.T) {
369 | if got := tt.r1.Intersect(tt.r2); !reflect.DeepEqual(got, tt.want) {
370 | t.Errorf("Rect.Intersect() = %v, want %v", got, tt.want)
371 | }
372 | })
373 | }
374 | }
375 |
376 | func TestRect_Intersects(t *testing.T) {
377 | for _, tt := range rectIntTests {
378 | t.Run(tt.name, func(t *testing.T) {
379 | if got := tt.r1.Intersects(tt.r2); got != tt.intersect {
380 | t.Errorf("Rect.Intersects() = %v, want %v", got, tt.want)
381 | }
382 | })
383 | }
384 | }
385 |
--------------------------------------------------------------------------------
/sprite.go:
--------------------------------------------------------------------------------
1 | package pixel
2 |
3 | import "image/color"
4 |
5 | // Sprite is a drawable frame of a Picture. It's anchored by the center of it's Picture's frame.
6 | //
7 | // Frame specifies a rectangular portion of the Picture that will be drawn. For example, this
8 | // creates a Sprite that draws the whole Picture:
9 | //
10 | // sprite := pixel.NewSprite(pic, pic.Bounds())
11 | //
12 | // Note, that Sprite caches the results of MakePicture from Targets it's drawn to for each Picture
13 | // it's set to. What it means is that using a Sprite with an unbounded number of Pictures leads to a
14 | // memory leak, since Sprite caches them and never forgets. In such a situation, create a new Sprite
15 | // for each Picture.
16 | type Sprite struct {
17 | tri *TrianglesData
18 | frame Rect
19 | d Drawer
20 |
21 | matrix Matrix
22 | mask RGBA
23 | }
24 |
25 | // NewSprite creates a Sprite from the supplied frame of a Picture.
26 | func NewSprite(pic Picture, frame Rect) *Sprite {
27 | tri := MakeTrianglesData(6)
28 | s := &Sprite{
29 | tri: tri,
30 | d: Drawer{Triangles: tri, Cached: true},
31 | }
32 | s.matrix = IM
33 | s.mask = Alpha(1)
34 | s.Set(pic, frame)
35 | return s
36 | }
37 |
38 | // Set sets a new frame of a Picture for this Sprite.
39 | func (s *Sprite) Set(pic Picture, frame Rect) {
40 | s.d.Picture = pic
41 | if frame != s.frame {
42 | s.frame = frame
43 | s.calcData()
44 | }
45 | }
46 |
47 | // SetCached makes the sprite cache all the
48 | // incoming pictures if the argument is true, and
49 | // doesn't make it do that if the argument is false.
50 | func (s *Sprite) SetCached(cached bool) {
51 | s.d.Cached = cached
52 | }
53 |
54 | // Picture returns the current Sprite's Picture.
55 | func (s *Sprite) Picture() Picture {
56 | return s.d.Picture
57 | }
58 |
59 | // Frame returns the current Sprite's frame.
60 | func (s *Sprite) Frame() Rect {
61 | return s.frame
62 | }
63 |
64 | // Draw draws the Sprite onto the provided Target. The Sprite will be transformed by the given Matrix.
65 | //
66 | // This method is equivalent to calling DrawColorMask with nil color mask.
67 | func (s *Sprite) Draw(t Target, matrix Matrix) {
68 | s.DrawColorMask(t, matrix, nil)
69 | }
70 |
71 | // DrawColorMask draws the Sprite onto the provided Target. The Sprite will be transformed by the
72 | // given Matrix and all of it's color will be multiplied by the given mask.
73 | //
74 | // If the mask is nil, a fully opaque white mask will be used, which causes no effect.
75 | func (s *Sprite) DrawColorMask(t Target, matrix Matrix, mask color.Color) {
76 | dirty := false
77 | if matrix != s.matrix {
78 | s.matrix = matrix
79 | dirty = true
80 | }
81 | if mask == nil {
82 | mask = Alpha(1)
83 | }
84 | rgba := ToRGBA(mask)
85 | if rgba != s.mask {
86 | s.mask = rgba
87 | dirty = true
88 | }
89 |
90 | if dirty {
91 | s.calcData()
92 | }
93 |
94 | s.d.Draw(t)
95 | }
96 |
97 | func (s *Sprite) calcData() {
98 | var (
99 | center = s.frame.Center()
100 | horizontal = V(s.frame.W()/2, 0)
101 | vertical = V(0, s.frame.H()/2)
102 | )
103 |
104 | (*s.tri)[0].Position = Vec{}.Sub(horizontal).Sub(vertical)
105 | (*s.tri)[1].Position = Vec{}.Add(horizontal).Sub(vertical)
106 | (*s.tri)[2].Position = Vec{}.Add(horizontal).Add(vertical)
107 | (*s.tri)[3].Position = Vec{}.Sub(horizontal).Sub(vertical)
108 | (*s.tri)[4].Position = Vec{}.Add(horizontal).Add(vertical)
109 | (*s.tri)[5].Position = Vec{}.Sub(horizontal).Add(vertical)
110 |
111 | for i := range *s.tri {
112 | (*s.tri)[i].Color = s.mask
113 | (*s.tri)[i].Picture = center.Add((*s.tri)[i].Position)
114 | (*s.tri)[i].Intensity = 1
115 | (*s.tri)[i].Position = s.matrix.Project((*s.tri)[i].Position)
116 | }
117 |
118 | s.d.Dirty()
119 | }
120 |
--------------------------------------------------------------------------------
/text/atlas.go:
--------------------------------------------------------------------------------
1 | package text
2 |
3 | import (
4 | "image"
5 | "image/draw"
6 | "sort"
7 | "unicode"
8 |
9 | "github.com/faiface/pixel"
10 | "golang.org/x/image/font"
11 | "golang.org/x/image/math/fixed"
12 | )
13 |
14 | // Atlas7x13 is an Atlas using basicfont.Face7x13 with the ASCII rune set
15 | var Atlas7x13 *Atlas
16 |
17 | // Glyph describes one glyph in an Atlas.
18 | type Glyph struct {
19 | Dot pixel.Vec
20 | Frame pixel.Rect
21 | Advance float64
22 | }
23 |
24 | // Atlas is a set of pre-drawn glyphs of a fixed set of runes. This allows for efficient text drawing.
25 | type Atlas struct {
26 | face font.Face
27 | pic pixel.Picture
28 | mapping map[rune]Glyph
29 | ascent float64
30 | descent float64
31 | lineHeight float64
32 | }
33 |
34 | // NewAtlas creates a new Atlas containing glyphs of the union of the given sets of runes (plus
35 | // unicode.ReplacementChar) from the given font face.
36 | //
37 | // Creating an Atlas is rather expensive, do not create a new Atlas each frame.
38 | //
39 | // Do not destroy or close the font.Face after creating the Atlas. Atlas still uses it.
40 | func NewAtlas(face font.Face, runeSets ...[]rune) *Atlas {
41 | seen := make(map[rune]bool)
42 | runes := []rune{unicode.ReplacementChar}
43 | for _, set := range runeSets {
44 | for _, r := range set {
45 | if !seen[r] {
46 | runes = append(runes, r)
47 | seen[r] = true
48 | }
49 | }
50 | }
51 |
52 | fixedMapping, fixedBounds := makeSquareMapping(face, runes, fixed.I(2))
53 |
54 | atlasImg := image.NewRGBA(image.Rect(
55 | fixedBounds.Min.X.Floor(),
56 | fixedBounds.Min.Y.Floor(),
57 | fixedBounds.Max.X.Ceil(),
58 | fixedBounds.Max.Y.Ceil(),
59 | ))
60 |
61 | for r, fg := range fixedMapping {
62 | if dr, mask, maskp, _, ok := face.Glyph(fg.dot, r); ok {
63 | draw.Draw(atlasImg, dr, mask, maskp, draw.Src)
64 | }
65 | }
66 |
67 | bounds := pixel.R(
68 | i2f(fixedBounds.Min.X),
69 | i2f(fixedBounds.Min.Y),
70 | i2f(fixedBounds.Max.X),
71 | i2f(fixedBounds.Max.Y),
72 | )
73 |
74 | mapping := make(map[rune]Glyph)
75 | for r, fg := range fixedMapping {
76 | mapping[r] = Glyph{
77 | Dot: pixel.V(
78 | i2f(fg.dot.X),
79 | bounds.Max.Y-(i2f(fg.dot.Y)-bounds.Min.Y),
80 | ),
81 | Frame: pixel.R(
82 | i2f(fg.frame.Min.X),
83 | bounds.Max.Y-(i2f(fg.frame.Min.Y)-bounds.Min.Y),
84 | i2f(fg.frame.Max.X),
85 | bounds.Max.Y-(i2f(fg.frame.Max.Y)-bounds.Min.Y),
86 | ).Norm(),
87 | Advance: i2f(fg.advance),
88 | }
89 | }
90 |
91 | return &Atlas{
92 | face: face,
93 | pic: pixel.PictureDataFromImage(atlasImg),
94 | mapping: mapping,
95 | ascent: i2f(face.Metrics().Ascent),
96 | descent: i2f(face.Metrics().Descent),
97 | lineHeight: i2f(face.Metrics().Height),
98 | }
99 | }
100 |
101 | // Picture returns the underlying Picture containing an arrangement of all the glyphs contained
102 | // within the Atlas.
103 | func (a *Atlas) Picture() pixel.Picture {
104 | return a.pic
105 | }
106 |
107 | // Contains reports wheter r in contained within the Atlas.
108 | func (a *Atlas) Contains(r rune) bool {
109 | _, ok := a.mapping[r]
110 | return ok
111 | }
112 |
113 | // Glyph returns the description of r within the Atlas.
114 | func (a *Atlas) Glyph(r rune) Glyph {
115 | return a.mapping[r]
116 | }
117 |
118 | // Kern returns the kerning distance between runes r0 and r1. Positive distance means that the
119 | // glyphs should be further apart.
120 | func (a *Atlas) Kern(r0, r1 rune) float64 {
121 | return i2f(a.face.Kern(r0, r1))
122 | }
123 |
124 | // Ascent returns the distance from the top of the line to the baseline.
125 | func (a *Atlas) Ascent() float64 {
126 | return a.ascent
127 | }
128 |
129 | // Descent returns the distance from the baseline to the bottom of the line.
130 | func (a *Atlas) Descent() float64 {
131 | return a.descent
132 | }
133 |
134 | // LineHeight returns the recommended vertical distance between two lines of text.
135 | func (a *Atlas) LineHeight() float64 {
136 | return a.lineHeight
137 | }
138 |
139 | // DrawRune returns parameters necessary for drawing a rune glyph.
140 | //
141 | // Rect is a rectangle where the glyph should be positioned. Frame is the glyph frame inside the
142 | // Atlas's Picture. NewDot is the new position of the dot.
143 | func (a *Atlas) DrawRune(prevR, r rune, dot pixel.Vec) (rect, frame, bounds pixel.Rect, newDot pixel.Vec) {
144 | if !a.Contains(r) {
145 | r = unicode.ReplacementChar
146 | }
147 | if !a.Contains(unicode.ReplacementChar) {
148 | return pixel.Rect{}, pixel.Rect{}, pixel.Rect{}, dot
149 | }
150 | if !a.Contains(prevR) {
151 | prevR = unicode.ReplacementChar
152 | }
153 |
154 | if prevR >= 0 {
155 | dot.X += a.Kern(prevR, r)
156 | }
157 |
158 | glyph := a.Glyph(r)
159 |
160 | rect = glyph.Frame.Moved(dot.Sub(glyph.Dot))
161 | bounds = rect
162 |
163 | if bounds.W()*bounds.H() != 0 {
164 | bounds = pixel.R(
165 | bounds.Min.X,
166 | dot.Y-a.Descent(),
167 | bounds.Max.X,
168 | dot.Y+a.Ascent(),
169 | )
170 | }
171 |
172 | dot.X += glyph.Advance
173 |
174 | return rect, glyph.Frame, bounds, dot
175 | }
176 |
177 | type fixedGlyph struct {
178 | dot fixed.Point26_6
179 | frame fixed.Rectangle26_6
180 | advance fixed.Int26_6
181 | }
182 |
183 | // makeSquareMapping finds an optimal glyph arrangement of the given runes, so that their common
184 | // bounding box is as square as possible.
185 | func makeSquareMapping(face font.Face, runes []rune, padding fixed.Int26_6) (map[rune]fixedGlyph, fixed.Rectangle26_6) {
186 | width := sort.Search(int(fixed.I(1024*1024)), func(i int) bool {
187 | width := fixed.Int26_6(i)
188 | _, bounds := makeMapping(face, runes, padding, width)
189 | return bounds.Max.X-bounds.Min.X >= bounds.Max.Y-bounds.Min.Y
190 | })
191 | return makeMapping(face, runes, padding, fixed.Int26_6(width))
192 | }
193 |
194 | // makeMapping arranges glyphs of the given runes into rows in such a way, that no glyph is located
195 | // fully to the right of the specified width. Specifically, it places glyphs in a row one by one and
196 | // once it reaches the specified width, it starts a new row.
197 | func makeMapping(face font.Face, runes []rune, padding, width fixed.Int26_6) (map[rune]fixedGlyph, fixed.Rectangle26_6) {
198 | mapping := make(map[rune]fixedGlyph)
199 | bounds := fixed.Rectangle26_6{}
200 |
201 | dot := fixed.P(0, 0)
202 |
203 | for _, r := range runes {
204 | b, advance, ok := face.GlyphBounds(r)
205 | if !ok {
206 | continue
207 | }
208 |
209 | // this is important for drawing, artifacts arise otherwise
210 | frame := fixed.Rectangle26_6{
211 | Min: fixed.P(b.Min.X.Floor(), b.Min.Y.Floor()),
212 | Max: fixed.P(b.Max.X.Ceil(), b.Max.Y.Ceil()),
213 | }
214 |
215 | dot.X -= frame.Min.X
216 | frame = frame.Add(dot)
217 |
218 | mapping[r] = fixedGlyph{
219 | dot: dot,
220 | frame: frame,
221 | advance: advance,
222 | }
223 | bounds = bounds.Union(frame)
224 |
225 | dot.X = frame.Max.X
226 |
227 | // padding + align to integer
228 | dot.X += padding
229 | dot.X = fixed.I(dot.X.Ceil())
230 |
231 | // width exceeded, new row
232 | if frame.Max.X >= width {
233 | dot.X = 0
234 | dot.Y += face.Metrics().Ascent + face.Metrics().Descent
235 |
236 | // padding + align to integer
237 | dot.Y += padding
238 | dot.Y = fixed.I(dot.Y.Ceil())
239 | }
240 | }
241 |
242 | return mapping, bounds
243 | }
244 |
245 | func i2f(i fixed.Int26_6) float64 {
246 | return float64(i) / (1 << 6)
247 | }
248 |
--------------------------------------------------------------------------------
/text/atlas_test.go:
--------------------------------------------------------------------------------
1 | package text_test
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/faiface/pixel/text"
7 | "golang.org/x/image/font/inconsolata"
8 | )
9 |
10 | func TestAtlas7x13(t *testing.T) {
11 | if text.Atlas7x13 == nil {
12 | t.Fatalf("Atlas7x13 is nil")
13 | }
14 |
15 | for _, tt := range []struct {
16 | runes []rune
17 | want bool
18 | }{{text.ASCII, true}, {[]rune("ÅÄÖ"), false}} {
19 | for _, r := range tt.runes {
20 | if got := text.Atlas7x13.Contains(r); got != tt.want {
21 | t.Fatalf("Atlas7x13.Contains('%s') = %v, want %v", string(r), got, tt.want)
22 | }
23 | }
24 | }
25 | }
26 |
27 | func TestAtlasInconsolata(t *testing.T) {
28 | text.NewAtlas(inconsolata.Regular8x16, text.ASCII)
29 | }
30 |
--------------------------------------------------------------------------------
/text/doc.go:
--------------------------------------------------------------------------------
1 | // Package text implements efficient text drawing for the Pixel library.
2 | package text
3 |
--------------------------------------------------------------------------------
/text/text.go:
--------------------------------------------------------------------------------
1 | package text
2 |
3 | import (
4 | "image/color"
5 | "math"
6 | "unicode"
7 | "unicode/utf8"
8 |
9 | "github.com/faiface/pixel"
10 | "golang.org/x/image/font/basicfont"
11 | )
12 |
13 | // ASCII is a set of all ASCII runes. These runes are codepoints from 32 to 127 inclusive.
14 | var ASCII []rune
15 |
16 | func init() {
17 | ASCII = make([]rune, unicode.MaxASCII-32)
18 | for i := range ASCII {
19 | ASCII[i] = rune(32 + i)
20 | }
21 | Atlas7x13 = NewAtlas(basicfont.Face7x13, ASCII)
22 | }
23 |
24 | // RangeTable takes a *unicode.RangeTable and generates a set of runes contained within that
25 | // RangeTable.
26 | func RangeTable(table *unicode.RangeTable) []rune {
27 | var runes []rune
28 | for _, rng := range table.R16 {
29 | for r := rng.Lo; r <= rng.Hi; r += rng.Stride {
30 | runes = append(runes, rune(r))
31 | }
32 | }
33 | for _, rng := range table.R32 {
34 | for r := rng.Lo; r <= rng.Hi; r += rng.Stride {
35 | runes = append(runes, rune(r))
36 | }
37 | }
38 | return runes
39 | }
40 |
41 | // Text allows for effiecient and convenient text drawing.
42 | //
43 | // To create a Text object, use the New constructor:
44 | // txt := text.New(pixel.ZV, text.NewAtlas(face, text.ASCII))
45 | //
46 | // As suggested by the constructor, a Text object is always associated with one font face and a
47 | // fixed set of runes. For example, the Text we created above can draw text using the font face
48 | // contained in the face variable and is capable of drawing ASCII characters.
49 | //
50 | // Here we create a Text object which can draw ASCII and Katakana characters:
51 | // txt := text.New(0, text.NewAtlas(face, text.ASCII, text.RangeTable(unicode.Katakana)))
52 | //
53 | // Similarly to IMDraw, Text functions as a buffer. It implements io.Writer interface, so writing
54 | // text to it is really simple:
55 | // fmt.Print(txt, "Hello, world!")
56 | //
57 | // Newlines, tabs and carriage returns are supported.
58 | //
59 | // Finally, if we want the written text to show up on some other Target, we can draw it:
60 | // txt.Draw(target)
61 | //
62 | // Text exports two important fields: Orig and Dot. Dot is the position where the next character
63 | // will be written. Dot is automatically moved when writing to a Text object, but you can also
64 | // manipulate it manually. Orig specifies the text origin, usually the top-left dot position. Dot is
65 | // always aligned to Orig when writing newlines. The Clear method resets the Dot to Orig.
66 | type Text struct {
67 | // Orig specifies the text origin, usually the top-left dot position. Dot is always aligned
68 | // to Orig when writing newlines.
69 | Orig pixel.Vec
70 |
71 | // Dot is the position where the next character will be written. Dot is automatically moved
72 | // when writing to a Text object, but you can also manipulate it manually
73 | Dot pixel.Vec
74 |
75 | // Color is the color of the text that is to be written. Defaults to white.
76 | Color color.Color
77 |
78 | // LineHeight is the vertical distance between two lines of text.
79 | //
80 | // Example:
81 | // txt.LineHeight = 1.5 * txt.Atlas().LineHeight()
82 | LineHeight float64
83 |
84 | // TabWidth is the horizontal tab width. Tab characters will align to the multiples of this
85 | // width.
86 | //
87 | // Example:
88 | // txt.TabWidth = 8 * txt.Atlas().Glyph(' ').Advance
89 | TabWidth float64
90 |
91 | atlas *Atlas
92 |
93 | buf []byte
94 | prevR rune
95 | bounds pixel.Rect
96 | glyph pixel.TrianglesData
97 | tris pixel.TrianglesData
98 |
99 | mat pixel.Matrix
100 | col pixel.RGBA
101 | trans pixel.TrianglesData
102 | transD pixel.Drawer
103 | dirty bool
104 | anchor pixel.Anchor
105 | }
106 |
107 | // New creates a new Text capable of drawing runes contained in the provided Atlas. Orig and Dot
108 | // will be initially set to orig.
109 | //
110 | // Here we create a Text capable of drawing ASCII characters using the Go Regular font.
111 | // ttf, err := truetype.Parse(goregular.TTF)
112 | // if err != nil {
113 | // panic(err)
114 | // }
115 | // face := truetype.NewFace(ttf, &truetype.Options{
116 | // Size: 14,
117 | // })
118 | // txt := text.New(orig, text.NewAtlas(face, text.ASCII))
119 | func New(orig pixel.Vec, atlas *Atlas) *Text {
120 | txt := &Text{
121 | Orig: orig,
122 | Dot: orig,
123 | Color: pixel.Alpha(1),
124 | LineHeight: atlas.LineHeight(),
125 | TabWidth: atlas.Glyph(' ').Advance * 4,
126 | atlas: atlas,
127 | mat: pixel.IM,
128 | col: pixel.Alpha(1),
129 | }
130 |
131 | txt.glyph.SetLen(6)
132 | for i := range txt.glyph {
133 | txt.glyph[i].Color = pixel.Alpha(1)
134 | txt.glyph[i].Intensity = 1
135 | }
136 |
137 | txt.transD.Picture = txt.atlas.pic
138 | txt.transD.Triangles = &txt.trans
139 | txt.transD.Cached = true
140 |
141 | txt.Clear()
142 |
143 | return txt
144 | }
145 |
146 | // Atlas returns the underlying Text's Atlas containing all of the pre-drawn glyphs. The Atlas is
147 | // also useful for getting values such as the recommended line height.
148 | func (txt *Text) Atlas() *Atlas {
149 | return txt.atlas
150 | }
151 |
152 | // Bounds returns the bounding box of the text currently written to the Text excluding whitespace.
153 | //
154 | // If the Text is empty, a zero rectangle is returned.
155 | func (txt *Text) Bounds() pixel.Rect {
156 | return txt.bounds
157 | }
158 |
159 | // BoundsOf returns the bounding box of s if it was to be written to the Text right now.
160 | func (txt *Text) BoundsOf(s string) pixel.Rect {
161 | dot := txt.Dot
162 | prevR := txt.prevR
163 | bounds := pixel.Rect{}
164 |
165 | for _, r := range s {
166 | var control bool
167 | dot, control = txt.controlRune(r, dot)
168 | if control {
169 | continue
170 | }
171 |
172 | var b pixel.Rect
173 | _, _, b, dot = txt.Atlas().DrawRune(prevR, r, dot)
174 |
175 | if bounds.W()*bounds.H() == 0 {
176 | bounds = b
177 | } else {
178 | bounds = bounds.Union(b)
179 | }
180 |
181 | prevR = r
182 | }
183 |
184 | return bounds
185 | }
186 |
187 | // AlignedTo returns the text moved by the given anchor.
188 | func (txt *Text) AlignedTo(anchor pixel.Anchor) *Text {
189 | txt.anchor = anchor
190 | return txt
191 | }
192 |
193 | // Clear removes all written text from the Text. The Dot field is reset to Orig.
194 | func (txt *Text) Clear() {
195 | txt.prevR = -1
196 | txt.bounds = pixel.Rect{}
197 | txt.tris.SetLen(0)
198 | txt.dirty = true
199 | txt.Dot = txt.Orig
200 | }
201 |
202 | // Write writes a slice of bytes to the Text. This method never fails, always returns len(p), nil.
203 | func (txt *Text) Write(p []byte) (n int, err error) {
204 | txt.buf = append(txt.buf, p...)
205 | txt.drawBuf()
206 | return len(p), nil
207 | }
208 |
209 | // WriteString writes a string to the Text. This method never fails, always returns len(s), nil.
210 | func (txt *Text) WriteString(s string) (n int, err error) {
211 | txt.buf = append(txt.buf, s...)
212 | txt.drawBuf()
213 | return len(s), nil
214 | }
215 |
216 | // WriteByte writes a byte to the Text. This method never fails, always returns nil.
217 | //
218 | // Writing a multi-byte rune byte-by-byte is perfectly supported.
219 | func (txt *Text) WriteByte(c byte) error {
220 | txt.buf = append(txt.buf, c)
221 | txt.drawBuf()
222 | return nil
223 | }
224 |
225 | // WriteRune writes a rune to the Text. This method never fails, always returns utf8.RuneLen(r), nil.
226 | func (txt *Text) WriteRune(r rune) (n int, err error) {
227 | var b [4]byte
228 | n = utf8.EncodeRune(b[:], r)
229 | txt.buf = append(txt.buf, b[:n]...)
230 | txt.drawBuf()
231 | return n, nil
232 | }
233 |
234 | // Draw draws all text written to the Text to the provided Target. The text is transformed by the
235 | // provided Matrix.
236 | //
237 | // This method is equivalent to calling DrawColorMask with nil color mask.
238 | //
239 | // If there's a lot of text written to the Text, changing a matrix or a color mask often might hurt
240 | // performance. Consider using your Target's SetMatrix or SetColorMask methods if available.
241 | func (txt *Text) Draw(t pixel.Target, matrix pixel.Matrix) {
242 | txt.DrawColorMask(t, matrix, nil)
243 | }
244 |
245 | // DrawColorMask draws all text written to the Text to the provided Target. The text is transformed
246 | // by the provided Matrix and masked by the provided color mask.
247 | //
248 | // If there's a lot of text written to the Text, changing a matrix or a color mask often might hurt
249 | // performance. Consider using your Target's SetMatrix or SetColorMask methods if available.
250 | func (txt *Text) DrawColorMask(t pixel.Target, matrix pixel.Matrix, mask color.Color) {
251 | if matrix != txt.mat {
252 | txt.mat = matrix
253 | txt.dirty = true
254 | }
255 |
256 | offset := txt.Orig.Sub(txt.Bounds().Max.Add(txt.Bounds().AnchorPos(txt.anchor.Opposite())))
257 | txt.mat = pixel.IM.Moved(offset).Chained(txt.mat)
258 |
259 | if mask == nil {
260 | mask = pixel.Alpha(1)
261 | }
262 | rgba := pixel.ToRGBA(mask)
263 | if rgba != txt.col {
264 | txt.col = rgba
265 | txt.dirty = true
266 | }
267 |
268 | if txt.dirty {
269 | txt.trans.SetLen(txt.tris.Len())
270 | txt.trans.Update(&txt.tris)
271 |
272 | for i := range txt.trans {
273 | txt.trans[i].Position = txt.mat.Project(txt.trans[i].Position)
274 | txt.trans[i].Color = txt.trans[i].Color.Mul(txt.col)
275 | }
276 |
277 | txt.transD.Dirty()
278 | txt.dirty = false
279 | }
280 |
281 | txt.transD.Draw(t)
282 | }
283 |
284 | // controlRune checks if r is a control rune (newline, tab, ...). If it is, a new dot position and
285 | // true is returned. If r is not a control rune, the original dot and false is returned.
286 | func (txt *Text) controlRune(r rune, dot pixel.Vec) (newDot pixel.Vec, control bool) {
287 | switch r {
288 | case '\n':
289 | dot.X = txt.Orig.X
290 | dot.Y -= txt.LineHeight
291 | case '\r':
292 | dot.X = txt.Orig.X
293 | case '\t':
294 | rem := math.Mod(dot.X-txt.Orig.X, txt.TabWidth)
295 | rem = math.Mod(rem, rem+txt.TabWidth)
296 | if rem == 0 {
297 | rem = txt.TabWidth
298 | }
299 | dot.X += rem
300 | default:
301 | return dot, false
302 | }
303 | return dot, true
304 | }
305 |
306 | func (txt *Text) drawBuf() {
307 | if !utf8.FullRune(txt.buf) {
308 | return
309 | }
310 |
311 | rgba := pixel.ToRGBA(txt.Color)
312 | for i := range txt.glyph {
313 | txt.glyph[i].Color = rgba
314 | }
315 |
316 | for utf8.FullRune(txt.buf) {
317 | r, size := utf8.DecodeRune(txt.buf)
318 | txt.buf = txt.buf[size:]
319 |
320 | var control bool
321 | txt.Dot, control = txt.controlRune(r, txt.Dot)
322 | if control {
323 | continue
324 | }
325 |
326 | var rect, frame, bounds pixel.Rect
327 | rect, frame, bounds, txt.Dot = txt.Atlas().DrawRune(txt.prevR, r, txt.Dot)
328 |
329 | txt.prevR = r
330 |
331 | rv := [...]pixel.Vec{
332 | {X: rect.Min.X, Y: rect.Min.Y},
333 | {X: rect.Max.X, Y: rect.Min.Y},
334 | {X: rect.Max.X, Y: rect.Max.Y},
335 | {X: rect.Min.X, Y: rect.Max.Y},
336 | }
337 |
338 | fv := [...]pixel.Vec{
339 | {X: frame.Min.X, Y: frame.Min.Y},
340 | {X: frame.Max.X, Y: frame.Min.Y},
341 | {X: frame.Max.X, Y: frame.Max.Y},
342 | {X: frame.Min.X, Y: frame.Max.Y},
343 | }
344 |
345 | for i, j := range [...]int{0, 1, 2, 0, 2, 3} {
346 | txt.glyph[i].Position = rv[j]
347 | txt.glyph[i].Picture = fv[j]
348 | }
349 |
350 | txt.tris = append(txt.tris, txt.glyph...)
351 | txt.dirty = true
352 |
353 | if txt.bounds.W()*txt.bounds.H() == 0 {
354 | txt.bounds = bounds
355 | } else {
356 | txt.bounds = txt.bounds.Union(bounds)
357 | }
358 | }
359 | }
360 |
--------------------------------------------------------------------------------
/text/text_test.go:
--------------------------------------------------------------------------------
1 | package text_test
2 |
3 | import (
4 | "fmt"
5 | "math/rand"
6 | "testing"
7 | "unicode"
8 |
9 | "golang.org/x/image/font/basicfont"
10 | "golang.org/x/image/font/gofont/goregular"
11 |
12 | "github.com/faiface/pixel"
13 | "github.com/faiface/pixel/text"
14 | "github.com/golang/freetype/truetype"
15 | )
16 |
17 | func TestClear(t *testing.T) {
18 | txt := text.New(pixel.ZV, text.Atlas7x13)
19 |
20 | if got, want := txt.Dot, pixel.ZV; !eqVectors(got, want) {
21 | t.Fatalf("txt.Dot = %v, want %v", got, want)
22 | }
23 |
24 | fmt.Fprint(txt, "Test\nClear")
25 |
26 | if got, want := txt.Dot, pixel.V(35, -13); !eqVectors(got, want) {
27 | t.Fatalf("txt.Dot = %v, want %v", got, want)
28 | }
29 |
30 | txt.Clear()
31 |
32 | if got, want := txt.Dot, pixel.ZV; !eqVectors(got, want) {
33 | t.Fatalf("txt.Dot = %v, want %v", got, want)
34 | }
35 | }
36 |
37 | func BenchmarkNewAtlas(b *testing.B) {
38 | runeSets := []struct {
39 | name string
40 | set []rune
41 | }{
42 | {"ASCII", text.ASCII},
43 | {"Latin", text.RangeTable(unicode.Latin)},
44 | }
45 |
46 | ttf, _ := truetype.Parse(goregular.TTF)
47 | face := truetype.NewFace(ttf, &truetype.Options{
48 | Size: 16,
49 | GlyphCacheEntries: 1,
50 | })
51 |
52 | for _, runeSet := range runeSets {
53 | b.Run(runeSet.name, func(b *testing.B) {
54 | for i := 0; i < b.N; i++ {
55 | _ = text.NewAtlas(face, runeSet.set)
56 | }
57 | })
58 | }
59 | }
60 |
61 | func BenchmarkTextWrite(b *testing.B) {
62 | runeSet := text.ASCII
63 | atlas := text.NewAtlas(basicfont.Face7x13, runeSet)
64 |
65 | lengths := []int{1, 10, 100, 1000}
66 | chunks := make([][]byte, len(lengths))
67 | for i := range chunks {
68 | chunk := make([]rune, lengths[i])
69 | for j := range chunk {
70 | chunk[j] = runeSet[rand.Intn(len(runeSet))]
71 | }
72 | chunks[i] = []byte(string(chunk))
73 | }
74 |
75 | for _, chunk := range chunks {
76 | b.Run(fmt.Sprintf("%d", len(chunk)), func(b *testing.B) {
77 | txt := text.New(pixel.ZV, atlas)
78 | for i := 0; i < b.N; i++ {
79 | _, _ = txt.Write(chunk)
80 | }
81 | })
82 | }
83 | }
84 |
85 | func eqVectors(a, b pixel.Vec) bool {
86 | return (a.X == b.X && a.Y == b.Y)
87 | }
88 |
--------------------------------------------------------------------------------
/vector.go:
--------------------------------------------------------------------------------
1 | package pixel
2 |
3 | import (
4 | "fmt"
5 | "math"
6 | )
7 |
8 | // Vec is a 2D vector type with X and Y coordinates.
9 | //
10 | // Create vectors with the V constructor:
11 | //
12 | // u := pixel.V(1, 2)
13 | // v := pixel.V(8, -3)
14 | //
15 | // Use various methods to manipulate them:
16 | //
17 | // w := u.Add(v)
18 | // fmt.Println(w) // Vec(9, -1)
19 | // fmt.Println(u.Sub(v)) // Vec(-7, 5)
20 | // u = pixel.V(2, 3)
21 | // v = pixel.V(8, 1)
22 | // if u.X < 0 {
23 | // fmt.Println("this won't happen")
24 | // }
25 | // x := u.Unit().Dot(v.Unit())
26 | type Vec struct {
27 | X, Y float64
28 | }
29 |
30 | // ZV is a zero vector.
31 | var ZV = Vec{0, 0}
32 |
33 | // V returns a new 2D vector with the given coordinates.
34 | func V(x, y float64) Vec {
35 | return Vec{x, y}
36 | }
37 |
38 | // nearlyEqual compares two float64s and returns whether they are equal, accounting for rounding errors.At worst, the
39 | // result is correct to 7 significant digits.
40 | func nearlyEqual(a, b float64) bool {
41 | epsilon := 0.000001
42 |
43 | if a == b {
44 | return true
45 | }
46 |
47 | diff := math.Abs(a - b)
48 |
49 | if a == 0.0 || b == 0.0 || diff < math.SmallestNonzeroFloat64 {
50 | return diff < (epsilon * math.SmallestNonzeroFloat64)
51 | }
52 |
53 | absA := math.Abs(a)
54 | absB := math.Abs(b)
55 |
56 | return diff/math.Min(absA+absB, math.MaxFloat64) < epsilon
57 | }
58 |
59 | // Eq will compare two vectors and return whether they are equal accounting for rounding errors. At worst, the result
60 | // is correct to 7 significant digits.
61 | func (u Vec) Eq(v Vec) bool {
62 | return nearlyEqual(u.X, v.X) && nearlyEqual(u.Y, v.Y)
63 | }
64 |
65 | // Unit returns a vector of length 1 facing the given angle.
66 | func Unit(angle float64) Vec {
67 | return Vec{1, 0}.Rotated(angle)
68 | }
69 |
70 | // String returns the string representation of the vector u.
71 | //
72 | // u := pixel.V(4.5, -1.3)
73 | // u.String() // returns "Vec(4.5, -1.3)"
74 | // fmt.Println(u) // Vec(4.5, -1.3)
75 | func (u Vec) String() string {
76 | return fmt.Sprintf("Vec(%v, %v)", u.X, u.Y)
77 | }
78 |
79 | // XY returns the components of the vector in two return values.
80 | func (u Vec) XY() (x, y float64) {
81 | return u.X, u.Y
82 | }
83 |
84 | // Add returns the sum of vectors u and v.
85 | func (u Vec) Add(v Vec) Vec {
86 | return Vec{
87 | u.X + v.X,
88 | u.Y + v.Y,
89 | }
90 | }
91 |
92 | // Sub returns the difference betweeen vectors u and v.
93 | func (u Vec) Sub(v Vec) Vec {
94 | return Vec{
95 | u.X - v.X,
96 | u.Y - v.Y,
97 | }
98 | }
99 |
100 | // Floor converts x and y to their integer equivalents.
101 | func (u Vec) Floor() Vec {
102 | return Vec{
103 | math.Floor(u.X),
104 | math.Floor(u.Y),
105 | }
106 | }
107 |
108 | // To returns the vector from u to v. Equivalent to v.Sub(u).
109 | func (u Vec) To(v Vec) Vec {
110 | return Vec{
111 | v.X - u.X,
112 | v.Y - u.Y,
113 | }
114 | }
115 |
116 | // Scaled returns the vector u multiplied by c.
117 | func (u Vec) Scaled(c float64) Vec {
118 | return Vec{u.X * c, u.Y * c}
119 | }
120 |
121 | // ScaledXY returns the vector u multiplied by the vector v component-wise.
122 | func (u Vec) ScaledXY(v Vec) Vec {
123 | return Vec{u.X * v.X, u.Y * v.Y}
124 | }
125 |
126 | // Len returns the length of the vector u.
127 | func (u Vec) Len() float64 {
128 | return math.Hypot(u.X, u.Y)
129 | }
130 |
131 | // SqLen returns the squared length of the vector u (faster to compute than Len).
132 | func (u Vec) SqLen() float64 {
133 | return u.X*u.X + u.Y*u.Y
134 | }
135 |
136 | // Angle returns the angle between the vector u and the x-axis. The result is in range [-Pi, Pi].
137 | func (u Vec) Angle() float64 {
138 | return math.Atan2(u.Y, u.X)
139 | }
140 |
141 | // Unit returns a vector of length 1 facing the direction of u (has the same angle).
142 | func (u Vec) Unit() Vec {
143 | if u.X == 0 && u.Y == 0 {
144 | return Vec{1, 0}
145 | }
146 | return u.Scaled(1 / u.Len())
147 | }
148 |
149 | // Rotated returns the vector u rotated by the given angle in radians.
150 | func (u Vec) Rotated(angle float64) Vec {
151 | sin, cos := math.Sincos(angle)
152 | return Vec{
153 | u.X*cos - u.Y*sin,
154 | u.X*sin + u.Y*cos,
155 | }
156 | }
157 |
158 | // Normal returns a vector normal to u. Equivalent to u.Rotated(math.Pi / 2), but faster.
159 | func (u Vec) Normal() Vec {
160 | return Vec{-u.Y, u.X}
161 | }
162 |
163 | // Dot returns the dot product of vectors u and v.
164 | func (u Vec) Dot(v Vec) float64 {
165 | return u.X*v.X + u.Y*v.Y
166 | }
167 |
168 | // Cross return the cross product of vectors u and v.
169 | func (u Vec) Cross(v Vec) float64 {
170 | return u.X*v.Y - v.X*u.Y
171 | }
172 |
173 | // Project returns a projection (or component) of vector u in the direction of vector v.
174 | //
175 | // Behaviour is undefined if v is a zero vector.
176 | func (u Vec) Project(v Vec) Vec {
177 | len := u.Dot(v) / v.Len()
178 | return v.Unit().Scaled(len)
179 | }
180 |
181 | // Map applies the function f to both x and y components of the vector u and returns the modified
182 | // vector.
183 | //
184 | // u := pixel.V(10.5, -1.5)
185 | // v := u.Map(math.Floor) // v is Vec(10, -2), both components of u floored
186 | func (u Vec) Map(f func(float64) float64) Vec {
187 | return Vec{
188 | f(u.X),
189 | f(u.Y),
190 | }
191 | }
192 |
193 | // Lerp returns a linear interpolation between vectors a and b.
194 | //
195 | // This function basically returns a point along the line between a and b and t chooses which one.
196 | // If t is 0, then a will be returned, if t is 1, b will be returned. Anything between 0 and 1 will
197 | // return the appropriate point between a and b and so on.
198 | func Lerp(a, b Vec, t float64) Vec {
199 | return a.Scaled(1 - t).Add(b.Scaled(t))
200 | }
201 |
202 | // Line is a 2D line segment, between points A and B.
203 | type Line struct {
204 | A, B Vec
205 | }
206 |
207 | // L creates and returns a new Line.
208 | func L(from, to Vec) Line {
209 | return Line{
210 | A: from,
211 | B: to,
212 | }
213 | }
214 |
215 | // Bounds returns the lines bounding box. This is in the form of a normalized Rect.
216 | func (l Line) Bounds() Rect {
217 | return R(l.A.X, l.A.Y, l.B.X, l.B.Y).Norm()
218 | }
219 |
220 | // Center will return the point at center of the line; that is, the point equidistant from either end.
221 | func (l Line) Center() Vec {
222 | return l.A.Add(l.A.To(l.B).Scaled(0.5))
223 | }
224 |
225 | // Closest will return the point on the line which is closest to the Vec provided.
226 | func (l Line) Closest(v Vec) Vec {
227 | // between is a helper function which determines whether x is greater than min(a, b) and less than max(a, b)
228 | between := func(a, b, x float64) bool {
229 | min := math.Min(a, b)
230 | max := math.Max(a, b)
231 | return min < x && x < max
232 | }
233 |
234 | // Closest point will be on a line which perpendicular to this line.
235 | // If and only if the infinite perpendicular line intersects the segment.
236 | m, b := l.Formula()
237 |
238 | // Account for horizontal lines
239 | if m == 0 {
240 | x := v.X
241 | y := l.A.Y
242 |
243 | // check if the X coordinate of v is on the line
244 | if between(l.A.X, l.B.X, v.X) {
245 | return V(x, y)
246 | }
247 |
248 | // Otherwise get the closest endpoint
249 | if l.A.To(v).Len() < l.B.To(v).Len() {
250 | return l.A
251 | }
252 | return l.B
253 | }
254 |
255 | // Account for vertical lines
256 | if math.IsInf(math.Abs(m), 1) {
257 | x := l.A.X
258 | y := v.Y
259 |
260 | // check if the Y coordinate of v is on the line
261 | if between(l.A.Y, l.B.Y, v.Y) {
262 | return V(x, y)
263 | }
264 |
265 | // Otherwise get the closest endpoint
266 | if l.A.To(v).Len() < l.B.To(v).Len() {
267 | return l.A
268 | }
269 | return l.B
270 | }
271 |
272 | perpendicularM := -1 / m
273 | perpendicularB := v.Y - (perpendicularM * v.X)
274 |
275 | // Coordinates of intersect (of infinite lines)
276 | x := (perpendicularB - b) / (m - perpendicularM)
277 | y := m*x + b
278 |
279 | // Check if the point lies between the x and y bounds of the segment
280 | if !between(l.A.X, l.B.X, x) && !between(l.A.Y, l.B.Y, y) {
281 | // Not within bounding box
282 | toStart := v.To(l.A)
283 | toEnd := v.To(l.B)
284 |
285 | if toStart.Len() < toEnd.Len() {
286 | return l.A
287 | }
288 | return l.B
289 | }
290 |
291 | return V(x, y)
292 | }
293 |
294 | // Contains returns whether the provided Vec lies on the line.
295 | func (l Line) Contains(v Vec) bool {
296 | return l.Closest(v).Eq(v)
297 | }
298 |
299 | // Formula will return the values that represent the line in the formula: y = mx + b
300 | // This function will return math.Inf+, math.Inf- for a vertical line.
301 | func (l Line) Formula() (m, b float64) {
302 | // Account for horizontal lines
303 | if l.B.Y == l.A.Y {
304 | return 0, l.A.Y
305 | }
306 |
307 | m = (l.B.Y - l.A.Y) / (l.B.X - l.A.X)
308 | b = l.A.Y - (m * l.A.X)
309 |
310 | return m, b
311 | }
312 |
313 | // Intersect will return the point of intersection for the two line segments. If the line segments do not intersect,
314 | // this function will return the zero-vector and false.
315 | func (l Line) Intersect(k Line) (Vec, bool) {
316 | // Check if the lines are parallel
317 | lDir := l.A.To(l.B)
318 | kDir := k.A.To(k.B)
319 | if lDir.X == kDir.X && lDir.Y == kDir.Y {
320 | return ZV, false
321 | }
322 |
323 | // The lines intersect - but potentially not within the line segments.
324 | // Get the intersection point for the lines if they were infinitely long, check if the point exists on both of the
325 | // segments
326 | lm, lb := l.Formula()
327 | km, kb := k.Formula()
328 |
329 | // Account for vertical lines
330 | if math.IsInf(math.Abs(lm), 1) && math.IsInf(math.Abs(km), 1) {
331 | // Both vertical, therefore parallel
332 | return ZV, false
333 | }
334 |
335 | var x, y float64
336 |
337 | if math.IsInf(math.Abs(lm), 1) || math.IsInf(math.Abs(km), 1) {
338 | // One line is vertical
339 | intersectM := lm
340 | intersectB := lb
341 | verticalLine := k
342 |
343 | if math.IsInf(math.Abs(lm), 1) {
344 | intersectM = km
345 | intersectB = kb
346 | verticalLine = l
347 | }
348 |
349 | y = intersectM*verticalLine.A.X + intersectB
350 | x = verticalLine.A.X
351 | } else {
352 | // Coordinates of intersect
353 | x = (kb - lb) / (lm - km)
354 | y = lm*x + lb
355 | }
356 |
357 | if l.Contains(V(x, y)) && k.Contains(V(x, y)) {
358 | // The intersect point is on both line segments, they intersect.
359 | return V(x, y), true
360 | }
361 |
362 | return ZV, false
363 | }
364 |
365 | // IntersectCircle will return the shortest Vec such that moving the Line by that Vec will cause the Line and Circle
366 | // to no longer intesect. If they do not intersect at all, this function will return a zero-vector.
367 | func (l Line) IntersectCircle(c Circle) Vec {
368 | // Get the point on the line closest to the center of the circle.
369 | closest := l.Closest(c.Center)
370 | cirToClosest := c.Center.To(closest)
371 |
372 | if cirToClosest.Len() >= c.Radius {
373 | return ZV
374 | }
375 |
376 | return cirToClosest.Scaled(cirToClosest.Len() - c.Radius)
377 | }
378 |
379 | // IntersectRect will return the shortest Vec such that moving the Line by that Vec will cause the Line and Rect to
380 | // no longer intesect. If they do not intersect at all, this function will return a zero-vector.
381 | func (l Line) IntersectRect(r Rect) Vec {
382 | // Check if either end of the line segment are within the rectangle
383 | if r.Contains(l.A) || r.Contains(l.B) {
384 | // Use the Rect.Intersect to get minimal return value
385 | rIntersect := l.Bounds().Intersect(r)
386 | if rIntersect.H() > rIntersect.W() {
387 | // Go vertical
388 | return V(0, rIntersect.H())
389 | }
390 | return V(rIntersect.W(), 0)
391 | }
392 |
393 | // Check if any of the rectangles' edges intersect with this line.
394 | for _, edge := range r.Edges() {
395 | if _, ok := l.Intersect(edge); ok {
396 | // Get the closest points on the line to each corner, where:
397 | // - the point is contained by the rectangle
398 | // - the point is not the corner itself
399 | corners := r.Vertices()
400 | var closest *Vec
401 | closestCorner := corners[0]
402 | for _, c := range corners {
403 | cc := l.Closest(c)
404 | if closest == nil || (closest.Len() > cc.Len() && r.Contains(cc)) {
405 | closest = &cc
406 | closestCorner = c
407 | }
408 | }
409 |
410 | return closest.To(closestCorner)
411 | }
412 | }
413 |
414 | // No intersect
415 | return ZV
416 | }
417 |
418 | // Len returns the length of the line segment.
419 | func (l Line) Len() float64 {
420 | return l.A.To(l.B).Len()
421 | }
422 |
423 | // Moved will return a line moved by the delta Vec provided.
424 | func (l Line) Moved(delta Vec) Line {
425 | return Line{
426 | A: l.A.Add(delta),
427 | B: l.B.Add(delta),
428 | }
429 | }
430 |
431 | // Rotated will rotate the line around the provided Vec.
432 | func (l Line) Rotated(around Vec, angle float64) Line {
433 | // Move the line so we can use `Vec.Rotated`
434 | lineShifted := l.Moved(around.Scaled(-1))
435 |
436 | lineRotated := Line{
437 | A: lineShifted.A.Rotated(angle),
438 | B: lineShifted.B.Rotated(angle),
439 | }
440 |
441 | return lineRotated.Moved(around)
442 | }
443 |
444 | // Scaled will return the line scaled around the center point.
445 | func (l Line) Scaled(scale float64) Line {
446 | return l.ScaledXY(l.Center(), scale)
447 | }
448 |
449 | // ScaledXY will return the line scaled around the Vec provided.
450 | func (l Line) ScaledXY(around Vec, scale float64) Line {
451 | toA := around.To(l.A).Scaled(scale)
452 | toB := around.To(l.B).Scaled(scale)
453 |
454 | return Line{
455 | A: around.Add(toA),
456 | B: around.Add(toB),
457 | }
458 | }
459 |
460 | func (l Line) String() string {
461 | return fmt.Sprintf("Line(%v, %v)", l.A, l.B)
462 | }
463 |
--------------------------------------------------------------------------------
/vector_test.go:
--------------------------------------------------------------------------------
1 | package pixel_test
2 |
3 | import (
4 | "fmt"
5 | "testing"
6 |
7 | "github.com/faiface/pixel"
8 | )
9 |
10 | type floorTest struct {
11 | input pixel.Vec
12 | expected pixel.Vec
13 | }
14 |
15 | func TestFloor(t *testing.T) {
16 | tests := []floorTest{
17 | {input: pixel.V(4.50, 6.70), expected: pixel.V(4, 6)},
18 | {input: pixel.V(9.0, 6.70), expected: pixel.V(9, 6)},
19 | }
20 |
21 | for _, tc := range tests {
22 | result := tc.input.Floor()
23 | if result != tc.expected {
24 | t.Error(fmt.Sprintf("Expected %v but got %v", tc.expected, result))
25 | }
26 | }
27 | }
28 |
--------------------------------------------------------------------------------