Animating a Circle
Tutorial Info
- Author: madskjeldgaard
- Required Knowledge:
- Reading Time: 10 minutes
Moving a circle about on the screen
In this tutorial we will cover the basics of moving a shape around in the window of a nannou app.
Let's start by making a simple program which draws a circle in the center of the screen.
We will be using the barebones app from Anatomy of a nannou app as a starting point for this.
Update the view function of your nannou-app to look like this:
#![allow(dead_code)] #![allow(unused_imports)] extern crate nannou; use nannou::prelude::*; struct Model{} fn main() { nannou::app(model) .event(event) .simple_window(view) .run(); } fn model(_app: &App) -> Model { Model {} } fn event(_app: &App, _model: &mut Model, _event: Event) { } fn view(app: &App, _model: &Model, frame: Frame) { // Prepare to draw. let draw = app.draw(); // Clear the background to purple. draw.background().color(PLUM); // Draw a blue ellipse with a radius of 10 at the (x,y) coordinates of (0.0, 0.0) draw.ellipse().color(STEELBLUE).x_y(0.0,0.0); draw.to_frame(app, &frame).unwrap(); }
Adding movement
Let's now add some movement to our circle to give it a bit of life.
To do this, we will make use of the ever wonderful sinewave.
These can be generated in nannou by taking the progressed time of the application and feeding it to a sine function.
#![allow(unreachable_code, unused_variables)] use nannou::prelude::*; fn main() { let app: App = unimplemented!(); let sine = app.time.sin(); }
Let's make another one but at half the speed by dividing the time value by two
#![allow(unreachable_code, unused_variables)] use nannou::prelude::*; fn main() { let app: App = unimplemented!(); let slowersine = (app.time / 2.0).sin(); }
Now that we have two functions generating nice, smooth wave movements, let's use them to control our little circle.
If we put these values directly in the ellipse's .x_y()
-method we would not see much movement. That's because the sine waves generate values between -1.0 and 1.0 and the coordinates expect a pixel position.
But how wide is our window ? To get a precise idea of this, we can use a handy method called window_rect which is available in the app
variable.
#![allow(unreachable_code, unused_variables)] use nannou::prelude::*; fn main() { let app: App = unimplemented!(); let boundary = app.window_rect(); }
This will give us the boundary of the window as a handy Rect
. This is a struct that responds to tons of useful methods that we can use to define the minimum and maximum values of our x and y coordinates respectively to constrain the movements of our circle.
The minimum x value is thus available as:
#![allow(unreachable_code, unused_variables)] use nannou::prelude::*; fn main() { let boundary: geom::Rect = unimplemented!(); boundary.left(); }
And the maximum x value is
#![allow(unreachable_code, unused_variables)] use nannou::prelude::*; fn main() { let boundary: geom::Rect = unimplemented!(); boundary.right(); }
The minimum y value is
#![allow(unreachable_code, unused_variables)] use nannou::prelude::*; fn main() { let boundary: geom::Rect = unimplemented!(); boundary.bottom(); }
And the maximum y value is
#![allow(unreachable_code, unused_variables)] use nannou::prelude::*; fn main() { let boundary: geom::Rect = unimplemented!(); boundary.top(); }
Mapping values to a range
Using these values, we can map our sine
and slowersine
values to ranges of values that are within the boundary of our window. To do this, we will use the map_range function available in nannou.
The map_range
function takes 5 arguments: val
, in_min
, in_max
, out_min
, out_max
. The val
here is our sinewaves which has a minimum value of -1.0 and a maximum value of 1.0. For the x-coordinate, we then map it to a range of values between the leftmost point and the rightmost point.
#![allow(unreachable_code, unused_variables)] use nannou::prelude::*; fn main() { let app: App = unimplemented!(); let sine = app.time.sin(); let boundary = app.window_rect(); let x = map_range(sine, -1.0, 1.0, boundary.left(), boundary.right()); }
And then the same for the y value but using the slowersine
variable.
#![allow(unreachable_code, unused_variables)] use nannou::prelude::*; fn main() { let app: App = unimplemented!(); let sine = app.time.sin(); let slowersine = (app.time / 2.0).sin(); let boundary = app.window_rect(); let y = map_range(slowersine, -1.0, 1.0, boundary.bottom(), boundary.top()); }
The only thing left to do now is to put this into the arguments of our circle-drawing function.
#![allow(unreachable_code, unused_variables)] use nannou::prelude::*; fn main() { let app: App = unimplemented!(); let draw = app.draw(); let sine = app.time.sin(); let slowersine = (app.time / 2.0).sin(); let boundary = app.window_rect(); let x = map_range(sine, -1.0, 1.0, boundary.left(), boundary.right()); let y = map_range(slowersine, -1.0, 1.0, boundary.bottom(), boundary.top()); draw.ellipse().color(STEELBLUE).x_y(x, y); }
Your updated view
function should now look something like this:
#![allow(dead_code, unreachable_code, unused_variables)] use nannou::prelude::*; struct Model{} fn main() {} fn view(app: &App, _model: &Model, frame: Frame) { // Prepare to draw. let draw = app.draw(); // Generate sine wave data based on the time of the app let sine = app.time.sin(); let slowersine = (app.time / 2.0).sin(); // Get boundary of the window (to constrain the movements of our circle) let boundary = app.window_rect(); // Map the sine wave functions to ranges between the boundaries of the window let x = map_range(sine, -1.0, 1.0, boundary.left(), boundary.right()); let y = map_range(slowersine, -1.0, 1.0, boundary.bottom(), boundary.top()); // Clear the background to purple. draw.background().color(PLUM); // Draw a blue ellipse at the x/y coordinates 0.0, 0.0 draw.ellipse().color(STEELBLUE).x_y(x, y); draw.to_frame(app, &frame).unwrap(); }