use super::api::{Api, ApiError, Skill, Specialization, Trait}; use super::bt::{BuildTemplate, TraitChoice, Traitline}; use image::{ imageops, CatmullRom, DynamicImage, GenericImage, GenericImageView, ImageBuffer, Pixel, Primitive, Rgba, RgbaImage, }; use imageproc::drawing; use num_traits::NumCast; use rusttype::{Font, Scale, SharedBytes}; use std::cmp::min; 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_use_gradient: bool, traitline_gradient_size: u32, 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: 137, traitline_width: 647, traitline_brightness: 100, traitline_use_gradient: true, traitline_gradient_size: 50, traitline_x_offset: 200, 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, } } } // These values were determined from the thief specialization background images, as those do not // have anything fancy going on and are instead transparent. Therefore, we can use them as a // "reference" for the intended specialization crop size. // // Note: Increasing those values will mean that thief build templates will end up with more // transparency than intended. const BG_CROP_HEIGHT: u32 = 136; const BG_CROP_WIDTH: u32 = 647; 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, 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, 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, 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, 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() / 2; let y_slice = buffer.height() / 3; let end_y = y_slice * (choice as u32 - 1) + y_slice / 2; draw_thick_line( buffer, (start_x, start_y), (end_x, end_y), self.options.line_height, self.options.line_color, ); if tier == 2 { return Ok(()); } draw_thick_line( buffer, (end_x + self.options.trait_size, end_y), (end_x + x_slice, start_y), self.options.line_height, 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, ) .resize( self.options.traitline_width, self.options.traitline_height, imageops::FilterType::CatmullRom, ) .to_rgba(); if self.options.traitline_use_gradient { buffer = brighten_gradient( &buffer, self.options.traitline_x_offset - self.options.traitline_gradient_size, self.options.traitline_x_offset, self.options.traitline_brightness, ); } else { buffer = imageops::colorops::brighten(&buffer, self.options.traitline_brightness); } 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]) }) } fn brighten_gradient( image: &I, start_x: u32, end_x: u32, value: i32, ) -> ImageBuffer> where I: GenericImageView, P: Pixel + 'static, S: Primitive + 'static, { use image::math::utils::clamp; let (width, height) = image.dimensions(); let mut out = ImageBuffer::new(width, height); let max = S::max_value(); let max: i32 = NumCast::from(max).unwrap(); for y in 0..height { for x in 0..width { let e = image.get_pixel(x, y).map_with_alpha( |b| { let interpol_value = if x < start_x { 0 } else if x > end_x { value } else { let frac = (x - start_x) as f32 / (end_x - start_x) as f32; (frac * value as f32).round() as i32 }; let c: i32 = NumCast::from(b).unwrap(); let d = clamp(c + interpol_value, 0, max); NumCast::from(d).unwrap() }, |alpha| alpha, ); out.put_pixel(x, y, e); } } out } fn draw_thick_line( image: &mut I, start: (u32, u32), end: (u32, u32), thickness: u32, color: I::Pixel, ) where I: GenericImage>, { let delta_x = end.0 as i32 - start.0 as i32; let delta_y = end.1 as i32 - start.1 as i32; let line_length = ((delta_x * delta_x + delta_y * delta_y) as f32) .sqrt() .round() as u32; let mut line_buffer: RgbaImage = ImageBuffer::new(line_length, line_length); let halfway = (line_length - thickness) / 2; for i in 0..thickness { for x in 0..line_length { let y = halfway + i; line_buffer.put_pixel(x, y, color); } } line_buffer = imageproc::geometric_transformations::rotate_about_center( &line_buffer, (delta_y as f32 / delta_x as f32).atan(), imageproc::geometric_transformations::Interpolation::Bicubic, Rgba([0, 0, 0, 0]), ); let half_x = (start.0 as i32 + delta_x / 2) as u32; let half_y = (start.1 as i32 + delta_y / 2) as u32; imageops::overlay( image, &line_buffer, half_x - line_length / 2, half_y - line_length / 2, ); }