Compare commits

...

649 Commits
v0.7 ... master

Author SHA1 Message Date
Allen Ray 0a251bc08b
Merge pull request #319 from duysqubix/master
Update README.md
2023-10-09 09:06:08 -04:00
Anten Skrabec d3c72ae210
Merge pull request #320 from duysqubix/patch-1
Update README.md
2023-10-09 01:05:26 -06:00
duysqubix 20c056b950
Update README.md 2023-10-08 19:51:40 -05:00
duysqubix 9ad345b415
Update README.md
updated link to point to gopxl/pixel repo
2023-10-07 07:07:29 -05:00
Allen Ray 174abb179a
Merge pull request #318 from duysqubix/notice 2023-10-06 10:11:23 -04:00
duysqubix 4c172faca7 updated readme with notice 2023-10-04 11:48:58 -05:00
Allen Ray d899a6bbed
Merge pull request #312 from faiface/dependabot/go_modules/golang.org/x/image-0.5.0
Bump golang.org/x/image from 0.0.0-20210628002857-a66eb6448b8d to 0.5.0
2023-03-10 09:11:16 -05:00
Allen Ray a9abe2ebb7
Merge pull request #313 from marc921/add-vec-squared-len
feat(Vec): add squared len method
2023-03-10 09:11:03 -05:00
Marc Brun ab135a4965 feat(Vec): add squared len method 2023-03-09 22:43:37 +01:00
dependabot[bot] 08fc8acdfa
Bump golang.org/x/image from 0.0.0-20210628002857-a66eb6448b8d to 0.5.0
Bumps [golang.org/x/image](https://github.com/golang/image) from 0.0.0-20210628002857-a66eb6448b8d to 0.5.0.
- [Release notes](https://github.com/golang/image/releases)
- [Commits](https://github.com/golang/image/commits/v0.5.0)

---
updated-dependencies:
- dependency-name: golang.org/x/image
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-03-07 03:27:23 +00:00
Anten Skrabec 069cc4e011
Merge pull request #308 from miluChen/patch-2
fix typo in interface.go
2022-06-27 03:22:05 -06:00
miluChen 2c3a9a03bd
fix typo 2022-06-26 16:42:25 -04:00
miluChen cb394bc26f
fix typo in interface.go 2022-06-26 10:45:46 -04:00
Allen Ray 8acf4e5195
Merge pull request #301 from cebarks/mod-update
update dependencies (testify and go-gl/glfw)
2021-10-13 21:17:34 -04:00
Anten Skrabec ccc8deb923 update dependencies (testify and go-gl/glfw) 2021-10-13 11:40:21 -06:00
Allen Ray e448943963
Merge pull request #299 from Benjyclay/fix/failing-builds
Fix failing builds
2021-10-13 09:45:17 -04:00
Ben 8ce328cb89 Update dependencies, including pixel/glhf to fix failing builds
Closes #229
Closes #295
2021-10-13 14:43:44 +01:00
Allen Ray 389317124e
Merge pull request #297 from zergon321/add-sprite-uncached
Added an opportunity to disable caching for sprites
2021-10-04 18:50:28 -04:00
zergon321 33024feabf Cached parameter added. 2021-10-01 18:31:18 +00:00
Allen Ray d119f130f6
Added PixelUI to related repositories. 2021-09-02 12:17:40 -04:00
Allen Ray b61f150701
Merge pull request #285 from cebarks/msaa
Add Multisample Anti-aliasing support
2021-08-31 08:36:26 -04:00
Allen Ray f079cc25fe
Update window.go 2021-08-31 08:35:57 -04:00
Allen Ray 9e8e09f1d7
Merge pull request #284 from dusk125/clipTris
Adding new triangles type: TrianglesClipped
2021-08-31 08:34:40 -04:00
Allen Ray 96863238e9
Merge pull request #291 from GGCristo/master
[README] Add "libXxf86vm-devel" dependency for CentOS/Fedora-like Linux
2021-08-31 08:34:28 -04:00
GGCristo 649411a23c [README] Add "libXxf86vm-devel" dependency for CentOS/Fedora-like Linux
distributions
2021-08-31 01:27:50 +01:00
Allen Ray d940a42454
Merge pull request #290 from dusk125/master
Fixing #277: Rect.Intersect and Rect.Intersects now have consistent behavior
2021-08-18 20:06:47 -04:00
Anten Skrabec 160e665d51 update CHANGELOG.md 2021-08-17 23:27:19 -06:00
Anten Skrabec 1d936eae7b add comment about power of two setting for msaa samples 2021-08-17 23:23:32 -06:00
Allen Ray 15972155d3 Fixing #277 2021-08-16 23:51:13 -04:00
Allen Ray 4964768d4e
Merge pull request #287 from cebarks/geometry-refactor
Break geometry.go & geometry_test.go into multiple smaller files
2021-08-16 23:30:59 -04:00
Anten Skrabec d5761bda94 make testing window invisible 2021-08-16 19:12:38 -06:00
Anten Skrabec 552871e324 update from master 2021-08-16 19:12:25 -06:00
Anten Skrabec 4a8b06746f actually add the files -.- 2021-08-16 19:10:10 -06:00
Anten Skrabec 7fe32e1553 break out geometry into their own files 2021-08-16 19:10:10 -06:00
Allen Ray 6c717101a7
Merge branch 'master' into clipTris 2021-08-16 09:55:30 -04:00
Allen Ray 881bfeda91
Merge pull request #288 from sudoless/master
Apply gofumpt and style changes + Optimization in GLShader.Update()
2021-08-16 09:41:10 -04:00
Allen Ray b4d312046c
Merge pull request #289 from cebarks/spriteplus
Add 2 related repos
2021-08-16 08:55:59 -04:00
Anten Skrabec f6e2f3a8d8 add check for valid msaa values in NewWindow() 2021-08-15 23:31:06 -06:00
Anten Skrabec 4ec3c64512 add spriteplus and pixelutils to README.md under "Related repos" 2021-08-15 23:12:03 -06:00
Alexandru-Paul Copil bb0a0f485e Optimize *GLShader.Update() by allocating array before for loop 2021-07-09 10:44:48 +03:00
Alexandru-Paul Copil aae9927b58 Apply gofumpt and basic styling changes 2021-07-09 10:43:10 +03:00
Allen Ray 9307280c91
Merge pull request #283 from dusk125/master 2021-06-22 07:49:51 -04:00
Anten Skrabec 7615c055cf move gl.Enable call to inside gl context in window creation 2021-06-21 14:13:43 -06:00
Anten Skrabec 80b107c039 add multisampling 2021-06-21 13:51:26 -06:00
Allen Ray 98f3e740e6
Merge pull request #272 from jstewart7/master
Added input event tracking to prevent missed inputs
2021-06-21 15:07:48 -04:00
Allen Ray 798c6c2a48 Adding new triangles type: TrianglesClipped 2021-06-21 10:29:53 -04:00
Allen Ray 063b8952f9 Adding Clipboard get/set functions to window 2021-06-21 08:42:27 -04:00
Alex R. Delp 842ae8d470 Add some unit tests to a few geometry functions. 2021-01-29 08:41:37 -08:00
Jacob Stewart b31c294d4d Added input event tracking to prevent missed inputs 2021-01-28 20:13:28 -05:00
Alex R. Delp 75e7b4120b
Update CHANGELOG.md 2021-01-26 17:42:25 -08:00
Alex R. Delp 0e9102e224
Update CHANGELOG.md 2021-01-26 17:41:59 -08:00
Alex R. Delp 2101dc89e3
Merge pull request #252 from roipoussiere/anchor
Add AnchorPos struct and functions
2021-01-26 17:41:24 -08:00
Alex R. Delp eae5a2e96b
Update CHANGELOG.md 2021-01-26 17:31:32 -08:00
Alex R. Delp 24ffdadbf8
Merge pull request #258 from crthpl/master
Clipboard Support
2021-01-26 17:30:48 -08:00
Alex R. Delp f931f69d6e
Update CHANGELOG.md 2020-11-21 20:24:00 -08:00
Alex R. Delp 6e34aae429
Merge pull request #246 from dusk125/tri_shader
#244 GLTriangle's fragment shader is used when rendered by the Canvas.
2020-11-21 20:22:37 -08:00
Alex R. Delp cef3a7e7a6
Update CHANGELOG.md 2020-11-21 19:58:32 -08:00
Alex R. Delp fd754b5263
Merge pull request #262 from snargleplax/rangeTargets
Use slice for range in Drawer.Dirty(), to improve performance
2020-11-21 19:56:25 -08:00
Luke Meyers ef6cafa0cb Use slice for range in Drawer.Dirty(), to improve performance 2020-10-12 22:31:00 -07:00
Theoo3 7a67b10cec undo go.mod change 2020-09-12 11:02:02 -07:00
Theoo3 7027267cbf Clipboard 2020-09-12 10:56:39 -07:00
Alex R. Delp ba7fa122b2
Update CHANGELOG.md 2020-08-22 09:50:41 -07:00
Alex R. Delp 65ff420b25
Merge pull request #253 from roipoussiere/fix-newatlas
tests and fixes faiface#123, SIGSEGV on text.NewAtlas if glyph absent
2020-08-22 09:49:59 -07:00
Alex R. Delp fd73dd5216
Update CHANGELOG.md 2020-08-22 09:43:47 -07:00
Alex R. Delp 775e23efac
Update CHANGELOG.md 2020-08-22 09:39:04 -07:00
Alex R. Delp ae6c93ba96
Update CHANGELOG.md 2020-08-22 09:37:06 -07:00
Nathanael Jourdane 3b599e70ec Fix text anchor position when txt.Bounds().W() != txt.Dot.X-txt.Orig.X 2020-08-22 15:31:07 +02:00
Nathanael Jourdane 7d92e04e63 tests and fixes faiface#123, SIGSEGV on text.NewAtlas if glyph absent 2020-08-13 11:57:30 +02:00
Nathanael Jourdane c9681cec8a Add Anchor.Opposite() func 2020-08-12 17:31:05 +02:00
Nathanael Jourdane 2342984744 bugfix: inverse all anchors 2020-08-12 17:24:29 +02:00
Nathanaël a8b7779fbe Add Text.AlignedTo() 2020-08-11 22:16:16 +02:00
Nathanaël Jourdane 7a106fa572 Makes Anchor a type Vec instead of a struct 2020-08-11 11:33:05 +02:00
Nathanaël e66e94b943 func AlignedTo is used for Rect instead of Matrix 2020-08-11 09:17:51 +02:00
Nathanaël d6663ba0a0 Add Matrix.Aligned() 2020-08-11 08:44:39 +02:00
Nathanaël Jourdane 50e1d8c96b Add Rect.AnchorTo() 2020-08-10 17:25:09 +02:00
Nathanaël Jourdane 2c79e7325a Add AnchorPos struct and functions 2020-08-10 15:57:56 +02:00
Allen Ray 865e930023
Merge branch 'master' into tri_shader 2020-06-28 20:03:04 -04:00
Alex R. Delp 88b1e1c2d3
Merge pull request #245 from dusk125/master
Adding Clipping rectangle support in GLTriangles
2020-06-28 16:54:03 -07:00
Allen Ray 850dd62a98 Updating changelog 2020-06-28 12:58:13 -04:00
Allen Ray 926463aa6f #244 GLTriangle's fragment shader is used when rendered by the Canvas.
Allows for custom fragment shaders to be used after canvas.MakeTriangles.
2020-06-21 23:27:28 -04:00
Allen Ray 543819d066 Forgot to add import in PR 2020-06-21 23:09:08 -04:00
Allen Ray c3e5e4fdb2 Moved triangle clipping to the proper location 2020-06-21 22:54:23 -04:00
Allen Ray e79a8c37e6 Adding support for clipping rectangles in GLTriangles 2020-06-21 22:51:08 -04:00
Alex R. Delp d65a63e962
Merge pull request #243 from andrebq/master
expose glfw.WaitEvent using Window.WaitInput
2020-06-17 12:21:18 -07:00
André 6cef9cf779 rename method and handle negative timeouts 2020-06-16 21:11:41 +02:00
André 1f914181d8 add entry to CHANGELOG 2020-06-14 18:28:14 +02:00
André a61d321615 expose glfw.WaitEvent using Window.WaitInput 2020-06-14 17:52:03 +02:00
Alex R. Delp 585f7c1f14
Merge pull request #237 from fgrosse/more-window-hints
Add window hints to created maximized or invisible windows
2020-05-24 00:01:25 -07:00
Friedrich Große 607e0a11c0 Update changelog 2020-05-21 12:00:13 +02:00
Friedrich Große 506588f40b Add window hints to created maximized or invisible windows 2020-05-21 11:56:00 +02:00
Alex R. Delp 42582c7f8e
Update CHANGELOG.md 2020-05-18 18:30:40 -07:00
Alex R. Delp 625c4c393d
Merge pull request #233 from zergon321/gamepad-support
Gamepad API added.
2020-05-18 18:30:17 -07:00
Alex R. Delp 5568202e30
Merge pull request #235 from fgrosse/window-initial-position
Support setting an initial window position
2020-05-16 09:09:35 -07:00
Friedrich Große 70eff126da
Merge branch 'master' into window-initial-position 2020-05-11 18:04:58 +02:00
Alex R. Delp 5d34c08754
Update CHANGELOG.md 2020-05-10 14:14:21 -07:00
Alex R. Delp c35e0abd1c
Update CHANGELOG.md 2020-05-10 14:09:06 -07:00
Alex R. Delp a3f51715d9
Merge pull request #234 from fgrosse/transparency
Support glfw.TransparentFramebuffer window hint
2020-05-10 14:05:59 -07:00
Alex R. Delp b5e4674b45
Merge branch 'master' into transparency 2020-05-10 14:04:36 -07:00
Alex R. Delp 9a9f831114
Merge pull request #236 from bcvery1/master
Fixing line intersection for lines through origin
2020-05-10 13:49:13 -07:00
Ben Cragg 6b71245324 Updated change log 2020-05-10 14:34:10 +01:00
Ben Cragg 5254b4eb62 Fixing 0,0 being legit closest point 2020-05-10 14:25:05 +01:00
Ben Cragg 76534cb8d0 Removing commented out tests 2020-05-10 14:24:07 +01:00
Ben Cragg 8792d4436c Adding vert and horz tests 2020-05-10 14:13:46 +01:00
Alex R. Delp d2d1f03c32 Add a clarifying comment in imdraw push 2020-05-09 12:46:09 -07:00
Alex R. Delp 6a71f07d12
Update README.md 2020-05-09 12:43:38 -07:00
Friedrich Große 79a3bd7c63 Update changelog 2020-05-09 21:17:52 +02:00
Friedrich Große b7d5e41486 Support setting an initial window position 2020-05-09 21:07:17 +02:00
Friedrich Große 5e3b82eeab Support glfw.TransparentFramebuffer window hint 2020-05-09 20:59:40 +02:00
Alex R. Delp d7884ac96a
Update CONTRIBUTING.md 2020-05-09 10:15:57 -07:00
Alex R. Delp b40ab4939f
Update CHANGELOG.md 2020-05-09 10:13:05 -07:00
Alex R. Delp 6b4a6905de
Update CHANGELOG.md 2020-05-09 10:12:32 -07:00
zergon321 0a8feedd8a Gamepad API added. 2020-05-09 17:17:24 +03:00
Alex R. Delp 914e25233f
Update CHANGELOG.md 2020-05-08 19:21:00 -07:00
Alex R. Delp 197e604220
Merge pull request #211 from zergon321/add-glfw-v33
pixelgl has been ported to GLFW 3.3.
2020-05-08 19:18:31 -07:00
Alex R. Delp e350f9fef1
Merge pull request #231 from delp/master
Update CHANGELOG.md Unreleased Section
2020-05-08 19:12:26 -07:00
Alex R. Delp d8a3c92052
Update CHANGELOG.md 2020-05-06 17:14:10 -07:00
Alex R. Delp d35b25061e
Merge pull request #214 from jared-nishikawa/master
Added DisableCursor
2020-05-06 17:02:32 -07:00
Alex R. Delp b9c32bc51f
Merge pull request #230 from fgrosse/fix-changelog
Fix CHANGELOG.md
2020-05-06 16:52:19 -07:00
Friedrich Große 0110a3c88a Fix CHANGELOG.md 2020-05-03 20:32:09 +02:00
Alex R. Delp 3be4e4cb84
Update README.md 2020-05-02 13:48:40 -07:00
Alex R. Delp bd68a95818
Update CHANGELOG.md 2020-05-02 12:54:03 -07:00
Alex R. Delp d181dae9ba
Update CHANGELOG.md 2020-05-02 12:53:29 -07:00
Alex R. Delp 42b1229128
Merge pull request #219 from snargleplax/master
Expose pixelgl.Window.SwapBuffers seperately from Window.Update
2020-05-02 12:32:44 -07:00
Alex R. Delp 37f588845a
Update CHANGELOG.md 2020-05-02 12:19:32 -07:00
zergon321 8909d199d1 Revert "Gamepad API added."
This reverts commit 484c1775b0.
2020-04-20 21:46:15 +03:00
zergon321 09ea5585a0 Revert "Magic number removed."
This reverts commit 7e3a88ef63.
2020-04-20 21:43:49 +03:00
Alex R. Delp 8012403b8f
Update README.md 2020-04-20 10:48:12 -07:00
Alex R. Delp dbdea628da
Update README.md 2020-04-20 10:31:12 -07:00
Alex R. Delp fc543042cc
Update README.md 2020-04-20 10:23:01 -07:00
Alex R. Delp ed63c18bb4
Update README.md 2020-04-20 10:13:31 -07:00
Alex R. Delp 9cc1cddcf2
Update README.md 2020-04-18 15:41:05 -07:00
Michal Štrba 73ce868aa9
looking for a new maintainer 2020-04-14 19:05:59 +02:00
Luke Meyers effd43bc56 Expose pixelgl.Window.SwapBuffers
Allow buffers to be swapped without polling input, offering decoupling symmetrical with that provided by UpdateInput.
2020-02-09 20:59:59 -08:00
Jared fc29ad7886 changed name to SetCursorDisabled 2020-02-07 16:22:39 -07:00
Jared 93a24aa6df Added DisableCursor 2020-01-08 22:24:48 -07:00
nightghost 7e3a88ef63 Magic number removed. 2020-01-05 21:47:11 +03:00
nightghost 484c1775b0 Gamepad API added. 2020-01-05 21:37:24 +03:00
nightghost a3aba6004d pixelgl has been ported to GLFW 3.5. 2019-12-31 01:59:20 +03:00
Michal Štrba e51d4a6676
fix some typos 2019-11-06 00:50:48 +01:00
Michal Štrba 87299dc29b
Merge pull request #205 from faiface/revert-203-comments-errors
Revert "Fixed a couple of errors in methods comments"
2019-11-06 00:46:39 +01:00
Michal Štrba 0d44d2cc9b
Revert "Fixed a couple of errors in methods comments" 2019-11-06 00:46:26 +01:00
Michal Štrba c4f9faea97
Merge pull request #203 from svera/comments-errors
Fixed a couple of errors in methods comments
2019-11-06 00:43:48 +01:00
Michal Štrba 18972c2818
Merge branch 'master' into comments-errors 2019-11-06 00:43:12 +01:00
Michal Štrba 853405f61c
Merge pull request #204 from Tskken/master
Merge adding Rect.Intersects(Rect)bool function
2019-11-06 00:39:51 +01:00
Tskken 8d0c306de9 typo fix
fixed typo in comments.
2019-11-05 16:32:55 -07:00
Tskken 837a4efa80 Fixed benchmark error
Fixed error for forgeting to change benchark function from IsIntersect to Intersects.
2019-11-05 16:16:29 -07:00
Tskken 706becb764 Update adding new Rect.Intersects Method
This addes a new Rect.Intersects method which is around 5x faster then Rect.Intersect when used for basic collision checks.
2019-11-05 16:14:36 -07:00
Dylan Blanchard bb82a87aaf
Merge pull request #1 from faiface/master
Merge faiface/pixel version to Tskken/pixel
2019-11-05 01:04:33 -07:00
Sergio Vera f24d465808 Fixed a couple of errors in methods comments 2019-10-24 10:29:11 +02:00
Michal Štrba aeef94f200
Merge pull request #195 from fgrosse/release-notes
Start to maintain a change log
2019-06-30 00:14:12 +02:00
Friedrich Große 2aa6d2d212 Update wording about tracked changes in README.md 2019-06-29 14:39:10 +02:00
Friedrich Große 8a9191c283 Start to maintain a change log 2019-06-23 11:33:59 +02:00
Michal Štrba 07754109cb
Merge pull request #186 from fgrosse/extra-window-hints
Adding AutoIconify and Floating GLFW window hints
2019-06-12 22:27:57 +02:00
Michal Štrba 16d04f467f
Merge pull request #190 from fgrosse/fix-modules
Fix Go modules
2019-05-30 16:53:47 +02:00
Friedrich Große a5c3bec928 Prevent default TravisCI install action 2019-05-30 15:23:12 +02:00
Friedrich Große d513ffd192 Force TravisCI to run tests using dependencies from the go.mod file 2019-05-30 14:56:19 +02:00
Friedrich Große c5c5127c8d Fix small typo in README.md 2019-05-30 14:52:32 +02:00
Friedrich Große 109cb03fae Update TravisCI to fail if dependencies are missing in go.mod 2019-05-30 14:47:19 +02:00
Friedrich Große 23171aea6c Add missing github.com/golang/freetype dependency 2019-05-30 14:36:11 +02:00
Friedrich Große dd5e0d8b09 Adding NoIconify and AlwaysOnTop GLFW window hints 2019-05-30 14:29:43 +02:00
Michal Štrba 3c6f33365b
Merge pull request #189 from udhos/master
Support for Modules.
2019-05-25 14:50:33 +02:00
udhos 38283d0e55 Support for Modules. 2019-05-24 17:37:22 -03:00
Michal Štrba daa8205b8c
Merge pull request #188 from Tskken/master
Added ZR for zero rect
2019-05-24 20:33:23 +02:00
Tsukinai ba6fc7db8a docs changes
changed wording to match zero-vector returns in other functions.
2019-05-24 12:25:35 -06:00
Tsukinai 285676ca17 gofmt and fixed docs
Fixed doc refrince to R(0,0,0,0) in Rect.Intersect() to be a ZR and for it to return a ZR.

Also ran gofmt...
2019-05-24 12:20:00 -06:00
Tsukinai b18647916f Added ZR for zero rect
Added the ZR for rectangle for both utility and consistensy with pixel.ZV
2019-05-24 12:00:11 -06:00
Michal Štrba de3617a259
Merge pull request #184 from fgrosse/fix-contributing-guidelines
Fixed link to examples repo in CONTRIBUTING.md
2019-05-19 22:08:02 +02:00
Friedrich Große 1f96c1e995 Fixed link to examples repo in CONTRIBUTING.md 2019-05-19 18:44:51 +02:00
Michal Štrba a68a4e38b4
Merge pull request #183 from bcvery1/TriangleEfficiency
Added TriangleData benchmarks and improvements
2019-04-25 14:17:09 +02:00
Ben Cragg 9e6e6197dd Added benchmark for MakeTrianglesData 2019-04-24 11:17:48 +01:00
Ben Cragg 159df28dcc Reverted changes on setlen 2019-04-24 10:32:35 +01:00
Ben Cragg d48ef44bdc Added TriangleData benchmarks and improvements 2019-04-24 10:11:20 +01:00
Michal Štrba 9aca3bfe7a
Merge pull request #182 from bcvery1/fixVertexRace
Fix vertex race
2019-04-16 10:27:08 +02:00
Ben Cragg 2cf81cd4df Setting dist 2019-04-16 09:01:36 +01:00
Ben Cragg 3aad3268cd Making xvfb a service 2019-04-16 08:47:42 +01:00
Ben Cragg 9d5c7afe12 Adding test for drawing sprites 2019-04-15 11:25:30 +01:00
Ben Cragg 85b18b567a prevented concurrent race condition 2019-04-15 11:20:40 +01:00
Michal Štrba e8ba24ea40
Merge pull request #181 from bcvery1/fixSpritePositioning
Setting position in correct order
2019-04-15 10:07:17 +02:00
Ben Cragg d601bc65e4 Setting position in correct order 2019-04-15 08:46:29 +01:00
Michal Štrba 2cb87efb8c
Merge pull request #176 from bcvery1/master
Avoiding floating point rounding errors
2019-04-14 22:50:57 +02:00
Michal Štrba ccf8e9dcdc
Merge pull request #177 from Immueggpain/patch-1
why 2 loops and assign twice? one is enough
2019-04-14 22:48:56 +02:00
Immueggpain 95ac5e1e2a
why 2 loops and assign twice? one is enough 2019-04-13 23:47:02 +08:00
Ben Cragg 96e0a8f3bf Added test cases 2019-04-10 12:47:21 +01:00
Ben Cragg f698bae1df Added floating point round error correction 2019-04-10 12:47:07 +01:00
Michal Štrba a6c8b92517
Merge pull request #174 from bcvery1/addLine
Add line geometry
2019-04-04 17:49:16 +02:00
Ben Cragg 9eb2834a10 fixed index issue 2019-04-04 16:20:21 +01:00
Ben Cragg 0092d6a577 implementing pre 1.8 sortslice solution 2019-04-04 16:09:25 +01:00
Ben Cragg b8bb00a161 Setting order of returned points on Circle 2019-04-04 15:49:40 +01:00
Ben Cragg 29b1220ec3 Setting order of returned points on Rect 2019-04-04 15:48:27 +01:00
Ben Cragg 364a7a84ae Prevented test results order mattering 2019-04-04 15:31:34 +01:00
Ben Cragg bcda85acd2 Adding fullstop at end of func comment 2019-04-04 15:13:47 +01:00
Ben Cragg 966150a856 Removing backticks 2019-04-04 15:13:24 +01:00
Ben Cragg f3377bb16f Added circle intersection points 2019-04-04 11:53:48 +01:00
Ben Cragg 4795a92b41 fixed line intersect and added rect intersection points 2019-04-03 16:58:30 +01:00
Ben Cragg 83c62a0313 fixing line intersect function 2019-04-03 16:21:57 +01:00
Ben Cragg c3e69c4f35 Tidying up function comments 2019-04-03 15:26:38 +01:00
Ben Cragg 352785e1b8 Supporting pre go1.10 2019-04-03 12:37:33 +01:00
Ben Cragg e6392a228d Making len more sensible 2019-04-03 12:22:21 +01:00
Ben Cragg 3592de858c Clarified comment 2019-04-03 12:19:30 +01:00
Ben Cragg a1d36f8c7e Clarified comment 2019-04-03 12:16:27 +01:00
Ben Cragg 98d5b9b417 Adding vertical/horizontal edge cases 2019-04-03 12:12:27 +01:00
Ben Cragg e5ff236d71 Removed debug lines 2019-04-03 12:03:58 +01:00
Ben Cragg faf6558294 wip line tests 2019-04-03 12:03:07 +01:00
Ben Cragg e99ac56daa wip line tests 2019-04-03 11:52:42 +01:00
Ben Cragg aa7ef59ecd wip line tests 2019-04-03 11:47:36 +01:00
Ben Cragg eb3d7e0787 wip line tests 2019-04-03 11:09:14 +01:00
Ben Cragg 11e2012ef5 WIP line tests 2019-04-03 08:23:34 +01:00
Ben Cragg 8cd352b7a3 WIP line tests 2019-04-03 08:13:33 +01:00
Ben Cragg 997f23dfb5 WIP line tests 2019-04-02 17:01:18 +01:00
Ben Cragg 04c3ef72a3 WIP line tests 2019-04-02 16:27:54 +01:00
Ben Cragg 237d77596f WIP line tests 2019-04-02 13:48:43 +01:00
Ben Cragg c7eac06499 WIP line tests 2019-04-02 13:46:50 +01:00
Ben Cragg 55b87ca5b1 WIP line tests 2019-04-02 13:33:41 +01:00
Ben Cragg 2478da5d12 wip adding line tests 2019-04-02 08:49:45 +01:00
Ben Cragg cca37c750e wip adding line tests 2019-04-01 16:01:08 +01:00
Ben Cragg 1a275b5929 Filled rect tests 2019-04-01 15:44:14 +01:00
Ben Cragg 9d07d69429 Fixed so corners are provided in anticlockwise pattern 2019-04-01 15:42:20 +01:00
Ben Cragg 128ec4d4c0 Added function to create a Line 2019-04-01 15:37:23 +01:00
Ben Cragg 1eac5d8dc2 Added test templates for new rect tests 2019-04-01 15:33:31 +01:00
Ben Cragg a5ea71811e Added test templates 2019-04-01 15:31:34 +01:00
Ben Cragg d3912a6486 Added Line struct and methods 2019-04-01 15:31:20 +01:00
Michal Štrba 0db4348595
Merge pull request #172 from bcvery1/addTilePixReadme
Add related repository TilePix to README
2019-03-26 18:35:12 +01:00
Ben Cragg b2c8bff6eb Adding related packages 2019-03-26 15:42:59 +00:00
Ben Cragg 95148365d0 Reverting back to origin 2019-03-26 15:42:12 +00:00
Ben Cragg 50ef216e79 Removing wip files 2019-03-26 15:41:32 +00:00
Ben Cragg 09db6d8b38 Merge branch 'master' of github.com:bcvery1/pixel 2019-03-26 15:32:42 +00:00
Ben Cragg 86de5123c0 Added related libraries 2019-03-26 15:25:41 +00:00
Michal Štrba 470791a59d
Merge pull request #170 from Tobaloidee/master
logo upload & readme update
2019-03-20 16:46:00 +01:00
tobaloidee 0475b1ddb4
Update README.md 2019-03-20 11:16:49 +08:00
tobaloidee c9328c9260
Delete logo 2019-03-20 11:12:07 +08:00
tobaloidee c7836cb3ad
logo upload 2019-03-20 11:10:30 +08:00
tobaloidee 3d2f083f70
Create logo 2019-03-20 11:09:54 +08:00
faiface 771e2e92e2 Merge branch 'master' of https://github.com/faiface/pixel 2019-03-18 22:56:18 +01:00
faiface 09b50d2296 add Gizmo to examples part of README 2019-03-18 22:55:57 +01:00
Michal Štrba 66670d9905
Merge pull request #164 from CodeLingoBot/rewrite
Fix function comments based on best practices from Effective Go
2019-03-12 21:59:53 +01:00
CodeLingo Bot 489b03138f Fix function comments based on best practices from Effective Go
Signed-off-by: CodeLingo Bot <bot@codelingo.io>
2019-03-11 00:45:01 +00:00
Michal Štrba 3cc0ec3d30
Merge pull request #162 from bcvery1/fixCircleIntersect
Fix circle intersect
2019-02-22 17:13:57 +01:00
Ben Cragg b3246f6234 Updated tests 2019-02-18 12:16:16 +00:00
Ben Cragg 680d16c17b Corrected returned Vectors for corner overlaps 2019-02-18 12:03:54 +00:00
Michal Štrba 8563c28493
Merge pull request #157 from bcvery1/addcircle
Added circle geometry - small feature/improvement
2019-02-14 16:00:46 +01:00
Ben Cragg 160bcb9960 Merge branch 'addcircle' of github.com:bcvery1/pixel into addcircle 2019-02-14 14:25:06 +00:00
Ben Cragg 6f1e3bbbf8 More intersection tests 2019-02-14 14:23:50 +00:00
Ben Cragg cc9f03d393 corrected function preambles 2019-02-14 14:23:50 +00:00
Ben Cragg b61602cdf4 fixed area tests 2019-02-14 14:23:50 +00:00
Ben Cragg f21c48599f Made naming consistent 2019-02-14 14:23:50 +00:00
Ben Cragg ab70533793 wip 2019-02-14 14:23:50 +00:00
Ben Cragg 0a0c8ff110 corrected area formula 2019-02-14 14:23:50 +00:00
Ben Cragg 5abf2d29a6 swapped radius and center order 2019-02-14 14:23:50 +00:00
Ben Cragg 4d30a8fe49 fixed intersect function 2019-02-14 14:23:50 +00:00
Ben Cragg ecda96a36f removed unneeded Min call 2019-02-14 14:23:50 +00:00
Ben Cragg 520459bc6d made test param generation more consistant 2019-02-14 14:23:50 +00:00
Ben Cragg e225e9a1ef corrected comment 2019-02-14 14:23:50 +00:00
Ben Cragg 13171c409b using Lerp 2019-02-14 14:23:50 +00:00
Ben Cragg 2e275ab0d5 Made test clearer 2019-02-14 14:23:50 +00:00
Ben Cragg 23168ee324 remove local var 2019-02-14 14:23:50 +00:00
Ben Cragg 44d030b9ca normalising before getting bigger/smaller 2019-02-14 14:23:50 +00:00
Ben Cragg b06a31baa3 Removed Diameter function 2019-02-14 14:23:50 +00:00
Ben Cragg 4c6d061455 added rect-circle intersection functions 2019-02-14 14:23:50 +00:00
Ben Cragg 620c551f70 moved rect test struct 2019-02-14 14:23:50 +00:00
Ben Cragg 16722d55e1 not exporting circle size comparisons 2019-02-14 14:22:52 +00:00
Ben Cragg ee24eeb67d fixed circle.Intersect 2019-02-14 14:22:52 +00:00
Ben Cragg 5072f34b91 Added Circle geometry and tests 2019-02-14 14:22:52 +00:00
Ben Cragg d4530ca9fe More intersection tests 2019-02-14 14:18:14 +00:00
Ben Cragg 3b63b7eff9 corrected function preambles 2019-02-14 14:15:26 +00:00
Ben Cragg 91c16c34da fixed area tests 2019-02-14 14:12:54 +00:00
Ben Cragg e8a3621140 Made naming consistent 2019-02-14 14:12:05 +00:00
Michal Štrba 730d33a341
Merge pull request #160 from jacekolszak/master
Fix Matrix.Unproject problem with rotated matrix
2019-02-14 15:06:00 +01:00
Jacek Olszak 42cc2a0376 #159 Use map instead of array for named Matrices 2019-02-13 15:25:12 +01:00
Jacek Olszak 742d1226d8 #159 Make namedMatrices an array instead of slice 2019-02-13 15:13:04 +01:00
Jacek Olszak 0a054f35fd #159 Name matrices in unit test to avoid confusion with test cases 2019-02-13 15:06:50 +01:00
Jacek Olszak 392fe90b11 #159 Test whether Matrix.Unprejected(Matrix.Projected(vertex)) == vertex 2019-02-13 14:39:47 +01:00
Jacek Olszak 2e0da4f44a #159 Another reduction of Simplify Matrix.Unproject formulas 2019-02-12 21:06:37 +01:00
Jacek Olszak b0ed22e0ec #159 Simplify Matrix.Unproject formulas 2019-02-12 21:00:05 +01:00
Jacek Olszak 0aec9fead0 #159 Format code in TestMatrix_Unproject 2019-02-12 18:53:38 +01:00
Jacek Olszak 1e19db5b25 #159 Add test case when matrix determinant is not 1 2019-02-12 18:40:15 +01:00
Jacek Olszak a0713598da #159 Add test case for rotated and moved matrix 2019-02-12 16:26:19 +01:00
Jacek Olszak 3b366d1edd #159 Use more precise delta for floats comparision 2019-02-12 15:24:17 +01:00
Jacek Olszak cabaee680e #159 Fix unproject for rotated matrix 2019-02-12 14:57:03 +01:00
Ben Cragg 8650692efa wip 2019-01-31 08:22:59 +00:00
Ben Cragg ba4f417559 corrected area formula 2019-01-30 08:38:51 +00:00
Ben Cragg ecd686769c swapped radius and center order 2019-01-30 08:37:27 +00:00
Ben Cragg 2cce50832d fixed intersect function 2019-01-29 12:31:59 +00:00
Ben Cragg c70d7575ce removed unneeded Min call 2019-01-29 12:27:38 +00:00
Ben Cragg 4eba5e37ae made test param generation more consistant 2019-01-29 12:18:35 +00:00
Ben Cragg 0cddb56114 corrected comment 2019-01-29 11:47:49 +00:00
Ben Cragg 4b6cf201f7 using Lerp 2019-01-29 11:45:00 +00:00
Ben Cragg cff8697c97 Made test clearer 2019-01-29 11:41:10 +00:00
Ben Cragg d0ceea6849 remove local var 2019-01-29 11:34:59 +00:00
Ben Cragg 56cd321ab8 normalising before getting bigger/smaller 2019-01-29 11:33:12 +00:00
Ben Cragg 8efed1de5c Removed Diameter function 2019-01-29 11:23:55 +00:00
Ben Cragg 9a32601c6b added rect-circle intersection functions 2019-01-29 11:20:21 +00:00
Ben Cragg fdc068e855 renamed test function to match convention 2019-01-29 09:40:44 +00:00
Ben Cragg 9b8d4c7461 moved rect test struct 2019-01-29 09:39:44 +00:00
Ben Cragg a56f7fa422 not exporting circle size comparisons 2019-01-29 09:38:24 +00:00
Ben Cragg 0d5ba92fbe fixed circle.Intersect 2019-01-29 09:33:20 +00:00
faiface 20b05d9ec2 add Vulkan support + strike-through GPU effects from the roadmap 2019-01-28 21:54:37 +01:00
Ben Cragg d102169151 Added Circle geometry and tests 2019-01-28 09:00:24 +00:00
Michal Štrba f1ad821f1a
Merge pull request #156 from Humpheh/master
Correctly index joysticks in internal state (fixes #155)
2019-01-22 22:53:08 +01:00
Humphrey Shotton 3be890ea80 Use JoystickLast instead of Joystick16 2019-01-22 21:48:24 +00:00
Humphrey Shotton a46f6f6cf4 Correctly index joysticks in internal state 2019-01-22 21:37:15 +00:00
Michal Štrba 9e0f11abbb
Merge pull request #154 from StephenMichaelis/master
Mouse/Cursor Operations
2019-01-21 23:18:30 +01:00
Stephen Michaelis 8d20deea05 update MouseEntered to MouseInsideWindow 2019-01-21 17:13:51 -05:00
Stephen Michaelis e7a625f5d2 updated Floor method 2019-01-20 10:00:35 -05:00
Stephen Michaelis d3f6331240 Adding a pixel.Vec Floor method to geometry.go 2019-01-20 09:56:30 -05:00
Stephen Michaelis ee54171247 Revert to 4c817d7 2019-01-19 14:39:29 -05:00
Stephen Michaelis 2b79131104 added go mod files 2019-01-19 13:59:38 -05:00
Stephen Michaelis 4c817d7c20 adding mouse operations 2019-01-19 10:24:01 -05:00
Ben Cragg 3bed5a8449 WIP adding tests 2019-01-18 11:37:17 +00:00
Michal Štrba a1bbfa3020
Merge pull request #152 from Humpheh/master
Added joystick input
2019-01-10 20:56:20 +01:00
Humphrey Shotton 026878de07 Mark API as experimental 2019-01-10 19:25:55 +00:00
Humphrey Shotton 52b4384240 Removed JoystickLast 2019-01-09 21:07:46 +00:00
Humphrey Shotton 676509d856 Added joystick input 2018-12-30 18:42:02 +00:00
Michal Štrba bdd9349b37
Merge pull request #147 from Lallassu/Lallassu-patch-1
Added position as out variable from vertex shader.
2018-12-11 23:55:13 +01:00
Magnus c18b8f1c29
Added position as out variable from vertex shader.
Adding the position out form vertex shader makes us skip computation of position in the fragment shader based on gl_FragCoord.
2018-12-11 09:24:17 +01:00
Michal Štrba 7b509e1d7d
Merge pull request #145 from gonutz/master
Add No-Brain Jogging to examples
2018-11-23 11:34:30 +01:00
Lars 4a0b2f5107 Replace startfield example with No-Brain Jogging 2018-11-23 11:23:38 +01:00
lars 17e407f0c1 Add No-Brain Jogging to examples 2018-11-21 23:44:46 +00:00
faiface 0c2d627cfa README: update the list of tutorials 2018-10-10 13:48:53 +02:00
Michal Štrba 749271768b
Merge pull request #142 from thegtproject/glslstandardization
standardized glsl variable naming
2018-10-07 20:37:30 +02:00
Brandon dc545043c1 lowercased SetUniform arguments 2018-10-07 12:28:20 -06:00
Brandon f4edf628b1 standardized glsl variable naming 2018-10-07 08:33:37 -06:00
faiface 8f9e91e617 fix .travis.yml 2018-10-06 23:34:21 +02:00
Michal Štrba d7dcc8d167
Merge pull request #141 from thegtproject/shaders
implemented custom fragment shader support
2018-10-06 23:22:31 +02:00
Brandon e9dfd22ffa pr 141 review #3 changes, see https://github.com/faiface/pixel/pull/141#pullrequestreview-162261776 2018-10-06 10:27:47 -06:00
Brandon fa10844351 pr 141 review #2 changes, see https://github.com/faiface/pixel/pull/141#pullrequestreview-162259357 2018-10-06 10:01:05 -06:00
Brandon a25a444cbf pr 141 review #1 changes, see https://github.com/faiface/pixel/pull/141#pullrequestreview-161208458 2018-10-03 13:02:10 -06:00
Brandon 45642083a2 renamed file shader.go to glshader.go 2018-10-03 09:37:10 -06:00
Brandon 55d3a6ab00 implemented custom fragment shader support 2018-10-02 08:59:37 -06:00
faiface 87980dd647 fix merge conflicts 2018-10-01 18:26:22 +02:00
faiface 88243fbfa4 README: fix links to examples 2018-09-07 17:55:20 +02:00
faiface c1ead8e0a0 move examples directory to faiface/pixel-examples repo (~90MB of data) 2018-09-07 17:51:10 +02:00
Michal Štrba 578343e58c Merge pull request #125 from jwangsadinata/add-scenes-guide
add a guide for making scenes
2018-09-05 22:14:50 +02:00
Michal Štrba ebabf74263 Merge pull request #136 from NaniteFactory/patch-2
fix typo for matrix
2018-09-05 22:13:28 +02:00
NaniteFactory abfdf18fb6 fix typo for matrix 2018-09-05 12:53:25 +09:00
Michal Štrba 49ee5d6e77 Merge pull request #134 from NaniteFactory/master
CI support for example/amidakuji
2018-09-03 20:34:24 +02:00
NaniteFactory 74ef0e3256 CI support for example/amidakuji 2018-09-03 21:00:36 +09:00
Michal Štrba a8270e58e0 Merge pull request #114 from dbriemann/feature/video-modes
Adds video mode support and a community example for its usage.
2018-05-04 22:58:59 +02:00
David Linus Briemann 446247e369 adjusts VideoMode and associated function to pull request change requests. 2018-05-04 18:03:25 +02:00
David Linus Briemann 2a1ab90ad5 adds a community example for the usage of video modes 2018-05-03 20:52:42 +02:00
faiface 9817425914 Merge branch 'master' of https://github.com/faiface/pixel 2018-03-29 16:18:46 +02:00
faiface 2e38a287de fix mutating *image.RGBA when converting PictureDataFromImage 2018-03-29 16:18:37 +02:00
Michal Štrba 1ea9235b6f Merge pull request #107 from mewpull/master
readme: remove tracking from Gitter URL
2018-03-26 13:58:02 +02:00
mewmew f2f9fd315d readme: remove tracking from Gitter URL 2018-03-26 10:37:03 +02:00
faiface d6a898e82f Merge branch 'master' of https://github.com/faiface/pixel 2018-02-27 09:53:25 +01:00
faiface 3cf59b1044 fix #100 2018-02-26 18:38:12 +01:00
Michal Štrba a7bcddc921 Merge pull request #97 from mewpull/master
contributing: remove tracking from link to Gitter
2018-02-19 12:21:47 +01:00
mewmew db33885652 contributing: remove tracking from link to Gitter 2018-02-19 09:30:03 +01:00
faiface baabf3adac update README.md 2018-02-18 20:58:41 +01:00
faiface bb1d0b2d10 add CONTRIBUTING.md 2018-02-18 20:53:23 +01:00
Michal Štrba 4498635b26 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 c871238e9f Change package to text_test 2018-01-19 23:09:48 +01:00
Peter Hellberg b157c890d6 Remove new7x13Atlas function 2018-01-19 23:08:06 +01:00
Peter Hellberg 8d8d5cc9db 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 ad738cddf8 Add Atlas7x13, Clear now sets Dot field to Orig
Remove unused f2i function
2018-01-19 22:20:54 +01:00
Michal Štrba 2dff742a8b Merge pull request #87 from svera/parallax-scroll
Parallax scroll
2018-01-12 12:59:03 +01:00
Michal Štrba d4ae092923 Merge pull request #86 from svera/update-image
Update image
2018-01-12 12:57:11 +01:00
Michal Štrba 9436339e0b Merge pull request #85 from svera/parallax-scroll
Parallax scroll (fixes #75)
2018-01-12 12:44:11 +01:00
Michal Štrba 42e7d07767 Merge pull request #82 from svera/procedural-terrain-1d
Procedural terrain 1D
2018-01-12 12:42:22 +01:00
faiface 295498ab3e add some community examples screenshots to README 2018-01-11 22:55:26 +01:00
Michal Štrba cb5a468ba2 Merge pull request #79 from mewpull/poll
pixelgl: export UpdateInput
2018-01-04 19:32:21 +01:00
mewmew 98be0b43c8 pixelgl: clarify UpdateInput docs 2018-01-04 19:23:53 +01:00
mewmew 6c69d9b06b pixelgl: export UpdateInput 2018-01-01 21:04:56 +01:00
Michal Štrba a151e31929 Merge pull request #70 from PlainSight/improve-geo-tests
Improve geometry tests
2017-11-20 02:17:18 +01:00
Michal Štrba 08308279ca 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 2a32545253 minor adjustments with how tests are named and run 2017-11-20 13:51:15 +13:00
ALex Ogier eedeb85b14 refactor and add test cases 2017-11-20 13:51:15 +13:00
ALex Ogier 3c78161418 add GetPos function to window 2017-11-20 13:37:22 +13:00
faiface 5e2a944a48 README: add Windows guide mention 2017-11-15 21:38:24 +01:00
faiface e554b1e3c1 dirty fix #58 (panic on minimizing windows) 2017-11-08 22:28:32 +01:00
Michal Štrba cc4c905b49 Merge pull request #61 from Schobers/master
Added SetPos to pixelgl.Window
2017-10-27 20:48:00 +02:00
Sander Schobers 48e37828c8 Added SetPos to pixelgl.Window 2017-10-27 20:35:45 +02:00
faiface 78a2373870 README: mention OpenGL version 2017-10-19 21:34:09 +02:00
faiface 721a10ef10 add Vec.Project 2017-10-15 19:50:41 +02:00
faiface c87741300d add pixel.Unit 2017-10-15 19:43:12 +02:00
faiface 62cefc262e add pixel.Clamp 2017-10-15 19:42:13 +02:00
faiface ce09bb1114 fix Matrix.Chained (wrong order of composition) 2017-09-04 00:40:12 +02:00
faiface 7c5e5588e2 fix Vec.Normal to rotate by pi/2, not -pi/2 2017-08-29 20:23:32 +02:00
Michal Štrba 977f5710f5 Merge pull request #48 from PlainSight/fix-rectangle-resize
Fix Rectangle "Resized" function
2017-07-29 01:11:02 +02:00
Alex e3d056d36f add more test transforms, now includes: center, origin, min, max and middle of side 2017-07-29 11:05:46 +12:00
Alex 3e3a9aaa48 fix operation order for rectangle resize function, add tests 2017-07-29 00:40:06 +12:00
faiface 96df742331 remove audio package in favor of faiface/beep 2017-07-14 18:34:28 +02:00
faiface 4d94c04476 audio: wav: fix Stream for non 2 chans 2 bytes configurations 2017-07-14 02:41:01 +02:00
faiface 9a6fbad6ce audio: wav: more precise Position and Seek 2017-07-14 02:22:33 +02:00
faiface 7624d11cfc audio: wav: fix Stream (move position, forgot to do it previously) 2017-07-14 02:10:12 +02:00
faiface 35308e4ebf audio: wav: simplify Stream 2017-07-14 02:09:36 +02:00
faiface 52b52e4eca merge master 2017-07-13 01:24:50 +02:00
faiface d325ce3198 audio: add Gain effect 2017-07-13 00:29:53 +02:00
faiface 8ad3d94d7e audio: minor change 2017-07-13 00:21:16 +02:00
faiface 1db65503e3 update .travis.yml for ALSA 2017-07-12 22:54:36 +02:00
faiface 217fbdb30c audio: minor change 2017-07-12 22:44:15 +02:00
faiface 3a40ec4435 audio: add Ctrl doc comments 2017-07-12 22:43:55 +02:00
faiface 82afa289f4 audio: wav: add doc comments 2017-07-12 21:30:35 +02:00
faiface 502347d987 audio: wav: simplify code, more DRY, encapsulate decoder type, only export Decode function 2017-07-12 19:37:20 +02:00
faiface 55c94b6cc5 audio: make Take propagate errors 2017-07-12 02:58:17 +02:00
faiface 81dbcb5bd1 audio: wav: update streamer to satisfy StreamSeekCloser 2017-07-12 00:32:37 +02:00
faiface 78674d35b9 add StreamSeeker, StreamCloser and StreamSeekCloser interfaces 2017-07-12 00:05:42 +02:00
faiface d1953900cc audio: add Err method to Streamer 2017-07-11 23:42:57 +02:00
faiface 35a24edbda audio: wav: fix seeking to include the header 2017-07-11 20:12:04 +02:00
faiface ce53548e84 audio: rename Ctrl.Duration -> Ctrl.Position 2017-07-11 20:01:15 +02:00
faiface 72b34b78c4 audio: wav: add Seek and Position 2017-07-11 20:00:29 +02:00
faiface 81de53181c audio: wav: rename decoder.go -> streamer.go 2017-07-11 18:31:33 +02:00
faiface 3851f37503 minor change 2017-07-11 17:36:00 +02:00
faiface a748a0cdce audio: add initial wav streamer implementation 2017-07-11 17:34:17 +02:00
faiface eb429bea68 audio: speaker: fix byte encoding 2017-07-11 16:42:16 +02:00
faiface 7ecd50bd45 audio: speaker: remove unnecessary channel confirmation 2017-07-09 16:34:49 +02:00
faiface c6b821bd67 audio: speaker: allow to call Init more than once 2017-07-08 22:01:59 +02:00
faiface f97894ad37 audio: add Mixer doc 2017-07-08 14:54:01 +02:00
faiface 95b9f23076 audio: optimize Mixer.Stream and Mix(...).Stream (remove unnecessary mix buffer) 2017-07-08 14:43:24 +02:00
faiface 0d3d384378 audio: add Mixer.Len 2017-07-08 14:28:34 +02:00
faiface 555a1cb8e4 audio: speaker: use Mixer to play sounds simultaneously 2017-07-07 18:46:40 +02:00
faiface a2ba6d090d audio: add Mixer 2017-07-07 18:46:26 +02:00
faiface 4b70585f1a audio: add Ctrl decorator 2017-07-07 18:46:20 +02:00
faiface ee258c1d13 audio: add Mix compositor 2017-07-07 16:11:48 +02:00
faiface 8e06ab198e minor change 2017-07-07 14:08:25 +02:00
faiface 5c00d43168 speaker: remove Update, add Lock and Unlock 2017-07-07 13:20:55 +02:00
Michal Štrba 033fca94e0 Merge pull request #45 from aerth/audio-dep
fix audio dependency
2017-07-07 00:16:32 +02:00
aerth 0eb5bf4c06 fix audio dependency 2017-07-06 15:08:41 -07:00
faiface 238400db27 fix edge sample value (-1 and +1) overflow 2017-07-06 23:36:18 +02:00
faiface 7858a55088 actually fix the race condition 2017-07-06 22:34:22 +02:00
faiface 140b050545 minor change 2017-07-06 22:26:47 +02:00
faiface 6c2bffed37 speaker: fix race condition 2017-07-06 22:26:20 +02:00
faiface bb2ffdec2c speaker: improve concurrency, only lock when and what necessary 2017-07-06 22:11:03 +02:00
faiface 8d9485af7e reimplement speaker 2017-07-06 21:44:34 +02:00
faiface cc1e4c9381 Merge branch 'audio' of https://github.com/faiface/pixel into audio 2017-07-06 20:52:12 +02:00
Michal Štrba 7518c708d3 Merge pull request #44 from alistanis/audio
initial incomplete speaker implementation
2017-07-06 20:52:02 +02:00
Christopher Cooper 00cab859c1 addresses some review comments 2017-07-06 11:49:35 -04:00
Christopher Cooper f542f75202 fix bounds check 2017-07-05 18:15:53 -04:00
Christopher Cooper 6dd3a9a643 mremove prints 2017-07-05 18:11:26 -04:00
Christopher Cooper 538d42f9a6 add initial speaker implementation 2017-07-05 18:10:03 -04:00
Christopher Cooper e7a7b16be2 Merge pull request #1 from faiface/audio
Audio
2017-07-05 18:05:50 -04:00
Michal Štrba c91a8fd6e9 Merge pull request #43 from aerth/audio
add libasound2-dev travis dependency
2017-07-05 22:04:44 +02:00
aerth 777c7f1717 add libasound2-dev dependency 2017-07-05 12:24:26 -07:00
faiface e64d97efb1 fix Rect.Intersect 2017-07-05 20:36:25 +02:00
faiface da52aa25ec clarify Rect.Intersect doc 2017-07-05 19:58:09 +02:00
faiface 69d4a37e5e add Rect.Intersect 2017-07-05 19:54:30 +02:00
faiface 2b9b4d07c4 add Rect.Area 2017-07-05 19:54:18 +02:00
faiface d41492df91 add Rect.Intersect 2017-07-05 19:51:54 +02:00
faiface 72a4dd03e7 add Rect.Area 2017-07-05 19:35:11 +02:00
faiface 5723022b5a add Take decorator function 2017-07-05 18:14:18 +02:00
faiface f3e2b915bd add Seq compositor 2017-07-05 17:59:43 +02:00
faiface c142c3eb0a add StreamerFunc helper type 2017-07-03 19:07:30 +02:00
faiface a1542703d5 change SampleRate to float64 2017-07-03 18:55:27 +02:00
faiface 019c0f83e1 fix grammar in Streamer doc 2017-07-03 17:37:55 +02:00
faiface ada24fe35c minor stylistic corrections in Streamer doc 2017-07-03 17:28:54 +02:00
faiface c3ce0517f1 audio: add Streamer interface 2017-07-03 14:05:08 +02:00
faiface be247e49f5 optimize ToRGBA (weird one) 2017-07-03 00:23:07 +02:00
faiface dd1ddd15b3 improve sprite.Draw(batch) benchmark 2017-07-03 00:22:45 +02:00
faiface 96f9ed3622 fix bug in text benchmark 2017-07-02 23:26:26 +02:00
faiface e41c8b585b optimize Drawer (reduce map access) 2017-07-02 23:23:27 +02:00
faiface 2c9c36ba28 add text benchmarks 2017-07-02 19:35:56 +02:00
faiface 03f6b2d854 remove unnecessary reassign in color benchmarks 2017-07-02 19:26:43 +02:00
faiface 95f90d3b0d add imdraw benchmarks 2017-07-02 19:22:40 +02:00
faiface 7f4bc89dc4 add ToRGBA, sprite.Draw(batch) and Matrix benchmarks 2017-07-02 19:04:20 +02:00
Michal Štrba 578db5e284 Merge pull request #40 from aerth/master
Use travis-ci
2017-06-16 01:14:23 +02:00
aerth ad2b94bfbc move button 2017-06-15 23:05:00 +00:00
aerth 983146ea91 Link to travis build 2017-06-15 22:52:47 +00:00
aerth 19b10859ad add travis config 2017-06-15 22:40:37 +00:00
faiface 886e7fa22a another minor code style change 2017-06-11 14:14:02 +02:00
faiface 99a43ec1a9 minor code style change 2017-06-11 14:06:45 +02:00
faiface 12df203229 Merge branch 'dev' 2017-06-11 01:19:57 +02:00
faiface d4cd1c33e2 one more tiny doc change 2017-06-11 01:18:23 +02:00
faiface 51cd0314d5 minor doc changes 2017-06-11 01:17:37 +02:00
Michal Štrba 6a9211310a Merge pull request #38 from seebs/master
more performance tweaks
2017-06-11 01:12:03 +02:00
Seebs ee5d49dbd3 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 daedc45ea9 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 678da34fc3 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 f68301dcd8 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 cfdc8beb81 switch back to OpenGL 3.3 (OS X issues) 2017-06-10 15:11:45 +02:00
faiface 1fd110ce4c optimize GLTriangles SetLen and Update 2017-06-10 01:10:59 +02:00
faiface 3665eaf702 Merge branch 'master' into gl2.1 2017-06-09 19:26:14 +02:00
faiface c0766504e3 minor, mostly stylistic, changes 2017-06-09 18:13:05 +02:00
Michal Štrba 5f2ced88ee Merge pull request #36 from seebs/master
revised performance tuning pull request
2017-06-09 18:04:25 +02:00
Seebs ce8687b80f 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 c6e7a83467 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 b6533006e7 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 9d1fc5bd1f 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 c321515d3c 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 1586e600a0 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 9bd9df98f2 add text tutorial link to README 2017-05-30 02:53:24 +02:00
faiface 793d1e6a9a fix typo in doc 2017-05-28 18:50:56 +02:00
faiface f0c42c6e56 add Canvas.Frame method 2017-05-28 18:44:30 +02:00
faiface fa2c741fcf switch to OpenGL 2.1 2017-05-27 13:14:13 +02:00
faiface 28aad855b8 remove accidentaly set theme 2017-05-25 22:51:17 +02:00
Michal Štrba f179118d9e Set theme jekyll-theme-time-machine 2017-05-25 20:03:02 +02:00
faiface 4a0a68effc update drawing features in README 2017-05-24 20:39:11 +02:00
faiface 4dc0fb6f8b add text input mention to README 2017-05-24 14:42:00 +02:00
faiface bbde3a6109 add text drawing feature mention to README 2017-05-24 14:36:18 +02:00
faiface a056444410 fix link in README 2017-05-24 14:34:49 +02:00
faiface a5603e8379 replace xor screenshot with typewriter screenshot in README 2017-05-24 14:33:50 +02:00
faiface b8a287a654 clarify Rect doc 2017-05-21 19:38:21 +02:00
faiface ce8408054d fix typo in ToRGBA doc 2017-05-21 19:33:01 +02:00
faiface 523e6d3e9a fix Matrix doc 2017-05-21 19:30:29 +02:00
faiface fcfeb200b6 replace complex128 Vec with a struct 2017-05-21 19:25:06 +02:00
faiface 8221ab58bc 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 9b7b8d5a0e minor change 2017-05-21 18:23:20 +02:00
faiface 85ef4290b8 update Sprite's doc 2017-05-21 15:31:07 +02:00
faiface 37cd58f0ae remove Text.Matrix() and Text.ColorMask() getters 2017-05-19 01:58:34 +02:00
faiface aa50147c35 change Text properties to fields 2017-05-18 23:59:42 +02:00
faiface 022f25895a change IMDraw properties to fields 2017-05-18 23:50:45 +02:00
faiface 6735475b44 change Sprite.Draw and Canvas.Draw signatures (include Matrix) 2017-05-17 23:45:22 +02:00
faiface fdddde2780 accept zero target size in Rect.Resized 2017-05-15 01:15:16 +02:00
faiface fb51cd9ecb improve Atlas creation time 2-3 times 2017-05-11 19:48:43 +02:00
faiface be2434cfa8 add Window.Repeat 2017-05-10 23:54:06 +02:00
faiface 9062f1eae9 fix and simplify input handling in Window 2017-05-10 21:22:47 +02:00
faiface c9319763d7 add Window.Typed 2017-05-10 21:10:10 +02:00
faiface 9554cd9c20 change text.New to take an Atlas 2017-05-10 17:56:09 +02:00
faiface ef86fe9b20 add text package doc 2017-05-09 16:48:26 +02:00
faiface 101637439e add Text.Matrix and Text.ColorMask 2017-05-09 16:46:11 +02:00
faiface b0e2bd1035 add examples on LineHeight and TabWidth to Text doc 2017-05-09 16:40:44 +02:00
faiface e1f364e5d1 add note about not destroying face.Face to Atlas doc 2017-05-09 16:39:03 +02:00
faiface ef5de4c8c3 mention control characters in Text doc 2017-05-09 16:36:59 +02:00
faiface ee6871c7b8 remove accidental markdown formating from Text doc 2017-05-09 16:35:51 +02:00
faiface fc8eafe3d5 fix typo in Text doc 2017-05-09 16:34:54 +02:00
faiface 81e2e645bd minor change in Text doc 2017-05-09 16:34:13 +02:00
faiface 067d9f48d9 minor change in Atlas doc 2017-05-09 16:32:29 +02:00
faiface 3035fcac9c clarify doc 2017-05-09 16:31:09 +02:00
faiface c5df68f8bb add Text doc 2017-05-09 16:27:55 +02:00
faiface c1f3267176 minor change 2017-05-09 15:26:50 +02:00
faiface e1dba0eb54 add Atlas doc 2017-05-09 15:25:08 +02:00
faiface e7a7ac4026 don't cache kerning in Atlas (too expensive and no benefit) 2017-05-09 15:10:35 +02:00
faiface 847b48292b improve Atlas creation, atlas is now square picture (was one row of characters) 2017-05-09 14:20:34 +02:00
faiface cfa9180bb7 fix PictureDataFromImage (wrong bounds when Min not (0, 0)) 2017-05-09 01:04:04 +02:00
faiface 6ce4094935 rename Text.LineHeight(scale) -> height, since now it's absolute height 2017-05-07 22:03:56 +02:00
faiface c0378a703e rename Glyph.Orig -> Dot 2017-05-07 21:49:26 +02:00
faiface b41a436dce minor change 2017-05-07 21:12:48 +02:00
faiface 68008f163a improve Text code 2017-05-07 21:08:10 +02:00
faiface 75d68a6963 add Text.BoundsOf 2017-05-07 21:00:19 +02:00
faiface ad606d2d0a restructure Text writing for more flexibility and consistency 2017-05-07 20:59:56 +02:00
faiface a5414dbb55 add Atlas.DrawRune 2017-05-07 20:59:41 +02:00
faiface f52e3db155 Merge branch 'dev' into text 2017-05-06 22:58:36 +02:00
faiface b333cd2e0b IMDraw: change default point color to (1, 1, 1, 1) (was (0, 0, 0, 0)) 2017-05-06 22:57:55 +02:00
faiface 8797440f38 fix Text.Bounds 2017-05-05 16:42:40 +02:00
faiface 75a25a0df6 remove Batch from Text and optimize it 2017-05-05 16:13:26 +02:00
faiface af0330d453 add Text.Bounds, Atlas.Ascent, Atlas.Descent 2017-05-05 16:02:47 +02:00
faiface 1619373062 add Rect.Union 2017-05-05 15:43:24 +02:00
faiface 1bb5353ec7 fix Text.Color 2017-05-04 22:30:18 +02:00
faiface 7dc94990e4 move Atlas type to a separate file 2017-05-03 23:59:37 +02:00
faiface 91448dcd68 change Text.LineHeight to use actual units instead of scale (such as 1.5) 2017-05-03 23:57:09 +02:00
faiface 48f3d5cb3e add Text.Atlas (Atlas has some useful stuff, e.g. line height) 2017-05-03 23:55:44 +02:00
faiface e112598b5c add Text.WriteByte 2017-05-03 23:55:10 +02:00
faiface 0d3fd03a40 export Atlas from text package + add Text.WriteRune and Text.WriteString 2017-05-03 23:54:24 +02:00
faiface 2a060fe944 add Text.SetMatrix and Text.SetColorMask 2017-05-03 21:48:05 +02:00
faiface 5f24ce9ccb add Text.LineHeight and text.TabWidth 2017-05-03 21:04:18 +02:00
faiface 90b9fcbf23 add Text.Orig (start of the text) 2017-05-03 20:56:06 +02:00
faiface e3632860b0 optimize Text.Write (remove one allocation) 2017-05-03 00:13:39 +02:00
faiface b463f91a6f add incomplete Text type in text package 2017-05-02 22:46:51 +02:00
faiface a9d735dafe optimize Drawer.Dirty (defer the hard part until drawing) 2017-05-02 22:46:27 +02:00
faiface ec4f5e3663 strikethrough advanced window manipulation mssing feature 2017-05-02 01:15:33 +02:00
faiface 650263a314 fix creating window with no icon 2017-05-01 12:18:23 +02:00
faiface 4de5df6980 just align doc comment to 100 chars per line 2017-05-01 01:38:57 +02:00
Michal Štrba a730295ce8 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 4e7e3956db Set value rather than append 2017-04-30 19:30:11 -04:00
Ousmane Traore ac4239754f Address review comments 2017-04-30 18:43:05 -04:00
Ousmane Traore 8ae8551fda Add Icons paramter to window config 2017-04-30 17:19:51 -04:00
faiface 2c1528a927 fix Window.CursorVisible intial value (was false) 2017-04-30 20:42:25 +02:00
Michal Štrba ce6b0a47c5 Merge pull request #13 from otraore/hide-cursor
Add ability to hide the cursor
2017-04-30 18:50:43 +02:00
Ousmane Traore b6620c9775 Address review comments 2017-04-30 12:33:27 -04:00
Ousmane Traore 2fa80f7d42 Add ability to hide the cursor 2017-04-30 10:40:31 -04:00
faiface c12d22757d add Canvas.SetPixels and Canvas.Pixels methods 2017-04-28 13:24:30 +02:00
faiface 31e5a6ad34 merge dev branch 2017-04-26 23:16:34 +02:00
faiface b7f67ee1f5 fix link in readme 2017-04-26 23:15:08 +02:00
faiface cadd699cf5 add note to readme about macOS, go 1.8 and xcode problems 2017-04-26 23:14:01 +02:00
Michal Štrba 5545a9cecc Merge pull request #6 from faiface/dev
merge shader error fix
2017-04-26 18:01:45 +02:00
Michal Štrba b991a8267d 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 8643f6d0d0 fix for 'texture' as a variable error, closes #4 2017-04-26 08:35:10 -07:00
faiface ca86b961f2 fix compiler error (ouch) 2017-04-26 15:11:12 +02:00
faiface 4cff721955 remove PictureData.SetColor
was confusing since Pictures in e.g. Sprites are not really updatable
2017-04-26 15:04:46 +02:00
faiface 5371f8a2dc add a note about memory leaks to Drawer and Sprite docs 2017-04-26 14:28:25 +02:00
faiface 8ea09ac04c change first sentence in readme 2017-04-24 22:51:43 +02:00
faiface 219fb230fb minor change in readme 2017-04-24 21:00:01 +02:00
faiface 1246119135 add contributing section to readme 2017-04-24 20:55:18 +02:00
faiface 75ab42464d minor change in readme 2017-04-24 20:39:18 +02:00
faiface 0850a249c9 adjust gitter badge location 2017-04-24 14:21:09 +02:00
Michal Štrba ad8f6b8c5a 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 f1db09ce02 Add Gitter badge 2017-04-24 12:18:51 +00:00
faiface 66acc3b742 change wording in readme 2017-04-23 22:29:23 +02:00
faiface 68abc0fffd put images into 2x2 table in readme 2017-04-23 20:57:36 +02:00
faiface 5329a93709 add useful links to readme 2017-04-23 20:35:58 +02:00
faiface e0d0d41037 add missing feature to readme (advanced window manipulation) 2017-04-23 17:48:54 +02:00
faiface 8b53137331 fix typo in readme 2017-04-23 17:30:03 +02:00
faiface 8135f7b3b9 add missing feature (tests and benchmarks) to readme 2017-04-23 14:35:56 +02:00
faiface c2935d3c5b add missing feature (mobile/web backend) to readme 2017-04-23 14:32:41 +02:00
faiface 2a258ce7a5 update a few sentences in readme 2017-04-23 13:54:53 +02:00
faiface 3eaf900a1c reorder features in readme 2017-04-23 01:30:15 +02:00
faiface 6f3d899404 add link to "PixelGL backend" in readme 2017-04-22 23:58:13 +02:00
faiface 7725b2d9e1 reword a sentence in readme 2017-04-22 23:46:08 +02:00
faiface c2c106d34d clarify readme 2017-04-22 23:44:17 +02:00
faiface 56e5dcf593 fix grammar in readme 2017-04-22 23:36:54 +02:00
faiface 91ecf9ee1b add feature to readme 2017-04-22 23:34:34 +02:00
faiface fbe1db6fd5 reorder features in readme 2017-04-22 23:34:06 +02:00
faiface d037091f5a add feature to readme 2017-04-22 23:33:35 +02:00
faiface b4ba58d433 fix typo in readme 2017-04-22 23:29:54 +02:00
faiface ece4dd1d1a change tutorial part in readme 2017-04-22 23:29:05 +02:00
faiface 812c2b5d9e add stunning readme (contributing part missing) 2017-04-22 23:27:54 +02:00
faiface 639bd44303 temporarily fix issue #1 2017-04-22 13:15:57 +02:00
faiface d214312c06 remove debug print 2017-04-21 23:10:02 +02:00
faiface 7b7b663f7d fix Canvas drawing when bounds don't start at (0, 0) 2017-04-21 23:07:48 +02:00
faiface acb14d8c9a minor change in pixel package doc 2017-04-21 17:01:46 +02:00
faiface 00bd0fe945 add pixelgl package doc 2017-04-21 17:00:18 +02:00
faiface 86ecc75efb minor change in pixel package doc 2017-04-21 16:57:36 +02:00
faiface aa22fe0dca add pixel doc 2017-04-21 16:57:08 +02:00
faiface 663727731c remove commented code 2017-04-21 16:43:56 +02:00
faiface ac61175aea fix drawing non-closed lines in IMDraw 2017-04-21 01:17:29 +02:00
faiface fc2a7a8afc simplify code in IMDraw 2017-04-16 00:59:07 +02:00
faiface 9d48c4a828 add IMDraw.Rectangle 2017-04-16 00:01:43 +02:00
faiface 73cb8ef789 minor change 2017-04-15 21:03:22 +02:00
faiface 2b83dae1f4 fix typo in Alpha doc 2017-04-15 18:04:03 +02:00
faiface 79b6412a14 minor change 2017-04-13 20:30:32 +02:00
faiface f0303c27af immediate-like-mode -> immediate-mode-like 2017-04-13 17:44:28 +02:00
faiface 7939b1fdb7 add imdraw package doc comment 2017-04-13 17:41:38 +02:00
faiface 8030595373 simplify code in Rect.Resized 2017-04-13 15:26:48 +02:00
faiface 9472b91565 fix grammar in Vec doc 2017-04-13 15:18:13 +02:00
faiface 4abba37aa3 fix grammar in Drawer doc 2017-04-13 15:15:17 +02:00
faiface 4ce6e2b3c4 remove a bunch of unnecessary Window control methods 2017-04-13 15:03:13 +02:00
faiface 0bfc4cd068 minor optimization in Sprite 2017-04-12 16:18:25 +02:00
faiface b2819732fc clarify IMDraw.Draw doc 2017-04-12 16:03:36 +02:00
faiface 87b57f21cb fix Canvas.Draw 2017-04-12 16:02:39 +02:00
faiface cb9e936395 add Canvas.Draw 2017-04-12 16:00:56 +02:00
faiface bce553aeec remove accidentaly kept updateLock field from GLTriangles 2017-04-12 11:25:11 +02:00
faiface 592eb4b8ef fix type in ComposeMethod doc 2017-04-11 17:28:27 +02:00
faiface af6241d010 add ComposeMethod.Compose 2017-04-11 17:15:02 +02:00
faiface 02e009054d move ComposeTarget to separate file 2017-04-11 16:45:56 +02:00
faiface 41e5e8ca77 fix race condition in GLTriangles 2017-04-11 15:02:58 +02:00
faiface 3e65588e00 adopt RGB and Alpha 2017-04-10 17:25:56 +02:00
faiface 5d82c3cdb9 rename ComposeDst* -> ComposeR* + add ComposePlus, ComposeCopy 2017-04-10 13:59:16 +02:00
faiface 8489bdad27 fix compile error 2017-04-10 00:48:17 +02:00
faiface c2633848f6 remove smooth argument from Canvas constructor 2017-04-10 00:47:06 +02:00
faiface 8379e17cac fix drawing onto Canvas 2017-04-10 00:41:56 +02:00
faiface 5be03c8beb add Window.SetComposeMethod 2017-04-10 00:41:48 +02:00
faiface 3068153419 add Canvas.SetComposeMethod 2017-04-10 00:30:50 +02:00
faiface c1d000859e add missing ComposeDstOver mode 2017-04-10 00:25:17 +02:00
faiface 8da51853be add ComposeTarget interface 2017-04-10 00:20:19 +02:00
faiface bb7b98eda8 fix spelling in doc 2017-04-09 23:19:30 +02:00
faiface 9238e701a7 use glhf.BlendFunc 2017-04-09 23:16:34 +02:00
faiface ed8abc4dc0 adjust RGB doc 2017-04-09 23:08:35 +02:00
faiface e0615451b4 add RGB and Alpha functions 2017-04-09 23:03:30 +02:00
faiface f4916a4272 replace NRGBA to RGBA because Porter-Duff (!!!) 2017-04-09 22:00:26 +02:00
faiface 7d31dd8d6f fix obsolete Batch doc 2017-04-09 15:32:31 +02:00
faiface e6af9eb3c2 fix typo in Window doc 2017-04-08 18:05:50 +02:00
faiface 5097bc9cac adjust WindowConfig doc, more consistent with the rest 2017-04-07 12:34:16 +02:00
faiface 8db95af845 fix GLFrame.SetBounds to not reallocate when not necessary 2017-04-05 23:20:55 +02:00
faiface b41e117ac6 fix Matrix.String 2017-04-04 14:10:39 +02:00
faiface 49338251d2 add Matrix.String 2017-04-04 14:08:37 +02:00
faiface b135012115 minor change 2017-04-04 14:02:39 +02:00
faiface 1a2ca3264d add Window.Color 2017-04-02 19:08:48 +02:00
faiface 46c8784df0 split Canvas into Canvas+GLFrame + add GLPicture 2017-04-01 21:54:44 +02:00
faiface f9482d311f remove Slice and Original from Canvas 2017-03-31 15:03:06 +02:00
faiface 1b32c1a04b make Sprite accept Picture and frame 2017-03-31 15:00:59 +02:00
faiface 0b3cfac547 remove Slice and Original from Picture interface 2017-03-30 23:34:07 +02:00
55 changed files with 6523 additions and 1306 deletions

34
.travis.yml Normal file
View File

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

63
CHANGELOG.md Normal file
View File

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

16
CONTRIBUTING.md Normal file
View File

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

188
README.md
View File

@ -1,5 +1,185 @@
# pixel
A simple and fast desktop multimedia/gamedev library.
# \*\*\*\*\*NOTICE\*\*\*\*\*
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.
This repo is not under active development anymore and has been archived. Continued development has been migrated to [Pixel2](https://github.com/gopxl/pixel). A big thank you to [faiface](https://github.com/faiface) for creating this awesome library and for all the hard work put into it. We encourage old and new users to check out the new repo and contribute to it.
<p align="center"><img src="logo/LOGOTYPE-HORIZONTAL-BLUE.png"></p>
# Pixel [![Build Status](https://travis-ci.org/faiface/pixel.svg?branch=master)](https://travis-ci.org/faiface/pixel) [![GoDoc](https://godoc.org/github.com/faiface/pixel?status.svg)](https://godoc.org/github.com/faiface/pixel) [![Go Report Card](https://goreportcard.com/badge/github.com/faiface/pixel)](https://goreportcard.com/report/github.com/faiface/pixel) [![Join the chat at https://gitter.im/pixellib/Lobby](https://badges.gitter.im/pixellib/Lobby.svg)](https://gitter.im/pixellib/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [![Discord Chat](https://img.shields.io/discord/699679031603494954)](https://discord.gg/q2DK4MP)
A hand-crafted 2D game library in Go. Take a look into the [features](#features) to see what it can
do.
```
go get github.com/faiface/pixel
```
If you are using Modules (Go 1.11 or higher) and want a mutable copy of the source code:
```
git clone https://github.com/faiface/pixel # clone outside of $GOPATH
cd pixel
go install ./...
```
See [requirements](#requirements) for the list of libraries necessary for compilation.
All significant changes are documented in [CHANGELOG.md](CHANGELOG.md).
## 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)
- [Using a custom fragment shader](https://github.com/faiface/pixel/wiki/Using-a-custom-fragment-shader)
## [Examples](https://github.com/faiface/pixel-examples)
The [examples](https://github.com/faiface/pixel-examples) repository 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 pixel-examples/platformer
$ go run main.go
```
Here are some screenshots from the examples!
| [Lights](https://github.com/faiface/pixel-examples/blob/master/lights) | [Platformer](https://github.com/faiface/pixel-examples/blob/master/platformer) |
| --- | --- |
| ![Lights](https://github.com/faiface/pixel-examples/blob/master/lights/screenshot.png) | ![Platformer](https://github.com/faiface/pixel-examples/blob/master/platformer/screenshot.png) |
| [Smoke](https://github.com/faiface/pixel-examples/blob/master/smoke) | [Typewriter](https://github.com/faiface/pixel-examples/blob/master/typewriter) |
| --- | --- |
| ![Smoke](https://github.com/faiface/pixel-examples/blob/master/smoke/screenshot.png) | ![Typewriter](https://github.com/faiface/pixel-examples/blob/master/typewriter/screenshot.png) |
| [Raycaster](https://github.com/faiface/pixel-examples/blob/master/community/raycaster) | [Gizmo](https://github.com/Lallassu/gizmo) |
| --- | --- |
| ![Raycaster](https://github.com/faiface/pixel-examples/blob/master/community/raycaster/screenshot.png) | ![Gizmo](https://github.com/Lallassu/gizmo/blob/master/preview.png) |
## Features
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
## Related repositories
Here are some packages which use Pixel:
- [TilePix](https://github.com/bcvery1/tilepix) Makes handling TMX files built with [Tiled](https://www.mapeditor.org/) trivially easy to work with using Pixel.
- [spriteplus](https://github.com/cebarks/spriteplus) Basic `SpriteSheet` and `Animation` implementations
- [PixelUI](https://github.com/dusk125/pixelui) Imgui-based GUIs for Pixel
- [pixelutils](https://github.com/dusk125/pixelutils) Variety of game related utilities (sprite packer, id generator, ticker, sprite loader, voronoia diagrams)
## Missing features
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)~~ (solved with the addition of GLSL effects)
- Tests and benchmarks
- Vulkan support
**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 libXxf86vm-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
Join us in the [Discord Chat!](https://discord.gg/q2DK4MP)
Pixel is in, let's say, mid-stage of development. Many of the important features are here, some are
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.
## 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)
@ -27,9 +26,9 @@ var _ BasicTarget = (*Batch)(nil)
//
// Note, that if the container does not support TrianglesColor, color masking will not work.
func NewBatch(container Triangles, pic Picture) *Batch {
b := &Batch{cont: Drawer{Triangles: container, Picture: pic}}
b := &Batch{cont: Drawer{Triangles: container, Picture: pic, Cached: true}}
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 {

334
circle.go Normal file
View File

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

466
circle_test.go Normal file
View File

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

View File

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

179
data.go
View File

@ -8,13 +8,25 @@ import (
"math"
)
// zeroValueTriangleData is the default value of a TriangleData element
var zeroValueTriangleData = struct {
Position Vec
Color RGBA
Picture Vec
Intensity float64
ClipRect Rect
IsClipped bool
}{Color: RGBA{1, 1, 1, 1}}
// TrianglesData specifies a list of Triangles vertices with three common properties:
// TrianglesPosition, TrianglesColor and TrianglesPicture.
type TrianglesData []struct {
Position Vec
Color NRGBA
Color RGBA
Picture Vec
Intensity float64
ClipRect Rect
IsClipped bool
}
// MakeTrianglesData creates TrianglesData of length len initialized with default property values.
@ -22,9 +34,11 @@ type TrianglesData []struct {
// Prefer this function to make(TrianglesData, len), because make zeros them, while this function
// does the correct intialization.
func MakeTrianglesData(len int) *TrianglesData {
td := &TrianglesData{}
td.SetLen(len)
return td
td := make(TrianglesData, len)
for i := 0; i < len; i++ {
td[i] = zeroValueTriangleData
}
return &td
}
// Len returns the number of vertices in TrianglesData.
@ -40,12 +54,7 @@ func (td *TrianglesData) SetLen(len int) {
if len > td.Len() {
needAppend := len - td.Len()
for i := 0; i < needAppend; i++ {
*td = append(*td, struct {
Position Vec
Color NRGBA
Picture Vec
Intensity float64
}{V(0, 0), NRGBA{1, 1, 1, 1}, V(0, 0), 0})
*td = append(*td, zeroValueTriangleData)
}
}
if len < td.Len() {
@ -82,6 +91,11 @@ func (td *TrianglesData) updateData(t Triangles) {
(*td)[i].Picture, (*td)[i].Intensity = t.Picture(i)
}
}
if t, ok := t.(TrianglesClipped); ok {
for i := range *td {
(*td)[i].ClipRect, (*td)[i].IsClipped = t.ClipRect(i)
}
}
}
// Update copies vertex properties from the supplied Triangles into this TrianglesData.
@ -96,10 +110,9 @@ func (td *TrianglesData) Update(t Triangles) {
// Copy returns an exact independent copy of this TrianglesData.
func (td *TrianglesData) Copy() Triangles {
copyTd := TrianglesData{}
copyTd.SetLen(td.Len())
copyTd := MakeTrianglesData(td.Len())
copyTd.Update(td)
return &copyTd
return copyTd
}
// Position returns the position property of i-th vertex.
@ -108,7 +121,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
}
@ -117,6 +130,11 @@ func (td *TrianglesData) Picture(i int) (pic Vec, intensity float64) {
return (*td)[i].Picture, (*td)[i].Intensity
}
// ClipRect returns the clipping rectangle property of the i-th vertex.
func (td *TrianglesData) ClipRect(i int) (rect Rect, has bool) {
return (*td)[i].ClipRect, (*td)[i].IsClipped
}
// PictureData specifies an in-memory rectangular area of pixels and implements Picture and
// PictureColor.
//
@ -126,36 +144,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 +183,23 @@ 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
} 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 +218,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 +239,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 +280,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)])
}

300
data_test.go Normal file
View File

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

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,31 @@ 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
Cached bool
tris map[Target]TargetTriangles
clean map[Target]bool
pics map[targetPicturePair]TargetPicture
inited bool
targets map[Target]*drawerTarget
allTargets []*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 +48,8 @@ func (d *Drawer) lazyInit() {
func (d *Drawer) Dirty() {
d.lazyInit()
for t := range d.clean {
d.clean[t] = false
for _, t := range d.allTargets {
t.clean = false
}
}
@ -60,30 +64,39 @@ 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
d.allTargets = append(d.allTargets, 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 = pic.Slice(d.Picture.Bounds()).(TargetPicture)
pic = t.MakePicture(d.Picture)
pic.Draw(tri)
if d.Cached {
dt.pics[d.Picture] = pic
}
}
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

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

15
go.mod Normal file
View File

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

54
go.sum Normal file
View File

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

View File

@ -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,20 @@ 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) {
// Assert that Color is of type pixel.RGBA,
if _, ok := imd.Color.(pixel.RGBA); !ok {
// otherwise cast it
imd.Color = pixel.ToRGBA(imd.Color)
}
opts := point{
col: imd.Color.(pixel.RGBA),
pic: imd.Picture,
in: imd.Intensity,
precision: imd.Precision,
endshape: imd.EndShape,
}
for _, pt := range pts {
imd.pushPt(pt, imd.opts)
imd.pushPt(pt, opts)
}
}
@ -125,33 +150,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 +158,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 +177,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 +261,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 +284,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 +360,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 +387,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 +414,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 +430,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 +438,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 +472,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 +496,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 +532,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 +558,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 +584,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 +615,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,10 +89,10 @@ 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.
// TrianglesPicture specifies Triangles with Picture property.
//
// The first value returned from Picture method is Picture coordinates. The second one specifies the
// weight of the Picture. Value of 0 means, that Picture should be completely ignored, 1 means that
@ -102,25 +102,21 @@ type TrianglesPicture interface {
Picture(i int) (pic Vec, intensity float64)
}
// TrianglesClipped specifies Triangles with Clipping Rectangle property.
//
// The first value returned from ClipRect method is the clipping rectangle. The second one specifies
// if the triangle is clipped.
type TrianglesClipped interface {
Triangles
ClipRect(i int) (rect Rect, is bool)
}
// Picture represents a rectangular area of raster data, such as a color. It has Bounds which
// specify the rectangle where data is located.
type Picture interface {
// Bounds returns the rectangle of the Picture. All data is located witih this rectangle.
// Bounds returns the rectangle of the Picture. All data is located within 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 +135,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
}

699
line_test.go Normal file
View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

15
math.go Normal file
View File

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

46
math_test.go Normal file
View File

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

98
matrix.go Normal file
View File

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

149
matrix_test.go Normal file
View File

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

68
pixel_test.go Normal file
View File

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

View File

@ -3,7 +3,6 @@ package pixelgl
import (
"fmt"
"image/color"
"math"
"github.com/faiface/glhf"
"github.com/faiface/mainthread"
@ -17,56 +16,58 @@ 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 *GLShader
// 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 {
var _ pixel.ComposeTarget = (*Canvas)(nil)
// NewCanvas creates a new empty, fully transparent Canvas with given bounds.
func NewCanvas(bounds pixel.Rect) *Canvas {
c := &Canvas{
smooth: smooth,
mat: mgl32.Ident3(),
col: mgl32.Vec4{1, 1, 1, 1},
gf: NewGLFrame(bounds),
mat: mgl32.Ident3(),
col: mgl32.Vec4{1, 1, 1, 1},
}
c.orig = c
mainthread.Call(func() {
var err error
c.s, err = glhf.NewShader(
canvasVertexFormat,
canvasUniformFormat,
canvasVertexShader,
canvasFragmentShader,
)
if err != nil {
panic(errors.Wrap(err, "failed to create Canvas, there's a bug in the shader"))
}
})
c.shader = NewGLShader(baseCanvasFragmentShader)
c.SetBounds(bounds)
return c
}
// SetUniform will update the named uniform with the value of any supported underlying
// attribute variable. If the uniform already exists, including defaults, they will be reassigned
// to the new value. The value can be a pointer.
func (c *Canvas) SetUniform(name string, value interface{}) {
c.shader.SetUniform(name, value)
}
// SetFragmentShader allows you to set a new fragment shader on the underlying
// framebuffer. Argument "src" is the GLSL source, not a filename.
func (c *Canvas) SetFragmentShader(src string) {
c.shader.fs = src
c.shader.Update()
}
// MakeTriangles creates a specialized copy of the supplied Triangles that draws onto this Canvas.
//
// TrianglesPosition, TrianglesColor and TrianglesPicture are supported.
func (c *Canvas) MakeTriangles(t pixel.Triangles) pixel.TargetTriangles {
if gt, ok := t.(*GLTriangles); ok {
return &canvasTriangles{
GLTriangles: gt,
dst: c,
}
}
return &canvasTriangles{
GLTriangles: NewGLTriangles(c.s, t),
GLTriangles: NewGLTriangles(c.shader, t),
dst: c,
}
}
@ -75,143 +76,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 +155,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 +205,120 @@ 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
smt := ct.dst.smooth
mat := ct.dst.mat
col := ct.dst.col
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()),
})
ct.dst.s.SetUniformAttr(canvasTransform, mat)
ct.dst.s.SetUniformAttr(canvasColorMask, col)
frame := ct.dst.gf.Frame()
shader := ct.shader.s
frame.Begin()
shader.Begin()
ct.shader.uniformDefaults.transform = mat
ct.shader.uniformDefaults.colormask = col
dstBounds := ct.dst.Bounds()
ct.shader.uniformDefaults.bounds = mgl32.Vec4{
float32(dstBounds.Min.X),
float32(dstBounds.Min.Y),
float32(dstBounds.W()),
float32(dstBounds.H()),
}
bx, by, bw, bh := intBounds(bounds)
ct.shader.uniformDefaults.texbounds = mgl32.Vec4{
float32(bx),
float32(by),
float32(bw),
float32(bh),
}
for loc, u := range ct.shader.uniforms {
ct.shader.s.SetUniformAttr(loc, u.Value())
}
if tex == nil {
ct.vs.Begin()
@ -339,21 +327,8 @@ 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()),
})
if tex.Smooth() != ct.dst.smooth {
tex.SetSmooth(ct.dst.smooth)
if tex.Smooth() != smt {
tex.SetSmooth(smt)
}
ct.vs.Begin()
@ -363,53 +338,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,130 +357,5 @@ 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)
ct.draw(cp.GLPicture.Texture(), cp.GLPicture.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)
}
const (
canvasPosition int = iota
canvasColor
canvasTexture
canvasIntensity
)
var canvasVertexFormat = glhf.AttrFormat{
canvasPosition: {Name: "position", Type: glhf.Vec2},
canvasColor: {Name: "color", Type: glhf.Vec4},
canvasTexture: {Name: "texture", Type: glhf.Vec2},
canvasIntensity: {Name: "intensity", Type: glhf.Float},
}
const (
canvasTransform int = iota
canvasColorMask
canvasBounds
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},
}
var canvasVertexShader = `
#version 330 core
in vec2 position;
in vec4 color;
in vec2 texture;
in float intensity;
out vec4 Color;
out vec2 Texture;
out float Intensity;
uniform mat3 transform;
uniform vec4 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);
gl_Position = vec4(normPos, 0.0, 1.0);
Color = color;
Texture = texture;
Intensity = intensity;
}
`
var canvasFragmentShader = `
#version 330 core
in vec4 Color;
in vec2 Texture;
in float Intensity;
out vec4 color;
uniform vec4 colorMask;
uniform vec4 texBorders;
uniform vec4 texBounds;
uniform sampler2D tex;
void main() {
if (Intensity == 0) {
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);
}
}
}
`

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

298
pixelgl/glshader.go Normal file
View File

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

View File

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

View File

@ -1,9 +1,11 @@
package pixelgl
import (
"time"
"github.com/faiface/mainthread"
"github.com/faiface/pixel"
"github.com/go-gl/glfw/v3.2/glfw"
"github.com/go-gl/glfw/v3.3/glfw"
)
// Pressed returns whether the Button is currently pressed down.
@ -11,14 +13,21 @@ func (w *Window) Pressed(button Button) bool {
return w.currInp.buttons[button]
}
// JustPressed returns whether the Button has just been pressed down.
// JustPressed returns whether the Button has been pressed in the last frame.
func (w *Window) JustPressed(button Button) bool {
return w.currInp.buttons[button] && !w.prevInp.buttons[button]
return w.pressEvents[button]
}
// JustReleased returns whether the Button has just been released up.
// JustReleased returns whether the Button has been released in the last frame.
func (w *Window) JustReleased(button Button) bool {
return !w.currInp.buttons[button] && w.prevInp.buttons[button]
return w.releaseEvents[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.
@ -26,11 +35,42 @@ func (w *Window) MousePosition() pixel.Vec {
return w.currInp.mouse
}
// MousePreviousPosition returns the previous mouse position in the Window's Bounds.
func (w *Window) MousePreviousPosition() pixel.Vec {
return w.prevInp.mouse
}
// SetMousePosition positions the mouse cursor anywhere within the Window's Bounds.
func (w *Window) SetMousePosition(v pixel.Vec) {
mainthread.Call(func() {
if (v.X >= 0 && v.X <= w.bounds.W()) &&
(v.Y >= 0 && v.Y <= w.bounds.H()) {
w.window.SetCursorPos(
v.X+w.bounds.Min.X,
(w.bounds.H()-v.Y)+w.bounds.Min.Y,
)
w.prevInp.mouse = v
w.currInp.mouse = v
w.tempInp.mouse = v
}
})
}
// MouseInsideWindow returns true if the mouse position is within the Window's Bounds.
func (w *Window) MouseInsideWindow() bool {
return w.cursorInsideWindow
}
// MouseScroll returns the mouse scroll amount (in both axes) since the last call to Window.Update.
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 +362,11 @@ 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.tempPressEvents[Button(button)] = true
w.tempInp.buttons[Button(button)] = true
case glfw.Release:
w.currInp.buttons[Button(button)] = false
w.tempReleaseEvents[Button(button)] = true
w.tempInp.buttons[Button(button)] = false
}
})
@ -334,38 +376,74 @@ func (w *Window) initInput() {
}
switch action {
case glfw.Press:
w.currInp.buttons[Button(key)] = true
w.tempPressEvents[Button(key)] = true
w.tempInp.buttons[Button(key)] = true
case glfw.Release:
w.currInp.buttons[Button(key)] = false
w.tempReleaseEvents[Button(key)] = true
w.tempInp.buttons[Button(key)] = false
case glfw.Repeat:
w.tempInp.repeat[Button(key)] = true
}
})
w.window.SetCursorEnterCallback(func(_ *glfw.Window, entered bool) {
w.cursorInsideWindow = entered
})
w.window.SetCursorPosCallback(func(_ *glfw.Window, x, y float64) {
w.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.doUpdateInput()
}
// UpdateInputWait blocks until an event is received or a timeout. If timeout is 0
// then it will wait indefinitely
func (w *Window) UpdateInputWait(timeout time.Duration) {
mainthread.Call(func() {
if timeout <= 0 {
glfw.WaitEvents()
} else {
glfw.WaitEventsTimeout(timeout.Seconds())
}
})
w.doUpdateInput()
}
// internal input bookkeeping
func (w *Window) doUpdateInput() {
w.prevInp = w.currInp
w.currInp = w.tempInp
w.pressEvents = w.tempPressEvents
w.releaseEvents = w.tempReleaseEvents
// Clear last frame's temporary status
w.tempPressEvents = [KeyLast + 1]bool{}
w.tempReleaseEvents = [KeyLast + 1]bool{}
w.tempInp.repeat = [KeyLast + 1]bool{}
w.tempInp.scroll = pixel.ZV
w.tempInp.typed = ""
w.updateJoystickInput()
}

193
pixelgl/joystick.go Normal file
View File

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

View File

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

View File

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

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,13 +1,16 @@
package pixelgl
import (
"fmt"
"image"
"image/color"
"runtime"
"github.com/faiface/glhf"
"github.com/faiface/mainthread"
"github.com/faiface/pixel"
"github.com/go-gl/glfw/v3.2/glfw"
"github.com/go-gl/gl/v3.3-core/gl"
"github.com/go-gl/glfw/v3.3/glfw"
"github.com/pkg/errors"
)
@ -20,51 +23,92 @@ 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
// specified Monitor.
Fullscreen *Monitor
// Initial window position
Position pixel.Vec
// Whether a Window is resizable.
// If set to nil, the Window will be windowed. Otherwise it will be fullscreen on the
// specified Monitor.
Monitor *Monitor
// Resizable specifies whether the window will be resizable by the user.
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 Window omits the borders and decorations (close button, etc.).
Undecorated bool
// If set to true, a Window will not get focused upon showing up.
Unfocused bool
// NoIconify specifies whether fullscreen windows should not automatically
// iconify (and restore the previous video mode) on focus loss.
NoIconify bool
// Whether a Window is maximized.
Maximized bool
// AlwaysOnTop specifies whether the windowed mode window will be floating
// above other regular windows, also called topmost or always-on-top.
// This is intended primarily for debugging purposes and cannot be used to
// implement proper full screen windows.
AlwaysOnTop bool
// TransparentFramebuffer specifies whether the window framebuffer will be
// transparent. If enabled and supported by the system, the window
// framebuffer alpha channel will be used to combine the framebuffer with
// the background. This does not affect window decorations.
TransparentFramebuffer bool
// VSync (vertical synchronization) synchronizes Window's framerate with the framerate of
// the monitor.
VSync bool
// Maximized specifies whether the window is maximized.
Maximized bool
// Invisible specifies whether the window will be initially hidden.
// You can make the window visible later using Window.Show().
Invisible bool
//SamplesMSAA specifies the level of MSAA to be used. Must be one of 0, 2, 4, 8, 16. 0 to disable.
SamplesMSAA int
}
// Window is a window handler. Use this type to manipulate a window (input, drawing, etc.).
type Window struct {
window *glfw.Window
bounds pixel.Rect
canvas *Canvas
vsync bool
bounds pixel.Rect
canvas *Canvas
vsync bool
cursorVisible bool
cursorInsideWindow 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
}
pressEvents, tempPressEvents [KeyLast + 1]bool
releaseEvents, tempReleaseEvents [KeyLast + 1]bool
prevJoy, currJoy, tempJoy joystickState
}
var currWin *Window
@ -78,7 +122,18 @@ func NewWindow(cfg WindowConfig) (*Window, error) {
false: glfw.False,
}
w := &Window{bounds: cfg.Bounds}
w := &Window{bounds: cfg.Bounds, cursorVisible: true}
flag := false
for _, v := range []int{0, 2, 4, 8, 16} {
if cfg.SamplesMSAA == v {
flag = true
break
}
}
if !flag {
return nil, fmt.Errorf("invalid value '%v' for msaaSamples", cfg.SamplesMSAA)
}
err := mainthread.CallErr(func() error {
var err error
@ -89,10 +144,17 @@ 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.Floating, bool2int[cfg.AlwaysOnTop])
glfw.WindowHint(glfw.AutoIconify, bool2int[!cfg.NoIconify])
glfw.WindowHint(glfw.TransparentFramebuffer, bool2int[cfg.TransparentFramebuffer])
glfw.WindowHint(glfw.Maximized, bool2int[cfg.Maximized])
glfw.WindowHint(glfw.Visible, bool2int[!cfg.Invisible])
glfw.WindowHint(glfw.Samples, cfg.SamplesMSAA)
if cfg.Position.X != 0 || cfg.Position.Y != 0 {
glfw.WindowHint(glfw.Visible, glfw.False)
}
var share *glfw.Window
if currWin != nil {
@ -110,8 +172,15 @@ func NewWindow(cfg WindowConfig) (*Window, error) {
return err
}
if cfg.Position.X != 0 || cfg.Position.Y != 0 {
w.window.SetPos(int(cfg.Position.X), int(cfg.Position.Y))
w.window.Show()
}
// enter the OpenGL context
w.begin()
glhf.Init()
gl.Enable(gl.MULTISAMPLE)
w.end()
return nil
@ -120,12 +189,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)
@ -142,13 +222,31 @@ func (w *Window) Destroy() {
// Update swaps buffers and polls events. Call this method at the end of each frame.
func (w *Window) Update() {
w.SwapBuffers()
w.UpdateInput()
}
// ClipboardText returns the current value of the systems clipboard.
func (w *Window) ClipboardText() string {
return w.window.GetClipboardString()
}
// SetClipboardText passes the given string to the underlying glfw window to set the
// systems clipboard.
func (w *Window) SetClipboardText(text string) {
w.window.SetClipboardString(text)
}
// SwapBuffers swaps buffers. Call this to swap buffers without polling window events.
// Note that Update invokes SwapBuffers.
func (w *Window) SwapBuffers() {
mainthread.Call(func() {
_, _, 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 +254,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)
@ -175,8 +274,6 @@ func (w *Window) Update() {
w.window.SwapBuffers()
w.end()
})
w.updateInput()
}
// SetClosed sets the closed flag of the Window.
@ -217,25 +314,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 +388,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 +403,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 +412,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,11 +422,36 @@ 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)
}
})
}
// SetCursorDisabled hides the cursor and provides unlimited virtual cursor movement
// make cursor visible using SetCursorVisible
func (w *Window) SetCursorDisabled() {
w.cursorVisible = false
mainthread.Call(func() {
w.window.SetInputMode(glfw.CursorMode, glfw.CursorDisabled)
})
}
// CursorVisible returns the visibility status of the mouse cursor.
func (w *Window) CursorVisible() bool {
return w.cursorVisible
}
// Note: must be called inside the main thread.
func (w *Window) begin() {
if currWin != w {
w.window.MakeContextCurrent()
glhf.Init()
currWin = w
}
}
@ -366,7 +471,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 +486,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 +508,37 @@ 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)
}
// Canvas returns the window's underlying Canvas
func (w *Window) Canvas() *Canvas {
return w.canvas
}
// Show makes the window visible, if it was previously hidden. If the window is
// already visible or is in full screen mode, this function does nothing.
func (w *Window) Show() {
mainthread.Call(func() {
w.window.Show()
})
}
// Clipboard returns the contents of the system clipboard.
func (w *Window) Clipboard() string {
var clipboard string
mainthread.Call(func() {
clipboard = w.window.GetClipboardString()
})
return clipboard
}
// SetClipboardString sets the system clipboard to the specified UTF-8 encoded string.
func (w *Window) SetClipboard(str string) {
mainthread.Call(func() {
w.window.SetClipboardString(str)
})
}

284
rectangle.go Normal file
View File

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

384
rectangle_test.go Normal file
View File

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

130
sprite.go
View File

@ -2,43 +2,53 @@ 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},
d: Drawer{Triangles: tri, Cached: true},
}
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()
// SetCached makes the sprite cache all the
// incoming pictures if the argument is true, and
// doesn't make it do that if the argument is false.
func (s *Sprite) SetCached(cached bool) {
s.d.Cached = cached
}
// Picture returns the current Sprite's Picture.
@ -46,65 +56,63 @@ 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
}
// matrix and mask
for i := range *s.tri {
(*s.tri)[i].Position = s.matrix.Project((*s.tri)[i].Position)
(*s.tri)[i].Color = s.mask
}
s.d.Dirty()

247
text/atlas.go Normal file
View File

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

29
text/atlas_test.go Normal file
View File

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

2
text/doc.go Normal file
View File

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

359
text/text.go Normal file
View File

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

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

462
vector.go Normal file
View File

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

27
vector_test.go Normal file
View File

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