ruby2d 0.12.1 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (620) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE.md +7 -0
  3. data/README.md +141 -0
  4. data/USAGE.md +2435 -0
  5. data/assets/Rakefile +637 -0
  6. data/assets/build_support/CMakeLists.txt +94 -0
  7. data/assets/build_support/mruby/build_config.rb +34 -0
  8. data/assets/build_support/mruby/build_config_wasm.rb +30 -0
  9. data/assets/build_support/sdl_test.c +174 -0
  10. data/assets/deps.yaml +16 -0
  11. data/assets/platform/include/SDL3/SDL.h +91 -0
  12. data/assets/platform/include/SDL3/SDL_assert.h +705 -0
  13. data/assets/platform/include/SDL3/SDL_asyncio.h +550 -0
  14. data/assets/platform/include/SDL3/SDL_atomic.h +692 -0
  15. data/assets/platform/include/SDL3/SDL_audio.h +2370 -0
  16. data/assets/platform/include/SDL3/SDL_begin_code.h +553 -0
  17. data/assets/{include/SDL2 → platform/include/SDL3}/SDL_bits.h +47 -27
  18. data/assets/{include/SDL2 → platform/include/SDL3}/SDL_blendmode.h +59 -55
  19. data/assets/platform/include/SDL3/SDL_camera.h +535 -0
  20. data/assets/platform/include/SDL3/SDL_clipboard.h +331 -0
  21. data/assets/{include/SDL2/close_code.h → platform/include/SDL3/SDL_close_code.h} +9 -8
  22. data/assets/{include/SDL2 → platform/include/SDL3}/SDL_copying.h +3 -1
  23. data/assets/platform/include/SDL3/SDL_cpuinfo.h +374 -0
  24. data/assets/platform/include/SDL3/SDL_dialog.h +343 -0
  25. data/assets/platform/include/SDL3/SDL_dlopennote.h +234 -0
  26. data/assets/{include/SDL2 → platform/include/SDL3}/SDL_egl.h +11 -8
  27. data/assets/platform/include/SDL3/SDL_endian.h +645 -0
  28. data/assets/platform/include/SDL3/SDL_error.h +226 -0
  29. data/assets/platform/include/SDL3/SDL_events.h +1645 -0
  30. data/assets/platform/include/SDL3/SDL_filesystem.h +537 -0
  31. data/assets/platform/include/SDL3/SDL_gamepad.h +1656 -0
  32. data/assets/platform/include/SDL3/SDL_gpu.h +4608 -0
  33. data/assets/platform/include/SDL3/SDL_guid.h +106 -0
  34. data/assets/platform/include/SDL3/SDL_haptic.h +1461 -0
  35. data/assets/platform/include/SDL3/SDL_hidapi.h +571 -0
  36. data/assets/platform/include/SDL3/SDL_hints.h +5001 -0
  37. data/assets/platform/include/SDL3/SDL_init.h +507 -0
  38. data/assets/platform/include/SDL3/SDL_intrin.h +410 -0
  39. data/assets/platform/include/SDL3/SDL_iostream.h +1379 -0
  40. data/assets/platform/include/SDL3/SDL_joystick.h +1385 -0
  41. data/assets/platform/include/SDL3/SDL_keyboard.h +608 -0
  42. data/assets/platform/include/SDL3/SDL_keycode.h +347 -0
  43. data/assets/platform/include/SDL3/SDL_loadso.h +145 -0
  44. data/assets/{include/SDL2 → platform/include/SDL3}/SDL_locale.h +40 -24
  45. data/assets/platform/include/SDL3/SDL_log.h +541 -0
  46. data/assets/platform/include/SDL3/SDL_main.h +704 -0
  47. data/assets/platform/include/SDL3/SDL_main_impl.h +151 -0
  48. data/assets/{include/SDL2 → platform/include/SDL3}/SDL_messagebox.h +85 -48
  49. data/assets/{include/SDL2 → platform/include/SDL3}/SDL_metal.h +29 -29
  50. data/assets/{include/SDL2 → platform/include/SDL3}/SDL_misc.h +13 -12
  51. data/assets/platform/include/SDL3/SDL_mouse.h +813 -0
  52. data/assets/platform/include/SDL3/SDL_mutex.h +1117 -0
  53. data/assets/platform/include/SDL3/SDL_oldnames.h +1339 -0
  54. data/assets/{include/SDL2 → platform/include/SDL3}/SDL_opengl.h +1247 -278
  55. data/assets/{include/SDL2 → platform/include/SDL3}/SDL_opengl_glext.h +5 -1
  56. data/assets/{include/SDL2 → platform/include/SDL3}/SDL_opengles.h +6 -7
  57. data/assets/{include/SDL2 → platform/include/SDL3}/SDL_opengles2.h +10 -11
  58. data/assets/platform/include/SDL3/SDL_pen.h +198 -0
  59. data/assets/platform/include/SDL3/SDL_pixels.h +1441 -0
  60. data/assets/{include/SDL2/SDL_test_log.h → platform/include/SDL3/SDL_platform.h} +25 -26
  61. data/assets/platform/include/SDL3/SDL_platform_defines.h +497 -0
  62. data/assets/{include/SDL2 → platform/include/SDL3}/SDL_power.h +40 -20
  63. data/assets/platform/include/SDL3/SDL_process.h +441 -0
  64. data/assets/platform/include/SDL3/SDL_properties.h +572 -0
  65. data/assets/platform/include/SDL3/SDL_rect.h +528 -0
  66. data/assets/platform/include/SDL3/SDL_render.h +3028 -0
  67. data/assets/platform/include/SDL3/SDL_revision.h +59 -0
  68. data/assets/{include/SDL2 → platform/include/SDL3}/SDL_scancode.h +59 -68
  69. data/assets/platform/include/SDL3/SDL_sensor.h +321 -0
  70. data/assets/platform/include/SDL3/SDL_stdinc.h +6186 -0
  71. data/assets/platform/include/SDL3/SDL_storage.h +686 -0
  72. data/assets/platform/include/SDL3/SDL_surface.h +1769 -0
  73. data/assets/platform/include/SDL3/SDL_system.h +840 -0
  74. data/assets/{include/SDL2 → platform/include/SDL3}/SDL_test.h +16 -22
  75. data/assets/{include/SDL2 → platform/include/SDL3}/SDL_test_assert.h +27 -34
  76. data/assets/{include/SDL2 → platform/include/SDL3}/SDL_test_common.h +109 -52
  77. data/assets/{include/SDL2 → platform/include/SDL3}/SDL_test_compare.h +22 -14
  78. data/assets/{include/SDL2 → platform/include/SDL3}/SDL_test_crc32.h +27 -30
  79. data/assets/{include/SDL2 → platform/include/SDL3}/SDL_test_font.h +50 -49
  80. data/assets/{include/SDL2 → platform/include/SDL3}/SDL_test_fuzzer.h +64 -79
  81. data/assets/{include/SDL2 → platform/include/SDL3}/SDL_test_harness.h +47 -30
  82. data/assets/platform/include/SDL3/SDL_test_log.h +83 -0
  83. data/assets/{include/SDL2 → platform/include/SDL3}/SDL_test_md5.h +21 -28
  84. data/assets/{include/SDL2 → platform/include/SDL3}/SDL_test_memory.h +19 -16
  85. data/assets/platform/include/SDL3/SDL_thread.h +602 -0
  86. data/assets/platform/include/SDL3/SDL_time.h +249 -0
  87. data/assets/platform/include/SDL3/SDL_timer.h +454 -0
  88. data/assets/platform/include/SDL3/SDL_touch.h +184 -0
  89. data/assets/platform/include/SDL3/SDL_tray.h +544 -0
  90. data/assets/platform/include/SDL3/SDL_version.h +198 -0
  91. data/assets/platform/include/SDL3/SDL_video.h +3497 -0
  92. data/assets/platform/include/SDL3/SDL_vulkan.h +287 -0
  93. data/assets/platform/include/SDL3_image/SDL_image.h +3328 -0
  94. data/assets/platform/include/SDL3_mixer/SDL_mixer.h +3369 -0
  95. data/assets/platform/include/SDL3_ttf/SDL_textengine.h +181 -0
  96. data/assets/platform/include/SDL3_ttf/SDL_ttf.h +2833 -0
  97. data/assets/{include → platform/include}/mrbconf.h +42 -31
  98. data/assets/{include → platform/include}/mruby/array.h +3 -4
  99. data/assets/{include → platform/include}/mruby/boxing_nan.h +10 -0
  100. data/assets/platform/include/mruby/boxing_no.h +59 -0
  101. data/assets/{include → platform/include}/mruby/boxing_word.h +72 -26
  102. data/assets/platform/include/mruby/class.h +157 -0
  103. data/assets/{include → platform/include}/mruby/common.h +30 -1
  104. data/assets/{include → platform/include}/mruby/compile.h +52 -74
  105. data/assets/{include → platform/include}/mruby/data.h +2 -2
  106. data/assets/{include → platform/include}/mruby/debug.h +12 -5
  107. data/assets/{include → platform/include}/mruby/dump.h +7 -9
  108. data/assets/{include → platform/include}/mruby/endian.h +1 -1
  109. data/assets/platform/include/mruby/error.h +173 -0
  110. data/assets/platform/include/mruby/gc.h +79 -0
  111. data/assets/{include → platform/include}/mruby/hash.h +1 -7
  112. data/assets/platform/include/mruby/internal.h +282 -0
  113. data/assets/platform/include/mruby/irep.h +143 -0
  114. data/assets/{include → platform/include}/mruby/istruct.h +1 -1
  115. data/assets/platform/include/mruby/khash.h +455 -0
  116. data/assets/platform/include/mruby/mempool.h +19 -0
  117. data/assets/{include → platform/include}/mruby/numeric.h +7 -10
  118. data/assets/platform/include/mruby/object.h +51 -0
  119. data/assets/{include → platform/include}/mruby/opcode.h +5 -5
  120. data/assets/{include → platform/include}/mruby/ops.h +19 -6
  121. data/assets/platform/include/mruby/presym/id.h +1447 -0
  122. data/assets/{include → platform/include}/mruby/presym/scanning.h +10 -7
  123. data/assets/platform/include/mruby/presym/table.h +2893 -0
  124. data/assets/platform/include/mruby/presym.h +65 -0
  125. data/assets/platform/include/mruby/proc.h +184 -0
  126. data/assets/{include → platform/include}/mruby/range.h +0 -2
  127. data/assets/{include → platform/include}/mruby/string.h +33 -53
  128. data/assets/platform/include/mruby/throw.h +81 -0
  129. data/assets/{include → platform/include}/mruby/value.h +64 -29
  130. data/assets/{include → platform/include}/mruby/variable.h +6 -23
  131. data/assets/{include → platform/include}/mruby/version.h +7 -7
  132. data/assets/{include → platform/include}/mruby.h +238 -116
  133. data/assets/platform/macos-arm64/bin/mrbc +0 -0
  134. data/assets/platform/macos-arm64/lib/libSDL3.a +0 -0
  135. data/assets/platform/macos-arm64/lib/libSDL3_image.a +0 -0
  136. data/assets/platform/macos-arm64/lib/libSDL3_mixer.a +0 -0
  137. data/assets/platform/macos-arm64/lib/libSDL3_ttf.a +0 -0
  138. data/assets/platform/macos-arm64/lib/libmruby.a +0 -0
  139. data/assets/platform/wasm/lib/libFLAC.a +0 -0
  140. data/assets/platform/wasm/lib/libSDL3.a +0 -0
  141. data/assets/platform/wasm/lib/libSDL3_image.a +0 -0
  142. data/assets/platform/wasm/lib/libSDL3_mixer.a +0 -0
  143. data/assets/platform/wasm/lib/libSDL3_ttf.a +0 -0
  144. data/assets/platform/wasm/lib/libfreetype.a +0 -0
  145. data/assets/platform/wasm/lib/libharfbuzz.a +0 -0
  146. data/assets/platform/wasm/lib/libmruby.a +0 -0
  147. data/assets/platform/wasm/lib/libogg.a +0 -0
  148. data/assets/platform/wasm/lib/libplutosvg.a +0 -0
  149. data/assets/platform/wasm/lib/libplutovg.a +0 -0
  150. data/assets/platform/wasm/lib/libpng.a +0 -0
  151. data/assets/platform/wasm/lib/libpng16.a +0 -0
  152. data/assets/platform/wasm/lib/libvorbis.a +0 -0
  153. data/assets/platform/wasm/lib/libvorbisfile.a +0 -0
  154. data/assets/platform/wasm/lib/libzlibstatic.a +0 -0
  155. data/assets/platform/windows-arm64-mingw-ucrt/bin/mrbc.exe +0 -0
  156. data/assets/platform/windows-arm64-mingw-ucrt/lib/libSDL3.a +0 -0
  157. data/assets/platform/windows-arm64-mingw-ucrt/lib/libSDL3_image.a +0 -0
  158. data/assets/platform/windows-arm64-mingw-ucrt/lib/libSDL3_mixer.a +0 -0
  159. data/assets/platform/windows-arm64-mingw-ucrt/lib/libSDL3_ttf.a +0 -0
  160. data/assets/platform/windows-arm64-mingw-ucrt/lib/libmruby.a +0 -0
  161. data/assets/platform/windows-x86_64-mingw-ucrt/bin/mrbc.exe +0 -0
  162. data/assets/platform/windows-x86_64-mingw-ucrt/lib/libSDL3.a +0 -0
  163. data/assets/platform/windows-x86_64-mingw-ucrt/lib/libSDL3_image.a +0 -0
  164. data/assets/platform/windows-x86_64-mingw-ucrt/lib/libSDL3_mixer.a +0 -0
  165. data/assets/platform/windows-x86_64-mingw-ucrt/lib/libSDL3_ttf.a +0 -0
  166. data/assets/platform/windows-x86_64-mingw-ucrt/lib/libmruby.a +0 -0
  167. data/assets/resources/fonts/outfit/OFL.txt +93 -0
  168. data/assets/resources/fonts/outfit/outfit.ttf +0 -0
  169. data/assets/resources/fonts/roboto_mono/OFL.txt +91 -0
  170. data/assets/resources/fonts/roboto_mono/roboto_mono.ttf +0 -0
  171. data/assets/resources/icons/icon.icns +0 -0
  172. data/assets/resources/icons/icon.png +0 -0
  173. data/assets/resources/spritesheets/License.txt +28 -0
  174. data/assets/resources/spritesheets/spritesheet-backgrounds.png +0 -0
  175. data/assets/resources/spritesheets/spritesheet-backgrounds.xml +16 -0
  176. data/assets/resources/spritesheets/spritesheet-characters.png +0 -0
  177. data/assets/resources/spritesheets/spritesheet-characters.xml +47 -0
  178. data/assets/resources/spritesheets/spritesheet-enemies.png +0 -0
  179. data/assets/resources/spritesheets/spritesheet-enemies.xml +62 -0
  180. data/assets/resources/spritesheets/spritesheet-tiles.png +0 -0
  181. data/assets/resources/spritesheets/spritesheet-tiles.xml +316 -0
  182. data/assets/{wasm → resources/web}/template.html +1 -1
  183. data/assets/target.rb +131 -0
  184. data/bin/ruby2d +281 -82
  185. data/examples/astar.rb +269 -0
  186. data/examples/asteroids.rb +403 -0
  187. data/examples/bezier_editor.rb +96 -0
  188. data/examples/boids.rb +191 -0
  189. data/examples/bouncing_balls.rb +119 -0
  190. data/examples/breakout.rb +186 -0
  191. data/examples/constellations.rb +100 -0
  192. data/examples/dashboard.rb +133 -0
  193. data/examples/double_pendulum.rb +254 -0
  194. data/examples/falling_sand.rb +291 -0
  195. data/examples/fireworks.rb +110 -0
  196. data/examples/fluid_dynamics.rb +340 -0
  197. data/examples/fourier_epicycles.rb +142 -0
  198. data/examples/fractal_tree.rb +326 -0
  199. data/examples/game_of_life.rb +148 -0
  200. data/examples/gamepads.rb +333 -0
  201. data/examples/hill_driver.rb +613 -0
  202. data/examples/logo_animation.rb +251 -0
  203. data/examples/lorenz_attractor.rb +115 -0
  204. data/examples/mandelbrot.rb +207 -0
  205. data/examples/marching_squares.rb +200 -0
  206. data/examples/maze.rb +323 -0
  207. data/examples/nbody.rb +338 -0
  208. data/examples/outrun.rb +391 -0
  209. data/examples/pixel_paint.rb +313 -0
  210. data/examples/platformer.rb +335 -0
  211. data/examples/ray_casting_maze.rb +175 -0
  212. data/examples/shadow_casting.rb +151 -0
  213. data/examples/shape_recognizer.rb +908 -0
  214. data/examples/snake.rb +144 -0
  215. data/examples/sprite_sheets.rb +246 -0
  216. data/examples/swarm.rb +206 -0
  217. data/examples/tsp_annealing.rb +104 -0
  218. data/examples/ui_controls.rb +163 -0
  219. data/examples/wireframe_3d.rb +139 -0
  220. data/ext/ruby2d/audio.c +324 -0
  221. data/ext/ruby2d/canvas.c +1715 -494
  222. data/ext/ruby2d/ext.c +541 -0
  223. data/ext/ruby2d/extconf.rb +122 -145
  224. data/ext/ruby2d/font.c +566 -20
  225. data/ext/ruby2d/fps.c +92 -0
  226. data/ext/ruby2d/image.c +466 -41
  227. data/ext/ruby2d/ruby2d.c +374 -1890
  228. data/ext/ruby2d/ruby2d.h +840 -460
  229. data/ext/ruby2d/shapes.c +931 -101
  230. data/ext/ruby2d/text.c +586 -21
  231. data/ext/ruby2d/window.c +1693 -283
  232. data/lib/ruby2d/atlas_parser.rb +194 -0
  233. data/lib/ruby2d/audio.rb +73 -0
  234. data/lib/ruby2d/benchmark.rb +259 -0
  235. data/lib/ruby2d/bitmap_text.rb +155 -0
  236. data/lib/ruby2d/button.rb +402 -0
  237. data/lib/ruby2d/canvas.rb +703 -279
  238. data/lib/ruby2d/circle.rb +206 -29
  239. data/lib/ruby2d/cli/browser.rb +314 -0
  240. data/lib/ruby2d/cli/build.rb +471 -257
  241. data/lib/ruby2d/cli/colorize.rb +7 -5
  242. data/lib/ruby2d/cli/examples.rb +127 -0
  243. data/lib/ruby2d/cli/launch.rb +24 -39
  244. data/lib/ruby2d/cli/lib_files.rb +48 -0
  245. data/lib/ruby2d/cli/messages.rb +40 -0
  246. data/lib/ruby2d/cli/setup.rb +273 -0
  247. data/lib/ruby2d/cli/static_server.rb +160 -0
  248. data/lib/ruby2d/cli/usage.rb +255 -0
  249. data/lib/ruby2d/color.rb +191 -69
  250. data/lib/ruby2d/core.rb +37 -19
  251. data/lib/ruby2d/deps_help.rb +89 -0
  252. data/lib/ruby2d/dsl.rb +51 -12
  253. data/lib/ruby2d/ellipse.rb +228 -0
  254. data/lib/ruby2d/exceptions.rb +0 -2
  255. data/lib/ruby2d/font.rb +34 -74
  256. data/lib/ruby2d/gamepad.rb +323 -0
  257. data/lib/ruby2d/gem_paths.rb +19 -0
  258. data/lib/ruby2d/image.rb +155 -52
  259. data/lib/ruby2d/interactive.rb +103 -0
  260. data/lib/ruby2d/json_parser.rb +268 -0
  261. data/lib/ruby2d/line.rb +194 -85
  262. data/lib/ruby2d/mruby_compat.rb +12 -0
  263. data/lib/ruby2d/polygon.rb +297 -0
  264. data/lib/ruby2d/polyline.rb +368 -0
  265. data/lib/ruby2d/quad.rb +358 -94
  266. data/lib/ruby2d/rectangle.rb +77 -19
  267. data/lib/ruby2d/renderable.rb +411 -11
  268. data/lib/ruby2d/sprite.rb +471 -180
  269. data/lib/ruby2d/sprite_sheet.rb +64 -0
  270. data/lib/ruby2d/square.rb +29 -17
  271. data/lib/ruby2d/text.rb +196 -65
  272. data/lib/ruby2d/tileset.rb +139 -81
  273. data/lib/ruby2d/triangle.rb +324 -89
  274. data/lib/ruby2d/version.rb +1 -3
  275. data/lib/ruby2d/vertices.rb +62 -85
  276. data/lib/ruby2d/warnings.rb +29 -0
  277. data/lib/ruby2d/window/class_methods.rb +218 -0
  278. data/lib/ruby2d/window/gamepad_events.rb +141 -0
  279. data/lib/ruby2d/window/key_events.rb +74 -0
  280. data/lib/ruby2d/window/mouse_events.rb +171 -0
  281. data/lib/ruby2d/window/object_events.rb +168 -0
  282. data/lib/ruby2d/window.rb +632 -585
  283. data/lib/ruby2d.rb +14 -19
  284. metadata +275 -443
  285. data/assets/app.icns +0 -0
  286. data/assets/include/GL/glew.h +0 -26427
  287. data/assets/include/GLES2/gl2.h +0 -656
  288. data/assets/include/GLES2/gl2ext.h +0 -3949
  289. data/assets/include/GLES2/gl2ext_angle.h +0 -701
  290. data/assets/include/GLES2/gl2platform.h +0 -27
  291. data/assets/include/GLES3/gl3.h +0 -1192
  292. data/assets/include/GLES3/gl31.h +0 -1507
  293. data/assets/include/GLES3/gl32.h +0 -1808
  294. data/assets/include/GLES3/gl3platform.h +0 -27
  295. data/assets/include/KHR/khrplatform.h +0 -290
  296. data/assets/include/SDL2/SDL.h +0 -233
  297. data/assets/include/SDL2/SDL_assert.h +0 -326
  298. data/assets/include/SDL2/SDL_atomic.h +0 -415
  299. data/assets/include/SDL2/SDL_audio.h +0 -1500
  300. data/assets/include/SDL2/SDL_clipboard.h +0 -141
  301. data/assets/include/SDL2/SDL_config.h +0 -61
  302. data/assets/include/SDL2/SDL_config_android.h +0 -194
  303. data/assets/include/SDL2/SDL_config_emscripten.h +0 -218
  304. data/assets/include/SDL2/SDL_config_iphoneos.h +0 -217
  305. data/assets/include/SDL2/SDL_config_macosx.h +0 -277
  306. data/assets/include/SDL2/SDL_config_minimal.h +0 -95
  307. data/assets/include/SDL2/SDL_config_ngage.h +0 -89
  308. data/assets/include/SDL2/SDL_config_os2.h +0 -207
  309. data/assets/include/SDL2/SDL_config_pandora.h +0 -141
  310. data/assets/include/SDL2/SDL_config_windows.h +0 -331
  311. data/assets/include/SDL2/SDL_config_wingdk.h +0 -253
  312. data/assets/include/SDL2/SDL_config_winrt.h +0 -220
  313. data/assets/include/SDL2/SDL_config_xbox.h +0 -235
  314. data/assets/include/SDL2/SDL_cpuinfo.h +0 -594
  315. data/assets/include/SDL2/SDL_endian.h +0 -348
  316. data/assets/include/SDL2/SDL_error.h +0 -163
  317. data/assets/include/SDL2/SDL_events.h +0 -1166
  318. data/assets/include/SDL2/SDL_filesystem.h +0 -149
  319. data/assets/include/SDL2/SDL_gamecontroller.h +0 -1074
  320. data/assets/include/SDL2/SDL_gesture.h +0 -117
  321. data/assets/include/SDL2/SDL_guid.h +0 -100
  322. data/assets/include/SDL2/SDL_haptic.h +0 -1341
  323. data/assets/include/SDL2/SDL_hidapi.h +0 -451
  324. data/assets/include/SDL2/SDL_hints.h +0 -2569
  325. data/assets/include/SDL2/SDL_image.h +0 -2173
  326. data/assets/include/SDL2/SDL_joystick.h +0 -1066
  327. data/assets/include/SDL2/SDL_keyboard.h +0 -353
  328. data/assets/include/SDL2/SDL_keycode.h +0 -358
  329. data/assets/include/SDL2/SDL_loadso.h +0 -115
  330. data/assets/include/SDL2/SDL_log.h +0 -404
  331. data/assets/include/SDL2/SDL_main.h +0 -275
  332. data/assets/include/SDL2/SDL_mixer.h +0 -2784
  333. data/assets/include/SDL2/SDL_mouse.h +0 -465
  334. data/assets/include/SDL2/SDL_mutex.h +0 -471
  335. data/assets/include/SDL2/SDL_name.h +0 -33
  336. data/assets/include/SDL2/SDL_pixels.h +0 -644
  337. data/assets/include/SDL2/SDL_platform.h +0 -261
  338. data/assets/include/SDL2/SDL_quit.h +0 -58
  339. data/assets/include/SDL2/SDL_rect.h +0 -376
  340. data/assets/include/SDL2/SDL_render.h +0 -1919
  341. data/assets/include/SDL2/SDL_revision.h +0 -6
  342. data/assets/include/SDL2/SDL_rwops.h +0 -841
  343. data/assets/include/SDL2/SDL_sensor.h +0 -322
  344. data/assets/include/SDL2/SDL_shape.h +0 -155
  345. data/assets/include/SDL2/SDL_stdinc.h +0 -830
  346. data/assets/include/SDL2/SDL_surface.h +0 -997
  347. data/assets/include/SDL2/SDL_system.h +0 -623
  348. data/assets/include/SDL2/SDL_syswm.h +0 -386
  349. data/assets/include/SDL2/SDL_test_images.h +0 -78
  350. data/assets/include/SDL2/SDL_test_random.h +0 -115
  351. data/assets/include/SDL2/SDL_thread.h +0 -464
  352. data/assets/include/SDL2/SDL_timer.h +0 -222
  353. data/assets/include/SDL2/SDL_touch.h +0 -150
  354. data/assets/include/SDL2/SDL_ttf.h +0 -2316
  355. data/assets/include/SDL2/SDL_types.h +0 -29
  356. data/assets/include/SDL2/SDL_version.h +0 -204
  357. data/assets/include/SDL2/SDL_video.h +0 -2150
  358. data/assets/include/SDL2/SDL_vulkan.h +0 -215
  359. data/assets/include/SDL2/begin_code.h +0 -187
  360. data/assets/include/mruby/boxing_no.h +0 -58
  361. data/assets/include/mruby/class.h +0 -108
  362. data/assets/include/mruby/error.h +0 -147
  363. data/assets/include/mruby/gc.h +0 -94
  364. data/assets/include/mruby/irep.h +0 -147
  365. data/assets/include/mruby/khash.h +0 -284
  366. data/assets/include/mruby/object.h +0 -43
  367. data/assets/include/mruby/presym/disable.h +0 -70
  368. data/assets/include/mruby/presym/enable.h +0 -37
  369. data/assets/include/mruby/presym.h +0 -40
  370. data/assets/include/mruby/proc.h +0 -211
  371. data/assets/include/mruby/throw.h +0 -66
  372. data/assets/macos/universal/bin/mrbc +0 -0
  373. data/assets/macos/universal/lib/libFLAC.a +0 -0
  374. data/assets/macos/universal/lib/libSDL2.a +0 -0
  375. data/assets/macos/universal/lib/libSDL2_image.a +0 -0
  376. data/assets/macos/universal/lib/libSDL2_mixer.a +0 -0
  377. data/assets/macos/universal/lib/libSDL2_ttf.a +0 -0
  378. data/assets/macos/universal/lib/libavif.a +0 -0
  379. data/assets/macos/universal/lib/libbrotlicommon-static.a +0 -0
  380. data/assets/macos/universal/lib/libbrotlidec-static.a +0 -0
  381. data/assets/macos/universal/lib/libfreetype.a +0 -0
  382. data/assets/macos/universal/lib/libgraphite2.a +0 -0
  383. data/assets/macos/universal/lib/libharfbuzz.a +0 -0
  384. data/assets/macos/universal/lib/libhwy.a +0 -0
  385. data/assets/macos/universal/lib/libjpeg.a +0 -0
  386. data/assets/macos/universal/lib/libjxl.a +0 -0
  387. data/assets/macos/universal/lib/libmodplug.a +0 -0
  388. data/assets/macos/universal/lib/libmpg123.a +0 -0
  389. data/assets/macos/universal/lib/libmruby.a +0 -0
  390. data/assets/macos/universal/lib/libogg.a +0 -0
  391. data/assets/macos/universal/lib/libpng.a +0 -0
  392. data/assets/macos/universal/lib/libtiff.a +0 -0
  393. data/assets/macos/universal/lib/libvorbis.a +0 -0
  394. data/assets/macos/universal/lib/libvorbisfile.a +0 -0
  395. data/assets/macos/universal/lib/libwebp.a +0 -0
  396. data/assets/macos/universal/lib/libzstd.a +0 -0
  397. data/assets/test_media/README.md +0 -3
  398. data/assets/test_media/bitstream_vera/COPYRIGHT.txt +0 -124
  399. data/assets/test_media/bitstream_vera/vera.ttf +0 -0
  400. data/assets/test_media/boom.png +0 -0
  401. data/assets/test_media/coin.png +0 -0
  402. data/assets/test_media/colors.png +0 -0
  403. data/assets/test_media/controller.png +0 -0
  404. data/assets/test_media/dial.wav +0 -0
  405. data/assets/test_media/hero.png +0 -0
  406. data/assets/test_media/image.bmp +0 -0
  407. data/assets/test_media/image.jpg +0 -0
  408. data/assets/test_media/image.png +0 -0
  409. data/assets/test_media/music.flac +0 -0
  410. data/assets/test_media/music.mp3 +0 -0
  411. data/assets/test_media/music.ogg +0 -0
  412. data/assets/test_media/music.wav +0 -0
  413. data/assets/test_media/originals/boom.pxm +0 -0
  414. data/assets/test_media/originals/coin.pxm +0 -0
  415. data/assets/test_media/originals/controller.sketch +0 -0
  416. data/assets/test_media/originals/hero.pxm +0 -0
  417. data/assets/test_media/originals/image.pxm +0 -0
  418. data/assets/test_media/originals/music.caf +0 -0
  419. data/assets/test_media/originals/texture_atlas.pxm +0 -0
  420. data/assets/test_media/rondo_alla_turka.ogg +0 -0
  421. data/assets/test_media/sound.flac +0 -0
  422. data/assets/test_media/sound.mp3 +0 -0
  423. data/assets/test_media/sound.ogg +0 -0
  424. data/assets/test_media/sound.wav +0 -0
  425. data/assets/test_media/sprite_sheet.png +0 -0
  426. data/assets/test_media/texture_atlas.png +0 -0
  427. data/assets/wasm/libmruby.a +0 -0
  428. data/assets/windows/mingw-w64-ucrt-x86_64/bin/mrbc.exe +0 -0
  429. data/assets/windows/mingw-w64-ucrt-x86_64/lib/libFLAC.a +0 -0
  430. data/assets/windows/mingw-w64-ucrt-x86_64/lib/libLerc.a +0 -0
  431. data/assets/windows/mingw-w64-ucrt-x86_64/lib/libSDL2.a +0 -0
  432. data/assets/windows/mingw-w64-ucrt-x86_64/lib/libSDL2_image.a +0 -0
  433. data/assets/windows/mingw-w64-ucrt-x86_64/lib/libSDL2_mixer.a +0 -0
  434. data/assets/windows/mingw-w64-ucrt-x86_64/lib/libSDL2_ttf.a +0 -0
  435. data/assets/windows/mingw-w64-ucrt-x86_64/lib/libbrotlicommon.a +0 -0
  436. data/assets/windows/mingw-w64-ucrt-x86_64/lib/libbrotlidec.a +0 -0
  437. data/assets/windows/mingw-w64-ucrt-x86_64/lib/libbz2.a +0 -0
  438. data/assets/windows/mingw-w64-ucrt-x86_64/lib/libdeflate.a +0 -0
  439. data/assets/windows/mingw-w64-ucrt-x86_64/lib/libfreetype.a +0 -0
  440. data/assets/windows/mingw-w64-ucrt-x86_64/lib/libglew32.a +0 -0
  441. data/assets/windows/mingw-w64-ucrt-x86_64/lib/libgraphite2.a +0 -0
  442. data/assets/windows/mingw-w64-ucrt-x86_64/lib/libharfbuzz.a +0 -0
  443. data/assets/windows/mingw-w64-ucrt-x86_64/lib/libhwy.a +0 -0
  444. data/assets/windows/mingw-w64-ucrt-x86_64/lib/libjbig.a +0 -0
  445. data/assets/windows/mingw-w64-ucrt-x86_64/lib/libjpeg.a +0 -0
  446. data/assets/windows/mingw-w64-ucrt-x86_64/lib/libjxl.a +0 -0
  447. data/assets/windows/mingw-w64-ucrt-x86_64/lib/liblzma.a +0 -0
  448. data/assets/windows/mingw-w64-ucrt-x86_64/lib/libmodplug.a +0 -0
  449. data/assets/windows/mingw-w64-ucrt-x86_64/lib/libmpg123.a +0 -0
  450. data/assets/windows/mingw-w64-ucrt-x86_64/lib/libmruby.a +0 -0
  451. data/assets/windows/mingw-w64-ucrt-x86_64/lib/libogg.a +0 -0
  452. data/assets/windows/mingw-w64-ucrt-x86_64/lib/libopus.a +0 -0
  453. data/assets/windows/mingw-w64-ucrt-x86_64/lib/libopusfile.a +0 -0
  454. data/assets/windows/mingw-w64-ucrt-x86_64/lib/libpng.a +0 -0
  455. data/assets/windows/mingw-w64-ucrt-x86_64/lib/libsndfile.a +0 -0
  456. data/assets/windows/mingw-w64-ucrt-x86_64/lib/libssp.a +0 -1
  457. data/assets/windows/mingw-w64-ucrt-x86_64/lib/libstdc++.a +0 -0
  458. data/assets/windows/mingw-w64-ucrt-x86_64/lib/libtiff.a +0 -0
  459. data/assets/windows/mingw-w64-ucrt-x86_64/lib/libvorbis.a +0 -0
  460. data/assets/windows/mingw-w64-ucrt-x86_64/lib/libvorbisfile.a +0 -0
  461. data/assets/windows/mingw-w64-ucrt-x86_64/lib/libwebp.a +0 -0
  462. data/assets/windows/mingw-w64-ucrt-x86_64/lib/libz.a +0 -0
  463. data/assets/windows/mingw-w64-ucrt-x86_64/lib/libzstd.a +0 -0
  464. data/assets/windows/mingw-w64-x86_64/bin/mrbc.exe +0 -0
  465. data/assets/windows/mingw-w64-x86_64/lib/libFLAC.a +0 -0
  466. data/assets/windows/mingw-w64-x86_64/lib/libLerc.a +0 -0
  467. data/assets/windows/mingw-w64-x86_64/lib/libSDL2.a +0 -0
  468. data/assets/windows/mingw-w64-x86_64/lib/libSDL2_image.a +0 -0
  469. data/assets/windows/mingw-w64-x86_64/lib/libSDL2_mixer.a +0 -0
  470. data/assets/windows/mingw-w64-x86_64/lib/libSDL2_ttf.a +0 -0
  471. data/assets/windows/mingw-w64-x86_64/lib/libbrotlicommon.a +0 -0
  472. data/assets/windows/mingw-w64-x86_64/lib/libbrotlidec.a +0 -0
  473. data/assets/windows/mingw-w64-x86_64/lib/libbz2.a +0 -0
  474. data/assets/windows/mingw-w64-x86_64/lib/libdeflate.a +0 -0
  475. data/assets/windows/mingw-w64-x86_64/lib/libfreetype.a +0 -0
  476. data/assets/windows/mingw-w64-x86_64/lib/libglew32.a +0 -0
  477. data/assets/windows/mingw-w64-x86_64/lib/libgraphite2.a +0 -0
  478. data/assets/windows/mingw-w64-x86_64/lib/libharfbuzz.a +0 -0
  479. data/assets/windows/mingw-w64-x86_64/lib/libhwy.a +0 -0
  480. data/assets/windows/mingw-w64-x86_64/lib/libjbig.a +0 -0
  481. data/assets/windows/mingw-w64-x86_64/lib/libjpeg.a +0 -0
  482. data/assets/windows/mingw-w64-x86_64/lib/libjxl.a +0 -0
  483. data/assets/windows/mingw-w64-x86_64/lib/liblzma.a +0 -0
  484. data/assets/windows/mingw-w64-x86_64/lib/libmodplug.a +0 -0
  485. data/assets/windows/mingw-w64-x86_64/lib/libmpg123.a +0 -0
  486. data/assets/windows/mingw-w64-x86_64/lib/libmruby.a +0 -0
  487. data/assets/windows/mingw-w64-x86_64/lib/libogg.a +0 -0
  488. data/assets/windows/mingw-w64-x86_64/lib/libopus.a +0 -0
  489. data/assets/windows/mingw-w64-x86_64/lib/libopusfile.a +0 -0
  490. data/assets/windows/mingw-w64-x86_64/lib/libpng.a +0 -0
  491. data/assets/windows/mingw-w64-x86_64/lib/libsndfile.a +0 -0
  492. data/assets/windows/mingw-w64-x86_64/lib/libssp.a +0 -1
  493. data/assets/windows/mingw-w64-x86_64/lib/libstdc++.a +0 -0
  494. data/assets/windows/mingw-w64-x86_64/lib/libtiff.a +0 -0
  495. data/assets/windows/mingw-w64-x86_64/lib/libvorbis.a +0 -0
  496. data/assets/windows/mingw-w64-x86_64/lib/libvorbisfile.a +0 -0
  497. data/assets/windows/mingw-w64-x86_64/lib/libwebp.a +0 -0
  498. data/assets/windows/mingw-w64-x86_64/lib/libz.a +0 -0
  499. data/assets/windows/mingw-w64-x86_64/lib/libzstd.a +0 -0
  500. data/assets/xcode/ios/Assets.xcassets/AppIcon.appiconset/Contents.json +0 -98
  501. data/assets/xcode/ios/Assets.xcassets/Contents.json +0 -6
  502. data/assets/xcode/ios/Base.lproj/LaunchScreen.storyboard +0 -27
  503. data/assets/xcode/ios/Info.plist +0 -43
  504. data/assets/xcode/ios/MRuby.framework/Headers/mrbconf.h +0 -143
  505. data/assets/xcode/ios/MRuby.framework/Headers/mruby/array.h +0 -280
  506. data/assets/xcode/ios/MRuby.framework/Headers/mruby/boxing_nan.h +0 -102
  507. data/assets/xcode/ios/MRuby.framework/Headers/mruby/boxing_no.h +0 -56
  508. data/assets/xcode/ios/MRuby.framework/Headers/mruby/boxing_word.h +0 -144
  509. data/assets/xcode/ios/MRuby.framework/Headers/mruby/class.h +0 -97
  510. data/assets/xcode/ios/MRuby.framework/Headers/mruby/common.h +0 -77
  511. data/assets/xcode/ios/MRuby.framework/Headers/mruby/compile.h +0 -195
  512. data/assets/xcode/ios/MRuby.framework/Headers/mruby/data.h +0 -75
  513. data/assets/xcode/ios/MRuby.framework/Headers/mruby/debug.h +0 -67
  514. data/assets/xcode/ios/MRuby.framework/Headers/mruby/dump.h +0 -196
  515. data/assets/xcode/ios/MRuby.framework/Headers/mruby/error.h +0 -75
  516. data/assets/xcode/ios/MRuby.framework/Headers/mruby/gc.h +0 -91
  517. data/assets/xcode/ios/MRuby.framework/Headers/mruby/hash.h +0 -215
  518. data/assets/xcode/ios/MRuby.framework/Headers/mruby/irep.h +0 -75
  519. data/assets/xcode/ios/MRuby.framework/Headers/mruby/istruct.h +0 -47
  520. data/assets/xcode/ios/MRuby.framework/Headers/mruby/khash.h +0 -274
  521. data/assets/xcode/ios/MRuby.framework/Headers/mruby/numeric.h +0 -161
  522. data/assets/xcode/ios/MRuby.framework/Headers/mruby/object.h +0 -44
  523. data/assets/xcode/ios/MRuby.framework/Headers/mruby/opcode.h +0 -69
  524. data/assets/xcode/ios/MRuby.framework/Headers/mruby/ops.h +0 -117
  525. data/assets/xcode/ios/MRuby.framework/Headers/mruby/proc.h +0 -131
  526. data/assets/xcode/ios/MRuby.framework/Headers/mruby/range.h +0 -49
  527. data/assets/xcode/ios/MRuby.framework/Headers/mruby/re.h +0 -16
  528. data/assets/xcode/ios/MRuby.framework/Headers/mruby/string.h +0 -444
  529. data/assets/xcode/ios/MRuby.framework/Headers/mruby/throw.h +0 -55
  530. data/assets/xcode/ios/MRuby.framework/Headers/mruby/value.h +0 -309
  531. data/assets/xcode/ios/MRuby.framework/Headers/mruby/variable.h +0 -136
  532. data/assets/xcode/ios/MRuby.framework/Headers/mruby/version.h +0 -110
  533. data/assets/xcode/ios/MRuby.framework/Headers/mruby.h +0 -1282
  534. data/assets/xcode/ios/MRuby.framework/Info.plist +0 -20
  535. data/assets/xcode/ios/MRuby.framework/MRuby +0 -0
  536. data/assets/xcode/ios/MyApp.xcodeproj/project.pbxproj +0 -368
  537. data/assets/xcode/ios/MyApp.xcodeproj/project.xcworkspace/contents.xcworkspacedata +0 -7
  538. data/assets/xcode/ios/MyApp.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +0 -8
  539. data/assets/xcode/ios/MyApp.xcodeproj/project.xcworkspace/xcshareddata/MyApp.xcscmblueprint +0 -30
  540. data/assets/xcode/ios/main.c +0 -1
  541. data/assets/xcode/tvos/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Large.imagestack/Back.imagestacklayer/Content.imageset/Contents.json +0 -16
  542. data/assets/xcode/tvos/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Large.imagestack/Back.imagestacklayer/Contents.json +0 -6
  543. data/assets/xcode/tvos/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Large.imagestack/Contents.json +0 -17
  544. data/assets/xcode/tvos/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Large.imagestack/Front.imagestacklayer/Content.imageset/Contents.json +0 -16
  545. data/assets/xcode/tvos/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Large.imagestack/Front.imagestacklayer/Contents.json +0 -6
  546. data/assets/xcode/tvos/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Large.imagestack/Middle.imagestacklayer/Content.imageset/Contents.json +0 -16
  547. data/assets/xcode/tvos/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Large.imagestack/Middle.imagestacklayer/Contents.json +0 -6
  548. data/assets/xcode/tvos/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Small.imagestack/Back.imagestacklayer/Content.imageset/Contents.json +0 -16
  549. data/assets/xcode/tvos/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Small.imagestack/Back.imagestacklayer/Contents.json +0 -6
  550. data/assets/xcode/tvos/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Small.imagestack/Contents.json +0 -17
  551. data/assets/xcode/tvos/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Small.imagestack/Front.imagestacklayer/Content.imageset/Contents.json +0 -16
  552. data/assets/xcode/tvos/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Small.imagestack/Front.imagestacklayer/Contents.json +0 -6
  553. data/assets/xcode/tvos/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Small.imagestack/Middle.imagestacklayer/Content.imageset/Contents.json +0 -16
  554. data/assets/xcode/tvos/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Small.imagestack/Middle.imagestacklayer/Contents.json +0 -6
  555. data/assets/xcode/tvos/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Contents.json +0 -32
  556. data/assets/xcode/tvos/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image Wide.imageset/Contents.json +0 -16
  557. data/assets/xcode/tvos/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image.imageset/Contents.json +0 -16
  558. data/assets/xcode/tvos/Assets.xcassets/Contents.json +0 -6
  559. data/assets/xcode/tvos/Assets.xcassets/LaunchImage.launchimage/Contents.json +0 -22
  560. data/assets/xcode/tvos/Info.plist +0 -30
  561. data/assets/xcode/tvos/MRuby.framework/Headers/mrbconf.h +0 -143
  562. data/assets/xcode/tvos/MRuby.framework/Headers/mruby/array.h +0 -280
  563. data/assets/xcode/tvos/MRuby.framework/Headers/mruby/boxing_nan.h +0 -102
  564. data/assets/xcode/tvos/MRuby.framework/Headers/mruby/boxing_no.h +0 -56
  565. data/assets/xcode/tvos/MRuby.framework/Headers/mruby/boxing_word.h +0 -144
  566. data/assets/xcode/tvos/MRuby.framework/Headers/mruby/class.h +0 -97
  567. data/assets/xcode/tvos/MRuby.framework/Headers/mruby/common.h +0 -77
  568. data/assets/xcode/tvos/MRuby.framework/Headers/mruby/compile.h +0 -195
  569. data/assets/xcode/tvos/MRuby.framework/Headers/mruby/data.h +0 -75
  570. data/assets/xcode/tvos/MRuby.framework/Headers/mruby/debug.h +0 -67
  571. data/assets/xcode/tvos/MRuby.framework/Headers/mruby/dump.h +0 -196
  572. data/assets/xcode/tvos/MRuby.framework/Headers/mruby/error.h +0 -75
  573. data/assets/xcode/tvos/MRuby.framework/Headers/mruby/gc.h +0 -91
  574. data/assets/xcode/tvos/MRuby.framework/Headers/mruby/hash.h +0 -215
  575. data/assets/xcode/tvos/MRuby.framework/Headers/mruby/irep.h +0 -75
  576. data/assets/xcode/tvos/MRuby.framework/Headers/mruby/istruct.h +0 -47
  577. data/assets/xcode/tvos/MRuby.framework/Headers/mruby/khash.h +0 -274
  578. data/assets/xcode/tvos/MRuby.framework/Headers/mruby/numeric.h +0 -161
  579. data/assets/xcode/tvos/MRuby.framework/Headers/mruby/object.h +0 -44
  580. data/assets/xcode/tvos/MRuby.framework/Headers/mruby/opcode.h +0 -69
  581. data/assets/xcode/tvos/MRuby.framework/Headers/mruby/ops.h +0 -117
  582. data/assets/xcode/tvos/MRuby.framework/Headers/mruby/proc.h +0 -131
  583. data/assets/xcode/tvos/MRuby.framework/Headers/mruby/range.h +0 -49
  584. data/assets/xcode/tvos/MRuby.framework/Headers/mruby/re.h +0 -16
  585. data/assets/xcode/tvos/MRuby.framework/Headers/mruby/string.h +0 -444
  586. data/assets/xcode/tvos/MRuby.framework/Headers/mruby/throw.h +0 -55
  587. data/assets/xcode/tvos/MRuby.framework/Headers/mruby/value.h +0 -309
  588. data/assets/xcode/tvos/MRuby.framework/Headers/mruby/variable.h +0 -136
  589. data/assets/xcode/tvos/MRuby.framework/Headers/mruby/version.h +0 -110
  590. data/assets/xcode/tvos/MRuby.framework/Headers/mruby.h +0 -1282
  591. data/assets/xcode/tvos/MRuby.framework/Info.plist +0 -20
  592. data/assets/xcode/tvos/MRuby.framework/MRuby +0 -0
  593. data/assets/xcode/tvos/MyApp.xcodeproj/project.pbxproj +0 -350
  594. data/assets/xcode/tvos/MyApp.xcodeproj/project.xcworkspace/contents.xcworkspacedata +0 -7
  595. data/assets/xcode/tvos/MyApp.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +0 -8
  596. data/assets/xcode/tvos/MyApp.xcodeproj/project.xcworkspace/xcshareddata/MyApp.xcscmblueprint +0 -30
  597. data/assets/xcode/tvos/main.c +0 -1
  598. data/ext/ruby2d/common.c +0 -185
  599. data/ext/ruby2d/controllers.c +0 -110
  600. data/ext/ruby2d/gl.c +0 -398
  601. data/ext/ruby2d/gl2.c +0 -86
  602. data/ext/ruby2d/gl3.c +0 -304
  603. data/ext/ruby2d/gles.c +0 -302
  604. data/ext/ruby2d/input.c +0 -48
  605. data/ext/ruby2d/music.c +0 -127
  606. data/ext/ruby2d/sound.c +0 -132
  607. data/lib/ruby2d/cli/console.rb +0 -65
  608. data/lib/ruby2d/cli/enable_console.rb +0 -7
  609. data/lib/ruby2d/cli/platform.rb +0 -17
  610. data/lib/ruby2d/music.rb +0 -77
  611. data/lib/ruby2d/pixel.rb +0 -18
  612. data/lib/ruby2d/pixmap.rb +0 -39
  613. data/lib/ruby2d/pixmap_atlas.rb +0 -56
  614. data/lib/ruby2d/sound.rb +0 -61
  615. data/lib/ruby2d/texture.rb +0 -38
  616. /data/assets/{include/SDL2 → platform/include/SDL3}/SDL_opengles2_gl2.h +0 -0
  617. /data/assets/{include/SDL2 → platform/include/SDL3}/SDL_opengles2_gl2ext.h +0 -0
  618. /data/assets/{include/SDL2 → platform/include/SDL3}/SDL_opengles2_gl2platform.h +0 -0
  619. /data/assets/{include/SDL2 → platform/include/SDL3}/SDL_opengles2_khrplatform.h +0 -0
  620. /data/assets/{include → platform/include}/mruby/re.h +0 -0
data/USAGE.md ADDED
@@ -0,0 +1,2435 @@
1
+ # Ruby 2D Usage Guide
2
+
3
+ Ruby 2D is a 2D graphics library for creating applications, games, visualizations, and anything else you can imagine. This guide covers the complete public API.
4
+
5
+ ## Table of Contents
6
+
7
+ - [Getting Started](#getting-started)
8
+ - [Window](#window)
9
+ - [Colors](#colors)
10
+ - [Shape Basics](#shape-basics)
11
+ - [Lines and Polygons](#lines-and-polygons)
12
+ - [Rectangles and Circles](#rectangles-and-circles)
13
+ - [Images](#images)
14
+ - [Text](#text)
15
+ - [Bitmap Text](#bitmap-text)
16
+ - [Sprites](#sprites)
17
+ - [Tilesets](#tilesets)
18
+ - [Canvas](#canvas)
19
+ - [Audio](#audio)
20
+ - [Button](#button)
21
+ - [Input Events](#input-events)
22
+ - [Gamepads](#gamepads)
23
+ - [Working with Objects](#working-with-objects)
24
+ - [Performance](#performance)
25
+ - [Building Native Applications](#building-native-applications)
26
+ - [Bundling Assets](#bundling-assets)
27
+ - [Building for the Web](#building-for-the-web)
28
+
29
+ ## Getting Started
30
+
31
+ ```ruby
32
+ require 'ruby2d'
33
+
34
+ set title: 'My App'
35
+ set background: 'navy'
36
+
37
+ Square.new(x: 50, y: 50, size: 100, color: 'red')
38
+
39
+ show
40
+ ```
41
+
42
+ Ruby 2D provides two usage patterns:
43
+
44
+ - **DSL pattern** — Top-level methods (`set`, `on`, `update`, `show`, etc.) for quick scripts.
45
+ - **Class pattern** — Subclass `Ruby2D::Window` for structured applications with polling-based input.
46
+
47
+ ### DSL Methods
48
+
49
+ When you `require 'ruby2d'`, these methods are available at the top level:
50
+
51
+ | Method | Description |
52
+ |---|---|
53
+ | `set(opts)` | Set window attributes |
54
+ | `get(sym)` | Get a window attribute by name (e.g. `:width`, `:mouse_x`, `:window`) |
55
+ | `on(event, &block)` | Register an event handler; returns an `EventDescriptor` |
56
+ | `off(descriptor)` | Remove a previously registered event handler |
57
+ | `update { }` | Set the update callback (called every frame) |
58
+ | `render { }` | Set the render callback (called every frame after update) |
59
+ | `elapsed` | Monotonic seconds since the engine started (timing, cooldowns, scheduling) |
60
+ | `clear` | Remove all objects from the window |
61
+ | `screenshot(path = nil)` | Save a screenshot to `path`, or a timestamped file if omitted |
62
+ | `show` | Open the window and start the main loop |
63
+ | `close` | Close the window immediately |
64
+ | `request_render` | Request a frame on the next tick (for `render_mode: :on_demand`) |
65
+
66
+ To use Ruby 2D classes without the DSL mixin, `require 'ruby2d/core'` instead.
67
+
68
+ ### Window Class Pattern
69
+
70
+ For structured applications, subclass `Ruby2D::Window`:
71
+
72
+ ```ruby
73
+ require 'ruby2d/core'
74
+
75
+ class Game < Ruby2D::Window
76
+ include Ruby2D # so shape names like Square resolve without the Ruby2D:: prefix
77
+
78
+ def initialize
79
+ super(title: 'My Game', width: 800, height: 600)
80
+ @player = Square.new(x: 100, y: 100, size: 50, color: 'blue')
81
+ end
82
+
83
+ def update
84
+ @player.x += 1 if key_held?('right')
85
+ @player.x -= 1 if key_held?('left')
86
+ end
87
+ end
88
+
89
+ Game.new.show
90
+ ```
91
+
92
+ `require 'ruby2d/core'` loads the classes under the `Ruby2D` namespace without the top-level mixin that `require 'ruby2d'` adds, so reference shapes as `Ruby2D::Square`, or `include Ruby2D` in your class, as above, to drop the prefix. Override `update` (and/or `render`); each is called every frame independently, so overriding just one is fine.
93
+
94
+ The class pattern provides polling-based input methods (`key_pressed?`, `key_held?`, `key_released?`, `mouse_pressed?`, `mouse_held?`, `mouse_released?`, plus per-`Gamepad` polling; see [Gamepads](#gamepads)) for use inside `update`, in place of the DSL's `on` event handlers.
95
+
96
+ ## Window
97
+
98
+ Ruby 2D is **single-window by design**. The window is created automatically the first time you use the DSL (`set`, `on`, a shape, `show`, …) or when you call `Window.new` (or `.new` on a `Window` subclass). Only one window can exist at a time; constructing a second one raises `Ruby2D::Error`. Both usage patterns share this single window, which is why the top-level DSL and the `Window.*` class methods always act on it.
99
+
100
+ ### Setting Window Attributes
101
+
102
+ ```ruby
103
+ set title: 'My App'
104
+ set width: 800, height: 600
105
+ set background: 'navy'
106
+ set fps_cap: 60
107
+ set icon: 'icon.png'
108
+ set resizable: true
109
+ set highdpi: true
110
+ set pixel_scale: true
111
+ set viewport_width: 320, viewport_height: 240
112
+ set viewport: :letterbox
113
+ set render_mode: :on_demand # or :continuous (default)
114
+ set cursor: :hidden # or :visible, :pointer, :crosshair, etc.
115
+ set show_fps: true
116
+ set diagnostics: true
117
+ set close_on_esc: true
118
+ ```
119
+
120
+ | Option | Type | Default | Description |
121
+ |---|---|---|---|
122
+ | `title` | String | `'Ruby 2D'` | Window title bar text |
123
+ | `width` | Integer | `640` | Window width in pixels |
124
+ | `height` | Integer | `480` | Window height in pixels |
125
+ | `background` | Color | Black | Window background color |
126
+ | `icon` | String | `nil` | Path to a window icon image |
127
+ | `fps_cap` | Number/`:infinity`/nil | `nil` | Frame-rate limit: `nil` = no cap (vsync-driven), a positive number caps at that rate, `:infinity` (or `Float::INFINITY`) renders uncapped. `0`/negative is invalid — the constructor raises, `set` warns and falls back to `nil`. In the browser, a numeric cap snaps to the nearest achievable rate — the display refresh divided by a whole number (e.g. 120Hz → 60, 40, 30…) |
128
+ | `resizable` | Boolean | `false` | Whether the window can be resized |
129
+ | `highdpi` | Boolean | `true` | Enable high-DPI rendering (fixed at window creation; cannot change after `show`) |
130
+ | `pixel_scale` | Boolean | `false` | Scale rendering to match pixel density |
131
+ | `viewport_width` | Integer | Same as `width` | Drawable area width |
132
+ | `viewport_height` | Integer | Same as `height` | Drawable area height |
133
+ | `viewport` | Symbol | `:letterbox` | How the viewport scales when the displayed size differs from the logical size (see [Viewport](#viewport)) |
134
+ | `render_mode` | Symbol | `:continuous` | `:continuous` or `:on_demand` (see [Render Mode](#render-mode)) |
135
+ | `cursor` | Symbol | `:visible` | `:visible`, `:hidden`, or a system cursor name (see [Cursor Control](#cursor-control)) |
136
+ | `show_fps` | Boolean | `false` | Display an FPS counter |
137
+ | `diagnostics` | Boolean | `false` | Print diagnostic (`[INFO]`) messages; also displays the FPS counter (implies `show_fps`) |
138
+ | `close_on_esc` | Boolean | `false` | Close the window when Escape is pressed |
139
+
140
+ These can be set before `show` or changed live afterward — `set title:`, `set width:`/`height:`, `set resizable:`, `set viewport_width:`/`viewport_height:`, and the rest take effect immediately. The one exception is `highdpi`, fixed when the window is created; `pixel_scale` can be toggled live but only has an effect when `highdpi` is enabled.
141
+
142
+ ### Pixel Scale
143
+
144
+ With `pixel_scale: true`, the drawable coordinate space maps 1:1 with physical pixels. On a 2× Retina display, a 640×480 window exposes a 1280×960 coordinate space.
145
+
146
+ ```
147
+ Window.width / Window.height → logical window size (e.g. 640×480)
148
+ Window.viewport_width / viewport_height → physical pixel space (e.g. 1280×960)
149
+ ```
150
+
151
+ Key rules:
152
+
153
+ - **Pass logical pixels to `set`.** `width` and `height` are always in logical pixels, regardless of `pixel_scale`.
154
+ - **Use `viewport_width`/`viewport_height` to size things that fill the window.** These reflect the physical pixel drawable area when `pixel_scale: true`. They are only correct after `show` starts, so create full-viewport objects in the `update` loop on the first frame:
155
+
156
+ ```ruby
157
+ set width: 640, height: 480, pixel_scale: true
158
+
159
+ canvas = nil
160
+
161
+ update do
162
+ next if canvas
163
+ canvas = Canvas.new(width: Window.viewport_width, height: Window.viewport_height)
164
+ end
165
+
166
+ show
167
+ ```
168
+
169
+ - **Use `display_width`/`display_height` to fill the display.** These return the display size in logical pixels — the right unit to pass to `set`:
170
+
171
+ ```ruby
172
+ set width: Window.display_width, height: Window.display_height, pixel_scale: true
173
+ ```
174
+
175
+ - **`display_pixel_width`/`display_pixel_height` are informational.** They return the display size in physical pixels, useful for knowing raw pixel counts but not for passing to `set`.
176
+
177
+ > **Known limitation.** The content scale is fixed when the window opens. Dragging a running window to a monitor with a different pixel density (e.g. from a Retina display to a non-Retina one) does not re-scale the rendering; content keeps drawing at the original monitor's density until the window is reopened.
178
+
179
+ ### Viewport
180
+
181
+ The `viewport:` mode controls how the fixed logical drawing area (`viewport_width` × `viewport_height`) is mapped onto the window whenever the two differ: on resize, on a HiDPI display, or with `pixel_scale`. Pass it to `set viewport: …`:
182
+
183
+ | Mode | Behavior |
184
+ |---|---|
185
+ | `:letterbox` | (default) Scale to fit, preserving aspect ratio; uncovered area shows as letterbox bars. |
186
+ | `:stretch` | Scale to fill the window, ignoring aspect ratio (content may distort). |
187
+ | `:integer` | Scale by whole-number multiples only, preserving aspect ratio (pixel-perfect; may leave a border). |
188
+ | `:overscan` | Scale to fill the window preserving aspect ratio, cropping whatever overflows. |
189
+ | `:expand` | Don't scale a fixed canvas — grow the logical drawing area to match the window, so more content becomes visible as it grows. `viewport_width`/`viewport_height` track the window size. |
190
+ | `:fixed` | No scaling; draw the viewport 1:1 and center it in the window. |
191
+
192
+ ### Reading Window Attributes
193
+
194
+ ```ruby
195
+ get :title
196
+ get :width
197
+ get :height
198
+ get :fps
199
+ get :fps_cap
200
+ get :frames
201
+ get :mouse_x
202
+ get :mouse_y
203
+ get :display_width # display size in logical pixels
204
+ get :display_height
205
+ get :display_pixel_width # display size in physical pixels
206
+ get :display_pixel_height
207
+ get :window # returns the Window instance itself
208
+ ```
209
+
210
+ All readable attributes are also available as methods on the `Window` class:
211
+
212
+ ```ruby
213
+ Window.width
214
+ Window.height
215
+ Window.fps
216
+ Window.mouse_x
217
+ Window.mouse_y
218
+ Window.display_width # display size in logical pixels
219
+ Window.display_height
220
+ Window.display_pixel_width # display size in physical pixels
221
+ Window.display_pixel_height
222
+ ```
223
+
224
+ ### Screenshots
225
+
226
+ ```ruby
227
+ screenshot './my_screenshot.png' # save to a specific path
228
+ screenshot # auto-generated timestamped filename
229
+
230
+ Window.screenshot './my_screenshot.png' # also available on the Window class
231
+ ```
232
+
233
+ The file is written at the end of the frame it was requested in, so `screenshot` returns the path before the file exists; it's on disk by the time the next `update` runs. Requesting one forces that frame to render, so a capture in `:on_demand` mode never grabs a parked frame, and this saves the image even though it closes the window in the same tick:
234
+
235
+ ```ruby
236
+ update do
237
+ screenshot './my_screenshot.png'
238
+ close
239
+ end
240
+ ```
241
+
242
+ Taking a screenshot after the window has closed raises, since no frame is left to write it.
243
+
244
+ On [the web](#building-for-the-web), `screenshot` does nothing and returns `nil`: the only filesystem there is Emscripten's in-memory one, so it could only write a file nobody can open.
245
+
246
+ ### Cursor Control
247
+
248
+ ```ruby
249
+ set cursor: :hidden
250
+ set cursor: :visible
251
+
252
+ # Or directly:
253
+ Window.cursor = :visible
254
+ Window.cursor = :hidden
255
+ Window.cursor # => :default, :hidden, :pointer, etc.
256
+ ```
257
+
258
+ `:visible` is a set-only convenience meaning "show the default arrow"; after setting it, the getter reports the active style (`:default` for a plain visible cursor, or a system-cursor name) or `:hidden`, never `:visible`.
259
+
260
+ #### System Cursors
261
+
262
+ Set the cursor to any system cursor style by passing its name to `set cursor:`:
263
+
264
+ ```ruby
265
+ set cursor: :pointer # pointing hand (links)
266
+ set cursor: :text # I-beam (text fields)
267
+ set cursor: :crosshair # crosshair
268
+ set cursor: :wait # hourglass / spinner
269
+ set cursor: :progress # busy with arrow
270
+ set cursor: :move # four-way arrow
271
+ set cursor: :not_allowed # slashed circle
272
+ set cursor: :default # default arrow
273
+
274
+ # Resize cursors
275
+ set cursor: :ew_resize # horizontal double arrow
276
+ set cursor: :ns_resize # vertical double arrow
277
+ set cursor: :nwse_resize # diagonal double arrow (NW–SE)
278
+ set cursor: :nesw_resize # diagonal double arrow (NE–SW)
279
+ set cursor: :n_resize # edge resize: north
280
+ set cursor: :ne_resize # edge resize: north-east
281
+ set cursor: :e_resize # edge resize: east
282
+ set cursor: :se_resize # edge resize: south-east
283
+ set cursor: :s_resize # edge resize: south
284
+ set cursor: :sw_resize # edge resize: south-west
285
+ set cursor: :w_resize # edge resize: west
286
+ set cursor: :nw_resize # edge resize: north-west
287
+
288
+ # Also available as a direct setter:
289
+ Window.cursor = :pointer
290
+ ```
291
+
292
+ ### Update Loop
293
+
294
+ The `update` block runs every frame and is the place to put application logic — updating state, responding to input, animating objects:
295
+
296
+ ```ruby
297
+ update do
298
+ @box.x += 1
299
+ end
300
+
301
+ show
302
+ ```
303
+
304
+ The loop runs at the display refresh rate, or up to `fps_cap` if one is set. To run uncapped — no frame limit, useful for benchmarking — set `fps_cap: :infinity`.
305
+
306
+ #### Frame-rate independence with `dt`
307
+
308
+ The block above moves the box by 1 pixel per frame, which means it travels at 60 px/s on a 60Hz display but 120 px/s on a 120Hz display. To make motion behave the same regardless of refresh rate, accept a `dt` argument — wall-clock seconds since the previous update — and scale by it:
309
+
310
+ ```ruby
311
+ update do |dt|
312
+ @box.x += 60 * dt # always 60 px/s, on any display
313
+ end
314
+ ```
315
+
316
+ `dt` is also available as `Window#delta_time` for the class pattern:
317
+
318
+ ```ruby
319
+ class MyApp < Ruby2D::Window
320
+ def update
321
+ @box.x += 60 * delta_time
322
+ end
323
+ end
324
+ ```
325
+
326
+ `dt` is `0.0` on the first frame and is clamped to a maximum of `0.1` seconds, so a paused window or stalled frame won't cause the next update to leap forward dramatically.
327
+
328
+ > **Use `dt`/`elapsed` for timing, not the system clock.** The native and web builds run on mruby, where the usual Ruby clocks don't port: `Process.clock_gettime` doesn't exist, and `Time.now` is a coarse wall clock whose millisecond-since-epoch values overflow mruby's 32-bit integers in the browser. Scale per-frame motion by the `dt` arg (also `Window#delta_time`); for absolute time — cooldowns, scheduling, "time since" — read `elapsed` (monotonic seconds since the engine started). Both are cross-platform and overflow-safe. Save `Time.now` for wall-clock dates.
329
+
330
+ #### Absolute time with `elapsed`
331
+
332
+ `elapsed` returns monotonic seconds since the engine started. Reach for it when `dt` is awkward — most often a cooldown or "every N seconds" check inside an event handler, which gets no `dt`:
333
+
334
+ ```ruby
335
+ # Fire at most once every 0.5s, even on rapid clicks:
336
+ on :mouse_down do
337
+ next unless elapsed >= (@next_shot || 0)
338
+ shoot
339
+ @next_shot = elapsed + 0.5
340
+ end
341
+ ```
342
+
343
+ It starts near `0` when the program launches, never ticks backward, and reads the same on CRuby and the mruby/web builds. For frame-to-frame motion, prefer `dt`.
344
+
345
+ ### Render Block
346
+
347
+ The `render` block also runs every frame, immediately after `update`. Use it for one-off drawing with `.render` methods:
348
+
349
+ ```ruby
350
+ render do
351
+ Rectangle.render(x: 0, y: 0, width: 50, height: 50, color: 'red')
352
+ end
353
+ ```
354
+
355
+ Objects created with `.new` are rendered automatically; the `render` block is only needed when you want per-frame custom drawing. For scenes that redraw many things every frame, persistent objects are cheaper; see [Performance](#performance).
356
+
357
+ By default the block draws **on top of every object**; it is the frontmost layer. Pass `z:` to place it elsewhere in the scene's [z-order](#managing-objects):
358
+
359
+ ```ruby
360
+ render do … end # :foreground (default), on top of all objects
361
+ render z: :background do … end # behind all objects
362
+ render z: 10 do … end # interleaved at that depth (same scale as object z)
363
+ ```
364
+
365
+ For a number, objects with `z` at or below it draw first, then the block, then the rest. This is how a persistent HUD sits above per-frame drawing: put backdrop objects at a low `z`, draw the world in a `render z: 10` block, and keep the HUD objects at a higher `z` (say `20`) so they stay on top.
366
+
367
+ > **Two `render`s, same word.** `render do … end` (a block) *registers* the callback shown above. `Shape.render(…)` (a method call) *draws* a single frame inside that callback. The first sets up the loop; the second runs inside it.
368
+
369
+ ### Render Mode
370
+
371
+ Ruby 2D supports two rendering modes that control when the window presents frames:
372
+
373
+ - **`:continuous`** (default) — the window renders every tick up to `fps_cap`. Right for games and anything with continuous animation.
374
+ - **`:on_demand`** — the window only presents a frame when `request_render` is called or when the OS signals a redraw (resize, expose, display change). Input, `update`, and frame pacing still run every tick, so the app remains responsive.
375
+
376
+ On-demand mode lets the GPU reach its deepest sleep state when nothing is changing on screen, useful for charts, editors, dashboards, and other non-game GUIs where the contents only change in response to input.
377
+
378
+ ```ruby
379
+ set render_mode: :on_demand
380
+
381
+ on :mouse_down do
382
+ @clicked = true
383
+ request_render # explicitly ask for the next frame
384
+ end
385
+
386
+ show
387
+ ```
388
+
389
+ Things to know:
390
+
391
+ - **The first frame is always rendered.** You don't need to call `request_render` for the window to appear.
392
+ - **Animations are opt-in.** A blinking caret, spinner, or fade-out needs to call `request_render` on whatever cadence you want; if nothing asks for frames, frames stop.
393
+ - **`request_render` is thread-safe** and idempotent within a tick. Calling it multiple times before the next frame has no extra cost.
394
+ - **`Canvas` mutations auto-request a render.** Every `Canvas#fill_*`, `Canvas#stroke_*`, `Canvas#draw_*`, and `Canvas#clear` call marks the next frame dirty for you, so you don't need `request_render` after canvas drawing. Mutating shape attributes (`rect.x = 100`, etc.) does *not*; call `request_render` yourself for those.
395
+ - **`show_fps` / `diagnostics` counters freeze in `:on_demand` mode** because the overlay is drawn as part of the frame. FPS is not meaningful when rendering is demand-driven.
396
+
397
+ ### Closing the Window
398
+
399
+ Call `close` to shut down the window immediately:
400
+
401
+ ```ruby
402
+ close
403
+ ```
404
+
405
+ To run code just before the window closes — whether the user clicks the OS close button or `close` is called — register a handler with `on(:close)`:
406
+
407
+ ```ruby
408
+ on :close do
409
+ puts 'Goodbye!'
410
+ end
411
+ ```
412
+
413
+ Unlike keyboard and mouse events, only one `:close` handler can be registered at a time. Registering a new one replaces the previous. You can also remove it explicitly:
414
+
415
+ ```ruby
416
+ handler = on :close do
417
+ puts 'Goodbye!'
418
+ end
419
+
420
+ off handler
421
+ ```
422
+
423
+ `close` and `on(:close)` work together: calling `close` fires the handler before shutting down.
424
+
425
+ On [the web](#building-for-the-web), `close` does nothing: a page can't close itself, only the person viewing it can. The handler isn't run and the app keeps going, so anything you'd put after a `close` — a farewell screen, a final score — should be drawn instead of waited for. A quit the viewer initiates still fires `on(:close)`.
426
+
427
+ ## Colors
428
+
429
+ Colors can be specified as:
430
+
431
+ - **Named color**: `'red'`, `'blue'`, `'green'`, etc.
432
+ - **Hex string**: `'#FF0000'`, `'#F00'`, `'#FF000080'` (with alpha)
433
+ - **Color array**: `[r, g, b]` or `[r, g, b, a]`, with each channel a number on the 0.0–1.0 scale (`[1.0, 0.5, 0.0]`). Alpha is optional and defaults to opaque. For 0–255 byte values, use a hex string instead.
434
+ - **`'random'`**: Generates a random color
435
+ - **Color object**: `Color.new('red')`
436
+
437
+ Channels are plain numbers on the 0.0–1.0 scale, the standard graphics convention, so `[1, 0, 0]` is full-intensity red and `[0, 0, 0]` is black; integer `0` and `1` work as the endpoints. Out-of-range channels emit a one-time warning and are clamped into range.
438
+
439
+ ### Named Colors
440
+
441
+ Based on [clrs.cc](https://clrs.cc): `navy`, `blue`, `aqua`, `teal`, `olive`, `green`, `lime`, `yellow`, `orange`, `red`, `brown`, `fuchsia`, `purple`, `maroon`, `white`, `silver`, `gray`, `black`.
442
+
443
+ ### Color Class
444
+
445
+ ```ruby
446
+ c = Color.new('red')
447
+ c = Color.new('#FF0000')
448
+ c = Color.new([1.0, 0.0, 0.0]) # rgb (0.0–1.0)
449
+ c = Color.new([1.0, 0.0, 0.0, 1.0]) # rgba (0.0–1.0)
450
+
451
+ c.r # => red component (always stored as 0.0..1.0)
452
+ c.g # => green component
453
+ c.b # => blue component
454
+ c.a # => alpha component
455
+ c.opacity # => alias for .a
456
+ c.opacity = 0.5
457
+ c.to_a # => [r, g, b, a]
458
+
459
+ Color.valid?('red') # => true
460
+ Color.hex?('#FF0000') # => true
461
+ ```
462
+
463
+ > **Note**: `colour` is accepted as a synonym for `color` everywhere, including compound kwargs (`stroke_colour`, `hover_colour`, `label_colour`).
464
+
465
+ > **Tip**: Color strings (names and hex) are parsed once and cached internally, so passing `color: 'red'` on a hot path is effectively as fast as passing a pre-built `Color`. You don't need to hoist `Color.new('red')` out of the loop to avoid parse overhead.
466
+
467
+ ### Per-Vertex Colors
468
+
469
+ Shapes that support per-vertex coloring accept an array of colors, one per vertex:
470
+
471
+ ```ruby
472
+ Triangle.new(
473
+ color: ['red', 'green', 'blue'] # one per vertex
474
+ )
475
+ ```
476
+
477
+ The array length must match the shape's vertex count: 3 for `Triangle`, 4 for `Quad` / `Rectangle` / `Square`, N for `Polygon` / `Polyline` (matching `points`). `Circle` and `Ellipse` are single-color only. `Line` is the special case: its array is 2 colors `[start, end]` for a gradient along the length.
478
+
479
+ Reading `color` back returns the same kind you set: a `Color` for a uniform fill, or a `Color::Set` for a per-vertex / gradient fill. Index a set's individual stops with `color.vertex(i)`, and `color.opacity` reports the first vertex's alpha (see [Opacity](#opacity)).
480
+
481
+ ### Opacity
482
+
483
+ All renderable objects support opacity through the `opacity:` keyword:
484
+
485
+ ```ruby
486
+ Square.new(x: 0, y: 0, size: 100, color: 'red', opacity: 0.5)
487
+ ```
488
+
489
+ Read or change it after creation with the `opacity` accessor:
490
+
491
+ ```ruby
492
+ shape.opacity # => 0.5
493
+ shape.opacity = 0.25
494
+ ```
495
+
496
+ For per-vertex fills, the getter returns the alpha of the first color, and the setter assigns the same alpha to every vertex. The underlying `shape.color.opacity = ...` form still works if you need it.
497
+
498
+ On stroked shapes (`Circle`, `Ellipse`, `Triangle`, `Quad`, `Polygon`), both `opacity:` at construction and the `opacity=` setter fade the fill **and** the stroke together. To fade them independently, set each color's opacity directly:
499
+
500
+ ```ruby
501
+ shape.color.opacity = 0.25 # fill only
502
+ shape.stroke_color.opacity = 1.0 # stroke only
503
+ ```
504
+
505
+ `Polyline` and `Canvas#draw_polyline` also accept a per-vertex `opacity:` array (one value per vertex), interpolated along the path the same way per-vertex `color:` is:
506
+
507
+ ```ruby
508
+ Polyline.new(
509
+ points: [[50, 200], [150, 150], [250, 200], [350, 120]],
510
+ stroke_width: 3, color: 'orange',
511
+ opacity: [1.0, 1.0, 1.0, 0.35]
512
+ )
513
+ ```
514
+
515
+ When `opacity:` is a single value, all vertices share it. When it's an array, each entry overrides the alpha of the corresponding vertex. The array length must equal the vertex count.
516
+
517
+ Opacity is clamped to the `0.0..1.0` range. A fade animation that momentarily overshoots (e.g. `opacity = 1.2` or `-0.1`) is pinned to fully opaque or fully transparent for that frame rather than raising, consistent with how runtime size setters degrade gracefully (see [Dimensions](#dimensions)).
518
+
519
+ ## Shape Basics
520
+
521
+ Common properties and patterns shared by every shape (`Line`, `Triangle`, `Quad`, `Rectangle`, `Square`, `Circle`, `Ellipse`, `Polygon`, `Polyline`). For each shape's full parameter list and examples, see [Lines and Polygons](#lines-and-polygons) and [Rectangles and Circles](#rectangles-and-circles).
522
+
523
+ All shapes are automatically added to the window when created. Every shape includes the `Renderable` module and shares these common features:
524
+
525
+ ```ruby
526
+ shape.x # x position
527
+ shape.y # y position
528
+ shape.z # depth (drawing order); higher z is drawn on top
529
+ shape.z = 10 # changing z re-inserts the object in the correct order
530
+ shape.width # width (bounding-box extent; e.g. a Circle's diameter)
531
+ shape.height # height (bounding-box extent)
532
+ shape.color # the color or color set
533
+ shape.color = 'blue'
534
+ shape.opacity # alpha of the color (per-vertex fills return the first vertex's alpha)
535
+ shape.opacity = 0.5
536
+ shape.add # add to the window (done automatically on creation)
537
+ shape.remove # remove from the window
538
+ shape.contains?(x, y) # hit-testing
539
+ ```
540
+
541
+ ### Dimensions
542
+
543
+ Size parameters — `width`, `height`, `radius`, `xradius`, `yradius`, `size` — must be **zero or positive**. A negative value at construction raises `ArgumentError`, because a negative extent still renders but disagrees with `contains?` hit-testing. Zero is allowed (the shape collapses to a point).
544
+
545
+ Runtime setters (`circle.radius = …`, `rect.width = …`) are deliberately **not** guarded, so an animation whose size momentarily dips below zero won't raise mid-run. The frame still renders: a negative extent draws a mirrored shape rather than nothing, and it won't agree with `contains?` hit-testing. Keep animated dimensions non-negative for predictable rendering.
546
+
547
+ ### Anchor Point (`x`, `y`)
548
+
549
+ What `shape.x` / `shape.y` mean depends on how the shape is defined:
550
+
551
+ - **Vertex-defined shapes** — `Triangle`, `Quad`, `Polygon`, `Polyline` — `x`/`y` is the **centroid** (the average of the vertex coordinates). Setting `tri.x = 100` translates every vertex by the same offset to move the centroid to that x.
552
+ - **Bounding-box shapes** — `Rectangle`, `Square`, `Image`, `Text`, `BitmapText`, `Sprite`, `Canvas` — `x`/`y` is the **top-left corner**. Setting `rect.x = 100` moves the rectangle so its left edge is at 100.
553
+ - **Center-defined shapes** — `Circle`, `Ellipse` — `x`/`y` is the **center**. Setting `circle.x = 100` moves the center to 100.
554
+
555
+ Rotation defaults to the shape's natural center: the centroid for vertex-defined shapes, the bounding-box center for bounding-box shapes, and the anchor itself for center-defined shapes. Override with `rx:` / `ry:` at construction or via the accessors.
556
+
557
+ ### Fill and Stroke
558
+
559
+ All closed shapes (`Triangle`, `Quad`, `Rectangle`, `Square`, `Circle`, `Ellipse`, `Polygon`) support both fill and stroke. By default shapes are filled; pass `stroke_width:` to add an outline.
560
+
561
+ ```ruby
562
+ # Filled (default)
563
+ Rectangle.new(x: 10, y: 10, width: 100, height: 50, color: 'red')
564
+
565
+ # Filled + outlined
566
+ Rectangle.new(x: 10, y: 10, width: 100, height: 50,
567
+ color: 'red', stroke_width: 3, stroke_color: 'white')
568
+
569
+ # Outline only
570
+ Rectangle.new(x: 10, y: 10, width: 100, height: 50,
571
+ fill: false, stroke_width: 3, stroke_color: 'white')
572
+ ```
573
+
574
+ | Parameter | Default | Description |
575
+ |---|---|---|
576
+ | `fill` | `true` | Whether to fill the shape |
577
+ | `stroke_width` | `0` | Outline thickness in pixels (0 = no outline) |
578
+ | `stroke_color` | Fill color | Outline color (single or per-vertex array matching the shape's vertex count) |
579
+
580
+ Notes:
581
+ - `stroke_color` defaults to the fill color. If the fill is per-vertex, the stroke gets the same per-vertex set so the outline traces the same gradient around the perimeter. Set it explicitly when you want a different outline color.
582
+ - `stroke_color:` accepts the same per-vertex vocabulary as `color:`; each vertex gets one color and pixels along each edge interpolate between the two endpoint colors. `Circle` and `Ellipse` strokes are single-color only.
583
+ - `opacity:` applies to both fill and stroke when both are drawn.
584
+ - Strokes rendered on persistent shapes use the same geometry as Canvas `stroke_*` methods, so a scene-graph outline and a Canvas outline of the same shape line up pixel-for-pixel (with minor anti-aliasing differences).
585
+ - Strokes are **centered on the shape boundary**: half the stroke width falls inside the shape, half outside. On a Canvas, any portion that falls outside the surface bounds is clipped; on persistent shapes, it renders freely to the window. Inset Canvas-drawn shapes by `stroke_width / 2` if you want the full stroke visible.
586
+ - `Line` and `Polyline` are always strokes; `fill:`, `stroke_color:`, and the fill-specific semantics do not apply to them.
587
+
588
+ ### Rotation
589
+
590
+ Every shape supports rotation via the `rotate` attribute (in degrees), with one exception: a `Tileset` has no whole-object `rotate` — a grid of tiles has no single pivot, so its tiles rotate individually, per tile type, through `define(..., rotate:)`. The rotation center defaults to the shape's natural center but can be overridden with `rx` and `ry`:
591
+
592
+ ```ruby
593
+ rect = Rectangle.new(x: 100, y: 100, width: 50, height: 50, rotate: 45)
594
+ rect.rotate = 90
595
+ rect.rx = 0 # rotate around the origin instead
596
+ rect.ry = 0
597
+ ```
598
+
599
+ `contains?` (and the object events built on it: `:click`, `:hover`, `:drag`, …) respects rotation: the clickable region matches the rotated shape you see, not its unrotated footprint. It tests against the **rendered fill**: for `Triangle`, `Quad`, and `Polygon` that's the filled area. For `Line` and `Polyline` it matches the drawn stroke, within half the stroke width along each segment, mitered at the corners, and butt-capped at the open ends (so it doesn't extend past them). A `Triangle` is always convex and hit-tests exactly against what's drawn, as do convex and concave `Quad`s and `Polygon`s. Only self-intersecting `Quad`s and `Polygon`s are unreliable: the fill renderer doesn't fully support them (it may draw only part of the shape), so `contains?` may not match the drawn pixels there; use `Polyline.new(closed: true)` plus `Canvas#fill_polygon` for such shapes. Points exactly on an edge follow the rasterized boundary (the lower/left edges count as inside, the upper/right edges as outside).
600
+
601
+ ### One-Shot Rendering
602
+
603
+ Shapes provide a class-level `.render` method for one-off drawing inside the window's `render do … end` block, which fires every frame after `update`. The block *registers* a callback; `Shape.render(…)` *draws* a single frame inside it. Arguments mirror `.new`'s visual and geometry kwargs: same defaults, same color vocabulary (names, hex, arrays, `Color`, per-vertex). Because a one-shot draw has no persistent object to order, register, or align, `.render` omits the scene-graph and lifecycle kwargs `.new` accepts (`z:`, `add:`, `visible:`, and the `padding*` keywords):
604
+
605
+ ```ruby
606
+ render do
607
+ Rectangle.render(x: 0, y: 0, width: 50, height: 50, color: 'red')
608
+ Circle.render(x: 100, y: 100, radius: 25, color: 'green', opacity: 0.5)
609
+ end
610
+ ```
611
+
612
+ `.render` produces no object: no per-object events, no `contains?`, no `z` ordering. Use `.new` when you need any of those.
613
+
614
+ #### Why only shapes have class-level `.render`
615
+
616
+ `Image`, `Text`, `BitmapText`, `Sprite`, `Canvas`, and `Tileset` **do not** offer a class-level `.render` (no `Image.render(...)`, no `Text.render(...)`). That's intentional. Shapes are cheap to describe per frame: a few coordinates and colors pushed straight to the GPU. Textured and text objects, by contrast, own a texture, glyph atlas, or pixel buffer that's expensive to build. Recreating that on every frame would be wasteful.
617
+
618
+ Their pattern is "construct once, render many" — build the object with `add: false` to keep it out of the scene graph, then call `instance.render(...)` with per-frame overrides inside your `render` block:
619
+
620
+ ```ruby
621
+ label = BitmapText.new('FPS', add: false)
622
+ img = Image.new('hero.png', add: false)
623
+
624
+ render do
625
+ label.render(x: 10, y: 10, color: 'lime')
626
+ img.render(x: Window.mouse_x, y: Window.mouse_y)
627
+ end
628
+ ```
629
+
630
+ The texture/atlas is built once when `.new` runs, then reused on each `.render` call. This gives you the same "no persistent object" effect as shapes' class-level `.render`, without rebuilding expensive state per frame.
631
+
632
+ ### Construction-time `add:` and `visible:`
633
+
634
+ Every renderable accepts `add:` and `visible:` at construction (both default to `true`). `add: false` builds the object without registering it in the window's scene graph, useful for entities you'll spawn later, or for the render-block override pattern (see [Images](#images), [Text](#text), etc., below) where you draw the object yourself each frame via `instance.render(...)`. `visible: false` keeps the object in the scene graph but skips drawing it.
635
+
636
+ An `add: false` object is not drawn, so it also doesn't receive object events (`:click`, `:hover`, …) even if you attach handlers; it isn't part of the scene until you `add` it. A hidden object (`visible: false`) stays in the scene graph and keeps receiving events. For an invisible-but-clickable region, use a [visual-less `Button`](#visual-less-button-hit-area).
637
+
638
+ ```ruby
639
+ powerup = Square.new(x: 200, y: 200, size: 32, color: 'yellow', add: false)
640
+ powerup.add # later, when the player triggers the spawn condition
641
+ ```
642
+
643
+ For runtime toggling (`.add` / `.remove` for scene-graph membership and `.show` / `.hide` for visibility), see [Managing Objects](#managing-objects).
644
+
645
+ ### Aligning to the window
646
+
647
+ Pass a symbol to `x:` or `y:` to align an object against the window instead of computing pixels:
648
+
649
+ ```ruby
650
+ Text.new('Paused', x: :center, y: :center)
651
+ Image.new('logo.png', x: :right, y: :top)
652
+ Rectangle.new(x: :center, y: :bottom, width: 200, height: 40)
653
+ ```
654
+
655
+ Accepted values: `:left`, `:center`, `:right` for `x`; `:top`, `:center`, `:bottom` for `y`. Resolution happens at draw time against `Window.viewport_width` / `viewport_height` and the object's own measured `width` / `height`, so a `:center`-aligned `Text` re-centers automatically when its `content` changes, and any aligned object follows window resizes.
656
+
657
+ `obj.x` and `obj.y` return the resolved numeric position (so hit-testing, `contains?`, and event handlers work normally). The intent is exposed separately as `obj.x_align` and `obj.y_align`. Assigning a number explicitly (`text.x = 100`) clears the alignment intent; assigning a symbol (`text.x = :center`) sets it, and so does `text.x_align = :center`.
658
+
659
+ This works for every shape with a well-defined bounding box: `Text`, `Rectangle`, `Square`, `Circle`, `Ellipse`, `Image`, `Sprite`, and `Button`. For the center-anchored `Circle` and `Ellipse`, the bounding box is aligned just like a rectangle's — `:left` puts the left edge against the wall, `:center` centers it — and the anchor (the center) follows. Shapes with no edge to hug don't support symbolic alignment: the centroid-anchored `Quad`, `Triangle`, `Polygon`, and `Polyline`, plus the pixel-buffer `Canvas` and `BitmapText`. Passing a symbol to their `x` / `y` raises a clear error rather than guessing; use `Text` for aligned text. A free-form `Line` has no `x` / `y` at all; it's positioned through its endpoints `x1` / `y1` / `x2` / `y2`.
660
+
661
+ #### Padding
662
+
663
+ Inset an aligned object from the edge it's anchored to:
664
+
665
+ ```ruby
666
+ Text.new('Score: 0', x: :right, y: :top, padding: 16) # 16px from top and right
667
+ Button.new(x: :center, y: :bottom, padding_bottom: 24, label: 'Start')
668
+ ```
669
+
670
+ `padding:` sets all four edges. Per-edge kwargs (`padding_top`, `padding_right`, `padding_bottom`, `padding_left`) override the uniform value for individual edges. Padding only takes effect on edge-anchored axes; it's a no-op for `:center` and for axes with a numeric position. Negative values are allowed (the object pushes past the edge). All four are also runtime accessors (`obj.padding_top = 8`).
671
+
672
+ > **Timing.** Aligned positions are `0` until the first frame draws, because the window's viewport dimensions are only known once `show` starts. If you read `obj.x` before the first render, you'll see the placeholder.
673
+
674
+ ## Lines and Polygons
675
+
676
+ Vertex-defined shapes. For `Triangle`, `Quad`, `Polygon`, and `Polyline`, `x` / `y` refer to the **centroid** — the average of the vertex coordinates — and setting `x=` translates every vertex by the same offset. `Line` is the exception: it has no `x`/`y` and is positioned through its endpoints `x1`/`y1`/`x2`/`y2`. For fill / stroke, rotation, alignment, and other common properties, see [Shape Basics](#shape-basics).
677
+
678
+ ### Line
679
+
680
+ ```ruby
681
+ Line.new(x1: 0, y1: 0, x2: 100, y2: 100)
682
+ # or:
683
+ Line.new(points: [[0, 0], [100, 100]])
684
+ ```
685
+
686
+ | Parameter | Default | Description |
687
+ |---|---|---|
688
+ | `points` | `nil` | `[[x, y], [x, y]]` — overrides the numbered kwargs if given |
689
+ | `x1` | `0` | Start x |
690
+ | `y1` | `0` | Start y |
691
+ | `x2` | `100` | End x |
692
+ | `y2` | `100` | End y |
693
+ | `z` | `0` | Depth |
694
+ | `stroke_width` | `1` | Line thickness |
695
+ | `dash` | `0` | Dash length in pixels (`0` = solid) |
696
+ | `gap` | `5` | Gap length in pixels (when dashed) |
697
+ | `rotate` | `0` | Rotation in degrees |
698
+ | `rx`, `ry` | Midpoint | Rotation center |
699
+ | `color` | `'white'` | Color (single, or 2-element array `[start, end]` for a gradient along the length; dashed lines interpolate endpoint colors per dash) |
700
+ | `opacity` | `nil` | Alpha override |
701
+ | `add` | `true` | Add to the window's scene graph on construction |
702
+ | `visible` | `true` | Initial visibility (drawn each frame while in the scene graph) |
703
+
704
+ **Example:**
705
+
706
+ ```ruby
707
+ line = Line.new(x1: 0, y1: 0, x2: 200, y2: 200)
708
+ line.length # => geometric length of the line
709
+ line.x1 = 50
710
+ line.stroke_width = 5
711
+
712
+ # Dashed line: set `dash:` (and optionally `gap:`) to opt in.
713
+ Line.new(x1: 10, y1: 50, x2: 400, y2: 50,
714
+ dash: 15, gap: 8, stroke_width: 2, color: 'yellow')
715
+ ```
716
+
717
+ ### Triangle
718
+
719
+ ```ruby
720
+ Triangle.new(x1: 50, y1: 0, x2: 100, y2: 100, x3: 0, y3: 100)
721
+ # or:
722
+ Triangle.new(points: [[50, 0], [100, 100], [0, 100]])
723
+ ```
724
+
725
+ | Parameter | Default | Description |
726
+ |---|---|---|
727
+ | `points` | `nil` | `[[x, y], [x, y], [x, y]]` — overrides the numbered kwargs if given |
728
+ | `x1` | `50` | Vertex 1 x |
729
+ | `y1` | `0` | Vertex 1 y |
730
+ | `x2` | `100` | Vertex 2 x |
731
+ | `y2` | `100` | Vertex 2 y |
732
+ | `x3` | `0` | Vertex 3 x |
733
+ | `y3` | `100` | Vertex 3 y |
734
+ | `z` | `0` | Depth |
735
+ | `rotate` | `0` | Rotation in degrees |
736
+ | `rx`, `ry` | Centroid | Rotation center |
737
+ | `color` | `'white'` | Color (single or 3-element array for per-vertex) |
738
+ | `opacity` | `nil` | Alpha override |
739
+ | `fill` | `true` | Whether to fill (see [Fill and Stroke](#fill-and-stroke)) |
740
+ | `stroke_width` | `0` | Outline thickness |
741
+ | `stroke_color` | Fill color | Outline color |
742
+ | `add` | `true` | Add to the window's scene graph on construction |
743
+ | `visible` | `true` | Initial visibility (drawn each frame while in the scene graph) |
744
+
745
+ **Example:**
746
+
747
+ ```ruby
748
+ tri = Triangle.new(x1: 50, y1: 0, x2: 100, y2: 100, x3: 0, y3: 100, color: 'green')
749
+ tri.x = 200 # translates all vertices (x and y refer to the centroid)
750
+ tri.color = 'red'
751
+ ```
752
+
753
+ ### Quad
754
+
755
+ A quadrilateral defined by four vertices in clockwise order.
756
+
757
+ ```ruby
758
+ Quad.new(x1: 0, y1: 0, x2: 100, y2: 0, x3: 100, y3: 100, x4: 0, y4: 100)
759
+ # or:
760
+ Quad.new(points: [[0, 0], [100, 0], [100, 100], [0, 100]])
761
+ ```
762
+
763
+ | Parameter | Default | Description |
764
+ |---|---|---|
765
+ | `points` | `nil` | `[[x, y], [x, y], [x, y], [x, y]]` (clockwise from top-left) — overrides the numbered kwargs if given |
766
+ | `x1`, `y1` | `0, 0` | Top-left vertex |
767
+ | `x2`, `y2` | `100, 0` | Top-right vertex |
768
+ | `x3`, `y3` | `100, 100` | Bottom-right vertex |
769
+ | `x4`, `y4` | `0, 100` | Bottom-left vertex |
770
+ | `z` | `0` | Depth |
771
+ | `rotate` | `0` | Rotation in degrees |
772
+ | `rx`, `ry` | Center of vertices | Rotation center |
773
+ | `color` | `'white'` | Color (single or 4-element array for per-vertex) |
774
+ | `opacity` | `nil` | Alpha override |
775
+ | `fill` | `true` | Whether to fill (see [Fill and Stroke](#fill-and-stroke)) |
776
+ | `stroke_width` | `0` | Outline thickness |
777
+ | `stroke_color` | Fill color | Outline color |
778
+ | `add` | `true` | Add to the window's scene graph on construction |
779
+ | `visible` | `true` | Initial visibility (drawn each frame while in the scene graph) |
780
+
781
+ **Example:**
782
+
783
+ ```ruby
784
+ quad = Quad.new(x1: 0, y1: 0, x2: 80, y2: 0, x3: 100, y3: 100, x4: 20, y4: 100)
785
+ quad.x = 150 # translates all four vertices by the same offset
786
+ quad.color = ['red', 'green', 'blue', 'yellow']
787
+ ```
788
+
789
+ ### Polygon
790
+
791
+ A closed polygon defined by N ≥ 3 vertices supplied as `[[x, y], [x, y], ...]`. Supports single or per-vertex colors.
792
+
793
+ ```ruby
794
+ Polygon.new(points: [[100, 50], [200, 100], [180, 200], [80, 200], [50, 120]])
795
+ ```
796
+
797
+ | Parameter | Default | Description |
798
+ |---|---|---|
799
+ | `points` | (required) | Array of `[x, y]` pairs (N ≥ 3) |
800
+ | `z` | `0` | Depth |
801
+ | `rotate` | `0` | Rotation in degrees |
802
+ | `rx`, `ry` | Centroid | Rotation center |
803
+ | `color` | `'white'` | Single color, or per-vertex array of length N |
804
+ | `opacity` | `nil` | Alpha override |
805
+ | `fill` | `true` | Whether to fill (see [Fill and Stroke](#fill-and-stroke)) |
806
+ | `stroke_width` | `0` | Outline thickness |
807
+ | `stroke_color` | Fill color | Outline color |
808
+ | `add` | `true` | Add to the window's scene graph on construction |
809
+ | `visible` | `true` | Initial visibility (drawn each frame while in the scene graph) |
810
+
811
+ Polygon fills work best for convex and simple non-convex shapes. For complex concave shapes, use `Polyline.new(closed: true)` for the outline and `Canvas#fill_polygon` for the interior if a fill is needed.
812
+
813
+ **Example:**
814
+
815
+ ```ruby
816
+ star = Polygon.new(
817
+ points: [[100, 10], [120, 80], [190, 80], [130, 120], [150, 190],
818
+ [100, 150], [50, 190], [70, 120], [10, 80], [80, 80]],
819
+ color: 'yellow', stroke_width: 2, stroke_color: 'orange'
820
+ )
821
+ ```
822
+
823
+ ### Polyline
824
+
825
+ A stroke-only open or closed path of connected line segments. No fill.
826
+
827
+ ```ruby
828
+ Polyline.new(points: [[50, 50], [150, 100], [250, 80], [350, 150]])
829
+ ```
830
+
831
+ | Parameter | Default | Description |
832
+ |---|---|---|
833
+ | `points` | (required) | Array of `[x, y]` pairs (N ≥ 2) |
834
+ | `z` | `0` | Depth |
835
+ | `rotate` | `0` | Rotation in degrees |
836
+ | `rx`, `ry` | Centroid | Rotation center |
837
+ | `color` | `'white'` | Stroke color: a single color, or a per-vertex array of length `vertex_count` for a gradient along the path |
838
+ | `opacity` | `nil` | Alpha override: single value for all vertices, or an array of per-vertex values |
839
+ | `stroke_width` | `1` | Stroke thickness |
840
+ | `closed` | `false` | Connect the last point back to the first |
841
+ | `add` | `true` | Add to the window's scene graph on construction |
842
+ | `visible` | `true` | Initial visibility (drawn each frame while in the scene graph) |
843
+
844
+ **Example:**
845
+
846
+ ```ruby
847
+ path = Polyline.new(
848
+ points: [[50, 200], [100, 150], [150, 200], [200, 120], [250, 200]],
849
+ stroke_width: 3, color: 'aqua'
850
+ )
851
+
852
+ # Closed polyline (just the outline; for a filled version, use Polygon)
853
+ outline = Polyline.new(
854
+ points: [[100, 100], [200, 100], [200, 200], [100, 200]],
855
+ stroke_width: 2, color: 'white', closed: true
856
+ )
857
+ ```
858
+
859
+ ## Rectangles and Circles
860
+
861
+ Anchor-defined shapes. `Rectangle` and `Square` use their **top-left corner** as the anchor; `Circle` and `Ellipse` use their **center**. Both groups support symbolic alignment (`x: :center`, etc.); see [Aligning to the window](#aligning-to-the-window). For fill / stroke, rotation, and other common properties, see [Shape Basics](#shape-basics).
862
+
863
+ ### Rectangle
864
+
865
+ A rectangle (subclass of Quad).
866
+
867
+ ```ruby
868
+ Rectangle.new(x: 0, y: 0, width: 200, height: 100)
869
+ ```
870
+
871
+ | Parameter | Default | Description |
872
+ |---|---|---|
873
+ | `x` | `0` | Top-left x (or `:left`/`:center`/`:right`; see [Aligning to the window](#aligning-to-the-window)) |
874
+ | `y` | `0` | Top-left y (or `:top`/`:center`/`:bottom`) |
875
+ | `width` | `200` | Width |
876
+ | `height` | `100` | Height |
877
+ | `z` | `0` | Depth |
878
+ | `rotate` | `0` | Rotation in degrees |
879
+ | `rx`, `ry` | Center | Rotation center |
880
+ | `color` | `'white'` | Color (single or 4-element array for per-vertex) |
881
+ | `opacity` | `nil` | Alpha override |
882
+ | `fill` | `true` | Whether to fill (see [Fill and Stroke](#fill-and-stroke)) |
883
+ | `stroke_width` | `0` | Outline thickness |
884
+ | `stroke_color` | Fill color | Outline color |
885
+ | `add` | `true` | Add to the window's scene graph on construction |
886
+ | `visible` | `true` | Initial visibility (drawn each frame while in the scene graph) |
887
+ | `padding` | `0` | Inset from anchored edges (also `padding_top`/`padding_right`/`padding_bottom`/`padding_left`) |
888
+
889
+ **Example:**
890
+
891
+ ```ruby
892
+ rect = Rectangle.new(x: 10, y: 10, width: 100, height: 50)
893
+ rect.width = 200
894
+ rect.height = 100
895
+ ```
896
+
897
+ ### Square
898
+
899
+ A square (subclass of Rectangle).
900
+
901
+ ```ruby
902
+ Square.new(x: 0, y: 0, size: 100)
903
+ ```
904
+
905
+ | Parameter | Default | Description |
906
+ |---|---|---|
907
+ | `x` | `0` | Top-left x (or `:left`/`:center`/`:right`; see [Aligning to the window](#aligning-to-the-window)) |
908
+ | `y` | `0` | Top-left y (or `:top`/`:center`/`:bottom`) |
909
+ | `size` | `100` | Side length |
910
+ | `z` | `0` | Depth |
911
+ | `rotate` | `0` | Rotation in degrees |
912
+ | `rx`, `ry` | Center | Rotation center |
913
+ | `color` | `'white'` | Color (single or 4-element array for per-vertex) |
914
+ | `opacity` | `nil` | Alpha override |
915
+ | `fill` | `true` | Whether to fill (see [Fill and Stroke](#fill-and-stroke)) |
916
+ | `stroke_width` | `0` | Outline thickness |
917
+ | `stroke_color` | Fill color | Outline color |
918
+ | `add` | `true` | Add to the window's scene graph on construction |
919
+ | `visible` | `true` | Initial visibility (drawn each frame while in the scene graph) |
920
+ | `padding` | `0` | Inset from anchored edges (also `padding_top`/`padding_right`/`padding_bottom`/`padding_left`) |
921
+
922
+ **Example:**
923
+
924
+ ```ruby
925
+ sq = Square.new(x: 10, y: 10, size: 50)
926
+ sq.size = 75
927
+ ```
928
+
929
+ ### Circle
930
+
931
+ ```ruby
932
+ Circle.new(x: 0, y: 0, radius: 50)
933
+ ```
934
+
935
+ | Parameter | Default | Description |
936
+ |---|---|---|
937
+ | `x` | `0` | Center x (or `:left`/`:center`/`:right`; see [Aligning to the window](#aligning-to-the-window)) |
938
+ | `y` | `0` | Center y (or `:top`/`:center`/`:bottom`) |
939
+ | `z` | `0` | Depth |
940
+ | `radius` | `50` | Radius |
941
+ | `sectors` | `30` | Number of segments around the perimeter (higher = smoother) |
942
+ | `rotate` | `0` | Rotation in degrees |
943
+ | `rx`, `ry` | Center | Rotation center |
944
+ | `color` | `'white'` | Single color only |
945
+ | `opacity` | `nil` | Alpha override |
946
+ | `fill` | `true` | Whether to fill (see [Fill and Stroke](#fill-and-stroke)) |
947
+ | `stroke_width` | `0` | Outline thickness |
948
+ | `stroke_color` | Fill color | Outline color |
949
+ | `add` | `true` | Add to the window's scene graph on construction |
950
+ | `visible` | `true` | Initial visibility (drawn each frame while in the scene graph) |
951
+ | `padding` | `0` | Inset from anchored edges (also `padding_top`/`padding_right`/`padding_bottom`/`padding_left`) |
952
+
953
+ **Example:**
954
+
955
+ ```ruby
956
+ circle = Circle.new(x: 200, y: 200, radius: 40, color: 'blue', sectors: 60)
957
+ circle.radius = 60
958
+ circle.color = 'purple'
959
+ ```
960
+
961
+ ### Ellipse
962
+
963
+ ```ruby
964
+ Ellipse.new(x: 100, y: 100, xradius: 60, yradius: 30)
965
+ ```
966
+
967
+ | Parameter | Default | Description |
968
+ |---|---|---|
969
+ | `x` | `0` | Center x (or `:left`/`:center`/`:right`; see [Aligning to the window](#aligning-to-the-window)) |
970
+ | `y` | `0` | Center y (or `:top`/`:center`/`:bottom`) |
971
+ | `z` | `0` | Depth |
972
+ | `xradius` | `50` | Horizontal radius |
973
+ | `yradius` | `30` | Vertical radius |
974
+ | `sectors` | `30` | Number of segments around the perimeter (higher = smoother) |
975
+ | `rotate` | `0` | Rotation in degrees |
976
+ | `rx`, `ry` | Center | Rotation center |
977
+ | `color` | `'white'` | Single color only |
978
+ | `opacity` | `nil` | Alpha override |
979
+ | `fill` | `true` | Whether to fill (see [Fill and Stroke](#fill-and-stroke)) |
980
+ | `stroke_width` | `0` | Outline thickness |
981
+ | `stroke_color` | Fill color | Outline color |
982
+ | `add` | `true` | Add to the window's scene graph on construction |
983
+ | `visible` | `true` | Initial visibility (drawn each frame while in the scene graph) |
984
+ | `padding` | `0` | Inset from anchored edges (also `padding_top`/`padding_right`/`padding_bottom`/`padding_left`) |
985
+
986
+ **Example:**
987
+
988
+ ```ruby
989
+ ell = Ellipse.new(x: 200, y: 200, xradius: 80, yradius: 40, color: 'teal',
990
+ stroke_width: 2, stroke_color: 'white')
991
+ ```
992
+
993
+ ## Images
994
+
995
+ ```ruby
996
+ img = Image.new('path/to/image.png')
997
+ ```
998
+
999
+ | Parameter | Default | Description |
1000
+ |---|---|---|
1001
+ | `path` | (required) | Path to the image file (positional argument) |
1002
+ | `x` | `0` | X position (or `:left`/`:center`/`:right`; see [Aligning to the window](#aligning-to-the-window)) |
1003
+ | `y` | `0` | Y position (or `:top`/`:center`/`:bottom`) |
1004
+ | `z` | `0` | Depth |
1005
+ | `width` | Native width | Display width |
1006
+ | `height` | Native height | Display height |
1007
+ | `rotate` | `0` | Rotation in degrees |
1008
+ | `rx`, `ry` | Center | Rotation center |
1009
+ | `tint` | `'white'` | Tint color — modulates the image's texture colors |
1010
+ | `opacity` | `nil` | Alpha override |
1011
+ | `add` | `true` | Add to the window's scene graph on construction |
1012
+ | `visible` | `true` | Initial visibility (drawn each frame while in the scene graph) |
1013
+ | `padding` | `0` | Inset from anchored edges (also `padding_top`/`padding_right`/`padding_bottom`/`padding_left`) |
1014
+
1015
+ **Example:**
1016
+
1017
+ ```ruby
1018
+ img = Image.new('hero.png', x: 100, y: 100, width: 64, height: 64)
1019
+ img.x = 200
1020
+ img.rotate = 45
1021
+ img.tint = 'red' # tint red: the texture's colors are multiplied by red
1022
+ ```
1023
+
1024
+ > Images use `tint` rather than `color` because the texture already has its own colors; `tint` modulates (multiplies) them. A white tint leaves the image unchanged; `tint: 'red'` makes it redder, not solid red.
1025
+
1026
+ ### Rendering Images in Render Blocks
1027
+
1028
+ ```ruby
1029
+ img = Image.new('tile.png', add: false)
1030
+
1031
+ render do
1032
+ img.render(x: 50, y: 50, width: 32, height: 32)
1033
+ end
1034
+ ```
1035
+
1036
+ ### SVG Images
1037
+
1038
+ SVGs are rasterized once at load. If `width` and `height` are passed to `Image.new`, the SVG is rasterized at 2× that size so small upscales and rotations stay crisp; otherwise it rasterizes at the SVG's intrinsic size. Setting `width=`/`height=` later just scales the cached raster — for a fresh, sharp rasterization at a new size, use `resize!`:
1039
+
1040
+ ```ruby
1041
+ bee = Image.new('bee.svg', width: 64, height: 64)
1042
+ bee.resize!(256, 256) # re-rasterize crisp at the new size
1043
+
1044
+ bee.width = 400 # cheap scale of the existing raster
1045
+ bee.resize! # commit the current width/height to a fresh raster
1046
+ ```
1047
+
1048
+ `resize!` also works on raster images (PNG/JPG/BMP), where it resamples the source, useful for trimming GPU memory when displaying a large source small. It re-decodes from disk, so call it on size changes, not every frame.
1049
+
1050
+ > `resize!` is not supported on Sprites built from a `SpriteSheet`: every sprite cut from the sheet shares one backing texture, so re-rasterizing it would corrupt all of them. It raises `Ruby2D::Error`; set `width`/`height` to change only this sprite's display size, or use a standalone `Image` if you need a true resize.
1051
+
1052
+ ## Text
1053
+
1054
+ Renders text using a TrueType font.
1055
+
1056
+ ```ruby
1057
+ text = Text.new('Hello, Ruby 2D!')
1058
+ ```
1059
+
1060
+ | Parameter | Default | Description |
1061
+ |---|---|---|
1062
+ | `content` | (required) | The string to display (positional argument) |
1063
+ | `x` | `0` | X position (or `:left`/`:center`/`:right`; see [Aligning to the window](#aligning-to-the-window)) |
1064
+ | `y` | `0` | Y position (or `:top`/`:center`/`:bottom`) |
1065
+ | `z` | `0` | Depth |
1066
+ | `size` | `20` | Font size in points |
1067
+ | `style` | `nil` | Font style: `:bold`, `:italic`, `:underline`, `:strikethrough`, or an array combining them |
1068
+ | `font` | `Font.default` | Path to a `.ttf` font file |
1069
+ | `rotate` | `0` | Rotation in degrees |
1070
+ | `rx`, `ry` | Center | Rotation center |
1071
+ | `color` | `'white'` | Text color |
1072
+ | `opacity` | `nil` | Alpha override |
1073
+ | `add` | `true` | Add to the window's scene graph on construction |
1074
+ | `visible` | `true` | Initial visibility (drawn each frame while in the scene graph) |
1075
+ | `padding` | `0` | Inset from anchored edges (also `padding_top`/`padding_right`/`padding_bottom`/`padding_left`) |
1076
+
1077
+ **Example:**
1078
+
1079
+ ```ruby
1080
+ text = Text.new('Score: 0', x: 10, y: 10, size: 24, color: 'yellow',
1081
+ font: '/path/to/font.ttf')
1082
+ text.content = 'Score: 100'
1083
+ text.size = 32
1084
+ text.width # => calculated width of the rendered text
1085
+ text.height # => calculated height of the rendered text
1086
+ ```
1087
+
1088
+ Embedded newlines (`"line one\nline two"`) are laid out as separate lines; `width` is the widest line and `height` covers the whole block.
1089
+
1090
+ Font `style` combines one or more of `:bold`, `:italic`, `:underline`, and `:strikethrough` (pass an array for several). It can be set at construction or changed later, and `font`/`size`/`style` all re-render the text:
1091
+
1092
+ ```ruby
1093
+ title = Text.new('Game Over', size: 48, style: [:bold, :italic])
1094
+ title.style = :underline # change it later
1095
+ ```
1096
+
1097
+ ### Rendering Text in Render Blocks
1098
+
1099
+ ```ruby
1100
+ label = Text.new('FPS', add: false)
1101
+
1102
+ render do
1103
+ label.render(x: 10, y: 10, color: 'white')
1104
+ end
1105
+ ```
1106
+
1107
+ ### Fonts
1108
+
1109
+ The `Font` class provides utilities for discovering and loading system fonts.
1110
+
1111
+ ```ruby
1112
+ Font.all # => ['arial', 'courier', ...] list of available font names
1113
+ Font.path('arial') # => '/Library/Fonts/Arial.ttf' (case-insensitive)
1114
+ Font.default # => path to the default font
1115
+ ```
1116
+
1117
+ Fonts are cached internally. You do not instantiate `Font` objects directly; they are managed by `Text`.
1118
+
1119
+ ## Bitmap Text
1120
+
1121
+ Renders text using a built-in bitmap font, with no TTF dependency. The font covers printable ASCII only (space through `~`); any other character — accented letters, non-Latin scripts, emoji, tabs, newlines — renders as a `?` placeholder. For full Unicode or multi-line text, use [`Text`](#text).
1122
+
1123
+ ```ruby
1124
+ bt = BitmapText.new('Hello!')
1125
+ ```
1126
+
1127
+ | Parameter | Default | Description |
1128
+ |---|---|---|
1129
+ | `content` | (required) | The string to display (positional argument) |
1130
+ | `x` | `0` | X position |
1131
+ | `y` | `0` | Y position |
1132
+ | `z` | `0` | Depth |
1133
+ | `scale` | `3` | Size multiplier (must be a positive number; a float truncates to an integer) |
1134
+ | `rotate` | `0` | Rotation in degrees |
1135
+ | `rx` | `nil` (center) | Rotation center x; defaults to the text's center |
1136
+ | `ry` | `nil` (center) | Rotation center y; defaults to the text's center |
1137
+ | `color` | `'white'` | Text color |
1138
+ | `opacity` | `nil` | Alpha override |
1139
+ | `add` | `true` | Add to the window's scene graph on construction |
1140
+ | `visible` | `true` | Initial visibility (drawn each frame while in the scene graph) |
1141
+
1142
+ **Example:**
1143
+
1144
+ ```ruby
1145
+ bt = BitmapText.new('Loading...', x: 10, y: 10, scale: 5, color: 'green')
1146
+ bt.content = 'Ready!'
1147
+ bt.scale = 4
1148
+ bt.rotate = 15 # rotates about the text's center; override with rx/ry
1149
+ ```
1150
+
1151
+ ### Rendering Bitmap Text in Render Blocks
1152
+
1153
+ ```ruby
1154
+ label = BitmapText.new('FPS', add: false)
1155
+
1156
+ render do
1157
+ label.render(x: 10, y: 10, scale: 2, color: 'white')
1158
+ end
1159
+ ```
1160
+
1161
+ ## Sprites
1162
+
1163
+ A sprite is an animated image from a sprite sheet (subclass of `Image`).
1164
+
1165
+ ### Choosing between Sprite, SpriteSheet, and Tileset
1166
+
1167
+ Quick rule of thumb: animated thing → `Sprite`; many sprites sharing a packed atlas → `SpriteSheet` + `Sprite(frame:)`; many static tile placements from a grid → `Tileset`.
1168
+
1169
+ - **`Sprite`** — one animated thing on screen. Carries per-instance state (current frame, current animation, elapsed time, speed, paused). Good for characters, projectiles, and effects.
1170
+ - **`SpriteSheet`** — a *resource*, not a drawable. It's a wrapper around a packed atlas (regular grid or arbitrary layout) that loads the source image once and provides named-frame lookup. You don't see a `SpriteSheet` in the scene graph; you build `Sprite`s from it via `Sprite.new(sheet, frame: '<name>')` or `Sprite.new(sheet, animations: { jump: %w[...] })`, and every sprite built from the same sheet shares one GPU texture.
1171
+ - **`Tileset`** — one source image with named cells drawn at many static positions in a single batched render. No per-cell animation or state. Good for tile-based maps, walls, and HUD elements built from a UI atlas.
1172
+
1173
+ The choice between `SpriteSheet` + many `Sprite`s and a single `Tileset` comes down to scene-graph cost: 100 sprites from one `SpriteSheet` produce 100 scene-graph entries sharing one texture (each independently animatable, each free to move); a `Tileset` with 100 placements produces 1 scene-graph entry, batched in one render call, with no per-cell animation.
1174
+
1175
+ "Sprite sheet" and "texture atlas" are used interchangeably in practice; Ruby 2D's class accepts the `TextureAtlas` name as an alias and supports both regular-grid (Sparrow XML) and packed-irregular (TexturePacker JSON) layouts. Lowercase "sprite sheet" elsewhere in this document refers to the general concept of multiple frames packed into one image; `Sprite` handles the simplest horizontal-strip case directly without needing a `SpriteSheet` wrapper.
1176
+
1177
+ ```ruby
1178
+ sprite = Sprite.new('characters.png',
1179
+ clip_width: 32, clip_height: 32,
1180
+ animations: {
1181
+ walk: 0..3,
1182
+ jump: [
1183
+ { x: 0, y: 32, width: 32, height: 32, time: 100 },
1184
+ { x: 32, y: 32, width: 32, height: 32, time: 200 }
1185
+ ]
1186
+ }
1187
+ )
1188
+ ```
1189
+
1190
+ | Parameter | Default | Description |
1191
+ |---|---|---|
1192
+ | `source` | (required) | Image path **or** a `SpriteSheet` (positional argument); see [Sprite Sheets](#sprite-sheets) |
1193
+ | `x` | `0` | X position (or `:left`/`:center`/`:right`; see [Aligning to the window](#aligning-to-the-window)) |
1194
+ | `y` | `0` | Y position (or `:top`/`:center`/`:bottom`) |
1195
+ | `z` | `0` | Depth |
1196
+ | `width` | Clip width | Display width |
1197
+ | `height` | Clip height | Display height |
1198
+ | `rotate` | `0` | Rotation in degrees |
1199
+ | `rx`, `ry` | Center | Rotation center |
1200
+ | `tint` | `'white'` | Tint color — modulates the sprite's texture colors |
1201
+ | `opacity` | `nil` | Alpha override |
1202
+ | `add` | `true` | Add to the window's scene graph on construction |
1203
+ | `visible` | `true` | Initial visibility (drawn each frame while in the scene graph) |
1204
+ | `padding` | `0` | Inset from anchored edges (also `padding_top`/`padding_right`/`padding_bottom`/`padding_left`) |
1205
+ | `frame` | `nil` | When `source` is a `SpriteSheet`, the named frame to display statically |
1206
+ | `clip_x` | `0` | X offset into the sprite sheet |
1207
+ | `clip_y` | `0` | Y offset into the sprite sheet |
1208
+ | `clip_width` | Image width | Width of each frame |
1209
+ | `clip_height` | Image height | Height of each frame |
1210
+ | `loop` | `false` | Whether the default animation loops |
1211
+ | `time` | `300` | Duration of each frame in milliseconds |
1212
+ | `speed` | `1.0` | Animation rate multiplier (also `sprite.speed=`) |
1213
+ | `animations` | `{}` | Hash of named animations |
1214
+ | `default` | `0` | Default frame index |
1215
+
1216
+ ### Defining Animations
1217
+
1218
+ Animations are defined as a hash where keys are names and values are either:
1219
+
1220
+ - **Range**: Frame indices across a horizontal strip. `walk: 0..3` plays frames 0, 1, 2, 3 using `clip_width` to determine each frame's position.
1221
+ - **Array**: Explicit frame regions, with optional per-frame timing. Each entry is one of:
1222
+ - A hash with `x`, `y`, `width`, `height`, and optional `time`.
1223
+ - A frame name (string) — only when the sprite was constructed from a `SpriteSheet`.
1224
+ - A hash with `name:` and optional `time:` — same as above with custom timing.
1225
+
1226
+ ```ruby
1227
+ animations: {
1228
+ idle: 0..0,
1229
+ walk: 0..7,
1230
+ attack: [
1231
+ { x: 0, y: 64, width: 48, height: 48, time: 100 },
1232
+ { x: 48, y: 64, width: 48, height: 48, time: 150 }
1233
+ ]
1234
+ }
1235
+ ```
1236
+
1237
+ For a horizontal-strip image, a `:default` animation is automatically created spanning all frames. Atlas-backed sprites (built from a `SpriteSheet`) skip this auto-default; define your own `:default` if you need one.
1238
+
1239
+ ### Playing Animations
1240
+
1241
+ ```ruby
1242
+ sprite.play(animation: :walk, loop: true)
1243
+ sprite.play(animation: :walk, flip: :horizontal)
1244
+
1245
+ # Non-looping animation: plays once, then holds on its last frame.
1246
+ # Use this for jump poses, attack follow-throughs, death poses, etc.
1247
+ sprite.play(animation: :jump)
1248
+
1249
+ # With a completion callback:
1250
+ sprite.play(animation: :attack) do
1251
+ sprite.play(animation: :idle, loop: true)
1252
+ end
1253
+
1254
+ sprite.stop # stop and revert to the default animation's frame
1255
+ sprite.stop(:walk) # stop only if :walk is currently playing
1256
+ ```
1257
+
1258
+ A non-looping animation that finishes **holds on its last frame** until you call `stop` or `play` something else; the sprite doesn't snap back to the default frame on its own. The completion block (if provided) fires once at that moment.
1259
+
1260
+ Re-calling `play` with the animation that's *already* playing doesn't restart it (no jump back to frame 0), so calling it every frame from your update loop is safe. An explicitly-passed `loop:` or completion block still takes effect, letting you adjust those mid-play. To change only the loop setting, `sprite.loop = false` does the same without touching the callback.
1261
+
1262
+ | `play` Parameter | Default | Description |
1263
+ |---|---|---|
1264
+ | `animation` | `:default` | Animation name |
1265
+ | `loop` | From defaults | Whether to loop |
1266
+ | `flip` | `nil` | `:horizontal`, `:vertical`, or `:both` |
1267
+ | Block | `nil` | Called once when a non-looping animation finishes |
1268
+
1269
+ ```ruby
1270
+ sprite.pause # freeze on the current frame
1271
+ sprite.resume # continue from where pause left off
1272
+ sprite.paused? # => true / false
1273
+ sprite.playing? # true while actively animating; false when idle, paused, or held
1274
+ sprite.looping? # true if the current animation loops
1275
+ sprite.loop = false # toggle looping mid-play (doesn't restart the animation)
1276
+ ```
1277
+
1278
+ `pause` is idempotent and a no-op when nothing is playing. `play` and `stop` both clear the paused state.
1279
+
1280
+ ```ruby
1281
+ sprite.speed = 2.0 # animate twice as fast
1282
+ sprite.speed = 0.5 # half speed
1283
+ sprite.speed = 0 # frozen (still 'playing', just not advancing)
1284
+ ```
1285
+
1286
+ `speed` is a multiplier on top of `time:` — the per-frame duration is effectively `time / speed`. Negative values clamp to `0`; reverse playback is not supported.
1287
+
1288
+ Animations advance on real elapsed time — the same frame delta described under [`dt`](#frame-rate-independence-with-dt) — not on a fixed step per frame. So playback runs at the same wall-clock rate on any display refresh rate, a high `speed` genuinely skips frames (it isn't capped at the refresh rate), and a momentary stall is caught up on the next frame rather than dropped. Each frame is shown for its own `time:`, so per-frame durations stay accurate even while skipping.
1289
+
1290
+ ```ruby
1291
+ sprite.frame # => current static frame name, or nil
1292
+ sprite.frame = 'walk_a' # swap to that named frame on an atlas-backed sprite
1293
+ ```
1294
+
1295
+ Assigning to `frame=` works on sprites built from a `SpriteSheet`. It updates the clip rect to the named frame, stops any playing animation, and (when no explicit `width`/`height` was given at construction) resizes the sprite to match the new frame.
1296
+
1297
+ ### Rendering Sprites in Render Blocks
1298
+
1299
+ ```ruby
1300
+ sprite = Sprite.new('sheet.png', clip_width: 32, clip_height: 32, add: false)
1301
+
1302
+ render do
1303
+ sprite.render(x: 10, y: 10, clip_x: 64, clip_y: 0, clip_width: 32, clip_height: 32)
1304
+ end
1305
+ ```
1306
+
1307
+ ### Sprite Sheets
1308
+
1309
+ A `SpriteSheet` (alias `TextureAtlas`) loads a packed atlas where named regions of a single image hold many frames. Two formats are supported:
1310
+
1311
+ - **Sparrow XML** (TexturePacker "Generic XML", Kenney.nl asset packs) — `<TextureAtlas imagePath="…"><SubTexture name="…" x y width height/></TextureAtlas>`.
1312
+ - **TexturePacker JSON** — both the Hash form (frames keyed by name) and the Array form (frames with `filename` fields).
1313
+
1314
+ The format is detected from the file extension (`.xml` / `.json`); the texture image referenced inside the atlas is loaded relative to the atlas file's directory. All `Sprite`s built from the same `SpriteSheet` share one GPU texture, so loading a single atlas once and constructing many sprites against it is cheap.
1315
+
1316
+ ```ruby
1317
+ sheet = SpriteSheet.new('characters.xml')
1318
+
1319
+ sheet.frame_names # => ['character_beige_idle', ...]
1320
+ sheet['character_beige_idle'] # => { x: 1285, y: 0, width: 256, height: 256 }
1321
+ sheet.frame?('character_beige_idle')
1322
+
1323
+ # Static frame: display one named region
1324
+ hero = Sprite.new(sheet, frame: 'character_beige_idle', x: 100, y: 100)
1325
+
1326
+ # Animations defined by frame names
1327
+ walker = Sprite.new(sheet,
1328
+ animations: {
1329
+ idle: ['character_beige_idle'],
1330
+ walk: %w[character_beige_walk_a character_beige_walk_b]
1331
+ }
1332
+ )
1333
+ walker.play(animation: :walk, loop: true)
1334
+
1335
+ # Mix per-frame timing with frame names
1336
+ runner = Sprite.new(sheet, animations: {
1337
+ run: [
1338
+ { name: 'character_beige_walk_a', time: 80 },
1339
+ { name: 'character_beige_walk_b', time: 120 }
1340
+ ]
1341
+ })
1342
+ ```
1343
+
1344
+ | `SpriteSheet.new` Parameter | Default | Description |
1345
+ |---|---|---|
1346
+ | `path` | (required) | Path to the atlas file (`.xml` or `.json`) |
1347
+
1348
+ | Method | Description |
1349
+ |---|---|
1350
+ | `path` | The atlas file path |
1351
+ | `image_path` | Resolved path to the atlas's texture image |
1352
+ | `texture` | The shared backing `Image` |
1353
+ | `frame_names` | All frame names, in declaration order |
1354
+ | `frame(name)` / `[name]` | Look up a frame's `{x:, y:, width:, height:}`, or `nil` if absent |
1355
+ | `frame?(name)` | Whether the sheet contains the named frame |
1356
+
1357
+ TexturePacker can pack frames rotated 90° (the `"rotated": true` flag). Ruby 2D doesn't draw rotated atlas frames yet; building a `Sprite` against one raises `Ruby2D::Error`. For now, repack the atlas with rotation disabled.
1358
+
1359
+ **Trimmed frames** are supported. When TexturePacker (or Aseprite, etc.) crops transparent edges from each frame, the atlas stores the original frame size (`sourceSize`/`frameWidth`,`frameHeight`) and the offset of the trimmed pixels within it (`spriteSourceSize.x`,`y` / `frameX`,`frameY`). Ruby 2D draws those frames at their original logical size: `sprite.width` and `sprite.height` reflect the un-trimmed footprint, and the packed pixels render at the correct offset. No setup is needed; if the atlas carries trim metadata, it just works.
1360
+
1361
+ ## Tilesets
1362
+
1363
+ For drawing tile-based maps from a single tileset image.
1364
+
1365
+ ```ruby
1366
+ ts = Tileset.new('tiles.png', tile_width: 16, tile_height: 16, scale: 2)
1367
+
1368
+ # Define tiles by their grid position in the image
1369
+ ts.define(:grass, 0, 0)
1370
+ ts.define(:water, 1, 0)
1371
+ ts.define(:wall, 0, 1, rotate: 90)
1372
+ ts.define(:tree, 2, 0, flip: :horizontal)
1373
+
1374
+ # Place tiles at screen coordinates
1375
+ ts.place(:grass, [[0, 0], [32, 0], [64, 0]])
1376
+ ts[0, 32] = :water
1377
+ ```
1378
+
1379
+ | Parameter | Default | Description |
1380
+ |---|---|---|
1381
+ | `path` | (required) | Path to the tileset image (positional argument) |
1382
+ | `tile_width` | `32` | Width of each tile in the source image |
1383
+ | `tile_height` | `32` | Height of each tile in the source image |
1384
+ | `z` | `0` | Depth |
1385
+ | `padding` | `0` | Padding around the tileset edges |
1386
+ | `spacing` | `0` | Spacing between tiles |
1387
+ | `scale` | `1` | Scale multiplier for tile rendering |
1388
+ | `add` | `true` | Add to the window's scene graph on construction |
1389
+ | `visible` | `true` | Initial visibility (drawn each frame while in the scene graph) |
1390
+
1391
+ ### Tileset Methods
1392
+
1393
+ ```ruby
1394
+ ts.define(name, x, y, rotate: 0, flip: nil) # define a tile type
1395
+ ts.place(name, coordinates) # stamp tiles at [[x, y], ...]
1396
+ ts[x, y] = name # stamp a single tile (replaces any existing)
1397
+ ts[x, y] # name placed at (x, y), or nil
1398
+ ts.delete(x, y) # remove the tile at (x, y)
1399
+ ts.clear # remove all placed tiles
1400
+ ```
1401
+
1402
+ Placing a tile at a coordinate that's already occupied replaces the existing one. The `flip:` option for `define` accepts `:horizontal`, `:vertical`, or `:both`.
1403
+
1404
+ Setting `tileset.tint` multiplies all placed tiles against the texture (white = untinted, the default).
1405
+
1406
+ ## Canvas
1407
+
1408
+ A pixel-level drawing surface for procedural graphics.
1409
+
1410
+ ```ruby
1411
+ canvas = Canvas.new(width: 200, height: 200)
1412
+ ```
1413
+
1414
+ | Parameter | Default | Description |
1415
+ |---|---|---|
1416
+ | `width` | (required) | Canvas width |
1417
+ | `height` | (required) | Canvas height |
1418
+ | `x` | `0` | X position |
1419
+ | `y` | `0` | Y position |
1420
+ | `z` | `0` | Depth |
1421
+ | `rotate` | `0` | Rotation in degrees |
1422
+ | `fill` | Transparent | Background fill color (RGBA array) |
1423
+ | `tint` | `'white'` | Multiplied against the canvas texture when drawn (white = untinted); also used as the implicit color for `fill_*` / `stroke_*` / `draw_*` methods when none is passed |
1424
+ | `opacity` | `nil` | Alpha override on the tint |
1425
+ | `add` | `true` | Add to the window's scene graph on construction |
1426
+ | `visible` | `true` | Initial visibility (drawn each frame while in the scene graph) |
1427
+
1428
+ A Canvas captures the display scale once, at construction, and bakes it into its pixel buffer; it never re-evaluates (unlike `Text`, which re-rasterizes on scale changes), because doing so would discard your accumulated drawing. With the default settings this is handled for you. With `pixel_scale: true`, construct the Canvas *after* `show` starts so it picks up the physical-pixel size; see the `update`-loop pattern under [Pixel Scale](#pixel-scale).
1429
+
1430
+ ### Filled Shapes
1431
+
1432
+ ```ruby
1433
+ canvas.fill_rectangle(x: 10, y: 10, width: 50, height: 30, color: 'red')
1434
+ canvas.fill_square(x: 10, y: 10, size: 50, color: 'blue')
1435
+ canvas.fill_triangle(x1: 0, y1: 0, x2: 50, y2: 0, x3: 25, y3: 50, color: 'green')
1436
+ canvas.fill_quad(x1: 0, y1: 0, x2: 50, y2: 0, x3: 50, y3: 50, x4: 0, y4: 50)
1437
+ canvas.fill_circle(x: 100, y: 100, radius: 25, color: 'yellow')
1438
+ canvas.fill_ellipse(x: 100, y: 100, xradius: 40, yradius: 20)
1439
+ canvas.fill_polygon(points: [[x1, y1], [x2, y2], [x3, y3], ...], color: 'purple')
1440
+ canvas.fill_rectangles(rectangles: [[x1, y1, w1, h1], [x2, y2, w2, h2], ...], color: 'blue')
1441
+ ```
1442
+
1443
+ `fill_rectangles` fills many rectangles with a single shared color in one call, useful for hot-path workloads that would otherwise issue hundreds of `fill_rectangle` calls per frame.
1444
+
1445
+ For grids where every cell has its *own* color (heatmaps, fluid sims, tile fields), use `fill_pixel_grid`. The cell geometry is implicit — you describe the grid once and pass a flat color buffer:
1446
+
1447
+ ```ruby
1448
+ # 100 x 75 grid of 8x8 cells; colors is a flat array of 100*75*4 floats (rgba per cell, 0..1)
1449
+ canvas.fill_pixel_grid(cols: 100, rows: 75, cell_w: 8, cell_h: 8, x: 0, y: 0, colors: colors)
1450
+ ```
1451
+
1452
+ Cell `(c, r)` covers `(x + c*cell_w, y + r*cell_h, cell_w, cell_h)` and reads its color from `colors[(r*cols + c)*4 .. +3]`. Cells with alpha 0 are skipped. One FFI crossing replaces an inner loop of `fill_rectangle` calls.
1453
+
1454
+ The vertex-based fills — `fill_triangle` (3), `fill_quad` / `fill_rectangle` / `fill_square` (4), and `fill_polygon` (N) — accept per-vertex colors by passing an array of colors (one per vertex), interpolated across the fill:
1455
+
1456
+ ```ruby
1457
+ canvas.fill_triangle(x1: 0, y1: 0, x2: 100, y2: 0, x3: 50, y3: 100,
1458
+ color: ['red', 'green', 'blue'])
1459
+ ```
1460
+
1461
+ The same methods accept a per-vertex `opacity:` array (one alpha per vertex) for a fade across the fill (with a single color or a per-vertex color array); see [Canvas Opacity Overrides](#canvas-opacity-overrides). `fill_circle` / `fill_ellipse` are rasterized, not vertex-based, so they take only a single color and a scalar `opacity:`.
1462
+
1463
+ ### Stroked (Outlined) Shapes
1464
+
1465
+ ```ruby
1466
+ canvas.stroke_triangle(x1: 0, y1: 0, x2: 50, y2: 0, x3: 25, y3: 50, stroke_width: 1)
1467
+ canvas.stroke_quad(x1: 0, y1: 0, x2: 50, y2: 0, x3: 50, y3: 50, x4: 0, y4: 50)
1468
+ canvas.stroke_rectangle(x: 10, y: 10, width: 50, height: 30, stroke_width: 2)
1469
+ canvas.stroke_square(x: 10, y: 10, size: 50)
1470
+ canvas.stroke_circle(x: 100, y: 100, radius: 25, sectors: 30)
1471
+ canvas.stroke_ellipse(x: 100, y: 100, xradius: 40, yradius: 20, sectors: 30)
1472
+ ```
1473
+
1474
+ ### Lines and Polylines
1475
+
1476
+ ```ruby
1477
+ canvas.draw_line(x1: 0, y1: 0, x2: 100, y2: 100, stroke_width: 2, color: 'white')
1478
+ canvas.draw_line(x1: 0, y1: 0, x2: 100, y2: 100, stroke_width: 2, dash: 10, gap: 5, color: 'white')
1479
+ canvas.draw_polyline(points: [[x1, y1], [x2, y2], ...], stroke_width: 1, closed: false)
1480
+ canvas.draw_lines(segments: [[[x1a, y1a], [x2a, y2a]], [[x1b, y1b], [x2b, y2b]], ...], stroke_width: 1, color: 'white')
1481
+ ```
1482
+
1483
+ `draw_line` accepts a 2-element `color:` array `[start, end]` for a gradient along the length, matching `Line` semantics. Dashed lines interpolate the endpoint colors per segment so the gradient carries smoothly across dashes. Set `closed: true` on `draw_polyline` to connect the last point back to the first.
1484
+
1485
+ `draw_polyline`, `stroke_triangle`, `stroke_quad`, `stroke_rectangle`, and `stroke_square` all accept a per-vertex `color:` array (length matching the vertex count) for a gradient around the path / perimeter. `stroke_circle` and `stroke_ellipse` are single-color only. `draw_polyline` also accepts a per-vertex `opacity:` array; see [Opacity](#opacity).
1486
+
1487
+ `draw_lines` draws many disconnected line segments with a single shared color and stroke width in one call, useful for hot-path workloads that would otherwise issue hundreds of `draw_line` calls per frame. Unlike `draw_polyline`, segments are independent pairs of endpoints, not a connected path.
1488
+
1489
+ ### Drawing Images and Text onto a Canvas
1490
+
1491
+ ```ruby
1492
+ img = Image.new('texture.png', add: false)
1493
+ canvas.draw_image(img, x: 10, y: 10, width: 64, height: 64)
1494
+
1495
+ label = Text.new('Canvas text', add: false)
1496
+ canvas.draw_text(label, x: 10, y: 80, color: 'yellow')
1497
+ ```
1498
+
1499
+ ### Canvas Opacity Overrides
1500
+
1501
+ Every Canvas draw method that accepts `color:` also accepts `opacity:` as an optional alpha override. When provided, it replaces the color's alpha channel; when omitted, the color's own alpha is used unchanged. The caller's `Color` object is not mutated.
1502
+
1503
+ ```ruby
1504
+ canvas.draw_line(x1: 0, y1: 0, x2: 100, y2: 100, color: 'red', opacity: 0.35)
1505
+ canvas.fill_rectangle(x: 0, y: 0, width: 50, height: 50, color: 'blue', opacity: 0.5)
1506
+ ```
1507
+
1508
+ For methods that accept a per-vertex color array, a scalar `opacity:` overrides alpha on every color — "fade the whole fill to this alpha":
1509
+
1510
+ ```ruby
1511
+ canvas.fill_rectangle(
1512
+ x: 0, y: 0, width: 100, height: 100,
1513
+ color: ['red', 'green', 'blue', 'yellow'],
1514
+ opacity: 0.35
1515
+ )
1516
+ ```
1517
+
1518
+ The vertex-based fills (`fill_triangle`, `fill_quad`, `fill_rectangle`, `fill_square`, `fill_polygon`) and `draw_polyline` also accept a per-vertex `opacity:` **array** — one alpha per vertex, interpolated across the fill — with either a single color or a per-vertex color array. The array length must match the vertex count, or a clear error is raised:
1519
+
1520
+ ```ruby
1521
+ canvas.fill_triangle(
1522
+ points: [[0, 0], [100, 0], [50, 100]],
1523
+ color: 'red', opacity: [1.0, 0.5, 0.0] # opaque tip fading to transparent
1524
+ )
1525
+ ```
1526
+
1527
+ ### Clearing the Canvas
1528
+
1529
+ ```ruby
1530
+ canvas.clear # reset to the fill color
1531
+ canvas.clear('red') # clear to a specific color
1532
+ canvas.clear(nil, x: 10, y: 10, width: 50, height: 50) # clear a region
1533
+ ```
1534
+
1535
+ Drawing methods take effect on their own; there is no commit step, and only the region you actually touched is re-uploaded to the GPU. In `:on_demand` render mode, canvas drawing calls (including `clear`) auto-request a render for you; see [Render Mode](#render-mode).
1536
+
1537
+ ### One-Shot Rendering
1538
+
1539
+ Like other textured renderables, `canvas.render(...)` accepts per-frame overrides for use inside a `render` block. Build the canvas with `add: false` and draw it yourself each frame:
1540
+
1541
+ ```ruby
1542
+ canvas = Canvas.new(width: 200, height: 200, add: false)
1543
+ canvas.fill_circle(x: 100, y: 100, radius: 80, color: 'red')
1544
+
1545
+ render do
1546
+ canvas.render(x: Window.mouse_x, y: Window.mouse_y, rotate: Window.frames, opacity: 0.5)
1547
+ end
1548
+ ```
1549
+
1550
+ Overrides: `x`, `y`, `width`, `height`, `rotate`, `tint`, `opacity`. `width:` and `height:` scale the displayed output; the underlying pixel buffer is fixed at construction.
1551
+
1552
+ ## Audio
1553
+
1554
+ A unified audio class for playing sound effects and music.
1555
+
1556
+ ```ruby
1557
+ sound = Audio.new('bang.wav')
1558
+ music = Audio.new('theme.ogg', loop: true)
1559
+ ```
1560
+
1561
+ | Parameter | Default | Description |
1562
+ |---|---|---|
1563
+ | `path` | (required) | Path to an audio file (positional argument) |
1564
+ | `loop` | `false` | Whether to loop playback |
1565
+
1566
+ **Instance methods:**
1567
+
1568
+ ```ruby
1569
+ audio.play
1570
+ audio.pause
1571
+ audio.resume
1572
+ audio.stop # stop immediately
1573
+ audio.stop(500) # fade out over 500 ms
1574
+ audio.length # duration in seconds
1575
+ audio.volume # current volume (0.0–1.0)
1576
+ audio.volume = 0.8
1577
+ audio.loop = true
1578
+ audio.looping? # => true
1579
+ ```
1580
+
1581
+ **Overlapping playback:** each `Audio` plays on a single voice, so calling `play` again while it's still sounding restarts that one voice rather than layering a second copy. To overlap the same sound (rapid gunshots, coin pickups), give each simultaneous voice its own `Audio` and cycle through them:
1582
+
1583
+ ```ruby
1584
+ bangs = Array.new(4) { Audio.new('bang.wav') }
1585
+ i = 0
1586
+ on :key_down do
1587
+ bangs[i].play # round-robin across voices so repeats overlap
1588
+ i = (i + 1) % bangs.size
1589
+ end
1590
+ ```
1591
+
1592
+ **Global mixer volume:**
1593
+
1594
+ ```ruby
1595
+ Audio.volume # get the mixer volume (0.0–1.0)
1596
+ Audio.volume = 0.5 # set the mixer volume
1597
+ ```
1598
+
1599
+ ## Button
1600
+
1601
+ A clickable UI region with optional label and hover effect. The trailing block is sugar for an `:click` handler; for any other event use `btn.on(...)` (see [Per-Object Events](#per-object-events)); `:click`, `:mouse_down`, `:mouse_up`, `:mouse_held`, `:hover`, `:hover_out`, `:drag`, and `:mouse_scroll` all fire on a Button.
1602
+
1603
+ ```ruby
1604
+ btn = Button.new(x: 100, y: 200, width: 150, height: 40,
1605
+ label: 'Click Me', color: '#333') do |event|
1606
+ puts 'Button clicked!'
1607
+ end
1608
+
1609
+ btn.on(:mouse_down) { btn.color = '#555' }
1610
+ btn.on(:mouse_up) { btn.color = '#333' }
1611
+ ```
1612
+
1613
+ | Parameter | Default | Description |
1614
+ |---|---|---|
1615
+ | `visual` | `nil` | An existing shape to use as the button (positional, optional) |
1616
+ | `x` | `0` | X position (or `:left`/`:center`/`:right`; see [Aligning to the window](#aligning-to-the-window); owned visuals only) |
1617
+ | `y` | `0` | Y position (or `:top`/`:center`/`:bottom`) |
1618
+ | `z` | `0` | Depth |
1619
+ | `width` | `200` | Width |
1620
+ | `height` | `50` | Height |
1621
+ | `label` | `nil` | Button label text |
1622
+ | `color` | `'#333'` | Background color |
1623
+ | `hover_color` | `nil` (no auto-tint) | Hover-tint color. Pass `:auto` to lighten the base, or any color to use it explicitly |
1624
+ | `pressed_color` | `nil` (no auto-tint) | Color while held. Pass `:auto` to darken the base, or any color to use it explicitly |
1625
+ | `stroke_color` | `nil` | Outline color (self-rendered Buttons only) |
1626
+ | `stroke_width` | `0` | Outline width in pixels (self-rendered Buttons only) |
1627
+ | `label_color` | `'white'` | Label text color |
1628
+ | `hover_label_color` | `nil` | Label color while hovering |
1629
+ | `pressed_label_color` | `nil` | Label color while held |
1630
+ | `label_size` | `20` | Label font size |
1631
+ | `padding` | `0` | Inset from anchored edges (also `padding_top`/`padding_right`/`padding_bottom`/`padding_left`; owned visuals only) |
1632
+ | Block | `nil` | Sugar for `on(:click)` |
1633
+
1634
+ ### Hover and Pressed States
1635
+
1636
+ `hover_color:` tints the Button while the cursor is over it; `pressed_color:` tints it while the mouse is held down. When both are set and the user is hovering+pressed, **pressed wins**. Dragging out while held releases the press tint; dragging back in re-engages it.
1637
+
1638
+ ```ruby
1639
+ Button.new(x: 100, y: 100, width: 80, height: 26,
1640
+ label: 'RUMBLE',
1641
+ color: '#333',
1642
+ stroke_color: '#888', stroke_width: 1,
1643
+ label_color: '#bbb',
1644
+ pressed_color: '#ff9800', # fill while held
1645
+ pressed_label_color: '#000') do
1646
+ pad&.rumble(strength: 0.8, duration: 0.4)
1647
+ end
1648
+ ```
1649
+
1650
+ `:auto` derives a tint from the base color: it lightens for hover and darkens for pressed. Useful when you want feedback without picking specific colors:
1651
+
1652
+ ```ruby
1653
+ Button.new(..., hover_color: :auto, pressed_color: :auto)
1654
+ ```
1655
+
1656
+ Tints work with a [gradient fill](#per-vertex-colors) too. `:auto` lightens or darkens each vertex, preserving the gradient; an explicit `hover_color:`/`pressed_color:` may itself be a single color or a 4-color gradient:
1657
+
1658
+ ```ruby
1659
+ Button.new(color: ['navy', 'blue', 'teal', 'aqua'], hover_color: :auto)
1660
+ ```
1661
+
1662
+ `hover_label_color:` and `pressed_label_color:` require a `label:` to tint; supplying them without one raises an `ArgumentError`. When a tint is configured, change the resting color through `btn.color = …` (which updates the base color the tint derives from), not by mutating the visual directly; a direct change is overwritten on the next hover/press. Reading `btn.color` back returns that resting color, not the transient hover/press tint.
1663
+
1664
+ ### Custom Visual Button
1665
+
1666
+ Wrap any shape as a button. The hit area follows the visual, and moving the button (`button.x = …` / `button.y = …`) moves the wrapped shape, the click region, and any label together.
1667
+
1668
+ ```ruby
1669
+ circle = Circle.new(x: 200, y: 200, radius: 30, color: 'blue')
1670
+ btn = Button.new(circle) { puts 'Circle clicked!' }
1671
+ ```
1672
+
1673
+ By default, wrapping a visual does *not* tint it on hover. Opt in with `hover_color:`:
1674
+
1675
+ ```ruby
1676
+ Button.new(circle, hover_color: :auto) # lighten on hover
1677
+ Button.new(circle, hover_color: '#1976d2') # explicit hover color
1678
+ ```
1679
+
1680
+ ### Visual-less Button (hit area)
1681
+
1682
+ Omit `label`, `color`, and `stroke_color` to get an interactive region that renders nothing. Useful when the visual is drawn elsewhere (e.g., onto a `Canvas`):
1683
+
1684
+ ```ruby
1685
+ btn = Button.new(x: 12, y: 10, width: 26, height: 26)
1686
+ btn.on(:click) { puts 'swatch clicked' }
1687
+ btn.on(:mouse_down) { fill.color = ACCENT }
1688
+ btn.on(:mouse_up) { fill.color = BG }
1689
+ ```
1690
+
1691
+ A visual-less Button with no handlers is not registered as interactive; it neither fires events nor blocks events from interactive shapes underneath. Adding any of `label:`, `color:`, or `stroke_color:` reverts to the rendered button.
1692
+
1693
+ ### Button Methods
1694
+
1695
+ ```ruby
1696
+ btn.x = 50
1697
+ btn.y = 50
1698
+ btn.label = 'New Label'
1699
+ btn.label # => current label string or nil
1700
+ btn.color = '#555' # set the fill color (the resting color when a tint is configured)
1701
+ btn.color # => current fill color, or nil for a visual-less button
1702
+ btn.contains?(x, y)
1703
+ btn.on(event) # see Per-Object Events
1704
+ btn.off(descriptor)
1705
+ btn.remove
1706
+ btn.add
1707
+ ```
1708
+
1709
+ ## Input Events
1710
+
1711
+ Keyboard and mouse event handling for the DSL pattern. For per-object event handlers (`rect.on(:click) { ... }`), see [Working with Objects](#working-with-objects). For gamepads, see [Gamepads](#gamepads).
1712
+
1713
+ ### DSL Event Handlers
1714
+
1715
+ Register handlers with `on`:
1716
+
1717
+ ```ruby
1718
+ on :key_down do |event|
1719
+ puts event.key
1720
+ end
1721
+ ```
1722
+
1723
+ The return value is an `EventDescriptor` that can be used to unregister:
1724
+
1725
+ ```ruby
1726
+ handler = on :key_down do |event|
1727
+ puts event.key
1728
+ end
1729
+
1730
+ off handler
1731
+ ```
1732
+
1733
+ For events that carry a key, button, or axis, `on` also accepts a hash that filters by value. Each pair registers its own handler against the same block, so a single call can subscribe to multiple inputs at once:
1734
+
1735
+ ```ruby
1736
+ on key_down: :escape do
1737
+ close
1738
+ end
1739
+
1740
+ on key_down: [:left, :a] do # array → match any
1741
+ player.move_left
1742
+ end
1743
+
1744
+ on key_down: :right, gamepad_button_down: :dpad_right do # multi-event
1745
+ player.move_right
1746
+ end
1747
+ ```
1748
+
1749
+ The filtering form works for `:key_down` / `:key_held` / `:key_up`, `:mouse_down` / `:mouse_held` / `:mouse_up`, `:gamepad_button_down` / `:gamepad_button_held` / `:gamepad_button_up`, and `:gamepad_axis`. Other events have no matcher field and use the regular `on :event do |e| ... end` form: this includes the `:key` and `:mouse` catch-alls (which fire for *any* key or mouse event, so there's no single value to filter on — branch on the event object's `type` instead), as well as `:mouse_move`, `:mouse_scroll`, `:close`, and so on. A single filter returns one `EventDescriptor`; multiple filters return an array. Gamepad events also accept a hash matcher; see [Gamepads](#gamepads).
1750
+
1751
+ ### Keyboard Events
1752
+
1753
+ | Event | Fires | Event Object |
1754
+ |---|---|---|
1755
+ | `:key` | On any key event | `KeyEvent` (type, key) |
1756
+ | `:key_down` | Once when a key is pressed | `KeyEvent` |
1757
+ | `:key_held` | Every frame while a key is held | `KeyEvent` |
1758
+ | `:key_up` | Once when a key is released | `KeyEvent` |
1759
+
1760
+ ```ruby
1761
+ on :key_down do |event|
1762
+ close if event.key? :escape
1763
+ end
1764
+
1765
+ on :key_held do |event|
1766
+ puts "Holding: #{event.key}"
1767
+ end
1768
+ ```
1769
+
1770
+ The `KeyEvent` struct has fields: `type` (`:down`, `:held`, `:up`) and `key` (lowercase string), plus `key?(name)` for matching. `key?` accepts either a string or a symbol: `event.key?(:space)` and `event.key?('space')` are equivalent.
1771
+
1772
+ **Class pattern polling:**
1773
+
1774
+ ```ruby
1775
+ def update
1776
+ close if key_pressed?('escape')
1777
+ @x += 1 if key_held?('right')
1778
+ puts 'released space' if key_released?('space')
1779
+ end
1780
+ ```
1781
+
1782
+ ### Mouse Events
1783
+
1784
+ | Event | Fires | Event Object |
1785
+ |---|---|---|
1786
+ | `:mouse` | On any mouse event | `MouseEvent` |
1787
+ | `:mouse_down` | Once when a button is pressed | `MouseEvent` |
1788
+ | `:mouse_held` | Every frame while a button is held | `MouseEvent` |
1789
+ | `:mouse_up` | Once when a button is released | `MouseEvent` |
1790
+ | `:mouse_scroll` | When the scroll wheel moves | `MouseEvent` |
1791
+ | `:mouse_move` | When the mouse moves | `MouseEvent` |
1792
+ | `:mouse_enter` | When the cursor enters the window | `MouseEvent` |
1793
+ | `:mouse_leave` | When the cursor leaves the window | `MouseEvent` |
1794
+
1795
+ The `MouseEvent` struct has fields: `type`, `button`, `direction`, `x`, `y`, `delta_x`, `delta_y`, plus `button?(name)` for matching, `position` returning `[x, y]`, and `delta` returning `[delta_x, delta_y]`. `:mouse_enter` / `:mouse_leave` carry only `type`; query `mouse_position` if you need the current location.
1796
+
1797
+ ```ruby
1798
+ on :mouse_down do |event|
1799
+ puts "#{event.button} pressed at (#{event.x}, #{event.y})"
1800
+ end
1801
+
1802
+ on :mouse_scroll do |event|
1803
+ puts "Scrolled #{event.direction}: dx=#{event.delta_x} dy=#{event.delta_y}"
1804
+ end
1805
+
1806
+ on :mouse_move do |event|
1807
+ puts "Mouse at (#{event.x}, #{event.y})"
1808
+ end
1809
+
1810
+ on :mouse_leave do
1811
+ puts 'cursor left the window, pausing input-driven effects'
1812
+ end
1813
+ ```
1814
+
1815
+ **Class pattern polling:**
1816
+
1817
+ ```ruby
1818
+ def update
1819
+ puts 'left click' if mouse_pressed?(:left)
1820
+ puts 'right released' if mouse_released?(:right)
1821
+ paint(mouse_x, mouse_y) if mouse_held?(:left)
1822
+
1823
+ if mouse_scrolled?
1824
+ puts mouse_scroll_direction
1825
+ puts mouse_scroll_delta_x
1826
+ puts mouse_scroll_delta_y
1827
+ end
1828
+
1829
+ if mouse_moved?
1830
+ puts mouse_move_delta_x
1831
+ puts mouse_move_delta_y
1832
+ end
1833
+
1834
+ # Mouse position is always available:
1835
+ puts mouse_x
1836
+ puts mouse_y
1837
+ mx, my = mouse_position # both as a pair when you need them together
1838
+
1839
+ paint(mouse_x, mouse_y) if mouse_inside? # cursor is over the window
1840
+ end
1841
+ ```
1842
+
1843
+ ## Gamepads
1844
+
1845
+ Multi-gamepad support is the default. Every event carries the `Gamepad` it came from, and per-pad state (button/axis polling, dead zone, feedback) lives on the `Gamepad` object. The same `pad` instance survives until the device is unplugged; reconnects produce a new object.
1846
+
1847
+ A few things worth knowing before you start:
1848
+
1849
+ - **Cardinal face button names, not labels.** `:south` / `:east` / `:west` / `:north` are the *positions* on the pad; `:south` is always the bottom face button, regardless of whether your pad prints A (Xbox), B (Nintendo), or ✕ (PlayStation). See "Buttons and axes" below for the layout. If you'd rather write `:a` / `:b` / `:x` / `:y` in your game code, define your own constants; the section "Common patterns" shows how.
1850
+ - **Reconnects produce a new `Gamepad`.** If a player swaps pads mid-game, the new pad is a fresh `Gamepad` object, not a revival of the old one. The old object stays valid but disconnected (polling returns safe defaults, feedback methods no-op). For stable player slots that survive disconnect, see "Common patterns" below.
1851
+ - **`gamepads[0]` is connect order, not "Player 1".** The collection is ordered by when each pad plugged in. If you want stable player slots, build the mapping yourself; connect order is rarely what you want once disconnects are in play.
1852
+
1853
+ ### Lifecycle
1854
+
1855
+ `:gamepad_connect` fires for every pad already plugged in when `show` is called, and again for any pad plugged in mid-game. `:gamepad_disconnect` fires on unplug. Both blocks receive the `Gamepad`:
1856
+
1857
+ ```ruby
1858
+ on :gamepad_connect do |pad|
1859
+ puts "Connected: #{pad.name} (id=#{pad.id}, type=#{pad.type})"
1860
+ end
1861
+
1862
+ on :gamepad_disconnect do |pad|
1863
+ puts "Disconnected: #{pad.name}"
1864
+ end
1865
+ ```
1866
+
1867
+ Polling a disconnected gamepad is safe: `held?` returns `false`, `axis` returns `0.0`, feedback methods return `false`. No exceptions.
1868
+
1869
+ ### Event handlers
1870
+
1871
+ Six gamepad event types. Block args are unpacked from the event:
1872
+
1873
+ | Event | Block args | Fires |
1874
+ |---|---|---|
1875
+ | `:gamepad_connect` | `(pad)` | When a pad is plugged in (and once at `show` for each pad already present) |
1876
+ | `:gamepad_disconnect` | `(pad)` | When a pad is unplugged |
1877
+ | `:gamepad_button_down` | `(pad, button)` | Once on transition to down |
1878
+ | `:gamepad_button_held` | `(pad, button)` | Every frame while the button is held |
1879
+ | `:gamepad_button_up` | `(pad, button)` | Once on transition to up |
1880
+ | `:gamepad_axis` | `(pad, axis, value)` | When a stick or trigger moves outside the dead zone |
1881
+
1882
+ ```ruby
1883
+ on :gamepad_button_down do |pad, button|
1884
+ player_for(pad).jump if button == :south
1885
+ end
1886
+
1887
+ on :gamepad_axis do |pad, axis, value|
1888
+ steer(pad, value) if axis == :left_x
1889
+ end
1890
+ ```
1891
+
1892
+ Filter form takes either a scalar / array (matched against `button` for button events, `axis` for axis events) or a hash matched against any of `gamepad`, `button`, `axis`:
1893
+
1894
+ ```ruby
1895
+ on gamepad_button_down: :south do |pad|
1896
+ player_for(pad).jump
1897
+ end
1898
+
1899
+ on gamepad_button_down: { gamepad: pad1, button: :south } do
1900
+ player1.jump
1901
+ end
1902
+
1903
+ on gamepad_axis: :left_x do |pad, axis, value|
1904
+ steer(pad, value)
1905
+ end
1906
+ ```
1907
+
1908
+ A filter narrows *which* events fire the handler; it doesn't change the block args. A filtered handler still receives the same arguments as its unfiltered form, so a filtered `:gamepad_axis` block takes `(pad, axis, value)`, not `(pad, value)`.
1909
+
1910
+ Cross-source filters compose with keyboard / mouse:
1911
+
1912
+ ```ruby
1913
+ on key_down: :right, gamepad_button_down: :dpad_right do
1914
+ player.move_right
1915
+ end
1916
+ ```
1917
+
1918
+ ### The `gamepads` collection
1919
+
1920
+ `window.gamepads` is an `Enumerable` of all currently connected pads in connect order. Index 0 is the first-connected pad still present. Iteration order is documented as a contract.
1921
+
1922
+ ```ruby
1923
+ gamepads # Array, Enumerable
1924
+ gamepads[0] # first connected pad, or nil
1925
+ gamepads.size
1926
+ gamepads.first
1927
+ gamepads.each { |pad| ... }
1928
+ gamepads.find { |pad| pad.id == n }
1929
+ ```
1930
+
1931
+ Polling every connected pad each frame is the natural shape for games that don't care which slot a player is in:
1932
+
1933
+ ```ruby
1934
+ update do |dt|
1935
+ gamepads.each do |pad|
1936
+ player_for(pad).move(pad.axis(:left_x), pad.axis(:left_y), dt)
1937
+ end
1938
+ end
1939
+ ```
1940
+
1941
+ `Gamepad` instances use identity equality, so they're stable `Hash` keys for the lifetime of the connection. Hang per-player state directly off the pad:
1942
+
1943
+ ```ruby
1944
+ players = {} # { Gamepad => player state }
1945
+
1946
+ on(:gamepad_connect) { |pad| players[pad] = { score: 0, color: NEXT_COLOR.call } }
1947
+ on(:gamepad_disconnect) { |pad| players.delete(pad) }
1948
+
1949
+ on :gamepad_button_down do |pad, button|
1950
+ players[pad][:score] += 1 if button == :south
1951
+ end
1952
+ ```
1953
+
1954
+ ### Common patterns
1955
+
1956
+ #### Single-pad games
1957
+
1958
+ `each` is a no-op when no pad is connected, so single-player games don't need a nil check:
1959
+
1960
+ ```ruby
1961
+ update do |dt|
1962
+ gamepads.each do |pad|
1963
+ player.x += pad.axis(:left_x) * SPEED * dt
1964
+ player.jump if pad.pressed?(:south)
1965
+ end
1966
+ end
1967
+ ```
1968
+
1969
+ #### Press-to-join lobby
1970
+
1971
+ Gate joins on a state flag and a known button (`:start` is the convention). The `Gamepad` becomes a hash key as soon as it joins:
1972
+
1973
+ ```ruby
1974
+ state = :lobby
1975
+ players = {}
1976
+
1977
+ on :gamepad_button_down do |pad, button|
1978
+ next unless state == :lobby && button == :start
1979
+ next if players.key?(pad) # already joined
1980
+
1981
+ players[pad] = { slot: players.size }
1982
+ state = :playing if players.size >= 2
1983
+ end
1984
+ ```
1985
+
1986
+ #### Slots that survive disconnect
1987
+
1988
+ If your game has stable player slots ("Player 1", "Player 2"), keep an array of pad references with `nil` for empty slots. Disconnect leaves a hole; the next pad to press `:start` fills it:
1989
+
1990
+ ```ruby
1991
+ slots = [nil, nil] # two-player game; nil = open slot
1992
+ paused = false
1993
+
1994
+ on :gamepad_disconnect do |pad|
1995
+ i = slots.index(pad) or next
1996
+ slots[i] = nil
1997
+ paused = true
1998
+ end
1999
+
2000
+ on :gamepad_button_down do |pad, button|
2001
+ next unless paused && button == :start
2002
+ i = slots.index(nil) or next
2003
+ slots[i] = pad
2004
+ paused = slots.any?(&:nil?)
2005
+ end
2006
+
2007
+ update do |dt|
2008
+ next if paused
2009
+ slots.each_with_index do |pad, i|
2010
+ next if pad.nil?
2011
+ dx = pad.axis(:left_x)
2012
+ dy = pad.axis(:left_y)
2013
+ # drive slot i's game state from (dx, dy) and any other pad input
2014
+ end
2015
+ end
2016
+ ```
2017
+
2018
+ The reconnecting pad is a new `Gamepad`; that's why slots hold raw pad references rather than being keyed by pad. The new pad takes over the open slot; the old `Gamepad` object is forgotten.
2019
+
2020
+ #### Per-pad input remapping
2021
+
2022
+ Control schemes are just data — keep a per-pad map and look up button names instead of hard-coding them:
2023
+
2024
+ ```ruby
2025
+ controls = {}
2026
+
2027
+ on :gamepad_connect do |pad|
2028
+ controls[pad] = { jump: :south, attack: :east, dodge: :west }
2029
+ end
2030
+
2031
+ on :gamepad_button_down do |pad, button|
2032
+ case controls[pad].key(button)
2033
+ when :jump then players[pad].jump
2034
+ when :attack then players[pad].attack
2035
+ when :dodge then players[pad].dodge
2036
+ end
2037
+ end
2038
+ ```
2039
+
2040
+ Two players can have different control maps by assigning different hashes when each pad connects.
2041
+
2042
+ ### Per-gamepad polling
2043
+
2044
+ ```ruby
2045
+ pad.held?(:south) # currently held
2046
+ pad.pressed?(:south) # transitioned to down this frame
2047
+ pad.released?(:south) # transitioned to up this frame
2048
+ pad.buttons_held # Array of currently-held buttons
2049
+
2050
+ pad.axis(:left_x) # -1.0..1.0, dead-zoned
2051
+ pad.axis(:left_x, raw: true) # raw, no dead zone
2052
+ pad.axes # full Hash {:left_x => 0.4, :left_y => 0.0, ...}
2053
+ pad.axis_moved?(:left_x) # axis moved this frame
2054
+ pad.axes_moved # Array of axes moved this frame
2055
+ ```
2056
+
2057
+ Unknown names are safe: `pad.held?(:not_a_button)` returns `false`, `pad.axis(:not_an_axis)` returns `0.0`. `pad.axes` always includes every axis with a default of `0.0` for axes that haven't fired events yet.
2058
+
2059
+ ### Dead zones
2060
+
2061
+ Sticks only: `:left_trigger` and `:right_trigger` are exempt because they rest at zero and rarely drift, so the full 0.0..1.0 range stays usable. Default 0.05. Below the threshold, sticks return `0.0` (symmetric around 0). `:gamepad_axis` events receive the dead-zoned value, and successive events with the same dead-zoned value are suppressed, so handlers don't get spammed by motion entirely inside the dead zone.
2062
+
2063
+ ```ruby
2064
+ pad.dead_zone # Float, default 0.05 (sticks only)
2065
+ pad.dead_zone = 0.0 # disable
2066
+ pad.axis(:left_x, raw: true) # bypass per-call
2067
+ ```
2068
+
2069
+ ### Buttons and axes
2070
+
2071
+ Names are positional; they describe the *layout*, not the labels printed on the pad. Face buttons use cardinal directions:
2072
+
2073
+ ```
2074
+ north
2075
+ (Y / X / △)
2076
+
2077
+ west east
2078
+ (X / Y / □) (B / A / ○)
2079
+
2080
+ south
2081
+ (A / B / ✕)
2082
+ ```
2083
+
2084
+ Labels in parens are Xbox / Nintendo / PlayStation, in that order. The cardinal name refers to the *position* on the pad; the same code works on any layout.
2085
+
2086
+ **Buttons** — `:south`, `:east`, `:west`, `:north`, `:back`, `:guide`, `:start`, `:left_stick`, `:right_stick`, `:left_shoulder`, `:right_shoulder`, `:dpad_up`, `:dpad_down`, `:dpad_left`, `:dpad_right`, `:misc1`, `:paddle1`, `:paddle2`, `:paddle3`, `:paddle4`, `:touchpad`, `:misc2`, `:misc3`, `:misc4`, `:misc5`, `:misc6`. Extended buttons (`:misc*`, `:paddle*`, `:touchpad`) only fire on pads that physically have them; use `pad.has?(:button, :paddle1)` to check.
2087
+
2088
+ **Axes** — `:left_x`, `:left_y`, `:right_x`, `:right_y` (range -1.0..1.0); `:left_trigger`, `:right_trigger` (range 0.0..1.0).
2089
+
2090
+ #### User-defined aliases
2091
+
2092
+ Cardinal is the canonical vocabulary because it's unambiguous across pad layouts, but Ruby's first-class symbols mean any project can layer game-specific names on top in two lines. This isn't a workaround; it's the intended pattern.
2093
+
2094
+ ```ruby
2095
+ # Shorter names matching your preferred layout
2096
+ A, B, X, Y = :south, :east, :west, :north
2097
+ pad.held?(A)
2098
+ on gamepad_button_down: B do |pad|
2099
+ punch(pad)
2100
+ end
2101
+
2102
+ # Or game-specific actions
2103
+ ACTIONS = { jump: :south, attack: :east, dodge: :west }
2104
+ pad.held?(ACTIONS[:jump])
2105
+ ```
2106
+
2107
+ ### Capability queries, type, and battery
2108
+
2109
+ ```ruby
2110
+ pad.id # identifier, opaque integer, stable for the connection
2111
+ pad.name # String, e.g. "Xbox Wireless Controller"
2112
+ pad.type # :xbox, :playstation, :nintendo, :generic, :unknown
2113
+ pad.connected? # false after disconnect
2114
+
2115
+ pad.has?(:rumble)
2116
+ pad.has?(:rumble_triggers)
2117
+ pad.has?(:led)
2118
+ pad.has?(:button, :paddle1)
2119
+ pad.has?(:axis, :left_trigger)
2120
+
2121
+ pad.battery # :wired, :full, :medium, :low, :empty, or nil
2122
+ ```
2123
+
2124
+ `id`, `name`, `type`, and capability checks are cached at connect time and never change for the lifetime of the connection. `battery` is queried live every call.
2125
+
2126
+ For debugging an unrecognized device or composing a custom mapping, `pad.debug_info` returns a Hash with the GUID, USB vendor / product / version IDs, serial number, connection state (`:wired` / `:wireless` / `:unknown`), un-remapped real type, touchpad count, and the resolved mapping string. Returns `nil` for a disconnected pad.
2127
+
2128
+ `pad.joystick_state` returns the raw, pre-mapping hardware snapshot — `{ buttons:, axes:, hats: }` with axes as raw `-32768..32767` values and hats as SDL hat bitmasks — for mapping-generation tools that need to observe the physical device before SDL remaps it. Returns `nil` for a disconnected pad.
2129
+
2130
+ ### Feedback
2131
+
2132
+ Rumble strengths are 0.0..1.0; durations are in seconds. `rumble` and `rumble_triggers` return `false` silently if the pad is disconnected or doesn't support the feature. The `led =` form is a Ruby assignment, so it evaluates to the assigned array rather than a success flag; call `pad.set_led(rgb)` instead when you need the `false`-on-failure return.
2133
+
2134
+ ```ruby
2135
+ pad.rumble(strength: 0.5, duration: 0.2)
2136
+ pad.rumble(low: 0.5, high: 1.0, duration: 0.2)
2137
+ pad.rumble_triggers(left: 1.0, right: 0.0, duration: 0.1)
2138
+ pad.led = [255, 0, 128]
2139
+ ```
2140
+
2141
+ ### Mappings
2142
+
2143
+ Ruby 2D ships with virtually all common mappings built in, so most projects never need this. For the rare exception:
2144
+
2145
+ - `~/.ruby2d/gamepads.txt` is loaded automatically when the window is shown.
2146
+ - `add_gamepad_mapping(path_or_string)` smart-parses its argument: if it points at an existing file, it loads it; otherwise it's treated as a single mapping string
2147
+
2148
+ The string format is the standard SDL gamepad mapping (e.g. `030000005e040000ea02000000007801,Xbox One S Controller,a:b0,b:b1,...`); the [SDL_GameControllerDB](https://github.com/mdqinc/SDL_GameControllerDB) community database works as-is.
2149
+
2150
+ The leading 32-hex segment is the gamepad's GUID: a stable identifier derived from the device's bus (USB or Bluetooth), USB vendor / product / version IDs, and a hash of its name. The same pad keeps the same GUID across reboots and machines, which is why a mapping written once works for every user with that hardware. Two things change it for the same physical pad: switching connection mode (a pad paired over USB and over Bluetooth gets two different GUIDs, and may need two mapping entries), and renaming the controller in the OS; on macOS, the label in System Settings → Game Controllers feeds into the name hash, so renaming a pad silently invalidates any mapping keyed to its old GUID.
2151
+
2152
+ ## Working with Objects
2153
+
2154
+ Every renderable object — shapes, images, text, sprites, canvases, buttons — can register per-object event handlers for mouse interactions, and has independent lifecycle controls for adding, removing, showing, and hiding.
2155
+
2156
+ ### Per-Object Events
2157
+
2158
+ Any renderable object can register its own event handlers for mouse interactions.
2159
+
2160
+ | Object Event | Description |
2161
+ |---|---|
2162
+ | `:click` | Mouse down and up on the same object |
2163
+ | `:hover` | Mouse enters the object |
2164
+ | `:hover_out` | Mouse leaves the object |
2165
+ | `:mouse_down` | Mouse button pressed on the object |
2166
+ | `:mouse_held` | Every frame while a button is held (fires on the object originally pressed) |
2167
+ | `:mouse_up` | Mouse button released. Fires on the originally-pressed object, plus on the topmost object under the cursor at release if different |
2168
+ | `:drag` | Mouse moved while pressed on the object |
2169
+ | `:mouse_scroll` | Scroll wheel while hovering over the object |
2170
+
2171
+ Events are dispatched to the topmost (highest z-order) interactive object at the mouse position.
2172
+
2173
+ **Example:**
2174
+
2175
+ ```ruby
2176
+ rect = Rectangle.new(x: 100, y: 100, width: 80, height: 80, color: 'blue')
2177
+
2178
+ rect.on :click do |event|
2179
+ puts "Clicked at (#{event.x}, #{event.y})"
2180
+ end
2181
+
2182
+ rect.on :hover do |event|
2183
+ rect.color = 'yellow'
2184
+ end
2185
+
2186
+ rect.on :hover_out do |event|
2187
+ rect.color = 'blue'
2188
+ end
2189
+
2190
+ rect.on :mouse_down do |event|
2191
+ puts "Mouse down: #{event.button}"
2192
+ end
2193
+
2194
+ rect.on :mouse_up do |event|
2195
+ puts "Mouse up"
2196
+ end
2197
+
2198
+ rect.on :drag do |event|
2199
+ rect.x += event.delta_x
2200
+ rect.y += event.delta_y
2201
+ end
2202
+
2203
+ rect.on :mouse_scroll do |event|
2204
+ puts "Scrolled #{event.direction}"
2205
+ end
2206
+ ```
2207
+
2208
+ The kwarg form filtering by button works on objects too, for events that carry a button (`:mouse_down`, `:mouse_held`, `:mouse_up`, `:click`, `:drag`):
2209
+
2210
+ ```ruby
2211
+ rect.on(click: :left) { puts 'left-clicked' }
2212
+ rect.on(mouse_down: :right, click: :right) { open_context_menu }
2213
+ ```
2214
+
2215
+ Remove a per-object handler:
2216
+
2217
+ ```ruby
2218
+ handler = rect.on(:click) { puts 'clicked' }
2219
+ rect.off(handler)
2220
+ ```
2221
+
2222
+ Check if an object has handlers:
2223
+
2224
+ ```ruby
2225
+ rect.interactive? # any handlers at all?
2226
+ rect.interactive?(:click) # handlers for :click specifically?
2227
+ ```
2228
+
2229
+ ### Managing Objects
2230
+
2231
+ Every renderable object has two independent lifecycle controls:
2232
+
2233
+ ```ruby
2234
+ sq = Square.new(x: 0, y: 0, size: 50)
2235
+
2236
+ # Scene-graph membership
2237
+ sq.remove # remove from the window's drawing list
2238
+ sq.add # re-add (appended at its z-bucket)
2239
+
2240
+ # Visibility (preserves scene-graph position and z-order)
2241
+ sq.hide # stop drawing, but stay in the scene graph
2242
+ sq.show # draw again
2243
+ sq.visible = false # same as .hide
2244
+ sq.visible? # => false
2245
+
2246
+ clear # remove all objects from the window
2247
+ ```
2248
+
2249
+ **When to use which:**
2250
+
2251
+ - `.hide` / `.show` for frame-by-frame visibility (blinking, UI overlays, paused entities). Cheap, preserves z-order among siblings.
2252
+ - `.remove` / `.add` for lifecycle (spawn, despawn, destroy). Use `.remove` only when you genuinely want to stop participating in the scene graph.
2253
+
2254
+ Toggling with `.remove` + `.add`, or by blanking a `Text`'s `content`, to achieve a hide/show effect is wasteful: `.remove` / `.add` re-inserts at the end of the object's z-bucket so z-equal siblings may reorder, and reassigning `content` rebuilds the texture on every toggle. Use `.hide` / `.show` instead.
2255
+
2256
+ The `z` property controls draw order. Higher values are drawn on top:
2257
+
2258
+ ```ruby
2259
+ bg = Rectangle.new(x: 0, y: 0, width: 640, height: 480, z: 0)
2260
+ fg = Square.new(x: 100, y: 100, size: 50, z: 10)
2261
+ ```
2262
+
2263
+ ## Performance
2264
+
2265
+ Most apps never need to think about performance. It starts to matter when a scene has hundreds of moving parts, or targets [the web](#building-for-the-web), where Ruby runs several times slower than native. Three habits cover nearly all of it:
2266
+
2267
+ **Create shapes once and mutate them each frame.** A persistent object made with `.new` costs far less per frame than redrawing with a `.render` call, which re-processes all of its arguments on every call.
2268
+
2269
+ **Recolor in place.** Assigning `shape.color = [r, g, b, a]` builds a new color object each time; assigning channels directly (`shape.color.r = 0.5`) allocates nothing.
2270
+
2271
+ ```ruby
2272
+ require 'ruby2d'
2273
+
2274
+ # Created once; the update block just reshapes and recolors them
2275
+ bars = Array.new(50) do |i|
2276
+ Rectangle.new(x: 10 + i * 12, y: 240, width: 10, height: 0, color: [0.2, 0.6, 1.0, 1])
2277
+ end
2278
+
2279
+ update do
2280
+ bars.each_with_index do |bar, i|
2281
+ height = 140 + Math.sin(elapsed * 2 + i * 0.3) * 120
2282
+ bar.y = 340 - height
2283
+ bar.height = height
2284
+ bar.color.g = 0.3 + 0.4 * (height / 260.0) # mutate the channel, no allocation
2285
+ end
2286
+ end
2287
+
2288
+ show
2289
+ ```
2290
+
2291
+ **In the [render block](#render-block), prefer string colors.** Color strings like `'red'` or `'#33aaff'` are resolved once and cached across calls; array colors are re-parsed on every call; if a per-frame array color shows up hot, switch that drawing to a persistent shape and mutate its color instead.
2292
+
2293
+ ## Building Native Applications
2294
+
2295
+ `ruby2d build app.rb` compiles your app into a standalone native executable in `build/native/` (on macOS it also produces an `App.app` bundle); run it with `ruby2d launch --native`. The build targets the machine it runs on and does not cross-compile.
2296
+
2297
+ Native builds link SDL3 and mruby as static libraries. The gem bundles these for the most common platforms — macOS on Apple silicon, and Windows on x86-64 and ARM64 — where native building works with no extra setup.
2298
+
2299
+ ### Setting Up Other Platforms
2300
+
2301
+ On a platform the gem doesn't bundle — such as Linux, Intel macOS, or BSD — Ruby 2D needs SDL3 to build its native extension. If SDL3 isn't found when the gem installs, it installs *without* the extension and prints how to finish; running an app before then (`require 'ruby2d'`) prints the same guidance. You have two ways to complete it.
2302
+
2303
+ **Install SDL3 with your system package manager**, then rebuild the extension:
2304
+
2305
+ ```bash
2306
+ sudo apt install libsdl3-dev libsdl3-image-dev libsdl3-mixer-dev libsdl3-ttf-dev # Debian/Ubuntu
2307
+ gem pristine ruby2d
2308
+ ```
2309
+
2310
+ Use your distribution's equivalent; the install message names the exact command for your system. SDL3 is recent, so some package repositories may not carry it yet; if yours doesn't, use `ruby2d setup` instead.
2311
+
2312
+ **Or build the libraries locally** with `ruby2d setup` — no SDL3 packages required, so this works even where your distro doesn't package SDL3 yet:
2313
+
2314
+ ```bash
2315
+ ruby2d setup
2316
+ ```
2317
+
2318
+ This clones and compiles SDL3 and mruby for your platform (it needs `git`, `cmake`, and a C compiler — on Linux, also `pkg-config` and the X11 or Wayland development libraries — and takes a few minutes), caches them, and rebuilds the extension for you. The same libraries are what `ruby2d build --native` links against, so this one step covers both running and building apps. `setup` checks for these up front and stops with a clear message if any are missing, naming what to install.
2319
+
2320
+ Because that's a lot to kick off, `setup` first prints the platform it detected, the exact cache location it will write to, and the steps it will run, then asks to continue before doing any of it. Press Enter to accept, or anything else to cancel. The prompt is skipped when standard input isn't a terminal, so scripts and CI runs aren't left hanging.
2321
+
2322
+ ```bash
2323
+ ruby2d setup --force # rebuild even if the libraries already exist
2324
+ ruby2d setup --clean # remove the built libraries for this platform
2325
+ ruby2d setup --yes # skip the confirmation prompt (also -y)
2326
+ ```
2327
+
2328
+ Re-run `ruby2d setup` after upgrading Ruby 2D: a newer version may pin newer SDL or mruby.
2329
+
2330
+ ## Bundling Assets
2331
+
2332
+ Apps that load external files — images, audio, sprite sheets — need those files available to the built app, whether it runs natively or in the browser. `ruby2d build` bundles a directory of assets for both targets: a web build mounts it into the WebAssembly virtual filesystem; a native build copies it next to the executable (under `build/native/`, and into the macOS `App.app` bundle). Either way the directory lands at the same relative path you name it, so one reference like `Image.new('media/x.png')` resolves in both.
2333
+
2334
+ Pass the directory with `--assets`:
2335
+
2336
+ ```bash
2337
+ ruby2d build --assets media app.rb
2338
+ ```
2339
+
2340
+ Or declare it inline, so no flag is needed — add a `# ruby2d:assets <dir>` directive anywhere in the source (one directory per line; repeat for several):
2341
+
2342
+ ```ruby
2343
+ require 'ruby2d'
2344
+
2345
+ # ruby2d:assets media
2346
+ ```
2347
+
2348
+ A directive behaves exactly like `--assets`, and the two combine (a build honors both the flag and every directive). Paths resolve relative to the directory `ruby2d build` runs in, and a declared directory that doesn't exist aborts the build.
2349
+
2350
+ ## Building for the Web
2351
+
2352
+ Ruby 2D apps can be compiled to WebAssembly using the `ruby2d` CLI. This requires [Emscripten](https://emscripten.org) — specifically the `emcc` compiler — to be installed and on your `PATH`; if it isn't, a plain `ruby2d build` skips the web build and still produces the native app, while an explicit `ruby2d build --web` reports the missing `emcc` as an error. Web builds also compile your Ruby with `mrbc`, the mruby compiler: it's bundled for macOS and Windows, and provided by [`ruby2d setup`](#setting-up-other-platforms) on other platforms.
2353
+
2354
+ ### Commands
2355
+
2356
+ ```bash
2357
+ # Build for all platforms (native + web)
2358
+ ruby2d build app.rb
2359
+
2360
+ # Build for web only
2361
+ ruby2d build --web app.rb
2362
+
2363
+ # Launch the built web app in a local server
2364
+ ruby2d launch --web
2365
+ ```
2366
+
2367
+ ### Output
2368
+
2369
+ A web build produces the following files in `build/web/`:
2370
+
2371
+ - `app.html` — the HTML shell page
2372
+ - `app.js` — the compiled JavaScript/Wasm loader
2373
+ - `app.wasm` — the WebAssembly binary
2374
+ - `app.data` — bundled asset data (always produced: the default font, plus any [bundled assets](#bundling-assets))
2375
+
2376
+ When you deploy these files, serve them with gzip or Brotli compression; `app.wasm` is by far the largest file and compresses to roughly a third of its size, which noticeably speeds up the first load. Most static hosts and CDNs (GitHub Pages, Netlify, Cloudflare, and the like) do this automatically; if you run your own server, enable it there.
2377
+
2378
+ ### Options
2379
+
2380
+ To bundle images, audio, or other media into the build, see [Bundling Assets](#bundling-assets); `--assets` and the `# ruby2d:assets` directive work for web and native builds alike.
2381
+
2382
+ **Use a custom HTML template** instead of the built-in page:
2383
+
2384
+ ```bash
2385
+ ruby2d build --web --template page.html app.rb
2386
+ ```
2387
+
2388
+ Your template must load the compiled app and provide the canvas it draws to. The simplest starting point is the built-in template; copy it from the output of a plain `ruby2d build --web` (it's the generated `build/web/app.html`) and edit from there. The essentials it sets up:
2389
+
2390
+ - A `<canvas id="canvas">` element for rendering.
2391
+ - A `Module` object whose `canvas` property points at that element (and, optionally, a `print` function to capture output).
2392
+ - `<script async src="app.js"></script>` to load the compiled app.
2393
+
2394
+ For a `--single-file` build (below) the template is passed to Emscripten as its [shell file](https://emscripten.org/docs/tools_reference/emcc.html) instead, so it follows that format: include the `{{{ SCRIPT }}}` placeholder where the inlined app code should go, rather than a `<script src>` tag.
2395
+
2396
+ **Produce a single self-contained HTML file** (no separate `.js`, `.wasm`, or `.data` files):
2397
+
2398
+ ```bash
2399
+ ruby2d build --web --single-file app.rb
2400
+ ```
2401
+
2402
+ ### mruby and CRuby Differences
2403
+
2404
+ Web builds compile your Ruby code using [mruby](https://mruby.org) rather than the standard CRuby interpreter. mruby is a lightweight, embeddable Ruby implementation and is not fully compatible with CRuby. Things to be aware of:
2405
+
2406
+ - The standard library is limited: many CRuby built-in classes and modules are unavailable or have reduced functionality.
2407
+ - Gems that rely on C extensions or CRuby internals will not work.
2408
+ - Some Ruby syntax and language features supported by CRuby may not be available in mruby.
2409
+
2410
+ Test your app with `ruby2d build --web` early to catch any incompatibilities.
2411
+
2412
+ ### Frame Rate
2413
+
2414
+ Web builds drive the render loop from the browser's `requestAnimationFrame`, so they run at the display's refresh rate, including high-refresh displays (120 Hz and up). Write motion to be [frame-rate independent with `dt`](#frame-rate-independence-with-dt) so it looks the same at any refresh rate; this matters more on the web, where a complex scene may not reach native frame rates (mruby is slower than CRuby, and WebAssembly trails native code).
2415
+
2416
+ Browser specifics:
2417
+
2418
+ - **Chrome** and **Firefox** run at the full refresh rate by default.
2419
+ - **Safari** caps `requestAnimationFrame` at 60 Hz unless you turn off the **"Prefer Page Rendering Updates near 60 fps"** flag (Safari ▸ Develop ▸ Feature Flags). With it off, Safari matches the display refresh rate too.
2420
+
2421
+ ### Detecting the Web Build
2422
+
2423
+ `Ruby2D.web?` reports whether the running app was compiled for the web, so a scene can be sized to the target — the browser being the slower one, give it less to do:
2424
+
2425
+ ```ruby
2426
+ particles = 500 if Ruby2D.web?
2427
+ ```
2428
+
2429
+ The answer is settled when the app is built rather than probed at runtime: `true` only in a web build, `false` in a native build and when running with `ruby`.
2430
+
2431
+ ### Cleaning Up
2432
+
2433
+ ```bash
2434
+ ruby2d build --clean
2435
+ ```