We assume that you already have the Rust toolchain installed and got it working with your basic "Hello, World!" program, by following these instructions.
Just to make sure let's take that "Hello, World!" a spin again:
- Open this repo as a folder in VSCode.
- Open a terminal window and run this command:
cargo run --bin hello
You should see something like this printed in the terminal window:
[eirik@kodeworks rust-nannou-workshop]$ cargo run --bin hello
Finished dev [unoptimized + debuginfo] target(s) in 0.19s
Running `target/debug/hello`
Hello, World!
Also, make sure that you have the rust-analyzer extension installed, which helps you a lot while coding.
⚠️ There is also a deprecated extension called Rust that you should not install.⚠️
🎉 We have Rust installed and are ready to get those graphics onto the screen!
There are two types of Nannou setups: sketches and apps.
- sketches is the simplest to set up and are mostly used for quick or simple experiments, where you don't need much state management, interaction with MIDI, audio etc.
- apps is what you would use for a more full fledged application. This is the type of setup we will use for this workshop.
If you want more explanations of the differences between apps and sketches you can read the Nannou guide chapter Basics - Sketch vs App.
The setup of a basic Nannou application involves building an application instance, initializing a model, creating a window, and attaching functions for updating the state and the view.
To make this ordeal a bit easier we have included a basic app at /basic_app/src/main.rs for you in this repo in the /basic_app
directory.
📎 When writing file paths in these documents, a leading slash means the repo root, i.e. the /basic_app
directory is stored on the same level as folders /part1
, /part2
, etc.
To run this basic app you can run this command in the terminal window:
cargo run --bin basic_app
The program will start compiling, and if you haven't compiled a Nannou program before it is going to download some crates and it may take some time. But when it is done a window with a cyan background and a pink circle is going to pop up on the screen, hopefully looking something like this:
The output from running cargo run --bin basic_app
If you are seeing a similar image in a window on your computer now, we have Nannou up and running.
🎉 We have compiled our first Nannou app!
We'll start going through the exercises now that we have set up the things we need to start experimenting. If you want to know more about how a basic Nannou app is structured you can open the /basic_app/src/main.rs file and read through the comments in that file. It will give some insights into what goes where, and some tips about the syntax and structure. You can now choose to follow the route step-by-step, jump ahead to whatever you find interesting in the workshop materials, or check out other examples that you find in the Sources of Inspiration document.
In these exercises we will gradually implement simple version of an computer art work from the 1960s by John Whitney, as seen in this Youtube-video
We'll build it in byte sized pieces and pick up some Rust specific topics along the way. If you find any of the exercises difficult, feel free to just look at the suggestion and move along. You can always come back to the material at a later time if you feel like it.
We start with a basic app setup for a Nannou program.
In the exercises for Part 1, we will work with the file part1/src/main.rs
.
This file is the same file as the basic_app
we just tested, but without all the comments.
We have also removed the line where the windows size was set, so that we now get the default window size.
You can open this file in your editor and make the changes to that file as we go along.
To run your code you can run this command the repo root in a terminal window:
cargo run --bin part1
For each exercise there is a corresponding suggestion for solution in the part1/src/suggestions
directory.
The suggestions for exercises are named after the exercise name.
So for exercise 1-A, the suggestion is found in the file part1/src/suggestion/part1a.rs
.
To run the suggestion file you can run this in the terminal:
cargo run --bin part1a
Exercises 1.A to 1.I are based on basic knowledge of how to move objects to different positions on the window, set the colors, translating the drawing context, and how to rotate objects using The Sons of Trigonomic Thrønder, sin()
and cos()
.
If this is all too familiar to you, you can take the Rocket Option™, and land on Exercise 1.J, where we will start actually building the Whitney piece.
After the exercises in Part 1 you will have a basic understanding of the following:
- 🎯 Minimal setup for Nannou app.
- 🎯 Draw a colored background
- 🎯 Draw ellipses, rectangles, triangles, and lines.
- 🎯 Specify and control the positions and colors for the shapes.
- 🎯 Understand the coordinate system for nannou; how position values correspond to location on the windows.
- 🎯 Specify relative positions for groups of shapes.
- 🎯 Animate parameters for the shapes relative to time.
We are going to set different colors for our background and our circle. In Nannou there are multiple ways to define colors. The easiest method is to use color constants
📎 The color constants link above points to the Cheat Sheet in texts/cheat-sheet.md
This document is a selection of tips, snippets, and info that can be helpful during the workshop.
📜 Define one variable for the circle color and use that to set the color of the ellipse.In the suggestion files we will continue to use cyan for our background, and magenta for the ellipse, but you can choose any colors you like.
💡 This is how you define a variable in Rust:
let x = 42; //a immutable variable containing a
🙈 Spoiler alert! Here is one way to do it:
fn view(app: &App, _model: &Model, frame: Frame){
//[...snip...]
draw.background()
.color(CYAN);
let circle_color = MAGENTA;
draw.ellipse()
.color(circle_color);
//[...snip...]
}
📎 The suggestions for solutions will only show the relevant changes to the files. Surrounding areas of the changes made are usually snipped out, which you can see with //[...snip...]
comments. Often but not always we will include the function signature for the areas that were changed. To see the whole suggestions you can open the corresponding file from its suggestion
directory.
🎉 You have changed the color of a graphic object in Nannou!
You can decide how high and low the ball will move.
💡 Tip: You can use the app time to move the circle.
app.time; //Gives the elapsed time in seconds
app.time.sin(); // You can use the sine function to generate a looping value
// between -1.0 and 1.0.
app.time.sin() * 100.0; //Multiplying the output from sin() maps the range
// -1.0 - 1.0 to -100.0 - 100.0
💡 Tip: Methods for setting positions of drawings
The type that is returned from theellipse()
methods is an instance of Drawing
.
In the type-hint that rust-analyzer
gives it says that it is a Drawing
i.e. it is a drawing-in-progress of a geometric _primitive_, specifically an ellipse in our case.
📎 You can of course use Nannou to draw other things than ellipses. In the cheat sheet is a list of other primitives
draw.ellipse()
.y(-100.0); // Set the Y coordinate to 100 pixels below the center of the screen.
💡 Tip: About screen coordinates
In Nannou, the window coordinates has origo, i.e. point (x: 0.0 y: 0.0), at the center of the window. If a window has size 1280x720, the right edge has X coordinate at 640 and Y coordinate at -640.🙈 Spoiler alert! A possible solution:
draw.ellipse()
.y(app.time.sin() * 200.0 )
.color(circle_color);
🎉 You have animated the position a circle in Nannou, in relation to time!
📜 Make the circle move all the way from the top of the screen to the bottom.
💡 Tip: You can use the data from the App
instance to get the screen height.
let win = app.window_rect();
win.w(); //total width
win.h(); //total height
win.top(); //top of the window
win.botton(); //bottom of the window
win.right(); //right edge of the window
win.left(); //left edge of the window
🙈 Spoiler alert! A possible solution:
fn view(app: &App, _model: &Model, frame: Frame){
//[...snip...]
let win = app.window_rect();
//The output range from the `sin()` function is -1.0 - 1.0
//Since the window coordinates for nannou has x:0.0,y:0.0 as the center of the window,
// converting the range -1.0 to 1.0 to the full height of the window is as simple as
// multiplying with half the window height.
let y_pos = app.time.sin() * win.h() * 0.5;
//Use the draw instance to draw an ellipse.
draw.ellipse()
.y(y_pos)
.color(circle_color);
//[...snip...]
}
🎉 Your circle is now swinging from the bottom to the top of the window.
📜 Convert the movement range to having the top at 2:3 of the window height, and the bottom at 1:4 of the screen height.
💡 There is a Nannou function that makes it really easy to convert from one number range to another:
let a = 0.5;
let b = map_range(a, 0.0, 1.0, 10.0, 20.0); // => 15.0
🙈 Spoiler alert! A possible solution:
//Use the top and bottom values from the window rect to make it simpler to convert value
// ranges.
const TOP_RATIO: f32 = 2.0/3.0; // The ratio need only be calculate once, so we use const.
const BOTTOM_RATIO: f32 = 1.0/4.0; // If we had used `let top_ratio = 2.0/3.0` the Rust
// compiler probably would have optimized this into a
// constant anyway. The default immutability of variables in
// Rust makes many it easier for the compiler to figure out
// ...But hey, can't a programmer have a bit of fun with
// unnecessary optimization from time to time..?
let y_top = map_range(TOP_RATIO, 0.0, 1.0, win.bottom(), win.top());
let y_bottom = map_range(BOTTOM_RATIO, 0.0, 1.0, win.bottom(), win.top());
let y_pos = map_range(app.time.sin(), -1.0, 1.0, y_bottom, y_top);
🎉 Your now have ninja-level precision over the circle's vertical position.
🎉 You can map numbers from one number range to another, using the map_range
function.
📜 Change the movement of the circle to do move in a circle around the center. The circle should go to the edges of the window both on the left/right edges and the top/bottom edges.
💡 To travel in a circular motion we can combine sin()
with its co(s)mpanion... They have a complex relationship.
//We can combine `sin` and `cos` to create movement in circles.
//This will rotate the circle in an even circle around origo, with a distance of 200.0 pixels.
draw.ellipse()
.y(app.time.sin() * 200.0)
.x(app.time.cos() * 200.0)
.color(MAGENTA);
🙈 Spoiler alert! A possible solution:
let win = app.window_rect();
let y_pos = map_range(app.time.sin(), -1.0, 1.0, win.bottom(), win.top());
let x_pos = map_range(app.time.cos(), -1.0, 1.0, win.left(), win.right());
draw.ellipse()
.y(y_pos)
.x(x_pos)
.color(circle_color);
🎉 You can now rotate things in Nannou! That will sure come in handy later.
📜 Add another circle to the drawings. The new circle should be half the size and travel twice as fast, in the opposite direction. Choose another nice color for the second circle. Both circles should touch the edge of the screen, i.e. nothing of the circles should go outside the window.
💡 You can find function for setting circle/ellipse properties, and much more, in the cheat sheet.
💡 What do you have to change for time to travel faster?...
The app.time
part is what's "moving the time", before it is evaluated as argument for the sin()
function.
Hence, if we multiply the time with 2.0, we travel twice as fast... through time.
💡 What do you have to change to change direction of rotation?...
You can negate either one of the sin()
or cos()
parts to change the direction.
🙈 Spoiler alert! A possible solution:
let circle_radius_a = 50.0;
let pos_a = vec2(
map_range(app.time.sin(), -1.0, 1.0,
win.left() + circle_radius_a,
win.right() - circle_radius_a),
map_range(app.time.cos(), -1.0, 1.0,
win.bottom() + circle_radius_a,
win.top() - circle_radius_a));
let circle_radius_b = circle_radius_a / 2.0;
let pos_b = vec2(
//Multiply the time before calling the sin() function doubles the frequency.
map_range((2.0 * app.time).sin(), -1.0, 1.0,
win.left() + circle_radius_b,
win.right() - circle_radius_b ),
//Negating either the sin() or cos() part to make it travel in opposite direction
map_range(-(2.0 * app.time).cos(), -1.0, 1.0,
win.bottom() + circle_radius_b,
win.top() - circle_radius_b ));
draw.ellipse()
.xy(pos_a)
.radius(circle_radius_a)
.color(MAGENTA);
draw.ellipse()
.xy(pos_b)
.radius(circle_radius_b)
.color(ORANGE);
🎉 You can now rotate multiple things in relation to the window dimensions!
We have two circles.
📜 In this exercise we will change the movement of circles circle_a
and circle_b
like this:
- Circle
circle_a
will move back and forth along the x axis, at the vertical center of the window.- This means that
circle_a
will go from the left to the right, while the y coordinate is 0.0.
- This means that
- Circle
circle_b
will circle aroundcircle_a
💡 We don't have to calculate that much. It is actually much easier to change the position of the drawing context itself:
We have already seen that we can change the position of an ellipse using thexy()
function.
Well, the same type of function can be used for the draw
instance as well:
let draw = draw.xy(vec2(-100.0, 20.0)); // set the position using a Vec2
let draw = draw.x_y(-100.0, 20.0); // set the position using x and y as separate values
More info on this in the cheat sheet
🙈 Spoiler alert! A possible solution:
let circle_radius_a = 50.0;
let pos_a = vec2( (app.time * 0.5).sin() * win.w() * 0.25, 0.0);
draw.ellipse()
.xy(pos_a)
.radius(circle_radius_a)
.color(MAGENTA);
//Instead of moving the next circle, we move the whole draw instance to a new position.
//This effectively moves the center for the drawing to the position of circle `a`, thus drawing
// circle `b` in relation to that point becomes much easier.
let draw = draw.xy(pos_a);
let circle_radius_b = circle_radius_a / 2.0;
let pos_b = vec2(
(3.0 * app.time).sin() * 100.0,
(3.0 * app.time).cos() * 100.0,
);
draw.ellipse()
.xy(pos_b)
.radius(circle_radius_b)
.color(ORANGE);
🎉 Good job! You can now rotate a thing relative to another things' position.
Wouldn't it be cool if we had a small skyscraper on the ground? 🤔
📜 Let's add a building with 4 floors, two width units on each floor. Start by making a rectangle for the building side. A simple rectangle should be good. How do we draw rectangles in Nannou you say?
💡 Let's see if the docs can help us...
This text goes into detail on how you can navigate the documentation for nannou to find out what methods are defined for the different Primitives in Nannou. This may be more interesting for you if you are eager to learn more about concepts in the Rust language and the documentation system. If that is not interesting for you, feel free to jump ahead for the next tip that uncovers the magical methods for drawing rectangles.
💡 How to draw a rectangle
In theDrawing
module we have the method rect()
that returns a drawing-in-progress-of-a-rectangle, or Drawing<Rect>
as it called in Rust-speak.
The Drawing<Rect>
support a lot of interesting methods, so here is a short selection.
📎 If you want to dig deeper into how to find these methods in the documentation and source yourself, read the text linked to in the previous tip if you haven't already done so. There is also some info about this listed in the Cheat Sheet
.xy(); // set position using a Vec2
.x_y(); // set position using x and y as separate arguments
.wh(); // set the width and height using a Vec2
.w_h(); // set the width and height as separate argument
.rotate(); // rotate by radians
.rgb(); // color in RGB
.no_fill(); // don't fill the rectangle with a color
.stroke_color(); // set the color for the stroke
Let's place the building rectangle at the center bottom of the window.
🙈 Here is one way to do it:
//Draw the basic structure of the building
let unit_size = 50.0;
draw.rect()
.x_y(0.0, win.bottom())
.height(unit_size * 4.0) //4 floors
.width(unit_size * 2.0) // 2 windows per floor
.color(GREY);
🎉 Great stuff, you've built a building, using pixels!
if you used the suggestions so far, running the code above gives a strange result: The building moves along with circle_a
.
A moving building...clearly that's not what we wanted.
📜 Can you figure out why this happened?
💡 Here is a hint about what going on:
What happened to the drawing context when we changed its position using .xy(pos_a)
?
Have a look at the method signature:
pub fn xy(&self, v: Vec2) -> Self
and the call site:
let draw = draw.xy(pos_a);
💡 Here is another hint on how to fix it:
The result ofdraw.xy(pos_a)
is assigned to our draw
variable, which caused lasting change to the position of the drawing context.
We need to make this temporary.
🙈 Here is a way to fix it:
pub fn xy(&self, v: Vec2) -> Self
The method signature for .xy()
tells us that we call the object as a read-only reference, because of the first &self
parameter.
This means that we cannot change the self object that the method is called on, but the .xy()
method returns a new version with a new drawing context centered around pos_a
in our case.
The -> Self
part tells you that what gets returned from the function is an owned type, it has no &
.
You can safely assume that a new version of the Draw
type is returned, since there is no way for an owned instance of the original Draw
instance to enter the function.
When we use this new version of Draw
to assign to the draw
variable, the old draw
instance is replaced.
let draw = draw.xy(pos_a);
If we don't want to replace the existing Draw
instance we can temporarily move the center to another position by calling the .xy()
method inline, right before we start drawing our rectangle:
draw.xy(pos_a) // move the draw context to a different position temporarily
.ellipse()
.xy(pos_b)
.radius(circle_radius_b)
.color(ORANGE);
//Draw the basic structure of the building
let unit_size = 50.0;
draw.rect()
.x_y(0.0, win.bottom()) //This time the origin is in the center of the window
.height(unit_size * 4.0) //4 floors
.width(unit_size * 2.0) // 2 windows per floor
.color(GREY);
//Just a triangle to check that it draw smack in the center, i.e. it hasn't been
// affected by changing the position of the drawing context in the rect drawing
// section above.
draw.tri()
.x_y(0.0, 0.0)
.color(BLUE);
🎉 Congratulations! You have fixed a very nasty building-moving bug.
🎉 You understand the concept of moving the position of the Draw
context both temporarily and ...not-so-temporarily.
📜 We have another bug in our code! 😱
You may have noticed that half of the building is outside the window.
This is because the position of the rectangle is defined in relation to its center.
So we could offset the position of the rectangle by half the height of the rectangle, but there is another more expressive way to place the rectangle: Using the nannou::geom::Rect
module.
This module offers a lot of methods relating to describing the geometrical properties of a rectangle.
💡 But didn't we just use the rect
thing for just drawing? surely, the rect
thing can't do a lot of other fancy geometrical tricks as well...? :
In programming, a general rule is that you try to keep your abstraction as well defined according to a given purpose as possible. It would not be a good design if we designed a rectangle abstraction that defines all its geometrical properties as well as how it is going to draw itself on the screen, and send an email to your followers on social media while doing so. That would be to break the single-responsibility principle.
You can rest assured (but possibly confused) that the rect
we use for drawing and rect
used for geometrical properties are not the same.
They live in different places in the namespace of nannou.
- The drawing type is
nannou::draw::primitive::rect::Rect
- The geometric type is
nannou::geom::Rect
The nodes in the namespace are separated by double colons, ::
.
💡 Placing a nannou::geom::Rect
Here is a selection of nannou::geom::Rect
methods:
use nannou::geom::Rect; //Bring it into current namespace so that only need `Rect`
let r = Rect::from_x_y_w_h(10.0, -100.0, 100.0, 100.0);
let r2 = Rect::from_w_h(50.0, 50.0).mid_top_of(r); // place second rect on the middle of the top of r
r2.wh(); //get the width and height as a Vec2
r2.xy(); //get the position as a Vec2
🙈 Here is a way to fix the second bug, using nannou::geom::Rect
//Draw the basic structure of the building
let unit_size = 50.0;
let num_floors = 4;
let windows_per_floor = 2;
//Convert the i32 primitive type to f32 using the
// `as` keyword. This way of converting types is only
// applicable for primitive types such as f32, u32
// etc.
let building_rect = Rect::from_w_h(
unit_size * windows_per_floor as f32,
unit_size * num_floors as f32
)
.mid_bottom_of(win);
draw.rect()
.xy(building_rect.xy()) // use the rect properties directly on the drawing
.wh(building_rect.wh())
.color(GREY);
🎉 You have more insight into placing rectangles, using the
nannou::geom::Rect
API.
🎉 You have seen Rect
's from different namespaces doing what each of them do best.
🎉 You have seen conversion between primitive values using the as
keyword.
🎉 You are becoming a danger to those poor building-placement bugs!
Ok, so we have come a good way along now. But what do all these circles and rectangles have to do with the John Whitney piece we are aiming to implement?
Fear not, because we are now going to start getting those dancin' lines onto the window.💃🕺
The code we have used for changing a scalar value in relation to the application time is the following:
(app.time * 2.0 + (0.5 * PI)).sin();
This expression has started to become used repeatedly around our code, and we get a feeling that it could be nice to create an abstraction for this. It would be an advantage to encapsulate this idea in a separate function that abstracts the idea of a sine oscillator more clearly, with the added advantage of being easier to reuse and to combine with other oscillators.
We have made some oscillator abstractions for controlling values for you to use in the following exercises.
These are found in the common/src/lib.rs
file.
You can see different oscillators in action by running the common/src/osc_functions.rs
file with this command:
cargo run --bin osc_functions
📜 Using the file /part1/src/whitney.rs
as a starting point, make the ellipse travel in circles using the Osc::sin_cos
function.
Also, add a line between the two ellipses. Another thing you can try is to give the two oscillator slightly different frequencies, so that go in and out of phase with eachother.
You run the code with the command:
cargo run --bin whitney
💡 Importing a module
We are going to use theOsc
struct from the common
module in our workspace.
To import the common
crate in out workbench you use the use
keyword:
use common::Osc;
We can now use the Osc
functions with e.g. Osc::sin
.
💡 Rotating oscillator
As we know from earlier, the combination ofsin
and cos
gives us a circular movement.
The Osc
struct has a method called sin_cos
that returns a Vec2
which is nice, because you can just put the returned value into any .xy()
method.
.xy(Osc::sin_cos(t, 0.25, 0.0) * 100.0) // multiplying the whole vector with 100.0 makes it move
// in a radius of 100.0 around its origo
We can now use the Osc
functions with e.g. Osc::sin
.
💡 Translating a circular movement
When you use the+
operator on a Vec2
you are actually moving the center position of the circular movement.
//Adding a `Vec2{x: 100.0, y: -100.0}` means shifting the center of rotation 100.0 pixels right
// and 100 pixels down.
.xy(Osc::sin_cos(t, 0.25, 0.0) * 100.0 + vec2(100.0, -100.0))
🙈 Two ellipses moving in circles with a connecting line
let a = Osc::sin_cos(t, 0.25, 0.0) * 100.0 + vec2(0.0, win.top() * 0.5);
let b = Osc::sin_cos(t, 0.3, 0.0) * 100.0 + vec2(0.0, win.bottom() * 0.5);
draw.ellipse()
.xy(a)
.radius(10.0)
.color(PINK);
draw.ellipse()
.xy(b)
.radius(10.0)
.color(PINK);
draw.line()
.points(a, b)
.stroke_weight(1.5)
.color(PINK);
You can run the suggestion with:
cargo run --bin part1i
🎉 You have imported a module to your code!
🎉 You have used a complex sine oscillator to make a circular movement!
In the Osc
struct we have the parametric
oscillator that can create more complex movements that can have a bit more variation.
Let's us this to control the movement of the points:
let aa = vec2(
Osc::parametric(t, 0.25, 0.0, 1.02, 0.5),
Osc::parametric(t, 0.3, Osc::PHASE90, 1.87, 0.3),
);
let bb = vec2(
Osc::parametric(t, 0.35, 0.0, 1.32, 0.9),
Osc::parametric(t, 0.5, Osc::PHASE90, 0.8, 0.1),
);
let a = aa * 300.0;
let b = bb * 300.0;
📜 Experiment with the settings for the parametric oscillators to se what makes for an interesting movement.
If you feel adventurous you can try to mix in, replace with, any combination of the functions from the Osc
module.
🙈 The correct answer...
...is whatever you find the most interesting.🎉 You are making digital art!
In the Whitney piece we see that there are multiple lines following the same path.
Can you think of a way to get this effect in our code?
📜 Make a tail of for examples 10 lines, that follow after the first line.
💡 Making multiple instances
A good ole'for
loop could do the trick.
The simplest way to loop over values in Rust is this:
for i in 0..10 {
println!("index: {}", i);
}
You can use the index value to offset the time argument for the Osc::parametric
function.
🙈 Iteratin'... u hatin'
let speed = 0.5; // Added a main speed control variable
for i in 0..10 { // 0..10 is the notation for a range in Rust
let tail_length = 0.1;
let tt = t + (i as f32 * tail_length); // offset the time with the scaled index value
let aa = vec2(
Osc::parametric(tt, 0.25 * speed, 0.0, 1.02, 0.5),
Osc::parametric(tt, 0.3 * speed, Osc::PHASE90, 1.87, 0.3),
);
let bb = vec2(
Osc::parametric(tt, 0.35 * speed, 0.0, 1.32, 0.9),
Osc::parametric(tt, 0.5 * speed, Osc::PHASE90, 0.8, 0.1),
);
let a = aa * 300.0;
let b = bb * 300.0;
draw.line()
.points(a, b)
.stroke_weight(1.5)
.color(PINK);
}
🎉 You are using a for-loop to create multiple lines that move along the same path!
In the trend of making multiple instances, we would like to have two trails of moving lines.
📜 Modify the code so that we can draw two different trails of lines. Make the line trails have different movement paths and color.
💡 Before we fire up the copy-pastamachine..🏜 Let's try to keep it DRY
This is a good chance to make the drawing of line trails into a separate function, so that we can call it with varying arguments.
May I suggest a function signature for you:
fn draw_line_trail(time: f32, draw: &Draw, num_lines: i32, speed: f32, tail_length: f32, color: Srgb) {...}
You can ...try to call it with this code (and see what happens...):
draw_line_trail(t, &draw, 10, 0.5, 0.1, PINK);
💡 Is the computer saying no to your color PINK
?
The Rust compiler is as may have heard, quite strict.
The color the Srgb
type expects is based on f32
types, whereas the type that is returned from a color constant such as PINK
is Rgb
.
The u8
is the relevant part of this.
But it is easy to convert between color type and color spaces in Nannou:
//We have to call .into_format() on the color constant to convert it into a f32 based color.
draw_line_trail(t, &draw, 10, 0.5, 0.1, PINK.into_format());
For more info about colors in Nannou there is stuff to read in the cheat sheet.
🙈 A suggestion for function and calling code.
The function can be something like this:fn draw_line_trail(time: f32, draw: &Draw, num_lines: i32, speed: f32, tail_length: f32, color: Srgb) {
for i in 0..num_lines { // 0..10 is the notation for a range in Rust
let tt = time + (i as f32 * tail_length); // offset the time with the scaled index value
let aa = vec2(
Osc::parametric(tt, 0.25 * speed, 0.0, 1.02, 0.5),
Osc::parametric(tt, 0.3 * speed, Osc::PHASE90, 1.87, 0.3),
);
let bb = vec2(
Osc::parametric(tt, 0.35 * speed, 0.0, 1.32, 0.9),
Osc::parametric(tt, 0.5 * speed, Osc::PHASE90, 0.8, 0.1),
);
let a = aa * 300.0;
let b = bb * 300.0;
draw.line()
.points(a, b)
.stroke_weight(1.5)
.color(color);
}
}
And you could call it like this:
//We have to call .into_format() on the color constant to convert it into a f32 based color.
draw_line_trail(t, &draw, 10, 0.5, 0.1, PINK.into_format());
draw_line_trail(t + 100.0, &draw, 13, 0.4, 0.15, rgb(0.1, 0.1, 0.9));
🎉 You are using your own specially made function to draw a trailing lines duet!
Good job! Take a break and a snack, you deserve it.
Next up: Part 2: Veiled kaleidoscope 🚂