From 0c03b809c8156d79b74f0db95c41c8102d83545c Mon Sep 17 00:00:00 2001 From: Pierre Le Fevre Date: Sat, 18 Jul 2026 18:00:28 +0800 Subject: [PATCH] Improve Opera gradient parity References isu issue 281. Files follow-up isu issue 407 for the remaining Chromium parity gaps. --- .isu/issues.json | 18 ++++++++- crates/e2e/scenarios/real-web/opera.com.we | 8 ++-- crates/render/src/gpu.rs | 45 +++++++++++++++++++--- 3 files changed, 60 insertions(+), 11 deletions(-) diff --git a/.isu/issues.json b/.isu/issues.json index a3da90a..a43dfbb 100644 --- a/.isu/issues.json +++ b/.isu/issues.json @@ -1,5 +1,5 @@ { - "next_id": 407, + "next_id": 408, "issues": [ { "id": 1, @@ -4976,6 +4976,22 @@ "author": "piefev", "state": "open", "created_at": "2026-07-18T09:37:46Z" + }, + { + "id": 407, + "repo": "we", + "title": "Track remaining Opera Chromium parity gaps", + "body": "Parent: isu issue 281\n\nThis pass improved Opera first-paint parity by interpolating CSS gradient colors in premultiplied RGBA and moved the Opera screenshot assertions before the keyboard interactivity step so they compare the same state as the Chromium golden capture. The scenario remains xfailed and issue 281 should stay open.\n\nRepro:\n cargo run -p we-e2e -- --scenario crates/e2e/scenarios/real-web/opera.com.we --out-dir crates/e2e/artifacts\n\nCurrent direct-run results after this pass:\n - desktop screenshot assertion at opera.com.we L95: 84.24% match; 193649/1228500 pixels differ at tolerance 4\n - mobile screenshot assertion at opera.com.we L109: 69.43% match; 100623/329160 pixels differ at tolerance 4\n\nArtifacts:\n - crates/e2e/artifacts/real-web/opera.com/desktop.png\n - crates/e2e/artifacts/real-web/opera.com/desktop.png.diff.png\n - crates/e2e/artifacts/real-web/opera.com/mobile.png\n - crates/e2e/artifacts/real-web/opera.com/mobile.png.diff.png\n - crates/e2e/artifacts/real-web/opera.com/desktop_dom.txt\n - crates/e2e/artifacts/real-web/opera.com/mobile_dom.txt\n - crates/e2e/artifacts/real-web/opera.com/desktop_console.txt\n - crates/e2e/artifacts/real-web/opera.com/mobile_console.txt\n\nObserved remaining blockers:\n - Chromium selects first-viewport WebP assets that we still skip; the Opera WebP sampled locally is VP8X with ALPH and lossy VP8 chunks, while we currently fall back to PNG.\n - Text rasterization and antialiasing still differ enough to affect large hero/cookie-panel text.\n - Cookie-panel shadow/radius/text fidelity remains visibly off against Chromium.\n\nAcceptance:\n Remove the Opera xfail once crates/e2e/scenarios/real-web/opera.com.we passes both opera.com.desktop.chromium.expected.png and opera.com.mobile.chromium.expected.png with the committed strict screenshot threshold.", + "labels": [ + "real-web", + "image", + "text", + "render" + ], + "assigned": [], + "author": "piefev", + "state": "open", + "created_at": "2026-07-18T09:59:47Z" } ] } diff --git a/crates/e2e/scenarios/real-web/opera.com.we b/crates/e2e/scenarios/real-web/opera.com.we index 63aa4c1..9c83156 100644 --- a/crates/e2e/scenarios/real-web/opera.com.we +++ b/crates/e2e/scenarios/real-web/opera.com.we @@ -91,23 +91,23 @@ goto_as https://www.opera.com/ crates/e2e/real-web/snapshots/opera.com/index.htm dump_dom real-web/opera.com/desktop_dom.txt dump_console real-web/opera.com/desktop_console.txt assert_dom_contains "Opera" +screenshot real-web/opera.com/desktop.png +assert_screenshot_matches real-web/opera.com/desktop.png opera.com.desktop.chromium.expected.png # The deferred menu scripts are offline; use the language form's return URL # input as a stable keyboard interaction target. type [id=footer__lang-input-next] interactivity dump_dom real-web/opera.com/desktop_interaction_dom.txt assert_dom_contains https://www.opera.com/interactivity assert_dom_contains survey_container -screenshot real-web/opera.com/desktop.png -assert_screenshot_matches real-web/opera.com/desktop.png opera.com.desktop.chromium.expected.png viewport 390 844 goto_as https://www.opera.com/ crates/e2e/real-web/snapshots/opera.com/index.html dump_dom real-web/opera.com/mobile_dom.txt dump_console real-web/opera.com/mobile_console.txt assert_dom_contains "Opera" +screenshot real-web/opera.com/mobile.png +assert_screenshot_matches real-web/opera.com/mobile.png opera.com.mobile.chromium.expected.png type [id=footer__lang-input-next] interactivity dump_dom real-web/opera.com/mobile_interaction_dom.txt assert_dom_contains https://www.opera.com/interactivity assert_dom_contains survey_container -screenshot real-web/opera.com/mobile.png -assert_screenshot_matches real-web/opera.com/mobile.png opera.com.mobile.chromium.expected.png diff --git a/crates/render/src/gpu.rs b/crates/render/src/gpu.rs index d8f8500..9d6338e 100644 --- a/crates/render/src/gpu.rs +++ b/crates/render/src/gpu.rs @@ -1558,17 +1558,28 @@ fn gradient_boundary_intersection( } fn lerp_color(a: Color, b: Color, t: f32) -> Color { - fn channel(a: u8, b: u8, t: f32) -> u8 { - (a as f32 + (b as f32 - a as f32) * t) + let alpha_a = a.a as f32 / 255.0; + let alpha_b = b.a as f32 / 255.0; + let alpha = alpha_a + (alpha_b - alpha_a) * t; + let out_alpha = (alpha * 255.0).round().clamp(0.0, 255.0) as u8; + + if alpha <= f32::EPSILON { + return Color::new(0, 0, 0, 0); + } + + fn premul_channel(a: u8, alpha_a: f32, b: u8, alpha_b: f32, alpha: f32, t: f32) -> u8 { + let premul_a = a as f32 * alpha_a; + let premul_b = b as f32 * alpha_b; + ((premul_a + (premul_b - premul_a) * t) / alpha) .round() .clamp(0.0, 255.0) as u8 } Color::new( - channel(a.r, b.r, t), - channel(a.g, b.g, t), - channel(a.b, b.b, t), - channel(a.a, b.a, t), + premul_channel(a.r, alpha_a, b.r, alpha_b, alpha, t), + premul_channel(a.g, alpha_a, b.g, alpha_b, alpha, t), + premul_channel(a.b, alpha_a, b.b, alpha_b, alpha, t), + out_alpha, ) } @@ -2053,6 +2064,28 @@ mod tests { assert_eq!(gradient_color_at(&gradient, 1.0), Color::rgb(0, 0, 255)); } + #[test] + fn gradient_color_at_interpolates_transparency_premultiplied() { + let gradient = LinearGradient { + direction: LinearGradientDirection::ToBottom, + stops: vec![ + we_css::values::GradientStop { + color: Color::new(100, 50, 200, 128), + position: Some(0.0), + }, + we_css::values::GradientStop { + color: Color::new(0, 0, 0, 0), + position: Some(1.0), + }, + ], + }; + + assert_eq!( + gradient_color_at(&gradient, 0.5), + Color::new(100, 50, 200, 64) + ); + } + #[test] fn unspecified_gradient_stops_are_evenly_distributed() { let gradient = LinearGradient { -- 2.51.2