jonobr1 / two.js

A renderer agnostic two-dimensional drawing api for the web.
https://two.js.org
MIT License
8.29k stars 454 forks source link

[Bug] Text rendering in headless mode incorrect size #673

Closed laurentvd closed 1 year ago

laurentvd commented 1 year ago

Describe the bug The text rendering is inconsistent between web and node canvasses.

To Reproduce I've created a separate repo to reproduce the issue. It can be found here: https://github.com/laurentvd/isomorphic-text-rendering-bug

Expected behavior I'd expect the HTML canvas to look exactly the same as the generated image from node-canvas. However, the text is rendered much smaller in the headless canvas.

Screenshots

HTML canvas: image

Node canvas: canvas

Environment (please select one):

Desktop (please complete the following information):

jonobr1 commented 1 year ago

Thanks for posting. Looks like the transformation matrixes aren't being applied in headless mode for some reason. I'll take a look at this on Monday

jonobr1 commented 1 year ago

So, the issue is that Two.js forms font styles with a leading. As per the specification here, the font command is the same as the CSS command of the same name which can be seen here. Because ctx.fillText is only one line, the line-height metric (which is the leading property in Two.js) is ignored in the browser.

Unfortunately, this simply breaks in node-canvas which is why the font size isn't parsed and ultimately not respected in your demo. We can approach this in two ways:

  1. File an issue with node-canvas team in the hopes to implement a parse with the line-height property.
  2. Make a patch for Two.js to remove that command in the ctx.font assignment when it is undefined or null.

As a bandaid, you can update line 20 of src/Renderer.js in your demo to this to make it work (though I don't know that this is a longterm solution):

scene.makeText('This is a test', 512, 512, {
    family: 'BlueStone',
    leading: null,
    size: 100,
    fill: '#FFFFFF',
    leading: null,
});
laurentvd commented 1 year ago

Thanks a lot for diving into this! Really appreciate it. I don't have enough knowledge about the specs yet to fully understand what you're saying, so I'll dive into it. I did try your band-aid, but for me it makes it render in the incorrect font (both node-canvas and canvas) and incorrect size.

Like this: canvas

laurentvd commented 1 year ago

Thanks again for your explanation @jonobr1, I understand the issue now. In my case I can actually define the leading. When I set the leading to be the same as the size, the font size is rendered exactly the same in both canvases. However, it seems to be placed a few pixels 'lower' using node-canvas. Do you know what could cause this? Because if I understood your explanation correctly, with the leading defined, it should render the same, right?

jonobr1 commented 1 year ago

Glad to hear you were able to get it working in both places. It should render the same, but there could be discrepancies between node-canvas and browser implementations for vertical alignment. You can try setting baseline to top, bottom, and middle (or some other ones based on the specification) like so:

scene.makeText('This is a test', 512, 512, {
    family: 'BlueStone',
    leading: null,
    size: 100,
    fill: '#FFFFFF',
    leading: 100,
    baseline: 'top'
});

The default for Two.js is middle. Perhaps a different baseline would be treated the same by both the browser and node-canvas?

laurentvd commented 1 year ago

Thanks @jonobr1 for the hint on where to look! I got it working properly for single line texts using these settings:

scene.makeText('This is a test', 512, 512, {
    family: 'BlueStone',
    fill: '#FFFFFF',
    leading: 100,
    size: 100,
    baseline: 'baseline',
});

Being restricted to baseline only is not ideal, but I can correct this by wrapping it in a group and setting the y position on the text. If node-canvas does indeed only support baseline: 'baseline', maybe we can add a note of that to the documentation?

One more thing; I am using https://github.com/juliendargelos/twojs-multiline-text/ to allow multiline texts in Two.js, but the large leading doesn't play nice with it. It works perfectly fine when the leading is not defined. I guess I have to take a look at that and figure out why it does that. Any pointers are very welcome :) And also, I was surprised to find out Two.js doesn't support multiline out of the box. But perhaps the complexity involved with it is the reason.

jonobr1 commented 1 year ago

Thanks. Glad you got it working.

I'm not deeply familiar with that multiline text plugin. For my purposes, multiline text (like in http://typatone.com/) was always done in DOM and then a Two.js scene is super (or in the case of Typatone sub) imposed and animations. But, it could make sense to make it an extra like Two.ZUI in the extras folder.

jonobr1 commented 1 year ago

I added a documentation note here: https://github.com/jonobr1/two.js/commit/8090e2b8b2e394aaeb132837b2e8017e60918069

laurentvd commented 1 year ago

Great stuff! Thanks for adding the note in the docs. One more thing for others finding this issue in the future; I was able to have consistent output using the following configuration:

const text = new MultilineText("this is my text", 0, 0, {
      family: "YourFont",
      size: 100,
      leading: 100,
      absoluteLeading: true, // This make it consistent between Node and browser
      fill: '#ff0000',
      measure: 'font',
      mode: 'pre',
      baseline: 'baseline'
});

Now that we've added a little warning in the docs, I'd say we can close this ticket. Would you agree @jonobr1?

jonobr1 commented 1 year ago

Thanks for that! I'll close this out then.