diff --git a/experiments/WebScene.NativeEngine.Probe/native/webscene_native_dom.cpp b/experiments/WebScene.NativeEngine.Probe/native/webscene_native_dom.cpp index ef0e82c3..d2e1d131 100644 --- a/experiments/WebScene.NativeEngine.Probe/native/webscene_native_dom.cpp +++ b/experiments/WebScene.NativeEngine.Probe/native/webscene_native_dom.cpp @@ -196,13 +196,16 @@ struct paint_z_index_update final { bool style_establishes_atomic_stacking_context(const dom_node& node) noexcept { + const auto& effects = node.style.textual().effect_values; + const auto filter = effects.find("filter"); return node.style.position == position_mode::fixed || node.style.position == position_mode::sticky || (node.style.position != position_mode::normal && !node.style.z_index_auto) || node.style.contain_stacking_context || node.style.opacity < 0.999F - || node.style.transform_stacking_context; + || node.style.transform_stacking_context + || (filter != effects.end() && filter->second != "none"); } paint_z_index_update update_paint_z_index( 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 98d8c9a8..09d6cffd 100644 --- a/experiments/WebScene.NativeEngine.Probe/native/webscene_native_dom_scene.inc +++ b/experiments/WebScene.NativeEngine.Probe/native/webscene_native_dom_scene.inc @@ -339,6 +339,40 @@ void native_document::append_scene( || opacity <= 0.001F) { return; } + const auto resolve_brightness_filter = [&]() -> 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) { + 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; + } + value.remove_prefix(prefix.size()); + value.remove_suffix(1U); + while (!value.empty() + && std::isspace(static_cast(value.front()))) { + value.remove_prefix(1U); + } + while (!value.empty() + && std::isspace(static_cast(value.back()))) { + value.remove_suffix(1U); + } + const auto parsed = css::parse_ascii_number_prefix(value); + if (!parsed.has_value() || parsed->value < 0) return std::nullopt; + const auto suffix = value.substr(parsed->consumed); + const auto amount = suffix.empty() + ? parsed->value + : suffix == "%" ? parsed->value / 100.0F : -1.0F; + return amount >= 0 && amount <= 10.0F + ? std::optional{amount} : std::nullopt; + }; + const auto brightness_filter = resolve_brightness_filter(); const auto has_opacity_group = opacity < 0.999F; if (has_opacity_group) { commands.push_back(webscene_scene_command{ @@ -354,6 +388,22 @@ void native_document::append_scene( 255L)), node.id}); } + if (brightness_filter.has_value()) { + commands.push_back(webscene_scene_command{ + 30U, + 1U << 31U, + node.layout.x, + node.layout.y, + node.layout.width, + node.layout.height, + 255U, + node.id, + 0, + 0, + 0, + 0, + *brightness_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) { @@ -1914,6 +1964,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 (brightness_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 (has_opacity_group) { commands.push_back(webscene_scene_command{ 31U, 0U, node.layout.x, node.layout.y, @@ -2266,6 +2321,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 (brightness_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 (has_opacity_group) { 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 6417c6ca..c279450e 100644 --- a/experiments/WebScene.NativeEngine.Probe/native/webscene_native_engine.h +++ b/experiments/WebScene.NativeEngine.Probe/native/webscene_native_engine.h @@ -155,6 +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. 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 4e3adf0b..542e35f7 100644 --- a/src/WebScene.Backend.Avalonia/NativeCanvasSceneRenderer.cs +++ b/src/WebScene.Backend.Avalonia/NativeCanvasSceneRenderer.cs @@ -44,6 +44,7 @@ internal sealed unsafe partial class NativeCanvasSceneRenderer private const uint OffscreenCanvasLayer = 1u << 31; private const uint DomPolygonClipResource = 1u << 31; private const uint DomPolygonClipIndexMask = ~DomPolygonClipResource; + private const uint DomBrightnessFilter = 1u << 31; private readonly Dictionary s_layers = new(); private readonly List s_orderedLayers = []; @@ -646,13 +647,8 @@ private static bool ValidateLayer(NativeSceneView* view, in NativeCanvasLayer la switch (command.Kind) { case 30: - opacity.Color = new SKColor( - 255, - 255, - 255, - (byte)(command.Rgba & 0xff)); - backdrop.SaveLayer(opacity); - overlay.SaveLayer(opacity); + SaveDomGroup(backdrop, command, opacity); + SaveDomGroup(overlay, command, opacity); break; case 31: backdrop.Restore(); @@ -835,6 +831,35 @@ private static void ApplyScale(SKCanvas canvas, in SceneCommand command) canvas.Translate(-command.X, -command.Y); } + private static void SaveDomGroup( + SKCanvas canvas, + in SceneCommand command, + SKPaint paint) + { + paint.Color = new SKColor( + 255, + 255, + 255, + (command.Flags & DomBrightnessFilter) != 0 + ? (byte)255 : (byte)(command.Rgba & 0xff)); + if ((command.Flags & DomBrightnessFilter) == 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 + ]); + paint.ColorFilter = filter; + canvas.SaveLayer(paint); + paint.ColorFilter = null; + } + private static void ApplyRotation(SKCanvas canvas, in SceneCommand command) { canvas.Save(); @@ -3786,8 +3811,7 @@ private void RenderOrderedGpuDom(SKCanvas canvas, Action? drawGpuI case 15: ApplyScale(canvas, command); break; case 19: ApplyRotation(canvas, command); break; case 30: - opacity.Color = new SKColor(255, 255, 255, (byte)(command.Rgba & 255)); - canvas.SaveLayer(opacity); break; + SaveDomGroup(canvas, command, opacity); break; case 13: case 16: case 20: case 31: // Never allow an invalid stream to pop the host's state. if (canvas.SaveCount <= save + 1) throw new InvalidOperationException("Unbalanced GPU scene state."); diff --git a/src/WebScene.Backend.Flutter/lib/src/scene_projector.dart b/src/WebScene.Backend.Flutter/lib/src/scene_projector.dart index 30fc0e45..c63015b0 100644 --- a/src/WebScene.Backend.Flutter/lib/src/scene_projector.dart +++ b/src/WebScene.Backend.Flutter/lib/src/scene_projector.dart @@ -19,6 +19,7 @@ const int _layerRemove = 2; const int _canvasEvenOdd = 1 << 16; const int _domPolygonClipResource = 1 << 31; const int _domPolygonClipIndexMask = _domPolygonClipResource - 1; +const int _domBrightnessFilter = 1 << 31; final class SceneApplyResult { const SceneApplyResult({ @@ -234,11 +235,7 @@ final class WebSceneSceneProjector extends ChangeNotifier { final command = scene.commands[index]; switch (command.kind) { case 30: - canvas.saveLayer( - null, - ui.Paint() - ..color = ui.Color.fromARGB(command.rgba & 0xff, 255, 255, 255), - ); + canvas.saveLayer(null, _domGroupPaint(command)); case 31: canvas.restore(); case 15: @@ -961,6 +958,21 @@ final class WebSceneSceneProjector extends ChangeNotifier { } } + static ui.Paint _domGroupPaint(WebSceneSceneCommand command) { + if (command.flags & _domBrightnessFilter == 0) { + return ui.Paint() + ..color = ui.Color.fromARGB(command.rgba & 0xff, 255, 255, 255); + } + final amount = command.strokeWidth.clamp(0.0, double.infinity).toDouble(); + 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, + ]); + } + static String _domString(WebSceneSceneView scene, int index) => _stringAt(scene, index);