Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a layer and filter interface in the 2D canvas #9537

Open
wants to merge 9 commits into
base: main
Choose a base branch
from

Conversation

graveljp
Copy link

@graveljp graveljp commented Jul 19, 2023

This adds new beginLayer and endLayer functions to open and close layers
in the canvas. While layers are active, draw calls operate on a separate
texture that gets composited to the parent output bitmap when the layer
is closed. An optional filter can be specified in beginLayer, allowing
effects to be applied to the layer's texture when it's composited its
parent.

Fixes #8476

(See WHATWG Working Mode: Changes for more details.)


/acknowledgements.html ( diff )
/canvas.html ( diff )
/index.html ( diff )
/infrastructure.html ( diff )

Copy link
Member

@Kaiido Kaiido left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Impressive work on this.
I still have some concerns about the layer rendering state being picked from the global settings instead of being defined along the filter as a parameter, and about the "when a canvas is presented" concept, but otherwise it all seems to make sense, I think.

Also, I've been a bit quick over the CanvasFilters part as I suppose it's mostly what had already been reviewed on #6763 by Aaron.

source Outdated Show resolved Hide resolved
source Outdated Show resolved Hide resolved
has one, is always just an alias to a <code>canvas</code> element's bitmap. When layers are
opened, implementations must behave as if draw calls operate on a separate <span>output
bitmap</span> that gets composited to the parent <span>output bitmap</span> when the layer is
closed. If the <code>canvas</code> element's bitmap needs to be presented while layers are opened,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"presented" is unfortunately a bit unclear as a concept, should it be clearly defined?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree, this has actually caused me some confusion as I was trying to understand how the canvas gets consumed by the event loop. I added a "concept-canvas-presented" definition above to clarify this.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great.
Hopefully if/when the "update the rendering" steps are rewritten to finally match the implementations we'll be able to add a line about that in there too?
My only remaining concern about this would be about other canvas consumers that don't go through the "check the usability of the image" algo. On the top of my head I can only think of the mediacapture-fromelement (canvas.captureStream()), where they never defined when the canvas frame should be moved to the stream.
I guess having this definition would help them to specify the apparent implementation which seems to be to wait for the next "update the rendering". (c.f. w3c/mediacapture-fromelement#94 and linked).

source Outdated Show resolved Hide resolved
source Outdated Show resolved Hide resolved
source Outdated

<div w-nodev>

<p>Before any operations could access the <code>canvas</code> element's bitmap pixels, the
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given that reading operations do throw if a layer is open, this leaves only the ill-defined "when the canvas is presented" case that could reach it, right?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That was the intent. But I removed this section. The CanvasRenderingContext2D now has a top level output bitmap and a current output bitmap. The top level output bitmap can always be presented to the user even when layers are opened. When layers are opened, draw operations are performed in a separate bitmap via current output bitmap.

source Outdated
Comment on lines 64618 to 64630
was flushed by running the steps for <dfn>restoring the drawing state stack</dfn>:</p>

<ol>
<li><p><span data-x="list iterate">For each</span> <var>stateEntry</var> of
<var>stateBackup</var>:</p></li>

<ol>
<li><p><span data-x="stack push">Push</span> <var>stateEntry</var> onto the <span>drawing state
stack</span>.</p></li>
</ol>

<li><p>Restore the context's current <span>drawing state</span> by running the <code
data-x="dom-context-2d-restore">restore()</code> method steps.</p></li>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So this doesn't restore any of the bitmaps, right? This means that if we have something like

ctx.globalAlpha = 0.2;
ctx.beginLayer();
ctx.fillRect(0, 0, 50, 50);

The rectangle will keep getting more opaque every time the canvas is "presented". Which is... every frame while it's visible on the page? Or every time the page has to be repainted? Or something else?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This section has been removed.

The idea was that when presenting a frame, all the layers would have been closed, the draw calls would have been rasterized, and then, all layers would have been re-opened. This algorithm was describing the logic for re-opening the layers. Only the states were to be re-populated, not the draw calls. On later frame, only new draw calls would get drawn.

This was all replaced with an alternative strategy where unclosed layers are never presented. Instead, only the draw calls up to the first beginLayer() gets rasterized. The content of the layer is preserved for the next frame, where layers can be closed and rasterized as a whole.

source Outdated
Comment on lines 64319 to 64320
<li><p>Set all current <span data-x="drawing state">drawing states</span> to the values they have
in <var>parentDrawingStates</var>.</p></li>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should probably exclude the canvas layer state.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This one is tricky to phrase. Let me know what you think.

source Outdated Show resolved Hide resolved
source Outdated Show resolved Hide resolved
source Outdated Show resolved Hide resolved
source Outdated Show resolved Hide resolved
source Outdated Show resolved Hide resolved
source Outdated
<span>CanvasFilterInput</span>? <dfn data-x="dom-context-2d-beginLayer-options-filter">filter</dfn> = null;
};

interface mixin <dfn interface>CanvasLayers</dfn> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will/should PaintRenderingContext2D also get this mixin?

Copy link
Author

@graveljp graveljp Aug 7, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It should probably. Since it's spec'ed in a different place, I can file a ticket to track this separately. Would that make sense?

source Outdated Show resolved Hide resolved
source Show resolved Hide resolved
source Show resolved Hide resolved
source Show resolved Hide resolved
source Outdated Show resolved Hide resolved
source Outdated Show resolved Hide resolved
source Outdated

<li><p>Set all current <span data-x="drawing state">drawing states</span> to the values they have
in <var>previousDrawingStates</var>.</p></li>
</ol>

<p>The <dfn method for="CanvasState"><code data-x="dom-context-2d-reset">reset()</code></dfn>
method steps are to <span>reset the rendering context to its default state</span>.</p>

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resetting a context should also close/discard all currently opened layers right?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. But I think that's already covered in step 3 and 4 below:

"To reset the rendering context to its default state:
....
3 - Clear the context's drawing state stack
4 - Set the context's layer-count to zero."

Isn't it?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But that should be done first. Here canvas's bitmap in step 1 may still be an alias to an open layer, and stating clearly what happens to pending layers may be a good thing too.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was interpreting "canvas's bitmap" to be the "canvas element's bitmap" (for element canvas), which is different than the "context output bitmap". For instance, in section 4.12.5.1.1, we have:
"The top level output bitmap of a rendering context, when it has one, is always just an alias to a canvas element's bitmap."

But you are right that it whould be made clear that the "context's current output bitmap", which changes when layers are opened, should be restored to point to the canvas element bitmap.

Copy link

@tuankiet65 tuankiet65 Aug 4, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I interpret step 3 and 4 as: literally clear the drawing state stack and not unwinding the stack (like say, if the drawing state stack is a stack variable, just call some clear() method to clear it), and set the layer-count variable to 0 without unwinding opened layers. I think for clarity purposes, it'd be helpful to explicitly specify it like "close all opened layers", or "while layer count is not zero, perform the steps in endLayer()" or even "discard all opened layers".

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The thing is, closing all layers is redundant if you are to clear the state stack and reset all states to their default value. In practice, implementations would not close all layers, compositing up all layer outputs and applying filters, just to discard everything in the end. The current spec already uses the step 3 (clear the context's drawing state stack). It's not saying "call restore() in a loop until the state stack is empty.

Maybe I can add a non-normative note saying that these steps have the effect of closing all layers?

Copy link
Member

@Kaiido Kaiido Aug 5, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There would be an observable difference though: flushing the layers (or calling endLayer) would update the origin-clean flag of the canvas bitmap, while if you don't something like the below snippet would not taint the canvas:

ctx.beginLayer()
ctx.drawImage(crossOriginImage, x, y)
ctx.reset()

I'm not quite sure what security risks this would involve (since the cross-origin resource would technically never have touched the bitmap), but that would be observable.

(Ps: Testing on the current Canary's implementation the above snippet does taint the canvas, so somehow at least part of the flushing might be done?)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added a bullet point specifying that the current output bitmap has to be reset to point to the top level output bitmap.

@Kaiido, thanks for pointing out the origin-clean issue. You are correct that closing all layers would give a different result than just resetting the state stack.

The reset() spec should actually say that origin-clean must be set to true. The context does reset the origin-clean flag if the canvas is reset by a size change:

“The flag can be reset in certain situations; for example, when changing the value of the width or the height content attribute of the canvas element to which a CanvasRenderingContext2D is bound, the bitmap is cleared and its origin-clean flag is reset.”
https://html.spec.whatwg.org/multipage/canvas.html#security-with-canvas-elements

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, I never noticed that sentence before, but I agree with your reading.
FWIW, this is not interoperable today, only Chrome does reset the flag when changing the canvas size. Can probably be left for another issue.

chromium-wpt-export-bot pushed a commit to web-platform-tests/wpt that referenced this pull request Oct 13, 2023
This API is incompatible with how the 2D canvas is rasterized when
it contains unclosed layers. Because layers can have filters that get
applied on their final content, they can't be presented until they are
closed. Instead, we normally keep the layer content alive after a
flush, so that it can be presented in a later frame when the layer is
finally closed.

OffscreenCanvas.transferToImageBitmap however is supposed to release
the canvas content, leaving the offscreen canvas empty. We cannot
release the recording if layers are incomplete, and if we kept the
layer content alive for later, we would not be leaving the canvas
empty as the spec requires.

This behavior is part of the current 2D Canvas Layer spec draft:
Explainer: https://github.com/fserb/canvas2D/blob/master/spec/layers.md
Spec draft: whatwg/html#9537

Bug: 1484741
Change-Id: Ic770b51a0343faf0b2c7477624d69f59187ce97f
chromium-wpt-export-bot pushed a commit to web-platform-tests/wpt that referenced this pull request Oct 14, 2023
This API is incompatible with how the 2D canvas is rasterized when
it contains unclosed layers. Because layers can have filters that get
applied on their final content, they can't be presented until they are
closed. Instead, we normally keep the layer content alive after a
flush, so that it can be presented in a later frame when the layer is
finally closed.

OffscreenCanvas.transferToImageBitmap however is supposed to release
the canvas content, leaving the offscreen canvas empty. We cannot
release the recording if layers are incomplete, and if we kept the
layer content alive for later, we would not be leaving the canvas
empty as the spec requires.

This behavior is part of the current 2D Canvas Layer spec draft:
Explainer: https://github.com/fserb/canvas2D/blob/master/spec/layers.md
Spec draft: whatwg/html#9537

Bug: 1484741
Change-Id: Ic770b51a0343faf0b2c7477624d69f59187ce97f
chromium-wpt-export-bot pushed a commit to web-platform-tests/wpt that referenced this pull request Oct 16, 2023
This API is incompatible with how the 2D canvas is rasterized when
it contains unclosed layers. Because layers can have filters that get
applied on their final content, they can't be presented until they are
closed. Instead, we normally keep the layer content alive after a
flush, so that it can be presented in a later frame when the layer is
finally closed.

OffscreenCanvas.transferToImageBitmap however is supposed to release
the canvas content, leaving the offscreen canvas empty. We cannot
release the recording if layers are incomplete, and if we kept the
layer content alive for later, we would not be leaving the canvas
empty as the spec requires.

This behavior is part of the current 2D Canvas Layer spec draft:
Explainer: https://github.com/fserb/canvas2D/blob/master/spec/layers.md
Spec draft: whatwg/html#9537

Bug: 1484741
Change-Id: Ic770b51a0343faf0b2c7477624d69f59187ce97f
chromium-wpt-export-bot pushed a commit to web-platform-tests/wpt that referenced this pull request Oct 18, 2023
This API is incompatible with how the 2D canvas is rasterized when
it contains unclosed layers. Because layers can have filters that get
applied on their final content, they can't be presented until they are
closed. Instead, we normally keep the layer content alive after a
flush, so that it can be presented in a later frame when the layer is
finally closed.

OffscreenCanvas.transferToImageBitmap however is supposed to release
the canvas content, leaving the offscreen canvas empty. We cannot
release the recording if layers are incomplete, and if we kept the
layer content alive for later, we would not be leaving the canvas
empty as the spec requires.

This behavior is part of the current 2D Canvas Layer spec draft:
Explainer: https://github.com/fserb/canvas2D/blob/master/spec/layers.md
Spec draft: whatwg/html#9537

Bug: 1484741
Change-Id: Ic770b51a0343faf0b2c7477624d69f59187ce97f
aarongable pushed a commit to chromium/chromium that referenced this pull request Oct 20, 2023
This API is incompatible with how the 2D canvas is rasterized when
it contains unclosed layers. Because layers can have filters that get
applied on their final content, they can't be presented until they are
closed. Instead, we normally keep the layer content alive after a
flush, so that it can be presented in a later frame when the layer is
finally closed.

OffscreenCanvas.transferToImageBitmap however is supposed to release
the canvas content, leaving the offscreen canvas empty. We cannot
release the recording if layers are incomplete, and if we kept the
layer content alive for later, we would not be leaving the canvas
empty as the spec requires.

This behavior is part of the current 2D Canvas Layer spec draft:
Explainer: https://github.com/fserb/canvas2D/blob/master/spec/layers.md
Spec draft: whatwg/html#9537

Bug: 1484741
Change-Id: Ic770b51a0343faf0b2c7477624d69f59187ce97f
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/4939633
Commit-Queue: Jean-Philippe Gravel <jpgravel@chromium.org>
Reviewed-by: Fernando Serboncini <fserb@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1212692}
chromium-wpt-export-bot pushed a commit to web-platform-tests/wpt that referenced this pull request Oct 20, 2023
This API is incompatible with how the 2D canvas is rasterized when
it contains unclosed layers. Because layers can have filters that get
applied on their final content, they can't be presented until they are
closed. Instead, we normally keep the layer content alive after a
flush, so that it can be presented in a later frame when the layer is
finally closed.

OffscreenCanvas.transferToImageBitmap however is supposed to release
the canvas content, leaving the offscreen canvas empty. We cannot
release the recording if layers are incomplete, and if we kept the
layer content alive for later, we would not be leaving the canvas
empty as the spec requires.

This behavior is part of the current 2D Canvas Layer spec draft:
Explainer: https://github.com/fserb/canvas2D/blob/master/spec/layers.md
Spec draft: whatwg/html#9537

Bug: 1484741
Change-Id: Ic770b51a0343faf0b2c7477624d69f59187ce97f
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/4939633
Commit-Queue: Jean-Philippe Gravel <jpgravel@chromium.org>
Reviewed-by: Fernando Serboncini <fserb@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1212692}
chromium-wpt-export-bot pushed a commit to web-platform-tests/wpt that referenced this pull request Oct 20, 2023
This API is incompatible with how the 2D canvas is rasterized when
it contains unclosed layers. Because layers can have filters that get
applied on their final content, they can't be presented until they are
closed. Instead, we normally keep the layer content alive after a
flush, so that it can be presented in a later frame when the layer is
finally closed.

OffscreenCanvas.transferToImageBitmap however is supposed to release
the canvas content, leaving the offscreen canvas empty. We cannot
release the recording if layers are incomplete, and if we kept the
layer content alive for later, we would not be leaving the canvas
empty as the spec requires.

This behavior is part of the current 2D Canvas Layer spec draft:
Explainer: https://github.com/fserb/canvas2D/blob/master/spec/layers.md
Spec draft: whatwg/html#9537

Bug: 1484741
Change-Id: Ic770b51a0343faf0b2c7477624d69f59187ce97f
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/4939633
Commit-Queue: Jean-Philippe Gravel <jpgravel@chromium.org>
Reviewed-by: Fernando Serboncini <fserb@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1212692}
chromium-wpt-export-bot pushed a commit to web-platform-tests/wpt that referenced this pull request Oct 20, 2023
This API is incompatible with how the 2D canvas is rasterized when
it contains unclosed layers. Because layers can have filters that get
applied on their final content, they can't be presented until they are
closed. Instead, we normally keep the layer content alive after a
flush, so that it can be presented in a later frame when the layer is
finally closed.

putImageData however is supposed to write to the canvas bitmap
wholesale, bypassing all render states. This means that we can't write
to the layer's content because the written pixels would then get
filtered when the layer is closed. We can't write to the main canvas
either because these pixels would later be overwritten by the layer's
result with draw calls that potentially happened before (e.g. in the
sequence `beginLayer(); fillRect(); putImageData(); endLayer();`,
`fillRect()` would write over `putImageData()`.

This behavior is part of the current 2D Canvas Layer spec draft:
Explainer: https://github.com/fserb/canvas2D/blob/master/spec/layers.md
Spec draft: whatwg/html#9537

Change-Id: I266a3155c32919a68dbbb093e4aff9b1dd13a3b5
Bug: 1484741
aarongable pushed a commit to chromium/chromium that referenced this pull request Oct 20, 2023
This API is incompatible with how the 2D canvas is rasterized when
it contains unclosed layers. Because layers can have filters that get
applied on their final content, they can't be presented until they are
closed. Instead, we normally keep the layer content alive after a
flush, so that it can be presented in a later frame when the layer is
finally closed.

putImageData however is supposed to write to the canvas bitmap
wholesale, bypassing all render states. This means that we can't write
to the layer's content because the written pixels would then get
filtered when the layer is closed. We can't write to the main canvas
either because these pixels would later be overwritten by the layer's
result with draw calls that potentially happened before (e.g. in the
sequence `beginLayer(); fillRect(); putImageData(); endLayer();`,
`fillRect()` would write over `putImageData()`.

This behavior is part of the current 2D Canvas Layer spec draft:
Explainer: https://github.com/fserb/canvas2D/blob/master/spec/layers.md
Spec draft: whatwg/html#9537

Change-Id: I266a3155c32919a68dbbb093e4aff9b1dd13a3b5
Bug: 1484741
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/4943172
Reviewed-by: Fernando Serboncini <fserb@chromium.org>
Commit-Queue: Jean-Philippe Gravel <jpgravel@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1212741}
chromium-wpt-export-bot pushed a commit to web-platform-tests/wpt that referenced this pull request Oct 20, 2023
This API is incompatible with how the 2D canvas is rasterized when
it contains unclosed layers. Because layers can have filters that get
applied on their final content, they can't be presented until they are
closed. Instead, we normally keep the layer content alive after a
flush, so that it can be presented in a later frame when the layer is
finally closed.

putImageData however is supposed to write to the canvas bitmap
wholesale, bypassing all render states. This means that we can't write
to the layer's content because the written pixels would then get
filtered when the layer is closed. We can't write to the main canvas
either because these pixels would later be overwritten by the layer's
result with draw calls that potentially happened before (e.g. in the
sequence `beginLayer(); fillRect(); putImageData(); endLayer();`,
`fillRect()` would write over `putImageData()`.

This behavior is part of the current 2D Canvas Layer spec draft:
Explainer: https://github.com/fserb/canvas2D/blob/master/spec/layers.md
Spec draft: whatwg/html#9537

Change-Id: I266a3155c32919a68dbbb093e4aff9b1dd13a3b5
Bug: 1484741
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/4943172
Reviewed-by: Fernando Serboncini <fserb@chromium.org>
Commit-Queue: Jean-Philippe Gravel <jpgravel@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1212741}
chromium-wpt-export-bot pushed a commit to web-platform-tests/wpt that referenced this pull request Oct 20, 2023
This API is incompatible with how the 2D canvas is rasterized when
it contains unclosed layers. Because layers can have filters that get
applied on their final content, they can't be presented until they are
closed. Instead, we normally keep the layer content alive after a
flush, so that it can be presented in a later frame when the layer is
finally closed.

putImageData however is supposed to write to the canvas bitmap
wholesale, bypassing all render states. This means that we can't write
to the layer's content because the written pixels would then get
filtered when the layer is closed. We can't write to the main canvas
either because these pixels would later be overwritten by the layer's
result with draw calls that potentially happened before (e.g. in the
sequence `beginLayer(); fillRect(); putImageData(); endLayer();`,
`fillRect()` would write over `putImageData()`.

This behavior is part of the current 2D Canvas Layer spec draft:
Explainer: https://github.com/fserb/canvas2D/blob/master/spec/layers.md
Spec draft: whatwg/html#9537

Change-Id: I266a3155c32919a68dbbb093e4aff9b1dd13a3b5
Bug: 1484741
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/4943172
Reviewed-by: Fernando Serboncini <fserb@chromium.org>
Commit-Queue: Jean-Philippe Gravel <jpgravel@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1212741}
moz-v2v-gh pushed a commit to mozilla/gecko-dev that referenced this pull request Nov 1, 2023
…map if canvas layers are opened, a=testonly

Automatic update from web-platform-tests
Throw an exception in transferToImageBitmap if canvas layers are opened

This API is incompatible with how the 2D canvas is rasterized when
it contains unclosed layers. Because layers can have filters that get
applied on their final content, they can't be presented until they are
closed. Instead, we normally keep the layer content alive after a
flush, so that it can be presented in a later frame when the layer is
finally closed.

OffscreenCanvas.transferToImageBitmap however is supposed to release
the canvas content, leaving the offscreen canvas empty. We cannot
release the recording if layers are incomplete, and if we kept the
layer content alive for later, we would not be leaving the canvas
empty as the spec requires.

This behavior is part of the current 2D Canvas Layer spec draft:
Explainer: https://github.com/fserb/canvas2D/blob/master/spec/layers.md
Spec draft: whatwg/html#9537

Bug: 1484741
Change-Id: Ic770b51a0343faf0b2c7477624d69f59187ce97f
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/4939633
Commit-Queue: Jean-Philippe Gravel <jpgravel@chromium.org>
Reviewed-by: Fernando Serboncini <fserb@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1212692}

--

wpt-commits: 3375400712353d2c9b011ed3dbb24c8d756b784f
wpt-pr: 42544
moz-v2v-gh pushed a commit to mozilla/gecko-dev that referenced this pull request Nov 1, 2023
…nvas layers are opened, a=testonly

Automatic update from web-platform-tests
Throw an exception in putImageData if canvas layers are opened

This API is incompatible with how the 2D canvas is rasterized when
it contains unclosed layers. Because layers can have filters that get
applied on their final content, they can't be presented until they are
closed. Instead, we normally keep the layer content alive after a
flush, so that it can be presented in a later frame when the layer is
finally closed.

putImageData however is supposed to write to the canvas bitmap
wholesale, bypassing all render states. This means that we can't write
to the layer's content because the written pixels would then get
filtered when the layer is closed. We can't write to the main canvas
either because these pixels would later be overwritten by the layer's
result with draw calls that potentially happened before (e.g. in the
sequence `beginLayer(); fillRect(); putImageData(); endLayer();`,
`fillRect()` would write over `putImageData()`.

This behavior is part of the current 2D Canvas Layer spec draft:
Explainer: https://github.com/fserb/canvas2D/blob/master/spec/layers.md
Spec draft: whatwg/html#9537

Change-Id: I266a3155c32919a68dbbb093e4aff9b1dd13a3b5
Bug: 1484741
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/4943172
Reviewed-by: Fernando Serboncini <fserb@chromium.org>
Commit-Queue: Jean-Philippe Gravel <jpgravel@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1212741}

--

wpt-commits: 6e6f9fbb0746983001d7fe80ab717567c2f73bfc
wpt-pr: 42662
ErichDonGubler pushed a commit to erichdongubler-mozilla/firefox that referenced this pull request Nov 2, 2023
…map if canvas layers are opened, a=testonly

Automatic update from web-platform-tests
Throw an exception in transferToImageBitmap if canvas layers are opened

This API is incompatible with how the 2D canvas is rasterized when
it contains unclosed layers. Because layers can have filters that get
applied on their final content, they can't be presented until they are
closed. Instead, we normally keep the layer content alive after a
flush, so that it can be presented in a later frame when the layer is
finally closed.

OffscreenCanvas.transferToImageBitmap however is supposed to release
the canvas content, leaving the offscreen canvas empty. We cannot
release the recording if layers are incomplete, and if we kept the
layer content alive for later, we would not be leaving the canvas
empty as the spec requires.

This behavior is part of the current 2D Canvas Layer spec draft:
Explainer: https://github.com/fserb/canvas2D/blob/master/spec/layers.md
Spec draft: whatwg/html#9537

Bug: 1484741
Change-Id: Ic770b51a0343faf0b2c7477624d69f59187ce97f
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/4939633
Commit-Queue: Jean-Philippe Gravel <jpgravel@chromium.org>
Reviewed-by: Fernando Serboncini <fserb@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1212692}

--

wpt-commits: 3375400712353d2c9b011ed3dbb24c8d756b784f
wpt-pr: 42544
ErichDonGubler pushed a commit to erichdongubler-mozilla/firefox that referenced this pull request Nov 2, 2023
…nvas layers are opened, a=testonly

Automatic update from web-platform-tests
Throw an exception in putImageData if canvas layers are opened

This API is incompatible with how the 2D canvas is rasterized when
it contains unclosed layers. Because layers can have filters that get
applied on their final content, they can't be presented until they are
closed. Instead, we normally keep the layer content alive after a
flush, so that it can be presented in a later frame when the layer is
finally closed.

putImageData however is supposed to write to the canvas bitmap
wholesale, bypassing all render states. This means that we can't write
to the layer's content because the written pixels would then get
filtered when the layer is closed. We can't write to the main canvas
either because these pixels would later be overwritten by the layer's
result with draw calls that potentially happened before (e.g. in the
sequence `beginLayer(); fillRect(); putImageData(); endLayer();`,
`fillRect()` would write over `putImageData()`.

This behavior is part of the current 2D Canvas Layer spec draft:
Explainer: https://github.com/fserb/canvas2D/blob/master/spec/layers.md
Spec draft: whatwg/html#9537

Change-Id: I266a3155c32919a68dbbb093e4aff9b1dd13a3b5
Bug: 1484741
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/4943172
Reviewed-by: Fernando Serboncini <fserb@chromium.org>
Commit-Queue: Jean-Philippe Gravel <jpgravel@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1212741}

--

wpt-commits: 6e6f9fbb0746983001d7fe80ab717567c2f73bfc
wpt-pr: 42662
Copy link
Author

@graveljp graveljp left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I uploaded a commit addressing all review comments. I also made these important changes:

  • beginLayer now accepts CSS filter strings.

  • The CanvasRenderingContext2D now has a "top level output bitmap" and a "current output bitmap". The "top level output bitmap" can always be presented to the user, even when layers are open. The content of layers only gets rasterized to the "top level output bitmap" when all layers are closed. This is done by having layers replace and restore the "current output bitmap" to which context draws.

source Outdated Show resolved Hide resolved
source Outdated Show resolved Hide resolved
source Outdated
<span>CanvasFilterInput</span>? <dfn data-x="dom-context-2d-beginLayer-options-filter">filter</dfn> = null;
};

interface mixin <dfn interface>CanvasLayers</dfn> {
Copy link
Author

@graveljp graveljp Aug 7, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It should probably. Since it's spec'ed in a different place, I can file a ticket to track this separately. Would that make sense?

source Outdated Show resolved Hide resolved
source Outdated
keeping track of the number of opened nested layers. To access the content of the context's
<span>output bitmap</span>, the <span>steps for reading the context output bitmap</span> must be
used.

<p>The <span>output bitmap</span> has an <span
data-x="concept-canvas-origin-clean">origin-clean</span> flag, which can be set to true or false.
Initially, when one of these bitmaps is created, its <span
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Layers behave the same as other draw calls for {alpha: false} contexts. The layer content and the layer itself is alpha-blended up to the point where it gets written to the top level output bitmap. I updated the paragraph below to clarify this, and added WPT tests validating this (https://crrev.com/c/5006000).

source Outdated Show resolved Hide resolved
source Show resolved Hide resolved
source Outdated
Comment on lines 64618 to 64630
was flushed by running the steps for <dfn>restoring the drawing state stack</dfn>:</p>

<ol>
<li><p><span data-x="list iterate">For each</span> <var>stateEntry</var> of
<var>stateBackup</var>:</p></li>

<ol>
<li><p><span data-x="stack push">Push</span> <var>stateEntry</var> onto the <span>drawing state
stack</span>.</p></li>
</ol>

<li><p>Restore the context's current <span>drawing state</span> by running the <code
data-x="dom-context-2d-restore">restore()</code> method steps.</p></li>
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This section has been removed.

The idea was that when presenting a frame, all the layers would have been closed, the draw calls would have been rasterized, and then, all layers would have been re-opened. This algorithm was describing the logic for re-opening the layers. Only the states were to be re-populated, not the draw calls. On later frame, only new draw calls would get drawn.

This was all replaced with an alternative strategy where unclosed layers are never presented. Instead, only the draw calls up to the first beginLayer() gets rasterized. The content of the layer is preserved for the next frame, where layers can be closed and rasterized as a whole.

source Show resolved Hide resolved
source Outdated Show resolved Hide resolved
This adds new beginLayer and endLayer functions to open and close layers
in the canvas. While layers are active, draw calls operate on a separate
texture that gets composited to the parent output bitmap when the layer
is closed. An optional filter can be specified in beginLayer, allowing
effects to be applied to the layer's texture when it's composited its
parent.

Tests:
 https://github.com/web-platform-tests/wpt/tree/master/html/canvas/element/layers
 https://github.com/web-platform-tests/wpt/tree/master/html/canvas/offscreen/layers

Fixes whatwg#8476
This update addresses 3 main things:
- Support for CSS filter strings was added to the beginLayer API
- Unclosed layers are now never rasterized when the canvas is presented
  to the user. Instead, the content of the layer is preserved and will
  be rasterized in a later frame, if/when the layer is closed.
- Replied to the first round of review comments.
gecko-dev-updater pushed a commit to marco-c/gecko-dev-wordified-and-comments-removed that referenced this pull request Nov 8, 2023
…map if canvas layers are opened, a=testonly

Automatic update from web-platform-tests
Throw an exception in transferToImageBitmap if canvas layers are opened

This API is incompatible with how the 2D canvas is rasterized when
it contains unclosed layers. Because layers can have filters that get
applied on their final content, they can't be presented until they are
closed. Instead, we normally keep the layer content alive after a
flush, so that it can be presented in a later frame when the layer is
finally closed.

OffscreenCanvas.transferToImageBitmap however is supposed to release
the canvas content, leaving the offscreen canvas empty. We cannot
release the recording if layers are incomplete, and if we kept the
layer content alive for later, we would not be leaving the canvas
empty as the spec requires.

This behavior is part of the current 2D Canvas Layer spec draft:
Explainer: https://github.com/fserb/canvas2D/blob/master/spec/layers.md
Spec draft: whatwg/html#9537

Bug: 1484741
Change-Id: Ic770b51a0343faf0b2c7477624d69f59187ce97f
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/4939633
Commit-Queue: Jean-Philippe Gravel <jpgravelchromium.org>
Reviewed-by: Fernando Serboncini <fserbchromium.org>
Cr-Commit-Position: refs/heads/main{#1212692}

--

wpt-commits: 3375400712353d2c9b011ed3dbb24c8d756b784f
wpt-pr: 42544

UltraBlame original commit: 5dcacd6ceadca3d78dab262fdd726b9646edbe7e
gecko-dev-updater pushed a commit to marco-c/gecko-dev-wordified-and-comments-removed that referenced this pull request Nov 8, 2023
…nvas layers are opened, a=testonly

Automatic update from web-platform-tests
Throw an exception in putImageData if canvas layers are opened

This API is incompatible with how the 2D canvas is rasterized when
it contains unclosed layers. Because layers can have filters that get
applied on their final content, they can't be presented until they are
closed. Instead, we normally keep the layer content alive after a
flush, so that it can be presented in a later frame when the layer is
finally closed.

putImageData however is supposed to write to the canvas bitmap
wholesale, bypassing all render states. This means that we can't write
to the layer's content because the written pixels would then get
filtered when the layer is closed. We can't write to the main canvas
either because these pixels would later be overwritten by the layer's
result with draw calls that potentially happened before (e.g. in the
sequence `beginLayer(); fillRect(); putImageData(); endLayer();`,
`fillRect()` would write over `putImageData()`.

This behavior is part of the current 2D Canvas Layer spec draft:
Explainer: https://github.com/fserb/canvas2D/blob/master/spec/layers.md
Spec draft: whatwg/html#9537

Change-Id: I266a3155c32919a68dbbb093e4aff9b1dd13a3b5
Bug: 1484741
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/4943172
Reviewed-by: Fernando Serboncini <fserbchromium.org>
Commit-Queue: Jean-Philippe Gravel <jpgravelchromium.org>
Cr-Commit-Position: refs/heads/main{#1212741}

--

wpt-commits: 6e6f9fbb0746983001d7fe80ab717567c2f73bfc
wpt-pr: 42662

UltraBlame original commit: 685e2ff9c9f305fea2dfd442a95067594d05a6d1
gecko-dev-updater pushed a commit to marco-c/gecko-dev-wordified that referenced this pull request Nov 8, 2023
…map if canvas layers are opened, a=testonly

Automatic update from web-platform-tests
Throw an exception in transferToImageBitmap if canvas layers are opened

This API is incompatible with how the 2D canvas is rasterized when
it contains unclosed layers. Because layers can have filters that get
applied on their final content, they can't be presented until they are
closed. Instead, we normally keep the layer content alive after a
flush, so that it can be presented in a later frame when the layer is
finally closed.

OffscreenCanvas.transferToImageBitmap however is supposed to release
the canvas content, leaving the offscreen canvas empty. We cannot
release the recording if layers are incomplete, and if we kept the
layer content alive for later, we would not be leaving the canvas
empty as the spec requires.

This behavior is part of the current 2D Canvas Layer spec draft:
Explainer: https://github.com/fserb/canvas2D/blob/master/spec/layers.md
Spec draft: whatwg/html#9537

Bug: 1484741
Change-Id: Ic770b51a0343faf0b2c7477624d69f59187ce97f
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/4939633
Commit-Queue: Jean-Philippe Gravel <jpgravelchromium.org>
Reviewed-by: Fernando Serboncini <fserbchromium.org>
Cr-Commit-Position: refs/heads/main{#1212692}

--

wpt-commits: 3375400712353d2c9b011ed3dbb24c8d756b784f
wpt-pr: 42544

UltraBlame original commit: 5dcacd6ceadca3d78dab262fdd726b9646edbe7e
@Kaiido
Copy link
Member

Kaiido commented Jun 11, 2024

They would be applied to filters applied via beginLayer.

That's one more surprising behavior and I don't see where it would come from in the current PR.
In #9537 (comment) you stated that it's how CSS and SVG work, but there, it's not only the filters that are affected by the transform, but the whole coord system. For instance in your example the default font-size has also been scaled by 3. If I'm not mistaken, the equivalent for layers would be to have the current transform affect how the whole layer is rendered, not just the filter.

I'll look at spec'ing and implementing BeginLayerOption with alpha, compositeOperation, filter and shadowXXX.

To clarify my personal opinion further, I believe we should have either BeginLayerOption, either have the context's current rendering states affect the layer's rendering, but not both. Having both will inevitably lead to confusion as to which states will apply, in which order, additive or replacing, etc.

@graveljp
Copy link
Author

That's one more surprising behavior and I don't see where it would come from in the current PR.

You are right that the current PR doesn't fully correctly this.

To avoid resampling layer's output, which would be slow and lower image quality, the current PR keeps the transform global and doesn't reset it when entering layers. Thus, draw calls write pixels at the position they will have in the final raster. Because there is no concepts of layer-local transforms and because ctx.filter doesn't observe the current transform, it follows that ctx.filter never observes the transform irrespective of whether it's nested in a layer. What's missing is for the spec to mention that the transform should apply to the filter in BeginLayerOption.

I think that in the above, the surprising behaviour is that context filters inside a layer are not affected by the transform in the parent layer. When scaling a whole layer, the content of the layer in all it's details should scale the same way and that should include ctx.filter. Maybe we need to add a concept of parent transform, which would be applied to ctx.filter and ctx.shadowXXX. If we did this, then indeed BeginLayerOptions would be equivalent to the context state plus a save/restore pair. BeginLayerOptions would then be just a syntactic sugar allowing a safer and nicer usage pattern.

You have mentioned the idea of resetting the transform inside layer. It's an interesting idea, but I don't know how we could spec that without requiring layer resampling. It looks like we would need to keep a global transform regardless, to draw pixels at their intended final position, and keep an "artificial" layer-local transform for the only purpose of having getTransform return a layer-local matrix. This adds complexity and disaligns the API with how the canvas really works, for little added value.

@graveljp
Copy link
Author

Making the transform layer-local (resetting it when we enter a layer) might have another advantage. If the transform is always global, calling setTransform from within a layer is really strange.

If a non-invertible matrix is used, nothing can get drawn to the canvas:

ctx.scale(0, 0);
ctx.fillRect(0, 0, 10, 10);  // Draws nothing.

This should apply to layers too:

ctx.scale(0, 0);
ctx.beginLayer();
  ctx.fillRect(0, 0, 10, 10);
ctx.endLayer()  // Draws nothing.

But what happens if setTransform is called from within the layer? If the transform is global, we have:

ctx.scale(0, 0);  // Non-invertible matrix.
ctx.beginLayer();  // The whole layer is non-rasterizable.
  ctx.fillRect(0, 0, 10, 10);  // Can't be drawn, the transform is still non-invertible.
  ctx.setTransform(1, 0, 0, 1, 0, 0);  // Restores the matrix to identity.
  ctx.fillRect(0, 0, 10, 10);  // Unclear what happens here.
ctx.endLayer();

If the transform is layer-local, we have:

ctx.scale(0, 0);  // Non-invertible matrix.
ctx.beginLayer();  // The whole layer is non-rasterizable.
  ctx.fillRect(0, 0, 10, 10);  // Can be drawn in the layer (but implementations could optimize away if wanted).
  ctx.setTransform(1, 0, 0, 1, 0, 0);  // No-op, the layer transform is already identity.
  ctx.fillRect(0, 0, 10, 10);  // Same: can be drawn.
ctx.endLayer();  // Draws nothing to the top level output bitmap.

I think we could spec that by maintaining a parent transform (already needed for ctx.filter), a layer transform, and then having all draw calls use parent transform x layer transform to transform strokes and images.

Would that work?

webkit-commit-queue pushed a commit to shallawa/WebKit that referenced this pull request Jun 19, 2024
https://bugs.webkit.org/show_bug.cgi?id=273923
rdar://127789082

Reviewed by Kimmo Kinnunen.

This is a draft patch for the canvas layers API. The discussion of this API is
in whatwg/html#9537.

Unlike what is stated in the discussion above, this PR adds the filter to the
layer rendering states. This means like other members in layer rendering state,
the filter will be cleared out from the context state once beginLayer() is called.
It will restored to the context state once endLayer() is called. Then the restored
filter will be applied to the layer before compositing it to the canvas context.

* LayoutTests/fast/canvas/canvas-layer-alpha-drawing-expected.html: Added.
* LayoutTests/fast/canvas/canvas-layer-alpha-drawing.html: Added.
* LayoutTests/fast/canvas/canvas-layer-composite-drawing-expected.html: Added.
* LayoutTests/fast/canvas/canvas-layer-composite-drawing.html: Added.
* LayoutTests/fast/canvas/canvas-layer-drawing-expected.html: Added.
* LayoutTests/fast/canvas/canvas-layer-drawing.html: Added.
* LayoutTests/fast/canvas/canvas-layer-filter-drawing-expected.html: Added.
* LayoutTests/fast/canvas/canvas-layer-filter-drawing.html: Added.
* LayoutTests/fast/canvas/canvas-multiple-layer-filter-drawing-expected.html: Added.
* LayoutTests/fast/canvas/canvas-multiple-layer-filter-drawing.html: Added.
* LayoutTests/fast/canvas/canvas-multiple-nested-layer-filter-drawing-expected.html: Added.
* LayoutTests/fast/canvas/canvas-multiple-nested-layer-filter-drawing.html: Added.
* Source/WTF/Scripts/Preferences/UnifiedWebPreferences.yaml:
* Source/WebCore/CMakeLists.txt:
* Source/WebCore/DerivedSources-input.xcfilelist:
* Source/WebCore/DerivedSources-output.xcfilelist:
* Source/WebCore/DerivedSources.make:
* Source/WebCore/Sources.txt:
* Source/WebCore/WebCore.xcodeproj/project.pbxproj:
* Source/WebCore/html/canvas/CanvasFilterContextSwitcher.cpp: Copied from Source/WebCore/html/canvas/CanvasFilterTargetSwitcher.h.
(WebCore::CanvasFilterContextSwitcher::create):
(WebCore::CanvasFilterContextSwitcher::CanvasFilterContextSwitcher):
(WebCore::CanvasFilterContextSwitcher::~CanvasFilterContextSwitcher):
(WebCore::CanvasFilterContextSwitcher::expandedBounds const):
* Source/WebCore/html/canvas/CanvasFilterContextSwitcher.h: Copied from Source/WebCore/html/canvas/CanvasFilterTargetSwitcher.h.
* Source/WebCore/html/canvas/CanvasLayerContextSwitcher.cpp: Renamed from Source/WebCore/html/canvas/CanvasFilterTargetSwitcher.cpp.
(WebCore::CanvasLayerContextSwitcher::create):
(WebCore::CanvasLayerContextSwitcher::CanvasLayerContextSwitcher):
(WebCore::CanvasLayerContextSwitcher::~CanvasLayerContextSwitcher):
(WebCore::CanvasLayerContextSwitcher::drawingContext const):
(WebCore::CanvasLayerContextSwitcher::outsets const):
* Source/WebCore/html/canvas/CanvasLayerContextSwitcher.h: Renamed from Source/WebCore/html/canvas/CanvasFilterTargetSwitcher.h.
* Source/WebCore/html/canvas/CanvasLayers.idl: Added.
* Source/WebCore/html/canvas/CanvasRenderingContext2D.cpp:
(WebCore::CanvasRenderingContext2D::createFilter const):
* Source/WebCore/html/canvas/CanvasRenderingContext2D.h:
* Source/WebCore/html/canvas/CanvasRenderingContext2D.idl:
* Source/WebCore/html/canvas/CanvasRenderingContext2DBase.cpp:
(WebCore::CanvasRenderingContext2DBase::beginLayer):
(WebCore::CanvasRenderingContext2DBase::endLayer):
(WebCore::CanvasRenderingContext2DBase::fillInternal):
(WebCore::CanvasRenderingContext2DBase::strokeInternal):
(WebCore::CanvasRenderingContext2DBase::fillRect):
(WebCore::CanvasRenderingContext2DBase::strokeRect):
(WebCore::CanvasRenderingContext2DBase::drawImage):
(WebCore::CanvasRenderingContext2DBase::drawingContext const):
(WebCore::CanvasRenderingContext2DBase::effectiveDrawingContext const):
(WebCore::CanvasRenderingContext2DBase::drawTextUnchecked):
* Source/WebCore/html/canvas/CanvasRenderingContext2DBase.h:
(WebCore::CanvasRenderingContext2DBase::createFilter const):
(WebCore::CanvasRenderingContext2DBase::setFilterTargetSwitcher): Deleted.
* Source/WebCore/html/canvas/OffscreenCanvasRenderingContext2D.idl:
* Source/WebCore/platform/graphics/GraphicsContextSwitcher.cpp: Renamed from Source/WebCore/platform/graphics/filters/FilterTargetSwitcher.cpp.
(WebCore::GraphicsContextSwitcher::create):
(WebCore::GraphicsContextSwitcher::GraphicsContextSwitcher):
* Source/WebCore/platform/graphics/GraphicsContextSwitcher.h: Renamed from Source/WebCore/platform/graphics/filters/FilterTargetSwitcher.h.
* Source/WebCore/platform/graphics/ImageBuffer.cpp:
(WebCore::ImageBuffer::filteredNativeImage):
* Source/WebCore/platform/graphics/ImageBufferContextSwitcher.cpp: Renamed from Source/WebCore/platform/graphics/filters/FilterImageTargetSwitcher.cpp.
(WebCore::ImageBufferContextSwitcher::ImageBufferContextSwitcher):
(WebCore::ImageBufferContextSwitcher::drawingContext const):
(WebCore::ImageBufferContextSwitcher::beginClipAndDrawSourceImage):
(WebCore::ImageBufferContextSwitcher::endClipAndDrawSourceImage):
(WebCore::ImageBufferContextSwitcher::endDrawSourceImage):
* Source/WebCore/platform/graphics/ImageBufferContextSwitcher.h: Renamed from Source/WebCore/platform/graphics/filters/FilterImageTargetSwitcher.h.
* Source/WebCore/platform/graphics/TransparencyLayerContextSwitcher.cpp: Renamed from Source/WebCore/platform/graphics/filters/FilterStyleTargetSwitcher.cpp.
(WebCore::TransparencyLayerContextSwitcher::TransparencyLayerContextSwitcher):
(WebCore::TransparencyLayerContextSwitcher::beginClipAndDrawSourceImage):
(WebCore::TransparencyLayerContextSwitcher::beginDrawSourceImage):
(WebCore::TransparencyLayerContextSwitcher::endDrawSourceImage):
* Source/WebCore/platform/graphics/TransparencyLayerContextSwitcher.h: Renamed from Source/WebCore/platform/graphics/filters/FilterStyleTargetSwitcher.h.
* Source/WebCore/rendering/RenderLayerFilters.cpp:
(WebCore::RenderLayerFilters::beginFilterEffect):
* Source/WebCore/rendering/RenderLayerFilters.h:
* Source/WebCore/rendering/svg/legacy/LegacyRenderSVGResourceFilter.cpp:
(WebCore::LegacyRenderSVGResourceFilter::applyResource):
* Source/WebCore/rendering/svg/legacy/LegacyRenderSVGResourceFilter.h:

Canonical link: https://commits.webkit.org/280148@main
With this change, the context filters now applies to the layer's output
bitmap and is resetted to "none" when entering layers.
This removes the filter argument of the beginLayer API.
BeginLayerOptions can possibliy be added to the specification as a
follow-up.
chromium-wpt-export-bot pushed a commit to web-platform-tests/wpt that referenced this pull request Aug 20, 2024
WebKit and Blink agreed that the `filter` context state should apply to
the output of layers.
whatwg/html#9537 (comment)

The context `filter` state is applied after the filter passed as
argument to `beginLayer`. The context `filter` is now reset when
entering layers.

Bug: 40249439
Change-Id: I5daf82672c334cdc96a90af8e9fe5dd4466849cf
aarongable pushed a commit to chromium/chromium that referenced this pull request Aug 20, 2024
WebKit and Blink agreed that the `filter` context state should apply to
the output of layers.
whatwg/html#9537 (comment)

The context `filter` state is applied after the filter passed as
argument to `beginLayer`. The context `filter` is now reset when
entering layers.

Bug: 40249439
Change-Id: I5daf82672c334cdc96a90af8e9fe5dd4466849cf
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/5626086
Commit-Queue: Jean-Philippe Gravel <jpgravel@chromium.org>
Reviewed-by: Andres Ricardo Perez <andresrperez@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1344173}
chromium-wpt-export-bot pushed a commit to web-platform-tests/wpt that referenced this pull request Aug 20, 2024
WebKit and Blink agreed that the `filter` context state should apply to
the output of layers.
whatwg/html#9537 (comment)

The context `filter` state is applied after the filter passed as
argument to `beginLayer`. The context `filter` is now reset when
entering layers.

Bug: 40249439
Change-Id: I5daf82672c334cdc96a90af8e9fe5dd4466849cf
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/5626086
Commit-Queue: Jean-Philippe Gravel <jpgravel@chromium.org>
Reviewed-by: Andres Ricardo Perez <andresrperez@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1344173}
chromium-wpt-export-bot pushed a commit to web-platform-tests/wpt that referenced this pull request Aug 20, 2024
WebKit and Blink agreed that the `filter` context state should apply to
the output of layers.
whatwg/html#9537 (comment)

The context `filter` state is applied after the filter passed as
argument to `beginLayer`. The context `filter` is now reset when
entering layers.

Bug: 40249439
Change-Id: I5daf82672c334cdc96a90af8e9fe5dd4466849cf
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/5626086
Commit-Queue: Jean-Philippe Gravel <jpgravel@chromium.org>
Reviewed-by: Andres Ricardo Perez <andresrperez@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1344173}
Westbrook pushed a commit to Westbrook/wpt that referenced this pull request Aug 21, 2024
WebKit and Blink agreed that the `filter` context state should apply to
the output of layers.
whatwg/html#9537 (comment)

The context `filter` state is applied after the filter passed as
argument to `beginLayer`. The context `filter` is now reset when
entering layers.

Bug: 40249439
Change-Id: I5daf82672c334cdc96a90af8e9fe5dd4466849cf
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/5626086
Commit-Queue: Jean-Philippe Gravel <jpgravel@chromium.org>
Reviewed-by: Andres Ricardo Perez <andresrperez@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1344173}
moz-v2v-gh pushed a commit to mozilla/gecko-dev that referenced this pull request Aug 21, 2024
…testonly

Automatic update from web-platform-tests
Apply ctx.filter to canvas 2d layers

WebKit and Blink agreed that the `filter` context state should apply to
the output of layers.
whatwg/html#9537 (comment)

The context `filter` state is applied after the filter passed as
argument to `beginLayer`. The context `filter` is now reset when
entering layers.

Bug: 40249439
Change-Id: I5daf82672c334cdc96a90af8e9fe5dd4466849cf
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/5626086
Commit-Queue: Jean-Philippe Gravel <jpgravel@chromium.org>
Reviewed-by: Andres Ricardo Perez <andresrperez@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1344173}

--

wpt-commits: 1e726204a02d7e34e062774bd1c6baabffa142c7
wpt-pr: 47687
i3roly pushed a commit to i3roly/firefox-dynasty that referenced this pull request Aug 22, 2024
…testonly

Automatic update from web-platform-tests
Apply ctx.filter to canvas 2d layers

WebKit and Blink agreed that the `filter` context state should apply to
the output of layers.
whatwg/html#9537 (comment)

The context `filter` state is applied after the filter passed as
argument to `beginLayer`. The context `filter` is now reset when
entering layers.

Bug: 40249439
Change-Id: I5daf82672c334cdc96a90af8e9fe5dd4466849cf
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/5626086
Commit-Queue: Jean-Philippe Gravel <jpgravel@chromium.org>
Reviewed-by: Andres Ricardo Perez <andresrperez@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1344173}

--

wpt-commits: 1e726204a02d7e34e062774bd1c6baabffa142c7
wpt-pr: 47687
gecko-dev-updater pushed a commit to marco-c/gecko-dev-wordified-and-comments-removed that referenced this pull request Aug 23, 2024
…testonly

Automatic update from web-platform-tests
Apply ctx.filter to canvas 2d layers

WebKit and Blink agreed that the `filter` context state should apply to
the output of layers.
whatwg/html#9537 (comment)

The context `filter` state is applied after the filter passed as
argument to `beginLayer`. The context `filter` is now reset when
entering layers.

Bug: 40249439
Change-Id: I5daf82672c334cdc96a90af8e9fe5dd4466849cf
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/5626086
Commit-Queue: Jean-Philippe Gravel <jpgravelchromium.org>
Reviewed-by: Andres Ricardo Perez <andresrperezchromium.org>
Cr-Commit-Position: refs/heads/main{#1344173}

--

wpt-commits: 1e726204a02d7e34e062774bd1c6baabffa142c7
wpt-pr: 47687

UltraBlame original commit: 46b4cff22c90772849bda2b547af616b79cd9260
gecko-dev-updater pushed a commit to marco-c/gecko-dev-wordified that referenced this pull request Aug 23, 2024
…testonly

Automatic update from web-platform-tests
Apply ctx.filter to canvas 2d layers

WebKit and Blink agreed that the `filter` context state should apply to
the output of layers.
whatwg/html#9537 (comment)

The context `filter` state is applied after the filter passed as
argument to `beginLayer`. The context `filter` is now reset when
entering layers.

Bug: 40249439
Change-Id: I5daf82672c334cdc96a90af8e9fe5dd4466849cf
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/5626086
Commit-Queue: Jean-Philippe Gravel <jpgravelchromium.org>
Reviewed-by: Andres Ricardo Perez <andresrperezchromium.org>
Cr-Commit-Position: refs/heads/main{#1344173}

--

wpt-commits: 1e726204a02d7e34e062774bd1c6baabffa142c7
wpt-pr: 47687

UltraBlame original commit: 46b4cff22c90772849bda2b547af616b79cd9260
ErichDonGubler pushed a commit to erichdongubler-mozilla/firefox that referenced this pull request Aug 26, 2024
…testonly

Automatic update from web-platform-tests
Apply ctx.filter to canvas 2d layers

WebKit and Blink agreed that the `filter` context state should apply to
the output of layers.
whatwg/html#9537 (comment)

The context `filter` state is applied after the filter passed as
argument to `beginLayer`. The context `filter` is now reset when
entering layers.

Bug: 40249439
Change-Id: I5daf82672c334cdc96a90af8e9fe5dd4466849cf
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/5626086
Commit-Queue: Jean-Philippe Gravel <jpgravel@chromium.org>
Reviewed-by: Andres Ricardo Perez <andresrperez@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1344173}

--

wpt-commits: 1e726204a02d7e34e062774bd1c6baabffa142c7
wpt-pr: 47687
mnutt pushed a commit to movableink/webkit that referenced this pull request Aug 27, 2024
https://bugs.webkit.org/show_bug.cgi?id=273923
rdar://127789082

Reviewed by Kimmo Kinnunen.

This is a draft patch for the canvas layers API. The discussion of this API is
in whatwg/html#9537.

Unlike what is stated in the discussion above, this PR adds the filter to the
layer rendering states. This means like other members in layer rendering state,
the filter will be cleared out from the context state once beginLayer() is called.
It will restored to the context state once endLayer() is called. Then the restored
filter will be applied to the layer before compositing it to the canvas context.

* LayoutTests/fast/canvas/canvas-layer-alpha-drawing-expected.html: Added.
* LayoutTests/fast/canvas/canvas-layer-alpha-drawing.html: Added.
* LayoutTests/fast/canvas/canvas-layer-composite-drawing-expected.html: Added.
* LayoutTests/fast/canvas/canvas-layer-composite-drawing.html: Added.
* LayoutTests/fast/canvas/canvas-layer-drawing-expected.html: Added.
* LayoutTests/fast/canvas/canvas-layer-drawing.html: Added.
* LayoutTests/fast/canvas/canvas-layer-filter-drawing-expected.html: Added.
* LayoutTests/fast/canvas/canvas-layer-filter-drawing.html: Added.
* LayoutTests/fast/canvas/canvas-multiple-layer-filter-drawing-expected.html: Added.
* LayoutTests/fast/canvas/canvas-multiple-layer-filter-drawing.html: Added.
* LayoutTests/fast/canvas/canvas-multiple-nested-layer-filter-drawing-expected.html: Added.
* LayoutTests/fast/canvas/canvas-multiple-nested-layer-filter-drawing.html: Added.
* Source/WTF/Scripts/Preferences/UnifiedWebPreferences.yaml:
* Source/WebCore/CMakeLists.txt:
* Source/WebCore/DerivedSources-input.xcfilelist:
* Source/WebCore/DerivedSources-output.xcfilelist:
* Source/WebCore/DerivedSources.make:
* Source/WebCore/Sources.txt:
* Source/WebCore/WebCore.xcodeproj/project.pbxproj:
* Source/WebCore/html/canvas/CanvasFilterContextSwitcher.cpp: Copied from Source/WebCore/html/canvas/CanvasFilterTargetSwitcher.h.
(WebCore::CanvasFilterContextSwitcher::create):
(WebCore::CanvasFilterContextSwitcher::CanvasFilterContextSwitcher):
(WebCore::CanvasFilterContextSwitcher::~CanvasFilterContextSwitcher):
(WebCore::CanvasFilterContextSwitcher::expandedBounds const):
* Source/WebCore/html/canvas/CanvasFilterContextSwitcher.h: Copied from Source/WebCore/html/canvas/CanvasFilterTargetSwitcher.h.
* Source/WebCore/html/canvas/CanvasLayerContextSwitcher.cpp: Renamed from Source/WebCore/html/canvas/CanvasFilterTargetSwitcher.cpp.
(WebCore::CanvasLayerContextSwitcher::create):
(WebCore::CanvasLayerContextSwitcher::CanvasLayerContextSwitcher):
(WebCore::CanvasLayerContextSwitcher::~CanvasLayerContextSwitcher):
(WebCore::CanvasLayerContextSwitcher::drawingContext const):
(WebCore::CanvasLayerContextSwitcher::outsets const):
* Source/WebCore/html/canvas/CanvasLayerContextSwitcher.h: Renamed from Source/WebCore/html/canvas/CanvasFilterTargetSwitcher.h.
* Source/WebCore/html/canvas/CanvasLayers.idl: Added.
* Source/WebCore/html/canvas/CanvasRenderingContext2D.cpp:
(WebCore::CanvasRenderingContext2D::createFilter const):
* Source/WebCore/html/canvas/CanvasRenderingContext2D.h:
* Source/WebCore/html/canvas/CanvasRenderingContext2D.idl:
* Source/WebCore/html/canvas/CanvasRenderingContext2DBase.cpp:
(WebCore::CanvasRenderingContext2DBase::beginLayer):
(WebCore::CanvasRenderingContext2DBase::endLayer):
(WebCore::CanvasRenderingContext2DBase::fillInternal):
(WebCore::CanvasRenderingContext2DBase::strokeInternal):
(WebCore::CanvasRenderingContext2DBase::fillRect):
(WebCore::CanvasRenderingContext2DBase::strokeRect):
(WebCore::CanvasRenderingContext2DBase::drawImage):
(WebCore::CanvasRenderingContext2DBase::drawingContext const):
(WebCore::CanvasRenderingContext2DBase::effectiveDrawingContext const):
(WebCore::CanvasRenderingContext2DBase::drawTextUnchecked):
* Source/WebCore/html/canvas/CanvasRenderingContext2DBase.h:
(WebCore::CanvasRenderingContext2DBase::createFilter const):
(WebCore::CanvasRenderingContext2DBase::setFilterTargetSwitcher): Deleted.
* Source/WebCore/html/canvas/OffscreenCanvasRenderingContext2D.idl:
* Source/WebCore/platform/graphics/GraphicsContextSwitcher.cpp: Renamed from Source/WebCore/platform/graphics/filters/FilterTargetSwitcher.cpp.
(WebCore::GraphicsContextSwitcher::create):
(WebCore::GraphicsContextSwitcher::GraphicsContextSwitcher):
* Source/WebCore/platform/graphics/GraphicsContextSwitcher.h: Renamed from Source/WebCore/platform/graphics/filters/FilterTargetSwitcher.h.
* Source/WebCore/platform/graphics/ImageBuffer.cpp:
(WebCore::ImageBuffer::filteredNativeImage):
* Source/WebCore/platform/graphics/ImageBufferContextSwitcher.cpp: Renamed from Source/WebCore/platform/graphics/filters/FilterImageTargetSwitcher.cpp.
(WebCore::ImageBufferContextSwitcher::ImageBufferContextSwitcher):
(WebCore::ImageBufferContextSwitcher::drawingContext const):
(WebCore::ImageBufferContextSwitcher::beginClipAndDrawSourceImage):
(WebCore::ImageBufferContextSwitcher::endClipAndDrawSourceImage):
(WebCore::ImageBufferContextSwitcher::endDrawSourceImage):
* Source/WebCore/platform/graphics/ImageBufferContextSwitcher.h: Renamed from Source/WebCore/platform/graphics/filters/FilterImageTargetSwitcher.h.
* Source/WebCore/platform/graphics/TransparencyLayerContextSwitcher.cpp: Renamed from Source/WebCore/platform/graphics/filters/FilterStyleTargetSwitcher.cpp.
(WebCore::TransparencyLayerContextSwitcher::TransparencyLayerContextSwitcher):
(WebCore::TransparencyLayerContextSwitcher::beginClipAndDrawSourceImage):
(WebCore::TransparencyLayerContextSwitcher::beginDrawSourceImage):
(WebCore::TransparencyLayerContextSwitcher::endDrawSourceImage):
* Source/WebCore/platform/graphics/TransparencyLayerContextSwitcher.h: Renamed from Source/WebCore/platform/graphics/filters/FilterStyleTargetSwitcher.h.
* Source/WebCore/rendering/RenderLayerFilters.cpp:
(WebCore::RenderLayerFilters::beginFilterEffect):
* Source/WebCore/rendering/RenderLayerFilters.h:
* Source/WebCore/rendering/svg/legacy/LegacyRenderSVGResourceFilter.cpp:
(WebCore::LegacyRenderSVGResourceFilter::applyResource):
* Source/WebCore/rendering/svg/legacy/LegacyRenderSVGResourceFilter.h:

Canonical link: https://commits.webkit.org/280148@main
The HTMLCanvasElement's bitmap was linkified to clarify to which bitmap
each uses of the word "bitmap" refers to (either the element's bitmap,
the context's top level output bitmap or the context's current output
bitmap).
This means that when entering a layer, the current transform now is
reset to the identity matrix. setTransform and getTransform are now
local to the current layer, meaning that calling setTransform sets a
matrix relative to the parent layer.
@graveljp
Copy link
Author

Hi all,

I uploaded a few updates to this PR following to the above discussions. The changes since the last update include:

  • ctx.filter and the current transformation matrix are now part of the the layer rendering states. This means that they are both reset to their default value when entering a layer.
  • As before, filters and shadows ignore the layer's current transform, but they are now impacted by the transforms of parent layers. This means that if you scale a layer containing a shadowed shape, the shape and the shadow will scale together.
  • I have removed the BeginLayerOptions dictionary, meaning that beginLayer() no longer has a parameter. Filters can now only be specified via ctx.filter.

The current transformation matrix change was tricky to do. On the one hand, shadows and filters are no longer global, so we need to keep track of the coordinate spaces in which they are drawn. On the other hand, we have concepts like the current default path which stores coordinates pre-transformed with with the global matrix. To make everything work, the spec now defines three different types of matrices:

  • The current transformation matrix: the matrix local to the current layer. This is the matrix manipulated by the transforms API.
  • The parents transformation matrix: the multiplication of the current transformation matrix of all ancestors of the current layer. This is the matrix applied to the shadows and filters.
  • The total transformation matrix: the multiplication of all matrices, equivalent to multiplying parents and the current transforms. This is the matrix applied to shapes, images and paths.

To apply the shadow on pre-transformed shapes like the ones using the current default path, I added an algorithm for applying a filter or shadow "in layer coordinate space".

Please review these latest changes and let me know if there's any issue.

@Kaiido
Copy link
Member

Kaiido commented Sep 26, 2024

I still haven't had enough time for a thorough review, but regarding the new total transformation matrix property, instead of being an actual object that is set, it's live and always the result of the multiplication of both other matrices, right?
Seems a bit unusual to my non-editor's eyes, but I'll let @domenic confirm it can work like this.

Otherwise at a glance it looks good (modulo some typos), but I'll try to take the time to think of all the edge cases.

@graveljp
Copy link
Author

regarding the new total transformation matrix property, instead of being an actual object that is set, it's live and always the result of the multiplication of both other matrices, right?

That's the idea. In the PR, I wrote:

The <span>total transformation matrix</span> [...] is always equals to the results of
multiplying the <span>parents transformation matrix</span> with the <span>current transformation
matrix</span>.

Otherwise at a glance it looks good (modulo some typos), but I'll try to take the time to think of all the edge cases.

Thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Development

Successfully merging this pull request may close these issues.

Canvas2D layers with filter support
7 participants