veloren_voxygen/ui/graphic/pixel_art.rs
1use common::util::{linear_to_srgba, srgba_to_linear};
2use common_base::span;
3/// Pixel art scaling
4/// Note: The current ui is locked to the pixel grid with little animation, if
5/// we want smoothly moving pixel art this should be done in the shaders
6/// useful links: https://gitlab.com/veloren/veloren/issues/257
7use image::RgbaImage;
8use vek::*;
9
10const EPSILON: f32 = 0.0001;
11
12// Averaging colors with alpha such that when blending with the background color
13// the same color will be produced as when the individual colors were blended
14// with the background and then averaged.
15//
16// Say we have two areas that we are combining to form a single pixel
17// A1 and A2 where these are the fraction of the area of the pixel each color
18// contributes to.
19//
20// Then if the colors were opaque we would say that the final
21// color output color o3 is
22// E1: o3 = A1 * o1 + A2 * o2
23// where o1 and o2 are the opaque colors of the two areas
24// now say the areas are actually translucent and these opaque colors are
25// derived by blending with a common background color b
26// E2: o1 = c1 * a1 + b * (1 - a1)
27// E3: o2 = c2 * a2 + b * (1 - a2)
28// we want to find the combined color (c3) and combined alpha (a3) such that
29// E4: o3 = c3 * a3 + b * (1 - a3)
30// substitution of E2 and E3 into E1 gives
31// E5: o3 = A1 * (c1 * a1 + b * (1 - a1)) + A2 * (c2 * a2 + b * (1 - a2))
32// combining E4 and E5 then separating like terms into separate equations gives
33// E6: c3 * a3 = A1 * c1 * a1 + A2 * c2 * a2
34// E7: b * (1 - a3) = A1 * b * (1 - a1) + A2 * b * (1 - a2)
35// dropping b from E7 and solving for a3
36// E8: a3 = 1 - A1 * (1 - a1) - A2 * (1 - a2)
37// we can now calculate the combined alpha value
38// and E6 can then be solved for c3
39// E9: c3 = (A1 * c1 * a1 + A2 * c2 * a2) / a3
40pub fn resize_pixel_art(image: &RgbaImage, new_width: u32, new_height: u32) -> RgbaImage {
41 span!(_guard, "resize_pixel_art");
42 let (width, height) = image.dimensions();
43 let mut new_image = RgbaImage::new(new_width, new_height);
44
45 // Ratio of old image dimensions to new dimensions
46 // Also the sampling dimensions within the old image for a single pixel in the
47 // new image
48 let wratio = width as f32 / new_width as f32;
49 let hratio = height as f32 / new_height as f32;
50
51 for x in 0..new_width {
52 // Calculate sampling strategy
53 let xsmin = x as f32 * wratio;
54 let xsmax = (xsmin + wratio).min(width as f32);
55 // Min and max pixels covered
56 let xminp = xsmin.floor() as u32;
57 let xmaxp = ((xsmax - EPSILON).ceil() as u32).saturating_sub(1);
58 // Fraction of first pixel to use
59 let first_x_frac = if xminp != xmaxp {
60 1.0 - xsmin.fract()
61 } else {
62 xsmax - xsmin
63 };
64 let last_x_frac = xsmax - xmaxp as f32;
65 for y in 0..new_height {
66 // Calculate sampling strategy
67 let ysmin = y as f32 * hratio;
68 let ysmax = (ysmin + hratio).min(height as f32);
69 // Min and max of pixels covered
70 let yminp = ysmin.floor() as u32;
71 let ymaxp = ((ysmax - EPSILON).ceil() as u32).saturating_sub(1);
72 // Fraction of first pixel to use
73 let first_y_frac = if yminp != ymaxp {
74 1.0 - ysmin.fract()
75 } else {
76 ysmax - ysmin
77 };
78 let last_y_frac = ysmax - ymaxp as f32;
79
80 let mut linear_color = Rgba::new(0.0, 0.0, 0.0, wratio * hratio);
81 // Left column
82 // First pixel sample (top left assuming that is the origin)
83 linear_color += get_linear_with_frac(image, xminp, yminp, first_x_frac * first_y_frac);
84 // Left edge
85 for j in yminp + 1..ymaxp {
86 linear_color += get_linear_with_frac(image, xminp, j, first_x_frac);
87 }
88 // Bottom left corner
89 if yminp != ymaxp {
90 linear_color +=
91 get_linear_with_frac(image, xminp, ymaxp, first_x_frac * last_y_frac);
92 }
93 // Interior columns
94 for i in xminp + 1..xmaxp {
95 // Top edge
96 linear_color += get_linear_with_frac(image, i, yminp, first_y_frac);
97 // Inner (entire pixel is encompassed by sample)
98 for j in yminp + 1..ymaxp {
99 linear_color += get_linear_with_frac(image, i, j, 1.0);
100 }
101 // Bottom edge
102 if yminp != ymaxp {
103 linear_color += get_linear_with_frac(image, xminp, ymaxp, last_y_frac);
104 }
105 }
106 // Right column
107 if xminp != xmaxp {
108 // Top right corner
109 linear_color +=
110 get_linear_with_frac(image, xmaxp, yminp, first_y_frac * last_x_frac);
111 // Right edge
112 for j in yminp + 1..ymaxp {
113 linear_color += get_linear_with_frac(image, xmaxp, j, last_x_frac);
114 }
115 // Bottom right corner
116 if yminp != ymaxp {
117 linear_color +=
118 get_linear_with_frac(image, xmaxp, ymaxp, last_x_frac * last_y_frac);
119 }
120 }
121 // Divide summed color by area sample covers and convert back to srgb
122 // I wonder if precalculating the inverse of these divs would have a significant
123 // effect
124 linear_color = linear_color / wratio / hratio;
125 linear_color =
126 Rgba::from_translucent(linear_color.rgb() / linear_color.a, linear_color.a);
127 new_image.put_pixel(
128 x,
129 y,
130 image::Rgba(
131 linear_to_srgba(linear_color)
132 .map(|e| (e * 255.0).round() as u8)
133 .into_array(),
134 ),
135 );
136 }
137 }
138 new_image
139}
140
141fn get_linear(image: &RgbaImage, x: u32, y: u32) -> Rgba<f32> {
142 srgba_to_linear(Rgba::<u8>::from(image.get_pixel(x, y).0).map(|e| e as f32 / 255.0))
143}
144
145// See comments above resize_pixel_art
146fn get_linear_with_frac(image: &RgbaImage, x: u32, y: u32, frac: f32) -> Rgba<f32> {
147 let rgba = get_linear(image, x, y);
148 let adjusted_rgb = rgba.rgb() * rgba.a * frac;
149 let adjusted_alpha = -frac * (1.0 - rgba.a);
150 Rgba::from_translucent(adjusted_rgb, adjusted_alpha)
151}