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 7 commits 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
source Outdated Show resolved Hide resolved
@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.

source Outdated Show resolved Hide resolved
source Outdated

<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.

Copy link
Author

Choose a reason for hiding this comment

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

Yes, it is one past the end. I hadn't realized I didn't clarify it in the spec. I'll add it explicitly as I don't know how conventional it is either.

source Outdated
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
Author

Choose a reason for hiding this comment

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

Yes, that is specified in the section for fillTextCluster(). The idea when I first wrote it was to just mention the behavior expected, even if the implementation would most likely be to store the parameters in the cluster (as is implemented right now in Chrome).

But, following a suggestion by @Kaiido in another comment, I changed the phrasing of that requirement by using TextCluster as an opaque object that stores those parameters in cluster when they are created. Hopefully this is more clear. Let me know what you think!

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.

source Outdated
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).

Copy link
Author

Choose a reason for hiding this comment

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

I hadn't noticed that objects like CanvasPattern were defined in this way as opaque. I like that concept and I feel it applies well to this use case. Thank you so much for the suggestion!

I have updated it to reflect this. For now I went with the second approach you mentioned (storing the text and the CanvasTextDrawingStyles directly in the cluster as internal values). Hopefully this provides more clarity. Let me know what you think!

source Outdated
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.

Copy link
Author

Choose a reason for hiding this comment

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

With the use of the internal values of the opaque object, hopefully the relationship is clearer now. Passing the exact object from the cluster won't always be possible due to the possibility of passing align and baseline to to fillTextCluster() when rendering, but most of the values now come from the internally stored value.

source Show resolved Hide resolved
source Show resolved Hide resolved
source Outdated Show resolved Hide resolved
@AndresRPerez12
Copy link
Author

Thank you both for your comments! I have addressed them and uploaded the changes. Please let me know what you think.

Answering some of the open questions:

  • Should there be a strokeTextCluster() method akin to strokeText()?

I have added it to the PR. I agree that it makes sense to have both.

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

We feel that for now, it's simpler to not make it serializable. Once we merge this and we know exactly what clusters look like and what values they are storing internally that would require serialization, we can look into it.

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

Thanks! I'm not fully sure how the tags work, so I really appreciate it!

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