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.
- checksums.yaml +4 -4
- data/LICENSE.md +7 -0
- data/README.md +141 -0
- data/USAGE.md +2435 -0
- data/assets/Rakefile +637 -0
- data/assets/build_support/CMakeLists.txt +94 -0
- data/assets/build_support/mruby/build_config.rb +34 -0
- data/assets/build_support/mruby/build_config_wasm.rb +30 -0
- data/assets/build_support/sdl_test.c +174 -0
- data/assets/deps.yaml +16 -0
- data/assets/platform/include/SDL3/SDL.h +91 -0
- data/assets/platform/include/SDL3/SDL_assert.h +705 -0
- data/assets/platform/include/SDL3/SDL_asyncio.h +550 -0
- data/assets/platform/include/SDL3/SDL_atomic.h +692 -0
- data/assets/platform/include/SDL3/SDL_audio.h +2370 -0
- data/assets/platform/include/SDL3/SDL_begin_code.h +553 -0
- data/assets/{include/SDL2 → platform/include/SDL3}/SDL_bits.h +47 -27
- data/assets/{include/SDL2 → platform/include/SDL3}/SDL_blendmode.h +59 -55
- data/assets/platform/include/SDL3/SDL_camera.h +535 -0
- data/assets/platform/include/SDL3/SDL_clipboard.h +331 -0
- data/assets/{include/SDL2/close_code.h → platform/include/SDL3/SDL_close_code.h} +9 -8
- data/assets/{include/SDL2 → platform/include/SDL3}/SDL_copying.h +3 -1
- data/assets/platform/include/SDL3/SDL_cpuinfo.h +374 -0
- data/assets/platform/include/SDL3/SDL_dialog.h +343 -0
- data/assets/platform/include/SDL3/SDL_dlopennote.h +234 -0
- data/assets/{include/SDL2 → platform/include/SDL3}/SDL_egl.h +11 -8
- data/assets/platform/include/SDL3/SDL_endian.h +645 -0
- data/assets/platform/include/SDL3/SDL_error.h +226 -0
- data/assets/platform/include/SDL3/SDL_events.h +1645 -0
- data/assets/platform/include/SDL3/SDL_filesystem.h +537 -0
- data/assets/platform/include/SDL3/SDL_gamepad.h +1656 -0
- data/assets/platform/include/SDL3/SDL_gpu.h +4608 -0
- data/assets/platform/include/SDL3/SDL_guid.h +106 -0
- data/assets/platform/include/SDL3/SDL_haptic.h +1461 -0
- data/assets/platform/include/SDL3/SDL_hidapi.h +571 -0
- data/assets/platform/include/SDL3/SDL_hints.h +5001 -0
- data/assets/platform/include/SDL3/SDL_init.h +507 -0
- data/assets/platform/include/SDL3/SDL_intrin.h +410 -0
- data/assets/platform/include/SDL3/SDL_iostream.h +1379 -0
- data/assets/platform/include/SDL3/SDL_joystick.h +1385 -0
- data/assets/platform/include/SDL3/SDL_keyboard.h +608 -0
- data/assets/platform/include/SDL3/SDL_keycode.h +347 -0
- data/assets/platform/include/SDL3/SDL_loadso.h +145 -0
- data/assets/{include/SDL2 → platform/include/SDL3}/SDL_locale.h +40 -24
- data/assets/platform/include/SDL3/SDL_log.h +541 -0
- data/assets/platform/include/SDL3/SDL_main.h +704 -0
- data/assets/platform/include/SDL3/SDL_main_impl.h +151 -0
- data/assets/{include/SDL2 → platform/include/SDL3}/SDL_messagebox.h +85 -48
- data/assets/{include/SDL2 → platform/include/SDL3}/SDL_metal.h +29 -29
- data/assets/{include/SDL2 → platform/include/SDL3}/SDL_misc.h +13 -12
- data/assets/platform/include/SDL3/SDL_mouse.h +813 -0
- data/assets/platform/include/SDL3/SDL_mutex.h +1117 -0
- data/assets/platform/include/SDL3/SDL_oldnames.h +1339 -0
- data/assets/{include/SDL2 → platform/include/SDL3}/SDL_opengl.h +1247 -278
- data/assets/{include/SDL2 → platform/include/SDL3}/SDL_opengl_glext.h +5 -1
- data/assets/{include/SDL2 → platform/include/SDL3}/SDL_opengles.h +6 -7
- data/assets/{include/SDL2 → platform/include/SDL3}/SDL_opengles2.h +10 -11
- data/assets/platform/include/SDL3/SDL_pen.h +198 -0
- data/assets/platform/include/SDL3/SDL_pixels.h +1441 -0
- data/assets/{include/SDL2/SDL_test_log.h → platform/include/SDL3/SDL_platform.h} +25 -26
- data/assets/platform/include/SDL3/SDL_platform_defines.h +497 -0
- data/assets/{include/SDL2 → platform/include/SDL3}/SDL_power.h +40 -20
- data/assets/platform/include/SDL3/SDL_process.h +441 -0
- data/assets/platform/include/SDL3/SDL_properties.h +572 -0
- data/assets/platform/include/SDL3/SDL_rect.h +528 -0
- data/assets/platform/include/SDL3/SDL_render.h +3028 -0
- data/assets/platform/include/SDL3/SDL_revision.h +59 -0
- data/assets/{include/SDL2 → platform/include/SDL3}/SDL_scancode.h +59 -68
- data/assets/platform/include/SDL3/SDL_sensor.h +321 -0
- data/assets/platform/include/SDL3/SDL_stdinc.h +6186 -0
- data/assets/platform/include/SDL3/SDL_storage.h +686 -0
- data/assets/platform/include/SDL3/SDL_surface.h +1769 -0
- data/assets/platform/include/SDL3/SDL_system.h +840 -0
- data/assets/{include/SDL2 → platform/include/SDL3}/SDL_test.h +16 -22
- data/assets/{include/SDL2 → platform/include/SDL3}/SDL_test_assert.h +27 -34
- data/assets/{include/SDL2 → platform/include/SDL3}/SDL_test_common.h +109 -52
- data/assets/{include/SDL2 → platform/include/SDL3}/SDL_test_compare.h +22 -14
- data/assets/{include/SDL2 → platform/include/SDL3}/SDL_test_crc32.h +27 -30
- data/assets/{include/SDL2 → platform/include/SDL3}/SDL_test_font.h +50 -49
- data/assets/{include/SDL2 → platform/include/SDL3}/SDL_test_fuzzer.h +64 -79
- data/assets/{include/SDL2 → platform/include/SDL3}/SDL_test_harness.h +47 -30
- data/assets/platform/include/SDL3/SDL_test_log.h +83 -0
- data/assets/{include/SDL2 → platform/include/SDL3}/SDL_test_md5.h +21 -28
- data/assets/{include/SDL2 → platform/include/SDL3}/SDL_test_memory.h +19 -16
- data/assets/platform/include/SDL3/SDL_thread.h +602 -0
- data/assets/platform/include/SDL3/SDL_time.h +249 -0
- data/assets/platform/include/SDL3/SDL_timer.h +454 -0
- data/assets/platform/include/SDL3/SDL_touch.h +184 -0
- data/assets/platform/include/SDL3/SDL_tray.h +544 -0
- data/assets/platform/include/SDL3/SDL_version.h +198 -0
- data/assets/platform/include/SDL3/SDL_video.h +3497 -0
- data/assets/platform/include/SDL3/SDL_vulkan.h +287 -0
- data/assets/platform/include/SDL3_image/SDL_image.h +3328 -0
- data/assets/platform/include/SDL3_mixer/SDL_mixer.h +3369 -0
- data/assets/platform/include/SDL3_ttf/SDL_textengine.h +181 -0
- data/assets/platform/include/SDL3_ttf/SDL_ttf.h +2833 -0
- data/assets/{include → platform/include}/mrbconf.h +42 -31
- data/assets/{include → platform/include}/mruby/array.h +3 -4
- data/assets/{include → platform/include}/mruby/boxing_nan.h +10 -0
- data/assets/platform/include/mruby/boxing_no.h +59 -0
- data/assets/{include → platform/include}/mruby/boxing_word.h +72 -26
- data/assets/platform/include/mruby/class.h +157 -0
- data/assets/{include → platform/include}/mruby/common.h +30 -1
- data/assets/{include → platform/include}/mruby/compile.h +52 -74
- data/assets/{include → platform/include}/mruby/data.h +2 -2
- data/assets/{include → platform/include}/mruby/debug.h +12 -5
- data/assets/{include → platform/include}/mruby/dump.h +7 -9
- data/assets/{include → platform/include}/mruby/endian.h +1 -1
- data/assets/platform/include/mruby/error.h +173 -0
- data/assets/platform/include/mruby/gc.h +79 -0
- data/assets/{include → platform/include}/mruby/hash.h +1 -7
- data/assets/platform/include/mruby/internal.h +282 -0
- data/assets/platform/include/mruby/irep.h +143 -0
- data/assets/{include → platform/include}/mruby/istruct.h +1 -1
- data/assets/platform/include/mruby/khash.h +455 -0
- data/assets/platform/include/mruby/mempool.h +19 -0
- data/assets/{include → platform/include}/mruby/numeric.h +7 -10
- data/assets/platform/include/mruby/object.h +51 -0
- data/assets/{include → platform/include}/mruby/opcode.h +5 -5
- data/assets/{include → platform/include}/mruby/ops.h +19 -6
- data/assets/platform/include/mruby/presym/id.h +1447 -0
- data/assets/{include → platform/include}/mruby/presym/scanning.h +10 -7
- data/assets/platform/include/mruby/presym/table.h +2893 -0
- data/assets/platform/include/mruby/presym.h +65 -0
- data/assets/platform/include/mruby/proc.h +184 -0
- data/assets/{include → platform/include}/mruby/range.h +0 -2
- data/assets/{include → platform/include}/mruby/string.h +33 -53
- data/assets/platform/include/mruby/throw.h +81 -0
- data/assets/{include → platform/include}/mruby/value.h +64 -29
- data/assets/{include → platform/include}/mruby/variable.h +6 -23
- data/assets/{include → platform/include}/mruby/version.h +7 -7
- data/assets/{include → platform/include}/mruby.h +238 -116
- data/assets/platform/macos-arm64/bin/mrbc +0 -0
- data/assets/platform/macos-arm64/lib/libSDL3.a +0 -0
- data/assets/platform/macos-arm64/lib/libSDL3_image.a +0 -0
- data/assets/platform/macos-arm64/lib/libSDL3_mixer.a +0 -0
- data/assets/platform/macos-arm64/lib/libSDL3_ttf.a +0 -0
- data/assets/platform/macos-arm64/lib/libmruby.a +0 -0
- data/assets/platform/wasm/lib/libFLAC.a +0 -0
- data/assets/platform/wasm/lib/libSDL3.a +0 -0
- data/assets/platform/wasm/lib/libSDL3_image.a +0 -0
- data/assets/platform/wasm/lib/libSDL3_mixer.a +0 -0
- data/assets/platform/wasm/lib/libSDL3_ttf.a +0 -0
- data/assets/platform/wasm/lib/libfreetype.a +0 -0
- data/assets/platform/wasm/lib/libharfbuzz.a +0 -0
- data/assets/platform/wasm/lib/libmruby.a +0 -0
- data/assets/platform/wasm/lib/libogg.a +0 -0
- data/assets/platform/wasm/lib/libplutosvg.a +0 -0
- data/assets/platform/wasm/lib/libplutovg.a +0 -0
- data/assets/platform/wasm/lib/libpng.a +0 -0
- data/assets/platform/wasm/lib/libpng16.a +0 -0
- data/assets/platform/wasm/lib/libvorbis.a +0 -0
- data/assets/platform/wasm/lib/libvorbisfile.a +0 -0
- data/assets/platform/wasm/lib/libzlibstatic.a +0 -0
- data/assets/platform/windows-arm64-mingw-ucrt/bin/mrbc.exe +0 -0
- data/assets/platform/windows-arm64-mingw-ucrt/lib/libSDL3.a +0 -0
- data/assets/platform/windows-arm64-mingw-ucrt/lib/libSDL3_image.a +0 -0
- data/assets/platform/windows-arm64-mingw-ucrt/lib/libSDL3_mixer.a +0 -0
- data/assets/platform/windows-arm64-mingw-ucrt/lib/libSDL3_ttf.a +0 -0
- data/assets/platform/windows-arm64-mingw-ucrt/lib/libmruby.a +0 -0
- data/assets/platform/windows-x86_64-mingw-ucrt/bin/mrbc.exe +0 -0
- data/assets/platform/windows-x86_64-mingw-ucrt/lib/libSDL3.a +0 -0
- data/assets/platform/windows-x86_64-mingw-ucrt/lib/libSDL3_image.a +0 -0
- data/assets/platform/windows-x86_64-mingw-ucrt/lib/libSDL3_mixer.a +0 -0
- data/assets/platform/windows-x86_64-mingw-ucrt/lib/libSDL3_ttf.a +0 -0
- data/assets/platform/windows-x86_64-mingw-ucrt/lib/libmruby.a +0 -0
- data/assets/resources/fonts/outfit/OFL.txt +93 -0
- data/assets/resources/fonts/outfit/outfit.ttf +0 -0
- data/assets/resources/fonts/roboto_mono/OFL.txt +91 -0
- data/assets/resources/fonts/roboto_mono/roboto_mono.ttf +0 -0
- data/assets/resources/icons/icon.icns +0 -0
- data/assets/resources/icons/icon.png +0 -0
- data/assets/resources/spritesheets/License.txt +28 -0
- data/assets/resources/spritesheets/spritesheet-backgrounds.png +0 -0
- data/assets/resources/spritesheets/spritesheet-backgrounds.xml +16 -0
- data/assets/resources/spritesheets/spritesheet-characters.png +0 -0
- data/assets/resources/spritesheets/spritesheet-characters.xml +47 -0
- data/assets/resources/spritesheets/spritesheet-enemies.png +0 -0
- data/assets/resources/spritesheets/spritesheet-enemies.xml +62 -0
- data/assets/resources/spritesheets/spritesheet-tiles.png +0 -0
- data/assets/resources/spritesheets/spritesheet-tiles.xml +316 -0
- data/assets/{wasm → resources/web}/template.html +1 -1
- data/assets/target.rb +131 -0
- data/bin/ruby2d +281 -82
- data/examples/astar.rb +269 -0
- data/examples/asteroids.rb +403 -0
- data/examples/bezier_editor.rb +96 -0
- data/examples/boids.rb +191 -0
- data/examples/bouncing_balls.rb +119 -0
- data/examples/breakout.rb +186 -0
- data/examples/constellations.rb +100 -0
- data/examples/dashboard.rb +133 -0
- data/examples/double_pendulum.rb +254 -0
- data/examples/falling_sand.rb +291 -0
- data/examples/fireworks.rb +110 -0
- data/examples/fluid_dynamics.rb +340 -0
- data/examples/fourier_epicycles.rb +142 -0
- data/examples/fractal_tree.rb +326 -0
- data/examples/game_of_life.rb +148 -0
- data/examples/gamepads.rb +333 -0
- data/examples/hill_driver.rb +613 -0
- data/examples/logo_animation.rb +251 -0
- data/examples/lorenz_attractor.rb +115 -0
- data/examples/mandelbrot.rb +207 -0
- data/examples/marching_squares.rb +200 -0
- data/examples/maze.rb +323 -0
- data/examples/nbody.rb +338 -0
- data/examples/outrun.rb +391 -0
- data/examples/pixel_paint.rb +313 -0
- data/examples/platformer.rb +335 -0
- data/examples/ray_casting_maze.rb +175 -0
- data/examples/shadow_casting.rb +151 -0
- data/examples/shape_recognizer.rb +908 -0
- data/examples/snake.rb +144 -0
- data/examples/sprite_sheets.rb +246 -0
- data/examples/swarm.rb +206 -0
- data/examples/tsp_annealing.rb +104 -0
- data/examples/ui_controls.rb +163 -0
- data/examples/wireframe_3d.rb +139 -0
- data/ext/ruby2d/audio.c +324 -0
- data/ext/ruby2d/canvas.c +1715 -494
- data/ext/ruby2d/ext.c +541 -0
- data/ext/ruby2d/extconf.rb +122 -145
- data/ext/ruby2d/font.c +566 -20
- data/ext/ruby2d/fps.c +92 -0
- data/ext/ruby2d/image.c +466 -41
- data/ext/ruby2d/ruby2d.c +374 -1890
- data/ext/ruby2d/ruby2d.h +840 -460
- data/ext/ruby2d/shapes.c +931 -101
- data/ext/ruby2d/text.c +586 -21
- data/ext/ruby2d/window.c +1693 -283
- data/lib/ruby2d/atlas_parser.rb +194 -0
- data/lib/ruby2d/audio.rb +73 -0
- data/lib/ruby2d/benchmark.rb +259 -0
- data/lib/ruby2d/bitmap_text.rb +155 -0
- data/lib/ruby2d/button.rb +402 -0
- data/lib/ruby2d/canvas.rb +703 -279
- data/lib/ruby2d/circle.rb +206 -29
- data/lib/ruby2d/cli/browser.rb +314 -0
- data/lib/ruby2d/cli/build.rb +471 -257
- data/lib/ruby2d/cli/colorize.rb +7 -5
- data/lib/ruby2d/cli/examples.rb +127 -0
- data/lib/ruby2d/cli/launch.rb +24 -39
- data/lib/ruby2d/cli/lib_files.rb +48 -0
- data/lib/ruby2d/cli/messages.rb +40 -0
- data/lib/ruby2d/cli/setup.rb +273 -0
- data/lib/ruby2d/cli/static_server.rb +160 -0
- data/lib/ruby2d/cli/usage.rb +255 -0
- data/lib/ruby2d/color.rb +191 -69
- data/lib/ruby2d/core.rb +37 -19
- data/lib/ruby2d/deps_help.rb +89 -0
- data/lib/ruby2d/dsl.rb +51 -12
- data/lib/ruby2d/ellipse.rb +228 -0
- data/lib/ruby2d/exceptions.rb +0 -2
- data/lib/ruby2d/font.rb +34 -74
- data/lib/ruby2d/gamepad.rb +323 -0
- data/lib/ruby2d/gem_paths.rb +19 -0
- data/lib/ruby2d/image.rb +155 -52
- data/lib/ruby2d/interactive.rb +103 -0
- data/lib/ruby2d/json_parser.rb +268 -0
- data/lib/ruby2d/line.rb +194 -85
- data/lib/ruby2d/mruby_compat.rb +12 -0
- data/lib/ruby2d/polygon.rb +297 -0
- data/lib/ruby2d/polyline.rb +368 -0
- data/lib/ruby2d/quad.rb +358 -94
- data/lib/ruby2d/rectangle.rb +77 -19
- data/lib/ruby2d/renderable.rb +411 -11
- data/lib/ruby2d/sprite.rb +471 -180
- data/lib/ruby2d/sprite_sheet.rb +64 -0
- data/lib/ruby2d/square.rb +29 -17
- data/lib/ruby2d/text.rb +196 -65
- data/lib/ruby2d/tileset.rb +139 -81
- data/lib/ruby2d/triangle.rb +324 -89
- data/lib/ruby2d/version.rb +1 -3
- data/lib/ruby2d/vertices.rb +62 -85
- data/lib/ruby2d/warnings.rb +29 -0
- data/lib/ruby2d/window/class_methods.rb +218 -0
- data/lib/ruby2d/window/gamepad_events.rb +141 -0
- data/lib/ruby2d/window/key_events.rb +74 -0
- data/lib/ruby2d/window/mouse_events.rb +171 -0
- data/lib/ruby2d/window/object_events.rb +168 -0
- data/lib/ruby2d/window.rb +632 -585
- data/lib/ruby2d.rb +14 -19
- metadata +275 -443
- data/assets/app.icns +0 -0
- data/assets/include/GL/glew.h +0 -26427
- data/assets/include/GLES2/gl2.h +0 -656
- data/assets/include/GLES2/gl2ext.h +0 -3949
- data/assets/include/GLES2/gl2ext_angle.h +0 -701
- data/assets/include/GLES2/gl2platform.h +0 -27
- data/assets/include/GLES3/gl3.h +0 -1192
- data/assets/include/GLES3/gl31.h +0 -1507
- data/assets/include/GLES3/gl32.h +0 -1808
- data/assets/include/GLES3/gl3platform.h +0 -27
- data/assets/include/KHR/khrplatform.h +0 -290
- data/assets/include/SDL2/SDL.h +0 -233
- data/assets/include/SDL2/SDL_assert.h +0 -326
- data/assets/include/SDL2/SDL_atomic.h +0 -415
- data/assets/include/SDL2/SDL_audio.h +0 -1500
- data/assets/include/SDL2/SDL_clipboard.h +0 -141
- data/assets/include/SDL2/SDL_config.h +0 -61
- data/assets/include/SDL2/SDL_config_android.h +0 -194
- data/assets/include/SDL2/SDL_config_emscripten.h +0 -218
- data/assets/include/SDL2/SDL_config_iphoneos.h +0 -217
- data/assets/include/SDL2/SDL_config_macosx.h +0 -277
- data/assets/include/SDL2/SDL_config_minimal.h +0 -95
- data/assets/include/SDL2/SDL_config_ngage.h +0 -89
- data/assets/include/SDL2/SDL_config_os2.h +0 -207
- data/assets/include/SDL2/SDL_config_pandora.h +0 -141
- data/assets/include/SDL2/SDL_config_windows.h +0 -331
- data/assets/include/SDL2/SDL_config_wingdk.h +0 -253
- data/assets/include/SDL2/SDL_config_winrt.h +0 -220
- data/assets/include/SDL2/SDL_config_xbox.h +0 -235
- data/assets/include/SDL2/SDL_cpuinfo.h +0 -594
- data/assets/include/SDL2/SDL_endian.h +0 -348
- data/assets/include/SDL2/SDL_error.h +0 -163
- data/assets/include/SDL2/SDL_events.h +0 -1166
- data/assets/include/SDL2/SDL_filesystem.h +0 -149
- data/assets/include/SDL2/SDL_gamecontroller.h +0 -1074
- data/assets/include/SDL2/SDL_gesture.h +0 -117
- data/assets/include/SDL2/SDL_guid.h +0 -100
- data/assets/include/SDL2/SDL_haptic.h +0 -1341
- data/assets/include/SDL2/SDL_hidapi.h +0 -451
- data/assets/include/SDL2/SDL_hints.h +0 -2569
- data/assets/include/SDL2/SDL_image.h +0 -2173
- data/assets/include/SDL2/SDL_joystick.h +0 -1066
- data/assets/include/SDL2/SDL_keyboard.h +0 -353
- data/assets/include/SDL2/SDL_keycode.h +0 -358
- data/assets/include/SDL2/SDL_loadso.h +0 -115
- data/assets/include/SDL2/SDL_log.h +0 -404
- data/assets/include/SDL2/SDL_main.h +0 -275
- data/assets/include/SDL2/SDL_mixer.h +0 -2784
- data/assets/include/SDL2/SDL_mouse.h +0 -465
- data/assets/include/SDL2/SDL_mutex.h +0 -471
- data/assets/include/SDL2/SDL_name.h +0 -33
- data/assets/include/SDL2/SDL_pixels.h +0 -644
- data/assets/include/SDL2/SDL_platform.h +0 -261
- data/assets/include/SDL2/SDL_quit.h +0 -58
- data/assets/include/SDL2/SDL_rect.h +0 -376
- data/assets/include/SDL2/SDL_render.h +0 -1919
- data/assets/include/SDL2/SDL_revision.h +0 -6
- data/assets/include/SDL2/SDL_rwops.h +0 -841
- data/assets/include/SDL2/SDL_sensor.h +0 -322
- data/assets/include/SDL2/SDL_shape.h +0 -155
- data/assets/include/SDL2/SDL_stdinc.h +0 -830
- data/assets/include/SDL2/SDL_surface.h +0 -997
- data/assets/include/SDL2/SDL_system.h +0 -623
- data/assets/include/SDL2/SDL_syswm.h +0 -386
- data/assets/include/SDL2/SDL_test_images.h +0 -78
- data/assets/include/SDL2/SDL_test_random.h +0 -115
- data/assets/include/SDL2/SDL_thread.h +0 -464
- data/assets/include/SDL2/SDL_timer.h +0 -222
- data/assets/include/SDL2/SDL_touch.h +0 -150
- data/assets/include/SDL2/SDL_ttf.h +0 -2316
- data/assets/include/SDL2/SDL_types.h +0 -29
- data/assets/include/SDL2/SDL_version.h +0 -204
- data/assets/include/SDL2/SDL_video.h +0 -2150
- data/assets/include/SDL2/SDL_vulkan.h +0 -215
- data/assets/include/SDL2/begin_code.h +0 -187
- data/assets/include/mruby/boxing_no.h +0 -58
- data/assets/include/mruby/class.h +0 -108
- data/assets/include/mruby/error.h +0 -147
- data/assets/include/mruby/gc.h +0 -94
- data/assets/include/mruby/irep.h +0 -147
- data/assets/include/mruby/khash.h +0 -284
- data/assets/include/mruby/object.h +0 -43
- data/assets/include/mruby/presym/disable.h +0 -70
- data/assets/include/mruby/presym/enable.h +0 -37
- data/assets/include/mruby/presym.h +0 -40
- data/assets/include/mruby/proc.h +0 -211
- data/assets/include/mruby/throw.h +0 -66
- data/assets/macos/universal/bin/mrbc +0 -0
- data/assets/macos/universal/lib/libFLAC.a +0 -0
- data/assets/macos/universal/lib/libSDL2.a +0 -0
- data/assets/macos/universal/lib/libSDL2_image.a +0 -0
- data/assets/macos/universal/lib/libSDL2_mixer.a +0 -0
- data/assets/macos/universal/lib/libSDL2_ttf.a +0 -0
- data/assets/macos/universal/lib/libavif.a +0 -0
- data/assets/macos/universal/lib/libbrotlicommon-static.a +0 -0
- data/assets/macos/universal/lib/libbrotlidec-static.a +0 -0
- data/assets/macos/universal/lib/libfreetype.a +0 -0
- data/assets/macos/universal/lib/libgraphite2.a +0 -0
- data/assets/macos/universal/lib/libharfbuzz.a +0 -0
- data/assets/macos/universal/lib/libhwy.a +0 -0
- data/assets/macos/universal/lib/libjpeg.a +0 -0
- data/assets/macos/universal/lib/libjxl.a +0 -0
- data/assets/macos/universal/lib/libmodplug.a +0 -0
- data/assets/macos/universal/lib/libmpg123.a +0 -0
- data/assets/macos/universal/lib/libmruby.a +0 -0
- data/assets/macos/universal/lib/libogg.a +0 -0
- data/assets/macos/universal/lib/libpng.a +0 -0
- data/assets/macos/universal/lib/libtiff.a +0 -0
- data/assets/macos/universal/lib/libvorbis.a +0 -0
- data/assets/macos/universal/lib/libvorbisfile.a +0 -0
- data/assets/macos/universal/lib/libwebp.a +0 -0
- data/assets/macos/universal/lib/libzstd.a +0 -0
- data/assets/test_media/README.md +0 -3
- data/assets/test_media/bitstream_vera/COPYRIGHT.txt +0 -124
- data/assets/test_media/bitstream_vera/vera.ttf +0 -0
- data/assets/test_media/boom.png +0 -0
- data/assets/test_media/coin.png +0 -0
- data/assets/test_media/colors.png +0 -0
- data/assets/test_media/controller.png +0 -0
- data/assets/test_media/dial.wav +0 -0
- data/assets/test_media/hero.png +0 -0
- data/assets/test_media/image.bmp +0 -0
- data/assets/test_media/image.jpg +0 -0
- data/assets/test_media/image.png +0 -0
- data/assets/test_media/music.flac +0 -0
- data/assets/test_media/music.mp3 +0 -0
- data/assets/test_media/music.ogg +0 -0
- data/assets/test_media/music.wav +0 -0
- data/assets/test_media/originals/boom.pxm +0 -0
- data/assets/test_media/originals/coin.pxm +0 -0
- data/assets/test_media/originals/controller.sketch +0 -0
- data/assets/test_media/originals/hero.pxm +0 -0
- data/assets/test_media/originals/image.pxm +0 -0
- data/assets/test_media/originals/music.caf +0 -0
- data/assets/test_media/originals/texture_atlas.pxm +0 -0
- data/assets/test_media/rondo_alla_turka.ogg +0 -0
- data/assets/test_media/sound.flac +0 -0
- data/assets/test_media/sound.mp3 +0 -0
- data/assets/test_media/sound.ogg +0 -0
- data/assets/test_media/sound.wav +0 -0
- data/assets/test_media/sprite_sheet.png +0 -0
- data/assets/test_media/texture_atlas.png +0 -0
- data/assets/wasm/libmruby.a +0 -0
- data/assets/windows/mingw-w64-ucrt-x86_64/bin/mrbc.exe +0 -0
- data/assets/windows/mingw-w64-ucrt-x86_64/lib/libFLAC.a +0 -0
- data/assets/windows/mingw-w64-ucrt-x86_64/lib/libLerc.a +0 -0
- data/assets/windows/mingw-w64-ucrt-x86_64/lib/libSDL2.a +0 -0
- data/assets/windows/mingw-w64-ucrt-x86_64/lib/libSDL2_image.a +0 -0
- data/assets/windows/mingw-w64-ucrt-x86_64/lib/libSDL2_mixer.a +0 -0
- data/assets/windows/mingw-w64-ucrt-x86_64/lib/libSDL2_ttf.a +0 -0
- data/assets/windows/mingw-w64-ucrt-x86_64/lib/libbrotlicommon.a +0 -0
- data/assets/windows/mingw-w64-ucrt-x86_64/lib/libbrotlidec.a +0 -0
- data/assets/windows/mingw-w64-ucrt-x86_64/lib/libbz2.a +0 -0
- data/assets/windows/mingw-w64-ucrt-x86_64/lib/libdeflate.a +0 -0
- data/assets/windows/mingw-w64-ucrt-x86_64/lib/libfreetype.a +0 -0
- data/assets/windows/mingw-w64-ucrt-x86_64/lib/libglew32.a +0 -0
- data/assets/windows/mingw-w64-ucrt-x86_64/lib/libgraphite2.a +0 -0
- data/assets/windows/mingw-w64-ucrt-x86_64/lib/libharfbuzz.a +0 -0
- data/assets/windows/mingw-w64-ucrt-x86_64/lib/libhwy.a +0 -0
- data/assets/windows/mingw-w64-ucrt-x86_64/lib/libjbig.a +0 -0
- data/assets/windows/mingw-w64-ucrt-x86_64/lib/libjpeg.a +0 -0
- data/assets/windows/mingw-w64-ucrt-x86_64/lib/libjxl.a +0 -0
- data/assets/windows/mingw-w64-ucrt-x86_64/lib/liblzma.a +0 -0
- data/assets/windows/mingw-w64-ucrt-x86_64/lib/libmodplug.a +0 -0
- data/assets/windows/mingw-w64-ucrt-x86_64/lib/libmpg123.a +0 -0
- data/assets/windows/mingw-w64-ucrt-x86_64/lib/libmruby.a +0 -0
- data/assets/windows/mingw-w64-ucrt-x86_64/lib/libogg.a +0 -0
- data/assets/windows/mingw-w64-ucrt-x86_64/lib/libopus.a +0 -0
- data/assets/windows/mingw-w64-ucrt-x86_64/lib/libopusfile.a +0 -0
- data/assets/windows/mingw-w64-ucrt-x86_64/lib/libpng.a +0 -0
- data/assets/windows/mingw-w64-ucrt-x86_64/lib/libsndfile.a +0 -0
- data/assets/windows/mingw-w64-ucrt-x86_64/lib/libssp.a +0 -1
- data/assets/windows/mingw-w64-ucrt-x86_64/lib/libstdc++.a +0 -0
- data/assets/windows/mingw-w64-ucrt-x86_64/lib/libtiff.a +0 -0
- data/assets/windows/mingw-w64-ucrt-x86_64/lib/libvorbis.a +0 -0
- data/assets/windows/mingw-w64-ucrt-x86_64/lib/libvorbisfile.a +0 -0
- data/assets/windows/mingw-w64-ucrt-x86_64/lib/libwebp.a +0 -0
- data/assets/windows/mingw-w64-ucrt-x86_64/lib/libz.a +0 -0
- data/assets/windows/mingw-w64-ucrt-x86_64/lib/libzstd.a +0 -0
- data/assets/windows/mingw-w64-x86_64/bin/mrbc.exe +0 -0
- data/assets/windows/mingw-w64-x86_64/lib/libFLAC.a +0 -0
- data/assets/windows/mingw-w64-x86_64/lib/libLerc.a +0 -0
- data/assets/windows/mingw-w64-x86_64/lib/libSDL2.a +0 -0
- data/assets/windows/mingw-w64-x86_64/lib/libSDL2_image.a +0 -0
- data/assets/windows/mingw-w64-x86_64/lib/libSDL2_mixer.a +0 -0
- data/assets/windows/mingw-w64-x86_64/lib/libSDL2_ttf.a +0 -0
- data/assets/windows/mingw-w64-x86_64/lib/libbrotlicommon.a +0 -0
- data/assets/windows/mingw-w64-x86_64/lib/libbrotlidec.a +0 -0
- data/assets/windows/mingw-w64-x86_64/lib/libbz2.a +0 -0
- data/assets/windows/mingw-w64-x86_64/lib/libdeflate.a +0 -0
- data/assets/windows/mingw-w64-x86_64/lib/libfreetype.a +0 -0
- data/assets/windows/mingw-w64-x86_64/lib/libglew32.a +0 -0
- data/assets/windows/mingw-w64-x86_64/lib/libgraphite2.a +0 -0
- data/assets/windows/mingw-w64-x86_64/lib/libharfbuzz.a +0 -0
- data/assets/windows/mingw-w64-x86_64/lib/libhwy.a +0 -0
- data/assets/windows/mingw-w64-x86_64/lib/libjbig.a +0 -0
- data/assets/windows/mingw-w64-x86_64/lib/libjpeg.a +0 -0
- data/assets/windows/mingw-w64-x86_64/lib/libjxl.a +0 -0
- data/assets/windows/mingw-w64-x86_64/lib/liblzma.a +0 -0
- data/assets/windows/mingw-w64-x86_64/lib/libmodplug.a +0 -0
- data/assets/windows/mingw-w64-x86_64/lib/libmpg123.a +0 -0
- data/assets/windows/mingw-w64-x86_64/lib/libmruby.a +0 -0
- data/assets/windows/mingw-w64-x86_64/lib/libogg.a +0 -0
- data/assets/windows/mingw-w64-x86_64/lib/libopus.a +0 -0
- data/assets/windows/mingw-w64-x86_64/lib/libopusfile.a +0 -0
- data/assets/windows/mingw-w64-x86_64/lib/libpng.a +0 -0
- data/assets/windows/mingw-w64-x86_64/lib/libsndfile.a +0 -0
- data/assets/windows/mingw-w64-x86_64/lib/libssp.a +0 -1
- data/assets/windows/mingw-w64-x86_64/lib/libstdc++.a +0 -0
- data/assets/windows/mingw-w64-x86_64/lib/libtiff.a +0 -0
- data/assets/windows/mingw-w64-x86_64/lib/libvorbis.a +0 -0
- data/assets/windows/mingw-w64-x86_64/lib/libvorbisfile.a +0 -0
- data/assets/windows/mingw-w64-x86_64/lib/libwebp.a +0 -0
- data/assets/windows/mingw-w64-x86_64/lib/libz.a +0 -0
- data/assets/windows/mingw-w64-x86_64/lib/libzstd.a +0 -0
- data/assets/xcode/ios/Assets.xcassets/AppIcon.appiconset/Contents.json +0 -98
- data/assets/xcode/ios/Assets.xcassets/Contents.json +0 -6
- data/assets/xcode/ios/Base.lproj/LaunchScreen.storyboard +0 -27
- data/assets/xcode/ios/Info.plist +0 -43
- data/assets/xcode/ios/MRuby.framework/Headers/mrbconf.h +0 -143
- data/assets/xcode/ios/MRuby.framework/Headers/mruby/array.h +0 -280
- data/assets/xcode/ios/MRuby.framework/Headers/mruby/boxing_nan.h +0 -102
- data/assets/xcode/ios/MRuby.framework/Headers/mruby/boxing_no.h +0 -56
- data/assets/xcode/ios/MRuby.framework/Headers/mruby/boxing_word.h +0 -144
- data/assets/xcode/ios/MRuby.framework/Headers/mruby/class.h +0 -97
- data/assets/xcode/ios/MRuby.framework/Headers/mruby/common.h +0 -77
- data/assets/xcode/ios/MRuby.framework/Headers/mruby/compile.h +0 -195
- data/assets/xcode/ios/MRuby.framework/Headers/mruby/data.h +0 -75
- data/assets/xcode/ios/MRuby.framework/Headers/mruby/debug.h +0 -67
- data/assets/xcode/ios/MRuby.framework/Headers/mruby/dump.h +0 -196
- data/assets/xcode/ios/MRuby.framework/Headers/mruby/error.h +0 -75
- data/assets/xcode/ios/MRuby.framework/Headers/mruby/gc.h +0 -91
- data/assets/xcode/ios/MRuby.framework/Headers/mruby/hash.h +0 -215
- data/assets/xcode/ios/MRuby.framework/Headers/mruby/irep.h +0 -75
- data/assets/xcode/ios/MRuby.framework/Headers/mruby/istruct.h +0 -47
- data/assets/xcode/ios/MRuby.framework/Headers/mruby/khash.h +0 -274
- data/assets/xcode/ios/MRuby.framework/Headers/mruby/numeric.h +0 -161
- data/assets/xcode/ios/MRuby.framework/Headers/mruby/object.h +0 -44
- data/assets/xcode/ios/MRuby.framework/Headers/mruby/opcode.h +0 -69
- data/assets/xcode/ios/MRuby.framework/Headers/mruby/ops.h +0 -117
- data/assets/xcode/ios/MRuby.framework/Headers/mruby/proc.h +0 -131
- data/assets/xcode/ios/MRuby.framework/Headers/mruby/range.h +0 -49
- data/assets/xcode/ios/MRuby.framework/Headers/mruby/re.h +0 -16
- data/assets/xcode/ios/MRuby.framework/Headers/mruby/string.h +0 -444
- data/assets/xcode/ios/MRuby.framework/Headers/mruby/throw.h +0 -55
- data/assets/xcode/ios/MRuby.framework/Headers/mruby/value.h +0 -309
- data/assets/xcode/ios/MRuby.framework/Headers/mruby/variable.h +0 -136
- data/assets/xcode/ios/MRuby.framework/Headers/mruby/version.h +0 -110
- data/assets/xcode/ios/MRuby.framework/Headers/mruby.h +0 -1282
- data/assets/xcode/ios/MRuby.framework/Info.plist +0 -20
- data/assets/xcode/ios/MRuby.framework/MRuby +0 -0
- data/assets/xcode/ios/MyApp.xcodeproj/project.pbxproj +0 -368
- data/assets/xcode/ios/MyApp.xcodeproj/project.xcworkspace/contents.xcworkspacedata +0 -7
- data/assets/xcode/ios/MyApp.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +0 -8
- data/assets/xcode/ios/MyApp.xcodeproj/project.xcworkspace/xcshareddata/MyApp.xcscmblueprint +0 -30
- data/assets/xcode/ios/main.c +0 -1
- data/assets/xcode/tvos/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Large.imagestack/Back.imagestacklayer/Content.imageset/Contents.json +0 -16
- data/assets/xcode/tvos/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Large.imagestack/Back.imagestacklayer/Contents.json +0 -6
- data/assets/xcode/tvos/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Large.imagestack/Contents.json +0 -17
- data/assets/xcode/tvos/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Large.imagestack/Front.imagestacklayer/Content.imageset/Contents.json +0 -16
- data/assets/xcode/tvos/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Large.imagestack/Front.imagestacklayer/Contents.json +0 -6
- data/assets/xcode/tvos/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Large.imagestack/Middle.imagestacklayer/Content.imageset/Contents.json +0 -16
- data/assets/xcode/tvos/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Large.imagestack/Middle.imagestacklayer/Contents.json +0 -6
- data/assets/xcode/tvos/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Small.imagestack/Back.imagestacklayer/Content.imageset/Contents.json +0 -16
- data/assets/xcode/tvos/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Small.imagestack/Back.imagestacklayer/Contents.json +0 -6
- data/assets/xcode/tvos/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Small.imagestack/Contents.json +0 -17
- data/assets/xcode/tvos/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Small.imagestack/Front.imagestacklayer/Content.imageset/Contents.json +0 -16
- data/assets/xcode/tvos/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Small.imagestack/Front.imagestacklayer/Contents.json +0 -6
- data/assets/xcode/tvos/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Small.imagestack/Middle.imagestacklayer/Content.imageset/Contents.json +0 -16
- data/assets/xcode/tvos/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Small.imagestack/Middle.imagestacklayer/Contents.json +0 -6
- data/assets/xcode/tvos/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Contents.json +0 -32
- data/assets/xcode/tvos/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image Wide.imageset/Contents.json +0 -16
- data/assets/xcode/tvos/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image.imageset/Contents.json +0 -16
- data/assets/xcode/tvos/Assets.xcassets/Contents.json +0 -6
- data/assets/xcode/tvos/Assets.xcassets/LaunchImage.launchimage/Contents.json +0 -22
- data/assets/xcode/tvos/Info.plist +0 -30
- data/assets/xcode/tvos/MRuby.framework/Headers/mrbconf.h +0 -143
- data/assets/xcode/tvos/MRuby.framework/Headers/mruby/array.h +0 -280
- data/assets/xcode/tvos/MRuby.framework/Headers/mruby/boxing_nan.h +0 -102
- data/assets/xcode/tvos/MRuby.framework/Headers/mruby/boxing_no.h +0 -56
- data/assets/xcode/tvos/MRuby.framework/Headers/mruby/boxing_word.h +0 -144
- data/assets/xcode/tvos/MRuby.framework/Headers/mruby/class.h +0 -97
- data/assets/xcode/tvos/MRuby.framework/Headers/mruby/common.h +0 -77
- data/assets/xcode/tvos/MRuby.framework/Headers/mruby/compile.h +0 -195
- data/assets/xcode/tvos/MRuby.framework/Headers/mruby/data.h +0 -75
- data/assets/xcode/tvos/MRuby.framework/Headers/mruby/debug.h +0 -67
- data/assets/xcode/tvos/MRuby.framework/Headers/mruby/dump.h +0 -196
- data/assets/xcode/tvos/MRuby.framework/Headers/mruby/error.h +0 -75
- data/assets/xcode/tvos/MRuby.framework/Headers/mruby/gc.h +0 -91
- data/assets/xcode/tvos/MRuby.framework/Headers/mruby/hash.h +0 -215
- data/assets/xcode/tvos/MRuby.framework/Headers/mruby/irep.h +0 -75
- data/assets/xcode/tvos/MRuby.framework/Headers/mruby/istruct.h +0 -47
- data/assets/xcode/tvos/MRuby.framework/Headers/mruby/khash.h +0 -274
- data/assets/xcode/tvos/MRuby.framework/Headers/mruby/numeric.h +0 -161
- data/assets/xcode/tvos/MRuby.framework/Headers/mruby/object.h +0 -44
- data/assets/xcode/tvos/MRuby.framework/Headers/mruby/opcode.h +0 -69
- data/assets/xcode/tvos/MRuby.framework/Headers/mruby/ops.h +0 -117
- data/assets/xcode/tvos/MRuby.framework/Headers/mruby/proc.h +0 -131
- data/assets/xcode/tvos/MRuby.framework/Headers/mruby/range.h +0 -49
- data/assets/xcode/tvos/MRuby.framework/Headers/mruby/re.h +0 -16
- data/assets/xcode/tvos/MRuby.framework/Headers/mruby/string.h +0 -444
- data/assets/xcode/tvos/MRuby.framework/Headers/mruby/throw.h +0 -55
- data/assets/xcode/tvos/MRuby.framework/Headers/mruby/value.h +0 -309
- data/assets/xcode/tvos/MRuby.framework/Headers/mruby/variable.h +0 -136
- data/assets/xcode/tvos/MRuby.framework/Headers/mruby/version.h +0 -110
- data/assets/xcode/tvos/MRuby.framework/Headers/mruby.h +0 -1282
- data/assets/xcode/tvos/MRuby.framework/Info.plist +0 -20
- data/assets/xcode/tvos/MRuby.framework/MRuby +0 -0
- data/assets/xcode/tvos/MyApp.xcodeproj/project.pbxproj +0 -350
- data/assets/xcode/tvos/MyApp.xcodeproj/project.xcworkspace/contents.xcworkspacedata +0 -7
- data/assets/xcode/tvos/MyApp.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +0 -8
- data/assets/xcode/tvos/MyApp.xcodeproj/project.xcworkspace/xcshareddata/MyApp.xcscmblueprint +0 -30
- data/assets/xcode/tvos/main.c +0 -1
- data/ext/ruby2d/common.c +0 -185
- data/ext/ruby2d/controllers.c +0 -110
- data/ext/ruby2d/gl.c +0 -398
- data/ext/ruby2d/gl2.c +0 -86
- data/ext/ruby2d/gl3.c +0 -304
- data/ext/ruby2d/gles.c +0 -302
- data/ext/ruby2d/input.c +0 -48
- data/ext/ruby2d/music.c +0 -127
- data/ext/ruby2d/sound.c +0 -132
- data/lib/ruby2d/cli/console.rb +0 -65
- data/lib/ruby2d/cli/enable_console.rb +0 -7
- data/lib/ruby2d/cli/platform.rb +0 -17
- data/lib/ruby2d/music.rb +0 -77
- data/lib/ruby2d/pixel.rb +0 -18
- data/lib/ruby2d/pixmap.rb +0 -39
- data/lib/ruby2d/pixmap_atlas.rb +0 -56
- data/lib/ruby2d/sound.rb +0 -61
- data/lib/ruby2d/texture.rb +0 -38
- /data/assets/{include/SDL2 → platform/include/SDL3}/SDL_opengles2_gl2.h +0 -0
- /data/assets/{include/SDL2 → platform/include/SDL3}/SDL_opengles2_gl2ext.h +0 -0
- /data/assets/{include/SDL2 → platform/include/SDL3}/SDL_opengles2_gl2platform.h +0 -0
- /data/assets/{include/SDL2 → platform/include/SDL3}/SDL_opengles2_khrplatform.h +0 -0
- /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
|
+
```
|