Add HTML-in-Canvas APIs - #11588
Add HTML-in-Canvas APIs#11588foolip wants to merge 129 commits into
Conversation
Handwavy things that need fleshing out are marked with 👋
Kaiido
left a comment
There was a problem hiding this comment.
Glad to see this being worked on, thanks.
Not quite sure how much discussion should be held at this stage. So to note, this doesn't seem to fully match the latest state of https://github.com/WICG/html-in-canvas. e.g. the rename to drawHTMLElement. The layoutsubtree attribute is also missing along with the implications to the existing fallback contents.
Still, thanks for making this move.
|
@Kaiido thank you for the review! I've fleshed things out more, renaming to There's still some handwaving going on of course, in particular what causes the subtree to be laid out but not painted. |
|
I've fleshed this out some more now, in particular the hit testing. |
There was a problem hiding this comment.
One common complain with the use of dictionaries in the Canvas2D API is that this makes GC kick in very often during animations which has a non-negligible performance cost.
This API shape makes a big use of such dictionaries with one for the wrapper CanvasElementHitTestRegion and then a nested one for the CanvasHitTestRect, and I guess there will be scenarios where multiple of these will need to be updated at every frame. Since the values are copied over from the passed objects to new internal objects, it's unclear if even a careful author, who would try to reuse the same objects, could avoid GC at all here.
On the other hand, I really like how this API shape enables future additions like using a Path2D, or even a bitmap mask, instead of a CanvasHitTestRect. (btw can we bikeshed on rect for that purpose?)
It's not my area of expertise, but would an actual exposed interface allow for non copy from JS, so that authors can just update the regions instead of setting new ones?
This reverts commit d3b6b03.
| <h3>The <dfn element-attr for="html-global"><code | ||
| data-x="attr-drawable">drawable</code></dfn> attribute</h3> | ||
|
|
||
| <p>All <span>HTML elements</span> may have the <code data-x="attr-drawable">drawable</code> |
There was a problem hiding this comment.
Wasn't it supposed to be set on Element? (WICG/html-in-canvas#134 (comment))
There was a problem hiding this comment.
Yeah, should be elements, not "HTML elements", so SVG and MathML can be drawable.
There was a problem hiding this comment.
There are three parts to how making drawable work on any element:
- The algorithms that create and draw snapshots should just talk about elements with the
drawableattribute. This is already the case, so per current spec it should work. - Where
drawableis reflected. Instead of putting things onElementwe more often useHTMLOrSVGOrMathMLElement, and I think that's probably the right call here too. - Conformance or requirements for authors. It should really be up to the SVG and MathML specs to say where
drawableattributes can appear. I'll file issues on those specs.
There was a problem hiding this comment.
Assuming that MathML and SVG agree to allow drawable, which it seems like they will, what do we say here? That is, assuming everything @foolip says above happens, what would this text say? Is it just "All ELements ..."?
| <li><p>When an element is drawn onto the <code>canvas</code> (or has its <span | ||
| data-x="concept-element-canvas-transform">canvas transform</span> set), the element's position is |
There was a problem hiding this comment.
Does that mean that if we draw an element on a canvas, then move it back into another container without calling clearElementGeometry, that element will still have the offset for hit testing?
There was a problem hiding this comment.
@Kaiido -- No, that's not the intention. We are still dialing in the behavior on this, but our intention is that the canvas transform associated with an element will be cleared when a DOM change occurs that makes the element no longer eligible to be drawn in the canvas.
There was a problem hiding this comment.
@szager-chromium can you give me the details of what needs to be spec'd here? Removal steps that look clear the canvas transform and any associated snapshot? Should the same also happen (synchronously) when the drawable attribute is removed?
| };</code></pre> | ||
|
|
||
| <dl class="domintro"> | ||
| <dt><code data-x=""><var>elementImage</var>.<span subdfn data-x="dom-ElementImage-width">width</span></code></dt> |
There was a problem hiding this comment.
Need more explication of width and height. Here's some non-conforming text:
These dimensions, when used as the dx and dy parameters to drawElementImage, will cause the image to be drawn at the same size it would be were it placed outside the canvas. when the dx and dy parameters are omitted from a call to drawElementImage, these are the default values that are used.
For the WebGL and WebGPU interfaces, if these dimensions are used as the destination size when drawing to a texture; and the texture is then blitted directly into the canvas (i.e., 1:1 texel-to-canvas-pixel ratio), then the element will appear in the canvas with the same size it would have were it placed outside the canvas.
I think it would be nifty if these definitions could somehow be worked into the existing definition of "natural dimensions", but that's beyond my spec-fu.
There was a problem hiding this comment.
will cause the image to be drawn at the same size it would be were it placed outside the canvas
Should note (if not noted already somewhere) whether this is assuming the default canvas sizing (1 canvas bitmap pixel = 1 CSS px) or assumes that the developer has made their canvas hidpi (which can be approximated with devicePixelRatio or done precisely using ResizeObserver).
I think it would be nifty if these definitions could somehow be worked into the existing definition of "natural dimensions", but that's beyond my spec-fu.
I'm not an expert in the CSS/HTML sizing stuff either, but I think you'll want a different concept than "natural dimensions". Imagine for example <canvas layoutsubtree><img src="/api/v1/web-embed/proxy?proxyUrl=https%3A%2F%2Fgithub.com%2Fwhatwg%2Fhtml%2Fpull%2Fmy5x5.png&mode=full" style="width:10px"></canvas>. The img is an "object" with "natural dimensions" of 5x5, but it's also an HTML element with a layout size of 10x10 (CSS) px (IDK the spec term for this). Since HIC views the DOM subtree and not the object, I think it should see a size of 10x10 (or 10*devicePixelRatio or whatever)
There was a problem hiding this comment.
The "natural size" makes no assumptions about the CSS-to-canvas-grid ratio.
Agreed that we can't just use "natural dimensions" outright, but this does feel like a related concept so I hope it can be proximal to "natural dimensions".
There was a problem hiding this comment.
As I understand it, the key problem is that we can't say "border box width in CSS pixels" because that's not what it is. It's actually the border box width/height multiplied by the ratio of output bitmap pixel size to CSS pixel size. Is that actually sufficiently descriptive to use?
We have the necessary language down below in the "Update the Rendering" section. I think the cleanest way to do this would be to create concepts called "snapshot width" and "snapshot height", then define them using the language from "Update the Rendering", and then you can refer to the concept every time you need this particular width and height.
There was a problem hiding this comment.
@schenney-chromium Snapshots already have a width and height, see right after the definition of "element image snapshot". See also the line starting with "Let width be element's border box's width in CSS pixels multiplied by scaleX".
Does this already do the right thing, then?
|
|
||
| <div algorithm> | ||
| <p>The <dfn method for="HTMLCanvasElement"><code | ||
| data-x="dom-canvas-updateElementGeometry">updateElementGeometry(<var>element</var>, |
There was a problem hiding this comment.
The argument should be ElementOrElementImage, and the error conditions need to be updated as well. Same goes for OffscreenCanvas.updateElementGeometry, below.
There was a problem hiding this comment.
The argument already is (Element or ElementImage) in the IDL and the "get element image snapshot" algorithm handles both, so this already seems correct to me. Do I misunderstand?
This reverts commit bbeffd6.
| <p>The <dfn element-attr for="canvas"><code | ||
| data-x="attr-canvas-layoutsubtree">layoutsubtree</code></dfn> attribute is a <span>boolean | ||
| attribute</span>. If present, <span data-x="concept-tree-descendant">descendants</span> of the | ||
| <code>canvas</code> element are laid out, so that those with the <code |
There was a problem hiding this comment.
Slightly wrong now, I think? Only drawable elements (and their descendants) are laid out, not all canvas descendants.
| data-x="attr-canvas-layoutsubtree">layoutsubtree</code></dfn> attribute is a <span>boolean | ||
| attribute</span>. If present, <span data-x="concept-tree-descendant">descendants</span> of the | ||
| <code>canvas</code> element are laid out, so that those with the <code | ||
| data-x="attr-drawable">drawable</code> attribute specified can be drawn using <code |
There was a problem hiding this comment.
We should have a definition for the drawable element, so we're not having to refer to them verbosely in several places. The explainer just uses "drawable element", which I think works, and then also "drawable subtree" for the descendants that aren't part of another drawable tree.
| <span>output bitmap</span> pixels), a <dfn | ||
| data-x="concept-element-image-snapshot-translation-matrix">translation matrix</dfn>, and a <dfn | ||
| data-x="concept-element-image-snapshot-scaling-matrix">scaling matrix</dfn>. A snapshot captures | ||
| the <span>border box</span> of the element it was created from. Descendants of the element that |
There was a problem hiding this comment.
I suspect we want to reuse the same language as "capture the image" in View Transitions here, with appropriate adjustments (only rendering the drawable subtree, probably adjusting which properties are ignored). It even has the right pattern for the nested drawables.
There was a problem hiding this comment.
Also, probably want to remove the "border box" text here. The border-box is used to define the origin of the drawing coords, and the default width/height, but the snapshot isn't actually limited to it in any way.
There was a problem hiding this comment.
I suspect we want to reuse the same language as "capture the image" in View Transitions here, with appropriate adjustments (only rendering the drawable subtree, probably adjusting which properties are ignored). It even has the right pattern for the nested drawables.
I don't agree with this. The view transitions capture is different in almost all of the details that it discusses. captureElementImage cannot capture the document root, does not necessarily capture ink overflow, still captures scrollbars, does not apply effects like opacity and filters from the ancestor chain that would otherwise affect the rendering. And we do not produce an image.
There was a problem hiding this comment.
Also, probably want to remove the "border box" text here. The border-box is used to define the origin of the drawing coords, and the default width/height, but the snapshot isn't actually limited to it in any way.
We do want to remove border box but it needs to be replaced with some clear statement like "everything within the src rect that is generated by drawing the element and its descendants".
There was a problem hiding this comment.
Source rect has no place here, there is no source rect associated with paint snaphsot. The paint snapshot has no clipping, and it always includes all sources of ink overflow regardless of extent. For example, if the element has a box-shadow with blur, then the paint snapshot includes all the information necessary to rasterize the element including the infinite box shadow.
| data-x="concept-element-image-originating-canvas">originating <code>canvas</code> element</span> | ||
| is not <span>this</span>, then throw a <code>TypeError</code>.</p></li> | ||
|
|
||
| <li><p>Let <var>targetElement</var> be <var>snapshot</var>'s <span |
There was a problem hiding this comment.
Isn't this just the element argument? This feels like a strange bit of indirection. (We will need to fetch the originating element if an ElementImage is passed, tho.)
There was a problem hiding this comment.
See my comment above.
| method steps are:</p> | ||
|
|
||
| <ol> | ||
| <li><p>Let <var>snapshot</var> be the result of <span>get element image snapshot</span> given |
There was a problem hiding this comment.
Why are we capturing a snapshot here, when we're just fetching data from the element?
There was a problem hiding this comment.
The "get element image snapshot" algorithm only gets the snapshot from the Element-or-ElementImage argument, it doesn't capture a snapshot. That only happens in the rendering loop before "paint" events are fired.
There was a problem hiding this comment.
I think Tab's point is that if step 1 is get element image snapshot given element then the snapshot's element must be element, then we just get the element again and set something on it. Why go round the circle? Is it to handle Element or ElementImage, in which case the algorithm should switch on which one we have.
If <var>element</var> is an Element:
Set originatingElement to <var>element</var>
Otherwise
Set originatingElement to <var>element</var>'s <span data-x="concept-element-image-snapshot-originating-element">originating element</span>
There was a problem hiding this comment.
It is roundabout and targetElement can end up being the same as element, but the snapshot is also how we make sure that the element belongs to the canvas (this). I've tried to do all checks like this, and never check if there's a drawable attribute or walk a tree to check preconditions in the API, instead leaving that entirely to snapshot creation time in the rendering loop.
Does that match the Chromium implementation? Concretely, can elm.updateElementGeometry() be called immediately after elm is inserted as a canvas descendent, or does the rendering loop have to spin once first?
| <code>TypeError</code>.</p></li> | ||
|
|
||
| <li> | ||
| <p>Paint the area of <var>snapshot</var> given by <var>sourceRect</var> to the specified |
There was a problem hiding this comment.
Assuming sourceRect is passed to the snapshot-capturing algo instead, the mention of it here can be dropped; the image you get will be perfectly sized already.
There was a problem hiding this comment.
Also, "specified rectangular area" isn't actually specified. You need to construct this area from dx/y/w/h.
There was a problem hiding this comment.
Yeah, you need to define the destination rect and then refer to it here.
There was a problem hiding this comment.
There's similar language (search "specified rect") for clearRect() and fillRect(), but it's not as obvious here, so I'll use more words.
| <p>Paint the area of <var>snapshot</var> given by <var>sourceRect</var> to the specified | ||
| rectangular area.</p> | ||
|
|
||
| <p>The element image must be painted in its original orientation, even if <var>dw</var> or |
There was a problem hiding this comment.
Does this imply that the top left corner of the source isn't necessarily painted at (dx, dy), but might be dx+dw and/or dy+dh if w/h are negative? Or is it always at dx/dy and you ignore the sign of width/height, or something else?
There was a problem hiding this comment.
It means the top left may not be at dx/dy, yes. This is a deliberate choice to match the behavior of drawImage.
| <h3>The <dfn element-attr for="html-global"><code | ||
| data-x="attr-drawable">drawable</code></dfn> attribute</h3> | ||
|
|
||
| <p>All <span>HTML elements</span> may have the <code data-x="attr-drawable">drawable</code> |
There was a problem hiding this comment.
Yeah, should be elements, not "HTML elements", so SVG and MathML can be drawable.
|
|
||
| <ul> | ||
| <li> | ||
| <p>Its descendants are laid out. For the purposes of this layout, the <code>canvas</code> parent |
There was a problem hiding this comment.
Only drawable descendants.
There was a problem hiding this comment.
We lay out everything under a content=drawable canvas because technically we need to. Other browsers would also have this requirement.
|
|
||
| <li><p>Descendants are visible to assistive technologies (ATs) by default, but are marked as | ||
| offscreen (allowing assistive technologies to filter them). When an element with a <code | ||
| data-x="attr-drawable">drawable</code> attribute specified is drawn onto the <code>canvas</code>, |
There was a problem hiding this comment.
only if {preserveElementGeometry:true} wasn't passed
There was a problem hiding this comment.
Also, it's updated from updateElementGeometry() even if not drawn.
There was a problem hiding this comment.
Yeah. Maybe use "has non-null canvas transform" or "is in the drawn elements list" or whatever that concept was.
schenney-chromium
left a comment
There was a problem hiding this comment.
The most significant things here are improving the concept of the canvas content state and clearing up what width and height are for snapshots.
It is editorial in the sense that it's about spec clarity rather than the behaviors.
| <ul class="brief"> | ||
| <li>The <dfn data-x-href="https://drafts.csswg.org/css-transforms/#transform-property">'transform'</dfn> property</li> | ||
| <li>The <dfn data-x-href="https://drafts.csswg.org/css-transforms/#transform-origin-property">'transform-origin'</dfn> property</li> | ||
| <li>The <dfn data-x-href="https://drafts.csswg.org/css-transforms/#transformation-matrix">transformation matrix</dfn></li> |
There was a problem hiding this comment.
| <li>The <dfn data-x-href="/api/v1/web-embed/proxy?proxyUrl=https%3A%2F%2Fdrafts.csswg.org%2Fcss-transforms%2F%23transformation-matrix&mode=full">transformation matrix</dfn></li> | |
| <li>The <dfn data-x-href="/api/v1/web-embed/proxy?proxyUrl=https%3A%2F%2Fdrafts.csswg.org%2Fcss-transforms%2F%23transformation-matrix&mode=full">'transformation matrix'</dfn></li> |
There was a problem hiding this comment.
That doesn't seem right, "transformation matrix" is a concept, not the name of a property.
| undefined <span data-x="dom-canvas-toBlob">toBlob</span>(<span>BlobCallback</span> _callback, optional DOMString type = "image/png", optional any quality); | ||
| <span>OffscreenCanvas</span> <span data-x="dom-canvas-transferControlToOffscreen">transferControlToOffscreen</span>(); | ||
|
|
||
| [<span>CEReactions</span>, <span data-x="xattr-Reflect">Reflect</span>] attribute DOMString <span data-x="dom-canvas-content">content</span>; |
There was a problem hiding this comment.
Move this up to right below width and height to keep the attributes together.
| data-x="concept-canvas-webgpu">webgpu</dfn> by algorithms defined in this specification.</p> | ||
|
|
||
| <p>To keep track of when the <code data-x="event-paint">paint</code> event needs to be fired, a | ||
| <code>canvas</code> element also has a <dfn data-x="concept-canvas-force-paint-event">force paint |
There was a problem hiding this comment.
| <code>canvas</code> element also has a <dfn data-x="concept-canvas-force-paint-event">force paint | |
| <code>canvas</code> element has a <dfn data-x="concept-canvas-force-paint-event">force paint |
My interpretation of the "also" in the prose above this is that "in addition to a rendering context bound to the canvas, it also has a canvas context mode ..." So for the things we are adding I would not include the "also".
| <span>output bitmap</span> pixels), a <dfn | ||
| data-x="concept-element-image-snapshot-translation-matrix">translation matrix</dfn>, and a <dfn | ||
| data-x="concept-element-image-snapshot-scaling-matrix">scaling matrix</dfn>. A snapshot captures | ||
| the <span>border box</span> of the element it was created from. Descendants of the element that |
There was a problem hiding this comment.
I suspect we want to reuse the same language as "capture the image" in View Transitions here, with appropriate adjustments (only rendering the drawable subtree, probably adjusting which properties are ignored). It even has the right pattern for the nested drawables.
I don't agree with this. The view transitions capture is different in almost all of the details that it discusses. captureElementImage cannot capture the document root, does not necessarily capture ink overflow, still captures scrollbars, does not apply effects like opacity and filters from the ancestor chain that would otherwise affect the rendering. And we do not produce an image.
| <span>output bitmap</span> pixels), a <dfn | ||
| data-x="concept-element-image-snapshot-translation-matrix">translation matrix</dfn>, and a <dfn | ||
| data-x="concept-element-image-snapshot-scaling-matrix">scaling matrix</dfn>. A snapshot captures | ||
| the <span>border box</span> of the element it was created from. Descendants of the element that |
There was a problem hiding this comment.
Also, probably want to remove the "border box" text here. The border-box is used to define the origin of the drawing coords, and the default width/height, but the snapshot isn't actually limited to it in any way.
We do want to remove border box but it needs to be replaced with some clear statement like "everything within the src rect that is generated by drawing the element and its descendants".
| <dt><code data-x=""><var>event</var>.<span subdfn data-x="dom-CanvasPaintEvent-changedElements">changedElements</span></code></dt> | ||
|
|
||
| <dd> | ||
| <p>Returns a <span>frozen array</span> of the elements that have changed.</p> |
There was a problem hiding this comment.
"... and may need to be drawn into the canvas to update their appearance".
| <h3>The <dfn element-attr for="html-global"><code | ||
| data-x="attr-drawable">drawable</code></dfn> attribute</h3> | ||
|
|
||
| <p>All <span>HTML elements</span> may have the <code data-x="attr-drawable">drawable</code> |
There was a problem hiding this comment.
Assuming that MathML and SVG agree to allow drawable, which it seems like they will, what do we say here? That is, assuming everything @foolip says above happens, what would this text say? Is it just "All ELements ..."?
| <p>All <span>HTML elements</span> may have the <code data-x="attr-drawable">drawable</code> | ||
| content attribute set. The <code data-x="attr-drawable">drawable</code> attribute is a | ||
| <span>boolean attribute</span>. When present on an element that is a descendant of a | ||
| <code>canvas</code> element whose <code data-x="attr-canvas-content">content</code> attribute is |
There was a problem hiding this comment.
This should refer to the internal state I talked about way back near the top, rather than the attribute.
|
|
||
| <ul> | ||
| <li> | ||
| <p>Its descendants are laid out. For the purposes of this layout, the <code>canvas</code> parent |
There was a problem hiding this comment.
We lay out everything under a content=drawable canvas because technically we need to. Other browsers would also have this requirement.
|
|
||
| <li><p>Descendants are visible to assistive technologies (ATs) by default, but are marked as | ||
| offscreen (allowing assistive technologies to filter them). When an element with a <code | ||
| data-x="attr-drawable">drawable</code> attribute specified is drawn onto the <code>canvas</code>, |
There was a problem hiding this comment.
Yeah. Maybe use "has non-null canvas transform" or "is in the drawn elements list" or whatever that concept was.
schenney-chromium
left a comment
There was a problem hiding this comment.
Some clarification after chatting with Stefan.
| <span>OffscreenCanvas</span> <span data-x="dom-canvas-transferControlToOffscreen">transferControlToOffscreen</span>(); | ||
|
|
||
| [<span>CEReactions</span>, <span data-x="xattr-Reflect">Reflect</span>] attribute DOMString <span data-x="dom-canvas-content">content</span>; | ||
| <span>ElementImage</span> <span data-x="dom-canvas-captureElementImage">captureElementImage</span>(<span>Element</span> element); |
There was a problem hiding this comment.
If we can capture ink overflow etc do we need to specify a src rect here after all? Or are we actually capturing a snapshot with width/height equal to the ink overflow rect?
| data-x="concept-element-image-snapshot-width">width</span> and <span | ||
| data-x="concept-element-image-snapshot-height">height</span>, respectively.</p></li> | ||
|
|
||
| <li><p>If not given, <var>sourceRect</var> must default to a source rectangle at (0, 0) with |
There was a problem hiding this comment.
I don't think we can kill the step because there is no src rect for createElementImage and we want to be able to use the same algorithm. The default src rect for createElementImage is not the same as the one for drawElementImage, or is it? If it is the same, is it impossible to get ink overflow with createElementImage?
| snapshot is immutable once created and is not affected by any later changes to the element.</p> | ||
|
|
||
| <p>The internal representation of an <span data-x="concept-element-image-snapshot">element image | ||
| snapshot</span> is <span>implementation-defined</span>, but must preserve crisp text and vector |
There was a problem hiding this comment.
Is there something more rigorous than "must preserve crisp text and vector graphics"? Like "must be scalable without introducing pixelation effects"?
| reference, an <dfn data-x="concept-element-image-originating-canvas">originating | ||
| <code>canvas</code> element</dfn> weak reference, <dfn | ||
| data-x="concept-element-image-snapshot-width">width</dfn> and <dfn | ||
| data-x="concept-element-image-snapshot-height">height</dfn> (dimensions in the <span |
There was a problem hiding this comment.
It may be better to omit "dimensions in the originating canvas element's output bitmap pixels" here. That's sort of half an explanation, which is probably worse than no explanation at all. Somewhere in the spec (but probably not here) we should have a clear explanation of the geometry concepts, and we should link to it wherever geometry comes into play.
I'm going to offer up some possible language for describing the geometry here, but I'm not sure where it should go in the spec...
First, we need to rigorously define a term which is conceptually similar to the density-corrected natural width and height of <img>'s. I'm going to use the term natural size here, but that overlaps with CSS natural dimensions so we need a differentiated term.
"""
By default, the area of an element drawn by a call to drawElementImage is its border box. Any regular or ink overflow that extends beyond the border box is clipped out. The sx, sy, swidth, and sheight parameters provide a way to adjust the default border box clip -- it can be expanded to include (regular and ink) overflow, or contracted to some area within the border box.
The dwidth and dheight arguments apply a scaling factor when drawing an element. If the dwidth and dheight arguments are not specified in a call to drawElementImage at a time when the canvas's current transformation matrix is the identity matrix, and regardless of any values provided for the sx, sy, swidth, and sheight parameters, the implementation must ensure that the element appears in the canvas with the same on-screen proportions as it would have were it placed outside the canvas with identical computed style.
The natural size of an element is defined as the values, when supplied as the dwidth and dheight arguments to drawElementImage, at a time when the canvas's current transformation matrix is the identity matrix, and with no values provided for the sx, sy, swidth, or sheight arguments, which will cause the element to appear in the canvas with the same on-screen proportions as the element would have were it placed outside the canvas with identical computed style.
The width and height properties of ElementImage contain the natural size of the element at the time the ElementImage was generated.
"""
This is the bedrock principle of the geometry stuff, and everything pretty much derives from it.
(See WHATWG Working Mode: Changes for more details.)
/browsers.html ( diff )
/canvas.html ( diff )
/dnd.html ( diff )
/dom.html ( diff )
/index.html ( diff )
/indices.html ( diff )
/infrastructure.html ( diff )
/interaction.html ( diff )
/popover.html ( diff )
/references.html ( diff )
/rendering.html ( diff )
/webappapis.html ( diff )