mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
[web] Add support for ColorFilter on images (#18111)
* Implement Color filter for images * Add blend modes * complete set of blend modes * Add golden test for blend modes, fix size setting when using drawImage instead of drawImageRect * Add comments * Update golden locks * Fix analyzer errors Co-authored-by: Harry Terkelsen <hterkelsen@users.noreply.github.com> * fix blend group count in test
This commit is contained in:
parent
c2c6ad93bc
commit
a0983d36f7
@ -1,2 +1,2 @@
|
||||
repository: https://github.com/flutter/goldens.git
|
||||
revision: f64d8957ae281d1558647f0591ff9742e6135385
|
||||
revision: 790616cbfb269fe17d44840ce52ec187fff5f9a7
|
||||
|
||||
@ -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,10 +474,7 @@ 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();
|
||||
}
|
||||
@ -441,6 +482,139 @@ class BitmapCanvas extends EngineCanvas {
|
||||
_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>';
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,225 @@
|
||||
// Copyright 2013 The Flutter Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
// @dart = 2.6
|
||||
import 'dart:html' as html;
|
||||
|
||||
import 'package:ui/ui.dart' hide TextStyle;
|
||||
import 'package:ui/src/engine.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
import 'package:web_engine_tester/golden_tester.dart';
|
||||
|
||||
void main() async {
|
||||
const double screenWidth = 600.0;
|
||||
const double screenHeight = 800.0;
|
||||
const Rect screenRect = Rect.fromLTWH(0, 0, screenWidth, screenHeight);
|
||||
|
||||
// Commit a recording canvas to a bitmap, and compare with the expected
|
||||
Future<void> _checkScreenshot(RecordingCanvas rc, String fileName,
|
||||
{Rect region = const Rect.fromLTWH(0, 0, 500, 500),
|
||||
double maxDiffRatePercent = 0.0}) async {
|
||||
final EngineCanvas engineCanvas = BitmapCanvas(screenRect);
|
||||
|
||||
rc.endRecording();
|
||||
rc.apply(engineCanvas, screenRect);
|
||||
|
||||
// Wrap in <flt-scene> so that our CSS selectors kick in.
|
||||
final html.Element sceneElement = html.Element.tag('flt-scene');
|
||||
try {
|
||||
sceneElement.append(engineCanvas.rootElement);
|
||||
html.document.body.append(sceneElement);
|
||||
await matchGoldenFile('$fileName.png', region: region, maxDiffRatePercent: maxDiffRatePercent);
|
||||
} finally {
|
||||
// The page is reused across tests, so remove the element after taking the
|
||||
// Scuba screenshot.
|
||||
sceneElement.remove();
|
||||
}
|
||||
}
|
||||
|
||||
setUp(() async {
|
||||
debugEmulateFlutterTesterEnvironment = true;
|
||||
await webOnlyInitializePlatform();
|
||||
webOnlyFontCollection.debugRegisterTestFonts();
|
||||
await webOnlyFontCollection.ensureFontsLoaded();
|
||||
});
|
||||
|
||||
const Color red = Color(0xFFFF0000);
|
||||
const Color green = Color(0xFF00FF00);
|
||||
const Color blue = Color(0xFF2196F3);
|
||||
const Color white = Color(0xFFFFFFFF);
|
||||
const Color grey = Color(0xFF808080);
|
||||
const Color black = Color(0xFF000000);
|
||||
|
||||
List<List<BlendMode>> modes = [[BlendMode.clear, BlendMode.src, BlendMode.dst,
|
||||
BlendMode.srcOver, BlendMode.dstOver, BlendMode.srcIn, BlendMode.dstIn, BlendMode.srcOut],
|
||||
[BlendMode.dstOut, BlendMode.srcATop, BlendMode.dstATop, BlendMode.xor,
|
||||
BlendMode.plus, BlendMode.modulate, BlendMode.screen, BlendMode.overlay],
|
||||
[BlendMode.darken, BlendMode.lighten, BlendMode.colorDodge, BlendMode.hardLight,
|
||||
BlendMode.softLight, BlendMode.difference, BlendMode.exclusion, BlendMode.multiply],
|
||||
[BlendMode.hue, BlendMode.saturation, BlendMode.color,
|
||||
BlendMode.luminosity]];
|
||||
|
||||
for (int blendGroup = 0; blendGroup < 4; ++blendGroup) {
|
||||
test('Draw image with Group$blendGroup blend modes', () async {
|
||||
final RecordingCanvas rc = RecordingCanvas(
|
||||
const Rect.fromLTRB(0, 0, 400, 400));
|
||||
rc.save();
|
||||
List<BlendMode> blendModes = modes[blendGroup];
|
||||
for (int row = 0; row < blendModes.length; row++) {
|
||||
// draw white background for first 4, black for next 4 blends.
|
||||
double top = row * 50.0;
|
||||
rc.drawRect(Rect.fromLTWH(0, top, 200, 50), Paint()
|
||||
..color = white);
|
||||
rc.drawRect(Rect.fromLTWH(200, top, 200, 50), Paint()
|
||||
..color = grey);
|
||||
BlendMode blendMode = blendModes[row];
|
||||
rc.drawImage(createTestImage(), Offset(0, top),
|
||||
Paint()
|
||||
..colorFilter = EngineColorFilter.mode(red, blendMode));
|
||||
rc.drawImage(createTestImage(), Offset(50, top),
|
||||
Paint()
|
||||
..colorFilter = EngineColorFilter.mode(green, blendMode));
|
||||
rc.drawImage(createTestImage(), Offset(100, top),
|
||||
Paint()
|
||||
..colorFilter = EngineColorFilter.mode(blue, blendMode));
|
||||
rc.drawImage(createTestImage(), Offset(150, top),
|
||||
Paint()
|
||||
..colorFilter = EngineColorFilter.mode(black, blendMode));
|
||||
rc.drawImage(createTestImage(), Offset(200, top),
|
||||
Paint()
|
||||
..colorFilter = EngineColorFilter.mode(red, blendMode));
|
||||
rc.drawImage(createTestImage(), Offset(250, top),
|
||||
Paint()
|
||||
..colorFilter = EngineColorFilter.mode(green, blendMode));
|
||||
rc.drawImage(createTestImage(), Offset(300, top),
|
||||
Paint()
|
||||
..colorFilter = EngineColorFilter.mode(blue, blendMode));
|
||||
rc.drawImage(createTestImage(), Offset(350, top),
|
||||
Paint()
|
||||
..colorFilter = EngineColorFilter.mode(black, blendMode));
|
||||
}
|
||||
rc.restore();
|
||||
await _checkScreenshot(rc, 'canvas_image_blend_group$blendGroup',
|
||||
maxDiffRatePercent: 8.0);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 50x50 pixel flutter logo image.
|
||||
const String _flutterLogoBase64 =
|
||||
'iVBORw0KGgoAAAANSUhEUgAAADIAAAAyCAYAAAAeP4ixAAAAAXNSR0IArs4c6QAAAKRlWElm'
|
||||
'TU0AKgAAAAgABQESAAMAAAABAAEAAAEaAAUAAAABAAAASgEbAAUAAAABAAAAUgExAAIAAAAg'
|
||||
'AAAAWodpAAQAAAABAAAAegAAAAAAAABIAAAAAQAAAEgAAAABQWRvYmUgUGhvdG9zaG9wIENT'
|
||||
'NiAoTWFjaW50b3NoKQAAA6ABAAMAAAABAAEAAKACAAQAAAABAAAAMqADAAQAAAABAAAAMgAA'
|
||||
'AABWBXsWAAAACXBIWXMAAAsTAAALEwEAmpwYAAAEemlUWHRYTUw6Y29tLmFkb2JlLnhtcAAA'
|
||||
'AAAAPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iWE1QIENv'
|
||||
'cmUgNS40LjAiPgogICA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5'
|
||||
'OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPgogICAgICA8cmRmOkRlc2NyaXB0aW9uIHJkZjph'
|
||||
'Ym91dD0iIgogICAgICAgICAgICB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94'
|
||||
'YXAvMS4wL21tLyIKICAgICAgICAgICAgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5j'
|
||||
'b20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiCiAgICAgICAgICAgIHhtbG5zOnhtcD0i'
|
||||
'aHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLyIKICAgICAgICAgICAgeG1sbnM6dGlmZj0i'
|
||||
'aHR0cDovL25zLmFkb2JlLmNvbS90aWZmLzEuMC8iPgogICAgICAgICA8eG1wTU06SW5zdGFu'
|
||||
'Y2VJRD54bXAuaWlkOjMyOERERjc5ODRCRjExRUE5QUE4OEM5NTZDREM5QkUyPC94bXBNTTp'
|
||||
'JbnN0YW5jZUlEPgogICAgICAgICA8eG1wTU06RG9jdW1lbnRJRD54bXAuZGlkOjMyOERERj'
|
||||
'dBODRCRjExRUE5QUE4OEM5NTZDREM5QkUyPC94bXBNTTpEb2N1bWVudElEPgogICAgICAgI'
|
||||
'CA8eG1wTU06T3JpZ2luYWxEb2N1bWVudElEPnhtcC5kaWQ6MDE4MDExNzQwNzIwNjgxMTgy'
|
||||
'MkFBQjU0OEFBMDMwM0E8L3htcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD4KICAgICAgICAgPHht'
|
||||
'cE1NOkRlcml2ZWRGcm9tIHJkZjpwYXJzZVR5cGU9IlJlc291cmNlIj4KICAgICAgICAgICA'
|
||||
'gPHN0UmVmOmluc3RhbmNlSUQ+eG1wLmlpZDowNDgwMTE3NDA3MjA2ODExODIyQUFCNTQ4QU'
|
||||
'EwMzAzQTwvc3RSZWY6aW5zdGFuY2VJRD4KICAgICAgICAgICAgPHN0UmVmOmRvY3VtZW50SU'
|
||||
'Q+eG1wLmRpZDowMTgwMTE3NDA3MjA2ODExODIyQUFCNTQ4QUEwMzAzQTwvc3RSZWY6ZG9jd'
|
||||
'W1lbnRJRD4KICAgICAgICAgPC94bXBNTTpEZXJpdmVkRnJvbT4KICAgICAgICAgPHhtcDpD'
|
||||
'cmVhdG9yVG9vbD5BZG9iZSBQaG90b3Nob3AgQ1M2IChNYWNpbnRvc2gpPC94bXA6Q3JlYXRv'
|
||||
'clRvb2w+CiAgICAgICAgIDx0aWZmOk9yaWVudGF0aW9uPjE8L3RpZmY6T3JpZW50YXRpb2'
|
||||
'4+CiAgICAgIDwvcmRmOkRlc2NyaXB0aW9uPgogICA8L3JkZjpSREY+CjwveDp4bXBtZXRhP'
|
||||
'gr/+ApQAAAQNUlEQVRoBbVaDYxdRRWemTv3vt2WbrctpRYxtCVU4io1LomElrjYWnbbgtr2'
|
||||
'CbISwGpR0UBUjD8YHyZCNCqIP5GmtLWSAn3SVYqw7PbnFUQFXGOMRUFlW9G2gKalu/veu38'
|
||||
'zfufce3ff275dWtBJ5s7cuTPnnO+cM2d+3pN2urDiVSHEVCFuOpgT35vVIuaLV8Tgtw8Icf'
|
||||
'PZkZgntDggrhNCbhHCuihD9J402Y4OLUulqNy1+iwvFIscaaqRMWqyQRofIxFH2ohpwqjDc'
|
||||
'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';
|
||||
|
||||
HtmlImage createTestImage() {
|
||||
return HtmlImage(
|
||||
html.ImageElement()
|
||||
..src = 'data:text/plain;base64,$_flutterLogoBase64',
|
||||
50,
|
||||
50,
|
||||
);
|
||||
}
|
||||
@ -652,7 +652,8 @@ void main() async {
|
||||
|
||||
typedef PaintSpreadPainter = void Function(RecordingCanvas canvas, SurfacePaint paint);
|
||||
|
||||
const String _base64Encoded20x20TestImage = 'iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAIAAAAC64paAAAACXBIWXMAAC4jAAAuIwF4pT92AAAA'
|
||||
const String _base64Encoded20x20TestImage = 'iVBORw0KGgoAAAANSUhEUgAAABQAAAAUC'
|
||||
'AIAAAAC64paAAAACXBIWXMAAC4jAAAuIwF4pT92AAAA'
|
||||
'B3RJTUUH5AMFFBksg4i3gQAAABl0RVh0Q29tbWVudABDcmVhdGVkIHdpdGggR0lNUFeBDhcAAAAj'
|
||||
'SURBVDjLY2TAC/7jlWVioACMah4ZmhnxpyHG0QAb1UyZZgBjWAIm/clP0AAAAABJRU5ErkJggg==';
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user