diff --git a/lib/web_ui/dev/goldens_lock.yaml b/lib/web_ui/dev/goldens_lock.yaml index eb438eef2a3bb..fa2c3c0f72424 100644 --- a/lib/web_ui/dev/goldens_lock.yaml +++ b/lib/web_ui/dev/goldens_lock.yaml @@ -1,2 +1,2 @@ repository: https://github.com/flutter/goldens.git -revision: 7efcec3e8b0bbb6748a992b23a0a89300aa323c7 \ No newline at end of file +revision: 686dd320f6cce6da9a7a43e3ec9c0147f39eb19d \ No newline at end of file diff --git a/lib/web_ui/dev/test_platform.dart b/lib/web_ui/dev/test_platform.dart index 8426676be88c3..3ad153e4b8101 100644 --- a/lib/web_ui/dev/test_platform.dart +++ b/lib/web_ui/dev/test_platform.dart @@ -146,12 +146,13 @@ class BrowserPlatform extends PlatformPlugin { final Map requestData = json.decode(payload); final String filename = requestData['filename']; final bool write = requestData['write']; + final double maxDiffRate = requestData['maxdiffrate']; final Map 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 _diffScreenshot(String filename, bool write, [ Map region ]) async { + Future _diffScreenshot(String filename, bool write, double maxDiffRateFailure, [ Map 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); diff --git a/lib/web_ui/lib/src/engine/bitmap_canvas.dart b/lib/web_ui/lib/src/engine/bitmap_canvas.dart index 6beddc120c50e..50258b6ae7967 100644 --- a/lib/web_ui/lib/src/engine/bitmap_canvas.dart +++ b/lib/web_ui/lib/src/engine/bitmap_canvas.dart @@ -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]); + } +} diff --git a/lib/web_ui/lib/src/engine/compositor/vertices.dart b/lib/web_ui/lib/src/engine/compositor/vertices.dart index 9644bfbef25eb..6b8490844e11b 100644 --- a/lib/web_ui/lib/src/engine/compositor/vertices.dart +++ b/lib/web_ui/lib/src/engine/compositor/vertices.dart @@ -28,6 +28,9 @@ Float32List _encodePointList(List 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 colors, List 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 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; } diff --git a/lib/web_ui/lib/src/engine/dom_canvas.dart b/lib/web_ui/lib/src/engine/dom_canvas.dart index 93df70a24b5f9..5d207f5a28b39 100644 --- a/lib/web_ui/lib/src/engine/dom_canvas.dart +++ b/lib/web_ui/lib/src/engine/dom_canvas.dart @@ -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(); + } } diff --git a/lib/web_ui/lib/src/engine/engine_canvas.dart b/lib/web_ui/lib/src/engine/engine_canvas.dart index 66854a3d597cf..e7c37ca0a79a6 100644 --- a/lib/web_ui/lib/src/engine/engine_canvas.dart +++ b/lib/web_ui/lib/src/engine/engine_canvas.dart @@ -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 diff --git a/lib/web_ui/lib/src/engine/houdini_canvas.dart b/lib/web_ui/lib/src/engine/houdini_canvas.dart index 53e3fd9275f88..2eb83b9c76269 100644 --- a/lib/web_ui/lib/src/engine/houdini_canvas.dart +++ b/lib/web_ui/lib/src/engine/houdini_canvas.dart @@ -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 { diff --git a/lib/web_ui/lib/src/engine/recording_canvas.dart b/lib/web_ui/lib/src/engine/recording_canvas.dart index 5fcc806b10c6b..fe6816d1e2f86 100644 --- a/lib/web_ui/lib/src/engine/recording_canvas.dart +++ b/lib/web_ui/lib/src/engine/recording_canvas.dart @@ -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> serializedCommands) { + throw UnimplementedError(); + } +} + class PaintDrawRect extends PaintCommand { final ui.Rect rect; final ui.PaintData paint; diff --git a/lib/web_ui/lib/src/ui/canvas.dart b/lib/web_ui/lib/src/ui/canvas.dart index 283757d975011..2a9e99361631b 100644 --- a/lib/web_ui/lib/src/ui/canvas.dart +++ b/lib/web_ui/lib/src/ui/canvas.dart @@ -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 positions, { + List textureCoordinates, + List colors, + List 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 positions, { - List textureCoordinates, - List colors, - List indices, - }) { + VertexMode mode, + List positions, { + List textureCoordinates, + List colors, + List 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 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. diff --git a/lib/web_ui/test/golden_tests/engine/draw_vertices_golden_test.dart b/lib/web_ui/test/golden_tests/engine/draw_vertices_golden_test.dart new file mode 100644 index 0000000000000..64edabfee6f24 --- /dev/null +++ b/lib/web_ui/test/golden_tests/engine/draw_vertices_golden_test.dart @@ -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 _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 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 _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([ + 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([ + 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([ + 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([ + 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)); + }); +} diff --git a/lib/web_ui/test/mock_engine_canvas.dart b/lib/web_ui/test/mock_engine_canvas.dart index 2cf562f4fd2e9..d5e8fade6d275 100644 --- a/lib/web_ui/test/mock_engine_canvas.dart +++ b/lib/web_ui/test/mock_engine_canvas.dart @@ -218,4 +218,14 @@ class MockEngineCanvas implements EngineCanvas { 'offset': offset, }); } + + @override + void drawVertices(Vertices vertices, BlendMode blendMode, + PaintData paint) { + _called('drawVertices', arguments: { + 'vertices': vertices, + 'blendMode': blendMode, + 'paint': paint, + }); + } } diff --git a/web_sdk/web_engine_tester/lib/golden_tester.dart b/web_sdk/web_engine_tester/lib/golden_tester.dart index 369a6270b83dd..1280fa4c64ae3 100644 --- a/web_sdk/web_engine_tester/lib/golden_tester.dart +++ b/web_sdk/web_engine_tester/lib/golden_tester.dart @@ -21,12 +21,24 @@ Future _callScreenshotServer(dynamic requestData) async { } /// Attempts to match the current browser state with the screenshot [filename]. -Future matchGoldenFile(String filename, { bool write = false, Rect region = null }) async { - final String response = await _callScreenshotServer({ +Future matchGoldenFile(String filename, + {bool write = false, Rect region = null, double maxDiffRate = null}) async { + Map serverParams = { '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;