denko 0.14.0 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (344) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/build_atmega_avr.yml +2 -1
  3. data/.github/workflows/build_atmega_megaavr.yml +2 -1
  4. data/.github/workflows/build_atsam3x.yml +1 -0
  5. data/.github/workflows/build_atsamd21.yml +2 -1
  6. data/.github/workflows/build_esp32.yml +4 -2
  7. data/.github/workflows/build_esp32c3.yml +4 -3
  8. data/.github/workflows/build_esp32c6.yml +4 -2
  9. data/.github/workflows/build_esp32h2.yml +4 -2
  10. data/.github/workflows/build_esp32s2.yml +4 -2
  11. data/.github/workflows/build_esp32s3.yml +4 -2
  12. data/.github/workflows/build_esp8266.yml +2 -1
  13. data/.github/workflows/build_ra4m1.yml +1 -0
  14. data/.github/workflows/build_rp2040.yml +4 -3
  15. data/.github/workflows/ruby.yml +1 -1
  16. data/CHANGELOG.md +203 -0
  17. data/DEPS_CLI.md +16 -16
  18. data/DEPS_IDE.md +31 -30
  19. data/MICROCONTROLLERS.md +103 -0
  20. data/PERIPHERALS.md +178 -0
  21. data/README.md +28 -21
  22. data/denko.gemspec +6 -1
  23. data/lib/denko/analog_io/ads1118.rb +5 -5
  24. data/lib/denko/analog_io/ads111x.rb +23 -19
  25. data/lib/denko/analog_io/joystick.rb +87 -0
  26. data/lib/denko/analog_io/potentiometer.rb +1 -5
  27. data/lib/denko/analog_io.rb +22 -8
  28. data/lib/denko/behaviors/bus_controller.rb +2 -1
  29. data/lib/denko/behaviors/bus_peripheral.rb +1 -1
  30. data/lib/denko/behaviors/callbacks.rb +18 -16
  31. data/lib/denko/behaviors/component.rb +0 -4
  32. data/lib/denko/behaviors/lifecycle.rb +1 -1
  33. data/lib/denko/behaviors/listener.rb +9 -3
  34. data/lib/denko/behaviors/multi_pin.rb +4 -6
  35. data/lib/denko/behaviors/poller.rb +11 -2
  36. data/lib/denko/behaviors/reader.rb +109 -21
  37. data/lib/denko/behaviors/single_pin.rb +2 -4
  38. data/lib/denko/behaviors/state.rb +18 -13
  39. data/lib/denko/behaviors/threaded.rb +19 -8
  40. data/lib/denko/behaviors.rb +36 -23
  41. data/lib/denko/board/eeprom.rb +1 -1
  42. data/lib/denko/board/i2c.rb +1 -1
  43. data/lib/denko/board/i2c_bit_bang.rb +9 -5
  44. data/lib/denko/board/map.rb +6 -2
  45. data/lib/denko/board/one_wire.rb +3 -3
  46. data/lib/denko/board/spi.rb +30 -30
  47. data/lib/denko/board/spi_bit_bang.rb +8 -11
  48. data/lib/denko/board.rb +6 -3
  49. data/lib/denko/connection/flow_control.rb +1 -1
  50. data/lib/denko/connection/serial.rb +5 -5
  51. data/lib/denko/digital_io/output.rb +12 -4
  52. data/lib/denko/digital_io/pcf8574.rb +114 -0
  53. data/lib/denko/digital_io/rotary_encoder.rb +10 -6
  54. data/lib/denko/digital_io.rb +24 -6
  55. data/lib/denko/display/canvas.rb +350 -157
  56. data/lib/denko/display/font/bmp_5x7.rb +142 -0
  57. data/lib/denko/display/font/bmp_6x8.rb +142 -0
  58. data/lib/denko/display/font/bmp_8x16.rb +141 -0
  59. data/lib/denko/display/font.rb +22 -0
  60. data/lib/denko/display/hd44780.rb +24 -20
  61. data/lib/denko/display/il0373.rb +186 -0
  62. data/lib/denko/display/mono_oled.rb +193 -0
  63. data/lib/denko/display/pcd8544.rb +154 -0
  64. data/lib/denko/display/pixel_common.rb +83 -0
  65. data/lib/denko/display/sh1106.rb +17 -21
  66. data/lib/denko/display/sh1107.rb +10 -0
  67. data/lib/denko/display/spi_common.rb +35 -0
  68. data/lib/denko/display/spi_epaper_common.rb +30 -0
  69. data/lib/denko/display/ssd1306.rb +6 -228
  70. data/lib/denko/display/ssd1680.rb +14 -0
  71. data/lib/denko/display/ssd1681.rb +8 -0
  72. data/lib/denko/display/ssd168x.rb +227 -0
  73. data/lib/denko/display/st7302.rb +207 -0
  74. data/lib/denko/display/st7565.rb +166 -0
  75. data/lib/denko/display.rb +40 -4
  76. data/lib/denko/eeprom/at24c.rb +67 -0
  77. data/lib/denko/eeprom/board.rb +69 -0
  78. data/lib/denko/eeprom.rb +15 -1
  79. data/lib/denko/helpers/engine_check.rb +13 -0
  80. data/lib/denko/{mutex_stub.rb → helpers/mutex_stub.rb} +6 -0
  81. data/lib/denko/helpers.rb +6 -0
  82. data/lib/denko/i2c/bit_bang.rb +1 -0
  83. data/lib/denko/i2c/bus_common.rb +9 -4
  84. data/lib/denko/i2c/peripheral.rb +5 -1
  85. data/lib/denko/i2c.rb +17 -4
  86. data/lib/denko/led/apa102.rb +1 -3
  87. data/lib/denko/led/base.rb +5 -0
  88. data/lib/denko/led/rgb.rb +16 -10
  89. data/lib/denko/led/seven_segment.rb +1 -1
  90. data/lib/denko/led.rb +17 -8
  91. data/lib/denko/motor/{stepper.rb → a3967.rb} +1 -1
  92. data/lib/denko/motor/servo.rb +16 -6
  93. data/lib/denko/motor.rb +16 -3
  94. data/lib/denko/one_wire/bus.rb +20 -16
  95. data/lib/denko/one_wire/bus_enumerator.rb +25 -14
  96. data/lib/denko/one_wire/helper.rb +4 -2
  97. data/lib/denko/one_wire.rb +18 -5
  98. data/lib/denko/pulse_io/buzzer.rb +2 -6
  99. data/lib/denko/pulse_io/ir_output.rb +1 -5
  100. data/lib/denko/pulse_io/pwm_output.rb +56 -31
  101. data/lib/denko/pulse_io.rb +17 -3
  102. data/lib/denko/rtc/ds3231.rb +4 -3
  103. data/lib/denko/rtc.rb +14 -1
  104. data/lib/denko/sensor/aht.rb +16 -20
  105. data/lib/denko/sensor/bme280.rb +23 -36
  106. data/lib/denko/sensor/bmp180.rb +8 -13
  107. data/lib/denko/sensor/dht.rb +17 -7
  108. data/lib/denko/sensor/ds18b20.rb +5 -4
  109. data/lib/denko/sensor/hdc1080.rb +174 -0
  110. data/lib/denko/sensor/htu21d.rb +17 -6
  111. data/lib/denko/sensor/htu31d.rb +6 -5
  112. data/lib/denko/sensor/jsnsr04t.rb +49 -0
  113. data/lib/denko/sensor/qmp6988.rb +14 -30
  114. data/lib/denko/sensor/rcwl9620.rb +1 -0
  115. data/lib/denko/sensor/sht3x.rb +6 -5
  116. data/lib/denko/sensor/sht4x.rb +125 -0
  117. data/lib/denko/sensor/vl53l0x.rb +58 -0
  118. data/lib/denko/sensor.rb +33 -15
  119. data/lib/denko/spi/base_register.rb +11 -7
  120. data/lib/denko/spi/bus_common.rb +12 -15
  121. data/lib/denko/spi/input_register.rb +6 -6
  122. data/lib/denko/spi/output_register.rb +13 -4
  123. data/lib/denko/spi/peripheral.rb +82 -84
  124. data/lib/denko/spi.rb +20 -10
  125. data/lib/denko/uart/bit_bang.rb +2 -27
  126. data/lib/denko/uart/common.rb +33 -0
  127. data/lib/denko/uart/hardware.rb +1 -26
  128. data/lib/denko/uart.rb +16 -2
  129. data/lib/denko/version.rb +1 -1
  130. data/lib/denko.rb +22 -25
  131. data/lib/denko_cli/targets.rb +7 -7
  132. data/lib/denko_cli/targets.txt +19 -20
  133. data/src/lib/Denko.cpp +26 -13
  134. data/src/lib/Denko.h +4 -4
  135. data/src/lib/DenkoDefines.h +7 -26
  136. data/src/lib/DenkoLEDArray.cpp +1 -8
  137. data/src/lib/DenkoSPI.cpp +31 -29
  138. data/src/lib/DenkoSPIBB.cpp +12 -14
  139. data/test/analog_io/input_test.rb +1 -1
  140. data/test/analog_io/potentiometer_test.rb +2 -2
  141. data/test/behaviors/bus_peripheral_test.rb +4 -4
  142. data/test/behaviors/callbacks_test.rb +20 -10
  143. data/test/behaviors/component_test.rb +18 -8
  144. data/test/board/board_test.rb +9 -9
  145. data/test/board/one_wire_test.rb +25 -14
  146. data/test/board/spi_test.rb +31 -15
  147. data/test/digital_io/input_test.rb +2 -2
  148. data/test/display/canvas_test.rb +306 -0
  149. data/test/display/hd44780_test.rb +34 -7
  150. data/test/eeprom/board_test.rb +45 -0
  151. data/test/helpers/mruby_minitest.rb +95 -0
  152. data/test/helpers/mruby_runner.rb +13 -0
  153. data/test/i2c/bus_test.rb +93 -30
  154. data/test/i2c/peripheral_test.rb +2 -2
  155. data/test/led/apa102_test.rb +24 -0
  156. data/test/led/rgb_test.rb +4 -4
  157. data/test/motor/{stepper_test.rb → a3967_test.rb} +2 -2
  158. data/test/one_wire/bus_enumerator_test.rb +1 -1
  159. data/test/one_wire/bus_test.rb +42 -35
  160. data/test/one_wire/peripheral_test.rb +5 -17
  161. data/test/pulse_io/ir_output_test.rb +5 -0
  162. data/test/pulse_io/pwm_output_test.rb +10 -10
  163. data/test/rtc/ds3231_test.rb +3 -2
  164. data/test/sensor/dht_test.rb +11 -11
  165. data/test/spi/bitbang_test.rb +27 -0
  166. data/test/spi/bus_test.rb +19 -29
  167. data/test/spi/input_register_test.rb +2 -2
  168. data/test/spi/{peripheral_multi_pin_test.rb → peripheral_test.rb} +25 -5
  169. data/test/test_helper.rb +44 -124
  170. data/vendor/board-maps/BoardMap.h +264 -0
  171. data/vendor/board-maps/yaml/ALFREDO_NOU3.yml +2 -0
  172. data/vendor/board-maps/yaml/ATD143_S3.yml +1 -0
  173. data/vendor/board-maps/yaml/BHARATPI_A7672S_4G.yml +14 -0
  174. data/vendor/board-maps/yaml/BHARATPI_LORA.yml +14 -0
  175. data/vendor/board-maps/yaml/BHARATPI_NODE_WIFI.yml +14 -0
  176. data/vendor/board-maps/yaml/BPI_LEAF_S3.yml +0 -1
  177. data/vendor/board-maps/yaml/CEZERIO_DEV_ESP32C6.yml +14 -0
  178. data/vendor/board-maps/yaml/CEZERIO_MINI_DEV_ESP32C6.yml +12 -0
  179. data/vendor/board-maps/yaml/CIRCUITART_ZERO_S3.yml +71 -0
  180. data/vendor/board-maps/yaml/CODECELLC3.yml +13 -0
  181. data/vendor/board-maps/yaml/CYOBOT_V2_ESP32S3.yml +7 -0
  182. data/vendor/board-maps/yaml/EDGES3D.yml +25 -0
  183. data/vendor/board-maps/yaml/ESP32C6_DEV.yml +3 -4
  184. data/vendor/board-maps/yaml/ESP32C6_THING_PLUS.yml +0 -1
  185. data/vendor/board-maps/yaml/ESP32H2_DEV.yml +0 -1
  186. data/vendor/board-maps/yaml/ESP32H2_DEVKIT_LIPO.yml +0 -1
  187. data/vendor/board-maps/yaml/ESP32P4_DEV.yml +35 -0
  188. data/vendor/board-maps/yaml/ESP32S2_DEV.yml +0 -1
  189. data/vendor/board-maps/yaml/ESP32S2_DEVKIT_LIPO.yml +0 -1
  190. data/vendor/board-maps/yaml/ESP32S2_DEVKIT_LIPO_USB.yml +0 -1
  191. data/vendor/board-maps/yaml/ESP32_2432S028R.yml +14 -0
  192. data/vendor/board-maps/yaml/FEATHERS3.yml +1 -1
  193. data/vendor/board-maps/yaml/FRI3D_2024_ESP32S3.yml +43 -0
  194. data/vendor/board-maps/yaml/GEEKBLE_ESP32C3.yml +0 -1
  195. data/vendor/board-maps/yaml/GEEKBLE_NANO_ESP32S3.yml +25 -0
  196. data/vendor/board-maps/yaml/HELTEC_VISION_MASTER_E290.yml +41 -0
  197. data/vendor/board-maps/yaml/HELTEC_VISION_MASTER_E_213.yml +41 -0
  198. data/vendor/board-maps/yaml/HELTEC_VISION_MASTER_T190.yml +41 -0
  199. data/vendor/board-maps/yaml/HUIDU_HD_WF2.yml +5 -0
  200. data/vendor/board-maps/yaml/HUIDU_HD_WF4.yml +1 -0
  201. data/vendor/board-maps/yaml/LILYGO_LORA_CC1101.yml +6 -0
  202. data/vendor/board-maps/yaml/LILYGO_LORA_LR1121.yml +6 -0
  203. data/vendor/board-maps/yaml/LILYGO_LORA_SI4432.yml +6 -0
  204. data/vendor/board-maps/yaml/LILYGO_LORA_SX1262.yml +6 -0
  205. data/vendor/board-maps/yaml/LILYGO_LORA_SX1280.yml +6 -0
  206. data/vendor/board-maps/yaml/LOLIN_C3_MINI.yml +0 -1
  207. data/vendor/board-maps/yaml/LOLIN_C3_PICO.yml +1 -2
  208. data/vendor/board-maps/yaml/LoPy.yml +0 -1
  209. data/vendor/board-maps/yaml/LoPy4.yml +0 -1
  210. data/vendor/board-maps/yaml/M5STACK_DINMETER.yml +8 -0
  211. data/vendor/board-maps/yaml/M5STACK_FIRE.yml +1 -1
  212. data/vendor/board-maps/yaml/OMGS3.yml +25 -0
  213. data/vendor/board-maps/yaml/PCBCUPID_GLYPHC3.yml +23 -0
  214. data/vendor/board-maps/yaml/PCBCUPID_GLYPHC6.yml +32 -0
  215. data/vendor/board-maps/yaml/PCBCUPID_GLYPHH2.yml +24 -0
  216. data/vendor/board-maps/yaml/PYCOM_GPY.yml +0 -1
  217. data/vendor/board-maps/yaml/SENSEBOX_MCU_ESP32S2.yml +1 -1
  218. data/vendor/board-maps/yaml/SPARKFUN_ESP32S3_THING_PLUS.yml +13 -0
  219. data/vendor/board-maps/yaml/SPARKLEMOTIONMINI_ESP32.yml +12 -0
  220. data/vendor/board-maps/yaml/SPARKLEMOTIONSTICK_ESP32.yml +11 -0
  221. data/vendor/board-maps/yaml/SPARKLEMOTION_ESP32.yml +12 -0
  222. data/vendor/board-maps/yaml/SQUIXL.yml +7 -0
  223. data/vendor/board-maps/yaml/THINGPULSE_EPULSE_FEATHER_C6.yml +0 -1
  224. data/vendor/board-maps/yaml/T_LORA_PAGER.yml +6 -0
  225. data/vendor/board-maps/yaml/T_WATCH_S3.yml +7 -0
  226. data/vendor/board-maps/yaml/T_WATCH_S3_ULTRA.yml +6 -0
  227. data/vendor/board-maps/yaml/WAVESHARE_ESP32_S3_LCD_146.yml +41 -0
  228. data/vendor/board-maps/yaml/WAVESHARE_ESP32_S3_LCD_147.yml +41 -0
  229. data/vendor/board-maps/yaml/WAVESHARE_ESP32_S3_LCD_169.yml +38 -0
  230. data/vendor/board-maps/yaml/WAVESHARE_ESP32_S3_LCD_185.yml +41 -0
  231. data/vendor/board-maps/yaml/WAVESHARE_ESP32_S3_RELAY_6CH.yml +41 -0
  232. data/vendor/board-maps/yaml/WAVESHARE_ESP32_S3_TOUCH_AMOLED_143.yml +7 -0
  233. data/vendor/board-maps/yaml/WAVESHARE_ESP32_S3_TOUCH_AMOLED_164.yml +7 -0
  234. data/vendor/board-maps/yaml/WAVESHARE_ESP32_S3_TOUCH_AMOLED_18.yml +38 -0
  235. data/vendor/board-maps/yaml/WAVESHARE_ESP32_S3_TOUCH_AMOLED_191.yml +7 -0
  236. data/vendor/board-maps/yaml/WAVESHARE_ESP32_S3_TOUCH_AMOLED_241.yml +7 -0
  237. data/vendor/board-maps/yaml/WAVESHARE_ESP32_S3_TOUCH_LCD_146.yml +41 -0
  238. data/vendor/board-maps/yaml/WAVESHARE_ESP32_S3_TOUCH_LCD_169.yml +38 -0
  239. data/vendor/board-maps/yaml/WAVESHARE_ESP32_S3_TOUCH_LCD_185.yml +41 -0
  240. data/vendor/board-maps/yaml/WAVESHARE_ESP32_S3_TOUCH_LCD_185_BOX.yml +41 -0
  241. data/vendor/board-maps/yaml/WAVESHARE_ESP32_S3_TOUCH_LCD_21.yml +41 -0
  242. data/vendor/board-maps/yaml/WAVESHARE_ESP32_S3_TOUCH_LCD_28.yml +41 -0
  243. data/vendor/board-maps/yaml/WAVESHARE_ESP32_S3_TOUCH_LCD_4.yml +36 -0
  244. data/vendor/board-maps/yaml/WAVESHARE_ESP32_S3_TOUCH_LCD_43.yml +38 -0
  245. data/vendor/board-maps/yaml/WAVESHARE_ESP32_S3_TOUCH_LCD_43B.yml +38 -0
  246. data/vendor/board-maps/yaml/WAVESHARE_ESP32_S3_TOUCH_LCD_5.yml +38 -0
  247. data/vendor/board-maps/yaml/WAVESHARE_ESP32_S3_TOUCH_LCD_5B.yml +38 -0
  248. data/vendor/board-maps/yaml/WAVESHARE_ESP32_S3_TOUCH_LCD_7.yml +38 -0
  249. data/vendor/board-maps/yaml/WAVESHARE_ESP32_S3_ZERO.yml +36 -0
  250. data/vendor/board-maps/yaml/WIPY3.yml +0 -1
  251. data/vendor/board-maps/yaml/WS_ESP32_S3_MATRIX.yml +38 -0
  252. data/vendor/board-maps/yaml/XIAO_ESP32S3_PLUS.yml +46 -0
  253. data/vendor/board-maps/yaml/YB_ESP32S3_AMP_V2.yml +28 -0
  254. data/vendor/board-maps/yaml/YB_ESP32S3_AMP_V3.yml +28 -0
  255. data/vendor/board-maps/yaml/YB_ESP32S3_ETH.yml +40 -0
  256. data/vendor/board-maps/yaml/mercury.yml +20 -0
  257. metadata +116 -101
  258. data/.vscode/settings.json +0 -5
  259. data/.vscode/tasks.json +0 -20
  260. data/HARDWARE.md +0 -263
  261. data/benchmarks/analog_listen.rb +0 -49
  262. data/benchmarks/digital_write.rb +0 -28
  263. data/benchmarks/i2c_ssd1306_refresh.rb +0 -91
  264. data/examples/advanced/m5_env3.rb +0 -46
  265. data/examples/advanced/rotary_encoder_mac_volume.rb +0 -53
  266. data/examples/advanced/ssd1306_time_temp_rh.rb +0 -43
  267. data/examples/analog_io/ads1100.rb +0 -48
  268. data/examples/analog_io/ads1115.rb +0 -57
  269. data/examples/analog_io/ads1118.rb +0 -65
  270. data/examples/analog_io/dac_loopback.rb +0 -34
  271. data/examples/analog_io/input.rb +0 -56
  272. data/examples/analog_io/input_smoothing.rb +0 -27
  273. data/examples/analog_io/potentiometer.rb +0 -31
  274. data/examples/connection/binary_echo.rb +0 -34
  275. data/examples/connection/tcp.rb +0 -19
  276. data/examples/digital_io/button.rb +0 -17
  277. data/examples/digital_io/relay.rb +0 -17
  278. data/examples/digital_io/rotary_encoder.rb +0 -36
  279. data/examples/display/hd44780.png +0 -0
  280. data/examples/display/hd44780.rb +0 -47
  281. data/examples/display/ssd1306.rb +0 -43
  282. data/examples/display/ssd1306_s2_pico.rb +0 -29
  283. data/examples/eeprom/built_in.rb +0 -32
  284. data/examples/i2c/search.rb +0 -39
  285. data/examples/led/apa102_bounce.rb +0 -32
  286. data/examples/led/apa102_fade.rb +0 -44
  287. data/examples/led/builtin_blink.rb +0 -14
  288. data/examples/led/builtin_fade.rb +0 -19
  289. data/examples/led/rgb_led.rb +0 -31
  290. data/examples/led/seven_segment_char_echo.rb +0 -17
  291. data/examples/led/ws2812_bounce.rb +0 -30
  292. data/examples/led/ws2812_builtin_blink.rb +0 -22
  293. data/examples/led/ws2812_fade.rb +0 -43
  294. data/examples/motor/l298.rb +0 -45
  295. data/examples/motor/servo.rb +0 -17
  296. data/examples/motor/stepper.png +0 -0
  297. data/examples/motor/stepper.rb +0 -43
  298. data/examples/one_wire/search.rb +0 -32
  299. data/examples/pulse_io/buzzer.rb +0 -35
  300. data/examples/pulse_io/ir_output.rb +0 -51
  301. data/examples/pulse_io/pwm_output.rb +0 -30
  302. data/examples/rtc/ds3231.rb +0 -31
  303. data/examples/sensor/aht10.rb +0 -17
  304. data/examples/sensor/aht20.rb +0 -17
  305. data/examples/sensor/bme280.rb +0 -38
  306. data/examples/sensor/bmp180.rb +0 -26
  307. data/examples/sensor/dht.rb +0 -29
  308. data/examples/sensor/ds18b20.rb +0 -57
  309. data/examples/sensor/generic_pir.rb +0 -27
  310. data/examples/sensor/hcsr04.rb +0 -17
  311. data/examples/sensor/htu21d.rb +0 -43
  312. data/examples/sensor/htu31d.rb +0 -33
  313. data/examples/sensor/neat_tph_readings.rb +0 -32
  314. data/examples/sensor/qmp6988.rb +0 -51
  315. data/examples/sensor/rcwl9620.rb +0 -15
  316. data/examples/sensor/sht3x.rb +0 -32
  317. data/examples/spi/bitbang_loopback.rb +0 -46
  318. data/examples/spi/input_register.rb +0 -40
  319. data/examples/spi/output_register.rb +0 -41
  320. data/examples/spi/ssd_through_registers.rb +0 -28
  321. data/examples/spi/two_registers.rb +0 -40
  322. data/examples/uart/bit_bang_demo.rb +0 -25
  323. data/examples/uart/board_passthrough.rb +0 -40
  324. data/examples/uart/hardware_loopback.rb +0 -16
  325. data/lib/denko/eeprom/built_in.rb +0 -72
  326. data/lib/denko/fonts.rb +0 -106
  327. data/test/eeprom/built_in_test.rb +0 -61
  328. data/test/spi/peripheral_single_pin_test.rb +0 -48
  329. data/tutorial/01-led/led.fzz +0 -0
  330. data/tutorial/01-led/led.pdf +0 -0
  331. data/tutorial/01-led/led.rb +0 -73
  332. data/tutorial/02-button/button.fzz +0 -0
  333. data/tutorial/02-button/button.pdf +0 -0
  334. data/tutorial/02-button/button.rb +0 -65
  335. data/tutorial/03-potentiometer/potentiometer.fzz +0 -0
  336. data/tutorial/03-potentiometer/potentiometer.pdf +0 -0
  337. data/tutorial/03-potentiometer/potentiometer.rb +0 -66
  338. data/tutorial/04-pwm_led/pwm_led.fzz +0 -0
  339. data/tutorial/04-pwm_led/pwm_led.pdf +0 -0
  340. data/tutorial/04-pwm_led/pwm_led.rb +0 -64
  341. data/tutorial/05-rgb_led/rgb_led.fzz +0 -0
  342. data/tutorial/05-rgb_led/rgb_led.pdf +0 -0
  343. data/tutorial/05-rgb_led/rgb_led.rb +0 -58
  344. data/tutorial/05-rgb_led/rgb_mapping.rb +0 -76
@@ -1,181 +1,283 @@
1
1
  module Denko
2
2
  module Display
3
3
  class Canvas
4
- include Denko::Fonts
5
-
6
- attr_reader :columns, :rows, :framebuffer
7
-
8
- def initialize(columns, rows)
9
- raise ArgumentError, "bitmap height must be divisible by 8" unless (rows % 8 == 0)
10
-
11
- @columns = columns
12
- @rows = rows
4
+ attr_reader :columns, :rows, :framebuffer, :framebuffers, :colors
13
5
 
6
+ def initialize(columns, rows, colors: 1)
7
+ @columns = columns
8
+ @rows = rows
9
+ @rows = ((rows / 8.0).ceil * 8) if (rows % 8 != 0)
14
10
  # Use a byte array for the framebuffer. Each byte is 8 pixels arranged vertically.
15
11
  # Each slice @columns long represents an area @columns wide * 8 pixels tall.
16
- @bytes = @columns * (@rows / 8)
17
- @framebuffer = Array.new(@bytes) { 0x00 }
12
+ @bytes = @columns * (@rows / 8)
13
+
14
+ # Framebuffer setup. 1-bit framebuffer for each color.
15
+ # Works for mono LCDs and OLEDs, or mono/multi-color e-paper.
16
+ @colors = colors
17
+ @framebuffers = []
18
+ @colors.times { @framebuffers << Array.new(@bytes) { 0x00 } }
19
+ # Only first framebuffer is used for mono displays.
20
+ @framebuffer = @framebuffers.first
21
+
22
+ # Default drawing state
23
+ self.font = Denko::Display::Font::BMP_6X8
24
+ @font_scale = 1
25
+ @current_color = 1
26
+
27
+ # Transformation state
28
+ @swap_xy = false
29
+ @invert_x = false
30
+ @invert_y = false
31
+ @rotation = 0
32
+ calculate_bounds
33
+ end
34
+
35
+ def clear
36
+ @framebuffers.each { |fb| fb.fill 0x00 }
18
37
  end
19
38
 
20
39
  def fill
21
- @framebuffer.fill(0xFF)
40
+ # Clear all buffers, then fill the first one, which is the only
41
+ # one for mono displays, black for multi-color e-paper.
42
+ clear
43
+ @framebuffers.first.fill 0xFF
22
44
  end
23
45
 
24
- def clear
25
- @framebuffer.fill(0x00)
46
+ #
47
+ # PIXEL OPERATIONS
48
+ #
49
+ def _get_pixel(x, y)
50
+ byte = ((y / 8) * @columns) + x
51
+ bit = y % 8
52
+
53
+ # Go through framebuffers and return that color when bit is set.
54
+ @framebuffers.each_with_index do |fb, index|
55
+ return index+1 if ((fb[byte] >> bit) & 0b1 == 1)
56
+ end
57
+
58
+ # If bit wasn't set in any framebuffer, color is 0.
59
+ return 0
26
60
  end
27
61
 
28
- def get_pixel(x, y)
29
- byte = ((y / 8) * @columns) + x
30
- bit = y % 8
31
- (@framebuffer[byte] >> bit) & 0b00000001
62
+ def get_pixel(x:, y:)
63
+ _get_pixel(x, y)
32
64
  end
33
65
 
34
- def set_pixel(x, y)
35
- byte = ((y / 8) * @columns) + x
36
- bit = y % 8
37
- @framebuffer[byte] |= (0b1 << bit)
66
+ def _set_pixel(x, y, color=current_color)
67
+ xt = (@invert_x) ? @x_max - x : x
68
+ yt = (@invert_y) ? @y_max - y : y
69
+ if (@swap_xy)
70
+ tt = xt
71
+ xt = yt
72
+ yt = tt
73
+ end
74
+
75
+ # Bounds check
76
+ return nil if (xt < 0 || yt < 0 || xt > @columns-1 || yt > @rows -1)
77
+ return nil if (color < 0) || (color > colors)
78
+
79
+ byte = ((yt / 8) * @columns) + xt
80
+ bit = yt % 8
81
+
82
+ # Set pixel bit in that color's buffer. Clear in others.
83
+ # When color == 0, clears in all buffers and sets in none.
84
+ for i in 1..colors
85
+ if (color == i)
86
+ @framebuffers[i-1][byte] |= (0b1 << bit)
87
+ else
88
+ @framebuffers[i-1][byte] &= ~(0b1 << bit)
89
+ end
90
+ end
38
91
  end
39
92
 
40
- def clear_pixel(x, y)
41
- byte = ((y / 8) * @columns) + x
42
- bit = y % 8
43
- @framebuffer[byte] &= ~(0b1 << bit)
93
+ def set_pixel(x:, y:, color:current_color)
94
+ _set_pixel(x, y, color)
44
95
  end
45
96
 
46
- def pixel(x, y, state=0)
47
- (state == 0) ? clear_pixel(x, y) : set_pixel(x, y)
97
+ def clear_pixel(x:, y:)
98
+ _set_pixel(x, y, 0)
48
99
  end
49
100
 
50
- # Draw a line based on Bresenham's line algorithm.
51
- def line(x1, y1, x2, y2, color=1)
101
+ #
102
+ # LINE
103
+ #
104
+ def _line(x1, y1, x2, y2, color=current_color)
52
105
  # Deltas in each axis.
53
106
  dy = y2 - y1
54
107
  dx = x2 - x1
55
108
 
56
- # Catch vertical lines to avoid division by 0.
109
+ # Optimize vertical lines, and avoid division by 0.
57
110
  if (dx == 0)
58
111
  # Ensure y1 < y2.
59
112
  y1, y2 = y2, y1 if (y2 < y1)
60
- (y1..y2).each do |y|
61
- pixel(x1, y, color)
62
- end
113
+ (y1..y2).each { |y| _set_pixel(x1, y, color) }
63
114
  return
64
115
  end
65
116
 
66
- gradient = dy.to_f / dx
67
-
68
- # Gradient magnitude <= 45 degrees: find y for each x.
69
- if (gradient >= -1) && (gradient <= 1)
117
+ # Optimize horizontal lines.
118
+ if (dy == 0)
70
119
  # Ensure x1 < x2.
71
- x1, y1, x2, y2 = x2, y2, x1, y1 if (x2 < x1)
72
-
73
- # When x increments, add gradient to y.
74
- y = y1
75
- y_step = gradient
76
- (x1..x2).each do |x|
77
- pixel(x, y.round, color)
78
- y = y + y_step
79
- end
120
+ x1, x2 = x2, x1 if (x2 < x1)
121
+ (x1..x2).each { |x| _set_pixel(x, y1, color) }
122
+ return
123
+ end
80
124
 
81
- # Gradient magnitude > 45 degrees: find x for each y.
82
- else
83
- # Ensure y1 < y2.
84
- x1, y1, x2, y2 = x2, y2, x1, y1 if (y2 < y1)
85
-
86
- # When y increments, add inverse of gradient to x.
87
- x = x1
88
- x_step = 1 / gradient
89
- (y1..y2).each do |y|
90
- pixel(x.round, y, color)
91
- x = x + x_step
125
+ # Based on Bresenham's line algorithm
126
+ # Slope calculations
127
+ step_axis = (dx.abs > dy.abs) ? :x : :y
128
+ step_count = (step_axis == :x) ? dx.abs : dy.abs
129
+ x_step = (dx > 0) ? 1 : -1
130
+ y_step = (dy > 0) ? 1 : -1
131
+
132
+ # Error calculations
133
+ error_step = (step_axis == :x) ? dy.abs : dx.abs
134
+ error_threshold = (step_axis == :x) ? dx.abs : dy.abs
135
+
136
+ x = x1
137
+ y = y1
138
+ error = 0
139
+ (0..step_count).each do |i|
140
+ _set_pixel(x, y, color)
141
+
142
+ if (step_axis == :x)
143
+ x += x_step
144
+ error += error_step
145
+ if (error >= error_threshold)
146
+ y += y_step
147
+ error -= error_threshold
148
+ end
149
+ else
150
+ y += y_step
151
+ error += error_step
152
+ if (error >= error_threshold)
153
+ x += x_step
154
+ error -= error_threshold
155
+ end
92
156
  end
93
157
  end
94
158
  end
95
159
 
96
- # Rectangles and squares as a combination of lines.
97
- def rectangle(x, y, width, height, color=1)
98
- line(x, y, x+width, y, color)
99
- line(x+width, y, x+width, y+height, color)
100
- line(x+width, y+height, x, y+height, color)
101
- line(x, y+height, x, y, color)
160
+ def line(x1:, y1:, x2:, y2:, color:current_color)
161
+ _line(x1, y1, x2, y2, color)
102
162
  end
103
163
 
104
- # Draw a vertical line for every x value to get a filled rectangle.
105
- def filled_rectangle(x, y_start, width, height, color=1)
106
- y_end = y_start + height
107
- y_start, y_end = y_end, y_start if (y_end < y_start)
108
- (y_start..y_end).each do |y|
109
- line(x, y, x+width, y, color)
164
+ #
165
+ # RECTANGLE & SQUARE
166
+ #
167
+ def _rectangle(x1, y1, x2, y2, filled=false, color=current_color)
168
+ if filled
169
+ y1, y2 = y2, y1 if (y2 < y1)
170
+ (y1..y2).each { |y| _line(x1, y, x2, y, color) }
171
+ else
172
+ _line(x1, y1, x2, y1, color)
173
+ _line(x2, y1, x2, y2, color)
174
+ _line(x2, y2, x1, y2, color)
175
+ _line(x1, y2, x1, y1, color)
110
176
  end
111
177
  end
112
178
 
113
- # Open ended path
114
- def path(points=[], color=1)
115
- return unless points
179
+ def rectangle(x1:nil, y1:nil, x2:nil, y2:nil, x:nil, y:nil, w:nil, h:nil, filled:false, color:current_color)
180
+ if (x || y || w || h) && (x1 || x2 || y1 || y2)
181
+ raise ArgumentError, "#rectangle accepts x:, y:, w:, h: OR x1:, y1:, x2:, y2: not a combination of both"
182
+ end
183
+
184
+ if (w && h)
185
+ x1 = x
186
+ x2 = x1 + w
187
+ x2 += 1 if (x2 < x1)
188
+ x2 -= 1 if (x1 < x2)
189
+
190
+ y1 = y
191
+ y2 = y1 + h
192
+ y2 += 1 if (y2 < y1)
193
+ y2 -= 1 if (y1 < y2)
194
+ end
195
+
196
+ _rectangle(x1, y1, x2, y2, filled, color)
197
+ end
198
+
199
+ def square(x:, y:, size:, filled:false, color:current_color)
200
+ rectangle(x: x, y: y, w: size, h: size, filled: filled, color: color)
201
+ end
202
+
203
+ #
204
+ # PATH, POLYGON & TRIANGLE
205
+ #
206
+ def _path(points, color=current_color)
116
207
  start = points[0]
117
208
  (1..points.length-1).each do |i|
118
209
  finish = points[i]
119
- line(start[0], start[1], finish[0], finish[1])
210
+ _line(start[0], start[1], finish[0], finish[1], color)
120
211
  start = finish
121
212
  end
122
213
  end
123
214
 
124
- # Close paths by repeating the start value at the end.
125
- def polygon(points=[], color=1)
126
- points << points[0]
127
- path(points)
215
+ def path(points, color:current_color)
216
+ _path(points, color)
128
217
  end
129
218
 
130
- # Filled polygon using horizontal ray casting + stroked polygon.
131
- def filled_polygon(points=[], color=1)
132
- # Get all the X and Y coordinates from the points as floats.
133
- coords_x = points.map { |point| point.first.to_f }
134
- coords_y = points.map { |point| point.last.to_f }
135
-
136
- # Get Y bounds of the polygon to limit rows.
137
- y_min = coords_y.min.to_i
138
- y_max = coords_y.max.to_i
139
-
140
- # Cast horizontal ray on each row, storing nodes where it intersects polygon edges.
141
- (y_min..y_max).each do |y|
142
- nodes = []
143
- i = 0
144
- j = points.count - 1
145
-
146
- while (i < points.count) do
147
- if (coords_y[i] < y && coords_y[j] >= y || coords_y[j] < y && coords_y[i] >= y)
148
- nodes << (coords_x[i] + (y - coords_y[i]) / (coords_y[j] - coords_y[i]) *(coords_x[j] - coords_x[i])).round
219
+ def _polygon(points, filled=false, color=current_color)
220
+ if filled
221
+ # Get all the X and Y coordinates from the points as floats.
222
+ coords_x = points.map { |point| point.first.to_f }
223
+ coords_y = points.map { |point| point.last.to_f }
224
+
225
+ # Get Y bounds of the polygon to limit rows.
226
+ y_min = coords_y.min.to_i
227
+ y_max = coords_y.max.to_i
228
+
229
+ # Cast horizontal ray on each row, storing nodes where it intersects polygon edges.
230
+ (y_min..y_max).each do |y|
231
+ nodes = []
232
+ i = 0
233
+ j = points.count - 1
234
+
235
+ while (i < points.count) do
236
+ # First condition excludes horizontal edges.
237
+ # Second and third check for +ve and -ve intersection respectively.
238
+ if (coords_y[i] != coords_y[j]) && ((coords_y[i] < y && coords_y[j] >= y) || (coords_y[j] < y && coords_y[i] >= y))
239
+ # Interoplate to find the intersection point (node).
240
+ nodes << (coords_x[i] + (y - coords_y[i]) / (coords_y[j] - coords_y[i]) * (coords_x[j] - coords_x[i])).round
241
+ end
242
+ j = i
243
+ i += 1
149
244
  end
150
- j = i
151
- i += 1
152
- end
153
- nodes = nodes.sort
245
+ nodes = nodes.sort
154
246
 
155
- # Take pairs of nodes and fill between them. This automatically ignores the spaces
156
- # between even then odd nodes, which are outside the polygon.
157
- nodes.each_slice(2) do |pair|
158
- next if pair.length < 2
159
- line(pair.first, y, pair.last, y, color)
247
+ # Take pairs of nodes and fill between them.
248
+ # This ignores the spaces between odd then even nodes (eg. 1->2), which are outside the polygon.
249
+ nodes.each_slice(2) do |pair|
250
+ _line(pair.first, y, pair.last, y, color) if pair.length == 2
251
+ end
160
252
  end
161
253
  end
162
254
 
163
- # Stroke the polygon anyway. Floating point math misses thin areas.
164
- polygon(points, color)
255
+ # Stroke regardless, since floating point math misses thin areas of fill.
256
+ _path(points, color)
257
+ # Close open path by adding a line between the first and last points.
258
+ _line(points[-1][0], points[-1][1], points[0][0], points[0][1], color)
259
+ end
260
+
261
+ def polygon(points, filled:false, color:current_color)
262
+ _polygon(points, filled, color)
165
263
  end
166
-
167
- # Triangle with 3 points as 6 flat args.
168
- def triangle(x1, y1, x2, y2, x3, y3, color=1)
169
- polygon([[x1,y1], [x2,y2], [x3,y3]], color)
264
+
265
+ def _triangle(x1, y1, x2, y2, x3, y3, filled=false, color=current_color)
266
+ if filled
267
+ _polygon([[x1,y1], [x2,y2], [x3,y3]], filled, color)
268
+ else
269
+ _line(x1, y1, x2, y2, color)
270
+ _line(x2, y2, x3, y3, color)
271
+ _line(x3, y3, x1, y1, color)
272
+ end
170
273
  end
171
274
 
172
- # Filled triangle with 3 points as 6 flat args.
173
- def filled_triangle(x1, y1, x2, y2, x3, y3, color=1)
174
- filled_polygon([[x1,y1], [x2,y2], [x3,y3]], color)
275
+ def triangle(x1:, y1:, x2:, y2:, x3:, y3:, filled:false, color:current_color)
276
+ _triangle(x1, y1, x2, y2, x3, y3, filled, color)
175
277
  end
176
278
 
177
- # Midpoint ellipse / circle based on Bresenham's circle algorithm.
178
- def ellipse(x_center, y_center, a, b, color=1, filled=false)
279
+ def _ellipse(x_center, y_center, a, b, filled=false, color=current_color)
280
+ # Midpoint ellipse / circle based on Bresenham's circle algorithm.
179
281
  # Start position
180
282
  x = -a
181
283
  y = 0
@@ -193,9 +295,15 @@ module Denko
193
295
  # Since starting at max negative X, continue until x is 0.
194
296
  while (x <= 0)
195
297
  if filled
196
- fill_quadrants(x_center, y_center, x, y, color)
298
+ # Fill left and right quadrants in single line, alternating +ve and -ve y.
299
+ _line(x_center - x, y_center + y, x_center + x, y_center + y, color)
300
+ _line(x_center - x, y_center - y, x_center + x, y_center - y, color)
197
301
  else
198
- stroke_quadrants(x_center, y_center, x, y, color)
302
+ # Stroke quadrants in order as if y-axis is reversed and going counter-clockwise from +ve X.
303
+ _set_pixel(x_center - x, y_center - y, color)
304
+ _set_pixel(x_center + x, y_center - y, color)
305
+ _set_pixel(x_center + x, y_center + y, color)
306
+ _set_pixel(x_center - x, y_center + y, color)
199
307
  end
200
308
 
201
309
  e2 = 2 * e1
@@ -214,69 +322,154 @@ module Denko
214
322
  # Continue if y hasn't reached the vertical size.
215
323
  while (y < b)
216
324
  y += 1
217
- pixel(x_center, y_center + y, color)
218
- pixel(x_center, y_center - y, color)
325
+ _set_pixel(x_center, y_center + y, color)
326
+ _set_pixel(x_center, y_center - y, color)
219
327
  end
220
328
  end
221
329
 
222
- def stroke_quadrants(x_center, y_center, x, y, color)
223
- # Quadrants in order as if y-axis is reversed and going counter-clockwise from +ve X.
224
- pixel(x_center - x, y_center - y, color)
225
- pixel(x_center + x, y_center - y, color)
226
- pixel(x_center + x, y_center + y, color)
227
- pixel(x_center - x, y_center + y, color)
330
+ def ellipse(x:, y:, a:, b:, filled:false, color:current_color)
331
+ _ellipse(x, y, a, b, filled, color)
228
332
  end
229
333
 
230
- def fill_quadrants(x_center, y_center, x, y, color)
231
- line(x_center - x, y_center + y, x_center + x, y_center + y, color)
232
- line(x_center - x, y_center - y, x_center + x, y_center - y, color)
334
+ def circle(x:, y:, r:, filled:false, color:current_color)
335
+ _ellipse(x, y, r, r, filled, color)
233
336
  end
234
337
 
235
- def circle(x_center, y_center, radius, color=1, filled=false)
236
- ellipse(x_center, y_center, radius, radius, color, filled)
338
+ #
339
+ # BITMAP TEXT
340
+ #
341
+ def text(str, color:current_color)
342
+ str.to_s.split("").each { |char| draw_char(char, color: color) }
237
343
  end
238
344
 
239
- def filled_circle(x_center, y_center, radius, color=1)
240
- ellipse(x_center, y_center, radius, radius, color, true)
345
+ def draw_char(char, color:current_color)
346
+ # 0th character in font is SPACE. Offset ASCII code and show ? if character doesn't exist in font.
347
+ index = char.ord - 32
348
+ index = 31 if (index < 0 || index > @font_last_character)
349
+ char_map = @font_characters[index]
350
+
351
+ # Offset by scaled height, since bottom left of char starts at text cursor.
352
+ x = text_cursor[0]
353
+ y = text_cursor[1] + 1 - (@font_height * @font_scale)
354
+
355
+ # Draw it
356
+ _draw_char(char_map, x, y, @font_width, @font_scale, color)
357
+
358
+ # Increment the text cursor, scaling width.
359
+ self.text_cursor[0] += @font_width * @font_scale
241
360
  end
242
361
 
243
- def text_cursor=(array=[])
244
- @text_cursor = array
362
+ def _draw_char(byte_array, x, y, width, scale, color=current_color)
363
+ byte_array.each_slice(width) do |slice|
364
+ slice.each_with_index do |byte, col_offset|
365
+ 8.times do |bit|
366
+ # Don't do anything if this bit isn't set in the font.
367
+ if (((byte >> bit) & 0b1) == 1)
368
+ scale.times do |x_offset|
369
+ scale.times do |y_offset|
370
+ _set_pixel(x + (col_offset * scale) + x_offset, y + (bit * scale) + y_offset, color)
371
+ end
372
+ end
373
+ end
374
+ end
375
+ end
376
+ y = y + (8 * scale)
377
+ end
378
+ end
379
+
380
+ #
381
+ # DRAWING STATE
382
+ #
383
+ def current_color=(color)
384
+ raise Argument error, "color must be within (0..#{colors})" if (color < 0) || (color > colors)
385
+ @current_color = color
245
386
  end
246
387
 
388
+ attr_reader :current_color
389
+
390
+ DEFAULT_TEXT_CURSOR = [0, 7]
391
+
247
392
  def text_cursor
248
- @text_cursor ||= [0, 7]
393
+ @text_cursor ||= DEFAULT_TEXT_CURSOR
249
394
  end
250
395
 
251
- def print(str)
252
- str.to_s.split("").each do |char|
253
- print_char(char)
254
- end
396
+ def text_cursor=(array=DEFAULT_TEXT_CURSOR)
397
+ @text_cursor = array
255
398
  end
256
399
 
257
- def print_char(char)
258
- # 0th character in font is SPACE. Offset and limit to printable chars.
259
- index = char.ord - 32
260
- index = 0 if (index < 0 || index > 94)
261
- char_map = FONT_6x8[index]
400
+ def font=(font)
401
+ if font.class == Symbol
402
+ @font = Display::Font.const_get(font.to_s.upcase)
403
+ else
404
+ @font = font
405
+ end
262
406
 
263
- raw_char(char_map)
407
+ @font_height = @font[:height]
408
+ @font_width = @font[:width]
409
+ @font_characters = @font[:characters]
410
+ @font_last_character = @font_characters.length - 1
264
411
  end
265
412
 
266
- def raw_char(byte_array)
267
- # Get the starting byte index.
268
- page = text_cursor[1] / 8
269
- byte_index = (@columns * page) + text_cursor[0]
413
+ def font_scale=(scale)
414
+ raise ArgumentError, "#font_scale must be a positive integer" unless (scale.class == Integer) && scale > 0
415
+ @font_scale = scale
416
+ end
270
417
 
271
- # Replace those bytes in the framebuffer with the character.
272
- byte_array.each do |byte|
273
- @framebuffer[byte_index] = byte
274
- byte_index += 1
418
+ attr_reader :font, :font_scale
419
+
420
+ #
421
+ # TRANSFORMATION
422
+ #
423
+ def rotate(degrees=180)
424
+ raise ArgumentError, "Canvas can only be rotated in multiples of 90 degrees" unless (degrees % 90 == 0)
425
+ old_rotation = @rotation
426
+ @rotation = (old_rotation + degrees) % 360
427
+ change = (@rotation - old_rotation) % 360
428
+
429
+ (change / 90).times do
430
+ @swap_xy = !@swap_xy
431
+ if (!@invert_x && !@invert_y)
432
+ @invert_x = true
433
+ @invert_y = false
434
+ elsif (@invert_x && !@invert_y)
435
+ @invert_x = true
436
+ @invert_y = true
437
+ elsif (@invert_x && @invert_y)
438
+ @invert_x = false
439
+ @invert_y = true
440
+ elsif (!@invert_x && @invert_y)
441
+ @invert_x = false
442
+ @invert_y = false
443
+ end
275
444
  end
445
+ calculate_bounds
446
+ end
276
447
 
277
- # Increment the text cursor.
278
- self.text_cursor[0] += byte_array.length
448
+ def reflect(axis)
449
+ raise ArgumentError "Canvas can only be reflected in :x or :y axis" unless [:x, :y].include? (axis)
450
+ (axis == :x) ? @invert_x = !@invert_x : @invert_y = !@invert_y
451
+ calculate_bounds
279
452
  end
453
+
454
+ def reflect_x
455
+ reflect(:x)
456
+ end
457
+
458
+ def reflect_y
459
+ reflect(:y)
460
+ end
461
+
462
+ def calculate_bounds
463
+ if @swap_xy
464
+ @x_max = @rows - 1
465
+ @y_max = @columns - 1
466
+ else
467
+ @x_max = @columns - 1
468
+ @y_max = @rows - 1
469
+ end
470
+ end
471
+
472
+ attr_reader :x_max, :y_max
280
473
  end
281
474
  end
282
475
  end