├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── batch.go ├── circle.go ├── circle_test.go ├── color.go ├── color_test.go ├── compose.go ├── data.go ├── data_test.go ├── doc.go ├── drawer.go ├── drawer_test.go ├── go.mod ├── go.sum ├── imdraw ├── imdraw.go └── imdraw_test.go ├── interface.go ├── line_test.go ├── logo ├── LOGOTYPE-HORIZONTAL-BLACK.png ├── LOGOTYPE-HORIZONTAL-BLUE.png ├── LOGOTYPE-HORIZONTAL-MAROON.png ├── LOGOTYPE-HORIZONTAL-PURPLE.png ├── LOGOTYPE-HORIZONTAL-RED.png └── LOGOTYPE-HORIZONTAL-WHITE.png ├── math.go ├── math_test.go ├── matrix.go ├── matrix_test.go ├── pixel_test.go ├── pixelgl ├── canvas.go ├── doc.go ├── glframe.go ├── glpicture.go ├── glshader.go ├── gltriangles.go ├── input.go ├── joystick.go ├── monitor.go ├── run.go ├── util.go └── window.go ├── rectangle.go ├── rectangle_test.go ├── sprite.go ├── text ├── atlas.go ├── atlas_test.go ├── doc.go ├── text.go └── text_test.go ├── vector.go └── vector_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | test 2 | .vscode -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | # https://github.com/golang/go/issues/31293 3 | dist: xenial 4 | sudo: false 5 | addons: 6 | apt: 7 | packages: 8 | - xorg-dev 9 | - libx11-dev 10 | - libxrandr-dev 11 | - libxinerama-dev 12 | - libxcursor-dev 13 | - libxi-dev 14 | - libopenal-dev 15 | - libasound2-dev 16 | - libgl1-mesa-dev 17 | 18 | services: 19 | - xvfb 20 | 21 | env: 22 | - GO111MODULE=on 23 | 24 | go: 25 | - tip 26 | - 1.12.x 27 | 28 | install: 29 | - # Do nothing. This is needed to prevent the default install action 30 | # "go get -t -v ./..." from happening here because we want it to happen 31 | # inside script step. 32 | 33 | script: 34 | - go test -v -race -mod=readonly ./... 35 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | ## [Unreleased] 8 | - Add AnchorPos struct and functions #252 9 | - Add Clipboard Support 10 | - Fix SIGSEGV on text.NewAtlas if glyph absent 11 | - Use slice for range in Drawer.Dirty(), to improve performance 12 | - GLTriangle's fragment shader is used when rendered by the Canvas. 13 | - Add MSAA support 14 | 15 | ## [v0.10.0] 2020-08-22 16 | - Add AnchorPos struct and functions 17 | - Gamepad API added 18 | - Support setting an initial window position 19 | - Support hiding the window initially 20 | - Support creating maximized windows 21 | - Support waiting for events to reduce CPU load 22 | - Adding clipping rectangle support in GLTriangles 23 | 24 | ## [v0.10.0-beta] 2020-05-10 25 | - Add `WindowConfig.TransparentFramebuffer` option to support window transparency onto the background 26 | - Fixed Line intersects failing on lines passing through (0, 0) 27 | 28 | ## [v0.10.0-alpha] 2020-05-08 29 | - Upgrade to GLFW 3.3! :tada: 30 | - Closes https://github.com/faiface/pixel/issues/137 31 | - Add support for glfw's DisableCursor 32 | - Closes https://github.com/faiface/pixel/issues/213 33 | 34 | ## [v0.9.0] - 2020-05-02 35 | - Added feature from https://github.com/faiface/pixel/pull/219 36 | - Exposing Window.SwapBuffers so buffers can be swapped without polling input 37 | - Add more examples 38 | - Add position as out variable from vertex shader 39 | - Add experimental joystick support 40 | - Add mouse cursor operations 41 | - Add `Vec.Floor(…)` function 42 | - Add circle geometry 43 | - Fix `Matrix.Unproject(…)` for rotated matrix 44 | - Add 2D Line geometry 45 | - Add floating point round error correction 46 | - Performance improvements 47 | - Fix race condition in `NewGLTriangles(…)` 48 | - Add `TriangleData` benchmarks and improvements 49 | - Add zero rectangle variable for utility and consistency 50 | - Add support for Go Modules 51 | - Add `NoIconify` and `AlwaysOnTop` window hints 52 | 53 | 54 | ## [v0.8.0] - 2018-10-10 55 | Changelog for this and older versions can be found on the corresponding [GitHub 56 | releases](https://github.com/faiface/pixel/releases). 57 | 58 | [Unreleased]: https://github.com/faiface/pixel/compare/v0.10.0...HEAD 59 | [v0.10.0]: https://github.com/faiface/pixel/compare/v0.10.0-beta...v0.10.0 60 | [v0.10.0-beta]: https://github.com/faiface/pixel/compare/v0.10.0-alpha...v0.10.0-beta 61 | [v0.10.0-alpha]: https://github.com/faiface/pixel/compare/v0.9.0...v0.10.0-alpha 62 | [v0.9.0]: https://github.com/faiface/pixel/compare/v0.8.0...v0.9.0 63 | [v0.8.0]: https://github.com/faiface/pixel/releases/tag/v0.8.0 64 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Pixel 2 | 3 | :tada: Hi! I'm really glad you're considering contributing to Pixel! :tada: 4 | 5 | ## Here are a few ways you can contribute 6 | 7 | 1. **Make a community example** and place it inside the `community` folder of the [examples repository][examples]. 8 | 2. **Add tests**. There only few tests in Pixel at the moment. Take a look at them and make some similar. 9 | 3. **Add a small feature or an improvement**. Feel like some small feature is missing? Just make a PR. Be ready that I might reject it, though, if I don't find it particularly appealing. 10 | 4. **Join the big development** by joining the discussion on our [Discord Server](https://discord.gg/q2DK4MP), where we can discuss bigger changes and implement them after that. 11 | 12 | ## How to make a pull request 13 | 14 | Go gives you a nice surprise when attempting to make a PR on Github. The thing is, that when user _xyz_ forks Pixel on Github, it ends up in _github.com/xyz/pixel_, which fucks up your import paths. Here's how you deal with that: https://www.reddit.com/r/golang/comments/2jdcw1/how_do_you_deal_with_github_forking/. 15 | 16 | [examples]: https://github.com/faiface/pixel-examples/tree/master/community 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Michal Štrba 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # \*\*\*\*\*NOTICE\*\*\*\*\* 2 | 3 | This repo is not under active development anymore and has been archived. Continued development has been migrated to [Pixel2](https://github.com/gopxl/pixel). A big thank you to [faiface](https://github.com/faiface) for creating this awesome library and for all the hard work put into it. We encourage old and new users to check out the new repo and contribute to it. 4 | 5 | 6 | 7 |

8 | 9 | # Pixel [![Build Status](https://travis-ci.org/faiface/pixel.svg?branch=master)](https://travis-ci.org/faiface/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) [![Discord Chat](https://img.shields.io/discord/699679031603494954)](https://discord.gg/q2DK4MP) 10 | 11 | A hand-crafted 2D game library in Go. Take a look into the [features](#features) to see what it can 12 | do. 13 | 14 | ``` 15 | go get github.com/faiface/pixel 16 | ``` 17 | 18 | If you are using Modules (Go 1.11 or higher) and want a mutable copy of the source code: 19 | 20 | ``` 21 | git clone https://github.com/faiface/pixel # clone outside of $GOPATH 22 | cd pixel 23 | go install ./... 24 | ``` 25 | 26 | See [requirements](#requirements) for the list of libraries necessary for compilation. 27 | 28 | All significant changes are documented in [CHANGELOG.md](CHANGELOG.md). 29 | 30 | ## Tutorial 31 | 32 | The [Wiki of this repo](https://github.com/faiface/pixel/wiki) contains an extensive tutorial 33 | covering several topics of Pixel. Here's the content of the tutorial parts so far: 34 | 35 | - [Creating a Window](https://github.com/faiface/pixel/wiki/Creating-a-Window) 36 | - [Drawing a Sprite](https://github.com/faiface/pixel/wiki/Drawing-a-Sprite) 37 | - [Moving, scaling and rotating with Matrix](https://github.com/faiface/pixel/wiki/Moving,-scaling-and-rotating-with-Matrix) 38 | - [Pressing keys and clicking mouse](https://github.com/faiface/pixel/wiki/Pressing-keys-and-clicking-mouse) 39 | - [Drawing efficiently with Batch](https://github.com/faiface/pixel/wiki/Drawing-efficiently-with-Batch) 40 | - [Drawing shapes with IMDraw](https://github.com/faiface/pixel/wiki/Drawing-shapes-with-IMDraw) 41 | - [Typing text on the screen](https://github.com/faiface/pixel/wiki/Typing-text-on-the-screen) 42 | - [Using a custom fragment shader](https://github.com/faiface/pixel/wiki/Using-a-custom-fragment-shader) 43 | 44 | ## [Examples](https://github.com/faiface/pixel-examples) 45 | 46 | The [examples](https://github.com/faiface/pixel-examples) repository contains a few 47 | examples demonstrating Pixel's functionality. 48 | 49 | **To run an example**, navigate to it's directory, then `go run` the `main.go` file. For example: 50 | 51 | ``` 52 | $ cd pixel-examples/platformer 53 | $ go run main.go 54 | ``` 55 | 56 | Here are some screenshots from the examples! 57 | 58 | | [Lights](https://github.com/faiface/pixel-examples/blob/master/lights) | [Platformer](https://github.com/faiface/pixel-examples/blob/master/platformer) | 59 | | --- | --- | 60 | | ![Lights](https://github.com/faiface/pixel-examples/blob/master/lights/screenshot.png) | ![Platformer](https://github.com/faiface/pixel-examples/blob/master/platformer/screenshot.png) | 61 | 62 | | [Smoke](https://github.com/faiface/pixel-examples/blob/master/smoke) | [Typewriter](https://github.com/faiface/pixel-examples/blob/master/typewriter) | 63 | | --- | --- | 64 | | ![Smoke](https://github.com/faiface/pixel-examples/blob/master/smoke/screenshot.png) | ![Typewriter](https://github.com/faiface/pixel-examples/blob/master/typewriter/screenshot.png) | 65 | 66 | | [Raycaster](https://github.com/faiface/pixel-examples/blob/master/community/raycaster) | [Gizmo](https://github.com/Lallassu/gizmo) | 67 | | --- | --- | 68 | | ![Raycaster](https://github.com/faiface/pixel-examples/blob/master/community/raycaster/screenshot.png) | ![Gizmo](https://github.com/Lallassu/gizmo/blob/master/preview.png) | 69 | 70 | ## Features 71 | 72 | Here's the list of the main features in Pixel. Although Pixel is still under heavy development, 73 | **there should be no major breakage in the API.** This is not a 100% guarantee, though. 74 | 75 | - Fast 2D graphics 76 | - Sprites 77 | - Primitive shapes with immediate mode style 78 | [IMDraw](https://github.com/faiface/pixel/wiki/Drawing-shapes-with-IMDraw) (circles, rectangles, 79 | lines, ...) 80 | - Optimized drawing with [Batch](https://github.com/faiface/pixel/wiki/Drawing-efficiently-with-Batch) 81 | - Text drawing with [text](https://godoc.org/github.com/faiface/pixel/text) package 82 | - Audio through a separate [Beep](https://github.com/faiface/beep) library. 83 | - Simple and convenient API 84 | - Drawing a sprite to a window is as simple as `sprite.Draw(window, matrix)` 85 | - Wanna know where the center of a window is? `window.Bounds().Center()` 86 | - [...](https://godoc.org/github.com/faiface/pixel) 87 | - Full documentation and tutorial 88 | - Works on Linux, macOS and Windows 89 | - Window creation and manipulation (resizing, fullscreen, multiple windows, ...) 90 | - Keyboard (key presses, text input) and mouse input without events 91 | - Well integrated with the Go standard library 92 | - Use `"image"` package for loading pictures 93 | - Use `"time"` package for measuring delta time and FPS 94 | - Use `"image/color"` for colors, or use Pixel's own `color.Color` format, which supports easy 95 | multiplication and a few more features 96 | - Pixel uses `float64` throughout the library, compatible with `"math"` package 97 | - Geometry transformations with 98 | [Matrix](https://github.com/faiface/pixel/wiki/Moving,-scaling-and-rotating-with-Matrix) 99 | - Moving, scaling, rotating 100 | - Easy camera implementation 101 | - Off-screen drawing to Canvas or any other target (Batch, IMDraw, ...) 102 | - Fully garbage collected, no `Close` or `Dispose` methods 103 | - Full [Porter-Duff](http://ssp.impulsetrain.com/porterduff.html) composition, which enables 104 | - 2D lighting 105 | - Cutting holes into objects 106 | - Much more... 107 | - Pixel let's you draw stuff and do your job, it doesn't impose any particular style or paradigm 108 | - Platform and backend independent [core](https://godoc.org/github.com/faiface/pixel) 109 | - Core Target/Triangles/Picture pattern makes it easy to create new drawing targets that do 110 | arbitrarily crazy stuff (e.g. graphical effects) 111 | - Small codebase, ~5K lines of code, including the backend [glhf](https://github.com/faiface/glhf) 112 | package 113 | 114 | ## Related repositories 115 | 116 | Here are some packages which use Pixel: 117 | - [TilePix](https://github.com/bcvery1/tilepix) Makes handling TMX files built with [Tiled](https://www.mapeditor.org/) trivially easy to work with using Pixel. 118 | - [spriteplus](https://github.com/cebarks/spriteplus) Basic `SpriteSheet` and `Animation` implementations 119 | - [PixelUI](https://github.com/dusk125/pixelui) Imgui-based GUIs for Pixel 120 | - [pixelutils](https://github.com/dusk125/pixelutils) Variety of game related utilities (sprite packer, id generator, ticker, sprite loader, voronoia diagrams) 121 | 122 | ## Missing features 123 | 124 | Pixel is in development and still missing few critical features. Here're the most critical ones. 125 | 126 | - ~~Audio~~ 127 | - ~~Drawing text~~ 128 | - Antialiasing (filtering is supported, though) 129 | - ~~Advanced window manipulation (cursor hiding, window icon, ...)~~ 130 | - Better support for Hi-DPI displays 131 | - Mobile (and perhaps HTML5?) backend 132 | - ~~More advanced graphical effects (e.g. blur)~~ (solved with the addition of GLSL effects) 133 | - Tests and benchmarks 134 | - Vulkan support 135 | 136 | **Implementing these features will get us to the 1.0 release.** Contribute, so that it's as soon as 137 | possible! 138 | 139 | ## Requirements 140 | 141 | If you're using Windows and having trouble building Pixel, please check [this 142 | guide](https://github.com/faiface/pixel/wiki/Building-Pixel-on-Windows) on the 143 | [wiki](https://github.com/faiface/pixel/wiki). 144 | 145 | [PixelGL](https://godoc.org/github.com/faiface/pixel/pixelgl) backend uses OpenGL to render 146 | graphics. Because of that, OpenGL development libraries are needed for compilation. The dependencies 147 | are same as for [GLFW](https://github.com/go-gl/glfw). 148 | 149 | The OpenGL version used is **OpenGL 3.3**. 150 | 151 | - On macOS, you need Xcode or Command Line Tools for Xcode (`xcode-select --install`) for required 152 | headers and libraries. 153 | - On Ubuntu/Debian-like Linux distributions, you need `libgl1-mesa-dev` and `xorg-dev` packages. 154 | - On CentOS/Fedora-like Linux distributions, you need `libX11-devel libXcursor-devel libXrandr-devel 155 | libXinerama-devel mesa-libGL-devel libXi-devel libXxf86vm-devel` packages. 156 | - See [here](http://www.glfw.org/docs/latest/compile.html#compile_deps) for full details. 157 | 158 | **The combination of Go 1.8, macOS and latest XCode seems to be problematic** as mentioned in issue 159 | [#7](https://github.com/faiface/pixel/issues/7). This issue is probably not related to Pixel. 160 | **Upgrading to Go 1.8.1 fixes the issue.** 161 | 162 | ## Contributing 163 | 164 | Join us in the [Discord Chat!](https://discord.gg/q2DK4MP) 165 | 166 | Pixel is in, let's say, mid-stage of development. Many of the important features are here, some are 167 | missing. That's why **contributions are very important and welcome!** All alone, I will be able to 168 | finish the library, but it'll take a lot of time. With your help, it'll take much less. I encourage 169 | everyone to contribute, even with just an idea. Especially welcome are **issues** and **pull 170 | requests**. 171 | 172 | **However, I won't accept everything. Pixel is being developed with thought and care.** Each 173 | component was designed and re-designed multiple times. Code and API quality is very important here. 174 | API is focused on simplicity and expressiveness. 175 | 176 | When contributing, keep these goals in mind. It doesn't mean that I'll only accept perfect pull 177 | requests. It just means that I might not like your idea. Or that your pull requests could need some 178 | rewriting. That's perfectly fine, don't let it put you off. In the end, we'll just end up with a 179 | better result. 180 | 181 | Take a look at [CONTRIBUTING.md](CONTRIBUTING.md) for further information. 182 | 183 | ## License 184 | 185 | [MIT](LICENSE) 186 | -------------------------------------------------------------------------------- /batch.go: -------------------------------------------------------------------------------- 1 | package pixel 2 | 3 | import ( 4 | "fmt" 5 | "image/color" 6 | ) 7 | 8 | // Batch is a Target that allows for efficient drawing of many objects with the same Picture. 9 | // 10 | // To put an object into a Batch, just draw it onto it: 11 | // object.Draw(batch) 12 | type Batch struct { 13 | cont Drawer 14 | 15 | mat Matrix 16 | col RGBA 17 | } 18 | 19 | var _ BasicTarget = (*Batch)(nil) 20 | 21 | // NewBatch creates an empty Batch with the specified Picture and container. 22 | // 23 | // The container is where objects get accumulated. Batch will support precisely those Triangles 24 | // properties, that the supplied container supports. If you retain access to the container and 25 | // change it, call Dirty to notify Batch about the change. 26 | // 27 | // Note, that if the container does not support TrianglesColor, color masking will not work. 28 | func NewBatch(container Triangles, pic Picture) *Batch { 29 | b := &Batch{cont: Drawer{Triangles: container, Picture: pic, Cached: true}} 30 | b.SetMatrix(IM) 31 | b.SetColorMask(Alpha(1)) 32 | return b 33 | } 34 | 35 | // Dirty notifies Batch about an external modification of it's container. If you retain access to 36 | // the Batch's container and change it, call Dirty to notify Batch about the change. 37 | // 38 | // container := &pixel.TrianglesData{} 39 | // batch := pixel.NewBatch(container, nil) 40 | // container.SetLen(10) // container changed from outside of Batch 41 | // batch.Dirty() // notify Batch about the change 42 | func (b *Batch) Dirty() { 43 | b.cont.Dirty() 44 | } 45 | 46 | // Clear removes all objects from the Batch. 47 | func (b *Batch) Clear() { 48 | b.cont.Triangles.SetLen(0) 49 | b.cont.Dirty() 50 | } 51 | 52 | // Draw draws all objects that are currently in the Batch onto another Target. 53 | func (b *Batch) Draw(t Target) { 54 | b.cont.Draw(t) 55 | } 56 | 57 | // SetMatrix sets a Matrix that every point will be projected by. 58 | func (b *Batch) SetMatrix(m Matrix) { 59 | b.mat = m 60 | } 61 | 62 | // SetColorMask sets a mask color used in the following draws onto the Batch. 63 | func (b *Batch) SetColorMask(c color.Color) { 64 | if c == nil { 65 | b.col = Alpha(1) 66 | return 67 | } 68 | b.col = ToRGBA(c) 69 | } 70 | 71 | // MakeTriangles returns a specialized copy of the provided Triangles that draws onto this Batch. 72 | func (b *Batch) MakeTriangles(t Triangles) TargetTriangles { 73 | bt := &batchTriangles{ 74 | tri: t.Copy(), 75 | tmp: MakeTrianglesData(t.Len()), 76 | dst: b, 77 | } 78 | return bt 79 | } 80 | 81 | // MakePicture returns a specialized copy of the provided Picture that draws onto this Batch. 82 | func (b *Batch) MakePicture(p Picture) TargetPicture { 83 | if p != b.cont.Picture { 84 | panic(fmt.Errorf("(%T).MakePicture: Picture is not the Batch's Picture", b)) 85 | } 86 | bp := &batchPicture{ 87 | pic: p, 88 | dst: b, 89 | } 90 | return bp 91 | } 92 | 93 | type batchTriangles struct { 94 | tri Triangles 95 | tmp *TrianglesData 96 | dst *Batch 97 | } 98 | 99 | func (bt *batchTriangles) Len() int { 100 | return bt.tri.Len() 101 | } 102 | 103 | func (bt *batchTriangles) SetLen(len int) { 104 | bt.tri.SetLen(len) 105 | bt.tmp.SetLen(len) 106 | } 107 | 108 | func (bt *batchTriangles) Slice(i, j int) Triangles { 109 | return &batchTriangles{ 110 | tri: bt.tri.Slice(i, j), 111 | tmp: bt.tmp.Slice(i, j).(*TrianglesData), 112 | dst: bt.dst, 113 | } 114 | } 115 | 116 | func (bt *batchTriangles) Update(t Triangles) { 117 | bt.tri.Update(t) 118 | } 119 | 120 | func (bt *batchTriangles) Copy() Triangles { 121 | return &batchTriangles{ 122 | tri: bt.tri.Copy(), 123 | tmp: bt.tmp.Copy().(*TrianglesData), 124 | dst: bt.dst, 125 | } 126 | } 127 | 128 | func (bt *batchTriangles) draw(bp *batchPicture) { 129 | bt.tmp.Update(bt.tri) 130 | 131 | for i := range *bt.tmp { 132 | (*bt.tmp)[i].Position = bt.dst.mat.Project((*bt.tmp)[i].Position) 133 | (*bt.tmp)[i].Color = bt.dst.col.Mul((*bt.tmp)[i].Color) 134 | } 135 | 136 | cont := bt.dst.cont.Triangles 137 | cont.SetLen(cont.Len() + bt.tri.Len()) 138 | added := cont.Slice(cont.Len()-bt.tri.Len(), cont.Len()) 139 | added.Update(bt.tri) 140 | added.Update(bt.tmp) 141 | bt.dst.cont.Dirty() 142 | } 143 | 144 | func (bt *batchTriangles) Draw() { 145 | bt.draw(nil) 146 | } 147 | 148 | type batchPicture struct { 149 | pic Picture 150 | dst *Batch 151 | } 152 | 153 | func (bp *batchPicture) Bounds() Rect { 154 | return bp.pic.Bounds() 155 | } 156 | 157 | func (bp *batchPicture) Draw(t TargetTriangles) { 158 | bt := t.(*batchTriangles) 159 | if bp.dst != bt.dst { 160 | panic(fmt.Errorf("(%T).Draw: TargetTriangles generated by different Batch", bp)) 161 | } 162 | bt.draw(bp) 163 | } 164 | -------------------------------------------------------------------------------- /circle.go: -------------------------------------------------------------------------------- 1 | package pixel 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | ) 7 | 8 | // Circle is a 2D circle. It is defined by two properties: 9 | // - Center vector 10 | // - Radius float64 11 | type Circle struct { 12 | Center Vec 13 | Radius float64 14 | } 15 | 16 | // C returns a new Circle with the given radius and center coordinates. 17 | // 18 | // Note that a negative radius is valid. 19 | func C(center Vec, radius float64) Circle { 20 | return Circle{ 21 | Center: center, 22 | Radius: radius, 23 | } 24 | } 25 | 26 | // String returns the string representation of the Circle. 27 | // 28 | // c := pixel.C(10.1234, pixel.ZV) 29 | // c.String() // returns "Circle(10.12, Vec(0, 0))" 30 | // fmt.Println(c) // Circle(10.12, Vec(0, 0)) 31 | func (c Circle) String() string { 32 | return fmt.Sprintf("Circle(%s, %.2f)", c.Center, c.Radius) 33 | } 34 | 35 | // Norm returns the Circle in normalized form - this sets the radius to its absolute value. 36 | // 37 | // c := pixel.C(-10, pixel.ZV) 38 | // c.Norm() // returns pixel.Circle{pixel.Vec{0, 0}, 10} 39 | func (c Circle) Norm() Circle { 40 | return Circle{ 41 | Center: c.Center, 42 | Radius: math.Abs(c.Radius), 43 | } 44 | } 45 | 46 | // Area returns the area of the Circle. 47 | func (c Circle) Area() float64 { 48 | return math.Pi * math.Pow(c.Radius, 2) 49 | } 50 | 51 | // Moved returns the Circle moved by the given vector delta. 52 | func (c Circle) Moved(delta Vec) Circle { 53 | return Circle{ 54 | Center: c.Center.Add(delta), 55 | Radius: c.Radius, 56 | } 57 | } 58 | 59 | // Resized returns the Circle resized by the given delta. The Circles center is use as the anchor. 60 | // 61 | // c := pixel.C(pixel.ZV, 10) 62 | // c.Resized(-5) // returns pixel.Circle{pixel.Vec{0, 0}, 5} 63 | // c.Resized(25) // returns pixel.Circle{pixel.Vec{0, 0}, 35} 64 | func (c Circle) Resized(radiusDelta float64) Circle { 65 | return Circle{ 66 | Center: c.Center, 67 | Radius: c.Radius + radiusDelta, 68 | } 69 | } 70 | 71 | // Contains checks whether a vector `u` is contained within this Circle (including it's perimeter). 72 | func (c Circle) Contains(u Vec) bool { 73 | toCenter := c.Center.To(u) 74 | return c.Radius >= toCenter.Len() 75 | } 76 | 77 | // Formula returns the values of h and k, for the equation of the circle: (x-h)^2 + (y-k)^2 = r^2 78 | // where r is the radius of the circle. 79 | func (c Circle) Formula() (h, k float64) { 80 | return c.Center.X, c.Center.Y 81 | } 82 | 83 | // maxCircle will return the larger circle based on the radius. 84 | func maxCircle(c, d Circle) Circle { 85 | if c.Radius < d.Radius { 86 | return d 87 | } 88 | return c 89 | } 90 | 91 | // minCircle will return the smaller circle based on the radius. 92 | func minCircle(c, d Circle) Circle { 93 | if c.Radius < d.Radius { 94 | return c 95 | } 96 | return d 97 | } 98 | 99 | // Union returns the minimal Circle which covers both `c` and `d`. 100 | func (c Circle) Union(d Circle) Circle { 101 | biggerC := maxCircle(c.Norm(), d.Norm()) 102 | smallerC := minCircle(c.Norm(), d.Norm()) 103 | 104 | // Get distance between centers 105 | dist := c.Center.To(d.Center).Len() 106 | 107 | // If the bigger Circle encompasses the smaller one, we have the result 108 | if dist+smallerC.Radius <= biggerC.Radius { 109 | return biggerC 110 | } 111 | 112 | // Calculate radius for encompassing Circle 113 | r := (dist + biggerC.Radius + smallerC.Radius) / 2 114 | 115 | // Calculate center for encompassing Circle 116 | theta := .5 + (biggerC.Radius-smallerC.Radius)/(2*dist) 117 | center := Lerp(smallerC.Center, biggerC.Center, theta) 118 | 119 | return Circle{ 120 | Center: center, 121 | Radius: r, 122 | } 123 | } 124 | 125 | // Intersect returns the maximal Circle which is covered by both `c` and `d`. 126 | // 127 | // If `c` and `d` don't overlap, this function returns a zero-sized circle at the centerpoint between the two Circle's 128 | // centers. 129 | func (c Circle) Intersect(d Circle) Circle { 130 | // Check if one of the circles encompasses the other; if so, return that one 131 | biggerC := maxCircle(c.Norm(), d.Norm()) 132 | smallerC := minCircle(c.Norm(), d.Norm()) 133 | 134 | if biggerC.Radius >= biggerC.Center.To(smallerC.Center).Len()+smallerC.Radius { 135 | return biggerC 136 | } 137 | 138 | // Calculate the midpoint between the two radii 139 | // Distance between centers 140 | dist := c.Center.To(d.Center).Len() 141 | // Difference between radii 142 | diff := dist - (c.Radius + d.Radius) 143 | // Distance from c.Center to the weighted midpoint 144 | distToMidpoint := c.Radius + 0.5*diff 145 | // Weighted midpoint 146 | center := Lerp(c.Center, d.Center, distToMidpoint/dist) 147 | 148 | // No need to calculate radius if the circles do not overlap 149 | if c.Center.To(d.Center).Len() >= c.Radius+d.Radius { 150 | return C(center, 0) 151 | } 152 | 153 | radius := c.Center.To(d.Center).Len() - (c.Radius + d.Radius) 154 | 155 | return Circle{ 156 | Center: center, 157 | Radius: math.Abs(radius), 158 | } 159 | } 160 | 161 | // IntersectLine will return the shortest Vec such that if the Circle is moved by the Vec returned, the Line and Rect no 162 | // longer intersect. 163 | func (c Circle) IntersectLine(l Line) Vec { 164 | return l.IntersectCircle(c).Scaled(-1) 165 | } 166 | 167 | // IntersectRect returns a minimal required Vector, such that moving the circle by that vector would stop the Circle 168 | // and the Rect intersecting. This function returns a zero-vector if the Circle and Rect do not overlap, and if only 169 | // the perimeters touch. 170 | // 171 | // This function will return a non-zero vector if: 172 | // - The Rect contains the Circle, partially or fully 173 | // - The Circle contains the Rect, partially of fully 174 | func (c Circle) IntersectRect(r Rect) Vec { 175 | // Checks if the c.Center is not in the diagonal quadrants of the rectangle 176 | if (r.Min.X <= c.Center.X && c.Center.X <= r.Max.X) || (r.Min.Y <= c.Center.Y && c.Center.Y <= r.Max.Y) { 177 | // 'grow' the Rect by c.Radius in each orthagonal 178 | grown := Rect{Min: r.Min.Sub(V(c.Radius, c.Radius)), Max: r.Max.Add(V(c.Radius, c.Radius))} 179 | if !grown.Contains(c.Center) { 180 | // c.Center not close enough to overlap, return zero-vector 181 | return ZV 182 | } 183 | 184 | // Get minimum distance to travel out of Rect 185 | rToC := r.Center().To(c.Center) 186 | h := c.Radius - math.Abs(rToC.X) + (r.W() / 2) 187 | v := c.Radius - math.Abs(rToC.Y) + (r.H() / 2) 188 | 189 | if rToC.X < 0 { 190 | h = -h 191 | } 192 | if rToC.Y < 0 { 193 | v = -v 194 | } 195 | 196 | // No intersect 197 | if h == 0 && v == 0 { 198 | return ZV 199 | } 200 | 201 | if math.Abs(h) > math.Abs(v) { 202 | // Vertical distance shorter 203 | return V(0, v) 204 | } 205 | return V(h, 0) 206 | } else { 207 | // The center is in the diagonal quadrants 208 | 209 | // Helper points to make code below easy to read. 210 | rectTopLeft := V(r.Min.X, r.Max.Y) 211 | rectBottomRight := V(r.Max.X, r.Min.Y) 212 | 213 | // Check for overlap. 214 | if !(c.Contains(r.Min) || c.Contains(r.Max) || c.Contains(rectTopLeft) || c.Contains(rectBottomRight)) { 215 | // No overlap. 216 | return ZV 217 | } 218 | 219 | var centerToCorner Vec 220 | if c.Center.To(r.Min).Len() <= c.Radius { 221 | // Closest to bottom-left 222 | centerToCorner = c.Center.To(r.Min) 223 | } 224 | if c.Center.To(r.Max).Len() <= c.Radius { 225 | // Closest to top-right 226 | centerToCorner = c.Center.To(r.Max) 227 | } 228 | if c.Center.To(rectTopLeft).Len() <= c.Radius { 229 | // Closest to top-left 230 | centerToCorner = c.Center.To(rectTopLeft) 231 | } 232 | if c.Center.To(rectBottomRight).Len() <= c.Radius { 233 | // Closest to bottom-right 234 | centerToCorner = c.Center.To(rectBottomRight) 235 | } 236 | 237 | cornerToCircumferenceLen := c.Radius - centerToCorner.Len() 238 | 239 | return centerToCorner.Unit().Scaled(cornerToCircumferenceLen) 240 | } 241 | } 242 | 243 | // IntersectionPoints returns all the points where the Circle intersects with the line provided. This can be zero, one or 244 | // two points, depending on the location of the shapes. The points of intersection will be returned in order of 245 | // closest-to-l.A to closest-to-l.B. 246 | func (c Circle) IntersectionPoints(l Line) []Vec { 247 | cContainsA := c.Contains(l.A) 248 | cContainsB := c.Contains(l.B) 249 | 250 | // Special case for both endpoint being contained within the circle 251 | if cContainsA && cContainsB { 252 | return []Vec{} 253 | } 254 | 255 | // Get closest point on the line to this circles' center 256 | closestToCenter := l.Closest(c.Center) 257 | 258 | // If the distance to the closest point is greater than the radius, there are no points of intersection 259 | if closestToCenter.To(c.Center).Len() > c.Radius { 260 | return []Vec{} 261 | } 262 | 263 | // If the distance to the closest point is equal to the radius, the line is tangent and the closest point is the 264 | // point at which it touches the circle. 265 | if closestToCenter.To(c.Center).Len() == c.Radius { 266 | return []Vec{closestToCenter} 267 | } 268 | 269 | // Special case for endpoint being on the circles' center 270 | if c.Center == l.A || c.Center == l.B { 271 | otherEnd := l.B 272 | if c.Center == l.B { 273 | otherEnd = l.A 274 | } 275 | intersect := c.Center.Add(c.Center.To(otherEnd).Unit().Scaled(c.Radius)) 276 | return []Vec{intersect} 277 | } 278 | 279 | // This means the distance to the closest point is less than the radius, so there is at least one intersection, 280 | // possibly two. 281 | 282 | // If one of the end points exists within the circle, there is only one intersection 283 | if cContainsA || cContainsB { 284 | containedPoint := l.A 285 | otherEnd := l.B 286 | if cContainsB { 287 | containedPoint = l.B 288 | otherEnd = l.A 289 | } 290 | 291 | // Use trigonometry to get the length of the line between the contained point and the intersection point. 292 | // The following is used to describe the triangle formed: 293 | // - a is the side between contained point and circle center 294 | // - b is the side between the center and the intersection point (radius) 295 | // - c is the side between the contained point and the intersection point 296 | // The captials of these letters are used as the angles opposite the respective sides. 297 | // a and b are known 298 | a := containedPoint.To(c.Center).Len() 299 | b := c.Radius 300 | // B can be calculated by subtracting the angle of b (to the x-axis) from the angle of c (to the x-axis) 301 | B := containedPoint.To(c.Center).Angle() - containedPoint.To(otherEnd).Angle() 302 | // Using the Sin rule we can get A 303 | A := math.Asin((a * math.Sin(B)) / b) 304 | // Using the rule that there are 180 degrees (or Pi radians) in a triangle, we can now get C 305 | C := math.Pi - A + B 306 | // If C is zero, the line segment is in-line with the center-intersect line. 307 | var c float64 308 | if C == 0 { 309 | c = b - a 310 | } else { 311 | // Using the Sine rule again, we can now get c 312 | c = (a * math.Sin(C)) / math.Sin(A) 313 | } 314 | // Travelling from the contained point to the other end by length of a will provide the intersection point. 315 | return []Vec{ 316 | containedPoint.Add(containedPoint.To(otherEnd).Unit().Scaled(c)), 317 | } 318 | } 319 | 320 | // Otherwise the endpoints exist outside of the circle, and the line segment intersects in two locations. 321 | // The vector formed by going from the closest point to the center of the circle will be perpendicular to the line; 322 | // this forms a right-angled triangle with the intersection points, with the radius as the hypotenuse. 323 | // Calculate the other triangles' sides' length. 324 | a := math.Sqrt(math.Pow(c.Radius, 2) - math.Pow(closestToCenter.To(c.Center).Len(), 2)) 325 | 326 | // Travelling in both directions from the closest point by length of a will provide the two intersection points. 327 | first := closestToCenter.Add(closestToCenter.To(l.A).Unit().Scaled(a)) 328 | second := closestToCenter.Add(closestToCenter.To(l.B).Unit().Scaled(a)) 329 | 330 | if first.To(l.A).Len() < second.To(l.A).Len() { 331 | return []Vec{first, second} 332 | } 333 | return []Vec{second, first} 334 | } 335 | -------------------------------------------------------------------------------- /circle_test.go: -------------------------------------------------------------------------------- 1 | package pixel_test 2 | 3 | import ( 4 | "math" 5 | "reflect" 6 | "testing" 7 | 8 | "github.com/faiface/pixel" 9 | ) 10 | 11 | func TestC(t *testing.T) { 12 | type args struct { 13 | radius float64 14 | center pixel.Vec 15 | } 16 | tests := []struct { 17 | name string 18 | args args 19 | want pixel.Circle 20 | }{ 21 | { 22 | name: "C(): positive radius", 23 | args: args{radius: 10, center: pixel.ZV}, 24 | want: pixel.Circle{Radius: 10, Center: pixel.ZV}, 25 | }, 26 | { 27 | name: "C(): zero radius", 28 | args: args{radius: 0, center: pixel.ZV}, 29 | want: pixel.Circle{Radius: 0, Center: pixel.ZV}, 30 | }, 31 | { 32 | name: "C(): negative radius", 33 | args: args{radius: -5, center: pixel.ZV}, 34 | want: pixel.Circle{Radius: -5, Center: pixel.ZV}, 35 | }, 36 | } 37 | for _, tt := range tests { 38 | t.Run(tt.name, func(t *testing.T) { 39 | if got := pixel.C(tt.args.center, tt.args.radius); !reflect.DeepEqual(got, tt.want) { 40 | t.Errorf("C() = %v, want %v", got, tt.want) 41 | } 42 | }) 43 | } 44 | } 45 | 46 | func TestCircle_String(t *testing.T) { 47 | type fields struct { 48 | radius float64 49 | center pixel.Vec 50 | } 51 | tests := []struct { 52 | name string 53 | fields fields 54 | want string 55 | }{ 56 | { 57 | name: "Circle.String(): positive radius", 58 | fields: fields{radius: 10, center: pixel.ZV}, 59 | want: "Circle(Vec(0, 0), 10.00)", 60 | }, 61 | { 62 | name: "Circle.String(): zero radius", 63 | fields: fields{radius: 0, center: pixel.ZV}, 64 | want: "Circle(Vec(0, 0), 0.00)", 65 | }, 66 | { 67 | name: "Circle.String(): negative radius", 68 | fields: fields{radius: -5, center: pixel.ZV}, 69 | want: "Circle(Vec(0, 0), -5.00)", 70 | }, 71 | { 72 | name: "Circle.String(): irrational radius", 73 | fields: fields{radius: math.Pi, center: pixel.ZV}, 74 | want: "Circle(Vec(0, 0), 3.14)", 75 | }, 76 | } 77 | for _, tt := range tests { 78 | t.Run(tt.name, func(t *testing.T) { 79 | c := pixel.C(tt.fields.center, tt.fields.radius) 80 | if got := c.String(); got != tt.want { 81 | t.Errorf("Circle.String() = %v, want %v", got, tt.want) 82 | } 83 | }) 84 | } 85 | } 86 | 87 | func TestCircle_Norm(t *testing.T) { 88 | type fields struct { 89 | radius float64 90 | center pixel.Vec 91 | } 92 | tests := []struct { 93 | name string 94 | fields fields 95 | want pixel.Circle 96 | }{ 97 | { 98 | name: "Circle.Norm(): positive radius", 99 | fields: fields{radius: 10, center: pixel.ZV}, 100 | want: pixel.C(pixel.ZV, 10), 101 | }, 102 | { 103 | name: "Circle.Norm(): zero radius", 104 | fields: fields{radius: 0, center: pixel.ZV}, 105 | want: pixel.C(pixel.ZV, 0), 106 | }, 107 | { 108 | name: "Circle.Norm(): negative radius", 109 | fields: fields{radius: -5, center: pixel.ZV}, 110 | want: pixel.C(pixel.ZV, 5), 111 | }, 112 | } 113 | for _, tt := range tests { 114 | t.Run(tt.name, func(t *testing.T) { 115 | c := pixel.C(tt.fields.center, tt.fields.radius) 116 | if got := c.Norm(); !reflect.DeepEqual(got, tt.want) { 117 | t.Errorf("Circle.Norm() = %v, want %v", got, tt.want) 118 | } 119 | }) 120 | } 121 | } 122 | 123 | func TestCircle_Area(t *testing.T) { 124 | type fields struct { 125 | radius float64 126 | center pixel.Vec 127 | } 128 | tests := []struct { 129 | name string 130 | fields fields 131 | want float64 132 | }{ 133 | { 134 | name: "Circle.Area(): positive radius", 135 | fields: fields{radius: 10, center: pixel.ZV}, 136 | want: 100 * math.Pi, 137 | }, 138 | { 139 | name: "Circle.Area(): zero radius", 140 | fields: fields{radius: 0, center: pixel.ZV}, 141 | want: 0, 142 | }, 143 | { 144 | name: "Circle.Area(): negative radius", 145 | fields: fields{radius: -5, center: pixel.ZV}, 146 | want: 25 * math.Pi, 147 | }, 148 | } 149 | for _, tt := range tests { 150 | t.Run(tt.name, func(t *testing.T) { 151 | c := pixel.C(tt.fields.center, tt.fields.radius) 152 | if got := c.Area(); got != tt.want { 153 | t.Errorf("Circle.Area() = %v, want %v", got, tt.want) 154 | } 155 | }) 156 | } 157 | } 158 | 159 | func TestCircle_Moved(t *testing.T) { 160 | type fields struct { 161 | radius float64 162 | center pixel.Vec 163 | } 164 | type args struct { 165 | delta pixel.Vec 166 | } 167 | tests := []struct { 168 | name string 169 | fields fields 170 | args args 171 | want pixel.Circle 172 | }{ 173 | { 174 | name: "Circle.Moved(): positive movement", 175 | fields: fields{radius: 10, center: pixel.ZV}, 176 | args: args{delta: pixel.V(10, 20)}, 177 | want: pixel.C(pixel.V(10, 20), 10), 178 | }, 179 | { 180 | name: "Circle.Moved(): zero movement", 181 | fields: fields{radius: 10, center: pixel.ZV}, 182 | args: args{delta: pixel.ZV}, 183 | want: pixel.C(pixel.V(0, 0), 10), 184 | }, 185 | { 186 | name: "Circle.Moved(): negative movement", 187 | fields: fields{radius: 10, center: pixel.ZV}, 188 | args: args{delta: pixel.V(-5, -10)}, 189 | want: pixel.C(pixel.V(-5, -10), 10), 190 | }, 191 | } 192 | for _, tt := range tests { 193 | t.Run(tt.name, func(t *testing.T) { 194 | c := pixel.C(tt.fields.center, tt.fields.radius) 195 | if got := c.Moved(tt.args.delta); !reflect.DeepEqual(got, tt.want) { 196 | t.Errorf("Circle.Moved() = %v, want %v", got, tt.want) 197 | } 198 | }) 199 | } 200 | } 201 | 202 | func TestCircle_Resized(t *testing.T) { 203 | type fields struct { 204 | radius float64 205 | center pixel.Vec 206 | } 207 | type args struct { 208 | radiusDelta float64 209 | } 210 | tests := []struct { 211 | name string 212 | fields fields 213 | args args 214 | want pixel.Circle 215 | }{ 216 | { 217 | name: "Circle.Resized(): positive delta", 218 | fields: fields{radius: 10, center: pixel.ZV}, 219 | args: args{radiusDelta: 5}, 220 | want: pixel.C(pixel.V(0, 0), 15), 221 | }, 222 | { 223 | name: "Circle.Resized(): zero delta", 224 | fields: fields{radius: 10, center: pixel.ZV}, 225 | args: args{radiusDelta: 0}, 226 | want: pixel.C(pixel.V(0, 0), 10), 227 | }, 228 | { 229 | name: "Circle.Resized(): negative delta", 230 | fields: fields{radius: 10, center: pixel.ZV}, 231 | args: args{radiusDelta: -5}, 232 | want: pixel.C(pixel.V(0, 0), 5), 233 | }, 234 | } 235 | for _, tt := range tests { 236 | t.Run(tt.name, func(t *testing.T) { 237 | c := pixel.C(tt.fields.center, tt.fields.radius) 238 | if got := c.Resized(tt.args.radiusDelta); !reflect.DeepEqual(got, tt.want) { 239 | t.Errorf("Circle.Resized() = %v, want %v", got, tt.want) 240 | } 241 | }) 242 | } 243 | } 244 | 245 | func TestCircle_Contains(t *testing.T) { 246 | type fields struct { 247 | radius float64 248 | center pixel.Vec 249 | } 250 | type args struct { 251 | u pixel.Vec 252 | } 253 | tests := []struct { 254 | name string 255 | fields fields 256 | args args 257 | want bool 258 | }{ 259 | { 260 | name: "Circle.Contains(): point on cicles' center", 261 | fields: fields{radius: 10, center: pixel.ZV}, 262 | args: args{u: pixel.ZV}, 263 | want: true, 264 | }, 265 | { 266 | name: "Circle.Contains(): point offcenter", 267 | fields: fields{radius: 10, center: pixel.V(5, 0)}, 268 | args: args{u: pixel.ZV}, 269 | want: true, 270 | }, 271 | { 272 | name: "Circle.Contains(): point on circumference", 273 | fields: fields{radius: 10, center: pixel.V(10, 0)}, 274 | args: args{u: pixel.ZV}, 275 | want: true, 276 | }, 277 | { 278 | name: "Circle.Contains(): point outside circle", 279 | fields: fields{radius: 10, center: pixel.V(15, 0)}, 280 | args: args{u: pixel.ZV}, 281 | want: false, 282 | }, 283 | } 284 | for _, tt := range tests { 285 | t.Run(tt.name, func(t *testing.T) { 286 | c := pixel.C(tt.fields.center, tt.fields.radius) 287 | if got := c.Contains(tt.args.u); got != tt.want { 288 | t.Errorf("Circle.Contains() = %v, want %v", got, tt.want) 289 | } 290 | }) 291 | } 292 | } 293 | 294 | func TestCircle_Union(t *testing.T) { 295 | type fields struct { 296 | radius float64 297 | center pixel.Vec 298 | } 299 | type args struct { 300 | d pixel.Circle 301 | } 302 | tests := []struct { 303 | name string 304 | fields fields 305 | args args 306 | want pixel.Circle 307 | }{ 308 | { 309 | name: "Circle.Union(): overlapping circles", 310 | fields: fields{radius: 5, center: pixel.ZV}, 311 | args: args{d: pixel.C(pixel.ZV, 5)}, 312 | want: pixel.C(pixel.ZV, 5), 313 | }, 314 | { 315 | name: "Circle.Union(): separate circles", 316 | fields: fields{radius: 1, center: pixel.ZV}, 317 | args: args{d: pixel.C(pixel.V(0, 2), 1)}, 318 | want: pixel.C(pixel.V(0, 1), 2), 319 | }, 320 | } 321 | for _, tt := range tests { 322 | t.Run(tt.name, func(t *testing.T) { 323 | c := pixel.C(tt.fields.center, tt.fields.radius) 324 | if got := c.Union(tt.args.d); !reflect.DeepEqual(got, tt.want) { 325 | t.Errorf("Circle.Union() = %v, want %v", got, tt.want) 326 | } 327 | }) 328 | } 329 | } 330 | 331 | func TestCircle_Intersect(t *testing.T) { 332 | type fields struct { 333 | radius float64 334 | center pixel.Vec 335 | } 336 | type args struct { 337 | d pixel.Circle 338 | } 339 | tests := []struct { 340 | name string 341 | fields fields 342 | args args 343 | want pixel.Circle 344 | }{ 345 | { 346 | name: "Circle.Intersect(): intersecting circles", 347 | fields: fields{radius: 1, center: pixel.ZV}, 348 | args: args{d: pixel.C(pixel.V(1, 0), 1)}, 349 | want: pixel.C(pixel.V(0.5, 0), 1), 350 | }, 351 | { 352 | name: "Circle.Intersect(): non-intersecting circles", 353 | fields: fields{radius: 1, center: pixel.ZV}, 354 | args: args{d: pixel.C(pixel.V(3, 3), 1)}, 355 | want: pixel.C(pixel.V(1.5, 1.5), 0), 356 | }, 357 | { 358 | name: "Circle.Intersect(): first circle encompassing second", 359 | fields: fields{radius: 10, center: pixel.ZV}, 360 | args: args{d: pixel.C(pixel.V(3, 3), 1)}, 361 | want: pixel.C(pixel.ZV, 10), 362 | }, 363 | { 364 | name: "Circle.Intersect(): second circle encompassing first", 365 | fields: fields{radius: 1, center: pixel.V(-1, -4)}, 366 | args: args{d: pixel.C(pixel.ZV, 10)}, 367 | want: pixel.C(pixel.ZV, 10), 368 | }, 369 | { 370 | name: "Circle.Intersect(): matching circles", 371 | fields: fields{radius: 1, center: pixel.ZV}, 372 | args: args{d: pixel.C(pixel.ZV, 1)}, 373 | want: pixel.C(pixel.ZV, 1), 374 | }, 375 | } 376 | for _, tt := range tests { 377 | t.Run(tt.name, func(t *testing.T) { 378 | c := pixel.C( 379 | tt.fields.center, 380 | tt.fields.radius, 381 | ) 382 | if got := c.Intersect(tt.args.d); !reflect.DeepEqual(got, tt.want) { 383 | t.Errorf("Circle.Intersect() = %v, want %v", got, tt.want) 384 | } 385 | }) 386 | } 387 | } 388 | 389 | func TestCircle_IntersectPoints(t *testing.T) { 390 | type fields struct { 391 | Center pixel.Vec 392 | Radius float64 393 | } 394 | type args struct { 395 | l pixel.Line 396 | } 397 | tests := []struct { 398 | name string 399 | fields fields 400 | args args 401 | want []pixel.Vec 402 | }{ 403 | { 404 | name: "Line intersects circle at two points", 405 | fields: fields{Center: pixel.V(2, 2), Radius: 1}, 406 | args: args{pixel.L(pixel.V(0, 0), pixel.V(10, 10))}, 407 | want: []pixel.Vec{pixel.V(1.292, 1.292), pixel.V(2.707, 2.707)}, 408 | }, 409 | { 410 | name: "Line intersects circle at one point", 411 | fields: fields{Center: pixel.V(-0.5, -0.5), Radius: 1}, 412 | args: args{pixel.L(pixel.V(0, 0), pixel.V(10, 10))}, 413 | want: []pixel.Vec{pixel.V(0.207, 0.207)}, 414 | }, 415 | { 416 | name: "Line endpoint is circle center", 417 | fields: fields{Center: pixel.V(0, 0), Radius: 1}, 418 | args: args{pixel.L(pixel.V(0, 0), pixel.V(10, 10))}, 419 | want: []pixel.Vec{pixel.V(0.707, 0.707)}, 420 | }, 421 | { 422 | name: "Both line endpoints within circle", 423 | fields: fields{Center: pixel.V(0, 0), Radius: 1}, 424 | args: args{pixel.L(pixel.V(0.2, 0.2), pixel.V(0.5, 0.5))}, 425 | want: []pixel.Vec{}, 426 | }, 427 | { 428 | name: "Line does not intersect circle", 429 | fields: fields{Center: pixel.V(10, 0), Radius: 1}, 430 | args: args{pixel.L(pixel.V(0, 0), pixel.V(10, 10))}, 431 | want: []pixel.Vec{}, 432 | }, 433 | { 434 | name: "Horizontal line intersects circle at two points", 435 | fields: fields{Center: pixel.V(5, 5), Radius: 1}, 436 | args: args{pixel.L(pixel.V(0, 5), pixel.V(10, 5))}, 437 | want: []pixel.Vec{pixel.V(4, 5), pixel.V(6, 5)}, 438 | }, 439 | { 440 | name: "Vertical line intersects circle at two points", 441 | fields: fields{Center: pixel.V(5, 5), Radius: 1}, 442 | args: args{pixel.L(pixel.V(5, 0), pixel.V(5, 10))}, 443 | want: []pixel.Vec{pixel.V(5, 4), pixel.V(5, 6)}, 444 | }, 445 | { 446 | name: "Left and down line intersects circle at two points", 447 | fields: fields{Center: pixel.V(5, 5), Radius: 1}, 448 | args: args{pixel.L(pixel.V(10, 10), pixel.V(0, 0))}, 449 | want: []pixel.Vec{pixel.V(5.707, 5.707), pixel.V(4.292, 4.292)}, 450 | }, 451 | } 452 | for _, tt := range tests { 453 | t.Run(tt.name, func(t *testing.T) { 454 | c := pixel.Circle{ 455 | Center: tt.fields.Center, 456 | Radius: tt.fields.Radius, 457 | } 458 | got := c.IntersectionPoints(tt.args.l) 459 | for i, v := range got { 460 | if !closeEnough(v.X, tt.want[i].X, 2) || !closeEnough(v.Y, tt.want[i].Y, 2) { 461 | t.Errorf("Circle.IntersectPoints() = %v, want %v", v, tt.want[i]) 462 | } 463 | } 464 | }) 465 | } 466 | } 467 | -------------------------------------------------------------------------------- /color.go: -------------------------------------------------------------------------------- 1 | package pixel 2 | 3 | import "image/color" 4 | 5 | // RGBA represents an alpha-premultiplied RGBA color with components within range [0, 1]. 6 | // 7 | // The difference between color.RGBA is that the value range is [0, 1] and the values are floats. 8 | type RGBA struct { 9 | R, G, B, A float64 10 | } 11 | 12 | // RGB returns a fully opaque RGBA color with the given RGB values. 13 | // 14 | // A common way to construct a transparent color is to create one with RGB constructor, then 15 | // multiply it by a color obtained from the Alpha constructor. 16 | func RGB(r, g, b float64) RGBA { 17 | return RGBA{r, g, b, 1} 18 | } 19 | 20 | // Alpha returns a white RGBA color with the given alpha component. 21 | func Alpha(a float64) RGBA { 22 | return RGBA{a, a, a, a} 23 | } 24 | 25 | // Add adds color d to color c component-wise and returns the result (the components are not 26 | // clamped). 27 | func (c RGBA) Add(d RGBA) RGBA { 28 | return RGBA{ 29 | R: c.R + d.R, 30 | G: c.G + d.G, 31 | B: c.B + d.B, 32 | A: c.A + d.A, 33 | } 34 | } 35 | 36 | // Sub subtracts color d from color c component-wise and returns the result (the components 37 | // are not clamped). 38 | func (c RGBA) Sub(d RGBA) RGBA { 39 | return RGBA{ 40 | R: c.R - d.R, 41 | G: c.G - d.G, 42 | B: c.B - d.B, 43 | A: c.A - d.A, 44 | } 45 | } 46 | 47 | // Mul multiplies color c by color d component-wise (the components are not clamped). 48 | func (c RGBA) Mul(d RGBA) RGBA { 49 | return RGBA{ 50 | R: c.R * d.R, 51 | G: c.G * d.G, 52 | B: c.B * d.B, 53 | A: c.A * d.A, 54 | } 55 | } 56 | 57 | // Scaled multiplies each component of color c by scale and returns the result (the components 58 | // are not clamped). 59 | func (c RGBA) Scaled(scale float64) RGBA { 60 | return RGBA{ 61 | R: c.R * scale, 62 | G: c.G * scale, 63 | B: c.B * scale, 64 | A: c.A * scale, 65 | } 66 | } 67 | 68 | // RGBA returns alpha-premultiplied red, green, blue and alpha components of the RGBA color. 69 | func (c RGBA) RGBA() (r, g, b, a uint32) { 70 | r = uint32(0xffff * c.R) 71 | g = uint32(0xffff * c.G) 72 | b = uint32(0xffff * c.B) 73 | a = uint32(0xffff * c.A) 74 | return 75 | } 76 | 77 | // ToRGBA converts a color to RGBA format. Using this function is preferred to using RGBAModel, for 78 | // performance (using RGBAModel introduces additional unnecessary allocations). 79 | func ToRGBA(c color.Color) RGBA { 80 | if c, ok := c.(RGBA); ok { 81 | return c 82 | } 83 | r, g, b, a := c.RGBA() 84 | return RGBA{ 85 | float64(r) / 0xffff, 86 | float64(g) / 0xffff, 87 | float64(b) / 0xffff, 88 | float64(a) / 0xffff, 89 | } 90 | } 91 | 92 | // RGBAModel converts colors to RGBA format. 93 | var RGBAModel = color.ModelFunc(rgbaModel) 94 | 95 | func rgbaModel(c color.Color) color.Color { 96 | return ToRGBA(c) 97 | } 98 | -------------------------------------------------------------------------------- /color_test.go: -------------------------------------------------------------------------------- 1 | package pixel_test 2 | 3 | import ( 4 | "fmt" 5 | "image/color" 6 | "testing" 7 | 8 | "github.com/faiface/pixel" 9 | ) 10 | 11 | func BenchmarkColorToRGBA(b *testing.B) { 12 | types := []color.Color{ 13 | color.NRGBA{R: 124, G: 14, B: 230, A: 42}, // slowest 14 | color.RGBA{R: 62, G: 32, B: 14, A: 63}, // faster 15 | pixel.RGB(0.8, 0.2, 0.5).Scaled(0.712), // fastest 16 | } 17 | for _, col := range types { 18 | b.Run(fmt.Sprintf("From %T", col), func(b *testing.B) { 19 | for i := 0; i < b.N; i++ { 20 | _ = pixel.ToRGBA(col) 21 | } 22 | }) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /compose.go: -------------------------------------------------------------------------------- 1 | package pixel 2 | 3 | import "errors" 4 | 5 | // ComposeTarget is a BasicTarget capable of Porter-Duff composition. 6 | type ComposeTarget interface { 7 | BasicTarget 8 | 9 | // SetComposeMethod sets a Porter-Duff composition method to be used. 10 | SetComposeMethod(ComposeMethod) 11 | } 12 | 13 | // ComposeMethod is a Porter-Duff composition method. 14 | type ComposeMethod int 15 | 16 | // Here's the list of all available Porter-Duff composition methods. Use ComposeOver for the basic 17 | // alpha blending. 18 | const ( 19 | ComposeOver ComposeMethod = iota 20 | ComposeIn 21 | ComposeOut 22 | ComposeAtop 23 | ComposeRover 24 | ComposeRin 25 | ComposeRout 26 | ComposeRatop 27 | ComposeXor 28 | ComposePlus 29 | ComposeCopy 30 | ) 31 | 32 | // Compose composes two colors together according to the ComposeMethod. A is the foreground, B is 33 | // the background. 34 | func (cm ComposeMethod) Compose(a, b RGBA) RGBA { 35 | var fa, fb float64 36 | 37 | switch cm { 38 | case ComposeOver: 39 | fa, fb = 1, 1-a.A 40 | case ComposeIn: 41 | fa, fb = b.A, 0 42 | case ComposeOut: 43 | fa, fb = 1-b.A, 0 44 | case ComposeAtop: 45 | fa, fb = b.A, 1-a.A 46 | case ComposeRover: 47 | fa, fb = 1-b.A, 1 48 | case ComposeRin: 49 | fa, fb = 0, a.A 50 | case ComposeRout: 51 | fa, fb = 0, 1-a.A 52 | case ComposeRatop: 53 | fa, fb = 1-b.A, a.A 54 | case ComposeXor: 55 | fa, fb = 1-b.A, 1-a.A 56 | case ComposePlus: 57 | fa, fb = 1, 1 58 | case ComposeCopy: 59 | fa, fb = 1, 0 60 | default: 61 | panic(errors.New("Compose: invalid ComposeMethod")) 62 | } 63 | 64 | return a.Mul(Alpha(fa)).Add(b.Mul(Alpha(fb))) 65 | } 66 | -------------------------------------------------------------------------------- /data.go: -------------------------------------------------------------------------------- 1 | package pixel 2 | 3 | import ( 4 | "fmt" 5 | "image" 6 | "image/color" 7 | "image/draw" 8 | "math" 9 | ) 10 | 11 | // zeroValueTriangleData is the default value of a TriangleData element 12 | var zeroValueTriangleData = struct { 13 | Position Vec 14 | Color RGBA 15 | Picture Vec 16 | Intensity float64 17 | ClipRect Rect 18 | IsClipped bool 19 | }{Color: RGBA{1, 1, 1, 1}} 20 | 21 | // TrianglesData specifies a list of Triangles vertices with three common properties: 22 | // TrianglesPosition, TrianglesColor and TrianglesPicture. 23 | type TrianglesData []struct { 24 | Position Vec 25 | Color RGBA 26 | Picture Vec 27 | Intensity float64 28 | ClipRect Rect 29 | IsClipped bool 30 | } 31 | 32 | // MakeTrianglesData creates TrianglesData of length len initialized with default property values. 33 | // 34 | // Prefer this function to make(TrianglesData, len), because make zeros them, while this function 35 | // does the correct intialization. 36 | func MakeTrianglesData(len int) *TrianglesData { 37 | td := make(TrianglesData, len) 38 | for i := 0; i < len; i++ { 39 | td[i] = zeroValueTriangleData 40 | } 41 | return &td 42 | } 43 | 44 | // Len returns the number of vertices in TrianglesData. 45 | func (td *TrianglesData) Len() int { 46 | return len(*td) 47 | } 48 | 49 | // SetLen resizes TrianglesData to len, while keeping the original content. 50 | // 51 | // If len is greater than TrianglesData's current length, the new data is filled with default 52 | // values ((0, 0), white, (0, 0), 0). 53 | func (td *TrianglesData) SetLen(len int) { 54 | if len > td.Len() { 55 | needAppend := len - td.Len() 56 | for i := 0; i < needAppend; i++ { 57 | *td = append(*td, zeroValueTriangleData) 58 | } 59 | } 60 | if len < td.Len() { 61 | *td = (*td)[:len] 62 | } 63 | } 64 | 65 | // Slice returns a sub-Triangles of this TrianglesData. 66 | func (td *TrianglesData) Slice(i, j int) Triangles { 67 | s := TrianglesData((*td)[i:j]) 68 | return &s 69 | } 70 | 71 | func (td *TrianglesData) updateData(t Triangles) { 72 | // fast path optimization 73 | if t, ok := t.(*TrianglesData); ok { 74 | copy(*td, *t) 75 | return 76 | } 77 | 78 | // slow path manual copy 79 | if t, ok := t.(TrianglesPosition); ok { 80 | for i := range *td { 81 | (*td)[i].Position = t.Position(i) 82 | } 83 | } 84 | if t, ok := t.(TrianglesColor); ok { 85 | for i := range *td { 86 | (*td)[i].Color = t.Color(i) 87 | } 88 | } 89 | if t, ok := t.(TrianglesPicture); ok { 90 | for i := range *td { 91 | (*td)[i].Picture, (*td)[i].Intensity = t.Picture(i) 92 | } 93 | } 94 | if t, ok := t.(TrianglesClipped); ok { 95 | for i := range *td { 96 | (*td)[i].ClipRect, (*td)[i].IsClipped = t.ClipRect(i) 97 | } 98 | } 99 | } 100 | 101 | // Update copies vertex properties from the supplied Triangles into this TrianglesData. 102 | // 103 | // TrianglesPosition, TrianglesColor and TrianglesTexture are supported. 104 | func (td *TrianglesData) Update(t Triangles) { 105 | if td.Len() != t.Len() { 106 | panic(fmt.Errorf("(%T).Update: invalid triangles length", td)) 107 | } 108 | td.updateData(t) 109 | } 110 | 111 | // Copy returns an exact independent copy of this TrianglesData. 112 | func (td *TrianglesData) Copy() Triangles { 113 | copyTd := MakeTrianglesData(td.Len()) 114 | copyTd.Update(td) 115 | return copyTd 116 | } 117 | 118 | // Position returns the position property of i-th vertex. 119 | func (td *TrianglesData) Position(i int) Vec { 120 | return (*td)[i].Position 121 | } 122 | 123 | // Color returns the color property of i-th vertex. 124 | func (td *TrianglesData) Color(i int) RGBA { 125 | return (*td)[i].Color 126 | } 127 | 128 | // Picture returns the picture property of i-th vertex. 129 | func (td *TrianglesData) Picture(i int) (pic Vec, intensity float64) { 130 | return (*td)[i].Picture, (*td)[i].Intensity 131 | } 132 | 133 | // ClipRect returns the clipping rectangle property of the i-th vertex. 134 | func (td *TrianglesData) ClipRect(i int) (rect Rect, has bool) { 135 | return (*td)[i].ClipRect, (*td)[i].IsClipped 136 | } 137 | 138 | // PictureData specifies an in-memory rectangular area of pixels and implements Picture and 139 | // PictureColor. 140 | // 141 | // Pixels are small rectangles of unit size of form (x, y, x+1, y+1), where x and y are integers. 142 | // PictureData contains and assigns a color to all pixels that are at least partially contained 143 | // within it's Bounds (Rect). 144 | // 145 | // The struct's innards are exposed for convenience, manual modification is at your own risk. 146 | // 147 | // The format of the pixels is color.RGBA and not pixel.RGBA for a very serious reason: 148 | // pixel.RGBA takes up 8x more memory than color.RGBA. 149 | type PictureData struct { 150 | Pix []color.RGBA 151 | Stride int 152 | Rect Rect 153 | } 154 | 155 | // MakePictureData creates a zero-initialized PictureData covering the given rectangle. 156 | func MakePictureData(rect Rect) *PictureData { 157 | w := int(math.Ceil(rect.Max.X)) - int(math.Floor(rect.Min.X)) 158 | h := int(math.Ceil(rect.Max.Y)) - int(math.Floor(rect.Min.Y)) 159 | pd := &PictureData{ 160 | Stride: w, 161 | Rect: rect, 162 | } 163 | pd.Pix = make([]color.RGBA, w*h) 164 | return pd 165 | } 166 | 167 | func verticalFlip(rgba *image.RGBA) { 168 | bounds := rgba.Bounds() 169 | width := bounds.Dx() 170 | 171 | tmpRow := make([]uint8, width*4) 172 | for i, j := 0, bounds.Dy()-1; i < j; i, j = i+1, j-1 { 173 | iRow := rgba.Pix[i*rgba.Stride : i*rgba.Stride+width*4] 174 | jRow := rgba.Pix[j*rgba.Stride : j*rgba.Stride+width*4] 175 | 176 | copy(tmpRow, iRow) 177 | copy(iRow, jRow) 178 | copy(jRow, tmpRow) 179 | } 180 | } 181 | 182 | // PictureDataFromImage converts an image.Image into PictureData. 183 | // 184 | // The resulting PictureData's Bounds will be the equivalent of the supplied image.Image's Bounds. 185 | func PictureDataFromImage(img image.Image) *PictureData { 186 | rgba := image.NewRGBA(img.Bounds()) 187 | draw.Draw(rgba, rgba.Bounds(), img, img.Bounds().Min, draw.Src) 188 | 189 | verticalFlip(rgba) 190 | 191 | pd := MakePictureData(R( 192 | float64(rgba.Bounds().Min.X), 193 | float64(rgba.Bounds().Min.Y), 194 | float64(rgba.Bounds().Max.X), 195 | float64(rgba.Bounds().Max.Y), 196 | )) 197 | 198 | for i := range pd.Pix { 199 | pd.Pix[i].R = rgba.Pix[i*4+0] 200 | pd.Pix[i].G = rgba.Pix[i*4+1] 201 | pd.Pix[i].B = rgba.Pix[i*4+2] 202 | pd.Pix[i].A = rgba.Pix[i*4+3] 203 | } 204 | 205 | return pd 206 | } 207 | 208 | // PictureDataFromPicture converts an arbitrary Picture into PictureData (the conversion may be 209 | // lossy, because PictureData works with unit-sized pixels). 210 | // 211 | // Bounds are preserved. 212 | func PictureDataFromPicture(pic Picture) *PictureData { 213 | if pd, ok := pic.(*PictureData); ok { 214 | return pd 215 | } 216 | 217 | bounds := pic.Bounds() 218 | pd := MakePictureData(bounds) 219 | 220 | if pic, ok := pic.(PictureColor); ok { 221 | for y := math.Floor(bounds.Min.Y); y < bounds.Max.Y; y++ { 222 | for x := math.Floor(bounds.Min.X); x < bounds.Max.X; x++ { 223 | // this together with the Floor is a trick to get all of the pixels 224 | at := V( 225 | math.Max(x, bounds.Min.X), 226 | math.Max(y, bounds.Min.Y), 227 | ) 228 | col := pic.Color(at) 229 | pd.Pix[pd.Index(at)] = color.RGBA{ 230 | R: uint8(col.R * 255), 231 | G: uint8(col.G * 255), 232 | B: uint8(col.B * 255), 233 | A: uint8(col.A * 255), 234 | } 235 | } 236 | } 237 | } 238 | 239 | return pd 240 | } 241 | 242 | // Image converts PictureData into an image.RGBA. 243 | // 244 | // The resulting image.RGBA's Bounds will be equivalent of the PictureData's Bounds. 245 | func (pd *PictureData) Image() *image.RGBA { 246 | bounds := image.Rect( 247 | int(math.Floor(pd.Rect.Min.X)), 248 | int(math.Floor(pd.Rect.Min.Y)), 249 | int(math.Ceil(pd.Rect.Max.X)), 250 | int(math.Ceil(pd.Rect.Max.Y)), 251 | ) 252 | rgba := image.NewRGBA(bounds) 253 | 254 | i := 0 255 | for y := bounds.Min.Y; y < bounds.Max.Y; y++ { 256 | for x := bounds.Min.X; x < bounds.Max.X; x++ { 257 | off := pd.Index(V(float64(x), float64(y))) 258 | rgba.Pix[i*4+0] = pd.Pix[off].R 259 | rgba.Pix[i*4+1] = pd.Pix[off].G 260 | rgba.Pix[i*4+2] = pd.Pix[off].B 261 | rgba.Pix[i*4+3] = pd.Pix[off].A 262 | i++ 263 | } 264 | } 265 | 266 | verticalFlip(rgba) 267 | 268 | return rgba 269 | } 270 | 271 | // Index returns the index of the pixel at the specified position inside the Pix slice. 272 | func (pd *PictureData) Index(at Vec) int { 273 | at = at.Sub(pd.Rect.Min.Map(math.Floor)) 274 | x, y := int(at.X), int(at.Y) 275 | return y*pd.Stride + x 276 | } 277 | 278 | // Bounds returns the bounds of this PictureData. 279 | func (pd *PictureData) Bounds() Rect { 280 | return pd.Rect 281 | } 282 | 283 | // Color returns the color located at the given position. 284 | func (pd *PictureData) Color(at Vec) RGBA { 285 | if !pd.Rect.Contains(at) { 286 | return RGBA{0, 0, 0, 0} 287 | } 288 | return ToRGBA(pd.Pix[pd.Index(at)]) 289 | } 290 | -------------------------------------------------------------------------------- /data_test.go: -------------------------------------------------------------------------------- 1 | package pixel_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/faiface/pixel" 7 | ) 8 | 9 | func BenchmarkMakeTrianglesData(b *testing.B) { 10 | tests := []struct { 11 | name string 12 | len int 13 | }{ 14 | { 15 | name: "Small slice", 16 | len: 10, 17 | }, 18 | { 19 | name: "Large slice", 20 | len: 10000, 21 | }, 22 | } 23 | 24 | for _, tt := range tests { 25 | b.Run(tt.name, func(b *testing.B) { 26 | for i := 0; i < b.N; i++ { 27 | _ = pixel.MakeTrianglesData(tt.len) 28 | } 29 | }) 30 | } 31 | } 32 | 33 | func BenchmarkTrianglesData_Len(b *testing.B) { 34 | tests := []struct { 35 | name string 36 | tData *pixel.TrianglesData 37 | }{ 38 | { 39 | name: "Small slice", 40 | tData: pixel.MakeTrianglesData(10), 41 | }, 42 | { 43 | name: "Large slice", 44 | tData: pixel.MakeTrianglesData(10000), 45 | }, 46 | } 47 | 48 | for _, tt := range tests { 49 | b.Run(tt.name, func(b *testing.B) { 50 | for i := 0; i < b.N; i++ { 51 | _ = tt.tData.Len() 52 | } 53 | }) 54 | } 55 | } 56 | 57 | func BenchmarkTrianglesData_SetLen(b *testing.B) { 58 | tests := []struct { 59 | name string 60 | tData *pixel.TrianglesData 61 | nextLenFunc func(int, int) (int, int) 62 | }{ 63 | { 64 | name: "Stay same size", 65 | tData: pixel.MakeTrianglesData(50), 66 | nextLenFunc: func(i, j int) (int, int) { return 50, 0 }, 67 | }, 68 | { 69 | name: "Change size", 70 | tData: pixel.MakeTrianglesData(50), 71 | nextLenFunc: func(i, j int) (int, int) { 72 | // 0 is shrink 73 | if j == 0 { 74 | next := i - 1 75 | if next < 1 { 76 | return 2, 1 77 | } 78 | return next, 0 79 | } 80 | 81 | // other than 0 is grow 82 | next := i + 1 83 | if next == 100 { 84 | return next, 0 85 | } 86 | return next, 1 87 | }, 88 | }, 89 | } 90 | 91 | for _, tt := range tests { 92 | b.Run(tt.name, func(b *testing.B) { 93 | var newLen int 94 | var c int 95 | for i := 0; i < b.N; i++ { 96 | newLen, c = tt.nextLenFunc(newLen, c) 97 | tt.tData.SetLen(newLen) 98 | } 99 | }) 100 | } 101 | } 102 | 103 | func BenchmarkTrianglesData_Slice(b *testing.B) { 104 | tests := []struct { 105 | name string 106 | tData *pixel.TrianglesData 107 | }{ 108 | { 109 | name: "Basic slice", 110 | tData: pixel.MakeTrianglesData(100), 111 | }, 112 | } 113 | 114 | for _, tt := range tests { 115 | b.Run(tt.name, func(b *testing.B) { 116 | for i := 0; i < b.N; i++ { 117 | _ = tt.tData.Slice(25, 50) 118 | } 119 | }) 120 | } 121 | } 122 | 123 | func BenchmarkTrianglesData_Update(b *testing.B) { 124 | tests := []struct { 125 | name string 126 | tData *pixel.TrianglesData 127 | t pixel.Triangles 128 | }{ 129 | { 130 | name: "Small Triangles", 131 | tData: pixel.MakeTrianglesData(20), 132 | t: pixel.MakeTrianglesData(20), 133 | }, 134 | { 135 | name: "Large Triangles", 136 | tData: pixel.MakeTrianglesData(10000), 137 | t: pixel.MakeTrianglesData(10000), 138 | }, 139 | } 140 | 141 | for _, tt := range tests { 142 | b.Run(tt.name, func(b *testing.B) { 143 | for i := 0; i < b.N; i++ { 144 | tt.tData.Update(tt.t) 145 | } 146 | }) 147 | } 148 | } 149 | 150 | func BenchmarkTrianglesData_Copy(b *testing.B) { 151 | tests := []struct { 152 | name string 153 | tData *pixel.TrianglesData 154 | }{ 155 | { 156 | name: "Small copy", 157 | tData: pixel.MakeTrianglesData(20), 158 | }, 159 | { 160 | name: "Large copy", 161 | tData: pixel.MakeTrianglesData(10000), 162 | }, 163 | } 164 | 165 | for _, tt := range tests { 166 | b.Run(tt.name, func(b *testing.B) { 167 | for i := 0; i < b.N; i++ { 168 | _ = tt.tData.Copy() 169 | } 170 | }) 171 | } 172 | } 173 | 174 | func BenchmarkTrianglesData_Position(b *testing.B) { 175 | tests := []struct { 176 | name string 177 | tData *pixel.TrianglesData 178 | position int 179 | }{ 180 | { 181 | name: "Getting beginning position", 182 | tData: pixel.MakeTrianglesData(1000), 183 | position: 2, 184 | }, 185 | { 186 | name: "Getting middle position", 187 | tData: pixel.MakeTrianglesData(1000), 188 | position: 500, 189 | }, 190 | { 191 | name: "Getting end position", 192 | tData: pixel.MakeTrianglesData(1000), 193 | position: 999, 194 | }, 195 | } 196 | 197 | for _, tt := range tests { 198 | b.Run(tt.name, func(b *testing.B) { 199 | for i := 0; i < b.N; i++ { 200 | _ = tt.tData.Position(tt.position) 201 | } 202 | }) 203 | } 204 | } 205 | 206 | func BenchmarkTrianglesData_Color(b *testing.B) { 207 | tests := []struct { 208 | name string 209 | tData *pixel.TrianglesData 210 | position int 211 | }{ 212 | { 213 | name: "Getting beginning position", 214 | tData: pixel.MakeTrianglesData(1000), 215 | position: 2, 216 | }, 217 | { 218 | name: "Getting middle position", 219 | tData: pixel.MakeTrianglesData(1000), 220 | position: 500, 221 | }, 222 | { 223 | name: "Getting end position", 224 | tData: pixel.MakeTrianglesData(1000), 225 | position: 999, 226 | }, 227 | } 228 | 229 | for _, tt := range tests { 230 | b.Run(tt.name, func(b *testing.B) { 231 | for i := 0; i < b.N; i++ { 232 | _ = tt.tData.Color(tt.position) 233 | } 234 | }) 235 | } 236 | } 237 | 238 | func BenchmarkTrianglesData_Picture(b *testing.B) { 239 | tests := []struct { 240 | name string 241 | tData *pixel.TrianglesData 242 | position int 243 | }{ 244 | { 245 | name: "Getting beginning position", 246 | tData: pixel.MakeTrianglesData(1000), 247 | position: 2, 248 | }, 249 | { 250 | name: "Getting middle position", 251 | tData: pixel.MakeTrianglesData(1000), 252 | position: 500, 253 | }, 254 | { 255 | name: "Getting end position", 256 | tData: pixel.MakeTrianglesData(1000), 257 | position: 999, 258 | }, 259 | } 260 | 261 | for _, tt := range tests { 262 | b.Run(tt.name, func(b *testing.B) { 263 | for i := 0; i < b.N; i++ { 264 | _, _ = tt.tData.Picture(tt.position) 265 | } 266 | }) 267 | } 268 | } 269 | 270 | func BenchmarkTrianglesData_ClipRect(b *testing.B) { 271 | tests := []struct { 272 | name string 273 | tData *pixel.TrianglesData 274 | position int 275 | }{ 276 | { 277 | name: "Getting beginning position", 278 | tData: pixel.MakeTrianglesData(1000), 279 | position: 2, 280 | }, 281 | { 282 | name: "Getting middle position", 283 | tData: pixel.MakeTrianglesData(1000), 284 | position: 500, 285 | }, 286 | { 287 | name: "Getting end position", 288 | tData: pixel.MakeTrianglesData(1000), 289 | position: 999, 290 | }, 291 | } 292 | 293 | for _, tt := range tests { 294 | b.Run(tt.name, func(b *testing.B) { 295 | for i := 0; i < b.N; i++ { 296 | _, _ = tt.tData.ClipRect(tt.position) 297 | } 298 | }) 299 | } 300 | } 301 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | // Package pixel implements platform and backend agnostic core of the Pixel game development 2 | // library. 3 | // 4 | // It specifies the core Target, Triangles, Picture pattern and implements standard elements, such 5 | // as Sprite, Batch, Vec, Matrix and RGBA in addition to the basic Triangles and Picture 6 | // implementations: TrianglesData and PictureData. 7 | package pixel 8 | -------------------------------------------------------------------------------- /drawer.go: -------------------------------------------------------------------------------- 1 | package pixel 2 | 3 | // Drawer glues all the fundamental interfaces (Target, Triangles, Picture) into a coherent and the 4 | // only intended usage pattern. 5 | // 6 | // Drawer makes it possible to draw any combination of Triangles and Picture onto any Target 7 | // efficiently. 8 | // 9 | // To create a Drawer, just assign it's Triangles and Picture fields: 10 | // 11 | // d := pixel.Drawer{Triangles: t, Picture: p} 12 | // 13 | // If Triangles is nil, nothing will be drawn. If Picture is nil, Triangles will be drawn without a 14 | // Picture. 15 | // 16 | // Whenever you change the Triangles, call Dirty to notify Drawer that Triangles changed. You don't 17 | // need to notify Drawer about a change of the Picture. 18 | // 19 | // Note, that Drawer caches the results of MakePicture from Targets it's drawn to for each Picture 20 | // it's set to. What it means is that using a Drawer with an unbounded number of Pictures leads to a 21 | // memory leak, since Drawer caches them and never forgets. In such a situation, create a new Drawer 22 | // for each Picture. 23 | type Drawer struct { 24 | Triangles Triangles 25 | Picture Picture 26 | Cached bool 27 | 28 | targets map[Target]*drawerTarget 29 | allTargets []*drawerTarget 30 | inited bool 31 | } 32 | 33 | type drawerTarget struct { 34 | tris TargetTriangles 35 | pics map[Picture]TargetPicture 36 | clean bool 37 | } 38 | 39 | func (d *Drawer) lazyInit() { 40 | if !d.inited { 41 | d.targets = make(map[Target]*drawerTarget) 42 | d.inited = true 43 | } 44 | } 45 | 46 | // Dirty marks the Triangles of this Drawer as changed. If not called, changes will not be visible 47 | // when drawing. 48 | func (d *Drawer) Dirty() { 49 | d.lazyInit() 50 | 51 | for _, t := range d.allTargets { 52 | t.clean = false 53 | } 54 | } 55 | 56 | // Draw efficiently draws Triangles with Picture onto the provided Target. 57 | // 58 | // If Triangles is nil, nothing will be drawn. If Picture is nil, Triangles will be drawn without a 59 | // Picture. 60 | func (d *Drawer) Draw(t Target) { 61 | d.lazyInit() 62 | 63 | if d.Triangles == nil { 64 | return 65 | } 66 | 67 | dt := d.targets[t] 68 | if dt == nil { 69 | dt = &drawerTarget{ 70 | pics: make(map[Picture]TargetPicture), 71 | } 72 | d.targets[t] = dt 73 | d.allTargets = append(d.allTargets, dt) 74 | } 75 | 76 | if dt.tris == nil { 77 | dt.tris = t.MakeTriangles(d.Triangles) 78 | dt.clean = true 79 | } 80 | 81 | if !dt.clean { 82 | dt.tris.SetLen(d.Triangles.Len()) 83 | dt.tris.Update(d.Triangles) 84 | dt.clean = true 85 | } 86 | 87 | if d.Picture == nil { 88 | dt.tris.Draw() 89 | return 90 | } 91 | 92 | pic := dt.pics[d.Picture] 93 | if pic == nil { 94 | pic = t.MakePicture(d.Picture) 95 | 96 | if d.Cached { 97 | dt.pics[d.Picture] = pic 98 | } 99 | } 100 | 101 | pic.Draw(dt.tris) 102 | } 103 | -------------------------------------------------------------------------------- /drawer_test.go: -------------------------------------------------------------------------------- 1 | package pixel_test 2 | 3 | import ( 4 | "image" 5 | "testing" 6 | 7 | "github.com/faiface/pixel" 8 | ) 9 | 10 | func BenchmarkSpriteDrawBatch(b *testing.B) { 11 | img := image.NewRGBA(image.Rect(0, 0, 64, 64)) 12 | pic := pixel.PictureDataFromImage(img) 13 | sprite := pixel.NewSprite(pic, pixel.R(0, 0, 64, 64)) 14 | batch := pixel.NewBatch(&pixel.TrianglesData{}, pic) 15 | for i := 0; i < b.N; i++ { 16 | sprite.Draw(batch, pixel.IM) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/faiface/pixel 2 | 3 | go 1.12 4 | 5 | require ( 6 | github.com/faiface/glhf v0.0.0-20211013000516-57b20770c369 7 | github.com/faiface/mainthread v0.0.0-20171120011319-8b78f0a41ae3 8 | github.com/go-gl/gl v0.0.0-20210905235341-f7a045908259 9 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20210727001814-0db043d8d5be 10 | github.com/go-gl/mathgl v1.0.0 11 | github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 12 | github.com/pkg/errors v0.9.1 13 | github.com/stretchr/testify v1.7.0 14 | golang.org/x/image v0.5.0 15 | ) 16 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 2 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/faiface/glhf v0.0.0-20211013000516-57b20770c369 h1:gv4BgP50atccdK/1tZHDyP6rMwiiutR2HPreR/OyLzI= 4 | github.com/faiface/glhf v0.0.0-20211013000516-57b20770c369/go.mod h1:dDdUO+G9ZnJ9sc8nIUvhLkE45k8PEKW6+A3TdWsfpV0= 5 | github.com/faiface/mainthread v0.0.0-20171120011319-8b78f0a41ae3 h1:baVdMKlASEHrj19iqjARrPbaRisD7EuZEVJj6ZMLl1Q= 6 | github.com/faiface/mainthread v0.0.0-20171120011319-8b78f0a41ae3/go.mod h1:VEPNJUlxl5KdWjDvz6Q1l+rJlxF2i6xqDeGuGAxa87M= 7 | github.com/go-gl/gl v0.0.0-20210905235341-f7a045908259 h1:8q7+xl2D2qHPLTII1t4vSMNP2VKwDcn+Avf2WXvdB1A= 8 | github.com/go-gl/gl v0.0.0-20210905235341-f7a045908259/go.mod h1:wjpnOv6ONl2SuJSxqCPVaPZibGFdSci9HFocT9qtVYM= 9 | github.com/go-gl/glfw v0.0.0-20210727001814-0db043d8d5be h1:UVW91pfMB1GRQfVwC7//RGVbqX6Ea8jURmJhlANak1M= 10 | github.com/go-gl/glfw v0.0.0-20210727001814-0db043d8d5be/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= 11 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20210727001814-0db043d8d5be h1:vEIVIuBApEBQTEJt19GfhoU+zFSV+sNTa9E9FdnRYfk= 12 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20210727001814-0db043d8d5be/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= 13 | github.com/go-gl/mathgl v1.0.0 h1:t9DznWJlXxxjeeKLIdovCOVJQk/GzDEL7h/h+Ro2B68= 14 | github.com/go-gl/mathgl v1.0.0/go.mod h1:yhpkQzEiH9yPyxDUGzkmgScbaBVlhC06qodikEM0ZwQ= 15 | github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g= 16 | github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= 17 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 18 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 19 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 20 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 21 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 22 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= 23 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 24 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 25 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 26 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 27 | golang.org/x/image v0.0.0-20190321063152-3fc05d484e9f/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= 28 | golang.org/x/image v0.5.0 h1:5JMiNunQeQw++mMOz48/ISeNu3Iweh/JaZU8ZLqHRrI= 29 | golang.org/x/image v0.5.0/go.mod h1:FVC7BI/5Ym8R25iw5OLsgshdUBbT1h5jZTpA+mvAdZ4= 30 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 31 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 32 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 33 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 34 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 35 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 36 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 37 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 38 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 39 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 40 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 41 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 42 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 43 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 44 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 45 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 46 | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 47 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 48 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 49 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 50 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 51 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 52 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 53 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 54 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 55 | -------------------------------------------------------------------------------- /imdraw/imdraw_test.go: -------------------------------------------------------------------------------- 1 | package imdraw_test 2 | 3 | import ( 4 | "fmt" 5 | "math/rand" 6 | "testing" 7 | 8 | "github.com/faiface/pixel" 9 | "github.com/faiface/pixel/imdraw" 10 | ) 11 | 12 | func BenchmarkPush(b *testing.B) { 13 | imd := imdraw.New(nil) 14 | for i := 0; i < b.N; i++ { 15 | imd.Push(pixel.V(123.1, 99.4)) 16 | } 17 | } 18 | 19 | func pointLists(counts ...int) [][]pixel.Vec { 20 | lists := make([][]pixel.Vec, len(counts)) 21 | for i := range lists { 22 | lists[i] = make([]pixel.Vec, counts[i]) 23 | for j := range lists[i] { 24 | lists[i][j] = pixel.V( 25 | rand.Float64()*5000-2500, 26 | rand.Float64()*5000-2500, 27 | ) 28 | } 29 | } 30 | return lists 31 | } 32 | 33 | func BenchmarkLine(b *testing.B) { 34 | lists := pointLists(2, 5, 10, 100, 1000) 35 | for _, pts := range lists { 36 | b.Run(fmt.Sprintf("%d", len(pts)), func(b *testing.B) { 37 | imd := imdraw.New(nil) 38 | for i := 0; i < b.N; i++ { 39 | imd.Push(pts...) 40 | imd.Line(1) 41 | } 42 | }) 43 | } 44 | } 45 | 46 | func BenchmarkRectangle(b *testing.B) { 47 | lists := pointLists(2, 10, 100, 1000) 48 | for _, pts := range lists { 49 | b.Run(fmt.Sprintf("%d", len(pts)), func(b *testing.B) { 50 | imd := imdraw.New(nil) 51 | for i := 0; i < b.N; i++ { 52 | imd.Push(pts...) 53 | imd.Rectangle(0) 54 | } 55 | }) 56 | } 57 | } 58 | 59 | func BenchmarkPolygon(b *testing.B) { 60 | lists := pointLists(3, 10, 100, 1000) 61 | for _, pts := range lists { 62 | b.Run(fmt.Sprintf("%d", len(pts)), func(b *testing.B) { 63 | imd := imdraw.New(nil) 64 | for i := 0; i < b.N; i++ { 65 | imd.Push(pts...) 66 | imd.Polygon(0) 67 | } 68 | }) 69 | } 70 | } 71 | 72 | func BenchmarkEllipseFill(b *testing.B) { 73 | lists := pointLists(1, 10, 100, 1000) 74 | for _, pts := range lists { 75 | b.Run(fmt.Sprintf("%d", len(pts)), func(b *testing.B) { 76 | imd := imdraw.New(nil) 77 | for i := 0; i < b.N; i++ { 78 | imd.Push(pts...) 79 | imd.Ellipse(pixel.V(50, 100), 0) 80 | } 81 | }) 82 | } 83 | } 84 | 85 | func BenchmarkEllipseOutline(b *testing.B) { 86 | lists := pointLists(1, 10, 100, 1000) 87 | for _, pts := range lists { 88 | b.Run(fmt.Sprintf("%d", len(pts)), func(b *testing.B) { 89 | imd := imdraw.New(nil) 90 | for i := 0; i < b.N; i++ { 91 | imd.Push(pts...) 92 | imd.Ellipse(pixel.V(50, 100), 1) 93 | } 94 | }) 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /interface.go: -------------------------------------------------------------------------------- 1 | package pixel 2 | 3 | import "image/color" 4 | 5 | // Target is something that can be drawn onto, such as a window, a canvas, and so on. 6 | // 7 | // You can notice, that there are no "drawing" methods in a Target. That's because all drawing 8 | // happens indirectly through Triangles and Picture instances generated via MakeTriangles and 9 | // MakePicture method. 10 | type Target interface { 11 | // MakeTriangles generates a specialized copy of the provided Triangles. 12 | // 13 | // When calling Draw method on the returned TargetTriangles, the TargetTriangles will be 14 | // drawn onto the Target that generated them. 15 | // 16 | // Note, that not every Target has to recognize all possible types of Triangles. Some may 17 | // only recognize TrianglesPosition and TrianglesColor and ignore all other properties (if 18 | // present) when making new TargetTriangles. This varies from Target to Target. 19 | MakeTriangles(Triangles) TargetTriangles 20 | 21 | // MakePicture generates a specialized copy of the provided Picture. 22 | // 23 | // When calling Draw method on the returned TargetPicture, the TargetPicture will be drawn 24 | // onto the Target that generated it together with the TargetTriangles supplied to the Draw 25 | // method. 26 | MakePicture(Picture) TargetPicture 27 | } 28 | 29 | // BasicTarget is a Target with additional basic adjustment methods. 30 | type BasicTarget interface { 31 | Target 32 | 33 | // SetMatrix sets a Matrix that every point will be projected by. 34 | SetMatrix(Matrix) 35 | 36 | // SetColorMask sets a color that will be multiplied with the TrianglesColor property of all 37 | // Triangles. 38 | SetColorMask(color.Color) 39 | } 40 | 41 | // Triangles represents a list of vertices, where each three vertices form a triangle. (First, 42 | // second and third is the first triangle, fourth, fifth and sixth is the second triangle, etc.) 43 | type Triangles interface { 44 | // Len returns the number of vertices. The number of triangles is the number of vertices 45 | // divided by 3. 46 | Len() int 47 | 48 | // SetLen resizes Triangles to len vertices. If Triangles B were obtained by calling Slice 49 | // method on Triangles A, the relationship between A and B is undefined after calling SetLen 50 | // on either one of them. 51 | SetLen(len int) 52 | 53 | // Slice returns a sub-Triangles of this Triangles, covering vertices in range [i, j). 54 | // 55 | // If Triangles B were obtained by calling Slice(4, 9) on Triangles A, then A and B must 56 | // share the same underlying data. Modifying B must change the contents of A in range 57 | // [4, 9). The vertex with index 0 at B is the vertex with index 4 in A, and so on. 58 | // 59 | // Returned Triangles must have the same underlying type. 60 | Slice(i, j int) Triangles 61 | 62 | // Update copies vertex properties from the supplied Triangles into this Triangles. 63 | // 64 | // Properies not supported by these Triangles should be ignored. Properties not supported by 65 | // the supplied Triangles should be left untouched. 66 | // 67 | // The two Triangles must have the same Len. 68 | Update(Triangles) 69 | 70 | // Copy creates an exact independent copy of this Triangles (with the same underlying type). 71 | Copy() Triangles 72 | } 73 | 74 | // TargetTriangles are Triangles generated by a Target with MakeTriangles method. They can be drawn 75 | // onto that (no other) Target. 76 | type TargetTriangles interface { 77 | Triangles 78 | 79 | // Draw draws Triangles onto an associated Target. 80 | Draw() 81 | } 82 | 83 | // TrianglesPosition specifies Triangles with Position property. 84 | type TrianglesPosition interface { 85 | Triangles 86 | Position(i int) Vec 87 | } 88 | 89 | // TrianglesColor specifies Triangles with Color property. 90 | type TrianglesColor interface { 91 | Triangles 92 | Color(i int) RGBA 93 | } 94 | 95 | // TrianglesPicture specifies Triangles with Picture property. 96 | // 97 | // The first value returned from Picture method is Picture coordinates. The second one specifies the 98 | // weight of the Picture. Value of 0 means, that Picture should be completely ignored, 1 means that 99 | // is should be fully included and anything in between means anything in between. 100 | type TrianglesPicture interface { 101 | Triangles 102 | Picture(i int) (pic Vec, intensity float64) 103 | } 104 | 105 | // TrianglesClipped specifies Triangles with Clipping Rectangle property. 106 | // 107 | // The first value returned from ClipRect method is the clipping rectangle. The second one specifies 108 | // if the triangle is clipped. 109 | type TrianglesClipped interface { 110 | Triangles 111 | ClipRect(i int) (rect Rect, is bool) 112 | } 113 | 114 | // Picture represents a rectangular area of raster data, such as a color. It has Bounds which 115 | // specify the rectangle where data is located. 116 | type Picture interface { 117 | // Bounds returns the rectangle of the Picture. All data is located within this rectangle. 118 | // Querying properties outside the rectangle should return default value of that property. 119 | Bounds() Rect 120 | } 121 | 122 | // TargetPicture is a Picture generated by a Target using MakePicture method. This Picture can be drawn onto 123 | // that (no other) Target together with a TargetTriangles generated by the same Target. 124 | // 125 | // The TargetTriangles specify where, shape and how the Picture should be drawn. 126 | type TargetPicture interface { 127 | Picture 128 | 129 | // Draw draws the supplied TargetTriangles (which must be generated by the same Target as 130 | // this TargetPicture) with this TargetPicture. The TargetTriangles should utilize the data 131 | // from this TargetPicture in some way. 132 | Draw(TargetTriangles) 133 | } 134 | 135 | // PictureColor specifies Picture with Color property, so that every position inside the Picture's 136 | // Bounds has a color. 137 | // 138 | // Positions outside the Picture's Bounds must return full transparent (Alpha(0)). 139 | type PictureColor interface { 140 | Picture 141 | Color(at Vec) RGBA 142 | } 143 | -------------------------------------------------------------------------------- /logo/LOGOTYPE-HORIZONTAL-BLACK.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/faiface/pixel/0a251bc08bfcfd68720fc237ee6a6268fa3a12e6/logo/LOGOTYPE-HORIZONTAL-BLACK.png -------------------------------------------------------------------------------- /logo/LOGOTYPE-HORIZONTAL-BLUE.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/faiface/pixel/0a251bc08bfcfd68720fc237ee6a6268fa3a12e6/logo/LOGOTYPE-HORIZONTAL-BLUE.png -------------------------------------------------------------------------------- /logo/LOGOTYPE-HORIZONTAL-MAROON.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/faiface/pixel/0a251bc08bfcfd68720fc237ee6a6268fa3a12e6/logo/LOGOTYPE-HORIZONTAL-MAROON.png -------------------------------------------------------------------------------- /logo/LOGOTYPE-HORIZONTAL-PURPLE.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/faiface/pixel/0a251bc08bfcfd68720fc237ee6a6268fa3a12e6/logo/LOGOTYPE-HORIZONTAL-PURPLE.png -------------------------------------------------------------------------------- /logo/LOGOTYPE-HORIZONTAL-RED.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/faiface/pixel/0a251bc08bfcfd68720fc237ee6a6268fa3a12e6/logo/LOGOTYPE-HORIZONTAL-RED.png -------------------------------------------------------------------------------- /logo/LOGOTYPE-HORIZONTAL-WHITE.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/faiface/pixel/0a251bc08bfcfd68720fc237ee6a6268fa3a12e6/logo/LOGOTYPE-HORIZONTAL-WHITE.png -------------------------------------------------------------------------------- /math.go: -------------------------------------------------------------------------------- 1 | package pixel 2 | 3 | // Clamp returns x clamped to the interval [min, max]. 4 | // 5 | // If x is less than min, min is returned. If x is more than max, max is returned. Otherwise, x is 6 | // returned. 7 | func Clamp(x, min, max float64) float64 { 8 | if x < min { 9 | return min 10 | } 11 | if x > max { 12 | return max 13 | } 14 | return x 15 | } 16 | -------------------------------------------------------------------------------- /math_test.go: -------------------------------------------------------------------------------- 1 | package pixel_test 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | "testing" 7 | 8 | "github.com/faiface/pixel" 9 | ) 10 | 11 | // closeEnough will shift the decimal point by the accuracy required, truncates the results and compares them. 12 | // Effectively this compares two floats to a given decimal point. 13 | // Example: 14 | // closeEnough(100.125342432, 100.125, 2) == true 15 | // closeEnough(math.Pi, 3.14, 2) == true 16 | // closeEnough(0.1234, 0.1245, 3) == false 17 | func closeEnough(got, expected float64, decimalAccuracy int) bool { 18 | gotShifted := got * math.Pow10(decimalAccuracy) 19 | expectedShifted := expected * math.Pow10(decimalAccuracy) 20 | 21 | return math.Trunc(gotShifted) == math.Trunc(expectedShifted) 22 | } 23 | 24 | type clampTest struct { 25 | number float64 26 | min float64 27 | max float64 28 | expected float64 29 | } 30 | 31 | func TestClamp(t *testing.T) { 32 | tests := []clampTest{ 33 | {number: 1, min: 0, max: 5, expected: 1}, 34 | {number: 2, min: 0, max: 5, expected: 2}, 35 | {number: 8, min: 0, max: 5, expected: 5}, 36 | {number: -5, min: 0, max: 5, expected: 0}, 37 | {number: -5, min: -4, max: 5, expected: -4}, 38 | } 39 | 40 | for _, tc := range tests { 41 | result := pixel.Clamp(tc.number, tc.min, tc.max) 42 | if result != tc.expected { 43 | t.Error(fmt.Sprintf("Clamping %v with min %v and max %v should have given %v, but gave %v", tc.number, tc.min, tc.max, tc.expected, result)) 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /matrix.go: -------------------------------------------------------------------------------- 1 | package pixel 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | ) 7 | 8 | // Matrix is a 2x3 affine matrix that can be used for all kinds of spatial transforms, such 9 | // as movement, scaling and rotations. 10 | // 11 | // Matrix has a handful of useful methods, each of which adds a transformation to the matrix. For 12 | // example: 13 | // 14 | // pixel.IM.Moved(pixel.V(100, 200)).Rotated(pixel.ZV, math.Pi/2) 15 | // 16 | // This code creates a Matrix that first moves everything by 100 units horizontally and 200 units 17 | // vertically and then rotates everything by 90 degrees around the origin. 18 | // 19 | // Layout is: 20 | // [0] [2] [4] 21 | // [1] [3] [5] 22 | // 0 0 1 (implicit row) 23 | type Matrix [6]float64 24 | 25 | // IM stands for identity matrix. Does nothing, no transformation. 26 | var IM = Matrix{1, 0, 0, 1, 0, 0} 27 | 28 | // String returns a string representation of the Matrix. 29 | // 30 | // m := pixel.IM 31 | // fmt.Println(m) // Matrix(1 0 0 | 0 1 0) 32 | func (m Matrix) String() string { 33 | return fmt.Sprintf( 34 | "Matrix(%v %v %v | %v %v %v)", 35 | m[0], m[2], m[4], 36 | m[1], m[3], m[5], 37 | ) 38 | } 39 | 40 | // Moved moves everything by the delta vector. 41 | func (m Matrix) Moved(delta Vec) Matrix { 42 | m[4], m[5] = m[4]+delta.X, m[5]+delta.Y 43 | return m 44 | } 45 | 46 | // ScaledXY scales everything around a given point by the scale factor in each axis respectively. 47 | func (m Matrix) ScaledXY(around Vec, scale Vec) Matrix { 48 | m[4], m[5] = m[4]-around.X, m[5]-around.Y 49 | m[0], m[2], m[4] = m[0]*scale.X, m[2]*scale.X, m[4]*scale.X 50 | m[1], m[3], m[5] = m[1]*scale.Y, m[3]*scale.Y, m[5]*scale.Y 51 | m[4], m[5] = m[4]+around.X, m[5]+around.Y 52 | return m 53 | } 54 | 55 | // Scaled scales everything around a given point by the scale factor. 56 | func (m Matrix) Scaled(around Vec, scale float64) Matrix { 57 | return m.ScaledXY(around, V(scale, scale)) 58 | } 59 | 60 | // Rotated rotates everything around a given point by the given angle in radians. 61 | func (m Matrix) Rotated(around Vec, angle float64) Matrix { 62 | sint, cost := math.Sincos(angle) 63 | m[4], m[5] = m[4]-around.X, m[5]-around.Y 64 | m = m.Chained(Matrix{cost, sint, -sint, cost, 0, 0}) 65 | m[4], m[5] = m[4]+around.X, m[5]+around.Y 66 | return m 67 | } 68 | 69 | // Chained adds another Matrix to this one. All tranformations by the next Matrix will be applied 70 | // after the transformations of this Matrix. 71 | func (m Matrix) Chained(next Matrix) Matrix { 72 | return Matrix{ 73 | next[0]*m[0] + next[2]*m[1], 74 | next[1]*m[0] + next[3]*m[1], 75 | next[0]*m[2] + next[2]*m[3], 76 | next[1]*m[2] + next[3]*m[3], 77 | next[0]*m[4] + next[2]*m[5] + next[4], 78 | next[1]*m[4] + next[3]*m[5] + next[5], 79 | } 80 | } 81 | 82 | // Project applies all transformations added to the Matrix to a vector u and returns the result. 83 | // 84 | // Time complexity is O(1). 85 | func (m Matrix) Project(u Vec) Vec { 86 | return Vec{m[0]*u.X + m[2]*u.Y + m[4], m[1]*u.X + m[3]*u.Y + m[5]} 87 | } 88 | 89 | // Unproject does the inverse operation to Project. 90 | // 91 | // Time complexity is O(1). 92 | func (m Matrix) Unproject(u Vec) Vec { 93 | det := m[0]*m[3] - m[2]*m[1] 94 | return Vec{ 95 | (m[3]*(u.X-m[4]) - m[2]*(u.Y-m[5])) / det, 96 | (-m[1]*(u.X-m[4]) + m[0]*(u.Y-m[5])) / det, 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /matrix_test.go: -------------------------------------------------------------------------------- 1 | package pixel_test 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | "math/rand" 7 | "testing" 8 | 9 | "github.com/faiface/pixel" 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | func BenchmarkMatrix(b *testing.B) { 14 | b.Run("Moved", func(b *testing.B) { 15 | m := pixel.IM 16 | for i := 0; i < b.N; i++ { 17 | m = m.Moved(pixel.V(4.217, -132.99)) 18 | } 19 | }) 20 | b.Run("ScaledXY", func(b *testing.B) { 21 | m := pixel.IM 22 | for i := 0; i < b.N; i++ { 23 | m = m.ScaledXY(pixel.V(-5.1, 9.3), pixel.V(2.1, 0.98)) 24 | } 25 | }) 26 | b.Run("Rotated", func(b *testing.B) { 27 | m := pixel.IM 28 | for i := 0; i < b.N; i++ { 29 | m = m.Rotated(pixel.V(-5.1, 9.3), 1.4) 30 | } 31 | }) 32 | b.Run("Chained", func(b *testing.B) { 33 | var m1, m2 pixel.Matrix 34 | for i := range m1 { 35 | m1[i] = rand.Float64() 36 | m2[i] = rand.Float64() 37 | } 38 | for i := 0; i < b.N; i++ { 39 | m1 = m1.Chained(m2) 40 | } 41 | }) 42 | b.Run("Project", func(b *testing.B) { 43 | var m pixel.Matrix 44 | for i := range m { 45 | m[i] = rand.Float64() 46 | } 47 | u := pixel.V(1, 1) 48 | for i := 0; i < b.N; i++ { 49 | u = m.Project(u) 50 | } 51 | }) 52 | b.Run("Unproject", func(b *testing.B) { 53 | again: 54 | var m pixel.Matrix 55 | for i := range m { 56 | m[i] = rand.Float64() 57 | } 58 | if (m[0]*m[3])-(m[1]*m[2]) == 0 { // zero determinant, not invertible 59 | goto again 60 | } 61 | u := pixel.V(1, 1) 62 | for i := 0; i < b.N; i++ { 63 | u = m.Unproject(u) 64 | } 65 | }) 66 | } 67 | 68 | func TestMatrix_Unproject(t *testing.T) { 69 | const delta = 1e-15 70 | t.Run("for rotated matrix", func(t *testing.T) { 71 | matrix := pixel.IM. 72 | Rotated(pixel.ZV, math.Pi/2) 73 | unprojected := matrix.Unproject(pixel.V(0, 1)) 74 | assert.InDelta(t, unprojected.X, 1, delta) 75 | assert.InDelta(t, unprojected.Y, 0, delta) 76 | }) 77 | t.Run("for moved matrix", func(t *testing.T) { 78 | matrix := pixel.IM. 79 | Moved(pixel.V(1, 2)) 80 | unprojected := matrix.Unproject(pixel.V(2, 5)) 81 | assert.InDelta(t, unprojected.X, 1, delta) 82 | assert.InDelta(t, unprojected.Y, 3, delta) 83 | }) 84 | t.Run("for scaled matrix", func(t *testing.T) { 85 | matrix := pixel.IM. 86 | Scaled(pixel.ZV, 2) 87 | unprojected := matrix.Unproject(pixel.V(2, 4)) 88 | assert.InDelta(t, unprojected.X, 1, delta) 89 | assert.InDelta(t, unprojected.Y, 2, delta) 90 | }) 91 | t.Run("for scaled, rotated and moved matrix", func(t *testing.T) { 92 | matrix := pixel.IM. 93 | Scaled(pixel.ZV, 2). 94 | Rotated(pixel.ZV, math.Pi/2). 95 | Moved(pixel.V(2, 2)) 96 | unprojected := matrix.Unproject(pixel.V(-2, 6)) 97 | assert.InDelta(t, unprojected.X, 2, delta) 98 | assert.InDelta(t, unprojected.Y, 2, delta) 99 | }) 100 | t.Run("for rotated and moved matrix", func(t *testing.T) { 101 | matrix := pixel.IM. 102 | Rotated(pixel.ZV, math.Pi/2). 103 | Moved(pixel.V(1, 1)) 104 | unprojected := matrix.Unproject(pixel.V(1, 2)) 105 | assert.InDelta(t, unprojected.X, 1, delta) 106 | assert.InDelta(t, unprojected.Y, 0, delta) 107 | }) 108 | t.Run("for projected vertices using all kinds of matrices", func(t *testing.T) { 109 | namedMatrices := map[string]pixel.Matrix{ 110 | "IM": pixel.IM, 111 | "Scaled": pixel.IM.Scaled(pixel.ZV, 0.5), 112 | "Scaled x 2": pixel.IM.Scaled(pixel.ZV, 2), 113 | "Rotated": pixel.IM.Rotated(pixel.ZV, math.Pi/4), 114 | "Moved": pixel.IM.Moved(pixel.V(0.5, 1)), 115 | "Moved 2": pixel.IM.Moved(pixel.V(-1, -0.5)), 116 | "Scaled and Rotated": pixel.IM.Scaled(pixel.ZV, 0.5).Rotated(pixel.ZV, math.Pi/4), 117 | "Scaled, Rotated and Moved": pixel.IM.Scaled(pixel.ZV, 0.5).Rotated(pixel.ZV, math.Pi/4).Moved(pixel.V(1, 2)), 118 | "Rotated and Moved": pixel.IM.Rotated(pixel.ZV, math.Pi/4).Moved(pixel.V(1, 2)), 119 | } 120 | vertices := [...]pixel.Vec{ 121 | pixel.V(0, 0), 122 | pixel.V(5, 0), 123 | pixel.V(5, 10), 124 | pixel.V(0, 10), 125 | pixel.V(-5, 10), 126 | pixel.V(-5, 0), 127 | pixel.V(-5, -10), 128 | pixel.V(0, -10), 129 | pixel.V(5, -10), 130 | } 131 | for matrixName, matrix := range namedMatrices { 132 | for _, vertex := range vertices { 133 | testCase := fmt.Sprintf("for matrix %s and vertex %v", matrixName, vertex) 134 | t.Run(testCase, func(t *testing.T) { 135 | projected := matrix.Project(vertex) 136 | unprojected := matrix.Unproject(projected) 137 | assert.InDelta(t, vertex.X, unprojected.X, delta) 138 | assert.InDelta(t, vertex.Y, unprojected.Y, delta) 139 | }) 140 | } 141 | } 142 | }) 143 | t.Run("for singular matrix", func(t *testing.T) { 144 | matrix := pixel.Matrix{0, 0, 0, 0, 0, 0} 145 | unprojected := matrix.Unproject(pixel.ZV) 146 | assert.True(t, math.IsNaN(unprojected.X)) 147 | assert.True(t, math.IsNaN(unprojected.Y)) 148 | }) 149 | } 150 | -------------------------------------------------------------------------------- /pixel_test.go: -------------------------------------------------------------------------------- 1 | package pixel_test 2 | 3 | import ( 4 | "bytes" 5 | "image" 6 | "os" 7 | "testing" 8 | 9 | _ "image/png" 10 | 11 | "github.com/faiface/pixel" 12 | "github.com/faiface/pixel/pixelgl" 13 | ) 14 | 15 | // onePixelImage is the byte representation of a 1x1 solid white png file 16 | var onePixelImage = []byte{ 17 | 137, 80, 78, 71, 13, 10, 26, 10, 0, 0, 0, 13, 73, 72, 68, 82, 0, 0, 0, 1, 0, 0, 0, 1, 8, 2, 18 | 0, 0, 0, 144, 119, 83, 222, 0, 0, 1, 130, 105, 67, 67, 80, 73, 67, 67, 32, 112, 114, 111, 102, 105, 108, 101, 0, 19 | 0, 40, 145, 125, 145, 59, 72, 3, 65, 20, 69, 143, 73, 68, 17, 37, 133, 41, 68, 44, 182, 80, 43, 5, 81, 17, 75, 20 | 141, 66, 16, 34, 132, 168, 96, 212, 194, 221, 141, 137, 66, 118, 13, 187, 9, 54, 150, 130, 109, 192, 194, 79, 21 | 227, 175, 176, 177, 214, 214, 194, 86, 16, 4, 63, 32, 54, 182, 86, 138, 54, 18, 214, 55, 73, 32, 65, 140, 3, 22 | 195, 28, 238, 188, 123, 121, 243, 6, 124, 71, 25, 211, 114, 3, 3, 96, 217, 57, 39, 30, 9, 107, 243, 137, 5, 173, 23 | 233, 149, 0, 65, 90, 104, 0, 221, 116, 179, 227, 177, 88, 148, 186, 235, 235, 94, 213, 193, 93, 191, 202, 170, 24 | 95, 247, 231, 106, 75, 174, 184, 38, 52, 104, 194, 99, 102, 214, 201, 9, 47, 11, 143, 108, 228, 178, 138, 247, 25 | 132, 67, 230, 170, 158, 20, 62, 23, 238, 115, 164, 65, 225, 71, 165, 27, 101, 126, 83, 156, 46, 177, 79, 101, 26 | 134, 156, 217, 248, 132, 112, 72, 88, 75, 215, 176, 81, 195, 230, 170, 99, 9, 15, 11, 119, 39, 45, 91, 242, 125, 27 | 243, 101, 78, 42, 222, 84, 108, 101, 242, 102, 165, 79, 245, 194, 214, 21, 123, 110, 70, 233, 178, 187, 136, 48, 28 | 197, 52, 49, 52, 12, 242, 172, 145, 33, 71, 191, 156, 182, 40, 46, 113, 185, 15, 215, 241, 119, 150, 252, 49, 29 | 113, 25, 226, 90, 195, 20, 199, 36, 235, 88, 232, 37, 63, 234, 15, 126, 207, 214, 77, 13, 13, 150, 147, 90, 195, 30 | 208, 248, 226, 121, 31, 61, 208, 180, 3, 197, 130, 231, 125, 31, 123, 94, 241, 4, 252, 207, 112, 101, 87, 253, 31 | 235, 71, 48, 250, 41, 122, 161, 170, 117, 31, 66, 112, 11, 46, 174, 171, 154, 177, 11, 151, 219, 208, 241, 148, 32 | 213, 29, 189, 36, 249, 101, 251, 82, 41, 120, 63, 147, 111, 74, 64, 251, 45, 180, 44, 150, 231, 86, 185, 231, 33 | 244, 1, 102, 101, 86, 209, 27, 216, 63, 128, 222, 180, 100, 47, 213, 121, 119, 115, 237, 220, 254, 173, 169, 34 | 204, 239, 7, 178, 211, 114, 90, 10, 150, 157, 65, 0, 0, 0, 9, 112, 72, 89, 115, 0, 0, 46, 35, 0, 0, 46, 35, 1, 35 | 120, 165, 63, 118, 0, 0, 0, 7, 116, 73, 77, 69, 7, 227, 4, 15, 10, 5, 36, 189, 4, 224, 88, 0, 0, 0, 25, 116, 69, 36 | 88, 116, 67, 111, 109, 109, 101, 110, 116, 0, 67, 114, 101, 97, 116, 101, 100, 32, 119, 105, 116, 104, 32, 71, 37 | 73, 77, 80, 87, 129, 14, 23, 0, 0, 0, 12, 73, 68, 65, 84, 8, 215, 99, 120, 241, 226, 61, 0, 5, 123, 2, 192, 194, 38 | 77, 211, 95, 0, 0, 0, 0, 73, 69, 78, 68, 174, 66, 96, 130, 39 | } 40 | 41 | func TestMain(m *testing.M) { 42 | pixelgl.Run(func() { 43 | os.Exit(m.Run()) 44 | }) 45 | } 46 | 47 | func TestSprite_Draw(t *testing.T) { 48 | img, _, err := image.Decode(bytes.NewReader(onePixelImage)) 49 | if err != nil { 50 | t.Fatalf("Could not decode image: %v", err) 51 | } 52 | pic := pixel.PictureDataFromImage(img) 53 | 54 | sprite := pixel.NewSprite(pic, pic.Bounds()) 55 | 56 | cfg := pixelgl.WindowConfig{ 57 | Title: "testing", 58 | Bounds: pixel.R(0, 0, 150, 150), 59 | Invisible: true, 60 | } 61 | 62 | win, err := pixelgl.NewWindow(cfg) 63 | if err != nil { 64 | t.Fatalf("Could not create window: %v", err) 65 | } 66 | 67 | sprite.Draw(win, pixel.IM) 68 | } 69 | -------------------------------------------------------------------------------- /pixelgl/canvas.go: -------------------------------------------------------------------------------- 1 | package pixelgl 2 | 3 | import ( 4 | "fmt" 5 | "image/color" 6 | 7 | "github.com/faiface/glhf" 8 | "github.com/faiface/mainthread" 9 | "github.com/faiface/pixel" 10 | "github.com/go-gl/mathgl/mgl32" 11 | "github.com/pkg/errors" 12 | ) 13 | 14 | // Canvas is an off-screen rectangular BasicTarget and Picture at the same time, that you can draw 15 | // onto. 16 | // 17 | // It supports TrianglesPosition, TrianglesColor, TrianglesPicture and PictureColor. 18 | type Canvas struct { 19 | gf *GLFrame 20 | shader *GLShader 21 | 22 | cmp pixel.ComposeMethod 23 | mat mgl32.Mat3 24 | col mgl32.Vec4 25 | smooth bool 26 | 27 | sprite *pixel.Sprite 28 | } 29 | 30 | var _ pixel.ComposeTarget = (*Canvas)(nil) 31 | 32 | // NewCanvas creates a new empty, fully transparent Canvas with given bounds. 33 | func NewCanvas(bounds pixel.Rect) *Canvas { 34 | c := &Canvas{ 35 | gf: NewGLFrame(bounds), 36 | mat: mgl32.Ident3(), 37 | col: mgl32.Vec4{1, 1, 1, 1}, 38 | } 39 | 40 | c.shader = NewGLShader(baseCanvasFragmentShader) 41 | c.SetBounds(bounds) 42 | return c 43 | } 44 | 45 | // SetUniform will update the named uniform with the value of any supported underlying 46 | // attribute variable. If the uniform already exists, including defaults, they will be reassigned 47 | // to the new value. The value can be a pointer. 48 | func (c *Canvas) SetUniform(name string, value interface{}) { 49 | c.shader.SetUniform(name, value) 50 | } 51 | 52 | // SetFragmentShader allows you to set a new fragment shader on the underlying 53 | // framebuffer. Argument "src" is the GLSL source, not a filename. 54 | func (c *Canvas) SetFragmentShader(src string) { 55 | c.shader.fs = src 56 | c.shader.Update() 57 | } 58 | 59 | // MakeTriangles creates a specialized copy of the supplied Triangles that draws onto this Canvas. 60 | // 61 | // TrianglesPosition, TrianglesColor and TrianglesPicture are supported. 62 | func (c *Canvas) MakeTriangles(t pixel.Triangles) pixel.TargetTriangles { 63 | if gt, ok := t.(*GLTriangles); ok { 64 | return &canvasTriangles{ 65 | GLTriangles: gt, 66 | dst: c, 67 | } 68 | } 69 | return &canvasTriangles{ 70 | GLTriangles: NewGLTriangles(c.shader, t), 71 | dst: c, 72 | } 73 | } 74 | 75 | // MakePicture create a specialized copy of the supplied Picture that draws onto this Canvas. 76 | // 77 | // PictureColor is supported. 78 | func (c *Canvas) MakePicture(p pixel.Picture) pixel.TargetPicture { 79 | if cp, ok := p.(*canvasPicture); ok { 80 | return &canvasPicture{ 81 | GLPicture: cp.GLPicture, 82 | dst: c, 83 | } 84 | } 85 | if gp, ok := p.(GLPicture); ok { 86 | return &canvasPicture{ 87 | GLPicture: gp, 88 | dst: c, 89 | } 90 | } 91 | return &canvasPicture{ 92 | GLPicture: NewGLPicture(p), 93 | dst: c, 94 | } 95 | } 96 | 97 | // SetMatrix sets a Matrix that every point will be projected by. 98 | func (c *Canvas) SetMatrix(m pixel.Matrix) { 99 | // pixel.Matrix is 3x2 with an implicit 0, 0, 1 row after it. So 100 | // [0] [2] [4] [0] [3] [6] 101 | // [1] [3] [5] => [1] [4] [7] 102 | // 0 0 1 0 0 1 103 | // since all matrix ops are affine, the last row never changes, and we don't need to copy it 104 | for i, j := range [...]int{0, 1, 3, 4, 6, 7} { 105 | c.mat[j] = float32(m[i]) 106 | } 107 | } 108 | 109 | // SetColorMask sets a color that every color in triangles or a picture will be multiplied by. 110 | func (c *Canvas) SetColorMask(col color.Color) { 111 | rgba := pixel.Alpha(1) 112 | if col != nil { 113 | rgba = pixel.ToRGBA(col) 114 | } 115 | c.col = mgl32.Vec4{ 116 | float32(rgba.R), 117 | float32(rgba.G), 118 | float32(rgba.B), 119 | float32(rgba.A), 120 | } 121 | } 122 | 123 | // SetComposeMethod sets a Porter-Duff composition method to be used in the following draws onto 124 | // this Canvas. 125 | func (c *Canvas) SetComposeMethod(cmp pixel.ComposeMethod) { 126 | c.cmp = cmp 127 | } 128 | 129 | // SetBounds resizes the Canvas to the new bounds. Old content will be preserved. 130 | func (c *Canvas) SetBounds(bounds pixel.Rect) { 131 | c.gf.SetBounds(bounds) 132 | if c.sprite == nil { 133 | c.sprite = pixel.NewSprite(nil, pixel.Rect{}) 134 | } 135 | c.sprite.Set(c, c.Bounds()) 136 | // c.sprite.SetMatrix(pixel.IM.Moved(c.Bounds().Center())) 137 | } 138 | 139 | // Bounds returns the rectangular bounds of the Canvas. 140 | func (c *Canvas) Bounds() pixel.Rect { 141 | return c.gf.Bounds() 142 | } 143 | 144 | // SetSmooth sets whether stretched Pictures drawn onto this Canvas should be drawn smooth or 145 | // pixely. 146 | func (c *Canvas) SetSmooth(smooth bool) { 147 | c.smooth = smooth 148 | } 149 | 150 | // Smooth returns whether stretched Pictures drawn onto this Canvas are set to be drawn smooth or 151 | // pixely. 152 | func (c *Canvas) Smooth() bool { 153 | return c.smooth 154 | } 155 | 156 | // must be manually called inside mainthread 157 | func (c *Canvas) setGlhfBounds() { 158 | _, _, bw, bh := intBounds(c.gf.Bounds()) 159 | glhf.Bounds(0, 0, bw, bh) 160 | } 161 | 162 | // must be manually called inside mainthread 163 | func setBlendFunc(cmp pixel.ComposeMethod) { 164 | switch cmp { 165 | case pixel.ComposeOver: 166 | glhf.BlendFunc(glhf.One, glhf.OneMinusSrcAlpha) 167 | case pixel.ComposeIn: 168 | glhf.BlendFunc(glhf.DstAlpha, glhf.Zero) 169 | case pixel.ComposeOut: 170 | glhf.BlendFunc(glhf.OneMinusDstAlpha, glhf.Zero) 171 | case pixel.ComposeAtop: 172 | glhf.BlendFunc(glhf.DstAlpha, glhf.OneMinusSrcAlpha) 173 | case pixel.ComposeRover: 174 | glhf.BlendFunc(glhf.OneMinusDstAlpha, glhf.One) 175 | case pixel.ComposeRin: 176 | glhf.BlendFunc(glhf.Zero, glhf.SrcAlpha) 177 | case pixel.ComposeRout: 178 | glhf.BlendFunc(glhf.Zero, glhf.OneMinusSrcAlpha) 179 | case pixel.ComposeRatop: 180 | glhf.BlendFunc(glhf.OneMinusDstAlpha, glhf.SrcAlpha) 181 | case pixel.ComposeXor: 182 | glhf.BlendFunc(glhf.OneMinusDstAlpha, glhf.OneMinusSrcAlpha) 183 | case pixel.ComposePlus: 184 | glhf.BlendFunc(glhf.One, glhf.One) 185 | case pixel.ComposeCopy: 186 | glhf.BlendFunc(glhf.One, glhf.Zero) 187 | default: 188 | panic(errors.New("Canvas: invalid compose method")) 189 | } 190 | } 191 | 192 | // Clear fills the whole Canvas with a single color. 193 | func (c *Canvas) Clear(color color.Color) { 194 | c.gf.Dirty() 195 | 196 | rgba := pixel.ToRGBA(color) 197 | 198 | // color masking 199 | rgba = rgba.Mul(pixel.RGBA{ 200 | R: float64(c.col[0]), 201 | G: float64(c.col[1]), 202 | B: float64(c.col[2]), 203 | A: float64(c.col[3]), 204 | }) 205 | 206 | mainthread.CallNonBlock(func() { 207 | c.setGlhfBounds() 208 | c.gf.Frame().Begin() 209 | glhf.Clear( 210 | float32(rgba.R), 211 | float32(rgba.G), 212 | float32(rgba.B), 213 | float32(rgba.A), 214 | ) 215 | c.gf.Frame().End() 216 | }) 217 | } 218 | 219 | // Color returns the color of the pixel over the given position inside the Canvas. 220 | func (c *Canvas) Color(at pixel.Vec) pixel.RGBA { 221 | return c.gf.Color(at) 222 | } 223 | 224 | // Texture returns the underlying OpenGL Texture of this Canvas. 225 | // 226 | // Implements GLPicture interface. 227 | func (c *Canvas) Texture() *glhf.Texture { 228 | return c.gf.Texture() 229 | } 230 | 231 | // Frame returns the underlying OpenGL Frame of this Canvas. 232 | func (c *Canvas) Frame() *glhf.Frame { 233 | return c.gf.frame 234 | } 235 | 236 | // SetPixels replaces the content of the Canvas with the provided pixels. The provided slice must be 237 | // an alpha-premultiplied RGBA sequence of correct length (4 * width * height). 238 | func (c *Canvas) SetPixels(pixels []uint8) { 239 | c.gf.Dirty() 240 | 241 | mainthread.Call(func() { 242 | tex := c.Texture() 243 | tex.Begin() 244 | tex.SetPixels(0, 0, tex.Width(), tex.Height(), pixels) 245 | tex.End() 246 | }) 247 | } 248 | 249 | // Pixels returns an alpha-premultiplied RGBA sequence of the content of the Canvas. 250 | func (c *Canvas) Pixels() []uint8 { 251 | var pixels []uint8 252 | 253 | mainthread.Call(func() { 254 | tex := c.Texture() 255 | tex.Begin() 256 | pixels = tex.Pixels(0, 0, tex.Width(), tex.Height()) 257 | tex.End() 258 | }) 259 | 260 | return pixels 261 | } 262 | 263 | // Draw draws the content of the Canvas onto another Target, transformed by the given Matrix, just 264 | // like if it was a Sprite containing the whole Canvas. 265 | func (c *Canvas) Draw(t pixel.Target, matrix pixel.Matrix) { 266 | c.sprite.Draw(t, matrix) 267 | } 268 | 269 | // DrawColorMask draws the content of the Canvas onto another Target, transformed by the given 270 | // Matrix and multiplied by the given mask, just like if it was a Sprite containing the whole Canvas. 271 | // 272 | // If the color mask is nil, a fully opaque white mask will be used causing no effect. 273 | func (c *Canvas) DrawColorMask(t pixel.Target, matrix pixel.Matrix, mask color.Color) { 274 | c.sprite.DrawColorMask(t, matrix, mask) 275 | } 276 | 277 | type canvasTriangles struct { 278 | *GLTriangles 279 | dst *Canvas 280 | } 281 | 282 | func (ct *canvasTriangles) draw(tex *glhf.Texture, bounds pixel.Rect) { 283 | ct.dst.gf.Dirty() 284 | 285 | // save the current state vars to avoid race condition 286 | cmp := ct.dst.cmp 287 | smt := ct.dst.smooth 288 | mat := ct.dst.mat 289 | col := ct.dst.col 290 | 291 | mainthread.CallNonBlock(func() { 292 | ct.dst.setGlhfBounds() 293 | setBlendFunc(cmp) 294 | 295 | frame := ct.dst.gf.Frame() 296 | shader := ct.shader.s 297 | 298 | frame.Begin() 299 | shader.Begin() 300 | 301 | ct.shader.uniformDefaults.transform = mat 302 | ct.shader.uniformDefaults.colormask = col 303 | dstBounds := ct.dst.Bounds() 304 | ct.shader.uniformDefaults.bounds = mgl32.Vec4{ 305 | float32(dstBounds.Min.X), 306 | float32(dstBounds.Min.Y), 307 | float32(dstBounds.W()), 308 | float32(dstBounds.H()), 309 | } 310 | 311 | bx, by, bw, bh := intBounds(bounds) 312 | ct.shader.uniformDefaults.texbounds = mgl32.Vec4{ 313 | float32(bx), 314 | float32(by), 315 | float32(bw), 316 | float32(bh), 317 | } 318 | 319 | for loc, u := range ct.shader.uniforms { 320 | ct.shader.s.SetUniformAttr(loc, u.Value()) 321 | } 322 | 323 | if tex == nil { 324 | ct.vs.Begin() 325 | ct.vs.Draw() 326 | ct.vs.End() 327 | } else { 328 | tex.Begin() 329 | 330 | if tex.Smooth() != smt { 331 | tex.SetSmooth(smt) 332 | } 333 | 334 | ct.vs.Begin() 335 | ct.vs.Draw() 336 | ct.vs.End() 337 | 338 | tex.End() 339 | } 340 | 341 | shader.End() 342 | frame.End() 343 | }) 344 | } 345 | 346 | func (ct *canvasTriangles) Draw() { 347 | ct.draw(nil, pixel.Rect{}) 348 | } 349 | 350 | type canvasPicture struct { 351 | GLPicture 352 | dst *Canvas 353 | } 354 | 355 | func (cp *canvasPicture) Draw(t pixel.TargetTriangles) { 356 | ct := t.(*canvasTriangles) 357 | if cp.dst != ct.dst { 358 | panic(fmt.Errorf("(%T).Draw: TargetTriangles generated by different Canvas", cp)) 359 | } 360 | ct.draw(cp.GLPicture.Texture(), cp.GLPicture.Bounds()) 361 | } 362 | -------------------------------------------------------------------------------- /pixelgl/doc.go: -------------------------------------------------------------------------------- 1 | // Package pixelgl implements efficient OpenGL targets and utilities for the Pixel game development 2 | // library, specifically Window and Canvas. 3 | // 4 | // It also contains a few additional utilities to help extend Pixel with OpenGL graphical effects. 5 | package pixelgl 6 | -------------------------------------------------------------------------------- /pixelgl/glframe.go: -------------------------------------------------------------------------------- 1 | package pixelgl 2 | 3 | import ( 4 | "github.com/faiface/glhf" 5 | "github.com/faiface/mainthread" 6 | "github.com/faiface/pixel" 7 | ) 8 | 9 | // GLFrame is a type that helps implementing OpenGL Targets. It implements most common methods to 10 | // avoid code redundancy. It contains an glhf.Frame that you can draw on. 11 | type GLFrame struct { 12 | frame *glhf.Frame 13 | bounds pixel.Rect 14 | pixels []uint8 15 | dirty bool 16 | } 17 | 18 | // NewGLFrame creates a new GLFrame with the given bounds. 19 | func NewGLFrame(bounds pixel.Rect) *GLFrame { 20 | gf := new(GLFrame) 21 | gf.SetBounds(bounds) 22 | return gf 23 | } 24 | 25 | // SetBounds resizes the GLFrame to the new bounds. 26 | func (gf *GLFrame) SetBounds(bounds pixel.Rect) { 27 | if bounds == gf.Bounds() { 28 | return 29 | } 30 | 31 | mainthread.Call(func() { 32 | oldF := gf.frame 33 | 34 | _, _, w, h := intBounds(bounds) 35 | if w <= 0 { 36 | w = 1 37 | } 38 | if h <= 0 { 39 | h = 1 40 | } 41 | gf.frame = glhf.NewFrame(w, h, false) 42 | 43 | // preserve old content 44 | if oldF != nil { 45 | ox, oy, ow, oh := intBounds(bounds) 46 | oldF.Blit( 47 | gf.frame, 48 | ox, oy, ox+ow, oy+oh, 49 | ox, oy, ox+ow, oy+oh, 50 | ) 51 | } 52 | }) 53 | 54 | gf.bounds = bounds 55 | gf.pixels = nil 56 | gf.dirty = true 57 | } 58 | 59 | // Bounds returns the current GLFrame's bounds. 60 | func (gf *GLFrame) Bounds() pixel.Rect { 61 | return gf.bounds 62 | } 63 | 64 | // Color returns the color of the pixel under the specified position. 65 | func (gf *GLFrame) Color(at pixel.Vec) pixel.RGBA { 66 | if gf.dirty { 67 | mainthread.Call(func() { 68 | tex := gf.frame.Texture() 69 | tex.Begin() 70 | gf.pixels = tex.Pixels(0, 0, tex.Width(), tex.Height()) 71 | tex.End() 72 | }) 73 | gf.dirty = false 74 | } 75 | if !gf.bounds.Contains(at) { 76 | return pixel.Alpha(0) 77 | } 78 | bx, by, bw, _ := intBounds(gf.bounds) 79 | x, y := int(at.X)-bx, int(at.Y)-by 80 | off := y*bw + x 81 | return pixel.RGBA{ 82 | R: float64(gf.pixels[off*4+0]) / 255, 83 | G: float64(gf.pixels[off*4+1]) / 255, 84 | B: float64(gf.pixels[off*4+2]) / 255, 85 | A: float64(gf.pixels[off*4+3]) / 255, 86 | } 87 | } 88 | 89 | // Frame returns the GLFrame's Frame that you can draw on. 90 | func (gf *GLFrame) Frame() *glhf.Frame { 91 | return gf.frame 92 | } 93 | 94 | // Texture returns the underlying Texture of the GLFrame's Frame. 95 | // 96 | // Implements GLPicture interface. 97 | func (gf *GLFrame) Texture() *glhf.Texture { 98 | return gf.frame.Texture() 99 | } 100 | 101 | // Dirty marks the GLFrame as changed. Always call this method when you draw onto the GLFrame's 102 | // Frame. 103 | func (gf *GLFrame) Dirty() { 104 | gf.dirty = true 105 | } 106 | -------------------------------------------------------------------------------- /pixelgl/glpicture.go: -------------------------------------------------------------------------------- 1 | package pixelgl 2 | 3 | import ( 4 | "math" 5 | 6 | "github.com/faiface/glhf" 7 | "github.com/faiface/mainthread" 8 | "github.com/faiface/pixel" 9 | ) 10 | 11 | // GLPicture is a pixel.PictureColor with a Texture. All OpenGL Targets should implement and accept 12 | // this interface, because it enables seamless drawing of one to another. 13 | // 14 | // Implementing this interface on an OpenGL Target enables other OpenGL Targets to efficiently draw 15 | // that Target onto them. 16 | type GLPicture interface { 17 | pixel.PictureColor 18 | Texture() *glhf.Texture 19 | } 20 | 21 | // NewGLPicture creates a new GLPicture with it's own static OpenGL texture. This function always 22 | // allocates a new texture that cannot (shouldn't) be further modified. 23 | func NewGLPicture(p pixel.Picture) GLPicture { 24 | bounds := p.Bounds() 25 | bx, by, bw, bh := intBounds(bounds) 26 | 27 | pixels := make([]uint8, 4*bw*bh) 28 | 29 | if pd, ok := p.(*pixel.PictureData); ok { 30 | // PictureData short path 31 | for y := 0; y < bh; y++ { 32 | for x := 0; x < bw; x++ { 33 | rgba := pd.Pix[y*pd.Stride+x] 34 | off := (y*bw + x) * 4 35 | pixels[off+0] = rgba.R 36 | pixels[off+1] = rgba.G 37 | pixels[off+2] = rgba.B 38 | pixels[off+3] = rgba.A 39 | } 40 | } 41 | } else if p, ok := p.(pixel.PictureColor); ok { 42 | for y := 0; y < bh; y++ { 43 | for x := 0; x < bw; x++ { 44 | at := pixel.V( 45 | math.Max(float64(bx+x), bounds.Min.X), 46 | math.Max(float64(by+y), bounds.Min.Y), 47 | ) 48 | color := p.Color(at) 49 | off := (y*bw + x) * 4 50 | pixels[off+0] = uint8(color.R * 255) 51 | pixels[off+1] = uint8(color.G * 255) 52 | pixels[off+2] = uint8(color.B * 255) 53 | pixels[off+3] = uint8(color.A * 255) 54 | } 55 | } 56 | } 57 | 58 | var tex *glhf.Texture 59 | mainthread.Call(func() { 60 | tex = glhf.NewTexture(bw, bh, false, pixels) 61 | }) 62 | 63 | gp := &glPicture{ 64 | bounds: bounds, 65 | tex: tex, 66 | pixels: pixels, 67 | } 68 | return gp 69 | } 70 | 71 | type glPicture struct { 72 | bounds pixel.Rect 73 | tex *glhf.Texture 74 | pixels []uint8 75 | } 76 | 77 | func (gp *glPicture) Bounds() pixel.Rect { 78 | return gp.bounds 79 | } 80 | 81 | func (gp *glPicture) Texture() *glhf.Texture { 82 | return gp.tex 83 | } 84 | 85 | func (gp *glPicture) Color(at pixel.Vec) pixel.RGBA { 86 | if !gp.bounds.Contains(at) { 87 | return pixel.Alpha(0) 88 | } 89 | bx, by, bw, _ := intBounds(gp.bounds) 90 | x, y := int(at.X)-bx, int(at.Y)-by 91 | off := y*bw + x 92 | return pixel.RGBA{ 93 | R: float64(gp.pixels[off*4+0]) / 255, 94 | G: float64(gp.pixels[off*4+1]) / 255, 95 | B: float64(gp.pixels[off*4+2]) / 255, 96 | A: float64(gp.pixels[off*4+3]) / 255, 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /pixelgl/glshader.go: -------------------------------------------------------------------------------- 1 | package pixelgl 2 | 3 | import ( 4 | "github.com/faiface/glhf" 5 | "github.com/faiface/mainthread" 6 | "github.com/go-gl/mathgl/mgl32" 7 | "github.com/pkg/errors" 8 | ) 9 | 10 | // GLShader is a type to assist with managing a canvas's underlying 11 | // shader configuration. This allows for customization of shaders on 12 | // a per canvas basis. 13 | type GLShader struct { 14 | s *glhf.Shader 15 | vf, uf glhf.AttrFormat 16 | vs, fs string 17 | 18 | uniforms []gsUniformAttr 19 | 20 | uniformDefaults struct { 21 | transform mgl32.Mat3 22 | colormask mgl32.Vec4 23 | bounds mgl32.Vec4 24 | texbounds mgl32.Vec4 25 | cliprect mgl32.Vec4 26 | } 27 | } 28 | 29 | type gsUniformAttr struct { 30 | Name string 31 | Type glhf.AttrType 32 | value interface{} 33 | ispointer bool 34 | } 35 | 36 | const ( 37 | canvasPosition int = iota 38 | canvasColor 39 | canvasTexCoords 40 | canvasIntensity 41 | canvasClip 42 | ) 43 | 44 | var defaultCanvasVertexFormat = glhf.AttrFormat{ 45 | canvasPosition: glhf.Attr{Name: "aPosition", Type: glhf.Vec2}, 46 | canvasColor: glhf.Attr{Name: "aColor", Type: glhf.Vec4}, 47 | canvasTexCoords: glhf.Attr{Name: "aTexCoords", Type: glhf.Vec2}, 48 | canvasIntensity: glhf.Attr{Name: "aIntensity", Type: glhf.Float}, 49 | canvasClip: glhf.Attr{Name: "aClipRect", Type: glhf.Vec4}, 50 | } 51 | 52 | // Sets up a base shader with everything needed for a Pixel 53 | // canvas to render correctly. The defaults can be overridden 54 | // by simply using the SetUniform function. 55 | func NewGLShader(fragmentShader string) *GLShader { 56 | gs := &GLShader{ 57 | vf: defaultCanvasVertexFormat, 58 | vs: baseCanvasVertexShader, 59 | fs: fragmentShader, 60 | } 61 | 62 | gs.SetUniform("uTransform", &gs.uniformDefaults.transform) 63 | gs.SetUniform("uColorMask", &gs.uniformDefaults.colormask) 64 | gs.SetUniform("uBounds", &gs.uniformDefaults.bounds) 65 | gs.SetUniform("uTexBounds", &gs.uniformDefaults.texbounds) 66 | 67 | gs.Update() 68 | 69 | return gs 70 | } 71 | 72 | // Update reinitialize GLShader data and recompile the underlying gl shader object 73 | func (gs *GLShader) Update() { 74 | gs.uf = make([]glhf.Attr, len(gs.uniforms)) 75 | for idx := range gs.uniforms { 76 | gs.uf[idx] = glhf.Attr{ 77 | Name: gs.uniforms[idx].Name, 78 | Type: gs.uniforms[idx].Type, 79 | } 80 | } 81 | 82 | var shader *glhf.Shader 83 | mainthread.Call(func() { 84 | var err error 85 | shader, err = glhf.NewShader( 86 | gs.vf, 87 | gs.uf, 88 | gs.vs, 89 | gs.fs, 90 | ) 91 | if err != nil { 92 | panic(errors.Wrap(err, "failed to create Canvas, there's a bug in the shader")) 93 | } 94 | }) 95 | 96 | gs.s = shader 97 | } 98 | 99 | // gets the uniform index from GLShader 100 | func (gs *GLShader) getUniform(Name string) int { 101 | for i, u := range gs.uniforms { 102 | if u.Name == Name { 103 | return i 104 | } 105 | } 106 | return -1 107 | } 108 | 109 | // SetUniform appends a custom uniform name and value to the shader. 110 | // if the uniform already exists, it will simply be overwritten. 111 | // 112 | // example: 113 | // 114 | // utime := float32(time.Since(starttime)).Seconds()) 115 | // mycanvas.shader.AddUniform("u_time", &utime) 116 | func (gs *GLShader) SetUniform(name string, value interface{}) { 117 | t, p := getAttrType(value) 118 | if loc := gs.getUniform(name); loc > -1 { 119 | gs.uniforms[loc].Name = name 120 | gs.uniforms[loc].Type = t 121 | gs.uniforms[loc].ispointer = p 122 | gs.uniforms[loc].value = value 123 | return 124 | } 125 | gs.uniforms = append(gs.uniforms, gsUniformAttr{ 126 | Name: name, 127 | Type: t, 128 | ispointer: p, 129 | value: value, 130 | }) 131 | } 132 | 133 | // Value returns the attribute's concrete value. If the stored value 134 | // is a pointer, we return the dereferenced value. 135 | func (gu *gsUniformAttr) Value() interface{} { 136 | if !gu.ispointer { 137 | return gu.value 138 | } 139 | switch gu.Type { 140 | case glhf.Vec2: 141 | return *gu.value.(*mgl32.Vec2) 142 | case glhf.Vec3: 143 | return *gu.value.(*mgl32.Vec3) 144 | case glhf.Vec4: 145 | return *gu.value.(*mgl32.Vec4) 146 | case glhf.Mat2: 147 | return *gu.value.(*mgl32.Mat2) 148 | case glhf.Mat23: 149 | return *gu.value.(*mgl32.Mat2x3) 150 | case glhf.Mat24: 151 | return *gu.value.(*mgl32.Mat2x4) 152 | case glhf.Mat3: 153 | return *gu.value.(*mgl32.Mat3) 154 | case glhf.Mat32: 155 | return *gu.value.(*mgl32.Mat3x2) 156 | case glhf.Mat34: 157 | return *gu.value.(*mgl32.Mat3x4) 158 | case glhf.Mat4: 159 | return *gu.value.(*mgl32.Mat4) 160 | case glhf.Mat42: 161 | return *gu.value.(*mgl32.Mat4x2) 162 | case glhf.Mat43: 163 | return *gu.value.(*mgl32.Mat4x3) 164 | case glhf.Int: 165 | return *gu.value.(*int32) 166 | case glhf.Float: 167 | return *gu.value.(*float32) 168 | default: 169 | panic("invalid attrtype") 170 | } 171 | } 172 | 173 | // Returns the type identifier for any (supported) attribute variable type 174 | // and whether or not it is a pointer of that type. 175 | func getAttrType(v interface{}) (glhf.AttrType, bool) { 176 | switch v.(type) { 177 | case int32: 178 | return glhf.Int, false 179 | case float32: 180 | return glhf.Float, false 181 | case mgl32.Vec2: 182 | return glhf.Vec2, false 183 | case mgl32.Vec3: 184 | return glhf.Vec3, false 185 | case mgl32.Vec4: 186 | return glhf.Vec4, false 187 | case mgl32.Mat2: 188 | return glhf.Mat2, false 189 | case mgl32.Mat2x3: 190 | return glhf.Mat23, false 191 | case mgl32.Mat2x4: 192 | return glhf.Mat24, false 193 | case mgl32.Mat3: 194 | return glhf.Mat3, false 195 | case mgl32.Mat3x2: 196 | return glhf.Mat32, false 197 | case mgl32.Mat3x4: 198 | return glhf.Mat34, false 199 | case mgl32.Mat4: 200 | return glhf.Mat4, false 201 | case mgl32.Mat4x2: 202 | return glhf.Mat42, false 203 | case mgl32.Mat4x3: 204 | return glhf.Mat43, false 205 | case *mgl32.Vec2: 206 | return glhf.Vec2, true 207 | case *mgl32.Vec3: 208 | return glhf.Vec3, true 209 | case *mgl32.Vec4: 210 | return glhf.Vec4, true 211 | case *mgl32.Mat2: 212 | return glhf.Mat2, true 213 | case *mgl32.Mat2x3: 214 | return glhf.Mat23, true 215 | case *mgl32.Mat2x4: 216 | return glhf.Mat24, true 217 | case *mgl32.Mat3: 218 | return glhf.Mat3, true 219 | case *mgl32.Mat3x2: 220 | return glhf.Mat32, true 221 | case *mgl32.Mat3x4: 222 | return glhf.Mat34, true 223 | case *mgl32.Mat4: 224 | return glhf.Mat4, true 225 | case *mgl32.Mat4x2: 226 | return glhf.Mat42, true 227 | case *mgl32.Mat4x3: 228 | return glhf.Mat43, true 229 | case *int32: 230 | return glhf.Int, true 231 | case *float32: 232 | return glhf.Float, true 233 | default: 234 | panic("invalid AttrType") 235 | } 236 | } 237 | 238 | var baseCanvasVertexShader = ` 239 | #version 330 core 240 | 241 | in vec2 aPosition; 242 | in vec4 aColor; 243 | in vec2 aTexCoords; 244 | in float aIntensity; 245 | in vec4 aClipRect; 246 | in float aIsClipped; 247 | 248 | out vec4 vColor; 249 | out vec2 vTexCoords; 250 | out float vIntensity; 251 | out vec2 vPosition; 252 | out vec4 vClipRect; 253 | 254 | uniform mat3 uTransform; 255 | uniform vec4 uBounds; 256 | 257 | void main() { 258 | vec2 transPos = (uTransform * vec3(aPosition, 1.0)).xy; 259 | vec2 normPos = (transPos - uBounds.xy) / uBounds.zw * 2 - vec2(1, 1); 260 | gl_Position = vec4(normPos, 0.0, 1.0); 261 | 262 | vColor = aColor; 263 | vPosition = aPosition; 264 | vTexCoords = aTexCoords; 265 | vIntensity = aIntensity; 266 | vClipRect = aClipRect; 267 | } 268 | ` 269 | 270 | var baseCanvasFragmentShader = ` 271 | #version 330 core 272 | 273 | in vec4 vColor; 274 | in vec2 vTexCoords; 275 | in float vIntensity; 276 | in vec4 vClipRect; 277 | 278 | out vec4 fragColor; 279 | 280 | uniform vec4 uColorMask; 281 | uniform vec4 uTexBounds; 282 | uniform sampler2D uTexture; 283 | 284 | void main() { 285 | if ((vClipRect != vec4(0,0,0,0)) && (gl_FragCoord.x < vClipRect.x || gl_FragCoord.y < vClipRect.y || gl_FragCoord.x > vClipRect.z || gl_FragCoord.y > vClipRect.w)) 286 | discard; 287 | 288 | if (vIntensity == 0) { 289 | fragColor = uColorMask * vColor; 290 | } else { 291 | fragColor = vec4(0, 0, 0, 0); 292 | fragColor += (1 - vIntensity) * vColor; 293 | vec2 t = (vTexCoords - uTexBounds.xy) / uTexBounds.zw; 294 | fragColor += vIntensity * vColor * texture(uTexture, t); 295 | fragColor *= uColorMask; 296 | } 297 | } 298 | ` 299 | -------------------------------------------------------------------------------- /pixelgl/gltriangles.go: -------------------------------------------------------------------------------- 1 | package pixelgl 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/faiface/glhf" 7 | "github.com/faiface/mainthread" 8 | "github.com/faiface/pixel" 9 | ) 10 | 11 | // GLTriangles are OpenGL triangles implemented using glhf.VertexSlice. 12 | // 13 | // Triangles returned from this function support TrianglesPosition, TrianglesColor and 14 | // TrianglesPicture. If you need to support more, you can "override" SetLen and Update methods. 15 | type GLTriangles struct { 16 | vs *glhf.VertexSlice 17 | data []float32 18 | shader *GLShader 19 | } 20 | 21 | var ( 22 | _ pixel.TrianglesPosition = (*GLTriangles)(nil) 23 | _ pixel.TrianglesColor = (*GLTriangles)(nil) 24 | _ pixel.TrianglesPicture = (*GLTriangles)(nil) 25 | _ pixel.TrianglesClipped = (*GLTriangles)(nil) 26 | ) 27 | 28 | // The following is a helper so that the indices of 29 | // each of these items is easier to see/debug. 30 | const ( 31 | triPosX = iota 32 | triPosY 33 | triColorR 34 | triColorG 35 | triColorB 36 | triColorA 37 | triPicX 38 | triPicY 39 | triIntensity 40 | triClipMinX 41 | triClipMinY 42 | triClipMaxX 43 | triClipMaxY 44 | trisAttrLen 45 | ) 46 | 47 | // NewGLTriangles returns GLTriangles initialized with the data from the supplied Triangles. 48 | // 49 | // Only draw the Triangles using the provided Shader. 50 | func NewGLTriangles(shader *GLShader, t pixel.Triangles) *GLTriangles { 51 | var gt *GLTriangles 52 | mainthread.Call(func() { 53 | gt = &GLTriangles{ 54 | vs: glhf.MakeVertexSlice(shader.s, 0, t.Len()), 55 | shader: shader, 56 | } 57 | }) 58 | gt.SetLen(t.Len()) 59 | gt.Update(t) 60 | return gt 61 | } 62 | 63 | // VertexSlice returns the VertexSlice of this GLTriangles. 64 | // 65 | // You can use it to draw them. 66 | func (gt *GLTriangles) VertexSlice() *glhf.VertexSlice { 67 | return gt.vs 68 | } 69 | 70 | // Shader returns the GLTriangles's associated shader. 71 | func (gt *GLTriangles) Shader() *GLShader { 72 | return gt.shader 73 | } 74 | 75 | // Len returns the number of vertices. 76 | func (gt *GLTriangles) Len() int { 77 | return len(gt.data) / gt.vs.Stride() 78 | } 79 | 80 | // SetLen efficiently resizes GLTriangles to len. 81 | // 82 | // Time complexity is amortized O(1). 83 | func (gt *GLTriangles) SetLen(length int) { 84 | switch { 85 | case length > gt.Len(): 86 | needAppend := length - gt.Len() 87 | for i := 0; i < needAppend; i++ { 88 | gt.data = append(gt.data, 89 | 0, 0, 90 | 1, 1, 1, 1, 91 | 0, 0, 92 | 0, 93 | 0, 0, 0, 0, 94 | ) 95 | } 96 | case length < gt.Len(): 97 | gt.data = gt.data[:length*gt.vs.Stride()] 98 | default: 99 | return 100 | } 101 | mainthread.Call(func() { 102 | gt.vs.Begin() 103 | gt.vs.SetLen(length) 104 | gt.vs.End() 105 | }) 106 | } 107 | 108 | // Slice returns a sub-Triangles of this GLTriangles in range [i, j). 109 | func (gt *GLTriangles) Slice(i, j int) pixel.Triangles { 110 | return &GLTriangles{ 111 | vs: gt.vs.Slice(i, j), 112 | data: gt.data[i*gt.vs.Stride() : j*gt.vs.Stride()], 113 | shader: gt.shader, 114 | } 115 | } 116 | 117 | func (gt *GLTriangles) updateData(t pixel.Triangles) { 118 | // glTriangles short path 119 | if t, ok := t.(*GLTriangles); ok { 120 | copy(gt.data, t.data) 121 | return 122 | } 123 | 124 | // TrianglesData short path 125 | stride := gt.vs.Stride() 126 | length := gt.Len() 127 | if t, ok := t.(*pixel.TrianglesData); ok { 128 | for i := 0; i < length; i++ { 129 | var ( 130 | px, py = (*t)[i].Position.XY() 131 | col = (*t)[i].Color 132 | tx, ty = (*t)[i].Picture.XY() 133 | in = (*t)[i].Intensity 134 | rec = (*t)[i].ClipRect 135 | ) 136 | d := gt.data[i*stride : i*stride+trisAttrLen] 137 | d[triPosX] = float32(px) 138 | d[triPosY] = float32(py) 139 | d[triColorR] = float32(col.R) 140 | d[triColorG] = float32(col.G) 141 | d[triColorB] = float32(col.B) 142 | d[triColorA] = float32(col.A) 143 | d[triPicX] = float32(tx) 144 | d[triPicY] = float32(ty) 145 | d[triIntensity] = float32(in) 146 | d[triClipMinX] = float32(rec.Min.X) 147 | d[triClipMinY] = float32(rec.Min.Y) 148 | d[triClipMaxX] = float32(rec.Max.X) 149 | d[triClipMaxY] = float32(rec.Max.Y) 150 | } 151 | return 152 | } 153 | 154 | if t, ok := t.(pixel.TrianglesPosition); ok { 155 | for i := 0; i < length; i++ { 156 | px, py := t.Position(i).XY() 157 | gt.data[i*stride+triPosX] = float32(px) 158 | gt.data[i*stride+triPosY] = float32(py) 159 | } 160 | } 161 | if t, ok := t.(pixel.TrianglesColor); ok { 162 | for i := 0; i < length; i++ { 163 | col := t.Color(i) 164 | gt.data[i*stride+triColorR] = float32(col.R) 165 | gt.data[i*stride+triColorG] = float32(col.G) 166 | gt.data[i*stride+triColorB] = float32(col.B) 167 | gt.data[i*stride+triColorA] = float32(col.A) 168 | } 169 | } 170 | if t, ok := t.(pixel.TrianglesPicture); ok { 171 | for i := 0; i < length; i++ { 172 | pic, intensity := t.Picture(i) 173 | gt.data[i*stride+triPicX] = float32(pic.X) 174 | gt.data[i*stride+triPicY] = float32(pic.Y) 175 | gt.data[i*stride+triIntensity] = float32(intensity) 176 | } 177 | } 178 | if t, ok := t.(pixel.TrianglesClipped); ok { 179 | for i := 0; i < length; i++ { 180 | rect, _ := t.ClipRect(i) 181 | gt.data[i*stride+triClipMinX] = float32(rect.Min.X) 182 | gt.data[i*stride+triClipMinY] = float32(rect.Min.Y) 183 | gt.data[i*stride+triClipMaxX] = float32(rect.Max.X) 184 | gt.data[i*stride+triClipMaxY] = float32(rect.Max.Y) 185 | } 186 | } 187 | } 188 | 189 | // Update copies vertex properties from the supplied Triangles into this GLTriangles. 190 | // 191 | // The two Triangles (gt and t) must be of the same len. 192 | func (gt *GLTriangles) Update(t pixel.Triangles) { 193 | if gt.Len() != t.Len() { 194 | panic(fmt.Errorf("(%T).Update: invalid triangles len", gt)) 195 | } 196 | gt.updateData(t) 197 | 198 | // Copy the verteces down to the glhf.VertexData 199 | gt.CopyVertices() 200 | } 201 | 202 | // CopyVertices copies the GLTriangle data down to the vertex data. 203 | func (gt *GLTriangles) CopyVertices() { 204 | // this code is supposed to copy the vertex data and CallNonBlock the update if 205 | // the data is small enough, otherwise it'll block and not copy the data 206 | if len(gt.data) < 256 { // arbitrary heurestic constant 207 | data := append([]float32{}, gt.data...) 208 | mainthread.CallNonBlock(func() { 209 | gt.vs.Begin() 210 | gt.vs.SetVertexData(data) 211 | gt.vs.End() 212 | }) 213 | } else { 214 | mainthread.Call(func() { 215 | gt.vs.Begin() 216 | gt.vs.SetVertexData(gt.data) 217 | gt.vs.End() 218 | }) 219 | } 220 | } 221 | 222 | // Copy returns an independent copy of this GLTriangles. 223 | // 224 | // The returned Triangles are *GLTriangles as the underlying type. 225 | func (gt *GLTriangles) Copy() pixel.Triangles { 226 | return NewGLTriangles(gt.shader, gt) 227 | } 228 | 229 | // index is a helper function that returns the index in the data 230 | // slice given the i-th vertex and the item index. 231 | func (gt *GLTriangles) index(i, idx int) int { 232 | return i*gt.vs.Stride() + idx 233 | } 234 | 235 | // Position returns the Position property of the i-th vertex. 236 | func (gt *GLTriangles) Position(i int) pixel.Vec { 237 | px := gt.data[gt.index(i, triPosX)] 238 | py := gt.data[gt.index(i, triPosY)] 239 | return pixel.V(float64(px), float64(py)) 240 | } 241 | 242 | // SetPosition sets the position property of the i-th vertex. 243 | func (gt *GLTriangles) SetPosition(i int, p pixel.Vec) { 244 | gt.data[gt.index(i, triPosX)] = float32(p.X) 245 | gt.data[gt.index(i, triPosY)] = float32(p.Y) 246 | } 247 | 248 | // Color returns the Color property of the i-th vertex. 249 | func (gt *GLTriangles) Color(i int) pixel.RGBA { 250 | r := gt.data[gt.index(i, triColorR)] 251 | g := gt.data[gt.index(i, triColorG)] 252 | b := gt.data[gt.index(i, triColorB)] 253 | a := gt.data[gt.index(i, triColorA)] 254 | return pixel.RGBA{ 255 | R: float64(r), 256 | G: float64(g), 257 | B: float64(b), 258 | A: float64(a), 259 | } 260 | } 261 | 262 | // SetColor sets the color property of the i-th vertex. 263 | func (gt *GLTriangles) SetColor(i int, c pixel.RGBA) { 264 | gt.data[gt.index(i, triColorR)] = float32(c.R) 265 | gt.data[gt.index(i, triColorG)] = float32(c.G) 266 | gt.data[gt.index(i, triColorB)] = float32(c.B) 267 | gt.data[gt.index(i, triColorA)] = float32(c.A) 268 | } 269 | 270 | // Picture returns the Picture property of the i-th vertex. 271 | func (gt *GLTriangles) Picture(i int) (pic pixel.Vec, intensity float64) { 272 | tx := gt.data[gt.index(i, triPicX)] 273 | ty := gt.data[gt.index(i, triPicY)] 274 | intensity = float64(gt.data[gt.index(i, triIntensity)]) 275 | return pixel.V(float64(tx), float64(ty)), intensity 276 | } 277 | 278 | // SetPicture sets the picture property of the i-th vertex. 279 | func (gt *GLTriangles) SetPicture(i int, pic pixel.Vec, intensity float64) { 280 | gt.data[gt.index(i, triPicX)] = float32(pic.X) 281 | gt.data[gt.index(i, triPicY)] = float32(pic.Y) 282 | gt.data[gt.index(i, triIntensity)] = float32(intensity) 283 | } 284 | 285 | // ClipRect returns the Clipping rectangle property of the i-th vertex. 286 | func (gt *GLTriangles) ClipRect(i int) (rect pixel.Rect, is bool) { 287 | mx := gt.data[gt.index(i, triClipMinX)] 288 | my := gt.data[gt.index(i, triClipMinY)] 289 | ax := gt.data[gt.index(i, triClipMaxX)] 290 | ay := gt.data[gt.index(i, triClipMaxY)] 291 | rect = pixel.R(float64(mx), float64(my), float64(ax), float64(ay)) 292 | is = rect.Area() != 0.0 293 | return 294 | } 295 | 296 | // SetClipRect sets the Clipping rectangle property of the i-th vertex. 297 | func (gt *GLTriangles) SetClipRect(i int, rect pixel.Rect) { 298 | gt.data[gt.index(i, triClipMinX)] = float32(rect.Min.X) 299 | gt.data[gt.index(i, triClipMinY)] = float32(rect.Min.Y) 300 | gt.data[gt.index(i, triClipMaxX)] = float32(rect.Max.X) 301 | gt.data[gt.index(i, triClipMaxY)] = float32(rect.Max.Y) 302 | } 303 | -------------------------------------------------------------------------------- /pixelgl/input.go: -------------------------------------------------------------------------------- 1 | package pixelgl 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/faiface/mainthread" 7 | "github.com/faiface/pixel" 8 | "github.com/go-gl/glfw/v3.3/glfw" 9 | ) 10 | 11 | // Pressed returns whether the Button is currently pressed down. 12 | func (w *Window) Pressed(button Button) bool { 13 | return w.currInp.buttons[button] 14 | } 15 | 16 | // JustPressed returns whether the Button has been pressed in the last frame. 17 | func (w *Window) JustPressed(button Button) bool { 18 | return w.pressEvents[button] 19 | } 20 | 21 | // JustReleased returns whether the Button has been released in the last frame. 22 | func (w *Window) JustReleased(button Button) bool { 23 | return w.releaseEvents[button] 24 | } 25 | 26 | // Repeated returns whether a repeat event has been triggered on button. 27 | // 28 | // Repeat event occurs repeatedly when a button is held down for some time. 29 | func (w *Window) Repeated(button Button) bool { 30 | return w.currInp.repeat[button] 31 | } 32 | 33 | // MousePosition returns the current mouse position in the Window's Bounds. 34 | func (w *Window) MousePosition() pixel.Vec { 35 | return w.currInp.mouse 36 | } 37 | 38 | // MousePreviousPosition returns the previous mouse position in the Window's Bounds. 39 | func (w *Window) MousePreviousPosition() pixel.Vec { 40 | return w.prevInp.mouse 41 | } 42 | 43 | // SetMousePosition positions the mouse cursor anywhere within the Window's Bounds. 44 | func (w *Window) SetMousePosition(v pixel.Vec) { 45 | mainthread.Call(func() { 46 | if (v.X >= 0 && v.X <= w.bounds.W()) && 47 | (v.Y >= 0 && v.Y <= w.bounds.H()) { 48 | w.window.SetCursorPos( 49 | v.X+w.bounds.Min.X, 50 | (w.bounds.H()-v.Y)+w.bounds.Min.Y, 51 | ) 52 | w.prevInp.mouse = v 53 | w.currInp.mouse = v 54 | w.tempInp.mouse = v 55 | } 56 | }) 57 | } 58 | 59 | // MouseInsideWindow returns true if the mouse position is within the Window's Bounds. 60 | func (w *Window) MouseInsideWindow() bool { 61 | return w.cursorInsideWindow 62 | } 63 | 64 | // MouseScroll returns the mouse scroll amount (in both axes) since the last call to Window.Update. 65 | func (w *Window) MouseScroll() pixel.Vec { 66 | return w.currInp.scroll 67 | } 68 | 69 | // Typed returns the text typed on the keyboard since the last call to Window.Update. 70 | func (w *Window) Typed() string { 71 | return w.currInp.typed 72 | } 73 | 74 | // Button is a keyboard or mouse button. Why distinguish? 75 | type Button int 76 | 77 | // List of all mouse buttons. 78 | const ( 79 | MouseButton1 = Button(glfw.MouseButton1) 80 | MouseButton2 = Button(glfw.MouseButton2) 81 | MouseButton3 = Button(glfw.MouseButton3) 82 | MouseButton4 = Button(glfw.MouseButton4) 83 | MouseButton5 = Button(glfw.MouseButton5) 84 | MouseButton6 = Button(glfw.MouseButton6) 85 | MouseButton7 = Button(glfw.MouseButton7) 86 | MouseButton8 = Button(glfw.MouseButton8) 87 | MouseButtonLast = Button(glfw.MouseButtonLast) 88 | MouseButtonLeft = Button(glfw.MouseButtonLeft) 89 | MouseButtonRight = Button(glfw.MouseButtonRight) 90 | MouseButtonMiddle = Button(glfw.MouseButtonMiddle) 91 | ) 92 | 93 | // List of all keyboard buttons. 94 | const ( 95 | KeyUnknown = Button(glfw.KeyUnknown) 96 | KeySpace = Button(glfw.KeySpace) 97 | KeyApostrophe = Button(glfw.KeyApostrophe) 98 | KeyComma = Button(glfw.KeyComma) 99 | KeyMinus = Button(glfw.KeyMinus) 100 | KeyPeriod = Button(glfw.KeyPeriod) 101 | KeySlash = Button(glfw.KeySlash) 102 | Key0 = Button(glfw.Key0) 103 | Key1 = Button(glfw.Key1) 104 | Key2 = Button(glfw.Key2) 105 | Key3 = Button(glfw.Key3) 106 | Key4 = Button(glfw.Key4) 107 | Key5 = Button(glfw.Key5) 108 | Key6 = Button(glfw.Key6) 109 | Key7 = Button(glfw.Key7) 110 | Key8 = Button(glfw.Key8) 111 | Key9 = Button(glfw.Key9) 112 | KeySemicolon = Button(glfw.KeySemicolon) 113 | KeyEqual = Button(glfw.KeyEqual) 114 | KeyA = Button(glfw.KeyA) 115 | KeyB = Button(glfw.KeyB) 116 | KeyC = Button(glfw.KeyC) 117 | KeyD = Button(glfw.KeyD) 118 | KeyE = Button(glfw.KeyE) 119 | KeyF = Button(glfw.KeyF) 120 | KeyG = Button(glfw.KeyG) 121 | KeyH = Button(glfw.KeyH) 122 | KeyI = Button(glfw.KeyI) 123 | KeyJ = Button(glfw.KeyJ) 124 | KeyK = Button(glfw.KeyK) 125 | KeyL = Button(glfw.KeyL) 126 | KeyM = Button(glfw.KeyM) 127 | KeyN = Button(glfw.KeyN) 128 | KeyO = Button(glfw.KeyO) 129 | KeyP = Button(glfw.KeyP) 130 | KeyQ = Button(glfw.KeyQ) 131 | KeyR = Button(glfw.KeyR) 132 | KeyS = Button(glfw.KeyS) 133 | KeyT = Button(glfw.KeyT) 134 | KeyU = Button(glfw.KeyU) 135 | KeyV = Button(glfw.KeyV) 136 | KeyW = Button(glfw.KeyW) 137 | KeyX = Button(glfw.KeyX) 138 | KeyY = Button(glfw.KeyY) 139 | KeyZ = Button(glfw.KeyZ) 140 | KeyLeftBracket = Button(glfw.KeyLeftBracket) 141 | KeyBackslash = Button(glfw.KeyBackslash) 142 | KeyRightBracket = Button(glfw.KeyRightBracket) 143 | KeyGraveAccent = Button(glfw.KeyGraveAccent) 144 | KeyWorld1 = Button(glfw.KeyWorld1) 145 | KeyWorld2 = Button(glfw.KeyWorld2) 146 | KeyEscape = Button(glfw.KeyEscape) 147 | KeyEnter = Button(glfw.KeyEnter) 148 | KeyTab = Button(glfw.KeyTab) 149 | KeyBackspace = Button(glfw.KeyBackspace) 150 | KeyInsert = Button(glfw.KeyInsert) 151 | KeyDelete = Button(glfw.KeyDelete) 152 | KeyRight = Button(glfw.KeyRight) 153 | KeyLeft = Button(glfw.KeyLeft) 154 | KeyDown = Button(glfw.KeyDown) 155 | KeyUp = Button(glfw.KeyUp) 156 | KeyPageUp = Button(glfw.KeyPageUp) 157 | KeyPageDown = Button(glfw.KeyPageDown) 158 | KeyHome = Button(glfw.KeyHome) 159 | KeyEnd = Button(glfw.KeyEnd) 160 | KeyCapsLock = Button(glfw.KeyCapsLock) 161 | KeyScrollLock = Button(glfw.KeyScrollLock) 162 | KeyNumLock = Button(glfw.KeyNumLock) 163 | KeyPrintScreen = Button(glfw.KeyPrintScreen) 164 | KeyPause = Button(glfw.KeyPause) 165 | KeyF1 = Button(glfw.KeyF1) 166 | KeyF2 = Button(glfw.KeyF2) 167 | KeyF3 = Button(glfw.KeyF3) 168 | KeyF4 = Button(glfw.KeyF4) 169 | KeyF5 = Button(glfw.KeyF5) 170 | KeyF6 = Button(glfw.KeyF6) 171 | KeyF7 = Button(glfw.KeyF7) 172 | KeyF8 = Button(glfw.KeyF8) 173 | KeyF9 = Button(glfw.KeyF9) 174 | KeyF10 = Button(glfw.KeyF10) 175 | KeyF11 = Button(glfw.KeyF11) 176 | KeyF12 = Button(glfw.KeyF12) 177 | KeyF13 = Button(glfw.KeyF13) 178 | KeyF14 = Button(glfw.KeyF14) 179 | KeyF15 = Button(glfw.KeyF15) 180 | KeyF16 = Button(glfw.KeyF16) 181 | KeyF17 = Button(glfw.KeyF17) 182 | KeyF18 = Button(glfw.KeyF18) 183 | KeyF19 = Button(glfw.KeyF19) 184 | KeyF20 = Button(glfw.KeyF20) 185 | KeyF21 = Button(glfw.KeyF21) 186 | KeyF22 = Button(glfw.KeyF22) 187 | KeyF23 = Button(glfw.KeyF23) 188 | KeyF24 = Button(glfw.KeyF24) 189 | KeyF25 = Button(glfw.KeyF25) 190 | KeyKP0 = Button(glfw.KeyKP0) 191 | KeyKP1 = Button(glfw.KeyKP1) 192 | KeyKP2 = Button(glfw.KeyKP2) 193 | KeyKP3 = Button(glfw.KeyKP3) 194 | KeyKP4 = Button(glfw.KeyKP4) 195 | KeyKP5 = Button(glfw.KeyKP5) 196 | KeyKP6 = Button(glfw.KeyKP6) 197 | KeyKP7 = Button(glfw.KeyKP7) 198 | KeyKP8 = Button(glfw.KeyKP8) 199 | KeyKP9 = Button(glfw.KeyKP9) 200 | KeyKPDecimal = Button(glfw.KeyKPDecimal) 201 | KeyKPDivide = Button(glfw.KeyKPDivide) 202 | KeyKPMultiply = Button(glfw.KeyKPMultiply) 203 | KeyKPSubtract = Button(glfw.KeyKPSubtract) 204 | KeyKPAdd = Button(glfw.KeyKPAdd) 205 | KeyKPEnter = Button(glfw.KeyKPEnter) 206 | KeyKPEqual = Button(glfw.KeyKPEqual) 207 | KeyLeftShift = Button(glfw.KeyLeftShift) 208 | KeyLeftControl = Button(glfw.KeyLeftControl) 209 | KeyLeftAlt = Button(glfw.KeyLeftAlt) 210 | KeyLeftSuper = Button(glfw.KeyLeftSuper) 211 | KeyRightShift = Button(glfw.KeyRightShift) 212 | KeyRightControl = Button(glfw.KeyRightControl) 213 | KeyRightAlt = Button(glfw.KeyRightAlt) 214 | KeyRightSuper = Button(glfw.KeyRightSuper) 215 | KeyMenu = Button(glfw.KeyMenu) 216 | KeyLast = Button(glfw.KeyLast) 217 | ) 218 | 219 | // String returns a human-readable string describing the Button. 220 | func (b Button) String() string { 221 | name, ok := buttonNames[b] 222 | if !ok { 223 | return "Invalid" 224 | } 225 | return name 226 | } 227 | 228 | var buttonNames = map[Button]string{ 229 | MouseButton4: "MouseButton4", 230 | MouseButton5: "MouseButton5", 231 | MouseButton6: "MouseButton6", 232 | MouseButton7: "MouseButton7", 233 | MouseButton8: "MouseButton8", 234 | MouseButtonLeft: "MouseButtonLeft", 235 | MouseButtonRight: "MouseButtonRight", 236 | MouseButtonMiddle: "MouseButtonMiddle", 237 | KeyUnknown: "Unknown", 238 | KeySpace: "Space", 239 | KeyApostrophe: "Apostrophe", 240 | KeyComma: "Comma", 241 | KeyMinus: "Minus", 242 | KeyPeriod: "Period", 243 | KeySlash: "Slash", 244 | Key0: "0", 245 | Key1: "1", 246 | Key2: "2", 247 | Key3: "3", 248 | Key4: "4", 249 | Key5: "5", 250 | Key6: "6", 251 | Key7: "7", 252 | Key8: "8", 253 | Key9: "9", 254 | KeySemicolon: "Semicolon", 255 | KeyEqual: "Equal", 256 | KeyA: "A", 257 | KeyB: "B", 258 | KeyC: "C", 259 | KeyD: "D", 260 | KeyE: "E", 261 | KeyF: "F", 262 | KeyG: "G", 263 | KeyH: "H", 264 | KeyI: "I", 265 | KeyJ: "J", 266 | KeyK: "K", 267 | KeyL: "L", 268 | KeyM: "M", 269 | KeyN: "N", 270 | KeyO: "O", 271 | KeyP: "P", 272 | KeyQ: "Q", 273 | KeyR: "R", 274 | KeyS: "S", 275 | KeyT: "T", 276 | KeyU: "U", 277 | KeyV: "V", 278 | KeyW: "W", 279 | KeyX: "X", 280 | KeyY: "Y", 281 | KeyZ: "Z", 282 | KeyLeftBracket: "LeftBracket", 283 | KeyBackslash: "Backslash", 284 | KeyRightBracket: "RightBracket", 285 | KeyGraveAccent: "GraveAccent", 286 | KeyWorld1: "World1", 287 | KeyWorld2: "World2", 288 | KeyEscape: "Escape", 289 | KeyEnter: "Enter", 290 | KeyTab: "Tab", 291 | KeyBackspace: "Backspace", 292 | KeyInsert: "Insert", 293 | KeyDelete: "Delete", 294 | KeyRight: "Right", 295 | KeyLeft: "Left", 296 | KeyDown: "Down", 297 | KeyUp: "Up", 298 | KeyPageUp: "PageUp", 299 | KeyPageDown: "PageDown", 300 | KeyHome: "Home", 301 | KeyEnd: "End", 302 | KeyCapsLock: "CapsLock", 303 | KeyScrollLock: "ScrollLock", 304 | KeyNumLock: "NumLock", 305 | KeyPrintScreen: "PrintScreen", 306 | KeyPause: "Pause", 307 | KeyF1: "F1", 308 | KeyF2: "F2", 309 | KeyF3: "F3", 310 | KeyF4: "F4", 311 | KeyF5: "F5", 312 | KeyF6: "F6", 313 | KeyF7: "F7", 314 | KeyF8: "F8", 315 | KeyF9: "F9", 316 | KeyF10: "F10", 317 | KeyF11: "F11", 318 | KeyF12: "F12", 319 | KeyF13: "F13", 320 | KeyF14: "F14", 321 | KeyF15: "F15", 322 | KeyF16: "F16", 323 | KeyF17: "F17", 324 | KeyF18: "F18", 325 | KeyF19: "F19", 326 | KeyF20: "F20", 327 | KeyF21: "F21", 328 | KeyF22: "F22", 329 | KeyF23: "F23", 330 | KeyF24: "F24", 331 | KeyF25: "F25", 332 | KeyKP0: "KP0", 333 | KeyKP1: "KP1", 334 | KeyKP2: "KP2", 335 | KeyKP3: "KP3", 336 | KeyKP4: "KP4", 337 | KeyKP5: "KP5", 338 | KeyKP6: "KP6", 339 | KeyKP7: "KP7", 340 | KeyKP8: "KP8", 341 | KeyKP9: "KP9", 342 | KeyKPDecimal: "KPDecimal", 343 | KeyKPDivide: "KPDivide", 344 | KeyKPMultiply: "KPMultiply", 345 | KeyKPSubtract: "KPSubtract", 346 | KeyKPAdd: "KPAdd", 347 | KeyKPEnter: "KPEnter", 348 | KeyKPEqual: "KPEqual", 349 | KeyLeftShift: "LeftShift", 350 | KeyLeftControl: "LeftControl", 351 | KeyLeftAlt: "LeftAlt", 352 | KeyLeftSuper: "LeftSuper", 353 | KeyRightShift: "RightShift", 354 | KeyRightControl: "RightControl", 355 | KeyRightAlt: "RightAlt", 356 | KeyRightSuper: "RightSuper", 357 | KeyMenu: "Menu", 358 | } 359 | 360 | func (w *Window) initInput() { 361 | mainthread.Call(func() { 362 | w.window.SetMouseButtonCallback(func(_ *glfw.Window, button glfw.MouseButton, action glfw.Action, mod glfw.ModifierKey) { 363 | switch action { 364 | case glfw.Press: 365 | w.tempPressEvents[Button(button)] = true 366 | w.tempInp.buttons[Button(button)] = true 367 | case glfw.Release: 368 | w.tempReleaseEvents[Button(button)] = true 369 | w.tempInp.buttons[Button(button)] = false 370 | } 371 | }) 372 | 373 | w.window.SetKeyCallback(func(_ *glfw.Window, key glfw.Key, scancode int, action glfw.Action, mods glfw.ModifierKey) { 374 | if key == glfw.KeyUnknown { 375 | return 376 | } 377 | switch action { 378 | case glfw.Press: 379 | w.tempPressEvents[Button(key)] = true 380 | w.tempInp.buttons[Button(key)] = true 381 | case glfw.Release: 382 | w.tempReleaseEvents[Button(key)] = true 383 | w.tempInp.buttons[Button(key)] = false 384 | case glfw.Repeat: 385 | w.tempInp.repeat[Button(key)] = true 386 | } 387 | }) 388 | 389 | w.window.SetCursorEnterCallback(func(_ *glfw.Window, entered bool) { 390 | w.cursorInsideWindow = entered 391 | }) 392 | 393 | w.window.SetCursorPosCallback(func(_ *glfw.Window, x, y float64) { 394 | w.tempInp.mouse = pixel.V( 395 | x+w.bounds.Min.X, 396 | (w.bounds.H()-y)+w.bounds.Min.Y, 397 | ) 398 | }) 399 | 400 | w.window.SetScrollCallback(func(_ *glfw.Window, xoff, yoff float64) { 401 | w.tempInp.scroll.X += xoff 402 | w.tempInp.scroll.Y += yoff 403 | }) 404 | 405 | w.window.SetCharCallback(func(_ *glfw.Window, r rune) { 406 | w.tempInp.typed += string(r) 407 | }) 408 | }) 409 | } 410 | 411 | // UpdateInput polls window events. Call this function to poll window events 412 | // without swapping buffers. Note that the Update method invokes UpdateInput. 413 | func (w *Window) UpdateInput() { 414 | mainthread.Call(func() { 415 | glfw.PollEvents() 416 | }) 417 | w.doUpdateInput() 418 | } 419 | 420 | // UpdateInputWait blocks until an event is received or a timeout. If timeout is 0 421 | // then it will wait indefinitely 422 | func (w *Window) UpdateInputWait(timeout time.Duration) { 423 | mainthread.Call(func() { 424 | if timeout <= 0 { 425 | glfw.WaitEvents() 426 | } else { 427 | glfw.WaitEventsTimeout(timeout.Seconds()) 428 | } 429 | }) 430 | w.doUpdateInput() 431 | } 432 | 433 | // internal input bookkeeping 434 | func (w *Window) doUpdateInput() { 435 | w.prevInp = w.currInp 436 | w.currInp = w.tempInp 437 | 438 | w.pressEvents = w.tempPressEvents 439 | w.releaseEvents = w.tempReleaseEvents 440 | 441 | // Clear last frame's temporary status 442 | w.tempPressEvents = [KeyLast + 1]bool{} 443 | w.tempReleaseEvents = [KeyLast + 1]bool{} 444 | w.tempInp.repeat = [KeyLast + 1]bool{} 445 | w.tempInp.scroll = pixel.ZV 446 | w.tempInp.typed = "" 447 | 448 | w.updateJoystickInput() 449 | } 450 | -------------------------------------------------------------------------------- /pixelgl/joystick.go: -------------------------------------------------------------------------------- 1 | package pixelgl 2 | 3 | import ( 4 | "github.com/go-gl/glfw/v3.3/glfw" 5 | ) 6 | 7 | // Joystick is a joystick or controller (gamepad). 8 | type Joystick int 9 | 10 | // List all of the joysticks. 11 | const ( 12 | Joystick1 = Joystick(glfw.Joystick1) 13 | Joystick2 = Joystick(glfw.Joystick2) 14 | Joystick3 = Joystick(glfw.Joystick3) 15 | Joystick4 = Joystick(glfw.Joystick4) 16 | Joystick5 = Joystick(glfw.Joystick5) 17 | Joystick6 = Joystick(glfw.Joystick6) 18 | Joystick7 = Joystick(glfw.Joystick7) 19 | Joystick8 = Joystick(glfw.Joystick8) 20 | Joystick9 = Joystick(glfw.Joystick9) 21 | Joystick10 = Joystick(glfw.Joystick10) 22 | Joystick11 = Joystick(glfw.Joystick11) 23 | Joystick12 = Joystick(glfw.Joystick12) 24 | Joystick13 = Joystick(glfw.Joystick13) 25 | Joystick14 = Joystick(glfw.Joystick14) 26 | Joystick15 = Joystick(glfw.Joystick15) 27 | Joystick16 = Joystick(glfw.Joystick16) 28 | 29 | JoystickLast = Joystick(glfw.JoystickLast) 30 | ) 31 | 32 | // GamepadAxis corresponds to a gamepad axis. 33 | type GamepadAxis int 34 | 35 | // Gamepad axis IDs. 36 | const ( 37 | AxisLeftX = GamepadAxis(glfw.AxisLeftX) 38 | AxisLeftY = GamepadAxis(glfw.AxisLeftY) 39 | AxisRightX = GamepadAxis(glfw.AxisRightX) 40 | AxisRightY = GamepadAxis(glfw.AxisRightY) 41 | AxisLeftTrigger = GamepadAxis(glfw.AxisLeftTrigger) 42 | AxisRightTrigger = GamepadAxis(glfw.AxisRightTrigger) 43 | AxisLast = GamepadAxis(glfw.AxisLast) 44 | ) 45 | 46 | // GamepadButton corresponds to a gamepad button. 47 | type GamepadButton int 48 | 49 | // Gamepad button IDs. 50 | const ( 51 | ButtonA = GamepadButton(glfw.ButtonA) 52 | ButtonB = GamepadButton(glfw.ButtonB) 53 | ButtonX = GamepadButton(glfw.ButtonX) 54 | ButtonY = GamepadButton(glfw.ButtonY) 55 | ButtonLeftBumper = GamepadButton(glfw.ButtonLeftBumper) 56 | ButtonRightBumper = GamepadButton(glfw.ButtonRightBumper) 57 | ButtonBack = GamepadButton(glfw.ButtonBack) 58 | ButtonStart = GamepadButton(glfw.ButtonStart) 59 | ButtonGuide = GamepadButton(glfw.ButtonGuide) 60 | ButtonLeftThumb = GamepadButton(glfw.ButtonLeftThumb) 61 | ButtonRightThumb = GamepadButton(glfw.ButtonRightThumb) 62 | ButtonDpadUp = GamepadButton(glfw.ButtonDpadUp) 63 | ButtonDpadRight = GamepadButton(glfw.ButtonDpadRight) 64 | ButtonDpadDown = GamepadButton(glfw.ButtonDpadDown) 65 | ButtonDpadLeft = GamepadButton(glfw.ButtonDpadLeft) 66 | ButtonLast = GamepadButton(glfw.ButtonLast) 67 | ButtonCross = GamepadButton(glfw.ButtonCross) 68 | ButtonCircle = GamepadButton(glfw.ButtonCircle) 69 | ButtonSquare = GamepadButton(glfw.ButtonSquare) 70 | ButtonTriangle = GamepadButton(glfw.ButtonTriangle) 71 | ) 72 | 73 | // JoystickPresent returns if the joystick is currently connected. 74 | // 75 | // This API is experimental. 76 | func (w *Window) JoystickPresent(js Joystick) bool { 77 | return w.currJoy.connected[js] 78 | } 79 | 80 | // JoystickName returns the name of the joystick. A disconnected joystick will return an 81 | // empty string. 82 | // 83 | // This API is experimental. 84 | func (w *Window) JoystickName(js Joystick) string { 85 | return w.currJoy.name[js] 86 | } 87 | 88 | // JoystickButtonCount returns the number of buttons a connected joystick has. 89 | // 90 | // This API is experimental. 91 | func (w *Window) JoystickButtonCount(js Joystick) int { 92 | return len(w.currJoy.buttons[js]) 93 | } 94 | 95 | // JoystickAxisCount returns the number of axes a connected joystick has. 96 | // 97 | // This API is experimental. 98 | func (w *Window) JoystickAxisCount(js Joystick) int { 99 | return len(w.currJoy.axis[js]) 100 | } 101 | 102 | // JoystickPressed returns whether the joystick Button is currently pressed down. 103 | // If the button index is out of range, this will return false. 104 | // 105 | // This API is experimental. 106 | func (w *Window) JoystickPressed(js Joystick, button GamepadButton) bool { 107 | return w.currJoy.getButton(js, int(button)) 108 | } 109 | 110 | // JoystickJustPressed returns whether the joystick Button has just been pressed down. 111 | // If the button index is out of range, this will return false. 112 | // 113 | // This API is experimental. 114 | func (w *Window) JoystickJustPressed(js Joystick, button GamepadButton) bool { 115 | return w.currJoy.getButton(js, int(button)) && !w.prevJoy.getButton(js, int(button)) 116 | } 117 | 118 | // JoystickJustReleased returns whether the joystick Button has just been released up. 119 | // If the button index is out of range, this will return false. 120 | // 121 | // This API is experimental. 122 | func (w *Window) JoystickJustReleased(js Joystick, button GamepadButton) bool { 123 | return !w.currJoy.getButton(js, int(button)) && w.prevJoy.getButton(js, int(button)) 124 | } 125 | 126 | // JoystickAxis returns the value of a joystick axis at the last call to Window.Update. 127 | // If the axis index is out of range, this will return 0. 128 | // 129 | // This API is experimental. 130 | func (w *Window) JoystickAxis(js Joystick, axis GamepadAxis) float64 { 131 | return w.currJoy.getAxis(js, int(axis)) 132 | } 133 | 134 | // Used internally during Window.UpdateInput to update the state of the joysticks. 135 | func (w *Window) updateJoystickInput() { 136 | for js := Joystick1; js <= JoystickLast; js++ { 137 | // Determine and store if the joystick was connected 138 | joystickPresent := glfw.Joystick(js).Present() 139 | w.tempJoy.connected[js] = joystickPresent 140 | 141 | if joystickPresent { 142 | if glfw.Joystick(js).IsGamepad() { 143 | gamepadInputs := glfw.Joystick(js).GetGamepadState() 144 | 145 | w.tempJoy.buttons[js] = gamepadInputs.Buttons[:] 146 | w.tempJoy.axis[js] = gamepadInputs.Axes[:] 147 | } else { 148 | w.tempJoy.buttons[js] = glfw.Joystick(js).GetButtons() 149 | w.tempJoy.axis[js] = glfw.Joystick(js).GetAxes() 150 | } 151 | 152 | if !w.currJoy.connected[js] { 153 | // The joystick was recently connected, we get the name 154 | w.tempJoy.name[js] = glfw.Joystick(js).GetName() 155 | } else { 156 | // Use the name from the previous one 157 | w.tempJoy.name[js] = w.currJoy.name[js] 158 | } 159 | } else { 160 | w.tempJoy.buttons[js] = []glfw.Action{} 161 | w.tempJoy.axis[js] = []float32{} 162 | w.tempJoy.name[js] = "" 163 | } 164 | } 165 | 166 | w.prevJoy = w.currJoy 167 | w.currJoy = w.tempJoy 168 | } 169 | 170 | type joystickState struct { 171 | connected [JoystickLast + 1]bool 172 | name [JoystickLast + 1]string 173 | buttons [JoystickLast + 1][]glfw.Action 174 | axis [JoystickLast + 1][]float32 175 | } 176 | 177 | // Returns if a button on a joystick is down, returning false if the button or joystick is invalid. 178 | func (js *joystickState) getButton(joystick Joystick, button int) bool { 179 | // Check that the joystick and button is valid, return false by default 180 | if js.buttons[joystick] == nil || button >= len(js.buttons[joystick]) || button < 0 { 181 | return false 182 | } 183 | return js.buttons[joystick][byte(button)] == glfw.Press 184 | } 185 | 186 | // Returns the value of a joystick axis, returning 0 if the button or joystick is invalid. 187 | func (js *joystickState) getAxis(joystick Joystick, axis int) float64 { 188 | // Check that the joystick and axis is valid, return 0 by default. 189 | if js.axis[joystick] == nil || axis >= len(js.axis[joystick]) || axis < 0 { 190 | return 0 191 | } 192 | return float64(js.axis[joystick][axis]) 193 | } 194 | -------------------------------------------------------------------------------- /pixelgl/monitor.go: -------------------------------------------------------------------------------- 1 | package pixelgl 2 | 3 | import ( 4 | "github.com/faiface/mainthread" 5 | "github.com/go-gl/glfw/v3.3/glfw" 6 | ) 7 | 8 | // Monitor represents a physical display attached to your computer. 9 | type Monitor struct { 10 | monitor *glfw.Monitor 11 | } 12 | 13 | // VideoMode represents all properties of a video mode and is 14 | // associated with a monitor if it is used in fullscreen mode. 15 | type VideoMode struct { 16 | // Width is the width of the vide mode in pixels. 17 | Width int 18 | // Height is the height of the video mode in pixels. 19 | Height int 20 | // RefreshRate holds the refresh rate of the associated monitor in Hz. 21 | RefreshRate int 22 | } 23 | 24 | // PrimaryMonitor returns the main monitor (usually the one with the taskbar and stuff). 25 | func PrimaryMonitor() *Monitor { 26 | var monitor *glfw.Monitor 27 | mainthread.Call(func() { 28 | monitor = glfw.GetPrimaryMonitor() 29 | }) 30 | return &Monitor{ 31 | monitor: monitor, 32 | } 33 | } 34 | 35 | // Monitors returns a slice of all currently available monitors. 36 | func Monitors() []*Monitor { 37 | var monitors []*Monitor 38 | mainthread.Call(func() { 39 | for _, monitor := range glfw.GetMonitors() { 40 | monitors = append(monitors, &Monitor{monitor: monitor}) 41 | } 42 | }) 43 | return monitors 44 | } 45 | 46 | // Name returns a human-readable name of the Monitor. 47 | func (m *Monitor) Name() string { 48 | var name string 49 | mainthread.Call(func() { 50 | name = m.monitor.GetName() 51 | }) 52 | return name 53 | } 54 | 55 | // PhysicalSize returns the size of the display area of the Monitor in millimeters. 56 | func (m *Monitor) PhysicalSize() (width, height float64) { 57 | var wi, hi int 58 | mainthread.Call(func() { 59 | wi, hi = m.monitor.GetPhysicalSize() 60 | }) 61 | width = float64(wi) 62 | height = float64(hi) 63 | return 64 | } 65 | 66 | // Position returns the position of the upper-left corner of the Monitor in screen coordinates. 67 | func (m *Monitor) Position() (x, y float64) { 68 | var xi, yi int 69 | mainthread.Call(func() { 70 | xi, yi = m.monitor.GetPos() 71 | }) 72 | x = float64(xi) 73 | y = float64(yi) 74 | return 75 | } 76 | 77 | // Size returns the resolution of the Monitor in pixels. 78 | func (m *Monitor) Size() (width, height float64) { 79 | var mode *glfw.VidMode 80 | mainthread.Call(func() { 81 | mode = m.monitor.GetVideoMode() 82 | }) 83 | width = float64(mode.Width) 84 | height = float64(mode.Height) 85 | return 86 | } 87 | 88 | // BitDepth returns the number of bits per color of the Monitor. 89 | func (m *Monitor) BitDepth() (red, green, blue int) { 90 | var mode *glfw.VidMode 91 | mainthread.Call(func() { 92 | mode = m.monitor.GetVideoMode() 93 | }) 94 | red = mode.RedBits 95 | green = mode.GreenBits 96 | blue = mode.BlueBits 97 | return 98 | } 99 | 100 | // RefreshRate returns the refresh frequency of the Monitor in Hz (refreshes/second). 101 | func (m *Monitor) RefreshRate() (rate float64) { 102 | var mode *glfw.VidMode 103 | mainthread.Call(func() { 104 | mode = m.monitor.GetVideoMode() 105 | }) 106 | rate = float64(mode.RefreshRate) 107 | return 108 | } 109 | 110 | // VideoModes returns all available video modes for the monitor. 111 | func (m *Monitor) VideoModes() (vmodes []VideoMode) { 112 | var modes []*glfw.VidMode 113 | mainthread.Call(func() { 114 | modes = m.monitor.GetVideoModes() 115 | }) 116 | for _, mode := range modes { 117 | vmodes = append(vmodes, VideoMode{ 118 | Width: mode.Width, 119 | Height: mode.Height, 120 | RefreshRate: mode.RefreshRate, 121 | }) 122 | } 123 | return 124 | } 125 | -------------------------------------------------------------------------------- /pixelgl/run.go: -------------------------------------------------------------------------------- 1 | package pixelgl 2 | 3 | import ( 4 | "github.com/faiface/mainthread" 5 | "github.com/go-gl/glfw/v3.3/glfw" 6 | "github.com/pkg/errors" 7 | ) 8 | 9 | // Run is essentially the main function of PixelGL. It exists mainly due to the technical 10 | // limitations of OpenGL and operating systems. In short, all graphics and window manipulating calls 11 | // must be done from the main thread. Run makes this possible. 12 | // 13 | // Call this function from the main function of your application. This is necessary, so that Run 14 | // runs on the main thread. 15 | // 16 | // func run() { 17 | // // interact with Pixel and PixelGL from here (even concurrently) 18 | // } 19 | // 20 | // func main() { 21 | // pixel.Run(run) 22 | // } 23 | // 24 | // You can spawn any number of goroutines from your run function and interact with PixelGL 25 | // concurrently. The only condition is that the Run function is called from your main function. 26 | func Run(run func()) { 27 | err := glfw.Init() 28 | if err != nil { 29 | panic(errors.Wrap(err, "failed to initialize GLFW")) 30 | } 31 | defer glfw.Terminate() 32 | mainthread.Run(run) 33 | } 34 | -------------------------------------------------------------------------------- /pixelgl/util.go: -------------------------------------------------------------------------------- 1 | package pixelgl 2 | 3 | import ( 4 | "math" 5 | 6 | "github.com/faiface/pixel" 7 | ) 8 | 9 | func intBounds(bounds pixel.Rect) (x, y, w, h int) { 10 | x0 := int(math.Floor(bounds.Min.X)) 11 | y0 := int(math.Floor(bounds.Min.Y)) 12 | x1 := int(math.Ceil(bounds.Max.X)) 13 | y1 := int(math.Ceil(bounds.Max.Y)) 14 | return x0, y0, x1 - x0, y1 - y0 15 | } 16 | -------------------------------------------------------------------------------- /rectangle.go: -------------------------------------------------------------------------------- 1 | package pixel 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | ) 7 | 8 | // Rect is a 2D rectangle aligned with the axes of the coordinate system. It is defined by two 9 | // points, Min and Max. 10 | // 11 | // The invariant should hold, that Max's components are greater or equal than Min's components 12 | // respectively. 13 | type Rect struct { 14 | Min, Max Vec 15 | } 16 | 17 | // ZR is a zero rectangle. 18 | var ZR = Rect{Min: ZV, Max: ZV} 19 | 20 | // R returns a new Rect with given the Min and Max coordinates. 21 | // 22 | // Note that the returned rectangle is not automatically normalized. 23 | func R(minX, minY, maxX, maxY float64) Rect { 24 | return Rect{ 25 | Min: Vec{minX, minY}, 26 | Max: Vec{maxX, maxY}, 27 | } 28 | } 29 | 30 | // String returns the string representation of the Rect. 31 | // 32 | // r := pixel.R(100, 50, 200, 300) 33 | // r.String() // returns "Rect(100, 50, 200, 300)" 34 | // fmt.Println(r) // Rect(100, 50, 200, 300) 35 | func (r Rect) String() string { 36 | return fmt.Sprintf("Rect(%v, %v, %v, %v)", r.Min.X, r.Min.Y, r.Max.X, r.Max.Y) 37 | } 38 | 39 | // Norm returns the Rect in normal form, such that Max is component-wise greater or equal than Min. 40 | func (r Rect) Norm() Rect { 41 | return Rect{ 42 | Min: Vec{ 43 | math.Min(r.Min.X, r.Max.X), 44 | math.Min(r.Min.Y, r.Max.Y), 45 | }, 46 | Max: Vec{ 47 | math.Max(r.Min.X, r.Max.X), 48 | math.Max(r.Min.Y, r.Max.Y), 49 | }, 50 | } 51 | } 52 | 53 | // W returns the width of the Rect. 54 | func (r Rect) W() float64 { 55 | return r.Max.X - r.Min.X 56 | } 57 | 58 | // H returns the height of the Rect. 59 | func (r Rect) H() float64 { 60 | return r.Max.Y - r.Min.Y 61 | } 62 | 63 | // Size returns the vector of width and height of the Rect. 64 | func (r Rect) Size() Vec { 65 | return V(r.W(), r.H()) 66 | } 67 | 68 | // Area returns the area of r. If r is not normalized, area may be negative. 69 | func (r Rect) Area() float64 { 70 | return r.W() * r.H() 71 | } 72 | 73 | // Edges will return the four lines which make up the edges of the rectangle. 74 | func (r Rect) Edges() [4]Line { 75 | corners := r.Vertices() 76 | 77 | return [4]Line{ 78 | {A: corners[0], B: corners[1]}, 79 | {A: corners[1], B: corners[2]}, 80 | {A: corners[2], B: corners[3]}, 81 | {A: corners[3], B: corners[0]}, 82 | } 83 | } 84 | 85 | // Anchor is a vector used to define anchors, such as `Center`, `Top`, `TopRight`, etc. 86 | type Anchor Vec 87 | 88 | var ( 89 | Center = Anchor{0.5, 0.5} 90 | Top = Anchor{0.5, 0} 91 | TopRight = Anchor{0, 0} 92 | Right = Anchor{0, 0.5} 93 | BottomRight = Anchor{0, 1} 94 | Bottom = Anchor{0.5, 1} 95 | BottomLeft = Anchor{1, 1} 96 | Left = Anchor{1, 0.5} 97 | TopLeft = Anchor{1, 0} 98 | ) 99 | 100 | var anchorStrings map[Anchor]string = map[Anchor]string{ 101 | Center: "center", 102 | Top: "top", 103 | TopRight: "top-right", 104 | Right: "right", 105 | BottomRight: "bottom-right", 106 | Bottom: "bottom", 107 | BottomLeft: "bottom-left", 108 | Left: "left", 109 | TopLeft: "top-left", 110 | } 111 | 112 | // String returns the string representation of an anchor. 113 | func (anchor Anchor) String() string { 114 | return anchorStrings[anchor] 115 | } 116 | 117 | var oppositeAnchors map[Anchor]Anchor = map[Anchor]Anchor{ 118 | Center: Center, 119 | Top: Bottom, 120 | Bottom: Top, 121 | Right: Left, 122 | Left: Right, 123 | TopRight: BottomLeft, 124 | BottomLeft: TopRight, 125 | BottomRight: TopLeft, 126 | TopLeft: BottomRight, 127 | } 128 | 129 | // Opposite returns the opposite position of the anchor (ie. Top -> Bottom; BottomLeft -> TopRight, etc.). 130 | func (anchor Anchor) Opposite() Anchor { 131 | return oppositeAnchors[anchor] 132 | } 133 | 134 | // AnchorPos returns the relative position of the given anchor. 135 | func (r Rect) AnchorPos(anchor Anchor) Vec { 136 | return r.Size().ScaledXY(V(0, 0).Sub(Vec(anchor))) 137 | } 138 | 139 | // AlignedTo returns the rect moved by the given anchor. 140 | func (rect Rect) AlignedTo(anchor Anchor) Rect { 141 | return rect.Moved(rect.AnchorPos(anchor)) 142 | } 143 | 144 | // Center returns the position of the center of the Rect. 145 | // `rect.Center()` is equivalent to `rect.Anchor(pixel.Anchor.Center)` 146 | func (r Rect) Center() Vec { 147 | return Lerp(r.Min, r.Max, 0.5) 148 | } 149 | 150 | // Moved returns the Rect moved (both Min and Max) by the given vector delta. 151 | func (r Rect) Moved(delta Vec) Rect { 152 | return Rect{ 153 | Min: r.Min.Add(delta), 154 | Max: r.Max.Add(delta), 155 | } 156 | } 157 | 158 | // Resized returns the Rect resized to the given size while keeping the position of the given 159 | // anchor. 160 | // 161 | // r.Resized(r.Min, size) // resizes while keeping the position of the lower-left corner 162 | // r.Resized(r.Max, size) // same with the top-right corner 163 | // r.Resized(r.Center(), size) // resizes around the center 164 | // 165 | // This function does not make sense for resizing a rectangle of zero area and will panic. Use 166 | // ResizedMin in the case of zero area. 167 | func (r Rect) Resized(anchor, size Vec) Rect { 168 | if r.W()*r.H() == 0 { 169 | panic(fmt.Errorf("(%T).Resize: zero area", r)) 170 | } 171 | fraction := Vec{size.X / r.W(), size.Y / r.H()} 172 | return Rect{ 173 | Min: anchor.Add(r.Min.Sub(anchor).ScaledXY(fraction)), 174 | Max: anchor.Add(r.Max.Sub(anchor).ScaledXY(fraction)), 175 | } 176 | } 177 | 178 | // ResizedMin returns the Rect resized to the given size while keeping the position of the Rect's 179 | // Min. 180 | // 181 | // Sizes of zero area are safe here. 182 | func (r Rect) ResizedMin(size Vec) Rect { 183 | return Rect{ 184 | Min: r.Min, 185 | Max: r.Min.Add(size), 186 | } 187 | } 188 | 189 | // Contains checks whether a vector u is contained within this Rect (including it's borders). 190 | func (r Rect) Contains(u Vec) bool { 191 | return r.Min.X <= u.X && u.X <= r.Max.X && r.Min.Y <= u.Y && u.Y <= r.Max.Y 192 | } 193 | 194 | // Union returns the minimal Rect which covers both r and s. Rects r and s must be normalized. 195 | func (r Rect) Union(s Rect) Rect { 196 | return R( 197 | math.Min(r.Min.X, s.Min.X), 198 | math.Min(r.Min.Y, s.Min.Y), 199 | math.Max(r.Max.X, s.Max.X), 200 | math.Max(r.Max.Y, s.Max.Y), 201 | ) 202 | } 203 | 204 | // Intersect returns the maximal Rect which is covered by both r and s. Rects r and s must be normalized. 205 | // 206 | // If r and s don't overlap, this function returns a zero-rectangle. 207 | func (r Rect) Intersect(s Rect) Rect { 208 | t := R( 209 | math.Max(r.Min.X, s.Min.X), 210 | math.Max(r.Min.Y, s.Min.Y), 211 | math.Min(r.Max.X, s.Max.X), 212 | math.Min(r.Max.Y, s.Max.Y), 213 | ) 214 | if t.Min.X >= t.Max.X || t.Min.Y >= t.Max.Y { 215 | return ZR 216 | } 217 | return t 218 | } 219 | 220 | // Intersects returns whether or not the given Rect intersects at any point with this Rect. 221 | // 222 | // This function is overall about 5x faster than Intersect, so it is better 223 | // to use if you have no need for the returned Rect from Intersect. 224 | func (r Rect) Intersects(s Rect) bool { 225 | return !(s.Max.X <= r.Min.X || 226 | s.Min.X >= r.Max.X || 227 | s.Max.Y <= r.Min.Y || 228 | s.Min.Y >= r.Max.Y) 229 | } 230 | 231 | // IntersectCircle returns a minimal required Vector, such that moving the rect by that vector would stop the Circle 232 | // and the Rect intersecting. This function returns a zero-vector if the Circle and Rect do not overlap, and if only 233 | // the perimeters touch. 234 | // 235 | // This function will return a non-zero vector if: 236 | // - The Rect contains the Circle, partially or fully 237 | // - The Circle contains the Rect, partially of fully 238 | func (r Rect) IntersectCircle(c Circle) Vec { 239 | return c.IntersectRect(r).Scaled(-1) 240 | } 241 | 242 | // IntersectLine will return the shortest Vec such that if the Rect is moved by the Vec returned, the Line and Rect no 243 | // longer intersect. 244 | func (r Rect) IntersectLine(l Line) Vec { 245 | return l.IntersectRect(r).Scaled(-1) 246 | } 247 | 248 | // IntersectionPoints returns all the points where the Rect intersects with the line provided. This can be zero, one or 249 | // two points, depending on the location of the shapes. The points of intersection will be returned in order of 250 | // closest-to-l.A to closest-to-l.B. 251 | func (r Rect) IntersectionPoints(l Line) []Vec { 252 | // Use map keys to ensure unique points 253 | pointMap := make(map[Vec]struct{}) 254 | 255 | for _, edge := range r.Edges() { 256 | if intersect, ok := l.Intersect(edge); ok { 257 | pointMap[intersect] = struct{}{} 258 | } 259 | } 260 | 261 | points := make([]Vec, 0, len(pointMap)) 262 | for point := range pointMap { 263 | points = append(points, point) 264 | } 265 | 266 | // Order the points 267 | if len(points) == 2 { 268 | if points[1].To(l.A).Len() < points[0].To(l.A).Len() { 269 | return []Vec{points[1], points[0]} 270 | } 271 | } 272 | 273 | return points 274 | } 275 | 276 | // Vertices returns a slice of the four corners which make up the rectangle. 277 | func (r Rect) Vertices() [4]Vec { 278 | return [4]Vec{ 279 | r.Min, 280 | V(r.Min.X, r.Max.Y), 281 | r.Max, 282 | V(r.Max.X, r.Min.Y), 283 | } 284 | } 285 | -------------------------------------------------------------------------------- /rectangle_test.go: -------------------------------------------------------------------------------- 1 | package pixel_test 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | "testing" 7 | 8 | "github.com/faiface/pixel" 9 | ) 10 | 11 | func TestRect_Resize(t *testing.T) { 12 | type rectTestTransform struct { 13 | name string 14 | f func(pixel.Rect) pixel.Rect 15 | } 16 | 17 | // rectangles 18 | squareAroundOrigin := pixel.R(-10, -10, 10, 10) 19 | squareAround2020 := pixel.R(10, 10, 30, 30) 20 | rectangleAroundOrigin := pixel.R(-20, -10, 20, 10) 21 | rectangleAround2020 := pixel.R(0, 10, 40, 30) 22 | 23 | // resize transformations 24 | resizeByHalfAroundCenter := rectTestTransform{"by half around center", func(rect pixel.Rect) pixel.Rect { 25 | return rect.Resized(rect.Center(), rect.Size().Scaled(0.5)) 26 | }} 27 | resizeByHalfAroundMin := rectTestTransform{"by half around Min", func(rect pixel.Rect) pixel.Rect { 28 | return rect.Resized(rect.Min, rect.Size().Scaled(0.5)) 29 | }} 30 | resizeByHalfAroundMax := rectTestTransform{"by half around Max", func(rect pixel.Rect) pixel.Rect { 31 | return rect.Resized(rect.Max, rect.Size().Scaled(0.5)) 32 | }} 33 | resizeByHalfAroundMiddleOfLeftSide := rectTestTransform{"by half around middle of left side", func(rect pixel.Rect) pixel.Rect { 34 | return rect.Resized(pixel.V(rect.Min.X, rect.Center().Y), rect.Size().Scaled(0.5)) 35 | }} 36 | resizeByHalfAroundOrigin := rectTestTransform{"by half around the origin", func(rect pixel.Rect) pixel.Rect { 37 | return rect.Resized(pixel.ZV, rect.Size().Scaled(0.5)) 38 | }} 39 | 40 | testCases := []struct { 41 | input pixel.Rect 42 | transform rectTestTransform 43 | answer pixel.Rect 44 | }{ 45 | {squareAroundOrigin, resizeByHalfAroundCenter, pixel.R(-5, -5, 5, 5)}, 46 | {squareAround2020, resizeByHalfAroundCenter, pixel.R(15, 15, 25, 25)}, 47 | {rectangleAroundOrigin, resizeByHalfAroundCenter, pixel.R(-10, -5, 10, 5)}, 48 | {rectangleAround2020, resizeByHalfAroundCenter, pixel.R(10, 15, 30, 25)}, 49 | 50 | {squareAroundOrigin, resizeByHalfAroundMin, pixel.R(-10, -10, 0, 0)}, 51 | {squareAround2020, resizeByHalfAroundMin, pixel.R(10, 10, 20, 20)}, 52 | {rectangleAroundOrigin, resizeByHalfAroundMin, pixel.R(-20, -10, 0, 0)}, 53 | {rectangleAround2020, resizeByHalfAroundMin, pixel.R(0, 10, 20, 20)}, 54 | 55 | {squareAroundOrigin, resizeByHalfAroundMax, pixel.R(0, 0, 10, 10)}, 56 | {squareAround2020, resizeByHalfAroundMax, pixel.R(20, 20, 30, 30)}, 57 | {rectangleAroundOrigin, resizeByHalfAroundMax, pixel.R(0, 0, 20, 10)}, 58 | {rectangleAround2020, resizeByHalfAroundMax, pixel.R(20, 20, 40, 30)}, 59 | 60 | {squareAroundOrigin, resizeByHalfAroundMiddleOfLeftSide, pixel.R(-10, -5, 0, 5)}, 61 | {squareAround2020, resizeByHalfAroundMiddleOfLeftSide, pixel.R(10, 15, 20, 25)}, 62 | {rectangleAroundOrigin, resizeByHalfAroundMiddleOfLeftSide, pixel.R(-20, -5, 0, 5)}, 63 | {rectangleAround2020, resizeByHalfAroundMiddleOfLeftSide, pixel.R(0, 15, 20, 25)}, 64 | 65 | {squareAroundOrigin, resizeByHalfAroundOrigin, pixel.R(-5, -5, 5, 5)}, 66 | {squareAround2020, resizeByHalfAroundOrigin, pixel.R(5, 5, 15, 15)}, 67 | {rectangleAroundOrigin, resizeByHalfAroundOrigin, pixel.R(-10, -5, 10, 5)}, 68 | {rectangleAround2020, resizeByHalfAroundOrigin, pixel.R(0, 5, 20, 15)}, 69 | } 70 | 71 | for _, testCase := range testCases { 72 | t.Run(fmt.Sprintf("Resize %v %s", testCase.input, testCase.transform.name), func(t *testing.T) { 73 | testResult := testCase.transform.f(testCase.input) 74 | if testResult != testCase.answer { 75 | t.Errorf("Got: %v, wanted: %v\n", testResult, testCase.answer) 76 | } 77 | }) 78 | } 79 | } 80 | 81 | func TestRect_Edges(t *testing.T) { 82 | type fields struct { 83 | Min pixel.Vec 84 | Max pixel.Vec 85 | } 86 | tests := []struct { 87 | name string 88 | fields fields 89 | want [4]pixel.Line 90 | }{ 91 | { 92 | name: "Get edges", 93 | fields: fields{Min: pixel.V(0, 0), Max: pixel.V(10, 10)}, 94 | want: [4]pixel.Line{ 95 | pixel.L(pixel.V(0, 0), pixel.V(0, 10)), 96 | pixel.L(pixel.V(0, 10), pixel.V(10, 10)), 97 | pixel.L(pixel.V(10, 10), pixel.V(10, 0)), 98 | pixel.L(pixel.V(10, 0), pixel.V(0, 0)), 99 | }, 100 | }, 101 | } 102 | for _, tt := range tests { 103 | t.Run(tt.name, func(t *testing.T) { 104 | r := pixel.Rect{ 105 | Min: tt.fields.Min, 106 | Max: tt.fields.Max, 107 | } 108 | if got := r.Edges(); !reflect.DeepEqual(got, tt.want) { 109 | t.Errorf("Rect.Edges() = %v, want %v", got, tt.want) 110 | } 111 | }) 112 | } 113 | } 114 | 115 | func TestRect_Vertices(t *testing.T) { 116 | type fields struct { 117 | Min pixel.Vec 118 | Max pixel.Vec 119 | } 120 | tests := []struct { 121 | name string 122 | fields fields 123 | want [4]pixel.Vec 124 | }{ 125 | { 126 | name: "Get corners", 127 | fields: fields{Min: pixel.V(0, 0), Max: pixel.V(10, 10)}, 128 | want: [4]pixel.Vec{ 129 | pixel.V(0, 0), 130 | pixel.V(0, 10), 131 | pixel.V(10, 10), 132 | pixel.V(10, 0), 133 | }, 134 | }, 135 | } 136 | for _, tt := range tests { 137 | t.Run(tt.name, func(t *testing.T) { 138 | r := pixel.Rect{ 139 | Min: tt.fields.Min, 140 | Max: tt.fields.Max, 141 | } 142 | if got := r.Vertices(); !reflect.DeepEqual(got, tt.want) { 143 | t.Errorf("Rect.Vertices() = %v, want %v", got, tt.want) 144 | } 145 | }) 146 | } 147 | } 148 | 149 | func TestRect_IntersectCircle(t *testing.T) { 150 | type fields struct { 151 | Min pixel.Vec 152 | Max pixel.Vec 153 | } 154 | type args struct { 155 | c pixel.Circle 156 | } 157 | tests := []struct { 158 | name string 159 | fields fields 160 | args args 161 | want pixel.Vec 162 | }{ 163 | { 164 | name: "Rect.IntersectCircle(): no overlap", 165 | fields: fields{Min: pixel.ZV, Max: pixel.V(10, 10)}, 166 | args: args{c: pixel.C(pixel.V(50, 50), 1)}, 167 | want: pixel.ZV, 168 | }, 169 | { 170 | name: "Rect.IntersectCircle(): circle contains rect", 171 | fields: fields{Min: pixel.ZV, Max: pixel.V(10, 10)}, 172 | args: args{c: pixel.C(pixel.V(5, 5), 10)}, 173 | want: pixel.V(-15, 0), 174 | }, 175 | { 176 | name: "Rect.IntersectCircle(): rect contains circle", 177 | fields: fields{Min: pixel.ZV, Max: pixel.V(10, 10)}, 178 | args: args{c: pixel.C(pixel.V(5, 5), 1)}, 179 | want: pixel.V(-6, 0), 180 | }, 181 | { 182 | name: "Rect.IntersectCircle(): circle overlaps bottom-left corner", 183 | fields: fields{Min: pixel.ZV, Max: pixel.V(10, 10)}, 184 | args: args{c: pixel.C(pixel.V(-0.5, -0.5), 1)}, 185 | want: pixel.V(-0.2, -0.2), 186 | }, 187 | { 188 | name: "Rect.IntersectCircle(): circle overlaps top-left corner", 189 | fields: fields{Min: pixel.ZV, Max: pixel.V(10, 10)}, 190 | args: args{c: pixel.C(pixel.V(-0.5, 10.5), 1)}, 191 | want: pixel.V(-0.2, 0.2), 192 | }, 193 | { 194 | name: "Rect.IntersectCircle(): circle overlaps bottom-right corner", 195 | fields: fields{Min: pixel.ZV, Max: pixel.V(10, 10)}, 196 | args: args{c: pixel.C(pixel.V(10.5, -0.5), 1)}, 197 | want: pixel.V(0.2, -0.2), 198 | }, 199 | { 200 | name: "Rect.IntersectCircle(): circle overlaps top-right corner", 201 | fields: fields{Min: pixel.ZV, Max: pixel.V(10, 10)}, 202 | args: args{c: pixel.C(pixel.V(10.5, 10.5), 1)}, 203 | want: pixel.V(0.2, 0.2), 204 | }, 205 | { 206 | name: "Rect.IntersectCircle(): circle overlaps two corners", 207 | fields: fields{Min: pixel.ZV, Max: pixel.V(10, 10)}, 208 | args: args{c: pixel.C(pixel.V(0, 5), 6)}, 209 | want: pixel.V(6, 0), 210 | }, 211 | { 212 | name: "Rect.IntersectCircle(): circle overlaps left edge", 213 | fields: fields{Min: pixel.ZV, Max: pixel.V(10, 10)}, 214 | args: args{c: pixel.C(pixel.V(0, 5), 1)}, 215 | want: pixel.V(1, 0), 216 | }, 217 | { 218 | name: "Rect.IntersectCircle(): circle overlaps bottom edge", 219 | fields: fields{Min: pixel.ZV, Max: pixel.V(10, 10)}, 220 | args: args{c: pixel.C(pixel.V(5, 0), 1)}, 221 | want: pixel.V(0, 1), 222 | }, 223 | { 224 | name: "Rect.IntersectCircle(): circle overlaps right edge", 225 | fields: fields{Min: pixel.ZV, Max: pixel.V(10, 10)}, 226 | args: args{c: pixel.C(pixel.V(10, 5), 1)}, 227 | want: pixel.V(-1, 0), 228 | }, 229 | { 230 | name: "Rect.IntersectCircle(): circle overlaps top edge", 231 | fields: fields{Min: pixel.ZV, Max: pixel.V(10, 10)}, 232 | args: args{c: pixel.C(pixel.V(5, 10), 1)}, 233 | want: pixel.V(0, -1), 234 | }, 235 | { 236 | name: "Rect.IntersectCircle(): edge is tangent of left side", 237 | fields: fields{Min: pixel.ZV, Max: pixel.V(10, 10)}, 238 | args: args{c: pixel.C(pixel.V(-1, 5), 1)}, 239 | want: pixel.ZV, 240 | }, 241 | { 242 | name: "Rect.IntersectCircle(): edge is tangent of top side", 243 | fields: fields{Min: pixel.ZV, Max: pixel.V(10, 10)}, 244 | args: args{c: pixel.C(pixel.V(5, -1), 1)}, 245 | want: pixel.ZV, 246 | }, 247 | { 248 | name: "Rect.IntersectCircle(): circle above rectangle", 249 | fields: fields{Min: pixel.ZV, Max: pixel.V(10, 10)}, 250 | args: args{c: pixel.C(pixel.V(5, 12), 1)}, 251 | want: pixel.ZV, 252 | }, 253 | { 254 | name: "Rect.IntersectCircle(): circle below rectangle", 255 | fields: fields{Min: pixel.ZV, Max: pixel.V(10, 10)}, 256 | args: args{c: pixel.C(pixel.V(5, -2), 1)}, 257 | want: pixel.ZV, 258 | }, 259 | { 260 | name: "Rect.IntersectCircle(): circle left of rectangle", 261 | fields: fields{Min: pixel.ZV, Max: pixel.V(10, 10)}, 262 | args: args{c: pixel.C(pixel.V(-1, 5), 1)}, 263 | want: pixel.ZV, 264 | }, 265 | { 266 | name: "Rect.IntersectCircle(): circle right of rectangle", 267 | fields: fields{Min: pixel.ZV, Max: pixel.V(10, 10)}, 268 | args: args{c: pixel.C(pixel.V(11, 5), 1)}, 269 | want: pixel.ZV, 270 | }, 271 | } 272 | for _, tt := range tests { 273 | t.Run(tt.name, func(t *testing.T) { 274 | r := pixel.Rect{ 275 | Min: tt.fields.Min, 276 | Max: tt.fields.Max, 277 | } 278 | got := r.IntersectCircle(tt.args.c) 279 | if !closeEnough(got.X, tt.want.X, 2) || !closeEnough(got.Y, tt.want.Y, 2) { 280 | t.Errorf("Rect.IntersectCircle() = %v, want %v", got, tt.want) 281 | } 282 | }) 283 | } 284 | } 285 | 286 | func TestRect_IntersectionPoints(t *testing.T) { 287 | type fields struct { 288 | Min pixel.Vec 289 | Max pixel.Vec 290 | } 291 | type args struct { 292 | l pixel.Line 293 | } 294 | tests := []struct { 295 | name string 296 | fields fields 297 | args args 298 | want []pixel.Vec 299 | }{ 300 | { 301 | name: "No intersection points", 302 | fields: fields{Min: pixel.V(1, 1), Max: pixel.V(5, 5)}, 303 | args: args{l: pixel.L(pixel.V(-5, 0), pixel.V(-2, 2))}, 304 | want: []pixel.Vec{}, 305 | }, 306 | { 307 | name: "One intersection point", 308 | fields: fields{Min: pixel.V(1, 1), Max: pixel.V(5, 5)}, 309 | args: args{l: pixel.L(pixel.V(2, 0), pixel.V(2, 3))}, 310 | want: []pixel.Vec{pixel.V(2, 1)}, 311 | }, 312 | { 313 | name: "Two intersection points", 314 | fields: fields{Min: pixel.V(1, 1), Max: pixel.V(5, 5)}, 315 | args: args{l: pixel.L(pixel.V(0, 2), pixel.V(6, 2))}, 316 | want: []pixel.Vec{pixel.V(1, 2), pixel.V(5, 2)}, 317 | }, 318 | } 319 | for _, tt := range tests { 320 | t.Run(tt.name, func(t *testing.T) { 321 | r := pixel.Rect{ 322 | Min: tt.fields.Min, 323 | Max: tt.fields.Max, 324 | } 325 | if got := r.IntersectionPoints(tt.args.l); !reflect.DeepEqual(got, tt.want) { 326 | t.Errorf("Rect.IntersectPoints() = %v, want %v", got, tt.want) 327 | } 328 | }) 329 | } 330 | } 331 | 332 | var rectIntTests = []struct { 333 | name string 334 | r1, r2 pixel.Rect 335 | want pixel.Rect 336 | intersect bool 337 | }{ 338 | { 339 | name: "Nothing touching", 340 | r1: pixel.R(0, 0, 10, 10), 341 | r2: pixel.R(21, 21, 40, 40), 342 | want: pixel.ZR, 343 | }, 344 | { 345 | name: "Edge touching", 346 | r1: pixel.R(0, 0, 10, 10), 347 | r2: pixel.R(10, 10, 20, 20), 348 | want: pixel.ZR, 349 | }, 350 | { 351 | name: "Bit of overlap", 352 | r1: pixel.R(0, 0, 10, 10), 353 | r2: pixel.R(0, 9, 20, 20), 354 | want: pixel.R(0, 9, 10, 10), 355 | intersect: true, 356 | }, 357 | { 358 | name: "Fully overlapped", 359 | r1: pixel.R(0, 0, 10, 10), 360 | r2: pixel.R(0, 0, 10, 10), 361 | want: pixel.R(0, 0, 10, 10), 362 | intersect: true, 363 | }, 364 | } 365 | 366 | func TestRect_Intersect(t *testing.T) { 367 | for _, tt := range rectIntTests { 368 | t.Run(tt.name, func(t *testing.T) { 369 | if got := tt.r1.Intersect(tt.r2); !reflect.DeepEqual(got, tt.want) { 370 | t.Errorf("Rect.Intersect() = %v, want %v", got, tt.want) 371 | } 372 | }) 373 | } 374 | } 375 | 376 | func TestRect_Intersects(t *testing.T) { 377 | for _, tt := range rectIntTests { 378 | t.Run(tt.name, func(t *testing.T) { 379 | if got := tt.r1.Intersects(tt.r2); got != tt.intersect { 380 | t.Errorf("Rect.Intersects() = %v, want %v", got, tt.want) 381 | } 382 | }) 383 | } 384 | } 385 | -------------------------------------------------------------------------------- /sprite.go: -------------------------------------------------------------------------------- 1 | package pixel 2 | 3 | import "image/color" 4 | 5 | // Sprite is a drawable frame of a Picture. It's anchored by the center of it's Picture's frame. 6 | // 7 | // Frame specifies a rectangular portion of the Picture that will be drawn. For example, this 8 | // creates a Sprite that draws the whole Picture: 9 | // 10 | // sprite := pixel.NewSprite(pic, pic.Bounds()) 11 | // 12 | // Note, that Sprite caches the results of MakePicture from Targets it's drawn to for each Picture 13 | // it's set to. What it means is that using a Sprite with an unbounded number of Pictures leads to a 14 | // memory leak, since Sprite caches them and never forgets. In such a situation, create a new Sprite 15 | // for each Picture. 16 | type Sprite struct { 17 | tri *TrianglesData 18 | frame Rect 19 | d Drawer 20 | 21 | matrix Matrix 22 | mask RGBA 23 | } 24 | 25 | // NewSprite creates a Sprite from the supplied frame of a Picture. 26 | func NewSprite(pic Picture, frame Rect) *Sprite { 27 | tri := MakeTrianglesData(6) 28 | s := &Sprite{ 29 | tri: tri, 30 | d: Drawer{Triangles: tri, Cached: true}, 31 | } 32 | s.matrix = IM 33 | s.mask = Alpha(1) 34 | s.Set(pic, frame) 35 | return s 36 | } 37 | 38 | // Set sets a new frame of a Picture for this Sprite. 39 | func (s *Sprite) Set(pic Picture, frame Rect) { 40 | s.d.Picture = pic 41 | if frame != s.frame { 42 | s.frame = frame 43 | s.calcData() 44 | } 45 | } 46 | 47 | // SetCached makes the sprite cache all the 48 | // incoming pictures if the argument is true, and 49 | // doesn't make it do that if the argument is false. 50 | func (s *Sprite) SetCached(cached bool) { 51 | s.d.Cached = cached 52 | } 53 | 54 | // Picture returns the current Sprite's Picture. 55 | func (s *Sprite) Picture() Picture { 56 | return s.d.Picture 57 | } 58 | 59 | // Frame returns the current Sprite's frame. 60 | func (s *Sprite) Frame() Rect { 61 | return s.frame 62 | } 63 | 64 | // Draw draws the Sprite onto the provided Target. The Sprite will be transformed by the given Matrix. 65 | // 66 | // This method is equivalent to calling DrawColorMask with nil color mask. 67 | func (s *Sprite) Draw(t Target, matrix Matrix) { 68 | s.DrawColorMask(t, matrix, nil) 69 | } 70 | 71 | // DrawColorMask draws the Sprite onto the provided Target. The Sprite will be transformed by the 72 | // given Matrix and all of it's color will be multiplied by the given mask. 73 | // 74 | // If the mask is nil, a fully opaque white mask will be used, which causes no effect. 75 | func (s *Sprite) DrawColorMask(t Target, matrix Matrix, mask color.Color) { 76 | dirty := false 77 | if matrix != s.matrix { 78 | s.matrix = matrix 79 | dirty = true 80 | } 81 | if mask == nil { 82 | mask = Alpha(1) 83 | } 84 | rgba := ToRGBA(mask) 85 | if rgba != s.mask { 86 | s.mask = rgba 87 | dirty = true 88 | } 89 | 90 | if dirty { 91 | s.calcData() 92 | } 93 | 94 | s.d.Draw(t) 95 | } 96 | 97 | func (s *Sprite) calcData() { 98 | var ( 99 | center = s.frame.Center() 100 | horizontal = V(s.frame.W()/2, 0) 101 | vertical = V(0, s.frame.H()/2) 102 | ) 103 | 104 | (*s.tri)[0].Position = Vec{}.Sub(horizontal).Sub(vertical) 105 | (*s.tri)[1].Position = Vec{}.Add(horizontal).Sub(vertical) 106 | (*s.tri)[2].Position = Vec{}.Add(horizontal).Add(vertical) 107 | (*s.tri)[3].Position = Vec{}.Sub(horizontal).Sub(vertical) 108 | (*s.tri)[4].Position = Vec{}.Add(horizontal).Add(vertical) 109 | (*s.tri)[5].Position = Vec{}.Sub(horizontal).Add(vertical) 110 | 111 | for i := range *s.tri { 112 | (*s.tri)[i].Color = s.mask 113 | (*s.tri)[i].Picture = center.Add((*s.tri)[i].Position) 114 | (*s.tri)[i].Intensity = 1 115 | (*s.tri)[i].Position = s.matrix.Project((*s.tri)[i].Position) 116 | } 117 | 118 | s.d.Dirty() 119 | } 120 | -------------------------------------------------------------------------------- /text/atlas.go: -------------------------------------------------------------------------------- 1 | package text 2 | 3 | import ( 4 | "image" 5 | "image/draw" 6 | "sort" 7 | "unicode" 8 | 9 | "github.com/faiface/pixel" 10 | "golang.org/x/image/font" 11 | "golang.org/x/image/math/fixed" 12 | ) 13 | 14 | // Atlas7x13 is an Atlas using basicfont.Face7x13 with the ASCII rune set 15 | var Atlas7x13 *Atlas 16 | 17 | // Glyph describes one glyph in an Atlas. 18 | type Glyph struct { 19 | Dot pixel.Vec 20 | Frame pixel.Rect 21 | Advance float64 22 | } 23 | 24 | // Atlas is a set of pre-drawn glyphs of a fixed set of runes. This allows for efficient text drawing. 25 | type Atlas struct { 26 | face font.Face 27 | pic pixel.Picture 28 | mapping map[rune]Glyph 29 | ascent float64 30 | descent float64 31 | lineHeight float64 32 | } 33 | 34 | // NewAtlas creates a new Atlas containing glyphs of the union of the given sets of runes (plus 35 | // unicode.ReplacementChar) from the given font face. 36 | // 37 | // Creating an Atlas is rather expensive, do not create a new Atlas each frame. 38 | // 39 | // Do not destroy or close the font.Face after creating the Atlas. Atlas still uses it. 40 | func NewAtlas(face font.Face, runeSets ...[]rune) *Atlas { 41 | seen := make(map[rune]bool) 42 | runes := []rune{unicode.ReplacementChar} 43 | for _, set := range runeSets { 44 | for _, r := range set { 45 | if !seen[r] { 46 | runes = append(runes, r) 47 | seen[r] = true 48 | } 49 | } 50 | } 51 | 52 | fixedMapping, fixedBounds := makeSquareMapping(face, runes, fixed.I(2)) 53 | 54 | atlasImg := image.NewRGBA(image.Rect( 55 | fixedBounds.Min.X.Floor(), 56 | fixedBounds.Min.Y.Floor(), 57 | fixedBounds.Max.X.Ceil(), 58 | fixedBounds.Max.Y.Ceil(), 59 | )) 60 | 61 | for r, fg := range fixedMapping { 62 | if dr, mask, maskp, _, ok := face.Glyph(fg.dot, r); ok { 63 | draw.Draw(atlasImg, dr, mask, maskp, draw.Src) 64 | } 65 | } 66 | 67 | bounds := pixel.R( 68 | i2f(fixedBounds.Min.X), 69 | i2f(fixedBounds.Min.Y), 70 | i2f(fixedBounds.Max.X), 71 | i2f(fixedBounds.Max.Y), 72 | ) 73 | 74 | mapping := make(map[rune]Glyph) 75 | for r, fg := range fixedMapping { 76 | mapping[r] = Glyph{ 77 | Dot: pixel.V( 78 | i2f(fg.dot.X), 79 | bounds.Max.Y-(i2f(fg.dot.Y)-bounds.Min.Y), 80 | ), 81 | Frame: pixel.R( 82 | i2f(fg.frame.Min.X), 83 | bounds.Max.Y-(i2f(fg.frame.Min.Y)-bounds.Min.Y), 84 | i2f(fg.frame.Max.X), 85 | bounds.Max.Y-(i2f(fg.frame.Max.Y)-bounds.Min.Y), 86 | ).Norm(), 87 | Advance: i2f(fg.advance), 88 | } 89 | } 90 | 91 | return &Atlas{ 92 | face: face, 93 | pic: pixel.PictureDataFromImage(atlasImg), 94 | mapping: mapping, 95 | ascent: i2f(face.Metrics().Ascent), 96 | descent: i2f(face.Metrics().Descent), 97 | lineHeight: i2f(face.Metrics().Height), 98 | } 99 | } 100 | 101 | // Picture returns the underlying Picture containing an arrangement of all the glyphs contained 102 | // within the Atlas. 103 | func (a *Atlas) Picture() pixel.Picture { 104 | return a.pic 105 | } 106 | 107 | // Contains reports wheter r in contained within the Atlas. 108 | func (a *Atlas) Contains(r rune) bool { 109 | _, ok := a.mapping[r] 110 | return ok 111 | } 112 | 113 | // Glyph returns the description of r within the Atlas. 114 | func (a *Atlas) Glyph(r rune) Glyph { 115 | return a.mapping[r] 116 | } 117 | 118 | // Kern returns the kerning distance between runes r0 and r1. Positive distance means that the 119 | // glyphs should be further apart. 120 | func (a *Atlas) Kern(r0, r1 rune) float64 { 121 | return i2f(a.face.Kern(r0, r1)) 122 | } 123 | 124 | // Ascent returns the distance from the top of the line to the baseline. 125 | func (a *Atlas) Ascent() float64 { 126 | return a.ascent 127 | } 128 | 129 | // Descent returns the distance from the baseline to the bottom of the line. 130 | func (a *Atlas) Descent() float64 { 131 | return a.descent 132 | } 133 | 134 | // LineHeight returns the recommended vertical distance between two lines of text. 135 | func (a *Atlas) LineHeight() float64 { 136 | return a.lineHeight 137 | } 138 | 139 | // DrawRune returns parameters necessary for drawing a rune glyph. 140 | // 141 | // Rect is a rectangle where the glyph should be positioned. Frame is the glyph frame inside the 142 | // Atlas's Picture. NewDot is the new position of the dot. 143 | func (a *Atlas) DrawRune(prevR, r rune, dot pixel.Vec) (rect, frame, bounds pixel.Rect, newDot pixel.Vec) { 144 | if !a.Contains(r) { 145 | r = unicode.ReplacementChar 146 | } 147 | if !a.Contains(unicode.ReplacementChar) { 148 | return pixel.Rect{}, pixel.Rect{}, pixel.Rect{}, dot 149 | } 150 | if !a.Contains(prevR) { 151 | prevR = unicode.ReplacementChar 152 | } 153 | 154 | if prevR >= 0 { 155 | dot.X += a.Kern(prevR, r) 156 | } 157 | 158 | glyph := a.Glyph(r) 159 | 160 | rect = glyph.Frame.Moved(dot.Sub(glyph.Dot)) 161 | bounds = rect 162 | 163 | if bounds.W()*bounds.H() != 0 { 164 | bounds = pixel.R( 165 | bounds.Min.X, 166 | dot.Y-a.Descent(), 167 | bounds.Max.X, 168 | dot.Y+a.Ascent(), 169 | ) 170 | } 171 | 172 | dot.X += glyph.Advance 173 | 174 | return rect, glyph.Frame, bounds, dot 175 | } 176 | 177 | type fixedGlyph struct { 178 | dot fixed.Point26_6 179 | frame fixed.Rectangle26_6 180 | advance fixed.Int26_6 181 | } 182 | 183 | // makeSquareMapping finds an optimal glyph arrangement of the given runes, so that their common 184 | // bounding box is as square as possible. 185 | func makeSquareMapping(face font.Face, runes []rune, padding fixed.Int26_6) (map[rune]fixedGlyph, fixed.Rectangle26_6) { 186 | width := sort.Search(int(fixed.I(1024*1024)), func(i int) bool { 187 | width := fixed.Int26_6(i) 188 | _, bounds := makeMapping(face, runes, padding, width) 189 | return bounds.Max.X-bounds.Min.X >= bounds.Max.Y-bounds.Min.Y 190 | }) 191 | return makeMapping(face, runes, padding, fixed.Int26_6(width)) 192 | } 193 | 194 | // makeMapping arranges glyphs of the given runes into rows in such a way, that no glyph is located 195 | // fully to the right of the specified width. Specifically, it places glyphs in a row one by one and 196 | // once it reaches the specified width, it starts a new row. 197 | func makeMapping(face font.Face, runes []rune, padding, width fixed.Int26_6) (map[rune]fixedGlyph, fixed.Rectangle26_6) { 198 | mapping := make(map[rune]fixedGlyph) 199 | bounds := fixed.Rectangle26_6{} 200 | 201 | dot := fixed.P(0, 0) 202 | 203 | for _, r := range runes { 204 | b, advance, ok := face.GlyphBounds(r) 205 | if !ok { 206 | continue 207 | } 208 | 209 | // this is important for drawing, artifacts arise otherwise 210 | frame := fixed.Rectangle26_6{ 211 | Min: fixed.P(b.Min.X.Floor(), b.Min.Y.Floor()), 212 | Max: fixed.P(b.Max.X.Ceil(), b.Max.Y.Ceil()), 213 | } 214 | 215 | dot.X -= frame.Min.X 216 | frame = frame.Add(dot) 217 | 218 | mapping[r] = fixedGlyph{ 219 | dot: dot, 220 | frame: frame, 221 | advance: advance, 222 | } 223 | bounds = bounds.Union(frame) 224 | 225 | dot.X = frame.Max.X 226 | 227 | // padding + align to integer 228 | dot.X += padding 229 | dot.X = fixed.I(dot.X.Ceil()) 230 | 231 | // width exceeded, new row 232 | if frame.Max.X >= width { 233 | dot.X = 0 234 | dot.Y += face.Metrics().Ascent + face.Metrics().Descent 235 | 236 | // padding + align to integer 237 | dot.Y += padding 238 | dot.Y = fixed.I(dot.Y.Ceil()) 239 | } 240 | } 241 | 242 | return mapping, bounds 243 | } 244 | 245 | func i2f(i fixed.Int26_6) float64 { 246 | return float64(i) / (1 << 6) 247 | } 248 | -------------------------------------------------------------------------------- /text/atlas_test.go: -------------------------------------------------------------------------------- 1 | package text_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/faiface/pixel/text" 7 | "golang.org/x/image/font/inconsolata" 8 | ) 9 | 10 | func TestAtlas7x13(t *testing.T) { 11 | if text.Atlas7x13 == nil { 12 | t.Fatalf("Atlas7x13 is nil") 13 | } 14 | 15 | for _, tt := range []struct { 16 | runes []rune 17 | want bool 18 | }{{text.ASCII, true}, {[]rune("ÅÄÖ"), false}} { 19 | for _, r := range tt.runes { 20 | if got := text.Atlas7x13.Contains(r); got != tt.want { 21 | t.Fatalf("Atlas7x13.Contains('%s') = %v, want %v", string(r), got, tt.want) 22 | } 23 | } 24 | } 25 | } 26 | 27 | func TestAtlasInconsolata(t *testing.T) { 28 | text.NewAtlas(inconsolata.Regular8x16, text.ASCII) 29 | } 30 | -------------------------------------------------------------------------------- /text/doc.go: -------------------------------------------------------------------------------- 1 | // Package text implements efficient text drawing for the Pixel library. 2 | package text 3 | -------------------------------------------------------------------------------- /text/text.go: -------------------------------------------------------------------------------- 1 | package text 2 | 3 | import ( 4 | "image/color" 5 | "math" 6 | "unicode" 7 | "unicode/utf8" 8 | 9 | "github.com/faiface/pixel" 10 | "golang.org/x/image/font/basicfont" 11 | ) 12 | 13 | // ASCII is a set of all ASCII runes. These runes are codepoints from 32 to 127 inclusive. 14 | var ASCII []rune 15 | 16 | func init() { 17 | ASCII = make([]rune, unicode.MaxASCII-32) 18 | for i := range ASCII { 19 | ASCII[i] = rune(32 + i) 20 | } 21 | Atlas7x13 = NewAtlas(basicfont.Face7x13, ASCII) 22 | } 23 | 24 | // RangeTable takes a *unicode.RangeTable and generates a set of runes contained within that 25 | // RangeTable. 26 | func RangeTable(table *unicode.RangeTable) []rune { 27 | var runes []rune 28 | for _, rng := range table.R16 { 29 | for r := rng.Lo; r <= rng.Hi; r += rng.Stride { 30 | runes = append(runes, rune(r)) 31 | } 32 | } 33 | for _, rng := range table.R32 { 34 | for r := rng.Lo; r <= rng.Hi; r += rng.Stride { 35 | runes = append(runes, rune(r)) 36 | } 37 | } 38 | return runes 39 | } 40 | 41 | // Text allows for effiecient and convenient text drawing. 42 | // 43 | // To create a Text object, use the New constructor: 44 | // txt := text.New(pixel.ZV, text.NewAtlas(face, text.ASCII)) 45 | // 46 | // As suggested by the constructor, a Text object is always associated with one font face and a 47 | // fixed set of runes. For example, the Text we created above can draw text using the font face 48 | // contained in the face variable and is capable of drawing ASCII characters. 49 | // 50 | // Here we create a Text object which can draw ASCII and Katakana characters: 51 | // txt := text.New(0, text.NewAtlas(face, text.ASCII, text.RangeTable(unicode.Katakana))) 52 | // 53 | // Similarly to IMDraw, Text functions as a buffer. It implements io.Writer interface, so writing 54 | // text to it is really simple: 55 | // fmt.Print(txt, "Hello, world!") 56 | // 57 | // Newlines, tabs and carriage returns are supported. 58 | // 59 | // Finally, if we want the written text to show up on some other Target, we can draw it: 60 | // txt.Draw(target) 61 | // 62 | // Text exports two important fields: Orig and Dot. Dot is the position where the next character 63 | // will be written. Dot is automatically moved when writing to a Text object, but you can also 64 | // manipulate it manually. Orig specifies the text origin, usually the top-left dot position. Dot is 65 | // always aligned to Orig when writing newlines. The Clear method resets the Dot to Orig. 66 | type Text struct { 67 | // Orig specifies the text origin, usually the top-left dot position. Dot is always aligned 68 | // to Orig when writing newlines. 69 | Orig pixel.Vec 70 | 71 | // Dot is the position where the next character will be written. Dot is automatically moved 72 | // when writing to a Text object, but you can also manipulate it manually 73 | Dot pixel.Vec 74 | 75 | // Color is the color of the text that is to be written. Defaults to white. 76 | Color color.Color 77 | 78 | // LineHeight is the vertical distance between two lines of text. 79 | // 80 | // Example: 81 | // txt.LineHeight = 1.5 * txt.Atlas().LineHeight() 82 | LineHeight float64 83 | 84 | // TabWidth is the horizontal tab width. Tab characters will align to the multiples of this 85 | // width. 86 | // 87 | // Example: 88 | // txt.TabWidth = 8 * txt.Atlas().Glyph(' ').Advance 89 | TabWidth float64 90 | 91 | atlas *Atlas 92 | 93 | buf []byte 94 | prevR rune 95 | bounds pixel.Rect 96 | glyph pixel.TrianglesData 97 | tris pixel.TrianglesData 98 | 99 | mat pixel.Matrix 100 | col pixel.RGBA 101 | trans pixel.TrianglesData 102 | transD pixel.Drawer 103 | dirty bool 104 | anchor pixel.Anchor 105 | } 106 | 107 | // New creates a new Text capable of drawing runes contained in the provided Atlas. Orig and Dot 108 | // will be initially set to orig. 109 | // 110 | // Here we create a Text capable of drawing ASCII characters using the Go Regular font. 111 | // ttf, err := truetype.Parse(goregular.TTF) 112 | // if err != nil { 113 | // panic(err) 114 | // } 115 | // face := truetype.NewFace(ttf, &truetype.Options{ 116 | // Size: 14, 117 | // }) 118 | // txt := text.New(orig, text.NewAtlas(face, text.ASCII)) 119 | func New(orig pixel.Vec, atlas *Atlas) *Text { 120 | txt := &Text{ 121 | Orig: orig, 122 | Dot: orig, 123 | Color: pixel.Alpha(1), 124 | LineHeight: atlas.LineHeight(), 125 | TabWidth: atlas.Glyph(' ').Advance * 4, 126 | atlas: atlas, 127 | mat: pixel.IM, 128 | col: pixel.Alpha(1), 129 | } 130 | 131 | txt.glyph.SetLen(6) 132 | for i := range txt.glyph { 133 | txt.glyph[i].Color = pixel.Alpha(1) 134 | txt.glyph[i].Intensity = 1 135 | } 136 | 137 | txt.transD.Picture = txt.atlas.pic 138 | txt.transD.Triangles = &txt.trans 139 | txt.transD.Cached = true 140 | 141 | txt.Clear() 142 | 143 | return txt 144 | } 145 | 146 | // Atlas returns the underlying Text's Atlas containing all of the pre-drawn glyphs. The Atlas is 147 | // also useful for getting values such as the recommended line height. 148 | func (txt *Text) Atlas() *Atlas { 149 | return txt.atlas 150 | } 151 | 152 | // Bounds returns the bounding box of the text currently written to the Text excluding whitespace. 153 | // 154 | // If the Text is empty, a zero rectangle is returned. 155 | func (txt *Text) Bounds() pixel.Rect { 156 | return txt.bounds 157 | } 158 | 159 | // BoundsOf returns the bounding box of s if it was to be written to the Text right now. 160 | func (txt *Text) BoundsOf(s string) pixel.Rect { 161 | dot := txt.Dot 162 | prevR := txt.prevR 163 | bounds := pixel.Rect{} 164 | 165 | for _, r := range s { 166 | var control bool 167 | dot, control = txt.controlRune(r, dot) 168 | if control { 169 | continue 170 | } 171 | 172 | var b pixel.Rect 173 | _, _, b, dot = txt.Atlas().DrawRune(prevR, r, dot) 174 | 175 | if bounds.W()*bounds.H() == 0 { 176 | bounds = b 177 | } else { 178 | bounds = bounds.Union(b) 179 | } 180 | 181 | prevR = r 182 | } 183 | 184 | return bounds 185 | } 186 | 187 | // AlignedTo returns the text moved by the given anchor. 188 | func (txt *Text) AlignedTo(anchor pixel.Anchor) *Text { 189 | txt.anchor = anchor 190 | return txt 191 | } 192 | 193 | // Clear removes all written text from the Text. The Dot field is reset to Orig. 194 | func (txt *Text) Clear() { 195 | txt.prevR = -1 196 | txt.bounds = pixel.Rect{} 197 | txt.tris.SetLen(0) 198 | txt.dirty = true 199 | txt.Dot = txt.Orig 200 | } 201 | 202 | // Write writes a slice of bytes to the Text. This method never fails, always returns len(p), nil. 203 | func (txt *Text) Write(p []byte) (n int, err error) { 204 | txt.buf = append(txt.buf, p...) 205 | txt.drawBuf() 206 | return len(p), nil 207 | } 208 | 209 | // WriteString writes a string to the Text. This method never fails, always returns len(s), nil. 210 | func (txt *Text) WriteString(s string) (n int, err error) { 211 | txt.buf = append(txt.buf, s...) 212 | txt.drawBuf() 213 | return len(s), nil 214 | } 215 | 216 | // WriteByte writes a byte to the Text. This method never fails, always returns nil. 217 | // 218 | // Writing a multi-byte rune byte-by-byte is perfectly supported. 219 | func (txt *Text) WriteByte(c byte) error { 220 | txt.buf = append(txt.buf, c) 221 | txt.drawBuf() 222 | return nil 223 | } 224 | 225 | // WriteRune writes a rune to the Text. This method never fails, always returns utf8.RuneLen(r), nil. 226 | func (txt *Text) WriteRune(r rune) (n int, err error) { 227 | var b [4]byte 228 | n = utf8.EncodeRune(b[:], r) 229 | txt.buf = append(txt.buf, b[:n]...) 230 | txt.drawBuf() 231 | return n, nil 232 | } 233 | 234 | // Draw draws all text written to the Text to the provided Target. The text is transformed by the 235 | // provided Matrix. 236 | // 237 | // This method is equivalent to calling DrawColorMask with nil color mask. 238 | // 239 | // If there's a lot of text written to the Text, changing a matrix or a color mask often might hurt 240 | // performance. Consider using your Target's SetMatrix or SetColorMask methods if available. 241 | func (txt *Text) Draw(t pixel.Target, matrix pixel.Matrix) { 242 | txt.DrawColorMask(t, matrix, nil) 243 | } 244 | 245 | // DrawColorMask draws all text written to the Text to the provided Target. The text is transformed 246 | // by the provided Matrix and masked by the provided color mask. 247 | // 248 | // If there's a lot of text written to the Text, changing a matrix or a color mask often might hurt 249 | // performance. Consider using your Target's SetMatrix or SetColorMask methods if available. 250 | func (txt *Text) DrawColorMask(t pixel.Target, matrix pixel.Matrix, mask color.Color) { 251 | if matrix != txt.mat { 252 | txt.mat = matrix 253 | txt.dirty = true 254 | } 255 | 256 | offset := txt.Orig.Sub(txt.Bounds().Max.Add(txt.Bounds().AnchorPos(txt.anchor.Opposite()))) 257 | txt.mat = pixel.IM.Moved(offset).Chained(txt.mat) 258 | 259 | if mask == nil { 260 | mask = pixel.Alpha(1) 261 | } 262 | rgba := pixel.ToRGBA(mask) 263 | if rgba != txt.col { 264 | txt.col = rgba 265 | txt.dirty = true 266 | } 267 | 268 | if txt.dirty { 269 | txt.trans.SetLen(txt.tris.Len()) 270 | txt.trans.Update(&txt.tris) 271 | 272 | for i := range txt.trans { 273 | txt.trans[i].Position = txt.mat.Project(txt.trans[i].Position) 274 | txt.trans[i].Color = txt.trans[i].Color.Mul(txt.col) 275 | } 276 | 277 | txt.transD.Dirty() 278 | txt.dirty = false 279 | } 280 | 281 | txt.transD.Draw(t) 282 | } 283 | 284 | // controlRune checks if r is a control rune (newline, tab, ...). If it is, a new dot position and 285 | // true is returned. If r is not a control rune, the original dot and false is returned. 286 | func (txt *Text) controlRune(r rune, dot pixel.Vec) (newDot pixel.Vec, control bool) { 287 | switch r { 288 | case '\n': 289 | dot.X = txt.Orig.X 290 | dot.Y -= txt.LineHeight 291 | case '\r': 292 | dot.X = txt.Orig.X 293 | case '\t': 294 | rem := math.Mod(dot.X-txt.Orig.X, txt.TabWidth) 295 | rem = math.Mod(rem, rem+txt.TabWidth) 296 | if rem == 0 { 297 | rem = txt.TabWidth 298 | } 299 | dot.X += rem 300 | default: 301 | return dot, false 302 | } 303 | return dot, true 304 | } 305 | 306 | func (txt *Text) drawBuf() { 307 | if !utf8.FullRune(txt.buf) { 308 | return 309 | } 310 | 311 | rgba := pixel.ToRGBA(txt.Color) 312 | for i := range txt.glyph { 313 | txt.glyph[i].Color = rgba 314 | } 315 | 316 | for utf8.FullRune(txt.buf) { 317 | r, size := utf8.DecodeRune(txt.buf) 318 | txt.buf = txt.buf[size:] 319 | 320 | var control bool 321 | txt.Dot, control = txt.controlRune(r, txt.Dot) 322 | if control { 323 | continue 324 | } 325 | 326 | var rect, frame, bounds pixel.Rect 327 | rect, frame, bounds, txt.Dot = txt.Atlas().DrawRune(txt.prevR, r, txt.Dot) 328 | 329 | txt.prevR = r 330 | 331 | rv := [...]pixel.Vec{ 332 | {X: rect.Min.X, Y: rect.Min.Y}, 333 | {X: rect.Max.X, Y: rect.Min.Y}, 334 | {X: rect.Max.X, Y: rect.Max.Y}, 335 | {X: rect.Min.X, Y: rect.Max.Y}, 336 | } 337 | 338 | fv := [...]pixel.Vec{ 339 | {X: frame.Min.X, Y: frame.Min.Y}, 340 | {X: frame.Max.X, Y: frame.Min.Y}, 341 | {X: frame.Max.X, Y: frame.Max.Y}, 342 | {X: frame.Min.X, Y: frame.Max.Y}, 343 | } 344 | 345 | for i, j := range [...]int{0, 1, 2, 0, 2, 3} { 346 | txt.glyph[i].Position = rv[j] 347 | txt.glyph[i].Picture = fv[j] 348 | } 349 | 350 | txt.tris = append(txt.tris, txt.glyph...) 351 | txt.dirty = true 352 | 353 | if txt.bounds.W()*txt.bounds.H() == 0 { 354 | txt.bounds = bounds 355 | } else { 356 | txt.bounds = txt.bounds.Union(bounds) 357 | } 358 | } 359 | } 360 | -------------------------------------------------------------------------------- /text/text_test.go: -------------------------------------------------------------------------------- 1 | package text_test 2 | 3 | import ( 4 | "fmt" 5 | "math/rand" 6 | "testing" 7 | "unicode" 8 | 9 | "golang.org/x/image/font/basicfont" 10 | "golang.org/x/image/font/gofont/goregular" 11 | 12 | "github.com/faiface/pixel" 13 | "github.com/faiface/pixel/text" 14 | "github.com/golang/freetype/truetype" 15 | ) 16 | 17 | func TestClear(t *testing.T) { 18 | txt := text.New(pixel.ZV, text.Atlas7x13) 19 | 20 | if got, want := txt.Dot, pixel.ZV; !eqVectors(got, want) { 21 | t.Fatalf("txt.Dot = %v, want %v", got, want) 22 | } 23 | 24 | fmt.Fprint(txt, "Test\nClear") 25 | 26 | if got, want := txt.Dot, pixel.V(35, -13); !eqVectors(got, want) { 27 | t.Fatalf("txt.Dot = %v, want %v", got, want) 28 | } 29 | 30 | txt.Clear() 31 | 32 | if got, want := txt.Dot, pixel.ZV; !eqVectors(got, want) { 33 | t.Fatalf("txt.Dot = %v, want %v", got, want) 34 | } 35 | } 36 | 37 | func BenchmarkNewAtlas(b *testing.B) { 38 | runeSets := []struct { 39 | name string 40 | set []rune 41 | }{ 42 | {"ASCII", text.ASCII}, 43 | {"Latin", text.RangeTable(unicode.Latin)}, 44 | } 45 | 46 | ttf, _ := truetype.Parse(goregular.TTF) 47 | face := truetype.NewFace(ttf, &truetype.Options{ 48 | Size: 16, 49 | GlyphCacheEntries: 1, 50 | }) 51 | 52 | for _, runeSet := range runeSets { 53 | b.Run(runeSet.name, func(b *testing.B) { 54 | for i := 0; i < b.N; i++ { 55 | _ = text.NewAtlas(face, runeSet.set) 56 | } 57 | }) 58 | } 59 | } 60 | 61 | func BenchmarkTextWrite(b *testing.B) { 62 | runeSet := text.ASCII 63 | atlas := text.NewAtlas(basicfont.Face7x13, runeSet) 64 | 65 | lengths := []int{1, 10, 100, 1000} 66 | chunks := make([][]byte, len(lengths)) 67 | for i := range chunks { 68 | chunk := make([]rune, lengths[i]) 69 | for j := range chunk { 70 | chunk[j] = runeSet[rand.Intn(len(runeSet))] 71 | } 72 | chunks[i] = []byte(string(chunk)) 73 | } 74 | 75 | for _, chunk := range chunks { 76 | b.Run(fmt.Sprintf("%d", len(chunk)), func(b *testing.B) { 77 | txt := text.New(pixel.ZV, atlas) 78 | for i := 0; i < b.N; i++ { 79 | _, _ = txt.Write(chunk) 80 | } 81 | }) 82 | } 83 | } 84 | 85 | func eqVectors(a, b pixel.Vec) bool { 86 | return (a.X == b.X && a.Y == b.Y) 87 | } 88 | -------------------------------------------------------------------------------- /vector.go: -------------------------------------------------------------------------------- 1 | package pixel 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | ) 7 | 8 | // Vec is a 2D vector type with X and Y coordinates. 9 | // 10 | // Create vectors with the V constructor: 11 | // 12 | // u := pixel.V(1, 2) 13 | // v := pixel.V(8, -3) 14 | // 15 | // Use various methods to manipulate them: 16 | // 17 | // w := u.Add(v) 18 | // fmt.Println(w) // Vec(9, -1) 19 | // fmt.Println(u.Sub(v)) // Vec(-7, 5) 20 | // u = pixel.V(2, 3) 21 | // v = pixel.V(8, 1) 22 | // if u.X < 0 { 23 | // fmt.Println("this won't happen") 24 | // } 25 | // x := u.Unit().Dot(v.Unit()) 26 | type Vec struct { 27 | X, Y float64 28 | } 29 | 30 | // ZV is a zero vector. 31 | var ZV = Vec{0, 0} 32 | 33 | // V returns a new 2D vector with the given coordinates. 34 | func V(x, y float64) Vec { 35 | return Vec{x, y} 36 | } 37 | 38 | // nearlyEqual compares two float64s and returns whether they are equal, accounting for rounding errors.At worst, the 39 | // result is correct to 7 significant digits. 40 | func nearlyEqual(a, b float64) bool { 41 | epsilon := 0.000001 42 | 43 | if a == b { 44 | return true 45 | } 46 | 47 | diff := math.Abs(a - b) 48 | 49 | if a == 0.0 || b == 0.0 || diff < math.SmallestNonzeroFloat64 { 50 | return diff < (epsilon * math.SmallestNonzeroFloat64) 51 | } 52 | 53 | absA := math.Abs(a) 54 | absB := math.Abs(b) 55 | 56 | return diff/math.Min(absA+absB, math.MaxFloat64) < epsilon 57 | } 58 | 59 | // Eq will compare two vectors and return whether they are equal accounting for rounding errors. At worst, the result 60 | // is correct to 7 significant digits. 61 | func (u Vec) Eq(v Vec) bool { 62 | return nearlyEqual(u.X, v.X) && nearlyEqual(u.Y, v.Y) 63 | } 64 | 65 | // Unit returns a vector of length 1 facing the given angle. 66 | func Unit(angle float64) Vec { 67 | return Vec{1, 0}.Rotated(angle) 68 | } 69 | 70 | // String returns the string representation of the vector u. 71 | // 72 | // u := pixel.V(4.5, -1.3) 73 | // u.String() // returns "Vec(4.5, -1.3)" 74 | // fmt.Println(u) // Vec(4.5, -1.3) 75 | func (u Vec) String() string { 76 | return fmt.Sprintf("Vec(%v, %v)", u.X, u.Y) 77 | } 78 | 79 | // XY returns the components of the vector in two return values. 80 | func (u Vec) XY() (x, y float64) { 81 | return u.X, u.Y 82 | } 83 | 84 | // Add returns the sum of vectors u and v. 85 | func (u Vec) Add(v Vec) Vec { 86 | return Vec{ 87 | u.X + v.X, 88 | u.Y + v.Y, 89 | } 90 | } 91 | 92 | // Sub returns the difference betweeen vectors u and v. 93 | func (u Vec) Sub(v Vec) Vec { 94 | return Vec{ 95 | u.X - v.X, 96 | u.Y - v.Y, 97 | } 98 | } 99 | 100 | // Floor converts x and y to their integer equivalents. 101 | func (u Vec) Floor() Vec { 102 | return Vec{ 103 | math.Floor(u.X), 104 | math.Floor(u.Y), 105 | } 106 | } 107 | 108 | // To returns the vector from u to v. Equivalent to v.Sub(u). 109 | func (u Vec) To(v Vec) Vec { 110 | return Vec{ 111 | v.X - u.X, 112 | v.Y - u.Y, 113 | } 114 | } 115 | 116 | // Scaled returns the vector u multiplied by c. 117 | func (u Vec) Scaled(c float64) Vec { 118 | return Vec{u.X * c, u.Y * c} 119 | } 120 | 121 | // ScaledXY returns the vector u multiplied by the vector v component-wise. 122 | func (u Vec) ScaledXY(v Vec) Vec { 123 | return Vec{u.X * v.X, u.Y * v.Y} 124 | } 125 | 126 | // Len returns the length of the vector u. 127 | func (u Vec) Len() float64 { 128 | return math.Hypot(u.X, u.Y) 129 | } 130 | 131 | // SqLen returns the squared length of the vector u (faster to compute than Len). 132 | func (u Vec) SqLen() float64 { 133 | return u.X*u.X + u.Y*u.Y 134 | } 135 | 136 | // Angle returns the angle between the vector u and the x-axis. The result is in range [-Pi, Pi]. 137 | func (u Vec) Angle() float64 { 138 | return math.Atan2(u.Y, u.X) 139 | } 140 | 141 | // Unit returns a vector of length 1 facing the direction of u (has the same angle). 142 | func (u Vec) Unit() Vec { 143 | if u.X == 0 && u.Y == 0 { 144 | return Vec{1, 0} 145 | } 146 | return u.Scaled(1 / u.Len()) 147 | } 148 | 149 | // Rotated returns the vector u rotated by the given angle in radians. 150 | func (u Vec) Rotated(angle float64) Vec { 151 | sin, cos := math.Sincos(angle) 152 | return Vec{ 153 | u.X*cos - u.Y*sin, 154 | u.X*sin + u.Y*cos, 155 | } 156 | } 157 | 158 | // Normal returns a vector normal to u. Equivalent to u.Rotated(math.Pi / 2), but faster. 159 | func (u Vec) Normal() Vec { 160 | return Vec{-u.Y, u.X} 161 | } 162 | 163 | // Dot returns the dot product of vectors u and v. 164 | func (u Vec) Dot(v Vec) float64 { 165 | return u.X*v.X + u.Y*v.Y 166 | } 167 | 168 | // Cross return the cross product of vectors u and v. 169 | func (u Vec) Cross(v Vec) float64 { 170 | return u.X*v.Y - v.X*u.Y 171 | } 172 | 173 | // Project returns a projection (or component) of vector u in the direction of vector v. 174 | // 175 | // Behaviour is undefined if v is a zero vector. 176 | func (u Vec) Project(v Vec) Vec { 177 | len := u.Dot(v) / v.Len() 178 | return v.Unit().Scaled(len) 179 | } 180 | 181 | // Map applies the function f to both x and y components of the vector u and returns the modified 182 | // vector. 183 | // 184 | // u := pixel.V(10.5, -1.5) 185 | // v := u.Map(math.Floor) // v is Vec(10, -2), both components of u floored 186 | func (u Vec) Map(f func(float64) float64) Vec { 187 | return Vec{ 188 | f(u.X), 189 | f(u.Y), 190 | } 191 | } 192 | 193 | // Lerp returns a linear interpolation between vectors a and b. 194 | // 195 | // This function basically returns a point along the line between a and b and t chooses which one. 196 | // If t is 0, then a will be returned, if t is 1, b will be returned. Anything between 0 and 1 will 197 | // return the appropriate point between a and b and so on. 198 | func Lerp(a, b Vec, t float64) Vec { 199 | return a.Scaled(1 - t).Add(b.Scaled(t)) 200 | } 201 | 202 | // Line is a 2D line segment, between points A and B. 203 | type Line struct { 204 | A, B Vec 205 | } 206 | 207 | // L creates and returns a new Line. 208 | func L(from, to Vec) Line { 209 | return Line{ 210 | A: from, 211 | B: to, 212 | } 213 | } 214 | 215 | // Bounds returns the lines bounding box. This is in the form of a normalized Rect. 216 | func (l Line) Bounds() Rect { 217 | return R(l.A.X, l.A.Y, l.B.X, l.B.Y).Norm() 218 | } 219 | 220 | // Center will return the point at center of the line; that is, the point equidistant from either end. 221 | func (l Line) Center() Vec { 222 | return l.A.Add(l.A.To(l.B).Scaled(0.5)) 223 | } 224 | 225 | // Closest will return the point on the line which is closest to the Vec provided. 226 | func (l Line) Closest(v Vec) Vec { 227 | // between is a helper function which determines whether x is greater than min(a, b) and less than max(a, b) 228 | between := func(a, b, x float64) bool { 229 | min := math.Min(a, b) 230 | max := math.Max(a, b) 231 | return min < x && x < max 232 | } 233 | 234 | // Closest point will be on a line which perpendicular to this line. 235 | // If and only if the infinite perpendicular line intersects the segment. 236 | m, b := l.Formula() 237 | 238 | // Account for horizontal lines 239 | if m == 0 { 240 | x := v.X 241 | y := l.A.Y 242 | 243 | // check if the X coordinate of v is on the line 244 | if between(l.A.X, l.B.X, v.X) { 245 | return V(x, y) 246 | } 247 | 248 | // Otherwise get the closest endpoint 249 | if l.A.To(v).Len() < l.B.To(v).Len() { 250 | return l.A 251 | } 252 | return l.B 253 | } 254 | 255 | // Account for vertical lines 256 | if math.IsInf(math.Abs(m), 1) { 257 | x := l.A.X 258 | y := v.Y 259 | 260 | // check if the Y coordinate of v is on the line 261 | if between(l.A.Y, l.B.Y, v.Y) { 262 | return V(x, y) 263 | } 264 | 265 | // Otherwise get the closest endpoint 266 | if l.A.To(v).Len() < l.B.To(v).Len() { 267 | return l.A 268 | } 269 | return l.B 270 | } 271 | 272 | perpendicularM := -1 / m 273 | perpendicularB := v.Y - (perpendicularM * v.X) 274 | 275 | // Coordinates of intersect (of infinite lines) 276 | x := (perpendicularB - b) / (m - perpendicularM) 277 | y := m*x + b 278 | 279 | // Check if the point lies between the x and y bounds of the segment 280 | if !between(l.A.X, l.B.X, x) && !between(l.A.Y, l.B.Y, y) { 281 | // Not within bounding box 282 | toStart := v.To(l.A) 283 | toEnd := v.To(l.B) 284 | 285 | if toStart.Len() < toEnd.Len() { 286 | return l.A 287 | } 288 | return l.B 289 | } 290 | 291 | return V(x, y) 292 | } 293 | 294 | // Contains returns whether the provided Vec lies on the line. 295 | func (l Line) Contains(v Vec) bool { 296 | return l.Closest(v).Eq(v) 297 | } 298 | 299 | // Formula will return the values that represent the line in the formula: y = mx + b 300 | // This function will return math.Inf+, math.Inf- for a vertical line. 301 | func (l Line) Formula() (m, b float64) { 302 | // Account for horizontal lines 303 | if l.B.Y == l.A.Y { 304 | return 0, l.A.Y 305 | } 306 | 307 | m = (l.B.Y - l.A.Y) / (l.B.X - l.A.X) 308 | b = l.A.Y - (m * l.A.X) 309 | 310 | return m, b 311 | } 312 | 313 | // Intersect will return the point of intersection for the two line segments. If the line segments do not intersect, 314 | // this function will return the zero-vector and false. 315 | func (l Line) Intersect(k Line) (Vec, bool) { 316 | // Check if the lines are parallel 317 | lDir := l.A.To(l.B) 318 | kDir := k.A.To(k.B) 319 | if lDir.X == kDir.X && lDir.Y == kDir.Y { 320 | return ZV, false 321 | } 322 | 323 | // The lines intersect - but potentially not within the line segments. 324 | // Get the intersection point for the lines if they were infinitely long, check if the point exists on both of the 325 | // segments 326 | lm, lb := l.Formula() 327 | km, kb := k.Formula() 328 | 329 | // Account for vertical lines 330 | if math.IsInf(math.Abs(lm), 1) && math.IsInf(math.Abs(km), 1) { 331 | // Both vertical, therefore parallel 332 | return ZV, false 333 | } 334 | 335 | var x, y float64 336 | 337 | if math.IsInf(math.Abs(lm), 1) || math.IsInf(math.Abs(km), 1) { 338 | // One line is vertical 339 | intersectM := lm 340 | intersectB := lb 341 | verticalLine := k 342 | 343 | if math.IsInf(math.Abs(lm), 1) { 344 | intersectM = km 345 | intersectB = kb 346 | verticalLine = l 347 | } 348 | 349 | y = intersectM*verticalLine.A.X + intersectB 350 | x = verticalLine.A.X 351 | } else { 352 | // Coordinates of intersect 353 | x = (kb - lb) / (lm - km) 354 | y = lm*x + lb 355 | } 356 | 357 | if l.Contains(V(x, y)) && k.Contains(V(x, y)) { 358 | // The intersect point is on both line segments, they intersect. 359 | return V(x, y), true 360 | } 361 | 362 | return ZV, false 363 | } 364 | 365 | // IntersectCircle will return the shortest Vec such that moving the Line by that Vec will cause the Line and Circle 366 | // to no longer intesect. If they do not intersect at all, this function will return a zero-vector. 367 | func (l Line) IntersectCircle(c Circle) Vec { 368 | // Get the point on the line closest to the center of the circle. 369 | closest := l.Closest(c.Center) 370 | cirToClosest := c.Center.To(closest) 371 | 372 | if cirToClosest.Len() >= c.Radius { 373 | return ZV 374 | } 375 | 376 | return cirToClosest.Scaled(cirToClosest.Len() - c.Radius) 377 | } 378 | 379 | // IntersectRect will return the shortest Vec such that moving the Line by that Vec will cause the Line and Rect to 380 | // no longer intesect. If they do not intersect at all, this function will return a zero-vector. 381 | func (l Line) IntersectRect(r Rect) Vec { 382 | // Check if either end of the line segment are within the rectangle 383 | if r.Contains(l.A) || r.Contains(l.B) { 384 | // Use the Rect.Intersect to get minimal return value 385 | rIntersect := l.Bounds().Intersect(r) 386 | if rIntersect.H() > rIntersect.W() { 387 | // Go vertical 388 | return V(0, rIntersect.H()) 389 | } 390 | return V(rIntersect.W(), 0) 391 | } 392 | 393 | // Check if any of the rectangles' edges intersect with this line. 394 | for _, edge := range r.Edges() { 395 | if _, ok := l.Intersect(edge); ok { 396 | // Get the closest points on the line to each corner, where: 397 | // - the point is contained by the rectangle 398 | // - the point is not the corner itself 399 | corners := r.Vertices() 400 | var closest *Vec 401 | closestCorner := corners[0] 402 | for _, c := range corners { 403 | cc := l.Closest(c) 404 | if closest == nil || (closest.Len() > cc.Len() && r.Contains(cc)) { 405 | closest = &cc 406 | closestCorner = c 407 | } 408 | } 409 | 410 | return closest.To(closestCorner) 411 | } 412 | } 413 | 414 | // No intersect 415 | return ZV 416 | } 417 | 418 | // Len returns the length of the line segment. 419 | func (l Line) Len() float64 { 420 | return l.A.To(l.B).Len() 421 | } 422 | 423 | // Moved will return a line moved by the delta Vec provided. 424 | func (l Line) Moved(delta Vec) Line { 425 | return Line{ 426 | A: l.A.Add(delta), 427 | B: l.B.Add(delta), 428 | } 429 | } 430 | 431 | // Rotated will rotate the line around the provided Vec. 432 | func (l Line) Rotated(around Vec, angle float64) Line { 433 | // Move the line so we can use `Vec.Rotated` 434 | lineShifted := l.Moved(around.Scaled(-1)) 435 | 436 | lineRotated := Line{ 437 | A: lineShifted.A.Rotated(angle), 438 | B: lineShifted.B.Rotated(angle), 439 | } 440 | 441 | return lineRotated.Moved(around) 442 | } 443 | 444 | // Scaled will return the line scaled around the center point. 445 | func (l Line) Scaled(scale float64) Line { 446 | return l.ScaledXY(l.Center(), scale) 447 | } 448 | 449 | // ScaledXY will return the line scaled around the Vec provided. 450 | func (l Line) ScaledXY(around Vec, scale float64) Line { 451 | toA := around.To(l.A).Scaled(scale) 452 | toB := around.To(l.B).Scaled(scale) 453 | 454 | return Line{ 455 | A: around.Add(toA), 456 | B: around.Add(toB), 457 | } 458 | } 459 | 460 | func (l Line) String() string { 461 | return fmt.Sprintf("Line(%v, %v)", l.A, l.B) 462 | } 463 | -------------------------------------------------------------------------------- /vector_test.go: -------------------------------------------------------------------------------- 1 | package pixel_test 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/faiface/pixel" 8 | ) 9 | 10 | type floorTest struct { 11 | input pixel.Vec 12 | expected pixel.Vec 13 | } 14 | 15 | func TestFloor(t *testing.T) { 16 | tests := []floorTest{ 17 | {input: pixel.V(4.50, 6.70), expected: pixel.V(4, 6)}, 18 | {input: pixel.V(9.0, 6.70), expected: pixel.V(9, 6)}, 19 | } 20 | 21 | for _, tc := range tests { 22 | result := tc.input.Floor() 23 | if result != tc.expected { 24 | t.Error(fmt.Sprintf("Expected %v but got %v", tc.expected, result)) 25 | } 26 | } 27 | } 28 | --------------------------------------------------------------------------------