-
Notifications
You must be signed in to change notification settings - Fork 0
/
README.Rmd
288 lines (216 loc) · 11.4 KB
/
README.Rmd
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
---
output: github_document
---
<!-- README.md is generated from README.Rmd. Please edit that file -->
```{r, include = FALSE}
knitr::opts_chunk$set(
collapse = TRUE,
comment = "#>",
fig.path = "man/figures/README-",
out.width = "100%"
)
library(dplyr)
library(eventloop)
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Generate the pkgdown documentation
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
if (FALSE) {
pkgdown::build_site(
# devel = TRUE,
override = list(destination = "../coolbutuseless.github.io/package/eventloop")
)
}
```
# eventloop <img src="man/figures/eventloop-logo.png" align="right" width="230"/>
<!-- badges: start -->
![](https://img.shields.io/badge/cool-useless-green.svg)
![](https://img.shields.io/badge/dependencies-zero-blue.svg)
[![R-CMD-check](https://github.com/coolbutuseless/eventloop/workflows/R-CMD-check/badge.svg)](https://github.com/coolbutuseless/eventloop/actions)
<!-- badges: end -->
The `{eventloop}` package provides a framework for rendering interactive graphics
and handling mouse+keyboard events from the user at speeds fast enough to be
considered interesting for games and other realtime applications.
`{eventloop}` is a wrapper around the built-in event handling available in
base R as part of `{grDevices}`, but presents it in a more palatable format
with some enhanced features.
The `{eventloop}` package will:
* Initiate an `x11()` window with monitoring for keyboard & mouse events.
* Capture events as they happen such that they will be available to the user-supplied function.
* Coordinate the setup of the user-defined callback function to be run
continually with access to the latest event information presented as the
function arguments.
* Optionally limit the frequency of screen updates by maintaining a frame-rate
specified by the user.
At each call, the user's function processes events and updates the display.
## What's in the box
* `run_loop()` takes a user-specified function and calls it continuously
within an event-driven loop.
## Supported Platforms
```{r echo=FALSE}
dplyr::tibble(
System = c('macOS', '*nix', 'Windows'),
"x11() device has 'onIdle()' event callback" = c('✅Yes', '✅Yes', '❌ No'),
"System supported in {eventloop}" = c('✅Yes', '✅Yes', '❌ No')
) %>%
knitr::kable()
```
#### Notes:
* windows `x11()` device does not support `onIdle` callback and hence this
package does not work on windows
* macOS `x11()` support is via [Xquartz](https://www.xquartz.org/). Xquartz may slow to
a crawl after running for a while. You will need to logout-and-log-back in,
or restart your machine to regain full speed. This bug may be in Xquartz
or how x11() support is implemented in macOS - I'm really not sure.
## Installation
Pre-requisites
* Unix-like systems
* R compiled with X11() support
* macOS
* [Xquartz](https://www.xquartz.org/) installed
* windows
* Sorry, but R on windows does not support features needed for this package
``` r
# install.package('remotes')
remotes::install_github('coolbutuseless/eventloop')
```
## Example - Basic Drawing app
This is a basic application which lets the user draw in
a window using the mouse.
```{r, eval = FALSE}
library(grid)
library(eventloop)
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Set up the global variables which store the state of the world
# 'drawing' = Is the mouse button currently pressed?
# last_x/last_y = the last mouse position is manually saved every time
# the callback function runs.
#
# These values will be updated manually by the user in the `draw()` function
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
drawing <- FALSE
last_x <- NA
last_y <- NA
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
#' Callback function - 'draw()'
#'
#' If 'event' is not NULL, then it means that the user interacted with the
#' display.
#'
#' The following events are handled by this callback:
#' - hold mouse to set drawing mode
#' - releasing the mouse button stops drawing mode
#' - pressing SPACE clears the canvas
#'
#' Press ESC to quit.
#'
#' @param event The event from the graphics device. Is NULL when no event
#' occurred. Otherwise has `type` element set to:
#' `event$type = 'mouse_down'`
#' - an event in which a mouse button was pressed
#' - `event$button` gives the index of the button
#' `event$type = 'mouse_up'`
#' - a mouse button was released
#' `event$type = 'mouse_move'`
#' - mouse was moved
#' `event$type = 'key_press'`
#' - a key was pressed
#' - `event$str` String describing which key was pressed. See \code{grDevices::setGraphicsEventHandlers} for more information.
#' @param mouse_x,mouse_y current location of mouse within window in normalised coordinates in the range [0, 1]. If mouse is
#' not within window, this will be set to the last available coordinates
#' @param frame_num Current frame number (integer)
#' @param fps_actual,fps_target the curent framerate and the framerate specified
#' by the user
#' @param dev_width,dev_height the width and height of the output device. Note:
#' this does not cope well if you resize the window
#' @param ... any extra arguments ignored
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
draw <- function(event, mouse_x, mouse_y, ...) {
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Process events
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
if (!is.null(event)) {
if (event$type == 'mouse_down') {
drawing <<- TRUE
} else if (event$type == 'mouse_up') {
drawing <<- FALSE
last_x <<- NA
last_y <<- NA
} else if (event$type == 'key_press' && event$str == ' ') {
grid::grid.rect(gp = gpar(col=NA, fill='white')) # clear screen
}
}
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# If 'drawing' is currently TRUE, then draw a line from last known
# coordinates to current mouse coordinates
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
if (drawing) {
if (!is.na(last_x)) {
grid::grid.lines(
x = c(last_x, mouse_x),
y = c(last_y, mouse_y),
gp = gpar(col = 'black')
)
}
# Keep track of where the mouse was for the next time we draw
last_x <<- mouse_x
last_y <<- mouse_y
}
}
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Start the event loop. Press ESC to quit.
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
eventloop::run_loop(draw, fps_target = NA, double_buffer = TRUE)
```
<img src="man/figures/hello-r.gif" />
Notes:
* Every time the callback function `draw()` is executed from within the event loop, it draws a line from the last mouse position to the current mouse position.
* The position of the mouse during the previous call is saved manually using global variables.
* A boolean variable (`drawing`) is used to note whether the mouse button
is currently pressed or not. Changes to the screen only happend if `drawing == TRUE`.
## Gallery of Puzzles, Games + Applications implemented in the vignettes
**Click an image to view the code/vignette**
The linked pages contain videos of realtime screen captures which
illustrate how the interactive nature of these applications work.
All examples are written in plain R using the `{eventloop}` package.
| | |
|---|---|
| [Grid-based drawing <br/><img src="man/figures/gallery/grid-based.png" width="89%" />](https://coolbutuseless.github.io/package/eventloop/articles/drawing-grid.html) | [Line-based drawing <br/><img src="man/figures/gallery/line-based.png" width="89%" />](https://coolbutuseless.github.io/package/eventloop/articles/drawing-lines.html) |
| [Streaming plot data <br/><img src="man/figures/gallery/plot-stream.png" width="89%" />](https://coolbutuseless.github.io/package/eventloop/articles/stream-plotting.html) | [Game of Life <br/><img src="man/figures/gallery/game-of-life.png" width="89%" />](https://coolbutuseless.github.io/package/eventloop/articles/game-of-life.html) |
| [Asteroids<br/><img src="man/figures/gallery/asteroids.png" width="89%" />](https://coolbutuseless.github.io/package/eventloop/articles/game-asteroids.html) | [Raycast 'Wolfenstein' 3d engine <br/><img src="man/figures/gallery/raycast.png" width="89%" />](https://coolbutuseless.github.io/package/eventloop/articles/game-raycaster.html) |
| [Wordle <br/><img src="man/figures/gallery/wordle.png" width="89%" />](https://coolbutuseless.github.io/package/eventloop/articles/game-wordle.html) | [Interactive Mystery Curves <br/><img src="man/figures/gallery/mystery-curves.png" width="89%" />](https://coolbutuseless.github.io/package/eventloop/articles/verbose-example.html) |
| [Verbose example <br/><img src="man/figures/gallery/debug.png" width="89%" />](https://coolbutuseless.github.io/package/eventloop/articles/verbose-example.html) | [Particles <br/><img src="man/figures/gallery/particles.png" width="89%" />](https://coolbutuseless.github.io/package/eventloop/articles/demo-particles.html) |
## FAQ
#### Why is Windows not supported?
No graphics device on windows supports the `onIdle` callback, which is essential
to the workings of this package.
The Windows operating system supports the concept of an `onIdle` callback, but
no one has yet written this into the R graphics device on this platform.
#### Why does this run so slow on macOS sometimes?
Through some unknown combination of factors, after running `x11()` windows
on macOS for some number of times or duration, the system will slowdown from hundreds-of-frames-per-second
to just ten-frames-per-second.
This feels like a bug in either the `XQuartz()` x11 framework, or it could be
a bug within R in how it interfaces with `XQuartz()`
Note that I have not seen any slowdowns when using `x11()` devices on Linux machines.
## Tech bits: What is an event loop?
[gameprogrammingpatterns.com](https://www.gameprogrammingpatterns.com/game-loop.html)
defines an event loop (also known as a *game loop*) as follows:
A game loop runs continuously during gameplay. Each turn of the loop, it
processes user input without blocking, updates the game state, and renders
the game. It tracks the passage of time to control the rate of gameplay.
## Tech bits: How is the event loop implemented in R?
Graphics windows in R can have *event handlers* attached which instruct the
device to run a function when a certain event occurs.
When a mouse or keyboard event occurs, `{eventloop}` stores the event in
an environment for later access.
When there is no event occuring, another function is called continuously. This
function is the *'onIdle' event callback* and is only available in the `x11()`
device on macOS and *nix.
The `{eventloop}` package orchestrates the events and window information into
arguments to the user-supplied 'onIdle' function - calling this function over and over
while the event loop is running.
<img src="man/figures/event-handlers.png" />
## Acknowledgements
* R Core for developing and maintaining the language.
* CRAN maintainers, for patiently shepherding packages onto CRAN and maintaining
the repository