Compare commits
317 Commits
Author | SHA1 | Date |
---|---|---|
|
0a251bc08b | |
|
d3c72ae210 | |
|
20c056b950 | |
|
9ad345b415 | |
|
174abb179a | |
|
4c172faca7 | |
|
d899a6bbed | |
|
a9abe2ebb7 | |
|
ab135a4965 | |
|
08fc8acdfa | |
|
069cc4e011 | |
|
2c3a9a03bd | |
|
cb394bc26f | |
|
8acf4e5195 | |
|
ccc8deb923 | |
|
e448943963 | |
|
8ce328cb89 | |
|
389317124e | |
|
33024feabf | |
|
d119f130f6 | |
|
b61f150701 | |
|
f079cc25fe | |
|
9e8e09f1d7 | |
|
96863238e9 | |
|
649411a23c | |
|
d940a42454 | |
|
160e665d51 | |
|
1d936eae7b | |
|
15972155d3 | |
|
4964768d4e | |
|
d5761bda94 | |
|
552871e324 | |
|
4a8b06746f | |
|
7fe32e1553 | |
|
6c717101a7 | |
|
881bfeda91 | |
|
b4d312046c | |
|
f6e2f3a8d8 | |
|
4ec3c64512 | |
|
bb0a0f485e | |
|
aae9927b58 | |
|
9307280c91 | |
|
7615c055cf | |
|
80b107c039 | |
|
98f3e740e6 | |
|
798c6c2a48 | |
|
063b8952f9 | |
|
842ae8d470 | |
|
b31c294d4d | |
|
75e7b4120b | |
|
0e9102e224 | |
|
2101dc89e3 | |
|
eae5a2e96b | |
|
24ffdadbf8 | |
|
f931f69d6e | |
|
6e34aae429 | |
|
cef3a7e7a6 | |
|
fd754b5263 | |
|
ef6cafa0cb | |
|
7a67b10cec | |
|
7027267cbf | |
|
ba7fa122b2 | |
|
65ff420b25 | |
|
fd73dd5216 | |
|
775e23efac | |
|
ae6c93ba96 | |
|
3b599e70ec | |
|
7d92e04e63 | |
|
c9681cec8a | |
|
2342984744 | |
|
a8b7779fbe | |
|
7a106fa572 | |
|
e66e94b943 | |
|
d6663ba0a0 | |
|
50e1d8c96b | |
|
2c79e7325a | |
|
865e930023 | |
|
88b1e1c2d3 | |
|
850dd62a98 | |
|
926463aa6f | |
|
543819d066 | |
|
c3e5e4fdb2 | |
|
e79a8c37e6 | |
|
d65a63e962 | |
|
6cef9cf779 | |
|
1f914181d8 | |
|
a61d321615 | |
|
585f7c1f14 | |
|
607e0a11c0 | |
|
506588f40b | |
|
42582c7f8e | |
|
625c4c393d | |
|
5568202e30 | |
|
70eff126da | |
|
5d34c08754 | |
|
c35e0abd1c | |
|
a3f51715d9 | |
|
b5e4674b45 | |
|
9a9f831114 | |
|
6b71245324 | |
|
5254b4eb62 | |
|
76534cb8d0 | |
|
8792d4436c | |
|
d2d1f03c32 | |
|
6a71f07d12 | |
|
79a3bd7c63 | |
|
b7d5e41486 | |
|
5e3b82eeab | |
|
d7884ac96a | |
|
b40ab4939f | |
|
6b4a6905de | |
|
0a8feedd8a | |
|
914e25233f | |
|
197e604220 | |
|
e350f9fef1 | |
|
d8a3c92052 | |
|
d35b25061e | |
|
b9c32bc51f | |
|
0110a3c88a | |
|
3be4e4cb84 | |
|
bd68a95818 | |
|
d181dae9ba | |
|
42b1229128 | |
|
37f588845a | |
|
8909d199d1 | |
|
09ea5585a0 | |
|
8012403b8f | |
|
dbdea628da | |
|
fc543042cc | |
|
ed63c18bb4 | |
|
9cc1cddcf2 | |
|
73ce868aa9 | |
|
effd43bc56 | |
|
fc29ad7886 | |
|
93a24aa6df | |
|
7e3a88ef63 | |
|
484c1775b0 | |
|
a3aba6004d | |
|
e51d4a6676 | |
|
87299dc29b | |
|
0d44d2cc9b | |
|
c4f9faea97 | |
|
18972c2818 | |
|
853405f61c | |
|
8d0c306de9 | |
|
837a4efa80 | |
|
706becb764 | |
|
bb82a87aaf | |
|
f24d465808 | |
|
aeef94f200 | |
|
2aa6d2d212 | |
|
8a9191c283 | |
|
07754109cb | |
|
16d04f467f | |
|
a5c3bec928 | |
|
d513ffd192 | |
|
c5c5127c8d | |
|
109cb03fae | |
|
23171aea6c | |
|
dd5e0d8b09 | |
|
3c6f33365b | |
|
38283d0e55 | |
|
daa8205b8c | |
|
ba6fc7db8a | |
|
285676ca17 | |
|
b18647916f | |
|
de3617a259 | |
|
1f96c1e995 | |
|
a68a4e38b4 | |
|
9e6e6197dd | |
|
159df28dcc | |
|
d48ef44bdc | |
|
9aca3bfe7a | |
|
2cf81cd4df | |
|
3aad3268cd | |
|
9d5c7afe12 | |
|
85b18b567a | |
|
e8ba24ea40 | |
|
d601bc65e4 | |
|
2cb87efb8c | |
|
ccf8e9dcdc | |
|
95ac5e1e2a | |
|
96e0a8f3bf | |
|
f698bae1df | |
|
a6c8b92517 | |
|
9eb2834a10 | |
|
0092d6a577 | |
|
b8bb00a161 | |
|
29b1220ec3 | |
|
364a7a84ae | |
|
bcda85acd2 | |
|
966150a856 | |
|
f3377bb16f | |
|
4795a92b41 | |
|
83c62a0313 | |
|
c3e69c4f35 | |
|
352785e1b8 | |
|
e6392a228d | |
|
3592de858c | |
|
a1d36f8c7e | |
|
98d5b9b417 | |
|
e5ff236d71 | |
|
faf6558294 | |
|
e99ac56daa | |
|
aa7ef59ecd | |
|
eb3d7e0787 | |
|
11e2012ef5 | |
|
8cd352b7a3 | |
|
997f23dfb5 | |
|
04c3ef72a3 | |
|
237d77596f | |
|
c7eac06499 | |
|
55b87ca5b1 | |
|
2478da5d12 | |
|
cca37c750e | |
|
1a275b5929 | |
|
9d07d69429 | |
|
128ec4d4c0 | |
|
1eac5d8dc2 | |
|
a5ea71811e | |
|
d3912a6486 | |
|
0db4348595 | |
|
b2c8bff6eb | |
|
95148365d0 | |
|
50ef216e79 | |
|
09db6d8b38 | |
|
86de5123c0 | |
|
470791a59d | |
|
0475b1ddb4 | |
|
c9328c9260 | |
|
c7836cb3ad | |
|
3d2f083f70 | |
|
771e2e92e2 | |
|
09b50d2296 | |
|
66670d9905 | |
|
489b03138f | |
|
3cc0ec3d30 | |
|
b3246f6234 | |
|
680d16c17b | |
|
8563c28493 | |
|
160bcb9960 | |
|
6f1e3bbbf8 | |
|
cc9f03d393 | |
|
b61602cdf4 | |
|
f21c48599f | |
|
ab70533793 | |
|
0a0c8ff110 | |
|
5abf2d29a6 | |
|
4d30a8fe49 | |
|
ecda96a36f | |
|
520459bc6d | |
|
e225e9a1ef | |
|
13171c409b | |
|
2e275ab0d5 | |
|
23168ee324 | |
|
44d030b9ca | |
|
b06a31baa3 | |
|
4c6d061455 | |
|
620c551f70 | |
|
16722d55e1 | |
|
ee24eeb67d | |
|
5072f34b91 | |
|
d4530ca9fe | |
|
3b63b7eff9 | |
|
91c16c34da | |
|
e8a3621140 | |
|
730d33a341 | |
|
42cc2a0376 | |
|
742d1226d8 | |
|
0a054f35fd | |
|
392fe90b11 | |
|
2e0da4f44a | |
|
b0ed22e0ec | |
|
0aec9fead0 | |
|
1e19db5b25 | |
|
a0713598da | |
|
3b366d1edd | |
|
cabaee680e | |
|
8650692efa | |
|
ba4f417559 | |
|
ecd686769c | |
|
2cce50832d | |
|
c70d7575ce | |
|
4eba5e37ae | |
|
0cddb56114 | |
|
4b6cf201f7 | |
|
cff8697c97 | |
|
d0ceea6849 | |
|
56cd321ab8 | |
|
8efed1de5c | |
|
9a32601c6b | |
|
fdc068e855 | |
|
9b8d4c7461 | |
|
a56f7fa422 | |
|
0d5ba92fbe | |
|
20b05d9ec2 | |
|
d102169151 | |
|
f1ad821f1a | |
|
3be890ea80 | |
|
a46f6f6cf4 | |
|
9e0f11abbb | |
|
8d20deea05 | |
|
e7a625f5d2 | |
|
d3f6331240 | |
|
ee54171247 | |
|
2b79131104 | |
|
4c817d7c20 | |
|
3bed5a8449 | |
|
a1bbfa3020 | |
|
026878de07 | |
|
52b4384240 | |
|
676509d856 | |
|
bdd9349b37 | |
|
c18b8f1c29 | |
|
7b509e1d7d | |
|
4a0b2f5107 | |
|
17e407f0c1 |
22
.travis.yml
22
.travis.yml
|
@ -1,4 +1,6 @@
|
|||
language: go
|
||||
# https://github.com/golang/go/issues/31293
|
||||
dist: xenial
|
||||
sudo: false
|
||||
addons:
|
||||
apt:
|
||||
|
@ -11,14 +13,22 @@ addons:
|
|||
- libxi-dev
|
||||
- libopenal-dev
|
||||
- libasound2-dev
|
||||
- libgl1-mesa-dev
|
||||
|
||||
services:
|
||||
- xvfb
|
||||
|
||||
env:
|
||||
- GO111MODULE=on
|
||||
|
||||
go:
|
||||
- 1.8
|
||||
- 1.7.4
|
||||
- tip
|
||||
- tip
|
||||
- 1.12.x
|
||||
|
||||
install:
|
||||
- go get -t ./...
|
||||
- # Do nothing. This is needed to prevent the default install action
|
||||
# "go get -t -v ./..." from happening here because we want it to happen
|
||||
# inside script step.
|
||||
|
||||
script:
|
||||
- go test -i -race ./...
|
||||
- go test -v -race ./...
|
||||
- go test -v -race -mod=readonly ./...
|
||||
|
|
|
@ -0,0 +1,63 @@
|
|||
# Changelog
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [Unreleased]
|
||||
- Add AnchorPos struct and functions #252
|
||||
- Add Clipboard Support
|
||||
- Fix SIGSEGV on text.NewAtlas if glyph absent
|
||||
- Use slice for range in Drawer.Dirty(), to improve performance
|
||||
- GLTriangle's fragment shader is used when rendered by the Canvas.
|
||||
- Add MSAA support
|
||||
|
||||
## [v0.10.0] 2020-08-22
|
||||
- Add AnchorPos struct and functions
|
||||
- Gamepad API added
|
||||
- Support setting an initial window position
|
||||
- Support hiding the window initially
|
||||
- Support creating maximized windows
|
||||
- Support waiting for events to reduce CPU load
|
||||
- Adding clipping rectangle support in GLTriangles
|
||||
|
||||
## [v0.10.0-beta] 2020-05-10
|
||||
- Add `WindowConfig.TransparentFramebuffer` option to support window transparency onto the background
|
||||
- Fixed Line intersects failing on lines passing through (0, 0)
|
||||
|
||||
## [v0.10.0-alpha] 2020-05-08
|
||||
- Upgrade to GLFW 3.3! :tada:
|
||||
- Closes https://github.com/faiface/pixel/issues/137
|
||||
- Add support for glfw's DisableCursor
|
||||
- Closes https://github.com/faiface/pixel/issues/213
|
||||
|
||||
## [v0.9.0] - 2020-05-02
|
||||
- Added feature from https://github.com/faiface/pixel/pull/219
|
||||
- Exposing Window.SwapBuffers so buffers can be swapped without polling input
|
||||
- Add more examples
|
||||
- Add position as out variable from vertex shader
|
||||
- Add experimental joystick support
|
||||
- Add mouse cursor operations
|
||||
- Add `Vec.Floor(…)` function
|
||||
- Add circle geometry
|
||||
- Fix `Matrix.Unproject(…)` for rotated matrix
|
||||
- Add 2D Line geometry
|
||||
- Add floating point round error correction
|
||||
- Performance improvements
|
||||
- Fix race condition in `NewGLTriangles(…)`
|
||||
- Add `TriangleData` benchmarks and improvements
|
||||
- Add zero rectangle variable for utility and consistency
|
||||
- Add support for Go Modules
|
||||
- Add `NoIconify` and `AlwaysOnTop` window hints
|
||||
|
||||
|
||||
## [v0.8.0] - 2018-10-10
|
||||
Changelog for this and older versions can be found on the corresponding [GitHub
|
||||
releases](https://github.com/faiface/pixel/releases).
|
||||
|
||||
[Unreleased]: https://github.com/faiface/pixel/compare/v0.10.0...HEAD
|
||||
[v0.10.0]: https://github.com/faiface/pixel/compare/v0.10.0-beta...v0.10.0
|
||||
[v0.10.0-beta]: https://github.com/faiface/pixel/compare/v0.10.0-alpha...v0.10.0-beta
|
||||
[v0.10.0-alpha]: https://github.com/faiface/pixel/compare/v0.9.0...v0.10.0-alpha
|
||||
[v0.9.0]: https://github.com/faiface/pixel/compare/v0.8.0...v0.9.0
|
||||
[v0.8.0]: https://github.com/faiface/pixel/releases/tag/v0.8.0
|
|
@ -4,11 +4,13 @@
|
|||
|
||||
## Here are a few ways you can contribute
|
||||
|
||||
1. **Make a community example** and place it inside the [examples/community](examples/community) folder.
|
||||
1. **Make a community example** and place it inside the `community` folder of the [examples repository][examples].
|
||||
2. **Add tests**. There only few tests in Pixel at the moment. Take a look at them and make some similar.
|
||||
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.
|
||||
4. **Join the big development** by joining the discussion at the [Gitter](https://gitter.im/pixellib/Lobby), where we can discuss bigger changes and implement them after that.
|
||||
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.
|
||||
|
||||
## How to make a pull request
|
||||
|
||||
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/.
|
||||
|
||||
[examples]: https://github.com/faiface/pixel-examples/tree/master/community
|
||||
|
|
42
README.md
42
README.md
|
@ -1,5 +1,12 @@
|
|||
# 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)
|
||||
# \*\*\*\*\*NOTICE\*\*\*\*\*
|
||||
|
||||
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.
|
||||
|
||||
|
||||
|
||||
<p align="center"><img src="logo/LOGOTYPE-HORIZONTAL-BLUE.png"></p>
|
||||
|
||||
# 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)
|
||||
|
||||
A hand-crafted 2D game library in Go. Take a look into the [features](#features) to see what it can
|
||||
do.
|
||||
|
@ -8,8 +15,18 @@ do.
|
|||
go get github.com/faiface/pixel
|
||||
```
|
||||
|
||||
If you are using Modules (Go 1.11 or higher) and want a mutable copy of the source code:
|
||||
|
||||
```
|
||||
git clone https://github.com/faiface/pixel # clone outside of $GOPATH
|
||||
cd pixel
|
||||
go install ./...
|
||||
```
|
||||
|
||||
See [requirements](#requirements) for the list of libraries necessary for compilation.
|
||||
|
||||
All significant changes are documented in [CHANGELOG.md](CHANGELOG.md).
|
||||
|
||||
## Tutorial
|
||||
|
||||
The [Wiki of this repo](https://github.com/faiface/pixel/wiki) contains an extensive tutorial
|
||||
|
@ -46,9 +63,9 @@ Here are some screenshots from the examples!
|
|||
| --- | --- |
|
||||
|  |  |
|
||||
|
||||
| [Raycaster](https://github.com/faiface/pixel-examples/blob/master/community/raycaster) | [Starfield](https://github.com/faiface/pixel-examples/blob/master/community/starfield) |
|
||||
| [Raycaster](https://github.com/faiface/pixel-examples/blob/master/community/raycaster) | [Gizmo](https://github.com/Lallassu/gizmo) |
|
||||
| --- | --- |
|
||||
|  |  |
|
||||
|  |  |
|
||||
|
||||
## Features
|
||||
|
||||
|
@ -94,6 +111,14 @@ Here's the list of the main features in Pixel. Although Pixel is still under hea
|
|||
- Small codebase, ~5K lines of code, including the backend [glhf](https://github.com/faiface/glhf)
|
||||
package
|
||||
|
||||
## Related repositories
|
||||
|
||||
Here are some packages which use Pixel:
|
||||
- [TilePix](https://github.com/bcvery1/tilepix) Makes handling TMX files built with [Tiled](https://www.mapeditor.org/) trivially easy to work with using Pixel.
|
||||
- [spriteplus](https://github.com/cebarks/spriteplus) Basic `SpriteSheet` and `Animation` implementations
|
||||
- [PixelUI](https://github.com/dusk125/pixelui) Imgui-based GUIs for Pixel
|
||||
- [pixelutils](https://github.com/dusk125/pixelutils) Variety of game related utilities (sprite packer, id generator, ticker, sprite loader, voronoia diagrams)
|
||||
|
||||
## Missing features
|
||||
|
||||
Pixel is in development and still missing few critical features. Here're the most critical ones.
|
||||
|
@ -104,8 +129,9 @@ Pixel is in development and still missing few critical features. Here're the mos
|
|||
- ~~Advanced window manipulation (cursor hiding, window icon, ...)~~
|
||||
- Better support for Hi-DPI displays
|
||||
- Mobile (and perhaps HTML5?) backend
|
||||
- More advanced graphical effects (e.g. blur)
|
||||
- ~~More advanced graphical effects (e.g. blur)~~ (solved with the addition of GLSL effects)
|
||||
- Tests and benchmarks
|
||||
- Vulkan support
|
||||
|
||||
**Implementing these features will get us to the 1.0 release.** Contribute, so that it's as soon as
|
||||
possible!
|
||||
|
@ -126,7 +152,7 @@ The OpenGL version used is **OpenGL 3.3**.
|
|||
headers and libraries.
|
||||
- On Ubuntu/Debian-like Linux distributions, you need `libgl1-mesa-dev` and `xorg-dev` packages.
|
||||
- On CentOS/Fedora-like Linux distributions, you need `libX11-devel libXcursor-devel libXrandr-devel
|
||||
libXinerama-devel mesa-libGL-devel libXi-devel` packages.
|
||||
libXinerama-devel mesa-libGL-devel libXi-devel libXxf86vm-devel` packages.
|
||||
- See [here](http://www.glfw.org/docs/latest/compile.html#compile_deps) for full details.
|
||||
|
||||
**The combination of Go 1.8, macOS and latest XCode seems to be problematic** as mentioned in issue
|
||||
|
@ -135,6 +161,8 @@ The OpenGL version used is **OpenGL 3.3**.
|
|||
|
||||
## Contributing
|
||||
|
||||
Join us in the [Discord Chat!](https://discord.gg/q2DK4MP)
|
||||
|
||||
Pixel is in, let's say, mid-stage of development. Many of the important features are here, some are
|
||||
missing. That's why **contributions are very important and welcome!** All alone, I will be able to
|
||||
finish the library, but it'll take a lot of time. With your help, it'll take much less. I encourage
|
||||
|
@ -152,10 +180,6 @@ better result.
|
|||
|
||||
Take a look at [CONTRIBUTING.md](CONTRIBUTING.md) for further information.
|
||||
|
||||
For any kind of discussion, feel free to use our
|
||||
[Gitter](https://gitter.im/pixellib/Lobby)
|
||||
community.
|
||||
|
||||
## License
|
||||
|
||||
[MIT](LICENSE)
|
||||
|
|
2
batch.go
2
batch.go
|
@ -26,7 +26,7 @@ var _ BasicTarget = (*Batch)(nil)
|
|||
//
|
||||
// Note, that if the container does not support TrianglesColor, color masking will not work.
|
||||
func NewBatch(container Triangles, pic Picture) *Batch {
|
||||
b := &Batch{cont: Drawer{Triangles: container, Picture: pic}}
|
||||
b := &Batch{cont: Drawer{Triangles: container, Picture: pic, Cached: true}}
|
||||
b.SetMatrix(IM)
|
||||
b.SetColorMask(Alpha(1))
|
||||
return b
|
||||
|
|
|
@ -0,0 +1,334 @@
|
|||
package pixel
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
)
|
||||
|
||||
// Circle is a 2D circle. It is defined by two properties:
|
||||
// - Center vector
|
||||
// - Radius float64
|
||||
type Circle struct {
|
||||
Center Vec
|
||||
Radius float64
|
||||
}
|
||||
|
||||
// C returns a new Circle with the given radius and center coordinates.
|
||||
//
|
||||
// Note that a negative radius is valid.
|
||||
func C(center Vec, radius float64) Circle {
|
||||
return Circle{
|
||||
Center: center,
|
||||
Radius: radius,
|
||||
}
|
||||
}
|
||||
|
||||
// String returns the string representation of the Circle.
|
||||
//
|
||||
// c := pixel.C(10.1234, pixel.ZV)
|
||||
// c.String() // returns "Circle(10.12, Vec(0, 0))"
|
||||
// fmt.Println(c) // Circle(10.12, Vec(0, 0))
|
||||
func (c Circle) String() string {
|
||||
return fmt.Sprintf("Circle(%s, %.2f)", c.Center, c.Radius)
|
||||
}
|
||||
|
||||
// Norm returns the Circle in normalized form - this sets the radius to its absolute value.
|
||||
//
|
||||
// c := pixel.C(-10, pixel.ZV)
|
||||
// c.Norm() // returns pixel.Circle{pixel.Vec{0, 0}, 10}
|
||||
func (c Circle) Norm() Circle {
|
||||
return Circle{
|
||||
Center: c.Center,
|
||||
Radius: math.Abs(c.Radius),
|
||||
}
|
||||
}
|
||||
|
||||
// Area returns the area of the Circle.
|
||||
func (c Circle) Area() float64 {
|
||||
return math.Pi * math.Pow(c.Radius, 2)
|
||||
}
|
||||
|
||||
// Moved returns the Circle moved by the given vector delta.
|
||||
func (c Circle) Moved(delta Vec) Circle {
|
||||
return Circle{
|
||||
Center: c.Center.Add(delta),
|
||||
Radius: c.Radius,
|
||||
}
|
||||
}
|
||||
|
||||
// Resized returns the Circle resized by the given delta. The Circles center is use as the anchor.
|
||||
//
|
||||
// c := pixel.C(pixel.ZV, 10)
|
||||
// c.Resized(-5) // returns pixel.Circle{pixel.Vec{0, 0}, 5}
|
||||
// c.Resized(25) // returns pixel.Circle{pixel.Vec{0, 0}, 35}
|
||||
func (c Circle) Resized(radiusDelta float64) Circle {
|
||||
return Circle{
|
||||
Center: c.Center,
|
||||
Radius: c.Radius + radiusDelta,
|
||||
}
|
||||
}
|
||||
|
||||
// Contains checks whether a vector `u` is contained within this Circle (including it's perimeter).
|
||||
func (c Circle) Contains(u Vec) bool {
|
||||
toCenter := c.Center.To(u)
|
||||
return c.Radius >= toCenter.Len()
|
||||
}
|
||||
|
||||
// Formula returns the values of h and k, for the equation of the circle: (x-h)^2 + (y-k)^2 = r^2
|
||||
// where r is the radius of the circle.
|
||||
func (c Circle) Formula() (h, k float64) {
|
||||
return c.Center.X, c.Center.Y
|
||||
}
|
||||
|
||||
// maxCircle will return the larger circle based on the radius.
|
||||
func maxCircle(c, d Circle) Circle {
|
||||
if c.Radius < d.Radius {
|
||||
return d
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
// minCircle will return the smaller circle based on the radius.
|
||||
func minCircle(c, d Circle) Circle {
|
||||
if c.Radius < d.Radius {
|
||||
return c
|
||||
}
|
||||
return d
|
||||
}
|
||||
|
||||
// Union returns the minimal Circle which covers both `c` and `d`.
|
||||
func (c Circle) Union(d Circle) Circle {
|
||||
biggerC := maxCircle(c.Norm(), d.Norm())
|
||||
smallerC := minCircle(c.Norm(), d.Norm())
|
||||
|
||||
// Get distance between centers
|
||||
dist := c.Center.To(d.Center).Len()
|
||||
|
||||
// If the bigger Circle encompasses the smaller one, we have the result
|
||||
if dist+smallerC.Radius <= biggerC.Radius {
|
||||
return biggerC
|
||||
}
|
||||
|
||||
// Calculate radius for encompassing Circle
|
||||
r := (dist + biggerC.Radius + smallerC.Radius) / 2
|
||||
|
||||
// Calculate center for encompassing Circle
|
||||
theta := .5 + (biggerC.Radius-smallerC.Radius)/(2*dist)
|
||||
center := Lerp(smallerC.Center, biggerC.Center, theta)
|
||||
|
||||
return Circle{
|
||||
Center: center,
|
||||
Radius: r,
|
||||
}
|
||||
}
|
||||
|
||||
// Intersect returns the maximal Circle which is covered by both `c` and `d`.
|
||||
//
|
||||
// If `c` and `d` don't overlap, this function returns a zero-sized circle at the centerpoint between the two Circle's
|
||||
// centers.
|
||||
func (c Circle) Intersect(d Circle) Circle {
|
||||
// Check if one of the circles encompasses the other; if so, return that one
|
||||
biggerC := maxCircle(c.Norm(), d.Norm())
|
||||
smallerC := minCircle(c.Norm(), d.Norm())
|
||||
|
||||
if biggerC.Radius >= biggerC.Center.To(smallerC.Center).Len()+smallerC.Radius {
|
||||
return biggerC
|
||||
}
|
||||
|
||||
// Calculate the midpoint between the two radii
|
||||
// Distance between centers
|
||||
dist := c.Center.To(d.Center).Len()
|
||||
// Difference between radii
|
||||
diff := dist - (c.Radius + d.Radius)
|
||||
// Distance from c.Center to the weighted midpoint
|
||||
distToMidpoint := c.Radius + 0.5*diff
|
||||
// Weighted midpoint
|
||||
center := Lerp(c.Center, d.Center, distToMidpoint/dist)
|
||||
|
||||
// No need to calculate radius if the circles do not overlap
|
||||
if c.Center.To(d.Center).Len() >= c.Radius+d.Radius {
|
||||
return C(center, 0)
|
||||
}
|
||||
|
||||
radius := c.Center.To(d.Center).Len() - (c.Radius + d.Radius)
|
||||
|
||||
return Circle{
|
||||
Center: center,
|
||||
Radius: math.Abs(radius),
|
||||
}
|
||||
}
|
||||
|
||||
// IntersectLine will return the shortest Vec such that if the Circle is moved by the Vec returned, the Line and Rect no
|
||||
// longer intersect.
|
||||
func (c Circle) IntersectLine(l Line) Vec {
|
||||
return l.IntersectCircle(c).Scaled(-1)
|
||||
}
|
||||
|
||||
// IntersectRect returns a minimal required Vector, such that moving the circle by that vector would stop the Circle
|
||||
// and the Rect intersecting. This function returns a zero-vector if the Circle and Rect do not overlap, and if only
|
||||
// the perimeters touch.
|
||||
//
|
||||
// This function will return a non-zero vector if:
|
||||
// - The Rect contains the Circle, partially or fully
|
||||
// - The Circle contains the Rect, partially of fully
|
||||
func (c Circle) IntersectRect(r Rect) Vec {
|
||||
// Checks if the c.Center is not in the diagonal quadrants of the rectangle
|
||||
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) {
|
||||
// 'grow' the Rect by c.Radius in each orthagonal
|
||||
grown := Rect{Min: r.Min.Sub(V(c.Radius, c.Radius)), Max: r.Max.Add(V(c.Radius, c.Radius))}
|
||||
if !grown.Contains(c.Center) {
|
||||
// c.Center not close enough to overlap, return zero-vector
|
||||
return ZV
|
||||
}
|
||||
|
||||
// Get minimum distance to travel out of Rect
|
||||
rToC := r.Center().To(c.Center)
|
||||
h := c.Radius - math.Abs(rToC.X) + (r.W() / 2)
|
||||
v := c.Radius - math.Abs(rToC.Y) + (r.H() / 2)
|
||||
|
||||
if rToC.X < 0 {
|
||||
h = -h
|
||||
}
|
||||
if rToC.Y < 0 {
|
||||
v = -v
|
||||
}
|
||||
|
||||
// No intersect
|
||||
if h == 0 && v == 0 {
|
||||
return ZV
|
||||
}
|
||||
|
||||
if math.Abs(h) > math.Abs(v) {
|
||||
// Vertical distance shorter
|
||||
return V(0, v)
|
||||
}
|
||||
return V(h, 0)
|
||||
} else {
|
||||
// The center is in the diagonal quadrants
|
||||
|
||||
// Helper points to make code below easy to read.
|
||||
rectTopLeft := V(r.Min.X, r.Max.Y)
|
||||
rectBottomRight := V(r.Max.X, r.Min.Y)
|
||||
|
||||
// Check for overlap.
|
||||
if !(c.Contains(r.Min) || c.Contains(r.Max) || c.Contains(rectTopLeft) || c.Contains(rectBottomRight)) {
|
||||
// No overlap.
|
||||
return ZV
|
||||
}
|
||||
|
||||
var centerToCorner Vec
|
||||
if c.Center.To(r.Min).Len() <= c.Radius {
|
||||
// Closest to bottom-left
|
||||
centerToCorner = c.Center.To(r.Min)
|
||||
}
|
||||
if c.Center.To(r.Max).Len() <= c.Radius {
|
||||
// Closest to top-right
|
||||
centerToCorner = c.Center.To(r.Max)
|
||||
}
|
||||
if c.Center.To(rectTopLeft).Len() <= c.Radius {
|
||||
// Closest to top-left
|
||||
centerToCorner = c.Center.To(rectTopLeft)
|
||||
}
|
||||
if c.Center.To(rectBottomRight).Len() <= c.Radius {
|
||||
// Closest to bottom-right
|
||||
centerToCorner = c.Center.To(rectBottomRight)
|
||||
}
|
||||
|
||||
cornerToCircumferenceLen := c.Radius - centerToCorner.Len()
|
||||
|
||||
return centerToCorner.Unit().Scaled(cornerToCircumferenceLen)
|
||||
}
|
||||
}
|
||||
|
||||
// IntersectionPoints returns all the points where the Circle intersects with the line provided. This can be zero, one or
|
||||
// two points, depending on the location of the shapes. The points of intersection will be returned in order of
|
||||
// closest-to-l.A to closest-to-l.B.
|
||||
func (c Circle) IntersectionPoints(l Line) []Vec {
|
||||
cContainsA := c.Contains(l.A)
|
||||
cContainsB := c.Contains(l.B)
|
||||
|
||||
// Special case for both endpoint being contained within the circle
|
||||
if cContainsA && cContainsB {
|
||||
return []Vec{}
|
||||
}
|
||||
|
||||
// Get closest point on the line to this circles' center
|
||||
closestToCenter := l.Closest(c.Center)
|
||||
|
||||
// If the distance to the closest point is greater than the radius, there are no points of intersection
|
||||
if closestToCenter.To(c.Center).Len() > c.Radius {
|
||||
return []Vec{}
|
||||
}
|
||||
|
||||
// If the distance to the closest point is equal to the radius, the line is tangent and the closest point is the
|
||||
// point at which it touches the circle.
|
||||
if closestToCenter.To(c.Center).Len() == c.Radius {
|
||||
return []Vec{closestToCenter}
|
||||
}
|
||||
|
||||
// Special case for endpoint being on the circles' center
|
||||
if c.Center == l.A || c.Center == l.B {
|
||||
otherEnd := l.B
|
||||
if c.Center == l.B {
|
||||
otherEnd = l.A
|
||||
}
|
||||
intersect := c.Center.Add(c.Center.To(otherEnd).Unit().Scaled(c.Radius))
|
||||
return []Vec{intersect}
|
||||
}
|
||||
|
||||
// This means the distance to the closest point is less than the radius, so there is at least one intersection,
|
||||
// possibly two.
|
||||
|
||||
// If one of the end points exists within the circle, there is only one intersection
|
||||
if cContainsA || cContainsB {
|
||||
containedPoint := l.A
|
||||
otherEnd := l.B
|
||||
if cContainsB {
|
||||
containedPoint = l.B
|
||||
otherEnd = l.A
|
||||
}
|
||||
|
||||
// Use trigonometry to get the length of the line between the contained point and the intersection point.
|
||||
// The following is used to describe the triangle formed:
|
||||
// - a is the side between contained point and circle center
|
||||
// - b is the side between the center and the intersection point (radius)
|
||||
// - c is the side between the contained point and the intersection point
|
||||
// The captials of these letters are used as the angles opposite the respective sides.
|
||||
// a and b are known
|
||||
a := containedPoint.To(c.Center).Len()
|
||||
b := c.Radius
|
||||
// B can be calculated by subtracting the angle of b (to the x-axis) from the angle of c (to the x-axis)
|
||||
B := containedPoint.To(c.Center).Angle() - containedPoint.To(otherEnd).Angle()
|
||||
// Using the Sin rule we can get A
|
||||
A := math.Asin((a * math.Sin(B)) / b)
|
||||
// Using the rule that there are 180 degrees (or Pi radians) in a triangle, we can now get C
|
||||
C := math.Pi - A + B
|
||||
// If C is zero, the line segment is in-line with the center-intersect line.
|
||||
var c float64
|
||||
if C == 0 {
|
||||
c = b - a
|
||||
} else {
|
||||
// Using the Sine rule again, we can now get c
|
||||
c = (a * math.Sin(C)) / math.Sin(A)
|
||||
}
|
||||
// Travelling from the contained point to the other end by length of a will provide the intersection point.
|
||||
return []Vec{
|
||||
containedPoint.Add(containedPoint.To(otherEnd).Unit().Scaled(c)),
|
||||
}
|
||||
}
|
||||
|
||||
// Otherwise the endpoints exist outside of the circle, and the line segment intersects in two locations.
|
||||
// The vector formed by going from the closest point to the center of the circle will be perpendicular to the line;
|
||||
// this forms a right-angled triangle with the intersection points, with the radius as the hypotenuse.
|
||||
// Calculate the other triangles' sides' length.
|
||||
a := math.Sqrt(math.Pow(c.Radius, 2) - math.Pow(closestToCenter.To(c.Center).Len(), 2))
|
||||
|
||||
// Travelling in both directions from the closest point by length of a will provide the two intersection points.
|
||||
first := closestToCenter.Add(closestToCenter.To(l.A).Unit().Scaled(a))
|
||||
second := closestToCenter.Add(closestToCenter.To(l.B).Unit().Scaled(a))
|
||||
|
||||
if first.To(l.A).Len() < second.To(l.A).Len() {
|
||||
return []Vec{first, second}
|
||||
}
|
||||
return []Vec{second, first}
|
||||
}
|
|
@ -0,0 +1,466 @@
|
|||
package pixel_test
|
||||
|
||||
import (
|
||||
"math"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/faiface/pixel"
|
||||
)
|
||||
|
||||
func TestC(t *testing.T) {
|
||||
type args struct {
|
||||
radius float64
|
||||
center pixel.Vec
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want pixel.Circle
|
||||
}{
|
||||
{
|
||||
name: "C(): positive radius",
|
||||
args: args{radius: 10, center: pixel.ZV},
|
||||
want: pixel.Circle{Radius: 10, Center: pixel.ZV},
|
||||
},
|
||||
{
|
||||
name: "C(): zero radius",
|
||||
args: args{radius: 0, center: pixel.ZV},
|
||||
want: pixel.Circle{Radius: 0, Center: pixel.ZV},
|
||||
},
|
||||
{
|
||||
name: "C(): negative radius",
|
||||
args: args{radius: -5, center: pixel.ZV},
|
||||
want: pixel.Circle{Radius: -5, Center: pixel.ZV},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := pixel.C(tt.args.center, tt.args.radius); !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("C() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCircle_String(t *testing.T) {
|
||||
type fields struct {
|
||||
radius float64
|
||||
center pixel.Vec
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "Circle.String(): positive radius",
|
||||
fields: fields{radius: 10, center: pixel.ZV},
|
||||
want: "Circle(Vec(0, 0), 10.00)",
|
||||
},
|
||||
{
|
||||
name: "Circle.String(): zero radius",
|
||||
fields: fields{radius: 0, center: pixel.ZV},
|
||||
want: "Circle(Vec(0, 0), 0.00)",
|
||||
},
|
||||
{
|
||||
name: "Circle.String(): negative radius",
|
||||
fields: fields{radius: -5, center: pixel.ZV},
|
||||
want: "Circle(Vec(0, 0), -5.00)",
|
||||
},
|
||||
{
|
||||
name: "Circle.String(): irrational radius",
|
||||
fields: fields{radius: math.Pi, center: pixel.ZV},
|
||||
want: "Circle(Vec(0, 0), 3.14)",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
c := pixel.C(tt.fields.center, tt.fields.radius)
|
||||
if got := c.String(); got != tt.want {
|
||||
t.Errorf("Circle.String() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCircle_Norm(t *testing.T) {
|
||||
type fields struct {
|
||||
radius float64
|
||||
center pixel.Vec
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
want pixel.Circle
|
||||
}{
|
||||
{
|
||||
name: "Circle.Norm(): positive radius",
|
||||
fields: fields{radius: 10, center: pixel.ZV},
|
||||
want: pixel.C(pixel.ZV, 10),
|
||||
},
|
||||
{
|
||||
name: "Circle.Norm(): zero radius",
|
||||
fields: fields{radius: 0, center: pixel.ZV},
|
||||
want: pixel.C(pixel.ZV, 0),
|
||||
},
|
||||
{
|
||||
name: "Circle.Norm(): negative radius",
|
||||
fields: fields{radius: -5, center: pixel.ZV},
|
||||
want: pixel.C(pixel.ZV, 5),
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
c := pixel.C(tt.fields.center, tt.fields.radius)
|
||||
if got := c.Norm(); !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("Circle.Norm() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCircle_Area(t *testing.T) {
|
||||
type fields struct {
|
||||
radius float64
|
||||
center pixel.Vec
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
want float64
|
||||
}{
|
||||
{
|
||||
name: "Circle.Area(): positive radius",
|
||||
fields: fields{radius: 10, center: pixel.ZV},
|
||||
want: 100 * math.Pi,
|
||||
},
|
||||
{
|
||||
name: "Circle.Area(): zero radius",
|
||||
fields: fields{radius: 0, center: pixel.ZV},
|
||||
want: 0,
|
||||
},
|
||||
{
|
||||
name: "Circle.Area(): negative radius",
|
||||
fields: fields{radius: -5, center: pixel.ZV},
|
||||
want: 25 * math.Pi,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
c := pixel.C(tt.fields.center, tt.fields.radius)
|
||||
if got := c.Area(); got != tt.want {
|
||||
t.Errorf("Circle.Area() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCircle_Moved(t *testing.T) {
|
||||
type fields struct {
|
||||
radius float64
|
||||
center pixel.Vec
|
||||
}
|
||||
type args struct {
|
||||
delta pixel.Vec
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
want pixel.Circle
|
||||
}{
|
||||
{
|
||||
name: "Circle.Moved(): positive movement",
|
||||
fields: fields{radius: 10, center: pixel.ZV},
|
||||
args: args{delta: pixel.V(10, 20)},
|
||||
want: pixel.C(pixel.V(10, 20), 10),
|
||||
},
|
||||
{
|
||||
name: "Circle.Moved(): zero movement",
|
||||
fields: fields{radius: 10, center: pixel.ZV},
|
||||
args: args{delta: pixel.ZV},
|
||||
want: pixel.C(pixel.V(0, 0), 10),
|
||||
},
|
||||
{
|
||||
name: "Circle.Moved(): negative movement",
|
||||
fields: fields{radius: 10, center: pixel.ZV},
|
||||
args: args{delta: pixel.V(-5, -10)},
|
||||
want: pixel.C(pixel.V(-5, -10), 10),
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
c := pixel.C(tt.fields.center, tt.fields.radius)
|
||||
if got := c.Moved(tt.args.delta); !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("Circle.Moved() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCircle_Resized(t *testing.T) {
|
||||
type fields struct {
|
||||
radius float64
|
||||
center pixel.Vec
|
||||
}
|
||||
type args struct {
|
||||
radiusDelta float64
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
want pixel.Circle
|
||||
}{
|
||||
{
|
||||
name: "Circle.Resized(): positive delta",
|
||||
fields: fields{radius: 10, center: pixel.ZV},
|
||||
args: args{radiusDelta: 5},
|
||||
want: pixel.C(pixel.V(0, 0), 15),
|
||||
},
|
||||
{
|
||||
name: "Circle.Resized(): zero delta",
|
||||
fields: fields{radius: 10, center: pixel.ZV},
|
||||
args: args{radiusDelta: 0},
|
||||
want: pixel.C(pixel.V(0, 0), 10),
|
||||
},
|
||||
{
|
||||
name: "Circle.Resized(): negative delta",
|
||||
fields: fields{radius: 10, center: pixel.ZV},
|
||||
args: args{radiusDelta: -5},
|
||||
want: pixel.C(pixel.V(0, 0), 5),
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
c := pixel.C(tt.fields.center, tt.fields.radius)
|
||||
if got := c.Resized(tt.args.radiusDelta); !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("Circle.Resized() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCircle_Contains(t *testing.T) {
|
||||
type fields struct {
|
||||
radius float64
|
||||
center pixel.Vec
|
||||
}
|
||||
type args struct {
|
||||
u pixel.Vec
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "Circle.Contains(): point on cicles' center",
|
||||
fields: fields{radius: 10, center: pixel.ZV},
|
||||
args: args{u: pixel.ZV},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "Circle.Contains(): point offcenter",
|
||||
fields: fields{radius: 10, center: pixel.V(5, 0)},
|
||||
args: args{u: pixel.ZV},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "Circle.Contains(): point on circumference",
|
||||
fields: fields{radius: 10, center: pixel.V(10, 0)},
|
||||
args: args{u: pixel.ZV},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "Circle.Contains(): point outside circle",
|
||||
fields: fields{radius: 10, center: pixel.V(15, 0)},
|
||||
args: args{u: pixel.ZV},
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
c := pixel.C(tt.fields.center, tt.fields.radius)
|
||||
if got := c.Contains(tt.args.u); got != tt.want {
|
||||
t.Errorf("Circle.Contains() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCircle_Union(t *testing.T) {
|
||||
type fields struct {
|
||||
radius float64
|
||||
center pixel.Vec
|
||||
}
|
||||
type args struct {
|
||||
d pixel.Circle
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
want pixel.Circle
|
||||
}{
|
||||
{
|
||||
name: "Circle.Union(): overlapping circles",
|
||||
fields: fields{radius: 5, center: pixel.ZV},
|
||||
args: args{d: pixel.C(pixel.ZV, 5)},
|
||||
want: pixel.C(pixel.ZV, 5),
|
||||
},
|
||||
{
|
||||
name: "Circle.Union(): separate circles",
|
||||
fields: fields{radius: 1, center: pixel.ZV},
|
||||
args: args{d: pixel.C(pixel.V(0, 2), 1)},
|
||||
want: pixel.C(pixel.V(0, 1), 2),
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
c := pixel.C(tt.fields.center, tt.fields.radius)
|
||||
if got := c.Union(tt.args.d); !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("Circle.Union() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCircle_Intersect(t *testing.T) {
|
||||
type fields struct {
|
||||
radius float64
|
||||
center pixel.Vec
|
||||
}
|
||||
type args struct {
|
||||
d pixel.Circle
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
want pixel.Circle
|
||||
}{
|
||||
{
|
||||
name: "Circle.Intersect(): intersecting circles",
|
||||
fields: fields{radius: 1, center: pixel.ZV},
|
||||
args: args{d: pixel.C(pixel.V(1, 0), 1)},
|
||||
want: pixel.C(pixel.V(0.5, 0), 1),
|
||||
},
|
||||
{
|
||||
name: "Circle.Intersect(): non-intersecting circles",
|
||||
fields: fields{radius: 1, center: pixel.ZV},
|
||||
args: args{d: pixel.C(pixel.V(3, 3), 1)},
|
||||
want: pixel.C(pixel.V(1.5, 1.5), 0),
|
||||
},
|
||||
{
|
||||
name: "Circle.Intersect(): first circle encompassing second",
|
||||
fields: fields{radius: 10, center: pixel.ZV},
|
||||
args: args{d: pixel.C(pixel.V(3, 3), 1)},
|
||||
want: pixel.C(pixel.ZV, 10),
|
||||
},
|
||||
{
|
||||
name: "Circle.Intersect(): second circle encompassing first",
|
||||
fields: fields{radius: 1, center: pixel.V(-1, -4)},
|
||||
args: args{d: pixel.C(pixel.ZV, 10)},
|
||||
want: pixel.C(pixel.ZV, 10),
|
||||
},
|
||||
{
|
||||
name: "Circle.Intersect(): matching circles",
|
||||
fields: fields{radius: 1, center: pixel.ZV},
|
||||
args: args{d: pixel.C(pixel.ZV, 1)},
|
||||
want: pixel.C(pixel.ZV, 1),
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
c := pixel.C(
|
||||
tt.fields.center,
|
||||
tt.fields.radius,
|
||||
)
|
||||
if got := c.Intersect(tt.args.d); !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("Circle.Intersect() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCircle_IntersectPoints(t *testing.T) {
|
||||
type fields struct {
|
||||
Center pixel.Vec
|
||||
Radius float64
|
||||
}
|
||||
type args struct {
|
||||
l pixel.Line
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
want []pixel.Vec
|
||||
}{
|
||||
{
|
||||
name: "Line intersects circle at two points",
|
||||
fields: fields{Center: pixel.V(2, 2), Radius: 1},
|
||||
args: args{pixel.L(pixel.V(0, 0), pixel.V(10, 10))},
|
||||
want: []pixel.Vec{pixel.V(1.292, 1.292), pixel.V(2.707, 2.707)},
|
||||
},
|
||||
{
|
||||
name: "Line intersects circle at one point",
|
||||
fields: fields{Center: pixel.V(-0.5, -0.5), Radius: 1},
|
||||
args: args{pixel.L(pixel.V(0, 0), pixel.V(10, 10))},
|
||||
want: []pixel.Vec{pixel.V(0.207, 0.207)},
|
||||
},
|
||||
{
|
||||
name: "Line endpoint is circle center",
|
||||
fields: fields{Center: pixel.V(0, 0), Radius: 1},
|
||||
args: args{pixel.L(pixel.V(0, 0), pixel.V(10, 10))},
|
||||
want: []pixel.Vec{pixel.V(0.707, 0.707)},
|
||||
},
|
||||
{
|
||||
name: "Both line endpoints within circle",
|
||||
fields: fields{Center: pixel.V(0, 0), Radius: 1},
|
||||
args: args{pixel.L(pixel.V(0.2, 0.2), pixel.V(0.5, 0.5))},
|
||||
want: []pixel.Vec{},
|
||||
},
|
||||
{
|
||||
name: "Line does not intersect circle",
|
||||
fields: fields{Center: pixel.V(10, 0), Radius: 1},
|
||||
args: args{pixel.L(pixel.V(0, 0), pixel.V(10, 10))},
|
||||
want: []pixel.Vec{},
|
||||
},
|
||||
{
|
||||
name: "Horizontal line intersects circle at two points",
|
||||
fields: fields{Center: pixel.V(5, 5), Radius: 1},
|
||||
args: args{pixel.L(pixel.V(0, 5), pixel.V(10, 5))},
|
||||
want: []pixel.Vec{pixel.V(4, 5), pixel.V(6, 5)},
|
||||
},
|
||||
{
|
||||
name: "Vertical line intersects circle at two points",
|
||||
fields: fields{Center: pixel.V(5, 5), Radius: 1},
|
||||
args: args{pixel.L(pixel.V(5, 0), pixel.V(5, 10))},
|
||||
want: []pixel.Vec{pixel.V(5, 4), pixel.V(5, 6)},
|
||||
},
|
||||
{
|
||||
name: "Left and down line intersects circle at two points",
|
||||
fields: fields{Center: pixel.V(5, 5), Radius: 1},
|
||||
args: args{pixel.L(pixel.V(10, 10), pixel.V(0, 0))},
|
||||
want: []pixel.Vec{pixel.V(5.707, 5.707), pixel.V(4.292, 4.292)},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
c := pixel.Circle{
|
||||
Center: tt.fields.Center,
|
||||
Radius: tt.fields.Radius,
|
||||
}
|
||||
got := c.IntersectionPoints(tt.args.l)
|
||||
for i, v := range got {
|
||||
if !closeEnough(v.X, tt.want[i].X, 2) || !closeEnough(v.Y, tt.want[i].Y, 2) {
|
||||
t.Errorf("Circle.IntersectPoints() = %v, want %v", v, tt.want[i])
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
42
data.go
42
data.go
|
@ -8,6 +8,16 @@ import (
|
|||
"math"
|
||||
)
|
||||
|
||||
// zeroValueTriangleData is the default value of a TriangleData element
|
||||
var zeroValueTriangleData = struct {
|
||||
Position Vec
|
||||
Color RGBA
|
||||
Picture Vec
|
||||
Intensity float64
|
||||
ClipRect Rect
|
||||
IsClipped bool
|
||||
}{Color: RGBA{1, 1, 1, 1}}
|
||||
|
||||
// TrianglesData specifies a list of Triangles vertices with three common properties:
|
||||
// TrianglesPosition, TrianglesColor and TrianglesPicture.
|
||||
type TrianglesData []struct {
|
||||
|
@ -15,6 +25,8 @@ type TrianglesData []struct {
|
|||
Color RGBA
|
||||
Picture Vec
|
||||
Intensity float64
|
||||
ClipRect Rect
|
||||
IsClipped bool
|
||||
}
|
||||
|
||||
// MakeTrianglesData creates TrianglesData of length len initialized with default property values.
|
||||
|
@ -22,9 +34,11 @@ type TrianglesData []struct {
|
|||
// Prefer this function to make(TrianglesData, len), because make zeros them, while this function
|
||||
// does the correct intialization.
|
||||
func MakeTrianglesData(len int) *TrianglesData {
|
||||
td := &TrianglesData{}
|
||||
td.SetLen(len)
|
||||
return td
|
||||
td := make(TrianglesData, len)
|
||||
for i := 0; i < len; i++ {
|
||||
td[i] = zeroValueTriangleData
|
||||
}
|
||||
return &td
|
||||
}
|
||||
|
||||
// Len returns the number of vertices in TrianglesData.
|
||||
|
@ -40,12 +54,7 @@ func (td *TrianglesData) SetLen(len int) {
|
|||
if len > td.Len() {
|
||||
needAppend := len - td.Len()
|
||||
for i := 0; i < needAppend; i++ {
|
||||
*td = append(*td, struct {
|
||||
Position Vec
|
||||
Color RGBA
|
||||
Picture Vec
|
||||
Intensity float64
|
||||
}{Color: RGBA{1, 1, 1, 1}})
|
||||
*td = append(*td, zeroValueTriangleData)
|
||||
}
|
||||
}
|
||||
if len < td.Len() {
|
||||
|
@ -82,6 +91,11 @@ func (td *TrianglesData) updateData(t Triangles) {
|
|||
(*td)[i].Picture, (*td)[i].Intensity = t.Picture(i)
|
||||
}
|
||||
}
|
||||
if t, ok := t.(TrianglesClipped); ok {
|
||||
for i := range *td {
|
||||
(*td)[i].ClipRect, (*td)[i].IsClipped = t.ClipRect(i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update copies vertex properties from the supplied Triangles into this TrianglesData.
|
||||
|
@ -96,10 +110,9 @@ func (td *TrianglesData) Update(t Triangles) {
|
|||
|
||||
// Copy returns an exact independent copy of this TrianglesData.
|
||||
func (td *TrianglesData) Copy() Triangles {
|
||||
copyTd := TrianglesData{}
|
||||
copyTd.SetLen(td.Len())
|
||||
copyTd := MakeTrianglesData(td.Len())
|
||||
copyTd.Update(td)
|
||||
return ©Td
|
||||
return copyTd
|
||||
}
|
||||
|
||||
// Position returns the position property of i-th vertex.
|
||||
|
@ -117,6 +130,11 @@ func (td *TrianglesData) Picture(i int) (pic Vec, intensity float64) {
|
|||
return (*td)[i].Picture, (*td)[i].Intensity
|
||||
}
|
||||
|
||||
// ClipRect returns the clipping rectangle property of the i-th vertex.
|
||||
func (td *TrianglesData) ClipRect(i int) (rect Rect, has bool) {
|
||||
return (*td)[i].ClipRect, (*td)[i].IsClipped
|
||||
}
|
||||
|
||||
// PictureData specifies an in-memory rectangular area of pixels and implements Picture and
|
||||
// PictureColor.
|
||||
//
|
||||
|
|
|
@ -0,0 +1,300 @@
|
|||
package pixel_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/faiface/pixel"
|
||||
)
|
||||
|
||||
func BenchmarkMakeTrianglesData(b *testing.B) {
|
||||
tests := []struct {
|
||||
name string
|
||||
len int
|
||||
}{
|
||||
{
|
||||
name: "Small slice",
|
||||
len: 10,
|
||||
},
|
||||
{
|
||||
name: "Large slice",
|
||||
len: 10000,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
b.Run(tt.name, func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = pixel.MakeTrianglesData(tt.len)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkTrianglesData_Len(b *testing.B) {
|
||||
tests := []struct {
|
||||
name string
|
||||
tData *pixel.TrianglesData
|
||||
}{
|
||||
{
|
||||
name: "Small slice",
|
||||
tData: pixel.MakeTrianglesData(10),
|
||||
},
|
||||
{
|
||||
name: "Large slice",
|
||||
tData: pixel.MakeTrianglesData(10000),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
b.Run(tt.name, func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = tt.tData.Len()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkTrianglesData_SetLen(b *testing.B) {
|
||||
tests := []struct {
|
||||
name string
|
||||
tData *pixel.TrianglesData
|
||||
nextLenFunc func(int, int) (int, int)
|
||||
}{
|
||||
{
|
||||
name: "Stay same size",
|
||||
tData: pixel.MakeTrianglesData(50),
|
||||
nextLenFunc: func(i, j int) (int, int) { return 50, 0 },
|
||||
},
|
||||
{
|
||||
name: "Change size",
|
||||
tData: pixel.MakeTrianglesData(50),
|
||||
nextLenFunc: func(i, j int) (int, int) {
|
||||
// 0 is shrink
|
||||
if j == 0 {
|
||||
next := i - 1
|
||||
if next < 1 {
|
||||
return 2, 1
|
||||
}
|
||||
return next, 0
|
||||
}
|
||||
|
||||
// other than 0 is grow
|
||||
next := i + 1
|
||||
if next == 100 {
|
||||
return next, 0
|
||||
}
|
||||
return next, 1
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
b.Run(tt.name, func(b *testing.B) {
|
||||
var newLen int
|
||||
var c int
|
||||
for i := 0; i < b.N; i++ {
|
||||
newLen, c = tt.nextLenFunc(newLen, c)
|
||||
tt.tData.SetLen(newLen)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkTrianglesData_Slice(b *testing.B) {
|
||||
tests := []struct {
|
||||
name string
|
||||
tData *pixel.TrianglesData
|
||||
}{
|
||||
{
|
||||
name: "Basic slice",
|
||||
tData: pixel.MakeTrianglesData(100),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
b.Run(tt.name, func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = tt.tData.Slice(25, 50)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkTrianglesData_Update(b *testing.B) {
|
||||
tests := []struct {
|
||||
name string
|
||||
tData *pixel.TrianglesData
|
||||
t pixel.Triangles
|
||||
}{
|
||||
{
|
||||
name: "Small Triangles",
|
||||
tData: pixel.MakeTrianglesData(20),
|
||||
t: pixel.MakeTrianglesData(20),
|
||||
},
|
||||
{
|
||||
name: "Large Triangles",
|
||||
tData: pixel.MakeTrianglesData(10000),
|
||||
t: pixel.MakeTrianglesData(10000),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
b.Run(tt.name, func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
tt.tData.Update(tt.t)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkTrianglesData_Copy(b *testing.B) {
|
||||
tests := []struct {
|
||||
name string
|
||||
tData *pixel.TrianglesData
|
||||
}{
|
||||
{
|
||||
name: "Small copy",
|
||||
tData: pixel.MakeTrianglesData(20),
|
||||
},
|
||||
{
|
||||
name: "Large copy",
|
||||
tData: pixel.MakeTrianglesData(10000),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
b.Run(tt.name, func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = tt.tData.Copy()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkTrianglesData_Position(b *testing.B) {
|
||||
tests := []struct {
|
||||
name string
|
||||
tData *pixel.TrianglesData
|
||||
position int
|
||||
}{
|
||||
{
|
||||
name: "Getting beginning position",
|
||||
tData: pixel.MakeTrianglesData(1000),
|
||||
position: 2,
|
||||
},
|
||||
{
|
||||
name: "Getting middle position",
|
||||
tData: pixel.MakeTrianglesData(1000),
|
||||
position: 500,
|
||||
},
|
||||
{
|
||||
name: "Getting end position",
|
||||
tData: pixel.MakeTrianglesData(1000),
|
||||
position: 999,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
b.Run(tt.name, func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = tt.tData.Position(tt.position)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkTrianglesData_Color(b *testing.B) {
|
||||
tests := []struct {
|
||||
name string
|
||||
tData *pixel.TrianglesData
|
||||
position int
|
||||
}{
|
||||
{
|
||||
name: "Getting beginning position",
|
||||
tData: pixel.MakeTrianglesData(1000),
|
||||
position: 2,
|
||||
},
|
||||
{
|
||||
name: "Getting middle position",
|
||||
tData: pixel.MakeTrianglesData(1000),
|
||||
position: 500,
|
||||
},
|
||||
{
|
||||
name: "Getting end position",
|
||||
tData: pixel.MakeTrianglesData(1000),
|
||||
position: 999,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
b.Run(tt.name, func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = tt.tData.Color(tt.position)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkTrianglesData_Picture(b *testing.B) {
|
||||
tests := []struct {
|
||||
name string
|
||||
tData *pixel.TrianglesData
|
||||
position int
|
||||
}{
|
||||
{
|
||||
name: "Getting beginning position",
|
||||
tData: pixel.MakeTrianglesData(1000),
|
||||
position: 2,
|
||||
},
|
||||
{
|
||||
name: "Getting middle position",
|
||||
tData: pixel.MakeTrianglesData(1000),
|
||||
position: 500,
|
||||
},
|
||||
{
|
||||
name: "Getting end position",
|
||||
tData: pixel.MakeTrianglesData(1000),
|
||||
position: 999,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
b.Run(tt.name, func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, _ = tt.tData.Picture(tt.position)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkTrianglesData_ClipRect(b *testing.B) {
|
||||
tests := []struct {
|
||||
name string
|
||||
tData *pixel.TrianglesData
|
||||
position int
|
||||
}{
|
||||
{
|
||||
name: "Getting beginning position",
|
||||
tData: pixel.MakeTrianglesData(1000),
|
||||
position: 2,
|
||||
},
|
||||
{
|
||||
name: "Getting middle position",
|
||||
tData: pixel.MakeTrianglesData(1000),
|
||||
position: 500,
|
||||
},
|
||||
{
|
||||
name: "Getting end position",
|
||||
tData: pixel.MakeTrianglesData(1000),
|
||||
position: 999,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
b.Run(tt.name, func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, _ = tt.tData.ClipRect(tt.position)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -23,8 +23,10 @@ package pixel
|
|||
type Drawer struct {
|
||||
Triangles Triangles
|
||||
Picture Picture
|
||||
Cached bool
|
||||
|
||||
targets map[Target]*drawerTarget
|
||||
allTargets []*drawerTarget
|
||||
inited bool
|
||||
}
|
||||
|
||||
|
@ -46,7 +48,7 @@ func (d *Drawer) lazyInit() {
|
|||
func (d *Drawer) Dirty() {
|
||||
d.lazyInit()
|
||||
|
||||
for _, t := range d.targets {
|
||||
for _, t := range d.allTargets {
|
||||
t.clean = false
|
||||
}
|
||||
}
|
||||
|
@ -68,6 +70,7 @@ func (d *Drawer) Draw(t Target) {
|
|||
pics: make(map[Picture]TargetPicture),
|
||||
}
|
||||
d.targets[t] = dt
|
||||
d.allTargets = append(d.allTargets, dt)
|
||||
}
|
||||
|
||||
if dt.tris == nil {
|
||||
|
@ -89,8 +92,11 @@ func (d *Drawer) Draw(t Target) {
|
|||
pic := dt.pics[d.Picture]
|
||||
if pic == nil {
|
||||
pic = t.MakePicture(d.Picture)
|
||||
|
||||
if d.Cached {
|
||||
dt.pics[d.Picture] = pic
|
||||
}
|
||||
}
|
||||
|
||||
pic.Draw(dt.tris)
|
||||
}
|
||||
|
|
405
geometry.go
405
geometry.go
|
@ -1,405 +0,0 @@
|
|||
package pixel
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
)
|
||||
|
||||
// Clamp returns x clamped to the interval [min, max].
|
||||
//
|
||||
// If x is less than min, min is returned. If x is more than max, max is returned. Otherwise, x is
|
||||
// returned.
|
||||
func Clamp(x, min, max float64) float64 {
|
||||
if x < min {
|
||||
return min
|
||||
}
|
||||
if x > max {
|
||||
return max
|
||||
}
|
||||
return x
|
||||
}
|
||||
|
||||
// Vec is a 2D vector type with X and Y coordinates.
|
||||
//
|
||||
// Create vectors with the V constructor:
|
||||
//
|
||||
// u := pixel.V(1, 2)
|
||||
// v := pixel.V(8, -3)
|
||||
//
|
||||
// Use various methods to manipulate them:
|
||||
//
|
||||
// w := u.Add(v)
|
||||
// fmt.Println(w) // Vec(9, -1)
|
||||
// fmt.Println(u.Sub(v)) // Vec(-7, 5)
|
||||
// u = pixel.V(2, 3)
|
||||
// v = pixel.V(8, 1)
|
||||
// if u.X < 0 {
|
||||
// fmt.Println("this won't happen")
|
||||
// }
|
||||
// x := u.Unit().Dot(v.Unit())
|
||||
type Vec struct {
|
||||
X, Y float64
|
||||
}
|
||||
|
||||
// ZV is a zero vector.
|
||||
var ZV = Vec{0, 0}
|
||||
|
||||
// V returns a new 2D vector with the given coordinates.
|
||||
func V(x, y float64) Vec {
|
||||
return Vec{x, y}
|
||||
}
|
||||
|
||||
// Unit returns a vector of length 1 facing the given angle.
|
||||
func Unit(angle float64) Vec {
|
||||
return Vec{1, 0}.Rotated(angle)
|
||||
}
|
||||
|
||||
// String returns the string representation of the vector u.
|
||||
//
|
||||
// u := pixel.V(4.5, -1.3)
|
||||
// u.String() // returns "Vec(4.5, -1.3)"
|
||||
// fmt.Println(u) // Vec(4.5, -1.3)
|
||||
func (u Vec) String() string {
|
||||
return fmt.Sprintf("Vec(%v, %v)", u.X, u.Y)
|
||||
}
|
||||
|
||||
// XY returns the components of the vector in two return values.
|
||||
func (u Vec) XY() (x, y float64) {
|
||||
return u.X, u.Y
|
||||
}
|
||||
|
||||
// Add returns the sum of vectors u and v.
|
||||
func (u Vec) Add(v Vec) Vec {
|
||||
return Vec{
|
||||
u.X + v.X,
|
||||
u.Y + v.Y,
|
||||
}
|
||||
}
|
||||
|
||||
// Sub returns the difference betweeen vectors u and v.
|
||||
func (u Vec) Sub(v Vec) Vec {
|
||||
return Vec{
|
||||
u.X - v.X,
|
||||
u.Y - v.Y,
|
||||
}
|
||||
}
|
||||
|
||||
// To returns the vector from u to v. Equivalent to v.Sub(u).
|
||||
func (u Vec) To(v Vec) Vec {
|
||||
return Vec{
|
||||
v.X - u.X,
|
||||
v.Y - u.Y,
|
||||
}
|
||||
}
|
||||
|
||||
// Scaled returns the vector u multiplied by c.
|
||||
func (u Vec) Scaled(c float64) Vec {
|
||||
return Vec{u.X * c, u.Y * c}
|
||||
}
|
||||
|
||||
// ScaledXY returns the vector u multiplied by the vector v component-wise.
|
||||
func (u Vec) ScaledXY(v Vec) Vec {
|
||||
return Vec{u.X * v.X, u.Y * v.Y}
|
||||
}
|
||||
|
||||
// Len returns the length of the vector u.
|
||||
func (u Vec) Len() float64 {
|
||||
return math.Hypot(u.X, u.Y)
|
||||
}
|
||||
|
||||
// Angle returns the angle between the vector u and the x-axis. The result is in range [-Pi, Pi].
|
||||
func (u Vec) Angle() float64 {
|
||||
return math.Atan2(u.Y, u.X)
|
||||
}
|
||||
|
||||
// Unit returns a vector of length 1 facing the direction of u (has the same angle).
|
||||
func (u Vec) Unit() Vec {
|
||||
if u.X == 0 && u.Y == 0 {
|
||||
return Vec{1, 0}
|
||||
}
|
||||
return u.Scaled(1 / u.Len())
|
||||
}
|
||||
|
||||
// Rotated returns the vector u rotated by the given angle in radians.
|
||||
func (u Vec) Rotated(angle float64) Vec {
|
||||
sin, cos := math.Sincos(angle)
|
||||
return Vec{
|
||||
u.X*cos - u.Y*sin,
|
||||
u.X*sin + u.Y*cos,
|
||||
}
|
||||
}
|
||||
|
||||
// Normal returns a vector normal to u. Equivalent to u.Rotated(math.Pi / 2), but faster.
|
||||
func (u Vec) Normal() Vec {
|
||||
return Vec{-u.Y, u.X}
|
||||
}
|
||||
|
||||
// Dot returns the dot product of vectors u and v.
|
||||
func (u Vec) Dot(v Vec) float64 {
|
||||
return u.X*v.X + u.Y*v.Y
|
||||
}
|
||||
|
||||
// Cross return the cross product of vectors u and v.
|
||||
func (u Vec) Cross(v Vec) float64 {
|
||||
return u.X*v.Y - v.X*u.Y
|
||||
}
|
||||
|
||||
// Project returns a projection (or component) of vector u in the direction of vector v.
|
||||
//
|
||||
// Behaviour is undefined if v is a zero vector.
|
||||
func (u Vec) Project(v Vec) Vec {
|
||||
len := u.Dot(v) / v.Len()
|
||||
return v.Unit().Scaled(len)
|
||||
}
|
||||
|
||||
// Map applies the function f to both x and y components of the vector u and returns the modified
|
||||
// vector.
|
||||
//
|
||||
// u := pixel.V(10.5, -1.5)
|
||||
// v := u.Map(math.Floor) // v is Vec(10, -2), both components of u floored
|
||||
func (u Vec) Map(f func(float64) float64) Vec {
|
||||
return Vec{
|
||||
f(u.X),
|
||||
f(u.Y),
|
||||
}
|
||||
}
|
||||
|
||||
// Lerp returns a linear interpolation between vectors a and b.
|
||||
//
|
||||
// This function basically returns a point along the line between a and b and t chooses which one.
|
||||
// If t is 0, then a will be returned, if t is 1, b will be returned. Anything between 0 and 1 will
|
||||
// return the appropriate point between a and b and so on.
|
||||
func Lerp(a, b Vec, t float64) Vec {
|
||||
return a.Scaled(1 - t).Add(b.Scaled(t))
|
||||
}
|
||||
|
||||
// Rect is a 2D rectangle aligned with the axes of the coordinate system. It is defined by two
|
||||
// points, Min and Max.
|
||||
//
|
||||
// The invariant should hold, that Max's components are greater or equal than Min's components
|
||||
// respectively.
|
||||
type Rect struct {
|
||||
Min, Max Vec
|
||||
}
|
||||
|
||||
// R returns a new Rect with given the Min and Max coordinates.
|
||||
//
|
||||
// Note that the returned rectangle is not automatically normalized.
|
||||
func R(minX, minY, maxX, maxY float64) Rect {
|
||||
return Rect{
|
||||
Min: Vec{minX, minY},
|
||||
Max: Vec{maxX, maxY},
|
||||
}
|
||||
}
|
||||
|
||||
// String returns the string representation of the Rect.
|
||||
//
|
||||
// r := pixel.R(100, 50, 200, 300)
|
||||
// r.String() // returns "Rect(100, 50, 200, 300)"
|
||||
// fmt.Println(r) // Rect(100, 50, 200, 300)
|
||||
func (r Rect) String() string {
|
||||
return fmt.Sprintf("Rect(%v, %v, %v, %v)", r.Min.X, r.Min.Y, r.Max.X, r.Max.Y)
|
||||
}
|
||||
|
||||
// Norm returns the Rect in normal form, such that Max is component-wise greater or equal than Min.
|
||||
func (r Rect) Norm() Rect {
|
||||
return Rect{
|
||||
Min: Vec{
|
||||
math.Min(r.Min.X, r.Max.X),
|
||||
math.Min(r.Min.Y, r.Max.Y),
|
||||
},
|
||||
Max: Vec{
|
||||
math.Max(r.Min.X, r.Max.X),
|
||||
math.Max(r.Min.Y, r.Max.Y),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// W returns the width of the Rect.
|
||||
func (r Rect) W() float64 {
|
||||
return r.Max.X - r.Min.X
|
||||
}
|
||||
|
||||
// H returns the height of the Rect.
|
||||
func (r Rect) H() float64 {
|
||||
return r.Max.Y - r.Min.Y
|
||||
}
|
||||
|
||||
// Size returns the vector of width and height of the Rect.
|
||||
func (r Rect) Size() Vec {
|
||||
return V(r.W(), r.H())
|
||||
}
|
||||
|
||||
// Area returns the area of r. If r is not normalized, area may be negative.
|
||||
func (r Rect) Area() float64 {
|
||||
return r.W() * r.H()
|
||||
}
|
||||
|
||||
// Center returns the position of the center of the Rect.
|
||||
func (r Rect) Center() Vec {
|
||||
return Lerp(r.Min, r.Max, 0.5)
|
||||
}
|
||||
|
||||
// Moved returns the Rect moved (both Min and Max) by the given vector delta.
|
||||
func (r Rect) Moved(delta Vec) Rect {
|
||||
return Rect{
|
||||
Min: r.Min.Add(delta),
|
||||
Max: r.Max.Add(delta),
|
||||
}
|
||||
}
|
||||
|
||||
// Resized returns the Rect resized to the given size while keeping the position of the given
|
||||
// anchor.
|
||||
//
|
||||
// r.Resized(r.Min, size) // resizes while keeping the position of the lower-left corner
|
||||
// r.Resized(r.Max, size) // same with the top-right corner
|
||||
// r.Resized(r.Center(), size) // resizes around the center
|
||||
//
|
||||
// This function does not make sense for resizing a rectangle of zero area and will panic. Use
|
||||
// ResizedMin in the case of zero area.
|
||||
func (r Rect) Resized(anchor, size Vec) Rect {
|
||||
if r.W()*r.H() == 0 {
|
||||
panic(fmt.Errorf("(%T).Resize: zero area", r))
|
||||
}
|
||||
fraction := Vec{size.X / r.W(), size.Y / r.H()}
|
||||
return Rect{
|
||||
Min: anchor.Add(r.Min.Sub(anchor).ScaledXY(fraction)),
|
||||
Max: anchor.Add(r.Max.Sub(anchor).ScaledXY(fraction)),
|
||||
}
|
||||
}
|
||||
|
||||
// ResizedMin returns the Rect resized to the given size while keeping the position of the Rect's
|
||||
// Min.
|
||||
//
|
||||
// Sizes of zero area are safe here.
|
||||
func (r Rect) ResizedMin(size Vec) Rect {
|
||||
return Rect{
|
||||
Min: r.Min,
|
||||
Max: r.Min.Add(size),
|
||||
}
|
||||
}
|
||||
|
||||
// Contains checks whether a vector u is contained within this Rect (including it's borders).
|
||||
func (r Rect) Contains(u Vec) bool {
|
||||
return r.Min.X <= u.X && u.X <= r.Max.X && r.Min.Y <= u.Y && u.Y <= r.Max.Y
|
||||
}
|
||||
|
||||
// Union returns the minimal Rect which covers both r and s. Rects r and s must be normalized.
|
||||
func (r Rect) Union(s Rect) Rect {
|
||||
return R(
|
||||
math.Min(r.Min.X, s.Min.X),
|
||||
math.Min(r.Min.Y, s.Min.Y),
|
||||
math.Max(r.Max.X, s.Max.X),
|
||||
math.Max(r.Max.Y, s.Max.Y),
|
||||
)
|
||||
}
|
||||
|
||||
// Intersect returns the maximal Rect which is covered by both r and s. Rects r and s must be normalized.
|
||||
//
|
||||
// If r and s don't overlap, this function returns R(0, 0, 0, 0).
|
||||
func (r Rect) Intersect(s Rect) Rect {
|
||||
t := R(
|
||||
math.Max(r.Min.X, s.Min.X),
|
||||
math.Max(r.Min.Y, s.Min.Y),
|
||||
math.Min(r.Max.X, s.Max.X),
|
||||
math.Min(r.Max.Y, s.Max.Y),
|
||||
)
|
||||
if t.Min.X >= t.Max.X || t.Min.Y >= t.Max.Y {
|
||||
return Rect{}
|
||||
}
|
||||
return t
|
||||
}
|
||||
|
||||
// Matrix is a 2x3 affine matrix that can be used for all kinds of spatial transforms, such
|
||||
// as movement, scaling and rotations.
|
||||
//
|
||||
// Matrix has a handful of useful methods, each of which adds a transformation to the matrix. For
|
||||
// example:
|
||||
//
|
||||
// pixel.IM.Moved(pixel.V(100, 200)).Rotated(pixel.ZV, math.Pi/2)
|
||||
//
|
||||
// This code creates a Matrix that first moves everything by 100 units horizontally and 200 units
|
||||
// vertically and then rotates everything by 90 degrees around the origin.
|
||||
//
|
||||
// Layout is:
|
||||
// [0] [2] [4]
|
||||
// [1] [3] [5]
|
||||
// 0 0 1 (implicit row)
|
||||
type Matrix [6]float64
|
||||
|
||||
// IM stands for identity matrix. Does nothing, no transformation.
|
||||
var IM = Matrix{1, 0, 0, 1, 0, 0}
|
||||
|
||||
// String returns a string representation of the Matrix.
|
||||
//
|
||||
// m := pixel.IM
|
||||
// fmt.Println(m) // Matrix(1 0 0 | 0 1 0)
|
||||
func (m Matrix) String() string {
|
||||
return fmt.Sprintf(
|
||||
"Matrix(%v %v %v | %v %v %v)",
|
||||
m[0], m[2], m[4],
|
||||
m[1], m[3], m[5],
|
||||
)
|
||||
}
|
||||
|
||||
// Moved moves everything by the delta vector.
|
||||
func (m Matrix) Moved(delta Vec) Matrix {
|
||||
m[4], m[5] = m[4]+delta.X, m[5]+delta.Y
|
||||
return m
|
||||
}
|
||||
|
||||
// ScaledXY scales everything around a given point by the scale factor in each axis respectively.
|
||||
func (m Matrix) ScaledXY(around Vec, scale Vec) Matrix {
|
||||
m[4], m[5] = m[4]-around.X, m[5]-around.Y
|
||||
m[0], m[2], m[4] = m[0]*scale.X, m[2]*scale.X, m[4]*scale.X
|
||||
m[1], m[3], m[5] = m[1]*scale.Y, m[3]*scale.Y, m[5]*scale.Y
|
||||
m[4], m[5] = m[4]+around.X, m[5]+around.Y
|
||||
return m
|
||||
}
|
||||
|
||||
// Scaled scales everything around a given point by the scale factor.
|
||||
func (m Matrix) Scaled(around Vec, scale float64) Matrix {
|
||||
return m.ScaledXY(around, V(scale, scale))
|
||||
}
|
||||
|
||||
// Rotated rotates everything around a given point by the given angle in radians.
|
||||
func (m Matrix) Rotated(around Vec, angle float64) Matrix {
|
||||
sint, cost := math.Sincos(angle)
|
||||
m[4], m[5] = m[4]-around.X, m[5]-around.Y
|
||||
m = m.Chained(Matrix{cost, sint, -sint, cost, 0, 0})
|
||||
m[4], m[5] = m[4]+around.X, m[5]+around.Y
|
||||
return m
|
||||
}
|
||||
|
||||
// Chained adds another Matrix to this one. All tranformations by the next Matrix will be applied
|
||||
// after the transformations of this Matrix.
|
||||
func (m Matrix) Chained(next Matrix) Matrix {
|
||||
return Matrix{
|
||||
next[0]*m[0] + next[2]*m[1],
|
||||
next[1]*m[0] + next[3]*m[1],
|
||||
next[0]*m[2] + next[2]*m[3],
|
||||
next[1]*m[2] + next[3]*m[3],
|
||||
next[0]*m[4] + next[2]*m[5] + next[4],
|
||||
next[1]*m[4] + next[3]*m[5] + next[5],
|
||||
}
|
||||
}
|
||||
|
||||
// Project applies all transformations added to the Matrix to a vector u and returns the result.
|
||||
//
|
||||
// Time complexity is O(1).
|
||||
func (m Matrix) Project(u Vec) Vec {
|
||||
return Vec{m[0]*u.X + m[2]*u.Y + m[4], m[1]*u.X + m[3]*u.Y + m[5]}
|
||||
}
|
||||
|
||||
// Unproject does the inverse operation to Project.
|
||||
//
|
||||
// It turns out that multiplying a vector by the inverse matrix of m can be nearly-accomplished by
|
||||
// subtracting the translate part of the matrix and multplying by the inverse of the top-left 2x2
|
||||
// matrix, and the inverse of a 2x2 matrix is simple enough to just be inlined in the computation.
|
||||
//
|
||||
// Time complexity is O(1).
|
||||
func (m Matrix) Unproject(u Vec) Vec {
|
||||
d := (m[0] * m[3]) - (m[1] * m[2])
|
||||
u.X, u.Y = (u.X-m[4])/d, (u.Y-m[5])/d
|
||||
return Vec{u.X*m[3] - u.Y*m[1], u.Y*m[0] - u.X*m[2]}
|
||||
}
|
|
@ -1,79 +0,0 @@
|
|||
package pixel_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/faiface/pixel"
|
||||
)
|
||||
|
||||
type rectTestTransform struct {
|
||||
name string
|
||||
f func(pixel.Rect) pixel.Rect
|
||||
}
|
||||
|
||||
func TestResizeRect(t *testing.T) {
|
||||
|
||||
// rectangles
|
||||
squareAroundOrigin := pixel.R(-10, -10, 10, 10)
|
||||
squareAround2020 := pixel.R(10, 10, 30, 30)
|
||||
rectangleAroundOrigin := pixel.R(-20, -10, 20, 10)
|
||||
rectangleAround2020 := pixel.R(0, 10, 40, 30)
|
||||
|
||||
// resize transformations
|
||||
resizeByHalfAroundCenter := rectTestTransform{"by half around center", func(rect pixel.Rect) pixel.Rect {
|
||||
return rect.Resized(rect.Center(), rect.Size().Scaled(0.5))
|
||||
}}
|
||||
resizeByHalfAroundMin := rectTestTransform{"by half around Min", func(rect pixel.Rect) pixel.Rect {
|
||||
return rect.Resized(rect.Min, rect.Size().Scaled(0.5))
|
||||
}}
|
||||
resizeByHalfAroundMax := rectTestTransform{"by half around Max", func(rect pixel.Rect) pixel.Rect {
|
||||
return rect.Resized(rect.Max, rect.Size().Scaled(0.5))
|
||||
}}
|
||||
resizeByHalfAroundMiddleOfLeftSide := rectTestTransform{"by half around middle of left side", func(rect pixel.Rect) pixel.Rect {
|
||||
return rect.Resized(pixel.V(rect.Min.X, rect.Center().Y), rect.Size().Scaled(0.5))
|
||||
}}
|
||||
resizeByHalfAroundOrigin := rectTestTransform{"by half around the origin", func(rect pixel.Rect) pixel.Rect {
|
||||
return rect.Resized(pixel.ZV, rect.Size().Scaled(0.5))
|
||||
}}
|
||||
|
||||
testCases := []struct {
|
||||
input pixel.Rect
|
||||
transform rectTestTransform
|
||||
answer pixel.Rect
|
||||
}{
|
||||
{squareAroundOrigin, resizeByHalfAroundCenter, pixel.R(-5, -5, 5, 5)},
|
||||
{squareAround2020, resizeByHalfAroundCenter, pixel.R(15, 15, 25, 25)},
|
||||
{rectangleAroundOrigin, resizeByHalfAroundCenter, pixel.R(-10, -5, 10, 5)},
|
||||
{rectangleAround2020, resizeByHalfAroundCenter, pixel.R(10, 15, 30, 25)},
|
||||
|
||||
{squareAroundOrigin, resizeByHalfAroundMin, pixel.R(-10, -10, 0, 0)},
|
||||
{squareAround2020, resizeByHalfAroundMin, pixel.R(10, 10, 20, 20)},
|
||||
{rectangleAroundOrigin, resizeByHalfAroundMin, pixel.R(-20, -10, 0, 0)},
|
||||
{rectangleAround2020, resizeByHalfAroundMin, pixel.R(0, 10, 20, 20)},
|
||||
|
||||
{squareAroundOrigin, resizeByHalfAroundMax, pixel.R(0, 0, 10, 10)},
|
||||
{squareAround2020, resizeByHalfAroundMax, pixel.R(20, 20, 30, 30)},
|
||||
{rectangleAroundOrigin, resizeByHalfAroundMax, pixel.R(0, 0, 20, 10)},
|
||||
{rectangleAround2020, resizeByHalfAroundMax, pixel.R(20, 20, 40, 30)},
|
||||
|
||||
{squareAroundOrigin, resizeByHalfAroundMiddleOfLeftSide, pixel.R(-10, -5, 0, 5)},
|
||||
{squareAround2020, resizeByHalfAroundMiddleOfLeftSide, pixel.R(10, 15, 20, 25)},
|
||||
{rectangleAroundOrigin, resizeByHalfAroundMiddleOfLeftSide, pixel.R(-20, -5, 0, 5)},
|
||||
{rectangleAround2020, resizeByHalfAroundMiddleOfLeftSide, pixel.R(0, 15, 20, 25)},
|
||||
|
||||
{squareAroundOrigin, resizeByHalfAroundOrigin, pixel.R(-5, -5, 5, 5)},
|
||||
{squareAround2020, resizeByHalfAroundOrigin, pixel.R(5, 5, 15, 15)},
|
||||
{rectangleAroundOrigin, resizeByHalfAroundOrigin, pixel.R(-10, -5, 10, 5)},
|
||||
{rectangleAround2020, resizeByHalfAroundOrigin, pixel.R(0, 5, 20, 15)},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
t.Run(fmt.Sprintf("Resize %v %s", testCase.input, testCase.transform.name), func(t *testing.T) {
|
||||
testResult := testCase.transform.f(testCase.input)
|
||||
if testResult != testCase.answer {
|
||||
t.Errorf("Got: %v, wanted: %v\n", testResult, testCase.answer)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
module github.com/faiface/pixel
|
||||
|
||||
go 1.12
|
||||
|
||||
require (
|
||||
github.com/faiface/glhf v0.0.0-20211013000516-57b20770c369
|
||||
github.com/faiface/mainthread v0.0.0-20171120011319-8b78f0a41ae3
|
||||
github.com/go-gl/gl v0.0.0-20210905235341-f7a045908259
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20210727001814-0db043d8d5be
|
||||
github.com/go-gl/mathgl v1.0.0
|
||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/stretchr/testify v1.7.0
|
||||
golang.org/x/image v0.5.0
|
||||
)
|
|
@ -0,0 +1,54 @@
|
|||
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/faiface/glhf v0.0.0-20211013000516-57b20770c369 h1:gv4BgP50atccdK/1tZHDyP6rMwiiutR2HPreR/OyLzI=
|
||||
github.com/faiface/glhf v0.0.0-20211013000516-57b20770c369/go.mod h1:dDdUO+G9ZnJ9sc8nIUvhLkE45k8PEKW6+A3TdWsfpV0=
|
||||
github.com/faiface/mainthread v0.0.0-20171120011319-8b78f0a41ae3 h1:baVdMKlASEHrj19iqjARrPbaRisD7EuZEVJj6ZMLl1Q=
|
||||
github.com/faiface/mainthread v0.0.0-20171120011319-8b78f0a41ae3/go.mod h1:VEPNJUlxl5KdWjDvz6Q1l+rJlxF2i6xqDeGuGAxa87M=
|
||||
github.com/go-gl/gl v0.0.0-20210905235341-f7a045908259 h1:8q7+xl2D2qHPLTII1t4vSMNP2VKwDcn+Avf2WXvdB1A=
|
||||
github.com/go-gl/gl v0.0.0-20210905235341-f7a045908259/go.mod h1:wjpnOv6ONl2SuJSxqCPVaPZibGFdSci9HFocT9qtVYM=
|
||||
github.com/go-gl/glfw v0.0.0-20210727001814-0db043d8d5be h1:UVW91pfMB1GRQfVwC7//RGVbqX6Ea8jURmJhlANak1M=
|
||||
github.com/go-gl/glfw v0.0.0-20210727001814-0db043d8d5be/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20210727001814-0db043d8d5be h1:vEIVIuBApEBQTEJt19GfhoU+zFSV+sNTa9E9FdnRYfk=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20210727001814-0db043d8d5be/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
github.com/go-gl/mathgl v1.0.0 h1:t9DznWJlXxxjeeKLIdovCOVJQk/GzDEL7h/h+Ro2B68=
|
||||
github.com/go-gl/mathgl v1.0.0/go.mod h1:yhpkQzEiH9yPyxDUGzkmgScbaBVlhC06qodikEM0ZwQ=
|
||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
|
||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/image v0.0.0-20190321063152-3fc05d484e9f/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||
golang.org/x/image v0.5.0 h1:5JMiNunQeQw++mMOz48/ISeNu3Iweh/JaZU8ZLqHRrI=
|
||||
golang.org/x/image v0.5.0/go.mod h1:FVC7BI/5Ym8R25iw5OLsgshdUBbT1h5jZTpA+mvAdZ4=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
|
@ -128,7 +128,9 @@ func (imd *IMDraw) Draw(t pixel.Target) {
|
|||
// Push adds some points to the IM queue. All Pushed points will have the same properties except for
|
||||
// the position.
|
||||
func (imd *IMDraw) Push(pts ...pixel.Vec) {
|
||||
// Assert that Color is of type pixel.RGBA,
|
||||
if _, ok := imd.Color.(pixel.RGBA); !ok {
|
||||
// otherwise cast it
|
||||
imd.Color = pixel.ToRGBA(imd.Color)
|
||||
}
|
||||
opts := point{
|
||||
|
|
13
interface.go
13
interface.go
|
@ -92,7 +92,7 @@ type TrianglesColor interface {
|
|||
Color(i int) RGBA
|
||||
}
|
||||
|
||||
// TrianglesPicture specifies Triangles with Picture propery.
|
||||
// TrianglesPicture specifies Triangles with Picture property.
|
||||
//
|
||||
// The first value returned from Picture method is Picture coordinates. The second one specifies the
|
||||
// weight of the Picture. Value of 0 means, that Picture should be completely ignored, 1 means that
|
||||
|
@ -102,10 +102,19 @@ type TrianglesPicture interface {
|
|||
Picture(i int) (pic Vec, intensity float64)
|
||||
}
|
||||
|
||||
// TrianglesClipped specifies Triangles with Clipping Rectangle property.
|
||||
//
|
||||
// The first value returned from ClipRect method is the clipping rectangle. The second one specifies
|
||||
// if the triangle is clipped.
|
||||
type TrianglesClipped interface {
|
||||
Triangles
|
||||
ClipRect(i int) (rect Rect, is bool)
|
||||
}
|
||||
|
||||
// Picture represents a rectangular area of raster data, such as a color. It has Bounds which
|
||||
// specify the rectangle where data is located.
|
||||
type Picture interface {
|
||||
// Bounds returns the rectangle of the Picture. All data is located witih this rectangle.
|
||||
// Bounds returns the rectangle of the Picture. All data is located within this rectangle.
|
||||
// Querying properties outside the rectangle should return default value of that property.
|
||||
Bounds() Rect
|
||||
}
|
||||
|
|
|
@ -0,0 +1,699 @@
|
|||
package pixel_test
|
||||
|
||||
import (
|
||||
"math"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/faiface/pixel"
|
||||
)
|
||||
|
||||
func TestLine_Bounds(t *testing.T) {
|
||||
type fields struct {
|
||||
A pixel.Vec
|
||||
B pixel.Vec
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
want pixel.Rect
|
||||
}{
|
||||
{
|
||||
name: "Positive slope",
|
||||
fields: fields{A: pixel.V(0, 0), B: pixel.V(10, 10)},
|
||||
want: pixel.R(0, 0, 10, 10),
|
||||
},
|
||||
{
|
||||
name: "Negative slope",
|
||||
fields: fields{A: pixel.V(10, 10), B: pixel.V(0, 0)},
|
||||
want: pixel.R(0, 0, 10, 10),
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
l := pixel.Line{
|
||||
A: tt.fields.A,
|
||||
B: tt.fields.B,
|
||||
}
|
||||
if got := l.Bounds(); !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("Line.Bounds() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLine_Center(t *testing.T) {
|
||||
type fields struct {
|
||||
A pixel.Vec
|
||||
B pixel.Vec
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
want pixel.Vec
|
||||
}{
|
||||
{
|
||||
name: "Positive slope",
|
||||
fields: fields{A: pixel.V(0, 0), B: pixel.V(10, 10)},
|
||||
want: pixel.V(5, 5),
|
||||
},
|
||||
{
|
||||
name: "Negative slope",
|
||||
fields: fields{A: pixel.V(10, 10), B: pixel.V(0, 0)},
|
||||
want: pixel.V(5, 5),
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
l := pixel.Line{
|
||||
A: tt.fields.A,
|
||||
B: tt.fields.B,
|
||||
}
|
||||
if got := l.Center(); !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("Line.Center() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLine_Closest(t *testing.T) {
|
||||
type fields struct {
|
||||
A pixel.Vec
|
||||
B pixel.Vec
|
||||
}
|
||||
type args struct {
|
||||
v pixel.Vec
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
want pixel.Vec
|
||||
}{
|
||||
{
|
||||
name: "Point on line",
|
||||
fields: fields{A: pixel.V(0, 0), B: pixel.V(10, 10)},
|
||||
args: args{v: pixel.V(5, 5)},
|
||||
want: pixel.V(5, 5),
|
||||
},
|
||||
{
|
||||
name: "Point on next to line",
|
||||
fields: fields{A: pixel.V(0, 0), B: pixel.V(10, 10)},
|
||||
args: args{v: pixel.V(0, 10)},
|
||||
want: pixel.V(5, 5),
|
||||
},
|
||||
{
|
||||
name: "Point on next to vertical line",
|
||||
fields: fields{A: pixel.V(5, 0), B: pixel.V(5, 10)},
|
||||
args: args{v: pixel.V(6, 5)},
|
||||
want: pixel.V(5, 5),
|
||||
},
|
||||
{
|
||||
name: "Point on next to horizontal line",
|
||||
fields: fields{A: pixel.V(0, 5), B: pixel.V(10, 5)},
|
||||
args: args{v: pixel.V(5, 6)},
|
||||
want: pixel.V(5, 5),
|
||||
},
|
||||
{
|
||||
name: "Point far from line",
|
||||
fields: fields{A: pixel.V(0, 0), B: pixel.V(10, 10)},
|
||||
args: args{v: pixel.V(80, -70)},
|
||||
want: pixel.V(5, 5),
|
||||
},
|
||||
{
|
||||
name: "Point on inline with line",
|
||||
fields: fields{A: pixel.V(0, 0), B: pixel.V(10, 10)},
|
||||
args: args{v: pixel.V(20, 20)},
|
||||
want: pixel.V(10, 10),
|
||||
},
|
||||
{
|
||||
name: "Vertical line",
|
||||
fields: fields{A: pixel.V(0, -10), B: pixel.V(0, 10)},
|
||||
args: args{v: pixel.V(-1, 0)},
|
||||
want: pixel.V(0, 0),
|
||||
},
|
||||
{
|
||||
name: "Horizontal line",
|
||||
fields: fields{A: pixel.V(-10, 0), B: pixel.V(10, 0)},
|
||||
args: args{v: pixel.V(0, -1)},
|
||||
want: pixel.V(0, 0),
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
l := pixel.Line{
|
||||
A: tt.fields.A,
|
||||
B: tt.fields.B,
|
||||
}
|
||||
if got := l.Closest(tt.args.v); !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("Line.Closest() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLine_Contains(t *testing.T) {
|
||||
type fields struct {
|
||||
A pixel.Vec
|
||||
B pixel.Vec
|
||||
}
|
||||
type args struct {
|
||||
v pixel.Vec
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "Point on line",
|
||||
fields: fields{A: pixel.V(0, 0), B: pixel.V(10, 10)},
|
||||
args: args{v: pixel.V(5, 5)},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "Point on negative sloped line",
|
||||
fields: fields{A: pixel.V(0, 10), B: pixel.V(10, 0)},
|
||||
args: args{v: pixel.V(5, 5)},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "Point not on line",
|
||||
fields: fields{A: pixel.V(0, 0), B: pixel.V(10, 10)},
|
||||
args: args{v: pixel.V(0, 10)},
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
l := pixel.Line{
|
||||
A: tt.fields.A,
|
||||
B: tt.fields.B,
|
||||
}
|
||||
if got := l.Contains(tt.args.v); got != tt.want {
|
||||
t.Errorf("Line.Contains() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLine_Formula(t *testing.T) {
|
||||
type fields struct {
|
||||
A pixel.Vec
|
||||
B pixel.Vec
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
wantM float64
|
||||
wantB float64
|
||||
}{
|
||||
{
|
||||
name: "Getting formula - 45 degs",
|
||||
fields: fields{A: pixel.V(0, 0), B: pixel.V(10, 10)},
|
||||
wantM: 1,
|
||||
wantB: 0,
|
||||
},
|
||||
{
|
||||
name: "Getting formula - 90 degs",
|
||||
fields: fields{A: pixel.V(0, 0), B: pixel.V(0, 10)},
|
||||
wantM: math.Inf(1),
|
||||
wantB: math.NaN(),
|
||||
},
|
||||
{
|
||||
name: "Getting formula - 0 degs",
|
||||
fields: fields{A: pixel.V(0, 0), B: pixel.V(10, 0)},
|
||||
wantM: 0,
|
||||
wantB: 0,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
l := pixel.Line{
|
||||
A: tt.fields.A,
|
||||
B: tt.fields.B,
|
||||
}
|
||||
gotM, gotB := l.Formula()
|
||||
if gotM != tt.wantM {
|
||||
t.Errorf("Line.Formula() gotM = %v, want %v", gotM, tt.wantM)
|
||||
}
|
||||
if gotB != tt.wantB {
|
||||
if math.IsNaN(tt.wantB) && !math.IsNaN(gotB) {
|
||||
t.Errorf("Line.Formula() gotB = %v, want %v", gotB, tt.wantB)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLine_Intersect(t *testing.T) {
|
||||
type fields struct {
|
||||
A pixel.Vec
|
||||
B pixel.Vec
|
||||
}
|
||||
type args struct {
|
||||
k pixel.Line
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
want pixel.Vec
|
||||
want1 bool
|
||||
}{
|
||||
{
|
||||
name: "Lines intersect",
|
||||
fields: fields{A: pixel.V(0, 0), B: pixel.V(10, 10)},
|
||||
args: args{k: pixel.L(pixel.V(0, 10), pixel.V(10, 0))},
|
||||
want: pixel.V(5, 5),
|
||||
want1: true,
|
||||
},
|
||||
{
|
||||
name: "Lines intersect 2",
|
||||
fields: fields{A: pixel.V(5, 1), B: pixel.V(1, 1)},
|
||||
args: args{k: pixel.L(pixel.V(2, 0), pixel.V(2, 3))},
|
||||
want: pixel.V(2, 1),
|
||||
want1: true,
|
||||
},
|
||||
{
|
||||
name: "Line intersect with vertical",
|
||||
fields: fields{A: pixel.V(5, 0), B: pixel.V(5, 10)},
|
||||
args: args{k: pixel.L(pixel.V(0, 0), pixel.V(10, 10))},
|
||||
want: pixel.V(5, 5),
|
||||
want1: true,
|
||||
},
|
||||
{
|
||||
name: "Line intersect with horizontal",
|
||||
fields: fields{A: pixel.V(0, 5), B: pixel.V(10, 5)},
|
||||
args: args{k: pixel.L(pixel.V(0, 0), pixel.V(10, 10))},
|
||||
want: pixel.V(5, 5),
|
||||
want1: true,
|
||||
},
|
||||
{
|
||||
name: "Lines don't intersect",
|
||||
fields: fields{A: pixel.V(0, 0), B: pixel.V(10, 10)},
|
||||
args: args{k: pixel.L(pixel.V(0, 10), pixel.V(1, 20))},
|
||||
want: pixel.ZV,
|
||||
want1: false,
|
||||
},
|
||||
{
|
||||
name: "Lines don't intersect 2",
|
||||
fields: fields{A: pixel.V(1, 1), B: pixel.V(1, 5)},
|
||||
args: args{k: pixel.L(pixel.V(-5, 0), pixel.V(-2, 2))},
|
||||
want: pixel.ZV,
|
||||
want1: false,
|
||||
},
|
||||
{
|
||||
name: "Lines don't intersect 3",
|
||||
fields: fields{A: pixel.V(2, 0), B: pixel.V(2, 3)},
|
||||
args: args{k: pixel.L(pixel.V(1, 5), pixel.V(5, 5))},
|
||||
want: pixel.ZV,
|
||||
want1: false,
|
||||
},
|
||||
{
|
||||
name: "Lines parallel",
|
||||
fields: fields{A: pixel.V(0, 0), B: pixel.V(10, 10)},
|
||||
args: args{k: pixel.L(pixel.V(0, 1), pixel.V(10, 11))},
|
||||
want: pixel.ZV,
|
||||
want1: false,
|
||||
}, {
|
||||
name: "Lines intersect",
|
||||
fields: fields{A: pixel.V(600, 600), B: pixel.V(925, 150)},
|
||||
args: args{k: pixel.L(pixel.V(740, 255), pixel.V(925, 255))},
|
||||
want: pixel.V(849.1666666666666, 255),
|
||||
want1: true,
|
||||
},
|
||||
{
|
||||
name: "Lines intersect",
|
||||
fields: fields{A: pixel.V(600, 600), B: pixel.V(925, 150)},
|
||||
args: args{k: pixel.L(pixel.V(740, 255), pixel.V(925, 255.0001))},
|
||||
want: pixel.V(849.1666240490657, 255.000059008986),
|
||||
want1: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
l := pixel.Line{
|
||||
A: tt.fields.A,
|
||||
B: tt.fields.B,
|
||||
}
|
||||
got, got1 := l.Intersect(tt.args.k)
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("Line.Intersect() got = %v, want %v", got, tt.want)
|
||||
}
|
||||
if got1 != tt.want1 {
|
||||
t.Errorf("Line.Intersect() got1 = %v, want %v", got1, tt.want1)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLine_IntersectCircle(t *testing.T) {
|
||||
type fields struct {
|
||||
A pixel.Vec
|
||||
B pixel.Vec
|
||||
}
|
||||
type args struct {
|
||||
c pixel.Circle
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
want pixel.Vec
|
||||
}{
|
||||
{
|
||||
name: "Cirle intersects",
|
||||
fields: fields{A: pixel.V(0, 0), B: pixel.V(10, 10)},
|
||||
args: args{c: pixel.C(pixel.V(6, 4), 2)},
|
||||
want: pixel.V(0.5857864376269049, -0.5857864376269049),
|
||||
},
|
||||
{
|
||||
name: "Cirle doesn't intersects",
|
||||
fields: fields{A: pixel.V(0, 0), B: pixel.V(10, 10)},
|
||||
args: args{c: pixel.C(pixel.V(0, 5), 1)},
|
||||
want: pixel.ZV,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
l := pixel.Line{
|
||||
A: tt.fields.A,
|
||||
B: tt.fields.B,
|
||||
}
|
||||
if got := l.IntersectCircle(tt.args.c); !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("Line.IntersectCircle() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLine_IntersectRect(t *testing.T) {
|
||||
type fields struct {
|
||||
A pixel.Vec
|
||||
B pixel.Vec
|
||||
}
|
||||
type args struct {
|
||||
r pixel.Rect
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
want pixel.Vec
|
||||
}{
|
||||
{
|
||||
name: "Line through rect vertically",
|
||||
fields: fields{A: pixel.V(0, 0), B: pixel.V(0, 10)},
|
||||
args: args{r: pixel.R(-1, 1, 5, 5)},
|
||||
want: pixel.V(-1, 0),
|
||||
},
|
||||
{
|
||||
name: "Line through rect horizontally",
|
||||
fields: fields{A: pixel.V(0, 1), B: pixel.V(10, 1)},
|
||||
args: args{r: pixel.R(1, 0, 5, 5)},
|
||||
want: pixel.V(0, -1),
|
||||
},
|
||||
{
|
||||
name: "Line through rect diagonally bottom and left edges",
|
||||
fields: fields{A: pixel.V(0, 0), B: pixel.V(10, 10)},
|
||||
args: args{r: pixel.R(0, 2, 3, 3)},
|
||||
want: pixel.V(-1, 1),
|
||||
},
|
||||
{
|
||||
name: "Line through rect diagonally top and right edges",
|
||||
fields: fields{A: pixel.V(10, 0), B: pixel.V(0, 10)},
|
||||
args: args{r: pixel.R(5, 0, 8, 3)},
|
||||
want: pixel.V(-2.5, -2.5),
|
||||
},
|
||||
{
|
||||
name: "Line with not rect intersect",
|
||||
fields: fields{A: pixel.V(0, 0), B: pixel.V(10, 10)},
|
||||
args: args{r: pixel.R(20, 20, 21, 21)},
|
||||
want: pixel.ZV,
|
||||
},
|
||||
{
|
||||
name: "Line intersects at 0,0",
|
||||
fields: fields{A: pixel.V(0, -10), B: pixel.V(0, 10)},
|
||||
args: args{r: pixel.R(-1, 0, 2, 2)},
|
||||
want: pixel.V(-1, 0),
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
l := pixel.Line{
|
||||
A: tt.fields.A,
|
||||
B: tt.fields.B,
|
||||
}
|
||||
if got := l.IntersectRect(tt.args.r); !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("Line.IntersectRect() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLine_Len(t *testing.T) {
|
||||
type fields struct {
|
||||
A pixel.Vec
|
||||
B pixel.Vec
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
want float64
|
||||
}{
|
||||
{
|
||||
name: "End right-up of start",
|
||||
fields: fields{A: pixel.V(0, 0), B: pixel.V(3, 4)},
|
||||
want: 5,
|
||||
},
|
||||
{
|
||||
name: "End left-up of start",
|
||||
fields: fields{A: pixel.V(0, 0), B: pixel.V(-3, 4)},
|
||||
want: 5,
|
||||
},
|
||||
{
|
||||
name: "End right-down of start",
|
||||
fields: fields{A: pixel.V(0, 0), B: pixel.V(3, -4)},
|
||||
want: 5,
|
||||
},
|
||||
{
|
||||
name: "End left-down of start",
|
||||
fields: fields{A: pixel.V(0, 0), B: pixel.V(-3, -4)},
|
||||
want: 5,
|
||||
},
|
||||
{
|
||||
name: "End same as start",
|
||||
fields: fields{A: pixel.V(0, 0), B: pixel.V(0, 0)},
|
||||
want: 0,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
l := pixel.Line{
|
||||
A: tt.fields.A,
|
||||
B: tt.fields.B,
|
||||
}
|
||||
if got := l.Len(); got != tt.want {
|
||||
t.Errorf("Line.Len() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLine_Rotated(t *testing.T) {
|
||||
// round returns the nearest integer, rounding ties away from zero.
|
||||
// This is required because `math.Round` wasn't introduced until Go1.10
|
||||
round := func(x float64) float64 {
|
||||
t := math.Trunc(x)
|
||||
if math.Abs(x-t) >= 0.5 {
|
||||
return t + math.Copysign(1, x)
|
||||
}
|
||||
return t
|
||||
}
|
||||
type fields struct {
|
||||
A pixel.Vec
|
||||
B pixel.Vec
|
||||
}
|
||||
type args struct {
|
||||
around pixel.Vec
|
||||
angle float64
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
want pixel.Line
|
||||
}{
|
||||
{
|
||||
name: "Rotating around line center",
|
||||
fields: fields{A: pixel.V(1, 1), B: pixel.V(3, 3)},
|
||||
args: args{around: pixel.V(2, 2), angle: math.Pi},
|
||||
want: pixel.L(pixel.V(3, 3), pixel.V(1, 1)),
|
||||
},
|
||||
{
|
||||
name: "Rotating around x-y origin",
|
||||
fields: fields{A: pixel.V(1, 1), B: pixel.V(3, 3)},
|
||||
args: args{around: pixel.V(0, 0), angle: math.Pi},
|
||||
want: pixel.L(pixel.V(-1, -1), pixel.V(-3, -3)),
|
||||
},
|
||||
{
|
||||
name: "Rotating around line end",
|
||||
fields: fields{A: pixel.V(1, 1), B: pixel.V(3, 3)},
|
||||
args: args{around: pixel.V(1, 1), angle: math.Pi},
|
||||
want: pixel.L(pixel.V(1, 1), pixel.V(-1, -1)),
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
l := pixel.Line{
|
||||
A: tt.fields.A,
|
||||
B: tt.fields.B,
|
||||
}
|
||||
// Have to round the results, due to floating-point in accuracies. Results are correct to approximately
|
||||
// 10 decimal places.
|
||||
got := l.Rotated(tt.args.around, tt.args.angle)
|
||||
if round(got.A.X) != tt.want.A.X ||
|
||||
round(got.B.X) != tt.want.B.X ||
|
||||
round(got.A.Y) != tt.want.A.Y ||
|
||||
round(got.B.Y) != tt.want.B.Y {
|
||||
t.Errorf("Line.Rotated() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLine_Scaled(t *testing.T) {
|
||||
type fields struct {
|
||||
A pixel.Vec
|
||||
B pixel.Vec
|
||||
}
|
||||
type args struct {
|
||||
scale float64
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
want pixel.Line
|
||||
}{
|
||||
{
|
||||
name: "Scaling by 1",
|
||||
fields: fields{A: pixel.V(0, 0), B: pixel.V(10, 10)},
|
||||
args: args{scale: 1},
|
||||
want: pixel.L(pixel.V(0, 0), pixel.V(10, 10)),
|
||||
},
|
||||
{
|
||||
name: "Scaling by >1",
|
||||
fields: fields{A: pixel.V(0, 0), B: pixel.V(10, 10)},
|
||||
args: args{scale: 2},
|
||||
want: pixel.L(pixel.V(-5, -5), pixel.V(15, 15)),
|
||||
},
|
||||
{
|
||||
name: "Scaling by <1",
|
||||
fields: fields{A: pixel.V(0, 0), B: pixel.V(10, 10)},
|
||||
args: args{scale: 0.5},
|
||||
want: pixel.L(pixel.V(2.5, 2.5), pixel.V(7.5, 7.5)),
|
||||
},
|
||||
{
|
||||
name: "Scaling by -1",
|
||||
fields: fields{A: pixel.V(0, 0), B: pixel.V(10, 10)},
|
||||
args: args{scale: -1},
|
||||
want: pixel.L(pixel.V(10, 10), pixel.V(0, 0)),
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
l := pixel.Line{
|
||||
A: tt.fields.A,
|
||||
B: tt.fields.B,
|
||||
}
|
||||
if got := l.Scaled(tt.args.scale); !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("Line.Scaled() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLine_ScaledXY(t *testing.T) {
|
||||
type fields struct {
|
||||
A pixel.Vec
|
||||
B pixel.Vec
|
||||
}
|
||||
type args struct {
|
||||
around pixel.Vec
|
||||
scale float64
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
want pixel.Line
|
||||
}{
|
||||
{
|
||||
name: "Scaling by 1 around origin",
|
||||
fields: fields{A: pixel.V(0, 0), B: pixel.V(10, 10)},
|
||||
args: args{around: pixel.ZV, scale: 1},
|
||||
want: pixel.L(pixel.V(0, 0), pixel.V(10, 10)),
|
||||
},
|
||||
{
|
||||
name: "Scaling by >1 around origin",
|
||||
fields: fields{A: pixel.V(0, 0), B: pixel.V(10, 10)},
|
||||
args: args{around: pixel.ZV, scale: 2},
|
||||
want: pixel.L(pixel.V(0, 0), pixel.V(20, 20)),
|
||||
},
|
||||
{
|
||||
name: "Scaling by <1 around origin",
|
||||
fields: fields{A: pixel.V(0, 0), B: pixel.V(10, 10)},
|
||||
args: args{around: pixel.ZV, scale: 0.5},
|
||||
want: pixel.L(pixel.V(0, 0), pixel.V(5, 5)),
|
||||
},
|
||||
{
|
||||
name: "Scaling by -1 around origin",
|
||||
fields: fields{A: pixel.V(0, 0), B: pixel.V(10, 10)},
|
||||
args: args{around: pixel.ZV, scale: -1},
|
||||
want: pixel.L(pixel.V(0, 0), pixel.V(-10, -10)),
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
l := pixel.Line{
|
||||
A: tt.fields.A,
|
||||
B: tt.fields.B,
|
||||
}
|
||||
if got := l.ScaledXY(tt.args.around, tt.args.scale); !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("Line.ScaledXY() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLine_String(t *testing.T) {
|
||||
type fields struct {
|
||||
A pixel.Vec
|
||||
B pixel.Vec
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "Getting string",
|
||||
fields: fields{A: pixel.V(0, 0), B: pixel.V(1, 1)},
|
||||
want: "Line(Vec(0, 0), Vec(1, 1))",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
l := pixel.Line{
|
||||
A: tt.fields.A,
|
||||
B: tt.fields.B,
|
||||
}
|
||||
if got := l.String(); got != tt.want {
|
||||
t.Errorf("Line.String() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
Binary file not shown.
After Width: | Height: | Size: 12 KiB |
Binary file not shown.
After Width: | Height: | Size: 19 KiB |
Binary file not shown.
After Width: | Height: | Size: 18 KiB |
Binary file not shown.
After Width: | Height: | Size: 20 KiB |
Binary file not shown.
After Width: | Height: | Size: 17 KiB |
Binary file not shown.
After Width: | Height: | Size: 12 KiB |
|
@ -0,0 +1,15 @@
|
|||
package pixel
|
||||
|
||||
// Clamp returns x clamped to the interval [min, max].
|
||||
//
|
||||
// If x is less than min, min is returned. If x is more than max, max is returned. Otherwise, x is
|
||||
// returned.
|
||||
func Clamp(x, min, max float64) float64 {
|
||||
if x < min {
|
||||
return min
|
||||
}
|
||||
if x > max {
|
||||
return max
|
||||
}
|
||||
return x
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
package pixel_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"testing"
|
||||
|
||||
"github.com/faiface/pixel"
|
||||
)
|
||||
|
||||
// closeEnough will shift the decimal point by the accuracy required, truncates the results and compares them.
|
||||
// Effectively this compares two floats to a given decimal point.
|
||||
// Example:
|
||||
// closeEnough(100.125342432, 100.125, 2) == true
|
||||
// closeEnough(math.Pi, 3.14, 2) == true
|
||||
// closeEnough(0.1234, 0.1245, 3) == false
|
||||
func closeEnough(got, expected float64, decimalAccuracy int) bool {
|
||||
gotShifted := got * math.Pow10(decimalAccuracy)
|
||||
expectedShifted := expected * math.Pow10(decimalAccuracy)
|
||||
|
||||
return math.Trunc(gotShifted) == math.Trunc(expectedShifted)
|
||||
}
|
||||
|
||||
type clampTest struct {
|
||||
number float64
|
||||
min float64
|
||||
max float64
|
||||
expected float64
|
||||
}
|
||||
|
||||
func TestClamp(t *testing.T) {
|
||||
tests := []clampTest{
|
||||
{number: 1, min: 0, max: 5, expected: 1},
|
||||
{number: 2, min: 0, max: 5, expected: 2},
|
||||
{number: 8, min: 0, max: 5, expected: 5},
|
||||
{number: -5, min: 0, max: 5, expected: 0},
|
||||
{number: -5, min: -4, max: 5, expected: -4},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
result := pixel.Clamp(tc.number, tc.min, tc.max)
|
||||
if result != tc.expected {
|
||||
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))
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,98 @@
|
|||
package pixel
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
)
|
||||
|
||||
// Matrix is a 2x3 affine matrix that can be used for all kinds of spatial transforms, such
|
||||
// as movement, scaling and rotations.
|
||||
//
|
||||
// Matrix has a handful of useful methods, each of which adds a transformation to the matrix. For
|
||||
// example:
|
||||
//
|
||||
// pixel.IM.Moved(pixel.V(100, 200)).Rotated(pixel.ZV, math.Pi/2)
|
||||
//
|
||||
// This code creates a Matrix that first moves everything by 100 units horizontally and 200 units
|
||||
// vertically and then rotates everything by 90 degrees around the origin.
|
||||
//
|
||||
// Layout is:
|
||||
// [0] [2] [4]
|
||||
// [1] [3] [5]
|
||||
// 0 0 1 (implicit row)
|
||||
type Matrix [6]float64
|
||||
|
||||
// IM stands for identity matrix. Does nothing, no transformation.
|
||||
var IM = Matrix{1, 0, 0, 1, 0, 0}
|
||||
|
||||
// String returns a string representation of the Matrix.
|
||||
//
|
||||
// m := pixel.IM
|
||||
// fmt.Println(m) // Matrix(1 0 0 | 0 1 0)
|
||||
func (m Matrix) String() string {
|
||||
return fmt.Sprintf(
|
||||
"Matrix(%v %v %v | %v %v %v)",
|
||||
m[0], m[2], m[4],
|
||||
m[1], m[3], m[5],
|
||||
)
|
||||
}
|
||||
|
||||
// Moved moves everything by the delta vector.
|
||||
func (m Matrix) Moved(delta Vec) Matrix {
|
||||
m[4], m[5] = m[4]+delta.X, m[5]+delta.Y
|
||||
return m
|
||||
}
|
||||
|
||||
// ScaledXY scales everything around a given point by the scale factor in each axis respectively.
|
||||
func (m Matrix) ScaledXY(around Vec, scale Vec) Matrix {
|
||||
m[4], m[5] = m[4]-around.X, m[5]-around.Y
|
||||
m[0], m[2], m[4] = m[0]*scale.X, m[2]*scale.X, m[4]*scale.X
|
||||
m[1], m[3], m[5] = m[1]*scale.Y, m[3]*scale.Y, m[5]*scale.Y
|
||||
m[4], m[5] = m[4]+around.X, m[5]+around.Y
|
||||
return m
|
||||
}
|
||||
|
||||
// Scaled scales everything around a given point by the scale factor.
|
||||
func (m Matrix) Scaled(around Vec, scale float64) Matrix {
|
||||
return m.ScaledXY(around, V(scale, scale))
|
||||
}
|
||||
|
||||
// Rotated rotates everything around a given point by the given angle in radians.
|
||||
func (m Matrix) Rotated(around Vec, angle float64) Matrix {
|
||||
sint, cost := math.Sincos(angle)
|
||||
m[4], m[5] = m[4]-around.X, m[5]-around.Y
|
||||
m = m.Chained(Matrix{cost, sint, -sint, cost, 0, 0})
|
||||
m[4], m[5] = m[4]+around.X, m[5]+around.Y
|
||||
return m
|
||||
}
|
||||
|
||||
// Chained adds another Matrix to this one. All tranformations by the next Matrix will be applied
|
||||
// after the transformations of this Matrix.
|
||||
func (m Matrix) Chained(next Matrix) Matrix {
|
||||
return Matrix{
|
||||
next[0]*m[0] + next[2]*m[1],
|
||||
next[1]*m[0] + next[3]*m[1],
|
||||
next[0]*m[2] + next[2]*m[3],
|
||||
next[1]*m[2] + next[3]*m[3],
|
||||
next[0]*m[4] + next[2]*m[5] + next[4],
|
||||
next[1]*m[4] + next[3]*m[5] + next[5],
|
||||
}
|
||||
}
|
||||
|
||||
// Project applies all transformations added to the Matrix to a vector u and returns the result.
|
||||
//
|
||||
// Time complexity is O(1).
|
||||
func (m Matrix) Project(u Vec) Vec {
|
||||
return Vec{m[0]*u.X + m[2]*u.Y + m[4], m[1]*u.X + m[3]*u.Y + m[5]}
|
||||
}
|
||||
|
||||
// Unproject does the inverse operation to Project.
|
||||
//
|
||||
// Time complexity is O(1).
|
||||
func (m Matrix) Unproject(u Vec) Vec {
|
||||
det := m[0]*m[3] - m[2]*m[1]
|
||||
return Vec{
|
||||
(m[3]*(u.X-m[4]) - m[2]*(u.Y-m[5])) / det,
|
||||
(-m[1]*(u.X-m[4]) + m[0]*(u.Y-m[5])) / det,
|
||||
}
|
||||
}
|
|
@ -1,10 +1,13 @@
|
|||
package pixel_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"math/rand"
|
||||
"testing"
|
||||
|
||||
"github.com/faiface/pixel"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func BenchmarkMatrix(b *testing.B) {
|
||||
|
@ -61,3 +64,86 @@ func BenchmarkMatrix(b *testing.B) {
|
|||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestMatrix_Unproject(t *testing.T) {
|
||||
const delta = 1e-15
|
||||
t.Run("for rotated matrix", func(t *testing.T) {
|
||||
matrix := pixel.IM.
|
||||
Rotated(pixel.ZV, math.Pi/2)
|
||||
unprojected := matrix.Unproject(pixel.V(0, 1))
|
||||
assert.InDelta(t, unprojected.X, 1, delta)
|
||||
assert.InDelta(t, unprojected.Y, 0, delta)
|
||||
})
|
||||
t.Run("for moved matrix", func(t *testing.T) {
|
||||
matrix := pixel.IM.
|
||||
Moved(pixel.V(1, 2))
|
||||
unprojected := matrix.Unproject(pixel.V(2, 5))
|
||||
assert.InDelta(t, unprojected.X, 1, delta)
|
||||
assert.InDelta(t, unprojected.Y, 3, delta)
|
||||
})
|
||||
t.Run("for scaled matrix", func(t *testing.T) {
|
||||
matrix := pixel.IM.
|
||||
Scaled(pixel.ZV, 2)
|
||||
unprojected := matrix.Unproject(pixel.V(2, 4))
|
||||
assert.InDelta(t, unprojected.X, 1, delta)
|
||||
assert.InDelta(t, unprojected.Y, 2, delta)
|
||||
})
|
||||
t.Run("for scaled, rotated and moved matrix", func(t *testing.T) {
|
||||
matrix := pixel.IM.
|
||||
Scaled(pixel.ZV, 2).
|
||||
Rotated(pixel.ZV, math.Pi/2).
|
||||
Moved(pixel.V(2, 2))
|
||||
unprojected := matrix.Unproject(pixel.V(-2, 6))
|
||||
assert.InDelta(t, unprojected.X, 2, delta)
|
||||
assert.InDelta(t, unprojected.Y, 2, delta)
|
||||
})
|
||||
t.Run("for rotated and moved matrix", func(t *testing.T) {
|
||||
matrix := pixel.IM.
|
||||
Rotated(pixel.ZV, math.Pi/2).
|
||||
Moved(pixel.V(1, 1))
|
||||
unprojected := matrix.Unproject(pixel.V(1, 2))
|
||||
assert.InDelta(t, unprojected.X, 1, delta)
|
||||
assert.InDelta(t, unprojected.Y, 0, delta)
|
||||
})
|
||||
t.Run("for projected vertices using all kinds of matrices", func(t *testing.T) {
|
||||
namedMatrices := map[string]pixel.Matrix{
|
||||
"IM": pixel.IM,
|
||||
"Scaled": pixel.IM.Scaled(pixel.ZV, 0.5),
|
||||
"Scaled x 2": pixel.IM.Scaled(pixel.ZV, 2),
|
||||
"Rotated": pixel.IM.Rotated(pixel.ZV, math.Pi/4),
|
||||
"Moved": pixel.IM.Moved(pixel.V(0.5, 1)),
|
||||
"Moved 2": pixel.IM.Moved(pixel.V(-1, -0.5)),
|
||||
"Scaled and Rotated": pixel.IM.Scaled(pixel.ZV, 0.5).Rotated(pixel.ZV, math.Pi/4),
|
||||
"Scaled, Rotated and Moved": pixel.IM.Scaled(pixel.ZV, 0.5).Rotated(pixel.ZV, math.Pi/4).Moved(pixel.V(1, 2)),
|
||||
"Rotated and Moved": pixel.IM.Rotated(pixel.ZV, math.Pi/4).Moved(pixel.V(1, 2)),
|
||||
}
|
||||
vertices := [...]pixel.Vec{
|
||||
pixel.V(0, 0),
|
||||
pixel.V(5, 0),
|
||||
pixel.V(5, 10),
|
||||
pixel.V(0, 10),
|
||||
pixel.V(-5, 10),
|
||||
pixel.V(-5, 0),
|
||||
pixel.V(-5, -10),
|
||||
pixel.V(0, -10),
|
||||
pixel.V(5, -10),
|
||||
}
|
||||
for matrixName, matrix := range namedMatrices {
|
||||
for _, vertex := range vertices {
|
||||
testCase := fmt.Sprintf("for matrix %s and vertex %v", matrixName, vertex)
|
||||
t.Run(testCase, func(t *testing.T) {
|
||||
projected := matrix.Project(vertex)
|
||||
unprojected := matrix.Unproject(projected)
|
||||
assert.InDelta(t, vertex.X, unprojected.X, delta)
|
||||
assert.InDelta(t, vertex.Y, unprojected.Y, delta)
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
t.Run("for singular matrix", func(t *testing.T) {
|
||||
matrix := pixel.Matrix{0, 0, 0, 0, 0, 0}
|
||||
unprojected := matrix.Unproject(pixel.ZV)
|
||||
assert.True(t, math.IsNaN(unprojected.X))
|
||||
assert.True(t, math.IsNaN(unprojected.Y))
|
||||
})
|
||||
}
|
||||
|
|
|
@ -0,0 +1,68 @@
|
|||
package pixel_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"image"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
_ "image/png"
|
||||
|
||||
"github.com/faiface/pixel"
|
||||
"github.com/faiface/pixel/pixelgl"
|
||||
)
|
||||
|
||||
// onePixelImage is the byte representation of a 1x1 solid white png file
|
||||
var onePixelImage = []byte{
|
||||
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,
|
||||
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,
|
||||
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,
|
||||
141, 66, 16, 34, 132, 168, 96, 212, 194, 221, 141, 137, 66, 118, 13, 187, 9, 54, 150, 130, 109, 192, 194, 79,
|
||||
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,
|
||||
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,
|
||||
233, 149, 0, 65, 90, 104, 0, 221, 116, 179, 227, 177, 88, 148, 186, 235, 235, 94, 213, 193, 93, 191, 202, 170,
|
||||
95, 247, 231, 106, 75, 174, 184, 38, 52, 104, 194, 99, 102, 214, 201, 9, 47, 11, 143, 108, 228, 178, 138, 247,
|
||||
132, 67, 230, 170, 158, 20, 62, 23, 238, 115, 164, 65, 225, 71, 165, 27, 101, 126, 83, 156, 46, 177, 79, 101,
|
||||
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,
|
||||
243, 101, 78, 42, 222, 84, 108, 101, 242, 102, 165, 79, 245, 194, 214, 21, 123, 110, 70, 233, 178, 187, 136, 48,
|
||||
197, 52, 49, 52, 12, 242, 172, 145, 33, 71, 191, 156, 182, 40, 46, 113, 185, 15, 215, 241, 119, 150, 252, 49,
|
||||
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,
|
||||
208, 248, 226, 121, 31, 61, 208, 180, 3, 197, 130, 231, 125, 31, 123, 94, 241, 4, 252, 207, 112, 101, 87, 253,
|
||||
235, 71, 48, 250, 41, 122, 161, 170, 117, 31, 66, 112, 11, 46, 174, 171, 154, 177, 11, 151, 219, 208, 241, 148,
|
||||
213, 29, 189, 36, 249, 101, 251, 82, 41, 120, 63, 147, 111, 74, 64, 251, 45, 180, 44, 150, 231, 86, 185, 231,
|
||||
244, 1, 102, 101, 86, 209, 27, 216, 63, 128, 222, 180, 100, 47, 213, 121, 119, 115, 237, 220, 254, 173, 169,
|
||||
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,
|
||||
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,
|
||||
88, 116, 67, 111, 109, 109, 101, 110, 116, 0, 67, 114, 101, 97, 116, 101, 100, 32, 119, 105, 116, 104, 32, 71,
|
||||
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,
|
||||
77, 211, 95, 0, 0, 0, 0, 73, 69, 78, 68, 174, 66, 96, 130,
|
||||
}
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
pixelgl.Run(func() {
|
||||
os.Exit(m.Run())
|
||||
})
|
||||
}
|
||||
|
||||
func TestSprite_Draw(t *testing.T) {
|
||||
img, _, err := image.Decode(bytes.NewReader(onePixelImage))
|
||||
if err != nil {
|
||||
t.Fatalf("Could not decode image: %v", err)
|
||||
}
|
||||
pic := pixel.PictureDataFromImage(img)
|
||||
|
||||
sprite := pixel.NewSprite(pic, pic.Bounds())
|
||||
|
||||
cfg := pixelgl.WindowConfig{
|
||||
Title: "testing",
|
||||
Bounds: pixel.R(0, 0, 150, 150),
|
||||
Invisible: true,
|
||||
}
|
||||
|
||||
win, err := pixelgl.NewWindow(cfg)
|
||||
if err != nil {
|
||||
t.Fatalf("Could not create window: %v", err)
|
||||
}
|
||||
|
||||
sprite.Draw(win, pixel.IM)
|
||||
}
|
|
@ -17,7 +17,7 @@ import (
|
|||
// It supports TrianglesPosition, TrianglesColor, TrianglesPicture and PictureColor.
|
||||
type Canvas struct {
|
||||
gf *GLFrame
|
||||
shader *glShader
|
||||
shader *GLShader
|
||||
|
||||
cmp pixel.ComposeMethod
|
||||
mat mgl32.Mat3
|
||||
|
@ -37,9 +37,8 @@ func NewCanvas(bounds pixel.Rect) *Canvas {
|
|||
col: mgl32.Vec4{1, 1, 1, 1},
|
||||
}
|
||||
|
||||
baseShader(c)
|
||||
c.shader = NewGLShader(baseCanvasFragmentShader)
|
||||
c.SetBounds(bounds)
|
||||
c.shader.update()
|
||||
return c
|
||||
}
|
||||
|
||||
|
@ -47,22 +46,28 @@ func NewCanvas(bounds pixel.Rect) *Canvas {
|
|||
// attribute variable. If the uniform already exists, including defaults, they will be reassigned
|
||||
// to the new value. The value can be a pointer.
|
||||
func (c *Canvas) SetUniform(name string, value interface{}) {
|
||||
c.shader.setUniform(name, value)
|
||||
c.shader.SetUniform(name, value)
|
||||
}
|
||||
|
||||
// SetFragmentShader allows you to set a new fragment shader on the underlying
|
||||
// framebuffer. Argument "src" is the GLSL source, not a filename.
|
||||
func (c *Canvas) SetFragmentShader(src string) {
|
||||
c.shader.fs = src
|
||||
c.shader.update()
|
||||
c.shader.Update()
|
||||
}
|
||||
|
||||
// MakeTriangles creates a specialized copy of the supplied Triangles that draws onto this Canvas.
|
||||
//
|
||||
// TrianglesPosition, TrianglesColor and TrianglesPicture are supported.
|
||||
func (c *Canvas) MakeTriangles(t pixel.Triangles) pixel.TargetTriangles {
|
||||
if gt, ok := t.(*GLTriangles); ok {
|
||||
return &canvasTriangles{
|
||||
GLTriangles: NewGLTriangles(c.shader.s, t),
|
||||
GLTriangles: gt,
|
||||
dst: c,
|
||||
}
|
||||
}
|
||||
return &canvasTriangles{
|
||||
GLTriangles: NewGLTriangles(c.shader, t),
|
||||
dst: c,
|
||||
}
|
||||
}
|
||||
|
@ -128,7 +133,7 @@ func (c *Canvas) SetBounds(bounds pixel.Rect) {
|
|||
c.sprite = pixel.NewSprite(nil, pixel.Rect{})
|
||||
}
|
||||
c.sprite.Set(c, c.Bounds())
|
||||
//c.sprite.SetMatrix(pixel.IM.Moved(c.Bounds().Center()))
|
||||
// c.sprite.SetMatrix(pixel.IM.Moved(c.Bounds().Center()))
|
||||
}
|
||||
|
||||
// Bounds returns the rectangular bounds of the Canvas.
|
||||
|
@ -288,15 +293,15 @@ func (ct *canvasTriangles) draw(tex *glhf.Texture, bounds pixel.Rect) {
|
|||
setBlendFunc(cmp)
|
||||
|
||||
frame := ct.dst.gf.Frame()
|
||||
shader := ct.dst.shader.s
|
||||
shader := ct.shader.s
|
||||
|
||||
frame.Begin()
|
||||
shader.Begin()
|
||||
|
||||
ct.dst.shader.uniformDefaults.transform = mat
|
||||
ct.dst.shader.uniformDefaults.colormask = col
|
||||
ct.shader.uniformDefaults.transform = mat
|
||||
ct.shader.uniformDefaults.colormask = col
|
||||
dstBounds := ct.dst.Bounds()
|
||||
ct.dst.shader.uniformDefaults.bounds = mgl32.Vec4{
|
||||
ct.shader.uniformDefaults.bounds = mgl32.Vec4{
|
||||
float32(dstBounds.Min.X),
|
||||
float32(dstBounds.Min.Y),
|
||||
float32(dstBounds.W()),
|
||||
|
@ -304,15 +309,15 @@ func (ct *canvasTriangles) draw(tex *glhf.Texture, bounds pixel.Rect) {
|
|||
}
|
||||
|
||||
bx, by, bw, bh := intBounds(bounds)
|
||||
ct.dst.shader.uniformDefaults.texbounds = mgl32.Vec4{
|
||||
ct.shader.uniformDefaults.texbounds = mgl32.Vec4{
|
||||
float32(bx),
|
||||
float32(by),
|
||||
float32(bw),
|
||||
float32(bh),
|
||||
}
|
||||
|
||||
for loc, u := range ct.dst.shader.uniforms {
|
||||
ct.dst.shader.s.SetUniformAttr(loc, u.Value())
|
||||
for loc, u := range ct.shader.uniforms {
|
||||
ct.shader.s.SetUniformAttr(loc, u.Value())
|
||||
}
|
||||
|
||||
if tex == nil {
|
||||
|
@ -354,17 +359,3 @@ func (cp *canvasPicture) Draw(t pixel.TargetTriangles) {
|
|||
}
|
||||
ct.draw(cp.GLPicture.Texture(), cp.GLPicture.Bounds())
|
||||
}
|
||||
|
||||
const (
|
||||
canvasPosition int = iota
|
||||
canvasColor
|
||||
canvasTexCoords
|
||||
canvasIntensity
|
||||
)
|
||||
|
||||
var defaultCanvasVertexFormat = glhf.AttrFormat{
|
||||
canvasPosition: {Name: "aPosition", Type: glhf.Vec2},
|
||||
canvasColor: {Name: "aColor", Type: glhf.Vec4},
|
||||
canvasTexCoords: {Name: "aTexCoords", Type: glhf.Vec2},
|
||||
canvasIntensity: {Name: "aIntensity", Type: glhf.Float},
|
||||
}
|
||||
|
|
|
@ -7,10 +7,10 @@ import (
|
|||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// glShader is a type to assist with managing a canvas's underlying
|
||||
// GLShader is a type to assist with managing a canvas's underlying
|
||||
// shader configuration. This allows for customization of shaders on
|
||||
// a per canvas basis.
|
||||
type glShader struct {
|
||||
type GLShader struct {
|
||||
s *glhf.Shader
|
||||
vf, uf glhf.AttrFormat
|
||||
vs, fs string
|
||||
|
@ -22,6 +22,7 @@ type glShader struct {
|
|||
colormask mgl32.Vec4
|
||||
bounds mgl32.Vec4
|
||||
texbounds mgl32.Vec4
|
||||
cliprect mgl32.Vec4
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -32,15 +33,52 @@ type gsUniformAttr struct {
|
|||
ispointer bool
|
||||
}
|
||||
|
||||
// reinitialize GLShader data and recompile the underlying gl shader object
|
||||
func (gs *glShader) update() {
|
||||
gs.uf = nil
|
||||
for _, u := range gs.uniforms {
|
||||
gs.uf = append(gs.uf, glhf.Attr{
|
||||
Name: u.Name,
|
||||
Type: u.Type,
|
||||
})
|
||||
const (
|
||||
canvasPosition int = iota
|
||||
canvasColor
|
||||
canvasTexCoords
|
||||
canvasIntensity
|
||||
canvasClip
|
||||
)
|
||||
|
||||
var defaultCanvasVertexFormat = glhf.AttrFormat{
|
||||
canvasPosition: glhf.Attr{Name: "aPosition", Type: glhf.Vec2},
|
||||
canvasColor: glhf.Attr{Name: "aColor", Type: glhf.Vec4},
|
||||
canvasTexCoords: glhf.Attr{Name: "aTexCoords", Type: glhf.Vec2},
|
||||
canvasIntensity: glhf.Attr{Name: "aIntensity", Type: glhf.Float},
|
||||
canvasClip: glhf.Attr{Name: "aClipRect", Type: glhf.Vec4},
|
||||
}
|
||||
|
||||
// Sets up a base shader with everything needed for a Pixel
|
||||
// canvas to render correctly. The defaults can be overridden
|
||||
// by simply using the SetUniform function.
|
||||
func NewGLShader(fragmentShader string) *GLShader {
|
||||
gs := &GLShader{
|
||||
vf: defaultCanvasVertexFormat,
|
||||
vs: baseCanvasVertexShader,
|
||||
fs: fragmentShader,
|
||||
}
|
||||
|
||||
gs.SetUniform("uTransform", &gs.uniformDefaults.transform)
|
||||
gs.SetUniform("uColorMask", &gs.uniformDefaults.colormask)
|
||||
gs.SetUniform("uBounds", &gs.uniformDefaults.bounds)
|
||||
gs.SetUniform("uTexBounds", &gs.uniformDefaults.texbounds)
|
||||
|
||||
gs.Update()
|
||||
|
||||
return gs
|
||||
}
|
||||
|
||||
// Update reinitialize GLShader data and recompile the underlying gl shader object
|
||||
func (gs *GLShader) Update() {
|
||||
gs.uf = make([]glhf.Attr, len(gs.uniforms))
|
||||
for idx := range gs.uniforms {
|
||||
gs.uf[idx] = glhf.Attr{
|
||||
Name: gs.uniforms[idx].Name,
|
||||
Type: gs.uniforms[idx].Type,
|
||||
}
|
||||
}
|
||||
|
||||
var shader *glhf.Shader
|
||||
mainthread.Call(func() {
|
||||
var err error
|
||||
|
@ -59,7 +97,7 @@ func (gs *glShader) update() {
|
|||
}
|
||||
|
||||
// gets the uniform index from GLShader
|
||||
func (gs *glShader) getUniform(Name string) int {
|
||||
func (gs *GLShader) getUniform(Name string) int {
|
||||
for i, u := range gs.uniforms {
|
||||
if u.Name == Name {
|
||||
return i
|
||||
|
@ -75,7 +113,7 @@ func (gs *glShader) getUniform(Name string) int {
|
|||
//
|
||||
// utime := float32(time.Since(starttime)).Seconds())
|
||||
// mycanvas.shader.AddUniform("u_time", &utime)
|
||||
func (gs *glShader) setUniform(name string, value interface{}) {
|
||||
func (gs *GLShader) SetUniform(name string, value interface{}) {
|
||||
t, p := getAttrType(value)
|
||||
if loc := gs.getUniform(name); loc > -1 {
|
||||
gs.uniforms[loc].Name = name
|
||||
|
@ -92,24 +130,6 @@ func (gs *glShader) setUniform(name string, value interface{}) {
|
|||
})
|
||||
}
|
||||
|
||||
// Sets up a base shader with everything needed for a Pixel
|
||||
// canvas to render correctly. The defaults can be overridden
|
||||
// by simply using the SetUniform function.
|
||||
func baseShader(c *Canvas) {
|
||||
gs := &glShader{
|
||||
vf: defaultCanvasVertexFormat,
|
||||
vs: baseCanvasVertexShader,
|
||||
fs: baseCanvasFragmentShader,
|
||||
}
|
||||
|
||||
gs.setUniform("uTransform", &gs.uniformDefaults.transform)
|
||||
gs.setUniform("uColorMask", &gs.uniformDefaults.colormask)
|
||||
gs.setUniform("uBounds", &gs.uniformDefaults.bounds)
|
||||
gs.setUniform("uTexBounds", &gs.uniformDefaults.texbounds)
|
||||
|
||||
c.shader = gs
|
||||
}
|
||||
|
||||
// Value returns the attribute's concrete value. If the stored value
|
||||
// is a pointer, we return the dereferenced value.
|
||||
func (gu *gsUniformAttr) Value() interface{} {
|
||||
|
@ -222,10 +242,14 @@ in vec2 aPosition;
|
|||
in vec4 aColor;
|
||||
in vec2 aTexCoords;
|
||||
in float aIntensity;
|
||||
in vec4 aClipRect;
|
||||
in float aIsClipped;
|
||||
|
||||
out vec4 vColor;
|
||||
out vec2 vTexCoords;
|
||||
out float vIntensity;
|
||||
out vec2 vPosition;
|
||||
out vec4 vClipRect;
|
||||
|
||||
uniform mat3 uTransform;
|
||||
uniform vec4 uBounds;
|
||||
|
@ -234,9 +258,12 @@ void main() {
|
|||
vec2 transPos = (uTransform * vec3(aPosition, 1.0)).xy;
|
||||
vec2 normPos = (transPos - uBounds.xy) / uBounds.zw * 2 - vec2(1, 1);
|
||||
gl_Position = vec4(normPos, 0.0, 1.0);
|
||||
|
||||
vColor = aColor;
|
||||
vPosition = aPosition;
|
||||
vTexCoords = aTexCoords;
|
||||
vIntensity = aIntensity;
|
||||
vClipRect = aClipRect;
|
||||
}
|
||||
`
|
||||
|
||||
|
@ -246,6 +273,7 @@ var baseCanvasFragmentShader = `
|
|||
in vec4 vColor;
|
||||
in vec2 vTexCoords;
|
||||
in float vIntensity;
|
||||
in vec4 vClipRect;
|
||||
|
||||
out vec4 fragColor;
|
||||
|
||||
|
@ -254,6 +282,9 @@ uniform vec4 uTexBounds;
|
|||
uniform sampler2D uTexture;
|
||||
|
||||
void main() {
|
||||
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))
|
||||
discard;
|
||||
|
||||
if (vIntensity == 0) {
|
||||
fragColor = uColorMask * vColor;
|
||||
} else {
|
||||
|
|
|
@ -15,23 +15,43 @@ import (
|
|||
type GLTriangles struct {
|
||||
vs *glhf.VertexSlice
|
||||
data []float32
|
||||
shader *glhf.Shader
|
||||
shader *GLShader
|
||||
}
|
||||
|
||||
var (
|
||||
_ pixel.TrianglesPosition = (*GLTriangles)(nil)
|
||||
_ pixel.TrianglesColor = (*GLTriangles)(nil)
|
||||
_ pixel.TrianglesPicture = (*GLTriangles)(nil)
|
||||
_ pixel.TrianglesClipped = (*GLTriangles)(nil)
|
||||
)
|
||||
|
||||
// The following is a helper so that the indices of
|
||||
// each of these items is easier to see/debug.
|
||||
const (
|
||||
triPosX = iota
|
||||
triPosY
|
||||
triColorR
|
||||
triColorG
|
||||
triColorB
|
||||
triColorA
|
||||
triPicX
|
||||
triPicY
|
||||
triIntensity
|
||||
triClipMinX
|
||||
triClipMinY
|
||||
triClipMaxX
|
||||
triClipMaxY
|
||||
trisAttrLen
|
||||
)
|
||||
|
||||
// NewGLTriangles returns GLTriangles initialized with the data from the supplied Triangles.
|
||||
//
|
||||
// Only draw the Triangles using the provided Shader.
|
||||
func NewGLTriangles(shader *glhf.Shader, t pixel.Triangles) *GLTriangles {
|
||||
func NewGLTriangles(shader *GLShader, t pixel.Triangles) *GLTriangles {
|
||||
var gt *GLTriangles
|
||||
mainthread.Call(func() {
|
||||
gt = &GLTriangles{
|
||||
vs: glhf.MakeVertexSlice(shader, 0, t.Len()),
|
||||
vs: glhf.MakeVertexSlice(shader.s, 0, t.Len()),
|
||||
shader: shader,
|
||||
}
|
||||
})
|
||||
|
@ -48,7 +68,7 @@ func (gt *GLTriangles) VertexSlice() *glhf.VertexSlice {
|
|||
}
|
||||
|
||||
// Shader returns the GLTriangles's associated shader.
|
||||
func (gt *GLTriangles) Shader() *glhf.Shader {
|
||||
func (gt *GLTriangles) Shader() *GLShader {
|
||||
return gt.shader
|
||||
}
|
||||
|
||||
|
@ -70,6 +90,7 @@ func (gt *GLTriangles) SetLen(length int) {
|
|||
1, 1, 1, 1,
|
||||
0, 0,
|
||||
0,
|
||||
0, 0, 0, 0,
|
||||
)
|
||||
}
|
||||
case length < gt.Len():
|
||||
|
@ -77,7 +98,7 @@ func (gt *GLTriangles) SetLen(length int) {
|
|||
default:
|
||||
return
|
||||
}
|
||||
mainthread.CallNonBlock(func() {
|
||||
mainthread.Call(func() {
|
||||
gt.vs.Begin()
|
||||
gt.vs.SetLen(length)
|
||||
gt.vs.End()
|
||||
|
@ -110,17 +131,22 @@ func (gt *GLTriangles) updateData(t pixel.Triangles) {
|
|||
col = (*t)[i].Color
|
||||
tx, ty = (*t)[i].Picture.XY()
|
||||
in = (*t)[i].Intensity
|
||||
rec = (*t)[i].ClipRect
|
||||
)
|
||||
d := gt.data[i*stride : i*stride+9]
|
||||
d[0] = float32(px)
|
||||
d[1] = float32(py)
|
||||
d[2] = float32(col.R)
|
||||
d[3] = float32(col.G)
|
||||
d[4] = float32(col.B)
|
||||
d[5] = float32(col.A)
|
||||
d[6] = float32(tx)
|
||||
d[7] = float32(ty)
|
||||
d[8] = float32(in)
|
||||
d := gt.data[i*stride : i*stride+trisAttrLen]
|
||||
d[triPosX] = float32(px)
|
||||
d[triPosY] = float32(py)
|
||||
d[triColorR] = float32(col.R)
|
||||
d[triColorG] = float32(col.G)
|
||||
d[triColorB] = float32(col.B)
|
||||
d[triColorA] = float32(col.A)
|
||||
d[triPicX] = float32(tx)
|
||||
d[triPicY] = float32(ty)
|
||||
d[triIntensity] = float32(in)
|
||||
d[triClipMinX] = float32(rec.Min.X)
|
||||
d[triClipMinY] = float32(rec.Min.Y)
|
||||
d[triClipMaxX] = float32(rec.Max.X)
|
||||
d[triClipMaxY] = float32(rec.Max.Y)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
@ -128,25 +154,34 @@ func (gt *GLTriangles) updateData(t pixel.Triangles) {
|
|||
if t, ok := t.(pixel.TrianglesPosition); ok {
|
||||
for i := 0; i < length; i++ {
|
||||
px, py := t.Position(i).XY()
|
||||
gt.data[i*stride+0] = float32(px)
|
||||
gt.data[i*stride+1] = float32(py)
|
||||
gt.data[i*stride+triPosX] = float32(px)
|
||||
gt.data[i*stride+triPosY] = float32(py)
|
||||
}
|
||||
}
|
||||
if t, ok := t.(pixel.TrianglesColor); ok {
|
||||
for i := 0; i < length; i++ {
|
||||
col := t.Color(i)
|
||||
gt.data[i*stride+2] = float32(col.R)
|
||||
gt.data[i*stride+3] = float32(col.G)
|
||||
gt.data[i*stride+4] = float32(col.B)
|
||||
gt.data[i*stride+5] = float32(col.A)
|
||||
gt.data[i*stride+triColorR] = float32(col.R)
|
||||
gt.data[i*stride+triColorG] = float32(col.G)
|
||||
gt.data[i*stride+triColorB] = float32(col.B)
|
||||
gt.data[i*stride+triColorA] = float32(col.A)
|
||||
}
|
||||
}
|
||||
if t, ok := t.(pixel.TrianglesPicture); ok {
|
||||
for i := 0; i < length; i++ {
|
||||
pic, intensity := t.Picture(i)
|
||||
gt.data[i*stride+6] = float32(pic.X)
|
||||
gt.data[i*stride+7] = float32(pic.Y)
|
||||
gt.data[i*stride+8] = float32(intensity)
|
||||
gt.data[i*stride+triPicX] = float32(pic.X)
|
||||
gt.data[i*stride+triPicY] = float32(pic.Y)
|
||||
gt.data[i*stride+triIntensity] = float32(intensity)
|
||||
}
|
||||
}
|
||||
if t, ok := t.(pixel.TrianglesClipped); ok {
|
||||
for i := 0; i < length; i++ {
|
||||
rect, _ := t.ClipRect(i)
|
||||
gt.data[i*stride+triClipMinX] = float32(rect.Min.X)
|
||||
gt.data[i*stride+triClipMinY] = float32(rect.Min.Y)
|
||||
gt.data[i*stride+triClipMaxX] = float32(rect.Max.X)
|
||||
gt.data[i*stride+triClipMaxY] = float32(rect.Max.Y)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -160,6 +195,12 @@ func (gt *GLTriangles) Update(t pixel.Triangles) {
|
|||
}
|
||||
gt.updateData(t)
|
||||
|
||||
// Copy the verteces down to the glhf.VertexData
|
||||
gt.CopyVertices()
|
||||
}
|
||||
|
||||
// CopyVertices copies the GLTriangle data down to the vertex data.
|
||||
func (gt *GLTriangles) CopyVertices() {
|
||||
// this code is supposed to copy the vertex data and CallNonBlock the update if
|
||||
// the data is small enough, otherwise it'll block and not copy the data
|
||||
if len(gt.data) < 256 { // arbitrary heurestic constant
|
||||
|
@ -185,19 +226,31 @@ func (gt *GLTriangles) Copy() pixel.Triangles {
|
|||
return NewGLTriangles(gt.shader, gt)
|
||||
}
|
||||
|
||||
// index is a helper function that returns the index in the data
|
||||
// slice given the i-th vertex and the item index.
|
||||
func (gt *GLTriangles) index(i, idx int) int {
|
||||
return i*gt.vs.Stride() + idx
|
||||
}
|
||||
|
||||
// Position returns the Position property of the i-th vertex.
|
||||
func (gt *GLTriangles) Position(i int) pixel.Vec {
|
||||
px := gt.data[i*gt.vs.Stride()+0]
|
||||
py := gt.data[i*gt.vs.Stride()+1]
|
||||
px := gt.data[gt.index(i, triPosX)]
|
||||
py := gt.data[gt.index(i, triPosY)]
|
||||
return pixel.V(float64(px), float64(py))
|
||||
}
|
||||
|
||||
// SetPosition sets the position property of the i-th vertex.
|
||||
func (gt *GLTriangles) SetPosition(i int, p pixel.Vec) {
|
||||
gt.data[gt.index(i, triPosX)] = float32(p.X)
|
||||
gt.data[gt.index(i, triPosY)] = float32(p.Y)
|
||||
}
|
||||
|
||||
// Color returns the Color property of the i-th vertex.
|
||||
func (gt *GLTriangles) Color(i int) pixel.RGBA {
|
||||
r := gt.data[i*gt.vs.Stride()+2]
|
||||
g := gt.data[i*gt.vs.Stride()+3]
|
||||
b := gt.data[i*gt.vs.Stride()+4]
|
||||
a := gt.data[i*gt.vs.Stride()+5]
|
||||
r := gt.data[gt.index(i, triColorR)]
|
||||
g := gt.data[gt.index(i, triColorG)]
|
||||
b := gt.data[gt.index(i, triColorB)]
|
||||
a := gt.data[gt.index(i, triColorA)]
|
||||
return pixel.RGBA{
|
||||
R: float64(r),
|
||||
G: float64(g),
|
||||
|
@ -206,10 +259,44 @@ func (gt *GLTriangles) Color(i int) pixel.RGBA {
|
|||
}
|
||||
}
|
||||
|
||||
// SetColor sets the color property of the i-th vertex.
|
||||
func (gt *GLTriangles) SetColor(i int, c pixel.RGBA) {
|
||||
gt.data[gt.index(i, triColorR)] = float32(c.R)
|
||||
gt.data[gt.index(i, triColorG)] = float32(c.G)
|
||||
gt.data[gt.index(i, triColorB)] = float32(c.B)
|
||||
gt.data[gt.index(i, triColorA)] = float32(c.A)
|
||||
}
|
||||
|
||||
// Picture returns the Picture property of the i-th vertex.
|
||||
func (gt *GLTriangles) Picture(i int) (pic pixel.Vec, intensity float64) {
|
||||
tx := gt.data[i*gt.vs.Stride()+6]
|
||||
ty := gt.data[i*gt.vs.Stride()+7]
|
||||
intensity = float64(gt.data[i*gt.vs.Stride()+8])
|
||||
tx := gt.data[gt.index(i, triPicX)]
|
||||
ty := gt.data[gt.index(i, triPicY)]
|
||||
intensity = float64(gt.data[gt.index(i, triIntensity)])
|
||||
return pixel.V(float64(tx), float64(ty)), intensity
|
||||
}
|
||||
|
||||
// SetPicture sets the picture property of the i-th vertex.
|
||||
func (gt *GLTriangles) SetPicture(i int, pic pixel.Vec, intensity float64) {
|
||||
gt.data[gt.index(i, triPicX)] = float32(pic.X)
|
||||
gt.data[gt.index(i, triPicY)] = float32(pic.Y)
|
||||
gt.data[gt.index(i, triIntensity)] = float32(intensity)
|
||||
}
|
||||
|
||||
// ClipRect returns the Clipping rectangle property of the i-th vertex.
|
||||
func (gt *GLTriangles) ClipRect(i int) (rect pixel.Rect, is bool) {
|
||||
mx := gt.data[gt.index(i, triClipMinX)]
|
||||
my := gt.data[gt.index(i, triClipMinY)]
|
||||
ax := gt.data[gt.index(i, triClipMaxX)]
|
||||
ay := gt.data[gt.index(i, triClipMaxY)]
|
||||
rect = pixel.R(float64(mx), float64(my), float64(ax), float64(ay))
|
||||
is = rect.Area() != 0.0
|
||||
return
|
||||
}
|
||||
|
||||
// SetClipRect sets the Clipping rectangle property of the i-th vertex.
|
||||
func (gt *GLTriangles) SetClipRect(i int, rect pixel.Rect) {
|
||||
gt.data[gt.index(i, triClipMinX)] = float32(rect.Min.X)
|
||||
gt.data[gt.index(i, triClipMinY)] = float32(rect.Min.Y)
|
||||
gt.data[gt.index(i, triClipMaxX)] = float32(rect.Max.X)
|
||||
gt.data[gt.index(i, triClipMaxY)] = float32(rect.Max.Y)
|
||||
}
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
package pixelgl
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/faiface/mainthread"
|
||||
"github.com/faiface/pixel"
|
||||
"github.com/go-gl/glfw/v3.2/glfw"
|
||||
"github.com/go-gl/glfw/v3.3/glfw"
|
||||
)
|
||||
|
||||
// Pressed returns whether the Button is currently pressed down.
|
||||
|
@ -11,14 +13,14 @@ func (w *Window) Pressed(button Button) bool {
|
|||
return w.currInp.buttons[button]
|
||||
}
|
||||
|
||||
// JustPressed returns whether the Button has just been pressed down.
|
||||
// JustPressed returns whether the Button has been pressed in the last frame.
|
||||
func (w *Window) JustPressed(button Button) bool {
|
||||
return w.currInp.buttons[button] && !w.prevInp.buttons[button]
|
||||
return w.pressEvents[button]
|
||||
}
|
||||
|
||||
// JustReleased returns whether the Button has just been released up.
|
||||
// JustReleased returns whether the Button has been released in the last frame.
|
||||
func (w *Window) JustReleased(button Button) bool {
|
||||
return !w.currInp.buttons[button] && w.prevInp.buttons[button]
|
||||
return w.releaseEvents[button]
|
||||
}
|
||||
|
||||
// Repeated returns whether a repeat event has been triggered on button.
|
||||
|
@ -33,6 +35,32 @@ func (w *Window) MousePosition() pixel.Vec {
|
|||
return w.currInp.mouse
|
||||
}
|
||||
|
||||
// MousePreviousPosition returns the previous mouse position in the Window's Bounds.
|
||||
func (w *Window) MousePreviousPosition() pixel.Vec {
|
||||
return w.prevInp.mouse
|
||||
}
|
||||
|
||||
// SetMousePosition positions the mouse cursor anywhere within the Window's Bounds.
|
||||
func (w *Window) SetMousePosition(v pixel.Vec) {
|
||||
mainthread.Call(func() {
|
||||
if (v.X >= 0 && v.X <= w.bounds.W()) &&
|
||||
(v.Y >= 0 && v.Y <= w.bounds.H()) {
|
||||
w.window.SetCursorPos(
|
||||
v.X+w.bounds.Min.X,
|
||||
(w.bounds.H()-v.Y)+w.bounds.Min.Y,
|
||||
)
|
||||
w.prevInp.mouse = v
|
||||
w.currInp.mouse = v
|
||||
w.tempInp.mouse = v
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// MouseInsideWindow returns true if the mouse position is within the Window's Bounds.
|
||||
func (w *Window) MouseInsideWindow() bool {
|
||||
return w.cursorInsideWindow
|
||||
}
|
||||
|
||||
// MouseScroll returns the mouse scroll amount (in both axes) since the last call to Window.Update.
|
||||
func (w *Window) MouseScroll() pixel.Vec {
|
||||
return w.currInp.scroll
|
||||
|
@ -334,8 +362,10 @@ func (w *Window) initInput() {
|
|||
w.window.SetMouseButtonCallback(func(_ *glfw.Window, button glfw.MouseButton, action glfw.Action, mod glfw.ModifierKey) {
|
||||
switch action {
|
||||
case glfw.Press:
|
||||
w.tempPressEvents[Button(button)] = true
|
||||
w.tempInp.buttons[Button(button)] = true
|
||||
case glfw.Release:
|
||||
w.tempReleaseEvents[Button(button)] = true
|
||||
w.tempInp.buttons[Button(button)] = false
|
||||
}
|
||||
})
|
||||
|
@ -346,14 +376,20 @@ func (w *Window) initInput() {
|
|||
}
|
||||
switch action {
|
||||
case glfw.Press:
|
||||
w.tempPressEvents[Button(key)] = true
|
||||
w.tempInp.buttons[Button(key)] = true
|
||||
case glfw.Release:
|
||||
w.tempReleaseEvents[Button(key)] = true
|
||||
w.tempInp.buttons[Button(key)] = false
|
||||
case glfw.Repeat:
|
||||
w.tempInp.repeat[Button(key)] = true
|
||||
}
|
||||
})
|
||||
|
||||
w.window.SetCursorEnterCallback(func(_ *glfw.Window, entered bool) {
|
||||
w.cursorInsideWindow = entered
|
||||
})
|
||||
|
||||
w.window.SetCursorPosCallback(func(_ *glfw.Window, x, y float64) {
|
||||
w.tempInp.mouse = pixel.V(
|
||||
x+w.bounds.Min.X,
|
||||
|
@ -378,11 +414,36 @@ func (w *Window) UpdateInput() {
|
|||
mainthread.Call(func() {
|
||||
glfw.PollEvents()
|
||||
})
|
||||
w.doUpdateInput()
|
||||
}
|
||||
|
||||
// UpdateInputWait blocks until an event is received or a timeout. If timeout is 0
|
||||
// then it will wait indefinitely
|
||||
func (w *Window) UpdateInputWait(timeout time.Duration) {
|
||||
mainthread.Call(func() {
|
||||
if timeout <= 0 {
|
||||
glfw.WaitEvents()
|
||||
} else {
|
||||
glfw.WaitEventsTimeout(timeout.Seconds())
|
||||
}
|
||||
})
|
||||
w.doUpdateInput()
|
||||
}
|
||||
|
||||
// internal input bookkeeping
|
||||
func (w *Window) doUpdateInput() {
|
||||
w.prevInp = w.currInp
|
||||
w.currInp = w.tempInp
|
||||
|
||||
w.pressEvents = w.tempPressEvents
|
||||
w.releaseEvents = w.tempReleaseEvents
|
||||
|
||||
// Clear last frame's temporary status
|
||||
w.tempPressEvents = [KeyLast + 1]bool{}
|
||||
w.tempReleaseEvents = [KeyLast + 1]bool{}
|
||||
w.tempInp.repeat = [KeyLast + 1]bool{}
|
||||
w.tempInp.scroll = pixel.ZV
|
||||
w.tempInp.typed = ""
|
||||
|
||||
w.updateJoystickInput()
|
||||
}
|
||||
|
|
|
@ -0,0 +1,193 @@
|
|||
package pixelgl
|
||||
|
||||
import (
|
||||
"github.com/go-gl/glfw/v3.3/glfw"
|
||||
)
|
||||
|
||||
// Joystick is a joystick or controller (gamepad).
|
||||
type Joystick int
|
||||
|
||||
// List all of the joysticks.
|
||||
const (
|
||||
Joystick1 = Joystick(glfw.Joystick1)
|
||||
Joystick2 = Joystick(glfw.Joystick2)
|
||||
Joystick3 = Joystick(glfw.Joystick3)
|
||||
Joystick4 = Joystick(glfw.Joystick4)
|
||||
Joystick5 = Joystick(glfw.Joystick5)
|
||||
Joystick6 = Joystick(glfw.Joystick6)
|
||||
Joystick7 = Joystick(glfw.Joystick7)
|
||||
Joystick8 = Joystick(glfw.Joystick8)
|
||||
Joystick9 = Joystick(glfw.Joystick9)
|
||||
Joystick10 = Joystick(glfw.Joystick10)
|
||||
Joystick11 = Joystick(glfw.Joystick11)
|
||||
Joystick12 = Joystick(glfw.Joystick12)
|
||||
Joystick13 = Joystick(glfw.Joystick13)
|
||||
Joystick14 = Joystick(glfw.Joystick14)
|
||||
Joystick15 = Joystick(glfw.Joystick15)
|
||||
Joystick16 = Joystick(glfw.Joystick16)
|
||||
|
||||
JoystickLast = Joystick(glfw.JoystickLast)
|
||||
)
|
||||
|
||||
// GamepadAxis corresponds to a gamepad axis.
|
||||
type GamepadAxis int
|
||||
|
||||
// Gamepad axis IDs.
|
||||
const (
|
||||
AxisLeftX = GamepadAxis(glfw.AxisLeftX)
|
||||
AxisLeftY = GamepadAxis(glfw.AxisLeftY)
|
||||
AxisRightX = GamepadAxis(glfw.AxisRightX)
|
||||
AxisRightY = GamepadAxis(glfw.AxisRightY)
|
||||
AxisLeftTrigger = GamepadAxis(glfw.AxisLeftTrigger)
|
||||
AxisRightTrigger = GamepadAxis(glfw.AxisRightTrigger)
|
||||
AxisLast = GamepadAxis(glfw.AxisLast)
|
||||
)
|
||||
|
||||
// GamepadButton corresponds to a gamepad button.
|
||||
type GamepadButton int
|
||||
|
||||
// Gamepad button IDs.
|
||||
const (
|
||||
ButtonA = GamepadButton(glfw.ButtonA)
|
||||
ButtonB = GamepadButton(glfw.ButtonB)
|
||||
ButtonX = GamepadButton(glfw.ButtonX)
|
||||
ButtonY = GamepadButton(glfw.ButtonY)
|
||||
ButtonLeftBumper = GamepadButton(glfw.ButtonLeftBumper)
|
||||
ButtonRightBumper = GamepadButton(glfw.ButtonRightBumper)
|
||||
ButtonBack = GamepadButton(glfw.ButtonBack)
|
||||
ButtonStart = GamepadButton(glfw.ButtonStart)
|
||||
ButtonGuide = GamepadButton(glfw.ButtonGuide)
|
||||
ButtonLeftThumb = GamepadButton(glfw.ButtonLeftThumb)
|
||||
ButtonRightThumb = GamepadButton(glfw.ButtonRightThumb)
|
||||
ButtonDpadUp = GamepadButton(glfw.ButtonDpadUp)
|
||||
ButtonDpadRight = GamepadButton(glfw.ButtonDpadRight)
|
||||
ButtonDpadDown = GamepadButton(glfw.ButtonDpadDown)
|
||||
ButtonDpadLeft = GamepadButton(glfw.ButtonDpadLeft)
|
||||
ButtonLast = GamepadButton(glfw.ButtonLast)
|
||||
ButtonCross = GamepadButton(glfw.ButtonCross)
|
||||
ButtonCircle = GamepadButton(glfw.ButtonCircle)
|
||||
ButtonSquare = GamepadButton(glfw.ButtonSquare)
|
||||
ButtonTriangle = GamepadButton(glfw.ButtonTriangle)
|
||||
)
|
||||
|
||||
// JoystickPresent returns if the joystick is currently connected.
|
||||
//
|
||||
// This API is experimental.
|
||||
func (w *Window) JoystickPresent(js Joystick) bool {
|
||||
return w.currJoy.connected[js]
|
||||
}
|
||||
|
||||
// JoystickName returns the name of the joystick. A disconnected joystick will return an
|
||||
// empty string.
|
||||
//
|
||||
// This API is experimental.
|
||||
func (w *Window) JoystickName(js Joystick) string {
|
||||
return w.currJoy.name[js]
|
||||
}
|
||||
|
||||
// JoystickButtonCount returns the number of buttons a connected joystick has.
|
||||
//
|
||||
// This API is experimental.
|
||||
func (w *Window) JoystickButtonCount(js Joystick) int {
|
||||
return len(w.currJoy.buttons[js])
|
||||
}
|
||||
|
||||
// JoystickAxisCount returns the number of axes a connected joystick has.
|
||||
//
|
||||
// This API is experimental.
|
||||
func (w *Window) JoystickAxisCount(js Joystick) int {
|
||||
return len(w.currJoy.axis[js])
|
||||
}
|
||||
|
||||
// JoystickPressed returns whether the joystick Button is currently pressed down.
|
||||
// If the button index is out of range, this will return false.
|
||||
//
|
||||
// This API is experimental.
|
||||
func (w *Window) JoystickPressed(js Joystick, button GamepadButton) bool {
|
||||
return w.currJoy.getButton(js, int(button))
|
||||
}
|
||||
|
||||
// JoystickJustPressed returns whether the joystick Button has just been pressed down.
|
||||
// If the button index is out of range, this will return false.
|
||||
//
|
||||
// This API is experimental.
|
||||
func (w *Window) JoystickJustPressed(js Joystick, button GamepadButton) bool {
|
||||
return w.currJoy.getButton(js, int(button)) && !w.prevJoy.getButton(js, int(button))
|
||||
}
|
||||
|
||||
// JoystickJustReleased returns whether the joystick Button has just been released up.
|
||||
// If the button index is out of range, this will return false.
|
||||
//
|
||||
// This API is experimental.
|
||||
func (w *Window) JoystickJustReleased(js Joystick, button GamepadButton) bool {
|
||||
return !w.currJoy.getButton(js, int(button)) && w.prevJoy.getButton(js, int(button))
|
||||
}
|
||||
|
||||
// JoystickAxis returns the value of a joystick axis at the last call to Window.Update.
|
||||
// If the axis index is out of range, this will return 0.
|
||||
//
|
||||
// This API is experimental.
|
||||
func (w *Window) JoystickAxis(js Joystick, axis GamepadAxis) float64 {
|
||||
return w.currJoy.getAxis(js, int(axis))
|
||||
}
|
||||
|
||||
// Used internally during Window.UpdateInput to update the state of the joysticks.
|
||||
func (w *Window) updateJoystickInput() {
|
||||
for js := Joystick1; js <= JoystickLast; js++ {
|
||||
// Determine and store if the joystick was connected
|
||||
joystickPresent := glfw.Joystick(js).Present()
|
||||
w.tempJoy.connected[js] = joystickPresent
|
||||
|
||||
if joystickPresent {
|
||||
if glfw.Joystick(js).IsGamepad() {
|
||||
gamepadInputs := glfw.Joystick(js).GetGamepadState()
|
||||
|
||||
w.tempJoy.buttons[js] = gamepadInputs.Buttons[:]
|
||||
w.tempJoy.axis[js] = gamepadInputs.Axes[:]
|
||||
} else {
|
||||
w.tempJoy.buttons[js] = glfw.Joystick(js).GetButtons()
|
||||
w.tempJoy.axis[js] = glfw.Joystick(js).GetAxes()
|
||||
}
|
||||
|
||||
if !w.currJoy.connected[js] {
|
||||
// The joystick was recently connected, we get the name
|
||||
w.tempJoy.name[js] = glfw.Joystick(js).GetName()
|
||||
} else {
|
||||
// Use the name from the previous one
|
||||
w.tempJoy.name[js] = w.currJoy.name[js]
|
||||
}
|
||||
} else {
|
||||
w.tempJoy.buttons[js] = []glfw.Action{}
|
||||
w.tempJoy.axis[js] = []float32{}
|
||||
w.tempJoy.name[js] = ""
|
||||
}
|
||||
}
|
||||
|
||||
w.prevJoy = w.currJoy
|
||||
w.currJoy = w.tempJoy
|
||||
}
|
||||
|
||||
type joystickState struct {
|
||||
connected [JoystickLast + 1]bool
|
||||
name [JoystickLast + 1]string
|
||||
buttons [JoystickLast + 1][]glfw.Action
|
||||
axis [JoystickLast + 1][]float32
|
||||
}
|
||||
|
||||
// Returns if a button on a joystick is down, returning false if the button or joystick is invalid.
|
||||
func (js *joystickState) getButton(joystick Joystick, button int) bool {
|
||||
// Check that the joystick and button is valid, return false by default
|
||||
if js.buttons[joystick] == nil || button >= len(js.buttons[joystick]) || button < 0 {
|
||||
return false
|
||||
}
|
||||
return js.buttons[joystick][byte(button)] == glfw.Press
|
||||
}
|
||||
|
||||
// Returns the value of a joystick axis, returning 0 if the button or joystick is invalid.
|
||||
func (js *joystickState) getAxis(joystick Joystick, axis int) float64 {
|
||||
// Check that the joystick and axis is valid, return 0 by default.
|
||||
if js.axis[joystick] == nil || axis >= len(js.axis[joystick]) || axis < 0 {
|
||||
return 0
|
||||
}
|
||||
return float64(js.axis[joystick][axis])
|
||||
}
|
|
@ -2,7 +2,7 @@ package pixelgl
|
|||
|
||||
import (
|
||||
"github.com/faiface/mainthread"
|
||||
"github.com/go-gl/glfw/v3.2/glfw"
|
||||
"github.com/go-gl/glfw/v3.3/glfw"
|
||||
)
|
||||
|
||||
// Monitor represents a physical display attached to your computer.
|
||||
|
|
|
@ -2,7 +2,7 @@ package pixelgl
|
|||
|
||||
import (
|
||||
"github.com/faiface/mainthread"
|
||||
"github.com/go-gl/glfw/v3.2/glfw"
|
||||
"github.com/go-gl/glfw/v3.3/glfw"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package pixelgl
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"image"
|
||||
"image/color"
|
||||
"runtime"
|
||||
|
@ -8,7 +9,8 @@ import (
|
|||
"github.com/faiface/glhf"
|
||||
"github.com/faiface/mainthread"
|
||||
"github.com/faiface/pixel"
|
||||
"github.com/go-gl/glfw/v3.2/glfw"
|
||||
"github.com/go-gl/gl/v3.3-core/gl"
|
||||
"github.com/go-gl/glfw/v3.3/glfw"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
|
@ -36,19 +38,48 @@ type WindowConfig struct {
|
|||
// Bounds specify the bounds of the Window in pixels.
|
||||
Bounds pixel.Rect
|
||||
|
||||
// Initial window position
|
||||
Position pixel.Vec
|
||||
|
||||
// If set to nil, the Window will be windowed. Otherwise it will be fullscreen on the
|
||||
// specified Monitor.
|
||||
Monitor *Monitor
|
||||
|
||||
// Whether the Window is resizable.
|
||||
// Resizable specifies whether the window will be resizable by the user.
|
||||
Resizable bool
|
||||
|
||||
// Undecorated Window ommits the borders and decorations (close button, etc.).
|
||||
// Undecorated Window omits the borders and decorations (close button, etc.).
|
||||
Undecorated bool
|
||||
|
||||
// NoIconify specifies whether fullscreen windows should not automatically
|
||||
// iconify (and restore the previous video mode) on focus loss.
|
||||
NoIconify bool
|
||||
|
||||
// AlwaysOnTop specifies whether the windowed mode window will be floating
|
||||
// above other regular windows, also called topmost or always-on-top.
|
||||
// This is intended primarily for debugging purposes and cannot be used to
|
||||
// implement proper full screen windows.
|
||||
AlwaysOnTop bool
|
||||
|
||||
// TransparentFramebuffer specifies whether the window framebuffer will be
|
||||
// transparent. If enabled and supported by the system, the window
|
||||
// framebuffer alpha channel will be used to combine the framebuffer with
|
||||
// the background. This does not affect window decorations.
|
||||
TransparentFramebuffer bool
|
||||
|
||||
// VSync (vertical synchronization) synchronizes Window's framerate with the framerate of
|
||||
// the monitor.
|
||||
VSync bool
|
||||
|
||||
// Maximized specifies whether the window is maximized.
|
||||
Maximized bool
|
||||
|
||||
// Invisible specifies whether the window will be initially hidden.
|
||||
// You can make the window visible later using Window.Show().
|
||||
Invisible bool
|
||||
|
||||
//SamplesMSAA specifies the level of MSAA to be used. Must be one of 0, 2, 4, 8, 16. 0 to disable.
|
||||
SamplesMSAA int
|
||||
}
|
||||
|
||||
// Window is a window handler. Use this type to manipulate a window (input, drawing, etc.).
|
||||
|
@ -59,6 +90,7 @@ type Window struct {
|
|||
canvas *Canvas
|
||||
vsync bool
|
||||
cursorVisible bool
|
||||
cursorInsideWindow bool
|
||||
|
||||
// need to save these to correctly restore a fullscreen window
|
||||
restore struct {
|
||||
|
@ -72,6 +104,11 @@ type Window struct {
|
|||
scroll pixel.Vec
|
||||
typed string
|
||||
}
|
||||
|
||||
pressEvents, tempPressEvents [KeyLast + 1]bool
|
||||
releaseEvents, tempReleaseEvents [KeyLast + 1]bool
|
||||
|
||||
prevJoy, currJoy, tempJoy joystickState
|
||||
}
|
||||
|
||||
var currWin *Window
|
||||
|
@ -87,6 +124,17 @@ func NewWindow(cfg WindowConfig) (*Window, error) {
|
|||
|
||||
w := &Window{bounds: cfg.Bounds, cursorVisible: true}
|
||||
|
||||
flag := false
|
||||
for _, v := range []int{0, 2, 4, 8, 16} {
|
||||
if cfg.SamplesMSAA == v {
|
||||
flag = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !flag {
|
||||
return nil, fmt.Errorf("invalid value '%v' for msaaSamples", cfg.SamplesMSAA)
|
||||
}
|
||||
|
||||
err := mainthread.CallErr(func() error {
|
||||
var err error
|
||||
|
||||
|
@ -97,6 +145,16 @@ func NewWindow(cfg WindowConfig) (*Window, error) {
|
|||
|
||||
glfw.WindowHint(glfw.Resizable, bool2int[cfg.Resizable])
|
||||
glfw.WindowHint(glfw.Decorated, bool2int[!cfg.Undecorated])
|
||||
glfw.WindowHint(glfw.Floating, bool2int[cfg.AlwaysOnTop])
|
||||
glfw.WindowHint(glfw.AutoIconify, bool2int[!cfg.NoIconify])
|
||||
glfw.WindowHint(glfw.TransparentFramebuffer, bool2int[cfg.TransparentFramebuffer])
|
||||
glfw.WindowHint(glfw.Maximized, bool2int[cfg.Maximized])
|
||||
glfw.WindowHint(glfw.Visible, bool2int[!cfg.Invisible])
|
||||
glfw.WindowHint(glfw.Samples, cfg.SamplesMSAA)
|
||||
|
||||
if cfg.Position.X != 0 || cfg.Position.Y != 0 {
|
||||
glfw.WindowHint(glfw.Visible, glfw.False)
|
||||
}
|
||||
|
||||
var share *glfw.Window
|
||||
if currWin != nil {
|
||||
|
@ -114,9 +172,15 @@ func NewWindow(cfg WindowConfig) (*Window, error) {
|
|||
return err
|
||||
}
|
||||
|
||||
if cfg.Position.X != 0 || cfg.Position.Y != 0 {
|
||||
w.window.SetPos(int(cfg.Position.X), int(cfg.Position.Y))
|
||||
w.window.Show()
|
||||
}
|
||||
|
||||
// enter the OpenGL context
|
||||
w.begin()
|
||||
glhf.Init()
|
||||
gl.Enable(gl.MULTISAMPLE)
|
||||
w.end()
|
||||
|
||||
return nil
|
||||
|
@ -158,6 +222,24 @@ func (w *Window) Destroy() {
|
|||
|
||||
// Update swaps buffers and polls events. Call this method at the end of each frame.
|
||||
func (w *Window) Update() {
|
||||
w.SwapBuffers()
|
||||
w.UpdateInput()
|
||||
}
|
||||
|
||||
// ClipboardText returns the current value of the systems clipboard.
|
||||
func (w *Window) ClipboardText() string {
|
||||
return w.window.GetClipboardString()
|
||||
}
|
||||
|
||||
// SetClipboardText passes the given string to the underlying glfw window to set the
|
||||
// systems clipboard.
|
||||
func (w *Window) SetClipboardText(text string) {
|
||||
w.window.SetClipboardString(text)
|
||||
}
|
||||
|
||||
// SwapBuffers swaps buffers. Call this to swap buffers without polling window events.
|
||||
// Note that Update invokes SwapBuffers.
|
||||
func (w *Window) SwapBuffers() {
|
||||
mainthread.Call(func() {
|
||||
_, _, oldW, oldH := intBounds(w.bounds)
|
||||
newW, newH := w.window.GetSize()
|
||||
|
@ -192,8 +274,6 @@ func (w *Window) Update() {
|
|||
w.window.SwapBuffers()
|
||||
w.end()
|
||||
})
|
||||
|
||||
w.UpdateInput()
|
||||
}
|
||||
|
||||
// SetClosed sets the closed flag of the Window.
|
||||
|
@ -354,6 +434,15 @@ func (w *Window) SetCursorVisible(visible bool) {
|
|||
})
|
||||
}
|
||||
|
||||
// SetCursorDisabled hides the cursor and provides unlimited virtual cursor movement
|
||||
// make cursor visible using SetCursorVisible
|
||||
func (w *Window) SetCursorDisabled() {
|
||||
w.cursorVisible = false
|
||||
mainthread.Call(func() {
|
||||
w.window.SetInputMode(glfw.CursorMode, glfw.CursorDisabled)
|
||||
})
|
||||
}
|
||||
|
||||
// CursorVisible returns the visibility status of the mouse cursor.
|
||||
func (w *Window) CursorVisible() bool {
|
||||
return w.cursorVisible
|
||||
|
@ -429,3 +518,27 @@ func (w *Window) Color(at pixel.Vec) pixel.RGBA {
|
|||
func (w *Window) Canvas() *Canvas {
|
||||
return w.canvas
|
||||
}
|
||||
|
||||
// Show makes the window visible, if it was previously hidden. If the window is
|
||||
// already visible or is in full screen mode, this function does nothing.
|
||||
func (w *Window) Show() {
|
||||
mainthread.Call(func() {
|
||||
w.window.Show()
|
||||
})
|
||||
}
|
||||
|
||||
// Clipboard returns the contents of the system clipboard.
|
||||
func (w *Window) Clipboard() string {
|
||||
var clipboard string
|
||||
mainthread.Call(func() {
|
||||
clipboard = w.window.GetClipboardString()
|
||||
})
|
||||
return clipboard
|
||||
}
|
||||
|
||||
// SetClipboardString sets the system clipboard to the specified UTF-8 encoded string.
|
||||
func (w *Window) SetClipboard(str string) {
|
||||
mainthread.Call(func() {
|
||||
w.window.SetClipboardString(str)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -0,0 +1,284 @@
|
|||
package pixel
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
)
|
||||
|
||||
// Rect is a 2D rectangle aligned with the axes of the coordinate system. It is defined by two
|
||||
// points, Min and Max.
|
||||
//
|
||||
// The invariant should hold, that Max's components are greater or equal than Min's components
|
||||
// respectively.
|
||||
type Rect struct {
|
||||
Min, Max Vec
|
||||
}
|
||||
|
||||
// ZR is a zero rectangle.
|
||||
var ZR = Rect{Min: ZV, Max: ZV}
|
||||
|
||||
// R returns a new Rect with given the Min and Max coordinates.
|
||||
//
|
||||
// Note that the returned rectangle is not automatically normalized.
|
||||
func R(minX, minY, maxX, maxY float64) Rect {
|
||||
return Rect{
|
||||
Min: Vec{minX, minY},
|
||||
Max: Vec{maxX, maxY},
|
||||
}
|
||||
}
|
||||
|
||||
// String returns the string representation of the Rect.
|
||||
//
|
||||
// r := pixel.R(100, 50, 200, 300)
|
||||
// r.String() // returns "Rect(100, 50, 200, 300)"
|
||||
// fmt.Println(r) // Rect(100, 50, 200, 300)
|
||||
func (r Rect) String() string {
|
||||
return fmt.Sprintf("Rect(%v, %v, %v, %v)", r.Min.X, r.Min.Y, r.Max.X, r.Max.Y)
|
||||
}
|
||||
|
||||
// Norm returns the Rect in normal form, such that Max is component-wise greater or equal than Min.
|
||||
func (r Rect) Norm() Rect {
|
||||
return Rect{
|
||||
Min: Vec{
|
||||
math.Min(r.Min.X, r.Max.X),
|
||||
math.Min(r.Min.Y, r.Max.Y),
|
||||
},
|
||||
Max: Vec{
|
||||
math.Max(r.Min.X, r.Max.X),
|
||||
math.Max(r.Min.Y, r.Max.Y),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// W returns the width of the Rect.
|
||||
func (r Rect) W() float64 {
|
||||
return r.Max.X - r.Min.X
|
||||
}
|
||||
|
||||
// H returns the height of the Rect.
|
||||
func (r Rect) H() float64 {
|
||||
return r.Max.Y - r.Min.Y
|
||||
}
|
||||
|
||||
// Size returns the vector of width and height of the Rect.
|
||||
func (r Rect) Size() Vec {
|
||||
return V(r.W(), r.H())
|
||||
}
|
||||
|
||||
// Area returns the area of r. If r is not normalized, area may be negative.
|
||||
func (r Rect) Area() float64 {
|
||||
return r.W() * r.H()
|
||||
}
|
||||
|
||||
// Edges will return the four lines which make up the edges of the rectangle.
|
||||
func (r Rect) Edges() [4]Line {
|
||||
corners := r.Vertices()
|
||||
|
||||
return [4]Line{
|
||||
{A: corners[0], B: corners[1]},
|
||||
{A: corners[1], B: corners[2]},
|
||||
{A: corners[2], B: corners[3]},
|
||||
{A: corners[3], B: corners[0]},
|
||||
}
|
||||
}
|
||||
|
||||
// Anchor is a vector used to define anchors, such as `Center`, `Top`, `TopRight`, etc.
|
||||
type Anchor Vec
|
||||
|
||||
var (
|
||||
Center = Anchor{0.5, 0.5}
|
||||
Top = Anchor{0.5, 0}
|
||||
TopRight = Anchor{0, 0}
|
||||
Right = Anchor{0, 0.5}
|
||||
BottomRight = Anchor{0, 1}
|
||||
Bottom = Anchor{0.5, 1}
|
||||
BottomLeft = Anchor{1, 1}
|
||||
Left = Anchor{1, 0.5}
|
||||
TopLeft = Anchor{1, 0}
|
||||
)
|
||||
|
||||
var anchorStrings map[Anchor]string = map[Anchor]string{
|
||||
Center: "center",
|
||||
Top: "top",
|
||||
TopRight: "top-right",
|
||||
Right: "right",
|
||||
BottomRight: "bottom-right",
|
||||
Bottom: "bottom",
|
||||
BottomLeft: "bottom-left",
|
||||
Left: "left",
|
||||
TopLeft: "top-left",
|
||||
}
|
||||
|
||||
// String returns the string representation of an anchor.
|
||||
func (anchor Anchor) String() string {
|
||||
return anchorStrings[anchor]
|
||||
}
|
||||
|
||||
var oppositeAnchors map[Anchor]Anchor = map[Anchor]Anchor{
|
||||
Center: Center,
|
||||
Top: Bottom,
|
||||
Bottom: Top,
|
||||
Right: Left,
|
||||
Left: Right,
|
||||
TopRight: BottomLeft,
|
||||
BottomLeft: TopRight,
|
||||
BottomRight: TopLeft,
|
||||
TopLeft: BottomRight,
|
||||
}
|
||||
|
||||
// Opposite returns the opposite position of the anchor (ie. Top -> Bottom; BottomLeft -> TopRight, etc.).
|
||||
func (anchor Anchor) Opposite() Anchor {
|
||||
return oppositeAnchors[anchor]
|
||||
}
|
||||
|
||||
// AnchorPos returns the relative position of the given anchor.
|
||||
func (r Rect) AnchorPos(anchor Anchor) Vec {
|
||||
return r.Size().ScaledXY(V(0, 0).Sub(Vec(anchor)))
|
||||
}
|
||||
|
||||
// AlignedTo returns the rect moved by the given anchor.
|
||||
func (rect Rect) AlignedTo(anchor Anchor) Rect {
|
||||
return rect.Moved(rect.AnchorPos(anchor))
|
||||
}
|
||||
|
||||
// Center returns the position of the center of the Rect.
|
||||
// `rect.Center()` is equivalent to `rect.Anchor(pixel.Anchor.Center)`
|
||||
func (r Rect) Center() Vec {
|
||||
return Lerp(r.Min, r.Max, 0.5)
|
||||
}
|
||||
|
||||
// Moved returns the Rect moved (both Min and Max) by the given vector delta.
|
||||
func (r Rect) Moved(delta Vec) Rect {
|
||||
return Rect{
|
||||
Min: r.Min.Add(delta),
|
||||
Max: r.Max.Add(delta),
|
||||
}
|
||||
}
|
||||
|
||||
// Resized returns the Rect resized to the given size while keeping the position of the given
|
||||
// anchor.
|
||||
//
|
||||
// r.Resized(r.Min, size) // resizes while keeping the position of the lower-left corner
|
||||
// r.Resized(r.Max, size) // same with the top-right corner
|
||||
// r.Resized(r.Center(), size) // resizes around the center
|
||||
//
|
||||
// This function does not make sense for resizing a rectangle of zero area and will panic. Use
|
||||
// ResizedMin in the case of zero area.
|
||||
func (r Rect) Resized(anchor, size Vec) Rect {
|
||||
if r.W()*r.H() == 0 {
|
||||
panic(fmt.Errorf("(%T).Resize: zero area", r))
|
||||
}
|
||||
fraction := Vec{size.X / r.W(), size.Y / r.H()}
|
||||
return Rect{
|
||||
Min: anchor.Add(r.Min.Sub(anchor).ScaledXY(fraction)),
|
||||
Max: anchor.Add(r.Max.Sub(anchor).ScaledXY(fraction)),
|
||||
}
|
||||
}
|
||||
|
||||
// ResizedMin returns the Rect resized to the given size while keeping the position of the Rect's
|
||||
// Min.
|
||||
//
|
||||
// Sizes of zero area are safe here.
|
||||
func (r Rect) ResizedMin(size Vec) Rect {
|
||||
return Rect{
|
||||
Min: r.Min,
|
||||
Max: r.Min.Add(size),
|
||||
}
|
||||
}
|
||||
|
||||
// Contains checks whether a vector u is contained within this Rect (including it's borders).
|
||||
func (r Rect) Contains(u Vec) bool {
|
||||
return r.Min.X <= u.X && u.X <= r.Max.X && r.Min.Y <= u.Y && u.Y <= r.Max.Y
|
||||
}
|
||||
|
||||
// Union returns the minimal Rect which covers both r and s. Rects r and s must be normalized.
|
||||
func (r Rect) Union(s Rect) Rect {
|
||||
return R(
|
||||
math.Min(r.Min.X, s.Min.X),
|
||||
math.Min(r.Min.Y, s.Min.Y),
|
||||
math.Max(r.Max.X, s.Max.X),
|
||||
math.Max(r.Max.Y, s.Max.Y),
|
||||
)
|
||||
}
|
||||
|
||||
// Intersect returns the maximal Rect which is covered by both r and s. Rects r and s must be normalized.
|
||||
//
|
||||
// If r and s don't overlap, this function returns a zero-rectangle.
|
||||
func (r Rect) Intersect(s Rect) Rect {
|
||||
t := R(
|
||||
math.Max(r.Min.X, s.Min.X),
|
||||
math.Max(r.Min.Y, s.Min.Y),
|
||||
math.Min(r.Max.X, s.Max.X),
|
||||
math.Min(r.Max.Y, s.Max.Y),
|
||||
)
|
||||
if t.Min.X >= t.Max.X || t.Min.Y >= t.Max.Y {
|
||||
return ZR
|
||||
}
|
||||
return t
|
||||
}
|
||||
|
||||
// Intersects returns whether or not the given Rect intersects at any point with this Rect.
|
||||
//
|
||||
// This function is overall about 5x faster than Intersect, so it is better
|
||||
// to use if you have no need for the returned Rect from Intersect.
|
||||
func (r Rect) Intersects(s Rect) bool {
|
||||
return !(s.Max.X <= r.Min.X ||
|
||||
s.Min.X >= r.Max.X ||
|
||||
s.Max.Y <= r.Min.Y ||
|
||||
s.Min.Y >= r.Max.Y)
|
||||
}
|
||||
|
||||
// IntersectCircle returns a minimal required Vector, such that moving the rect by that vector would stop the Circle
|
||||
// and the Rect intersecting. This function returns a zero-vector if the Circle and Rect do not overlap, and if only
|
||||
// the perimeters touch.
|
||||
//
|
||||
// This function will return a non-zero vector if:
|
||||
// - The Rect contains the Circle, partially or fully
|
||||
// - The Circle contains the Rect, partially of fully
|
||||
func (r Rect) IntersectCircle(c Circle) Vec {
|
||||
return c.IntersectRect(r).Scaled(-1)
|
||||
}
|
||||
|
||||
// IntersectLine will return the shortest Vec such that if the Rect is moved by the Vec returned, the Line and Rect no
|
||||
// longer intersect.
|
||||
func (r Rect) IntersectLine(l Line) Vec {
|
||||
return l.IntersectRect(r).Scaled(-1)
|
||||
}
|
||||
|
||||
// IntersectionPoints returns all the points where the Rect intersects with the line provided. This can be zero, one or
|
||||
// two points, depending on the location of the shapes. The points of intersection will be returned in order of
|
||||
// closest-to-l.A to closest-to-l.B.
|
||||
func (r Rect) IntersectionPoints(l Line) []Vec {
|
||||
// Use map keys to ensure unique points
|
||||
pointMap := make(map[Vec]struct{})
|
||||
|
||||
for _, edge := range r.Edges() {
|
||||
if intersect, ok := l.Intersect(edge); ok {
|
||||
pointMap[intersect] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
points := make([]Vec, 0, len(pointMap))
|
||||
for point := range pointMap {
|
||||
points = append(points, point)
|
||||
}
|
||||
|
||||
// Order the points
|
||||
if len(points) == 2 {
|
||||
if points[1].To(l.A).Len() < points[0].To(l.A).Len() {
|
||||
return []Vec{points[1], points[0]}
|
||||
}
|
||||
}
|
||||
|
||||
return points
|
||||
}
|
||||
|
||||
// Vertices returns a slice of the four corners which make up the rectangle.
|
||||
func (r Rect) Vertices() [4]Vec {
|
||||
return [4]Vec{
|
||||
r.Min,
|
||||
V(r.Min.X, r.Max.Y),
|
||||
r.Max,
|
||||
V(r.Max.X, r.Min.Y),
|
||||
}
|
||||
}
|
|
@ -0,0 +1,384 @@
|
|||
package pixel_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/faiface/pixel"
|
||||
)
|
||||
|
||||
func TestRect_Resize(t *testing.T) {
|
||||
type rectTestTransform struct {
|
||||
name string
|
||||
f func(pixel.Rect) pixel.Rect
|
||||
}
|
||||
|
||||
// rectangles
|
||||
squareAroundOrigin := pixel.R(-10, -10, 10, 10)
|
||||
squareAround2020 := pixel.R(10, 10, 30, 30)
|
||||
rectangleAroundOrigin := pixel.R(-20, -10, 20, 10)
|
||||
rectangleAround2020 := pixel.R(0, 10, 40, 30)
|
||||
|
||||
// resize transformations
|
||||
resizeByHalfAroundCenter := rectTestTransform{"by half around center", func(rect pixel.Rect) pixel.Rect {
|
||||
return rect.Resized(rect.Center(), rect.Size().Scaled(0.5))
|
||||
}}
|
||||
resizeByHalfAroundMin := rectTestTransform{"by half around Min", func(rect pixel.Rect) pixel.Rect {
|
||||
return rect.Resized(rect.Min, rect.Size().Scaled(0.5))
|
||||
}}
|
||||
resizeByHalfAroundMax := rectTestTransform{"by half around Max", func(rect pixel.Rect) pixel.Rect {
|
||||
return rect.Resized(rect.Max, rect.Size().Scaled(0.5))
|
||||
}}
|
||||
resizeByHalfAroundMiddleOfLeftSide := rectTestTransform{"by half around middle of left side", func(rect pixel.Rect) pixel.Rect {
|
||||
return rect.Resized(pixel.V(rect.Min.X, rect.Center().Y), rect.Size().Scaled(0.5))
|
||||
}}
|
||||
resizeByHalfAroundOrigin := rectTestTransform{"by half around the origin", func(rect pixel.Rect) pixel.Rect {
|
||||
return rect.Resized(pixel.ZV, rect.Size().Scaled(0.5))
|
||||
}}
|
||||
|
||||
testCases := []struct {
|
||||
input pixel.Rect
|
||||
transform rectTestTransform
|
||||
answer pixel.Rect
|
||||
}{
|
||||
{squareAroundOrigin, resizeByHalfAroundCenter, pixel.R(-5, -5, 5, 5)},
|
||||
{squareAround2020, resizeByHalfAroundCenter, pixel.R(15, 15, 25, 25)},
|
||||
{rectangleAroundOrigin, resizeByHalfAroundCenter, pixel.R(-10, -5, 10, 5)},
|
||||
{rectangleAround2020, resizeByHalfAroundCenter, pixel.R(10, 15, 30, 25)},
|
||||
|
||||
{squareAroundOrigin, resizeByHalfAroundMin, pixel.R(-10, -10, 0, 0)},
|
||||
{squareAround2020, resizeByHalfAroundMin, pixel.R(10, 10, 20, 20)},
|
||||
{rectangleAroundOrigin, resizeByHalfAroundMin, pixel.R(-20, -10, 0, 0)},
|
||||
{rectangleAround2020, resizeByHalfAroundMin, pixel.R(0, 10, 20, 20)},
|
||||
|
||||
{squareAroundOrigin, resizeByHalfAroundMax, pixel.R(0, 0, 10, 10)},
|
||||
{squareAround2020, resizeByHalfAroundMax, pixel.R(20, 20, 30, 30)},
|
||||
{rectangleAroundOrigin, resizeByHalfAroundMax, pixel.R(0, 0, 20, 10)},
|
||||
{rectangleAround2020, resizeByHalfAroundMax, pixel.R(20, 20, 40, 30)},
|
||||
|
||||
{squareAroundOrigin, resizeByHalfAroundMiddleOfLeftSide, pixel.R(-10, -5, 0, 5)},
|
||||
{squareAround2020, resizeByHalfAroundMiddleOfLeftSide, pixel.R(10, 15, 20, 25)},
|
||||
{rectangleAroundOrigin, resizeByHalfAroundMiddleOfLeftSide, pixel.R(-20, -5, 0, 5)},
|
||||
{rectangleAround2020, resizeByHalfAroundMiddleOfLeftSide, pixel.R(0, 15, 20, 25)},
|
||||
|
||||
{squareAroundOrigin, resizeByHalfAroundOrigin, pixel.R(-5, -5, 5, 5)},
|
||||
{squareAround2020, resizeByHalfAroundOrigin, pixel.R(5, 5, 15, 15)},
|
||||
{rectangleAroundOrigin, resizeByHalfAroundOrigin, pixel.R(-10, -5, 10, 5)},
|
||||
{rectangleAround2020, resizeByHalfAroundOrigin, pixel.R(0, 5, 20, 15)},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
t.Run(fmt.Sprintf("Resize %v %s", testCase.input, testCase.transform.name), func(t *testing.T) {
|
||||
testResult := testCase.transform.f(testCase.input)
|
||||
if testResult != testCase.answer {
|
||||
t.Errorf("Got: %v, wanted: %v\n", testResult, testCase.answer)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRect_Edges(t *testing.T) {
|
||||
type fields struct {
|
||||
Min pixel.Vec
|
||||
Max pixel.Vec
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
want [4]pixel.Line
|
||||
}{
|
||||
{
|
||||
name: "Get edges",
|
||||
fields: fields{Min: pixel.V(0, 0), Max: pixel.V(10, 10)},
|
||||
want: [4]pixel.Line{
|
||||
pixel.L(pixel.V(0, 0), pixel.V(0, 10)),
|
||||
pixel.L(pixel.V(0, 10), pixel.V(10, 10)),
|
||||
pixel.L(pixel.V(10, 10), pixel.V(10, 0)),
|
||||
pixel.L(pixel.V(10, 0), pixel.V(0, 0)),
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
r := pixel.Rect{
|
||||
Min: tt.fields.Min,
|
||||
Max: tt.fields.Max,
|
||||
}
|
||||
if got := r.Edges(); !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("Rect.Edges() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRect_Vertices(t *testing.T) {
|
||||
type fields struct {
|
||||
Min pixel.Vec
|
||||
Max pixel.Vec
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
want [4]pixel.Vec
|
||||
}{
|
||||
{
|
||||
name: "Get corners",
|
||||
fields: fields{Min: pixel.V(0, 0), Max: pixel.V(10, 10)},
|
||||
want: [4]pixel.Vec{
|
||||
pixel.V(0, 0),
|
||||
pixel.V(0, 10),
|
||||
pixel.V(10, 10),
|
||||
pixel.V(10, 0),
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
r := pixel.Rect{
|
||||
Min: tt.fields.Min,
|
||||
Max: tt.fields.Max,
|
||||
}
|
||||
if got := r.Vertices(); !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("Rect.Vertices() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRect_IntersectCircle(t *testing.T) {
|
||||
type fields struct {
|
||||
Min pixel.Vec
|
||||
Max pixel.Vec
|
||||
}
|
||||
type args struct {
|
||||
c pixel.Circle
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
want pixel.Vec
|
||||
}{
|
||||
{
|
||||
name: "Rect.IntersectCircle(): no overlap",
|
||||
fields: fields{Min: pixel.ZV, Max: pixel.V(10, 10)},
|
||||
args: args{c: pixel.C(pixel.V(50, 50), 1)},
|
||||
want: pixel.ZV,
|
||||
},
|
||||
{
|
||||
name: "Rect.IntersectCircle(): circle contains rect",
|
||||
fields: fields{Min: pixel.ZV, Max: pixel.V(10, 10)},
|
||||
args: args{c: pixel.C(pixel.V(5, 5), 10)},
|
||||
want: pixel.V(-15, 0),
|
||||
},
|
||||
{
|
||||
name: "Rect.IntersectCircle(): rect contains circle",
|
||||
fields: fields{Min: pixel.ZV, Max: pixel.V(10, 10)},
|
||||
args: args{c: pixel.C(pixel.V(5, 5), 1)},
|
||||
want: pixel.V(-6, 0),
|
||||
},
|
||||
{
|
||||
name: "Rect.IntersectCircle(): circle overlaps bottom-left corner",
|
||||
fields: fields{Min: pixel.ZV, Max: pixel.V(10, 10)},
|
||||
args: args{c: pixel.C(pixel.V(-0.5, -0.5), 1)},
|
||||
want: pixel.V(-0.2, -0.2),
|
||||
},
|
||||
{
|
||||
name: "Rect.IntersectCircle(): circle overlaps top-left corner",
|
||||
fields: fields{Min: pixel.ZV, Max: pixel.V(10, 10)},
|
||||
args: args{c: pixel.C(pixel.V(-0.5, 10.5), 1)},
|
||||
want: pixel.V(-0.2, 0.2),
|
||||
},
|
||||
{
|
||||
name: "Rect.IntersectCircle(): circle overlaps bottom-right corner",
|
||||
fields: fields{Min: pixel.ZV, Max: pixel.V(10, 10)},
|
||||
args: args{c: pixel.C(pixel.V(10.5, -0.5), 1)},
|
||||
want: pixel.V(0.2, -0.2),
|
||||
},
|
||||
{
|
||||
name: "Rect.IntersectCircle(): circle overlaps top-right corner",
|
||||
fields: fields{Min: pixel.ZV, Max: pixel.V(10, 10)},
|
||||
args: args{c: pixel.C(pixel.V(10.5, 10.5), 1)},
|
||||
want: pixel.V(0.2, 0.2),
|
||||
},
|
||||
{
|
||||
name: "Rect.IntersectCircle(): circle overlaps two corners",
|
||||
fields: fields{Min: pixel.ZV, Max: pixel.V(10, 10)},
|
||||
args: args{c: pixel.C(pixel.V(0, 5), 6)},
|
||||
want: pixel.V(6, 0),
|
||||
},
|
||||
{
|
||||
name: "Rect.IntersectCircle(): circle overlaps left edge",
|
||||
fields: fields{Min: pixel.ZV, Max: pixel.V(10, 10)},
|
||||
args: args{c: pixel.C(pixel.V(0, 5), 1)},
|
||||
want: pixel.V(1, 0),
|
||||
},
|
||||
{
|
||||
name: "Rect.IntersectCircle(): circle overlaps bottom edge",
|
||||
fields: fields{Min: pixel.ZV, Max: pixel.V(10, 10)},
|
||||
args: args{c: pixel.C(pixel.V(5, 0), 1)},
|
||||
want: pixel.V(0, 1),
|
||||
},
|
||||
{
|
||||
name: "Rect.IntersectCircle(): circle overlaps right edge",
|
||||
fields: fields{Min: pixel.ZV, Max: pixel.V(10, 10)},
|
||||
args: args{c: pixel.C(pixel.V(10, 5), 1)},
|
||||
want: pixel.V(-1, 0),
|
||||
},
|
||||
{
|
||||
name: "Rect.IntersectCircle(): circle overlaps top edge",
|
||||
fields: fields{Min: pixel.ZV, Max: pixel.V(10, 10)},
|
||||
args: args{c: pixel.C(pixel.V(5, 10), 1)},
|
||||
want: pixel.V(0, -1),
|
||||
},
|
||||
{
|
||||
name: "Rect.IntersectCircle(): edge is tangent of left side",
|
||||
fields: fields{Min: pixel.ZV, Max: pixel.V(10, 10)},
|
||||
args: args{c: pixel.C(pixel.V(-1, 5), 1)},
|
||||
want: pixel.ZV,
|
||||
},
|
||||
{
|
||||
name: "Rect.IntersectCircle(): edge is tangent of top side",
|
||||
fields: fields{Min: pixel.ZV, Max: pixel.V(10, 10)},
|
||||
args: args{c: pixel.C(pixel.V(5, -1), 1)},
|
||||
want: pixel.ZV,
|
||||
},
|
||||
{
|
||||
name: "Rect.IntersectCircle(): circle above rectangle",
|
||||
fields: fields{Min: pixel.ZV, Max: pixel.V(10, 10)},
|
||||
args: args{c: pixel.C(pixel.V(5, 12), 1)},
|
||||
want: pixel.ZV,
|
||||
},
|
||||
{
|
||||
name: "Rect.IntersectCircle(): circle below rectangle",
|
||||
fields: fields{Min: pixel.ZV, Max: pixel.V(10, 10)},
|
||||
args: args{c: pixel.C(pixel.V(5, -2), 1)},
|
||||
want: pixel.ZV,
|
||||
},
|
||||
{
|
||||
name: "Rect.IntersectCircle(): circle left of rectangle",
|
||||
fields: fields{Min: pixel.ZV, Max: pixel.V(10, 10)},
|
||||
args: args{c: pixel.C(pixel.V(-1, 5), 1)},
|
||||
want: pixel.ZV,
|
||||
},
|
||||
{
|
||||
name: "Rect.IntersectCircle(): circle right of rectangle",
|
||||
fields: fields{Min: pixel.ZV, Max: pixel.V(10, 10)},
|
||||
args: args{c: pixel.C(pixel.V(11, 5), 1)},
|
||||
want: pixel.ZV,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
r := pixel.Rect{
|
||||
Min: tt.fields.Min,
|
||||
Max: tt.fields.Max,
|
||||
}
|
||||
got := r.IntersectCircle(tt.args.c)
|
||||
if !closeEnough(got.X, tt.want.X, 2) || !closeEnough(got.Y, tt.want.Y, 2) {
|
||||
t.Errorf("Rect.IntersectCircle() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRect_IntersectionPoints(t *testing.T) {
|
||||
type fields struct {
|
||||
Min pixel.Vec
|
||||
Max pixel.Vec
|
||||
}
|
||||
type args struct {
|
||||
l pixel.Line
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
want []pixel.Vec
|
||||
}{
|
||||
{
|
||||
name: "No intersection points",
|
||||
fields: fields{Min: pixel.V(1, 1), Max: pixel.V(5, 5)},
|
||||
args: args{l: pixel.L(pixel.V(-5, 0), pixel.V(-2, 2))},
|
||||
want: []pixel.Vec{},
|
||||
},
|
||||
{
|
||||
name: "One intersection point",
|
||||
fields: fields{Min: pixel.V(1, 1), Max: pixel.V(5, 5)},
|
||||
args: args{l: pixel.L(pixel.V(2, 0), pixel.V(2, 3))},
|
||||
want: []pixel.Vec{pixel.V(2, 1)},
|
||||
},
|
||||
{
|
||||
name: "Two intersection points",
|
||||
fields: fields{Min: pixel.V(1, 1), Max: pixel.V(5, 5)},
|
||||
args: args{l: pixel.L(pixel.V(0, 2), pixel.V(6, 2))},
|
||||
want: []pixel.Vec{pixel.V(1, 2), pixel.V(5, 2)},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
r := pixel.Rect{
|
||||
Min: tt.fields.Min,
|
||||
Max: tt.fields.Max,
|
||||
}
|
||||
if got := r.IntersectionPoints(tt.args.l); !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("Rect.IntersectPoints() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
var rectIntTests = []struct {
|
||||
name string
|
||||
r1, r2 pixel.Rect
|
||||
want pixel.Rect
|
||||
intersect bool
|
||||
}{
|
||||
{
|
||||
name: "Nothing touching",
|
||||
r1: pixel.R(0, 0, 10, 10),
|
||||
r2: pixel.R(21, 21, 40, 40),
|
||||
want: pixel.ZR,
|
||||
},
|
||||
{
|
||||
name: "Edge touching",
|
||||
r1: pixel.R(0, 0, 10, 10),
|
||||
r2: pixel.R(10, 10, 20, 20),
|
||||
want: pixel.ZR,
|
||||
},
|
||||
{
|
||||
name: "Bit of overlap",
|
||||
r1: pixel.R(0, 0, 10, 10),
|
||||
r2: pixel.R(0, 9, 20, 20),
|
||||
want: pixel.R(0, 9, 10, 10),
|
||||
intersect: true,
|
||||
},
|
||||
{
|
||||
name: "Fully overlapped",
|
||||
r1: pixel.R(0, 0, 10, 10),
|
||||
r2: pixel.R(0, 0, 10, 10),
|
||||
want: pixel.R(0, 0, 10, 10),
|
||||
intersect: true,
|
||||
},
|
||||
}
|
||||
|
||||
func TestRect_Intersect(t *testing.T) {
|
||||
for _, tt := range rectIntTests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := tt.r1.Intersect(tt.r2); !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("Rect.Intersect() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRect_Intersects(t *testing.T) {
|
||||
for _, tt := range rectIntTests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := tt.r1.Intersects(tt.r2); got != tt.intersect {
|
||||
t.Errorf("Rect.Intersects() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
14
sprite.go
14
sprite.go
|
@ -27,7 +27,7 @@ func NewSprite(pic Picture, frame Rect) *Sprite {
|
|||
tri := MakeTrianglesData(6)
|
||||
s := &Sprite{
|
||||
tri: tri,
|
||||
d: Drawer{Triangles: tri},
|
||||
d: Drawer{Triangles: tri, Cached: true},
|
||||
}
|
||||
s.matrix = IM
|
||||
s.mask = Alpha(1)
|
||||
|
@ -44,6 +44,13 @@ func (s *Sprite) Set(pic Picture, frame Rect) {
|
|||
}
|
||||
}
|
||||
|
||||
// SetCached makes the sprite cache all the
|
||||
// incoming pictures if the argument is true, and
|
||||
// doesn't make it do that if the argument is false.
|
||||
func (s *Sprite) SetCached(cached bool) {
|
||||
s.d.Cached = cached
|
||||
}
|
||||
|
||||
// Picture returns the current Sprite's Picture.
|
||||
func (s *Sprite) Picture() Picture {
|
||||
return s.d.Picture
|
||||
|
@ -105,12 +112,7 @@ func (s *Sprite) calcData() {
|
|||
(*s.tri)[i].Color = s.mask
|
||||
(*s.tri)[i].Picture = center.Add((*s.tri)[i].Position)
|
||||
(*s.tri)[i].Intensity = 1
|
||||
}
|
||||
|
||||
// matrix and mask
|
||||
for i := range *s.tri {
|
||||
(*s.tri)[i].Position = s.matrix.Project((*s.tri)[i].Position)
|
||||
(*s.tri)[i].Color = s.mask
|
||||
}
|
||||
|
||||
s.d.Dirty()
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
package text
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"image"
|
||||
"image/draw"
|
||||
"sort"
|
||||
|
@ -60,9 +59,10 @@ func NewAtlas(face font.Face, runeSets ...[]rune) *Atlas {
|
|||
))
|
||||
|
||||
for r, fg := range fixedMapping {
|
||||
dr, mask, maskp, _, _ := face.Glyph(fg.dot, r)
|
||||
if dr, mask, maskp, _, ok := face.Glyph(fg.dot, r); ok {
|
||||
draw.Draw(atlasImg, dr, mask, maskp, draw.Src)
|
||||
}
|
||||
}
|
||||
|
||||
bounds := pixel.R(
|
||||
i2f(fixedBounds.Min.X),
|
||||
|
@ -203,7 +203,6 @@ func makeMapping(face font.Face, runes []rune, padding, width fixed.Int26_6) (ma
|
|||
for _, r := range runes {
|
||||
b, advance, ok := face.GlyphBounds(r)
|
||||
if !ok {
|
||||
fmt.Println(r)
|
||||
continue
|
||||
}
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@ import (
|
|||
"testing"
|
||||
|
||||
"github.com/faiface/pixel/text"
|
||||
"golang.org/x/image/font/inconsolata"
|
||||
)
|
||||
|
||||
func TestAtlas7x13(t *testing.T) {
|
||||
|
@ -22,3 +23,7 @@ func TestAtlas7x13(t *testing.T) {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestAtlasInconsolata(t *testing.T) {
|
||||
text.NewAtlas(inconsolata.Regular8x16, text.ASCII)
|
||||
}
|
||||
|
|
12
text/text.go
12
text/text.go
|
@ -101,6 +101,7 @@ type Text struct {
|
|||
trans pixel.TrianglesData
|
||||
transD pixel.Drawer
|
||||
dirty bool
|
||||
anchor pixel.Anchor
|
||||
}
|
||||
|
||||
// New creates a new Text capable of drawing runes contained in the provided Atlas. Orig and Dot
|
||||
|
@ -135,6 +136,7 @@ func New(orig pixel.Vec, atlas *Atlas) *Text {
|
|||
|
||||
txt.transD.Picture = txt.atlas.pic
|
||||
txt.transD.Triangles = &txt.trans
|
||||
txt.transD.Cached = true
|
||||
|
||||
txt.Clear()
|
||||
|
||||
|
@ -182,6 +184,12 @@ func (txt *Text) BoundsOf(s string) pixel.Rect {
|
|||
return bounds
|
||||
}
|
||||
|
||||
// AlignedTo returns the text moved by the given anchor.
|
||||
func (txt *Text) AlignedTo(anchor pixel.Anchor) *Text {
|
||||
txt.anchor = anchor
|
||||
return txt
|
||||
}
|
||||
|
||||
// Clear removes all written text from the Text. The Dot field is reset to Orig.
|
||||
func (txt *Text) Clear() {
|
||||
txt.prevR = -1
|
||||
|
@ -244,6 +252,10 @@ func (txt *Text) DrawColorMask(t pixel.Target, matrix pixel.Matrix, mask color.C
|
|||
txt.mat = matrix
|
||||
txt.dirty = true
|
||||
}
|
||||
|
||||
offset := txt.Orig.Sub(txt.Bounds().Max.Add(txt.Bounds().AnchorPos(txt.anchor.Opposite())))
|
||||
txt.mat = pixel.IM.Moved(offset).Chained(txt.mat)
|
||||
|
||||
if mask == nil {
|
||||
mask = pixel.Alpha(1)
|
||||
}
|
||||
|
|
|
@ -76,7 +76,7 @@ func BenchmarkTextWrite(b *testing.B) {
|
|||
b.Run(fmt.Sprintf("%d", len(chunk)), func(b *testing.B) {
|
||||
txt := text.New(pixel.ZV, atlas)
|
||||
for i := 0; i < b.N; i++ {
|
||||
txt.Write(chunk)
|
||||
_, _ = txt.Write(chunk)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -0,0 +1,462 @@
|
|||
package pixel
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
)
|
||||
|
||||
// Vec is a 2D vector type with X and Y coordinates.
|
||||
//
|
||||
// Create vectors with the V constructor:
|
||||
//
|
||||
// u := pixel.V(1, 2)
|
||||
// v := pixel.V(8, -3)
|
||||
//
|
||||
// Use various methods to manipulate them:
|
||||
//
|
||||
// w := u.Add(v)
|
||||
// fmt.Println(w) // Vec(9, -1)
|
||||
// fmt.Println(u.Sub(v)) // Vec(-7, 5)
|
||||
// u = pixel.V(2, 3)
|
||||
// v = pixel.V(8, 1)
|
||||
// if u.X < 0 {
|
||||
// fmt.Println("this won't happen")
|
||||
// }
|
||||
// x := u.Unit().Dot(v.Unit())
|
||||
type Vec struct {
|
||||
X, Y float64
|
||||
}
|
||||
|
||||
// ZV is a zero vector.
|
||||
var ZV = Vec{0, 0}
|
||||
|
||||
// V returns a new 2D vector with the given coordinates.
|
||||
func V(x, y float64) Vec {
|
||||
return Vec{x, y}
|
||||
}
|
||||
|
||||
// nearlyEqual compares two float64s and returns whether they are equal, accounting for rounding errors.At worst, the
|
||||
// result is correct to 7 significant digits.
|
||||
func nearlyEqual(a, b float64) bool {
|
||||
epsilon := 0.000001
|
||||
|
||||
if a == b {
|
||||
return true
|
||||
}
|
||||
|
||||
diff := math.Abs(a - b)
|
||||
|
||||
if a == 0.0 || b == 0.0 || diff < math.SmallestNonzeroFloat64 {
|
||||
return diff < (epsilon * math.SmallestNonzeroFloat64)
|
||||
}
|
||||
|
||||
absA := math.Abs(a)
|
||||
absB := math.Abs(b)
|
||||
|
||||
return diff/math.Min(absA+absB, math.MaxFloat64) < epsilon
|
||||
}
|
||||
|
||||
// Eq will compare two vectors and return whether they are equal accounting for rounding errors. At worst, the result
|
||||
// is correct to 7 significant digits.
|
||||
func (u Vec) Eq(v Vec) bool {
|
||||
return nearlyEqual(u.X, v.X) && nearlyEqual(u.Y, v.Y)
|
||||
}
|
||||
|
||||
// Unit returns a vector of length 1 facing the given angle.
|
||||
func Unit(angle float64) Vec {
|
||||
return Vec{1, 0}.Rotated(angle)
|
||||
}
|
||||
|
||||
// String returns the string representation of the vector u.
|
||||
//
|
||||
// u := pixel.V(4.5, -1.3)
|
||||
// u.String() // returns "Vec(4.5, -1.3)"
|
||||
// fmt.Println(u) // Vec(4.5, -1.3)
|
||||
func (u Vec) String() string {
|
||||
return fmt.Sprintf("Vec(%v, %v)", u.X, u.Y)
|
||||
}
|
||||
|
||||
// XY returns the components of the vector in two return values.
|
||||
func (u Vec) XY() (x, y float64) {
|
||||
return u.X, u.Y
|
||||
}
|
||||
|
||||
// Add returns the sum of vectors u and v.
|
||||
func (u Vec) Add(v Vec) Vec {
|
||||
return Vec{
|
||||
u.X + v.X,
|
||||
u.Y + v.Y,
|
||||
}
|
||||
}
|
||||
|
||||
// Sub returns the difference betweeen vectors u and v.
|
||||
func (u Vec) Sub(v Vec) Vec {
|
||||
return Vec{
|
||||
u.X - v.X,
|
||||
u.Y - v.Y,
|
||||
}
|
||||
}
|
||||
|
||||
// Floor converts x and y to their integer equivalents.
|
||||
func (u Vec) Floor() Vec {
|
||||
return Vec{
|
||||
math.Floor(u.X),
|
||||
math.Floor(u.Y),
|
||||
}
|
||||
}
|
||||
|
||||
// To returns the vector from u to v. Equivalent to v.Sub(u).
|
||||
func (u Vec) To(v Vec) Vec {
|
||||
return Vec{
|
||||
v.X - u.X,
|
||||
v.Y - u.Y,
|
||||
}
|
||||
}
|
||||
|
||||
// Scaled returns the vector u multiplied by c.
|
||||
func (u Vec) Scaled(c float64) Vec {
|
||||
return Vec{u.X * c, u.Y * c}
|
||||
}
|
||||
|
||||
// ScaledXY returns the vector u multiplied by the vector v component-wise.
|
||||
func (u Vec) ScaledXY(v Vec) Vec {
|
||||
return Vec{u.X * v.X, u.Y * v.Y}
|
||||
}
|
||||
|
||||
// Len returns the length of the vector u.
|
||||
func (u Vec) Len() float64 {
|
||||
return math.Hypot(u.X, u.Y)
|
||||
}
|
||||
|
||||
// SqLen returns the squared length of the vector u (faster to compute than Len).
|
||||
func (u Vec) SqLen() float64 {
|
||||
return u.X*u.X + u.Y*u.Y
|
||||
}
|
||||
|
||||
// Angle returns the angle between the vector u and the x-axis. The result is in range [-Pi, Pi].
|
||||
func (u Vec) Angle() float64 {
|
||||
return math.Atan2(u.Y, u.X)
|
||||
}
|
||||
|
||||
// Unit returns a vector of length 1 facing the direction of u (has the same angle).
|
||||
func (u Vec) Unit() Vec {
|
||||
if u.X == 0 && u.Y == 0 {
|
||||
return Vec{1, 0}
|
||||
}
|
||||
return u.Scaled(1 / u.Len())
|
||||
}
|
||||
|
||||
// Rotated returns the vector u rotated by the given angle in radians.
|
||||
func (u Vec) Rotated(angle float64) Vec {
|
||||
sin, cos := math.Sincos(angle)
|
||||
return Vec{
|
||||
u.X*cos - u.Y*sin,
|
||||
u.X*sin + u.Y*cos,
|
||||
}
|
||||
}
|
||||
|
||||
// Normal returns a vector normal to u. Equivalent to u.Rotated(math.Pi / 2), but faster.
|
||||
func (u Vec) Normal() Vec {
|
||||
return Vec{-u.Y, u.X}
|
||||
}
|
||||
|
||||
// Dot returns the dot product of vectors u and v.
|
||||
func (u Vec) Dot(v Vec) float64 {
|
||||
return u.X*v.X + u.Y*v.Y
|
||||
}
|
||||
|
||||
// Cross return the cross product of vectors u and v.
|
||||
func (u Vec) Cross(v Vec) float64 {
|
||||
return u.X*v.Y - v.X*u.Y
|
||||
}
|
||||
|
||||
// Project returns a projection (or component) of vector u in the direction of vector v.
|
||||
//
|
||||
// Behaviour is undefined if v is a zero vector.
|
||||
func (u Vec) Project(v Vec) Vec {
|
||||
len := u.Dot(v) / v.Len()
|
||||
return v.Unit().Scaled(len)
|
||||
}
|
||||
|
||||
// Map applies the function f to both x and y components of the vector u and returns the modified
|
||||
// vector.
|
||||
//
|
||||
// u := pixel.V(10.5, -1.5)
|
||||
// v := u.Map(math.Floor) // v is Vec(10, -2), both components of u floored
|
||||
func (u Vec) Map(f func(float64) float64) Vec {
|
||||
return Vec{
|
||||
f(u.X),
|
||||
f(u.Y),
|
||||
}
|
||||
}
|
||||
|
||||
// Lerp returns a linear interpolation between vectors a and b.
|
||||
//
|
||||
// This function basically returns a point along the line between a and b and t chooses which one.
|
||||
// If t is 0, then a will be returned, if t is 1, b will be returned. Anything between 0 and 1 will
|
||||
// return the appropriate point between a and b and so on.
|
||||
func Lerp(a, b Vec, t float64) Vec {
|
||||
return a.Scaled(1 - t).Add(b.Scaled(t))
|
||||
}
|
||||
|
||||
// Line is a 2D line segment, between points A and B.
|
||||
type Line struct {
|
||||
A, B Vec
|
||||
}
|
||||
|
||||
// L creates and returns a new Line.
|
||||
func L(from, to Vec) Line {
|
||||
return Line{
|
||||
A: from,
|
||||
B: to,
|
||||
}
|
||||
}
|
||||
|
||||
// Bounds returns the lines bounding box. This is in the form of a normalized Rect.
|
||||
func (l Line) Bounds() Rect {
|
||||
return R(l.A.X, l.A.Y, l.B.X, l.B.Y).Norm()
|
||||
}
|
||||
|
||||
// Center will return the point at center of the line; that is, the point equidistant from either end.
|
||||
func (l Line) Center() Vec {
|
||||
return l.A.Add(l.A.To(l.B).Scaled(0.5))
|
||||
}
|
||||
|
||||
// Closest will return the point on the line which is closest to the Vec provided.
|
||||
func (l Line) Closest(v Vec) Vec {
|
||||
// between is a helper function which determines whether x is greater than min(a, b) and less than max(a, b)
|
||||
between := func(a, b, x float64) bool {
|
||||
min := math.Min(a, b)
|
||||
max := math.Max(a, b)
|
||||
return min < x && x < max
|
||||
}
|
||||
|
||||
// Closest point will be on a line which perpendicular to this line.
|
||||
// If and only if the infinite perpendicular line intersects the segment.
|
||||
m, b := l.Formula()
|
||||
|
||||
// Account for horizontal lines
|
||||
if m == 0 {
|
||||
x := v.X
|
||||
y := l.A.Y
|
||||
|
||||
// check if the X coordinate of v is on the line
|
||||
if between(l.A.X, l.B.X, v.X) {
|
||||
return V(x, y)
|
||||
}
|
||||
|
||||
// Otherwise get the closest endpoint
|
||||
if l.A.To(v).Len() < l.B.To(v).Len() {
|
||||
return l.A
|
||||
}
|
||||
return l.B
|
||||
}
|
||||
|
||||
// Account for vertical lines
|
||||
if math.IsInf(math.Abs(m), 1) {
|
||||
x := l.A.X
|
||||
y := v.Y
|
||||
|
||||
// check if the Y coordinate of v is on the line
|
||||
if between(l.A.Y, l.B.Y, v.Y) {
|
||||
return V(x, y)
|
||||
}
|
||||
|
||||
// Otherwise get the closest endpoint
|
||||
if l.A.To(v).Len() < l.B.To(v).Len() {
|
||||
return l.A
|
||||
}
|
||||
return l.B
|
||||
}
|
||||
|
||||
perpendicularM := -1 / m
|
||||
perpendicularB := v.Y - (perpendicularM * v.X)
|
||||
|
||||
// Coordinates of intersect (of infinite lines)
|
||||
x := (perpendicularB - b) / (m - perpendicularM)
|
||||
y := m*x + b
|
||||
|
||||
// Check if the point lies between the x and y bounds of the segment
|
||||
if !between(l.A.X, l.B.X, x) && !between(l.A.Y, l.B.Y, y) {
|
||||
// Not within bounding box
|
||||
toStart := v.To(l.A)
|
||||
toEnd := v.To(l.B)
|
||||
|
||||
if toStart.Len() < toEnd.Len() {
|
||||
return l.A
|
||||
}
|
||||
return l.B
|
||||
}
|
||||
|
||||
return V(x, y)
|
||||
}
|
||||
|
||||
// Contains returns whether the provided Vec lies on the line.
|
||||
func (l Line) Contains(v Vec) bool {
|
||||
return l.Closest(v).Eq(v)
|
||||
}
|
||||
|
||||
// Formula will return the values that represent the line in the formula: y = mx + b
|
||||
// This function will return math.Inf+, math.Inf- for a vertical line.
|
||||
func (l Line) Formula() (m, b float64) {
|
||||
// Account for horizontal lines
|
||||
if l.B.Y == l.A.Y {
|
||||
return 0, l.A.Y
|
||||
}
|
||||
|
||||
m = (l.B.Y - l.A.Y) / (l.B.X - l.A.X)
|
||||
b = l.A.Y - (m * l.A.X)
|
||||
|
||||
return m, b
|
||||
}
|
||||
|
||||
// Intersect will return the point of intersection for the two line segments. If the line segments do not intersect,
|
||||
// this function will return the zero-vector and false.
|
||||
func (l Line) Intersect(k Line) (Vec, bool) {
|
||||
// Check if the lines are parallel
|
||||
lDir := l.A.To(l.B)
|
||||
kDir := k.A.To(k.B)
|
||||
if lDir.X == kDir.X && lDir.Y == kDir.Y {
|
||||
return ZV, false
|
||||
}
|
||||
|
||||
// The lines intersect - but potentially not within the line segments.
|
||||
// Get the intersection point for the lines if they were infinitely long, check if the point exists on both of the
|
||||
// segments
|
||||
lm, lb := l.Formula()
|
||||
km, kb := k.Formula()
|
||||
|
||||
// Account for vertical lines
|
||||
if math.IsInf(math.Abs(lm), 1) && math.IsInf(math.Abs(km), 1) {
|
||||
// Both vertical, therefore parallel
|
||||
return ZV, false
|
||||
}
|
||||
|
||||
var x, y float64
|
||||
|
||||
if math.IsInf(math.Abs(lm), 1) || math.IsInf(math.Abs(km), 1) {
|
||||
// One line is vertical
|
||||
intersectM := lm
|
||||
intersectB := lb
|
||||
verticalLine := k
|
||||
|
||||
if math.IsInf(math.Abs(lm), 1) {
|
||||
intersectM = km
|
||||
intersectB = kb
|
||||
verticalLine = l
|
||||
}
|
||||
|
||||
y = intersectM*verticalLine.A.X + intersectB
|
||||
x = verticalLine.A.X
|
||||
} else {
|
||||
// Coordinates of intersect
|
||||
x = (kb - lb) / (lm - km)
|
||||
y = lm*x + lb
|
||||
}
|
||||
|
||||
if l.Contains(V(x, y)) && k.Contains(V(x, y)) {
|
||||
// The intersect point is on both line segments, they intersect.
|
||||
return V(x, y), true
|
||||
}
|
||||
|
||||
return ZV, false
|
||||
}
|
||||
|
||||
// IntersectCircle will return the shortest Vec such that moving the Line by that Vec will cause the Line and Circle
|
||||
// to no longer intesect. If they do not intersect at all, this function will return a zero-vector.
|
||||
func (l Line) IntersectCircle(c Circle) Vec {
|
||||
// Get the point on the line closest to the center of the circle.
|
||||
closest := l.Closest(c.Center)
|
||||
cirToClosest := c.Center.To(closest)
|
||||
|
||||
if cirToClosest.Len() >= c.Radius {
|
||||
return ZV
|
||||
}
|
||||
|
||||
return cirToClosest.Scaled(cirToClosest.Len() - c.Radius)
|
||||
}
|
||||
|
||||
// IntersectRect will return the shortest Vec such that moving the Line by that Vec will cause the Line and Rect to
|
||||
// no longer intesect. If they do not intersect at all, this function will return a zero-vector.
|
||||
func (l Line) IntersectRect(r Rect) Vec {
|
||||
// Check if either end of the line segment are within the rectangle
|
||||
if r.Contains(l.A) || r.Contains(l.B) {
|
||||
// Use the Rect.Intersect to get minimal return value
|
||||
rIntersect := l.Bounds().Intersect(r)
|
||||
if rIntersect.H() > rIntersect.W() {
|
||||
// Go vertical
|
||||
return V(0, rIntersect.H())
|
||||
}
|
||||
return V(rIntersect.W(), 0)
|
||||
}
|
||||
|
||||
// Check if any of the rectangles' edges intersect with this line.
|
||||
for _, edge := range r.Edges() {
|
||||
if _, ok := l.Intersect(edge); ok {
|
||||
// Get the closest points on the line to each corner, where:
|
||||
// - the point is contained by the rectangle
|
||||
// - the point is not the corner itself
|
||||
corners := r.Vertices()
|
||||
var closest *Vec
|
||||
closestCorner := corners[0]
|
||||
for _, c := range corners {
|
||||
cc := l.Closest(c)
|
||||
if closest == nil || (closest.Len() > cc.Len() && r.Contains(cc)) {
|
||||
closest = &cc
|
||||
closestCorner = c
|
||||
}
|
||||
}
|
||||
|
||||
return closest.To(closestCorner)
|
||||
}
|
||||
}
|
||||
|
||||
// No intersect
|
||||
return ZV
|
||||
}
|
||||
|
||||
// Len returns the length of the line segment.
|
||||
func (l Line) Len() float64 {
|
||||
return l.A.To(l.B).Len()
|
||||
}
|
||||
|
||||
// Moved will return a line moved by the delta Vec provided.
|
||||
func (l Line) Moved(delta Vec) Line {
|
||||
return Line{
|
||||
A: l.A.Add(delta),
|
||||
B: l.B.Add(delta),
|
||||
}
|
||||
}
|
||||
|
||||
// Rotated will rotate the line around the provided Vec.
|
||||
func (l Line) Rotated(around Vec, angle float64) Line {
|
||||
// Move the line so we can use `Vec.Rotated`
|
||||
lineShifted := l.Moved(around.Scaled(-1))
|
||||
|
||||
lineRotated := Line{
|
||||
A: lineShifted.A.Rotated(angle),
|
||||
B: lineShifted.B.Rotated(angle),
|
||||
}
|
||||
|
||||
return lineRotated.Moved(around)
|
||||
}
|
||||
|
||||
// Scaled will return the line scaled around the center point.
|
||||
func (l Line) Scaled(scale float64) Line {
|
||||
return l.ScaledXY(l.Center(), scale)
|
||||
}
|
||||
|
||||
// ScaledXY will return the line scaled around the Vec provided.
|
||||
func (l Line) ScaledXY(around Vec, scale float64) Line {
|
||||
toA := around.To(l.A).Scaled(scale)
|
||||
toB := around.To(l.B).Scaled(scale)
|
||||
|
||||
return Line{
|
||||
A: around.Add(toA),
|
||||
B: around.Add(toB),
|
||||
}
|
||||
}
|
||||
|
||||
func (l Line) String() string {
|
||||
return fmt.Sprintf("Line(%v, %v)", l.A, l.B)
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
package pixel_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/faiface/pixel"
|
||||
)
|
||||
|
||||
type floorTest struct {
|
||||
input pixel.Vec
|
||||
expected pixel.Vec
|
||||
}
|
||||
|
||||
func TestFloor(t *testing.T) {
|
||||
tests := []floorTest{
|
||||
{input: pixel.V(4.50, 6.70), expected: pixel.V(4, 6)},
|
||||
{input: pixel.V(9.0, 6.70), expected: pixel.V(9, 6)},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
result := tc.input.Floor()
|
||||
if result != tc.expected {
|
||||
t.Error(fmt.Sprintf("Expected %v but got %v", tc.expected, result))
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue