1use crate::kernels::geometry::{js_ceil, js_floor, js_round, Rect};
7
8#[derive(Clone, Copy, PartialEq, Eq)]
9pub enum Axis {
10 X,
11 Y,
12}
13
14#[derive(Clone, Copy, PartialEq, Eq)]
15pub enum Choose {
16 First,
17 Last,
18}
19
20#[allow(clippy::too_many_arguments)]
24pub fn stationary_boundary(
25 rgba: &[u8],
26 width: usize,
27 height: usize,
28 axis: Axis,
29 from: f64,
30 to: f64,
31 cross_from: f64,
32 cross_to: f64,
33 choose: Choose,
34) -> Option<f64> {
35 let (w, h) = (width as i64, height as i64);
36 let limit = if axis == Axis::X { w } else { h };
37 let cross_limit = if axis == Axis::X { h } else { w };
38 let step = (((cross_to - cross_from) / 160.0).floor() as i64).max(1);
40 let mut found = None;
41 let mut v = if from > 1.0 { from } else { 1.0 };
44 let v_end = (limit as f64).min(to + 1.0);
45 let c_start = (cross_from + 2.0).max(0.0);
46 let c_end = (cross_limit as f64).min(cross_to - 2.0);
47 while v < v_end {
48 let (mut strong, mut count, mut sum) = (0u32, 0u32, 0.0f64);
49 let mut poisoned = false;
53 let mut c = c_start;
54 while c < c_end {
55 let (px, py) = if axis == Axis::X { (v, c) } else { (c, v) };
56 let idx = (py * w as f64 + px) * 4.0;
57 if idx.fract() != 0.0 {
58 poisoned = true;
59 break;
60 }
61 let i = idx as usize;
62 let j = if axis == Axis::X {
63 i - 4
64 } else {
65 i - (w as usize) * 4
66 };
67 let d = ((rgba[i] as i32 - rgba[j] as i32).abs()
68 + (rgba[i + 1] as i32 - rgba[j + 1] as i32).abs()
69 + (rgba[i + 2] as i32 - rgba[j + 2] as i32).abs()) as f64
70 / 3.0;
71 if d > 3.0 {
72 strong += 1;
73 }
74 sum += d;
75 count += 1;
76 c += step as f64;
77 }
78 if !poisoned && count > 0 && strong as f64 / count as f64 > 0.68 && sum / count as f64 > 3.0
79 {
80 found = Some(v);
82 if choose == Choose::First {
83 break;
84 }
85 }
86 v += 1.0;
87 }
88 found
89}
90
91#[allow(clippy::too_many_arguments)]
102pub fn sticky_occlusions(
103 previous: &[u8],
104 current: &[u8],
105 width: usize,
106 height: usize,
107 region: Rect,
108 motion_x: f64,
109 motion_y: f64,
110 carry_in: &[Rect],
111) -> Vec<Rect> {
112 let (w, h) = (width as i64, height as i64);
113 let carry: Vec<Rect> = carry_in
116 .iter()
117 .copied()
118 .filter(|o| {
119 if o.x < 0.0 || o.y < 0.0 || o.x + o.width > w as f64 || o.y + o.height > h as f64 {
120 return false;
121 }
122 let y_end = o.y + o.height;
123 let mut y = o.y.ceil();
124 while y < y_end {
125 let start = ((y as i64 * w) as f64 + (o.x).ceil()) as usize * 4;
126 let end = ((y as i64 * w) as f64 + (o.x + o.width).ceil()) as usize * 4;
127 if previous[start..end] != current[start..end] {
128 return false;
129 }
130 y += 1.0;
131 }
132 true
133 })
134 .collect();
135 if crate::kernels::geometry::js_hypot(motion_x, motion_y) < 2.0 {
136 return carry;
137 }
138 let r = region;
139 let (dx, dy) = (js_round(motion_x) as i64, js_round(motion_y) as i64);
140 let end = (h - 2).min(js_floor(r.y + (288.0f64).min(r.height * 0.23)) as i64);
141 let start = 2i64.max(js_floor(r.y) as i64);
142 let step = 1i64.max(js_floor(r.width / 700.0) as i64);
143 let (mut last_fixed, mut strong_rows, mut matched) = (-1i64, 0u32, 0u32);
144 let x_start = 2i64.max(js_ceil(r.x) as i64);
145 let x_end = (w - 2) as f64;
146 let window = (dy.abs() / 4).min(16);
147 let mut samples: Vec<usize> = Vec::new();
148 for y in start..end {
149 if y + dy < 2 || y + dy >= h - 2 {
150 continue;
151 }
152 let (mut zero, mut shifted) = (0i64, 0i64);
153 samples.clear();
154 let mut x = x_start;
155 while (x as f64) < x_end.min(r.x + r.width) {
157 if x + dx < 2 || x + dx >= w - 2 {
158 x += step;
159 continue;
160 }
161 let i = ((y * w + x) * 4) as usize;
162 if (current[i - 4] as i32 - current[i + 4] as i32).abs() < 24 {
163 x += step;
164 continue;
165 }
166 let j = (((y + dy) * w + x + dx) * 4) as usize;
167 zero += (previous[i] as i64 - current[i] as i64).abs();
168 shifted += (previous[j] as i64 - current[i] as i64).abs();
169 samples.push(i);
170 x += step;
171 }
172 let n = samples.len() as u32;
173 let nf = n as f64;
174 if n < 8 || (shifted as f64) / nf <= 18.0 {
175 continue;
176 }
177 let mut still = (zero as f64) / nf < 6.0;
181 let page_inside = (y + dy) as f64 >= r.y && ((y + dy) as f64) < r.y + r.height;
182 let mut k = if page_inside { -window } else { window + 1 };
183 while !still && k <= window {
184 if k != 0 && (dy - k).abs() >= 8 && y + k >= 2 && y + k < h - 2 {
185 let offset = k * w * 4;
186 let sum: i64 = samples
187 .iter()
188 .map(|&i| {
189 (previous[(i as i64 + offset) as usize] as i64 - current[i] as i64).abs()
190 })
191 .sum();
192 still = (sum as f64) / nf < 6.0;
193 }
194 k += 1;
195 }
196 if still {
197 last_fixed = y;
198 strong_rows += 1;
199 matched += n;
200 }
201 }
202 if strong_rows < 4 || matched < 64 || last_fixed < 0 {
203 return carry;
204 }
205 let edge = stationary_boundary(
206 current,
207 width,
208 height,
209 Axis::Y,
210 (last_fixed + 1) as f64,
211 (end.min(last_fixed + 80)) as f64,
212 r.x,
213 r.x + r.width,
214 Choose::First,
215 );
216 match edge {
217 Some(edge) if edge - start as f64 <= r.height * 0.23 => vec![Rect {
218 x: r.x,
219 y: r.y,
220 width: r.width,
221 height: edge - r.y + 1.0,
222 }],
223 _ => carry,
224 }
225}
226
227#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
234pub struct Structure {
235 pub vertical: u32,
236 pub horizontal: u32,
237 pub rows: u32,
238 pub cols: u32,
239}
240
241#[allow(clippy::too_many_arguments)]
242pub fn visible_structure(
243 rgba: &[u8],
244 width: usize,
245 height: usize,
246 mask: &[u8],
247 mask_width: usize,
248 mask_height: usize,
249 factor: f64,
250) -> Structure {
251 let (w, h) = (width as i64, height as i64);
252 let (mw, mh) = (mask_width as i64, mask_height as i64);
253 let step = (js_round(factor) as i64).max(1);
254 let inside =
255 |x: i64, y: i64| x >= 0 && y >= 0 && x < mw && y < mh && mask[(y * mw + x) as usize] != 0;
256 let red = |x: i64, y: i64| rgba[((y * w + x) * 4) as usize] as i32;
257 let mut out = Structure::default();
258 let mut cols = vec![false; mask_width];
259 let mut y = 1i64;
260 while y < mh - 1 {
261 let ny = js_floor(y as f64 * factor) as i64;
262 let mut row_hit = false;
263 let mut x = 1i64;
264 while x < mw - 1 {
265 if inside(x, y) {
266 let nx = js_floor(x as f64 * factor) as i64;
267 let mut hit = false;
268 if inside(x, y - 1)
269 && inside(x, y + 1)
270 && ny - step >= 0
271 && ny + step < h
272 && nx < w
273 && (red(nx, ny - step) - red(nx, ny + step)).abs() > 12
274 {
275 out.vertical += 1;
276 hit = true;
277 }
278 if inside(x - 1, y)
279 && inside(x + 1, y)
280 && nx - step >= 0
281 && nx + step < w
282 && ny < h
283 && (red(nx - step, ny) - red(nx + step, ny)).abs() > 12
284 {
285 out.horizontal += 1;
286 hit = true;
287 }
288 if hit {
289 row_hit = true;
290 cols[x as usize] = true;
291 }
292 }
293 x += 2;
294 }
295 if row_hit {
296 out.rows += 1;
297 }
298 y += 2;
299 }
300 out.cols = cols.iter().filter(|&&c| c).count() as u32;
301 out
302}
303
304#[cfg(test)]
305mod tests {
306 use super::*;
317
318 const W: usize = 400;
319 const H: usize = 600;
320 const BAND: i64 = 64;
321
322 fn noise(x: i64, y: i64, seed: u32) -> u8 {
324 let mut v =
325 (x as u32).wrapping_mul(0x9e37_79b9) ^ (y as u32).wrapping_mul(0x85eb_ca6b) ^ seed;
326 v ^= v >> 15;
327 v = v.wrapping_mul(0x2c1b_3c6d);
328 v ^= v >> 12;
329 (v & 0xff) as u8
330 }
331
332 fn put(frame: &mut [u8], x: usize, y: usize, v: u8) {
333 let i = (y * W + x) * 4;
334 frame[i..i + 4].copy_from_slice(&[v, v.wrapping_mul(3), 255 - v, 255]);
335 }
336
337 fn frame(page: i64, band_top: Option<i64>, page_row: &dyn Fn(i64) -> i64) -> Vec<u8> {
341 let mut out = vec![0u8; W * H * 4];
342 for y in 0..H as i64 {
343 for x in 0..W as i64 {
344 let v = match band_top {
345 Some(top) if y >= top && y < top + BAND => {
346 if y - top == BAND - 1 {
347 30
348 } else {
349 noise(x, y - top, 7)
350 }
351 }
352 _ => noise(x, page_row(y + page), 1),
353 };
354 put(&mut out, x as usize, y as usize, v);
355 }
356 }
357 out
358 }
359
360 fn region() -> Rect {
361 Rect {
362 x: 0.0,
363 y: 0.0,
364 width: W as f64,
365 height: H as f64,
366 }
367 }
368
369 fn plain(y: i64) -> i64 {
370 y
371 }
372
373 fn bands(previous: &[u8], current: &[u8], dy: f64) -> Vec<Rect> {
374 sticky_occlusions(previous, current, W, H, region(), 0.0, dy, &[])
375 }
376
377 fn band_bottom(found: &[Rect]) -> Option<f64> {
378 found.first().map(|b| b.y + b.height)
379 }
380
381 #[test]
382 fn a_still_band_over_a_scrolling_page_is_found() {
383 let previous = frame(0, Some(0), &plain);
384 let current = frame(40, Some(0), &plain);
385 assert_eq!(band_bottom(&bands(&previous, ¤t, 40.0)), Some(64.0));
386 }
387
388 #[test]
389 fn a_band_sliding_at_its_own_small_speed_is_found() {
390 for (k, dy) in [(2, 60.0), (5, 60.0), (12, 60.0), (3, 135.0)] {
391 let previous = frame(0, Some(0), &plain);
392 let current = frame(dy as i64, Some(-k), &plain);
393 assert_eq!(
394 band_bottom(&bands(&previous, ¤t, dy)),
395 Some((BAND - k) as f64),
396 "band sliding {k} px over a page moving {dy} px"
397 );
398 }
399 }
400
401 #[test]
402 fn a_band_sliding_down_while_the_page_scrolls_back_is_found() {
403 let previous = frame(-40, Some(20), &plain);
406 let current = frame(-80, Some(26), &plain);
407 let region = Rect {
408 x: 0.0,
409 y: 20.0,
410 width: W as f64,
411 height: H as f64 - 20.0,
412 };
413 let found = sticky_occlusions(&previous, ¤t, W, H, region, 0.0, -40.0, &[]);
414 assert_eq!(band_bottom(&found), Some(26.0 + BAND as f64));
415 }
416
417 #[test]
418 fn page_content_is_never_a_band() {
419 let previous = frame(0, None, &plain);
420 let current = frame(40, None, &plain);
421 assert!(bands(&previous, ¤t, 40.0).is_empty());
422 let periodic = |y: i64| y.rem_euclid(6);
425 let previous = frame(0, None, &periodic);
426 let current = frame(40, None, &periodic);
427 assert!(bands(&previous, ¤t, 40.0).is_empty());
428 }
429
430 #[test]
431 fn rows_whose_page_partner_is_chrome_above_the_region_take_no_shift() {
432 let periodic = |y: i64| y.rem_euclid(9);
436 let strip = |f: &mut Vec<u8>| {
437 for y in 0..40 {
438 for x in 0..W {
439 put(f, x, y, 30);
440 }
441 }
442 };
443 let mut previous = frame(0, None, &periodic);
444 let mut current = frame(-37, None, &periodic);
445 strip(&mut previous);
446 strip(&mut current);
447 let region = Rect {
448 x: 0.0,
449 y: 40.0,
450 width: W as f64,
451 height: H as f64 - 40.0,
452 };
453 assert!(sticky_occlusions(&previous, ¤t, W, H, region, 0.0, -37.0, &[]).is_empty());
454 }
455
456 #[test]
457 fn content_at_a_large_share_of_the_page_speed_is_not_a_band() {
458 let previous = frame(0, Some(0), &plain);
460 let current = frame(40, Some(-20), &plain);
461 assert!(bands(&previous, ¤t, 40.0).is_empty());
462 let current = frame(40, Some(-11), &plain);
464 assert!(bands(&previous, ¤t, 40.0).is_empty());
465 }
466
467 #[test]
468 fn content_slower_than_a_quarter_of_the_page_is_a_band() {
469 let previous = frame(0, Some(0), &plain);
472 let current = frame(40, Some(-8), &plain);
473 assert_eq!(
474 band_bottom(&bands(&previous, ¤t, 40.0)),
475 Some((BAND - 8) as f64)
476 );
477 }
478
479 #[test]
480 fn a_slow_scroll_keeps_the_shift_away_from_the_page() {
481 let previous = frame(0, Some(0), &plain);
483 let current = frame(10, Some(-2), &plain);
484 assert_eq!(
485 band_bottom(&bands(&previous, ¤t, 10.0)),
486 Some((BAND - 2) as f64)
487 );
488 let current = frame(9, Some(-2), &plain);
490 assert!(bands(&previous, ¤t, 9.0).is_empty());
491 let current = frame(8, Some(-2), &plain);
493 assert!(bands(&previous, ¤t, 8.0).is_empty());
494 }
495
496 #[test]
497 fn the_shift_window_never_reads_above_the_frame() {
498 let previous = frame(0, Some(0), &plain);
501 let current = frame(40, Some(3), &plain);
502 assert_eq!(
503 band_bottom(&bands(&previous, ¤t, 40.0)),
504 Some((BAND + 3) as f64)
505 );
506 }
507}
508
509#[cfg(test)]
510mod structure_tests {
511 use super::*;
512
513 fn frame(w: usize, h: usize, paint: impl Fn(usize, usize) -> u8) -> Vec<u8> {
515 let mut out = vec![255u8; w * h * 4];
516 for y in 0..h {
517 for x in 0..w {
518 let v = paint(x, y);
519 out[(y * w + x) * 4..(y * w + x) * 4 + 3].fill(v);
520 }
521 }
522 out
523 }
524
525 #[test]
526 fn a_blank_area_has_no_structure() {
527 let img = frame(40, 40, |_, _| 0);
528 let s = visible_structure(&img, 40, 40, &[1; 1600], 40, 40, 1.0);
529 assert_eq!(s, Structure::default());
530 }
531
532 #[test]
533 fn a_single_horizontal_line_sits_on_at_most_two_sampled_rows() {
534 let img = frame(40, 40, |_, y| if y == 10 { 80 } else { 0 });
536 let s = visible_structure(&img, 40, 40, &[1; 1600], 40, 40, 1.0);
537 assert!(s.rows <= 2 && s.cols >= 15 && s.horizontal == 0, "{s:?}");
538 }
539
540 #[test]
541 fn a_single_vertical_line_sits_on_at_most_two_sampled_columns() {
542 let img = frame(40, 40, |x, _| if x == 10 { 80 } else { 0 });
543 let s = visible_structure(&img, 40, 40, &[1; 1600], 40, 40, 1.0);
544 assert!(s.cols <= 2 && s.rows >= 15 && s.vertical == 0, "{s:?}");
545 }
546
547 #[test]
548 fn icons_spread_over_the_area_have_structure_on_many_rows_and_columns() {
549 let img = frame(
550 40,
551 40,
552 |x, y| if x % 10 < 4 && y % 10 < 4 { 200 } else { 0 },
553 );
554 let s = visible_structure(&img, 40, 40, &[1; 1600], 40, 40, 1.0);
555 assert!(
556 s.rows >= 3 && s.cols >= 3 && s.vertical > 0 && s.horizontal > 0,
557 "{s:?}"
558 );
559 }
560
561 #[test]
562 fn structure_outside_the_mask_does_not_count() {
563 let img = frame(40, 40, |x, y| {
566 if (10..30).contains(&x) && (10..30).contains(&y) {
567 0
568 } else {
569 ((x * 37 + y * 91) % 256) as u8
570 }
571 });
572 let mut mask = vec![0u8; 1600];
573 for y in 10..30 {
574 mask[y * 40 + 10..y * 40 + 30].fill(1);
575 }
576 let s = visible_structure(&img, 40, 40, &mask, 40, 40, 1.0);
577 assert_eq!(s, Structure::default());
578 }
579
580 #[test]
581 fn native_points_are_read_at_the_factor() {
582 let img = frame(40, 40, |x, y| {
584 if (x / 5) % 3 == 0 && (y / 5) % 3 == 0 {
585 200
586 } else {
587 0
588 }
589 });
590 let s = visible_structure(&img, 40, 40, &[1; 64], 8, 8, 5.0);
591 assert!(s.rows >= 3 && s.cols >= 3, "{s:?}");
592 }
593}