Compare commits
388 Commits
Author | SHA1 | Date |
---|---|---|
|
2d9a739e40 | |
|
cbf4b9106d | |
|
5af5565fb6 | |
|
902248de11 | |
|
93a9cf4093 | |
|
5ed20d5485 | |
|
f72c39f382 | |
|
5f53a59dd6 | |
|
1c68874dc8 | |
|
e788339625 | |
|
c1abca5cab | |
|
8e4da92871 | |
|
c1a222d84b | |
|
0183f7fbbb | |
|
452ef16712 | |
|
3706fa8879 | |
|
0e9ff7b70e | |
|
1a41307942 | |
|
14ace447b5 | |
|
b4c1033c47 | |
|
50dd323ba4 | |
|
b40854c1d7 | |
|
2f957ede44 | |
|
0f4afdc244 | |
|
d04bf34361 | |
|
ff0040bbb1 | |
|
1f1324dbe8 | |
|
dc889f0631 | |
|
8dd236979d | |
|
9942ab3da3 | |
|
3afca9093d | |
|
97f07acd5e | |
|
da55b9ddca | |
|
b50671c353 | |
|
dc2b6fe7b1 | |
|
bd468c8629 | |
|
060142a8f9 | |
|
75e90bb010 | |
|
f4b7c74524 | |
|
55fa9fd100 | |
|
814aff8f1c | |
|
79a78f2f1c | |
|
95f4386cf5 | |
|
f7b8a5a1a3 | |
|
4dc0b013f3 | |
|
597550c16b | |
|
c65ed3a739 | |
|
b04ac93aaa | |
|
9b325eee4e | |
|
82de22b7cf | |
|
302f1f5411 | |
|
5b2ad29f7b | |
|
225de575de | |
|
cceaac897e | |
|
1c4776d672 | |
|
c9df32add0 | |
|
41a3073be3 | |
|
be9fbb76bd | |
|
3d6f497bd5 | |
|
0d89f0e36e | |
|
8e68e71083 | |
|
5612defeba | |
|
926b8181b3 | |
|
6e27fc84e7 | |
|
6215259c1d | |
|
4792a9ebd8 | |
|
1de2dbe561 | |
|
9eaf82e088 | |
|
78d02fe8ea | |
|
35ebc39ba6 | |
|
d34b63676d | |
|
321c494681 | |
|
7fbd71cecc | |
|
264bdff76d | |
|
447050b720 | |
|
4573035f89 | |
|
63ababf515 | |
|
cce4cccd26 | |
|
accdb6f51c | |
|
d4f724a0d3 | |
|
e3d1835268 | |
|
66a8b51d54 | |
|
9a1d011a9f | |
|
e48de5789c | |
|
af52257c66 | |
|
31eeca4179 | |
|
5b75852399 | |
|
0d5ad8a0bf | |
|
bf50427297 | |
|
f3647fc451 | |
|
73a3f65adb | |
|
099522c410 | |
|
3ca575a4e4 | |
|
dc4b4ec61f | |
|
eca7b33334 | |
|
5ca0239191 | |
|
af9bcae596 | |
|
62fcf430d2 | |
|
b3cceb1b7d | |
|
006e4f5481 | |
|
33c6f0ca5d | |
|
c5078ff6b0 | |
|
9398991a01 | |
|
1a58fea88b | |
|
6c527c3b89 | |
|
0777dc3bc1 | |
|
e2823f2580 | |
|
f47736b7cb | |
|
5d3f082240 | |
|
d0f6e646ac | |
|
fdb6359fbb | |
|
0737b86059 | |
|
cb4bb4c3ef | |
|
41963b01cc | |
|
065f4f3c90 | |
|
8e571bfe8e | |
|
32ff29438d | |
|
1a88ab2edd | |
|
f9972888a2 | |
|
6ac68670bd | |
|
57b578bba7 | |
|
cdce974e49 | |
|
d7487f1f7a | |
|
4137f87f22 | |
|
bee65f5833 | |
|
46e79f21b9 | |
|
2a8c17c33c | |
|
c49c77a116 | |
|
d640879775 | |
|
c86834b8f6 | |
|
330e9da360 | |
|
7f2b8b6fe9 | |
|
42737212d8 | |
|
c7bb0a1639 | |
|
a48d07ff6b | |
|
99573a5f1e | |
|
c23446fb49 | |
|
578ae8fa53 | |
|
6af6195bd0 | |
|
0c28c0785e | |
|
17f735c2d0 | |
|
7629b6ef5e | |
|
ae0526fda0 | |
|
80cfdfcb6a | |
|
b01300dab9 | |
|
c078a58652 | |
|
b58d6bf5ec | |
|
75ab96923c | |
|
2a9b7e5210 | |
|
79c5d20194 | |
|
e6484064aa | |
|
3fcad7503f | |
|
1545bd7af5 | |
|
0dc27e409b | |
|
f2ef87f198 | |
|
ef6a44fef8 | |
|
9a6e6066bd | |
|
7ebbf7e9b5 | |
|
31fc049ab7 | |
|
c331fe2583 | |
|
6b9ea45e96 | |
|
2e4c6018c9 | |
|
7215265523 | |
|
fc858bff4d | |
|
918031892a | |
|
9a7ab1c6b0 | |
|
34cdd8729b | |
|
0358330d3b | |
|
4b7553cd73 | |
|
781c44f119 | |
|
c385b247b3 | |
|
3706d040ce | |
|
4749e3ee7e | |
|
e06acda99b | |
|
9e0c65d8dd | |
|
f80edafc7b | |
|
67a69d96d6 | |
|
bbeab0aebf | |
|
d5f7088b7d | |
|
9a401948ae | |
|
abc99bdef8 | |
|
debdbea894 | |
|
fc30e51016 | |
|
90432a7857 | |
|
51c32e407f | |
|
659dc6fd5f | |
|
50ba35d4cb | |
|
9102076f1b | |
|
2834411318 | |
|
7fe45b5a88 | |
|
51e1843f4b | |
|
eab5be6207 | |
|
62fa797088 | |
|
970faf0b63 | |
|
6c269fc8a5 | |
|
a794d27972 | |
|
bc08b65073 | |
|
bf6e20a04b | |
|
ecdd8462bb | |
|
3af9c2b20e | |
|
3ae612a84d | |
|
0ac5371d7e | |
|
fccedc5a9d | |
|
f7aac5ed09 | |
|
1d928485d6 | |
|
b832e83517 | |
|
53167788d6 | |
|
9d60c5fa32 | |
|
cce26f0a51 | |
|
b15c10298e | |
|
3a14aae310 | |
|
e86120db20 | |
|
a510048648 | |
|
feb12a1c7e | |
|
3ffbbb9cda | |
|
863e1e2f0c | |
|
e3268db31e | |
|
7b10ad8497 | |
|
abcdff5960 | |
|
dc3f9857d8 | |
|
dd6d38b8f3 | |
|
46122dd826 | |
|
c8114d8467 | |
|
f58676289a | |
|
80735cfc0c | |
|
dcdc812af5 | |
|
248d68f6aa | |
|
3841afb70f | |
|
217ac0c4d7 | |
|
65236863fe | |
|
cfaff8c0cb | |
|
2d1f61f746 | |
|
883bdc32c7 | |
|
c9eea2639e | |
|
5b524dadd8 | |
|
a86876a1cd | |
|
29785fb937 | |
|
1d17e45825 | |
|
b91d8be6cd | |
|
c47d77b2b5 | |
|
e9a3c900cf | |
|
3d3cbd6027 | |
|
4e6d6eeb3a | |
|
17ddf4fec5 | |
|
e1e1815537 | |
|
a05abdca76 | |
|
317124058e | |
|
3b1e0eaa21 | |
|
6a7500959f | |
|
e2a16764c4 | |
|
424b6e0f0b | |
|
bc145eb1b8 | |
|
5303ec5648 | |
|
f7dad2f3c2 | |
|
3b5bfa90e6 | |
|
dff622523b | |
|
51a9702fc8 | |
|
f7bb304b92 | |
|
5efd04b420 | |
|
ea7bc5aff9 | |
|
7b8a0c152e | |
|
2115296062 | |
|
dc745825d6 | |
|
29fe3b16ca | |
|
d37ad8f1ba | |
|
85ba21a2f4 | |
|
915faeee0c | |
|
113d052872 | |
|
9571b4339b | |
|
64aa32a4b4 | |
|
01db742aba | |
|
ff64cf248b | |
|
d595d5f647 | |
|
951c7f4c59 | |
|
e8aa765ed3 | |
|
bdbce3a27b | |
|
0389ee7bf9 | |
|
736f4549a9 | |
|
ef9fd8fe10 | |
|
4246e90215 | |
|
1d0169b2b7 | |
|
2626892e97 | |
|
1e2eb29a4f | |
|
b60b7e5207 | |
|
8165c38c6d | |
|
82fa73952e | |
|
bab33976cc | |
|
0e54ed1080 | |
|
db4f08aefc | |
|
fd6e056f92 | |
|
e93b9a1d4c | |
|
d9a94fd157 | |
|
6e5cbaa493 | |
|
9e150a7a1b | |
|
9c74e66118 | |
|
ee74866587 | |
|
a2ad3dbf7c | |
|
af1ccf885f | |
|
b756edfac6 | |
|
c69ef58898 | |
|
6e63f27d6e | |
|
acfd836a7f | |
|
8679a702a9 | |
|
e9380d7eed | |
|
3ed8c0016c | |
|
2afe44a9c9 | |
|
c028b66b68 | |
|
080735510c | |
|
862f30a004 | |
|
c0ddb0b287 | |
|
3e493c13e1 | |
|
f9f61911a7 | |
|
d30b73fb8b | |
|
4aea56198f | |
|
591fadaaa5 | |
|
497df7842b | |
|
a403cfe50b | |
|
bdbba8f3c1 | |
|
c485793a83 | |
|
97158ba502 | |
|
01ff4230da | |
|
37dcef6ab6 | |
|
ab9d608c45 | |
|
05da8e6f92 | |
|
49c2beeca9 | |
|
f4ce166964 | |
|
93e9ab79b1 | |
|
58be215a76 | |
|
ce083a9a9f | |
|
2ef17e7a95 | |
|
80f48bc0b6 | |
|
420e83bc61 | |
|
b51b3e8f38 | |
|
620784b34c | |
|
368dab0e7c | |
|
9bfb83861a | |
|
4e30fb9f5c | |
|
11c76944b6 | |
|
30af7c6612 | |
|
411ad7c27d | |
|
37b0f0956b | |
|
10684b6add | |
|
cf666b5866 | |
|
f325092c02 | |
|
939a76923d | |
|
fcbc61c570 | |
|
e55d490801 | |
|
b5a2c12175 | |
|
14a01a5522 | |
|
3276c4e4a1 | |
|
c61b677fa9 | |
|
c9e0f7262d | |
|
a2499cf90d | |
|
6d2aeb64e7 | |
|
34a6d020a2 | |
|
9b624ae466 | |
|
f2a0a19f6e | |
|
a555999120 | |
|
aa2c560d4c | |
|
4eca5f2d1e | |
|
4374bb7be1 | |
|
3fa31cdab5 | |
|
219559cf20 | |
|
3d3f1c6e11 | |
|
58e8b8f892 | |
|
13b9e6aee5 | |
|
9e8697cdf6 | |
|
134fe2bf7b | |
|
9df8a3761b | |
|
a081d5b0c8 | |
|
32ae09e1e5 | |
|
317cfb17b0 | |
|
a9e6f41315 | |
|
9cbce8f638 | |
|
e3f7901f2c | |
|
ee19c6b361 | |
|
5ad013b286 | |
|
7fdf8c0d60 | |
|
3a25f78b1a | |
|
ad733f5946 | |
|
8d5879070f | |
|
2155babc5d | |
|
dc9afb5557 | |
|
79f7f4fb42 | |
|
2562f6b754 | |
|
b138fc5d5b | |
|
15a270e689 | |
|
538ad90185 |
|
@ -0,0 +1,23 @@
|
|||
language: go
|
||||
sudo: false
|
||||
addons:
|
||||
apt:
|
||||
packages:
|
||||
- xorg-dev
|
||||
- libx11-dev
|
||||
- libxrandr-dev
|
||||
- libxinerama-dev
|
||||
- libxcursor-dev
|
||||
- libxi-dev
|
||||
- libopenal-dev
|
||||
- libasound2-dev
|
||||
go:
|
||||
- 1.8
|
||||
- 1.7.4
|
||||
- tip
|
||||
install:
|
||||
- go get -t ./...
|
||||
script:
|
||||
- go test -i -race ./...
|
||||
- go test -v -race ./...
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
# Contributing to Pixel
|
||||
|
||||
:tada: Hi! I'm really glad you're considering contributing to Pixel! :tada:
|
||||
|
||||
## Here are a few ways you can contribute
|
||||
|
||||
1. **Make a community example** and place it inside the [examples/community](examples/community) folder.
|
||||
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.
|
||||
|
||||
## 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/.
|
163
README.md
|
@ -1,5 +1,160 @@
|
|||
# pixel
|
||||
A simple and fast desktop multimedia/gamedev library.
|
||||
# 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)
|
||||
|
||||
The core features of the library are pretty much completed. I'm doing some proofreading and
|
||||
improving quality of the code. Will **announce** the library after.
|
||||
|
||||
A hand-crafted 2D game library in Go. Take a look into the [features](#features) to see what it can
|
||||
do.
|
||||
|
||||
```
|
||||
go get github.com/faiface/pixel
|
||||
```
|
||||
|
||||
See [requirements](#requirements) for the list of libraries necessary for compilation.
|
||||
|
||||
## Tutorial
|
||||
|
||||
The [Wiki of this repo](https://github.com/faiface/pixel/wiki) contains an extensive tutorial
|
||||
covering several topics of Pixel. Here's the content of the tutorial parts so far:
|
||||
|
||||
- [Creating a Window](https://github.com/faiface/pixel/wiki/Creating-a-Window)
|
||||
- [Drawing a Sprite](https://github.com/faiface/pixel/wiki/Drawing-a-Sprite)
|
||||
- [Moving, scaling and rotating with Matrix](https://github.com/faiface/pixel/wiki/Moving,-scaling-and-rotating-with-Matrix)
|
||||
- [Pressing keys and clicking mouse](https://github.com/faiface/pixel/wiki/Pressing-keys-and-clicking-mouse)
|
||||
- [Drawing efficiently with Batch](https://github.com/faiface/pixel/wiki/Drawing-efficiently-with-Batch)
|
||||
- [Drawing shapes with IMDraw](https://github.com/faiface/pixel/wiki/Drawing-shapes-with-IMDraw)
|
||||
- [Typing text on the screen](https://github.com/faiface/pixel/wiki/Typing-text-on-the-screen)
|
||||
|
||||
## Examples
|
||||
|
||||
The [examples](https://github.com/faiface/pixel/tree/master/examples) directory contains a few
|
||||
examples demonstrating Pixel's functionality.
|
||||
|
||||
**To run an example**, navigate to it's directory, then `go run` the `main.go` file. For example:
|
||||
|
||||
```
|
||||
$ cd examples/platformer
|
||||
$ go run main.go
|
||||
```
|
||||
|
||||
Here are some screenshots from the examples!
|
||||
|
||||
| [Lights](examples/lights) | [Platformer](examples/platformer) |
|
||||
| --- | --- |
|
||||
|  |  |
|
||||
|
||||
| [Smoke](examples/smoke) | [Typewriter](examples/typewriter) |
|
||||
| --- | --- |
|
||||
|  |  |
|
||||
|
||||
| [Raycaster](examples/community/raycaster) | [Starfield](examples/community/starfield) |
|
||||
| --- | --- |
|
||||
|  |  |
|
||||
|
||||
## Features
|
||||
|
||||
Here's the list of the main features in Pixel. Although Pixel is still under heavy development,
|
||||
**there should be no major breakage in the API.** This is not a 100% guarantee, though.
|
||||
|
||||
- Fast 2D graphics
|
||||
- Sprites
|
||||
- Primitive shapes with immediate mode style
|
||||
[IMDraw](https://github.com/faiface/pixel/wiki/Drawing-shapes-with-IMDraw) (circles, rectangles,
|
||||
lines, ...)
|
||||
- Optimized drawing with [Batch](https://github.com/faiface/pixel/wiki/Drawing-efficiently-with-Batch)
|
||||
- Text drawing with [text](https://godoc.org/github.com/faiface/pixel/text) package
|
||||
- Audio through a separate [Beep](https://github.com/faiface/beep) library.
|
||||
- Simple and convenient API
|
||||
- Drawing a sprite to a window is as simple as `sprite.Draw(window, matrix)`
|
||||
- Wanna know where the center of a window is? `window.Bounds().Center()`
|
||||
- [...](https://godoc.org/github.com/faiface/pixel)
|
||||
- Full documentation and tutorial
|
||||
- Works on Linux, macOS and Windows
|
||||
- Window creation and manipulation (resizing, fullscreen, multiple windows, ...)
|
||||
- Keyboard (key presses, text input) and mouse input without events
|
||||
- Well integrated with the Go standard library
|
||||
- Use `"image"` package for loading pictures
|
||||
- Use `"time"` package for measuring delta time and FPS
|
||||
- Use `"image/color"` for colors, or use Pixel's own `color.Color` format, which supports easy
|
||||
multiplication and a few more features
|
||||
- Pixel uses `float64` throughout the library, compatible with `"math"` package
|
||||
- Geometry transformations with
|
||||
[Matrix](https://github.com/faiface/pixel/wiki/Moving,-scaling-and-rotating-with-Matrix)
|
||||
- Moving, scaling, rotating
|
||||
- Easy camera implementation
|
||||
- Off-screen drawing to Canvas or any other target (Batch, IMDraw, ...)
|
||||
- Fully garbage collected, no `Close` or `Dispose` methods
|
||||
- Full [Porter-Duff](http://ssp.impulsetrain.com/porterduff.html) composition, which enables
|
||||
- 2D lighting
|
||||
- Cutting holes into objects
|
||||
- Much more...
|
||||
- Pixel let's you draw stuff and do your job, it doesn't impose any particular style or paradigm
|
||||
- Platform and backend independent [core](https://godoc.org/github.com/faiface/pixel)
|
||||
- Core Target/Triangles/Picture pattern makes it easy to create new drawing targets that do
|
||||
arbitrarily crazy stuff (e.g. graphical effects)
|
||||
- Small codebase, ~5K lines of code, including the backend [glhf](https://github.com/faiface/glhf)
|
||||
package
|
||||
|
||||
## Missing features
|
||||
|
||||
Pixel is in development and still missing few critical features. Here're the most critical ones.
|
||||
|
||||
- ~~Audio~~
|
||||
- ~~Drawing text~~
|
||||
- Antialiasing (filtering is supported, though)
|
||||
- ~~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)
|
||||
- Tests and benchmarks
|
||||
|
||||
**Implementing these features will get us to the 1.0 release.** Contribute, so that it's as soon as
|
||||
possible!
|
||||
|
||||
## Requirements
|
||||
|
||||
If you're using Windows and having trouble building Pixel, please check [this
|
||||
guide](https://github.com/faiface/pixel/wiki/Building-Pixel-on-Windows) on the
|
||||
[wiki](https://github.com/faiface/pixel/wiki).
|
||||
|
||||
[PixelGL](https://godoc.org/github.com/faiface/pixel/pixelgl) backend uses OpenGL to render
|
||||
graphics. Because of that, OpenGL development libraries are needed for compilation. The dependencies
|
||||
are same as for [GLFW](https://github.com/go-gl/glfw).
|
||||
|
||||
The OpenGL version used is **OpenGL 3.3**.
|
||||
|
||||
- On macOS, you need Xcode or Command Line Tools for Xcode (`xcode-select --install`) for required
|
||||
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.
|
||||
- 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
|
||||
[#7](https://github.com/faiface/pixel/issues/7). This issue is probably not related to Pixel.
|
||||
**Upgrading to Go 1.8.1 fixes the issue.**
|
||||
|
||||
## Contributing
|
||||
|
||||
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
|
||||
everyone to contribute, even with just an idea. Especially welcome are **issues** and **pull
|
||||
requests**.
|
||||
|
||||
**However, I won't accept everything. Pixel is being developed with thought and care.** Each
|
||||
component was designed and re-designed multiple times. Code and API quality is very important here.
|
||||
API is focused on simplicity and expressiveness.
|
||||
|
||||
When contributing, keep these goals in mind. It doesn't mean that I'll only accept perfect pull
|
||||
requests. It just means that I might not like your idea. Or that your pull requests could need some
|
||||
rewriting. That's perfectly fine, don't let it put you off. In the end, we'll just end up with a
|
||||
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?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
||||
community.
|
||||
|
||||
## License
|
||||
|
||||
[MIT](LICENSE)
|
||||
|
|
34
batch.go
|
@ -5,8 +5,7 @@ import (
|
|||
"image/color"
|
||||
)
|
||||
|
||||
// Batch is a Target that allows for efficient drawing of many objects with the same Picture (but
|
||||
// different slices of the same Picture are allowed).
|
||||
// Batch is a Target that allows for efficient drawing of many objects with the same Picture.
|
||||
//
|
||||
// To put an object into a Batch, just draw it onto it:
|
||||
// object.Draw(batch)
|
||||
|
@ -14,7 +13,7 @@ type Batch struct {
|
|||
cont Drawer
|
||||
|
||||
mat Matrix
|
||||
col NRGBA
|
||||
col RGBA
|
||||
}
|
||||
|
||||
var _ BasicTarget = (*Batch)(nil)
|
||||
|
@ -29,7 +28,7 @@ var _ BasicTarget = (*Batch)(nil)
|
|||
func NewBatch(container Triangles, pic Picture) *Batch {
|
||||
b := &Batch{cont: Drawer{Triangles: container, Picture: pic}}
|
||||
b.SetMatrix(IM)
|
||||
b.SetColorMask(NRGBA{1, 1, 1, 1})
|
||||
b.SetColorMask(Alpha(1))
|
||||
return b
|
||||
}
|
||||
|
||||
|
@ -63,10 +62,10 @@ func (b *Batch) SetMatrix(m Matrix) {
|
|||
// SetColorMask sets a mask color used in the following draws onto the Batch.
|
||||
func (b *Batch) SetColorMask(c color.Color) {
|
||||
if c == nil {
|
||||
b.col = NRGBA{1, 1, 1, 1}
|
||||
b.col = Alpha(1)
|
||||
return
|
||||
}
|
||||
b.col = ToNRGBA(c)
|
||||
b.col = ToRGBA(c)
|
||||
}
|
||||
|
||||
// MakeTriangles returns a specialized copy of the provided Triangles that draws onto this Batch.
|
||||
|
@ -81,21 +80,19 @@ func (b *Batch) MakeTriangles(t Triangles) TargetTriangles {
|
|||
|
||||
// MakePicture returns a specialized copy of the provided Picture that draws onto this Batch.
|
||||
func (b *Batch) MakePicture(p Picture) TargetPicture {
|
||||
if p.Original() != b.cont.Picture.Original() {
|
||||
panic(fmt.Errorf("(%T).MakePicture: Picture is not a slice of Batch's Picture", b))
|
||||
if p != b.cont.Picture {
|
||||
panic(fmt.Errorf("(%T).MakePicture: Picture is not the Batch's Picture", b))
|
||||
}
|
||||
bp := &batchPicture{
|
||||
pic: p,
|
||||
dst: b,
|
||||
}
|
||||
bp.orig = bp
|
||||
return bp
|
||||
}
|
||||
|
||||
type batchTriangles struct {
|
||||
tri Triangles
|
||||
tmp *TrianglesData
|
||||
|
||||
dst *Batch
|
||||
}
|
||||
|
||||
|
@ -149,27 +146,14 @@ func (bt *batchTriangles) Draw() {
|
|||
}
|
||||
|
||||
type batchPicture struct {
|
||||
pic Picture
|
||||
orig *batchPicture
|
||||
dst *Batch
|
||||
pic Picture
|
||||
dst *Batch
|
||||
}
|
||||
|
||||
func (bp *batchPicture) Bounds() Rect {
|
||||
return bp.pic.Bounds()
|
||||
}
|
||||
|
||||
func (bp *batchPicture) Slice(r Rect) Picture {
|
||||
return &batchPicture{
|
||||
pic: bp.pic.Slice(r),
|
||||
orig: bp.orig,
|
||||
dst: bp.dst,
|
||||
}
|
||||
}
|
||||
|
||||
func (bp *batchPicture) Original() Picture {
|
||||
return bp.orig
|
||||
}
|
||||
|
||||
func (bp *batchPicture) Draw(t TargetTriangles) {
|
||||
bt := t.(*batchTriangles)
|
||||
if bp.dst != bt.dst {
|
||||
|
|
94
color.go
|
@ -2,17 +2,30 @@ package pixel
|
|||
|
||||
import "image/color"
|
||||
|
||||
// NRGBA represents a non-alpha-premultiplied RGBA color with components within range [0, 1].
|
||||
// RGBA represents an alpha-premultiplied RGBA color with components within range [0, 1].
|
||||
//
|
||||
// The difference between color.NRGBA is that the value range is [0, 1] and the values are floats.
|
||||
type NRGBA struct {
|
||||
// The difference between color.RGBA is that the value range is [0, 1] and the values are floats.
|
||||
type RGBA struct {
|
||||
R, G, B, A float64
|
||||
}
|
||||
|
||||
// RGB returns a fully opaque RGBA color with the given RGB values.
|
||||
//
|
||||
// A common way to construct a transparent color is to create one with RGB constructor, then
|
||||
// multiply it by a color obtained from the Alpha constructor.
|
||||
func RGB(r, g, b float64) RGBA {
|
||||
return RGBA{r, g, b, 1}
|
||||
}
|
||||
|
||||
// Alpha returns a white RGBA color with the given alpha component.
|
||||
func Alpha(a float64) RGBA {
|
||||
return RGBA{a, a, a, a}
|
||||
}
|
||||
|
||||
// Add adds color d to color c component-wise and returns the result (the components are not
|
||||
// clamped).
|
||||
func (c NRGBA) Add(d NRGBA) NRGBA {
|
||||
return NRGBA{
|
||||
func (c RGBA) Add(d RGBA) RGBA {
|
||||
return RGBA{
|
||||
R: c.R + d.R,
|
||||
G: c.G + d.G,
|
||||
B: c.B + d.B,
|
||||
|
@ -22,8 +35,8 @@ func (c NRGBA) Add(d NRGBA) NRGBA {
|
|||
|
||||
// Sub subtracts color d from color c component-wise and returns the result (the components
|
||||
// are not clamped).
|
||||
func (c NRGBA) Sub(d NRGBA) NRGBA {
|
||||
return NRGBA{
|
||||
func (c RGBA) Sub(d RGBA) RGBA {
|
||||
return RGBA{
|
||||
R: c.R - d.R,
|
||||
G: c.G - d.G,
|
||||
B: c.B - d.B,
|
||||
|
@ -32,8 +45,8 @@ func (c NRGBA) Sub(d NRGBA) NRGBA {
|
|||
}
|
||||
|
||||
// Mul multiplies color c by color d component-wise (the components are not clamped).
|
||||
func (c NRGBA) Mul(d NRGBA) NRGBA {
|
||||
return NRGBA{
|
||||
func (c RGBA) Mul(d RGBA) RGBA {
|
||||
return RGBA{
|
||||
R: c.R * d.R,
|
||||
G: c.G * d.G,
|
||||
B: c.B * d.B,
|
||||
|
@ -43,8 +56,8 @@ func (c NRGBA) Mul(d NRGBA) NRGBA {
|
|||
|
||||
// Scaled multiplies each component of color c by scale and returns the result (the components
|
||||
// are not clamped).
|
||||
func (c NRGBA) Scaled(scale float64) NRGBA {
|
||||
return NRGBA{
|
||||
func (c RGBA) Scaled(scale float64) RGBA {
|
||||
return RGBA{
|
||||
R: c.R * scale,
|
||||
G: c.G * scale,
|
||||
B: c.B * scale,
|
||||
|
@ -52,58 +65,33 @@ func (c NRGBA) Scaled(scale float64) NRGBA {
|
|||
}
|
||||
}
|
||||
|
||||
// RGBA returns alpha-premultiplied red, green, blue and alpha components of the NRGBA color.
|
||||
func (c NRGBA) RGBA() (r, g, b, a uint32) {
|
||||
c.R = clamp(c.R, 0, 1)
|
||||
c.G = clamp(c.G, 0, 1)
|
||||
c.B = clamp(c.B, 0, 1)
|
||||
c.A = clamp(c.A, 0, 1)
|
||||
r = uint32(0xffff * c.R * c.A)
|
||||
g = uint32(0xffff * c.G * c.A)
|
||||
b = uint32(0xffff * c.B * c.A)
|
||||
// RGBA returns alpha-premultiplied red, green, blue and alpha components of the RGBA color.
|
||||
func (c RGBA) RGBA() (r, g, b, a uint32) {
|
||||
r = uint32(0xffff * c.R)
|
||||
g = uint32(0xffff * c.G)
|
||||
b = uint32(0xffff * c.B)
|
||||
a = uint32(0xffff * c.A)
|
||||
return
|
||||
}
|
||||
|
||||
func clamp(x, low, high float64) float64 {
|
||||
if x < low {
|
||||
return low
|
||||
}
|
||||
if x > high {
|
||||
return high
|
||||
}
|
||||
return x
|
||||
}
|
||||
|
||||
// ToNRGBA converts a color to NRGBA format. Using this function is preferred to using NRGBAModel,
|
||||
// for performance (using NRGBAModel introduced additional unnecessary allocations).
|
||||
func ToNRGBA(c color.Color) NRGBA {
|
||||
if c, ok := c.(NRGBA); ok {
|
||||
// ToRGBA converts a color to RGBA format. Using this function is preferred to using RGBAModel, for
|
||||
// performance (using RGBAModel introduces additional unnecessary allocations).
|
||||
func ToRGBA(c color.Color) RGBA {
|
||||
if c, ok := c.(RGBA); ok {
|
||||
return c
|
||||
}
|
||||
if c, ok := c.(color.NRGBA); ok {
|
||||
return NRGBA{
|
||||
R: float64(c.R) / 255,
|
||||
G: float64(c.G) / 255,
|
||||
B: float64(c.B) / 255,
|
||||
A: float64(c.A) / 255,
|
||||
}
|
||||
}
|
||||
r, g, b, a := c.RGBA()
|
||||
if a == 0 {
|
||||
return NRGBA{0, 0, 0, 0}
|
||||
}
|
||||
return NRGBA{
|
||||
float64(r) / float64(a),
|
||||
float64(g) / float64(a),
|
||||
float64(b) / float64(a),
|
||||
return RGBA{
|
||||
float64(r) / 0xffff,
|
||||
float64(g) / 0xffff,
|
||||
float64(b) / 0xffff,
|
||||
float64(a) / 0xffff,
|
||||
}
|
||||
}
|
||||
|
||||
// NRGBAModel converts colors to NRGBA format.
|
||||
var NRGBAModel = color.ModelFunc(nrgbaModel)
|
||||
// RGBAModel converts colors to RGBA format.
|
||||
var RGBAModel = color.ModelFunc(rgbaModel)
|
||||
|
||||
func nrgbaModel(c color.Color) color.Color {
|
||||
return ToNRGBA(c)
|
||||
func rgbaModel(c color.Color) color.Color {
|
||||
return ToRGBA(c)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
package pixel_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"image/color"
|
||||
"testing"
|
||||
|
||||
"github.com/faiface/pixel"
|
||||
)
|
||||
|
||||
func BenchmarkColorToRGBA(b *testing.B) {
|
||||
types := []color.Color{
|
||||
color.NRGBA{R: 124, G: 14, B: 230, A: 42}, // slowest
|
||||
color.RGBA{R: 62, G: 32, B: 14, A: 63}, // faster
|
||||
pixel.RGB(0.8, 0.2, 0.5).Scaled(0.712), // fastest
|
||||
}
|
||||
for _, col := range types {
|
||||
b.Run(fmt.Sprintf("From %T", col), func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = pixel.ToRGBA(col)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -0,0 +1,65 @@
|
|||
package pixel
|
||||
|
||||
import "errors"
|
||||
|
||||
// ComposeTarget is a BasicTarget capable of Porter-Duff composition.
|
||||
type ComposeTarget interface {
|
||||
BasicTarget
|
||||
|
||||
// SetComposeMethod sets a Porter-Duff composition method to be used.
|
||||
SetComposeMethod(ComposeMethod)
|
||||
}
|
||||
|
||||
// ComposeMethod is a Porter-Duff composition method.
|
||||
type ComposeMethod int
|
||||
|
||||
// Here's the list of all available Porter-Duff composition methods. Use ComposeOver for the basic
|
||||
// alpha blending.
|
||||
const (
|
||||
ComposeOver ComposeMethod = iota
|
||||
ComposeIn
|
||||
ComposeOut
|
||||
ComposeAtop
|
||||
ComposeRover
|
||||
ComposeRin
|
||||
ComposeRout
|
||||
ComposeRatop
|
||||
ComposeXor
|
||||
ComposePlus
|
||||
ComposeCopy
|
||||
)
|
||||
|
||||
// Compose composes two colors together according to the ComposeMethod. A is the foreground, B is
|
||||
// the background.
|
||||
func (cm ComposeMethod) Compose(a, b RGBA) RGBA {
|
||||
var fa, fb float64
|
||||
|
||||
switch cm {
|
||||
case ComposeOver:
|
||||
fa, fb = 1, 1-a.A
|
||||
case ComposeIn:
|
||||
fa, fb = b.A, 0
|
||||
case ComposeOut:
|
||||
fa, fb = 1-b.A, 0
|
||||
case ComposeAtop:
|
||||
fa, fb = b.A, 1-a.A
|
||||
case ComposeRover:
|
||||
fa, fb = 1-b.A, 1
|
||||
case ComposeRin:
|
||||
fa, fb = 0, a.A
|
||||
case ComposeRout:
|
||||
fa, fb = 0, 1-a.A
|
||||
case ComposeRatop:
|
||||
fa, fb = 1-b.A, a.A
|
||||
case ComposeXor:
|
||||
fa, fb = 1-b.A, 1-a.A
|
||||
case ComposePlus:
|
||||
fa, fb = 1, 1
|
||||
case ComposeCopy:
|
||||
fa, fb = 1, 0
|
||||
default:
|
||||
panic(errors.New("Compose: invalid ComposeMethod"))
|
||||
}
|
||||
|
||||
return a.Mul(Alpha(fa)).Add(b.Mul(Alpha(fb)))
|
||||
}
|
142
data.go
|
@ -12,7 +12,7 @@ import (
|
|||
// TrianglesPosition, TrianglesColor and TrianglesPicture.
|
||||
type TrianglesData []struct {
|
||||
Position Vec
|
||||
Color NRGBA
|
||||
Color RGBA
|
||||
Picture Vec
|
||||
Intensity float64
|
||||
}
|
||||
|
@ -42,10 +42,10 @@ func (td *TrianglesData) SetLen(len int) {
|
|||
for i := 0; i < needAppend; i++ {
|
||||
*td = append(*td, struct {
|
||||
Position Vec
|
||||
Color NRGBA
|
||||
Color RGBA
|
||||
Picture Vec
|
||||
Intensity float64
|
||||
}{V(0, 0), NRGBA{1, 1, 1, 1}, V(0, 0), 0})
|
||||
}{Color: RGBA{1, 1, 1, 1}})
|
||||
}
|
||||
}
|
||||
if len < td.Len() {
|
||||
|
@ -108,7 +108,7 @@ func (td *TrianglesData) Position(i int) Vec {
|
|||
}
|
||||
|
||||
// Color returns the color property of i-th vertex.
|
||||
func (td *TrianglesData) Color(i int) NRGBA {
|
||||
func (td *TrianglesData) Color(i int) RGBA {
|
||||
return (*td)[i].Color
|
||||
}
|
||||
|
||||
|
@ -126,36 +126,34 @@ func (td *TrianglesData) Picture(i int) (pic Vec, intensity float64) {
|
|||
//
|
||||
// The struct's innards are exposed for convenience, manual modification is at your own risk.
|
||||
//
|
||||
// The format of the pixels is color.NRGBA and not pixel.NRGBA for a very serious reason:
|
||||
// pixel.NRGBA takes up 8x more memory than color.NRGBA.
|
||||
// The format of the pixels is color.RGBA and not pixel.RGBA for a very serious reason:
|
||||
// pixel.RGBA takes up 8x more memory than color.RGBA.
|
||||
type PictureData struct {
|
||||
Pix []color.NRGBA
|
||||
Pix []color.RGBA
|
||||
Stride int
|
||||
Rect Rect
|
||||
Orig *PictureData
|
||||
}
|
||||
|
||||
// MakePictureData creates a zero-initialized PictureData covering the given rectangle.
|
||||
func MakePictureData(rect Rect) *PictureData {
|
||||
w := int(math.Ceil(rect.Max.X())) - int(math.Floor(rect.Min.X()))
|
||||
h := int(math.Ceil(rect.Max.Y())) - int(math.Floor(rect.Min.Y()))
|
||||
w := int(math.Ceil(rect.Max.X)) - int(math.Floor(rect.Min.X))
|
||||
h := int(math.Ceil(rect.Max.Y)) - int(math.Floor(rect.Min.Y))
|
||||
pd := &PictureData{
|
||||
Stride: w,
|
||||
Rect: rect,
|
||||
}
|
||||
pd.Pix = make([]color.NRGBA, w*h)
|
||||
pd.Orig = pd
|
||||
pd.Pix = make([]color.RGBA, w*h)
|
||||
return pd
|
||||
}
|
||||
|
||||
func verticalFlip(nrgba *image.NRGBA) {
|
||||
bounds := nrgba.Bounds()
|
||||
func verticalFlip(rgba *image.RGBA) {
|
||||
bounds := rgba.Bounds()
|
||||
width := bounds.Dx()
|
||||
|
||||
tmpRow := make([]uint8, width*4)
|
||||
for i, j := 0, bounds.Dy()-1; i < j; i, j = i+1, j-1 {
|
||||
iRow := nrgba.Pix[i*nrgba.Stride : i*nrgba.Stride+width*4]
|
||||
jRow := nrgba.Pix[j*nrgba.Stride : j*nrgba.Stride+width*4]
|
||||
iRow := rgba.Pix[i*rgba.Stride : i*rgba.Stride+width*4]
|
||||
jRow := rgba.Pix[j*rgba.Stride : j*rgba.Stride+width*4]
|
||||
|
||||
copy(tmpRow, iRow)
|
||||
copy(iRow, jRow)
|
||||
|
@ -167,28 +165,28 @@ func verticalFlip(nrgba *image.NRGBA) {
|
|||
//
|
||||
// The resulting PictureData's Bounds will be the equivalent of the supplied image.Image's Bounds.
|
||||
func PictureDataFromImage(img image.Image) *PictureData {
|
||||
var nrgba *image.NRGBA
|
||||
if nrgbaImg, ok := img.(*image.NRGBA); ok {
|
||||
nrgba = nrgbaImg
|
||||
var rgba *image.RGBA
|
||||
if rgbaImg, ok := img.(*image.RGBA); ok {
|
||||
rgba = rgbaImg
|
||||
} else {
|
||||
nrgba = image.NewNRGBA(img.Bounds())
|
||||
draw.Draw(nrgba, nrgba.Bounds(), img, img.Bounds().Min, draw.Src)
|
||||
rgba = image.NewRGBA(img.Bounds())
|
||||
draw.Draw(rgba, rgba.Bounds(), img, img.Bounds().Min, draw.Src)
|
||||
}
|
||||
|
||||
verticalFlip(nrgba)
|
||||
verticalFlip(rgba)
|
||||
|
||||
pd := MakePictureData(R(
|
||||
float64(nrgba.Bounds().Min.X),
|
||||
float64(nrgba.Bounds().Min.Y),
|
||||
float64(nrgba.Bounds().Dx()),
|
||||
float64(nrgba.Bounds().Dy()),
|
||||
float64(rgba.Bounds().Min.X),
|
||||
float64(rgba.Bounds().Min.Y),
|
||||
float64(rgba.Bounds().Max.X),
|
||||
float64(rgba.Bounds().Max.Y),
|
||||
))
|
||||
|
||||
for i := range pd.Pix {
|
||||
pd.Pix[i].R = nrgba.Pix[i*4+0]
|
||||
pd.Pix[i].G = nrgba.Pix[i*4+1]
|
||||
pd.Pix[i].B = nrgba.Pix[i*4+2]
|
||||
pd.Pix[i].A = nrgba.Pix[i*4+3]
|
||||
pd.Pix[i].R = rgba.Pix[i*4+0]
|
||||
pd.Pix[i].G = rgba.Pix[i*4+1]
|
||||
pd.Pix[i].B = rgba.Pix[i*4+2]
|
||||
pd.Pix[i].A = rgba.Pix[i*4+3]
|
||||
}
|
||||
|
||||
return pd
|
||||
|
@ -207,14 +205,20 @@ func PictureDataFromPicture(pic Picture) *PictureData {
|
|||
pd := MakePictureData(bounds)
|
||||
|
||||
if pic, ok := pic.(PictureColor); ok {
|
||||
for y := math.Floor(bounds.Min.Y()); y < bounds.Max.Y(); y++ {
|
||||
for x := math.Floor(bounds.Min.X()); x < bounds.Max.X(); x++ {
|
||||
for y := math.Floor(bounds.Min.Y); y < bounds.Max.Y; y++ {
|
||||
for x := math.Floor(bounds.Min.X); x < bounds.Max.X; x++ {
|
||||
// this together with the Floor is a trick to get all of the pixels
|
||||
at := V(
|
||||
math.Max(x, bounds.Min.X()),
|
||||
math.Max(y, bounds.Min.Y()),
|
||||
math.Max(x, bounds.Min.X),
|
||||
math.Max(y, bounds.Min.Y),
|
||||
)
|
||||
pd.SetColor(at, pic.Color(at))
|
||||
col := pic.Color(at)
|
||||
pd.Pix[pd.Index(at)] = color.RGBA{
|
||||
R: uint8(col.R * 255),
|
||||
G: uint8(col.G * 255),
|
||||
B: uint8(col.B * 255),
|
||||
A: uint8(col.A * 255),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -222,39 +226,39 @@ func PictureDataFromPicture(pic Picture) *PictureData {
|
|||
return pd
|
||||
}
|
||||
|
||||
// Image converts PictureData into an image.NRGBA.
|
||||
// Image converts PictureData into an image.RGBA.
|
||||
//
|
||||
// The resulting image.NRGBA's Bounds will be equivalent of the PictureData's Bounds.
|
||||
func (pd *PictureData) Image() *image.NRGBA {
|
||||
// The resulting image.RGBA's Bounds will be equivalent of the PictureData's Bounds.
|
||||
func (pd *PictureData) Image() *image.RGBA {
|
||||
bounds := image.Rect(
|
||||
int(math.Floor(pd.Rect.Min.X())),
|
||||
int(math.Floor(pd.Rect.Min.Y())),
|
||||
int(math.Ceil(pd.Rect.Max.X())),
|
||||
int(math.Ceil(pd.Rect.Max.Y())),
|
||||
int(math.Floor(pd.Rect.Min.X)),
|
||||
int(math.Floor(pd.Rect.Min.Y)),
|
||||
int(math.Ceil(pd.Rect.Max.X)),
|
||||
int(math.Ceil(pd.Rect.Max.Y)),
|
||||
)
|
||||
nrgba := image.NewNRGBA(bounds)
|
||||
rgba := image.NewRGBA(bounds)
|
||||
|
||||
i := 0
|
||||
for y := bounds.Min.Y; y < bounds.Max.Y; y++ {
|
||||
for x := bounds.Min.X; x < bounds.Max.X; x++ {
|
||||
off := pd.Index(V(float64(x), float64(y)))
|
||||
nrgba.Pix[i*4+0] = pd.Pix[off].R
|
||||
nrgba.Pix[i*4+1] = pd.Pix[off].G
|
||||
nrgba.Pix[i*4+2] = pd.Pix[off].B
|
||||
nrgba.Pix[i*4+3] = pd.Pix[off].A
|
||||
rgba.Pix[i*4+0] = pd.Pix[off].R
|
||||
rgba.Pix[i*4+1] = pd.Pix[off].G
|
||||
rgba.Pix[i*4+2] = pd.Pix[off].B
|
||||
rgba.Pix[i*4+3] = pd.Pix[off].A
|
||||
i++
|
||||
}
|
||||
}
|
||||
|
||||
verticalFlip(nrgba)
|
||||
verticalFlip(rgba)
|
||||
|
||||
return nrgba
|
||||
return rgba
|
||||
}
|
||||
|
||||
// Index returns the index of the pixel at the specified position inside the Pix slice.
|
||||
func (pd *PictureData) Index(at Vec) int {
|
||||
at -= pd.Rect.Min.Map(math.Floor)
|
||||
x, y := int(at.X()), int(at.Y())
|
||||
at = at.Sub(pd.Rect.Min.Map(math.Floor))
|
||||
x, y := int(at.X), int(at.Y)
|
||||
return y*pd.Stride + x
|
||||
}
|
||||
|
||||
|
@ -263,40 +267,10 @@ func (pd *PictureData) Bounds() Rect {
|
|||
return pd.Rect
|
||||
}
|
||||
|
||||
// Slice returns a sub-Picture of this PictureData inside the supplied rectangle.
|
||||
func (pd *PictureData) Slice(r Rect) Picture {
|
||||
return &PictureData{
|
||||
Pix: pd.Pix[pd.Index(r.Min):],
|
||||
Stride: pd.Stride,
|
||||
Rect: r,
|
||||
Orig: pd.Orig,
|
||||
}
|
||||
}
|
||||
|
||||
// Original returns the most original PictureData that this PictureData was obtained from using
|
||||
// Slice-ing.
|
||||
func (pd *PictureData) Original() Picture {
|
||||
return pd.Orig
|
||||
}
|
||||
|
||||
// Color returns the color located at the given position.
|
||||
func (pd *PictureData) Color(at Vec) NRGBA {
|
||||
func (pd *PictureData) Color(at Vec) RGBA {
|
||||
if !pd.Rect.Contains(at) {
|
||||
return NRGBA{0, 0, 0, 0}
|
||||
}
|
||||
return ToNRGBA(pd.Pix[pd.Index(at)])
|
||||
}
|
||||
|
||||
// SetColor changes the color located at the given position.
|
||||
func (pd *PictureData) SetColor(at Vec, col color.Color) {
|
||||
if !pd.Rect.Contains(at) {
|
||||
return
|
||||
}
|
||||
nrgba := ToNRGBA(col)
|
||||
pd.Pix[pd.Index(at)] = color.NRGBA{
|
||||
R: uint8(nrgba.R * 255),
|
||||
G: uint8(nrgba.G * 255),
|
||||
B: uint8(nrgba.B * 255),
|
||||
A: uint8(nrgba.A * 255),
|
||||
return RGBA{0, 0, 0, 0}
|
||||
}
|
||||
return ToRGBA(pd.Pix[pd.Index(at)])
|
||||
}
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
// Package pixel implements platform and backend agnostic core of the Pixel game development
|
||||
// library.
|
||||
//
|
||||
// It specifies the core Target, Triangles, Picture pattern and implements standard elements, such
|
||||
// as Sprite, Batch, Vec, Matrix and RGBA in addition to the basic Triangles and Picture
|
||||
// implementations: TrianglesData and PictureData.
|
||||
package pixel
|
63
drawer.go
|
@ -14,27 +14,29 @@ package pixel
|
|||
// Picture.
|
||||
//
|
||||
// Whenever you change the Triangles, call Dirty to notify Drawer that Triangles changed. You don't
|
||||
// need to notify Drawer about a change of Picture.
|
||||
// need to notify Drawer about a change of the Picture.
|
||||
//
|
||||
// Note, that Drawer caches the results of MakePicture from Targets it's drawn to for each Picture
|
||||
// it's set to. What it means is that using a Drawer with an unbounded number of Pictures leads to a
|
||||
// memory leak, since Drawer caches them and never forgets. In such a situation, create a new Drawer
|
||||
// for each Picture.
|
||||
type Drawer struct {
|
||||
Triangles Triangles
|
||||
Picture Picture
|
||||
|
||||
tris map[Target]TargetTriangles
|
||||
clean map[Target]bool
|
||||
pics map[targetPicturePair]TargetPicture
|
||||
inited bool
|
||||
targets map[Target]*drawerTarget
|
||||
inited bool
|
||||
}
|
||||
|
||||
type targetPicturePair struct {
|
||||
Target Target
|
||||
Picture Picture
|
||||
type drawerTarget struct {
|
||||
tris TargetTriangles
|
||||
pics map[Picture]TargetPicture
|
||||
clean bool
|
||||
}
|
||||
|
||||
func (d *Drawer) lazyInit() {
|
||||
if !d.inited {
|
||||
d.tris = make(map[Target]TargetTriangles)
|
||||
d.clean = make(map[Target]bool)
|
||||
d.pics = make(map[targetPicturePair]TargetPicture)
|
||||
d.targets = make(map[Target]*drawerTarget)
|
||||
d.inited = true
|
||||
}
|
||||
}
|
||||
|
@ -44,8 +46,8 @@ func (d *Drawer) lazyInit() {
|
|||
func (d *Drawer) Dirty() {
|
||||
d.lazyInit()
|
||||
|
||||
for t := range d.clean {
|
||||
d.clean[t] = false
|
||||
for _, t := range d.targets {
|
||||
t.clean = false
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -60,30 +62,35 @@ func (d *Drawer) Draw(t Target) {
|
|||
return
|
||||
}
|
||||
|
||||
tri := d.tris[t]
|
||||
if tri == nil {
|
||||
tri = t.MakeTriangles(d.Triangles)
|
||||
d.tris[t] = tri
|
||||
d.clean[t] = true
|
||||
dt := d.targets[t]
|
||||
if dt == nil {
|
||||
dt = &drawerTarget{
|
||||
pics: make(map[Picture]TargetPicture),
|
||||
}
|
||||
d.targets[t] = dt
|
||||
}
|
||||
|
||||
if !d.clean[t] {
|
||||
tri.SetLen(d.Triangles.Len())
|
||||
tri.Update(d.Triangles)
|
||||
d.clean[t] = true
|
||||
if dt.tris == nil {
|
||||
dt.tris = t.MakeTriangles(d.Triangles)
|
||||
dt.clean = true
|
||||
}
|
||||
|
||||
if !dt.clean {
|
||||
dt.tris.SetLen(d.Triangles.Len())
|
||||
dt.tris.Update(d.Triangles)
|
||||
dt.clean = true
|
||||
}
|
||||
|
||||
if d.Picture == nil {
|
||||
tri.Draw()
|
||||
dt.tris.Draw()
|
||||
return
|
||||
}
|
||||
|
||||
pic := d.pics[targetPicturePair{t, d.Picture.Original()}]
|
||||
pic := dt.pics[d.Picture]
|
||||
if pic == nil {
|
||||
pic = t.MakePicture(d.Picture.Original())
|
||||
d.pics[targetPicturePair{t, d.Picture.Original()}] = pic
|
||||
pic = t.MakePicture(d.Picture)
|
||||
dt.pics[d.Picture] = pic
|
||||
}
|
||||
pic = pic.Slice(d.Picture.Bounds()).(TargetPicture)
|
||||
|
||||
pic.Draw(tri)
|
||||
pic.Draw(dt.tris)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
package pixel_test
|
||||
|
||||
import (
|
||||
"image"
|
||||
"testing"
|
||||
|
||||
"github.com/faiface/pixel"
|
||||
)
|
||||
|
||||
func BenchmarkSpriteDrawBatch(b *testing.B) {
|
||||
img := image.NewRGBA(image.Rect(0, 0, 64, 64))
|
||||
pic := pixel.PictureDataFromImage(img)
|
||||
sprite := pixel.NewSprite(pic, pixel.R(0, 0, 64, 64))
|
||||
batch := pixel.NewBatch(&pixel.TrianglesData{}, pic)
|
||||
for i := 0; i < b.N; i++ {
|
||||
sprite.Draw(batch, pixel.IM)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
# bouncing
|
||||
|
||||
Bouncing particles using the [imdraw](https://godoc.org/github.com/faiface/pixel/imdraw) package.
|
||||
|
||||
Made by [Peter Hellberg](https://github.com/peterhellberg/) as part of his [pixel-experiments](https://github.com/peterhellberg/pixel-experiments)
|
||||
|
||||
## Screenshots
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
## Links
|
||||
|
||||
- https://github.com/peterhellberg/pixel-experiments/tree/master/bouncing
|
||||
- https://gist.github.com/peterhellberg/674f32a15a7d2d249e634ce781f333e8
|
|
@ -0,0 +1,303 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"image/color"
|
||||
"math"
|
||||
"math/rand"
|
||||
"time"
|
||||
|
||||
"github.com/faiface/pixel"
|
||||
"github.com/faiface/pixel/imdraw"
|
||||
"github.com/faiface/pixel/pixelgl"
|
||||
)
|
||||
|
||||
var (
|
||||
w, h, s, scale = float64(640), float64(360), float64(2.3), float64(32)
|
||||
|
||||
p, bg = newPalette(Colors), color.RGBA{32, p.color().G, 32, 255}
|
||||
|
||||
balls = []*ball{
|
||||
newRandomBall(scale),
|
||||
newRandomBall(scale),
|
||||
}
|
||||
)
|
||||
|
||||
func run() {
|
||||
win, err := pixelgl.NewWindow(pixelgl.WindowConfig{
|
||||
Bounds: pixel.R(0, 0, w, h),
|
||||
VSync: true,
|
||||
Undecorated: true,
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
imd := imdraw.New(nil)
|
||||
|
||||
imd.EndShape = imdraw.RoundEndShape
|
||||
imd.Precision = 3
|
||||
|
||||
go func() {
|
||||
start := time.Now()
|
||||
|
||||
for range time.Tick(16 * time.Millisecond) {
|
||||
bg = color.RGBA{32 + (p.color().R/128)*4, 32 + (p.color().G/128)*4, 32 + (p.color().B/128)*4, 255}
|
||||
s = pixel.V(math.Sin(time.Since(start).Seconds())*0.8, 0).Len()*2 - 1
|
||||
scale = 64 + 15*s
|
||||
imd.Intensity = 1.2 * s
|
||||
}
|
||||
}()
|
||||
|
||||
for !win.Closed() {
|
||||
win.SetClosed(win.JustPressed(pixelgl.KeyEscape) || win.JustPressed(pixelgl.KeyQ))
|
||||
|
||||
if win.JustPressed(pixelgl.KeySpace) {
|
||||
for _, ball := range balls {
|
||||
ball.color = ball.palette.next()
|
||||
}
|
||||
}
|
||||
|
||||
if win.JustPressed(pixelgl.KeyEnter) {
|
||||
for _, ball := range balls {
|
||||
ball.pos = center()
|
||||
ball.vel = randomVelocity()
|
||||
}
|
||||
}
|
||||
|
||||
imd.Clear()
|
||||
|
||||
for _, ball := range balls {
|
||||
imd.Color = ball.color
|
||||
imd.Push(ball.pos)
|
||||
}
|
||||
|
||||
imd.Polygon(scale)
|
||||
|
||||
for _, ball := range balls {
|
||||
imd.Color = color.RGBA{ball.color.R, ball.color.G, ball.color.B, 128 - uint8(128*s)}
|
||||
imd.Push(ball.pos)
|
||||
}
|
||||
|
||||
imd.Polygon(scale * s)
|
||||
|
||||
for _, ball := range balls {
|
||||
aliveParticles := []*particle{}
|
||||
|
||||
for _, particle := range ball.particles {
|
||||
if particle.life > 0 {
|
||||
aliveParticles = append(aliveParticles, particle)
|
||||
}
|
||||
}
|
||||
|
||||
for _, particle := range aliveParticles {
|
||||
imd.Color = particle.color
|
||||
imd.Push(particle.pos)
|
||||
imd.Circle(16*particle.life, 0)
|
||||
}
|
||||
}
|
||||
|
||||
win.Clear(bg)
|
||||
imd.Draw(win)
|
||||
win.Update()
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
rand.Seed(4)
|
||||
|
||||
go func() {
|
||||
for range time.Tick(32 * time.Millisecond) {
|
||||
for _, ball := range balls {
|
||||
go ball.update()
|
||||
|
||||
for _, particle := range ball.particles {
|
||||
go particle.update()
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
pixelgl.Run(run)
|
||||
}
|
||||
|
||||
func newParticleAt(pos, vel pixel.Vec) *particle {
|
||||
c := p.color()
|
||||
c.A = 5
|
||||
|
||||
return &particle{pos, vel, c, rand.Float64() * 1.5}
|
||||
}
|
||||
|
||||
func newRandomBall(radius float64) *ball {
|
||||
return &ball{
|
||||
center(), randomVelocity(),
|
||||
math.Pi * (radius * radius),
|
||||
radius, p.random(), p, []*particle{},
|
||||
}
|
||||
}
|
||||
|
||||
func center() pixel.Vec {
|
||||
return pixel.V(w/2, h/2)
|
||||
}
|
||||
|
||||
func randomVelocity() pixel.Vec {
|
||||
return pixel.V((rand.Float64()*2)-1, (rand.Float64()*2)-1).Scaled(scale / 4)
|
||||
}
|
||||
|
||||
type particle struct {
|
||||
pos pixel.Vec
|
||||
vel pixel.Vec
|
||||
color color.RGBA
|
||||
life float64
|
||||
}
|
||||
|
||||
func (p *particle) update() {
|
||||
p.pos = p.pos.Add(p.vel)
|
||||
p.life -= 0.03
|
||||
|
||||
switch {
|
||||
case p.pos.Y < 0 || p.pos.Y >= h:
|
||||
p.vel.Y *= -1.0
|
||||
case p.pos.X < 0 || p.pos.X >= w:
|
||||
p.vel.X *= -1.0
|
||||
}
|
||||
}
|
||||
|
||||
type ball struct {
|
||||
pos pixel.Vec
|
||||
vel pixel.Vec
|
||||
mass float64
|
||||
radius float64
|
||||
color color.RGBA
|
||||
palette *Palette
|
||||
particles []*particle
|
||||
}
|
||||
|
||||
func (b *ball) update() {
|
||||
b.pos = b.pos.Add(b.vel)
|
||||
|
||||
var bounced bool
|
||||
|
||||
switch {
|
||||
case b.pos.Y <= b.radius || b.pos.Y >= h-b.radius:
|
||||
b.vel.Y *= -1.0
|
||||
bounced = true
|
||||
|
||||
if b.pos.Y < b.radius {
|
||||
b.pos.Y = b.radius
|
||||
} else {
|
||||
b.pos.Y = h - b.radius
|
||||
}
|
||||
case b.pos.X <= b.radius || b.pos.X >= w-b.radius:
|
||||
b.vel.X *= -1.0
|
||||
bounced = true
|
||||
|
||||
if b.pos.X < b.radius {
|
||||
b.pos.X = b.radius
|
||||
} else {
|
||||
b.pos.X = w - b.radius
|
||||
}
|
||||
}
|
||||
|
||||
for _, a := range balls {
|
||||
if a != b {
|
||||
d := a.pos.Sub(b.pos)
|
||||
|
||||
if d.Len() > a.radius+b.radius {
|
||||
continue
|
||||
}
|
||||
|
||||
pen := d.Unit().Scaled(a.radius + b.radius - d.Len())
|
||||
|
||||
a.pos = a.pos.Add(pen.Scaled(b.mass / (a.mass + b.mass)))
|
||||
b.pos = b.pos.Sub(pen.Scaled(a.mass / (a.mass + b.mass)))
|
||||
|
||||
u := d.Unit()
|
||||
v := 2 * (a.vel.Dot(u) - b.vel.Dot(u)) / (a.mass + b.mass)
|
||||
|
||||
a.vel = a.vel.Sub(u.Scaled(v * b.mass))
|
||||
b.vel = b.vel.Add(u.Scaled(v * a.mass))
|
||||
|
||||
bounced = true
|
||||
}
|
||||
}
|
||||
|
||||
if bounced {
|
||||
b.color = p.next()
|
||||
b.particles = append(b.particles,
|
||||
newParticleAt(b.pos, b.vel.Rotated(1).Scaled(rand.Float64())),
|
||||
newParticleAt(b.pos, b.vel.Rotated(2).Scaled(rand.Float64())),
|
||||
newParticleAt(b.pos, b.vel.Rotated(3).Scaled(rand.Float64())),
|
||||
newParticleAt(b.pos, b.vel.Rotated(4).Scaled(rand.Float64())),
|
||||
newParticleAt(b.pos, b.vel.Rotated(5).Scaled(rand.Float64())),
|
||||
newParticleAt(b.pos, b.vel.Rotated(6).Scaled(rand.Float64())),
|
||||
newParticleAt(b.pos, b.vel.Rotated(7).Scaled(rand.Float64())),
|
||||
newParticleAt(b.pos, b.vel.Rotated(8).Scaled(rand.Float64())),
|
||||
newParticleAt(b.pos, b.vel.Rotated(9).Scaled(rand.Float64())),
|
||||
|
||||
newParticleAt(b.pos, b.vel.Rotated(10).Scaled(rand.Float64()+1)),
|
||||
newParticleAt(b.pos, b.vel.Rotated(20).Scaled(rand.Float64()+1)),
|
||||
newParticleAt(b.pos, b.vel.Rotated(30).Scaled(rand.Float64()+1)),
|
||||
newParticleAt(b.pos, b.vel.Rotated(40).Scaled(rand.Float64()+1)),
|
||||
newParticleAt(b.pos, b.vel.Rotated(50).Scaled(rand.Float64()+1)),
|
||||
newParticleAt(b.pos, b.vel.Rotated(60).Scaled(rand.Float64()+1)),
|
||||
newParticleAt(b.pos, b.vel.Rotated(70).Scaled(rand.Float64()+1)),
|
||||
newParticleAt(b.pos, b.vel.Rotated(80).Scaled(rand.Float64()+1)),
|
||||
newParticleAt(b.pos, b.vel.Rotated(90).Scaled(rand.Float64()+1)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func newPalette(cc []color.Color) *Palette {
|
||||
colors := []color.RGBA{}
|
||||
|
||||
for _, v := range cc {
|
||||
if c, ok := v.(color.RGBA); ok {
|
||||
colors = append(colors, c)
|
||||
}
|
||||
}
|
||||
|
||||
return &Palette{colors, len(colors), 0}
|
||||
}
|
||||
|
||||
type Palette struct {
|
||||
colors []color.RGBA
|
||||
size int
|
||||
index int
|
||||
}
|
||||
|
||||
func (p *Palette) clone() *Palette {
|
||||
return &Palette{p.colors, p.size, p.index}
|
||||
}
|
||||
|
||||
func (p *Palette) next() color.RGBA {
|
||||
if p.index++; p.index >= p.size {
|
||||
p.index = 0
|
||||
}
|
||||
|
||||
return p.colors[p.index]
|
||||
}
|
||||
|
||||
func (p *Palette) color() color.RGBA {
|
||||
return p.colors[p.index]
|
||||
}
|
||||
|
||||
func (p *Palette) random() color.RGBA {
|
||||
p.index = rand.Intn(p.size)
|
||||
|
||||
return p.colors[p.index]
|
||||
}
|
||||
|
||||
var Colors = []color.Color{
|
||||
color.RGBA{190, 38, 51, 255},
|
||||
color.RGBA{224, 111, 139, 255},
|
||||
color.RGBA{73, 60, 43, 255},
|
||||
color.RGBA{164, 100, 34, 255},
|
||||
color.RGBA{235, 137, 49, 255},
|
||||
color.RGBA{247, 226, 107, 255},
|
||||
color.RGBA{47, 72, 78, 255},
|
||||
color.RGBA{68, 137, 26, 255},
|
||||
color.RGBA{163, 206, 39, 255},
|
||||
color.RGBA{0, 87, 132, 255},
|
||||
color.RGBA{49, 162, 242, 255},
|
||||
color.RGBA{178, 220, 239, 255},
|
||||
}
|
After Width: | Height: | Size: 6.9 KiB |
|
@ -0,0 +1,20 @@
|
|||
# Conway's Game of Lfe
|
||||
|
||||
Created by [Nathan Leniz](https://github.com/terakilobyte).
|
||||
Inspired by and heavily uses [the doc](https://golang.org/doc/play/life.go)
|
||||
|
||||
> The Game of Life, also known simply as Life, is a cellular automaton devised by the British mathematician John Horton Conway in 1970. The "game" is a zero-player game, meaning that its evolution is determined by its initial state, requiring no further input. One interacts with the Game of Life by creating an initial configuration and observing how it evolves, or, for advanced "players", by creating patterns with particular properties. The Game has been reprogrammed multiple times in various coding languages.
|
||||
|
||||
For more information, please see the [wikipedia](https://en.wikipedia.org/wiki/Conway%27s_Game_of_Life) article.
|
||||
|
||||
## Use
|
||||
|
||||
go run main.go -h
|
||||
-frameRate duration
|
||||
The framerate in milliseconds (default 33ms)
|
||||
-size int
|
||||
The size of each cell (default 5)
|
||||
-windowSize float
|
||||
The pixel size of one side of the grid (default 800)
|
||||
|
||||

|
After Width: | Height: | Size: 86 KiB |
|
@ -0,0 +1,74 @@
|
|||
package life
|
||||
|
||||
import (
|
||||
"github.com/faiface/pixel"
|
||||
"github.com/faiface/pixel/imdraw"
|
||||
"golang.org/x/image/colornames"
|
||||
)
|
||||
|
||||
// Shamelessly taken/inspired by https://golang.org/doc/play/life.go
|
||||
// Grid is the structure in which the cellular automota live
|
||||
type Grid struct {
|
||||
h int
|
||||
cellSize int
|
||||
Cells [][]bool
|
||||
}
|
||||
|
||||
// NewGrid constructs a new Grid
|
||||
func NewGrid(h, size int) *Grid {
|
||||
cells := make([][]bool, h)
|
||||
for i := 0; i < h; i++ {
|
||||
cells[i] = make([]bool, h)
|
||||
}
|
||||
return &Grid{h: h, cellSize: size, Cells: cells}
|
||||
}
|
||||
|
||||
// Alive returns whether the specified position is alive
|
||||
func (g *Grid) Alive(x, y int) bool {
|
||||
x += g.h
|
||||
x %= g.h
|
||||
y += g.h
|
||||
y %= g.h
|
||||
return g.Cells[y][x]
|
||||
}
|
||||
|
||||
// Set sets the state of a specific location
|
||||
func (g *Grid) Set(x, y int, state bool) {
|
||||
g.Cells[y][x] = state
|
||||
}
|
||||
|
||||
// Draw draws the grid
|
||||
func (g *Grid) Draw(imd *imdraw.IMDraw) {
|
||||
for i := 0; i < g.h; i++ {
|
||||
for j := 0; j < g.h; j++ {
|
||||
if g.Alive(i, j) {
|
||||
imd.Color = colornames.Black
|
||||
} else {
|
||||
imd.Color = colornames.White
|
||||
}
|
||||
imd.Push(
|
||||
pixel.V(float64(i*g.cellSize), float64(j*g.cellSize)),
|
||||
pixel.V(float64(i*g.cellSize+g.cellSize), float64(j*g.cellSize+g.cellSize)),
|
||||
)
|
||||
imd.Rectangle(0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Next returns the next state
|
||||
func (g *Grid) Next(x, y int) bool {
|
||||
// Count the adjacent cells that are alive.
|
||||
alive := 0
|
||||
for i := -1; i <= 1; i++ {
|
||||
for j := -1; j <= 1; j++ {
|
||||
if (j != 0 || i != 0) && g.Alive(x+i, y+j) {
|
||||
alive++
|
||||
}
|
||||
}
|
||||
}
|
||||
// Return next state according to the game rules:
|
||||
// exactly 3 neighbors: on,
|
||||
// exactly 2 neighbors: maintain current state,
|
||||
// otherwise: off.
|
||||
return alive == 3 || alive == 2 && g.Alive(x, y)
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
// Package life manages the "game" state
|
||||
// Shamelessly taken from https://golang.org/doc/play/life.go
|
||||
package life
|
||||
|
||||
import "math/rand"
|
||||
|
||||
// Life stores the state of a round of Conway's Game of Life.
|
||||
type Life struct {
|
||||
A, b *Grid
|
||||
h int
|
||||
}
|
||||
|
||||
// NewLife returns a new Life game state with a random initial state.
|
||||
func NewLife(h, size int) *Life {
|
||||
a := NewGrid(h, size)
|
||||
for i := 0; i < (h * h / 2); i++ {
|
||||
a.Set(rand.Intn(h), rand.Intn(h), true)
|
||||
}
|
||||
return &Life{
|
||||
A: a, b: NewGrid(h, size),
|
||||
h: h,
|
||||
}
|
||||
}
|
||||
|
||||
// Step advances the game by one instant, recomputing and updating all cells.
|
||||
func (l *Life) Step() {
|
||||
// Update the state of the next field (b) from the current field (a).
|
||||
for y := 0; y < l.h; y++ {
|
||||
for x := 0; x < l.h; x++ {
|
||||
l.b.Set(x, y, l.A.Next(x, y))
|
||||
}
|
||||
}
|
||||
// Swap fields a and b.
|
||||
l.A, l.b = l.b, l.A
|
||||
}
|
|
@ -0,0 +1,64 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"math/rand"
|
||||
"time"
|
||||
|
||||
"golang.org/x/image/colornames"
|
||||
|
||||
"github.com/faiface/pixel"
|
||||
"github.com/faiface/pixel/examples/community/game_of_life/life"
|
||||
"github.com/faiface/pixel/imdraw"
|
||||
"github.com/faiface/pixel/pixelgl"
|
||||
)
|
||||
|
||||
var (
|
||||
size *int
|
||||
windowSize *float64
|
||||
frameRate *time.Duration
|
||||
)
|
||||
|
||||
func init() {
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
size = flag.Int("size", 5, "The size of each cell")
|
||||
windowSize = flag.Float64("windowSize", 800, "The pixel size of one side of the grid")
|
||||
frameRate = flag.Duration("frameRate", 33*time.Millisecond, "The framerate in milliseconds")
|
||||
flag.Parse()
|
||||
}
|
||||
|
||||
func main() {
|
||||
pixelgl.Run(run)
|
||||
}
|
||||
|
||||
func run() {
|
||||
|
||||
cfg := pixelgl.WindowConfig{
|
||||
Title: "Pixel Rocks!",
|
||||
Bounds: pixel.R(0, 0, *windowSize, *windowSize),
|
||||
VSync: true,
|
||||
}
|
||||
win, err := pixelgl.NewWindow(cfg)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
win.Clear(colornames.White)
|
||||
|
||||
// since the game board is square, rows and cols will be the same
|
||||
rows := int(*windowSize) / *size
|
||||
|
||||
gridDraw := imdraw.New(nil)
|
||||
game := life.NewLife(rows, *size)
|
||||
tick := time.Tick(*frameRate)
|
||||
for !win.Closed() {
|
||||
// game loop
|
||||
select {
|
||||
case <-tick:
|
||||
gridDraw.Clear()
|
||||
game.A.Draw(gridDraw)
|
||||
gridDraw.Draw(win)
|
||||
game.Step()
|
||||
}
|
||||
win.Update()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
# Isometric view basics
|
||||
|
||||
Created by [Sergio Vera](https://github.com/svera).
|
||||
|
||||
Isometric view is a display method used to create an illusion of 3D for an otherwise 2D game - sometimes referred to as pseudo 3D or 2.5D.
|
||||
|
||||
Implementing an isometric view can be done in many ways, but for the sake of simplicity we'll implement a tile-based approach, which is the most efficient and widely used method.
|
||||
|
||||
In the tile-based approach, each visual element is broken down into smaller pieces, called tiles, of a standard size. These tiles will be arranged to form the game world according to pre-determined level data - usually a 2D array.
|
||||
|
||||
For a detailed explanation about the maths behind this, read [http://clintbellanger.net/articles/isometric_math/](http://clintbellanger.net/articles/isometric_math/).
|
||||
|
||||

|
After Width: | Height: | Size: 26 KiB |
|
@ -0,0 +1,102 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"image"
|
||||
"os"
|
||||
|
||||
"github.com/faiface/pixel"
|
||||
"github.com/faiface/pixel/pixelgl"
|
||||
|
||||
_ "image/png"
|
||||
)
|
||||
|
||||
const (
|
||||
windowWidth = 800
|
||||
windowHeight = 800
|
||||
// sprite tiles are squared, 64x64 size
|
||||
tileSize = 64
|
||||
f = 0 // floor identifier
|
||||
w = 1 // wall identifier
|
||||
)
|
||||
|
||||
var levelData = [][]uint{
|
||||
{f, f, f, f, f, f}, // This row will be rendered in the lower left part of the screen (closer to the viewer)
|
||||
{w, f, f, f, f, w},
|
||||
{w, f, f, f, f, w},
|
||||
{w, f, f, f, f, w},
|
||||
{w, f, f, f, f, w},
|
||||
{w, w, w, w, w, w}, // And this in the upper right
|
||||
}
|
||||
var win *pixelgl.Window
|
||||
var offset = pixel.V(400, 325)
|
||||
var floorTile, wallTile *pixel.Sprite
|
||||
|
||||
func loadPicture(path string) (pixel.Picture, error) {
|
||||
file, err := os.Open(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer file.Close()
|
||||
img, _, err := image.Decode(file)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return pixel.PictureDataFromImage(img), nil
|
||||
}
|
||||
|
||||
func run() {
|
||||
var err error
|
||||
|
||||
cfg := pixelgl.WindowConfig{
|
||||
Title: "Isometric demo",
|
||||
Bounds: pixel.R(0, 0, windowWidth, windowHeight),
|
||||
VSync: true,
|
||||
}
|
||||
win, err = pixelgl.NewWindow(cfg)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
pic, err := loadPicture("castle.png")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
wallTile = pixel.NewSprite(pic, pixel.R(0, 448, tileSize, 512))
|
||||
floorTile = pixel.NewSprite(pic, pixel.R(0, 128, tileSize, 192))
|
||||
|
||||
depthSort()
|
||||
|
||||
for !win.Closed() {
|
||||
win.Update()
|
||||
}
|
||||
}
|
||||
|
||||
// Draw level data tiles to window, from farthest to closest.
|
||||
// In order to achieve the depth effect, we need to render tiles up to down, being lower
|
||||
// closer to the viewer (see painter's algorithm). To do that, we need to process levelData in reverse order,
|
||||
// so its first row is rendered last, as OpenGL considers its origin to be in the lower left corner of the display.
|
||||
func depthSort() {
|
||||
for x := len(levelData) - 1; x >= 0; x-- {
|
||||
for y := len(levelData[x]) - 1; y >= 0; y-- {
|
||||
isoCoords := cartesianToIso(pixel.V(float64(x), float64(y)))
|
||||
mat := pixel.IM.Moved(offset.Add(isoCoords))
|
||||
// Not really needed, just put to show bigger blocks
|
||||
mat = mat.ScaledXY(win.Bounds().Center(), pixel.V(2, 2))
|
||||
tileType := levelData[x][y]
|
||||
if tileType == f {
|
||||
floorTile.Draw(win, mat)
|
||||
} else {
|
||||
wallTile.Draw(win, mat)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func cartesianToIso(pt pixel.Vec) pixel.Vec {
|
||||
return pixel.V((pt.X-pt.Y)*(tileSize/2), (pt.X+pt.Y)*(tileSize/4))
|
||||
}
|
||||
|
||||
func main() {
|
||||
pixelgl.Run(run)
|
||||
}
|
After Width: | Height: | Size: 126 KiB |
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2017 stephen
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
|
@ -0,0 +1,19 @@
|
|||
# Maze generator in Go
|
||||
|
||||
Created by [Stephen Chavez](https://github.com/redragonx)
|
||||
|
||||
This uses the game engine: Pixel. Install it here: https://github.com/faiface/pixel
|
||||
|
||||
I made this to improve my understanding of Go and some game concepts with some basic maze generating algorithms.
|
||||
|
||||
Controls: Press 'R' to restart the maze.
|
||||
|
||||
Optional command-line arguments: `go run ./maze-generator.go`
|
||||
- `-w` sets the maze's width in pixels.
|
||||
- `-h` sets the maze's height in pixels.
|
||||
- `-c` sets the maze cell's size in pixels.
|
||||
|
||||
Code based on the Recursive backtracker algorithm.
|
||||
- https://en.wikipedia.org/wiki/Maze_generation_algorithm#Recursive_backtracker
|
||||
|
||||

|
|
@ -0,0 +1,317 @@
|
|||
package main
|
||||
|
||||
// Code based on the Recursive backtracker algorithm.
|
||||
// https://en.wikipedia.org/wiki/Maze_generation_algorithm#Recursive_backtracker
|
||||
// See https://youtu.be/HyK_Q5rrcr4 as an example
|
||||
// YouTube example ported to Go for the Pixel library.
|
||||
|
||||
// Created by Stephen Chavez
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"time"
|
||||
|
||||
"github.com/faiface/pixel"
|
||||
"github.com/faiface/pixel/examples/community/maze/stack"
|
||||
"github.com/faiface/pixel/imdraw"
|
||||
"github.com/faiface/pixel/pixelgl"
|
||||
|
||||
"github.com/pkg/profile"
|
||||
"golang.org/x/image/colornames"
|
||||
)
|
||||
|
||||
var visitedColor = pixel.RGB(0.5, 0, 1).Mul(pixel.Alpha(0.35))
|
||||
var hightlightColor = pixel.RGB(0.3, 0, 0).Mul(pixel.Alpha(0.45))
|
||||
var debug = false
|
||||
|
||||
type cell struct {
|
||||
walls [4]bool // Wall order: top, right, bottom, left
|
||||
|
||||
row int
|
||||
col int
|
||||
visited bool
|
||||
}
|
||||
|
||||
func (c *cell) Draw(imd *imdraw.IMDraw, wallSize int) {
|
||||
drawCol := c.col * wallSize // x
|
||||
drawRow := c.row * wallSize // y
|
||||
|
||||
imd.Color = colornames.White
|
||||
if c.walls[0] {
|
||||
// top line
|
||||
imd.Push(pixel.V(float64(drawCol), float64(drawRow)), pixel.V(float64(drawCol+wallSize), float64(drawRow)))
|
||||
imd.Line(3)
|
||||
}
|
||||
if c.walls[1] {
|
||||
// right Line
|
||||
imd.Push(pixel.V(float64(drawCol+wallSize), float64(drawRow)), pixel.V(float64(drawCol+wallSize), float64(drawRow+wallSize)))
|
||||
imd.Line(3)
|
||||
}
|
||||
if c.walls[2] {
|
||||
// bottom line
|
||||
imd.Push(pixel.V(float64(drawCol+wallSize), float64(drawRow+wallSize)), pixel.V(float64(drawCol), float64(drawRow+wallSize)))
|
||||
imd.Line(3)
|
||||
}
|
||||
if c.walls[3] {
|
||||
// left line
|
||||
imd.Push(pixel.V(float64(drawCol), float64(drawRow+wallSize)), pixel.V(float64(drawCol), float64(drawRow)))
|
||||
imd.Line(3)
|
||||
}
|
||||
imd.EndShape = imdraw.SharpEndShape
|
||||
|
||||
if c.visited {
|
||||
imd.Color = visitedColor
|
||||
imd.Push(pixel.V(float64(drawCol), (float64(drawRow))), pixel.V(float64(drawCol+wallSize), float64(drawRow+wallSize)))
|
||||
imd.Rectangle(0)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *cell) GetNeighbors(grid []*cell, cols int, rows int) ([]*cell, error) {
|
||||
neighbors := []*cell{}
|
||||
j := c.row
|
||||
i := c.col
|
||||
|
||||
top, _ := getCellAt(i, j-1, cols, rows, grid)
|
||||
right, _ := getCellAt(i+1, j, cols, rows, grid)
|
||||
bottom, _ := getCellAt(i, j+1, cols, rows, grid)
|
||||
left, _ := getCellAt(i-1, j, cols, rows, grid)
|
||||
|
||||
if top != nil && !top.visited {
|
||||
neighbors = append(neighbors, top)
|
||||
}
|
||||
if right != nil && !right.visited {
|
||||
neighbors = append(neighbors, right)
|
||||
}
|
||||
if bottom != nil && !bottom.visited {
|
||||
neighbors = append(neighbors, bottom)
|
||||
}
|
||||
if left != nil && !left.visited {
|
||||
neighbors = append(neighbors, left)
|
||||
}
|
||||
|
||||
if len(neighbors) == 0 {
|
||||
return nil, errors.New("We checked all cells...")
|
||||
}
|
||||
return neighbors, nil
|
||||
}
|
||||
|
||||
func (c *cell) GetRandomNeighbor(grid []*cell, cols int, rows int) (*cell, error) {
|
||||
neighbors, err := c.GetNeighbors(grid, cols, rows)
|
||||
if neighbors == nil {
|
||||
return nil, err
|
||||
}
|
||||
nBig, err := rand.Int(rand.Reader, big.NewInt(int64(len(neighbors))))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
randomIndex := nBig.Int64()
|
||||
return neighbors[randomIndex], nil
|
||||
}
|
||||
|
||||
func (c *cell) hightlight(imd *imdraw.IMDraw, wallSize int) {
|
||||
x := c.col * wallSize
|
||||
y := c.row * wallSize
|
||||
|
||||
imd.Color = hightlightColor
|
||||
imd.Push(pixel.V(float64(x), float64(y)), pixel.V(float64(x+wallSize), float64(y+wallSize)))
|
||||
imd.Rectangle(0)
|
||||
}
|
||||
|
||||
func newCell(col int, row int) *cell {
|
||||
newCell := new(cell)
|
||||
newCell.row = row
|
||||
newCell.col = col
|
||||
|
||||
for i := range newCell.walls {
|
||||
newCell.walls[i] = true
|
||||
}
|
||||
return newCell
|
||||
}
|
||||
|
||||
// Creates the inital maze slice for use.
|
||||
func initGrid(cols, rows int) []*cell {
|
||||
grid := []*cell{}
|
||||
for j := 0; j < rows; j++ {
|
||||
for i := 0; i < cols; i++ {
|
||||
newCell := newCell(i, j)
|
||||
grid = append(grid, newCell)
|
||||
}
|
||||
}
|
||||
return grid
|
||||
}
|
||||
|
||||
func setupMaze(cols, rows int) ([]*cell, *stack.Stack, *cell) {
|
||||
// Make an empty grid
|
||||
grid := initGrid(cols, rows)
|
||||
backTrackStack := stack.NewStack(len(grid))
|
||||
currentCell := grid[0]
|
||||
|
||||
return grid, backTrackStack, currentCell
|
||||
}
|
||||
|
||||
func cellIndex(i, j, cols, rows int) int {
|
||||
if i < 0 || j < 0 || i > cols-1 || j > rows-1 {
|
||||
return -1
|
||||
}
|
||||
return i + j*cols
|
||||
}
|
||||
|
||||
func getCellAt(i int, j int, cols int, rows int, grid []*cell) (*cell, error) {
|
||||
possibleIndex := cellIndex(i, j, cols, rows)
|
||||
|
||||
if possibleIndex == -1 {
|
||||
return nil, fmt.Errorf("cellIndex: CellIndex is a negative number %d", possibleIndex)
|
||||
}
|
||||
return grid[possibleIndex], nil
|
||||
}
|
||||
|
||||
func removeWalls(a *cell, b *cell) {
|
||||
x := a.col - b.col
|
||||
|
||||
if x == 1 {
|
||||
a.walls[3] = false
|
||||
b.walls[1] = false
|
||||
} else if x == -1 {
|
||||
a.walls[1] = false
|
||||
b.walls[3] = false
|
||||
}
|
||||
|
||||
y := a.row - b.row
|
||||
|
||||
if y == 1 {
|
||||
a.walls[0] = false
|
||||
b.walls[2] = false
|
||||
} else if y == -1 {
|
||||
a.walls[2] = false
|
||||
b.walls[0] = false
|
||||
}
|
||||
}
|
||||
|
||||
func run() {
|
||||
// unsiged integers, because easier parsing error checks.
|
||||
// We must convert these to intergers, as done below...
|
||||
uScreenWidth, uScreenHeight, uWallSize := parseArgs()
|
||||
|
||||
var (
|
||||
// In pixels
|
||||
// Defualt is 800x800x40 = 20x20 wallgrid
|
||||
screenWidth = int(uScreenWidth)
|
||||
screenHeight = int(uScreenHeight)
|
||||
wallSize = int(uWallSize)
|
||||
|
||||
frames = 0
|
||||
second = time.Tick(time.Second)
|
||||
|
||||
grid = []*cell{}
|
||||
cols = screenWidth / wallSize
|
||||
rows = screenHeight / wallSize
|
||||
currentCell = new(cell)
|
||||
backTrackStack = stack.NewStack(1)
|
||||
)
|
||||
|
||||
// Set game FPS manually
|
||||
fps := time.Tick(time.Second / 60)
|
||||
|
||||
cfg := pixelgl.WindowConfig{
|
||||
Title: "Pixel Rocks! - Maze example",
|
||||
Bounds: pixel.R(0, 0, float64(screenHeight), float64(screenWidth)),
|
||||
}
|
||||
|
||||
win, err := pixelgl.NewWindow(cfg)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
grid, backTrackStack, currentCell = setupMaze(cols, rows)
|
||||
|
||||
gridIMDraw := imdraw.New(nil)
|
||||
|
||||
for !win.Closed() {
|
||||
if win.JustReleased(pixelgl.KeyR) {
|
||||
fmt.Println("R pressed")
|
||||
grid, backTrackStack, currentCell = setupMaze(cols, rows)
|
||||
}
|
||||
|
||||
win.Clear(colornames.Gray)
|
||||
gridIMDraw.Clear()
|
||||
|
||||
for i := range grid {
|
||||
grid[i].Draw(gridIMDraw, wallSize)
|
||||
}
|
||||
|
||||
// step 1
|
||||
// Make the initial cell the current cell and mark it as visited
|
||||
currentCell.visited = true
|
||||
currentCell.hightlight(gridIMDraw, wallSize)
|
||||
|
||||
// step 2.1
|
||||
// If the current cell has any neighbours which have not been visited
|
||||
// Choose a random unvisited cell
|
||||
nextCell, _ := currentCell.GetRandomNeighbor(grid, cols, rows)
|
||||
if nextCell != nil && !nextCell.visited {
|
||||
// step 2.2
|
||||
// Push the current cell to the stack
|
||||
backTrackStack.Push(currentCell)
|
||||
|
||||
// step 2.3
|
||||
// Remove the wall between the current cell and the chosen cell
|
||||
|
||||
removeWalls(currentCell, nextCell)
|
||||
|
||||
// step 2.4
|
||||
// Make the chosen cell the current cell and mark it as visited
|
||||
nextCell.visited = true
|
||||
currentCell = nextCell
|
||||
} else if backTrackStack.Len() > 0 {
|
||||
currentCell = backTrackStack.Pop().(*cell)
|
||||
}
|
||||
|
||||
gridIMDraw.Draw(win)
|
||||
win.Update()
|
||||
<-fps
|
||||
updateFPSDisplay(win, &cfg, &frames, grid, second)
|
||||
}
|
||||
}
|
||||
|
||||
// Parses the maze arguments, all of them are optional.
|
||||
// Uses uint as implicit error checking :)
|
||||
func parseArgs() (uint, uint, uint) {
|
||||
var mazeWidthPtr = flag.Uint("w", 800, "w sets the maze's width in pixels.")
|
||||
var mazeHeightPtr = flag.Uint("h", 800, "h sets the maze's height in pixels.")
|
||||
var wallSizePtr = flag.Uint("c", 40, "c sets the maze cell's size in pixels.")
|
||||
|
||||
flag.Parse()
|
||||
|
||||
// If these aren't default values AND if they're not the same values.
|
||||
// We should warn the user that the maze will look funny.
|
||||
if *mazeWidthPtr != 800 || *mazeHeightPtr != 800 {
|
||||
if *mazeWidthPtr != *mazeHeightPtr {
|
||||
fmt.Printf("WARNING: maze width: %d and maze height: %d don't match. \n", *mazeWidthPtr, *mazeHeightPtr)
|
||||
fmt.Println("Maze will look funny because the maze size is bond to the window size!")
|
||||
}
|
||||
}
|
||||
|
||||
return *mazeWidthPtr, *mazeHeightPtr, *wallSizePtr
|
||||
}
|
||||
|
||||
func updateFPSDisplay(win *pixelgl.Window, cfg *pixelgl.WindowConfig, frames *int, grid []*cell, second <-chan time.Time) {
|
||||
*frames++
|
||||
select {
|
||||
case <-second:
|
||||
win.SetTitle(fmt.Sprintf("%s | FPS: %d with %d Cells", cfg.Title, *frames, len(grid)))
|
||||
*frames = 0
|
||||
default:
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func main() {
|
||||
if debug {
|
||||
defer profile.Start().Stop()
|
||||
}
|
||||
pixelgl.Run(run)
|
||||
}
|
After Width: | Height: | Size: 14 KiB |
|
@ -0,0 +1,86 @@
|
|||
package stack
|
||||
|
||||
type Stack struct {
|
||||
top *Element
|
||||
size int
|
||||
max int
|
||||
}
|
||||
|
||||
type Element struct {
|
||||
value interface{}
|
||||
next *Element
|
||||
}
|
||||
|
||||
func NewStack(max int) *Stack {
|
||||
return &Stack{max: max}
|
||||
}
|
||||
|
||||
// Return the stack's length
|
||||
func (s *Stack) Len() int {
|
||||
return s.size
|
||||
}
|
||||
|
||||
// Return the stack's max
|
||||
func (s *Stack) Max() int {
|
||||
return s.max
|
||||
}
|
||||
|
||||
// Push a new element onto the stack
|
||||
func (s *Stack) Push(value interface{}) {
|
||||
if s.size+1 > s.max {
|
||||
if last := s.PopLast(); last == nil {
|
||||
panic("Unexpected nil in stack")
|
||||
}
|
||||
}
|
||||
s.top = &Element{value, s.top}
|
||||
s.size++
|
||||
}
|
||||
|
||||
// Remove the top element from the stack and return it's value
|
||||
// If the stack is empty, return nil
|
||||
func (s *Stack) Pop() (value interface{}) {
|
||||
if s.size > 0 {
|
||||
value, s.top = s.top.value, s.top.next
|
||||
s.size--
|
||||
return
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Stack) PopLast() (value interface{}) {
|
||||
if lastElem := s.popLast(s.top); lastElem != nil {
|
||||
return lastElem.value
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
//Peek returns a top without removing it from list
|
||||
func (s *Stack) Peek() (value interface{}, exists bool) {
|
||||
exists = false
|
||||
if s.size > 0 {
|
||||
value = s.top.value
|
||||
exists = true
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (s *Stack) popLast(elem *Element) *Element {
|
||||
if elem == nil {
|
||||
return nil
|
||||
}
|
||||
// not last because it has next and a grandchild
|
||||
if elem.next != nil && elem.next.next != nil {
|
||||
return s.popLast(elem.next)
|
||||
}
|
||||
|
||||
// current elem is second from bottom, as next elem has no child
|
||||
if elem.next != nil && elem.next.next == nil {
|
||||
last := elem.next
|
||||
// make current elem bottom of stack by removing its next element
|
||||
elem.next = nil
|
||||
s.size--
|
||||
return last
|
||||
}
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
# Parallax scrolling demo
|
||||
|
||||
Created by [Sergio Vera](https://github.com/svera)
|
||||
|
||||
This example shows how to implement an infinite side scrolling background with a depth effect, using [parallax scrolling](https://en.wikipedia.org/wiki/Parallax_scrolling). Code is based in the [infinite scrolling background](https://github.com/faiface/pixel/tree/master/examples/community/scrolling-background) demo.
|
||||
|
||||
Credits to [Peter Hellberg](https://github.com/peterhellberg) for the improved background images.
|
||||
|
||||

|
After Width: | Height: | Size: 38 KiB |
After Width: | Height: | Size: 25 KiB |
|
@ -0,0 +1,74 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"image"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
_ "image/png"
|
||||
|
||||
"github.com/faiface/pixel"
|
||||
"github.com/faiface/pixel/pixelgl"
|
||||
)
|
||||
|
||||
func loadPicture(path string) (pixel.Picture, error) {
|
||||
file, err := os.Open(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer file.Close()
|
||||
img, _, err := image.Decode(file)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return pixel.PictureDataFromImage(img), nil
|
||||
}
|
||||
|
||||
const (
|
||||
windowWidth = 600
|
||||
windowHeight = 450
|
||||
foregroundHeight = 149
|
||||
// This is the scrolling speed (pixels per second)
|
||||
// Negative values will make background to scroll to the left,
|
||||
// positive to the right.
|
||||
backgroundSpeed = -60
|
||||
foregroundSpeed = -120
|
||||
)
|
||||
|
||||
func run() {
|
||||
cfg := pixelgl.WindowConfig{
|
||||
Title: "Parallax scrolling demo",
|
||||
Bounds: pixel.R(0, 0, windowWidth, windowHeight),
|
||||
VSync: true,
|
||||
}
|
||||
win, err := pixelgl.NewWindow(cfg)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// Pic must have double the width of the window, as it will scroll to the left or right
|
||||
picBackground, err := loadPicture("background.png")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
picForeground, err := loadPicture("foreground.png")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
background := NewScrollingBackground(picBackground, windowWidth, windowHeight, backgroundSpeed)
|
||||
foreground := NewScrollingBackground(picForeground, windowWidth, foregroundHeight, foregroundSpeed)
|
||||
|
||||
last := time.Now()
|
||||
for !win.Closed() {
|
||||
dt := time.Since(last).Seconds()
|
||||
last = time.Now()
|
||||
background.Update(win, dt)
|
||||
foreground.Update(win, dt)
|
||||
win.Update()
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
pixelgl.Run(run)
|
||||
}
|
After Width: | Height: | Size: 91 KiB |
|
@ -0,0 +1,64 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"math"
|
||||
|
||||
"github.com/faiface/pixel"
|
||||
"github.com/faiface/pixel/pixelgl"
|
||||
)
|
||||
|
||||
// ScrollingBackground stores all needed information to scroll a background
|
||||
// to the left or right
|
||||
type ScrollingBackground struct {
|
||||
width float64
|
||||
height float64
|
||||
displacement float64
|
||||
speed float64
|
||||
backgrounds [2]*pixel.Sprite
|
||||
positions [2]pixel.Vec
|
||||
}
|
||||
|
||||
// NewScrollingBackground construct and returns a new instance of scrollingBackground,
|
||||
// positioning the background images according to the speed value
|
||||
func NewScrollingBackground(pic pixel.Picture, width, height, speed float64) *ScrollingBackground {
|
||||
sb := &ScrollingBackground{
|
||||
width: width,
|
||||
height: height,
|
||||
speed: speed,
|
||||
backgrounds: [2]*pixel.Sprite{
|
||||
pixel.NewSprite(pic, pixel.R(0, 0, width, height)),
|
||||
pixel.NewSprite(pic, pixel.R(width, 0, width*2, height)),
|
||||
},
|
||||
}
|
||||
|
||||
sb.positionImages()
|
||||
return sb
|
||||
}
|
||||
|
||||
// If scrolling speed > 0, put second background image ouside the screen,
|
||||
// at the left side, otherwise put it at the right side.
|
||||
func (sb *ScrollingBackground) positionImages() {
|
||||
if sb.speed > 0 {
|
||||
sb.positions = [2]pixel.Vec{
|
||||
pixel.V(sb.width/2, sb.height/2),
|
||||
pixel.V((sb.width/2)-sb.width, sb.height/2),
|
||||
}
|
||||
} else {
|
||||
sb.positions = [2]pixel.Vec{
|
||||
pixel.V(sb.width/2, sb.height/2),
|
||||
pixel.V(sb.width+(sb.width/2), sb.height/2),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update will move backgrounds certain pixels, depending of the amount of time passed
|
||||
func (sb *ScrollingBackground) Update(win *pixelgl.Window, dt float64) {
|
||||
if math.Abs(sb.displacement) >= sb.width {
|
||||
sb.displacement = 0
|
||||
sb.positions[0], sb.positions[1] = sb.positions[1], sb.positions[0]
|
||||
}
|
||||
d := pixel.V(sb.displacement, 0)
|
||||
sb.backgrounds[0].Draw(win, pixel.IM.Moved(sb.positions[0].Add(d)))
|
||||
sb.backgrounds[1].Draw(win, pixel.IM.Moved(sb.positions[1].Add(d)))
|
||||
sb.displacement += sb.speed * dt
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
# Procedural 1D terrain generator
|
||||
|
||||
Created by [Sergio Vera](https://github.com/svera).
|
||||
|
||||
This is a demo of a 1D terrain generator using [Perlin noise](https://en.wikipedia.org/wiki/Perlin_noise) algorithm.
|
||||
Press *space* to generate a random terrain.
|
||||
|
||||
Uses [Go-Perlin](https://github.com/aquilax/go-perlin).
|
||||
|
||||
Texture by [hh316](https://hhh316.deviantart.com/art/Seamless-stone-cliff-face-mountain-texture-377076626).
|
||||
|
||||

|
|
@ -0,0 +1,104 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"image"
|
||||
"math/rand"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
_ "image/jpeg"
|
||||
|
||||
perlin "github.com/aquilax/go-perlin"
|
||||
"github.com/faiface/pixel"
|
||||
"github.com/faiface/pixel/imdraw"
|
||||
"github.com/faiface/pixel/pixelgl"
|
||||
"golang.org/x/image/colornames"
|
||||
)
|
||||
|
||||
const (
|
||||
width = 800
|
||||
height = 600
|
||||
// Top of the mountain must be around the half of the screen height
|
||||
verticalOffset = height / 2
|
||||
// Perlin noise provides variations in values between -1 and 1,
|
||||
// we multiply those so they're visible on screen
|
||||
scale = 100
|
||||
waveLength = 100
|
||||
alpha = 2.
|
||||
beta = 2.
|
||||
n = 3
|
||||
maximumSeedValue = 100
|
||||
)
|
||||
|
||||
func init() {
|
||||
rand.Seed(time.Now().UTC().UnixNano())
|
||||
}
|
||||
|
||||
func main() {
|
||||
pixelgl.Run(run)
|
||||
}
|
||||
|
||||
func run() {
|
||||
cfg := pixelgl.WindowConfig{
|
||||
Title: "Procedural terrain 1D",
|
||||
Bounds: pixel.R(0, 0, width, height),
|
||||
VSync: true,
|
||||
}
|
||||
win, err := pixelgl.NewWindow(cfg)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
pic, err := loadPicture("stone.jpg")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
imd := imdraw.New(pic)
|
||||
|
||||
drawTerrain(win, imd)
|
||||
|
||||
for !win.Closed() {
|
||||
if win.JustPressed(pixelgl.KeySpace) {
|
||||
drawTerrain(win, imd)
|
||||
}
|
||||
win.Update()
|
||||
}
|
||||
}
|
||||
|
||||
func loadPicture(path string) (pixel.Picture, error) {
|
||||
file, err := os.Open(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer file.Close()
|
||||
img, _, err := image.Decode(file)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return pixel.PictureDataFromImage(img), nil
|
||||
}
|
||||
|
||||
func drawTerrain(win *pixelgl.Window, imd *imdraw.IMDraw) {
|
||||
var seed = rand.Int63n(maximumSeedValue)
|
||||
p := perlin.NewPerlin(alpha, beta, n, seed)
|
||||
|
||||
imd.Clear()
|
||||
win.Clear(colornames.Skyblue)
|
||||
for x := 0.; x < width; x++ {
|
||||
y := p.Noise1D(x/waveLength)*scale + verticalOffset
|
||||
renderTexturedLine(x, y, imd)
|
||||
}
|
||||
imd.Draw(win)
|
||||
}
|
||||
|
||||
// Render a textured line in position x with a height y.
|
||||
// Note that the textured line is just a 1 px width rectangle.
|
||||
// We push the opposite vertices of that rectangle and specify the points of the
|
||||
// texture we want to apply to them. Pixel will fill the rest of the rectangle interpolating the texture.
|
||||
func renderTexturedLine(x, y float64, imd *imdraw.IMDraw) {
|
||||
imd.Intensity = 1.
|
||||
imd.Picture = pixel.V(x, 0)
|
||||
imd.Push(pixel.V(x, 0))
|
||||
imd.Picture = pixel.V(x+1, y)
|
||||
imd.Push(pixel.V(x+1, y))
|
||||
imd.Rectangle(0)
|
||||
}
|
After Width: | Height: | Size: 759 KiB |
After Width: | Height: | Size: 368 KiB |
|
@ -0,0 +1,22 @@
|
|||
# raycaster
|
||||
|
||||
A raycaster made by [Peter Hellberg](https://github.com/peterhellberg/) as part of his [pixel-experiments](https://github.com/peterhellberg/pixel-experiments).
|
||||
|
||||
Based on Lode’s article on [raycasting](http://lodev.org/cgtutor/raycasting.html).
|
||||
|
||||
## Controls
|
||||
|
||||
WASD for strafing and arrow keys for rotation.
|
||||
|
||||
Place blocks using the number keys.
|
||||
|
||||
## Screenshots
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
## Links
|
||||
|
||||
- https://github.com/peterhellberg/pixel-experiments/tree/master/raycaster
|
||||
- https://gist.github.com/peterhellberg/835eccabf95800555120cc8f0c9e16c2
|
After Width: | Height: | Size: 47 KiB |
|
@ -0,0 +1,7 @@
|
|||
# Infinite scrolling background demo
|
||||
|
||||
Created by [Sergio Vera](https://github.com/svera)
|
||||
|
||||
This example shows how to implement an infinite side scrolling background.
|
||||
|
||||
Credits to [Peter Hellberg](https://github.com/peterhellberg) for the improved background image.
|
After Width: | Height: | Size: 56 KiB |
|
@ -0,0 +1,83 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"image"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
_ "image/png"
|
||||
|
||||
"github.com/faiface/pixel"
|
||||
"github.com/faiface/pixel/pixelgl"
|
||||
)
|
||||
|
||||
func loadPicture(path string) (pixel.Picture, error) {
|
||||
file, err := os.Open(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer file.Close()
|
||||
img, _, err := image.Decode(file)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return pixel.PictureDataFromImage(img), nil
|
||||
}
|
||||
|
||||
const (
|
||||
windowWidth = 600
|
||||
windowHeight = 450
|
||||
// This is the scrolling speed
|
||||
linesPerSecond = 60
|
||||
)
|
||||
|
||||
func run() {
|
||||
cfg := pixelgl.WindowConfig{
|
||||
Title: "Scrolling background demo",
|
||||
Bounds: pixel.R(0, 0, windowWidth, windowHeight),
|
||||
VSync: true,
|
||||
}
|
||||
win, err := pixelgl.NewWindow(cfg)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// Pic must have double the width of the window, as it will scroll to the left
|
||||
pic, err := loadPicture("gamebackground.png")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// Backgrounds are made taking the left and right halves of the image
|
||||
background1 := pixel.NewSprite(pic, pixel.R(0, 0, windowWidth, windowHeight))
|
||||
background2 := pixel.NewSprite(pic, pixel.R(windowWidth, 0, windowWidth*2, windowHeight))
|
||||
|
||||
// In the beginning, vector1 will put background1 filling the whole window, while vector2 will
|
||||
// put background2 just at the right side of the window, out of view
|
||||
vector1 := pixel.V(windowWidth/2, windowHeight/2)
|
||||
vector2 := pixel.V(windowWidth+(windowWidth/2), windowHeight/2)
|
||||
|
||||
i := float64(0)
|
||||
last := time.Now()
|
||||
for !win.Closed() {
|
||||
dt := time.Since(last).Seconds()
|
||||
last = time.Now()
|
||||
// When one of the backgrounds has completely scrolled, we swap displacement vectors,
|
||||
// so the backgrounds will swap positions too regarding the previous iteration,
|
||||
// thus making the background endless.
|
||||
if i <= -windowWidth {
|
||||
i = 0
|
||||
vector1, vector2 = vector2, vector1
|
||||
}
|
||||
// This delta vector will move the backgrounds to the left
|
||||
d := pixel.V(-i, 0)
|
||||
background1.Draw(win, pixel.IM.Moved(vector1.Sub(d)))
|
||||
background2.Draw(win, pixel.IM.Moved(vector2.Sub(d)))
|
||||
i -= linesPerSecond * dt
|
||||
win.Update()
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
pixelgl.Run(run)
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
# starfield
|
||||
|
||||
Classic starfield… with [supposedly accurate stellar colors](http://www.vendian.org/mncharity/dir3/starcolor/)
|
||||
|
||||
Made by [Peter Hellberg](https://github.com/peterhellberg/) as part of his [pixel-experiments](https://github.com/peterhellberg/pixel-experiments)
|
||||
|
||||
## Controls
|
||||
|
||||
Arrow up and down to change speed. Space bar to almost stop.
|
||||
|
||||
## Screenshots
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
## Links
|
||||
|
||||
- https://github.com/peterhellberg/pixel-experiments/tree/master/starfield
|
||||
- https://gist.github.com/peterhellberg/4018e228cced61a0bb26991e49299c96
|
After Width: | Height: | Size: 6.0 KiB |
|
@ -0,0 +1,165 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"image/color"
|
||||
"math/rand"
|
||||
"time"
|
||||
|
||||
"github.com/faiface/pixel"
|
||||
"github.com/faiface/pixel/imdraw"
|
||||
"github.com/faiface/pixel/pixelgl"
|
||||
)
|
||||
|
||||
const w, h = float64(1024), float64(512)
|
||||
|
||||
var speed = float64(200)
|
||||
|
||||
var stars [1024]*star
|
||||
|
||||
func init() {
|
||||
rand.Seed(4)
|
||||
|
||||
for i := 0; i < len(stars); i++ {
|
||||
stars[i] = newStar()
|
||||
}
|
||||
}
|
||||
|
||||
type star struct {
|
||||
pixel.Vec
|
||||
Z float64
|
||||
P float64
|
||||
C color.RGBA
|
||||
}
|
||||
|
||||
func newStar() *star {
|
||||
return &star{
|
||||
pixel.V(random(-w, w), random(-h, h)),
|
||||
random(0, w), 0, Colors[rand.Intn(len(Colors))],
|
||||
}
|
||||
}
|
||||
|
||||
func (s *star) update(d float64) {
|
||||
s.P = s.Z
|
||||
s.Z -= d * speed
|
||||
|
||||
if s.Z < 0 {
|
||||
s.X = random(-w, w)
|
||||
s.Y = random(-h, h)
|
||||
s.Z = w
|
||||
s.P = s.Z
|
||||
}
|
||||
}
|
||||
|
||||
func (s *star) draw(imd *imdraw.IMDraw) {
|
||||
p := pixel.V(
|
||||
scale(s.X/s.Z, 0, 1, 0, w),
|
||||
scale(s.Y/s.Z, 0, 1, 0, h),
|
||||
)
|
||||
|
||||
o := pixel.V(
|
||||
scale(s.X/s.P, 0, 1, 0, w),
|
||||
scale(s.Y/s.P, 0, 1, 0, h),
|
||||
)
|
||||
|
||||
r := scale(s.Z, 0, w, 11, 0)
|
||||
|
||||
imd.Color = s.C
|
||||
|
||||
if p.Sub(o).Len() > 6 {
|
||||
imd.Push(p, o)
|
||||
imd.Line(r)
|
||||
}
|
||||
|
||||
imd.Push(p)
|
||||
imd.Circle(r, 0)
|
||||
}
|
||||
|
||||
func run() {
|
||||
win, err := pixelgl.NewWindow(pixelgl.WindowConfig{
|
||||
Bounds: pixel.R(0, 0, w, h),
|
||||
VSync: true,
|
||||
Undecorated: true,
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
imd := imdraw.New(nil)
|
||||
|
||||
imd.Precision = 7
|
||||
|
||||
imd.SetMatrix(pixel.IM.Moved(win.Bounds().Center()))
|
||||
|
||||
last := time.Now()
|
||||
|
||||
for !win.Closed() {
|
||||
win.SetClosed(win.JustPressed(pixelgl.KeyEscape) || win.JustPressed(pixelgl.KeyQ))
|
||||
|
||||
if win.Pressed(pixelgl.KeyUp) {
|
||||
speed += 10
|
||||
}
|
||||
|
||||
if win.Pressed(pixelgl.KeyDown) {
|
||||
if speed > 10 {
|
||||
speed -= 10
|
||||
}
|
||||
}
|
||||
|
||||
if win.Pressed(pixelgl.KeySpace) {
|
||||
speed = 100
|
||||
}
|
||||
|
||||
d := time.Since(last).Seconds()
|
||||
|
||||
last = time.Now()
|
||||
|
||||
imd.Clear()
|
||||
|
||||
for _, s := range stars {
|
||||
s.update(d)
|
||||
s.draw(imd)
|
||||
}
|
||||
|
||||
win.Clear(color.Black)
|
||||
imd.Draw(win)
|
||||
win.Update()
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
pixelgl.Run(run)
|
||||
}
|
||||
|
||||
func random(min, max float64) float64 {
|
||||
return rand.Float64()*(max-min) + min
|
||||
}
|
||||
|
||||
func scale(unscaledNum, min, max, minAllowed, maxAllowed float64) float64 {
|
||||
return (maxAllowed-minAllowed)*(unscaledNum-min)/(max-min) + minAllowed
|
||||
}
|
||||
|
||||
// Colors based on stellar types listed at
|
||||
// http://www.vendian.org/mncharity/dir3/starcolor/
|
||||
var Colors = []color.RGBA{
|
||||
color.RGBA{157, 180, 255, 255},
|
||||
color.RGBA{162, 185, 255, 255},
|
||||
color.RGBA{167, 188, 255, 255},
|
||||
color.RGBA{170, 191, 255, 255},
|
||||
color.RGBA{175, 195, 255, 255},
|
||||
color.RGBA{186, 204, 255, 255},
|
||||
color.RGBA{192, 209, 255, 255},
|
||||
color.RGBA{202, 216, 255, 255},
|
||||
color.RGBA{228, 232, 255, 255},
|
||||
color.RGBA{237, 238, 255, 255},
|
||||
color.RGBA{251, 248, 255, 255},
|
||||
color.RGBA{255, 249, 249, 255},
|
||||
color.RGBA{255, 245, 236, 255},
|
||||
color.RGBA{255, 244, 232, 255},
|
||||
color.RGBA{255, 241, 223, 255},
|
||||
color.RGBA{255, 235, 209, 255},
|
||||
color.RGBA{255, 215, 174, 255},
|
||||
color.RGBA{255, 198, 144, 255},
|
||||
color.RGBA{255, 190, 127, 255},
|
||||
color.RGBA{255, 187, 123, 255},
|
||||
color.RGBA{255, 187, 123, 255},
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"github.com/faiface/pixel"
|
||||
"github.com/faiface/pixel/pixelgl"
|
||||
"golang.org/x/image/colornames"
|
||||
)
|
||||
|
||||
func run() {
|
||||
cfg := pixelgl.WindowConfig{
|
||||
Title: "Pixel Rocks!",
|
||||
Bounds: pixel.R(0, 0, 1024, 768),
|
||||
VSync: true,
|
||||
}
|
||||
win, err := pixelgl.NewWindow(cfg)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
win.Clear(colornames.Skyblue)
|
||||
|
||||
for !win.Closed() {
|
||||
win.Update()
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
pixelgl.Run(run)
|
||||
}
|
After Width: | Height: | Size: 68 KiB |
|
@ -0,0 +1,56 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"image"
|
||||
"os"
|
||||
|
||||
_ "image/png"
|
||||
|
||||
"github.com/faiface/pixel"
|
||||
"github.com/faiface/pixel/pixelgl"
|
||||
"golang.org/x/image/colornames"
|
||||
)
|
||||
|
||||
func loadPicture(path string) (pixel.Picture, error) {
|
||||
file, err := os.Open(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer file.Close()
|
||||
img, _, err := image.Decode(file)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return pixel.PictureDataFromImage(img), nil
|
||||
}
|
||||
|
||||
func run() {
|
||||
cfg := pixelgl.WindowConfig{
|
||||
Title: "Pixel Rocks!",
|
||||
Bounds: pixel.R(0, 0, 1024, 768),
|
||||
VSync: true,
|
||||
}
|
||||
win, err := pixelgl.NewWindow(cfg)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
pic, err := loadPicture("hiking.png")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
sprite := pixel.NewSprite(pic, pic.Bounds())
|
||||
|
||||
win.Clear(colornames.Greenyellow)
|
||||
|
||||
sprite.Draw(win, pixel.IM.Moved(win.Bounds().Center()))
|
||||
|
||||
for !win.Closed() {
|
||||
win.Update()
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
pixelgl.Run(run)
|
||||
}
|
After Width: | Height: | Size: 68 KiB |
|
@ -0,0 +1,70 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"image"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
_ "image/png"
|
||||
|
||||
"github.com/faiface/pixel"
|
||||
"github.com/faiface/pixel/pixelgl"
|
||||
"golang.org/x/image/colornames"
|
||||
)
|
||||
|
||||
func loadPicture(path string) (pixel.Picture, error) {
|
||||
file, err := os.Open(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer file.Close()
|
||||
img, _, err := image.Decode(file)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return pixel.PictureDataFromImage(img), nil
|
||||
}
|
||||
|
||||
func run() {
|
||||
cfg := pixelgl.WindowConfig{
|
||||
Title: "Pixel Rocks!",
|
||||
Bounds: pixel.R(0, 0, 1024, 768),
|
||||
VSync: true,
|
||||
}
|
||||
win, err := pixelgl.NewWindow(cfg)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
win.SetSmooth(true)
|
||||
|
||||
pic, err := loadPicture("hiking.png")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
sprite := pixel.NewSprite(pic, pic.Bounds())
|
||||
|
||||
angle := 0.0
|
||||
|
||||
last := time.Now()
|
||||
for !win.Closed() {
|
||||
dt := time.Since(last).Seconds()
|
||||
last = time.Now()
|
||||
|
||||
angle += 3 * dt
|
||||
|
||||
win.Clear(colornames.Firebrick)
|
||||
|
||||
mat := pixel.IM
|
||||
mat = mat.Rotated(pixel.ZV, angle)
|
||||
mat = mat.Moved(win.Bounds().Center())
|
||||
sprite.Draw(win, mat)
|
||||
|
||||
win.Update()
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
pixelgl.Run(run)
|
||||
}
|
|
@ -0,0 +1,102 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"image"
|
||||
"math"
|
||||
"math/rand"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
_ "image/png"
|
||||
|
||||
"github.com/faiface/pixel"
|
||||
"github.com/faiface/pixel/pixelgl"
|
||||
"golang.org/x/image/colornames"
|
||||
)
|
||||
|
||||
func loadPicture(path string) (pixel.Picture, error) {
|
||||
file, err := os.Open(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer file.Close()
|
||||
img, _, err := image.Decode(file)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return pixel.PictureDataFromImage(img), nil
|
||||
}
|
||||
|
||||
func run() {
|
||||
cfg := pixelgl.WindowConfig{
|
||||
Title: "Pixel Rocks!",
|
||||
Bounds: pixel.R(0, 0, 1024, 768),
|
||||
VSync: true,
|
||||
}
|
||||
win, err := pixelgl.NewWindow(cfg)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
spritesheet, err := loadPicture("trees.png")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
var treesFrames []pixel.Rect
|
||||
for x := spritesheet.Bounds().Min.X; x < spritesheet.Bounds().Max.X; x += 32 {
|
||||
for y := spritesheet.Bounds().Min.Y; y < spritesheet.Bounds().Max.Y; y += 32 {
|
||||
treesFrames = append(treesFrames, pixel.R(x, y, x+32, y+32))
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
camPos = pixel.ZV
|
||||
camSpeed = 500.0
|
||||
camZoom = 1.0
|
||||
camZoomSpeed = 1.2
|
||||
trees []*pixel.Sprite
|
||||
matrices []pixel.Matrix
|
||||
)
|
||||
|
||||
last := time.Now()
|
||||
for !win.Closed() {
|
||||
dt := time.Since(last).Seconds()
|
||||
last = time.Now()
|
||||
|
||||
cam := pixel.IM.Scaled(camPos, camZoom).Moved(win.Bounds().Center().Sub(camPos))
|
||||
win.SetMatrix(cam)
|
||||
|
||||
if win.JustPressed(pixelgl.MouseButtonLeft) {
|
||||
tree := pixel.NewSprite(spritesheet, treesFrames[rand.Intn(len(treesFrames))])
|
||||
trees = append(trees, tree)
|
||||
mouse := cam.Unproject(win.MousePosition())
|
||||
matrices = append(matrices, pixel.IM.Scaled(pixel.ZV, 4).Moved(mouse))
|
||||
}
|
||||
if win.Pressed(pixelgl.KeyLeft) {
|
||||
camPos.X -= camSpeed * dt
|
||||
}
|
||||
if win.Pressed(pixelgl.KeyRight) {
|
||||
camPos.X += camSpeed * dt
|
||||
}
|
||||
if win.Pressed(pixelgl.KeyDown) {
|
||||
camPos.Y -= camSpeed * dt
|
||||
}
|
||||
if win.Pressed(pixelgl.KeyUp) {
|
||||
camPos.Y += camSpeed * dt
|
||||
}
|
||||
camZoom *= math.Pow(camZoomSpeed, win.MouseScroll().Y)
|
||||
|
||||
win.Clear(colornames.Forestgreen)
|
||||
|
||||
for i, tree := range trees {
|
||||
tree.Draw(win, matrices[i])
|
||||
}
|
||||
|
||||
win.Update()
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
pixelgl.Run(run)
|
||||
}
|
After Width: | Height: | Size: 2.5 KiB |
|
@ -0,0 +1,110 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"image"
|
||||
"math"
|
||||
"math/rand"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
_ "image/png"
|
||||
|
||||
"github.com/faiface/pixel"
|
||||
"github.com/faiface/pixel/pixelgl"
|
||||
"golang.org/x/image/colornames"
|
||||
)
|
||||
|
||||
func loadPicture(path string) (pixel.Picture, error) {
|
||||
file, err := os.Open(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer file.Close()
|
||||
img, _, err := image.Decode(file)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return pixel.PictureDataFromImage(img), nil
|
||||
}
|
||||
|
||||
func run() {
|
||||
cfg := pixelgl.WindowConfig{
|
||||
Title: "Pixel Rocks!",
|
||||
Bounds: pixel.R(0, 0, 1024, 768),
|
||||
}
|
||||
win, err := pixelgl.NewWindow(cfg)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
spritesheet, err := loadPicture("trees.png")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
batch := pixel.NewBatch(&pixel.TrianglesData{}, spritesheet)
|
||||
|
||||
var treesFrames []pixel.Rect
|
||||
for x := spritesheet.Bounds().Min.X; x < spritesheet.Bounds().Max.X; x += 32 {
|
||||
for y := spritesheet.Bounds().Min.Y; y < spritesheet.Bounds().Max.Y; y += 32 {
|
||||
treesFrames = append(treesFrames, pixel.R(x, y, x+32, y+32))
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
camPos = pixel.ZV
|
||||
camSpeed = 500.0
|
||||
camZoom = 1.0
|
||||
camZoomSpeed = 1.2
|
||||
)
|
||||
|
||||
var (
|
||||
frames = 0
|
||||
second = time.Tick(time.Second)
|
||||
)
|
||||
|
||||
last := time.Now()
|
||||
for !win.Closed() {
|
||||
dt := time.Since(last).Seconds()
|
||||
last = time.Now()
|
||||
|
||||
cam := pixel.IM.Scaled(camPos, camZoom).Moved(win.Bounds().Center().Sub(camPos))
|
||||
win.SetMatrix(cam)
|
||||
|
||||
if win.Pressed(pixelgl.MouseButtonLeft) {
|
||||
tree := pixel.NewSprite(spritesheet, treesFrames[rand.Intn(len(treesFrames))])
|
||||
mouse := cam.Unproject(win.MousePosition())
|
||||
tree.Draw(batch, pixel.IM.Scaled(pixel.ZV, 4).Moved(mouse))
|
||||
}
|
||||
if win.Pressed(pixelgl.KeyLeft) {
|
||||
camPos.X -= camSpeed * dt
|
||||
}
|
||||
if win.Pressed(pixelgl.KeyRight) {
|
||||
camPos.X += camSpeed * dt
|
||||
}
|
||||
if win.Pressed(pixelgl.KeyDown) {
|
||||
camPos.Y -= camSpeed * dt
|
||||
}
|
||||
if win.Pressed(pixelgl.KeyUp) {
|
||||
camPos.Y += camSpeed * dt
|
||||
}
|
||||
camZoom *= math.Pow(camZoomSpeed, win.MouseScroll().Y)
|
||||
|
||||
win.Clear(colornames.Forestgreen)
|
||||
batch.Draw(win)
|
||||
win.Update()
|
||||
|
||||
frames++
|
||||
select {
|
||||
case <-second:
|
||||
win.SetTitle(fmt.Sprintf("%s | FPS: %d", cfg.Title, frames))
|
||||
frames = 0
|
||||
default:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
pixelgl.Run(run)
|
||||
}
|
After Width: | Height: | Size: 2.5 KiB |
|
@ -0,0 +1,53 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"math"
|
||||
|
||||
"github.com/faiface/pixel"
|
||||
"github.com/faiface/pixel/imdraw"
|
||||
"github.com/faiface/pixel/pixelgl"
|
||||
"golang.org/x/image/colornames"
|
||||
)
|
||||
|
||||
func run() {
|
||||
cfg := pixelgl.WindowConfig{
|
||||
Title: "Pixel Rocks!",
|
||||
Bounds: pixel.R(0, 0, 1024, 768),
|
||||
VSync: true,
|
||||
}
|
||||
win, err := pixelgl.NewWindow(cfg)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
imd := imdraw.New(nil)
|
||||
|
||||
imd.Color = colornames.Blueviolet
|
||||
imd.EndShape = imdraw.RoundEndShape
|
||||
imd.Push(pixel.V(100, 100), pixel.V(700, 100))
|
||||
imd.EndShape = imdraw.SharpEndShape
|
||||
imd.Push(pixel.V(100, 500), pixel.V(700, 500))
|
||||
imd.Line(30)
|
||||
|
||||
imd.Color = colornames.Limegreen
|
||||
imd.Push(pixel.V(500, 500))
|
||||
imd.Circle(300, 50)
|
||||
imd.Color = colornames.Navy
|
||||
imd.Push(pixel.V(200, 500), pixel.V(800, 500))
|
||||
imd.Ellipse(pixel.V(120, 80), 0)
|
||||
|
||||
imd.Color = colornames.Red
|
||||
imd.EndShape = imdraw.RoundEndShape
|
||||
imd.Push(pixel.V(500, 350))
|
||||
imd.CircleArc(150, -math.Pi, 0, 30)
|
||||
|
||||
for !win.Closed() {
|
||||
win.Clear(colornames.Aliceblue)
|
||||
imd.Draw(win)
|
||||
win.Update()
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
pixelgl.Run(run)
|
||||
}
|
|
@ -0,0 +1,78 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/faiface/pixel"
|
||||
"github.com/faiface/pixel/pixelgl"
|
||||
"github.com/faiface/pixel/text"
|
||||
"github.com/golang/freetype/truetype"
|
||||
"golang.org/x/image/colornames"
|
||||
"golang.org/x/image/font"
|
||||
)
|
||||
|
||||
func loadTTF(path string, size float64) (font.Face, error) {
|
||||
file, err := os.Open(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
bytes, err := ioutil.ReadAll(file)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
font, err := truetype.Parse(bytes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return truetype.NewFace(font, &truetype.Options{
|
||||
Size: size,
|
||||
GlyphCacheEntries: 1,
|
||||
}), nil
|
||||
}
|
||||
|
||||
func run() {
|
||||
cfg := pixelgl.WindowConfig{
|
||||
Title: "Pixel Rocks!",
|
||||
Bounds: pixel.R(0, 0, 1024, 768),
|
||||
}
|
||||
win, err := pixelgl.NewWindow(cfg)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
win.SetSmooth(true)
|
||||
|
||||
face, err := loadTTF("intuitive.ttf", 80)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
atlas := text.NewAtlas(face, text.ASCII)
|
||||
txt := text.New(pixel.V(50, 500), atlas)
|
||||
|
||||
txt.Color = colornames.Lightgrey
|
||||
|
||||
fps := time.Tick(time.Second / 120)
|
||||
|
||||
for !win.Closed() {
|
||||
txt.WriteString(win.Typed())
|
||||
if win.JustPressed(pixelgl.KeyEnter) || win.Repeated(pixelgl.KeyEnter) {
|
||||
txt.WriteRune('\n')
|
||||
}
|
||||
|
||||
win.Clear(colornames.Darkcyan)
|
||||
txt.Draw(win, pixel.IM.Moved(win.Bounds().Center().Sub(txt.Bounds().Center())))
|
||||
win.Update()
|
||||
|
||||
<-fps
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
pixelgl.Run(run)
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
# Lights
|
||||
|
||||
This example demonstrates powerful Porter-Duff composition used to create a nice noisy light effect.
|
||||
|
||||
**Use W and S keys** to adjust the level of "dust".
|
||||
|
||||
The FPS is limited to 30, because the effect is a little expensive and my computer couldn't handle
|
||||
60 FPS. If you have a more powerful computer (which is quite likely), peek into the code and disable
|
||||
the limit.
|
||||
|
||||
Credit for the panda art goes to [Ján Štrba](https://www.artstation.com/artist/janstrba).
|
||||
|
||||

|
|
@ -0,0 +1,195 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"image"
|
||||
"math"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
_ "image/jpeg"
|
||||
_ "image/png"
|
||||
|
||||
"github.com/faiface/pixel"
|
||||
"github.com/faiface/pixel/imdraw"
|
||||
"github.com/faiface/pixel/pixelgl"
|
||||
)
|
||||
|
||||
func loadPicture(path string) (pixel.Picture, error) {
|
||||
file, err := os.Open(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer file.Close()
|
||||
img, _, err := image.Decode(file)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return pixel.PictureDataFromImage(img), nil
|
||||
}
|
||||
|
||||
type colorlight struct {
|
||||
color pixel.RGBA
|
||||
point pixel.Vec
|
||||
angle float64
|
||||
radius float64
|
||||
dust float64
|
||||
|
||||
spread float64
|
||||
|
||||
imd *imdraw.IMDraw
|
||||
}
|
||||
|
||||
func (cl *colorlight) apply(dst pixel.ComposeTarget, center pixel.Vec, src, noise *pixel.Sprite) {
|
||||
// create the light arc if not created already
|
||||
if cl.imd == nil {
|
||||
imd := imdraw.New(nil)
|
||||
imd.Color = pixel.Alpha(1)
|
||||
imd.Push(pixel.ZV)
|
||||
imd.Color = pixel.Alpha(0)
|
||||
for angle := -cl.spread / 2; angle <= cl.spread/2; angle += cl.spread / 64 {
|
||||
imd.Push(pixel.V(1, 0).Rotated(angle))
|
||||
}
|
||||
imd.Polygon(0)
|
||||
cl.imd = imd
|
||||
}
|
||||
|
||||
// draw the light arc
|
||||
dst.SetMatrix(pixel.IM.Scaled(pixel.ZV, cl.radius).Rotated(pixel.ZV, cl.angle).Moved(cl.point))
|
||||
dst.SetColorMask(pixel.Alpha(1))
|
||||
dst.SetComposeMethod(pixel.ComposePlus)
|
||||
cl.imd.Draw(dst)
|
||||
|
||||
// draw the noise inside the light
|
||||
dst.SetMatrix(pixel.IM)
|
||||
dst.SetComposeMethod(pixel.ComposeIn)
|
||||
noise.Draw(dst, pixel.IM.Moved(center))
|
||||
|
||||
// draw an image inside the noisy light
|
||||
dst.SetColorMask(cl.color)
|
||||
dst.SetComposeMethod(pixel.ComposeIn)
|
||||
src.Draw(dst, pixel.IM.Moved(center))
|
||||
|
||||
// draw the light reflected from the dust
|
||||
dst.SetMatrix(pixel.IM.Scaled(pixel.ZV, cl.radius).Rotated(pixel.ZV, cl.angle).Moved(cl.point))
|
||||
dst.SetColorMask(cl.color.Mul(pixel.Alpha(cl.dust)))
|
||||
dst.SetComposeMethod(pixel.ComposePlus)
|
||||
cl.imd.Draw(dst)
|
||||
}
|
||||
|
||||
func run() {
|
||||
pandaPic, err := loadPicture("panda.png")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
noisePic, err := loadPicture("noise.png")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
cfg := pixelgl.WindowConfig{
|
||||
Title: "Lights",
|
||||
Bounds: pixel.R(0, 0, 1024, 768),
|
||||
VSync: true,
|
||||
}
|
||||
win, err := pixelgl.NewWindow(cfg)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
panda := pixel.NewSprite(pandaPic, pandaPic.Bounds())
|
||||
noise := pixel.NewSprite(noisePic, noisePic.Bounds())
|
||||
|
||||
colors := []pixel.RGBA{
|
||||
pixel.RGB(1, 0, 0),
|
||||
pixel.RGB(0, 1, 0),
|
||||
pixel.RGB(0, 0, 1),
|
||||
pixel.RGB(1/math.Sqrt2, 1/math.Sqrt2, 0),
|
||||
}
|
||||
|
||||
points := []pixel.Vec{
|
||||
{X: win.Bounds().Min.X, Y: win.Bounds().Min.Y},
|
||||
{X: win.Bounds().Max.X, Y: win.Bounds().Min.Y},
|
||||
{X: win.Bounds().Max.X, Y: win.Bounds().Max.Y},
|
||||
{X: win.Bounds().Min.X, Y: win.Bounds().Max.Y},
|
||||
}
|
||||
|
||||
angles := []float64{
|
||||
math.Pi / 4,
|
||||
math.Pi/4 + math.Pi/2,
|
||||
math.Pi/4 + 2*math.Pi/2,
|
||||
math.Pi/4 + 3*math.Pi/2,
|
||||
}
|
||||
|
||||
lights := make([]colorlight, 4)
|
||||
for i := range lights {
|
||||
lights[i] = colorlight{
|
||||
color: colors[i],
|
||||
point: points[i],
|
||||
angle: angles[i],
|
||||
radius: 800,
|
||||
dust: 0.3,
|
||||
spread: math.Pi / math.E,
|
||||
}
|
||||
}
|
||||
|
||||
speed := []float64{11.0 / 23, 13.0 / 23, 17.0 / 23, 19.0 / 23}
|
||||
|
||||
oneLight := pixelgl.NewCanvas(win.Bounds())
|
||||
allLight := pixelgl.NewCanvas(win.Bounds())
|
||||
|
||||
fps30 := time.Tick(time.Second / 30)
|
||||
|
||||
start := time.Now()
|
||||
for !win.Closed() {
|
||||
if win.Pressed(pixelgl.KeyW) {
|
||||
for i := range lights {
|
||||
lights[i].dust += 0.05
|
||||
if lights[i].dust > 1 {
|
||||
lights[i].dust = 1
|
||||
}
|
||||
}
|
||||
}
|
||||
if win.Pressed(pixelgl.KeyS) {
|
||||
for i := range lights {
|
||||
lights[i].dust -= 0.05
|
||||
if lights[i].dust < 0 {
|
||||
lights[i].dust = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
since := time.Since(start).Seconds()
|
||||
for i := range lights {
|
||||
lights[i].angle = angles[i] + math.Sin(since*speed[i])*math.Pi/8
|
||||
}
|
||||
|
||||
win.Clear(pixel.RGB(0, 0, 0))
|
||||
|
||||
// draw the panda visible outside the light
|
||||
win.SetColorMask(pixel.Alpha(0.4))
|
||||
win.SetComposeMethod(pixel.ComposePlus)
|
||||
panda.Draw(win, pixel.IM.Moved(win.Bounds().Center()))
|
||||
|
||||
allLight.Clear(pixel.Alpha(0))
|
||||
allLight.SetComposeMethod(pixel.ComposePlus)
|
||||
|
||||
// accumulate all the lights
|
||||
for i := range lights {
|
||||
oneLight.Clear(pixel.Alpha(0))
|
||||
lights[i].apply(oneLight, oneLight.Bounds().Center(), panda, noise)
|
||||
oneLight.Draw(allLight, pixel.IM.Moved(allLight.Bounds().Center()))
|
||||
}
|
||||
|
||||
// compose the final result
|
||||
win.SetColorMask(pixel.Alpha(1))
|
||||
allLight.Draw(win, pixel.IM.Moved(win.Bounds().Center()))
|
||||
|
||||
win.Update()
|
||||
|
||||
<-fps30 // maintain 30 fps, because my computer couldn't handle 60 here
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
pixelgl.Run(run)
|
||||
}
|
After Width: | Height: | Size: 838 KiB |
After Width: | Height: | Size: 1.2 MiB |
After Width: | Height: | Size: 1.1 MiB |
|
@ -0,0 +1,14 @@
|
|||
# Platformer
|
||||
|
||||
This example demostrates a way to put things together and create a simple platformer game with a
|
||||
Gopher!
|
||||
|
||||
Use **arrow keys** to run and jump around. Press **ENTER** to restart. (And hush, hush, secret.
|
||||
Press TAB for slo-mo!)
|
||||
|
||||
The retro feel is, other than from the pixel art spritesheet, achieved by using a 160x120px large
|
||||
off-screen canvas, drawing everything to it and then stretching it to fit the window.
|
||||
|
||||
The Gopher spritesheet comes from excellent [Egon Elbre](https://github.com/egonelbre/gophers).
|
||||
|
||||

|
|
@ -0,0 +1,394 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"encoding/csv"
|
||||
"image"
|
||||
"image/color"
|
||||
"io"
|
||||
"math"
|
||||
"math/rand"
|
||||
"os"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
_ "image/png"
|
||||
|
||||
"github.com/faiface/pixel"
|
||||
"github.com/faiface/pixel/imdraw"
|
||||
"github.com/faiface/pixel/pixelgl"
|
||||
"github.com/pkg/errors"
|
||||
"golang.org/x/image/colornames"
|
||||
)
|
||||
|
||||
func loadAnimationSheet(sheetPath, descPath string, frameWidth float64) (sheet pixel.Picture, anims map[string][]pixel.Rect, err error) {
|
||||
// total hack, nicely format the error at the end, so I don't have to type it every time
|
||||
defer func() {
|
||||
if err != nil {
|
||||
err = errors.Wrap(err, "error loading animation sheet")
|
||||
}
|
||||
}()
|
||||
|
||||
// open and load the spritesheet
|
||||
sheetFile, err := os.Open(sheetPath)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
defer sheetFile.Close()
|
||||
sheetImg, _, err := image.Decode(sheetFile)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
sheet = pixel.PictureDataFromImage(sheetImg)
|
||||
|
||||
// create a slice of frames inside the spritesheet
|
||||
var frames []pixel.Rect
|
||||
for x := 0.0; x+frameWidth <= sheet.Bounds().Max.X; x += frameWidth {
|
||||
frames = append(frames, pixel.R(
|
||||
x,
|
||||
0,
|
||||
x+frameWidth,
|
||||
sheet.Bounds().H(),
|
||||
))
|
||||
}
|
||||
|
||||
descFile, err := os.Open(descPath)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
defer descFile.Close()
|
||||
|
||||
anims = make(map[string][]pixel.Rect)
|
||||
|
||||
// load the animation information, name and interval inside the spritesheet
|
||||
desc := csv.NewReader(descFile)
|
||||
for {
|
||||
anim, err := desc.Read()
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
name := anim[0]
|
||||
start, _ := strconv.Atoi(anim[1])
|
||||
end, _ := strconv.Atoi(anim[2])
|
||||
|
||||
anims[name] = frames[start : end+1]
|
||||
}
|
||||
|
||||
return sheet, anims, nil
|
||||
}
|
||||
|
||||
type platform struct {
|
||||
rect pixel.Rect
|
||||
color color.Color
|
||||
}
|
||||
|
||||
func (p *platform) draw(imd *imdraw.IMDraw) {
|
||||
imd.Color = p.color
|
||||
imd.Push(p.rect.Min, p.rect.Max)
|
||||
imd.Rectangle(0)
|
||||
}
|
||||
|
||||
type gopherPhys struct {
|
||||
gravity float64
|
||||
runSpeed float64
|
||||
jumpSpeed float64
|
||||
|
||||
rect pixel.Rect
|
||||
vel pixel.Vec
|
||||
ground bool
|
||||
}
|
||||
|
||||
func (gp *gopherPhys) update(dt float64, ctrl pixel.Vec, platforms []platform) {
|
||||
// apply controls
|
||||
switch {
|
||||
case ctrl.X < 0:
|
||||
gp.vel.X = -gp.runSpeed
|
||||
case ctrl.X > 0:
|
||||
gp.vel.X = +gp.runSpeed
|
||||
default:
|
||||
gp.vel.X = 0
|
||||
}
|
||||
|
||||
// apply gravity and velocity
|
||||
gp.vel.Y += gp.gravity * dt
|
||||
gp.rect = gp.rect.Moved(gp.vel.Scaled(dt))
|
||||
|
||||
// check collisions against each platform
|
||||
gp.ground = false
|
||||
if gp.vel.Y <= 0 {
|
||||
for _, p := range platforms {
|
||||
if gp.rect.Max.X <= p.rect.Min.X || gp.rect.Min.X >= p.rect.Max.X {
|
||||
continue
|
||||
}
|
||||
if gp.rect.Min.Y > p.rect.Max.Y || gp.rect.Min.Y < p.rect.Max.Y+gp.vel.Y*dt {
|
||||
continue
|
||||
}
|
||||
gp.vel.Y = 0
|
||||
gp.rect = gp.rect.Moved(pixel.V(0, p.rect.Max.Y-gp.rect.Min.Y))
|
||||
gp.ground = true
|
||||
}
|
||||
}
|
||||
|
||||
// jump if on the ground and the player wants to jump
|
||||
if gp.ground && ctrl.Y > 0 {
|
||||
gp.vel.Y = gp.jumpSpeed
|
||||
}
|
||||
}
|
||||
|
||||
type animState int
|
||||
|
||||
const (
|
||||
idle animState = iota
|
||||
running
|
||||
jumping
|
||||
)
|
||||
|
||||
type gopherAnim struct {
|
||||
sheet pixel.Picture
|
||||
anims map[string][]pixel.Rect
|
||||
rate float64
|
||||
|
||||
state animState
|
||||
counter float64
|
||||
dir float64
|
||||
|
||||
frame pixel.Rect
|
||||
|
||||
sprite *pixel.Sprite
|
||||
}
|
||||
|
||||
func (ga *gopherAnim) update(dt float64, phys *gopherPhys) {
|
||||
ga.counter += dt
|
||||
|
||||
// determine the new animation state
|
||||
var newState animState
|
||||
switch {
|
||||
case !phys.ground:
|
||||
newState = jumping
|
||||
case phys.vel.Len() == 0:
|
||||
newState = idle
|
||||
case phys.vel.Len() > 0:
|
||||
newState = running
|
||||
}
|
||||
|
||||
// reset the time counter if the state changed
|
||||
if ga.state != newState {
|
||||
ga.state = newState
|
||||
ga.counter = 0
|
||||
}
|
||||
|
||||
// determine the correct animation frame
|
||||
switch ga.state {
|
||||
case idle:
|
||||
ga.frame = ga.anims["Front"][0]
|
||||
case running:
|
||||
i := int(math.Floor(ga.counter / ga.rate))
|
||||
ga.frame = ga.anims["Run"][i%len(ga.anims["Run"])]
|
||||
case jumping:
|
||||
speed := phys.vel.Y
|
||||
i := int((-speed/phys.jumpSpeed + 1) / 2 * float64(len(ga.anims["Jump"])))
|
||||
if i < 0 {
|
||||
i = 0
|
||||
}
|
||||
if i >= len(ga.anims["Jump"]) {
|
||||
i = len(ga.anims["Jump"]) - 1
|
||||
}
|
||||
ga.frame = ga.anims["Jump"][i]
|
||||
}
|
||||
|
||||
// set the facing direction of the gopher
|
||||
if phys.vel.X != 0 {
|
||||
if phys.vel.X > 0 {
|
||||
ga.dir = +1
|
||||
} else {
|
||||
ga.dir = -1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (ga *gopherAnim) draw(t pixel.Target, phys *gopherPhys) {
|
||||
if ga.sprite == nil {
|
||||
ga.sprite = pixel.NewSprite(nil, pixel.Rect{})
|
||||
}
|
||||
// draw the correct frame with the correct position and direction
|
||||
ga.sprite.Set(ga.sheet, ga.frame)
|
||||
ga.sprite.Draw(t, pixel.IM.
|
||||
ScaledXY(pixel.ZV, pixel.V(
|
||||
phys.rect.W()/ga.sprite.Frame().W(),
|
||||
phys.rect.H()/ga.sprite.Frame().H(),
|
||||
)).
|
||||
ScaledXY(pixel.ZV, pixel.V(-ga.dir, 1)).
|
||||
Moved(phys.rect.Center()),
|
||||
)
|
||||
}
|
||||
|
||||
type goal struct {
|
||||
pos pixel.Vec
|
||||
radius float64
|
||||
step float64
|
||||
|
||||
counter float64
|
||||
cols [5]pixel.RGBA
|
||||
}
|
||||
|
||||
func (g *goal) update(dt float64) {
|
||||
g.counter += dt
|
||||
for g.counter > g.step {
|
||||
g.counter -= g.step
|
||||
for i := len(g.cols) - 2; i >= 0; i-- {
|
||||
g.cols[i+1] = g.cols[i]
|
||||
}
|
||||
g.cols[0] = randomNiceColor()
|
||||
}
|
||||
}
|
||||
|
||||
func (g *goal) draw(imd *imdraw.IMDraw) {
|
||||
for i := len(g.cols) - 1; i >= 0; i-- {
|
||||
imd.Color = g.cols[i]
|
||||
imd.Push(g.pos)
|
||||
imd.Circle(float64(i+1)*g.radius/float64(len(g.cols)), 0)
|
||||
}
|
||||
}
|
||||
|
||||
func randomNiceColor() pixel.RGBA {
|
||||
again:
|
||||
r := rand.Float64()
|
||||
g := rand.Float64()
|
||||
b := rand.Float64()
|
||||
len := math.Sqrt(r*r + g*g + b*b)
|
||||
if len == 0 {
|
||||
goto again
|
||||
}
|
||||
return pixel.RGB(r/len, g/len, b/len)
|
||||
}
|
||||
|
||||
func run() {
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
|
||||
sheet, anims, err := loadAnimationSheet("sheet.png", "sheet.csv", 12)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
cfg := pixelgl.WindowConfig{
|
||||
Title: "Platformer",
|
||||
Bounds: pixel.R(0, 0, 1024, 768),
|
||||
VSync: true,
|
||||
}
|
||||
win, err := pixelgl.NewWindow(cfg)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
phys := &gopherPhys{
|
||||
gravity: -512,
|
||||
runSpeed: 64,
|
||||
jumpSpeed: 192,
|
||||
rect: pixel.R(-6, -7, 6, 7),
|
||||
}
|
||||
|
||||
anim := &gopherAnim{
|
||||
sheet: sheet,
|
||||
anims: anims,
|
||||
rate: 1.0 / 10,
|
||||
dir: +1,
|
||||
}
|
||||
|
||||
// hardcoded level
|
||||
platforms := []platform{
|
||||
{rect: pixel.R(-50, -34, 50, -32)},
|
||||
{rect: pixel.R(20, 0, 70, 2)},
|
||||
{rect: pixel.R(-100, 10, -50, 12)},
|
||||
{rect: pixel.R(120, -22, 140, -20)},
|
||||
{rect: pixel.R(120, -72, 140, -70)},
|
||||
{rect: pixel.R(120, -122, 140, -120)},
|
||||
{rect: pixel.R(-100, -152, 100, -150)},
|
||||
{rect: pixel.R(-150, -127, -140, -125)},
|
||||
{rect: pixel.R(-180, -97, -170, -95)},
|
||||
{rect: pixel.R(-150, -67, -140, -65)},
|
||||
{rect: pixel.R(-180, -37, -170, -35)},
|
||||
{rect: pixel.R(-150, -7, -140, -5)},
|
||||
}
|
||||
for i := range platforms {
|
||||
platforms[i].color = randomNiceColor()
|
||||
}
|
||||
|
||||
gol := &goal{
|
||||
pos: pixel.V(-75, 40),
|
||||
radius: 18,
|
||||
step: 1.0 / 7,
|
||||
}
|
||||
|
||||
canvas := pixelgl.NewCanvas(pixel.R(-160/2, -120/2, 160/2, 120/2))
|
||||
imd := imdraw.New(sheet)
|
||||
imd.Precision = 32
|
||||
|
||||
camPos := pixel.ZV
|
||||
|
||||
last := time.Now()
|
||||
for !win.Closed() {
|
||||
dt := time.Since(last).Seconds()
|
||||
last = time.Now()
|
||||
|
||||
// lerp the camera position towards the gopher
|
||||
camPos = pixel.Lerp(camPos, phys.rect.Center(), 1-math.Pow(1.0/128, dt))
|
||||
cam := pixel.IM.Moved(camPos.Scaled(-1))
|
||||
canvas.SetMatrix(cam)
|
||||
|
||||
// slow motion with tab
|
||||
if win.Pressed(pixelgl.KeyTab) {
|
||||
dt /= 8
|
||||
}
|
||||
|
||||
// restart the level on pressing enter
|
||||
if win.JustPressed(pixelgl.KeyEnter) {
|
||||
phys.rect = phys.rect.Moved(phys.rect.Center().Scaled(-1))
|
||||
phys.vel = pixel.ZV
|
||||
}
|
||||
|
||||
// control the gopher with keys
|
||||
ctrl := pixel.ZV
|
||||
if win.Pressed(pixelgl.KeyLeft) {
|
||||
ctrl.X--
|
||||
}
|
||||
if win.Pressed(pixelgl.KeyRight) {
|
||||
ctrl.X++
|
||||
}
|
||||
if win.JustPressed(pixelgl.KeyUp) {
|
||||
ctrl.Y = 1
|
||||
}
|
||||
|
||||
// update the physics and animation
|
||||
phys.update(dt, ctrl, platforms)
|
||||
gol.update(dt)
|
||||
anim.update(dt, phys)
|
||||
|
||||
// draw the scene to the canvas using IMDraw
|
||||
canvas.Clear(colornames.Black)
|
||||
imd.Clear()
|
||||
for _, p := range platforms {
|
||||
p.draw(imd)
|
||||
}
|
||||
gol.draw(imd)
|
||||
anim.draw(imd, phys)
|
||||
imd.Draw(canvas)
|
||||
|
||||
// stretch the canvas to the window
|
||||
win.Clear(colornames.White)
|
||||
win.SetMatrix(pixel.IM.Scaled(pixel.ZV,
|
||||
math.Min(
|
||||
win.Bounds().W()/canvas.Bounds().W(),
|
||||
win.Bounds().H()/canvas.Bounds().H(),
|
||||
),
|
||||
).Moved(win.Bounds().Center()))
|
||||
canvas.Draw(win, pixel.IM.Moved(canvas.Bounds().Center()))
|
||||
win.Update()
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
pixelgl.Run(run)
|
||||
}
|
After Width: | Height: | Size: 8.3 KiB |
|
@ -0,0 +1,9 @@
|
|||
Front,0,0
|
||||
FrontBlink,1,1
|
||||
LookUp,2,2
|
||||
Left,3,7
|
||||
LeftRight,4,6
|
||||
LeftBlink,7,7
|
||||
Walk,8,15
|
||||
Run,16,23
|
||||
Jump,24,26
|
|
After Width: | Height: | Size: 530 B |
|
@ -0,0 +1,8 @@
|
|||
# Smoke
|
||||
|
||||
This example implements a smoke particle effect using sprites. It uses a spritesheet with a CSV
|
||||
description.
|
||||
|
||||
The art in the spritesheet comes from [Kenney](https://kenney.nl/).
|
||||
|
||||

|
|
@ -0,0 +1,25 @@
|
|||
1543,1146,362,336
|
||||
396,0,398,364
|
||||
761,1535,386,342
|
||||
795,794,351,367
|
||||
394,1163,386,364
|
||||
1120,1163,377,348
|
||||
795,0,368,407
|
||||
0,0,395,397
|
||||
1164,0,378,415
|
||||
781,1163,338,360
|
||||
1543,0,372,370
|
||||
1148,1535,393,327
|
||||
387,1535,373,364
|
||||
396,365,371,388
|
||||
0,758,378,404
|
||||
379,758,378,371
|
||||
1543,774,360,371
|
||||
1543,1483,350,398
|
||||
0,398,382,359
|
||||
1164,416,356,382
|
||||
1164,799,369,350
|
||||
0,1535,386,394
|
||||
795,408,366,385
|
||||
1543,371,367,402
|
||||
0,1163,393,371
|
|
After Width: | Height: | Size: 3.0 MiB |
|
@ -0,0 +1,230 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"container/list"
|
||||
"encoding/csv"
|
||||
"image"
|
||||
"io"
|
||||
"math"
|
||||
"math/rand"
|
||||
"os"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
_ "image/png"
|
||||
|
||||
"github.com/faiface/pixel"
|
||||
"github.com/faiface/pixel/pixelgl"
|
||||
"golang.org/x/image/colornames"
|
||||
)
|
||||
|
||||
type particle struct {
|
||||
Sprite *pixel.Sprite
|
||||
Pos pixel.Vec
|
||||
Rot, Scale float64
|
||||
Mask pixel.RGBA
|
||||
Data interface{}
|
||||
}
|
||||
|
||||
type particles struct {
|
||||
Generate func() *particle
|
||||
Update func(dt float64, p *particle) bool
|
||||
SpawnAvg, SpawnDist float64
|
||||
|
||||
parts list.List
|
||||
spawnTime float64
|
||||
}
|
||||
|
||||
func (p *particles) UpdateAll(dt float64) {
|
||||
p.spawnTime -= dt
|
||||
for p.spawnTime <= 0 {
|
||||
p.parts.PushFront(p.Generate())
|
||||
p.spawnTime += math.Max(0, p.SpawnAvg+rand.NormFloat64()*p.SpawnDist)
|
||||
}
|
||||
|
||||
for e := p.parts.Front(); e != nil; e = e.Next() {
|
||||
part := e.Value.(*particle)
|
||||
if !p.Update(dt, part) {
|
||||
defer p.parts.Remove(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (p *particles) DrawAll(t pixel.Target) {
|
||||
for e := p.parts.Front(); e != nil; e = e.Next() {
|
||||
part := e.Value.(*particle)
|
||||
|
||||
part.Sprite.DrawColorMask(
|
||||
t,
|
||||
pixel.IM.
|
||||
Scaled(pixel.ZV, part.Scale).
|
||||
Rotated(pixel.ZV, part.Rot).
|
||||
Moved(part.Pos),
|
||||
part.Mask,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
type smokeData struct {
|
||||
Vel pixel.Vec
|
||||
Time float64
|
||||
Life float64
|
||||
}
|
||||
|
||||
type smokeSystem struct {
|
||||
Sheet pixel.Picture
|
||||
Rects []pixel.Rect
|
||||
Orig pixel.Vec
|
||||
|
||||
VelBasis []pixel.Vec
|
||||
VelDist float64
|
||||
|
||||
LifeAvg, LifeDist float64
|
||||
}
|
||||
|
||||
func (ss *smokeSystem) Generate() *particle {
|
||||
sd := new(smokeData)
|
||||
for _, base := range ss.VelBasis {
|
||||
c := math.Max(0, 1+rand.NormFloat64()*ss.VelDist)
|
||||
sd.Vel = sd.Vel.Add(base.Scaled(c))
|
||||
}
|
||||
sd.Vel = sd.Vel.Scaled(1 / float64(len(ss.VelBasis)))
|
||||
sd.Life = math.Max(0, ss.LifeAvg+rand.NormFloat64()*ss.LifeDist)
|
||||
|
||||
p := new(particle)
|
||||
p.Data = sd
|
||||
|
||||
p.Pos = ss.Orig
|
||||
p.Scale = 1
|
||||
p.Mask = pixel.Alpha(1)
|
||||
p.Sprite = pixel.NewSprite(ss.Sheet, ss.Rects[rand.Intn(len(ss.Rects))])
|
||||
|
||||
return p
|
||||
}
|
||||
|
||||
func (ss *smokeSystem) Update(dt float64, p *particle) bool {
|
||||
sd := p.Data.(*smokeData)
|
||||
sd.Time += dt
|
||||
|
||||
frac := sd.Time / sd.Life
|
||||
|
||||
p.Pos = p.Pos.Add(sd.Vel.Scaled(dt))
|
||||
p.Scale = 0.5 + frac*1.5
|
||||
|
||||
const (
|
||||
fadeIn = 0.2
|
||||
fadeOut = 0.4
|
||||
)
|
||||
if frac < fadeIn {
|
||||
p.Mask = pixel.Alpha(math.Pow(frac/fadeIn, 0.75))
|
||||
} else if frac >= fadeOut {
|
||||
p.Mask = pixel.Alpha(math.Pow(1-(frac-fadeOut)/(1-fadeOut), 1.5))
|
||||
} else {
|
||||
p.Mask = pixel.Alpha(1)
|
||||
}
|
||||
|
||||
return sd.Time < sd.Life
|
||||
}
|
||||
|
||||
func loadSpriteSheet(sheetPath, descriptionPath string) (sheet pixel.Picture, rects []pixel.Rect, err error) {
|
||||
sheetFile, err := os.Open(sheetPath)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
defer sheetFile.Close()
|
||||
|
||||
sheetImg, _, err := image.Decode(sheetFile)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
sheet = pixel.PictureDataFromImage(sheetImg)
|
||||
|
||||
descriptionFile, err := os.Open(descriptionPath)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
defer descriptionFile.Close()
|
||||
|
||||
description := csv.NewReader(descriptionFile)
|
||||
for {
|
||||
record, err := description.Read()
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
x, _ := strconv.ParseFloat(record[0], 64)
|
||||
y, _ := strconv.ParseFloat(record[1], 64)
|
||||
w, _ := strconv.ParseFloat(record[2], 64)
|
||||
h, _ := strconv.ParseFloat(record[3], 64)
|
||||
|
||||
y = sheet.Bounds().H() - y - h
|
||||
|
||||
rects = append(rects, pixel.R(x, y, x+w, y+h))
|
||||
}
|
||||
|
||||
return sheet, rects, nil
|
||||
}
|
||||
|
||||
func run() {
|
||||
sheet, rects, err := loadSpriteSheet("blackSmoke.png", "blackSmoke.csv")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
cfg := pixelgl.WindowConfig{
|
||||
Title: "Smoke",
|
||||
Bounds: pixel.R(0, 0, 1024, 768),
|
||||
Resizable: true,
|
||||
VSync: true,
|
||||
}
|
||||
win, err := pixelgl.NewWindow(cfg)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
ss := &smokeSystem{
|
||||
Rects: rects,
|
||||
Orig: pixel.ZV,
|
||||
VelBasis: []pixel.Vec{pixel.V(-100, 100), pixel.V(100, 100), pixel.V(0, 100)},
|
||||
VelDist: 0.1,
|
||||
LifeAvg: 7,
|
||||
LifeDist: 0.5,
|
||||
}
|
||||
|
||||
p := &particles{
|
||||
Generate: ss.Generate,
|
||||
Update: ss.Update,
|
||||
SpawnAvg: 0.3,
|
||||
SpawnDist: 0.1,
|
||||
}
|
||||
|
||||
batch := pixel.NewBatch(&pixel.TrianglesData{}, sheet)
|
||||
|
||||
last := time.Now()
|
||||
for !win.Closed() {
|
||||
dt := time.Since(last).Seconds()
|
||||
last = time.Now()
|
||||
|
||||
p.UpdateAll(dt)
|
||||
|
||||
win.Clear(colornames.Aliceblue)
|
||||
|
||||
orig := win.Bounds().Center()
|
||||
orig.Y -= win.Bounds().H() / 2
|
||||
win.SetMatrix(pixel.IM.Moved(orig))
|
||||
|
||||
batch.Clear()
|
||||
p.DrawAll(batch)
|
||||
batch.Draw(win)
|
||||
|
||||
win.Update()
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
pixelgl.Run(run)
|
||||
}
|
After Width: | Height: | Size: 381 KiB |
|
@ -0,0 +1,13 @@
|
|||
# Typewriter
|
||||
|
||||
This example demonstrates text drawing and text input facilities by implementing a fancy typewriter
|
||||
with a red laser cursor. Screen shakes a bit when typing and some letters turn bold or italic
|
||||
randomly.
|
||||
|
||||
ASCII and Latin characters are supported here. Feel free to add support for more characters in the
|
||||
code (it's easy, but increases load time).
|
||||
|
||||
The seemingly buggy letters (one over another) in the screenshot are not bugs, but a result of using
|
||||
the typewriter backspace functionality.
|
||||
|
||||

|
|
@ -0,0 +1,317 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"image/color"
|
||||
"math"
|
||||
"math/rand"
|
||||
"sync"
|
||||
"time"
|
||||
"unicode"
|
||||
|
||||
"github.com/faiface/pixel"
|
||||
"github.com/faiface/pixel/imdraw"
|
||||
"github.com/faiface/pixel/pixelgl"
|
||||
"github.com/faiface/pixel/text"
|
||||
"github.com/golang/freetype/truetype"
|
||||
"golang.org/x/image/colornames"
|
||||
"golang.org/x/image/font"
|
||||
"golang.org/x/image/font/gofont/gobold"
|
||||
"golang.org/x/image/font/gofont/goitalic"
|
||||
"golang.org/x/image/font/gofont/goregular"
|
||||
)
|
||||
|
||||
func ttfFromBytesMust(b []byte, size float64) font.Face {
|
||||
ttf, err := truetype.Parse(b)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return truetype.NewFace(ttf, &truetype.Options{
|
||||
Size: size,
|
||||
GlyphCacheEntries: 1,
|
||||
})
|
||||
}
|
||||
|
||||
type typewriter struct {
|
||||
mu sync.Mutex
|
||||
|
||||
regular *text.Text
|
||||
bold *text.Text
|
||||
italic *text.Text
|
||||
|
||||
offset pixel.Vec
|
||||
position pixel.Vec
|
||||
move pixel.Vec
|
||||
}
|
||||
|
||||
func newTypewriter(c color.Color, regular, bold, italic *text.Atlas) *typewriter {
|
||||
tw := &typewriter{
|
||||
regular: text.New(pixel.ZV, regular),
|
||||
bold: text.New(pixel.ZV, bold),
|
||||
italic: text.New(pixel.ZV, italic),
|
||||
}
|
||||
tw.regular.Color = c
|
||||
tw.bold.Color = c
|
||||
tw.italic.Color = c
|
||||
return tw
|
||||
}
|
||||
|
||||
func (tw *typewriter) Ribbon(r rune) {
|
||||
tw.mu.Lock()
|
||||
defer tw.mu.Unlock()
|
||||
|
||||
dice := rand.Intn(21)
|
||||
switch {
|
||||
case 0 <= dice && dice <= 18:
|
||||
tw.regular.WriteRune(r)
|
||||
case dice == 19:
|
||||
tw.bold.Dot = tw.regular.Dot
|
||||
tw.bold.WriteRune(r)
|
||||
tw.regular.Dot = tw.bold.Dot
|
||||
case dice == 20:
|
||||
tw.italic.Dot = tw.regular.Dot
|
||||
tw.italic.WriteRune(r)
|
||||
tw.regular.Dot = tw.italic.Dot
|
||||
}
|
||||
}
|
||||
|
||||
func (tw *typewriter) Back() {
|
||||
tw.mu.Lock()
|
||||
defer tw.mu.Unlock()
|
||||
tw.regular.Dot = tw.regular.Dot.Sub(pixel.V(tw.regular.Atlas().Glyph(' ').Advance, 0))
|
||||
}
|
||||
|
||||
func (tw *typewriter) Offset(off pixel.Vec) {
|
||||
tw.mu.Lock()
|
||||
defer tw.mu.Unlock()
|
||||
tw.offset = tw.offset.Add(off)
|
||||
}
|
||||
|
||||
func (tw *typewriter) Position() pixel.Vec {
|
||||
tw.mu.Lock()
|
||||
defer tw.mu.Unlock()
|
||||
return tw.position
|
||||
}
|
||||
|
||||
func (tw *typewriter) Move(vel pixel.Vec) {
|
||||
tw.mu.Lock()
|
||||
defer tw.mu.Unlock()
|
||||
tw.move = vel
|
||||
}
|
||||
|
||||
func (tw *typewriter) Dot() pixel.Vec {
|
||||
tw.mu.Lock()
|
||||
defer tw.mu.Unlock()
|
||||
return tw.regular.Dot
|
||||
}
|
||||
|
||||
func (tw *typewriter) Update(dt float64) {
|
||||
tw.mu.Lock()
|
||||
defer tw.mu.Unlock()
|
||||
tw.position = tw.position.Add(tw.move.Scaled(dt))
|
||||
}
|
||||
|
||||
func (tw *typewriter) Draw(t pixel.Target, m pixel.Matrix) {
|
||||
tw.mu.Lock()
|
||||
defer tw.mu.Unlock()
|
||||
|
||||
m = pixel.IM.Moved(tw.position.Add(tw.offset)).Chained(m)
|
||||
tw.regular.Draw(t, m)
|
||||
tw.bold.Draw(t, m)
|
||||
tw.italic.Draw(t, m)
|
||||
}
|
||||
|
||||
func typeRune(tw *typewriter, r rune) {
|
||||
tw.Ribbon(r)
|
||||
if !unicode.IsSpace(r) {
|
||||
go shake(tw, 3, 17)
|
||||
}
|
||||
}
|
||||
|
||||
func back(tw *typewriter) {
|
||||
tw.Back()
|
||||
}
|
||||
|
||||
func shake(tw *typewriter, intensity, friction float64) {
|
||||
const (
|
||||
freq = 24
|
||||
dt = 1.0 / freq
|
||||
)
|
||||
ticker := time.NewTicker(time.Second / freq)
|
||||
defer ticker.Stop()
|
||||
|
||||
off := pixel.ZV
|
||||
|
||||
for range ticker.C {
|
||||
tw.Offset(off.Scaled(-1))
|
||||
|
||||
if intensity < 0.01*dt {
|
||||
break
|
||||
}
|
||||
|
||||
off = pixel.V((rand.Float64()-0.5)*intensity*2, (rand.Float64()-0.5)*intensity*2)
|
||||
intensity -= friction * dt
|
||||
|
||||
tw.Offset(off)
|
||||
}
|
||||
}
|
||||
|
||||
func scroll(tw *typewriter, intensity, speedUp float64) {
|
||||
const (
|
||||
freq = 120
|
||||
dt = 1.0 / freq
|
||||
)
|
||||
ticker := time.NewTicker(time.Second / freq)
|
||||
defer ticker.Stop()
|
||||
|
||||
speed := 0.0
|
||||
|
||||
for range ticker.C {
|
||||
if math.Abs(tw.Dot().Y+tw.Position().Y) < 0.01 {
|
||||
break
|
||||
}
|
||||
|
||||
targetSpeed := -(tw.Dot().Y + tw.Position().Y) * intensity
|
||||
if speed < targetSpeed {
|
||||
speed += speedUp * dt
|
||||
} else {
|
||||
speed = targetSpeed
|
||||
}
|
||||
|
||||
tw.Move(pixel.V(0, speed))
|
||||
}
|
||||
}
|
||||
|
||||
type dotlight struct {
|
||||
tw *typewriter
|
||||
color color.Color
|
||||
radius float64
|
||||
intensity float64
|
||||
acceleration float64
|
||||
maxSpeed float64
|
||||
|
||||
pos pixel.Vec
|
||||
vel pixel.Vec
|
||||
|
||||
imd *imdraw.IMDraw
|
||||
}
|
||||
|
||||
func newDotlight(tw *typewriter, c color.Color, radius, intensity, acceleration, maxSpeed float64) *dotlight {
|
||||
return &dotlight{
|
||||
tw: tw,
|
||||
color: c,
|
||||
radius: radius,
|
||||
intensity: intensity,
|
||||
acceleration: acceleration,
|
||||
maxSpeed: maxSpeed,
|
||||
pos: tw.Dot(),
|
||||
vel: pixel.ZV,
|
||||
imd: imdraw.New(nil),
|
||||
}
|
||||
}
|
||||
|
||||
func (dl *dotlight) Update(dt float64) {
|
||||
targetVel := dl.tw.Dot().Add(dl.tw.Position()).Sub(dl.pos).Scaled(dl.intensity)
|
||||
acc := targetVel.Sub(dl.vel).Scaled(dl.acceleration)
|
||||
dl.vel = dl.vel.Add(acc.Scaled(dt))
|
||||
if dl.vel.Len() > dl.maxSpeed {
|
||||
dl.vel = dl.vel.Unit().Scaled(dl.maxSpeed)
|
||||
}
|
||||
dl.pos = dl.pos.Add(dl.vel.Scaled(dt))
|
||||
}
|
||||
|
||||
func (dl *dotlight) Draw(t pixel.Target, m pixel.Matrix) {
|
||||
dl.imd.Clear()
|
||||
dl.imd.SetMatrix(m)
|
||||
dl.imd.Color = dl.color
|
||||
dl.imd.Push(dl.pos)
|
||||
dl.imd.Color = pixel.Alpha(0)
|
||||
for i := 0.0; i <= 32; i++ {
|
||||
angle := i * 2 * math.Pi / 32
|
||||
dl.imd.Push(dl.pos.Add(pixel.V(dl.radius, 0).Rotated(angle)))
|
||||
}
|
||||
dl.imd.Polygon(0)
|
||||
dl.imd.Draw(t)
|
||||
}
|
||||
|
||||
func run() {
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
|
||||
cfg := pixelgl.WindowConfig{
|
||||
Title: "Typewriter",
|
||||
Bounds: pixel.R(0, 0, 1024, 768),
|
||||
Resizable: true,
|
||||
}
|
||||
win, err := pixelgl.NewWindow(cfg)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
win.SetSmooth(true)
|
||||
|
||||
var (
|
||||
regular = text.NewAtlas(
|
||||
ttfFromBytesMust(goregular.TTF, 42),
|
||||
text.ASCII, text.RangeTable(unicode.Latin),
|
||||
)
|
||||
bold = text.NewAtlas(
|
||||
ttfFromBytesMust(gobold.TTF, 42),
|
||||
text.ASCII, text.RangeTable(unicode.Latin),
|
||||
)
|
||||
italic = text.NewAtlas(
|
||||
ttfFromBytesMust(goitalic.TTF, 42),
|
||||
text.ASCII, text.RangeTable(unicode.Latin),
|
||||
)
|
||||
|
||||
bgColor = color.RGBA{
|
||||
R: 241,
|
||||
G: 241,
|
||||
B: 212,
|
||||
A: 255,
|
||||
}
|
||||
fgColor = color.RGBA{
|
||||
R: 0,
|
||||
G: 15,
|
||||
B: 85,
|
||||
A: 255,
|
||||
}
|
||||
|
||||
tw = newTypewriter(pixel.ToRGBA(fgColor).Scaled(0.9), regular, bold, italic)
|
||||
dl = newDotlight(tw, colornames.Red, 6, 30, 20, 1600)
|
||||
)
|
||||
|
||||
fps := time.Tick(time.Second / 120)
|
||||
last := time.Now()
|
||||
for !win.Closed() {
|
||||
for _, r := range win.Typed() {
|
||||
go typeRune(tw, r)
|
||||
}
|
||||
if win.JustPressed(pixelgl.KeyTab) || win.Repeated(pixelgl.KeyTab) {
|
||||
go typeRune(tw, '\t')
|
||||
}
|
||||
if win.JustPressed(pixelgl.KeyEnter) || win.Repeated(pixelgl.KeyEnter) {
|
||||
go typeRune(tw, '\n')
|
||||
go scroll(tw, 20, 6400)
|
||||
}
|
||||
if win.JustPressed(pixelgl.KeyBackspace) || win.Repeated(pixelgl.KeyBackspace) {
|
||||
go back(tw)
|
||||
}
|
||||
|
||||
dt := time.Since(last).Seconds()
|
||||
last = time.Now()
|
||||
|
||||
tw.Update(dt)
|
||||
dl.Update(dt)
|
||||
|
||||
win.Clear(bgColor)
|
||||
|
||||
m := pixel.IM.Moved(pixel.V(32, 32))
|
||||
tw.Draw(win, m)
|
||||
dl.Draw(win, m)
|
||||
|
||||
win.Update()
|
||||
<-fps
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
pixelgl.Run(run)
|
||||
}
|
After Width: | Height: | Size: 156 KiB |
|
@ -0,0 +1,8 @@
|
|||
# Xor
|
||||
|
||||
This example demonstrates an unusual Porter-Duff composition method: Xor. (And the capability of
|
||||
drawing circles.)
|
||||
|
||||
Just thought it was cool.
|
||||
|
||||

|
|
@ -0,0 +1,76 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"math"
|
||||
"time"
|
||||
|
||||
"github.com/faiface/pixel"
|
||||
"github.com/faiface/pixel/imdraw"
|
||||
"github.com/faiface/pixel/pixelgl"
|
||||
"golang.org/x/image/colornames"
|
||||
)
|
||||
|
||||
func run() {
|
||||
cfg := pixelgl.WindowConfig{
|
||||
Title: "Xor",
|
||||
Bounds: pixel.R(0, 0, 1024, 768),
|
||||
Resizable: true,
|
||||
VSync: true,
|
||||
}
|
||||
win, err := pixelgl.NewWindow(cfg)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
imd := imdraw.New(nil)
|
||||
|
||||
canvas := pixelgl.NewCanvas(win.Bounds())
|
||||
|
||||
start := time.Now()
|
||||
for !win.Closed() {
|
||||
// in case window got resized, we also need to resize our canvas
|
||||
canvas.SetBounds(win.Bounds())
|
||||
|
||||
offset := math.Sin(time.Since(start).Seconds()) * 300
|
||||
|
||||
// clear the canvas to be totally transparent and set the xor compose method
|
||||
canvas.Clear(pixel.Alpha(0))
|
||||
canvas.SetComposeMethod(pixel.ComposeXor)
|
||||
|
||||
// red circle
|
||||
imd.Clear()
|
||||
imd.Color = pixel.RGB(1, 0, 0)
|
||||
imd.Push(win.Bounds().Center().Add(pixel.V(-offset, 0)))
|
||||
imd.Circle(200, 0)
|
||||
imd.Draw(canvas)
|
||||
|
||||
// blue circle
|
||||
imd.Clear()
|
||||
imd.Color = pixel.RGB(0, 0, 1)
|
||||
imd.Push(win.Bounds().Center().Add(pixel.V(offset, 0)))
|
||||
imd.Circle(150, 0)
|
||||
imd.Draw(canvas)
|
||||
|
||||
// yellow circle
|
||||
imd.Clear()
|
||||
imd.Color = pixel.RGB(1, 1, 0)
|
||||
imd.Push(win.Bounds().Center().Add(pixel.V(0, -offset)))
|
||||
imd.Circle(100, 0)
|
||||
imd.Draw(canvas)
|
||||
|
||||
// magenta circle
|
||||
imd.Clear()
|
||||
imd.Color = pixel.RGB(1, 0, 1)
|
||||
imd.Push(win.Bounds().Center().Add(pixel.V(0, offset)))
|
||||
imd.Circle(50, 0)
|
||||
imd.Draw(canvas)
|
||||
|
||||
win.Clear(colornames.Green)
|
||||
canvas.Draw(win, pixel.IM.Moved(win.Bounds().Center()))
|
||||
win.Update()
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
pixelgl.Run(run)
|
||||
}
|
After Width: | Height: | Size: 10 KiB |
358
geometry.go
|
@ -3,50 +3,55 @@ package pixel
|
|||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"math/cmplx"
|
||||
|
||||
"github.com/go-gl/mathgl/mgl64"
|
||||
)
|
||||
|
||||
// Vec is a 2D vector type. It is unusually implemented as complex128 for convenience. Since
|
||||
// Go does not allow operator overloading, implementing vector as a struct leads to a bunch of
|
||||
// methods for addition, subtraction and multiplication of vectors. With complex128, much of
|
||||
// this functionality is given through operators.
|
||||
// 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)
|
||||
//
|
||||
// Add and subtract them using the standard + and - operators:
|
||||
// Use various methods to manipulate them:
|
||||
//
|
||||
// w := u + v
|
||||
// fmt.Println(w) // Vec(9, -1)
|
||||
// fmt.Println(u - v) // Vec(-7, 5)
|
||||
//
|
||||
// Additional standard vector operations can be obtained with methods:
|
||||
//
|
||||
// u := pixel.V(2, 3)
|
||||
// v := pixel.V(8, 1)
|
||||
// if u.X() < 0 {
|
||||
// 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 complex128
|
||||
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(complex(x, y))
|
||||
return Vec{x, y}
|
||||
}
|
||||
|
||||
// X returns a 2D vector with coordinates (x, 0).
|
||||
func X(x float64) Vec {
|
||||
return V(x, 0)
|
||||
}
|
||||
|
||||
// Y returns a 2D vector with coordinates (0, y).
|
||||
func Y(y float64) Vec {
|
||||
return V(0, 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.
|
||||
|
@ -55,76 +60,96 @@ func Y(y float64) Vec {
|
|||
// 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())
|
||||
}
|
||||
|
||||
// X returns the x coordinate of the vector u.
|
||||
func (u Vec) X() float64 {
|
||||
return real(u)
|
||||
}
|
||||
|
||||
// Y returns the y coordinate of the vector u.
|
||||
func (u Vec) Y() float64 {
|
||||
return imag(u)
|
||||
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 real(u), imag(u)
|
||||
return u.X, u.Y
|
||||
}
|
||||
|
||||
// Len returns the length of the vector u.
|
||||
func (u Vec) Len() float64 {
|
||||
return cmplx.Abs(complex128(u))
|
||||
}
|
||||
|
||||
// Angle returns the angle between the vector u and the x-axis. The result is in the range [-Pi, Pi].
|
||||
func (u Vec) Angle() float64 {
|
||||
return cmplx.Phase(complex128(u))
|
||||
}
|
||||
|
||||
// Unit returns a vector of length 1 facing the direction of u (has the same angle).
|
||||
func (u Vec) Unit() Vec {
|
||||
if u == 0 {
|
||||
return 1
|
||||
// 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,
|
||||
}
|
||||
return u / V(u.Len(), 0)
|
||||
}
|
||||
|
||||
// Scaled returns the vector u multiplied by c.
|
||||
func (u Vec) Scaled(c float64) Vec {
|
||||
return u * V(c, 0)
|
||||
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 V(u.X()*v.X(), u.Y()*v.Y())
|
||||
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 u * V(cos, sin)
|
||||
return Vec{
|
||||
u.X*cos - u.Y*sin,
|
||||
u.X*sin + u.Y*cos,
|
||||
}
|
||||
}
|
||||
|
||||
// WithX return the vector u with the x coordinate changed to the given value.
|
||||
func (u Vec) WithX(x float64) Vec {
|
||||
return V(x, u.Y())
|
||||
}
|
||||
|
||||
// WithY returns the vector u with the y coordinate changed to the given value.
|
||||
func (u Vec) WithY(y float64) Vec {
|
||||
return V(u.X(), y)
|
||||
// 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()
|
||||
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()
|
||||
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
|
||||
|
@ -133,10 +158,10 @@ func (u Vec) Cross(v Vec) float64 {
|
|||
// 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 V(
|
||||
f(u.X()),
|
||||
f(u.Y()),
|
||||
)
|
||||
return Vec{
|
||||
f(u.X),
|
||||
f(u.Y),
|
||||
}
|
||||
}
|
||||
|
||||
// Lerp returns a linear interpolation between vectors a and b.
|
||||
|
@ -145,7 +170,7 @@ func (u Vec) Map(f func(float64) float64) Vec {
|
|||
// 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) + b.Scaled(t)
|
||||
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
|
||||
|
@ -158,24 +183,12 @@ type Rect struct {
|
|||
}
|
||||
|
||||
// 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: V(minX, minY),
|
||||
Max: V(maxX, maxY),
|
||||
}
|
||||
}
|
||||
|
||||
// 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: V(
|
||||
math.Min(r.Min.X(), r.Max.X()),
|
||||
math.Min(r.Min.Y(), r.Max.Y()),
|
||||
),
|
||||
Max: V(
|
||||
math.Max(r.Min.X(), r.Max.X()),
|
||||
math.Max(r.Min.Y(), r.Max.Y()),
|
||||
),
|
||||
Min: Vec{minX, minY},
|
||||
Max: Vec{maxX, maxY},
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -185,17 +198,31 @@ func (r Rect) Norm() Rect {
|
|||
// 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())
|
||||
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()
|
||||
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()
|
||||
return r.Max.Y - r.Min.Y
|
||||
}
|
||||
|
||||
// Size returns the vector of width and height of the Rect.
|
||||
|
@ -203,36 +230,21 @@ 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 (r.Min + r.Max) / 2
|
||||
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 + delta,
|
||||
Max: r.Max + delta,
|
||||
}
|
||||
}
|
||||
|
||||
// WithMin returns the Rect with it's Min changed to the given position.
|
||||
//
|
||||
// Note, that the Rect is not automatically normalized.
|
||||
func (r Rect) WithMin(min Vec) Rect {
|
||||
return Rect{
|
||||
Min: min,
|
||||
Max: r.Max,
|
||||
}
|
||||
}
|
||||
|
||||
// WithMax returns the Rect with it's Max changed to the given position.
|
||||
//
|
||||
// Note, that the Rect is not automatically normalized.
|
||||
func (r Rect) WithMax(max Vec) Rect {
|
||||
return Rect{
|
||||
Min: r.Min,
|
||||
Max: max,
|
||||
Min: r.Min.Add(delta),
|
||||
Max: r.Max.Add(delta),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -243,16 +255,16 @@ func (r Rect) WithMax(max Vec) Rect {
|
|||
// 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 sizes of zero area and will panic. Use ResizedMin in the
|
||||
// case of zero area.
|
||||
// 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 || size.X()*size.Y() == 0 {
|
||||
if r.W()*r.H() == 0 {
|
||||
panic(fmt.Errorf("(%T).Resize: zero area", r))
|
||||
}
|
||||
fraction := size.ScaledXY(V(1/r.W(), 1/r.H()))
|
||||
fraction := Vec{size.X / r.W(), size.Y / r.H()}
|
||||
return Rect{
|
||||
Min: anchor + (r.Min - anchor).ScaledXY(fraction),
|
||||
Max: anchor + (r.Max - anchor).ScaledXY(fraction),
|
||||
Min: anchor.Add(r.Min.Sub(anchor).ScaledXY(fraction)),
|
||||
Max: anchor.Add(r.Max.Sub(anchor).ScaledXY(fraction)),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -263,44 +275,86 @@ func (r Rect) Resized(anchor, size Vec) Rect {
|
|||
func (r Rect) ResizedMin(size Vec) Rect {
|
||||
return Rect{
|
||||
Min: r.Min,
|
||||
Max: r.Min + size,
|
||||
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()
|
||||
return r.Min.X <= u.X && u.X <= r.Max.X && r.Min.Y <= u.Y && u.Y <= r.Max.Y
|
||||
}
|
||||
|
||||
// Matrix is a 3x3 transformation matrix that can be used for all kinds of spacial transforms, such
|
||||
// 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 3x2 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(0, math.Pi/2)
|
||||
// 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.
|
||||
type Matrix [9]float64
|
||||
//
|
||||
// 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(mgl64.Ident3())
|
||||
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 {
|
||||
m3 := mgl64.Mat3(m)
|
||||
m3 = mgl64.Translate2D(delta.XY()).Mul3(m3)
|
||||
return Matrix(m3)
|
||||
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 {
|
||||
m3 := mgl64.Mat3(m)
|
||||
m3 = mgl64.Translate2D((-around).XY()).Mul3(m3)
|
||||
m3 = mgl64.Scale2D(scale.XY()).Mul3(m3)
|
||||
m3 = mgl64.Translate2D(around.XY()).Mul3(m3)
|
||||
return Matrix(m3)
|
||||
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.
|
||||
|
@ -310,36 +364,42 @@ func (m Matrix) Scaled(around Vec, scale float64) Matrix {
|
|||
|
||||
// Rotated rotates everything around a given point by the given angle in radians.
|
||||
func (m Matrix) Rotated(around Vec, angle float64) Matrix {
|
||||
m3 := mgl64.Mat3(m)
|
||||
m3 = mgl64.Translate2D((-around).XY()).Mul3(m3)
|
||||
m3 = mgl64.Rotate3DZ(angle).Mul3(m3)
|
||||
m3 = mgl64.Translate2D(around.XY()).Mul3(m3)
|
||||
return Matrix(m3)
|
||||
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 {
|
||||
m3 := mgl64.Mat3(m)
|
||||
m3 = mgl64.Mat3(next).Mul3(m3)
|
||||
return Matrix(m3)
|
||||
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 {
|
||||
m3 := mgl64.Mat3(m)
|
||||
proj := m3.Mul3x1(mgl64.Vec3{u.X(), u.Y(), 1})
|
||||
return V(proj.X(), proj.Y())
|
||||
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 {
|
||||
m3 := mgl64.Mat3(m)
|
||||
inv := m3.Inv()
|
||||
unproj := inv.Mul3x1(mgl64.Vec3{u.X(), u.Y(), 1})
|
||||
return V(unproj.X(), unproj.Y())
|
||||
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]}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,79 @@
|
|||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
340
imdraw/imdraw.go
|
@ -1,3 +1,5 @@
|
|||
// Package imdraw implements a basic primitive geometry shape and pictured polygon drawing for Pixel
|
||||
// with a nice immediate-mode-like API.
|
||||
package imdraw
|
||||
|
||||
import (
|
||||
|
@ -7,7 +9,7 @@ import (
|
|||
"github.com/faiface/pixel"
|
||||
)
|
||||
|
||||
// IMDraw is an immediate-like-mode shape drawer and BasicTarget. IMDraw supports TrianglesPosition,
|
||||
// IMDraw is an immediate-mode-like shape drawer and BasicTarget. IMDraw supports TrianglesPosition,
|
||||
// TrianglesColor, TrianglesPicture and PictureColor.
|
||||
//
|
||||
// IMDraw, other than a regular BasicTarget, is used to draw shapes. To draw shapes, you first need
|
||||
|
@ -21,9 +23,9 @@ import (
|
|||
//
|
||||
// imd.Line(20) // draws a 20 units thick line
|
||||
//
|
||||
// Use various methods to change properties of Pushed points:
|
||||
// Set exported fields to change properties of Pushed points:
|
||||
//
|
||||
// imd.Color(pixel.NRGBA{R: 1, G: 0, B: 0, A: 1})
|
||||
// imd.Color = pixel.RGB(1, 0, 0)
|
||||
// imd.Push(pixel.V(200, 200))
|
||||
// imd.Circle(400, 0)
|
||||
//
|
||||
|
@ -43,10 +45,16 @@ import (
|
|||
// - Ellipse
|
||||
// - Ellipse arc
|
||||
type IMDraw struct {
|
||||
Color color.Color
|
||||
Picture pixel.Vec
|
||||
Intensity float64
|
||||
Precision int
|
||||
EndShape EndShape
|
||||
|
||||
points []point
|
||||
opts point
|
||||
pool [][]point
|
||||
matrix pixel.Matrix
|
||||
mask pixel.NRGBA
|
||||
mask pixel.RGBA
|
||||
|
||||
tri *pixel.TrianglesData
|
||||
batch *pixel.Batch
|
||||
|
@ -56,7 +64,7 @@ var _ pixel.BasicTarget = (*IMDraw)(nil)
|
|||
|
||||
type point struct {
|
||||
pos pixel.Vec
|
||||
col pixel.NRGBA
|
||||
col pixel.RGBA
|
||||
pic pixel.Vec
|
||||
in float64
|
||||
precision int
|
||||
|
@ -87,7 +95,7 @@ func New(pic pixel.Picture) *IMDraw {
|
|||
batch: pixel.NewBatch(tri, pic),
|
||||
}
|
||||
im.SetMatrix(pixel.IM)
|
||||
im.SetColorMask(pixel.NRGBA{R: 1, G: 1, B: 1, A: 1})
|
||||
im.SetColorMask(pixel.Alpha(1))
|
||||
im.Reset()
|
||||
return im
|
||||
}
|
||||
|
@ -102,12 +110,17 @@ func (imd *IMDraw) Clear() {
|
|||
//
|
||||
// This does not affect matrix and color mask set by SetMatrix and SetColorMask.
|
||||
func (imd *IMDraw) Reset() {
|
||||
imd.points = nil
|
||||
imd.opts = point{}
|
||||
imd.Precision(64)
|
||||
imd.points = imd.points[:0]
|
||||
imd.Color = pixel.Alpha(1)
|
||||
imd.Picture = pixel.ZV
|
||||
imd.Intensity = 0
|
||||
imd.Precision = 64
|
||||
imd.EndShape = NoEndShape
|
||||
}
|
||||
|
||||
// Draw draws all currently drawn shapes inside the IM onto another Target.
|
||||
//
|
||||
// Note, that IMDraw's matrix and color mask have no effect here.
|
||||
func (imd *IMDraw) Draw(t pixel.Target) {
|
||||
imd.batch.Draw(t)
|
||||
}
|
||||
|
@ -115,8 +128,18 @@ 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) {
|
||||
if _, ok := imd.Color.(pixel.RGBA); !ok {
|
||||
imd.Color = pixel.ToRGBA(imd.Color)
|
||||
}
|
||||
opts := point{
|
||||
col: imd.Color.(pixel.RGBA),
|
||||
pic: imd.Picture,
|
||||
in: imd.Intensity,
|
||||
precision: imd.Precision,
|
||||
endshape: imd.EndShape,
|
||||
}
|
||||
for _, pt := range pts {
|
||||
imd.pushPt(pt, imd.opts)
|
||||
imd.pushPt(pt, opts)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -125,33 +148,6 @@ func (imd *IMDraw) pushPt(pos pixel.Vec, pt point) {
|
|||
imd.points = append(imd.points, pt)
|
||||
}
|
||||
|
||||
// Color sets the color of the next Pushed points.
|
||||
func (imd *IMDraw) Color(color color.Color) {
|
||||
imd.opts.col = pixel.ToNRGBA(color)
|
||||
}
|
||||
|
||||
// Picture sets the Picture coordinates of the next Pushed points.
|
||||
func (imd *IMDraw) Picture(pic pixel.Vec) {
|
||||
imd.opts.pic = pic
|
||||
}
|
||||
|
||||
// Intensity sets the picture Intensity of the next Pushed points.
|
||||
func (imd *IMDraw) Intensity(in float64) {
|
||||
imd.opts.in = in
|
||||
}
|
||||
|
||||
// Precision sets the curve/circle drawing precision of the next Pushed points.
|
||||
//
|
||||
// It is the number of segments per 360 degrees.
|
||||
func (imd *IMDraw) Precision(p int) {
|
||||
imd.opts.precision = p
|
||||
}
|
||||
|
||||
// EndShape sets the endshape of the next Pushed points.
|
||||
func (imd *IMDraw) EndShape(es EndShape) {
|
||||
imd.opts.endshape = es
|
||||
}
|
||||
|
||||
// SetMatrix sets a Matrix that all further points will be transformed by.
|
||||
func (imd *IMDraw) SetMatrix(m pixel.Matrix) {
|
||||
imd.matrix = m
|
||||
|
@ -160,7 +156,7 @@ func (imd *IMDraw) SetMatrix(m pixel.Matrix) {
|
|||
|
||||
// SetColorMask sets a color that all further point's color will be multiplied by.
|
||||
func (imd *IMDraw) SetColorMask(color color.Color) {
|
||||
imd.mask = pixel.ToNRGBA(color)
|
||||
imd.mask = pixel.ToRGBA(color)
|
||||
imd.batch.SetColorMask(imd.mask)
|
||||
}
|
||||
|
||||
|
@ -179,6 +175,20 @@ func (imd *IMDraw) Line(thickness float64) {
|
|||
imd.polyline(thickness, false)
|
||||
}
|
||||
|
||||
// Rectangle draws a rectangle between each two subsequent Pushed points. Drawing a rectangle
|
||||
// between two points means drawing a rectangle with sides parallel to the axes of the coordinate
|
||||
// system, where the two points specify it's two opposite corners.
|
||||
//
|
||||
// If the thickness is 0, rectangles will be filled, otherwise will be outlined with the given
|
||||
// thickness.
|
||||
func (imd *IMDraw) Rectangle(thickness float64) {
|
||||
if thickness == 0 {
|
||||
imd.fillRectangle()
|
||||
} else {
|
||||
imd.outlineRectangle(thickness)
|
||||
}
|
||||
}
|
||||
|
||||
// Polygon draws a polygon from the Pushed points. If the thickness is 0, the convex polygon will be
|
||||
// filled. Otherwise, an outline of the specified thickness will be drawn. The outline does not have
|
||||
// to be convex.
|
||||
|
@ -249,10 +259,22 @@ func (imd *IMDraw) EllipseArc(radius pixel.Vec, low, high, thickness float64) {
|
|||
|
||||
func (imd *IMDraw) getAndClearPoints() []point {
|
||||
points := imd.points
|
||||
imd.points = nil
|
||||
// use one of the existing pools so we don't reallocate as often
|
||||
if len(imd.pool) > 0 {
|
||||
pos := len(imd.pool) - 1
|
||||
imd.points = imd.pool[pos][:0]
|
||||
imd.pool = imd.pool[:pos]
|
||||
} else {
|
||||
imd.points = nil
|
||||
}
|
||||
return points
|
||||
}
|
||||
|
||||
func (imd *IMDraw) restorePoints(points []point) {
|
||||
imd.pool = append(imd.pool, imd.points)
|
||||
imd.points = points[:0]
|
||||
}
|
||||
|
||||
func (imd *IMDraw) applyMatrixAndMask(off int) {
|
||||
for i := range (*imd.tri)[off:] {
|
||||
(*imd.tri)[off+i].Position = imd.matrix.Project((*imd.tri)[off+i].Position)
|
||||
|
@ -260,10 +282,75 @@ func (imd *IMDraw) applyMatrixAndMask(off int) {
|
|||
}
|
||||
}
|
||||
|
||||
func (imd *IMDraw) fillRectangle() {
|
||||
points := imd.getAndClearPoints()
|
||||
|
||||
if len(points) < 2 {
|
||||
imd.restorePoints(points)
|
||||
return
|
||||
}
|
||||
|
||||
off := imd.tri.Len()
|
||||
imd.tri.SetLen(imd.tri.Len() + 6*(len(points)-1))
|
||||
|
||||
for i, j := 0, off; i+1 < len(points); i, j = i+1, j+6 {
|
||||
a, b := points[i], points[i+1]
|
||||
c := point{
|
||||
pos: pixel.V(a.pos.X, b.pos.Y),
|
||||
col: a.col.Add(b.col).Mul(pixel.Alpha(0.5)),
|
||||
pic: pixel.V(a.pic.X, b.pic.Y),
|
||||
in: (a.in + b.in) / 2,
|
||||
}
|
||||
d := point{
|
||||
pos: pixel.V(b.pos.X, a.pos.Y),
|
||||
col: a.col.Add(b.col).Mul(pixel.Alpha(0.5)),
|
||||
pic: pixel.V(b.pic.X, a.pic.Y),
|
||||
in: (a.in + b.in) / 2,
|
||||
}
|
||||
|
||||
for k, p := range [...]point{a, b, c, a, b, d} {
|
||||
(*imd.tri)[j+k].Position = p.pos
|
||||
(*imd.tri)[j+k].Color = p.col
|
||||
(*imd.tri)[j+k].Picture = p.pic
|
||||
(*imd.tri)[j+k].Intensity = p.in
|
||||
}
|
||||
}
|
||||
|
||||
imd.applyMatrixAndMask(off)
|
||||
imd.batch.Dirty()
|
||||
|
||||
imd.restorePoints(points)
|
||||
}
|
||||
|
||||
func (imd *IMDraw) outlineRectangle(thickness float64) {
|
||||
points := imd.getAndClearPoints()
|
||||
|
||||
if len(points) < 2 {
|
||||
imd.restorePoints(points)
|
||||
return
|
||||
}
|
||||
|
||||
for i := 0; i+1 < len(points); i++ {
|
||||
a, b := points[i], points[i+1]
|
||||
mid := a
|
||||
mid.col = a.col.Add(b.col).Mul(pixel.Alpha(0.5))
|
||||
mid.in = (a.in + b.in) / 2
|
||||
|
||||
imd.pushPt(a.pos, a)
|
||||
imd.pushPt(pixel.V(a.pos.X, b.pos.Y), mid)
|
||||
imd.pushPt(b.pos, b)
|
||||
imd.pushPt(pixel.V(b.pos.X, a.pos.Y), mid)
|
||||
imd.polyline(thickness, true)
|
||||
}
|
||||
|
||||
imd.restorePoints(points)
|
||||
}
|
||||
|
||||
func (imd *IMDraw) fillPolygon() {
|
||||
points := imd.getAndClearPoints()
|
||||
|
||||
if len(points) < 3 {
|
||||
imd.restorePoints(points)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -271,24 +358,19 @@ func (imd *IMDraw) fillPolygon() {
|
|||
imd.tri.SetLen(imd.tri.Len() + 3*(len(points)-2))
|
||||
|
||||
for i, j := 1, off; i+1 < len(points); i, j = i+1, j+3 {
|
||||
(*imd.tri)[j+0].Position = points[0].pos
|
||||
(*imd.tri)[j+0].Color = points[0].col
|
||||
(*imd.tri)[j+0].Picture = points[0].pic
|
||||
(*imd.tri)[j+0].Intensity = points[0].in
|
||||
|
||||
(*imd.tri)[j+1].Position = points[i].pos
|
||||
(*imd.tri)[j+1].Color = points[i].col
|
||||
(*imd.tri)[j+1].Picture = points[i].pic
|
||||
(*imd.tri)[j+1].Intensity = points[i].in
|
||||
|
||||
(*imd.tri)[j+2].Position = points[i+1].pos
|
||||
(*imd.tri)[j+2].Color = points[i+1].col
|
||||
(*imd.tri)[j+2].Picture = points[i+1].pic
|
||||
(*imd.tri)[j+2].Intensity = points[i+1].in
|
||||
for k, p := range [...]int{0, i, i + 1} {
|
||||
tri := &(*imd.tri)[j+k]
|
||||
tri.Position = points[p].pos
|
||||
tri.Color = points[p].col
|
||||
tri.Picture = points[p].pic
|
||||
tri.Intensity = points[p].in
|
||||
}
|
||||
}
|
||||
|
||||
imd.applyMatrixAndMask(off)
|
||||
imd.batch.Dirty()
|
||||
|
||||
imd.restorePoints(points)
|
||||
}
|
||||
|
||||
func (imd *IMDraw) fillEllipseArc(radius pixel.Vec, low, high float64) {
|
||||
|
@ -303,24 +385,24 @@ func (imd *IMDraw) fillEllipseArc(radius pixel.Vec, low, high float64) {
|
|||
|
||||
for i := range (*imd.tri)[off:] {
|
||||
(*imd.tri)[off+i].Color = pt.col
|
||||
(*imd.tri)[off+i].Picture = 0
|
||||
(*imd.tri)[off+i].Picture = pixel.ZV
|
||||
(*imd.tri)[off+i].Intensity = 0
|
||||
}
|
||||
|
||||
for i, j := 0.0, off; i < num; i, j = i+1, j+3 {
|
||||
angle := low + i*delta
|
||||
sin, cos := math.Sincos(angle)
|
||||
a := pt.pos + pixel.V(
|
||||
radius.X()*cos,
|
||||
radius.Y()*sin,
|
||||
)
|
||||
a := pt.pos.Add(pixel.V(
|
||||
radius.X*cos,
|
||||
radius.Y*sin,
|
||||
))
|
||||
|
||||
angle = low + (i+1)*delta
|
||||
sin, cos = math.Sincos(angle)
|
||||
b := pt.pos + pixel.V(
|
||||
radius.X()*cos,
|
||||
radius.Y()*sin,
|
||||
)
|
||||
b := pt.pos.Add(pixel.V(
|
||||
radius.X*cos,
|
||||
radius.Y*sin,
|
||||
))
|
||||
|
||||
(*imd.tri)[j+0].Position = pt.pos
|
||||
(*imd.tri)[j+1].Position = a
|
||||
|
@ -330,6 +412,8 @@ func (imd *IMDraw) fillEllipseArc(radius pixel.Vec, low, high float64) {
|
|||
imd.applyMatrixAndMask(off)
|
||||
imd.batch.Dirty()
|
||||
}
|
||||
|
||||
imd.restorePoints(points)
|
||||
}
|
||||
|
||||
func (imd *IMDraw) outlineEllipseArc(radius pixel.Vec, low, high, thickness float64, doEndShape bool) {
|
||||
|
@ -344,7 +428,7 @@ func (imd *IMDraw) outlineEllipseArc(radius pixel.Vec, low, high, thickness floa
|
|||
|
||||
for i := range (*imd.tri)[off:] {
|
||||
(*imd.tri)[off+i].Color = pt.col
|
||||
(*imd.tri)[off+i].Picture = 0
|
||||
(*imd.tri)[off+i].Picture = pixel.ZV
|
||||
(*imd.tri)[off+i].Intensity = 0
|
||||
}
|
||||
|
||||
|
@ -352,26 +436,26 @@ func (imd *IMDraw) outlineEllipseArc(radius pixel.Vec, low, high, thickness floa
|
|||
angle := low + i*delta
|
||||
sin, cos := math.Sincos(angle)
|
||||
normalSin, normalCos := pixel.V(sin, cos).ScaledXY(radius).Unit().XY()
|
||||
a := pt.pos + pixel.V(
|
||||
radius.X()*cos-thickness/2*normalCos,
|
||||
radius.Y()*sin-thickness/2*normalSin,
|
||||
)
|
||||
b := pt.pos + pixel.V(
|
||||
radius.X()*cos+thickness/2*normalCos,
|
||||
radius.Y()*sin+thickness/2*normalSin,
|
||||
)
|
||||
a := pt.pos.Add(pixel.V(
|
||||
radius.X*cos-thickness/2*normalCos,
|
||||
radius.Y*sin-thickness/2*normalSin,
|
||||
))
|
||||
b := pt.pos.Add(pixel.V(
|
||||
radius.X*cos+thickness/2*normalCos,
|
||||
radius.Y*sin+thickness/2*normalSin,
|
||||
))
|
||||
|
||||
angle = low + (i+1)*delta
|
||||
sin, cos = math.Sincos(angle)
|
||||
normalSin, normalCos = pixel.V(sin, cos).ScaledXY(radius).Unit().XY()
|
||||
c := pt.pos + pixel.V(
|
||||
radius.X()*cos-thickness/2*normalCos,
|
||||
radius.Y()*sin-thickness/2*normalSin,
|
||||
)
|
||||
d := pt.pos + pixel.V(
|
||||
radius.X()*cos+thickness/2*normalCos,
|
||||
radius.Y()*sin+thickness/2*normalSin,
|
||||
)
|
||||
c := pt.pos.Add(pixel.V(
|
||||
radius.X*cos-thickness/2*normalCos,
|
||||
radius.Y*sin-thickness/2*normalSin,
|
||||
))
|
||||
d := pt.pos.Add(pixel.V(
|
||||
radius.X*cos+thickness/2*normalCos,
|
||||
radius.Y*sin+thickness/2*normalSin,
|
||||
))
|
||||
|
||||
(*imd.tri)[j+0].Position = a
|
||||
(*imd.tri)[j+1].Position = b
|
||||
|
@ -386,18 +470,18 @@ func (imd *IMDraw) outlineEllipseArc(radius pixel.Vec, low, high, thickness floa
|
|||
|
||||
if doEndShape {
|
||||
lowSin, lowCos := math.Sincos(low)
|
||||
lowCenter := pt.pos + pixel.V(
|
||||
radius.X()*lowCos,
|
||||
radius.Y()*lowSin,
|
||||
)
|
||||
lowCenter := pt.pos.Add(pixel.V(
|
||||
radius.X*lowCos,
|
||||
radius.Y*lowSin,
|
||||
))
|
||||
normalLowSin, normalLowCos := pixel.V(lowSin, lowCos).ScaledXY(radius).Unit().XY()
|
||||
normalLow := pixel.V(normalLowCos, normalLowSin).Angle()
|
||||
|
||||
highSin, highCos := math.Sincos(high)
|
||||
highCenter := pt.pos + pixel.V(
|
||||
radius.X()*highCos,
|
||||
radius.Y()*highSin,
|
||||
)
|
||||
highCenter := pt.pos.Add(pixel.V(
|
||||
radius.X*highCos,
|
||||
radius.Y*highSin,
|
||||
))
|
||||
normalHighSin, normalHighCos := pixel.V(highSin, highCos).ScaledXY(radius).Unit().XY()
|
||||
normalHigh := pixel.V(normalHighCos, normalHighSin).Angle()
|
||||
|
||||
|
@ -410,30 +494,33 @@ func (imd *IMDraw) outlineEllipseArc(radius pixel.Vec, low, high, thickness floa
|
|||
case NoEndShape:
|
||||
// nothing
|
||||
case SharpEndShape:
|
||||
thick := pixel.X(thickness / 2).Rotated(normalLow)
|
||||
imd.pushPt(lowCenter+thick, pt)
|
||||
imd.pushPt(lowCenter-thick, pt)
|
||||
imd.pushPt(lowCenter-thick.Rotated(math.Pi/2*orientation), pt)
|
||||
thick := pixel.V(thickness/2, 0).Rotated(normalLow)
|
||||
imd.pushPt(lowCenter.Add(thick), pt)
|
||||
imd.pushPt(lowCenter.Sub(thick), pt)
|
||||
imd.pushPt(lowCenter.Sub(thick.Normal().Scaled(orientation)), pt)
|
||||
imd.fillPolygon()
|
||||
thick = pixel.X(thickness / 2).Rotated(normalHigh)
|
||||
imd.pushPt(highCenter+thick, pt)
|
||||
imd.pushPt(highCenter-thick, pt)
|
||||
imd.pushPt(highCenter+thick.Rotated(math.Pi/2*orientation), pt)
|
||||
thick = pixel.V(thickness/2, 0).Rotated(normalHigh)
|
||||
imd.pushPt(highCenter.Add(thick), pt)
|
||||
imd.pushPt(highCenter.Sub(thick), pt)
|
||||
imd.pushPt(highCenter.Add(thick.Normal().Scaled(orientation)), pt)
|
||||
imd.fillPolygon()
|
||||
case RoundEndShape:
|
||||
imd.pushPt(lowCenter, pt)
|
||||
imd.fillEllipseArc(pixel.V(thickness, thickness)/2, normalLow, normalLow-math.Pi*orientation)
|
||||
imd.fillEllipseArc(pixel.V(thickness/2, thickness/2), normalLow, normalLow-math.Pi*orientation)
|
||||
imd.pushPt(highCenter, pt)
|
||||
imd.fillEllipseArc(pixel.V(thickness, thickness)/2, normalHigh, normalHigh+math.Pi*orientation)
|
||||
imd.fillEllipseArc(pixel.V(thickness/2, thickness/2), normalHigh, normalHigh+math.Pi*orientation)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
imd.restorePoints(points)
|
||||
}
|
||||
|
||||
func (imd *IMDraw) polyline(thickness float64, closed bool) {
|
||||
points := imd.getAndClearPoints()
|
||||
|
||||
if len(points) == 0 {
|
||||
imd.restorePoints(points)
|
||||
return
|
||||
}
|
||||
if len(points) == 1 {
|
||||
|
@ -443,25 +530,25 @@ func (imd *IMDraw) polyline(thickness float64, closed bool) {
|
|||
|
||||
// first point
|
||||
j, i := 0, 1
|
||||
normal := (points[i].pos - points[j].pos).Rotated(math.Pi / 2).Unit().Scaled(thickness / 2)
|
||||
ijNormal := points[0].pos.To(points[1].pos).Normal().Unit().Scaled(thickness / 2)
|
||||
|
||||
if !closed {
|
||||
switch points[j].endshape {
|
||||
case NoEndShape:
|
||||
// nothing
|
||||
case SharpEndShape:
|
||||
imd.pushPt(points[j].pos+normal, points[j])
|
||||
imd.pushPt(points[j].pos-normal, points[j])
|
||||
imd.pushPt(points[j].pos+normal.Rotated(math.Pi/2), points[j])
|
||||
imd.pushPt(points[j].pos.Add(ijNormal), points[j])
|
||||
imd.pushPt(points[j].pos.Sub(ijNormal), points[j])
|
||||
imd.pushPt(points[j].pos.Add(ijNormal.Normal()), points[j])
|
||||
imd.fillPolygon()
|
||||
case RoundEndShape:
|
||||
imd.pushPt(points[j].pos, points[j])
|
||||
imd.fillEllipseArc(pixel.V(thickness, thickness)/2, normal.Angle(), normal.Angle()+math.Pi)
|
||||
imd.fillEllipseArc(pixel.V(thickness/2, thickness/2), ijNormal.Angle(), ijNormal.Angle()+math.Pi)
|
||||
}
|
||||
}
|
||||
|
||||
imd.pushPt(points[j].pos+normal, points[j])
|
||||
imd.pushPt(points[j].pos-normal, points[j])
|
||||
imd.pushPt(points[j].pos.Add(ijNormal), points[j])
|
||||
imd.pushPt(points[j].pos.Sub(ijNormal), points[j])
|
||||
|
||||
// middle points
|
||||
for i := 0; i < len(points); i++ {
|
||||
|
@ -469,26 +556,25 @@ func (imd *IMDraw) polyline(thickness float64, closed bool) {
|
|||
|
||||
closing := false
|
||||
if j >= len(points) {
|
||||
if !closed {
|
||||
break
|
||||
}
|
||||
j %= len(points)
|
||||
closing = true
|
||||
}
|
||||
if k >= len(points) {
|
||||
if !closed {
|
||||
break
|
||||
}
|
||||
k %= len(points)
|
||||
}
|
||||
|
||||
ijNormal := (points[j].pos - points[i].pos).Rotated(math.Pi / 2).Unit().Scaled(thickness / 2)
|
||||
jkNormal := (points[k].pos - points[j].pos).Rotated(math.Pi / 2).Unit().Scaled(thickness / 2)
|
||||
jkNormal := points[j].pos.To(points[k].pos).Normal().Unit().Scaled(thickness / 2)
|
||||
|
||||
orientation := 1.0
|
||||
if ijNormal.Cross(jkNormal) > 0 {
|
||||
orientation = -1.0
|
||||
}
|
||||
|
||||
imd.pushPt(points[j].pos-ijNormal, points[j])
|
||||
imd.pushPt(points[j].pos+ijNormal, points[j])
|
||||
imd.pushPt(points[j].pos.Sub(ijNormal), points[j])
|
||||
imd.pushPt(points[j].pos.Add(ijNormal), points[j])
|
||||
imd.fillPolygon()
|
||||
|
||||
switch points[j].endshape {
|
||||
|
@ -496,28 +582,30 @@ func (imd *IMDraw) polyline(thickness float64, closed bool) {
|
|||
// nothing
|
||||
case SharpEndShape:
|
||||
imd.pushPt(points[j].pos, points[j])
|
||||
imd.pushPt(points[j].pos+ijNormal.Scaled(orientation), points[j])
|
||||
imd.pushPt(points[j].pos+jkNormal.Scaled(orientation), points[j])
|
||||
imd.pushPt(points[j].pos.Add(ijNormal.Scaled(orientation)), points[j])
|
||||
imd.pushPt(points[j].pos.Add(jkNormal.Scaled(orientation)), points[j])
|
||||
imd.fillPolygon()
|
||||
case RoundEndShape:
|
||||
imd.pushPt(points[j].pos, points[j])
|
||||
imd.fillEllipseArc(pixel.V(thickness, thickness)/2, ijNormal.Angle(), ijNormal.Angle()-math.Pi)
|
||||
imd.fillEllipseArc(pixel.V(thickness/2, thickness/2), ijNormal.Angle(), ijNormal.Angle()-math.Pi)
|
||||
imd.pushPt(points[j].pos, points[j])
|
||||
imd.fillEllipseArc(pixel.V(thickness, thickness)/2, jkNormal.Angle(), jkNormal.Angle()+math.Pi)
|
||||
imd.fillEllipseArc(pixel.V(thickness/2, thickness/2), jkNormal.Angle(), jkNormal.Angle()+math.Pi)
|
||||
}
|
||||
|
||||
if !closing {
|
||||
imd.pushPt(points[j].pos+jkNormal, points[j])
|
||||
imd.pushPt(points[j].pos-jkNormal, points[j])
|
||||
imd.pushPt(points[j].pos.Add(jkNormal), points[j])
|
||||
imd.pushPt(points[j].pos.Sub(jkNormal), points[j])
|
||||
}
|
||||
// "next" normal becomes previous normal
|
||||
ijNormal = jkNormal
|
||||
}
|
||||
|
||||
// last point
|
||||
i, j = len(points)-2, len(points)-1
|
||||
normal = (points[j].pos - points[i].pos).Rotated(math.Pi / 2).Unit().Scaled(thickness / 2)
|
||||
ijNormal = points[i].pos.To(points[j].pos).Normal().Unit().Scaled(thickness / 2)
|
||||
|
||||
imd.pushPt(points[j].pos-normal, points[j])
|
||||
imd.pushPt(points[j].pos+normal, points[j])
|
||||
imd.pushPt(points[j].pos.Sub(ijNormal), points[j])
|
||||
imd.pushPt(points[j].pos.Add(ijNormal), points[j])
|
||||
imd.fillPolygon()
|
||||
|
||||
if !closed {
|
||||
|
@ -525,13 +613,15 @@ func (imd *IMDraw) polyline(thickness float64, closed bool) {
|
|||
case NoEndShape:
|
||||
// nothing
|
||||
case SharpEndShape:
|
||||
imd.pushPt(points[j].pos+normal, points[j])
|
||||
imd.pushPt(points[j].pos-normal, points[j])
|
||||
imd.pushPt(points[j].pos+normal.Rotated(-math.Pi/2), points[j])
|
||||
imd.pushPt(points[j].pos.Add(ijNormal), points[j])
|
||||
imd.pushPt(points[j].pos.Sub(ijNormal), points[j])
|
||||
imd.pushPt(points[j].pos.Add(ijNormal.Normal().Scaled(-1)), points[j])
|
||||
imd.fillPolygon()
|
||||
case RoundEndShape:
|
||||
imd.pushPt(points[j].pos, points[j])
|
||||
imd.fillEllipseArc(pixel.V(thickness, thickness)/2, normal.Angle(), normal.Angle()-math.Pi)
|
||||
imd.fillEllipseArc(pixel.V(thickness/2, thickness/2), ijNormal.Angle(), ijNormal.Angle()-math.Pi)
|
||||
}
|
||||
}
|
||||
|
||||
imd.restorePoints(points)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,96 @@
|
|||
package imdraw_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"testing"
|
||||
|
||||
"github.com/faiface/pixel"
|
||||
"github.com/faiface/pixel/imdraw"
|
||||
)
|
||||
|
||||
func BenchmarkPush(b *testing.B) {
|
||||
imd := imdraw.New(nil)
|
||||
for i := 0; i < b.N; i++ {
|
||||
imd.Push(pixel.V(123.1, 99.4))
|
||||
}
|
||||
}
|
||||
|
||||
func pointLists(counts ...int) [][]pixel.Vec {
|
||||
lists := make([][]pixel.Vec, len(counts))
|
||||
for i := range lists {
|
||||
lists[i] = make([]pixel.Vec, counts[i])
|
||||
for j := range lists[i] {
|
||||
lists[i][j] = pixel.V(
|
||||
rand.Float64()*5000-2500,
|
||||
rand.Float64()*5000-2500,
|
||||
)
|
||||
}
|
||||
}
|
||||
return lists
|
||||
}
|
||||
|
||||
func BenchmarkLine(b *testing.B) {
|
||||
lists := pointLists(2, 5, 10, 100, 1000)
|
||||
for _, pts := range lists {
|
||||
b.Run(fmt.Sprintf("%d", len(pts)), func(b *testing.B) {
|
||||
imd := imdraw.New(nil)
|
||||
for i := 0; i < b.N; i++ {
|
||||
imd.Push(pts...)
|
||||
imd.Line(1)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkRectangle(b *testing.B) {
|
||||
lists := pointLists(2, 10, 100, 1000)
|
||||
for _, pts := range lists {
|
||||
b.Run(fmt.Sprintf("%d", len(pts)), func(b *testing.B) {
|
||||
imd := imdraw.New(nil)
|
||||
for i := 0; i < b.N; i++ {
|
||||
imd.Push(pts...)
|
||||
imd.Rectangle(0)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkPolygon(b *testing.B) {
|
||||
lists := pointLists(3, 10, 100, 1000)
|
||||
for _, pts := range lists {
|
||||
b.Run(fmt.Sprintf("%d", len(pts)), func(b *testing.B) {
|
||||
imd := imdraw.New(nil)
|
||||
for i := 0; i < b.N; i++ {
|
||||
imd.Push(pts...)
|
||||
imd.Polygon(0)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkEllipseFill(b *testing.B) {
|
||||
lists := pointLists(1, 10, 100, 1000)
|
||||
for _, pts := range lists {
|
||||
b.Run(fmt.Sprintf("%d", len(pts)), func(b *testing.B) {
|
||||
imd := imdraw.New(nil)
|
||||
for i := 0; i < b.N; i++ {
|
||||
imd.Push(pts...)
|
||||
imd.Ellipse(pixel.V(50, 100), 0)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkEllipseOutline(b *testing.B) {
|
||||
lists := pointLists(1, 10, 100, 1000)
|
||||
for _, pts := range lists {
|
||||
b.Run(fmt.Sprintf("%d", len(pts)), func(b *testing.B) {
|
||||
imd := imdraw.New(nil)
|
||||
for i := 0; i < b.N; i++ {
|
||||
imd.Push(pts...)
|
||||
imd.Ellipse(pixel.V(50, 100), 1)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
19
interface.go
|
@ -89,7 +89,7 @@ type TrianglesPosition interface {
|
|||
// TrianglesColor specifies Triangles with Color property.
|
||||
type TrianglesColor interface {
|
||||
Triangles
|
||||
Color(i int) NRGBA
|
||||
Color(i int) RGBA
|
||||
}
|
||||
|
||||
// TrianglesPicture specifies Triangles with Picture propery.
|
||||
|
@ -108,19 +108,6 @@ type Picture interface {
|
|||
// Bounds returns the rectangle of the Picture. All data is located witih this rectangle.
|
||||
// Querying properties outside the rectangle should return default value of that property.
|
||||
Bounds() Rect
|
||||
|
||||
// Slice returns a sub-Picture with specified Bounds.
|
||||
//
|
||||
// A result of Slice-ing outside the original Bounds is unspecified.
|
||||
Slice(Rect) Picture
|
||||
|
||||
// Original returns the most original Picture (may be itself) that this Picture was created
|
||||
// from using Slice-ing.
|
||||
//
|
||||
// Since the Original and this Picture should share the underlying data and this Picture can
|
||||
// be obtained just by slicing the Original, this method can be used for more efficient
|
||||
// caching of Pictures.
|
||||
Original() Picture
|
||||
}
|
||||
|
||||
// TargetPicture is a Picture generated by a Target using MakePicture method. This Picture can be drawn onto
|
||||
|
@ -139,8 +126,8 @@ type TargetPicture interface {
|
|||
// PictureColor specifies Picture with Color property, so that every position inside the Picture's
|
||||
// Bounds has a color.
|
||||
//
|
||||
// Positions outside the Picture's Bounds must return transparent black (NRGBA{R: 0, G: 0, B: 0, A: 0}).
|
||||
// Positions outside the Picture's Bounds must return full transparent (Alpha(0)).
|
||||
type PictureColor interface {
|
||||
Picture
|
||||
Color(at Vec) NRGBA
|
||||
Color(at Vec) RGBA
|
||||
}
|
||||
|
|
|
@ -0,0 +1,63 @@
|
|||
package pixel_test
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"testing"
|
||||
|
||||
"github.com/faiface/pixel"
|
||||
)
|
||||
|
||||
func BenchmarkMatrix(b *testing.B) {
|
||||
b.Run("Moved", func(b *testing.B) {
|
||||
m := pixel.IM
|
||||
for i := 0; i < b.N; i++ {
|
||||
m = m.Moved(pixel.V(4.217, -132.99))
|
||||
}
|
||||
})
|
||||
b.Run("ScaledXY", func(b *testing.B) {
|
||||
m := pixel.IM
|
||||
for i := 0; i < b.N; i++ {
|
||||
m = m.ScaledXY(pixel.V(-5.1, 9.3), pixel.V(2.1, 0.98))
|
||||
}
|
||||
})
|
||||
b.Run("Rotated", func(b *testing.B) {
|
||||
m := pixel.IM
|
||||
for i := 0; i < b.N; i++ {
|
||||
m = m.Rotated(pixel.V(-5.1, 9.3), 1.4)
|
||||
}
|
||||
})
|
||||
b.Run("Chained", func(b *testing.B) {
|
||||
var m1, m2 pixel.Matrix
|
||||
for i := range m1 {
|
||||
m1[i] = rand.Float64()
|
||||
m2[i] = rand.Float64()
|
||||
}
|
||||
for i := 0; i < b.N; i++ {
|
||||
m1 = m1.Chained(m2)
|
||||
}
|
||||
})
|
||||
b.Run("Project", func(b *testing.B) {
|
||||
var m pixel.Matrix
|
||||
for i := range m {
|
||||
m[i] = rand.Float64()
|
||||
}
|
||||
u := pixel.V(1, 1)
|
||||
for i := 0; i < b.N; i++ {
|
||||
u = m.Project(u)
|
||||
}
|
||||
})
|
||||
b.Run("Unproject", func(b *testing.B) {
|
||||
again:
|
||||
var m pixel.Matrix
|
||||
for i := range m {
|
||||
m[i] = rand.Float64()
|
||||
}
|
||||
if (m[0]*m[3])-(m[1]*m[2]) == 0 { // zero determinant, not invertible
|
||||
goto again
|
||||
}
|
||||
u := pixel.V(1, 1)
|
||||
for i := 0; i < b.N; i++ {
|
||||
u = m.Unproject(u)
|
||||
}
|
||||
})
|
||||
}
|
|
@ -3,7 +3,6 @@ package pixelgl
|
|||
import (
|
||||
"fmt"
|
||||
"image/color"
|
||||
"math"
|
||||
|
||||
"github.com/faiface/glhf"
|
||||
"github.com/faiface/mainthread"
|
||||
|
@ -17,35 +16,33 @@ import (
|
|||
//
|
||||
// It supports TrianglesPosition, TrianglesColor, TrianglesPicture and PictureColor.
|
||||
type Canvas struct {
|
||||
// these should **only** be accessed through orig
|
||||
f *glhf.Frame
|
||||
borders pixel.Rect
|
||||
pixels []uint8
|
||||
dirty bool
|
||||
gf *GLFrame
|
||||
shader *glhf.Shader
|
||||
|
||||
// these should **never** be accessed through orig
|
||||
s *glhf.Shader
|
||||
bounds pixel.Rect
|
||||
cmp pixel.ComposeMethod
|
||||
mat mgl32.Mat3
|
||||
col mgl32.Vec4
|
||||
smooth bool
|
||||
|
||||
orig *Canvas
|
||||
sprite *pixel.Sprite
|
||||
}
|
||||
|
||||
// NewCanvas creates a new empty, fully transparent Canvas with given bounds. If the smooth flag is
|
||||
// set, then stretched Pictures will be smoothed and will not be drawn pixely onto this Canvas.
|
||||
func NewCanvas(bounds pixel.Rect, smooth bool) *Canvas {
|
||||
c := &Canvas{
|
||||
smooth: smooth,
|
||||
mat: mgl32.Ident3(),
|
||||
col: mgl32.Vec4{1, 1, 1, 1},
|
||||
}
|
||||
c.orig = c
|
||||
var _ pixel.ComposeTarget = (*Canvas)(nil)
|
||||
|
||||
// NewCanvas creates a new empty, fully transparent Canvas with given bounds.
|
||||
func NewCanvas(bounds pixel.Rect) *Canvas {
|
||||
c := &Canvas{
|
||||
gf: NewGLFrame(bounds),
|
||||
mat: mgl32.Ident3(),
|
||||
col: mgl32.Vec4{1, 1, 1, 1},
|
||||
}
|
||||
|
||||
c.SetBounds(bounds)
|
||||
|
||||
var shader *glhf.Shader
|
||||
mainthread.Call(func() {
|
||||
var err error
|
||||
c.s, err = glhf.NewShader(
|
||||
shader, err = glhf.NewShader(
|
||||
canvasVertexFormat,
|
||||
canvasUniformFormat,
|
||||
canvasVertexShader,
|
||||
|
@ -55,8 +52,7 @@ func NewCanvas(bounds pixel.Rect, smooth bool) *Canvas {
|
|||
panic(errors.Wrap(err, "failed to create Canvas, there's a bug in the shader"))
|
||||
}
|
||||
})
|
||||
|
||||
c.SetBounds(bounds)
|
||||
c.shader = shader
|
||||
|
||||
return c
|
||||
}
|
||||
|
@ -66,7 +62,7 @@ func NewCanvas(bounds pixel.Rect, smooth bool) *Canvas {
|
|||
// TrianglesPosition, TrianglesColor and TrianglesPicture are supported.
|
||||
func (c *Canvas) MakeTriangles(t pixel.Triangles) pixel.TargetTriangles {
|
||||
return &canvasTriangles{
|
||||
GLTriangles: NewGLTriangles(c.s, t),
|
||||
GLTriangles: NewGLTriangles(c.shader, t),
|
||||
dst: c,
|
||||
}
|
||||
}
|
||||
|
@ -75,143 +71,69 @@ func (c *Canvas) MakeTriangles(t pixel.Triangles) pixel.TargetTriangles {
|
|||
//
|
||||
// PictureColor is supported.
|
||||
func (c *Canvas) MakePicture(p pixel.Picture) pixel.TargetPicture {
|
||||
// short paths
|
||||
if cp, ok := p.(*canvasPicture); ok {
|
||||
tp := new(canvasPicture)
|
||||
*tp = *cp
|
||||
tp.dst = c
|
||||
return tp
|
||||
}
|
||||
if ccp, ok := p.(*canvasCanvasPicture); ok {
|
||||
tp := new(canvasCanvasPicture)
|
||||
*tp = *ccp
|
||||
tp.dst = c
|
||||
return tp
|
||||
}
|
||||
|
||||
// Canvas special case
|
||||
if canvas, ok := p.(*Canvas); ok {
|
||||
return &canvasCanvasPicture{
|
||||
src: canvas,
|
||||
dst: c,
|
||||
bounds: c.bounds,
|
||||
return &canvasPicture{
|
||||
GLPicture: cp.GLPicture,
|
||||
dst: c,
|
||||
}
|
||||
}
|
||||
|
||||
bounds := p.Bounds()
|
||||
bx, by, bw, bh := intBounds(bounds)
|
||||
|
||||
pixels := make([]uint8, 4*bw*bh)
|
||||
|
||||
if pd, ok := p.(*pixel.PictureData); ok {
|
||||
// PictureData short path
|
||||
for y := 0; y < bh; y++ {
|
||||
for x := 0; x < bw; x++ {
|
||||
nrgba := pd.Pix[y*pd.Stride+x]
|
||||
off := (y*bw + x) * 4
|
||||
pixels[off+0] = nrgba.R
|
||||
pixels[off+1] = nrgba.G
|
||||
pixels[off+2] = nrgba.B
|
||||
pixels[off+3] = nrgba.A
|
||||
}
|
||||
}
|
||||
} else if p, ok := p.(pixel.PictureColor); ok {
|
||||
for y := 0; y < bh; y++ {
|
||||
for x := 0; x < bw; x++ {
|
||||
at := pixel.V(
|
||||
math.Max(float64(bx+x), bounds.Min.X()),
|
||||
math.Max(float64(by+y), bounds.Min.Y()),
|
||||
)
|
||||
color := p.Color(at)
|
||||
off := (y*bw + x) * 4
|
||||
pixels[off+0] = uint8(color.R * 255)
|
||||
pixels[off+1] = uint8(color.G * 255)
|
||||
pixels[off+2] = uint8(color.B * 255)
|
||||
pixels[off+3] = uint8(color.A * 255)
|
||||
}
|
||||
if gp, ok := p.(GLPicture); ok {
|
||||
return &canvasPicture{
|
||||
GLPicture: gp,
|
||||
dst: c,
|
||||
}
|
||||
}
|
||||
|
||||
var tex *glhf.Texture
|
||||
mainthread.Call(func() {
|
||||
tex = glhf.NewTexture(bw, bh, c.smooth, pixels)
|
||||
})
|
||||
|
||||
cp := &canvasPicture{
|
||||
tex: tex,
|
||||
pixels: pixels,
|
||||
borders: pixel.R(
|
||||
float64(bx), float64(by),
|
||||
float64(bw), float64(bh),
|
||||
),
|
||||
bounds: bounds,
|
||||
dst: c,
|
||||
return &canvasPicture{
|
||||
GLPicture: NewGLPicture(p),
|
||||
dst: c,
|
||||
}
|
||||
cp.orig = cp
|
||||
return cp
|
||||
}
|
||||
|
||||
// SetMatrix sets a Matrix that every point will be projected by.
|
||||
func (c *Canvas) SetMatrix(m pixel.Matrix) {
|
||||
for i := range m {
|
||||
c.mat[i] = float32(m[i])
|
||||
// pixel.Matrix is 3x2 with an implicit 0, 0, 1 row after it. So
|
||||
// [0] [2] [4] [0] [3] [6]
|
||||
// [1] [3] [5] => [1] [4] [7]
|
||||
// 0 0 1 0 0 1
|
||||
// since all matrix ops are affine, the last row never changes, and we don't need to copy it
|
||||
for i, j := range [...]int{0, 1, 3, 4, 6, 7} {
|
||||
c.mat[j] = float32(m[i])
|
||||
}
|
||||
}
|
||||
|
||||
// SetColorMask sets a color that every color in triangles or a picture will be multiplied by.
|
||||
func (c *Canvas) SetColorMask(col color.Color) {
|
||||
nrgba := pixel.NRGBA{R: 1, G: 1, B: 1, A: 1}
|
||||
rgba := pixel.Alpha(1)
|
||||
if col != nil {
|
||||
nrgba = pixel.ToNRGBA(col)
|
||||
rgba = pixel.ToRGBA(col)
|
||||
}
|
||||
c.col = mgl32.Vec4{
|
||||
float32(nrgba.R),
|
||||
float32(nrgba.G),
|
||||
float32(nrgba.B),
|
||||
float32(nrgba.A),
|
||||
float32(rgba.R),
|
||||
float32(rgba.G),
|
||||
float32(rgba.B),
|
||||
float32(rgba.A),
|
||||
}
|
||||
}
|
||||
|
||||
// SetComposeMethod sets a Porter-Duff composition method to be used in the following draws onto
|
||||
// this Canvas.
|
||||
func (c *Canvas) SetComposeMethod(cmp pixel.ComposeMethod) {
|
||||
c.cmp = cmp
|
||||
}
|
||||
|
||||
// SetBounds resizes the Canvas to the new bounds. Old content will be preserved.
|
||||
//
|
||||
// If the new Bounds fit into the Original borders, no new Canvas will be allocated.
|
||||
func (c *Canvas) SetBounds(bounds pixel.Rect) {
|
||||
c.bounds = bounds
|
||||
|
||||
// if this bounds fit into the original bounds, no need to reallocate
|
||||
if c.orig.borders.Contains(bounds.Min) && c.orig.borders.Contains(bounds.Max) {
|
||||
return
|
||||
c.gf.SetBounds(bounds)
|
||||
if c.sprite == nil {
|
||||
c.sprite = pixel.NewSprite(nil, pixel.Rect{})
|
||||
}
|
||||
|
||||
mainthread.Call(func() {
|
||||
oldF := c.orig.f
|
||||
|
||||
_, _, w, h := intBounds(bounds)
|
||||
c.f = glhf.NewFrame(w, h, c.smooth)
|
||||
|
||||
// preserve old content
|
||||
if oldF != nil {
|
||||
relBounds := bounds
|
||||
relBounds = relBounds.Moved(-c.orig.borders.Min)
|
||||
ox, oy, ow, oh := intBounds(relBounds)
|
||||
oldF.Blit(
|
||||
c.f,
|
||||
ox, oy, ox+ow, oy+oh,
|
||||
ox, oy, ox+ow, oy+oh,
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
// detach from orig
|
||||
c.borders = bounds
|
||||
c.pixels = nil
|
||||
c.dirty = true
|
||||
c.orig = c
|
||||
c.sprite.Set(c, c.Bounds())
|
||||
//c.sprite.SetMatrix(pixel.IM.Moved(c.Bounds().Center()))
|
||||
}
|
||||
|
||||
// Bounds returns the rectangular bounds of the Canvas.
|
||||
func (c *Canvas) Bounds() pixel.Rect {
|
||||
return c.bounds
|
||||
return c.gf.Bounds()
|
||||
}
|
||||
|
||||
// SetSmooth sets whether stretched Pictures drawn onto this Canvas should be drawn smooth or
|
||||
|
@ -228,20 +150,48 @@ func (c *Canvas) Smooth() bool {
|
|||
|
||||
// must be manually called inside mainthread
|
||||
func (c *Canvas) setGlhfBounds() {
|
||||
bounds := c.bounds
|
||||
bounds.Moved(c.orig.borders.Min)
|
||||
bx, by, bw, bh := intBounds(bounds)
|
||||
glhf.Bounds(bx, by, bw, bh)
|
||||
_, _, bw, bh := intBounds(c.gf.Bounds())
|
||||
glhf.Bounds(0, 0, bw, bh)
|
||||
}
|
||||
|
||||
// must be manually called inside mainthread
|
||||
func setBlendFunc(cmp pixel.ComposeMethod) {
|
||||
switch cmp {
|
||||
case pixel.ComposeOver:
|
||||
glhf.BlendFunc(glhf.One, glhf.OneMinusSrcAlpha)
|
||||
case pixel.ComposeIn:
|
||||
glhf.BlendFunc(glhf.DstAlpha, glhf.Zero)
|
||||
case pixel.ComposeOut:
|
||||
glhf.BlendFunc(glhf.OneMinusDstAlpha, glhf.Zero)
|
||||
case pixel.ComposeAtop:
|
||||
glhf.BlendFunc(glhf.DstAlpha, glhf.OneMinusSrcAlpha)
|
||||
case pixel.ComposeRover:
|
||||
glhf.BlendFunc(glhf.OneMinusDstAlpha, glhf.One)
|
||||
case pixel.ComposeRin:
|
||||
glhf.BlendFunc(glhf.Zero, glhf.SrcAlpha)
|
||||
case pixel.ComposeRout:
|
||||
glhf.BlendFunc(glhf.Zero, glhf.OneMinusSrcAlpha)
|
||||
case pixel.ComposeRatop:
|
||||
glhf.BlendFunc(glhf.OneMinusDstAlpha, glhf.SrcAlpha)
|
||||
case pixel.ComposeXor:
|
||||
glhf.BlendFunc(glhf.OneMinusDstAlpha, glhf.OneMinusSrcAlpha)
|
||||
case pixel.ComposePlus:
|
||||
glhf.BlendFunc(glhf.One, glhf.One)
|
||||
case pixel.ComposeCopy:
|
||||
glhf.BlendFunc(glhf.One, glhf.Zero)
|
||||
default:
|
||||
panic(errors.New("Canvas: invalid compose method"))
|
||||
}
|
||||
}
|
||||
|
||||
// Clear fills the whole Canvas with a single color.
|
||||
func (c *Canvas) Clear(color color.Color) {
|
||||
c.orig.dirty = true
|
||||
c.gf.Dirty()
|
||||
|
||||
nrgba := pixel.ToNRGBA(color)
|
||||
rgba := pixel.ToRGBA(color)
|
||||
|
||||
// color masking
|
||||
nrgba = nrgba.Mul(pixel.NRGBA{
|
||||
rgba = rgba.Mul(pixel.RGBA{
|
||||
R: float64(c.col[0]),
|
||||
G: float64(c.col[1]),
|
||||
B: float64(c.col[2]),
|
||||
|
@ -250,87 +200,108 @@ func (c *Canvas) Clear(color color.Color) {
|
|||
|
||||
mainthread.CallNonBlock(func() {
|
||||
c.setGlhfBounds()
|
||||
c.orig.f.Begin()
|
||||
c.gf.Frame().Begin()
|
||||
glhf.Clear(
|
||||
float32(nrgba.R),
|
||||
float32(nrgba.G),
|
||||
float32(nrgba.B),
|
||||
float32(nrgba.A),
|
||||
float32(rgba.R),
|
||||
float32(rgba.G),
|
||||
float32(rgba.B),
|
||||
float32(rgba.A),
|
||||
)
|
||||
c.orig.f.End()
|
||||
c.gf.Frame().End()
|
||||
})
|
||||
}
|
||||
|
||||
// Slice returns a sub-Canvas with the specified Bounds.
|
||||
//
|
||||
// The type of the returned value is *Canvas, the type of the return value is a general
|
||||
// pixel.Picture just so that Canvas implements pixel.Picture interface.
|
||||
func (c *Canvas) Slice(bounds pixel.Rect) pixel.Picture {
|
||||
sc := new(Canvas)
|
||||
*sc = *c
|
||||
sc.bounds = bounds
|
||||
return sc
|
||||
}
|
||||
|
||||
// Original returns the most original Canvas that this Canvas was created from using Slice-ing.
|
||||
//
|
||||
// The type of the returned value is *Canvas, the type of the return value is a general
|
||||
// pixel.Picture just so that Canvas implements pixel.Picture interface.
|
||||
func (c *Canvas) Original() pixel.Picture {
|
||||
return c.orig
|
||||
}
|
||||
|
||||
// Color returns the color of the pixel over the given position inside the Canvas.
|
||||
func (c *Canvas) Color(at pixel.Vec) pixel.NRGBA {
|
||||
if c.orig.dirty {
|
||||
mainthread.Call(func() {
|
||||
tex := c.orig.f.Texture()
|
||||
tex.Begin()
|
||||
c.orig.pixels = tex.Pixels(0, 0, tex.Width(), tex.Height())
|
||||
tex.End()
|
||||
})
|
||||
c.orig.dirty = false
|
||||
}
|
||||
if !c.bounds.Contains(at) {
|
||||
return pixel.NRGBA{}
|
||||
}
|
||||
bx, by, bw, _ := intBounds(c.orig.borders)
|
||||
x, y := int(at.X())-bx, int(at.Y())-by
|
||||
off := y*bw + x
|
||||
return pixel.NRGBA{
|
||||
R: float64(c.orig.pixels[off*4+0]) / 255,
|
||||
G: float64(c.orig.pixels[off*4+1]) / 255,
|
||||
B: float64(c.orig.pixels[off*4+2]) / 255,
|
||||
A: float64(c.orig.pixels[off*4+3]) / 255,
|
||||
}
|
||||
func (c *Canvas) Color(at pixel.Vec) pixel.RGBA {
|
||||
return c.gf.Color(at)
|
||||
}
|
||||
|
||||
// Texture returns the underlying OpenGL Texture of this Canvas.
|
||||
//
|
||||
// Implements GLPicture interface.
|
||||
func (c *Canvas) Texture() *glhf.Texture {
|
||||
return c.gf.Texture()
|
||||
}
|
||||
|
||||
// Frame returns the underlying OpenGL Frame of this Canvas.
|
||||
func (c *Canvas) Frame() *glhf.Frame {
|
||||
return c.gf.frame
|
||||
}
|
||||
|
||||
// SetPixels replaces the content of the Canvas with the provided pixels. The provided slice must be
|
||||
// an alpha-premultiplied RGBA sequence of correct length (4 * width * height).
|
||||
func (c *Canvas) SetPixels(pixels []uint8) {
|
||||
c.gf.Dirty()
|
||||
|
||||
mainthread.Call(func() {
|
||||
tex := c.Texture()
|
||||
tex.Begin()
|
||||
tex.SetPixels(0, 0, tex.Width(), tex.Height(), pixels)
|
||||
tex.End()
|
||||
})
|
||||
}
|
||||
|
||||
// Pixels returns an alpha-premultiplied RGBA sequence of the content of the Canvas.
|
||||
func (c *Canvas) Pixels() []uint8 {
|
||||
var pixels []uint8
|
||||
|
||||
mainthread.Call(func() {
|
||||
tex := c.Texture()
|
||||
tex.Begin()
|
||||
pixels = tex.Pixels(0, 0, tex.Width(), tex.Height())
|
||||
tex.End()
|
||||
})
|
||||
|
||||
return pixels
|
||||
}
|
||||
|
||||
// Draw draws the content of the Canvas onto another Target, transformed by the given Matrix, just
|
||||
// like if it was a Sprite containing the whole Canvas.
|
||||
func (c *Canvas) Draw(t pixel.Target, matrix pixel.Matrix) {
|
||||
c.sprite.Draw(t, matrix)
|
||||
}
|
||||
|
||||
// DrawColorMask draws the content of the Canvas onto another Target, transformed by the given
|
||||
// Matrix and multiplied by the given mask, just like if it was a Sprite containing the whole Canvas.
|
||||
//
|
||||
// If the color mask is nil, a fully opaque white mask will be used causing no effect.
|
||||
func (c *Canvas) DrawColorMask(t pixel.Target, matrix pixel.Matrix, mask color.Color) {
|
||||
c.sprite.DrawColorMask(t, matrix, mask)
|
||||
}
|
||||
|
||||
type canvasTriangles struct {
|
||||
*GLTriangles
|
||||
|
||||
dst *Canvas
|
||||
}
|
||||
|
||||
func (ct *canvasTriangles) draw(tex *glhf.Texture, borders, bounds pixel.Rect) {
|
||||
ct.dst.orig.dirty = true
|
||||
func (ct *canvasTriangles) draw(tex *glhf.Texture, bounds pixel.Rect) {
|
||||
ct.dst.gf.Dirty()
|
||||
|
||||
// save the current state vars to avoid race condition
|
||||
cmp := ct.dst.cmp
|
||||
mat := ct.dst.mat
|
||||
col := ct.dst.col
|
||||
smt := ct.dst.smooth
|
||||
|
||||
mainthread.CallNonBlock(func() {
|
||||
ct.dst.setGlhfBounds()
|
||||
ct.dst.orig.f.Begin()
|
||||
ct.dst.s.Begin()
|
||||
setBlendFunc(cmp)
|
||||
|
||||
ct.dst.s.SetUniformAttr(canvasBounds, mgl32.Vec4{
|
||||
float32(ct.dst.bounds.Min.X()),
|
||||
float32(ct.dst.bounds.Min.Y()),
|
||||
float32(ct.dst.bounds.W()),
|
||||
float32(ct.dst.bounds.H()),
|
||||
frame := ct.dst.gf.Frame()
|
||||
shader := ct.dst.shader
|
||||
|
||||
frame.Begin()
|
||||
shader.Begin()
|
||||
|
||||
dstBounds := ct.dst.Bounds()
|
||||
shader.SetUniformAttr(canvasBounds, mgl32.Vec4{
|
||||
float32(dstBounds.Min.X),
|
||||
float32(dstBounds.Min.Y),
|
||||
float32(dstBounds.W()),
|
||||
float32(dstBounds.H()),
|
||||
})
|
||||
ct.dst.s.SetUniformAttr(canvasTransform, mat)
|
||||
ct.dst.s.SetUniformAttr(canvasColorMask, col)
|
||||
shader.SetUniformAttr(canvasTransform, mat)
|
||||
shader.SetUniformAttr(canvasColorMask, col)
|
||||
|
||||
if tex == nil {
|
||||
ct.vs.Begin()
|
||||
|
@ -339,21 +310,16 @@ func (ct *canvasTriangles) draw(tex *glhf.Texture, borders, bounds pixel.Rect) {
|
|||
} else {
|
||||
tex.Begin()
|
||||
|
||||
ct.dst.s.SetUniformAttr(canvasTexBorders, mgl32.Vec4{
|
||||
float32(borders.Min.X()),
|
||||
float32(borders.Min.Y()),
|
||||
float32(borders.W()),
|
||||
float32(borders.H()),
|
||||
})
|
||||
ct.dst.s.SetUniformAttr(canvasTexBounds, mgl32.Vec4{
|
||||
float32(bounds.Min.X()),
|
||||
float32(bounds.Min.Y()),
|
||||
float32(bounds.W()),
|
||||
float32(bounds.H()),
|
||||
bx, by, bw, bh := intBounds(bounds)
|
||||
shader.SetUniformAttr(canvasTexBounds, mgl32.Vec4{
|
||||
float32(bx),
|
||||
float32(by),
|
||||
float32(bw),
|
||||
float32(bh),
|
||||
})
|
||||
|
||||
if tex.Smooth() != ct.dst.smooth {
|
||||
tex.SetSmooth(ct.dst.smooth)
|
||||
if tex.Smooth() != smt {
|
||||
tex.SetSmooth(smt)
|
||||
}
|
||||
|
||||
ct.vs.Begin()
|
||||
|
@ -363,53 +329,18 @@ func (ct *canvasTriangles) draw(tex *glhf.Texture, borders, bounds pixel.Rect) {
|
|||
tex.End()
|
||||
}
|
||||
|
||||
ct.dst.s.End()
|
||||
ct.dst.orig.f.End()
|
||||
shader.End()
|
||||
frame.End()
|
||||
})
|
||||
}
|
||||
|
||||
func (ct *canvasTriangles) Draw() {
|
||||
ct.draw(nil, pixel.Rect{}, pixel.Rect{})
|
||||
ct.draw(nil, pixel.Rect{})
|
||||
}
|
||||
|
||||
type canvasPicture struct {
|
||||
tex *glhf.Texture
|
||||
pixels []uint8
|
||||
borders pixel.Rect
|
||||
bounds pixel.Rect
|
||||
|
||||
orig *canvasPicture
|
||||
dst *Canvas
|
||||
}
|
||||
|
||||
func (cp *canvasPicture) Bounds() pixel.Rect {
|
||||
return cp.bounds
|
||||
}
|
||||
|
||||
func (cp *canvasPicture) Slice(r pixel.Rect) pixel.Picture {
|
||||
sp := new(canvasPicture)
|
||||
*sp = *cp
|
||||
sp.bounds = r
|
||||
return sp
|
||||
}
|
||||
|
||||
func (cp *canvasPicture) Original() pixel.Picture {
|
||||
return cp.orig
|
||||
}
|
||||
|
||||
func (cp *canvasPicture) Color(at pixel.Vec) pixel.NRGBA {
|
||||
if !cp.bounds.Contains(at) {
|
||||
return pixel.NRGBA{}
|
||||
}
|
||||
bx, by, bw, _ := intBounds(cp.borders)
|
||||
x, y := int(at.X())-bx, int(at.Y())-by
|
||||
off := y*bw + x
|
||||
return pixel.NRGBA{
|
||||
R: float64(cp.pixels[off*4+0]) / 255,
|
||||
G: float64(cp.pixels[off*4+1]) / 255,
|
||||
B: float64(cp.pixels[off*4+2]) / 255,
|
||||
A: float64(cp.pixels[off*4+3]) / 255,
|
||||
}
|
||||
GLPicture
|
||||
dst *Canvas
|
||||
}
|
||||
|
||||
func (cp *canvasPicture) Draw(t pixel.TargetTriangles) {
|
||||
|
@ -417,56 +348,20 @@ func (cp *canvasPicture) Draw(t pixel.TargetTriangles) {
|
|||
if cp.dst != ct.dst {
|
||||
panic(fmt.Errorf("(%T).Draw: TargetTriangles generated by different Canvas", cp))
|
||||
}
|
||||
ct.draw(cp.tex, cp.borders, cp.bounds)
|
||||
}
|
||||
|
||||
type canvasCanvasPicture struct {
|
||||
src, dst *Canvas
|
||||
bounds pixel.Rect
|
||||
orig *canvasCanvasPicture
|
||||
}
|
||||
|
||||
func (ccp *canvasCanvasPicture) Bounds() pixel.Rect {
|
||||
return ccp.bounds
|
||||
}
|
||||
|
||||
func (ccp *canvasCanvasPicture) Slice(r pixel.Rect) pixel.Picture {
|
||||
sp := new(canvasCanvasPicture)
|
||||
*sp = *ccp
|
||||
sp.bounds = r
|
||||
return sp
|
||||
}
|
||||
|
||||
func (ccp *canvasCanvasPicture) Original() pixel.Picture {
|
||||
return ccp.orig
|
||||
}
|
||||
|
||||
func (ccp *canvasCanvasPicture) Color(at pixel.Vec) pixel.NRGBA {
|
||||
if !ccp.bounds.Contains(at) {
|
||||
return pixel.NRGBA{}
|
||||
}
|
||||
return ccp.src.Color(at)
|
||||
}
|
||||
|
||||
func (ccp *canvasCanvasPicture) Draw(t pixel.TargetTriangles) {
|
||||
ct := t.(*canvasTriangles)
|
||||
if ccp.dst != ct.dst {
|
||||
panic(fmt.Errorf("(%T).Draw: TargetTriangles generated by different Canvas", ccp))
|
||||
}
|
||||
ct.draw(ccp.src.orig.f.Texture(), ccp.src.orig.borders, ccp.bounds)
|
||||
ct.draw(cp.GLPicture.Texture(), cp.GLPicture.Bounds())
|
||||
}
|
||||
|
||||
const (
|
||||
canvasPosition int = iota
|
||||
canvasColor
|
||||
canvasTexture
|
||||
canvasTexCoords
|
||||
canvasIntensity
|
||||
)
|
||||
|
||||
var canvasVertexFormat = glhf.AttrFormat{
|
||||
canvasPosition: {Name: "position", Type: glhf.Vec2},
|
||||
canvasColor: {Name: "color", Type: glhf.Vec4},
|
||||
canvasTexture: {Name: "texture", Type: glhf.Vec2},
|
||||
canvasTexCoords: {Name: "texCoords", Type: glhf.Vec2},
|
||||
canvasIntensity: {Name: "intensity", Type: glhf.Float},
|
||||
}
|
||||
|
||||
|
@ -474,16 +369,14 @@ const (
|
|||
canvasTransform int = iota
|
||||
canvasColorMask
|
||||
canvasBounds
|
||||
canvasTexBorders
|
||||
canvasTexBounds
|
||||
)
|
||||
|
||||
var canvasUniformFormat = glhf.AttrFormat{
|
||||
canvasTransform: {Name: "transform", Type: glhf.Mat3},
|
||||
canvasColorMask: {Name: "colorMask", Type: glhf.Vec4},
|
||||
canvasBounds: {Name: "bounds", Type: glhf.Vec4},
|
||||
canvasTexBorders: {Name: "texBorders", Type: glhf.Vec4},
|
||||
canvasTexBounds: {Name: "texBounds", Type: glhf.Vec4},
|
||||
canvasTransform: {Name: "transform", Type: glhf.Mat3},
|
||||
canvasColorMask: {Name: "colorMask", Type: glhf.Vec4},
|
||||
canvasBounds: {Name: "bounds", Type: glhf.Vec4},
|
||||
canvasTexBounds: {Name: "texBounds", Type: glhf.Vec4},
|
||||
}
|
||||
|
||||
var canvasVertexShader = `
|
||||
|
@ -491,23 +384,22 @@ var canvasVertexShader = `
|
|||
|
||||
in vec2 position;
|
||||
in vec4 color;
|
||||
in vec2 texture;
|
||||
in vec2 texCoords;
|
||||
in float intensity;
|
||||
|
||||
out vec4 Color;
|
||||
out vec2 Texture;
|
||||
out vec2 TexCoords;
|
||||
out float Intensity;
|
||||
|
||||
uniform mat3 transform;
|
||||
uniform vec4 borders;
|
||||
uniform vec4 bounds;
|
||||
|
||||
void main() {
|
||||
vec2 transPos = (transform * vec3(position, 1.0)).xy;
|
||||
vec2 normPos = (transPos - bounds.xy) / (bounds.zw) * 2 - vec2(1, 1);
|
||||
vec2 normPos = (transPos - bounds.xy) / bounds.zw * 2 - vec2(1, 1);
|
||||
gl_Position = vec4(normPos, 0.0, 1.0);
|
||||
Color = color;
|
||||
Texture = texture;
|
||||
TexCoords = texCoords;
|
||||
Intensity = intensity;
|
||||
}
|
||||
`
|
||||
|
@ -516,13 +408,12 @@ var canvasFragmentShader = `
|
|||
#version 330 core
|
||||
|
||||
in vec4 Color;
|
||||
in vec2 Texture;
|
||||
in vec2 TexCoords;
|
||||
in float Intensity;
|
||||
|
||||
out vec4 color;
|
||||
|
||||
uniform vec4 colorMask;
|
||||
uniform vec4 texBorders;
|
||||
uniform vec4 texBounds;
|
||||
uniform sampler2D tex;
|
||||
|
||||
|
@ -531,16 +422,10 @@ void main() {
|
|||
color = colorMask * Color;
|
||||
} else {
|
||||
color = vec4(0, 0, 0, 0);
|
||||
color += (1 - Intensity) * colorMask * Color;
|
||||
|
||||
float bx = texBounds.x;
|
||||
float by = texBounds.y;
|
||||
float bw = texBounds.z;
|
||||
float bh = texBounds.w;
|
||||
if (bx <= Texture.x && Texture.x <= bx + bw && by <= Texture.y && Texture.y <= by + bh) {
|
||||
vec2 t = (Texture - texBorders.xy) / texBorders.zw;
|
||||
color += Intensity * colorMask * Color * texture(tex, t);
|
||||
}
|
||||
color += (1 - Intensity) * Color;
|
||||
vec2 t = (TexCoords - texBounds.xy) / texBounds.zw;
|
||||
color += Intensity * Color * texture(tex, t);
|
||||
color *= colorMask;
|
||||
}
|
||||
}
|
||||
`
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
// Package pixelgl implements efficient OpenGL targets and utilities for the Pixel game development
|
||||
// library, specifically Window and Canvas.
|
||||
//
|
||||
// It also contains a few additional utilities to help extend Pixel with OpenGL graphical effects.
|
||||
package pixelgl
|
|
@ -0,0 +1,105 @@
|
|||
package pixelgl
|
||||
|
||||
import (
|
||||
"github.com/faiface/glhf"
|
||||
"github.com/faiface/mainthread"
|
||||
"github.com/faiface/pixel"
|
||||
)
|
||||
|
||||
// GLFrame is a type that helps implementing OpenGL Targets. It implements most common methods to
|
||||
// avoid code redundancy. It contains an glhf.Frame that you can draw on.
|
||||
type GLFrame struct {
|
||||
frame *glhf.Frame
|
||||
bounds pixel.Rect
|
||||
pixels []uint8
|
||||
dirty bool
|
||||
}
|
||||
|
||||
// NewGLFrame creates a new GLFrame with the given bounds.
|
||||
func NewGLFrame(bounds pixel.Rect) *GLFrame {
|
||||
gf := new(GLFrame)
|
||||
gf.SetBounds(bounds)
|
||||
return gf
|
||||
}
|
||||
|
||||
// SetBounds resizes the GLFrame to the new bounds.
|
||||
func (gf *GLFrame) SetBounds(bounds pixel.Rect) {
|
||||
if bounds == gf.Bounds() {
|
||||
return
|
||||
}
|
||||
|
||||
mainthread.Call(func() {
|
||||
oldF := gf.frame
|
||||
|
||||
_, _, w, h := intBounds(bounds)
|
||||
if w <= 0 {
|
||||
w = 1
|
||||
}
|
||||
if h <= 0 {
|
||||
h = 1
|
||||
}
|
||||
gf.frame = glhf.NewFrame(w, h, false)
|
||||
|
||||
// preserve old content
|
||||
if oldF != nil {
|
||||
ox, oy, ow, oh := intBounds(bounds)
|
||||
oldF.Blit(
|
||||
gf.frame,
|
||||
ox, oy, ox+ow, oy+oh,
|
||||
ox, oy, ox+ow, oy+oh,
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
gf.bounds = bounds
|
||||
gf.pixels = nil
|
||||
gf.dirty = true
|
||||
}
|
||||
|
||||
// Bounds returns the current GLFrame's bounds.
|
||||
func (gf *GLFrame) Bounds() pixel.Rect {
|
||||
return gf.bounds
|
||||
}
|
||||
|
||||
// Color returns the color of the pixel under the specified position.
|
||||
func (gf *GLFrame) Color(at pixel.Vec) pixel.RGBA {
|
||||
if gf.dirty {
|
||||
mainthread.Call(func() {
|
||||
tex := gf.frame.Texture()
|
||||
tex.Begin()
|
||||
gf.pixels = tex.Pixels(0, 0, tex.Width(), tex.Height())
|
||||
tex.End()
|
||||
})
|
||||
gf.dirty = false
|
||||
}
|
||||
if !gf.bounds.Contains(at) {
|
||||
return pixel.Alpha(0)
|
||||
}
|
||||
bx, by, bw, _ := intBounds(gf.bounds)
|
||||
x, y := int(at.X)-bx, int(at.Y)-by
|
||||
off := y*bw + x
|
||||
return pixel.RGBA{
|
||||
R: float64(gf.pixels[off*4+0]) / 255,
|
||||
G: float64(gf.pixels[off*4+1]) / 255,
|
||||
B: float64(gf.pixels[off*4+2]) / 255,
|
||||
A: float64(gf.pixels[off*4+3]) / 255,
|
||||
}
|
||||
}
|
||||
|
||||
// Frame returns the GLFrame's Frame that you can draw on.
|
||||
func (gf *GLFrame) Frame() *glhf.Frame {
|
||||
return gf.frame
|
||||
}
|
||||
|
||||
// Texture returns the underlying Texture of the GLFrame's Frame.
|
||||
//
|
||||
// Implements GLPicture interface.
|
||||
func (gf *GLFrame) Texture() *glhf.Texture {
|
||||
return gf.frame.Texture()
|
||||
}
|
||||
|
||||
// Dirty marks the GLFrame as changed. Always call this method when you draw onto the GLFrame's
|
||||
// Frame.
|
||||
func (gf *GLFrame) Dirty() {
|
||||
gf.dirty = true
|
||||
}
|
|
@ -0,0 +1,98 @@
|
|||
package pixelgl
|
||||
|
||||
import (
|
||||
"math"
|
||||
|
||||
"github.com/faiface/glhf"
|
||||
"github.com/faiface/mainthread"
|
||||
"github.com/faiface/pixel"
|
||||
)
|
||||
|
||||
// GLPicture is a pixel.PictureColor with a Texture. All OpenGL Targets should implement and accept
|
||||
// this interface, because it enables seamless drawing of one to another.
|
||||
//
|
||||
// Implementing this interface on an OpenGL Target enables other OpenGL Targets to efficiently draw
|
||||
// that Target onto them.
|
||||
type GLPicture interface {
|
||||
pixel.PictureColor
|
||||
Texture() *glhf.Texture
|
||||
}
|
||||
|
||||
// NewGLPicture creates a new GLPicture with it's own static OpenGL texture. This function always
|
||||
// allocates a new texture that cannot (shouldn't) be further modified.
|
||||
func NewGLPicture(p pixel.Picture) GLPicture {
|
||||
bounds := p.Bounds()
|
||||
bx, by, bw, bh := intBounds(bounds)
|
||||
|
||||
pixels := make([]uint8, 4*bw*bh)
|
||||
|
||||
if pd, ok := p.(*pixel.PictureData); ok {
|
||||
// PictureData short path
|
||||
for y := 0; y < bh; y++ {
|
||||
for x := 0; x < bw; x++ {
|
||||
rgba := pd.Pix[y*pd.Stride+x]
|
||||
off := (y*bw + x) * 4
|
||||
pixels[off+0] = rgba.R
|
||||
pixels[off+1] = rgba.G
|
||||
pixels[off+2] = rgba.B
|
||||
pixels[off+3] = rgba.A
|
||||
}
|
||||
}
|
||||
} else if p, ok := p.(pixel.PictureColor); ok {
|
||||
for y := 0; y < bh; y++ {
|
||||
for x := 0; x < bw; x++ {
|
||||
at := pixel.V(
|
||||
math.Max(float64(bx+x), bounds.Min.X),
|
||||
math.Max(float64(by+y), bounds.Min.Y),
|
||||
)
|
||||
color := p.Color(at)
|
||||
off := (y*bw + x) * 4
|
||||
pixels[off+0] = uint8(color.R * 255)
|
||||
pixels[off+1] = uint8(color.G * 255)
|
||||
pixels[off+2] = uint8(color.B * 255)
|
||||
pixels[off+3] = uint8(color.A * 255)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var tex *glhf.Texture
|
||||
mainthread.Call(func() {
|
||||
tex = glhf.NewTexture(bw, bh, false, pixels)
|
||||
})
|
||||
|
||||
gp := &glPicture{
|
||||
bounds: bounds,
|
||||
tex: tex,
|
||||
pixels: pixels,
|
||||
}
|
||||
return gp
|
||||
}
|
||||
|
||||
type glPicture struct {
|
||||
bounds pixel.Rect
|
||||
tex *glhf.Texture
|
||||
pixels []uint8
|
||||
}
|
||||
|
||||
func (gp *glPicture) Bounds() pixel.Rect {
|
||||
return gp.bounds
|
||||
}
|
||||
|
||||
func (gp *glPicture) Texture() *glhf.Texture {
|
||||
return gp.tex
|
||||
}
|
||||
|
||||
func (gp *glPicture) Color(at pixel.Vec) pixel.RGBA {
|
||||
if !gp.bounds.Contains(at) {
|
||||
return pixel.Alpha(0)
|
||||
}
|
||||
bx, by, bw, _ := intBounds(gp.bounds)
|
||||
x, y := int(at.X)-bx, int(at.Y)-by
|
||||
off := y*bw + x
|
||||
return pixel.RGBA{
|
||||
R: float64(gp.pixels[off*4+0]) / 255,
|
||||
G: float64(gp.pixels[off*4+1]) / 255,
|
||||
B: float64(gp.pixels[off*4+2]) / 255,
|
||||
A: float64(gp.pixels[off*4+3]) / 255,
|
||||
}
|
||||
}
|
|
@ -60,9 +60,10 @@ func (gt *GLTriangles) Len() int {
|
|||
// SetLen efficiently resizes GLTriangles to len.
|
||||
//
|
||||
// Time complexity is amortized O(1).
|
||||
func (gt *GLTriangles) SetLen(len int) {
|
||||
if len > gt.Len() {
|
||||
needAppend := len - gt.Len()
|
||||
func (gt *GLTriangles) SetLen(length int) {
|
||||
switch {
|
||||
case length > gt.Len():
|
||||
needAppend := length - gt.Len()
|
||||
for i := 0; i < needAppend; i++ {
|
||||
gt.data = append(gt.data,
|
||||
0, 0,
|
||||
|
@ -71,10 +72,16 @@ func (gt *GLTriangles) SetLen(len int) {
|
|||
0,
|
||||
)
|
||||
}
|
||||
case length < gt.Len():
|
||||
gt.data = gt.data[:length*gt.vs.Stride()]
|
||||
default:
|
||||
return
|
||||
}
|
||||
if len < gt.Len() {
|
||||
gt.data = gt.data[:len*gt.vs.Stride()]
|
||||
}
|
||||
mainthread.CallNonBlock(func() {
|
||||
gt.vs.Begin()
|
||||
gt.vs.SetLen(length)
|
||||
gt.vs.End()
|
||||
})
|
||||
}
|
||||
|
||||
// Slice returns a sub-Triangles of this GLTriangles in range [i, j).
|
||||
|
@ -94,64 +101,56 @@ func (gt *GLTriangles) updateData(t pixel.Triangles) {
|
|||
}
|
||||
|
||||
// TrianglesData short path
|
||||
stride := gt.vs.Stride()
|
||||
length := gt.Len()
|
||||
if t, ok := t.(*pixel.TrianglesData); ok {
|
||||
for i := 0; i < gt.Len(); i++ {
|
||||
for i := 0; i < length; i++ {
|
||||
var (
|
||||
px, py = (*t)[i].Position.XY()
|
||||
col = (*t)[i].Color
|
||||
tx, ty = (*t)[i].Picture.XY()
|
||||
in = (*t)[i].Intensity
|
||||
)
|
||||
gt.data[i*gt.vs.Stride()+0] = float32(px)
|
||||
gt.data[i*gt.vs.Stride()+1] = float32(py)
|
||||
gt.data[i*gt.vs.Stride()+2] = float32(col.R)
|
||||
gt.data[i*gt.vs.Stride()+3] = float32(col.G)
|
||||
gt.data[i*gt.vs.Stride()+4] = float32(col.B)
|
||||
gt.data[i*gt.vs.Stride()+5] = float32(col.A)
|
||||
gt.data[i*gt.vs.Stride()+6] = float32(tx)
|
||||
gt.data[i*gt.vs.Stride()+7] = float32(ty)
|
||||
gt.data[i*gt.vs.Stride()+8] = float32(in)
|
||||
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)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if t, ok := t.(pixel.TrianglesPosition); ok {
|
||||
for i := 0; i < gt.Len(); i++ {
|
||||
for i := 0; i < length; i++ {
|
||||
px, py := t.Position(i).XY()
|
||||
gt.data[i*gt.vs.Stride()+0] = float32(px)
|
||||
gt.data[i*gt.vs.Stride()+1] = float32(py)
|
||||
gt.data[i*stride+0] = float32(px)
|
||||
gt.data[i*stride+1] = float32(py)
|
||||
}
|
||||
}
|
||||
if t, ok := t.(pixel.TrianglesColor); ok {
|
||||
for i := 0; i < gt.Len(); i++ {
|
||||
for i := 0; i < length; i++ {
|
||||
col := t.Color(i)
|
||||
gt.data[i*gt.vs.Stride()+2] = float32(col.R)
|
||||
gt.data[i*gt.vs.Stride()+3] = float32(col.G)
|
||||
gt.data[i*gt.vs.Stride()+4] = float32(col.B)
|
||||
gt.data[i*gt.vs.Stride()+5] = float32(col.A)
|
||||
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)
|
||||
}
|
||||
}
|
||||
if t, ok := t.(pixel.TrianglesPicture); ok {
|
||||
for i := 0; i < gt.Len(); i++ {
|
||||
for i := 0; i < length; i++ {
|
||||
pic, intensity := t.Picture(i)
|
||||
gt.data[i*gt.vs.Stride()+6] = float32(pic.X())
|
||||
gt.data[i*gt.vs.Stride()+7] = float32(pic.Y())
|
||||
gt.data[i*gt.vs.Stride()+8] = float32(intensity)
|
||||
gt.data[i*stride+6] = float32(pic.X)
|
||||
gt.data[i*stride+7] = float32(pic.Y)
|
||||
gt.data[i*stride+8] = float32(intensity)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (gt *GLTriangles) submitData() {
|
||||
data := append([]float32{}, gt.data...) // avoid race condition
|
||||
mainthread.CallNonBlock(func() {
|
||||
gt.vs.Begin()
|
||||
dataLen := len(data) / gt.vs.Stride()
|
||||
gt.vs.SetLen(dataLen)
|
||||
gt.vs.SetVertexData(gt.data)
|
||||
gt.vs.End()
|
||||
})
|
||||
}
|
||||
|
||||
// Update copies vertex properties from the supplied Triangles into this GLTriangles.
|
||||
//
|
||||
// The two Triangles (gt and t) must be of the same len.
|
||||
|
@ -160,7 +159,23 @@ func (gt *GLTriangles) Update(t pixel.Triangles) {
|
|||
panic(fmt.Errorf("(%T).Update: invalid triangles len", gt))
|
||||
}
|
||||
gt.updateData(t)
|
||||
gt.submitData()
|
||||
|
||||
// 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
|
||||
data := append([]float32{}, gt.data...)
|
||||
mainthread.CallNonBlock(func() {
|
||||
gt.vs.Begin()
|
||||
gt.vs.SetVertexData(data)
|
||||
gt.vs.End()
|
||||
})
|
||||
} else {
|
||||
mainthread.Call(func() {
|
||||
gt.vs.Begin()
|
||||
gt.vs.SetVertexData(gt.data)
|
||||
gt.vs.End()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Copy returns an independent copy of this GLTriangles.
|
||||
|
@ -178,12 +193,12 @@ func (gt *GLTriangles) Position(i int) pixel.Vec {
|
|||
}
|
||||
|
||||
// Color returns the Color property of the i-th vertex.
|
||||
func (gt *GLTriangles) Color(i int) pixel.NRGBA {
|
||||
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]
|
||||
return pixel.NRGBA{
|
||||
return pixel.RGBA{
|
||||
R: float64(r),
|
||||
G: float64(g),
|
||||
B: float64(b),
|
||||
|
|
|
@ -21,6 +21,13 @@ func (w *Window) JustReleased(button Button) bool {
|
|||
return !w.currInp.buttons[button] && w.prevInp.buttons[button]
|
||||
}
|
||||
|
||||
// Repeated returns whether a repeat event has been triggered on button.
|
||||
//
|
||||
// Repeat event occurs repeatedly when a button is held down for some time.
|
||||
func (w *Window) Repeated(button Button) bool {
|
||||
return w.currInp.repeat[button]
|
||||
}
|
||||
|
||||
// MousePosition returns the current mouse position in the Window's Bounds.
|
||||
func (w *Window) MousePosition() pixel.Vec {
|
||||
return w.currInp.mouse
|
||||
|
@ -31,6 +38,11 @@ func (w *Window) MouseScroll() pixel.Vec {
|
|||
return w.currInp.scroll
|
||||
}
|
||||
|
||||
// Typed returns the text typed on the keyboard since the last call to Window.Update.
|
||||
func (w *Window) Typed() string {
|
||||
return w.currInp.typed
|
||||
}
|
||||
|
||||
// Button is a keyboard or mouse button. Why distinguish?
|
||||
type Button int
|
||||
|
||||
|
@ -322,9 +334,9 @@ 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.currInp.buttons[Button(button)] = true
|
||||
w.tempInp.buttons[Button(button)] = true
|
||||
case glfw.Release:
|
||||
w.currInp.buttons[Button(button)] = false
|
||||
w.tempInp.buttons[Button(button)] = false
|
||||
}
|
||||
})
|
||||
|
||||
|
@ -334,38 +346,43 @@ func (w *Window) initInput() {
|
|||
}
|
||||
switch action {
|
||||
case glfw.Press:
|
||||
w.currInp.buttons[Button(key)] = true
|
||||
w.tempInp.buttons[Button(key)] = true
|
||||
case glfw.Release:
|
||||
w.currInp.buttons[Button(key)] = false
|
||||
w.tempInp.buttons[Button(key)] = false
|
||||
case glfw.Repeat:
|
||||
w.tempInp.repeat[Button(key)] = true
|
||||
}
|
||||
})
|
||||
|
||||
w.window.SetCursorPosCallback(func(_ *glfw.Window, x, y float64) {
|
||||
w.currInp.mouse = pixel.V(
|
||||
x+w.bounds.Min.X(),
|
||||
(w.bounds.H()-y)+w.bounds.Min.Y(),
|
||||
w.tempInp.mouse = pixel.V(
|
||||
x+w.bounds.Min.X,
|
||||
(w.bounds.H()-y)+w.bounds.Min.Y,
|
||||
)
|
||||
})
|
||||
|
||||
w.window.SetScrollCallback(func(_ *glfw.Window, xoff, yoff float64) {
|
||||
w.currInp.scroll += pixel.V(xoff, yoff)
|
||||
w.tempInp.scroll.X += xoff
|
||||
w.tempInp.scroll.Y += yoff
|
||||
})
|
||||
|
||||
w.window.SetCharCallback(func(_ *glfw.Window, r rune) {
|
||||
w.tempInp.typed += string(r)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func (w *Window) updateInput() {
|
||||
// copy temp to prev
|
||||
w.prevInp = w.tempInp
|
||||
|
||||
// zero current scroll (but keep what was added in callbacks outside of this function)
|
||||
w.currInp.scroll -= w.tempInp.scroll
|
||||
|
||||
// get events (usually calls callbacks, but callbacks can be called outside too)
|
||||
// UpdateInput polls window events. Call this function to poll window events
|
||||
// without swapping buffers. Note that the Update method invokes UpdateInput.
|
||||
func (w *Window) UpdateInput() {
|
||||
mainthread.Call(func() {
|
||||
glfw.PollEvents()
|
||||
})
|
||||
|
||||
// cache current state to temp (so that if there are callbacks outside this function,
|
||||
// everything works)
|
||||
w.tempInp = w.currInp
|
||||
w.prevInp = w.currInp
|
||||
w.currInp = w.tempInp
|
||||
|
||||
w.tempInp.repeat = [KeyLast + 1]bool{}
|
||||
w.tempInp.scroll = pixel.ZV
|
||||
w.tempInp.typed = ""
|
||||
}
|
||||
|
|
|
@ -7,9 +7,9 @@ import (
|
|||
)
|
||||
|
||||
func intBounds(bounds pixel.Rect) (x, y, w, h int) {
|
||||
x0 := int(math.Floor(bounds.Min.X()))
|
||||
y0 := int(math.Floor(bounds.Min.Y()))
|
||||
x1 := int(math.Ceil(bounds.Max.X()))
|
||||
y1 := int(math.Ceil(bounds.Max.Y()))
|
||||
x0 := int(math.Floor(bounds.Min.X))
|
||||
y0 := int(math.Floor(bounds.Min.Y))
|
||||
x1 := int(math.Ceil(bounds.Max.X))
|
||||
y1 := int(math.Ceil(bounds.Max.Y))
|
||||
return x0, y0, x1 - x0, y1 - y0
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package pixelgl
|
||||
|
||||
import (
|
||||
"image"
|
||||
"image/color"
|
||||
"runtime"
|
||||
|
||||
|
@ -20,28 +21,31 @@ type WindowConfig struct {
|
|||
// Title at the top of the Window.
|
||||
Title string
|
||||
|
||||
// Icon specifies the icon images available to be used by the window. This is usually
|
||||
// displayed in the top bar of the window or in the task bar of the desktop environment.
|
||||
//
|
||||
// If passed one image, it will use that image, if passed an array of images those of or
|
||||
// closest to the sizes desired by the system are selected. The desired image sizes varies
|
||||
// depending on platform and system settings. The selected images will be rescaled as
|
||||
// needed. Good sizes include 16x16, 32x32 and 48x48.
|
||||
//
|
||||
// Note: Setting this value doesn't have an effect on OSX. You'll need to set the icon when
|
||||
// bundling your application for release.
|
||||
Icon []pixel.Picture
|
||||
|
||||
// Bounds specify the bounds of the Window in pixels.
|
||||
Bounds pixel.Rect
|
||||
|
||||
// If set to nil, a Window will be windowed. Otherwise it will be fullscreen on the
|
||||
// If set to nil, the Window will be windowed. Otherwise it will be fullscreen on the
|
||||
// specified Monitor.
|
||||
Fullscreen *Monitor
|
||||
Monitor *Monitor
|
||||
|
||||
// Whether a Window is resizable.
|
||||
// Whether the Window is resizable.
|
||||
Resizable bool
|
||||
|
||||
// If set to true, the Window will be initially invisible.
|
||||
Hidden bool
|
||||
|
||||
// Undecorated Window ommits the borders and decorations (close button, etc.).
|
||||
Undecorated bool
|
||||
|
||||
// If set to true, a Window will not get focused upon showing up.
|
||||
Unfocused bool
|
||||
|
||||
// Whether a Window is maximized.
|
||||
Maximized bool
|
||||
|
||||
// VSync (vertical synchronization) synchronizes Window's framerate with the framerate of
|
||||
// the monitor.
|
||||
VSync bool
|
||||
|
@ -51,19 +55,22 @@ type WindowConfig struct {
|
|||
type Window struct {
|
||||
window *glfw.Window
|
||||
|
||||
bounds pixel.Rect
|
||||
canvas *Canvas
|
||||
vsync bool
|
||||
bounds pixel.Rect
|
||||
canvas *Canvas
|
||||
vsync bool
|
||||
cursorVisible bool
|
||||
|
||||
// need to save these to correctly restore a fullscreen window
|
||||
restore struct {
|
||||
xpos, ypos, width, height int
|
||||
}
|
||||
|
||||
prevInp, tempInp, currInp struct {
|
||||
prevInp, currInp, tempInp struct {
|
||||
mouse pixel.Vec
|
||||
buttons [KeyLast + 1]bool
|
||||
repeat [KeyLast + 1]bool
|
||||
scroll pixel.Vec
|
||||
typed string
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -78,7 +85,7 @@ func NewWindow(cfg WindowConfig) (*Window, error) {
|
|||
false: glfw.False,
|
||||
}
|
||||
|
||||
w := &Window{bounds: cfg.Bounds}
|
||||
w := &Window{bounds: cfg.Bounds, cursorVisible: true}
|
||||
|
||||
err := mainthread.CallErr(func() error {
|
||||
var err error
|
||||
|
@ -89,10 +96,7 @@ func NewWindow(cfg WindowConfig) (*Window, error) {
|
|||
glfw.WindowHint(glfw.OpenGLForwardCompatible, glfw.True)
|
||||
|
||||
glfw.WindowHint(glfw.Resizable, bool2int[cfg.Resizable])
|
||||
glfw.WindowHint(glfw.Visible, bool2int[!cfg.Hidden])
|
||||
glfw.WindowHint(glfw.Decorated, bool2int[!cfg.Undecorated])
|
||||
glfw.WindowHint(glfw.Focused, bool2int[!cfg.Unfocused])
|
||||
glfw.WindowHint(glfw.Maximized, bool2int[cfg.Maximized])
|
||||
|
||||
var share *glfw.Window
|
||||
if currWin != nil {
|
||||
|
@ -120,12 +124,23 @@ func NewWindow(cfg WindowConfig) (*Window, error) {
|
|||
return nil, errors.Wrap(err, "creating window failed")
|
||||
}
|
||||
|
||||
if len(cfg.Icon) > 0 {
|
||||
imgs := make([]image.Image, len(cfg.Icon))
|
||||
for i, icon := range cfg.Icon {
|
||||
pic := pixel.PictureDataFromPicture(icon)
|
||||
imgs[i] = pic.Image()
|
||||
}
|
||||
mainthread.Call(func() {
|
||||
w.window.SetIcon(imgs)
|
||||
})
|
||||
}
|
||||
|
||||
w.SetVSync(cfg.VSync)
|
||||
|
||||
w.initInput()
|
||||
w.SetMonitor(cfg.Fullscreen)
|
||||
w.SetMonitor(cfg.Monitor)
|
||||
|
||||
w.canvas = NewCanvas(cfg.Bounds, false)
|
||||
w.canvas = NewCanvas(cfg.Bounds)
|
||||
w.Update()
|
||||
|
||||
runtime.SetFinalizer(w, (*Window).Destroy)
|
||||
|
@ -145,10 +160,10 @@ func (w *Window) Update() {
|
|||
mainthread.Call(func() {
|
||||
_, _, oldW, oldH := intBounds(w.bounds)
|
||||
newW, newH := w.window.GetSize()
|
||||
w.bounds = w.bounds.ResizedMin(w.bounds.Size() + pixel.V(
|
||||
w.bounds = w.bounds.ResizedMin(w.bounds.Size().Add(pixel.V(
|
||||
float64(newW-oldW),
|
||||
float64(newH-oldH),
|
||||
))
|
||||
)))
|
||||
})
|
||||
|
||||
w.canvas.SetBounds(w.bounds)
|
||||
|
@ -156,16 +171,17 @@ func (w *Window) Update() {
|
|||
mainthread.Call(func() {
|
||||
w.begin()
|
||||
|
||||
glhf.Bounds(0, 0, w.canvas.f.Texture().Width(), w.canvas.f.Texture().Height())
|
||||
framebufferWidth, framebufferHeight := w.window.GetFramebufferSize()
|
||||
glhf.Bounds(0, 0, framebufferWidth, framebufferHeight)
|
||||
|
||||
glhf.Clear(0, 0, 0, 0)
|
||||
w.canvas.f.Begin()
|
||||
w.canvas.f.Blit(
|
||||
w.canvas.gf.Frame().Begin()
|
||||
w.canvas.gf.Frame().Blit(
|
||||
nil,
|
||||
0, 0, w.canvas.f.Texture().Width(), w.canvas.f.Texture().Height(),
|
||||
0, 0, w.canvas.f.Texture().Width(), w.canvas.f.Texture().Height(),
|
||||
0, 0, w.canvas.Texture().Width(), w.canvas.Texture().Height(),
|
||||
0, 0, framebufferWidth, framebufferHeight,
|
||||
)
|
||||
w.canvas.f.End()
|
||||
w.canvas.gf.Frame().End()
|
||||
|
||||
if w.vsync {
|
||||
glfw.SwapInterval(1)
|
||||
|
@ -176,7 +192,7 @@ func (w *Window) Update() {
|
|||
w.end()
|
||||
})
|
||||
|
||||
w.updateInput()
|
||||
w.UpdateInput()
|
||||
}
|
||||
|
||||
// SetClosed sets the closed flag of the Window.
|
||||
|
@ -217,25 +233,34 @@ func (w *Window) SetBounds(bounds pixel.Rect) {
|
|||
})
|
||||
}
|
||||
|
||||
// SetPos sets the position, in screen coordinates, of the upper-left corner
|
||||
// of the client area of the window. Position can be fractional, but the actual position
|
||||
// of the window will be rounded to integers.
|
||||
//
|
||||
// If it is a full screen window, this function does nothing.
|
||||
func (w *Window) SetPos(pos pixel.Vec) {
|
||||
mainthread.Call(func() {
|
||||
left, top := int(pos.X), int(pos.Y)
|
||||
w.window.SetPos(left, top)
|
||||
})
|
||||
}
|
||||
|
||||
// GetPos gets the position, in screen coordinates, of the upper-left corner
|
||||
// of the client area of the window. The position is rounded to integers.
|
||||
func (w *Window) GetPos() pixel.Vec {
|
||||
var v pixel.Vec
|
||||
mainthread.Call(func() {
|
||||
x, y := w.window.GetPos()
|
||||
v = pixel.V(float64(x), float64(y))
|
||||
})
|
||||
return v
|
||||
}
|
||||
|
||||
// Bounds returns the current bounds of the Window.
|
||||
func (w *Window) Bounds() pixel.Rect {
|
||||
return w.bounds
|
||||
}
|
||||
|
||||
// Show makes the Window visible if it was hidden.
|
||||
func (w *Window) Show() {
|
||||
mainthread.Call(func() {
|
||||
w.window.Show()
|
||||
})
|
||||
}
|
||||
|
||||
// Hide hides the Window if it was visible.
|
||||
func (w *Window) Hide() {
|
||||
mainthread.Call(func() {
|
||||
w.window.Hide()
|
||||
})
|
||||
}
|
||||
|
||||
func (w *Window) setFullscreen(monitor *Monitor) {
|
||||
mainthread.Call(func() {
|
||||
w.restore.xpos, w.restore.ypos = w.window.GetPos()
|
||||
|
@ -282,11 +307,6 @@ func (w *Window) SetMonitor(monitor *Monitor) {
|
|||
}
|
||||
}
|
||||
|
||||
// IsFullscreen returns true if the Window is in fullscreen mode.
|
||||
func (w *Window) IsFullscreen() bool {
|
||||
return w.Monitor() != nil
|
||||
}
|
||||
|
||||
// Monitor returns a monitor the Window is fullscreen on. If the Window is not fullscreen, this
|
||||
// function returns nil.
|
||||
func (w *Window) Monitor() *Monitor {
|
||||
|
@ -302,13 +322,6 @@ func (w *Window) Monitor() *Monitor {
|
|||
}
|
||||
}
|
||||
|
||||
// Focus brings the Window to the front and sets input focus.
|
||||
func (w *Window) Focus() {
|
||||
mainthread.Call(func() {
|
||||
w.window.Focus()
|
||||
})
|
||||
}
|
||||
|
||||
// Focused returns true if the Window has input focus.
|
||||
func (w *Window) Focused() bool {
|
||||
var focused bool
|
||||
|
@ -318,20 +331,6 @@ func (w *Window) Focused() bool {
|
|||
return focused
|
||||
}
|
||||
|
||||
// Maximize puts the Window to the maximized state.
|
||||
func (w *Window) Maximize() {
|
||||
mainthread.Call(func() {
|
||||
w.window.Maximize()
|
||||
})
|
||||
}
|
||||
|
||||
// Restore restores the Window from the maximized state.
|
||||
func (w *Window) Restore() {
|
||||
mainthread.Call(func() {
|
||||
w.window.Restore()
|
||||
})
|
||||
}
|
||||
|
||||
// SetVSync sets whether the Window's Update should synchronize with the monitor refresh rate.
|
||||
func (w *Window) SetVSync(vsync bool) {
|
||||
w.vsync = vsync
|
||||
|
@ -342,6 +341,23 @@ func (w *Window) VSync() bool {
|
|||
return w.vsync
|
||||
}
|
||||
|
||||
// SetCursorVisible sets the visibility of the mouse cursor inside the Window client area.
|
||||
func (w *Window) SetCursorVisible(visible bool) {
|
||||
w.cursorVisible = visible
|
||||
mainthread.Call(func() {
|
||||
if visible {
|
||||
w.window.SetInputMode(glfw.CursorMode, glfw.CursorNormal)
|
||||
} else {
|
||||
w.window.SetInputMode(glfw.CursorMode, glfw.CursorHidden)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// CursorVisible returns the visibility status of the mouse cursor.
|
||||
func (w *Window) CursorVisible() bool {
|
||||
return w.cursorVisible
|
||||
}
|
||||
|
||||
// Note: must be called inside the main thread.
|
||||
func (w *Window) begin() {
|
||||
if currWin != w {
|
||||
|
@ -366,7 +382,7 @@ func (w *Window) MakeTriangles(t pixel.Triangles) pixel.TargetTriangles {
|
|||
|
||||
// MakePicture generates a specialized copy of the supplied Picture that will draw onto this Window.
|
||||
//
|
||||
// Window support PictureColor.
|
||||
// Window supports PictureColor.
|
||||
func (w *Window) MakePicture(p pixel.Picture) pixel.TargetPicture {
|
||||
return w.canvas.MakePicture(p)
|
||||
}
|
||||
|
@ -381,6 +397,12 @@ func (w *Window) SetColorMask(c color.Color) {
|
|||
w.canvas.SetColorMask(c)
|
||||
}
|
||||
|
||||
// SetComposeMethod sets a Porter-Duff composition method to be used in the following draws onto
|
||||
// this Window.
|
||||
func (w *Window) SetComposeMethod(cmp pixel.ComposeMethod) {
|
||||
w.canvas.SetComposeMethod(cmp)
|
||||
}
|
||||
|
||||
// SetSmooth sets whether the stretched Pictures drawn onto this Window should be drawn smooth or
|
||||
// pixely.
|
||||
func (w *Window) SetSmooth(smooth bool) {
|
||||
|
@ -397,3 +419,8 @@ func (w *Window) Smooth() bool {
|
|||
func (w *Window) Clear(c color.Color) {
|
||||
w.canvas.Clear(c)
|
||||
}
|
||||
|
||||
// Color returns the color of the pixel over the given position inside the Window.
|
||||
func (w *Window) Color(at pixel.Vec) pixel.RGBA {
|
||||
return w.canvas.Color(at)
|
||||
}
|
||||
|
|
118
sprite.go
|
@ -2,43 +2,46 @@ package pixel
|
|||
|
||||
import "image/color"
|
||||
|
||||
// Sprite is a drawable Picture. It's anchored by the center of it's Picture.
|
||||
// Sprite is a drawable frame of a Picture. It's anchored by the center of it's Picture's frame.
|
||||
//
|
||||
// To achieve different anchoring, transformations and color masking, use SetMatrix and SetColorMask
|
||||
// methods.
|
||||
// Frame specifies a rectangular portion of the Picture that will be drawn. For example, this
|
||||
// creates a Sprite that draws the whole Picture:
|
||||
//
|
||||
// sprite := pixel.NewSprite(pic, pic.Bounds())
|
||||
//
|
||||
// Note, that Sprite caches the results of MakePicture from Targets it's drawn to for each Picture
|
||||
// it's set to. What it means is that using a Sprite with an unbounded number of Pictures leads to a
|
||||
// memory leak, since Sprite caches them and never forgets. In such a situation, create a new Sprite
|
||||
// for each Picture.
|
||||
type Sprite struct {
|
||||
tri *TrianglesData
|
||||
bounds Rect
|
||||
d Drawer
|
||||
tri *TrianglesData
|
||||
frame Rect
|
||||
d Drawer
|
||||
|
||||
matrix Matrix
|
||||
mask NRGBA
|
||||
mask RGBA
|
||||
}
|
||||
|
||||
// NewSprite creates a Sprite from the supplied Picture.
|
||||
func NewSprite(pic Picture) *Sprite {
|
||||
// NewSprite creates a Sprite from the supplied frame of a Picture.
|
||||
func NewSprite(pic Picture, frame Rect) *Sprite {
|
||||
tri := MakeTrianglesData(6)
|
||||
s := &Sprite{
|
||||
tri: tri,
|
||||
d: Drawer{Triangles: tri},
|
||||
}
|
||||
s.matrix = IM
|
||||
s.mask = NRGBA{1, 1, 1, 1}
|
||||
s.SetPicture(pic)
|
||||
s.mask = Alpha(1)
|
||||
s.Set(pic, frame)
|
||||
return s
|
||||
}
|
||||
|
||||
// SetPicture changes the Sprite's Picture. The new Picture may have a different size, everything
|
||||
// works.
|
||||
func (s *Sprite) SetPicture(pic Picture) {
|
||||
// Set sets a new frame of a Picture for this Sprite.
|
||||
func (s *Sprite) Set(pic Picture, frame Rect) {
|
||||
s.d.Picture = pic
|
||||
|
||||
if s.bounds == pic.Bounds() {
|
||||
return
|
||||
if frame != s.frame {
|
||||
s.frame = frame
|
||||
s.calcData()
|
||||
}
|
||||
s.bounds = pic.Bounds()
|
||||
|
||||
s.calcData()
|
||||
}
|
||||
|
||||
// Picture returns the current Sprite's Picture.
|
||||
|
@ -46,58 +49,61 @@ func (s *Sprite) Picture() Picture {
|
|||
return s.d.Picture
|
||||
}
|
||||
|
||||
// SetMatrix sets a Matrix that this Sprite will be transformed by. This overrides any previously
|
||||
// set Matrix.
|
||||
// Frame returns the current Sprite's frame.
|
||||
func (s *Sprite) Frame() Rect {
|
||||
return s.frame
|
||||
}
|
||||
|
||||
// Draw draws the Sprite onto the provided Target. The Sprite will be transformed by the given Matrix.
|
||||
//
|
||||
// Note, that this has nothing to do with BasicTarget's SetMatrix method. This only affects this
|
||||
// Sprite and is usable with any Target.
|
||||
func (s *Sprite) SetMatrix(matrix Matrix) {
|
||||
s.matrix = matrix
|
||||
s.calcData()
|
||||
// This method is equivalent to calling DrawColorMask with nil color mask.
|
||||
func (s *Sprite) Draw(t Target, matrix Matrix) {
|
||||
s.DrawColorMask(t, matrix, nil)
|
||||
}
|
||||
|
||||
// Matrix returns the currently set Matrix.
|
||||
func (s *Sprite) Matrix() Matrix {
|
||||
return s.matrix
|
||||
}
|
||||
|
||||
// SetColorMask sets a color that this Sprite will be multiplied by. This overrides any previously
|
||||
// set color mask.
|
||||
// DrawColorMask draws the Sprite onto the provided Target. The Sprite will be transformed by the
|
||||
// given Matrix and all of it's color will be multiplied by the given mask.
|
||||
//
|
||||
// Note, that this has nothing to do with BasicTarget's SetColorMask method. This only affects this
|
||||
// Sprite and is usable with any Target.
|
||||
func (s *Sprite) SetColorMask(mask color.Color) {
|
||||
s.mask = ToNRGBA(mask)
|
||||
s.calcData()
|
||||
}
|
||||
// If the mask is nil, a fully opaque white mask will be used, which causes no effect.
|
||||
func (s *Sprite) DrawColorMask(t Target, matrix Matrix, mask color.Color) {
|
||||
dirty := false
|
||||
if matrix != s.matrix {
|
||||
s.matrix = matrix
|
||||
dirty = true
|
||||
}
|
||||
if mask == nil {
|
||||
mask = Alpha(1)
|
||||
}
|
||||
rgba := ToRGBA(mask)
|
||||
if rgba != s.mask {
|
||||
s.mask = rgba
|
||||
dirty = true
|
||||
}
|
||||
|
||||
// ColorMask returns the currently set color mask.
|
||||
func (s *Sprite) ColorMask() NRGBA {
|
||||
return s.mask
|
||||
}
|
||||
if dirty {
|
||||
s.calcData()
|
||||
}
|
||||
|
||||
// Draw draws the Sprite onto the provided Target.
|
||||
func (s *Sprite) Draw(t Target) {
|
||||
s.d.Draw(t)
|
||||
}
|
||||
|
||||
func (s *Sprite) calcData() {
|
||||
var (
|
||||
center = s.bounds.Center()
|
||||
horizontal = X(s.bounds.W() / 2)
|
||||
vertical = Y(s.bounds.H() / 2)
|
||||
center = s.frame.Center()
|
||||
horizontal = V(s.frame.W()/2, 0)
|
||||
vertical = V(0, s.frame.H()/2)
|
||||
)
|
||||
|
||||
(*s.tri)[0].Position = -horizontal - vertical
|
||||
(*s.tri)[1].Position = +horizontal - vertical
|
||||
(*s.tri)[2].Position = +horizontal + vertical
|
||||
(*s.tri)[3].Position = -horizontal - vertical
|
||||
(*s.tri)[4].Position = +horizontal + vertical
|
||||
(*s.tri)[5].Position = -horizontal + vertical
|
||||
(*s.tri)[0].Position = Vec{}.Sub(horizontal).Sub(vertical)
|
||||
(*s.tri)[1].Position = Vec{}.Add(horizontal).Sub(vertical)
|
||||
(*s.tri)[2].Position = Vec{}.Add(horizontal).Add(vertical)
|
||||
(*s.tri)[3].Position = Vec{}.Sub(horizontal).Sub(vertical)
|
||||
(*s.tri)[4].Position = Vec{}.Add(horizontal).Add(vertical)
|
||||
(*s.tri)[5].Position = Vec{}.Sub(horizontal).Add(vertical)
|
||||
|
||||
for i := range *s.tri {
|
||||
(*s.tri)[i].Color = s.mask
|
||||
(*s.tri)[i].Picture = center + (*s.tri)[i].Position
|
||||
(*s.tri)[i].Picture = center.Add((*s.tri)[i].Position)
|
||||
(*s.tri)[i].Intensity = 1
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,248 @@
|
|||
package text
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"image"
|
||||
"image/draw"
|
||||
"sort"
|
||||
"unicode"
|
||||
|
||||
"github.com/faiface/pixel"
|
||||
"golang.org/x/image/font"
|
||||
"golang.org/x/image/math/fixed"
|
||||
)
|
||||
|
||||
// Atlas7x13 is an Atlas using basicfont.Face7x13 with the ASCII rune set
|
||||
var Atlas7x13 *Atlas
|
||||
|
||||
// Glyph describes one glyph in an Atlas.
|
||||
type Glyph struct {
|
||||
Dot pixel.Vec
|
||||
Frame pixel.Rect
|
||||
Advance float64
|
||||
}
|
||||
|
||||
// Atlas is a set of pre-drawn glyphs of a fixed set of runes. This allows for efficient text drawing.
|
||||
type Atlas struct {
|
||||
face font.Face
|
||||
pic pixel.Picture
|
||||
mapping map[rune]Glyph
|
||||
ascent float64
|
||||
descent float64
|
||||
lineHeight float64
|
||||
}
|
||||
|
||||
// NewAtlas creates a new Atlas containing glyphs of the union of the given sets of runes (plus
|
||||
// unicode.ReplacementChar) from the given font face.
|
||||
//
|
||||
// Creating an Atlas is rather expensive, do not create a new Atlas each frame.
|
||||
//
|
||||
// Do not destroy or close the font.Face after creating the Atlas. Atlas still uses it.
|
||||
func NewAtlas(face font.Face, runeSets ...[]rune) *Atlas {
|
||||
seen := make(map[rune]bool)
|
||||
runes := []rune{unicode.ReplacementChar}
|
||||
for _, set := range runeSets {
|
||||
for _, r := range set {
|
||||
if !seen[r] {
|
||||
runes = append(runes, r)
|
||||
seen[r] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fixedMapping, fixedBounds := makeSquareMapping(face, runes, fixed.I(2))
|
||||
|
||||
atlasImg := image.NewRGBA(image.Rect(
|
||||
fixedBounds.Min.X.Floor(),
|
||||
fixedBounds.Min.Y.Floor(),
|
||||
fixedBounds.Max.X.Ceil(),
|
||||
fixedBounds.Max.Y.Ceil(),
|
||||
))
|
||||
|
||||
for r, fg := range fixedMapping {
|
||||
dr, mask, maskp, _, _ := face.Glyph(fg.dot, r)
|
||||
draw.Draw(atlasImg, dr, mask, maskp, draw.Src)
|
||||
}
|
||||
|
||||
bounds := pixel.R(
|
||||
i2f(fixedBounds.Min.X),
|
||||
i2f(fixedBounds.Min.Y),
|
||||
i2f(fixedBounds.Max.X),
|
||||
i2f(fixedBounds.Max.Y),
|
||||
)
|
||||
|
||||
mapping := make(map[rune]Glyph)
|
||||
for r, fg := range fixedMapping {
|
||||
mapping[r] = Glyph{
|
||||
Dot: pixel.V(
|
||||
i2f(fg.dot.X),
|
||||
bounds.Max.Y-(i2f(fg.dot.Y)-bounds.Min.Y),
|
||||
),
|
||||
Frame: pixel.R(
|
||||
i2f(fg.frame.Min.X),
|
||||
bounds.Max.Y-(i2f(fg.frame.Min.Y)-bounds.Min.Y),
|
||||
i2f(fg.frame.Max.X),
|
||||
bounds.Max.Y-(i2f(fg.frame.Max.Y)-bounds.Min.Y),
|
||||
).Norm(),
|
||||
Advance: i2f(fg.advance),
|
||||
}
|
||||
}
|
||||
|
||||
return &Atlas{
|
||||
face: face,
|
||||
pic: pixel.PictureDataFromImage(atlasImg),
|
||||
mapping: mapping,
|
||||
ascent: i2f(face.Metrics().Ascent),
|
||||
descent: i2f(face.Metrics().Descent),
|
||||
lineHeight: i2f(face.Metrics().Height),
|
||||
}
|
||||
}
|
||||
|
||||
// Picture returns the underlying Picture containing an arrangement of all the glyphs contained
|
||||
// within the Atlas.
|
||||
func (a *Atlas) Picture() pixel.Picture {
|
||||
return a.pic
|
||||
}
|
||||
|
||||
// Contains reports wheter r in contained within the Atlas.
|
||||
func (a *Atlas) Contains(r rune) bool {
|
||||
_, ok := a.mapping[r]
|
||||
return ok
|
||||
}
|
||||
|
||||
// Glyph returns the description of r within the Atlas.
|
||||
func (a *Atlas) Glyph(r rune) Glyph {
|
||||
return a.mapping[r]
|
||||
}
|
||||
|
||||
// Kern returns the kerning distance between runes r0 and r1. Positive distance means that the
|
||||
// glyphs should be further apart.
|
||||
func (a *Atlas) Kern(r0, r1 rune) float64 {
|
||||
return i2f(a.face.Kern(r0, r1))
|
||||
}
|
||||
|
||||
// Ascent returns the distance from the top of the line to the baseline.
|
||||
func (a *Atlas) Ascent() float64 {
|
||||
return a.ascent
|
||||
}
|
||||
|
||||
// Descent returns the distance from the baseline to the bottom of the line.
|
||||
func (a *Atlas) Descent() float64 {
|
||||
return a.descent
|
||||
}
|
||||
|
||||
// LineHeight returns the recommended vertical distance between two lines of text.
|
||||
func (a *Atlas) LineHeight() float64 {
|
||||
return a.lineHeight
|
||||
}
|
||||
|
||||
// DrawRune returns parameters necessary for drawing a rune glyph.
|
||||
//
|
||||
// Rect is a rectangle where the glyph should be positioned. Frame is the glyph frame inside the
|
||||
// Atlas's Picture. NewDot is the new position of the dot.
|
||||
func (a *Atlas) DrawRune(prevR, r rune, dot pixel.Vec) (rect, frame, bounds pixel.Rect, newDot pixel.Vec) {
|
||||
if !a.Contains(r) {
|
||||
r = unicode.ReplacementChar
|
||||
}
|
||||
if !a.Contains(unicode.ReplacementChar) {
|
||||
return pixel.Rect{}, pixel.Rect{}, pixel.Rect{}, dot
|
||||
}
|
||||
if !a.Contains(prevR) {
|
||||
prevR = unicode.ReplacementChar
|
||||
}
|
||||
|
||||
if prevR >= 0 {
|
||||
dot.X += a.Kern(prevR, r)
|
||||
}
|
||||
|
||||
glyph := a.Glyph(r)
|
||||
|
||||
rect = glyph.Frame.Moved(dot.Sub(glyph.Dot))
|
||||
bounds = rect
|
||||
|
||||
if bounds.W()*bounds.H() != 0 {
|
||||
bounds = pixel.R(
|
||||
bounds.Min.X,
|
||||
dot.Y-a.Descent(),
|
||||
bounds.Max.X,
|
||||
dot.Y+a.Ascent(),
|
||||
)
|
||||
}
|
||||
|
||||
dot.X += glyph.Advance
|
||||
|
||||
return rect, glyph.Frame, bounds, dot
|
||||
}
|
||||
|
||||
type fixedGlyph struct {
|
||||
dot fixed.Point26_6
|
||||
frame fixed.Rectangle26_6
|
||||
advance fixed.Int26_6
|
||||
}
|
||||
|
||||
// makeSquareMapping finds an optimal glyph arrangement of the given runes, so that their common
|
||||
// bounding box is as square as possible.
|
||||
func makeSquareMapping(face font.Face, runes []rune, padding fixed.Int26_6) (map[rune]fixedGlyph, fixed.Rectangle26_6) {
|
||||
width := sort.Search(int(fixed.I(1024*1024)), func(i int) bool {
|
||||
width := fixed.Int26_6(i)
|
||||
_, bounds := makeMapping(face, runes, padding, width)
|
||||
return bounds.Max.X-bounds.Min.X >= bounds.Max.Y-bounds.Min.Y
|
||||
})
|
||||
return makeMapping(face, runes, padding, fixed.Int26_6(width))
|
||||
}
|
||||
|
||||
// makeMapping arranges glyphs of the given runes into rows in such a way, that no glyph is located
|
||||
// fully to the right of the specified width. Specifically, it places glyphs in a row one by one and
|
||||
// once it reaches the specified width, it starts a new row.
|
||||
func makeMapping(face font.Face, runes []rune, padding, width fixed.Int26_6) (map[rune]fixedGlyph, fixed.Rectangle26_6) {
|
||||
mapping := make(map[rune]fixedGlyph)
|
||||
bounds := fixed.Rectangle26_6{}
|
||||
|
||||
dot := fixed.P(0, 0)
|
||||
|
||||
for _, r := range runes {
|
||||
b, advance, ok := face.GlyphBounds(r)
|
||||
if !ok {
|
||||
fmt.Println(r)
|
||||
continue
|
||||
}
|
||||
|
||||
// this is important for drawing, artifacts arise otherwise
|
||||
frame := fixed.Rectangle26_6{
|
||||
Min: fixed.P(b.Min.X.Floor(), b.Min.Y.Floor()),
|
||||
Max: fixed.P(b.Max.X.Ceil(), b.Max.Y.Ceil()),
|
||||
}
|
||||
|
||||
dot.X -= frame.Min.X
|
||||
frame = frame.Add(dot)
|
||||
|
||||
mapping[r] = fixedGlyph{
|
||||
dot: dot,
|
||||
frame: frame,
|
||||
advance: advance,
|
||||
}
|
||||
bounds = bounds.Union(frame)
|
||||
|
||||
dot.X = frame.Max.X
|
||||
|
||||
// padding + align to integer
|
||||
dot.X += padding
|
||||
dot.X = fixed.I(dot.X.Ceil())
|
||||
|
||||
// width exceeded, new row
|
||||
if frame.Max.X >= width {
|
||||
dot.X = 0
|
||||
dot.Y += face.Metrics().Ascent + face.Metrics().Descent
|
||||
|
||||
// padding + align to integer
|
||||
dot.Y += padding
|
||||
dot.Y = fixed.I(dot.Y.Ceil())
|
||||
}
|
||||
}
|
||||
|
||||
return mapping, bounds
|
||||
}
|
||||
|
||||
func i2f(i fixed.Int26_6) float64 {
|
||||
return float64(i) / (1 << 6)
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
package text_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/faiface/pixel/text"
|
||||
)
|
||||
|
||||
func TestAtlas7x13(t *testing.T) {
|
||||
if text.Atlas7x13 == nil {
|
||||
t.Fatalf("Atlas7x13 is nil")
|
||||
}
|
||||
|
||||
for _, tt := range []struct {
|
||||
runes []rune
|
||||
want bool
|
||||
}{{text.ASCII, true}, {[]rune("ÅÄÖ"), false}} {
|
||||
for _, r := range tt.runes {
|
||||
if got := text.Atlas7x13.Contains(r); got != tt.want {
|
||||
t.Fatalf("Atlas7x13.Contains('%s') = %v, want %v", string(r), got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
// Package text implements efficient text drawing for the Pixel library.
|
||||
package text
|
|
@ -0,0 +1,347 @@
|
|||
package text
|
||||
|
||||
import (
|
||||
"image/color"
|
||||
"math"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/faiface/pixel"
|
||||
"golang.org/x/image/font/basicfont"
|
||||
)
|
||||
|
||||
// ASCII is a set of all ASCII runes. These runes are codepoints from 32 to 127 inclusive.
|
||||
var ASCII []rune
|
||||
|
||||
func init() {
|
||||
ASCII = make([]rune, unicode.MaxASCII-32)
|
||||
for i := range ASCII {
|
||||
ASCII[i] = rune(32 + i)
|
||||
}
|
||||
Atlas7x13 = NewAtlas(basicfont.Face7x13, ASCII)
|
||||
}
|
||||
|
||||
// RangeTable takes a *unicode.RangeTable and generates a set of runes contained within that
|
||||
// RangeTable.
|
||||
func RangeTable(table *unicode.RangeTable) []rune {
|
||||
var runes []rune
|
||||
for _, rng := range table.R16 {
|
||||
for r := rng.Lo; r <= rng.Hi; r += rng.Stride {
|
||||
runes = append(runes, rune(r))
|
||||
}
|
||||
}
|
||||
for _, rng := range table.R32 {
|
||||
for r := rng.Lo; r <= rng.Hi; r += rng.Stride {
|
||||
runes = append(runes, rune(r))
|
||||
}
|
||||
}
|
||||
return runes
|
||||
}
|
||||
|
||||
// Text allows for effiecient and convenient text drawing.
|
||||
//
|
||||
// To create a Text object, use the New constructor:
|
||||
// txt := text.New(pixel.ZV, text.NewAtlas(face, text.ASCII))
|
||||
//
|
||||
// As suggested by the constructor, a Text object is always associated with one font face and a
|
||||
// fixed set of runes. For example, the Text we created above can draw text using the font face
|
||||
// contained in the face variable and is capable of drawing ASCII characters.
|
||||
//
|
||||
// Here we create a Text object which can draw ASCII and Katakana characters:
|
||||
// txt := text.New(0, text.NewAtlas(face, text.ASCII, text.RangeTable(unicode.Katakana)))
|
||||
//
|
||||
// Similarly to IMDraw, Text functions as a buffer. It implements io.Writer interface, so writing
|
||||
// text to it is really simple:
|
||||
// fmt.Print(txt, "Hello, world!")
|
||||
//
|
||||
// Newlines, tabs and carriage returns are supported.
|
||||
//
|
||||
// Finally, if we want the written text to show up on some other Target, we can draw it:
|
||||
// txt.Draw(target)
|
||||
//
|
||||
// Text exports two important fields: Orig and Dot. Dot is the position where the next character
|
||||
// will be written. Dot is automatically moved when writing to a Text object, but you can also
|
||||
// manipulate it manually. Orig specifies the text origin, usually the top-left dot position. Dot is
|
||||
// always aligned to Orig when writing newlines. The Clear method resets the Dot to Orig.
|
||||
type Text struct {
|
||||
// Orig specifies the text origin, usually the top-left dot position. Dot is always aligned
|
||||
// to Orig when writing newlines.
|
||||
Orig pixel.Vec
|
||||
|
||||
// Dot is the position where the next character will be written. Dot is automatically moved
|
||||
// when writing to a Text object, but you can also manipulate it manually
|
||||
Dot pixel.Vec
|
||||
|
||||
// Color is the color of the text that is to be written. Defaults to white.
|
||||
Color color.Color
|
||||
|
||||
// LineHeight is the vertical distance between two lines of text.
|
||||
//
|
||||
// Example:
|
||||
// txt.LineHeight = 1.5 * txt.Atlas().LineHeight()
|
||||
LineHeight float64
|
||||
|
||||
// TabWidth is the horizontal tab width. Tab characters will align to the multiples of this
|
||||
// width.
|
||||
//
|
||||
// Example:
|
||||
// txt.TabWidth = 8 * txt.Atlas().Glyph(' ').Advance
|
||||
TabWidth float64
|
||||
|
||||
atlas *Atlas
|
||||
|
||||
buf []byte
|
||||
prevR rune
|
||||
bounds pixel.Rect
|
||||
glyph pixel.TrianglesData
|
||||
tris pixel.TrianglesData
|
||||
|
||||
mat pixel.Matrix
|
||||
col pixel.RGBA
|
||||
trans pixel.TrianglesData
|
||||
transD pixel.Drawer
|
||||
dirty bool
|
||||
}
|
||||
|
||||
// New creates a new Text capable of drawing runes contained in the provided Atlas. Orig and Dot
|
||||
// will be initially set to orig.
|
||||
//
|
||||
// Here we create a Text capable of drawing ASCII characters using the Go Regular font.
|
||||
// ttf, err := truetype.Parse(goregular.TTF)
|
||||
// if err != nil {
|
||||
// panic(err)
|
||||
// }
|
||||
// face := truetype.NewFace(ttf, &truetype.Options{
|
||||
// Size: 14,
|
||||
// })
|
||||
// txt := text.New(orig, text.NewAtlas(face, text.ASCII))
|
||||
func New(orig pixel.Vec, atlas *Atlas) *Text {
|
||||
txt := &Text{
|
||||
Orig: orig,
|
||||
Dot: orig,
|
||||
Color: pixel.Alpha(1),
|
||||
LineHeight: atlas.LineHeight(),
|
||||
TabWidth: atlas.Glyph(' ').Advance * 4,
|
||||
atlas: atlas,
|
||||
mat: pixel.IM,
|
||||
col: pixel.Alpha(1),
|
||||
}
|
||||
|
||||
txt.glyph.SetLen(6)
|
||||
for i := range txt.glyph {
|
||||
txt.glyph[i].Color = pixel.Alpha(1)
|
||||
txt.glyph[i].Intensity = 1
|
||||
}
|
||||
|
||||
txt.transD.Picture = txt.atlas.pic
|
||||
txt.transD.Triangles = &txt.trans
|
||||
|
||||
txt.Clear()
|
||||
|
||||
return txt
|
||||
}
|
||||
|
||||
// Atlas returns the underlying Text's Atlas containing all of the pre-drawn glyphs. The Atlas is
|
||||
// also useful for getting values such as the recommended line height.
|
||||
func (txt *Text) Atlas() *Atlas {
|
||||
return txt.atlas
|
||||
}
|
||||
|
||||
// Bounds returns the bounding box of the text currently written to the Text excluding whitespace.
|
||||
//
|
||||
// If the Text is empty, a zero rectangle is returned.
|
||||
func (txt *Text) Bounds() pixel.Rect {
|
||||
return txt.bounds
|
||||
}
|
||||
|
||||
// BoundsOf returns the bounding box of s if it was to be written to the Text right now.
|
||||
func (txt *Text) BoundsOf(s string) pixel.Rect {
|
||||
dot := txt.Dot
|
||||
prevR := txt.prevR
|
||||
bounds := pixel.Rect{}
|
||||
|
||||
for _, r := range s {
|
||||
var control bool
|
||||
dot, control = txt.controlRune(r, dot)
|
||||
if control {
|
||||
continue
|
||||
}
|
||||
|
||||
var b pixel.Rect
|
||||
_, _, b, dot = txt.Atlas().DrawRune(prevR, r, dot)
|
||||
|
||||
if bounds.W()*bounds.H() == 0 {
|
||||
bounds = b
|
||||
} else {
|
||||
bounds = bounds.Union(b)
|
||||
}
|
||||
|
||||
prevR = r
|
||||
}
|
||||
|
||||
return bounds
|
||||
}
|
||||
|
||||
// Clear removes all written text from the Text. The Dot field is reset to Orig.
|
||||
func (txt *Text) Clear() {
|
||||
txt.prevR = -1
|
||||
txt.bounds = pixel.Rect{}
|
||||
txt.tris.SetLen(0)
|
||||
txt.dirty = true
|
||||
txt.Dot = txt.Orig
|
||||
}
|
||||
|
||||
// Write writes a slice of bytes to the Text. This method never fails, always returns len(p), nil.
|
||||
func (txt *Text) Write(p []byte) (n int, err error) {
|
||||
txt.buf = append(txt.buf, p...)
|
||||
txt.drawBuf()
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
// WriteString writes a string to the Text. This method never fails, always returns len(s), nil.
|
||||
func (txt *Text) WriteString(s string) (n int, err error) {
|
||||
txt.buf = append(txt.buf, s...)
|
||||
txt.drawBuf()
|
||||
return len(s), nil
|
||||
}
|
||||
|
||||
// WriteByte writes a byte to the Text. This method never fails, always returns nil.
|
||||
//
|
||||
// Writing a multi-byte rune byte-by-byte is perfectly supported.
|
||||
func (txt *Text) WriteByte(c byte) error {
|
||||
txt.buf = append(txt.buf, c)
|
||||
txt.drawBuf()
|
||||
return nil
|
||||
}
|
||||
|
||||
// WriteRune writes a rune to the Text. This method never fails, always returns utf8.RuneLen(r), nil.
|
||||
func (txt *Text) WriteRune(r rune) (n int, err error) {
|
||||
var b [4]byte
|
||||
n = utf8.EncodeRune(b[:], r)
|
||||
txt.buf = append(txt.buf, b[:n]...)
|
||||
txt.drawBuf()
|
||||
return n, nil
|
||||
}
|
||||
|
||||
// Draw draws all text written to the Text to the provided Target. The text is transformed by the
|
||||
// provided Matrix.
|
||||
//
|
||||
// This method is equivalent to calling DrawColorMask with nil color mask.
|
||||
//
|
||||
// If there's a lot of text written to the Text, changing a matrix or a color mask often might hurt
|
||||
// performance. Consider using your Target's SetMatrix or SetColorMask methods if available.
|
||||
func (txt *Text) Draw(t pixel.Target, matrix pixel.Matrix) {
|
||||
txt.DrawColorMask(t, matrix, nil)
|
||||
}
|
||||
|
||||
// DrawColorMask draws all text written to the Text to the provided Target. The text is transformed
|
||||
// by the provided Matrix and masked by the provided color mask.
|
||||
//
|
||||
// If there's a lot of text written to the Text, changing a matrix or a color mask often might hurt
|
||||
// performance. Consider using your Target's SetMatrix or SetColorMask methods if available.
|
||||
func (txt *Text) DrawColorMask(t pixel.Target, matrix pixel.Matrix, mask color.Color) {
|
||||
if matrix != txt.mat {
|
||||
txt.mat = matrix
|
||||
txt.dirty = true
|
||||
}
|
||||
if mask == nil {
|
||||
mask = pixel.Alpha(1)
|
||||
}
|
||||
rgba := pixel.ToRGBA(mask)
|
||||
if rgba != txt.col {
|
||||
txt.col = rgba
|
||||
txt.dirty = true
|
||||
}
|
||||
|
||||
if txt.dirty {
|
||||
txt.trans.SetLen(txt.tris.Len())
|
||||
txt.trans.Update(&txt.tris)
|
||||
|
||||
for i := range txt.trans {
|
||||
txt.trans[i].Position = txt.mat.Project(txt.trans[i].Position)
|
||||
txt.trans[i].Color = txt.trans[i].Color.Mul(txt.col)
|
||||
}
|
||||
|
||||
txt.transD.Dirty()
|
||||
txt.dirty = false
|
||||
}
|
||||
|
||||
txt.transD.Draw(t)
|
||||
}
|
||||
|
||||
// controlRune checks if r is a control rune (newline, tab, ...). If it is, a new dot position and
|
||||
// true is returned. If r is not a control rune, the original dot and false is returned.
|
||||
func (txt *Text) controlRune(r rune, dot pixel.Vec) (newDot pixel.Vec, control bool) {
|
||||
switch r {
|
||||
case '\n':
|
||||
dot.X = txt.Orig.X
|
||||
dot.Y -= txt.LineHeight
|
||||
case '\r':
|
||||
dot.X = txt.Orig.X
|
||||
case '\t':
|
||||
rem := math.Mod(dot.X-txt.Orig.X, txt.TabWidth)
|
||||
rem = math.Mod(rem, rem+txt.TabWidth)
|
||||
if rem == 0 {
|
||||
rem = txt.TabWidth
|
||||
}
|
||||
dot.X += rem
|
||||
default:
|
||||
return dot, false
|
||||
}
|
||||
return dot, true
|
||||
}
|
||||
|
||||
func (txt *Text) drawBuf() {
|
||||
if !utf8.FullRune(txt.buf) {
|
||||
return
|
||||
}
|
||||
|
||||
rgba := pixel.ToRGBA(txt.Color)
|
||||
for i := range txt.glyph {
|
||||
txt.glyph[i].Color = rgba
|
||||
}
|
||||
|
||||
for utf8.FullRune(txt.buf) {
|
||||
r, size := utf8.DecodeRune(txt.buf)
|
||||
txt.buf = txt.buf[size:]
|
||||
|
||||
var control bool
|
||||
txt.Dot, control = txt.controlRune(r, txt.Dot)
|
||||
if control {
|
||||
continue
|
||||
}
|
||||
|
||||
var rect, frame, bounds pixel.Rect
|
||||
rect, frame, bounds, txt.Dot = txt.Atlas().DrawRune(txt.prevR, r, txt.Dot)
|
||||
|
||||
txt.prevR = r
|
||||
|
||||
rv := [...]pixel.Vec{
|
||||
{X: rect.Min.X, Y: rect.Min.Y},
|
||||
{X: rect.Max.X, Y: rect.Min.Y},
|
||||
{X: rect.Max.X, Y: rect.Max.Y},
|
||||
{X: rect.Min.X, Y: rect.Max.Y},
|
||||
}
|
||||
|
||||
fv := [...]pixel.Vec{
|
||||
{X: frame.Min.X, Y: frame.Min.Y},
|
||||
{X: frame.Max.X, Y: frame.Min.Y},
|
||||
{X: frame.Max.X, Y: frame.Max.Y},
|
||||
{X: frame.Min.X, Y: frame.Max.Y},
|
||||
}
|
||||
|
||||
for i, j := range [...]int{0, 1, 2, 0, 2, 3} {
|
||||
txt.glyph[i].Position = rv[j]
|
||||
txt.glyph[i].Picture = fv[j]
|
||||
}
|
||||
|
||||
txt.tris = append(txt.tris, txt.glyph...)
|
||||
txt.dirty = true
|
||||
|
||||
if txt.bounds.W()*txt.bounds.H() == 0 {
|
||||
txt.bounds = bounds
|
||||
} else {
|
||||
txt.bounds = txt.bounds.Union(bounds)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,87 @@
|
|||
package text_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"testing"
|
||||
"unicode"
|
||||
|
||||
"golang.org/x/image/font/basicfont"
|
||||
"golang.org/x/image/font/gofont/goregular"
|
||||
|
||||
"github.com/faiface/pixel"
|
||||
"github.com/faiface/pixel/text"
|
||||
"github.com/golang/freetype/truetype"
|
||||
)
|
||||
|
||||
func TestClear(t *testing.T) {
|
||||
txt := text.New(pixel.ZV, text.Atlas7x13)
|
||||
|
||||
if got, want := txt.Dot, pixel.ZV; !eqVectors(got, want) {
|
||||
t.Fatalf("txt.Dot = %v, want %v", got, want)
|
||||
}
|
||||
|
||||
fmt.Fprint(txt, "Test\nClear")
|
||||
|
||||
if got, want := txt.Dot, pixel.V(35, -13); !eqVectors(got, want) {
|
||||
t.Fatalf("txt.Dot = %v, want %v", got, want)
|
||||
}
|
||||
|
||||
txt.Clear()
|
||||
|
||||
if got, want := txt.Dot, pixel.ZV; !eqVectors(got, want) {
|
||||
t.Fatalf("txt.Dot = %v, want %v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkNewAtlas(b *testing.B) {
|
||||
runeSets := []struct {
|
||||
name string
|
||||
set []rune
|
||||
}{
|
||||
{"ASCII", text.ASCII},
|
||||
{"Latin", text.RangeTable(unicode.Latin)},
|
||||
}
|
||||
|
||||
ttf, _ := truetype.Parse(goregular.TTF)
|
||||
face := truetype.NewFace(ttf, &truetype.Options{
|
||||
Size: 16,
|
||||
GlyphCacheEntries: 1,
|
||||
})
|
||||
|
||||
for _, runeSet := range runeSets {
|
||||
b.Run(runeSet.name, func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = text.NewAtlas(face, runeSet.set)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkTextWrite(b *testing.B) {
|
||||
runeSet := text.ASCII
|
||||
atlas := text.NewAtlas(basicfont.Face7x13, runeSet)
|
||||
|
||||
lengths := []int{1, 10, 100, 1000}
|
||||
chunks := make([][]byte, len(lengths))
|
||||
for i := range chunks {
|
||||
chunk := make([]rune, lengths[i])
|
||||
for j := range chunk {
|
||||
chunk[j] = runeSet[rand.Intn(len(runeSet))]
|
||||
}
|
||||
chunks[i] = []byte(string(chunk))
|
||||
}
|
||||
|
||||
for _, chunk := range chunks {
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func eqVectors(a, b pixel.Vec) bool {
|
||||
return (a.X == b.X && a.Y == b.Y)
|
||||
}
|