use super::{Def, DrawnLayer, Found, Kind, LintCode, LintOptions, Sink, pixel_position};
use crate::{Color, Image, Sprite};
pub(super) struct Canvas {
image: Image,
pub w: i64,
pub h: i64,
}
impl Canvas {
pub fn new(sprite: &Sprite) -> Canvas {
Canvas {
image: sprite.render(),
w: sprite.width as i64,
h: sprite.height as i64,
}
}
pub fn at(&self, x: i64, y: i64) -> Color {
if x < 0 || y < 0 || x >= self.w || y >= self.h {
return Color::TRANSPARENT;
}
let c = self
.image
.pixel(x as u32, y as u32)
.unwrap_or(Color::TRANSPARENT);
if c.a == 0 { Color::TRANSPARENT } else { c }
}
pub fn visible(&self, x: i64, y: i64) -> bool {
self.at(x, y).a > 0
}
pub fn has_silhouette(&self) -> bool {
(0..self.h).any(|y| (0..self.w).any(|x| !self.visible(x, y)))
}
pub fn on_silhouette(&self, x: i64, y: i64) -> bool {
self.visible(x, y)
&& [(-1, 0), (1, 0), (0, -1), (0, 1)]
.iter()
.any(|&(dx, dy)| !self.visible(x + dx, y + dy))
}
pub fn outline_color(&self) -> Option<Color> {
if !self.has_silhouette() {
return None;
}
let mut counts: std::collections::HashMap<[u8; 4], usize> = Default::default();
let mut total = 0;
for y in 0..self.h {
for x in 0..self.w {
if self.on_silhouette(x, y) {
total += 1;
*counts.entry(self.at(x, y).to_array()).or_default() += 1;
}
}
}
let (&outline, &n) = counts
.iter()
.max_by_key(|&(c, n)| (*n, std::cmp::Reverse(*c)))?;
if n * 2 < total {
return None;
}
let color = Color::rgba(outline[0], outline[1], outline[2], outline[3]);
let around = (0..self.h).any(|y| {
(0..self.w)
.any(|x| self.visible(x, y) && !self.on_silhouette(x, y) && self.at(x, y) != color)
});
around.then_some(color)
}
}
pub(super) const AROUND: [(i64, i64); 8] = [
(-1, -1),
(0, -1),
(1, -1),
(-1, 0),
(1, 0),
(-1, 1),
(0, 1),
(1, 1),
];
pub(super) fn check(canvas: &Canvas, report: &Report, options: &LintOptions, sink: &mut Sink) {
if canvas.w < 3 || canvas.h < 3 {
return;
}
orphan_pixels(canvas, report, options, sink);
jaggies(canvas, report, sink);
double_outlines(canvas, report, sink);
stray_alpha(canvas, report, sink);
}
pub(super) struct Report<'r> {
pub sprite: &'r Sprite,
pub def: &'r Def,
pub layers: &'r [DrawnLayer],
}
impl Report<'_> {
pub fn push(
&self,
sink: &mut Sink,
code: LintCode,
pixels: Vec<(u32, u32)>,
message: impl FnOnce() -> String,
) {
let position = pixels
.first()
.and_then(|&(x, y)| pixel_position(self.sprite, self.layers, x, y))
.unwrap_or(self.def.pos);
let found = Found {
code,
kind: Kind::Sprite,
name: &self.sprite.name,
position: Some(position),
sprite: Some(&self.sprite.name),
pixels,
};
sink.push(found, message);
}
}
fn luma(c: Color) -> i32 {
(299 * c.r as i32 + 587 * c.g as i32 + 114 * c.b as i32) / 1000
}
fn orphan_pixels(canvas: &Canvas, report: &Report, options: &LintOptions, sink: &mut Sink) {
let name = &report.sprite.name;
for y in 0..canvas.h {
for x in 0..canvas.w {
let c = canvas.at(x, y);
if c.a == 0 {
continue;
}
let at = |(dx, dy): (i64, i64)| canvas.at(x + dx, y + dy);
let sides = [(-1, 0), (1, 0), (0, -1), (0, 1)].map(at);
let corners = [(-1, -1), (1, -1), (-1, 1), (1, 1)].map(at);
let around = sides[0];
if sides.iter().any(|&s| s != around) || around == c {
continue;
}
let isolated = around.a == 0;
if isolated {
if corners.iter().any(|k| k.a > 0) {
continue;
}
} else {
let contrast = (luma(c) - luma(around)).abs();
if corners.contains(&c) || contrast > options.orphan_contrast as i32 {
continue;
}
}
let (x, y) = (x as u32, y as u32);
report.push(sink, LintCode::OrphanPixel, vec![(x, y)], || {
if isolated {
format!("A lone pixel in sprite `{name}` at ({x}, {y}), touching nothing")
} else {
format!(
"A lone pixel in sprite `{name}` at ({x}, {y}), close in lightness to \
the color around it, reads as noise"
)
}
});
}
}
}
#[derive(Clone, Copy)]
struct Run {
start: i64,
len: i64,
value: i64,
}
fn jaggies(canvas: &Canvas, report: &Report, sink: &mut Sink) {
let name = &report.sprite.name;
let mut reported: std::collections::HashSet<(u32, u32)> = Default::default();
for side in 0..4 {
let (count, depth) = if side < 2 {
(canvas.w, canvas.h)
} else {
(canvas.h, canvas.w)
};
let edge = |i: i64| -> Option<i64> {
let mut steps = 0..depth;
let mut find = |d: i64| {
let d = if side % 2 == 0 { d } else { depth - 1 - d };
let (x, y) = if side < 2 { (i, d) } else { (d, i) };
canvas.visible(x, y).then_some(d)
};
steps.find_map(&mut find)
};
let pixel = |i: i64, value: i64| -> (u32, u32) {
if side < 2 {
(i as u32, value as u32)
} else {
(value as u32, i as u32)
}
};
let mut runs: Vec<Run> = Vec::new();
for i in 0..count {
let value = edge(i);
match (value, runs.last_mut()) {
(Some(v), Some(last)) if last.value == v && last.start + last.len == i => {
last.len += 1;
}
(Some(v), _) => runs.push(Run {
start: i,
len: 1,
value: v,
}),
(None, _) => {}
}
}
let mut first = 0;
while first < runs.len() {
let mut last = first;
let mut direction = 0;
let mut turns = false;
while last + 1 < runs.len() {
let (a, b) = (runs[last], runs[last + 1]);
let step = b.value - a.value;
let touching = a.start + a.len == b.start;
if !touching || step.abs() != 1 {
break;
}
if direction != 0 && step != direction {
turns = true;
break;
}
direction = step;
last += 1;
}
let stretch = &runs[first..=last];
first = if turns { last } else { last + 1 };
if stretch.len() < 5 {
continue;
}
let inner = &stretch[1..stretch.len() - 1];
let odd: Vec<Run> = inner
.windows(3)
.filter(|w| {
let (before, step, after) = (w[0].len, w[1].len, w[2].len);
(step >= before + 2 && step >= after + 2)
|| (step + 2 <= before && step + 2 <= after)
})
.map(|w| w[1])
.collect();
let steps: Vec<String> = inner.iter().map(|r| r.len.to_string()).collect();
for run in odd {
let pixels: Vec<(u32, u32)> = (run.start..run.start + run.len)
.map(|i| pixel(i, run.value))
.collect();
if pixels.iter().any(|p| reported.contains(p)) {
continue;
}
reported.extend(pixels.iter().copied());
let (x, y) = pixels[0];
let steps = &steps;
report.push(sink, LintCode::Jaggy, pixels, || {
format!(
"Uneven steps along the edge of sprite `{name}`: {} pixels long; the \
step of {} at ({x}, {y}) breaks the line's rhythm",
steps.join(", "),
run.len
)
});
}
}
}
}
fn double_outlines(canvas: &Canvas, report: &Report, sink: &mut Sink) {
let sprite = report.sprite;
let (w, h) = (canvas.w, canvas.h);
let inside = |x: i64, y: i64| x >= 0 && y >= 0 && x < w && y < h;
let index = |x: i64, y: i64| (y * w + x) as usize;
let Some(outline) = canvas.outline_color() else {
return;
};
let edge = |x: i64, y: i64| canvas.on_silhouette(x, y);
let outlined = |x: i64, y: i64| canvas.visible(x, y) && canvas.at(x, y) == outline;
let mut mask = vec![false; (w * h) as usize];
let mut stack: Vec<(i64, i64)> = (0..h)
.flat_map(|y| (0..w).map(move |x| (x, y)))
.filter(|&(x, y)| edge(x, y) && outlined(x, y))
.collect();
while let Some((x, y)) = stack.pop() {
if mask[index(x, y)] {
continue;
}
mask[index(x, y)] = true;
for (dx, dy) in AROUND {
let (nx, ny) = (x + dx, y + dy);
if inside(nx, ny) && !mask[index(nx, ny)] && canvas.visible(nx, ny) && outlined(nx, ny)
{
stack.push((nx, ny));
}
}
}
let on = |mask: &[bool], x: i64, y: i64| inside(x, y) && mask[index(x, y)];
for y in 0..h {
for x in 0..w {
if !on(&mask, x, y) {
continue;
}
let neighbours: Vec<(i64, i64)> = AROUND
.iter()
.map(|&(dx, dy)| (x + dx, y + dy))
.filter(|&(nx, ny)| on(&mask, nx, ny))
.collect();
let across = [(-1, 0), (1, 0)]
.iter()
.filter(|&&(dx, dy)| on(&mask, x + dx, y + dy))
.count();
let down = [(0, -1), (0, 1)]
.iter()
.filter(|&&(dx, dy)| on(&mask, x + dx, y + dy))
.count();
if neighbours.len() < 3 || across != 1 || down != 1 {
continue;
}
let square = [(-1, -1), (0, -1), (-1, 0), (0, 0)]
.iter()
.any(|&(sx, sy)| {
[(0, 0), (1, 0), (0, 1), (1, 1)]
.iter()
.all(|&(dx, dy)| on(&mask, x + sx + dx, y + sy + dy))
});
let fill = (-2..=2).any(|dy: i64| {
(-2..=2).any(|dx: i64| {
let (nx, ny) = (x + dx, y + dy);
canvas.visible(nx, ny) && !outlined(nx, ny)
})
});
if square || !fill || !connected(&neighbours) {
continue;
}
mask[index(x, y)] = false;
let (x, y) = (x as u32, y as u32);
report.push(sink, LintCode::DoubleOutline, vec![(x, y)], || {
format!(
"The outline of sprite `{}` is two pixels thick at ({x}, {y}); one \
diagonal step would do",
sprite.name
)
});
}
}
}
fn connected(pixels: &[(i64, i64)]) -> bool {
let mut reached = vec![false; pixels.len()];
let mut stack = vec![0];
while let Some(i) = stack.pop() {
if std::mem::replace(&mut reached[i], true) {
continue;
}
let (x, y) = pixels[i];
for (j, &(nx, ny)) in pixels.iter().enumerate() {
if !reached[j] && (nx - x).abs() <= 1 && (ny - y).abs() <= 1 {
stack.push(j);
}
}
}
reached.iter().all(|&r| r)
}
fn stray_alpha(canvas: &Canvas, report: &Report, sink: &mut Sink) {
let translucent = |x: i64, y: i64| {
let a = canvas.at(x, y).a;
a > 0 && a < 255
};
let (mut visible, mut partial) = (0usize, 0usize);
for y in 0..canvas.h {
for x in 0..canvas.w {
visible += canvas.visible(x, y) as usize;
partial += translucent(x, y) as usize;
}
}
if partial == 0 || partial * 8 >= visible {
return;
}
let name = &report.sprite.name;
for y in 0..canvas.h {
for x in 0..canvas.w {
if !translucent(x, y) || AROUND.iter().any(|&(dx, dy)| translucent(x + dx, y + dy)) {
continue;
}
let alpha = canvas.at(x, y).a;
let (x, y) = (x as u32, y as u32);
report.push(sink, LintCode::StrayAlpha, vec![(x, y)], || {
format!(
"A lone translucent pixel in sprite `{name}` at ({x}, {y}), alpha {alpha}: \
a leftover of smoothing that blurs when scaled"
)
});
}
}
}