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}