Compare commits
113 Commits
Author | SHA1 | Date |
---|---|---|
|
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 |
34
.travis.yml
|
@ -1,34 +0,0 @@
|
||||||
language: go
|
|
||||||
# https://github.com/golang/go/issues/31293
|
|
||||||
dist: xenial
|
|
||||||
sudo: false
|
|
||||||
addons:
|
|
||||||
apt:
|
|
||||||
packages:
|
|
||||||
- xorg-dev
|
|
||||||
- libx11-dev
|
|
||||||
- libxrandr-dev
|
|
||||||
- libxinerama-dev
|
|
||||||
- libxcursor-dev
|
|
||||||
- libxi-dev
|
|
||||||
- libopenal-dev
|
|
||||||
- libasound2-dev
|
|
||||||
- libgl1-mesa-dev
|
|
||||||
|
|
||||||
services:
|
|
||||||
- xvfb
|
|
||||||
|
|
||||||
env:
|
|
||||||
- GO111MODULE=on
|
|
||||||
|
|
||||||
go:
|
|
||||||
- tip
|
|
||||||
- 1.12.x
|
|
||||||
|
|
||||||
install:
|
|
||||||
- # Do nothing. This is needed to prevent the default install action
|
|
||||||
# "go get -t -v ./..." from happening here because we want it to happen
|
|
||||||
# inside script step.
|
|
||||||
|
|
||||||
script:
|
|
||||||
- go test -v -race -mod=readonly ./...
|
|
63
CHANGELOG.md
|
@ -1,63 +0,0 @@
|
||||||
# Changelog
|
|
||||||
All notable changes to this project will be documented in this file.
|
|
||||||
|
|
||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
||||||
|
|
||||||
## [Unreleased]
|
|
||||||
- Add AnchorPos struct and functions #252
|
|
||||||
- Add Clipboard Support
|
|
||||||
- Fix SIGSEGV on text.NewAtlas if glyph absent
|
|
||||||
- Use slice for range in Drawer.Dirty(), to improve performance
|
|
||||||
- GLTriangle's fragment shader is used when rendered by the Canvas.
|
|
||||||
- Add MSAA support
|
|
||||||
|
|
||||||
## [v0.10.0] 2020-08-22
|
|
||||||
- Add AnchorPos struct and functions
|
|
||||||
- Gamepad API added
|
|
||||||
- Support setting an initial window position
|
|
||||||
- Support hiding the window initially
|
|
||||||
- Support creating maximized windows
|
|
||||||
- Support waiting for events to reduce CPU load
|
|
||||||
- Adding clipping rectangle support in GLTriangles
|
|
||||||
|
|
||||||
## [v0.10.0-beta] 2020-05-10
|
|
||||||
- Add `WindowConfig.TransparentFramebuffer` option to support window transparency onto the background
|
|
||||||
- Fixed Line intersects failing on lines passing through (0, 0)
|
|
||||||
|
|
||||||
## [v0.10.0-alpha] 2020-05-08
|
|
||||||
- Upgrade to GLFW 3.3! :tada:
|
|
||||||
- Closes https://github.com/faiface/pixel/issues/137
|
|
||||||
- Add support for glfw's DisableCursor
|
|
||||||
- Closes https://github.com/faiface/pixel/issues/213
|
|
||||||
|
|
||||||
## [v0.9.0] - 2020-05-02
|
|
||||||
- Added feature from https://github.com/faiface/pixel/pull/219
|
|
||||||
- Exposing Window.SwapBuffers so buffers can be swapped without polling input
|
|
||||||
- Add more examples
|
|
||||||
- Add position as out variable from vertex shader
|
|
||||||
- Add experimental joystick support
|
|
||||||
- Add mouse cursor operations
|
|
||||||
- Add `Vec.Floor(…)` function
|
|
||||||
- Add circle geometry
|
|
||||||
- Fix `Matrix.Unproject(…)` for rotated matrix
|
|
||||||
- Add 2D Line geometry
|
|
||||||
- Add floating point round error correction
|
|
||||||
- Performance improvements
|
|
||||||
- Fix race condition in `NewGLTriangles(…)`
|
|
||||||
- Add `TriangleData` benchmarks and improvements
|
|
||||||
- Add zero rectangle variable for utility and consistency
|
|
||||||
- Add support for Go Modules
|
|
||||||
- Add `NoIconify` and `AlwaysOnTop` window hints
|
|
||||||
|
|
||||||
|
|
||||||
## [v0.8.0] - 2018-10-10
|
|
||||||
Changelog for this and older versions can be found on the corresponding [GitHub
|
|
||||||
releases](https://github.com/faiface/pixel/releases).
|
|
||||||
|
|
||||||
[Unreleased]: https://github.com/faiface/pixel/compare/v0.10.0...HEAD
|
|
||||||
[v0.10.0]: https://github.com/faiface/pixel/compare/v0.10.0-beta...v0.10.0
|
|
||||||
[v0.10.0-beta]: https://github.com/faiface/pixel/compare/v0.10.0-alpha...v0.10.0-beta
|
|
||||||
[v0.10.0-alpha]: https://github.com/faiface/pixel/compare/v0.9.0...v0.10.0-alpha
|
|
||||||
[v0.9.0]: https://github.com/faiface/pixel/compare/v0.8.0...v0.9.0
|
|
||||||
[v0.8.0]: https://github.com/faiface/pixel/releases/tag/v0.8.0
|
|
|
@ -1,16 +0,0 @@
|
||||||
# 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 `community` folder of the [examples repository][examples].
|
|
||||||
2. **Add tests**. There only few tests in Pixel at the moment. Take a look at them and make some similar.
|
|
||||||
3. **Add a small feature or an improvement**. Feel like some small feature is missing? Just make a PR. Be ready that I might reject it, though, if I don't find it particularly appealing.
|
|
||||||
4. **Join the big development** by joining the discussion on our [Discord Server](https://discord.gg/q2DK4MP), where we can discuss bigger changes and implement them after that.
|
|
||||||
|
|
||||||
## How to make a pull request
|
|
||||||
|
|
||||||
Go gives you a nice surprise when attempting to make a PR on Github. The thing is, that when user _xyz_ forks Pixel on Github, it ends up in _github.com/xyz/pixel_, which fucks up your import paths. Here's how you deal with that: https://www.reddit.com/r/golang/comments/2jdcw1/how_do_you_deal_with_github_forking/.
|
|
||||||
|
|
||||||
[examples]: https://github.com/faiface/pixel-examples/tree/master/community
|
|
85
README.md
|
@ -1,12 +1,4 @@
|
||||||
# \*\*\*\*\*NOTICE\*\*\*\*\*
|
# 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)
|
||||||
|
|
||||||
This repo is not under active development anymore and has been archived. Continued development has been migrated to [Pixel2](https://github.com/gopxl/pixel). A big thank you to [faiface](https://github.com/faiface) for creating this awesome library and for all the hard work put into it. We encourage old and new users to check out the new repo and contribute to it.
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<p align="center"><img src="logo/LOGOTYPE-HORIZONTAL-BLUE.png"></p>
|
|
||||||
|
|
||||||
# Pixel [](https://travis-ci.org/faiface/pixel) [](https://godoc.org/github.com/faiface/pixel) [](https://goreportcard.com/report/github.com/faiface/pixel) [](https://gitter.im/pixellib/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [](https://discord.gg/q2DK4MP)
|
|
||||||
|
|
||||||
A hand-crafted 2D game library in Go. Take a look into the [features](#features) to see what it can
|
A hand-crafted 2D game library in Go. Take a look into the [features](#features) to see what it can
|
||||||
do.
|
do.
|
||||||
|
@ -15,18 +7,8 @@ do.
|
||||||
go get github.com/faiface/pixel
|
go get github.com/faiface/pixel
|
||||||
```
|
```
|
||||||
|
|
||||||
If you are using Modules (Go 1.11 or higher) and want a mutable copy of the source code:
|
|
||||||
|
|
||||||
```
|
|
||||||
git clone https://github.com/faiface/pixel # clone outside of $GOPATH
|
|
||||||
cd pixel
|
|
||||||
go install ./...
|
|
||||||
```
|
|
||||||
|
|
||||||
See [requirements](#requirements) for the list of libraries necessary for compilation.
|
See [requirements](#requirements) for the list of libraries necessary for compilation.
|
||||||
|
|
||||||
All significant changes are documented in [CHANGELOG.md](CHANGELOG.md).
|
|
||||||
|
|
||||||
## Tutorial
|
## Tutorial
|
||||||
|
|
||||||
The [Wiki of this repo](https://github.com/faiface/pixel/wiki) contains an extensive tutorial
|
The [Wiki of this repo](https://github.com/faiface/pixel/wiki) contains an extensive tutorial
|
||||||
|
@ -38,34 +20,28 @@ covering several topics of Pixel. Here's the content of the tutorial parts so fa
|
||||||
- [Pressing keys and clicking mouse](https://github.com/faiface/pixel/wiki/Pressing-keys-and-clicking-mouse)
|
- [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 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)
|
- [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)
|
|
||||||
- [Using a custom fragment shader](https://github.com/faiface/pixel/wiki/Using-a-custom-fragment-shader)
|
|
||||||
|
|
||||||
## [Examples](https://github.com/faiface/pixel-examples)
|
## Examples
|
||||||
|
|
||||||
The [examples](https://github.com/faiface/pixel-examples) repository contains a few
|
The [examples](https://github.com/faiface/pixel/tree/master/examples) directory contains a few
|
||||||
examples demonstrating Pixel's functionality.
|
examples demonstrating Pixel's functionality.
|
||||||
|
|
||||||
**To run an example**, navigate to it's directory, then `go run` the `main.go` file. For example:
|
**To run an example**, navigate to it's directory, then `go run` the `main.go` file. For example:
|
||||||
|
|
||||||
```
|
```
|
||||||
$ cd pixel-examples/platformer
|
$ cd examples/platformer
|
||||||
$ go run main.go
|
$ go run main.go
|
||||||
```
|
```
|
||||||
|
|
||||||
Here are some screenshots from the examples!
|
Here are some screenshots from the examples!
|
||||||
|
|
||||||
| [Lights](https://github.com/faiface/pixel-examples/blob/master/lights) | [Platformer](https://github.com/faiface/pixel-examples/blob/master/platformer) |
|
| [Lights](examples/lights) | [Platformer](examples/platformer) |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
|  |  |
|
|  |  |
|
||||||
|
|
||||||
| [Smoke](https://github.com/faiface/pixel-examples/blob/master/smoke) | [Typewriter](https://github.com/faiface/pixel-examples/blob/master/typewriter) |
|
| [Smoke](examples/smoke) | [Xor](examples/xor) |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
|  |  |
|
|  |  |
|
||||||
|
|
||||||
| [Raycaster](https://github.com/faiface/pixel-examples/blob/master/community/raycaster) | [Gizmo](https://github.com/Lallassu/gizmo) |
|
|
||||||
| --- | --- |
|
|
||||||
|  |  |
|
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
|
@ -78,16 +54,15 @@ Here's the list of the main features in Pixel. Although Pixel is still under hea
|
||||||
[IMDraw](https://github.com/faiface/pixel/wiki/Drawing-shapes-with-IMDraw) (circles, rectangles,
|
[IMDraw](https://github.com/faiface/pixel/wiki/Drawing-shapes-with-IMDraw) (circles, rectangles,
|
||||||
lines, ...)
|
lines, ...)
|
||||||
- Optimized drawing with [Batch](https://github.com/faiface/pixel/wiki/Drawing-efficiently-with-Batch)
|
- 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
|
- Simple and convenient API
|
||||||
- Drawing a sprite to a window is as simple as `sprite.Draw(window, matrix)`
|
- Drawing a sprite to a window is as simple as `sprite.Draw(window)`
|
||||||
|
- Adding and subtracting vectors with `+` and `-` operators... how?
|
||||||
- Wanna know where the center of a window is? `window.Bounds().Center()`
|
- Wanna know where the center of a window is? `window.Bounds().Center()`
|
||||||
- [...](https://godoc.org/github.com/faiface/pixel)
|
- [...](https://godoc.org/github.com/faiface/pixel)
|
||||||
- Full documentation and tutorial
|
- Full documentation and tutorial
|
||||||
- Works on Linux, macOS and Windows
|
- Works on Linux, macOS and Windows
|
||||||
- Window creation and manipulation (resizing, fullscreen, multiple windows, ...)
|
- Window creation and manipulation (resizing, fullscreen, multiple windows, ...)
|
||||||
- Keyboard (key presses, text input) and mouse input without events
|
- Keyboard and mouse input without events
|
||||||
- Well integrated with the Go standard library
|
- Well integrated with the Go standard library
|
||||||
- Use `"image"` package for loading pictures
|
- Use `"image"` package for loading pictures
|
||||||
- Use `"time"` package for measuring delta time and FPS
|
- Use `"time"` package for measuring delta time and FPS
|
||||||
|
@ -111,58 +86,37 @@ Here's the list of the main features in Pixel. Although Pixel is still under hea
|
||||||
- Small codebase, ~5K lines of code, including the backend [glhf](https://github.com/faiface/glhf)
|
- Small codebase, ~5K lines of code, including the backend [glhf](https://github.com/faiface/glhf)
|
||||||
package
|
package
|
||||||
|
|
||||||
## Related repositories
|
|
||||||
|
|
||||||
Here are some packages which use Pixel:
|
|
||||||
- [TilePix](https://github.com/bcvery1/tilepix) Makes handling TMX files built with [Tiled](https://www.mapeditor.org/) trivially easy to work with using Pixel.
|
|
||||||
- [spriteplus](https://github.com/cebarks/spriteplus) Basic `SpriteSheet` and `Animation` implementations
|
|
||||||
- [PixelUI](https://github.com/dusk125/pixelui) Imgui-based GUIs for Pixel
|
|
||||||
- [pixelutils](https://github.com/dusk125/pixelutils) Variety of game related utilities (sprite packer, id generator, ticker, sprite loader, voronoia diagrams)
|
|
||||||
|
|
||||||
## Missing features
|
## Missing features
|
||||||
|
|
||||||
Pixel is in development and still missing few critical features. Here're the most critical ones.
|
Pixel is in development and still missing few critical features. Here're the most critical ones.
|
||||||
|
|
||||||
- ~~Audio~~
|
- Audio
|
||||||
- ~~Drawing text~~
|
- Drawing text
|
||||||
- Antialiasing (filtering is supported, though)
|
- Antialiasing (filtering is supported, though)
|
||||||
- ~~Advanced window manipulation (cursor hiding, window icon, ...)~~
|
- Advanced window manipulation (cursor hiding, window icon, ...)
|
||||||
- Better support for Hi-DPI displays
|
- Better support for Hi-DPI displays
|
||||||
- Mobile (and perhaps HTML5?) backend
|
- Mobile (and perhaps HTML5?) backend
|
||||||
- ~~More advanced graphical effects (e.g. blur)~~ (solved with the addition of GLSL effects)
|
- More advanced graphical effects (e.g. blur)
|
||||||
- Tests and benchmarks
|
- Tests and benchmarks
|
||||||
- Vulkan support
|
|
||||||
|
|
||||||
**Implementing these features will get us to the 1.0 release.** Contribute, so that it's as soon as
|
**Implementing these features will get us to the 1.0 release.** Contribute, so that it's as soon as
|
||||||
possible!
|
possible!
|
||||||
|
|
||||||
## Requirements
|
## 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
|
[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
|
graphics. Because of that, OpenGL development libraries are needed for compilation. The dependencies
|
||||||
are same as for [GLFW](https://github.com/go-gl/glfw).
|
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
|
- On macOS, you need Xcode or Command Line Tools for Xcode (`xcode-select --install`) for required
|
||||||
headers and libraries.
|
headers and libraries.
|
||||||
- On Ubuntu/Debian-like Linux distributions, you need `libgl1-mesa-dev` and `xorg-dev` packages.
|
- 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
|
- On CentOS/Fedora-like Linux distributions, you need `libX11-devel libXcursor-devel libXrandr-devel
|
||||||
libXinerama-devel mesa-libGL-devel libXi-devel libXxf86vm-devel` packages.
|
libXinerama-devel mesa-libGL-devel libXi-devel` packages.
|
||||||
- See [here](http://www.glfw.org/docs/latest/compile.html#compile_deps) for full details.
|
- 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
|
## Contributing
|
||||||
|
|
||||||
Join us in the [Discord Chat!](https://discord.gg/q2DK4MP)
|
|
||||||
|
|
||||||
Pixel is in, let's say, mid-stage of development. Many of the important features are here, some are
|
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
|
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
|
finish the library, but it'll take a lot of time. With your help, it'll take much less. I encourage
|
||||||
|
@ -178,7 +132,12 @@ requests. It just means that I might not like your idea. Or that your pull reque
|
||||||
rewriting. That's perfectly fine, don't let it put you off. In the end, we'll just end up with a
|
rewriting. That's perfectly fine, don't let it put you off. In the end, we'll just end up with a
|
||||||
better result.
|
better result.
|
||||||
|
|
||||||
Take a look at [CONTRIBUTING.md](CONTRIBUTING.md) for further information.
|
**Don't start working on a pull request before submiting an issue or commenting on one. Proposals
|
||||||
|
also take the form of issues.**
|
||||||
|
|
||||||
|
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
|
## License
|
||||||
|
|
||||||
|
|
2
batch.go
|
@ -26,7 +26,7 @@ var _ BasicTarget = (*Batch)(nil)
|
||||||
//
|
//
|
||||||
// Note, that if the container does not support TrianglesColor, color masking will not work.
|
// Note, that if the container does not support TrianglesColor, color masking will not work.
|
||||||
func NewBatch(container Triangles, pic Picture) *Batch {
|
func NewBatch(container Triangles, pic Picture) *Batch {
|
||||||
b := &Batch{cont: Drawer{Triangles: container, Picture: pic, Cached: true}}
|
b := &Batch{cont: Drawer{Triangles: container, Picture: pic}}
|
||||||
b.SetMatrix(IM)
|
b.SetMatrix(IM)
|
||||||
b.SetColorMask(Alpha(1))
|
b.SetColorMask(Alpha(1))
|
||||||
return b
|
return b
|
||||||
|
|
334
circle.go
|
@ -1,334 +0,0 @@
|
||||||
package pixel
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"math"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Circle is a 2D circle. It is defined by two properties:
|
|
||||||
// - Center vector
|
|
||||||
// - Radius float64
|
|
||||||
type Circle struct {
|
|
||||||
Center Vec
|
|
||||||
Radius float64
|
|
||||||
}
|
|
||||||
|
|
||||||
// C returns a new Circle with the given radius and center coordinates.
|
|
||||||
//
|
|
||||||
// Note that a negative radius is valid.
|
|
||||||
func C(center Vec, radius float64) Circle {
|
|
||||||
return Circle{
|
|
||||||
Center: center,
|
|
||||||
Radius: radius,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// String returns the string representation of the Circle.
|
|
||||||
//
|
|
||||||
// c := pixel.C(10.1234, pixel.ZV)
|
|
||||||
// c.String() // returns "Circle(10.12, Vec(0, 0))"
|
|
||||||
// fmt.Println(c) // Circle(10.12, Vec(0, 0))
|
|
||||||
func (c Circle) String() string {
|
|
||||||
return fmt.Sprintf("Circle(%s, %.2f)", c.Center, c.Radius)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Norm returns the Circle in normalized form - this sets the radius to its absolute value.
|
|
||||||
//
|
|
||||||
// c := pixel.C(-10, pixel.ZV)
|
|
||||||
// c.Norm() // returns pixel.Circle{pixel.Vec{0, 0}, 10}
|
|
||||||
func (c Circle) Norm() Circle {
|
|
||||||
return Circle{
|
|
||||||
Center: c.Center,
|
|
||||||
Radius: math.Abs(c.Radius),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Area returns the area of the Circle.
|
|
||||||
func (c Circle) Area() float64 {
|
|
||||||
return math.Pi * math.Pow(c.Radius, 2)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Moved returns the Circle moved by the given vector delta.
|
|
||||||
func (c Circle) Moved(delta Vec) Circle {
|
|
||||||
return Circle{
|
|
||||||
Center: c.Center.Add(delta),
|
|
||||||
Radius: c.Radius,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Resized returns the Circle resized by the given delta. The Circles center is use as the anchor.
|
|
||||||
//
|
|
||||||
// c := pixel.C(pixel.ZV, 10)
|
|
||||||
// c.Resized(-5) // returns pixel.Circle{pixel.Vec{0, 0}, 5}
|
|
||||||
// c.Resized(25) // returns pixel.Circle{pixel.Vec{0, 0}, 35}
|
|
||||||
func (c Circle) Resized(radiusDelta float64) Circle {
|
|
||||||
return Circle{
|
|
||||||
Center: c.Center,
|
|
||||||
Radius: c.Radius + radiusDelta,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Contains checks whether a vector `u` is contained within this Circle (including it's perimeter).
|
|
||||||
func (c Circle) Contains(u Vec) bool {
|
|
||||||
toCenter := c.Center.To(u)
|
|
||||||
return c.Radius >= toCenter.Len()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Formula returns the values of h and k, for the equation of the circle: (x-h)^2 + (y-k)^2 = r^2
|
|
||||||
// where r is the radius of the circle.
|
|
||||||
func (c Circle) Formula() (h, k float64) {
|
|
||||||
return c.Center.X, c.Center.Y
|
|
||||||
}
|
|
||||||
|
|
||||||
// maxCircle will return the larger circle based on the radius.
|
|
||||||
func maxCircle(c, d Circle) Circle {
|
|
||||||
if c.Radius < d.Radius {
|
|
||||||
return d
|
|
||||||
}
|
|
||||||
return c
|
|
||||||
}
|
|
||||||
|
|
||||||
// minCircle will return the smaller circle based on the radius.
|
|
||||||
func minCircle(c, d Circle) Circle {
|
|
||||||
if c.Radius < d.Radius {
|
|
||||||
return c
|
|
||||||
}
|
|
||||||
return d
|
|
||||||
}
|
|
||||||
|
|
||||||
// Union returns the minimal Circle which covers both `c` and `d`.
|
|
||||||
func (c Circle) Union(d Circle) Circle {
|
|
||||||
biggerC := maxCircle(c.Norm(), d.Norm())
|
|
||||||
smallerC := minCircle(c.Norm(), d.Norm())
|
|
||||||
|
|
||||||
// Get distance between centers
|
|
||||||
dist := c.Center.To(d.Center).Len()
|
|
||||||
|
|
||||||
// If the bigger Circle encompasses the smaller one, we have the result
|
|
||||||
if dist+smallerC.Radius <= biggerC.Radius {
|
|
||||||
return biggerC
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate radius for encompassing Circle
|
|
||||||
r := (dist + biggerC.Radius + smallerC.Radius) / 2
|
|
||||||
|
|
||||||
// Calculate center for encompassing Circle
|
|
||||||
theta := .5 + (biggerC.Radius-smallerC.Radius)/(2*dist)
|
|
||||||
center := Lerp(smallerC.Center, biggerC.Center, theta)
|
|
||||||
|
|
||||||
return Circle{
|
|
||||||
Center: center,
|
|
||||||
Radius: r,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Intersect returns the maximal Circle which is covered by both `c` and `d`.
|
|
||||||
//
|
|
||||||
// If `c` and `d` don't overlap, this function returns a zero-sized circle at the centerpoint between the two Circle's
|
|
||||||
// centers.
|
|
||||||
func (c Circle) Intersect(d Circle) Circle {
|
|
||||||
// Check if one of the circles encompasses the other; if so, return that one
|
|
||||||
biggerC := maxCircle(c.Norm(), d.Norm())
|
|
||||||
smallerC := minCircle(c.Norm(), d.Norm())
|
|
||||||
|
|
||||||
if biggerC.Radius >= biggerC.Center.To(smallerC.Center).Len()+smallerC.Radius {
|
|
||||||
return biggerC
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate the midpoint between the two radii
|
|
||||||
// Distance between centers
|
|
||||||
dist := c.Center.To(d.Center).Len()
|
|
||||||
// Difference between radii
|
|
||||||
diff := dist - (c.Radius + d.Radius)
|
|
||||||
// Distance from c.Center to the weighted midpoint
|
|
||||||
distToMidpoint := c.Radius + 0.5*diff
|
|
||||||
// Weighted midpoint
|
|
||||||
center := Lerp(c.Center, d.Center, distToMidpoint/dist)
|
|
||||||
|
|
||||||
// No need to calculate radius if the circles do not overlap
|
|
||||||
if c.Center.To(d.Center).Len() >= c.Radius+d.Radius {
|
|
||||||
return C(center, 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
radius := c.Center.To(d.Center).Len() - (c.Radius + d.Radius)
|
|
||||||
|
|
||||||
return Circle{
|
|
||||||
Center: center,
|
|
||||||
Radius: math.Abs(radius),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// IntersectLine will return the shortest Vec such that if the Circle is moved by the Vec returned, the Line and Rect no
|
|
||||||
// longer intersect.
|
|
||||||
func (c Circle) IntersectLine(l Line) Vec {
|
|
||||||
return l.IntersectCircle(c).Scaled(-1)
|
|
||||||
}
|
|
||||||
|
|
||||||
// IntersectRect returns a minimal required Vector, such that moving the circle by that vector would stop the Circle
|
|
||||||
// and the Rect intersecting. This function returns a zero-vector if the Circle and Rect do not overlap, and if only
|
|
||||||
// the perimeters touch.
|
|
||||||
//
|
|
||||||
// This function will return a non-zero vector if:
|
|
||||||
// - The Rect contains the Circle, partially or fully
|
|
||||||
// - The Circle contains the Rect, partially of fully
|
|
||||||
func (c Circle) IntersectRect(r Rect) Vec {
|
|
||||||
// Checks if the c.Center is not in the diagonal quadrants of the rectangle
|
|
||||||
if (r.Min.X <= c.Center.X && c.Center.X <= r.Max.X) || (r.Min.Y <= c.Center.Y && c.Center.Y <= r.Max.Y) {
|
|
||||||
// 'grow' the Rect by c.Radius in each orthagonal
|
|
||||||
grown := Rect{Min: r.Min.Sub(V(c.Radius, c.Radius)), Max: r.Max.Add(V(c.Radius, c.Radius))}
|
|
||||||
if !grown.Contains(c.Center) {
|
|
||||||
// c.Center not close enough to overlap, return zero-vector
|
|
||||||
return ZV
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get minimum distance to travel out of Rect
|
|
||||||
rToC := r.Center().To(c.Center)
|
|
||||||
h := c.Radius - math.Abs(rToC.X) + (r.W() / 2)
|
|
||||||
v := c.Radius - math.Abs(rToC.Y) + (r.H() / 2)
|
|
||||||
|
|
||||||
if rToC.X < 0 {
|
|
||||||
h = -h
|
|
||||||
}
|
|
||||||
if rToC.Y < 0 {
|
|
||||||
v = -v
|
|
||||||
}
|
|
||||||
|
|
||||||
// No intersect
|
|
||||||
if h == 0 && v == 0 {
|
|
||||||
return ZV
|
|
||||||
}
|
|
||||||
|
|
||||||
if math.Abs(h) > math.Abs(v) {
|
|
||||||
// Vertical distance shorter
|
|
||||||
return V(0, v)
|
|
||||||
}
|
|
||||||
return V(h, 0)
|
|
||||||
} else {
|
|
||||||
// The center is in the diagonal quadrants
|
|
||||||
|
|
||||||
// Helper points to make code below easy to read.
|
|
||||||
rectTopLeft := V(r.Min.X, r.Max.Y)
|
|
||||||
rectBottomRight := V(r.Max.X, r.Min.Y)
|
|
||||||
|
|
||||||
// Check for overlap.
|
|
||||||
if !(c.Contains(r.Min) || c.Contains(r.Max) || c.Contains(rectTopLeft) || c.Contains(rectBottomRight)) {
|
|
||||||
// No overlap.
|
|
||||||
return ZV
|
|
||||||
}
|
|
||||||
|
|
||||||
var centerToCorner Vec
|
|
||||||
if c.Center.To(r.Min).Len() <= c.Radius {
|
|
||||||
// Closest to bottom-left
|
|
||||||
centerToCorner = c.Center.To(r.Min)
|
|
||||||
}
|
|
||||||
if c.Center.To(r.Max).Len() <= c.Radius {
|
|
||||||
// Closest to top-right
|
|
||||||
centerToCorner = c.Center.To(r.Max)
|
|
||||||
}
|
|
||||||
if c.Center.To(rectTopLeft).Len() <= c.Radius {
|
|
||||||
// Closest to top-left
|
|
||||||
centerToCorner = c.Center.To(rectTopLeft)
|
|
||||||
}
|
|
||||||
if c.Center.To(rectBottomRight).Len() <= c.Radius {
|
|
||||||
// Closest to bottom-right
|
|
||||||
centerToCorner = c.Center.To(rectBottomRight)
|
|
||||||
}
|
|
||||||
|
|
||||||
cornerToCircumferenceLen := c.Radius - centerToCorner.Len()
|
|
||||||
|
|
||||||
return centerToCorner.Unit().Scaled(cornerToCircumferenceLen)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// IntersectionPoints returns all the points where the Circle intersects with the line provided. This can be zero, one or
|
|
||||||
// two points, depending on the location of the shapes. The points of intersection will be returned in order of
|
|
||||||
// closest-to-l.A to closest-to-l.B.
|
|
||||||
func (c Circle) IntersectionPoints(l Line) []Vec {
|
|
||||||
cContainsA := c.Contains(l.A)
|
|
||||||
cContainsB := c.Contains(l.B)
|
|
||||||
|
|
||||||
// Special case for both endpoint being contained within the circle
|
|
||||||
if cContainsA && cContainsB {
|
|
||||||
return []Vec{}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get closest point on the line to this circles' center
|
|
||||||
closestToCenter := l.Closest(c.Center)
|
|
||||||
|
|
||||||
// If the distance to the closest point is greater than the radius, there are no points of intersection
|
|
||||||
if closestToCenter.To(c.Center).Len() > c.Radius {
|
|
||||||
return []Vec{}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the distance to the closest point is equal to the radius, the line is tangent and the closest point is the
|
|
||||||
// point at which it touches the circle.
|
|
||||||
if closestToCenter.To(c.Center).Len() == c.Radius {
|
|
||||||
return []Vec{closestToCenter}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Special case for endpoint being on the circles' center
|
|
||||||
if c.Center == l.A || c.Center == l.B {
|
|
||||||
otherEnd := l.B
|
|
||||||
if c.Center == l.B {
|
|
||||||
otherEnd = l.A
|
|
||||||
}
|
|
||||||
intersect := c.Center.Add(c.Center.To(otherEnd).Unit().Scaled(c.Radius))
|
|
||||||
return []Vec{intersect}
|
|
||||||
}
|
|
||||||
|
|
||||||
// This means the distance to the closest point is less than the radius, so there is at least one intersection,
|
|
||||||
// possibly two.
|
|
||||||
|
|
||||||
// If one of the end points exists within the circle, there is only one intersection
|
|
||||||
if cContainsA || cContainsB {
|
|
||||||
containedPoint := l.A
|
|
||||||
otherEnd := l.B
|
|
||||||
if cContainsB {
|
|
||||||
containedPoint = l.B
|
|
||||||
otherEnd = l.A
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use trigonometry to get the length of the line between the contained point and the intersection point.
|
|
||||||
// The following is used to describe the triangle formed:
|
|
||||||
// - a is the side between contained point and circle center
|
|
||||||
// - b is the side between the center and the intersection point (radius)
|
|
||||||
// - c is the side between the contained point and the intersection point
|
|
||||||
// The captials of these letters are used as the angles opposite the respective sides.
|
|
||||||
// a and b are known
|
|
||||||
a := containedPoint.To(c.Center).Len()
|
|
||||||
b := c.Radius
|
|
||||||
// B can be calculated by subtracting the angle of b (to the x-axis) from the angle of c (to the x-axis)
|
|
||||||
B := containedPoint.To(c.Center).Angle() - containedPoint.To(otherEnd).Angle()
|
|
||||||
// Using the Sin rule we can get A
|
|
||||||
A := math.Asin((a * math.Sin(B)) / b)
|
|
||||||
// Using the rule that there are 180 degrees (or Pi radians) in a triangle, we can now get C
|
|
||||||
C := math.Pi - A + B
|
|
||||||
// If C is zero, the line segment is in-line with the center-intersect line.
|
|
||||||
var c float64
|
|
||||||
if C == 0 {
|
|
||||||
c = b - a
|
|
||||||
} else {
|
|
||||||
// Using the Sine rule again, we can now get c
|
|
||||||
c = (a * math.Sin(C)) / math.Sin(A)
|
|
||||||
}
|
|
||||||
// Travelling from the contained point to the other end by length of a will provide the intersection point.
|
|
||||||
return []Vec{
|
|
||||||
containedPoint.Add(containedPoint.To(otherEnd).Unit().Scaled(c)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Otherwise the endpoints exist outside of the circle, and the line segment intersects in two locations.
|
|
||||||
// The vector formed by going from the closest point to the center of the circle will be perpendicular to the line;
|
|
||||||
// this forms a right-angled triangle with the intersection points, with the radius as the hypotenuse.
|
|
||||||
// Calculate the other triangles' sides' length.
|
|
||||||
a := math.Sqrt(math.Pow(c.Radius, 2) - math.Pow(closestToCenter.To(c.Center).Len(), 2))
|
|
||||||
|
|
||||||
// Travelling in both directions from the closest point by length of a will provide the two intersection points.
|
|
||||||
first := closestToCenter.Add(closestToCenter.To(l.A).Unit().Scaled(a))
|
|
||||||
second := closestToCenter.Add(closestToCenter.To(l.B).Unit().Scaled(a))
|
|
||||||
|
|
||||||
if first.To(l.A).Len() < second.To(l.A).Len() {
|
|
||||||
return []Vec{first, second}
|
|
||||||
}
|
|
||||||
return []Vec{second, first}
|
|
||||||
}
|
|
466
circle_test.go
|
@ -1,466 +0,0 @@
|
||||||
package pixel_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"math"
|
|
||||||
"reflect"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/faiface/pixel"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestC(t *testing.T) {
|
|
||||||
type args struct {
|
|
||||||
radius float64
|
|
||||||
center pixel.Vec
|
|
||||||
}
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
args args
|
|
||||||
want pixel.Circle
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "C(): positive radius",
|
|
||||||
args: args{radius: 10, center: pixel.ZV},
|
|
||||||
want: pixel.Circle{Radius: 10, Center: pixel.ZV},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "C(): zero radius",
|
|
||||||
args: args{radius: 0, center: pixel.ZV},
|
|
||||||
want: pixel.Circle{Radius: 0, Center: pixel.ZV},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "C(): negative radius",
|
|
||||||
args: args{radius: -5, center: pixel.ZV},
|
|
||||||
want: pixel.Circle{Radius: -5, Center: pixel.ZV},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
if got := pixel.C(tt.args.center, tt.args.radius); !reflect.DeepEqual(got, tt.want) {
|
|
||||||
t.Errorf("C() = %v, want %v", got, tt.want)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCircle_String(t *testing.T) {
|
|
||||||
type fields struct {
|
|
||||||
radius float64
|
|
||||||
center pixel.Vec
|
|
||||||
}
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
fields fields
|
|
||||||
want string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "Circle.String(): positive radius",
|
|
||||||
fields: fields{radius: 10, center: pixel.ZV},
|
|
||||||
want: "Circle(Vec(0, 0), 10.00)",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Circle.String(): zero radius",
|
|
||||||
fields: fields{radius: 0, center: pixel.ZV},
|
|
||||||
want: "Circle(Vec(0, 0), 0.00)",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Circle.String(): negative radius",
|
|
||||||
fields: fields{radius: -5, center: pixel.ZV},
|
|
||||||
want: "Circle(Vec(0, 0), -5.00)",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Circle.String(): irrational radius",
|
|
||||||
fields: fields{radius: math.Pi, center: pixel.ZV},
|
|
||||||
want: "Circle(Vec(0, 0), 3.14)",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
c := pixel.C(tt.fields.center, tt.fields.radius)
|
|
||||||
if got := c.String(); got != tt.want {
|
|
||||||
t.Errorf("Circle.String() = %v, want %v", got, tt.want)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCircle_Norm(t *testing.T) {
|
|
||||||
type fields struct {
|
|
||||||
radius float64
|
|
||||||
center pixel.Vec
|
|
||||||
}
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
fields fields
|
|
||||||
want pixel.Circle
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "Circle.Norm(): positive radius",
|
|
||||||
fields: fields{radius: 10, center: pixel.ZV},
|
|
||||||
want: pixel.C(pixel.ZV, 10),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Circle.Norm(): zero radius",
|
|
||||||
fields: fields{radius: 0, center: pixel.ZV},
|
|
||||||
want: pixel.C(pixel.ZV, 0),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Circle.Norm(): negative radius",
|
|
||||||
fields: fields{radius: -5, center: pixel.ZV},
|
|
||||||
want: pixel.C(pixel.ZV, 5),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
c := pixel.C(tt.fields.center, tt.fields.radius)
|
|
||||||
if got := c.Norm(); !reflect.DeepEqual(got, tt.want) {
|
|
||||||
t.Errorf("Circle.Norm() = %v, want %v", got, tt.want)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCircle_Area(t *testing.T) {
|
|
||||||
type fields struct {
|
|
||||||
radius float64
|
|
||||||
center pixel.Vec
|
|
||||||
}
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
fields fields
|
|
||||||
want float64
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "Circle.Area(): positive radius",
|
|
||||||
fields: fields{radius: 10, center: pixel.ZV},
|
|
||||||
want: 100 * math.Pi,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Circle.Area(): zero radius",
|
|
||||||
fields: fields{radius: 0, center: pixel.ZV},
|
|
||||||
want: 0,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Circle.Area(): negative radius",
|
|
||||||
fields: fields{radius: -5, center: pixel.ZV},
|
|
||||||
want: 25 * math.Pi,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
c := pixel.C(tt.fields.center, tt.fields.radius)
|
|
||||||
if got := c.Area(); got != tt.want {
|
|
||||||
t.Errorf("Circle.Area() = %v, want %v", got, tt.want)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCircle_Moved(t *testing.T) {
|
|
||||||
type fields struct {
|
|
||||||
radius float64
|
|
||||||
center pixel.Vec
|
|
||||||
}
|
|
||||||
type args struct {
|
|
||||||
delta pixel.Vec
|
|
||||||
}
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
fields fields
|
|
||||||
args args
|
|
||||||
want pixel.Circle
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "Circle.Moved(): positive movement",
|
|
||||||
fields: fields{radius: 10, center: pixel.ZV},
|
|
||||||
args: args{delta: pixel.V(10, 20)},
|
|
||||||
want: pixel.C(pixel.V(10, 20), 10),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Circle.Moved(): zero movement",
|
|
||||||
fields: fields{radius: 10, center: pixel.ZV},
|
|
||||||
args: args{delta: pixel.ZV},
|
|
||||||
want: pixel.C(pixel.V(0, 0), 10),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Circle.Moved(): negative movement",
|
|
||||||
fields: fields{radius: 10, center: pixel.ZV},
|
|
||||||
args: args{delta: pixel.V(-5, -10)},
|
|
||||||
want: pixel.C(pixel.V(-5, -10), 10),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
c := pixel.C(tt.fields.center, tt.fields.radius)
|
|
||||||
if got := c.Moved(tt.args.delta); !reflect.DeepEqual(got, tt.want) {
|
|
||||||
t.Errorf("Circle.Moved() = %v, want %v", got, tt.want)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCircle_Resized(t *testing.T) {
|
|
||||||
type fields struct {
|
|
||||||
radius float64
|
|
||||||
center pixel.Vec
|
|
||||||
}
|
|
||||||
type args struct {
|
|
||||||
radiusDelta float64
|
|
||||||
}
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
fields fields
|
|
||||||
args args
|
|
||||||
want pixel.Circle
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "Circle.Resized(): positive delta",
|
|
||||||
fields: fields{radius: 10, center: pixel.ZV},
|
|
||||||
args: args{radiusDelta: 5},
|
|
||||||
want: pixel.C(pixel.V(0, 0), 15),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Circle.Resized(): zero delta",
|
|
||||||
fields: fields{radius: 10, center: pixel.ZV},
|
|
||||||
args: args{radiusDelta: 0},
|
|
||||||
want: pixel.C(pixel.V(0, 0), 10),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Circle.Resized(): negative delta",
|
|
||||||
fields: fields{radius: 10, center: pixel.ZV},
|
|
||||||
args: args{radiusDelta: -5},
|
|
||||||
want: pixel.C(pixel.V(0, 0), 5),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
c := pixel.C(tt.fields.center, tt.fields.radius)
|
|
||||||
if got := c.Resized(tt.args.radiusDelta); !reflect.DeepEqual(got, tt.want) {
|
|
||||||
t.Errorf("Circle.Resized() = %v, want %v", got, tt.want)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCircle_Contains(t *testing.T) {
|
|
||||||
type fields struct {
|
|
||||||
radius float64
|
|
||||||
center pixel.Vec
|
|
||||||
}
|
|
||||||
type args struct {
|
|
||||||
u pixel.Vec
|
|
||||||
}
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
fields fields
|
|
||||||
args args
|
|
||||||
want bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "Circle.Contains(): point on cicles' center",
|
|
||||||
fields: fields{radius: 10, center: pixel.ZV},
|
|
||||||
args: args{u: pixel.ZV},
|
|
||||||
want: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Circle.Contains(): point offcenter",
|
|
||||||
fields: fields{radius: 10, center: pixel.V(5, 0)},
|
|
||||||
args: args{u: pixel.ZV},
|
|
||||||
want: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Circle.Contains(): point on circumference",
|
|
||||||
fields: fields{radius: 10, center: pixel.V(10, 0)},
|
|
||||||
args: args{u: pixel.ZV},
|
|
||||||
want: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Circle.Contains(): point outside circle",
|
|
||||||
fields: fields{radius: 10, center: pixel.V(15, 0)},
|
|
||||||
args: args{u: pixel.ZV},
|
|
||||||
want: false,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
c := pixel.C(tt.fields.center, tt.fields.radius)
|
|
||||||
if got := c.Contains(tt.args.u); got != tt.want {
|
|
||||||
t.Errorf("Circle.Contains() = %v, want %v", got, tt.want)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCircle_Union(t *testing.T) {
|
|
||||||
type fields struct {
|
|
||||||
radius float64
|
|
||||||
center pixel.Vec
|
|
||||||
}
|
|
||||||
type args struct {
|
|
||||||
d pixel.Circle
|
|
||||||
}
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
fields fields
|
|
||||||
args args
|
|
||||||
want pixel.Circle
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "Circle.Union(): overlapping circles",
|
|
||||||
fields: fields{radius: 5, center: pixel.ZV},
|
|
||||||
args: args{d: pixel.C(pixel.ZV, 5)},
|
|
||||||
want: pixel.C(pixel.ZV, 5),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Circle.Union(): separate circles",
|
|
||||||
fields: fields{radius: 1, center: pixel.ZV},
|
|
||||||
args: args{d: pixel.C(pixel.V(0, 2), 1)},
|
|
||||||
want: pixel.C(pixel.V(0, 1), 2),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
c := pixel.C(tt.fields.center, tt.fields.radius)
|
|
||||||
if got := c.Union(tt.args.d); !reflect.DeepEqual(got, tt.want) {
|
|
||||||
t.Errorf("Circle.Union() = %v, want %v", got, tt.want)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCircle_Intersect(t *testing.T) {
|
|
||||||
type fields struct {
|
|
||||||
radius float64
|
|
||||||
center pixel.Vec
|
|
||||||
}
|
|
||||||
type args struct {
|
|
||||||
d pixel.Circle
|
|
||||||
}
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
fields fields
|
|
||||||
args args
|
|
||||||
want pixel.Circle
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "Circle.Intersect(): intersecting circles",
|
|
||||||
fields: fields{radius: 1, center: pixel.ZV},
|
|
||||||
args: args{d: pixel.C(pixel.V(1, 0), 1)},
|
|
||||||
want: pixel.C(pixel.V(0.5, 0), 1),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Circle.Intersect(): non-intersecting circles",
|
|
||||||
fields: fields{radius: 1, center: pixel.ZV},
|
|
||||||
args: args{d: pixel.C(pixel.V(3, 3), 1)},
|
|
||||||
want: pixel.C(pixel.V(1.5, 1.5), 0),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Circle.Intersect(): first circle encompassing second",
|
|
||||||
fields: fields{radius: 10, center: pixel.ZV},
|
|
||||||
args: args{d: pixel.C(pixel.V(3, 3), 1)},
|
|
||||||
want: pixel.C(pixel.ZV, 10),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Circle.Intersect(): second circle encompassing first",
|
|
||||||
fields: fields{radius: 1, center: pixel.V(-1, -4)},
|
|
||||||
args: args{d: pixel.C(pixel.ZV, 10)},
|
|
||||||
want: pixel.C(pixel.ZV, 10),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Circle.Intersect(): matching circles",
|
|
||||||
fields: fields{radius: 1, center: pixel.ZV},
|
|
||||||
args: args{d: pixel.C(pixel.ZV, 1)},
|
|
||||||
want: pixel.C(pixel.ZV, 1),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
c := pixel.C(
|
|
||||||
tt.fields.center,
|
|
||||||
tt.fields.radius,
|
|
||||||
)
|
|
||||||
if got := c.Intersect(tt.args.d); !reflect.DeepEqual(got, tt.want) {
|
|
||||||
t.Errorf("Circle.Intersect() = %v, want %v", got, tt.want)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCircle_IntersectPoints(t *testing.T) {
|
|
||||||
type fields struct {
|
|
||||||
Center pixel.Vec
|
|
||||||
Radius float64
|
|
||||||
}
|
|
||||||
type args struct {
|
|
||||||
l pixel.Line
|
|
||||||
}
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
fields fields
|
|
||||||
args args
|
|
||||||
want []pixel.Vec
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "Line intersects circle at two points",
|
|
||||||
fields: fields{Center: pixel.V(2, 2), Radius: 1},
|
|
||||||
args: args{pixel.L(pixel.V(0, 0), pixel.V(10, 10))},
|
|
||||||
want: []pixel.Vec{pixel.V(1.292, 1.292), pixel.V(2.707, 2.707)},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Line intersects circle at one point",
|
|
||||||
fields: fields{Center: pixel.V(-0.5, -0.5), Radius: 1},
|
|
||||||
args: args{pixel.L(pixel.V(0, 0), pixel.V(10, 10))},
|
|
||||||
want: []pixel.Vec{pixel.V(0.207, 0.207)},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Line endpoint is circle center",
|
|
||||||
fields: fields{Center: pixel.V(0, 0), Radius: 1},
|
|
||||||
args: args{pixel.L(pixel.V(0, 0), pixel.V(10, 10))},
|
|
||||||
want: []pixel.Vec{pixel.V(0.707, 0.707)},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Both line endpoints within circle",
|
|
||||||
fields: fields{Center: pixel.V(0, 0), Radius: 1},
|
|
||||||
args: args{pixel.L(pixel.V(0.2, 0.2), pixel.V(0.5, 0.5))},
|
|
||||||
want: []pixel.Vec{},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Line does not intersect circle",
|
|
||||||
fields: fields{Center: pixel.V(10, 0), Radius: 1},
|
|
||||||
args: args{pixel.L(pixel.V(0, 0), pixel.V(10, 10))},
|
|
||||||
want: []pixel.Vec{},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Horizontal line intersects circle at two points",
|
|
||||||
fields: fields{Center: pixel.V(5, 5), Radius: 1},
|
|
||||||
args: args{pixel.L(pixel.V(0, 5), pixel.V(10, 5))},
|
|
||||||
want: []pixel.Vec{pixel.V(4, 5), pixel.V(6, 5)},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Vertical line intersects circle at two points",
|
|
||||||
fields: fields{Center: pixel.V(5, 5), Radius: 1},
|
|
||||||
args: args{pixel.L(pixel.V(5, 0), pixel.V(5, 10))},
|
|
||||||
want: []pixel.Vec{pixel.V(5, 4), pixel.V(5, 6)},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Left and down line intersects circle at two points",
|
|
||||||
fields: fields{Center: pixel.V(5, 5), Radius: 1},
|
|
||||||
args: args{pixel.L(pixel.V(10, 10), pixel.V(0, 0))},
|
|
||||||
want: []pixel.Vec{pixel.V(5.707, 5.707), pixel.V(4.292, 4.292)},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
c := pixel.Circle{
|
|
||||||
Center: tt.fields.Center,
|
|
||||||
Radius: tt.fields.Radius,
|
|
||||||
}
|
|
||||||
got := c.IntersectionPoints(tt.args.l)
|
|
||||||
for i, v := range got {
|
|
||||||
if !closeEnough(v.X, tt.want[i].X, 2) || !closeEnough(v.Y, tt.want[i].Y, 2) {
|
|
||||||
t.Errorf("Circle.IntersectPoints() = %v, want %v", v, tt.want[i])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
10
color.go
|
@ -75,11 +75,19 @@ func (c RGBA) RGBA() (r, g, b, a uint32) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// ToRGBA converts a color to RGBA format. Using this function is preferred to using RGBAModel, for
|
// ToRGBA converts a color to RGBA format. Using this function is preferred to using RGBAModel, for
|
||||||
// performance (using RGBAModel introduces additional unnecessary allocations).
|
// performance (using RGBAModel introduced additional unnecessary allocations).
|
||||||
func ToRGBA(c color.Color) RGBA {
|
func ToRGBA(c color.Color) RGBA {
|
||||||
if c, ok := c.(RGBA); ok {
|
if c, ok := c.(RGBA); ok {
|
||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
|
if c, ok := c.(color.RGBA); ok {
|
||||||
|
return RGBA{
|
||||||
|
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()
|
r, g, b, a := c.RGBA()
|
||||||
return RGBA{
|
return RGBA{
|
||||||
float64(r) / 0xffff,
|
float64(r) / 0xffff,
|
||||||
|
|
|
@ -1,24 +0,0 @@
|
||||||
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)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
101
data.go
|
@ -8,16 +8,6 @@ import (
|
||||||
"math"
|
"math"
|
||||||
)
|
)
|
||||||
|
|
||||||
// zeroValueTriangleData is the default value of a TriangleData element
|
|
||||||
var zeroValueTriangleData = struct {
|
|
||||||
Position Vec
|
|
||||||
Color RGBA
|
|
||||||
Picture Vec
|
|
||||||
Intensity float64
|
|
||||||
ClipRect Rect
|
|
||||||
IsClipped bool
|
|
||||||
}{Color: RGBA{1, 1, 1, 1}}
|
|
||||||
|
|
||||||
// TrianglesData specifies a list of Triangles vertices with three common properties:
|
// TrianglesData specifies a list of Triangles vertices with three common properties:
|
||||||
// TrianglesPosition, TrianglesColor and TrianglesPicture.
|
// TrianglesPosition, TrianglesColor and TrianglesPicture.
|
||||||
type TrianglesData []struct {
|
type TrianglesData []struct {
|
||||||
|
@ -25,8 +15,6 @@ type TrianglesData []struct {
|
||||||
Color RGBA
|
Color RGBA
|
||||||
Picture Vec
|
Picture Vec
|
||||||
Intensity float64
|
Intensity float64
|
||||||
ClipRect Rect
|
|
||||||
IsClipped bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MakeTrianglesData creates TrianglesData of length len initialized with default property values.
|
// MakeTrianglesData creates TrianglesData of length len initialized with default property values.
|
||||||
|
@ -34,11 +22,9 @@ type TrianglesData []struct {
|
||||||
// Prefer this function to make(TrianglesData, len), because make zeros them, while this function
|
// Prefer this function to make(TrianglesData, len), because make zeros them, while this function
|
||||||
// does the correct intialization.
|
// does the correct intialization.
|
||||||
func MakeTrianglesData(len int) *TrianglesData {
|
func MakeTrianglesData(len int) *TrianglesData {
|
||||||
td := make(TrianglesData, len)
|
td := &TrianglesData{}
|
||||||
for i := 0; i < len; i++ {
|
td.SetLen(len)
|
||||||
td[i] = zeroValueTriangleData
|
return td
|
||||||
}
|
|
||||||
return &td
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Len returns the number of vertices in TrianglesData.
|
// Len returns the number of vertices in TrianglesData.
|
||||||
|
@ -54,7 +40,12 @@ func (td *TrianglesData) SetLen(len int) {
|
||||||
if len > td.Len() {
|
if len > td.Len() {
|
||||||
needAppend := len - td.Len()
|
needAppend := len - td.Len()
|
||||||
for i := 0; i < needAppend; i++ {
|
for i := 0; i < needAppend; i++ {
|
||||||
*td = append(*td, zeroValueTriangleData)
|
*td = append(*td, struct {
|
||||||
|
Position Vec
|
||||||
|
Color RGBA
|
||||||
|
Picture Vec
|
||||||
|
Intensity float64
|
||||||
|
}{V(0, 0), Alpha(1), V(0, 0), 0})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if len < td.Len() {
|
if len < td.Len() {
|
||||||
|
@ -91,11 +82,6 @@ func (td *TrianglesData) updateData(t Triangles) {
|
||||||
(*td)[i].Picture, (*td)[i].Intensity = t.Picture(i)
|
(*td)[i].Picture, (*td)[i].Intensity = t.Picture(i)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if t, ok := t.(TrianglesClipped); ok {
|
|
||||||
for i := range *td {
|
|
||||||
(*td)[i].ClipRect, (*td)[i].IsClipped = t.ClipRect(i)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update copies vertex properties from the supplied Triangles into this TrianglesData.
|
// Update copies vertex properties from the supplied Triangles into this TrianglesData.
|
||||||
|
@ -110,9 +96,10 @@ func (td *TrianglesData) Update(t Triangles) {
|
||||||
|
|
||||||
// Copy returns an exact independent copy of this TrianglesData.
|
// Copy returns an exact independent copy of this TrianglesData.
|
||||||
func (td *TrianglesData) Copy() Triangles {
|
func (td *TrianglesData) Copy() Triangles {
|
||||||
copyTd := MakeTrianglesData(td.Len())
|
copyTd := TrianglesData{}
|
||||||
|
copyTd.SetLen(td.Len())
|
||||||
copyTd.Update(td)
|
copyTd.Update(td)
|
||||||
return copyTd
|
return ©Td
|
||||||
}
|
}
|
||||||
|
|
||||||
// Position returns the position property of i-th vertex.
|
// Position returns the position property of i-th vertex.
|
||||||
|
@ -130,11 +117,6 @@ func (td *TrianglesData) Picture(i int) (pic Vec, intensity float64) {
|
||||||
return (*td)[i].Picture, (*td)[i].Intensity
|
return (*td)[i].Picture, (*td)[i].Intensity
|
||||||
}
|
}
|
||||||
|
|
||||||
// ClipRect returns the clipping rectangle property of the i-th vertex.
|
|
||||||
func (td *TrianglesData) ClipRect(i int) (rect Rect, has bool) {
|
|
||||||
return (*td)[i].ClipRect, (*td)[i].IsClipped
|
|
||||||
}
|
|
||||||
|
|
||||||
// PictureData specifies an in-memory rectangular area of pixels and implements Picture and
|
// PictureData specifies an in-memory rectangular area of pixels and implements Picture and
|
||||||
// PictureColor.
|
// PictureColor.
|
||||||
//
|
//
|
||||||
|
@ -154,8 +136,8 @@ type PictureData struct {
|
||||||
|
|
||||||
// MakePictureData creates a zero-initialized PictureData covering the given rectangle.
|
// MakePictureData creates a zero-initialized PictureData covering the given rectangle.
|
||||||
func MakePictureData(rect Rect) *PictureData {
|
func MakePictureData(rect Rect) *PictureData {
|
||||||
w := int(math.Ceil(rect.Max.X)) - int(math.Floor(rect.Min.X))
|
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))
|
h := int(math.Ceil(rect.Max.Y())) - int(math.Floor(rect.Min.Y()))
|
||||||
pd := &PictureData{
|
pd := &PictureData{
|
||||||
Stride: w,
|
Stride: w,
|
||||||
Rect: rect,
|
Rect: rect,
|
||||||
|
@ -183,16 +165,21 @@ func verticalFlip(rgba *image.RGBA) {
|
||||||
//
|
//
|
||||||
// The resulting PictureData's Bounds will be the equivalent of the supplied image.Image's Bounds.
|
// The resulting PictureData's Bounds will be the equivalent of the supplied image.Image's Bounds.
|
||||||
func PictureDataFromImage(img image.Image) *PictureData {
|
func PictureDataFromImage(img image.Image) *PictureData {
|
||||||
rgba := image.NewRGBA(img.Bounds())
|
var rgba *image.RGBA
|
||||||
draw.Draw(rgba, rgba.Bounds(), img, img.Bounds().Min, draw.Src)
|
if rgbaImg, ok := img.(*image.RGBA); ok {
|
||||||
|
rgba = rgbaImg
|
||||||
|
} else {
|
||||||
|
rgba = image.NewRGBA(img.Bounds())
|
||||||
|
draw.Draw(rgba, rgba.Bounds(), img, img.Bounds().Min, draw.Src)
|
||||||
|
}
|
||||||
|
|
||||||
verticalFlip(rgba)
|
verticalFlip(rgba)
|
||||||
|
|
||||||
pd := MakePictureData(R(
|
pd := MakePictureData(R(
|
||||||
float64(rgba.Bounds().Min.X),
|
float64(rgba.Bounds().Min.X),
|
||||||
float64(rgba.Bounds().Min.Y),
|
float64(rgba.Bounds().Min.Y),
|
||||||
float64(rgba.Bounds().Max.X),
|
float64(rgba.Bounds().Dx()),
|
||||||
float64(rgba.Bounds().Max.Y),
|
float64(rgba.Bounds().Dy()),
|
||||||
))
|
))
|
||||||
|
|
||||||
for i := range pd.Pix {
|
for i := range pd.Pix {
|
||||||
|
@ -218,20 +205,14 @@ func PictureDataFromPicture(pic Picture) *PictureData {
|
||||||
pd := MakePictureData(bounds)
|
pd := MakePictureData(bounds)
|
||||||
|
|
||||||
if pic, ok := pic.(PictureColor); ok {
|
if pic, ok := pic.(PictureColor); ok {
|
||||||
for y := math.Floor(bounds.Min.Y); y < bounds.Max.Y; y++ {
|
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 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
|
// this together with the Floor is a trick to get all of the pixels
|
||||||
at := V(
|
at := V(
|
||||||
math.Max(x, bounds.Min.X),
|
math.Max(x, bounds.Min.X()),
|
||||||
math.Max(y, bounds.Min.Y),
|
math.Max(y, bounds.Min.Y()),
|
||||||
)
|
)
|
||||||
col := pic.Color(at)
|
pd.SetColor(at, 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),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -244,10 +225,10 @@ func PictureDataFromPicture(pic Picture) *PictureData {
|
||||||
// The resulting image.RGBA's Bounds will be equivalent of the PictureData's Bounds.
|
// The resulting image.RGBA's Bounds will be equivalent of the PictureData's Bounds.
|
||||||
func (pd *PictureData) Image() *image.RGBA {
|
func (pd *PictureData) Image() *image.RGBA {
|
||||||
bounds := image.Rect(
|
bounds := image.Rect(
|
||||||
int(math.Floor(pd.Rect.Min.X)),
|
int(math.Floor(pd.Rect.Min.X())),
|
||||||
int(math.Floor(pd.Rect.Min.Y)),
|
int(math.Floor(pd.Rect.Min.Y())),
|
||||||
int(math.Ceil(pd.Rect.Max.X)),
|
int(math.Ceil(pd.Rect.Max.X())),
|
||||||
int(math.Ceil(pd.Rect.Max.Y)),
|
int(math.Ceil(pd.Rect.Max.Y())),
|
||||||
)
|
)
|
||||||
rgba := image.NewRGBA(bounds)
|
rgba := image.NewRGBA(bounds)
|
||||||
|
|
||||||
|
@ -270,8 +251,8 @@ func (pd *PictureData) Image() *image.RGBA {
|
||||||
|
|
||||||
// Index returns the index of the pixel at the specified position inside the Pix slice.
|
// Index returns the index of the pixel at the specified position inside the Pix slice.
|
||||||
func (pd *PictureData) Index(at Vec) int {
|
func (pd *PictureData) Index(at Vec) int {
|
||||||
at = at.Sub(pd.Rect.Min.Map(math.Floor))
|
at -= pd.Rect.Min.Map(math.Floor)
|
||||||
x, y := int(at.X), int(at.Y)
|
x, y := int(at.X()), int(at.Y())
|
||||||
return y*pd.Stride + x
|
return y*pd.Stride + x
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -287,3 +268,17 @@ func (pd *PictureData) Color(at Vec) RGBA {
|
||||||
}
|
}
|
||||||
return ToRGBA(pd.Pix[pd.Index(at)])
|
return ToRGBA(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
|
||||||
|
}
|
||||||
|
rgba := ToRGBA(col)
|
||||||
|
pd.Pix[pd.Index(at)] = color.RGBA{
|
||||||
|
R: uint8(rgba.R * 255),
|
||||||
|
G: uint8(rgba.G * 255),
|
||||||
|
B: uint8(rgba.B * 255),
|
||||||
|
A: uint8(rgba.A * 255),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
300
data_test.go
|
@ -1,300 +0,0 @@
|
||||||
package pixel_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/faiface/pixel"
|
|
||||||
)
|
|
||||||
|
|
||||||
func BenchmarkMakeTrianglesData(b *testing.B) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
len int
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "Small slice",
|
|
||||||
len: 10,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Large slice",
|
|
||||||
len: 10000,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
b.Run(tt.name, func(b *testing.B) {
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
_ = pixel.MakeTrianglesData(tt.len)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func BenchmarkTrianglesData_Len(b *testing.B) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
tData *pixel.TrianglesData
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "Small slice",
|
|
||||||
tData: pixel.MakeTrianglesData(10),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Large slice",
|
|
||||||
tData: pixel.MakeTrianglesData(10000),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
b.Run(tt.name, func(b *testing.B) {
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
_ = tt.tData.Len()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func BenchmarkTrianglesData_SetLen(b *testing.B) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
tData *pixel.TrianglesData
|
|
||||||
nextLenFunc func(int, int) (int, int)
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "Stay same size",
|
|
||||||
tData: pixel.MakeTrianglesData(50),
|
|
||||||
nextLenFunc: func(i, j int) (int, int) { return 50, 0 },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Change size",
|
|
||||||
tData: pixel.MakeTrianglesData(50),
|
|
||||||
nextLenFunc: func(i, j int) (int, int) {
|
|
||||||
// 0 is shrink
|
|
||||||
if j == 0 {
|
|
||||||
next := i - 1
|
|
||||||
if next < 1 {
|
|
||||||
return 2, 1
|
|
||||||
}
|
|
||||||
return next, 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// other than 0 is grow
|
|
||||||
next := i + 1
|
|
||||||
if next == 100 {
|
|
||||||
return next, 0
|
|
||||||
}
|
|
||||||
return next, 1
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
b.Run(tt.name, func(b *testing.B) {
|
|
||||||
var newLen int
|
|
||||||
var c int
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
newLen, c = tt.nextLenFunc(newLen, c)
|
|
||||||
tt.tData.SetLen(newLen)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func BenchmarkTrianglesData_Slice(b *testing.B) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
tData *pixel.TrianglesData
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "Basic slice",
|
|
||||||
tData: pixel.MakeTrianglesData(100),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
b.Run(tt.name, func(b *testing.B) {
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
_ = tt.tData.Slice(25, 50)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func BenchmarkTrianglesData_Update(b *testing.B) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
tData *pixel.TrianglesData
|
|
||||||
t pixel.Triangles
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "Small Triangles",
|
|
||||||
tData: pixel.MakeTrianglesData(20),
|
|
||||||
t: pixel.MakeTrianglesData(20),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Large Triangles",
|
|
||||||
tData: pixel.MakeTrianglesData(10000),
|
|
||||||
t: pixel.MakeTrianglesData(10000),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
b.Run(tt.name, func(b *testing.B) {
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
tt.tData.Update(tt.t)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func BenchmarkTrianglesData_Copy(b *testing.B) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
tData *pixel.TrianglesData
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "Small copy",
|
|
||||||
tData: pixel.MakeTrianglesData(20),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Large copy",
|
|
||||||
tData: pixel.MakeTrianglesData(10000),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
b.Run(tt.name, func(b *testing.B) {
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
_ = tt.tData.Copy()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func BenchmarkTrianglesData_Position(b *testing.B) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
tData *pixel.TrianglesData
|
|
||||||
position int
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "Getting beginning position",
|
|
||||||
tData: pixel.MakeTrianglesData(1000),
|
|
||||||
position: 2,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Getting middle position",
|
|
||||||
tData: pixel.MakeTrianglesData(1000),
|
|
||||||
position: 500,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Getting end position",
|
|
||||||
tData: pixel.MakeTrianglesData(1000),
|
|
||||||
position: 999,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
b.Run(tt.name, func(b *testing.B) {
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
_ = tt.tData.Position(tt.position)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func BenchmarkTrianglesData_Color(b *testing.B) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
tData *pixel.TrianglesData
|
|
||||||
position int
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "Getting beginning position",
|
|
||||||
tData: pixel.MakeTrianglesData(1000),
|
|
||||||
position: 2,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Getting middle position",
|
|
||||||
tData: pixel.MakeTrianglesData(1000),
|
|
||||||
position: 500,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Getting end position",
|
|
||||||
tData: pixel.MakeTrianglesData(1000),
|
|
||||||
position: 999,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
b.Run(tt.name, func(b *testing.B) {
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
_ = tt.tData.Color(tt.position)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func BenchmarkTrianglesData_Picture(b *testing.B) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
tData *pixel.TrianglesData
|
|
||||||
position int
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "Getting beginning position",
|
|
||||||
tData: pixel.MakeTrianglesData(1000),
|
|
||||||
position: 2,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Getting middle position",
|
|
||||||
tData: pixel.MakeTrianglesData(1000),
|
|
||||||
position: 500,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Getting end position",
|
|
||||||
tData: pixel.MakeTrianglesData(1000),
|
|
||||||
position: 999,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
b.Run(tt.name, func(b *testing.B) {
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
_, _ = tt.tData.Picture(tt.position)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func BenchmarkTrianglesData_ClipRect(b *testing.B) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
tData *pixel.TrianglesData
|
|
||||||
position int
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "Getting beginning position",
|
|
||||||
tData: pixel.MakeTrianglesData(1000),
|
|
||||||
position: 2,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Getting middle position",
|
|
||||||
tData: pixel.MakeTrianglesData(1000),
|
|
||||||
position: 500,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Getting end position",
|
|
||||||
tData: pixel.MakeTrianglesData(1000),
|
|
||||||
position: 999,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
b.Run(tt.name, func(b *testing.B) {
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
_, _ = tt.tData.ClipRect(tt.position)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
64
drawer.go
|
@ -15,30 +15,26 @@ package pixel
|
||||||
//
|
//
|
||||||
// Whenever you change the Triangles, call Dirty to notify Drawer that Triangles changed. You don't
|
// Whenever you change the Triangles, call Dirty to notify Drawer that Triangles changed. You don't
|
||||||
// need to notify Drawer about a change of the 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 {
|
type Drawer struct {
|
||||||
Triangles Triangles
|
Triangles Triangles
|
||||||
Picture Picture
|
Picture Picture
|
||||||
Cached bool
|
|
||||||
|
|
||||||
targets map[Target]*drawerTarget
|
tris map[Target]TargetTriangles
|
||||||
allTargets []*drawerTarget
|
clean map[Target]bool
|
||||||
inited bool
|
pics map[targetPicturePair]TargetPicture
|
||||||
|
inited bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type drawerTarget struct {
|
type targetPicturePair struct {
|
||||||
tris TargetTriangles
|
Target Target
|
||||||
pics map[Picture]TargetPicture
|
Picture Picture
|
||||||
clean bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Drawer) lazyInit() {
|
func (d *Drawer) lazyInit() {
|
||||||
if !d.inited {
|
if !d.inited {
|
||||||
d.targets = make(map[Target]*drawerTarget)
|
d.tris = make(map[Target]TargetTriangles)
|
||||||
|
d.clean = make(map[Target]bool)
|
||||||
|
d.pics = make(map[targetPicturePair]TargetPicture)
|
||||||
d.inited = true
|
d.inited = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -48,8 +44,8 @@ func (d *Drawer) lazyInit() {
|
||||||
func (d *Drawer) Dirty() {
|
func (d *Drawer) Dirty() {
|
||||||
d.lazyInit()
|
d.lazyInit()
|
||||||
|
|
||||||
for _, t := range d.allTargets {
|
for t := range d.clean {
|
||||||
t.clean = false
|
d.clean[t] = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -64,39 +60,29 @@ func (d *Drawer) Draw(t Target) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
dt := d.targets[t]
|
tri := d.tris[t]
|
||||||
if dt == nil {
|
if tri == nil {
|
||||||
dt = &drawerTarget{
|
tri = t.MakeTriangles(d.Triangles)
|
||||||
pics: make(map[Picture]TargetPicture),
|
d.tris[t] = tri
|
||||||
}
|
d.clean[t] = true
|
||||||
d.targets[t] = dt
|
|
||||||
d.allTargets = append(d.allTargets, dt)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if dt.tris == nil {
|
if !d.clean[t] {
|
||||||
dt.tris = t.MakeTriangles(d.Triangles)
|
tri.SetLen(d.Triangles.Len())
|
||||||
dt.clean = true
|
tri.Update(d.Triangles)
|
||||||
}
|
d.clean[t] = true
|
||||||
|
|
||||||
if !dt.clean {
|
|
||||||
dt.tris.SetLen(d.Triangles.Len())
|
|
||||||
dt.tris.Update(d.Triangles)
|
|
||||||
dt.clean = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if d.Picture == nil {
|
if d.Picture == nil {
|
||||||
dt.tris.Draw()
|
tri.Draw()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
pic := dt.pics[d.Picture]
|
pic := d.pics[targetPicturePair{t, d.Picture}]
|
||||||
if pic == nil {
|
if pic == nil {
|
||||||
pic = t.MakePicture(d.Picture)
|
pic = t.MakePicture(d.Picture)
|
||||||
|
d.pics[targetPicturePair{t, d.Picture}] = pic
|
||||||
if d.Cached {
|
|
||||||
dt.pics[d.Picture] = pic
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pic.Draw(dt.tris)
|
pic.Draw(tri)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,18 +0,0 @@
|
||||||
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,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,57 @@
|
||||||
|
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.SetMatrix(pixel.IM.Moved(win.Bounds().Center()))
|
||||||
|
sprite.Draw(win)
|
||||||
|
|
||||||
|
for !win.Closed() {
|
||||||
|
win.Update()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
pixelgl.Run(run)
|
||||||
|
}
|
After Width: | Height: | Size: 68 KiB |
|
@ -0,0 +1,71 @@
|
||||||
|
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(0, angle)
|
||||||
|
mat = mat.Moved(win.Bounds().Center())
|
||||||
|
sprite.SetMatrix(mat)
|
||||||
|
sprite.Draw(win)
|
||||||
|
|
||||||
|
win.Update()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
pixelgl.Run(run)
|
||||||
|
}
|
|
@ -0,0 +1,101 @@
|
||||||
|
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.V(0, 0)
|
||||||
|
camSpeed = 500.0
|
||||||
|
camZoom = 1.0
|
||||||
|
camZoomSpeed = 1.2
|
||||||
|
trees []*pixel.Sprite
|
||||||
|
)
|
||||||
|
|
||||||
|
last := time.Now()
|
||||||
|
for !win.Closed() {
|
||||||
|
dt := time.Since(last).Seconds()
|
||||||
|
last = time.Now()
|
||||||
|
|
||||||
|
cam := pixel.IM.Scaled(camPos, camZoom).Moved(win.Bounds().Center() - camPos)
|
||||||
|
win.SetMatrix(cam)
|
||||||
|
|
||||||
|
if win.JustPressed(pixelgl.MouseButtonLeft) {
|
||||||
|
tree := pixel.NewSprite(spritesheet, treesFrames[rand.Intn(len(treesFrames))])
|
||||||
|
mouse := cam.Unproject(win.MousePosition())
|
||||||
|
tree.SetMatrix(pixel.IM.Scaled(0, 4).Moved(mouse))
|
||||||
|
trees = append(trees, tree)
|
||||||
|
}
|
||||||
|
if win.Pressed(pixelgl.KeyLeft) {
|
||||||
|
camPos -= pixel.X(camSpeed * dt)
|
||||||
|
}
|
||||||
|
if win.Pressed(pixelgl.KeyRight) {
|
||||||
|
camPos += pixel.X(camSpeed * dt)
|
||||||
|
}
|
||||||
|
if win.Pressed(pixelgl.KeyDown) {
|
||||||
|
camPos -= pixel.Y(camSpeed * dt)
|
||||||
|
}
|
||||||
|
if win.Pressed(pixelgl.KeyUp) {
|
||||||
|
camPos += pixel.Y(camSpeed * dt)
|
||||||
|
}
|
||||||
|
camZoom *= math.Pow(camZoomSpeed, win.MouseScroll().Y())
|
||||||
|
|
||||||
|
win.Clear(colornames.Forestgreen)
|
||||||
|
|
||||||
|
for _, tree := range trees {
|
||||||
|
tree.Draw(win)
|
||||||
|
}
|
||||||
|
|
||||||
|
win.Update()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
pixelgl.Run(run)
|
||||||
|
}
|
After Width: | Height: | Size: 2.5 KiB |
|
@ -0,0 +1,111 @@
|
||||||
|
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.V(0, 0)
|
||||||
|
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() - camPos)
|
||||||
|
win.SetMatrix(cam)
|
||||||
|
|
||||||
|
if win.Pressed(pixelgl.MouseButtonLeft) {
|
||||||
|
tree := pixel.NewSprite(spritesheet, treesFrames[rand.Intn(len(treesFrames))])
|
||||||
|
mouse := cam.Unproject(win.MousePosition())
|
||||||
|
tree.SetMatrix(pixel.IM.Scaled(0, 4).Moved(mouse))
|
||||||
|
tree.Draw(batch)
|
||||||
|
}
|
||||||
|
if win.Pressed(pixelgl.KeyLeft) {
|
||||||
|
camPos -= pixel.X(camSpeed * dt)
|
||||||
|
}
|
||||||
|
if win.Pressed(pixelgl.KeyRight) {
|
||||||
|
camPos += pixel.X(camSpeed * dt)
|
||||||
|
}
|
||||||
|
if win.Pressed(pixelgl.KeyDown) {
|
||||||
|
camPos -= pixel.Y(camSpeed * dt)
|
||||||
|
}
|
||||||
|
if win.Pressed(pixelgl.KeyUp) {
|
||||||
|
camPos += pixel.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,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,201 @@
|
||||||
|
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 drawer interface {
|
||||||
|
Draw(pixel.Target)
|
||||||
|
}
|
||||||
|
|
||||||
|
type colorlight struct {
|
||||||
|
color pixel.RGBA
|
||||||
|
point pixel.Vec
|
||||||
|
angle float64
|
||||||
|
radius float64
|
||||||
|
dust float64
|
||||||
|
|
||||||
|
spread float64
|
||||||
|
|
||||||
|
imd *imdraw.IMDraw
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cl *colorlight) apply(src, noise drawer, dst pixel.ComposeTarget) {
|
||||||
|
// create the light arc if not created already
|
||||||
|
if cl.imd == nil {
|
||||||
|
imd := imdraw.New(nil)
|
||||||
|
imd.Color(pixel.Alpha(1))
|
||||||
|
imd.Push(0)
|
||||||
|
imd.Color(pixel.Alpha(0))
|
||||||
|
for angle := -cl.spread / 2; angle <= cl.spread/2; angle += cl.spread / 64 {
|
||||||
|
imd.Push(pixel.X(1).Rotated(angle))
|
||||||
|
}
|
||||||
|
imd.Polygon(0)
|
||||||
|
cl.imd = imd
|
||||||
|
}
|
||||||
|
|
||||||
|
// draw the light arc
|
||||||
|
dst.SetMatrix(pixel.IM.Scaled(0, cl.radius).Rotated(0, 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)
|
||||||
|
|
||||||
|
// draw an image inside the noisy light
|
||||||
|
dst.SetColorMask(cl.color)
|
||||||
|
dst.SetComposeMethod(pixel.ComposeIn)
|
||||||
|
src.Draw(dst)
|
||||||
|
|
||||||
|
// draw the light reflected from the dust
|
||||||
|
dst.SetMatrix(pixel.IM.Scaled(0, cl.radius).Rotated(0, 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())
|
||||||
|
panda.SetMatrix(pixel.IM.Moved(win.Bounds().Center()))
|
||||||
|
noise := pixel.NewSprite(noisePic, noisePic.Bounds())
|
||||||
|
noise.SetMatrix(pixel.IM.Moved(win.Bounds().Center()))
|
||||||
|
|
||||||
|
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{
|
||||||
|
pixel.V(win.Bounds().Min.X(), win.Bounds().Min.Y()),
|
||||||
|
pixel.V(win.Bounds().Max.X(), win.Bounds().Min.Y()),
|
||||||
|
pixel.V(win.Bounds().Max.X(), win.Bounds().Max.Y()),
|
||||||
|
pixel.V(win.Bounds().Min.X(), 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)
|
||||||
|
|
||||||
|
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(panda, noise, oneLight)
|
||||||
|
oneLight.Draw(allLight)
|
||||||
|
}
|
||||||
|
|
||||||
|
// compose the final result
|
||||||
|
win.SetColorMask(pixel.Alpha(1))
|
||||||
|
allLight.Draw(win)
|
||||||
|
|
||||||
|
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,395 @@
|
||||||
|
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 = gp.vel.WithX(-gp.runSpeed)
|
||||||
|
case ctrl.X() > 0:
|
||||||
|
gp.vel = gp.vel.WithX(+gp.runSpeed)
|
||||||
|
default:
|
||||||
|
gp.vel = gp.vel.WithX(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// apply gravity and velocity
|
||||||
|
gp.vel += pixel.Y(gp.gravity).Scaled(dt)
|
||||||
|
gp.rect = gp.rect.Moved(gp.vel.Scaled(dt))
|
||||||
|
|
||||||
|
// check collisions agains 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 = gp.vel.WithY(0)
|
||||||
|
gp.rect = gp.rect.Moved(pixel.Y(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 = gp.vel.WithY(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 positon and direction
|
||||||
|
ga.sprite.Set(ga.sheet, ga.frame)
|
||||||
|
ga.sprite.SetMatrix(pixel.IM.
|
||||||
|
ScaledXY(0, pixel.V(
|
||||||
|
phys.rect.W()/ga.sprite.Frame().W(),
|
||||||
|
phys.rect.H()/ga.sprite.Frame().H(),
|
||||||
|
)).
|
||||||
|
ScaledXY(0, pixel.V(-ga.dir, 1)).
|
||||||
|
Moved(phys.rect.Center()),
|
||||||
|
)
|
||||||
|
ga.sprite.Draw(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
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.V(0, 0)
|
||||||
|
|
||||||
|
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)
|
||||||
|
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())
|
||||||
|
phys.vel = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// control the gopher with keys
|
||||||
|
ctrl := pixel.V(0, 0)
|
||||||
|
if win.Pressed(pixelgl.KeyLeft) {
|
||||||
|
ctrl -= pixel.X(1)
|
||||||
|
}
|
||||||
|
if win.Pressed(pixelgl.KeyRight) {
|
||||||
|
ctrl += pixel.X(1)
|
||||||
|
}
|
||||||
|
if win.JustPressed(pixelgl.KeyUp) {
|
||||||
|
ctrl = ctrl.WithY(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(0,
|
||||||
|
math.Min(
|
||||||
|
win.Bounds().W()/canvas.Bounds().W(),
|
||||||
|
win.Bounds().H()/canvas.Bounds().H(),
|
||||||
|
),
|
||||||
|
).Moved(win.Bounds().Center()))
|
||||||
|
canvas.Draw(win)
|
||||||
|
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,226 @@
|
||||||
|
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.SetMatrix(pixel.IM.
|
||||||
|
Scaled(0, part.Scale).
|
||||||
|
Rotated(0, part.Rot).
|
||||||
|
Moved(part.Pos),
|
||||||
|
)
|
||||||
|
part.Sprite.SetColorMask(part.Mask)
|
||||||
|
part.Sprite.Draw(t)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 += 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 += 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: 0,
|
||||||
|
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)
|
||||||
|
win.SetMatrix(pixel.IM.Moved(win.Bounds().Center() - pixel.Y(win.Bounds().H()/2)))
|
||||||
|
|
||||||
|
batch.Clear()
|
||||||
|
p.DrawAll(batch)
|
||||||
|
batch.Draw(win)
|
||||||
|
|
||||||
|
win.Update()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
pixelgl.Run(run)
|
||||||
|
}
|
After Width: | Height: | Size: 381 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() - pixel.X(offset))
|
||||||
|
imd.Circle(200, 0)
|
||||||
|
imd.Draw(canvas)
|
||||||
|
|
||||||
|
// blue circle
|
||||||
|
imd.Clear()
|
||||||
|
imd.Color(pixel.RGB(0, 0, 1))
|
||||||
|
imd.Push(win.Bounds().Center() + pixel.X(offset))
|
||||||
|
imd.Circle(150, 0)
|
||||||
|
imd.Draw(canvas)
|
||||||
|
|
||||||
|
// yellow circle
|
||||||
|
imd.Clear()
|
||||||
|
imd.Color(pixel.RGB(1, 1, 0))
|
||||||
|
imd.Push(win.Bounds().Center() - pixel.Y(offset))
|
||||||
|
imd.Circle(100, 0)
|
||||||
|
imd.Draw(canvas)
|
||||||
|
|
||||||
|
// magenta circle
|
||||||
|
imd.Clear()
|
||||||
|
imd.Color(pixel.RGB(1, 0, 1))
|
||||||
|
imd.Push(win.Bounds().Center() + pixel.Y(offset))
|
||||||
|
imd.Circle(50, 0)
|
||||||
|
imd.Draw(canvas)
|
||||||
|
|
||||||
|
win.Clear(colornames.Green)
|
||||||
|
canvas.Draw(win)
|
||||||
|
win.Update()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
pixelgl.Run(run)
|
||||||
|
}
|
After Width: | Height: | Size: 10 KiB |
|
@ -0,0 +1,358 @@
|
||||||
|
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.
|
||||||
|
//
|
||||||
|
// 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:
|
||||||
|
//
|
||||||
|
// 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 {
|
||||||
|
// fmt.Println("this won't happen")
|
||||||
|
// }
|
||||||
|
// x := u.Unit().Dot(v.Unit())
|
||||||
|
type Vec complex128
|
||||||
|
|
||||||
|
// V returns a new 2D vector with the given coordinates.
|
||||||
|
func V(x, y float64) Vec {
|
||||||
|
return Vec(complex(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)
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns the string representation of the vector u.
|
||||||
|
//
|
||||||
|
// u := pixel.V(4.5, -1.3)
|
||||||
|
// u.String() // returns "Vec(4.5, -1.3)"
|
||||||
|
// fmt.Println(u) // Vec(4.5, -1.3)
|
||||||
|
func (u Vec) String() string {
|
||||||
|
return fmt.Sprintf("Vec(%v, %v)", u.X(), u.Y())
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
// XY returns the components of the vector in two return values.
|
||||||
|
func (u Vec) XY() (x, y float64) {
|
||||||
|
return real(u), imag(u)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 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
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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())
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dot returns the dot product of vectors u and v.
|
||||||
|
func (u Vec) Dot(v Vec) float64 {
|
||||||
|
return u.X()*v.X() + u.Y()*v.Y()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cross return the cross product of vectors u and v.
|
||||||
|
func (u Vec) Cross(v Vec) float64 {
|
||||||
|
return u.X()*v.Y() - v.X()*u.Y()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Map applies the function f to both x and y components of the vector u and returns the modified
|
||||||
|
// vector.
|
||||||
|
//
|
||||||
|
// u := pixel.V(10.5, -1.5)
|
||||||
|
// v := u.Map(math.Floor) // v is Vec(10, -2), both components of u floored
|
||||||
|
func (u Vec) Map(f func(float64) float64) Vec {
|
||||||
|
return V(
|
||||||
|
f(u.X()),
|
||||||
|
f(u.Y()),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lerp returns a linear interpolation between vectors a and b.
|
||||||
|
//
|
||||||
|
// This function basically returns a point along the line between a and b and t chooses which one.
|
||||||
|
// If t is 0, then a will be returned, if t is 1, b will be returned. Anything between 0 and 1 will
|
||||||
|
// return the appropriate point between a and b and so on.
|
||||||
|
func Lerp(a, b Vec, t float64) Vec {
|
||||||
|
return a.Scaled(1-t) + b.Scaled(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rect is a 2D rectangle aligned with the axes of the coordinate system. It is defined by two
|
||||||
|
// points, Min and Max.
|
||||||
|
//
|
||||||
|
// The invariant should hold, that Max's components are greater or equal than Min's components
|
||||||
|
// respectively.
|
||||||
|
type Rect struct {
|
||||||
|
Min, Max Vec
|
||||||
|
}
|
||||||
|
|
||||||
|
// R returns a new Rect with given the Min and Max coordinates.
|
||||||
|
func R(minX, minY, maxX, maxY float64) Rect {
|
||||||
|
return Rect{
|
||||||
|
Min: V(minX, minY),
|
||||||
|
Max: V(maxX, maxY),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns the string representation of the Rect.
|
||||||
|
//
|
||||||
|
// r := pixel.R(100, 50, 200, 300)
|
||||||
|
// r.String() // returns "Rect(100, 50, 200, 300)"
|
||||||
|
// fmt.Println(r) // Rect(100, 50, 200, 300)
|
||||||
|
func (r Rect) String() string {
|
||||||
|
return fmt.Sprintf("Rect(%v, %v, %v, %v)", r.Min.X(), r.Min.Y(), r.Max.X(), r.Max.Y())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Norm returns the Rect in normal form, such that Max is component-wise greater or equal than Min.
|
||||||
|
func (r Rect) Norm() Rect {
|
||||||
|
return Rect{
|
||||||
|
Min: 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()),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// W returns the width of the Rect.
|
||||||
|
func (r Rect) W() float64 {
|
||||||
|
return r.Max.X() - r.Min.X()
|
||||||
|
}
|
||||||
|
|
||||||
|
// H returns the height of the Rect.
|
||||||
|
func (r Rect) H() float64 {
|
||||||
|
return r.Max.Y() - r.Min.Y()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Size returns the vector of width and height of the Rect.
|
||||||
|
func (r Rect) Size() Vec {
|
||||||
|
return V(r.W(), r.H())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Center returns the position of the center of the Rect.
|
||||||
|
func (r Rect) Center() Vec {
|
||||||
|
return (r.Min + r.Max) / 2
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resized returns the Rect resized to the given size while keeping the position of the given
|
||||||
|
// anchor.
|
||||||
|
//
|
||||||
|
// r.Resized(r.Min, size) // resizes while keeping the position of the lower-left corner
|
||||||
|
// r.Resized(r.Max, size) // same with the top-right corner
|
||||||
|
// r.Resized(r.Center(), size) // resizes around the center
|
||||||
|
//
|
||||||
|
// This function does not make sense for sizes 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 {
|
||||||
|
panic(fmt.Errorf("(%T).Resize: zero area", r))
|
||||||
|
}
|
||||||
|
fraction := V(size.X()/r.W(), size.Y()/r.H())
|
||||||
|
return Rect{
|
||||||
|
Min: anchor + (r.Min - anchor).ScaledXY(fraction),
|
||||||
|
Max: anchor + (r.Max - anchor).ScaledXY(fraction),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResizedMin returns the Rect resized to the given size while keeping the position of the Rect's
|
||||||
|
// Min.
|
||||||
|
//
|
||||||
|
// Sizes of zero area are safe here.
|
||||||
|
func (r Rect) ResizedMin(size Vec) Rect {
|
||||||
|
return Rect{
|
||||||
|
Min: r.Min,
|
||||||
|
Max: r.Min + 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()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Matrix is a 3x3 transformation matrix that can be used for all kinds of spacial 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)
|
||||||
|
//
|
||||||
|
// 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
|
||||||
|
|
||||||
|
// IM stands for identity matrix. Does nothing, no transformation.
|
||||||
|
var IM = Matrix(mgl64.Ident3())
|
||||||
|
|
||||||
|
// String returns a string representation of the Matrix.
|
||||||
|
//
|
||||||
|
// m := pixel.IM
|
||||||
|
// fmt.Println(m) // Matrix(1 0 0 | 0 1 0 | 0 0 1)
|
||||||
|
func (m Matrix) String() string {
|
||||||
|
return fmt.Sprintf(
|
||||||
|
"Matrix(%v %v %v | %v %v %v | %v %v %v)",
|
||||||
|
m[0], m[3], m[6],
|
||||||
|
m[1], m[4], m[7],
|
||||||
|
m[2], m[5], m[8],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scaled scales everything around a given point by the scale factor.
|
||||||
|
func (m Matrix) Scaled(around Vec, scale float64) Matrix {
|
||||||
|
return m.ScaledXY(around, V(scale, scale))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rotated rotates everything around a given point by the given angle in radians.
|
||||||
|
func (m Matrix) Rotated(around Vec, angle float64) Matrix {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unproject does the inverse operation to Project.
|
||||||
|
//
|
||||||
|
// 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())
|
||||||
|
}
|
15
go.mod
|
@ -1,15 +0,0 @@
|
||||||
module github.com/faiface/pixel
|
|
||||||
|
|
||||||
go 1.12
|
|
||||||
|
|
||||||
require (
|
|
||||||
github.com/faiface/glhf v0.0.0-20211013000516-57b20770c369
|
|
||||||
github.com/faiface/mainthread v0.0.0-20171120011319-8b78f0a41ae3
|
|
||||||
github.com/go-gl/gl v0.0.0-20210905235341-f7a045908259
|
|
||||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20210727001814-0db043d8d5be
|
|
||||||
github.com/go-gl/mathgl v1.0.0
|
|
||||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0
|
|
||||||
github.com/pkg/errors v0.9.1
|
|
||||||
github.com/stretchr/testify v1.7.0
|
|
||||||
golang.org/x/image v0.5.0
|
|
||||||
)
|
|
54
go.sum
|
@ -1,54 +0,0 @@
|
||||||
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
|
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
|
||||||
github.com/faiface/glhf v0.0.0-20211013000516-57b20770c369 h1:gv4BgP50atccdK/1tZHDyP6rMwiiutR2HPreR/OyLzI=
|
|
||||||
github.com/faiface/glhf v0.0.0-20211013000516-57b20770c369/go.mod h1:dDdUO+G9ZnJ9sc8nIUvhLkE45k8PEKW6+A3TdWsfpV0=
|
|
||||||
github.com/faiface/mainthread v0.0.0-20171120011319-8b78f0a41ae3 h1:baVdMKlASEHrj19iqjARrPbaRisD7EuZEVJj6ZMLl1Q=
|
|
||||||
github.com/faiface/mainthread v0.0.0-20171120011319-8b78f0a41ae3/go.mod h1:VEPNJUlxl5KdWjDvz6Q1l+rJlxF2i6xqDeGuGAxa87M=
|
|
||||||
github.com/go-gl/gl v0.0.0-20210905235341-f7a045908259 h1:8q7+xl2D2qHPLTII1t4vSMNP2VKwDcn+Avf2WXvdB1A=
|
|
||||||
github.com/go-gl/gl v0.0.0-20210905235341-f7a045908259/go.mod h1:wjpnOv6ONl2SuJSxqCPVaPZibGFdSci9HFocT9qtVYM=
|
|
||||||
github.com/go-gl/glfw v0.0.0-20210727001814-0db043d8d5be h1:UVW91pfMB1GRQfVwC7//RGVbqX6Ea8jURmJhlANak1M=
|
|
||||||
github.com/go-gl/glfw v0.0.0-20210727001814-0db043d8d5be/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
|
||||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20210727001814-0db043d8d5be h1:vEIVIuBApEBQTEJt19GfhoU+zFSV+sNTa9E9FdnRYfk=
|
|
||||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20210727001814-0db043d8d5be/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
|
||||||
github.com/go-gl/mathgl v1.0.0 h1:t9DznWJlXxxjeeKLIdovCOVJQk/GzDEL7h/h+Ro2B68=
|
|
||||||
github.com/go-gl/mathgl v1.0.0/go.mod h1:yhpkQzEiH9yPyxDUGzkmgScbaBVlhC06qodikEM0ZwQ=
|
|
||||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
|
|
||||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
|
|
||||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
|
||||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
|
||||||
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
|
|
||||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
|
||||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
|
||||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
|
||||||
golang.org/x/image v0.0.0-20190321063152-3fc05d484e9f/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
|
||||||
golang.org/x/image v0.5.0 h1:5JMiNunQeQw++mMOz48/ISeNu3Iweh/JaZU8ZLqHRrI=
|
|
||||||
golang.org/x/image v0.5.0/go.mod h1:FVC7BI/5Ym8R25iw5OLsgshdUBbT1h5jZTpA+mvAdZ4=
|
|
||||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
|
||||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
|
||||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
|
||||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
|
||||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
|
||||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
|
||||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
|
||||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
|
||||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
|
||||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
|
||||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
|
||||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
|
||||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
|
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
|
254
imdraw/imdraw.go
|
@ -23,9 +23,9 @@ import (
|
||||||
//
|
//
|
||||||
// imd.Line(20) // draws a 20 units thick line
|
// imd.Line(20) // draws a 20 units thick line
|
||||||
//
|
//
|
||||||
// Set exported fields to change properties of Pushed points:
|
// Use various methods to change properties of Pushed points:
|
||||||
//
|
//
|
||||||
// imd.Color = pixel.RGB(1, 0, 0)
|
// imd.Color(pixel.RGB(1, 0, 0))
|
||||||
// imd.Push(pixel.V(200, 200))
|
// imd.Push(pixel.V(200, 200))
|
||||||
// imd.Circle(400, 0)
|
// imd.Circle(400, 0)
|
||||||
//
|
//
|
||||||
|
@ -45,14 +45,8 @@ import (
|
||||||
// - Ellipse
|
// - Ellipse
|
||||||
// - Ellipse arc
|
// - Ellipse arc
|
||||||
type IMDraw struct {
|
type IMDraw struct {
|
||||||
Color color.Color
|
|
||||||
Picture pixel.Vec
|
|
||||||
Intensity float64
|
|
||||||
Precision int
|
|
||||||
EndShape EndShape
|
|
||||||
|
|
||||||
points []point
|
points []point
|
||||||
pool [][]point
|
opts point
|
||||||
matrix pixel.Matrix
|
matrix pixel.Matrix
|
||||||
mask pixel.RGBA
|
mask pixel.RGBA
|
||||||
|
|
||||||
|
@ -110,12 +104,9 @@ func (imd *IMDraw) Clear() {
|
||||||
//
|
//
|
||||||
// This does not affect matrix and color mask set by SetMatrix and SetColorMask.
|
// This does not affect matrix and color mask set by SetMatrix and SetColorMask.
|
||||||
func (imd *IMDraw) Reset() {
|
func (imd *IMDraw) Reset() {
|
||||||
imd.points = imd.points[:0]
|
imd.points = nil
|
||||||
imd.Color = pixel.Alpha(1)
|
imd.opts = point{}
|
||||||
imd.Picture = pixel.ZV
|
imd.Precision(64)
|
||||||
imd.Intensity = 0
|
|
||||||
imd.Precision = 64
|
|
||||||
imd.EndShape = NoEndShape
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draw draws all currently drawn shapes inside the IM onto another Target.
|
// Draw draws all currently drawn shapes inside the IM onto another Target.
|
||||||
|
@ -128,20 +119,8 @@ 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
|
// Push adds some points to the IM queue. All Pushed points will have the same properties except for
|
||||||
// the position.
|
// the position.
|
||||||
func (imd *IMDraw) Push(pts ...pixel.Vec) {
|
func (imd *IMDraw) Push(pts ...pixel.Vec) {
|
||||||
// Assert that Color is of type pixel.RGBA,
|
|
||||||
if _, ok := imd.Color.(pixel.RGBA); !ok {
|
|
||||||
// otherwise cast it
|
|
||||||
imd.Color = pixel.ToRGBA(imd.Color)
|
|
||||||
}
|
|
||||||
opts := point{
|
|
||||||
col: imd.Color.(pixel.RGBA),
|
|
||||||
pic: imd.Picture,
|
|
||||||
in: imd.Intensity,
|
|
||||||
precision: imd.Precision,
|
|
||||||
endshape: imd.EndShape,
|
|
||||||
}
|
|
||||||
for _, pt := range pts {
|
for _, pt := range pts {
|
||||||
imd.pushPt(pt, opts)
|
imd.pushPt(pt, imd.opts)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -150,6 +129,33 @@ func (imd *IMDraw) pushPt(pos pixel.Vec, pt point) {
|
||||||
imd.points = append(imd.points, pt)
|
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.ToRGBA(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.
|
// SetMatrix sets a Matrix that all further points will be transformed by.
|
||||||
func (imd *IMDraw) SetMatrix(m pixel.Matrix) {
|
func (imd *IMDraw) SetMatrix(m pixel.Matrix) {
|
||||||
imd.matrix = m
|
imd.matrix = m
|
||||||
|
@ -261,22 +267,10 @@ func (imd *IMDraw) EllipseArc(radius pixel.Vec, low, high, thickness float64) {
|
||||||
|
|
||||||
func (imd *IMDraw) getAndClearPoints() []point {
|
func (imd *IMDraw) getAndClearPoints() []point {
|
||||||
points := imd.points
|
points := imd.points
|
||||||
// use one of the existing pools so we don't reallocate as often
|
imd.points = nil
|
||||||
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
|
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) {
|
func (imd *IMDraw) applyMatrixAndMask(off int) {
|
||||||
for i := range (*imd.tri)[off:] {
|
for i := range (*imd.tri)[off:] {
|
||||||
(*imd.tri)[off+i].Position = imd.matrix.Project((*imd.tri)[off+i].Position)
|
(*imd.tri)[off+i].Position = imd.matrix.Project((*imd.tri)[off+i].Position)
|
||||||
|
@ -288,7 +282,6 @@ func (imd *IMDraw) fillRectangle() {
|
||||||
points := imd.getAndClearPoints()
|
points := imd.getAndClearPoints()
|
||||||
|
|
||||||
if len(points) < 2 {
|
if len(points) < 2 {
|
||||||
imd.restorePoints(points)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -298,19 +291,19 @@ func (imd *IMDraw) fillRectangle() {
|
||||||
for i, j := 0, off; i+1 < len(points); i, j = i+1, j+6 {
|
for i, j := 0, off; i+1 < len(points); i, j = i+1, j+6 {
|
||||||
a, b := points[i], points[i+1]
|
a, b := points[i], points[i+1]
|
||||||
c := point{
|
c := point{
|
||||||
pos: pixel.V(a.pos.X, b.pos.Y),
|
pos: pixel.V(a.pos.X(), b.pos.Y()),
|
||||||
col: a.col.Add(b.col).Mul(pixel.Alpha(0.5)),
|
col: a.col.Add(b.col).Mul(pixel.Alpha(0.5)),
|
||||||
pic: pixel.V(a.pic.X, b.pic.Y),
|
pic: pixel.V(a.pic.X(), b.pic.Y()),
|
||||||
in: (a.in + b.in) / 2,
|
in: (a.in + b.in) / 2,
|
||||||
}
|
}
|
||||||
d := point{
|
d := point{
|
||||||
pos: pixel.V(b.pos.X, a.pos.Y),
|
pos: pixel.V(b.pos.X(), a.pos.Y()),
|
||||||
col: a.col.Add(b.col).Mul(pixel.Alpha(0.5)),
|
col: a.col.Add(b.col).Mul(pixel.Alpha(0.5)),
|
||||||
pic: pixel.V(b.pic.X, a.pic.Y),
|
pic: pixel.V(b.pic.X(), a.pic.Y()),
|
||||||
in: (a.in + b.in) / 2,
|
in: (a.in + b.in) / 2,
|
||||||
}
|
}
|
||||||
|
|
||||||
for k, p := range [...]point{a, b, c, a, b, d} {
|
for k, p := range []point{a, b, c, a, b, d} {
|
||||||
(*imd.tri)[j+k].Position = p.pos
|
(*imd.tri)[j+k].Position = p.pos
|
||||||
(*imd.tri)[j+k].Color = p.col
|
(*imd.tri)[j+k].Color = p.col
|
||||||
(*imd.tri)[j+k].Picture = p.pic
|
(*imd.tri)[j+k].Picture = p.pic
|
||||||
|
@ -320,15 +313,12 @@ func (imd *IMDraw) fillRectangle() {
|
||||||
|
|
||||||
imd.applyMatrixAndMask(off)
|
imd.applyMatrixAndMask(off)
|
||||||
imd.batch.Dirty()
|
imd.batch.Dirty()
|
||||||
|
|
||||||
imd.restorePoints(points)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (imd *IMDraw) outlineRectangle(thickness float64) {
|
func (imd *IMDraw) outlineRectangle(thickness float64) {
|
||||||
points := imd.getAndClearPoints()
|
points := imd.getAndClearPoints()
|
||||||
|
|
||||||
if len(points) < 2 {
|
if len(points) < 2 {
|
||||||
imd.restorePoints(points)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -339,20 +329,17 @@ func (imd *IMDraw) outlineRectangle(thickness float64) {
|
||||||
mid.in = (a.in + b.in) / 2
|
mid.in = (a.in + b.in) / 2
|
||||||
|
|
||||||
imd.pushPt(a.pos, a)
|
imd.pushPt(a.pos, a)
|
||||||
imd.pushPt(pixel.V(a.pos.X, b.pos.Y), mid)
|
imd.pushPt(pixel.V(a.pos.X(), b.pos.Y()), mid)
|
||||||
imd.pushPt(b.pos, b)
|
imd.pushPt(b.pos, b)
|
||||||
imd.pushPt(pixel.V(b.pos.X, a.pos.Y), mid)
|
imd.pushPt(pixel.V(b.pos.X(), a.pos.Y()), mid)
|
||||||
imd.polyline(thickness, true)
|
imd.polyline(thickness, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
imd.restorePoints(points)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (imd *IMDraw) fillPolygon() {
|
func (imd *IMDraw) fillPolygon() {
|
||||||
points := imd.getAndClearPoints()
|
points := imd.getAndClearPoints()
|
||||||
|
|
||||||
if len(points) < 3 {
|
if len(points) < 3 {
|
||||||
imd.restorePoints(points)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -360,19 +347,16 @@ func (imd *IMDraw) fillPolygon() {
|
||||||
imd.tri.SetLen(imd.tri.Len() + 3*(len(points)-2))
|
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 {
|
for i, j := 1, off; i+1 < len(points); i, j = i+1, j+3 {
|
||||||
for k, p := range [...]int{0, i, i + 1} {
|
for k, p := range []point{points[0], points[i], points[i+1]} {
|
||||||
tri := &(*imd.tri)[j+k]
|
(*imd.tri)[j+k].Position = p.pos
|
||||||
tri.Position = points[p].pos
|
(*imd.tri)[j+k].Color = p.col
|
||||||
tri.Color = points[p].col
|
(*imd.tri)[j+k].Picture = p.pic
|
||||||
tri.Picture = points[p].pic
|
(*imd.tri)[j+k].Intensity = p.in
|
||||||
tri.Intensity = points[p].in
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
imd.applyMatrixAndMask(off)
|
imd.applyMatrixAndMask(off)
|
||||||
imd.batch.Dirty()
|
imd.batch.Dirty()
|
||||||
|
|
||||||
imd.restorePoints(points)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (imd *IMDraw) fillEllipseArc(radius pixel.Vec, low, high float64) {
|
func (imd *IMDraw) fillEllipseArc(radius pixel.Vec, low, high float64) {
|
||||||
|
@ -387,24 +371,24 @@ func (imd *IMDraw) fillEllipseArc(radius pixel.Vec, low, high float64) {
|
||||||
|
|
||||||
for i := range (*imd.tri)[off:] {
|
for i := range (*imd.tri)[off:] {
|
||||||
(*imd.tri)[off+i].Color = pt.col
|
(*imd.tri)[off+i].Color = pt.col
|
||||||
(*imd.tri)[off+i].Picture = pixel.ZV
|
(*imd.tri)[off+i].Picture = 0
|
||||||
(*imd.tri)[off+i].Intensity = 0
|
(*imd.tri)[off+i].Intensity = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
for i, j := 0.0, off; i < num; i, j = i+1, j+3 {
|
for i, j := 0.0, off; i < num; i, j = i+1, j+3 {
|
||||||
angle := low + i*delta
|
angle := low + i*delta
|
||||||
sin, cos := math.Sincos(angle)
|
sin, cos := math.Sincos(angle)
|
||||||
a := pt.pos.Add(pixel.V(
|
a := pt.pos + pixel.V(
|
||||||
radius.X*cos,
|
radius.X()*cos,
|
||||||
radius.Y*sin,
|
radius.Y()*sin,
|
||||||
))
|
)
|
||||||
|
|
||||||
angle = low + (i+1)*delta
|
angle = low + (i+1)*delta
|
||||||
sin, cos = math.Sincos(angle)
|
sin, cos = math.Sincos(angle)
|
||||||
b := pt.pos.Add(pixel.V(
|
b := pt.pos + pixel.V(
|
||||||
radius.X*cos,
|
radius.X()*cos,
|
||||||
radius.Y*sin,
|
radius.Y()*sin,
|
||||||
))
|
)
|
||||||
|
|
||||||
(*imd.tri)[j+0].Position = pt.pos
|
(*imd.tri)[j+0].Position = pt.pos
|
||||||
(*imd.tri)[j+1].Position = a
|
(*imd.tri)[j+1].Position = a
|
||||||
|
@ -414,8 +398,6 @@ func (imd *IMDraw) fillEllipseArc(radius pixel.Vec, low, high float64) {
|
||||||
imd.applyMatrixAndMask(off)
|
imd.applyMatrixAndMask(off)
|
||||||
imd.batch.Dirty()
|
imd.batch.Dirty()
|
||||||
}
|
}
|
||||||
|
|
||||||
imd.restorePoints(points)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (imd *IMDraw) outlineEllipseArc(radius pixel.Vec, low, high, thickness float64, doEndShape bool) {
|
func (imd *IMDraw) outlineEllipseArc(radius pixel.Vec, low, high, thickness float64, doEndShape bool) {
|
||||||
|
@ -430,7 +412,7 @@ func (imd *IMDraw) outlineEllipseArc(radius pixel.Vec, low, high, thickness floa
|
||||||
|
|
||||||
for i := range (*imd.tri)[off:] {
|
for i := range (*imd.tri)[off:] {
|
||||||
(*imd.tri)[off+i].Color = pt.col
|
(*imd.tri)[off+i].Color = pt.col
|
||||||
(*imd.tri)[off+i].Picture = pixel.ZV
|
(*imd.tri)[off+i].Picture = 0
|
||||||
(*imd.tri)[off+i].Intensity = 0
|
(*imd.tri)[off+i].Intensity = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -438,26 +420,26 @@ func (imd *IMDraw) outlineEllipseArc(radius pixel.Vec, low, high, thickness floa
|
||||||
angle := low + i*delta
|
angle := low + i*delta
|
||||||
sin, cos := math.Sincos(angle)
|
sin, cos := math.Sincos(angle)
|
||||||
normalSin, normalCos := pixel.V(sin, cos).ScaledXY(radius).Unit().XY()
|
normalSin, normalCos := pixel.V(sin, cos).ScaledXY(radius).Unit().XY()
|
||||||
a := pt.pos.Add(pixel.V(
|
a := pt.pos + pixel.V(
|
||||||
radius.X*cos-thickness/2*normalCos,
|
radius.X()*cos-thickness/2*normalCos,
|
||||||
radius.Y*sin-thickness/2*normalSin,
|
radius.Y()*sin-thickness/2*normalSin,
|
||||||
))
|
)
|
||||||
b := pt.pos.Add(pixel.V(
|
b := pt.pos + pixel.V(
|
||||||
radius.X*cos+thickness/2*normalCos,
|
radius.X()*cos+thickness/2*normalCos,
|
||||||
radius.Y*sin+thickness/2*normalSin,
|
radius.Y()*sin+thickness/2*normalSin,
|
||||||
))
|
)
|
||||||
|
|
||||||
angle = low + (i+1)*delta
|
angle = low + (i+1)*delta
|
||||||
sin, cos = math.Sincos(angle)
|
sin, cos = math.Sincos(angle)
|
||||||
normalSin, normalCos = pixel.V(sin, cos).ScaledXY(radius).Unit().XY()
|
normalSin, normalCos = pixel.V(sin, cos).ScaledXY(radius).Unit().XY()
|
||||||
c := pt.pos.Add(pixel.V(
|
c := pt.pos + pixel.V(
|
||||||
radius.X*cos-thickness/2*normalCos,
|
radius.X()*cos-thickness/2*normalCos,
|
||||||
radius.Y*sin-thickness/2*normalSin,
|
radius.Y()*sin-thickness/2*normalSin,
|
||||||
))
|
)
|
||||||
d := pt.pos.Add(pixel.V(
|
d := pt.pos + pixel.V(
|
||||||
radius.X*cos+thickness/2*normalCos,
|
radius.X()*cos+thickness/2*normalCos,
|
||||||
radius.Y*sin+thickness/2*normalSin,
|
radius.Y()*sin+thickness/2*normalSin,
|
||||||
))
|
)
|
||||||
|
|
||||||
(*imd.tri)[j+0].Position = a
|
(*imd.tri)[j+0].Position = a
|
||||||
(*imd.tri)[j+1].Position = b
|
(*imd.tri)[j+1].Position = b
|
||||||
|
@ -472,18 +454,18 @@ func (imd *IMDraw) outlineEllipseArc(radius pixel.Vec, low, high, thickness floa
|
||||||
|
|
||||||
if doEndShape {
|
if doEndShape {
|
||||||
lowSin, lowCos := math.Sincos(low)
|
lowSin, lowCos := math.Sincos(low)
|
||||||
lowCenter := pt.pos.Add(pixel.V(
|
lowCenter := pt.pos + pixel.V(
|
||||||
radius.X*lowCos,
|
radius.X()*lowCos,
|
||||||
radius.Y*lowSin,
|
radius.Y()*lowSin,
|
||||||
))
|
)
|
||||||
normalLowSin, normalLowCos := pixel.V(lowSin, lowCos).ScaledXY(radius).Unit().XY()
|
normalLowSin, normalLowCos := pixel.V(lowSin, lowCos).ScaledXY(radius).Unit().XY()
|
||||||
normalLow := pixel.V(normalLowCos, normalLowSin).Angle()
|
normalLow := pixel.V(normalLowCos, normalLowSin).Angle()
|
||||||
|
|
||||||
highSin, highCos := math.Sincos(high)
|
highSin, highCos := math.Sincos(high)
|
||||||
highCenter := pt.pos.Add(pixel.V(
|
highCenter := pt.pos + pixel.V(
|
||||||
radius.X*highCos,
|
radius.X()*highCos,
|
||||||
radius.Y*highSin,
|
radius.Y()*highSin,
|
||||||
))
|
)
|
||||||
normalHighSin, normalHighCos := pixel.V(highSin, highCos).ScaledXY(radius).Unit().XY()
|
normalHighSin, normalHighCos := pixel.V(highSin, highCos).ScaledXY(radius).Unit().XY()
|
||||||
normalHigh := pixel.V(normalHighCos, normalHighSin).Angle()
|
normalHigh := pixel.V(normalHighCos, normalHighSin).Angle()
|
||||||
|
|
||||||
|
@ -496,33 +478,30 @@ func (imd *IMDraw) outlineEllipseArc(radius pixel.Vec, low, high, thickness floa
|
||||||
case NoEndShape:
|
case NoEndShape:
|
||||||
// nothing
|
// nothing
|
||||||
case SharpEndShape:
|
case SharpEndShape:
|
||||||
thick := pixel.V(thickness/2, 0).Rotated(normalLow)
|
thick := pixel.X(thickness / 2).Rotated(normalLow)
|
||||||
imd.pushPt(lowCenter.Add(thick), pt)
|
imd.pushPt(lowCenter+thick, pt)
|
||||||
imd.pushPt(lowCenter.Sub(thick), pt)
|
imd.pushPt(lowCenter-thick, pt)
|
||||||
imd.pushPt(lowCenter.Sub(thick.Normal().Scaled(orientation)), pt)
|
imd.pushPt(lowCenter-thick.Rotated(math.Pi/2*orientation), pt)
|
||||||
imd.fillPolygon()
|
imd.fillPolygon()
|
||||||
thick = pixel.V(thickness/2, 0).Rotated(normalHigh)
|
thick = pixel.X(thickness / 2).Rotated(normalHigh)
|
||||||
imd.pushPt(highCenter.Add(thick), pt)
|
imd.pushPt(highCenter+thick, pt)
|
||||||
imd.pushPt(highCenter.Sub(thick), pt)
|
imd.pushPt(highCenter-thick, pt)
|
||||||
imd.pushPt(highCenter.Add(thick.Normal().Scaled(orientation)), pt)
|
imd.pushPt(highCenter+thick.Rotated(math.Pi/2*orientation), pt)
|
||||||
imd.fillPolygon()
|
imd.fillPolygon()
|
||||||
case RoundEndShape:
|
case RoundEndShape:
|
||||||
imd.pushPt(lowCenter, pt)
|
imd.pushPt(lowCenter, pt)
|
||||||
imd.fillEllipseArc(pixel.V(thickness/2, thickness/2), normalLow, normalLow-math.Pi*orientation)
|
imd.fillEllipseArc(pixel.V(thickness, thickness)/2, normalLow, normalLow-math.Pi*orientation)
|
||||||
imd.pushPt(highCenter, pt)
|
imd.pushPt(highCenter, pt)
|
||||||
imd.fillEllipseArc(pixel.V(thickness/2, thickness/2), normalHigh, normalHigh+math.Pi*orientation)
|
imd.fillEllipseArc(pixel.V(thickness, thickness)/2, normalHigh, normalHigh+math.Pi*orientation)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
imd.restorePoints(points)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (imd *IMDraw) polyline(thickness float64, closed bool) {
|
func (imd *IMDraw) polyline(thickness float64, closed bool) {
|
||||||
points := imd.getAndClearPoints()
|
points := imd.getAndClearPoints()
|
||||||
|
|
||||||
if len(points) == 0 {
|
if len(points) == 0 {
|
||||||
imd.restorePoints(points)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if len(points) == 1 {
|
if len(points) == 1 {
|
||||||
|
@ -532,25 +511,25 @@ func (imd *IMDraw) polyline(thickness float64, closed bool) {
|
||||||
|
|
||||||
// first point
|
// first point
|
||||||
j, i := 0, 1
|
j, i := 0, 1
|
||||||
ijNormal := points[0].pos.To(points[1].pos).Normal().Unit().Scaled(thickness / 2)
|
normal := (points[i].pos - points[j].pos).Rotated(math.Pi / 2).Unit().Scaled(thickness / 2)
|
||||||
|
|
||||||
if !closed {
|
if !closed {
|
||||||
switch points[j].endshape {
|
switch points[j].endshape {
|
||||||
case NoEndShape:
|
case NoEndShape:
|
||||||
// nothing
|
// nothing
|
||||||
case SharpEndShape:
|
case SharpEndShape:
|
||||||
imd.pushPt(points[j].pos.Add(ijNormal), points[j])
|
imd.pushPt(points[j].pos+normal, points[j])
|
||||||
imd.pushPt(points[j].pos.Sub(ijNormal), points[j])
|
imd.pushPt(points[j].pos-normal, points[j])
|
||||||
imd.pushPt(points[j].pos.Add(ijNormal.Normal()), points[j])
|
imd.pushPt(points[j].pos+normal.Rotated(math.Pi/2), points[j])
|
||||||
imd.fillPolygon()
|
imd.fillPolygon()
|
||||||
case RoundEndShape:
|
case RoundEndShape:
|
||||||
imd.pushPt(points[j].pos, points[j])
|
imd.pushPt(points[j].pos, points[j])
|
||||||
imd.fillEllipseArc(pixel.V(thickness/2, thickness/2), ijNormal.Angle(), ijNormal.Angle()+math.Pi)
|
imd.fillEllipseArc(pixel.V(thickness, thickness)/2, normal.Angle(), normal.Angle()+math.Pi)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
imd.pushPt(points[j].pos.Add(ijNormal), points[j])
|
imd.pushPt(points[j].pos+normal, points[j])
|
||||||
imd.pushPt(points[j].pos.Sub(ijNormal), points[j])
|
imd.pushPt(points[j].pos-normal, points[j])
|
||||||
|
|
||||||
// middle points
|
// middle points
|
||||||
for i := 0; i < len(points); i++ {
|
for i := 0; i < len(points); i++ {
|
||||||
|
@ -568,15 +547,16 @@ func (imd *IMDraw) polyline(thickness float64, closed bool) {
|
||||||
k %= len(points)
|
k %= len(points)
|
||||||
}
|
}
|
||||||
|
|
||||||
jkNormal := points[j].pos.To(points[k].pos).Normal().Unit().Scaled(thickness / 2)
|
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)
|
||||||
|
|
||||||
orientation := 1.0
|
orientation := 1.0
|
||||||
if ijNormal.Cross(jkNormal) > 0 {
|
if ijNormal.Cross(jkNormal) > 0 {
|
||||||
orientation = -1.0
|
orientation = -1.0
|
||||||
}
|
}
|
||||||
|
|
||||||
imd.pushPt(points[j].pos.Sub(ijNormal), points[j])
|
imd.pushPt(points[j].pos-ijNormal, points[j])
|
||||||
imd.pushPt(points[j].pos.Add(ijNormal), points[j])
|
imd.pushPt(points[j].pos+ijNormal, points[j])
|
||||||
imd.fillPolygon()
|
imd.fillPolygon()
|
||||||
|
|
||||||
switch points[j].endshape {
|
switch points[j].endshape {
|
||||||
|
@ -584,30 +564,28 @@ func (imd *IMDraw) polyline(thickness float64, closed bool) {
|
||||||
// nothing
|
// nothing
|
||||||
case SharpEndShape:
|
case SharpEndShape:
|
||||||
imd.pushPt(points[j].pos, points[j])
|
imd.pushPt(points[j].pos, points[j])
|
||||||
imd.pushPt(points[j].pos.Add(ijNormal.Scaled(orientation)), points[j])
|
imd.pushPt(points[j].pos+ijNormal.Scaled(orientation), points[j])
|
||||||
imd.pushPt(points[j].pos.Add(jkNormal.Scaled(orientation)), points[j])
|
imd.pushPt(points[j].pos+jkNormal.Scaled(orientation), points[j])
|
||||||
imd.fillPolygon()
|
imd.fillPolygon()
|
||||||
case RoundEndShape:
|
case RoundEndShape:
|
||||||
imd.pushPt(points[j].pos, points[j])
|
imd.pushPt(points[j].pos, points[j])
|
||||||
imd.fillEllipseArc(pixel.V(thickness/2, thickness/2), ijNormal.Angle(), ijNormal.Angle()-math.Pi)
|
imd.fillEllipseArc(pixel.V(thickness, thickness)/2, ijNormal.Angle(), ijNormal.Angle()-math.Pi)
|
||||||
imd.pushPt(points[j].pos, points[j])
|
imd.pushPt(points[j].pos, points[j])
|
||||||
imd.fillEllipseArc(pixel.V(thickness/2, thickness/2), jkNormal.Angle(), jkNormal.Angle()+math.Pi)
|
imd.fillEllipseArc(pixel.V(thickness, thickness)/2, jkNormal.Angle(), jkNormal.Angle()+math.Pi)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !closing {
|
if !closing {
|
||||||
imd.pushPt(points[j].pos.Add(jkNormal), points[j])
|
imd.pushPt(points[j].pos+jkNormal, points[j])
|
||||||
imd.pushPt(points[j].pos.Sub(jkNormal), points[j])
|
imd.pushPt(points[j].pos-jkNormal, points[j])
|
||||||
}
|
}
|
||||||
// "next" normal becomes previous normal
|
|
||||||
ijNormal = jkNormal
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// last point
|
// last point
|
||||||
i, j = len(points)-2, len(points)-1
|
i, j = len(points)-2, len(points)-1
|
||||||
ijNormal = points[i].pos.To(points[j].pos).Normal().Unit().Scaled(thickness / 2)
|
normal = (points[j].pos - points[i].pos).Rotated(math.Pi / 2).Unit().Scaled(thickness / 2)
|
||||||
|
|
||||||
imd.pushPt(points[j].pos.Sub(ijNormal), points[j])
|
imd.pushPt(points[j].pos-normal, points[j])
|
||||||
imd.pushPt(points[j].pos.Add(ijNormal), points[j])
|
imd.pushPt(points[j].pos+normal, points[j])
|
||||||
imd.fillPolygon()
|
imd.fillPolygon()
|
||||||
|
|
||||||
if !closed {
|
if !closed {
|
||||||
|
@ -615,15 +593,13 @@ func (imd *IMDraw) polyline(thickness float64, closed bool) {
|
||||||
case NoEndShape:
|
case NoEndShape:
|
||||||
// nothing
|
// nothing
|
||||||
case SharpEndShape:
|
case SharpEndShape:
|
||||||
imd.pushPt(points[j].pos.Add(ijNormal), points[j])
|
imd.pushPt(points[j].pos+normal, points[j])
|
||||||
imd.pushPt(points[j].pos.Sub(ijNormal), points[j])
|
imd.pushPt(points[j].pos-normal, points[j])
|
||||||
imd.pushPt(points[j].pos.Add(ijNormal.Normal().Scaled(-1)), points[j])
|
imd.pushPt(points[j].pos+normal.Rotated(-math.Pi/2), points[j])
|
||||||
imd.fillPolygon()
|
imd.fillPolygon()
|
||||||
case RoundEndShape:
|
case RoundEndShape:
|
||||||
imd.pushPt(points[j].pos, points[j])
|
imd.pushPt(points[j].pos, points[j])
|
||||||
imd.fillEllipseArc(pixel.V(thickness/2, thickness/2), ijNormal.Angle(), ijNormal.Angle()-math.Pi)
|
imd.fillEllipseArc(pixel.V(thickness, thickness)/2, normal.Angle(), normal.Angle()-math.Pi)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
imd.restorePoints(points)
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,96 +0,0 @@
|
||||||
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)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
13
interface.go
|
@ -92,7 +92,7 @@ type TrianglesColor interface {
|
||||||
Color(i int) RGBA
|
Color(i int) RGBA
|
||||||
}
|
}
|
||||||
|
|
||||||
// TrianglesPicture specifies Triangles with Picture property.
|
// TrianglesPicture specifies Triangles with Picture propery.
|
||||||
//
|
//
|
||||||
// The first value returned from Picture method is Picture coordinates. The second one specifies the
|
// The first value returned from Picture method is Picture coordinates. The second one specifies the
|
||||||
// weight of the Picture. Value of 0 means, that Picture should be completely ignored, 1 means that
|
// weight of the Picture. Value of 0 means, that Picture should be completely ignored, 1 means that
|
||||||
|
@ -102,19 +102,10 @@ type TrianglesPicture interface {
|
||||||
Picture(i int) (pic Vec, intensity float64)
|
Picture(i int) (pic Vec, intensity float64)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TrianglesClipped specifies Triangles with Clipping Rectangle property.
|
|
||||||
//
|
|
||||||
// The first value returned from ClipRect method is the clipping rectangle. The second one specifies
|
|
||||||
// if the triangle is clipped.
|
|
||||||
type TrianglesClipped interface {
|
|
||||||
Triangles
|
|
||||||
ClipRect(i int) (rect Rect, is bool)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Picture represents a rectangular area of raster data, such as a color. It has Bounds which
|
// Picture represents a rectangular area of raster data, such as a color. It has Bounds which
|
||||||
// specify the rectangle where data is located.
|
// specify the rectangle where data is located.
|
||||||
type Picture interface {
|
type Picture interface {
|
||||||
// Bounds returns the rectangle of the Picture. All data is located within this rectangle.
|
// 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.
|
// Querying properties outside the rectangle should return default value of that property.
|
||||||
Bounds() Rect
|
Bounds() Rect
|
||||||
}
|
}
|
||||||
|
|
699
line_test.go
|
@ -1,699 +0,0 @@
|
||||||
package pixel_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"math"
|
|
||||||
"reflect"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/faiface/pixel"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestLine_Bounds(t *testing.T) {
|
|
||||||
type fields struct {
|
|
||||||
A pixel.Vec
|
|
||||||
B pixel.Vec
|
|
||||||
}
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
fields fields
|
|
||||||
want pixel.Rect
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "Positive slope",
|
|
||||||
fields: fields{A: pixel.V(0, 0), B: pixel.V(10, 10)},
|
|
||||||
want: pixel.R(0, 0, 10, 10),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Negative slope",
|
|
||||||
fields: fields{A: pixel.V(10, 10), B: pixel.V(0, 0)},
|
|
||||||
want: pixel.R(0, 0, 10, 10),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
l := pixel.Line{
|
|
||||||
A: tt.fields.A,
|
|
||||||
B: tt.fields.B,
|
|
||||||
}
|
|
||||||
if got := l.Bounds(); !reflect.DeepEqual(got, tt.want) {
|
|
||||||
t.Errorf("Line.Bounds() = %v, want %v", got, tt.want)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestLine_Center(t *testing.T) {
|
|
||||||
type fields struct {
|
|
||||||
A pixel.Vec
|
|
||||||
B pixel.Vec
|
|
||||||
}
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
fields fields
|
|
||||||
want pixel.Vec
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "Positive slope",
|
|
||||||
fields: fields{A: pixel.V(0, 0), B: pixel.V(10, 10)},
|
|
||||||
want: pixel.V(5, 5),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Negative slope",
|
|
||||||
fields: fields{A: pixel.V(10, 10), B: pixel.V(0, 0)},
|
|
||||||
want: pixel.V(5, 5),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
l := pixel.Line{
|
|
||||||
A: tt.fields.A,
|
|
||||||
B: tt.fields.B,
|
|
||||||
}
|
|
||||||
if got := l.Center(); !reflect.DeepEqual(got, tt.want) {
|
|
||||||
t.Errorf("Line.Center() = %v, want %v", got, tt.want)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestLine_Closest(t *testing.T) {
|
|
||||||
type fields struct {
|
|
||||||
A pixel.Vec
|
|
||||||
B pixel.Vec
|
|
||||||
}
|
|
||||||
type args struct {
|
|
||||||
v pixel.Vec
|
|
||||||
}
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
fields fields
|
|
||||||
args args
|
|
||||||
want pixel.Vec
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "Point on line",
|
|
||||||
fields: fields{A: pixel.V(0, 0), B: pixel.V(10, 10)},
|
|
||||||
args: args{v: pixel.V(5, 5)},
|
|
||||||
want: pixel.V(5, 5),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Point on next to line",
|
|
||||||
fields: fields{A: pixel.V(0, 0), B: pixel.V(10, 10)},
|
|
||||||
args: args{v: pixel.V(0, 10)},
|
|
||||||
want: pixel.V(5, 5),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Point on next to vertical line",
|
|
||||||
fields: fields{A: pixel.V(5, 0), B: pixel.V(5, 10)},
|
|
||||||
args: args{v: pixel.V(6, 5)},
|
|
||||||
want: pixel.V(5, 5),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Point on next to horizontal line",
|
|
||||||
fields: fields{A: pixel.V(0, 5), B: pixel.V(10, 5)},
|
|
||||||
args: args{v: pixel.V(5, 6)},
|
|
||||||
want: pixel.V(5, 5),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Point far from line",
|
|
||||||
fields: fields{A: pixel.V(0, 0), B: pixel.V(10, 10)},
|
|
||||||
args: args{v: pixel.V(80, -70)},
|
|
||||||
want: pixel.V(5, 5),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Point on inline with line",
|
|
||||||
fields: fields{A: pixel.V(0, 0), B: pixel.V(10, 10)},
|
|
||||||
args: args{v: pixel.V(20, 20)},
|
|
||||||
want: pixel.V(10, 10),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Vertical line",
|
|
||||||
fields: fields{A: pixel.V(0, -10), B: pixel.V(0, 10)},
|
|
||||||
args: args{v: pixel.V(-1, 0)},
|
|
||||||
want: pixel.V(0, 0),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Horizontal line",
|
|
||||||
fields: fields{A: pixel.V(-10, 0), B: pixel.V(10, 0)},
|
|
||||||
args: args{v: pixel.V(0, -1)},
|
|
||||||
want: pixel.V(0, 0),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
l := pixel.Line{
|
|
||||||
A: tt.fields.A,
|
|
||||||
B: tt.fields.B,
|
|
||||||
}
|
|
||||||
if got := l.Closest(tt.args.v); !reflect.DeepEqual(got, tt.want) {
|
|
||||||
t.Errorf("Line.Closest() = %v, want %v", got, tt.want)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestLine_Contains(t *testing.T) {
|
|
||||||
type fields struct {
|
|
||||||
A pixel.Vec
|
|
||||||
B pixel.Vec
|
|
||||||
}
|
|
||||||
type args struct {
|
|
||||||
v pixel.Vec
|
|
||||||
}
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
fields fields
|
|
||||||
args args
|
|
||||||
want bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "Point on line",
|
|
||||||
fields: fields{A: pixel.V(0, 0), B: pixel.V(10, 10)},
|
|
||||||
args: args{v: pixel.V(5, 5)},
|
|
||||||
want: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Point on negative sloped line",
|
|
||||||
fields: fields{A: pixel.V(0, 10), B: pixel.V(10, 0)},
|
|
||||||
args: args{v: pixel.V(5, 5)},
|
|
||||||
want: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Point not on line",
|
|
||||||
fields: fields{A: pixel.V(0, 0), B: pixel.V(10, 10)},
|
|
||||||
args: args{v: pixel.V(0, 10)},
|
|
||||||
want: false,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
l := pixel.Line{
|
|
||||||
A: tt.fields.A,
|
|
||||||
B: tt.fields.B,
|
|
||||||
}
|
|
||||||
if got := l.Contains(tt.args.v); got != tt.want {
|
|
||||||
t.Errorf("Line.Contains() = %v, want %v", got, tt.want)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestLine_Formula(t *testing.T) {
|
|
||||||
type fields struct {
|
|
||||||
A pixel.Vec
|
|
||||||
B pixel.Vec
|
|
||||||
}
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
fields fields
|
|
||||||
wantM float64
|
|
||||||
wantB float64
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "Getting formula - 45 degs",
|
|
||||||
fields: fields{A: pixel.V(0, 0), B: pixel.V(10, 10)},
|
|
||||||
wantM: 1,
|
|
||||||
wantB: 0,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Getting formula - 90 degs",
|
|
||||||
fields: fields{A: pixel.V(0, 0), B: pixel.V(0, 10)},
|
|
||||||
wantM: math.Inf(1),
|
|
||||||
wantB: math.NaN(),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Getting formula - 0 degs",
|
|
||||||
fields: fields{A: pixel.V(0, 0), B: pixel.V(10, 0)},
|
|
||||||
wantM: 0,
|
|
||||||
wantB: 0,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
l := pixel.Line{
|
|
||||||
A: tt.fields.A,
|
|
||||||
B: tt.fields.B,
|
|
||||||
}
|
|
||||||
gotM, gotB := l.Formula()
|
|
||||||
if gotM != tt.wantM {
|
|
||||||
t.Errorf("Line.Formula() gotM = %v, want %v", gotM, tt.wantM)
|
|
||||||
}
|
|
||||||
if gotB != tt.wantB {
|
|
||||||
if math.IsNaN(tt.wantB) && !math.IsNaN(gotB) {
|
|
||||||
t.Errorf("Line.Formula() gotB = %v, want %v", gotB, tt.wantB)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestLine_Intersect(t *testing.T) {
|
|
||||||
type fields struct {
|
|
||||||
A pixel.Vec
|
|
||||||
B pixel.Vec
|
|
||||||
}
|
|
||||||
type args struct {
|
|
||||||
k pixel.Line
|
|
||||||
}
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
fields fields
|
|
||||||
args args
|
|
||||||
want pixel.Vec
|
|
||||||
want1 bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "Lines intersect",
|
|
||||||
fields: fields{A: pixel.V(0, 0), B: pixel.V(10, 10)},
|
|
||||||
args: args{k: pixel.L(pixel.V(0, 10), pixel.V(10, 0))},
|
|
||||||
want: pixel.V(5, 5),
|
|
||||||
want1: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Lines intersect 2",
|
|
||||||
fields: fields{A: pixel.V(5, 1), B: pixel.V(1, 1)},
|
|
||||||
args: args{k: pixel.L(pixel.V(2, 0), pixel.V(2, 3))},
|
|
||||||
want: pixel.V(2, 1),
|
|
||||||
want1: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Line intersect with vertical",
|
|
||||||
fields: fields{A: pixel.V(5, 0), B: pixel.V(5, 10)},
|
|
||||||
args: args{k: pixel.L(pixel.V(0, 0), pixel.V(10, 10))},
|
|
||||||
want: pixel.V(5, 5),
|
|
||||||
want1: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Line intersect with horizontal",
|
|
||||||
fields: fields{A: pixel.V(0, 5), B: pixel.V(10, 5)},
|
|
||||||
args: args{k: pixel.L(pixel.V(0, 0), pixel.V(10, 10))},
|
|
||||||
want: pixel.V(5, 5),
|
|
||||||
want1: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Lines don't intersect",
|
|
||||||
fields: fields{A: pixel.V(0, 0), B: pixel.V(10, 10)},
|
|
||||||
args: args{k: pixel.L(pixel.V(0, 10), pixel.V(1, 20))},
|
|
||||||
want: pixel.ZV,
|
|
||||||
want1: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Lines don't intersect 2",
|
|
||||||
fields: fields{A: pixel.V(1, 1), B: pixel.V(1, 5)},
|
|
||||||
args: args{k: pixel.L(pixel.V(-5, 0), pixel.V(-2, 2))},
|
|
||||||
want: pixel.ZV,
|
|
||||||
want1: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Lines don't intersect 3",
|
|
||||||
fields: fields{A: pixel.V(2, 0), B: pixel.V(2, 3)},
|
|
||||||
args: args{k: pixel.L(pixel.V(1, 5), pixel.V(5, 5))},
|
|
||||||
want: pixel.ZV,
|
|
||||||
want1: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Lines parallel",
|
|
||||||
fields: fields{A: pixel.V(0, 0), B: pixel.V(10, 10)},
|
|
||||||
args: args{k: pixel.L(pixel.V(0, 1), pixel.V(10, 11))},
|
|
||||||
want: pixel.ZV,
|
|
||||||
want1: false,
|
|
||||||
}, {
|
|
||||||
name: "Lines intersect",
|
|
||||||
fields: fields{A: pixel.V(600, 600), B: pixel.V(925, 150)},
|
|
||||||
args: args{k: pixel.L(pixel.V(740, 255), pixel.V(925, 255))},
|
|
||||||
want: pixel.V(849.1666666666666, 255),
|
|
||||||
want1: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Lines intersect",
|
|
||||||
fields: fields{A: pixel.V(600, 600), B: pixel.V(925, 150)},
|
|
||||||
args: args{k: pixel.L(pixel.V(740, 255), pixel.V(925, 255.0001))},
|
|
||||||
want: pixel.V(849.1666240490657, 255.000059008986),
|
|
||||||
want1: true,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
l := pixel.Line{
|
|
||||||
A: tt.fields.A,
|
|
||||||
B: tt.fields.B,
|
|
||||||
}
|
|
||||||
got, got1 := l.Intersect(tt.args.k)
|
|
||||||
if !reflect.DeepEqual(got, tt.want) {
|
|
||||||
t.Errorf("Line.Intersect() got = %v, want %v", got, tt.want)
|
|
||||||
}
|
|
||||||
if got1 != tt.want1 {
|
|
||||||
t.Errorf("Line.Intersect() got1 = %v, want %v", got1, tt.want1)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestLine_IntersectCircle(t *testing.T) {
|
|
||||||
type fields struct {
|
|
||||||
A pixel.Vec
|
|
||||||
B pixel.Vec
|
|
||||||
}
|
|
||||||
type args struct {
|
|
||||||
c pixel.Circle
|
|
||||||
}
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
fields fields
|
|
||||||
args args
|
|
||||||
want pixel.Vec
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "Cirle intersects",
|
|
||||||
fields: fields{A: pixel.V(0, 0), B: pixel.V(10, 10)},
|
|
||||||
args: args{c: pixel.C(pixel.V(6, 4), 2)},
|
|
||||||
want: pixel.V(0.5857864376269049, -0.5857864376269049),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Cirle doesn't intersects",
|
|
||||||
fields: fields{A: pixel.V(0, 0), B: pixel.V(10, 10)},
|
|
||||||
args: args{c: pixel.C(pixel.V(0, 5), 1)},
|
|
||||||
want: pixel.ZV,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
l := pixel.Line{
|
|
||||||
A: tt.fields.A,
|
|
||||||
B: tt.fields.B,
|
|
||||||
}
|
|
||||||
if got := l.IntersectCircle(tt.args.c); !reflect.DeepEqual(got, tt.want) {
|
|
||||||
t.Errorf("Line.IntersectCircle() = %v, want %v", got, tt.want)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestLine_IntersectRect(t *testing.T) {
|
|
||||||
type fields struct {
|
|
||||||
A pixel.Vec
|
|
||||||
B pixel.Vec
|
|
||||||
}
|
|
||||||
type args struct {
|
|
||||||
r pixel.Rect
|
|
||||||
}
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
fields fields
|
|
||||||
args args
|
|
||||||
want pixel.Vec
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "Line through rect vertically",
|
|
||||||
fields: fields{A: pixel.V(0, 0), B: pixel.V(0, 10)},
|
|
||||||
args: args{r: pixel.R(-1, 1, 5, 5)},
|
|
||||||
want: pixel.V(-1, 0),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Line through rect horizontally",
|
|
||||||
fields: fields{A: pixel.V(0, 1), B: pixel.V(10, 1)},
|
|
||||||
args: args{r: pixel.R(1, 0, 5, 5)},
|
|
||||||
want: pixel.V(0, -1),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Line through rect diagonally bottom and left edges",
|
|
||||||
fields: fields{A: pixel.V(0, 0), B: pixel.V(10, 10)},
|
|
||||||
args: args{r: pixel.R(0, 2, 3, 3)},
|
|
||||||
want: pixel.V(-1, 1),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Line through rect diagonally top and right edges",
|
|
||||||
fields: fields{A: pixel.V(10, 0), B: pixel.V(0, 10)},
|
|
||||||
args: args{r: pixel.R(5, 0, 8, 3)},
|
|
||||||
want: pixel.V(-2.5, -2.5),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Line with not rect intersect",
|
|
||||||
fields: fields{A: pixel.V(0, 0), B: pixel.V(10, 10)},
|
|
||||||
args: args{r: pixel.R(20, 20, 21, 21)},
|
|
||||||
want: pixel.ZV,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Line intersects at 0,0",
|
|
||||||
fields: fields{A: pixel.V(0, -10), B: pixel.V(0, 10)},
|
|
||||||
args: args{r: pixel.R(-1, 0, 2, 2)},
|
|
||||||
want: pixel.V(-1, 0),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
l := pixel.Line{
|
|
||||||
A: tt.fields.A,
|
|
||||||
B: tt.fields.B,
|
|
||||||
}
|
|
||||||
if got := l.IntersectRect(tt.args.r); !reflect.DeepEqual(got, tt.want) {
|
|
||||||
t.Errorf("Line.IntersectRect() = %v, want %v", got, tt.want)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestLine_Len(t *testing.T) {
|
|
||||||
type fields struct {
|
|
||||||
A pixel.Vec
|
|
||||||
B pixel.Vec
|
|
||||||
}
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
fields fields
|
|
||||||
want float64
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "End right-up of start",
|
|
||||||
fields: fields{A: pixel.V(0, 0), B: pixel.V(3, 4)},
|
|
||||||
want: 5,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "End left-up of start",
|
|
||||||
fields: fields{A: pixel.V(0, 0), B: pixel.V(-3, 4)},
|
|
||||||
want: 5,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "End right-down of start",
|
|
||||||
fields: fields{A: pixel.V(0, 0), B: pixel.V(3, -4)},
|
|
||||||
want: 5,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "End left-down of start",
|
|
||||||
fields: fields{A: pixel.V(0, 0), B: pixel.V(-3, -4)},
|
|
||||||
want: 5,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "End same as start",
|
|
||||||
fields: fields{A: pixel.V(0, 0), B: pixel.V(0, 0)},
|
|
||||||
want: 0,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
l := pixel.Line{
|
|
||||||
A: tt.fields.A,
|
|
||||||
B: tt.fields.B,
|
|
||||||
}
|
|
||||||
if got := l.Len(); got != tt.want {
|
|
||||||
t.Errorf("Line.Len() = %v, want %v", got, tt.want)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestLine_Rotated(t *testing.T) {
|
|
||||||
// round returns the nearest integer, rounding ties away from zero.
|
|
||||||
// This is required because `math.Round` wasn't introduced until Go1.10
|
|
||||||
round := func(x float64) float64 {
|
|
||||||
t := math.Trunc(x)
|
|
||||||
if math.Abs(x-t) >= 0.5 {
|
|
||||||
return t + math.Copysign(1, x)
|
|
||||||
}
|
|
||||||
return t
|
|
||||||
}
|
|
||||||
type fields struct {
|
|
||||||
A pixel.Vec
|
|
||||||
B pixel.Vec
|
|
||||||
}
|
|
||||||
type args struct {
|
|
||||||
around pixel.Vec
|
|
||||||
angle float64
|
|
||||||
}
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
fields fields
|
|
||||||
args args
|
|
||||||
want pixel.Line
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "Rotating around line center",
|
|
||||||
fields: fields{A: pixel.V(1, 1), B: pixel.V(3, 3)},
|
|
||||||
args: args{around: pixel.V(2, 2), angle: math.Pi},
|
|
||||||
want: pixel.L(pixel.V(3, 3), pixel.V(1, 1)),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Rotating around x-y origin",
|
|
||||||
fields: fields{A: pixel.V(1, 1), B: pixel.V(3, 3)},
|
|
||||||
args: args{around: pixel.V(0, 0), angle: math.Pi},
|
|
||||||
want: pixel.L(pixel.V(-1, -1), pixel.V(-3, -3)),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Rotating around line end",
|
|
||||||
fields: fields{A: pixel.V(1, 1), B: pixel.V(3, 3)},
|
|
||||||
args: args{around: pixel.V(1, 1), angle: math.Pi},
|
|
||||||
want: pixel.L(pixel.V(1, 1), pixel.V(-1, -1)),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
l := pixel.Line{
|
|
||||||
A: tt.fields.A,
|
|
||||||
B: tt.fields.B,
|
|
||||||
}
|
|
||||||
// Have to round the results, due to floating-point in accuracies. Results are correct to approximately
|
|
||||||
// 10 decimal places.
|
|
||||||
got := l.Rotated(tt.args.around, tt.args.angle)
|
|
||||||
if round(got.A.X) != tt.want.A.X ||
|
|
||||||
round(got.B.X) != tt.want.B.X ||
|
|
||||||
round(got.A.Y) != tt.want.A.Y ||
|
|
||||||
round(got.B.Y) != tt.want.B.Y {
|
|
||||||
t.Errorf("Line.Rotated() = %v, want %v", got, tt.want)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestLine_Scaled(t *testing.T) {
|
|
||||||
type fields struct {
|
|
||||||
A pixel.Vec
|
|
||||||
B pixel.Vec
|
|
||||||
}
|
|
||||||
type args struct {
|
|
||||||
scale float64
|
|
||||||
}
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
fields fields
|
|
||||||
args args
|
|
||||||
want pixel.Line
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "Scaling by 1",
|
|
||||||
fields: fields{A: pixel.V(0, 0), B: pixel.V(10, 10)},
|
|
||||||
args: args{scale: 1},
|
|
||||||
want: pixel.L(pixel.V(0, 0), pixel.V(10, 10)),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Scaling by >1",
|
|
||||||
fields: fields{A: pixel.V(0, 0), B: pixel.V(10, 10)},
|
|
||||||
args: args{scale: 2},
|
|
||||||
want: pixel.L(pixel.V(-5, -5), pixel.V(15, 15)),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Scaling by <1",
|
|
||||||
fields: fields{A: pixel.V(0, 0), B: pixel.V(10, 10)},
|
|
||||||
args: args{scale: 0.5},
|
|
||||||
want: pixel.L(pixel.V(2.5, 2.5), pixel.V(7.5, 7.5)),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Scaling by -1",
|
|
||||||
fields: fields{A: pixel.V(0, 0), B: pixel.V(10, 10)},
|
|
||||||
args: args{scale: -1},
|
|
||||||
want: pixel.L(pixel.V(10, 10), pixel.V(0, 0)),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
l := pixel.Line{
|
|
||||||
A: tt.fields.A,
|
|
||||||
B: tt.fields.B,
|
|
||||||
}
|
|
||||||
if got := l.Scaled(tt.args.scale); !reflect.DeepEqual(got, tt.want) {
|
|
||||||
t.Errorf("Line.Scaled() = %v, want %v", got, tt.want)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestLine_ScaledXY(t *testing.T) {
|
|
||||||
type fields struct {
|
|
||||||
A pixel.Vec
|
|
||||||
B pixel.Vec
|
|
||||||
}
|
|
||||||
type args struct {
|
|
||||||
around pixel.Vec
|
|
||||||
scale float64
|
|
||||||
}
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
fields fields
|
|
||||||
args args
|
|
||||||
want pixel.Line
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "Scaling by 1 around origin",
|
|
||||||
fields: fields{A: pixel.V(0, 0), B: pixel.V(10, 10)},
|
|
||||||
args: args{around: pixel.ZV, scale: 1},
|
|
||||||
want: pixel.L(pixel.V(0, 0), pixel.V(10, 10)),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Scaling by >1 around origin",
|
|
||||||
fields: fields{A: pixel.V(0, 0), B: pixel.V(10, 10)},
|
|
||||||
args: args{around: pixel.ZV, scale: 2},
|
|
||||||
want: pixel.L(pixel.V(0, 0), pixel.V(20, 20)),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Scaling by <1 around origin",
|
|
||||||
fields: fields{A: pixel.V(0, 0), B: pixel.V(10, 10)},
|
|
||||||
args: args{around: pixel.ZV, scale: 0.5},
|
|
||||||
want: pixel.L(pixel.V(0, 0), pixel.V(5, 5)),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Scaling by -1 around origin",
|
|
||||||
fields: fields{A: pixel.V(0, 0), B: pixel.V(10, 10)},
|
|
||||||
args: args{around: pixel.ZV, scale: -1},
|
|
||||||
want: pixel.L(pixel.V(0, 0), pixel.V(-10, -10)),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
l := pixel.Line{
|
|
||||||
A: tt.fields.A,
|
|
||||||
B: tt.fields.B,
|
|
||||||
}
|
|
||||||
if got := l.ScaledXY(tt.args.around, tt.args.scale); !reflect.DeepEqual(got, tt.want) {
|
|
||||||
t.Errorf("Line.ScaledXY() = %v, want %v", got, tt.want)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestLine_String(t *testing.T) {
|
|
||||||
type fields struct {
|
|
||||||
A pixel.Vec
|
|
||||||
B pixel.Vec
|
|
||||||
}
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
fields fields
|
|
||||||
want string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "Getting string",
|
|
||||||
fields: fields{A: pixel.V(0, 0), B: pixel.V(1, 1)},
|
|
||||||
want: "Line(Vec(0, 0), Vec(1, 1))",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
l := pixel.Line{
|
|
||||||
A: tt.fields.A,
|
|
||||||
B: tt.fields.B,
|
|
||||||
}
|
|
||||||
if got := l.String(); got != tt.want {
|
|
||||||
t.Errorf("Line.String() = %v, want %v", got, tt.want)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
Before Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 19 KiB |
Before Width: | Height: | Size: 18 KiB |
Before Width: | Height: | Size: 20 KiB |
Before Width: | Height: | Size: 17 KiB |
Before Width: | Height: | Size: 12 KiB |
15
math.go
|
@ -1,15 +0,0 @@
|
||||||
package pixel
|
|
||||||
|
|
||||||
// Clamp returns x clamped to the interval [min, max].
|
|
||||||
//
|
|
||||||
// If x is less than min, min is returned. If x is more than max, max is returned. Otherwise, x is
|
|
||||||
// returned.
|
|
||||||
func Clamp(x, min, max float64) float64 {
|
|
||||||
if x < min {
|
|
||||||
return min
|
|
||||||
}
|
|
||||||
if x > max {
|
|
||||||
return max
|
|
||||||
}
|
|
||||||
return x
|
|
||||||
}
|
|
46
math_test.go
|
@ -1,46 +0,0 @@
|
||||||
package pixel_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"math"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/faiface/pixel"
|
|
||||||
)
|
|
||||||
|
|
||||||
// closeEnough will shift the decimal point by the accuracy required, truncates the results and compares them.
|
|
||||||
// Effectively this compares two floats to a given decimal point.
|
|
||||||
// Example:
|
|
||||||
// closeEnough(100.125342432, 100.125, 2) == true
|
|
||||||
// closeEnough(math.Pi, 3.14, 2) == true
|
|
||||||
// closeEnough(0.1234, 0.1245, 3) == false
|
|
||||||
func closeEnough(got, expected float64, decimalAccuracy int) bool {
|
|
||||||
gotShifted := got * math.Pow10(decimalAccuracy)
|
|
||||||
expectedShifted := expected * math.Pow10(decimalAccuracy)
|
|
||||||
|
|
||||||
return math.Trunc(gotShifted) == math.Trunc(expectedShifted)
|
|
||||||
}
|
|
||||||
|
|
||||||
type clampTest struct {
|
|
||||||
number float64
|
|
||||||
min float64
|
|
||||||
max float64
|
|
||||||
expected float64
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestClamp(t *testing.T) {
|
|
||||||
tests := []clampTest{
|
|
||||||
{number: 1, min: 0, max: 5, expected: 1},
|
|
||||||
{number: 2, min: 0, max: 5, expected: 2},
|
|
||||||
{number: 8, min: 0, max: 5, expected: 5},
|
|
||||||
{number: -5, min: 0, max: 5, expected: 0},
|
|
||||||
{number: -5, min: -4, max: 5, expected: -4},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tc := range tests {
|
|
||||||
result := pixel.Clamp(tc.number, tc.min, tc.max)
|
|
||||||
if result != tc.expected {
|
|
||||||
t.Error(fmt.Sprintf("Clamping %v with min %v and max %v should have given %v, but gave %v", tc.number, tc.min, tc.max, tc.expected, result))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
98
matrix.go
|
@ -1,98 +0,0 @@
|
||||||
package pixel
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"math"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Matrix is a 2x3 affine matrix that can be used for all kinds of spatial transforms, such
|
|
||||||
// as movement, scaling and rotations.
|
|
||||||
//
|
|
||||||
// Matrix has a handful of useful methods, each of which adds a transformation to the matrix. For
|
|
||||||
// example:
|
|
||||||
//
|
|
||||||
// pixel.IM.Moved(pixel.V(100, 200)).Rotated(pixel.ZV, math.Pi/2)
|
|
||||||
//
|
|
||||||
// This code creates a Matrix that first moves everything by 100 units horizontally and 200 units
|
|
||||||
// vertically and then rotates everything by 90 degrees around the origin.
|
|
||||||
//
|
|
||||||
// Layout is:
|
|
||||||
// [0] [2] [4]
|
|
||||||
// [1] [3] [5]
|
|
||||||
// 0 0 1 (implicit row)
|
|
||||||
type Matrix [6]float64
|
|
||||||
|
|
||||||
// IM stands for identity matrix. Does nothing, no transformation.
|
|
||||||
var IM = Matrix{1, 0, 0, 1, 0, 0}
|
|
||||||
|
|
||||||
// String returns a string representation of the Matrix.
|
|
||||||
//
|
|
||||||
// m := pixel.IM
|
|
||||||
// fmt.Println(m) // Matrix(1 0 0 | 0 1 0)
|
|
||||||
func (m Matrix) String() string {
|
|
||||||
return fmt.Sprintf(
|
|
||||||
"Matrix(%v %v %v | %v %v %v)",
|
|
||||||
m[0], m[2], m[4],
|
|
||||||
m[1], m[3], m[5],
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Moved moves everything by the delta vector.
|
|
||||||
func (m Matrix) Moved(delta Vec) Matrix {
|
|
||||||
m[4], m[5] = m[4]+delta.X, m[5]+delta.Y
|
|
||||||
return m
|
|
||||||
}
|
|
||||||
|
|
||||||
// ScaledXY scales everything around a given point by the scale factor in each axis respectively.
|
|
||||||
func (m Matrix) ScaledXY(around Vec, scale Vec) Matrix {
|
|
||||||
m[4], m[5] = m[4]-around.X, m[5]-around.Y
|
|
||||||
m[0], m[2], m[4] = m[0]*scale.X, m[2]*scale.X, m[4]*scale.X
|
|
||||||
m[1], m[3], m[5] = m[1]*scale.Y, m[3]*scale.Y, m[5]*scale.Y
|
|
||||||
m[4], m[5] = m[4]+around.X, m[5]+around.Y
|
|
||||||
return m
|
|
||||||
}
|
|
||||||
|
|
||||||
// Scaled scales everything around a given point by the scale factor.
|
|
||||||
func (m Matrix) Scaled(around Vec, scale float64) Matrix {
|
|
||||||
return m.ScaledXY(around, V(scale, scale))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Rotated rotates everything around a given point by the given angle in radians.
|
|
||||||
func (m Matrix) Rotated(around Vec, angle float64) Matrix {
|
|
||||||
sint, cost := math.Sincos(angle)
|
|
||||||
m[4], m[5] = m[4]-around.X, m[5]-around.Y
|
|
||||||
m = m.Chained(Matrix{cost, sint, -sint, cost, 0, 0})
|
|
||||||
m[4], m[5] = m[4]+around.X, m[5]+around.Y
|
|
||||||
return m
|
|
||||||
}
|
|
||||||
|
|
||||||
// Chained adds another Matrix to this one. All tranformations by the next Matrix will be applied
|
|
||||||
// after the transformations of this Matrix.
|
|
||||||
func (m Matrix) Chained(next Matrix) Matrix {
|
|
||||||
return Matrix{
|
|
||||||
next[0]*m[0] + next[2]*m[1],
|
|
||||||
next[1]*m[0] + next[3]*m[1],
|
|
||||||
next[0]*m[2] + next[2]*m[3],
|
|
||||||
next[1]*m[2] + next[3]*m[3],
|
|
||||||
next[0]*m[4] + next[2]*m[5] + next[4],
|
|
||||||
next[1]*m[4] + next[3]*m[5] + next[5],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Project applies all transformations added to the Matrix to a vector u and returns the result.
|
|
||||||
//
|
|
||||||
// Time complexity is O(1).
|
|
||||||
func (m Matrix) Project(u Vec) Vec {
|
|
||||||
return Vec{m[0]*u.X + m[2]*u.Y + m[4], m[1]*u.X + m[3]*u.Y + m[5]}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Unproject does the inverse operation to Project.
|
|
||||||
//
|
|
||||||
// Time complexity is O(1).
|
|
||||||
func (m Matrix) Unproject(u Vec) Vec {
|
|
||||||
det := m[0]*m[3] - m[2]*m[1]
|
|
||||||
return Vec{
|
|
||||||
(m[3]*(u.X-m[4]) - m[2]*(u.Y-m[5])) / det,
|
|
||||||
(-m[1]*(u.X-m[4]) + m[0]*(u.Y-m[5])) / det,
|
|
||||||
}
|
|
||||||
}
|
|
149
matrix_test.go
|
@ -1,149 +0,0 @@
|
||||||
package pixel_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"math"
|
|
||||||
"math/rand"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/faiface/pixel"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
)
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestMatrix_Unproject(t *testing.T) {
|
|
||||||
const delta = 1e-15
|
|
||||||
t.Run("for rotated matrix", func(t *testing.T) {
|
|
||||||
matrix := pixel.IM.
|
|
||||||
Rotated(pixel.ZV, math.Pi/2)
|
|
||||||
unprojected := matrix.Unproject(pixel.V(0, 1))
|
|
||||||
assert.InDelta(t, unprojected.X, 1, delta)
|
|
||||||
assert.InDelta(t, unprojected.Y, 0, delta)
|
|
||||||
})
|
|
||||||
t.Run("for moved matrix", func(t *testing.T) {
|
|
||||||
matrix := pixel.IM.
|
|
||||||
Moved(pixel.V(1, 2))
|
|
||||||
unprojected := matrix.Unproject(pixel.V(2, 5))
|
|
||||||
assert.InDelta(t, unprojected.X, 1, delta)
|
|
||||||
assert.InDelta(t, unprojected.Y, 3, delta)
|
|
||||||
})
|
|
||||||
t.Run("for scaled matrix", func(t *testing.T) {
|
|
||||||
matrix := pixel.IM.
|
|
||||||
Scaled(pixel.ZV, 2)
|
|
||||||
unprojected := matrix.Unproject(pixel.V(2, 4))
|
|
||||||
assert.InDelta(t, unprojected.X, 1, delta)
|
|
||||||
assert.InDelta(t, unprojected.Y, 2, delta)
|
|
||||||
})
|
|
||||||
t.Run("for scaled, rotated and moved matrix", func(t *testing.T) {
|
|
||||||
matrix := pixel.IM.
|
|
||||||
Scaled(pixel.ZV, 2).
|
|
||||||
Rotated(pixel.ZV, math.Pi/2).
|
|
||||||
Moved(pixel.V(2, 2))
|
|
||||||
unprojected := matrix.Unproject(pixel.V(-2, 6))
|
|
||||||
assert.InDelta(t, unprojected.X, 2, delta)
|
|
||||||
assert.InDelta(t, unprojected.Y, 2, delta)
|
|
||||||
})
|
|
||||||
t.Run("for rotated and moved matrix", func(t *testing.T) {
|
|
||||||
matrix := pixel.IM.
|
|
||||||
Rotated(pixel.ZV, math.Pi/2).
|
|
||||||
Moved(pixel.V(1, 1))
|
|
||||||
unprojected := matrix.Unproject(pixel.V(1, 2))
|
|
||||||
assert.InDelta(t, unprojected.X, 1, delta)
|
|
||||||
assert.InDelta(t, unprojected.Y, 0, delta)
|
|
||||||
})
|
|
||||||
t.Run("for projected vertices using all kinds of matrices", func(t *testing.T) {
|
|
||||||
namedMatrices := map[string]pixel.Matrix{
|
|
||||||
"IM": pixel.IM,
|
|
||||||
"Scaled": pixel.IM.Scaled(pixel.ZV, 0.5),
|
|
||||||
"Scaled x 2": pixel.IM.Scaled(pixel.ZV, 2),
|
|
||||||
"Rotated": pixel.IM.Rotated(pixel.ZV, math.Pi/4),
|
|
||||||
"Moved": pixel.IM.Moved(pixel.V(0.5, 1)),
|
|
||||||
"Moved 2": pixel.IM.Moved(pixel.V(-1, -0.5)),
|
|
||||||
"Scaled and Rotated": pixel.IM.Scaled(pixel.ZV, 0.5).Rotated(pixel.ZV, math.Pi/4),
|
|
||||||
"Scaled, Rotated and Moved": pixel.IM.Scaled(pixel.ZV, 0.5).Rotated(pixel.ZV, math.Pi/4).Moved(pixel.V(1, 2)),
|
|
||||||
"Rotated and Moved": pixel.IM.Rotated(pixel.ZV, math.Pi/4).Moved(pixel.V(1, 2)),
|
|
||||||
}
|
|
||||||
vertices := [...]pixel.Vec{
|
|
||||||
pixel.V(0, 0),
|
|
||||||
pixel.V(5, 0),
|
|
||||||
pixel.V(5, 10),
|
|
||||||
pixel.V(0, 10),
|
|
||||||
pixel.V(-5, 10),
|
|
||||||
pixel.V(-5, 0),
|
|
||||||
pixel.V(-5, -10),
|
|
||||||
pixel.V(0, -10),
|
|
||||||
pixel.V(5, -10),
|
|
||||||
}
|
|
||||||
for matrixName, matrix := range namedMatrices {
|
|
||||||
for _, vertex := range vertices {
|
|
||||||
testCase := fmt.Sprintf("for matrix %s and vertex %v", matrixName, vertex)
|
|
||||||
t.Run(testCase, func(t *testing.T) {
|
|
||||||
projected := matrix.Project(vertex)
|
|
||||||
unprojected := matrix.Unproject(projected)
|
|
||||||
assert.InDelta(t, vertex.X, unprojected.X, delta)
|
|
||||||
assert.InDelta(t, vertex.Y, unprojected.Y, delta)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
t.Run("for singular matrix", func(t *testing.T) {
|
|
||||||
matrix := pixel.Matrix{0, 0, 0, 0, 0, 0}
|
|
||||||
unprojected := matrix.Unproject(pixel.ZV)
|
|
||||||
assert.True(t, math.IsNaN(unprojected.X))
|
|
||||||
assert.True(t, math.IsNaN(unprojected.Y))
|
|
||||||
})
|
|
||||||
}
|
|
|
@ -1,68 +0,0 @@
|
||||||
package pixel_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"image"
|
|
||||||
"os"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
_ "image/png"
|
|
||||||
|
|
||||||
"github.com/faiface/pixel"
|
|
||||||
"github.com/faiface/pixel/pixelgl"
|
|
||||||
)
|
|
||||||
|
|
||||||
// onePixelImage is the byte representation of a 1x1 solid white png file
|
|
||||||
var onePixelImage = []byte{
|
|
||||||
137, 80, 78, 71, 13, 10, 26, 10, 0, 0, 0, 13, 73, 72, 68, 82, 0, 0, 0, 1, 0, 0, 0, 1, 8, 2,
|
|
||||||
0, 0, 0, 144, 119, 83, 222, 0, 0, 1, 130, 105, 67, 67, 80, 73, 67, 67, 32, 112, 114, 111, 102, 105, 108, 101, 0,
|
|
||||||
0, 40, 145, 125, 145, 59, 72, 3, 65, 20, 69, 143, 73, 68, 17, 37, 133, 41, 68, 44, 182, 80, 43, 5, 81, 17, 75,
|
|
||||||
141, 66, 16, 34, 132, 168, 96, 212, 194, 221, 141, 137, 66, 118, 13, 187, 9, 54, 150, 130, 109, 192, 194, 79,
|
|
||||||
227, 175, 176, 177, 214, 214, 194, 86, 16, 4, 63, 32, 54, 182, 86, 138, 54, 18, 214, 55, 73, 32, 65, 140, 3,
|
|
||||||
195, 28, 238, 188, 123, 121, 243, 6, 124, 71, 25, 211, 114, 3, 3, 96, 217, 57, 39, 30, 9, 107, 243, 137, 5, 173,
|
|
||||||
233, 149, 0, 65, 90, 104, 0, 221, 116, 179, 227, 177, 88, 148, 186, 235, 235, 94, 213, 193, 93, 191, 202, 170,
|
|
||||||
95, 247, 231, 106, 75, 174, 184, 38, 52, 104, 194, 99, 102, 214, 201, 9, 47, 11, 143, 108, 228, 178, 138, 247,
|
|
||||||
132, 67, 230, 170, 158, 20, 62, 23, 238, 115, 164, 65, 225, 71, 165, 27, 101, 126, 83, 156, 46, 177, 79, 101,
|
|
||||||
134, 156, 217, 248, 132, 112, 72, 88, 75, 215, 176, 81, 195, 230, 170, 99, 9, 15, 11, 119, 39, 45, 91, 242, 125,
|
|
||||||
243, 101, 78, 42, 222, 84, 108, 101, 242, 102, 165, 79, 245, 194, 214, 21, 123, 110, 70, 233, 178, 187, 136, 48,
|
|
||||||
197, 52, 49, 52, 12, 242, 172, 145, 33, 71, 191, 156, 182, 40, 46, 113, 185, 15, 215, 241, 119, 150, 252, 49,
|
|
||||||
113, 25, 226, 90, 195, 20, 199, 36, 235, 88, 232, 37, 63, 234, 15, 126, 207, 214, 77, 13, 13, 150, 147, 90, 195,
|
|
||||||
208, 248, 226, 121, 31, 61, 208, 180, 3, 197, 130, 231, 125, 31, 123, 94, 241, 4, 252, 207, 112, 101, 87, 253,
|
|
||||||
235, 71, 48, 250, 41, 122, 161, 170, 117, 31, 66, 112, 11, 46, 174, 171, 154, 177, 11, 151, 219, 208, 241, 148,
|
|
||||||
213, 29, 189, 36, 249, 101, 251, 82, 41, 120, 63, 147, 111, 74, 64, 251, 45, 180, 44, 150, 231, 86, 185, 231,
|
|
||||||
244, 1, 102, 101, 86, 209, 27, 216, 63, 128, 222, 180, 100, 47, 213, 121, 119, 115, 237, 220, 254, 173, 169,
|
|
||||||
204, 239, 7, 178, 211, 114, 90, 10, 150, 157, 65, 0, 0, 0, 9, 112, 72, 89, 115, 0, 0, 46, 35, 0, 0, 46, 35, 1,
|
|
||||||
120, 165, 63, 118, 0, 0, 0, 7, 116, 73, 77, 69, 7, 227, 4, 15, 10, 5, 36, 189, 4, 224, 88, 0, 0, 0, 25, 116, 69,
|
|
||||||
88, 116, 67, 111, 109, 109, 101, 110, 116, 0, 67, 114, 101, 97, 116, 101, 100, 32, 119, 105, 116, 104, 32, 71,
|
|
||||||
73, 77, 80, 87, 129, 14, 23, 0, 0, 0, 12, 73, 68, 65, 84, 8, 215, 99, 120, 241, 226, 61, 0, 5, 123, 2, 192, 194,
|
|
||||||
77, 211, 95, 0, 0, 0, 0, 73, 69, 78, 68, 174, 66, 96, 130,
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestMain(m *testing.M) {
|
|
||||||
pixelgl.Run(func() {
|
|
||||||
os.Exit(m.Run())
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSprite_Draw(t *testing.T) {
|
|
||||||
img, _, err := image.Decode(bytes.NewReader(onePixelImage))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Could not decode image: %v", err)
|
|
||||||
}
|
|
||||||
pic := pixel.PictureDataFromImage(img)
|
|
||||||
|
|
||||||
sprite := pixel.NewSprite(pic, pic.Bounds())
|
|
||||||
|
|
||||||
cfg := pixelgl.WindowConfig{
|
|
||||||
Title: "testing",
|
|
||||||
Bounds: pixel.R(0, 0, 150, 150),
|
|
||||||
Invisible: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
win, err := pixelgl.NewWindow(cfg)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Could not create window: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
sprite.Draw(win, pixel.IM)
|
|
||||||
}
|
|
|
@ -17,7 +17,7 @@ import (
|
||||||
// It supports TrianglesPosition, TrianglesColor, TrianglesPicture and PictureColor.
|
// It supports TrianglesPosition, TrianglesColor, TrianglesPicture and PictureColor.
|
||||||
type Canvas struct {
|
type Canvas struct {
|
||||||
gf *GLFrame
|
gf *GLFrame
|
||||||
shader *GLShader
|
shader *glhf.Shader
|
||||||
|
|
||||||
cmp pixel.ComposeMethod
|
cmp pixel.ComposeMethod
|
||||||
mat mgl32.Mat3
|
mat mgl32.Mat3
|
||||||
|
@ -37,35 +37,30 @@ func NewCanvas(bounds pixel.Rect) *Canvas {
|
||||||
col: mgl32.Vec4{1, 1, 1, 1},
|
col: mgl32.Vec4{1, 1, 1, 1},
|
||||||
}
|
}
|
||||||
|
|
||||||
c.shader = NewGLShader(baseCanvasFragmentShader)
|
|
||||||
c.SetBounds(bounds)
|
c.SetBounds(bounds)
|
||||||
|
|
||||||
|
var shader *glhf.Shader
|
||||||
|
mainthread.Call(func() {
|
||||||
|
var err error
|
||||||
|
shader, err = glhf.NewShader(
|
||||||
|
canvasVertexFormat,
|
||||||
|
canvasUniformFormat,
|
||||||
|
canvasVertexShader,
|
||||||
|
canvasFragmentShader,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
panic(errors.Wrap(err, "failed to create Canvas, there's a bug in the shader"))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
c.shader = shader
|
||||||
|
|
||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetUniform will update the named uniform with the value of any supported underlying
|
|
||||||
// attribute variable. If the uniform already exists, including defaults, they will be reassigned
|
|
||||||
// to the new value. The value can be a pointer.
|
|
||||||
func (c *Canvas) SetUniform(name string, value interface{}) {
|
|
||||||
c.shader.SetUniform(name, value)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetFragmentShader allows you to set a new fragment shader on the underlying
|
|
||||||
// framebuffer. Argument "src" is the GLSL source, not a filename.
|
|
||||||
func (c *Canvas) SetFragmentShader(src string) {
|
|
||||||
c.shader.fs = src
|
|
||||||
c.shader.Update()
|
|
||||||
}
|
|
||||||
|
|
||||||
// MakeTriangles creates a specialized copy of the supplied Triangles that draws onto this Canvas.
|
// MakeTriangles creates a specialized copy of the supplied Triangles that draws onto this Canvas.
|
||||||
//
|
//
|
||||||
// TrianglesPosition, TrianglesColor and TrianglesPicture are supported.
|
// TrianglesPosition, TrianglesColor and TrianglesPicture are supported.
|
||||||
func (c *Canvas) MakeTriangles(t pixel.Triangles) pixel.TargetTriangles {
|
func (c *Canvas) MakeTriangles(t pixel.Triangles) pixel.TargetTriangles {
|
||||||
if gt, ok := t.(*GLTriangles); ok {
|
|
||||||
return &canvasTriangles{
|
|
||||||
GLTriangles: gt,
|
|
||||||
dst: c,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return &canvasTriangles{
|
return &canvasTriangles{
|
||||||
GLTriangles: NewGLTriangles(c.shader, t),
|
GLTriangles: NewGLTriangles(c.shader, t),
|
||||||
dst: c,
|
dst: c,
|
||||||
|
@ -96,13 +91,8 @@ func (c *Canvas) MakePicture(p pixel.Picture) pixel.TargetPicture {
|
||||||
|
|
||||||
// SetMatrix sets a Matrix that every point will be projected by.
|
// SetMatrix sets a Matrix that every point will be projected by.
|
||||||
func (c *Canvas) SetMatrix(m pixel.Matrix) {
|
func (c *Canvas) SetMatrix(m pixel.Matrix) {
|
||||||
// pixel.Matrix is 3x2 with an implicit 0, 0, 1 row after it. So
|
for i := range m {
|
||||||
// [0] [2] [4] [0] [3] [6]
|
c.mat[i] = float32(m[i])
|
||||||
// [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])
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -133,7 +123,7 @@ func (c *Canvas) SetBounds(bounds pixel.Rect) {
|
||||||
c.sprite = pixel.NewSprite(nil, pixel.Rect{})
|
c.sprite = pixel.NewSprite(nil, pixel.Rect{})
|
||||||
}
|
}
|
||||||
c.sprite.Set(c, c.Bounds())
|
c.sprite.Set(c, c.Bounds())
|
||||||
// c.sprite.SetMatrix(pixel.IM.Moved(c.Bounds().Center()))
|
c.sprite.SetMatrix(pixel.IM.Moved(c.Bounds().Center()))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Bounds returns the rectangular bounds of the Canvas.
|
// Bounds returns the rectangular bounds of the Canvas.
|
||||||
|
@ -228,50 +218,12 @@ func (c *Canvas) Texture() *glhf.Texture {
|
||||||
return c.gf.Texture()
|
return c.gf.Texture()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Frame returns the underlying OpenGL Frame of this Canvas.
|
// Draw draws a rectangle equal to Canvas's Bounds containing the Canvas's content to another
|
||||||
func (c *Canvas) Frame() *glhf.Frame {
|
// Target.
|
||||||
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.
|
// Note, that the matrix and the color mask of this Canvas have no effect here.
|
||||||
func (c *Canvas) DrawColorMask(t pixel.Target, matrix pixel.Matrix, mask color.Color) {
|
func (c *Canvas) Draw(t pixel.Target) {
|
||||||
c.sprite.DrawColorMask(t, matrix, mask)
|
c.sprite.Draw(t)
|
||||||
}
|
}
|
||||||
|
|
||||||
type canvasTriangles struct {
|
type canvasTriangles struct {
|
||||||
|
@ -284,41 +236,29 @@ func (ct *canvasTriangles) draw(tex *glhf.Texture, bounds pixel.Rect) {
|
||||||
|
|
||||||
// save the current state vars to avoid race condition
|
// save the current state vars to avoid race condition
|
||||||
cmp := ct.dst.cmp
|
cmp := ct.dst.cmp
|
||||||
smt := ct.dst.smooth
|
|
||||||
mat := ct.dst.mat
|
mat := ct.dst.mat
|
||||||
col := ct.dst.col
|
col := ct.dst.col
|
||||||
|
smt := ct.dst.smooth
|
||||||
|
|
||||||
mainthread.CallNonBlock(func() {
|
mainthread.CallNonBlock(func() {
|
||||||
ct.dst.setGlhfBounds()
|
ct.dst.setGlhfBounds()
|
||||||
setBlendFunc(cmp)
|
setBlendFunc(cmp)
|
||||||
|
|
||||||
frame := ct.dst.gf.Frame()
|
frame := ct.dst.gf.Frame()
|
||||||
shader := ct.shader.s
|
shader := ct.dst.shader
|
||||||
|
|
||||||
frame.Begin()
|
frame.Begin()
|
||||||
shader.Begin()
|
shader.Begin()
|
||||||
|
|
||||||
ct.shader.uniformDefaults.transform = mat
|
|
||||||
ct.shader.uniformDefaults.colormask = col
|
|
||||||
dstBounds := ct.dst.Bounds()
|
dstBounds := ct.dst.Bounds()
|
||||||
ct.shader.uniformDefaults.bounds = mgl32.Vec4{
|
shader.SetUniformAttr(canvasBounds, mgl32.Vec4{
|
||||||
float32(dstBounds.Min.X),
|
float32(dstBounds.Min.X()),
|
||||||
float32(dstBounds.Min.Y),
|
float32(dstBounds.Min.Y()),
|
||||||
float32(dstBounds.W()),
|
float32(dstBounds.W()),
|
||||||
float32(dstBounds.H()),
|
float32(dstBounds.H()),
|
||||||
}
|
})
|
||||||
|
shader.SetUniformAttr(canvasTransform, mat)
|
||||||
bx, by, bw, bh := intBounds(bounds)
|
shader.SetUniformAttr(canvasColorMask, col)
|
||||||
ct.shader.uniformDefaults.texbounds = mgl32.Vec4{
|
|
||||||
float32(bx),
|
|
||||||
float32(by),
|
|
||||||
float32(bw),
|
|
||||||
float32(bh),
|
|
||||||
}
|
|
||||||
|
|
||||||
for loc, u := range ct.shader.uniforms {
|
|
||||||
ct.shader.s.SetUniformAttr(loc, u.Value())
|
|
||||||
}
|
|
||||||
|
|
||||||
if tex == nil {
|
if tex == nil {
|
||||||
ct.vs.Begin()
|
ct.vs.Begin()
|
||||||
|
@ -327,6 +267,14 @@ func (ct *canvasTriangles) draw(tex *glhf.Texture, bounds pixel.Rect) {
|
||||||
} else {
|
} else {
|
||||||
tex.Begin()
|
tex.Begin()
|
||||||
|
|
||||||
|
bx, by, bw, bh := intBounds(bounds)
|
||||||
|
shader.SetUniformAttr(canvasTexBounds, mgl32.Vec4{
|
||||||
|
float32(bx),
|
||||||
|
float32(by),
|
||||||
|
float32(bw),
|
||||||
|
float32(bh),
|
||||||
|
})
|
||||||
|
|
||||||
if tex.Smooth() != smt {
|
if tex.Smooth() != smt {
|
||||||
tex.SetSmooth(smt)
|
tex.SetSmooth(smt)
|
||||||
}
|
}
|
||||||
|
@ -359,3 +307,82 @@ func (cp *canvasPicture) Draw(t pixel.TargetTriangles) {
|
||||||
}
|
}
|
||||||
ct.draw(cp.GLPicture.Texture(), cp.GLPicture.Bounds())
|
ct.draw(cp.GLPicture.Texture(), cp.GLPicture.Bounds())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
canvasPosition int = iota
|
||||||
|
canvasColor
|
||||||
|
canvasTexture
|
||||||
|
canvasIntensity
|
||||||
|
)
|
||||||
|
|
||||||
|
var canvasVertexFormat = glhf.AttrFormat{
|
||||||
|
canvasPosition: {Name: "position", Type: glhf.Vec2},
|
||||||
|
canvasColor: {Name: "color", Type: glhf.Vec4},
|
||||||
|
canvasTexture: {Name: "texture", Type: glhf.Vec2},
|
||||||
|
canvasIntensity: {Name: "intensity", Type: glhf.Float},
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
canvasTransform int = iota
|
||||||
|
canvasColorMask
|
||||||
|
canvasBounds
|
||||||
|
canvasTexBounds
|
||||||
|
)
|
||||||
|
|
||||||
|
var canvasUniformFormat = glhf.AttrFormat{
|
||||||
|
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 = `
|
||||||
|
#version 330 core
|
||||||
|
|
||||||
|
in vec2 position;
|
||||||
|
in vec4 color;
|
||||||
|
in vec2 texture;
|
||||||
|
in float intensity;
|
||||||
|
|
||||||
|
out vec4 Color;
|
||||||
|
out vec2 Texture;
|
||||||
|
out float Intensity;
|
||||||
|
|
||||||
|
uniform mat3 transform;
|
||||||
|
uniform vec4 bounds;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
vec2 transPos = (transform * vec3(position, 1.0)).xy;
|
||||||
|
vec2 normPos = (transPos - bounds.xy) / bounds.zw * 2 - vec2(1, 1);
|
||||||
|
gl_Position = vec4(normPos, 0.0, 1.0);
|
||||||
|
Color = color;
|
||||||
|
Texture = texture;
|
||||||
|
Intensity = intensity;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
var canvasFragmentShader = `
|
||||||
|
#version 330 core
|
||||||
|
|
||||||
|
in vec4 Color;
|
||||||
|
in vec2 Texture;
|
||||||
|
in float Intensity;
|
||||||
|
|
||||||
|
out vec4 color;
|
||||||
|
|
||||||
|
uniform vec4 colorMask;
|
||||||
|
uniform vec4 texBounds;
|
||||||
|
uniform sampler2D tex;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
if (Intensity == 0) {
|
||||||
|
color = colorMask * Color;
|
||||||
|
} else {
|
||||||
|
color = vec4(0, 0, 0, 0);
|
||||||
|
color += (1 - Intensity) * Color;
|
||||||
|
vec2 t = (Texture - texBounds.xy) / texBounds.zw;
|
||||||
|
color += Intensity * Color * texture(tex, t);
|
||||||
|
color *= colorMask;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
|
@ -32,12 +32,6 @@ func (gf *GLFrame) SetBounds(bounds pixel.Rect) {
|
||||||
oldF := gf.frame
|
oldF := gf.frame
|
||||||
|
|
||||||
_, _, w, h := intBounds(bounds)
|
_, _, w, h := intBounds(bounds)
|
||||||
if w <= 0 {
|
|
||||||
w = 1
|
|
||||||
}
|
|
||||||
if h <= 0 {
|
|
||||||
h = 1
|
|
||||||
}
|
|
||||||
gf.frame = glhf.NewFrame(w, h, false)
|
gf.frame = glhf.NewFrame(w, h, false)
|
||||||
|
|
||||||
// preserve old content
|
// preserve old content
|
||||||
|
@ -76,7 +70,7 @@ func (gf *GLFrame) Color(at pixel.Vec) pixel.RGBA {
|
||||||
return pixel.Alpha(0)
|
return pixel.Alpha(0)
|
||||||
}
|
}
|
||||||
bx, by, bw, _ := intBounds(gf.bounds)
|
bx, by, bw, _ := intBounds(gf.bounds)
|
||||||
x, y := int(at.X)-bx, int(at.Y)-by
|
x, y := int(at.X())-bx, int(at.Y())-by
|
||||||
off := y*bw + x
|
off := y*bw + x
|
||||||
return pixel.RGBA{
|
return pixel.RGBA{
|
||||||
R: float64(gf.pixels[off*4+0]) / 255,
|
R: float64(gf.pixels[off*4+0]) / 255,
|
||||||
|
|
|
@ -42,8 +42,8 @@ func NewGLPicture(p pixel.Picture) GLPicture {
|
||||||
for y := 0; y < bh; y++ {
|
for y := 0; y < bh; y++ {
|
||||||
for x := 0; x < bw; x++ {
|
for x := 0; x < bw; x++ {
|
||||||
at := pixel.V(
|
at := pixel.V(
|
||||||
math.Max(float64(bx+x), bounds.Min.X),
|
math.Max(float64(bx+x), bounds.Min.X()),
|
||||||
math.Max(float64(by+y), bounds.Min.Y),
|
math.Max(float64(by+y), bounds.Min.Y()),
|
||||||
)
|
)
|
||||||
color := p.Color(at)
|
color := p.Color(at)
|
||||||
off := (y*bw + x) * 4
|
off := (y*bw + x) * 4
|
||||||
|
@ -87,7 +87,7 @@ func (gp *glPicture) Color(at pixel.Vec) pixel.RGBA {
|
||||||
return pixel.Alpha(0)
|
return pixel.Alpha(0)
|
||||||
}
|
}
|
||||||
bx, by, bw, _ := intBounds(gp.bounds)
|
bx, by, bw, _ := intBounds(gp.bounds)
|
||||||
x, y := int(at.X)-bx, int(at.Y)-by
|
x, y := int(at.X())-bx, int(at.Y())-by
|
||||||
off := y*bw + x
|
off := y*bw + x
|
||||||
return pixel.RGBA{
|
return pixel.RGBA{
|
||||||
R: float64(gp.pixels[off*4+0]) / 255,
|
R: float64(gp.pixels[off*4+0]) / 255,
|
||||||
|
|
|
@ -1,298 +0,0 @@
|
||||||
package pixelgl
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/faiface/glhf"
|
|
||||||
"github.com/faiface/mainthread"
|
|
||||||
"github.com/go-gl/mathgl/mgl32"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
)
|
|
||||||
|
|
||||||
// GLShader is a type to assist with managing a canvas's underlying
|
|
||||||
// shader configuration. This allows for customization of shaders on
|
|
||||||
// a per canvas basis.
|
|
||||||
type GLShader struct {
|
|
||||||
s *glhf.Shader
|
|
||||||
vf, uf glhf.AttrFormat
|
|
||||||
vs, fs string
|
|
||||||
|
|
||||||
uniforms []gsUniformAttr
|
|
||||||
|
|
||||||
uniformDefaults struct {
|
|
||||||
transform mgl32.Mat3
|
|
||||||
colormask mgl32.Vec4
|
|
||||||
bounds mgl32.Vec4
|
|
||||||
texbounds mgl32.Vec4
|
|
||||||
cliprect mgl32.Vec4
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type gsUniformAttr struct {
|
|
||||||
Name string
|
|
||||||
Type glhf.AttrType
|
|
||||||
value interface{}
|
|
||||||
ispointer bool
|
|
||||||
}
|
|
||||||
|
|
||||||
const (
|
|
||||||
canvasPosition int = iota
|
|
||||||
canvasColor
|
|
||||||
canvasTexCoords
|
|
||||||
canvasIntensity
|
|
||||||
canvasClip
|
|
||||||
)
|
|
||||||
|
|
||||||
var defaultCanvasVertexFormat = glhf.AttrFormat{
|
|
||||||
canvasPosition: glhf.Attr{Name: "aPosition", Type: glhf.Vec2},
|
|
||||||
canvasColor: glhf.Attr{Name: "aColor", Type: glhf.Vec4},
|
|
||||||
canvasTexCoords: glhf.Attr{Name: "aTexCoords", Type: glhf.Vec2},
|
|
||||||
canvasIntensity: glhf.Attr{Name: "aIntensity", Type: glhf.Float},
|
|
||||||
canvasClip: glhf.Attr{Name: "aClipRect", Type: glhf.Vec4},
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sets up a base shader with everything needed for a Pixel
|
|
||||||
// canvas to render correctly. The defaults can be overridden
|
|
||||||
// by simply using the SetUniform function.
|
|
||||||
func NewGLShader(fragmentShader string) *GLShader {
|
|
||||||
gs := &GLShader{
|
|
||||||
vf: defaultCanvasVertexFormat,
|
|
||||||
vs: baseCanvasVertexShader,
|
|
||||||
fs: fragmentShader,
|
|
||||||
}
|
|
||||||
|
|
||||||
gs.SetUniform("uTransform", &gs.uniformDefaults.transform)
|
|
||||||
gs.SetUniform("uColorMask", &gs.uniformDefaults.colormask)
|
|
||||||
gs.SetUniform("uBounds", &gs.uniformDefaults.bounds)
|
|
||||||
gs.SetUniform("uTexBounds", &gs.uniformDefaults.texbounds)
|
|
||||||
|
|
||||||
gs.Update()
|
|
||||||
|
|
||||||
return gs
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update reinitialize GLShader data and recompile the underlying gl shader object
|
|
||||||
func (gs *GLShader) Update() {
|
|
||||||
gs.uf = make([]glhf.Attr, len(gs.uniforms))
|
|
||||||
for idx := range gs.uniforms {
|
|
||||||
gs.uf[idx] = glhf.Attr{
|
|
||||||
Name: gs.uniforms[idx].Name,
|
|
||||||
Type: gs.uniforms[idx].Type,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var shader *glhf.Shader
|
|
||||||
mainthread.Call(func() {
|
|
||||||
var err error
|
|
||||||
shader, err = glhf.NewShader(
|
|
||||||
gs.vf,
|
|
||||||
gs.uf,
|
|
||||||
gs.vs,
|
|
||||||
gs.fs,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
panic(errors.Wrap(err, "failed to create Canvas, there's a bug in the shader"))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
gs.s = shader
|
|
||||||
}
|
|
||||||
|
|
||||||
// gets the uniform index from GLShader
|
|
||||||
func (gs *GLShader) getUniform(Name string) int {
|
|
||||||
for i, u := range gs.uniforms {
|
|
||||||
if u.Name == Name {
|
|
||||||
return i
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return -1
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetUniform appends a custom uniform name and value to the shader.
|
|
||||||
// if the uniform already exists, it will simply be overwritten.
|
|
||||||
//
|
|
||||||
// example:
|
|
||||||
//
|
|
||||||
// utime := float32(time.Since(starttime)).Seconds())
|
|
||||||
// mycanvas.shader.AddUniform("u_time", &utime)
|
|
||||||
func (gs *GLShader) SetUniform(name string, value interface{}) {
|
|
||||||
t, p := getAttrType(value)
|
|
||||||
if loc := gs.getUniform(name); loc > -1 {
|
|
||||||
gs.uniforms[loc].Name = name
|
|
||||||
gs.uniforms[loc].Type = t
|
|
||||||
gs.uniforms[loc].ispointer = p
|
|
||||||
gs.uniforms[loc].value = value
|
|
||||||
return
|
|
||||||
}
|
|
||||||
gs.uniforms = append(gs.uniforms, gsUniformAttr{
|
|
||||||
Name: name,
|
|
||||||
Type: t,
|
|
||||||
ispointer: p,
|
|
||||||
value: value,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Value returns the attribute's concrete value. If the stored value
|
|
||||||
// is a pointer, we return the dereferenced value.
|
|
||||||
func (gu *gsUniformAttr) Value() interface{} {
|
|
||||||
if !gu.ispointer {
|
|
||||||
return gu.value
|
|
||||||
}
|
|
||||||
switch gu.Type {
|
|
||||||
case glhf.Vec2:
|
|
||||||
return *gu.value.(*mgl32.Vec2)
|
|
||||||
case glhf.Vec3:
|
|
||||||
return *gu.value.(*mgl32.Vec3)
|
|
||||||
case glhf.Vec4:
|
|
||||||
return *gu.value.(*mgl32.Vec4)
|
|
||||||
case glhf.Mat2:
|
|
||||||
return *gu.value.(*mgl32.Mat2)
|
|
||||||
case glhf.Mat23:
|
|
||||||
return *gu.value.(*mgl32.Mat2x3)
|
|
||||||
case glhf.Mat24:
|
|
||||||
return *gu.value.(*mgl32.Mat2x4)
|
|
||||||
case glhf.Mat3:
|
|
||||||
return *gu.value.(*mgl32.Mat3)
|
|
||||||
case glhf.Mat32:
|
|
||||||
return *gu.value.(*mgl32.Mat3x2)
|
|
||||||
case glhf.Mat34:
|
|
||||||
return *gu.value.(*mgl32.Mat3x4)
|
|
||||||
case glhf.Mat4:
|
|
||||||
return *gu.value.(*mgl32.Mat4)
|
|
||||||
case glhf.Mat42:
|
|
||||||
return *gu.value.(*mgl32.Mat4x2)
|
|
||||||
case glhf.Mat43:
|
|
||||||
return *gu.value.(*mgl32.Mat4x3)
|
|
||||||
case glhf.Int:
|
|
||||||
return *gu.value.(*int32)
|
|
||||||
case glhf.Float:
|
|
||||||
return *gu.value.(*float32)
|
|
||||||
default:
|
|
||||||
panic("invalid attrtype")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returns the type identifier for any (supported) attribute variable type
|
|
||||||
// and whether or not it is a pointer of that type.
|
|
||||||
func getAttrType(v interface{}) (glhf.AttrType, bool) {
|
|
||||||
switch v.(type) {
|
|
||||||
case int32:
|
|
||||||
return glhf.Int, false
|
|
||||||
case float32:
|
|
||||||
return glhf.Float, false
|
|
||||||
case mgl32.Vec2:
|
|
||||||
return glhf.Vec2, false
|
|
||||||
case mgl32.Vec3:
|
|
||||||
return glhf.Vec3, false
|
|
||||||
case mgl32.Vec4:
|
|
||||||
return glhf.Vec4, false
|
|
||||||
case mgl32.Mat2:
|
|
||||||
return glhf.Mat2, false
|
|
||||||
case mgl32.Mat2x3:
|
|
||||||
return glhf.Mat23, false
|
|
||||||
case mgl32.Mat2x4:
|
|
||||||
return glhf.Mat24, false
|
|
||||||
case mgl32.Mat3:
|
|
||||||
return glhf.Mat3, false
|
|
||||||
case mgl32.Mat3x2:
|
|
||||||
return glhf.Mat32, false
|
|
||||||
case mgl32.Mat3x4:
|
|
||||||
return glhf.Mat34, false
|
|
||||||
case mgl32.Mat4:
|
|
||||||
return glhf.Mat4, false
|
|
||||||
case mgl32.Mat4x2:
|
|
||||||
return glhf.Mat42, false
|
|
||||||
case mgl32.Mat4x3:
|
|
||||||
return glhf.Mat43, false
|
|
||||||
case *mgl32.Vec2:
|
|
||||||
return glhf.Vec2, true
|
|
||||||
case *mgl32.Vec3:
|
|
||||||
return glhf.Vec3, true
|
|
||||||
case *mgl32.Vec4:
|
|
||||||
return glhf.Vec4, true
|
|
||||||
case *mgl32.Mat2:
|
|
||||||
return glhf.Mat2, true
|
|
||||||
case *mgl32.Mat2x3:
|
|
||||||
return glhf.Mat23, true
|
|
||||||
case *mgl32.Mat2x4:
|
|
||||||
return glhf.Mat24, true
|
|
||||||
case *mgl32.Mat3:
|
|
||||||
return glhf.Mat3, true
|
|
||||||
case *mgl32.Mat3x2:
|
|
||||||
return glhf.Mat32, true
|
|
||||||
case *mgl32.Mat3x4:
|
|
||||||
return glhf.Mat34, true
|
|
||||||
case *mgl32.Mat4:
|
|
||||||
return glhf.Mat4, true
|
|
||||||
case *mgl32.Mat4x2:
|
|
||||||
return glhf.Mat42, true
|
|
||||||
case *mgl32.Mat4x3:
|
|
||||||
return glhf.Mat43, true
|
|
||||||
case *int32:
|
|
||||||
return glhf.Int, true
|
|
||||||
case *float32:
|
|
||||||
return glhf.Float, true
|
|
||||||
default:
|
|
||||||
panic("invalid AttrType")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var baseCanvasVertexShader = `
|
|
||||||
#version 330 core
|
|
||||||
|
|
||||||
in vec2 aPosition;
|
|
||||||
in vec4 aColor;
|
|
||||||
in vec2 aTexCoords;
|
|
||||||
in float aIntensity;
|
|
||||||
in vec4 aClipRect;
|
|
||||||
in float aIsClipped;
|
|
||||||
|
|
||||||
out vec4 vColor;
|
|
||||||
out vec2 vTexCoords;
|
|
||||||
out float vIntensity;
|
|
||||||
out vec2 vPosition;
|
|
||||||
out vec4 vClipRect;
|
|
||||||
|
|
||||||
uniform mat3 uTransform;
|
|
||||||
uniform vec4 uBounds;
|
|
||||||
|
|
||||||
void main() {
|
|
||||||
vec2 transPos = (uTransform * vec3(aPosition, 1.0)).xy;
|
|
||||||
vec2 normPos = (transPos - uBounds.xy) / uBounds.zw * 2 - vec2(1, 1);
|
|
||||||
gl_Position = vec4(normPos, 0.0, 1.0);
|
|
||||||
|
|
||||||
vColor = aColor;
|
|
||||||
vPosition = aPosition;
|
|
||||||
vTexCoords = aTexCoords;
|
|
||||||
vIntensity = aIntensity;
|
|
||||||
vClipRect = aClipRect;
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
var baseCanvasFragmentShader = `
|
|
||||||
#version 330 core
|
|
||||||
|
|
||||||
in vec4 vColor;
|
|
||||||
in vec2 vTexCoords;
|
|
||||||
in float vIntensity;
|
|
||||||
in vec4 vClipRect;
|
|
||||||
|
|
||||||
out vec4 fragColor;
|
|
||||||
|
|
||||||
uniform vec4 uColorMask;
|
|
||||||
uniform vec4 uTexBounds;
|
|
||||||
uniform sampler2D uTexture;
|
|
||||||
|
|
||||||
void main() {
|
|
||||||
if ((vClipRect != vec4(0,0,0,0)) && (gl_FragCoord.x < vClipRect.x || gl_FragCoord.y < vClipRect.y || gl_FragCoord.x > vClipRect.z || gl_FragCoord.y > vClipRect.w))
|
|
||||||
discard;
|
|
||||||
|
|
||||||
if (vIntensity == 0) {
|
|
||||||
fragColor = uColorMask * vColor;
|
|
||||||
} else {
|
|
||||||
fragColor = vec4(0, 0, 0, 0);
|
|
||||||
fragColor += (1 - vIntensity) * vColor;
|
|
||||||
vec2 t = (vTexCoords - uTexBounds.xy) / uTexBounds.zw;
|
|
||||||
fragColor += vIntensity * vColor * texture(uTexture, t);
|
|
||||||
fragColor *= uColorMask;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`
|
|
|
@ -15,43 +15,23 @@ import (
|
||||||
type GLTriangles struct {
|
type GLTriangles struct {
|
||||||
vs *glhf.VertexSlice
|
vs *glhf.VertexSlice
|
||||||
data []float32
|
data []float32
|
||||||
shader *GLShader
|
shader *glhf.Shader
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
_ pixel.TrianglesPosition = (*GLTriangles)(nil)
|
_ pixel.TrianglesPosition = (*GLTriangles)(nil)
|
||||||
_ pixel.TrianglesColor = (*GLTriangles)(nil)
|
_ pixel.TrianglesColor = (*GLTriangles)(nil)
|
||||||
_ pixel.TrianglesPicture = (*GLTriangles)(nil)
|
_ pixel.TrianglesPicture = (*GLTriangles)(nil)
|
||||||
_ pixel.TrianglesClipped = (*GLTriangles)(nil)
|
|
||||||
)
|
|
||||||
|
|
||||||
// The following is a helper so that the indices of
|
|
||||||
// each of these items is easier to see/debug.
|
|
||||||
const (
|
|
||||||
triPosX = iota
|
|
||||||
triPosY
|
|
||||||
triColorR
|
|
||||||
triColorG
|
|
||||||
triColorB
|
|
||||||
triColorA
|
|
||||||
triPicX
|
|
||||||
triPicY
|
|
||||||
triIntensity
|
|
||||||
triClipMinX
|
|
||||||
triClipMinY
|
|
||||||
triClipMaxX
|
|
||||||
triClipMaxY
|
|
||||||
trisAttrLen
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// NewGLTriangles returns GLTriangles initialized with the data from the supplied Triangles.
|
// NewGLTriangles returns GLTriangles initialized with the data from the supplied Triangles.
|
||||||
//
|
//
|
||||||
// Only draw the Triangles using the provided Shader.
|
// Only draw the Triangles using the provided Shader.
|
||||||
func NewGLTriangles(shader *GLShader, t pixel.Triangles) *GLTriangles {
|
func NewGLTriangles(shader *glhf.Shader, t pixel.Triangles) *GLTriangles {
|
||||||
var gt *GLTriangles
|
var gt *GLTriangles
|
||||||
mainthread.Call(func() {
|
mainthread.Call(func() {
|
||||||
gt = &GLTriangles{
|
gt = &GLTriangles{
|
||||||
vs: glhf.MakeVertexSlice(shader.s, 0, t.Len()),
|
vs: glhf.MakeVertexSlice(shader, 0, t.Len()),
|
||||||
shader: shader,
|
shader: shader,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -68,7 +48,7 @@ func (gt *GLTriangles) VertexSlice() *glhf.VertexSlice {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Shader returns the GLTriangles's associated shader.
|
// Shader returns the GLTriangles's associated shader.
|
||||||
func (gt *GLTriangles) Shader() *GLShader {
|
func (gt *GLTriangles) Shader() *glhf.Shader {
|
||||||
return gt.shader
|
return gt.shader
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -80,29 +60,22 @@ func (gt *GLTriangles) Len() int {
|
||||||
// SetLen efficiently resizes GLTriangles to len.
|
// SetLen efficiently resizes GLTriangles to len.
|
||||||
//
|
//
|
||||||
// Time complexity is amortized O(1).
|
// Time complexity is amortized O(1).
|
||||||
func (gt *GLTriangles) SetLen(length int) {
|
func (gt *GLTriangles) SetLen(len int) {
|
||||||
switch {
|
if len > gt.Len() {
|
||||||
case length > gt.Len():
|
needAppend := len - gt.Len()
|
||||||
needAppend := length - gt.Len()
|
|
||||||
for i := 0; i < needAppend; i++ {
|
for i := 0; i < needAppend; i++ {
|
||||||
gt.data = append(gt.data,
|
gt.data = append(gt.data,
|
||||||
0, 0,
|
0, 0,
|
||||||
1, 1, 1, 1,
|
1, 1, 1, 1,
|
||||||
0, 0,
|
0, 0,
|
||||||
0,
|
0,
|
||||||
0, 0, 0, 0,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
case length < gt.Len():
|
|
||||||
gt.data = gt.data[:length*gt.vs.Stride()]
|
|
||||||
default:
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
mainthread.Call(func() {
|
if len < gt.Len() {
|
||||||
gt.vs.Begin()
|
gt.data = gt.data[:len*gt.vs.Stride()]
|
||||||
gt.vs.SetLen(length)
|
}
|
||||||
gt.vs.End()
|
gt.submitData()
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Slice returns a sub-Triangles of this GLTriangles in range [i, j).
|
// Slice returns a sub-Triangles of this GLTriangles in range [i, j).
|
||||||
|
@ -122,67 +95,73 @@ func (gt *GLTriangles) updateData(t pixel.Triangles) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// TrianglesData short path
|
// TrianglesData short path
|
||||||
stride := gt.vs.Stride()
|
|
||||||
length := gt.Len()
|
|
||||||
if t, ok := t.(*pixel.TrianglesData); ok {
|
if t, ok := t.(*pixel.TrianglesData); ok {
|
||||||
for i := 0; i < length; i++ {
|
for i := 0; i < gt.Len(); i++ {
|
||||||
var (
|
var (
|
||||||
px, py = (*t)[i].Position.XY()
|
px, py = (*t)[i].Position.XY()
|
||||||
col = (*t)[i].Color
|
col = (*t)[i].Color
|
||||||
tx, ty = (*t)[i].Picture.XY()
|
tx, ty = (*t)[i].Picture.XY()
|
||||||
in = (*t)[i].Intensity
|
in = (*t)[i].Intensity
|
||||||
rec = (*t)[i].ClipRect
|
|
||||||
)
|
)
|
||||||
d := gt.data[i*stride : i*stride+trisAttrLen]
|
gt.data[i*gt.vs.Stride()+0] = float32(px)
|
||||||
d[triPosX] = float32(px)
|
gt.data[i*gt.vs.Stride()+1] = float32(py)
|
||||||
d[triPosY] = float32(py)
|
gt.data[i*gt.vs.Stride()+2] = float32(col.R)
|
||||||
d[triColorR] = float32(col.R)
|
gt.data[i*gt.vs.Stride()+3] = float32(col.G)
|
||||||
d[triColorG] = float32(col.G)
|
gt.data[i*gt.vs.Stride()+4] = float32(col.B)
|
||||||
d[triColorB] = float32(col.B)
|
gt.data[i*gt.vs.Stride()+5] = float32(col.A)
|
||||||
d[triColorA] = float32(col.A)
|
gt.data[i*gt.vs.Stride()+6] = float32(tx)
|
||||||
d[triPicX] = float32(tx)
|
gt.data[i*gt.vs.Stride()+7] = float32(ty)
|
||||||
d[triPicY] = float32(ty)
|
gt.data[i*gt.vs.Stride()+8] = float32(in)
|
||||||
d[triIntensity] = float32(in)
|
|
||||||
d[triClipMinX] = float32(rec.Min.X)
|
|
||||||
d[triClipMinY] = float32(rec.Min.Y)
|
|
||||||
d[triClipMaxX] = float32(rec.Max.X)
|
|
||||||
d[triClipMaxY] = float32(rec.Max.Y)
|
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if t, ok := t.(pixel.TrianglesPosition); ok {
|
if t, ok := t.(pixel.TrianglesPosition); ok {
|
||||||
for i := 0; i < length; i++ {
|
for i := 0; i < gt.Len(); i++ {
|
||||||
px, py := t.Position(i).XY()
|
px, py := t.Position(i).XY()
|
||||||
gt.data[i*stride+triPosX] = float32(px)
|
gt.data[i*gt.vs.Stride()+0] = float32(px)
|
||||||
gt.data[i*stride+triPosY] = float32(py)
|
gt.data[i*gt.vs.Stride()+1] = float32(py)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if t, ok := t.(pixel.TrianglesColor); ok {
|
if t, ok := t.(pixel.TrianglesColor); ok {
|
||||||
for i := 0; i < length; i++ {
|
for i := 0; i < gt.Len(); i++ {
|
||||||
col := t.Color(i)
|
col := t.Color(i)
|
||||||
gt.data[i*stride+triColorR] = float32(col.R)
|
gt.data[i*gt.vs.Stride()+2] = float32(col.R)
|
||||||
gt.data[i*stride+triColorG] = float32(col.G)
|
gt.data[i*gt.vs.Stride()+3] = float32(col.G)
|
||||||
gt.data[i*stride+triColorB] = float32(col.B)
|
gt.data[i*gt.vs.Stride()+4] = float32(col.B)
|
||||||
gt.data[i*stride+triColorA] = float32(col.A)
|
gt.data[i*gt.vs.Stride()+5] = float32(col.A)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if t, ok := t.(pixel.TrianglesPicture); ok {
|
if t, ok := t.(pixel.TrianglesPicture); ok {
|
||||||
for i := 0; i < length; i++ {
|
for i := 0; i < gt.Len(); i++ {
|
||||||
pic, intensity := t.Picture(i)
|
pic, intensity := t.Picture(i)
|
||||||
gt.data[i*stride+triPicX] = float32(pic.X)
|
gt.data[i*gt.vs.Stride()+6] = float32(pic.X())
|
||||||
gt.data[i*stride+triPicY] = float32(pic.Y)
|
gt.data[i*gt.vs.Stride()+7] = float32(pic.Y())
|
||||||
gt.data[i*stride+triIntensity] = float32(intensity)
|
gt.data[i*gt.vs.Stride()+8] = float32(intensity)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if t, ok := t.(pixel.TrianglesClipped); ok {
|
}
|
||||||
for i := 0; i < length; i++ {
|
|
||||||
rect, _ := t.ClipRect(i)
|
func (gt *GLTriangles) submitData() {
|
||||||
gt.data[i*stride+triClipMinX] = float32(rect.Min.X)
|
// this code is supposed to copy the vertex data and CallNonBlock the update if
|
||||||
gt.data[i*stride+triClipMinY] = float32(rect.Min.Y)
|
// the data is small enough, otherwise it'll block and not copy the data
|
||||||
gt.data[i*stride+triClipMaxX] = float32(rect.Max.X)
|
if len(gt.data) < 256 { // arbitrary heurestic constant
|
||||||
gt.data[i*stride+triClipMaxY] = float32(rect.Max.Y)
|
data := append([]float32{}, gt.data...)
|
||||||
}
|
mainthread.CallNonBlock(func() {
|
||||||
|
gt.vs.Begin()
|
||||||
|
dataLen := len(data) / gt.vs.Stride()
|
||||||
|
gt.vs.SetLen(dataLen)
|
||||||
|
gt.vs.SetVertexData(data)
|
||||||
|
gt.vs.End()
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
mainthread.Call(func() {
|
||||||
|
gt.vs.Begin()
|
||||||
|
dataLen := len(gt.data) / gt.vs.Stride()
|
||||||
|
gt.vs.SetLen(dataLen)
|
||||||
|
gt.vs.SetVertexData(gt.data)
|
||||||
|
gt.vs.End()
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -194,29 +173,7 @@ func (gt *GLTriangles) Update(t pixel.Triangles) {
|
||||||
panic(fmt.Errorf("(%T).Update: invalid triangles len", gt))
|
panic(fmt.Errorf("(%T).Update: invalid triangles len", gt))
|
||||||
}
|
}
|
||||||
gt.updateData(t)
|
gt.updateData(t)
|
||||||
|
gt.submitData()
|
||||||
// Copy the verteces down to the glhf.VertexData
|
|
||||||
gt.CopyVertices()
|
|
||||||
}
|
|
||||||
|
|
||||||
// CopyVertices copies the GLTriangle data down to the vertex data.
|
|
||||||
func (gt *GLTriangles) CopyVertices() {
|
|
||||||
// this code is supposed to copy the vertex data and CallNonBlock the update if
|
|
||||||
// the data is small enough, otherwise it'll block and not copy the data
|
|
||||||
if len(gt.data) < 256 { // arbitrary heurestic constant
|
|
||||||
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.
|
// Copy returns an independent copy of this GLTriangles.
|
||||||
|
@ -226,31 +183,19 @@ func (gt *GLTriangles) Copy() pixel.Triangles {
|
||||||
return NewGLTriangles(gt.shader, gt)
|
return NewGLTriangles(gt.shader, gt)
|
||||||
}
|
}
|
||||||
|
|
||||||
// index is a helper function that returns the index in the data
|
|
||||||
// slice given the i-th vertex and the item index.
|
|
||||||
func (gt *GLTriangles) index(i, idx int) int {
|
|
||||||
return i*gt.vs.Stride() + idx
|
|
||||||
}
|
|
||||||
|
|
||||||
// Position returns the Position property of the i-th vertex.
|
// Position returns the Position property of the i-th vertex.
|
||||||
func (gt *GLTriangles) Position(i int) pixel.Vec {
|
func (gt *GLTriangles) Position(i int) pixel.Vec {
|
||||||
px := gt.data[gt.index(i, triPosX)]
|
px := gt.data[i*gt.vs.Stride()+0]
|
||||||
py := gt.data[gt.index(i, triPosY)]
|
py := gt.data[i*gt.vs.Stride()+1]
|
||||||
return pixel.V(float64(px), float64(py))
|
return pixel.V(float64(px), float64(py))
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetPosition sets the position property of the i-th vertex.
|
|
||||||
func (gt *GLTriangles) SetPosition(i int, p pixel.Vec) {
|
|
||||||
gt.data[gt.index(i, triPosX)] = float32(p.X)
|
|
||||||
gt.data[gt.index(i, triPosY)] = float32(p.Y)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Color returns the Color property of the i-th vertex.
|
// Color returns the Color property of the i-th vertex.
|
||||||
func (gt *GLTriangles) Color(i int) pixel.RGBA {
|
func (gt *GLTriangles) Color(i int) pixel.RGBA {
|
||||||
r := gt.data[gt.index(i, triColorR)]
|
r := gt.data[i*gt.vs.Stride()+2]
|
||||||
g := gt.data[gt.index(i, triColorG)]
|
g := gt.data[i*gt.vs.Stride()+3]
|
||||||
b := gt.data[gt.index(i, triColorB)]
|
b := gt.data[i*gt.vs.Stride()+4]
|
||||||
a := gt.data[gt.index(i, triColorA)]
|
a := gt.data[i*gt.vs.Stride()+5]
|
||||||
return pixel.RGBA{
|
return pixel.RGBA{
|
||||||
R: float64(r),
|
R: float64(r),
|
||||||
G: float64(g),
|
G: float64(g),
|
||||||
|
@ -259,44 +204,10 @@ func (gt *GLTriangles) Color(i int) pixel.RGBA {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetColor sets the color property of the i-th vertex.
|
|
||||||
func (gt *GLTriangles) SetColor(i int, c pixel.RGBA) {
|
|
||||||
gt.data[gt.index(i, triColorR)] = float32(c.R)
|
|
||||||
gt.data[gt.index(i, triColorG)] = float32(c.G)
|
|
||||||
gt.data[gt.index(i, triColorB)] = float32(c.B)
|
|
||||||
gt.data[gt.index(i, triColorA)] = float32(c.A)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Picture returns the Picture property of the i-th vertex.
|
// Picture returns the Picture property of the i-th vertex.
|
||||||
func (gt *GLTriangles) Picture(i int) (pic pixel.Vec, intensity float64) {
|
func (gt *GLTriangles) Picture(i int) (pic pixel.Vec, intensity float64) {
|
||||||
tx := gt.data[gt.index(i, triPicX)]
|
tx := gt.data[i*gt.vs.Stride()+6]
|
||||||
ty := gt.data[gt.index(i, triPicY)]
|
ty := gt.data[i*gt.vs.Stride()+7]
|
||||||
intensity = float64(gt.data[gt.index(i, triIntensity)])
|
intensity = float64(gt.data[i*gt.vs.Stride()+8])
|
||||||
return pixel.V(float64(tx), float64(ty)), intensity
|
return pixel.V(float64(tx), float64(ty)), intensity
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetPicture sets the picture property of the i-th vertex.
|
|
||||||
func (gt *GLTriangles) SetPicture(i int, pic pixel.Vec, intensity float64) {
|
|
||||||
gt.data[gt.index(i, triPicX)] = float32(pic.X)
|
|
||||||
gt.data[gt.index(i, triPicY)] = float32(pic.Y)
|
|
||||||
gt.data[gt.index(i, triIntensity)] = float32(intensity)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ClipRect returns the Clipping rectangle property of the i-th vertex.
|
|
||||||
func (gt *GLTriangles) ClipRect(i int) (rect pixel.Rect, is bool) {
|
|
||||||
mx := gt.data[gt.index(i, triClipMinX)]
|
|
||||||
my := gt.data[gt.index(i, triClipMinY)]
|
|
||||||
ax := gt.data[gt.index(i, triClipMaxX)]
|
|
||||||
ay := gt.data[gt.index(i, triClipMaxY)]
|
|
||||||
rect = pixel.R(float64(mx), float64(my), float64(ax), float64(ay))
|
|
||||||
is = rect.Area() != 0.0
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetClipRect sets the Clipping rectangle property of the i-th vertex.
|
|
||||||
func (gt *GLTriangles) SetClipRect(i int, rect pixel.Rect) {
|
|
||||||
gt.data[gt.index(i, triClipMinX)] = float32(rect.Min.X)
|
|
||||||
gt.data[gt.index(i, triClipMinY)] = float32(rect.Min.Y)
|
|
||||||
gt.data[gt.index(i, triClipMaxX)] = float32(rect.Max.X)
|
|
||||||
gt.data[gt.index(i, triClipMaxY)] = float32(rect.Max.Y)
|
|
||||||
}
|
|
||||||
|
|
128
pixelgl/input.go
|
@ -1,11 +1,9 @@
|
||||||
package pixelgl
|
package pixelgl
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/faiface/mainthread"
|
"github.com/faiface/mainthread"
|
||||||
"github.com/faiface/pixel"
|
"github.com/faiface/pixel"
|
||||||
"github.com/go-gl/glfw/v3.3/glfw"
|
"github.com/go-gl/glfw/v3.2/glfw"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Pressed returns whether the Button is currently pressed down.
|
// Pressed returns whether the Button is currently pressed down.
|
||||||
|
@ -13,21 +11,14 @@ func (w *Window) Pressed(button Button) bool {
|
||||||
return w.currInp.buttons[button]
|
return w.currInp.buttons[button]
|
||||||
}
|
}
|
||||||
|
|
||||||
// JustPressed returns whether the Button has been pressed in the last frame.
|
// JustPressed returns whether the Button has just been pressed down.
|
||||||
func (w *Window) JustPressed(button Button) bool {
|
func (w *Window) JustPressed(button Button) bool {
|
||||||
return w.pressEvents[button]
|
return w.currInp.buttons[button] && !w.prevInp.buttons[button]
|
||||||
}
|
}
|
||||||
|
|
||||||
// JustReleased returns whether the Button has been released in the last frame.
|
// JustReleased returns whether the Button has just been released up.
|
||||||
func (w *Window) JustReleased(button Button) bool {
|
func (w *Window) JustReleased(button Button) bool {
|
||||||
return w.releaseEvents[button]
|
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.
|
// MousePosition returns the current mouse position in the Window's Bounds.
|
||||||
|
@ -35,42 +26,11 @@ func (w *Window) MousePosition() pixel.Vec {
|
||||||
return w.currInp.mouse
|
return w.currInp.mouse
|
||||||
}
|
}
|
||||||
|
|
||||||
// MousePreviousPosition returns the previous mouse position in the Window's Bounds.
|
|
||||||
func (w *Window) MousePreviousPosition() pixel.Vec {
|
|
||||||
return w.prevInp.mouse
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetMousePosition positions the mouse cursor anywhere within the Window's Bounds.
|
|
||||||
func (w *Window) SetMousePosition(v pixel.Vec) {
|
|
||||||
mainthread.Call(func() {
|
|
||||||
if (v.X >= 0 && v.X <= w.bounds.W()) &&
|
|
||||||
(v.Y >= 0 && v.Y <= w.bounds.H()) {
|
|
||||||
w.window.SetCursorPos(
|
|
||||||
v.X+w.bounds.Min.X,
|
|
||||||
(w.bounds.H()-v.Y)+w.bounds.Min.Y,
|
|
||||||
)
|
|
||||||
w.prevInp.mouse = v
|
|
||||||
w.currInp.mouse = v
|
|
||||||
w.tempInp.mouse = v
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// MouseInsideWindow returns true if the mouse position is within the Window's Bounds.
|
|
||||||
func (w *Window) MouseInsideWindow() bool {
|
|
||||||
return w.cursorInsideWindow
|
|
||||||
}
|
|
||||||
|
|
||||||
// MouseScroll returns the mouse scroll amount (in both axes) since the last call to Window.Update.
|
// MouseScroll returns the mouse scroll amount (in both axes) since the last call to Window.Update.
|
||||||
func (w *Window) MouseScroll() pixel.Vec {
|
func (w *Window) MouseScroll() pixel.Vec {
|
||||||
return w.currInp.scroll
|
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?
|
// Button is a keyboard or mouse button. Why distinguish?
|
||||||
type Button int
|
type Button int
|
||||||
|
|
||||||
|
@ -362,11 +322,9 @@ func (w *Window) initInput() {
|
||||||
w.window.SetMouseButtonCallback(func(_ *glfw.Window, button glfw.MouseButton, action glfw.Action, mod glfw.ModifierKey) {
|
w.window.SetMouseButtonCallback(func(_ *glfw.Window, button glfw.MouseButton, action glfw.Action, mod glfw.ModifierKey) {
|
||||||
switch action {
|
switch action {
|
||||||
case glfw.Press:
|
case glfw.Press:
|
||||||
w.tempPressEvents[Button(button)] = true
|
w.currInp.buttons[Button(button)] = true
|
||||||
w.tempInp.buttons[Button(button)] = true
|
|
||||||
case glfw.Release:
|
case glfw.Release:
|
||||||
w.tempReleaseEvents[Button(button)] = true
|
w.currInp.buttons[Button(button)] = false
|
||||||
w.tempInp.buttons[Button(button)] = false
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -376,74 +334,38 @@ func (w *Window) initInput() {
|
||||||
}
|
}
|
||||||
switch action {
|
switch action {
|
||||||
case glfw.Press:
|
case glfw.Press:
|
||||||
w.tempPressEvents[Button(key)] = true
|
w.currInp.buttons[Button(key)] = true
|
||||||
w.tempInp.buttons[Button(key)] = true
|
|
||||||
case glfw.Release:
|
case glfw.Release:
|
||||||
w.tempReleaseEvents[Button(key)] = true
|
w.currInp.buttons[Button(key)] = false
|
||||||
w.tempInp.buttons[Button(key)] = false
|
|
||||||
case glfw.Repeat:
|
|
||||||
w.tempInp.repeat[Button(key)] = true
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
w.window.SetCursorEnterCallback(func(_ *glfw.Window, entered bool) {
|
|
||||||
w.cursorInsideWindow = entered
|
|
||||||
})
|
|
||||||
|
|
||||||
w.window.SetCursorPosCallback(func(_ *glfw.Window, x, y float64) {
|
w.window.SetCursorPosCallback(func(_ *glfw.Window, x, y float64) {
|
||||||
w.tempInp.mouse = pixel.V(
|
w.currInp.mouse = pixel.V(
|
||||||
x+w.bounds.Min.X,
|
x+w.bounds.Min.X(),
|
||||||
(w.bounds.H()-y)+w.bounds.Min.Y,
|
(w.bounds.H()-y)+w.bounds.Min.Y(),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
w.window.SetScrollCallback(func(_ *glfw.Window, xoff, yoff float64) {
|
w.window.SetScrollCallback(func(_ *glfw.Window, xoff, yoff float64) {
|
||||||
w.tempInp.scroll.X += xoff
|
w.currInp.scroll += pixel.V(xoff, yoff)
|
||||||
w.tempInp.scroll.Y += yoff
|
|
||||||
})
|
|
||||||
|
|
||||||
w.window.SetCharCallback(func(_ *glfw.Window, r rune) {
|
|
||||||
w.tempInp.typed += string(r)
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateInput polls window events. Call this function to poll window events
|
func (w *Window) updateInput() {
|
||||||
// without swapping buffers. Note that the Update method invokes UpdateInput.
|
// copy temp to prev
|
||||||
func (w *Window) UpdateInput() {
|
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)
|
||||||
mainthread.Call(func() {
|
mainthread.Call(func() {
|
||||||
glfw.PollEvents()
|
glfw.PollEvents()
|
||||||
})
|
})
|
||||||
w.doUpdateInput()
|
|
||||||
}
|
// cache current state to temp (so that if there are callbacks outside this function,
|
||||||
|
// everything works)
|
||||||
// UpdateInputWait blocks until an event is received or a timeout. If timeout is 0
|
w.tempInp = w.currInp
|
||||||
// then it will wait indefinitely
|
|
||||||
func (w *Window) UpdateInputWait(timeout time.Duration) {
|
|
||||||
mainthread.Call(func() {
|
|
||||||
if timeout <= 0 {
|
|
||||||
glfw.WaitEvents()
|
|
||||||
} else {
|
|
||||||
glfw.WaitEventsTimeout(timeout.Seconds())
|
|
||||||
}
|
|
||||||
})
|
|
||||||
w.doUpdateInput()
|
|
||||||
}
|
|
||||||
|
|
||||||
// internal input bookkeeping
|
|
||||||
func (w *Window) doUpdateInput() {
|
|
||||||
w.prevInp = w.currInp
|
|
||||||
w.currInp = w.tempInp
|
|
||||||
|
|
||||||
w.pressEvents = w.tempPressEvents
|
|
||||||
w.releaseEvents = w.tempReleaseEvents
|
|
||||||
|
|
||||||
// Clear last frame's temporary status
|
|
||||||
w.tempPressEvents = [KeyLast + 1]bool{}
|
|
||||||
w.tempReleaseEvents = [KeyLast + 1]bool{}
|
|
||||||
w.tempInp.repeat = [KeyLast + 1]bool{}
|
|
||||||
w.tempInp.scroll = pixel.ZV
|
|
||||||
w.tempInp.typed = ""
|
|
||||||
|
|
||||||
w.updateJoystickInput()
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,193 +0,0 @@
|
||||||
package pixelgl
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/go-gl/glfw/v3.3/glfw"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Joystick is a joystick or controller (gamepad).
|
|
||||||
type Joystick int
|
|
||||||
|
|
||||||
// List all of the joysticks.
|
|
||||||
const (
|
|
||||||
Joystick1 = Joystick(glfw.Joystick1)
|
|
||||||
Joystick2 = Joystick(glfw.Joystick2)
|
|
||||||
Joystick3 = Joystick(glfw.Joystick3)
|
|
||||||
Joystick4 = Joystick(glfw.Joystick4)
|
|
||||||
Joystick5 = Joystick(glfw.Joystick5)
|
|
||||||
Joystick6 = Joystick(glfw.Joystick6)
|
|
||||||
Joystick7 = Joystick(glfw.Joystick7)
|
|
||||||
Joystick8 = Joystick(glfw.Joystick8)
|
|
||||||
Joystick9 = Joystick(glfw.Joystick9)
|
|
||||||
Joystick10 = Joystick(glfw.Joystick10)
|
|
||||||
Joystick11 = Joystick(glfw.Joystick11)
|
|
||||||
Joystick12 = Joystick(glfw.Joystick12)
|
|
||||||
Joystick13 = Joystick(glfw.Joystick13)
|
|
||||||
Joystick14 = Joystick(glfw.Joystick14)
|
|
||||||
Joystick15 = Joystick(glfw.Joystick15)
|
|
||||||
Joystick16 = Joystick(glfw.Joystick16)
|
|
||||||
|
|
||||||
JoystickLast = Joystick(glfw.JoystickLast)
|
|
||||||
)
|
|
||||||
|
|
||||||
// GamepadAxis corresponds to a gamepad axis.
|
|
||||||
type GamepadAxis int
|
|
||||||
|
|
||||||
// Gamepad axis IDs.
|
|
||||||
const (
|
|
||||||
AxisLeftX = GamepadAxis(glfw.AxisLeftX)
|
|
||||||
AxisLeftY = GamepadAxis(glfw.AxisLeftY)
|
|
||||||
AxisRightX = GamepadAxis(glfw.AxisRightX)
|
|
||||||
AxisRightY = GamepadAxis(glfw.AxisRightY)
|
|
||||||
AxisLeftTrigger = GamepadAxis(glfw.AxisLeftTrigger)
|
|
||||||
AxisRightTrigger = GamepadAxis(glfw.AxisRightTrigger)
|
|
||||||
AxisLast = GamepadAxis(glfw.AxisLast)
|
|
||||||
)
|
|
||||||
|
|
||||||
// GamepadButton corresponds to a gamepad button.
|
|
||||||
type GamepadButton int
|
|
||||||
|
|
||||||
// Gamepad button IDs.
|
|
||||||
const (
|
|
||||||
ButtonA = GamepadButton(glfw.ButtonA)
|
|
||||||
ButtonB = GamepadButton(glfw.ButtonB)
|
|
||||||
ButtonX = GamepadButton(glfw.ButtonX)
|
|
||||||
ButtonY = GamepadButton(glfw.ButtonY)
|
|
||||||
ButtonLeftBumper = GamepadButton(glfw.ButtonLeftBumper)
|
|
||||||
ButtonRightBumper = GamepadButton(glfw.ButtonRightBumper)
|
|
||||||
ButtonBack = GamepadButton(glfw.ButtonBack)
|
|
||||||
ButtonStart = GamepadButton(glfw.ButtonStart)
|
|
||||||
ButtonGuide = GamepadButton(glfw.ButtonGuide)
|
|
||||||
ButtonLeftThumb = GamepadButton(glfw.ButtonLeftThumb)
|
|
||||||
ButtonRightThumb = GamepadButton(glfw.ButtonRightThumb)
|
|
||||||
ButtonDpadUp = GamepadButton(glfw.ButtonDpadUp)
|
|
||||||
ButtonDpadRight = GamepadButton(glfw.ButtonDpadRight)
|
|
||||||
ButtonDpadDown = GamepadButton(glfw.ButtonDpadDown)
|
|
||||||
ButtonDpadLeft = GamepadButton(glfw.ButtonDpadLeft)
|
|
||||||
ButtonLast = GamepadButton(glfw.ButtonLast)
|
|
||||||
ButtonCross = GamepadButton(glfw.ButtonCross)
|
|
||||||
ButtonCircle = GamepadButton(glfw.ButtonCircle)
|
|
||||||
ButtonSquare = GamepadButton(glfw.ButtonSquare)
|
|
||||||
ButtonTriangle = GamepadButton(glfw.ButtonTriangle)
|
|
||||||
)
|
|
||||||
|
|
||||||
// JoystickPresent returns if the joystick is currently connected.
|
|
||||||
//
|
|
||||||
// This API is experimental.
|
|
||||||
func (w *Window) JoystickPresent(js Joystick) bool {
|
|
||||||
return w.currJoy.connected[js]
|
|
||||||
}
|
|
||||||
|
|
||||||
// JoystickName returns the name of the joystick. A disconnected joystick will return an
|
|
||||||
// empty string.
|
|
||||||
//
|
|
||||||
// This API is experimental.
|
|
||||||
func (w *Window) JoystickName(js Joystick) string {
|
|
||||||
return w.currJoy.name[js]
|
|
||||||
}
|
|
||||||
|
|
||||||
// JoystickButtonCount returns the number of buttons a connected joystick has.
|
|
||||||
//
|
|
||||||
// This API is experimental.
|
|
||||||
func (w *Window) JoystickButtonCount(js Joystick) int {
|
|
||||||
return len(w.currJoy.buttons[js])
|
|
||||||
}
|
|
||||||
|
|
||||||
// JoystickAxisCount returns the number of axes a connected joystick has.
|
|
||||||
//
|
|
||||||
// This API is experimental.
|
|
||||||
func (w *Window) JoystickAxisCount(js Joystick) int {
|
|
||||||
return len(w.currJoy.axis[js])
|
|
||||||
}
|
|
||||||
|
|
||||||
// JoystickPressed returns whether the joystick Button is currently pressed down.
|
|
||||||
// If the button index is out of range, this will return false.
|
|
||||||
//
|
|
||||||
// This API is experimental.
|
|
||||||
func (w *Window) JoystickPressed(js Joystick, button GamepadButton) bool {
|
|
||||||
return w.currJoy.getButton(js, int(button))
|
|
||||||
}
|
|
||||||
|
|
||||||
// JoystickJustPressed returns whether the joystick Button has just been pressed down.
|
|
||||||
// If the button index is out of range, this will return false.
|
|
||||||
//
|
|
||||||
// This API is experimental.
|
|
||||||
func (w *Window) JoystickJustPressed(js Joystick, button GamepadButton) bool {
|
|
||||||
return w.currJoy.getButton(js, int(button)) && !w.prevJoy.getButton(js, int(button))
|
|
||||||
}
|
|
||||||
|
|
||||||
// JoystickJustReleased returns whether the joystick Button has just been released up.
|
|
||||||
// If the button index is out of range, this will return false.
|
|
||||||
//
|
|
||||||
// This API is experimental.
|
|
||||||
func (w *Window) JoystickJustReleased(js Joystick, button GamepadButton) bool {
|
|
||||||
return !w.currJoy.getButton(js, int(button)) && w.prevJoy.getButton(js, int(button))
|
|
||||||
}
|
|
||||||
|
|
||||||
// JoystickAxis returns the value of a joystick axis at the last call to Window.Update.
|
|
||||||
// If the axis index is out of range, this will return 0.
|
|
||||||
//
|
|
||||||
// This API is experimental.
|
|
||||||
func (w *Window) JoystickAxis(js Joystick, axis GamepadAxis) float64 {
|
|
||||||
return w.currJoy.getAxis(js, int(axis))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Used internally during Window.UpdateInput to update the state of the joysticks.
|
|
||||||
func (w *Window) updateJoystickInput() {
|
|
||||||
for js := Joystick1; js <= JoystickLast; js++ {
|
|
||||||
// Determine and store if the joystick was connected
|
|
||||||
joystickPresent := glfw.Joystick(js).Present()
|
|
||||||
w.tempJoy.connected[js] = joystickPresent
|
|
||||||
|
|
||||||
if joystickPresent {
|
|
||||||
if glfw.Joystick(js).IsGamepad() {
|
|
||||||
gamepadInputs := glfw.Joystick(js).GetGamepadState()
|
|
||||||
|
|
||||||
w.tempJoy.buttons[js] = gamepadInputs.Buttons[:]
|
|
||||||
w.tempJoy.axis[js] = gamepadInputs.Axes[:]
|
|
||||||
} else {
|
|
||||||
w.tempJoy.buttons[js] = glfw.Joystick(js).GetButtons()
|
|
||||||
w.tempJoy.axis[js] = glfw.Joystick(js).GetAxes()
|
|
||||||
}
|
|
||||||
|
|
||||||
if !w.currJoy.connected[js] {
|
|
||||||
// The joystick was recently connected, we get the name
|
|
||||||
w.tempJoy.name[js] = glfw.Joystick(js).GetName()
|
|
||||||
} else {
|
|
||||||
// Use the name from the previous one
|
|
||||||
w.tempJoy.name[js] = w.currJoy.name[js]
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
w.tempJoy.buttons[js] = []glfw.Action{}
|
|
||||||
w.tempJoy.axis[js] = []float32{}
|
|
||||||
w.tempJoy.name[js] = ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
w.prevJoy = w.currJoy
|
|
||||||
w.currJoy = w.tempJoy
|
|
||||||
}
|
|
||||||
|
|
||||||
type joystickState struct {
|
|
||||||
connected [JoystickLast + 1]bool
|
|
||||||
name [JoystickLast + 1]string
|
|
||||||
buttons [JoystickLast + 1][]glfw.Action
|
|
||||||
axis [JoystickLast + 1][]float32
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returns if a button on a joystick is down, returning false if the button or joystick is invalid.
|
|
||||||
func (js *joystickState) getButton(joystick Joystick, button int) bool {
|
|
||||||
// Check that the joystick and button is valid, return false by default
|
|
||||||
if js.buttons[joystick] == nil || button >= len(js.buttons[joystick]) || button < 0 {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return js.buttons[joystick][byte(button)] == glfw.Press
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returns the value of a joystick axis, returning 0 if the button or joystick is invalid.
|
|
||||||
func (js *joystickState) getAxis(joystick Joystick, axis int) float64 {
|
|
||||||
// Check that the joystick and axis is valid, return 0 by default.
|
|
||||||
if js.axis[joystick] == nil || axis >= len(js.axis[joystick]) || axis < 0 {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
return float64(js.axis[joystick][axis])
|
|
||||||
}
|
|
|
@ -2,7 +2,7 @@ package pixelgl
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/faiface/mainthread"
|
"github.com/faiface/mainthread"
|
||||||
"github.com/go-gl/glfw/v3.3/glfw"
|
"github.com/go-gl/glfw/v3.2/glfw"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Monitor represents a physical display attached to your computer.
|
// Monitor represents a physical display attached to your computer.
|
||||||
|
@ -10,17 +10,6 @@ type Monitor struct {
|
||||||
monitor *glfw.Monitor
|
monitor *glfw.Monitor
|
||||||
}
|
}
|
||||||
|
|
||||||
// VideoMode represents all properties of a video mode and is
|
|
||||||
// associated with a monitor if it is used in fullscreen mode.
|
|
||||||
type VideoMode struct {
|
|
||||||
// Width is the width of the vide mode in pixels.
|
|
||||||
Width int
|
|
||||||
// Height is the height of the video mode in pixels.
|
|
||||||
Height int
|
|
||||||
// RefreshRate holds the refresh rate of the associated monitor in Hz.
|
|
||||||
RefreshRate int
|
|
||||||
}
|
|
||||||
|
|
||||||
// PrimaryMonitor returns the main monitor (usually the one with the taskbar and stuff).
|
// PrimaryMonitor returns the main monitor (usually the one with the taskbar and stuff).
|
||||||
func PrimaryMonitor() *Monitor {
|
func PrimaryMonitor() *Monitor {
|
||||||
var monitor *glfw.Monitor
|
var monitor *glfw.Monitor
|
||||||
|
@ -106,19 +95,3 @@ func (m *Monitor) RefreshRate() (rate float64) {
|
||||||
rate = float64(mode.RefreshRate)
|
rate = float64(mode.RefreshRate)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// VideoModes returns all available video modes for the monitor.
|
|
||||||
func (m *Monitor) VideoModes() (vmodes []VideoMode) {
|
|
||||||
var modes []*glfw.VidMode
|
|
||||||
mainthread.Call(func() {
|
|
||||||
modes = m.monitor.GetVideoModes()
|
|
||||||
})
|
|
||||||
for _, mode := range modes {
|
|
||||||
vmodes = append(vmodes, VideoMode{
|
|
||||||
Width: mode.Width,
|
|
||||||
Height: mode.Height,
|
|
||||||
RefreshRate: mode.RefreshRate,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
|
@ -2,7 +2,7 @@ package pixelgl
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/faiface/mainthread"
|
"github.com/faiface/mainthread"
|
||||||
"github.com/go-gl/glfw/v3.3/glfw"
|
"github.com/go-gl/glfw/v3.2/glfw"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -7,9 +7,9 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func intBounds(bounds pixel.Rect) (x, y, w, h int) {
|
func intBounds(bounds pixel.Rect) (x, y, w, h int) {
|
||||||
x0 := int(math.Floor(bounds.Min.X))
|
x0 := int(math.Floor(bounds.Min.X()))
|
||||||
y0 := int(math.Floor(bounds.Min.Y))
|
y0 := int(math.Floor(bounds.Min.Y()))
|
||||||
x1 := int(math.Ceil(bounds.Max.X))
|
x1 := int(math.Ceil(bounds.Max.X()))
|
||||||
y1 := int(math.Ceil(bounds.Max.Y))
|
y1 := int(math.Ceil(bounds.Max.Y()))
|
||||||
return x0, y0, x1 - x0, y1 - y0
|
return x0, y0, x1 - x0, y1 - y0
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,16 +1,13 @@
|
||||||
package pixelgl
|
package pixelgl
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"image"
|
|
||||||
"image/color"
|
"image/color"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
|
||||||
"github.com/faiface/glhf"
|
"github.com/faiface/glhf"
|
||||||
"github.com/faiface/mainthread"
|
"github.com/faiface/mainthread"
|
||||||
"github.com/faiface/pixel"
|
"github.com/faiface/pixel"
|
||||||
"github.com/go-gl/gl/v3.3-core/gl"
|
"github.com/go-gl/glfw/v3.2/glfw"
|
||||||
"github.com/go-gl/glfw/v3.3/glfw"
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -23,92 +20,42 @@ type WindowConfig struct {
|
||||||
// Title at the top of the Window.
|
// Title at the top of the Window.
|
||||||
Title string
|
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 specify the bounds of the Window in pixels.
|
||||||
Bounds pixel.Rect
|
Bounds pixel.Rect
|
||||||
|
|
||||||
// Initial window position
|
|
||||||
Position pixel.Vec
|
|
||||||
|
|
||||||
// If set to nil, the Window will be windowed. Otherwise it will be fullscreen on the
|
// If set to nil, the Window will be windowed. Otherwise it will be fullscreen on the
|
||||||
// specified Monitor.
|
// specified Monitor.
|
||||||
Monitor *Monitor
|
Monitor *Monitor
|
||||||
|
|
||||||
// Resizable specifies whether the window will be resizable by the user.
|
// Whether the Window is resizable.
|
||||||
Resizable bool
|
Resizable bool
|
||||||
|
|
||||||
// Undecorated Window omits the borders and decorations (close button, etc.).
|
// Undecorated Window ommits the borders and decorations (close button, etc.).
|
||||||
Undecorated bool
|
Undecorated bool
|
||||||
|
|
||||||
// NoIconify specifies whether fullscreen windows should not automatically
|
|
||||||
// iconify (and restore the previous video mode) on focus loss.
|
|
||||||
NoIconify bool
|
|
||||||
|
|
||||||
// AlwaysOnTop specifies whether the windowed mode window will be floating
|
|
||||||
// above other regular windows, also called topmost or always-on-top.
|
|
||||||
// This is intended primarily for debugging purposes and cannot be used to
|
|
||||||
// implement proper full screen windows.
|
|
||||||
AlwaysOnTop bool
|
|
||||||
|
|
||||||
// TransparentFramebuffer specifies whether the window framebuffer will be
|
|
||||||
// transparent. If enabled and supported by the system, the window
|
|
||||||
// framebuffer alpha channel will be used to combine the framebuffer with
|
|
||||||
// the background. This does not affect window decorations.
|
|
||||||
TransparentFramebuffer bool
|
|
||||||
|
|
||||||
// VSync (vertical synchronization) synchronizes Window's framerate with the framerate of
|
// VSync (vertical synchronization) synchronizes Window's framerate with the framerate of
|
||||||
// the monitor.
|
// the monitor.
|
||||||
VSync bool
|
VSync bool
|
||||||
|
|
||||||
// Maximized specifies whether the window is maximized.
|
|
||||||
Maximized bool
|
|
||||||
|
|
||||||
// Invisible specifies whether the window will be initially hidden.
|
|
||||||
// You can make the window visible later using Window.Show().
|
|
||||||
Invisible bool
|
|
||||||
|
|
||||||
//SamplesMSAA specifies the level of MSAA to be used. Must be one of 0, 2, 4, 8, 16. 0 to disable.
|
|
||||||
SamplesMSAA int
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Window is a window handler. Use this type to manipulate a window (input, drawing, etc.).
|
// Window is a window handler. Use this type to manipulate a window (input, drawing, etc.).
|
||||||
type Window struct {
|
type Window struct {
|
||||||
window *glfw.Window
|
window *glfw.Window
|
||||||
|
|
||||||
bounds pixel.Rect
|
bounds pixel.Rect
|
||||||
canvas *Canvas
|
canvas *Canvas
|
||||||
vsync bool
|
vsync bool
|
||||||
cursorVisible bool
|
|
||||||
cursorInsideWindow bool
|
|
||||||
|
|
||||||
// need to save these to correctly restore a fullscreen window
|
// need to save these to correctly restore a fullscreen window
|
||||||
restore struct {
|
restore struct {
|
||||||
xpos, ypos, width, height int
|
xpos, ypos, width, height int
|
||||||
}
|
}
|
||||||
|
|
||||||
prevInp, currInp, tempInp struct {
|
prevInp, tempInp, currInp struct {
|
||||||
mouse pixel.Vec
|
mouse pixel.Vec
|
||||||
buttons [KeyLast + 1]bool
|
buttons [KeyLast + 1]bool
|
||||||
repeat [KeyLast + 1]bool
|
|
||||||
scroll pixel.Vec
|
scroll pixel.Vec
|
||||||
typed string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pressEvents, tempPressEvents [KeyLast + 1]bool
|
|
||||||
releaseEvents, tempReleaseEvents [KeyLast + 1]bool
|
|
||||||
|
|
||||||
prevJoy, currJoy, tempJoy joystickState
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var currWin *Window
|
var currWin *Window
|
||||||
|
@ -122,18 +69,7 @@ func NewWindow(cfg WindowConfig) (*Window, error) {
|
||||||
false: glfw.False,
|
false: glfw.False,
|
||||||
}
|
}
|
||||||
|
|
||||||
w := &Window{bounds: cfg.Bounds, cursorVisible: true}
|
w := &Window{bounds: cfg.Bounds}
|
||||||
|
|
||||||
flag := false
|
|
||||||
for _, v := range []int{0, 2, 4, 8, 16} {
|
|
||||||
if cfg.SamplesMSAA == v {
|
|
||||||
flag = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !flag {
|
|
||||||
return nil, fmt.Errorf("invalid value '%v' for msaaSamples", cfg.SamplesMSAA)
|
|
||||||
}
|
|
||||||
|
|
||||||
err := mainthread.CallErr(func() error {
|
err := mainthread.CallErr(func() error {
|
||||||
var err error
|
var err error
|
||||||
|
@ -145,16 +81,6 @@ func NewWindow(cfg WindowConfig) (*Window, error) {
|
||||||
|
|
||||||
glfw.WindowHint(glfw.Resizable, bool2int[cfg.Resizable])
|
glfw.WindowHint(glfw.Resizable, bool2int[cfg.Resizable])
|
||||||
glfw.WindowHint(glfw.Decorated, bool2int[!cfg.Undecorated])
|
glfw.WindowHint(glfw.Decorated, bool2int[!cfg.Undecorated])
|
||||||
glfw.WindowHint(glfw.Floating, bool2int[cfg.AlwaysOnTop])
|
|
||||||
glfw.WindowHint(glfw.AutoIconify, bool2int[!cfg.NoIconify])
|
|
||||||
glfw.WindowHint(glfw.TransparentFramebuffer, bool2int[cfg.TransparentFramebuffer])
|
|
||||||
glfw.WindowHint(glfw.Maximized, bool2int[cfg.Maximized])
|
|
||||||
glfw.WindowHint(glfw.Visible, bool2int[!cfg.Invisible])
|
|
||||||
glfw.WindowHint(glfw.Samples, cfg.SamplesMSAA)
|
|
||||||
|
|
||||||
if cfg.Position.X != 0 || cfg.Position.Y != 0 {
|
|
||||||
glfw.WindowHint(glfw.Visible, glfw.False)
|
|
||||||
}
|
|
||||||
|
|
||||||
var share *glfw.Window
|
var share *glfw.Window
|
||||||
if currWin != nil {
|
if currWin != nil {
|
||||||
|
@ -172,15 +98,8 @@ func NewWindow(cfg WindowConfig) (*Window, error) {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if cfg.Position.X != 0 || cfg.Position.Y != 0 {
|
|
||||||
w.window.SetPos(int(cfg.Position.X), int(cfg.Position.Y))
|
|
||||||
w.window.Show()
|
|
||||||
}
|
|
||||||
|
|
||||||
// enter the OpenGL context
|
// enter the OpenGL context
|
||||||
w.begin()
|
w.begin()
|
||||||
glhf.Init()
|
|
||||||
gl.Enable(gl.MULTISAMPLE)
|
|
||||||
w.end()
|
w.end()
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
@ -189,17 +108,6 @@ func NewWindow(cfg WindowConfig) (*Window, error) {
|
||||||
return nil, errors.Wrap(err, "creating window failed")
|
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.SetVSync(cfg.VSync)
|
||||||
|
|
||||||
w.initInput()
|
w.initInput()
|
||||||
|
@ -222,31 +130,13 @@ func (w *Window) Destroy() {
|
||||||
|
|
||||||
// Update swaps buffers and polls events. Call this method at the end of each frame.
|
// Update swaps buffers and polls events. Call this method at the end of each frame.
|
||||||
func (w *Window) Update() {
|
func (w *Window) Update() {
|
||||||
w.SwapBuffers()
|
|
||||||
w.UpdateInput()
|
|
||||||
}
|
|
||||||
|
|
||||||
// ClipboardText returns the current value of the systems clipboard.
|
|
||||||
func (w *Window) ClipboardText() string {
|
|
||||||
return w.window.GetClipboardString()
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetClipboardText passes the given string to the underlying glfw window to set the
|
|
||||||
// systems clipboard.
|
|
||||||
func (w *Window) SetClipboardText(text string) {
|
|
||||||
w.window.SetClipboardString(text)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SwapBuffers swaps buffers. Call this to swap buffers without polling window events.
|
|
||||||
// Note that Update invokes SwapBuffers.
|
|
||||||
func (w *Window) SwapBuffers() {
|
|
||||||
mainthread.Call(func() {
|
mainthread.Call(func() {
|
||||||
_, _, oldW, oldH := intBounds(w.bounds)
|
_, _, oldW, oldH := intBounds(w.bounds)
|
||||||
newW, newH := w.window.GetSize()
|
newW, newH := w.window.GetSize()
|
||||||
w.bounds = w.bounds.ResizedMin(w.bounds.Size().Add(pixel.V(
|
w.bounds = w.bounds.ResizedMin(w.bounds.Size() + pixel.V(
|
||||||
float64(newW-oldW),
|
float64(newW-oldW),
|
||||||
float64(newH-oldH),
|
float64(newH-oldH),
|
||||||
)))
|
))
|
||||||
})
|
})
|
||||||
|
|
||||||
w.canvas.SetBounds(w.bounds)
|
w.canvas.SetBounds(w.bounds)
|
||||||
|
@ -274,6 +164,8 @@ func (w *Window) SwapBuffers() {
|
||||||
w.window.SwapBuffers()
|
w.window.SwapBuffers()
|
||||||
w.end()
|
w.end()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
w.updateInput()
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetClosed sets the closed flag of the Window.
|
// SetClosed sets the closed flag of the Window.
|
||||||
|
@ -314,29 +206,6 @@ 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.
|
// Bounds returns the current bounds of the Window.
|
||||||
func (w *Window) Bounds() pixel.Rect {
|
func (w *Window) Bounds() pixel.Rect {
|
||||||
return w.bounds
|
return w.bounds
|
||||||
|
@ -422,36 +291,11 @@ func (w *Window) VSync() bool {
|
||||||
return w.vsync
|
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)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetCursorDisabled hides the cursor and provides unlimited virtual cursor movement
|
|
||||||
// make cursor visible using SetCursorVisible
|
|
||||||
func (w *Window) SetCursorDisabled() {
|
|
||||||
w.cursorVisible = false
|
|
||||||
mainthread.Call(func() {
|
|
||||||
w.window.SetInputMode(glfw.CursorMode, glfw.CursorDisabled)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// CursorVisible returns the visibility status of the mouse cursor.
|
|
||||||
func (w *Window) CursorVisible() bool {
|
|
||||||
return w.cursorVisible
|
|
||||||
}
|
|
||||||
|
|
||||||
// Note: must be called inside the main thread.
|
// Note: must be called inside the main thread.
|
||||||
func (w *Window) begin() {
|
func (w *Window) begin() {
|
||||||
if currWin != w {
|
if currWin != w {
|
||||||
w.window.MakeContextCurrent()
|
w.window.MakeContextCurrent()
|
||||||
|
glhf.Init()
|
||||||
currWin = w
|
currWin = w
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -513,32 +357,3 @@ func (w *Window) Clear(c color.Color) {
|
||||||
func (w *Window) Color(at pixel.Vec) pixel.RGBA {
|
func (w *Window) Color(at pixel.Vec) pixel.RGBA {
|
||||||
return w.canvas.Color(at)
|
return w.canvas.Color(at)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Canvas returns the window's underlying Canvas
|
|
||||||
func (w *Window) Canvas() *Canvas {
|
|
||||||
return w.canvas
|
|
||||||
}
|
|
||||||
|
|
||||||
// Show makes the window visible, if it was previously hidden. If the window is
|
|
||||||
// already visible or is in full screen mode, this function does nothing.
|
|
||||||
func (w *Window) Show() {
|
|
||||||
mainthread.Call(func() {
|
|
||||||
w.window.Show()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clipboard returns the contents of the system clipboard.
|
|
||||||
func (w *Window) Clipboard() string {
|
|
||||||
var clipboard string
|
|
||||||
mainthread.Call(func() {
|
|
||||||
clipboard = w.window.GetClipboardString()
|
|
||||||
})
|
|
||||||
return clipboard
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetClipboardString sets the system clipboard to the specified UTF-8 encoded string.
|
|
||||||
func (w *Window) SetClipboard(str string) {
|
|
||||||
mainthread.Call(func() {
|
|
||||||
w.window.SetClipboardString(str)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
284
rectangle.go
|
@ -1,284 +0,0 @@
|
||||||
package pixel
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"math"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Rect is a 2D rectangle aligned with the axes of the coordinate system. It is defined by two
|
|
||||||
// points, Min and Max.
|
|
||||||
//
|
|
||||||
// The invariant should hold, that Max's components are greater or equal than Min's components
|
|
||||||
// respectively.
|
|
||||||
type Rect struct {
|
|
||||||
Min, Max Vec
|
|
||||||
}
|
|
||||||
|
|
||||||
// ZR is a zero rectangle.
|
|
||||||
var ZR = Rect{Min: ZV, Max: ZV}
|
|
||||||
|
|
||||||
// R returns a new Rect with given the Min and Max coordinates.
|
|
||||||
//
|
|
||||||
// Note that the returned rectangle is not automatically normalized.
|
|
||||||
func R(minX, minY, maxX, maxY float64) Rect {
|
|
||||||
return Rect{
|
|
||||||
Min: Vec{minX, minY},
|
|
||||||
Max: Vec{maxX, maxY},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// String returns the string representation of the Rect.
|
|
||||||
//
|
|
||||||
// r := pixel.R(100, 50, 200, 300)
|
|
||||||
// r.String() // returns "Rect(100, 50, 200, 300)"
|
|
||||||
// fmt.Println(r) // Rect(100, 50, 200, 300)
|
|
||||||
func (r Rect) String() string {
|
|
||||||
return fmt.Sprintf("Rect(%v, %v, %v, %v)", r.Min.X, r.Min.Y, r.Max.X, r.Max.Y)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Norm returns the Rect in normal form, such that Max is component-wise greater or equal than Min.
|
|
||||||
func (r Rect) Norm() Rect {
|
|
||||||
return Rect{
|
|
||||||
Min: Vec{
|
|
||||||
math.Min(r.Min.X, r.Max.X),
|
|
||||||
math.Min(r.Min.Y, r.Max.Y),
|
|
||||||
},
|
|
||||||
Max: Vec{
|
|
||||||
math.Max(r.Min.X, r.Max.X),
|
|
||||||
math.Max(r.Min.Y, r.Max.Y),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// W returns the width of the Rect.
|
|
||||||
func (r Rect) W() float64 {
|
|
||||||
return r.Max.X - r.Min.X
|
|
||||||
}
|
|
||||||
|
|
||||||
// H returns the height of the Rect.
|
|
||||||
func (r Rect) H() float64 {
|
|
||||||
return r.Max.Y - r.Min.Y
|
|
||||||
}
|
|
||||||
|
|
||||||
// Size returns the vector of width and height of the Rect.
|
|
||||||
func (r Rect) Size() Vec {
|
|
||||||
return V(r.W(), r.H())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Area returns the area of r. If r is not normalized, area may be negative.
|
|
||||||
func (r Rect) Area() float64 {
|
|
||||||
return r.W() * r.H()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Edges will return the four lines which make up the edges of the rectangle.
|
|
||||||
func (r Rect) Edges() [4]Line {
|
|
||||||
corners := r.Vertices()
|
|
||||||
|
|
||||||
return [4]Line{
|
|
||||||
{A: corners[0], B: corners[1]},
|
|
||||||
{A: corners[1], B: corners[2]},
|
|
||||||
{A: corners[2], B: corners[3]},
|
|
||||||
{A: corners[3], B: corners[0]},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Anchor is a vector used to define anchors, such as `Center`, `Top`, `TopRight`, etc.
|
|
||||||
type Anchor Vec
|
|
||||||
|
|
||||||
var (
|
|
||||||
Center = Anchor{0.5, 0.5}
|
|
||||||
Top = Anchor{0.5, 0}
|
|
||||||
TopRight = Anchor{0, 0}
|
|
||||||
Right = Anchor{0, 0.5}
|
|
||||||
BottomRight = Anchor{0, 1}
|
|
||||||
Bottom = Anchor{0.5, 1}
|
|
||||||
BottomLeft = Anchor{1, 1}
|
|
||||||
Left = Anchor{1, 0.5}
|
|
||||||
TopLeft = Anchor{1, 0}
|
|
||||||
)
|
|
||||||
|
|
||||||
var anchorStrings map[Anchor]string = map[Anchor]string{
|
|
||||||
Center: "center",
|
|
||||||
Top: "top",
|
|
||||||
TopRight: "top-right",
|
|
||||||
Right: "right",
|
|
||||||
BottomRight: "bottom-right",
|
|
||||||
Bottom: "bottom",
|
|
||||||
BottomLeft: "bottom-left",
|
|
||||||
Left: "left",
|
|
||||||
TopLeft: "top-left",
|
|
||||||
}
|
|
||||||
|
|
||||||
// String returns the string representation of an anchor.
|
|
||||||
func (anchor Anchor) String() string {
|
|
||||||
return anchorStrings[anchor]
|
|
||||||
}
|
|
||||||
|
|
||||||
var oppositeAnchors map[Anchor]Anchor = map[Anchor]Anchor{
|
|
||||||
Center: Center,
|
|
||||||
Top: Bottom,
|
|
||||||
Bottom: Top,
|
|
||||||
Right: Left,
|
|
||||||
Left: Right,
|
|
||||||
TopRight: BottomLeft,
|
|
||||||
BottomLeft: TopRight,
|
|
||||||
BottomRight: TopLeft,
|
|
||||||
TopLeft: BottomRight,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Opposite returns the opposite position of the anchor (ie. Top -> Bottom; BottomLeft -> TopRight, etc.).
|
|
||||||
func (anchor Anchor) Opposite() Anchor {
|
|
||||||
return oppositeAnchors[anchor]
|
|
||||||
}
|
|
||||||
|
|
||||||
// AnchorPos returns the relative position of the given anchor.
|
|
||||||
func (r Rect) AnchorPos(anchor Anchor) Vec {
|
|
||||||
return r.Size().ScaledXY(V(0, 0).Sub(Vec(anchor)))
|
|
||||||
}
|
|
||||||
|
|
||||||
// AlignedTo returns the rect moved by the given anchor.
|
|
||||||
func (rect Rect) AlignedTo(anchor Anchor) Rect {
|
|
||||||
return rect.Moved(rect.AnchorPos(anchor))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Center returns the position of the center of the Rect.
|
|
||||||
// `rect.Center()` is equivalent to `rect.Anchor(pixel.Anchor.Center)`
|
|
||||||
func (r Rect) Center() Vec {
|
|
||||||
return Lerp(r.Min, r.Max, 0.5)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Moved returns the Rect moved (both Min and Max) by the given vector delta.
|
|
||||||
func (r Rect) Moved(delta Vec) Rect {
|
|
||||||
return Rect{
|
|
||||||
Min: r.Min.Add(delta),
|
|
||||||
Max: r.Max.Add(delta),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Resized returns the Rect resized to the given size while keeping the position of the given
|
|
||||||
// anchor.
|
|
||||||
//
|
|
||||||
// r.Resized(r.Min, size) // resizes while keeping the position of the lower-left corner
|
|
||||||
// r.Resized(r.Max, size) // same with the top-right corner
|
|
||||||
// r.Resized(r.Center(), size) // resizes around the center
|
|
||||||
//
|
|
||||||
// This function does not make sense for resizing a rectangle of zero area and will panic. Use
|
|
||||||
// ResizedMin in the case of zero area.
|
|
||||||
func (r Rect) Resized(anchor, size Vec) Rect {
|
|
||||||
if r.W()*r.H() == 0 {
|
|
||||||
panic(fmt.Errorf("(%T).Resize: zero area", r))
|
|
||||||
}
|
|
||||||
fraction := Vec{size.X / r.W(), size.Y / r.H()}
|
|
||||||
return Rect{
|
|
||||||
Min: anchor.Add(r.Min.Sub(anchor).ScaledXY(fraction)),
|
|
||||||
Max: anchor.Add(r.Max.Sub(anchor).ScaledXY(fraction)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ResizedMin returns the Rect resized to the given size while keeping the position of the Rect's
|
|
||||||
// Min.
|
|
||||||
//
|
|
||||||
// Sizes of zero area are safe here.
|
|
||||||
func (r Rect) ResizedMin(size Vec) Rect {
|
|
||||||
return Rect{
|
|
||||||
Min: r.Min,
|
|
||||||
Max: r.Min.Add(size),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Contains checks whether a vector u is contained within this Rect (including it's borders).
|
|
||||||
func (r Rect) Contains(u Vec) bool {
|
|
||||||
return r.Min.X <= u.X && u.X <= r.Max.X && r.Min.Y <= u.Y && u.Y <= r.Max.Y
|
|
||||||
}
|
|
||||||
|
|
||||||
// Union returns the minimal Rect which covers both r and s. Rects r and s must be normalized.
|
|
||||||
func (r Rect) Union(s Rect) Rect {
|
|
||||||
return R(
|
|
||||||
math.Min(r.Min.X, s.Min.X),
|
|
||||||
math.Min(r.Min.Y, s.Min.Y),
|
|
||||||
math.Max(r.Max.X, s.Max.X),
|
|
||||||
math.Max(r.Max.Y, s.Max.Y),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Intersect returns the maximal Rect which is covered by both r and s. Rects r and s must be normalized.
|
|
||||||
//
|
|
||||||
// If r and s don't overlap, this function returns a zero-rectangle.
|
|
||||||
func (r Rect) Intersect(s Rect) Rect {
|
|
||||||
t := R(
|
|
||||||
math.Max(r.Min.X, s.Min.X),
|
|
||||||
math.Max(r.Min.Y, s.Min.Y),
|
|
||||||
math.Min(r.Max.X, s.Max.X),
|
|
||||||
math.Min(r.Max.Y, s.Max.Y),
|
|
||||||
)
|
|
||||||
if t.Min.X >= t.Max.X || t.Min.Y >= t.Max.Y {
|
|
||||||
return ZR
|
|
||||||
}
|
|
||||||
return t
|
|
||||||
}
|
|
||||||
|
|
||||||
// Intersects returns whether or not the given Rect intersects at any point with this Rect.
|
|
||||||
//
|
|
||||||
// This function is overall about 5x faster than Intersect, so it is better
|
|
||||||
// to use if you have no need for the returned Rect from Intersect.
|
|
||||||
func (r Rect) Intersects(s Rect) bool {
|
|
||||||
return !(s.Max.X <= r.Min.X ||
|
|
||||||
s.Min.X >= r.Max.X ||
|
|
||||||
s.Max.Y <= r.Min.Y ||
|
|
||||||
s.Min.Y >= r.Max.Y)
|
|
||||||
}
|
|
||||||
|
|
||||||
// IntersectCircle returns a minimal required Vector, such that moving the rect by that vector would stop the Circle
|
|
||||||
// and the Rect intersecting. This function returns a zero-vector if the Circle and Rect do not overlap, and if only
|
|
||||||
// the perimeters touch.
|
|
||||||
//
|
|
||||||
// This function will return a non-zero vector if:
|
|
||||||
// - The Rect contains the Circle, partially or fully
|
|
||||||
// - The Circle contains the Rect, partially of fully
|
|
||||||
func (r Rect) IntersectCircle(c Circle) Vec {
|
|
||||||
return c.IntersectRect(r).Scaled(-1)
|
|
||||||
}
|
|
||||||
|
|
||||||
// IntersectLine will return the shortest Vec such that if the Rect is moved by the Vec returned, the Line and Rect no
|
|
||||||
// longer intersect.
|
|
||||||
func (r Rect) IntersectLine(l Line) Vec {
|
|
||||||
return l.IntersectRect(r).Scaled(-1)
|
|
||||||
}
|
|
||||||
|
|
||||||
// IntersectionPoints returns all the points where the Rect intersects with the line provided. This can be zero, one or
|
|
||||||
// two points, depending on the location of the shapes. The points of intersection will be returned in order of
|
|
||||||
// closest-to-l.A to closest-to-l.B.
|
|
||||||
func (r Rect) IntersectionPoints(l Line) []Vec {
|
|
||||||
// Use map keys to ensure unique points
|
|
||||||
pointMap := make(map[Vec]struct{})
|
|
||||||
|
|
||||||
for _, edge := range r.Edges() {
|
|
||||||
if intersect, ok := l.Intersect(edge); ok {
|
|
||||||
pointMap[intersect] = struct{}{}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
points := make([]Vec, 0, len(pointMap))
|
|
||||||
for point := range pointMap {
|
|
||||||
points = append(points, point)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Order the points
|
|
||||||
if len(points) == 2 {
|
|
||||||
if points[1].To(l.A).Len() < points[0].To(l.A).Len() {
|
|
||||||
return []Vec{points[1], points[0]}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return points
|
|
||||||
}
|
|
||||||
|
|
||||||
// Vertices returns a slice of the four corners which make up the rectangle.
|
|
||||||
func (r Rect) Vertices() [4]Vec {
|
|
||||||
return [4]Vec{
|
|
||||||
r.Min,
|
|
||||||
V(r.Min.X, r.Max.Y),
|
|
||||||
r.Max,
|
|
||||||
V(r.Max.X, r.Min.Y),
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,384 +0,0 @@
|
||||||
package pixel_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"reflect"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/faiface/pixel"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestRect_Resize(t *testing.T) {
|
|
||||||
type rectTestTransform struct {
|
|
||||||
name string
|
|
||||||
f func(pixel.Rect) pixel.Rect
|
|
||||||
}
|
|
||||||
|
|
||||||
// rectangles
|
|
||||||
squareAroundOrigin := pixel.R(-10, -10, 10, 10)
|
|
||||||
squareAround2020 := pixel.R(10, 10, 30, 30)
|
|
||||||
rectangleAroundOrigin := pixel.R(-20, -10, 20, 10)
|
|
||||||
rectangleAround2020 := pixel.R(0, 10, 40, 30)
|
|
||||||
|
|
||||||
// resize transformations
|
|
||||||
resizeByHalfAroundCenter := rectTestTransform{"by half around center", func(rect pixel.Rect) pixel.Rect {
|
|
||||||
return rect.Resized(rect.Center(), rect.Size().Scaled(0.5))
|
|
||||||
}}
|
|
||||||
resizeByHalfAroundMin := rectTestTransform{"by half around Min", func(rect pixel.Rect) pixel.Rect {
|
|
||||||
return rect.Resized(rect.Min, rect.Size().Scaled(0.5))
|
|
||||||
}}
|
|
||||||
resizeByHalfAroundMax := rectTestTransform{"by half around Max", func(rect pixel.Rect) pixel.Rect {
|
|
||||||
return rect.Resized(rect.Max, rect.Size().Scaled(0.5))
|
|
||||||
}}
|
|
||||||
resizeByHalfAroundMiddleOfLeftSide := rectTestTransform{"by half around middle of left side", func(rect pixel.Rect) pixel.Rect {
|
|
||||||
return rect.Resized(pixel.V(rect.Min.X, rect.Center().Y), rect.Size().Scaled(0.5))
|
|
||||||
}}
|
|
||||||
resizeByHalfAroundOrigin := rectTestTransform{"by half around the origin", func(rect pixel.Rect) pixel.Rect {
|
|
||||||
return rect.Resized(pixel.ZV, rect.Size().Scaled(0.5))
|
|
||||||
}}
|
|
||||||
|
|
||||||
testCases := []struct {
|
|
||||||
input pixel.Rect
|
|
||||||
transform rectTestTransform
|
|
||||||
answer pixel.Rect
|
|
||||||
}{
|
|
||||||
{squareAroundOrigin, resizeByHalfAroundCenter, pixel.R(-5, -5, 5, 5)},
|
|
||||||
{squareAround2020, resizeByHalfAroundCenter, pixel.R(15, 15, 25, 25)},
|
|
||||||
{rectangleAroundOrigin, resizeByHalfAroundCenter, pixel.R(-10, -5, 10, 5)},
|
|
||||||
{rectangleAround2020, resizeByHalfAroundCenter, pixel.R(10, 15, 30, 25)},
|
|
||||||
|
|
||||||
{squareAroundOrigin, resizeByHalfAroundMin, pixel.R(-10, -10, 0, 0)},
|
|
||||||
{squareAround2020, resizeByHalfAroundMin, pixel.R(10, 10, 20, 20)},
|
|
||||||
{rectangleAroundOrigin, resizeByHalfAroundMin, pixel.R(-20, -10, 0, 0)},
|
|
||||||
{rectangleAround2020, resizeByHalfAroundMin, pixel.R(0, 10, 20, 20)},
|
|
||||||
|
|
||||||
{squareAroundOrigin, resizeByHalfAroundMax, pixel.R(0, 0, 10, 10)},
|
|
||||||
{squareAround2020, resizeByHalfAroundMax, pixel.R(20, 20, 30, 30)},
|
|
||||||
{rectangleAroundOrigin, resizeByHalfAroundMax, pixel.R(0, 0, 20, 10)},
|
|
||||||
{rectangleAround2020, resizeByHalfAroundMax, pixel.R(20, 20, 40, 30)},
|
|
||||||
|
|
||||||
{squareAroundOrigin, resizeByHalfAroundMiddleOfLeftSide, pixel.R(-10, -5, 0, 5)},
|
|
||||||
{squareAround2020, resizeByHalfAroundMiddleOfLeftSide, pixel.R(10, 15, 20, 25)},
|
|
||||||
{rectangleAroundOrigin, resizeByHalfAroundMiddleOfLeftSide, pixel.R(-20, -5, 0, 5)},
|
|
||||||
{rectangleAround2020, resizeByHalfAroundMiddleOfLeftSide, pixel.R(0, 15, 20, 25)},
|
|
||||||
|
|
||||||
{squareAroundOrigin, resizeByHalfAroundOrigin, pixel.R(-5, -5, 5, 5)},
|
|
||||||
{squareAround2020, resizeByHalfAroundOrigin, pixel.R(5, 5, 15, 15)},
|
|
||||||
{rectangleAroundOrigin, resizeByHalfAroundOrigin, pixel.R(-10, -5, 10, 5)},
|
|
||||||
{rectangleAround2020, resizeByHalfAroundOrigin, pixel.R(0, 5, 20, 15)},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, testCase := range testCases {
|
|
||||||
t.Run(fmt.Sprintf("Resize %v %s", testCase.input, testCase.transform.name), func(t *testing.T) {
|
|
||||||
testResult := testCase.transform.f(testCase.input)
|
|
||||||
if testResult != testCase.answer {
|
|
||||||
t.Errorf("Got: %v, wanted: %v\n", testResult, testCase.answer)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRect_Edges(t *testing.T) {
|
|
||||||
type fields struct {
|
|
||||||
Min pixel.Vec
|
|
||||||
Max pixel.Vec
|
|
||||||
}
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
fields fields
|
|
||||||
want [4]pixel.Line
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "Get edges",
|
|
||||||
fields: fields{Min: pixel.V(0, 0), Max: pixel.V(10, 10)},
|
|
||||||
want: [4]pixel.Line{
|
|
||||||
pixel.L(pixel.V(0, 0), pixel.V(0, 10)),
|
|
||||||
pixel.L(pixel.V(0, 10), pixel.V(10, 10)),
|
|
||||||
pixel.L(pixel.V(10, 10), pixel.V(10, 0)),
|
|
||||||
pixel.L(pixel.V(10, 0), pixel.V(0, 0)),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
r := pixel.Rect{
|
|
||||||
Min: tt.fields.Min,
|
|
||||||
Max: tt.fields.Max,
|
|
||||||
}
|
|
||||||
if got := r.Edges(); !reflect.DeepEqual(got, tt.want) {
|
|
||||||
t.Errorf("Rect.Edges() = %v, want %v", got, tt.want)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRect_Vertices(t *testing.T) {
|
|
||||||
type fields struct {
|
|
||||||
Min pixel.Vec
|
|
||||||
Max pixel.Vec
|
|
||||||
}
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
fields fields
|
|
||||||
want [4]pixel.Vec
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "Get corners",
|
|
||||||
fields: fields{Min: pixel.V(0, 0), Max: pixel.V(10, 10)},
|
|
||||||
want: [4]pixel.Vec{
|
|
||||||
pixel.V(0, 0),
|
|
||||||
pixel.V(0, 10),
|
|
||||||
pixel.V(10, 10),
|
|
||||||
pixel.V(10, 0),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
r := pixel.Rect{
|
|
||||||
Min: tt.fields.Min,
|
|
||||||
Max: tt.fields.Max,
|
|
||||||
}
|
|
||||||
if got := r.Vertices(); !reflect.DeepEqual(got, tt.want) {
|
|
||||||
t.Errorf("Rect.Vertices() = %v, want %v", got, tt.want)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRect_IntersectCircle(t *testing.T) {
|
|
||||||
type fields struct {
|
|
||||||
Min pixel.Vec
|
|
||||||
Max pixel.Vec
|
|
||||||
}
|
|
||||||
type args struct {
|
|
||||||
c pixel.Circle
|
|
||||||
}
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
fields fields
|
|
||||||
args args
|
|
||||||
want pixel.Vec
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "Rect.IntersectCircle(): no overlap",
|
|
||||||
fields: fields{Min: pixel.ZV, Max: pixel.V(10, 10)},
|
|
||||||
args: args{c: pixel.C(pixel.V(50, 50), 1)},
|
|
||||||
want: pixel.ZV,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Rect.IntersectCircle(): circle contains rect",
|
|
||||||
fields: fields{Min: pixel.ZV, Max: pixel.V(10, 10)},
|
|
||||||
args: args{c: pixel.C(pixel.V(5, 5), 10)},
|
|
||||||
want: pixel.V(-15, 0),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Rect.IntersectCircle(): rect contains circle",
|
|
||||||
fields: fields{Min: pixel.ZV, Max: pixel.V(10, 10)},
|
|
||||||
args: args{c: pixel.C(pixel.V(5, 5), 1)},
|
|
||||||
want: pixel.V(-6, 0),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Rect.IntersectCircle(): circle overlaps bottom-left corner",
|
|
||||||
fields: fields{Min: pixel.ZV, Max: pixel.V(10, 10)},
|
|
||||||
args: args{c: pixel.C(pixel.V(-0.5, -0.5), 1)},
|
|
||||||
want: pixel.V(-0.2, -0.2),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Rect.IntersectCircle(): circle overlaps top-left corner",
|
|
||||||
fields: fields{Min: pixel.ZV, Max: pixel.V(10, 10)},
|
|
||||||
args: args{c: pixel.C(pixel.V(-0.5, 10.5), 1)},
|
|
||||||
want: pixel.V(-0.2, 0.2),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Rect.IntersectCircle(): circle overlaps bottom-right corner",
|
|
||||||
fields: fields{Min: pixel.ZV, Max: pixel.V(10, 10)},
|
|
||||||
args: args{c: pixel.C(pixel.V(10.5, -0.5), 1)},
|
|
||||||
want: pixel.V(0.2, -0.2),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Rect.IntersectCircle(): circle overlaps top-right corner",
|
|
||||||
fields: fields{Min: pixel.ZV, Max: pixel.V(10, 10)},
|
|
||||||
args: args{c: pixel.C(pixel.V(10.5, 10.5), 1)},
|
|
||||||
want: pixel.V(0.2, 0.2),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Rect.IntersectCircle(): circle overlaps two corners",
|
|
||||||
fields: fields{Min: pixel.ZV, Max: pixel.V(10, 10)},
|
|
||||||
args: args{c: pixel.C(pixel.V(0, 5), 6)},
|
|
||||||
want: pixel.V(6, 0),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Rect.IntersectCircle(): circle overlaps left edge",
|
|
||||||
fields: fields{Min: pixel.ZV, Max: pixel.V(10, 10)},
|
|
||||||
args: args{c: pixel.C(pixel.V(0, 5), 1)},
|
|
||||||
want: pixel.V(1, 0),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Rect.IntersectCircle(): circle overlaps bottom edge",
|
|
||||||
fields: fields{Min: pixel.ZV, Max: pixel.V(10, 10)},
|
|
||||||
args: args{c: pixel.C(pixel.V(5, 0), 1)},
|
|
||||||
want: pixel.V(0, 1),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Rect.IntersectCircle(): circle overlaps right edge",
|
|
||||||
fields: fields{Min: pixel.ZV, Max: pixel.V(10, 10)},
|
|
||||||
args: args{c: pixel.C(pixel.V(10, 5), 1)},
|
|
||||||
want: pixel.V(-1, 0),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Rect.IntersectCircle(): circle overlaps top edge",
|
|
||||||
fields: fields{Min: pixel.ZV, Max: pixel.V(10, 10)},
|
|
||||||
args: args{c: pixel.C(pixel.V(5, 10), 1)},
|
|
||||||
want: pixel.V(0, -1),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Rect.IntersectCircle(): edge is tangent of left side",
|
|
||||||
fields: fields{Min: pixel.ZV, Max: pixel.V(10, 10)},
|
|
||||||
args: args{c: pixel.C(pixel.V(-1, 5), 1)},
|
|
||||||
want: pixel.ZV,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Rect.IntersectCircle(): edge is tangent of top side",
|
|
||||||
fields: fields{Min: pixel.ZV, Max: pixel.V(10, 10)},
|
|
||||||
args: args{c: pixel.C(pixel.V(5, -1), 1)},
|
|
||||||
want: pixel.ZV,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Rect.IntersectCircle(): circle above rectangle",
|
|
||||||
fields: fields{Min: pixel.ZV, Max: pixel.V(10, 10)},
|
|
||||||
args: args{c: pixel.C(pixel.V(5, 12), 1)},
|
|
||||||
want: pixel.ZV,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Rect.IntersectCircle(): circle below rectangle",
|
|
||||||
fields: fields{Min: pixel.ZV, Max: pixel.V(10, 10)},
|
|
||||||
args: args{c: pixel.C(pixel.V(5, -2), 1)},
|
|
||||||
want: pixel.ZV,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Rect.IntersectCircle(): circle left of rectangle",
|
|
||||||
fields: fields{Min: pixel.ZV, Max: pixel.V(10, 10)},
|
|
||||||
args: args{c: pixel.C(pixel.V(-1, 5), 1)},
|
|
||||||
want: pixel.ZV,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Rect.IntersectCircle(): circle right of rectangle",
|
|
||||||
fields: fields{Min: pixel.ZV, Max: pixel.V(10, 10)},
|
|
||||||
args: args{c: pixel.C(pixel.V(11, 5), 1)},
|
|
||||||
want: pixel.ZV,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
r := pixel.Rect{
|
|
||||||
Min: tt.fields.Min,
|
|
||||||
Max: tt.fields.Max,
|
|
||||||
}
|
|
||||||
got := r.IntersectCircle(tt.args.c)
|
|
||||||
if !closeEnough(got.X, tt.want.X, 2) || !closeEnough(got.Y, tt.want.Y, 2) {
|
|
||||||
t.Errorf("Rect.IntersectCircle() = %v, want %v", got, tt.want)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRect_IntersectionPoints(t *testing.T) {
|
|
||||||
type fields struct {
|
|
||||||
Min pixel.Vec
|
|
||||||
Max pixel.Vec
|
|
||||||
}
|
|
||||||
type args struct {
|
|
||||||
l pixel.Line
|
|
||||||
}
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
fields fields
|
|
||||||
args args
|
|
||||||
want []pixel.Vec
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "No intersection points",
|
|
||||||
fields: fields{Min: pixel.V(1, 1), Max: pixel.V(5, 5)},
|
|
||||||
args: args{l: pixel.L(pixel.V(-5, 0), pixel.V(-2, 2))},
|
|
||||||
want: []pixel.Vec{},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "One intersection point",
|
|
||||||
fields: fields{Min: pixel.V(1, 1), Max: pixel.V(5, 5)},
|
|
||||||
args: args{l: pixel.L(pixel.V(2, 0), pixel.V(2, 3))},
|
|
||||||
want: []pixel.Vec{pixel.V(2, 1)},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Two intersection points",
|
|
||||||
fields: fields{Min: pixel.V(1, 1), Max: pixel.V(5, 5)},
|
|
||||||
args: args{l: pixel.L(pixel.V(0, 2), pixel.V(6, 2))},
|
|
||||||
want: []pixel.Vec{pixel.V(1, 2), pixel.V(5, 2)},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
r := pixel.Rect{
|
|
||||||
Min: tt.fields.Min,
|
|
||||||
Max: tt.fields.Max,
|
|
||||||
}
|
|
||||||
if got := r.IntersectionPoints(tt.args.l); !reflect.DeepEqual(got, tt.want) {
|
|
||||||
t.Errorf("Rect.IntersectPoints() = %v, want %v", got, tt.want)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var rectIntTests = []struct {
|
|
||||||
name string
|
|
||||||
r1, r2 pixel.Rect
|
|
||||||
want pixel.Rect
|
|
||||||
intersect bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "Nothing touching",
|
|
||||||
r1: pixel.R(0, 0, 10, 10),
|
|
||||||
r2: pixel.R(21, 21, 40, 40),
|
|
||||||
want: pixel.ZR,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Edge touching",
|
|
||||||
r1: pixel.R(0, 0, 10, 10),
|
|
||||||
r2: pixel.R(10, 10, 20, 20),
|
|
||||||
want: pixel.ZR,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Bit of overlap",
|
|
||||||
r1: pixel.R(0, 0, 10, 10),
|
|
||||||
r2: pixel.R(0, 9, 20, 20),
|
|
||||||
want: pixel.R(0, 9, 10, 10),
|
|
||||||
intersect: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Fully overlapped",
|
|
||||||
r1: pixel.R(0, 0, 10, 10),
|
|
||||||
r2: pixel.R(0, 0, 10, 10),
|
|
||||||
want: pixel.R(0, 0, 10, 10),
|
|
||||||
intersect: true,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRect_Intersect(t *testing.T) {
|
|
||||||
for _, tt := range rectIntTests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
if got := tt.r1.Intersect(tt.r2); !reflect.DeepEqual(got, tt.want) {
|
|
||||||
t.Errorf("Rect.Intersect() = %v, want %v", got, tt.want)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRect_Intersects(t *testing.T) {
|
|
||||||
for _, tt := range rectIntTests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
if got := tt.r1.Intersects(tt.r2); got != tt.intersect {
|
|
||||||
t.Errorf("Rect.Intersects() = %v, want %v", got, tt.want)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
95
sprite.go
|
@ -9,10 +9,8 @@ import "image/color"
|
||||||
//
|
//
|
||||||
// sprite := pixel.NewSprite(pic, pic.Bounds())
|
// sprite := pixel.NewSprite(pic, pic.Bounds())
|
||||||
//
|
//
|
||||||
// Note, that Sprite caches the results of MakePicture from Targets it's drawn to for each Picture
|
// To achieve different anchoring, transformations and color masking, use SetMatrix and SetColorMask
|
||||||
// it's set to. What it means is that using a Sprite with an unbounded number of Pictures leads to a
|
// methods.
|
||||||
// memory leak, since Sprite caches them and never forgets. In such a situation, create a new Sprite
|
|
||||||
// for each Picture.
|
|
||||||
type Sprite struct {
|
type Sprite struct {
|
||||||
tri *TrianglesData
|
tri *TrianglesData
|
||||||
frame Rect
|
frame Rect
|
||||||
|
@ -27,7 +25,7 @@ func NewSprite(pic Picture, frame Rect) *Sprite {
|
||||||
tri := MakeTrianglesData(6)
|
tri := MakeTrianglesData(6)
|
||||||
s := &Sprite{
|
s := &Sprite{
|
||||||
tri: tri,
|
tri: tri,
|
||||||
d: Drawer{Triangles: tri, Cached: true},
|
d: Drawer{Triangles: tri},
|
||||||
}
|
}
|
||||||
s.matrix = IM
|
s.matrix = IM
|
||||||
s.mask = Alpha(1)
|
s.mask = Alpha(1)
|
||||||
|
@ -44,13 +42,6 @@ func (s *Sprite) Set(pic Picture, frame Rect) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetCached makes the sprite cache all the
|
|
||||||
// incoming pictures if the argument is true, and
|
|
||||||
// doesn't make it do that if the argument is false.
|
|
||||||
func (s *Sprite) SetCached(cached bool) {
|
|
||||||
s.d.Cached = cached
|
|
||||||
}
|
|
||||||
|
|
||||||
// Picture returns the current Sprite's Picture.
|
// Picture returns the current Sprite's Picture.
|
||||||
func (s *Sprite) Picture() Picture {
|
func (s *Sprite) Picture() Picture {
|
||||||
return s.d.Picture
|
return s.d.Picture
|
||||||
|
@ -61,58 +52,70 @@ func (s *Sprite) Frame() Rect {
|
||||||
return s.frame
|
return s.frame
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draw draws the Sprite onto the provided Target. The Sprite will be transformed by the given Matrix.
|
// SetMatrix sets a Matrix that this Sprite will be transformed by. This overrides any previously
|
||||||
|
// set Matrix.
|
||||||
//
|
//
|
||||||
// This method is equivalent to calling DrawColorMask with nil color mask.
|
// Note, that this has nothing to do with BasicTarget's SetMatrix method. This only affects this
|
||||||
func (s *Sprite) Draw(t Target, matrix Matrix) {
|
// Sprite and is usable with any Target.
|
||||||
s.DrawColorMask(t, matrix, nil)
|
func (s *Sprite) SetMatrix(matrix Matrix) {
|
||||||
}
|
if s.matrix != matrix {
|
||||||
|
|
||||||
// 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.
|
|
||||||
//
|
|
||||||
// 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
|
s.matrix = matrix
|
||||||
dirty = true
|
|
||||||
}
|
|
||||||
if mask == nil {
|
|
||||||
mask = Alpha(1)
|
|
||||||
}
|
|
||||||
rgba := ToRGBA(mask)
|
|
||||||
if rgba != s.mask {
|
|
||||||
s.mask = rgba
|
|
||||||
dirty = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if dirty {
|
|
||||||
s.calcData()
|
s.calcData()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
//
|
||||||
|
// 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) {
|
||||||
|
rgba := ToRGBA(mask)
|
||||||
|
if s.mask != rgba {
|
||||||
|
s.mask = ToRGBA(mask)
|
||||||
|
s.calcData()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ColorMask returns the currently set color mask.
|
||||||
|
func (s *Sprite) ColorMask() RGBA {
|
||||||
|
return s.mask
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw draws the Sprite onto the provided Target.
|
||||||
|
func (s *Sprite) Draw(t Target) {
|
||||||
s.d.Draw(t)
|
s.d.Draw(t)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Sprite) calcData() {
|
func (s *Sprite) calcData() {
|
||||||
var (
|
var (
|
||||||
center = s.frame.Center()
|
center = s.frame.Center()
|
||||||
horizontal = V(s.frame.W()/2, 0)
|
horizontal = X(s.frame.W() / 2)
|
||||||
vertical = V(0, s.frame.H()/2)
|
vertical = Y(s.frame.H() / 2)
|
||||||
)
|
)
|
||||||
|
|
||||||
(*s.tri)[0].Position = Vec{}.Sub(horizontal).Sub(vertical)
|
(*s.tri)[0].Position = -horizontal - vertical
|
||||||
(*s.tri)[1].Position = Vec{}.Add(horizontal).Sub(vertical)
|
(*s.tri)[1].Position = +horizontal - vertical
|
||||||
(*s.tri)[2].Position = Vec{}.Add(horizontal).Add(vertical)
|
(*s.tri)[2].Position = +horizontal + vertical
|
||||||
(*s.tri)[3].Position = Vec{}.Sub(horizontal).Sub(vertical)
|
(*s.tri)[3].Position = -horizontal - vertical
|
||||||
(*s.tri)[4].Position = Vec{}.Add(horizontal).Add(vertical)
|
(*s.tri)[4].Position = +horizontal + vertical
|
||||||
(*s.tri)[5].Position = Vec{}.Sub(horizontal).Add(vertical)
|
(*s.tri)[5].Position = -horizontal + vertical
|
||||||
|
|
||||||
for i := range *s.tri {
|
for i := range *s.tri {
|
||||||
(*s.tri)[i].Color = s.mask
|
(*s.tri)[i].Color = s.mask
|
||||||
(*s.tri)[i].Picture = center.Add((*s.tri)[i].Position)
|
(*s.tri)[i].Picture = center + (*s.tri)[i].Position
|
||||||
(*s.tri)[i].Intensity = 1
|
(*s.tri)[i].Intensity = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// matrix and mask
|
||||||
|
for i := range *s.tri {
|
||||||
(*s.tri)[i].Position = s.matrix.Project((*s.tri)[i].Position)
|
(*s.tri)[i].Position = s.matrix.Project((*s.tri)[i].Position)
|
||||||
|
(*s.tri)[i].Color = s.mask
|
||||||
}
|
}
|
||||||
|
|
||||||
s.d.Dirty()
|
s.d.Dirty()
|
||||||
|
|
247
text/atlas.go
|
@ -1,247 +0,0 @@
|
||||||
package text
|
|
||||||
|
|
||||||
import (
|
|
||||||
"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 {
|
|
||||||
if dr, mask, maskp, _, ok := face.Glyph(fg.dot, r); ok {
|
|
||||||
draw.Draw(atlasImg, dr, mask, maskp, draw.Src)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bounds := pixel.R(
|
|
||||||
i2f(fixedBounds.Min.X),
|
|
||||||
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 {
|
|
||||||
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)
|
|
||||||
}
|
|
|
@ -1,29 +0,0 @@
|
||||||
package text_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/faiface/pixel/text"
|
|
||||||
"golang.org/x/image/font/inconsolata"
|
|
||||||
)
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAtlasInconsolata(t *testing.T) {
|
|
||||||
text.NewAtlas(inconsolata.Regular8x16, text.ASCII)
|
|
||||||
}
|
|
|
@ -1,2 +0,0 @@
|
||||||
// Package text implements efficient text drawing for the Pixel library.
|
|
||||||
package text
|
|
359
text/text.go
|
@ -1,359 +0,0 @@
|
||||||
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
|
|
||||||
anchor pixel.Anchor
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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.transD.Cached = true
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
// AlignedTo returns the text moved by the given anchor.
|
|
||||||
func (txt *Text) AlignedTo(anchor pixel.Anchor) *Text {
|
|
||||||
txt.anchor = anchor
|
|
||||||
return txt
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clear removes all written text from the Text. The Dot field is reset to Orig.
|
|
||||||
func (txt *Text) Clear() {
|
|
||||||
txt.prevR = -1
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
offset := txt.Orig.Sub(txt.Bounds().Max.Add(txt.Bounds().AnchorPos(txt.anchor.Opposite())))
|
|
||||||
txt.mat = pixel.IM.Moved(offset).Chained(txt.mat)
|
|
||||||
|
|
||||||
if mask == nil {
|
|
||||||
mask = pixel.Alpha(1)
|
|
||||||
}
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,87 +0,0 @@
|
||||||
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)
|
|
||||||
}
|
|
462
vector.go
|
@ -1,462 +0,0 @@
|
||||||
package pixel
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"math"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Vec is a 2D vector type with X and Y coordinates.
|
|
||||||
//
|
|
||||||
// Create vectors with the V constructor:
|
|
||||||
//
|
|
||||||
// u := pixel.V(1, 2)
|
|
||||||
// v := pixel.V(8, -3)
|
|
||||||
//
|
|
||||||
// Use various methods to manipulate them:
|
|
||||||
//
|
|
||||||
// w := u.Add(v)
|
|
||||||
// fmt.Println(w) // Vec(9, -1)
|
|
||||||
// fmt.Println(u.Sub(v)) // Vec(-7, 5)
|
|
||||||
// u = pixel.V(2, 3)
|
|
||||||
// v = pixel.V(8, 1)
|
|
||||||
// if u.X < 0 {
|
|
||||||
// fmt.Println("this won't happen")
|
|
||||||
// }
|
|
||||||
// x := u.Unit().Dot(v.Unit())
|
|
||||||
type Vec struct {
|
|
||||||
X, Y float64
|
|
||||||
}
|
|
||||||
|
|
||||||
// ZV is a zero vector.
|
|
||||||
var ZV = Vec{0, 0}
|
|
||||||
|
|
||||||
// V returns a new 2D vector with the given coordinates.
|
|
||||||
func V(x, y float64) Vec {
|
|
||||||
return Vec{x, y}
|
|
||||||
}
|
|
||||||
|
|
||||||
// nearlyEqual compares two float64s and returns whether they are equal, accounting for rounding errors.At worst, the
|
|
||||||
// result is correct to 7 significant digits.
|
|
||||||
func nearlyEqual(a, b float64) bool {
|
|
||||||
epsilon := 0.000001
|
|
||||||
|
|
||||||
if a == b {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
diff := math.Abs(a - b)
|
|
||||||
|
|
||||||
if a == 0.0 || b == 0.0 || diff < math.SmallestNonzeroFloat64 {
|
|
||||||
return diff < (epsilon * math.SmallestNonzeroFloat64)
|
|
||||||
}
|
|
||||||
|
|
||||||
absA := math.Abs(a)
|
|
||||||
absB := math.Abs(b)
|
|
||||||
|
|
||||||
return diff/math.Min(absA+absB, math.MaxFloat64) < epsilon
|
|
||||||
}
|
|
||||||
|
|
||||||
// Eq will compare two vectors and return whether they are equal accounting for rounding errors. At worst, the result
|
|
||||||
// is correct to 7 significant digits.
|
|
||||||
func (u Vec) Eq(v Vec) bool {
|
|
||||||
return nearlyEqual(u.X, v.X) && nearlyEqual(u.Y, v.Y)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Unit returns a vector of length 1 facing the given angle.
|
|
||||||
func Unit(angle float64) Vec {
|
|
||||||
return Vec{1, 0}.Rotated(angle)
|
|
||||||
}
|
|
||||||
|
|
||||||
// String returns the string representation of the vector u.
|
|
||||||
//
|
|
||||||
// u := pixel.V(4.5, -1.3)
|
|
||||||
// u.String() // returns "Vec(4.5, -1.3)"
|
|
||||||
// fmt.Println(u) // Vec(4.5, -1.3)
|
|
||||||
func (u Vec) String() string {
|
|
||||||
return fmt.Sprintf("Vec(%v, %v)", u.X, u.Y)
|
|
||||||
}
|
|
||||||
|
|
||||||
// XY returns the components of the vector in two return values.
|
|
||||||
func (u Vec) XY() (x, y float64) {
|
|
||||||
return u.X, u.Y
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add returns the sum of vectors u and v.
|
|
||||||
func (u Vec) Add(v Vec) Vec {
|
|
||||||
return Vec{
|
|
||||||
u.X + v.X,
|
|
||||||
u.Y + v.Y,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sub returns the difference betweeen vectors u and v.
|
|
||||||
func (u Vec) Sub(v Vec) Vec {
|
|
||||||
return Vec{
|
|
||||||
u.X - v.X,
|
|
||||||
u.Y - v.Y,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Floor converts x and y to their integer equivalents.
|
|
||||||
func (u Vec) Floor() Vec {
|
|
||||||
return Vec{
|
|
||||||
math.Floor(u.X),
|
|
||||||
math.Floor(u.Y),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// To returns the vector from u to v. Equivalent to v.Sub(u).
|
|
||||||
func (u Vec) To(v Vec) Vec {
|
|
||||||
return Vec{
|
|
||||||
v.X - u.X,
|
|
||||||
v.Y - u.Y,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Scaled returns the vector u multiplied by c.
|
|
||||||
func (u Vec) Scaled(c float64) Vec {
|
|
||||||
return Vec{u.X * c, u.Y * c}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ScaledXY returns the vector u multiplied by the vector v component-wise.
|
|
||||||
func (u Vec) ScaledXY(v Vec) Vec {
|
|
||||||
return Vec{u.X * v.X, u.Y * v.Y}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Len returns the length of the vector u.
|
|
||||||
func (u Vec) Len() float64 {
|
|
||||||
return math.Hypot(u.X, u.Y)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SqLen returns the squared length of the vector u (faster to compute than Len).
|
|
||||||
func (u Vec) SqLen() float64 {
|
|
||||||
return u.X*u.X + u.Y*u.Y
|
|
||||||
}
|
|
||||||
|
|
||||||
// Angle returns the angle between the vector u and the x-axis. The result is in range [-Pi, Pi].
|
|
||||||
func (u Vec) Angle() float64 {
|
|
||||||
return math.Atan2(u.Y, u.X)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Unit returns a vector of length 1 facing the direction of u (has the same angle).
|
|
||||||
func (u Vec) Unit() Vec {
|
|
||||||
if u.X == 0 && u.Y == 0 {
|
|
||||||
return Vec{1, 0}
|
|
||||||
}
|
|
||||||
return u.Scaled(1 / u.Len())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Rotated returns the vector u rotated by the given angle in radians.
|
|
||||||
func (u Vec) Rotated(angle float64) Vec {
|
|
||||||
sin, cos := math.Sincos(angle)
|
|
||||||
return Vec{
|
|
||||||
u.X*cos - u.Y*sin,
|
|
||||||
u.X*sin + u.Y*cos,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Normal returns a vector normal to u. Equivalent to u.Rotated(math.Pi / 2), but faster.
|
|
||||||
func (u Vec) Normal() Vec {
|
|
||||||
return Vec{-u.Y, u.X}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Dot returns the dot product of vectors u and v.
|
|
||||||
func (u Vec) Dot(v Vec) float64 {
|
|
||||||
return u.X*v.X + u.Y*v.Y
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cross return the cross product of vectors u and v.
|
|
||||||
func (u Vec) Cross(v Vec) float64 {
|
|
||||||
return u.X*v.Y - v.X*u.Y
|
|
||||||
}
|
|
||||||
|
|
||||||
// Project returns a projection (or component) of vector u in the direction of vector v.
|
|
||||||
//
|
|
||||||
// Behaviour is undefined if v is a zero vector.
|
|
||||||
func (u Vec) Project(v Vec) Vec {
|
|
||||||
len := u.Dot(v) / v.Len()
|
|
||||||
return v.Unit().Scaled(len)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Map applies the function f to both x and y components of the vector u and returns the modified
|
|
||||||
// vector.
|
|
||||||
//
|
|
||||||
// u := pixel.V(10.5, -1.5)
|
|
||||||
// v := u.Map(math.Floor) // v is Vec(10, -2), both components of u floored
|
|
||||||
func (u Vec) Map(f func(float64) float64) Vec {
|
|
||||||
return Vec{
|
|
||||||
f(u.X),
|
|
||||||
f(u.Y),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Lerp returns a linear interpolation between vectors a and b.
|
|
||||||
//
|
|
||||||
// This function basically returns a point along the line between a and b and t chooses which one.
|
|
||||||
// If t is 0, then a will be returned, if t is 1, b will be returned. Anything between 0 and 1 will
|
|
||||||
// return the appropriate point between a and b and so on.
|
|
||||||
func Lerp(a, b Vec, t float64) Vec {
|
|
||||||
return a.Scaled(1 - t).Add(b.Scaled(t))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Line is a 2D line segment, between points A and B.
|
|
||||||
type Line struct {
|
|
||||||
A, B Vec
|
|
||||||
}
|
|
||||||
|
|
||||||
// L creates and returns a new Line.
|
|
||||||
func L(from, to Vec) Line {
|
|
||||||
return Line{
|
|
||||||
A: from,
|
|
||||||
B: to,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Bounds returns the lines bounding box. This is in the form of a normalized Rect.
|
|
||||||
func (l Line) Bounds() Rect {
|
|
||||||
return R(l.A.X, l.A.Y, l.B.X, l.B.Y).Norm()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Center will return the point at center of the line; that is, the point equidistant from either end.
|
|
||||||
func (l Line) Center() Vec {
|
|
||||||
return l.A.Add(l.A.To(l.B).Scaled(0.5))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Closest will return the point on the line which is closest to the Vec provided.
|
|
||||||
func (l Line) Closest(v Vec) Vec {
|
|
||||||
// between is a helper function which determines whether x is greater than min(a, b) and less than max(a, b)
|
|
||||||
between := func(a, b, x float64) bool {
|
|
||||||
min := math.Min(a, b)
|
|
||||||
max := math.Max(a, b)
|
|
||||||
return min < x && x < max
|
|
||||||
}
|
|
||||||
|
|
||||||
// Closest point will be on a line which perpendicular to this line.
|
|
||||||
// If and only if the infinite perpendicular line intersects the segment.
|
|
||||||
m, b := l.Formula()
|
|
||||||
|
|
||||||
// Account for horizontal lines
|
|
||||||
if m == 0 {
|
|
||||||
x := v.X
|
|
||||||
y := l.A.Y
|
|
||||||
|
|
||||||
// check if the X coordinate of v is on the line
|
|
||||||
if between(l.A.X, l.B.X, v.X) {
|
|
||||||
return V(x, y)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Otherwise get the closest endpoint
|
|
||||||
if l.A.To(v).Len() < l.B.To(v).Len() {
|
|
||||||
return l.A
|
|
||||||
}
|
|
||||||
return l.B
|
|
||||||
}
|
|
||||||
|
|
||||||
// Account for vertical lines
|
|
||||||
if math.IsInf(math.Abs(m), 1) {
|
|
||||||
x := l.A.X
|
|
||||||
y := v.Y
|
|
||||||
|
|
||||||
// check if the Y coordinate of v is on the line
|
|
||||||
if between(l.A.Y, l.B.Y, v.Y) {
|
|
||||||
return V(x, y)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Otherwise get the closest endpoint
|
|
||||||
if l.A.To(v).Len() < l.B.To(v).Len() {
|
|
||||||
return l.A
|
|
||||||
}
|
|
||||||
return l.B
|
|
||||||
}
|
|
||||||
|
|
||||||
perpendicularM := -1 / m
|
|
||||||
perpendicularB := v.Y - (perpendicularM * v.X)
|
|
||||||
|
|
||||||
// Coordinates of intersect (of infinite lines)
|
|
||||||
x := (perpendicularB - b) / (m - perpendicularM)
|
|
||||||
y := m*x + b
|
|
||||||
|
|
||||||
// Check if the point lies between the x and y bounds of the segment
|
|
||||||
if !between(l.A.X, l.B.X, x) && !between(l.A.Y, l.B.Y, y) {
|
|
||||||
// Not within bounding box
|
|
||||||
toStart := v.To(l.A)
|
|
||||||
toEnd := v.To(l.B)
|
|
||||||
|
|
||||||
if toStart.Len() < toEnd.Len() {
|
|
||||||
return l.A
|
|
||||||
}
|
|
||||||
return l.B
|
|
||||||
}
|
|
||||||
|
|
||||||
return V(x, y)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Contains returns whether the provided Vec lies on the line.
|
|
||||||
func (l Line) Contains(v Vec) bool {
|
|
||||||
return l.Closest(v).Eq(v)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Formula will return the values that represent the line in the formula: y = mx + b
|
|
||||||
// This function will return math.Inf+, math.Inf- for a vertical line.
|
|
||||||
func (l Line) Formula() (m, b float64) {
|
|
||||||
// Account for horizontal lines
|
|
||||||
if l.B.Y == l.A.Y {
|
|
||||||
return 0, l.A.Y
|
|
||||||
}
|
|
||||||
|
|
||||||
m = (l.B.Y - l.A.Y) / (l.B.X - l.A.X)
|
|
||||||
b = l.A.Y - (m * l.A.X)
|
|
||||||
|
|
||||||
return m, b
|
|
||||||
}
|
|
||||||
|
|
||||||
// Intersect will return the point of intersection for the two line segments. If the line segments do not intersect,
|
|
||||||
// this function will return the zero-vector and false.
|
|
||||||
func (l Line) Intersect(k Line) (Vec, bool) {
|
|
||||||
// Check if the lines are parallel
|
|
||||||
lDir := l.A.To(l.B)
|
|
||||||
kDir := k.A.To(k.B)
|
|
||||||
if lDir.X == kDir.X && lDir.Y == kDir.Y {
|
|
||||||
return ZV, false
|
|
||||||
}
|
|
||||||
|
|
||||||
// The lines intersect - but potentially not within the line segments.
|
|
||||||
// Get the intersection point for the lines if they were infinitely long, check if the point exists on both of the
|
|
||||||
// segments
|
|
||||||
lm, lb := l.Formula()
|
|
||||||
km, kb := k.Formula()
|
|
||||||
|
|
||||||
// Account for vertical lines
|
|
||||||
if math.IsInf(math.Abs(lm), 1) && math.IsInf(math.Abs(km), 1) {
|
|
||||||
// Both vertical, therefore parallel
|
|
||||||
return ZV, false
|
|
||||||
}
|
|
||||||
|
|
||||||
var x, y float64
|
|
||||||
|
|
||||||
if math.IsInf(math.Abs(lm), 1) || math.IsInf(math.Abs(km), 1) {
|
|
||||||
// One line is vertical
|
|
||||||
intersectM := lm
|
|
||||||
intersectB := lb
|
|
||||||
verticalLine := k
|
|
||||||
|
|
||||||
if math.IsInf(math.Abs(lm), 1) {
|
|
||||||
intersectM = km
|
|
||||||
intersectB = kb
|
|
||||||
verticalLine = l
|
|
||||||
}
|
|
||||||
|
|
||||||
y = intersectM*verticalLine.A.X + intersectB
|
|
||||||
x = verticalLine.A.X
|
|
||||||
} else {
|
|
||||||
// Coordinates of intersect
|
|
||||||
x = (kb - lb) / (lm - km)
|
|
||||||
y = lm*x + lb
|
|
||||||
}
|
|
||||||
|
|
||||||
if l.Contains(V(x, y)) && k.Contains(V(x, y)) {
|
|
||||||
// The intersect point is on both line segments, they intersect.
|
|
||||||
return V(x, y), true
|
|
||||||
}
|
|
||||||
|
|
||||||
return ZV, false
|
|
||||||
}
|
|
||||||
|
|
||||||
// IntersectCircle will return the shortest Vec such that moving the Line by that Vec will cause the Line and Circle
|
|
||||||
// to no longer intesect. If they do not intersect at all, this function will return a zero-vector.
|
|
||||||
func (l Line) IntersectCircle(c Circle) Vec {
|
|
||||||
// Get the point on the line closest to the center of the circle.
|
|
||||||
closest := l.Closest(c.Center)
|
|
||||||
cirToClosest := c.Center.To(closest)
|
|
||||||
|
|
||||||
if cirToClosest.Len() >= c.Radius {
|
|
||||||
return ZV
|
|
||||||
}
|
|
||||||
|
|
||||||
return cirToClosest.Scaled(cirToClosest.Len() - c.Radius)
|
|
||||||
}
|
|
||||||
|
|
||||||
// IntersectRect will return the shortest Vec such that moving the Line by that Vec will cause the Line and Rect to
|
|
||||||
// no longer intesect. If they do not intersect at all, this function will return a zero-vector.
|
|
||||||
func (l Line) IntersectRect(r Rect) Vec {
|
|
||||||
// Check if either end of the line segment are within the rectangle
|
|
||||||
if r.Contains(l.A) || r.Contains(l.B) {
|
|
||||||
// Use the Rect.Intersect to get minimal return value
|
|
||||||
rIntersect := l.Bounds().Intersect(r)
|
|
||||||
if rIntersect.H() > rIntersect.W() {
|
|
||||||
// Go vertical
|
|
||||||
return V(0, rIntersect.H())
|
|
||||||
}
|
|
||||||
return V(rIntersect.W(), 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if any of the rectangles' edges intersect with this line.
|
|
||||||
for _, edge := range r.Edges() {
|
|
||||||
if _, ok := l.Intersect(edge); ok {
|
|
||||||
// Get the closest points on the line to each corner, where:
|
|
||||||
// - the point is contained by the rectangle
|
|
||||||
// - the point is not the corner itself
|
|
||||||
corners := r.Vertices()
|
|
||||||
var closest *Vec
|
|
||||||
closestCorner := corners[0]
|
|
||||||
for _, c := range corners {
|
|
||||||
cc := l.Closest(c)
|
|
||||||
if closest == nil || (closest.Len() > cc.Len() && r.Contains(cc)) {
|
|
||||||
closest = &cc
|
|
||||||
closestCorner = c
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return closest.To(closestCorner)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// No intersect
|
|
||||||
return ZV
|
|
||||||
}
|
|
||||||
|
|
||||||
// Len returns the length of the line segment.
|
|
||||||
func (l Line) Len() float64 {
|
|
||||||
return l.A.To(l.B).Len()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Moved will return a line moved by the delta Vec provided.
|
|
||||||
func (l Line) Moved(delta Vec) Line {
|
|
||||||
return Line{
|
|
||||||
A: l.A.Add(delta),
|
|
||||||
B: l.B.Add(delta),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Rotated will rotate the line around the provided Vec.
|
|
||||||
func (l Line) Rotated(around Vec, angle float64) Line {
|
|
||||||
// Move the line so we can use `Vec.Rotated`
|
|
||||||
lineShifted := l.Moved(around.Scaled(-1))
|
|
||||||
|
|
||||||
lineRotated := Line{
|
|
||||||
A: lineShifted.A.Rotated(angle),
|
|
||||||
B: lineShifted.B.Rotated(angle),
|
|
||||||
}
|
|
||||||
|
|
||||||
return lineRotated.Moved(around)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Scaled will return the line scaled around the center point.
|
|
||||||
func (l Line) Scaled(scale float64) Line {
|
|
||||||
return l.ScaledXY(l.Center(), scale)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ScaledXY will return the line scaled around the Vec provided.
|
|
||||||
func (l Line) ScaledXY(around Vec, scale float64) Line {
|
|
||||||
toA := around.To(l.A).Scaled(scale)
|
|
||||||
toB := around.To(l.B).Scaled(scale)
|
|
||||||
|
|
||||||
return Line{
|
|
||||||
A: around.Add(toA),
|
|
||||||
B: around.Add(toB),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l Line) String() string {
|
|
||||||
return fmt.Sprintf("Line(%v, %v)", l.A, l.B)
|
|
||||||
}
|
|
|
@ -1,27 +0,0 @@
|
||||||
package pixel_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/faiface/pixel"
|
|
||||||
)
|
|
||||||
|
|
||||||
type floorTest struct {
|
|
||||||
input pixel.Vec
|
|
||||||
expected pixel.Vec
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestFloor(t *testing.T) {
|
|
||||||
tests := []floorTest{
|
|
||||||
{input: pixel.V(4.50, 6.70), expected: pixel.V(4, 6)},
|
|
||||||
{input: pixel.V(9.0, 6.70), expected: pixel.V(9, 6)},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tc := range tests {
|
|
||||||
result := tc.input.Floor()
|
|
||||||
if result != tc.expected {
|
|
||||||
t.Error(fmt.Sprintf("Expected %v but got %v", tc.expected, result))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|