Browse Source

LibWeb: Implement SVG opacity properties

This implements the stop-opacity, fill-opacity, and stroke-opacity
properties (in CSS). This replaces the existing more ad-hoc
fill-opacity attribute handling.
MacDue 2 years ago
parent
commit
00cda96e2d

+ 12 - 0
Userland/Libraries/LibWeb/CSS/ComputedValues.h

@@ -62,6 +62,9 @@ public:
     static float flex_shrink() { return 1.0f; }
     static int order() { return 0; }
     static float opacity() { return 1.0f; }
+    static float fill_opacity() { return 1.0f; }
+    static float stroke_opacity() { return 1.0f; }
+    static float stop_opacity() { return 1.0f; }
     static CSS::Length border_radius() { return Length::make_px(0); }
     static Variant<CSS::VerticalAlign, CSS::LengthPercentage> vertical_align() { return CSS::VerticalAlign::Baseline; }
     static CSS::LengthBox inset() { return { CSS::Length::make_auto(), CSS::Length::make_auto(), CSS::Length::make_auto(), CSS::Length::make_auto() }; }
@@ -284,8 +287,11 @@ public:
 
     Optional<SVGPaint> const& fill() const { return m_inherited.fill; }
     Optional<SVGPaint> const& stroke() const { return m_inherited.stroke; }
+    float fill_opacity() const { return m_inherited.fill_opacity; }
+    float stroke_opacity() const { return m_inherited.stroke_opacity; }
     Optional<LengthPercentage> const& stroke_width() const { return m_inherited.stroke_width; }
     Color stop_color() const { return m_noninherited.stop_color; }
+    float stop_opacity() const { return m_noninherited.stop_opacity; }
 
     Vector<CSS::Transformation> const& transformations() const { return m_noninherited.transformations; }
     CSS::TransformOrigin const& transform_origin() const { return m_noninherited.transform_origin; }
@@ -321,6 +327,8 @@ protected:
 
         Optional<SVGPaint> fill;
         Optional<SVGPaint> stroke;
+        float fill_opacity { InitialValues::fill_opacity() };
+        float stroke_opacity { InitialValues::stroke_opacity() };
         Optional<LengthPercentage> stroke_width;
     } m_inherited;
 
@@ -388,6 +396,7 @@ protected:
         CSS::BorderCollapse border_collapse { InitialValues::border_collapse() };
         Vector<Vector<String>> grid_template_areas { InitialValues::grid_template_areas() };
         Gfx::Color stop_color { InitialValues::stop_color() };
+        float stop_opacity { InitialValues::stop_opacity() };
     } m_noninherited;
 };
 
@@ -475,8 +484,11 @@ public:
 
     void set_fill(SVGPaint value) { m_inherited.fill = value; }
     void set_stroke(SVGPaint value) { m_inherited.stroke = value; }
+    void set_fill_opacity(float value) { m_inherited.fill_opacity = value; }
+    void set_stroke_opacity(float value) { m_inherited.stroke_opacity = value; }
     void set_stroke_width(LengthPercentage value) { m_inherited.stroke_width = value; }
     void set_stop_color(Color value) { m_noninherited.stop_color = value; }
+    void set_stop_opacity(float value) { m_noninherited.stop_opacity = value; }
 };
 
 }

+ 27 - 0
Userland/Libraries/LibWeb/CSS/Properties.json

@@ -629,6 +629,15 @@
       "none"
     ]
   },
+  "fill-opacity": {
+    "affects-layout": false,
+    "inherited": true,
+    "initial": "1",
+    "valid-types": [
+      "number",
+      "percentage"
+    ]
+  },
   "flex": {
     "inherited": false,
     "initial": "0 1 auto",
@@ -1406,6 +1415,15 @@
       "none"
     ]
   },
+  "stroke-opacity": {
+    "affects-layout": false,
+    "inherited": true,
+    "initial": "1",
+    "valid-types": [
+      "number",
+      "percentage"
+    ]
+  },
   "stop-color": {
     "affects-layout": false,
     "inherited": false,
@@ -1414,6 +1432,15 @@
       "color"
     ]
   },
+  "stop-opacity": {
+    "affects-layout": false,
+    "inherited": false,
+    "initial": "1",
+    "valid-types": [
+      "number",
+      "percentage"
+    ]
+  },
   "stroke-width": {
     "affects-layout": false,
     "inherited": true,

+ 35 - 13
Userland/Libraries/LibWeb/CSS/StyleProperties.cpp

@@ -236,36 +236,58 @@ Optional<int> StyleProperties::z_index() const
     return {};
 }
 
-float StyleProperties::opacity() const
+static float resolve_opacity_value(CSS::StyleValue const& value)
 {
-    auto value = property(CSS::PropertyID::Opacity);
-
     float unclamped_opacity = 1.0f;
 
-    if (value->has_number()) {
-        unclamped_opacity = value->to_number();
-    } else if (value->is_calculated()) {
-        auto& calculated = value->as_calculated();
+    if (value.has_number()) {
+        unclamped_opacity = value.to_number();
+    } else if (value.is_calculated()) {
+        auto& calculated = value.as_calculated();
         if (calculated.resolved_type() == CalculatedStyleValue::ResolvedType::Percentage) {
-            auto maybe_percentage = value->as_calculated().resolve_percentage();
+            auto maybe_percentage = value.as_calculated().resolve_percentage();
             if (maybe_percentage.has_value())
                 unclamped_opacity = maybe_percentage->as_fraction();
             else
-                dbgln("Unable to resolve calc() as opacity (percentage): {}", value->to_string());
+                dbgln("Unable to resolve calc() as opacity (percentage): {}", value.to_string());
         } else {
-            auto maybe_number = const_cast<CalculatedStyleValue&>(value->as_calculated()).resolve_number();
+            auto maybe_number = const_cast<CalculatedStyleValue&>(value.as_calculated()).resolve_number();
             if (maybe_number.has_value())
                 unclamped_opacity = maybe_number.value();
             else
-                dbgln("Unable to resolve calc() as opacity (number): {}", value->to_string());
+                dbgln("Unable to resolve calc() as opacity (number): {}", value.to_string());
         }
-    } else if (value->is_percentage()) {
-        unclamped_opacity = value->as_percentage().percentage().as_fraction();
+    } else if (value.is_percentage()) {
+        unclamped_opacity = value.as_percentage().percentage().as_fraction();
     }
 
     return clamp(unclamped_opacity, 0.0f, 1.0f);
 }
 
+float StyleProperties::opacity() const
+{
+    auto value = property(CSS::PropertyID::Opacity);
+    return resolve_opacity_value(*value);
+}
+
+float StyleProperties::fill_opacity() const
+{
+    auto value = property(CSS::PropertyID::FillOpacity);
+    return resolve_opacity_value(*value);
+}
+
+float StyleProperties::stroke_opacity() const
+{
+    auto value = property(CSS::PropertyID::StrokeOpacity);
+    return resolve_opacity_value(*value);
+}
+
+float StyleProperties::stop_opacity() const
+{
+    auto value = property(CSS::PropertyID::StopOpacity);
+    return resolve_opacity_value(*value);
+}
+
 Optional<CSS::FlexDirection> StyleProperties::flex_direction() const
 {
     auto value = property(CSS::PropertyID::FlexDirection);

+ 3 - 0
Userland/Libraries/LibWeb/CSS/StyleProperties.h

@@ -99,6 +99,9 @@ public:
     CSS::TransformOrigin transform_origin() const;
 
     Color stop_color() const;
+    float stop_opacity() const;
+    float fill_opacity() const;
+    float stroke_opacity() const;
 
     Gfx::Font const& computed_font() const
     {

+ 4 - 0
Userland/Libraries/LibWeb/Layout/Node.cpp

@@ -668,6 +668,10 @@ void NodeWithStyle::apply_style(const CSS::StyleProperties& computed_style)
     else
         computed_values.set_stroke_width(stroke_width->to_length());
 
+    computed_values.set_fill_opacity(computed_style.fill_opacity());
+    computed_values.set_stroke_opacity(computed_style.stroke_opacity());
+    computed_values.set_stop_opacity(computed_style.stop_opacity());
+
     computed_values.set_column_gap(computed_style.size_value(CSS::PropertyID::ColumnGap));
     computed_values.set_row_gap(computed_style.size_value(CSS::PropertyID::RowGap));
 

+ 5 - 2
Userland/Libraries/LibWeb/Painting/SVGGeometryPaintable.cpp

@@ -101,19 +101,22 @@ void SVGGeometryPaintable::paint(PaintContext& context, PaintPhase phase) const
         .transform = paint_transform
     };
 
+    // FIXME: Apply fill opacity to paint styles?
+    auto fill_opacity = geometry_element.fill_opacity().value_or(svg_context.fill_opacity());
     if (auto paint_style = geometry_element.fill_paint_style(paint_context); paint_style.has_value()) {
         painter.fill_path(
             closed_path(),
             *paint_style,
             Gfx::Painter::WindingRule::EvenOdd);
-    } else if (auto fill_color = geometry_element.fill_color().value_or(svg_context.fill_color()); fill_color.alpha() > 0) {
+    } else if (auto fill_color = geometry_element.fill_color().value_or(svg_context.fill_color()).with_opacity(fill_opacity); fill_color.alpha() > 0) {
         painter.fill_path(
             closed_path(),
             fill_color,
             Gfx::Painter::WindingRule::EvenOdd);
     }
 
-    if (auto stroke_color = geometry_element.stroke_color().value_or(svg_context.stroke_color()); stroke_color.alpha() > 0) {
+    auto stroke_opacity = geometry_element.stroke_opacity().value_or(svg_context.stroke_opacity());
+    if (auto stroke_color = geometry_element.stroke_color().value_or(svg_context.stroke_color()).with_opacity(stroke_opacity); stroke_color.alpha() > 0) {
         painter.stroke_path(
             path,
             stroke_color,

+ 10 - 6
Userland/Libraries/LibWeb/Painting/SVGGraphicsPaintable.cpp

@@ -32,12 +32,16 @@ void SVGGraphicsPaintable::before_children_paint(PaintContext& context, PaintPha
 
     auto& graphics_element = layout_box().dom_node();
 
-    if (graphics_element.fill_color().has_value())
-        context.svg_context().set_fill_color(graphics_element.fill_color().value());
-    if (graphics_element.stroke_color().has_value())
-        context.svg_context().set_stroke_color(graphics_element.stroke_color().value());
-    if (graphics_element.stroke_width().has_value())
-        context.svg_context().set_stroke_width(graphics_element.stroke_width().value());
+    if (auto fill_color = graphics_element.fill_color(); fill_color.has_value())
+        context.svg_context().set_fill_color(*fill_color);
+    if (auto stroke_color = graphics_element.stroke_color(); stroke_color.has_value())
+        context.svg_context().set_stroke_color(*stroke_color);
+    if (auto stroke_width = graphics_element.stroke_width(); stroke_width.has_value())
+        context.svg_context().set_stroke_width(*stroke_width);
+    if (auto fill_opacity = graphics_element.fill_opacity(); fill_opacity.has_value())
+        context.svg_context().set_fill_opacity(*fill_opacity);
+    if (auto stroke_opacity = graphics_element.stroke_opacity(); stroke_opacity.has_value())
+        context.svg_context().set_stroke_opacity(*stroke_opacity);
 }
 
 }

+ 7 - 1
Userland/Libraries/LibWeb/SVG/SVGContext.h

@@ -23,10 +23,14 @@ public:
     Gfx::Color fill_color() const { return state().fill_color; }
     Gfx::Color stroke_color() const { return state().stroke_color; }
     float stroke_width() const { return state().stroke_width; }
+    float fill_opacity() const { return state().fill_opacity; }
+    float stroke_opacity() const { return state().stroke_opacity; }
 
     void set_fill_color(Gfx::Color color) { state().fill_color = color; }
     void set_stroke_color(Gfx::Color color) { state().stroke_color = color; }
     void set_stroke_width(float width) { state().stroke_width = width; }
+    void set_fill_opacity(float opacity) { state().fill_opacity = opacity; }
+    void set_stroke_opacity(float opacity) { state().stroke_opacity = opacity; }
 
     CSSPixelPoint svg_element_position() const { return m_svg_element_bounds.top_left(); }
     CSSPixelSize svg_element_size() const { return m_svg_element_bounds.size(); }
@@ -38,7 +42,9 @@ private:
     struct State {
         Gfx::Color fill_color { Gfx::Color::Transparent };
         Gfx::Color stroke_color { Gfx::Color::Transparent };
-        float stroke_width { 1.0 };
+        float stroke_width { 1.0f };
+        float fill_opacity { 1.0f };
+        float stroke_opacity { 1.0f };
     };
 
     State const& state() const { return m_states.last(); }

+ 1 - 1
Userland/Libraries/LibWeb/SVG/SVGGradientElement.cpp

@@ -74,7 +74,7 @@ void SVGGradientElement::add_color_stops(Gfx::SVGGradientPaintStyle& paint_style
         // stop's offset value. If a given gradient stop's offset value is not equal to or greater than all
         // previous offset values, then the offset value is adjusted to be equal to the largest of all previous
         // offset values.
-        paint_style.add_color_stop(stop_offset, stop.stop_color()).release_value_but_fixme_should_propagate_errors();
+        paint_style.add_color_stop(stop_offset, stop.stop_color().with_opacity(stop.stop_opacity())).release_value_but_fixme_should_propagate_errors();
     });
 }
 

+ 22 - 4
Userland/Libraries/LibWeb/SVG/SVGGraphicsElement.cpp

@@ -33,9 +33,7 @@ JS::ThrowCompletionOr<void> SVGGraphicsElement::initialize(JS::Realm& realm)
 void SVGGraphicsElement::parse_attribute(DeprecatedFlyString const& name, DeprecatedString const& value)
 {
     SVGElement::parse_attribute(name, value);
-    if (name == "fill-opacity"sv) {
-        m_fill_opacity = AttributeParser::parse_length(value);
-    } else if (name == "transform"sv) {
+    if (name == "transform"sv) {
         auto transform_list = AttributeParser::parse_transform(value);
         if (transform_list.has_value())
             m_transform = transform_from_transform_list(*transform_list);
@@ -120,6 +118,12 @@ void SVGGraphicsElement::apply_presentational_hints(CSS::StyleProperties& style)
         } else if (name.equals_ignoring_ascii_case("stroke-width"sv)) {
             if (auto stroke_width_value = parse_css_value(parsing_context, value, CSS::PropertyID::StrokeWidth).release_value_but_fixme_should_propagate_errors())
                 style.set_property(CSS::PropertyID::StrokeWidth, stroke_width_value.release_nonnull());
+        } else if (name.equals_ignoring_ascii_case("fill-opacity"sv)) {
+            if (auto fill_opacity_value = parse_css_value(parsing_context, value, CSS::PropertyID::FillOpacity).release_value_but_fixme_should_propagate_errors())
+                style.set_property(CSS::PropertyID::FillOpacity, fill_opacity_value.release_nonnull());
+        } else if (name.equals_ignoring_ascii_case("stroke-opacity"sv)) {
+            if (auto stroke_opacity_value = parse_css_value(parsing_context, value, CSS::PropertyID::FillOpacity).release_value_but_fixme_should_propagate_errors())
+                style.set_property(CSS::PropertyID::StrokeOpacity, stroke_opacity_value.release_nonnull());
         }
     });
 }
@@ -133,7 +137,7 @@ Optional<Gfx::Color> SVGGraphicsElement::fill_color() const
     return layout_node()->computed_values().fill().map([&](auto& paint) -> Gfx::Color {
         if (!paint.is_color())
             return Color::Black;
-        return paint.as_color().with_alpha(m_fill_opacity.value_or(1) * 255);
+        return paint.as_color();
     });
 }
 
@@ -150,6 +154,20 @@ Optional<Gfx::Color> SVGGraphicsElement::stroke_color() const
     });
 }
 
+Optional<float> SVGGraphicsElement::fill_opacity() const
+{
+    if (!layout_node())
+        return {};
+    return layout_node()->computed_values().fill_opacity();
+}
+
+Optional<float> SVGGraphicsElement::stroke_opacity() const
+{
+    if (!layout_node())
+        return {};
+    return layout_node()->computed_values().stroke_opacity();
+}
+
 Optional<float> SVGGraphicsElement::stroke_width() const
 {
     if (!layout_node())

+ 2 - 1
Userland/Libraries/LibWeb/SVG/SVGGraphicsElement.h

@@ -30,6 +30,8 @@ public:
     Gfx::Painter::WindingRule fill_rule() const;
     Optional<Gfx::Color> stroke_color() const;
     Optional<float> stroke_width() const;
+    Optional<float> fill_opacity() const;
+    Optional<float> stroke_opacity() const;
 
     float visible_stroke_width() const
     {
@@ -47,7 +49,6 @@ protected:
 
     virtual JS::ThrowCompletionOr<void> initialize(JS::Realm&) override;
 
-    Optional<float> m_fill_opacity = {};
     Gfx::AffineTransform m_transform = {};
 };
 

+ 12 - 1
Userland/Libraries/LibWeb/SVG/SVGStopElement.cpp

@@ -31,11 +31,15 @@ void SVGStopElement::apply_presentational_hints(CSS::StyleProperties& style) con
 {
     CSS::Parser::ParsingContext parsing_context { document() };
     for_each_attribute([&](auto& name, auto& value) {
+        CSS::Parser::ParsingContext parsing_context { document() };
         if (name.equals_ignoring_ascii_case("stop-color"sv)) {
-            CSS::Parser::ParsingContext parsing_context { document() };
             if (auto stop_color = parse_css_value(parsing_context, value, CSS::PropertyID::StopColor).release_value_but_fixme_should_propagate_errors()) {
                 style.set_property(CSS::PropertyID::StopColor, stop_color.release_nonnull());
             }
+        } else if (name.equals_ignoring_ascii_case("stop-opacity"sv)) {
+            if (auto stop_opacity = parse_css_value(parsing_context, value, CSS::PropertyID::StopOpacity).release_value_but_fixme_should_propagate_errors()) {
+                style.set_property(CSS::PropertyID::StopOpacity, stop_opacity.release_nonnull());
+            }
         }
     });
 }
@@ -47,6 +51,13 @@ Gfx::Color SVGStopElement::stop_color() const
     return Color::Black;
 }
 
+float SVGStopElement::stop_opacity() const
+{
+    if (auto css_values = computed_css_values())
+        return css_values->stop_opacity();
+    return 1;
+}
+
 JS::NonnullGCPtr<SVGAnimatedNumber> SVGStopElement::offset() const
 {
     TODO();

+ 1 - 0
Userland/Libraries/LibWeb/SVG/SVGStopElement.h

@@ -27,6 +27,7 @@ public:
 
     NumberPercentage stop_offset() const { return m_offset.value_or(NumberPercentage::create_number(0)); }
     Gfx::Color stop_color() const;
+    float stop_opacity() const;
 
 private:
     SVGStopElement(DOM::Document&, DOM::QualifiedName);