1
Fork 0
mirror of https://github.com/RGBCube/serenity synced 2025-07-24 16:47:42 +00:00

LibWeb: Add initial implementation of CRC2D.clip()

This allows clipping your drawing by any path you like! To do this
all painting has been wrapped around a new draw_clipped() helper
method, which handles the clipping before/after painting.

Note: This clipping is currently missing support for intersecting
clip paths.
This commit is contained in:
MacDue 2023-04-06 20:57:59 +01:00 committed by Linus Groh
parent 92d9b6edb8
commit 20e9213cc4
5 changed files with 126 additions and 102 deletions

View file

@ -24,7 +24,8 @@ public:
virtual void stroke() = 0; virtual void stroke() = 0;
virtual void stroke(Path2D const& path) = 0; virtual void stroke(Path2D const& path) = 0;
virtual void clip() = 0; virtual void clip(DeprecatedString const& fill_rule) = 0;
virtual void clip(Path2D& path, DeprecatedString const& fill_rule) = 0;
protected: protected:
CanvasDrawPath() = default; CanvasDrawPath() = default;

View file

@ -13,10 +13,10 @@ interface mixin CanvasDrawPath {
undefined stroke(); undefined stroke();
undefined stroke(Path2D path); undefined stroke(Path2D path);
// FIXME: Replace this with these two definitions: // FIXME: `DOMString` should be `CanvasFillRule`
// undefined clip(optional CanvasFillRule fillRule = "nonzero"); undefined clip(optional DOMString fillRule = "nonzero");
// undefined clip(Path2D path, optional CanvasFillRule fillRule = "nonzero"); // FIXME: `DOMString` should be `CanvasFillRule`
undefined clip(); undefined clip(Path2D path, optional DOMString fillRule = "nonzero");
// FIXME: boolean isPointInPath(unrestricted double x, unrestricted double y, optional CanvasFillRule fillRule = "nonzero"); // FIXME: boolean isPointInPath(unrestricted double x, unrestricted double y, optional CanvasFillRule fillRule = "nonzero");
// FIXME: boolean isPointInPath(Path2D path, unrestricted double x, unrestricted double y, optional CanvasFillRule fillRule = "nonzero"); // FIXME: boolean isPointInPath(Path2D path, unrestricted double x, unrestricted double y, optional CanvasFillRule fillRule = "nonzero");

View file

@ -13,6 +13,7 @@
#include <LibGfx/Color.h> #include <LibGfx/Color.h>
#include <LibGfx/PaintStyle.h> #include <LibGfx/PaintStyle.h>
#include <LibWeb/Bindings/CanvasRenderingContext2DPrototype.h> #include <LibWeb/Bindings/CanvasRenderingContext2DPrototype.h>
#include <LibWeb/HTML/Canvas/CanvasPathClipper.h>
#include <LibWeb/HTML/CanvasGradient.h> #include <LibWeb/HTML/CanvasGradient.h>
#include <LibWeb/HTML/CanvasPattern.h> #include <LibWeb/HTML/CanvasPattern.h>
@ -77,6 +78,7 @@ public:
float line_width { 1 }; float line_width { 1 };
bool image_smoothing_enabled { true }; bool image_smoothing_enabled { true };
Bindings::ImageSmoothingQuality image_smoothing_quality { Bindings::ImageSmoothingQuality::Low }; Bindings::ImageSmoothingQuality image_smoothing_quality { Bindings::ImageSmoothingQuality::Low };
Optional<CanvasClip> clip;
}; };
DrawingState& drawing_state() { return m_drawing_state; } DrawingState& drawing_state() { return m_drawing_state; }
DrawingState const& drawing_state() const { return m_drawing_state; } DrawingState const& drawing_state() const { return m_drawing_state; }

View file

@ -71,58 +71,51 @@ JS::NonnullGCPtr<HTMLCanvasElement> CanvasRenderingContext2D::canvas_for_binding
void CanvasRenderingContext2D::fill_rect(float x, float y, float width, float height) void CanvasRenderingContext2D::fill_rect(float x, float y, float width, float height)
{ {
auto painter = this->antialiased_painter(); draw_clipped([&](auto& painter) {
if (!painter.has_value()) auto& drawing_state = this->drawing_state();
return; auto rect = drawing_state.transform.map(Gfx::FloatRect(x, y, width, height));
auto color_fill = drawing_state.fill_style.as_color();
auto& drawing_state = this->drawing_state(); if (color_fill.has_value()) {
painter.fill_rect(rect, *color_fill);
auto rect = drawing_state.transform.map(Gfx::FloatRect(x, y, width, height)); } else {
auto color_fill = drawing_state.fill_style.as_color(); // FIXME: This should use AntiAliasingPainter::fill_rect() too but that does not support FillPath yet.
if (color_fill.has_value()) { painter.underlying_painter().fill_rect(rect.to_rounded<int>(), *drawing_state.fill_style.to_gfx_paint_style());
painter->fill_rect(rect, *color_fill); }
} else { return rect;
// FIXME: This should use AntiAliasingPainter::fill_rect() too but that does not support FillPath yet. });
painter->underlying_painter().fill_rect(rect.to_rounded<int>(), *drawing_state.fill_style.to_gfx_paint_style());
}
did_draw(rect);
} }
void CanvasRenderingContext2D::clear_rect(float x, float y, float width, float height) void CanvasRenderingContext2D::clear_rect(float x, float y, float width, float height)
{ {
auto painter = this->painter(); draw_clipped([&](auto& painter) {
if (!painter) auto rect = drawing_state().transform.map(Gfx::FloatRect(x, y, width, height));
return; painter.underlying_painter().clear_rect(enclosing_int_rect(rect), Color());
return rect;
auto rect = drawing_state().transform.map(Gfx::FloatRect(x, y, width, height)); });
painter->clear_rect(enclosing_int_rect(rect), Color());
did_draw(rect);
} }
void CanvasRenderingContext2D::stroke_rect(float x, float y, float width, float height) void CanvasRenderingContext2D::stroke_rect(float x, float y, float width, float height)
{ {
auto painter = this->antialiased_painter(); draw_clipped([&](auto& painter) {
if (!painter.has_value()) auto& drawing_state = this->drawing_state();
return;
auto& drawing_state = this->drawing_state(); auto rect = drawing_state.transform.map(Gfx::FloatRect(x, y, width, height));
// We could remove the rounding here, but the lines look better when they have whole number pixel endpoints.
auto top_left = drawing_state.transform.map(Gfx::FloatPoint(x, y)).to_rounded<float>();
auto top_right = drawing_state.transform.map(Gfx::FloatPoint(x + width - 1, y)).to_rounded<float>();
auto bottom_left = drawing_state.transform.map(Gfx::FloatPoint(x, y + height - 1)).to_rounded<float>();
auto bottom_right = drawing_state.transform.map(Gfx::FloatPoint(x + width - 1, y + height - 1)).to_rounded<float>();
auto rect = drawing_state.transform.map(Gfx::FloatRect(x, y, width, height)); Gfx::Path path;
// We could remove the rounding here, but the lines look better when they have whole number pixel endpoints. path.move_to(top_left);
auto top_left = drawing_state.transform.map(Gfx::FloatPoint(x, y)).to_rounded<float>(); path.line_to(top_right);
auto top_right = drawing_state.transform.map(Gfx::FloatPoint(x + width - 1, y)).to_rounded<float>(); path.line_to(bottom_right);
auto bottom_left = drawing_state.transform.map(Gfx::FloatPoint(x, y + height - 1)).to_rounded<float>(); path.line_to(bottom_left);
auto bottom_right = drawing_state.transform.map(Gfx::FloatPoint(x + width - 1, y + height - 1)).to_rounded<float>(); path.line_to(top_left);
painter.stroke_path(path, drawing_state.stroke_style.to_color_but_fixme_should_accept_any_paint_style(), drawing_state.line_width);
Gfx::Path path; return rect;
path.move_to(top_left); });
path.line_to(top_right);
path.line_to(bottom_right);
path.line_to(bottom_left);
path.line_to(top_left);
painter->stroke_path(path, drawing_state.stroke_style.to_color_but_fixme_should_accept_any_paint_style(), drawing_state.line_width);
did_draw(rect);
} }
// 4.12.5.1.14 Drawing images, https://html.spec.whatwg.org/multipage/canvas.html#drawing-images // 4.12.5.1.14 Drawing images, https://html.spec.whatwg.org/multipage/canvas.html#drawing-images
@ -170,20 +163,21 @@ WebIDL::ExceptionOr<void> CanvasRenderingContext2D::draw_image_internal(CanvasIm
return {}; return {};
// 6. Paint the region of the image argument specified by the source rectangle on the region of the rendering context's output bitmap specified by the destination rectangle, after applying the current transformation matrix to the destination rectangle. // 6. Paint the region of the image argument specified by the source rectangle on the region of the rendering context's output bitmap specified by the destination rectangle, after applying the current transformation matrix to the destination rectangle.
auto painter = this->painter(); draw_clipped([&](auto& painter) {
if (!painter) auto scaling_mode = Gfx::Painter::ScalingMode::NearestNeighbor;
return {}; if (drawing_state().image_smoothing_enabled) {
// FIXME: Honor drawing_state().image_smoothing_quality
scaling_mode = Gfx::Painter::ScalingMode::BilinearBlend;
}
auto scaling_mode = Gfx::Painter::ScalingMode::NearestNeighbor; painter.underlying_painter().draw_scaled_bitmap_with_transform(destination_rect.to_rounded<int>(), *bitmap, source_rect, drawing_state().transform, 1.0f, scaling_mode);
if (drawing_state().image_smoothing_enabled) {
// FIXME: Honor drawing_state().image_smoothing_quality
scaling_mode = Gfx::Painter::ScalingMode::BilinearBlend;
}
painter->draw_scaled_bitmap_with_transform(destination_rect.to_rounded<int>(), *bitmap, source_rect, drawing_state().transform, 1.0f, scaling_mode);
// 7. If image is not origin-clean, then set the CanvasRenderingContext2D's origin-clean flag to false. // 7. If image is not origin-clean, then set the CanvasRenderingContext2D's origin-clean flag to false.
if (image_is_not_origin_clean(image)) if (image_is_not_origin_clean(image))
m_origin_clean = false; m_origin_clean = false;
return destination_rect;
});
return {}; return {};
} }
@ -219,16 +213,14 @@ void CanvasRenderingContext2D::fill_text(DeprecatedString const& text, float x,
if (max_width.has_value() && max_width.value() <= 0) if (max_width.has_value() && max_width.value() <= 0)
return; return;
auto painter = this->painter(); draw_clipped([&](auto& painter) {
if (!painter) auto& drawing_state = this->drawing_state();
return; auto& base_painter = painter.underlying_painter();
auto text_rect = Gfx::FloatRect(x, y, max_width.has_value() ? static_cast<float>(max_width.value()) : base_painter.font().width(text), base_painter.font().pixel_size());
auto& drawing_state = this->drawing_state(); auto transformed_rect = drawing_state.transform.map(text_rect);
base_painter.draw_text(transformed_rect, text, Gfx::TextAlignment::TopLeft, drawing_state.fill_style.to_color_but_fixme_should_accept_any_paint_style());
auto text_rect = Gfx::FloatRect(x, y, max_width.has_value() ? static_cast<float>(max_width.value()) : painter->font().width(text), painter->font().pixel_size()); return transformed_rect;
auto transformed_rect = drawing_state.transform.map(text_rect); });
painter->draw_text(transformed_rect, text, Gfx::TextAlignment::TopLeft, drawing_state.fill_style.to_color_but_fixme_should_accept_any_paint_style());
did_draw(transformed_rect);
} }
void CanvasRenderingContext2D::stroke_text(DeprecatedString const& text, float x, float y, Optional<double> max_width) void CanvasRenderingContext2D::stroke_text(DeprecatedString const& text, float x, float y, Optional<double> max_width)
@ -244,14 +236,12 @@ void CanvasRenderingContext2D::begin_path()
void CanvasRenderingContext2D::stroke_internal(Gfx::Path const& path) void CanvasRenderingContext2D::stroke_internal(Gfx::Path const& path)
{ {
auto painter = this->antialiased_painter(); draw_clipped([&](auto& painter) {
if (!painter.has_value()) auto& drawing_state = this->drawing_state();
return;
auto& drawing_state = this->drawing_state(); painter.stroke_path(path, drawing_state.stroke_style.to_color_but_fixme_should_accept_any_paint_style(), drawing_state.line_width);
return path.bounding_box();
painter->stroke_path(path, drawing_state.stroke_style.to_color_but_fixme_should_accept_any_paint_style(), drawing_state.line_width); });
did_draw(path.bounding_box());
} }
void CanvasRenderingContext2D::stroke() void CanvasRenderingContext2D::stroke()
@ -266,24 +256,23 @@ void CanvasRenderingContext2D::stroke(Path2D const& path)
stroke_internal(transformed_path); stroke_internal(transformed_path);
} }
void CanvasRenderingContext2D::fill_internal(Gfx::Path& path, DeprecatedString const& fill_rule) static Gfx::Painter::WindingRule parse_fill_rule(StringView fill_rule)
{ {
auto painter = this->antialiased_painter(); if (fill_rule == "evenodd"sv)
if (!painter.has_value()) return Gfx::Painter::WindingRule::EvenOdd;
return; if (fill_rule == "nonzero"sv)
return Gfx::Painter::WindingRule::Nonzero;
dbgln("Unrecognized fillRule for CRC2D.fill() - this problem goes away once we pass an enum instead of a string");
return Gfx::Painter::WindingRule::Nonzero;
}
path.close_all_subpaths(); void CanvasRenderingContext2D::fill_internal(Gfx::Path& path, StringView fill_rule)
{
auto winding = Gfx::Painter::WindingRule::Nonzero; draw_clipped([&](auto& painter) {
if (fill_rule == "evenodd") path.close_all_subpaths();
winding = Gfx::Painter::WindingRule::EvenOdd; painter.fill_path(path, *drawing_state().fill_style.to_gfx_paint_style(), parse_fill_rule(fill_rule));
else if (fill_rule == "nonzero") return path.bounding_box();
winding = Gfx::Painter::WindingRule::Nonzero; });
else
dbgln("Unrecognized fillRule for CRC2D.fill() - this problem goes away once we pass an enum instead of a string");
painter->fill_path(path, *drawing_state().fill_style.to_gfx_paint_style(), winding);
did_draw(path.bounding_box());
} }
void CanvasRenderingContext2D::fill(DeprecatedString const& fill_rule) void CanvasRenderingContext2D::fill(DeprecatedString const& fill_rule)
@ -345,13 +334,10 @@ WebIDL::ExceptionOr<JS::GCPtr<ImageData>> CanvasRenderingContext2D::get_image_da
void CanvasRenderingContext2D::put_image_data(ImageData const& image_data, float x, float y) void CanvasRenderingContext2D::put_image_data(ImageData const& image_data, float x, float y)
{ {
auto painter = this->painter(); draw_clipped([&](auto& painter) {
if (!painter) painter.underlying_painter().blit(Gfx::IntPoint(x, y), image_data.bitmap(), image_data.bitmap().rect());
return; return Gfx::FloatRect(x, y, image_data.width(), image_data.height());
});
painter->blit(Gfx::IntPoint(x, y), image_data.bitmap(), image_data.bitmap().rect());
did_draw(Gfx::FloatRect(x, y, image_data.width(), image_data.height()));
} }
// https://html.spec.whatwg.org/multipage/canvas.html#reset-the-rendering-context-to-its-default-state // https://html.spec.whatwg.org/multipage/canvas.html#reset-the-rendering-context-to-its-default-state
@ -503,9 +489,27 @@ CanvasRenderingContext2D::PreparedText CanvasRenderingContext2D::prepare_text(De
return prepared_text; return prepared_text;
} }
void CanvasRenderingContext2D::clip() void CanvasRenderingContext2D::clip_internal(Gfx::Path& path, StringView fill_rule)
{ {
// FIXME: Implement. // FIXME: This should calculate the new clip path by intersecting the given path with the current one.
// See: https://html.spec.whatwg.org/multipage/canvas.html#dom-context-2d-clip-dev
path.close_all_subpaths();
if (drawing_state().clip.has_value()) {
dbgln("FIXME: CRC2D: Calculate the new clip path by intersecting the given path with the current one.");
}
drawing_state().clip = CanvasClip { path, parse_fill_rule(fill_rule) };
}
void CanvasRenderingContext2D::clip(DeprecatedString const& fill_rule)
{
auto transformed_path = path().copy_transformed(drawing_state().transform);
return clip_internal(transformed_path, fill_rule);
}
void CanvasRenderingContext2D::clip(Path2D& path, DeprecatedString const& fill_rule)
{
auto transformed_path = path.path().copy_transformed(drawing_state().transform);
return clip_internal(transformed_path, fill_rule);
} }
// https://html.spec.whatwg.org/multipage/canvas.html#check-the-usability-of-the-image-argument // https://html.spec.whatwg.org/multipage/canvas.html#check-the-usability-of-the-image-argument

View file

@ -22,6 +22,7 @@
#include <LibWeb/HTML/Canvas/CanvasImageData.h> #include <LibWeb/HTML/Canvas/CanvasImageData.h>
#include <LibWeb/HTML/Canvas/CanvasImageSmoothing.h> #include <LibWeb/HTML/Canvas/CanvasImageSmoothing.h>
#include <LibWeb/HTML/Canvas/CanvasPath.h> #include <LibWeb/HTML/Canvas/CanvasPath.h>
#include <LibWeb/HTML/Canvas/CanvasPathClipper.h>
#include <LibWeb/HTML/Canvas/CanvasPathDrawingStyles.h> #include <LibWeb/HTML/Canvas/CanvasPathDrawingStyles.h>
#include <LibWeb/HTML/Canvas/CanvasRect.h> #include <LibWeb/HTML/Canvas/CanvasRect.h>
#include <LibWeb/HTML/Canvas/CanvasState.h> #include <LibWeb/HTML/Canvas/CanvasState.h>
@ -84,7 +85,8 @@ public:
virtual JS::NonnullGCPtr<TextMetrics> measure_text(DeprecatedString const& text) override; virtual JS::NonnullGCPtr<TextMetrics> measure_text(DeprecatedString const& text) override;
virtual void clip() override; virtual void clip(DeprecatedString const& fill_rule) override;
virtual void clip(Path2D& path, DeprecatedString const& fill_rule) override;
virtual bool image_smoothing_enabled() const override; virtual bool image_smoothing_enabled() const override;
virtual void set_image_smoothing_enabled(bool) override; virtual void set_image_smoothing_enabled(bool) override;
@ -109,6 +111,20 @@ private:
}; };
void did_draw(Gfx::FloatRect const&); void did_draw(Gfx::FloatRect const&);
template<typename TDrawFunction>
void draw_clipped(TDrawFunction draw_function)
{
auto painter = this->antialiased_painter();
if (!painter.has_value())
return;
ScopedCanvasPathClip clipper(painter->underlying_painter(), drawing_state().clip);
auto draw_rect = draw_function(*painter);
if (drawing_state().clip.has_value())
draw_rect.intersect(drawing_state().clip->path.bounding_box());
did_draw(draw_rect);
}
PreparedText prepare_text(DeprecatedString const& text, float max_width = INFINITY); PreparedText prepare_text(DeprecatedString const& text, float max_width = INFINITY);
Gfx::Painter* painter(); Gfx::Painter* painter();
@ -118,7 +134,8 @@ private:
HTMLCanvasElement const& canvas_element() const; HTMLCanvasElement const& canvas_element() const;
void stroke_internal(Gfx::Path const&); void stroke_internal(Gfx::Path const&);
void fill_internal(Gfx::Path&, DeprecatedString const& fill_rule); void fill_internal(Gfx::Path&, StringView fill_rule);
void clip_internal(Gfx::Path&, StringView fill_rule);
JS::NonnullGCPtr<HTMLCanvasElement> m_element; JS::NonnullGCPtr<HTMLCanvasElement> m_element;
OwnPtr<Gfx::Painter> m_painter; OwnPtr<Gfx::Painter> m_painter;