diff --git a/ci/licenses_golden/licenses_flutter b/ci/licenses_golden/licenses_flutter index b1018822b05e9..3dca5f1ffbf84 100755 --- a/ci/licenses_golden/licenses_flutter +++ b/ci/licenses_golden/licenses_flutter @@ -489,6 +489,7 @@ FILE: ../../../flutter/lib/web_ui/lib/src/engine/html/recording_canvas.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/html/render_vertices.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/html/scene.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/html/scene_builder.dart +FILE: ../../../flutter/lib/web_ui/lib/src/engine/html/shaders/normalized_gradient.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/html/shaders/shader.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/html/shaders/shader_builder.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/html/surface.dart diff --git a/lib/web_ui/lib/src/engine.dart b/lib/web_ui/lib/src/engine.dart index d8b372ba5387a..8799a62ea4b19 100644 --- a/lib/web_ui/lib/src/engine.dart +++ b/lib/web_ui/lib/src/engine.dart @@ -89,6 +89,7 @@ part 'engine/html/recording_canvas.dart'; part 'engine/html/render_vertices.dart'; part 'engine/html/scene.dart'; part 'engine/html/scene_builder.dart'; +part 'engine/html/shaders/normalized_gradient.dart'; part 'engine/html/shaders/shader.dart'; part 'engine/html/shaders/shader_builder.dart'; part 'engine/html/surface.dart'; diff --git a/lib/web_ui/lib/src/engine/bitmap_canvas.dart b/lib/web_ui/lib/src/engine/bitmap_canvas.dart index afe4a6e07844a..557106a26412a 100644 --- a/lib/web_ui/lib/src/engine/bitmap_canvas.dart +++ b/lib/web_ui/lib/src/engine/bitmap_canvas.dart @@ -227,8 +227,8 @@ class BitmapCanvas extends EngineCanvas { } /// Sets the global paint styles to correspond to [paint]. - void _setUpPaint(SurfacePaintData paint) { - _canvasPool.contextHandle.setUpPaint(paint); + void _setUpPaint(SurfacePaintData paint, ui.Rect? shaderBounds) { + _canvasPool.contextHandle.setUpPaint(paint, shaderBounds); } void _tearDownPaint() { @@ -352,7 +352,9 @@ class BitmapCanvas extends EngineCanvas { ..lineTo(p2.dx, p2.dy); drawPath(path, paint); } else { - _setUpPaint(paint); + ui.Rect? shaderBounds = (paint.shader != null) ? + ui.Rect.fromPoints(p1, p2) : null; + _setUpPaint(paint, shaderBounds); _canvasPool.strokeLine(p1, p2); _tearDownPaint(); } @@ -363,7 +365,9 @@ class BitmapCanvas extends EngineCanvas { if (_useDomForRendering(paint)) { drawRect(_computeScreenBounds(_canvasPool._currentTransform), paint); } else { - _setUpPaint(paint); + ui.Rect? shaderBounds = (paint.shader != null) ? + _computePictureBounds() : null; + _setUpPaint(paint, shaderBounds); _canvasPool.fill(); _tearDownPaint(); } @@ -380,7 +384,7 @@ class BitmapCanvas extends EngineCanvas { math.min(rect.left, rect.right), math.min(rect.top, rect.bottom)), paint); } else { - _setUpPaint(paint); + _setUpPaint(paint, rect); _canvasPool.drawRect(rect, paint.style); _tearDownPaint(); } @@ -423,15 +427,15 @@ class BitmapCanvas extends EngineCanvas { math.min(rect.left, rect.right), math.min(rect.top, rect.bottom)), paint); } else { - _setUpPaint(paint); - _canvasPool.drawRRect(rrect, paint.style); + _setUpPaint(paint, rrect.outerRect); + _canvasPool.drawRRect(rrect, paint.style); _tearDownPaint(); } } @override void drawDRRect(ui.RRect outer, ui.RRect inner, SurfacePaintData paint) { - _setUpPaint(paint); + _setUpPaint(paint, outer.outerRect); _canvasPool.drawDRRect(outer, inner, paint.style); _tearDownPaint(); } @@ -449,7 +453,7 @@ class BitmapCanvas extends EngineCanvas { element.style.borderRadius = '${(rect.width / 2.0)}px / ${(rect.height / 2.0)}px'; } else { - _setUpPaint(paint); + _setUpPaint(paint, rect); _canvasPool.drawOval(rect, paint.style); _tearDownPaint(); } @@ -468,7 +472,8 @@ class BitmapCanvas extends EngineCanvas { paint); element.style.borderRadius = '50%'; } else { - _setUpPaint(paint); + _setUpPaint(paint, paint.shader != null + ? ui.Rect.fromCircle(center: c, radius: radius) : null); _canvasPool.drawCircle(c, radius, paint.style); _tearDownPaint(); } @@ -490,7 +495,7 @@ class BitmapCanvas extends EngineCanvas { } _drawElement(svgElm, ui.Offset(0, 0), paint); } else { - _setUpPaint(paint); + _setUpPaint(paint, paint.shader != null ? path.getBounds() : null); _canvasPool.drawPath(path, paint.style); _tearDownPaint(); } @@ -813,7 +818,7 @@ class BitmapCanvas extends EngineCanvas { ctx.font = style.cssFontString; _cachedLastStyle = style; } - _setUpPaint(paragraph._paint!.paintData); + _setUpPaint(paragraph._paint!.paintData, null); double y = offset.dy + paragraph.alphabeticBaseline; final int len = lines.length; for (int i = 0; i < len; i++) { @@ -920,7 +925,7 @@ class BitmapCanvas extends EngineCanvas { _drawPointsPaint.strokeWidth = paint.strokeWidth; _drawPointsPaint.maskFilter = paint.maskFilter; - _setUpPaint(_drawPointsPaint); + _setUpPaint(_drawPointsPaint, null); _canvasPool.drawPoints(pointMode, points, paint.strokeWidth! / 2.0); _tearDownPaint(); } @@ -969,6 +974,11 @@ class BitmapCanvas extends EngineCanvas { math.max(topRight.y, math.max(bottomRight.y, bottomLeft.y))), ); } + + /// Computes paint bounds to completely cover picture. + ui.Rect _computePictureBounds() { + return ui.Rect.fromLTRB(0, 0, _bounds.width, _bounds.height); + } } String? _stringForBlendMode(ui.BlendMode? blendMode) { diff --git a/lib/web_ui/lib/src/engine/canvas_pool.dart b/lib/web_ui/lib/src/engine/canvas_pool.dart index a645fc2aaaa55..054ac12953acf 100644 --- a/lib/web_ui/lib/src/engine/canvas_pool.dart +++ b/lib/web_ui/lib/src/engine/canvas_pool.dart @@ -763,7 +763,7 @@ class ContextStateHandle { /// Sets paint properties on the current canvas. /// /// [tearDownPaint] must be called after calling this method. - void setUpPaint(SurfacePaintData paint) { + void setUpPaint(SurfacePaintData paint, ui.Rect? shaderBounds) { if (assertionsEnabled) { assert(!_debugIsPaintSetUp); _debugIsPaintSetUp = true; @@ -778,7 +778,7 @@ class ContextStateHandle { if (paint.shader != null) { final EngineGradient engineShader = paint.shader as EngineGradient; final Object paintStyle = - engineShader.createPaintStyle(_canvasPool.context); + engineShader.createPaintStyle(_canvasPool.context, shaderBounds); fillStyle = paintStyle; strokeStyle = paintStyle; } else if (paint.color != null) { diff --git a/lib/web_ui/lib/src/engine/html/render_vertices.dart b/lib/web_ui/lib/src/engine/html/render_vertices.dart index 0720dd4a2a864..c0d564bca1e76 100644 --- a/lib/web_ui/lib/src/engine/html/render_vertices.dart +++ b/lib/web_ui/lib/src/engine/html/render_vertices.dart @@ -55,7 +55,7 @@ void initWebGl() { } void disposeWebGl() { - _OffscreenCanvas.dispose(); + _GlContextCache.dispose(); _glRenderer = null; } @@ -69,6 +69,9 @@ abstract class _GlRenderer { ui.BlendMode blendMode, SurfacePaintData paint); + Object? drawRect(ui.Rect targetRect, _GlContext gl, _GlProgram glProgram, + NormalizedGradient gradient, int widthInPixels, int heightInPixels); + void drawHairline(html.CanvasRenderingContext2D? _ctx, Float32List positions); } @@ -77,6 +80,9 @@ abstract class _GlRenderer { /// This class gets instantiated on demand by Vertices constructor. For apps /// that don't use Vertices WebGlRenderer will be removed from release binary. class _WebGlRenderer implements _GlRenderer { + + /// Cached vertex shader reused by [drawVertices] and gradients. + static String? _baseVertexShader; @override void drawVertices( html.CanvasRenderingContext2D? context, @@ -116,13 +122,13 @@ class _WebGlRenderer implements _GlRenderer { if (widthInPixels == 0 || heightInPixels == 0) { return; } - final String vertexShader = _writeVerticesVertexShader(); + final String vertexShader = writeBaseVertexShader(); final String fragmentShader = _writeVerticesFragmentShader(); - _GlContext gl = - _OffscreenCanvas.createGlContext(widthInPixels, heightInPixels)!; + _GlContext gl = _GlContextCache.createGlContext(widthInPixels, heightInPixels)!; + _GlProgram glProgram = gl.useAndCacheProgram(vertexShader, fragmentShader)!; - Object? transformUniform = gl.getUniformLocation(glProgram.program, + Object transformUniform = gl.getUniformLocation(glProgram.program, 'u_ctransform'); Matrix4 transformAtOffset = transform.clone() ..translate(-offsetX, -offsetY); @@ -130,10 +136,10 @@ class _WebGlRenderer implements _GlRenderer { // Set uniform to scale 0..width/height pixels coordinates to -1..1 // clipspace range and flip the Y axis. - Object? resolution = gl.getUniformLocation(glProgram.program, 'u_scale'); + Object resolution = gl.getUniformLocation(glProgram.program, 'u_scale'); gl.setUniform4f(resolution, 2.0 / widthInPixels.toDouble(), -2.0 / heightInPixels.toDouble(), 1, 1); - Object? shift = gl.getUniformLocation(glProgram.program, 'u_shift'); + Object shift = gl.getUniformLocation(glProgram.program, 'u_shift'); gl.setUniform4f(shift, -1, 1, 0, 0); // Setup geometry. @@ -141,9 +147,9 @@ class _WebGlRenderer implements _GlRenderer { assert(positionsBuffer != null); // ignore: unnecessary_null_comparison gl.bindArrayBuffer(positionsBuffer); gl.bufferData(positions, gl.kStaticDraw); - Object? positionLoc = gl.getAttribLocation(glProgram.program, 'position'); + Object? positionLoc = gl.getAttributeLocation(glProgram.program, 'position'); js_util.callMethod( - gl.glContext!, 'vertexAttribPointer', [ + gl.glContext, 'vertexAttribPointer', [ positionLoc, 2, gl.kFloat, false, 0, 0, ]); gl.enableVertexAttribArray(0); @@ -153,8 +159,8 @@ class _WebGlRenderer implements _GlRenderer { gl.bindArrayBuffer(colorsBuffer); // Buffer kBGRA_8888. gl.bufferData(vertices._colors, gl.kStaticDraw); - Object? colorLoc = gl.getAttribLocation(glProgram.program, 'color'); - js_util.callMethod(gl.glContext!, 'vertexAttribPointer', + Object colorLoc = gl.getAttributeLocation(glProgram.program, 'color'); + js_util.callMethod(gl.glContext, 'vertexAttribPointer', [colorLoc, 4, gl.kUnsignedByte, true, 0, 0]); gl.enableVertexAttribArray(1); gl.clear(); @@ -167,7 +173,90 @@ class _WebGlRenderer implements _GlRenderer { context.restore(); } - /// Vertex shader transforms pixel space [Vertices.positions] to + static final Uint16List _vertexIndicesForRect = Uint16List.fromList( + [ + 0, 1, 2, 2, 3, 0 + ] + ); + + /// Renders a rectangle using given program into an image resource. + /// + /// Browsers that support OffscreenCanvas and the transferToImageBitmap api + /// will return ImageBitmap, otherwise will return CanvasElement. + Object? drawRect(ui.Rect targetRect, _GlContext gl, _GlProgram glProgram, + NormalizedGradient gradient, int widthInPixels, int heightInPixels) { + // Setup rectangle coordinates. + final double left = targetRect.left; + final double top = targetRect.top; + final double right = targetRect.right; + final double bottom = targetRect.bottom; + // Form 2 triangles for rectangle. + final Float32List vertices = Float32List(8); + vertices[0] = left; + vertices[1] = top; + vertices[2] = right; + vertices[3] = top; + vertices[4] = right; + vertices[5] = bottom; + vertices[6] = left; + vertices[7] = bottom; + + Object transformUniform = gl.getUniformLocation( + glProgram.program, 'u_ctransform'); + gl.setUniformMatrix4fv(transformUniform, false, Matrix4.identity().storage); + + // Set uniform to scale 0..width/height pixels coordinates to -1..1 + // clipspace range and flip the Y axis. + Object resolution = gl.getUniformLocation(glProgram.program, 'u_scale'); + gl.setUniform4f(resolution, 2.0 / widthInPixels.toDouble(), + -2.0 / heightInPixels.toDouble(), 1, 1); + Object shift = gl.getUniformLocation(glProgram.program, 'u_shift'); + gl.setUniform4f(shift, -1, 1, 0, 0); + + // Setup geometry. + Object positionsBuffer = gl.createBuffer()!; + assert(positionsBuffer != null); // ignore: unnecessary_null_comparison + gl.bindArrayBuffer(positionsBuffer); + gl.bufferData(vertices, gl.kStaticDraw); + // Point an attribute to the currently bound vertex buffer object. + 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. + final Int32List colors = Int32List.fromList([ + 0xFF00FF00, 0xFF0000FF, 0xFFFFFF00, 0xFF00FFFF, + ]); + gl.bufferData(colors, gl.kStaticDraw); + js_util.callMethod(gl.glContext, 'vertexAttribPointer', + [1, 4, gl.kUnsignedByte, true, 0, 0]); + gl.enableVertexAttribArray(1); + + Object? indexBuffer = gl.createBuffer(); + gl.bindElementArrayBuffer(indexBuffer); + gl.bufferElementData(_vertexIndicesForRect, gl.kStaticDraw); + + Object uRes = gl.getUniformLocation(glProgram.program, 'u_resolution'); + gl.setUniform2f(uRes, widthInPixels.toDouble(), heightInPixels.toDouble()); + + gl.clear(); + gl.viewport(0, 0, widthInPixels.toDouble(), heightInPixels.toDouble()); + + gl.drawElements(gl.kTriangles, _vertexIndicesForRect.length, gl.kUnsignedShort); + + Object? image = gl.readPatternData(); + + gl.bindArrayBuffer(null); + gl.bindElementArrayBuffer(null); + + return image; + } + + /// Creates a vertex shader transforms pixel space [Vertices.positions] to /// final clipSpace -1..1 coordinates with inverted Y Axis. /// #version 300 es /// layout (location=0) in vec4 position; @@ -180,18 +269,22 @@ class _WebGlRenderer implements _GlRenderer { /// gl_Position = ((u_ctransform * position) * u_scale) + u_shift; /// v_color = color.zyxw; /// } - String _writeVerticesVertexShader() { - ShaderBuilder builder = ShaderBuilder(webGLVersion); - builder.addIn(ShaderType.kVec4, name: 'position'); - builder.addIn(ShaderType.kVec4, name: 'color'); - builder.addUniform(ShaderType.kMat4, name: 'u_ctransform'); - builder.addUniform(ShaderType.kVec4, name: 'u_scale'); - builder.addUniform(ShaderType.kVec4, name: 'u_shift'); - builder.addOut(ShaderType.kVec4, name: 'v_color'); - ShaderMethod method = builder.addMethod('main'); - method.addStatement('gl_Position = ((u_ctransform * position) * u_scale) + u_shift;'); - method.addStatement('v_color = color.zyxw;'); - return builder.build(); + static String writeBaseVertexShader() { + if (_baseVertexShader == null) { + ShaderBuilder builder = ShaderBuilder(webGLVersion); + builder.addIn(ShaderType.kVec4, name: 'position'); + builder.addIn(ShaderType.kVec4, name: 'color'); + builder.addUniform(ShaderType.kMat4, name: 'u_ctransform'); + builder.addUniform(ShaderType.kVec4, name: 'u_scale'); + builder.addUniform(ShaderType.kVec4, name: 'u_shift'); + builder.addOut(ShaderType.kVec4, name: 'v_color'); + ShaderMethod method = builder.addMethod('main'); + method.addStatement( + 'gl_Position = ((u_ctransform * position) * u_scale) + u_shift;'); + method.addStatement('v_color = color.zyxw;'); + _baseVertexShader = builder.build(); + } + return _baseVertexShader!; } /// This fragment shader enables Int32List of colors to be passed directly @@ -339,30 +432,33 @@ Float32List _convertVertexPositions(ui.VertexMode mode, Float32List positions) { /// Compiled and cached gl program. class _GlProgram { - final Object? program; + final Object program; _GlProgram(this.program); } /// JS Interop helper for webgl apis. class _GlContext { - final Object? glContext; + final Object glContext; final bool isOffscreen; dynamic _kCompileStatus; dynamic _kArrayBuffer; + dynamic _kElementArrayBuffer; dynamic _kStaticDraw; dynamic _kFloat; dynamic _kColorBufferBit; dynamic _kTriangles; dynamic _kLinkStatus; dynamic _kUnsignedByte; + dynamic _kUnsignedShort; dynamic _kRGBA; + Object? _canvas; int? _widthInPixels; int? _heightInPixels; static late Map _programCache; _GlContext.fromOffscreenCanvas(html.OffscreenCanvas canvas) - : glContext = canvas.getContext('webgl2', {'premultipliedAlpha': false}), + : glContext = canvas.getContext('webgl2', {'premultipliedAlpha': false})!, isOffscreen = true { _programCache = {}; _canvas = canvas; @@ -370,7 +466,7 @@ class _GlContext { _GlContext.fromCanvas(html.CanvasElement canvas, bool useWebGl1) : glContext = canvas.getContext(useWebGl1 ? 'webgl' : 'webgl2', - {'premultipliedAlpha': false}), + {'premultipliedAlpha': false})!, isOffscreen = false { _programCache = {}; _canvas = canvas; @@ -388,7 +484,7 @@ class _GlContext { // source/destination to draw part of the image data. js_util.callMethod(context, 'drawImage', [_canvas, 0, 0, _widthInPixels, _heightInPixels, - left, top, _widthInPixels, _heightInPixels]); + left, top, _widthInPixels, _heightInPixels]); } _GlProgram? useAndCacheProgram( @@ -399,9 +495,9 @@ class _GlContext { // Create and compile shaders. Object vertexShader = compileShader('VERTEX_SHADER', vertexShaderSource); Object fragmentShader = - compileShader('FRAGMENT_SHADER', fragmentShaderSource); + compileShader('FRAGMENT_SHADER', fragmentShaderSource); // Create a gl program and link shaders. - Object? program = createProgram(); + Object program = createProgram(); attachShader(program, vertexShader); attachShader(program, fragmentShader); linkProgram(program); @@ -417,57 +513,65 @@ class _GlContext { if (shader == null) { throw Exception(error); } - js_util.callMethod(glContext!, 'shaderSource', [shader, source]); - js_util.callMethod(glContext!, 'compileShader', [shader]); + js_util.callMethod(glContext, 'shaderSource', [shader, source]); + js_util.callMethod(glContext, 'compileShader', [shader]); bool shaderStatus = js_util - .callMethod(glContext!, 'getShaderParameter', [shader, compileStatus]); + .callMethod(glContext, 'getShaderParameter', [shader, compileStatus]); if (!shaderStatus) { throw Exception('Shader compilation failed: ${getShaderInfoLog(shader)}'); } return shader; } - Object? createProgram() => - js_util.callMethod(glContext!, 'createProgram', const []); + Object createProgram() => + js_util.callMethod(glContext, 'createProgram', const [])!; void attachShader(Object? program, Object shader) { - js_util.callMethod(glContext!, 'attachShader', [program, shader]); + js_util.callMethod(glContext, 'attachShader', [program, shader]); } - void linkProgram(Object? program) { - js_util.callMethod(glContext!, 'linkProgram', [program]); + void linkProgram(Object program) { + js_util.callMethod(glContext, 'linkProgram', [program]); if (!js_util - .callMethod(glContext!, 'getProgramParameter', [program, kLinkStatus])) { + .callMethod(glContext, 'getProgramParameter', [program, kLinkStatus])) { throw Exception(getProgramInfoLog(program)); } } void useProgram(Object? program) { - js_util.callMethod(glContext!, 'useProgram', [program]); + js_util.callMethod(glContext, 'useProgram', [program]); } Object? createBuffer() => - js_util.callMethod(glContext!, 'createBuffer', const []); + js_util.callMethod(glContext, 'createBuffer', const []); void bindArrayBuffer(Object? buffer) { - js_util.callMethod(glContext!, 'bindBuffer', [kArrayBuffer, buffer]); + js_util.callMethod(glContext, 'bindBuffer', [kArrayBuffer, buffer]); + } + + void bindElementArrayBuffer(Object? buffer) { + js_util.callMethod(glContext, 'bindBuffer', [kElementArrayBuffer, buffer]); } void deleteBuffer(Object buffer) { - js_util.callMethod(glContext!, 'deleteBuffer', [buffer]); + js_util.callMethod(glContext, 'deleteBuffer', [buffer]); } void bufferData(TypedData? data, dynamic type) { - js_util.callMethod(glContext!, 'bufferData', [kArrayBuffer, data, type]); + js_util.callMethod(glContext, 'bufferData', [kArrayBuffer, data, type]); + } + + void bufferElementData(TypedData? data, dynamic type) { + js_util.callMethod(glContext, 'bufferData', [kElementArrayBuffer, data, type]); } void enableVertexAttribArray(int index) { - js_util.callMethod(glContext!, 'enableVertexAttribArray', [index]); + js_util.callMethod(glContext, 'enableVertexAttribArray', [index]); } /// Clear background. void clear() { - js_util.callMethod(glContext!, 'clear', [kColorBufferBit]); + js_util.callMethod(glContext, 'clear', [kColorBufferBit]); } /// Destroys gl context. @@ -476,25 +580,29 @@ class _GlContext { } void deleteProgram(Object program) { - js_util.callMethod(glContext!, 'deleteProgram', [program]); + js_util.callMethod(glContext, 'deleteProgram', [program]); } void deleteShader(Object shader) { - js_util.callMethod(glContext!, 'deleteShader', [shader]); + js_util.callMethod(glContext, 'deleteShader', [shader]); } dynamic _getExtension(String extensionName) => - js_util.callMethod(glContext!, 'getExtension', [extensionName]); + js_util.callMethod(glContext, 'getExtension', [extensionName]); void drawTriangles(int triangleCount, ui.VertexMode vertexMode) { dynamic mode = _triangleTypeFromMode(vertexMode); - js_util.callMethod(glContext!, 'drawArrays', [mode, 0, triangleCount]); + js_util.callMethod(glContext, 'drawArrays', [mode, 0, triangleCount]); + } + + void drawElements(dynamic type, int indexCount, dynamic indexType) { + js_util.callMethod(glContext, 'drawElements', [type, indexCount, indexType, 0]); } /// 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]); + js_util.callMethod(glContext, 'viewport', [x, y, width, height]); } dynamic _triangleTypeFromMode(ui.VertexMode mode) { @@ -509,183 +617,225 @@ class _GlContext { } Object? _createShader(String shaderType) => js_util.callMethod( - glContext!, 'createShader', [js_util.getProperty(glContext!, shaderType)]); + glContext, 'createShader', [js_util.getProperty(glContext, shaderType)]); /// Error state of gl context. - dynamic get error => js_util.callMethod(glContext!, 'getError', const []); + 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'); + _kCompileStatus ??= js_util.getProperty(glContext, 'COMPILE_STATUS'); dynamic get kArrayBuffer => - _kArrayBuffer ??= js_util.getProperty(glContext!, 'ARRAY_BUFFER'); + _kArrayBuffer ??= js_util.getProperty(glContext, 'ARRAY_BUFFER'); + + dynamic get kElementArrayBuffer => + _kElementArrayBuffer ??= js_util.getProperty(glContext, + 'ELEMENT_ARRAY_BUFFER'); dynamic get kLinkStatus => - _kLinkStatus ??= js_util.getProperty(glContext!, 'LINK_STATUS'); + _kLinkStatus ??= js_util.getProperty(glContext, 'LINK_STATUS'); - dynamic get kFloat => _kFloat ??= js_util.getProperty(glContext!, 'FLOAT'); + dynamic get kFloat => _kFloat ??= js_util.getProperty(glContext, 'FLOAT'); - dynamic get kRGBA => _kRGBA ??= js_util.getProperty(glContext!, 'RGBA'); + dynamic get kRGBA => _kRGBA ??= js_util.getProperty(glContext, 'RGBA'); dynamic get kUnsignedByte => - _kUnsignedByte ??= js_util.getProperty(glContext!, 'UNSIGNED_BYTE'); + _kUnsignedByte ??= js_util.getProperty(glContext, 'UNSIGNED_BYTE'); + + dynamic get kUnsignedShort => + _kUnsignedShort ??= js_util.getProperty(glContext, 'UNSIGNED_SHORT'); dynamic get kStaticDraw => - _kStaticDraw ??= js_util.getProperty(glContext!, 'STATIC_DRAW'); + _kStaticDraw ??= js_util.getProperty(glContext, 'STATIC_DRAW'); dynamic get kTriangles => - _kTriangles ??= js_util.getProperty(glContext!, 'TRIANGLES'); + _kTriangles ??= js_util.getProperty(glContext, 'TRIANGLES'); dynamic get kTriangleFan => - _kTriangles ??= js_util.getProperty(glContext!, 'TRIANGLE_FAN'); + _kTriangles ??= js_util.getProperty(glContext, 'TRIANGLE_FAN'); dynamic get kTriangleStrip => - _kTriangles ??= js_util.getProperty(glContext!, 'TRIANGLE_STRIP'); + _kTriangles ??= js_util.getProperty(glContext, 'TRIANGLE_STRIP'); dynamic get kColorBufferBit => - _kColorBufferBit ??= js_util.getProperty(glContext!, 'COLOR_BUFFER_BIT'); + _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]); + Object getUniformLocation(Object program, String uniformName) { + Object? res = js_util + .callMethod(glContext, 'getUniformLocation', [program, uniformName]); + if (res == null) { + throw Exception('$uniformName not found'); + } else { + return res; + } } - /// Returns reference to attribute in program. - Object? getAttribLocation(Object? program, String uniformName) { + /// Returns reference to uniform in program. + Object getAttributeLocation(Object program, String attribName) { + Object? res = js_util + .callMethod(glContext, 'getAttribLocation', [program, attribName]); + if (res == null) { + throw Exception('$attribName not found'); + } else { + return res; + } + } + + /// Sets float uniform value. + void setUniform1f(Object uniform, double value) { return js_util - .callMethod(glContext!, 'getAttribLocation', - [program, uniformName]); + .callMethod(glContext, 'uniform1f', [uniform, value]); } /// Sets vec2 uniform values. void setUniform2f(Object uniform, double value1, double value2) { return js_util - .callMethod(glContext!, 'uniform2f', [uniform, value1, value2]); + .callMethod(glContext, 'uniform2f', [uniform, value1, value2]); } /// Sets vec4 uniform values. - void setUniform4f(Object? uniform, double value1, double value2, double value3, + void setUniform4f(Object uniform, double value1, double value2, double value3, double value4) { return js_util.callMethod( - glContext!, 'uniform4f', [uniform, value1, value2, value3, value4]); + glContext, 'uniform4f', [uniform, value1, value2, value3, value4]); } /// Sets mat4 uniform values. - void setUniformMatrix4fv(Object? uniform, bool transpose, Float32List? value) { + void setUniformMatrix4fv(Object uniform, bool transpose, Float32List value) { return js_util.callMethod( - glContext!, 'uniformMatrix4fv', [uniform, transpose, value]); + glContext, 'uniformMatrix4fv', [uniform, transpose, value]); } /// Shader compile error log. dynamic getShaderInfoLog(Object glShader) { - return js_util.callMethod(glContext!, 'getShaderInfoLog', [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]); + String? getProgramInfoLog(Object glProgram) { + return js_util.callMethod(glContext, 'getProgramInfoLog', [glProgram]); } int? get drawingBufferWidth => - js_util.getProperty(glContext!, 'drawingBufferWidth'); + js_util.getProperty(glContext, 'drawingBufferWidth'); int? get drawingBufferHeight => - js_util.getProperty(glContext!, 'drawingBufferWidth'); + js_util.getProperty(glContext, 'drawingBufferWidth'); + /// Reads gl contents as image data. + /// + /// Warning: data is read bottom up (flipped). html.ImageData readImageData() { + const int kBytesPerPixel = 4; + final int bufferWidth = _widthInPixels!; + final int bufferHeight = _heightInPixels!; if (browserEngine == BrowserEngine.webkit || browserEngine == BrowserEngine.firefox) { - const int kBytesPerPixel = 4; - final int bufferWidth = _widthInPixels!; - final int bufferHeight = _heightInPixels!; final Uint8List pixels = Uint8List(bufferWidth * bufferHeight * kBytesPerPixel); - js_util.callMethod(glContext!, 'readPixels', + js_util.callMethod(glContext, 'readPixels', [0, 0, bufferWidth, bufferHeight, kRGBA, kUnsignedByte, pixels]); return html.ImageData( Uint8ClampedList.fromList(pixels), bufferWidth, bufferHeight); } else { - const int kBytesPerPixel = 4; - final int bufferWidth = _widthInPixels!; - final int bufferHeight = _heightInPixels!; final Uint8ClampedList pixels = Uint8ClampedList(bufferWidth * bufferHeight * kBytesPerPixel); - js_util.callMethod(glContext!, 'readPixels', + js_util.callMethod(glContext, 'readPixels', [0, 0, bufferWidth, bufferHeight, kRGBA, kUnsignedByte, pixels]); return html.ImageData(pixels, bufferWidth, bufferHeight); } } + + /// Returns image data in a form that can be used to create Canvas + /// context patterns. + Object? readPatternData() { + // When using OffscreenCanvas and transferToImageBitmap is supported by + // browser create ImageBitmap otherwise use more expensive canvas + // allocation. + if (_canvas != null && + js_util.hasProperty(_canvas!, 'transferToImageBitmap')) { + js_util.callMethod(_canvas!, 'getContext', ['webgl2']); + Object?imageBitmap = js_util.callMethod(_canvas!, 'transferToImageBitmap', + []); + return imageBitmap; + } else { + html.CanvasElement canvas = html.CanvasElement(width: _widthInPixels, height: _heightInPixels); + final html.CanvasRenderingContext2D ctx = canvas.context2D; + drawImage(ctx, 0, 0); + return canvas; + } + } } -/// Shared Cached OffscreenCanvas for webgl rendering to image. -class _OffscreenCanvas { - static html.OffscreenCanvas? _canvas; - static int _maxPixelWidth = 0; - static int _maxPixelHeight = 0; - static html.CanvasElement? _glCanvas; - static _GlContext? _cachedContext; +/// Polyfill for html.OffscreenCanvas that is not supported on some browsers. +class _OffScreenCanvas { + html.OffscreenCanvas? _canvas; + html.CanvasElement? _glCanvas; + int width; + int height; - _OffscreenCanvas(int width, int height) { - assert(width > 0 && height > 0); - if (width > _maxPixelWidth || height > _maxPixelHeight) { - // Allocate bigger offscreen canvas. + _OffScreenCanvas(this.width, this.height) { + if (_OffScreenCanvas.supported) { _canvas = html.OffscreenCanvas(width, height); - _maxPixelWidth = width; - _maxPixelHeight = height; - _cachedContext?.dispose(); - _cachedContext = null; + } else { + _glCanvas = html.CanvasElement( + width: width, + height: height, + ); + _glCanvas!.className = 'gl-canvas'; + final double cssWidth = width / EngineWindow.browserDevicePixelRatio; + final double cssHeight = height / EngineWindow.browserDevicePixelRatio; + _glCanvas!.style + ..position = 'absolute' + ..width = '${cssWidth}px' + ..height = '${cssHeight}px'; } } - static void dispose() { + void dispose() { _canvas = null; + _glCanvas = null; + } + + /// Feature detects OffscreenCanvas. + static bool get supported => + js_util.hasProperty(html.window, 'OffscreenCanvas'); +} + +/// Creates gl context from cached OffscreenCanvas for webgl rendering to image. +class _GlContextCache { + static int _maxPixelWidth = 0; + static int _maxPixelHeight = 0; + static _GlContext? _cachedContext; + static _OffScreenCanvas? _offScreenCanvas; + + static void dispose() { _maxPixelWidth = 0; _maxPixelHeight = 0; - _glCanvas = null; _cachedContext = null; + _offScreenCanvas?.dispose(); } - html.OffscreenCanvas? get canvas => _canvas; - static _GlContext? createGlContext(int widthInPixels, int heightInPixels) { - final bool isWebKit = (browserEngine == BrowserEngine.webkit); - - if (_OffscreenCanvas.supported) { - final _OffscreenCanvas offScreenCanvas = - _OffscreenCanvas(widthInPixels, heightInPixels); - _cachedContext ??= _GlContext.fromOffscreenCanvas(offScreenCanvas.canvas!); - _cachedContext!.setViewportSize(widthInPixels, heightInPixels); - return _cachedContext; + if (widthInPixels > _maxPixelWidth || heightInPixels > _maxPixelHeight) { + _cachedContext?.dispose(); + _cachedContext = null; + _offScreenCanvas = null; + _maxPixelWidth = math.max(_maxPixelWidth, widthInPixels); + _maxPixelHeight = math.max(_maxPixelHeight, widthInPixels); + } + _offScreenCanvas ??= _OffScreenCanvas(widthInPixels, heightInPixels); + if (_OffScreenCanvas.supported) { + _cachedContext ??= + _GlContext.fromOffscreenCanvas(_offScreenCanvas!._canvas!); } else { - // Allocate new canvas element is size is larger. - if (widthInPixels > _maxPixelWidth || heightInPixels > _maxPixelHeight) { - _glCanvas = html.CanvasElement( - width: widthInPixels, - height: heightInPixels, - ); - _glCanvas!.className = 'gl-canvas'; - final double cssWidth = widthInPixels / EngineWindow.browserDevicePixelRatio; - final double cssHeight = heightInPixels / EngineWindow.browserDevicePixelRatio; - _glCanvas!.style - ..position = 'absolute' - ..width = '${cssWidth}px' - ..height = '${cssHeight}px'; - _maxPixelWidth = widthInPixels; - _maxPixelHeight = heightInPixels; - _cachedContext?.dispose(); - _cachedContext = null; - } - _cachedContext ??= _GlContext.fromCanvas(_glCanvas!, isWebKit); - _cachedContext!.setViewportSize(widthInPixels, heightInPixels); - return _cachedContext; + _cachedContext ??= _GlContext.fromCanvas(_offScreenCanvas!._glCanvas!, + webGLVersion == 1); } + _cachedContext!.setViewportSize(widthInPixels, heightInPixels); + return _cachedContext; } - - /// Feature detects OffscreenCanvas. - static bool get supported => - js_util.hasProperty(html.window, 'OffscreenCanvas'); } diff --git a/lib/web_ui/lib/src/engine/html/shaders/normalized_gradient.dart b/lib/web_ui/lib/src/engine/html/shaders/normalized_gradient.dart new file mode 100644 index 0000000000000..f53dc4d7fc946 --- /dev/null +++ b/lib/web_ui/lib/src/engine/html/shaders/normalized_gradient.dart @@ -0,0 +1,162 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// @dart = 2.10 +part of engine; + +/// Converts colors and stops to typed array of bias, scale and threshold to use +/// in shaders. +/// +/// A color is generated by taking a t value [0..1] and computing +/// t * scale + bias. +/// +/// Example: For stops 0.0 t1, t2, 1.0 and colors c0, c1, c2, c3 +/// Given t1 colors, {List? stops}) { + // If colorStops is not provided, then only two stops, at 0.0 and 1.0, + // are implied (and colors must therefore only have two entries). + assert(stops != null || colors.length == 2); + stops ??= const [0.0, 1.0]; + final int colorCount = colors.length; + int normalizedCount = colorCount; + bool addFirst = stops[0] != 0.0; + bool addLast = stops.last != 1.0; + if (addFirst) { + normalizedCount++; + } + if (addLast) { + normalizedCount++; + } + final Float32List bias = Float32List(normalizedCount * 4); + final Float32List scale = Float32List(normalizedCount * 4); + final Float32List thresholds = Float32List(4 * ((normalizedCount - 1)~/4 + 1)); + int targetIndex = 0; + int thresholdIndex = 0; + if (addFirst) { + ui.Color c = colors[0]; + bias[targetIndex++] = c.red / 255.0; + bias[targetIndex++] = c.green / 255.0; + bias[targetIndex++] = c.blue / 255.0; + bias[targetIndex++] = c.alpha / 255.0; + thresholds[thresholdIndex++] = 0.0; + } + for (ui.Color c in colors) { + bias[targetIndex++] = c.red / 255.0; + bias[targetIndex++] = c.green / 255.0; + bias[targetIndex++] = c.blue / 255.0; + bias[targetIndex++] = c.alpha / 255.0; + } + for (double stop in stops) { + thresholds[thresholdIndex++] = stop; + } + if (addLast) { + ui.Color c = colors.last; + bias[targetIndex++] = c.red / 255.0; + bias[targetIndex++] = c.green / 255.0; + bias[targetIndex++] = c.blue / 255.0; + bias[targetIndex++] = c.alpha / 255.0; + thresholds[thresholdIndex++] = 1.0; + } + // Now that we have bias for each color stop, we can compute scale based + // on delta between colors. + int lastColorIndex = 4 * (normalizedCount - 1); + for (int i = 0; i < lastColorIndex; i++) { + int thresholdIndex = i >> 2; + scale[i] = (bias[i + 4] - bias[i]) / + (thresholds[thresholdIndex + 1] - thresholds[thresholdIndex]); + } + scale[lastColorIndex] = 0.0; + scale[lastColorIndex + 1] = 0.0; + scale[lastColorIndex + 2] = 0.0; + scale[lastColorIndex + 3] = 0.0; + // Compute bias = colorAtStop - stopValue * (scale). + for (int i = 0; i < normalizedCount; i++) { + double t = thresholds[i]; + int colorIndex = i * 4; + bias[colorIndex] -= t * scale[colorIndex]; + bias[colorIndex + 1] -= t * scale[colorIndex + 1]; + bias[colorIndex + 2] -= t * scale[colorIndex + 2]; + bias[colorIndex + 3] -= t * scale[colorIndex + 3]; + } + return NormalizedGradient._(normalizedCount, thresholds, scale, bias); + } + + /// Sets uniforms for threshold, bias and scale for program. + void setupUniforms(_GlContext gl, _GlProgram glProgram) { + for (int i = 0; i < thresholdCount; i++) { + Object biasId = gl.getUniformLocation(glProgram.program, 'bias_$i'); + gl.setUniform4f(biasId, _bias[i * 4], _bias[i * 4 + 1], _bias[i * 4 + 2], _bias[i * 4 + 3]); + Object scaleId = gl.getUniformLocation(glProgram.program, 'scale_$i'); + gl.setUniform4f(scaleId, _scale[i * 4], _scale[i * 4 + 1], _scale[i * 4 + 2], _scale[i * 4 + 3]); + } + for (int i = 0; i < _thresholds.length; i += 4) { + Object thresId = gl.getUniformLocation(glProgram.program, 'threshold_${i ~/ 4}'); + gl.setUniform4f(thresId, _thresholds[i], _thresholds[i + 1], _thresholds[i + 2], _thresholds[i + 3]); + } + } + + /// Returns bias component at index. + double biasAt(int index) => _bias[index]; + + /// Returns scale component at index. + double scaleAt(int index) => _scale[index]; + + /// Returns threshold at index. + double thresholdAt(int index) => _thresholds[index]; +} + +/// Writes fragment shader code to search for probe value in source data and set +/// bias and scale to be used for computation. +/// +/// Source data for thresholds is provided using ceil(count/4) packed vec4 +/// uniforms. +/// +/// Bias and scale data are vec4 uniforms that hold color data. +void _writeUnrolledBinarySearch(ShaderMethod method, int start, int end, + {required String probe, + required String sourcePrefix, required String biasName, + required String scaleName}) { + if (start == end) { + String biasSource = '${biasName}_${start}'; + method.addStatement('${biasName} = ${biasSource};'); + String scaleSource = '${scaleName}_${start}'; + method.addStatement('${scaleName} = ${scaleSource};'); + } else { + // Add probe check. + int mid = (start + end) ~/ 2; + String thresholdAtMid = '${sourcePrefix}_${(mid + 1)~/4}'; + thresholdAtMid += '.${_vectorComponentIndexToName((mid + 1) % 4)}'; + method.addStatement('if ($probe < $thresholdAtMid) {'); + method.indent(); + _writeUnrolledBinarySearch(method, start, mid, + probe: probe, sourcePrefix: sourcePrefix, biasName: biasName, + scaleName: scaleName); + method.unindent(); + method.addStatement('} else {'); + method.indent(); + _writeUnrolledBinarySearch(method, mid + 1, end, + probe: probe, sourcePrefix: sourcePrefix, biasName: biasName, + scaleName: scaleName); + method.unindent(); + method.addStatement('}'); + } +} + +String _vectorComponentIndexToName(int index) { + assert(index >=0 && index <= 4); + return 'xyzw'[index]; +} diff --git a/lib/web_ui/lib/src/engine/html/shaders/shader.dart b/lib/web_ui/lib/src/engine/html/shaders/shader.dart index 4dae88f674bf9..3c79c7c983cfd 100644 --- a/lib/web_ui/lib/src/engine/html/shaders/shader.dart +++ b/lib/web_ui/lib/src/engine/html/shaders/shader.dart @@ -10,7 +10,8 @@ abstract class EngineGradient implements ui.Gradient { EngineGradient._(); /// Creates a fill style to be used in painting. - Object createPaintStyle(html.CanvasRenderingContext2D? ctx); + Object createPaintStyle(html.CanvasRenderingContext2D? ctx, + ui.Rect? shaderBounds); } class GradientSweep extends EngineGradient { @@ -22,14 +23,111 @@ class GradientSweep extends EngineGradient { assert(startAngle != null), // ignore: unnecessary_null_comparison assert(endAngle != null), // ignore: unnecessary_null_comparison assert(startAngle < endAngle), - assert(matrix4 == null || _matrix4IsValid(matrix4)), super._() { _validateColorStops(colors, colorStops); } @override - Object createPaintStyle(html.CanvasRenderingContext2D? ctx) { - throw UnimplementedError(); + Object createPaintStyle(html.CanvasRenderingContext2D? ctx, + ui.Rect? shaderBounds) { + assert(shaderBounds != null); + int widthInPixels = shaderBounds!.right.ceil(); + int heightInPixels = shaderBounds.bottom.ceil(); + assert(widthInPixels > 0 && heightInPixels > 0); + + initWebGl(); + // Render gradient into a bitmap and create a canvas pattern. + _OffScreenCanvas offScreenCanvas = + _OffScreenCanvas(widthInPixels, heightInPixels); + _GlContext gl = _OffScreenCanvas.supported + ? _GlContext.fromOffscreenCanvas(offScreenCanvas._canvas!) + : _GlContext.fromCanvas(offScreenCanvas._glCanvas!, + webGLVersion == 1); + gl.setViewportSize(widthInPixels, heightInPixels); + + NormalizedGradient normalizedGradient = NormalizedGradient( + colors, stops: colorStops); + + _GlProgram glProgram = gl.useAndCacheProgram( + _WebGlRenderer.writeBaseVertexShader(), + _createSweepFragmentShader(normalizedGradient, tileMode))!; + + Object tileOffset = gl.getUniformLocation(glProgram.program, 'u_tile_offset'); + double centerX = (center.dx - shaderBounds.left) / (shaderBounds.width); + double centerY = (center.dy - shaderBounds.top) / (shaderBounds.height); + gl.setUniform2f(tileOffset, + shaderBounds.left + 2 * (shaderBounds.width * (centerX - 0.5)), + -shaderBounds.top - 2 * (shaderBounds.height * (centerY - 0.5))); + Object angleRange = gl.getUniformLocation(glProgram.program, 'angle_range'); + gl.setUniform2f(angleRange, startAngle, endAngle); + normalizedGradient.setupUniforms(gl, glProgram); + if (matrix4 != null) { + Object gradientMatrix = gl.getUniformLocation( + glProgram.program, 'm_gradient'); + gl.setUniformMatrix4fv(gradientMatrix, false, matrix4!); + } + + Object? imageBitmap = _glRenderer!.drawRect(shaderBounds, gl, + glProgram, normalizedGradient, widthInPixels, heightInPixels); + + return ctx!.createPattern(imageBitmap!, 'no-repeat')!; + } + + String _createSweepFragmentShader(NormalizedGradient gradient, + ui.TileMode tileMode) { + ShaderBuilder builder = ShaderBuilder.fragment(webGLVersion); + builder.floatPrecision = ShaderPrecision.kMedium; + builder.addIn(ShaderType.kVec4, name: 'v_color'); + builder.addUniform(ShaderType.kVec2, name: 'u_resolution'); + builder.addUniform(ShaderType.kVec2, name: 'u_tile_offset'); + builder.addUniform(ShaderType.kVec2, name: 'angle_range'); + builder.addUniform(ShaderType.kMat4, name: 'm_gradient'); + ShaderDeclaration fragColor = builder.fragmentColor; + ShaderMethod method = builder.addMethod('main'); + // Sweep gradient + method.addStatement( + 'vec2 center = 0.5 * (u_resolution + u_tile_offset);'); + method.addStatement( + 'vec4 localCoord = vec4(gl_FragCoord.x - center.x, center.y - gl_FragCoord.y, 0, 1) * m_gradient;'); + method.addStatement( + 'float angle = atan(-localCoord.y, -localCoord.x) + ${math.pi};'); + method.addStatement( + 'float sweep = angle_range.y - angle_range.x;'); + method.addStatement( + 'angle = (angle - angle_range.x) / sweep;'); + method.addStatement('' + 'float st = angle;'); + + method.addStatement('vec4 bias;'); + method.addStatement('vec4 scale;'); + // Write uniforms for each threshold, bias and scale. + for (int i = 0; i < (gradient.thresholdCount - 1) ~/ 4 + 1; i++) { + builder.addUniform(ShaderType.kVec4, name: 'threshold_${i}'); + } + for (int i = 0; i < gradient.thresholdCount; i++) { + builder.addUniform(ShaderType.kVec4, name: 'bias_$i'); + builder.addUniform(ShaderType.kVec4, name: 'scale_$i'); + } + String probeName = 'st'; + switch (tileMode) { + case ui.TileMode.clamp: + break; + case ui.TileMode.repeated: + method.addStatement('float tiled_st = fract(st);'); + probeName = 'tiled_st'; + break; + case ui.TileMode.mirror: + method.addStatement('float t_1 = (st - 1.0);'); + method.addStatement('float tiled_st = abs((t_1 - 2.0 * floor(t_1 * 0.5)) - 1.0);'); + probeName = 'tiled_st'; + break; + } + _writeUnrolledBinarySearch(method, 0, gradient.thresholdCount - 1, + probe: probeName, sourcePrefix: 'threshold', + biasName: 'bias', scaleName: 'scale'); + method.addStatement('${fragColor.name} = ${probeName} * scale + bias;'); + String shader = builder.build(); + return shader; } final ui.Offset center; @@ -68,7 +166,8 @@ class GradientLinear extends EngineGradient { final _FastMatrix64? matrix4; @override - html.CanvasGradient createPaintStyle(html.CanvasRenderingContext2D? ctx) { + html.CanvasGradient createPaintStyle(html.CanvasRenderingContext2D? ctx, + ui.Rect? shaderBounds) { _FastMatrix64? matrix4 = this.matrix4; html.CanvasGradient gradient; if (matrix4 != null) { @@ -115,7 +214,8 @@ class GradientRadial extends EngineGradient { final Float32List? matrix4; @override - Object createPaintStyle(html.CanvasRenderingContext2D? ctx) { + Object createPaintStyle(html.CanvasRenderingContext2D? ctx, + ui.Rect? shaderBounds) { if (!experimentalUseSkia) { if (tileMode != ui.TileMode.clamp) { throw UnimplementedError( @@ -154,7 +254,8 @@ class GradientConical extends EngineGradient { final Float32List? matrix4; @override - Object createPaintStyle(html.CanvasRenderingContext2D? ctx) { + Object createPaintStyle(html.CanvasRenderingContext2D? ctx, + ui.Rect? shaderBounds) { throw UnimplementedError(); } } diff --git a/lib/web_ui/lib/src/engine/html/shaders/shader_builder.dart b/lib/web_ui/lib/src/engine/html/shaders/shader_builder.dart index c07fe90985adc..d5d90cb93eee0 100644 --- a/lib/web_ui/lib/src/engine/html/shaders/shader_builder.dart +++ b/lib/web_ui/lib/src/engine/html/shaders/shader_builder.dart @@ -58,7 +58,7 @@ class ShaderBuilder { ShaderDeclaration? _fragmentColorDeclaration; ShaderBuilder(this.version) : isWebGl2 = version == WebGLVersion.webgl2, - _isFragmentShader = false; + _isFragmentShader = false; ShaderBuilder.fragment(this.version) : isWebGl2 = version == WebGLVersion.webgl2, @@ -238,9 +238,23 @@ class ShaderMethod { final String returnType = 'void'; final String name; final List _statements = []; + int _indentLevel = 1; + + void indent() { + ++_indentLevel; + } + + void unindent() { + assert(_indentLevel != 1); + --_indentLevel; + } void addStatement(String statement) { - _statements.add(statement); + if (assertionsEnabled) { + _statements.add(' ' * _indentLevel + statement); + } else { + _statements.add(statement); + } } void write(StringBuffer buffer) { diff --git a/lib/web_ui/lib/src/engine/util.dart b/lib/web_ui/lib/src/engine/util.dart index 277f5484e2f07..4d0951527978f 100644 --- a/lib/web_ui/lib/src/engine/util.dart +++ b/lib/web_ui/lib/src/engine/util.dart @@ -507,6 +507,12 @@ class _FastMatrix64 { transformedX = matrix[12] + (matrix[0] * x) + (matrix[4] * y); transformedY = matrix[13] + (matrix[1] * x) + (matrix[5] * y); } + + String debugToString() => + '${matrix[0].toStringAsFixed(3)}, ${matrix[4].toStringAsFixed(3)}, ${matrix[8].toStringAsFixed(3)}, ${matrix[12].toStringAsFixed(3)}\n' + '${matrix[1].toStringAsFixed(3)}, ${matrix[5].toStringAsFixed(3)}, ${matrix[9].toStringAsFixed(3)}, ${matrix[13].toStringAsFixed(3)}\n' + '${matrix[2].toStringAsFixed(3)}, ${matrix[6].toStringAsFixed(3)}, ${matrix[10].toStringAsFixed(3)}, ${matrix[14].toStringAsFixed(3)}\n' + '${matrix[3].toStringAsFixed(3)}, ${matrix[7].toStringAsFixed(3)}, ${matrix[11].toStringAsFixed(3)}, ${matrix[15].toStringAsFixed(3)}\n'; } /// Roughly the inverse of [ui.Shadow.convertRadiusToSigma]. diff --git a/lib/web_ui/test/engine/surface/shaders/normalized_gradient_test.dart b/lib/web_ui/test/engine/surface/shaders/normalized_gradient_test.dart new file mode 100644 index 0000000000000..b413bb65a220f --- /dev/null +++ b/lib/web_ui/test/engine/surface/shaders/normalized_gradient_test.dart @@ -0,0 +1,125 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// @dart = 2.6 +import 'package:test/bootstrap/browser.dart'; +import 'package:test/test.dart'; +import 'package:ui/src/engine.dart'; +import 'package:ui/ui.dart' as ui hide window; + +void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() { + group('Shader Normalized Gradient', () { + test('3 stop at start', () { + NormalizedGradient gradient = NormalizedGradient([ + ui.Color(0xFF000000), ui.Color(0xFFFF7f3f) + ], stops: [0.0, 0.5]); + int res = _computeColorAt(gradient, 0.0); + assert(res == 0xFF000000); + res = _computeColorAt(gradient, 0.25); + assert(res == 0xFF7f3f1f); + res = _computeColorAt(gradient, 0.5); + assert(res == 0xFFFF7f3f); + res = _computeColorAt(gradient, 0.7); + assert(res == 0xFFFF7f3f); + res = _computeColorAt(gradient, 1.0); + assert(res == 0xFFFF7f3f); + }); + + test('3 stop at end', () { + NormalizedGradient gradient = NormalizedGradient([ + ui.Color(0xFF000000), ui.Color(0xFFFF7f3f) + ], stops: [0.5, 1.0]); + int res = _computeColorAt(gradient, 0.0); + assert(res == 0xFF000000); + res = _computeColorAt(gradient, 0.25); + assert(res == 0xFF000000); + res = _computeColorAt(gradient, 0.5); + assert(res == 0xFF000000); + res = _computeColorAt(gradient, 0.75); + assert(res == 0xFF7f3f1f); + res = _computeColorAt(gradient, 1.0); + assert(res == 0xFFFF7f3f); + }); + + test('4 stop', () { + NormalizedGradient gradient = NormalizedGradient([ + ui.Color(0xFF000000), ui.Color(0xFFFF7f3f) + ], stops: [0.25, 0.5]); + int res = _computeColorAt(gradient, 0.0); + assert(res == 0xFF000000); + res = _computeColorAt(gradient, 0.25); + assert(res == 0xFF000000); + res = _computeColorAt(gradient, 0.4); + assert(res == 0xFF994c25); + res = _computeColorAt(gradient, 0.5); + assert(res == 0xFFFF7f3f); + res = _computeColorAt(gradient, 0.75); + assert(res == 0xFFFF7f3f); + res = _computeColorAt(gradient, 1.0); + assert(res == 0xFFFF7f3f); + }); + + test('5 stop', () { + NormalizedGradient gradient = NormalizedGradient([ + ui.Color(0x10000000), ui.Color(0x20FF0000), + ui.Color(0x4000FF00), ui.Color(0x800000FF), + ui.Color(0xFFFFFFFF) + ], stops: [0.0, 0.1, 0.2, 0.5, 1.0]); + int res = _computeColorAt(gradient, 0.0); + assert(res == 0x10000000); + res = _computeColorAt(gradient, 0.05); + assert(res == 0x187f0000); + res = _computeColorAt(gradient, 0.1); + assert(res == 0x20ff0000); + res = _computeColorAt(gradient, 0.15); + assert(res == 0x307f7f00); + res = _computeColorAt(gradient, 0.2); + assert(res == 0x4000ff00); + res = _computeColorAt(gradient, 0.4); + assert(res == 0x6a0054a9); + res = _computeColorAt(gradient, 0.5); + assert(res == 0x800000fe); + res = _computeColorAt(gradient, 0.9); + assert(res == 0xe5ccccff); + res = _computeColorAt(gradient, 1.0); + assert(res == 0xffffffff); + }); + + test('2 stops at ends', () { + NormalizedGradient gradient = NormalizedGradient([ + ui.Color(0x00000000), ui.Color(0xFFFFFFFF) + ]); + int res = _computeColorAt(gradient, 0.0); + assert(res == 0); + res = _computeColorAt(gradient, 1.0); + assert(res == 0xFFFFFFFF); + res = _computeColorAt(gradient, 0.5); + assert(res == 0x7f7f7f7f); + }); + }); +} + +int _computeColorAt(NormalizedGradient gradient, double t) { + int i = 0; + while (t > gradient.thresholdAt(i + 1)) { + ++i; + } + double r = t * gradient.scaleAt(i * 4) + gradient.biasAt(i * 4); + double g = t * gradient.scaleAt(i * 4 + 1) + gradient.biasAt(i * 4 + 1); + double b = t * gradient.scaleAt(i * 4 + 2) + gradient.biasAt(i * 4 + 2); + double a = t * gradient.scaleAt(i * 4 + 3) + gradient.biasAt(i * 4 + 3); + int val = 0; + val |= (a * 0xFF).toInt() & 0xFF; + val<<=8; + val |= (r * 0xFF).toInt() & 0xFF; + val<<=8; + val |= (g * 0xFF).toInt() & 0xFF; + val<<=8; + val |= (b * 0xFF).toInt() & 0xFF; + return val; +} diff --git a/lib/web_ui/test/engine/surface/shaders/shader_builder_test.dart b/lib/web_ui/test/engine/surface/shaders/shader_builder_test.dart index 3af3db222a870..336cf4f8d8890 100644 --- a/lib/web_ui/test/engine/surface/shaders/shader_builder_test.dart +++ b/lib/web_ui/test/engine/surface/shaders/shader_builder_test.dart @@ -201,7 +201,7 @@ void testMain() { 'precision mediump float;\n' 'uniform float ${variable.name};\n' 'void main() {\n' - 'f1 = 5.0;\n' + ' f1 = 5.0;\n' '}\n'); }); }); diff --git a/lib/web_ui/test/golden_tests/engine/gradient_golden_test.dart b/lib/web_ui/test/golden_tests/engine/gradient_golden_test.dart new file mode 100644 index 0000000000000..196938ff5eeed --- /dev/null +++ b/lib/web_ui/test/golden_tests/engine/gradient_golden_test.dart @@ -0,0 +1,318 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// @dart = 2.6 +import 'dart:html' as html; +import 'dart:math' as math; +import 'dart:typed_data'; + +import 'package:test/bootstrap/browser.dart'; +import 'package:test/test.dart'; +import 'package:ui/ui.dart'; +import 'package:ui/src/engine.dart'; + +import 'package:web_engine_tester/golden_tester.dart'; + +import 'scuba.dart'; + +void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() 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, 240), + double maxDiffRatePercent = 0.0, bool write: false}) async { + final EngineCanvas engineCanvas = BitmapCanvas(screenRect); + + rc.endRecording(); + rc.apply(engineCanvas, screenRect); + + // 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); + await matchGoldenFile('$fileName.png', + region: region, maxDiffRatePercent: maxDiffRatePercent, write: write); + } finally { + // The page is reused across tests, so remove the element after taking the + // Scuba screenshot. + sceneElement.remove(); + } + } + + setUp(() async { + debugEmulateFlutterTesterEnvironment = true; + }); + + setUpStableTestFonts(); + + test('Paints sweep gradient rectangles', () async { + final RecordingCanvas canvas = + RecordingCanvas(const Rect.fromLTRB(0, 0, 400, 300)); + canvas.save(); + + final Paint borderPaint = Paint() + ..style = PaintingStyle.stroke + ..strokeWidth = 1 + ..color = Color(0xFF000000); + + List colors = [ + Color(0xFF000000), + Color(0xFFFF3C38), + Color(0xFFFF8C42), + Color(0xFFFFF275), + Color(0xFF6699CC), + Color(0xFF656D78),]; + List stops = [0.0, 0.05, 0.4, 0.6, 0.9, 1.0]; + + EngineGradient sweepGradient = GradientSweep(Offset(0.5, 0.5), + colors, stops, TileMode.clamp, + 0, 360.0 / 180.0 * math.pi, + Matrix4.rotationZ(math.pi / 6.0).storage); + + EngineGradient sweepGradientRotated = GradientSweep(Offset(0.5, 0.5), + colors, stops, TileMode.clamp, + 0, 360.0 / 180.0 * math.pi, + Matrix4.rotationZ(math.pi / 6.0).storage); + + const double kBoxWidth = 150; + const double kBoxHeight = 80; + // Gradient with default center. + Rect rectBounds = Rect.fromLTWH(10, 20, kBoxWidth, kBoxHeight); + canvas.drawRect(rectBounds, + Paint()..shader = engineGradientToShader(sweepGradient, rectBounds)); + canvas.drawRect(rectBounds, borderPaint); + + // Gradient with shifted center and rotation. + rectBounds = rectBounds.translate(kBoxWidth + 10, 0); + canvas.drawRect(rectBounds, + Paint()..shader = engineGradientToShader(sweepGradientRotated, Rect.fromLTWH(rectBounds.center.dx, rectBounds.top, rectBounds.width / 2, rectBounds.height))); + canvas.drawRect(rectBounds, borderPaint); + + // Gradient with start/endangle. + sweepGradient = GradientSweep(Offset(0.5, 0.5), + colors, stops, TileMode.clamp, + math.pi / 6, 3 * math.pi / 4, + Matrix4.rotationZ(math.pi / 6.0).storage); + + rectBounds = rectBounds.translate(kBoxWidth + 10, 0); + canvas.drawRect(rectBounds, + new Paint()..shader = engineGradientToShader(sweepGradient, rectBounds)); + canvas.drawRect(rectBounds, borderPaint); + + // Tile mode repeat + rectBounds = Rect.fromLTWH(10, 110, kBoxWidth, kBoxHeight); + sweepGradient = GradientSweep(Offset(0.5, 0.5), + colors, stops, TileMode.repeated, + math.pi / 6, 3 * math.pi / 4, + Matrix4.rotationZ(math.pi / 6.0).storage); + + canvas.drawRect(rectBounds, + new Paint()..shader = engineGradientToShader(sweepGradient, rectBounds)); + canvas.drawRect(rectBounds, borderPaint); + + // Tile mode mirror + rectBounds = rectBounds.translate(kBoxWidth + 10, 0); + sweepGradient = GradientSweep(Offset(0.5, 0.5), + colors, stops, TileMode.mirror, + math.pi / 6, 3 * math.pi / 4, + Matrix4.rotationZ(math.pi / 6.0).storage); + canvas.drawRect(rectBounds, + new Paint()..shader = engineGradientToShader(sweepGradient, rectBounds)); + canvas.drawRect(rectBounds, borderPaint); + + canvas.restore(); + await _checkScreenshot(canvas, 'sweep_gradient_rect'); + }); + + test('Paints sweep gradient ovals', () async { + final RecordingCanvas canvas = + RecordingCanvas(const Rect.fromLTRB(0, 0, 400, 300)); + canvas.save(); + + final Paint borderPaint = Paint() + ..style = PaintingStyle.stroke + ..strokeWidth = 1 + ..color = Color(0xFF000000); + + List colors = [ + Color(0xFF000000), + Color(0xFFFF3C38), + Color(0xFFFF8C42), + Color(0xFFFFF275), + Color(0xFF6699CC), + Color(0xFF656D78),]; + List stops = [0.0, 0.05, 0.4, 0.6, 0.9, 1.0]; + + EngineGradient sweepGradient = GradientSweep(Offset(0.5, 0.5), + colors, stops, TileMode.clamp, + 0, 360.0 / 180.0 * math.pi, + Matrix4.rotationZ(math.pi / 6.0).storage); + + EngineGradient sweepGradientRotated = GradientSweep(Offset(0.5, 0.5), + colors, stops, TileMode.clamp, + 0, 360.0 / 180.0 * math.pi, + Matrix4.rotationZ(math.pi / 6.0).storage); + + const double kBoxWidth = 150; + const double kBoxHeight = 80; + // Gradient with default center. + Rect rectBounds = Rect.fromLTWH(10, 20, kBoxWidth, kBoxHeight); + canvas.drawOval(rectBounds, + Paint()..shader = engineGradientToShader(sweepGradient, rectBounds)); + canvas.drawRect(rectBounds, borderPaint); + + // Gradient with shifted center and rotation. + rectBounds = rectBounds.translate(kBoxWidth + 10, 0); + canvas.drawOval(rectBounds, + Paint()..shader = engineGradientToShader(sweepGradientRotated, Rect.fromLTWH(rectBounds.center.dx, rectBounds.top, rectBounds.width / 2, rectBounds.height))); + canvas.drawRect(rectBounds, borderPaint); + + // Gradient with start/endangle. + sweepGradient = GradientSweep(Offset(0.5, 0.5), + colors, stops, TileMode.clamp, + math.pi / 6, 3 * math.pi / 4, + Matrix4.rotationZ(math.pi / 6.0).storage); + + rectBounds = rectBounds.translate(kBoxWidth + 10, 0); + canvas.drawOval(rectBounds, + new Paint()..shader = engineGradientToShader(sweepGradient, rectBounds)); + canvas.drawRect(rectBounds, borderPaint); + + // Tile mode repeat + rectBounds = Rect.fromLTWH(10, 110, kBoxWidth, kBoxHeight); + sweepGradient = GradientSweep(Offset(0.5, 0.5), + colors, stops, TileMode.repeated, + math.pi / 6, 3 * math.pi / 4, + Matrix4.rotationZ(math.pi / 6.0).storage); + + canvas.drawOval(rectBounds, + new Paint()..shader = engineGradientToShader(sweepGradient, rectBounds)); + canvas.drawRect(rectBounds, borderPaint); + + // Tile mode mirror + rectBounds = rectBounds.translate(kBoxWidth + 10, 0); + sweepGradient = GradientSweep(Offset(0.5, 0.5), + colors, stops, TileMode.mirror, + math.pi / 6, 3 * math.pi / 4, + Matrix4.rotationZ(math.pi / 6.0).storage); + canvas.drawOval(rectBounds, + new Paint()..shader = engineGradientToShader(sweepGradient, rectBounds)); + canvas.drawRect(rectBounds, borderPaint); + + canvas.restore(); + await _checkScreenshot(canvas, 'sweep_gradient_oval'); + }); + + test('Paints sweep gradient paths', () async { + final RecordingCanvas canvas = + RecordingCanvas(const Rect.fromLTRB(0, 0, 400, 300)); + canvas.save(); + + final Paint borderPaint = Paint() + ..style = PaintingStyle.stroke + ..strokeWidth = 1 + ..color = Color(0xFF000000); + + List colors = [ + Color(0xFF000000), + Color(0xFFFF3C38), + Color(0xFFFF8C42), + Color(0xFFFFF275), + Color(0xFF6699CC), + Color(0xFF656D78),]; + List stops = [0.0, 0.05, 0.4, 0.6, 0.9, 1.0]; + + EngineGradient sweepGradient = GradientSweep(Offset(0.5, 0.5), + colors, stops, TileMode.clamp, + 0, 360.0 / 180.0 * math.pi, + Matrix4.rotationZ(math.pi / 6.0).storage); + + EngineGradient sweepGradientRotated = GradientSweep(Offset(0.5, 0.5), + colors, stops, TileMode.clamp, + 0, 360.0 / 180.0 * math.pi, + Matrix4.rotationZ(math.pi / 6.0).storage); + + const double kBoxWidth = 150; + const double kBoxHeight = 80; + // Gradient with default center. + Rect rectBounds = Rect.fromLTWH(10, 20, kBoxWidth, kBoxHeight); + Path path = samplePathFromRect(rectBounds); + canvas.drawPath(path, + Paint()..shader = engineGradientToShader(sweepGradient, rectBounds)); + canvas.drawRect(rectBounds, borderPaint); + + // Gradient with shifted center and rotation. + rectBounds = rectBounds.translate(kBoxWidth + 10, 0); + path = samplePathFromRect(rectBounds); + canvas.drawPath(path, + Paint()..shader = engineGradientToShader(sweepGradientRotated, Rect.fromLTWH(rectBounds.center.dx, rectBounds.top, rectBounds.width / 2, rectBounds.height))); + canvas.drawRect(rectBounds, borderPaint); + + // Gradient with start/endangle. + sweepGradient = GradientSweep(Offset(0.5, 0.5), + colors, stops, TileMode.clamp, + math.pi / 6, 3 * math.pi / 4, + Matrix4.rotationZ(math.pi / 6.0).storage); + + rectBounds = rectBounds.translate(kBoxWidth + 10, 0); + path = samplePathFromRect(rectBounds); + canvas.drawPath(path, + new Paint()..shader = engineGradientToShader(sweepGradient, rectBounds)); + canvas.drawRect(rectBounds, borderPaint); + + // Tile mode repeat + rectBounds = Rect.fromLTWH(10, 110, kBoxWidth, kBoxHeight); + sweepGradient = GradientSweep(Offset(0.5, 0.5), + colors, stops, TileMode.repeated, + math.pi / 6, 3 * math.pi / 4, + Matrix4.rotationZ(math.pi / 6.0).storage); + + path = samplePathFromRect(rectBounds); + canvas.drawPath(path, + new Paint()..shader = engineGradientToShader(sweepGradient, rectBounds)); + canvas.drawRect(rectBounds, borderPaint); + + // Tile mode mirror + rectBounds = rectBounds.translate(kBoxWidth + 10, 0); + sweepGradient = GradientSweep(Offset(0.5, 0.5), + colors, stops, TileMode.mirror, + math.pi / 6, 3 * math.pi / 4, + Matrix4.rotationZ(math.pi / 6.0).storage); + path = samplePathFromRect(rectBounds); + canvas.drawPath(path, + new Paint()..shader = engineGradientToShader(sweepGradient, rectBounds)); + canvas.drawRect(rectBounds, borderPaint); + + canvas.restore(); + await _checkScreenshot(canvas, 'sweep_gradient_path'); + }); +} + +Shader engineGradientToShader(GradientSweep gradient, Rect rect) { + return Gradient.sweep( + Offset(rect.left + gradient.center.dx * rect.width, + rect.top + gradient.center.dy * rect.height), + gradient.colors, gradient.colorStops, gradient.tileMode, + gradient.startAngle, + gradient.endAngle, + gradient.matrix4 == null ? null : + Float64List.fromList(gradient.matrix4), + ); +} + +Path samplePathFromRect(Rect rectBounds) => + Path() + ..moveTo(rectBounds.center.dx, rectBounds.top) + ..lineTo(rectBounds.left, rectBounds.bottom) + ..quadraticBezierTo(rectBounds.center.dx + 20, rectBounds.bottom - 40, + rectBounds.right, rectBounds.bottom) + ..close();