1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
use common::util::{linear_to_srgba, srgba_to_linear};
use common_base::span;
/// Pixel art scaling
/// Note: The current ui is locked to the pixel grid with little animation, if
/// we want smoothly moving pixel art this should be done in the shaders
/// useful links: https://gitlab.com/veloren/veloren/issues/257
use image::RgbaImage;
use vek::*;

const EPSILON: f32 = 0.0001;

// Averaging colors with alpha such that when blending with the background color
// the same color will be produced as when the individual colors were blended
// with the background and then averaged.
//
// Say we have two areas that we are combining to form a single pixel
// A1 and A2 where these are the fraction of the area of the pixel each color
// contributes to.
//
// Then if the colors were opaque we would say that the final
// color output color o3 is
//     E1: o3 = A1 * o1 + A2 * o2
// where o1 and o2 are the opaque colors of the two areas
// now say the areas are actually translucent and these opaque colors are
// derived by blending with a common background color b
//     E2: o1 = c1 * a1 + b * (1 - a1)
//     E3: o2 = c2 * a2 + b * (1 - a2)
// we want to find the combined color (c3) and combined alpha (a3) such that
//     E4: o3 = c3 * a3 + b * (1 - a3)
// substitution of E2 and E3 into E1 gives
//     E5: o3 = A1 * (c1 * a1 + b * (1 - a1)) + A2 * (c2 * a2 + b * (1 - a2))
// combining E4 and E5 then separating like terms into separate equations gives
//     E6: c3 * a3 = A1 * c1 * a1 + A2 * c2 * a2
//     E7: b * (1 - a3) = A1 * b * (1 - a1) + A2 * b * (1 - a2)
// dropping b from E7 and solving for a3
//     E8: a3 = 1 - A1 * (1 - a1) - A2 * (1 - a2)
// we can now calculate the combined alpha value
// and E6 can then be solved for c3
//     E9: c3 = (A1 * c1 * a1 + A2 * c2 * a2) / a3
pub fn resize_pixel_art(image: &RgbaImage, new_width: u32, new_height: u32) -> RgbaImage {
    span!(_guard, "resize_pixel_art");
    let (width, height) = image.dimensions();
    let mut new_image = RgbaImage::new(new_width, new_height);

    // Ratio of old image dimensions to new dimensions
    // Also the sampling dimensions within the old image for a single pixel in the
    // new image
    let wratio = width as f32 / new_width as f32;
    let hratio = height as f32 / new_height as f32;

    for x in 0..new_width {
        // Calculate sampling strategy
        let xsmin = x as f32 * wratio;
        let xsmax = (xsmin + wratio).min(width as f32);
        // Min and max pixels covered
        let xminp = xsmin.floor() as u32;
        let xmaxp = ((xsmax - EPSILON).ceil() as u32).saturating_sub(1);
        // Fraction of first pixel to use
        let first_x_frac = if xminp != xmaxp {
            1.0 - xsmin.fract()
        } else {
            xsmax - xsmin
        };
        let last_x_frac = xsmax - xmaxp as f32;
        for y in 0..new_height {
            // Calculate sampling strategy
            let ysmin = y as f32 * hratio;
            let ysmax = (ysmin + hratio).min(height as f32);
            // Min and max of pixels covered
            let yminp = ysmin.floor() as u32;
            let ymaxp = ((ysmax - EPSILON).ceil() as u32).saturating_sub(1);
            // Fraction of first pixel to use
            let first_y_frac = if yminp != ymaxp {
                1.0 - ysmin.fract()
            } else {
                ysmax - ysmin
            };
            let last_y_frac = ysmax - ymaxp as f32;

            let mut linear_color = Rgba::new(0.0, 0.0, 0.0, wratio * hratio);
            // Left column
            // First pixel sample (top left assuming that is the origin)
            linear_color += get_linear_with_frac(image, xminp, yminp, first_x_frac * first_y_frac);
            // Left edge
            for j in yminp + 1..ymaxp {
                linear_color += get_linear_with_frac(image, xminp, j, first_x_frac);
            }
            // Bottom left corner
            if yminp != ymaxp {
                linear_color +=
                    get_linear_with_frac(image, xminp, ymaxp, first_x_frac * last_y_frac);
            }
            // Interior columns
            for i in xminp + 1..xmaxp {
                // Top edge
                linear_color += get_linear_with_frac(image, i, yminp, first_y_frac);
                // Inner (entire pixel is encompassed by sample)
                for j in yminp + 1..ymaxp {
                    linear_color += get_linear_with_frac(image, i, j, 1.0);
                }
                // Bottom edge
                if yminp != ymaxp {
                    linear_color += get_linear_with_frac(image, xminp, ymaxp, last_y_frac);
                }
            }
            // Right column
            if xminp != xmaxp {
                // Top right corner
                linear_color +=
                    get_linear_with_frac(image, xmaxp, yminp, first_y_frac * last_x_frac);
                // Right edge
                for j in yminp + 1..ymaxp {
                    linear_color += get_linear_with_frac(image, xmaxp, j, last_x_frac);
                }
                // Bottom right corner
                if yminp != ymaxp {
                    linear_color +=
                        get_linear_with_frac(image, xmaxp, ymaxp, last_x_frac * last_y_frac);
                }
            }
            // Divide summed color by area sample covers and convert back to srgb
            // I wonder if precalculating the inverse of these divs would have a significant
            // effect
            linear_color = linear_color / wratio / hratio;
            linear_color =
                Rgba::from_translucent(linear_color.rgb() / linear_color.a, linear_color.a);
            new_image.put_pixel(
                x,
                y,
                image::Rgba(
                    linear_to_srgba(linear_color)
                        .map(|e| (e * 255.0).round() as u8)
                        .into_array(),
                ),
            );
        }
    }
    new_image
}

fn get_linear(image: &RgbaImage, x: u32, y: u32) -> Rgba<f32> {
    srgba_to_linear(Rgba::<u8>::from(image.get_pixel(x, y).0).map(|e| e as f32 / 255.0))
}

// See comments above resize_pixel_art
fn get_linear_with_frac(image: &RgbaImage, x: u32, y: u32, frac: f32) -> Rgba<f32> {
    let rgba = get_linear(image, x, y);
    let adjusted_rgb = rgba.rgb() * rgba.a * frac;
    let adjusted_alpha = -frac * (1.0 - rgba.a);
    Rgba::from_translucent(adjusted_rgb, adjusted_alpha)
}