diff --git a/.isu/issues.json b/.isu/issues.json index 50a6f4b..cde1347 100644 --- a/.isu/issues.json +++ b/.isu/issues.json @@ -1,5 +1,5 @@ { - "next_id": 386, + "next_id": 387, "issues": [ { "id": 1, @@ -4712,6 +4712,19 @@ "author": "piefev", "state": "open", "created_at": "2026-07-16T11:54:04Z" + }, + { + "id": 386, + "repo": "we", + "title": "Bing parity remains blocked after diagonal gradient geometry fix", + "body": "Parent: isu issue 280. Follow-up from the diagonal linear-gradient geometry fix in crates/render/src/gpu.rs. The fix makes diagonal gradients account for the painted rectangle aspect ratio, but the bing.com real-web xfail still cannot be removed.\n\nRepro:\n cargo run -p we-e2e -- --scenario crates/e2e/scenarios/real-web/bing.com.we --out-dir crates/e2e/artifacts\n\nLatest result after the fix:\n desktop screenshot assertion: 4.85% match; 1168901 / 1228500 pixels differ at tolerance 4\n mobile screenshot assertion: 5.17% match; 312140 / 329160 pixels differ at tolerance 4\n\nSame-cache Chromium comparison from the latest artifacts remains far above the screenshot threshold:\n desktop: 54.60% match; 557798 / 1228500 pixels differ at tolerance 4\n mobile: 49.15% match; 167362 / 329160 pixels differ at tolerance 4\n\nArtifacts:\n crates/e2e/artifacts/real-web/bing.com/desktop.png\n crates/e2e/artifacts/real-web/bing.com/desktop.png.diff.png\n crates/e2e/artifacts/real-web/bing.com/desktop.scenario-cache.chromium.png\n crates/e2e/artifacts/real-web/bing.com/mobile.png\n crates/e2e/artifacts/real-web/bing.com/mobile.png.diff.png\n crates/e2e/artifacts/real-web/bing.com/mobile.scenario-cache.chromium.png\n\nNotes:\n DOM, hydration, and the existing click interactivity assertion pass. The committed Chromium goldens are still stale relative to the fixture (see isu issue 305), but the same-cache Chromium comparison also shows a deterministic renderer gap. Remaining likely candidates include unsupported CSS filter/drop-shadow on the Bing logo, text/shadow/image compositing parity around the search box and carousel, and any residual SVG/image raster differences.\n\nAcceptance:\n After the stale golden issue and the deterministic parity gaps are resolved, remove the xfail from crates/e2e/scenarios/real-web/bing.com.we and pass both screenshot assertions within the default threshold.", + "labels": [ + "real-web" + ], + "assigned": [], + "author": "piefev", + "state": "open", + "created_at": "2026-07-16T12:13:28Z" } ] } diff --git a/crates/render/src/gpu.rs b/crates/render/src/gpu.rs index f5b3c8a..30c0d4c 100644 --- a/crates/render/src/gpu.rs +++ b/crates/render/src/gpu.rs @@ -1107,7 +1107,7 @@ fn push_linear_gradient_quad( if t1 - t0 <= GRADIENT_EPSILON { continue; } - let polygon = clip_gradient_interval(gradient.direction, t0, t1); + let polygon = clip_gradient_interval(gradient.direction, w, h, t0, t1); if polygon.len() < 3 { continue; } @@ -1117,7 +1117,7 @@ fn push_linear_gradient_quad( let p1 = polygon[i]; let p2 = polygon[i + 1]; for p in [p0, p1, p2] { - let t = gradient_t(gradient.direction, p.x, p.y); + let t = gradient_t(gradient.direction, p.x, p.y, w, h); let color = color_to_f32(&gradient_color_at_interval(&stops, t, t0, t1)); out.push(vert(x + p.x * w, y + p.y * h, color)); } @@ -1125,17 +1125,31 @@ fn push_linear_gradient_quad( } } -fn gradient_t(direction: LinearGradientDirection, x: f32, y: f32) -> f32 { +fn gradient_t(direction: LinearGradientDirection, x: f32, y: f32, width: f32, height: f32) -> f32 { match direction { LinearGradientDirection::ToBottom => y, LinearGradientDirection::ToTop => 1.0 - y, LinearGradientDirection::ToRight => x, LinearGradientDirection::ToLeft => 1.0 - x, - LinearGradientDirection::ToBottomRight => (x + y) * 0.5, - LinearGradientDirection::ToBottomLeft => ((1.0 - x) + y) * 0.5, - LinearGradientDirection::ToTopRight => (x + (1.0 - y)) * 0.5, - LinearGradientDirection::ToTopLeft => ((1.0 - x) + (1.0 - y)) * 0.5, + LinearGradientDirection::ToBottomRight => { + diagonal_gradient_t(x, y, width, height, 1.0, 1.0) + } + LinearGradientDirection::ToBottomLeft => { + diagonal_gradient_t(x, y, width, height, -1.0, 1.0) + } + LinearGradientDirection::ToTopRight => diagonal_gradient_t(x, y, width, height, 1.0, -1.0), + LinearGradientDirection::ToTopLeft => diagonal_gradient_t(x, y, width, height, -1.0, -1.0), + } +} + +fn diagonal_gradient_t(x: f32, y: f32, width: f32, height: f32, dir_x: f32, dir_y: f32) -> f32 { + let span = width.abs() * dir_x.abs() + height.abs() * dir_y.abs(); + if span <= f32::EPSILON { + return 0.0; } + + let projection = (x - 0.5) * width * dir_x + (y - 0.5) * height * dir_y; + ((projection + span * 0.5) / span).clamp(0.0, 1.0) } #[cfg(test)] @@ -1251,6 +1265,8 @@ fn has_repeated_gradient_stop_at(stops: &[ResolvedGradientStop], position: f32) fn clip_gradient_interval( direction: LinearGradientDirection, + width: f32, + height: f32, min_t: f32, max_t: f32, ) -> Vec { @@ -1260,13 +1276,15 @@ fn clip_gradient_interval( GradientPoint { x: 1.0, y: 1.0 }, GradientPoint { x: 0.0, y: 1.0 }, ]; - let polygon = clip_gradient_half_plane(polygon, direction, min_t, true); - clip_gradient_half_plane(polygon, direction, max_t, false) + let polygon = clip_gradient_half_plane(polygon, direction, width, height, min_t, true); + clip_gradient_half_plane(polygon, direction, width, height, max_t, false) } fn clip_gradient_half_plane( polygon: Vec, direction: LinearGradientDirection, + width: f32, + height: f32, boundary: f32, keep_greater: bool, ) -> Vec { @@ -1276,10 +1294,10 @@ fn clip_gradient_half_plane( let mut out = Vec::new(); let mut prev = *polygon.last().unwrap(); - let mut prev_t = gradient_t(direction, prev.x, prev.y); + let mut prev_t = gradient_t(direction, prev.x, prev.y, width, height); let mut prev_inside = gradient_inside(prev_t, boundary, keep_greater); for current in polygon { - let current_t = gradient_t(direction, current.x, current.y); + let current_t = gradient_t(direction, current.x, current.y, width, height); let current_inside = gradient_inside(current_t, boundary, keep_greater); if current_inside != prev_inside { @@ -1774,6 +1792,60 @@ mod tests { ); } + #[test] + fn diagonal_gradient_coordinates_use_rect_aspect_ratio() { + let square_t = gradient_t( + LinearGradientDirection::ToBottomRight, + 1.0, + 0.0, + 100.0, + 100.0, + ); + let wide_t = gradient_t( + LinearGradientDirection::ToBottomRight, + 1.0, + 0.0, + 300.0, + 100.0, + ); + let tall_t = gradient_t( + LinearGradientDirection::ToBottomRight, + 1.0, + 0.0, + 100.0, + 300.0, + ); + + assert!((square_t - 0.5).abs() < 0.001); + assert!((wide_t - 0.75).abs() < 0.001); + assert!((tall_t - 0.25).abs() < 0.001); + } + + #[test] + fn diagonal_gradient_interval_clip_uses_rect_aspect_ratio() { + let square = clip_gradient_interval( + LinearGradientDirection::ToBottomRight, + 100.0, + 100.0, + 0.5, + 1.0, + ); + let wide = clip_gradient_interval( + LinearGradientDirection::ToBottomRight, + 300.0, + 100.0, + 0.75, + 1.0, + ); + + assert!(square + .iter() + .any(|p| (p.x - 1.0).abs() < 0.001 && (p.y - 0.0).abs() < 0.001)); + assert!(wide + .iter() + .any(|p| (p.x - 1.0).abs() < 0.001 && (p.y - 0.0).abs() < 0.001)); + } + #[test] fn positioned_gradient_stop_splits_quad() { let gradient = LinearGradient {