From 6ebd4006950359b36f42bf23c9016070a0a63ea6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wies=C5=82aw=20=C5=A0olt=C3=A9s?= Date: Fri, 18 Sep 2026 20:09:36 +0200 Subject: [PATCH] Render foreground contrast filters --- .../native/webscene_native_dom_scene.inc | 27 ++++++++++++ .../native/webscene_native_engine.h | 6 +-- .../NativeCanvasSceneRenderer.cs | 30 +++++++++++--- .../lib/src/scene_projector.dart | 41 ++++++++++++------- 4 files changed, 81 insertions(+), 23 deletions(-) diff --git a/experiments/WebScene.NativeEngine.Probe/native/webscene_native_dom_scene.inc b/experiments/WebScene.NativeEngine.Probe/native/webscene_native_dom_scene.inc index 7614c049..9c19bc65 100644 --- a/experiments/WebScene.NativeEngine.Probe/native/webscene_native_dom_scene.inc +++ b/experiments/WebScene.NativeEngine.Probe/native/webscene_native_dom_scene.inc @@ -375,6 +375,7 @@ void native_document::append_scene( }; const auto brightness_filter = resolve_single_filter("brightness", 10.0F); const auto grayscale_filter = resolve_single_filter("grayscale", 1.0F); + const auto contrast_filter = resolve_single_filter("contrast", 10.0F); const auto has_opacity_group = opacity < 0.999F; if (has_opacity_group) { commands.push_back(webscene_scene_command{ @@ -422,6 +423,22 @@ void native_document::append_scene( 0, *grayscale_filter}); } + if (contrast_filter.has_value()) { + commands.push_back(webscene_scene_command{ + 30U, + 1U << 29U, + node.layout.x, + node.layout.y, + node.layout.width, + node.layout.height, + 255U, + node.id, + 0, + 0, + 0, + 0, + *contrast_filter}); + } const auto resolve_transform_origin = [](css_length value, float available, float fallback) { if (value.unit == length_unit::pixels) return value.value; if (value.unit == length_unit::percent) { @@ -1982,6 +1999,11 @@ void native_document::append_scene( if (has_scale_transform) { commands.push_back(webscene_scene_command{16U, 0U, 0, 0, 0, 0, 0U, node.id}); } + if (contrast_filter.has_value()) { + commands.push_back(webscene_scene_command{ + 31U, 0U, node.layout.x, node.layout.y, + node.layout.width, node.layout.height, 0U, node.id}); + } if (grayscale_filter.has_value()) { commands.push_back(webscene_scene_command{ 31U, 0U, node.layout.x, node.layout.y, @@ -2344,6 +2366,11 @@ void native_document::append_scene( if (has_scale_transform) { commands.push_back(webscene_scene_command{16U, 0U, 0, 0, 0, 0, 0U, node.id}); } + if (contrast_filter.has_value()) { + commands.push_back(webscene_scene_command{ + 31U, 0U, node.layout.x, node.layout.y, + node.layout.width, node.layout.height, 0U, node.id}); + } if (grayscale_filter.has_value()) { commands.push_back(webscene_scene_command{ 31U, 0U, node.layout.x, node.layout.y, diff --git a/experiments/WebScene.NativeEngine.Probe/native/webscene_native_engine.h b/experiments/WebScene.NativeEngine.Probe/native/webscene_native_engine.h index 844e8d79..a12319f8 100644 --- a/experiments/WebScene.NativeEngine.Probe/native/webscene_native_engine.h +++ b/experiments/WebScene.NativeEngine.Probe/native/webscene_native_engine.h @@ -155,9 +155,9 @@ typedef struct webscene_scene_header { // Clip kind 12 uses flags bit 31 to select an SVG path stored in the indexed // scene string; the remaining bits are its string index. A zero flag retains // the rounded-rectangle fields used by existing producers and presenters. -// Group kind 30 uses flags bit 31 for brightness and bit 30 for grayscale; -// stroke_width carries the bounded non-negative amount. A zero flag retains -// the opacity-group alpha stored in the low byte of rgba. +// Group kind 30 uses flags bit 31 for brightness, bit 30 for grayscale, and +// bit 29 for contrast; stroke_width carries the bounded non-negative amount. +// A zero flag retains the opacity-group alpha stored in the low byte of rgba. typedef struct webscene_scene_command { uint32_t kind; uint32_t flags; diff --git a/src/WebScene.Backend.Avalonia/NativeCanvasSceneRenderer.cs b/src/WebScene.Backend.Avalonia/NativeCanvasSceneRenderer.cs index 0dcfc243..6e3e09d1 100644 --- a/src/WebScene.Backend.Avalonia/NativeCanvasSceneRenderer.cs +++ b/src/WebScene.Backend.Avalonia/NativeCanvasSceneRenderer.cs @@ -46,7 +46,9 @@ internal sealed unsafe partial class NativeCanvasSceneRenderer private const uint DomPolygonClipIndexMask = ~DomPolygonClipResource; private const uint DomBrightnessFilter = 1u << 31; private const uint DomGrayscaleFilter = 1u << 30; - private const uint DomColorFilterMask = DomBrightnessFilter | DomGrayscaleFilter; + private const uint DomContrastFilter = 1u << 29; + private const uint DomColorFilterMask = + DomBrightnessFilter | DomGrayscaleFilter | DomContrastFilter; private readonly Dictionary s_layers = new(); private readonly List s_orderedLayers = []; @@ -851,19 +853,35 @@ private static void SaveDomGroup( return; } var amount = Math.Max(0, command.StrokeWidth); - var matrix = (command.Flags & DomBrightnessFilter) != 0 - ? new float[] { + float[] matrix; + if ((command.Flags & DomBrightnessFilter) != 0) + { + matrix = [ amount, 0, 0, 0, 0, 0, amount, 0, 0, 0, 0, 0, amount, 0, 0, 0, 0, 0, 1, 0 - } - : new float[] { + ]; + } + else if ((command.Flags & DomGrayscaleFilter) != 0) + { + matrix = [ 1 - 0.7874f * amount, 0.7152f * amount, 0.0722f * amount, 0, 0, 0.2126f * amount, 1 - 0.2848f * amount, 0.0722f * amount, 0, 0, 0.2126f * amount, 0.7152f * amount, 1 - 0.9278f * amount, 0, 0, 0, 0, 0, 1, 0 - }; + ]; + } + else + { + var intercept = 127.5f * (1 - amount); + matrix = [ + amount, 0, 0, 0, intercept, + 0, amount, 0, 0, intercept, + 0, 0, amount, 0, intercept, + 0, 0, 0, 1, 0 + ]; + } using var filter = SKColorFilter.CreateColorMatrix(matrix); paint.ColorFilter = filter; canvas.SaveLayer(paint); diff --git a/src/WebScene.Backend.Flutter/lib/src/scene_projector.dart b/src/WebScene.Backend.Flutter/lib/src/scene_projector.dart index 67d78095..d2c80784 100644 --- a/src/WebScene.Backend.Flutter/lib/src/scene_projector.dart +++ b/src/WebScene.Backend.Flutter/lib/src/scene_projector.dart @@ -21,7 +21,9 @@ const int _domPolygonClipResource = 1 << 31; const int _domPolygonClipIndexMask = _domPolygonClipResource - 1; const int _domBrightnessFilter = 1 << 31; const int _domGrayscaleFilter = 1 << 30; -const int _domColorFilterMask = _domBrightnessFilter | _domGrayscaleFilter; +const int _domContrastFilter = 1 << 29; +const int _domColorFilterMask = + _domBrightnessFilter | _domGrayscaleFilter | _domContrastFilter; final class SceneApplyResult { const SceneApplyResult({ @@ -966,19 +968,30 @@ final class WebSceneSceneProjector extends ChangeNotifier { ..color = ui.Color.fromARGB(command.rgba & 0xff, 255, 255, 255); } final amount = command.strokeWidth.clamp(0.0, double.infinity).toDouble(); - final matrix = command.flags & _domBrightnessFilter != 0 - ? [ - amount, 0, 0, 0, 0, - 0, amount, 0, 0, 0, - 0, 0, amount, 0, 0, - 0, 0, 0, 1, 0, - ] - : [ - 1 - 0.7874 * amount, 0.7152 * amount, 0.0722 * amount, 0, 0, - 0.2126 * amount, 1 - 0.2848 * amount, 0.0722 * amount, 0, 0, - 0.2126 * amount, 0.7152 * amount, 1 - 0.9278 * amount, 0, 0, - 0, 0, 0, 1, 0, - ]; + final List matrix; + if (command.flags & _domBrightnessFilter != 0) { + matrix = [ + amount, 0, 0, 0, 0, + 0, amount, 0, 0, 0, + 0, 0, amount, 0, 0, + 0, 0, 0, 1, 0, + ]; + } else if (command.flags & _domGrayscaleFilter != 0) { + matrix = [ + 1 - 0.7874 * amount, 0.7152 * amount, 0.0722 * amount, 0, 0, + 0.2126 * amount, 1 - 0.2848 * amount, 0.0722 * amount, 0, 0, + 0.2126 * amount, 0.7152 * amount, 1 - 0.9278 * amount, 0, 0, + 0, 0, 0, 1, 0, + ]; + } else { + final intercept = 127.5 * (1 - amount); + matrix = [ + amount, 0, 0, 0, intercept, + 0, amount, 0, 0, intercept, + 0, 0, amount, 0, intercept, + 0, 0, 0, 1, 0, + ]; + } return ui.Paint() ..colorFilter = ui.ColorFilter.matrix(matrix); }