Various improvements to the contributors example (#12217)
				
					
				
			# Objective / Solution - Use `Hsla` for the color constants (Fixes #12203) And a few other improvements: - Make contributor colors persistent between runs - Move text to top left where the birbs can't reach and add padding - Remove `Contributor:` text. It's obvious what is being shown from context, and then we can remove `has_triggered`. - Show the number of commits authored - Some system names were postfixed with `_system` and some weren't. Removed `_system` for consistency. - Clean up collision code slightly with a bounding volume - Someone accidentally typed "bird" instead of "birb" in one comment. - Other misc. cleanup ## Before <img width="1280" alt="image" src="https://github.com/bevyengine/bevy/assets/200550/9c6229d6-313a-464d-8a97-0220aa16901f"> ## After <img width="1280" alt="image" src="https://github.com/bevyengine/bevy/assets/200550/0c00e95b-2f50-4f50-b177-def4ca405313">
This commit is contained in:
		
							parent
							
								
									4aca55d76a
								
							
						
					
					
						commit
						bcdca068ad
					
				| @ -1,12 +1,14 @@ | |||||||
| //! This example displays each contributor to the bevy source code as a bouncing bevy-ball.
 | //! This example displays each contributor to the bevy source code as a bouncing bevy-ball.
 | ||||||
| 
 | 
 | ||||||
| use bevy::{ | use bevy::{ | ||||||
|  |     math::bounding::Aabb2d, | ||||||
|     prelude::*, |     prelude::*, | ||||||
|     utils::{thiserror, HashSet}, |     utils::{thiserror, HashMap}, | ||||||
| }; | }; | ||||||
| use rand::{prelude::SliceRandom, Rng}; | use rand::{prelude::SliceRandom, Rng}; | ||||||
| use std::{ | use std::{ | ||||||
|     env::VarError, |     env::VarError, | ||||||
|  |     hash::{DefaultHasher, Hash, Hasher}, | ||||||
|     io::{self, BufRead, BufReader}, |     io::{self, BufRead, BufReader}, | ||||||
|     process::Stdio, |     process::Stdio, | ||||||
| }; | }; | ||||||
| @ -14,22 +16,14 @@ use std::{ | |||||||
| fn main() { | fn main() { | ||||||
|     App::new() |     App::new() | ||||||
|         .add_plugins(DefaultPlugins) |         .add_plugins(DefaultPlugins) | ||||||
|         .init_resource::<SelectionState>() |         .init_resource::<SelectionTimer>() | ||||||
|         .add_systems(Startup, (setup_contributor_selection, setup)) |         .add_systems(Startup, (setup_contributor_selection, setup)) | ||||||
|         .add_systems( |         .add_systems(Update, (gravity, movement, collisions, selection)) | ||||||
|             Update, |  | ||||||
|             ( |  | ||||||
|                 velocity_system, |  | ||||||
|                 move_system, |  | ||||||
|                 collision_system, |  | ||||||
|                 select_system, |  | ||||||
|             ), |  | ||||||
|         ) |  | ||||||
|         .run(); |         .run(); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // Store contributors in a collection that preserves the uniqueness
 | // Store contributors with their commit count in a collection that preserves the uniqueness
 | ||||||
| type Contributors = HashSet<String>; | type Contributors = HashMap<String, usize>; | ||||||
| 
 | 
 | ||||||
| #[derive(Resource)] | #[derive(Resource)] | ||||||
| struct ContributorSelection { | struct ContributorSelection { | ||||||
| @ -38,17 +32,14 @@ struct ContributorSelection { | |||||||
| } | } | ||||||
| 
 | 
 | ||||||
| #[derive(Resource)] | #[derive(Resource)] | ||||||
| struct SelectionState { | struct SelectionTimer(Timer); | ||||||
|     timer: Timer, |  | ||||||
|     has_triggered: bool, |  | ||||||
| } |  | ||||||
| 
 | 
 | ||||||
| impl Default for SelectionState { | impl Default for SelectionTimer { | ||||||
|     fn default() -> Self { |     fn default() -> Self { | ||||||
|         Self { |         Self(Timer::from_seconds( | ||||||
|             timer: Timer::from_seconds(SHOWCASE_TIMER_SECS, TimerMode::Repeating), |             SHOWCASE_TIMER_SECS, | ||||||
|             has_triggered: false, |             TimerMode::Repeating, | ||||||
|         } |         )) | ||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| @ -58,6 +49,7 @@ struct ContributorDisplay; | |||||||
| #[derive(Component)] | #[derive(Component)] | ||||||
| struct Contributor { | struct Contributor { | ||||||
|     name: String, |     name: String, | ||||||
|  |     num_commits: usize, | ||||||
|     hue: f32, |     hue: f32, | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| @ -70,11 +62,8 @@ struct Velocity { | |||||||
| const GRAVITY: f32 = 9.821 * 100.0; | const GRAVITY: f32 = 9.821 * 100.0; | ||||||
| const SPRITE_SIZE: f32 = 75.0; | const SPRITE_SIZE: f32 = 75.0; | ||||||
| 
 | 
 | ||||||
| const SATURATION_DESELECTED: f32 = 0.3; | const SELECTED: Hsla = Hsla::hsl(0.0, 0.9, 0.7); | ||||||
| const LIGHTNESS_DESELECTED: f32 = 0.2; | const DESELECTED: Hsla = Hsla::new(0.0, 0.3, 0.2, 0.92); | ||||||
| const SATURATION_SELECTED: f32 = 0.9; |  | ||||||
| const LIGHTNESS_SELECTED: f32 = 0.7; |  | ||||||
| const ALPHA: f32 = 0.92; |  | ||||||
| 
 | 
 | ||||||
| const SHOWCASE_TIMER_SECS: f32 = 3.0; | const SHOWCASE_TIMER_SECS: f32 = 3.0; | ||||||
| 
 | 
 | ||||||
| @ -82,11 +71,12 @@ const CONTRIBUTORS_LIST: &[&str] = &["Carter Anderson", "And Many More"]; | |||||||
| 
 | 
 | ||||||
| fn setup_contributor_selection(mut commands: Commands, asset_server: Res<AssetServer>) { | fn setup_contributor_selection(mut commands: Commands, asset_server: Res<AssetServer>) { | ||||||
|     // Load contributors from the git history log or use default values from
 |     // Load contributors from the git history log or use default values from
 | ||||||
|     // the constant array. Contributors must be unique, so they are stored in a HashSet
 |     // the constant array. Contributors are stored in a HashMap with their
 | ||||||
|  |     // commit count.
 | ||||||
|     let contribs = contributors().unwrap_or_else(|_| { |     let contribs = contributors().unwrap_or_else(|_| { | ||||||
|         CONTRIBUTORS_LIST |         CONTRIBUTORS_LIST | ||||||
|             .iter() |             .iter() | ||||||
|             .map(|name| name.to_string()) |             .map(|name| (name.to_string(), 1)) | ||||||
|             .collect() |             .collect() | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
| @ -99,28 +89,31 @@ fn setup_contributor_selection(mut commands: Commands, asset_server: Res<AssetSe | |||||||
| 
 | 
 | ||||||
|     let mut rng = rand::thread_rng(); |     let mut rng = rand::thread_rng(); | ||||||
| 
 | 
 | ||||||
|     for name in contribs { |     for (name, num_commits) in contribs { | ||||||
|         let pos = (rng.gen_range(-400.0..400.0), rng.gen_range(0.0..400.0)); |         let transform = | ||||||
|  |             Transform::from_xyz(rng.gen_range(-400.0..400.0), rng.gen_range(0.0..400.0), 0.0); | ||||||
|         let dir = rng.gen_range(-1.0..1.0); |         let dir = rng.gen_range(-1.0..1.0); | ||||||
|         let velocity = Vec3::new(dir * 500.0, 0.0, 0.0); |         let velocity = Vec3::new(dir * 500.0, 0.0, 0.0); | ||||||
|         let hue = rng.gen_range(0.0..=360.0); |         let hue = name_to_hue(&name); | ||||||
| 
 | 
 | ||||||
|         // some sprites should be flipped
 |         // Some sprites should be flipped for variety
 | ||||||
|         let flipped = rng.gen_bool(0.5); |         let flipped = rng.gen(); | ||||||
| 
 |  | ||||||
|         let transform = Transform::from_xyz(pos.0, pos.1, 0.0); |  | ||||||
| 
 | 
 | ||||||
|         let entity = commands |         let entity = commands | ||||||
|             .spawn(( |             .spawn(( | ||||||
|                 Contributor { name, hue }, |                 Contributor { | ||||||
|  |                     name, | ||||||
|  |                     num_commits, | ||||||
|  |                     hue, | ||||||
|  |                 }, | ||||||
|                 Velocity { |                 Velocity { | ||||||
|                     translation: velocity, |                     translation: velocity, | ||||||
|                     rotation: -dir * 5.0, |                     rotation: -dir * 5.0, | ||||||
|                 }, |                 }, | ||||||
|                 SpriteBundle { |                 SpriteBundle { | ||||||
|                     sprite: Sprite { |                     sprite: Sprite { | ||||||
|                         custom_size: Some(Vec2::new(1.0, 1.0) * SPRITE_SIZE), |                         custom_size: Some(Vec2::splat(SPRITE_SIZE)), | ||||||
|                         color: Color::hsla(hue, SATURATION_DESELECTED, LIGHTNESS_DESELECTED, ALPHA), |                         color: DESELECTED.with_hue(hue).into(), | ||||||
|                         flip_x: flipped, |                         flip_x: flipped, | ||||||
|                         ..default() |                         ..default() | ||||||
|                     }, |                     }, | ||||||
| @ -142,24 +135,24 @@ fn setup_contributor_selection(mut commands: Commands, asset_server: Res<AssetSe | |||||||
| fn setup(mut commands: Commands, asset_server: Res<AssetServer>) { | fn setup(mut commands: Commands, asset_server: Res<AssetServer>) { | ||||||
|     commands.spawn(Camera2dBundle::default()); |     commands.spawn(Camera2dBundle::default()); | ||||||
| 
 | 
 | ||||||
|  |     let text_style = TextStyle { | ||||||
|  |         font: asset_server.load("fonts/FiraSans-Bold.ttf"), | ||||||
|  |         font_size: 60.0, | ||||||
|  |         ..default() | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|     commands.spawn(( |     commands.spawn(( | ||||||
|         TextBundle::from_sections([ |         TextBundle::from_sections([ | ||||||
|             TextSection::new( |             TextSection::new("Contributor showcase", text_style.clone()), | ||||||
|                 "Contributor showcase", |  | ||||||
|                 TextStyle { |  | ||||||
|                     font: asset_server.load("fonts/FiraSans-Bold.ttf"), |  | ||||||
|                     font_size: 60.0, |  | ||||||
|                     ..default() |  | ||||||
|                 }, |  | ||||||
|             ), |  | ||||||
|             TextSection::from_style(TextStyle { |             TextSection::from_style(TextStyle { | ||||||
|                 font: asset_server.load("fonts/FiraSans-Bold.ttf"), |                 font_size: 30., | ||||||
|                 font_size: 60.0, |                 ..text_style | ||||||
|                 ..default() |  | ||||||
|             }), |             }), | ||||||
|         ]) |         ]) | ||||||
|         .with_style(Style { |         .with_style(Style { | ||||||
|             align_self: AlignSelf::FlexEnd, |             position_type: PositionType::Absolute, | ||||||
|  |             top: Val::Px(12.), | ||||||
|  |             left: Val::Px(12.), | ||||||
|             ..default() |             ..default() | ||||||
|         }), |         }), | ||||||
|         ContributorDisplay, |         ContributorDisplay, | ||||||
| @ -167,28 +160,26 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) { | |||||||
| } | } | ||||||
| 
 | 
 | ||||||
| /// Finds the next contributor to display and selects the entity
 | /// Finds the next contributor to display and selects the entity
 | ||||||
| fn select_system( | fn selection( | ||||||
|     mut timer: ResMut<SelectionState>, |     mut timer: ResMut<SelectionTimer>, | ||||||
|     mut contributor_selection: ResMut<ContributorSelection>, |     mut contributor_selection: ResMut<ContributorSelection>, | ||||||
|     mut text_query: Query<&mut Text, With<ContributorDisplay>>, |     mut text_query: Query<&mut Text, With<ContributorDisplay>>, | ||||||
|     mut query: Query<(&Contributor, &mut Sprite, &mut Transform)>, |     mut query: Query<(&Contributor, &mut Sprite, &mut Transform)>, | ||||||
|     time: Res<Time>, |     time: Res<Time>, | ||||||
| ) { | ) { | ||||||
|     if !timer.timer.tick(time.delta()).just_finished() { |     if !timer.0.tick(time.delta()).just_finished() { | ||||||
|         return; |         return; | ||||||
|     } |     } | ||||||
|     if !timer.has_triggered { |  | ||||||
|         let mut text = text_query.single_mut(); |  | ||||||
|         text.sections[0].value = "Contributor: ".to_string(); |  | ||||||
| 
 | 
 | ||||||
|         timer.has_triggered = true; |     // Deselect the previous contributor
 | ||||||
|     } |  | ||||||
| 
 | 
 | ||||||
|     let entity = contributor_selection.order[contributor_selection.idx]; |     let entity = contributor_selection.order[contributor_selection.idx]; | ||||||
|     if let Ok((contributor, mut sprite, mut transform)) = query.get_mut(entity) { |     if let Ok((contributor, mut sprite, mut transform)) = query.get_mut(entity) { | ||||||
|         deselect(&mut sprite, contributor, &mut transform); |         deselect(&mut sprite, contributor, &mut transform); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     // Select the next contributor
 | ||||||
|  | 
 | ||||||
|     if (contributor_selection.idx + 1) < contributor_selection.order.len() { |     if (contributor_selection.idx + 1) < contributor_selection.order.len() { | ||||||
|         contributor_selection.idx += 1; |         contributor_selection.idx += 1; | ||||||
|     } else { |     } else { | ||||||
| @ -211,34 +202,29 @@ fn select( | |||||||
|     transform: &mut Transform, |     transform: &mut Transform, | ||||||
|     text: &mut Text, |     text: &mut Text, | ||||||
| ) { | ) { | ||||||
|     sprite.color = Color::hsla( |     sprite.color = SELECTED.with_hue(contributor.hue).into(); | ||||||
|         contributor.hue, |  | ||||||
|         SATURATION_SELECTED, |  | ||||||
|         LIGHTNESS_SELECTED, |  | ||||||
|         ALPHA, |  | ||||||
|     ); |  | ||||||
| 
 | 
 | ||||||
|     transform.translation.z = 100.0; |     transform.translation.z = 100.0; | ||||||
| 
 | 
 | ||||||
|     text.sections[1].value.clone_from(&contributor.name); |     text.sections[0].value.clone_from(&contributor.name); | ||||||
|     text.sections[1].style.color = sprite.color; |     text.sections[1].value = format!( | ||||||
|  |         "\n{} commit{}", | ||||||
|  |         contributor.num_commits, | ||||||
|  |         if contributor.num_commits > 1 { "s" } else { "" } | ||||||
|  |     ); | ||||||
|  |     text.sections[0].style.color = sprite.color; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| /// Change the modulate color to the "deselected" color and push
 | /// Change the tint color to the "deselected" color and push
 | ||||||
| /// the object to the back.
 | /// the object to the back.
 | ||||||
| fn deselect(sprite: &mut Sprite, contributor: &Contributor, transform: &mut Transform) { | fn deselect(sprite: &mut Sprite, contributor: &Contributor, transform: &mut Transform) { | ||||||
|     sprite.color = Color::hsla( |     sprite.color = DESELECTED.with_hue(contributor.hue).into(); | ||||||
|         contributor.hue, |  | ||||||
|         SATURATION_DESELECTED, |  | ||||||
|         LIGHTNESS_DESELECTED, |  | ||||||
|         ALPHA, |  | ||||||
|     ); |  | ||||||
| 
 | 
 | ||||||
|     transform.translation.z = 0.0; |     transform.translation.z = 0.0; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| /// Applies gravity to all entities with velocity
 | /// Applies gravity to all entities with a velocity.
 | ||||||
| fn velocity_system(time: Res<Time>, mut velocity_query: Query<&mut Velocity>) { | fn gravity(time: Res<Time>, mut velocity_query: Query<&mut Velocity>) { | ||||||
|     let delta = time.delta_seconds(); |     let delta = time.delta_seconds(); | ||||||
| 
 | 
 | ||||||
|     for mut velocity in &mut velocity_query { |     for mut velocity in &mut velocity_query { | ||||||
| @ -246,56 +232,53 @@ fn velocity_system(time: Res<Time>, mut velocity_query: Query<&mut Velocity>) { | |||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| /// Checks for collisions of contributor-birds.
 | /// Checks for collisions of contributor-birbs.
 | ||||||
| ///
 | ///
 | ||||||
| /// On collision with left-or-right wall it resets the horizontal
 | /// On collision with left-or-right wall it resets the horizontal
 | ||||||
| /// velocity. On collision with the ground it applies an upwards
 | /// velocity. On collision with the ground it applies an upwards
 | ||||||
| /// force.
 | /// force.
 | ||||||
| fn collision_system( | fn collisions( | ||||||
|     windows: Query<&Window>, |     windows: Query<&Window>, | ||||||
|     mut query: Query<(&mut Velocity, &mut Transform), With<Contributor>>, |     mut query: Query<(&mut Velocity, &mut Transform), With<Contributor>>, | ||||||
| ) { | ) { | ||||||
|     let window = windows.single(); |     let window = windows.single(); | ||||||
|  |     let window_size = Vec2::new(window.width(), window.height()); | ||||||
| 
 | 
 | ||||||
|     let ceiling = window.height() / 2.; |     let collision_area = Aabb2d::new(Vec2::ZERO, (window_size - SPRITE_SIZE) / 2.); | ||||||
|     let ground = -window.height() / 2.; |  | ||||||
| 
 |  | ||||||
|     let wall_left = -window.width() / 2.; |  | ||||||
|     let wall_right = window.width() / 2.; |  | ||||||
| 
 | 
 | ||||||
|     // The maximum height the birbs should try to reach is one birb below the top of the window.
 |     // The maximum height the birbs should try to reach is one birb below the top of the window.
 | ||||||
|     let max_bounce_height = (window.height() - SPRITE_SIZE * 2.0).max(0.0); |     let max_bounce_height = (window_size.y - SPRITE_SIZE * 2.0).max(0.0); | ||||||
|  |     let min_bounce_height = max_bounce_height * 0.4; | ||||||
| 
 | 
 | ||||||
|     let mut rng = rand::thread_rng(); |     let mut rng = rand::thread_rng(); | ||||||
| 
 | 
 | ||||||
|     for (mut velocity, mut transform) in &mut query { |     for (mut velocity, mut transform) in &mut query { | ||||||
|         let left = transform.translation.x - SPRITE_SIZE / 2.0; |         // Clamp the translation to not go out of the bounds
 | ||||||
|         let right = transform.translation.x + SPRITE_SIZE / 2.0; |         if transform.translation.y < collision_area.min.y { | ||||||
|         let top = transform.translation.y + SPRITE_SIZE / 2.0; |             transform.translation.y = collision_area.min.y; | ||||||
|         let bottom = transform.translation.y - SPRITE_SIZE / 2.0; |  | ||||||
| 
 |  | ||||||
|         // clamp the translation to not go out of the bounds
 |  | ||||||
|         if bottom < ground { |  | ||||||
|             transform.translation.y = ground + SPRITE_SIZE / 2.0; |  | ||||||
| 
 | 
 | ||||||
|             // How high this birb will bounce.
 |             // How high this birb will bounce.
 | ||||||
|             let bounce_height = rng.gen_range((max_bounce_height * 0.4)..=max_bounce_height); |             let bounce_height = rng.gen_range(min_bounce_height..=max_bounce_height); | ||||||
| 
 | 
 | ||||||
|             // Apply the velocity that would bounce the birb up to bounce_height.
 |             // Apply the velocity that would bounce the birb up to bounce_height.
 | ||||||
|             velocity.translation.y = (bounce_height * GRAVITY * 2.).sqrt(); |             velocity.translation.y = (bounce_height * GRAVITY * 2.).sqrt(); | ||||||
|         } |         } | ||||||
|         if top > ceiling { | 
 | ||||||
|             transform.translation.y = ceiling - SPRITE_SIZE / 2.0; |         // Birbs might hit the ceiling if the window is resized.
 | ||||||
|  |         // If they do, bounce them.
 | ||||||
|  |         if transform.translation.y > collision_area.max.y { | ||||||
|  |             transform.translation.y = collision_area.max.y; | ||||||
|             velocity.translation.y *= -1.0; |             velocity.translation.y *= -1.0; | ||||||
|         } |         } | ||||||
|         // on side walls flip the horizontal velocity
 | 
 | ||||||
|         if left < wall_left { |         // On side walls flip the horizontal velocity
 | ||||||
|             transform.translation.x = wall_left + SPRITE_SIZE / 2.0; |         if transform.translation.x < collision_area.min.x { | ||||||
|  |             transform.translation.x = collision_area.min.x; | ||||||
|             velocity.translation.x *= -1.0; |             velocity.translation.x *= -1.0; | ||||||
|             velocity.rotation *= -1.0; |             velocity.rotation *= -1.0; | ||||||
|         } |         } | ||||||
|         if right > wall_right { |         if transform.translation.x > collision_area.max.x { | ||||||
|             transform.translation.x = wall_right - SPRITE_SIZE / 2.0; |             transform.translation.x = collision_area.max.x; | ||||||
|             velocity.translation.x *= -1.0; |             velocity.translation.x *= -1.0; | ||||||
|             velocity.rotation *= -1.0; |             velocity.rotation *= -1.0; | ||||||
|         } |         } | ||||||
| @ -303,7 +286,7 @@ fn collision_system( | |||||||
| } | } | ||||||
| 
 | 
 | ||||||
| /// Apply velocity to positions and rotations.
 | /// Apply velocity to positions and rotations.
 | ||||||
| fn move_system(time: Res<Time>, mut query: Query<(&Velocity, &mut Transform)>) { | fn movement(time: Res<Time>, mut query: Query<(&Velocity, &mut Transform)>) { | ||||||
|     let delta = time.delta_seconds(); |     let delta = time.delta_seconds(); | ||||||
| 
 | 
 | ||||||
|     for (velocity, mut transform) in &mut query { |     for (velocity, mut transform) in &mut query { | ||||||
| @ -322,9 +305,8 @@ enum LoadContributorsError { | |||||||
|     Stdout, |     Stdout, | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| /// Get the names of all contributors from the git log.
 | /// Get the names and commit counts of all contributors from the git log.
 | ||||||
| ///
 | ///
 | ||||||
| /// The names are deduplicated.
 |  | ||||||
| /// This function only works if `git` is installed and
 | /// This function only works if `git` is installed and
 | ||||||
| /// the program is run through `cargo`.
 | /// the program is run through `cargo`.
 | ||||||
| fn contributors() -> Result<Contributors, LoadContributorsError> { | fn contributors() -> Result<Contributors, LoadContributorsError> { | ||||||
| @ -338,10 +320,22 @@ fn contributors() -> Result<Contributors, LoadContributorsError> { | |||||||
| 
 | 
 | ||||||
|     let stdout = cmd.stdout.take().ok_or(LoadContributorsError::Stdout)?; |     let stdout = cmd.stdout.take().ok_or(LoadContributorsError::Stdout)?; | ||||||
| 
 | 
 | ||||||
|     let contributors = BufReader::new(stdout) |     // Take the list of commit author names and collect them into a HashMap,
 | ||||||
|         .lines() |     // keeping a count of how many commits they authored.
 | ||||||
|         .map_while(|x| x.ok()) |     let contributors = BufReader::new(stdout).lines().map_while(Result::ok).fold( | ||||||
|         .collect(); |         HashMap::new(), | ||||||
|  |         |mut acc, word| { | ||||||
|  |             *acc.entry(word).or_insert(0) += 1; | ||||||
|  |             acc | ||||||
|  |         }, | ||||||
|  |     ); | ||||||
| 
 | 
 | ||||||
|     Ok(contributors) |     Ok(contributors) | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | /// Give each unique contributor name a particular hue that is stable between runs.
 | ||||||
|  | fn name_to_hue(s: &str) -> f32 { | ||||||
|  |     let mut hasher = DefaultHasher::new(); | ||||||
|  |     s.hash(&mut hasher); | ||||||
|  |     hasher.finish() as f32 / u64::MAX as f32 * 360. | ||||||
|  | } | ||||||
|  | |||||||
		Loading…
	
		Reference in New Issue
	
	Block a user
	 Rob Parrett
						Rob Parrett