Compare commits

...

218 Commits

Author SHA1 Message Date
faiface 4b7553cd73 add maze generator community example 2017-05-30 13:30:09 +02:00
faiface 781c44f119 add text tutorial link to README 2017-05-30 02:53:24 +02:00
faiface c385b247b3 add 07 guide code 2017-05-30 02:52:33 +02:00
faiface 3706d040ce fix typo in doc 2017-05-28 18:50:56 +02:00
faiface 4749e3ee7e add Canvas.Frame method 2017-05-28 18:44:30 +02:00
faiface e06acda99b minorly simplify typewriter example 2017-05-28 00:06:29 +02:00
faiface 9e0c65d8dd remove profiling from typewriter example 2017-05-27 19:19:31 +02:00
faiface bbeab0aebf update 06 guide code 2017-05-26 21:26:06 +02:00
faiface d5f7088b7d update 05 guide code 2017-05-26 14:04:44 +02:00
faiface 9a401948ae remove accidentaly set theme 2017-05-25 22:51:17 +02:00
Michal Štrba abc99bdef8 Set theme jekyll-theme-time-machine 2017-05-25 20:03:02 +02:00
faiface debdbea894 limit ttf face glyphcacheentries size in typewriter example 2017-05-25 15:21:28 +02:00
faiface fc30e51016 update 04 guide code 2017-05-24 22:05:01 +02:00
faiface 90432a7857 update drawing features in README 2017-05-24 20:39:11 +02:00
faiface 51c32e407f update 03 guide code 2017-05-24 20:34:46 +02:00
faiface 659dc6fd5f update 02 guide code 2017-05-24 16:16:59 +02:00
faiface 50ba35d4cb add text input mention to README 2017-05-24 14:42:00 +02:00
faiface 9102076f1b add text drawing feature mention to README 2017-05-24 14:36:18 +02:00
faiface 2834411318 fix link in README 2017-05-24 14:34:49 +02:00
faiface 7fe45b5a88 replace xor screenshot with typewriter screenshot in README 2017-05-24 14:33:50 +02:00
faiface 51e1843f4b remove debug print from typewriter example 2017-05-24 02:21:25 +02:00
faiface eab5be6207 fiddle with constants in typewriter example 2017-05-24 00:01:33 +02:00
faiface 62fa797088 clarify typewriter doc 2017-05-23 22:06:43 +02:00
faiface 970faf0b63 clarify typewriter example readme 2017-05-23 20:40:45 +02:00
faiface 6c269fc8a5 add typewriter example 2017-05-23 20:07:23 +02:00
faiface a794d27972 clarify Rect doc 2017-05-21 19:38:21 +02:00
faiface bc08b65073 fix typo in ToRGBA doc 2017-05-21 19:33:01 +02:00
faiface bf6e20a04b fix Matrix doc 2017-05-21 19:30:29 +02:00
faiface ecdd8462bb replace complex128 Vec with a struct 2017-05-21 19:25:06 +02:00
faiface 3af9c2b20e remove Text.SetMatrix and Text.SetColorMask, add Text.Draw(target, matrix) and Text.DrawColorMask(target, matrix, mask) 2017-05-21 18:23:51 +02:00
faiface 3ae612a84d minor change 2017-05-21 18:23:20 +02:00
faiface 0ac5371d7e update Sprite's doc 2017-05-21 15:31:07 +02:00
faiface fccedc5a9d remove Text.Matrix() and Text.ColorMask() getters 2017-05-19 01:58:34 +02:00
faiface f7aac5ed09 change Text properties to fields 2017-05-18 23:59:42 +02:00
faiface 1d928485d6 change IMDraw properties to fields 2017-05-18 23:50:45 +02:00
faiface b832e83517 change Sprite.Draw and Canvas.Draw signatures (include Matrix) 2017-05-17 23:45:22 +02:00
faiface 53167788d6 accept zero target size in Rect.Resized 2017-05-15 01:15:16 +02:00
faiface 9d60c5fa32 improve Atlas creation time 2-3 times 2017-05-11 19:48:43 +02:00
faiface cce26f0a51 add Window.Repeat 2017-05-10 23:54:06 +02:00
faiface b15c10298e fix and simplify input handling in Window 2017-05-10 21:22:47 +02:00
faiface 3a14aae310 add Window.Typed 2017-05-10 21:10:10 +02:00
faiface e86120db20 change text.New to take an Atlas 2017-05-10 17:56:09 +02:00
faiface a510048648 add text package doc 2017-05-09 16:48:26 +02:00
faiface feb12a1c7e add Text.Matrix and Text.ColorMask 2017-05-09 16:46:11 +02:00
faiface 3ffbbb9cda add examples on LineHeight and TabWidth to Text doc 2017-05-09 16:40:44 +02:00
faiface 863e1e2f0c add note about not destroying face.Face to Atlas doc 2017-05-09 16:39:03 +02:00
faiface e3268db31e mention control characters in Text doc 2017-05-09 16:36:59 +02:00
faiface 7b10ad8497 remove accidental markdown formating from Text doc 2017-05-09 16:35:51 +02:00
faiface abcdff5960 fix typo in Text doc 2017-05-09 16:34:54 +02:00
faiface dc3f9857d8 minor change in Text doc 2017-05-09 16:34:13 +02:00
faiface dd6d38b8f3 minor change in Atlas doc 2017-05-09 16:32:29 +02:00
faiface 46122dd826 clarify doc 2017-05-09 16:31:09 +02:00
faiface c8114d8467 add Text doc 2017-05-09 16:27:55 +02:00
faiface f58676289a minor change 2017-05-09 15:26:50 +02:00
faiface 80735cfc0c add Atlas doc 2017-05-09 15:25:08 +02:00
faiface dcdc812af5 don't cache kerning in Atlas (too expensive and no benefit) 2017-05-09 15:10:35 +02:00
faiface 248d68f6aa improve Atlas creation, atlas is now square picture (was one row of characters) 2017-05-09 14:20:34 +02:00
faiface 3841afb70f fix PictureDataFromImage (wrong bounds when Min not (0, 0)) 2017-05-09 01:04:04 +02:00
faiface 217ac0c4d7 rename Text.LineHeight(scale) -> height, since now it's absolute height 2017-05-07 22:03:56 +02:00
faiface 65236863fe rename Glyph.Orig -> Dot 2017-05-07 21:49:26 +02:00
faiface cfaff8c0cb minor change 2017-05-07 21:12:48 +02:00
faiface 2d1f61f746 improve Text code 2017-05-07 21:08:10 +02:00
faiface 883bdc32c7 add Text.BoundsOf 2017-05-07 21:00:19 +02:00
faiface c9eea2639e restructure Text writing for more flexibility and consistency 2017-05-07 20:59:56 +02:00
faiface 5b524dadd8 add Atlas.DrawRune 2017-05-07 20:59:41 +02:00
faiface a86876a1cd Merge branch 'dev' into text 2017-05-06 22:58:36 +02:00
faiface 29785fb937 IMDraw: change default point color to (1, 1, 1, 1) (was (0, 0, 0, 0)) 2017-05-06 22:57:55 +02:00
faiface 1d17e45825 fix Text.Bounds 2017-05-05 16:42:40 +02:00
faiface b91d8be6cd remove Batch from Text and optimize it 2017-05-05 16:13:26 +02:00
faiface c47d77b2b5 add Text.Bounds, Atlas.Ascent, Atlas.Descent 2017-05-05 16:02:47 +02:00
faiface e9a3c900cf add Rect.Union 2017-05-05 15:43:24 +02:00
faiface 3d3cbd6027 fix Text.Color 2017-05-04 22:30:18 +02:00
faiface 4e6d6eeb3a move Atlas type to a separate file 2017-05-03 23:59:37 +02:00
faiface 17ddf4fec5 change Text.LineHeight to use actual units instead of scale (such as 1.5) 2017-05-03 23:57:09 +02:00
faiface e1e1815537 add Text.Atlas (Atlas has some useful stuff, e.g. line height) 2017-05-03 23:55:44 +02:00
faiface a05abdca76 add Text.WriteByte 2017-05-03 23:55:10 +02:00
faiface 317124058e export Atlas from text package + add Text.WriteRune and Text.WriteString 2017-05-03 23:54:24 +02:00
faiface 3b1e0eaa21 add Text.SetMatrix and Text.SetColorMask 2017-05-03 21:48:05 +02:00
faiface 6a7500959f add Text.LineHeight and text.TabWidth 2017-05-03 21:04:18 +02:00
faiface e2a16764c4 add Text.Orig (start of the text) 2017-05-03 20:56:06 +02:00
faiface 424b6e0f0b optimize Text.Write (remove one allocation) 2017-05-03 00:13:39 +02:00
faiface bc145eb1b8 add incomplete Text type in text package 2017-05-02 22:46:51 +02:00
faiface 5303ec5648 optimize Drawer.Dirty (defer the hard part until drawing) 2017-05-02 22:46:27 +02:00
faiface f7dad2f3c2 strikethrough advanced window manipulation mssing feature 2017-05-02 01:15:33 +02:00
faiface 3b5bfa90e6 fix creating window with no icon 2017-05-01 12:18:23 +02:00
faiface dff622523b just align doc comment to 100 chars per line 2017-05-01 01:38:57 +02:00
Michal Štrba 51a9702fc8 Merge pull request #14 from otraore/set-icon
Add support for setting an icon of a window
2017-05-01 01:33:59 +02:00
Ousmane Traore f7bb304b92 Set value rather than append 2017-04-30 19:30:11 -04:00
Ousmane Traore 5efd04b420 Address review comments 2017-04-30 18:43:05 -04:00
Ousmane Traore ea7bc5aff9 Add Icons paramter to window config 2017-04-30 17:19:51 -04:00
faiface 7b8a0c152e fix Window.CursorVisible intial value (was false) 2017-04-30 20:42:25 +02:00
Michal Štrba 2115296062 Merge pull request #13 from otraore/hide-cursor
Add ability to hide the cursor
2017-04-30 18:50:43 +02:00
Ousmane Traore dc745825d6 Address review comments 2017-04-30 12:33:27 -04:00
Ousmane Traore 29fe3b16ca Add ability to hide the cursor 2017-04-30 10:40:31 -04:00
faiface d37ad8f1ba fix two typos in comments in platformer example 2017-04-28 18:11:29 +02:00
faiface 85ba21a2f4 add Canvas.SetPixels and Canvas.Pixels methods 2017-04-28 13:24:30 +02:00
faiface 915faeee0c merge dev branch 2017-04-26 23:16:34 +02:00
faiface 113d052872 fix link in readme 2017-04-26 23:15:08 +02:00
faiface 9571b4339b add note to readme about macOS, go 1.8 and xcode problems 2017-04-26 23:14:01 +02:00
Michal Štrba 64aa32a4b4 Merge pull request #6 from faiface/dev
merge shader error fix
2017-04-26 18:01:45 +02:00
Michal Štrba 01db742aba Merge pull request #5 from ivanov/fix-4
fix "Attempt to use 'texture' as a variable" shader compiler error
2017-04-26 17:58:39 +02:00
Paul Ivanov ff64cf248b fix for 'texture' as a variable error, closes #4 2017-04-26 08:35:10 -07:00
faiface d595d5f647 fix compiler error (ouch) 2017-04-26 15:11:12 +02:00
faiface 951c7f4c59 remove PictureData.SetColor
was confusing since Pictures in e.g. Sprites are not really updatable
2017-04-26 15:04:46 +02:00
faiface e8aa765ed3 add a note about memory leaks to Drawer and Sprite docs 2017-04-26 14:28:25 +02:00
faiface bdbce3a27b change first sentence in readme 2017-04-24 22:51:43 +02:00
faiface 0389ee7bf9 minor change in readme 2017-04-24 21:00:01 +02:00
faiface 736f4549a9 add contributing section to readme 2017-04-24 20:55:18 +02:00
faiface ef9fd8fe10 minor change in readme 2017-04-24 20:39:18 +02:00
faiface 4246e90215 update example screenshots 2017-04-24 20:07:32 +02:00
faiface 1d0169b2b7 remove fps counters from examples for simplicity and consistency 2017-04-24 19:34:31 +02:00
faiface 2626892e97 adjust gitter badge location 2017-04-24 14:21:09 +02:00
Michal Štrba 1e2eb29a4f Merge pull request #2 from gitter-badger/gitter-badge
Add a Gitter chat badge to README.md
2017-04-24 14:20:12 +02:00
The Gitter Badger b60b7e5207 Add Gitter badge 2017-04-24 12:18:51 +00:00
faiface 8165c38c6d change wording in readme 2017-04-23 22:29:23 +02:00
faiface 82fa73952e put images into 2x2 table in readme 2017-04-23 20:57:36 +02:00
faiface bab33976cc add useful links to readme 2017-04-23 20:35:58 +02:00
faiface 0e54ed1080 add missing feature to readme (advanced window manipulation) 2017-04-23 17:48:54 +02:00
faiface db4f08aefc fix typo in readme 2017-04-23 17:30:03 +02:00
faiface fd6e056f92 add missing feature (tests and benchmarks) to readme 2017-04-23 14:35:56 +02:00
faiface e93b9a1d4c add missing feature (mobile/web backend) to readme 2017-04-23 14:32:41 +02:00
faiface d9a94fd157 update a few sentences in readme 2017-04-23 13:54:53 +02:00
faiface 6e5cbaa493 reorder features in readme 2017-04-23 01:30:15 +02:00
faiface 9e150a7a1b add link to "PixelGL backend" in readme 2017-04-22 23:58:13 +02:00
faiface 9c74e66118 reword a sentence in readme 2017-04-22 23:46:08 +02:00
faiface ee74866587 clarify readme 2017-04-22 23:44:17 +02:00
faiface a2ad3dbf7c fix grammar in readme 2017-04-22 23:36:54 +02:00
faiface af1ccf885f add feature to readme 2017-04-22 23:34:34 +02:00
faiface b756edfac6 reorder features in readme 2017-04-22 23:34:06 +02:00
faiface c69ef58898 add feature to readme 2017-04-22 23:33:35 +02:00
faiface 6e63f27d6e fix typo in readme 2017-04-22 23:29:54 +02:00
faiface acfd836a7f change tutorial part in readme 2017-04-22 23:29:05 +02:00
faiface 8679a702a9 add stunning readme (contributing part missing) 2017-04-22 23:27:54 +02:00
faiface e9380d7eed minor change in platformer example readme 2017-04-22 16:17:33 +02:00
faiface 3ed8c0016c minor change in platformer example readme 2017-04-22 15:56:56 +02:00
faiface 2afe44a9c9 add instructions to platformer readme 2017-04-22 14:22:09 +02:00
faiface c028b66b68 adjust camera movement in platformer example 2017-04-22 14:20:51 +02:00
faiface 080735510c temporarily fix issue #1 2017-04-22 13:15:57 +02:00
faiface 862f30a004 fix screenshot in platformer example readme 2017-04-22 13:11:26 +02:00
faiface c0ddb0b287 add platformer example 2017-04-22 13:09:23 +02:00
faiface 3e493c13e1 remove debug print 2017-04-21 23:10:02 +02:00
faiface f9f61911a7 fix Canvas drawing when bounds don't start at (0, 0) 2017-04-21 23:07:48 +02:00
faiface d30b73fb8b minor change in pixel package doc 2017-04-21 17:01:46 +02:00
faiface 4aea56198f add pixelgl package doc 2017-04-21 17:00:18 +02:00
faiface 591fadaaa5 minor change in pixel package doc 2017-04-21 16:57:36 +02:00
faiface 497df7842b add pixel doc 2017-04-21 16:57:08 +02:00
faiface a403cfe50b remove commented code 2017-04-21 16:43:56 +02:00
faiface bdbba8f3c1 add code for the guide 06 2017-04-21 01:42:50 +02:00
faiface c485793a83 fix drawing non-closed lines in IMDraw 2017-04-21 01:17:29 +02:00
faiface 97158ba502 simplify code in IMDraw 2017-04-16 00:59:07 +02:00
faiface 01ff4230da add IMDraw.Rectangle 2017-04-16 00:01:43 +02:00
faiface 37dcef6ab6 minor change 2017-04-15 21:03:22 +02:00
faiface ab9d608c45 fix typo in Alpha doc 2017-04-15 18:04:03 +02:00
faiface 05da8e6f92 minor change in xor example code 2017-04-15 17:16:57 +02:00
faiface 49c2beeca9 fix typo in xor example code 2017-04-15 17:15:12 +02:00
faiface f4ce166964 clarify xor example readme 2017-04-15 17:14:15 +02:00
faiface 93e9ab79b1 add xor example 2017-04-15 17:10:35 +02:00
faiface 58be215a76 update screenshot in smoke example 2017-04-15 16:05:29 +02:00
faiface ce083a9a9f add smoke example 2017-04-15 16:03:27 +02:00
faiface 2ef17e7a95 change window title + update screenshot in lights example 2017-04-15 14:25:38 +02:00
faiface 80f48bc0b6 minor change in lights example code 2017-04-15 14:21:27 +02:00
faiface 420e83bc61 update lights example code 2017-04-15 14:09:06 +02:00
faiface b51b3e8f38 revert previous commit 2017-04-14 22:46:46 +02:00
faiface 620784b34c fix window title in lights example 2017-04-14 22:45:44 +02:00
faiface 368dab0e7c fix screenshot in lights example readme 2017-04-14 22:41:14 +02:00
faiface 9bfb83861a add screenshot to lights example readme 2017-04-14 22:39:15 +02:00
faiface 4e30fb9f5c add lights screenshot 2017-04-14 22:37:41 +02:00
faiface 11c76944b6 minor change in light example readme 2017-04-14 22:29:03 +02:00
faiface 30af7c6612 add lights example readme 2017-04-14 22:28:30 +02:00
faiface 411ad7c27d add lights example 2017-04-14 22:20:12 +02:00
faiface 37b0f0956b add code for part 05 of the guide 2017-04-14 16:03:07 +02:00
faiface 10684b6add minor change 2017-04-13 20:30:32 +02:00
faiface cf666b5866 immediate-like-mode -> immediate-mode-like 2017-04-13 17:44:28 +02:00
faiface f325092c02 add imdraw package doc comment 2017-04-13 17:41:38 +02:00
faiface 939a76923d simplify code in Rect.Resized 2017-04-13 15:26:48 +02:00
faiface fcbc61c570 fix grammar in Vec doc 2017-04-13 15:18:13 +02:00
faiface e55d490801 fix grammar in Drawer doc 2017-04-13 15:15:17 +02:00
faiface b5a2c12175 remove a bunch of unnecessary Window control methods 2017-04-13 15:03:13 +02:00
faiface 14a01a5522 minor optimization in Sprite 2017-04-12 16:18:25 +02:00
faiface 3276c4e4a1 clarify IMDraw.Draw doc 2017-04-12 16:03:36 +02:00
faiface c61b677fa9 fix Canvas.Draw 2017-04-12 16:02:39 +02:00
faiface c9e0f7262d add Canvas.Draw 2017-04-12 16:00:56 +02:00
faiface a2499cf90d remove accidentaly kept updateLock field from GLTriangles 2017-04-12 11:25:11 +02:00
faiface 6d2aeb64e7 fix type in ComposeMethod doc 2017-04-11 17:28:27 +02:00
faiface 34a6d020a2 add ComposeMethod.Compose 2017-04-11 17:15:02 +02:00
faiface 9b624ae466 move ComposeTarget to separate file 2017-04-11 16:45:56 +02:00
faiface f2a0a19f6e fix race condition in GLTriangles 2017-04-11 15:02:58 +02:00
faiface a555999120 adopt RGB and Alpha 2017-04-10 17:25:56 +02:00
faiface aa2c560d4c rename ComposeDst* -> ComposeR* + add ComposePlus, ComposeCopy 2017-04-10 13:59:16 +02:00
faiface 4eca5f2d1e fix compile error 2017-04-10 00:48:17 +02:00
faiface 4374bb7be1 remove smooth argument from Canvas constructor 2017-04-10 00:47:06 +02:00
faiface 3fa31cdab5 fix drawing onto Canvas 2017-04-10 00:41:56 +02:00
faiface 219559cf20 add Window.SetComposeMethod 2017-04-10 00:41:48 +02:00
faiface 3d3f1c6e11 add Canvas.SetComposeMethod 2017-04-10 00:30:50 +02:00
faiface 58e8b8f892 add missing ComposeDstOver mode 2017-04-10 00:25:17 +02:00
faiface 13b9e6aee5 add ComposeTarget interface 2017-04-10 00:20:19 +02:00
faiface 9e8697cdf6 fix spelling in doc 2017-04-09 23:19:30 +02:00
faiface 134fe2bf7b use glhf.BlendFunc 2017-04-09 23:16:34 +02:00
faiface 9df8a3761b adjust RGB doc 2017-04-09 23:08:35 +02:00
faiface a081d5b0c8 add RGB and Alpha functions 2017-04-09 23:03:30 +02:00
faiface 32ae09e1e5 replace NRGBA to RGBA because Porter-Duff (!!!) 2017-04-09 22:00:26 +02:00
faiface 317cfb17b0 add code for 04 guide 2017-04-09 15:32:46 +02:00
faiface a9e6f41315 fix obsolete Batch doc 2017-04-09 15:32:31 +02:00
faiface 9cbce8f638 fix typo in Window doc 2017-04-08 18:05:50 +02:00
faiface e3f7901f2c adjust WindowConfig doc, more consistent with the rest 2017-04-07 12:34:16 +02:00
faiface ee19c6b361 fix GLFrame.SetBounds to not reallocate when not necessary 2017-04-05 23:20:55 +02:00
faiface 5ad013b286 fix Matrix.String 2017-04-04 14:10:39 +02:00
faiface 7fdf8c0d60 add Matrix.String 2017-04-04 14:08:37 +02:00
faiface 3a25f78b1a minor change 2017-04-04 14:02:39 +02:00
faiface ad733f5946 update code for 03 guide 2017-04-03 17:38:12 +02:00
faiface 8d5879070f add code for 03_moving_scaling_and_rotating guide 2017-04-03 13:52:52 +02:00
faiface 2155babc5d add Window.Color 2017-04-02 19:08:48 +02:00
faiface dc9afb5557 add code for 02_drawing_a_sprite guide 2017-04-02 19:07:28 +02:00
faiface 79f7f4fb42 split Canvas into Canvas+GLFrame + add GLPicture 2017-04-01 21:54:44 +02:00
faiface 2562f6b754 remove Slice and Original from Canvas 2017-03-31 15:03:06 +02:00
faiface b138fc5d5b make Sprite accept Picture and frame 2017-03-31 15:00:59 +02:00
faiface 15a270e689 remove Slice and Original from Picture interface 2017-03-30 23:34:07 +02:00
faiface 538ad90185 add code for 01_creating_a_window guide 2017-03-27 00:57:13 +02:00
60 changed files with 4083 additions and 906 deletions

152
README.md
View File

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

View File

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

View File

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

65
compose.go Normal file
View File

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

142
data.go
View File

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

7
doc.go Normal file
View File

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

View File

@ -14,7 +14,12 @@ package pixel
// Picture.
//
// Whenever you change the Triangles, call Dirty to notify Drawer that Triangles changed. You don't
// need to notify Drawer about a change of Picture.
// need to notify Drawer about a change of the Picture.
//
// Note, that Drawer caches the results of MakePicture from Targets it's drawn to for each Picture
// it's set to. What it means is that using a Drawer with an unbounded number of Pictures leads to a
// memory leak, since Drawer caches them and never forgets. In such a situation, create a new Drawer
// for each Picture.
type Drawer struct {
Triangles Triangles
Picture Picture
@ -22,6 +27,7 @@ type Drawer struct {
tris map[Target]TargetTriangles
clean map[Target]bool
pics map[targetPicturePair]TargetPicture
dirty bool
inited bool
}
@ -44,9 +50,7 @@ func (d *Drawer) lazyInit() {
func (d *Drawer) Dirty() {
d.lazyInit()
for t := range d.clean {
d.clean[t] = false
}
d.dirty = true
}
// Draw efficiently draws Triangles with Picture onto the provided Target.
@ -56,6 +60,13 @@ func (d *Drawer) Dirty() {
func (d *Drawer) Draw(t Target) {
d.lazyInit()
if d.dirty {
for t := range d.clean {
d.clean[t] = false
}
d.dirty = false
}
if d.Triangles == nil {
return
}
@ -78,12 +89,11 @@ func (d *Drawer) Draw(t Target) {
return
}
pic := d.pics[targetPicturePair{t, d.Picture.Original()}]
pic := d.pics[targetPicturePair{t, d.Picture}]
if pic == nil {
pic = t.MakePicture(d.Picture.Original())
d.pics[targetPicturePair{t, d.Picture.Original()}] = pic
pic = t.MakePicture(d.Picture)
d.pics[targetPicturePair{t, d.Picture}] = pic
}
pic = pic.Slice(d.Picture.Bounds()).(TargetPicture)
pic.Draw(tri)
}

View File

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

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

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

View File

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

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

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

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

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

BIN
examples/lights/noise.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 838 KiB

BIN
examples/lights/panda.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

View File

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

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

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 530 B

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

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 MiB

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

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 381 KiB

View File

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

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

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 156 KiB

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

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

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

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

BIN
examples/xor/screenshot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View File

@ -3,50 +3,38 @@ package pixel
import (
"fmt"
"math"
"math/cmplx"
"github.com/go-gl/mathgl/mgl64"
)
// Vec is a 2D vector type. It is unusually implemented as complex128 for convenience. Since
// Go does not allow operator overloading, implementing vector as a struct leads to a bunch of
// methods for addition, subtraction and multiplication of vectors. With complex128, much of
// this functionality is given through operators.
// Vec is a 2D vector type with X and Y coordinates.
//
// Create vectors with the V constructor:
//
// u := pixel.V(1, 2)
// v := pixel.V(8, -3)
//
// Add and subtract them using the standard + and - operators:
// Use various methods to manipulate them:
//
// w := u + v
// fmt.Println(w) // Vec(9, -1)
// fmt.Println(u - v) // Vec(-7, 5)
//
// Additional standard vector operations can be obtained with methods:
//
// u := pixel.V(2, 3)
// v := pixel.V(8, 1)
// if u.X() < 0 {
// w := u.Add(v)
// fmt.Println(w) // Vec(9, -1)
// fmt.Println(u.Sub(v)) // Vec(-7, 5)
// u = pixel.V(2, 3)
// v = pixel.V(8, 1)
// if u.X < 0 {
// fmt.Println("this won't happen")
// }
// x := u.Unit().Dot(v.Unit())
type Vec complex128
type Vec struct {
X, Y float64
}
// ZV is a zero vector.
var ZV = Vec{0, 0}
// V returns a new 2D vector with the given coordinates.
func V(x, y float64) Vec {
return Vec(complex(x, y))
}
// X returns a 2D vector with coordinates (x, 0).
func X(x float64) Vec {
return V(x, 0)
}
// Y returns a 2D vector with coordinates (0, y).
func Y(y float64) Vec {
return V(0, y)
return Vec{x, y}
}
// String returns the string representation of the vector u.
@ -55,76 +43,75 @@ func Y(y float64) Vec {
// u.String() // returns "Vec(4.5, -1.3)"
// fmt.Println(u) // Vec(4.5, -1.3)
func (u Vec) String() string {
return fmt.Sprintf("Vec(%v, %v)", u.X(), u.Y())
}
// X returns the x coordinate of the vector u.
func (u Vec) X() float64 {
return real(u)
}
// Y returns the y coordinate of the vector u.
func (u Vec) Y() float64 {
return imag(u)
return fmt.Sprintf("Vec(%v, %v)", u.X, u.Y)
}
// XY returns the components of the vector in two return values.
func (u Vec) XY() (x, y float64) {
return real(u), imag(u)
return u.X, u.Y
}
// Len returns the length of the vector u.
func (u Vec) Len() float64 {
return cmplx.Abs(complex128(u))
}
// Angle returns the angle between the vector u and the x-axis. The result is in the range [-Pi, Pi].
func (u Vec) Angle() float64 {
return cmplx.Phase(complex128(u))
}
// Unit returns a vector of length 1 facing the direction of u (has the same angle).
func (u Vec) Unit() Vec {
if u == 0 {
return 1
// Add returns the sum of vectors u and v.
func (u Vec) Add(v Vec) Vec {
return Vec{
u.X + v.X,
u.Y + v.Y,
}
}
// Sub returns the difference betweeen vectors u and v.
func (u Vec) Sub(v Vec) Vec {
return Vec{
u.X - v.X,
u.Y - v.Y,
}
return u / V(u.Len(), 0)
}
// Scaled returns the vector u multiplied by c.
func (u Vec) Scaled(c float64) Vec {
return u * V(c, 0)
return Vec{u.X * c, u.Y * c}
}
// ScaledXY returns the vector u multiplied by the vector v component-wise.
func (u Vec) ScaledXY(v Vec) Vec {
return V(u.X()*v.X(), u.Y()*v.Y())
return Vec{u.X * v.X, u.Y * v.Y}
}
// Len returns the length of the vector u.
func (u Vec) Len() float64 {
return math.Hypot(u.X, u.Y)
}
// Angle returns the angle between the vector u and the x-axis. The result is in range [-Pi, Pi].
func (u Vec) Angle() float64 {
return math.Atan2(u.Y, u.X)
}
// Unit returns a vector of length 1 facing the direction of u (has the same angle).
func (u Vec) Unit() Vec {
if u.X == 0 && u.Y == 0 {
return Vec{1, 0}
}
return u.Scaled(1 / u.Len())
}
// Rotated returns the vector u rotated by the given angle in radians.
func (u Vec) Rotated(angle float64) Vec {
sin, cos := math.Sincos(angle)
return u * V(cos, sin)
}
// WithX return the vector u with the x coordinate changed to the given value.
func (u Vec) WithX(x float64) Vec {
return V(x, u.Y())
}
// WithY returns the vector u with the y coordinate changed to the given value.
func (u Vec) WithY(y float64) Vec {
return V(u.X(), y)
return Vec{
u.X*cos - u.Y*sin,
u.X*sin + u.Y*cos,
}
}
// Dot returns the dot product of vectors u and v.
func (u Vec) Dot(v Vec) float64 {
return u.X()*v.X() + u.Y()*v.Y()
return u.X*v.X + u.Y*v.Y
}
// Cross return the cross product of vectors u and v.
func (u Vec) Cross(v Vec) float64 {
return u.X()*v.Y() - v.X()*u.Y()
return u.X*v.Y - v.X*u.Y
}
// Map applies the function f to both x and y components of the vector u and returns the modified
@ -133,10 +120,10 @@ func (u Vec) Cross(v Vec) float64 {
// u := pixel.V(10.5, -1.5)
// v := u.Map(math.Floor) // v is Vec(10, -2), both components of u floored
func (u Vec) Map(f func(float64) float64) Vec {
return V(
f(u.X()),
f(u.Y()),
)
return Vec{
f(u.X),
f(u.Y),
}
}
// Lerp returns a linear interpolation between vectors a and b.
@ -145,7 +132,7 @@ func (u Vec) Map(f func(float64) float64) Vec {
// If t is 0, then a will be returned, if t is 1, b will be returned. Anything between 0 and 1 will
// return the appropriate point between a and b and so on.
func Lerp(a, b Vec, t float64) Vec {
return a.Scaled(1-t) + b.Scaled(t)
return a.Scaled(1 - t).Add(b.Scaled(t))
}
// Rect is a 2D rectangle aligned with the axes of the coordinate system. It is defined by two
@ -158,6 +145,8 @@ type Rect struct {
}
// R returns a new Rect with given the Min and Max coordinates.
//
// Note that the returned rectangle is not automatically normalized.
func R(minX, minY, maxX, maxY float64) Rect {
return Rect{
Min: V(minX, minY),
@ -165,37 +154,37 @@ func R(minX, minY, maxX, maxY float64) Rect {
}
}
// Norm returns the Rect in normal form, such that Max is component-wise greater or equal than Min.
func (r Rect) Norm() Rect {
return Rect{
Min: V(
math.Min(r.Min.X(), r.Max.X()),
math.Min(r.Min.Y(), r.Max.Y()),
),
Max: V(
math.Max(r.Min.X(), r.Max.X()),
math.Max(r.Min.Y(), r.Max.Y()),
),
}
}
// String returns the string representation of the Rect.
//
// r := pixel.R(100, 50, 200, 300)
// r.String() // returns "Rect(100, 50, 200, 300)"
// fmt.Println(r) // Rect(100, 50, 200, 300)
func (r Rect) String() string {
return fmt.Sprintf("Rect(%v, %v, %v, %v)", r.Min.X(), r.Min.Y(), r.Max.X(), r.Max.Y())
return fmt.Sprintf("Rect(%v, %v, %v, %v)", r.Min.X, r.Min.Y, r.Max.X, r.Max.Y)
}
// Norm returns the Rect in normal form, such that Max is component-wise greater or equal than Min.
func (r Rect) Norm() Rect {
return Rect{
Min: Vec{
math.Min(r.Min.X, r.Max.X),
math.Min(r.Min.Y, r.Max.Y),
},
Max: Vec{
math.Max(r.Min.X, r.Max.X),
math.Max(r.Min.Y, r.Max.Y),
},
}
}
// W returns the width of the Rect.
func (r Rect) W() float64 {
return r.Max.X() - r.Min.X()
return r.Max.X - r.Min.X
}
// H returns the height of the Rect.
func (r Rect) H() float64 {
return r.Max.Y() - r.Min.Y()
return r.Max.Y - r.Min.Y
}
// Size returns the vector of width and height of the Rect.
@ -205,34 +194,14 @@ func (r Rect) Size() Vec {
// Center returns the position of the center of the Rect.
func (r Rect) Center() Vec {
return (r.Min + r.Max) / 2
return Lerp(r.Min, r.Max, 0.5)
}
// Moved returns the Rect moved (both Min and Max) by the given vector delta.
func (r Rect) Moved(delta Vec) Rect {
return Rect{
Min: r.Min + delta,
Max: r.Max + delta,
}
}
// WithMin returns the Rect with it's Min changed to the given position.
//
// Note, that the Rect is not automatically normalized.
func (r Rect) WithMin(min Vec) Rect {
return Rect{
Min: min,
Max: r.Max,
}
}
// WithMax returns the Rect with it's Max changed to the given position.
//
// Note, that the Rect is not automatically normalized.
func (r Rect) WithMax(max Vec) Rect {
return Rect{
Min: r.Min,
Max: max,
Min: r.Min.Add(delta),
Max: r.Max.Add(delta),
}
}
@ -243,16 +212,16 @@ func (r Rect) WithMax(max Vec) Rect {
// r.Resized(r.Max, size) // same with the top-right corner
// r.Resized(r.Center(), size) // resizes around the center
//
// This function does not make sense for sizes of zero area and will panic. Use ResizedMin in the
// case of zero area.
// This function does not make sense for resizing a rectangle of zero area and will panic. Use
// ResizedMin in the case of zero area.
func (r Rect) Resized(anchor, size Vec) Rect {
if r.W()*r.H() == 0 || size.X()*size.Y() == 0 {
if r.W()*r.H() == 0 {
panic(fmt.Errorf("(%T).Resize: zero area", r))
}
fraction := size.ScaledXY(V(1/r.W(), 1/r.H()))
fraction := Vec{size.X / r.W(), size.Y / r.H()}
return Rect{
Min: anchor + (r.Min - anchor).ScaledXY(fraction),
Max: anchor + (r.Max - anchor).ScaledXY(fraction),
Min: anchor.Add(r.Min.Sub(anchor)).ScaledXY(fraction),
Max: anchor.Add(r.Max.Sub(anchor)).ScaledXY(fraction),
}
}
@ -263,13 +232,23 @@ func (r Rect) Resized(anchor, size Vec) Rect {
func (r Rect) ResizedMin(size Vec) Rect {
return Rect{
Min: r.Min,
Max: r.Min + size,
Max: r.Min.Add(size),
}
}
// Contains checks whether a vector u is contained within this Rect (including it's borders).
func (r Rect) Contains(u Vec) bool {
return r.Min.X() <= u.X() && u.X() <= r.Max.X() && r.Min.Y() <= u.Y() && u.Y() <= r.Max.Y()
return r.Min.X <= u.X && u.X <= r.Max.X && r.Min.Y <= u.Y && u.Y <= r.Max.Y
}
// Union returns a minimal Rect which covers both r and s. Rects r and s should 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),
)
}
// Matrix is a 3x3 transformation matrix that can be used for all kinds of spacial transforms, such
@ -278,7 +257,7 @@ func (r Rect) Contains(u Vec) bool {
// Matrix has a handful of useful methods, each of which adds a transformation to the matrix. For
// example:
//
// pixel.IM.Moved(pixel.V(100, 200)).Rotated(0, math.Pi/2)
// pixel.IM.Moved(pixel.V(100, 200)).Rotated(pixel.ZV, math.Pi/2)
//
// This code creates a Matrix that first moves everything by 100 units horizontally and 200 units
// vertically and then rotates everything by 90 degrees around the origin.
@ -287,6 +266,19 @@ type Matrix [9]float64
// IM stands for identity matrix. Does nothing, no transformation.
var IM = Matrix(mgl64.Ident3())
// String returns a string representation of the Matrix.
//
// m := pixel.IM
// fmt.Println(m) // Matrix(1 0 0 | 0 1 0 | 0 0 1)
func (m Matrix) String() string {
return fmt.Sprintf(
"Matrix(%v %v %v | %v %v %v | %v %v %v)",
m[0], m[3], m[6],
m[1], m[4], m[7],
m[2], m[5], m[8],
)
}
// Moved moves everything by the delta vector.
func (m Matrix) Moved(delta Vec) Matrix {
m3 := mgl64.Mat3(m)
@ -297,7 +289,7 @@ func (m Matrix) Moved(delta Vec) Matrix {
// ScaledXY scales everything around a given point by the scale factor in each axis respectively.
func (m Matrix) ScaledXY(around Vec, scale Vec) Matrix {
m3 := mgl64.Mat3(m)
m3 = mgl64.Translate2D((-around).XY()).Mul3(m3)
m3 = mgl64.Translate2D(around.Scaled(-1).XY()).Mul3(m3)
m3 = mgl64.Scale2D(scale.XY()).Mul3(m3)
m3 = mgl64.Translate2D(around.XY()).Mul3(m3)
return Matrix(m3)
@ -311,7 +303,7 @@ func (m Matrix) Scaled(around Vec, scale float64) Matrix {
// Rotated rotates everything around a given point by the given angle in radians.
func (m Matrix) Rotated(around Vec, angle float64) Matrix {
m3 := mgl64.Mat3(m)
m3 = mgl64.Translate2D((-around).XY()).Mul3(m3)
m3 = mgl64.Translate2D(around.Scaled(-1).XY()).Mul3(m3)
m3 = mgl64.Rotate3DZ(angle).Mul3(m3)
m3 = mgl64.Translate2D(around.XY()).Mul3(m3)
return Matrix(m3)
@ -330,7 +322,7 @@ func (m Matrix) Chained(next Matrix) Matrix {
// Time complexity is O(1).
func (m Matrix) Project(u Vec) Vec {
m3 := mgl64.Mat3(m)
proj := m3.Mul3x1(mgl64.Vec3{u.X(), u.Y(), 1})
proj := m3.Mul3x1(mgl64.Vec3{u.X, u.Y, 1})
return V(proj.X(), proj.Y())
}
@ -340,6 +332,6 @@ func (m Matrix) Project(u Vec) Vec {
func (m Matrix) Unproject(u Vec) Vec {
m3 := mgl64.Mat3(m)
inv := m3.Inv()
unproj := inv.Mul3x1(mgl64.Vec3{u.X(), u.Y(), 1})
unproj := inv.Mul3x1(mgl64.Vec3{u.X, u.Y, 1})
return V(unproj.X(), unproj.Y())
}

View File

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

View File

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

View File

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

5
pixelgl/doc.go Normal file
View File

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

99
pixelgl/glframe.go Normal file
View File

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

98
pixelgl/glpicture.go Normal file
View File

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

View File

@ -75,6 +75,7 @@ func (gt *GLTriangles) SetLen(len int) {
if len < gt.Len() {
gt.data = gt.data[:len*gt.vs.Stride()]
}
gt.submitData()
}
// Slice returns a sub-Triangles of this GLTriangles in range [i, j).
@ -134,22 +135,34 @@ func (gt *GLTriangles) updateData(t pixel.Triangles) {
if t, ok := t.(pixel.TrianglesPicture); ok {
for i := 0; i < gt.Len(); i++ {
pic, intensity := t.Picture(i)
gt.data[i*gt.vs.Stride()+6] = float32(pic.X())
gt.data[i*gt.vs.Stride()+7] = float32(pic.Y())
gt.data[i*gt.vs.Stride()+6] = float32(pic.X)
gt.data[i*gt.vs.Stride()+7] = float32(pic.Y)
gt.data[i*gt.vs.Stride()+8] = float32(intensity)
}
}
}
func (gt *GLTriangles) submitData() {
data := append([]float32{}, gt.data...) // avoid race condition
mainthread.CallNonBlock(func() {
gt.vs.Begin()
dataLen := len(data) / gt.vs.Stride()
gt.vs.SetLen(dataLen)
gt.vs.SetVertexData(gt.data)
gt.vs.End()
})
// this code is supposed to copy the vertex data and CallNonBlock the update if
// the data is small enough, otherwise it'll block and not copy the data
if len(gt.data) < 256 { // arbitrary heurestic constant
data := append([]float32{}, gt.data...)
mainthread.CallNonBlock(func() {
gt.vs.Begin()
dataLen := len(data) / gt.vs.Stride()
gt.vs.SetLen(dataLen)
gt.vs.SetVertexData(data)
gt.vs.End()
})
} else {
mainthread.Call(func() {
gt.vs.Begin()
dataLen := len(gt.data) / gt.vs.Stride()
gt.vs.SetLen(dataLen)
gt.vs.SetVertexData(gt.data)
gt.vs.End()
})
}
}
// Update copies vertex properties from the supplied Triangles into this GLTriangles.
@ -178,12 +191,12 @@ func (gt *GLTriangles) Position(i int) pixel.Vec {
}
// Color returns the Color property of the i-th vertex.
func (gt *GLTriangles) Color(i int) pixel.NRGBA {
func (gt *GLTriangles) Color(i int) pixel.RGBA {
r := gt.data[i*gt.vs.Stride()+2]
g := gt.data[i*gt.vs.Stride()+3]
b := gt.data[i*gt.vs.Stride()+4]
a := gt.data[i*gt.vs.Stride()+5]
return pixel.NRGBA{
return pixel.RGBA{
R: float64(r),
G: float64(g),
B: float64(b),

View File

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

View File

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

View File

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

118
sprite.go
View File

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

249
text/atlas.go Normal file
View File

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

2
text/doc.go Normal file
View File

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

347
text/text.go Normal file
View File

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