This repository was archived by the owner on Feb 25, 2025. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 6k
[web] Add support for ColorFilter on images #18111
Merged
Merged
Changes from all commits
Commits
Show all changes
15 commits
Select commit
Hold shift + click to select a range
115bd2e
Implement Color filter for images
ferhatb 9729b55
Add blend modes
ferhatb f2f733b
complete set of blend modes
ferhatb 264c87d
Add golden test for blend modes, fix size setting when using drawImag…
ferhatb 4c403f1
Add comments
ferhatb 707a459
Update golden locks
ferhatb 7c74b7c
Merge remote-tracking branch 'upstream/master' into colorfilter
ferhatb 338eb36
Fix analyzer errors
ferhatb 6f3e3cf
disable write:true on golden
ferhatb 9ce84ab
Update lib/web_ui/lib/src/engine/bitmap_canvas.dart
ferhatb aee791a
Merge remote-tracking branch 'upstream/master' into colorfilter
ferhatb 2e57b6e
Merge remote-tracking branch 'upstream/master' into colorfilter
ferhatb 8cc4c40
Add logo image size to comment
ferhatb 8c84c84
Merge branch 'colorfilter' of github.com:ferhatb/engine into colorfilter
ferhatb b1790c7
fix blend group count in test
ferhatb File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,2 +1,2 @@ | ||
| repository: https://github.com/flutter/goldens.git | ||
| revision: f64d8957ae281d1558647f0591ff9742e6135385 | ||
| revision: 790616cbfb269fe17d44840ce52ec187fff5f9a7 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -353,21 +353,56 @@ class BitmapCanvas extends EngineCanvas { | |
|
|
||
| @override | ||
| void drawImage(ui.Image image, ui.Offset p, SurfacePaintData paint) { | ||
| _drawImage(image, p, paint); | ||
| final html.HtmlElement imageElement = _drawImage(image, p, paint); | ||
| if (paint.colorFilter != null) { | ||
| _applyTargetSize(imageElement, image.width.toDouble(), | ||
| image.height.toDouble()); | ||
| } | ||
| _childOverdraw = true; | ||
| _canvasPool.closeCurrentCanvas(); | ||
| _cachedLastStyle = null; | ||
| } | ||
|
|
||
| html.ImageElement _drawImage( | ||
| html.HtmlElement _drawImage( | ||
| ui.Image image, ui.Offset p, SurfacePaintData paint) { | ||
| final HtmlImage htmlImage = image; | ||
| final html.Element imgElement = htmlImage.cloneImageElement(); | ||
| final ui.BlendMode blendMode = paint.blendMode; | ||
| final EngineColorFilter colorFilter = paint.colorFilter as EngineColorFilter; | ||
| final ui.BlendMode colorFilterBlendMode = colorFilter?._blendMode; | ||
| html.HtmlElement imgElement; | ||
| if (colorFilterBlendMode == null) { | ||
| // No Blending, create an image by cloning original loaded image. | ||
| imgElement = htmlImage.cloneImageElement(); | ||
| } else { | ||
| switch (colorFilterBlendMode) { | ||
| case ui.BlendMode.colorBurn: | ||
| case ui.BlendMode.colorDodge: | ||
| case ui.BlendMode.hue: | ||
| case ui.BlendMode.modulate: | ||
| case ui.BlendMode.overlay: | ||
| case ui.BlendMode.plus: | ||
| case ui.BlendMode.srcIn: | ||
| case ui.BlendMode.srcATop: | ||
| case ui.BlendMode.srcOut: | ||
| case ui.BlendMode.saturation: | ||
| case ui.BlendMode.color: | ||
| case ui.BlendMode.luminosity: | ||
| case ui.BlendMode.xor: | ||
| imgElement = _createImageElementWithSvgFilter(image, | ||
| colorFilter._color, colorFilterBlendMode, paint); | ||
| break; | ||
| default: | ||
| imgElement = _createBackgroundImageWithBlend(image, | ||
| colorFilter._color, colorFilterBlendMode, paint); | ||
| break; | ||
| } | ||
| } | ||
| imgElement.style.mixBlendMode = _stringForBlendMode(blendMode); | ||
| if (_canvasPool.isClipped) { | ||
| // Reset width/height since they may have been previously set. | ||
| imgElement.style..removeProperty('width')..removeProperty('height'); | ||
| imgElement.style | ||
| ..removeProperty('width') | ||
| ..removeProperty('height'); | ||
| final List<html.Element> clipElements = _clipContent( | ||
| _canvasPool._clipStack, imgElement, p, _canvasPool.currentTransform); | ||
| for (html.Element clipElement in clipElements) { | ||
|
|
@@ -396,10 +431,19 @@ class BitmapCanvas extends EngineCanvas { | |
| src.top != 0 || | ||
| src.width != image.width || | ||
| src.height != image.height; | ||
| // If source and destination sizes are identical, we can skip the longer | ||
| // code path that sets the size of the element and clips. | ||
| // | ||
| // If there is a color filter set however, we maybe using background-image | ||
| // to render therefore we have to explicitely set width/height of the | ||
| // element for blending to work with background-color. | ||
| if (dst.width == image.width && | ||
| dst.height == image.height && | ||
| !requiresClipping) { | ||
| drawImage(image, dst.topLeft, paint); | ||
| !requiresClipping && | ||
| paint.colorFilter == null) { | ||
| _drawImage(image, dst.topLeft, paint); | ||
| _childOverdraw = true; | ||
| _canvasPool.closeCurrentCanvas(); | ||
| } else { | ||
| if (requiresClipping) { | ||
| save(); | ||
|
|
@@ -418,7 +462,7 @@ class BitmapCanvas extends EngineCanvas { | |
| } | ||
| } | ||
|
|
||
| final html.ImageElement imgElement = | ||
| final html.Element imgElement = | ||
| _drawImage(image, ui.Offset(targetLeft, targetTop), paint); | ||
| // To scale set width / height on destination image. | ||
| // For clipping we need to scale according to | ||
|
|
@@ -430,17 +474,147 @@ class BitmapCanvas extends EngineCanvas { | |
| targetWidth *= image.width / src.width; | ||
| targetHeight *= image.height / src.height; | ||
| } | ||
| final html.CssStyleDeclaration imageStyle = imgElement.style; | ||
| imageStyle | ||
| ..width = '${targetWidth.toStringAsFixed(2)}px' | ||
| ..height = '${targetHeight.toStringAsFixed(2)}px'; | ||
| _applyTargetSize(imgElement, targetWidth, targetHeight); | ||
| if (requiresClipping) { | ||
| restore(); | ||
| } | ||
| } | ||
| _closeCurrentCanvas(); | ||
| } | ||
|
|
||
| void _applyTargetSize(html.HtmlElement imageElement, double targetWidth, | ||
| double targetHeight) { | ||
| final html.CssStyleDeclaration imageStyle = imageElement.style; | ||
| final String widthPx = '${targetWidth.toStringAsFixed(2)}px'; | ||
| final String heightPx = '${targetHeight.toStringAsFixed(2)}px'; | ||
| imageStyle | ||
| // left,top are set to 0 (although position is absolute) because | ||
| // Chrome will glitch if you leave them out, reproducable with | ||
| // canvas_image_blend_test on row 6, MacOS / Chrome 81.04. | ||
| ..left = "0px" | ||
| ..top = "0px" | ||
| ..width = widthPx | ||
| ..height = heightPx; | ||
| if (imageElement is! html.ImageElement) { | ||
| imageElement.style.backgroundSize = '$widthPx $heightPx'; | ||
| } | ||
| } | ||
|
|
||
| // Creates a Div element to render an image using background-image css | ||
| // attribute to be able to use background blend mode(s) when possible. | ||
| // | ||
| // Example: <div style=" | ||
| // position:absolute; | ||
| // background-image:url(....); | ||
| // background-blend-mode:"darken" | ||
| // background-color: #RRGGBB"> | ||
| // | ||
| // Special cases: | ||
| // For clear,dstOut it generates a blank element. | ||
| // For src,srcOver it only sets background-color attribute. | ||
| // For dst,dstIn , it only sets source not background color. | ||
| html.HtmlElement _createBackgroundImageWithBlend(HtmlImage image, | ||
| ui.Color filterColor, ui.BlendMode colorFilterBlendMode, | ||
| SurfacePaintData paint) { | ||
| // When blending with color we can't use an image element. | ||
| // Instead use a div element with background image, color and | ||
| // background blend mode. | ||
| final html.HtmlElement imgElement = html.DivElement(); | ||
| final html.CssStyleDeclaration style = imgElement.style; | ||
| switch (colorFilterBlendMode) { | ||
| case ui.BlendMode.clear: | ||
| case ui.BlendMode.dstOut: | ||
| style.position = 'absolute'; | ||
| break; | ||
| case ui.BlendMode.src: | ||
| case ui.BlendMode.srcOver: | ||
| style | ||
| ..position = 'absolute' | ||
| ..backgroundColor = colorToCssString(filterColor); | ||
| break; | ||
| case ui.BlendMode.dst: | ||
| case ui.BlendMode.dstIn: | ||
| style | ||
| ..position = 'absolute' | ||
| ..backgroundImage = "url('${image.imgElement.src}')"; | ||
| break; | ||
| default: | ||
| style | ||
| ..position = 'absolute' | ||
| ..backgroundImage = "url('${image.imgElement.src}')" | ||
| ..backgroundBlendMode = _stringForBlendMode(colorFilterBlendMode) | ||
| ..backgroundColor = colorToCssString(filterColor); | ||
| break; | ||
| } | ||
| return imgElement; | ||
| } | ||
|
|
||
| // Creates an image element and an svg filter to apply on the element. | ||
| html.HtmlElement _createImageElementWithSvgFilter(HtmlImage image, | ||
| ui.Color filterColor, ui.BlendMode colorFilterBlendMode, | ||
| SurfacePaintData paint) { | ||
| // For srcIn blendMode, we use an svg filter to apply to image element. | ||
| String svgFilter; | ||
| switch (colorFilterBlendMode) { | ||
| case ui.BlendMode.srcIn: | ||
| case ui.BlendMode.srcATop: | ||
| svgFilter = _srcInColorFilterToSvg(filterColor); | ||
| break; | ||
| case ui.BlendMode.srcOut: | ||
| svgFilter = _srcOutColorFilterToSvg(filterColor); | ||
| break; | ||
| case ui.BlendMode.xor: | ||
| svgFilter = _xorColorFilterToSvg(filterColor); | ||
| break; | ||
| case ui.BlendMode.plus: | ||
| // Porter duff source + destination. | ||
| svgFilter = _compositeColorFilterToSvg(filterColor, 0, 1, 1, 0); | ||
| break; | ||
| case ui.BlendMode.modulate: | ||
| // Porter duff source * destination but preserves alpha. | ||
| svgFilter = _modulateColorFilterToSvg(filterColor); | ||
| break; | ||
| case ui.BlendMode.overlay: | ||
| // Since overlay is the same as hard-light by swapping layers, | ||
| // pass hard-light blend function. | ||
| svgFilter = _blendColorFilterToSvg(filterColor, 'hard-light', | ||
| swapLayers: true); | ||
| break; | ||
| // Several of the filters below (although supported) do not render the | ||
| // same (close but not exact) as native flutter when used as blend mode | ||
| // for a background-image with a background color. They only look | ||
| // identical when feBlend is used within an svg filter definition. | ||
| // | ||
| // Saturation filter uses destination when source is transparent. | ||
| // cMax = math.max(r, math.max(b, g)); | ||
| // cMin = math.min(r, math.min(b, g)); | ||
| // delta = cMax - cMin; | ||
| // lightness = (cMax + cMin) / 2.0; | ||
| // saturation = delta / (1.0 - (2 * lightness - 1.0).abs()); | ||
| case ui.BlendMode.saturation: | ||
| case ui.BlendMode.colorDodge: | ||
| case ui.BlendMode.colorBurn: | ||
| case ui.BlendMode.hue: | ||
| case ui.BlendMode.color: | ||
| case ui.BlendMode.luminosity: | ||
| svgFilter = _blendColorFilterToSvg(filterColor, | ||
| _stringForBlendMode(colorFilterBlendMode)); | ||
| break; | ||
| default: | ||
| break; | ||
| } | ||
| final html.Element filterElement = | ||
| html.Element.html(svgFilter, treeSanitizer: _NullTreeSanitizer()); | ||
| rootElement.append(filterElement); | ||
| _children.add(filterElement); | ||
| final html.HtmlElement imgElement = image.cloneImageElement(); | ||
| imgElement.style.filter = 'url(#_fcf${_filterIdCounter})'; | ||
| if (colorFilterBlendMode == ui.BlendMode.saturation) { | ||
| imgElement.style.backgroundColor = colorToCssString(filterColor); | ||
| } | ||
| return imgElement; | ||
| } | ||
|
|
||
| // Should be called when we add new html elements into rootElement so that | ||
| // paint order is preserved. | ||
| // | ||
|
|
@@ -797,3 +971,110 @@ String _maskFilterToCss(ui.MaskFilter maskFilter) { | |
| } | ||
| return 'blur(${maskFilter.webOnlySigma}px)'; | ||
| } | ||
|
|
||
| int _filterIdCounter = 0; | ||
|
|
||
| // The color matrix for feColorMatrix element changes colors based on | ||
| // the following: | ||
| // | ||
| // | R' | | r1 r2 r3 r4 r5 | | R | | ||
| // | G' | | g1 g2 g3 g4 g5 | | G | | ||
| // | B' | = | b1 b2 b3 b4 b5 | * | B | | ||
| // | A' | | a1 a2 a3 a4 a5 | | A | | ||
| // | 1 | | 0 0 0 0 1 | | 1 | | ||
| // | ||
| // R' = r1*R + r2*G + r3*B + r4*A + r5 | ||
| // G' = g1*R + g2*G + g3*B + g4*A + g5 | ||
| // B' = b1*R + b2*G + b3*B + b4*A + b5 | ||
| // A' = a1*R + a2*G + a3*B + a4*A + a5 | ||
| String _srcInColorFilterToSvg(ui.Color color) { | ||
| _filterIdCounter += 1; | ||
| return '<svg width="0" height="0">' | ||
| '<filter id="_fcf$_filterIdCounter" ' | ||
| 'filterUnits="objectBoundingBox" x="0%" y="0%" width="100%" height="100%">' | ||
| '<feColorMatrix values="0 0 0 0 1 ' // Ignore input, set it to absolute. | ||
| '0 0 0 0 1 ' | ||
| '0 0 0 0 1 ' | ||
| '0 0 0 1 0" result="destalpha"/>' // Just take alpha channel of destination | ||
| '<feFlood flood-color="${colorToCssString(color)}" flood-opacity="1" result="flood">' | ||
| '</feFlood>' | ||
| '<feComposite in="flood" in2="destalpha" ' | ||
| 'operator="arithmetic" k1="1" k2="0" k3="0" k4="0" result="comp">' | ||
| '</feComposite>' | ||
| '</filter></svg>'; | ||
| } | ||
|
|
||
| String _srcOutColorFilterToSvg(ui.Color color) { | ||
| _filterIdCounter += 1; | ||
| return '<svg width="0" height="0">' | ||
| '<filter id="_fcf$_filterIdCounter" ' | ||
| 'filterUnits="objectBoundingBox" x="0%" y="0%" width="100%" height="100%">' | ||
| '<feFlood flood-color="${colorToCssString(color)}" flood-opacity="1" result="flood">' | ||
| '</feFlood>' | ||
| '<feComposite in="flood" in2="SourceGraphic" operator="out" result="comp">' | ||
| '</feComposite>' | ||
| '</filter></svg>'; | ||
| } | ||
|
|
||
| String _xorColorFilterToSvg(ui.Color color) { | ||
| _filterIdCounter += 1; | ||
| return '<svg width="0" height="0">' | ||
| '<filter id="_fcf$_filterIdCounter" ' | ||
| 'filterUnits="objectBoundingBox" x="0%" y="0%" width="100%" height="100%">' | ||
| '<feFlood flood-color="${colorToCssString(color)}" flood-opacity="1" result="flood">' | ||
| '</feFlood>' | ||
| '<feComposite in="flood" in2="SourceGraphic" operator="xor" result="comp">' | ||
| '</feComposite>' | ||
| '</filter></svg>'; | ||
| } | ||
|
|
||
| // The source image and color are composited using : | ||
| // result = k1 *in*in2 + k2*in + k3*in2 + k4. | ||
| String _compositeColorFilterToSvg(ui.Color color, double k1, double k2, double k3 , double k4) { | ||
| _filterIdCounter += 1; | ||
| return '<svg width="0" height="0">' | ||
| '<filter id="_fcf$_filterIdCounter" ' | ||
| 'filterUnits="objectBoundingBox" x="0%" y="0%" width="100%" height="100%">' | ||
| '<feFlood flood-color="${colorToCssString(color)}" flood-opacity="1" result="flood">' | ||
| '</feFlood>' | ||
| '<feComposite in="flood" in2="SourceGraphic" ' | ||
| 'operator="arithmetic" k1="$k1" k2="$k2" k3="$k3" k4="$k4" result="comp">' | ||
| '</feComposite>' | ||
| '</filter></svg>'; | ||
| } | ||
|
|
||
| // Porter duff source * destination , keep source alpha. | ||
| // First apply color filter to source to change it to [color], then | ||
| // composite using multiplication. | ||
| String _modulateColorFilterToSvg(ui.Color color) { | ||
| _filterIdCounter += 1; | ||
| final double r = color.red / 255.0; | ||
| final double b = color.blue / 255.0; | ||
| final double g = color.green / 255.0; | ||
| return '<svg width="0" height="0">' | ||
| '<filter id="_fcf$_filterIdCounter" ' | ||
| 'filterUnits="objectBoundingBox" x="0%" y="0%" width="100%" height="100%">' | ||
| '<feColorMatrix values="0 0 0 0 $r ' // Ignore input, set it to absolute. | ||
| '0 0 0 0 $g ' | ||
| '0 0 0 0 $b ' | ||
| '0 0 0 1 0" result="recolor"/>' | ||
| '<feComposite in="recolor" in2="SourceGraphic" ' | ||
| 'operator="arithmetic" k1="1" k2="0" k3="0" k4="0" result="comp">' | ||
| '</feComposite>' | ||
| '</filter></svg>'; | ||
| } | ||
|
|
||
| // Uses feBlend element to blend source image with a color. | ||
| String _blendColorFilterToSvg(ui.Color color, String feBlend, | ||
| {bool swapLayers = false}) { | ||
| _filterIdCounter += 1; | ||
| return '<svg width="0" height="0">' | ||
| '<filter id="_fcf$_filterIdCounter" filterUnits="objectBoundingBox" ' | ||
| 'x="0%" y="0%" width="100%" height="100%">' | ||
| '<feFlood flood-color="${colorToCssString(color)}" flood-opacity="1" result="flood">' | ||
| '</feFlood>' + | ||
| (swapLayers | ||
| ? '<feBlend in="SourceGraphic" in2="flood" mode="$feBlend"/>' | ||
| : '<feBlend in="flood" in2="SourceGraphic" mode="$feBlend"/>') + | ||
| '</filter></svg>'; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. These SVG snippets contain a lot of static content that need to be parsed from HTML. Wouldn't using the SVG API be more efficient?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Didn't want to pull in svg library, not as big as dart:html but not small.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Something tells me we already have it all included :) It's OK. We can profile it later and see what the size/speed trade-off is like. |
||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: this line should move down, closer to the location where it's used