LibWeb/SVG: Implement the feTurbulence filter

This commit is contained in:
Luke Wilde 2026-02-10 15:08:11 +00:00 committed by Andreas Kling
parent a89b02e5c3
commit e9f5df2131
20 changed files with 314 additions and 0 deletions

View file

@ -13,6 +13,7 @@
#include <core/SkScalar.h>
#include <effects/SkColorMatrix.h>
#include <effects/SkImageFilters.h>
#include <effects/SkPerlinNoiseShader.h>
namespace Gfx {
@ -296,4 +297,20 @@ Filter Filter::offset(float dx, float dy, Optional<Filter const&> input)
return Filter(Impl::create(SkImageFilters::Offset(dx, dy, input_skia)));
}
Filter Filter::turbulence(TurbulenceType turbulence_type, float base_frequency_x, float base_frequency_y, i32 num_octaves, float seed, Gfx::IntSize const& tile_stitch_size)
{
sk_sp<SkShader> turbulence_shader = [&] {
auto skia_size = SkISize::Make(tile_stitch_size.width(), tile_stitch_size.height());
switch (turbulence_type) {
case TurbulenceType::Turbulence:
return SkShaders::MakeTurbulence(base_frequency_x, base_frequency_y, num_octaves, seed, &skia_size);
case TurbulenceType::FractalNoise:
return SkShaders::MakeFractalNoise(base_frequency_x, base_frequency_y, num_octaves, seed, &skia_size);
}
VERIFY_NOT_REACHED();
}();
return Filter(Impl::create(SkImageFilters::Shader(move(turbulence_shader))));
}
}

View file

@ -25,6 +25,11 @@ enum class ColorFilterType {
Sepia
};
enum class TurbulenceType {
FractalNoise,
Turbulence,
};
struct FilterImpl;
class Filter {
@ -50,6 +55,7 @@ public:
static Filter offset(float dx, float dy, Optional<Filter const&> input = {});
static Filter erode(float radius_x, float radius_y, Optional<Filter> const& input = {});
static Filter dilate(float radius_x, float radius_y, Optional<Filter> const& input = {});
static Filter turbulence(TurbulenceType turbulence_type, float base_frequency_x, float base_frequency_y, i32 num_octaves, float seed, Gfx::IntSize const& tile_stitch_size);
FilterImpl const& impl() const;

View file

@ -964,6 +964,7 @@ set(SOURCES
SVG/SVGFEMergeNodeElement.cpp
SVG/SVGFEMorphologyElement.cpp
SVG/SVGFEOffsetElement.cpp
SVG/SVGFETurbulenceElement.cpp
SVG/SVGFilterElement.cpp
SVG/SVGFilterPrimitiveStandardAttributes.cpp
SVG/SVGFitToViewBox.cpp

View file

@ -110,6 +110,7 @@
#include <LibWeb/SVG/SVGFEMergeNodeElement.h>
#include <LibWeb/SVG/SVGFEMorphologyElement.h>
#include <LibWeb/SVG/SVGFEOffsetElement.h>
#include <LibWeb/SVG/SVGFETurbulenceElement.h>
#include <LibWeb/SVG/SVGFilterElement.h>
#include <LibWeb/SVG/SVGForeignObjectElement.h>
#include <LibWeb/SVG/SVGGElement.h>
@ -517,6 +518,8 @@ static GC::Ref<SVG::SVGElement> create_svg_element(JS::Realm& realm, Document& d
return realm.create<SVG::SVGFEMorphologyElement>(document, move(qualified_name));
if (local_name == SVG::TagNames::feOffset)
return realm.create<SVG::SVGFEOffsetElement>(document, move(qualified_name));
if (local_name == SVG::TagNames::feTurbulence)
return realm.create<SVG::SVGFETurbulenceElement>(document, move(qualified_name));
if (local_name == SVG::TagNames::filter)
return realm.create<SVG::SVGFilterElement>(document, move(qualified_name));
if (local_name.equals_ignoring_ascii_case(SVG::TagNames::foreignObject))

View file

@ -1152,6 +1152,7 @@ class SVGFEFuncRElement;
class SVGFEGaussianBlurElement;
class SVGFEImageElement;
class SVGFEMorphologyElement;
class SVGFETurbulenceElement;
class SVGFilterElement;
class SVGFitToViewBox;
class SVGForeignObjectElement;

View file

@ -87,6 +87,7 @@ namespace Web::SVG::AttributeNames {
__ENUMERATE_SVG_ATTRIBUTE(rotate, "rotate") \
__ENUMERATE_SVG_ATTRIBUTE(rx, "rx") \
__ENUMERATE_SVG_ATTRIBUTE(ry, "ry") \
__ENUMERATE_SVG_ATTRIBUTE(seed, "seed") \
__ENUMERATE_SVG_ATTRIBUTE(slope, "slope") \
__ENUMERATE_SVG_ATTRIBUTE(specularConstant, "specularConstant") \
__ENUMERATE_SVG_ATTRIBUTE(specularExponent, "specularExponent") \

View file

@ -0,0 +1,116 @@
/*
* Copyright (c) 2026, Luke Wilde <luke@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibWeb/Bindings/SVGFETurbulenceElementPrototype.h>
#include <LibWeb/SVG/AttributeNames.h>
#include <LibWeb/SVG/SVGFETurbulenceElement.h>
namespace Web::SVG {
GC_DEFINE_ALLOCATOR(SVGFETurbulenceElement);
SVGFETurbulenceElement::SVGFETurbulenceElement(DOM::Document& document, DOM::QualifiedName qualified_name)
: SVGElement(document, move(qualified_name))
{
}
void SVGFETurbulenceElement::initialize(JS::Realm& realm)
{
WEB_SET_PROTOTYPE_FOR_INTERFACE(SVGFETurbulenceElement);
Base::initialize(realm);
}
void SVGFETurbulenceElement::visit_edges(Cell::Visitor& visitor)
{
Base::visit_edges(visitor);
SVGFilterPrimitiveStandardAttributes::visit_edges(visitor);
visitor.visit(m_base_frequency_x);
visitor.visit(m_base_frequency_y);
visitor.visit(m_num_octaves);
visitor.visit(m_seed);
visitor.visit(m_stitch_tiles);
visitor.visit(m_type);
}
// https://drafts.csswg.org/filter-effects/#dom-svgfeturbulenceelement-basefrequencyx
GC::Ref<SVGAnimatedNumber> SVGFETurbulenceElement::base_frequency_x()
{
if (!m_base_frequency_x) {
m_base_frequency_x = SVGAnimatedNumber::create(realm(), *this, DOM::QualifiedName { AttributeNames::baseFrequency, OptionalNone {}, OptionalNone {} }, 0.f,
SVGAnimatedNumber::SupportsSecondValue::Yes, SVGAnimatedNumber::ValueRepresented::First);
}
return *m_base_frequency_x;
}
// https://drafts.csswg.org/filter-effects/#dom-svgfeturbulenceelement-basefrequencyy
GC::Ref<SVGAnimatedNumber> SVGFETurbulenceElement::base_frequency_y()
{
if (!m_base_frequency_y) {
m_base_frequency_y = SVGAnimatedNumber::create(realm(), *this, DOM::QualifiedName { AttributeNames::baseFrequency, OptionalNone {}, OptionalNone {} }, 0.f,
SVGAnimatedNumber::SupportsSecondValue::Yes, SVGAnimatedNumber::ValueRepresented::Second);
}
return *m_base_frequency_y;
}
// https://drafts.csswg.org/filter-effects/#dom-svgfeturbulenceelement-numoctaves
GC::Ref<SVGAnimatedInteger> SVGFETurbulenceElement::num_octaves()
{
if (!m_num_octaves)
m_num_octaves = SVGAnimatedInteger::create(realm(), *this, DOM::QualifiedName { AttributeNames::numOctaves, OptionalNone {}, OptionalNone {} }, 1);
return *m_num_octaves;
}
// https://drafts.csswg.org/filter-effects/#dom-svgfeturbulenceelement-seed
GC::Ref<SVGAnimatedNumber> SVGFETurbulenceElement::seed()
{
if (!m_seed)
m_seed = SVGAnimatedNumber::create(realm(), *this, DOM::QualifiedName { AttributeNames::seed, OptionalNone {}, OptionalNone {} }, 0);
return *m_seed;
}
// // https://drafts.csswg.org/filter-effects/#element-attrdef-feturbulence-stitchtiles
static SVGFETurbulenceElement::StitchType parse_stitch_tiles(String const& value)
{
if (value == "stitch"sv)
return SVGFETurbulenceElement::StitchType::Stitch;
if (value == "noStitch"sv)
return SVGFETurbulenceElement::StitchType::NoStitch;
return SVGFETurbulenceElement::StitchType::NoStitch;
}
// https://drafts.csswg.org/filter-effects/#element-attrdef-feturbulence-stitchtiles
GC::Ref<SVGAnimatedEnumeration> SVGFETurbulenceElement::stitch_tiles()
{
// FIXME: Support reflection, don't return a new object every time.
auto stitch_tiles = parse_stitch_tiles(get_attribute_value(AttributeNames::stitchTiles));
return SVGAnimatedEnumeration::create(realm(), to_underlying(stitch_tiles));
}
// https://drafts.csswg.org/filter-effects/#element-attrdef-feturbulence-type
static SVGFETurbulenceElement::TurbulenceType parse_turbulence_type(String const& value)
{
if (value == "turbulence"sv)
return SVGFETurbulenceElement::TurbulenceType::Turbulence;
if (value == "fractalNoise"sv)
return SVGFETurbulenceElement::TurbulenceType::FractalNoise;
return SVGFETurbulenceElement::TurbulenceType::Turbulence;
}
// https://drafts.csswg.org/filter-effects/#dom-svgfeturbulenceelement-type
GC::Ref<SVGAnimatedEnumeration> SVGFETurbulenceElement::type()
{
// FIXME: Support reflection, don't return a new object every time.
auto turbulence_type = parse_turbulence_type(get_attribute_value(AttributeNames::type));
return SVGAnimatedEnumeration::create(realm(), to_underlying(turbulence_type));
}
}

View file

@ -0,0 +1,62 @@
/*
* Copyright (c) 2026, Luke Wilde <luke@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <LibWeb/SVG/SVGAnimatedEnumeration.h>
#include <LibWeb/SVG/SVGAnimatedInteger.h>
#include <LibWeb/SVG/SVGAnimatedNumber.h>
#include <LibWeb/SVG/SVGAnimatedString.h>
#include <LibWeb/SVG/SVGElement.h>
#include <LibWeb/SVG/SVGFilterPrimitiveStandardAttributes.h>
namespace Web::SVG {
class SVGFETurbulenceElement final
: public SVGElement
, public SVGFilterPrimitiveStandardAttributes<SVGFETurbulenceElement> {
WEB_PLATFORM_OBJECT(SVGFETurbulenceElement, SVGElement);
GC_DECLARE_ALLOCATOR(SVGFETurbulenceElement);
public:
enum class TurbulenceType {
Unknown = 0,
FractalNoise = 1,
Turbulence = 2,
};
enum class StitchType {
Unknown = 0,
Stitch = 1,
NoStitch = 2,
};
virtual ~SVGFETurbulenceElement() override = default;
GC::Ref<SVGAnimatedNumber> base_frequency_x();
GC::Ref<SVGAnimatedNumber> base_frequency_y();
GC::Ref<SVGAnimatedInteger> num_octaves();
GC::Ref<SVGAnimatedNumber> seed();
GC::Ref<SVGAnimatedEnumeration> stitch_tiles();
GC::Ref<SVGAnimatedEnumeration> type();
protected:
SVGFETurbulenceElement(DOM::Document&, DOM::QualifiedName);
virtual void initialize(JS::Realm&) override;
private:
virtual void visit_edges(Cell::Visitor&) override;
GC::Ptr<SVGAnimatedNumber> m_base_frequency_x;
GC::Ptr<SVGAnimatedNumber> m_base_frequency_y;
GC::Ptr<SVGAnimatedInteger> m_num_octaves;
GC::Ptr<SVGAnimatedNumber> m_seed;
GC::Ptr<SVGAnimatedEnumeration> m_stitch_tiles;
GC::Ptr<SVGAnimatedEnumeration> m_type;
};
}

View file

@ -0,0 +1,28 @@
#import <SVG/SVGAnimatedEnumeration.idl>
#import <SVG/SVGAnimatedInteger.idl>
#import <SVG/SVGAnimatedNumber.idl>
#import <SVG/SVGAnimatedString.idl>
#import <SVG/SVGFilterPrimitiveStandardAttributes.idl>
// https://drafts.csswg.org/filter-effects/#svgfeturbulenceelement
[Exposed=Window]
interface SVGFETurbulenceElement : SVGElement {
// Turbulence Types
const unsigned short SVG_TURBULENCE_TYPE_UNKNOWN = 0;
const unsigned short SVG_TURBULENCE_TYPE_FRACTALNOISE = 1;
const unsigned short SVG_TURBULENCE_TYPE_TURBULENCE = 2;
// Stitch Options
const unsigned short SVG_STITCHTYPE_UNKNOWN = 0;
const unsigned short SVG_STITCHTYPE_STITCH = 1;
const unsigned short SVG_STITCHTYPE_NOSTITCH = 2;
readonly attribute SVGAnimatedNumber baseFrequencyX;
readonly attribute SVGAnimatedNumber baseFrequencyY;
readonly attribute SVGAnimatedInteger numOctaves;
readonly attribute SVGAnimatedNumber seed;
readonly attribute SVGAnimatedEnumeration stitchTiles;
readonly attribute SVGAnimatedEnumeration type;
};
SVGFETurbulenceElement includes SVGFilterPrimitiveStandardAttributes;

View file

@ -31,6 +31,7 @@
#include <LibWeb/SVG/SVGFEMergeNodeElement.h>
#include <LibWeb/SVG/SVGFEMorphologyElement.h>
#include <LibWeb/SVG/SVGFEOffsetElement.h>
#include <LibWeb/SVG/SVGFETurbulenceElement.h>
#include <LibWeb/SVG/SVGFilterElement.h>
namespace Web::SVG {
@ -359,6 +360,39 @@ Optional<Gfx::Filter> SVGFilterElement::gfx_filter(Layout::NodeWithStyle const&
// </feMerge>
root_filter = Gfx::Filter::merge({ colored_shadow, input });
update_result_map(*drop_shadow);
} else if (auto* turbulence = as_if<SVGFETurbulenceElement>(node)) {
auto base_frequency_x = turbulence->base_frequency_x()->base_val();
auto base_frequency_y = turbulence->base_frequency_y()->base_val();
auto num_octaves = turbulence->num_octaves()->base_val();
auto seed = turbulence->seed()->base_val();
auto type = [turbulence] {
auto turbulence_type = turbulence->type()->base_val();
switch (turbulence_type) {
case to_underlying(SVGFETurbulenceElement::TurbulenceType::Turbulence):
return Gfx::TurbulenceType::Turbulence;
case to_underlying(SVGFETurbulenceElement::TurbulenceType::FractalNoise):
return Gfx::TurbulenceType::FractalNoise;
default:
VERIFY_NOT_REACHED();
}
}();
auto tile_stitch_size = [turbulence] {
auto stitch_tiles = turbulence->stitch_tiles()->base_val();
switch (stitch_tiles) {
case to_underlying(SVGFETurbulenceElement::StitchType::Stitch):
// FIXME: Are these the correct width and height?
return Gfx::IntSize { turbulence->width()->base_val()->value(), turbulence->height()->base_val()->value() };
case to_underlying(SVGFETurbulenceElement::StitchType::NoStitch):
return Gfx::IntSize {};
default:
VERIFY_NOT_REACHED();
}
}();
root_filter = Gfx::Filter::turbulence(type, base_frequency_x, base_frequency_y, num_octaves, seed, tile_stitch_size);
update_result_map(*turbulence);
} else {
dbgln("SVGFilterElement::gfx_filter(): Unknown or unsupported filter element '{}'", node.debug_description());
}

View file

@ -17,6 +17,7 @@
#include <LibWeb/SVG/SVGFEMergeElement.h>
#include <LibWeb/SVG/SVGFEMorphologyElement.h>
#include <LibWeb/SVG/SVGFEOffsetElement.h>
#include <LibWeb/SVG/SVGFETurbulenceElement.h>
#include <LibWeb/SVG/SVGFilterPrimitiveStandardAttributes.h>
namespace Web::SVG {
@ -65,5 +66,6 @@ template class SVGFilterPrimitiveStandardAttributes<SVGFEImageElement>;
template class SVGFilterPrimitiveStandardAttributes<SVGFEMergeElement>;
template class SVGFilterPrimitiveStandardAttributes<SVGFEMorphologyElement>;
template class SVGFilterPrimitiveStandardAttributes<SVGFEOffsetElement>;
template class SVGFilterPrimitiveStandardAttributes<SVGFETurbulenceElement>;
}

View file

@ -33,6 +33,7 @@ namespace Web::SVG::TagNames {
__ENUMERATE_SVG_TAG(feMergeNode) \
__ENUMERATE_SVG_TAG(feMorphology) \
__ENUMERATE_SVG_TAG(feOffset) \
__ENUMERATE_SVG_TAG(feTurbulence) \
__ENUMERATE_SVG_TAG(filter) \
__ENUMERATE_SVG_TAG(foreignObject) \
__ENUMERATE_SVG_TAG(g) \

View file

@ -423,6 +423,7 @@ libweb_js_bindings(SVG/SVGFEMergeElement)
libweb_js_bindings(SVG/SVGFEMergeNodeElement)
libweb_js_bindings(SVG/SVGFEMorphologyElement)
libweb_js_bindings(SVG/SVGFEOffsetElement)
libweb_js_bindings(SVG/SVGFETurbulenceElement)
libweb_js_bindings(SVG/SVGFilterElement)
libweb_js_bindings(SVG/SVGForeignObjectElement)
libweb_js_bindings(SVG/SVGLength)

View file

@ -0,0 +1,6 @@
<!DOCTYPE html>
<style>
* { margin: 0; }
body { background-color: white; }
</style>
<img src="../images/svg-feTurbulence-fractalNoise-ref.png">

View file

@ -0,0 +1,6 @@
<!DOCTYPE html>
<style>
* { margin: 0; }
body { background-color: white; }
</style>
<img src="../images/svg-feTurbulence-ref.png">

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

View file

@ -0,0 +1,14 @@
<!DOCTYPE html>
<link rel="match" href="../expected/svg-feTurbulence-fractalNoise-ref.html" />
<style>
* { margin: 0; }
body { background-color: white; }
</style>
<svg width="200" height="200" xmlns="http://www.w3.org/2000/svg">
<defs>
<filter id="turbulence">
<feTurbulence type="fractalNoise" baseFrequency="0.05" numOctaves="3" seed="5" />
</filter>
</defs>
<rect width="200" height="200" filter="url(#turbulence)" />
</svg>

View file

@ -0,0 +1,14 @@
<!DOCTYPE html>
<link rel="match" href="../expected/svg-feTurbulence-ref.html" />
<style>
* { margin: 0; }
body { background-color: white; }
</style>
<svg width="200" height="200" xmlns="http://www.w3.org/2000/svg">
<defs>
<filter id="turbulence">
<feTurbulence baseFrequency="0.05" numOctaves="2" />
</filter>
</defs>
<rect width="200" height="200" filter="url(#turbulence)" />
</svg>

View file

@ -401,6 +401,7 @@ SVGFEMergeElement
SVGFEMergeNodeElement
SVGFEMorphologyElement
SVGFEOffsetElement
SVGFETurbulenceElement
SVGFilterElement
SVGForeignObjectElement
SVGGElement