3

I have a use case where I need to render a significant amount (~50,000 glyphs) of crisp, scalable text strings on a canvas element. The best solution I've tried so far involves triangulating text drawn on a canvas element (Text was drawn using fillText method), uploading matrix uniforms and the Float32Array of triangles representing that string to the GPU via WebGL. Using this method, I was able to render 100,000 glyphs at about 30fps. Glyphs become blocky at very high zoom levels, but that is fine for my use case.

However, this method has overhead of about ~250ms per string, since I first draw the string to a canvas element in memory, read pixel data, turn the bitmap image into a vector and then triangulate the vector data. Searching the web for solutions, I came across two interesting open-source projects:

So now I want to re-write my initial proof of concept to use OpenType and Earcut. OpenType for feeding curve data into Earcut, and Earcut for triangulating that data and returning an array representing the point for each triangle.

My problem is, I can't figure out how to get the data OpenType provides and convert it into the format that Earcut accepts. Can anyone provide assistance for this?

More Info:

This StackOverflow question had some great information, but lacks some of the implementation details: Better Quality Text in WebGL. I suppose what I am trying to accomplish is the "Font as Geometry" approach described in the first answer.

Synthesize
  • 33
  • 3
  • This sounds like an [XY problem](https://meta.stackexchange.com/a/66378); why do you need that many glyphs rendered all at once on a single canvas? – Mike 'Pomax' Kamermans May 28 '18 at 19:45
  • Thanks for the comment. My use case is for a project management web application where each task can have five different strings on it at maximum zoom level, and the user can pan/zoom around the schedule. There is already culling so glyphs offscreen don't render as well as a level of detail system so more detail is rendered on zoom in. However, text appears at a relatively low zoom level with one line of a task description visible. It is common for 1,000+ tasks to be visible at once. – Synthesize May 29 '18 at 01:08
  • I think the question was - do you really need WebGL? Regular 2d canvas use the same hardware acceleration, and have more sophisticated algorithms for filling in outlines without having to break down curves into segments and do triangulation. – riv May 29 '18 at 14:49
  • So my first attempt for this was to use two canvas elements. One with a WebGL context, and one with a CanvasRenderingContext2D. All text was drawn on the CanvasRenderingContext2D, which was then overlaid on the main GL context canvas. This worked great on Chrome and iOS Safari, however drawing the text was significantly slower on Firefox and Edge with a high number of strings (Used 5,000 as a test case), which ended up being a no-go for us. – Synthesize May 29 '18 at 15:00

1 Answers1

6

You can create a path using Font.getPath. Path consists of move-to, line-to, curve-to, quad-to and close instructions, accessed via path.commands. You will need to convert bezier curve instructions into small segments first, of course.

Once you have a set of closed paths, you need to determine which ones are holes. Inner outlines will be oriented in an opposite direction to outer ones, and you can assign them to the smallest outer outline containing them. Once you have groups of <outer outline and a set of holes> you should be able to feed it to earcut library.

This is a simple implementation that assumes there are no intersections. For me it worked very well for most fonts, except for very few "fancy" fonts that have intersecting paths.

Here's a working example: https://jsbin.com/gecakub/edit?html,js,output

Instead of creating meshes for each string, you could also create them for individual characters, and then position them yourself using kerning data from the library.

Edit: this solution will only work for TTF fonts, though it can be easily adjusted for CCF (.otf) by ignoring path orientation and using a better "path A is inside path B" check, unless the font has intersecting paths.

riv
  • 6,846
  • 2
  • 34
  • 63
  • Thank you! This is an awesome answer and exactly what I was looking for. – Synthesize May 29 '18 at 14:38
  • In OpenType fonts, CFF outlines use the crossing number to determine holes, whereas TTF outlines use path direction to determine holes, with SVG outlines literally using "whichever you want, just set the `fillrule` appropriately" per glyph. So, I don't know if OpenType.js ignores those differences and just rewrites paths, or whether it respects the original glyph type, but if you know which of those two it is, it would be worth updating the bit where you explain how to determine which subpaths might represent holes – Mike 'Pomax' Kamermans Jun 04 '18 at 16:24
  • Ah that's a good point, I only used TTF fonts, no idea if the library does any processing for other font types (I used FreeType in my original project, so I have even less idea about OpenType.js). This solution will definitely break on outlines where winding direction does not matter, but it can be fixed by replacing direction check with a more robust code to check if path X is inside path Y (though it won't be possible to process self intersections). – riv Jun 04 '18 at 18:34