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.
This commit is contained in:
parent
120e5b6b6f
commit
00cda96e2d
Notes:
sideshowbarker
2024-07-16 21:51:02 +09:00
Author: https://github.com/MacDue Commit: https://github.com/SerenityOS/serenity/commit/00cda96e2d Pull-request: https://github.com/SerenityOS/serenity/pull/18927
13 changed files with 141 additions and 29 deletions
|
@ -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; }
|
||||
};
|
||||
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
{
|
||||
|
|
|
@ -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));
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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(); }
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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 = {};
|
||||
};
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
|
|
Loading…
Add table
Reference in a new issue