diff options
Diffstat (limited to 'src/render.rs')
-rw-r--r-- | src/render.rs | 325 |
1 files changed, 325 insertions, 0 deletions
diff --git a/src/render.rs b/src/render.rs new file mode 100644 index 0000000..77a847f --- /dev/null +++ b/src/render.rs @@ -0,0 +1,325 @@ +use super::api::{Api, ApiError, Skill, Specialization, Trait}; +use super::bt::{BuildTemplate, TraitChoice, Traitline}; +use image::{imageops, DynamicImage, GenericImage, GenericImageView, ImageBuffer, Rgba, RgbaImage}; +use imageproc::drawing::{self, Point}; +use rusttype::{Font, Scale, SharedBytes}; + +quick_error! { + #[derive(Debug)] + pub enum RenderError { + ApiError(err: ApiError) { + cause(err) + from() + } + ImageError(err: image::ImageError) { + cause(err) + from() + } + } +} + +#[derive(Debug, Clone)] +pub struct RenderOptions { + skill_size: u32, + traitline_height: u32, + traitline_width: u32, + traitline_brightness: i32, + traitline_x_offset: u32, + trait_size: u32, + line_color: Rgba<u8>, + line_height: u32, + font: Font<'static>, + text_color: Rgba<u8>, + text_size: u32, + background_color: Rgba<u8>, + render_specialization_names: bool, + skill_offset: u32, +} + +impl Default for RenderOptions { + fn default() -> Self { + RenderOptions { + skill_size: 100, + traitline_height: 160, + traitline_width: 550, + traitline_brightness: 100, + traitline_x_offset: 0, + trait_size: 30, + line_color: Rgba([0, 0, 0, 255]), + line_height: 4, + font: Font::from_bytes(SharedBytes::ByRef(include_bytes!("LiberationMono.ttf"))) + .expect("Invalid font data for default font"), + text_color: Rgba([0, 0, 0, 255]), + text_size: 20, + background_color: Rgba([0, 0, 0, 0]), + render_specialization_names: true, + skill_offset: 30, + } + } +} + +const BG_CROP_HEIGHT: u32 = 160; +const BG_CROP_WIDTH: u32 = 550; + +pub struct Renderer<'r> { + api: &'r mut Api, + options: RenderOptions, + minor_mask: DynamicImage, +} + +impl<'r> Renderer<'r> { + /// Create a new renderer using the given API. + pub fn new(api: &mut Api, options: RenderOptions) -> Renderer<'_> { + let minor_mask = image::load_from_memory(include_bytes!("minor_trait_mask.png")) + .expect("Mask image could not be loaded") + .resize( + options.trait_size, + options.trait_size, + imageops::FilterType::CatmullRom, + ); + Renderer { + api, + options, + minor_mask, + } + } + + pub fn render_skills(&mut self, skills: &[Option<Skill>]) -> Result<RgbaImage, RenderError> { + let mut buffer = ImageBuffer::new( + skills.len() as u32 * self.options.skill_size, + self.options.skill_size, + ); + for (i, skill) in skills.iter().enumerate() { + if let Some(skill) = skill { + let img = self.api.get_image(&skill.icon)?.resize( + self.options.skill_size, + self.options.skill_size, + imageops::FilterType::CatmullRom, + ); + buffer.copy_from(&img, i as u32 * self.options.skill_size, 0); + } + } + Ok(buffer) + } + + fn render_minor_trait( + &mut self, + buffer: &mut RgbaImage, + minor: &Trait, + ) -> Result<(), RenderError> { + let minor_img = self.api.get_image(&minor.icon)?.resize( + self.options.trait_size, + self.options.trait_size, + imageops::FilterType::CatmullRom, + ); + let minor_img = imageproc::map::map_pixels(&minor_img, |x, y, p| { + let alpha = self.minor_mask.get_pixel(x, y)[3]; + Rgba([p[0], p[1], p[2], alpha]) + }); + let y_pos = (buffer.height() - minor_img.height()) / 2; + let x_slice = (buffer.width() - self.options.traitline_x_offset) / 6; + let x_pos = 2 * (minor.tier - 1) * x_slice + + (x_slice - minor_img.width()) / 2 + + self.options.traitline_x_offset; + imageops::overlay(buffer, &minor_img, x_pos, y_pos); + Ok(()) + } + + fn render_major_trait( + &mut self, + buffer: &mut RgbaImage, + major: &Trait, + vertical_pos: u8, + chosen: bool, + ) -> Result<(), RenderError> { + let major_img = self.api.get_image(&major.icon)?.resize( + self.options.trait_size, + self.options.trait_size, + imageops::FilterType::CatmullRom, + ); + let major_img = if !chosen { + major_img.grayscale() + } else { + major_img + }; + let y_slice = buffer.height() / 3; + let y_pos = vertical_pos as u32 * y_slice + (y_slice - major_img.height()) / 2; + let x_slice = (buffer.width() - self.options.traitline_x_offset) / 6; + let x_pos = 2 * (major.tier - 1) * x_slice + + x_slice + + (x_slice - major_img.width()) / 2 + + self.options.traitline_x_offset; + imageops::overlay(buffer, &major_img, x_pos, y_pos); + Ok(()) + } + + fn render_line( + &mut self, + buffer: &mut RgbaImage, + tier: u8, + choice: TraitChoice, + ) -> Result<(), RenderError> { + if choice == TraitChoice::None { + return Ok(()); + } + + let x_slice = (buffer.width() - self.options.traitline_x_offset) / 6; + let start_x = 2 * tier as u32 * x_slice + + (x_slice + self.options.trait_size) / 2 + + self.options.traitline_x_offset; + let end_x = start_x + x_slice - self.options.trait_size; + let start_y = (buffer.height() - self.options.line_height) / 2; + let y_slice = buffer.height() / 3; + let end_y = y_slice * (choice as u32 - 1) + (y_slice - self.options.line_height) / 2; + + drawing::draw_convex_polygon_mut( + buffer, + &[ + Point::new(start_x as i32, start_y as i32), + Point::new( + start_x as i32, + start_y as i32 + self.options.line_height as i32, + ), + Point::new(end_x as i32, end_y as i32 + self.options.line_height as i32), + Point::new(end_x as i32, end_y as i32), + ], + self.options.line_color, + ); + if tier == 2 { + return Ok(()); + } + + drawing::draw_convex_polygon_mut( + buffer, + &[ + Point::new( + 2 * x_slice as i32 + start_x as i32 - self.options.trait_size as i32, + start_y as i32, + ), + Point::new( + 2 * x_slice as i32 + start_x as i32 - self.options.trait_size as i32, + start_y as i32 + self.options.line_height as i32, + ), + Point::new( + self.options.trait_size as i32 + end_x as i32, + end_y as i32 + self.options.line_height as i32, + ), + Point::new(self.options.trait_size as i32 + end_x as i32, end_y as i32), + ], + self.options.line_color, + ); + + Ok(()) + } + + pub fn render_traitline(&mut self, traitline: &Traitline) -> Result<RgbaImage, RenderError> { + let (spec, choices) = traitline; + let mut background = self.api.get_image(&spec.background)?; + let mut buffer = background + .crop( + 0, + background.height() - BG_CROP_HEIGHT, + BG_CROP_WIDTH, + BG_CROP_HEIGHT, + ) + .brighten(self.options.traitline_brightness) + .resize( + self.options.traitline_width, + self.options.traitline_height, + imageops::FilterType::CatmullRom, + ) + .to_rgba(); + + let minor_traits = self.api.get_traits(&spec.minor_traits)?; + for minor in &minor_traits { + self.render_minor_trait(&mut buffer, &minor)?; + } + + let major_traits = self.api.get_traits(&spec.major_traits)?; + for (i, major) in major_traits.iter().enumerate() { + let choice = choices[major.tier as usize - 1]; + let vert_pos = (i % 3) as u8; + let chosen = choice as u8 == vert_pos + 1; + self.render_major_trait(&mut buffer, &major, vert_pos, chosen)?; + } + + for (tier, choice) in choices.iter().enumerate() { + self.render_line(&mut buffer, tier as u8, *choice)?; + } + Ok(buffer) + } + + pub fn render_specialization_name( + &mut self, + specialization: &Specialization, + ) -> Result<RgbaImage, RenderError> { + let scale = self.options.text_size; + let mut buffer = ImageBuffer::new(self.options.traitline_width, scale); + drawing::draw_text_mut( + &mut buffer, + self.options.text_color, + 0, + 0, + Scale { + x: scale as f32, + y: scale as f32, + }, + &self.options.font, + &specialization.name, + ); + Ok(buffer) + } + + pub fn render_buildtemplate( + &mut self, + build: &BuildTemplate, + ) -> Result<RgbaImage, RenderError> { + let images = self.construct_parts(build)?; + self.merge_parts(&images) + } + + fn construct_parts(&mut self, build: &BuildTemplate) -> Result<Vec<RgbaImage>, RenderError> { + let mut images: Vec<RgbaImage> = Vec::new(); + + for traitline in build.traitlines().iter().filter(|x| x.is_some()) { + let traitline = traitline.as_ref().unwrap(); + if self.options.render_specialization_names { + let header = self.render_specialization_name(&traitline.0)?; + images.push(header); + } + let inner = self.render_traitline(&traitline)?; + images.push(inner); + } + + let needs_space = build.skill_count() > 0 && build.traitline_count() > 0; + if needs_space { + let separator = ImageBuffer::from_pixel( + self.options.traitline_width, + self.options.skill_offset, + self.options.background_color, + ); + images.push(separator); + } + + if build.skill_count() > 0 { + let skills = self.render_skills(build.skills())?; + images.push(skills); + } + + Ok(images) + } + + fn merge_parts(&mut self, images: &[RgbaImage]) -> Result<RgbaImage, RenderError> { + let width = images.iter().map(RgbaImage::width).max().unwrap(); + let height = images.iter().map(RgbaImage::height).sum(); + let mut buffer = ImageBuffer::from_pixel(width, height, self.options.background_color); + let mut pos_y = 0; + + for image in images { + imageops::overlay(&mut buffer, image, 0, pos_y); + pos_y += image.height(); + } + + Ok(buffer) + } +} |