LibWeb: Add Canvas Context2D basic font property support

This commit is contained in:
Bastiaan van der Plaat 2023-08-07 21:48:49 +02:00 committed by Andreas Kling
parent b05fe22d39
commit bba14f6014
Notes: sideshowbarker 2024-07-17 03:19:14 +09:00
7 changed files with 181 additions and 27 deletions

View file

@ -7,15 +7,110 @@
<body>
<h1>Canvas Text Examples</h1>
<p><i>The red boxes are the measured text rects</i></p>
<em>Canvas text-align</em><br>
<canvas id="canvas0" style="border: 1px solid black;"></canvas><br><br>
<em>Canvas font size</em><br>
<canvas id="canvas0" width="600" height="400" style="border: 1px solid black;"></canvas><br><br>
<script>
(function () {
const canvas = document.getElementById('canvas0');
const ctx = canvas.getContext('2d');
ctx.textBaseline = 'top';
ctx.textAlign = 'left';
ctx.strokeStyle = '#f00';
let y = 8;
for (let fontSize = 8; fontSize <= 64; fontSize += 8) {
ctx.font = `${fontSize}px sans-serif`;
const text = `Font size: ${fontSize}px`;
ctx.fillText(text, 8, y);
ctx.strokeRect(8, y, ctx.measureText(text).width, fontSize);
y += fontSize + 8;
}
})();
</script>
<em>Canvas font family</em><br>
<canvas id="canvas1" width="600" height="250" style="border: 1px solid black;"></canvas><br><br>
<script>
(function () {
const canvas = document.getElementById('canvas1');
const ctx = canvas.getContext('2d');
ctx.textBaseline = 'top';
ctx.textAlign = 'left';
ctx.strokeStyle = '#f00';
const families = ['monospace', 'serif', 'fantasy', 'sans-serif', 'cursive'];
let y = 8;
for (const family of families) {
ctx.font = `32px ${family}`;
const text = `Font family: ${family}`;
ctx.fillText(text, 8, y);
ctx.strokeRect(8, y, ctx.measureText(text).width, 32);
y += 32 + 8;
}
})();
</script>
<em>Canvas font weight</em><br>
<canvas id="canvas2" width="600" height="400" style="border: 1px solid black;"></canvas><br><br>
<script>
(function () {
const canvas = document.getElementById('canvas2');
const ctx = canvas.getContext('2d');
ctx.textBaseline = 'top';
ctx.textAlign = 'left';
ctx.strokeStyle = '#f00';
let y = 8;
for (let fontWeight = 100; fontWeight <= 900; fontWeight += 100) {
ctx.font = `${fontWeight} 32px sans-serif`;
const text = `Font weight: ${fontWeight}`;
ctx.fillText(text, 8, y);
ctx.strokeRect(8, y, ctx.measureText(text).width, 32);
y += 32 + 8;
}
})();
</script>
<em>Canvas font style</em><br>
<canvas id="canvas3" width="600" height="150" style="border: 1px solid black;"></canvas><br><br>
<script>
(function () {
const canvas = document.getElementById('canvas3');
const ctx = canvas.getContext('2d');
ctx.textBaseline = 'top';
ctx.textAlign = 'left';
ctx.strokeStyle = '#f00';
const styles = ['normal', 'italic', 'oblique'];
let y = 8;
for (const style of styles) {
ctx.font = `${style} 32px sans-serif`;
const text = `Font style: ${style}`;
ctx.fillText(text, 8, y);
ctx.strokeRect(8, y, ctx.measureText(text).width, 32);
y += 32 + 8;
}
})();
</script>
<em>Canvas text-align</em><br>
<canvas id="canvas4" style="border: 1px solid black;"></canvas><br><br>
<script>
(function () {
const canvas = document.getElementById('canvas4');
const ctx = canvas.getContext('2d');
ctx.strokeStyle = 'red';
ctx.beginPath();
ctx.moveTo(canvas.width / 2, 0);
@ -36,11 +131,11 @@
</script>
<em>Canvas text-baseline</em><br>
<canvas id="canvas1" width="1000" style="border: 1px solid black;"></canvas><br><br>
<canvas id="canvas5" width="1000" style="border: 1px solid black;"></canvas><br><br>
<script>
(function () {
const canvas = document.getElementById('canvas1');
const canvas = document.getElementById('canvas5');
const ctx = canvas.getContext('2d');
ctx.strokeStyle = 'red';

View file

@ -2293,6 +2293,8 @@ CSSPixels StyleComputer::parent_or_root_element_line_height(DOM::Element const*
if (!parent_element)
return m_root_element_font_metrics.line_height;
auto const* computed_values = parent_element->computed_css_values();
if (!computed_values)
return m_root_element_font_metrics.line_height;
auto parent_font_pixel_metrics = computed_values->computed_font().pixel_metrics();
auto parent_font_size = computed_values->property(CSS::PropertyID::FontSize)->as_length().length();
// FIXME: Can the parent font size be non-absolute here?

View file

@ -80,6 +80,8 @@ public:
Bindings::ImageSmoothingQuality image_smoothing_quality { Bindings::ImageSmoothingQuality::Low };
float global_alpha = { 1 };
Optional<CanvasClip> clip;
RefPtr<CSS::StyleValue> font_style_value { nullptr };
RefPtr<Gfx::Font const> current_font { nullptr };
Bindings::CanvasTextAlign text_align { Bindings::CanvasTextAlign::Start };
Bindings::CanvasTextBaseline text_baseline { Bindings::CanvasTextBaseline::Alphabetic };
};

View file

@ -6,6 +6,10 @@
#pragma once
#include <LibWeb/CSS/Parser/Parser.h>
#include <LibWeb/CSS/StyleComputer.h>
#include <LibWeb/CSS/StyleValues/FontStyleValue.h>
#include <LibWeb/DOM/Document.h>
#include <LibWeb/HTML/Canvas/CanvasState.h>
namespace Web::HTML {
@ -16,11 +20,47 @@ class CanvasTextDrawingStyles {
public:
~CanvasTextDrawingStyles() = default;
void set_text_align(Bindings::CanvasTextAlign text_align) { my_drawing_state().text_align = text_align; }
Bindings::CanvasTextAlign text_align() const { return my_drawing_state().text_align; }
DeprecatedString font() const
{
// When font style value is empty return default string
if (!my_drawing_state().font_style_value) {
return "10px sans-serif";
}
// On getting, the font attribute must return the serialized form of the current font of the context (with no 'line-height' component).
auto const& font_style_value = my_drawing_state().font_style_value->as_font();
return DeprecatedString::formatted("{} {} {} {}",
font_style_value.font_style()->to_string().release_value_but_fixme_should_propagate_errors(),
font_style_value.font_weight()->to_string().release_value_but_fixme_should_propagate_errors(),
font_style_value.font_size()->to_string().release_value_but_fixme_should_propagate_errors(),
font_style_value.font_families()->to_string().release_value_but_fixme_should_propagate_errors());
}
void set_font(DeprecatedString const& font)
{
// The font IDL attribute, on setting, must be parsed as a CSS <'font'> value (but without supporting property-independent style sheet syntax like 'inherit'),
// and the resulting font must be assigned to the context, with the 'line-height' component forced to 'normal', with the 'font-size' component converted to CSS pixels,
// and with system fonts being computed to explicit values.
auto parsing_context = CSS::Parser::ParsingContext { reinterpret_cast<IncludingClass&>(*this).realm() };
auto font_style_value_result = parse_css_value(parsing_context, font, CSS::PropertyID::Font);
// If the new value is syntactically incorrect (including using property-independent style sheet syntax like 'inherit' or 'initial'), then it must be ignored, without assigning a new font value.
if (font_style_value_result.is_error()) {
return;
}
my_drawing_state().font_style_value = font_style_value_result.value();
// Load font with font style value properties
auto const& font_style_value = my_drawing_state().font_style_value->as_font();
auto& canvas_element = reinterpret_cast<IncludingClass&>(*this).canvas_element();
my_drawing_state().current_font = canvas_element.document().style_computer().compute_font_for_style_values(&canvas_element, {}, font_style_value.font_families(), font_style_value.font_size(), font_style_value.font_style(), font_style_value.font_weight(), font_style_value.font_stretch());
}
Bindings::CanvasTextAlign text_align() const { return my_drawing_state().text_align; }
void set_text_align(Bindings::CanvasTextAlign text_align) { my_drawing_state().text_align = text_align; }
void set_text_baseline(Bindings::CanvasTextBaseline text_baseline) { my_drawing_state().text_baseline = text_baseline; }
Bindings::CanvasTextBaseline text_baseline() const { return my_drawing_state().text_baseline; }
void set_text_baseline(Bindings::CanvasTextBaseline text_baseline) { my_drawing_state().text_baseline = text_baseline; }
protected:
CanvasTextDrawingStyles() = default;

View file

@ -9,7 +9,7 @@ enum CanvasTextRendering { "auto", "optimizeSpeed", "optimizeLegibility", "geome
// https://html.spec.whatwg.org/multipage/canvas.html#canvastextdrawingstyles
interface mixin CanvasTextDrawingStyles {
// FIXME: attribute DOMString font; // (default 10px sans-serif)
attribute DOMString font; // (default 10px sans-serif)
attribute CanvasTextAlign textAlign; // (default: "start")
attribute CanvasTextBaseline textBaseline; // (default: "alphabetic")
// FIXME: attribute CanvasDirection direction; // (default: "inherit")

View file

@ -204,7 +204,10 @@ void CanvasRenderingContext2D::fill_text(DeprecatedString const& text, float x,
auto& drawing_state = this->drawing_state();
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 font = current_font();
// Create text rect from font
auto text_rect = Gfx::FloatRect(x, y, max_width.has_value() ? static_cast<float>(max_width.value()) : font->width(text), font->pixel_size());
// Apply text align to text_rect
// FIXME: CanvasTextAlign::Start and CanvasTextAlign::End currently do not nothing for right-to-left languages:
@ -223,15 +226,15 @@ void CanvasRenderingContext2D::fill_text(DeprecatedString const& text, float x,
// 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) {
text_rect.translate_by(0, -base_painter.font().pixel_size() / 2);
text_rect.translate_by(0, -font->pixel_size() / 2);
}
if (drawing_state.text_baseline == Bindings::CanvasTextBaseline::Alphabetic || drawing_state.text_baseline == Bindings::CanvasTextBaseline::Ideographic || drawing_state.text_baseline == Bindings::CanvasTextBaseline::Bottom) {
text_rect.translate_by(0, -base_painter.font().pixel_size());
text_rect.translate_by(0, -font->pixel_size());
}
auto transformed_rect = drawing_state.transform.map(text_rect);
auto color = drawing_state.fill_style.to_color_but_fixme_should_accept_any_paint_style();
base_painter.draw_text(transformed_rect, text, Gfx::TextAlignment::TopLeft, color.with_opacity(drawing_state.global_alpha));
base_painter.draw_text(transformed_rect, text, *font, Gfx::TextAlignment::TopLeft, color.with_opacity(drawing_state.global_alpha));
return transformed_rect;
});
}
@ -393,7 +396,7 @@ JS::NonnullGCPtr<TextMetrics> CanvasRenderingContext2D::measure_text(DeprecatedS
auto prepared_text = prepare_text(text);
auto metrics = TextMetrics::create(realm()).release_value_but_fixme_should_propagate_errors();
// FIXME: Use the font that was used to create the glyphs in prepared_text.
auto& font = Platform::FontPlugin::the().default_font();
auto font = current_font();
// width attribute: The width of that inline box, in CSS pixels. (The text's advance width.)
metrics->set_width(prepared_text.bounding_box.width());
@ -402,19 +405,19 @@ JS::NonnullGCPtr<TextMetrics> CanvasRenderingContext2D::measure_text(DeprecatedS
// actualBoundingBoxRight attribute: The distance parallel to the baseline from the alignment point given by the textAlign attribute to the right side of the bounding rectangle of the given text, in CSS pixels; positive numbers indicating a distance going right from the given alignment point.
metrics->set_actual_bounding_box_right(prepared_text.bounding_box.right());
// fontBoundingBoxAscent attribute: The distance from the horizontal line indicated by the textBaseline attribute to the ascent metric of the first available font, in CSS pixels; positive numbers indicating a distance going up from the given baseline.
metrics->set_font_bounding_box_ascent(font.baseline());
metrics->set_font_bounding_box_ascent(font->baseline());
// fontBoundingBoxDescent attribute: The distance from the horizontal line indicated by the textBaseline attribute to the descent metric of the first available font, in CSS pixels; positive numbers indicating a distance going down from the given baseline.
metrics->set_font_bounding_box_descent(prepared_text.bounding_box.height() - font.baseline());
metrics->set_font_bounding_box_descent(prepared_text.bounding_box.height() - font->baseline());
// actualBoundingBoxAscent attribute: The distance from the horizontal line indicated by the textBaseline attribute to the top of the bounding rectangle of the given text, in CSS pixels; positive numbers indicating a distance going up from the given baseline.
metrics->set_actual_bounding_box_ascent(font.baseline());
metrics->set_actual_bounding_box_ascent(font->baseline());
// actualBoundingBoxDescent attribute: The distance from the horizontal line indicated by the textBaseline attribute to the bottom of the bounding rectangle of the given text, in CSS pixels; positive numbers indicating a distance going down from the given baseline.
metrics->set_actual_bounding_box_descent(prepared_text.bounding_box.height() - font.baseline());
metrics->set_actual_bounding_box_descent(prepared_text.bounding_box.height() - font->baseline());
// emHeightAscent attribute: The distance from the horizontal line indicated by the textBaseline attribute to the highest top of the em squares in the inline box, in CSS pixels; positive numbers indicating that the given baseline is below the top of that em square (so this value will usually be positive). Zero if the given baseline is the top of that em square; half the font size if the given baseline is the middle of that em square.
metrics->set_em_height_ascent(font.baseline());
metrics->set_em_height_ascent(font->baseline());
// emHeightDescent attribute: The distance from the horizontal line indicated by the textBaseline attribute to the lowest bottom of the em squares in the inline box, in CSS pixels; positive numbers indicating that the given baseline is above the bottom of that em square. (Zero if the given baseline is the bottom of that em square.)
metrics->set_em_height_descent(prepared_text.bounding_box.height() - font.baseline());
metrics->set_em_height_descent(prepared_text.bounding_box.height() - font->baseline());
// hangingBaseline attribute: The distance from the horizontal line indicated by the textBaseline attribute to the hanging baseline of the inline box, in CSS pixels; positive numbers indicating that the given baseline is below the hanging baseline. (Zero if the given baseline is the hanging baseline.)
metrics->set_hanging_baseline(font.baseline());
metrics->set_hanging_baseline(font->baseline());
// alphabeticBaseline attribute: The distance from the horizontal line indicated by the textBaseline attribute to the alphabetic baseline of the inline box, in CSS pixels; positive numbers indicating that the given baseline is below the alphabetic baseline. (Zero if the given baseline is the alphabetic baseline.)
metrics->set_font_bounding_box_ascent(0);
// ideographicBaseline attribute: The distance from the horizontal line indicated by the textBaseline attribute to the ideographic-under baseline of the inline box, in CSS pixels; positive numbers indicating that the given baseline is below the ideographic-under baseline. (Zero if the given baseline is the ideographic-under baseline.)
@ -423,6 +426,17 @@ JS::NonnullGCPtr<TextMetrics> CanvasRenderingContext2D::measure_text(DeprecatedS
return metrics;
}
RefPtr<Gfx::Font const> CanvasRenderingContext2D::current_font()
{
// When font style value is empty load default font
if (!drawing_state().font_style_value) {
set_font("10px sans-serif");
}
// Get current loaded font
return drawing_state().current_font;
}
// https://html.spec.whatwg.org/multipage/canvas.html#text-preparation-algorithm
CanvasRenderingContext2D::PreparedText CanvasRenderingContext2D::prepare_text(DeprecatedString const& text, float max_width)
{
@ -439,7 +453,7 @@ CanvasRenderingContext2D::PreparedText CanvasRenderingContext2D::prepare_text(De
auto replaced_text = builder.to_deprecated_string();
// 3. Let font be the current font of target, as given by that object's font attribute.
// FIXME: Once we have CanvasTextDrawingStyles, implement font selection.
auto font = current_font();
// 4. Apply the appropriate step from the following list to determine the value of direction:
// 4.1. If the target object's direction attribute has the value "ltr": Let direction be 'ltr'.
@ -462,13 +476,12 @@ CanvasRenderingContext2D::PreparedText CanvasRenderingContext2D::prepare_text(De
// ...and with all other properties set to their initial values.
// FIXME: Actually use a LineBox here instead of, you know, using the default font and measuring its size (which is not the spec at all).
// FIXME: Once we have CanvasTextDrawingStyles, add the CSS attributes.
auto& font = Platform::FontPlugin::the().default_font();
size_t width = 0;
size_t height = font.pixel_size();
size_t height = font->pixel_size();
Utf8View replaced_text_view { replaced_text };
for (auto it = replaced_text_view.begin(); it != replaced_text_view.end(); ++it)
width += font.glyph_or_emoji_width(it);
width += font->glyph_or_emoji_width(it);
// 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.
// FIXME: Record the font size used for this piece of text, and actually retry with a smaller size if needed.

View file

@ -100,6 +100,9 @@ public:
virtual float global_alpha() const override;
virtual void set_global_alpha(float) override;
HTMLCanvasElement& canvas_element();
HTMLCanvasElement const& canvas_element() const;
private:
explicit CanvasRenderingContext2D(JS::Realm&, HTMLCanvasElement&);
@ -132,14 +135,13 @@ private:
did_draw(draw_rect);
}
RefPtr<Gfx::Font const> current_font();
PreparedText prepare_text(DeprecatedString const& text, float max_width = INFINITY);
Gfx::Painter* painter();
Optional<Gfx::AntiAliasingPainter> antialiased_painter();
HTMLCanvasElement& canvas_element();
HTMLCanvasElement const& canvas_element() const;
Gfx::Path rect_path(float x, float y, float width, float height);
void stroke_internal(Gfx::Path const&);