Compare commits

..

113 Commits

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

View File

@ -1,34 +0,0 @@
language: go
# https://github.com/golang/go/issues/31293
dist: xenial
sudo: false
addons:
apt:
packages:
- xorg-dev
- libx11-dev
- libxrandr-dev
- libxinerama-dev
- libxcursor-dev
- libxi-dev
- libopenal-dev
- libasound2-dev
- libgl1-mesa-dev
services:
- xvfb
env:
- GO111MODULE=on
go:
- tip
- 1.12.x
install:
- # Do nothing. This is needed to prevent the default install action
# "go get -t -v ./..." from happening here because we want it to happen
# inside script step.
script:
- go test -v -race -mod=readonly ./...

View File

@ -1,63 +0,0 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
- Add AnchorPos struct and functions #252
- Add Clipboard Support
- Fix SIGSEGV on text.NewAtlas if glyph absent
- Use slice for range in Drawer.Dirty(), to improve performance
- GLTriangle's fragment shader is used when rendered by the Canvas.
- Add MSAA support
## [v0.10.0] 2020-08-22
- Add AnchorPos struct and functions
- Gamepad API added
- Support setting an initial window position
- Support hiding the window initially
- Support creating maximized windows
- Support waiting for events to reduce CPU load
- Adding clipping rectangle support in GLTriangles
## [v0.10.0-beta] 2020-05-10
- Add `WindowConfig.TransparentFramebuffer` option to support window transparency onto the background
- Fixed Line intersects failing on lines passing through (0, 0)
## [v0.10.0-alpha] 2020-05-08
- Upgrade to GLFW 3.3! :tada:
- Closes https://github.com/faiface/pixel/issues/137
- Add support for glfw's DisableCursor
- Closes https://github.com/faiface/pixel/issues/213
## [v0.9.0] - 2020-05-02
- Added feature from https://github.com/faiface/pixel/pull/219
- Exposing Window.SwapBuffers so buffers can be swapped without polling input
- Add more examples
- Add position as out variable from vertex shader
- Add experimental joystick support
- Add mouse cursor operations
- Add `Vec.Floor(…)` function
- Add circle geometry
- Fix `Matrix.Unproject(…)` for rotated matrix
- Add 2D Line geometry
- Add floating point round error correction
- Performance improvements
- Fix race condition in `NewGLTriangles(…)`
- Add `TriangleData` benchmarks and improvements
- Add zero rectangle variable for utility and consistency
- Add support for Go Modules
- Add `NoIconify` and `AlwaysOnTop` window hints
## [v0.8.0] - 2018-10-10
Changelog for this and older versions can be found on the corresponding [GitHub
releases](https://github.com/faiface/pixel/releases).
[Unreleased]: https://github.com/faiface/pixel/compare/v0.10.0...HEAD
[v0.10.0]: https://github.com/faiface/pixel/compare/v0.10.0-beta...v0.10.0
[v0.10.0-beta]: https://github.com/faiface/pixel/compare/v0.10.0-alpha...v0.10.0-beta
[v0.10.0-alpha]: https://github.com/faiface/pixel/compare/v0.9.0...v0.10.0-alpha
[v0.9.0]: https://github.com/faiface/pixel/compare/v0.8.0...v0.9.0
[v0.8.0]: https://github.com/faiface/pixel/releases/tag/v0.8.0

View File

@ -1,16 +0,0 @@
# Contributing to Pixel
:tada: Hi! I'm really glad you're considering contributing to Pixel! :tada:
## Here are a few ways you can contribute
1. **Make a community example** and place it inside the `community` folder of the [examples repository][examples].
2. **Add tests**. There only few tests in Pixel at the moment. Take a look at them and make some similar.
3. **Add a small feature or an improvement**. Feel like some small feature is missing? Just make a PR. Be ready that I might reject it, though, if I don't find it particularly appealing.
4. **Join the big development** by joining the discussion on our [Discord Server](https://discord.gg/q2DK4MP), where we can discuss bigger changes and implement them after that.
## How to make a pull request
Go gives you a nice surprise when attempting to make a PR on Github. The thing is, that when user _xyz_ forks Pixel on Github, it ends up in _github.com/xyz/pixel_, which fucks up your import paths. Here's how you deal with that: https://www.reddit.com/r/golang/comments/2jdcw1/how_do_you_deal_with_github_forking/.
[examples]: https://github.com/faiface/pixel-examples/tree/master/community

View File

@ -1,12 +1,4 @@
# \*\*\*\*\*NOTICE\*\*\*\*\* # 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)
This repo is not under active development anymore and has been archived. Continued development has been migrated to [Pixel2](https://github.com/gopxl/pixel). A big thank you to [faiface](https://github.com/faiface) for creating this awesome library and for all the hard work put into it. We encourage old and new users to check out the new repo and contribute to it.
<p align="center"><img src="logo/LOGOTYPE-HORIZONTAL-BLUE.png"></p>
# Pixel [![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)
A hand-crafted 2D game library in Go. Take a look into the [features](#features) to see what it can A hand-crafted 2D game library in Go. Take a look into the [features](#features) to see what it can
do. do.
@ -15,18 +7,8 @@ do.
go get github.com/faiface/pixel go get github.com/faiface/pixel
``` ```
If you are using Modules (Go 1.11 or higher) and want a mutable copy of the source code:
```
git clone https://github.com/faiface/pixel # clone outside of $GOPATH
cd pixel
go install ./...
```
See [requirements](#requirements) for the list of libraries necessary for compilation. See [requirements](#requirements) for the list of libraries necessary for compilation.
All significant changes are documented in [CHANGELOG.md](CHANGELOG.md).
## Tutorial ## Tutorial
The [Wiki of this repo](https://github.com/faiface/pixel/wiki) contains an extensive tutorial The [Wiki of this repo](https://github.com/faiface/pixel/wiki) contains an extensive tutorial
@ -38,34 +20,28 @@ covering several topics of Pixel. Here's the content of the tutorial parts so fa
- [Pressing keys and clicking mouse](https://github.com/faiface/pixel/wiki/Pressing-keys-and-clicking-mouse) - [Pressing keys and clicking mouse](https://github.com/faiface/pixel/wiki/Pressing-keys-and-clicking-mouse)
- [Drawing efficiently with Batch](https://github.com/faiface/pixel/wiki/Drawing-efficiently-with-Batch) - [Drawing efficiently with Batch](https://github.com/faiface/pixel/wiki/Drawing-efficiently-with-Batch)
- [Drawing shapes with IMDraw](https://github.com/faiface/pixel/wiki/Drawing-shapes-with-IMDraw) - [Drawing shapes with IMDraw](https://github.com/faiface/pixel/wiki/Drawing-shapes-with-IMDraw)
- [Typing text on the screen](https://github.com/faiface/pixel/wiki/Typing-text-on-the-screen)
- [Using a custom fragment shader](https://github.com/faiface/pixel/wiki/Using-a-custom-fragment-shader)
## [Examples](https://github.com/faiface/pixel-examples) ## Examples
The [examples](https://github.com/faiface/pixel-examples) repository contains a few The [examples](https://github.com/faiface/pixel/tree/master/examples) directory contains a few
examples demonstrating Pixel's functionality. examples demonstrating Pixel's functionality.
**To run an example**, navigate to it's directory, then `go run` the `main.go` file. For example: **To run an example**, navigate to it's directory, then `go run` the `main.go` file. For example:
``` ```
$ cd pixel-examples/platformer $ cd examples/platformer
$ go run main.go $ go run main.go
``` ```
Here are some screenshots from the examples! Here are some screenshots from the examples!
| [Lights](https://github.com/faiface/pixel-examples/blob/master/lights) | [Platformer](https://github.com/faiface/pixel-examples/blob/master/platformer) | | [Lights](examples/lights) | [Platformer](examples/platformer) |
| --- | --- | | --- | --- |
| ![Lights](https://github.com/faiface/pixel-examples/blob/master/lights/screenshot.png) | ![Platformer](https://github.com/faiface/pixel-examples/blob/master/platformer/screenshot.png) | | ![Lights](examples/lights/screenshot.png) | ![Platformer](examples/platformer/screenshot.png) |
| [Smoke](https://github.com/faiface/pixel-examples/blob/master/smoke) | [Typewriter](https://github.com/faiface/pixel-examples/blob/master/typewriter) | | [Smoke](examples/smoke) | [Xor](examples/xor) |
| --- | --- | | --- | --- |
| ![Smoke](https://github.com/faiface/pixel-examples/blob/master/smoke/screenshot.png) | ![Typewriter](https://github.com/faiface/pixel-examples/blob/master/typewriter/screenshot.png) | | ![Smoke](examples/smoke/screenshot.png) | ![Xor](examples/xor/screenshot.png) |
| [Raycaster](https://github.com/faiface/pixel-examples/blob/master/community/raycaster) | [Gizmo](https://github.com/Lallassu/gizmo) |
| --- | --- |
| ![Raycaster](https://github.com/faiface/pixel-examples/blob/master/community/raycaster/screenshot.png) | ![Gizmo](https://github.com/Lallassu/gizmo/blob/master/preview.png) |
## Features ## Features
@ -78,16 +54,15 @@ Here's the list of the main features in Pixel. Although Pixel is still under hea
[IMDraw](https://github.com/faiface/pixel/wiki/Drawing-shapes-with-IMDraw) (circles, rectangles, [IMDraw](https://github.com/faiface/pixel/wiki/Drawing-shapes-with-IMDraw) (circles, rectangles,
lines, ...) lines, ...)
- Optimized drawing with [Batch](https://github.com/faiface/pixel/wiki/Drawing-efficiently-with-Batch) - Optimized drawing with [Batch](https://github.com/faiface/pixel/wiki/Drawing-efficiently-with-Batch)
- Text drawing with [text](https://godoc.org/github.com/faiface/pixel/text) package
- Audio through a separate [Beep](https://github.com/faiface/beep) library.
- Simple and convenient API - Simple and convenient API
- Drawing a sprite to a window is as simple as `sprite.Draw(window, matrix)` - Drawing a sprite to a window is as simple as `sprite.Draw(window)`
- Adding and subtracting vectors with `+` and `-` operators... how?
- Wanna know where the center of a window is? `window.Bounds().Center()` - Wanna know where the center of a window is? `window.Bounds().Center()`
- [...](https://godoc.org/github.com/faiface/pixel) - [...](https://godoc.org/github.com/faiface/pixel)
- Full documentation and tutorial - Full documentation and tutorial
- Works on Linux, macOS and Windows - Works on Linux, macOS and Windows
- Window creation and manipulation (resizing, fullscreen, multiple windows, ...) - Window creation and manipulation (resizing, fullscreen, multiple windows, ...)
- Keyboard (key presses, text input) and mouse input without events - Keyboard and mouse input without events
- Well integrated with the Go standard library - Well integrated with the Go standard library
- Use `"image"` package for loading pictures - Use `"image"` package for loading pictures
- Use `"time"` package for measuring delta time and FPS - Use `"time"` package for measuring delta time and FPS
@ -111,58 +86,37 @@ Here's the list of the main features in Pixel. Although Pixel is still under hea
- Small codebase, ~5K lines of code, including the backend [glhf](https://github.com/faiface/glhf) - Small codebase, ~5K lines of code, including the backend [glhf](https://github.com/faiface/glhf)
package package
## Related repositories
Here are some packages which use Pixel:
- [TilePix](https://github.com/bcvery1/tilepix) Makes handling TMX files built with [Tiled](https://www.mapeditor.org/) trivially easy to work with using Pixel.
- [spriteplus](https://github.com/cebarks/spriteplus) Basic `SpriteSheet` and `Animation` implementations
- [PixelUI](https://github.com/dusk125/pixelui) Imgui-based GUIs for Pixel
- [pixelutils](https://github.com/dusk125/pixelutils) Variety of game related utilities (sprite packer, id generator, ticker, sprite loader, voronoia diagrams)
## Missing features ## Missing features
Pixel is in development and still missing few critical features. Here're the most critical ones. Pixel is in development and still missing few critical features. Here're the most critical ones.
- ~~Audio~~ - Audio
- ~~Drawing text~~ - Drawing text
- Antialiasing (filtering is supported, though) - Antialiasing (filtering is supported, though)
- ~~Advanced window manipulation (cursor hiding, window icon, ...)~~ - Advanced window manipulation (cursor hiding, window icon, ...)
- Better support for Hi-DPI displays - Better support for Hi-DPI displays
- Mobile (and perhaps HTML5?) backend - Mobile (and perhaps HTML5?) backend
- ~~More advanced graphical effects (e.g. blur)~~ (solved with the addition of GLSL effects) - More advanced graphical effects (e.g. blur)
- Tests and benchmarks - Tests and benchmarks
- Vulkan support
**Implementing these features will get us to the 1.0 release.** Contribute, so that it's as soon as **Implementing these features will get us to the 1.0 release.** Contribute, so that it's as soon as
possible! possible!
## Requirements ## Requirements
If you're using Windows and having trouble building Pixel, please check [this
guide](https://github.com/faiface/pixel/wiki/Building-Pixel-on-Windows) on the
[wiki](https://github.com/faiface/pixel/wiki).
[PixelGL](https://godoc.org/github.com/faiface/pixel/pixelgl) backend uses OpenGL to render [PixelGL](https://godoc.org/github.com/faiface/pixel/pixelgl) backend uses OpenGL to render
graphics. Because of that, OpenGL development libraries are needed for compilation. The dependencies graphics. Because of that, OpenGL development libraries are needed for compilation. The dependencies
are same as for [GLFW](https://github.com/go-gl/glfw). are same as for [GLFW](https://github.com/go-gl/glfw).
The OpenGL version used is **OpenGL 3.3**.
- On macOS, you need Xcode or Command Line Tools for Xcode (`xcode-select --install`) for required - On macOS, you need Xcode or Command Line Tools for Xcode (`xcode-select --install`) for required
headers and libraries. headers and libraries.
- On Ubuntu/Debian-like Linux distributions, you need `libgl1-mesa-dev` and `xorg-dev` packages. - On Ubuntu/Debian-like Linux distributions, you need `libgl1-mesa-dev` and `xorg-dev` packages.
- On CentOS/Fedora-like Linux distributions, you need `libX11-devel libXcursor-devel libXrandr-devel - On CentOS/Fedora-like Linux distributions, you need `libX11-devel libXcursor-devel libXrandr-devel
libXinerama-devel mesa-libGL-devel libXi-devel libXxf86vm-devel` packages. libXinerama-devel mesa-libGL-devel libXi-devel` packages.
- See [here](http://www.glfw.org/docs/latest/compile.html#compile_deps) for full details. - See [here](http://www.glfw.org/docs/latest/compile.html#compile_deps) for full details.
**The combination of Go 1.8, macOS and latest XCode seems to be problematic** as mentioned in issue
[#7](https://github.com/faiface/pixel/issues/7). This issue is probably not related to Pixel.
**Upgrading to Go 1.8.1 fixes the issue.**
## Contributing ## Contributing
Join us in the [Discord Chat!](https://discord.gg/q2DK4MP)
Pixel is in, let's say, mid-stage of development. Many of the important features are here, some are Pixel is in, let's say, mid-stage of development. Many of the important features are here, some are
missing. That's why **contributions are very important and welcome!** All alone, I will be able to missing. That's why **contributions are very important and welcome!** All alone, I will be able to
finish the library, but it'll take a lot of time. With your help, it'll take much less. I encourage finish the library, but it'll take a lot of time. With your help, it'll take much less. I encourage
@ -178,7 +132,12 @@ requests. It just means that I might not like your idea. Or that your pull reque
rewriting. That's perfectly fine, don't let it put you off. In the end, we'll just end up with a rewriting. That's perfectly fine, don't let it put you off. In the end, we'll just end up with a
better result. better result.
Take a look at [CONTRIBUTING.md](CONTRIBUTING.md) for further information. **Don't start working on a pull request before submiting an issue or commenting on one. Proposals
also take the form of issues.**
For any kind of discussion, feel free to use our
[Gitter](https://gitter.im/pixellib/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
community.
## License ## License

View File

@ -26,7 +26,7 @@ var _ BasicTarget = (*Batch)(nil)
// //
// Note, that if the container does not support TrianglesColor, color masking will not work. // Note, that if the container does not support TrianglesColor, color masking will not work.
func NewBatch(container Triangles, pic Picture) *Batch { func NewBatch(container Triangles, pic Picture) *Batch {
b := &Batch{cont: Drawer{Triangles: container, Picture: pic, Cached: true}} b := &Batch{cont: Drawer{Triangles: container, Picture: pic}}
b.SetMatrix(IM) b.SetMatrix(IM)
b.SetColorMask(Alpha(1)) b.SetColorMask(Alpha(1))
return b return b

334
circle.go
View File

@ -1,334 +0,0 @@
package pixel
import (
"fmt"
"math"
)
// Circle is a 2D circle. It is defined by two properties:
// - Center vector
// - Radius float64
type Circle struct {
Center Vec
Radius float64
}
// C returns a new Circle with the given radius and center coordinates.
//
// Note that a negative radius is valid.
func C(center Vec, radius float64) Circle {
return Circle{
Center: center,
Radius: radius,
}
}
// String returns the string representation of the Circle.
//
// c := pixel.C(10.1234, pixel.ZV)
// c.String() // returns "Circle(10.12, Vec(0, 0))"
// fmt.Println(c) // Circle(10.12, Vec(0, 0))
func (c Circle) String() string {
return fmt.Sprintf("Circle(%s, %.2f)", c.Center, c.Radius)
}
// Norm returns the Circle in normalized form - this sets the radius to its absolute value.
//
// c := pixel.C(-10, pixel.ZV)
// c.Norm() // returns pixel.Circle{pixel.Vec{0, 0}, 10}
func (c Circle) Norm() Circle {
return Circle{
Center: c.Center,
Radius: math.Abs(c.Radius),
}
}
// Area returns the area of the Circle.
func (c Circle) Area() float64 {
return math.Pi * math.Pow(c.Radius, 2)
}
// Moved returns the Circle moved by the given vector delta.
func (c Circle) Moved(delta Vec) Circle {
return Circle{
Center: c.Center.Add(delta),
Radius: c.Radius,
}
}
// Resized returns the Circle resized by the given delta. The Circles center is use as the anchor.
//
// c := pixel.C(pixel.ZV, 10)
// c.Resized(-5) // returns pixel.Circle{pixel.Vec{0, 0}, 5}
// c.Resized(25) // returns pixel.Circle{pixel.Vec{0, 0}, 35}
func (c Circle) Resized(radiusDelta float64) Circle {
return Circle{
Center: c.Center,
Radius: c.Radius + radiusDelta,
}
}
// Contains checks whether a vector `u` is contained within this Circle (including it's perimeter).
func (c Circle) Contains(u Vec) bool {
toCenter := c.Center.To(u)
return c.Radius >= toCenter.Len()
}
// Formula returns the values of h and k, for the equation of the circle: (x-h)^2 + (y-k)^2 = r^2
// where r is the radius of the circle.
func (c Circle) Formula() (h, k float64) {
return c.Center.X, c.Center.Y
}
// maxCircle will return the larger circle based on the radius.
func maxCircle(c, d Circle) Circle {
if c.Radius < d.Radius {
return d
}
return c
}
// minCircle will return the smaller circle based on the radius.
func minCircle(c, d Circle) Circle {
if c.Radius < d.Radius {
return c
}
return d
}
// Union returns the minimal Circle which covers both `c` and `d`.
func (c Circle) Union(d Circle) Circle {
biggerC := maxCircle(c.Norm(), d.Norm())
smallerC := minCircle(c.Norm(), d.Norm())
// Get distance between centers
dist := c.Center.To(d.Center).Len()
// If the bigger Circle encompasses the smaller one, we have the result
if dist+smallerC.Radius <= biggerC.Radius {
return biggerC
}
// Calculate radius for encompassing Circle
r := (dist + biggerC.Radius + smallerC.Radius) / 2
// Calculate center for encompassing Circle
theta := .5 + (biggerC.Radius-smallerC.Radius)/(2*dist)
center := Lerp(smallerC.Center, biggerC.Center, theta)
return Circle{
Center: center,
Radius: r,
}
}
// Intersect returns the maximal Circle which is covered by both `c` and `d`.
//
// If `c` and `d` don't overlap, this function returns a zero-sized circle at the centerpoint between the two Circle's
// centers.
func (c Circle) Intersect(d Circle) Circle {
// Check if one of the circles encompasses the other; if so, return that one
biggerC := maxCircle(c.Norm(), d.Norm())
smallerC := minCircle(c.Norm(), d.Norm())
if biggerC.Radius >= biggerC.Center.To(smallerC.Center).Len()+smallerC.Radius {
return biggerC
}
// Calculate the midpoint between the two radii
// Distance between centers
dist := c.Center.To(d.Center).Len()
// Difference between radii
diff := dist - (c.Radius + d.Radius)
// Distance from c.Center to the weighted midpoint
distToMidpoint := c.Radius + 0.5*diff
// Weighted midpoint
center := Lerp(c.Center, d.Center, distToMidpoint/dist)
// No need to calculate radius if the circles do not overlap
if c.Center.To(d.Center).Len() >= c.Radius+d.Radius {
return C(center, 0)
}
radius := c.Center.To(d.Center).Len() - (c.Radius + d.Radius)
return Circle{
Center: center,
Radius: math.Abs(radius),
}
}
// IntersectLine will return the shortest Vec such that if the Circle is moved by the Vec returned, the Line and Rect no
// longer intersect.
func (c Circle) IntersectLine(l Line) Vec {
return l.IntersectCircle(c).Scaled(-1)
}
// IntersectRect returns a minimal required Vector, such that moving the circle by that vector would stop the Circle
// and the Rect intersecting. This function returns a zero-vector if the Circle and Rect do not overlap, and if only
// the perimeters touch.
//
// This function will return a non-zero vector if:
// - The Rect contains the Circle, partially or fully
// - The Circle contains the Rect, partially of fully
func (c Circle) IntersectRect(r Rect) Vec {
// Checks if the c.Center is not in the diagonal quadrants of the rectangle
if (r.Min.X <= c.Center.X && c.Center.X <= r.Max.X) || (r.Min.Y <= c.Center.Y && c.Center.Y <= r.Max.Y) {
// 'grow' the Rect by c.Radius in each orthagonal
grown := Rect{Min: r.Min.Sub(V(c.Radius, c.Radius)), Max: r.Max.Add(V(c.Radius, c.Radius))}
if !grown.Contains(c.Center) {
// c.Center not close enough to overlap, return zero-vector
return ZV
}
// Get minimum distance to travel out of Rect
rToC := r.Center().To(c.Center)
h := c.Radius - math.Abs(rToC.X) + (r.W() / 2)
v := c.Radius - math.Abs(rToC.Y) + (r.H() / 2)
if rToC.X < 0 {
h = -h
}
if rToC.Y < 0 {
v = -v
}
// No intersect
if h == 0 && v == 0 {
return ZV
}
if math.Abs(h) > math.Abs(v) {
// Vertical distance shorter
return V(0, v)
}
return V(h, 0)
} else {
// The center is in the diagonal quadrants
// Helper points to make code below easy to read.
rectTopLeft := V(r.Min.X, r.Max.Y)
rectBottomRight := V(r.Max.X, r.Min.Y)
// Check for overlap.
if !(c.Contains(r.Min) || c.Contains(r.Max) || c.Contains(rectTopLeft) || c.Contains(rectBottomRight)) {
// No overlap.
return ZV
}
var centerToCorner Vec
if c.Center.To(r.Min).Len() <= c.Radius {
// Closest to bottom-left
centerToCorner = c.Center.To(r.Min)
}
if c.Center.To(r.Max).Len() <= c.Radius {
// Closest to top-right
centerToCorner = c.Center.To(r.Max)
}
if c.Center.To(rectTopLeft).Len() <= c.Radius {
// Closest to top-left
centerToCorner = c.Center.To(rectTopLeft)
}
if c.Center.To(rectBottomRight).Len() <= c.Radius {
// Closest to bottom-right
centerToCorner = c.Center.To(rectBottomRight)
}
cornerToCircumferenceLen := c.Radius - centerToCorner.Len()
return centerToCorner.Unit().Scaled(cornerToCircumferenceLen)
}
}
// IntersectionPoints returns all the points where the Circle intersects with the line provided. This can be zero, one or
// two points, depending on the location of the shapes. The points of intersection will be returned in order of
// closest-to-l.A to closest-to-l.B.
func (c Circle) IntersectionPoints(l Line) []Vec {
cContainsA := c.Contains(l.A)
cContainsB := c.Contains(l.B)
// Special case for both endpoint being contained within the circle
if cContainsA && cContainsB {
return []Vec{}
}
// Get closest point on the line to this circles' center
closestToCenter := l.Closest(c.Center)
// If the distance to the closest point is greater than the radius, there are no points of intersection
if closestToCenter.To(c.Center).Len() > c.Radius {
return []Vec{}
}
// If the distance to the closest point is equal to the radius, the line is tangent and the closest point is the
// point at which it touches the circle.
if closestToCenter.To(c.Center).Len() == c.Radius {
return []Vec{closestToCenter}
}
// Special case for endpoint being on the circles' center
if c.Center == l.A || c.Center == l.B {
otherEnd := l.B
if c.Center == l.B {
otherEnd = l.A
}
intersect := c.Center.Add(c.Center.To(otherEnd).Unit().Scaled(c.Radius))
return []Vec{intersect}
}
// This means the distance to the closest point is less than the radius, so there is at least one intersection,
// possibly two.
// If one of the end points exists within the circle, there is only one intersection
if cContainsA || cContainsB {
containedPoint := l.A
otherEnd := l.B
if cContainsB {
containedPoint = l.B
otherEnd = l.A
}
// Use trigonometry to get the length of the line between the contained point and the intersection point.
// The following is used to describe the triangle formed:
// - a is the side between contained point and circle center
// - b is the side between the center and the intersection point (radius)
// - c is the side between the contained point and the intersection point
// The captials of these letters are used as the angles opposite the respective sides.
// a and b are known
a := containedPoint.To(c.Center).Len()
b := c.Radius
// B can be calculated by subtracting the angle of b (to the x-axis) from the angle of c (to the x-axis)
B := containedPoint.To(c.Center).Angle() - containedPoint.To(otherEnd).Angle()
// Using the Sin rule we can get A
A := math.Asin((a * math.Sin(B)) / b)
// Using the rule that there are 180 degrees (or Pi radians) in a triangle, we can now get C
C := math.Pi - A + B
// If C is zero, the line segment is in-line with the center-intersect line.
var c float64
if C == 0 {
c = b - a
} else {
// Using the Sine rule again, we can now get c
c = (a * math.Sin(C)) / math.Sin(A)
}
// Travelling from the contained point to the other end by length of a will provide the intersection point.
return []Vec{
containedPoint.Add(containedPoint.To(otherEnd).Unit().Scaled(c)),
}
}
// Otherwise the endpoints exist outside of the circle, and the line segment intersects in two locations.
// The vector formed by going from the closest point to the center of the circle will be perpendicular to the line;
// this forms a right-angled triangle with the intersection points, with the radius as the hypotenuse.
// Calculate the other triangles' sides' length.
a := math.Sqrt(math.Pow(c.Radius, 2) - math.Pow(closestToCenter.To(c.Center).Len(), 2))
// Travelling in both directions from the closest point by length of a will provide the two intersection points.
first := closestToCenter.Add(closestToCenter.To(l.A).Unit().Scaled(a))
second := closestToCenter.Add(closestToCenter.To(l.B).Unit().Scaled(a))
if first.To(l.A).Len() < second.To(l.A).Len() {
return []Vec{first, second}
}
return []Vec{second, first}
}

View File

@ -1,466 +0,0 @@
package pixel_test
import (
"math"
"reflect"
"testing"
"github.com/faiface/pixel"
)
func TestC(t *testing.T) {
type args struct {
radius float64
center pixel.Vec
}
tests := []struct {
name string
args args
want pixel.Circle
}{
{
name: "C(): positive radius",
args: args{radius: 10, center: pixel.ZV},
want: pixel.Circle{Radius: 10, Center: pixel.ZV},
},
{
name: "C(): zero radius",
args: args{radius: 0, center: pixel.ZV},
want: pixel.Circle{Radius: 0, Center: pixel.ZV},
},
{
name: "C(): negative radius",
args: args{radius: -5, center: pixel.ZV},
want: pixel.Circle{Radius: -5, Center: pixel.ZV},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := pixel.C(tt.args.center, tt.args.radius); !reflect.DeepEqual(got, tt.want) {
t.Errorf("C() = %v, want %v", got, tt.want)
}
})
}
}
func TestCircle_String(t *testing.T) {
type fields struct {
radius float64
center pixel.Vec
}
tests := []struct {
name string
fields fields
want string
}{
{
name: "Circle.String(): positive radius",
fields: fields{radius: 10, center: pixel.ZV},
want: "Circle(Vec(0, 0), 10.00)",
},
{
name: "Circle.String(): zero radius",
fields: fields{radius: 0, center: pixel.ZV},
want: "Circle(Vec(0, 0), 0.00)",
},
{
name: "Circle.String(): negative radius",
fields: fields{radius: -5, center: pixel.ZV},
want: "Circle(Vec(0, 0), -5.00)",
},
{
name: "Circle.String(): irrational radius",
fields: fields{radius: math.Pi, center: pixel.ZV},
want: "Circle(Vec(0, 0), 3.14)",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := pixel.C(tt.fields.center, tt.fields.radius)
if got := c.String(); got != tt.want {
t.Errorf("Circle.String() = %v, want %v", got, tt.want)
}
})
}
}
func TestCircle_Norm(t *testing.T) {
type fields struct {
radius float64
center pixel.Vec
}
tests := []struct {
name string
fields fields
want pixel.Circle
}{
{
name: "Circle.Norm(): positive radius",
fields: fields{radius: 10, center: pixel.ZV},
want: pixel.C(pixel.ZV, 10),
},
{
name: "Circle.Norm(): zero radius",
fields: fields{radius: 0, center: pixel.ZV},
want: pixel.C(pixel.ZV, 0),
},
{
name: "Circle.Norm(): negative radius",
fields: fields{radius: -5, center: pixel.ZV},
want: pixel.C(pixel.ZV, 5),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := pixel.C(tt.fields.center, tt.fields.radius)
if got := c.Norm(); !reflect.DeepEqual(got, tt.want) {
t.Errorf("Circle.Norm() = %v, want %v", got, tt.want)
}
})
}
}
func TestCircle_Area(t *testing.T) {
type fields struct {
radius float64
center pixel.Vec
}
tests := []struct {
name string
fields fields
want float64
}{
{
name: "Circle.Area(): positive radius",
fields: fields{radius: 10, center: pixel.ZV},
want: 100 * math.Pi,
},
{
name: "Circle.Area(): zero radius",
fields: fields{radius: 0, center: pixel.ZV},
want: 0,
},
{
name: "Circle.Area(): negative radius",
fields: fields{radius: -5, center: pixel.ZV},
want: 25 * math.Pi,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := pixel.C(tt.fields.center, tt.fields.radius)
if got := c.Area(); got != tt.want {
t.Errorf("Circle.Area() = %v, want %v", got, tt.want)
}
})
}
}
func TestCircle_Moved(t *testing.T) {
type fields struct {
radius float64
center pixel.Vec
}
type args struct {
delta pixel.Vec
}
tests := []struct {
name string
fields fields
args args
want pixel.Circle
}{
{
name: "Circle.Moved(): positive movement",
fields: fields{radius: 10, center: pixel.ZV},
args: args{delta: pixel.V(10, 20)},
want: pixel.C(pixel.V(10, 20), 10),
},
{
name: "Circle.Moved(): zero movement",
fields: fields{radius: 10, center: pixel.ZV},
args: args{delta: pixel.ZV},
want: pixel.C(pixel.V(0, 0), 10),
},
{
name: "Circle.Moved(): negative movement",
fields: fields{radius: 10, center: pixel.ZV},
args: args{delta: pixel.V(-5, -10)},
want: pixel.C(pixel.V(-5, -10), 10),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := pixel.C(tt.fields.center, tt.fields.radius)
if got := c.Moved(tt.args.delta); !reflect.DeepEqual(got, tt.want) {
t.Errorf("Circle.Moved() = %v, want %v", got, tt.want)
}
})
}
}
func TestCircle_Resized(t *testing.T) {
type fields struct {
radius float64
center pixel.Vec
}
type args struct {
radiusDelta float64
}
tests := []struct {
name string
fields fields
args args
want pixel.Circle
}{
{
name: "Circle.Resized(): positive delta",
fields: fields{radius: 10, center: pixel.ZV},
args: args{radiusDelta: 5},
want: pixel.C(pixel.V(0, 0), 15),
},
{
name: "Circle.Resized(): zero delta",
fields: fields{radius: 10, center: pixel.ZV},
args: args{radiusDelta: 0},
want: pixel.C(pixel.V(0, 0), 10),
},
{
name: "Circle.Resized(): negative delta",
fields: fields{radius: 10, center: pixel.ZV},
args: args{radiusDelta: -5},
want: pixel.C(pixel.V(0, 0), 5),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := pixel.C(tt.fields.center, tt.fields.radius)
if got := c.Resized(tt.args.radiusDelta); !reflect.DeepEqual(got, tt.want) {
t.Errorf("Circle.Resized() = %v, want %v", got, tt.want)
}
})
}
}
func TestCircle_Contains(t *testing.T) {
type fields struct {
radius float64
center pixel.Vec
}
type args struct {
u pixel.Vec
}
tests := []struct {
name string
fields fields
args args
want bool
}{
{
name: "Circle.Contains(): point on cicles' center",
fields: fields{radius: 10, center: pixel.ZV},
args: args{u: pixel.ZV},
want: true,
},
{
name: "Circle.Contains(): point offcenter",
fields: fields{radius: 10, center: pixel.V(5, 0)},
args: args{u: pixel.ZV},
want: true,
},
{
name: "Circle.Contains(): point on circumference",
fields: fields{radius: 10, center: pixel.V(10, 0)},
args: args{u: pixel.ZV},
want: true,
},
{
name: "Circle.Contains(): point outside circle",
fields: fields{radius: 10, center: pixel.V(15, 0)},
args: args{u: pixel.ZV},
want: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := pixel.C(tt.fields.center, tt.fields.radius)
if got := c.Contains(tt.args.u); got != tt.want {
t.Errorf("Circle.Contains() = %v, want %v", got, tt.want)
}
})
}
}
func TestCircle_Union(t *testing.T) {
type fields struct {
radius float64
center pixel.Vec
}
type args struct {
d pixel.Circle
}
tests := []struct {
name string
fields fields
args args
want pixel.Circle
}{
{
name: "Circle.Union(): overlapping circles",
fields: fields{radius: 5, center: pixel.ZV},
args: args{d: pixel.C(pixel.ZV, 5)},
want: pixel.C(pixel.ZV, 5),
},
{
name: "Circle.Union(): separate circles",
fields: fields{radius: 1, center: pixel.ZV},
args: args{d: pixel.C(pixel.V(0, 2), 1)},
want: pixel.C(pixel.V(0, 1), 2),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := pixel.C(tt.fields.center, tt.fields.radius)
if got := c.Union(tt.args.d); !reflect.DeepEqual(got, tt.want) {
t.Errorf("Circle.Union() = %v, want %v", got, tt.want)
}
})
}
}
func TestCircle_Intersect(t *testing.T) {
type fields struct {
radius float64
center pixel.Vec
}
type args struct {
d pixel.Circle
}
tests := []struct {
name string
fields fields
args args
want pixel.Circle
}{
{
name: "Circle.Intersect(): intersecting circles",
fields: fields{radius: 1, center: pixel.ZV},
args: args{d: pixel.C(pixel.V(1, 0), 1)},
want: pixel.C(pixel.V(0.5, 0), 1),
},
{
name: "Circle.Intersect(): non-intersecting circles",
fields: fields{radius: 1, center: pixel.ZV},
args: args{d: pixel.C(pixel.V(3, 3), 1)},
want: pixel.C(pixel.V(1.5, 1.5), 0),
},
{
name: "Circle.Intersect(): first circle encompassing second",
fields: fields{radius: 10, center: pixel.ZV},
args: args{d: pixel.C(pixel.V(3, 3), 1)},
want: pixel.C(pixel.ZV, 10),
},
{
name: "Circle.Intersect(): second circle encompassing first",
fields: fields{radius: 1, center: pixel.V(-1, -4)},
args: args{d: pixel.C(pixel.ZV, 10)},
want: pixel.C(pixel.ZV, 10),
},
{
name: "Circle.Intersect(): matching circles",
fields: fields{radius: 1, center: pixel.ZV},
args: args{d: pixel.C(pixel.ZV, 1)},
want: pixel.C(pixel.ZV, 1),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := pixel.C(
tt.fields.center,
tt.fields.radius,
)
if got := c.Intersect(tt.args.d); !reflect.DeepEqual(got, tt.want) {
t.Errorf("Circle.Intersect() = %v, want %v", got, tt.want)
}
})
}
}
func TestCircle_IntersectPoints(t *testing.T) {
type fields struct {
Center pixel.Vec
Radius float64
}
type args struct {
l pixel.Line
}
tests := []struct {
name string
fields fields
args args
want []pixel.Vec
}{
{
name: "Line intersects circle at two points",
fields: fields{Center: pixel.V(2, 2), Radius: 1},
args: args{pixel.L(pixel.V(0, 0), pixel.V(10, 10))},
want: []pixel.Vec{pixel.V(1.292, 1.292), pixel.V(2.707, 2.707)},
},
{
name: "Line intersects circle at one point",
fields: fields{Center: pixel.V(-0.5, -0.5), Radius: 1},
args: args{pixel.L(pixel.V(0, 0), pixel.V(10, 10))},
want: []pixel.Vec{pixel.V(0.207, 0.207)},
},
{
name: "Line endpoint is circle center",
fields: fields{Center: pixel.V(0, 0), Radius: 1},
args: args{pixel.L(pixel.V(0, 0), pixel.V(10, 10))},
want: []pixel.Vec{pixel.V(0.707, 0.707)},
},
{
name: "Both line endpoints within circle",
fields: fields{Center: pixel.V(0, 0), Radius: 1},
args: args{pixel.L(pixel.V(0.2, 0.2), pixel.V(0.5, 0.5))},
want: []pixel.Vec{},
},
{
name: "Line does not intersect circle",
fields: fields{Center: pixel.V(10, 0), Radius: 1},
args: args{pixel.L(pixel.V(0, 0), pixel.V(10, 10))},
want: []pixel.Vec{},
},
{
name: "Horizontal line intersects circle at two points",
fields: fields{Center: pixel.V(5, 5), Radius: 1},
args: args{pixel.L(pixel.V(0, 5), pixel.V(10, 5))},
want: []pixel.Vec{pixel.V(4, 5), pixel.V(6, 5)},
},
{
name: "Vertical line intersects circle at two points",
fields: fields{Center: pixel.V(5, 5), Radius: 1},
args: args{pixel.L(pixel.V(5, 0), pixel.V(5, 10))},
want: []pixel.Vec{pixel.V(5, 4), pixel.V(5, 6)},
},
{
name: "Left and down line intersects circle at two points",
fields: fields{Center: pixel.V(5, 5), Radius: 1},
args: args{pixel.L(pixel.V(10, 10), pixel.V(0, 0))},
want: []pixel.Vec{pixel.V(5.707, 5.707), pixel.V(4.292, 4.292)},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := pixel.Circle{
Center: tt.fields.Center,
Radius: tt.fields.Radius,
}
got := c.IntersectionPoints(tt.args.l)
for i, v := range got {
if !closeEnough(v.X, tt.want[i].X, 2) || !closeEnough(v.Y, tt.want[i].Y, 2) {
t.Errorf("Circle.IntersectPoints() = %v, want %v", v, tt.want[i])
}
}
})
}
}

View File

@ -75,11 +75,19 @@ func (c RGBA) RGBA() (r, g, b, a uint32) {
} }
// ToRGBA converts a color to RGBA format. Using this function is preferred to using RGBAModel, for // ToRGBA converts a color to RGBA format. Using this function is preferred to using RGBAModel, for
// performance (using RGBAModel introduces additional unnecessary allocations). // performance (using RGBAModel introduced additional unnecessary allocations).
func ToRGBA(c color.Color) RGBA { func ToRGBA(c color.Color) RGBA {
if c, ok := c.(RGBA); ok { if c, ok := c.(RGBA); ok {
return c return c
} }
if c, ok := c.(color.RGBA); ok {
return RGBA{
R: float64(c.R) / 255,
G: float64(c.G) / 255,
B: float64(c.B) / 255,
A: float64(c.A) / 255,
}
}
r, g, b, a := c.RGBA() r, g, b, a := c.RGBA()
return RGBA{ return RGBA{
float64(r) / 0xffff, float64(r) / 0xffff,

View File

@ -1,24 +0,0 @@
package pixel_test
import (
"fmt"
"image/color"
"testing"
"github.com/faiface/pixel"
)
func BenchmarkColorToRGBA(b *testing.B) {
types := []color.Color{
color.NRGBA{R: 124, G: 14, B: 230, A: 42}, // slowest
color.RGBA{R: 62, G: 32, B: 14, A: 63}, // faster
pixel.RGB(0.8, 0.2, 0.5).Scaled(0.712), // fastest
}
for _, col := range types {
b.Run(fmt.Sprintf("From %T", col), func(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = pixel.ToRGBA(col)
}
})
}
}

101
data.go
View File

@ -8,16 +8,6 @@ import (
"math" "math"
) )
// zeroValueTriangleData is the default value of a TriangleData element
var zeroValueTriangleData = struct {
Position Vec
Color RGBA
Picture Vec
Intensity float64
ClipRect Rect
IsClipped bool
}{Color: RGBA{1, 1, 1, 1}}
// TrianglesData specifies a list of Triangles vertices with three common properties: // TrianglesData specifies a list of Triangles vertices with three common properties:
// TrianglesPosition, TrianglesColor and TrianglesPicture. // TrianglesPosition, TrianglesColor and TrianglesPicture.
type TrianglesData []struct { type TrianglesData []struct {
@ -25,8 +15,6 @@ type TrianglesData []struct {
Color RGBA Color RGBA
Picture Vec Picture Vec
Intensity float64 Intensity float64
ClipRect Rect
IsClipped bool
} }
// MakeTrianglesData creates TrianglesData of length len initialized with default property values. // MakeTrianglesData creates TrianglesData of length len initialized with default property values.
@ -34,11 +22,9 @@ type TrianglesData []struct {
// Prefer this function to make(TrianglesData, len), because make zeros them, while this function // Prefer this function to make(TrianglesData, len), because make zeros them, while this function
// does the correct intialization. // does the correct intialization.
func MakeTrianglesData(len int) *TrianglesData { func MakeTrianglesData(len int) *TrianglesData {
td := make(TrianglesData, len) td := &TrianglesData{}
for i := 0; i < len; i++ { td.SetLen(len)
td[i] = zeroValueTriangleData return td
}
return &td
} }
// Len returns the number of vertices in TrianglesData. // Len returns the number of vertices in TrianglesData.
@ -54,7 +40,12 @@ func (td *TrianglesData) SetLen(len int) {
if len > td.Len() { if len > td.Len() {
needAppend := len - td.Len() needAppend := len - td.Len()
for i := 0; i < needAppend; i++ { for i := 0; i < needAppend; i++ {
*td = append(*td, zeroValueTriangleData) *td = append(*td, struct {
Position Vec
Color RGBA
Picture Vec
Intensity float64
}{V(0, 0), Alpha(1), V(0, 0), 0})
} }
} }
if len < td.Len() { if len < td.Len() {
@ -91,11 +82,6 @@ func (td *TrianglesData) updateData(t Triangles) {
(*td)[i].Picture, (*td)[i].Intensity = t.Picture(i) (*td)[i].Picture, (*td)[i].Intensity = t.Picture(i)
} }
} }
if t, ok := t.(TrianglesClipped); ok {
for i := range *td {
(*td)[i].ClipRect, (*td)[i].IsClipped = t.ClipRect(i)
}
}
} }
// Update copies vertex properties from the supplied Triangles into this TrianglesData. // Update copies vertex properties from the supplied Triangles into this TrianglesData.
@ -110,9 +96,10 @@ func (td *TrianglesData) Update(t Triangles) {
// Copy returns an exact independent copy of this TrianglesData. // Copy returns an exact independent copy of this TrianglesData.
func (td *TrianglesData) Copy() Triangles { func (td *TrianglesData) Copy() Triangles {
copyTd := MakeTrianglesData(td.Len()) copyTd := TrianglesData{}
copyTd.SetLen(td.Len())
copyTd.Update(td) copyTd.Update(td)
return copyTd return &copyTd
} }
// Position returns the position property of i-th vertex. // Position returns the position property of i-th vertex.
@ -130,11 +117,6 @@ func (td *TrianglesData) Picture(i int) (pic Vec, intensity float64) {
return (*td)[i].Picture, (*td)[i].Intensity return (*td)[i].Picture, (*td)[i].Intensity
} }
// ClipRect returns the clipping rectangle property of the i-th vertex.
func (td *TrianglesData) ClipRect(i int) (rect Rect, has bool) {
return (*td)[i].ClipRect, (*td)[i].IsClipped
}
// PictureData specifies an in-memory rectangular area of pixels and implements Picture and // PictureData specifies an in-memory rectangular area of pixels and implements Picture and
// PictureColor. // PictureColor.
// //
@ -154,8 +136,8 @@ type PictureData struct {
// MakePictureData creates a zero-initialized PictureData covering the given rectangle. // MakePictureData creates a zero-initialized PictureData covering the given rectangle.
func MakePictureData(rect Rect) *PictureData { func MakePictureData(rect Rect) *PictureData {
w := int(math.Ceil(rect.Max.X)) - int(math.Floor(rect.Min.X)) w := int(math.Ceil(rect.Max.X())) - int(math.Floor(rect.Min.X()))
h := int(math.Ceil(rect.Max.Y)) - int(math.Floor(rect.Min.Y)) h := int(math.Ceil(rect.Max.Y())) - int(math.Floor(rect.Min.Y()))
pd := &PictureData{ pd := &PictureData{
Stride: w, Stride: w,
Rect: rect, Rect: rect,
@ -183,16 +165,21 @@ func verticalFlip(rgba *image.RGBA) {
// //
// The resulting PictureData's Bounds will be the equivalent of the supplied image.Image's Bounds. // The resulting PictureData's Bounds will be the equivalent of the supplied image.Image's Bounds.
func PictureDataFromImage(img image.Image) *PictureData { func PictureDataFromImage(img image.Image) *PictureData {
rgba := image.NewRGBA(img.Bounds()) var rgba *image.RGBA
draw.Draw(rgba, rgba.Bounds(), img, img.Bounds().Min, draw.Src) if rgbaImg, ok := img.(*image.RGBA); ok {
rgba = rgbaImg
} else {
rgba = image.NewRGBA(img.Bounds())
draw.Draw(rgba, rgba.Bounds(), img, img.Bounds().Min, draw.Src)
}
verticalFlip(rgba) verticalFlip(rgba)
pd := MakePictureData(R( pd := MakePictureData(R(
float64(rgba.Bounds().Min.X), float64(rgba.Bounds().Min.X),
float64(rgba.Bounds().Min.Y), float64(rgba.Bounds().Min.Y),
float64(rgba.Bounds().Max.X), float64(rgba.Bounds().Dx()),
float64(rgba.Bounds().Max.Y), float64(rgba.Bounds().Dy()),
)) ))
for i := range pd.Pix { for i := range pd.Pix {
@ -218,20 +205,14 @@ func PictureDataFromPicture(pic Picture) *PictureData {
pd := MakePictureData(bounds) pd := MakePictureData(bounds)
if pic, ok := pic.(PictureColor); ok { if pic, ok := pic.(PictureColor); ok {
for y := math.Floor(bounds.Min.Y); y < bounds.Max.Y; y++ { for y := math.Floor(bounds.Min.Y()); y < bounds.Max.Y(); y++ {
for x := math.Floor(bounds.Min.X); x < bounds.Max.X; x++ { for x := math.Floor(bounds.Min.X()); x < bounds.Max.X(); x++ {
// this together with the Floor is a trick to get all of the pixels // this together with the Floor is a trick to get all of the pixels
at := V( at := V(
math.Max(x, bounds.Min.X), math.Max(x, bounds.Min.X()),
math.Max(y, bounds.Min.Y), math.Max(y, bounds.Min.Y()),
) )
col := pic.Color(at) pd.SetColor(at, pic.Color(at))
pd.Pix[pd.Index(at)] = color.RGBA{
R: uint8(col.R * 255),
G: uint8(col.G * 255),
B: uint8(col.B * 255),
A: uint8(col.A * 255),
}
} }
} }
} }
@ -244,10 +225,10 @@ func PictureDataFromPicture(pic Picture) *PictureData {
// The resulting image.RGBA's Bounds will be equivalent of the PictureData's Bounds. // The resulting image.RGBA's Bounds will be equivalent of the PictureData's Bounds.
func (pd *PictureData) Image() *image.RGBA { func (pd *PictureData) Image() *image.RGBA {
bounds := image.Rect( bounds := image.Rect(
int(math.Floor(pd.Rect.Min.X)), int(math.Floor(pd.Rect.Min.X())),
int(math.Floor(pd.Rect.Min.Y)), int(math.Floor(pd.Rect.Min.Y())),
int(math.Ceil(pd.Rect.Max.X)), int(math.Ceil(pd.Rect.Max.X())),
int(math.Ceil(pd.Rect.Max.Y)), int(math.Ceil(pd.Rect.Max.Y())),
) )
rgba := image.NewRGBA(bounds) rgba := image.NewRGBA(bounds)
@ -270,8 +251,8 @@ func (pd *PictureData) Image() *image.RGBA {
// Index returns the index of the pixel at the specified position inside the Pix slice. // Index returns the index of the pixel at the specified position inside the Pix slice.
func (pd *PictureData) Index(at Vec) int { func (pd *PictureData) Index(at Vec) int {
at = at.Sub(pd.Rect.Min.Map(math.Floor)) at -= pd.Rect.Min.Map(math.Floor)
x, y := int(at.X), int(at.Y) x, y := int(at.X()), int(at.Y())
return y*pd.Stride + x return y*pd.Stride + x
} }
@ -287,3 +268,17 @@ func (pd *PictureData) Color(at Vec) RGBA {
} }
return ToRGBA(pd.Pix[pd.Index(at)]) return ToRGBA(pd.Pix[pd.Index(at)])
} }
// SetColor changes the color located at the given position.
func (pd *PictureData) SetColor(at Vec, col color.Color) {
if !pd.Rect.Contains(at) {
return
}
rgba := ToRGBA(col)
pd.Pix[pd.Index(at)] = color.RGBA{
R: uint8(rgba.R * 255),
G: uint8(rgba.G * 255),
B: uint8(rgba.B * 255),
A: uint8(rgba.A * 255),
}
}

View File

@ -1,300 +0,0 @@
package pixel_test
import (
"testing"
"github.com/faiface/pixel"
)
func BenchmarkMakeTrianglesData(b *testing.B) {
tests := []struct {
name string
len int
}{
{
name: "Small slice",
len: 10,
},
{
name: "Large slice",
len: 10000,
},
}
for _, tt := range tests {
b.Run(tt.name, func(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = pixel.MakeTrianglesData(tt.len)
}
})
}
}
func BenchmarkTrianglesData_Len(b *testing.B) {
tests := []struct {
name string
tData *pixel.TrianglesData
}{
{
name: "Small slice",
tData: pixel.MakeTrianglesData(10),
},
{
name: "Large slice",
tData: pixel.MakeTrianglesData(10000),
},
}
for _, tt := range tests {
b.Run(tt.name, func(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = tt.tData.Len()
}
})
}
}
func BenchmarkTrianglesData_SetLen(b *testing.B) {
tests := []struct {
name string
tData *pixel.TrianglesData
nextLenFunc func(int, int) (int, int)
}{
{
name: "Stay same size",
tData: pixel.MakeTrianglesData(50),
nextLenFunc: func(i, j int) (int, int) { return 50, 0 },
},
{
name: "Change size",
tData: pixel.MakeTrianglesData(50),
nextLenFunc: func(i, j int) (int, int) {
// 0 is shrink
if j == 0 {
next := i - 1
if next < 1 {
return 2, 1
}
return next, 0
}
// other than 0 is grow
next := i + 1
if next == 100 {
return next, 0
}
return next, 1
},
},
}
for _, tt := range tests {
b.Run(tt.name, func(b *testing.B) {
var newLen int
var c int
for i := 0; i < b.N; i++ {
newLen, c = tt.nextLenFunc(newLen, c)
tt.tData.SetLen(newLen)
}
})
}
}
func BenchmarkTrianglesData_Slice(b *testing.B) {
tests := []struct {
name string
tData *pixel.TrianglesData
}{
{
name: "Basic slice",
tData: pixel.MakeTrianglesData(100),
},
}
for _, tt := range tests {
b.Run(tt.name, func(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = tt.tData.Slice(25, 50)
}
})
}
}
func BenchmarkTrianglesData_Update(b *testing.B) {
tests := []struct {
name string
tData *pixel.TrianglesData
t pixel.Triangles
}{
{
name: "Small Triangles",
tData: pixel.MakeTrianglesData(20),
t: pixel.MakeTrianglesData(20),
},
{
name: "Large Triangles",
tData: pixel.MakeTrianglesData(10000),
t: pixel.MakeTrianglesData(10000),
},
}
for _, tt := range tests {
b.Run(tt.name, func(b *testing.B) {
for i := 0; i < b.N; i++ {
tt.tData.Update(tt.t)
}
})
}
}
func BenchmarkTrianglesData_Copy(b *testing.B) {
tests := []struct {
name string
tData *pixel.TrianglesData
}{
{
name: "Small copy",
tData: pixel.MakeTrianglesData(20),
},
{
name: "Large copy",
tData: pixel.MakeTrianglesData(10000),
},
}
for _, tt := range tests {
b.Run(tt.name, func(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = tt.tData.Copy()
}
})
}
}
func BenchmarkTrianglesData_Position(b *testing.B) {
tests := []struct {
name string
tData *pixel.TrianglesData
position int
}{
{
name: "Getting beginning position",
tData: pixel.MakeTrianglesData(1000),
position: 2,
},
{
name: "Getting middle position",
tData: pixel.MakeTrianglesData(1000),
position: 500,
},
{
name: "Getting end position",
tData: pixel.MakeTrianglesData(1000),
position: 999,
},
}
for _, tt := range tests {
b.Run(tt.name, func(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = tt.tData.Position(tt.position)
}
})
}
}
func BenchmarkTrianglesData_Color(b *testing.B) {
tests := []struct {
name string
tData *pixel.TrianglesData
position int
}{
{
name: "Getting beginning position",
tData: pixel.MakeTrianglesData(1000),
position: 2,
},
{
name: "Getting middle position",
tData: pixel.MakeTrianglesData(1000),
position: 500,
},
{
name: "Getting end position",
tData: pixel.MakeTrianglesData(1000),
position: 999,
},
}
for _, tt := range tests {
b.Run(tt.name, func(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = tt.tData.Color(tt.position)
}
})
}
}
func BenchmarkTrianglesData_Picture(b *testing.B) {
tests := []struct {
name string
tData *pixel.TrianglesData
position int
}{
{
name: "Getting beginning position",
tData: pixel.MakeTrianglesData(1000),
position: 2,
},
{
name: "Getting middle position",
tData: pixel.MakeTrianglesData(1000),
position: 500,
},
{
name: "Getting end position",
tData: pixel.MakeTrianglesData(1000),
position: 999,
},
}
for _, tt := range tests {
b.Run(tt.name, func(b *testing.B) {
for i := 0; i < b.N; i++ {
_, _ = tt.tData.Picture(tt.position)
}
})
}
}
func BenchmarkTrianglesData_ClipRect(b *testing.B) {
tests := []struct {
name string
tData *pixel.TrianglesData
position int
}{
{
name: "Getting beginning position",
tData: pixel.MakeTrianglesData(1000),
position: 2,
},
{
name: "Getting middle position",
tData: pixel.MakeTrianglesData(1000),
position: 500,
},
{
name: "Getting end position",
tData: pixel.MakeTrianglesData(1000),
position: 999,
},
}
for _, tt := range tests {
b.Run(tt.name, func(b *testing.B) {
for i := 0; i < b.N; i++ {
_, _ = tt.tData.ClipRect(tt.position)
}
})
}
}

View File

@ -15,30 +15,26 @@ package pixel
// //
// Whenever you change the Triangles, call Dirty to notify Drawer that Triangles changed. You don't // Whenever you change the Triangles, call Dirty to notify Drawer that Triangles changed. You don't
// need to notify Drawer about a change of the Picture. // need to notify Drawer about a change of the Picture.
//
// Note, that Drawer caches the results of MakePicture from Targets it's drawn to for each Picture
// it's set to. What it means is that using a Drawer with an unbounded number of Pictures leads to a
// memory leak, since Drawer caches them and never forgets. In such a situation, create a new Drawer
// for each Picture.
type Drawer struct { type Drawer struct {
Triangles Triangles Triangles Triangles
Picture Picture Picture Picture
Cached bool
targets map[Target]*drawerTarget tris map[Target]TargetTriangles
allTargets []*drawerTarget clean map[Target]bool
inited bool pics map[targetPicturePair]TargetPicture
inited bool
} }
type drawerTarget struct { type targetPicturePair struct {
tris TargetTriangles Target Target
pics map[Picture]TargetPicture Picture Picture
clean bool
} }
func (d *Drawer) lazyInit() { func (d *Drawer) lazyInit() {
if !d.inited { if !d.inited {
d.targets = make(map[Target]*drawerTarget) d.tris = make(map[Target]TargetTriangles)
d.clean = make(map[Target]bool)
d.pics = make(map[targetPicturePair]TargetPicture)
d.inited = true d.inited = true
} }
} }
@ -48,8 +44,8 @@ func (d *Drawer) lazyInit() {
func (d *Drawer) Dirty() { func (d *Drawer) Dirty() {
d.lazyInit() d.lazyInit()
for _, t := range d.allTargets { for t := range d.clean {
t.clean = false d.clean[t] = false
} }
} }
@ -64,39 +60,29 @@ func (d *Drawer) Draw(t Target) {
return return
} }
dt := d.targets[t] tri := d.tris[t]
if dt == nil { if tri == nil {
dt = &drawerTarget{ tri = t.MakeTriangles(d.Triangles)
pics: make(map[Picture]TargetPicture), d.tris[t] = tri
} d.clean[t] = true
d.targets[t] = dt
d.allTargets = append(d.allTargets, dt)
} }
if dt.tris == nil { if !d.clean[t] {
dt.tris = t.MakeTriangles(d.Triangles) tri.SetLen(d.Triangles.Len())
dt.clean = true tri.Update(d.Triangles)
} d.clean[t] = true
if !dt.clean {
dt.tris.SetLen(d.Triangles.Len())
dt.tris.Update(d.Triangles)
dt.clean = true
} }
if d.Picture == nil { if d.Picture == nil {
dt.tris.Draw() tri.Draw()
return return
} }
pic := dt.pics[d.Picture] pic := d.pics[targetPicturePair{t, d.Picture}]
if pic == nil { if pic == nil {
pic = t.MakePicture(d.Picture) pic = t.MakePicture(d.Picture)
d.pics[targetPicturePair{t, d.Picture}] = pic
if d.Cached {
dt.pics[d.Picture] = pic
}
} }
pic.Draw(dt.tris) pic.Draw(tri)
} }

View File

@ -1,18 +0,0 @@
package pixel_test
import (
"image"
"testing"
"github.com/faiface/pixel"
)
func BenchmarkSpriteDrawBatch(b *testing.B) {
img := image.NewRGBA(image.Rect(0, 0, 64, 64))
pic := pixel.PictureDataFromImage(img)
sprite := pixel.NewSprite(pic, pixel.R(0, 0, 64, 64))
batch := pixel.NewBatch(&pixel.TrianglesData{}, pic)
for i := 0; i < b.N; i++ {
sprite.Draw(batch, pixel.IM)
}
}

View File

@ -0,0 +1,29 @@
package main
import (
"github.com/faiface/pixel"
"github.com/faiface/pixel/pixelgl"
"golang.org/x/image/colornames"
)
func run() {
cfg := pixelgl.WindowConfig{
Title: "Pixel Rocks!",
Bounds: pixel.R(0, 0, 1024, 768),
VSync: true,
}
win, err := pixelgl.NewWindow(cfg)
if err != nil {
panic(err)
}
win.Clear(colornames.Skyblue)
for !win.Closed() {
win.Update()
}
}
func main() {
pixelgl.Run(run)
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@ -0,0 +1,53 @@
package main
import (
"math"
"github.com/faiface/pixel"
"github.com/faiface/pixel/imdraw"
"github.com/faiface/pixel/pixelgl"
"golang.org/x/image/colornames"
)
func run() {
cfg := pixelgl.WindowConfig{
Title: "Pixel Rocks!",
Bounds: pixel.R(0, 0, 1024, 768),
VSync: true,
}
win, err := pixelgl.NewWindow(cfg)
if err != nil {
panic(err)
}
imd := imdraw.New(nil)
imd.Color(colornames.Blueviolet)
imd.EndShape(imdraw.RoundEndShape)
imd.Push(pixel.V(100, 100), pixel.V(700, 100))
imd.EndShape(imdraw.SharpEndShape)
imd.Push(pixel.V(100, 500), pixel.V(700, 500))
imd.Line(30)
imd.Color(colornames.Limegreen)
imd.Push(pixel.V(500, 500))
imd.Circle(300, 50)
imd.Color(colornames.Navy)
imd.Push(pixel.V(200, 500), pixel.V(800, 500))
imd.Ellipse(pixel.V(120, 80), 0)
imd.Color(colornames.Red)
imd.EndShape(imdraw.RoundEndShape)
imd.Push(pixel.V(500, 350))
imd.CircleArc(150, -math.Pi, 0, 30)
for !win.Closed() {
win.Clear(colornames.Aliceblue)
imd.Draw(win)
win.Update()
}
}
func main() {
pixelgl.Run(run)
}

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

@ -0,0 +1,13 @@
# Lights
This example demonstrates powerful Porter-Duff composition used to create a nice noisy light effect.
**Use W and S keys** to adjust the level of "dust".
The FPS is limited to 30, because the effect is a little expensive and my computer couldn't handle
60 FPS. If you have a more powerful computer (which is quite likely), peek into the code and disable
the limit.
Credit for the panda art goes to [Ján Štrba](https://www.artstation.com/artist/janstrba).
![Screenshot](screenshot.png)

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

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

BIN
examples/lights/noise.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 838 KiB

BIN
examples/lights/panda.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

View File

@ -0,0 +1,14 @@
# Platformer
This example demostrates a way to put things together and create a simple platformer game with a
Gopher!
Use **arrow keys** to run and jump around. Press **ENTER** to restart. (And hush, hush, secret.
Press TAB for slo-mo!)
The retro feel is, other than from the pixel art spritesheet, achieved by using a 160x120px large
off-screen canvas, drawing everything to it and then stretching it to fit the window.
The Gopher spritesheet comes from excellent [Egon Elbre](https://github.com/egonelbre/gophers).
![Screenshot](screenshot.png)

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

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

View File

@ -0,0 +1,9 @@
Front,0,0
FrontBlink,1,1
LookUp,2,2
Left,3,7
LeftRight,4,6
LeftBlink,7,7
Walk,8,15
Run,16,23
Jump,24,26
1 Front 0 0
2 FrontBlink 1 1
3 LookUp 2 2
4 Left 3 7
5 LeftRight 4 6
6 LeftBlink 7 7
7 Walk 8 15
8 Run 16 23
9 Jump 24 26

Binary file not shown.

After

Width:  |  Height:  |  Size: 530 B

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

@ -0,0 +1,8 @@
# Smoke
This example implements a smoke particle effect using sprites. It uses a spritesheet with a CSV
description.
The art in the spritesheet comes from [Kenney](https://kenney.nl/).
![Screenshot](screenshot.png)

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 MiB

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

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 381 KiB

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

@ -0,0 +1,8 @@
# Xor
This example demonstrates an unusual Porter-Duff composition method: Xor. (And the capability of
drawing circles.)
Just thought it was cool.
![Screenshot](screenshot.png)

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

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

BIN
examples/xor/screenshot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

358
geometry.go Normal file
View File

@ -0,0 +1,358 @@
package pixel
import (
"fmt"
"math"
"math/cmplx"
"github.com/go-gl/mathgl/mgl64"
)
// Vec is a 2D vector type. It is unusually implemented as complex128 for convenience. Since
// Go does not allow operator overloading, implementing vector as a struct leads to a bunch of
// methods for addition, subtraction and multiplication of vectors. With complex128, much of
// this functionality is given through operators.
//
// Create vectors with the V constructor:
//
// u := pixel.V(1, 2)
// v := pixel.V(8, -3)
//
// Add and subtract them using the standard + and - operators:
//
// w := u + v
// fmt.Println(w) // Vec(9, -1)
// fmt.Println(u - v) // Vec(-7, 5)
//
// Additional standard vector operations can be obtained with methods:
//
// u := pixel.V(2, 3)
// v := pixel.V(8, 1)
// if u.X() < 0 {
// fmt.Println("this won't happen")
// }
// x := u.Unit().Dot(v.Unit())
type Vec complex128
// V returns a new 2D vector with the given coordinates.
func V(x, y float64) Vec {
return Vec(complex(x, y))
}
// X returns a 2D vector with coordinates (x, 0).
func X(x float64) Vec {
return V(x, 0)
}
// Y returns a 2D vector with coordinates (0, y).
func Y(y float64) Vec {
return V(0, y)
}
// String returns the string representation of the vector u.
//
// u := pixel.V(4.5, -1.3)
// u.String() // returns "Vec(4.5, -1.3)"
// fmt.Println(u) // Vec(4.5, -1.3)
func (u Vec) String() string {
return fmt.Sprintf("Vec(%v, %v)", u.X(), u.Y())
}
// X returns the x coordinate of the vector u.
func (u Vec) X() float64 {
return real(u)
}
// Y returns the y coordinate of the vector u.
func (u Vec) Y() float64 {
return imag(u)
}
// XY returns the components of the vector in two return values.
func (u Vec) XY() (x, y float64) {
return real(u), imag(u)
}
// Len returns the length of the vector u.
func (u Vec) Len() float64 {
return cmplx.Abs(complex128(u))
}
// Angle returns the angle between the vector u and the x-axis. The result is in range [-Pi, Pi].
func (u Vec) Angle() float64 {
return cmplx.Phase(complex128(u))
}
// Unit returns a vector of length 1 facing the direction of u (has the same angle).
func (u Vec) Unit() Vec {
if u == 0 {
return 1
}
return u / V(u.Len(), 0)
}
// Scaled returns the vector u multiplied by c.
func (u Vec) Scaled(c float64) Vec {
return u * V(c, 0)
}
// ScaledXY returns the vector u multiplied by the vector v component-wise.
func (u Vec) ScaledXY(v Vec) Vec {
return V(u.X()*v.X(), u.Y()*v.Y())
}
// Rotated returns the vector u rotated by the given angle in radians.
func (u Vec) Rotated(angle float64) Vec {
sin, cos := math.Sincos(angle)
return u * V(cos, sin)
}
// WithX return the vector u with the x coordinate changed to the given value.
func (u Vec) WithX(x float64) Vec {
return V(x, u.Y())
}
// WithY returns the vector u with the y coordinate changed to the given value.
func (u Vec) WithY(y float64) Vec {
return V(u.X(), y)
}
// Dot returns the dot product of vectors u and v.
func (u Vec) Dot(v Vec) float64 {
return u.X()*v.X() + u.Y()*v.Y()
}
// Cross return the cross product of vectors u and v.
func (u Vec) Cross(v Vec) float64 {
return u.X()*v.Y() - v.X()*u.Y()
}
// Map applies the function f to both x and y components of the vector u and returns the modified
// vector.
//
// u := pixel.V(10.5, -1.5)
// v := u.Map(math.Floor) // v is Vec(10, -2), both components of u floored
func (u Vec) Map(f func(float64) float64) Vec {
return V(
f(u.X()),
f(u.Y()),
)
}
// Lerp returns a linear interpolation between vectors a and b.
//
// This function basically returns a point along the line between a and b and t chooses which one.
// If t is 0, then a will be returned, if t is 1, b will be returned. Anything between 0 and 1 will
// return the appropriate point between a and b and so on.
func Lerp(a, b Vec, t float64) Vec {
return a.Scaled(1-t) + b.Scaled(t)
}
// Rect is a 2D rectangle aligned with the axes of the coordinate system. It is defined by two
// points, Min and Max.
//
// The invariant should hold, that Max's components are greater or equal than Min's components
// respectively.
type Rect struct {
Min, Max Vec
}
// R returns a new Rect with given the Min and Max coordinates.
func R(minX, minY, maxX, maxY float64) Rect {
return Rect{
Min: V(minX, minY),
Max: V(maxX, maxY),
}
}
// String returns the string representation of the Rect.
//
// r := pixel.R(100, 50, 200, 300)
// r.String() // returns "Rect(100, 50, 200, 300)"
// fmt.Println(r) // Rect(100, 50, 200, 300)
func (r Rect) String() string {
return fmt.Sprintf("Rect(%v, %v, %v, %v)", r.Min.X(), r.Min.Y(), r.Max.X(), r.Max.Y())
}
// Norm returns the Rect in normal form, such that Max is component-wise greater or equal than Min.
func (r Rect) Norm() Rect {
return Rect{
Min: V(
math.Min(r.Min.X(), r.Max.X()),
math.Min(r.Min.Y(), r.Max.Y()),
),
Max: V(
math.Max(r.Min.X(), r.Max.X()),
math.Max(r.Min.Y(), r.Max.Y()),
),
}
}
// W returns the width of the Rect.
func (r Rect) W() float64 {
return r.Max.X() - r.Min.X()
}
// H returns the height of the Rect.
func (r Rect) H() float64 {
return r.Max.Y() - r.Min.Y()
}
// Size returns the vector of width and height of the Rect.
func (r Rect) Size() Vec {
return V(r.W(), r.H())
}
// Center returns the position of the center of the Rect.
func (r Rect) Center() Vec {
return (r.Min + r.Max) / 2
}
// Moved returns the Rect moved (both Min and Max) by the given vector delta.
func (r Rect) Moved(delta Vec) Rect {
return Rect{
Min: r.Min + delta,
Max: r.Max + delta,
}
}
// WithMin returns the Rect with it's Min changed to the given position.
//
// Note, that the Rect is not automatically normalized.
func (r Rect) WithMin(min Vec) Rect {
return Rect{
Min: min,
Max: r.Max,
}
}
// WithMax returns the Rect with it's Max changed to the given position.
//
// Note, that the Rect is not automatically normalized.
func (r Rect) WithMax(max Vec) Rect {
return Rect{
Min: r.Min,
Max: max,
}
}
// Resized returns the Rect resized to the given size while keeping the position of the given
// anchor.
//
// r.Resized(r.Min, size) // resizes while keeping the position of the lower-left corner
// r.Resized(r.Max, size) // same with the top-right corner
// r.Resized(r.Center(), size) // resizes around the center
//
// This function does not make sense for sizes of zero area and will panic. Use ResizedMin in the
// case of zero area.
func (r Rect) Resized(anchor, size Vec) Rect {
if r.W()*r.H() == 0 || size.X()*size.Y() == 0 {
panic(fmt.Errorf("(%T).Resize: zero area", r))
}
fraction := V(size.X()/r.W(), size.Y()/r.H())
return Rect{
Min: anchor + (r.Min - anchor).ScaledXY(fraction),
Max: anchor + (r.Max - anchor).ScaledXY(fraction),
}
}
// ResizedMin returns the Rect resized to the given size while keeping the position of the Rect's
// Min.
//
// Sizes of zero area are safe here.
func (r Rect) ResizedMin(size Vec) Rect {
return Rect{
Min: r.Min,
Max: r.Min + size,
}
}
// Contains checks whether a vector u is contained within this Rect (including it's borders).
func (r Rect) Contains(u Vec) bool {
return r.Min.X() <= u.X() && u.X() <= r.Max.X() && r.Min.Y() <= u.Y() && u.Y() <= r.Max.Y()
}
// Matrix is a 3x3 transformation matrix that can be used for all kinds of spacial transforms, such
// as movement, scaling and rotations.
//
// Matrix has a handful of useful methods, each of which adds a transformation to the matrix. For
// example:
//
// pixel.IM.Moved(pixel.V(100, 200)).Rotated(0, math.Pi/2)
//
// This code creates a Matrix that first moves everything by 100 units horizontally and 200 units
// vertically and then rotates everything by 90 degrees around the origin.
type Matrix [9]float64
// IM stands for identity matrix. Does nothing, no transformation.
var IM = Matrix(mgl64.Ident3())
// String returns a string representation of the Matrix.
//
// m := pixel.IM
// fmt.Println(m) // Matrix(1 0 0 | 0 1 0 | 0 0 1)
func (m Matrix) String() string {
return fmt.Sprintf(
"Matrix(%v %v %v | %v %v %v | %v %v %v)",
m[0], m[3], m[6],
m[1], m[4], m[7],
m[2], m[5], m[8],
)
}
// Moved moves everything by the delta vector.
func (m Matrix) Moved(delta Vec) Matrix {
m3 := mgl64.Mat3(m)
m3 = mgl64.Translate2D(delta.XY()).Mul3(m3)
return Matrix(m3)
}
// ScaledXY scales everything around a given point by the scale factor in each axis respectively.
func (m Matrix) ScaledXY(around Vec, scale Vec) Matrix {
m3 := mgl64.Mat3(m)
m3 = mgl64.Translate2D((-around).XY()).Mul3(m3)
m3 = mgl64.Scale2D(scale.XY()).Mul3(m3)
m3 = mgl64.Translate2D(around.XY()).Mul3(m3)
return Matrix(m3)
}
// Scaled scales everything around a given point by the scale factor.
func (m Matrix) Scaled(around Vec, scale float64) Matrix {
return m.ScaledXY(around, V(scale, scale))
}
// Rotated rotates everything around a given point by the given angle in radians.
func (m Matrix) Rotated(around Vec, angle float64) Matrix {
m3 := mgl64.Mat3(m)
m3 = mgl64.Translate2D((-around).XY()).Mul3(m3)
m3 = mgl64.Rotate3DZ(angle).Mul3(m3)
m3 = mgl64.Translate2D(around.XY()).Mul3(m3)
return Matrix(m3)
}
// Chained adds another Matrix to this one. All tranformations by the next Matrix will be applied
// after the transformations of this Matrix.
func (m Matrix) Chained(next Matrix) Matrix {
m3 := mgl64.Mat3(m)
m3 = mgl64.Mat3(next).Mul3(m3)
return Matrix(m3)
}
// Project applies all transformations added to the Matrix to a vector u and returns the result.
//
// Time complexity is O(1).
func (m Matrix) Project(u Vec) Vec {
m3 := mgl64.Mat3(m)
proj := m3.Mul3x1(mgl64.Vec3{u.X(), u.Y(), 1})
return V(proj.X(), proj.Y())
}
// Unproject does the inverse operation to Project.
//
// Time complexity is O(1).
func (m Matrix) Unproject(u Vec) Vec {
m3 := mgl64.Mat3(m)
inv := m3.Inv()
unproj := inv.Mul3x1(mgl64.Vec3{u.X(), u.Y(), 1})
return V(unproj.X(), unproj.Y())
}

15
go.mod
View File

@ -1,15 +0,0 @@
module github.com/faiface/pixel
go 1.12
require (
github.com/faiface/glhf v0.0.0-20211013000516-57b20770c369
github.com/faiface/mainthread v0.0.0-20171120011319-8b78f0a41ae3
github.com/go-gl/gl v0.0.0-20210905235341-f7a045908259
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20210727001814-0db043d8d5be
github.com/go-gl/mathgl v1.0.0
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0
github.com/pkg/errors v0.9.1
github.com/stretchr/testify v1.7.0
golang.org/x/image v0.5.0
)

54
go.sum
View File

@ -1,54 +0,0 @@
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/faiface/glhf v0.0.0-20211013000516-57b20770c369 h1:gv4BgP50atccdK/1tZHDyP6rMwiiutR2HPreR/OyLzI=
github.com/faiface/glhf v0.0.0-20211013000516-57b20770c369/go.mod h1:dDdUO+G9ZnJ9sc8nIUvhLkE45k8PEKW6+A3TdWsfpV0=
github.com/faiface/mainthread v0.0.0-20171120011319-8b78f0a41ae3 h1:baVdMKlASEHrj19iqjARrPbaRisD7EuZEVJj6ZMLl1Q=
github.com/faiface/mainthread v0.0.0-20171120011319-8b78f0a41ae3/go.mod h1:VEPNJUlxl5KdWjDvz6Q1l+rJlxF2i6xqDeGuGAxa87M=
github.com/go-gl/gl v0.0.0-20210905235341-f7a045908259 h1:8q7+xl2D2qHPLTII1t4vSMNP2VKwDcn+Avf2WXvdB1A=
github.com/go-gl/gl v0.0.0-20210905235341-f7a045908259/go.mod h1:wjpnOv6ONl2SuJSxqCPVaPZibGFdSci9HFocT9qtVYM=
github.com/go-gl/glfw v0.0.0-20210727001814-0db043d8d5be h1:UVW91pfMB1GRQfVwC7//RGVbqX6Ea8jURmJhlANak1M=
github.com/go-gl/glfw v0.0.0-20210727001814-0db043d8d5be/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20210727001814-0db043d8d5be h1:vEIVIuBApEBQTEJt19GfhoU+zFSV+sNTa9E9FdnRYfk=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20210727001814-0db043d8d5be/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/mathgl v1.0.0 h1:t9DznWJlXxxjeeKLIdovCOVJQk/GzDEL7h/h+Ro2B68=
github.com/go-gl/mathgl v1.0.0/go.mod h1:yhpkQzEiH9yPyxDUGzkmgScbaBVlhC06qodikEM0ZwQ=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/image v0.0.0-20190321063152-3fc05d484e9f/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.5.0 h1:5JMiNunQeQw++mMOz48/ISeNu3Iweh/JaZU8ZLqHRrI=
golang.org/x/image v0.5.0/go.mod h1:FVC7BI/5Ym8R25iw5OLsgshdUBbT1h5jZTpA+mvAdZ4=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@ -23,9 +23,9 @@ import (
// //
// imd.Line(20) // draws a 20 units thick line // imd.Line(20) // draws a 20 units thick line
// //
// Set exported fields to change properties of Pushed points: // Use various methods to change properties of Pushed points:
// //
// imd.Color = pixel.RGB(1, 0, 0) // imd.Color(pixel.RGB(1, 0, 0))
// imd.Push(pixel.V(200, 200)) // imd.Push(pixel.V(200, 200))
// imd.Circle(400, 0) // imd.Circle(400, 0)
// //
@ -45,14 +45,8 @@ import (
// - Ellipse // - Ellipse
// - Ellipse arc // - Ellipse arc
type IMDraw struct { type IMDraw struct {
Color color.Color
Picture pixel.Vec
Intensity float64
Precision int
EndShape EndShape
points []point points []point
pool [][]point opts point
matrix pixel.Matrix matrix pixel.Matrix
mask pixel.RGBA mask pixel.RGBA
@ -110,12 +104,9 @@ func (imd *IMDraw) Clear() {
// //
// This does not affect matrix and color mask set by SetMatrix and SetColorMask. // This does not affect matrix and color mask set by SetMatrix and SetColorMask.
func (imd *IMDraw) Reset() { func (imd *IMDraw) Reset() {
imd.points = imd.points[:0] imd.points = nil
imd.Color = pixel.Alpha(1) imd.opts = point{}
imd.Picture = pixel.ZV imd.Precision(64)
imd.Intensity = 0
imd.Precision = 64
imd.EndShape = NoEndShape
} }
// Draw draws all currently drawn shapes inside the IM onto another Target. // Draw draws all currently drawn shapes inside the IM onto another Target.
@ -128,20 +119,8 @@ func (imd *IMDraw) Draw(t pixel.Target) {
// Push adds some points to the IM queue. All Pushed points will have the same properties except for // Push adds some points to the IM queue. All Pushed points will have the same properties except for
// the position. // the position.
func (imd *IMDraw) Push(pts ...pixel.Vec) { func (imd *IMDraw) Push(pts ...pixel.Vec) {
// Assert that Color is of type pixel.RGBA,
if _, ok := imd.Color.(pixel.RGBA); !ok {
// otherwise cast it
imd.Color = pixel.ToRGBA(imd.Color)
}
opts := point{
col: imd.Color.(pixel.RGBA),
pic: imd.Picture,
in: imd.Intensity,
precision: imd.Precision,
endshape: imd.EndShape,
}
for _, pt := range pts { for _, pt := range pts {
imd.pushPt(pt, opts) imd.pushPt(pt, imd.opts)
} }
} }
@ -150,6 +129,33 @@ func (imd *IMDraw) pushPt(pos pixel.Vec, pt point) {
imd.points = append(imd.points, pt) imd.points = append(imd.points, pt)
} }
// Color sets the color of the next Pushed points.
func (imd *IMDraw) Color(color color.Color) {
imd.opts.col = pixel.ToRGBA(color)
}
// Picture sets the Picture coordinates of the next Pushed points.
func (imd *IMDraw) Picture(pic pixel.Vec) {
imd.opts.pic = pic
}
// Intensity sets the picture Intensity of the next Pushed points.
func (imd *IMDraw) Intensity(in float64) {
imd.opts.in = in
}
// Precision sets the curve/circle drawing precision of the next Pushed points.
//
// It is the number of segments per 360 degrees.
func (imd *IMDraw) Precision(p int) {
imd.opts.precision = p
}
// EndShape sets the endshape of the next Pushed points.
func (imd *IMDraw) EndShape(es EndShape) {
imd.opts.endshape = es
}
// SetMatrix sets a Matrix that all further points will be transformed by. // SetMatrix sets a Matrix that all further points will be transformed by.
func (imd *IMDraw) SetMatrix(m pixel.Matrix) { func (imd *IMDraw) SetMatrix(m pixel.Matrix) {
imd.matrix = m imd.matrix = m
@ -261,22 +267,10 @@ func (imd *IMDraw) EllipseArc(radius pixel.Vec, low, high, thickness float64) {
func (imd *IMDraw) getAndClearPoints() []point { func (imd *IMDraw) getAndClearPoints() []point {
points := imd.points points := imd.points
// use one of the existing pools so we don't reallocate as often imd.points = nil
if len(imd.pool) > 0 {
pos := len(imd.pool) - 1
imd.points = imd.pool[pos][:0]
imd.pool = imd.pool[:pos]
} else {
imd.points = nil
}
return points return points
} }
func (imd *IMDraw) restorePoints(points []point) {
imd.pool = append(imd.pool, imd.points)
imd.points = points[:0]
}
func (imd *IMDraw) applyMatrixAndMask(off int) { func (imd *IMDraw) applyMatrixAndMask(off int) {
for i := range (*imd.tri)[off:] { for i := range (*imd.tri)[off:] {
(*imd.tri)[off+i].Position = imd.matrix.Project((*imd.tri)[off+i].Position) (*imd.tri)[off+i].Position = imd.matrix.Project((*imd.tri)[off+i].Position)
@ -288,7 +282,6 @@ func (imd *IMDraw) fillRectangle() {
points := imd.getAndClearPoints() points := imd.getAndClearPoints()
if len(points) < 2 { if len(points) < 2 {
imd.restorePoints(points)
return return
} }
@ -298,19 +291,19 @@ func (imd *IMDraw) fillRectangle() {
for i, j := 0, off; i+1 < len(points); i, j = i+1, j+6 { for i, j := 0, off; i+1 < len(points); i, j = i+1, j+6 {
a, b := points[i], points[i+1] a, b := points[i], points[i+1]
c := point{ c := point{
pos: pixel.V(a.pos.X, b.pos.Y), pos: pixel.V(a.pos.X(), b.pos.Y()),
col: a.col.Add(b.col).Mul(pixel.Alpha(0.5)), col: a.col.Add(b.col).Mul(pixel.Alpha(0.5)),
pic: pixel.V(a.pic.X, b.pic.Y), pic: pixel.V(a.pic.X(), b.pic.Y()),
in: (a.in + b.in) / 2, in: (a.in + b.in) / 2,
} }
d := point{ d := point{
pos: pixel.V(b.pos.X, a.pos.Y), pos: pixel.V(b.pos.X(), a.pos.Y()),
col: a.col.Add(b.col).Mul(pixel.Alpha(0.5)), col: a.col.Add(b.col).Mul(pixel.Alpha(0.5)),
pic: pixel.V(b.pic.X, a.pic.Y), pic: pixel.V(b.pic.X(), a.pic.Y()),
in: (a.in + b.in) / 2, in: (a.in + b.in) / 2,
} }
for k, p := range [...]point{a, b, c, a, b, d} { for k, p := range []point{a, b, c, a, b, d} {
(*imd.tri)[j+k].Position = p.pos (*imd.tri)[j+k].Position = p.pos
(*imd.tri)[j+k].Color = p.col (*imd.tri)[j+k].Color = p.col
(*imd.tri)[j+k].Picture = p.pic (*imd.tri)[j+k].Picture = p.pic
@ -320,15 +313,12 @@ func (imd *IMDraw) fillRectangle() {
imd.applyMatrixAndMask(off) imd.applyMatrixAndMask(off)
imd.batch.Dirty() imd.batch.Dirty()
imd.restorePoints(points)
} }
func (imd *IMDraw) outlineRectangle(thickness float64) { func (imd *IMDraw) outlineRectangle(thickness float64) {
points := imd.getAndClearPoints() points := imd.getAndClearPoints()
if len(points) < 2 { if len(points) < 2 {
imd.restorePoints(points)
return return
} }
@ -339,20 +329,17 @@ func (imd *IMDraw) outlineRectangle(thickness float64) {
mid.in = (a.in + b.in) / 2 mid.in = (a.in + b.in) / 2
imd.pushPt(a.pos, a) imd.pushPt(a.pos, a)
imd.pushPt(pixel.V(a.pos.X, b.pos.Y), mid) imd.pushPt(pixel.V(a.pos.X(), b.pos.Y()), mid)
imd.pushPt(b.pos, b) imd.pushPt(b.pos, b)
imd.pushPt(pixel.V(b.pos.X, a.pos.Y), mid) imd.pushPt(pixel.V(b.pos.X(), a.pos.Y()), mid)
imd.polyline(thickness, true) imd.polyline(thickness, true)
} }
imd.restorePoints(points)
} }
func (imd *IMDraw) fillPolygon() { func (imd *IMDraw) fillPolygon() {
points := imd.getAndClearPoints() points := imd.getAndClearPoints()
if len(points) < 3 { if len(points) < 3 {
imd.restorePoints(points)
return return
} }
@ -360,19 +347,16 @@ func (imd *IMDraw) fillPolygon() {
imd.tri.SetLen(imd.tri.Len() + 3*(len(points)-2)) imd.tri.SetLen(imd.tri.Len() + 3*(len(points)-2))
for i, j := 1, off; i+1 < len(points); i, j = i+1, j+3 { for i, j := 1, off; i+1 < len(points); i, j = i+1, j+3 {
for k, p := range [...]int{0, i, i + 1} { for k, p := range []point{points[0], points[i], points[i+1]} {
tri := &(*imd.tri)[j+k] (*imd.tri)[j+k].Position = p.pos
tri.Position = points[p].pos (*imd.tri)[j+k].Color = p.col
tri.Color = points[p].col (*imd.tri)[j+k].Picture = p.pic
tri.Picture = points[p].pic (*imd.tri)[j+k].Intensity = p.in
tri.Intensity = points[p].in
} }
} }
imd.applyMatrixAndMask(off) imd.applyMatrixAndMask(off)
imd.batch.Dirty() imd.batch.Dirty()
imd.restorePoints(points)
} }
func (imd *IMDraw) fillEllipseArc(radius pixel.Vec, low, high float64) { func (imd *IMDraw) fillEllipseArc(radius pixel.Vec, low, high float64) {
@ -387,24 +371,24 @@ func (imd *IMDraw) fillEllipseArc(radius pixel.Vec, low, high float64) {
for i := range (*imd.tri)[off:] { for i := range (*imd.tri)[off:] {
(*imd.tri)[off+i].Color = pt.col (*imd.tri)[off+i].Color = pt.col
(*imd.tri)[off+i].Picture = pixel.ZV (*imd.tri)[off+i].Picture = 0
(*imd.tri)[off+i].Intensity = 0 (*imd.tri)[off+i].Intensity = 0
} }
for i, j := 0.0, off; i < num; i, j = i+1, j+3 { for i, j := 0.0, off; i < num; i, j = i+1, j+3 {
angle := low + i*delta angle := low + i*delta
sin, cos := math.Sincos(angle) sin, cos := math.Sincos(angle)
a := pt.pos.Add(pixel.V( a := pt.pos + pixel.V(
radius.X*cos, radius.X()*cos,
radius.Y*sin, radius.Y()*sin,
)) )
angle = low + (i+1)*delta angle = low + (i+1)*delta
sin, cos = math.Sincos(angle) sin, cos = math.Sincos(angle)
b := pt.pos.Add(pixel.V( b := pt.pos + pixel.V(
radius.X*cos, radius.X()*cos,
radius.Y*sin, radius.Y()*sin,
)) )
(*imd.tri)[j+0].Position = pt.pos (*imd.tri)[j+0].Position = pt.pos
(*imd.tri)[j+1].Position = a (*imd.tri)[j+1].Position = a
@ -414,8 +398,6 @@ func (imd *IMDraw) fillEllipseArc(radius pixel.Vec, low, high float64) {
imd.applyMatrixAndMask(off) imd.applyMatrixAndMask(off)
imd.batch.Dirty() imd.batch.Dirty()
} }
imd.restorePoints(points)
} }
func (imd *IMDraw) outlineEllipseArc(radius pixel.Vec, low, high, thickness float64, doEndShape bool) { func (imd *IMDraw) outlineEllipseArc(radius pixel.Vec, low, high, thickness float64, doEndShape bool) {
@ -430,7 +412,7 @@ func (imd *IMDraw) outlineEllipseArc(radius pixel.Vec, low, high, thickness floa
for i := range (*imd.tri)[off:] { for i := range (*imd.tri)[off:] {
(*imd.tri)[off+i].Color = pt.col (*imd.tri)[off+i].Color = pt.col
(*imd.tri)[off+i].Picture = pixel.ZV (*imd.tri)[off+i].Picture = 0
(*imd.tri)[off+i].Intensity = 0 (*imd.tri)[off+i].Intensity = 0
} }
@ -438,26 +420,26 @@ func (imd *IMDraw) outlineEllipseArc(radius pixel.Vec, low, high, thickness floa
angle := low + i*delta angle := low + i*delta
sin, cos := math.Sincos(angle) sin, cos := math.Sincos(angle)
normalSin, normalCos := pixel.V(sin, cos).ScaledXY(radius).Unit().XY() normalSin, normalCos := pixel.V(sin, cos).ScaledXY(radius).Unit().XY()
a := pt.pos.Add(pixel.V( a := pt.pos + pixel.V(
radius.X*cos-thickness/2*normalCos, radius.X()*cos-thickness/2*normalCos,
radius.Y*sin-thickness/2*normalSin, radius.Y()*sin-thickness/2*normalSin,
)) )
b := pt.pos.Add(pixel.V( b := pt.pos + pixel.V(
radius.X*cos+thickness/2*normalCos, radius.X()*cos+thickness/2*normalCos,
radius.Y*sin+thickness/2*normalSin, radius.Y()*sin+thickness/2*normalSin,
)) )
angle = low + (i+1)*delta angle = low + (i+1)*delta
sin, cos = math.Sincos(angle) sin, cos = math.Sincos(angle)
normalSin, normalCos = pixel.V(sin, cos).ScaledXY(radius).Unit().XY() normalSin, normalCos = pixel.V(sin, cos).ScaledXY(radius).Unit().XY()
c := pt.pos.Add(pixel.V( c := pt.pos + pixel.V(
radius.X*cos-thickness/2*normalCos, radius.X()*cos-thickness/2*normalCos,
radius.Y*sin-thickness/2*normalSin, radius.Y()*sin-thickness/2*normalSin,
)) )
d := pt.pos.Add(pixel.V( d := pt.pos + pixel.V(
radius.X*cos+thickness/2*normalCos, radius.X()*cos+thickness/2*normalCos,
radius.Y*sin+thickness/2*normalSin, radius.Y()*sin+thickness/2*normalSin,
)) )
(*imd.tri)[j+0].Position = a (*imd.tri)[j+0].Position = a
(*imd.tri)[j+1].Position = b (*imd.tri)[j+1].Position = b
@ -472,18 +454,18 @@ func (imd *IMDraw) outlineEllipseArc(radius pixel.Vec, low, high, thickness floa
if doEndShape { if doEndShape {
lowSin, lowCos := math.Sincos(low) lowSin, lowCos := math.Sincos(low)
lowCenter := pt.pos.Add(pixel.V( lowCenter := pt.pos + pixel.V(
radius.X*lowCos, radius.X()*lowCos,
radius.Y*lowSin, radius.Y()*lowSin,
)) )
normalLowSin, normalLowCos := pixel.V(lowSin, lowCos).ScaledXY(radius).Unit().XY() normalLowSin, normalLowCos := pixel.V(lowSin, lowCos).ScaledXY(radius).Unit().XY()
normalLow := pixel.V(normalLowCos, normalLowSin).Angle() normalLow := pixel.V(normalLowCos, normalLowSin).Angle()
highSin, highCos := math.Sincos(high) highSin, highCos := math.Sincos(high)
highCenter := pt.pos.Add(pixel.V( highCenter := pt.pos + pixel.V(
radius.X*highCos, radius.X()*highCos,
radius.Y*highSin, radius.Y()*highSin,
)) )
normalHighSin, normalHighCos := pixel.V(highSin, highCos).ScaledXY(radius).Unit().XY() normalHighSin, normalHighCos := pixel.V(highSin, highCos).ScaledXY(radius).Unit().XY()
normalHigh := pixel.V(normalHighCos, normalHighSin).Angle() normalHigh := pixel.V(normalHighCos, normalHighSin).Angle()
@ -496,33 +478,30 @@ func (imd *IMDraw) outlineEllipseArc(radius pixel.Vec, low, high, thickness floa
case NoEndShape: case NoEndShape:
// nothing // nothing
case SharpEndShape: case SharpEndShape:
thick := pixel.V(thickness/2, 0).Rotated(normalLow) thick := pixel.X(thickness / 2).Rotated(normalLow)
imd.pushPt(lowCenter.Add(thick), pt) imd.pushPt(lowCenter+thick, pt)
imd.pushPt(lowCenter.Sub(thick), pt) imd.pushPt(lowCenter-thick, pt)
imd.pushPt(lowCenter.Sub(thick.Normal().Scaled(orientation)), pt) imd.pushPt(lowCenter-thick.Rotated(math.Pi/2*orientation), pt)
imd.fillPolygon() imd.fillPolygon()
thick = pixel.V(thickness/2, 0).Rotated(normalHigh) thick = pixel.X(thickness / 2).Rotated(normalHigh)
imd.pushPt(highCenter.Add(thick), pt) imd.pushPt(highCenter+thick, pt)
imd.pushPt(highCenter.Sub(thick), pt) imd.pushPt(highCenter-thick, pt)
imd.pushPt(highCenter.Add(thick.Normal().Scaled(orientation)), pt) imd.pushPt(highCenter+thick.Rotated(math.Pi/2*orientation), pt)
imd.fillPolygon() imd.fillPolygon()
case RoundEndShape: case RoundEndShape:
imd.pushPt(lowCenter, pt) imd.pushPt(lowCenter, pt)
imd.fillEllipseArc(pixel.V(thickness/2, thickness/2), normalLow, normalLow-math.Pi*orientation) imd.fillEllipseArc(pixel.V(thickness, thickness)/2, normalLow, normalLow-math.Pi*orientation)
imd.pushPt(highCenter, pt) imd.pushPt(highCenter, pt)
imd.fillEllipseArc(pixel.V(thickness/2, thickness/2), normalHigh, normalHigh+math.Pi*orientation) imd.fillEllipseArc(pixel.V(thickness, thickness)/2, normalHigh, normalHigh+math.Pi*orientation)
} }
} }
} }
imd.restorePoints(points)
} }
func (imd *IMDraw) polyline(thickness float64, closed bool) { func (imd *IMDraw) polyline(thickness float64, closed bool) {
points := imd.getAndClearPoints() points := imd.getAndClearPoints()
if len(points) == 0 { if len(points) == 0 {
imd.restorePoints(points)
return return
} }
if len(points) == 1 { if len(points) == 1 {
@ -532,25 +511,25 @@ func (imd *IMDraw) polyline(thickness float64, closed bool) {
// first point // first point
j, i := 0, 1 j, i := 0, 1
ijNormal := points[0].pos.To(points[1].pos).Normal().Unit().Scaled(thickness / 2) normal := (points[i].pos - points[j].pos).Rotated(math.Pi / 2).Unit().Scaled(thickness / 2)
if !closed { if !closed {
switch points[j].endshape { switch points[j].endshape {
case NoEndShape: case NoEndShape:
// nothing // nothing
case SharpEndShape: case SharpEndShape:
imd.pushPt(points[j].pos.Add(ijNormal), points[j]) imd.pushPt(points[j].pos+normal, points[j])
imd.pushPt(points[j].pos.Sub(ijNormal), points[j]) imd.pushPt(points[j].pos-normal, points[j])
imd.pushPt(points[j].pos.Add(ijNormal.Normal()), points[j]) imd.pushPt(points[j].pos+normal.Rotated(math.Pi/2), points[j])
imd.fillPolygon() imd.fillPolygon()
case RoundEndShape: case RoundEndShape:
imd.pushPt(points[j].pos, points[j]) imd.pushPt(points[j].pos, points[j])
imd.fillEllipseArc(pixel.V(thickness/2, thickness/2), ijNormal.Angle(), ijNormal.Angle()+math.Pi) imd.fillEllipseArc(pixel.V(thickness, thickness)/2, normal.Angle(), normal.Angle()+math.Pi)
} }
} }
imd.pushPt(points[j].pos.Add(ijNormal), points[j]) imd.pushPt(points[j].pos+normal, points[j])
imd.pushPt(points[j].pos.Sub(ijNormal), points[j]) imd.pushPt(points[j].pos-normal, points[j])
// middle points // middle points
for i := 0; i < len(points); i++ { for i := 0; i < len(points); i++ {
@ -568,15 +547,16 @@ func (imd *IMDraw) polyline(thickness float64, closed bool) {
k %= len(points) k %= len(points)
} }
jkNormal := points[j].pos.To(points[k].pos).Normal().Unit().Scaled(thickness / 2) ijNormal := (points[j].pos - points[i].pos).Rotated(math.Pi / 2).Unit().Scaled(thickness / 2)
jkNormal := (points[k].pos - points[j].pos).Rotated(math.Pi / 2).Unit().Scaled(thickness / 2)
orientation := 1.0 orientation := 1.0
if ijNormal.Cross(jkNormal) > 0 { if ijNormal.Cross(jkNormal) > 0 {
orientation = -1.0 orientation = -1.0
} }
imd.pushPt(points[j].pos.Sub(ijNormal), points[j]) imd.pushPt(points[j].pos-ijNormal, points[j])
imd.pushPt(points[j].pos.Add(ijNormal), points[j]) imd.pushPt(points[j].pos+ijNormal, points[j])
imd.fillPolygon() imd.fillPolygon()
switch points[j].endshape { switch points[j].endshape {
@ -584,30 +564,28 @@ func (imd *IMDraw) polyline(thickness float64, closed bool) {
// nothing // nothing
case SharpEndShape: case SharpEndShape:
imd.pushPt(points[j].pos, points[j]) imd.pushPt(points[j].pos, points[j])
imd.pushPt(points[j].pos.Add(ijNormal.Scaled(orientation)), points[j]) imd.pushPt(points[j].pos+ijNormal.Scaled(orientation), points[j])
imd.pushPt(points[j].pos.Add(jkNormal.Scaled(orientation)), points[j]) imd.pushPt(points[j].pos+jkNormal.Scaled(orientation), points[j])
imd.fillPolygon() imd.fillPolygon()
case RoundEndShape: case RoundEndShape:
imd.pushPt(points[j].pos, points[j]) imd.pushPt(points[j].pos, points[j])
imd.fillEllipseArc(pixel.V(thickness/2, thickness/2), ijNormal.Angle(), ijNormal.Angle()-math.Pi) imd.fillEllipseArc(pixel.V(thickness, thickness)/2, ijNormal.Angle(), ijNormal.Angle()-math.Pi)
imd.pushPt(points[j].pos, points[j]) imd.pushPt(points[j].pos, points[j])
imd.fillEllipseArc(pixel.V(thickness/2, thickness/2), jkNormal.Angle(), jkNormal.Angle()+math.Pi) imd.fillEllipseArc(pixel.V(thickness, thickness)/2, jkNormal.Angle(), jkNormal.Angle()+math.Pi)
} }
if !closing { if !closing {
imd.pushPt(points[j].pos.Add(jkNormal), points[j]) imd.pushPt(points[j].pos+jkNormal, points[j])
imd.pushPt(points[j].pos.Sub(jkNormal), points[j]) imd.pushPt(points[j].pos-jkNormal, points[j])
} }
// "next" normal becomes previous normal
ijNormal = jkNormal
} }
// last point // last point
i, j = len(points)-2, len(points)-1 i, j = len(points)-2, len(points)-1
ijNormal = points[i].pos.To(points[j].pos).Normal().Unit().Scaled(thickness / 2) normal = (points[j].pos - points[i].pos).Rotated(math.Pi / 2).Unit().Scaled(thickness / 2)
imd.pushPt(points[j].pos.Sub(ijNormal), points[j]) imd.pushPt(points[j].pos-normal, points[j])
imd.pushPt(points[j].pos.Add(ijNormal), points[j]) imd.pushPt(points[j].pos+normal, points[j])
imd.fillPolygon() imd.fillPolygon()
if !closed { if !closed {
@ -615,15 +593,13 @@ func (imd *IMDraw) polyline(thickness float64, closed bool) {
case NoEndShape: case NoEndShape:
// nothing // nothing
case SharpEndShape: case SharpEndShape:
imd.pushPt(points[j].pos.Add(ijNormal), points[j]) imd.pushPt(points[j].pos+normal, points[j])
imd.pushPt(points[j].pos.Sub(ijNormal), points[j]) imd.pushPt(points[j].pos-normal, points[j])
imd.pushPt(points[j].pos.Add(ijNormal.Normal().Scaled(-1)), points[j]) imd.pushPt(points[j].pos+normal.Rotated(-math.Pi/2), points[j])
imd.fillPolygon() imd.fillPolygon()
case RoundEndShape: case RoundEndShape:
imd.pushPt(points[j].pos, points[j]) imd.pushPt(points[j].pos, points[j])
imd.fillEllipseArc(pixel.V(thickness/2, thickness/2), ijNormal.Angle(), ijNormal.Angle()-math.Pi) imd.fillEllipseArc(pixel.V(thickness, thickness)/2, normal.Angle(), normal.Angle()-math.Pi)
} }
} }
imd.restorePoints(points)
} }

View File

@ -1,96 +0,0 @@
package imdraw_test
import (
"fmt"
"math/rand"
"testing"
"github.com/faiface/pixel"
"github.com/faiface/pixel/imdraw"
)
func BenchmarkPush(b *testing.B) {
imd := imdraw.New(nil)
for i := 0; i < b.N; i++ {
imd.Push(pixel.V(123.1, 99.4))
}
}
func pointLists(counts ...int) [][]pixel.Vec {
lists := make([][]pixel.Vec, len(counts))
for i := range lists {
lists[i] = make([]pixel.Vec, counts[i])
for j := range lists[i] {
lists[i][j] = pixel.V(
rand.Float64()*5000-2500,
rand.Float64()*5000-2500,
)
}
}
return lists
}
func BenchmarkLine(b *testing.B) {
lists := pointLists(2, 5, 10, 100, 1000)
for _, pts := range lists {
b.Run(fmt.Sprintf("%d", len(pts)), func(b *testing.B) {
imd := imdraw.New(nil)
for i := 0; i < b.N; i++ {
imd.Push(pts...)
imd.Line(1)
}
})
}
}
func BenchmarkRectangle(b *testing.B) {
lists := pointLists(2, 10, 100, 1000)
for _, pts := range lists {
b.Run(fmt.Sprintf("%d", len(pts)), func(b *testing.B) {
imd := imdraw.New(nil)
for i := 0; i < b.N; i++ {
imd.Push(pts...)
imd.Rectangle(0)
}
})
}
}
func BenchmarkPolygon(b *testing.B) {
lists := pointLists(3, 10, 100, 1000)
for _, pts := range lists {
b.Run(fmt.Sprintf("%d", len(pts)), func(b *testing.B) {
imd := imdraw.New(nil)
for i := 0; i < b.N; i++ {
imd.Push(pts...)
imd.Polygon(0)
}
})
}
}
func BenchmarkEllipseFill(b *testing.B) {
lists := pointLists(1, 10, 100, 1000)
for _, pts := range lists {
b.Run(fmt.Sprintf("%d", len(pts)), func(b *testing.B) {
imd := imdraw.New(nil)
for i := 0; i < b.N; i++ {
imd.Push(pts...)
imd.Ellipse(pixel.V(50, 100), 0)
}
})
}
}
func BenchmarkEllipseOutline(b *testing.B) {
lists := pointLists(1, 10, 100, 1000)
for _, pts := range lists {
b.Run(fmt.Sprintf("%d", len(pts)), func(b *testing.B) {
imd := imdraw.New(nil)
for i := 0; i < b.N; i++ {
imd.Push(pts...)
imd.Ellipse(pixel.V(50, 100), 1)
}
})
}
}

View File

@ -92,7 +92,7 @@ type TrianglesColor interface {
Color(i int) RGBA Color(i int) RGBA
} }
// TrianglesPicture specifies Triangles with Picture property. // TrianglesPicture specifies Triangles with Picture propery.
// //
// The first value returned from Picture method is Picture coordinates. The second one specifies the // The first value returned from Picture method is Picture coordinates. The second one specifies the
// weight of the Picture. Value of 0 means, that Picture should be completely ignored, 1 means that // weight of the Picture. Value of 0 means, that Picture should be completely ignored, 1 means that
@ -102,19 +102,10 @@ type TrianglesPicture interface {
Picture(i int) (pic Vec, intensity float64) Picture(i int) (pic Vec, intensity float64)
} }
// TrianglesClipped specifies Triangles with Clipping Rectangle property.
//
// The first value returned from ClipRect method is the clipping rectangle. The second one specifies
// if the triangle is clipped.
type TrianglesClipped interface {
Triangles
ClipRect(i int) (rect Rect, is bool)
}
// Picture represents a rectangular area of raster data, such as a color. It has Bounds which // Picture represents a rectangular area of raster data, such as a color. It has Bounds which
// specify the rectangle where data is located. // specify the rectangle where data is located.
type Picture interface { type Picture interface {
// Bounds returns the rectangle of the Picture. All data is located within this rectangle. // Bounds returns the rectangle of the Picture. All data is located witih this rectangle.
// Querying properties outside the rectangle should return default value of that property. // Querying properties outside the rectangle should return default value of that property.
Bounds() Rect Bounds() Rect
} }

View File

@ -1,699 +0,0 @@
package pixel_test
import (
"math"
"reflect"
"testing"
"github.com/faiface/pixel"
)
func TestLine_Bounds(t *testing.T) {
type fields struct {
A pixel.Vec
B pixel.Vec
}
tests := []struct {
name string
fields fields
want pixel.Rect
}{
{
name: "Positive slope",
fields: fields{A: pixel.V(0, 0), B: pixel.V(10, 10)},
want: pixel.R(0, 0, 10, 10),
},
{
name: "Negative slope",
fields: fields{A: pixel.V(10, 10), B: pixel.V(0, 0)},
want: pixel.R(0, 0, 10, 10),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
l := pixel.Line{
A: tt.fields.A,
B: tt.fields.B,
}
if got := l.Bounds(); !reflect.DeepEqual(got, tt.want) {
t.Errorf("Line.Bounds() = %v, want %v", got, tt.want)
}
})
}
}
func TestLine_Center(t *testing.T) {
type fields struct {
A pixel.Vec
B pixel.Vec
}
tests := []struct {
name string
fields fields
want pixel.Vec
}{
{
name: "Positive slope",
fields: fields{A: pixel.V(0, 0), B: pixel.V(10, 10)},
want: pixel.V(5, 5),
},
{
name: "Negative slope",
fields: fields{A: pixel.V(10, 10), B: pixel.V(0, 0)},
want: pixel.V(5, 5),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
l := pixel.Line{
A: tt.fields.A,
B: tt.fields.B,
}
if got := l.Center(); !reflect.DeepEqual(got, tt.want) {
t.Errorf("Line.Center() = %v, want %v", got, tt.want)
}
})
}
}
func TestLine_Closest(t *testing.T) {
type fields struct {
A pixel.Vec
B pixel.Vec
}
type args struct {
v pixel.Vec
}
tests := []struct {
name string
fields fields
args args
want pixel.Vec
}{
{
name: "Point on line",
fields: fields{A: pixel.V(0, 0), B: pixel.V(10, 10)},
args: args{v: pixel.V(5, 5)},
want: pixel.V(5, 5),
},
{
name: "Point on next to line",
fields: fields{A: pixel.V(0, 0), B: pixel.V(10, 10)},
args: args{v: pixel.V(0, 10)},
want: pixel.V(5, 5),
},
{
name: "Point on next to vertical line",
fields: fields{A: pixel.V(5, 0), B: pixel.V(5, 10)},
args: args{v: pixel.V(6, 5)},
want: pixel.V(5, 5),
},
{
name: "Point on next to horizontal line",
fields: fields{A: pixel.V(0, 5), B: pixel.V(10, 5)},
args: args{v: pixel.V(5, 6)},
want: pixel.V(5, 5),
},
{
name: "Point far from line",
fields: fields{A: pixel.V(0, 0), B: pixel.V(10, 10)},
args: args{v: pixel.V(80, -70)},
want: pixel.V(5, 5),
},
{
name: "Point on inline with line",
fields: fields{A: pixel.V(0, 0), B: pixel.V(10, 10)},
args: args{v: pixel.V(20, 20)},
want: pixel.V(10, 10),
},
{
name: "Vertical line",
fields: fields{A: pixel.V(0, -10), B: pixel.V(0, 10)},
args: args{v: pixel.V(-1, 0)},
want: pixel.V(0, 0),
},
{
name: "Horizontal line",
fields: fields{A: pixel.V(-10, 0), B: pixel.V(10, 0)},
args: args{v: pixel.V(0, -1)},
want: pixel.V(0, 0),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
l := pixel.Line{
A: tt.fields.A,
B: tt.fields.B,
}
if got := l.Closest(tt.args.v); !reflect.DeepEqual(got, tt.want) {
t.Errorf("Line.Closest() = %v, want %v", got, tt.want)
}
})
}
}
func TestLine_Contains(t *testing.T) {
type fields struct {
A pixel.Vec
B pixel.Vec
}
type args struct {
v pixel.Vec
}
tests := []struct {
name string
fields fields
args args
want bool
}{
{
name: "Point on line",
fields: fields{A: pixel.V(0, 0), B: pixel.V(10, 10)},
args: args{v: pixel.V(5, 5)},
want: true,
},
{
name: "Point on negative sloped line",
fields: fields{A: pixel.V(0, 10), B: pixel.V(10, 0)},
args: args{v: pixel.V(5, 5)},
want: true,
},
{
name: "Point not on line",
fields: fields{A: pixel.V(0, 0), B: pixel.V(10, 10)},
args: args{v: pixel.V(0, 10)},
want: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
l := pixel.Line{
A: tt.fields.A,
B: tt.fields.B,
}
if got := l.Contains(tt.args.v); got != tt.want {
t.Errorf("Line.Contains() = %v, want %v", got, tt.want)
}
})
}
}
func TestLine_Formula(t *testing.T) {
type fields struct {
A pixel.Vec
B pixel.Vec
}
tests := []struct {
name string
fields fields
wantM float64
wantB float64
}{
{
name: "Getting formula - 45 degs",
fields: fields{A: pixel.V(0, 0), B: pixel.V(10, 10)},
wantM: 1,
wantB: 0,
},
{
name: "Getting formula - 90 degs",
fields: fields{A: pixel.V(0, 0), B: pixel.V(0, 10)},
wantM: math.Inf(1),
wantB: math.NaN(),
},
{
name: "Getting formula - 0 degs",
fields: fields{A: pixel.V(0, 0), B: pixel.V(10, 0)},
wantM: 0,
wantB: 0,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
l := pixel.Line{
A: tt.fields.A,
B: tt.fields.B,
}
gotM, gotB := l.Formula()
if gotM != tt.wantM {
t.Errorf("Line.Formula() gotM = %v, want %v", gotM, tt.wantM)
}
if gotB != tt.wantB {
if math.IsNaN(tt.wantB) && !math.IsNaN(gotB) {
t.Errorf("Line.Formula() gotB = %v, want %v", gotB, tt.wantB)
}
}
})
}
}
func TestLine_Intersect(t *testing.T) {
type fields struct {
A pixel.Vec
B pixel.Vec
}
type args struct {
k pixel.Line
}
tests := []struct {
name string
fields fields
args args
want pixel.Vec
want1 bool
}{
{
name: "Lines intersect",
fields: fields{A: pixel.V(0, 0), B: pixel.V(10, 10)},
args: args{k: pixel.L(pixel.V(0, 10), pixel.V(10, 0))},
want: pixel.V(5, 5),
want1: true,
},
{
name: "Lines intersect 2",
fields: fields{A: pixel.V(5, 1), B: pixel.V(1, 1)},
args: args{k: pixel.L(pixel.V(2, 0), pixel.V(2, 3))},
want: pixel.V(2, 1),
want1: true,
},
{
name: "Line intersect with vertical",
fields: fields{A: pixel.V(5, 0), B: pixel.V(5, 10)},
args: args{k: pixel.L(pixel.V(0, 0), pixel.V(10, 10))},
want: pixel.V(5, 5),
want1: true,
},
{
name: "Line intersect with horizontal",
fields: fields{A: pixel.V(0, 5), B: pixel.V(10, 5)},
args: args{k: pixel.L(pixel.V(0, 0), pixel.V(10, 10))},
want: pixel.V(5, 5),
want1: true,
},
{
name: "Lines don't intersect",
fields: fields{A: pixel.V(0, 0), B: pixel.V(10, 10)},
args: args{k: pixel.L(pixel.V(0, 10), pixel.V(1, 20))},
want: pixel.ZV,
want1: false,
},
{
name: "Lines don't intersect 2",
fields: fields{A: pixel.V(1, 1), B: pixel.V(1, 5)},
args: args{k: pixel.L(pixel.V(-5, 0), pixel.V(-2, 2))},
want: pixel.ZV,
want1: false,
},
{
name: "Lines don't intersect 3",
fields: fields{A: pixel.V(2, 0), B: pixel.V(2, 3)},
args: args{k: pixel.L(pixel.V(1, 5), pixel.V(5, 5))},
want: pixel.ZV,
want1: false,
},
{
name: "Lines parallel",
fields: fields{A: pixel.V(0, 0), B: pixel.V(10, 10)},
args: args{k: pixel.L(pixel.V(0, 1), pixel.V(10, 11))},
want: pixel.ZV,
want1: false,
}, {
name: "Lines intersect",
fields: fields{A: pixel.V(600, 600), B: pixel.V(925, 150)},
args: args{k: pixel.L(pixel.V(740, 255), pixel.V(925, 255))},
want: pixel.V(849.1666666666666, 255),
want1: true,
},
{
name: "Lines intersect",
fields: fields{A: pixel.V(600, 600), B: pixel.V(925, 150)},
args: args{k: pixel.L(pixel.V(740, 255), pixel.V(925, 255.0001))},
want: pixel.V(849.1666240490657, 255.000059008986),
want1: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
l := pixel.Line{
A: tt.fields.A,
B: tt.fields.B,
}
got, got1 := l.Intersect(tt.args.k)
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("Line.Intersect() got = %v, want %v", got, tt.want)
}
if got1 != tt.want1 {
t.Errorf("Line.Intersect() got1 = %v, want %v", got1, tt.want1)
}
})
}
}
func TestLine_IntersectCircle(t *testing.T) {
type fields struct {
A pixel.Vec
B pixel.Vec
}
type args struct {
c pixel.Circle
}
tests := []struct {
name string
fields fields
args args
want pixel.Vec
}{
{
name: "Cirle intersects",
fields: fields{A: pixel.V(0, 0), B: pixel.V(10, 10)},
args: args{c: pixel.C(pixel.V(6, 4), 2)},
want: pixel.V(0.5857864376269049, -0.5857864376269049),
},
{
name: "Cirle doesn't intersects",
fields: fields{A: pixel.V(0, 0), B: pixel.V(10, 10)},
args: args{c: pixel.C(pixel.V(0, 5), 1)},
want: pixel.ZV,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
l := pixel.Line{
A: tt.fields.A,
B: tt.fields.B,
}
if got := l.IntersectCircle(tt.args.c); !reflect.DeepEqual(got, tt.want) {
t.Errorf("Line.IntersectCircle() = %v, want %v", got, tt.want)
}
})
}
}
func TestLine_IntersectRect(t *testing.T) {
type fields struct {
A pixel.Vec
B pixel.Vec
}
type args struct {
r pixel.Rect
}
tests := []struct {
name string
fields fields
args args
want pixel.Vec
}{
{
name: "Line through rect vertically",
fields: fields{A: pixel.V(0, 0), B: pixel.V(0, 10)},
args: args{r: pixel.R(-1, 1, 5, 5)},
want: pixel.V(-1, 0),
},
{
name: "Line through rect horizontally",
fields: fields{A: pixel.V(0, 1), B: pixel.V(10, 1)},
args: args{r: pixel.R(1, 0, 5, 5)},
want: pixel.V(0, -1),
},
{
name: "Line through rect diagonally bottom and left edges",
fields: fields{A: pixel.V(0, 0), B: pixel.V(10, 10)},
args: args{r: pixel.R(0, 2, 3, 3)},
want: pixel.V(-1, 1),
},
{
name: "Line through rect diagonally top and right edges",
fields: fields{A: pixel.V(10, 0), B: pixel.V(0, 10)},
args: args{r: pixel.R(5, 0, 8, 3)},
want: pixel.V(-2.5, -2.5),
},
{
name: "Line with not rect intersect",
fields: fields{A: pixel.V(0, 0), B: pixel.V(10, 10)},
args: args{r: pixel.R(20, 20, 21, 21)},
want: pixel.ZV,
},
{
name: "Line intersects at 0,0",
fields: fields{A: pixel.V(0, -10), B: pixel.V(0, 10)},
args: args{r: pixel.R(-1, 0, 2, 2)},
want: pixel.V(-1, 0),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
l := pixel.Line{
A: tt.fields.A,
B: tt.fields.B,
}
if got := l.IntersectRect(tt.args.r); !reflect.DeepEqual(got, tt.want) {
t.Errorf("Line.IntersectRect() = %v, want %v", got, tt.want)
}
})
}
}
func TestLine_Len(t *testing.T) {
type fields struct {
A pixel.Vec
B pixel.Vec
}
tests := []struct {
name string
fields fields
want float64
}{
{
name: "End right-up of start",
fields: fields{A: pixel.V(0, 0), B: pixel.V(3, 4)},
want: 5,
},
{
name: "End left-up of start",
fields: fields{A: pixel.V(0, 0), B: pixel.V(-3, 4)},
want: 5,
},
{
name: "End right-down of start",
fields: fields{A: pixel.V(0, 0), B: pixel.V(3, -4)},
want: 5,
},
{
name: "End left-down of start",
fields: fields{A: pixel.V(0, 0), B: pixel.V(-3, -4)},
want: 5,
},
{
name: "End same as start",
fields: fields{A: pixel.V(0, 0), B: pixel.V(0, 0)},
want: 0,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
l := pixel.Line{
A: tt.fields.A,
B: tt.fields.B,
}
if got := l.Len(); got != tt.want {
t.Errorf("Line.Len() = %v, want %v", got, tt.want)
}
})
}
}
func TestLine_Rotated(t *testing.T) {
// round returns the nearest integer, rounding ties away from zero.
// This is required because `math.Round` wasn't introduced until Go1.10
round := func(x float64) float64 {
t := math.Trunc(x)
if math.Abs(x-t) >= 0.5 {
return t + math.Copysign(1, x)
}
return t
}
type fields struct {
A pixel.Vec
B pixel.Vec
}
type args struct {
around pixel.Vec
angle float64
}
tests := []struct {
name string
fields fields
args args
want pixel.Line
}{
{
name: "Rotating around line center",
fields: fields{A: pixel.V(1, 1), B: pixel.V(3, 3)},
args: args{around: pixel.V(2, 2), angle: math.Pi},
want: pixel.L(pixel.V(3, 3), pixel.V(1, 1)),
},
{
name: "Rotating around x-y origin",
fields: fields{A: pixel.V(1, 1), B: pixel.V(3, 3)},
args: args{around: pixel.V(0, 0), angle: math.Pi},
want: pixel.L(pixel.V(-1, -1), pixel.V(-3, -3)),
},
{
name: "Rotating around line end",
fields: fields{A: pixel.V(1, 1), B: pixel.V(3, 3)},
args: args{around: pixel.V(1, 1), angle: math.Pi},
want: pixel.L(pixel.V(1, 1), pixel.V(-1, -1)),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
l := pixel.Line{
A: tt.fields.A,
B: tt.fields.B,
}
// Have to round the results, due to floating-point in accuracies. Results are correct to approximately
// 10 decimal places.
got := l.Rotated(tt.args.around, tt.args.angle)
if round(got.A.X) != tt.want.A.X ||
round(got.B.X) != tt.want.B.X ||
round(got.A.Y) != tt.want.A.Y ||
round(got.B.Y) != tt.want.B.Y {
t.Errorf("Line.Rotated() = %v, want %v", got, tt.want)
}
})
}
}
func TestLine_Scaled(t *testing.T) {
type fields struct {
A pixel.Vec
B pixel.Vec
}
type args struct {
scale float64
}
tests := []struct {
name string
fields fields
args args
want pixel.Line
}{
{
name: "Scaling by 1",
fields: fields{A: pixel.V(0, 0), B: pixel.V(10, 10)},
args: args{scale: 1},
want: pixel.L(pixel.V(0, 0), pixel.V(10, 10)),
},
{
name: "Scaling by >1",
fields: fields{A: pixel.V(0, 0), B: pixel.V(10, 10)},
args: args{scale: 2},
want: pixel.L(pixel.V(-5, -5), pixel.V(15, 15)),
},
{
name: "Scaling by <1",
fields: fields{A: pixel.V(0, 0), B: pixel.V(10, 10)},
args: args{scale: 0.5},
want: pixel.L(pixel.V(2.5, 2.5), pixel.V(7.5, 7.5)),
},
{
name: "Scaling by -1",
fields: fields{A: pixel.V(0, 0), B: pixel.V(10, 10)},
args: args{scale: -1},
want: pixel.L(pixel.V(10, 10), pixel.V(0, 0)),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
l := pixel.Line{
A: tt.fields.A,
B: tt.fields.B,
}
if got := l.Scaled(tt.args.scale); !reflect.DeepEqual(got, tt.want) {
t.Errorf("Line.Scaled() = %v, want %v", got, tt.want)
}
})
}
}
func TestLine_ScaledXY(t *testing.T) {
type fields struct {
A pixel.Vec
B pixel.Vec
}
type args struct {
around pixel.Vec
scale float64
}
tests := []struct {
name string
fields fields
args args
want pixel.Line
}{
{
name: "Scaling by 1 around origin",
fields: fields{A: pixel.V(0, 0), B: pixel.V(10, 10)},
args: args{around: pixel.ZV, scale: 1},
want: pixel.L(pixel.V(0, 0), pixel.V(10, 10)),
},
{
name: "Scaling by >1 around origin",
fields: fields{A: pixel.V(0, 0), B: pixel.V(10, 10)},
args: args{around: pixel.ZV, scale: 2},
want: pixel.L(pixel.V(0, 0), pixel.V(20, 20)),
},
{
name: "Scaling by <1 around origin",
fields: fields{A: pixel.V(0, 0), B: pixel.V(10, 10)},
args: args{around: pixel.ZV, scale: 0.5},
want: pixel.L(pixel.V(0, 0), pixel.V(5, 5)),
},
{
name: "Scaling by -1 around origin",
fields: fields{A: pixel.V(0, 0), B: pixel.V(10, 10)},
args: args{around: pixel.ZV, scale: -1},
want: pixel.L(pixel.V(0, 0), pixel.V(-10, -10)),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
l := pixel.Line{
A: tt.fields.A,
B: tt.fields.B,
}
if got := l.ScaledXY(tt.args.around, tt.args.scale); !reflect.DeepEqual(got, tt.want) {
t.Errorf("Line.ScaledXY() = %v, want %v", got, tt.want)
}
})
}
}
func TestLine_String(t *testing.T) {
type fields struct {
A pixel.Vec
B pixel.Vec
}
tests := []struct {
name string
fields fields
want string
}{
{
name: "Getting string",
fields: fields{A: pixel.V(0, 0), B: pixel.V(1, 1)},
want: "Line(Vec(0, 0), Vec(1, 1))",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
l := pixel.Line{
A: tt.fields.A,
B: tt.fields.B,
}
if got := l.String(); got != tt.want {
t.Errorf("Line.String() = %v, want %v", got, tt.want)
}
})
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

15
math.go
View File

@ -1,15 +0,0 @@
package pixel
// Clamp returns x clamped to the interval [min, max].
//
// If x is less than min, min is returned. If x is more than max, max is returned. Otherwise, x is
// returned.
func Clamp(x, min, max float64) float64 {
if x < min {
return min
}
if x > max {
return max
}
return x
}

View File

@ -1,46 +0,0 @@
package pixel_test
import (
"fmt"
"math"
"testing"
"github.com/faiface/pixel"
)
// closeEnough will shift the decimal point by the accuracy required, truncates the results and compares them.
// Effectively this compares two floats to a given decimal point.
// Example:
// closeEnough(100.125342432, 100.125, 2) == true
// closeEnough(math.Pi, 3.14, 2) == true
// closeEnough(0.1234, 0.1245, 3) == false
func closeEnough(got, expected float64, decimalAccuracy int) bool {
gotShifted := got * math.Pow10(decimalAccuracy)
expectedShifted := expected * math.Pow10(decimalAccuracy)
return math.Trunc(gotShifted) == math.Trunc(expectedShifted)
}
type clampTest struct {
number float64
min float64
max float64
expected float64
}
func TestClamp(t *testing.T) {
tests := []clampTest{
{number: 1, min: 0, max: 5, expected: 1},
{number: 2, min: 0, max: 5, expected: 2},
{number: 8, min: 0, max: 5, expected: 5},
{number: -5, min: 0, max: 5, expected: 0},
{number: -5, min: -4, max: 5, expected: -4},
}
for _, tc := range tests {
result := pixel.Clamp(tc.number, tc.min, tc.max)
if result != tc.expected {
t.Error(fmt.Sprintf("Clamping %v with min %v and max %v should have given %v, but gave %v", tc.number, tc.min, tc.max, tc.expected, result))
}
}
}

View File

@ -1,98 +0,0 @@
package pixel
import (
"fmt"
"math"
)
// Matrix is a 2x3 affine matrix that can be used for all kinds of spatial transforms, such
// as movement, scaling and rotations.
//
// Matrix has a handful of useful methods, each of which adds a transformation to the matrix. For
// example:
//
// pixel.IM.Moved(pixel.V(100, 200)).Rotated(pixel.ZV, math.Pi/2)
//
// This code creates a Matrix that first moves everything by 100 units horizontally and 200 units
// vertically and then rotates everything by 90 degrees around the origin.
//
// Layout is:
// [0] [2] [4]
// [1] [3] [5]
// 0 0 1 (implicit row)
type Matrix [6]float64
// IM stands for identity matrix. Does nothing, no transformation.
var IM = Matrix{1, 0, 0, 1, 0, 0}
// String returns a string representation of the Matrix.
//
// m := pixel.IM
// fmt.Println(m) // Matrix(1 0 0 | 0 1 0)
func (m Matrix) String() string {
return fmt.Sprintf(
"Matrix(%v %v %v | %v %v %v)",
m[0], m[2], m[4],
m[1], m[3], m[5],
)
}
// Moved moves everything by the delta vector.
func (m Matrix) Moved(delta Vec) Matrix {
m[4], m[5] = m[4]+delta.X, m[5]+delta.Y
return m
}
// ScaledXY scales everything around a given point by the scale factor in each axis respectively.
func (m Matrix) ScaledXY(around Vec, scale Vec) Matrix {
m[4], m[5] = m[4]-around.X, m[5]-around.Y
m[0], m[2], m[4] = m[0]*scale.X, m[2]*scale.X, m[4]*scale.X
m[1], m[3], m[5] = m[1]*scale.Y, m[3]*scale.Y, m[5]*scale.Y
m[4], m[5] = m[4]+around.X, m[5]+around.Y
return m
}
// Scaled scales everything around a given point by the scale factor.
func (m Matrix) Scaled(around Vec, scale float64) Matrix {
return m.ScaledXY(around, V(scale, scale))
}
// Rotated rotates everything around a given point by the given angle in radians.
func (m Matrix) Rotated(around Vec, angle float64) Matrix {
sint, cost := math.Sincos(angle)
m[4], m[5] = m[4]-around.X, m[5]-around.Y
m = m.Chained(Matrix{cost, sint, -sint, cost, 0, 0})
m[4], m[5] = m[4]+around.X, m[5]+around.Y
return m
}
// Chained adds another Matrix to this one. All tranformations by the next Matrix will be applied
// after the transformations of this Matrix.
func (m Matrix) Chained(next Matrix) Matrix {
return Matrix{
next[0]*m[0] + next[2]*m[1],
next[1]*m[0] + next[3]*m[1],
next[0]*m[2] + next[2]*m[3],
next[1]*m[2] + next[3]*m[3],
next[0]*m[4] + next[2]*m[5] + next[4],
next[1]*m[4] + next[3]*m[5] + next[5],
}
}
// Project applies all transformations added to the Matrix to a vector u and returns the result.
//
// Time complexity is O(1).
func (m Matrix) Project(u Vec) Vec {
return Vec{m[0]*u.X + m[2]*u.Y + m[4], m[1]*u.X + m[3]*u.Y + m[5]}
}
// Unproject does the inverse operation to Project.
//
// Time complexity is O(1).
func (m Matrix) Unproject(u Vec) Vec {
det := m[0]*m[3] - m[2]*m[1]
return Vec{
(m[3]*(u.X-m[4]) - m[2]*(u.Y-m[5])) / det,
(-m[1]*(u.X-m[4]) + m[0]*(u.Y-m[5])) / det,
}
}

View File

@ -1,149 +0,0 @@
package pixel_test
import (
"fmt"
"math"
"math/rand"
"testing"
"github.com/faiface/pixel"
"github.com/stretchr/testify/assert"
)
func BenchmarkMatrix(b *testing.B) {
b.Run("Moved", func(b *testing.B) {
m := pixel.IM
for i := 0; i < b.N; i++ {
m = m.Moved(pixel.V(4.217, -132.99))
}
})
b.Run("ScaledXY", func(b *testing.B) {
m := pixel.IM
for i := 0; i < b.N; i++ {
m = m.ScaledXY(pixel.V(-5.1, 9.3), pixel.V(2.1, 0.98))
}
})
b.Run("Rotated", func(b *testing.B) {
m := pixel.IM
for i := 0; i < b.N; i++ {
m = m.Rotated(pixel.V(-5.1, 9.3), 1.4)
}
})
b.Run("Chained", func(b *testing.B) {
var m1, m2 pixel.Matrix
for i := range m1 {
m1[i] = rand.Float64()
m2[i] = rand.Float64()
}
for i := 0; i < b.N; i++ {
m1 = m1.Chained(m2)
}
})
b.Run("Project", func(b *testing.B) {
var m pixel.Matrix
for i := range m {
m[i] = rand.Float64()
}
u := pixel.V(1, 1)
for i := 0; i < b.N; i++ {
u = m.Project(u)
}
})
b.Run("Unproject", func(b *testing.B) {
again:
var m pixel.Matrix
for i := range m {
m[i] = rand.Float64()
}
if (m[0]*m[3])-(m[1]*m[2]) == 0 { // zero determinant, not invertible
goto again
}
u := pixel.V(1, 1)
for i := 0; i < b.N; i++ {
u = m.Unproject(u)
}
})
}
func TestMatrix_Unproject(t *testing.T) {
const delta = 1e-15
t.Run("for rotated matrix", func(t *testing.T) {
matrix := pixel.IM.
Rotated(pixel.ZV, math.Pi/2)
unprojected := matrix.Unproject(pixel.V(0, 1))
assert.InDelta(t, unprojected.X, 1, delta)
assert.InDelta(t, unprojected.Y, 0, delta)
})
t.Run("for moved matrix", func(t *testing.T) {
matrix := pixel.IM.
Moved(pixel.V(1, 2))
unprojected := matrix.Unproject(pixel.V(2, 5))
assert.InDelta(t, unprojected.X, 1, delta)
assert.InDelta(t, unprojected.Y, 3, delta)
})
t.Run("for scaled matrix", func(t *testing.T) {
matrix := pixel.IM.
Scaled(pixel.ZV, 2)
unprojected := matrix.Unproject(pixel.V(2, 4))
assert.InDelta(t, unprojected.X, 1, delta)
assert.InDelta(t, unprojected.Y, 2, delta)
})
t.Run("for scaled, rotated and moved matrix", func(t *testing.T) {
matrix := pixel.IM.
Scaled(pixel.ZV, 2).
Rotated(pixel.ZV, math.Pi/2).
Moved(pixel.V(2, 2))
unprojected := matrix.Unproject(pixel.V(-2, 6))
assert.InDelta(t, unprojected.X, 2, delta)
assert.InDelta(t, unprojected.Y, 2, delta)
})
t.Run("for rotated and moved matrix", func(t *testing.T) {
matrix := pixel.IM.
Rotated(pixel.ZV, math.Pi/2).
Moved(pixel.V(1, 1))
unprojected := matrix.Unproject(pixel.V(1, 2))
assert.InDelta(t, unprojected.X, 1, delta)
assert.InDelta(t, unprojected.Y, 0, delta)
})
t.Run("for projected vertices using all kinds of matrices", func(t *testing.T) {
namedMatrices := map[string]pixel.Matrix{
"IM": pixel.IM,
"Scaled": pixel.IM.Scaled(pixel.ZV, 0.5),
"Scaled x 2": pixel.IM.Scaled(pixel.ZV, 2),
"Rotated": pixel.IM.Rotated(pixel.ZV, math.Pi/4),
"Moved": pixel.IM.Moved(pixel.V(0.5, 1)),
"Moved 2": pixel.IM.Moved(pixel.V(-1, -0.5)),
"Scaled and Rotated": pixel.IM.Scaled(pixel.ZV, 0.5).Rotated(pixel.ZV, math.Pi/4),
"Scaled, Rotated and Moved": pixel.IM.Scaled(pixel.ZV, 0.5).Rotated(pixel.ZV, math.Pi/4).Moved(pixel.V(1, 2)),
"Rotated and Moved": pixel.IM.Rotated(pixel.ZV, math.Pi/4).Moved(pixel.V(1, 2)),
}
vertices := [...]pixel.Vec{
pixel.V(0, 0),
pixel.V(5, 0),
pixel.V(5, 10),
pixel.V(0, 10),
pixel.V(-5, 10),
pixel.V(-5, 0),
pixel.V(-5, -10),
pixel.V(0, -10),
pixel.V(5, -10),
}
for matrixName, matrix := range namedMatrices {
for _, vertex := range vertices {
testCase := fmt.Sprintf("for matrix %s and vertex %v", matrixName, vertex)
t.Run(testCase, func(t *testing.T) {
projected := matrix.Project(vertex)
unprojected := matrix.Unproject(projected)
assert.InDelta(t, vertex.X, unprojected.X, delta)
assert.InDelta(t, vertex.Y, unprojected.Y, delta)
})
}
}
})
t.Run("for singular matrix", func(t *testing.T) {
matrix := pixel.Matrix{0, 0, 0, 0, 0, 0}
unprojected := matrix.Unproject(pixel.ZV)
assert.True(t, math.IsNaN(unprojected.X))
assert.True(t, math.IsNaN(unprojected.Y))
})
}

View File

@ -1,68 +0,0 @@
package pixel_test
import (
"bytes"
"image"
"os"
"testing"
_ "image/png"
"github.com/faiface/pixel"
"github.com/faiface/pixel/pixelgl"
)
// onePixelImage is the byte representation of a 1x1 solid white png file
var onePixelImage = []byte{
137, 80, 78, 71, 13, 10, 26, 10, 0, 0, 0, 13, 73, 72, 68, 82, 0, 0, 0, 1, 0, 0, 0, 1, 8, 2,
0, 0, 0, 144, 119, 83, 222, 0, 0, 1, 130, 105, 67, 67, 80, 73, 67, 67, 32, 112, 114, 111, 102, 105, 108, 101, 0,
0, 40, 145, 125, 145, 59, 72, 3, 65, 20, 69, 143, 73, 68, 17, 37, 133, 41, 68, 44, 182, 80, 43, 5, 81, 17, 75,
141, 66, 16, 34, 132, 168, 96, 212, 194, 221, 141, 137, 66, 118, 13, 187, 9, 54, 150, 130, 109, 192, 194, 79,
227, 175, 176, 177, 214, 214, 194, 86, 16, 4, 63, 32, 54, 182, 86, 138, 54, 18, 214, 55, 73, 32, 65, 140, 3,
195, 28, 238, 188, 123, 121, 243, 6, 124, 71, 25, 211, 114, 3, 3, 96, 217, 57, 39, 30, 9, 107, 243, 137, 5, 173,
233, 149, 0, 65, 90, 104, 0, 221, 116, 179, 227, 177, 88, 148, 186, 235, 235, 94, 213, 193, 93, 191, 202, 170,
95, 247, 231, 106, 75, 174, 184, 38, 52, 104, 194, 99, 102, 214, 201, 9, 47, 11, 143, 108, 228, 178, 138, 247,
132, 67, 230, 170, 158, 20, 62, 23, 238, 115, 164, 65, 225, 71, 165, 27, 101, 126, 83, 156, 46, 177, 79, 101,
134, 156, 217, 248, 132, 112, 72, 88, 75, 215, 176, 81, 195, 230, 170, 99, 9, 15, 11, 119, 39, 45, 91, 242, 125,
243, 101, 78, 42, 222, 84, 108, 101, 242, 102, 165, 79, 245, 194, 214, 21, 123, 110, 70, 233, 178, 187, 136, 48,
197, 52, 49, 52, 12, 242, 172, 145, 33, 71, 191, 156, 182, 40, 46, 113, 185, 15, 215, 241, 119, 150, 252, 49,
113, 25, 226, 90, 195, 20, 199, 36, 235, 88, 232, 37, 63, 234, 15, 126, 207, 214, 77, 13, 13, 150, 147, 90, 195,
208, 248, 226, 121, 31, 61, 208, 180, 3, 197, 130, 231, 125, 31, 123, 94, 241, 4, 252, 207, 112, 101, 87, 253,
235, 71, 48, 250, 41, 122, 161, 170, 117, 31, 66, 112, 11, 46, 174, 171, 154, 177, 11, 151, 219, 208, 241, 148,
213, 29, 189, 36, 249, 101, 251, 82, 41, 120, 63, 147, 111, 74, 64, 251, 45, 180, 44, 150, 231, 86, 185, 231,
244, 1, 102, 101, 86, 209, 27, 216, 63, 128, 222, 180, 100, 47, 213, 121, 119, 115, 237, 220, 254, 173, 169,
204, 239, 7, 178, 211, 114, 90, 10, 150, 157, 65, 0, 0, 0, 9, 112, 72, 89, 115, 0, 0, 46, 35, 0, 0, 46, 35, 1,
120, 165, 63, 118, 0, 0, 0, 7, 116, 73, 77, 69, 7, 227, 4, 15, 10, 5, 36, 189, 4, 224, 88, 0, 0, 0, 25, 116, 69,
88, 116, 67, 111, 109, 109, 101, 110, 116, 0, 67, 114, 101, 97, 116, 101, 100, 32, 119, 105, 116, 104, 32, 71,
73, 77, 80, 87, 129, 14, 23, 0, 0, 0, 12, 73, 68, 65, 84, 8, 215, 99, 120, 241, 226, 61, 0, 5, 123, 2, 192, 194,
77, 211, 95, 0, 0, 0, 0, 73, 69, 78, 68, 174, 66, 96, 130,
}
func TestMain(m *testing.M) {
pixelgl.Run(func() {
os.Exit(m.Run())
})
}
func TestSprite_Draw(t *testing.T) {
img, _, err := image.Decode(bytes.NewReader(onePixelImage))
if err != nil {
t.Fatalf("Could not decode image: %v", err)
}
pic := pixel.PictureDataFromImage(img)
sprite := pixel.NewSprite(pic, pic.Bounds())
cfg := pixelgl.WindowConfig{
Title: "testing",
Bounds: pixel.R(0, 0, 150, 150),
Invisible: true,
}
win, err := pixelgl.NewWindow(cfg)
if err != nil {
t.Fatalf("Could not create window: %v", err)
}
sprite.Draw(win, pixel.IM)
}

View File

@ -17,7 +17,7 @@ import (
// It supports TrianglesPosition, TrianglesColor, TrianglesPicture and PictureColor. // It supports TrianglesPosition, TrianglesColor, TrianglesPicture and PictureColor.
type Canvas struct { type Canvas struct {
gf *GLFrame gf *GLFrame
shader *GLShader shader *glhf.Shader
cmp pixel.ComposeMethod cmp pixel.ComposeMethod
mat mgl32.Mat3 mat mgl32.Mat3
@ -37,35 +37,30 @@ func NewCanvas(bounds pixel.Rect) *Canvas {
col: mgl32.Vec4{1, 1, 1, 1}, col: mgl32.Vec4{1, 1, 1, 1},
} }
c.shader = NewGLShader(baseCanvasFragmentShader)
c.SetBounds(bounds) c.SetBounds(bounds)
var shader *glhf.Shader
mainthread.Call(func() {
var err error
shader, err = glhf.NewShader(
canvasVertexFormat,
canvasUniformFormat,
canvasVertexShader,
canvasFragmentShader,
)
if err != nil {
panic(errors.Wrap(err, "failed to create Canvas, there's a bug in the shader"))
}
})
c.shader = shader
return c return c
} }
// SetUniform will update the named uniform with the value of any supported underlying
// attribute variable. If the uniform already exists, including defaults, they will be reassigned
// to the new value. The value can be a pointer.
func (c *Canvas) SetUniform(name string, value interface{}) {
c.shader.SetUniform(name, value)
}
// SetFragmentShader allows you to set a new fragment shader on the underlying
// framebuffer. Argument "src" is the GLSL source, not a filename.
func (c *Canvas) SetFragmentShader(src string) {
c.shader.fs = src
c.shader.Update()
}
// MakeTriangles creates a specialized copy of the supplied Triangles that draws onto this Canvas. // MakeTriangles creates a specialized copy of the supplied Triangles that draws onto this Canvas.
// //
// TrianglesPosition, TrianglesColor and TrianglesPicture are supported. // TrianglesPosition, TrianglesColor and TrianglesPicture are supported.
func (c *Canvas) MakeTriangles(t pixel.Triangles) pixel.TargetTriangles { func (c *Canvas) MakeTriangles(t pixel.Triangles) pixel.TargetTriangles {
if gt, ok := t.(*GLTriangles); ok {
return &canvasTriangles{
GLTriangles: gt,
dst: c,
}
}
return &canvasTriangles{ return &canvasTriangles{
GLTriangles: NewGLTriangles(c.shader, t), GLTriangles: NewGLTriangles(c.shader, t),
dst: c, dst: c,
@ -96,13 +91,8 @@ func (c *Canvas) MakePicture(p pixel.Picture) pixel.TargetPicture {
// SetMatrix sets a Matrix that every point will be projected by. // SetMatrix sets a Matrix that every point will be projected by.
func (c *Canvas) SetMatrix(m pixel.Matrix) { func (c *Canvas) SetMatrix(m pixel.Matrix) {
// pixel.Matrix is 3x2 with an implicit 0, 0, 1 row after it. So for i := range m {
// [0] [2] [4] [0] [3] [6] c.mat[i] = float32(m[i])
// [1] [3] [5] => [1] [4] [7]
// 0 0 1 0 0 1
// since all matrix ops are affine, the last row never changes, and we don't need to copy it
for i, j := range [...]int{0, 1, 3, 4, 6, 7} {
c.mat[j] = float32(m[i])
} }
} }
@ -133,7 +123,7 @@ func (c *Canvas) SetBounds(bounds pixel.Rect) {
c.sprite = pixel.NewSprite(nil, pixel.Rect{}) c.sprite = pixel.NewSprite(nil, pixel.Rect{})
} }
c.sprite.Set(c, c.Bounds()) c.sprite.Set(c, c.Bounds())
// c.sprite.SetMatrix(pixel.IM.Moved(c.Bounds().Center())) c.sprite.SetMatrix(pixel.IM.Moved(c.Bounds().Center()))
} }
// Bounds returns the rectangular bounds of the Canvas. // Bounds returns the rectangular bounds of the Canvas.
@ -228,50 +218,12 @@ func (c *Canvas) Texture() *glhf.Texture {
return c.gf.Texture() return c.gf.Texture()
} }
// Frame returns the underlying OpenGL Frame of this Canvas. // Draw draws a rectangle equal to Canvas's Bounds containing the Canvas's content to another
func (c *Canvas) Frame() *glhf.Frame { // Target.
return c.gf.frame
}
// SetPixels replaces the content of the Canvas with the provided pixels. The provided slice must be
// an alpha-premultiplied RGBA sequence of correct length (4 * width * height).
func (c *Canvas) SetPixels(pixels []uint8) {
c.gf.Dirty()
mainthread.Call(func() {
tex := c.Texture()
tex.Begin()
tex.SetPixels(0, 0, tex.Width(), tex.Height(), pixels)
tex.End()
})
}
// Pixels returns an alpha-premultiplied RGBA sequence of the content of the Canvas.
func (c *Canvas) Pixels() []uint8 {
var pixels []uint8
mainthread.Call(func() {
tex := c.Texture()
tex.Begin()
pixels = tex.Pixels(0, 0, tex.Width(), tex.Height())
tex.End()
})
return pixels
}
// Draw draws the content of the Canvas onto another Target, transformed by the given Matrix, just
// like if it was a Sprite containing the whole Canvas.
func (c *Canvas) Draw(t pixel.Target, matrix pixel.Matrix) {
c.sprite.Draw(t, matrix)
}
// DrawColorMask draws the content of the Canvas onto another Target, transformed by the given
// Matrix and multiplied by the given mask, just like if it was a Sprite containing the whole Canvas.
// //
// If the color mask is nil, a fully opaque white mask will be used causing no effect. // Note, that the matrix and the color mask of this Canvas have no effect here.
func (c *Canvas) DrawColorMask(t pixel.Target, matrix pixel.Matrix, mask color.Color) { func (c *Canvas) Draw(t pixel.Target) {
c.sprite.DrawColorMask(t, matrix, mask) c.sprite.Draw(t)
} }
type canvasTriangles struct { type canvasTriangles struct {
@ -284,41 +236,29 @@ func (ct *canvasTriangles) draw(tex *glhf.Texture, bounds pixel.Rect) {
// save the current state vars to avoid race condition // save the current state vars to avoid race condition
cmp := ct.dst.cmp cmp := ct.dst.cmp
smt := ct.dst.smooth
mat := ct.dst.mat mat := ct.dst.mat
col := ct.dst.col col := ct.dst.col
smt := ct.dst.smooth
mainthread.CallNonBlock(func() { mainthread.CallNonBlock(func() {
ct.dst.setGlhfBounds() ct.dst.setGlhfBounds()
setBlendFunc(cmp) setBlendFunc(cmp)
frame := ct.dst.gf.Frame() frame := ct.dst.gf.Frame()
shader := ct.shader.s shader := ct.dst.shader
frame.Begin() frame.Begin()
shader.Begin() shader.Begin()
ct.shader.uniformDefaults.transform = mat
ct.shader.uniformDefaults.colormask = col
dstBounds := ct.dst.Bounds() dstBounds := ct.dst.Bounds()
ct.shader.uniformDefaults.bounds = mgl32.Vec4{ shader.SetUniformAttr(canvasBounds, mgl32.Vec4{
float32(dstBounds.Min.X), float32(dstBounds.Min.X()),
float32(dstBounds.Min.Y), float32(dstBounds.Min.Y()),
float32(dstBounds.W()), float32(dstBounds.W()),
float32(dstBounds.H()), float32(dstBounds.H()),
} })
shader.SetUniformAttr(canvasTransform, mat)
bx, by, bw, bh := intBounds(bounds) shader.SetUniformAttr(canvasColorMask, col)
ct.shader.uniformDefaults.texbounds = mgl32.Vec4{
float32(bx),
float32(by),
float32(bw),
float32(bh),
}
for loc, u := range ct.shader.uniforms {
ct.shader.s.SetUniformAttr(loc, u.Value())
}
if tex == nil { if tex == nil {
ct.vs.Begin() ct.vs.Begin()
@ -327,6 +267,14 @@ func (ct *canvasTriangles) draw(tex *glhf.Texture, bounds pixel.Rect) {
} else { } else {
tex.Begin() tex.Begin()
bx, by, bw, bh := intBounds(bounds)
shader.SetUniformAttr(canvasTexBounds, mgl32.Vec4{
float32(bx),
float32(by),
float32(bw),
float32(bh),
})
if tex.Smooth() != smt { if tex.Smooth() != smt {
tex.SetSmooth(smt) tex.SetSmooth(smt)
} }
@ -359,3 +307,82 @@ func (cp *canvasPicture) Draw(t pixel.TargetTriangles) {
} }
ct.draw(cp.GLPicture.Texture(), cp.GLPicture.Bounds()) ct.draw(cp.GLPicture.Texture(), cp.GLPicture.Bounds())
} }
const (
canvasPosition int = iota
canvasColor
canvasTexture
canvasIntensity
)
var canvasVertexFormat = glhf.AttrFormat{
canvasPosition: {Name: "position", Type: glhf.Vec2},
canvasColor: {Name: "color", Type: glhf.Vec4},
canvasTexture: {Name: "texture", Type: glhf.Vec2},
canvasIntensity: {Name: "intensity", Type: glhf.Float},
}
const (
canvasTransform int = iota
canvasColorMask
canvasBounds
canvasTexBounds
)
var canvasUniformFormat = glhf.AttrFormat{
canvasTransform: {Name: "transform", Type: glhf.Mat3},
canvasColorMask: {Name: "colorMask", Type: glhf.Vec4},
canvasBounds: {Name: "bounds", Type: glhf.Vec4},
canvasTexBounds: {Name: "texBounds", Type: glhf.Vec4},
}
var canvasVertexShader = `
#version 330 core
in vec2 position;
in vec4 color;
in vec2 texture;
in float intensity;
out vec4 Color;
out vec2 Texture;
out float Intensity;
uniform mat3 transform;
uniform vec4 bounds;
void main() {
vec2 transPos = (transform * vec3(position, 1.0)).xy;
vec2 normPos = (transPos - bounds.xy) / bounds.zw * 2 - vec2(1, 1);
gl_Position = vec4(normPos, 0.0, 1.0);
Color = color;
Texture = texture;
Intensity = intensity;
}
`
var canvasFragmentShader = `
#version 330 core
in vec4 Color;
in vec2 Texture;
in float Intensity;
out vec4 color;
uniform vec4 colorMask;
uniform vec4 texBounds;
uniform sampler2D tex;
void main() {
if (Intensity == 0) {
color = colorMask * Color;
} else {
color = vec4(0, 0, 0, 0);
color += (1 - Intensity) * Color;
vec2 t = (Texture - texBounds.xy) / texBounds.zw;
color += Intensity * Color * texture(tex, t);
color *= colorMask;
}
}
`

View File

@ -32,12 +32,6 @@ func (gf *GLFrame) SetBounds(bounds pixel.Rect) {
oldF := gf.frame oldF := gf.frame
_, _, w, h := intBounds(bounds) _, _, w, h := intBounds(bounds)
if w <= 0 {
w = 1
}
if h <= 0 {
h = 1
}
gf.frame = glhf.NewFrame(w, h, false) gf.frame = glhf.NewFrame(w, h, false)
// preserve old content // preserve old content
@ -76,7 +70,7 @@ func (gf *GLFrame) Color(at pixel.Vec) pixel.RGBA {
return pixel.Alpha(0) return pixel.Alpha(0)
} }
bx, by, bw, _ := intBounds(gf.bounds) bx, by, bw, _ := intBounds(gf.bounds)
x, y := int(at.X)-bx, int(at.Y)-by x, y := int(at.X())-bx, int(at.Y())-by
off := y*bw + x off := y*bw + x
return pixel.RGBA{ return pixel.RGBA{
R: float64(gf.pixels[off*4+0]) / 255, R: float64(gf.pixels[off*4+0]) / 255,

View File

@ -42,8 +42,8 @@ func NewGLPicture(p pixel.Picture) GLPicture {
for y := 0; y < bh; y++ { for y := 0; y < bh; y++ {
for x := 0; x < bw; x++ { for x := 0; x < bw; x++ {
at := pixel.V( at := pixel.V(
math.Max(float64(bx+x), bounds.Min.X), math.Max(float64(bx+x), bounds.Min.X()),
math.Max(float64(by+y), bounds.Min.Y), math.Max(float64(by+y), bounds.Min.Y()),
) )
color := p.Color(at) color := p.Color(at)
off := (y*bw + x) * 4 off := (y*bw + x) * 4
@ -87,7 +87,7 @@ func (gp *glPicture) Color(at pixel.Vec) pixel.RGBA {
return pixel.Alpha(0) return pixel.Alpha(0)
} }
bx, by, bw, _ := intBounds(gp.bounds) bx, by, bw, _ := intBounds(gp.bounds)
x, y := int(at.X)-bx, int(at.Y)-by x, y := int(at.X())-bx, int(at.Y())-by
off := y*bw + x off := y*bw + x
return pixel.RGBA{ return pixel.RGBA{
R: float64(gp.pixels[off*4+0]) / 255, R: float64(gp.pixels[off*4+0]) / 255,

View File

@ -1,298 +0,0 @@
package pixelgl
import (
"github.com/faiface/glhf"
"github.com/faiface/mainthread"
"github.com/go-gl/mathgl/mgl32"
"github.com/pkg/errors"
)
// GLShader is a type to assist with managing a canvas's underlying
// shader configuration. This allows for customization of shaders on
// a per canvas basis.
type GLShader struct {
s *glhf.Shader
vf, uf glhf.AttrFormat
vs, fs string
uniforms []gsUniformAttr
uniformDefaults struct {
transform mgl32.Mat3
colormask mgl32.Vec4
bounds mgl32.Vec4
texbounds mgl32.Vec4
cliprect mgl32.Vec4
}
}
type gsUniformAttr struct {
Name string
Type glhf.AttrType
value interface{}
ispointer bool
}
const (
canvasPosition int = iota
canvasColor
canvasTexCoords
canvasIntensity
canvasClip
)
var defaultCanvasVertexFormat = glhf.AttrFormat{
canvasPosition: glhf.Attr{Name: "aPosition", Type: glhf.Vec2},
canvasColor: glhf.Attr{Name: "aColor", Type: glhf.Vec4},
canvasTexCoords: glhf.Attr{Name: "aTexCoords", Type: glhf.Vec2},
canvasIntensity: glhf.Attr{Name: "aIntensity", Type: glhf.Float},
canvasClip: glhf.Attr{Name: "aClipRect", Type: glhf.Vec4},
}
// Sets up a base shader with everything needed for a Pixel
// canvas to render correctly. The defaults can be overridden
// by simply using the SetUniform function.
func NewGLShader(fragmentShader string) *GLShader {
gs := &GLShader{
vf: defaultCanvasVertexFormat,
vs: baseCanvasVertexShader,
fs: fragmentShader,
}
gs.SetUniform("uTransform", &gs.uniformDefaults.transform)
gs.SetUniform("uColorMask", &gs.uniformDefaults.colormask)
gs.SetUniform("uBounds", &gs.uniformDefaults.bounds)
gs.SetUniform("uTexBounds", &gs.uniformDefaults.texbounds)
gs.Update()
return gs
}
// Update reinitialize GLShader data and recompile the underlying gl shader object
func (gs *GLShader) Update() {
gs.uf = make([]glhf.Attr, len(gs.uniforms))
for idx := range gs.uniforms {
gs.uf[idx] = glhf.Attr{
Name: gs.uniforms[idx].Name,
Type: gs.uniforms[idx].Type,
}
}
var shader *glhf.Shader
mainthread.Call(func() {
var err error
shader, err = glhf.NewShader(
gs.vf,
gs.uf,
gs.vs,
gs.fs,
)
if err != nil {
panic(errors.Wrap(err, "failed to create Canvas, there's a bug in the shader"))
}
})
gs.s = shader
}
// gets the uniform index from GLShader
func (gs *GLShader) getUniform(Name string) int {
for i, u := range gs.uniforms {
if u.Name == Name {
return i
}
}
return -1
}
// SetUniform appends a custom uniform name and value to the shader.
// if the uniform already exists, it will simply be overwritten.
//
// example:
//
// utime := float32(time.Since(starttime)).Seconds())
// mycanvas.shader.AddUniform("u_time", &utime)
func (gs *GLShader) SetUniform(name string, value interface{}) {
t, p := getAttrType(value)
if loc := gs.getUniform(name); loc > -1 {
gs.uniforms[loc].Name = name
gs.uniforms[loc].Type = t
gs.uniforms[loc].ispointer = p
gs.uniforms[loc].value = value
return
}
gs.uniforms = append(gs.uniforms, gsUniformAttr{
Name: name,
Type: t,
ispointer: p,
value: value,
})
}
// Value returns the attribute's concrete value. If the stored value
// is a pointer, we return the dereferenced value.
func (gu *gsUniformAttr) Value() interface{} {
if !gu.ispointer {
return gu.value
}
switch gu.Type {
case glhf.Vec2:
return *gu.value.(*mgl32.Vec2)
case glhf.Vec3:
return *gu.value.(*mgl32.Vec3)
case glhf.Vec4:
return *gu.value.(*mgl32.Vec4)
case glhf.Mat2:
return *gu.value.(*mgl32.Mat2)
case glhf.Mat23:
return *gu.value.(*mgl32.Mat2x3)
case glhf.Mat24:
return *gu.value.(*mgl32.Mat2x4)
case glhf.Mat3:
return *gu.value.(*mgl32.Mat3)
case glhf.Mat32:
return *gu.value.(*mgl32.Mat3x2)
case glhf.Mat34:
return *gu.value.(*mgl32.Mat3x4)
case glhf.Mat4:
return *gu.value.(*mgl32.Mat4)
case glhf.Mat42:
return *gu.value.(*mgl32.Mat4x2)
case glhf.Mat43:
return *gu.value.(*mgl32.Mat4x3)
case glhf.Int:
return *gu.value.(*int32)
case glhf.Float:
return *gu.value.(*float32)
default:
panic("invalid attrtype")
}
}
// Returns the type identifier for any (supported) attribute variable type
// and whether or not it is a pointer of that type.
func getAttrType(v interface{}) (glhf.AttrType, bool) {
switch v.(type) {
case int32:
return glhf.Int, false
case float32:
return glhf.Float, false
case mgl32.Vec2:
return glhf.Vec2, false
case mgl32.Vec3:
return glhf.Vec3, false
case mgl32.Vec4:
return glhf.Vec4, false
case mgl32.Mat2:
return glhf.Mat2, false
case mgl32.Mat2x3:
return glhf.Mat23, false
case mgl32.Mat2x4:
return glhf.Mat24, false
case mgl32.Mat3:
return glhf.Mat3, false
case mgl32.Mat3x2:
return glhf.Mat32, false
case mgl32.Mat3x4:
return glhf.Mat34, false
case mgl32.Mat4:
return glhf.Mat4, false
case mgl32.Mat4x2:
return glhf.Mat42, false
case mgl32.Mat4x3:
return glhf.Mat43, false
case *mgl32.Vec2:
return glhf.Vec2, true
case *mgl32.Vec3:
return glhf.Vec3, true
case *mgl32.Vec4:
return glhf.Vec4, true
case *mgl32.Mat2:
return glhf.Mat2, true
case *mgl32.Mat2x3:
return glhf.Mat23, true
case *mgl32.Mat2x4:
return glhf.Mat24, true
case *mgl32.Mat3:
return glhf.Mat3, true
case *mgl32.Mat3x2:
return glhf.Mat32, true
case *mgl32.Mat3x4:
return glhf.Mat34, true
case *mgl32.Mat4:
return glhf.Mat4, true
case *mgl32.Mat4x2:
return glhf.Mat42, true
case *mgl32.Mat4x3:
return glhf.Mat43, true
case *int32:
return glhf.Int, true
case *float32:
return glhf.Float, true
default:
panic("invalid AttrType")
}
}
var baseCanvasVertexShader = `
#version 330 core
in vec2 aPosition;
in vec4 aColor;
in vec2 aTexCoords;
in float aIntensity;
in vec4 aClipRect;
in float aIsClipped;
out vec4 vColor;
out vec2 vTexCoords;
out float vIntensity;
out vec2 vPosition;
out vec4 vClipRect;
uniform mat3 uTransform;
uniform vec4 uBounds;
void main() {
vec2 transPos = (uTransform * vec3(aPosition, 1.0)).xy;
vec2 normPos = (transPos - uBounds.xy) / uBounds.zw * 2 - vec2(1, 1);
gl_Position = vec4(normPos, 0.0, 1.0);
vColor = aColor;
vPosition = aPosition;
vTexCoords = aTexCoords;
vIntensity = aIntensity;
vClipRect = aClipRect;
}
`
var baseCanvasFragmentShader = `
#version 330 core
in vec4 vColor;
in vec2 vTexCoords;
in float vIntensity;
in vec4 vClipRect;
out vec4 fragColor;
uniform vec4 uColorMask;
uniform vec4 uTexBounds;
uniform sampler2D uTexture;
void main() {
if ((vClipRect != vec4(0,0,0,0)) && (gl_FragCoord.x < vClipRect.x || gl_FragCoord.y < vClipRect.y || gl_FragCoord.x > vClipRect.z || gl_FragCoord.y > vClipRect.w))
discard;
if (vIntensity == 0) {
fragColor = uColorMask * vColor;
} else {
fragColor = vec4(0, 0, 0, 0);
fragColor += (1 - vIntensity) * vColor;
vec2 t = (vTexCoords - uTexBounds.xy) / uTexBounds.zw;
fragColor += vIntensity * vColor * texture(uTexture, t);
fragColor *= uColorMask;
}
}
`

View File

@ -15,43 +15,23 @@ import (
type GLTriangles struct { type GLTriangles struct {
vs *glhf.VertexSlice vs *glhf.VertexSlice
data []float32 data []float32
shader *GLShader shader *glhf.Shader
} }
var ( var (
_ pixel.TrianglesPosition = (*GLTriangles)(nil) _ pixel.TrianglesPosition = (*GLTriangles)(nil)
_ pixel.TrianglesColor = (*GLTriangles)(nil) _ pixel.TrianglesColor = (*GLTriangles)(nil)
_ pixel.TrianglesPicture = (*GLTriangles)(nil) _ pixel.TrianglesPicture = (*GLTriangles)(nil)
_ pixel.TrianglesClipped = (*GLTriangles)(nil)
)
// The following is a helper so that the indices of
// each of these items is easier to see/debug.
const (
triPosX = iota
triPosY
triColorR
triColorG
triColorB
triColorA
triPicX
triPicY
triIntensity
triClipMinX
triClipMinY
triClipMaxX
triClipMaxY
trisAttrLen
) )
// NewGLTriangles returns GLTriangles initialized with the data from the supplied Triangles. // NewGLTriangles returns GLTriangles initialized with the data from the supplied Triangles.
// //
// Only draw the Triangles using the provided Shader. // Only draw the Triangles using the provided Shader.
func NewGLTriangles(shader *GLShader, t pixel.Triangles) *GLTriangles { func NewGLTriangles(shader *glhf.Shader, t pixel.Triangles) *GLTriangles {
var gt *GLTriangles var gt *GLTriangles
mainthread.Call(func() { mainthread.Call(func() {
gt = &GLTriangles{ gt = &GLTriangles{
vs: glhf.MakeVertexSlice(shader.s, 0, t.Len()), vs: glhf.MakeVertexSlice(shader, 0, t.Len()),
shader: shader, shader: shader,
} }
}) })
@ -68,7 +48,7 @@ func (gt *GLTriangles) VertexSlice() *glhf.VertexSlice {
} }
// Shader returns the GLTriangles's associated shader. // Shader returns the GLTriangles's associated shader.
func (gt *GLTriangles) Shader() *GLShader { func (gt *GLTriangles) Shader() *glhf.Shader {
return gt.shader return gt.shader
} }
@ -80,29 +60,22 @@ func (gt *GLTriangles) Len() int {
// SetLen efficiently resizes GLTriangles to len. // SetLen efficiently resizes GLTriangles to len.
// //
// Time complexity is amortized O(1). // Time complexity is amortized O(1).
func (gt *GLTriangles) SetLen(length int) { func (gt *GLTriangles) SetLen(len int) {
switch { if len > gt.Len() {
case length > gt.Len(): needAppend := len - gt.Len()
needAppend := length - gt.Len()
for i := 0; i < needAppend; i++ { for i := 0; i < needAppend; i++ {
gt.data = append(gt.data, gt.data = append(gt.data,
0, 0, 0, 0,
1, 1, 1, 1, 1, 1, 1, 1,
0, 0, 0, 0,
0, 0,
0, 0, 0, 0,
) )
} }
case length < gt.Len():
gt.data = gt.data[:length*gt.vs.Stride()]
default:
return
} }
mainthread.Call(func() { if len < gt.Len() {
gt.vs.Begin() gt.data = gt.data[:len*gt.vs.Stride()]
gt.vs.SetLen(length) }
gt.vs.End() gt.submitData()
})
} }
// Slice returns a sub-Triangles of this GLTriangles in range [i, j). // Slice returns a sub-Triangles of this GLTriangles in range [i, j).
@ -122,67 +95,73 @@ func (gt *GLTriangles) updateData(t pixel.Triangles) {
} }
// TrianglesData short path // TrianglesData short path
stride := gt.vs.Stride()
length := gt.Len()
if t, ok := t.(*pixel.TrianglesData); ok { if t, ok := t.(*pixel.TrianglesData); ok {
for i := 0; i < length; i++ { for i := 0; i < gt.Len(); i++ {
var ( var (
px, py = (*t)[i].Position.XY() px, py = (*t)[i].Position.XY()
col = (*t)[i].Color col = (*t)[i].Color
tx, ty = (*t)[i].Picture.XY() tx, ty = (*t)[i].Picture.XY()
in = (*t)[i].Intensity in = (*t)[i].Intensity
rec = (*t)[i].ClipRect
) )
d := gt.data[i*stride : i*stride+trisAttrLen] gt.data[i*gt.vs.Stride()+0] = float32(px)
d[triPosX] = float32(px) gt.data[i*gt.vs.Stride()+1] = float32(py)
d[triPosY] = float32(py) gt.data[i*gt.vs.Stride()+2] = float32(col.R)
d[triColorR] = float32(col.R) gt.data[i*gt.vs.Stride()+3] = float32(col.G)
d[triColorG] = float32(col.G) gt.data[i*gt.vs.Stride()+4] = float32(col.B)
d[triColorB] = float32(col.B) gt.data[i*gt.vs.Stride()+5] = float32(col.A)
d[triColorA] = float32(col.A) gt.data[i*gt.vs.Stride()+6] = float32(tx)
d[triPicX] = float32(tx) gt.data[i*gt.vs.Stride()+7] = float32(ty)
d[triPicY] = float32(ty) gt.data[i*gt.vs.Stride()+8] = float32(in)
d[triIntensity] = float32(in)
d[triClipMinX] = float32(rec.Min.X)
d[triClipMinY] = float32(rec.Min.Y)
d[triClipMaxX] = float32(rec.Max.X)
d[triClipMaxY] = float32(rec.Max.Y)
} }
return return
} }
if t, ok := t.(pixel.TrianglesPosition); ok { if t, ok := t.(pixel.TrianglesPosition); ok {
for i := 0; i < length; i++ { for i := 0; i < gt.Len(); i++ {
px, py := t.Position(i).XY() px, py := t.Position(i).XY()
gt.data[i*stride+triPosX] = float32(px) gt.data[i*gt.vs.Stride()+0] = float32(px)
gt.data[i*stride+triPosY] = float32(py) gt.data[i*gt.vs.Stride()+1] = float32(py)
} }
} }
if t, ok := t.(pixel.TrianglesColor); ok { if t, ok := t.(pixel.TrianglesColor); ok {
for i := 0; i < length; i++ { for i := 0; i < gt.Len(); i++ {
col := t.Color(i) col := t.Color(i)
gt.data[i*stride+triColorR] = float32(col.R) gt.data[i*gt.vs.Stride()+2] = float32(col.R)
gt.data[i*stride+triColorG] = float32(col.G) gt.data[i*gt.vs.Stride()+3] = float32(col.G)
gt.data[i*stride+triColorB] = float32(col.B) gt.data[i*gt.vs.Stride()+4] = float32(col.B)
gt.data[i*stride+triColorA] = float32(col.A) gt.data[i*gt.vs.Stride()+5] = float32(col.A)
} }
} }
if t, ok := t.(pixel.TrianglesPicture); ok { if t, ok := t.(pixel.TrianglesPicture); ok {
for i := 0; i < length; i++ { for i := 0; i < gt.Len(); i++ {
pic, intensity := t.Picture(i) pic, intensity := t.Picture(i)
gt.data[i*stride+triPicX] = float32(pic.X) gt.data[i*gt.vs.Stride()+6] = float32(pic.X())
gt.data[i*stride+triPicY] = float32(pic.Y) gt.data[i*gt.vs.Stride()+7] = float32(pic.Y())
gt.data[i*stride+triIntensity] = float32(intensity) gt.data[i*gt.vs.Stride()+8] = float32(intensity)
} }
} }
if t, ok := t.(pixel.TrianglesClipped); ok { }
for i := 0; i < length; i++ {
rect, _ := t.ClipRect(i) func (gt *GLTriangles) submitData() {
gt.data[i*stride+triClipMinX] = float32(rect.Min.X) // this code is supposed to copy the vertex data and CallNonBlock the update if
gt.data[i*stride+triClipMinY] = float32(rect.Min.Y) // the data is small enough, otherwise it'll block and not copy the data
gt.data[i*stride+triClipMaxX] = float32(rect.Max.X) if len(gt.data) < 256 { // arbitrary heurestic constant
gt.data[i*stride+triClipMaxY] = float32(rect.Max.Y) data := append([]float32{}, gt.data...)
} mainthread.CallNonBlock(func() {
gt.vs.Begin()
dataLen := len(data) / gt.vs.Stride()
gt.vs.SetLen(dataLen)
gt.vs.SetVertexData(data)
gt.vs.End()
})
} else {
mainthread.Call(func() {
gt.vs.Begin()
dataLen := len(gt.data) / gt.vs.Stride()
gt.vs.SetLen(dataLen)
gt.vs.SetVertexData(gt.data)
gt.vs.End()
})
} }
} }
@ -194,29 +173,7 @@ func (gt *GLTriangles) Update(t pixel.Triangles) {
panic(fmt.Errorf("(%T).Update: invalid triangles len", gt)) panic(fmt.Errorf("(%T).Update: invalid triangles len", gt))
} }
gt.updateData(t) gt.updateData(t)
gt.submitData()
// Copy the verteces down to the glhf.VertexData
gt.CopyVertices()
}
// CopyVertices copies the GLTriangle data down to the vertex data.
func (gt *GLTriangles) CopyVertices() {
// this code is supposed to copy the vertex data and CallNonBlock the update if
// the data is small enough, otherwise it'll block and not copy the data
if len(gt.data) < 256 { // arbitrary heurestic constant
data := append([]float32{}, gt.data...)
mainthread.CallNonBlock(func() {
gt.vs.Begin()
gt.vs.SetVertexData(data)
gt.vs.End()
})
} else {
mainthread.Call(func() {
gt.vs.Begin()
gt.vs.SetVertexData(gt.data)
gt.vs.End()
})
}
} }
// Copy returns an independent copy of this GLTriangles. // Copy returns an independent copy of this GLTriangles.
@ -226,31 +183,19 @@ func (gt *GLTriangles) Copy() pixel.Triangles {
return NewGLTriangles(gt.shader, gt) return NewGLTriangles(gt.shader, gt)
} }
// index is a helper function that returns the index in the data
// slice given the i-th vertex and the item index.
func (gt *GLTriangles) index(i, idx int) int {
return i*gt.vs.Stride() + idx
}
// Position returns the Position property of the i-th vertex. // Position returns the Position property of the i-th vertex.
func (gt *GLTriangles) Position(i int) pixel.Vec { func (gt *GLTriangles) Position(i int) pixel.Vec {
px := gt.data[gt.index(i, triPosX)] px := gt.data[i*gt.vs.Stride()+0]
py := gt.data[gt.index(i, triPosY)] py := gt.data[i*gt.vs.Stride()+1]
return pixel.V(float64(px), float64(py)) return pixel.V(float64(px), float64(py))
} }
// SetPosition sets the position property of the i-th vertex.
func (gt *GLTriangles) SetPosition(i int, p pixel.Vec) {
gt.data[gt.index(i, triPosX)] = float32(p.X)
gt.data[gt.index(i, triPosY)] = float32(p.Y)
}
// Color returns the Color property of the i-th vertex. // Color returns the Color property of the i-th vertex.
func (gt *GLTriangles) Color(i int) pixel.RGBA { func (gt *GLTriangles) Color(i int) pixel.RGBA {
r := gt.data[gt.index(i, triColorR)] r := gt.data[i*gt.vs.Stride()+2]
g := gt.data[gt.index(i, triColorG)] g := gt.data[i*gt.vs.Stride()+3]
b := gt.data[gt.index(i, triColorB)] b := gt.data[i*gt.vs.Stride()+4]
a := gt.data[gt.index(i, triColorA)] a := gt.data[i*gt.vs.Stride()+5]
return pixel.RGBA{ return pixel.RGBA{
R: float64(r), R: float64(r),
G: float64(g), G: float64(g),
@ -259,44 +204,10 @@ func (gt *GLTriangles) Color(i int) pixel.RGBA {
} }
} }
// SetColor sets the color property of the i-th vertex.
func (gt *GLTriangles) SetColor(i int, c pixel.RGBA) {
gt.data[gt.index(i, triColorR)] = float32(c.R)
gt.data[gt.index(i, triColorG)] = float32(c.G)
gt.data[gt.index(i, triColorB)] = float32(c.B)
gt.data[gt.index(i, triColorA)] = float32(c.A)
}
// Picture returns the Picture property of the i-th vertex. // Picture returns the Picture property of the i-th vertex.
func (gt *GLTriangles) Picture(i int) (pic pixel.Vec, intensity float64) { func (gt *GLTriangles) Picture(i int) (pic pixel.Vec, intensity float64) {
tx := gt.data[gt.index(i, triPicX)] tx := gt.data[i*gt.vs.Stride()+6]
ty := gt.data[gt.index(i, triPicY)] ty := gt.data[i*gt.vs.Stride()+7]
intensity = float64(gt.data[gt.index(i, triIntensity)]) intensity = float64(gt.data[i*gt.vs.Stride()+8])
return pixel.V(float64(tx), float64(ty)), intensity return pixel.V(float64(tx), float64(ty)), intensity
} }
// SetPicture sets the picture property of the i-th vertex.
func (gt *GLTriangles) SetPicture(i int, pic pixel.Vec, intensity float64) {
gt.data[gt.index(i, triPicX)] = float32(pic.X)
gt.data[gt.index(i, triPicY)] = float32(pic.Y)
gt.data[gt.index(i, triIntensity)] = float32(intensity)
}
// ClipRect returns the Clipping rectangle property of the i-th vertex.
func (gt *GLTriangles) ClipRect(i int) (rect pixel.Rect, is bool) {
mx := gt.data[gt.index(i, triClipMinX)]
my := gt.data[gt.index(i, triClipMinY)]
ax := gt.data[gt.index(i, triClipMaxX)]
ay := gt.data[gt.index(i, triClipMaxY)]
rect = pixel.R(float64(mx), float64(my), float64(ax), float64(ay))
is = rect.Area() != 0.0
return
}
// SetClipRect sets the Clipping rectangle property of the i-th vertex.
func (gt *GLTriangles) SetClipRect(i int, rect pixel.Rect) {
gt.data[gt.index(i, triClipMinX)] = float32(rect.Min.X)
gt.data[gt.index(i, triClipMinY)] = float32(rect.Min.Y)
gt.data[gt.index(i, triClipMaxX)] = float32(rect.Max.X)
gt.data[gt.index(i, triClipMaxY)] = float32(rect.Max.Y)
}

View File

@ -1,11 +1,9 @@
package pixelgl package pixelgl
import ( import (
"time"
"github.com/faiface/mainthread" "github.com/faiface/mainthread"
"github.com/faiface/pixel" "github.com/faiface/pixel"
"github.com/go-gl/glfw/v3.3/glfw" "github.com/go-gl/glfw/v3.2/glfw"
) )
// Pressed returns whether the Button is currently pressed down. // Pressed returns whether the Button is currently pressed down.
@ -13,21 +11,14 @@ func (w *Window) Pressed(button Button) bool {
return w.currInp.buttons[button] return w.currInp.buttons[button]
} }
// JustPressed returns whether the Button has been pressed in the last frame. // JustPressed returns whether the Button has just been pressed down.
func (w *Window) JustPressed(button Button) bool { func (w *Window) JustPressed(button Button) bool {
return w.pressEvents[button] return w.currInp.buttons[button] && !w.prevInp.buttons[button]
} }
// JustReleased returns whether the Button has been released in the last frame. // JustReleased returns whether the Button has just been released up.
func (w *Window) JustReleased(button Button) bool { func (w *Window) JustReleased(button Button) bool {
return w.releaseEvents[button] return !w.currInp.buttons[button] && w.prevInp.buttons[button]
}
// Repeated returns whether a repeat event has been triggered on button.
//
// Repeat event occurs repeatedly when a button is held down for some time.
func (w *Window) Repeated(button Button) bool {
return w.currInp.repeat[button]
} }
// MousePosition returns the current mouse position in the Window's Bounds. // MousePosition returns the current mouse position in the Window's Bounds.
@ -35,42 +26,11 @@ func (w *Window) MousePosition() pixel.Vec {
return w.currInp.mouse return w.currInp.mouse
} }
// MousePreviousPosition returns the previous mouse position in the Window's Bounds.
func (w *Window) MousePreviousPosition() pixel.Vec {
return w.prevInp.mouse
}
// SetMousePosition positions the mouse cursor anywhere within the Window's Bounds.
func (w *Window) SetMousePosition(v pixel.Vec) {
mainthread.Call(func() {
if (v.X >= 0 && v.X <= w.bounds.W()) &&
(v.Y >= 0 && v.Y <= w.bounds.H()) {
w.window.SetCursorPos(
v.X+w.bounds.Min.X,
(w.bounds.H()-v.Y)+w.bounds.Min.Y,
)
w.prevInp.mouse = v
w.currInp.mouse = v
w.tempInp.mouse = v
}
})
}
// MouseInsideWindow returns true if the mouse position is within the Window's Bounds.
func (w *Window) MouseInsideWindow() bool {
return w.cursorInsideWindow
}
// MouseScroll returns the mouse scroll amount (in both axes) since the last call to Window.Update. // MouseScroll returns the mouse scroll amount (in both axes) since the last call to Window.Update.
func (w *Window) MouseScroll() pixel.Vec { func (w *Window) MouseScroll() pixel.Vec {
return w.currInp.scroll return w.currInp.scroll
} }
// Typed returns the text typed on the keyboard since the last call to Window.Update.
func (w *Window) Typed() string {
return w.currInp.typed
}
// Button is a keyboard or mouse button. Why distinguish? // Button is a keyboard or mouse button. Why distinguish?
type Button int type Button int
@ -362,11 +322,9 @@ func (w *Window) initInput() {
w.window.SetMouseButtonCallback(func(_ *glfw.Window, button glfw.MouseButton, action glfw.Action, mod glfw.ModifierKey) { w.window.SetMouseButtonCallback(func(_ *glfw.Window, button glfw.MouseButton, action glfw.Action, mod glfw.ModifierKey) {
switch action { switch action {
case glfw.Press: case glfw.Press:
w.tempPressEvents[Button(button)] = true w.currInp.buttons[Button(button)] = true
w.tempInp.buttons[Button(button)] = true
case glfw.Release: case glfw.Release:
w.tempReleaseEvents[Button(button)] = true w.currInp.buttons[Button(button)] = false
w.tempInp.buttons[Button(button)] = false
} }
}) })
@ -376,74 +334,38 @@ func (w *Window) initInput() {
} }
switch action { switch action {
case glfw.Press: case glfw.Press:
w.tempPressEvents[Button(key)] = true w.currInp.buttons[Button(key)] = true
w.tempInp.buttons[Button(key)] = true
case glfw.Release: case glfw.Release:
w.tempReleaseEvents[Button(key)] = true w.currInp.buttons[Button(key)] = false
w.tempInp.buttons[Button(key)] = false
case glfw.Repeat:
w.tempInp.repeat[Button(key)] = true
} }
}) })
w.window.SetCursorEnterCallback(func(_ *glfw.Window, entered bool) {
w.cursorInsideWindow = entered
})
w.window.SetCursorPosCallback(func(_ *glfw.Window, x, y float64) { w.window.SetCursorPosCallback(func(_ *glfw.Window, x, y float64) {
w.tempInp.mouse = pixel.V( w.currInp.mouse = pixel.V(
x+w.bounds.Min.X, x+w.bounds.Min.X(),
(w.bounds.H()-y)+w.bounds.Min.Y, (w.bounds.H()-y)+w.bounds.Min.Y(),
) )
}) })
w.window.SetScrollCallback(func(_ *glfw.Window, xoff, yoff float64) { w.window.SetScrollCallback(func(_ *glfw.Window, xoff, yoff float64) {
w.tempInp.scroll.X += xoff w.currInp.scroll += pixel.V(xoff, yoff)
w.tempInp.scroll.Y += yoff
})
w.window.SetCharCallback(func(_ *glfw.Window, r rune) {
w.tempInp.typed += string(r)
}) })
}) })
} }
// UpdateInput polls window events. Call this function to poll window events func (w *Window) updateInput() {
// without swapping buffers. Note that the Update method invokes UpdateInput. // copy temp to prev
func (w *Window) UpdateInput() { w.prevInp = w.tempInp
// zero current scroll (but keep what was added in callbacks outside of this function)
w.currInp.scroll -= w.tempInp.scroll
// get events (usually calls callbacks, but callbacks can be called outside too)
mainthread.Call(func() { mainthread.Call(func() {
glfw.PollEvents() glfw.PollEvents()
}) })
w.doUpdateInput()
} // cache current state to temp (so that if there are callbacks outside this function,
// everything works)
// UpdateInputWait blocks until an event is received or a timeout. If timeout is 0 w.tempInp = w.currInp
// then it will wait indefinitely
func (w *Window) UpdateInputWait(timeout time.Duration) {
mainthread.Call(func() {
if timeout <= 0 {
glfw.WaitEvents()
} else {
glfw.WaitEventsTimeout(timeout.Seconds())
}
})
w.doUpdateInput()
}
// internal input bookkeeping
func (w *Window) doUpdateInput() {
w.prevInp = w.currInp
w.currInp = w.tempInp
w.pressEvents = w.tempPressEvents
w.releaseEvents = w.tempReleaseEvents
// Clear last frame's temporary status
w.tempPressEvents = [KeyLast + 1]bool{}
w.tempReleaseEvents = [KeyLast + 1]bool{}
w.tempInp.repeat = [KeyLast + 1]bool{}
w.tempInp.scroll = pixel.ZV
w.tempInp.typed = ""
w.updateJoystickInput()
} }

View File

@ -1,193 +0,0 @@
package pixelgl
import (
"github.com/go-gl/glfw/v3.3/glfw"
)
// Joystick is a joystick or controller (gamepad).
type Joystick int
// List all of the joysticks.
const (
Joystick1 = Joystick(glfw.Joystick1)
Joystick2 = Joystick(glfw.Joystick2)
Joystick3 = Joystick(glfw.Joystick3)
Joystick4 = Joystick(glfw.Joystick4)
Joystick5 = Joystick(glfw.Joystick5)
Joystick6 = Joystick(glfw.Joystick6)
Joystick7 = Joystick(glfw.Joystick7)
Joystick8 = Joystick(glfw.Joystick8)
Joystick9 = Joystick(glfw.Joystick9)
Joystick10 = Joystick(glfw.Joystick10)
Joystick11 = Joystick(glfw.Joystick11)
Joystick12 = Joystick(glfw.Joystick12)
Joystick13 = Joystick(glfw.Joystick13)
Joystick14 = Joystick(glfw.Joystick14)
Joystick15 = Joystick(glfw.Joystick15)
Joystick16 = Joystick(glfw.Joystick16)
JoystickLast = Joystick(glfw.JoystickLast)
)
// GamepadAxis corresponds to a gamepad axis.
type GamepadAxis int
// Gamepad axis IDs.
const (
AxisLeftX = GamepadAxis(glfw.AxisLeftX)
AxisLeftY = GamepadAxis(glfw.AxisLeftY)
AxisRightX = GamepadAxis(glfw.AxisRightX)
AxisRightY = GamepadAxis(glfw.AxisRightY)
AxisLeftTrigger = GamepadAxis(glfw.AxisLeftTrigger)
AxisRightTrigger = GamepadAxis(glfw.AxisRightTrigger)
AxisLast = GamepadAxis(glfw.AxisLast)
)
// GamepadButton corresponds to a gamepad button.
type GamepadButton int
// Gamepad button IDs.
const (
ButtonA = GamepadButton(glfw.ButtonA)
ButtonB = GamepadButton(glfw.ButtonB)
ButtonX = GamepadButton(glfw.ButtonX)
ButtonY = GamepadButton(glfw.ButtonY)
ButtonLeftBumper = GamepadButton(glfw.ButtonLeftBumper)
ButtonRightBumper = GamepadButton(glfw.ButtonRightBumper)
ButtonBack = GamepadButton(glfw.ButtonBack)
ButtonStart = GamepadButton(glfw.ButtonStart)
ButtonGuide = GamepadButton(glfw.ButtonGuide)
ButtonLeftThumb = GamepadButton(glfw.ButtonLeftThumb)
ButtonRightThumb = GamepadButton(glfw.ButtonRightThumb)
ButtonDpadUp = GamepadButton(glfw.ButtonDpadUp)
ButtonDpadRight = GamepadButton(glfw.ButtonDpadRight)
ButtonDpadDown = GamepadButton(glfw.ButtonDpadDown)
ButtonDpadLeft = GamepadButton(glfw.ButtonDpadLeft)
ButtonLast = GamepadButton(glfw.ButtonLast)
ButtonCross = GamepadButton(glfw.ButtonCross)
ButtonCircle = GamepadButton(glfw.ButtonCircle)
ButtonSquare = GamepadButton(glfw.ButtonSquare)
ButtonTriangle = GamepadButton(glfw.ButtonTriangle)
)
// JoystickPresent returns if the joystick is currently connected.
//
// This API is experimental.
func (w *Window) JoystickPresent(js Joystick) bool {
return w.currJoy.connected[js]
}
// JoystickName returns the name of the joystick. A disconnected joystick will return an
// empty string.
//
// This API is experimental.
func (w *Window) JoystickName(js Joystick) string {
return w.currJoy.name[js]
}
// JoystickButtonCount returns the number of buttons a connected joystick has.
//
// This API is experimental.
func (w *Window) JoystickButtonCount(js Joystick) int {
return len(w.currJoy.buttons[js])
}
// JoystickAxisCount returns the number of axes a connected joystick has.
//
// This API is experimental.
func (w *Window) JoystickAxisCount(js Joystick) int {
return len(w.currJoy.axis[js])
}
// JoystickPressed returns whether the joystick Button is currently pressed down.
// If the button index is out of range, this will return false.
//
// This API is experimental.
func (w *Window) JoystickPressed(js Joystick, button GamepadButton) bool {
return w.currJoy.getButton(js, int(button))
}
// JoystickJustPressed returns whether the joystick Button has just been pressed down.
// If the button index is out of range, this will return false.
//
// This API is experimental.
func (w *Window) JoystickJustPressed(js Joystick, button GamepadButton) bool {
return w.currJoy.getButton(js, int(button)) && !w.prevJoy.getButton(js, int(button))
}
// JoystickJustReleased returns whether the joystick Button has just been released up.
// If the button index is out of range, this will return false.
//
// This API is experimental.
func (w *Window) JoystickJustReleased(js Joystick, button GamepadButton) bool {
return !w.currJoy.getButton(js, int(button)) && w.prevJoy.getButton(js, int(button))
}
// JoystickAxis returns the value of a joystick axis at the last call to Window.Update.
// If the axis index is out of range, this will return 0.
//
// This API is experimental.
func (w *Window) JoystickAxis(js Joystick, axis GamepadAxis) float64 {
return w.currJoy.getAxis(js, int(axis))
}
// Used internally during Window.UpdateInput to update the state of the joysticks.
func (w *Window) updateJoystickInput() {
for js := Joystick1; js <= JoystickLast; js++ {
// Determine and store if the joystick was connected
joystickPresent := glfw.Joystick(js).Present()
w.tempJoy.connected[js] = joystickPresent
if joystickPresent {
if glfw.Joystick(js).IsGamepad() {
gamepadInputs := glfw.Joystick(js).GetGamepadState()
w.tempJoy.buttons[js] = gamepadInputs.Buttons[:]
w.tempJoy.axis[js] = gamepadInputs.Axes[:]
} else {
w.tempJoy.buttons[js] = glfw.Joystick(js).GetButtons()
w.tempJoy.axis[js] = glfw.Joystick(js).GetAxes()
}
if !w.currJoy.connected[js] {
// The joystick was recently connected, we get the name
w.tempJoy.name[js] = glfw.Joystick(js).GetName()
} else {
// Use the name from the previous one
w.tempJoy.name[js] = w.currJoy.name[js]
}
} else {
w.tempJoy.buttons[js] = []glfw.Action{}
w.tempJoy.axis[js] = []float32{}
w.tempJoy.name[js] = ""
}
}
w.prevJoy = w.currJoy
w.currJoy = w.tempJoy
}
type joystickState struct {
connected [JoystickLast + 1]bool
name [JoystickLast + 1]string
buttons [JoystickLast + 1][]glfw.Action
axis [JoystickLast + 1][]float32
}
// Returns if a button on a joystick is down, returning false if the button or joystick is invalid.
func (js *joystickState) getButton(joystick Joystick, button int) bool {
// Check that the joystick and button is valid, return false by default
if js.buttons[joystick] == nil || button >= len(js.buttons[joystick]) || button < 0 {
return false
}
return js.buttons[joystick][byte(button)] == glfw.Press
}
// Returns the value of a joystick axis, returning 0 if the button or joystick is invalid.
func (js *joystickState) getAxis(joystick Joystick, axis int) float64 {
// Check that the joystick and axis is valid, return 0 by default.
if js.axis[joystick] == nil || axis >= len(js.axis[joystick]) || axis < 0 {
return 0
}
return float64(js.axis[joystick][axis])
}

View File

@ -2,7 +2,7 @@ package pixelgl
import ( import (
"github.com/faiface/mainthread" "github.com/faiface/mainthread"
"github.com/go-gl/glfw/v3.3/glfw" "github.com/go-gl/glfw/v3.2/glfw"
) )
// Monitor represents a physical display attached to your computer. // Monitor represents a physical display attached to your computer.
@ -10,17 +10,6 @@ type Monitor struct {
monitor *glfw.Monitor monitor *glfw.Monitor
} }
// VideoMode represents all properties of a video mode and is
// associated with a monitor if it is used in fullscreen mode.
type VideoMode struct {
// Width is the width of the vide mode in pixels.
Width int
// Height is the height of the video mode in pixels.
Height int
// RefreshRate holds the refresh rate of the associated monitor in Hz.
RefreshRate int
}
// PrimaryMonitor returns the main monitor (usually the one with the taskbar and stuff). // PrimaryMonitor returns the main monitor (usually the one with the taskbar and stuff).
func PrimaryMonitor() *Monitor { func PrimaryMonitor() *Monitor {
var monitor *glfw.Monitor var monitor *glfw.Monitor
@ -106,19 +95,3 @@ func (m *Monitor) RefreshRate() (rate float64) {
rate = float64(mode.RefreshRate) rate = float64(mode.RefreshRate)
return return
} }
// VideoModes returns all available video modes for the monitor.
func (m *Monitor) VideoModes() (vmodes []VideoMode) {
var modes []*glfw.VidMode
mainthread.Call(func() {
modes = m.monitor.GetVideoModes()
})
for _, mode := range modes {
vmodes = append(vmodes, VideoMode{
Width: mode.Width,
Height: mode.Height,
RefreshRate: mode.RefreshRate,
})
}
return
}

View File

@ -2,7 +2,7 @@ package pixelgl
import ( import (
"github.com/faiface/mainthread" "github.com/faiface/mainthread"
"github.com/go-gl/glfw/v3.3/glfw" "github.com/go-gl/glfw/v3.2/glfw"
"github.com/pkg/errors" "github.com/pkg/errors"
) )

View File

@ -7,9 +7,9 @@ import (
) )
func intBounds(bounds pixel.Rect) (x, y, w, h int) { func intBounds(bounds pixel.Rect) (x, y, w, h int) {
x0 := int(math.Floor(bounds.Min.X)) x0 := int(math.Floor(bounds.Min.X()))
y0 := int(math.Floor(bounds.Min.Y)) y0 := int(math.Floor(bounds.Min.Y()))
x1 := int(math.Ceil(bounds.Max.X)) x1 := int(math.Ceil(bounds.Max.X()))
y1 := int(math.Ceil(bounds.Max.Y)) y1 := int(math.Ceil(bounds.Max.Y()))
return x0, y0, x1 - x0, y1 - y0 return x0, y0, x1 - x0, y1 - y0
} }

View File

@ -1,16 +1,13 @@
package pixelgl package pixelgl
import ( import (
"fmt"
"image"
"image/color" "image/color"
"runtime" "runtime"
"github.com/faiface/glhf" "github.com/faiface/glhf"
"github.com/faiface/mainthread" "github.com/faiface/mainthread"
"github.com/faiface/pixel" "github.com/faiface/pixel"
"github.com/go-gl/gl/v3.3-core/gl" "github.com/go-gl/glfw/v3.2/glfw"
"github.com/go-gl/glfw/v3.3/glfw"
"github.com/pkg/errors" "github.com/pkg/errors"
) )
@ -23,92 +20,42 @@ type WindowConfig struct {
// Title at the top of the Window. // Title at the top of the Window.
Title string Title string
// Icon specifies the icon images available to be used by the window. This is usually
// displayed in the top bar of the window or in the task bar of the desktop environment.
//
// If passed one image, it will use that image, if passed an array of images those of or
// closest to the sizes desired by the system are selected. The desired image sizes varies
// depending on platform and system settings. The selected images will be rescaled as
// needed. Good sizes include 16x16, 32x32 and 48x48.
//
// Note: Setting this value doesn't have an effect on OSX. You'll need to set the icon when
// bundling your application for release.
Icon []pixel.Picture
// Bounds specify the bounds of the Window in pixels. // Bounds specify the bounds of the Window in pixels.
Bounds pixel.Rect Bounds pixel.Rect
// Initial window position
Position pixel.Vec
// If set to nil, the Window will be windowed. Otherwise it will be fullscreen on the // If set to nil, the Window will be windowed. Otherwise it will be fullscreen on the
// specified Monitor. // specified Monitor.
Monitor *Monitor Monitor *Monitor
// Resizable specifies whether the window will be resizable by the user. // Whether the Window is resizable.
Resizable bool Resizable bool
// Undecorated Window omits the borders and decorations (close button, etc.). // Undecorated Window ommits the borders and decorations (close button, etc.).
Undecorated bool Undecorated bool
// NoIconify specifies whether fullscreen windows should not automatically
// iconify (and restore the previous video mode) on focus loss.
NoIconify bool
// AlwaysOnTop specifies whether the windowed mode window will be floating
// above other regular windows, also called topmost or always-on-top.
// This is intended primarily for debugging purposes and cannot be used to
// implement proper full screen windows.
AlwaysOnTop bool
// TransparentFramebuffer specifies whether the window framebuffer will be
// transparent. If enabled and supported by the system, the window
// framebuffer alpha channel will be used to combine the framebuffer with
// the background. This does not affect window decorations.
TransparentFramebuffer bool
// VSync (vertical synchronization) synchronizes Window's framerate with the framerate of // VSync (vertical synchronization) synchronizes Window's framerate with the framerate of
// the monitor. // the monitor.
VSync bool VSync bool
// Maximized specifies whether the window is maximized.
Maximized bool
// Invisible specifies whether the window will be initially hidden.
// You can make the window visible later using Window.Show().
Invisible bool
//SamplesMSAA specifies the level of MSAA to be used. Must be one of 0, 2, 4, 8, 16. 0 to disable.
SamplesMSAA int
} }
// Window is a window handler. Use this type to manipulate a window (input, drawing, etc.). // Window is a window handler. Use this type to manipulate a window (input, drawing, etc.).
type Window struct { type Window struct {
window *glfw.Window window *glfw.Window
bounds pixel.Rect bounds pixel.Rect
canvas *Canvas canvas *Canvas
vsync bool vsync bool
cursorVisible bool
cursorInsideWindow bool
// need to save these to correctly restore a fullscreen window // need to save these to correctly restore a fullscreen window
restore struct { restore struct {
xpos, ypos, width, height int xpos, ypos, width, height int
} }
prevInp, currInp, tempInp struct { prevInp, tempInp, currInp struct {
mouse pixel.Vec mouse pixel.Vec
buttons [KeyLast + 1]bool buttons [KeyLast + 1]bool
repeat [KeyLast + 1]bool
scroll pixel.Vec scroll pixel.Vec
typed string
} }
pressEvents, tempPressEvents [KeyLast + 1]bool
releaseEvents, tempReleaseEvents [KeyLast + 1]bool
prevJoy, currJoy, tempJoy joystickState
} }
var currWin *Window var currWin *Window
@ -122,18 +69,7 @@ func NewWindow(cfg WindowConfig) (*Window, error) {
false: glfw.False, false: glfw.False,
} }
w := &Window{bounds: cfg.Bounds, cursorVisible: true} w := &Window{bounds: cfg.Bounds}
flag := false
for _, v := range []int{0, 2, 4, 8, 16} {
if cfg.SamplesMSAA == v {
flag = true
break
}
}
if !flag {
return nil, fmt.Errorf("invalid value '%v' for msaaSamples", cfg.SamplesMSAA)
}
err := mainthread.CallErr(func() error { err := mainthread.CallErr(func() error {
var err error var err error
@ -145,16 +81,6 @@ func NewWindow(cfg WindowConfig) (*Window, error) {
glfw.WindowHint(glfw.Resizable, bool2int[cfg.Resizable]) glfw.WindowHint(glfw.Resizable, bool2int[cfg.Resizable])
glfw.WindowHint(glfw.Decorated, bool2int[!cfg.Undecorated]) glfw.WindowHint(glfw.Decorated, bool2int[!cfg.Undecorated])
glfw.WindowHint(glfw.Floating, bool2int[cfg.AlwaysOnTop])
glfw.WindowHint(glfw.AutoIconify, bool2int[!cfg.NoIconify])
glfw.WindowHint(glfw.TransparentFramebuffer, bool2int[cfg.TransparentFramebuffer])
glfw.WindowHint(glfw.Maximized, bool2int[cfg.Maximized])
glfw.WindowHint(glfw.Visible, bool2int[!cfg.Invisible])
glfw.WindowHint(glfw.Samples, cfg.SamplesMSAA)
if cfg.Position.X != 0 || cfg.Position.Y != 0 {
glfw.WindowHint(glfw.Visible, glfw.False)
}
var share *glfw.Window var share *glfw.Window
if currWin != nil { if currWin != nil {
@ -172,15 +98,8 @@ func NewWindow(cfg WindowConfig) (*Window, error) {
return err return err
} }
if cfg.Position.X != 0 || cfg.Position.Y != 0 {
w.window.SetPos(int(cfg.Position.X), int(cfg.Position.Y))
w.window.Show()
}
// enter the OpenGL context // enter the OpenGL context
w.begin() w.begin()
glhf.Init()
gl.Enable(gl.MULTISAMPLE)
w.end() w.end()
return nil return nil
@ -189,17 +108,6 @@ func NewWindow(cfg WindowConfig) (*Window, error) {
return nil, errors.Wrap(err, "creating window failed") return nil, errors.Wrap(err, "creating window failed")
} }
if len(cfg.Icon) > 0 {
imgs := make([]image.Image, len(cfg.Icon))
for i, icon := range cfg.Icon {
pic := pixel.PictureDataFromPicture(icon)
imgs[i] = pic.Image()
}
mainthread.Call(func() {
w.window.SetIcon(imgs)
})
}
w.SetVSync(cfg.VSync) w.SetVSync(cfg.VSync)
w.initInput() w.initInput()
@ -222,31 +130,13 @@ func (w *Window) Destroy() {
// Update swaps buffers and polls events. Call this method at the end of each frame. // Update swaps buffers and polls events. Call this method at the end of each frame.
func (w *Window) Update() { func (w *Window) Update() {
w.SwapBuffers()
w.UpdateInput()
}
// ClipboardText returns the current value of the systems clipboard.
func (w *Window) ClipboardText() string {
return w.window.GetClipboardString()
}
// SetClipboardText passes the given string to the underlying glfw window to set the
// systems clipboard.
func (w *Window) SetClipboardText(text string) {
w.window.SetClipboardString(text)
}
// SwapBuffers swaps buffers. Call this to swap buffers without polling window events.
// Note that Update invokes SwapBuffers.
func (w *Window) SwapBuffers() {
mainthread.Call(func() { mainthread.Call(func() {
_, _, oldW, oldH := intBounds(w.bounds) _, _, oldW, oldH := intBounds(w.bounds)
newW, newH := w.window.GetSize() newW, newH := w.window.GetSize()
w.bounds = w.bounds.ResizedMin(w.bounds.Size().Add(pixel.V( w.bounds = w.bounds.ResizedMin(w.bounds.Size() + pixel.V(
float64(newW-oldW), float64(newW-oldW),
float64(newH-oldH), float64(newH-oldH),
))) ))
}) })
w.canvas.SetBounds(w.bounds) w.canvas.SetBounds(w.bounds)
@ -274,6 +164,8 @@ func (w *Window) SwapBuffers() {
w.window.SwapBuffers() w.window.SwapBuffers()
w.end() w.end()
}) })
w.updateInput()
} }
// SetClosed sets the closed flag of the Window. // SetClosed sets the closed flag of the Window.
@ -314,29 +206,6 @@ func (w *Window) SetBounds(bounds pixel.Rect) {
}) })
} }
// SetPos sets the position, in screen coordinates, of the upper-left corner
// of the client area of the window. Position can be fractional, but the actual position
// of the window will be rounded to integers.
//
// If it is a full screen window, this function does nothing.
func (w *Window) SetPos(pos pixel.Vec) {
mainthread.Call(func() {
left, top := int(pos.X), int(pos.Y)
w.window.SetPos(left, top)
})
}
// GetPos gets the position, in screen coordinates, of the upper-left corner
// of the client area of the window. The position is rounded to integers.
func (w *Window) GetPos() pixel.Vec {
var v pixel.Vec
mainthread.Call(func() {
x, y := w.window.GetPos()
v = pixel.V(float64(x), float64(y))
})
return v
}
// Bounds returns the current bounds of the Window. // Bounds returns the current bounds of the Window.
func (w *Window) Bounds() pixel.Rect { func (w *Window) Bounds() pixel.Rect {
return w.bounds return w.bounds
@ -422,36 +291,11 @@ func (w *Window) VSync() bool {
return w.vsync return w.vsync
} }
// SetCursorVisible sets the visibility of the mouse cursor inside the Window client area.
func (w *Window) SetCursorVisible(visible bool) {
w.cursorVisible = visible
mainthread.Call(func() {
if visible {
w.window.SetInputMode(glfw.CursorMode, glfw.CursorNormal)
} else {
w.window.SetInputMode(glfw.CursorMode, glfw.CursorHidden)
}
})
}
// SetCursorDisabled hides the cursor and provides unlimited virtual cursor movement
// make cursor visible using SetCursorVisible
func (w *Window) SetCursorDisabled() {
w.cursorVisible = false
mainthread.Call(func() {
w.window.SetInputMode(glfw.CursorMode, glfw.CursorDisabled)
})
}
// CursorVisible returns the visibility status of the mouse cursor.
func (w *Window) CursorVisible() bool {
return w.cursorVisible
}
// Note: must be called inside the main thread. // Note: must be called inside the main thread.
func (w *Window) begin() { func (w *Window) begin() {
if currWin != w { if currWin != w {
w.window.MakeContextCurrent() w.window.MakeContextCurrent()
glhf.Init()
currWin = w currWin = w
} }
} }
@ -513,32 +357,3 @@ func (w *Window) Clear(c color.Color) {
func (w *Window) Color(at pixel.Vec) pixel.RGBA { func (w *Window) Color(at pixel.Vec) pixel.RGBA {
return w.canvas.Color(at) return w.canvas.Color(at)
} }
// Canvas returns the window's underlying Canvas
func (w *Window) Canvas() *Canvas {
return w.canvas
}
// Show makes the window visible, if it was previously hidden. If the window is
// already visible or is in full screen mode, this function does nothing.
func (w *Window) Show() {
mainthread.Call(func() {
w.window.Show()
})
}
// Clipboard returns the contents of the system clipboard.
func (w *Window) Clipboard() string {
var clipboard string
mainthread.Call(func() {
clipboard = w.window.GetClipboardString()
})
return clipboard
}
// SetClipboardString sets the system clipboard to the specified UTF-8 encoded string.
func (w *Window) SetClipboard(str string) {
mainthread.Call(func() {
w.window.SetClipboardString(str)
})
}

View File

@ -1,284 +0,0 @@
package pixel
import (
"fmt"
"math"
)
// Rect is a 2D rectangle aligned with the axes of the coordinate system. It is defined by two
// points, Min and Max.
//
// The invariant should hold, that Max's components are greater or equal than Min's components
// respectively.
type Rect struct {
Min, Max Vec
}
// ZR is a zero rectangle.
var ZR = Rect{Min: ZV, Max: ZV}
// R returns a new Rect with given the Min and Max coordinates.
//
// Note that the returned rectangle is not automatically normalized.
func R(minX, minY, maxX, maxY float64) Rect {
return Rect{
Min: Vec{minX, minY},
Max: Vec{maxX, maxY},
}
}
// String returns the string representation of the Rect.
//
// r := pixel.R(100, 50, 200, 300)
// r.String() // returns "Rect(100, 50, 200, 300)"
// fmt.Println(r) // Rect(100, 50, 200, 300)
func (r Rect) String() string {
return fmt.Sprintf("Rect(%v, %v, %v, %v)", r.Min.X, r.Min.Y, r.Max.X, r.Max.Y)
}
// Norm returns the Rect in normal form, such that Max is component-wise greater or equal than Min.
func (r Rect) Norm() Rect {
return Rect{
Min: Vec{
math.Min(r.Min.X, r.Max.X),
math.Min(r.Min.Y, r.Max.Y),
},
Max: Vec{
math.Max(r.Min.X, r.Max.X),
math.Max(r.Min.Y, r.Max.Y),
},
}
}
// W returns the width of the Rect.
func (r Rect) W() float64 {
return r.Max.X - r.Min.X
}
// H returns the height of the Rect.
func (r Rect) H() float64 {
return r.Max.Y - r.Min.Y
}
// Size returns the vector of width and height of the Rect.
func (r Rect) Size() Vec {
return V(r.W(), r.H())
}
// Area returns the area of r. If r is not normalized, area may be negative.
func (r Rect) Area() float64 {
return r.W() * r.H()
}
// Edges will return the four lines which make up the edges of the rectangle.
func (r Rect) Edges() [4]Line {
corners := r.Vertices()
return [4]Line{
{A: corners[0], B: corners[1]},
{A: corners[1], B: corners[2]},
{A: corners[2], B: corners[3]},
{A: corners[3], B: corners[0]},
}
}
// Anchor is a vector used to define anchors, such as `Center`, `Top`, `TopRight`, etc.
type Anchor Vec
var (
Center = Anchor{0.5, 0.5}
Top = Anchor{0.5, 0}
TopRight = Anchor{0, 0}
Right = Anchor{0, 0.5}
BottomRight = Anchor{0, 1}
Bottom = Anchor{0.5, 1}
BottomLeft = Anchor{1, 1}
Left = Anchor{1, 0.5}
TopLeft = Anchor{1, 0}
)
var anchorStrings map[Anchor]string = map[Anchor]string{
Center: "center",
Top: "top",
TopRight: "top-right",
Right: "right",
BottomRight: "bottom-right",
Bottom: "bottom",
BottomLeft: "bottom-left",
Left: "left",
TopLeft: "top-left",
}
// String returns the string representation of an anchor.
func (anchor Anchor) String() string {
return anchorStrings[anchor]
}
var oppositeAnchors map[Anchor]Anchor = map[Anchor]Anchor{
Center: Center,
Top: Bottom,
Bottom: Top,
Right: Left,
Left: Right,
TopRight: BottomLeft,
BottomLeft: TopRight,
BottomRight: TopLeft,
TopLeft: BottomRight,
}
// Opposite returns the opposite position of the anchor (ie. Top -> Bottom; BottomLeft -> TopRight, etc.).
func (anchor Anchor) Opposite() Anchor {
return oppositeAnchors[anchor]
}
// AnchorPos returns the relative position of the given anchor.
func (r Rect) AnchorPos(anchor Anchor) Vec {
return r.Size().ScaledXY(V(0, 0).Sub(Vec(anchor)))
}
// AlignedTo returns the rect moved by the given anchor.
func (rect Rect) AlignedTo(anchor Anchor) Rect {
return rect.Moved(rect.AnchorPos(anchor))
}
// Center returns the position of the center of the Rect.
// `rect.Center()` is equivalent to `rect.Anchor(pixel.Anchor.Center)`
func (r Rect) Center() Vec {
return Lerp(r.Min, r.Max, 0.5)
}
// Moved returns the Rect moved (both Min and Max) by the given vector delta.
func (r Rect) Moved(delta Vec) Rect {
return Rect{
Min: r.Min.Add(delta),
Max: r.Max.Add(delta),
}
}
// Resized returns the Rect resized to the given size while keeping the position of the given
// anchor.
//
// r.Resized(r.Min, size) // resizes while keeping the position of the lower-left corner
// r.Resized(r.Max, size) // same with the top-right corner
// r.Resized(r.Center(), size) // resizes around the center
//
// This function does not make sense for resizing a rectangle of zero area and will panic. Use
// ResizedMin in the case of zero area.
func (r Rect) Resized(anchor, size Vec) Rect {
if r.W()*r.H() == 0 {
panic(fmt.Errorf("(%T).Resize: zero area", r))
}
fraction := Vec{size.X / r.W(), size.Y / r.H()}
return Rect{
Min: anchor.Add(r.Min.Sub(anchor).ScaledXY(fraction)),
Max: anchor.Add(r.Max.Sub(anchor).ScaledXY(fraction)),
}
}
// ResizedMin returns the Rect resized to the given size while keeping the position of the Rect's
// Min.
//
// Sizes of zero area are safe here.
func (r Rect) ResizedMin(size Vec) Rect {
return Rect{
Min: r.Min,
Max: r.Min.Add(size),
}
}
// Contains checks whether a vector u is contained within this Rect (including it's borders).
func (r Rect) Contains(u Vec) bool {
return r.Min.X <= u.X && u.X <= r.Max.X && r.Min.Y <= u.Y && u.Y <= r.Max.Y
}
// Union returns the minimal Rect which covers both r and s. Rects r and s must be normalized.
func (r Rect) Union(s Rect) Rect {
return R(
math.Min(r.Min.X, s.Min.X),
math.Min(r.Min.Y, s.Min.Y),
math.Max(r.Max.X, s.Max.X),
math.Max(r.Max.Y, s.Max.Y),
)
}
// Intersect returns the maximal Rect which is covered by both r and s. Rects r and s must be normalized.
//
// If r and s don't overlap, this function returns a zero-rectangle.
func (r Rect) Intersect(s Rect) Rect {
t := R(
math.Max(r.Min.X, s.Min.X),
math.Max(r.Min.Y, s.Min.Y),
math.Min(r.Max.X, s.Max.X),
math.Min(r.Max.Y, s.Max.Y),
)
if t.Min.X >= t.Max.X || t.Min.Y >= t.Max.Y {
return ZR
}
return t
}
// Intersects returns whether or not the given Rect intersects at any point with this Rect.
//
// This function is overall about 5x faster than Intersect, so it is better
// to use if you have no need for the returned Rect from Intersect.
func (r Rect) Intersects(s Rect) bool {
return !(s.Max.X <= r.Min.X ||
s.Min.X >= r.Max.X ||
s.Max.Y <= r.Min.Y ||
s.Min.Y >= r.Max.Y)
}
// IntersectCircle returns a minimal required Vector, such that moving the rect by that vector would stop the Circle
// and the Rect intersecting. This function returns a zero-vector if the Circle and Rect do not overlap, and if only
// the perimeters touch.
//
// This function will return a non-zero vector if:
// - The Rect contains the Circle, partially or fully
// - The Circle contains the Rect, partially of fully
func (r Rect) IntersectCircle(c Circle) Vec {
return c.IntersectRect(r).Scaled(-1)
}
// IntersectLine will return the shortest Vec such that if the Rect is moved by the Vec returned, the Line and Rect no
// longer intersect.
func (r Rect) IntersectLine(l Line) Vec {
return l.IntersectRect(r).Scaled(-1)
}
// IntersectionPoints returns all the points where the Rect intersects with the line provided. This can be zero, one or
// two points, depending on the location of the shapes. The points of intersection will be returned in order of
// closest-to-l.A to closest-to-l.B.
func (r Rect) IntersectionPoints(l Line) []Vec {
// Use map keys to ensure unique points
pointMap := make(map[Vec]struct{})
for _, edge := range r.Edges() {
if intersect, ok := l.Intersect(edge); ok {
pointMap[intersect] = struct{}{}
}
}
points := make([]Vec, 0, len(pointMap))
for point := range pointMap {
points = append(points, point)
}
// Order the points
if len(points) == 2 {
if points[1].To(l.A).Len() < points[0].To(l.A).Len() {
return []Vec{points[1], points[0]}
}
}
return points
}
// Vertices returns a slice of the four corners which make up the rectangle.
func (r Rect) Vertices() [4]Vec {
return [4]Vec{
r.Min,
V(r.Min.X, r.Max.Y),
r.Max,
V(r.Max.X, r.Min.Y),
}
}

View File

@ -1,384 +0,0 @@
package pixel_test
import (
"fmt"
"reflect"
"testing"
"github.com/faiface/pixel"
)
func TestRect_Resize(t *testing.T) {
type rectTestTransform struct {
name string
f func(pixel.Rect) pixel.Rect
}
// rectangles
squareAroundOrigin := pixel.R(-10, -10, 10, 10)
squareAround2020 := pixel.R(10, 10, 30, 30)
rectangleAroundOrigin := pixel.R(-20, -10, 20, 10)
rectangleAround2020 := pixel.R(0, 10, 40, 30)
// resize transformations
resizeByHalfAroundCenter := rectTestTransform{"by half around center", func(rect pixel.Rect) pixel.Rect {
return rect.Resized(rect.Center(), rect.Size().Scaled(0.5))
}}
resizeByHalfAroundMin := rectTestTransform{"by half around Min", func(rect pixel.Rect) pixel.Rect {
return rect.Resized(rect.Min, rect.Size().Scaled(0.5))
}}
resizeByHalfAroundMax := rectTestTransform{"by half around Max", func(rect pixel.Rect) pixel.Rect {
return rect.Resized(rect.Max, rect.Size().Scaled(0.5))
}}
resizeByHalfAroundMiddleOfLeftSide := rectTestTransform{"by half around middle of left side", func(rect pixel.Rect) pixel.Rect {
return rect.Resized(pixel.V(rect.Min.X, rect.Center().Y), rect.Size().Scaled(0.5))
}}
resizeByHalfAroundOrigin := rectTestTransform{"by half around the origin", func(rect pixel.Rect) pixel.Rect {
return rect.Resized(pixel.ZV, rect.Size().Scaled(0.5))
}}
testCases := []struct {
input pixel.Rect
transform rectTestTransform
answer pixel.Rect
}{
{squareAroundOrigin, resizeByHalfAroundCenter, pixel.R(-5, -5, 5, 5)},
{squareAround2020, resizeByHalfAroundCenter, pixel.R(15, 15, 25, 25)},
{rectangleAroundOrigin, resizeByHalfAroundCenter, pixel.R(-10, -5, 10, 5)},
{rectangleAround2020, resizeByHalfAroundCenter, pixel.R(10, 15, 30, 25)},
{squareAroundOrigin, resizeByHalfAroundMin, pixel.R(-10, -10, 0, 0)},
{squareAround2020, resizeByHalfAroundMin, pixel.R(10, 10, 20, 20)},
{rectangleAroundOrigin, resizeByHalfAroundMin, pixel.R(-20, -10, 0, 0)},
{rectangleAround2020, resizeByHalfAroundMin, pixel.R(0, 10, 20, 20)},
{squareAroundOrigin, resizeByHalfAroundMax, pixel.R(0, 0, 10, 10)},
{squareAround2020, resizeByHalfAroundMax, pixel.R(20, 20, 30, 30)},
{rectangleAroundOrigin, resizeByHalfAroundMax, pixel.R(0, 0, 20, 10)},
{rectangleAround2020, resizeByHalfAroundMax, pixel.R(20, 20, 40, 30)},
{squareAroundOrigin, resizeByHalfAroundMiddleOfLeftSide, pixel.R(-10, -5, 0, 5)},
{squareAround2020, resizeByHalfAroundMiddleOfLeftSide, pixel.R(10, 15, 20, 25)},
{rectangleAroundOrigin, resizeByHalfAroundMiddleOfLeftSide, pixel.R(-20, -5, 0, 5)},
{rectangleAround2020, resizeByHalfAroundMiddleOfLeftSide, pixel.R(0, 15, 20, 25)},
{squareAroundOrigin, resizeByHalfAroundOrigin, pixel.R(-5, -5, 5, 5)},
{squareAround2020, resizeByHalfAroundOrigin, pixel.R(5, 5, 15, 15)},
{rectangleAroundOrigin, resizeByHalfAroundOrigin, pixel.R(-10, -5, 10, 5)},
{rectangleAround2020, resizeByHalfAroundOrigin, pixel.R(0, 5, 20, 15)},
}
for _, testCase := range testCases {
t.Run(fmt.Sprintf("Resize %v %s", testCase.input, testCase.transform.name), func(t *testing.T) {
testResult := testCase.transform.f(testCase.input)
if testResult != testCase.answer {
t.Errorf("Got: %v, wanted: %v\n", testResult, testCase.answer)
}
})
}
}
func TestRect_Edges(t *testing.T) {
type fields struct {
Min pixel.Vec
Max pixel.Vec
}
tests := []struct {
name string
fields fields
want [4]pixel.Line
}{
{
name: "Get edges",
fields: fields{Min: pixel.V(0, 0), Max: pixel.V(10, 10)},
want: [4]pixel.Line{
pixel.L(pixel.V(0, 0), pixel.V(0, 10)),
pixel.L(pixel.V(0, 10), pixel.V(10, 10)),
pixel.L(pixel.V(10, 10), pixel.V(10, 0)),
pixel.L(pixel.V(10, 0), pixel.V(0, 0)),
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := pixel.Rect{
Min: tt.fields.Min,
Max: tt.fields.Max,
}
if got := r.Edges(); !reflect.DeepEqual(got, tt.want) {
t.Errorf("Rect.Edges() = %v, want %v", got, tt.want)
}
})
}
}
func TestRect_Vertices(t *testing.T) {
type fields struct {
Min pixel.Vec
Max pixel.Vec
}
tests := []struct {
name string
fields fields
want [4]pixel.Vec
}{
{
name: "Get corners",
fields: fields{Min: pixel.V(0, 0), Max: pixel.V(10, 10)},
want: [4]pixel.Vec{
pixel.V(0, 0),
pixel.V(0, 10),
pixel.V(10, 10),
pixel.V(10, 0),
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := pixel.Rect{
Min: tt.fields.Min,
Max: tt.fields.Max,
}
if got := r.Vertices(); !reflect.DeepEqual(got, tt.want) {
t.Errorf("Rect.Vertices() = %v, want %v", got, tt.want)
}
})
}
}
func TestRect_IntersectCircle(t *testing.T) {
type fields struct {
Min pixel.Vec
Max pixel.Vec
}
type args struct {
c pixel.Circle
}
tests := []struct {
name string
fields fields
args args
want pixel.Vec
}{
{
name: "Rect.IntersectCircle(): no overlap",
fields: fields{Min: pixel.ZV, Max: pixel.V(10, 10)},
args: args{c: pixel.C(pixel.V(50, 50), 1)},
want: pixel.ZV,
},
{
name: "Rect.IntersectCircle(): circle contains rect",
fields: fields{Min: pixel.ZV, Max: pixel.V(10, 10)},
args: args{c: pixel.C(pixel.V(5, 5), 10)},
want: pixel.V(-15, 0),
},
{
name: "Rect.IntersectCircle(): rect contains circle",
fields: fields{Min: pixel.ZV, Max: pixel.V(10, 10)},
args: args{c: pixel.C(pixel.V(5, 5), 1)},
want: pixel.V(-6, 0),
},
{
name: "Rect.IntersectCircle(): circle overlaps bottom-left corner",
fields: fields{Min: pixel.ZV, Max: pixel.V(10, 10)},
args: args{c: pixel.C(pixel.V(-0.5, -0.5), 1)},
want: pixel.V(-0.2, -0.2),
},
{
name: "Rect.IntersectCircle(): circle overlaps top-left corner",
fields: fields{Min: pixel.ZV, Max: pixel.V(10, 10)},
args: args{c: pixel.C(pixel.V(-0.5, 10.5), 1)},
want: pixel.V(-0.2, 0.2),
},
{
name: "Rect.IntersectCircle(): circle overlaps bottom-right corner",
fields: fields{Min: pixel.ZV, Max: pixel.V(10, 10)},
args: args{c: pixel.C(pixel.V(10.5, -0.5), 1)},
want: pixel.V(0.2, -0.2),
},
{
name: "Rect.IntersectCircle(): circle overlaps top-right corner",
fields: fields{Min: pixel.ZV, Max: pixel.V(10, 10)},
args: args{c: pixel.C(pixel.V(10.5, 10.5), 1)},
want: pixel.V(0.2, 0.2),
},
{
name: "Rect.IntersectCircle(): circle overlaps two corners",
fields: fields{Min: pixel.ZV, Max: pixel.V(10, 10)},
args: args{c: pixel.C(pixel.V(0, 5), 6)},
want: pixel.V(6, 0),
},
{
name: "Rect.IntersectCircle(): circle overlaps left edge",
fields: fields{Min: pixel.ZV, Max: pixel.V(10, 10)},
args: args{c: pixel.C(pixel.V(0, 5), 1)},
want: pixel.V(1, 0),
},
{
name: "Rect.IntersectCircle(): circle overlaps bottom edge",
fields: fields{Min: pixel.ZV, Max: pixel.V(10, 10)},
args: args{c: pixel.C(pixel.V(5, 0), 1)},
want: pixel.V(0, 1),
},
{
name: "Rect.IntersectCircle(): circle overlaps right edge",
fields: fields{Min: pixel.ZV, Max: pixel.V(10, 10)},
args: args{c: pixel.C(pixel.V(10, 5), 1)},
want: pixel.V(-1, 0),
},
{
name: "Rect.IntersectCircle(): circle overlaps top edge",
fields: fields{Min: pixel.ZV, Max: pixel.V(10, 10)},
args: args{c: pixel.C(pixel.V(5, 10), 1)},
want: pixel.V(0, -1),
},
{
name: "Rect.IntersectCircle(): edge is tangent of left side",
fields: fields{Min: pixel.ZV, Max: pixel.V(10, 10)},
args: args{c: pixel.C(pixel.V(-1, 5), 1)},
want: pixel.ZV,
},
{
name: "Rect.IntersectCircle(): edge is tangent of top side",
fields: fields{Min: pixel.ZV, Max: pixel.V(10, 10)},
args: args{c: pixel.C(pixel.V(5, -1), 1)},
want: pixel.ZV,
},
{
name: "Rect.IntersectCircle(): circle above rectangle",
fields: fields{Min: pixel.ZV, Max: pixel.V(10, 10)},
args: args{c: pixel.C(pixel.V(5, 12), 1)},
want: pixel.ZV,
},
{
name: "Rect.IntersectCircle(): circle below rectangle",
fields: fields{Min: pixel.ZV, Max: pixel.V(10, 10)},
args: args{c: pixel.C(pixel.V(5, -2), 1)},
want: pixel.ZV,
},
{
name: "Rect.IntersectCircle(): circle left of rectangle",
fields: fields{Min: pixel.ZV, Max: pixel.V(10, 10)},
args: args{c: pixel.C(pixel.V(-1, 5), 1)},
want: pixel.ZV,
},
{
name: "Rect.IntersectCircle(): circle right of rectangle",
fields: fields{Min: pixel.ZV, Max: pixel.V(10, 10)},
args: args{c: pixel.C(pixel.V(11, 5), 1)},
want: pixel.ZV,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := pixel.Rect{
Min: tt.fields.Min,
Max: tt.fields.Max,
}
got := r.IntersectCircle(tt.args.c)
if !closeEnough(got.X, tt.want.X, 2) || !closeEnough(got.Y, tt.want.Y, 2) {
t.Errorf("Rect.IntersectCircle() = %v, want %v", got, tt.want)
}
})
}
}
func TestRect_IntersectionPoints(t *testing.T) {
type fields struct {
Min pixel.Vec
Max pixel.Vec
}
type args struct {
l pixel.Line
}
tests := []struct {
name string
fields fields
args args
want []pixel.Vec
}{
{
name: "No intersection points",
fields: fields{Min: pixel.V(1, 1), Max: pixel.V(5, 5)},
args: args{l: pixel.L(pixel.V(-5, 0), pixel.V(-2, 2))},
want: []pixel.Vec{},
},
{
name: "One intersection point",
fields: fields{Min: pixel.V(1, 1), Max: pixel.V(5, 5)},
args: args{l: pixel.L(pixel.V(2, 0), pixel.V(2, 3))},
want: []pixel.Vec{pixel.V(2, 1)},
},
{
name: "Two intersection points",
fields: fields{Min: pixel.V(1, 1), Max: pixel.V(5, 5)},
args: args{l: pixel.L(pixel.V(0, 2), pixel.V(6, 2))},
want: []pixel.Vec{pixel.V(1, 2), pixel.V(5, 2)},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := pixel.Rect{
Min: tt.fields.Min,
Max: tt.fields.Max,
}
if got := r.IntersectionPoints(tt.args.l); !reflect.DeepEqual(got, tt.want) {
t.Errorf("Rect.IntersectPoints() = %v, want %v", got, tt.want)
}
})
}
}
var rectIntTests = []struct {
name string
r1, r2 pixel.Rect
want pixel.Rect
intersect bool
}{
{
name: "Nothing touching",
r1: pixel.R(0, 0, 10, 10),
r2: pixel.R(21, 21, 40, 40),
want: pixel.ZR,
},
{
name: "Edge touching",
r1: pixel.R(0, 0, 10, 10),
r2: pixel.R(10, 10, 20, 20),
want: pixel.ZR,
},
{
name: "Bit of overlap",
r1: pixel.R(0, 0, 10, 10),
r2: pixel.R(0, 9, 20, 20),
want: pixel.R(0, 9, 10, 10),
intersect: true,
},
{
name: "Fully overlapped",
r1: pixel.R(0, 0, 10, 10),
r2: pixel.R(0, 0, 10, 10),
want: pixel.R(0, 0, 10, 10),
intersect: true,
},
}
func TestRect_Intersect(t *testing.T) {
for _, tt := range rectIntTests {
t.Run(tt.name, func(t *testing.T) {
if got := tt.r1.Intersect(tt.r2); !reflect.DeepEqual(got, tt.want) {
t.Errorf("Rect.Intersect() = %v, want %v", got, tt.want)
}
})
}
}
func TestRect_Intersects(t *testing.T) {
for _, tt := range rectIntTests {
t.Run(tt.name, func(t *testing.T) {
if got := tt.r1.Intersects(tt.r2); got != tt.intersect {
t.Errorf("Rect.Intersects() = %v, want %v", got, tt.want)
}
})
}
}

View File

@ -9,10 +9,8 @@ import "image/color"
// //
// sprite := pixel.NewSprite(pic, pic.Bounds()) // sprite := pixel.NewSprite(pic, pic.Bounds())
// //
// Note, that Sprite caches the results of MakePicture from Targets it's drawn to for each Picture // To achieve different anchoring, transformations and color masking, use SetMatrix and SetColorMask
// it's set to. What it means is that using a Sprite with an unbounded number of Pictures leads to a // methods.
// memory leak, since Sprite caches them and never forgets. In such a situation, create a new Sprite
// for each Picture.
type Sprite struct { type Sprite struct {
tri *TrianglesData tri *TrianglesData
frame Rect frame Rect
@ -27,7 +25,7 @@ func NewSprite(pic Picture, frame Rect) *Sprite {
tri := MakeTrianglesData(6) tri := MakeTrianglesData(6)
s := &Sprite{ s := &Sprite{
tri: tri, tri: tri,
d: Drawer{Triangles: tri, Cached: true}, d: Drawer{Triangles: tri},
} }
s.matrix = IM s.matrix = IM
s.mask = Alpha(1) s.mask = Alpha(1)
@ -44,13 +42,6 @@ func (s *Sprite) Set(pic Picture, frame Rect) {
} }
} }
// SetCached makes the sprite cache all the
// incoming pictures if the argument is true, and
// doesn't make it do that if the argument is false.
func (s *Sprite) SetCached(cached bool) {
s.d.Cached = cached
}
// Picture returns the current Sprite's Picture. // Picture returns the current Sprite's Picture.
func (s *Sprite) Picture() Picture { func (s *Sprite) Picture() Picture {
return s.d.Picture return s.d.Picture
@ -61,58 +52,70 @@ func (s *Sprite) Frame() Rect {
return s.frame return s.frame
} }
// Draw draws the Sprite onto the provided Target. The Sprite will be transformed by the given Matrix. // SetMatrix sets a Matrix that this Sprite will be transformed by. This overrides any previously
// set Matrix.
// //
// This method is equivalent to calling DrawColorMask with nil color mask. // Note, that this has nothing to do with BasicTarget's SetMatrix method. This only affects this
func (s *Sprite) Draw(t Target, matrix Matrix) { // Sprite and is usable with any Target.
s.DrawColorMask(t, matrix, nil) func (s *Sprite) SetMatrix(matrix Matrix) {
} if s.matrix != matrix {
// DrawColorMask draws the Sprite onto the provided Target. The Sprite will be transformed by the
// given Matrix and all of it's color will be multiplied by the given mask.
//
// If the mask is nil, a fully opaque white mask will be used, which causes no effect.
func (s *Sprite) DrawColorMask(t Target, matrix Matrix, mask color.Color) {
dirty := false
if matrix != s.matrix {
s.matrix = matrix s.matrix = matrix
dirty = true
}
if mask == nil {
mask = Alpha(1)
}
rgba := ToRGBA(mask)
if rgba != s.mask {
s.mask = rgba
dirty = true
}
if dirty {
s.calcData() s.calcData()
} }
}
// Matrix returns the currently set Matrix.
func (s *Sprite) Matrix() Matrix {
return s.matrix
}
// SetColorMask sets a color that this Sprite will be multiplied by. This overrides any previously
// set color mask.
//
// Note, that this has nothing to do with BasicTarget's SetColorMask method. This only affects this
// Sprite and is usable with any Target.
func (s *Sprite) SetColorMask(mask color.Color) {
rgba := ToRGBA(mask)
if s.mask != rgba {
s.mask = ToRGBA(mask)
s.calcData()
}
}
// ColorMask returns the currently set color mask.
func (s *Sprite) ColorMask() RGBA {
return s.mask
}
// Draw draws the Sprite onto the provided Target.
func (s *Sprite) Draw(t Target) {
s.d.Draw(t) s.d.Draw(t)
} }
func (s *Sprite) calcData() { func (s *Sprite) calcData() {
var ( var (
center = s.frame.Center() center = s.frame.Center()
horizontal = V(s.frame.W()/2, 0) horizontal = X(s.frame.W() / 2)
vertical = V(0, s.frame.H()/2) vertical = Y(s.frame.H() / 2)
) )
(*s.tri)[0].Position = Vec{}.Sub(horizontal).Sub(vertical) (*s.tri)[0].Position = -horizontal - vertical
(*s.tri)[1].Position = Vec{}.Add(horizontal).Sub(vertical) (*s.tri)[1].Position = +horizontal - vertical
(*s.tri)[2].Position = Vec{}.Add(horizontal).Add(vertical) (*s.tri)[2].Position = +horizontal + vertical
(*s.tri)[3].Position = Vec{}.Sub(horizontal).Sub(vertical) (*s.tri)[3].Position = -horizontal - vertical
(*s.tri)[4].Position = Vec{}.Add(horizontal).Add(vertical) (*s.tri)[4].Position = +horizontal + vertical
(*s.tri)[5].Position = Vec{}.Sub(horizontal).Add(vertical) (*s.tri)[5].Position = -horizontal + vertical
for i := range *s.tri { for i := range *s.tri {
(*s.tri)[i].Color = s.mask (*s.tri)[i].Color = s.mask
(*s.tri)[i].Picture = center.Add((*s.tri)[i].Position) (*s.tri)[i].Picture = center + (*s.tri)[i].Position
(*s.tri)[i].Intensity = 1 (*s.tri)[i].Intensity = 1
}
// matrix and mask
for i := range *s.tri {
(*s.tri)[i].Position = s.matrix.Project((*s.tri)[i].Position) (*s.tri)[i].Position = s.matrix.Project((*s.tri)[i].Position)
(*s.tri)[i].Color = s.mask
} }
s.d.Dirty() s.d.Dirty()

View File

@ -1,247 +0,0 @@
package text
import (
"image"
"image/draw"
"sort"
"unicode"
"github.com/faiface/pixel"
"golang.org/x/image/font"
"golang.org/x/image/math/fixed"
)
// Atlas7x13 is an Atlas using basicfont.Face7x13 with the ASCII rune set
var Atlas7x13 *Atlas
// Glyph describes one glyph in an Atlas.
type Glyph struct {
Dot pixel.Vec
Frame pixel.Rect
Advance float64
}
// Atlas is a set of pre-drawn glyphs of a fixed set of runes. This allows for efficient text drawing.
type Atlas struct {
face font.Face
pic pixel.Picture
mapping map[rune]Glyph
ascent float64
descent float64
lineHeight float64
}
// NewAtlas creates a new Atlas containing glyphs of the union of the given sets of runes (plus
// unicode.ReplacementChar) from the given font face.
//
// Creating an Atlas is rather expensive, do not create a new Atlas each frame.
//
// Do not destroy or close the font.Face after creating the Atlas. Atlas still uses it.
func NewAtlas(face font.Face, runeSets ...[]rune) *Atlas {
seen := make(map[rune]bool)
runes := []rune{unicode.ReplacementChar}
for _, set := range runeSets {
for _, r := range set {
if !seen[r] {
runes = append(runes, r)
seen[r] = true
}
}
}
fixedMapping, fixedBounds := makeSquareMapping(face, runes, fixed.I(2))
atlasImg := image.NewRGBA(image.Rect(
fixedBounds.Min.X.Floor(),
fixedBounds.Min.Y.Floor(),
fixedBounds.Max.X.Ceil(),
fixedBounds.Max.Y.Ceil(),
))
for r, fg := range fixedMapping {
if dr, mask, maskp, _, ok := face.Glyph(fg.dot, r); ok {
draw.Draw(atlasImg, dr, mask, maskp, draw.Src)
}
}
bounds := pixel.R(
i2f(fixedBounds.Min.X),
i2f(fixedBounds.Min.Y),
i2f(fixedBounds.Max.X),
i2f(fixedBounds.Max.Y),
)
mapping := make(map[rune]Glyph)
for r, fg := range fixedMapping {
mapping[r] = Glyph{
Dot: pixel.V(
i2f(fg.dot.X),
bounds.Max.Y-(i2f(fg.dot.Y)-bounds.Min.Y),
),
Frame: pixel.R(
i2f(fg.frame.Min.X),
bounds.Max.Y-(i2f(fg.frame.Min.Y)-bounds.Min.Y),
i2f(fg.frame.Max.X),
bounds.Max.Y-(i2f(fg.frame.Max.Y)-bounds.Min.Y),
).Norm(),
Advance: i2f(fg.advance),
}
}
return &Atlas{
face: face,
pic: pixel.PictureDataFromImage(atlasImg),
mapping: mapping,
ascent: i2f(face.Metrics().Ascent),
descent: i2f(face.Metrics().Descent),
lineHeight: i2f(face.Metrics().Height),
}
}
// Picture returns the underlying Picture containing an arrangement of all the glyphs contained
// within the Atlas.
func (a *Atlas) Picture() pixel.Picture {
return a.pic
}
// Contains reports wheter r in contained within the Atlas.
func (a *Atlas) Contains(r rune) bool {
_, ok := a.mapping[r]
return ok
}
// Glyph returns the description of r within the Atlas.
func (a *Atlas) Glyph(r rune) Glyph {
return a.mapping[r]
}
// Kern returns the kerning distance between runes r0 and r1. Positive distance means that the
// glyphs should be further apart.
func (a *Atlas) Kern(r0, r1 rune) float64 {
return i2f(a.face.Kern(r0, r1))
}
// Ascent returns the distance from the top of the line to the baseline.
func (a *Atlas) Ascent() float64 {
return a.ascent
}
// Descent returns the distance from the baseline to the bottom of the line.
func (a *Atlas) Descent() float64 {
return a.descent
}
// LineHeight returns the recommended vertical distance between two lines of text.
func (a *Atlas) LineHeight() float64 {
return a.lineHeight
}
// DrawRune returns parameters necessary for drawing a rune glyph.
//
// Rect is a rectangle where the glyph should be positioned. Frame is the glyph frame inside the
// Atlas's Picture. NewDot is the new position of the dot.
func (a *Atlas) DrawRune(prevR, r rune, dot pixel.Vec) (rect, frame, bounds pixel.Rect, newDot pixel.Vec) {
if !a.Contains(r) {
r = unicode.ReplacementChar
}
if !a.Contains(unicode.ReplacementChar) {
return pixel.Rect{}, pixel.Rect{}, pixel.Rect{}, dot
}
if !a.Contains(prevR) {
prevR = unicode.ReplacementChar
}
if prevR >= 0 {
dot.X += a.Kern(prevR, r)
}
glyph := a.Glyph(r)
rect = glyph.Frame.Moved(dot.Sub(glyph.Dot))
bounds = rect
if bounds.W()*bounds.H() != 0 {
bounds = pixel.R(
bounds.Min.X,
dot.Y-a.Descent(),
bounds.Max.X,
dot.Y+a.Ascent(),
)
}
dot.X += glyph.Advance
return rect, glyph.Frame, bounds, dot
}
type fixedGlyph struct {
dot fixed.Point26_6
frame fixed.Rectangle26_6
advance fixed.Int26_6
}
// makeSquareMapping finds an optimal glyph arrangement of the given runes, so that their common
// bounding box is as square as possible.
func makeSquareMapping(face font.Face, runes []rune, padding fixed.Int26_6) (map[rune]fixedGlyph, fixed.Rectangle26_6) {
width := sort.Search(int(fixed.I(1024*1024)), func(i int) bool {
width := fixed.Int26_6(i)
_, bounds := makeMapping(face, runes, padding, width)
return bounds.Max.X-bounds.Min.X >= bounds.Max.Y-bounds.Min.Y
})
return makeMapping(face, runes, padding, fixed.Int26_6(width))
}
// makeMapping arranges glyphs of the given runes into rows in such a way, that no glyph is located
// fully to the right of the specified width. Specifically, it places glyphs in a row one by one and
// once it reaches the specified width, it starts a new row.
func makeMapping(face font.Face, runes []rune, padding, width fixed.Int26_6) (map[rune]fixedGlyph, fixed.Rectangle26_6) {
mapping := make(map[rune]fixedGlyph)
bounds := fixed.Rectangle26_6{}
dot := fixed.P(0, 0)
for _, r := range runes {
b, advance, ok := face.GlyphBounds(r)
if !ok {
continue
}
// this is important for drawing, artifacts arise otherwise
frame := fixed.Rectangle26_6{
Min: fixed.P(b.Min.X.Floor(), b.Min.Y.Floor()),
Max: fixed.P(b.Max.X.Ceil(), b.Max.Y.Ceil()),
}
dot.X -= frame.Min.X
frame = frame.Add(dot)
mapping[r] = fixedGlyph{
dot: dot,
frame: frame,
advance: advance,
}
bounds = bounds.Union(frame)
dot.X = frame.Max.X
// padding + align to integer
dot.X += padding
dot.X = fixed.I(dot.X.Ceil())
// width exceeded, new row
if frame.Max.X >= width {
dot.X = 0
dot.Y += face.Metrics().Ascent + face.Metrics().Descent
// padding + align to integer
dot.Y += padding
dot.Y = fixed.I(dot.Y.Ceil())
}
}
return mapping, bounds
}
func i2f(i fixed.Int26_6) float64 {
return float64(i) / (1 << 6)
}

View File

@ -1,29 +0,0 @@
package text_test
import (
"testing"
"github.com/faiface/pixel/text"
"golang.org/x/image/font/inconsolata"
)
func TestAtlas7x13(t *testing.T) {
if text.Atlas7x13 == nil {
t.Fatalf("Atlas7x13 is nil")
}
for _, tt := range []struct {
runes []rune
want bool
}{{text.ASCII, true}, {[]rune("ÅÄÖ"), false}} {
for _, r := range tt.runes {
if got := text.Atlas7x13.Contains(r); got != tt.want {
t.Fatalf("Atlas7x13.Contains('%s') = %v, want %v", string(r), got, tt.want)
}
}
}
}
func TestAtlasInconsolata(t *testing.T) {
text.NewAtlas(inconsolata.Regular8x16, text.ASCII)
}

View File

@ -1,2 +0,0 @@
// Package text implements efficient text drawing for the Pixel library.
package text

View File

@ -1,359 +0,0 @@
package text
import (
"image/color"
"math"
"unicode"
"unicode/utf8"
"github.com/faiface/pixel"
"golang.org/x/image/font/basicfont"
)
// ASCII is a set of all ASCII runes. These runes are codepoints from 32 to 127 inclusive.
var ASCII []rune
func init() {
ASCII = make([]rune, unicode.MaxASCII-32)
for i := range ASCII {
ASCII[i] = rune(32 + i)
}
Atlas7x13 = NewAtlas(basicfont.Face7x13, ASCII)
}
// RangeTable takes a *unicode.RangeTable and generates a set of runes contained within that
// RangeTable.
func RangeTable(table *unicode.RangeTable) []rune {
var runes []rune
for _, rng := range table.R16 {
for r := rng.Lo; r <= rng.Hi; r += rng.Stride {
runes = append(runes, rune(r))
}
}
for _, rng := range table.R32 {
for r := rng.Lo; r <= rng.Hi; r += rng.Stride {
runes = append(runes, rune(r))
}
}
return runes
}
// Text allows for effiecient and convenient text drawing.
//
// To create a Text object, use the New constructor:
// txt := text.New(pixel.ZV, text.NewAtlas(face, text.ASCII))
//
// As suggested by the constructor, a Text object is always associated with one font face and a
// fixed set of runes. For example, the Text we created above can draw text using the font face
// contained in the face variable and is capable of drawing ASCII characters.
//
// Here we create a Text object which can draw ASCII and Katakana characters:
// txt := text.New(0, text.NewAtlas(face, text.ASCII, text.RangeTable(unicode.Katakana)))
//
// Similarly to IMDraw, Text functions as a buffer. It implements io.Writer interface, so writing
// text to it is really simple:
// fmt.Print(txt, "Hello, world!")
//
// Newlines, tabs and carriage returns are supported.
//
// Finally, if we want the written text to show up on some other Target, we can draw it:
// txt.Draw(target)
//
// Text exports two important fields: Orig and Dot. Dot is the position where the next character
// will be written. Dot is automatically moved when writing to a Text object, but you can also
// manipulate it manually. Orig specifies the text origin, usually the top-left dot position. Dot is
// always aligned to Orig when writing newlines. The Clear method resets the Dot to Orig.
type Text struct {
// Orig specifies the text origin, usually the top-left dot position. Dot is always aligned
// to Orig when writing newlines.
Orig pixel.Vec
// Dot is the position where the next character will be written. Dot is automatically moved
// when writing to a Text object, but you can also manipulate it manually
Dot pixel.Vec
// Color is the color of the text that is to be written. Defaults to white.
Color color.Color
// LineHeight is the vertical distance between two lines of text.
//
// Example:
// txt.LineHeight = 1.5 * txt.Atlas().LineHeight()
LineHeight float64
// TabWidth is the horizontal tab width. Tab characters will align to the multiples of this
// width.
//
// Example:
// txt.TabWidth = 8 * txt.Atlas().Glyph(' ').Advance
TabWidth float64
atlas *Atlas
buf []byte
prevR rune
bounds pixel.Rect
glyph pixel.TrianglesData
tris pixel.TrianglesData
mat pixel.Matrix
col pixel.RGBA
trans pixel.TrianglesData
transD pixel.Drawer
dirty bool
anchor pixel.Anchor
}
// New creates a new Text capable of drawing runes contained in the provided Atlas. Orig and Dot
// will be initially set to orig.
//
// Here we create a Text capable of drawing ASCII characters using the Go Regular font.
// ttf, err := truetype.Parse(goregular.TTF)
// if err != nil {
// panic(err)
// }
// face := truetype.NewFace(ttf, &truetype.Options{
// Size: 14,
// })
// txt := text.New(orig, text.NewAtlas(face, text.ASCII))
func New(orig pixel.Vec, atlas *Atlas) *Text {
txt := &Text{
Orig: orig,
Dot: orig,
Color: pixel.Alpha(1),
LineHeight: atlas.LineHeight(),
TabWidth: atlas.Glyph(' ').Advance * 4,
atlas: atlas,
mat: pixel.IM,
col: pixel.Alpha(1),
}
txt.glyph.SetLen(6)
for i := range txt.glyph {
txt.glyph[i].Color = pixel.Alpha(1)
txt.glyph[i].Intensity = 1
}
txt.transD.Picture = txt.atlas.pic
txt.transD.Triangles = &txt.trans
txt.transD.Cached = true
txt.Clear()
return txt
}
// Atlas returns the underlying Text's Atlas containing all of the pre-drawn glyphs. The Atlas is
// also useful for getting values such as the recommended line height.
func (txt *Text) Atlas() *Atlas {
return txt.atlas
}
// Bounds returns the bounding box of the text currently written to the Text excluding whitespace.
//
// If the Text is empty, a zero rectangle is returned.
func (txt *Text) Bounds() pixel.Rect {
return txt.bounds
}
// BoundsOf returns the bounding box of s if it was to be written to the Text right now.
func (txt *Text) BoundsOf(s string) pixel.Rect {
dot := txt.Dot
prevR := txt.prevR
bounds := pixel.Rect{}
for _, r := range s {
var control bool
dot, control = txt.controlRune(r, dot)
if control {
continue
}
var b pixel.Rect
_, _, b, dot = txt.Atlas().DrawRune(prevR, r, dot)
if bounds.W()*bounds.H() == 0 {
bounds = b
} else {
bounds = bounds.Union(b)
}
prevR = r
}
return bounds
}
// AlignedTo returns the text moved by the given anchor.
func (txt *Text) AlignedTo(anchor pixel.Anchor) *Text {
txt.anchor = anchor
return txt
}
// Clear removes all written text from the Text. The Dot field is reset to Orig.
func (txt *Text) Clear() {
txt.prevR = -1
txt.bounds = pixel.Rect{}
txt.tris.SetLen(0)
txt.dirty = true
txt.Dot = txt.Orig
}
// Write writes a slice of bytes to the Text. This method never fails, always returns len(p), nil.
func (txt *Text) Write(p []byte) (n int, err error) {
txt.buf = append(txt.buf, p...)
txt.drawBuf()
return len(p), nil
}
// WriteString writes a string to the Text. This method never fails, always returns len(s), nil.
func (txt *Text) WriteString(s string) (n int, err error) {
txt.buf = append(txt.buf, s...)
txt.drawBuf()
return len(s), nil
}
// WriteByte writes a byte to the Text. This method never fails, always returns nil.
//
// Writing a multi-byte rune byte-by-byte is perfectly supported.
func (txt *Text) WriteByte(c byte) error {
txt.buf = append(txt.buf, c)
txt.drawBuf()
return nil
}
// WriteRune writes a rune to the Text. This method never fails, always returns utf8.RuneLen(r), nil.
func (txt *Text) WriteRune(r rune) (n int, err error) {
var b [4]byte
n = utf8.EncodeRune(b[:], r)
txt.buf = append(txt.buf, b[:n]...)
txt.drawBuf()
return n, nil
}
// Draw draws all text written to the Text to the provided Target. The text is transformed by the
// provided Matrix.
//
// This method is equivalent to calling DrawColorMask with nil color mask.
//
// If there's a lot of text written to the Text, changing a matrix or a color mask often might hurt
// performance. Consider using your Target's SetMatrix or SetColorMask methods if available.
func (txt *Text) Draw(t pixel.Target, matrix pixel.Matrix) {
txt.DrawColorMask(t, matrix, nil)
}
// DrawColorMask draws all text written to the Text to the provided Target. The text is transformed
// by the provided Matrix and masked by the provided color mask.
//
// If there's a lot of text written to the Text, changing a matrix or a color mask often might hurt
// performance. Consider using your Target's SetMatrix or SetColorMask methods if available.
func (txt *Text) DrawColorMask(t pixel.Target, matrix pixel.Matrix, mask color.Color) {
if matrix != txt.mat {
txt.mat = matrix
txt.dirty = true
}
offset := txt.Orig.Sub(txt.Bounds().Max.Add(txt.Bounds().AnchorPos(txt.anchor.Opposite())))
txt.mat = pixel.IM.Moved(offset).Chained(txt.mat)
if mask == nil {
mask = pixel.Alpha(1)
}
rgba := pixel.ToRGBA(mask)
if rgba != txt.col {
txt.col = rgba
txt.dirty = true
}
if txt.dirty {
txt.trans.SetLen(txt.tris.Len())
txt.trans.Update(&txt.tris)
for i := range txt.trans {
txt.trans[i].Position = txt.mat.Project(txt.trans[i].Position)
txt.trans[i].Color = txt.trans[i].Color.Mul(txt.col)
}
txt.transD.Dirty()
txt.dirty = false
}
txt.transD.Draw(t)
}
// controlRune checks if r is a control rune (newline, tab, ...). If it is, a new dot position and
// true is returned. If r is not a control rune, the original dot and false is returned.
func (txt *Text) controlRune(r rune, dot pixel.Vec) (newDot pixel.Vec, control bool) {
switch r {
case '\n':
dot.X = txt.Orig.X
dot.Y -= txt.LineHeight
case '\r':
dot.X = txt.Orig.X
case '\t':
rem := math.Mod(dot.X-txt.Orig.X, txt.TabWidth)
rem = math.Mod(rem, rem+txt.TabWidth)
if rem == 0 {
rem = txt.TabWidth
}
dot.X += rem
default:
return dot, false
}
return dot, true
}
func (txt *Text) drawBuf() {
if !utf8.FullRune(txt.buf) {
return
}
rgba := pixel.ToRGBA(txt.Color)
for i := range txt.glyph {
txt.glyph[i].Color = rgba
}
for utf8.FullRune(txt.buf) {
r, size := utf8.DecodeRune(txt.buf)
txt.buf = txt.buf[size:]
var control bool
txt.Dot, control = txt.controlRune(r, txt.Dot)
if control {
continue
}
var rect, frame, bounds pixel.Rect
rect, frame, bounds, txt.Dot = txt.Atlas().DrawRune(txt.prevR, r, txt.Dot)
txt.prevR = r
rv := [...]pixel.Vec{
{X: rect.Min.X, Y: rect.Min.Y},
{X: rect.Max.X, Y: rect.Min.Y},
{X: rect.Max.X, Y: rect.Max.Y},
{X: rect.Min.X, Y: rect.Max.Y},
}
fv := [...]pixel.Vec{
{X: frame.Min.X, Y: frame.Min.Y},
{X: frame.Max.X, Y: frame.Min.Y},
{X: frame.Max.X, Y: frame.Max.Y},
{X: frame.Min.X, Y: frame.Max.Y},
}
for i, j := range [...]int{0, 1, 2, 0, 2, 3} {
txt.glyph[i].Position = rv[j]
txt.glyph[i].Picture = fv[j]
}
txt.tris = append(txt.tris, txt.glyph...)
txt.dirty = true
if txt.bounds.W()*txt.bounds.H() == 0 {
txt.bounds = bounds
} else {
txt.bounds = txt.bounds.Union(bounds)
}
}
}

View File

@ -1,87 +0,0 @@
package text_test
import (
"fmt"
"math/rand"
"testing"
"unicode"
"golang.org/x/image/font/basicfont"
"golang.org/x/image/font/gofont/goregular"
"github.com/faiface/pixel"
"github.com/faiface/pixel/text"
"github.com/golang/freetype/truetype"
)
func TestClear(t *testing.T) {
txt := text.New(pixel.ZV, text.Atlas7x13)
if got, want := txt.Dot, pixel.ZV; !eqVectors(got, want) {
t.Fatalf("txt.Dot = %v, want %v", got, want)
}
fmt.Fprint(txt, "Test\nClear")
if got, want := txt.Dot, pixel.V(35, -13); !eqVectors(got, want) {
t.Fatalf("txt.Dot = %v, want %v", got, want)
}
txt.Clear()
if got, want := txt.Dot, pixel.ZV; !eqVectors(got, want) {
t.Fatalf("txt.Dot = %v, want %v", got, want)
}
}
func BenchmarkNewAtlas(b *testing.B) {
runeSets := []struct {
name string
set []rune
}{
{"ASCII", text.ASCII},
{"Latin", text.RangeTable(unicode.Latin)},
}
ttf, _ := truetype.Parse(goregular.TTF)
face := truetype.NewFace(ttf, &truetype.Options{
Size: 16,
GlyphCacheEntries: 1,
})
for _, runeSet := range runeSets {
b.Run(runeSet.name, func(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = text.NewAtlas(face, runeSet.set)
}
})
}
}
func BenchmarkTextWrite(b *testing.B) {
runeSet := text.ASCII
atlas := text.NewAtlas(basicfont.Face7x13, runeSet)
lengths := []int{1, 10, 100, 1000}
chunks := make([][]byte, len(lengths))
for i := range chunks {
chunk := make([]rune, lengths[i])
for j := range chunk {
chunk[j] = runeSet[rand.Intn(len(runeSet))]
}
chunks[i] = []byte(string(chunk))
}
for _, chunk := range chunks {
b.Run(fmt.Sprintf("%d", len(chunk)), func(b *testing.B) {
txt := text.New(pixel.ZV, atlas)
for i := 0; i < b.N; i++ {
_, _ = txt.Write(chunk)
}
})
}
}
func eqVectors(a, b pixel.Vec) bool {
return (a.X == b.X && a.Y == b.Y)
}

462
vector.go
View File

@ -1,462 +0,0 @@
package pixel
import (
"fmt"
"math"
)
// Vec is a 2D vector type with X and Y coordinates.
//
// Create vectors with the V constructor:
//
// u := pixel.V(1, 2)
// v := pixel.V(8, -3)
//
// Use various methods to manipulate them:
//
// w := u.Add(v)
// fmt.Println(w) // Vec(9, -1)
// fmt.Println(u.Sub(v)) // Vec(-7, 5)
// u = pixel.V(2, 3)
// v = pixel.V(8, 1)
// if u.X < 0 {
// fmt.Println("this won't happen")
// }
// x := u.Unit().Dot(v.Unit())
type Vec struct {
X, Y float64
}
// ZV is a zero vector.
var ZV = Vec{0, 0}
// V returns a new 2D vector with the given coordinates.
func V(x, y float64) Vec {
return Vec{x, y}
}
// nearlyEqual compares two float64s and returns whether they are equal, accounting for rounding errors.At worst, the
// result is correct to 7 significant digits.
func nearlyEqual(a, b float64) bool {
epsilon := 0.000001
if a == b {
return true
}
diff := math.Abs(a - b)
if a == 0.0 || b == 0.0 || diff < math.SmallestNonzeroFloat64 {
return diff < (epsilon * math.SmallestNonzeroFloat64)
}
absA := math.Abs(a)
absB := math.Abs(b)
return diff/math.Min(absA+absB, math.MaxFloat64) < epsilon
}
// Eq will compare two vectors and return whether they are equal accounting for rounding errors. At worst, the result
// is correct to 7 significant digits.
func (u Vec) Eq(v Vec) bool {
return nearlyEqual(u.X, v.X) && nearlyEqual(u.Y, v.Y)
}
// Unit returns a vector of length 1 facing the given angle.
func Unit(angle float64) Vec {
return Vec{1, 0}.Rotated(angle)
}
// String returns the string representation of the vector u.
//
// u := pixel.V(4.5, -1.3)
// u.String() // returns "Vec(4.5, -1.3)"
// fmt.Println(u) // Vec(4.5, -1.3)
func (u Vec) String() string {
return fmt.Sprintf("Vec(%v, %v)", u.X, u.Y)
}
// XY returns the components of the vector in two return values.
func (u Vec) XY() (x, y float64) {
return u.X, u.Y
}
// Add returns the sum of vectors u and v.
func (u Vec) Add(v Vec) Vec {
return Vec{
u.X + v.X,
u.Y + v.Y,
}
}
// Sub returns the difference betweeen vectors u and v.
func (u Vec) Sub(v Vec) Vec {
return Vec{
u.X - v.X,
u.Y - v.Y,
}
}
// Floor converts x and y to their integer equivalents.
func (u Vec) Floor() Vec {
return Vec{
math.Floor(u.X),
math.Floor(u.Y),
}
}
// To returns the vector from u to v. Equivalent to v.Sub(u).
func (u Vec) To(v Vec) Vec {
return Vec{
v.X - u.X,
v.Y - u.Y,
}
}
// Scaled returns the vector u multiplied by c.
func (u Vec) Scaled(c float64) Vec {
return Vec{u.X * c, u.Y * c}
}
// ScaledXY returns the vector u multiplied by the vector v component-wise.
func (u Vec) ScaledXY(v Vec) Vec {
return Vec{u.X * v.X, u.Y * v.Y}
}
// Len returns the length of the vector u.
func (u Vec) Len() float64 {
return math.Hypot(u.X, u.Y)
}
// SqLen returns the squared length of the vector u (faster to compute than Len).
func (u Vec) SqLen() float64 {
return u.X*u.X + u.Y*u.Y
}
// Angle returns the angle between the vector u and the x-axis. The result is in range [-Pi, Pi].
func (u Vec) Angle() float64 {
return math.Atan2(u.Y, u.X)
}
// Unit returns a vector of length 1 facing the direction of u (has the same angle).
func (u Vec) Unit() Vec {
if u.X == 0 && u.Y == 0 {
return Vec{1, 0}
}
return u.Scaled(1 / u.Len())
}
// Rotated returns the vector u rotated by the given angle in radians.
func (u Vec) Rotated(angle float64) Vec {
sin, cos := math.Sincos(angle)
return Vec{
u.X*cos - u.Y*sin,
u.X*sin + u.Y*cos,
}
}
// Normal returns a vector normal to u. Equivalent to u.Rotated(math.Pi / 2), but faster.
func (u Vec) Normal() Vec {
return Vec{-u.Y, u.X}
}
// Dot returns the dot product of vectors u and v.
func (u Vec) Dot(v Vec) float64 {
return u.X*v.X + u.Y*v.Y
}
// Cross return the cross product of vectors u and v.
func (u Vec) Cross(v Vec) float64 {
return u.X*v.Y - v.X*u.Y
}
// Project returns a projection (or component) of vector u in the direction of vector v.
//
// Behaviour is undefined if v is a zero vector.
func (u Vec) Project(v Vec) Vec {
len := u.Dot(v) / v.Len()
return v.Unit().Scaled(len)
}
// Map applies the function f to both x and y components of the vector u and returns the modified
// vector.
//
// u := pixel.V(10.5, -1.5)
// v := u.Map(math.Floor) // v is Vec(10, -2), both components of u floored
func (u Vec) Map(f func(float64) float64) Vec {
return Vec{
f(u.X),
f(u.Y),
}
}
// Lerp returns a linear interpolation between vectors a and b.
//
// This function basically returns a point along the line between a and b and t chooses which one.
// If t is 0, then a will be returned, if t is 1, b will be returned. Anything between 0 and 1 will
// return the appropriate point between a and b and so on.
func Lerp(a, b Vec, t float64) Vec {
return a.Scaled(1 - t).Add(b.Scaled(t))
}
// Line is a 2D line segment, between points A and B.
type Line struct {
A, B Vec
}
// L creates and returns a new Line.
func L(from, to Vec) Line {
return Line{
A: from,
B: to,
}
}
// Bounds returns the lines bounding box. This is in the form of a normalized Rect.
func (l Line) Bounds() Rect {
return R(l.A.X, l.A.Y, l.B.X, l.B.Y).Norm()
}
// Center will return the point at center of the line; that is, the point equidistant from either end.
func (l Line) Center() Vec {
return l.A.Add(l.A.To(l.B).Scaled(0.5))
}
// Closest will return the point on the line which is closest to the Vec provided.
func (l Line) Closest(v Vec) Vec {
// between is a helper function which determines whether x is greater than min(a, b) and less than max(a, b)
between := func(a, b, x float64) bool {
min := math.Min(a, b)
max := math.Max(a, b)
return min < x && x < max
}
// Closest point will be on a line which perpendicular to this line.
// If and only if the infinite perpendicular line intersects the segment.
m, b := l.Formula()
// Account for horizontal lines
if m == 0 {
x := v.X
y := l.A.Y
// check if the X coordinate of v is on the line
if between(l.A.X, l.B.X, v.X) {
return V(x, y)
}
// Otherwise get the closest endpoint
if l.A.To(v).Len() < l.B.To(v).Len() {
return l.A
}
return l.B
}
// Account for vertical lines
if math.IsInf(math.Abs(m), 1) {
x := l.A.X
y := v.Y
// check if the Y coordinate of v is on the line
if between(l.A.Y, l.B.Y, v.Y) {
return V(x, y)
}
// Otherwise get the closest endpoint
if l.A.To(v).Len() < l.B.To(v).Len() {
return l.A
}
return l.B
}
perpendicularM := -1 / m
perpendicularB := v.Y - (perpendicularM * v.X)
// Coordinates of intersect (of infinite lines)
x := (perpendicularB - b) / (m - perpendicularM)
y := m*x + b
// Check if the point lies between the x and y bounds of the segment
if !between(l.A.X, l.B.X, x) && !between(l.A.Y, l.B.Y, y) {
// Not within bounding box
toStart := v.To(l.A)
toEnd := v.To(l.B)
if toStart.Len() < toEnd.Len() {
return l.A
}
return l.B
}
return V(x, y)
}
// Contains returns whether the provided Vec lies on the line.
func (l Line) Contains(v Vec) bool {
return l.Closest(v).Eq(v)
}
// Formula will return the values that represent the line in the formula: y = mx + b
// This function will return math.Inf+, math.Inf- for a vertical line.
func (l Line) Formula() (m, b float64) {
// Account for horizontal lines
if l.B.Y == l.A.Y {
return 0, l.A.Y
}
m = (l.B.Y - l.A.Y) / (l.B.X - l.A.X)
b = l.A.Y - (m * l.A.X)
return m, b
}
// Intersect will return the point of intersection for the two line segments. If the line segments do not intersect,
// this function will return the zero-vector and false.
func (l Line) Intersect(k Line) (Vec, bool) {
// Check if the lines are parallel
lDir := l.A.To(l.B)
kDir := k.A.To(k.B)
if lDir.X == kDir.X && lDir.Y == kDir.Y {
return ZV, false
}
// The lines intersect - but potentially not within the line segments.
// Get the intersection point for the lines if they were infinitely long, check if the point exists on both of the
// segments
lm, lb := l.Formula()
km, kb := k.Formula()
// Account for vertical lines
if math.IsInf(math.Abs(lm), 1) && math.IsInf(math.Abs(km), 1) {
// Both vertical, therefore parallel
return ZV, false
}
var x, y float64
if math.IsInf(math.Abs(lm), 1) || math.IsInf(math.Abs(km), 1) {
// One line is vertical
intersectM := lm
intersectB := lb
verticalLine := k
if math.IsInf(math.Abs(lm), 1) {
intersectM = km
intersectB = kb
verticalLine = l
}
y = intersectM*verticalLine.A.X + intersectB
x = verticalLine.A.X
} else {
// Coordinates of intersect
x = (kb - lb) / (lm - km)
y = lm*x + lb
}
if l.Contains(V(x, y)) && k.Contains(V(x, y)) {
// The intersect point is on both line segments, they intersect.
return V(x, y), true
}
return ZV, false
}
// IntersectCircle will return the shortest Vec such that moving the Line by that Vec will cause the Line and Circle
// to no longer intesect. If they do not intersect at all, this function will return a zero-vector.
func (l Line) IntersectCircle(c Circle) Vec {
// Get the point on the line closest to the center of the circle.
closest := l.Closest(c.Center)
cirToClosest := c.Center.To(closest)
if cirToClosest.Len() >= c.Radius {
return ZV
}
return cirToClosest.Scaled(cirToClosest.Len() - c.Radius)
}
// IntersectRect will return the shortest Vec such that moving the Line by that Vec will cause the Line and Rect to
// no longer intesect. If they do not intersect at all, this function will return a zero-vector.
func (l Line) IntersectRect(r Rect) Vec {
// Check if either end of the line segment are within the rectangle
if r.Contains(l.A) || r.Contains(l.B) {
// Use the Rect.Intersect to get minimal return value
rIntersect := l.Bounds().Intersect(r)
if rIntersect.H() > rIntersect.W() {
// Go vertical
return V(0, rIntersect.H())
}
return V(rIntersect.W(), 0)
}
// Check if any of the rectangles' edges intersect with this line.
for _, edge := range r.Edges() {
if _, ok := l.Intersect(edge); ok {
// Get the closest points on the line to each corner, where:
// - the point is contained by the rectangle
// - the point is not the corner itself
corners := r.Vertices()
var closest *Vec
closestCorner := corners[0]
for _, c := range corners {
cc := l.Closest(c)
if closest == nil || (closest.Len() > cc.Len() && r.Contains(cc)) {
closest = &cc
closestCorner = c
}
}
return closest.To(closestCorner)
}
}
// No intersect
return ZV
}
// Len returns the length of the line segment.
func (l Line) Len() float64 {
return l.A.To(l.B).Len()
}
// Moved will return a line moved by the delta Vec provided.
func (l Line) Moved(delta Vec) Line {
return Line{
A: l.A.Add(delta),
B: l.B.Add(delta),
}
}
// Rotated will rotate the line around the provided Vec.
func (l Line) Rotated(around Vec, angle float64) Line {
// Move the line so we can use `Vec.Rotated`
lineShifted := l.Moved(around.Scaled(-1))
lineRotated := Line{
A: lineShifted.A.Rotated(angle),
B: lineShifted.B.Rotated(angle),
}
return lineRotated.Moved(around)
}
// Scaled will return the line scaled around the center point.
func (l Line) Scaled(scale float64) Line {
return l.ScaledXY(l.Center(), scale)
}
// ScaledXY will return the line scaled around the Vec provided.
func (l Line) ScaledXY(around Vec, scale float64) Line {
toA := around.To(l.A).Scaled(scale)
toB := around.To(l.B).Scaled(scale)
return Line{
A: around.Add(toA),
B: around.Add(toB),
}
}
func (l Line) String() string {
return fmt.Sprintf("Line(%v, %v)", l.A, l.B)
}

View File

@ -1,27 +0,0 @@
package pixel_test
import (
"fmt"
"testing"
"github.com/faiface/pixel"
)
type floorTest struct {
input pixel.Vec
expected pixel.Vec
}
func TestFloor(t *testing.T) {
tests := []floorTest{
{input: pixel.V(4.50, 6.70), expected: pixel.V(4, 6)},
{input: pixel.V(9.0, 6.70), expected: pixel.V(9, 6)},
}
for _, tc := range tests {
result := tc.input.Floor()
if result != tc.expected {
t.Error(fmt.Sprintf("Expected %v but got %v", tc.expected, result))
}
}
}