Compare commits
218 Commits
Author | SHA1 | Date |
---|---|---|
|
4b7553cd73 | |
|
781c44f119 | |
|
c385b247b3 | |
|
3706d040ce | |
|
4749e3ee7e | |
|
e06acda99b | |
|
9e0c65d8dd | |
|
bbeab0aebf | |
|
d5f7088b7d | |
|
9a401948ae | |
|
abc99bdef8 | |
|
debdbea894 | |
|
fc30e51016 | |
|
90432a7857 | |
|
51c32e407f | |
|
659dc6fd5f | |
|
50ba35d4cb | |
|
9102076f1b | |
|
2834411318 | |
|
7fe45b5a88 | |
|
51e1843f4b | |
|
eab5be6207 | |
|
62fa797088 | |
|
970faf0b63 | |
|
6c269fc8a5 | |
|
a794d27972 | |
|
bc08b65073 | |
|
bf6e20a04b | |
|
ecdd8462bb | |
|
3af9c2b20e | |
|
3ae612a84d | |
|
0ac5371d7e | |
|
fccedc5a9d | |
|
f7aac5ed09 | |
|
1d928485d6 | |
|
b832e83517 | |
|
53167788d6 | |
|
9d60c5fa32 | |
|
cce26f0a51 | |
|
b15c10298e | |
|
3a14aae310 | |
|
e86120db20 | |
|
a510048648 | |
|
feb12a1c7e | |
|
3ffbbb9cda | |
|
863e1e2f0c | |
|
e3268db31e | |
|
7b10ad8497 | |
|
abcdff5960 | |
|
dc3f9857d8 | |
|
dd6d38b8f3 | |
|
46122dd826 | |
|
c8114d8467 | |
|
f58676289a | |
|
80735cfc0c | |
|
dcdc812af5 | |
|
248d68f6aa | |
|
3841afb70f | |
|
217ac0c4d7 | |
|
65236863fe | |
|
cfaff8c0cb | |
|
2d1f61f746 | |
|
883bdc32c7 | |
|
c9eea2639e | |
|
5b524dadd8 | |
|
a86876a1cd | |
|
29785fb937 | |
|
1d17e45825 | |
|
b91d8be6cd | |
|
c47d77b2b5 | |
|
e9a3c900cf | |
|
3d3cbd6027 | |
|
4e6d6eeb3a | |
|
17ddf4fec5 | |
|
e1e1815537 | |
|
a05abdca76 | |
|
317124058e | |
|
3b1e0eaa21 | |
|
6a7500959f | |
|
e2a16764c4 | |
|
424b6e0f0b | |
|
bc145eb1b8 | |
|
5303ec5648 | |
|
f7dad2f3c2 | |
|
3b5bfa90e6 | |
|
dff622523b | |
|
51a9702fc8 | |
|
f7bb304b92 | |
|
5efd04b420 | |
|
ea7bc5aff9 | |
|
7b8a0c152e | |
|
2115296062 | |
|
dc745825d6 | |
|
29fe3b16ca | |
|
d37ad8f1ba | |
|
85ba21a2f4 | |
|
915faeee0c | |
|
113d052872 | |
|
9571b4339b | |
|
64aa32a4b4 | |
|
01db742aba | |
|
ff64cf248b | |
|
d595d5f647 | |
|
951c7f4c59 | |
|
e8aa765ed3 | |
|
bdbce3a27b | |
|
0389ee7bf9 | |
|
736f4549a9 | |
|
ef9fd8fe10 | |
|
4246e90215 | |
|
1d0169b2b7 | |
|
2626892e97 | |
|
1e2eb29a4f | |
|
b60b7e5207 | |
|
8165c38c6d | |
|
82fa73952e | |
|
bab33976cc | |
|
0e54ed1080 | |
|
db4f08aefc | |
|
fd6e056f92 | |
|
e93b9a1d4c | |
|
d9a94fd157 | |
|
6e5cbaa493 | |
|
9e150a7a1b | |
|
9c74e66118 | |
|
ee74866587 | |
|
a2ad3dbf7c | |
|
af1ccf885f | |
|
b756edfac6 | |
|
c69ef58898 | |
|
6e63f27d6e | |
|
acfd836a7f | |
|
8679a702a9 | |
|
e9380d7eed | |
|
3ed8c0016c | |
|
2afe44a9c9 | |
|
c028b66b68 | |
|
080735510c | |
|
862f30a004 | |
|
c0ddb0b287 | |
|
3e493c13e1 | |
|
f9f61911a7 | |
|
d30b73fb8b | |
|
4aea56198f | |
|
591fadaaa5 | |
|
497df7842b | |
|
a403cfe50b | |
|
bdbba8f3c1 | |
|
c485793a83 | |
|
97158ba502 | |
|
01ff4230da | |
|
37dcef6ab6 | |
|
ab9d608c45 | |
|
05da8e6f92 | |
|
49c2beeca9 | |
|
f4ce166964 | |
|
93e9ab79b1 | |
|
58be215a76 | |
|
ce083a9a9f | |
|
2ef17e7a95 | |
|
80f48bc0b6 | |
|
420e83bc61 | |
|
b51b3e8f38 | |
|
620784b34c | |
|
368dab0e7c | |
|
9bfb83861a | |
|
4e30fb9f5c | |
|
11c76944b6 | |
|
30af7c6612 | |
|
411ad7c27d | |
|
37b0f0956b | |
|
10684b6add | |
|
cf666b5866 | |
|
f325092c02 | |
|
939a76923d | |
|
fcbc61c570 | |
|
e55d490801 | |
|
b5a2c12175 | |
|
14a01a5522 | |
|
3276c4e4a1 | |
|
c61b677fa9 | |
|
c9e0f7262d | |
|
a2499cf90d | |
|
6d2aeb64e7 | |
|
34a6d020a2 | |
|
9b624ae466 | |
|
f2a0a19f6e | |
|
a555999120 | |
|
aa2c560d4c | |
|
4eca5f2d1e | |
|
4374bb7be1 | |
|
3fa31cdab5 | |
|
219559cf20 | |
|
3d3f1c6e11 | |
|
58e8b8f892 | |
|
13b9e6aee5 | |
|
9e8697cdf6 | |
|
134fe2bf7b | |
|
9df8a3761b | |
|
a081d5b0c8 | |
|
32ae09e1e5 | |
|
317cfb17b0 | |
|
a9e6f41315 | |
|
9cbce8f638 | |
|
e3f7901f2c | |
|
ee19c6b361 | |
|
5ad013b286 | |
|
7fdf8c0d60 | |
|
3a25f78b1a | |
|
ad733f5946 | |
|
8d5879070f | |
|
2155babc5d | |
|
dc9afb5557 | |
|
79f7f4fb42 | |
|
2562f6b754 | |
|
b138fc5d5b | |
|
15a270e689 | |
|
538ad90185 |
152
README.md
|
@ -1,5 +1,149 @@
|
|||
# pixel
|
||||
A simple and fast desktop multimedia/gamedev library.
|
||||
# Pixel [](https://godoc.org/github.com/faiface/pixel) [](https://goreportcard.com/report/github.com/faiface/pixel) [](https://gitter.im/pixellib/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
||||
|
||||
The core features of the library are pretty much completed. I'm doing some proofreading and
|
||||
improving quality of the code. Will **announce** the library after.
|
||||
A hand-crafted 2D game library in Go. Take a look into the [features](#features) to see what it can
|
||||
do.
|
||||
|
||||
```
|
||||
go get github.com/faiface/pixel
|
||||
```
|
||||
|
||||
See [requirements](#requirements) for the list of libraries necessary for compilation.
|
||||
|
||||
## Tutorial
|
||||
|
||||
The [Wiki of this repo](https://github.com/faiface/pixel/wiki) contains an extensive tutorial
|
||||
covering several topics of Pixel. Here's the content of the tutorial parts so far:
|
||||
|
||||
- [Creating a Window](https://github.com/faiface/pixel/wiki/Creating-a-Window)
|
||||
- [Drawing a Sprite](https://github.com/faiface/pixel/wiki/Drawing-a-Sprite)
|
||||
- [Moving, scaling and rotating with Matrix](https://github.com/faiface/pixel/wiki/Moving,-scaling-and-rotating-with-Matrix)
|
||||
- [Pressing keys and clicking mouse](https://github.com/faiface/pixel/wiki/Pressing-keys-and-clicking-mouse)
|
||||
- [Drawing efficiently with Batch](https://github.com/faiface/pixel/wiki/Drawing-efficiently-with-Batch)
|
||||
- [Drawing shapes with IMDraw](https://github.com/faiface/pixel/wiki/Drawing-shapes-with-IMDraw)
|
||||
- [Typing text on the screen](https://github.com/faiface/pixel/wiki/Typing-text-on-the-screen)
|
||||
|
||||
## Examples
|
||||
|
||||
The [examples](https://github.com/faiface/pixel/tree/master/examples) directory contains a few
|
||||
examples demonstrating Pixel's functionality.
|
||||
|
||||
**To run an example**, navigate to it's directory, then `go run` the `main.go` file. For example:
|
||||
|
||||
```
|
||||
$ cd examples/platformer
|
||||
$ go run main.go
|
||||
```
|
||||
|
||||
Here are some screenshots from the examples!
|
||||
|
||||
| [Lights](examples/lights) | [Platformer](examples/platformer) |
|
||||
| --- | --- |
|
||||
|  |  |
|
||||
|
||||
| [Smoke](examples/smoke) | [Typewriter](examples/typewriter) |
|
||||
| --- | --- |
|
||||
|  |  |
|
||||
|
||||
## Features
|
||||
|
||||
Here's the list of the main features in Pixel. Although Pixel is still under heavy development,
|
||||
**there should be no major breakage in the API.** This is not a 100% guarantee, though.
|
||||
|
||||
- Fast 2D graphics
|
||||
- Sprites
|
||||
- Primitive shapes with immediate mode style
|
||||
[IMDraw](https://github.com/faiface/pixel/wiki/Drawing-shapes-with-IMDraw) (circles, rectangles,
|
||||
lines, ...)
|
||||
- Optimized drawing with [Batch](https://github.com/faiface/pixel/wiki/Drawing-efficiently-with-Batch)
|
||||
- Text drawing with [text](https://godoc.org/github.com/faiface/pixel/text) package
|
||||
- Simple and convenient API
|
||||
- Drawing a sprite to a window is as simple as `sprite.Draw(window, matrix)`
|
||||
- Wanna know where the center of a window is? `window.Bounds().Center()`
|
||||
- [...](https://godoc.org/github.com/faiface/pixel)
|
||||
- Full documentation and tutorial
|
||||
- Works on Linux, macOS and Windows
|
||||
- Window creation and manipulation (resizing, fullscreen, multiple windows, ...)
|
||||
- Keyboard (key presses, text input) and mouse input without events
|
||||
- Well integrated with the Go standard library
|
||||
- Use `"image"` package for loading pictures
|
||||
- Use `"time"` package for measuring delta time and FPS
|
||||
- Use `"image/color"` for colors, or use Pixel's own `color.Color` format, which supports easy
|
||||
multiplication and a few more features
|
||||
- Pixel uses `float64` throughout the library, compatible with `"math"` package
|
||||
- Geometry transformations with
|
||||
[Matrix](https://github.com/faiface/pixel/wiki/Moving,-scaling-and-rotating-with-Matrix)
|
||||
- Moving, scaling, rotating
|
||||
- Easy camera implementation
|
||||
- Off-screen drawing to Canvas or any other target (Batch, IMDraw, ...)
|
||||
- Fully garbage collected, no `Close` or `Dispose` methods
|
||||
- Full [Porter-Duff](http://ssp.impulsetrain.com/porterduff.html) composition, which enables
|
||||
- 2D lighting
|
||||
- Cutting holes into objects
|
||||
- Much more...
|
||||
- Pixel let's you draw stuff and do your job, it doesn't impose any particular style or paradigm
|
||||
- Platform and backend independent [core](https://godoc.org/github.com/faiface/pixel)
|
||||
- Core Target/Triangles/Picture pattern makes it easy to create new drawing targets that do
|
||||
arbitrarily crazy stuff (e.g. graphical effects)
|
||||
- Small codebase, ~5K lines of code, including the backend [glhf](https://github.com/faiface/glhf)
|
||||
package
|
||||
|
||||
## Missing features
|
||||
|
||||
Pixel is in development and still missing few critical features. Here're the most critical ones.
|
||||
|
||||
- Audio
|
||||
- ~~Drawing text~~
|
||||
- Antialiasing (filtering is supported, though)
|
||||
- ~~Advanced window manipulation (cursor hiding, window icon, ...)~~
|
||||
- Better support for Hi-DPI displays
|
||||
- Mobile (and perhaps HTML5?) backend
|
||||
- More advanced graphical effects (e.g. blur)
|
||||
- Tests and benchmarks
|
||||
|
||||
**Implementing these features will get us to the 1.0 release.** Contribute, so that it's as soon as
|
||||
possible!
|
||||
|
||||
## Requirements
|
||||
|
||||
[PixelGL](https://godoc.org/github.com/faiface/pixel/pixelgl) backend uses OpenGL to render
|
||||
graphics. Because of that, OpenGL development libraries are needed for compilation. The dependencies
|
||||
are same as for [GLFW](https://github.com/go-gl/glfw).
|
||||
|
||||
- On macOS, you need Xcode or Command Line Tools for Xcode (`xcode-select --install`) for required
|
||||
headers and libraries.
|
||||
- On Ubuntu/Debian-like Linux distributions, you need `libgl1-mesa-dev` and `xorg-dev` packages.
|
||||
- On CentOS/Fedora-like Linux distributions, you need `libX11-devel libXcursor-devel libXrandr-devel
|
||||
libXinerama-devel mesa-libGL-devel libXi-devel` packages.
|
||||
- See [here](http://www.glfw.org/docs/latest/compile.html#compile_deps) for full details.
|
||||
|
||||
**The combination of Go 1.8, macOS and latest XCode seems to be problematic** as mentioned in issue
|
||||
[#7](https://github.com/faiface/pixel/issues/7). This issue is probably not related to Pixel.
|
||||
**Upgrading to Go 1.8.1 fixes the issue.**
|
||||
|
||||
## Contributing
|
||||
|
||||
Pixel is in, let's say, mid-stage of development. Many of the important features are here, some are
|
||||
missing. That's why **contributions are very important and welcome!** All alone, I will be able to
|
||||
finish the library, but it'll take a lot of time. With your help, it'll take much less. I encourage
|
||||
everyone to contribute, even with just an idea. Especially welcome are **issues** and **pull
|
||||
requests**.
|
||||
|
||||
**However, I won't accept everything. Pixel is being developed with thought and care.** Each
|
||||
component was designed and re-designed multiple times. Code and API quality is very important here.
|
||||
API is focused on simplicity and expressiveness.
|
||||
|
||||
When contributing, keep these goals in mind. It doesn't mean that I'll only accept perfect pull
|
||||
requests. It just means that I might not like your idea. Or that your pull requests could need some
|
||||
rewriting. That's perfectly fine, don't let it put you off. In the end, we'll just end up with a
|
||||
better result.
|
||||
|
||||
**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
|
||||
|
||||
[MIT](LICENSE)
|
||||
|
|
34
batch.go
|
@ -5,8 +5,7 @@ import (
|
|||
"image/color"
|
||||
)
|
||||
|
||||
// Batch is a Target that allows for efficient drawing of many objects with the same Picture (but
|
||||
// different slices of the same Picture are allowed).
|
||||
// Batch is a Target that allows for efficient drawing of many objects with the same Picture.
|
||||
//
|
||||
// To put an object into a Batch, just draw it onto it:
|
||||
// object.Draw(batch)
|
||||
|
@ -14,7 +13,7 @@ type Batch struct {
|
|||
cont Drawer
|
||||
|
||||
mat Matrix
|
||||
col NRGBA
|
||||
col RGBA
|
||||
}
|
||||
|
||||
var _ BasicTarget = (*Batch)(nil)
|
||||
|
@ -29,7 +28,7 @@ var _ BasicTarget = (*Batch)(nil)
|
|||
func NewBatch(container Triangles, pic Picture) *Batch {
|
||||
b := &Batch{cont: Drawer{Triangles: container, Picture: pic}}
|
||||
b.SetMatrix(IM)
|
||||
b.SetColorMask(NRGBA{1, 1, 1, 1})
|
||||
b.SetColorMask(Alpha(1))
|
||||
return b
|
||||
}
|
||||
|
||||
|
@ -63,10 +62,10 @@ func (b *Batch) SetMatrix(m Matrix) {
|
|||
// SetColorMask sets a mask color used in the following draws onto the Batch.
|
||||
func (b *Batch) SetColorMask(c color.Color) {
|
||||
if c == nil {
|
||||
b.col = NRGBA{1, 1, 1, 1}
|
||||
b.col = Alpha(1)
|
||||
return
|
||||
}
|
||||
b.col = ToNRGBA(c)
|
||||
b.col = ToRGBA(c)
|
||||
}
|
||||
|
||||
// MakeTriangles returns a specialized copy of the provided Triangles that draws onto this Batch.
|
||||
|
@ -81,21 +80,19 @@ func (b *Batch) MakeTriangles(t Triangles) TargetTriangles {
|
|||
|
||||
// MakePicture returns a specialized copy of the provided Picture that draws onto this Batch.
|
||||
func (b *Batch) MakePicture(p Picture) TargetPicture {
|
||||
if p.Original() != b.cont.Picture.Original() {
|
||||
panic(fmt.Errorf("(%T).MakePicture: Picture is not a slice of Batch's Picture", b))
|
||||
if p != b.cont.Picture {
|
||||
panic(fmt.Errorf("(%T).MakePicture: Picture is not the Batch's Picture", b))
|
||||
}
|
||||
bp := &batchPicture{
|
||||
pic: p,
|
||||
dst: b,
|
||||
}
|
||||
bp.orig = bp
|
||||
return bp
|
||||
}
|
||||
|
||||
type batchTriangles struct {
|
||||
tri Triangles
|
||||
tmp *TrianglesData
|
||||
|
||||
dst *Batch
|
||||
}
|
||||
|
||||
|
@ -149,27 +146,14 @@ func (bt *batchTriangles) Draw() {
|
|||
}
|
||||
|
||||
type batchPicture struct {
|
||||
pic Picture
|
||||
orig *batchPicture
|
||||
dst *Batch
|
||||
pic Picture
|
||||
dst *Batch
|
||||
}
|
||||
|
||||
func (bp *batchPicture) Bounds() Rect {
|
||||
return bp.pic.Bounds()
|
||||
}
|
||||
|
||||
func (bp *batchPicture) Slice(r Rect) Picture {
|
||||
return &batchPicture{
|
||||
pic: bp.pic.Slice(r),
|
||||
orig: bp.orig,
|
||||
dst: bp.dst,
|
||||
}
|
||||
}
|
||||
|
||||
func (bp *batchPicture) Original() Picture {
|
||||
return bp.orig
|
||||
}
|
||||
|
||||
func (bp *batchPicture) Draw(t TargetTriangles) {
|
||||
bt := t.(*batchTriangles)
|
||||
if bp.dst != bt.dst {
|
||||
|
|
90
color.go
|
@ -2,17 +2,30 @@ package pixel
|
|||
|
||||
import "image/color"
|
||||
|
||||
// NRGBA represents a non-alpha-premultiplied RGBA color with components within range [0, 1].
|
||||
// RGBA represents an alpha-premultiplied RGBA color with components within range [0, 1].
|
||||
//
|
||||
// The difference between color.NRGBA is that the value range is [0, 1] and the values are floats.
|
||||
type NRGBA struct {
|
||||
// The difference between color.RGBA is that the value range is [0, 1] and the values are floats.
|
||||
type RGBA struct {
|
||||
R, G, B, A float64
|
||||
}
|
||||
|
||||
// RGB returns a fully opaque RGBA color with the given RGB values.
|
||||
//
|
||||
// A common way to construct a transparent color is to create one with RGB constructor, then
|
||||
// multiply it by a color obtained from the Alpha constructor.
|
||||
func RGB(r, g, b float64) RGBA {
|
||||
return RGBA{r, g, b, 1}
|
||||
}
|
||||
|
||||
// Alpha returns a white RGBA color with the given alpha component.
|
||||
func Alpha(a float64) RGBA {
|
||||
return RGBA{a, a, a, a}
|
||||
}
|
||||
|
||||
// Add adds color d to color c component-wise and returns the result (the components are not
|
||||
// clamped).
|
||||
func (c NRGBA) Add(d NRGBA) NRGBA {
|
||||
return NRGBA{
|
||||
func (c RGBA) Add(d RGBA) RGBA {
|
||||
return RGBA{
|
||||
R: c.R + d.R,
|
||||
G: c.G + d.G,
|
||||
B: c.B + d.B,
|
||||
|
@ -22,8 +35,8 @@ func (c NRGBA) Add(d NRGBA) NRGBA {
|
|||
|
||||
// Sub subtracts color d from color c component-wise and returns the result (the components
|
||||
// are not clamped).
|
||||
func (c NRGBA) Sub(d NRGBA) NRGBA {
|
||||
return NRGBA{
|
||||
func (c RGBA) Sub(d RGBA) RGBA {
|
||||
return RGBA{
|
||||
R: c.R - d.R,
|
||||
G: c.G - d.G,
|
||||
B: c.B - d.B,
|
||||
|
@ -32,8 +45,8 @@ func (c NRGBA) Sub(d NRGBA) NRGBA {
|
|||
}
|
||||
|
||||
// Mul multiplies color c by color d component-wise (the components are not clamped).
|
||||
func (c NRGBA) Mul(d NRGBA) NRGBA {
|
||||
return NRGBA{
|
||||
func (c RGBA) Mul(d RGBA) RGBA {
|
||||
return RGBA{
|
||||
R: c.R * d.R,
|
||||
G: c.G * d.G,
|
||||
B: c.B * d.B,
|
||||
|
@ -43,8 +56,8 @@ func (c NRGBA) Mul(d NRGBA) NRGBA {
|
|||
|
||||
// Scaled multiplies each component of color c by scale and returns the result (the components
|
||||
// are not clamped).
|
||||
func (c NRGBA) Scaled(scale float64) NRGBA {
|
||||
return NRGBA{
|
||||
func (c RGBA) Scaled(scale float64) RGBA {
|
||||
return RGBA{
|
||||
R: c.R * scale,
|
||||
G: c.G * scale,
|
||||
B: c.B * scale,
|
||||
|
@ -52,37 +65,23 @@ func (c NRGBA) Scaled(scale float64) NRGBA {
|
|||
}
|
||||
}
|
||||
|
||||
// RGBA returns alpha-premultiplied red, green, blue and alpha components of the NRGBA color.
|
||||
func (c NRGBA) RGBA() (r, g, b, a uint32) {
|
||||
c.R = clamp(c.R, 0, 1)
|
||||
c.G = clamp(c.G, 0, 1)
|
||||
c.B = clamp(c.B, 0, 1)
|
||||
c.A = clamp(c.A, 0, 1)
|
||||
r = uint32(0xffff * c.R * c.A)
|
||||
g = uint32(0xffff * c.G * c.A)
|
||||
b = uint32(0xffff * c.B * c.A)
|
||||
// RGBA returns alpha-premultiplied red, green, blue and alpha components of the RGBA color.
|
||||
func (c RGBA) RGBA() (r, g, b, a uint32) {
|
||||
r = uint32(0xffff * c.R)
|
||||
g = uint32(0xffff * c.G)
|
||||
b = uint32(0xffff * c.B)
|
||||
a = uint32(0xffff * c.A)
|
||||
return
|
||||
}
|
||||
|
||||
func clamp(x, low, high float64) float64 {
|
||||
if x < low {
|
||||
return low
|
||||
}
|
||||
if x > high {
|
||||
return high
|
||||
}
|
||||
return x
|
||||
}
|
||||
|
||||
// ToNRGBA converts a color to NRGBA format. Using this function is preferred to using NRGBAModel,
|
||||
// for performance (using NRGBAModel introduced additional unnecessary allocations).
|
||||
func ToNRGBA(c color.Color) NRGBA {
|
||||
if c, ok := c.(NRGBA); ok {
|
||||
// ToRGBA converts a color to RGBA format. Using this function is preferred to using RGBAModel, for
|
||||
// performance (using RGBAModel introduces additional unnecessary allocations).
|
||||
func ToRGBA(c color.Color) RGBA {
|
||||
if c, ok := c.(RGBA); ok {
|
||||
return c
|
||||
}
|
||||
if c, ok := c.(color.NRGBA); ok {
|
||||
return NRGBA{
|
||||
if c, ok := c.(color.RGBA); ok {
|
||||
return RGBA{
|
||||
R: float64(c.R) / 255,
|
||||
G: float64(c.G) / 255,
|
||||
B: float64(c.B) / 255,
|
||||
|
@ -90,20 +89,17 @@ func ToNRGBA(c color.Color) NRGBA {
|
|||
}
|
||||
}
|
||||
r, g, b, a := c.RGBA()
|
||||
if a == 0 {
|
||||
return NRGBA{0, 0, 0, 0}
|
||||
}
|
||||
return NRGBA{
|
||||
float64(r) / float64(a),
|
||||
float64(g) / float64(a),
|
||||
float64(b) / float64(a),
|
||||
return RGBA{
|
||||
float64(r) / 0xffff,
|
||||
float64(g) / 0xffff,
|
||||
float64(b) / 0xffff,
|
||||
float64(a) / 0xffff,
|
||||
}
|
||||
}
|
||||
|
||||
// NRGBAModel converts colors to NRGBA format.
|
||||
var NRGBAModel = color.ModelFunc(nrgbaModel)
|
||||
// RGBAModel converts colors to RGBA format.
|
||||
var RGBAModel = color.ModelFunc(rgbaModel)
|
||||
|
||||
func nrgbaModel(c color.Color) color.Color {
|
||||
return ToNRGBA(c)
|
||||
func rgbaModel(c color.Color) color.Color {
|
||||
return ToRGBA(c)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,65 @@
|
|||
package pixel
|
||||
|
||||
import "errors"
|
||||
|
||||
// ComposeTarget is a BasicTarget capable of Porter-Duff composition.
|
||||
type ComposeTarget interface {
|
||||
BasicTarget
|
||||
|
||||
// SetComposeMethod sets a Porter-Duff composition method to be used.
|
||||
SetComposeMethod(ComposeMethod)
|
||||
}
|
||||
|
||||
// ComposeMethod is a Porter-Duff composition method.
|
||||
type ComposeMethod int
|
||||
|
||||
// Here's the list of all available Porter-Duff composition methods. Use ComposeOver for the basic
|
||||
// alpha blending.
|
||||
const (
|
||||
ComposeOver ComposeMethod = iota
|
||||
ComposeIn
|
||||
ComposeOut
|
||||
ComposeAtop
|
||||
ComposeRover
|
||||
ComposeRin
|
||||
ComposeRout
|
||||
ComposeRatop
|
||||
ComposeXor
|
||||
ComposePlus
|
||||
ComposeCopy
|
||||
)
|
||||
|
||||
// Compose composes two colors together according to the ComposeMethod. A is the foreground, B is
|
||||
// the background.
|
||||
func (cm ComposeMethod) Compose(a, b RGBA) RGBA {
|
||||
var fa, fb float64
|
||||
|
||||
switch cm {
|
||||
case ComposeOver:
|
||||
fa, fb = 1, 1-a.A
|
||||
case ComposeIn:
|
||||
fa, fb = b.A, 0
|
||||
case ComposeOut:
|
||||
fa, fb = 1-b.A, 0
|
||||
case ComposeAtop:
|
||||
fa, fb = b.A, 1-a.A
|
||||
case ComposeRover:
|
||||
fa, fb = 1-b.A, 1
|
||||
case ComposeRin:
|
||||
fa, fb = 0, a.A
|
||||
case ComposeRout:
|
||||
fa, fb = 0, 1-a.A
|
||||
case ComposeRatop:
|
||||
fa, fb = 1-b.A, a.A
|
||||
case ComposeXor:
|
||||
fa, fb = 1-b.A, 1-a.A
|
||||
case ComposePlus:
|
||||
fa, fb = 1, 1
|
||||
case ComposeCopy:
|
||||
fa, fb = 1, 0
|
||||
default:
|
||||
panic(errors.New("Compose: invalid ComposeMethod"))
|
||||
}
|
||||
|
||||
return a.Mul(Alpha(fa)).Add(b.Mul(Alpha(fb)))
|
||||
}
|
142
data.go
|
@ -12,7 +12,7 @@ import (
|
|||
// TrianglesPosition, TrianglesColor and TrianglesPicture.
|
||||
type TrianglesData []struct {
|
||||
Position Vec
|
||||
Color NRGBA
|
||||
Color RGBA
|
||||
Picture Vec
|
||||
Intensity float64
|
||||
}
|
||||
|
@ -42,10 +42,10 @@ func (td *TrianglesData) SetLen(len int) {
|
|||
for i := 0; i < needAppend; i++ {
|
||||
*td = append(*td, struct {
|
||||
Position Vec
|
||||
Color NRGBA
|
||||
Color RGBA
|
||||
Picture Vec
|
||||
Intensity float64
|
||||
}{V(0, 0), NRGBA{1, 1, 1, 1}, V(0, 0), 0})
|
||||
}{ZV, Alpha(1), ZV, 0})
|
||||
}
|
||||
}
|
||||
if len < td.Len() {
|
||||
|
@ -108,7 +108,7 @@ func (td *TrianglesData) Position(i int) Vec {
|
|||
}
|
||||
|
||||
// Color returns the color property of i-th vertex.
|
||||
func (td *TrianglesData) Color(i int) NRGBA {
|
||||
func (td *TrianglesData) Color(i int) RGBA {
|
||||
return (*td)[i].Color
|
||||
}
|
||||
|
||||
|
@ -126,36 +126,34 @@ func (td *TrianglesData) Picture(i int) (pic Vec, intensity float64) {
|
|||
//
|
||||
// The struct's innards are exposed for convenience, manual modification is at your own risk.
|
||||
//
|
||||
// The format of the pixels is color.NRGBA and not pixel.NRGBA for a very serious reason:
|
||||
// pixel.NRGBA takes up 8x more memory than color.NRGBA.
|
||||
// The format of the pixels is color.RGBA and not pixel.RGBA for a very serious reason:
|
||||
// pixel.RGBA takes up 8x more memory than color.RGBA.
|
||||
type PictureData struct {
|
||||
Pix []color.NRGBA
|
||||
Pix []color.RGBA
|
||||
Stride int
|
||||
Rect Rect
|
||||
Orig *PictureData
|
||||
}
|
||||
|
||||
// MakePictureData creates a zero-initialized PictureData covering the given rectangle.
|
||||
func MakePictureData(rect Rect) *PictureData {
|
||||
w := int(math.Ceil(rect.Max.X())) - int(math.Floor(rect.Min.X()))
|
||||
h := int(math.Ceil(rect.Max.Y())) - int(math.Floor(rect.Min.Y()))
|
||||
w := int(math.Ceil(rect.Max.X)) - int(math.Floor(rect.Min.X))
|
||||
h := int(math.Ceil(rect.Max.Y)) - int(math.Floor(rect.Min.Y))
|
||||
pd := &PictureData{
|
||||
Stride: w,
|
||||
Rect: rect,
|
||||
}
|
||||
pd.Pix = make([]color.NRGBA, w*h)
|
||||
pd.Orig = pd
|
||||
pd.Pix = make([]color.RGBA, w*h)
|
||||
return pd
|
||||
}
|
||||
|
||||
func verticalFlip(nrgba *image.NRGBA) {
|
||||
bounds := nrgba.Bounds()
|
||||
func verticalFlip(rgba *image.RGBA) {
|
||||
bounds := rgba.Bounds()
|
||||
width := bounds.Dx()
|
||||
|
||||
tmpRow := make([]uint8, width*4)
|
||||
for i, j := 0, bounds.Dy()-1; i < j; i, j = i+1, j-1 {
|
||||
iRow := nrgba.Pix[i*nrgba.Stride : i*nrgba.Stride+width*4]
|
||||
jRow := nrgba.Pix[j*nrgba.Stride : j*nrgba.Stride+width*4]
|
||||
iRow := rgba.Pix[i*rgba.Stride : i*rgba.Stride+width*4]
|
||||
jRow := rgba.Pix[j*rgba.Stride : j*rgba.Stride+width*4]
|
||||
|
||||
copy(tmpRow, iRow)
|
||||
copy(iRow, jRow)
|
||||
|
@ -167,28 +165,28 @@ func verticalFlip(nrgba *image.NRGBA) {
|
|||
//
|
||||
// The resulting PictureData's Bounds will be the equivalent of the supplied image.Image's Bounds.
|
||||
func PictureDataFromImage(img image.Image) *PictureData {
|
||||
var nrgba *image.NRGBA
|
||||
if nrgbaImg, ok := img.(*image.NRGBA); ok {
|
||||
nrgba = nrgbaImg
|
||||
var rgba *image.RGBA
|
||||
if rgbaImg, ok := img.(*image.RGBA); ok {
|
||||
rgba = rgbaImg
|
||||
} else {
|
||||
nrgba = image.NewNRGBA(img.Bounds())
|
||||
draw.Draw(nrgba, nrgba.Bounds(), img, img.Bounds().Min, draw.Src)
|
||||
rgba = image.NewRGBA(img.Bounds())
|
||||
draw.Draw(rgba, rgba.Bounds(), img, img.Bounds().Min, draw.Src)
|
||||
}
|
||||
|
||||
verticalFlip(nrgba)
|
||||
verticalFlip(rgba)
|
||||
|
||||
pd := MakePictureData(R(
|
||||
float64(nrgba.Bounds().Min.X),
|
||||
float64(nrgba.Bounds().Min.Y),
|
||||
float64(nrgba.Bounds().Dx()),
|
||||
float64(nrgba.Bounds().Dy()),
|
||||
float64(rgba.Bounds().Min.X),
|
||||
float64(rgba.Bounds().Min.Y),
|
||||
float64(rgba.Bounds().Max.X),
|
||||
float64(rgba.Bounds().Max.Y),
|
||||
))
|
||||
|
||||
for i := range pd.Pix {
|
||||
pd.Pix[i].R = nrgba.Pix[i*4+0]
|
||||
pd.Pix[i].G = nrgba.Pix[i*4+1]
|
||||
pd.Pix[i].B = nrgba.Pix[i*4+2]
|
||||
pd.Pix[i].A = nrgba.Pix[i*4+3]
|
||||
pd.Pix[i].R = rgba.Pix[i*4+0]
|
||||
pd.Pix[i].G = rgba.Pix[i*4+1]
|
||||
pd.Pix[i].B = rgba.Pix[i*4+2]
|
||||
pd.Pix[i].A = rgba.Pix[i*4+3]
|
||||
}
|
||||
|
||||
return pd
|
||||
|
@ -207,14 +205,20 @@ func PictureDataFromPicture(pic Picture) *PictureData {
|
|||
pd := MakePictureData(bounds)
|
||||
|
||||
if pic, ok := pic.(PictureColor); ok {
|
||||
for y := math.Floor(bounds.Min.Y()); y < bounds.Max.Y(); y++ {
|
||||
for x := math.Floor(bounds.Min.X()); x < bounds.Max.X(); x++ {
|
||||
for y := math.Floor(bounds.Min.Y); y < bounds.Max.Y; y++ {
|
||||
for x := math.Floor(bounds.Min.X); x < bounds.Max.X; x++ {
|
||||
// this together with the Floor is a trick to get all of the pixels
|
||||
at := V(
|
||||
math.Max(x, bounds.Min.X()),
|
||||
math.Max(y, bounds.Min.Y()),
|
||||
math.Max(x, bounds.Min.X),
|
||||
math.Max(y, bounds.Min.Y),
|
||||
)
|
||||
pd.SetColor(at, pic.Color(at))
|
||||
col := pic.Color(at)
|
||||
pd.Pix[pd.Index(at)] = color.RGBA{
|
||||
R: uint8(col.R * 255),
|
||||
G: uint8(col.G * 255),
|
||||
B: uint8(col.B * 255),
|
||||
A: uint8(col.A * 255),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -222,39 +226,39 @@ func PictureDataFromPicture(pic Picture) *PictureData {
|
|||
return pd
|
||||
}
|
||||
|
||||
// Image converts PictureData into an image.NRGBA.
|
||||
// Image converts PictureData into an image.RGBA.
|
||||
//
|
||||
// The resulting image.NRGBA's Bounds will be equivalent of the PictureData's Bounds.
|
||||
func (pd *PictureData) Image() *image.NRGBA {
|
||||
// The resulting image.RGBA's Bounds will be equivalent of the PictureData's Bounds.
|
||||
func (pd *PictureData) Image() *image.RGBA {
|
||||
bounds := image.Rect(
|
||||
int(math.Floor(pd.Rect.Min.X())),
|
||||
int(math.Floor(pd.Rect.Min.Y())),
|
||||
int(math.Ceil(pd.Rect.Max.X())),
|
||||
int(math.Ceil(pd.Rect.Max.Y())),
|
||||
int(math.Floor(pd.Rect.Min.X)),
|
||||
int(math.Floor(pd.Rect.Min.Y)),
|
||||
int(math.Ceil(pd.Rect.Max.X)),
|
||||
int(math.Ceil(pd.Rect.Max.Y)),
|
||||
)
|
||||
nrgba := image.NewNRGBA(bounds)
|
||||
rgba := image.NewRGBA(bounds)
|
||||
|
||||
i := 0
|
||||
for y := bounds.Min.Y; y < bounds.Max.Y; y++ {
|
||||
for x := bounds.Min.X; x < bounds.Max.X; x++ {
|
||||
off := pd.Index(V(float64(x), float64(y)))
|
||||
nrgba.Pix[i*4+0] = pd.Pix[off].R
|
||||
nrgba.Pix[i*4+1] = pd.Pix[off].G
|
||||
nrgba.Pix[i*4+2] = pd.Pix[off].B
|
||||
nrgba.Pix[i*4+3] = pd.Pix[off].A
|
||||
rgba.Pix[i*4+0] = pd.Pix[off].R
|
||||
rgba.Pix[i*4+1] = pd.Pix[off].G
|
||||
rgba.Pix[i*4+2] = pd.Pix[off].B
|
||||
rgba.Pix[i*4+3] = pd.Pix[off].A
|
||||
i++
|
||||
}
|
||||
}
|
||||
|
||||
verticalFlip(nrgba)
|
||||
verticalFlip(rgba)
|
||||
|
||||
return nrgba
|
||||
return rgba
|
||||
}
|
||||
|
||||
// Index returns the index of the pixel at the specified position inside the Pix slice.
|
||||
func (pd *PictureData) Index(at Vec) int {
|
||||
at -= pd.Rect.Min.Map(math.Floor)
|
||||
x, y := int(at.X()), int(at.Y())
|
||||
at = at.Sub(pd.Rect.Min.Map(math.Floor))
|
||||
x, y := int(at.X), int(at.Y)
|
||||
return y*pd.Stride + x
|
||||
}
|
||||
|
||||
|
@ -263,40 +267,10 @@ func (pd *PictureData) Bounds() Rect {
|
|||
return pd.Rect
|
||||
}
|
||||
|
||||
// Slice returns a sub-Picture of this PictureData inside the supplied rectangle.
|
||||
func (pd *PictureData) Slice(r Rect) Picture {
|
||||
return &PictureData{
|
||||
Pix: pd.Pix[pd.Index(r.Min):],
|
||||
Stride: pd.Stride,
|
||||
Rect: r,
|
||||
Orig: pd.Orig,
|
||||
}
|
||||
}
|
||||
|
||||
// Original returns the most original PictureData that this PictureData was obtained from using
|
||||
// Slice-ing.
|
||||
func (pd *PictureData) Original() Picture {
|
||||
return pd.Orig
|
||||
}
|
||||
|
||||
// Color returns the color located at the given position.
|
||||
func (pd *PictureData) Color(at Vec) NRGBA {
|
||||
func (pd *PictureData) Color(at Vec) RGBA {
|
||||
if !pd.Rect.Contains(at) {
|
||||
return NRGBA{0, 0, 0, 0}
|
||||
}
|
||||
return ToNRGBA(pd.Pix[pd.Index(at)])
|
||||
}
|
||||
|
||||
// SetColor changes the color located at the given position.
|
||||
func (pd *PictureData) SetColor(at Vec, col color.Color) {
|
||||
if !pd.Rect.Contains(at) {
|
||||
return
|
||||
}
|
||||
nrgba := ToNRGBA(col)
|
||||
pd.Pix[pd.Index(at)] = color.NRGBA{
|
||||
R: uint8(nrgba.R * 255),
|
||||
G: uint8(nrgba.G * 255),
|
||||
B: uint8(nrgba.B * 255),
|
||||
A: uint8(nrgba.A * 255),
|
||||
return RGBA{0, 0, 0, 0}
|
||||
}
|
||||
return ToRGBA(pd.Pix[pd.Index(at)])
|
||||
}
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
// Package pixel implements platform and backend agnostic core of the Pixel game development
|
||||
// library.
|
||||
//
|
||||
// It specifies the core Target, Triangles, Picture pattern and implements standard elements, such
|
||||
// as Sprite, Batch, Vec, Matrix and RGBA in addition to the basic Triangles and Picture
|
||||
// implementations: TrianglesData and PictureData.
|
||||
package pixel
|
26
drawer.go
|
@ -14,7 +14,12 @@ package pixel
|
|||
// Picture.
|
||||
//
|
||||
// Whenever you change the Triangles, call Dirty to notify Drawer that Triangles changed. You don't
|
||||
// need to notify Drawer about a change of Picture.
|
||||
// need to notify Drawer about a change of the Picture.
|
||||
//
|
||||
// Note, that Drawer caches the results of MakePicture from Targets it's drawn to for each Picture
|
||||
// it's set to. What it means is that using a Drawer with an unbounded number of Pictures leads to a
|
||||
// memory leak, since Drawer caches them and never forgets. In such a situation, create a new Drawer
|
||||
// for each Picture.
|
||||
type Drawer struct {
|
||||
Triangles Triangles
|
||||
Picture Picture
|
||||
|
@ -22,6 +27,7 @@ type Drawer struct {
|
|||
tris map[Target]TargetTriangles
|
||||
clean map[Target]bool
|
||||
pics map[targetPicturePair]TargetPicture
|
||||
dirty bool
|
||||
inited bool
|
||||
}
|
||||
|
||||
|
@ -44,9 +50,7 @@ func (d *Drawer) lazyInit() {
|
|||
func (d *Drawer) Dirty() {
|
||||
d.lazyInit()
|
||||
|
||||
for t := range d.clean {
|
||||
d.clean[t] = false
|
||||
}
|
||||
d.dirty = true
|
||||
}
|
||||
|
||||
// Draw efficiently draws Triangles with Picture onto the provided Target.
|
||||
|
@ -56,6 +60,13 @@ func (d *Drawer) Dirty() {
|
|||
func (d *Drawer) Draw(t Target) {
|
||||
d.lazyInit()
|
||||
|
||||
if d.dirty {
|
||||
for t := range d.clean {
|
||||
d.clean[t] = false
|
||||
}
|
||||
d.dirty = false
|
||||
}
|
||||
|
||||
if d.Triangles == nil {
|
||||
return
|
||||
}
|
||||
|
@ -78,12 +89,11 @@ func (d *Drawer) Draw(t Target) {
|
|||
return
|
||||
}
|
||||
|
||||
pic := d.pics[targetPicturePair{t, d.Picture.Original()}]
|
||||
pic := d.pics[targetPicturePair{t, d.Picture}]
|
||||
if pic == nil {
|
||||
pic = t.MakePicture(d.Picture.Original())
|
||||
d.pics[targetPicturePair{t, d.Picture.Original()}] = pic
|
||||
pic = t.MakePicture(d.Picture)
|
||||
d.pics[targetPicturePair{t, d.Picture}] = pic
|
||||
}
|
||||
pic = pic.Slice(d.Picture.Bounds()).(TargetPicture)
|
||||
|
||||
pic.Draw(tri)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2017 stephen
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
|
@ -0,0 +1,19 @@
|
|||
# Maze generator in Go
|
||||
|
||||
Created by [Stephen Chavez](https://github.com/redragonx)
|
||||
|
||||
This uses the game engine: Pixel. Install it here: https://github.com/faiface/pixel
|
||||
|
||||
I made this to improve my understanding of Go and some game concepts with some basic maze generating algorithms.
|
||||
|
||||
Controls: Press 'R' to restart the maze.
|
||||
|
||||
Optional command-line arguments: `go run ./maze-generator.go`
|
||||
- `-w` sets the maze's width in pixels.
|
||||
- `-h` sets the maze's height in pixels.
|
||||
- `-c` sets the maze cell's size in pixels.
|
||||
|
||||
Code based on the Recursive backtracker algorithm.
|
||||
- https://en.wikipedia.org/wiki/Maze_generation_algorithm#Recursive_backtracker
|
||||
|
||||

|
|
@ -0,0 +1,317 @@
|
|||
package main
|
||||
|
||||
// Code based on the Recursive backtracker algorithm.
|
||||
// https://en.wikipedia.org/wiki/Maze_generation_algorithm#Recursive_backtracker
|
||||
// See https://youtu.be/HyK_Q5rrcr4 as an example
|
||||
// YouTube example ported to Go for the Pixel library.
|
||||
|
||||
// Created by Stephen Chavez
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"time"
|
||||
|
||||
"github.com/faiface/pixel"
|
||||
"github.com/faiface/pixel/examples/community/maze/stack"
|
||||
"github.com/faiface/pixel/imdraw"
|
||||
"github.com/faiface/pixel/pixelgl"
|
||||
|
||||
"github.com/pkg/profile"
|
||||
"golang.org/x/image/colornames"
|
||||
)
|
||||
|
||||
var visitedColor = pixel.RGB(0.5, 0, 1).Mul(pixel.Alpha(0.35))
|
||||
var hightlightColor = pixel.RGB(0.3, 0, 0).Mul(pixel.Alpha(0.45))
|
||||
var debug = false
|
||||
|
||||
type cell struct {
|
||||
walls [4]bool // Wall order: top, right, bottom, left
|
||||
|
||||
row int
|
||||
col int
|
||||
visited bool
|
||||
}
|
||||
|
||||
func (c *cell) Draw(imd *imdraw.IMDraw, wallSize int) {
|
||||
drawCol := c.col * wallSize // x
|
||||
drawRow := c.row * wallSize // y
|
||||
|
||||
imd.Color = colornames.White
|
||||
if c.walls[0] {
|
||||
// top line
|
||||
imd.Push(pixel.V(float64(drawCol), float64(drawRow)), pixel.V(float64(drawCol+wallSize), float64(drawRow)))
|
||||
imd.Line(3)
|
||||
}
|
||||
if c.walls[1] {
|
||||
// right Line
|
||||
imd.Push(pixel.V(float64(drawCol+wallSize), float64(drawRow)), pixel.V(float64(drawCol+wallSize), float64(drawRow+wallSize)))
|
||||
imd.Line(3)
|
||||
}
|
||||
if c.walls[2] {
|
||||
// bottom line
|
||||
imd.Push(pixel.V(float64(drawCol+wallSize), float64(drawRow+wallSize)), pixel.V(float64(drawCol), float64(drawRow+wallSize)))
|
||||
imd.Line(3)
|
||||
}
|
||||
if c.walls[3] {
|
||||
// left line
|
||||
imd.Push(pixel.V(float64(drawCol), float64(drawRow+wallSize)), pixel.V(float64(drawCol), float64(drawRow)))
|
||||
imd.Line(3)
|
||||
}
|
||||
imd.EndShape = imdraw.SharpEndShape
|
||||
|
||||
if c.visited {
|
||||
imd.Color = visitedColor
|
||||
imd.Push(pixel.V(float64(drawCol), (float64(drawRow))), pixel.V(float64(drawCol+wallSize), float64(drawRow+wallSize)))
|
||||
imd.Rectangle(0)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *cell) GetNeighbors(grid []*cell, cols int, rows int) ([]*cell, error) {
|
||||
neighbors := []*cell{}
|
||||
j := c.row
|
||||
i := c.col
|
||||
|
||||
top, _ := getCellAt(i, j-1, cols, rows, grid)
|
||||
right, _ := getCellAt(i+1, j, cols, rows, grid)
|
||||
bottom, _ := getCellAt(i, j+1, cols, rows, grid)
|
||||
left, _ := getCellAt(i-1, j, cols, rows, grid)
|
||||
|
||||
if top != nil && !top.visited {
|
||||
neighbors = append(neighbors, top)
|
||||
}
|
||||
if right != nil && !right.visited {
|
||||
neighbors = append(neighbors, right)
|
||||
}
|
||||
if bottom != nil && !bottom.visited {
|
||||
neighbors = append(neighbors, bottom)
|
||||
}
|
||||
if left != nil && !left.visited {
|
||||
neighbors = append(neighbors, left)
|
||||
}
|
||||
|
||||
if len(neighbors) == 0 {
|
||||
return nil, errors.New("We checked all cells...")
|
||||
}
|
||||
return neighbors, nil
|
||||
}
|
||||
|
||||
func (c *cell) GetRandomNeighbor(grid []*cell, cols int, rows int) (*cell, error) {
|
||||
neighbors, err := c.GetNeighbors(grid, cols, rows)
|
||||
if neighbors == nil {
|
||||
return nil, err
|
||||
}
|
||||
nBig, err := rand.Int(rand.Reader, big.NewInt(int64(len(neighbors))))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
randomIndex := nBig.Int64()
|
||||
return neighbors[randomIndex], nil
|
||||
}
|
||||
|
||||
func (c *cell) hightlight(imd *imdraw.IMDraw, wallSize int) {
|
||||
x := c.col * wallSize
|
||||
y := c.row * wallSize
|
||||
|
||||
imd.Color = hightlightColor
|
||||
imd.Push(pixel.V(float64(x), float64(y)), pixel.V(float64(x+wallSize), float64(y+wallSize)))
|
||||
imd.Rectangle(0)
|
||||
}
|
||||
|
||||
func newCell(col int, row int) *cell {
|
||||
newCell := new(cell)
|
||||
newCell.row = row
|
||||
newCell.col = col
|
||||
|
||||
for i := range newCell.walls {
|
||||
newCell.walls[i] = true
|
||||
}
|
||||
return newCell
|
||||
}
|
||||
|
||||
// Creates the inital maze slice for use.
|
||||
func initGrid(cols, rows int) []*cell {
|
||||
grid := []*cell{}
|
||||
for j := 0; j < rows; j++ {
|
||||
for i := 0; i < cols; i++ {
|
||||
newCell := newCell(i, j)
|
||||
grid = append(grid, newCell)
|
||||
}
|
||||
}
|
||||
return grid
|
||||
}
|
||||
|
||||
func setupMaze(cols, rows int) ([]*cell, *stack.Stack, *cell) {
|
||||
// Make an empty grid
|
||||
grid := initGrid(cols, rows)
|
||||
backTrackStack := stack.NewStack(len(grid))
|
||||
currentCell := grid[0]
|
||||
|
||||
return grid, backTrackStack, currentCell
|
||||
}
|
||||
|
||||
func cellIndex(i, j, cols, rows int) int {
|
||||
if i < 0 || j < 0 || i > cols-1 || j > rows-1 {
|
||||
return -1
|
||||
}
|
||||
return i + j*cols
|
||||
}
|
||||
|
||||
func getCellAt(i int, j int, cols int, rows int, grid []*cell) (*cell, error) {
|
||||
possibleIndex := cellIndex(i, j, cols, rows)
|
||||
|
||||
if possibleIndex == -1 {
|
||||
return nil, fmt.Errorf("cellIndex: CellIndex is a negative number %d", possibleIndex)
|
||||
}
|
||||
return grid[possibleIndex], nil
|
||||
}
|
||||
|
||||
func removeWalls(a *cell, b *cell) {
|
||||
x := a.col - b.col
|
||||
|
||||
if x == 1 {
|
||||
a.walls[3] = false
|
||||
b.walls[1] = false
|
||||
} else if x == -1 {
|
||||
a.walls[1] = false
|
||||
b.walls[3] = false
|
||||
}
|
||||
|
||||
y := a.row - b.row
|
||||
|
||||
if y == 1 {
|
||||
a.walls[0] = false
|
||||
b.walls[2] = false
|
||||
} else if y == -1 {
|
||||
a.walls[2] = false
|
||||
b.walls[0] = false
|
||||
}
|
||||
}
|
||||
|
||||
func run() {
|
||||
// unsiged integers, because easier parsing error checks.
|
||||
// We must convert these to intergers, as done below...
|
||||
uScreenWidth, uScreenHeight, uWallSize := parseArgs()
|
||||
|
||||
var (
|
||||
// In pixels
|
||||
// Defualt is 800x800x40 = 20x20 wallgrid
|
||||
screenWidth = int(uScreenWidth)
|
||||
screenHeight = int(uScreenHeight)
|
||||
wallSize = int(uWallSize)
|
||||
|
||||
frames = 0
|
||||
second = time.Tick(time.Second)
|
||||
|
||||
grid = []*cell{}
|
||||
cols = screenWidth / wallSize
|
||||
rows = screenHeight / wallSize
|
||||
currentCell = new(cell)
|
||||
backTrackStack = stack.NewStack(1)
|
||||
)
|
||||
|
||||
// Set game FPS manually
|
||||
fps := time.Tick(time.Second / 60)
|
||||
|
||||
cfg := pixelgl.WindowConfig{
|
||||
Title: "Pixel Rocks! - Maze example",
|
||||
Bounds: pixel.R(0, 0, float64(screenHeight), float64(screenWidth)),
|
||||
}
|
||||
|
||||
win, err := pixelgl.NewWindow(cfg)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
grid, backTrackStack, currentCell = setupMaze(cols, rows)
|
||||
|
||||
gridIMDraw := imdraw.New(nil)
|
||||
|
||||
for !win.Closed() {
|
||||
if win.JustReleased(pixelgl.KeyR) {
|
||||
fmt.Println("R pressed")
|
||||
grid, backTrackStack, currentCell = setupMaze(cols, rows)
|
||||
}
|
||||
|
||||
win.Clear(colornames.Gray)
|
||||
gridIMDraw.Clear()
|
||||
|
||||
for i := range grid {
|
||||
grid[i].Draw(gridIMDraw, wallSize)
|
||||
}
|
||||
|
||||
// step 1
|
||||
// Make the initial cell the current cell and mark it as visited
|
||||
currentCell.visited = true
|
||||
currentCell.hightlight(gridIMDraw, wallSize)
|
||||
|
||||
// step 2.1
|
||||
// If the current cell has any neighbours which have not been visited
|
||||
// Choose a random unvisited cell
|
||||
nextCell, _ := currentCell.GetRandomNeighbor(grid, cols, rows)
|
||||
if nextCell != nil && !nextCell.visited {
|
||||
// step 2.2
|
||||
// Push the current cell to the stack
|
||||
backTrackStack.Push(currentCell)
|
||||
|
||||
// step 2.3
|
||||
// Remove the wall between the current cell and the chosen cell
|
||||
|
||||
removeWalls(currentCell, nextCell)
|
||||
|
||||
// step 2.4
|
||||
// Make the chosen cell the current cell and mark it as visited
|
||||
nextCell.visited = true
|
||||
currentCell = nextCell
|
||||
} else if backTrackStack.Len() > 0 {
|
||||
currentCell = backTrackStack.Pop().(*cell)
|
||||
}
|
||||
|
||||
gridIMDraw.Draw(win)
|
||||
win.Update()
|
||||
<-fps
|
||||
updateFPSDisplay(win, &cfg, &frames, grid, second)
|
||||
}
|
||||
}
|
||||
|
||||
// Parses the maze arguments, all of them are optional.
|
||||
// Uses uint as implicit error checking :)
|
||||
func parseArgs() (uint, uint, uint) {
|
||||
var mazeWidthPtr = flag.Uint("w", 800, "w sets the maze's width in pixels.")
|
||||
var mazeHeightPtr = flag.Uint("h", 800, "h sets the maze's height in pixels.")
|
||||
var wallSizePtr = flag.Uint("c", 40, "c sets the maze cell's size in pixels.")
|
||||
|
||||
flag.Parse()
|
||||
|
||||
// If these aren't default values AND if they're not the same values.
|
||||
// We should warn the user that the maze will look funny.
|
||||
if *mazeWidthPtr != 800 || *mazeHeightPtr != 800 {
|
||||
if *mazeWidthPtr != *mazeHeightPtr {
|
||||
fmt.Printf("WARNING: maze width: %d and maze height: %d don't match. \n", *mazeWidthPtr, *mazeHeightPtr)
|
||||
fmt.Println("Maze will look funny because the maze size is bond to the window size!")
|
||||
}
|
||||
}
|
||||
|
||||
return *mazeWidthPtr, *mazeHeightPtr, *wallSizePtr
|
||||
}
|
||||
|
||||
func updateFPSDisplay(win *pixelgl.Window, cfg *pixelgl.WindowConfig, frames *int, grid []*cell, second <-chan time.Time) {
|
||||
*frames++
|
||||
select {
|
||||
case <-second:
|
||||
win.SetTitle(fmt.Sprintf("%s | FPS: %d with %d Cells", cfg.Title, *frames, len(grid)))
|
||||
*frames = 0
|
||||
default:
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func main() {
|
||||
if debug {
|
||||
defer profile.Start().Stop()
|
||||
}
|
||||
pixelgl.Run(run)
|
||||
}
|
After Width: | Height: | Size: 14 KiB |
|
@ -0,0 +1,86 @@
|
|||
package stack
|
||||
|
||||
type Stack struct {
|
||||
top *Element
|
||||
size int
|
||||
max int
|
||||
}
|
||||
|
||||
type Element struct {
|
||||
value interface{}
|
||||
next *Element
|
||||
}
|
||||
|
||||
func NewStack(max int) *Stack {
|
||||
return &Stack{max: max}
|
||||
}
|
||||
|
||||
// Return the stack's length
|
||||
func (s *Stack) Len() int {
|
||||
return s.size
|
||||
}
|
||||
|
||||
// Return the stack's max
|
||||
func (s *Stack) Max() int {
|
||||
return s.max
|
||||
}
|
||||
|
||||
// Push a new element onto the stack
|
||||
func (s *Stack) Push(value interface{}) {
|
||||
if s.size+1 > s.max {
|
||||
if last := s.PopLast(); last == nil {
|
||||
panic("Unexpected nil in stack")
|
||||
}
|
||||
}
|
||||
s.top = &Element{value, s.top}
|
||||
s.size++
|
||||
}
|
||||
|
||||
// Remove the top element from the stack and return it's value
|
||||
// If the stack is empty, return nil
|
||||
func (s *Stack) Pop() (value interface{}) {
|
||||
if s.size > 0 {
|
||||
value, s.top = s.top.value, s.top.next
|
||||
s.size--
|
||||
return
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Stack) PopLast() (value interface{}) {
|
||||
if lastElem := s.popLast(s.top); lastElem != nil {
|
||||
return lastElem.value
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
//Peek returns a top without removing it from list
|
||||
func (s *Stack) Peek() (value interface{}, exists bool) {
|
||||
exists = false
|
||||
if s.size > 0 {
|
||||
value = s.top.value
|
||||
exists = true
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (s *Stack) popLast(elem *Element) *Element {
|
||||
if elem == nil {
|
||||
return nil
|
||||
}
|
||||
// not last because it has next and a grandchild
|
||||
if elem.next != nil && elem.next.next != nil {
|
||||
return s.popLast(elem.next)
|
||||
}
|
||||
|
||||
// current elem is second from bottom, as next elem has no child
|
||||
if elem.next != nil && elem.next.next == nil {
|
||||
last := elem.next
|
||||
// make current elem bottom of stack by removing its next element
|
||||
elem.next = nil
|
||||
s.size--
|
||||
return last
|
||||
}
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"github.com/faiface/pixel"
|
||||
"github.com/faiface/pixel/pixelgl"
|
||||
"golang.org/x/image/colornames"
|
||||
)
|
||||
|
||||
func run() {
|
||||
cfg := pixelgl.WindowConfig{
|
||||
Title: "Pixel Rocks!",
|
||||
Bounds: pixel.R(0, 0, 1024, 768),
|
||||
VSync: true,
|
||||
}
|
||||
win, err := pixelgl.NewWindow(cfg)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
win.Clear(colornames.Skyblue)
|
||||
|
||||
for !win.Closed() {
|
||||
win.Update()
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
pixelgl.Run(run)
|
||||
}
|
After Width: | Height: | Size: 68 KiB |
|
@ -0,0 +1,56 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"image"
|
||||
"os"
|
||||
|
||||
_ "image/png"
|
||||
|
||||
"github.com/faiface/pixel"
|
||||
"github.com/faiface/pixel/pixelgl"
|
||||
"golang.org/x/image/colornames"
|
||||
)
|
||||
|
||||
func loadPicture(path string) (pixel.Picture, error) {
|
||||
file, err := os.Open(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer file.Close()
|
||||
img, _, err := image.Decode(file)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return pixel.PictureDataFromImage(img), nil
|
||||
}
|
||||
|
||||
func run() {
|
||||
cfg := pixelgl.WindowConfig{
|
||||
Title: "Pixel Rocks!",
|
||||
Bounds: pixel.R(0, 0, 1024, 768),
|
||||
VSync: true,
|
||||
}
|
||||
win, err := pixelgl.NewWindow(cfg)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
pic, err := loadPicture("hiking.png")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
sprite := pixel.NewSprite(pic, pic.Bounds())
|
||||
|
||||
win.Clear(colornames.Greenyellow)
|
||||
|
||||
sprite.Draw(win, pixel.IM.Moved(win.Bounds().Center()))
|
||||
|
||||
for !win.Closed() {
|
||||
win.Update()
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
pixelgl.Run(run)
|
||||
}
|
After Width: | Height: | Size: 68 KiB |
|
@ -0,0 +1,70 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"image"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
_ "image/png"
|
||||
|
||||
"github.com/faiface/pixel"
|
||||
"github.com/faiface/pixel/pixelgl"
|
||||
"golang.org/x/image/colornames"
|
||||
)
|
||||
|
||||
func loadPicture(path string) (pixel.Picture, error) {
|
||||
file, err := os.Open(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer file.Close()
|
||||
img, _, err := image.Decode(file)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return pixel.PictureDataFromImage(img), nil
|
||||
}
|
||||
|
||||
func run() {
|
||||
cfg := pixelgl.WindowConfig{
|
||||
Title: "Pixel Rocks!",
|
||||
Bounds: pixel.R(0, 0, 1024, 768),
|
||||
VSync: true,
|
||||
}
|
||||
win, err := pixelgl.NewWindow(cfg)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
win.SetSmooth(true)
|
||||
|
||||
pic, err := loadPicture("hiking.png")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
sprite := pixel.NewSprite(pic, pic.Bounds())
|
||||
|
||||
angle := 0.0
|
||||
|
||||
last := time.Now()
|
||||
for !win.Closed() {
|
||||
dt := time.Since(last).Seconds()
|
||||
last = time.Now()
|
||||
|
||||
angle += 3 * dt
|
||||
|
||||
win.Clear(colornames.Firebrick)
|
||||
|
||||
mat := pixel.IM
|
||||
mat = mat.Rotated(pixel.ZV, angle)
|
||||
mat = mat.Moved(win.Bounds().Center())
|
||||
sprite.Draw(win, mat)
|
||||
|
||||
win.Update()
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
pixelgl.Run(run)
|
||||
}
|
|
@ -0,0 +1,102 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"image"
|
||||
"math"
|
||||
"math/rand"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
_ "image/png"
|
||||
|
||||
"github.com/faiface/pixel"
|
||||
"github.com/faiface/pixel/pixelgl"
|
||||
"golang.org/x/image/colornames"
|
||||
)
|
||||
|
||||
func loadPicture(path string) (pixel.Picture, error) {
|
||||
file, err := os.Open(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer file.Close()
|
||||
img, _, err := image.Decode(file)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return pixel.PictureDataFromImage(img), nil
|
||||
}
|
||||
|
||||
func run() {
|
||||
cfg := pixelgl.WindowConfig{
|
||||
Title: "Pixel Rocks!",
|
||||
Bounds: pixel.R(0, 0, 1024, 768),
|
||||
VSync: true,
|
||||
}
|
||||
win, err := pixelgl.NewWindow(cfg)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
spritesheet, err := loadPicture("trees.png")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
var treesFrames []pixel.Rect
|
||||
for x := spritesheet.Bounds().Min.X; x < spritesheet.Bounds().Max.X; x += 32 {
|
||||
for y := spritesheet.Bounds().Min.Y; y < spritesheet.Bounds().Max.Y; y += 32 {
|
||||
treesFrames = append(treesFrames, pixel.R(x, y, x+32, y+32))
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
camPos = pixel.ZV
|
||||
camSpeed = 500.0
|
||||
camZoom = 1.0
|
||||
camZoomSpeed = 1.2
|
||||
trees []*pixel.Sprite
|
||||
matrices []pixel.Matrix
|
||||
)
|
||||
|
||||
last := time.Now()
|
||||
for !win.Closed() {
|
||||
dt := time.Since(last).Seconds()
|
||||
last = time.Now()
|
||||
|
||||
cam := pixel.IM.Scaled(camPos, camZoom).Moved(win.Bounds().Center().Sub(camPos))
|
||||
win.SetMatrix(cam)
|
||||
|
||||
if win.JustPressed(pixelgl.MouseButtonLeft) {
|
||||
tree := pixel.NewSprite(spritesheet, treesFrames[rand.Intn(len(treesFrames))])
|
||||
trees = append(trees, tree)
|
||||
mouse := cam.Unproject(win.MousePosition())
|
||||
matrices = append(matrices, pixel.IM.Scaled(pixel.ZV, 4).Moved(mouse))
|
||||
}
|
||||
if win.Pressed(pixelgl.KeyLeft) {
|
||||
camPos.X -= camSpeed * dt
|
||||
}
|
||||
if win.Pressed(pixelgl.KeyRight) {
|
||||
camPos.X += camSpeed * dt
|
||||
}
|
||||
if win.Pressed(pixelgl.KeyDown) {
|
||||
camPos.Y -= camSpeed * dt
|
||||
}
|
||||
if win.Pressed(pixelgl.KeyUp) {
|
||||
camPos.Y += camSpeed * dt
|
||||
}
|
||||
camZoom *= math.Pow(camZoomSpeed, win.MouseScroll().Y)
|
||||
|
||||
win.Clear(colornames.Forestgreen)
|
||||
|
||||
for i, tree := range trees {
|
||||
tree.Draw(win, matrices[i])
|
||||
}
|
||||
|
||||
win.Update()
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
pixelgl.Run(run)
|
||||
}
|
After Width: | Height: | Size: 2.5 KiB |
|
@ -0,0 +1,110 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"image"
|
||||
"math"
|
||||
"math/rand"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
_ "image/png"
|
||||
|
||||
"github.com/faiface/pixel"
|
||||
"github.com/faiface/pixel/pixelgl"
|
||||
"golang.org/x/image/colornames"
|
||||
)
|
||||
|
||||
func loadPicture(path string) (pixel.Picture, error) {
|
||||
file, err := os.Open(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer file.Close()
|
||||
img, _, err := image.Decode(file)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return pixel.PictureDataFromImage(img), nil
|
||||
}
|
||||
|
||||
func run() {
|
||||
cfg := pixelgl.WindowConfig{
|
||||
Title: "Pixel Rocks!",
|
||||
Bounds: pixel.R(0, 0, 1024, 768),
|
||||
}
|
||||
win, err := pixelgl.NewWindow(cfg)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
spritesheet, err := loadPicture("trees.png")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
batch := pixel.NewBatch(&pixel.TrianglesData{}, spritesheet)
|
||||
|
||||
var treesFrames []pixel.Rect
|
||||
for x := spritesheet.Bounds().Min.X; x < spritesheet.Bounds().Max.X; x += 32 {
|
||||
for y := spritesheet.Bounds().Min.Y; y < spritesheet.Bounds().Max.Y; y += 32 {
|
||||
treesFrames = append(treesFrames, pixel.R(x, y, x+32, y+32))
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
camPos = pixel.ZV
|
||||
camSpeed = 500.0
|
||||
camZoom = 1.0
|
||||
camZoomSpeed = 1.2
|
||||
)
|
||||
|
||||
var (
|
||||
frames = 0
|
||||
second = time.Tick(time.Second)
|
||||
)
|
||||
|
||||
last := time.Now()
|
||||
for !win.Closed() {
|
||||
dt := time.Since(last).Seconds()
|
||||
last = time.Now()
|
||||
|
||||
cam := pixel.IM.Scaled(camPos, camZoom).Moved(win.Bounds().Center().Sub(camPos))
|
||||
win.SetMatrix(cam)
|
||||
|
||||
if win.Pressed(pixelgl.MouseButtonLeft) {
|
||||
tree := pixel.NewSprite(spritesheet, treesFrames[rand.Intn(len(treesFrames))])
|
||||
mouse := cam.Unproject(win.MousePosition())
|
||||
tree.Draw(batch, pixel.IM.Scaled(pixel.ZV, 4).Moved(mouse))
|
||||
}
|
||||
if win.Pressed(pixelgl.KeyLeft) {
|
||||
camPos.X -= camSpeed * dt
|
||||
}
|
||||
if win.Pressed(pixelgl.KeyRight) {
|
||||
camPos.X += camSpeed * dt
|
||||
}
|
||||
if win.Pressed(pixelgl.KeyDown) {
|
||||
camPos.Y -= camSpeed * dt
|
||||
}
|
||||
if win.Pressed(pixelgl.KeyUp) {
|
||||
camPos.Y += camSpeed * dt
|
||||
}
|
||||
camZoom *= math.Pow(camZoomSpeed, win.MouseScroll().Y)
|
||||
|
||||
win.Clear(colornames.Forestgreen)
|
||||
batch.Draw(win)
|
||||
win.Update()
|
||||
|
||||
frames++
|
||||
select {
|
||||
case <-second:
|
||||
win.SetTitle(fmt.Sprintf("%s | FPS: %d", cfg.Title, frames))
|
||||
frames = 0
|
||||
default:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
pixelgl.Run(run)
|
||||
}
|
After Width: | Height: | Size: 2.5 KiB |
|
@ -0,0 +1,53 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"math"
|
||||
|
||||
"github.com/faiface/pixel"
|
||||
"github.com/faiface/pixel/imdraw"
|
||||
"github.com/faiface/pixel/pixelgl"
|
||||
"golang.org/x/image/colornames"
|
||||
)
|
||||
|
||||
func run() {
|
||||
cfg := pixelgl.WindowConfig{
|
||||
Title: "Pixel Rocks!",
|
||||
Bounds: pixel.R(0, 0, 1024, 768),
|
||||
VSync: true,
|
||||
}
|
||||
win, err := pixelgl.NewWindow(cfg)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
imd := imdraw.New(nil)
|
||||
|
||||
imd.Color = colornames.Blueviolet
|
||||
imd.EndShape = imdraw.RoundEndShape
|
||||
imd.Push(pixel.V(100, 100), pixel.V(700, 100))
|
||||
imd.EndShape = imdraw.SharpEndShape
|
||||
imd.Push(pixel.V(100, 500), pixel.V(700, 500))
|
||||
imd.Line(30)
|
||||
|
||||
imd.Color = colornames.Limegreen
|
||||
imd.Push(pixel.V(500, 500))
|
||||
imd.Circle(300, 50)
|
||||
imd.Color = colornames.Navy
|
||||
imd.Push(pixel.V(200, 500), pixel.V(800, 500))
|
||||
imd.Ellipse(pixel.V(120, 80), 0)
|
||||
|
||||
imd.Color = colornames.Red
|
||||
imd.EndShape = imdraw.RoundEndShape
|
||||
imd.Push(pixel.V(500, 350))
|
||||
imd.CircleArc(150, -math.Pi, 0, 30)
|
||||
|
||||
for !win.Closed() {
|
||||
win.Clear(colornames.Aliceblue)
|
||||
imd.Draw(win)
|
||||
win.Update()
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
pixelgl.Run(run)
|
||||
}
|
|
@ -0,0 +1,78 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/faiface/pixel"
|
||||
"github.com/faiface/pixel/pixelgl"
|
||||
"github.com/faiface/pixel/text"
|
||||
"github.com/golang/freetype/truetype"
|
||||
"golang.org/x/image/colornames"
|
||||
"golang.org/x/image/font"
|
||||
)
|
||||
|
||||
func loadTTF(path string, size float64) (font.Face, error) {
|
||||
file, err := os.Open(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
bytes, err := ioutil.ReadAll(file)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
font, err := truetype.Parse(bytes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return truetype.NewFace(font, &truetype.Options{
|
||||
Size: size,
|
||||
GlyphCacheEntries: 1,
|
||||
}), nil
|
||||
}
|
||||
|
||||
func run() {
|
||||
cfg := pixelgl.WindowConfig{
|
||||
Title: "Pixel Rocks!",
|
||||
Bounds: pixel.R(0, 0, 1024, 768),
|
||||
}
|
||||
win, err := pixelgl.NewWindow(cfg)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
win.SetSmooth(true)
|
||||
|
||||
face, err := loadTTF("intuitive.ttf", 80)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
atlas := text.NewAtlas(face, text.ASCII)
|
||||
txt := text.New(pixel.V(50, 500), atlas)
|
||||
|
||||
txt.Color = colornames.Lightgrey
|
||||
|
||||
fps := time.Tick(time.Second / 120)
|
||||
|
||||
for !win.Closed() {
|
||||
txt.WriteString(win.Typed())
|
||||
if win.JustPressed(pixelgl.KeyEnter) || win.Repeated(pixelgl.KeyEnter) {
|
||||
txt.WriteRune('\n')
|
||||
}
|
||||
|
||||
win.Clear(colornames.Darkcyan)
|
||||
txt.Draw(win, pixel.IM.Moved(win.Bounds().Center().Sub(txt.Bounds().Center())))
|
||||
win.Update()
|
||||
|
||||
<-fps
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
pixelgl.Run(run)
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
# Lights
|
||||
|
||||
This example demonstrates powerful Porter-Duff composition used to create a nice noisy light effect.
|
||||
|
||||
**Use W and S keys** to adjust the level of "dust".
|
||||
|
||||
The FPS is limited to 30, because the effect is a little expensive and my computer couldn't handle
|
||||
60 FPS. If you have a more powerful computer (which is quite likely), peek into the code and disable
|
||||
the limit.
|
||||
|
||||
Credit for the panda art goes to [Ján Štrba](https://www.artstation.com/artist/janstrba).
|
||||
|
||||

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

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

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

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

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