diff --git a/Userland/Libraries/LibWeb/CMakeLists.txt b/Userland/Libraries/LibWeb/CMakeLists.txt index dc7546bf46..4ee4d3af27 100644 --- a/Userland/Libraries/LibWeb/CMakeLists.txt +++ b/Userland/Libraries/LibWeb/CMakeLists.txt @@ -135,6 +135,7 @@ set(SOURCES HTML/AttributeNames.cpp HTML/BrowsingContext.cpp HTML/BrowsingContextContainer.cpp + HTML/Canvas/CanvasPath.cpp HTML/CanvasGradient.cpp HTML/CanvasRenderingContext2D.cpp HTML/CrossOrigin/Reporting.cpp diff --git a/Userland/Libraries/LibWeb/HTML/Canvas/CanvasPath.cpp b/Userland/Libraries/LibWeb/HTML/Canvas/CanvasPath.cpp new file mode 100644 index 0000000000..d87f9cbe9a --- /dev/null +++ b/Userland/Libraries/LibWeb/HTML/Canvas/CanvasPath.cpp @@ -0,0 +1,117 @@ +/* + * Copyright (c) 2020-2022, Andreas Kling + * Copyright (c) 2022, Sam Atkins + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include + +namespace Web::HTML { + +void CanvasPath::close_path() +{ + m_path.close(); +} + +void CanvasPath::move_to(float x, float y) +{ + m_path.move_to({ x, y }); +} + +void CanvasPath::line_to(float x, float y) +{ + m_path.line_to({ x, y }); +} + +void CanvasPath::quadratic_curve_to(float cx, float cy, float x, float y) +{ + m_path.quadratic_bezier_curve_to({ cx, cy }, { x, y }); +} + +void CanvasPath::bezier_curve_to(double cp1x, double cp1y, double cp2x, double cp2y, double x, double y) +{ + m_path.cubic_bezier_curve_to(Gfx::FloatPoint(cp1x, cp1y), Gfx::FloatPoint(cp2x, cp2y), Gfx::FloatPoint(x, y)); +} + +DOM::ExceptionOr CanvasPath::arc(float x, float y, float radius, float start_angle, float end_angle, bool counter_clockwise) +{ + if (radius < 0) + return DOM::IndexSizeError::create(String::formatted("The radius provided ({}) is negative.", radius)); + return ellipse(x, y, radius, radius, 0, start_angle, end_angle, counter_clockwise); +} + +DOM::ExceptionOr CanvasPath::ellipse(float x, float y, float radius_x, float radius_y, float rotation, float start_angle, float end_angle, bool counter_clockwise) +{ + if (radius_x < 0) + return DOM::IndexSizeError::create(String::formatted("The major-axis radius provided ({}) is negative.", radius_x)); + + if (radius_y < 0) + return DOM::IndexSizeError::create(String::formatted("The minor-axis radius provided ({}) is negative.", radius_y)); + + if (constexpr float tau = M_TAU; (!counter_clockwise && (end_angle - start_angle) >= tau) + || (counter_clockwise && (start_angle - end_angle) >= tau)) { + start_angle = 0; + end_angle = tau; + } else { + start_angle = fmodf(start_angle, tau); + end_angle = fmodf(end_angle, tau); + } + + // Then, figure out where the ends of the arc are. + // To do so, we can pretend that the center of this ellipse is at (0, 0), + // and the whole coordinate system is rotated `rotation` radians around the x axis, centered on `center`. + // The sign of the resulting relative positions is just whether our angle is on one of the left quadrants. + auto sin_rotation = sinf(rotation); + auto cos_rotation = cosf(rotation); + + auto resolve_point_with_angle = [&](float angle) { + auto tan_relative = tanf(angle); + auto tan2 = tan_relative * tan_relative; + + auto ab = radius_x * radius_y; + auto a2 = radius_x * radius_x; + auto b2 = radius_y * radius_y; + auto sqrt = sqrtf(b2 + a2 * tan2); + + auto relative_x_position = ab / sqrt; + auto relative_y_position = ab * tan_relative / sqrt; + + // Make sure to set the correct sign + float sn = sinf(angle) >= 0 ? 1 : -1; + relative_x_position *= sn; + relative_y_position *= sn; + + // Now rotate it (back) around the center point by 'rotation' radians, then move it back to our actual origin. + auto relative_rotated_x_position = relative_x_position * cos_rotation - relative_y_position * sin_rotation; + auto relative_rotated_y_position = relative_x_position * sin_rotation + relative_y_position * cos_rotation; + return Gfx::FloatPoint { relative_rotated_x_position + x, relative_rotated_y_position + y }; + }; + + auto start_point = resolve_point_with_angle(start_angle); + auto end_point = resolve_point_with_angle(end_angle); + + m_path.move_to(start_point); + + double delta_theta = end_angle - start_angle; + + // FIXME: This is still goofy for some values. + m_path.elliptical_arc_to(end_point, { radius_x, radius_y }, rotation, delta_theta > M_PI, !counter_clockwise); + + m_path.close(); + return {}; +} + +void CanvasPath::rect(float x, float y, float width, float height) +{ + m_path.move_to({ x, y }); + if (width == 0 || height == 0) + return; + m_path.line_to({ x + width, y }); + m_path.line_to({ x + width, y + height }); + m_path.line_to({ x, y + height }); + m_path.close(); +} + +} diff --git a/Userland/Libraries/LibWeb/HTML/Canvas/CanvasPath.h b/Userland/Libraries/LibWeb/HTML/Canvas/CanvasPath.h new file mode 100644 index 0000000000..0fb4e1fd42 --- /dev/null +++ b/Userland/Libraries/LibWeb/HTML/Canvas/CanvasPath.h @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2022, Sam Atkins + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include + +namespace Web::HTML { + +// https://html.spec.whatwg.org/multipage/canvas.html#canvaspath +class CanvasPath { +public: + ~CanvasPath() = default; + + void close_path(); + + void move_to(float x, float y); + void line_to(float x, float y); + void quadratic_curve_to(float cx, float cy, float x, float y); + void bezier_curve_to(double cp1x, double cp1y, double cp2x, double cp2y, double x, double y); + void rect(float x, float y, float width, float height); + DOM::ExceptionOr arc(float x, float y, float radius, float start_angle, float end_angle, bool counter_clockwise); + DOM::ExceptionOr ellipse(float x, float y, float radius_x, float radius_y, float rotation, float start_angle, float end_angle, bool counter_clockwise); + + Gfx::Path& path() { return m_path; } + Gfx::Path const& path() const { return m_path; } + +protected: + CanvasPath() = default; + +private: + Gfx::Path m_path; +}; + +} diff --git a/Userland/Libraries/LibWeb/HTML/Canvas/CanvasPath.idl b/Userland/Libraries/LibWeb/HTML/Canvas/CanvasPath.idl new file mode 100644 index 0000000000..5ba418a91e --- /dev/null +++ b/Userland/Libraries/LibWeb/HTML/Canvas/CanvasPath.idl @@ -0,0 +1,13 @@ +// https://html.spec.whatwg.org/multipage/canvas.html#canvaspath +interface mixin CanvasPath { + undefined closePath(); + undefined moveTo(unrestricted double x, unrestricted double y); + undefined lineTo(unrestricted double x, unrestricted double y); + undefined quadraticCurveTo(unrestricted double cpx, unrestricted double cpy, unrestricted double x, unrestricted double y); + undefined bezierCurveTo(unrestricted double cp1x, unrestricted double cp1y, unrestricted double cp2x, unrestricted double cp2y, unrestricted double x, unrestricted double y); + // FIXME: undefined arcTo(unrestricted double x1, unrestricted double y1, unrestricted double x2, unrestricted double y2, unrestricted double radius); + undefined rect(unrestricted double x, unrestricted double y, unrestricted double width, unrestricted double height); + // FIXME: undefined roundRect(unrestricted double x, unrestricted double y, unrestricted double w, unrestricted double h, optional (unrestricted double or DOMPointInit or sequence<(unrestricted double or DOMPointInit)>) radii = 0); + undefined arc(unrestricted double x, unrestricted double y, unrestricted double radius, unrestricted double startAngle, unrestricted double endAngle, optional boolean counterclockwise = false); + undefined ellipse(unrestricted double x, unrestricted double y, unrestricted double radiusX, unrestricted double radiusY, unrestricted double rotation, unrestricted double startAngle, unrestricted double endAngle, optional boolean counterclockwise = false); +}; diff --git a/Userland/Libraries/LibWeb/HTML/CanvasRenderingContext2D.cpp b/Userland/Libraries/LibWeb/HTML/CanvasRenderingContext2D.cpp index ae196db494..a8bc6478dd 100644 --- a/Userland/Libraries/LibWeb/HTML/CanvasRenderingContext2D.cpp +++ b/Userland/Libraries/LibWeb/HTML/CanvasRenderingContext2D.cpp @@ -6,7 +6,6 @@ */ #include -#include #include #include #include @@ -317,111 +316,7 @@ void CanvasRenderingContext2D::stroke_text(String const& text, float x, float y, void CanvasRenderingContext2D::begin_path() { - m_path = Gfx::Path(); -} - -void CanvasRenderingContext2D::close_path() -{ - m_path.close(); -} - -void CanvasRenderingContext2D::move_to(float x, float y) -{ - m_path.move_to({ x, y }); -} - -void CanvasRenderingContext2D::line_to(float x, float y) -{ - m_path.line_to({ x, y }); -} - -void CanvasRenderingContext2D::quadratic_curve_to(float cx, float cy, float x, float y) -{ - m_path.quadratic_bezier_curve_to({ cx, cy }, { x, y }); -} - -void CanvasRenderingContext2D::bezier_curve_to(double cp1x, double cp1y, double cp2x, double cp2y, double x, double y) -{ - m_path.cubic_bezier_curve_to(Gfx::FloatPoint(cp1x, cp1y), Gfx::FloatPoint(cp2x, cp2y), Gfx::FloatPoint(x, y)); -} - -DOM::ExceptionOr CanvasRenderingContext2D::arc(float x, float y, float radius, float start_angle, float end_angle, bool counter_clockwise) -{ - if (radius < 0) - return DOM::IndexSizeError::create(String::formatted("The radius provided ({}) is negative.", radius)); - return ellipse(x, y, radius, radius, 0, start_angle, end_angle, counter_clockwise); -} - -DOM::ExceptionOr CanvasRenderingContext2D::ellipse(float x, float y, float radius_x, float radius_y, float rotation, float start_angle, float end_angle, bool counter_clockwise) -{ - if (radius_x < 0) - return DOM::IndexSizeError::create(String::formatted("The major-axis radius provided ({}) is negative.", radius_x)); - - if (radius_y < 0) - return DOM::IndexSizeError::create(String::formatted("The minor-axis radius provided ({}) is negative.", radius_y)); - - if (constexpr float tau = M_TAU; (!counter_clockwise && (end_angle - start_angle) >= tau) - || (counter_clockwise && (start_angle - end_angle) >= tau)) { - start_angle = 0; - end_angle = tau; - } else { - start_angle = fmodf(start_angle, tau); - end_angle = fmodf(end_angle, tau); - } - - // Then, figure out where the ends of the arc are. - // To do so, we can pretend that the center of this ellipse is at (0, 0), - // and the whole coordinate system is rotated `rotation` radians around the x axis, centered on `center`. - // The sign of the resulting relative positions is just whether our angle is on one of the left quadrants. - auto sin_rotation = sinf(rotation); - auto cos_rotation = cosf(rotation); - - auto resolve_point_with_angle = [&](float angle) { - auto tan_relative = tanf(angle); - auto tan2 = tan_relative * tan_relative; - - auto ab = radius_x * radius_y; - auto a2 = radius_x * radius_x; - auto b2 = radius_y * radius_y; - auto sqrt = sqrtf(b2 + a2 * tan2); - - auto relative_x_position = ab / sqrt; - auto relative_y_position = ab * tan_relative / sqrt; - - // Make sure to set the correct sign - float sn = sinf(angle) >= 0 ? 1 : -1; - relative_x_position *= sn; - relative_y_position *= sn; - - // Now rotate it (back) around the center point by 'rotation' radians, then move it back to our actual origin. - auto relative_rotated_x_position = relative_x_position * cos_rotation - relative_y_position * sin_rotation; - auto relative_rotated_y_position = relative_x_position * sin_rotation + relative_y_position * cos_rotation; - return Gfx::FloatPoint { relative_rotated_x_position + x, relative_rotated_y_position + y }; - }; - - auto start_point = resolve_point_with_angle(start_angle); - auto end_point = resolve_point_with_angle(end_angle); - - m_path.move_to(start_point); - - double delta_theta = end_angle - start_angle; - - // FIXME: This is still goofy for some values. - m_path.elliptical_arc_to(end_point, { radius_x, radius_y }, rotation, delta_theta > M_PI, !counter_clockwise); - - m_path.close(); - return {}; -} - -void CanvasRenderingContext2D::rect(float x, float y, float width, float height) -{ - m_path.move_to({ x, y }); - if (width == 0 || height == 0) - return; - m_path.line_to({ x + width, y }); - m_path.line_to({ x + width, y + height }); - m_path.line_to({ x, y + height }); - m_path.close(); + path().clear(); } void CanvasRenderingContext2D::stroke() @@ -430,8 +325,8 @@ void CanvasRenderingContext2D::stroke() if (!painter) return; - painter->stroke_path(m_path, m_drawing_state.stroke_style, m_drawing_state.line_width); - did_draw(m_path.bounding_box()); + painter->stroke_path(path(), m_drawing_state.stroke_style, m_drawing_state.line_width); + did_draw(path().bounding_box()); } void CanvasRenderingContext2D::fill(Gfx::Painter::WindingRule winding) @@ -440,10 +335,10 @@ void CanvasRenderingContext2D::fill(Gfx::Painter::WindingRule winding) if (!painter) return; - auto path = m_path; + auto path = this->path(); path.close_all_subpaths(); painter->fill_path(path, m_drawing_state.fill_style, winding); - did_draw(m_path.bounding_box()); + did_draw(path.bounding_box()); } void CanvasRenderingContext2D::fill(String const& fill_rule) @@ -553,7 +448,7 @@ void CanvasRenderingContext2D::reset_to_default_state() painter->clear_rect(painter->target()->rect(), Color::Transparent); // 2. Empty the list of subpaths in context's current default path. - m_path.clear(); + path().clear(); // 3. Clear the context's drawing state stack. m_drawing_state_stack.clear(); diff --git a/Userland/Libraries/LibWeb/HTML/CanvasRenderingContext2D.h b/Userland/Libraries/LibWeb/HTML/CanvasRenderingContext2D.h index b54dc410de..c225a47110 100644 --- a/Userland/Libraries/LibWeb/HTML/CanvasRenderingContext2D.h +++ b/Userland/Libraries/LibWeb/HTML/CanvasRenderingContext2D.h @@ -16,6 +16,7 @@ #include #include #include +#include #include #include #include @@ -28,7 +29,8 @@ using CanvasImageSource = Variant, NonnullRefPtr class CanvasRenderingContext2D : public RefCountForwarder - , public Bindings::Wrappable { + , public Bindings::Wrappable + , public CanvasPath { AK_MAKE_NONCOPYABLE(CanvasRenderingContext2D); AK_MAKE_NONMOVABLE(CanvasRenderingContext2D); @@ -61,15 +63,6 @@ public: float line_width() const { return m_drawing_state.line_width; } void begin_path(); - void close_path(); - void move_to(float x, float y); - void line_to(float x, float y); - void quadratic_curve_to(float cx, float cy, float x, float y); - void bezier_curve_to(double cp1x, double cp1y, double cp2x, double cp2y, double x, double y); - - DOM::ExceptionOr arc(float x, float y, float radius, float start_angle, float end_angle, bool counter_clockwise); - DOM::ExceptionOr ellipse(float x, float y, float radius_x, float radius_y, float rotation, float start_angle, float end_angle, bool counter_clockwise); - void rect(float x, float y, float width, float height); void stroke(); void fill_text(String const&, float x, float y, Optional max_width); @@ -141,8 +134,6 @@ private: // https://html.spec.whatwg.org/multipage/canvas.html#concept-canvas-context-lost bool m_context_lost { false }; - - Gfx::Path m_path; }; enum class CanvasImageSourceUsability { diff --git a/Userland/Libraries/LibWeb/HTML/CanvasRenderingContext2D.idl b/Userland/Libraries/LibWeb/HTML/CanvasRenderingContext2D.idl index 4d17bf1d15..51c34c3ec0 100644 --- a/Userland/Libraries/LibWeb/HTML/CanvasRenderingContext2D.idl +++ b/Userland/Libraries/LibWeb/HTML/CanvasRenderingContext2D.idl @@ -3,7 +3,10 @@ #import #import #import +#import +// https://html.spec.whatwg.org/multipage/canvas.html#canvasrenderingcontext2d +[Exposed=Window] interface CanvasRenderingContext2D { undefined fillRect(double x, double y, double w, double h); @@ -15,17 +18,8 @@ interface CanvasRenderingContext2D { undefined rotate(double radians); undefined beginPath(); - undefined closePath(); undefined fill(optional DOMString fillRule = "nonzero"); undefined stroke(); - undefined moveTo(double x, double y); - undefined lineTo(double x, double y); - undefined quadraticCurveTo(double cpx, double cpy, double x, double y); - undefined bezierCurveTo(double cp1x, double cp1y, double cp2x, double cp2y, double x, double y); - - undefined arc(double x, double y, double radius, double startAngle, double endAngle, optional boolean counterclockwise = false); - undefined ellipse(double x, double y, double radiusX, double radiusY, double rotation, double startAngle, double endAngle, optional boolean counterclockwise = false); - undefined rect(double x, double y, double width, double height); undefined fillText(DOMString text, double x, double y, optional double maxWidth); undefined strokeText(DOMString text, double x, double y, optional double maxWidth); @@ -65,3 +59,5 @@ interface CanvasRenderingContext2D { undefined clip(); }; + +CanvasRenderingContext2D includes CanvasPath;