Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Addresses #3610
Changes:
_getTintedImageCanvas
to useglobalCompositeOperation
instead of modifying thepixels
array_getTintedImageCanvas
PR Checklist
npm run lint
passesExplanation
Benchmarks
I added
test/manual-test-examples/tint-performance
to demonstrate the issue and the effect of the fix. This is the same astest/manual-test-examples/tint
, but uses larger source images to demonstrate the problem, shows the frame rate in the corner, and also translates the images over time so you can see the dropped frames.The old tint implementation gets 8fps on my 2015 Macbook Pro. The new implementation gets 20fps (still not perfect, but much better.)
Here is an online version of this benchmark: https://editor.p5js.org/davepagurek/sketches/D_ehdpTjO Try commenting out the redefinition of
_getTintedImageCanvas
to see the performance of the original.Why is the implementation so complicated?
At first I thought the whole tint operation could be replaced with a single
multiply
blend mode to apply the tint color, and then usingglobalAlpha
to apply the alpha tint. However, themultiply
blend mode destroys the alpha channel in the process and mixes the colors with white before applying the tint:This is not a result of the top layer having full opacity. Even when the top layer has alpha values that match the bottom layer, it still blends the bottom layer with white before applying the tint and then finally applying the top layer's alpha. This would lead to semi-transparent parts of the tinted image getting progressively whiter as they fade out:
I address this by reconstructing a fully opaque version of the original image before doing the color tint by rendering the image three times: first the original, then with the
luminosity
andchroma
blend modes, which also wipe out the alpha channel, but restore the original color. (Unfortunately, due to the canvas storing colors with premultiplied alpha, we lose color precision by doing this.) Then we can tint the color without any white mixing in, and then bring the alpha back later with thedestination-in
blend mode.So, yes, it takes four render passes to achieve what we did before by looping through
pixels
. I've tried to add a lot of comments explaining this since it's not the most intuitive thing to read. In practice, this method is still faster, showing a >2x speedup on my machine for color tints.Since some people want to use
tint
just to apply opacity, I added a branch to optimize this by drawing just usingglobalAlpha
. This branch is quite fast and hits 60fps for me!Future work
The four-pass render is only necessary because the
multiply
blend mode method breaks images that have semitransparent sections. We could make this even faster still if we know images don't have any semitransparent pixels, but that would require having a fast way of checking for it, and looping throughpixels
is slow. If anyone has any ideas how to detect that, it would speed up what is probably the more common use case!