asny / three-d

2D/3D renderer - makes it simple to draw stuff across platforms (including web)
MIT License
1.24k stars 105 forks source link

On web, the window module always resizes the <canvas> #339

Closed silverlyra closed 1 year ago

silverlyra commented 1 year ago

👋🏻 Hello!

I’m using three-d’s window module, and I’d like to keep doing so – it’s a convenient wrapper around winit, and I’d rather work with three-d’s FrameInput and Event types.

But I’m trying to use three-d to render to <canvas> that exists within the layout of its web page, and since 34287bd53f52e1f16a002174ab65d164aada7ce2, three-d resizes the canvas to have the same inner width and height as its parent window. Because this happens unconditionally in render_loop, I can’t work around this, even via from_winit_window.

I could fork three-d and remove that, but if you’re open to changing this behavior or making it optional, I’d like to start a discussion about how the API could change, and I’d be happy to open a PR to implement it.

winit itself always sets an explicit canvas size, which is also behavior that I wish I could opt out of. But for my purposes, a size field on WindowSettings for wasm targets, or even respecting the existing max_size field, would work perfectly.

asny commented 1 year ago

Hi Lyra 👋

I think that's a very fair request, I'm definitely open to changing that. To provide some context, up until now, the window module has existed to make it easy to get started, not necessarily to be used for the end application. However, I think most people use it for the end application and there's often not a reason to use anything else. For example, the difference between this and eframe is not that big (also according to the author of reframe), both are thin wrappers on top of winit. Also, it is definitely convenient to have the FrameInput and so on. So I'm all for making the final improvements to make it production ready and this is one of the issues that should be fixed.

So to me the nicest solution would be if we didn't have to set the size of the canvas at all, that can all be specified another place. I think my reason for doing it like this was that the canvas size didn't take into account the device pixel ratio. That meant rendering to a canvas resulted in a blurry result if the device pixel ratio is not 1. It might also be to make the canvas fill the entire browser window, I don't remember exactly. So this was just my hacky solution because my css skills are.. well.. not very good 😄 do you know if there's a solution for both of these problems in css or html or something? And is it even possible if winit anyway sets the canvas size and there's no way to opt out of that functionality? (We could of course make a feature request to winit)

The other solution would be to be able to specify the size explicitly. Wouldn't that be possible by using the already existing window settings min and max size and then update the canvas on resize or something? Maybe a bit confusing to use the window settings for the canvas, but it's kind of like the window on web 🙂

asny commented 1 year ago

I had some time to experiment and it turned out I didn't remember correctly (surprise surprise). winit handles device pixel ratio just fine, it was an issue at the time where I had to make my own web window handling at the time where winit didn't support web. It was more about resizing the canvas to fill the inner size, a sort of maximised mode. I'm sure you can do that from outside this crate as well, but I guess it's nice to have support for this mode. However, right now, as you say, it's not possible to avoid it.

On desktop, the max_size setting defines the initial size of the window and if it's None the window is maximised. So I updated your PR to use this setting instead of adding a new setting. So if you define the max_size, the canvas is resized to that size at construction and three-d will not change it in the render loop. If it's None it will follow the size of the browser (the current behaviour). I'm not sure it's the nicest API, but I think it should be improved for both desktop and web if we want to do that. What do you think? It's also an advantage if the PR doesn't change the API (which it doesn't at the moment), but it's also always nice to improve things, so I'm also ok with changes to the API if it's for the better.

To be able to avoid setting the size of the canvas at all, we need changes to winit.

silverlyra commented 1 year ago

On desktop, the max_size setting defines the initial size of the window and if it's None the window is maximised. So I updated your PR to use this setting instead of adding a new setting.

Ah great! I had started down that path last week, but then I discovered the resize code also lived in WindowedContext, and added size to a struct that would be visible there, too. But I see on #340 you’ve deleted that from WindowedContext, so 🎉 yeah I think using max_size for this makes sense.

To be able to avoid setting the size of the canvas at all, we need changes to winit.

Since opening this issue, I’ve learned that I’d actually need changes to HTML itself for that. 😅 According to MDN, the width and height attributes of <canvas> are the only way to set the actual size of the canvas area. If CSS is used to make the canvas element larger than that, the browser will scale it up as if it were an <img>, and won’t extend the drawing area. If you don’t set width and height, you get a 300×150 canvas by default 🤦🏼‍♀️, so winit’s default of 1024×768 is as good as any.

Screenshot 2023-03-20 at 9 11 08 PM

What I’d need in order to truly integrate my three-d canvas to the page would be a way to update the canvas size when the window is resized – the page has a responsive layout, so if the window becomes narrow enough, I’d want to alter the canvas size. But I think I can handle that from JavaScript now that three-d will stop resizing the canvas on every frame.

There is also an open winit issue (rust-windowing/winit#1661) and pull request (rust-windowing/winit#2074) to automatically keep the canvas drawing dimensions in sync with the size of the <canvas> element; I’ll see if there’s any way I can contribute there. Apparently winit’s web platform support currently has no maintainer. 🫠

To provide some context, up until now, the window module has existed to make it easy to get started, not necessarily to be used for the end application. However, I think most people use it for the end application and there's often not a reason to use anything else.

That makes total sense to me. I think another nice change would be to refactor this match which (among other things) translates winit::Events to three_d::renderer::Events and make that a function I can use on its own; that way I could handle winit initialization myself, but still use (e.g.) CameraControl.

asny commented 1 year ago

Ah great! I had started down that path last week, but then I discovered the resize code also lived in WindowedContext, and added size to a struct that would be visible there, too. But I see on https://github.com/asny/three-d/pull/340 you’ve deleted that from WindowedContext, so 🎉 yeah I think using max_size for this makes sense.

Yeah, I actually added that a week ago or something to get the right size before starting the render loop, but it makes more sense that it is applied when constructing the window, not when constructing the context 🙂

Since opening this issue, I’ve learned that I’d actually need changes to HTML itself for that. 😅 According to MDN, the width and height attributes of are the only way to set the actual size of the canvas area. If CSS is used to make the canvas element larger than that, the browser will scale it up as if it were an , and won’t extend the drawing area. If you don’t set width and height, you get a 300×150 canvas by default 🤦🏼‍♀️, so winit’s default of 1024×768 is as good as any.

Haha, yeah ok, then I guess there's nothing to do about that 😆 The default size really gives you a sense of how old that API is 😄 I think this is also what I remembered about supporting device pixel ratio, I think I had to set both the canvas size in logical and the css size in physical pixels. Took me a while to figure that out.

What I’d need in order to truly integrate my three-d canvas to the page would be a way to update the canvas size when the window is resized – the page has a responsive layout, so if the window becomes narrow enough, I’d want to alter the canvas size. But I think I can handle that from JavaScript now that three-d will stop resizing the canvas on every frame.

That makes sense. Let me know if you need anything 👍 I'll merge #340 💪

There is also an open winit issue (https://github.com/rust-windowing/winit/issues/1661) and pull request (https://github.com/rust-windowing/winit/pull/2074) to automatically keep the canvas drawing dimensions in sync with the size of the element; I’ll see if there’s any way I can contribute there. Apparently winit’s web platform support currently has no maintainer. 🫠

Oh that would be awesome. But maybe not very easy to do in practice since it's been that long underway?

That makes total sense to me. I think another nice change would be to refactor this match which (among other things) translates winit::Events to three_d::renderer::Events and make that a function I can use on its own; that way I could handle winit initialization myself, but still use (e.g.) CameraControl.

That's a nice suggestion. I recently separated the window and context creation so you could create the winit window yourself and have full control over it and still easily create a three-d context. It makes sense to also be able to get the event loop functionality even though you create a winit window yourself 👍

Finally, can you explain a noob what the advantage of

let html_canvas = self.window.canvas();
let browser_window = html_canvas
    .owner_document()
    .and_then(|doc| doc.default_view())
    .or_else(web_sys::window)
    .unwrap();

is compared to

web_sys::window().unwrap()
silverlyra commented 1 year ago

Finally, can you explain a noob what the advantage of […]

ah @asny, it’s pretty pedantic, but technically the Window that the canvas appears in may not be the JS global window. So it’s looking up the window which is displaying the Document that the canvas is a part of, and then falling back to the global in case … 🤷🏼‍♀️ the canvas isn’t yet added to a document or something.

asny commented 1 year ago

Ah, thanks for the explanation. And in this case, I think it's good to be a bit pedantic 🙂