diff --git a/Cargo.toml b/Cargo.toml index a2b9495b..6937ad70 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -52,10 +52,6 @@ rodio = { version = "0.8", optional = true } stdweb = "0.4.9" webgl_stdweb = "0.2" -[[example]] -name = "basic" -required-features = [] - [[example]] name = "draw-geometry" required-features = [] @@ -87,3 +83,7 @@ required-features = ["complex_shapes"] [[example]] name = "stopwatch" required-features = [] + +[[example]] +name = "camera" +required-features = [] diff --git a/README.md b/README.md index 02a340de..21c0bd5b 100644 --- a/README.md +++ b/README.md @@ -63,8 +63,6 @@ fn main() { Run this with `cargo run` or, if you have the wasm32 toolchain installed, you can build for the web (instructions below). -You should see a red square in the top-left, and a green circle with a blue rectangle inside it -on the bottom-right. ## Building and Deploying a Quicksilver application @@ -87,6 +85,9 @@ files produced (found at "target/wasm32-unknown-unknown/release") and any assets If you want to test your application locally, use `cargo +nightly web start --target wasm32-unknown-unknown` and open your favorite browser to the port it provides. +## Learning Quicksilver + +Currently there isn't a book-like tutorial for Quicksilver, but there ## Optional Features diff --git a/examples/basic.rs b/examples/basic.rs deleted file mode 100644 index d3e43e22..00000000 --- a/examples/basic.rs +++ /dev/null @@ -1,22 +0,0 @@ -// The most basic example- it should just open a black window and set the window title to Hello -// world! -extern crate quicksilver; - -use quicksilver::{ - Result, - geom::Vector, - lifecycle::{Settings, State, run} -}; - -// An empty structure because we don't need to store any state -struct BlackScreen; - -impl State for BlackScreen { - fn new() -> Result { - Ok(BlackScreen) - } -} - -fn main() { - run::("Hello World", Vector::new(800, 600), Settings::default()); -} diff --git a/examples/camera.rs b/examples/camera.rs new file mode 100644 index 00000000..17017ddf --- /dev/null +++ b/examples/camera.rs @@ -0,0 +1,66 @@ +// Demonstrate adding a View to the draw-geometry example +// The camera can be controlled with the arrow keys +extern crate quicksilver; + +use quicksilver::{ + Result, + geom::{Circle, Line, Rectangle, Shape, Transform, Triangle, Vector}, + graphics::{Background::Col, Color, View}, + input::{Key}, + lifecycle::{Settings, State, Window, run}, +}; + +struct Camera { + view: Rectangle +} + +impl State for Camera { + // Initialize the struct + fn new() -> Result { + Ok(Camera { + view: Rectangle::new_sized((800, 600)) + }) + } + + fn update(&mut self, window: &mut Window) -> Result<()> { + if window.keyboard()[Key::Left].is_down() { + self.view = self.view.translate((-4, 0)); + } + if window.keyboard()[Key::Right].is_down() { + self.view = self.view.translate((4, 0)); + } + if window.keyboard()[Key::Down].is_down() { + self.view = self.view.translate((0, 4)); + } + if window.keyboard()[Key::Up].is_down() { + self.view = self.view.translate((0, -4)); + } + window.set_view(View::new(self.view)); + Ok(()) + } + + fn draw(&mut self, window: &mut Window) -> Result<()> { + window.clear(Color::WHITE)?; + window.draw(&Rectangle::new((100, 100), (32, 32)), Col(Color::BLUE)); + window.draw_ex(&Rectangle::new((400, 300), (32, 32)), Col(Color::BLUE), Transform::rotate(45), 10); + window.draw(&Circle::new((400, 300), 100), Col(Color::GREEN)); + window.draw_ex( + &Line::new((50, 80),(600, 450)).with_thickness(2.0), + Col(Color::RED), + Transform::IDENTITY, + 5 + ); + window.draw_ex( + &Triangle::new((500, 50), (450, 100), (650, 150)), + Col(Color::RED), + Transform::rotate(45) * Transform::scale((0.5, 0.5)), + 0 + ); + Ok(()) + } +} + +fn main() { + run::("Camera", Vector::new(800, 600), Settings::default()); +} + diff --git a/examples/draw-geometry.rs b/examples/draw-geometry.rs index 0d131930..b942e1db 100644 --- a/examples/draw-geometry.rs +++ b/examples/draw-geometry.rs @@ -1,4 +1,5 @@ // Draw some multi-colored geometry to the screen +// This is a good place to get a feel for the basic structure of a Quicksilver app extern crate quicksilver; use quicksilver::{ @@ -8,35 +9,53 @@ use quicksilver::{ lifecycle::{Settings, State, Window, run}, }; +// A unit struct that we're going to use to run the Quicksilver functions +// If we wanted to store persistent state, we would put it in here. struct DrawGeometry; impl State for DrawGeometry { + // Initialize the struct fn new() -> Result { Ok(DrawGeometry) } fn draw(&mut self, window: &mut Window) -> Result<()> { + // Remove any lingering artifacts from the previous frame window.clear(Color::WHITE)?; + // Draw a rectangle with a top-left corner at (100, 100) and a width and height of 32 with + // a blue background window.draw(&Rectangle::new((100, 100), (32, 32)), Col(Color::BLUE)); + // Draw another rectangle, rotated by 45 degrees, with a z-height of 10 window.draw_ex(&Rectangle::new((400, 300), (32, 32)), Col(Color::BLUE), Transform::rotate(45), 10); + // Draw a circle with its center at (400, 300) and a radius of 100, with a background of + // green window.draw(&Circle::new((400, 300), 100), Col(Color::GREEN)); + // Draw a line with a thickness of 2 pixels, a red background, + // and a z-height of 5 window.draw_ex( &Line::new((50, 80),(600, 450)).with_thickness(2.0), Col(Color::RED), Transform::IDENTITY, 5 ); + // Draw a triangle with a red background, rotated by 45 degrees, and scaled down to half + // its size window.draw_ex( &Triangle::new((500, 50), (450, 100), (650, 150)), Col(Color::RED), Transform::rotate(45) * Transform::scale((0.5, 0.5)), 0 ); + // We completed with no errors Ok(()) } } +// The main isn't that important in Quicksilver: it just serves as an entrypoint into the event +// loop fn main() { + // Run with DrawGeometry as the event handler, with a window title of 'Draw Geometry' and a + // size of (800, 600) run::("Draw Geometry", Vector::new(800, 600), Settings::default()); } diff --git a/src/lib.rs b/src/lib.rs index 9fa8ca8a..c4dd224d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -128,6 +128,8 @@ pub mod sound; pub use error::QuicksilverError as Error; pub use file::load_file; +pub mod tutorials; + /// A Result that returns either success or a Quicksilver Error pub type Result = ::std::result::Result; /// Types that represents a "future" computation, used to load assets diff --git a/src/tutorials/_01_basic.rs b/src/tutorials/_01_basic.rs new file mode 100644 index 00000000..aca3ee34 --- /dev/null +++ b/src/tutorials/_01_basic.rs @@ -0,0 +1,81 @@ +//! Our first tutorial is simple: Just create a blank window. +//! +//! Here's the full source: +//! +//! ```no_run +//! extern crate quicksilver; +//! +//! use quicksilver::{ +//! Result, +//! geom::Vector, +//! lifecycle::{State, run} +//! }; +//! +//! struct Screen; +//! +//! impl State for Screen { +//! fn new() -> Result { +//! Ok(Screen) +//! } +//! } +//! +//! fn main() { +//! run::("Hello World", Vector::new(800, 600), Default::default()); +//! } +//! ``` +//! Let's start with importing what we need from Quicksilver: +//! ```no_run +//! extern crate quicksilver; +//! +//! use quicksilver::{ +//! Result, +//! geom::Vector, +//! lifecycle::{State, run} +//! }; +//! ``` +//! +//! Quicksilver has its own `Result` type, which is just the same as `std::result::Result`. +//! We use `Vector` for anything 2-dimensional: position, speed, or size, for example. +//! `State` is the trait that defines how we handle the core loop of Quicksilver +//! `run` is the function that kicks off the core loop. +//! +//! Next we declare our `State` handler: +//! ```no_run +//! struct Screen; +//! ``` +//! It's a unit struct (a struct with no fields) because we don't need to store anything. +//! +//! Now we implement `State` for our handler: +//! +//! ```no_run +//! # use quicksilver::{ +//! # Result, +//! # geom::Vector, +//! # lifecycle::{State, run} +//! # }; +//! # struct Screen; +//! impl State for Screen { +//! fn new() -> Result { +//! Ok(Screen) +//! } +//! } +//! ``` +//! All we have to do is implement the `new` function, and Quicksilver will take care of all the other functions. +//! The other functions we could override are `draw`, `update`, and `event`, which will be covered in later tutorials. +//! ```no_run +//! # use quicksilver::{ +//! # Result, +//! # geom::Vector, +//! # lifecycle::{State, run} +//! # }; +//! # struct Screen; +//! # impl State for Screen { +//! # fn new() -> Result { +//! # Ok(Screen) +//! # } +//! # } +//! fn main() { +//! run::("Hello World", Vector::new(800, 600), Default::default()); +//! } +//! ``` +//! Lastly, we create a main that calls `run`, starting the event loop and showing our window diff --git a/src/tutorials/_02_drawing.rs b/src/tutorials/_02_drawing.rs new file mode 100644 index 00000000..2cbbadf2 --- /dev/null +++ b/src/tutorials/_02_drawing.rs @@ -0,0 +1,141 @@ +//! Creating a blank window is all well and good, but drawing something to it is even better. +//! +//! Rendering in Quicksilver usually takes the form: +//! ```no_run +//! # use quicksilver::{graphics::{Background, Drawable}, lifecycle::Window}; +//! # fn func(window: &mut Window, some_drawable: impl Drawable, some_background: Background) { +//! window.draw(&some_drawable, some_background); +//! # } +//! ``` +//! `Drawable` is a trait which allows an object to determine how to lay out some points to draw, +//! like a rectangle or a circle. A Background is what to fill those points with, like a solid +//! color or an image. For example, drawing a red rectangle with a top-left coordinate of (50, 50), +//! a width of 100, and a height of 200 would look like: +//! ```no_run +//! # use quicksilver::{geom::{Rectangle}, graphics::{Background, Color, Drawable}, lifecycle::Window}; +//! # fn func(window: &mut Window) { +//! let area = Rectangle::new((50, 50), (100, 200)); +//! let background = Background::Col(Color::RED); +//! window.draw(&area, background); +//! # } +//! ``` +//! If we wanted to switch out our rectangle for a Circle with a center at (100, 100) and a radius +//! of 50, we could do: +//! ```no_run +//! # use quicksilver::{geom::{Circle, Rectangle}, graphics::{Background, Color, Drawable}, lifecycle::Window}; +//! # fn func(window: &mut Window) { +//! let area = Circle::new((100, 100), 50); +//! let background = Background::Col(Color::RED); +//! window.draw(&area, background); +//! # } +//! ``` +//! The next step is actually integrating some drawing code into our blank window: +//! ```no_run +//! extern crate quicksilver; +//! +//! use quicksilver::{ +//! Result, +//! geom::{Rectangle, Vector}, // We'll need to import Rectangle now +//! graphics::{Background, Color}, // Also Background and Color +//! lifecycle::{State, Window, run} +//! }; +//! +//! struct Screen; +//! +//! impl State for Screen { +//! fn new() -> Result { +//! Ok(Screen) +//! } +//! +//! fn draw(&mut self, window: &mut Window) -> Result<()> { +//! // Clear the contents of the window to a white background +//! window.clear(Color::WHITE)?; +//! // Draw a red rectangle +//! window.draw(&Rectangle::new((50, 50), (100, 200)), Background::Col(Color::RED)); +//! Ok(()) +//! } +//! } +//! +//! fn main() { +//! run::("Hello World", Vector::new(800, 600), Default::default()); +//! } +//! ``` +//! We've made two changes from the previous example: imported `Rectangle`, `Background`, and +//! `Color`, as well as implementing the `draw` function. By default, `draw` will be called by the +//! host environment whenever the screen refreshes. First we clear out the window's previous +//! contents, then we draw a red rectangle. +//! +//! If we want the rectangle to be smaller, or bigger, or a different color, the code we wrote is +//! sufficient. Just tweak the input values and you could have a blue Rectangle that's twice as +//! big. But how could we do a rotation, or efficiently do a complex scaling or translation +//! operation? The answer is the `Transform` struct. If you're familiar with matrix math or linear +//! algebra, `Transform` is a 3x3 transformation matrix. If you don't know the underlying math, +//! worry not! There are 4 main ways to create a transform: +//! +//! - `Transform::IDENTITY`: Create a Transform that does nothing. When you apply this transform, +//! everything will look exactly the same +//! - `Transform::rotate(angle)`: Create a Transform that rotates counter-clockwise by a given +//! amount of degrees +//! - `Transform::translate(vector)`: Create a Transform that moves an object by a given vector +//! - `Transform::scale(vector)`: Create a Transform with a given x and y axis scale factor +//! +//! We combine Transform objects using the `*` operator, with the last transform in a chain being +//! applied first. This means that +//! ```no_run +//! # use quicksilver::geom::Transform; +//! Transform::rotate(30) * Transform::translate((0, -6)); +//! ``` +//! first translates an object up six pixels and then rotates it by 30 degrees. +//! +//! The last drawing concept for now is z-ordering. Sometimes you don't want to draw objects to the +//! screen in the order they're drawn, but with some other sorting method. Here you use z-ordering: +//! an object with a higher z value gets drawn on top of an object with a lower z value. +//! +//! If you want to use a transform or z-ordering, you need to use the more advanced draw function, +//! which takes the form: +//! ```no_run +//! # use quicksilver::{geom::{Transform}, graphics::{Background, Drawable}, lifecycle::Window}; +//! # fn func(window: &mut Window, some_drawable: impl Drawable, some_background: Background, +//! # some_transform_value: Transform, some_z_value: f32) { +//! window.draw_ex(&some_drawable, some_background, some_transform_value, some_z_value); +//! # } +//! ``` +//! Armed with Transform values, we can turn our little red rectangle into a little red diamond: +//! ```no_run +//! extern crate quicksilver; +//! +//! use quicksilver::{ +//! Result, +//! geom::{Rectangle, Transform, Vector}, // Now we need Transform +//! graphics::{Background, Color}, +//! lifecycle::{State, Window, run} +//! }; +//! +//! struct Screen; +//! +//! impl State for Screen { +//! fn new() -> Result { +//! Ok(Screen) +//! } +//! +//! fn draw(&mut self, window: &mut Window) -> Result<()> { +//! window.clear(Color::WHITE)?; +//! // Draw a red diamond +//! window.draw_ex( +//! &Rectangle::new((50, 50), (50, 50)), +//! Background::Col(Color::RED), +//! Transform::rotate(45), // Rotate by 45 degrees +//! 0 // we don't really care about the Z value +//! ); +//! Ok(()) +//! } +//! } +//! +//! fn main() { +//! run::("Hello World", Vector::new(800, 600), Default::default()); +//! } +//! ``` +//! Quicksilver gives you a number of `Drawable` objects to work with by default: `Rectangle`, +//! `Vector`, `Circle`, `Line`, and `Triangle`. Most applications will only ever need these, or +//! even just a subset of these, but you can feel free to define your own `Drawable` objects. This +//! is covered later in the `mesh` tutorial. diff --git a/src/tutorials/_03_input.rs b/src/tutorials/_03_input.rs new file mode 100644 index 00000000..1175232f --- /dev/null +++ b/src/tutorials/_03_input.rs @@ -0,0 +1,179 @@ +//! Now we can draw all manner of colorful geometry, but that's not enough for an interesting +//! application. +//! +//! If we wanted to add keyboard support to our previous example, so that the user can use the left +//! and right arrow keys to move the square back ond forth, it would look like this: +//! ```no_run +//! extern crate quicksilver; +//! +//! use quicksilver::{ +//! Result, +//! geom::{Rectangle, Vector}, +//! graphics::{Background, Color}, +//! input::Key, // We need the Key enum +//! lifecycle::{State, Window, run} +//! }; +//! +//! struct Screen { +//! position: Vector // We need to store the position as state +//! } +//! +//! impl State for Screen { +//! fn new() -> Result { +//! Ok(Screen { +//! position: Vector::new(50, 50) +//! }) +//! } +//! +//! fn update(&mut self, window: &mut Window) -> Result<()> { +//! if window.keyboard()[Key::Right].is_down() { +//! self.position.x += 2.5; +//! } +//! if window.keyboard()[Key::Left].is_down() { +//! self.position.x -= 2.5; +//! } +//! Ok(()) +//! } +//! +//! fn draw(&mut self, window: &mut Window) -> Result<()> { +//! window.clear(Color::WHITE)?; +//! window.draw(&Rectangle::new(self.position, (100, 200)), Background::Col(Color::RED)); +//! Ok(()) +//! } +//! } +//! +//! fn main() { +//! run::("Hello World", Vector::new(800, 600), Default::default()); +//! } +//! ``` +//! Now we have very basic keyboard input controls. Every frame that the right arrow is held down, +//! the box will move 2.5 pixels to the right, and the same for left. +//! +//! The input API generally follows this principal: an input source is indexed by a button enum, +//! and returns a `ButtonState` enum. A button state can be `Pressed`, `Held`, `Released` or +//! `NotPressed`, and a convenience method `is_down` checks if the button is either pressed or +//! held. +//! +//! If we wanted to give the user more freedom, and allow them to use the mouse buttons or gamepad triggers instead of the arrow +//! keys, we could do that fairly easily: +//! ```no_run +//! extern crate quicksilver; +//! +//! use quicksilver::{ +//! Result, +//! geom::{Rectangle, Vector}, +//! graphics::{Background, Color}, +//! input::{GamepadButton, Key, MouseButton}, // We need the mouse and gamepad buttons +//! lifecycle::{State, Window, run} +//! }; +//! +//! struct Screen { +//! position: Vector // We need to store the position as state +//! } +//! +//! impl State for Screen { +//! fn new() -> Result { +//! Ok(Screen { +//! position: Vector::new(50, 50) +//! }) +//! } +//! +//! fn update(&mut self, window: &mut Window) -> Result<()> { +//! if window.keyboard()[Key::Right].is_down() || +//! window.mouse()[MouseButton::Right].is_down() || +//! window.gamepads().iter().any(|pad| pad[GamepadButton::TriggerRight].is_down()) +//! { +//! self.position.x += 2.5; +//! } +//! if window.keyboard()[Key::Left].is_down() || +//! window.mouse()[MouseButton::Left].is_down() || +//! window.gamepads().iter().any(|pad| pad[GamepadButton::TriggerLeft].is_down()) +//! { +//! self.position.x -= 2.5; +//! } +//! Ok(()) +//! } +//! +//! fn draw(&mut self, window: &mut Window) -> Result<()> { +//! window.clear(Color::WHITE)?; +//! window.draw(&Rectangle::new(self.position, (100, 200)), Background::Col(Color::RED)); +//! Ok(()) +//! } +//! } +//! +//! fn main() { +//! run::("Hello World", Vector::new(800, 600), Default::default()); +//! } +//! ``` +//! Unlike mice and keyboards, which generally are one-per-system, a machine may have many gamepads +//! connected. More advanced applications may wish to assign specific gamepads to specific +//! The input API generally follows this principal: an input source is indexed by a button enum, +//! and returns a `ButtonState` enum. A button state can be `Pressed`, `Held`, `Released` or +//! `NotPressed`, and a convenience method `is_down` checks if the button is either pressed or +//! held. +//! functions or specific users, but for our case checking against any gamepad does just fine. +//! +//! If we want to only apply an effect once per input submission, we have two options. One is to +//! check if the button state is exactly `Pressed`: that is, the button was not pressed the last +//! update, but now is. The other is to implement the `event` method of State, and listen for a +//! keypress event. To compare, here is an implementation that checks for `Pressed` for up and uses +//! an event for down: +//! ```no_run +//! extern crate quicksilver; +//! +//! use quicksilver::{ +//! Result, +//! geom::{Rectangle, Vector}, +//! graphics::{Background, Color}, +//! input::{ButtonState, GamepadButton, Key, MouseButton}, // We need to match ButtonState +//! lifecycle::{Event, State, Window, run} // We need to match against Event +//! }; +//! +//! struct Screen { +//! position: Vector // We need to store the position as state +//! } +//! +//! impl State for Screen { +//! fn new() -> Result { +//! Ok(Screen { +//! position: Vector::new(50, 50) +//! }) +//! } +//! +//! fn event(&mut self, event: &Event, window: &mut Window) -> Result<()> { +//! if let Event::Key(Key::Down, ButtonState::Pressed) = event { +//! self.position.y += 10.0; +//! } +//! Ok(()) +//! } +//! +//! fn update(&mut self, window: &mut Window) -> Result<()> { +//! if window.keyboard()[Key::Right].is_down() || +//! window.mouse()[MouseButton::Right].is_down() || +//! window.gamepads().iter().any(|pad| pad[GamepadButton::TriggerRight].is_down()) +//! { +//! self.position.x += 2.5; +//! } +//! if window.keyboard()[Key::Left].is_down() || +//! window.mouse()[MouseButton::Left].is_down() || +//! window.gamepads().iter().any(|pad| pad[GamepadButton::TriggerLeft].is_down()) +//! { +//! self.position.x -= 2.5; +//! } +//! if window.keyboard()[Key::Up] == ButtonState::Pressed { +//! self.position.y -= 10.0; +//! } +//! Ok(()) +//! } +//! +//! fn draw(&mut self, window: &mut Window) -> Result<()> { +//! window.clear(Color::WHITE)?; +//! window.draw(&Rectangle::new(self.position, (100, 200)), Background::Col(Color::RED)); +//! Ok(()) +//! } +//! } +//! +//! fn main() { +//! run::("Hello World", Vector::new(800, 600), Default::default()); +//! } +//! ``` diff --git a/src/tutorials/_04_lifecycle.rs b/src/tutorials/_04_lifecycle.rs new file mode 100644 index 00000000..9e77d148 --- /dev/null +++ b/src/tutorials/_04_lifecycle.rs @@ -0,0 +1,59 @@ +//! We've now seen the four main methods that form the Quicksilver application lifecycle: `new`, +//! `update`, `draw`, and `event`. Before we go on, it might help to have an understanding of these +//! methods and when exactly they get called. +//! +//! ## new +//! +//! `new` is the only mandatory function of `State`, which every Quicksilver application must +//! implement. Start all asset loading here, as well as initializing physics worlds or other +//! persistent state. +//! +//! Do not attempt to use *any* Quicksilver features before `new` runs! For example, do not call +//! `Image::load` in your main before you invoke `run`. Platform-specific setup occurs +//! behind-the-scenes, so just use `new` for all your initialization. +//! +//! ## draw +//! +//! `draw` is not mandatory, but it may as well be. By default, it will run as fast as vsync will +//! allow. You can choose to run it less often, by providing higher values to `draw_rate` in +//! Settings. For example, to only draw once every 35 milliseconds (approximately 30 FPS), you +//! could use the following `Settings` declaration: +//! ```no_run +//! # use quicksilver::{geom::Vector, lifecycle::{State, Settings, run}}; +//! # fn func(some_title: &'static str, some_dimensions: Vector) { +//! run::(some_title, some_dimensions, Settings { +//! draw_rate: 35.0, +//! ..Settings::default() +//! }); +//! # } +//! ``` +//! If you want to run the draw function as often as possible, you may want to disable vsync. You +//! can again do it with `Settings`: +//! ```no_run +//! # use quicksilver::{geom::Vector, lifecycle::{State, Settings, run}}; +//! # fn func(some_title: &'static str, some_dimensions: Vector) { +//! run::(some_title, some_dimensions, Settings { +//! vsync: false, +//! ..Settings::default() +//! }); +//! # } +//! ``` +//! After each call to `draw`, the buffers are flipped (meaning your changes become visible to the +//! user.) +//! +//! ## update +//! +//! `update` is useful for any fixed-rate calculations or ticks. By default, it is called 60 times +//! per second, and will attempt to make up for any lost time. See [this Gaffer on Games blog +//! post](https://gafferongames.com/post/fix_your_timestep/) for a description of the algorithm. +//! You can change the tick rate with the `update_rate` setting, which determines how many +//! milliseconds take place between ticks. +//! +//! ## event +//! +//! `event` is called when the events are triggered, either immediately or buffered before the next +//! update. Events can form their own custom lifecycle: for example, listening for an +//! `Event::Closed` means you can run code to save the game state before the application +//! terminates. However, events aren't guaranteed to fire. If the user pulls the battery out of +//! their computer or a power outage shuts down a desktop, no event handler can ensure your code +//! runs. diff --git a/src/tutorials/_05_images.rs b/src/tutorials/_05_images.rs new file mode 100644 index 00000000..3318d5da --- /dev/null +++ b/src/tutorials/_05_images.rs @@ -0,0 +1,112 @@ +//! Interactability might be important, but just sticking to regular shapes can get a bit boring. +//! +//! It's time for the wonderful world of images! Drawing images is almost the same as drawing +//! colors, but instead of using `Background::Col` we use `Background::Img`. The big change to +//! learn with images is asset loading. +//! On desktop, you can make a blocking file load read, but that's not an option on web. This means +//! that *all* Quicksilver asset loading is asynchronous, through the `Asset` system. +//! ```no_run +//! // Draw an image to the screen +//! extern crate quicksilver; +//! +//! use quicksilver::{ +//! Result, +//! geom::{Shape, Vector}, +//! graphics::{Background::Img, Color, Image}, // We need Image and image backgrounds +//! lifecycle::{Asset, Settings, State, Window, run}, // To load anything, we need Asset +//! }; +//! +//! struct ImageViewer { +//! asset: Asset, // an image asset isn't state, but it does need to persist +//! } +//! +//! impl State for ImageViewer { +//! fn new() -> Result { +//! let asset = Asset::new(Image::load("image.png")); // Start loading the asset +//! Ok(ImageViewer { asset }) +//! } +//! +//! fn draw(&mut self, window: &mut Window) -> Result<()> { +//! window.clear(Color::WHITE)?; +//! self.asset.execute(|image| { +//! // If we've loaded the image, draw it +//! window.draw(&image.area().with_center((400, 300)), Img(&image)); +//! Ok(()) +//! }) +//! } +//! } +//! +//! fn main() { +//! run::("Image Example", Vector::new(800, 600), Settings { +//! icon_path: Some("image.png"), // Set the window icon +//! ..Settings::default() +//! }); +//! } +//! ``` +//! You'll notice we provided a path to `Image::load`, which was "image.png." This asset is stored +//! in the `static` directory, not in the crate root. This is a current limitation of `cargo-web` +//! and may be changed in the future, but for now all assets must be stored in `static.` You can +//! use the [Quicksilver test +//! image](https://github.com/ryanisaacg/quicksilver/blob/development/static/image.png). +//! +//! The asset system uses Futures, which are an asychronous programming concept. Basically, a +//! Future is a computation that will complete at some point in, well, the future. For example, +//! loading an image over a network is a future: the web browser doesn't have the image downloaded +//! *yet* but it will (or it will produce an error.) +//! +//! This asset system is probably temporary, as async / await promise to be much more ergonomic +//! methods of dealing with futures. However, Rust's async / await story isn't stable yet, so the +//! Asset system is the most convenient way of loading things with Quicksilver. The execute +//! function on an Asset runs the provided closure if loading is complete, with the actual asset +//! data passed as a parameter. In the example above, the window is cleared every draw frame and +//! once the image is loaded, it is drawn to the screen. +//! +//! Additionally, we now set the application icon path. The icon is also sourced from `static`, and +//! determines the tab icon on the web and the window icon on desktop. +//! +//! Images can have subimages, which allows for spritesheets, texture atlases, and sprite batching: +//! ```no_run +//! // Draw an image to the screen +//! extern crate quicksilver; +//! +//! use quicksilver::{ +//! Future, Result, // We need the Future trait to operate on a future +//! geom::{Rectangle, Shape, Vector}, +//! graphics::{Background::Img, Color, Image}, +//! lifecycle::{Asset, Settings, State, Window, run}, +//! }; +//! +//! struct ImageViewer { +//! asset: Asset, +//! } +//! +//! impl State for ImageViewer { +//! fn new() -> Result { +//! let asset = Asset::new( +//! Image::load("image.png") +//! // Between the image loading and the asset being "done", take a slice from it +//! .map(|image| image.subimage(Rectangle::new((0, 0), (32, 64)))) +//! ); +//! Ok(ImageViewer { asset }) +//! } +//! +//! fn draw(&mut self, window: &mut Window) -> Result<()> { +//! window.clear(Color::WHITE)?; +//! self.asset.execute(|image| { +//! // If we've loaded the image, draw it +//! window.draw(&image.area().with_center((400, 300)), Img(&image)); +//! Ok(()) +//! }) +//! } +//! } +//! +//! fn main() { +//! run::("Image Example", Vector::new(800, 600), Settings { +//! icon_path: Some("image.png"), // Set the window icon +//! ..Settings::default() +//! }); +//! } +//! ``` +//! Here `map` applies a transformation to the image after it is loaded; the ability to chain +//! multiple assets together or apply complex operations to loading assets will be covered more in +//! depth in the asset combinator tutorial. diff --git a/src/tutorials/_06_asset_combinators.rs b/src/tutorials/_06_asset_combinators.rs new file mode 100644 index 00000000..bad3a738 --- /dev/null +++ b/src/tutorials/_06_asset_combinators.rs @@ -0,0 +1,35 @@ +//! Asset combinators (or in the general case, Future combinators) allow us to chain async +//! computations together. +//! +//! In the last tutorial, we briefly touched on asset combinators. While sometimes you want to wait +//! for a simple future to elapse (like loading a single image) sometimes you want something more, +//! like loading 2 images, loading an image then taking several subimages from it, or loading a +//! text file then using its contents to load an image. The last example could be written: +//! ```no_run +//! use quicksilver::{ +//! Future, Result, +//! combinators::ok, +//! graphics::Image, +//! lifecycle::Asset, +//! load_file, +//! }; +//! fn load_image_from_file(filename: &'static str) -> Asset { +//! Asset::new(load_file(filename) +//! .and_then(|contents| ok(String::from_utf8(contents).expect("The file must be UTF-8"))) +//! .and_then(|image_path| Image::load(image_path))) +//! } +//! ``` +//! This example uses 2 combinators: `result` and `and_then`. `result` takes a Result type and +//! converts it into a Future that immediately resolves. `and_then` chains a Future onto the +//! previous one, if the previous one completes. If we were to re-write the Futures code here as a +//! description, it would look something like: +//! ```text +//! - First load the file at path filename +//! - When that completes, if it succeeded, convert it to UTF8 +//! - When that completes, if it succeeded, start loading an image pointed to by the file +//! ``` +//! The combinator module is re-exported from the `future` module of the `futures` crate, +//! +//! **Note: Do not use the `wait` method on a `Future`; on WASM, it will panic. In the near future, +//! [when Rust has async / await](https://github.com/rust-lang/rust/issues/50547), await can be +//! used in place of `wait`. diff --git a/src/tutorials/_07_font.rs b/src/tutorials/_07_font.rs new file mode 100644 index 00000000..a87fd307 --- /dev/null +++ b/src/tutorials/_07_font.rs @@ -0,0 +1,50 @@ +//! Armed with asset combinators and the ability to draw textures, we can now do some text +//! rendering. +//! +//! Fonts are loaded just like images: with a `load` function. A simple font example +//! could be: +//! ```no_run +//! // Draw some sample text to the screen +//! extern crate quicksilver; +//! +//! use quicksilver::{ +//! Future, Result, +//! combinators::result, +//! geom::{Shape, Vector}, +//! graphics::{Background::Img, Color, Font, FontStyle, Image}, +//! lifecycle::{Asset, Settings, State, Window, run}, +//! }; +//! +//! struct SampleText { +//! asset: Asset, +//! } +//! +//! impl State for SampleText { +//! fn new() -> Result { +//! let asset = Asset::new(Font::load("font.ttf") +//! .and_then(|font| { +//! let style = FontStyle::new(72.0, Color::BLACK); +//! result(font.render("Sample Text", &style)) +//! })); +//! Ok(SampleText { asset }) +//! } +//! +//! fn draw(&mut self, window: &mut Window) -> Result<()> { +//! window.clear(Color::WHITE)?; +//! self.asset.execute(|image| { +//! window.draw(&image.area().with_center((400, 300)), Img(&image)); +//! Ok(()) +//! }) +//! } +//! } +//! +//! fn main() { +//! run::("Font Example", Vector::new(800, 600), Settings::default()); +//! } +//! ``` +//! `Font::render` renders a string into an image with a given font style. It is not recommended to +//! call this function often, as it is fairly expensive; future updates to Quicksilver should make +//! it cheaper. +//! +//! Each different font face (including bold and italic) requires a new `Font` object, and all +//! fonts must be loaded from local files (system fonts will not be found.) diff --git a/src/tutorials/_08_sound.rs b/src/tutorials/_08_sound.rs new file mode 100644 index 00000000..80d4694d --- /dev/null +++ b/src/tutorials/_08_sound.rs @@ -0,0 +1,10 @@ +//! Quicksilver's sound capabilities are currently somewhat limited: sound can be loaded and played +//! at various volumes. +//! +//! `Sound::load` creates a `Future` that resolves to a `Sound`, and each +//! `Sound` instance provides `volume` to query volume, `set_volume` to set the volume, and `play` +//! to play the sound. Volume will not be applied to currently-playing sounds, and multiple +//! different sound clips played will overlap. +//! +//! Quicksilver's sound capabilities (and by extension this tutorial) are planned to be expanded in +//! an upcoming release. diff --git a/src/tutorials/_09_animations_and_atlases.rs b/src/tutorials/_09_animations_and_atlases.rs new file mode 100644 index 00000000..4f589845 --- /dev/null +++ b/src/tutorials/_09_animations_and_atlases.rs @@ -0,0 +1,18 @@ +//! When image loading and asset combinators are combined, we can go from loading a simple image +//! and drawing it to the screen to more complex uses of image files, like spritesheets or texture +//! atlases. +//! +//! An `Animation` is a linear series of `Image`s that loops +//! when it completes. You can provide either an iterator of images to `Animation::new` (as well as +//! how many frames Animation ticks) or an image and an iterator of regions to +//! `Animation::from_spritesheet`. Either way, the Animation will have a linear collection of +//! frames. Each time you draw, you should call the `Animation::tick` function so that the animation +//! advances; you can access the current frame of the animation with the +//! `Animation::current_frame` at any time. +//! +//! An `Atlas` is a structure that stores animations and images in a single actual image file. This +//! greatly improves GPU performance by reducing the number of texture unit switches. Quicksilver +//! uses the LibGDX file format to load an `Atlas`, described [on Spine's +//! website.](http://esotericsoftware.com/spine-atlas-format) You can query from an `Atlas` with +//! the `Atlas::get` function which returns an AtlasItem. An AtlasItem is just an enum of Image and +//! Animation. diff --git a/src/tutorials/_10_views.rs b/src/tutorials/_10_views.rs new file mode 100644 index 00000000..8e488e14 --- /dev/null +++ b/src/tutorials/_10_views.rs @@ -0,0 +1,80 @@ +//! Quicksilver uses the `View` structure as an abstraction for both graphical and input projection. +//! +//! This means that a view can be thought of like a camera: it +//! determines what coordinates in draw calls appear where on screen, as well as the relationship +//! between the mouse location on the screen and the reported coordinates. +//! +//! Important to understanding `View` is understanding *world* versus *screen* coordinates. +//! *Screen* coordinates map the the window on the user's device. (0, 0) on the screen is the +//! top-left, and screen coordinates span to the pixel width and height of the window. *World* +//! coordinates are defined by the active view. By default, the world is a rectangle with the size +//! of the initial window. +//! +//! Here is a View in action (the camera example): +//! ```no_run +//! // Demonstrate adding a View to the draw-geometry example +//! // The camera can be controlled with the arrow keys +//! extern crate quicksilver; +//! +//! use quicksilver::{ +//! Result, +//! geom::{Circle, Line, Rectangle, Shape, Transform, Triangle, Vector}, +//! graphics::{Background::Col, Color, View}, +//! input::{Key}, +//! lifecycle::{Settings, State, Window, run}, +//! }; +//! +//! struct Camera { +//! view: Rectangle +//! } +//! +//! impl State for Camera { +//! // Initialize the struct +//! fn new() -> Result { +//! Ok(Camera { +//! view: Rectangle::new_sized((800, 600)) +//! }) +//! } +//! +//! fn update(&mut self, window: &mut Window) -> Result<()> { +//! if window.keyboard()[Key::Left].is_down() { +//! self.view = self.view.translate((-4, 0)); +//! } +//! if window.keyboard()[Key::Right].is_down() { +//! self.view = self.view.translate((4, 0)); +//! } +//! if window.keyboard()[Key::Down].is_down() { +//! self.view = self.view.translate((0, 4)); +//! } +//! if window.keyboard()[Key::Up].is_down() { +//! self.view = self.view.translate((0, -4)); +//! } +//! window.set_view(View::new(self.view)); +//! Ok(()) +//! } +//! +//! fn draw(&mut self, window: &mut Window) -> Result<()> { +//! window.clear(Color::WHITE)?; +//! window.draw(&Rectangle::new((100, 100), (32, 32)), Col(Color::BLUE)); +//! window.draw_ex(&Rectangle::new((400, 300), (32, 32)), Col(Color::BLUE), Transform::rotate(45), 10); +//! window.draw(&Circle::new((400, 300), 100), Col(Color::GREEN)); +//! window.draw_ex( +//! &Line::new((50, 80),(600, 450)).with_thickness(2.0), +//! Col(Color::RED), +//! Transform::IDENTITY, +//! 5 +//! ); +//! window.draw_ex( +//! &Triangle::new((500, 50), (450, 100), (650, 150)), +//! Col(Color::RED), +//! Transform::rotate(45) * Transform::scale((0.5, 0.5)), +//! 0 +//! ); +//! Ok(()) +//! } +//! } +//! +//! fn main() { +//! run::("Camera", Vector::new(800, 600), Settings::default()); +//! } +//! ``` diff --git a/src/tutorials/_11_mesh.rs b/src/tutorials/_11_mesh.rs new file mode 100644 index 00000000..f4877bcb --- /dev/null +++ b/src/tutorials/_11_mesh.rs @@ -0,0 +1,45 @@ +//! A `Mesh` is a low-level graphics concept in Quicksilver consisting of a series of polygon +//! vertices and a list of triangles. +//! +//! This concept can be used to draw any shape: to make a +//! rectangle, put the four vertices in the vertex list, and then create two triangles that +//! together make up the rectangle. +//! +//! Most user code need never encounter a mesh, but `Mesh` is extremely powerful. In fact, there is +//! no special sauce available to Quicksilver's implementation of `Drawable` for `Rectangle` that a +//! regular user of the library cannot access with a Mesh. +//! +//! There are two ways to use `Mesh`: instantiate your own with `Mesh::new` or use `Window::mesh` to +//! access the internal mesh of a window. To concatenate two meshes, use the `Mesh::apply` function. +//! +//! To create a mesh that contains a triangle with a red vertex, a blue vertex, and a green vertex, +//! you could write: +//! +//! ```no_run +//! use quicksilver::graphics::{Background::Col, Color, GpuTriangle, Mesh, Vertex}; +//! let vertices = vec![ +//! Vertex::new((400, 200), None, Col(Color::RED)), +//! Vertex::new((200, 400), None, Col(Color::BLUE)), +//! Vertex::new((600, 400), None, Col(Color::GREEN)) +//! ]; +//! let triangles = vec![ GpuTriangle::new(0, [0, 1, 2], 0.0, Col(Color::WHITE)) ]; +//! let mesh = Mesh { vertices, triangles }; +//! ``` +//! +//! To draw it you can then do: +//! +//! ```no_run +//! # use quicksilver::{graphics::{Background::Col, Color, GpuTriangle, Mesh, Vertex}, lifecycle::Window }; +//! # fn func(window: &mut Window) { +//! # let vertices = vec![ +//! # Vertex::new((400, 200), None, Col(Color::RED)), +//! # Vertex::new((200, 400), None, Col(Color::BLUE)), +//! # Vertex::new((600, 400), None, Col(Color::GREEN)) +//! # ]; +//! # let triangles = vec![ GpuTriangle::new(0, [0, 1, 2], 0.0, Col(Color::WHITE)) ]; +//! # let mesh = Mesh { vertices, triangles }; +//! window.mesh().apply(&mesh); +//! # } +//! ``` +//! +//! You have to do this every frame, because the window's mesh is cleared after it's drawn. diff --git a/src/tutorials/mod.rs b/src/tutorials/mod.rs new file mode 100644 index 00000000..bc8f3b6e --- /dev/null +++ b/src/tutorials/mod.rs @@ -0,0 +1,23 @@ +//! The quicksilver tutorials, generated through Rustdoc +//! +//! While this isn't a traditional way of hosting a tutorial, Rustdoc ensures that all the code in +//! the tutorials is checked when CI runs, keeping it nice and up-to-date. +//! +//! Before you jump into the tutorials below, make sure your development environment is ready. If +//! you're just targeting desktop, all you need is the latest stable Rust. If you're targeting the +//! web, first make sure you have a nightly toolchain installed (`rustup update nightly`), and the +//! wasm target installed on nightly (`rustup target add wasm32-unknown-unknown --toolchain +//! nightly`.) Once that's done, install cargo-web (`cargo +nightly install -f cargo-web`) and you +//! should be good. + +pub mod _01_basic; +pub mod _02_drawing; +pub mod _03_input; +pub mod _04_lifecycle; +pub mod _05_images; +pub mod _06_asset_combinators; +pub mod _07_font; +pub mod _08_sound; +pub mod _09_animations_and_atlases; +pub mod _10_views; +pub mod _11_mesh;