[web] Add basic color per vertex drawVertices API support (#13066)

* Implement basic drawVertices for BlendMode.srcOver and hairline rendering
* Implement maxDiffRate parameter for screenshot tests
This commit is contained in:
Ferhat 2019-10-14 13:16:05 -07:00 committed by GitHub
parent c643366c19
commit 3fd877715b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 750 additions and 29 deletions

View File

@ -1,2 +1,2 @@
repository: https://github.com/flutter/goldens.git
revision: 7efcec3e8b0bbb6748a992b23a0a89300aa323c7
revision: 686dd320f6cce6da9a7a43e3ec9c0147f39eb19d

View File

@ -146,12 +146,13 @@ class BrowserPlatform extends PlatformPlugin {
final Map<String, dynamic> requestData = json.decode(payload);
final String filename = requestData['filename'];
final bool write = requestData['write'];
final double maxDiffRate = requestData['maxdiffrate'];
final Map<String, dynamic> region = requestData['region'];
final String result = await _diffScreenshot(filename, write, region);
final String result = await _diffScreenshot(filename, write, maxDiffRate ?? _kMaxDiffRateFailure, region);
return shelf.Response.ok(json.encode(result));
}
Future<String> _diffScreenshot(String filename, bool write, [ Map<String, dynamic> region ]) async {
Future<String> _diffScreenshot(String filename, bool write, double maxDiffRateFailure, [ Map<String, dynamic> region ]) async {
if (doUpdateScreenshotGoldens) {
write = true;
}
@ -282,7 +283,7 @@ Golden file $filename did not match the image generated by the test.
final StringBuffer message = StringBuffer();
message.writeln('Golden file $filename did not match the image generated by the test.');
message.writeln(getPrintableDiffFilesInfo(diff.rate, _kMaxDiffRateFailure));
message.writeln(getPrintableDiffFilesInfo(diff.rate, maxDiffRateFailure));
message.writeln('You can view the test report in your browser by opening:');
// Cirrus cannot serve HTML pages generated by build jobs, so we
@ -314,7 +315,7 @@ Golden file $filename did not match the image generated by the test.
message.writeln('Golden file: ${expectedFile.path}');
message.writeln('Actual file: ${actualFile.path}');
if (diff.rate < _kMaxDiffRateFailure) {
if (diff.rate < maxDiffRateFailure) {
// Issue a warning but do not fail the test.
print('WARNING:');
print(message);

View File

@ -664,6 +664,225 @@ class BitmapCanvas extends EngineCanvas with SaveStackTracking {
picture.recordingCanvas.apply(this);
}
// Vertex shader transforms pixel space [Vertices.positions] to
// final clipSpace -1..1 coordinates with inverted Y Axis.
static const _vertexShaderTriangle = '''
#version 300 es
layout (location=0) in vec4 position;
layout (location=1) in vec4 color;
uniform vec4 u_scale;
uniform vec4 u_shift;
out vec4 vColor;
void main() {
gl_Position = (position * u_scale) + u_shift;
vColor = color.zyxw;
}''';
// This fragment shader enables Int32List of colors to be passed directly
// to gl context buffer for rendering by decoding RGBA8888.
static const _fragmentShaderTriangle = '''
#version 300 es
precision highp float;
in vec4 vColor;
out vec4 fragColor;
void main() {
fragColor = vColor;
}''';
// WebGL 1 version of shaders above for compatibility with Safari.
static const _vertexShaderTriangleEs1 = '''
attribute vec4 position;
attribute vec4 color;
uniform vec4 u_scale;
uniform vec4 u_shift;
varying vec4 vColor;
void main() {
gl_Position = (position * u_scale) + u_shift;
vColor = color.zyxw;
}''';
// WebGL 1 version of shaders above for compatibility with Safari.
static const _fragmentShaderTriangleEs1 = '''
precision highp float;
varying vec4 vColor;
void main() {
gl_FragColor = vColor;
}''';
/// Draws vertices on a gl context.
///
/// If both colors and textures is specified in paint data,
/// for [BlendMode.source] we skip colors and use textures,
/// for [BlendMode.dst] we only use colors and ignore textures.
/// We also skip paint shader when no texture is specified.
///
/// If no colors or textures are specified, stroke hairlines with
/// [Paint.color].
///
/// If colors is specified, convert colors to premultiplied (alpha) colors
/// and use a SkTriColorShader to render.
@override
void drawVertices(
ui.Vertices vertices, ui.BlendMode blendMode, ui.PaintData paint) {
// TODO(flutter_web): Implement shaders for [Paint.shader] and
// blendMode. https://github.com/flutter/flutter/issues/40096
// Move rendering to OffscreenCanvas so that transform is preserved
// as well.
assert(paint.shader == null,
'Linear/Radial/SweepGradient and ImageShader not supported yet');
assert(blendMode == ui.BlendMode.srcOver);
final Int32List colors = vertices.colors;
final ui.VertexMode mode = vertices.mode;
if (colors == null) {
final Float32List positions = mode == ui.VertexMode.triangles
? vertices.positions
: _convertVertexPositions(mode, vertices.positions);
// Draw hairline for vertices if no vertex colors are specified.
_drawHairline(positions, paint.color ?? ui.Color(0xFF000000));
return;
}
final html.CanvasElement glCanvas = html.CanvasElement(
width: _widthInBitmapPixels,
height: _heightInBitmapPixels,
);
glCanvas.style
..position = 'absolute'
..width = _canvas.style.width
..height = _canvas.style.height;
glCanvas.className = 'gl-canvas';
_children.add(glCanvas);
rootElement.append(glCanvas);
final bool isWebKit = (browserEngine == BrowserEngine.webkit);
_GlContext gl = _GlContext(glCanvas, isWebKit);
// Create and compile shaders.
Object vertexShader = gl.compileShader('VERTEX_SHADER',
isWebKit ? _vertexShaderTriangleEs1 : _vertexShaderTriangle);
Object fragmentShader = gl.compileShader('FRAGMENT_SHADER',
isWebKit ? _fragmentShaderTriangleEs1 : _fragmentShaderTriangle);
// Create a gl program and link shaders.
Object program = gl.createProgram();
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
gl.linkProgram(program);
gl.useProgram(program);
// Set uniform to scale 0..width/height pixels coordinates to -1..1
// clipspace range and flip the Y axis.
Object resolution = gl.getUniformLocation(program, 'u_scale');
gl.setUniform4f(resolution, 2.0 / _widthInBitmapPixels.toDouble(),
-2.0 / _heightInBitmapPixels.toDouble(), 1, 1);
Object shift = gl.getUniformLocation(program, 'u_shift');
gl.setUniform4f(shift, -1, 1, 0, 0);
// Setup geometry.
Object positionsBuffer = gl.createBuffer();
assert(positionsBuffer != null);
gl.bindArrayBuffer(positionsBuffer);
final Float32List positions = vertices.positions;
gl.bufferData(positions, gl.kStaticDraw);
js_util.callMethod(
gl.glContext, 'vertexAttribPointer', [0, 2, gl.kFloat, false, 0, 0]);
gl.enableVertexAttribArray(0);
// Setup color buffer.
Object colorsBuffer = gl.createBuffer();
gl.bindArrayBuffer(colorsBuffer);
// Buffer kBGRA_8888.
gl.bufferData(colors, gl.kStaticDraw);
js_util.callMethod(gl.glContext, 'vertexAttribPointer',
[1, 4, gl.kUnsignedByte, true, 0, 0]);
gl.enableVertexAttribArray(1);
gl.clear();
final int vertexCount = positions.length ~/ 2;
gl.drawTriangles(vertexCount, mode);
}
void _drawHairline(Float32List positions, ui.Color color) {
assert(positions != null);
html.CanvasRenderingContext2D _ctx = ctx;
save();
final int pointCount = positions.length ~/ 2;
_setFillAndStrokeStyle('', color.toCssString());
_ctx.lineWidth = 1.0;
_ctx.beginPath();
for (int i = 0, len = pointCount * 2; i < len;) {
for (int triangleVertexIndex = 0;
triangleVertexIndex < 3;
triangleVertexIndex++, i += 2) {
final double dx = positions[i];
final double dy = positions[i + 1];
switch (triangleVertexIndex) {
case 0:
_ctx.moveTo(dx, dy);
break;
case 1:
_ctx.lineTo(dx, dy);
break;
case 2:
_ctx.lineTo(dx, dy);
_ctx.closePath();
_ctx.stroke();
}
}
}
restore();
}
// Converts from [VertexMode] triangleFan and triangleStrip to triangles.
Float32List _convertVertexPositions(
ui.VertexMode mode, Float32List positions) {
assert(mode != ui.VertexMode.triangles);
if (mode == ui.VertexMode.triangleFan) {
final int coordinateCount = positions.length ~/ 2;
final int triangleCount = coordinateCount - 2;
final Float32List triangleList = Float32List(triangleCount * 3 * 2);
double centerX = positions[0];
double centerY = positions[1];
int destIndex = 0;
int positionIndex = 2;
for (int triangleIndex = 0;
triangleIndex < triangleCount;
triangleIndex++, positionIndex += 2) {
triangleList[destIndex++] = centerX;
triangleList[destIndex++] = centerY;
triangleList[destIndex++] = positions[positionIndex];
triangleList[destIndex++] = positions[positionIndex + 1];
triangleList[destIndex++] = positions[positionIndex + 2];
triangleList[destIndex++] = positions[positionIndex + 3];
}
return triangleList;
} else {
assert(mode == ui.VertexMode.triangleStrip);
// Set of connected triangles. Each triangle shares 2 last vertices.
final int vertexCount = positions.length ~/ 2;
int triangleCount = vertexCount - 2;
double x0 = positions[0];
double y0 = positions[1];
double x1 = positions[2];
double y1 = positions[3];
final Float32List triangleList = Float32List(triangleCount * 3 * 2);
int destIndex = 0;
for (int i = 0, positionIndex = 4; i < triangleCount; i++) {
final double x2 = positions[positionIndex++];
final double y2 = positions[positionIndex++];
triangleList[destIndex++] = x0;
triangleList[destIndex++] = y0;
triangleList[destIndex++] = x1;
triangleList[destIndex++] = y1;
triangleList[destIndex++] = x2;
triangleList[destIndex++] = y2;
x0 = x1;
y0 = y1;
x1 = x2;
y1 = y2;
}
return triangleList;
}
}
/// 'Runs' the given [path] by applying all of its commands to the canvas.
void _runPath(ui.Path path) {
ctx.beginPath();
@ -700,8 +919,8 @@ class BitmapCanvas extends EngineCanvas with SaveStackTracking {
break;
case PathCommandTypes.rRect:
final RRectCommand rrectCommand = command;
_RRectToCanvasRenderer(ctx).render(rrectCommand.rrect,
startNewPath: false);
_RRectToCanvasRenderer(ctx)
.render(rrectCommand.rrect, startNewPath: false);
break;
case PathCommandTypes.rect:
final RectCommand rectCommand = command;
@ -902,3 +1121,162 @@ String _cssTransformAtOffset(
return matrix4ToCssTransform(
transformWithOffset(transform, ui.Offset(offsetX, offsetY)));
}
/// JS Interop helper for webgl apis.
class _GlContext {
final Object glContext;
dynamic _kCompileStatus;
dynamic _kArrayBuffer;
dynamic _kStaticDraw;
dynamic _kFloat;
dynamic _kColorBufferBit;
dynamic _kTriangles;
dynamic _kLinkStatus;
dynamic _kUnsignedByte;
_GlContext(html.CanvasElement canvas, bool useWebGl1)
: glContext = canvas.getContext(useWebGl1 ? 'webgl' : 'webgl2');
Object compileShader(String shaderType, String source) {
Object shader = _createShader(shaderType);
js_util.callMethod(glContext, 'shaderSource', [shader, source]);
js_util.callMethod(glContext, 'compileShader', [shader]);
bool shaderStatus = js_util
.callMethod(glContext, 'getShaderParameter', [shader, compileStatus]);
if (!shaderStatus) {
throw Exception('Shader compilation failed: ${getShaderInfoLog(shader)}');
}
return shader;
}
Object createProgram() =>
js_util.callMethod(glContext, 'createProgram', const []);
void attachShader(Object program, Object shader) {
js_util.callMethod(glContext, 'attachShader', [program, shader]);
}
void linkProgram(Object program) {
js_util.callMethod(glContext, 'linkProgram', [program]);
if (!js_util
.callMethod(glContext, 'getProgramParameter', [program, kLinkStatus])) {
throw Exception(getProgramInfoLog(program));
}
}
void useProgram(Object program) {
js_util.callMethod(glContext, 'useProgram', [program]);
}
Object createBuffer() =>
js_util.callMethod(glContext, 'createBuffer', const []);
void bindArrayBuffer(Object buffer) {
js_util.callMethod(glContext, 'bindBuffer', [kArrayBuffer, buffer]);
}
void bufferData(TypedData data, dynamic type) {
js_util.callMethod(glContext, 'bufferData', [kArrayBuffer, data, type]);
}
void enableVertexAttribArray(int index) {
js_util.callMethod(glContext, 'enableVertexAttribArray', [index]);
}
/// Clear background.
void clear() {
js_util.callMethod(glContext, 'clear', [kColorBufferBit]);
}
void drawTriangles(int triangleCount, ui.VertexMode vertexMode) {
dynamic mode = _triangleTypeFromMode(vertexMode);
js_util.callMethod(glContext, 'drawArrays', [mode, 0, triangleCount]);
}
/// Sets affine transformation from normalized device coordinates
/// to window coordinates
void viewport(double x, double y, double width, double height) {
js_util.callMethod(glContext, 'viewport', [x, y, width, height]);
}
dynamic _triangleTypeFromMode(ui.VertexMode mode) {
switch (mode) {
case ui.VertexMode.triangles:
return kTriangles;
break;
case ui.VertexMode.triangleFan:
return kTriangleFan;
break;
case ui.VertexMode.triangleStrip:
return kTriangleStrip;
break;
}
}
Object _createShader(String shaderType) => js_util.callMethod(
glContext, 'createShader', [js_util.getProperty(glContext, shaderType)]);
/// Error state of gl context.
dynamic get error => js_util.callMethod(glContext, 'getError', const []);
/// Shader compiler error, if this returns [kFalse], to get details use
/// [getShaderInfoLog].
dynamic get compileStatus =>
_kCompileStatus ??= js_util.getProperty(glContext, 'COMPILE_STATUS');
dynamic get kArrayBuffer =>
_kArrayBuffer ??= js_util.getProperty(glContext, 'ARRAY_BUFFER');
dynamic get kLinkStatus =>
_kLinkStatus ??= js_util.getProperty(glContext, 'LINK_STATUS');
dynamic get kFloat => _kFloat ??= js_util.getProperty(glContext, 'FLOAT');
dynamic get kUnsignedByte =>
_kUnsignedByte ??= js_util.getProperty(glContext, 'UNSIGNED_BYTE');
dynamic get kStaticDraw =>
_kStaticDraw ??= js_util.getProperty(glContext, 'STATIC_DRAW');
dynamic get kTriangles =>
_kTriangles ??= js_util.getProperty(glContext, 'TRIANGLES');
dynamic get kTriangleFan =>
_kTriangles ??= js_util.getProperty(glContext, 'TRIANGLE_FAN');
dynamic get kTriangleStrip =>
_kTriangles ??= js_util.getProperty(glContext, 'TRIANGLE_STRIP');
dynamic get kColorBufferBit =>
_kColorBufferBit ??= js_util.getProperty(glContext, 'COLOR_BUFFER_BIT');
/// Returns reference to uniform in program.
Object getUniformLocation(Object program, String uniformName) {
return js_util
.callMethod(glContext, 'getUniformLocation', [program, uniformName]);
}
/// Sets vec2 uniform values.
void setUniform2f(Object uniform, double value1, double value2) {
return js_util
.callMethod(glContext, 'uniform2f', [uniform, value1, value2]);
}
/// Sets vec4 uniform values.
void setUniform4f(Object uniform, double value1, double value2, double value3,
double value4) {
return js_util.callMethod(
glContext, 'uniform4f', [uniform, value1, value2, value3, value4]);
}
/// Shader compile error log.
dynamic getShaderInfoLog(Object glShader) {
return js_util.callMethod(glContext, 'getShaderInfoLog', [glShader]);
}
/// Errors that occurred during failed linking or validation of program
/// objects. Typically called after [linkProgram].
String getProgramInfoLog(Object glProgram) {
return js_util.callMethod(glContext, 'getProgramInfoLog', [glProgram]);
}
}

View File

@ -28,6 +28,9 @@ Float32List _encodePointList(List<ui.Offset> points) {
class SkVertices implements ui.Vertices {
js.JsObject skVertices;
final Int32List _colors;
final Float32List _positions;
final ui.VertexMode _mode;
SkVertices(
ui.VertexMode mode,
@ -36,7 +39,10 @@ class SkVertices implements ui.Vertices {
List<ui.Color> colors,
List<int> indices,
}) : assert(mode != null),
assert(positions != null) {
assert(positions != null),
_colors = Int32List.fromList(colors.map((ui.Color c) => c.value)),
_positions = _offsetListToInt32List(positions),
_mode = mode {
if (textureCoordinates != null &&
textureCoordinates.length != positions.length)
throw ArgumentError(
@ -69,7 +75,10 @@ class SkVertices implements ui.Vertices {
Int32List colors,
Uint16List indices,
}) : assert(mode != null),
assert(positions != null) {
assert(positions != null),
_colors = colors,
_positions = positions,
_mode = mode {
if (textureCoordinates != null &&
textureCoordinates.length != positions.length)
throw ArgumentError(
@ -130,4 +139,26 @@ class SkVertices implements ui.Vertices {
}
return encodedPoints;
}
static Float32List _offsetListToInt32List(List<ui.Offset> offsetList) {
if (offsetList == null) {
return null;
}
final int length = offsetList.length;
final floatList = Float32List(length * 2);
for (int i = 0, destIndex = 0; i < length; i++, destIndex += 2) {
floatList[destIndex] = offsetList[i].dx;
floatList[destIndex + 1] = offsetList[i].dx;
}
return floatList;
}
@override
Int32List get colors => _colors;
@override
Float32List get positions => _positions;
@override
ui.VertexMode get mode => _mode;
}

View File

@ -173,4 +173,10 @@ class DomCanvas extends EngineCanvas with SaveElementStackTracking {
_drawParagraphElement(paragraph, offset, transform: currentTransform);
currentElement.append(paragraphElement);
}
@override
void drawVertices(ui.Vertices vertices, ui.BlendMode blendMode,
ui.PaintData paint) {
throw UnimplementedError();
}
}

View File

@ -65,6 +65,9 @@ abstract class EngineCanvas {
ui.Image image, ui.Rect src, ui.Rect dst, ui.PaintData paint);
void drawParagraph(EngineParagraph paragraph, ui.Offset offset);
void drawVertices(ui.Vertices vertices, ui.BlendMode blendMode,
ui.PaintData paint);
}
/// Adds an [offset] transformation to a [transform] matrix and returns the

View File

@ -227,6 +227,12 @@ class HoudiniCanvas extends EngineCanvas with SaveElementStackTracking {
_drawParagraphElement(paragraph, offset, transform: currentTransform);
currentElement.append(paragraphElement);
}
@override
void drawVertices(ui.Vertices vertices, ui.BlendMode blendMode,
ui.PaintData paint) {
// TODO(flutter_web): implement.
}
}
class _SaveElementStackEntry {

View File

@ -356,8 +356,30 @@ class RecordingCanvas {
_commands.add(PaintDrawShadow(path, color, elevation, transparentOccluder));
}
void drawVertices(ui.Vertices vertices, ui.BlendMode blendMode, ui.Paint paint) {
throw new UnimplementedError();
void drawVertices(ui.Vertices vertices, ui.BlendMode blendMode,
ui.Paint paint) {
_hasArbitraryPaint = true;
_didDraw = true;
final Float32List positions = vertices.positions;
assert(positions.length >= 2);
double minValueX, maxValueX, minValueY, maxValueY;
minValueX = maxValueX = positions[0];
minValueY = maxValueY = positions[1];
for (int i = 2, len = positions.length; i < len; i += 2) {
final double x = positions[i];
final double y = positions[i + 1];
if (x.isNaN || y.isNaN) {
// Follows skia implementation that sets bounds to empty
// and aborts.
return;
}
minValueX = math.min(minValueX, x);
maxValueX = math.max(maxValueX, x);
minValueY = math.min(minValueY, y);
maxValueY = math.max(maxValueY, y);
}
_paintBounds.growLTRB(minValueX, minValueY, maxValueX, maxValueY);
_commands.add(PaintVertices(vertices, blendMode, paint.webOnlyPaintData));
}
int saveCount = 1;
@ -715,6 +737,32 @@ class PaintDrawPaint extends PaintCommand {
}
}
class PaintVertices extends PaintCommand {
final ui.Vertices vertices;
final ui.BlendMode blendMode;
final ui.PaintData paint;
PaintVertices(this.vertices, this.blendMode, this.paint);
@override
void apply(EngineCanvas canvas) {
canvas.drawVertices(vertices, blendMode, paint);
}
@override
String toString() {
if (assertionsEnabled) {
return 'drawVertices($vertices, $blendMode, $paint)';
} else {
return super.toString();
}
}
@override
void serializeToCssPaint(List<List<dynamic>> serializedCommands) {
throw UnimplementedError();
}
}
class PaintDrawRect extends PaintCommand {
final ui.Rect rect;
final ui.PaintData paint;

View File

@ -59,37 +59,91 @@ enum VertexMode {
/// A set of vertex data used by [Canvas.drawVertices].
class Vertices {
final VertexMode _mode;
final Float32List _positions;
final Float32List _textureCoordinates;
final Int32List _colors;
final Uint16List _indices;
Vertices._(
VertexMode mode,
List<Offset> positions, {
List<Offset> textureCoordinates,
List<Color> colors,
List<int> indices,
}) : assert(mode != null),
assert(positions != null),
_mode = mode,
_colors = Int32List.fromList(colors.map((Color c) => c.value)),
_indices = Uint16List.fromList(indices),
_positions = _offsetListToInt32List(positions),
_textureCoordinates = _offsetListToInt32List(textureCoordinates);
factory Vertices(
VertexMode mode,
List<Offset> positions, {
List<Offset> textureCoordinates,
List<Color> colors,
List<int> indices,
}) {
VertexMode mode,
List<Offset> positions, {
List<Offset> textureCoordinates,
List<Color> colors,
List<int> indices,
}) {
if (engine.experimentalUseSkia) {
return engine.SkVertices(mode, positions,
textureCoordinates: textureCoordinates,
colors: colors,
indices: indices);
}
return null;
return Vertices._(mode, positions,
textureCoordinates: textureCoordinates,
colors: colors , indices: indices);
}
Vertices._raw(
VertexMode mode,
Float32List positions, {
Float32List textureCoordinates,
Int32List colors,
Uint16List indices,
}) : assert(mode != null),
assert(positions != null),
_mode = mode,
_positions = positions,
_textureCoordinates = textureCoordinates,
_colors = colors,
_indices = indices;
static Float32List _offsetListToInt32List(List<Offset> offsetList) {
if (offsetList == null) {
return null;
}
final int length = offsetList.length;
final floatList = Float32List(length * 2);
for (int i = 0, destIndex = 0; i < length; i++, destIndex += 2) {
floatList[destIndex] = offsetList[i].dx;
floatList[destIndex + 1] = offsetList[i].dx;
}
return floatList;
}
factory Vertices.raw(
VertexMode mode,
Float32List positions, {
Float32List textureCoordinates,
Int32List colors,
Uint16List indices,
}) {
VertexMode mode,
Float32List positions, {
Float32List textureCoordinates,
Int32List colors,
Uint16List indices,
}) {
if (engine.experimentalUseSkia) {
return engine.SkVertices.raw(mode, positions,
textureCoordinates: textureCoordinates,
colors: colors,
indices: indices);
}
return null;
return Vertices._raw(mode, positions,
textureCoordinates: textureCoordinates, colors: colors , indices: indices);
}
VertexMode get mode => _mode;
Int32List get colors => _colors;
Float32List get positions => _positions;
}
/// Records a [Picture] containing a sequence of graphical operations.

View File

@ -0,0 +1,172 @@
// 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.
import 'dart:html' as html;
import 'dart:typed_data';
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),
bool write = false}) async {
final EngineCanvas engineCanvas = BitmapCanvas(screenRect);
rc.apply(engineCanvas);
// 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);
// Set rate to 0.66% for webGL difference across platforms.
await matchGoldenFile('$fileName.png', region: region, write: write,
maxDiffRate: 0.66 / 100.0);
} finally {
// The page is reused across tests, so remove the element after taking the
// golden screenshot.
sceneElement.remove();
}
}
setUp(() async {
debugEmulateFlutterTesterEnvironment = true;
await webOnlyInitializePlatform();
webOnlyFontCollection.debugRegisterTestFonts();
await webOnlyFontCollection.ensureFontsLoaded();
});
Future<void> _testVertices(String fileName, Vertices vertices,
BlendMode blendMode,
Paint paint) async {
final RecordingCanvas rc =
RecordingCanvas(const Rect.fromLTRB(0, 0, 500, 500));
rc.drawVertices(vertices, blendMode, paint);
await _checkScreenshot(rc, fileName);
}
test('Should draw green hairline triangles when colors array is null.',
() async {
final Vertices vertices = Vertices.raw(VertexMode.triangles,
Float32List.fromList([
20.0, 20.0, 220.0, 10.0, 110.0, 220.0,
220.0, 320.0, 20.0, 310.0, 200.0, 420.0
]));
await _testVertices(
'draw_vertices_hairline_triangle',
vertices,
BlendMode.srcOver,
Paint()..color = Color.fromARGB(255, 0, 128, 0));
});
test('Should draw black hairline triangles when colors array is null'
' and Paint() has no color.',
() async {
final Int32List colors = Int32List.fromList(<int>[
0xFFFF0000, 0xFF00FF00, 0xFF0000FF,
0xFFFF0000, 0xFF00FF00, 0xFF0000FF,
0xFFFF0000, 0xFF00FF00, 0xFF0000FF,
0xFFFF0000, 0xFF00FF00, 0xFF0000FF]);
final Vertices vertices = Vertices.raw(VertexMode.triangles,
Float32List.fromList([
20.0, 20.0, 220.0, 10.0, 110.0, 220.0,
220.0, 320.0, 20.0, 310.0, 200.0, 420.0
]));
await _testVertices(
'draw_vertices_hairline_triangle_black',
vertices,
BlendMode.srcOver,
Paint());
});
test('Should draw hairline triangleFan.',
() async {
final Vertices vertices = Vertices.raw(VertexMode.triangleFan,
Float32List.fromList([
150.0, 150.0, 20.0, 10.0, 80.0, 20.0,
220.0, 15.0, 280.0, 30.0, 300.0, 420.0
]));
await _testVertices(
'draw_vertices_hairline_triangle_fan',
vertices,
BlendMode.srcOver,
Paint()..color = Color.fromARGB(255, 0, 128, 0));
});
test('Should draw hairline triangleStrip.',
() async {
final Vertices vertices = Vertices.raw(VertexMode.triangleStrip,
Float32List.fromList([
20.0, 20.0, 220.0, 10.0, 110.0, 220.0,
220.0, 320.0, 20.0, 310.0, 200.0, 420.0
]));
await _testVertices(
'draw_vertices_hairline_triangle_strip',
vertices,
BlendMode.srcOver,
Paint()..color = Color.fromARGB(255, 0, 128, 0));
});
test('Should draw triangles with colors.',
() async {
final Int32List colors = Int32List.fromList(<int>[
0xFFFF0000, 0xFF00FF00, 0xFF0000FF,
0xFFFF0000, 0xFF00FF00, 0xFF0000FF]);
final Vertices vertices = Vertices.raw(VertexMode.triangles,
Float32List.fromList([
150.0, 150.0, 20.0, 10.0, 80.0, 20.0,
220.0, 15.0, 280.0, 30.0, 300.0, 420.0
]), colors: colors);
await _testVertices(
'draw_vertices_triangles',
vertices,
BlendMode.srcOver,
Paint()..color = Color.fromARGB(255, 0, 128, 0));
});
test('Should draw triangleFan with colors.',
() async {
final Int32List colors = Int32List.fromList(<int>[
0xFFFF0000, 0xFF00FF00, 0xFF0000FF,
0xFFFF0000, 0xFF00FF00, 0xFF0000FF]);
final Vertices vertices = Vertices.raw(VertexMode.triangleFan,
Float32List.fromList([
150.0, 150.0, 20.0, 10.0, 80.0, 20.0,
220.0, 15.0, 280.0, 30.0, 300.0, 420.0
]), colors: colors);
await _testVertices(
'draw_vertices_triangle_fan',
vertices,
BlendMode.srcOver,
Paint()..color = Color.fromARGB(255, 0, 128, 0));
});
test('Should draw triangleStrip with colors.',
() async {
final Int32List colors = Int32List.fromList(<int>[
0xFFFF0000, 0xFF00FF00, 0xFF0000FF,
0xFFFF0000, 0xFF00FF00, 0xFF0000FF]);
final Vertices vertices = Vertices.raw(VertexMode.triangleStrip,
Float32List.fromList([
20.0, 20.0, 220.0, 10.0, 110.0, 220.0,
220.0, 320.0, 20.0, 310.0, 200.0, 420.0
]), colors: colors);
await _testVertices(
'draw_vertices_triangle_strip',
vertices,
BlendMode.srcOver,
Paint()..color = Color.fromARGB(255, 0, 128, 0));
});
}

View File

@ -218,4 +218,14 @@ class MockEngineCanvas implements EngineCanvas {
'offset': offset,
});
}
@override
void drawVertices(Vertices vertices, BlendMode blendMode,
PaintData paint) {
_called('drawVertices', arguments: <String, dynamic>{
'vertices': vertices,
'blendMode': blendMode,
'paint': paint,
});
}
}

View File

@ -21,12 +21,24 @@ Future<dynamic> _callScreenshotServer(dynamic requestData) async {
}
/// Attempts to match the current browser state with the screenshot [filename].
Future<void> matchGoldenFile(String filename, { bool write = false, Rect region = null }) async {
final String response = await _callScreenshotServer(<String, dynamic>{
Future<void> matchGoldenFile(String filename,
{bool write = false, Rect region = null, double maxDiffRate = null}) async {
Map<String, dynamic> serverParams = <String, dynamic>{
'filename': filename,
'write': write,
'region': region == null ? null : {'x': region.left, 'y': region.top, 'width': region.width, 'height': region.height},
});
'region': region == null
? null
: {
'x': region.left,
'y': region.top,
'width': region.width,
'height': region.height
}
};
if (maxDiffRate != null) {
serverParams['maxdiffrate'] = maxDiffRate;
}
final String response = await _callScreenshotServer(serverParams);
if (response == 'OK') {
// Pass
return;