Browse Source

LibWeb: Implement CanvasRenderingContext2D.createPattern()

This is a first pass at implementing CRC2D.createPattern() and the
associated CanvasPattern object. This implementation only works for a
few of the required image sources [like CRC2D.drawImage()], and does
not yet support transforms. Other than that it supports everything
else (which is mainly the various repeat modes).
MacDue 2 years ago
parent
commit
f74e2da875

+ 1 - 0
Userland/Libraries/LibWeb/CMakeLists.txt

@@ -170,6 +170,7 @@ set(SOURCES
     HTML/Canvas/CanvasPath.cpp
     HTML/Canvas/CanvasPath.cpp
     HTML/Canvas/CanvasState.cpp
     HTML/Canvas/CanvasState.cpp
     HTML/CanvasGradient.cpp
     HTML/CanvasGradient.cpp
+    HTML/CanvasPattern.cpp
     HTML/CanvasRenderingContext2D.cpp
     HTML/CanvasRenderingContext2D.cpp
     HTML/CloseEvent.cpp
     HTML/CloseEvent.cpp
     HTML/CrossOrigin/AbstractOperations.cpp
     HTML/CrossOrigin/AbstractOperations.cpp

+ 8 - 1
Userland/Libraries/LibWeb/HTML/Canvas/CanvasFillStrokeStyles.h

@@ -12,6 +12,7 @@
 #include <AK/DeprecatedString.h>
 #include <AK/DeprecatedString.h>
 #include <LibWeb/HTML/Canvas/CanvasState.h>
 #include <LibWeb/HTML/Canvas/CanvasState.h>
 #include <LibWeb/HTML/CanvasGradient.h>
 #include <LibWeb/HTML/CanvasGradient.h>
+#include <LibWeb/HTML/CanvasPattern.h>
 
 
 namespace Web::HTML {
 namespace Web::HTML {
 
 
@@ -20,7 +21,7 @@ template<typename IncludingClass>
 class CanvasFillStrokeStyles {
 class CanvasFillStrokeStyles {
 public:
 public:
     ~CanvasFillStrokeStyles() = default;
     ~CanvasFillStrokeStyles() = default;
-    using FillOrStrokeStyleVariant = Variant<DeprecatedString, JS::Handle<CanvasGradient>>;
+    using FillOrStrokeStyleVariant = Variant<DeprecatedString, JS::Handle<CanvasGradient>, JS::Handle<CanvasPattern>>;
 
 
     static CanvasState::FillOrStrokeStyle to_canvas_state_fill_or_stroke_style(auto const& style)
     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);
         return CanvasGradient::create_conic(realm, start_angle, x, y);
     }
     }
 
 
+    WebIDL::ExceptionOr<JS::GCPtr<CanvasPattern>> create_pattern(CanvasImageSource const& image, StringView repetition)
+    {
+        auto& realm = static_cast<IncludingClass&>(*this).realm();
+        return CanvasPattern::create(realm, image, repetition);
+    }
+
 protected:
 protected:
     CanvasFillStrokeStyles() = default;
     CanvasFillStrokeStyles() = default;
 
 

+ 7 - 5
Userland/Libraries/LibWeb/HTML/Canvas/CanvasFillStrokeStyles.idl

@@ -1,13 +1,15 @@
 #import <HTML/CanvasGradient.idl>
 #import <HTML/CanvasGradient.idl>
+#import <HTML/CanvasPattern.idl>
+#import <HTML/HTMLCanvasElement.idl>
+#import <HTML/HTMLImageElement.idl>
 
 
 // https://html.spec.whatwg.org/multipage/canvas.html#canvasfillstrokestyles
 // https://html.spec.whatwg.org/multipage/canvas.html#canvasfillstrokestyles
 interface mixin 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 createLinearGradient(double x0, double y0, double x1, double y1);
     CanvasGradient createRadialGradient(double x0, double y0, double r0, double x1, double y1, double r1);
     CanvasGradient createRadialGradient(double x0, double y0, double r0, double x1, double y1, double r1);
     CanvasGradient createConicGradient(double startAngle, double x, double y);
     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);
 };
 };

+ 3 - 2
Userland/Libraries/LibWeb/HTML/Canvas/CanvasState.h

@@ -13,6 +13,7 @@
 #include <LibGfx/Color.h>
 #include <LibGfx/Color.h>
 #include <LibGfx/PaintStyle.h>
 #include <LibGfx/PaintStyle.h>
 #include <LibWeb/HTML/CanvasGradient.h>
 #include <LibWeb/HTML/CanvasGradient.h>
+#include <LibWeb/HTML/CanvasPattern.h>
 
 
 namespace Web::HTML {
 namespace Web::HTML {
 
 
@@ -26,7 +27,7 @@ public:
     void reset();
     void reset();
     bool is_context_lost();
     bool is_context_lost();
 
 
-    using FillOrStrokeVariant = Variant<Gfx::Color, JS::Handle<CanvasGradient>>;
+    using FillOrStrokeVariant = Variant<Gfx::Color, JS::Handle<CanvasGradient>, JS::Handle<CanvasPattern>>;
 
 
     struct FillOrStrokeStyle {
     struct FillOrStrokeStyle {
         FillOrStrokeStyle(Gfx::Color color)
         FillOrStrokeStyle(Gfx::Color color)
@@ -49,7 +50,7 @@ public:
         Optional<Gfx::Color> as_color() const;
         Optional<Gfx::Color> as_color() const;
         Gfx::Color to_color_but_fixme_should_accept_any_paint_style() const;
         Gfx::Color to_color_but_fixme_should_accept_any_paint_style() const;
 
 
-        using JsFillOrStrokeStyle = Variant<DeprecatedString, JS::Handle<CanvasGradient>>;
+        using JsFillOrStrokeStyle = Variant<DeprecatedString, JS::Handle<CanvasGradient>, JS::Handle<CanvasPattern>>;
 
 
         JsFillOrStrokeStyle to_js_fill_or_stroke_style() const
         JsFillOrStrokeStyle to_js_fill_or_stroke_style() const
         {
         {

+ 144 - 0
Userland/Libraries/LibWeb/HTML/CanvasPattern.cpp

@@ -0,0 +1,144 @@
+/*
+ * Copyright (c) 2023, MacDue <macdue@dueutil.tech>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include <LibGfx/Bitmap.h>
+#include <LibWeb/Bindings/Intrinsics.h>
+#include <LibWeb/HTML/CanvasPattern.h>
+#include <LibWeb/HTML/CanvasRenderingContext2D.h>
+
+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<JS::GCPtr<CanvasPattern>> CanvasPattern::create(JS::Realm& realm, CanvasImageSource const& image, StringView repetition)
+{
+    auto parse_repetition = [&](auto repetition) -> Optional<CanvasPatternPaintStyle::Repetition> {
+        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<CanvasPattern> {};
+
+    // 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<CanvasPattern>(realm, realm, *pattern));
+}
+
+JS::ThrowCompletionOr<void> CanvasPattern::initialize(JS::Realm& realm)
+{
+    MUST_OR_THROW_OOM(Base::initialize(realm));
+    set_prototype(&Bindings::ensure_web_prototype<Bindings::CanvasPatternPrototype>(realm, "CanvasPattern"));
+
+    return {};
+}
+
+}

+ 60 - 0
Userland/Libraries/LibWeb/HTML/CanvasPattern.h

@@ -0,0 +1,60 @@
+/*
+ * Copyright (c) 2023, MacDue <macdue@dueutil.tech>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#pragma once
+
+#include <LibGfx/PaintStyle.h>
+#include <LibWeb/Bindings/PlatformObject.h>
+#include <LibWeb/HTML/Canvas/CanvasDrawImage.h>
+
+namespace Web::HTML {
+
+class CanvasPatternPaintStyle final : public Gfx::PaintStyle {
+public:
+    enum class Repetition {
+        Repeat,
+        RepeatX,
+        RepeatY,
+        NoRepeat
+    };
+
+    static NonnullRefPtr<CanvasPatternPaintStyle> 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<Gfx::Bitmap const> m_bitmap;
+    Repetition m_repetition { Repetition::Repeat };
+};
+
+class CanvasPattern final : public Bindings::PlatformObject {
+    WEB_PLATFORM_OBJECT(CanvasPattern, Bindings::PlatformObject);
+
+public:
+    static WebIDL::ExceptionOr<JS::GCPtr<CanvasPattern>> create(JS::Realm&, CanvasImageSource const& image, StringView repetition);
+
+    ~CanvasPattern();
+
+    NonnullRefPtr<Gfx::PaintStyle> to_gfx_paint_style() { return m_pattern; }
+
+private:
+    CanvasPattern(JS::Realm&, CanvasPatternPaintStyle&);
+
+    virtual JS::ThrowCompletionOr<void> initialize(JS::Realm&) override;
+
+    NonnullRefPtr<CanvasPatternPaintStyle> m_pattern;
+};
+
+}

+ 5 - 0
Userland/Libraries/LibWeb/HTML/CanvasPattern.idl

@@ -0,0 +1,5 @@
+[Exposed=(Window,Worker)]
+interface CanvasPattern {
+  // opaque object
+  // FIXME: undefined setTransform(optional DOMMatrix2DInit transform = {});
+};

+ 1 - 0
Userland/Libraries/LibWeb/idl_files.cmake

@@ -65,6 +65,7 @@ libweb_js_bindings(Geometry/DOMRect)
 libweb_js_bindings(Geometry/DOMRectList)
 libweb_js_bindings(Geometry/DOMRectList)
 libweb_js_bindings(Geometry/DOMRectReadOnly)
 libweb_js_bindings(Geometry/DOMRectReadOnly)
 libweb_js_bindings(HTML/CanvasGradient)
 libweb_js_bindings(HTML/CanvasGradient)
+libweb_js_bindings(HTML/CanvasPattern)
 libweb_js_bindings(HTML/CanvasRenderingContext2D)
 libweb_js_bindings(HTML/CanvasRenderingContext2D)
 libweb_js_bindings(HTML/CloseEvent)
 libweb_js_bindings(HTML/CloseEvent)
 libweb_js_bindings(HTML/DOMParser)
 libweb_js_bindings(HTML/DOMParser)