Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion lib/web_ui/dev/goldens_lock.yaml
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
303 changes: 292 additions & 11 deletions lib/web_ui/lib/src/engine/bitmap_canvas.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Copy link
Contributor

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

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) {
Expand Down Expand Up @@ -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();
Expand All @@ -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
Expand All @@ -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.
//
Expand Down Expand Up @@ -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>';
Copy link
Contributor

Choose a reason for hiding this comment

The 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?

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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.

Copy link
Contributor

Choose a reason for hiding this comment

The 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.

}
2 changes: 1 addition & 1 deletion lib/web_ui/lib/src/engine/html_image_codec.dart
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ class HtmlImage implements ui.Image {
return imgElement.clone(true);
} else {
_requiresClone = true;
imgElement.style..position = 'absolute';
imgElement.style.position = 'absolute';
return imgElement;
}
}
Expand Down
Loading