Compare commits

...

317 Commits

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
45 changed files with 4120 additions and 638 deletions

View File

@ -1,4 +1,6 @@
language: go language: go
# https://github.com/golang/go/issues/31293
dist: xenial
sudo: false sudo: false
addons: addons:
apt: apt:
@ -11,14 +13,22 @@ addons:
- libxi-dev - libxi-dev
- libopenal-dev - libopenal-dev
- libasound2-dev - libasound2-dev
- libgl1-mesa-dev
services:
- xvfb
env:
- GO111MODULE=on
go: go:
- 1.8 - tip
- 1.7.4 - 1.12.x
- tip
install: install:
- go get -t ./... - # 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: script:
- go test -i -race ./... - go test -v -race -mod=readonly ./...
- go test -v -race ./...

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

View File

@ -4,11 +4,13 @@
## Here are a few ways you can contribute ## Here are a few ways you can contribute
1. **Make a community example** and place it inside the [examples/community](examples/community) folder. 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. 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. 3. **Add a small feature or an improvement**. Feel like some small feature is missing? Just make a PR. Be ready that I might reject it, though, if I don't find it particularly appealing.
4. **Join the big development** by joining the discussion at the [Gitter](https://gitter.im/pixellib/Lobby), where we can discuss bigger changes and implement them after that. 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 ## 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/. Go gives you a nice surprise when attempting to make a PR on Github. The thing is, that when user _xyz_ forks Pixel on Github, it ends up in _github.com/xyz/pixel_, which fucks up your import paths. Here's how you deal with that: https://www.reddit.com/r/golang/comments/2jdcw1/how_do_you_deal_with_github_forking/.
[examples]: https://github.com/faiface/pixel-examples/tree/master/community

View File

@ -1,5 +1,12 @@
# 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) # \*\*\*\*\*NOTICE\*\*\*\*\*
This repo is not under active development anymore and has been archived. Continued development has been migrated to [Pixel2](https://github.com/gopxl/pixel). A big thank you to [faiface](https://github.com/faiface) for creating this awesome library and for all the hard work put into it. We encourage old and new users to check out the new repo and contribute to it.
<p align="center"><img src="logo/LOGOTYPE-HORIZONTAL-BLUE.png"></p>
# Pixel [![Build Status](https://travis-ci.org/faiface/pixel.svg?branch=master)](https://travis-ci.org/faiface/pixel) [![GoDoc](https://godoc.org/github.com/faiface/pixel?status.svg)](https://godoc.org/github.com/faiface/pixel) [![Go Report Card](https://goreportcard.com/badge/github.com/faiface/pixel)](https://goreportcard.com/report/github.com/faiface/pixel) [![Join the chat at https://gitter.im/pixellib/Lobby](https://badges.gitter.im/pixellib/Lobby.svg)](https://gitter.im/pixellib/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [![Discord Chat](https://img.shields.io/discord/699679031603494954)](https://discord.gg/q2DK4MP)
A hand-crafted 2D game library in Go. Take a look into the [features](#features) to see what it can A hand-crafted 2D game library in Go. Take a look into the [features](#features) to see what it can
do. do.
@ -8,8 +15,18 @@ do.
go get github.com/faiface/pixel go get github.com/faiface/pixel
``` ```
If you are using Modules (Go 1.11 or higher) and want a mutable copy of the source code:
```
git clone https://github.com/faiface/pixel # clone outside of $GOPATH
cd pixel
go install ./...
```
See [requirements](#requirements) for the list of libraries necessary for compilation. See [requirements](#requirements) for the list of libraries necessary for compilation.
All significant changes are documented in [CHANGELOG.md](CHANGELOG.md).
## Tutorial ## Tutorial
The [Wiki of this repo](https://github.com/faiface/pixel/wiki) contains an extensive tutorial The [Wiki of this repo](https://github.com/faiface/pixel/wiki) contains an extensive tutorial
@ -46,9 +63,9 @@ Here are some screenshots from the examples!
| --- | --- | | --- | --- |
| ![Smoke](https://github.com/faiface/pixel-examples/blob/master/smoke/screenshot.png) | ![Typewriter](https://github.com/faiface/pixel-examples/blob/master/typewriter/screenshot.png) | | ![Smoke](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) | [Starfield](https://github.com/faiface/pixel-examples/blob/master/community/starfield) | | [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) | ![Starfield](https://github.com/faiface/pixel-examples/blob/master/community/starfield/screenshot.png) | | ![Raycaster](https://github.com/faiface/pixel-examples/blob/master/community/raycaster/screenshot.png) | ![Gizmo](https://github.com/Lallassu/gizmo/blob/master/preview.png) |
## Features ## Features
@ -94,6 +111,14 @@ Here's the list of the main features in Pixel. Although Pixel is still under hea
- Small codebase, ~5K lines of code, including the backend [glhf](https://github.com/faiface/glhf) - Small codebase, ~5K lines of code, including the backend [glhf](https://github.com/faiface/glhf)
package package
## Related repositories
Here are some packages which use Pixel:
- [TilePix](https://github.com/bcvery1/tilepix) Makes handling TMX files built with [Tiled](https://www.mapeditor.org/) trivially easy to work with using Pixel.
- [spriteplus](https://github.com/cebarks/spriteplus) Basic `SpriteSheet` and `Animation` implementations
- [PixelUI](https://github.com/dusk125/pixelui) Imgui-based GUIs for Pixel
- [pixelutils](https://github.com/dusk125/pixelutils) Variety of game related utilities (sprite packer, id generator, ticker, sprite loader, voronoia diagrams)
## Missing features ## Missing features
Pixel is in development and still missing few critical features. Here're the most critical ones. Pixel is in development and still missing few critical features. Here're the most critical ones.
@ -104,8 +129,9 @@ Pixel is in development and still missing few critical features. Here're the mos
- ~~Advanced window manipulation (cursor hiding, window icon, ...)~~ - ~~Advanced window manipulation (cursor hiding, window icon, ...)~~
- Better support for Hi-DPI displays - Better support for Hi-DPI displays
- Mobile (and perhaps HTML5?) backend - Mobile (and perhaps HTML5?) backend
- More advanced graphical effects (e.g. blur) - ~~More advanced graphical effects (e.g. blur)~~ (solved with the addition of GLSL effects)
- Tests and benchmarks - Tests and benchmarks
- Vulkan support
**Implementing these features will get us to the 1.0 release.** Contribute, so that it's as soon as **Implementing these features will get us to the 1.0 release.** Contribute, so that it's as soon as
possible! possible!
@ -126,7 +152,7 @@ The OpenGL version used is **OpenGL 3.3**.
headers and libraries. headers and libraries.
- On Ubuntu/Debian-like Linux distributions, you need `libgl1-mesa-dev` and `xorg-dev` packages. - On Ubuntu/Debian-like Linux distributions, you need `libgl1-mesa-dev` and `xorg-dev` packages.
- On CentOS/Fedora-like Linux distributions, you need `libX11-devel libXcursor-devel libXrandr-devel - On CentOS/Fedora-like Linux distributions, you need `libX11-devel libXcursor-devel libXrandr-devel
libXinerama-devel mesa-libGL-devel libXi-devel` packages. 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. - 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 **The combination of Go 1.8, macOS and latest XCode seems to be problematic** as mentioned in issue
@ -135,6 +161,8 @@ The OpenGL version used is **OpenGL 3.3**.
## Contributing ## Contributing
Join us in the [Discord Chat!](https://discord.gg/q2DK4MP)
Pixel is in, let's say, mid-stage of development. Many of the important features are here, some are Pixel is in, let's say, mid-stage of development. Many of the important features are here, some are
missing. That's why **contributions are very important and welcome!** All alone, I will be able to missing. That's why **contributions are very important and welcome!** All alone, I will be able to
finish the library, but it'll take a lot of time. With your help, it'll take much less. I encourage finish the library, but it'll take a lot of time. With your help, it'll take much less. I encourage
@ -152,10 +180,6 @@ better result.
Take a look at [CONTRIBUTING.md](CONTRIBUTING.md) for further information. Take a look at [CONTRIBUTING.md](CONTRIBUTING.md) for further information.
For any kind of discussion, feel free to use our
[Gitter](https://gitter.im/pixellib/Lobby)
community.
## License ## License
[MIT](LICENSE) [MIT](LICENSE)

View File

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

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

42
data.go
View File

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

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

View File

@ -23,9 +23,11 @@ package pixel
type Drawer struct { type Drawer struct {
Triangles Triangles Triangles Triangles
Picture Picture Picture Picture
Cached bool
targets map[Target]*drawerTarget targets map[Target]*drawerTarget
inited bool allTargets []*drawerTarget
inited bool
} }
type drawerTarget struct { type drawerTarget struct {
@ -46,7 +48,7 @@ func (d *Drawer) lazyInit() {
func (d *Drawer) Dirty() { func (d *Drawer) Dirty() {
d.lazyInit() d.lazyInit()
for _, t := range d.targets { for _, t := range d.allTargets {
t.clean = false t.clean = false
} }
} }
@ -68,6 +70,7 @@ func (d *Drawer) Draw(t Target) {
pics: make(map[Picture]TargetPicture), pics: make(map[Picture]TargetPicture),
} }
d.targets[t] = dt d.targets[t] = dt
d.allTargets = append(d.allTargets, dt)
} }
if dt.tris == nil { if dt.tris == nil {
@ -89,7 +92,10 @@ func (d *Drawer) Draw(t Target) {
pic := dt.pics[d.Picture] pic := dt.pics[d.Picture]
if pic == nil { if pic == nil {
pic = t.MakePicture(d.Picture) pic = t.MakePicture(d.Picture)
dt.pics[d.Picture] = pic
if d.Cached {
dt.pics[d.Picture] = pic
}
} }
pic.Draw(dt.tris) pic.Draw(dt.tris)

View File

@ -1,405 +0,0 @@
package pixel
import (
"fmt"
"math"
)
// Clamp returns x clamped to the interval [min, max].
//
// If x is less than min, min is returned. If x is more than max, max is returned. Otherwise, x is
// returned.
func Clamp(x, min, max float64) float64 {
if x < min {
return min
}
if x > max {
return max
}
return x
}
// Vec is a 2D vector type with X and Y coordinates.
//
// Create vectors with the V constructor:
//
// u := pixel.V(1, 2)
// v := pixel.V(8, -3)
//
// 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}
}
// 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,
}
}
// 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)
}
// 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))
}
// 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.
//
// 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()
}
// Center returns the position of the center of the Rect.
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 R(0, 0, 0, 0).
func (r Rect) Intersect(s Rect) Rect {
t := R(
math.Max(r.Min.X, s.Min.X),
math.Max(r.Min.Y, s.Min.Y),
math.Min(r.Max.X, s.Max.X),
math.Min(r.Max.Y, s.Max.Y),
)
if t.Min.X >= t.Max.X || t.Min.Y >= t.Max.Y {
return Rect{}
}
return t
}
// Matrix is a 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.
//
// It turns out that multiplying a vector by the inverse matrix of m can be nearly-accomplished by
// subtracting the translate part of the matrix and multplying by the inverse of the top-left 2x2
// matrix, and the inverse of a 2x2 matrix is simple enough to just be inlined in the computation.
//
// Time complexity is O(1).
func (m Matrix) Unproject(u Vec) Vec {
d := (m[0] * m[3]) - (m[1] * m[2])
u.X, u.Y = (u.X-m[4])/d, (u.Y-m[5])/d
return Vec{u.X*m[3] - u.Y*m[1], u.Y*m[0] - u.X*m[2]}
}

View File

@ -1,79 +0,0 @@
package pixel_test
import (
"fmt"
"testing"
"github.com/faiface/pixel"
)
type rectTestTransform struct {
name string
f func(pixel.Rect) pixel.Rect
}
func TestResizeRect(t *testing.T) {
// rectangles
squareAroundOrigin := pixel.R(-10, -10, 10, 10)
squareAround2020 := pixel.R(10, 10, 30, 30)
rectangleAroundOrigin := pixel.R(-20, -10, 20, 10)
rectangleAround2020 := pixel.R(0, 10, 40, 30)
// resize transformations
resizeByHalfAroundCenter := rectTestTransform{"by half around center", func(rect pixel.Rect) pixel.Rect {
return rect.Resized(rect.Center(), rect.Size().Scaled(0.5))
}}
resizeByHalfAroundMin := rectTestTransform{"by half around Min", func(rect pixel.Rect) pixel.Rect {
return rect.Resized(rect.Min, rect.Size().Scaled(0.5))
}}
resizeByHalfAroundMax := rectTestTransform{"by half around Max", func(rect pixel.Rect) pixel.Rect {
return rect.Resized(rect.Max, rect.Size().Scaled(0.5))
}}
resizeByHalfAroundMiddleOfLeftSide := rectTestTransform{"by half around middle of left side", func(rect pixel.Rect) pixel.Rect {
return rect.Resized(pixel.V(rect.Min.X, rect.Center().Y), rect.Size().Scaled(0.5))
}}
resizeByHalfAroundOrigin := rectTestTransform{"by half around the origin", func(rect pixel.Rect) pixel.Rect {
return rect.Resized(pixel.ZV, rect.Size().Scaled(0.5))
}}
testCases := []struct {
input pixel.Rect
transform rectTestTransform
answer pixel.Rect
}{
{squareAroundOrigin, resizeByHalfAroundCenter, pixel.R(-5, -5, 5, 5)},
{squareAround2020, resizeByHalfAroundCenter, pixel.R(15, 15, 25, 25)},
{rectangleAroundOrigin, resizeByHalfAroundCenter, pixel.R(-10, -5, 10, 5)},
{rectangleAround2020, resizeByHalfAroundCenter, pixel.R(10, 15, 30, 25)},
{squareAroundOrigin, resizeByHalfAroundMin, pixel.R(-10, -10, 0, 0)},
{squareAround2020, resizeByHalfAroundMin, pixel.R(10, 10, 20, 20)},
{rectangleAroundOrigin, resizeByHalfAroundMin, pixel.R(-20, -10, 0, 0)},
{rectangleAround2020, resizeByHalfAroundMin, pixel.R(0, 10, 20, 20)},
{squareAroundOrigin, resizeByHalfAroundMax, pixel.R(0, 0, 10, 10)},
{squareAround2020, resizeByHalfAroundMax, pixel.R(20, 20, 30, 30)},
{rectangleAroundOrigin, resizeByHalfAroundMax, pixel.R(0, 0, 20, 10)},
{rectangleAround2020, resizeByHalfAroundMax, pixel.R(20, 20, 40, 30)},
{squareAroundOrigin, resizeByHalfAroundMiddleOfLeftSide, pixel.R(-10, -5, 0, 5)},
{squareAround2020, resizeByHalfAroundMiddleOfLeftSide, pixel.R(10, 15, 20, 25)},
{rectangleAroundOrigin, resizeByHalfAroundMiddleOfLeftSide, pixel.R(-20, -5, 0, 5)},
{rectangleAround2020, resizeByHalfAroundMiddleOfLeftSide, pixel.R(0, 15, 20, 25)},
{squareAroundOrigin, resizeByHalfAroundOrigin, pixel.R(-5, -5, 5, 5)},
{squareAround2020, resizeByHalfAroundOrigin, pixel.R(5, 5, 15, 15)},
{rectangleAroundOrigin, resizeByHalfAroundOrigin, pixel.R(-10, -5, 10, 5)},
{rectangleAround2020, resizeByHalfAroundOrigin, pixel.R(0, 5, 20, 15)},
}
for _, testCase := range testCases {
t.Run(fmt.Sprintf("Resize %v %s", testCase.input, testCase.transform.name), func(t *testing.T) {
testResult := testCase.transform.f(testCase.input)
if testResult != testCase.answer {
t.Errorf("Got: %v, wanted: %v\n", testResult, testCase.answer)
}
})
}
}

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

@ -128,7 +128,9 @@ func (imd *IMDraw) Draw(t pixel.Target) {
// Push adds some points to the IM queue. All Pushed points will have the same properties except for // Push adds some points to the IM queue. All Pushed points will have the same properties except for
// the position. // the position.
func (imd *IMDraw) Push(pts ...pixel.Vec) { func (imd *IMDraw) Push(pts ...pixel.Vec) {
// Assert that Color is of type pixel.RGBA,
if _, ok := imd.Color.(pixel.RGBA); !ok { if _, ok := imd.Color.(pixel.RGBA); !ok {
// otherwise cast it
imd.Color = pixel.ToRGBA(imd.Color) imd.Color = pixel.ToRGBA(imd.Color)
} }
opts := point{ opts := point{

View File

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

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

View File

@ -1,10 +1,13 @@
package pixel_test package pixel_test
import ( import (
"fmt"
"math"
"math/rand" "math/rand"
"testing" "testing"
"github.com/faiface/pixel" "github.com/faiface/pixel"
"github.com/stretchr/testify/assert"
) )
func BenchmarkMatrix(b *testing.B) { func BenchmarkMatrix(b *testing.B) {
@ -61,3 +64,86 @@ func BenchmarkMatrix(b *testing.B) {
} }
}) })
} }
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

@ -17,7 +17,7 @@ import (
// It supports TrianglesPosition, TrianglesColor, TrianglesPicture and PictureColor. // It supports TrianglesPosition, TrianglesColor, TrianglesPicture and PictureColor.
type Canvas struct { type Canvas struct {
gf *GLFrame gf *GLFrame
shader *glShader shader *GLShader
cmp pixel.ComposeMethod cmp pixel.ComposeMethod
mat mgl32.Mat3 mat mgl32.Mat3
@ -37,9 +37,8 @@ func NewCanvas(bounds pixel.Rect) *Canvas {
col: mgl32.Vec4{1, 1, 1, 1}, col: mgl32.Vec4{1, 1, 1, 1},
} }
baseShader(c) c.shader = NewGLShader(baseCanvasFragmentShader)
c.SetBounds(bounds) c.SetBounds(bounds)
c.shader.update()
return c return c
} }
@ -47,22 +46,28 @@ func NewCanvas(bounds pixel.Rect) *Canvas {
// attribute variable. If the uniform already exists, including defaults, they will be reassigned // attribute variable. If the uniform already exists, including defaults, they will be reassigned
// to the new value. The value can be a pointer. // to the new value. The value can be a pointer.
func (c *Canvas) SetUniform(name string, value interface{}) { func (c *Canvas) SetUniform(name string, value interface{}) {
c.shader.setUniform(name, value) c.shader.SetUniform(name, value)
} }
// SetFragmentShader allows you to set a new fragment shader on the underlying // SetFragmentShader allows you to set a new fragment shader on the underlying
// framebuffer. Argument "src" is the GLSL source, not a filename. // framebuffer. Argument "src" is the GLSL source, not a filename.
func (c *Canvas) SetFragmentShader(src string) { func (c *Canvas) SetFragmentShader(src string) {
c.shader.fs = src c.shader.fs = src
c.shader.update() c.shader.Update()
} }
// MakeTriangles creates a specialized copy of the supplied Triangles that draws onto this Canvas. // MakeTriangles creates a specialized copy of the supplied Triangles that draws onto this Canvas.
// //
// TrianglesPosition, TrianglesColor and TrianglesPicture are supported. // TrianglesPosition, TrianglesColor and TrianglesPicture are supported.
func (c *Canvas) MakeTriangles(t pixel.Triangles) pixel.TargetTriangles { func (c *Canvas) MakeTriangles(t pixel.Triangles) pixel.TargetTriangles {
if gt, ok := t.(*GLTriangles); ok {
return &canvasTriangles{
GLTriangles: gt,
dst: c,
}
}
return &canvasTriangles{ return &canvasTriangles{
GLTriangles: NewGLTriangles(c.shader.s, t), GLTriangles: NewGLTriangles(c.shader, t),
dst: c, dst: c,
} }
} }
@ -128,7 +133,7 @@ func (c *Canvas) SetBounds(bounds pixel.Rect) {
c.sprite = pixel.NewSprite(nil, pixel.Rect{}) c.sprite = pixel.NewSprite(nil, pixel.Rect{})
} }
c.sprite.Set(c, c.Bounds()) c.sprite.Set(c, c.Bounds())
//c.sprite.SetMatrix(pixel.IM.Moved(c.Bounds().Center())) // c.sprite.SetMatrix(pixel.IM.Moved(c.Bounds().Center()))
} }
// Bounds returns the rectangular bounds of the Canvas. // Bounds returns the rectangular bounds of the Canvas.
@ -288,15 +293,15 @@ func (ct *canvasTriangles) draw(tex *glhf.Texture, bounds pixel.Rect) {
setBlendFunc(cmp) setBlendFunc(cmp)
frame := ct.dst.gf.Frame() frame := ct.dst.gf.Frame()
shader := ct.dst.shader.s shader := ct.shader.s
frame.Begin() frame.Begin()
shader.Begin() shader.Begin()
ct.dst.shader.uniformDefaults.transform = mat ct.shader.uniformDefaults.transform = mat
ct.dst.shader.uniformDefaults.colormask = col ct.shader.uniformDefaults.colormask = col
dstBounds := ct.dst.Bounds() dstBounds := ct.dst.Bounds()
ct.dst.shader.uniformDefaults.bounds = mgl32.Vec4{ ct.shader.uniformDefaults.bounds = mgl32.Vec4{
float32(dstBounds.Min.X), float32(dstBounds.Min.X),
float32(dstBounds.Min.Y), float32(dstBounds.Min.Y),
float32(dstBounds.W()), float32(dstBounds.W()),
@ -304,15 +309,15 @@ func (ct *canvasTriangles) draw(tex *glhf.Texture, bounds pixel.Rect) {
} }
bx, by, bw, bh := intBounds(bounds) bx, by, bw, bh := intBounds(bounds)
ct.dst.shader.uniformDefaults.texbounds = mgl32.Vec4{ ct.shader.uniformDefaults.texbounds = mgl32.Vec4{
float32(bx), float32(bx),
float32(by), float32(by),
float32(bw), float32(bw),
float32(bh), float32(bh),
} }
for loc, u := range ct.dst.shader.uniforms { for loc, u := range ct.shader.uniforms {
ct.dst.shader.s.SetUniformAttr(loc, u.Value()) ct.shader.s.SetUniformAttr(loc, u.Value())
} }
if tex == nil { if tex == nil {
@ -354,17 +359,3 @@ func (cp *canvasPicture) Draw(t pixel.TargetTriangles) {
} }
ct.draw(cp.GLPicture.Texture(), cp.GLPicture.Bounds()) ct.draw(cp.GLPicture.Texture(), cp.GLPicture.Bounds())
} }
const (
canvasPosition int = iota
canvasColor
canvasTexCoords
canvasIntensity
)
var defaultCanvasVertexFormat = glhf.AttrFormat{
canvasPosition: {Name: "aPosition", Type: glhf.Vec2},
canvasColor: {Name: "aColor", Type: glhf.Vec4},
canvasTexCoords: {Name: "aTexCoords", Type: glhf.Vec2},
canvasIntensity: {Name: "aIntensity", Type: glhf.Float},
}

View File

@ -7,10 +7,10 @@ import (
"github.com/pkg/errors" "github.com/pkg/errors"
) )
// glShader is a type to assist with managing a canvas's underlying // GLShader is a type to assist with managing a canvas's underlying
// shader configuration. This allows for customization of shaders on // shader configuration. This allows for customization of shaders on
// a per canvas basis. // a per canvas basis.
type glShader struct { type GLShader struct {
s *glhf.Shader s *glhf.Shader
vf, uf glhf.AttrFormat vf, uf glhf.AttrFormat
vs, fs string vs, fs string
@ -22,6 +22,7 @@ type glShader struct {
colormask mgl32.Vec4 colormask mgl32.Vec4
bounds mgl32.Vec4 bounds mgl32.Vec4
texbounds mgl32.Vec4 texbounds mgl32.Vec4
cliprect mgl32.Vec4
} }
} }
@ -32,15 +33,52 @@ type gsUniformAttr struct {
ispointer bool ispointer bool
} }
// reinitialize GLShader data and recompile the underlying gl shader object const (
func (gs *glShader) update() { canvasPosition int = iota
gs.uf = nil canvasColor
for _, u := range gs.uniforms { canvasTexCoords
gs.uf = append(gs.uf, glhf.Attr{ canvasIntensity
Name: u.Name, canvasClip
Type: u.Type, )
})
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 var shader *glhf.Shader
mainthread.Call(func() { mainthread.Call(func() {
var err error var err error
@ -59,7 +97,7 @@ func (gs *glShader) update() {
} }
// gets the uniform index from GLShader // gets the uniform index from GLShader
func (gs *glShader) getUniform(Name string) int { func (gs *GLShader) getUniform(Name string) int {
for i, u := range gs.uniforms { for i, u := range gs.uniforms {
if u.Name == Name { if u.Name == Name {
return i return i
@ -75,7 +113,7 @@ func (gs *glShader) getUniform(Name string) int {
// //
// utime := float32(time.Since(starttime)).Seconds()) // utime := float32(time.Since(starttime)).Seconds())
// mycanvas.shader.AddUniform("u_time", &utime) // mycanvas.shader.AddUniform("u_time", &utime)
func (gs *glShader) setUniform(name string, value interface{}) { func (gs *GLShader) SetUniform(name string, value interface{}) {
t, p := getAttrType(value) t, p := getAttrType(value)
if loc := gs.getUniform(name); loc > -1 { if loc := gs.getUniform(name); loc > -1 {
gs.uniforms[loc].Name = name gs.uniforms[loc].Name = name
@ -92,24 +130,6 @@ func (gs *glShader) setUniform(name string, value interface{}) {
}) })
} }
// 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 baseShader(c *Canvas) {
gs := &glShader{
vf: defaultCanvasVertexFormat,
vs: baseCanvasVertexShader,
fs: baseCanvasFragmentShader,
}
gs.setUniform("uTransform", &gs.uniformDefaults.transform)
gs.setUniform("uColorMask", &gs.uniformDefaults.colormask)
gs.setUniform("uBounds", &gs.uniformDefaults.bounds)
gs.setUniform("uTexBounds", &gs.uniformDefaults.texbounds)
c.shader = gs
}
// Value returns the attribute's concrete value. If the stored value // Value returns the attribute's concrete value. If the stored value
// is a pointer, we return the dereferenced value. // is a pointer, we return the dereferenced value.
func (gu *gsUniformAttr) Value() interface{} { func (gu *gsUniformAttr) Value() interface{} {
@ -222,10 +242,14 @@ in vec2 aPosition;
in vec4 aColor; in vec4 aColor;
in vec2 aTexCoords; in vec2 aTexCoords;
in float aIntensity; in float aIntensity;
in vec4 aClipRect;
in float aIsClipped;
out vec4 vColor; out vec4 vColor;
out vec2 vTexCoords; out vec2 vTexCoords;
out float vIntensity; out float vIntensity;
out vec2 vPosition;
out vec4 vClipRect;
uniform mat3 uTransform; uniform mat3 uTransform;
uniform vec4 uBounds; uniform vec4 uBounds;
@ -234,9 +258,12 @@ void main() {
vec2 transPos = (uTransform * vec3(aPosition, 1.0)).xy; vec2 transPos = (uTransform * vec3(aPosition, 1.0)).xy;
vec2 normPos = (transPos - uBounds.xy) / uBounds.zw * 2 - vec2(1, 1); vec2 normPos = (transPos - uBounds.xy) / uBounds.zw * 2 - vec2(1, 1);
gl_Position = vec4(normPos, 0.0, 1.0); gl_Position = vec4(normPos, 0.0, 1.0);
vColor = aColor; vColor = aColor;
vPosition = aPosition;
vTexCoords = aTexCoords; vTexCoords = aTexCoords;
vIntensity = aIntensity; vIntensity = aIntensity;
vClipRect = aClipRect;
} }
` `
@ -246,6 +273,7 @@ var baseCanvasFragmentShader = `
in vec4 vColor; in vec4 vColor;
in vec2 vTexCoords; in vec2 vTexCoords;
in float vIntensity; in float vIntensity;
in vec4 vClipRect;
out vec4 fragColor; out vec4 fragColor;
@ -254,6 +282,9 @@ uniform vec4 uTexBounds;
uniform sampler2D uTexture; uniform sampler2D uTexture;
void main() { 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) { if (vIntensity == 0) {
fragColor = uColorMask * vColor; fragColor = uColorMask * vColor;
} else { } else {

View File

@ -15,23 +15,43 @@ import (
type GLTriangles struct { type GLTriangles struct {
vs *glhf.VertexSlice vs *glhf.VertexSlice
data []float32 data []float32
shader *glhf.Shader shader *GLShader
} }
var ( var (
_ pixel.TrianglesPosition = (*GLTriangles)(nil) _ pixel.TrianglesPosition = (*GLTriangles)(nil)
_ pixel.TrianglesColor = (*GLTriangles)(nil) _ pixel.TrianglesColor = (*GLTriangles)(nil)
_ pixel.TrianglesPicture = (*GLTriangles)(nil) _ pixel.TrianglesPicture = (*GLTriangles)(nil)
_ pixel.TrianglesClipped = (*GLTriangles)(nil)
)
// The following is a helper so that the indices of
// each of these items is easier to see/debug.
const (
triPosX = iota
triPosY
triColorR
triColorG
triColorB
triColorA
triPicX
triPicY
triIntensity
triClipMinX
triClipMinY
triClipMaxX
triClipMaxY
trisAttrLen
) )
// NewGLTriangles returns GLTriangles initialized with the data from the supplied Triangles. // NewGLTriangles returns GLTriangles initialized with the data from the supplied Triangles.
// //
// Only draw the Triangles using the provided Shader. // Only draw the Triangles using the provided Shader.
func NewGLTriangles(shader *glhf.Shader, t pixel.Triangles) *GLTriangles { func NewGLTriangles(shader *GLShader, t pixel.Triangles) *GLTriangles {
var gt *GLTriangles var gt *GLTriangles
mainthread.Call(func() { mainthread.Call(func() {
gt = &GLTriangles{ gt = &GLTriangles{
vs: glhf.MakeVertexSlice(shader, 0, t.Len()), vs: glhf.MakeVertexSlice(shader.s, 0, t.Len()),
shader: shader, shader: shader,
} }
}) })
@ -48,7 +68,7 @@ func (gt *GLTriangles) VertexSlice() *glhf.VertexSlice {
} }
// Shader returns the GLTriangles's associated shader. // Shader returns the GLTriangles's associated shader.
func (gt *GLTriangles) Shader() *glhf.Shader { func (gt *GLTriangles) Shader() *GLShader {
return gt.shader return gt.shader
} }
@ -70,6 +90,7 @@ func (gt *GLTriangles) SetLen(length int) {
1, 1, 1, 1, 1, 1, 1, 1,
0, 0, 0, 0,
0, 0,
0, 0, 0, 0,
) )
} }
case length < gt.Len(): case length < gt.Len():
@ -77,7 +98,7 @@ func (gt *GLTriangles) SetLen(length int) {
default: default:
return return
} }
mainthread.CallNonBlock(func() { mainthread.Call(func() {
gt.vs.Begin() gt.vs.Begin()
gt.vs.SetLen(length) gt.vs.SetLen(length)
gt.vs.End() gt.vs.End()
@ -110,17 +131,22 @@ func (gt *GLTriangles) updateData(t pixel.Triangles) {
col = (*t)[i].Color col = (*t)[i].Color
tx, ty = (*t)[i].Picture.XY() tx, ty = (*t)[i].Picture.XY()
in = (*t)[i].Intensity in = (*t)[i].Intensity
rec = (*t)[i].ClipRect
) )
d := gt.data[i*stride : i*stride+9] d := gt.data[i*stride : i*stride+trisAttrLen]
d[0] = float32(px) d[triPosX] = float32(px)
d[1] = float32(py) d[triPosY] = float32(py)
d[2] = float32(col.R) d[triColorR] = float32(col.R)
d[3] = float32(col.G) d[triColorG] = float32(col.G)
d[4] = float32(col.B) d[triColorB] = float32(col.B)
d[5] = float32(col.A) d[triColorA] = float32(col.A)
d[6] = float32(tx) d[triPicX] = float32(tx)
d[7] = float32(ty) d[triPicY] = float32(ty)
d[8] = float32(in) d[triIntensity] = float32(in)
d[triClipMinX] = float32(rec.Min.X)
d[triClipMinY] = float32(rec.Min.Y)
d[triClipMaxX] = float32(rec.Max.X)
d[triClipMaxY] = float32(rec.Max.Y)
} }
return return
} }
@ -128,25 +154,34 @@ func (gt *GLTriangles) updateData(t pixel.Triangles) {
if t, ok := t.(pixel.TrianglesPosition); ok { if t, ok := t.(pixel.TrianglesPosition); ok {
for i := 0; i < length; i++ { for i := 0; i < length; i++ {
px, py := t.Position(i).XY() px, py := t.Position(i).XY()
gt.data[i*stride+0] = float32(px) gt.data[i*stride+triPosX] = float32(px)
gt.data[i*stride+1] = float32(py) gt.data[i*stride+triPosY] = float32(py)
} }
} }
if t, ok := t.(pixel.TrianglesColor); ok { if t, ok := t.(pixel.TrianglesColor); ok {
for i := 0; i < length; i++ { for i := 0; i < length; i++ {
col := t.Color(i) col := t.Color(i)
gt.data[i*stride+2] = float32(col.R) gt.data[i*stride+triColorR] = float32(col.R)
gt.data[i*stride+3] = float32(col.G) gt.data[i*stride+triColorG] = float32(col.G)
gt.data[i*stride+4] = float32(col.B) gt.data[i*stride+triColorB] = float32(col.B)
gt.data[i*stride+5] = float32(col.A) gt.data[i*stride+triColorA] = float32(col.A)
} }
} }
if t, ok := t.(pixel.TrianglesPicture); ok { if t, ok := t.(pixel.TrianglesPicture); ok {
for i := 0; i < length; i++ { for i := 0; i < length; i++ {
pic, intensity := t.Picture(i) pic, intensity := t.Picture(i)
gt.data[i*stride+6] = float32(pic.X) gt.data[i*stride+triPicX] = float32(pic.X)
gt.data[i*stride+7] = float32(pic.Y) gt.data[i*stride+triPicY] = float32(pic.Y)
gt.data[i*stride+8] = float32(intensity) 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)
} }
} }
} }
@ -160,6 +195,12 @@ func (gt *GLTriangles) Update(t pixel.Triangles) {
} }
gt.updateData(t) gt.updateData(t)
// 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 // 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 // the data is small enough, otherwise it'll block and not copy the data
if len(gt.data) < 256 { // arbitrary heurestic constant if len(gt.data) < 256 { // arbitrary heurestic constant
@ -185,19 +226,31 @@ func (gt *GLTriangles) Copy() pixel.Triangles {
return NewGLTriangles(gt.shader, gt) return NewGLTriangles(gt.shader, gt)
} }
// index is a helper function that returns the index in the data
// slice given the i-th vertex and the item index.
func (gt *GLTriangles) index(i, idx int) int {
return i*gt.vs.Stride() + idx
}
// Position returns the Position property of the i-th vertex. // Position returns the Position property of the i-th vertex.
func (gt *GLTriangles) Position(i int) pixel.Vec { func (gt *GLTriangles) Position(i int) pixel.Vec {
px := gt.data[i*gt.vs.Stride()+0] px := gt.data[gt.index(i, triPosX)]
py := gt.data[i*gt.vs.Stride()+1] py := gt.data[gt.index(i, triPosY)]
return pixel.V(float64(px), float64(py)) return pixel.V(float64(px), float64(py))
} }
// SetPosition sets the position property of the i-th vertex.
func (gt *GLTriangles) SetPosition(i int, p pixel.Vec) {
gt.data[gt.index(i, triPosX)] = float32(p.X)
gt.data[gt.index(i, triPosY)] = float32(p.Y)
}
// Color returns the Color property of the i-th vertex. // Color returns the Color property of the i-th vertex.
func (gt *GLTriangles) Color(i int) pixel.RGBA { func (gt *GLTriangles) Color(i int) pixel.RGBA {
r := gt.data[i*gt.vs.Stride()+2] r := gt.data[gt.index(i, triColorR)]
g := gt.data[i*gt.vs.Stride()+3] g := gt.data[gt.index(i, triColorG)]
b := gt.data[i*gt.vs.Stride()+4] b := gt.data[gt.index(i, triColorB)]
a := gt.data[i*gt.vs.Stride()+5] a := gt.data[gt.index(i, triColorA)]
return pixel.RGBA{ return pixel.RGBA{
R: float64(r), R: float64(r),
G: float64(g), G: float64(g),
@ -206,10 +259,44 @@ func (gt *GLTriangles) Color(i int) pixel.RGBA {
} }
} }
// SetColor sets the color property of the i-th vertex.
func (gt *GLTriangles) SetColor(i int, c pixel.RGBA) {
gt.data[gt.index(i, triColorR)] = float32(c.R)
gt.data[gt.index(i, triColorG)] = float32(c.G)
gt.data[gt.index(i, triColorB)] = float32(c.B)
gt.data[gt.index(i, triColorA)] = float32(c.A)
}
// Picture returns the Picture property of the i-th vertex. // Picture returns the Picture property of the i-th vertex.
func (gt *GLTriangles) Picture(i int) (pic pixel.Vec, intensity float64) { func (gt *GLTriangles) Picture(i int) (pic pixel.Vec, intensity float64) {
tx := gt.data[i*gt.vs.Stride()+6] tx := gt.data[gt.index(i, triPicX)]
ty := gt.data[i*gt.vs.Stride()+7] ty := gt.data[gt.index(i, triPicY)]
intensity = float64(gt.data[i*gt.vs.Stride()+8]) intensity = float64(gt.data[gt.index(i, triIntensity)])
return pixel.V(float64(tx), float64(ty)), intensity return pixel.V(float64(tx), float64(ty)), intensity
} }
// SetPicture sets the picture property of the i-th vertex.
func (gt *GLTriangles) SetPicture(i int, pic pixel.Vec, intensity float64) {
gt.data[gt.index(i, triPicX)] = float32(pic.X)
gt.data[gt.index(i, triPicY)] = float32(pic.Y)
gt.data[gt.index(i, triIntensity)] = float32(intensity)
}
// ClipRect returns the Clipping rectangle property of the i-th vertex.
func (gt *GLTriangles) ClipRect(i int) (rect pixel.Rect, is bool) {
mx := gt.data[gt.index(i, triClipMinX)]
my := gt.data[gt.index(i, triClipMinY)]
ax := gt.data[gt.index(i, triClipMaxX)]
ay := gt.data[gt.index(i, triClipMaxY)]
rect = pixel.R(float64(mx), float64(my), float64(ax), float64(ay))
is = rect.Area() != 0.0
return
}
// SetClipRect sets the Clipping rectangle property of the i-th vertex.
func (gt *GLTriangles) SetClipRect(i int, rect pixel.Rect) {
gt.data[gt.index(i, triClipMinX)] = float32(rect.Min.X)
gt.data[gt.index(i, triClipMinY)] = float32(rect.Min.Y)
gt.data[gt.index(i, triClipMaxX)] = float32(rect.Max.X)
gt.data[gt.index(i, triClipMaxY)] = float32(rect.Max.Y)
}

View File

@ -1,9 +1,11 @@
package pixelgl package pixelgl
import ( import (
"time"
"github.com/faiface/mainthread" "github.com/faiface/mainthread"
"github.com/faiface/pixel" "github.com/faiface/pixel"
"github.com/go-gl/glfw/v3.2/glfw" "github.com/go-gl/glfw/v3.3/glfw"
) )
// Pressed returns whether the Button is currently pressed down. // Pressed returns whether the Button is currently pressed down.
@ -11,14 +13,14 @@ func (w *Window) Pressed(button Button) bool {
return w.currInp.buttons[button] 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 { 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 { 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. // Repeated returns whether a repeat event has been triggered on button.
@ -33,6 +35,32 @@ func (w *Window) MousePosition() pixel.Vec {
return w.currInp.mouse return w.currInp.mouse
} }
// MousePreviousPosition returns the previous mouse position in the Window's Bounds.
func (w *Window) MousePreviousPosition() pixel.Vec {
return w.prevInp.mouse
}
// SetMousePosition positions the mouse cursor anywhere within the Window's Bounds.
func (w *Window) SetMousePosition(v pixel.Vec) {
mainthread.Call(func() {
if (v.X >= 0 && v.X <= w.bounds.W()) &&
(v.Y >= 0 && v.Y <= w.bounds.H()) {
w.window.SetCursorPos(
v.X+w.bounds.Min.X,
(w.bounds.H()-v.Y)+w.bounds.Min.Y,
)
w.prevInp.mouse = v
w.currInp.mouse = v
w.tempInp.mouse = v
}
})
}
// MouseInsideWindow returns true if the mouse position is within the Window's Bounds.
func (w *Window) MouseInsideWindow() bool {
return w.cursorInsideWindow
}
// MouseScroll returns the mouse scroll amount (in both axes) since the last call to Window.Update. // MouseScroll returns the mouse scroll amount (in both axes) since the last call to Window.Update.
func (w *Window) MouseScroll() pixel.Vec { func (w *Window) MouseScroll() pixel.Vec {
return w.currInp.scroll return w.currInp.scroll
@ -334,8 +362,10 @@ func (w *Window) initInput() {
w.window.SetMouseButtonCallback(func(_ *glfw.Window, button glfw.MouseButton, action glfw.Action, mod glfw.ModifierKey) { w.window.SetMouseButtonCallback(func(_ *glfw.Window, button glfw.MouseButton, action glfw.Action, mod glfw.ModifierKey) {
switch action { switch action {
case glfw.Press: case glfw.Press:
w.tempPressEvents[Button(button)] = true
w.tempInp.buttons[Button(button)] = true w.tempInp.buttons[Button(button)] = true
case glfw.Release: case glfw.Release:
w.tempReleaseEvents[Button(button)] = true
w.tempInp.buttons[Button(button)] = false w.tempInp.buttons[Button(button)] = false
} }
}) })
@ -346,14 +376,20 @@ func (w *Window) initInput() {
} }
switch action { switch action {
case glfw.Press: case glfw.Press:
w.tempPressEvents[Button(key)] = true
w.tempInp.buttons[Button(key)] = true w.tempInp.buttons[Button(key)] = true
case glfw.Release: case glfw.Release:
w.tempReleaseEvents[Button(key)] = true
w.tempInp.buttons[Button(key)] = false w.tempInp.buttons[Button(key)] = false
case glfw.Repeat: case glfw.Repeat:
w.tempInp.repeat[Button(key)] = true w.tempInp.repeat[Button(key)] = true
} }
}) })
w.window.SetCursorEnterCallback(func(_ *glfw.Window, entered bool) {
w.cursorInsideWindow = entered
})
w.window.SetCursorPosCallback(func(_ *glfw.Window, x, y float64) { w.window.SetCursorPosCallback(func(_ *glfw.Window, x, y float64) {
w.tempInp.mouse = pixel.V( w.tempInp.mouse = pixel.V(
x+w.bounds.Min.X, x+w.bounds.Min.X,
@ -378,11 +414,36 @@ func (w *Window) UpdateInput() {
mainthread.Call(func() { mainthread.Call(func() {
glfw.PollEvents() glfw.PollEvents()
}) })
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.prevInp = w.currInp
w.currInp = w.tempInp 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.repeat = [KeyLast + 1]bool{}
w.tempInp.scroll = pixel.ZV w.tempInp.scroll = pixel.ZV
w.tempInp.typed = "" 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 ( import (
"github.com/faiface/mainthread" "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. // Monitor represents a physical display attached to your computer.

View File

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

View File

@ -1,6 +1,7 @@
package pixelgl package pixelgl
import ( import (
"fmt"
"image" "image"
"image/color" "image/color"
"runtime" "runtime"
@ -8,7 +9,8 @@ import (
"github.com/faiface/glhf" "github.com/faiface/glhf"
"github.com/faiface/mainthread" "github.com/faiface/mainthread"
"github.com/faiface/pixel" "github.com/faiface/pixel"
"github.com/go-gl/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" "github.com/pkg/errors"
) )
@ -36,29 +38,59 @@ type WindowConfig struct {
// Bounds specify the bounds of the Window in pixels. // Bounds specify the bounds of the Window in pixels.
Bounds pixel.Rect Bounds pixel.Rect
// Initial window position
Position pixel.Vec
// If set to nil, the Window will be windowed. Otherwise it will be fullscreen on the // If set to nil, the Window will be windowed. Otherwise it will be fullscreen on the
// specified Monitor. // specified Monitor.
Monitor *Monitor Monitor *Monitor
// Whether the Window is resizable. // Resizable specifies whether the window will be resizable by the user.
Resizable bool Resizable bool
// Undecorated Window ommits the borders and decorations (close button, etc.). // Undecorated Window omits the borders and decorations (close button, etc.).
Undecorated bool Undecorated bool
// NoIconify specifies whether fullscreen windows should not automatically
// iconify (and restore the previous video mode) on focus loss.
NoIconify bool
// AlwaysOnTop specifies whether the windowed mode window will be floating
// above other regular windows, also called topmost or always-on-top.
// This is intended primarily for debugging purposes and cannot be used to
// implement proper full screen windows.
AlwaysOnTop bool
// TransparentFramebuffer specifies whether the window framebuffer will be
// transparent. If enabled and supported by the system, the window
// framebuffer alpha channel will be used to combine the framebuffer with
// the background. This does not affect window decorations.
TransparentFramebuffer bool
// VSync (vertical synchronization) synchronizes Window's framerate with the framerate of // VSync (vertical synchronization) synchronizes Window's framerate with the framerate of
// the monitor. // the monitor.
VSync bool VSync bool
// Maximized specifies whether the window is maximized.
Maximized bool
// Invisible specifies whether the window will be initially hidden.
// You can make the window visible later using Window.Show().
Invisible bool
//SamplesMSAA specifies the level of MSAA to be used. Must be one of 0, 2, 4, 8, 16. 0 to disable.
SamplesMSAA int
} }
// Window is a window handler. Use this type to manipulate a window (input, drawing, etc.). // Window is a window handler. Use this type to manipulate a window (input, drawing, etc.).
type Window struct { type Window struct {
window *glfw.Window window *glfw.Window
bounds pixel.Rect bounds pixel.Rect
canvas *Canvas canvas *Canvas
vsync bool vsync bool
cursorVisible bool cursorVisible bool
cursorInsideWindow bool
// need to save these to correctly restore a fullscreen window // need to save these to correctly restore a fullscreen window
restore struct { restore struct {
@ -72,6 +104,11 @@ type Window struct {
scroll pixel.Vec scroll pixel.Vec
typed string typed string
} }
pressEvents, tempPressEvents [KeyLast + 1]bool
releaseEvents, tempReleaseEvents [KeyLast + 1]bool
prevJoy, currJoy, tempJoy joystickState
} }
var currWin *Window var currWin *Window
@ -87,6 +124,17 @@ func NewWindow(cfg WindowConfig) (*Window, error) {
w := &Window{bounds: cfg.Bounds, cursorVisible: true} 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 { err := mainthread.CallErr(func() error {
var err error var err error
@ -97,6 +145,16 @@ func NewWindow(cfg WindowConfig) (*Window, error) {
glfw.WindowHint(glfw.Resizable, bool2int[cfg.Resizable]) glfw.WindowHint(glfw.Resizable, bool2int[cfg.Resizable])
glfw.WindowHint(glfw.Decorated, bool2int[!cfg.Undecorated]) glfw.WindowHint(glfw.Decorated, bool2int[!cfg.Undecorated])
glfw.WindowHint(glfw.Floating, bool2int[cfg.AlwaysOnTop])
glfw.WindowHint(glfw.AutoIconify, bool2int[!cfg.NoIconify])
glfw.WindowHint(glfw.TransparentFramebuffer, bool2int[cfg.TransparentFramebuffer])
glfw.WindowHint(glfw.Maximized, bool2int[cfg.Maximized])
glfw.WindowHint(glfw.Visible, bool2int[!cfg.Invisible])
glfw.WindowHint(glfw.Samples, cfg.SamplesMSAA)
if cfg.Position.X != 0 || cfg.Position.Y != 0 {
glfw.WindowHint(glfw.Visible, glfw.False)
}
var share *glfw.Window var share *glfw.Window
if currWin != nil { if currWin != nil {
@ -114,9 +172,15 @@ func NewWindow(cfg WindowConfig) (*Window, error) {
return err return err
} }
if cfg.Position.X != 0 || cfg.Position.Y != 0 {
w.window.SetPos(int(cfg.Position.X), int(cfg.Position.Y))
w.window.Show()
}
// enter the OpenGL context // enter the OpenGL context
w.begin() w.begin()
glhf.Init() glhf.Init()
gl.Enable(gl.MULTISAMPLE)
w.end() w.end()
return nil return nil
@ -158,6 +222,24 @@ func (w *Window) Destroy() {
// Update swaps buffers and polls events. Call this method at the end of each frame. // Update swaps buffers and polls events. Call this method at the end of each frame.
func (w *Window) Update() { func (w *Window) Update() {
w.SwapBuffers()
w.UpdateInput()
}
// ClipboardText returns the current value of the systems clipboard.
func (w *Window) ClipboardText() string {
return w.window.GetClipboardString()
}
// SetClipboardText passes the given string to the underlying glfw window to set the
// systems clipboard.
func (w *Window) SetClipboardText(text string) {
w.window.SetClipboardString(text)
}
// SwapBuffers swaps buffers. Call this to swap buffers without polling window events.
// Note that Update invokes SwapBuffers.
func (w *Window) SwapBuffers() {
mainthread.Call(func() { mainthread.Call(func() {
_, _, oldW, oldH := intBounds(w.bounds) _, _, oldW, oldH := intBounds(w.bounds)
newW, newH := w.window.GetSize() newW, newH := w.window.GetSize()
@ -192,8 +274,6 @@ func (w *Window) Update() {
w.window.SwapBuffers() w.window.SwapBuffers()
w.end() w.end()
}) })
w.UpdateInput()
} }
// SetClosed sets the closed flag of the Window. // SetClosed sets the closed flag of the Window.
@ -354,6 +434,15 @@ func (w *Window) SetCursorVisible(visible bool) {
}) })
} }
// 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. // CursorVisible returns the visibility status of the mouse cursor.
func (w *Window) CursorVisible() bool { func (w *Window) CursorVisible() bool {
return w.cursorVisible return w.cursorVisible
@ -429,3 +518,27 @@ func (w *Window) Color(at pixel.Vec) pixel.RGBA {
func (w *Window) Canvas() *Canvas { func (w *Window) Canvas() *Canvas {
return w.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)
}
})
}
}

View File

@ -27,7 +27,7 @@ func NewSprite(pic Picture, frame Rect) *Sprite {
tri := MakeTrianglesData(6) tri := MakeTrianglesData(6)
s := &Sprite{ s := &Sprite{
tri: tri, tri: tri,
d: Drawer{Triangles: tri}, d: Drawer{Triangles: tri, Cached: true},
} }
s.matrix = IM s.matrix = IM
s.mask = Alpha(1) s.mask = Alpha(1)
@ -44,6 +44,13 @@ func (s *Sprite) Set(pic Picture, frame Rect) {
} }
} }
// SetCached makes the sprite cache all the
// incoming pictures if the argument is true, and
// doesn't make it do that if the argument is false.
func (s *Sprite) SetCached(cached bool) {
s.d.Cached = cached
}
// Picture returns the current Sprite's Picture. // Picture returns the current Sprite's Picture.
func (s *Sprite) Picture() Picture { func (s *Sprite) Picture() Picture {
return s.d.Picture return s.d.Picture
@ -105,12 +112,7 @@ func (s *Sprite) calcData() {
(*s.tri)[i].Color = s.mask (*s.tri)[i].Color = s.mask
(*s.tri)[i].Picture = center.Add((*s.tri)[i].Position) (*s.tri)[i].Picture = center.Add((*s.tri)[i].Position)
(*s.tri)[i].Intensity = 1 (*s.tri)[i].Intensity = 1
}
// matrix and mask
for i := range *s.tri {
(*s.tri)[i].Position = s.matrix.Project((*s.tri)[i].Position) (*s.tri)[i].Position = s.matrix.Project((*s.tri)[i].Position)
(*s.tri)[i].Color = s.mask
} }
s.d.Dirty() s.d.Dirty()

View File

@ -1,7 +1,6 @@
package text package text
import ( import (
"fmt"
"image" "image"
"image/draw" "image/draw"
"sort" "sort"
@ -60,8 +59,9 @@ func NewAtlas(face font.Face, runeSets ...[]rune) *Atlas {
)) ))
for r, fg := range fixedMapping { for r, fg := range fixedMapping {
dr, mask, maskp, _, _ := face.Glyph(fg.dot, r) if dr, mask, maskp, _, ok := face.Glyph(fg.dot, r); ok {
draw.Draw(atlasImg, dr, mask, maskp, draw.Src) draw.Draw(atlasImg, dr, mask, maskp, draw.Src)
}
} }
bounds := pixel.R( bounds := pixel.R(
@ -203,7 +203,6 @@ func makeMapping(face font.Face, runes []rune, padding, width fixed.Int26_6) (ma
for _, r := range runes { for _, r := range runes {
b, advance, ok := face.GlyphBounds(r) b, advance, ok := face.GlyphBounds(r)
if !ok { if !ok {
fmt.Println(r)
continue continue
} }

View File

@ -4,6 +4,7 @@ import (
"testing" "testing"
"github.com/faiface/pixel/text" "github.com/faiface/pixel/text"
"golang.org/x/image/font/inconsolata"
) )
func TestAtlas7x13(t *testing.T) { func TestAtlas7x13(t *testing.T) {
@ -22,3 +23,7 @@ func TestAtlas7x13(t *testing.T) {
} }
} }
} }
func TestAtlasInconsolata(t *testing.T) {
text.NewAtlas(inconsolata.Regular8x16, text.ASCII)
}

View File

@ -101,6 +101,7 @@ type Text struct {
trans pixel.TrianglesData trans pixel.TrianglesData
transD pixel.Drawer transD pixel.Drawer
dirty bool dirty bool
anchor pixel.Anchor
} }
// New creates a new Text capable of drawing runes contained in the provided Atlas. Orig and Dot // New creates a new Text capable of drawing runes contained in the provided Atlas. Orig and Dot
@ -135,6 +136,7 @@ func New(orig pixel.Vec, atlas *Atlas) *Text {
txt.transD.Picture = txt.atlas.pic txt.transD.Picture = txt.atlas.pic
txt.transD.Triangles = &txt.trans txt.transD.Triangles = &txt.trans
txt.transD.Cached = true
txt.Clear() txt.Clear()
@ -182,6 +184,12 @@ func (txt *Text) BoundsOf(s string) pixel.Rect {
return bounds 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. // Clear removes all written text from the Text. The Dot field is reset to Orig.
func (txt *Text) Clear() { func (txt *Text) Clear() {
txt.prevR = -1 txt.prevR = -1
@ -244,6 +252,10 @@ func (txt *Text) DrawColorMask(t pixel.Target, matrix pixel.Matrix, mask color.C
txt.mat = matrix txt.mat = matrix
txt.dirty = true 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 { if mask == nil {
mask = pixel.Alpha(1) mask = pixel.Alpha(1)
} }

View File

@ -76,7 +76,7 @@ func BenchmarkTextWrite(b *testing.B) {
b.Run(fmt.Sprintf("%d", len(chunk)), func(b *testing.B) { b.Run(fmt.Sprintf("%d", len(chunk)), func(b *testing.B) {
txt := text.New(pixel.ZV, atlas) txt := text.New(pixel.ZV, atlas)
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
txt.Write(chunk) _, _ = txt.Write(chunk)
} }
}) })
} }

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