Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Extend Canvas TextMetrics for editing and text styling #11000

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

AndresRPerez12
Copy link

@AndresRPerez12 AndresRPerez12 commented Feb 5, 2025

This change adds the following functionalities to TextMetrics objects:

  • Get the set of rectangles that the user agent would render as the selection background when a particular character range of the measured text is selected.
  • Get the rectangle corresponding to the actual bounding box for a character range of the measured text. This is the equivalent to TextMetrics.actualBoundingBox restricted to the given range.
  • Get the string index for the character at a given offset distance from the start position of the measured text.
  • Split a character range of the measured text into grapheme clusters. That is, for the given range, it returns the minimal logical units of text, each of which can be rendered, along with their corresponding positional data.

Additionally, a new method is added to the Canvas 2D rendering context to render these clusters. When rendering, all relevant CanvasTextDrawingStyles are used as they were when the text was measured.

See the discussion in the issue: #10677

(See WHATWG Working Mode: Changes for more details.)


/acknowledgements.html ( diff )
/canvas.html ( diff )
/infrastructure.html ( diff )

@AndresRPerez12 AndresRPerez12 marked this pull request as draft February 5, 2025 15:10
<dt><code data-x=""><var>metrics</var>.<span subdfn data-x="dom-textmetrics-getIndexFromOffset">getIndexFromOffset</span>(<var>offset</var>)</code></dt>
<dt><code data-x=""><var>metrics</var>.<span subdfn data-x="dom-textmetrics-getTextClusters">getTextClusters</span>(<var>start</var>, <var>end</var> [, <var>options</var> ])</code></dt>

<dt><code data-x=""><var>context</var>.<span subdfn data-x="dom-context-2d-fillTextCluster">fillTextCluster</span>(<var>textCluster</var>, <var>x</var>, <var>y</var> [, <var>options</var> ])</code></dt>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fill method belongs up with the other drawing methods.

@@ -65506,6 +65515,7 @@ interface mixin <dfn interface>CanvasText</dfn> {
undefined <span data-x="dom-context-2d-fillText">fillText</span>(DOMString text, unrestricted double x, unrestricted double y, optional unrestricted double maxWidth);
undefined <span data-x="dom-context-2d-strokeText">strokeText</span>(DOMString text, unrestricted double x, unrestricted double y, optional unrestricted double maxWidth);
<span>TextMetrics</span> <span data-x="dom-context-2d-measureText">measureText</span>(DOMString text);
undefined <span data-x="dom-context-2d-fillTextCluster">fillTextCluster</span>(<span>TextCluster</span> textCluster, double x, double y, optional <span>TextClusterOptions</span> options);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would expect this to be up with drawing methods.

readonly attribute <span>CanvasTextBaseline</span> <span data-x="dom-TextCluster-baseline">baseline</span>;
};

dictionary <dfn dictionary>TextClusterOptions</dfn> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like dictionaries go before their first use.

@AndresRPerez12
Copy link
Author

Tagging: @whatwg/canvas

Copy link
Contributor

@schenney-chromium schenney-chromium left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry for leaving some comments out of the review.

data-x="dom-context-2d-measureText">measureText()</code> was called) in relation to the text
as a whole.</p>

<p>The x position specified for each cluster corresponds to the aligment point given by the
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo aligment


<dt><dfn attribute for="TextCluster"><code data-x="dom-TextCluster-end">end</code></dfn> attribute</dt>

<dd><p>The ending index for the range of <span data-x="code point">code points</span> that are
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this the index one past the end? Range APIs seem to behave that way but I'm not sure how conventional it is.

Comment on lines +69473 to +69476
<p class="note">The user agent might internally store in the <code>TextCluster</code> object the
complete text, as well as all the <code>CanvasTextDrawingStyles</code> at the time that <code
data-x="dom-context-2d-measureText">measureText()</code> was called, as they will be needed to
correctly render the clusters as they were measured.</p>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it a requirement that the drawing command uses the same parameters as the measurement? If so I would say that directly here, because right now it seems suggestive but not prescribed.

Copy link
Member

@Kaiido Kaiido left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for doing this!

A couple of questions that came to mind:

  • Should there be a strokeTextCluster() method akin to strokeText()?
  • Should the TextCluster be serializable? (so that we can send it to/from a Worker).

Also, I didn't receive the @whatwg/canvas notification from your previous comment. Hopefully this one will work better.

Comment on lines +69493 to +69494
The cluster is rendered where it would be if the whole text that was passed to <code
data-x="dom-context-2d-measureText">measureText()</code> to obtain the cluster was rendered at
Copy link
Member

@Kaiido Kaiido Feb 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Disclaimer: I'm not an editor, so take this with a grain of salt.

I feel this would be a lot clearer if the TextMetrics interface had an internal value to store the original text for each instance and reuse it where needed, instead of this tedious "the text that was passed to measureText() to obtain the cluster".

See for instance how CanvasPattern objects have a transformation matrix that isn't exposed:

Patterns have a transformation matrix, which controls how the pattern is used when it is painted. Initially, a pattern's transformation matrix must be the identity matrix.

It might even be needed to have a clear relationship established between the TextCluster and the TextMetrics from where it's been created, so that you can do something like the cluster's textmetrics's text, or, given that when the TextCluster is created text is available, and TextMetrics may not need it otherwise, you could maybe store it directly along the TextCluster (still in an internal value).

Comment on lines +69501 to +69504
<li><p>Run the <span>text preparation algorithm</span>, passing it the <var>text</var>
originally passed to <code data-x="dom-context-2d-measureText">measureText()</code> to obtain
the cluster, and an object with the <code>CanvasTextDrawingStyles</code> values as they were
when the cluster was measured, with the exception of <code
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This object should probably be created explicitly sooner, in the getTextClusters() method. However I'm not sure how this should be done since we want the computed values at that time.

The cluster is rendered where it would be if the whole text that was passed to <code
data-x="dom-context-2d-measureText">measureText()</code> to obtain the cluster was rendered at
position (<var>x</var>,<var>y</var>), unless the positioning is modified with the options
argument. Specifically, when the methods are invoked, the user agent must run these steps:</p>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The plural in "the methods are" sounds weird. What methods does this refer to?

data-x="dom-TextCluster-baseline">baseline</code> attributes of <var>cluster</var>. Let
<var>glyphs</var> be the result.</p></li>

<li><p>Filter <var>glyphs</var> to include only glyphs that contain <span
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

typo: "to include only [the] glyphs that"

Comment on lines +69521 to +69527
<p>If a <code>TextClusterOptions</code> options dictionary is passed and it has an <code
data-x="">options.x</code> value, move all the shapes in <var>glyphs</var> to the right by
<code data-x="">options.x-cluster.x</code>.</p>

<p>If a <code>TextClusterOptions</code> options dictionary is passed and it has an <code
data-x="">options.y</code> value, move all the shapes in <var>glyphs</var> down by <code
data-x="">options.y-cluster.y</code>.</p>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think at this point options and cluster are infra maps, and thus this should be <var>options["x"]</var> instead of options.x. Also I think the - should be &minus; in free text. So e.g. for the last one, that'd give <var>options["y"]</var> &minus; <var>cluster["y"]</var>, but here again take this with a grain of salt.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Development

Successfully merging this pull request may close these issues.

3 participants