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)
}