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 09d6cffd..7614c049 100644 --- a/experiments/WebScene.NativeEngine.Probe/native/webscene_native_dom_scene.inc +++ b/experiments/WebScene.NativeEngine.Probe/native/webscene_native_dom_scene.inc @@ -339,21 +339,22 @@ void native_document::append_scene( || opacity <= 0.001F) { return; } - const auto resolve_brightness_filter = [&]() -> std::optional { + const auto resolve_single_filter = [&](std::string_view name, float maximum) + -> std::optional { const auto& effects = node.style.textual().effect_values; const auto known = effects.find("filter"); if (known == effects.end()) return std::nullopt; auto value = std::string_view{known->second}; - constexpr std::string_view prefix = "brightness("; - if (value.size() < prefix.size() + 1U || value.back() != ')') return std::nullopt; - for (size_t index = 0; index < prefix.size(); ++index) { + if (value.size() < name.size() + 2U + || value[name.size()] != '(' || value.back() != ')') return std::nullopt; + for (size_t index = 0; index < name.size(); ++index) { const auto character = static_cast(value[index]); const auto lower = character >= 'A' && character <= 'Z' ? static_cast(character - 'A' + 'a') : static_cast(character); - if (lower != prefix[index]) return std::nullopt; + if (lower != name[index]) return std::nullopt; } - value.remove_prefix(prefix.size()); + value.remove_prefix(name.size() + 1U); value.remove_suffix(1U); while (!value.empty() && std::isspace(static_cast(value.front()))) { @@ -369,10 +370,11 @@ void native_document::append_scene( const auto amount = suffix.empty() ? parsed->value : suffix == "%" ? parsed->value / 100.0F : -1.0F; - return amount >= 0 && amount <= 10.0F + return amount >= 0 && amount <= maximum ? std::optional{amount} : std::nullopt; }; - const auto brightness_filter = resolve_brightness_filter(); + const auto brightness_filter = resolve_single_filter("brightness", 10.0F); + const auto grayscale_filter = resolve_single_filter("grayscale", 1.0F); const auto has_opacity_group = opacity < 0.999F; if (has_opacity_group) { commands.push_back(webscene_scene_command{ @@ -404,6 +406,22 @@ void native_document::append_scene( 0, *brightness_filter}); } + if (grayscale_filter.has_value()) { + commands.push_back(webscene_scene_command{ + 30U, + 1U << 30U, + node.layout.x, + node.layout.y, + node.layout.width, + node.layout.height, + 255U, + node.id, + 0, + 0, + 0, + 0, + *grayscale_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) { @@ -1964,6 +1982,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 (grayscale_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 (brightness_filter.has_value()) { commands.push_back(webscene_scene_command{ 31U, 0U, node.layout.x, node.layout.y, @@ -2321,6 +2344,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 (grayscale_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 (brightness_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 c279450e..844e8d79 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 to select a foreground brightness filter; -// stroke_width carries its non-negative multiplier. A zero flag retains the -// opacity-group alpha stored in the low byte of rgba. +// 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. 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 542e35f7..0dcfc243 100644 --- a/src/WebScene.Backend.Avalonia/NativeCanvasSceneRenderer.cs +++ b/src/WebScene.Backend.Avalonia/NativeCanvasSceneRenderer.cs @@ -45,6 +45,8 @@ internal sealed unsafe partial class NativeCanvasSceneRenderer private const uint DomPolygonClipResource = 1u << 31; private const uint DomPolygonClipIndexMask = ~DomPolygonClipResource; private const uint DomBrightnessFilter = 1u << 31; + private const uint DomGrayscaleFilter = 1u << 30; + private const uint DomColorFilterMask = DomBrightnessFilter | DomGrayscaleFilter; private readonly Dictionary s_layers = new(); private readonly List s_orderedLayers = []; @@ -840,21 +842,29 @@ private static void SaveDomGroup( 255, 255, 255, - (command.Flags & DomBrightnessFilter) != 0 + (command.Flags & DomColorFilterMask) != 0 ? (byte)255 : (byte)(command.Rgba & 0xff)); - if ((command.Flags & DomBrightnessFilter) == 0) + if ((command.Flags & DomColorFilterMask) == 0) { paint.ColorFilter = null; canvas.SaveLayer(paint); return; } var amount = Math.Max(0, command.StrokeWidth); - using var filter = SKColorFilter.CreateColorMatrix([ - amount, 0, 0, 0, 0, - 0, amount, 0, 0, 0, - 0, 0, amount, 0, 0, - 0, 0, 0, 1, 0 - ]); + var matrix = (command.Flags & DomBrightnessFilter) != 0 + ? new float[] { + amount, 0, 0, 0, 0, + 0, amount, 0, 0, 0, + 0, 0, amount, 0, 0, + 0, 0, 0, 1, 0 + } + : new float[] { + 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 + }; + using var filter = SKColorFilter.CreateColorMatrix(matrix); paint.ColorFilter = filter; canvas.SaveLayer(paint); paint.ColorFilter = null; diff --git a/src/WebScene.Backend.Flutter/lib/src/scene_projector.dart b/src/WebScene.Backend.Flutter/lib/src/scene_projector.dart index c63015b0..67d78095 100644 --- a/src/WebScene.Backend.Flutter/lib/src/scene_projector.dart +++ b/src/WebScene.Backend.Flutter/lib/src/scene_projector.dart @@ -20,6 +20,8 @@ const int _canvasEvenOdd = 1 << 16; 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; final class SceneApplyResult { const SceneApplyResult({ @@ -959,18 +961,26 @@ final class WebSceneSceneProjector extends ChangeNotifier { } static ui.Paint _domGroupPaint(WebSceneSceneCommand command) { - if (command.flags & _domBrightnessFilter == 0) { + if (command.flags & _domColorFilterMask == 0) { return ui.Paint() ..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, + ]; return ui.Paint() - ..colorFilter = ui.ColorFilter.matrix([ - amount, 0, 0, 0, 0, - 0, amount, 0, 0, 0, - 0, 0, amount, 0, 0, - 0, 0, 0, 1, 0, - ]); + ..colorFilter = ui.ColorFilter.matrix(matrix); } static String _domString(WebSceneSceneView scene, int index) =>