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

Canvas works, SVG works, WebGL renders nothing #736

Open dan-fritchman opened 2 weeks ago

dan-fritchman commented 2 weeks ago

This is probably gonna be a pretty crappy bug report, but I'm not really sure what to report.

In our application SVG and Canvas have always worked. WebGL has never worked. It just renders a blank canvas.
It's a big complicated application; I'm not sure how to peel off a minimal repro case.

Currently using Two v0.8.14. Mostly in current Chrome, but also tried in Firefox & Safari, all on current MacOS. And I did notice something today: calling functions like makeRectangle and makeCircle on our Two instance in the browser developer console does get them to show up:

image image

And noting, our application generally does not use those makeXyz functions. We do something more like:

const top = new Group();
const middle = new Group();
top.add(middle);
const bottom = new Rectangle(someArgs);
bottom.addTo(middle);
// etc

I guess the question is: does anything come to mind as to why that combination wouldn't work?

dan-fritchman commented 2 weeks ago

I should add: leafing through that same browser console, it does appear that our application's content is in the two.scene; it has a big hierarchy of children (generally deeply nested Groups).

jonobr1 commented 1 week ago

Hmmm, that is sufficiently vague. Couple things stand out as potential next steps:

  1. Does it work in older versions? Currently the WebGL examples do work on the two.js.org site which are using the latest NPM version
  2. Would you be open to adding me as a collaborator to debug? It is very possible that there is some discrepancy between the renderers that needs to be addressed. But, it's hard to tell from this vague of a use case.

Generally, if you're not using the makeXYZ methods on a specific instance. You'll need to do this:

const two = new Two({ type: Two.Types.webgl, autostart: true }).appendTo(document.body);
// If you haven't autostarted you need
// to call two.update();
const rectangle = new Two.Rectangle(x, y, width, height);
two.add(rectangle);

With groups as intermediaries it is possible that all the nested elements in a group could get missed on the update invocation. But, without an example it's hard to know for sure.

dan-fritchman commented 1 week ago

With groups as intermediaries it is possible that all the nested elements in a group could get missed on the update invocation. But, without an example it's hard to know for sure.

What do ya mean there? Is there anything we could dig into? Or something about the conditions which might make that happen?

Thanks again!

jonobr1 commented 1 week ago

Could you export one scene in SVG? Then I could import it into Two.js using WebGL and it should produce the same error.

What I mean regarding groups, is if you can tell me the exact order that elements are added then maybe I can reproduce the issue.

To confirm, this only happens with simple paths? Or also with text and images?

This could give a hint because Text and Images are not based on Two.Path. All other primitive shapes are.

dan-fritchman commented 1 week ago

OK that makes sense, I'll export some SVGs.
To more quickly answer your question - we use text, but no images. And definitely lots of Paths and Groups.

dan-fritchman commented 1 week ago

OK attaching an example SVG.
That is generated with the (I think recommended method), something like:

  svgString = (): Result<string> => {
    this.two.update();
    const { innerHTML } = this.twoParentDiv;

    // Stick that `xmlns` attribute in there, so most readers will accept it.
    const s = innerHTML.replace(
      "<svg",
      '<svg xmlns="http://www.w3.org/2000/svg"'
    );
    return Result.Ok(s);
  };

example

Just double-checked that swapping to the webgl renderer renders this blank.

jonobr1 commented 1 week ago

🤔 There are some differences. But when I load this SVG into the same webpage with a WebGL and an SVG instance they both render:

Screenshot 2024-09-06 at 12 33 37 PM

Left is WebGL Right is SVG

This is the code:

const a = new Two({
  type: Two.Types.webgl,
  autostart: true,
  width: 400,
  height: 400
}).appendTo(document.body);

const b = new Two({
  type: Two.Types.svg,
  autostart: true,
  width: 400,
  height: 400
}).appendTo(document.body);

a.renderer.domElement.style.border = '1px solid #ccc';
b.renderer.domElement.style.border = '1px solid #ccc';

a.add(a.load('./736.svg', (svg) => { svg.scale = 0.25 }));
b.add(b.load('./736.svg', (svg) => { svg.scale = 0.25 }));
jonobr1 commented 1 week ago

An unrelated note: if at all possible I would highly recommend making the grid a single path.

dan-fritchman commented 1 week ago

Very interesting indeed!
I guess that points to something about how we load/ add elements and eventually update/ render?

And one big path sounds like a pretty good idea.
I suppose just "snaking" that around offscreen would get it done.

jonobr1 commented 1 week ago

Yep, it would reduce the draw calls

dan-fritchman commented 1 week ago

FWIW passing that SVG content to two.load in the dev-console of our app running the WebGL back-end does in fact render that SVG content. (And continues to render nothing from our other groups.)

jonobr1 commented 1 week ago

Hmm, that would indicate that there's something you're doing (or not doing) that two.load does.

This is all the method does once the content is loaded and available in the DOM

      for (i = 0; i < dom.temp.children.length; i++) {
        elem = dom.temp.children[i];
        child = this.interpret(elem, false, false);
        if (child !== null) {
          group.add(child);
        }
      }
dan-fritchman commented 21 hours ago

Is it possible that transforming (scaling & positioning) a group works differently in the webgl renderer than in the others?
We are generally transforming the coordinates in some external data into on-screen ones with something like this:

  // Apply to a `Two.Shape`.
  // Notably `Two.Group` is a `Shape`, and is the most common use-case.
  apply: (t: CoordTransform, shape: Shape) => {
    const { scale, offset } = t;
    // Note y-axis inversion is applied right here!
    shape.scale = new Vector(scale, -scale);
    shape.position = new Vector(offset.x, offset.y);
  },

Usually the shape argument to this function is a Group, which is in turn the sole child of our two.scene group.
This doesn't seem to be updating as it does with the other renderers. I'm wondering if everything is rendering, but is just off-screen due to a misunderstanding of this coordinate transform.

dan-fritchman commented 21 hours ago

I'll try to make a minimal demo case.

dan-fritchman commented 18 hours ago

OK yeah that's it, or something to do with it.
Here's a minimal example: https://codepen.io/dan-fritchman/pen/vYqqLwz

import Two from 'https://cdn.skypack.dev/two.js@latest';

const two = new Two({
  type: Two.Types.svg,
  fullscreen: true,
  autostart: true
}).appendTo(document.body);

// Create a single child group
const g = new Two.Group();
g.addTo(two);

// Add a rectangle to it
const r = new Two.Rectangle(100,-100,100,100);
r.fill = "red";
r.addTo(g);

// Now scale and shift the parent group
g.scale = new Two.Vector(1, -1);
g.position = new Two.Vector(100, 100);

two.render();

Looks like:

image

Change to:

  type: Two.Types.webgl,

And it looks like this:

image