use crate::kernels::geometry::{js_ceil, js_floor, js_round, Rect};
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum Axis {
X,
Y,
}
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum Choose {
First,
Last,
}
#[allow(clippy::too_many_arguments)]
pub fn stationary_boundary(
rgba: &[u8],
width: usize,
height: usize,
axis: Axis,
from: f64,
to: f64,
cross_from: f64,
cross_to: f64,
choose: Choose,
) -> Option<f64> {
let (w, h) = (width as i64, height as i64);
let limit = if axis == Axis::X { w } else { h };
let cross_limit = if axis == Axis::X { h } else { w };
let step = (((cross_to - cross_from) / 160.0).floor() as i64).max(1);
let mut found = None;
let mut v = if from > 1.0 { from } else { 1.0 };
let v_end = (limit as f64).min(to + 1.0);
let c_start = (cross_from + 2.0).max(0.0);
let c_end = (cross_limit as f64).min(cross_to - 2.0);
while v < v_end {
let (mut strong, mut count, mut sum) = (0u32, 0u32, 0.0f64);
let mut poisoned = false;
let mut c = c_start;
while c < c_end {
let (px, py) = if axis == Axis::X { (v, c) } else { (c, v) };
let idx = (py * w as f64 + px) * 4.0;
if idx.fract() != 0.0 {
poisoned = true;
break;
}
let i = idx as usize;
let j = if axis == Axis::X {
i - 4
} else {
i - (w as usize) * 4
};
let d = ((rgba[i] as i32 - rgba[j] as i32).abs()
+ (rgba[i + 1] as i32 - rgba[j + 1] as i32).abs()
+ (rgba[i + 2] as i32 - rgba[j + 2] as i32).abs()) as f64
/ 3.0;
if d > 3.0 {
strong += 1;
}
sum += d;
count += 1;
c += step as f64;
}
if !poisoned && count > 0 && strong as f64 / count as f64 > 0.68 && sum / count as f64 > 3.0
{
found = Some(v);
if choose == Choose::First {
break;
}
}
v += 1.0;
}
found
}
#[allow(clippy::too_many_arguments)]
pub fn sticky_occlusions(
previous: &[u8],
current: &[u8],
width: usize,
height: usize,
region: Rect,
motion_x: f64,
motion_y: f64,
carry_in: &[Rect],
) -> Vec<Rect> {
let (w, h) = (width as i64, height as i64);
let carry: Vec<Rect> = carry_in
.iter()
.copied()
.filter(|o| {
if o.x < 0.0 || o.y < 0.0 || o.x + o.width > w as f64 || o.y + o.height > h as f64 {
return false;
}
let y_end = o.y + o.height;
let mut y = o.y.ceil();
while y < y_end {
let start = ((y as i64 * w) as f64 + (o.x).ceil()) as usize * 4;
let end = ((y as i64 * w) as f64 + (o.x + o.width).ceil()) as usize * 4;
if previous[start..end] != current[start..end] {
return false;
}
y += 1.0;
}
true
})
.collect();
if crate::kernels::geometry::js_hypot(motion_x, motion_y) < 2.0 {
return carry;
}
let r = region;
let (dx, dy) = (js_round(motion_x) as i64, js_round(motion_y) as i64);
let end = (h - 2).min(js_floor(r.y + (288.0f64).min(r.height * 0.23)) as i64);
let start = 2i64.max(js_floor(r.y) as i64);
let step = 1i64.max(js_floor(r.width / 700.0) as i64);
let (mut last_fixed, mut strong_rows, mut matched) = (-1i64, 0u32, 0u32);
let x_start = 2i64.max(js_ceil(r.x) as i64);
let x_end = (w - 2) as f64;
let window = (dy.abs() / 4).min(16);
let mut samples: Vec<usize> = Vec::new();
for y in start..end {
if y + dy < 2 || y + dy >= h - 2 {
continue;
}
let (mut zero, mut shifted) = (0i64, 0i64);
samples.clear();
let mut x = x_start;
while (x as f64) < x_end.min(r.x + r.width) {
if x + dx < 2 || x + dx >= w - 2 {
x += step;
continue;
}
let i = ((y * w + x) * 4) as usize;
if (current[i - 4] as i32 - current[i + 4] as i32).abs() < 24 {
x += step;
continue;
}
let j = (((y + dy) * w + x + dx) * 4) as usize;
zero += (previous[i] as i64 - current[i] as i64).abs();
shifted += (previous[j] as i64 - current[i] as i64).abs();
samples.push(i);
x += step;
}
let n = samples.len() as u32;
let nf = n as f64;
if n < 8 || (shifted as f64) / nf <= 18.0 {
continue;
}
let mut still = (zero as f64) / nf < 6.0;
let page_inside = (y + dy) as f64 >= r.y && ((y + dy) as f64) < r.y + r.height;
let mut k = if page_inside { -window } else { window + 1 };
while !still && k <= window {
if k != 0 && (dy - k).abs() >= 8 && y + k >= 2 && y + k < h - 2 {
let offset = k * w * 4;
let sum: i64 = samples
.iter()
.map(|&i| {
(previous[(i as i64 + offset) as usize] as i64 - current[i] as i64).abs()
})
.sum();
still = (sum as f64) / nf < 6.0;
}
k += 1;
}
if still {
last_fixed = y;
strong_rows += 1;
matched += n;
}
}
if strong_rows < 4 || matched < 64 || last_fixed < 0 {
return carry;
}
let edge = stationary_boundary(
current,
width,
height,
Axis::Y,
(last_fixed + 1) as f64,
(end.min(last_fixed + 80)) as f64,
r.x,
r.x + r.width,
Choose::First,
);
match edge {
Some(edge) if edge - start as f64 <= r.height * 0.23 => vec![Rect {
x: r.x,
y: r.y,
width: r.width,
height: edge - r.y + 1.0,
}],
_ => carry,
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct Structure {
pub vertical: u32,
pub horizontal: u32,
pub rows: u32,
pub cols: u32,
}
#[allow(clippy::too_many_arguments)]
pub fn visible_structure(
rgba: &[u8],
width: usize,
height: usize,
mask: &[u8],
mask_width: usize,
mask_height: usize,
factor: f64,
) -> Structure {
let (w, h) = (width as i64, height as i64);
let (mw, mh) = (mask_width as i64, mask_height as i64);
let step = (js_round(factor) as i64).max(1);
let inside =
|x: i64, y: i64| x >= 0 && y >= 0 && x < mw && y < mh && mask[(y * mw + x) as usize] != 0;
let red = |x: i64, y: i64| rgba[((y * w + x) * 4) as usize] as i32;
let mut out = Structure::default();
let mut cols = vec![false; mask_width];
let mut y = 1i64;
while y < mh - 1 {
let ny = js_floor(y as f64 * factor) as i64;
let mut row_hit = false;
let mut x = 1i64;
while x < mw - 1 {
if inside(x, y) {
let nx = js_floor(x as f64 * factor) as i64;
let mut hit = false;
if inside(x, y - 1)
&& inside(x, y + 1)
&& ny - step >= 0
&& ny + step < h
&& nx < w
&& (red(nx, ny - step) - red(nx, ny + step)).abs() > 12
{
out.vertical += 1;
hit = true;
}
if inside(x - 1, y)
&& inside(x + 1, y)
&& nx - step >= 0
&& nx + step < w
&& ny < h
&& (red(nx - step, ny) - red(nx + step, ny)).abs() > 12
{
out.horizontal += 1;
hit = true;
}
if hit {
row_hit = true;
cols[x as usize] = true;
}
}
x += 2;
}
if row_hit {
out.rows += 1;
}
y += 2;
}
out.cols = cols.iter().filter(|&&c| c).count() as u32;
out
}
#[cfg(test)]
mod tests {
use super::*;
const W: usize = 400;
const H: usize = 600;
const BAND: i64 = 64;
fn noise(x: i64, y: i64, seed: u32) -> u8 {
let mut v =
(x as u32).wrapping_mul(0x9e37_79b9) ^ (y as u32).wrapping_mul(0x85eb_ca6b) ^ seed;
v ^= v >> 15;
v = v.wrapping_mul(0x2c1b_3c6d);
v ^= v >> 12;
(v & 0xff) as u8
}
fn put(frame: &mut [u8], x: usize, y: usize, v: u8) {
let i = (y * W + x) * 4;
frame[i..i + 4].copy_from_slice(&[v, v.wrapping_mul(3), 255 - v, 255]);
}
fn frame(page: i64, band_top: Option<i64>, page_row: &dyn Fn(i64) -> i64) -> Vec<u8> {
let mut out = vec![0u8; W * H * 4];
for y in 0..H as i64 {
for x in 0..W as i64 {
let v = match band_top {
Some(top) if y >= top && y < top + BAND => {
if y - top == BAND - 1 {
30
} else {
noise(x, y - top, 7)
}
}
_ => noise(x, page_row(y + page), 1),
};
put(&mut out, x as usize, y as usize, v);
}
}
out
}
fn region() -> Rect {
Rect {
x: 0.0,
y: 0.0,
width: W as f64,
height: H as f64,
}
}
fn plain(y: i64) -> i64 {
y
}
fn bands(previous: &[u8], current: &[u8], dy: f64) -> Vec<Rect> {
sticky_occlusions(previous, current, W, H, region(), 0.0, dy, &[])
}
fn band_bottom(found: &[Rect]) -> Option<f64> {
found.first().map(|b| b.y + b.height)
}
#[test]
fn a_still_band_over_a_scrolling_page_is_found() {
let previous = frame(0, Some(0), &plain);
let current = frame(40, Some(0), &plain);
assert_eq!(band_bottom(&bands(&previous, ¤t, 40.0)), Some(64.0));
}
#[test]
fn a_band_sliding_at_its_own_small_speed_is_found() {
for (k, dy) in [(2, 60.0), (5, 60.0), (12, 60.0), (3, 135.0)] {
let previous = frame(0, Some(0), &plain);
let current = frame(dy as i64, Some(-k), &plain);
assert_eq!(
band_bottom(&bands(&previous, ¤t, dy)),
Some((BAND - k) as f64),
"band sliding {k} px over a page moving {dy} px"
);
}
}
#[test]
fn a_band_sliding_down_while_the_page_scrolls_back_is_found() {
let previous = frame(-40, Some(20), &plain);
let current = frame(-80, Some(26), &plain);
let region = Rect {
x: 0.0,
y: 20.0,
width: W as f64,
height: H as f64 - 20.0,
};
let found = sticky_occlusions(&previous, ¤t, W, H, region, 0.0, -40.0, &[]);
assert_eq!(band_bottom(&found), Some(26.0 + BAND as f64));
}
#[test]
fn page_content_is_never_a_band() {
let previous = frame(0, None, &plain);
let current = frame(40, None, &plain);
assert!(bands(&previous, ¤t, 40.0).is_empty());
let periodic = |y: i64| y.rem_euclid(6);
let previous = frame(0, None, &periodic);
let current = frame(40, None, &periodic);
assert!(bands(&previous, ¤t, 40.0).is_empty());
}
#[test]
fn rows_whose_page_partner_is_chrome_above_the_region_take_no_shift() {
let periodic = |y: i64| y.rem_euclid(9);
let strip = |f: &mut Vec<u8>| {
for y in 0..40 {
for x in 0..W {
put(f, x, y, 30);
}
}
};
let mut previous = frame(0, None, &periodic);
let mut current = frame(-37, None, &periodic);
strip(&mut previous);
strip(&mut current);
let region = Rect {
x: 0.0,
y: 40.0,
width: W as f64,
height: H as f64 - 40.0,
};
assert!(sticky_occlusions(&previous, ¤t, W, H, region, 0.0, -37.0, &[]).is_empty());
}
#[test]
fn content_at_a_large_share_of_the_page_speed_is_not_a_band() {
let previous = frame(0, Some(0), &plain);
let current = frame(40, Some(-20), &plain);
assert!(bands(&previous, ¤t, 40.0).is_empty());
let current = frame(40, Some(-11), &plain);
assert!(bands(&previous, ¤t, 40.0).is_empty());
}
#[test]
fn content_slower_than_a_quarter_of_the_page_is_a_band() {
let previous = frame(0, Some(0), &plain);
let current = frame(40, Some(-8), &plain);
assert_eq!(
band_bottom(&bands(&previous, ¤t, 40.0)),
Some((BAND - 8) as f64)
);
}
#[test]
fn a_slow_scroll_keeps_the_shift_away_from_the_page() {
let previous = frame(0, Some(0), &plain);
let current = frame(10, Some(-2), &plain);
assert_eq!(
band_bottom(&bands(&previous, ¤t, 10.0)),
Some((BAND - 2) as f64)
);
let current = frame(9, Some(-2), &plain);
assert!(bands(&previous, ¤t, 9.0).is_empty());
let current = frame(8, Some(-2), &plain);
assert!(bands(&previous, ¤t, 8.0).is_empty());
}
#[test]
fn the_shift_window_never_reads_above_the_frame() {
let previous = frame(0, Some(0), &plain);
let current = frame(40, Some(3), &plain);
assert_eq!(
band_bottom(&bands(&previous, ¤t, 40.0)),
Some((BAND + 3) as f64)
);
}
}
#[cfg(test)]
mod structure_tests {
use super::*;
fn frame(w: usize, h: usize, paint: impl Fn(usize, usize) -> u8) -> Vec<u8> {
let mut out = vec![255u8; w * h * 4];
for y in 0..h {
for x in 0..w {
let v = paint(x, y);
out[(y * w + x) * 4..(y * w + x) * 4 + 3].fill(v);
}
}
out
}
#[test]
fn a_blank_area_has_no_structure() {
let img = frame(40, 40, |_, _| 0);
let s = visible_structure(&img, 40, 40, &[1; 1600], 40, 40, 1.0);
assert_eq!(s, Structure::default());
}
#[test]
fn a_single_horizontal_line_sits_on_at_most_two_sampled_rows() {
let img = frame(40, 40, |_, y| if y == 10 { 80 } else { 0 });
let s = visible_structure(&img, 40, 40, &[1; 1600], 40, 40, 1.0);
assert!(s.rows <= 2 && s.cols >= 15 && s.horizontal == 0, "{s:?}");
}
#[test]
fn a_single_vertical_line_sits_on_at_most_two_sampled_columns() {
let img = frame(40, 40, |x, _| if x == 10 { 80 } else { 0 });
let s = visible_structure(&img, 40, 40, &[1; 1600], 40, 40, 1.0);
assert!(s.cols <= 2 && s.rows >= 15 && s.vertical == 0, "{s:?}");
}
#[test]
fn icons_spread_over_the_area_have_structure_on_many_rows_and_columns() {
let img = frame(
40,
40,
|x, y| if x % 10 < 4 && y % 10 < 4 { 200 } else { 0 },
);
let s = visible_structure(&img, 40, 40, &[1; 1600], 40, 40, 1.0);
assert!(
s.rows >= 3 && s.cols >= 3 && s.vertical > 0 && s.horizontal > 0,
"{s:?}"
);
}
#[test]
fn structure_outside_the_mask_does_not_count() {
let img = frame(40, 40, |x, y| {
if (10..30).contains(&x) && (10..30).contains(&y) {
0
} else {
((x * 37 + y * 91) % 256) as u8
}
});
let mut mask = vec![0u8; 1600];
for y in 10..30 {
mask[y * 40 + 10..y * 40 + 30].fill(1);
}
let s = visible_structure(&img, 40, 40, &mask, 40, 40, 1.0);
assert_eq!(s, Structure::default());
}
#[test]
fn native_points_are_read_at_the_factor() {
let img = frame(40, 40, |x, y| {
if (x / 5) % 3 == 0 && (y / 5) % 3 == 0 {
200
} else {
0
}
});
let s = visible_structure(&img, 40, 40, &[1; 64], 8, 8, 5.0);
assert!(s.rows >= 3 && s.cols >= 3, "{s:?}");
}
}