Compare commits

...

113 Commits

Author SHA1 Message Date
faiface bdbce3a27b change first sentence in readme 2017-04-24 22:51:43 +02:00
faiface 0389ee7bf9 minor change in readme 2017-04-24 21:00:01 +02:00
faiface 736f4549a9 add contributing section to readme 2017-04-24 20:55:18 +02:00
faiface ef9fd8fe10 minor change in readme 2017-04-24 20:39:18 +02:00
faiface 4246e90215 update example screenshots 2017-04-24 20:07:32 +02:00
faiface 1d0169b2b7 remove fps counters from examples for simplicity and consistency 2017-04-24 19:34:31 +02:00
faiface 2626892e97 adjust gitter badge location 2017-04-24 14:21:09 +02:00
Michal Štrba 1e2eb29a4f Merge pull request #2 from gitter-badger/gitter-badge
Add a Gitter chat badge to README.md
2017-04-24 14:20:12 +02:00
The Gitter Badger b60b7e5207 Add Gitter badge 2017-04-24 12:18:51 +00:00
faiface 8165c38c6d change wording in readme 2017-04-23 22:29:23 +02:00
faiface 82fa73952e put images into 2x2 table in readme 2017-04-23 20:57:36 +02:00
faiface bab33976cc add useful links to readme 2017-04-23 20:35:58 +02:00
faiface 0e54ed1080 add missing feature to readme (advanced window manipulation) 2017-04-23 17:48:54 +02:00
faiface db4f08aefc fix typo in readme 2017-04-23 17:30:03 +02:00
faiface fd6e056f92 add missing feature (tests and benchmarks) to readme 2017-04-23 14:35:56 +02:00
faiface e93b9a1d4c add missing feature (mobile/web backend) to readme 2017-04-23 14:32:41 +02:00
faiface d9a94fd157 update a few sentences in readme 2017-04-23 13:54:53 +02:00
faiface 6e5cbaa493 reorder features in readme 2017-04-23 01:30:15 +02:00
faiface 9e150a7a1b add link to "PixelGL backend" in readme 2017-04-22 23:58:13 +02:00
faiface 9c74e66118 reword a sentence in readme 2017-04-22 23:46:08 +02:00
faiface ee74866587 clarify readme 2017-04-22 23:44:17 +02:00
faiface a2ad3dbf7c fix grammar in readme 2017-04-22 23:36:54 +02:00
faiface af1ccf885f add feature to readme 2017-04-22 23:34:34 +02:00
faiface b756edfac6 reorder features in readme 2017-04-22 23:34:06 +02:00
faiface c69ef58898 add feature to readme 2017-04-22 23:33:35 +02:00
faiface 6e63f27d6e fix typo in readme 2017-04-22 23:29:54 +02:00
faiface acfd836a7f change tutorial part in readme 2017-04-22 23:29:05 +02:00
faiface 8679a702a9 add stunning readme (contributing part missing) 2017-04-22 23:27:54 +02:00
faiface e9380d7eed minor change in platformer example readme 2017-04-22 16:17:33 +02:00
faiface 3ed8c0016c minor change in platformer example readme 2017-04-22 15:56:56 +02:00
faiface 2afe44a9c9 add instructions to platformer readme 2017-04-22 14:22:09 +02:00
faiface c028b66b68 adjust camera movement in platformer example 2017-04-22 14:20:51 +02:00
faiface 080735510c temporarily fix issue #1 2017-04-22 13:15:57 +02:00
faiface 862f30a004 fix screenshot in platformer example readme 2017-04-22 13:11:26 +02:00
faiface c0ddb0b287 add platformer example 2017-04-22 13:09:23 +02:00
faiface 3e493c13e1 remove debug print 2017-04-21 23:10:02 +02:00
faiface f9f61911a7 fix Canvas drawing when bounds don't start at (0, 0) 2017-04-21 23:07:48 +02:00
faiface d30b73fb8b minor change in pixel package doc 2017-04-21 17:01:46 +02:00
faiface 4aea56198f add pixelgl package doc 2017-04-21 17:00:18 +02:00
faiface 591fadaaa5 minor change in pixel package doc 2017-04-21 16:57:36 +02:00
faiface 497df7842b add pixel doc 2017-04-21 16:57:08 +02:00
faiface a403cfe50b remove commented code 2017-04-21 16:43:56 +02:00
faiface bdbba8f3c1 add code for the guide 06 2017-04-21 01:42:50 +02:00
faiface c485793a83 fix drawing non-closed lines in IMDraw 2017-04-21 01:17:29 +02:00
faiface 97158ba502 simplify code in IMDraw 2017-04-16 00:59:07 +02:00
faiface 01ff4230da add IMDraw.Rectangle 2017-04-16 00:01:43 +02:00
faiface 37dcef6ab6 minor change 2017-04-15 21:03:22 +02:00
faiface ab9d608c45 fix typo in Alpha doc 2017-04-15 18:04:03 +02:00
faiface 05da8e6f92 minor change in xor example code 2017-04-15 17:16:57 +02:00
faiface 49c2beeca9 fix typo in xor example code 2017-04-15 17:15:12 +02:00
faiface f4ce166964 clarify xor example readme 2017-04-15 17:14:15 +02:00
faiface 93e9ab79b1 add xor example 2017-04-15 17:10:35 +02:00
faiface 58be215a76 update screenshot in smoke example 2017-04-15 16:05:29 +02:00
faiface ce083a9a9f add smoke example 2017-04-15 16:03:27 +02:00
faiface 2ef17e7a95 change window title + update screenshot in lights example 2017-04-15 14:25:38 +02:00
faiface 80f48bc0b6 minor change in lights example code 2017-04-15 14:21:27 +02:00
faiface 420e83bc61 update lights example code 2017-04-15 14:09:06 +02:00
faiface b51b3e8f38 revert previous commit 2017-04-14 22:46:46 +02:00
faiface 620784b34c fix window title in lights example 2017-04-14 22:45:44 +02:00
faiface 368dab0e7c fix screenshot in lights example readme 2017-04-14 22:41:14 +02:00
faiface 9bfb83861a add screenshot to lights example readme 2017-04-14 22:39:15 +02:00
faiface 4e30fb9f5c add lights screenshot 2017-04-14 22:37:41 +02:00
faiface 11c76944b6 minor change in light example readme 2017-04-14 22:29:03 +02:00
faiface 30af7c6612 add lights example readme 2017-04-14 22:28:30 +02:00
faiface 411ad7c27d add lights example 2017-04-14 22:20:12 +02:00
faiface 37b0f0956b add code for part 05 of the guide 2017-04-14 16:03:07 +02:00
faiface 10684b6add minor change 2017-04-13 20:30:32 +02:00
faiface cf666b5866 immediate-like-mode -> immediate-mode-like 2017-04-13 17:44:28 +02:00
faiface f325092c02 add imdraw package doc comment 2017-04-13 17:41:38 +02:00
faiface 939a76923d simplify code in Rect.Resized 2017-04-13 15:26:48 +02:00
faiface fcbc61c570 fix grammar in Vec doc 2017-04-13 15:18:13 +02:00
faiface e55d490801 fix grammar in Drawer doc 2017-04-13 15:15:17 +02:00
faiface b5a2c12175 remove a bunch of unnecessary Window control methods 2017-04-13 15:03:13 +02:00
faiface 14a01a5522 minor optimization in Sprite 2017-04-12 16:18:25 +02:00
faiface 3276c4e4a1 clarify IMDraw.Draw doc 2017-04-12 16:03:36 +02:00
faiface c61b677fa9 fix Canvas.Draw 2017-04-12 16:02:39 +02:00
faiface c9e0f7262d add Canvas.Draw 2017-04-12 16:00:56 +02:00
faiface a2499cf90d remove accidentaly kept updateLock field from GLTriangles 2017-04-12 11:25:11 +02:00
faiface 6d2aeb64e7 fix type in ComposeMethod doc 2017-04-11 17:28:27 +02:00
faiface 34a6d020a2 add ComposeMethod.Compose 2017-04-11 17:15:02 +02:00
faiface 9b624ae466 move ComposeTarget to separate file 2017-04-11 16:45:56 +02:00
faiface f2a0a19f6e fix race condition in GLTriangles 2017-04-11 15:02:58 +02:00
faiface a555999120 adopt RGB and Alpha 2017-04-10 17:25:56 +02:00
faiface aa2c560d4c rename ComposeDst* -> ComposeR* + add ComposePlus, ComposeCopy 2017-04-10 13:59:16 +02:00
faiface 4eca5f2d1e fix compile error 2017-04-10 00:48:17 +02:00
faiface 4374bb7be1 remove smooth argument from Canvas constructor 2017-04-10 00:47:06 +02:00
faiface 3fa31cdab5 fix drawing onto Canvas 2017-04-10 00:41:56 +02:00
faiface 219559cf20 add Window.SetComposeMethod 2017-04-10 00:41:48 +02:00
faiface 3d3f1c6e11 add Canvas.SetComposeMethod 2017-04-10 00:30:50 +02:00
faiface 58e8b8f892 add missing ComposeDstOver mode 2017-04-10 00:25:17 +02:00
faiface 13b9e6aee5 add ComposeTarget interface 2017-04-10 00:20:19 +02:00
faiface 9e8697cdf6 fix spelling in doc 2017-04-09 23:19:30 +02:00
faiface 134fe2bf7b use glhf.BlendFunc 2017-04-09 23:16:34 +02:00
faiface 9df8a3761b adjust RGB doc 2017-04-09 23:08:35 +02:00
faiface a081d5b0c8 add RGB and Alpha functions 2017-04-09 23:03:30 +02:00
faiface 32ae09e1e5 replace NRGBA to RGBA because Porter-Duff (!!!) 2017-04-09 22:00:26 +02:00
faiface 317cfb17b0 add code for 04 guide 2017-04-09 15:32:46 +02:00
faiface a9e6f41315 fix obsolete Batch doc 2017-04-09 15:32:31 +02:00
faiface 9cbce8f638 fix typo in Window doc 2017-04-08 18:05:50 +02:00
faiface e3f7901f2c adjust WindowConfig doc, more consistent with the rest 2017-04-07 12:34:16 +02:00
faiface ee19c6b361 fix GLFrame.SetBounds to not reallocate when not necessary 2017-04-05 23:20:55 +02:00
faiface 5ad013b286 fix Matrix.String 2017-04-04 14:10:39 +02:00
faiface 7fdf8c0d60 add Matrix.String 2017-04-04 14:08:37 +02:00
faiface 3a25f78b1a minor change 2017-04-04 14:02:39 +02:00
faiface ad733f5946 update code for 03 guide 2017-04-03 17:38:12 +02:00
faiface 8d5879070f add code for 03_moving_scaling_and_rotating guide 2017-04-03 13:52:52 +02:00
faiface 2155babc5d add Window.Color 2017-04-02 19:08:48 +02:00
faiface dc9afb5557 add code for 02_drawing_a_sprite guide 2017-04-02 19:07:28 +02:00
faiface 79f7f4fb42 split Canvas into Canvas+GLFrame + add GLPicture 2017-04-01 21:54:44 +02:00
faiface 2562f6b754 remove Slice and Original from Canvas 2017-03-31 15:03:06 +02:00
faiface b138fc5d5b make Sprite accept Picture and frame 2017-03-31 15:00:59 +02:00
faiface 15a270e689 remove Slice and Original from Picture interface 2017-03-30 23:34:07 +02:00
faiface 538ad90185 add code for 01_creating_a_window guide 2017-03-27 00:57:13 +02:00
45 changed files with 2265 additions and 600 deletions

147
README.md
View File

@ -1,5 +1,144 @@
# pixel
A simple and fast desktop multimedia/gamedev library.
# Pixel [![GoDoc](https://godoc.org/github.com/faiface/pixel?status.svg)](https://godoc.org/github.com/faiface/pixel) [![Go Report Card](https://goreportcard.com/badge/github.com/faiface/pixel)](https://goreportcard.com/report/github.com/faiface/pixel) [![Join the chat at https://gitter.im/pixellib/Lobby](https://badges.gitter.im/pixellib/Lobby.svg)](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)
## 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) |
| --- | --- |
| ![Lights](examples/lights/screenshot.png) | ![Platformer](examples/platformer/screenshot.png) |
| [Smoke](examples/smoke) | [Xor](examples/xor) |
| --- | --- |
| ![Smoke](examples/smoke/screenshot.png) | ![Xor](examples/xor/screenshot.png) |
## 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)
- Simple and convenient API
- Drawing a sprite to a window is as simple as `sprite.Draw(window)`
- Adding and subtracting vectors with `+` and `-` operators... how?
- Wanna know where the center of a window is? `window.Bounds().Center()`
- [...](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 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.
## 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)

View File

@ -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 {

View File

@ -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 introduced 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)
}

65
compose.go Normal file
View File

@ -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)))
}

108
data.go
View File

@ -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})
}{V(0, 0), Alpha(1), V(0, 0), 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,13 +126,12 @@ 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.
@ -143,19 +142,18 @@ func MakePictureData(rect Rect) *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().Dx()),
float64(rgba.Bounds().Dy()),
))
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
@ -222,33 +220,33 @@ 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())),
)
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.
@ -263,28 +261,12 @@ 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 RGBA{0, 0, 0, 0}
}
return ToNRGBA(pd.Pix[pd.Index(at)])
return ToRGBA(pd.Pix[pd.Index(at)])
}
// SetColor changes the color located at the given position.
@ -292,11 +274,11 @@ 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),
rgba := ToRGBA(col)
pd.Pix[pd.Index(at)] = color.RGBA{
R: uint8(rgba.R * 255),
G: uint8(rgba.G * 255),
B: uint8(rgba.B * 255),
A: uint8(rgba.A * 255),
}
}

7
doc.go Normal file
View File

@ -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

View File

@ -14,7 +14,7 @@ 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.
type Drawer struct {
Triangles Triangles
Picture Picture
@ -78,12 +78,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)
}

View File

@ -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)
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

View File

@ -0,0 +1,57 @@
package main
import (
"image"
"os"
_ "image/png"
"github.com/faiface/pixel"
"github.com/faiface/pixel/pixelgl"
"golang.org/x/image/colornames"
)
func loadPicture(path string) (pixel.Picture, error) {
file, err := os.Open(path)
if err != nil {
return nil, err
}
defer file.Close()
img, _, err := image.Decode(file)
if err != nil {
return nil, err
}
return pixel.PictureDataFromImage(img), nil
}
func run() {
cfg := pixelgl.WindowConfig{
Title: "Pixel Rocks!",
Bounds: pixel.R(0, 0, 1024, 768),
VSync: true,
}
win, err := pixelgl.NewWindow(cfg)
if err != nil {
panic(err)
}
pic, err := loadPicture("hiking.png")
if err != nil {
panic(err)
}
sprite := pixel.NewSprite(pic, pic.Bounds())
win.Clear(colornames.Greenyellow)
sprite.SetMatrix(pixel.IM.Moved(win.Bounds().Center()))
sprite.Draw(win)
for !win.Closed() {
win.Update()
}
}
func main() {
pixelgl.Run(run)
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

View File

@ -0,0 +1,71 @@
package main
import (
"image"
"os"
"time"
_ "image/png"
"github.com/faiface/pixel"
"github.com/faiface/pixel/pixelgl"
"golang.org/x/image/colornames"
)
func loadPicture(path string) (pixel.Picture, error) {
file, err := os.Open(path)
if err != nil {
return nil, err
}
defer file.Close()
img, _, err := image.Decode(file)
if err != nil {
return nil, err
}
return pixel.PictureDataFromImage(img), nil
}
func run() {
cfg := pixelgl.WindowConfig{
Title: "Pixel Rocks!",
Bounds: pixel.R(0, 0, 1024, 768),
VSync: true,
}
win, err := pixelgl.NewWindow(cfg)
if err != nil {
panic(err)
}
win.SetSmooth(true)
pic, err := loadPicture("hiking.png")
if err != nil {
panic(err)
}
sprite := pixel.NewSprite(pic, pic.Bounds())
angle := 0.0
last := time.Now()
for !win.Closed() {
dt := time.Since(last).Seconds()
last = time.Now()
angle += 3 * dt
win.Clear(colornames.Firebrick)
mat := pixel.IM
mat = mat.Rotated(0, angle)
mat = mat.Moved(win.Bounds().Center())
sprite.SetMatrix(mat)
sprite.Draw(win)
win.Update()
}
}
func main() {
pixelgl.Run(run)
}

View File

@ -0,0 +1,101 @@
package main
import (
"image"
"math"
"math/rand"
"os"
"time"
_ "image/png"
"github.com/faiface/pixel"
"github.com/faiface/pixel/pixelgl"
"golang.org/x/image/colornames"
)
func loadPicture(path string) (pixel.Picture, error) {
file, err := os.Open(path)
if err != nil {
return nil, err
}
defer file.Close()
img, _, err := image.Decode(file)
if err != nil {
return nil, err
}
return pixel.PictureDataFromImage(img), nil
}
func run() {
cfg := pixelgl.WindowConfig{
Title: "Pixel Rocks!",
Bounds: pixel.R(0, 0, 1024, 768),
VSync: true,
}
win, err := pixelgl.NewWindow(cfg)
if err != nil {
panic(err)
}
spritesheet, err := loadPicture("trees.png")
if err != nil {
panic(err)
}
var treesFrames []pixel.Rect
for x := spritesheet.Bounds().Min.X(); x < spritesheet.Bounds().Max.X(); x += 32 {
for y := spritesheet.Bounds().Min.Y(); y < spritesheet.Bounds().Max.Y(); y += 32 {
treesFrames = append(treesFrames, pixel.R(x, y, x+32, y+32))
}
}
var (
camPos = pixel.V(0, 0)
camSpeed = 500.0
camZoom = 1.0
camZoomSpeed = 1.2
trees []*pixel.Sprite
)
last := time.Now()
for !win.Closed() {
dt := time.Since(last).Seconds()
last = time.Now()
cam := pixel.IM.Scaled(camPos, camZoom).Moved(win.Bounds().Center() - camPos)
win.SetMatrix(cam)
if win.JustPressed(pixelgl.MouseButtonLeft) {
tree := pixel.NewSprite(spritesheet, treesFrames[rand.Intn(len(treesFrames))])
mouse := cam.Unproject(win.MousePosition())
tree.SetMatrix(pixel.IM.Scaled(0, 4).Moved(mouse))
trees = append(trees, tree)
}
if win.Pressed(pixelgl.KeyLeft) {
camPos -= pixel.X(camSpeed * dt)
}
if win.Pressed(pixelgl.KeyRight) {
camPos += pixel.X(camSpeed * dt)
}
if win.Pressed(pixelgl.KeyDown) {
camPos -= pixel.Y(camSpeed * dt)
}
if win.Pressed(pixelgl.KeyUp) {
camPos += pixel.Y(camSpeed * dt)
}
camZoom *= math.Pow(camZoomSpeed, win.MouseScroll().Y())
win.Clear(colornames.Forestgreen)
for _, tree := range trees {
tree.Draw(win)
}
win.Update()
}
}
func main() {
pixelgl.Run(run)
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@ -0,0 +1,111 @@
package main
import (
"fmt"
"image"
"math"
"math/rand"
"os"
"time"
_ "image/png"
"github.com/faiface/pixel"
"github.com/faiface/pixel/pixelgl"
"golang.org/x/image/colornames"
)
func loadPicture(path string) (pixel.Picture, error) {
file, err := os.Open(path)
if err != nil {
return nil, err
}
defer file.Close()
img, _, err := image.Decode(file)
if err != nil {
return nil, err
}
return pixel.PictureDataFromImage(img), nil
}
func run() {
cfg := pixelgl.WindowConfig{
Title: "Pixel Rocks!",
Bounds: pixel.R(0, 0, 1024, 768),
}
win, err := pixelgl.NewWindow(cfg)
if err != nil {
panic(err)
}
spritesheet, err := loadPicture("trees.png")
if err != nil {
panic(err)
}
batch := pixel.NewBatch(&pixel.TrianglesData{}, spritesheet)
var treesFrames []pixel.Rect
for x := spritesheet.Bounds().Min.X(); x < spritesheet.Bounds().Max.X(); x += 32 {
for y := spritesheet.Bounds().Min.Y(); y < spritesheet.Bounds().Max.Y(); y += 32 {
treesFrames = append(treesFrames, pixel.R(x, y, x+32, y+32))
}
}
var (
camPos = pixel.V(0, 0)
camSpeed = 500.0
camZoom = 1.0
camZoomSpeed = 1.2
)
var (
frames = 0
second = time.Tick(time.Second)
)
last := time.Now()
for !win.Closed() {
dt := time.Since(last).Seconds()
last = time.Now()
cam := pixel.IM.Scaled(camPos, camZoom).Moved(win.Bounds().Center() - camPos)
win.SetMatrix(cam)
if win.Pressed(pixelgl.MouseButtonLeft) {
tree := pixel.NewSprite(spritesheet, treesFrames[rand.Intn(len(treesFrames))])
mouse := cam.Unproject(win.MousePosition())
tree.SetMatrix(pixel.IM.Scaled(0, 4).Moved(mouse))
tree.Draw(batch)
}
if win.Pressed(pixelgl.KeyLeft) {
camPos -= pixel.X(camSpeed * dt)
}
if win.Pressed(pixelgl.KeyRight) {
camPos += pixel.X(camSpeed * dt)
}
if win.Pressed(pixelgl.KeyDown) {
camPos -= pixel.Y(camSpeed * dt)
}
if win.Pressed(pixelgl.KeyUp) {
camPos += pixel.Y(camSpeed * dt)
}
camZoom *= math.Pow(camZoomSpeed, win.MouseScroll().Y())
win.Clear(colornames.Forestgreen)
batch.Draw(win)
win.Update()
frames++
select {
case <-second:
win.SetTitle(fmt.Sprintf("%s | FPS: %d", cfg.Title, frames))
frames = 0
default:
}
}
}
func main() {
pixelgl.Run(run)
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@ -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)
}

13
examples/lights/README.md Normal file
View File

@ -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).
![Screenshot](screenshot.png)

201
examples/lights/main.go Normal file
View File

@ -0,0 +1,201 @@
package main
import (
"image"
"math"
"os"
"time"
_ "image/jpeg"
_ "image/png"
"github.com/faiface/pixel"
"github.com/faiface/pixel/imdraw"
"github.com/faiface/pixel/pixelgl"
)
func loadPicture(path string) (pixel.Picture, error) {
file, err := os.Open(path)
if err != nil {
return nil, err
}
defer file.Close()
img, _, err := image.Decode(file)
if err != nil {
return nil, err
}
return pixel.PictureDataFromImage(img), nil
}
type drawer interface {
Draw(pixel.Target)
}
type colorlight struct {
color pixel.RGBA
point pixel.Vec
angle float64
radius float64
dust float64
spread float64
imd *imdraw.IMDraw
}
func (cl *colorlight) apply(src, noise drawer, dst pixel.ComposeTarget) {
// create the light arc if not created already
if cl.imd == nil {
imd := imdraw.New(nil)
imd.Color(pixel.Alpha(1))
imd.Push(0)
imd.Color(pixel.Alpha(0))
for angle := -cl.spread / 2; angle <= cl.spread/2; angle += cl.spread / 64 {
imd.Push(pixel.X(1).Rotated(angle))
}
imd.Polygon(0)
cl.imd = imd
}
// draw the light arc
dst.SetMatrix(pixel.IM.Scaled(0, cl.radius).Rotated(0, cl.angle).Moved(cl.point))
dst.SetColorMask(pixel.Alpha(1))
dst.SetComposeMethod(pixel.ComposePlus)
cl.imd.Draw(dst)
// draw the noise inside the light
dst.SetMatrix(pixel.IM)
dst.SetComposeMethod(pixel.ComposeIn)
noise.Draw(dst)
// draw an image inside the noisy light
dst.SetColorMask(cl.color)
dst.SetComposeMethod(pixel.ComposeIn)
src.Draw(dst)
// draw the light reflected from the dust
dst.SetMatrix(pixel.IM.Scaled(0, cl.radius).Rotated(0, cl.angle).Moved(cl.point))
dst.SetColorMask(cl.color.Mul(pixel.Alpha(cl.dust)))
dst.SetComposeMethod(pixel.ComposePlus)
cl.imd.Draw(dst)
}
func run() {
pandaPic, err := loadPicture("panda.png")
if err != nil {
panic(err)
}
noisePic, err := loadPicture("noise.png")
if err != nil {
panic(err)
}
cfg := pixelgl.WindowConfig{
Title: "Lights",
Bounds: pixel.R(0, 0, 1024, 768),
VSync: true,
}
win, err := pixelgl.NewWindow(cfg)
if err != nil {
panic(err)
}
panda := pixel.NewSprite(pandaPic, pandaPic.Bounds())
panda.SetMatrix(pixel.IM.Moved(win.Bounds().Center()))
noise := pixel.NewSprite(noisePic, noisePic.Bounds())
noise.SetMatrix(pixel.IM.Moved(win.Bounds().Center()))
colors := []pixel.RGBA{
pixel.RGB(1, 0, 0),
pixel.RGB(0, 1, 0),
pixel.RGB(0, 0, 1),
pixel.RGB(1/math.Sqrt2, 1/math.Sqrt2, 0),
}
points := []pixel.Vec{
pixel.V(win.Bounds().Min.X(), win.Bounds().Min.Y()),
pixel.V(win.Bounds().Max.X(), win.Bounds().Min.Y()),
pixel.V(win.Bounds().Max.X(), win.Bounds().Max.Y()),
pixel.V(win.Bounds().Min.X(), win.Bounds().Max.Y()),
}
angles := []float64{
math.Pi / 4,
math.Pi/4 + math.Pi/2,
math.Pi/4 + 2*math.Pi/2,
math.Pi/4 + 3*math.Pi/2,
}
lights := make([]colorlight, 4)
for i := range lights {
lights[i] = colorlight{
color: colors[i],
point: points[i],
angle: angles[i],
radius: 800,
dust: 0.3,
spread: math.Pi / math.E,
}
}
speed := []float64{11.0 / 23, 13.0 / 23, 17.0 / 23, 19.0 / 23}
oneLight := pixelgl.NewCanvas(win.Bounds())
allLight := pixelgl.NewCanvas(win.Bounds())
fps30 := time.Tick(time.Second / 30)
start := time.Now()
for !win.Closed() {
if win.Pressed(pixelgl.KeyW) {
for i := range lights {
lights[i].dust += 0.05
if lights[i].dust > 1 {
lights[i].dust = 1
}
}
}
if win.Pressed(pixelgl.KeyS) {
for i := range lights {
lights[i].dust -= 0.05
if lights[i].dust < 0 {
lights[i].dust = 0
}
}
}
since := time.Since(start).Seconds()
for i := range lights {
lights[i].angle = angles[i] + math.Sin(since*speed[i])*math.Pi/8
}
win.Clear(pixel.RGB(0, 0, 0))
// draw the panda visible outside the light
win.SetColorMask(pixel.Alpha(0.4))
win.SetComposeMethod(pixel.ComposePlus)
panda.Draw(win)
allLight.Clear(pixel.Alpha(0))
allLight.SetComposeMethod(pixel.ComposePlus)
// accumulate all the lights
for i := range lights {
oneLight.Clear(pixel.Alpha(0))
lights[i].apply(panda, noise, oneLight)
oneLight.Draw(allLight)
}
// compose the final result
win.SetColorMask(pixel.Alpha(1))
allLight.Draw(win)
win.Update()
<-fps30 // maintain 30 fps, because my computer couldn't handle 60 here
}
}
func main() {
pixelgl.Run(run)
}

BIN
examples/lights/noise.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 838 KiB

BIN
examples/lights/panda.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

View File

@ -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).
![Screenshot](screenshot.png)

395
examples/platformer/main.go Normal file
View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

View File

@ -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
1 Front 0 0
2 FrontBlink 1 1
3 LookUp 2 2
4 Left 3 7
5 LeftRight 4 6
6 LeftBlink 7 7
7 Walk 8 15
8 Run 16 23
9 Jump 24 26

Binary file not shown.

After

Width:  |  Height:  |  Size: 530 B

8
examples/smoke/README.md Normal file
View File

@ -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/).
![Screenshot](screenshot.png)

View File

@ -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
1 1543 1146 362 336
2 396 0 398 364
3 761 1535 386 342
4 795 794 351 367
5 394 1163 386 364
6 1120 1163 377 348
7 795 0 368 407
8 0 0 395 397
9 1164 0 378 415
10 781 1163 338 360
11 1543 0 372 370
12 1148 1535 393 327
13 387 1535 373 364
14 396 365 371 388
15 0 758 378 404
16 379 758 378 371
17 1543 774 360 371
18 1543 1483 350 398
19 0 398 382 359
20 1164 416 356 382
21 1164 799 369 350
22 0 1535 386 394
23 795 408 366 385
24 1543 371 367 402
25 0 1163 393 371

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 MiB

226
examples/smoke/main.go Normal file
View File

@ -0,0 +1,226 @@
package main
import (
"container/list"
"encoding/csv"
"image"
"io"
"math"
"math/rand"
"os"
"strconv"
"time"
_ "image/png"
"github.com/faiface/pixel"
"github.com/faiface/pixel/pixelgl"
"golang.org/x/image/colornames"
)
type particle struct {
Sprite *pixel.Sprite
Pos pixel.Vec
Rot, Scale float64
Mask pixel.RGBA
Data interface{}
}
type particles struct {
Generate func() *particle
Update func(dt float64, p *particle) bool
SpawnAvg, SpawnDist float64
parts list.List
spawnTime float64
}
func (p *particles) UpdateAll(dt float64) {
p.spawnTime -= dt
for p.spawnTime <= 0 {
p.parts.PushFront(p.Generate())
p.spawnTime += math.Max(0, p.SpawnAvg+rand.NormFloat64()*p.SpawnDist)
}
for e := p.parts.Front(); e != nil; e = e.Next() {
part := e.Value.(*particle)
if !p.Update(dt, part) {
defer p.parts.Remove(e)
}
}
}
func (p *particles) DrawAll(t pixel.Target) {
for e := p.parts.Front(); e != nil; e = e.Next() {
part := e.Value.(*particle)
part.Sprite.SetMatrix(pixel.IM.
Scaled(0, part.Scale).
Rotated(0, part.Rot).
Moved(part.Pos),
)
part.Sprite.SetColorMask(part.Mask)
part.Sprite.Draw(t)
}
}
type smokeData struct {
Vel pixel.Vec
Time float64
Life float64
}
type smokeSystem struct {
Sheet pixel.Picture
Rects []pixel.Rect
Orig pixel.Vec
VelBasis []pixel.Vec
VelDist float64
LifeAvg, LifeDist float64
}
func (ss *smokeSystem) Generate() *particle {
sd := new(smokeData)
for _, base := range ss.VelBasis {
c := math.Max(0, 1+rand.NormFloat64()*ss.VelDist)
sd.Vel += base.Scaled(c)
}
sd.Vel = sd.Vel.Scaled(1 / float64(len(ss.VelBasis)))
sd.Life = math.Max(0, ss.LifeAvg+rand.NormFloat64()*ss.LifeDist)
p := new(particle)
p.Data = sd
p.Pos = ss.Orig
p.Scale = 1
p.Mask = pixel.Alpha(1)
p.Sprite = pixel.NewSprite(ss.Sheet, ss.Rects[rand.Intn(len(ss.Rects))])
return p
}
func (ss *smokeSystem) Update(dt float64, p *particle) bool {
sd := p.Data.(*smokeData)
sd.Time += dt
frac := sd.Time / sd.Life
p.Pos += sd.Vel.Scaled(dt)
p.Scale = 0.5 + frac*1.5
const (
fadeIn = 0.2
fadeOut = 0.4
)
if frac < fadeIn {
p.Mask = pixel.Alpha(math.Pow(frac/fadeIn, 0.75))
} else if frac >= fadeOut {
p.Mask = pixel.Alpha(math.Pow(1-(frac-fadeOut)/(1-fadeOut), 1.5))
} else {
p.Mask = pixel.Alpha(1)
}
return sd.Time < sd.Life
}
func loadSpriteSheet(sheetPath, descriptionPath string) (sheet pixel.Picture, rects []pixel.Rect, err error) {
sheetFile, err := os.Open(sheetPath)
if err != nil {
return nil, nil, err
}
defer sheetFile.Close()
sheetImg, _, err := image.Decode(sheetFile)
if err != nil {
return nil, nil, err
}
sheet = pixel.PictureDataFromImage(sheetImg)
descriptionFile, err := os.Open(descriptionPath)
if err != nil {
return nil, nil, err
}
defer descriptionFile.Close()
description := csv.NewReader(descriptionFile)
for {
record, err := description.Read()
if err == io.EOF {
break
}
if err != nil {
return nil, nil, err
}
x, _ := strconv.ParseFloat(record[0], 64)
y, _ := strconv.ParseFloat(record[1], 64)
w, _ := strconv.ParseFloat(record[2], 64)
h, _ := strconv.ParseFloat(record[3], 64)
y = sheet.Bounds().H() - y - h
rects = append(rects, pixel.R(x, y, x+w, y+h))
}
return sheet, rects, nil
}
func run() {
sheet, rects, err := loadSpriteSheet("blackSmoke.png", "blackSmoke.csv")
if err != nil {
panic(err)
}
cfg := pixelgl.WindowConfig{
Title: "Smoke",
Bounds: pixel.R(0, 0, 1024, 768),
Resizable: true,
VSync: true,
}
win, err := pixelgl.NewWindow(cfg)
if err != nil {
panic(err)
}
ss := &smokeSystem{
Rects: rects,
Orig: 0,
VelBasis: []pixel.Vec{pixel.V(-100, 100), pixel.V(100, 100), pixel.V(0, 100)},
VelDist: 0.1,
LifeAvg: 7,
LifeDist: 0.5,
}
p := &particles{
Generate: ss.Generate,
Update: ss.Update,
SpawnAvg: 0.3,
SpawnDist: 0.1,
}
batch := pixel.NewBatch(&pixel.TrianglesData{}, sheet)
last := time.Now()
for !win.Closed() {
dt := time.Since(last).Seconds()
last = time.Now()
p.UpdateAll(dt)
win.Clear(colornames.Aliceblue)
win.SetMatrix(pixel.IM.Moved(win.Bounds().Center() - pixel.Y(win.Bounds().H()/2)))
batch.Clear()
p.DrawAll(batch)
batch.Draw(win)
win.Update()
}
}
func main() {
pixelgl.Run(run)
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 381 KiB

8
examples/xor/README.md Normal file
View File

@ -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.
![Screenshot](screenshot.png)

76
examples/xor/main.go Normal file
View File

@ -0,0 +1,76 @@
package main
import (
"math"
"time"
"github.com/faiface/pixel"
"github.com/faiface/pixel/imdraw"
"github.com/faiface/pixel/pixelgl"
"golang.org/x/image/colornames"
)
func run() {
cfg := pixelgl.WindowConfig{
Title: "Xor",
Bounds: pixel.R(0, 0, 1024, 768),
Resizable: true,
VSync: true,
}
win, err := pixelgl.NewWindow(cfg)
if err != nil {
panic(err)
}
imd := imdraw.New(nil)
canvas := pixelgl.NewCanvas(win.Bounds())
start := time.Now()
for !win.Closed() {
// in case window got resized, we also need to resize our canvas
canvas.SetBounds(win.Bounds())
offset := math.Sin(time.Since(start).Seconds()) * 300
// clear the canvas to be totally transparent and set the xor compose method
canvas.Clear(pixel.Alpha(0))
canvas.SetComposeMethod(pixel.ComposeXor)
// red circle
imd.Clear()
imd.Color(pixel.RGB(1, 0, 0))
imd.Push(win.Bounds().Center() - pixel.X(offset))
imd.Circle(200, 0)
imd.Draw(canvas)
// blue circle
imd.Clear()
imd.Color(pixel.RGB(0, 0, 1))
imd.Push(win.Bounds().Center() + pixel.X(offset))
imd.Circle(150, 0)
imd.Draw(canvas)
// yellow circle
imd.Clear()
imd.Color(pixel.RGB(1, 1, 0))
imd.Push(win.Bounds().Center() - pixel.Y(offset))
imd.Circle(100, 0)
imd.Draw(canvas)
// magenta circle
imd.Clear()
imd.Color(pixel.RGB(1, 0, 1))
imd.Push(win.Bounds().Center() + pixel.Y(offset))
imd.Circle(50, 0)
imd.Draw(canvas)
win.Clear(colornames.Green)
canvas.Draw(win)
win.Update()
}
}
func main() {
pixelgl.Run(run)
}

BIN
examples/xor/screenshot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View File

@ -78,7 +78,7 @@ 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].
// Angle returns the angle between the vector u and the x-axis. The result is in range [-Pi, Pi].
func (u Vec) Angle() float64 {
return cmplx.Phase(complex128(u))
}
@ -165,6 +165,15 @@ func R(minX, minY, maxX, maxY float64) Rect {
}
}
// String returns the string representation of the Rect.
//
// r := pixel.R(100, 50, 200, 300)
// r.String() // returns "Rect(100, 50, 200, 300)"
// fmt.Println(r) // Rect(100, 50, 200, 300)
func (r Rect) String() string {
return fmt.Sprintf("Rect(%v, %v, %v, %v)", r.Min.X(), r.Min.Y(), r.Max.X(), r.Max.Y())
}
// Norm returns the Rect in normal form, such that Max is component-wise greater or equal than Min.
func (r Rect) Norm() Rect {
return Rect{
@ -179,15 +188,6 @@ func (r Rect) Norm() Rect {
}
}
// 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())
}
// W returns the width of the Rect.
func (r Rect) W() float64 {
return r.Max.X() - r.Min.X()
@ -249,7 +249,7 @@ func (r Rect) Resized(anchor, size Vec) Rect {
if r.W()*r.H() == 0 || size.X()*size.Y() == 0 {
panic(fmt.Errorf("(%T).Resize: zero area", r))
}
fraction := size.ScaledXY(V(1/r.W(), 1/r.H()))
fraction := V(size.X()/r.W(), size.Y()/r.H())
return Rect{
Min: anchor + (r.Min - anchor).ScaledXY(fraction),
Max: anchor + (r.Max - anchor).ScaledXY(fraction),
@ -287,6 +287,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)

View File

@ -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
@ -23,7 +25,7 @@ import (
//
// Use various methods 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)
//
@ -46,7 +48,7 @@ type IMDraw struct {
points []point
opts point
matrix pixel.Matrix
mask pixel.NRGBA
mask pixel.RGBA
tri *pixel.TrianglesData
batch *pixel.Batch
@ -56,7 +58,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 +89,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
}
@ -108,6 +110,8 @@ func (imd *IMDraw) Reset() {
}
// 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)
}
@ -127,7 +131,7 @@ func (imd *IMDraw) pushPt(pos pixel.Vec, pt point) {
// Color sets the color of the next Pushed points.
func (imd *IMDraw) Color(color color.Color) {
imd.opts.col = pixel.ToNRGBA(color)
imd.opts.col = pixel.ToRGBA(color)
}
// Picture sets the Picture coordinates of the next Pushed points.
@ -160,7 +164,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 +183,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 +278,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 +347,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)
@ -469,13 +537,13 @@ 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)
}

View File

@ -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
}

View File

@ -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,70 @@ 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()
}
// Draw draws a rectangle equal to Canvas's Bounds containing the Canvas's content to another
// Target.
//
// Note, that the matrix and the color mask of this Canvas have no effect here.
func (c *Canvas) Draw(t pixel.Target) {
c.sprite.Draw(t)
}
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 +267,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 +286,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,43 +305,7 @@ 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 (
@ -474,16 +326,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 = `
@ -499,12 +349,11 @@ out vec2 Texture;
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;
@ -522,7 +371,6 @@ in float Intensity;
out vec4 color;
uniform vec4 colorMask;
uniform vec4 texBorders;
uniform vec4 texBounds;
uniform sampler2D tex;
@ -531,16 +379,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 = (Texture - texBounds.xy) / texBounds.zw;
color += Intensity * Color * texture(tex, t);
color *= colorMask;
}
}
`

5
pixelgl/doc.go Normal file
View File

@ -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

99
pixelgl/glframe.go Normal file
View File

@ -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
}

98
pixelgl/glpicture.go Normal file
View File

@ -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,
}
}

View File

@ -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).
@ -142,14 +143,26 @@ func (gt *GLTriangles) updateData(t pixel.Triangles) {
}
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),

View File

@ -23,25 +23,16 @@ type WindowConfig struct {
// 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
@ -89,10 +80,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 {
@ -123,9 +111,9 @@ func NewWindow(cfg WindowConfig) (*Window, error) {
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)
@ -156,16 +144,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 +211,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 +257,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 +272,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 +281,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
@ -366,7 +315,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 +330,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 +352,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)
}

View File

@ -2,43 +2,44 @@ 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.
//
// 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())
//
// To achieve different anchoring, transformations and color masking, use SetMatrix and SetColorMask
// methods.
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,14 +47,21 @@ func (s *Sprite) Picture() Picture {
return s.d.Picture
}
// Frame returns the current Sprite's frame.
func (s *Sprite) Frame() Rect {
return s.frame
}
// SetMatrix sets a Matrix that this Sprite will be transformed by. This overrides any previously
// set 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()
if s.matrix != matrix {
s.matrix = matrix
s.calcData()
}
}
// Matrix returns the currently set Matrix.
@ -67,12 +75,15 @@ func (s *Sprite) Matrix() Matrix {
// 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()
rgba := ToRGBA(mask)
if s.mask != rgba {
s.mask = ToRGBA(mask)
s.calcData()
}
}
// ColorMask returns the currently set color mask.
func (s *Sprite) ColorMask() NRGBA {
func (s *Sprite) ColorMask() RGBA {
return s.mask
}
@ -83,9 +94,9 @@ func (s *Sprite) Draw(t Target) {
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 = X(s.frame.W() / 2)
vertical = Y(s.frame.H() / 2)
)
(*s.tri)[0].Position = -horizontal - vertical