Skip to content

Canvas Text Shaping - #137

Open
shallawa wants to merge 3 commits into
WebKit:mainfrom
shallawa:canvas-text-shaping
Open

shallawa wants to merge 3 commits into
WebKit:mainfrom
shallawa:canvas-text-shaping

Conversation

@shallawa

Copy link
Copy Markdown

Add an explainer for WebKit proposal for Canvas Text shaping. Another proposal has been discussed here[1].

[1] https://github.com/fserb/canvas2D/blob/master/spec/enhanced-textmetrics.md

Add an explainer for WebKit proposal for Canvas Text shaping. Another proposal
has been discussed here[1].

[1] https://github.com/fserb/canvas2D/blob/master/spec/enhanced-textmetrics.md

* CanvasTextShaping/CanvasTextShaping.md: Added.
* CanvasTextShaping/drawing-colored-clusters-display.png: Added.
* CanvasTextShaping/drawing-colored-justified-words-display.png: Added.
* CanvasTextShaping/drawing-text.png: Added.
* CanvasTextShaping/drawing-text-with-styles.png: Added.
* CanvasTextShaping/drawing-text-with-styles-display.png: Added.
* CanvasTextShaping/drawing-wrapped-justified-text-with-styled-display.png: Added.
Add an explainer for WebKit proposal for Canvas Text shaping. Another proposal
from Google has been discussed here[1].

[1] https://github.com/fserb/canvas2D/blob/master/spec/enhanced-textmetrics.md

Reviewed by NOBODY (OOPS!).

Explanation of why this fixes the bug (OOPS!).

* CanvasTextShaping/CanvasTextShaping.md: Added.
* CanvasTextShaping/drawing-colored-clusters-display.png: Added.
* CanvasTextShaping/drawing-colored-justified-words-display.png: Added.
* CanvasTextShaping/drawing-text.png: Added.
* CanvasTextShaping/drawing-text-with-styles.png: Added.
* CanvasTextShaping/drawing-text-with-styles-display.png: Added.
* CanvasTextShaping/drawing-wrapped-justified-text-with-styled-display.png: Added.
}
```

## Examples

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Could you add an example of text-on-path? I'm having trouble seeing how you would draw a sequence of GlyphRun along a non-linear path.

@shallawa shallawa Sep 16, 2026 •

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

It can be something like this:

function fillClustersOnCircle(ctx, text, centerX, centerY, radius)
{
    const clusters = ctx.splitText(text);
    const clustersWidth = clusters.reduce((width, cluster) => width + cluster.textMetrics().width, 0);

    let distance = 0;
    for (const cluster of clusters) {
        const ratio = distance / clustersWidth;
        const angle = 2 * Math.PI * ratio;
        const x = radius * Math.cos(angle) + centerX;
        const y = radius * Math.sin(angle) + centerY;

        ctx.save();
        ctx.translate(x, y);
        ctx.rotate(angle + Math.PI / 2);
        ctx.fillGlyphRun(cluster, 0, 0);
        ctx.restore();

        distance += cluster.textMetrics().width;
    }
}

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Having fillText(), fillGlyphRuns() and fillGlyphRun() give freedom to do different scenarios:

  • fillText() can be used when no special breaking or styling is required. All text can be displayed in one call.
  • fillGlyphRuns() can be used when special breaking (like clusters or word boundaries) or styling is required. But the text is displayed in the increasing horizontal direction of the context.
  • fillGlyphRun() can be used similar to fillGlyphRuns() but the display position is customizable.

Add an explainer for WebKit proposal for Canvas Text shaping. Another proposal
from Google has been discussed here[1].

[1] https://github.com/fserb/canvas2D/blob/master/spec/enhanced-textmetrics.md

Reviewed by NOBODY (OOPS!).

* CanvasTextShaping/CanvasTextShaping.md: Added.
* CanvasTextShaping/drawing-clusters-on-circle-display.png: Added.
* CanvasTextShaping/drawing-colored-clusters-display.png
* CanvasTextShaping/drawing-colored-justified-words-display.png
* CanvasTextShaping/drawing-text-with-styles-display.png
* CanvasTextShaping/drawing-text-with-styles.png
* CanvasTextShaping/drawing-text.png
* CanvasTextShaping/drawing-wrapped-justified-text-with-styled-display.png
// GlyphRun drawing
// ---------------------------------------------------------
double fillGlyphRun(GlyphRun run, double x, double y);
double strokeGlyphRun(GlyphRun run, double x, double y);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

What is returned from these methods?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

They return the width of the drawn GlyphRun. So this code

for (const glyphRun of glyphRuns)
    x += ctx.fillGlyphRun(glyphRun, x, y);

is equivalent to this:

for (const glyphRun of glyphRuns) {
    ctx.fillGlyphRun(glyphRun, x, y);
    x += glyphRun.textMetrics().width;
}

```
interface TextRun {
readonly attribute unsigned long start;
readonly attribute DOMString text;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Is there a strong case for providing a text substring instead of an end index? The downsides I see:

  • API friction: I posit most clients want an end index, not the substring content. Looking at the code samples, the only uses for text are to get its length to calculate the end index.
  • Allocation overhead: implementers may need to allocate separate storage for each substring (as opposed to cheap views). This could introduce measurable overhead for tight shaping/cluster loops.
  • Ambiguity: at fist glance, it's unclear what the relationship is between text and the input string, or which one start applies to. start/end are self-documenting.
  • If needed, clients can always track the input themselves, and substring as needed based on start/end indices.

readonly attribute DOMString text;
};

Intl.Segmenter.breakText(string)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Intl.Segmenter already offers a well defined API for grapheme/word/sentence segmentation. Why not extend that API for line granularity instead of introducing a new entry point?

Also, segmentation most likely needs to be performed via instance methods, to observe the segmenter instance properties (mainly the locale).


```
interface GlyphRun {
readonly attribute TextRun textRun;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Assuming TextRun is updated to store start/end indices, it would make sense to expose those directly in GlyphRun also, instead of adding a limited-value wrapper. That would avoid allocating TextRun objects for clients that don't interact with the higher-level functionality (only care about basic text shaping and glyph clusters).

// Text APIs
// ---------------------------------------------------------
// Text analysis - `segments` is used to apply custom slicing
sequence<GlyphRun> shapeText(DOMString text, optional sequence<unsigned long> segments, optional StyleCallback? styleCallback);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Have you considered returning a wrapper object (e.g. ShapedText) instead of a plain GlyphRun sequence? I can think of a couple of reasons for preferring that:

  1. Implementation-wise, the result must retain some context that captures the shaped text. If we return a glyph run sequence, then each glyph run must hold a reference to this context; if we return a wrapper, only the wrapper needs to retain that context reference (cutting down glyph run's size).
  2. A wrapper offers a natural anchor point for related APIs (hit testing, selection rects, etc), so we could avoid adding those to the crowded rendering context.

```
interface GlyphRun {
readonly attribute TextRun textRun;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

We should also surface glyph run/cluster positioning info (x, y) such that clients can access that directly instead of being forced to use an accumulator loop. I believe that's needed for random-order processing.

It might also be worthwhile exposing the advance (width) at this level, to avoid per-cluster calls to textMetrics() in hot loops.

This branch has not been deployed

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants