Skip to content

Commit 5ff0329

Browse files
Merge pull request #54 from AndresRPerez12/master
Add fillTextCluster() options and rename caretPositionFromPoint() to getIndexFromOffset()
2 parents 37d8784 + c64027a commit 5ff0329

File tree

2 files changed

+59
-16
lines changed

2 files changed

+59
-16
lines changed

images/text-clusters-circle.png

52.2 KB
Loading

spec/enhanced-textmetrics.md

+59-16
Original file line numberDiff line numberDiff line change
@@ -18,15 +18,17 @@ We also want to provide more power to current canvas text rendering APIs.
1818
## Proposal
1919

2020
```webidl
21-
dictionary TextAnchorPoint {
21+
dictionary TextClusterOptions {
2222
DOMString align;
2323
DOMString baseline;
24+
double x;
25+
double y;
2426
};
2527
2628
[Exposed=(Window,Worker)]
2729
interface TextCluster {
28-
attribute double x;
29-
attribute double y;
30+
readonly attribute double x;
31+
readonly attribute double y;
3032
readonly attribute unsigned long begin;
3133
readonly attribute unsigned long end;
3234
readonly attribute DOMString align;
@@ -38,34 +40,38 @@ interface TextCluster {
3840
3941
sequence<DOMRectReadOnly> getSelectionRects(unsigned long start, unsigned long end);
4042
DOMRectReadOnly getActualBoundingBox(unsigned long start, unsigned long end);
41-
sequence<TextCluster> getTextClusters(unsigned long start, unsigned long end, optional TextAnchorPoint anchor_point);
43+
sequence<TextCluster> getTextClusters(unsigned long start, unsigned long end, optional TextClusterOptions options);
4244
43-
unsigned long caretPositionFromOffset(double offset);
45+
unsigned long getIndexFromOffset(double offset);
4446
};
4547
4648
interface CanvasRenderingContext2D {
4749
// ... extended from current CanvasRenderingContext2D.
4850
49-
void fillTextCluster(TextCluster textCluster, double x, double y);
51+
void fillTextCluster(TextCluster textCluster, double x, double y, optional TextClusterOptions options);
5052
};
5153
```
5254

5355
`getSelectionRects()` returns the set of rectangles that the UA would render as selection to select a particular character range.
5456

5557
`getActualBoundingBox()` returns an equivalent box to `TextMetrics.actualBoundingBox`, i.e., the bounding rectangle for the drawing of that range. Notice that this can be (and usually is) different from the selection rect, as those are about the flow and advance of the text. A font that is particularly slanted or whose accents go beyond the flow of text will have a different paint bounding box. For example: if you select this: ***W*** you will see that the end of the W is outside the selection area, which would be covered by the paint (actual bounding box) area.
5658

57-
`caretPositionFromPoint()` returns the character offset for the character at the given `offset` distance from the start position of the text run (accounting for `textAlign` and `textBaseline`) with offset always increasing
59+
The `getIndexFromOffset` method returns the strign indext for the character at the given `offset` distance from the start position of the text run (accounting for `textAlign` and `textBaseline`) with offset always increasing
5860
left to right (so negative offsets are valid). Values to the left or right of the text bounds will return 0 or
59-
`num_characters` depending on the writing direction. The functionality is similar but not identical to [`document.caretPositionFromPoint`](https://developer.mozilla.org/en-US/docs/Web/API/Document/caretPositionFromPoint). In particular, there is no need to return the element containing the caret and offsets beyond the boundaries of the string are acceptable.
61+
`string.length` depending on the writing direction. The functionality is similar but not identical to [`document.caretPositionFromPoint`](https://developer.mozilla.org/en-US/docs/Web/API/Document/caretPositionFromPoint). In particular, there is no need to return the element containing the caret and offsets beyond the boundaries of the string are acceptable.
6062

61-
`getTextClusters()` provides the ability to render minimal grapheme clusters (in conjunction with a new method for the canvas rendering context, more on that later). That is, for the character range given as in input, it returns the minimal rendering operations broken down as much as logically possible, with their corresponding positional data. The position is calculated with the original anchor point for the text as reference, while the `text_align` and `text_baseline` parameters determine the desired alignment of each cluster.
63+
`getTextClusters()` provides the ability to render minimal grapheme clusters (in conjunction with a new method for the canvas rendering context, more on that later). That is, for the character range given as in input, it returns the minimal rendering operations broken down as much as logically possible, with their corresponding positional data. The position is calculated with the original anchor point for the text as reference, while the `align` and `baseline` parameters in the options dictionary determine the desired alignment of each cluster.
6264

63-
To actually render these clusters on the screen, a new method for the rendering context is proposed: `fillTextCluster()`. It renders the cluster with the `text_align` and `text_baseline` stored in the object, ignoring the values set in the context. Additionally, to guarantee that the rendered cluster is accurate with the measured text, the rest of the `CanvasTextDrawingStyles` must be applied as they were when `ctx.measureText()` was called, regardless of any changes in these values on the context since. Note that to guarantee that the shaping of each cluster is indeed the same as it was when measured, it's necessary to use the whole string as context when rendering each cluster.
65+
To actually render these clusters on the screen, a new method for the rendering context is proposed: `fillTextCluster()`. It renders the cluster with the `align` and `baseline` stored in the object, ignoring the values set in the context. Additionally, to guarantee that the rendered cluster is accurate with the measured text, the rest of the `CanvasTextDrawingStyles` must be applied as they were when `ctx.measureText()` was called, regardless of any changes in these values on the context since. Note that to guarantee that the shaping of each cluster is indeed the same as it was when measured, it's necessary to use the whole string as context when rendering each cluster.
6466

65-
For `text_align` specifically, the position is calculated in regards of the advance of said grapheme cluster in the text. For example: if the `text_align` passed to the function is `center`, for the letter **T** in the string **Test**, the position returned will be not exactly be in the middle of the **T**. This is because the advance is reduced by the kerning between the first two letters, making it less than the width of a **T** rendered on its own.
67+
For `align` specifically, the position is calculated in regards of the advance of said grapheme cluster in the text. For example: if the `align` passed to the function is `center`, for the letter **T** in the string **Test**, the position returned will be not exactly be in the middle of the **T**. This is because the advance is reduced by the kerning between the first two letters, making it less than the width of a **T** rendered on its own.
68+
69+
To enable additional flexibility, an options dictionary can be passed to `fillTextCluster()` to override the values for `align`, `baseline`, `x`, and `y` that will be used to render that cluster. For example, calling `ctx.fillTextCluster(cluster, 10, 10, {x: 0, y:0})` will render the cluster exactly at position `(10, 10)`, instead of rendering as if the text as a whole was placed at `(10, 10)` (which is what the internal `x` and `y` values of the cluster represent). This same overriding applies to the `align` and `baseline` parameters if they are passed in the options dictionary. These options passed to `fillTextCluster()` don't modify the underlying cluster object, and only apply to the rendering of that specific call.
6670

6771
`getSelectionRects()`, `getActualBoundingBox()`, and `getTextClusters()` operate in character ranges and use positions relative to the text’s origin (i.e., `textBaseline`/`textAlign` is taken into account).
6872

73+
`getIndexFromOffset()` is recent rename. The previous name was `caretPositionFromPoint()` and is available in Chrome Canary from version `128.0.6587.0`.
74+
6975
## Example usage
7076

7177
```js
@@ -92,7 +98,7 @@ ctx.fillText("let's do this");
9298

9399
Expected output:
94100

95-
![enhanced textMetrics output](../images/enhanced-textmetrics-output.png)
101+
!["let's do this" with a red rectangle tightly bounding "do" and a blue rectangle around "this" extending beyond the text](../images/enhanced-textmetrics-output.png)
96102

97103
`getSelectionRects()` and `getActualBoundingBox()` can be used on Chrome Canary (starting from version `127.0.6483.0` and `128.0.6573.0` respectively) by enabling the feature with `--enable-features=ExtendedTextMetrics` (or the general `--enable-experimental-web-platform-features`).
98104

@@ -106,9 +112,7 @@ ctx.textBaseline = 'middle';
106112

107113
const text = 'Colors 🎨 are 🏎️ fine!';
108114
let tm = ctx.measureText(text);
109-
let clusters = tm.getTextClustersForRange(0, text.length,
110-
{align: 'left',
111-
baseline: 'middle'});
115+
let clusters = tm.getTextClustersForRange(0, text.length);
112116

113117
const colors = ['orange', 'navy', 'teal', 'crimson'];
114118
for(let cluster of clusters) {
@@ -119,7 +123,46 @@ for(let cluster of clusters) {
119123

120124
Expected output:
121125

122-
![enhanced textMetrics output](../images/text-clusters-output.png)
126+
![A text string containing emoji with each character colored differently](../images/text-clusters-output.png)
127+
128+
```js
129+
const canvas = document.getElementById("canvas");
130+
const ctx = canvas.getContext('2d');
131+
132+
const center_x = 250;
133+
const center_y = 250;
134+
const radius = 150;
135+
ctx.font = '50px serif';
136+
ctx.textAlign = 'left';
137+
let text = "🐞 Render this text on a circle! 🐈‍⬛";
138+
139+
const tm = ctx.measureText(text);
140+
// We want the x-position of the center of each cluster.
141+
const clusters = tm.getTextClusters(0, text.length, {align: 'center'});
142+
143+
for (const cluster of clusters) {
144+
// Since ctx.textAlign was set to 'left' before measuring, all values of
145+
// cluster.x are positive.
146+
let p = cluster.x / tm.width;
147+
let rad = 2 * Math.PI * p;
148+
let x = radius * Math.cos(rad) + center_x;
149+
let y = radius * Math.sin(rad) + center_y;
150+
ctx.save();
151+
ctx.translate(x, y);
152+
ctx.rotate(rad + Math.PI / 2);
153+
ctx.translate(-x, -y);
154+
// The cluster is rendered at precisely (x, y), using align as 'center'
155+
// and baseline as 'middle', even if different values were used when
156+
// measuring.
157+
ctx.fillTextCluster(cluster, x, y,
158+
{align: 'center', baseline: 'middle', x: 0, y: 0});
159+
ctx.restore();
160+
}
161+
```
162+
163+
Expected output:
164+
165+
![A text string containing emoji rendered in a circle, with each glyph rotated according to its position](../images/text-clusters-circle.png)
123166

124167
`getTextClusters()` and `fillTextCluster()` can be used on Chrome Canary (starting from version `132.0.6783.0`) by enabling the feature with `--enable-features=ExtendedTextMetrics` (or the general `--enable-experimental-web-platform-features`).
125168

0 commit comments

Comments
 (0)