aboutsummaryrefslogtreecommitdiff
path: root/src/render.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/render.rs')
-rw-r--r--src/render.rs325
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)
+ }
+}