diff --git a/Userland/Libraries/LibWeb/CMakeLists.txt b/Userland/Libraries/LibWeb/CMakeLists.txt index a9cb0fc2e9..a1bd191deb 100644 --- a/Userland/Libraries/LibWeb/CMakeLists.txt +++ b/Userland/Libraries/LibWeb/CMakeLists.txt @@ -170,6 +170,7 @@ set(SOURCES HTML/Canvas/CanvasPath.cpp HTML/Canvas/CanvasState.cpp HTML/CanvasGradient.cpp + HTML/CanvasPattern.cpp HTML/CanvasRenderingContext2D.cpp HTML/CloseEvent.cpp HTML/CrossOrigin/AbstractOperations.cpp diff --git a/Userland/Libraries/LibWeb/HTML/Canvas/CanvasFillStrokeStyles.h b/Userland/Libraries/LibWeb/HTML/Canvas/CanvasFillStrokeStyles.h index 25c3cec261..ae0492b5f8 100644 --- a/Userland/Libraries/LibWeb/HTML/Canvas/CanvasFillStrokeStyles.h +++ b/Userland/Libraries/LibWeb/HTML/Canvas/CanvasFillStrokeStyles.h @@ -12,6 +12,7 @@ #include #include #include +#include namespace Web::HTML { @@ -20,7 +21,7 @@ template class CanvasFillStrokeStyles { public: ~CanvasFillStrokeStyles() = default; - using FillOrStrokeStyleVariant = Variant>; + using FillOrStrokeStyleVariant = Variant, JS::Handle>; static CanvasState::FillOrStrokeStyle to_canvas_state_fill_or_stroke_style(auto const& style) { @@ -73,6 +74,12 @@ public: return CanvasGradient::create_conic(realm, start_angle, x, y); } + WebIDL::ExceptionOr> create_pattern(CanvasImageSource const& image, StringView repetition) + { + auto& realm = static_cast(*this).realm(); + return CanvasPattern::create(realm, image, repetition); + } + protected: CanvasFillStrokeStyles() = default; diff --git a/Userland/Libraries/LibWeb/HTML/Canvas/CanvasFillStrokeStyles.idl b/Userland/Libraries/LibWeb/HTML/Canvas/CanvasFillStrokeStyles.idl index ee8e8c1134..149a2334a8 100644 --- a/Userland/Libraries/LibWeb/HTML/Canvas/CanvasFillStrokeStyles.idl +++ b/Userland/Libraries/LibWeb/HTML/Canvas/CanvasFillStrokeStyles.idl @@ -1,13 +1,15 @@ #import +#import +#import +#import // https://html.spec.whatwg.org/multipage/canvas.html#canvasfillstrokestyles interface mixin CanvasFillStrokeStyles { - // FIXME: Should be `(DOMString or CanvasGradient or CanvasPattern)` - attribute (DOMString or CanvasGradient) strokeStyle; - // FIXME: Should be `(DOMString or CanvasGradient or CanvasPattern)` - attribute (DOMString or CanvasGradient) fillStyle; + attribute (DOMString or CanvasGradient or CanvasPattern) strokeStyle; + attribute (DOMString or CanvasGradient or CanvasPattern) fillStyle; CanvasGradient createLinearGradient(double x0, double y0, double x1, double y1); CanvasGradient createRadialGradient(double x0, double y0, double r0, double x1, double y1, double r1); CanvasGradient createConicGradient(double startAngle, double x, double y); - // FIXME: CanvasPattern? createPattern(CanvasImageSource image, [LegacyNullToEmptyString] DOMString repetition); + // FIXME: 'image' should be a CanvasImageSource + CanvasPattern? createPattern((HTMLImageElement or HTMLCanvasElement) image, [LegacyNullToEmptyString] DOMString repetition); }; diff --git a/Userland/Libraries/LibWeb/HTML/Canvas/CanvasState.h b/Userland/Libraries/LibWeb/HTML/Canvas/CanvasState.h index d125b45b21..3c9723c6cd 100644 --- a/Userland/Libraries/LibWeb/HTML/Canvas/CanvasState.h +++ b/Userland/Libraries/LibWeb/HTML/Canvas/CanvasState.h @@ -13,6 +13,7 @@ #include #include #include +#include namespace Web::HTML { @@ -26,7 +27,7 @@ public: void reset(); bool is_context_lost(); - using FillOrStrokeVariant = Variant>; + using FillOrStrokeVariant = Variant, JS::Handle>; struct FillOrStrokeStyle { FillOrStrokeStyle(Gfx::Color color) @@ -49,7 +50,7 @@ public: Optional as_color() const; Gfx::Color to_color_but_fixme_should_accept_any_paint_style() const; - using JsFillOrStrokeStyle = Variant>; + using JsFillOrStrokeStyle = Variant, JS::Handle>; JsFillOrStrokeStyle to_js_fill_or_stroke_style() const { diff --git a/Userland/Libraries/LibWeb/HTML/CanvasPattern.cpp b/Userland/Libraries/LibWeb/HTML/CanvasPattern.cpp new file mode 100644 index 0000000000..2fa1a89ee8 --- /dev/null +++ b/Userland/Libraries/LibWeb/HTML/CanvasPattern.cpp @@ -0,0 +1,144 @@ +/* + * Copyright (c) 2023, MacDue + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include +#include +#include + +namespace Web::HTML { + +void CanvasPatternPaintStyle::paint(Gfx::IntRect physical_bounding_box, PaintFunction paint) const +{ + // 1. Create an infinite transparent black bitmap. + // *waves magic wand 🪄* + // Done! + + // 2. Place a copy of the image on the bitmap, anchored such that its top left corner + // is at the origin of the coordinate space, with one coordinate space unit per CSS pixel of the image, + // then place repeated copies of this image horizontally to the left and right, if the repetition behavior + // is "repeat-x", or vertically up and down, if the repetition behavior is "repeat-y", or in all four directions + // all over the bitmap, if the repetition behavior is "repeat". + + // FIMXE: If the original image data is a bitmap image, then the value painted at a point in the area of + // the repetitions is computed by filtering the original image data. When scaling up, if the imageSmoothingEnabled + // attribute is set to false, then the image must be rendered using nearest-neighbor interpolation. + // Otherwise, the user agent may use any filtering algorithm (for example bilinear interpolation or nearest-neighbor). + // User agents which support multiple filtering algorithms may use the value of the imageSmoothingQuality attribute + // to guide the choice of filtering algorithm. When such a filtering algorithm requires a pixel value from outside + // the original image data, it must instead use the value from wrapping the pixel's coordinates to the original + // image's dimensions. (That is, the filter uses 'repeat' behavior, regardless of the value of the pattern's repetition behavior.) + + // FIXME: 3. Transform the resulting bitmap according to the pattern's transformation matrix. + + // FIXME: 4. Transform the resulting bitmap again, this time according to the current transformation matrix. + + // 5. Replace any part of the image outside the area in which the pattern is to be rendered with transparent black. + + // 6. The resulting bitmap is what is to be rendered, with the same origin and same scale. + + auto const bitmap_width = m_bitmap->width(); + auto const bitmap_height = m_bitmap->height(); + + paint([=, this](auto point) { + point.translate_by(physical_bounding_box.location()); + point = [&]() -> Gfx::IntPoint { + switch (m_repetition) { + case Repetition::NoRepeat: { + return point; + } + case Repetition::Repeat: { + return { + point.x() % bitmap_width, + point.y() % bitmap_height + }; + } + case Repetition::RepeatX: { + return { + point.x() % bitmap_width, + point.y() + }; + } + case Repetition::RepeatY: { + return { + point.x(), + point.y() % bitmap_height + }; + } + default: + VERIFY_NOT_REACHED(); + } + }(); + if (m_bitmap->rect().contains(point)) + return m_bitmap->get_pixel(point); + return Gfx::Color(); + }); +} + +CanvasPattern::CanvasPattern(JS::Realm& realm, CanvasPatternPaintStyle& pattern) + : PlatformObject(realm) + , m_pattern(pattern) +{ +} + +CanvasPattern::~CanvasPattern() = default; + +// https://html.spec.whatwg.org/multipage/canvas.html#dom-context-2d-createpattern +WebIDL::ExceptionOr> CanvasPattern::create(JS::Realm& realm, CanvasImageSource const& image, StringView repetition) +{ + auto parse_repetition = [&](auto repetition) -> Optional { + if (repetition == "repeat"sv) + return CanvasPatternPaintStyle::Repetition::Repeat; + if (repetition == "repeat-x"sv) + return CanvasPatternPaintStyle::Repetition::RepeatX; + if (repetition == "repeat-y"sv) + return CanvasPatternPaintStyle::Repetition::RepeatY; + if (repetition == "no-repeat"sv) + return CanvasPatternPaintStyle::Repetition::NoRepeat; + return {}; + }; + + // 1. Let usability be the result of checking the usability of image. + auto usability = TRY(check_usability_of_image(image)); + + // 2. If usability is bad, then return null. + if (usability == CanvasImageSourceUsability::Bad) + return JS::GCPtr {}; + + // 3. Assert: usability is good. + VERIFY(usability == CanvasImageSourceUsability::Good); + + // 4. If repetition is the empty string, then set it to "repeat". + if (repetition.is_empty()) + repetition = "repeat"sv; + + // 5. If repetition is not identical to one of "repeat", "repeat-x", "repeat-y", or "no-repeat", + // then throw a "SyntaxError" DOMException. + auto repetition_value = parse_repetition(repetition); + if (!repetition_value.has_value()) + return WebIDL::SyntaxError::create(realm, "Repetition value is not valid"); + + // Note: Bitmap won't be null here, as if it were it would have "bad" usability. + auto const& bitmap = *image.visit([](auto const& source) -> Gfx::Bitmap const* { return source->bitmap(); }); + + // 6. Let pattern be a new CanvasPattern object with the image image and the repetition behavior given by repetition. + auto pattern = CanvasPatternPaintStyle::create(bitmap, *repetition_value); + + // FIXME: 7. If image is not origin-clean, then mark pattern as not origin-clean. + + // 8. Return pattern. + return MUST_OR_THROW_OOM(realm.heap().allocate(realm, realm, *pattern)); +} + +JS::ThrowCompletionOr CanvasPattern::initialize(JS::Realm& realm) +{ + MUST_OR_THROW_OOM(Base::initialize(realm)); + set_prototype(&Bindings::ensure_web_prototype(realm, "CanvasPattern")); + + return {}; +} + +} diff --git a/Userland/Libraries/LibWeb/HTML/CanvasPattern.h b/Userland/Libraries/LibWeb/HTML/CanvasPattern.h new file mode 100644 index 0000000000..d8c7fc10f8 --- /dev/null +++ b/Userland/Libraries/LibWeb/HTML/CanvasPattern.h @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2023, MacDue + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include +#include + +namespace Web::HTML { + +class CanvasPatternPaintStyle final : public Gfx::PaintStyle { +public: + enum class Repetition { + Repeat, + RepeatX, + RepeatY, + NoRepeat + }; + + static NonnullRefPtr create(Gfx::Bitmap const& bitmap, Repetition repetition) + { + return adopt_ref(*new CanvasPatternPaintStyle(bitmap, repetition)); + } + + virtual void paint(Gfx::IntRect physical_bounding_box, PaintFunction paint) const override; + +private: + CanvasPatternPaintStyle(Gfx::Bitmap const& bitmap, Repetition repetition) + : m_bitmap(bitmap) + , m_repetition(repetition) + { + } + + NonnullRefPtr m_bitmap; + Repetition m_repetition { Repetition::Repeat }; +}; + +class CanvasPattern final : public Bindings::PlatformObject { + WEB_PLATFORM_OBJECT(CanvasPattern, Bindings::PlatformObject); + +public: + static WebIDL::ExceptionOr> create(JS::Realm&, CanvasImageSource const& image, StringView repetition); + + ~CanvasPattern(); + + NonnullRefPtr to_gfx_paint_style() { return m_pattern; } + +private: + CanvasPattern(JS::Realm&, CanvasPatternPaintStyle&); + + virtual JS::ThrowCompletionOr initialize(JS::Realm&) override; + + NonnullRefPtr m_pattern; +}; + +} diff --git a/Userland/Libraries/LibWeb/HTML/CanvasPattern.idl b/Userland/Libraries/LibWeb/HTML/CanvasPattern.idl new file mode 100644 index 0000000000..046c235e5b --- /dev/null +++ b/Userland/Libraries/LibWeb/HTML/CanvasPattern.idl @@ -0,0 +1,5 @@ +[Exposed=(Window,Worker)] +interface CanvasPattern { + // opaque object + // FIXME: undefined setTransform(optional DOMMatrix2DInit transform = {}); +}; diff --git a/Userland/Libraries/LibWeb/idl_files.cmake b/Userland/Libraries/LibWeb/idl_files.cmake index 2ae06b8972..0148066b3e 100644 --- a/Userland/Libraries/LibWeb/idl_files.cmake +++ b/Userland/Libraries/LibWeb/idl_files.cmake @@ -65,6 +65,7 @@ libweb_js_bindings(Geometry/DOMRect) libweb_js_bindings(Geometry/DOMRectList) libweb_js_bindings(Geometry/DOMRectReadOnly) libweb_js_bindings(HTML/CanvasGradient) +libweb_js_bindings(HTML/CanvasPattern) libweb_js_bindings(HTML/CanvasRenderingContext2D) libweb_js_bindings(HTML/CloseEvent) libweb_js_bindings(HTML/DOMParser)