Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -376,6 +376,37 @@ 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 blur_filter = [&]() -> std::optional<float> {
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 auto name = std::string_view{"blur"};
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<unsigned char>(value[index]);
const auto lower = character >= 'A' && character <= 'Z'
? static_cast<char>(character - 'A' + 'a')
: static_cast<char>(character);
if (lower != name[index]) return std::nullopt;
}
value.remove_prefix(name.size() + 1U);
value.remove_suffix(1U);
while (!value.empty()
&& std::isspace(static_cast<unsigned char>(value.front())))
value.remove_prefix(1U);
while (!value.empty()
&& std::isspace(static_cast<unsigned char>(value.back())))
value.remove_suffix(1U);
const auto parsed = css::parse_ascii_number_prefix(value);
if (!parsed.has_value() || parsed->value < 0.0F) return std::nullopt;
const auto suffix = value.substr(parsed->consumed);
if (suffix != "px" && !(suffix.empty() && parsed->value == 0.0F))
return std::nullopt;
return parsed->value <= 256.0F
? std::optional<float>{parsed->value} : std::nullopt;
}();
const auto has_opacity_group = opacity < 0.999F;
if (has_opacity_group) {
commands.push_back(webscene_scene_command{
Expand Down Expand Up @@ -439,6 +470,22 @@ void native_document::append_scene(
0,
*contrast_filter});
}
if (blur_filter.has_value()) {
commands.push_back(webscene_scene_command{
30U,
1U << 28U,
node.layout.x,
node.layout.y,
node.layout.width,
node.layout.height,
255U,
node.id,
0,
0,
0,
0,
*blur_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) {
Expand Down Expand Up @@ -1999,6 +2046,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 (blur_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 (contrast_filter.has_value()) {
commands.push_back(webscene_scene_command{
31U, 0U, node.layout.x, node.layout.y,
Expand Down Expand Up @@ -2366,6 +2418,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 (blur_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 (contrast_filter.has_value()) {
commands.push_back(webscene_scene_command{
31U, 0U, node.layout.x, node.layout.y,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,8 @@ typedef struct webscene_scene_header {
// 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, bit 30 for grayscale, and
// bit 29 for contrast; stroke_width carries the bounded non-negative amount.
// bit 29 for contrast, and bit 28 for foreground blur; stroke_width carries
// the bounded non-negative multiplier or CSS blur standard deviation.
// A zero flag retains the opacity-group alpha stored in the low byte of rgba.
typedef struct webscene_scene_command {
uint32_t kind;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ struct clip_scene_counts final {
uint32_t inset_clip_begins{};
uint32_t inset_clip_ends{};
uint32_t clipped_fills{};
uint32_t blur_filter_begins{};
uint32_t command_count{};
bool transform_clip_nested{};
};
Expand Down Expand Up @@ -105,6 +106,10 @@ clip_scene_counts wait_for_inset_clip_scene(webscene_engine* engine, uint32_t ex
} else if ((command.kind == 1U || command.kind == 9U)
&& command.rgba == 0x285078FFU) {
++latest.clipped_fills;
} else if (command.kind == 30U
&& (command.flags & (1U << 28U)) != 0U
&& std::abs(command.stroke_width - 2.0F) < 0.01F) {
++latest.blur_filter_begins;
}
}
webscene_scene_acknowledge_v3(lease);
Expand Down Expand Up @@ -177,6 +182,7 @@ int main()
clip-path: inset(0px 1px); filter: brightness(0.5); backdrop-filter: blur(1px); }
#effects.alternate > span { clip-path: circle(25%); filter: contrast(2); }
#effects > span:first-child { transform: scale(1.25) rotate(3deg); }
#effects > span:last-child { filter: blur(2px); }
`;
document.head.appendChild(rules);
const host = document.createElement('main');
Expand All @@ -193,6 +199,9 @@ int main()
|| first.offsetWidth !== 8 || first.offsetHeight !== 2) {
throw new Error('initial effect values failed');
}
if (getComputedStyle(host.lastElementChild).getPropertyValue('filter') !== 'blur(2px)') {
throw new Error('initial blur filter value failed');
}
})()
)JS", "native-effects-fixture.js");

Expand Down Expand Up @@ -259,6 +268,8 @@ int main()
"retained scene did not preserve all 4096 clipped fills");
require(initial_clip_scene.transform_clip_nested,
"transform commands did not wrap the inset clip scope");
require(initial_clip_scene.blur_filter_begins == 1U,
"retained scene did not emit the bounded foreground blur group");
const auto initial_scene_command_bytes =
static_cast<uint64_t>(initial_clip_scene.command_count)
* sizeof(webscene_scene_command);
Expand Down Expand Up @@ -370,6 +381,7 @@ int main()
<< " clip-begins=" << initial_clip_scene.inset_clip_begins
<< " clip-ends=" << initial_clip_scene.inset_clip_ends
<< " clipped-fills=" << initial_clip_scene.clipped_fills
<< " blur-filter-begins=" << initial_clip_scene.blur_filter_begins
<< " transform-clip-nested=" << initial_clip_scene.transform_clip_nested
<< " initial-scene-command-bytes=" << initial_scene_command_bytes
<< " peak-textual-style-count-delta=" << peak_textual_style_count_delta
Expand Down
17 changes: 14 additions & 3 deletions src/WebScene.Backend.Avalonia/NativeCanvasSceneRenderer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,10 @@ internal sealed unsafe partial class NativeCanvasSceneRenderer
private const uint DomBrightnessFilter = 1u << 31;
private const uint DomGrayscaleFilter = 1u << 30;
private const uint DomContrastFilter = 1u << 29;
private const uint DomBlurFilter = 1u << 28;
private const uint DomColorFilterMask =
DomBrightnessFilter | DomGrayscaleFilter | DomContrastFilter;
private const uint DomEffectFilterMask = DomColorFilterMask | DomBlurFilter;

private readonly Dictionary<uint, RetainedLayer> s_layers = new();
private readonly List<RetainedLayer> s_orderedLayers = [];
Expand Down Expand Up @@ -840,19 +842,28 @@ private static void SaveDomGroup(
in SceneCommand command,
SKPaint paint)
{
paint.ColorFilter = null;
paint.ImageFilter = null;
paint.Color = new SKColor(
255,
255,
255,
(command.Flags & DomColorFilterMask) != 0
(command.Flags & DomEffectFilterMask) != 0
? (byte)255 : (byte)(command.Rgba & 0xff));
if ((command.Flags & DomColorFilterMask) == 0)
if ((command.Flags & DomEffectFilterMask) == 0)
{
paint.ColorFilter = null;
canvas.SaveLayer(paint);
return;
}
var amount = Math.Max(0, command.StrokeWidth);
if ((command.Flags & DomBlurFilter) != 0)
{
using var blur = SKImageFilter.CreateBlur(amount, amount);
paint.ImageFilter = blur;
canvas.SaveLayer(paint);
paint.ImageFilter = null;
return;
}
float[] matrix;
if ((command.Flags & DomBrightnessFilter) != 0)
{
Expand Down
8 changes: 7 additions & 1 deletion src/WebScene.Backend.Flutter/lib/src/scene_projector.dart
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,10 @@ const int _domPolygonClipIndexMask = _domPolygonClipResource - 1;
const int _domBrightnessFilter = 1 << 31;
const int _domGrayscaleFilter = 1 << 30;
const int _domContrastFilter = 1 << 29;
const int _domBlurFilter = 1 << 28;
const int _domColorFilterMask =
_domBrightnessFilter | _domGrayscaleFilter | _domContrastFilter;
const int _domEffectFilterMask = _domColorFilterMask | _domBlurFilter;

final class SceneApplyResult {
const SceneApplyResult({
Expand Down Expand Up @@ -963,11 +965,15 @@ final class WebSceneSceneProjector extends ChangeNotifier {
}

static ui.Paint _domGroupPaint(WebSceneSceneCommand command) {
if (command.flags & _domColorFilterMask == 0) {
if (command.flags & _domEffectFilterMask == 0) {
return ui.Paint()
..color = ui.Color.fromARGB(command.rgba & 0xff, 255, 255, 255);
}
final amount = command.strokeWidth.clamp(0.0, double.infinity).toDouble();
if (command.flags & _domBlurFilter != 0) {
return ui.Paint()
..imageFilter = ui.ImageFilter.blur(sigmaX: amount, sigmaY: amount);
}
final List<double> matrix;
if (command.flags & _domBrightnessFilter != 0) {
matrix = <double>[
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@
effect.style.setProperty('filter', 'blur(2px)');
effect.style.setProperty('clip-path', 'inset(2px)');
const after = effect.getBoundingClientRect();
assert_equals(getComputedStyle(effect).getPropertyValue('filter'), 'blur(2px)');
assert_equals(after.width, before.width);
assert_equals(after.height, before.height);
}, 'effect syntax does not change principal box geometry');
Expand Down
Loading