Open denniskaselow opened 2 years ago
The current behavior is expected according to MDN, so it's possible the Dart side needs to be updated. Adding @srujzs for more info here.
Yes you are right that it should be nullable, in case the rendering context was created by an OffscreenCanvas
, but I created the RenderingContext2
from an existing CanvasElement
and as I understand the MDN article, if the RenderingContext2
was created from an existing CanvasElement
then that one should be returned.
It might be
null
if it is not associated with a<canvas>
element or anOffscreenCanvas
object.
EDIT: It may not be nullable at the moment, because no browser supports OffscreenCanvas
, so if an associated canvas exists and thus gl.canvas
is not null, then it must have been created from a CanvasElement
-object and gl.canvas.canvas
must be that object.
It's unclear what can be done on the dart:html
side here. These methods route to their native definitions (querySelector
, getContext
, and both definitions of canvas
), so if there's unexpected behavior, it's likely coming from the browser afaict. Do you have a complete repro we can try out?
If canvas
is returning null
in a legitimate usage, though, I think this warrants a breaking change in dart:html
to make the getter nullable. We may need to version this to avoid excessive breaks - not sure.
Okay, I tried to narrow down whats happening and it's a bit more weird than I thought.
index.html
<!DOCTYPE html>
<script defer src="main.dart.js"></script>
<canvas id='webgl1'></canvas>
<canvas id='webgl2'></canvas>
<script>
let canvas1 = document.querySelector('#webgl1');
let webgl1ctx = canvas1.getContext('webgl');
console.log(`Javascript webgl1ctx.canvas = ${webgl1ctx.canvas}`); // CanvasElement
let canvas2 = document.querySelector('#webgl2');
let webgl2ctx = canvas2.getContext('webgl2');
console.log(`Javascript webgl2ctx.canvas = ${webgl2ctx.canvas}`); // CanvasElement
</script>
main.dart
import 'dart:html';
import 'dart:web_gl';
void main() {
final canvas1 = querySelector('#webgl1') as CanvasElement;
final canvas2 = querySelector('#webgl2') as CanvasElement;
final webgl1ctx = canvas1.getContext('webgl') as RenderingContext;
final webgl2ctx = canvas2.getContext('webgl2') as RenderingContext2;
print('Dart webgl1ctx.canvas = ${webgl1ctx.canvas.runtimeType}'); // CanvasElement
print('Dart webgl2ctx.canvas = ${webgl2ctx.canvas?.runtimeType}'); // signature says Canvas, but runtimeType is CanvasElement
print('Dart webgl2ctx.canvas.canvas = ${webgl2ctx.canvas?.canvas}'); // null
print('Dart webgl2ctx.canvas.offscreenCanvas = ${webgl2ctx.canvas?.offscreenCanvas}'); // null
}
The only strange result is webgl2ctx.canvas
. The method signature in Dart does not return a CanvasElement
but a Canvas?
: https://github.com/dart-lang/sdk/blob/6f4f2f5f129500a3671a4af78463b5b34df3c478/sdk/lib/web_gl/dart2js/web_gl_dart2js.dart#L1367
This is the Canvas
class (which is not a superclass of CanvasElement
): https://github.com/dart-lang/sdk/blob/6f4f2f5f129500a3671a4af78463b5b34df3c478/sdk/lib/web_gl/dart2js/web_gl_dart2js.dart#L81-L93
That's why I tried to access the canvas
-getter of that class, but the runtimeType
when translated to javascript is CanvasElement
, not Canvas
. Casting webgl2ctx.canvas
to CanvasElement
actually works and is what I want to access (but will probably break at some point in the future once it returns what the signature says).
RenderingContext2.canvas
say Canvas
but actually is a CanvasElememt
? RenderingContext.canvas
still returns CanvasElement
. Will that change at some point in the future to be the same as for RenderingContext2
to include OffscreenCanvas
via that intermediate Canvas
-class?OffscreenCanvasRenderingContext2D.canvas
which returns OffscreenCanvas
so the canvas
-getter for CanvasRenderingContext2D
will probably remain as is.Canvas
class works as advertised and OffscreenCanvas
is supported by browser would CanvasElement
need to be CanvasElement?
as far as I can tell.Thanks for the repro! Sorry for the late response.
Why does the signature of RenderingContext2.canvas say Canvas but actually is a CanvasElememt?
I suspect this may be a relic. Canvas
corresponds to the native type WebGLCanvas
, which from what I can tell, no longer exists. This should shift to CanvasElement
.
RenderingContext.canvas still returns CanvasElement. Will that change at some point in the future to be the same as for RenderingContext2 to include OffscreenCanvas via that intermediate Canvas-class?
Just discovered OffscreenCanvasRenderingContext2D.canvas which returns OffscreenCanvas so the canvas-getter for CanvasRenderingContext2D will probably remain as is.
https://developer.mozilla.org/en-US/docs/Web/API/WebGLRenderingContext/canvas https://caniuse.com/mdn-api_offscreencanvasrenderingcontext2d
It isn't clear to me in what context I should expect OffscreenCanvasRenderingContext2D
to occur. Is it simply when getContext
is called on an OffscreenCanvas
object? In that case, addressing your previous question, wouldn't I expect RenderingContext
and RenderingContext2
to only return CanvasElement
s? If they returned an OffscreenCanvas
, I would expect them to be OffscreenCanvasRenderingContext
s instead. The MDN page does say it can return an OffscreenCanvas
only for Firefox in a very specific case, but supporting that might be quite complicated.
Once that intermediate Canvas class works as advertised and OffscreenCanvas is supported by browser would CanvasElement need to be CanvasElement? as far as I can tell.
I still think we should get rid of Canvas
in favor of CanvasElement
, but yes, if it can be nullable, it should be written as such. When would the condition "It might be null if it is not associated with a canvas element or an OffscreenCanvas object." hold true?
It seems like the only major change here is moving RenderingContext.canvas
and RenderingContext2.canvas
to CanvasElement?
types. It's a breaking change, so I'm not sure what we might do here in the near future. To unblock yourself, you can use workarounds using interop (@staticInterop
might work well here to wrap a RenderingContext
type): https://github.com/dart-lang/sdk/blob/main/sdk/lib/html/doc/WORKAROUNDS.md
I suspect this may be a relic.
Canvas
corresponds to the native typeWebGLCanvas
, which from what I can tell, no longer exists. This should shift toCanvasElement
.
I see, I was assuming the Canvas-class is a workaround because there are no union types in Dart to represent CanvasElement | OffscreenCanvas
and dynamic would be an ugly return type.
It isn't clear to me in what context I should expect
OffscreenCanvasRenderingContext2D
to occur. Is it simply whengetContext
is called on anOffscreenCanvas
object?
Yes, that's what I understand. Wouldn't make sense otherwise.
In that case, addressing your previous question, wouldn't I expect
RenderingContext
andRenderingContext2
to only returnCanvasElement
s? If they returned anOffscreenCanvas
, I would expect them to beOffscreenCanvasRenderingContext
s instead.
Yes, that sounds reasonable, though I don't see any OffscreenCanvasRenderingContext
other than the 2D one, even in the spec: https://html.spec.whatwg.org/multipage/canvas.html#the-offscreencanvas-interface
typedef (OffscreenCanvasRenderingContext2D or ImageBitmapRenderingContext or WebGLRenderingContext or WebGL2RenderingContext or GPUCanvasContext) OffscreenRenderingContext;
I still think we should get rid of
Canvas
in favor ofCanvasElement
, but yes, if it can be nullable, it should be written as such. When would the condition "It might be null if it is not associated with a canvas element or an OffscreenCanvas object." hold true?
I have absolutely no idea when it can be null. It's just the MDN page (https://developer.mozilla.org/en-US/docs/Web/API/WebGLRenderingContext/canvas) saying it can be null, and the current RenderingContext2
implementation that already says Canvas?
. The spec doesn't say anything about null either: https://www.khronos.org/registry/webgl/specs/latest/1.0/#WebGLRenderingContext.
Using the names from the spec, both WebGLRenderingContext
and WebLG2RenderingContext
use WebGLRenderingContextBase
, which has the line readonly attribute (HTMLCanvasElement or OffscreenCanvas) canvas;
, same goes for WebGPU https://gpuweb.github.io/gpuweb/#canvas-context. So yeah, no clue where that nullability in the MDN or the relic Canvas?
is even coming from.
It seems like the only major change here is moving
RenderingContext.canvas
andRenderingContext2.canvas
toCanvasElement?
types.
As there doesn't seem to be nullability according to the spec RenderingContext.canvas
seems fine like it currently is except that OffscreenCanvas
is also a possible return type, like defined in the spec. I haven't used OffscreenCanvas yet, so I have not run into that issue, but I just tried the example/repro code with an OffscreenCanvas (final webgl1ctx = canvas1.transferControlToOffscreen().getContext('webgl') as RenderingContext;
), and both webgl and webgl2 return a runtimeType
of OffscreenCanvas$
in Chrome when accessing canvas
.
So.. solution one is to have the signature for both to be CanvasElement get canvas
until something like CanvasElement | OffscreenCanvas
may be possible in the far future. Or second solution have some workaround like that Canvas
class. The first solution would be less breaking, as it's just a signature change for webgl2 but current code would keep working, while the second solution would break current code but would be clearer how/that it's possible to get a OffscreenCanvas
.
I'm glad it's not my decision to make ;).
EDIT: I'm totally fine with solution one. Get rid of that Canvas
thing and have RenderingContext2
simply return a non nullable CanvasElement
. No need for additional OffscreenCanvasRenderingContext
-variations that don't exist in the spec. If I ever need to use a OffscreenCanvas
, I'm now aware how to access that one. When I need the canvas, I only need it for accessing width and height or drawing it onto another canvas, and that works for both even without casting to OffscreenCanvas
, so it doesn't really matter that the returned object may be an OffscreenCanvas
instead of a CanvasElement
. At least to me, and not seeing any other issues concerning OffscreenCanvas
this way, probably to no one else either.
Can/will this be fixed with Dart 3.0?
For completeness I've added 2d and OffscreenCanvas
as a repro (spoiler: only the 2d context has the correct signature for CanvasElement
and OffscreenCanvas
):
index.html
<!DOCTYPE html>
<script defer src="main.dart.js"></script>
<canvas id='webgl1'></canvas>
<canvas id='webgl2'></canvas>
<canvas id='ctx2d'></canvas>
<script>
const canvas1 = document.querySelector('#webgl1');
const webgl1ctx = canvas1.getContext('webgl');
console.log(`Javascript webgl1ctx.canvas = ${webgl1ctx.canvas}`); // HTMLCanvasElement
const canvas2 = document.querySelector('#webgl2');
const webgl2ctx = canvas2.getContext('webgl2');
console.log(`Javascript webgl2ctx.canvas = ${webgl2ctx.canvas}`); // HTMLCanvasElement
const canvas2d = document.querySelector('#ctx2d');
const ctx2d = canvas2d.getContext('2d');
console.log(`Javascript ctx2d.canvas = ${ctx2d.canvas}`); // HTMLCanvasElement
const offscreen1 = new OffscreenCanvas(256, 256);
const offscreenWebgl1ctx = offscreen1.getContext('webgl');
console.log(`Javascript offscreenWebgl1ctx.canvas = ${offscreenWebgl1ctx.canvas}`); // OffscreenCanvas
const offscreen2 = new OffscreenCanvas(256, 256);
const offscreenWebgl2ctx = offscreen2.getContext('webgl2');
console.log(`Javascript offscreenWebgl2ctx.canvas = ${offscreenWebgl2ctx.canvas}`); // OffscreenCanvas
const offscreen2d = new OffscreenCanvas(256, 256);
const offscreenCtx2d = offscreen2d.getContext('2d');
console.log(`Javascript offscreenCtx2d.canvas = ${offscreenCtx2d.canvas}`); // OffscreenCanvas
</script>
main.dart
import 'dart:html';
import 'dart:web_gl';
void main() {
// CanvasElement
final canvas1 = querySelector('#webgl1') as CanvasElement;
final webgl1ctx = canvas1.getContext('webgl') as RenderingContext;
print('Dart webgl1ctx.canvas = ${webgl1ctx.canvas.runtimeType}'); // correct: CanvasElement
final canvas2 = querySelector('#webgl2') as CanvasElement;
final webgl2ctx = canvas2.getContext('webgl2') as RenderingContext2;
print('Dart webgl2ctx.canvas = ${webgl2ctx.canvas.runtimeType}'); // wrong: signature says Canvas, but runtimeType is CanvasElement
print('Dart webgl2ctx.canvas.canvas = ${webgl2ctx.canvas?.canvas}'); // null
print('Dart webgl2ctx.canvas.offscreenCanvas = ${webgl2ctx.canvas?.offscreenCanvas}'); // null
final canvas2d = querySelector('#ctx2d') as CanvasElement;
final ctx2d = canvas2d.context2D;
print('Dart ctx2d.canvas = ${ctx2d.canvas.runtimeType}'); // correct: CanvasElement
// OffscreenCanvas
final offscreen1 = OffscreenCanvas(256, 256);
final offscreenWebgl1ctx = offscreen1.getContext('webgl') as RenderingContext;
print('Dart offscreenWebgl1ctx.canvas = ${offscreenWebgl1ctx.canvas.runtimeType}'); // wrong: signature says CanvasElement, but runtimeType is OffscreenCanvas$
final offscreen2 = OffscreenCanvas(256, 256);
final offscreenWebgl2ctx = offscreen2.getContext('webgl2') as RenderingContext2;
print('Dart offscreenWebgl2ctx.canvas = ${offscreenWebgl2ctx.canvas.runtimeType}'); // wrong: signature says Canvas, but runtimeType is OffscreenCanvas$
print('Dart offscreenWebgl2ctx.canvas.canvas = ${offscreenWebgl2ctx.canvas?.canvas}'); // null
print('Dart offscreenWebgl2ctx.canvas.offscreenCanvas = ${offscreenWebgl2ctx.canvas?.offscreenCanvas}'); // null
final offscreenCanvas2d = OffscreenCanvas(256, 256);
final offscreenCtx2d = offscreenCanvas2d.getContext('2d') as OffscreenCanvasRenderingContext2D;
print('Dart offscreenCtx2d.canvas = ${offscreenCtx2d.canvas.runtimeType}'); // correct: OffscreenCanvas$
}
Possible fixes I can think of:
OffscreenRenderingContext
and OffscreenRenderingContext2
like the OffscreenCanvasRenderingContext2D
but the latter actually exists in the spec, not so the former - and return CanvasElement
for RenderingContext
and RenderingContext2
and OffscreenCanvas
for the newly introduced classes and remove the Canvas
-class. Maybe even remove the nullability of CanvasElement
, as that only exists in the MDN but not in the specs and I'm not aware of a way to create a context without a canvas.canvas
getter for the RenderingContext2
actually return a Canvas
where you can either access the CanvasElement
or the OffscreenCanvas
(in this case the CanvasElement
should be nullable because it can be null when created from an OffscreenCanvas
). But then it would also be needed for the RenderingContext
and Canvas
may not need to be nullable for both for the same reason as solution 1.I seem to have missed responding to your previous comment, sorry! We've been busy with moving towards more interop features (with inline classes and expanding the functionality of the existing @staticInterop) and with it, an interop-based DOM library instead. Part of the motivation was issues like these where the Dart code surrounding the native calls became either stale or incorrect.
Can/will this be fixed with Dart 3.0?
This specific issue won't be addressed within dart:html in the next few months, but https://github.com/dart-lang/web (feel free to take a look, but it's very much a WIP to refactor/clean up/etc.) will hopefully be available in the next few months for people to use for Dart 3. It'll expose the DOM (and browser APIs in general) with interop and shy away from some of the Dart-esque decisions we've made with dart:html.
We'll be sticking to the WebRef specs largely to expose these types, so I expect we'll come closer to addressing your first solution. The nullability too will adhere to the spec instead. With the newer library, we should conform to the behavior of the JS code in your above html code.
The CanvasElement associated with a RenderingContext2 when using webgl2 is missing:
The
CanvasElement
is missing with ddc and dart2js.Dart SDK version: 2.17.0-132.0.dev (dev) (Sun Feb 20 15:40:08 2022 -0800) on "windows_x64"