@@ -353,21 +353,56 @@ class BitmapCanvas extends EngineCanvas {
353353
354354 @override
355355 void drawImage (ui.Image image, ui.Offset p, SurfacePaintData paint) {
356- _drawImage (image, p, paint);
356+ final html.HtmlElement imageElement = _drawImage (image, p, paint);
357+ if (paint.colorFilter != null ) {
358+ _applyTargetSize (imageElement, image.width.toDouble (),
359+ image.height.toDouble ());
360+ }
357361 _childOverdraw = true ;
358362 _canvasPool.closeCurrentCanvas ();
359363 _cachedLastStyle = null ;
360364 }
361365
362- html.ImageElement _drawImage (
366+ html.HtmlElement _drawImage (
363367 ui.Image image, ui.Offset p, SurfacePaintData paint) {
364368 final HtmlImage htmlImage = image;
365- final html.Element imgElement = htmlImage.cloneImageElement ();
366369 final ui.BlendMode blendMode = paint.blendMode;
370+ final EngineColorFilter colorFilter = paint.colorFilter as EngineColorFilter ;
371+ final ui.BlendMode colorFilterBlendMode = colorFilter? ._blendMode;
372+ html.HtmlElement imgElement;
373+ if (colorFilterBlendMode == null ) {
374+ // No Blending, create an image by cloning original loaded image.
375+ imgElement = htmlImage.cloneImageElement ();
376+ } else {
377+ switch (colorFilterBlendMode) {
378+ case ui.BlendMode .colorBurn:
379+ case ui.BlendMode .colorDodge:
380+ case ui.BlendMode .hue:
381+ case ui.BlendMode .modulate:
382+ case ui.BlendMode .overlay:
383+ case ui.BlendMode .plus:
384+ case ui.BlendMode .srcIn:
385+ case ui.BlendMode .srcATop:
386+ case ui.BlendMode .srcOut:
387+ case ui.BlendMode .saturation:
388+ case ui.BlendMode .color:
389+ case ui.BlendMode .luminosity:
390+ case ui.BlendMode .xor:
391+ imgElement = _createImageElementWithSvgFilter (image,
392+ colorFilter._color, colorFilterBlendMode, paint);
393+ break ;
394+ default :
395+ imgElement = _createBackgroundImageWithBlend (image,
396+ colorFilter._color, colorFilterBlendMode, paint);
397+ break ;
398+ }
399+ }
367400 imgElement.style.mixBlendMode = _stringForBlendMode (blendMode);
368401 if (_canvasPool.isClipped) {
369402 // Reset width/height since they may have been previously set.
370- imgElement.style..removeProperty ('width' )..removeProperty ('height' );
403+ imgElement.style
404+ ..removeProperty ('width' )
405+ ..removeProperty ('height' );
371406 final List <html.Element > clipElements = _clipContent (
372407 _canvasPool._clipStack, imgElement, p, _canvasPool.currentTransform);
373408 for (html.Element clipElement in clipElements) {
@@ -396,10 +431,19 @@ class BitmapCanvas extends EngineCanvas {
396431 src.top != 0 ||
397432 src.width != image.width ||
398433 src.height != image.height;
434+ // If source and destination sizes are identical, we can skip the longer
435+ // code path that sets the size of the element and clips.
436+ //
437+ // If there is a color filter set however, we maybe using background-image
438+ // to render therefore we have to explicitely set width/height of the
439+ // element for blending to work with background-color.
399440 if (dst.width == image.width &&
400441 dst.height == image.height &&
401- ! requiresClipping) {
402- drawImage (image, dst.topLeft, paint);
442+ ! requiresClipping &&
443+ paint.colorFilter == null ) {
444+ _drawImage (image, dst.topLeft, paint);
445+ _childOverdraw = true ;
446+ _canvasPool.closeCurrentCanvas ();
403447 } else {
404448 if (requiresClipping) {
405449 save ();
@@ -418,7 +462,7 @@ class BitmapCanvas extends EngineCanvas {
418462 }
419463 }
420464
421- final html.ImageElement imgElement =
465+ final html.Element imgElement =
422466 _drawImage (image, ui.Offset (targetLeft, targetTop), paint);
423467 // To scale set width / height on destination image.
424468 // For clipping we need to scale according to
@@ -430,17 +474,147 @@ class BitmapCanvas extends EngineCanvas {
430474 targetWidth *= image.width / src.width;
431475 targetHeight *= image.height / src.height;
432476 }
433- final html.CssStyleDeclaration imageStyle = imgElement.style;
434- imageStyle
435- ..width = '${targetWidth .toStringAsFixed (2 )}px'
436- ..height = '${targetHeight .toStringAsFixed (2 )}px' ;
477+ _applyTargetSize (imgElement, targetWidth, targetHeight);
437478 if (requiresClipping) {
438479 restore ();
439480 }
440481 }
441482 _closeCurrentCanvas ();
442483 }
443484
485+ void _applyTargetSize (html.HtmlElement imageElement, double targetWidth,
486+ double targetHeight) {
487+ final html.CssStyleDeclaration imageStyle = imageElement.style;
488+ final String widthPx = '${targetWidth .toStringAsFixed (2 )}px' ;
489+ final String heightPx = '${targetHeight .toStringAsFixed (2 )}px' ;
490+ imageStyle
491+ // left,top are set to 0 (although position is absolute) because
492+ // Chrome will glitch if you leave them out, reproducable with
493+ // canvas_image_blend_test on row 6, MacOS / Chrome 81.04.
494+ ..left = "0px"
495+ ..top = "0px"
496+ ..width = widthPx
497+ ..height = heightPx;
498+ if (imageElement is ! html.ImageElement ) {
499+ imageElement.style.backgroundSize = '$widthPx $heightPx ' ;
500+ }
501+ }
502+
503+ // Creates a Div element to render an image using background-image css
504+ // attribute to be able to use background blend mode(s) when possible.
505+ //
506+ // Example: <div style="
507+ // position:absolute;
508+ // background-image:url(....);
509+ // background-blend-mode:"darken"
510+ // background-color: #RRGGBB">
511+ //
512+ // Special cases:
513+ // For clear,dstOut it generates a blank element.
514+ // For src,srcOver it only sets background-color attribute.
515+ // For dst,dstIn , it only sets source not background color.
516+ html.HtmlElement _createBackgroundImageWithBlend (HtmlImage image,
517+ ui.Color filterColor, ui.BlendMode colorFilterBlendMode,
518+ SurfacePaintData paint) {
519+ // When blending with color we can't use an image element.
520+ // Instead use a div element with background image, color and
521+ // background blend mode.
522+ final html.HtmlElement imgElement = html.DivElement ();
523+ final html.CssStyleDeclaration style = imgElement.style;
524+ switch (colorFilterBlendMode) {
525+ case ui.BlendMode .clear:
526+ case ui.BlendMode .dstOut:
527+ style.position = 'absolute' ;
528+ break ;
529+ case ui.BlendMode .src:
530+ case ui.BlendMode .srcOver:
531+ style
532+ ..position = 'absolute'
533+ ..backgroundColor = colorToCssString (filterColor);
534+ break ;
535+ case ui.BlendMode .dst:
536+ case ui.BlendMode .dstIn:
537+ style
538+ ..position = 'absolute'
539+ ..backgroundImage = "url('${image .imgElement .src }')" ;
540+ break ;
541+ default :
542+ style
543+ ..position = 'absolute'
544+ ..backgroundImage = "url('${image .imgElement .src }')"
545+ ..backgroundBlendMode = _stringForBlendMode (colorFilterBlendMode)
546+ ..backgroundColor = colorToCssString (filterColor);
547+ break ;
548+ }
549+ return imgElement;
550+ }
551+
552+ // Creates an image element and an svg filter to apply on the element.
553+ html.HtmlElement _createImageElementWithSvgFilter (HtmlImage image,
554+ ui.Color filterColor, ui.BlendMode colorFilterBlendMode,
555+ SurfacePaintData paint) {
556+ // For srcIn blendMode, we use an svg filter to apply to image element.
557+ String svgFilter;
558+ switch (colorFilterBlendMode) {
559+ case ui.BlendMode .srcIn:
560+ case ui.BlendMode .srcATop:
561+ svgFilter = _srcInColorFilterToSvg (filterColor);
562+ break ;
563+ case ui.BlendMode .srcOut:
564+ svgFilter = _srcOutColorFilterToSvg (filterColor);
565+ break ;
566+ case ui.BlendMode .xor:
567+ svgFilter = _xorColorFilterToSvg (filterColor);
568+ break ;
569+ case ui.BlendMode .plus:
570+ // Porter duff source + destination.
571+ svgFilter = _compositeColorFilterToSvg (filterColor, 0 , 1 , 1 , 0 );
572+ break ;
573+ case ui.BlendMode .modulate:
574+ // Porter duff source * destination but preserves alpha.
575+ svgFilter = _modulateColorFilterToSvg (filterColor);
576+ break ;
577+ case ui.BlendMode .overlay:
578+ // Since overlay is the same as hard-light by swapping layers,
579+ // pass hard-light blend function.
580+ svgFilter = _blendColorFilterToSvg (filterColor, 'hard-light' ,
581+ swapLayers: true );
582+ break ;
583+ // Several of the filters below (although supported) do not render the
584+ // same (close but not exact) as native flutter when used as blend mode
585+ // for a background-image with a background color. They only look
586+ // identical when feBlend is used within an svg filter definition.
587+ //
588+ // Saturation filter uses destination when source is transparent.
589+ // cMax = math.max(r, math.max(b, g));
590+ // cMin = math.min(r, math.min(b, g));
591+ // delta = cMax - cMin;
592+ // lightness = (cMax + cMin) / 2.0;
593+ // saturation = delta / (1.0 - (2 * lightness - 1.0).abs());
594+ case ui.BlendMode .saturation:
595+ case ui.BlendMode .colorDodge:
596+ case ui.BlendMode .colorBurn:
597+ case ui.BlendMode .hue:
598+ case ui.BlendMode .color:
599+ case ui.BlendMode .luminosity:
600+ svgFilter = _blendColorFilterToSvg (filterColor,
601+ _stringForBlendMode (colorFilterBlendMode));
602+ break ;
603+ default :
604+ break ;
605+ }
606+ final html.Element filterElement =
607+ html.Element .html (svgFilter, treeSanitizer: _NullTreeSanitizer ());
608+ rootElement.append (filterElement);
609+ _children.add (filterElement);
610+ final html.HtmlElement imgElement = image.cloneImageElement ();
611+ imgElement.style.filter = 'url(#_fcf${_filterIdCounter })' ;
612+ if (colorFilterBlendMode == ui.BlendMode .saturation) {
613+ imgElement.style.backgroundColor = colorToCssString (filterColor);
614+ }
615+ return imgElement;
616+ }
617+
444618 // Should be called when we add new html elements into rootElement so that
445619 // paint order is preserved.
446620 //
@@ -797,3 +971,110 @@ String _maskFilterToCss(ui.MaskFilter maskFilter) {
797971 }
798972 return 'blur(${maskFilter .webOnlySigma }px)' ;
799973}
974+
975+ int _filterIdCounter = 0 ;
976+
977+ // The color matrix for feColorMatrix element changes colors based on
978+ // the following:
979+ //
980+ // | R' | | r1 r2 r3 r4 r5 | | R |
981+ // | G' | | g1 g2 g3 g4 g5 | | G |
982+ // | B' | = | b1 b2 b3 b4 b5 | * | B |
983+ // | A' | | a1 a2 a3 a4 a5 | | A |
984+ // | 1 | | 0 0 0 0 1 | | 1 |
985+ //
986+ // R' = r1*R + r2*G + r3*B + r4*A + r5
987+ // G' = g1*R + g2*G + g3*B + g4*A + g5
988+ // B' = b1*R + b2*G + b3*B + b4*A + b5
989+ // A' = a1*R + a2*G + a3*B + a4*A + a5
990+ String _srcInColorFilterToSvg (ui.Color color) {
991+ _filterIdCounter += 1 ;
992+ return '<svg width="0" height="0">'
993+ '<filter id="_fcf$_filterIdCounter " '
994+ 'filterUnits="objectBoundingBox" x="0%" y="0%" width="100%" height="100%">'
995+ '<feColorMatrix values="0 0 0 0 1 ' // Ignore input, set it to absolute.
996+ '0 0 0 0 1 '
997+ '0 0 0 0 1 '
998+ '0 0 0 1 0" result="destalpha"/>' // Just take alpha channel of destination
999+ '<feFlood flood-color="${colorToCssString (color )}" flood-opacity="1" result="flood">'
1000+ '</feFlood>'
1001+ '<feComposite in="flood" in2="destalpha" '
1002+ 'operator="arithmetic" k1="1" k2="0" k3="0" k4="0" result="comp">'
1003+ '</feComposite>'
1004+ '</filter></svg>' ;
1005+ }
1006+
1007+ String _srcOutColorFilterToSvg (ui.Color color) {
1008+ _filterIdCounter += 1 ;
1009+ return '<svg width="0" height="0">'
1010+ '<filter id="_fcf$_filterIdCounter " '
1011+ 'filterUnits="objectBoundingBox" x="0%" y="0%" width="100%" height="100%">'
1012+ '<feFlood flood-color="${colorToCssString (color )}" flood-opacity="1" result="flood">'
1013+ '</feFlood>'
1014+ '<feComposite in="flood" in2="SourceGraphic" operator="out" result="comp">'
1015+ '</feComposite>'
1016+ '</filter></svg>' ;
1017+ }
1018+
1019+ String _xorColorFilterToSvg (ui.Color color) {
1020+ _filterIdCounter += 1 ;
1021+ return '<svg width="0" height="0">'
1022+ '<filter id="_fcf$_filterIdCounter " '
1023+ 'filterUnits="objectBoundingBox" x="0%" y="0%" width="100%" height="100%">'
1024+ '<feFlood flood-color="${colorToCssString (color )}" flood-opacity="1" result="flood">'
1025+ '</feFlood>'
1026+ '<feComposite in="flood" in2="SourceGraphic" operator="xor" result="comp">'
1027+ '</feComposite>'
1028+ '</filter></svg>' ;
1029+ }
1030+
1031+ // The source image and color are composited using :
1032+ // result = k1 *in*in2 + k2*in + k3*in2 + k4.
1033+ String _compositeColorFilterToSvg (ui.Color color, double k1, double k2, double k3 , double k4) {
1034+ _filterIdCounter += 1 ;
1035+ return '<svg width="0" height="0">'
1036+ '<filter id="_fcf$_filterIdCounter " '
1037+ 'filterUnits="objectBoundingBox" x="0%" y="0%" width="100%" height="100%">'
1038+ '<feFlood flood-color="${colorToCssString (color )}" flood-opacity="1" result="flood">'
1039+ '</feFlood>'
1040+ '<feComposite in="flood" in2="SourceGraphic" '
1041+ 'operator="arithmetic" k1="$k1 " k2="$k2 " k3="$k3 " k4="$k4 " result="comp">'
1042+ '</feComposite>'
1043+ '</filter></svg>' ;
1044+ }
1045+
1046+ // Porter duff source * destination , keep source alpha.
1047+ // First apply color filter to source to change it to [color], then
1048+ // composite using multiplication.
1049+ String _modulateColorFilterToSvg (ui.Color color) {
1050+ _filterIdCounter += 1 ;
1051+ final double r = color.red / 255.0 ;
1052+ final double b = color.blue / 255.0 ;
1053+ final double g = color.green / 255.0 ;
1054+ return '<svg width="0" height="0">'
1055+ '<filter id="_fcf$_filterIdCounter " '
1056+ 'filterUnits="objectBoundingBox" x="0%" y="0%" width="100%" height="100%">'
1057+ '<feColorMatrix values="0 0 0 0 $r ' // Ignore input, set it to absolute.
1058+ '0 0 0 0 $g '
1059+ '0 0 0 0 $b '
1060+ '0 0 0 1 0" result="recolor"/>'
1061+ '<feComposite in="recolor" in2="SourceGraphic" '
1062+ 'operator="arithmetic" k1="1" k2="0" k3="0" k4="0" result="comp">'
1063+ '</feComposite>'
1064+ '</filter></svg>' ;
1065+ }
1066+
1067+ // Uses feBlend element to blend source image with a color.
1068+ String _blendColorFilterToSvg (ui.Color color, String feBlend,
1069+ {bool swapLayers = false }) {
1070+ _filterIdCounter += 1 ;
1071+ return '<svg width="0" height="0">'
1072+ '<filter id="_fcf$_filterIdCounter " filterUnits="objectBoundingBox" '
1073+ 'x="0%" y="0%" width="100%" height="100%">'
1074+ '<feFlood flood-color="${colorToCssString (color )}" flood-opacity="1" result="flood">'
1075+ '</feFlood>' +
1076+ (swapLayers
1077+ ? '<feBlend in="SourceGraphic" in2="flood" mode="$feBlend "/>'
1078+ : '<feBlend in="flood" in2="SourceGraphic" mode="$feBlend "/>' ) +
1079+ '</filter></svg>' ;
1080+ }
0 commit comments