mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
[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:
parent
c643366c19
commit
3fd877715b
@ -1,2 +1,2 @@
|
||||
repository: https://github.com/flutter/goldens.git
|
||||
revision: 7efcec3e8b0bbb6748a992b23a0a89300aa323c7
|
||||
revision: 686dd320f6cce6da9a7a43e3ec9c0147f39eb19d
|
||||
@ -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);
|
||||
|
||||
@ -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]);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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));
|
||||
});
|
||||
}
|
||||
@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user