├── .gitignore ├── examples ├── res │ ├── 0x72_dungeon_ii.png │ ├── Roboto-Regular.ttf │ └── README.md ├── hello_world.rs ├── game_context.rs ├── animation.rs ├── text.rs ├── ecs.rs ├── orcmark.rs └── color_palette.rs ├── .vscode └── settings.json ├── src ├── input │ └── mouse.rs ├── ecs │ ├── entity.rs │ ├── entity_builder.rs │ ├── generation.rs │ ├── allocator.rs │ └── world.rs ├── error.rs ├── graphics │ ├── context.rs │ ├── view.rs │ ├── rectangle.rs │ ├── font.rs │ ├── animation.rs │ ├── text.rs │ ├── image.rs │ └── color.rs ├── time.rs ├── fps_tracker.rs ├── window.rs ├── graphics.rs ├── ecs.rs ├── lib.rs ├── vector2.rs ├── input.rs └── context.rs ├── shell.nix ├── .travis.yml ├── Cargo.toml ├── LICENSE ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | Cargo.lock 4 | -------------------------------------------------------------------------------- /examples/res/0x72_dungeon_ii.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxdeviant/peacock/HEAD/examples/res/0x72_dungeon_ii.png -------------------------------------------------------------------------------- /examples/res/Roboto-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxdeviant/peacock/HEAD/examples/res/Roboto-Regular.ttf -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.tabSize": 4, 3 | "editor.detectIndentation": false, 4 | "nixEnvSelector.nixShellConfig": "${workspaceRoot}/shell.nix" 5 | } -------------------------------------------------------------------------------- /src/input/mouse.rs: -------------------------------------------------------------------------------- 1 | use crate::{Context, Vector2f}; 2 | 3 | /// Returns the position of the mouse. 4 | pub fn position(ctx: &Context) -> Vector2f { 5 | ctx.mouse.position 6 | } 7 | -------------------------------------------------------------------------------- /examples/res/README.md: -------------------------------------------------------------------------------- 1 | # Example Resources 2 | 3 | ## 16x16 DungeonTileset II by 0x72 4 | 5 | - [itch.io](https://0x72.itch.io/dungeontileset-ii) 6 | 7 | ![16x16 DungeonTileset II by 0x72](https://github.com/maxdeviant/peacock/blob/master/examples/res/0x72_dungeon_ii.png) 8 | -------------------------------------------------------------------------------- /shell.nix: -------------------------------------------------------------------------------- 1 | with import {}; 2 | 3 | stdenv.mkDerivation { 4 | name = "peacock"; 5 | 6 | buildInputs = [ 7 | stdenv 8 | SDL2 9 | SDL2_image 10 | SDL2_ttf 11 | ] ++ lib.optionals stdenv.isDarwin [ 12 | darwin.apple_sdk.frameworks.Security 13 | ]; 14 | } 15 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: rust 2 | 3 | rust: 4 | - stable 5 | - nightly 6 | 7 | matrix: 8 | allow_failures: 9 | - rust: nightly 10 | fast_finish: true 11 | 12 | addons: 13 | apt: 14 | packages: 15 | - libsdl2-dev 16 | - libsdl2-image-dev 17 | - libsdl2-ttf-dev 18 | 19 | script: 20 | - cargo test --verbose 21 | -------------------------------------------------------------------------------- /src/ecs/entity.rs: -------------------------------------------------------------------------------- 1 | use crate::ecs::Generation; 2 | 3 | #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] 4 | pub struct Entity(Generation, i32); 5 | 6 | impl Entity { 7 | pub(crate) fn new(generation: Generation, id: i32) -> Self { 8 | Self(generation, id) 9 | } 10 | 11 | pub(crate) fn generation(&self) -> Generation { 12 | self.0 13 | } 14 | 15 | pub(crate) fn id(&self) -> i32 { 16 | self.1 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | use sdl2::ttf::FontError; 2 | use thiserror::Error; 3 | 4 | pub(crate) use anyhow::Context as AnyhowContext; 5 | pub use anyhow::Result; 6 | 7 | /// An error originating from SDL2. 8 | #[derive(Debug, Error)] 9 | pub enum Sdl2Error { 10 | /// An SDL2 font error. 11 | #[error("Encountered an SDL2 font error")] 12 | FontError(#[from] FontError), 13 | 14 | /// A generic SDL2 error. 15 | #[error("Encountered an SDL2 error: {0}")] 16 | ErrorMessage(String), 17 | } 18 | -------------------------------------------------------------------------------- /src/graphics/context.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use sdl2::render::Texture as SdlTexture; 4 | 5 | #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] 6 | pub struct AssetRef(pub(crate) u32); 7 | 8 | pub(crate) struct GraphicsContext { 9 | pub(crate) textures: HashMap, 10 | pub(crate) counter: u32, 11 | } 12 | 13 | impl GraphicsContext { 14 | pub(crate) fn new() -> Self { 15 | Self { 16 | textures: HashMap::new(), 17 | counter: 0, 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/time.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use crate::Context; 4 | 5 | pub fn duration_to_f64(duration: Duration) -> f64 { 6 | let seconds = duration.as_secs() as f64; 7 | let nanos = f64::from(duration.subsec_nanos()) * 1e-9; 8 | seconds + nanos 9 | } 10 | 11 | pub fn f64_to_duration(duration: f64) -> Duration { 12 | debug_assert!(duration >= 0.0); 13 | let seconds = duration.trunc() as u64; 14 | let nanos = (duration.fract() * 1e9) as u32; 15 | Duration::new(seconds, nanos) 16 | } 17 | 18 | pub fn get_fps(ctx: &Context) -> f64 { 19 | ctx.fps_tracker.fps() 20 | } 21 | -------------------------------------------------------------------------------- /examples/hello_world.rs: -------------------------------------------------------------------------------- 1 | use peacock::Result; 2 | use peacock::{ContextBuilder, State}; 3 | 4 | type Context = peacock::Context<()>; 5 | 6 | struct HelloWorldExample; 7 | 8 | impl State for HelloWorldExample { 9 | type Context = (); 10 | 11 | fn update(&mut self, _ctx: &mut Context) -> Result<()> { 12 | Ok(()) 13 | } 14 | 15 | fn draw(&mut self, _ctx: &mut Context, _dt: f64) -> Result<()> { 16 | Ok(()) 17 | } 18 | } 19 | 20 | fn main() -> Result<()> { 21 | ContextBuilder::new("Hello, world!", 1920, 1080) 22 | .build_empty()? 23 | .run(&mut HelloWorldExample) 24 | } 25 | -------------------------------------------------------------------------------- /src/graphics/view.rs: -------------------------------------------------------------------------------- 1 | use crate::Vector2f; 2 | 3 | #[derive(Debug)] 4 | pub struct View { 5 | pub(crate) center: Vector2f, 6 | pub(crate) size: Vector2f, 7 | pub(crate) rotation: f32, 8 | pub(crate) zoom: f32, 9 | } 10 | 11 | impl View { 12 | pub fn new(center: Vector2f, size: Vector2f) -> Self { 13 | Self { 14 | center, 15 | size, 16 | rotation: 0.0, 17 | zoom: 1.0, 18 | } 19 | } 20 | 21 | pub fn set_rotation(&mut self, rotation: f32) { 22 | self.rotation = rotation; 23 | } 24 | 25 | pub fn set_zoom(&mut self, zoom: f32) { 26 | self.zoom = zoom; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "peacock" 3 | version = "0.0.1" 4 | description = "A game engine for making beautiful games." 5 | repository = "https://github.com/maxdeviant/peacock" 6 | documentation = "https://docs.rs/peacock/" 7 | categories = ["game-engines"] 8 | keywords = ["peacock", "game", "engine", "gamedev"] 9 | authors = ["Marshall Bowers "] 10 | license = "MIT" 11 | edition = "2018" 12 | 13 | [badges] 14 | maintenance = { status = "actively-developed" } 15 | 16 | [dependencies] 17 | anyhow = "1.0" 18 | hashbrown = "0.1.8" 19 | lazy_static = "1.0" 20 | rand = "0.6.5" 21 | sdl2 = { version = "0.32", features = ["image", "ttf", "unsafe_textures"] } 22 | thiserror = "1.0" 23 | -------------------------------------------------------------------------------- /src/fps_tracker.rs: -------------------------------------------------------------------------------- 1 | use std::collections::VecDeque; 2 | use std::time::Duration; 3 | 4 | use crate::time; 5 | 6 | pub(crate) struct FpsTracker { 7 | samples: VecDeque, 8 | } 9 | 10 | impl FpsTracker { 11 | pub fn new() -> Self { 12 | let mut samples = VecDeque::with_capacity(200); 13 | samples.resize(200, 1.0 / 60.0); 14 | 15 | Self { samples } 16 | } 17 | 18 | pub fn fps(&self) -> f64 { 19 | 1.0 / (self.samples.iter().sum::() / self.samples.len() as f64) 20 | } 21 | 22 | pub fn tick(&mut self, elapsed_time: Duration) { 23 | self.samples.pop_front(); 24 | self.samples.push_back(time::duration_to_f64(elapsed_time)); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/graphics/rectangle.rs: -------------------------------------------------------------------------------- 1 | use std::ops::Add; 2 | 3 | /// A rectangle. 4 | #[derive(Debug, Copy, Clone, PartialEq)] 5 | pub struct Rectangle { 6 | pub x: T, 7 | pub y: T, 8 | pub width: T, 9 | pub height: T, 10 | } 11 | 12 | impl Rectangle 13 | where 14 | T: Copy + Add, 15 | { 16 | /// Creates a new [`Rectangle`]. 17 | pub fn new(x: T, y: T, width: T, height: T) -> Self { 18 | Self { 19 | x, 20 | y, 21 | width, 22 | height, 23 | } 24 | } 25 | 26 | pub fn left(&self) -> T { 27 | self.x 28 | } 29 | 30 | pub fn top(&self) -> T { 31 | self.y 32 | } 33 | 34 | pub fn right(&self) -> T { 35 | self.x + self.width 36 | } 37 | 38 | pub fn bottom(&self) -> T { 39 | self.y + self.height 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/window.rs: -------------------------------------------------------------------------------- 1 | use sdl2::rect::Rect as SdlRect; 2 | 3 | use crate::graphics::View; 4 | use crate::Context; 5 | 6 | /// Sets the title of the window. 7 | pub fn set_title(ctx: &mut Context, title: &str) { 8 | ctx.canvas 9 | .window_mut() 10 | .set_title(title) 11 | .expect("Failed to set window title"); 12 | } 13 | 14 | /// Sets a new view for the window. 15 | pub fn set_view(ctx: &mut Context, view: &View) { 16 | ctx.canvas.set_viewport(SdlRect::new( 17 | view.center.x as i32, 18 | view.center.y as i32, 19 | view.size.x as u32, 20 | view.size.y as u32, 21 | )); 22 | ctx.canvas 23 | .set_scale(view.zoom, view.zoom) 24 | .expect("Failed to set scale"); 25 | } 26 | 27 | /// Sets whether the mouse cursor is visible in the window. 28 | pub fn set_mouse_cursor_visible(ctx: &mut Context, visible: bool) { 29 | ctx.sdl_context.mouse().show_cursor(visible); 30 | } 31 | -------------------------------------------------------------------------------- /src/graphics/font.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | 3 | use sdl2::ttf::{Font as SdlFont, FontError as SdlFontError}; 4 | 5 | use crate::error::{AnyhowContext, Sdl2Error}; 6 | use crate::{Context, Result, SDL_TTF_CONTEXT}; 7 | 8 | /// A font. 9 | pub struct Font { 10 | pub(crate) font: SdlFont<'static, 'static>, 11 | } 12 | 13 | impl fmt::Debug for Font { 14 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 15 | write!(f, "Font") 16 | } 17 | } 18 | 19 | impl Font { 20 | /// Creates a new [`Font`] from a file. 21 | pub fn from_file(ctx: &mut Context, filename: &str, size: u16) -> Result { 22 | let _ = ctx; 23 | let font = SDL_TTF_CONTEXT 24 | .load_font(filename, size) 25 | .map_err(SdlFontError::SdlError) 26 | .map_err(Sdl2Error::FontError) 27 | .with_context(|| format!("Failed to create font from file: {}", filename))?; 28 | Ok(Self { font }) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/graphics.rs: -------------------------------------------------------------------------------- 1 | mod animation; 2 | mod color; 3 | mod context; 4 | mod font; 5 | mod image; 6 | mod rectangle; 7 | mod text; 8 | mod view; 9 | 10 | pub use self::animation::*; 11 | pub use self::color::*; 12 | pub(crate) use self::context::*; 13 | pub use self::font::*; 14 | pub use self::image::*; 15 | pub use self::rectangle::*; 16 | pub use self::text::*; 17 | pub use self::view::*; 18 | 19 | use crate::{Context, Result}; 20 | 21 | pub trait Drawable { 22 | type Params; 23 | 24 | fn draw(&self, ctx: &mut Context, params: &Self::Params) -> Result<()>; 25 | } 26 | 27 | /// Clears the screen using the given [`Color`]. 28 | pub fn clear(ctx: &mut Context, color: Color) { 29 | ctx.canvas.set_draw_color(color); 30 | ctx.canvas.clear(); 31 | } 32 | 33 | /// Draws a [`Drawable`] object to the current render target. 34 | pub fn draw>( 35 | ctx: &mut Context, 36 | drawable: &D, 37 | params: &D::Params, 38 | ) -> Result<()> { 39 | drawable.draw(ctx, params) 40 | } 41 | -------------------------------------------------------------------------------- /src/ecs/entity_builder.rs: -------------------------------------------------------------------------------- 1 | use std::any::{Any, TypeId}; 2 | use std::collections::HashMap; 3 | 4 | use crate::ecs::{Component, Entity, World}; 5 | 6 | pub struct EntityBuilder<'a> { 7 | world: &'a mut World, 8 | components: HashMap>, 9 | } 10 | 11 | impl<'a> EntityBuilder<'a> { 12 | pub(crate) fn new(world: &'a mut World) -> Self { 13 | Self { 14 | world, 15 | components: HashMap::new(), 16 | } 17 | } 18 | 19 | pub fn with(mut self, component: T) -> Self { 20 | self.components 21 | .insert(TypeId::of::(), Box::new(component)); 22 | self 23 | } 24 | 25 | pub fn build(self) -> Entity { 26 | let entity = self.world.allocator.allocate(); 27 | 28 | for (component_type, component) in self.components { 29 | self.world 30 | .attach_component(entity, component_type, component); 31 | } 32 | 33 | self.world.entities.push(entity); 34 | 35 | entity 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Marshall Bowers 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/ecs.rs: -------------------------------------------------------------------------------- 1 | mod allocator; 2 | mod entity; 3 | mod entity_builder; 4 | mod generation; 5 | mod world; 6 | 7 | pub(crate) use self::allocator::*; 8 | pub use self::entity::*; 9 | pub use self::entity_builder::*; 10 | pub(crate) use self::generation::*; 11 | pub(crate) use self::world::*; 12 | 13 | use crate::Context; 14 | 15 | pub trait Component: Send + Sync + 'static {} 16 | 17 | pub fn create_entity(ctx: &mut Context) -> EntityBuilder { 18 | EntityBuilder::new(&mut ctx.world) 19 | } 20 | 21 | pub fn entities(ctx: &Context) -> Vec { 22 | ctx.world.entities.clone() 23 | } 24 | 25 | pub fn has_component(ctx: &mut Context, entity: Entity) -> bool { 26 | ctx.world.has_component::(entity) 27 | } 28 | 29 | pub fn get_component(ctx: &Context, entity: Entity) -> Option<&T> { 30 | ctx.world.get_component(entity) 31 | } 32 | 33 | pub fn get_component_mut(ctx: &mut Context, entity: Entity) -> Option<&mut T> { 34 | ctx.world.get_component_mut(entity) 35 | } 36 | 37 | pub fn delete_entity(ctx: &mut Context, entity: Entity) { 38 | unimplemented!() 39 | } 40 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Peacock is a game engine for making beautiful games. 2 | //! 3 | //! [![Crates.io](https://img.shields.io/crates/v/peacock.svg)](https://crates.io/crates/peacock) 4 | //! [![Docs.rs](https://docs.rs/peacock/badge.svg)](https://docs.rs/peacock/) 5 | //! [![Crates.io](https://img.shields.io/crates/l/peacock.svg)](https://github.com/maxdeviant/peacock/blob/master/LICENSE) 6 | //! [![Travis](https://img.shields.io/travis/maxdeviant/peacock.svg?style=flat)](https://travis-ci.org/maxdeviant/peacock) 7 | //! 8 | //! ## Installation 9 | //! ```toml 10 | //! [dependencies] 11 | //! peacock = "0.0.1" 12 | //! ``` 13 | 14 | #![warn(missing_docs)] 15 | 16 | pub mod ecs; 17 | pub mod error; 18 | pub mod graphics; 19 | pub mod input; 20 | pub mod time; 21 | pub mod window; 22 | 23 | mod context; 24 | mod fps_tracker; 25 | mod vector2; 26 | 27 | pub use crate::context::*; 28 | pub use crate::error::Result; 29 | pub(crate) use crate::fps_tracker::*; 30 | pub use crate::vector2::*; 31 | 32 | pub trait State { 33 | type Context; 34 | 35 | fn update(&mut self, ctx: &mut Context) -> Result<()>; 36 | fn draw(&mut self, ctx: &mut Context, dt: f64) -> Result<()>; 37 | } 38 | -------------------------------------------------------------------------------- /examples/game_context.rs: -------------------------------------------------------------------------------- 1 | use peacock::graphics::{self, DrawTextParams, Font, Text}; 2 | use peacock::{ContextBuilder, Result, State, Vector2f}; 3 | 4 | type Context = peacock::Context; 5 | 6 | struct GameContext { 7 | font: Font, 8 | } 9 | 10 | impl GameContext { 11 | fn new(ctx: &mut peacock::Context<()>) -> Result { 12 | Ok(Self { 13 | font: Font::from_file(ctx, "examples/res/Roboto-Regular.ttf", 24)?, 14 | }) 15 | } 16 | } 17 | 18 | struct GameContextExample { 19 | message: Text, 20 | } 21 | 22 | impl GameContextExample { 23 | fn new(ctx: &mut Context) -> Result { 24 | Ok(Self { 25 | message: Text::new(ctx, "Hello", &ctx.game().font)?, 26 | }) 27 | } 28 | } 29 | 30 | impl State for GameContextExample { 31 | type Context = GameContext; 32 | 33 | fn update(&mut self, _ctx: &mut Context) -> Result<()> { 34 | Ok(()) 35 | } 36 | 37 | fn draw(&mut self, ctx: &mut Context, _dt: f64) -> Result<()> { 38 | graphics::draw( 39 | ctx, 40 | &self.message, 41 | &DrawTextParams { 42 | position: Vector2f::ZERO, 43 | ..Default::default() 44 | }, 45 | )?; 46 | 47 | Ok(()) 48 | } 49 | } 50 | 51 | fn main() -> Result<()> { 52 | ContextBuilder::new("Game Context", 1920, 1080) 53 | .build(GameContext::new)? 54 | .run_with_result(GameContextExample::new) 55 | } 56 | -------------------------------------------------------------------------------- /src/ecs/generation.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] 2 | pub(crate) struct Generation(i32); 3 | 4 | pub(crate) trait ZeroableGeneration { 5 | fn id(&self) -> i32; 6 | fn is_alive(&self) -> bool; 7 | fn die(self) -> Generation; 8 | fn raise(self) -> Generation; 9 | } 10 | 11 | impl Generation { 12 | pub fn one() -> Self { 13 | Self(1) 14 | } 15 | 16 | pub fn new(id: i32) -> Result { 17 | if id == 0 { 18 | return Err("Generation ID must be non-zero"); 19 | } 20 | 21 | Ok(Self(id)) 22 | } 23 | 24 | pub fn id(&self) -> i32 { 25 | self.0 26 | } 27 | 28 | pub fn is_alive(&self) -> bool { 29 | self.id() > 0 30 | } 31 | 32 | pub fn die(self) -> Self { 33 | assert!(self.is_alive()); 34 | 35 | Self(-self.id()) 36 | } 37 | 38 | pub fn raise(self) -> Self { 39 | assert!(!self.is_alive()); 40 | 41 | Self(1 - self.id()) 42 | } 43 | } 44 | 45 | impl ZeroableGeneration for Option { 46 | fn id(&self) -> i32 { 47 | self.map(|gen| gen.id()).unwrap_or(0) 48 | } 49 | 50 | fn is_alive(&self) -> bool { 51 | self.id() > 0 52 | } 53 | 54 | fn die(self) -> Generation { 55 | assert!(self.is_alive()); 56 | 57 | Generation(-self.id()) 58 | } 59 | 60 | fn raise(self) -> Generation { 61 | assert!(!self.is_alive()); 62 | 63 | Generation(1 - self.id()) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [Unreleased] 9 | 10 | ### Added 11 | 12 | - Added `Vector2` 13 | - Added animation support with `Animation` 14 | - Added all of the [X11 web colors](https://en.wikipedia.org/wiki/Web_colors#X11_color_names) as constants on `Color` 15 | - Added `graphics::clear` for clearing the screen 16 | - Added `Drawable` trait for objects that can be drawn to the screen 17 | - Added `graphics::draw` for drawing `Drawable`s to the screen 18 | - Added `graphics::draw_sprite` for drawing a sprite to the screen 19 | - Added `graphics::draw_text` for drawing text to the screen 20 | - Added `window::set_title` for setting the window title 21 | - Added `window::set_view` for setting the view on the window 22 | 23 | ### Changed 24 | 25 | - `Context.window` and `Context.fps_tracker` are no longer visible outside of the crate 26 | - Replaced SFML `Color` with custom `Color` 27 | 28 | ### Fixed 29 | 30 | - Fixed sprite batching by removing some hard-coded values 31 | 32 | ## 0.0.1 - 2019-02-09 33 | 34 | ### Added 35 | 36 | - Added base game constructs 37 | - `State` 38 | - `Context` 39 | - `SpriteBatch` 40 | 41 | [unreleased]: https://github.com/maxdeviant/peacock/compare/v0.0.1...HEAD 42 | [0.0.1]: https://github.com/maxdeviant/peacock/compare/2e44af3...v0.0.1 43 | -------------------------------------------------------------------------------- /examples/animation.rs: -------------------------------------------------------------------------------- 1 | use peacock::graphics::{self, Animation, DrawAnimationParams, Image, Rectangle, View}; 2 | use peacock::window; 3 | use peacock::{ContextBuilder, Result, State}; 4 | 5 | type Context = peacock::Context<()>; 6 | 7 | struct AnimationExample { 8 | animation: Animation, 9 | } 10 | 11 | impl AnimationExample { 12 | fn new(ctx: &mut Context) -> Result { 13 | let sprite_sheet = Image::from_file(ctx, "examples/res/0x72_dungeon_ii.png")?; 14 | 15 | let animation = Animation::new( 16 | sprite_sheet, 17 | vec![ 18 | Rectangle::::new(128, 76, 15, 20), 19 | Rectangle::::new(144, 76, 15, 20), 20 | Rectangle::::new(160, 76, 15, 20), 21 | Rectangle::::new(176, 76, 15, 20), 22 | ], 23 | 8, 24 | ); 25 | 26 | Ok(Self { animation }) 27 | } 28 | } 29 | 30 | impl State for AnimationExample { 31 | type Context = (); 32 | 33 | fn update(&mut self, _ctx: &mut Context) -> Result<()> { 34 | self.animation.tick(); 35 | 36 | Ok(()) 37 | } 38 | 39 | fn draw(&mut self, ctx: &mut Context, _dt: f64) -> Result<()> { 40 | let mut view = View::new((0.0, 0.0).into(), (1920.0, 1080.0).into()); 41 | view.set_zoom(8.0); 42 | 43 | window::set_view(ctx, &view); 44 | 45 | graphics::draw( 46 | ctx, 47 | &self.animation, 48 | &DrawAnimationParams { 49 | ..DrawAnimationParams::default() 50 | }, 51 | )?; 52 | 53 | Ok(()) 54 | } 55 | } 56 | 57 | fn main() -> Result<()> { 58 | ContextBuilder::new("Animation", 1920, 1080) 59 | .build_empty()? 60 | .run_with_result(AnimationExample::new) 61 | } 62 | -------------------------------------------------------------------------------- /examples/text.rs: -------------------------------------------------------------------------------- 1 | use peacock::graphics::{self, DrawTextParams, Font, Text}; 2 | use peacock::Result; 3 | use peacock::{ContextBuilder, State, Vector2f}; 4 | 5 | type Context = peacock::Context<()>; 6 | 7 | struct TextExample { 8 | greeting_message: Text, 9 | centered_text: Text, 10 | } 11 | 12 | impl TextExample { 13 | fn new(ctx: &mut Context) -> Result { 14 | let font = Font::from_file(ctx, "examples/res/Roboto-Regular.ttf", 24)?; 15 | 16 | let greeting_message = Text::new( 17 | ctx, 18 | "Hello, world!\n\nI hope you enjoy using Peacock!", 19 | &font, 20 | )?; 21 | 22 | let centered_text = Text::new(ctx, "This text is centered on the screen", &font)?; 23 | 24 | Ok(Self { 25 | greeting_message, 26 | centered_text, 27 | }) 28 | } 29 | } 30 | 31 | impl State for TextExample { 32 | type Context = (); 33 | 34 | fn update(&mut self, _ctx: &mut Context) -> Result<()> { 35 | Ok(()) 36 | } 37 | 38 | fn draw(&mut self, ctx: &mut Context, _dt: f64) -> Result<()> { 39 | graphics::draw( 40 | ctx, 41 | &self.greeting_message, 42 | &DrawTextParams { 43 | position: Vector2f::new(10.0, 10.0), 44 | ..Default::default() 45 | }, 46 | )?; 47 | 48 | graphics::draw( 49 | ctx, 50 | &self.centered_text, 51 | &DrawTextParams { 52 | position: Vector2f::new((1920 / 2 - self.centered_text.size().x / 2) as f32, 50.0), 53 | ..Default::default() 54 | }, 55 | )?; 56 | 57 | Ok(()) 58 | } 59 | } 60 | 61 | fn main() -> Result<()> { 62 | ContextBuilder::new("Text", 1920, 1080) 63 | .build_empty()? 64 | .run_with_result(TextExample::new) 65 | } 66 | -------------------------------------------------------------------------------- /src/graphics/animation.rs: -------------------------------------------------------------------------------- 1 | use crate::graphics::{Color, DrawImageParams, Drawable, Image, Rectangle}; 2 | use crate::{Context, Result, Vector2f}; 3 | 4 | #[derive(Debug)] 5 | pub struct Animation { 6 | texture: Image, 7 | 8 | /// The frames in the animation. 9 | frames: Vec>, 10 | 11 | /// The length of a frame, in ticks. 12 | frame_length: i32, 13 | 14 | /// The index of the current animation frame. 15 | current_frame: usize, 16 | timer: i32, 17 | } 18 | 19 | impl Animation { 20 | pub fn new(texture: Image, frames: Vec>, frame_length: i32) -> Self { 21 | Self { 22 | texture, 23 | frames, 24 | frame_length, 25 | current_frame: 0, 26 | timer: 0, 27 | } 28 | } 29 | 30 | pub fn tick(&mut self) { 31 | self.timer += 1; 32 | 33 | if self.timer >= self.frame_length { 34 | self.current_frame = (self.current_frame + 1) % self.frames.len(); 35 | self.timer = 0; 36 | } 37 | } 38 | 39 | /// Restarts the animation from the beginning. 40 | pub fn restart(&mut self) { 41 | self.current_frame = 0; 42 | self.timer = 0; 43 | } 44 | } 45 | 46 | /// The parameters for drawing an [`Animation`] to the current render target. 47 | #[derive(Debug, Default)] 48 | pub struct DrawAnimationParams { 49 | /// The position at which to draw the [`Animation`]. 50 | pub position: Vector2f, 51 | 52 | pub color: Option, 53 | 54 | pub scale: Option, 55 | } 56 | 57 | impl Drawable for Animation { 58 | type Params = DrawAnimationParams; 59 | 60 | fn draw(&self, ctx: &mut Context, params: &DrawAnimationParams) -> Result<()> { 61 | self.texture.draw( 62 | ctx, 63 | &DrawImageParams { 64 | clip_rect: Some(self.frames[self.current_frame]), 65 | position: params.position, 66 | color: params.color, 67 | scale: params.scale, 68 | }, 69 | ) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/ecs/allocator.rs: -------------------------------------------------------------------------------- 1 | use std::collections::VecDeque; 2 | 3 | use crate::ecs::{Entity, Generation, ZeroableGeneration}; 4 | 5 | pub(crate) struct Allocator { 6 | generations: Vec>, 7 | cache: VecDeque, 8 | next_id: i32, 9 | } 10 | 11 | impl Allocator { 12 | pub fn new() -> Self { 13 | Self { 14 | generations: Vec::new(), 15 | cache: VecDeque::new(), 16 | next_id: 0, 17 | } 18 | } 19 | 20 | pub fn allocate(&mut self) -> Entity { 21 | let id = self.cache.pop_front().unwrap_or_else(|| { 22 | self.next_id += 1; 23 | self.next_id 24 | }); 25 | 26 | self.update_generation_length(id as usize); 27 | let generation = self.generations[id as usize].raise(); 28 | self.generations[id as usize] = Some(generation); 29 | 30 | Entity::new(generation, id) 31 | } 32 | 33 | pub fn is_alive(&self, entity: Entity) -> bool { 34 | entity.generation() == self.get_generation(entity) 35 | } 36 | 37 | pub fn kill(&mut self, entity: Entity) { 38 | self.kill_many(std::iter::once(entity)); 39 | } 40 | 41 | pub fn kill_many>(&mut self, entities: I) { 42 | for entity in entities { 43 | if !self.is_alive(entity) { 44 | panic!("Wrong generation") 45 | } 46 | 47 | let id = entity.id(); 48 | 49 | self.update_generation_length(id as usize); 50 | 51 | self.generations[id as usize] = Some(self.generations[id as usize].die()); 52 | 53 | self.cache.push_back(id); 54 | } 55 | } 56 | 57 | fn get_generation(&self, entity: Entity) -> Generation { 58 | if self.generations.len() <= entity.id() as usize { 59 | return Generation::one(); 60 | } 61 | 62 | let generation = self.generations[entity.id() as usize]; 63 | generation.unwrap_or_else(Generation::one) 64 | } 65 | 66 | fn update_generation_length(&mut self, desired_length: usize) { 67 | if self.generations.len() <= desired_length { 68 | self.generations.resize(desired_length + 1, None); 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/graphics/text.rs: -------------------------------------------------------------------------------- 1 | use sdl2::rect::Rect as SdlRect; 2 | 3 | use crate::error::{AnyhowContext, Sdl2Error}; 4 | use crate::graphics::{AssetRef, Color, Drawable, Font}; 5 | use crate::vector2::Vector2f; 6 | use crate::{Context, Result, Vector2u}; 7 | 8 | #[derive(Debug)] 9 | pub struct Text { 10 | pub(crate) string: String, 11 | pub(crate) texture: AssetRef, 12 | size: Vector2u, 13 | } 14 | 15 | impl Text { 16 | pub fn new>(ctx: &mut Context, string: S, font: &Font) -> Result { 17 | let texture_creator = ctx.canvas.texture_creator(); 18 | 19 | let string = string.into(); 20 | let surface = font 21 | .font 22 | .render(&string) 23 | .blended(Color::WHITE) 24 | .map_err(Sdl2Error::FontError)?; 25 | let texture = texture_creator.create_texture_from_surface(&surface)?; 26 | let texture_query = texture.query(); 27 | 28 | let texture_ref = AssetRef(ctx.graphics.counter); 29 | 30 | ctx.graphics.counter += 1; 31 | 32 | ctx.graphics.textures.insert(texture_ref, texture); 33 | 34 | Ok(Self { 35 | string, 36 | texture: texture_ref, 37 | size: (texture_query.width, texture_query.height).into(), 38 | }) 39 | } 40 | 41 | /// Returns the size of the rendered text. 42 | pub fn size(&self) -> Vector2u { 43 | self.size 44 | } 45 | } 46 | 47 | /// The parameters for drawing [`Text`] to the current render target. 48 | #[derive(Debug)] 49 | pub struct DrawTextParams { 50 | /// The position at which to draw the [`Text`]. 51 | pub position: Vector2f, 52 | 53 | /// The color with which to draw the [`Text`]. 54 | pub color: Option, 55 | } 56 | 57 | impl Default for DrawTextParams { 58 | fn default() -> Self { 59 | Self { 60 | position: Vector2f::ZERO, 61 | color: None, 62 | } 63 | } 64 | } 65 | 66 | impl Drawable for Text { 67 | type Params = DrawTextParams; 68 | 69 | fn draw(&self, ctx: &mut Context, params: &DrawTextParams) -> Result<()> { 70 | let texture = ctx.graphics.textures.get_mut(&self.texture).unwrap(); 71 | let texture_query = texture.query(); 72 | 73 | let color = params.color.unwrap_or(Color::WHITE); 74 | texture.set_color_mod(color.r, color.g, color.b); 75 | 76 | ctx.canvas 77 | .copy( 78 | &texture, 79 | None, 80 | SdlRect::new( 81 | params.position.x as i32, 82 | params.position.y as i32, 83 | texture_query.width as u32, 84 | texture_query.height as u32, 85 | ), 86 | ) 87 | .map_err(Sdl2Error::ErrorMessage) 88 | .context("Failed to copy texture to canvas")?; 89 | 90 | Ok(()) 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /examples/ecs.rs: -------------------------------------------------------------------------------- 1 | use peacock::ecs::{self, Component, Entity}; 2 | use peacock::graphics::{self, DrawImageParams, Image, Rectangle}; 3 | use peacock::input::{self, Key}; 4 | use peacock::Result; 5 | use peacock::{ContextBuilder, State, Vector2f}; 6 | 7 | type Context = peacock::Context<()>; 8 | 9 | #[derive(Debug)] 10 | struct Transform { 11 | pub position: Vector2f, 12 | } 13 | 14 | impl Component for Transform {} 15 | 16 | #[derive(Debug)] 17 | struct StaticSprite { 18 | pub source: Rectangle, 19 | } 20 | 21 | impl Component for StaticSprite {} 22 | 23 | struct EcsExample { 24 | sprite_sheet: Image, 25 | player: Entity, 26 | } 27 | 28 | impl EcsExample { 29 | fn new(ctx: &mut Context) -> Result { 30 | let sprite_sheet = Image::from_file(ctx, "examples/res/0x72_dungeon_ii.png")?; 31 | 32 | let player = ecs::create_entity(ctx) 33 | .with(Transform { 34 | position: Vector2f::new(0.0, 0.0), 35 | }) 36 | .with(StaticSprite { 37 | source: Rectangle::::new(128, 76, 15, 20), 38 | }) 39 | .build(); 40 | 41 | Ok(Self { 42 | sprite_sheet, 43 | player, 44 | }) 45 | } 46 | } 47 | 48 | impl State for EcsExample { 49 | type Context = (); 50 | fn update(&mut self, ctx: &mut Context) -> Result<()> { 51 | let direction = { 52 | let mut direction = Vector2f::ZERO; 53 | 54 | if input::is_key_down(ctx, Key::A) { 55 | direction += -Vector2f::UNIT_X 56 | } 57 | 58 | if input::is_key_down(ctx, Key::D) { 59 | direction += Vector2f::UNIT_X; 60 | } 61 | 62 | if input::is_key_down(ctx, Key::W) { 63 | direction += -Vector2f::UNIT_Y; 64 | } 65 | 66 | if input::is_key_down(ctx, Key::S) { 67 | direction += Vector2f::UNIT_Y; 68 | } 69 | 70 | if direction == Vector2f::ZERO { 71 | direction 72 | } else { 73 | direction.normalize() 74 | } 75 | }; 76 | 77 | if let Some(transform) = ecs::get_component_mut::<_, Transform>(ctx, self.player) { 78 | let speed = 10.0; 79 | 80 | transform.position += direction * speed; 81 | } 82 | 83 | Ok(()) 84 | } 85 | 86 | fn draw(&mut self, ctx: &mut Context, _dt: f64) -> Result<()> { 87 | for entity in ecs::entities(ctx) { 88 | let transform = ecs::get_component::<_, Transform>(ctx, entity); 89 | let static_sprite = ecs::get_component::<_, StaticSprite>(ctx, entity); 90 | 91 | match (transform, static_sprite) { 92 | (Some(transform), Some(static_sprite)) => { 93 | let draw_params = DrawImageParams { 94 | position: transform.position, 95 | clip_rect: Some(static_sprite.source), 96 | scale: Some(Vector2f::new(8.0, 8.0)), 97 | ..Default::default() 98 | }; 99 | 100 | graphics::draw(ctx, &self.sprite_sheet, &draw_params)?; 101 | } 102 | _ => {} 103 | }; 104 | } 105 | 106 | Ok(()) 107 | } 108 | } 109 | 110 | fn main() -> Result<()> { 111 | ContextBuilder::new("Entity Component System", 1920, 1080) 112 | .build_empty()? 113 | .run_with_result(EcsExample::new) 114 | } 115 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at elliott.codes@gmail.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /src/ecs/world.rs: -------------------------------------------------------------------------------- 1 | use core::any::{Any, TypeId}; 2 | use std::collections::HashMap; 3 | use std::iter::FromIterator; 4 | 5 | use crate::ecs::{Allocator, Component, Entity, EntityBuilder}; 6 | 7 | pub(crate) struct World { 8 | pub(crate) entities: Vec, 9 | components: HashMap>>>, 10 | pub(crate) allocator: Allocator, 11 | } 12 | 13 | impl World { 14 | pub(crate) const DEFAULT_STORAGE_CAPACITY: usize = 256; 15 | 16 | pub fn new() -> Self { 17 | Self { 18 | entities: Vec::new(), 19 | components: HashMap::new(), 20 | allocator: Allocator::new(), 21 | } 22 | } 23 | 24 | pub fn create_entity<'a>(&'a mut self) -> EntityBuilder<'a> { 25 | EntityBuilder::new(self) 26 | } 27 | 28 | pub fn kill_entity(&mut self, entity: Entity) { 29 | self.entities.retain(|e| *e != entity); 30 | 31 | let all_component_types = Vec::from_iter(self.components.keys().map(Clone::clone)); 32 | for component_type in all_component_types { 33 | self.remove_component(entity, component_type); 34 | } 35 | 36 | self.allocator.kill(entity); 37 | } 38 | 39 | pub fn has_component(&self, entity: Entity) -> bool { 40 | self.get_component::(entity).is_some() 41 | } 42 | 43 | pub fn get_component(&self, entity: Entity) -> Option<&T> { 44 | let component_type = TypeId::of::(); 45 | let components = self.components.get(&component_type)?; 46 | 47 | components[entity.id() as usize] 48 | .as_ref() 49 | .and_then(|component| component.downcast_ref::()) 50 | } 51 | 52 | pub fn get_component_mut(&mut self, entity: Entity) -> Option<&mut T> { 53 | let component_type = TypeId::of::(); 54 | let components = self.components.get_mut(&component_type)?; 55 | 56 | components[entity.id() as usize] 57 | .as_mut() 58 | .and_then(|component| component.downcast_mut::()) 59 | } 60 | 61 | pub fn attach_component( 62 | &mut self, 63 | entity: Entity, 64 | component_type: TypeId, 65 | component: Box, 66 | ) { 67 | let components = self 68 | .components 69 | .entry(component_type) 70 | .or_insert(Vec::with_capacity(Self::DEFAULT_STORAGE_CAPACITY)); 71 | 72 | if components.len() <= entity.id() as usize { 73 | components.resize_with( 74 | components.len() + Self::DEFAULT_STORAGE_CAPACITY, 75 | Default::default, 76 | ); 77 | } 78 | 79 | components[entity.id() as usize] = Some(component); 80 | } 81 | 82 | pub fn remove_component(&mut self, entity: Entity, component_type: TypeId) { 83 | if !self.components.contains_key(&component_type) { 84 | return; 85 | } 86 | 87 | let components = self.components.get_mut(&component_type).unwrap(); 88 | if components.len() <= entity.id() as usize { 89 | return; 90 | } 91 | 92 | components[entity.id() as usize] = None; 93 | } 94 | } 95 | 96 | #[cfg(test)] 97 | mod tests { 98 | use super::*; 99 | 100 | use crate::Vector2f; 101 | 102 | #[derive(Debug)] 103 | struct Transform { 104 | pub position: Vector2f, 105 | } 106 | 107 | impl Component for Transform {} 108 | 109 | #[test] 110 | fn get_component_works() { 111 | let mut world = World::new(); 112 | 113 | let entity = world 114 | .create_entity() 115 | .with(Transform { 116 | position: Vector2f::new(10.0, 10.0), 117 | }) 118 | .build(); 119 | 120 | let transform = world.get_component::(entity); 121 | 122 | assert_eq!( 123 | transform.map(|transform| transform.position), 124 | Some(Vector2f::new(10.0, 10.0)) 125 | ); 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/graphics/image.rs: -------------------------------------------------------------------------------- 1 | use sdl2::image::LoadTexture; 2 | use sdl2::rect::Rect as SdlRect; 3 | 4 | use crate::error::{AnyhowContext, Sdl2Error}; 5 | use crate::graphics::{AssetRef, Color, Drawable, Rectangle}; 6 | use crate::{Context, Result, Vector2f, Vector2u}; 7 | 8 | #[derive(Debug)] 9 | pub struct Image { 10 | pub(crate) texture: AssetRef, 11 | } 12 | 13 | impl Image { 14 | pub fn from_file(ctx: &mut Context, filename: &str) -> Result { 15 | let texture_creator = ctx.canvas.texture_creator(); 16 | let texture = texture_creator 17 | .load_texture(filename) 18 | .map_err(Sdl2Error::ErrorMessage) 19 | .with_context(|| format!("Failed to create image from file: {}", filename))?; 20 | 21 | let texture_ref = AssetRef(ctx.graphics.counter); 22 | 23 | ctx.graphics.counter += 1; 24 | 25 | ctx.graphics.textures.insert(texture_ref, texture); 26 | 27 | Ok(Self { 28 | texture: texture_ref, 29 | }) 30 | } 31 | 32 | pub fn from_color(ctx: &mut Context, size: Vector2u, color: Color) -> Result { 33 | const ERROR_CONTEXT: &'static str = "Failed to create image from color"; 34 | 35 | let texture_creator = ctx.canvas.texture_creator(); 36 | let mut texture = texture_creator 37 | .create_texture_target(None, size.x, size.y) 38 | .context(ERROR_CONTEXT)?; 39 | 40 | ctx.canvas 41 | .with_texture_canvas(&mut texture, |texture_canvas| { 42 | texture_canvas.set_draw_color(color); 43 | texture_canvas.clear(); 44 | }) 45 | .context(ERROR_CONTEXT)?; 46 | 47 | let texture_ref = AssetRef(ctx.graphics.counter); 48 | 49 | ctx.graphics.counter += 1; 50 | 51 | ctx.graphics.textures.insert(texture_ref, texture); 52 | 53 | Ok(Self { 54 | texture: texture_ref, 55 | }) 56 | } 57 | } 58 | 59 | /// The parameters for drawing an [`Image`] to the current render target. 60 | #[derive(Debug)] 61 | pub struct DrawImageParams { 62 | /// The position at which to draw the [`Image`]. 63 | pub position: Vector2f, 64 | 65 | pub clip_rect: Option>, 66 | 67 | pub color: Option, 68 | 69 | pub scale: Option, 70 | } 71 | 72 | impl Default for DrawImageParams { 73 | fn default() -> Self { 74 | Self { 75 | position: Vector2f::ZERO, 76 | clip_rect: None, 77 | color: None, 78 | scale: None, 79 | } 80 | } 81 | } 82 | 83 | impl Drawable for Image { 84 | type Params = DrawImageParams; 85 | 86 | fn draw(&self, ctx: &mut Context, params: &DrawImageParams) -> Result<()> { 87 | let texture = ctx.graphics.textures.get_mut(&self.texture).unwrap(); 88 | let texture_query = texture.query(); 89 | 90 | let (width, height) = if let Some(clip_rect) = params.clip_rect { 91 | (clip_rect.width, clip_rect.height) 92 | } else { 93 | (texture_query.width as i32, texture_query.height as i32) 94 | }; 95 | 96 | let clip_rect = params.clip_rect.map(|clip_rect| { 97 | SdlRect::new( 98 | clip_rect.x, 99 | clip_rect.y, 100 | clip_rect.width as u32, 101 | clip_rect.height as u32, 102 | ) 103 | }); 104 | 105 | let scale = params.scale.unwrap_or(Vector2f::UNIT); 106 | 107 | let color = params.color.unwrap_or(Color::WHITE); 108 | texture.set_color_mod(color.r, color.g, color.b); 109 | texture.set_alpha_mod(color.a); 110 | 111 | ctx.canvas 112 | .copy( 113 | &texture, 114 | clip_rect, 115 | SdlRect::new( 116 | params.position.x as i32, 117 | params.position.y as i32, 118 | (width as f32 * scale.x) as u32, 119 | (height as f32 * scale.y) as u32, 120 | ), 121 | ) 122 | .map_err(Sdl2Error::ErrorMessage) 123 | .context("Failed to copy texture to canvas") 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Peacock 2 | 3 | [![Crates.io](https://img.shields.io/crates/v/peacock.svg)](https://crates.io/crates/peacock) 4 | [![Docs.rs](https://docs.rs/peacock/badge.svg)](https://docs.rs/peacock/) 5 | [![Crates.io](https://img.shields.io/crates/l/peacock.svg)](https://github.com/maxdeviant/peacock/blob/master/LICENSE) 6 | [![Travis](https://img.shields.io/travis/maxdeviant/peacock/master.svg?style=flat)](https://travis-ci.org/maxdeviant/peacock) 7 | 8 | Peacock is a game engine for making beautiful games. 9 | 10 | #### 🚧 UNDER CONSTRUCTION 🚧 11 | 12 | Peacock is still very much a work-in-progress. You're welcome to use it and report any issues you find, but I make no promises about stability or supporting your specific use cases until we hit `1.0.0`. 13 | 14 | ## Installation 15 | 16 | To install Peacock, add the following to your `Cargo.toml`: 17 | 18 | ```toml 19 | [dependencies] 20 | peacock = "0.0.1" 21 | ``` 22 | 23 | If you're like me and enjoy living on the bleeding edge, you can use a `git` dependency: 24 | 25 | ```toml 26 | [dependencies] 27 | peacock = { git = "https://github.com/maxdeviant/peacock" } 28 | ``` 29 | 30 | > Since we're still in the early days, I would suggest opting for the `git` dependency. 31 | 32 | ### Dependencies 33 | 34 | Peacock is built on top of [SDL2](https://www.libsdl.org/download-2.0.php), so you will need to install the SDL2 libraries for your platform. 35 | 36 | #### Nix/NixOS 37 | 38 | ```sh 39 | nix-shell 40 | ``` 41 | 42 | #### Debian/Ubuntu 43 | 44 | If you're running a Debian or Ubuntu-based Linux distribution you can (most likely) do the following: 45 | 46 | ```sh 47 | sudo apt install libsdl2-dev libsdl2-image-dev libsdl2-ttf-dev 48 | ``` 49 | 50 | ### macOS 51 | 52 | ```sh 53 | brew install sdl2 sdl2_image sdl2_ttf 54 | ``` 55 | 56 | For other platforms check out the [`rust-sdl2`](https://github.com/Rust-SDL2/rust-sdl2) docs. 57 | 58 | ## Usage 59 | 60 | ```rust 61 | use peacock::graphics::{self, Color}; 62 | use peacock::{Context, ContextBuilder, Result, State}; 63 | 64 | struct GameState; 65 | 66 | impl State for GameState { 67 | fn update(&mut self, ctx: &mut Context) -> Result<()> { 68 | Ok(()) 69 | } 70 | 71 | fn draw(&mut self, ctx: &mut Context, dt: f64) -> Result<()> { 72 | Ok(()) 73 | } 74 | } 75 | 76 | fn main() -> Result<()> { 77 | ContextBuilder::new("Hello, world!", 800, 600) 78 | .build()? 79 | .run(&mut GameState) 80 | } 81 | ``` 82 | 83 | ### Examples 84 | 85 | Check out the [`examples`](./examples) directory for more in-depth examples. 86 | 87 | ## Motivation 88 | 89 | ![xkcd: Standards](https://imgs.xkcd.com/comics/standards.png) 90 | 91 | > Fig 1: The state of Rust game engines. 92 | 93 | With the abundance of 2D game engines for Rust it does seem a little silly that I would add yet another one into the mix, so I'll do my best to explain my motivations for writing Peacock. 94 | 95 | Up until recently I was using [`ggez`](http://ggez.rs) for my games, but it got to the point where it was pretty frustrating to work with. The `0.4.0` branch had a lot of shortcomings, bugs, and APIs that were on the chopping block. This led me to switch to the `devel` branch, which cleaned up some stuff, but in turn had issues of its own. Now, in light of [The State of GGEZ 2019](https://wiki.alopex.li/TheStateOfGGEZ2019), I figured it would be a good time to search for greener pastures. 96 | 97 | [Tetra](https://tetra.seventeencups.net/) was the next engine to catch my eye, and at a glance it makes a lot of the same decisions I would for an engine. But having just come off `ggez` I found it hard to sell myself on an even more immature engine. Even the thought of getting in at the ground-floor and contributing to Tetra didn't quite appeal to me either. 98 | 99 | This got me thinking about what it is that I really want in a Rust game engine. After some thought, I think what it really comes down to is control. I want to be able to build games the way I want to without worrying about hitching my cart to an engine that goes down a direction that doesn't align with my goals. 100 | 101 | ## Goals 102 | 103 | - Safety 104 | - We're using Rust, so we want things to be safe 105 | - Some of our libraries make this difficult: 106 | - SDL2's C bindings are unsafe 107 | - Peacock doesn't directly depend on it (yet), but in userland `specs` likes to panic a lot 108 | - "Batteries Included" 109 | - Peacock should have everything in it that allows you to get games off the ground fast 110 | - This may mean including things that aren't generic enough for _every_ game (e.g., input, pathfinding, etc.) 111 | - We may be able to investigate making things modular here, but we don't want to overdo it and turn into [Piston](https://www.piston.rs/) 112 | - Consistent APIs 113 | 114 | ### Non-Goals 115 | 116 | Things that I don't care about at this juncture: 117 | 118 | - Targeting web and mobile devices 119 | - 3D support 120 | 121 | ## Prior Art 122 | 123 | Here are some of the engines that have served as inspiration for Peacock: 124 | 125 | - [ggez](http://ggez.rs) 126 | - [Tetra](https://tetra.seventeencups.net/) 127 | - [luxe](https://luxeengine.com/) 128 | - [MonoGame](http://www.monogame.net/) 129 | -------------------------------------------------------------------------------- /examples/orcmark.rs: -------------------------------------------------------------------------------- 1 | use rand::rngs::ThreadRng; 2 | use rand::{self, Rng}; 3 | 4 | use peacock::graphics::{self, DrawImageParams, Image, Rectangle}; 5 | use peacock::input::{self, Key}; 6 | use peacock::time; 7 | use peacock::window; 8 | use peacock::{ContextBuilder, Result, State, Vector2f}; 9 | 10 | type Context = peacock::Context<()>; 11 | 12 | const WIDTH: u32 = 1920; 13 | const HEIGHT: u32 = 1080; 14 | 15 | const INITIAL_ORCS: usize = 100; 16 | const ORC_SCALE: f32 = 2.0; 17 | 18 | const ORC_GRUNT_WIDTH: i32 = 11; 19 | const ORC_GRUNT_HEIGHT: i32 = 16; 20 | 21 | const ORC_SHAMAN_WIDTH: i32 = 11; 22 | const ORC_SHAMAN_HEIGHT: i32 = 15; 23 | 24 | const GRAVITY: f32 = 0.5; 25 | 26 | enum OrcKind { 27 | Grunt, 28 | Shaman, 29 | } 30 | 31 | struct Orc { 32 | kind: OrcKind, 33 | position: Vector2f, 34 | velocity: Vector2f, 35 | } 36 | 37 | impl Orc { 38 | fn new(rng: &mut ThreadRng) -> Self { 39 | let kind = if rng.gen::() { 40 | OrcKind::Grunt 41 | } else { 42 | OrcKind::Shaman 43 | }; 44 | 45 | let velocity = Vector2f::new(rng.gen::() * 5.0, rng.gen::() * 5.0 - 2.5); 46 | 47 | Self { 48 | kind, 49 | position: Vector2f::ZERO, 50 | velocity, 51 | } 52 | } 53 | } 54 | 55 | struct OrcMarkExample { 56 | rng: ThreadRng, 57 | sprite_sheet: Image, 58 | orcs: Vec, 59 | spawn_timer: i32, 60 | } 61 | 62 | impl OrcMarkExample { 63 | fn new(ctx: &mut Context) -> Result { 64 | let mut rng = rand::thread_rng(); 65 | let sprite_sheet = Image::from_file(ctx, "examples/res/0x72_dungeon_ii.png")?; 66 | let mut orcs = Vec::with_capacity(INITIAL_ORCS); 67 | 68 | for _ in 0..INITIAL_ORCS { 69 | orcs.push(Orc::new(&mut rng)); 70 | } 71 | 72 | Ok(Self { 73 | rng, 74 | sprite_sheet, 75 | orcs, 76 | spawn_timer: 0, 77 | }) 78 | } 79 | } 80 | 81 | impl State for OrcMarkExample { 82 | type Context = (); 83 | 84 | fn update(&mut self, ctx: &mut Context) -> Result<()> { 85 | if self.spawn_timer > 0 { 86 | self.spawn_timer -= 1; 87 | } 88 | 89 | if input::is_key_down(ctx, Key::Space) && self.spawn_timer == 0 { 90 | for _ in 0..INITIAL_ORCS { 91 | self.orcs.push(Orc::new(&mut self.rng)); 92 | } 93 | 94 | self.spawn_timer = 10; 95 | } 96 | 97 | for orc in &mut self.orcs { 98 | let (orc_width, orc_height) = match orc.kind { 99 | OrcKind::Grunt => (ORC_GRUNT_WIDTH, ORC_GRUNT_HEIGHT), 100 | OrcKind::Shaman => (ORC_SHAMAN_WIDTH, ORC_SHAMAN_HEIGHT), 101 | }; 102 | 103 | let max_x = WIDTH as f32 - (orc_width as f32 * ORC_SCALE); 104 | let max_y = HEIGHT as f32 - (orc_height as f32 * ORC_SCALE); 105 | 106 | orc.position += orc.velocity; 107 | orc.velocity.y += GRAVITY; 108 | 109 | if orc.position.x > max_x { 110 | orc.velocity.x *= -1.0; 111 | orc.position.x = max_x; 112 | } else if orc.position.x < 0.0 { 113 | orc.velocity.x *= -1.0; 114 | orc.position.x = 0.0; 115 | } 116 | 117 | if orc.position.y > max_y { 118 | orc.velocity.y *= -0.8; 119 | orc.position.y = max_y; 120 | 121 | if self.rng.gen::() { 122 | orc.velocity.y -= 3.0 + (self.rng.gen::() * 4.0); 123 | } 124 | } else if orc.position.y < 0.0 { 125 | orc.velocity.y = 0.0; 126 | orc.position.y = 0.0; 127 | } 128 | } 129 | 130 | Ok(()) 131 | } 132 | 133 | fn draw(&mut self, ctx: &mut Context, _dt: f64) -> Result<()> { 134 | for orc in &self.orcs { 135 | let clip_rect = match orc.kind { 136 | OrcKind::Grunt => { 137 | Rectangle::::new(372, 208, ORC_GRUNT_WIDTH, ORC_GRUNT_HEIGHT) 138 | } 139 | OrcKind::Shaman => { 140 | Rectangle::::new(372, 241, ORC_SHAMAN_WIDTH, ORC_SHAMAN_HEIGHT) 141 | } 142 | }; 143 | 144 | graphics::draw( 145 | ctx, 146 | &self.sprite_sheet, 147 | &DrawImageParams { 148 | position: orc.position, 149 | clip_rect: Some(clip_rect), 150 | scale: Some(Vector2f::new(ORC_SCALE, ORC_SCALE)), 151 | ..Default::default() 152 | }, 153 | )?; 154 | } 155 | 156 | window::set_title( 157 | ctx, 158 | &format!( 159 | "OrcMark - {} orcs - {:.0} FPS", 160 | self.orcs.len(), 161 | time::get_fps(ctx) 162 | ), 163 | ); 164 | 165 | Ok(()) 166 | } 167 | } 168 | 169 | fn main() -> Result<()> { 170 | ContextBuilder::new("OrcMark", WIDTH, HEIGHT) 171 | .build_empty()? 172 | .run_with_result(OrcMarkExample::new) 173 | } 174 | -------------------------------------------------------------------------------- /src/vector2.rs: -------------------------------------------------------------------------------- 1 | use std::ops::{Add, AddAssign, Div, DivAssign, Mul, MulAssign, Neg, Sub, SubAssign}; 2 | 3 | /// A two-dimensional vector. 4 | #[derive(Clone, PartialOrd, Ord, PartialEq, Eq, Debug, Copy, Default, Hash)] 5 | pub struct Vector2 { 6 | /// The x-component of the vector. 7 | pub x: T, 8 | 9 | /// The y-component of the vector. 10 | pub y: T, 11 | } 12 | 13 | impl Vector2 { 14 | /// Returns a new [`Vector2`] with the given components. 15 | pub fn new(x: T, y: T) -> Self { 16 | Self { x, y } 17 | } 18 | } 19 | 20 | impl From<(T, T)> for Vector2 { 21 | fn from((x, y): (T, T)) -> Self { 22 | Self { x, y } 23 | } 24 | } 25 | 26 | macro_rules! impl_ops { 27 | ( $_trait:ident, $_func:ident, $( $_type:ty ),+ ) => { 28 | impl $_trait for Vector2 { 29 | type Output = Vector2; 30 | 31 | fn $_func(self, rhs: T) -> Vector2 { 32 | Vector2 { 33 | x: $_trait::$_func(self.x, rhs), 34 | y: $_trait::$_func(self.y, rhs) 35 | } 36 | } 37 | } 38 | 39 | $( 40 | impl $_trait> for $_type { 41 | type Output = Vector2<$_type>; 42 | 43 | fn $_func(self, rhs: Vector2<$_type>) -> Vector2<$_type> { 44 | Vector2 { 45 | x: $_trait::$_func(self, rhs.x), 46 | y: $_trait::$_func(self, rhs.y) 47 | } 48 | } 49 | } 50 | )+ 51 | } 52 | } 53 | 54 | impl_ops!(Add, add, i32, u32, f32); 55 | impl_ops!(Sub, sub, i32, u32, f32); 56 | impl_ops!(Mul, mul, i32, u32, f32); 57 | impl_ops!(Div, div, i32, u32, f32); 58 | 59 | impl Add for Vector2 { 60 | type Output = Vector2; 61 | 62 | fn add(self, rhs: Vector2) -> Vector2 { 63 | Vector2 { 64 | x: self.x + rhs.x, 65 | y: self.y + rhs.y, 66 | } 67 | } 68 | } 69 | 70 | impl AddAssign for Vector2 { 71 | fn add_assign(&mut self, rhs: Self) { 72 | self.x += rhs.x; 73 | self.y += rhs.y; 74 | } 75 | } 76 | 77 | impl Sub for Vector2 { 78 | type Output = Vector2; 79 | 80 | fn sub(self, rhs: Vector2) -> Vector2 { 81 | Vector2 { 82 | x: self.x - rhs.x, 83 | y: self.y - rhs.y, 84 | } 85 | } 86 | } 87 | 88 | impl SubAssign for Vector2 { 89 | fn sub_assign(&mut self, rhs: Self) { 90 | self.x -= rhs.x; 91 | self.y -= rhs.y; 92 | } 93 | } 94 | 95 | impl Mul for Vector2 { 96 | type Output = Vector2; 97 | 98 | fn mul(self, rhs: Vector2) -> Vector2 { 99 | Vector2 { 100 | x: self.x * rhs.x, 101 | y: self.y * rhs.y, 102 | } 103 | } 104 | } 105 | 106 | impl MulAssign for Vector2 { 107 | fn mul_assign(&mut self, rhs: T) { 108 | self.x *= rhs; 109 | self.y *= rhs; 110 | } 111 | } 112 | 113 | impl Div for Vector2 { 114 | type Output = Vector2; 115 | 116 | fn div(self, rhs: Vector2) -> Vector2 { 117 | Vector2 { 118 | x: self.x / rhs.x, 119 | y: self.y / rhs.y, 120 | } 121 | } 122 | } 123 | 124 | impl DivAssign for Vector2 { 125 | fn div_assign(&mut self, rhs: T) { 126 | self.x /= rhs; 127 | self.y /= rhs; 128 | } 129 | } 130 | 131 | impl> Neg for Vector2 { 132 | type Output = Self; 133 | 134 | fn neg(self) -> Self { 135 | Vector2 { 136 | x: -self.x, 137 | y: -self.y, 138 | } 139 | } 140 | } 141 | 142 | /// A [`Vector2`] with [`i32`] components. 143 | pub type Vector2i = Vector2; 144 | 145 | impl Vector2i { 146 | /// A vector with components (0, 0). 147 | pub const ZERO: Self = Self { x: 0, y: 0 }; 148 | 149 | /// A vector with components (1, 0). 150 | pub const UNIT_X: Self = Self { x: 1, y: 0 }; 151 | 152 | /// A vector with components (0, 1). 153 | pub const UNIT_Y: Self = Self { x: 0, y: 1 }; 154 | 155 | /// A vector with components (1, 1). 156 | pub const UNIT: Self = Self { x: 1, y: 1 }; 157 | } 158 | 159 | /// A [`Vector2`] with [`u32`] components. 160 | pub type Vector2u = Vector2; 161 | 162 | impl Vector2u { 163 | /// A vector with components (0, 0). 164 | pub const ZERO: Self = Self { x: 0u32, y: 0u32 }; 165 | 166 | /// A vector with components (1, 0). 167 | pub const UNIT_X: Self = Self { x: 1u32, y: 0u32 }; 168 | 169 | /// A vector with components (0, 1). 170 | pub const UNIT_Y: Self = Self { x: 0u32, y: 1u32 }; 171 | 172 | /// A vector with components (1, 1). 173 | pub const UNIT: Self = Self { x: 1u32, y: 1u32 }; 174 | } 175 | 176 | /// A [`Vector2`] with [`f32`] components. 177 | pub type Vector2f = Vector2; 178 | 179 | impl Vector2f { 180 | /// A vector with components (0, 0). 181 | pub const ZERO: Self = Self { x: 0.0, y: 0.0 }; 182 | 183 | /// A vector with components (1, 0). 184 | pub const UNIT_X: Self = Self { x: 1.0, y: 0.0 }; 185 | 186 | /// A vector with components (0, 1). 187 | pub const UNIT_Y: Self = Self { x: 0.0, y: 1.0 }; 188 | 189 | /// A vector with components (1, 1). 190 | pub const UNIT: Self = Self { x: 1.0, y: 1.0 }; 191 | 192 | pub fn normalize(self) -> Self { 193 | let value = 1.0 / f32::sqrt((self.x * self.x) + (self.y * self.y)); 194 | Self { 195 | x: self.x * value, 196 | y: self.y * value, 197 | } 198 | } 199 | } 200 | 201 | #[cfg(test)] 202 | mod tests { 203 | use super::*; 204 | 205 | #[test] 206 | fn it_can_use_the_addition_operator() { 207 | assert_eq!( 208 | Vector2i::new(1, 2) + Vector2i::new(3, 4), 209 | Vector2i::new(4, 6) 210 | ); 211 | } 212 | } 213 | -------------------------------------------------------------------------------- /examples/color_palette.rs: -------------------------------------------------------------------------------- 1 | use peacock::graphics::{self, Color, DrawImageParams, Image}; 2 | use peacock::{ContextBuilder, Result, State}; 3 | 4 | type Context = peacock::Context<()>; 5 | 6 | struct ColorPaletteExample { 7 | swatches: Vec, 8 | } 9 | 10 | impl ColorPaletteExample { 11 | fn new(ctx: &mut Context) -> Result { 12 | let all_colors = vec![ 13 | // Pinks 14 | Color::PINK, 15 | Color::LIGHT_PINK, 16 | Color::HOT_PINK, 17 | Color::DEEP_PINK, 18 | Color::PALE_VIOLET_RED, 19 | Color::MEDIUM_VIOLET_RED, 20 | // Reds 21 | Color::LIGHT_SALMON, 22 | Color::SALMON, 23 | Color::DARK_SALMON, 24 | Color::LIGHT_CORAL, 25 | Color::INDIAN_RED, 26 | Color::CRIMSON, 27 | Color::FIREBRICK, 28 | Color::DARK_RED, 29 | Color::RED, 30 | // Oranges 31 | Color::ORANGE_RED, 32 | Color::TOMATO, 33 | Color::CORAL, 34 | Color::DARK_ORANGE, 35 | Color::ORANGE, 36 | // Yellows 37 | Color::YELLOW, 38 | Color::LIGHT_YELLOW, 39 | Color::LEMON_CHIFFON, 40 | Color::LIGHT_GOLDENROD_YELLOW, 41 | Color::PAPAYA_WHIP, 42 | Color::MOCCASIN, 43 | Color::PEACH_PUFF, 44 | Color::PALE_GOLDENROD, 45 | Color::KHAKI, 46 | Color::DARK_KHAKI, 47 | Color::GOLD, 48 | // Browns 49 | Color::CORNSILK, 50 | Color::BLANCHED_ALMOND, 51 | Color::BISQUE, 52 | Color::NAVAJO_WHITE, 53 | Color::WHEAT, 54 | Color::BURLYWOOD, 55 | Color::TAN, 56 | Color::ROSY_BROWN, 57 | Color::SANDY_BROWN, 58 | Color::GOLDENROD, 59 | Color::DARK_GOLDENROD, 60 | Color::PERU, 61 | Color::CHOCOLATE, 62 | Color::SADDLE_BROWN, 63 | Color::SIENNA, 64 | Color::BROWN, 65 | Color::MAROON, 66 | // Greens 67 | Color::DARK_OLIVE_GREEN, 68 | Color::OLIVE, 69 | Color::OLIVE_DRAB, 70 | Color::YELLOW_GREEN, 71 | Color::LIME_GREEN, 72 | Color::LIME, 73 | Color::LAWN_GREEN, 74 | Color::CHARTREUSE, 75 | Color::GREEN_YELLOW, 76 | Color::SPRING_GREEN, 77 | Color::MEDIUM_SPRING_GREEN, 78 | Color::LIGHT_GREEN, 79 | Color::PALE_GREEN, 80 | Color::DARK_SEA_GREEN, 81 | Color::MEDIUM_AQUAMARINE, 82 | Color::MEDIUM_SEA_GREEN, 83 | Color::SEA_GREEN, 84 | Color::FOREST_GREEN, 85 | Color::GREEN, 86 | Color::DARK_GREEN, 87 | // Cyans 88 | Color::AQUA, 89 | Color::CYAN, 90 | Color::LIGHT_CYAN, 91 | Color::PALE_TURQUOISE, 92 | Color::AQUAMARINE, 93 | Color::TURQUOISE, 94 | Color::MEDIUM_TURQUOISE, 95 | Color::DARK_TURQUOISE, 96 | Color::LIGHT_SEA_GREEN, 97 | Color::CADET_BLUE, 98 | Color::DARK_CYAN, 99 | Color::TEAL, 100 | // Blues 101 | Color::LIGHT_STEEL_BLUE, 102 | Color::POWDER_BLUE, 103 | Color::LIGHT_BLUE, 104 | Color::SKY_BLUE, 105 | Color::LIGHT_SKY_BLUE, 106 | Color::DEEP_SKY_BLUE, 107 | Color::DODGER_BLUE, 108 | Color::CORNFLOWER_BLUE, 109 | Color::STEEL_BLUE, 110 | Color::ROYAL_BLUE, 111 | Color::BLUE, 112 | Color::MEDIUM_BLUE, 113 | Color::DARK_BLUE, 114 | Color::NAVY, 115 | Color::MIDNIGHT_BLUE, 116 | // Purples, violets, and magentas 117 | Color::LAVENDER, 118 | Color::THISTLE, 119 | Color::PLUM, 120 | Color::VIOLET, 121 | Color::ORCHID, 122 | Color::FUCHSIA, 123 | Color::MAGENTA, 124 | Color::MEDIUM_ORCHID, 125 | Color::MEDIUM_PURPLE, 126 | Color::BLUE_VIOLET, 127 | Color::DARK_VIOLET, 128 | Color::DARK_ORCHID, 129 | Color::DARK_MAGENTA, 130 | Color::PURPLE, 131 | Color::INDIGO, 132 | Color::DARK_SLATE_BLUE, 133 | Color::SLATE_BLUE, 134 | Color::MEDIUM_SLATE_BLUE, 135 | // Whites 136 | Color::WHITE, 137 | Color::SNOW, 138 | Color::HONEYDEW, 139 | Color::MINT_CREAM, 140 | Color::AZURE, 141 | Color::ALICE_BLUE, 142 | Color::GHOST_WHITE, 143 | Color::WHITE_SMOKE, 144 | Color::SEASHELL, 145 | Color::BEIGE, 146 | Color::OLD_LACE, 147 | Color::FLORAL_WHITE, 148 | Color::IVORY, 149 | Color::ANTIQUE_WHITE, 150 | Color::LINEN, 151 | Color::LAVENDER_BLUSH, 152 | Color::MISTY_ROSE, 153 | // Grays and blacks 154 | Color::GAINSBORO, 155 | Color::LIGHT_GRAY, 156 | Color::SILVER, 157 | Color::DARK_GRAY, 158 | Color::GRAY, 159 | Color::DIM_GRAY, 160 | Color::LIGHT_SLATE_GRAY, 161 | Color::SLATE_GRAY, 162 | Color::DARK_SLATE_GRAY, 163 | Color::BLACK, 164 | ]; 165 | 166 | let mut swatches = Vec::with_capacity(all_colors.len()); 167 | for color in all_colors { 168 | swatches.push(Image::from_color(ctx, (32, 32).into(), color)?); 169 | } 170 | 171 | Ok(Self { swatches }) 172 | } 173 | } 174 | 175 | impl State for ColorPaletteExample { 176 | type Context = (); 177 | 178 | fn update(&mut self, _ctx: &mut Context) -> Result<()> { 179 | Ok(()) 180 | } 181 | 182 | fn draw(&mut self, ctx: &mut Context, _dt: f64) -> Result<()> { 183 | graphics::clear(ctx, Color::BLACK); 184 | 185 | let (width, height) = (12, 12); 186 | 187 | for x in 0..width { 188 | for y in 0..height { 189 | let index = x + (y * width); 190 | if index > self.swatches.len() - 1 { 191 | break; 192 | } 193 | 194 | graphics::draw( 195 | ctx, 196 | &self.swatches[index], 197 | &DrawImageParams { 198 | position: (x as f32 * 32.0, y as f32 * 32.0).into(), 199 | ..Default::default() 200 | }, 201 | )?; 202 | } 203 | } 204 | 205 | Ok(()) 206 | } 207 | } 208 | 209 | fn main() -> Result<()> { 210 | ContextBuilder::new("Color Palette", 384, 384) 211 | .build_empty()? 212 | .run_with_result(ColorPaletteExample::new) 213 | } 214 | -------------------------------------------------------------------------------- /src/input.rs: -------------------------------------------------------------------------------- 1 | pub mod mouse; 2 | 3 | use hashbrown::HashSet; 4 | use sdl2::event::Event; 5 | use sdl2::keyboard::Keycode as SdlKeycode; 6 | 7 | use crate::{Context, Result, Vector2f}; 8 | 9 | #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] 10 | pub enum Key { 11 | /// An unknown key. 12 | Unknown, 13 | 14 | /// The `A` key. 15 | A, 16 | 17 | /// The `B` key. 18 | B, 19 | 20 | /// The `C` key. 21 | C, 22 | 23 | /// The `D` key. 24 | D, 25 | 26 | /// The `E` key. 27 | E, 28 | 29 | /// The `F` key. 30 | F, 31 | 32 | /// The `G` key. 33 | G, 34 | 35 | /// The `H` key. 36 | H, 37 | 38 | /// The `I` key. 39 | I, 40 | 41 | /// The `J` key. 42 | J, 43 | 44 | /// The `K` key. 45 | K, 46 | 47 | /// The `L` key. 48 | L, 49 | 50 | /// The `M` key. 51 | M, 52 | 53 | /// The `N` key. 54 | N, 55 | 56 | /// The `O` key. 57 | O, 58 | 59 | /// The `P` key. 60 | P, 61 | 62 | /// The `Q` key. 63 | Q, 64 | 65 | /// The `R` key. 66 | R, 67 | 68 | /// The `S` key. 69 | S, 70 | 71 | /// The `T` key. 72 | T, 73 | 74 | /// The `U` key. 75 | U, 76 | 77 | /// The `V` key. 78 | V, 79 | 80 | /// The `W` key. 81 | W, 82 | 83 | /// The `X` key. 84 | X, 85 | 86 | /// The `Y` key. 87 | Y, 88 | 89 | /// The `Z` key. 90 | Z, 91 | 92 | /// The `0` key. 93 | Num0, 94 | 95 | /// The `1` key. 96 | Num1, 97 | 98 | /// The `2` key. 99 | Num2, 100 | 101 | /// The `3` key. 102 | Num3, 103 | 104 | /// The `4` key. 105 | Num4, 106 | 107 | /// The `5` key. 108 | Num5, 109 | 110 | /// The `6` key. 111 | Num6, 112 | 113 | /// The `7` key. 114 | Num7, 115 | 116 | /// The `8` key. 117 | Num8, 118 | 119 | /// The `9` key. 120 | Num9, 121 | 122 | /// The `F1` key. 123 | F1, 124 | 125 | /// The `F2` key. 126 | F2, 127 | 128 | /// The `F3` key. 129 | F3, 130 | 131 | /// The `F4` key. 132 | F4, 133 | 134 | /// The `F5` key. 135 | F5, 136 | 137 | /// The `F6` key. 138 | F6, 139 | 140 | /// The `F7` key. 141 | F7, 142 | 143 | /// The `F8` key. 144 | F8, 145 | 146 | /// The `F9` key. 147 | F9, 148 | 149 | /// The `F10` key. 150 | F10, 151 | 152 | /// The `F11` key. 153 | F11, 154 | 155 | /// The `F12` key. 156 | F12, 157 | 158 | /// The left arrow. 159 | Left, 160 | 161 | /// The right arrow. 162 | Right, 163 | 164 | /// The up arrow. 165 | Up, 166 | 167 | /// The down arrow. 168 | Down, 169 | 170 | /// The `Space` key. 171 | Space, 172 | } 173 | 174 | impl From for Key { 175 | fn from(key: SdlKeycode) -> Self { 176 | match key { 177 | SdlKeycode::A => Key::A, 178 | SdlKeycode::B => Key::B, 179 | SdlKeycode::C => Key::C, 180 | SdlKeycode::D => Key::D, 181 | SdlKeycode::E => Key::E, 182 | SdlKeycode::F => Key::F, 183 | SdlKeycode::G => Key::G, 184 | SdlKeycode::H => Key::H, 185 | SdlKeycode::I => Key::I, 186 | SdlKeycode::J => Key::J, 187 | SdlKeycode::K => Key::K, 188 | SdlKeycode::L => Key::L, 189 | SdlKeycode::M => Key::M, 190 | SdlKeycode::N => Key::N, 191 | SdlKeycode::O => Key::O, 192 | SdlKeycode::P => Key::P, 193 | SdlKeycode::Q => Key::Q, 194 | SdlKeycode::R => Key::R, 195 | SdlKeycode::S => Key::S, 196 | SdlKeycode::T => Key::T, 197 | SdlKeycode::U => Key::U, 198 | SdlKeycode::V => Key::V, 199 | SdlKeycode::W => Key::W, 200 | SdlKeycode::X => Key::X, 201 | SdlKeycode::Y => Key::Y, 202 | SdlKeycode::Z => Key::Z, 203 | SdlKeycode::Num0 => Key::Num0, 204 | SdlKeycode::Num1 => Key::Num1, 205 | SdlKeycode::Num2 => Key::Num2, 206 | SdlKeycode::Num3 => Key::Num3, 207 | SdlKeycode::Num4 => Key::Num4, 208 | SdlKeycode::Num5 => Key::Num5, 209 | SdlKeycode::Num6 => Key::Num6, 210 | SdlKeycode::Num7 => Key::Num7, 211 | SdlKeycode::Num8 => Key::Num8, 212 | SdlKeycode::Num9 => Key::Num9, 213 | SdlKeycode::F1 => Key::F1, 214 | SdlKeycode::F2 => Key::F2, 215 | SdlKeycode::F3 => Key::F3, 216 | SdlKeycode::F4 => Key::F4, 217 | SdlKeycode::F5 => Key::F5, 218 | SdlKeycode::F6 => Key::F6, 219 | SdlKeycode::F7 => Key::F7, 220 | SdlKeycode::F8 => Key::F8, 221 | SdlKeycode::F9 => Key::F9, 222 | SdlKeycode::F10 => Key::F10, 223 | SdlKeycode::F11 => Key::F11, 224 | SdlKeycode::F12 => Key::F12, 225 | SdlKeycode::Left => Key::Left, 226 | SdlKeycode::Right => Key::Right, 227 | SdlKeycode::Up => Key::Up, 228 | SdlKeycode::Down => Key::Down, 229 | SdlKeycode::Space => Key::Space, 230 | _ => Key::Unknown, 231 | } 232 | } 233 | } 234 | 235 | pub(crate) struct KeyboardContext { 236 | last_pressed_keys: HashSet, 237 | pressed_keys: HashSet, 238 | } 239 | 240 | impl KeyboardContext { 241 | pub(crate) fn new() -> Self { 242 | Self { 243 | last_pressed_keys: HashSet::with_capacity(256), 244 | pressed_keys: HashSet::with_capacity(256), 245 | } 246 | } 247 | } 248 | 249 | pub(crate) struct MouseContext { 250 | position: Vector2f, 251 | } 252 | 253 | impl MouseContext { 254 | pub(crate) fn new() -> Self { 255 | Self { 256 | position: Vector2f::ZERO, 257 | } 258 | } 259 | } 260 | 261 | pub(crate) fn handle_event(ctx: &mut Context, event: Event) -> Result<()> { 262 | match event { 263 | Event::KeyDown { keycode, .. } => { 264 | if let Some(keycode) = keycode { 265 | ctx.keyboard.pressed_keys.insert(keycode.into()); 266 | } 267 | } 268 | Event::KeyUp { keycode, .. } => { 269 | if let Some(keycode) = keycode { 270 | ctx.keyboard.pressed_keys.remove(&keycode.into()); 271 | } 272 | } 273 | Event::MouseMotion { x, y, .. } => { 274 | ctx.mouse.position = Vector2f::new(x as f32, y as f32); 275 | } 276 | _ => {} 277 | }; 278 | 279 | Ok(()) 280 | } 281 | 282 | pub(crate) fn cleanup_after_state_update(ctx: &mut Context) { 283 | ctx.keyboard.last_pressed_keys = ctx.keyboard.pressed_keys.clone(); 284 | } 285 | 286 | /// Returns whether the specified [`Key`] is down. 287 | pub fn is_key_down(ctx: &Context, key: Key) -> bool { 288 | ctx.keyboard.pressed_keys.contains(&key) 289 | } 290 | 291 | /// Returns whether the specified [`Key`] is up. 292 | pub fn is_key_up(ctx: &Context, key: Key) -> bool { 293 | !ctx.keyboard.pressed_keys.contains(&key) 294 | } 295 | 296 | pub fn was_key_pressed(ctx: &Context, key: Key) -> bool { 297 | !ctx.keyboard.last_pressed_keys.contains(&key) && is_key_down(ctx, key) 298 | } 299 | 300 | pub fn was_key_released(ctx: &Context, key: Key) -> bool { 301 | ctx.keyboard.last_pressed_keys.contains(&key) && is_key_up(ctx, key) 302 | } 303 | -------------------------------------------------------------------------------- /src/context.rs: -------------------------------------------------------------------------------- 1 | use std::time::{Duration, Instant}; 2 | 3 | use lazy_static::*; 4 | use sdl2::event::Event; 5 | use sdl2::render::Canvas; 6 | use sdl2::ttf::Sdl2TtfContext; 7 | use sdl2::video::Window; 8 | use sdl2::Sdl; 9 | 10 | use crate::ecs::World; 11 | use crate::error::{AnyhowContext, Result, Sdl2Error}; 12 | use crate::graphics::{self, Color, GraphicsContext}; 13 | use crate::input::{self, KeyboardContext, MouseContext}; 14 | use crate::time; 15 | use crate::{FpsTracker, State}; 16 | 17 | lazy_static! { 18 | pub(crate) static ref SDL_TTF_CONTEXT: Sdl2TtfContext = sdl2::ttf::init().unwrap(); 19 | } 20 | 21 | pub struct Context { 22 | pub(crate) sdl_context: Sdl, 23 | pub(crate) canvas: Canvas, 24 | is_running: bool, 25 | tick_rate: Duration, 26 | pub(crate) fps_tracker: FpsTracker, 27 | pub(crate) world: World, 28 | pub(crate) graphics: GraphicsContext, 29 | pub(crate) keyboard: KeyboardContext, 30 | pub(crate) mouse: MouseContext, 31 | game: G, 32 | } 33 | 34 | impl Context { 35 | /// Returns the game context. 36 | pub fn game<'a, 'b>(&'a self) -> &'b G { 37 | // Extend the lifetime of the game context so that we can borrow it at 38 | // the same time as the rest of the context. 39 | // 40 | // This *should* be safe because: 41 | // (1) The game context is opaque within Peacock, so nothing in the 42 | // engine will be modifying it, 43 | // (2) the game context is not modifiable externally unless 44 | // (3) the caller obtains a mutable reference with `game_mut`, which 45 | // is then checked by the borrow checker. 46 | unsafe { std::mem::transmute::<&'a G, &'b G>(&self.game) } 47 | } 48 | 49 | /// Returns a mutable reference to game context. 50 | pub fn game_mut(&mut self) -> &mut G { 51 | &mut self.game 52 | } 53 | 54 | /// Runs the context using the provided game state. 55 | pub fn run(&mut self, state: &mut S) -> Result<()> 56 | where 57 | S: State, 58 | { 59 | let mut last_time = Instant::now(); 60 | let mut lag = Duration::from_secs(0); 61 | 62 | self.is_running = true; 63 | 64 | let mut event_pump = self 65 | .sdl_context 66 | .event_pump() 67 | .map_err(Sdl2Error::ErrorMessage) 68 | .context("Failed to obtain the SDL2 event pump")?; 69 | 70 | while self.is_running { 71 | let current_time = Instant::now(); 72 | let elapsed_time = current_time - last_time; 73 | last_time = current_time; 74 | lag += elapsed_time; 75 | 76 | self.fps_tracker.tick(elapsed_time); 77 | 78 | for event in event_pump.poll_iter() { 79 | if let Err(err) = self 80 | .handle_event(event) 81 | .and_then(|event| input::handle_event(self, event)) 82 | { 83 | self.is_running = false; 84 | return Err(err); 85 | } 86 | } 87 | 88 | while lag >= self.tick_rate { 89 | if let Err(err) = state.update(self) { 90 | self.is_running = false; 91 | return Err(err); 92 | } 93 | 94 | input::cleanup_after_state_update(self); 95 | lag -= self.tick_rate; 96 | } 97 | 98 | let dt = time::duration_to_f64(lag) / time::duration_to_f64(self.tick_rate); 99 | 100 | graphics::clear(self, Color::CADET_BLUE); 101 | 102 | if let Err(err) = state.draw(self, dt) { 103 | self.is_running = false; 104 | return Err(err); 105 | } 106 | 107 | self.canvas.present(); 108 | 109 | std::thread::yield_now(); 110 | } 111 | 112 | Ok(()) 113 | } 114 | 115 | /// Runs the context using the game state returned from the provided function. 116 | pub fn run_with(&mut self, get_state: F) -> Result<()> 117 | where 118 | S: State, 119 | F: FnOnce(&mut Self) -> S, 120 | { 121 | let mut state = get_state(self); 122 | self.run(&mut state) 123 | } 124 | 125 | /// Runs the context using the game state returned from the provided (fallible) function. 126 | pub fn run_with_result(&mut self, get_state: F) -> Result<()> 127 | where 128 | S: State, 129 | F: FnOnce(&mut Self) -> Result, 130 | { 131 | let mut state = get_state(self)?; 132 | self.run(&mut state) 133 | } 134 | 135 | fn handle_event(&mut self, event: Event) -> Result { 136 | match event { 137 | Event::Quit { .. } => self.is_running = false, 138 | _ => {} 139 | } 140 | 141 | Ok(event) 142 | } 143 | } 144 | 145 | #[derive(Debug, Clone)] 146 | pub struct ContextBuilder<'a> { 147 | title: &'a str, 148 | width: u32, 149 | height: u32, 150 | vsync: bool, 151 | tick_rate: f64, 152 | fullscreen: bool, 153 | quit_on_escape: bool, 154 | } 155 | 156 | impl<'a> ContextBuilder<'a> { 157 | pub fn new(title: &'a str, width: u32, height: u32) -> Self { 158 | Self { 159 | title, 160 | width, 161 | height, 162 | ..ContextBuilder::default() 163 | } 164 | } 165 | 166 | pub fn vsync(&mut self, vsync: bool) -> &mut Self { 167 | self.vsync = vsync; 168 | self 169 | } 170 | 171 | pub fn tick_rate(&mut self, tick_rate: f64) -> &mut Self { 172 | self.tick_rate = tick_rate; 173 | self 174 | } 175 | 176 | pub fn fullscreen(&mut self, fullscreen: bool) -> &mut Self { 177 | self.fullscreen = fullscreen; 178 | self 179 | } 180 | 181 | pub fn build(&self, build_game_ctx: F) -> Result> 182 | where 183 | F: FnOnce(&mut Context<()>) -> Result, 184 | { 185 | let sdl_context = sdl2::init() 186 | .map_err(Sdl2Error::ErrorMessage) 187 | .context("Failed to initialize SDL2 context")?; 188 | let video_subsystem = sdl_context 189 | .video() 190 | .map_err(Sdl2Error::ErrorMessage) 191 | .context("Failed to initialize SDL2 video subsystem")?; 192 | 193 | let window = video_subsystem 194 | .window(self.title, self.width, self.height) 195 | .position_centered() 196 | .build() 197 | .context("Failed to build SDL2 window")?; 198 | 199 | let canvas = window 200 | .into_canvas() 201 | .build() 202 | .context("Failed to build SDL2 canvas")?; 203 | 204 | let mut ctx = Context { 205 | sdl_context, 206 | canvas, 207 | is_running: false, 208 | tick_rate: time::f64_to_duration(self.tick_rate), 209 | fps_tracker: FpsTracker::new(), 210 | world: World::new(), 211 | graphics: GraphicsContext::new(), 212 | keyboard: KeyboardContext::new(), 213 | mouse: MouseContext::new(), 214 | game: (), 215 | }; 216 | 217 | let game_ctx = build_game_ctx(&mut ctx)?; 218 | 219 | Ok(Context { 220 | sdl_context: ctx.sdl_context, 221 | canvas: ctx.canvas, 222 | is_running: ctx.is_running, 223 | tick_rate: ctx.tick_rate, 224 | fps_tracker: ctx.fps_tracker, 225 | world: ctx.world, 226 | graphics: ctx.graphics, 227 | keyboard: ctx.keyboard, 228 | mouse: ctx.mouse, 229 | game: game_ctx, 230 | }) 231 | } 232 | 233 | pub fn build_empty(&self) -> Result> { 234 | self.build(|_| Ok(())) 235 | } 236 | } 237 | 238 | impl<'a> Default for ContextBuilder<'a> { 239 | fn default() -> Self { 240 | Self { 241 | title: "Game", 242 | width: 800, 243 | height: 600, 244 | vsync: true, 245 | tick_rate: 1.0 / 60.0, 246 | fullscreen: false, 247 | quit_on_escape: true, 248 | } 249 | } 250 | } 251 | -------------------------------------------------------------------------------- /src/graphics/color.rs: -------------------------------------------------------------------------------- 1 | use sdl2::pixels::Color as SdlColor; 2 | 3 | /// An RGBA color. 4 | #[derive(Copy, Clone, PartialEq, Eq, Debug)] 5 | pub struct Color { 6 | /// The red component. 7 | pub r: u8, 8 | 9 | /// The green component. 10 | pub g: u8, 11 | 12 | /// The blue component. 13 | pub b: u8, 14 | 15 | /// The alpha component. 16 | pub a: u8, 17 | } 18 | 19 | macro_rules! predefined_rgb { 20 | ($name:ident, $r:expr, $g:expr, $b:expr) => { 21 | /// A predefined RGB [`Color`]. 22 | pub const $name: Self = Self { 23 | r: $r, 24 | g: $g, 25 | b: $b, 26 | a: 255, 27 | }; 28 | }; 29 | } 30 | 31 | macro_rules! predefined_rgba { 32 | ($name:ident, $r:expr, $g:expr, $b:expr, $a:expr) => { 33 | /// A predefined RGBA [`Color`]. 34 | pub const $name: Self = Self { 35 | r: $r, 36 | g: $g, 37 | b: $b, 38 | a: $a, 39 | }; 40 | }; 41 | } 42 | 43 | impl Color { 44 | /// Creates a new [`Color`] with RGB components. 45 | pub fn rgb(r: u8, g: u8, b: u8) -> Self { 46 | Self { r, g, b, a: 255 } 47 | } 48 | 49 | /// Creates a new [`Color`] with RGBA components. 50 | pub fn rgba(r: u8, g: u8, b: u8, a: u8) -> Self { 51 | Self { r, g, b, a } 52 | } 53 | 54 | predefined_rgba!(TRANSPARENT, 0, 0, 0, 0); 55 | 56 | predefined_rgb!(ALICE_BLUE, 240, 248, 255); 57 | predefined_rgb!(ANTIQUE_WHITE, 250, 235, 215); 58 | predefined_rgb!(AQUA, 0, 255, 255); 59 | predefined_rgb!(AQUAMARINE, 127, 255, 212); 60 | predefined_rgb!(AZURE, 240, 255, 255); 61 | predefined_rgb!(BEIGE, 245, 245, 220); 62 | predefined_rgb!(BISQUE, 255, 228, 196); 63 | predefined_rgb!(BLACK, 0, 0, 0); 64 | predefined_rgb!(BLANCHED_ALMOND, 255, 235, 205); 65 | predefined_rgb!(BLUE_VIOLET, 138, 43, 226); 66 | predefined_rgb!(BLUE, 0, 0, 255); 67 | predefined_rgb!(BROWN, 165, 42, 42); 68 | predefined_rgb!(BURLYWOOD, 222, 184, 135); 69 | predefined_rgb!(CADET_BLUE, 95, 158, 160); 70 | predefined_rgb!(CHARTREUSE, 127, 255, 0); 71 | predefined_rgb!(CHOCOLATE, 210, 105, 30); 72 | predefined_rgb!(CORAL, 255, 127, 80); 73 | predefined_rgb!(CORNFLOWER_BLUE, 100, 149, 237); 74 | predefined_rgb!(CORNSILK, 255, 248, 220); 75 | predefined_rgb!(CRIMSON, 220, 20, 60); 76 | predefined_rgb!(CYAN, 0, 255, 255); 77 | predefined_rgb!(DARK_BLUE, 0, 0, 139); 78 | predefined_rgb!(DARK_CYAN, 0, 139, 139); 79 | predefined_rgb!(DARK_GOLDENROD, 184, 134, 11); 80 | predefined_rgb!(DARK_GRAY, 169, 169, 169); 81 | predefined_rgb!(DARK_GREEN, 0, 100, 0); 82 | predefined_rgb!(DARK_KHAKI, 189, 183, 107); 83 | predefined_rgb!(DARK_MAGENTA, 139, 0, 139); 84 | predefined_rgb!(DARK_OLIVE_GREEN, 85, 107, 47); 85 | predefined_rgb!(DARK_ORANGE, 255, 140, 0); 86 | predefined_rgb!(DARK_ORCHID, 153, 50, 204); 87 | predefined_rgb!(DARK_RED, 139, 0, 0); 88 | predefined_rgb!(DARK_SALMON, 233, 150, 122); 89 | predefined_rgb!(DARK_SEA_GREEN, 143, 188, 143); 90 | predefined_rgb!(DARK_SLATE_BLUE, 72, 61, 139); 91 | predefined_rgb!(DARK_SLATE_GRAY, 47, 79, 79); 92 | predefined_rgb!(DARK_TURQUOISE, 0, 206, 209); 93 | predefined_rgb!(DARK_VIOLET, 148, 0, 211); 94 | predefined_rgb!(DEEP_PINK, 255, 20, 147); 95 | predefined_rgb!(DEEP_SKY_BLUE, 0, 191, 255); 96 | predefined_rgb!(DIM_GRAY, 105, 105, 105); 97 | predefined_rgb!(DODGER_BLUE, 30, 144, 255); 98 | predefined_rgb!(FIREBRICK, 178, 34, 34); 99 | predefined_rgb!(FLORAL_WHITE, 255, 250, 240); 100 | predefined_rgb!(FOREST_GREEN, 34, 139, 34); 101 | predefined_rgb!(FUCHSIA, 255, 0, 255); 102 | predefined_rgb!(GAINSBORO, 220, 220, 220); 103 | predefined_rgb!(GHOST_WHITE, 248, 248, 255); 104 | predefined_rgb!(GOLD, 255, 215, 0); 105 | predefined_rgb!(GOLDENROD, 218, 165, 32); 106 | predefined_rgb!(GRAY, 128, 128, 128); 107 | predefined_rgb!(GREEN_YELLOW, 173, 255, 47); 108 | predefined_rgb!(GREEN, 0, 128, 0); 109 | predefined_rgb!(HONEYDEW, 240, 255, 240); 110 | predefined_rgb!(HOT_PINK, 255, 105, 180); 111 | predefined_rgb!(INDIAN_RED, 205, 92, 92); 112 | predefined_rgb!(INDIGO, 75, 0, 130); 113 | predefined_rgb!(IVORY, 255, 255, 240); 114 | predefined_rgb!(KHAKI, 240, 230, 140); 115 | predefined_rgb!(LAVENDER_BLUSH, 255, 240, 245); 116 | predefined_rgb!(LAVENDER, 230, 230, 250); 117 | predefined_rgb!(LAWN_GREEN, 124, 252, 0); 118 | predefined_rgb!(LEMON_CHIFFON, 255, 250, 205); 119 | predefined_rgb!(LIGHT_BLUE, 173, 216, 230); 120 | predefined_rgb!(LIGHT_CORAL, 240, 128, 128); 121 | predefined_rgb!(LIGHT_CYAN, 224, 255, 255); 122 | predefined_rgb!(LIGHT_GOLDENROD_YELLOW, 250, 250, 210); 123 | predefined_rgb!(LIGHT_GRAY, 211, 211, 211); 124 | predefined_rgb!(LIGHT_GREEN, 144, 238, 144); 125 | predefined_rgb!(LIGHT_PINK, 255, 182, 193); 126 | predefined_rgb!(LIGHT_SALMON, 255, 160, 122); 127 | predefined_rgb!(LIGHT_SEA_GREEN, 32, 178, 170); 128 | predefined_rgb!(LIGHT_SKY_BLUE, 135, 206, 250); 129 | predefined_rgb!(LIGHT_SLATE_GRAY, 119, 136, 144); 130 | predefined_rgb!(LIGHT_STEEL_BLUE, 176, 196, 222); 131 | predefined_rgb!(LIGHT_YELLOW, 255, 255, 224); 132 | predefined_rgb!(LIME_GREEN, 50, 205, 50); 133 | predefined_rgb!(LIME, 0, 255, 0); 134 | predefined_rgb!(LINEN, 250, 240, 230); 135 | predefined_rgb!(MAGENTA, 255, 0, 255); 136 | predefined_rgb!(MAROON, 128, 0, 0); 137 | predefined_rgb!(MEDIUM_AQUAMARINE, 102, 205, 170); 138 | predefined_rgb!(MEDIUM_BLUE, 0, 0, 205); 139 | predefined_rgb!(MEDIUM_ORCHID, 186, 85, 211); 140 | predefined_rgb!(MEDIUM_PURPLE, 147, 112, 219); 141 | predefined_rgb!(MEDIUM_SEA_GREEN, 60, 179, 113); 142 | predefined_rgb!(MEDIUM_SLATE_BLUE, 123, 104, 238); 143 | predefined_rgb!(MEDIUM_SPRING_GREEN, 0, 250, 154); 144 | predefined_rgb!(MEDIUM_TURQUOISE, 72, 209, 204); 145 | predefined_rgb!(MEDIUM_VIOLET_RED, 199, 21, 133); 146 | predefined_rgb!(MIDNIGHT_BLUE, 25, 25, 112); 147 | predefined_rgb!(MINT_CREAM, 245, 255, 250); 148 | predefined_rgb!(MISTY_ROSE, 255, 228, 225); 149 | predefined_rgb!(MOCCASIN, 255, 228, 181); 150 | predefined_rgb!(NAVAJO_WHITE, 255, 222, 173); 151 | predefined_rgb!(NAVY, 0, 0, 128); 152 | predefined_rgb!(OLD_LACE, 253, 245, 230); 153 | predefined_rgb!(OLIVE_DRAB, 107, 142, 35); 154 | predefined_rgb!(OLIVE, 128, 128, 0); 155 | predefined_rgb!(ORANGE_RED, 255, 69, 0); 156 | predefined_rgb!(ORANGE, 255, 165, 0); 157 | predefined_rgb!(ORCHID, 218, 112, 214); 158 | predefined_rgb!(PALE_GOLDENROD, 238, 232, 170); 159 | predefined_rgb!(PALE_GREEN, 152, 251, 152); 160 | predefined_rgb!(PALE_TURQUOISE, 175, 238, 238); 161 | predefined_rgb!(PALE_VIOLET_RED, 219, 112, 147); 162 | predefined_rgb!(PAPAYA_WHIP, 255, 239, 213); 163 | predefined_rgb!(PEACH_PUFF, 255, 218, 185); 164 | predefined_rgb!(PERU, 205, 133, 63); 165 | predefined_rgb!(PINK, 255, 192, 203); 166 | predefined_rgb!(PLUM, 221, 160, 221); 167 | predefined_rgb!(POWDER_BLUE, 176, 224, 230); 168 | predefined_rgb!(PURPLE, 128, 0, 128); 169 | predefined_rgb!(RED, 255, 0, 0); 170 | predefined_rgb!(ROSY_BROWN, 188, 143, 143); 171 | predefined_rgb!(ROYAL_BLUE, 65, 105, 225); 172 | predefined_rgb!(SADDLE_BROWN, 139, 69, 19); 173 | predefined_rgb!(SALMON, 250, 128, 114); 174 | predefined_rgb!(SANDY_BROWN, 244, 164, 96); 175 | predefined_rgb!(SEA_GREEN, 46, 139, 87); 176 | predefined_rgb!(SEASHELL, 255, 245, 238); 177 | predefined_rgb!(SIENNA, 160, 82, 45); 178 | predefined_rgb!(SILVER, 192, 192, 192); 179 | predefined_rgb!(SKY_BLUE, 135, 206, 235); 180 | predefined_rgb!(SLATE_BLUE, 106, 90, 205); 181 | predefined_rgb!(SLATE_GRAY, 112, 128, 144); 182 | predefined_rgb!(SNOW, 255, 250, 250); 183 | predefined_rgb!(SPRING_GREEN, 0, 255, 127); 184 | predefined_rgb!(STEEL_BLUE, 70, 130, 180); 185 | predefined_rgb!(TAN, 210, 180, 140); 186 | predefined_rgb!(TEAL, 0, 128, 128); 187 | predefined_rgb!(THISTLE, 216, 191, 216); 188 | predefined_rgb!(TOMATO, 255, 99, 71); 189 | predefined_rgb!(TURQUOISE, 64, 224, 208); 190 | predefined_rgb!(VIOLET, 238, 130, 238); 191 | predefined_rgb!(WHEAT, 245, 222, 179); 192 | predefined_rgb!(WHITE_SMOKE, 245, 245, 245); 193 | predefined_rgb!(WHITE, 255, 255, 255); 194 | predefined_rgb!(YELLOW_GREEN, 154, 205, 50); 195 | predefined_rgb!(YELLOW, 255, 255, 0); 196 | } 197 | 198 | impl From for Color { 199 | fn from(color: SdlColor) -> Self { 200 | Self { 201 | r: color.r, 202 | g: color.g, 203 | b: color.b, 204 | a: color.a, 205 | } 206 | } 207 | } 208 | 209 | impl From for SdlColor { 210 | fn from(color: Color) -> Self { 211 | Self { 212 | r: color.r, 213 | g: color.g, 214 | b: color.b, 215 | a: color.a, 216 | } 217 | } 218 | } 219 | --------------------------------------------------------------------------------