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 @@ -467,6 +467,69 @@ void native_document::append_scene(
}
return result;
}();
const auto linear_mask_resource = [&]() -> std::optional<uint32_t> {
const auto& effects = node.style.textual().effect_values;
const auto known = effects.find("mask-image");
if (known == effects.end()) return std::nullopt;
auto image = std::string_view{known->second};
while (!image.empty()
&& std::isspace(static_cast<unsigned char>(image.front())))
image.remove_prefix(1U);
while (!image.empty()
&& std::isspace(static_cast<unsigned char>(image.back())))
image.remove_suffix(1U);
constexpr auto prefix = std::string_view{"linear-gradient("};
if (image.size() <= prefix.size()) return std::nullopt;
for (size_t index = 0U; index < prefix.size(); ++index) {
const auto character = static_cast<unsigned char>(image[index]);
const auto lower = character >= 'A' && character <= 'Z'
? static_cast<char>(character - 'A' + 'a')
: static_cast<char>(character);
if (lower != prefix[index]) return std::nullopt;
}
auto depth = 1U;
auto close = std::string_view::npos;
for (size_t index = prefix.size(); index < image.size(); ++index) {
if (image[index] == '(') ++depth;
else if (image[index] == ')' && --depth == 0U) {
close = index;
break;
}
}
if (close != image.size() - 1U) return std::nullopt;
const auto value = [&](std::string_view name, std::string_view fallback) {
const auto found = effects.find(std::string{name});
return found == effects.end() ? std::string{fallback} : found->second;
};
const auto composite = value("mask-composite", "add");
if (composite != "add") return std::nullopt;
const auto mode = value("mask-mode", "match-source");
if (mode != "match-source" && mode != "alpha") return std::nullopt;
auto repeat = value("mask-repeat", "repeat");
for (auto& character : repeat) {
const auto byte = static_cast<unsigned char>(character);
if (byte >= 'A' && byte <= 'Z') character = static_cast<char>(byte - 'A' + 'a');
}
if (repeat != "repeat" && repeat != "no-repeat"
&& repeat != "repeat-x" && repeat != "repeat-y") {
return std::nullopt;
}
const auto position = value("mask-position", "0% 0%");
const auto size = value("mask-size", "auto");
if (position.find(',') != std::string::npos
|| size.find(',') != std::string::npos) {
return std::nullopt;
}
auto resource = std::string{"webscene-bg-v2\t"};
resource.append(image);
resource += '\t';
resource += repeat;
resource += '\t';
resource += position;
resource += '\t';
resource += size;
return append_scene_string(resource, strings, string_bytes);
}();
const auto has_opacity_group = opacity < 0.999F;
if (has_opacity_group) {
commands.push_back(webscene_scene_command{
Expand All @@ -482,6 +545,17 @@ void native_document::append_scene(
255L)),
node.id});
}
if (linear_mask_resource.has_value()) {
commands.push_back(webscene_scene_command{
30U,
1U << 26U,
node.layout.x,
node.layout.y,
node.layout.width,
node.layout.height,
255U,
node.id});
}
// SaveLayer effects apply when their groups restore. Open in reverse CSS
// order so restoration applies the authored function list left-to-right.
for (auto filter = foreground_filters.rbegin();
Expand Down Expand Up @@ -2142,6 +2216,16 @@ void native_document::append_scene(
31U, 0U, node.layout.x, node.layout.y,
node.layout.width, node.layout.height, 0U, node.id});
}
if (linear_mask_resource.has_value()) {
commands.push_back(webscene_scene_command{
47U, *linear_mask_resource,
node.layout.x, node.layout.y,
node.layout.width, node.layout.height,
0U, node.id});
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,
Expand Down Expand Up @@ -2500,6 +2584,16 @@ void native_document::append_scene(
31U, 0U, node.layout.x, node.layout.y,
node.layout.width, node.layout.height, 0U, node.id});
}
if (linear_mask_resource.has_value()) {
commands.push_back(webscene_scene_command{
47U, *linear_mask_resource,
node.layout.x, node.layout.y,
node.layout.width, node.layout.height,
0U, node.id});
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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -157,9 +157,12 @@ typedef struct webscene_scene_header {
// the rounded-rectangle fields used by existing producers and presenters.
// Group kind 30 uses flags bit 31 for brightness, bit 30 for grayscale,
// bit 29 for contrast, bit 28 for foreground blur, and bit 27 for saturation;
// bit 26 opens a neutral isolated layer for a following kind-47 linear mask.
// 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.
// DOM kind 47 applies the indexed webscene-bg-v2 linear-gradient resource to
// the current isolated layer using destination-in before kind 31 restores it.
typedef struct webscene_scene_command {
uint32_t kind;
uint32_t flags;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ void store_maximum(std::atomic<uint64_t>& target, uint64_t value)

bool command_uses_dom_string(const webscene_scene_command& command)
{
return command.kind >= 3U && command.kind <= 6U;
return (command.kind >= 3U && command.kind <= 6U) || command.kind == 47U;
}

std::string_view command_dom_string(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ struct clip_scene_counts final {
uint32_t clipped_fills{};
uint32_t blur_filter_begins{};
uint32_t functional_blur_begins{};
uint32_t linear_mask_commands{};
uint32_t command_count{};
bool transform_clip_nested{};
bool compound_filter_ordered{};
Expand Down Expand Up @@ -151,6 +152,8 @@ clip_scene_counts wait_for_inset_clip_scene(
&& (command.flags & (1U << 28U)) != 0U
&& std::abs(command.stroke_width - 4.0F) < 0.01F) {
++latest.functional_blur_begins;
} else if (command.kind == 47U) {
++latest.linear_mask_commands;
}
}
webscene_scene_acknowledge_v3(lease);
Expand Down Expand Up @@ -382,6 +385,8 @@ int main()
"retained scene did not preserve compound foreground filter order");
require(initial_clip_scene.functional_blur_begins == 1U,
"retained scene did not resolve the functional blur radius");
require(initial_clip_scene.linear_mask_commands == 4096U,
"retained scene did not emit all linear-gradient mask commands");
const auto initial_scene_command_bytes =
static_cast<uint64_t>(initial_clip_scene.command_count)
* sizeof(webscene_scene_command);
Expand Down Expand Up @@ -514,6 +519,7 @@ int main()
<< " clipped-fills=" << initial_clip_scene.clipped_fills
<< " blur-filter-begins=" << initial_clip_scene.blur_filter_begins
<< " functional-blur-begins=" << initial_clip_scene.functional_blur_begins
<< " linear-mask-commands=" << initial_clip_scene.linear_mask_commands
<< " compound-filter-ordered=" << initial_clip_scene.compound_filter_ordered
<< " transform-clip-nested=" << initial_clip_scene.transform_clip_nested
<< " initial-scene-command-bytes=" << initial_scene_command_bytes
Expand Down
116 changes: 110 additions & 6 deletions src/WebScene.Backend.Avalonia/NativeCanvasSceneRenderer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,11 @@ internal sealed unsafe partial class NativeCanvasSceneRenderer
private const uint DomContrastFilter = 1u << 29;
private const uint DomBlurFilter = 1u << 28;
private const uint DomSaturateFilter = 1u << 27;
private const uint DomLinearMaskGroup = 1u << 26;
private const uint DomColorFilterMask =
DomBrightnessFilter | DomGrayscaleFilter | DomContrastFilter | DomSaturateFilter;
private const uint DomEffectFilterMask = DomColorFilterMask | DomBlurFilter;
private const uint DomEffectFilterMask =
DomColorFilterMask | DomBlurFilter | DomLinearMaskGroup;

private readonly Dictionary<uint, RetainedLayer> s_layers = new();
private readonly List<RetainedLayer> s_orderedLayers = [];
Expand Down Expand Up @@ -661,6 +663,10 @@ private static bool ValidateLayer(NativeSceneView* view, in NativeCanvasLayer la
backdrop.Restore();
overlay.Restore();
break;
case 47:
DrawDomLinearMask(backdrop, view, command);
DrawDomLinearMask(overlay, view, command);
break;
case 15:
ApplyScale(backdrop, command);
ApplyScale(overlay, command);
Expand Down Expand Up @@ -856,6 +862,11 @@ private static void SaveDomGroup(
canvas.SaveLayer(paint);
return;
}
if ((command.Flags & DomLinearMaskGroup) != 0)
{
canvas.SaveLayer(paint);
return;
}
var amount = Math.Max(0, command.StrokeWidth);
if ((command.Flags & DomBlurFilter) != 0)
{
Expand Down Expand Up @@ -1207,6 +1218,19 @@ private static void DrawDomLinearGradient(
radii);
}

private static void DrawDomLinearMask(
SKCanvas canvas,
NativeSceneView* view,
in SceneCommand command)
{
DrawDomBackgroundLayers(
canvas,
DomStringAt(view, command.Flags),
command,
default,
SKBlendMode.DstIn);
}

internal static void DrawDomBackgroundForTest(
SKCanvas canvas,
string resource,
Expand All @@ -1217,7 +1241,8 @@ private static void DrawDomBackgroundLayers(
SKCanvas canvas,
string value,
in SceneCommand command,
in DomCornerRadii radii)
in DomCornerRadii radii,
SKBlendMode blendMode = SKBlendMode.SrcOver)
{
var resource = DecodeDomBackgroundResource(value);
var layers = SplitTopLevel(resource.Image, ',');
Expand Down Expand Up @@ -1246,7 +1271,8 @@ private static void DrawDomBackgroundLayers(
var repeat = LayerValue(repeats, layerIndex, "repeat");
var position = LayerValue(positions, layerIndex, "0% 0%");
var size = LayerValue(sizes, layerIndex, "auto");
DrawDomGradientTiles(canvas, layer, command, repeat, position, size);
DrawDomGradientTiles(
canvas, layer, command, repeat, position, size, blendMode);
}
}
finally
Expand Down Expand Up @@ -1307,11 +1333,20 @@ private static void DrawDomGradientTiles(
in SceneCommand command,
string repeatValue,
string positionValue,
string sizeValue)
string sizeValue,
SKBlendMode blendMode = SKBlendMode.SrcOver)
{
ResolveDomBackgroundSize(sizeValue, command.Width, command.Height,
out var tileWidth, out var tileHeight);
if (tileWidth <= 0 || tileHeight <= 0) return;
if (tileWidth <= 0 || tileHeight <= 0)
{
if (blendMode == SKBlendMode.DstIn)
{
ClearDomMaskRect(canvas, command.X, command.Y,
command.Width, command.Height);
}
return;
}
ResolveDomBackgroundPosition(positionValue, command.Width, command.Height,
tileWidth, tileHeight, out var offsetX, out var offsetY);

Expand All @@ -1335,6 +1370,12 @@ private static void DrawDomGradientTiles(

var endX = repeatX ? command.X + command.Width : firstX + tileWidth;
var endY = repeatY ? command.Y + command.Height : firstY + tileHeight;
if (blendMode == SKBlendMode.DstIn)
{
ClearDomMaskOutsideCoverage(canvas, command,
repeatX, repeatY, firstX, firstY, tileWidth, tileHeight);
}
var drewTile = false;
for (var y = firstY; y < endY; y += tileHeight)
{
for (var x = firstX; x < endX; x += tileWidth)
Expand All @@ -1360,13 +1401,64 @@ private static void DrawDomGradientTiles(
{
IsAntialias = false,
Style = SKPaintStyle.Fill,
Shader = shader
Shader = shader,
BlendMode = blendMode
};
canvas.DrawRect(x, y, tileWidth, tileHeight, paint);
drewTile = true;
if (!repeatX) break;
}
if (!repeatY) break;
}
if (blendMode == SKBlendMode.DstIn && !drewTile)
{
ClearDomMaskRect(canvas, command.X, command.Y,
command.Width, command.Height);
}
}

private static void ClearDomMaskOutsideCoverage(
SKCanvas canvas,
in SceneCommand command,
bool repeatX,
bool repeatY,
float firstX,
float firstY,
float tileWidth,
float tileHeight)
{
var left = command.X;
var top = command.Y;
var right = command.X + command.Width;
var bottom = command.Y + command.Height;
var coverageLeft = repeatX ? left : Math.Clamp(firstX, left, right);
var coverageRight = repeatX ? right : Math.Clamp(firstX + tileWidth, left, right);
var coverageTop = repeatY ? top : Math.Clamp(firstY, top, bottom);
var coverageBottom = repeatY ? bottom : Math.Clamp(firstY + tileHeight, top, bottom);
if (coverageRight <= coverageLeft || coverageBottom <= coverageTop)
{
ClearDomMaskRect(canvas, left, top, command.Width, command.Height);
return;
}
ClearDomMaskRect(canvas, left, top, command.Width, coverageTop - top);
ClearDomMaskRect(canvas, left, coverageBottom,
command.Width, bottom - coverageBottom);
ClearDomMaskRect(canvas, left, coverageTop,
coverageLeft - left, coverageBottom - coverageTop);
ClearDomMaskRect(canvas, coverageRight, coverageTop,
right - coverageRight, coverageBottom - coverageTop);
}

private static void ClearDomMaskRect(
SKCanvas canvas,
float x,
float y,
float width,
float height)
{
if (width <= 0 || height <= 0) return;
using var clear = new SKPaint { BlendMode = SKBlendMode.Clear };
canvas.DrawRect(x, y, width, height, clear);
}

private static void ExpandPremultipliedGradientStops(
Expand Down Expand Up @@ -1494,6 +1586,18 @@ private static float ResolveDomBackgroundLength(
{
var normalized = value.Trim().ToLowerInvariant();
if (normalized == "auto") return fallback;
var calc = System.Text.RegularExpressions.Regex.Match(
normalized,
@"^calc\(\s*([-+]?(?:\d+(?:\.\d*)?|\.\d+))%\s*([-+])\s*([-+]?(?:\d+(?:\.\d*)?|\.\d+))px\s*\)$");
if (calc.Success
&& float.TryParse(calc.Groups[1].Value, NumberStyles.Float,
CultureInfo.InvariantCulture, out var calcPercent)
&& float.TryParse(calc.Groups[3].Value, NumberStyles.Float,
CultureInfo.InvariantCulture, out var calcPixels))
{
var sign = calc.Groups[2].Value == "-" ? -1f : 1f;
return percentageBasis * calcPercent / 100f + sign * calcPixels;
}
if (normalized.EndsWith('%')
&& float.TryParse(normalized[..^1], NumberStyles.Float,
CultureInfo.InvariantCulture, out var percentage))
Expand Down
Loading
Loading