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
Draft
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
209 changes: 209 additions & 0 deletions source
Original file line number Diff line number Diff line change
Expand Up @@ -4530,6 +4530,15 @@ a.setAttribute('href', 'https://example.com/'); // change the content attribute
</ul>
</dd>

<dt>Unicode</dt>
<dd>
<p>The following terms are defined in <cite>Unicode</cite>: <ref>UNICODE</ref></p>

<ul class="brief">
<li><dfn data-x-href="https://www.unicode.org/reports/tr29/#Grapheme_Cluster_Boundaries">grapheme cluster</dfn></li>
</ul>
</dd>

<dt>Web App Manifest</dt>

<dd>
Expand Down Expand Up @@ -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.

};

interface mixin <dfn interface>CanvasDrawImage</dfn> {
Expand Down Expand Up @@ -65604,6 +65614,28 @@ interface <dfn interface>TextMetrics</dfn> {
readonly attribute double <span data-x="dom-textmetrics-hangingBaseline">hangingBaseline</span>;
readonly attribute double <span data-x="dom-textmetrics-alphabeticBaseline">alphabeticBaseline</span>;
readonly attribute double <span data-x="dom-textmetrics-ideographicBaseline">ideographicBaseline</span>;

sequence&lt;DOMRectReadOnly> <span data-x="dom-textmetrics-getSelectionRects">getSelectionRects</span>(unsigned long start, unsigned long end);
DOMRectReadOnly <span data-x="dom-textmetrics-getActualBoundingBox">getActualBoundingBox</span>(unsigned long start, unsigned long end);
unsigned long <span data-x="dom-textmetrics-getIndexFromOffset">getIndexFromOffset</span>(double offset);
sequence&lt;<span>TextCluster</span>> <span data-x="dom-textmetrics-getTextClusters">getTextClusters</span>(unsigned long start, unsigned long end, optional <span>TextClusterOptions</span> options);
};

[Exposed=(Window,Worker)]
interface <dfn interface>TextCluster</dfn> {
readonly attribute double <span data-x="dom-TextCluster-x">x</span>;
readonly attribute double <span data-x="dom-TextCluster-y">y</span>;
readonly attribute unsigned long <span data-x="dom-TextCluster-begin">begin</span>;
readonly attribute unsigned long <span data-x="dom-TextCluster-end">end</span>;
readonly attribute <span>CanvasTextAlign</span> <span data-x="dom-TextCluster-align">align</span>;
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.

<span>CanvasTextAlign</span> align;
<span>CanvasTextBaseline</span> baseline;
double x;
double y;
};

dictionary <dfn dictionary>ImageDataSettings</dfn> {
Expand Down Expand Up @@ -69143,6 +69175,12 @@ try {
<dt><code data-x=""><var>metrics</var>.<span subdfn data-x="dom-textmetrics-ideographicBaseline">ideographicBaseline</span></code></dt>

<dd><p>Returns the measurement described below.</p></dd>
<dt><code data-x=""><var>metrics</var>.<span subdfn data-x="dom-textmetrics-getSelectionRects">getSelectionRects</span>(<var>start</var>, <var>end</var>)</code></dt>
<dt><code data-x=""><var>metrics</var>.<span subdfn data-x="dom-textmetrics-getActualBoundingBox">getActualBoundingBox</span>(<var>start</var>, <var>end</var>)</code></dt>
<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.

</dl>

<div w-nodev>
Expand Down Expand Up @@ -69327,6 +69365,116 @@ try {
positive numbers indicating that the given baseline is below the <span>ideographic-under
baseline</span>. (Zero if the given baseline is the <span>ideographic-under
baseline</span>.)</p></dd>

<dt><dfn method for="TextMetrics"><code data-x="dom-textmetrics-getSelectionRects">getSelectionRects(<var>start</var>, <var>end</var>)</code></dfn> method</dt>

<dd><p>Returns the set of rectangles that the UA would render as selection to select
a particular character range, in <span data-x="'px'">CSS pixels</span>. The positions are returned
relative to the alignment point given by the <code
data-x="dom-context-2d-textAlign">textAlign</code> and <code
data-x="dom-context-2d-textBaseline">textBaseline</code> attributes.</p></dd>

<dt><dfn method for="TextMetrics"><code data-x="dom-textmetrics-getActualBoundingBox">getActualBoundingBox(<var>start</var>, <var>end</var>)</code></dfn> method</dt>

<dd>
<p>Returns the rectangle equivalent to the box described by <code
data-x="dom-textmetrics-actualBoundingBoxLeft">actualBoundingBoxLeft</code>, <code
data-x="dom-textmetrics-actualBoundingBoxRight">actualBoundingBoxRight</code>, <code
data-x="dom-textmetrics-actualBoundingBoxAscent">actualBoundingBoxAscent</code>, <code
data-x="dom-textmetrics-actualBoundingBoxDescent">actualBoundingBoxDescent</code>, for the given
range. The positions are returned relative to the alignment point given by the <code
data-x="dom-context-2d-textAlign">textAlign</code> and <code
data-x="dom-context-2d-textBaseline">textBaseline</code> attributes.</p>

<p class="note">The bounding box can be (and usually is) different from the selection
rectangles, which are based on the advance of the text. A font that is particularly slanted or
with accents that go beyond the flow of text will have a different paint bounding box.</p>
</dd>

<dt><dfn method for="TextMetrics"><code data-x="dom-textmetrics-getIndexFromOffset">getIndexFromOffset(<var>offset</var>)</code></dfn> method</dt>

<dd><p>Returns the string index for the character at the given offset distance from the start
position of the text run, relative to the alignment point given by the <code
data-x="dom-context-2d-textAlign">textAlign</code> attribute, with offset always increasing left
to right (negative offsets are valid). Values to the left or right of the text bounds will return
0 or the length of the text, depending on the writing direction.</p></dd>

<dt><dfn method for="TextMetrics"><code data-x="dom-textmetrics-getTextClusters">getTextClusters(<var>start</var>, <var>end</var>, <var>options</var>)</code></dfn> method</dt>

<dd>
<p>Splits the given range of the text into <span data-x="grapheme cluster">grapheme clusters</span>, and returns the positional data
for each cluster, as a list of new <code>TextCluster</code> objects, with members behaving as
described in the following list: <ref>UNICODE</ref></p>

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

<dd>
<p>The x coordinate of the cluster, on a coordinate space using <span
data-x="'px'">CSS pixels</span>, with its origin at the anchor point defined by the <code
data-x="dom-context-2d-textAlign">textAlign</code> attribute (at the time <code
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

<code data-x="dom-TextCluster-align">align</code> attribute of the cluster (e.g. if
this attribute is set to "<code data-x="">left</code>", the calculated position corresponds
to the <code data-x="dom-context-2d-textAlign-left">left</code> of each cluster). The
selection criteria for this alignment point is explained in the section for this attribute
of the cluster.</p>
</dd>

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

<dd>
<p>The y coordinate of the cluster, on a coordinate space using <span
data-x="'px'">CSS pixels</span>, with its origin at the anchor point defined by the <code
data-x="dom-context-2d-textBaseline">textBaseline</code> attribute (at the time <code
data-x="dom-context-2d-measureText">measureText()</code> was called) in relation to the text
as a whole.</p>

<p>The y position specified for each cluster corresponds to the aligment point given by the
<code data-x="dom-TextCluster-baseline">baseline</code> attribute of the cluster (e.g. if
this attribute is set to "<code data-x="">top</code>", the calculated position corresponds
to the <code data-x="dom-context-2d-textBaseline-top">top</code> of each cluster). The
selection criteria for this alignment point is explained in the section for this attribute
of the cluster.</p>
</dd>

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

<dd><p>The starting index for the range of <span data-x="code point">code points</span> that are
rendered as this cluster.</p></dd>

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

rendered as this cluster.</p></dd>

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

<dd><p>The align for the specific point returned for the cluster. If a
<code>TextClusterOptions</code> options dictionary is passed, and it has a value for
<code data-x="">options.align</code>, this will be the assigned value. Otherwise, it will be
set as the <code data-x="dom-context-2d-textAlign">textAlign</code> attribute. Note that this
doesn't change the origin of the coordinate system, just which point is specified for each
cluster.</p></dd>

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

<dd><p>The baseline for the specific point returned for the cluster. If a
<code>TextClusterOptions</code> options dictionary is passed, and it has a value for
<code data-x="">options.baseline</code>, this will be the assigned value. Otherwise, it will
be set as the <code data-x="dom-context-2d-textBaseline">textBaseline</code> attribute. Note
that this doesn't change the origin of the coordinate system, just which point is specified
for each cluster.</p></dd>
</dl>

<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>
Comment on lines +69473 to +69476
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.

</dd>
</dl>

<p class="note">Glyphs rendered using <code data-x="dom-context-2d-fillText">fillText()</code> and
Expand All @@ -69339,7 +69487,67 @@ try {
documents, rendered using CSS, straight to the canvas. This would be provided in preference to a
dedicated way of doing multiline layout.</p>

<p>The <dfn method for="CanvasText"><code
data-x="dom-context-2d-fillTextCluster">fillTextCluster(<var>textCluster</var>, <var>x</var>,
<var>y</var>, <var>options</var>)</code></dfn> method renders a <code>TextCluster</code> object.
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
Comment on lines +69493 to +69494
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).

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?


<ol>
<li><p>If any of the arguments are infinite or NaN, then return.</p></li>

<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
Comment on lines +69501 to +69504
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.

data-x="dom-context-2d-textAlign">textAlign</code> and <code
data-x="dom-context-2d-textBaseline">textBaseline</code>, which are taken from the <code
data-x="dom-TextCluster-align">align</code> and <code
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"

data-x="code point">code points</span> within the range <code
data-x="dom-TextCluster-begin">cluster.begin</code> to <code
data-x="dom-TextCluster-end">cluster.end</code>.</p></li>

<li><p>Move all the shapes in <var>glyphs</var> to the right by <var>x</var>
<span data-x="'px'">CSS pixels</span> and down by <var>y</var> <span data-x="'px'">CSS
pixels</span>.</p></li>

<li>
<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>
Comment on lines +69521 to +69527
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.

</li>

<li>
<p>Paint the shapes given in <var>glyphs</var>, as transformed by the <span
data-x="dom-context-2d-transformation">current transformation matrix</span>, with each <span
data-x="'px'">CSS pixel</span> in the coordinate space of <var>glyphs</var> mapped to one
coordinate space unit.</p>

<p><span>this</span>'s <span
data-x="concept-CanvasFillStrokeStyles-fill-style">fill style</span> must be applied to the
shapes and <span>this</span>'s <span
data-x="concept-CanvasFillStrokeStyles-stroke-style">stroke style</span> must be ignored.

<p>These shapes are painted without affecting the current path, and are subject to <span
data-x="shadows">shadow effects</span>, <span data-x="concept-canvas-global-alpha">global
alpha</span>, the <span>clipping region</span>, and the <span>current compositing and blending
operator</span>.</p>
</li>
</ol>

<p class="note">By setting <code data-x="">options.x</code> and <code data-x="">options.y</code>
to 0, the cluster will be rendered exactly at the position (<var>x</var>,<var>y</var>) passed to
<code data-x="dom-context-2d-fillTextCluster">fillTextCluster()</code>.</p>

<h6>Drawing paths to the canvas</h6>

Expand Down Expand Up @@ -146080,6 +146288,7 @@ INSERT INTERFACES HERE
Andreas Kling,
Andrei Popescu,
Andres Gomez,
Andrés Ricardo Pérez Rojas,
Andres Rios,
Andreu Botella,
Andrew Barfield,
Expand Down