diff --git a/Tests/LibWeb/Ref/canvas-text.html b/Tests/LibWeb/Ref/canvas-text.html new file mode 100644 index 0000000000..d29efeffac --- /dev/null +++ b/Tests/LibWeb/Ref/canvas-text.html @@ -0,0 +1,46 @@ + + + + + + + + + diff --git a/Tests/LibWeb/Ref/reference/canvas-text-ref.html b/Tests/LibWeb/Ref/reference/canvas-text-ref.html new file mode 100644 index 0000000000..968b109850 --- /dev/null +++ b/Tests/LibWeb/Ref/reference/canvas-text-ref.html @@ -0,0 +1,9 @@ + + diff --git a/Tests/LibWeb/Ref/reference/images/canvas-text-ref.png b/Tests/LibWeb/Ref/reference/images/canvas-text-ref.png new file mode 100644 index 0000000000..0684ebcba5 Binary files /dev/null and b/Tests/LibWeb/Ref/reference/images/canvas-text-ref.png differ diff --git a/Userland/Libraries/LibWeb/HTML/CanvasRenderingContext2D.cpp b/Userland/Libraries/LibWeb/HTML/CanvasRenderingContext2D.cpp index 7d769c20d0..229497a219 100644 --- a/Userland/Libraries/LibWeb/HTML/CanvasRenderingContext2D.cpp +++ b/Userland/Libraries/LibWeb/HTML/CanvasRenderingContext2D.cpp @@ -198,7 +198,7 @@ Optional CanvasRenderingContext2D::antialiased_painter return {}; } -void CanvasRenderingContext2D::fill_text(StringView text, float x, float y, Optional max_width) +void CanvasRenderingContext2D::bitmap_font_fill_text(StringView text, float x, float y, Optional max_width) { if (max_width.has_value() && max_width.value() <= 0) return; @@ -207,9 +207,8 @@ void CanvasRenderingContext2D::fill_text(StringView text, float x, float y, Opti auto& drawing_state = this->drawing_state(); auto& base_painter = painter.underlying_painter(); - auto font = current_font(); - // Create text rect from font + auto font = current_font(); auto text_rect = Gfx::FloatRect(x, y, max_width.has_value() ? static_cast(max_width.value()) : font->width(text), font->pixel_size()); // Apply text align to text_rect @@ -242,10 +241,72 @@ void CanvasRenderingContext2D::fill_text(StringView text, float x, float y, Opti }); } +Gfx::Path CanvasRenderingContext2D::text_path(StringView text, float x, float y, Optional max_width) +{ + if (max_width.has_value() && max_width.value() <= 0) + return {}; + + auto& drawing_state = this->drawing_state(); + auto font = current_font(); + + Gfx::Path path; + path.move_to({ x, y }); + path.text(Utf8View { text }, *font); + + auto text_width = path.bounding_box().width(); + Gfx::AffineTransform transform = {}; + + // https://html.spec.whatwg.org/multipage/canvas.html#text-preparation-algorithm: + // 6. If maxWidth was provided and the hypothetical width of the inline box in the hypothetical line box + // is greater than maxWidth CSS pixels, then change font to have a more condensed font (if one is + // available or if a reasonably readable one can be synthesized by applying a horizontal scale + // factor to the font) or a smaller font, and return to the previous step. + if (max_width.has_value() && text_width > float(*max_width)) { + auto horizontal_scale = float(*max_width) / text_width; + transform = Gfx::AffineTransform {}.scale({ horizontal_scale, 1 }); + text_width *= horizontal_scale; + } + + // Apply text align + // FIXME: CanvasTextAlign::Start and CanvasTextAlign::End currently do not nothing for right-to-left languages: + // https://html.spec.whatwg.org/multipage/canvas.html#dom-context-2d-textalign-start + // Default alignment of draw_text is left so do nothing by CanvasTextAlign::Start and CanvasTextAlign::Left + if (drawing_state.text_align == Bindings::CanvasTextAlign::Center) { + transform = Gfx::AffineTransform {}.set_translation({ -text_width / 2, 0 }).multiply(transform); + } + if (drawing_state.text_align == Bindings::CanvasTextAlign::End || drawing_state.text_align == Bindings::CanvasTextAlign::Right) { + transform = Gfx::AffineTransform {}.set_translation({ -text_width, 0 }).multiply(transform); + } + + // Apply text baseline + // FIXME: Implement CanvasTextBasline::Hanging, Bindings::CanvasTextAlign::Alphabetic and Bindings::CanvasTextAlign::Ideographic for real + // right now they are just handled as textBaseline = top or bottom. + // https://html.spec.whatwg.org/multipage/canvas.html#dom-context-2d-textbaseline-hanging + // Default baseline of draw_text is top so do nothing by CanvasTextBaseline::Top and CanvasTextBasline::Hanging + if (drawing_state.text_baseline == Bindings::CanvasTextBaseline::Middle) { + transform = Gfx::AffineTransform {}.set_translation({ 0, font->pixel_size() / 2 }).multiply(transform); + } + if (drawing_state.text_baseline == Bindings::CanvasTextBaseline::Top || drawing_state.text_baseline == Bindings::CanvasTextBaseline::Hanging) { + transform = Gfx::AffineTransform {}.set_translation({ 0, font->pixel_size() }).multiply(transform); + } + + transform = Gfx::AffineTransform { drawing_state.transform }.multiply(transform); + path = path.copy_transformed(transform); + return path; +} + +void CanvasRenderingContext2D::fill_text(StringView text, float x, float y, Optional max_width) +{ + if (is(*current_font())) + return bitmap_font_fill_text(text, x, y, max_width); + fill_internal(text_path(text, x, y, max_width), Gfx::Painter::WindingRule::Nonzero); +} + void CanvasRenderingContext2D::stroke_text(StringView text, float x, float y, Optional max_width) { - // FIXME: Stroke the text instead of filling it. - fill_text(text, x, y, max_width); + if (is(*current_font())) + return bitmap_font_fill_text(text, x, y, max_width); + stroke_internal(text_path(text, x, y, max_width)); } void CanvasRenderingContext2D::begin_path() diff --git a/Userland/Libraries/LibWeb/HTML/CanvasRenderingContext2D.h b/Userland/Libraries/LibWeb/HTML/CanvasRenderingContext2D.h index cce9f5a3f4..7bfc536a7b 100644 --- a/Userland/Libraries/LibWeb/HTML/CanvasRenderingContext2D.h +++ b/Userland/Libraries/LibWeb/HTML/CanvasRenderingContext2D.h @@ -145,6 +145,9 @@ private: Gfx::Path rect_path(float x, float y, float width, float height); + Gfx::Path text_path(StringView text, float x, float y, Optional max_width); + void bitmap_font_fill_text(StringView text, float x, float y, Optional max_width); + void stroke_internal(Gfx::Path const&); void fill_internal(Gfx::Path const&, Gfx::Painter::WindingRule); void clip_internal(Gfx::Path&, Gfx::Painter::WindingRule);