Compare commits

...

388 Commits
master ... v0.7

Author SHA1 Message Date
Michal Štrba 2d9a739e40
Merge pull request #97 from mewpull/master
contributing: remove tracking from link to Gitter
2018-02-19 12:21:47 +01:00
mewmew cbf4b9106d contributing: remove tracking from link to Gitter 2018-02-19 09:30:03 +01:00
faiface 5af5565fb6 update README.md 2018-02-18 20:58:41 +01:00
faiface 902248de11 add CONTRIBUTING.md 2018-02-18 20:53:23 +01:00
Michal Štrba 93a9cf4093
Merge pull request #90 from peterhellberg/text-reset-and-default-atlas
Add Atlas7x13, Clear now sets Dot field to Orig
2018-01-19 23:29:34 +01:00
Peter Hellberg 5ed20d5485 Change package to text_test 2018-01-19 23:09:48 +01:00
Peter Hellberg f72c39f382 Remove new7x13Atlas function 2018-01-19 23:08:06 +01:00
Peter Hellberg 5f53a59dd6 Document that Clear resets the Dot to Orig
Remove note on how to reset Dot to the Orig
2018-01-19 23:00:33 +01:00
Peter Hellberg 1c68874dc8 Add Atlas7x13, Clear now sets Dot field to Orig
Remove unused f2i function
2018-01-19 22:20:54 +01:00
Michal Štrba e788339625
Merge pull request #89 from terakilobyte/master
adds a game of life example
2018-01-19 15:26:04 +01:00
Nathan c1abca5cab addressing PR feedback 2018-01-18 20:44:54 -05:00
Nathan 8e4da92871 adds a game of life example 2018-01-18 19:52:52 -05:00
Michal Štrba c1a222d84b
Merge pull request #87 from svera/parallax-scroll
Parallax scroll
2018-01-12 12:59:03 +01:00
Michal Štrba 0183f7fbbb
Merge pull request #86 from svera/update-image
Update image
2018-01-12 12:57:11 +01:00
Sergio Vera Castellano 452ef16712 Removed unneeded parentheses 2018-01-12 12:52:50 +01:00
Sergio Vera Castellano 3706fa8879 Removed unneeded parentheses 2018-01-12 12:51:07 +01:00
Sergio Vera Castellano 0e9ff7b70e Added new image 2018-01-12 12:48:09 +01:00
Sergio Vera Castellano 1a41307942 Fixed image positioning 2018-01-12 12:47:16 +01:00
Michal Štrba 14ace447b5
Merge pull request #85 from svera/parallax-scroll
Parallax scroll (fixes #75)
2018-01-12 12:44:11 +01:00
Sergio Vera Castellano b4c1033c47 Corrected image placement 2018-01-12 12:43:09 +01:00
Michal Štrba 50dd323ba4
Merge pull request #82 from svera/procedural-terrain-1d
Procedural terrain 1D
2018-01-12 12:42:22 +01:00
Sergio Vera Castellano b40854c1d7 Update image 2018-01-12 12:40:02 +01:00
Sergio Vera Castellano 2f957ede44 Updated comment 2018-01-12 12:31:00 +01:00
Sergio Vera Castellano 0f4afdc244 Added result image 2018-01-12 12:29:08 +01:00
Sergio Vera Castellano d04bf34361 Updated window title 2018-01-12 12:23:10 +01:00
Sergio Vera Castellano ff0040bbb1 Small adjustments 2018-01-12 12:19:58 +01:00
Sergio Vera Castellano 1f1324dbe8 Fixed images 2018-01-12 10:37:52 +01:00
Sergio Vera Castellano dc889f0631 Fixed comments 2018-01-12 10:33:24 +01:00
Michal Štrba 8dd236979d
Merge pull request #84 from peterhellberg/change-screenshot-aspect-ratios
Change screenshot aspect ratios
2018-01-11 23:18:13 +01:00
Peter Hellberg 9942ab3da3 Change screenshot aspect ratios 2018-01-11 23:14:54 +01:00
faiface 3afca9093d add some community examples screenshots to README 2018-01-11 22:55:26 +01:00
Michal Štrba 97f07acd5e
Merge pull request #83 from peterhellberg/add-pixel-experiments-to-community-examples
Add pixel experiments to community examples
2018-01-11 22:50:56 +01:00
Peter Hellberg da55b9ddca Add bouncing experiment to community examples 2018-01-11 22:42:04 +01:00
Peter Hellberg b50671c353 Add starfield experiment to community examples 2018-01-11 22:39:45 +01:00
Peter Hellberg dc2b6fe7b1 Add raycaster experiment to community examples 2018-01-11 22:39:36 +01:00
Sergio Vera Castellano bd468c8629 Added result image 2018-01-11 17:53:16 +01:00
Sergio Vera Castellano 060142a8f9 Added readme 2018-01-11 17:43:39 +01:00
Sergio Vera Castellano 75e90bb010 Refactored code 2018-01-11 16:55:20 +01:00
Sergio Vera Castellano f4b7c74524 First commit 2018-01-11 14:52:23 +01:00
Sergio Vera Castellano 55fa9fd100 Fixed readme 2018-01-11 09:58:34 +01:00
Sergio Vera Castellano 814aff8f1c Removed log 2018-01-11 09:56:36 +01:00
Sergio Vera Castellano 79a78f2f1c First commit 2018-01-11 09:54:39 +01:00
Michal Štrba 95f4386cf5
Merge pull request #79 from mewpull/poll
pixelgl: export UpdateInput
2018-01-04 19:32:21 +01:00
mewmew f7b8a5a1a3 pixelgl: clarify UpdateInput docs 2018-01-04 19:23:53 +01:00
mewmew 4dc0b013f3 pixelgl: export UpdateInput 2018-01-01 21:04:56 +01:00
Michal Štrba 597550c16b
Merge pull request #77 from svera/isometric-basics
Isometric community example
2017-12-31 18:33:17 +01:00
Sergio Vera Castellano c65ed3a739 Added sample image 2017-12-31 18:07:00 +01:00
Sergio Vera Castellano b04ac93aaa First commit 2017-12-31 17:56:58 +01:00
Michal Štrba 9b325eee4e
Merge pull request #74 from svera/scrolling-background
Infinite scrolling background demo
2017-12-28 12:20:12 +01:00
Sergio Vera Castellano 82de22b7cf made framerate independent 2017-12-28 10:43:17 +01:00
Sergio Vera Castellano 302f1f5411 Small example explanation 2017-12-27 17:05:43 +01:00
Sergio Vera Castellano 5b2ad29f7b First commit 2017-12-27 16:29:21 +01:00
Michal Štrba 225de575de
Merge pull request #70 from PlainSight/improve-geo-tests
Improve geometry tests
2017-11-20 02:17:18 +01:00
Michal Štrba cceaac897e
Merge pull request #69 from PlainSight/add-getpos-to-window
Add GetPos function to window
2017-11-20 02:15:20 +01:00
ALex Ogier 1c4776d672 minor adjustments with how tests are named and run 2017-11-20 13:51:15 +13:00
ALex Ogier c9df32add0 refactor and add test cases 2017-11-20 13:51:15 +13:00
ALex Ogier 41a3073be3 add GetPos function to window 2017-11-20 13:37:22 +13:00
faiface be9fbb76bd README: add Windows guide mention 2017-11-15 21:38:24 +01:00
faiface 3d6f497bd5 dirty fix #58 (panic on minimizing windows) 2017-11-08 22:28:32 +01:00
Michal Štrba 0d89f0e36e
Merge pull request #61 from Schobers/master
Added SetPos to pixelgl.Window
2017-10-27 20:48:00 +02:00
Sander Schobers 8e68e71083 Added SetPos to pixelgl.Window 2017-10-27 20:35:45 +02:00
faiface 5612defeba README: mention OpenGL version 2017-10-19 21:34:09 +02:00
faiface 926b8181b3 add Vec.Project 2017-10-15 19:50:41 +02:00
faiface 6e27fc84e7 add pixel.Unit 2017-10-15 19:43:12 +02:00
faiface 6215259c1d add pixel.Clamp 2017-10-15 19:42:13 +02:00
faiface 4792a9ebd8 fix Matrix.Chained (wrong order of composition) 2017-09-04 00:40:12 +02:00
faiface 1de2dbe561 fix Vec.Normal to rotate by pi/2, not -pi/2 2017-08-29 20:23:32 +02:00
Michal Štrba 9eaf82e088 Merge pull request #48 from PlainSight/fix-rectangle-resize
Fix Rectangle "Resized" function
2017-07-29 01:11:02 +02:00
Alex 78d02fe8ea add more test transforms, now includes: center, origin, min, max and middle of side 2017-07-29 11:05:46 +12:00
Alex 35ebc39ba6 fix operation order for rectangle resize function, add tests 2017-07-29 00:40:06 +12:00
faiface d34b63676d remove audio package in favor of faiface/beep 2017-07-14 18:34:28 +02:00
faiface 321c494681 audio: wav: fix Stream for non 2 chans 2 bytes configurations 2017-07-14 02:41:01 +02:00
faiface 7fbd71cecc audio: wav: more precise Position and Seek 2017-07-14 02:22:33 +02:00
faiface 264bdff76d audio: wav: fix Stream (move position, forgot to do it previously) 2017-07-14 02:10:12 +02:00
faiface 447050b720 audio: wav: simplify Stream 2017-07-14 02:09:36 +02:00
faiface 4573035f89 merge master 2017-07-13 01:24:50 +02:00
faiface 63ababf515 audio: add Gain effect 2017-07-13 00:29:53 +02:00
faiface cce4cccd26 audio: minor change 2017-07-13 00:21:16 +02:00
faiface accdb6f51c update .travis.yml for ALSA 2017-07-12 22:54:36 +02:00
faiface d4f724a0d3 audio: minor change 2017-07-12 22:44:15 +02:00
faiface e3d1835268 audio: add Ctrl doc comments 2017-07-12 22:43:55 +02:00
faiface 66a8b51d54 audio: wav: add doc comments 2017-07-12 21:30:35 +02:00
faiface 9a1d011a9f audio: wav: simplify code, more DRY, encapsulate decoder type, only export Decode function 2017-07-12 19:37:20 +02:00
faiface e48de5789c audio: make Take propagate errors 2017-07-12 02:58:17 +02:00
faiface af52257c66 audio: wav: update streamer to satisfy StreamSeekCloser 2017-07-12 00:32:37 +02:00
faiface 31eeca4179 add StreamSeeker, StreamCloser and StreamSeekCloser interfaces 2017-07-12 00:05:42 +02:00
faiface 5b75852399 audio: add Err method to Streamer 2017-07-11 23:42:57 +02:00
faiface 0d5ad8a0bf audio: wav: fix seeking to include the header 2017-07-11 20:12:04 +02:00
faiface bf50427297 audio: rename Ctrl.Duration -> Ctrl.Position 2017-07-11 20:01:15 +02:00
faiface f3647fc451 audio: wav: add Seek and Position 2017-07-11 20:00:29 +02:00
faiface 73a3f65adb audio: wav: rename decoder.go -> streamer.go 2017-07-11 18:31:33 +02:00
faiface 099522c410 minor change 2017-07-11 17:36:00 +02:00
faiface 3ca575a4e4 audio: add initial wav streamer implementation 2017-07-11 17:34:17 +02:00
faiface dc4b4ec61f audio: speaker: fix byte encoding 2017-07-11 16:42:16 +02:00
faiface eca7b33334 audio: speaker: remove unnecessary channel confirmation 2017-07-09 16:34:49 +02:00
faiface 5ca0239191 audio: speaker: allow to call Init more than once 2017-07-08 22:01:59 +02:00
faiface af9bcae596 audio: add Mixer doc 2017-07-08 14:54:01 +02:00
faiface 62fcf430d2 audio: optimize Mixer.Stream and Mix(...).Stream (remove unnecessary mix buffer) 2017-07-08 14:43:24 +02:00
faiface b3cceb1b7d audio: add Mixer.Len 2017-07-08 14:28:34 +02:00
faiface 006e4f5481 audio: speaker: use Mixer to play sounds simultaneously 2017-07-07 18:46:40 +02:00
faiface 33c6f0ca5d audio: add Mixer 2017-07-07 18:46:26 +02:00
faiface c5078ff6b0 audio: add Ctrl decorator 2017-07-07 18:46:20 +02:00
faiface 9398991a01 audio: add Mix compositor 2017-07-07 16:11:48 +02:00
faiface 1a58fea88b minor change 2017-07-07 14:08:25 +02:00
faiface 6c527c3b89 speaker: remove Update, add Lock and Unlock 2017-07-07 13:20:55 +02:00
Michal Štrba 0777dc3bc1 Merge pull request #45 from aerth/audio-dep
fix audio dependency
2017-07-07 00:16:32 +02:00
aerth e2823f2580
fix audio dependency 2017-07-06 15:08:41 -07:00
faiface f47736b7cb fix edge sample value (-1 and +1) overflow 2017-07-06 23:36:18 +02:00
faiface 5d3f082240 actually fix the race condition 2017-07-06 22:34:22 +02:00
faiface d0f6e646ac minor change 2017-07-06 22:26:47 +02:00
faiface fdb6359fbb speaker: fix race condition 2017-07-06 22:26:20 +02:00
faiface 0737b86059 speaker: improve concurrency, only lock when and what necessary 2017-07-06 22:11:03 +02:00
faiface cb4bb4c3ef reimplement speaker 2017-07-06 21:44:34 +02:00
faiface 41963b01cc Merge branch 'audio' of https://github.com/faiface/pixel into audio 2017-07-06 20:52:12 +02:00
Michal Štrba 065f4f3c90 Merge pull request #44 from alistanis/audio
initial incomplete speaker implementation
2017-07-06 20:52:02 +02:00
Christopher Cooper 8e571bfe8e addresses some review comments 2017-07-06 11:49:35 -04:00
Christopher Cooper 32ff29438d fix bounds check 2017-07-05 18:15:53 -04:00
Christopher Cooper 1a88ab2edd mremove prints 2017-07-05 18:11:26 -04:00
Christopher Cooper f9972888a2 add initial speaker implementation 2017-07-05 18:10:03 -04:00
Christopher Cooper 6ac68670bd Merge pull request #1 from faiface/audio
Audio
2017-07-05 18:05:50 -04:00
Michal Štrba 57b578bba7 Merge pull request #43 from aerth/audio
add libasound2-dev travis dependency
2017-07-05 22:04:44 +02:00
aerth cdce974e49
add libasound2-dev dependency 2017-07-05 12:24:26 -07:00
faiface d7487f1f7a fix Rect.Intersect 2017-07-05 20:36:25 +02:00
faiface 4137f87f22 clarify Rect.Intersect doc 2017-07-05 19:58:09 +02:00
faiface bee65f5833 add Rect.Intersect 2017-07-05 19:54:30 +02:00
faiface 46e79f21b9 add Rect.Area 2017-07-05 19:54:18 +02:00
faiface 2a8c17c33c add Rect.Intersect 2017-07-05 19:51:54 +02:00
faiface c49c77a116 add Rect.Area 2017-07-05 19:35:11 +02:00
faiface d640879775 add Take decorator function 2017-07-05 18:14:18 +02:00
faiface c86834b8f6 add Seq compositor 2017-07-05 17:59:43 +02:00
faiface 330e9da360 add StreamerFunc helper type 2017-07-03 19:07:30 +02:00
faiface 7f2b8b6fe9 change SampleRate to float64 2017-07-03 18:55:27 +02:00
faiface 42737212d8 fix grammar in Streamer doc 2017-07-03 17:37:55 +02:00
faiface c7bb0a1639 minor stylistic corrections in Streamer doc 2017-07-03 17:28:54 +02:00
faiface a48d07ff6b audio: add Streamer interface 2017-07-03 14:05:08 +02:00
faiface 99573a5f1e optimize ToRGBA (weird one) 2017-07-03 00:23:07 +02:00
faiface c23446fb49 improve sprite.Draw(batch) benchmark 2017-07-03 00:22:45 +02:00
faiface 578ae8fa53 fix bug in text benchmark 2017-07-02 23:26:26 +02:00
faiface 6af6195bd0 optimize Drawer (reduce map access) 2017-07-02 23:23:27 +02:00
faiface 0c28c0785e add text benchmarks 2017-07-02 19:35:56 +02:00
faiface 17f735c2d0 remove unnecessary reassign in color benchmarks 2017-07-02 19:26:43 +02:00
faiface 7629b6ef5e add imdraw benchmarks 2017-07-02 19:22:40 +02:00
faiface ae0526fda0 add ToRGBA, sprite.Draw(batch) and Matrix benchmarks 2017-07-02 19:04:20 +02:00
Michal Štrba 80cfdfcb6a Merge pull request #40 from aerth/master
Use travis-ci
2017-06-16 01:14:23 +02:00
aerth b01300dab9
move button 2017-06-15 23:05:00 +00:00
aerth c078a58652
Link to travis build 2017-06-15 22:52:47 +00:00
aerth b58d6bf5ec
add travis config 2017-06-15 22:40:37 +00:00
faiface 75ab96923c another minor code style change 2017-06-11 14:14:02 +02:00
faiface 2a9b7e5210 minor code style change 2017-06-11 14:06:45 +02:00
faiface 79c5d20194 Merge branch 'dev' 2017-06-11 01:19:57 +02:00
faiface e6484064aa one more tiny doc change 2017-06-11 01:18:23 +02:00
faiface 3fcad7503f minor doc changes 2017-06-11 01:17:37 +02:00
Michal Štrba 1545bd7af5 Merge pull request #38 from seebs/master
more performance tweaks
2017-06-11 01:12:03 +02:00
Seebs 0dc27e409b Push: Don't convert pixel.RGBA to pixel.RGBA
Because that's expensive, even in the case where the conversion
is trivial. Use type assertion first. Reduces runtime cost of
imdraw.Push from ~15.3% to 8.4%, so not-quite-50% of runtime
cost of pushing points.

If you were setting imd.Color to Color objects that aren't RGBA
every single point, not much help. But if you set it and then
draw a bunch of points, this will be a big win.
2017-06-10 17:56:15 -05:00
Seebs f2ef87f198 Improve normal calculations
Soooo. It turns out that the bunch of smallish (~4-5% of runtime)
loads associated with Len(), Unit(), Rotated(), and so on... Were
actually more like 15% or more of computational effort. I first
figured this out by creating:

	func (u Vec) Normal(v Vec) Vec

which gives you a vector normal to u->v. That consumed a lot
of CPU time, and was followed by .Unit().Scaled(imd.thickness / 2),
which consumed a bit more CPU time.

After some poking, and in the interests of avoiding UI cruft,
the final selection is
	func (u Vec) Normal() Vec

This returns the vector rotated 90 degrees, which turns out to
be the most common problem.
2017-06-10 17:55:16 -05:00
Seebs ef6a44fef8 Slightly clean up normal calculations
We never actually need the "normal" value; it's an extra calculation
we didn't need, because ijNormal is the same value early on. It's
totally possible that we could further simplify this; there's a lot
of time going into the normal computations.
2017-06-10 10:47:22 -05:00
Seebs 9a6e6066bd don't call Len() when it can't change
updateData()'s loops checking gt.Len() turns out to have been costing
significant computation, not least because each call then in turn
called gt.vs.Stride().
2017-06-10 10:47:18 -05:00
faiface 7ebbf7e9b5 switch back to OpenGL 3.3 (OS X issues) 2017-06-10 15:11:45 +02:00
faiface 31fc049ab7 optimize GLTriangles SetLen and Update 2017-06-10 01:10:59 +02:00
faiface c331fe2583 Merge branch 'master' into gl2.1 2017-06-09 19:26:14 +02:00
faiface 6b9ea45e96 minor, mostly stylistic, changes 2017-06-09 18:13:05 +02:00
Michal Štrba 2e4c6018c9 Merge pull request #36 from seebs/master
revised performance tuning pull request
2017-06-09 18:04:25 +02:00
Seebs 7215265523 Don't duplicate computations in gltriangles.go
The computation including a call to Stride() can't be optimized away
safely because the compiler can't tell that Stride() is effectively
constant, but we know it won't change so we can make a slice pointing
at that part of the array.

CPU time for updateData goes from 26.35% to 18.65% in my test case.
2017-06-09 10:37:47 -05:00
Seebs fc858bff4d Reduce copying in fillPolygon
A slice of points means copying every point into the slice, then
copying every point's data from the slice to TrianglesData. An
array of indicies lets the compiler make better choices.
2017-06-09 10:37:47 -05:00
Seebs 918031892a smaller imdraw optimizations
For polyline, don't compute each normal twice; when we're going through a line,
the "next" normal for segment N is always the "previous" normal for segment
N+1, and we can compute fewer of them.
2017-06-09 10:37:47 -05:00
Seebs 9a7ab1c6b0 use point pool
For internal operations (anything using getAndClearPoints), there's a
pretty good chance that the operation will repeatedly invoke something
like fillPolygon(), meaning that it needs to push "a few" points
and then invoke something that uses those points.

So, we add a slice for containing spare slices of points, and on the
way out of each such function, shove the current imd.points (as used
inside that function) onto a stack, and set imd.points to [0:0] of
the thing it was called with.

Performance goes from 11-13fps to 17-18fps on my test case.
2017-06-09 10:37:47 -05:00
Seebs 34cdd8729b Simplify Matrix math, use 6-value affine matrixes.
It turns out that affine matrices are much simpler than the 3x3 matrices
they imply, and we can use this to dramatically streamline some code.
For a test program, this was about a 50% gain in frame rate just from
the cost of the applyMatrixAndMask calls in imdraw, which were calling
matrix.Project() many times. Simplifying matrix.Project, alone, got a
nearly 50% frame rate boost!

Also modify pixelgl's SetMatrix to copy the six values of a 3x2
Affine into the corresponding locations of a 3x3 matrix.
2017-06-09 10:37:43 -05:00
Seebs 0358330d3b The initializer is surprisingly expensive.
Removing the call to Alpha(1) and replacing it with an inline definition
produces measurable improvements. Replacing each instance of ZV with
Vec{} further improves things. We keep an inline RGBA because there
are circumstances (mostly when using pictures) where we don't want to
have to set colors to get default behavior.

For a fairly triangle-heavy thing, this reduces time spent in SetLen
from something over 10% of execution time to around 2.5% of execution
time.
2017-06-07 21:25:54 -05:00
faiface 4b7553cd73 add maze generator community example 2017-05-30 13:30:09 +02:00
faiface 781c44f119 add text tutorial link to README 2017-05-30 02:53:24 +02:00
faiface c385b247b3 add 07 guide code 2017-05-30 02:52:33 +02:00
faiface 3706d040ce fix typo in doc 2017-05-28 18:50:56 +02:00
faiface 4749e3ee7e add Canvas.Frame method 2017-05-28 18:44:30 +02:00
faiface e06acda99b minorly simplify typewriter example 2017-05-28 00:06:29 +02:00
faiface 9e0c65d8dd remove profiling from typewriter example 2017-05-27 19:19:31 +02:00
faiface f80edafc7b remove profiling from typewriter example 2017-05-27 19:14:25 +02:00
faiface 67a69d96d6 switch to OpenGL 2.1 2017-05-27 13:14:13 +02:00
faiface bbeab0aebf update 06 guide code 2017-05-26 21:26:06 +02:00
faiface d5f7088b7d update 05 guide code 2017-05-26 14:04:44 +02:00
faiface 9a401948ae remove accidentaly set theme 2017-05-25 22:51:17 +02:00
Michal Štrba abc99bdef8 Set theme jekyll-theme-time-machine 2017-05-25 20:03:02 +02:00
faiface debdbea894 limit ttf face glyphcacheentries size in typewriter example 2017-05-25 15:21:28 +02:00
faiface fc30e51016 update 04 guide code 2017-05-24 22:05:01 +02:00
faiface 90432a7857 update drawing features in README 2017-05-24 20:39:11 +02:00
faiface 51c32e407f update 03 guide code 2017-05-24 20:34:46 +02:00
faiface 659dc6fd5f update 02 guide code 2017-05-24 16:16:59 +02:00
faiface 50ba35d4cb add text input mention to README 2017-05-24 14:42:00 +02:00
faiface 9102076f1b add text drawing feature mention to README 2017-05-24 14:36:18 +02:00
faiface 2834411318 fix link in README 2017-05-24 14:34:49 +02:00
faiface 7fe45b5a88 replace xor screenshot with typewriter screenshot in README 2017-05-24 14:33:50 +02:00
faiface 51e1843f4b remove debug print from typewriter example 2017-05-24 02:21:25 +02:00
faiface eab5be6207 fiddle with constants in typewriter example 2017-05-24 00:01:33 +02:00
faiface 62fa797088 clarify typewriter doc 2017-05-23 22:06:43 +02:00
faiface 970faf0b63 clarify typewriter example readme 2017-05-23 20:40:45 +02:00
faiface 6c269fc8a5 add typewriter example 2017-05-23 20:07:23 +02:00
faiface a794d27972 clarify Rect doc 2017-05-21 19:38:21 +02:00
faiface bc08b65073 fix typo in ToRGBA doc 2017-05-21 19:33:01 +02:00
faiface bf6e20a04b fix Matrix doc 2017-05-21 19:30:29 +02:00
faiface ecdd8462bb replace complex128 Vec with a struct 2017-05-21 19:25:06 +02:00
faiface 3af9c2b20e remove Text.SetMatrix and Text.SetColorMask, add Text.Draw(target, matrix) and Text.DrawColorMask(target, matrix, mask) 2017-05-21 18:23:51 +02:00
faiface 3ae612a84d minor change 2017-05-21 18:23:20 +02:00
faiface 0ac5371d7e update Sprite's doc 2017-05-21 15:31:07 +02:00
faiface fccedc5a9d remove Text.Matrix() and Text.ColorMask() getters 2017-05-19 01:58:34 +02:00
faiface f7aac5ed09 change Text properties to fields 2017-05-18 23:59:42 +02:00
faiface 1d928485d6 change IMDraw properties to fields 2017-05-18 23:50:45 +02:00
faiface b832e83517 change Sprite.Draw and Canvas.Draw signatures (include Matrix) 2017-05-17 23:45:22 +02:00
faiface 53167788d6 accept zero target size in Rect.Resized 2017-05-15 01:15:16 +02:00
faiface 9d60c5fa32 improve Atlas creation time 2-3 times 2017-05-11 19:48:43 +02:00
faiface cce26f0a51 add Window.Repeat 2017-05-10 23:54:06 +02:00
faiface b15c10298e fix and simplify input handling in Window 2017-05-10 21:22:47 +02:00
faiface 3a14aae310 add Window.Typed 2017-05-10 21:10:10 +02:00
faiface e86120db20 change text.New to take an Atlas 2017-05-10 17:56:09 +02:00
faiface a510048648 add text package doc 2017-05-09 16:48:26 +02:00
faiface feb12a1c7e add Text.Matrix and Text.ColorMask 2017-05-09 16:46:11 +02:00
faiface 3ffbbb9cda add examples on LineHeight and TabWidth to Text doc 2017-05-09 16:40:44 +02:00
faiface 863e1e2f0c add note about not destroying face.Face to Atlas doc 2017-05-09 16:39:03 +02:00
faiface e3268db31e mention control characters in Text doc 2017-05-09 16:36:59 +02:00
faiface 7b10ad8497 remove accidental markdown formating from Text doc 2017-05-09 16:35:51 +02:00
faiface abcdff5960 fix typo in Text doc 2017-05-09 16:34:54 +02:00
faiface dc3f9857d8 minor change in Text doc 2017-05-09 16:34:13 +02:00
faiface dd6d38b8f3 minor change in Atlas doc 2017-05-09 16:32:29 +02:00
faiface 46122dd826 clarify doc 2017-05-09 16:31:09 +02:00
faiface c8114d8467 add Text doc 2017-05-09 16:27:55 +02:00
faiface f58676289a minor change 2017-05-09 15:26:50 +02:00
faiface 80735cfc0c add Atlas doc 2017-05-09 15:25:08 +02:00
faiface dcdc812af5 don't cache kerning in Atlas (too expensive and no benefit) 2017-05-09 15:10:35 +02:00
faiface 248d68f6aa improve Atlas creation, atlas is now square picture (was one row of characters) 2017-05-09 14:20:34 +02:00
faiface 3841afb70f fix PictureDataFromImage (wrong bounds when Min not (0, 0)) 2017-05-09 01:04:04 +02:00
faiface 217ac0c4d7 rename Text.LineHeight(scale) -> height, since now it's absolute height 2017-05-07 22:03:56 +02:00
faiface 65236863fe rename Glyph.Orig -> Dot 2017-05-07 21:49:26 +02:00
faiface cfaff8c0cb minor change 2017-05-07 21:12:48 +02:00
faiface 2d1f61f746 improve Text code 2017-05-07 21:08:10 +02:00
faiface 883bdc32c7 add Text.BoundsOf 2017-05-07 21:00:19 +02:00
faiface c9eea2639e restructure Text writing for more flexibility and consistency 2017-05-07 20:59:56 +02:00
faiface 5b524dadd8 add Atlas.DrawRune 2017-05-07 20:59:41 +02:00
faiface a86876a1cd Merge branch 'dev' into text 2017-05-06 22:58:36 +02:00
faiface 29785fb937 IMDraw: change default point color to (1, 1, 1, 1) (was (0, 0, 0, 0)) 2017-05-06 22:57:55 +02:00
faiface 1d17e45825 fix Text.Bounds 2017-05-05 16:42:40 +02:00
faiface b91d8be6cd remove Batch from Text and optimize it 2017-05-05 16:13:26 +02:00
faiface c47d77b2b5 add Text.Bounds, Atlas.Ascent, Atlas.Descent 2017-05-05 16:02:47 +02:00
faiface e9a3c900cf add Rect.Union 2017-05-05 15:43:24 +02:00
faiface 3d3cbd6027 fix Text.Color 2017-05-04 22:30:18 +02:00
faiface 4e6d6eeb3a move Atlas type to a separate file 2017-05-03 23:59:37 +02:00
faiface 17ddf4fec5 change Text.LineHeight to use actual units instead of scale (such as 1.5) 2017-05-03 23:57:09 +02:00
faiface e1e1815537 add Text.Atlas (Atlas has some useful stuff, e.g. line height) 2017-05-03 23:55:44 +02:00
faiface a05abdca76 add Text.WriteByte 2017-05-03 23:55:10 +02:00
faiface 317124058e export Atlas from text package + add Text.WriteRune and Text.WriteString 2017-05-03 23:54:24 +02:00
faiface 3b1e0eaa21 add Text.SetMatrix and Text.SetColorMask 2017-05-03 21:48:05 +02:00
faiface 6a7500959f add Text.LineHeight and text.TabWidth 2017-05-03 21:04:18 +02:00
faiface e2a16764c4 add Text.Orig (start of the text) 2017-05-03 20:56:06 +02:00
faiface 424b6e0f0b optimize Text.Write (remove one allocation) 2017-05-03 00:13:39 +02:00
faiface bc145eb1b8 add incomplete Text type in text package 2017-05-02 22:46:51 +02:00
faiface 5303ec5648 optimize Drawer.Dirty (defer the hard part until drawing) 2017-05-02 22:46:27 +02:00
faiface f7dad2f3c2 strikethrough advanced window manipulation mssing feature 2017-05-02 01:15:33 +02:00
faiface 3b5bfa90e6 fix creating window with no icon 2017-05-01 12:18:23 +02:00
faiface dff622523b just align doc comment to 100 chars per line 2017-05-01 01:38:57 +02:00
Michal Štrba 51a9702fc8 Merge pull request #14 from otraore/set-icon
Add support for setting an icon of a window
2017-05-01 01:33:59 +02:00
Ousmane Traore f7bb304b92 Set value rather than append 2017-04-30 19:30:11 -04:00
Ousmane Traore 5efd04b420 Address review comments 2017-04-30 18:43:05 -04:00
Ousmane Traore ea7bc5aff9 Add Icons paramter to window config 2017-04-30 17:19:51 -04:00
faiface 7b8a0c152e fix Window.CursorVisible intial value (was false) 2017-04-30 20:42:25 +02:00
Michal Štrba 2115296062 Merge pull request #13 from otraore/hide-cursor
Add ability to hide the cursor
2017-04-30 18:50:43 +02:00
Ousmane Traore dc745825d6 Address review comments 2017-04-30 12:33:27 -04:00
Ousmane Traore 29fe3b16ca Add ability to hide the cursor 2017-04-30 10:40:31 -04:00
faiface d37ad8f1ba fix two typos in comments in platformer example 2017-04-28 18:11:29 +02:00
faiface 85ba21a2f4 add Canvas.SetPixels and Canvas.Pixels methods 2017-04-28 13:24:30 +02:00
faiface 915faeee0c merge dev branch 2017-04-26 23:16:34 +02:00
faiface 113d052872 fix link in readme 2017-04-26 23:15:08 +02:00
faiface 9571b4339b add note to readme about macOS, go 1.8 and xcode problems 2017-04-26 23:14:01 +02:00
Michal Štrba 64aa32a4b4 Merge pull request #6 from faiface/dev
merge shader error fix
2017-04-26 18:01:45 +02:00
Michal Štrba 01db742aba Merge pull request #5 from ivanov/fix-4
fix "Attempt to use 'texture' as a variable" shader compiler error
2017-04-26 17:58:39 +02:00
Paul Ivanov ff64cf248b fix for 'texture' as a variable error, closes #4 2017-04-26 08:35:10 -07:00
faiface d595d5f647 fix compiler error (ouch) 2017-04-26 15:11:12 +02:00
faiface 951c7f4c59 remove PictureData.SetColor
was confusing since Pictures in e.g. Sprites are not really updatable
2017-04-26 15:04:46 +02:00
faiface e8aa765ed3 add a note about memory leaks to Drawer and Sprite docs 2017-04-26 14:28:25 +02:00
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
100 changed files with 6470 additions and 990 deletions

23
.travis.yml Normal file
View File

@ -0,0 +1,23 @@
language: go
sudo: false
addons:
apt:
packages:
- xorg-dev
- libx11-dev
- libxrandr-dev
- libxinerama-dev
- libxcursor-dev
- libxi-dev
- libopenal-dev
- libasound2-dev
go:
- 1.8
- 1.7.4
- tip
install:
- go get -t ./...
script:
- go test -i -race ./...
- go test -v -race ./...

14
CONTRIBUTING.md Normal file
View File

@ -0,0 +1,14 @@
# 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 [examples/community](examples/community) folder.
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 at the [Gitter](https://gitter.im/pixellib/Lobby), 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/.

163
README.md
View File

@ -1,5 +1,160 @@
# pixel
A simple and fast desktop multimedia/gamedev library.
# 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)
The core features of the library are pretty much completed. I'm doing some proofreading and
improving quality of the code. Will **announce** the library after.
A hand-crafted 2D game library in Go. Take a look into the [features](#features) to see what it can
do.
```
go get github.com/faiface/pixel
```
See [requirements](#requirements) for the list of libraries necessary for compilation.
## Tutorial
The [Wiki of this repo](https://github.com/faiface/pixel/wiki) contains an extensive tutorial
covering several topics of Pixel. Here's the content of the tutorial parts so far:
- [Creating a Window](https://github.com/faiface/pixel/wiki/Creating-a-Window)
- [Drawing a Sprite](https://github.com/faiface/pixel/wiki/Drawing-a-Sprite)
- [Moving, scaling and rotating with Matrix](https://github.com/faiface/pixel/wiki/Moving,-scaling-and-rotating-with-Matrix)
- [Pressing keys and clicking mouse](https://github.com/faiface/pixel/wiki/Pressing-keys-and-clicking-mouse)
- [Drawing efficiently with Batch](https://github.com/faiface/pixel/wiki/Drawing-efficiently-with-Batch)
- [Drawing shapes with IMDraw](https://github.com/faiface/pixel/wiki/Drawing-shapes-with-IMDraw)
- [Typing text on the screen](https://github.com/faiface/pixel/wiki/Typing-text-on-the-screen)
## Examples
The [examples](https://github.com/faiface/pixel/tree/master/examples) directory contains a few
examples demonstrating Pixel's functionality.
**To run an example**, navigate to it's directory, then `go run` the `main.go` file. For example:
```
$ cd examples/platformer
$ go run main.go
```
Here are some screenshots from the examples!
| [Lights](examples/lights) | [Platformer](examples/platformer) |
| --- | --- |
| ![Lights](examples/lights/screenshot.png) | ![Platformer](examples/platformer/screenshot.png) |
| [Smoke](examples/smoke) | [Typewriter](examples/typewriter) |
| --- | --- |
| ![Smoke](examples/smoke/screenshot.png) | ![Typewriter](examples/typewriter/screenshot.png) |
| [Raycaster](examples/community/raycaster) | [Starfield](examples/community/starfield) |
| --- | --- |
| ![Raycaster](examples/community/raycaster/screenshot.png) | ![Starfield](examples/community/starfield/screenshot.png) |
## Features
Here's the list of the main features in Pixel. Although Pixel is still under heavy development,
**there should be no major breakage in the API.** This is not a 100% guarantee, though.
- Fast 2D graphics
- Sprites
- Primitive shapes with immediate mode style
[IMDraw](https://github.com/faiface/pixel/wiki/Drawing-shapes-with-IMDraw) (circles, rectangles,
lines, ...)
- Optimized drawing with [Batch](https://github.com/faiface/pixel/wiki/Drawing-efficiently-with-Batch)
- 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
- Drawing a sprite to a window is as simple as `sprite.Draw(window, matrix)`
- Wanna know where the center of a window is? `window.Bounds().Center()`
- [...](https://godoc.org/github.com/faiface/pixel)
- Full documentation and tutorial
- Works on Linux, macOS and Windows
- Window creation and manipulation (resizing, fullscreen, multiple windows, ...)
- Keyboard (key presses, text input) and mouse input without events
- Well integrated with the Go standard library
- Use `"image"` package for loading pictures
- Use `"time"` package for measuring delta time and FPS
- Use `"image/color"` for colors, or use Pixel's own `color.Color` format, which supports easy
multiplication and a few more features
- Pixel uses `float64` throughout the library, compatible with `"math"` package
- Geometry transformations with
[Matrix](https://github.com/faiface/pixel/wiki/Moving,-scaling-and-rotating-with-Matrix)
- Moving, scaling, rotating
- Easy camera implementation
- Off-screen drawing to Canvas or any other target (Batch, IMDraw, ...)
- Fully garbage collected, no `Close` or `Dispose` methods
- Full [Porter-Duff](http://ssp.impulsetrain.com/porterduff.html) composition, which enables
- 2D lighting
- Cutting holes into objects
- Much more...
- Pixel let's you draw stuff and do your job, it doesn't impose any particular style or paradigm
- Platform and backend independent [core](https://godoc.org/github.com/faiface/pixel)
- Core Target/Triangles/Picture pattern makes it easy to create new drawing targets that do
arbitrarily crazy stuff (e.g. graphical effects)
- Small codebase, ~5K lines of code, including the backend [glhf](https://github.com/faiface/glhf)
package
## Missing features
Pixel is in development and still missing few critical features. Here're the most critical ones.
- ~~Audio~~
- ~~Drawing text~~
- Antialiasing (filtering is supported, though)
- ~~Advanced window manipulation (cursor hiding, window icon, ...)~~
- Better support for Hi-DPI displays
- Mobile (and perhaps HTML5?) backend
- More advanced graphical effects (e.g. blur)
- Tests and benchmarks
**Implementing these features will get us to the 1.0 release.** Contribute, so that it's as soon as
possible!
## Requirements
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
graphics. Because of that, OpenGL development libraries are needed for compilation. The dependencies
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
headers and libraries.
- On Ubuntu/Debian-like Linux distributions, you need `libgl1-mesa-dev` and `xorg-dev` packages.
- On CentOS/Fedora-like Linux distributions, you need `libX11-devel libXcursor-devel libXrandr-devel
libXinerama-devel mesa-libGL-devel libXi-devel` packages.
- See [here](http://www.glfw.org/docs/latest/compile.html#compile_deps) for full details.
**The combination of Go 1.8, macOS and latest XCode seems to be problematic** as mentioned in issue
[#7](https://github.com/faiface/pixel/issues/7). This issue is probably not related to Pixel.
**Upgrading to Go 1.8.1 fixes the issue.**
## Contributing
Pixel is in, let's say, mid-stage of development. Many of the important features are here, some are
missing. That's why **contributions are very important and welcome!** All alone, I will be able to
finish the library, but it'll take a lot of time. With your help, it'll take much less. I encourage
everyone to contribute, even with just an idea. Especially welcome are **issues** and **pull
requests**.
**However, I won't accept everything. Pixel is being developed with thought and care.** Each
component was designed and re-designed multiple times. Code and API quality is very important here.
API is focused on simplicity and expressiveness.
When contributing, keep these goals in mind. It doesn't mean that I'll only accept perfect pull
requests. It just means that I might not like your idea. Or that your pull requests could need some
rewriting. That's perfectly fine, don't let it put you off. In the end, we'll just end up with a
better result.
Take a look at [CONTRIBUTING.md](CONTRIBUTING.md) for further information.
For any kind of discussion, feel free to use our
[Gitter](https://gitter.im/pixellib/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
community.
## License
[MIT](LICENSE)

View File

@ -5,8 +5,7 @@ import (
"image/color"
)
// Batch is a Target that allows for efficient drawing of many objects with the same Picture (but
// different slices of the same Picture are allowed).
// Batch is a Target that allows for efficient drawing of many objects with the same Picture.
//
// To put an object into a Batch, just draw it onto it:
// object.Draw(batch)
@ -14,7 +13,7 @@ type Batch struct {
cont Drawer
mat Matrix
col NRGBA
col RGBA
}
var _ BasicTarget = (*Batch)(nil)
@ -29,7 +28,7 @@ var _ BasicTarget = (*Batch)(nil)
func NewBatch(container Triangles, pic Picture) *Batch {
b := &Batch{cont: Drawer{Triangles: container, Picture: pic}}
b.SetMatrix(IM)
b.SetColorMask(NRGBA{1, 1, 1, 1})
b.SetColorMask(Alpha(1))
return b
}
@ -63,10 +62,10 @@ func (b *Batch) SetMatrix(m Matrix) {
// SetColorMask sets a mask color used in the following draws onto the Batch.
func (b *Batch) SetColorMask(c color.Color) {
if c == nil {
b.col = NRGBA{1, 1, 1, 1}
b.col = Alpha(1)
return
}
b.col = ToNRGBA(c)
b.col = ToRGBA(c)
}
// MakeTriangles returns a specialized copy of the provided Triangles that draws onto this Batch.
@ -81,21 +80,19 @@ func (b *Batch) MakeTriangles(t Triangles) TargetTriangles {
// MakePicture returns a specialized copy of the provided Picture that draws onto this Batch.
func (b *Batch) MakePicture(p Picture) TargetPicture {
if p.Original() != b.cont.Picture.Original() {
panic(fmt.Errorf("(%T).MakePicture: Picture is not a slice of Batch's Picture", b))
if p != b.cont.Picture {
panic(fmt.Errorf("(%T).MakePicture: Picture is not the Batch's Picture", b))
}
bp := &batchPicture{
pic: p,
dst: b,
}
bp.orig = bp
return bp
}
type batchTriangles struct {
tri Triangles
tmp *TrianglesData
dst *Batch
}
@ -149,27 +146,14 @@ func (bt *batchTriangles) Draw() {
}
type batchPicture struct {
pic Picture
orig *batchPicture
dst *Batch
pic Picture
dst *Batch
}
func (bp *batchPicture) Bounds() Rect {
return bp.pic.Bounds()
}
func (bp *batchPicture) Slice(r Rect) Picture {
return &batchPicture{
pic: bp.pic.Slice(r),
orig: bp.orig,
dst: bp.dst,
}
}
func (bp *batchPicture) Original() Picture {
return bp.orig
}
func (bp *batchPicture) Draw(t TargetTriangles) {
bt := t.(*batchTriangles)
if bp.dst != bt.dst {

View File

@ -2,17 +2,30 @@ package pixel
import "image/color"
// NRGBA represents a non-alpha-premultiplied RGBA color with components within range [0, 1].
// RGBA represents an alpha-premultiplied RGBA color with components within range [0, 1].
//
// The difference between color.NRGBA is that the value range is [0, 1] and the values are floats.
type NRGBA struct {
// The difference between color.RGBA is that the value range is [0, 1] and the values are floats.
type RGBA struct {
R, G, B, A float64
}
// RGB returns a fully opaque RGBA color with the given RGB values.
//
// A common way to construct a transparent color is to create one with RGB constructor, then
// multiply it by a color obtained from the Alpha constructor.
func RGB(r, g, b float64) RGBA {
return RGBA{r, g, b, 1}
}
// Alpha returns a white RGBA color with the given alpha component.
func Alpha(a float64) RGBA {
return RGBA{a, a, a, a}
}
// Add adds color d to color c component-wise and returns the result (the components are not
// clamped).
func (c NRGBA) Add(d NRGBA) NRGBA {
return NRGBA{
func (c RGBA) Add(d RGBA) RGBA {
return RGBA{
R: c.R + d.R,
G: c.G + d.G,
B: c.B + d.B,
@ -22,8 +35,8 @@ func (c NRGBA) Add(d NRGBA) NRGBA {
// Sub subtracts color d from color c component-wise and returns the result (the components
// are not clamped).
func (c NRGBA) Sub(d NRGBA) NRGBA {
return NRGBA{
func (c RGBA) Sub(d RGBA) RGBA {
return RGBA{
R: c.R - d.R,
G: c.G - d.G,
B: c.B - d.B,
@ -32,8 +45,8 @@ func (c NRGBA) Sub(d NRGBA) NRGBA {
}
// Mul multiplies color c by color d component-wise (the components are not clamped).
func (c NRGBA) Mul(d NRGBA) NRGBA {
return NRGBA{
func (c RGBA) Mul(d RGBA) RGBA {
return RGBA{
R: c.R * d.R,
G: c.G * d.G,
B: c.B * d.B,
@ -43,8 +56,8 @@ func (c NRGBA) Mul(d NRGBA) NRGBA {
// Scaled multiplies each component of color c by scale and returns the result (the components
// are not clamped).
func (c NRGBA) Scaled(scale float64) NRGBA {
return NRGBA{
func (c RGBA) Scaled(scale float64) RGBA {
return RGBA{
R: c.R * scale,
G: c.G * scale,
B: c.B * scale,
@ -52,58 +65,33 @@ func (c NRGBA) Scaled(scale float64) NRGBA {
}
}
// RGBA returns alpha-premultiplied red, green, blue and alpha components of the NRGBA color.
func (c NRGBA) RGBA() (r, g, b, a uint32) {
c.R = clamp(c.R, 0, 1)
c.G = clamp(c.G, 0, 1)
c.B = clamp(c.B, 0, 1)
c.A = clamp(c.A, 0, 1)
r = uint32(0xffff * c.R * c.A)
g = uint32(0xffff * c.G * c.A)
b = uint32(0xffff * c.B * c.A)
// RGBA returns alpha-premultiplied red, green, blue and alpha components of the RGBA color.
func (c RGBA) RGBA() (r, g, b, a uint32) {
r = uint32(0xffff * c.R)
g = uint32(0xffff * c.G)
b = uint32(0xffff * c.B)
a = uint32(0xffff * c.A)
return
}
func clamp(x, low, high float64) float64 {
if x < low {
return low
}
if x > high {
return high
}
return x
}
// ToNRGBA converts a color to NRGBA format. Using this function is preferred to using NRGBAModel,
// for performance (using NRGBAModel introduced additional unnecessary allocations).
func ToNRGBA(c color.Color) NRGBA {
if c, ok := c.(NRGBA); ok {
// ToRGBA converts a color to RGBA format. Using this function is preferred to using RGBAModel, for
// performance (using RGBAModel introduces additional unnecessary allocations).
func ToRGBA(c color.Color) RGBA {
if c, ok := c.(RGBA); ok {
return c
}
if c, ok := c.(color.NRGBA); ok {
return NRGBA{
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()
if a == 0 {
return NRGBA{0, 0, 0, 0}
}
return NRGBA{
float64(r) / float64(a),
float64(g) / float64(a),
float64(b) / float64(a),
return RGBA{
float64(r) / 0xffff,
float64(g) / 0xffff,
float64(b) / 0xffff,
float64(a) / 0xffff,
}
}
// NRGBAModel converts colors to NRGBA format.
var NRGBAModel = color.ModelFunc(nrgbaModel)
// RGBAModel converts colors to RGBA format.
var RGBAModel = color.ModelFunc(rgbaModel)
func nrgbaModel(c color.Color) color.Color {
return ToNRGBA(c)
func rgbaModel(c color.Color) color.Color {
return ToRGBA(c)
}

24
color_test.go Normal file
View File

@ -0,0 +1,24 @@
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)
}
})
}
}

65
compose.go Normal file
View File

@ -0,0 +1,65 @@
package pixel
import "errors"
// ComposeTarget is a BasicTarget capable of Porter-Duff composition.
type ComposeTarget interface {
BasicTarget
// SetComposeMethod sets a Porter-Duff composition method to be used.
SetComposeMethod(ComposeMethod)
}
// ComposeMethod is a Porter-Duff composition method.
type ComposeMethod int
// Here's the list of all available Porter-Duff composition methods. Use ComposeOver for the basic
// alpha blending.
const (
ComposeOver ComposeMethod = iota
ComposeIn
ComposeOut
ComposeAtop
ComposeRover
ComposeRin
ComposeRout
ComposeRatop
ComposeXor
ComposePlus
ComposeCopy
)
// Compose composes two colors together according to the ComposeMethod. A is the foreground, B is
// the background.
func (cm ComposeMethod) Compose(a, b RGBA) RGBA {
var fa, fb float64
switch cm {
case ComposeOver:
fa, fb = 1, 1-a.A
case ComposeIn:
fa, fb = b.A, 0
case ComposeOut:
fa, fb = 1-b.A, 0
case ComposeAtop:
fa, fb = b.A, 1-a.A
case ComposeRover:
fa, fb = 1-b.A, 1
case ComposeRin:
fa, fb = 0, a.A
case ComposeRout:
fa, fb = 0, 1-a.A
case ComposeRatop:
fa, fb = 1-b.A, a.A
case ComposeXor:
fa, fb = 1-b.A, 1-a.A
case ComposePlus:
fa, fb = 1, 1
case ComposeCopy:
fa, fb = 1, 0
default:
panic(errors.New("Compose: invalid ComposeMethod"))
}
return a.Mul(Alpha(fa)).Add(b.Mul(Alpha(fb)))
}

142
data.go
View File

@ -12,7 +12,7 @@ import (
// TrianglesPosition, TrianglesColor and TrianglesPicture.
type TrianglesData []struct {
Position Vec
Color NRGBA
Color RGBA
Picture Vec
Intensity float64
}
@ -42,10 +42,10 @@ func (td *TrianglesData) SetLen(len int) {
for i := 0; i < needAppend; i++ {
*td = append(*td, struct {
Position Vec
Color NRGBA
Color RGBA
Picture Vec
Intensity float64
}{V(0, 0), NRGBA{1, 1, 1, 1}, V(0, 0), 0})
}{Color: RGBA{1, 1, 1, 1}})
}
}
if len < td.Len() {
@ -108,7 +108,7 @@ func (td *TrianglesData) Position(i int) Vec {
}
// Color returns the color property of i-th vertex.
func (td *TrianglesData) Color(i int) NRGBA {
func (td *TrianglesData) Color(i int) RGBA {
return (*td)[i].Color
}
@ -126,36 +126,34 @@ func (td *TrianglesData) Picture(i int) (pic Vec, intensity float64) {
//
// The struct's innards are exposed for convenience, manual modification is at your own risk.
//
// The format of the pixels is color.NRGBA and not pixel.NRGBA for a very serious reason:
// pixel.NRGBA takes up 8x more memory than color.NRGBA.
// The format of the pixels is color.RGBA and not pixel.RGBA for a very serious reason:
// pixel.RGBA takes up 8x more memory than color.RGBA.
type PictureData struct {
Pix []color.NRGBA
Pix []color.RGBA
Stride int
Rect Rect
Orig *PictureData
}
// MakePictureData creates a zero-initialized PictureData covering the given rectangle.
func MakePictureData(rect Rect) *PictureData {
w := int(math.Ceil(rect.Max.X())) - int(math.Floor(rect.Min.X()))
h := int(math.Ceil(rect.Max.Y())) - int(math.Floor(rect.Min.Y()))
w := int(math.Ceil(rect.Max.X)) - int(math.Floor(rect.Min.X))
h := int(math.Ceil(rect.Max.Y)) - int(math.Floor(rect.Min.Y))
pd := &PictureData{
Stride: w,
Rect: rect,
}
pd.Pix = make([]color.NRGBA, w*h)
pd.Orig = pd
pd.Pix = make([]color.RGBA, w*h)
return pd
}
func verticalFlip(nrgba *image.NRGBA) {
bounds := nrgba.Bounds()
func verticalFlip(rgba *image.RGBA) {
bounds := rgba.Bounds()
width := bounds.Dx()
tmpRow := make([]uint8, width*4)
for i, j := 0, bounds.Dy()-1; i < j; i, j = i+1, j-1 {
iRow := nrgba.Pix[i*nrgba.Stride : i*nrgba.Stride+width*4]
jRow := nrgba.Pix[j*nrgba.Stride : j*nrgba.Stride+width*4]
iRow := rgba.Pix[i*rgba.Stride : i*rgba.Stride+width*4]
jRow := rgba.Pix[j*rgba.Stride : j*rgba.Stride+width*4]
copy(tmpRow, iRow)
copy(iRow, jRow)
@ -167,28 +165,28 @@ func verticalFlip(nrgba *image.NRGBA) {
//
// The resulting PictureData's Bounds will be the equivalent of the supplied image.Image's Bounds.
func PictureDataFromImage(img image.Image) *PictureData {
var nrgba *image.NRGBA
if nrgbaImg, ok := img.(*image.NRGBA); ok {
nrgba = nrgbaImg
var rgba *image.RGBA
if rgbaImg, ok := img.(*image.RGBA); ok {
rgba = rgbaImg
} else {
nrgba = image.NewNRGBA(img.Bounds())
draw.Draw(nrgba, nrgba.Bounds(), img, img.Bounds().Min, draw.Src)
rgba = image.NewRGBA(img.Bounds())
draw.Draw(rgba, rgba.Bounds(), img, img.Bounds().Min, draw.Src)
}
verticalFlip(nrgba)
verticalFlip(rgba)
pd := MakePictureData(R(
float64(nrgba.Bounds().Min.X),
float64(nrgba.Bounds().Min.Y),
float64(nrgba.Bounds().Dx()),
float64(nrgba.Bounds().Dy()),
float64(rgba.Bounds().Min.X),
float64(rgba.Bounds().Min.Y),
float64(rgba.Bounds().Max.X),
float64(rgba.Bounds().Max.Y),
))
for i := range pd.Pix {
pd.Pix[i].R = nrgba.Pix[i*4+0]
pd.Pix[i].G = nrgba.Pix[i*4+1]
pd.Pix[i].B = nrgba.Pix[i*4+2]
pd.Pix[i].A = nrgba.Pix[i*4+3]
pd.Pix[i].R = rgba.Pix[i*4+0]
pd.Pix[i].G = rgba.Pix[i*4+1]
pd.Pix[i].B = rgba.Pix[i*4+2]
pd.Pix[i].A = rgba.Pix[i*4+3]
}
return pd
@ -207,14 +205,20 @@ func PictureDataFromPicture(pic Picture) *PictureData {
pd := MakePictureData(bounds)
if pic, ok := pic.(PictureColor); ok {
for y := math.Floor(bounds.Min.Y()); y < bounds.Max.Y(); y++ {
for x := math.Floor(bounds.Min.X()); x < bounds.Max.X(); x++ {
for y := math.Floor(bounds.Min.Y); y < bounds.Max.Y; y++ {
for x := math.Floor(bounds.Min.X); x < bounds.Max.X; x++ {
// this together with the Floor is a trick to get all of the pixels
at := V(
math.Max(x, bounds.Min.X()),
math.Max(y, bounds.Min.Y()),
math.Max(x, bounds.Min.X),
math.Max(y, bounds.Min.Y),
)
pd.SetColor(at, pic.Color(at))
col := pic.Color(at)
pd.Pix[pd.Index(at)] = color.RGBA{
R: uint8(col.R * 255),
G: uint8(col.G * 255),
B: uint8(col.B * 255),
A: uint8(col.A * 255),
}
}
}
}
@ -222,39 +226,39 @@ func PictureDataFromPicture(pic Picture) *PictureData {
return pd
}
// Image converts PictureData into an image.NRGBA.
// Image converts PictureData into an image.RGBA.
//
// The resulting image.NRGBA's Bounds will be equivalent of the PictureData's Bounds.
func (pd *PictureData) Image() *image.NRGBA {
// The resulting image.RGBA's Bounds will be equivalent of the PictureData's Bounds.
func (pd *PictureData) Image() *image.RGBA {
bounds := image.Rect(
int(math.Floor(pd.Rect.Min.X())),
int(math.Floor(pd.Rect.Min.Y())),
int(math.Ceil(pd.Rect.Max.X())),
int(math.Ceil(pd.Rect.Max.Y())),
int(math.Floor(pd.Rect.Min.X)),
int(math.Floor(pd.Rect.Min.Y)),
int(math.Ceil(pd.Rect.Max.X)),
int(math.Ceil(pd.Rect.Max.Y)),
)
nrgba := image.NewNRGBA(bounds)
rgba := image.NewRGBA(bounds)
i := 0
for y := bounds.Min.Y; y < bounds.Max.Y; y++ {
for x := bounds.Min.X; x < bounds.Max.X; x++ {
off := pd.Index(V(float64(x), float64(y)))
nrgba.Pix[i*4+0] = pd.Pix[off].R
nrgba.Pix[i*4+1] = pd.Pix[off].G
nrgba.Pix[i*4+2] = pd.Pix[off].B
nrgba.Pix[i*4+3] = pd.Pix[off].A
rgba.Pix[i*4+0] = pd.Pix[off].R
rgba.Pix[i*4+1] = pd.Pix[off].G
rgba.Pix[i*4+2] = pd.Pix[off].B
rgba.Pix[i*4+3] = pd.Pix[off].A
i++
}
}
verticalFlip(nrgba)
verticalFlip(rgba)
return nrgba
return rgba
}
// Index returns the index of the pixel at the specified position inside the Pix slice.
func (pd *PictureData) Index(at Vec) int {
at -= pd.Rect.Min.Map(math.Floor)
x, y := int(at.X()), int(at.Y())
at = at.Sub(pd.Rect.Min.Map(math.Floor))
x, y := int(at.X), int(at.Y)
return y*pd.Stride + x
}
@ -263,40 +267,10 @@ func (pd *PictureData) Bounds() Rect {
return pd.Rect
}
// Slice returns a sub-Picture of this PictureData inside the supplied rectangle.
func (pd *PictureData) Slice(r Rect) Picture {
return &PictureData{
Pix: pd.Pix[pd.Index(r.Min):],
Stride: pd.Stride,
Rect: r,
Orig: pd.Orig,
}
}
// Original returns the most original PictureData that this PictureData was obtained from using
// Slice-ing.
func (pd *PictureData) Original() Picture {
return pd.Orig
}
// Color returns the color located at the given position.
func (pd *PictureData) Color(at Vec) NRGBA {
func (pd *PictureData) Color(at Vec) RGBA {
if !pd.Rect.Contains(at) {
return NRGBA{0, 0, 0, 0}
}
return ToNRGBA(pd.Pix[pd.Index(at)])
}
// SetColor changes the color located at the given position.
func (pd *PictureData) SetColor(at Vec, col color.Color) {
if !pd.Rect.Contains(at) {
return
}
nrgba := ToNRGBA(col)
pd.Pix[pd.Index(at)] = color.NRGBA{
R: uint8(nrgba.R * 255),
G: uint8(nrgba.G * 255),
B: uint8(nrgba.B * 255),
A: uint8(nrgba.A * 255),
return RGBA{0, 0, 0, 0}
}
return ToRGBA(pd.Pix[pd.Index(at)])
}

7
doc.go Normal file
View File

@ -0,0 +1,7 @@
// Package pixel implements platform and backend agnostic core of the Pixel game development
// library.
//
// It specifies the core Target, Triangles, Picture pattern and implements standard elements, such
// as Sprite, Batch, Vec, Matrix and RGBA in addition to the basic Triangles and Picture
// implementations: TrianglesData and PictureData.
package pixel

View File

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

18
drawer_test.go Normal file
View File

@ -0,0 +1,18 @@
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,16 @@
# bouncing
Bouncing particles using the [imdraw](https://godoc.org/github.com/faiface/pixel/imdraw) package.
Made by [Peter Hellberg](https://github.com/peterhellberg/) as part of his [pixel-experiments](https://github.com/peterhellberg/pixel-experiments)
## Screenshots
![bouncing animation](https://user-images.githubusercontent.com/565124/32401910-7cd87fb2-c119-11e7-8121-7fb46e5e11a8.gif)
![bouncing screenshot](screenshot.png)
## Links
- https://github.com/peterhellberg/pixel-experiments/tree/master/bouncing
- https://gist.github.com/peterhellberg/674f32a15a7d2d249e634ce781f333e8

View File

@ -0,0 +1,303 @@
package main
import (
"image/color"
"math"
"math/rand"
"time"
"github.com/faiface/pixel"
"github.com/faiface/pixel/imdraw"
"github.com/faiface/pixel/pixelgl"
)
var (
w, h, s, scale = float64(640), float64(360), float64(2.3), float64(32)
p, bg = newPalette(Colors), color.RGBA{32, p.color().G, 32, 255}
balls = []*ball{
newRandomBall(scale),
newRandomBall(scale),
}
)
func run() {
win, err := pixelgl.NewWindow(pixelgl.WindowConfig{
Bounds: pixel.R(0, 0, w, h),
VSync: true,
Undecorated: true,
})
if err != nil {
panic(err)
}
imd := imdraw.New(nil)
imd.EndShape = imdraw.RoundEndShape
imd.Precision = 3
go func() {
start := time.Now()
for range time.Tick(16 * time.Millisecond) {
bg = color.RGBA{32 + (p.color().R/128)*4, 32 + (p.color().G/128)*4, 32 + (p.color().B/128)*4, 255}
s = pixel.V(math.Sin(time.Since(start).Seconds())*0.8, 0).Len()*2 - 1
scale = 64 + 15*s
imd.Intensity = 1.2 * s
}
}()
for !win.Closed() {
win.SetClosed(win.JustPressed(pixelgl.KeyEscape) || win.JustPressed(pixelgl.KeyQ))
if win.JustPressed(pixelgl.KeySpace) {
for _, ball := range balls {
ball.color = ball.palette.next()
}
}
if win.JustPressed(pixelgl.KeyEnter) {
for _, ball := range balls {
ball.pos = center()
ball.vel = randomVelocity()
}
}
imd.Clear()
for _, ball := range balls {
imd.Color = ball.color
imd.Push(ball.pos)
}
imd.Polygon(scale)
for _, ball := range balls {
imd.Color = color.RGBA{ball.color.R, ball.color.G, ball.color.B, 128 - uint8(128*s)}
imd.Push(ball.pos)
}
imd.Polygon(scale * s)
for _, ball := range balls {
aliveParticles := []*particle{}
for _, particle := range ball.particles {
if particle.life > 0 {
aliveParticles = append(aliveParticles, particle)
}
}
for _, particle := range aliveParticles {
imd.Color = particle.color
imd.Push(particle.pos)
imd.Circle(16*particle.life, 0)
}
}
win.Clear(bg)
imd.Draw(win)
win.Update()
}
}
func main() {
rand.Seed(4)
go func() {
for range time.Tick(32 * time.Millisecond) {
for _, ball := range balls {
go ball.update()
for _, particle := range ball.particles {
go particle.update()
}
}
}
}()
pixelgl.Run(run)
}
func newParticleAt(pos, vel pixel.Vec) *particle {
c := p.color()
c.A = 5
return &particle{pos, vel, c, rand.Float64() * 1.5}
}
func newRandomBall(radius float64) *ball {
return &ball{
center(), randomVelocity(),
math.Pi * (radius * radius),
radius, p.random(), p, []*particle{},
}
}
func center() pixel.Vec {
return pixel.V(w/2, h/2)
}
func randomVelocity() pixel.Vec {
return pixel.V((rand.Float64()*2)-1, (rand.Float64()*2)-1).Scaled(scale / 4)
}
type particle struct {
pos pixel.Vec
vel pixel.Vec
color color.RGBA
life float64
}
func (p *particle) update() {
p.pos = p.pos.Add(p.vel)
p.life -= 0.03
switch {
case p.pos.Y < 0 || p.pos.Y >= h:
p.vel.Y *= -1.0
case p.pos.X < 0 || p.pos.X >= w:
p.vel.X *= -1.0
}
}
type ball struct {
pos pixel.Vec
vel pixel.Vec
mass float64
radius float64
color color.RGBA
palette *Palette
particles []*particle
}
func (b *ball) update() {
b.pos = b.pos.Add(b.vel)
var bounced bool
switch {
case b.pos.Y <= b.radius || b.pos.Y >= h-b.radius:
b.vel.Y *= -1.0
bounced = true
if b.pos.Y < b.radius {
b.pos.Y = b.radius
} else {
b.pos.Y = h - b.radius
}
case b.pos.X <= b.radius || b.pos.X >= w-b.radius:
b.vel.X *= -1.0
bounced = true
if b.pos.X < b.radius {
b.pos.X = b.radius
} else {
b.pos.X = w - b.radius
}
}
for _, a := range balls {
if a != b {
d := a.pos.Sub(b.pos)
if d.Len() > a.radius+b.radius {
continue
}
pen := d.Unit().Scaled(a.radius + b.radius - d.Len())
a.pos = a.pos.Add(pen.Scaled(b.mass / (a.mass + b.mass)))
b.pos = b.pos.Sub(pen.Scaled(a.mass / (a.mass + b.mass)))
u := d.Unit()
v := 2 * (a.vel.Dot(u) - b.vel.Dot(u)) / (a.mass + b.mass)
a.vel = a.vel.Sub(u.Scaled(v * b.mass))
b.vel = b.vel.Add(u.Scaled(v * a.mass))
bounced = true
}
}
if bounced {
b.color = p.next()
b.particles = append(b.particles,
newParticleAt(b.pos, b.vel.Rotated(1).Scaled(rand.Float64())),
newParticleAt(b.pos, b.vel.Rotated(2).Scaled(rand.Float64())),
newParticleAt(b.pos, b.vel.Rotated(3).Scaled(rand.Float64())),
newParticleAt(b.pos, b.vel.Rotated(4).Scaled(rand.Float64())),
newParticleAt(b.pos, b.vel.Rotated(5).Scaled(rand.Float64())),
newParticleAt(b.pos, b.vel.Rotated(6).Scaled(rand.Float64())),
newParticleAt(b.pos, b.vel.Rotated(7).Scaled(rand.Float64())),
newParticleAt(b.pos, b.vel.Rotated(8).Scaled(rand.Float64())),
newParticleAt(b.pos, b.vel.Rotated(9).Scaled(rand.Float64())),
newParticleAt(b.pos, b.vel.Rotated(10).Scaled(rand.Float64()+1)),
newParticleAt(b.pos, b.vel.Rotated(20).Scaled(rand.Float64()+1)),
newParticleAt(b.pos, b.vel.Rotated(30).Scaled(rand.Float64()+1)),
newParticleAt(b.pos, b.vel.Rotated(40).Scaled(rand.Float64()+1)),
newParticleAt(b.pos, b.vel.Rotated(50).Scaled(rand.Float64()+1)),
newParticleAt(b.pos, b.vel.Rotated(60).Scaled(rand.Float64()+1)),
newParticleAt(b.pos, b.vel.Rotated(70).Scaled(rand.Float64()+1)),
newParticleAt(b.pos, b.vel.Rotated(80).Scaled(rand.Float64()+1)),
newParticleAt(b.pos, b.vel.Rotated(90).Scaled(rand.Float64()+1)),
)
}
}
func newPalette(cc []color.Color) *Palette {
colors := []color.RGBA{}
for _, v := range cc {
if c, ok := v.(color.RGBA); ok {
colors = append(colors, c)
}
}
return &Palette{colors, len(colors), 0}
}
type Palette struct {
colors []color.RGBA
size int
index int
}
func (p *Palette) clone() *Palette {
return &Palette{p.colors, p.size, p.index}
}
func (p *Palette) next() color.RGBA {
if p.index++; p.index >= p.size {
p.index = 0
}
return p.colors[p.index]
}
func (p *Palette) color() color.RGBA {
return p.colors[p.index]
}
func (p *Palette) random() color.RGBA {
p.index = rand.Intn(p.size)
return p.colors[p.index]
}
var Colors = []color.Color{
color.RGBA{190, 38, 51, 255},
color.RGBA{224, 111, 139, 255},
color.RGBA{73, 60, 43, 255},
color.RGBA{164, 100, 34, 255},
color.RGBA{235, 137, 49, 255},
color.RGBA{247, 226, 107, 255},
color.RGBA{47, 72, 78, 255},
color.RGBA{68, 137, 26, 255},
color.RGBA{163, 206, 39, 255},
color.RGBA{0, 87, 132, 255},
color.RGBA{49, 162, 242, 255},
color.RGBA{178, 220, 239, 255},
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

View File

@ -0,0 +1,20 @@
# Conway's Game of Lfe
Created by [Nathan Leniz](https://github.com/terakilobyte).
Inspired by and heavily uses [the doc](https://golang.org/doc/play/life.go)
> The Game of Life, also known simply as Life, is a cellular automaton devised by the British mathematician John Horton Conway in 1970. The "game" is a zero-player game, meaning that its evolution is determined by its initial state, requiring no further input. One interacts with the Game of Life by creating an initial configuration and observing how it evolves, or, for advanced "players", by creating patterns with particular properties. The Game has been reprogrammed multiple times in various coding languages.
For more information, please see the [wikipedia](https://en.wikipedia.org/wiki/Conway%27s_Game_of_Life) article.
## Use
go run main.go -h
-frameRate duration
The framerate in milliseconds (default 33ms)
-size int
The size of each cell (default 5)
-windowSize float
The pixel size of one side of the grid (default 800)
![life](life.png)

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

View File

@ -0,0 +1,74 @@
package life
import (
"github.com/faiface/pixel"
"github.com/faiface/pixel/imdraw"
"golang.org/x/image/colornames"
)
// Shamelessly taken/inspired by https://golang.org/doc/play/life.go
// Grid is the structure in which the cellular automota live
type Grid struct {
h int
cellSize int
Cells [][]bool
}
// NewGrid constructs a new Grid
func NewGrid(h, size int) *Grid {
cells := make([][]bool, h)
for i := 0; i < h; i++ {
cells[i] = make([]bool, h)
}
return &Grid{h: h, cellSize: size, Cells: cells}
}
// Alive returns whether the specified position is alive
func (g *Grid) Alive(x, y int) bool {
x += g.h
x %= g.h
y += g.h
y %= g.h
return g.Cells[y][x]
}
// Set sets the state of a specific location
func (g *Grid) Set(x, y int, state bool) {
g.Cells[y][x] = state
}
// Draw draws the grid
func (g *Grid) Draw(imd *imdraw.IMDraw) {
for i := 0; i < g.h; i++ {
for j := 0; j < g.h; j++ {
if g.Alive(i, j) {
imd.Color = colornames.Black
} else {
imd.Color = colornames.White
}
imd.Push(
pixel.V(float64(i*g.cellSize), float64(j*g.cellSize)),
pixel.V(float64(i*g.cellSize+g.cellSize), float64(j*g.cellSize+g.cellSize)),
)
imd.Rectangle(0)
}
}
}
// Next returns the next state
func (g *Grid) Next(x, y int) bool {
// Count the adjacent cells that are alive.
alive := 0
for i := -1; i <= 1; i++ {
for j := -1; j <= 1; j++ {
if (j != 0 || i != 0) && g.Alive(x+i, y+j) {
alive++
}
}
}
// Return next state according to the game rules:
// exactly 3 neighbors: on,
// exactly 2 neighbors: maintain current state,
// otherwise: off.
return alive == 3 || alive == 2 && g.Alive(x, y)
}

View File

@ -0,0 +1,35 @@
// Package life manages the "game" state
// Shamelessly taken from https://golang.org/doc/play/life.go
package life
import "math/rand"
// Life stores the state of a round of Conway's Game of Life.
type Life struct {
A, b *Grid
h int
}
// NewLife returns a new Life game state with a random initial state.
func NewLife(h, size int) *Life {
a := NewGrid(h, size)
for i := 0; i < (h * h / 2); i++ {
a.Set(rand.Intn(h), rand.Intn(h), true)
}
return &Life{
A: a, b: NewGrid(h, size),
h: h,
}
}
// Step advances the game by one instant, recomputing and updating all cells.
func (l *Life) Step() {
// Update the state of the next field (b) from the current field (a).
for y := 0; y < l.h; y++ {
for x := 0; x < l.h; x++ {
l.b.Set(x, y, l.A.Next(x, y))
}
}
// Swap fields a and b.
l.A, l.b = l.b, l.A
}

View File

@ -0,0 +1,64 @@
package main
import (
"flag"
"math/rand"
"time"
"golang.org/x/image/colornames"
"github.com/faiface/pixel"
"github.com/faiface/pixel/examples/community/game_of_life/life"
"github.com/faiface/pixel/imdraw"
"github.com/faiface/pixel/pixelgl"
)
var (
size *int
windowSize *float64
frameRate *time.Duration
)
func init() {
rand.Seed(time.Now().UnixNano())
size = flag.Int("size", 5, "The size of each cell")
windowSize = flag.Float64("windowSize", 800, "The pixel size of one side of the grid")
frameRate = flag.Duration("frameRate", 33*time.Millisecond, "The framerate in milliseconds")
flag.Parse()
}
func main() {
pixelgl.Run(run)
}
func run() {
cfg := pixelgl.WindowConfig{
Title: "Pixel Rocks!",
Bounds: pixel.R(0, 0, *windowSize, *windowSize),
VSync: true,
}
win, err := pixelgl.NewWindow(cfg)
if err != nil {
panic(err)
}
win.Clear(colornames.White)
// since the game board is square, rows and cols will be the same
rows := int(*windowSize) / *size
gridDraw := imdraw.New(nil)
game := life.NewLife(rows, *size)
tick := time.Tick(*frameRate)
for !win.Closed() {
// game loop
select {
case <-tick:
gridDraw.Clear()
game.A.Draw(gridDraw)
gridDraw.Draw(win)
game.Step()
}
win.Update()
}
}

View File

@ -0,0 +1,13 @@
# Isometric view basics
Created by [Sergio Vera](https://github.com/svera).
Isometric view is a display method used to create an illusion of 3D for an otherwise 2D game - sometimes referred to as pseudo 3D or 2.5D.
Implementing an isometric view can be done in many ways, but for the sake of simplicity we'll implement a tile-based approach, which is the most efficient and widely used method.
In the tile-based approach, each visual element is broken down into smaller pieces, called tiles, of a standard size. These tiles will be arranged to form the game world according to pre-determined level data - usually a 2D array.
For a detailed explanation about the maths behind this, read [http://clintbellanger.net/articles/isometric_math/](http://clintbellanger.net/articles/isometric_math/).
![Result](result.png)

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

View File

@ -0,0 +1,102 @@
package main
import (
"image"
"os"
"github.com/faiface/pixel"
"github.com/faiface/pixel/pixelgl"
_ "image/png"
)
const (
windowWidth = 800
windowHeight = 800
// sprite tiles are squared, 64x64 size
tileSize = 64
f = 0 // floor identifier
w = 1 // wall identifier
)
var levelData = [][]uint{
{f, f, f, f, f, f}, // This row will be rendered in the lower left part of the screen (closer to the viewer)
{w, f, f, f, f, w},
{w, f, f, f, f, w},
{w, f, f, f, f, w},
{w, f, f, f, f, w},
{w, w, w, w, w, w}, // And this in the upper right
}
var win *pixelgl.Window
var offset = pixel.V(400, 325)
var floorTile, wallTile *pixel.Sprite
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() {
var err error
cfg := pixelgl.WindowConfig{
Title: "Isometric demo",
Bounds: pixel.R(0, 0, windowWidth, windowHeight),
VSync: true,
}
win, err = pixelgl.NewWindow(cfg)
if err != nil {
panic(err)
}
pic, err := loadPicture("castle.png")
if err != nil {
panic(err)
}
wallTile = pixel.NewSprite(pic, pixel.R(0, 448, tileSize, 512))
floorTile = pixel.NewSprite(pic, pixel.R(0, 128, tileSize, 192))
depthSort()
for !win.Closed() {
win.Update()
}
}
// Draw level data tiles to window, from farthest to closest.
// In order to achieve the depth effect, we need to render tiles up to down, being lower
// closer to the viewer (see painter's algorithm). To do that, we need to process levelData in reverse order,
// so its first row is rendered last, as OpenGL considers its origin to be in the lower left corner of the display.
func depthSort() {
for x := len(levelData) - 1; x >= 0; x-- {
for y := len(levelData[x]) - 1; y >= 0; y-- {
isoCoords := cartesianToIso(pixel.V(float64(x), float64(y)))
mat := pixel.IM.Moved(offset.Add(isoCoords))
// Not really needed, just put to show bigger blocks
mat = mat.ScaledXY(win.Bounds().Center(), pixel.V(2, 2))
tileType := levelData[x][y]
if tileType == f {
floorTile.Draw(win, mat)
} else {
wallTile.Draw(win, mat)
}
}
}
}
func cartesianToIso(pt pixel.Vec) pixel.Vec {
return pixel.V((pt.X-pt.Y)*(tileSize/2), (pt.X+pt.Y)*(tileSize/4))
}
func main() {
pixelgl.Run(run)
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 KiB

View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2017 stephen
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -0,0 +1,19 @@
# Maze generator in Go
Created by [Stephen Chavez](https://github.com/redragonx)
This uses the game engine: Pixel. Install it here: https://github.com/faiface/pixel
I made this to improve my understanding of Go and some game concepts with some basic maze generating algorithms.
Controls: Press 'R' to restart the maze.
Optional command-line arguments: `go run ./maze-generator.go`
- `-w` sets the maze's width in pixels.
- `-h` sets the maze's height in pixels.
- `-c` sets the maze cell's size in pixels.
Code based on the Recursive backtracker algorithm.
- https://en.wikipedia.org/wiki/Maze_generation_algorithm#Recursive_backtracker
![Screenshot](screenshot.png)

View File

@ -0,0 +1,317 @@
package main
// Code based on the Recursive backtracker algorithm.
// https://en.wikipedia.org/wiki/Maze_generation_algorithm#Recursive_backtracker
// See https://youtu.be/HyK_Q5rrcr4 as an example
// YouTube example ported to Go for the Pixel library.
// Created by Stephen Chavez
import (
"crypto/rand"
"errors"
"flag"
"fmt"
"math/big"
"time"
"github.com/faiface/pixel"
"github.com/faiface/pixel/examples/community/maze/stack"
"github.com/faiface/pixel/imdraw"
"github.com/faiface/pixel/pixelgl"
"github.com/pkg/profile"
"golang.org/x/image/colornames"
)
var visitedColor = pixel.RGB(0.5, 0, 1).Mul(pixel.Alpha(0.35))
var hightlightColor = pixel.RGB(0.3, 0, 0).Mul(pixel.Alpha(0.45))
var debug = false
type cell struct {
walls [4]bool // Wall order: top, right, bottom, left
row int
col int
visited bool
}
func (c *cell) Draw(imd *imdraw.IMDraw, wallSize int) {
drawCol := c.col * wallSize // x
drawRow := c.row * wallSize // y
imd.Color = colornames.White
if c.walls[0] {
// top line
imd.Push(pixel.V(float64(drawCol), float64(drawRow)), pixel.V(float64(drawCol+wallSize), float64(drawRow)))
imd.Line(3)
}
if c.walls[1] {
// right Line
imd.Push(pixel.V(float64(drawCol+wallSize), float64(drawRow)), pixel.V(float64(drawCol+wallSize), float64(drawRow+wallSize)))
imd.Line(3)
}
if c.walls[2] {
// bottom line
imd.Push(pixel.V(float64(drawCol+wallSize), float64(drawRow+wallSize)), pixel.V(float64(drawCol), float64(drawRow+wallSize)))
imd.Line(3)
}
if c.walls[3] {
// left line
imd.Push(pixel.V(float64(drawCol), float64(drawRow+wallSize)), pixel.V(float64(drawCol), float64(drawRow)))
imd.Line(3)
}
imd.EndShape = imdraw.SharpEndShape
if c.visited {
imd.Color = visitedColor
imd.Push(pixel.V(float64(drawCol), (float64(drawRow))), pixel.V(float64(drawCol+wallSize), float64(drawRow+wallSize)))
imd.Rectangle(0)
}
}
func (c *cell) GetNeighbors(grid []*cell, cols int, rows int) ([]*cell, error) {
neighbors := []*cell{}
j := c.row
i := c.col
top, _ := getCellAt(i, j-1, cols, rows, grid)
right, _ := getCellAt(i+1, j, cols, rows, grid)
bottom, _ := getCellAt(i, j+1, cols, rows, grid)
left, _ := getCellAt(i-1, j, cols, rows, grid)
if top != nil && !top.visited {
neighbors = append(neighbors, top)
}
if right != nil && !right.visited {
neighbors = append(neighbors, right)
}
if bottom != nil && !bottom.visited {
neighbors = append(neighbors, bottom)
}
if left != nil && !left.visited {
neighbors = append(neighbors, left)
}
if len(neighbors) == 0 {
return nil, errors.New("We checked all cells...")
}
return neighbors, nil
}
func (c *cell) GetRandomNeighbor(grid []*cell, cols int, rows int) (*cell, error) {
neighbors, err := c.GetNeighbors(grid, cols, rows)
if neighbors == nil {
return nil, err
}
nBig, err := rand.Int(rand.Reader, big.NewInt(int64(len(neighbors))))
if err != nil {
panic(err)
}
randomIndex := nBig.Int64()
return neighbors[randomIndex], nil
}
func (c *cell) hightlight(imd *imdraw.IMDraw, wallSize int) {
x := c.col * wallSize
y := c.row * wallSize
imd.Color = hightlightColor
imd.Push(pixel.V(float64(x), float64(y)), pixel.V(float64(x+wallSize), float64(y+wallSize)))
imd.Rectangle(0)
}
func newCell(col int, row int) *cell {
newCell := new(cell)
newCell.row = row
newCell.col = col
for i := range newCell.walls {
newCell.walls[i] = true
}
return newCell
}
// Creates the inital maze slice for use.
func initGrid(cols, rows int) []*cell {
grid := []*cell{}
for j := 0; j < rows; j++ {
for i := 0; i < cols; i++ {
newCell := newCell(i, j)
grid = append(grid, newCell)
}
}
return grid
}
func setupMaze(cols, rows int) ([]*cell, *stack.Stack, *cell) {
// Make an empty grid
grid := initGrid(cols, rows)
backTrackStack := stack.NewStack(len(grid))
currentCell := grid[0]
return grid, backTrackStack, currentCell
}
func cellIndex(i, j, cols, rows int) int {
if i < 0 || j < 0 || i > cols-1 || j > rows-1 {
return -1
}
return i + j*cols
}
func getCellAt(i int, j int, cols int, rows int, grid []*cell) (*cell, error) {
possibleIndex := cellIndex(i, j, cols, rows)
if possibleIndex == -1 {
return nil, fmt.Errorf("cellIndex: CellIndex is a negative number %d", possibleIndex)
}
return grid[possibleIndex], nil
}
func removeWalls(a *cell, b *cell) {
x := a.col - b.col
if x == 1 {
a.walls[3] = false
b.walls[1] = false
} else if x == -1 {
a.walls[1] = false
b.walls[3] = false
}
y := a.row - b.row
if y == 1 {
a.walls[0] = false
b.walls[2] = false
} else if y == -1 {
a.walls[2] = false
b.walls[0] = false
}
}
func run() {
// unsiged integers, because easier parsing error checks.
// We must convert these to intergers, as done below...
uScreenWidth, uScreenHeight, uWallSize := parseArgs()
var (
// In pixels
// Defualt is 800x800x40 = 20x20 wallgrid
screenWidth = int(uScreenWidth)
screenHeight = int(uScreenHeight)
wallSize = int(uWallSize)
frames = 0
second = time.Tick(time.Second)
grid = []*cell{}
cols = screenWidth / wallSize
rows = screenHeight / wallSize
currentCell = new(cell)
backTrackStack = stack.NewStack(1)
)
// Set game FPS manually
fps := time.Tick(time.Second / 60)
cfg := pixelgl.WindowConfig{
Title: "Pixel Rocks! - Maze example",
Bounds: pixel.R(0, 0, float64(screenHeight), float64(screenWidth)),
}
win, err := pixelgl.NewWindow(cfg)
if err != nil {
panic(err)
}
grid, backTrackStack, currentCell = setupMaze(cols, rows)
gridIMDraw := imdraw.New(nil)
for !win.Closed() {
if win.JustReleased(pixelgl.KeyR) {
fmt.Println("R pressed")
grid, backTrackStack, currentCell = setupMaze(cols, rows)
}
win.Clear(colornames.Gray)
gridIMDraw.Clear()
for i := range grid {
grid[i].Draw(gridIMDraw, wallSize)
}
// step 1
// Make the initial cell the current cell and mark it as visited
currentCell.visited = true
currentCell.hightlight(gridIMDraw, wallSize)
// step 2.1
// If the current cell has any neighbours which have not been visited
// Choose a random unvisited cell
nextCell, _ := currentCell.GetRandomNeighbor(grid, cols, rows)
if nextCell != nil && !nextCell.visited {
// step 2.2
// Push the current cell to the stack
backTrackStack.Push(currentCell)
// step 2.3
// Remove the wall between the current cell and the chosen cell
removeWalls(currentCell, nextCell)
// step 2.4
// Make the chosen cell the current cell and mark it as visited
nextCell.visited = true
currentCell = nextCell
} else if backTrackStack.Len() > 0 {
currentCell = backTrackStack.Pop().(*cell)
}
gridIMDraw.Draw(win)
win.Update()
<-fps
updateFPSDisplay(win, &cfg, &frames, grid, second)
}
}
// Parses the maze arguments, all of them are optional.
// Uses uint as implicit error checking :)
func parseArgs() (uint, uint, uint) {
var mazeWidthPtr = flag.Uint("w", 800, "w sets the maze's width in pixels.")
var mazeHeightPtr = flag.Uint("h", 800, "h sets the maze's height in pixels.")
var wallSizePtr = flag.Uint("c", 40, "c sets the maze cell's size in pixels.")
flag.Parse()
// If these aren't default values AND if they're not the same values.
// We should warn the user that the maze will look funny.
if *mazeWidthPtr != 800 || *mazeHeightPtr != 800 {
if *mazeWidthPtr != *mazeHeightPtr {
fmt.Printf("WARNING: maze width: %d and maze height: %d don't match. \n", *mazeWidthPtr, *mazeHeightPtr)
fmt.Println("Maze will look funny because the maze size is bond to the window size!")
}
}
return *mazeWidthPtr, *mazeHeightPtr, *wallSizePtr
}
func updateFPSDisplay(win *pixelgl.Window, cfg *pixelgl.WindowConfig, frames *int, grid []*cell, second <-chan time.Time) {
*frames++
select {
case <-second:
win.SetTitle(fmt.Sprintf("%s | FPS: %d with %d Cells", cfg.Title, *frames, len(grid)))
*frames = 0
default:
}
}
func main() {
if debug {
defer profile.Start().Stop()
}
pixelgl.Run(run)
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

@ -0,0 +1,86 @@
package stack
type Stack struct {
top *Element
size int
max int
}
type Element struct {
value interface{}
next *Element
}
func NewStack(max int) *Stack {
return &Stack{max: max}
}
// Return the stack's length
func (s *Stack) Len() int {
return s.size
}
// Return the stack's max
func (s *Stack) Max() int {
return s.max
}
// Push a new element onto the stack
func (s *Stack) Push(value interface{}) {
if s.size+1 > s.max {
if last := s.PopLast(); last == nil {
panic("Unexpected nil in stack")
}
}
s.top = &Element{value, s.top}
s.size++
}
// Remove the top element from the stack and return it's value
// If the stack is empty, return nil
func (s *Stack) Pop() (value interface{}) {
if s.size > 0 {
value, s.top = s.top.value, s.top.next
s.size--
return
}
return nil
}
func (s *Stack) PopLast() (value interface{}) {
if lastElem := s.popLast(s.top); lastElem != nil {
return lastElem.value
}
return nil
}
//Peek returns a top without removing it from list
func (s *Stack) Peek() (value interface{}, exists bool) {
exists = false
if s.size > 0 {
value = s.top.value
exists = true
}
return
}
func (s *Stack) popLast(elem *Element) *Element {
if elem == nil {
return nil
}
// not last because it has next and a grandchild
if elem.next != nil && elem.next.next != nil {
return s.popLast(elem.next)
}
// current elem is second from bottom, as next elem has no child
if elem.next != nil && elem.next.next == nil {
last := elem.next
// make current elem bottom of stack by removing its next element
elem.next = nil
s.size--
return last
}
return nil
}

View File

@ -0,0 +1,9 @@
# Parallax scrolling demo
Created by [Sergio Vera](https://github.com/svera)
This example shows how to implement an infinite side scrolling background with a depth effect, using [parallax scrolling](https://en.wikipedia.org/wiki/Parallax_scrolling). Code is based in the [infinite scrolling background](https://github.com/faiface/pixel/tree/master/examples/community/scrolling-background) demo.
Credits to [Peter Hellberg](https://github.com/peterhellberg) for the improved background images.
![Parallax scrolling background](result.png)

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

View File

@ -0,0 +1,74 @@
package main
import (
"image"
"os"
"time"
_ "image/png"
"github.com/faiface/pixel"
"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
}
const (
windowWidth = 600
windowHeight = 450
foregroundHeight = 149
// This is the scrolling speed (pixels per second)
// Negative values will make background to scroll to the left,
// positive to the right.
backgroundSpeed = -60
foregroundSpeed = -120
)
func run() {
cfg := pixelgl.WindowConfig{
Title: "Parallax scrolling demo",
Bounds: pixel.R(0, 0, windowWidth, windowHeight),
VSync: true,
}
win, err := pixelgl.NewWindow(cfg)
if err != nil {
panic(err)
}
// Pic must have double the width of the window, as it will scroll to the left or right
picBackground, err := loadPicture("background.png")
if err != nil {
panic(err)
}
picForeground, err := loadPicture("foreground.png")
if err != nil {
panic(err)
}
background := NewScrollingBackground(picBackground, windowWidth, windowHeight, backgroundSpeed)
foreground := NewScrollingBackground(picForeground, windowWidth, foregroundHeight, foregroundSpeed)
last := time.Now()
for !win.Closed() {
dt := time.Since(last).Seconds()
last = time.Now()
background.Update(win, dt)
foreground.Update(win, dt)
win.Update()
}
}
func main() {
pixelgl.Run(run)
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

View File

@ -0,0 +1,64 @@
package main
import (
"math"
"github.com/faiface/pixel"
"github.com/faiface/pixel/pixelgl"
)
// ScrollingBackground stores all needed information to scroll a background
// to the left or right
type ScrollingBackground struct {
width float64
height float64
displacement float64
speed float64
backgrounds [2]*pixel.Sprite
positions [2]pixel.Vec
}
// NewScrollingBackground construct and returns a new instance of scrollingBackground,
// positioning the background images according to the speed value
func NewScrollingBackground(pic pixel.Picture, width, height, speed float64) *ScrollingBackground {
sb := &ScrollingBackground{
width: width,
height: height,
speed: speed,
backgrounds: [2]*pixel.Sprite{
pixel.NewSprite(pic, pixel.R(0, 0, width, height)),
pixel.NewSprite(pic, pixel.R(width, 0, width*2, height)),
},
}
sb.positionImages()
return sb
}
// If scrolling speed > 0, put second background image ouside the screen,
// at the left side, otherwise put it at the right side.
func (sb *ScrollingBackground) positionImages() {
if sb.speed > 0 {
sb.positions = [2]pixel.Vec{
pixel.V(sb.width/2, sb.height/2),
pixel.V((sb.width/2)-sb.width, sb.height/2),
}
} else {
sb.positions = [2]pixel.Vec{
pixel.V(sb.width/2, sb.height/2),
pixel.V(sb.width+(sb.width/2), sb.height/2),
}
}
}
// Update will move backgrounds certain pixels, depending of the amount of time passed
func (sb *ScrollingBackground) Update(win *pixelgl.Window, dt float64) {
if math.Abs(sb.displacement) >= sb.width {
sb.displacement = 0
sb.positions[0], sb.positions[1] = sb.positions[1], sb.positions[0]
}
d := pixel.V(sb.displacement, 0)
sb.backgrounds[0].Draw(win, pixel.IM.Moved(sb.positions[0].Add(d)))
sb.backgrounds[1].Draw(win, pixel.IM.Moved(sb.positions[1].Add(d)))
sb.displacement += sb.speed * dt
}

View File

@ -0,0 +1,12 @@
# Procedural 1D terrain generator
Created by [Sergio Vera](https://github.com/svera).
This is a demo of a 1D terrain generator using [Perlin noise](https://en.wikipedia.org/wiki/Perlin_noise) algorithm.
Press *space* to generate a random terrain.
Uses [Go-Perlin](https://github.com/aquilax/go-perlin).
Texture by [hh316](https://hhh316.deviantart.com/art/Seamless-stone-cliff-face-mountain-texture-377076626).
![Randomly generated 1D terrain](result.png)

View File

@ -0,0 +1,104 @@
package main
import (
"image"
"math/rand"
"os"
"time"
_ "image/jpeg"
perlin "github.com/aquilax/go-perlin"
"github.com/faiface/pixel"
"github.com/faiface/pixel/imdraw"
"github.com/faiface/pixel/pixelgl"
"golang.org/x/image/colornames"
)
const (
width = 800
height = 600
// Top of the mountain must be around the half of the screen height
verticalOffset = height / 2
// Perlin noise provides variations in values between -1 and 1,
// we multiply those so they're visible on screen
scale = 100
waveLength = 100
alpha = 2.
beta = 2.
n = 3
maximumSeedValue = 100
)
func init() {
rand.Seed(time.Now().UTC().UnixNano())
}
func main() {
pixelgl.Run(run)
}
func run() {
cfg := pixelgl.WindowConfig{
Title: "Procedural terrain 1D",
Bounds: pixel.R(0, 0, width, height),
VSync: true,
}
win, err := pixelgl.NewWindow(cfg)
if err != nil {
panic(err)
}
pic, err := loadPicture("stone.jpg")
if err != nil {
panic(err)
}
imd := imdraw.New(pic)
drawTerrain(win, imd)
for !win.Closed() {
if win.JustPressed(pixelgl.KeySpace) {
drawTerrain(win, imd)
}
win.Update()
}
}
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 drawTerrain(win *pixelgl.Window, imd *imdraw.IMDraw) {
var seed = rand.Int63n(maximumSeedValue)
p := perlin.NewPerlin(alpha, beta, n, seed)
imd.Clear()
win.Clear(colornames.Skyblue)
for x := 0.; x < width; x++ {
y := p.Noise1D(x/waveLength)*scale + verticalOffset
renderTexturedLine(x, y, imd)
}
imd.Draw(win)
}
// Render a textured line in position x with a height y.
// Note that the textured line is just a 1 px width rectangle.
// We push the opposite vertices of that rectangle and specify the points of the
// texture we want to apply to them. Pixel will fill the rest of the rectangle interpolating the texture.
func renderTexturedLine(x, y float64, imd *imdraw.IMDraw) {
imd.Intensity = 1.
imd.Picture = pixel.V(x, 0)
imd.Push(pixel.V(x, 0))
imd.Picture = pixel.V(x+1, y)
imd.Push(pixel.V(x+1, y))
imd.Rectangle(0)
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 759 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 368 KiB

View File

@ -0,0 +1,22 @@
# raycaster
A raycaster made by [Peter Hellberg](https://github.com/peterhellberg/) as part of his [pixel-experiments](https://github.com/peterhellberg/pixel-experiments).
Based on Lodes article on [raycasting](http://lodev.org/cgtutor/raycasting.html).
## Controls
WASD for strafing and arrow keys for rotation.
Place blocks using the number keys.
## Screenshots
![raycaster animation](https://user-images.githubusercontent.com/565124/31828029-798e6620-b5b9-11e7-96b7-fda540755745.gif)
![raycaster screenshot](screenshot.png)
## Links
- https://github.com/peterhellberg/pixel-experiments/tree/master/raycaster
- https://gist.github.com/peterhellberg/835eccabf95800555120cc8f0c9e16c2

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

View File

@ -0,0 +1,7 @@
# Infinite scrolling background demo
Created by [Sergio Vera](https://github.com/svera)
This example shows how to implement an infinite side scrolling background.
Credits to [Peter Hellberg](https://github.com/peterhellberg) for the improved background image.

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

View File

@ -0,0 +1,83 @@
package main
import (
"image"
"os"
"time"
_ "image/png"
"github.com/faiface/pixel"
"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
}
const (
windowWidth = 600
windowHeight = 450
// This is the scrolling speed
linesPerSecond = 60
)
func run() {
cfg := pixelgl.WindowConfig{
Title: "Scrolling background demo",
Bounds: pixel.R(0, 0, windowWidth, windowHeight),
VSync: true,
}
win, err := pixelgl.NewWindow(cfg)
if err != nil {
panic(err)
}
// Pic must have double the width of the window, as it will scroll to the left
pic, err := loadPicture("gamebackground.png")
if err != nil {
panic(err)
}
// Backgrounds are made taking the left and right halves of the image
background1 := pixel.NewSprite(pic, pixel.R(0, 0, windowWidth, windowHeight))
background2 := pixel.NewSprite(pic, pixel.R(windowWidth, 0, windowWidth*2, windowHeight))
// In the beginning, vector1 will put background1 filling the whole window, while vector2 will
// put background2 just at the right side of the window, out of view
vector1 := pixel.V(windowWidth/2, windowHeight/2)
vector2 := pixel.V(windowWidth+(windowWidth/2), windowHeight/2)
i := float64(0)
last := time.Now()
for !win.Closed() {
dt := time.Since(last).Seconds()
last = time.Now()
// When one of the backgrounds has completely scrolled, we swap displacement vectors,
// so the backgrounds will swap positions too regarding the previous iteration,
// thus making the background endless.
if i <= -windowWidth {
i = 0
vector1, vector2 = vector2, vector1
}
// This delta vector will move the backgrounds to the left
d := pixel.V(-i, 0)
background1.Draw(win, pixel.IM.Moved(vector1.Sub(d)))
background2.Draw(win, pixel.IM.Moved(vector2.Sub(d)))
i -= linesPerSecond * dt
win.Update()
}
}
func main() {
pixelgl.Run(run)
}

View File

@ -0,0 +1,20 @@
# starfield
Classic starfield… with [supposedly accurate stellar colors](http://www.vendian.org/mncharity/dir3/starcolor/)
Made by [Peter Hellberg](https://github.com/peterhellberg/) as part of his [pixel-experiments](https://github.com/peterhellberg/pixel-experiments)
## Controls
Arrow up and down to change speed. Space bar to almost stop.
## Screenshots
![starfield animation](https://user-images.githubusercontent.com/565124/32411599-a5fcba72-c1df-11e7-8730-a570470a4eee.gif)
![starfield screenshot](screenshot.png)
## Links
- https://github.com/peterhellberg/pixel-experiments/tree/master/starfield
- https://gist.github.com/peterhellberg/4018e228cced61a0bb26991e49299c96

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

View File

@ -0,0 +1,165 @@
package main
import (
"image/color"
"math/rand"
"time"
"github.com/faiface/pixel"
"github.com/faiface/pixel/imdraw"
"github.com/faiface/pixel/pixelgl"
)
const w, h = float64(1024), float64(512)
var speed = float64(200)
var stars [1024]*star
func init() {
rand.Seed(4)
for i := 0; i < len(stars); i++ {
stars[i] = newStar()
}
}
type star struct {
pixel.Vec
Z float64
P float64
C color.RGBA
}
func newStar() *star {
return &star{
pixel.V(random(-w, w), random(-h, h)),
random(0, w), 0, Colors[rand.Intn(len(Colors))],
}
}
func (s *star) update(d float64) {
s.P = s.Z
s.Z -= d * speed
if s.Z < 0 {
s.X = random(-w, w)
s.Y = random(-h, h)
s.Z = w
s.P = s.Z
}
}
func (s *star) draw(imd *imdraw.IMDraw) {
p := pixel.V(
scale(s.X/s.Z, 0, 1, 0, w),
scale(s.Y/s.Z, 0, 1, 0, h),
)
o := pixel.V(
scale(s.X/s.P, 0, 1, 0, w),
scale(s.Y/s.P, 0, 1, 0, h),
)
r := scale(s.Z, 0, w, 11, 0)
imd.Color = s.C
if p.Sub(o).Len() > 6 {
imd.Push(p, o)
imd.Line(r)
}
imd.Push(p)
imd.Circle(r, 0)
}
func run() {
win, err := pixelgl.NewWindow(pixelgl.WindowConfig{
Bounds: pixel.R(0, 0, w, h),
VSync: true,
Undecorated: true,
})
if err != nil {
panic(err)
}
imd := imdraw.New(nil)
imd.Precision = 7
imd.SetMatrix(pixel.IM.Moved(win.Bounds().Center()))
last := time.Now()
for !win.Closed() {
win.SetClosed(win.JustPressed(pixelgl.KeyEscape) || win.JustPressed(pixelgl.KeyQ))
if win.Pressed(pixelgl.KeyUp) {
speed += 10
}
if win.Pressed(pixelgl.KeyDown) {
if speed > 10 {
speed -= 10
}
}
if win.Pressed(pixelgl.KeySpace) {
speed = 100
}
d := time.Since(last).Seconds()
last = time.Now()
imd.Clear()
for _, s := range stars {
s.update(d)
s.draw(imd)
}
win.Clear(color.Black)
imd.Draw(win)
win.Update()
}
}
func main() {
pixelgl.Run(run)
}
func random(min, max float64) float64 {
return rand.Float64()*(max-min) + min
}
func scale(unscaledNum, min, max, minAllowed, maxAllowed float64) float64 {
return (maxAllowed-minAllowed)*(unscaledNum-min)/(max-min) + minAllowed
}
// Colors based on stellar types listed at
// http://www.vendian.org/mncharity/dir3/starcolor/
var Colors = []color.RGBA{
color.RGBA{157, 180, 255, 255},
color.RGBA{162, 185, 255, 255},
color.RGBA{167, 188, 255, 255},
color.RGBA{170, 191, 255, 255},
color.RGBA{175, 195, 255, 255},
color.RGBA{186, 204, 255, 255},
color.RGBA{192, 209, 255, 255},
color.RGBA{202, 216, 255, 255},
color.RGBA{228, 232, 255, 255},
color.RGBA{237, 238, 255, 255},
color.RGBA{251, 248, 255, 255},
color.RGBA{255, 249, 249, 255},
color.RGBA{255, 245, 236, 255},
color.RGBA{255, 244, 232, 255},
color.RGBA{255, 241, 223, 255},
color.RGBA{255, 235, 209, 255},
color.RGBA{255, 215, 174, 255},
color.RGBA{255, 198, 144, 255},
color.RGBA{255, 190, 127, 255},
color.RGBA{255, 187, 123, 255},
color.RGBA{255, 187, 123, 255},
}

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

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

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

View File

@ -0,0 +1,78 @@
package main
import (
"io/ioutil"
"os"
"time"
"github.com/faiface/pixel"
"github.com/faiface/pixel/pixelgl"
"github.com/faiface/pixel/text"
"github.com/golang/freetype/truetype"
"golang.org/x/image/colornames"
"golang.org/x/image/font"
)
func loadTTF(path string, size float64) (font.Face, error) {
file, err := os.Open(path)
if err != nil {
return nil, err
}
defer file.Close()
bytes, err := ioutil.ReadAll(file)
if err != nil {
return nil, err
}
font, err := truetype.Parse(bytes)
if err != nil {
return nil, err
}
return truetype.NewFace(font, &truetype.Options{
Size: size,
GlyphCacheEntries: 1,
}), nil
}
func run() {
cfg := pixelgl.WindowConfig{
Title: "Pixel Rocks!",
Bounds: pixel.R(0, 0, 1024, 768),
}
win, err := pixelgl.NewWindow(cfg)
if err != nil {
panic(err)
}
win.SetSmooth(true)
face, err := loadTTF("intuitive.ttf", 80)
if err != nil {
panic(err)
}
atlas := text.NewAtlas(face, text.ASCII)
txt := text.New(pixel.V(50, 500), atlas)
txt.Color = colornames.Lightgrey
fps := time.Tick(time.Second / 120)
for !win.Closed() {
txt.WriteString(win.Typed())
if win.JustPressed(pixelgl.KeyEnter) || win.Repeated(pixelgl.KeyEnter) {
txt.WriteRune('\n')
}
win.Clear(colornames.Darkcyan)
txt.Draw(win, pixel.IM.Moved(win.Bounds().Center().Sub(txt.Bounds().Center())))
win.Update()
<-fps
}
}
func main() {
pixelgl.Run(run)
}

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)

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

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

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)

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

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

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

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

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 381 KiB

View File

@ -0,0 +1,13 @@
# Typewriter
This example demonstrates text drawing and text input facilities by implementing a fancy typewriter
with a red laser cursor. Screen shakes a bit when typing and some letters turn bold or italic
randomly.
ASCII and Latin characters are supported here. Feel free to add support for more characters in the
code (it's easy, but increases load time).
The seemingly buggy letters (one over another) in the screenshot are not bugs, but a result of using
the typewriter backspace functionality.
![Screenshot](screenshot.png)

317
examples/typewriter/main.go Normal file
View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 156 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().Add(pixel.V(-offset, 0)))
imd.Circle(200, 0)
imd.Draw(canvas)
// blue circle
imd.Clear()
imd.Color = pixel.RGB(0, 0, 1)
imd.Push(win.Bounds().Center().Add(pixel.V(offset, 0)))
imd.Circle(150, 0)
imd.Draw(canvas)
// yellow circle
imd.Clear()
imd.Color = pixel.RGB(1, 1, 0)
imd.Push(win.Bounds().Center().Add(pixel.V(0, -offset)))
imd.Circle(100, 0)
imd.Draw(canvas)
// magenta circle
imd.Clear()
imd.Color = pixel.RGB(1, 0, 1)
imd.Push(win.Bounds().Center().Add(pixel.V(0, offset)))
imd.Circle(50, 0)
imd.Draw(canvas)
win.Clear(colornames.Green)
canvas.Draw(win, pixel.IM.Moved(win.Bounds().Center()))
win.Update()
}
}
func main() {
pixelgl.Run(run)
}

BIN
examples/xor/screenshot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View File

@ -3,50 +3,55 @@ 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.
// 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
}
// Vec is a 2D vector type with X and Y coordinates.
//
// Create vectors with the V constructor:
//
// u := pixel.V(1, 2)
// v := pixel.V(8, -3)
//
// Add and subtract them using the standard + and - operators:
// Use various methods to manipulate them:
//
// w := u + v
// fmt.Println(w) // Vec(9, -1)
// fmt.Println(u - v) // Vec(-7, 5)
//
// Additional standard vector operations can be obtained with methods:
//
// u := pixel.V(2, 3)
// v := pixel.V(8, 1)
// if u.X() < 0 {
// w := u.Add(v)
// fmt.Println(w) // Vec(9, -1)
// fmt.Println(u.Sub(v)) // Vec(-7, 5)
// u = pixel.V(2, 3)
// v = pixel.V(8, 1)
// if u.X < 0 {
// fmt.Println("this won't happen")
// }
// x := u.Unit().Dot(v.Unit())
type Vec complex128
type Vec struct {
X, Y float64
}
// ZV is a zero vector.
var ZV = Vec{0, 0}
// V returns a new 2D vector with the given coordinates.
func V(x, y float64) Vec {
return Vec(complex(x, y))
return Vec{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)
// 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.
@ -55,76 +60,96 @@ func Y(y float64) Vec {
// u.String() // returns "Vec(4.5, -1.3)"
// fmt.Println(u) // Vec(4.5, -1.3)
func (u Vec) String() string {
return fmt.Sprintf("Vec(%v, %v)", u.X(), u.Y())
}
// X returns the x coordinate of the vector u.
func (u Vec) X() float64 {
return real(u)
}
// Y returns the y coordinate of the vector u.
func (u Vec) Y() float64 {
return imag(u)
return fmt.Sprintf("Vec(%v, %v)", u.X, u.Y)
}
// XY returns the components of the vector in two return values.
func (u Vec) XY() (x, y float64) {
return real(u), imag(u)
return u.X, u.Y
}
// Len returns the length of the vector u.
func (u Vec) Len() float64 {
return cmplx.Abs(complex128(u))
}
// Angle returns the angle between the vector u and the x-axis. The result is in the range [-Pi, Pi].
func (u Vec) Angle() float64 {
return cmplx.Phase(complex128(u))
}
// Unit returns a vector of length 1 facing the direction of u (has the same angle).
func (u Vec) Unit() Vec {
if u == 0 {
return 1
// Add returns the sum of vectors u and v.
func (u Vec) Add(v Vec) Vec {
return Vec{
u.X + v.X,
u.Y + v.Y,
}
}
// Sub returns the difference betweeen vectors u and v.
func (u Vec) Sub(v Vec) Vec {
return Vec{
u.X - v.X,
u.Y - v.Y,
}
}
// 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,
}
return u / V(u.Len(), 0)
}
// Scaled returns the vector u multiplied by c.
func (u Vec) Scaled(c float64) Vec {
return u * V(c, 0)
return Vec{u.X * c, u.Y * c}
}
// ScaledXY returns the vector u multiplied by the vector v component-wise.
func (u Vec) ScaledXY(v Vec) Vec {
return V(u.X()*v.X(), u.Y()*v.Y())
return Vec{u.X * v.X, u.Y * v.Y}
}
// Len returns the length of the vector u.
func (u Vec) Len() float64 {
return math.Hypot(u.X, u.Y)
}
// Angle returns the angle between the vector u and the x-axis. The result is in range [-Pi, Pi].
func (u Vec) Angle() float64 {
return math.Atan2(u.Y, u.X)
}
// Unit returns a vector of length 1 facing the direction of u (has the same angle).
func (u Vec) Unit() Vec {
if u.X == 0 && u.Y == 0 {
return Vec{1, 0}
}
return u.Scaled(1 / u.Len())
}
// Rotated returns the vector u rotated by the given angle in radians.
func (u Vec) Rotated(angle float64) Vec {
sin, cos := math.Sincos(angle)
return u * V(cos, sin)
return Vec{
u.X*cos - u.Y*sin,
u.X*sin + u.Y*cos,
}
}
// 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)
// 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()
return u.X*v.X + u.Y*v.Y
}
// Cross return the cross product of vectors u and v.
func (u Vec) Cross(v Vec) float64 {
return u.X()*v.Y() - v.X()*u.Y()
return u.X*v.Y - v.X*u.Y
}
// 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
@ -133,10 +158,10 @@ func (u Vec) Cross(v Vec) float64 {
// u := pixel.V(10.5, -1.5)
// v := u.Map(math.Floor) // v is Vec(10, -2), both components of u floored
func (u Vec) Map(f func(float64) float64) Vec {
return V(
f(u.X()),
f(u.Y()),
)
return Vec{
f(u.X),
f(u.Y),
}
}
// Lerp returns a linear interpolation between vectors a and b.
@ -145,7 +170,7 @@ func (u Vec) Map(f func(float64) float64) Vec {
// If t is 0, then a will be returned, if t is 1, b will be returned. Anything between 0 and 1 will
// return the appropriate point between a and b and so on.
func Lerp(a, b Vec, t float64) Vec {
return a.Scaled(1-t) + b.Scaled(t)
return a.Scaled(1 - t).Add(b.Scaled(t))
}
// Rect is a 2D rectangle aligned with the axes of the coordinate system. It is defined by two
@ -158,24 +183,12 @@ type Rect struct {
}
// R returns a new Rect with given the Min and Max coordinates.
//
// Note that the returned rectangle is not automatically normalized.
func R(minX, minY, maxX, maxY float64) Rect {
return Rect{
Min: V(minX, minY),
Max: V(maxX, maxY),
}
}
// 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()),
),
Min: Vec{minX, minY},
Max: Vec{maxX, maxY},
}
}
@ -185,17 +198,31 @@ func (r Rect) Norm() Rect {
// r.String() // returns "Rect(100, 50, 200, 300)"
// fmt.Println(r) // Rect(100, 50, 200, 300)
func (r Rect) String() string {
return fmt.Sprintf("Rect(%v, %v, %v, %v)", r.Min.X(), r.Min.Y(), r.Max.X(), r.Max.Y())
return fmt.Sprintf("Rect(%v, %v, %v, %v)", r.Min.X, r.Min.Y, r.Max.X, r.Max.Y)
}
// Norm returns the Rect in normal form, such that Max is component-wise greater or equal than Min.
func (r Rect) Norm() Rect {
return Rect{
Min: Vec{
math.Min(r.Min.X, r.Max.X),
math.Min(r.Min.Y, r.Max.Y),
},
Max: Vec{
math.Max(r.Min.X, r.Max.X),
math.Max(r.Min.Y, r.Max.Y),
},
}
}
// W returns the width of the Rect.
func (r Rect) W() float64 {
return r.Max.X() - r.Min.X()
return r.Max.X - r.Min.X
}
// H returns the height of the Rect.
func (r Rect) H() float64 {
return r.Max.Y() - r.Min.Y()
return r.Max.Y - r.Min.Y
}
// Size returns the vector of width and height of the Rect.
@ -203,36 +230,21 @@ 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()
}
// Center returns the position of the center of the Rect.
func (r Rect) Center() Vec {
return (r.Min + r.Max) / 2
return Lerp(r.Min, r.Max, 0.5)
}
// Moved returns the Rect moved (both Min and Max) by the given vector delta.
func (r Rect) Moved(delta Vec) Rect {
return Rect{
Min: r.Min + delta,
Max: r.Max + delta,
}
}
// WithMin returns the Rect with it's Min changed to the given position.
//
// Note, that the Rect is not automatically normalized.
func (r Rect) WithMin(min Vec) Rect {
return Rect{
Min: min,
Max: r.Max,
}
}
// WithMax returns the Rect with it's Max changed to the given position.
//
// Note, that the Rect is not automatically normalized.
func (r Rect) WithMax(max Vec) Rect {
return Rect{
Min: r.Min,
Max: max,
Min: r.Min.Add(delta),
Max: r.Max.Add(delta),
}
}
@ -243,16 +255,16 @@ func (r Rect) WithMax(max Vec) Rect {
// r.Resized(r.Max, size) // same with the top-right corner
// r.Resized(r.Center(), size) // resizes around the center
//
// This function does not make sense for sizes of zero area and will panic. Use ResizedMin in the
// case of zero area.
// This function does not make sense for resizing a rectangle of zero area and will panic. Use
// ResizedMin in the case of zero area.
func (r Rect) Resized(anchor, size Vec) Rect {
if r.W()*r.H() == 0 || size.X()*size.Y() == 0 {
if r.W()*r.H() == 0 {
panic(fmt.Errorf("(%T).Resize: zero area", r))
}
fraction := size.ScaledXY(V(1/r.W(), 1/r.H()))
fraction := Vec{size.X / r.W(), size.Y / r.H()}
return Rect{
Min: anchor + (r.Min - anchor).ScaledXY(fraction),
Max: anchor + (r.Max - anchor).ScaledXY(fraction),
Min: anchor.Add(r.Min.Sub(anchor).ScaledXY(fraction)),
Max: anchor.Add(r.Max.Sub(anchor).ScaledXY(fraction)),
}
}
@ -263,44 +275,86 @@ func (r Rect) Resized(anchor, size Vec) Rect {
func (r Rect) ResizedMin(size Vec) Rect {
return Rect{
Min: r.Min,
Max: r.Min + size,
Max: r.Min.Add(size),
}
}
// Contains checks whether a vector u is contained within this Rect (including it's borders).
func (r Rect) Contains(u Vec) bool {
return r.Min.X() <= u.X() && u.X() <= r.Max.X() && r.Min.Y() <= u.Y() && u.Y() <= r.Max.Y()
return r.Min.X <= u.X && u.X <= r.Max.X && r.Min.Y <= u.Y && u.Y <= r.Max.Y
}
// Matrix is a 3x3 transformation matrix that can be used for all kinds of spacial transforms, such
// 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 R(0, 0, 0, 0).
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 Rect{}
}
return t
}
// Matrix is a 3x2 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(0, math.Pi/2)
// pixel.IM.Moved(pixel.V(100, 200)).Rotated(pixel.ZV, math.Pi/2)
//
// This code creates a Matrix that first moves everything by 100 units horizontally and 200 units
// vertically and then rotates everything by 90 degrees around the origin.
type Matrix [9]float64
//
// 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(mgl64.Ident3())
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 {
m3 := mgl64.Mat3(m)
m3 = mgl64.Translate2D(delta.XY()).Mul3(m3)
return Matrix(m3)
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 {
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)
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.
@ -310,36 +364,42 @@ func (m Matrix) Scaled(around Vec, scale float64) Matrix {
// Rotated rotates everything around a given point by the given angle in radians.
func (m Matrix) Rotated(around Vec, angle float64) Matrix {
m3 := mgl64.Mat3(m)
m3 = mgl64.Translate2D((-around).XY()).Mul3(m3)
m3 = mgl64.Rotate3DZ(angle).Mul3(m3)
m3 = mgl64.Translate2D(around.XY()).Mul3(m3)
return Matrix(m3)
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 {
m3 := mgl64.Mat3(m)
m3 = mgl64.Mat3(next).Mul3(m3)
return Matrix(m3)
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 {
m3 := mgl64.Mat3(m)
proj := m3.Mul3x1(mgl64.Vec3{u.X(), u.Y(), 1})
return V(proj.X(), proj.Y())
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.
//
// It turns out that multiplying a vector by the inverse matrix of m can be nearly-accomplished by
// subtracting the translate part of the matrix and multplying by the inverse of the top-left 2x2
// matrix, and the inverse of a 2x2 matrix is simple enough to just be inlined in the computation.
//
// 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())
d := (m[0] * m[3]) - (m[1] * m[2])
u.X, u.Y = (u.X-m[4])/d, (u.Y-m[5])/d
return Vec{u.X*m[3] - u.Y*m[1], u.Y*m[0] - u.X*m[2]}
}

79
geometry_test.go Normal file
View File

@ -0,0 +1,79 @@
package pixel_test
import (
"fmt"
"testing"
"github.com/faiface/pixel"
)
type rectTestTransform struct {
name string
f func(pixel.Rect) pixel.Rect
}
func TestResizeRect(t *testing.T) {
// 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)
}
})
}
}

View File

@ -1,3 +1,5 @@
// Package imdraw implements a basic primitive geometry shape and pictured polygon drawing for Pixel
// with a nice immediate-mode-like API.
package imdraw
import (
@ -7,7 +9,7 @@ import (
"github.com/faiface/pixel"
)
// IMDraw is an immediate-like-mode shape drawer and BasicTarget. IMDraw supports TrianglesPosition,
// IMDraw is an immediate-mode-like shape drawer and BasicTarget. IMDraw supports TrianglesPosition,
// TrianglesColor, TrianglesPicture and PictureColor.
//
// IMDraw, other than a regular BasicTarget, is used to draw shapes. To draw shapes, you first need
@ -21,9 +23,9 @@ import (
//
// imd.Line(20) // draws a 20 units thick line
//
// Use various methods to change properties of Pushed points:
// Set exported fields to change properties of Pushed points:
//
// imd.Color(pixel.NRGBA{R: 1, G: 0, B: 0, A: 1})
// imd.Color = pixel.RGB(1, 0, 0)
// imd.Push(pixel.V(200, 200))
// imd.Circle(400, 0)
//
@ -43,10 +45,16 @@ import (
// - Ellipse
// - Ellipse arc
type IMDraw struct {
Color color.Color
Picture pixel.Vec
Intensity float64
Precision int
EndShape EndShape
points []point
opts point
pool [][]point
matrix pixel.Matrix
mask pixel.NRGBA
mask pixel.RGBA
tri *pixel.TrianglesData
batch *pixel.Batch
@ -56,7 +64,7 @@ var _ pixel.BasicTarget = (*IMDraw)(nil)
type point struct {
pos pixel.Vec
col pixel.NRGBA
col pixel.RGBA
pic pixel.Vec
in float64
precision int
@ -87,7 +95,7 @@ func New(pic pixel.Picture) *IMDraw {
batch: pixel.NewBatch(tri, pic),
}
im.SetMatrix(pixel.IM)
im.SetColorMask(pixel.NRGBA{R: 1, G: 1, B: 1, A: 1})
im.SetColorMask(pixel.Alpha(1))
im.Reset()
return im
}
@ -102,12 +110,17 @@ func (imd *IMDraw) Clear() {
//
// This does not affect matrix and color mask set by SetMatrix and SetColorMask.
func (imd *IMDraw) Reset() {
imd.points = nil
imd.opts = point{}
imd.Precision(64)
imd.points = imd.points[:0]
imd.Color = pixel.Alpha(1)
imd.Picture = pixel.ZV
imd.Intensity = 0
imd.Precision = 64
imd.EndShape = NoEndShape
}
// Draw draws all currently drawn shapes inside the IM onto another Target.
//
// Note, that IMDraw's matrix and color mask have no effect here.
func (imd *IMDraw) Draw(t pixel.Target) {
imd.batch.Draw(t)
}
@ -115,8 +128,18 @@ func (imd *IMDraw) Draw(t pixel.Target) {
// Push adds some points to the IM queue. All Pushed points will have the same properties except for
// the position.
func (imd *IMDraw) Push(pts ...pixel.Vec) {
if _, ok := imd.Color.(pixel.RGBA); !ok {
imd.Color = pixel.ToRGBA(imd.Color)
}
opts := point{
col: imd.Color.(pixel.RGBA),
pic: imd.Picture,
in: imd.Intensity,
precision: imd.Precision,
endshape: imd.EndShape,
}
for _, pt := range pts {
imd.pushPt(pt, imd.opts)
imd.pushPt(pt, opts)
}
}
@ -125,33 +148,6 @@ func (imd *IMDraw) pushPt(pos pixel.Vec, pt point) {
imd.points = append(imd.points, pt)
}
// Color sets the color of the next Pushed points.
func (imd *IMDraw) Color(color color.Color) {
imd.opts.col = pixel.ToNRGBA(color)
}
// Picture sets the Picture coordinates of the next Pushed points.
func (imd *IMDraw) Picture(pic pixel.Vec) {
imd.opts.pic = pic
}
// Intensity sets the picture Intensity of the next Pushed points.
func (imd *IMDraw) Intensity(in float64) {
imd.opts.in = in
}
// Precision sets the curve/circle drawing precision of the next Pushed points.
//
// It is the number of segments per 360 degrees.
func (imd *IMDraw) Precision(p int) {
imd.opts.precision = p
}
// EndShape sets the endshape of the next Pushed points.
func (imd *IMDraw) EndShape(es EndShape) {
imd.opts.endshape = es
}
// SetMatrix sets a Matrix that all further points will be transformed by.
func (imd *IMDraw) SetMatrix(m pixel.Matrix) {
imd.matrix = m
@ -160,7 +156,7 @@ func (imd *IMDraw) SetMatrix(m pixel.Matrix) {
// SetColorMask sets a color that all further point's color will be multiplied by.
func (imd *IMDraw) SetColorMask(color color.Color) {
imd.mask = pixel.ToNRGBA(color)
imd.mask = pixel.ToRGBA(color)
imd.batch.SetColorMask(imd.mask)
}
@ -179,6 +175,20 @@ func (imd *IMDraw) Line(thickness float64) {
imd.polyline(thickness, false)
}
// Rectangle draws a rectangle between each two subsequent Pushed points. Drawing a rectangle
// between two points means drawing a rectangle with sides parallel to the axes of the coordinate
// system, where the two points specify it's two opposite corners.
//
// If the thickness is 0, rectangles will be filled, otherwise will be outlined with the given
// thickness.
func (imd *IMDraw) Rectangle(thickness float64) {
if thickness == 0 {
imd.fillRectangle()
} else {
imd.outlineRectangle(thickness)
}
}
// Polygon draws a polygon from the Pushed points. If the thickness is 0, the convex polygon will be
// filled. Otherwise, an outline of the specified thickness will be drawn. The outline does not have
// to be convex.
@ -249,10 +259,22 @@ func (imd *IMDraw) EllipseArc(radius pixel.Vec, low, high, thickness float64) {
func (imd *IMDraw) getAndClearPoints() []point {
points := imd.points
imd.points = nil
// use one of the existing pools so we don't reallocate as often
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
}
func (imd *IMDraw) restorePoints(points []point) {
imd.pool = append(imd.pool, imd.points)
imd.points = points[:0]
}
func (imd *IMDraw) applyMatrixAndMask(off int) {
for i := range (*imd.tri)[off:] {
(*imd.tri)[off+i].Position = imd.matrix.Project((*imd.tri)[off+i].Position)
@ -260,10 +282,75 @@ func (imd *IMDraw) applyMatrixAndMask(off int) {
}
}
func (imd *IMDraw) fillRectangle() {
points := imd.getAndClearPoints()
if len(points) < 2 {
imd.restorePoints(points)
return
}
off := imd.tri.Len()
imd.tri.SetLen(imd.tri.Len() + 6*(len(points)-1))
for i, j := 0, off; i+1 < len(points); i, j = i+1, j+6 {
a, b := points[i], points[i+1]
c := point{
pos: pixel.V(a.pos.X, b.pos.Y),
col: a.col.Add(b.col).Mul(pixel.Alpha(0.5)),
pic: pixel.V(a.pic.X, b.pic.Y),
in: (a.in + b.in) / 2,
}
d := point{
pos: pixel.V(b.pos.X, a.pos.Y),
col: a.col.Add(b.col).Mul(pixel.Alpha(0.5)),
pic: pixel.V(b.pic.X, a.pic.Y),
in: (a.in + b.in) / 2,
}
for k, p := range [...]point{a, b, c, a, b, d} {
(*imd.tri)[j+k].Position = p.pos
(*imd.tri)[j+k].Color = p.col
(*imd.tri)[j+k].Picture = p.pic
(*imd.tri)[j+k].Intensity = p.in
}
}
imd.applyMatrixAndMask(off)
imd.batch.Dirty()
imd.restorePoints(points)
}
func (imd *IMDraw) outlineRectangle(thickness float64) {
points := imd.getAndClearPoints()
if len(points) < 2 {
imd.restorePoints(points)
return
}
for i := 0; i+1 < len(points); i++ {
a, b := points[i], points[i+1]
mid := a
mid.col = a.col.Add(b.col).Mul(pixel.Alpha(0.5))
mid.in = (a.in + b.in) / 2
imd.pushPt(a.pos, a)
imd.pushPt(pixel.V(a.pos.X, b.pos.Y), mid)
imd.pushPt(b.pos, b)
imd.pushPt(pixel.V(b.pos.X, a.pos.Y), mid)
imd.polyline(thickness, true)
}
imd.restorePoints(points)
}
func (imd *IMDraw) fillPolygon() {
points := imd.getAndClearPoints()
if len(points) < 3 {
imd.restorePoints(points)
return
}
@ -271,24 +358,19 @@ func (imd *IMDraw) fillPolygon() {
imd.tri.SetLen(imd.tri.Len() + 3*(len(points)-2))
for i, j := 1, off; i+1 < len(points); i, j = i+1, j+3 {
(*imd.tri)[j+0].Position = points[0].pos
(*imd.tri)[j+0].Color = points[0].col
(*imd.tri)[j+0].Picture = points[0].pic
(*imd.tri)[j+0].Intensity = points[0].in
(*imd.tri)[j+1].Position = points[i].pos
(*imd.tri)[j+1].Color = points[i].col
(*imd.tri)[j+1].Picture = points[i].pic
(*imd.tri)[j+1].Intensity = points[i].in
(*imd.tri)[j+2].Position = points[i+1].pos
(*imd.tri)[j+2].Color = points[i+1].col
(*imd.tri)[j+2].Picture = points[i+1].pic
(*imd.tri)[j+2].Intensity = points[i+1].in
for k, p := range [...]int{0, i, i + 1} {
tri := &(*imd.tri)[j+k]
tri.Position = points[p].pos
tri.Color = points[p].col
tri.Picture = points[p].pic
tri.Intensity = points[p].in
}
}
imd.applyMatrixAndMask(off)
imd.batch.Dirty()
imd.restorePoints(points)
}
func (imd *IMDraw) fillEllipseArc(radius pixel.Vec, low, high float64) {
@ -303,24 +385,24 @@ func (imd *IMDraw) fillEllipseArc(radius pixel.Vec, low, high float64) {
for i := range (*imd.tri)[off:] {
(*imd.tri)[off+i].Color = pt.col
(*imd.tri)[off+i].Picture = 0
(*imd.tri)[off+i].Picture = pixel.ZV
(*imd.tri)[off+i].Intensity = 0
}
for i, j := 0.0, off; i < num; i, j = i+1, j+3 {
angle := low + i*delta
sin, cos := math.Sincos(angle)
a := pt.pos + pixel.V(
radius.X()*cos,
radius.Y()*sin,
)
a := pt.pos.Add(pixel.V(
radius.X*cos,
radius.Y*sin,
))
angle = low + (i+1)*delta
sin, cos = math.Sincos(angle)
b := pt.pos + pixel.V(
radius.X()*cos,
radius.Y()*sin,
)
b := pt.pos.Add(pixel.V(
radius.X*cos,
radius.Y*sin,
))
(*imd.tri)[j+0].Position = pt.pos
(*imd.tri)[j+1].Position = a
@ -330,6 +412,8 @@ func (imd *IMDraw) fillEllipseArc(radius pixel.Vec, low, high float64) {
imd.applyMatrixAndMask(off)
imd.batch.Dirty()
}
imd.restorePoints(points)
}
func (imd *IMDraw) outlineEllipseArc(radius pixel.Vec, low, high, thickness float64, doEndShape bool) {
@ -344,7 +428,7 @@ func (imd *IMDraw) outlineEllipseArc(radius pixel.Vec, low, high, thickness floa
for i := range (*imd.tri)[off:] {
(*imd.tri)[off+i].Color = pt.col
(*imd.tri)[off+i].Picture = 0
(*imd.tri)[off+i].Picture = pixel.ZV
(*imd.tri)[off+i].Intensity = 0
}
@ -352,26 +436,26 @@ func (imd *IMDraw) outlineEllipseArc(radius pixel.Vec, low, high, thickness floa
angle := low + i*delta
sin, cos := math.Sincos(angle)
normalSin, normalCos := pixel.V(sin, cos).ScaledXY(radius).Unit().XY()
a := pt.pos + pixel.V(
radius.X()*cos-thickness/2*normalCos,
radius.Y()*sin-thickness/2*normalSin,
)
b := pt.pos + pixel.V(
radius.X()*cos+thickness/2*normalCos,
radius.Y()*sin+thickness/2*normalSin,
)
a := pt.pos.Add(pixel.V(
radius.X*cos-thickness/2*normalCos,
radius.Y*sin-thickness/2*normalSin,
))
b := pt.pos.Add(pixel.V(
radius.X*cos+thickness/2*normalCos,
radius.Y*sin+thickness/2*normalSin,
))
angle = low + (i+1)*delta
sin, cos = math.Sincos(angle)
normalSin, normalCos = pixel.V(sin, cos).ScaledXY(radius).Unit().XY()
c := pt.pos + pixel.V(
radius.X()*cos-thickness/2*normalCos,
radius.Y()*sin-thickness/2*normalSin,
)
d := pt.pos + pixel.V(
radius.X()*cos+thickness/2*normalCos,
radius.Y()*sin+thickness/2*normalSin,
)
c := pt.pos.Add(pixel.V(
radius.X*cos-thickness/2*normalCos,
radius.Y*sin-thickness/2*normalSin,
))
d := pt.pos.Add(pixel.V(
radius.X*cos+thickness/2*normalCos,
radius.Y*sin+thickness/2*normalSin,
))
(*imd.tri)[j+0].Position = a
(*imd.tri)[j+1].Position = b
@ -386,18 +470,18 @@ func (imd *IMDraw) outlineEllipseArc(radius pixel.Vec, low, high, thickness floa
if doEndShape {
lowSin, lowCos := math.Sincos(low)
lowCenter := pt.pos + pixel.V(
radius.X()*lowCos,
radius.Y()*lowSin,
)
lowCenter := pt.pos.Add(pixel.V(
radius.X*lowCos,
radius.Y*lowSin,
))
normalLowSin, normalLowCos := pixel.V(lowSin, lowCos).ScaledXY(radius).Unit().XY()
normalLow := pixel.V(normalLowCos, normalLowSin).Angle()
highSin, highCos := math.Sincos(high)
highCenter := pt.pos + pixel.V(
radius.X()*highCos,
radius.Y()*highSin,
)
highCenter := pt.pos.Add(pixel.V(
radius.X*highCos,
radius.Y*highSin,
))
normalHighSin, normalHighCos := pixel.V(highSin, highCos).ScaledXY(radius).Unit().XY()
normalHigh := pixel.V(normalHighCos, normalHighSin).Angle()
@ -410,30 +494,33 @@ func (imd *IMDraw) outlineEllipseArc(radius pixel.Vec, low, high, thickness floa
case NoEndShape:
// nothing
case SharpEndShape:
thick := pixel.X(thickness / 2).Rotated(normalLow)
imd.pushPt(lowCenter+thick, pt)
imd.pushPt(lowCenter-thick, pt)
imd.pushPt(lowCenter-thick.Rotated(math.Pi/2*orientation), pt)
thick := pixel.V(thickness/2, 0).Rotated(normalLow)
imd.pushPt(lowCenter.Add(thick), pt)
imd.pushPt(lowCenter.Sub(thick), pt)
imd.pushPt(lowCenter.Sub(thick.Normal().Scaled(orientation)), pt)
imd.fillPolygon()
thick = pixel.X(thickness / 2).Rotated(normalHigh)
imd.pushPt(highCenter+thick, pt)
imd.pushPt(highCenter-thick, pt)
imd.pushPt(highCenter+thick.Rotated(math.Pi/2*orientation), pt)
thick = pixel.V(thickness/2, 0).Rotated(normalHigh)
imd.pushPt(highCenter.Add(thick), pt)
imd.pushPt(highCenter.Sub(thick), pt)
imd.pushPt(highCenter.Add(thick.Normal().Scaled(orientation)), pt)
imd.fillPolygon()
case RoundEndShape:
imd.pushPt(lowCenter, pt)
imd.fillEllipseArc(pixel.V(thickness, thickness)/2, normalLow, normalLow-math.Pi*orientation)
imd.fillEllipseArc(pixel.V(thickness/2, thickness/2), normalLow, normalLow-math.Pi*orientation)
imd.pushPt(highCenter, pt)
imd.fillEllipseArc(pixel.V(thickness, thickness)/2, normalHigh, normalHigh+math.Pi*orientation)
imd.fillEllipseArc(pixel.V(thickness/2, thickness/2), normalHigh, normalHigh+math.Pi*orientation)
}
}
}
imd.restorePoints(points)
}
func (imd *IMDraw) polyline(thickness float64, closed bool) {
points := imd.getAndClearPoints()
if len(points) == 0 {
imd.restorePoints(points)
return
}
if len(points) == 1 {
@ -443,25 +530,25 @@ func (imd *IMDraw) polyline(thickness float64, closed bool) {
// first point
j, i := 0, 1
normal := (points[i].pos - points[j].pos).Rotated(math.Pi / 2).Unit().Scaled(thickness / 2)
ijNormal := points[0].pos.To(points[1].pos).Normal().Unit().Scaled(thickness / 2)
if !closed {
switch points[j].endshape {
case NoEndShape:
// nothing
case SharpEndShape:
imd.pushPt(points[j].pos+normal, points[j])
imd.pushPt(points[j].pos-normal, points[j])
imd.pushPt(points[j].pos+normal.Rotated(math.Pi/2), points[j])
imd.pushPt(points[j].pos.Add(ijNormal), points[j])
imd.pushPt(points[j].pos.Sub(ijNormal), points[j])
imd.pushPt(points[j].pos.Add(ijNormal.Normal()), points[j])
imd.fillPolygon()
case RoundEndShape:
imd.pushPt(points[j].pos, points[j])
imd.fillEllipseArc(pixel.V(thickness, thickness)/2, normal.Angle(), normal.Angle()+math.Pi)
imd.fillEllipseArc(pixel.V(thickness/2, thickness/2), ijNormal.Angle(), ijNormal.Angle()+math.Pi)
}
}
imd.pushPt(points[j].pos+normal, points[j])
imd.pushPt(points[j].pos-normal, points[j])
imd.pushPt(points[j].pos.Add(ijNormal), points[j])
imd.pushPt(points[j].pos.Sub(ijNormal), points[j])
// middle points
for i := 0; i < len(points); i++ {
@ -469,26 +556,25 @@ func (imd *IMDraw) polyline(thickness float64, closed bool) {
closing := false
if j >= len(points) {
if !closed {
break
}
j %= len(points)
closing = true
}
if k >= len(points) {
if !closed {
break
}
k %= len(points)
}
ijNormal := (points[j].pos - points[i].pos).Rotated(math.Pi / 2).Unit().Scaled(thickness / 2)
jkNormal := (points[k].pos - points[j].pos).Rotated(math.Pi / 2).Unit().Scaled(thickness / 2)
jkNormal := points[j].pos.To(points[k].pos).Normal().Unit().Scaled(thickness / 2)
orientation := 1.0
if ijNormal.Cross(jkNormal) > 0 {
orientation = -1.0
}
imd.pushPt(points[j].pos-ijNormal, points[j])
imd.pushPt(points[j].pos+ijNormal, points[j])
imd.pushPt(points[j].pos.Sub(ijNormal), points[j])
imd.pushPt(points[j].pos.Add(ijNormal), points[j])
imd.fillPolygon()
switch points[j].endshape {
@ -496,28 +582,30 @@ func (imd *IMDraw) polyline(thickness float64, closed bool) {
// nothing
case SharpEndShape:
imd.pushPt(points[j].pos, points[j])
imd.pushPt(points[j].pos+ijNormal.Scaled(orientation), points[j])
imd.pushPt(points[j].pos+jkNormal.Scaled(orientation), points[j])
imd.pushPt(points[j].pos.Add(ijNormal.Scaled(orientation)), points[j])
imd.pushPt(points[j].pos.Add(jkNormal.Scaled(orientation)), points[j])
imd.fillPolygon()
case RoundEndShape:
imd.pushPt(points[j].pos, points[j])
imd.fillEllipseArc(pixel.V(thickness, thickness)/2, ijNormal.Angle(), ijNormal.Angle()-math.Pi)
imd.fillEllipseArc(pixel.V(thickness/2, thickness/2), ijNormal.Angle(), ijNormal.Angle()-math.Pi)
imd.pushPt(points[j].pos, points[j])
imd.fillEllipseArc(pixel.V(thickness, thickness)/2, jkNormal.Angle(), jkNormal.Angle()+math.Pi)
imd.fillEllipseArc(pixel.V(thickness/2, thickness/2), jkNormal.Angle(), jkNormal.Angle()+math.Pi)
}
if !closing {
imd.pushPt(points[j].pos+jkNormal, points[j])
imd.pushPt(points[j].pos-jkNormal, points[j])
imd.pushPt(points[j].pos.Add(jkNormal), points[j])
imd.pushPt(points[j].pos.Sub(jkNormal), points[j])
}
// "next" normal becomes previous normal
ijNormal = jkNormal
}
// last point
i, j = len(points)-2, len(points)-1
normal = (points[j].pos - points[i].pos).Rotated(math.Pi / 2).Unit().Scaled(thickness / 2)
ijNormal = points[i].pos.To(points[j].pos).Normal().Unit().Scaled(thickness / 2)
imd.pushPt(points[j].pos-normal, points[j])
imd.pushPt(points[j].pos+normal, points[j])
imd.pushPt(points[j].pos.Sub(ijNormal), points[j])
imd.pushPt(points[j].pos.Add(ijNormal), points[j])
imd.fillPolygon()
if !closed {
@ -525,13 +613,15 @@ func (imd *IMDraw) polyline(thickness float64, closed bool) {
case NoEndShape:
// nothing
case SharpEndShape:
imd.pushPt(points[j].pos+normal, points[j])
imd.pushPt(points[j].pos-normal, points[j])
imd.pushPt(points[j].pos+normal.Rotated(-math.Pi/2), points[j])
imd.pushPt(points[j].pos.Add(ijNormal), points[j])
imd.pushPt(points[j].pos.Sub(ijNormal), points[j])
imd.pushPt(points[j].pos.Add(ijNormal.Normal().Scaled(-1)), points[j])
imd.fillPolygon()
case RoundEndShape:
imd.pushPt(points[j].pos, points[j])
imd.fillEllipseArc(pixel.V(thickness, thickness)/2, normal.Angle(), normal.Angle()-math.Pi)
imd.fillEllipseArc(pixel.V(thickness/2, thickness/2), ijNormal.Angle(), ijNormal.Angle()-math.Pi)
}
}
imd.restorePoints(points)
}

96
imdraw/imdraw_test.go Normal file
View File

@ -0,0 +1,96 @@
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

@ -89,7 +89,7 @@ type TrianglesPosition interface {
// TrianglesColor specifies Triangles with Color property.
type TrianglesColor interface {
Triangles
Color(i int) NRGBA
Color(i int) RGBA
}
// TrianglesPicture specifies Triangles with Picture propery.
@ -108,19 +108,6 @@ type Picture interface {
// Bounds returns the rectangle of the Picture. All data is located witih this rectangle.
// Querying properties outside the rectangle should return default value of that property.
Bounds() Rect
// Slice returns a sub-Picture with specified Bounds.
//
// A result of Slice-ing outside the original Bounds is unspecified.
Slice(Rect) Picture
// Original returns the most original Picture (may be itself) that this Picture was created
// from using Slice-ing.
//
// Since the Original and this Picture should share the underlying data and this Picture can
// be obtained just by slicing the Original, this method can be used for more efficient
// caching of Pictures.
Original() Picture
}
// TargetPicture is a Picture generated by a Target using MakePicture method. This Picture can be drawn onto
@ -139,8 +126,8 @@ type TargetPicture interface {
// PictureColor specifies Picture with Color property, so that every position inside the Picture's
// Bounds has a color.
//
// Positions outside the Picture's Bounds must return transparent black (NRGBA{R: 0, G: 0, B: 0, A: 0}).
// Positions outside the Picture's Bounds must return full transparent (Alpha(0)).
type PictureColor interface {
Picture
Color(at Vec) NRGBA
Color(at Vec) RGBA
}

63
matrix_test.go Normal file
View File

@ -0,0 +1,63 @@
package pixel_test
import (
"math/rand"
"testing"
"github.com/faiface/pixel"
)
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)
}
})
}

View File

@ -3,7 +3,6 @@ package pixelgl
import (
"fmt"
"image/color"
"math"
"github.com/faiface/glhf"
"github.com/faiface/mainthread"
@ -17,35 +16,33 @@ import (
//
// It supports TrianglesPosition, TrianglesColor, TrianglesPicture and PictureColor.
type Canvas struct {
// these should **only** be accessed through orig
f *glhf.Frame
borders pixel.Rect
pixels []uint8
dirty bool
gf *GLFrame
shader *glhf.Shader
// these should **never** be accessed through orig
s *glhf.Shader
bounds pixel.Rect
cmp pixel.ComposeMethod
mat mgl32.Mat3
col mgl32.Vec4
smooth bool
orig *Canvas
sprite *pixel.Sprite
}
// NewCanvas creates a new empty, fully transparent Canvas with given bounds. If the smooth flag is
// set, then stretched Pictures will be smoothed and will not be drawn pixely onto this Canvas.
func NewCanvas(bounds pixel.Rect, smooth bool) *Canvas {
c := &Canvas{
smooth: smooth,
mat: mgl32.Ident3(),
col: mgl32.Vec4{1, 1, 1, 1},
}
c.orig = c
var _ pixel.ComposeTarget = (*Canvas)(nil)
// NewCanvas creates a new empty, fully transparent Canvas with given bounds.
func NewCanvas(bounds pixel.Rect) *Canvas {
c := &Canvas{
gf: NewGLFrame(bounds),
mat: mgl32.Ident3(),
col: mgl32.Vec4{1, 1, 1, 1},
}
c.SetBounds(bounds)
var shader *glhf.Shader
mainthread.Call(func() {
var err error
c.s, err = glhf.NewShader(
shader, err = glhf.NewShader(
canvasVertexFormat,
canvasUniformFormat,
canvasVertexShader,
@ -55,8 +52,7 @@ func NewCanvas(bounds pixel.Rect, smooth bool) *Canvas {
panic(errors.Wrap(err, "failed to create Canvas, there's a bug in the shader"))
}
})
c.SetBounds(bounds)
c.shader = shader
return c
}
@ -66,7 +62,7 @@ func NewCanvas(bounds pixel.Rect, smooth bool) *Canvas {
// TrianglesPosition, TrianglesColor and TrianglesPicture are supported.
func (c *Canvas) MakeTriangles(t pixel.Triangles) pixel.TargetTriangles {
return &canvasTriangles{
GLTriangles: NewGLTriangles(c.s, t),
GLTriangles: NewGLTriangles(c.shader, t),
dst: c,
}
}
@ -75,143 +71,69 @@ func (c *Canvas) MakeTriangles(t pixel.Triangles) pixel.TargetTriangles {
//
// PictureColor is supported.
func (c *Canvas) MakePicture(p pixel.Picture) pixel.TargetPicture {
// short paths
if cp, ok := p.(*canvasPicture); ok {
tp := new(canvasPicture)
*tp = *cp
tp.dst = c
return tp
}
if ccp, ok := p.(*canvasCanvasPicture); ok {
tp := new(canvasCanvasPicture)
*tp = *ccp
tp.dst = c
return tp
}
// Canvas special case
if canvas, ok := p.(*Canvas); ok {
return &canvasCanvasPicture{
src: canvas,
dst: c,
bounds: c.bounds,
return &canvasPicture{
GLPicture: cp.GLPicture,
dst: c,
}
}
bounds := p.Bounds()
bx, by, bw, bh := intBounds(bounds)
pixels := make([]uint8, 4*bw*bh)
if pd, ok := p.(*pixel.PictureData); ok {
// PictureData short path
for y := 0; y < bh; y++ {
for x := 0; x < bw; x++ {
nrgba := pd.Pix[y*pd.Stride+x]
off := (y*bw + x) * 4
pixels[off+0] = nrgba.R
pixels[off+1] = nrgba.G
pixels[off+2] = nrgba.B
pixels[off+3] = nrgba.A
}
}
} else if p, ok := p.(pixel.PictureColor); ok {
for y := 0; y < bh; y++ {
for x := 0; x < bw; x++ {
at := pixel.V(
math.Max(float64(bx+x), bounds.Min.X()),
math.Max(float64(by+y), bounds.Min.Y()),
)
color := p.Color(at)
off := (y*bw + x) * 4
pixels[off+0] = uint8(color.R * 255)
pixels[off+1] = uint8(color.G * 255)
pixels[off+2] = uint8(color.B * 255)
pixels[off+3] = uint8(color.A * 255)
}
if gp, ok := p.(GLPicture); ok {
return &canvasPicture{
GLPicture: gp,
dst: c,
}
}
var tex *glhf.Texture
mainthread.Call(func() {
tex = glhf.NewTexture(bw, bh, c.smooth, pixels)
})
cp := &canvasPicture{
tex: tex,
pixels: pixels,
borders: pixel.R(
float64(bx), float64(by),
float64(bw), float64(bh),
),
bounds: bounds,
dst: c,
return &canvasPicture{
GLPicture: NewGLPicture(p),
dst: c,
}
cp.orig = cp
return cp
}
// SetMatrix sets a Matrix that every point will be projected by.
func (c *Canvas) SetMatrix(m pixel.Matrix) {
for i := range m {
c.mat[i] = float32(m[i])
// pixel.Matrix is 3x2 with an implicit 0, 0, 1 row after it. So
// [0] [2] [4] [0] [3] [6]
// [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])
}
}
// SetColorMask sets a color that every color in triangles or a picture will be multiplied by.
func (c *Canvas) SetColorMask(col color.Color) {
nrgba := pixel.NRGBA{R: 1, G: 1, B: 1, A: 1}
rgba := pixel.Alpha(1)
if col != nil {
nrgba = pixel.ToNRGBA(col)
rgba = pixel.ToRGBA(col)
}
c.col = mgl32.Vec4{
float32(nrgba.R),
float32(nrgba.G),
float32(nrgba.B),
float32(nrgba.A),
float32(rgba.R),
float32(rgba.G),
float32(rgba.B),
float32(rgba.A),
}
}
// SetComposeMethod sets a Porter-Duff composition method to be used in the following draws onto
// this Canvas.
func (c *Canvas) SetComposeMethod(cmp pixel.ComposeMethod) {
c.cmp = cmp
}
// SetBounds resizes the Canvas to the new bounds. Old content will be preserved.
//
// If the new Bounds fit into the Original borders, no new Canvas will be allocated.
func (c *Canvas) SetBounds(bounds pixel.Rect) {
c.bounds = bounds
// if this bounds fit into the original bounds, no need to reallocate
if c.orig.borders.Contains(bounds.Min) && c.orig.borders.Contains(bounds.Max) {
return
c.gf.SetBounds(bounds)
if c.sprite == nil {
c.sprite = pixel.NewSprite(nil, pixel.Rect{})
}
mainthread.Call(func() {
oldF := c.orig.f
_, _, w, h := intBounds(bounds)
c.f = glhf.NewFrame(w, h, c.smooth)
// preserve old content
if oldF != nil {
relBounds := bounds
relBounds = relBounds.Moved(-c.orig.borders.Min)
ox, oy, ow, oh := intBounds(relBounds)
oldF.Blit(
c.f,
ox, oy, ox+ow, oy+oh,
ox, oy, ox+ow, oy+oh,
)
}
})
// detach from orig
c.borders = bounds
c.pixels = nil
c.dirty = true
c.orig = c
c.sprite.Set(c, c.Bounds())
//c.sprite.SetMatrix(pixel.IM.Moved(c.Bounds().Center()))
}
// Bounds returns the rectangular bounds of the Canvas.
func (c *Canvas) Bounds() pixel.Rect {
return c.bounds
return c.gf.Bounds()
}
// SetSmooth sets whether stretched Pictures drawn onto this Canvas should be drawn smooth or
@ -228,20 +150,48 @@ func (c *Canvas) Smooth() bool {
// must be manually called inside mainthread
func (c *Canvas) setGlhfBounds() {
bounds := c.bounds
bounds.Moved(c.orig.borders.Min)
bx, by, bw, bh := intBounds(bounds)
glhf.Bounds(bx, by, bw, bh)
_, _, bw, bh := intBounds(c.gf.Bounds())
glhf.Bounds(0, 0, bw, bh)
}
// must be manually called inside mainthread
func setBlendFunc(cmp pixel.ComposeMethod) {
switch cmp {
case pixel.ComposeOver:
glhf.BlendFunc(glhf.One, glhf.OneMinusSrcAlpha)
case pixel.ComposeIn:
glhf.BlendFunc(glhf.DstAlpha, glhf.Zero)
case pixel.ComposeOut:
glhf.BlendFunc(glhf.OneMinusDstAlpha, glhf.Zero)
case pixel.ComposeAtop:
glhf.BlendFunc(glhf.DstAlpha, glhf.OneMinusSrcAlpha)
case pixel.ComposeRover:
glhf.BlendFunc(glhf.OneMinusDstAlpha, glhf.One)
case pixel.ComposeRin:
glhf.BlendFunc(glhf.Zero, glhf.SrcAlpha)
case pixel.ComposeRout:
glhf.BlendFunc(glhf.Zero, glhf.OneMinusSrcAlpha)
case pixel.ComposeRatop:
glhf.BlendFunc(glhf.OneMinusDstAlpha, glhf.SrcAlpha)
case pixel.ComposeXor:
glhf.BlendFunc(glhf.OneMinusDstAlpha, glhf.OneMinusSrcAlpha)
case pixel.ComposePlus:
glhf.BlendFunc(glhf.One, glhf.One)
case pixel.ComposeCopy:
glhf.BlendFunc(glhf.One, glhf.Zero)
default:
panic(errors.New("Canvas: invalid compose method"))
}
}
// Clear fills the whole Canvas with a single color.
func (c *Canvas) Clear(color color.Color) {
c.orig.dirty = true
c.gf.Dirty()
nrgba := pixel.ToNRGBA(color)
rgba := pixel.ToRGBA(color)
// color masking
nrgba = nrgba.Mul(pixel.NRGBA{
rgba = rgba.Mul(pixel.RGBA{
R: float64(c.col[0]),
G: float64(c.col[1]),
B: float64(c.col[2]),
@ -250,87 +200,108 @@ func (c *Canvas) Clear(color color.Color) {
mainthread.CallNonBlock(func() {
c.setGlhfBounds()
c.orig.f.Begin()
c.gf.Frame().Begin()
glhf.Clear(
float32(nrgba.R),
float32(nrgba.G),
float32(nrgba.B),
float32(nrgba.A),
float32(rgba.R),
float32(rgba.G),
float32(rgba.B),
float32(rgba.A),
)
c.orig.f.End()
c.gf.Frame().End()
})
}
// Slice returns a sub-Canvas with the specified Bounds.
//
// The type of the returned value is *Canvas, the type of the return value is a general
// pixel.Picture just so that Canvas implements pixel.Picture interface.
func (c *Canvas) Slice(bounds pixel.Rect) pixel.Picture {
sc := new(Canvas)
*sc = *c
sc.bounds = bounds
return sc
}
// Original returns the most original Canvas that this Canvas was created from using Slice-ing.
//
// The type of the returned value is *Canvas, the type of the return value is a general
// pixel.Picture just so that Canvas implements pixel.Picture interface.
func (c *Canvas) Original() pixel.Picture {
return c.orig
}
// Color returns the color of the pixel over the given position inside the Canvas.
func (c *Canvas) Color(at pixel.Vec) pixel.NRGBA {
if c.orig.dirty {
mainthread.Call(func() {
tex := c.orig.f.Texture()
tex.Begin()
c.orig.pixels = tex.Pixels(0, 0, tex.Width(), tex.Height())
tex.End()
})
c.orig.dirty = false
}
if !c.bounds.Contains(at) {
return pixel.NRGBA{}
}
bx, by, bw, _ := intBounds(c.orig.borders)
x, y := int(at.X())-bx, int(at.Y())-by
off := y*bw + x
return pixel.NRGBA{
R: float64(c.orig.pixels[off*4+0]) / 255,
G: float64(c.orig.pixels[off*4+1]) / 255,
B: float64(c.orig.pixels[off*4+2]) / 255,
A: float64(c.orig.pixels[off*4+3]) / 255,
}
func (c *Canvas) Color(at pixel.Vec) pixel.RGBA {
return c.gf.Color(at)
}
// Texture returns the underlying OpenGL Texture of this Canvas.
//
// Implements GLPicture interface.
func (c *Canvas) Texture() *glhf.Texture {
return c.gf.Texture()
}
// Frame returns the underlying OpenGL Frame of this Canvas.
func (c *Canvas) Frame() *glhf.Frame {
return c.gf.frame
}
// SetPixels replaces the content of the Canvas with the provided pixels. The provided slice must be
// an alpha-premultiplied RGBA sequence of correct length (4 * width * height).
func (c *Canvas) SetPixels(pixels []uint8) {
c.gf.Dirty()
mainthread.Call(func() {
tex := c.Texture()
tex.Begin()
tex.SetPixels(0, 0, tex.Width(), tex.Height(), pixels)
tex.End()
})
}
// Pixels returns an alpha-premultiplied RGBA sequence of the content of the Canvas.
func (c *Canvas) Pixels() []uint8 {
var pixels []uint8
mainthread.Call(func() {
tex := c.Texture()
tex.Begin()
pixels = tex.Pixels(0, 0, tex.Width(), tex.Height())
tex.End()
})
return pixels
}
// Draw draws the content of the Canvas onto another Target, transformed by the given Matrix, just
// like if it was a Sprite containing the whole Canvas.
func (c *Canvas) Draw(t pixel.Target, matrix pixel.Matrix) {
c.sprite.Draw(t, matrix)
}
// DrawColorMask draws the content of the Canvas onto another Target, transformed by the given
// Matrix and multiplied by the given mask, just like if it was a Sprite containing the whole Canvas.
//
// If the color mask is nil, a fully opaque white mask will be used causing no effect.
func (c *Canvas) DrawColorMask(t pixel.Target, matrix pixel.Matrix, mask color.Color) {
c.sprite.DrawColorMask(t, matrix, mask)
}
type canvasTriangles struct {
*GLTriangles
dst *Canvas
}
func (ct *canvasTriangles) draw(tex *glhf.Texture, borders, bounds pixel.Rect) {
ct.dst.orig.dirty = true
func (ct *canvasTriangles) draw(tex *glhf.Texture, bounds pixel.Rect) {
ct.dst.gf.Dirty()
// save the current state vars to avoid race condition
cmp := ct.dst.cmp
mat := ct.dst.mat
col := ct.dst.col
smt := ct.dst.smooth
mainthread.CallNonBlock(func() {
ct.dst.setGlhfBounds()
ct.dst.orig.f.Begin()
ct.dst.s.Begin()
setBlendFunc(cmp)
ct.dst.s.SetUniformAttr(canvasBounds, mgl32.Vec4{
float32(ct.dst.bounds.Min.X()),
float32(ct.dst.bounds.Min.Y()),
float32(ct.dst.bounds.W()),
float32(ct.dst.bounds.H()),
frame := ct.dst.gf.Frame()
shader := ct.dst.shader
frame.Begin()
shader.Begin()
dstBounds := ct.dst.Bounds()
shader.SetUniformAttr(canvasBounds, mgl32.Vec4{
float32(dstBounds.Min.X),
float32(dstBounds.Min.Y),
float32(dstBounds.W()),
float32(dstBounds.H()),
})
ct.dst.s.SetUniformAttr(canvasTransform, mat)
ct.dst.s.SetUniformAttr(canvasColorMask, col)
shader.SetUniformAttr(canvasTransform, mat)
shader.SetUniformAttr(canvasColorMask, col)
if tex == nil {
ct.vs.Begin()
@ -339,21 +310,16 @@ func (ct *canvasTriangles) draw(tex *glhf.Texture, borders, bounds pixel.Rect) {
} else {
tex.Begin()
ct.dst.s.SetUniformAttr(canvasTexBorders, mgl32.Vec4{
float32(borders.Min.X()),
float32(borders.Min.Y()),
float32(borders.W()),
float32(borders.H()),
})
ct.dst.s.SetUniformAttr(canvasTexBounds, mgl32.Vec4{
float32(bounds.Min.X()),
float32(bounds.Min.Y()),
float32(bounds.W()),
float32(bounds.H()),
bx, by, bw, bh := intBounds(bounds)
shader.SetUniformAttr(canvasTexBounds, mgl32.Vec4{
float32(bx),
float32(by),
float32(bw),
float32(bh),
})
if tex.Smooth() != ct.dst.smooth {
tex.SetSmooth(ct.dst.smooth)
if tex.Smooth() != smt {
tex.SetSmooth(smt)
}
ct.vs.Begin()
@ -363,53 +329,18 @@ func (ct *canvasTriangles) draw(tex *glhf.Texture, borders, bounds pixel.Rect) {
tex.End()
}
ct.dst.s.End()
ct.dst.orig.f.End()
shader.End()
frame.End()
})
}
func (ct *canvasTriangles) Draw() {
ct.draw(nil, pixel.Rect{}, pixel.Rect{})
ct.draw(nil, pixel.Rect{})
}
type canvasPicture struct {
tex *glhf.Texture
pixels []uint8
borders pixel.Rect
bounds pixel.Rect
orig *canvasPicture
dst *Canvas
}
func (cp *canvasPicture) Bounds() pixel.Rect {
return cp.bounds
}
func (cp *canvasPicture) Slice(r pixel.Rect) pixel.Picture {
sp := new(canvasPicture)
*sp = *cp
sp.bounds = r
return sp
}
func (cp *canvasPicture) Original() pixel.Picture {
return cp.orig
}
func (cp *canvasPicture) Color(at pixel.Vec) pixel.NRGBA {
if !cp.bounds.Contains(at) {
return pixel.NRGBA{}
}
bx, by, bw, _ := intBounds(cp.borders)
x, y := int(at.X())-bx, int(at.Y())-by
off := y*bw + x
return pixel.NRGBA{
R: float64(cp.pixels[off*4+0]) / 255,
G: float64(cp.pixels[off*4+1]) / 255,
B: float64(cp.pixels[off*4+2]) / 255,
A: float64(cp.pixels[off*4+3]) / 255,
}
GLPicture
dst *Canvas
}
func (cp *canvasPicture) Draw(t pixel.TargetTriangles) {
@ -417,56 +348,20 @@ func (cp *canvasPicture) Draw(t pixel.TargetTriangles) {
if cp.dst != ct.dst {
panic(fmt.Errorf("(%T).Draw: TargetTriangles generated by different Canvas", cp))
}
ct.draw(cp.tex, cp.borders, cp.bounds)
}
type canvasCanvasPicture struct {
src, dst *Canvas
bounds pixel.Rect
orig *canvasCanvasPicture
}
func (ccp *canvasCanvasPicture) Bounds() pixel.Rect {
return ccp.bounds
}
func (ccp *canvasCanvasPicture) Slice(r pixel.Rect) pixel.Picture {
sp := new(canvasCanvasPicture)
*sp = *ccp
sp.bounds = r
return sp
}
func (ccp *canvasCanvasPicture) Original() pixel.Picture {
return ccp.orig
}
func (ccp *canvasCanvasPicture) Color(at pixel.Vec) pixel.NRGBA {
if !ccp.bounds.Contains(at) {
return pixel.NRGBA{}
}
return ccp.src.Color(at)
}
func (ccp *canvasCanvasPicture) Draw(t pixel.TargetTriangles) {
ct := t.(*canvasTriangles)
if ccp.dst != ct.dst {
panic(fmt.Errorf("(%T).Draw: TargetTriangles generated by different Canvas", ccp))
}
ct.draw(ccp.src.orig.f.Texture(), ccp.src.orig.borders, ccp.bounds)
ct.draw(cp.GLPicture.Texture(), cp.GLPicture.Bounds())
}
const (
canvasPosition int = iota
canvasColor
canvasTexture
canvasTexCoords
canvasIntensity
)
var canvasVertexFormat = glhf.AttrFormat{
canvasPosition: {Name: "position", Type: glhf.Vec2},
canvasColor: {Name: "color", Type: glhf.Vec4},
canvasTexture: {Name: "texture", Type: glhf.Vec2},
canvasTexCoords: {Name: "texCoords", Type: glhf.Vec2},
canvasIntensity: {Name: "intensity", Type: glhf.Float},
}
@ -474,16 +369,14 @@ const (
canvasTransform int = iota
canvasColorMask
canvasBounds
canvasTexBorders
canvasTexBounds
)
var canvasUniformFormat = glhf.AttrFormat{
canvasTransform: {Name: "transform", Type: glhf.Mat3},
canvasColorMask: {Name: "colorMask", Type: glhf.Vec4},
canvasBounds: {Name: "bounds", Type: glhf.Vec4},
canvasTexBorders: {Name: "texBorders", Type: glhf.Vec4},
canvasTexBounds: {Name: "texBounds", Type: glhf.Vec4},
canvasTransform: {Name: "transform", Type: glhf.Mat3},
canvasColorMask: {Name: "colorMask", Type: glhf.Vec4},
canvasBounds: {Name: "bounds", Type: glhf.Vec4},
canvasTexBounds: {Name: "texBounds", Type: glhf.Vec4},
}
var canvasVertexShader = `
@ -491,23 +384,22 @@ var canvasVertexShader = `
in vec2 position;
in vec4 color;
in vec2 texture;
in vec2 texCoords;
in float intensity;
out vec4 Color;
out vec2 Texture;
out vec2 TexCoords;
out float Intensity;
uniform mat3 transform;
uniform vec4 borders;
uniform vec4 bounds;
void main() {
vec2 transPos = (transform * vec3(position, 1.0)).xy;
vec2 normPos = (transPos - bounds.xy) / (bounds.zw) * 2 - vec2(1, 1);
vec2 normPos = (transPos - bounds.xy) / bounds.zw * 2 - vec2(1, 1);
gl_Position = vec4(normPos, 0.0, 1.0);
Color = color;
Texture = texture;
TexCoords = texCoords;
Intensity = intensity;
}
`
@ -516,13 +408,12 @@ var canvasFragmentShader = `
#version 330 core
in vec4 Color;
in vec2 Texture;
in vec2 TexCoords;
in float Intensity;
out vec4 color;
uniform vec4 colorMask;
uniform vec4 texBorders;
uniform vec4 texBounds;
uniform sampler2D tex;
@ -531,16 +422,10 @@ void main() {
color = colorMask * Color;
} else {
color = vec4(0, 0, 0, 0);
color += (1 - Intensity) * colorMask * Color;
float bx = texBounds.x;
float by = texBounds.y;
float bw = texBounds.z;
float bh = texBounds.w;
if (bx <= Texture.x && Texture.x <= bx + bw && by <= Texture.y && Texture.y <= by + bh) {
vec2 t = (Texture - texBorders.xy) / texBorders.zw;
color += Intensity * colorMask * Color * texture(tex, t);
}
color += (1 - Intensity) * Color;
vec2 t = (TexCoords - texBounds.xy) / texBounds.zw;
color += Intensity * Color * texture(tex, t);
color *= colorMask;
}
}
`

5
pixelgl/doc.go Normal file
View File

@ -0,0 +1,5 @@
// Package pixelgl implements efficient OpenGL targets and utilities for the Pixel game development
// library, specifically Window and Canvas.
//
// It also contains a few additional utilities to help extend Pixel with OpenGL graphical effects.
package pixelgl

105
pixelgl/glframe.go Normal file
View File

@ -0,0 +1,105 @@
package pixelgl
import (
"github.com/faiface/glhf"
"github.com/faiface/mainthread"
"github.com/faiface/pixel"
)
// GLFrame is a type that helps implementing OpenGL Targets. It implements most common methods to
// avoid code redundancy. It contains an glhf.Frame that you can draw on.
type GLFrame struct {
frame *glhf.Frame
bounds pixel.Rect
pixels []uint8
dirty bool
}
// NewGLFrame creates a new GLFrame with the given bounds.
func NewGLFrame(bounds pixel.Rect) *GLFrame {
gf := new(GLFrame)
gf.SetBounds(bounds)
return gf
}
// SetBounds resizes the GLFrame to the new bounds.
func (gf *GLFrame) SetBounds(bounds pixel.Rect) {
if bounds == gf.Bounds() {
return
}
mainthread.Call(func() {
oldF := gf.frame
_, _, w, h := intBounds(bounds)
if w <= 0 {
w = 1
}
if h <= 0 {
h = 1
}
gf.frame = glhf.NewFrame(w, h, false)
// preserve old content
if oldF != nil {
ox, oy, ow, oh := intBounds(bounds)
oldF.Blit(
gf.frame,
ox, oy, ox+ow, oy+oh,
ox, oy, ox+ow, oy+oh,
)
}
})
gf.bounds = bounds
gf.pixels = nil
gf.dirty = true
}
// Bounds returns the current GLFrame's bounds.
func (gf *GLFrame) Bounds() pixel.Rect {
return gf.bounds
}
// Color returns the color of the pixel under the specified position.
func (gf *GLFrame) Color(at pixel.Vec) pixel.RGBA {
if gf.dirty {
mainthread.Call(func() {
tex := gf.frame.Texture()
tex.Begin()
gf.pixels = tex.Pixels(0, 0, tex.Width(), tex.Height())
tex.End()
})
gf.dirty = false
}
if !gf.bounds.Contains(at) {
return pixel.Alpha(0)
}
bx, by, bw, _ := intBounds(gf.bounds)
x, y := int(at.X)-bx, int(at.Y)-by
off := y*bw + x
return pixel.RGBA{
R: float64(gf.pixels[off*4+0]) / 255,
G: float64(gf.pixels[off*4+1]) / 255,
B: float64(gf.pixels[off*4+2]) / 255,
A: float64(gf.pixels[off*4+3]) / 255,
}
}
// Frame returns the GLFrame's Frame that you can draw on.
func (gf *GLFrame) Frame() *glhf.Frame {
return gf.frame
}
// Texture returns the underlying Texture of the GLFrame's Frame.
//
// Implements GLPicture interface.
func (gf *GLFrame) Texture() *glhf.Texture {
return gf.frame.Texture()
}
// Dirty marks the GLFrame as changed. Always call this method when you draw onto the GLFrame's
// Frame.
func (gf *GLFrame) Dirty() {
gf.dirty = true
}

98
pixelgl/glpicture.go Normal file
View File

@ -0,0 +1,98 @@
package pixelgl
import (
"math"
"github.com/faiface/glhf"
"github.com/faiface/mainthread"
"github.com/faiface/pixel"
)
// GLPicture is a pixel.PictureColor with a Texture. All OpenGL Targets should implement and accept
// this interface, because it enables seamless drawing of one to another.
//
// Implementing this interface on an OpenGL Target enables other OpenGL Targets to efficiently draw
// that Target onto them.
type GLPicture interface {
pixel.PictureColor
Texture() *glhf.Texture
}
// NewGLPicture creates a new GLPicture with it's own static OpenGL texture. This function always
// allocates a new texture that cannot (shouldn't) be further modified.
func NewGLPicture(p pixel.Picture) GLPicture {
bounds := p.Bounds()
bx, by, bw, bh := intBounds(bounds)
pixels := make([]uint8, 4*bw*bh)
if pd, ok := p.(*pixel.PictureData); ok {
// PictureData short path
for y := 0; y < bh; y++ {
for x := 0; x < bw; x++ {
rgba := pd.Pix[y*pd.Stride+x]
off := (y*bw + x) * 4
pixels[off+0] = rgba.R
pixels[off+1] = rgba.G
pixels[off+2] = rgba.B
pixels[off+3] = rgba.A
}
}
} else if p, ok := p.(pixel.PictureColor); ok {
for y := 0; y < bh; y++ {
for x := 0; x < bw; x++ {
at := pixel.V(
math.Max(float64(bx+x), bounds.Min.X),
math.Max(float64(by+y), bounds.Min.Y),
)
color := p.Color(at)
off := (y*bw + x) * 4
pixels[off+0] = uint8(color.R * 255)
pixels[off+1] = uint8(color.G * 255)
pixels[off+2] = uint8(color.B * 255)
pixels[off+3] = uint8(color.A * 255)
}
}
}
var tex *glhf.Texture
mainthread.Call(func() {
tex = glhf.NewTexture(bw, bh, false, pixels)
})
gp := &glPicture{
bounds: bounds,
tex: tex,
pixels: pixels,
}
return gp
}
type glPicture struct {
bounds pixel.Rect
tex *glhf.Texture
pixels []uint8
}
func (gp *glPicture) Bounds() pixel.Rect {
return gp.bounds
}
func (gp *glPicture) Texture() *glhf.Texture {
return gp.tex
}
func (gp *glPicture) Color(at pixel.Vec) pixel.RGBA {
if !gp.bounds.Contains(at) {
return pixel.Alpha(0)
}
bx, by, bw, _ := intBounds(gp.bounds)
x, y := int(at.X)-bx, int(at.Y)-by
off := y*bw + x
return pixel.RGBA{
R: float64(gp.pixels[off*4+0]) / 255,
G: float64(gp.pixels[off*4+1]) / 255,
B: float64(gp.pixels[off*4+2]) / 255,
A: float64(gp.pixels[off*4+3]) / 255,
}
}

View File

@ -60,9 +60,10 @@ func (gt *GLTriangles) Len() int {
// SetLen efficiently resizes GLTriangles to len.
//
// Time complexity is amortized O(1).
func (gt *GLTriangles) SetLen(len int) {
if len > gt.Len() {
needAppend := len - gt.Len()
func (gt *GLTriangles) SetLen(length int) {
switch {
case length > gt.Len():
needAppend := length - gt.Len()
for i := 0; i < needAppend; i++ {
gt.data = append(gt.data,
0, 0,
@ -71,10 +72,16 @@ func (gt *GLTriangles) SetLen(len int) {
0,
)
}
case length < gt.Len():
gt.data = gt.data[:length*gt.vs.Stride()]
default:
return
}
if len < gt.Len() {
gt.data = gt.data[:len*gt.vs.Stride()]
}
mainthread.CallNonBlock(func() {
gt.vs.Begin()
gt.vs.SetLen(length)
gt.vs.End()
})
}
// Slice returns a sub-Triangles of this GLTriangles in range [i, j).
@ -94,64 +101,56 @@ func (gt *GLTriangles) updateData(t pixel.Triangles) {
}
// TrianglesData short path
stride := gt.vs.Stride()
length := gt.Len()
if t, ok := t.(*pixel.TrianglesData); ok {
for i := 0; i < gt.Len(); i++ {
for i := 0; i < length; i++ {
var (
px, py = (*t)[i].Position.XY()
col = (*t)[i].Color
tx, ty = (*t)[i].Picture.XY()
in = (*t)[i].Intensity
)
gt.data[i*gt.vs.Stride()+0] = float32(px)
gt.data[i*gt.vs.Stride()+1] = float32(py)
gt.data[i*gt.vs.Stride()+2] = float32(col.R)
gt.data[i*gt.vs.Stride()+3] = float32(col.G)
gt.data[i*gt.vs.Stride()+4] = float32(col.B)
gt.data[i*gt.vs.Stride()+5] = float32(col.A)
gt.data[i*gt.vs.Stride()+6] = float32(tx)
gt.data[i*gt.vs.Stride()+7] = float32(ty)
gt.data[i*gt.vs.Stride()+8] = float32(in)
d := gt.data[i*stride : i*stride+9]
d[0] = float32(px)
d[1] = float32(py)
d[2] = float32(col.R)
d[3] = float32(col.G)
d[4] = float32(col.B)
d[5] = float32(col.A)
d[6] = float32(tx)
d[7] = float32(ty)
d[8] = float32(in)
}
return
}
if t, ok := t.(pixel.TrianglesPosition); ok {
for i := 0; i < gt.Len(); i++ {
for i := 0; i < length; i++ {
px, py := t.Position(i).XY()
gt.data[i*gt.vs.Stride()+0] = float32(px)
gt.data[i*gt.vs.Stride()+1] = float32(py)
gt.data[i*stride+0] = float32(px)
gt.data[i*stride+1] = float32(py)
}
}
if t, ok := t.(pixel.TrianglesColor); ok {
for i := 0; i < gt.Len(); i++ {
for i := 0; i < length; i++ {
col := t.Color(i)
gt.data[i*gt.vs.Stride()+2] = float32(col.R)
gt.data[i*gt.vs.Stride()+3] = float32(col.G)
gt.data[i*gt.vs.Stride()+4] = float32(col.B)
gt.data[i*gt.vs.Stride()+5] = float32(col.A)
gt.data[i*stride+2] = float32(col.R)
gt.data[i*stride+3] = float32(col.G)
gt.data[i*stride+4] = float32(col.B)
gt.data[i*stride+5] = float32(col.A)
}
}
if t, ok := t.(pixel.TrianglesPicture); ok {
for i := 0; i < gt.Len(); i++ {
for i := 0; i < length; i++ {
pic, intensity := t.Picture(i)
gt.data[i*gt.vs.Stride()+6] = float32(pic.X())
gt.data[i*gt.vs.Stride()+7] = float32(pic.Y())
gt.data[i*gt.vs.Stride()+8] = float32(intensity)
gt.data[i*stride+6] = float32(pic.X)
gt.data[i*stride+7] = float32(pic.Y)
gt.data[i*stride+8] = float32(intensity)
}
}
}
func (gt *GLTriangles) submitData() {
data := append([]float32{}, gt.data...) // avoid race condition
mainthread.CallNonBlock(func() {
gt.vs.Begin()
dataLen := len(data) / gt.vs.Stride()
gt.vs.SetLen(dataLen)
gt.vs.SetVertexData(gt.data)
gt.vs.End()
})
}
// Update copies vertex properties from the supplied Triangles into this GLTriangles.
//
// The two Triangles (gt and t) must be of the same len.
@ -160,7 +159,23 @@ func (gt *GLTriangles) Update(t pixel.Triangles) {
panic(fmt.Errorf("(%T).Update: invalid triangles len", gt))
}
gt.updateData(t)
gt.submitData()
// 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.
@ -178,12 +193,12 @@ func (gt *GLTriangles) Position(i int) pixel.Vec {
}
// Color returns the Color property of the i-th vertex.
func (gt *GLTriangles) Color(i int) pixel.NRGBA {
func (gt *GLTriangles) Color(i int) pixel.RGBA {
r := gt.data[i*gt.vs.Stride()+2]
g := gt.data[i*gt.vs.Stride()+3]
b := gt.data[i*gt.vs.Stride()+4]
a := gt.data[i*gt.vs.Stride()+5]
return pixel.NRGBA{
return pixel.RGBA{
R: float64(r),
G: float64(g),
B: float64(b),

View File

@ -21,6 +21,13 @@ func (w *Window) JustReleased(button Button) bool {
return !w.currInp.buttons[button] && w.prevInp.buttons[button]
}
// Repeated returns whether a repeat event has been triggered on button.
//
// Repeat event occurs repeatedly when a button is held down for some time.
func (w *Window) Repeated(button Button) bool {
return w.currInp.repeat[button]
}
// MousePosition returns the current mouse position in the Window's Bounds.
func (w *Window) MousePosition() pixel.Vec {
return w.currInp.mouse
@ -31,6 +38,11 @@ func (w *Window) MouseScroll() pixel.Vec {
return w.currInp.scroll
}
// Typed returns the text typed on the keyboard since the last call to Window.Update.
func (w *Window) Typed() string {
return w.currInp.typed
}
// Button is a keyboard or mouse button. Why distinguish?
type Button int
@ -322,9 +334,9 @@ func (w *Window) initInput() {
w.window.SetMouseButtonCallback(func(_ *glfw.Window, button glfw.MouseButton, action glfw.Action, mod glfw.ModifierKey) {
switch action {
case glfw.Press:
w.currInp.buttons[Button(button)] = true
w.tempInp.buttons[Button(button)] = true
case glfw.Release:
w.currInp.buttons[Button(button)] = false
w.tempInp.buttons[Button(button)] = false
}
})
@ -334,38 +346,43 @@ func (w *Window) initInput() {
}
switch action {
case glfw.Press:
w.currInp.buttons[Button(key)] = true
w.tempInp.buttons[Button(key)] = true
case glfw.Release:
w.currInp.buttons[Button(key)] = false
w.tempInp.buttons[Button(key)] = false
case glfw.Repeat:
w.tempInp.repeat[Button(key)] = true
}
})
w.window.SetCursorPosCallback(func(_ *glfw.Window, x, y float64) {
w.currInp.mouse = pixel.V(
x+w.bounds.Min.X(),
(w.bounds.H()-y)+w.bounds.Min.Y(),
w.tempInp.mouse = pixel.V(
x+w.bounds.Min.X,
(w.bounds.H()-y)+w.bounds.Min.Y,
)
})
w.window.SetScrollCallback(func(_ *glfw.Window, xoff, yoff float64) {
w.currInp.scroll += pixel.V(xoff, yoff)
w.tempInp.scroll.X += xoff
w.tempInp.scroll.Y += yoff
})
w.window.SetCharCallback(func(_ *glfw.Window, r rune) {
w.tempInp.typed += string(r)
})
})
}
func (w *Window) updateInput() {
// copy temp to prev
w.prevInp = w.tempInp
// zero current scroll (but keep what was added in callbacks outside of this function)
w.currInp.scroll -= w.tempInp.scroll
// get events (usually calls callbacks, but callbacks can be called outside too)
// UpdateInput polls window events. Call this function to poll window events
// without swapping buffers. Note that the Update method invokes UpdateInput.
func (w *Window) UpdateInput() {
mainthread.Call(func() {
glfw.PollEvents()
})
// cache current state to temp (so that if there are callbacks outside this function,
// everything works)
w.tempInp = w.currInp
w.prevInp = w.currInp
w.currInp = w.tempInp
w.tempInp.repeat = [KeyLast + 1]bool{}
w.tempInp.scroll = pixel.ZV
w.tempInp.typed = ""
}

View File

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

View File

@ -1,6 +1,7 @@
package pixelgl
import (
"image"
"image/color"
"runtime"
@ -20,28 +21,31 @@ type WindowConfig struct {
// Title at the top of the Window.
Title string
// Icon specifies the icon images available to be used by the window. This is usually
// displayed in the top bar of the window or in the task bar of the desktop environment.
//
// If passed one image, it will use that image, if passed an array of images those of or
// closest to the sizes desired by the system are selected. The desired image sizes varies
// depending on platform and system settings. The selected images will be rescaled as
// needed. Good sizes include 16x16, 32x32 and 48x48.
//
// Note: Setting this value doesn't have an effect on OSX. You'll need to set the icon when
// bundling your application for release.
Icon []pixel.Picture
// Bounds specify the bounds of the Window in pixels.
Bounds pixel.Rect
// If set to nil, a Window will be windowed. Otherwise it will be fullscreen on the
// If set to nil, the Window will be windowed. Otherwise it will be fullscreen on the
// specified Monitor.
Fullscreen *Monitor
Monitor *Monitor
// Whether a Window is resizable.
// Whether the Window is resizable.
Resizable bool
// If set to true, the Window will be initially invisible.
Hidden bool
// Undecorated Window ommits the borders and decorations (close button, etc.).
Undecorated bool
// If set to true, a Window will not get focused upon showing up.
Unfocused bool
// Whether a Window is maximized.
Maximized bool
// VSync (vertical synchronization) synchronizes Window's framerate with the framerate of
// the monitor.
VSync bool
@ -51,19 +55,22 @@ type WindowConfig struct {
type Window struct {
window *glfw.Window
bounds pixel.Rect
canvas *Canvas
vsync bool
bounds pixel.Rect
canvas *Canvas
vsync bool
cursorVisible bool
// need to save these to correctly restore a fullscreen window
restore struct {
xpos, ypos, width, height int
}
prevInp, tempInp, currInp struct {
prevInp, currInp, tempInp struct {
mouse pixel.Vec
buttons [KeyLast + 1]bool
repeat [KeyLast + 1]bool
scroll pixel.Vec
typed string
}
}
@ -78,7 +85,7 @@ func NewWindow(cfg WindowConfig) (*Window, error) {
false: glfw.False,
}
w := &Window{bounds: cfg.Bounds}
w := &Window{bounds: cfg.Bounds, cursorVisible: true}
err := mainthread.CallErr(func() error {
var err error
@ -89,10 +96,7 @@ func NewWindow(cfg WindowConfig) (*Window, error) {
glfw.WindowHint(glfw.OpenGLForwardCompatible, glfw.True)
glfw.WindowHint(glfw.Resizable, bool2int[cfg.Resizable])
glfw.WindowHint(glfw.Visible, bool2int[!cfg.Hidden])
glfw.WindowHint(glfw.Decorated, bool2int[!cfg.Undecorated])
glfw.WindowHint(glfw.Focused, bool2int[!cfg.Unfocused])
glfw.WindowHint(glfw.Maximized, bool2int[cfg.Maximized])
var share *glfw.Window
if currWin != nil {
@ -120,12 +124,23 @@ func NewWindow(cfg WindowConfig) (*Window, error) {
return nil, errors.Wrap(err, "creating window failed")
}
if len(cfg.Icon) > 0 {
imgs := make([]image.Image, len(cfg.Icon))
for i, icon := range cfg.Icon {
pic := pixel.PictureDataFromPicture(icon)
imgs[i] = pic.Image()
}
mainthread.Call(func() {
w.window.SetIcon(imgs)
})
}
w.SetVSync(cfg.VSync)
w.initInput()
w.SetMonitor(cfg.Fullscreen)
w.SetMonitor(cfg.Monitor)
w.canvas = NewCanvas(cfg.Bounds, false)
w.canvas = NewCanvas(cfg.Bounds)
w.Update()
runtime.SetFinalizer(w, (*Window).Destroy)
@ -145,10 +160,10 @@ func (w *Window) Update() {
mainthread.Call(func() {
_, _, oldW, oldH := intBounds(w.bounds)
newW, newH := w.window.GetSize()
w.bounds = w.bounds.ResizedMin(w.bounds.Size() + pixel.V(
w.bounds = w.bounds.ResizedMin(w.bounds.Size().Add(pixel.V(
float64(newW-oldW),
float64(newH-oldH),
))
)))
})
w.canvas.SetBounds(w.bounds)
@ -156,16 +171,17 @@ func (w *Window) Update() {
mainthread.Call(func() {
w.begin()
glhf.Bounds(0, 0, w.canvas.f.Texture().Width(), w.canvas.f.Texture().Height())
framebufferWidth, framebufferHeight := w.window.GetFramebufferSize()
glhf.Bounds(0, 0, framebufferWidth, framebufferHeight)
glhf.Clear(0, 0, 0, 0)
w.canvas.f.Begin()
w.canvas.f.Blit(
w.canvas.gf.Frame().Begin()
w.canvas.gf.Frame().Blit(
nil,
0, 0, w.canvas.f.Texture().Width(), w.canvas.f.Texture().Height(),
0, 0, w.canvas.f.Texture().Width(), w.canvas.f.Texture().Height(),
0, 0, w.canvas.Texture().Width(), w.canvas.Texture().Height(),
0, 0, framebufferWidth, framebufferHeight,
)
w.canvas.f.End()
w.canvas.gf.Frame().End()
if w.vsync {
glfw.SwapInterval(1)
@ -176,7 +192,7 @@ func (w *Window) Update() {
w.end()
})
w.updateInput()
w.UpdateInput()
}
// SetClosed sets the closed flag of the Window.
@ -217,25 +233,34 @@ 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.
func (w *Window) Bounds() pixel.Rect {
return w.bounds
}
// Show makes the Window visible if it was hidden.
func (w *Window) Show() {
mainthread.Call(func() {
w.window.Show()
})
}
// Hide hides the Window if it was visible.
func (w *Window) Hide() {
mainthread.Call(func() {
w.window.Hide()
})
}
func (w *Window) setFullscreen(monitor *Monitor) {
mainthread.Call(func() {
w.restore.xpos, w.restore.ypos = w.window.GetPos()
@ -282,11 +307,6 @@ func (w *Window) SetMonitor(monitor *Monitor) {
}
}
// IsFullscreen returns true if the Window is in fullscreen mode.
func (w *Window) IsFullscreen() bool {
return w.Monitor() != nil
}
// Monitor returns a monitor the Window is fullscreen on. If the Window is not fullscreen, this
// function returns nil.
func (w *Window) Monitor() *Monitor {
@ -302,13 +322,6 @@ func (w *Window) Monitor() *Monitor {
}
}
// Focus brings the Window to the front and sets input focus.
func (w *Window) Focus() {
mainthread.Call(func() {
w.window.Focus()
})
}
// Focused returns true if the Window has input focus.
func (w *Window) Focused() bool {
var focused bool
@ -318,20 +331,6 @@ func (w *Window) Focused() bool {
return focused
}
// Maximize puts the Window to the maximized state.
func (w *Window) Maximize() {
mainthread.Call(func() {
w.window.Maximize()
})
}
// Restore restores the Window from the maximized state.
func (w *Window) Restore() {
mainthread.Call(func() {
w.window.Restore()
})
}
// SetVSync sets whether the Window's Update should synchronize with the monitor refresh rate.
func (w *Window) SetVSync(vsync bool) {
w.vsync = vsync
@ -342,6 +341,23 @@ func (w *Window) VSync() bool {
return w.vsync
}
// SetCursorVisible sets the visibility of the mouse cursor inside the Window client area.
func (w *Window) SetCursorVisible(visible bool) {
w.cursorVisible = visible
mainthread.Call(func() {
if visible {
w.window.SetInputMode(glfw.CursorMode, glfw.CursorNormal)
} else {
w.window.SetInputMode(glfw.CursorMode, glfw.CursorHidden)
}
})
}
// CursorVisible returns the visibility status of the mouse cursor.
func (w *Window) CursorVisible() bool {
return w.cursorVisible
}
// Note: must be called inside the main thread.
func (w *Window) begin() {
if currWin != w {
@ -366,7 +382,7 @@ func (w *Window) MakeTriangles(t pixel.Triangles) pixel.TargetTriangles {
// MakePicture generates a specialized copy of the supplied Picture that will draw onto this Window.
//
// Window support PictureColor.
// Window supports PictureColor.
func (w *Window) MakePicture(p pixel.Picture) pixel.TargetPicture {
return w.canvas.MakePicture(p)
}
@ -381,6 +397,12 @@ func (w *Window) SetColorMask(c color.Color) {
w.canvas.SetColorMask(c)
}
// SetComposeMethod sets a Porter-Duff composition method to be used in the following draws onto
// this Window.
func (w *Window) SetComposeMethod(cmp pixel.ComposeMethod) {
w.canvas.SetComposeMethod(cmp)
}
// SetSmooth sets whether the stretched Pictures drawn onto this Window should be drawn smooth or
// pixely.
func (w *Window) SetSmooth(smooth bool) {
@ -397,3 +419,8 @@ func (w *Window) Smooth() bool {
func (w *Window) Clear(c color.Color) {
w.canvas.Clear(c)
}
// Color returns the color of the pixel over the given position inside the Window.
func (w *Window) Color(at pixel.Vec) pixel.RGBA {
return w.canvas.Color(at)
}

118
sprite.go
View File

@ -2,43 +2,46 @@ package pixel
import "image/color"
// Sprite is a drawable Picture. It's anchored by the center of it's Picture.
// Sprite is a drawable frame of a Picture. It's anchored by the center of it's Picture's frame.
//
// To achieve different anchoring, transformations and color masking, use SetMatrix and SetColorMask
// methods.
// Frame specifies a rectangular portion of the Picture that will be drawn. For example, this
// creates a Sprite that draws the whole Picture:
//
// sprite := pixel.NewSprite(pic, pic.Bounds())
//
// Note, that Sprite caches the results of MakePicture from Targets it's drawn to for each Picture
// it's set to. What it means is that using a Sprite with an unbounded number of Pictures leads to a
// memory leak, since Sprite caches them and never forgets. In such a situation, create a new Sprite
// for each Picture.
type Sprite struct {
tri *TrianglesData
bounds Rect
d Drawer
tri *TrianglesData
frame Rect
d Drawer
matrix Matrix
mask NRGBA
mask RGBA
}
// NewSprite creates a Sprite from the supplied Picture.
func NewSprite(pic Picture) *Sprite {
// NewSprite creates a Sprite from the supplied frame of a Picture.
func NewSprite(pic Picture, frame Rect) *Sprite {
tri := MakeTrianglesData(6)
s := &Sprite{
tri: tri,
d: Drawer{Triangles: tri},
}
s.matrix = IM
s.mask = NRGBA{1, 1, 1, 1}
s.SetPicture(pic)
s.mask = Alpha(1)
s.Set(pic, frame)
return s
}
// SetPicture changes the Sprite's Picture. The new Picture may have a different size, everything
// works.
func (s *Sprite) SetPicture(pic Picture) {
// Set sets a new frame of a Picture for this Sprite.
func (s *Sprite) Set(pic Picture, frame Rect) {
s.d.Picture = pic
if s.bounds == pic.Bounds() {
return
if frame != s.frame {
s.frame = frame
s.calcData()
}
s.bounds = pic.Bounds()
s.calcData()
}
// Picture returns the current Sprite's Picture.
@ -46,58 +49,61 @@ func (s *Sprite) Picture() Picture {
return s.d.Picture
}
// SetMatrix sets a Matrix that this Sprite will be transformed by. This overrides any previously
// set Matrix.
// Frame returns the current Sprite's frame.
func (s *Sprite) Frame() Rect {
return s.frame
}
// Draw draws the Sprite onto the provided Target. The Sprite will be transformed by the given Matrix.
//
// Note, that this has nothing to do with BasicTarget's SetMatrix method. This only affects this
// Sprite and is usable with any Target.
func (s *Sprite) SetMatrix(matrix Matrix) {
s.matrix = matrix
s.calcData()
// This method is equivalent to calling DrawColorMask with nil color mask.
func (s *Sprite) Draw(t Target, matrix Matrix) {
s.DrawColorMask(t, matrix, nil)
}
// Matrix returns the currently set Matrix.
func (s *Sprite) Matrix() Matrix {
return s.matrix
}
// SetColorMask sets a color that this Sprite will be multiplied by. This overrides any previously
// set color mask.
// DrawColorMask draws the Sprite onto the provided Target. The Sprite will be transformed by the
// given Matrix and all of it's color will be multiplied by the given mask.
//
// Note, that this has nothing to do with BasicTarget's SetColorMask method. This only affects this
// Sprite and is usable with any Target.
func (s *Sprite) SetColorMask(mask color.Color) {
s.mask = ToNRGBA(mask)
s.calcData()
}
// If the mask is nil, a fully opaque white mask will be used, which causes no effect.
func (s *Sprite) DrawColorMask(t Target, matrix Matrix, mask color.Color) {
dirty := false
if matrix != s.matrix {
s.matrix = matrix
dirty = true
}
if mask == nil {
mask = Alpha(1)
}
rgba := ToRGBA(mask)
if rgba != s.mask {
s.mask = rgba
dirty = true
}
// ColorMask returns the currently set color mask.
func (s *Sprite) ColorMask() NRGBA {
return s.mask
}
if dirty {
s.calcData()
}
// Draw draws the Sprite onto the provided Target.
func (s *Sprite) Draw(t Target) {
s.d.Draw(t)
}
func (s *Sprite) calcData() {
var (
center = s.bounds.Center()
horizontal = X(s.bounds.W() / 2)
vertical = Y(s.bounds.H() / 2)
center = s.frame.Center()
horizontal = V(s.frame.W()/2, 0)
vertical = V(0, s.frame.H()/2)
)
(*s.tri)[0].Position = -horizontal - vertical
(*s.tri)[1].Position = +horizontal - vertical
(*s.tri)[2].Position = +horizontal + vertical
(*s.tri)[3].Position = -horizontal - vertical
(*s.tri)[4].Position = +horizontal + vertical
(*s.tri)[5].Position = -horizontal + vertical
(*s.tri)[0].Position = Vec{}.Sub(horizontal).Sub(vertical)
(*s.tri)[1].Position = Vec{}.Add(horizontal).Sub(vertical)
(*s.tri)[2].Position = Vec{}.Add(horizontal).Add(vertical)
(*s.tri)[3].Position = Vec{}.Sub(horizontal).Sub(vertical)
(*s.tri)[4].Position = Vec{}.Add(horizontal).Add(vertical)
(*s.tri)[5].Position = Vec{}.Sub(horizontal).Add(vertical)
for i := range *s.tri {
(*s.tri)[i].Color = s.mask
(*s.tri)[i].Picture = center + (*s.tri)[i].Position
(*s.tri)[i].Picture = center.Add((*s.tri)[i].Position)
(*s.tri)[i].Intensity = 1
}

248
text/atlas.go Normal file
View File

@ -0,0 +1,248 @@
package text
import (
"fmt"
"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 {
dr, mask, maskp, _, _ := face.Glyph(fg.dot, r)
draw.Draw(atlasImg, dr, mask, maskp, draw.Src)
}
bounds := pixel.R(
i2f(fixedBounds.Min.X),
i2f(fixedBounds.Min.Y),
i2f(fixedBounds.Max.X),
i2f(fixedBounds.Max.Y),
)
mapping := make(map[rune]Glyph)
for r, fg := range fixedMapping {
mapping[r] = Glyph{
Dot: pixel.V(
i2f(fg.dot.X),
bounds.Max.Y-(i2f(fg.dot.Y)-bounds.Min.Y),
),
Frame: pixel.R(
i2f(fg.frame.Min.X),
bounds.Max.Y-(i2f(fg.frame.Min.Y)-bounds.Min.Y),
i2f(fg.frame.Max.X),
bounds.Max.Y-(i2f(fg.frame.Max.Y)-bounds.Min.Y),
).Norm(),
Advance: i2f(fg.advance),
}
}
return &Atlas{
face: face,
pic: pixel.PictureDataFromImage(atlasImg),
mapping: mapping,
ascent: i2f(face.Metrics().Ascent),
descent: i2f(face.Metrics().Descent),
lineHeight: i2f(face.Metrics().Height),
}
}
// Picture returns the underlying Picture containing an arrangement of all the glyphs contained
// within the Atlas.
func (a *Atlas) Picture() pixel.Picture {
return a.pic
}
// Contains reports wheter r in contained within the Atlas.
func (a *Atlas) Contains(r rune) bool {
_, ok := a.mapping[r]
return ok
}
// Glyph returns the description of r within the Atlas.
func (a *Atlas) Glyph(r rune) Glyph {
return a.mapping[r]
}
// Kern returns the kerning distance between runes r0 and r1. Positive distance means that the
// glyphs should be further apart.
func (a *Atlas) Kern(r0, r1 rune) float64 {
return i2f(a.face.Kern(r0, r1))
}
// Ascent returns the distance from the top of the line to the baseline.
func (a *Atlas) Ascent() float64 {
return a.ascent
}
// Descent returns the distance from the baseline to the bottom of the line.
func (a *Atlas) Descent() float64 {
return a.descent
}
// LineHeight returns the recommended vertical distance between two lines of text.
func (a *Atlas) LineHeight() float64 {
return a.lineHeight
}
// DrawRune returns parameters necessary for drawing a rune glyph.
//
// Rect is a rectangle where the glyph should be positioned. Frame is the glyph frame inside the
// Atlas's Picture. NewDot is the new position of the dot.
func (a *Atlas) DrawRune(prevR, r rune, dot pixel.Vec) (rect, frame, bounds pixel.Rect, newDot pixel.Vec) {
if !a.Contains(r) {
r = unicode.ReplacementChar
}
if !a.Contains(unicode.ReplacementChar) {
return pixel.Rect{}, pixel.Rect{}, pixel.Rect{}, dot
}
if !a.Contains(prevR) {
prevR = unicode.ReplacementChar
}
if prevR >= 0 {
dot.X += a.Kern(prevR, r)
}
glyph := a.Glyph(r)
rect = glyph.Frame.Moved(dot.Sub(glyph.Dot))
bounds = rect
if bounds.W()*bounds.H() != 0 {
bounds = pixel.R(
bounds.Min.X,
dot.Y-a.Descent(),
bounds.Max.X,
dot.Y+a.Ascent(),
)
}
dot.X += glyph.Advance
return rect, glyph.Frame, bounds, dot
}
type fixedGlyph struct {
dot fixed.Point26_6
frame fixed.Rectangle26_6
advance fixed.Int26_6
}
// makeSquareMapping finds an optimal glyph arrangement of the given runes, so that their common
// bounding box is as square as possible.
func makeSquareMapping(face font.Face, runes []rune, padding fixed.Int26_6) (map[rune]fixedGlyph, fixed.Rectangle26_6) {
width := sort.Search(int(fixed.I(1024*1024)), func(i int) bool {
width := fixed.Int26_6(i)
_, bounds := makeMapping(face, runes, padding, width)
return bounds.Max.X-bounds.Min.X >= bounds.Max.Y-bounds.Min.Y
})
return makeMapping(face, runes, padding, fixed.Int26_6(width))
}
// makeMapping arranges glyphs of the given runes into rows in such a way, that no glyph is located
// fully to the right of the specified width. Specifically, it places glyphs in a row one by one and
// once it reaches the specified width, it starts a new row.
func makeMapping(face font.Face, runes []rune, padding, width fixed.Int26_6) (map[rune]fixedGlyph, fixed.Rectangle26_6) {
mapping := make(map[rune]fixedGlyph)
bounds := fixed.Rectangle26_6{}
dot := fixed.P(0, 0)
for _, r := range runes {
b, advance, ok := face.GlyphBounds(r)
if !ok {
fmt.Println(r)
continue
}
// this is important for drawing, artifacts arise otherwise
frame := fixed.Rectangle26_6{
Min: fixed.P(b.Min.X.Floor(), b.Min.Y.Floor()),
Max: fixed.P(b.Max.X.Ceil(), b.Max.Y.Ceil()),
}
dot.X -= frame.Min.X
frame = frame.Add(dot)
mapping[r] = fixedGlyph{
dot: dot,
frame: frame,
advance: advance,
}
bounds = bounds.Union(frame)
dot.X = frame.Max.X
// padding + align to integer
dot.X += padding
dot.X = fixed.I(dot.X.Ceil())
// width exceeded, new row
if frame.Max.X >= width {
dot.X = 0
dot.Y += face.Metrics().Ascent + face.Metrics().Descent
// padding + align to integer
dot.Y += padding
dot.Y = fixed.I(dot.Y.Ceil())
}
}
return mapping, bounds
}
func i2f(i fixed.Int26_6) float64 {
return float64(i) / (1 << 6)
}

24
text/atlas_test.go Normal file
View File

@ -0,0 +1,24 @@
package text_test
import (
"testing"
"github.com/faiface/pixel/text"
)
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)
}
}
}
}

2
text/doc.go Normal file
View File

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

347
text/text.go Normal file
View File

@ -0,0 +1,347 @@
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
}
// New creates a new Text capable of drawing runes contained in the provided Atlas. Orig and Dot
// will be initially set to orig.
//
// Here we create a Text capable of drawing ASCII characters using the Go Regular font.
// ttf, err := truetype.Parse(goregular.TTF)
// if err != nil {
// panic(err)
// }
// face := truetype.NewFace(ttf, &truetype.Options{
// Size: 14,
// })
// txt := text.New(orig, text.NewAtlas(face, text.ASCII))
func New(orig pixel.Vec, atlas *Atlas) *Text {
txt := &Text{
Orig: orig,
Dot: orig,
Color: pixel.Alpha(1),
LineHeight: atlas.LineHeight(),
TabWidth: atlas.Glyph(' ').Advance * 4,
atlas: atlas,
mat: pixel.IM,
col: pixel.Alpha(1),
}
txt.glyph.SetLen(6)
for i := range txt.glyph {
txt.glyph[i].Color = pixel.Alpha(1)
txt.glyph[i].Intensity = 1
}
txt.transD.Picture = txt.atlas.pic
txt.transD.Triangles = &txt.trans
txt.Clear()
return txt
}
// Atlas returns the underlying Text's Atlas containing all of the pre-drawn glyphs. The Atlas is
// also useful for getting values such as the recommended line height.
func (txt *Text) Atlas() *Atlas {
return txt.atlas
}
// Bounds returns the bounding box of the text currently written to the Text excluding whitespace.
//
// If the Text is empty, a zero rectangle is returned.
func (txt *Text) Bounds() pixel.Rect {
return txt.bounds
}
// BoundsOf returns the bounding box of s if it was to be written to the Text right now.
func (txt *Text) BoundsOf(s string) pixel.Rect {
dot := txt.Dot
prevR := txt.prevR
bounds := pixel.Rect{}
for _, r := range s {
var control bool
dot, control = txt.controlRune(r, dot)
if control {
continue
}
var b pixel.Rect
_, _, b, dot = txt.Atlas().DrawRune(prevR, r, dot)
if bounds.W()*bounds.H() == 0 {
bounds = b
} else {
bounds = bounds.Union(b)
}
prevR = r
}
return bounds
}
// Clear removes all written text from the Text. 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
}
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)
}
}
}

87
text/text_test.go Normal file
View File

@ -0,0 +1,87 @@
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)
}