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, line_height: u32, font: Font<'static>, text_color: Rgba, text_size: u32, background_color: Rgba, 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]) -> Result { 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 = with_mask(&minor_img, &self.minor_mask); 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 { with_mask(&major_img.grayscale(), &major_img) } else { major_img.to_rgba() }; 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 { 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 { 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 { let images = self.construct_parts(build)?; self.merge_parts(&images) } fn construct_parts(&mut self, build: &BuildTemplate) -> Result, RenderError> { let mut images: Vec = 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 { 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) } } fn with_mask(input: &I, mask: &J) -> RgbaImage where I: GenericImage>, J: GenericImage>, { imageproc::map::map_pixels(input, |x, y, p| { let alpha = mask.get_pixel(x, y)[3]; Rgba([p[0], p[1], p[2], alpha]) }) }