| // 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:math' as math; |
| import 'dart:typed_data'; |
| |
| import 'package:ui/ui.dart' as ui; |
| |
| import '../../browser_detection.dart'; |
| import '../../util.dart'; |
| import '../../validators.dart'; |
| import '../../vector_math.dart'; |
| import '../offscreen_canvas.dart'; |
| import '../path/path_utils.dart'; |
| import '../render_vertices.dart'; |
| import 'normalized_gradient.dart'; |
| import 'shader_builder.dart'; |
| import 'vertex_shaders.dart'; |
| import 'webgl_context.dart'; |
| |
| const double kFltEpsilon = 1.19209290E-07; // == 1 / (2 ^ 23) |
| const double kFltEpsilonSquared = 1.19209290E-07 * 1.19209290E-07; |
| |
| abstract class EngineGradient implements ui.Gradient { |
| /// Hidden constructor to prevent subclassing. |
| EngineGradient._(); |
| |
| /// Creates a fill style to be used in painting. |
| Object createPaintStyle(html.CanvasRenderingContext2D? ctx, |
| ui.Rect? shaderBounds, double density); |
| |
| /// Creates a CanvasImageSource to paint gradient. |
| Object createImageBitmap( |
| ui.Rect? shaderBounds, double density, bool createDataUrl); |
| } |
| |
| class GradientSweep extends EngineGradient { |
| GradientSweep(this.center, this.colors, this.colorStops, this.tileMode, |
| this.startAngle, this.endAngle, this.matrix4) |
| : assert(offsetIsValid(center)), |
| assert(colors != null), // ignore: unnecessary_null_comparison |
| assert(tileMode != null), // ignore: unnecessary_null_comparison |
| assert(startAngle != null), // ignore: unnecessary_null_comparison |
| assert(endAngle != null), // ignore: unnecessary_null_comparison |
| assert(startAngle < endAngle), |
| super._() { |
| validateColorStops(colors, colorStops); |
| } |
| |
| @override |
| Object createImageBitmap( |
| ui.Rect? shaderBounds, double density, bool createDataUrl) { |
| assert(shaderBounds != null); |
| final int widthInPixels = shaderBounds!.width.ceil(); |
| final int heightInPixels = shaderBounds.height.ceil(); |
| assert(widthInPixels > 0 && heightInPixels > 0); |
| |
| initWebGl(); |
| // Render gradient into a bitmap and create a canvas pattern. |
| final OffScreenCanvas offScreenCanvas = |
| OffScreenCanvas(widthInPixels, heightInPixels); |
| final GlContext gl = GlContext(offScreenCanvas); |
| gl.setViewportSize(widthInPixels, heightInPixels); |
| |
| final NormalizedGradient normalizedGradient = |
| NormalizedGradient(colors, stops: colorStops); |
| |
| final GlProgram glProgram = gl.cacheProgram(VertexShaders.writeBaseVertexShader(), |
| _createSweepFragmentShader(normalizedGradient, tileMode)); |
| gl.useProgram(glProgram); |
| |
| final Object tileOffset = |
| gl.getUniformLocation(glProgram.program, 'u_tile_offset'); |
| final double centerX = (center.dx - shaderBounds.left) / (shaderBounds.width); |
| final double centerY = (center.dy - shaderBounds.top) / (shaderBounds.height); |
| gl.setUniform2f(tileOffset, 2 * (shaderBounds.width * (centerX - 0.5)), |
| 2 * (shaderBounds.height * (centerY - 0.5))); |
| final Object angleRange = gl.getUniformLocation(glProgram.program, 'angle_range'); |
| gl.setUniform2f(angleRange, startAngle, endAngle); |
| normalizedGradient.setupUniforms(gl, glProgram); |
| if (matrix4 != null) { |
| final Object gradientMatrix = |
| gl.getUniformLocation(glProgram.program, 'm_gradient'); |
| gl.setUniformMatrix4fv(gradientMatrix, false, matrix4!); |
| } |
| if (createDataUrl) { |
| return glRenderer!.drawRectToImageUrl( |
| ui.Rect.fromLTWH(0, 0, shaderBounds.width, shaderBounds.height), |
| gl, |
| glProgram, |
| normalizedGradient, |
| widthInPixels, |
| heightInPixels); |
| } else { |
| return glRenderer!.drawRect( |
| ui.Rect.fromLTWH(0, 0, shaderBounds.width, shaderBounds.height), |
| gl, |
| glProgram, |
| normalizedGradient, |
| widthInPixels, |
| heightInPixels)!; |
| } |
| } |
| |
| @override |
| Object createPaintStyle(html.CanvasRenderingContext2D? ctx, |
| ui.Rect? shaderBounds, double density) { |
| final Object imageBitmap = createImageBitmap(shaderBounds, density, false); |
| return ctx!.createPattern(imageBitmap, 'no-repeat')!; |
| } |
| |
| String _createSweepFragmentShader( |
| NormalizedGradient gradient, ui.TileMode tileMode) { |
| final 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'); |
| final ShaderDeclaration fragColor = builder.fragmentColor; |
| final 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;'); |
| |
| final String probeName = |
| _writeSharedGradientShader(builder, method, gradient, tileMode); |
| method.addStatement('${fragColor.name} = $probeName * scale + bias;'); |
| |
| final String shader = builder.build(); |
| return shader; |
| } |
| |
| final ui.Offset center; |
| final List<ui.Color> colors; |
| final List<double>? colorStops; |
| final ui.TileMode tileMode; |
| final double startAngle; |
| final double endAngle; |
| final Float32List? matrix4; |
| } |
| |
| class GradientLinear extends EngineGradient { |
| GradientLinear( |
| this.from, |
| this.to, |
| this.colors, |
| this.colorStops, |
| this.tileMode, |
| Float32List? matrix, |
| ) : assert(offsetIsValid(from)), |
| assert(offsetIsValid(to)), |
| assert(colors != null), // ignore: unnecessary_null_comparison |
| assert(tileMode != null), // ignore: unnecessary_null_comparison |
| matrix4 = matrix == null ? null : FastMatrix32(matrix), |
| super._() { |
| if (assertionsEnabled) { |
| validateColorStops(colors, colorStops); |
| } |
| } |
| |
| final ui.Offset from; |
| final ui.Offset to; |
| final List<ui.Color> colors; |
| final List<double>? colorStops; |
| final ui.TileMode tileMode; |
| final FastMatrix32? matrix4; |
| |
| @override |
| Object createPaintStyle(html.CanvasRenderingContext2D? ctx, |
| ui.Rect? shaderBounds, double density) { |
| if (tileMode == ui.TileMode.clamp || tileMode == ui.TileMode.decal) { |
| return _createCanvasGradient(ctx, shaderBounds, density); |
| } else { |
| return _createGlGradient(ctx, shaderBounds, density); |
| } |
| } |
| |
| html.CanvasGradient _createCanvasGradient(html.CanvasRenderingContext2D? ctx, |
| ui.Rect? shaderBounds, double density) { |
| final FastMatrix32? matrix4 = this.matrix4; |
| html.CanvasGradient gradient; |
| final double offsetX = shaderBounds!.left; |
| final double offsetY = shaderBounds.top; |
| if (matrix4 != null) { |
| // The matrix is relative to shaderBounds so we shift center by |
| // shaderBounds top-left origin. |
| final double centerX = (from.dx + to.dx) / 2.0 - shaderBounds.left; |
| final double centerY = (from.dy + to.dy) / 2.0 - shaderBounds.top; |
| |
| matrix4.transform(from.dx - centerX, from.dy - centerY); |
| final double fromX = matrix4.transformedX + centerX; |
| final double fromY = matrix4.transformedY + centerY; |
| matrix4.transform(to.dx - centerX, to.dy - centerY); |
| gradient = ctx!.createLinearGradient( |
| fromX - offsetX, |
| fromY - offsetY, |
| matrix4.transformedX + centerX - offsetX, |
| matrix4.transformedY + centerY - offsetY); |
| } else { |
| gradient = ctx!.createLinearGradient(from.dx - offsetX, from.dy - offsetY, |
| to.dx - offsetX, to.dy - offsetY); |
| } |
| _addColorStopsToCanvasGradient( |
| gradient, colors, colorStops, tileMode == ui.TileMode.decal); |
| return gradient; |
| } |
| |
| @override |
| Object createImageBitmap( |
| ui.Rect? shaderBounds, double density, bool createDataUrl) { |
| assert(shaderBounds != null); |
| final int widthInPixels = shaderBounds!.width.ceil(); |
| final int heightInPixels = shaderBounds.height.ceil(); |
| assert(widthInPixels > 0 && heightInPixels > 0); |
| initWebGl(); |
| // Render gradient into a bitmap and create a canvas pattern. |
| final OffScreenCanvas offScreenCanvas = |
| OffScreenCanvas(widthInPixels, heightInPixels); |
| final GlContext gl = GlContext(offScreenCanvas); |
| gl.setViewportSize(widthInPixels, heightInPixels); |
| |
| final NormalizedGradient normalizedGradient = |
| NormalizedGradient(colors, stops: colorStops); |
| |
| final GlProgram glProgram = gl.cacheProgram(VertexShaders.writeBaseVertexShader(), |
| _createLinearFragmentShader(normalizedGradient, tileMode)); |
| gl.useProgram(glProgram); |
| |
| /// When creating an image to apply to a dom element, render |
| /// contents at 0,0 and adjust gradient vector for shaderBounds. |
| final bool translateToOrigin = createDataUrl; |
| |
| if (translateToOrigin) { |
| shaderBounds = shaderBounds.translate(-shaderBounds.left, -shaderBounds.top); |
| } |
| |
| // Setup from/to uniforms. |
| // |
| // From/to is relative to shaderBounds. |
| // |
| // To compute t value between 0..1 for any point on the screen, |
| // we need to use from,to point pair to construct a matrix that will |
| // take any fragment coordinate and transform it to a t value. |
| // |
| // We compute the matrix by: |
| // 1- Shift from,to vector to origin. |
| // 2- Rotate the vector to align with x axis. |
| // 3- Scale it to unit vector. |
| final double fromX = from.dx; |
| final double fromY = from.dy; |
| final double toX = to.dx; |
| final double toY = to.dy; |
| |
| final double dx = toX - fromX; |
| final double dy = toY - fromY; |
| final double length = math.sqrt(dx * dx + dy * dy); |
| // sin(theta) = dy / length. |
| // cos(theta) = dx / length. |
| // Flip dy for gl flip. |
| final double sinVal = length < kFltEpsilon ? 0 : -dy / length; |
| final double cosVal = length < kFltEpsilon ? 1 : dx / length; |
| // If tile mode is repeated we need to shift the center of from->to |
| // vector to the center of shader bounds. |
| final bool isRepeated = tileMode != ui.TileMode.clamp; |
| final double originX = isRepeated |
| ? (shaderBounds.width / 2) |
| : (fromX + toX) / 2.0 - shaderBounds.left; |
| final double originY = isRepeated |
| ? (shaderBounds.height / 2) |
| : (fromY + toY) / 2.0 - shaderBounds.top; |
| |
| final Matrix4 originTranslation = |
| Matrix4.translationValues(-originX, -originY, 0); |
| // Rotate around Z axis. |
| final Matrix4 rotationZ = Matrix4.identity(); |
| final Float32List storage = rotationZ.storage; |
| storage[0] = cosVal; |
| // Sign is flipped since gl coordinate system is flipped around y axis. |
| storage[1] = sinVal; |
| storage[4] = -sinVal; |
| storage[5] = cosVal; |
| final Matrix4 gradientTransform = Matrix4.identity(); |
| // We compute location based on gl_FragCoord to center distance which |
| // returns 0.0 at center. To make sure we align center of gradient to this |
| // point, we shift by 0.5 to get st value for center of gradient. |
| if (tileMode != ui.TileMode.repeated) { |
| gradientTransform.translate(0.5, 0); |
| } |
| if (length > kFltEpsilon) { |
| gradientTransform.scale(1.0 / length); |
| } |
| if (matrix4 != null) { |
| // Flutter GradientTransform is defined in shaderBounds coordinate system |
| // with flipped y axis. |
| // We flip y axis, translate to center, multiply matrix and translate |
| // and flip back so it is applied correctly. |
| final Matrix4 m4 = Matrix4.fromFloat32List(matrix4!.matrix); |
| gradientTransform.scale(1, -1); |
| gradientTransform.translate( |
| -shaderBounds.center.dx, -shaderBounds.center.dy); |
| gradientTransform.multiply(m4); |
| gradientTransform.translate( |
| shaderBounds.center.dx, shaderBounds.center.dy); |
| gradientTransform.scale(1, -1); |
| } |
| |
| gradientTransform.multiply(rotationZ); |
| gradientTransform.multiply(originTranslation); |
| // Setup gradient uniforms for t search. |
| normalizedGradient.setupUniforms(gl, glProgram); |
| // Setup matrix transform uniform. |
| final Object gradientMatrix = |
| gl.getUniformLocation(glProgram.program, 'm_gradient'); |
| gl.setUniformMatrix4fv(gradientMatrix, false, gradientTransform.storage); |
| |
| final Object uRes = gl.getUniformLocation(glProgram.program, 'u_resolution'); |
| gl.setUniform2f(uRes, widthInPixels.toDouble(), heightInPixels.toDouble()); |
| |
| if (createDataUrl) { |
| return glRenderer!.drawRectToImageUrl( |
| ui.Rect.fromLTWH(0, 0, shaderBounds.width, |
| shaderBounds.height) /* !! shaderBounds */, |
| gl, |
| glProgram, |
| normalizedGradient, |
| widthInPixels, |
| heightInPixels, |
| ); |
| } else { |
| return glRenderer!.drawRect( |
| ui.Rect.fromLTWH(0, 0, shaderBounds.width, |
| shaderBounds.height) /* !! shaderBounds */, |
| gl, |
| glProgram, |
| normalizedGradient, |
| widthInPixels, |
| heightInPixels, |
| )!; |
| } |
| } |
| |
| /// Creates a linear gradient with tiling repeat or mirror. |
| html.CanvasPattern _createGlGradient(html.CanvasRenderingContext2D? ctx, |
| ui.Rect? shaderBounds, double density) { |
| final Object imageBitmap = createImageBitmap(shaderBounds, density, false); |
| return ctx!.createPattern(imageBitmap, 'no-repeat')!; |
| } |
| |
| String _createLinearFragmentShader( |
| NormalizedGradient gradient, ui.TileMode tileMode) { |
| final 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.kMat4, name: 'm_gradient'); |
| final ShaderDeclaration fragColor = builder.fragmentColor; |
| final ShaderMethod method = builder.addMethod('main'); |
| // Linear gradient. |
| // Multiply with m_gradient transform to convert from fragment coordinate to |
| // distance on the from-to line. |
| method.addStatement('vec4 localCoord = m_gradient * vec4(gl_FragCoord.x, ' |
| 'u_resolution.y - gl_FragCoord.y, 0, 1);'); |
| method.addStatement('float st = localCoord.x;'); |
| final String probeName = |
| _writeSharedGradientShader(builder, method, gradient, tileMode); |
| method.addStatement('${fragColor.name} = $probeName * scale + bias;'); |
| final String shader = builder.build(); |
| return shader; |
| } |
| } |
| |
| void _addColorStopsToCanvasGradient(html.CanvasGradient gradient, |
| List<ui.Color> colors, List<double>? colorStops, bool isDecal) { |
| double scale, offset; |
| if (isDecal) { |
| scale = 0.999; |
| offset = (1.0 - scale) / 2.0; |
| gradient.addColorStop(0, '#00000000'); |
| } else { |
| scale = 1.0; |
| offset = 0.0; |
| } |
| if (colorStops == null) { |
| assert(colors.length == 2); |
| gradient.addColorStop(offset, colorToCssString(colors[0])!); |
| gradient.addColorStop(1 - offset, colorToCssString(colors[1])!); |
| } else { |
| for (int i = 0; i < colors.length; i++) { |
| final double colorStop = colorStops[i].clamp(0.0, 1.0); |
| gradient.addColorStop( |
| colorStop * scale + offset, colorToCssString(colors[i])!); |
| } |
| } |
| if (isDecal) { |
| gradient.addColorStop(1, '#00000000'); |
| } |
| } |
| |
| /// Writes shader code to map fragment value to gradient color. |
| /// |
| /// Returns name of gradient treshold variable to use to compute color. |
| String _writeSharedGradientShader(ShaderBuilder builder, ShaderMethod method, |
| NormalizedGradient gradient, ui.TileMode tileMode) { |
| 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'); |
| } |
| |
| // Use st variable name if clamped or decaled, otherwise write code to compute |
| // tiled_st. |
| String probeName = 'st'; |
| switch (tileMode) { |
| case ui.TileMode.clamp: |
| method.addStatement('float tiled_st = clamp(st, 0.0, 1.0);'); |
| probeName = 'tiled_st'; |
| break; |
| case ui.TileMode.decal: |
| break; |
| case ui.TileMode.repeated: |
| // st represents our distance from center. Flutter maps the center to |
| // center of gradient ramp so we need to add 0.5 to make sure repeated |
| // pattern center is at origin. |
| 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'); |
| return probeName; |
| } |
| |
| class GradientRadial extends EngineGradient { |
| GradientRadial(this.center, this.radius, this.colors, this.colorStops, |
| this.tileMode, this.matrix4) |
| : super._(); |
| |
| final ui.Offset center; |
| final double radius; |
| final List<ui.Color> colors; |
| final List<double>? colorStops; |
| final ui.TileMode tileMode; |
| final Float32List? matrix4; |
| |
| @override |
| Object createPaintStyle(html.CanvasRenderingContext2D? ctx, |
| ui.Rect? shaderBounds, double density) { |
| if (tileMode == ui.TileMode.clamp || tileMode == ui.TileMode.decal) { |
| return _createCanvasGradient(ctx, shaderBounds, density); |
| } else { |
| return _createGlGradient(ctx, shaderBounds, density); |
| } |
| } |
| |
| Object _createCanvasGradient(html.CanvasRenderingContext2D? ctx, |
| ui.Rect? shaderBounds, double density) { |
| final double offsetX = shaderBounds!.left; |
| final double offsetY = shaderBounds.top; |
| final html.CanvasGradient gradient = ctx!.createRadialGradient( |
| center.dx - offsetX, |
| center.dy - offsetY, |
| 0, |
| center.dx - offsetX, |
| center.dy - offsetY, |
| radius); |
| _addColorStopsToCanvasGradient( |
| gradient, colors, colorStops, tileMode == ui.TileMode.decal); |
| return gradient; |
| } |
| |
| @override |
| Object createImageBitmap( |
| ui.Rect? shaderBounds, double density, bool createDataUrl) { |
| assert(shaderBounds != null); |
| final int widthInPixels = shaderBounds!.width.ceil(); |
| final int heightInPixels = shaderBounds.height.ceil(); |
| assert(widthInPixels > 0 && heightInPixels > 0); |
| |
| initWebGl(); |
| // Render gradient into a bitmap and create a canvas pattern. |
| final OffScreenCanvas offScreenCanvas = |
| OffScreenCanvas(widthInPixels, heightInPixels); |
| final GlContext gl = GlContext(offScreenCanvas); |
| gl.setViewportSize(widthInPixels, heightInPixels); |
| |
| final NormalizedGradient normalizedGradient = |
| NormalizedGradient(colors, stops: colorStops); |
| |
| final GlProgram glProgram = gl.cacheProgram( |
| VertexShaders.writeBaseVertexShader(), |
| _createRadialFragmentShader( |
| normalizedGradient, shaderBounds, tileMode)); |
| gl.useProgram(glProgram); |
| |
| final Object tileOffset = |
| gl.getUniformLocation(glProgram.program, 'u_tile_offset'); |
| final double centerX = (center.dx - shaderBounds.left) / (shaderBounds.width); |
| final double centerY = (center.dy - shaderBounds.top) / (shaderBounds.height); |
| gl.setUniform2f(tileOffset, 2 * (shaderBounds.width * (centerX - 0.5)), |
| 2 * (shaderBounds.height * (centerY - 0.5))); |
| final Object radiusUniform = gl.getUniformLocation(glProgram.program, 'u_radius'); |
| gl.setUniform1f(radiusUniform, radius); |
| normalizedGradient.setupUniforms(gl, glProgram); |
| |
| final Object gradientMatrix = |
| gl.getUniformLocation(glProgram.program, 'm_gradient'); |
| gl.setUniformMatrix4fv(gradientMatrix, false, |
| matrix4 == null ? Matrix4.identity().storage : matrix4!); |
| |
| if (createDataUrl) { |
| return glRenderer!.drawRectToImageUrl( |
| ui.Rect.fromLTWH(0, 0, shaderBounds.width, shaderBounds.height), |
| gl, |
| glProgram, |
| normalizedGradient, |
| widthInPixels, |
| heightInPixels); |
| } else { |
| return glRenderer!.drawRect( |
| ui.Rect.fromLTWH(0, 0, shaderBounds.width, shaderBounds.height), |
| gl, |
| glProgram, |
| normalizedGradient, |
| widthInPixels, |
| heightInPixels)!; |
| } |
| } |
| |
| /// Creates a radial gradient with tiling repeat or mirror. |
| html.CanvasPattern _createGlGradient(html.CanvasRenderingContext2D? ctx, |
| ui.Rect? shaderBounds, double density) { |
| final Object imageBitmap = createImageBitmap(shaderBounds, density, false); |
| return ctx!.createPattern(imageBitmap, 'no-repeat')!; |
| } |
| |
| String _createRadialFragmentShader( |
| NormalizedGradient gradient, ui.Rect shaderBounds, ui.TileMode tileMode) { |
| final 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.kFloat, name: 'u_radius'); |
| builder.addUniform(ShaderType.kMat4, name: 'm_gradient'); |
| final ShaderDeclaration fragColor = builder.fragmentColor; |
| final 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 dist = length(localCoord);'); |
| method.addStatement('' |
| 'float st = abs(dist / u_radius);'); |
| final String probeName = |
| _writeSharedGradientShader(builder, method, gradient, tileMode); |
| method.addStatement('${fragColor.name} = $probeName * scale + bias;'); |
| final String shader = builder.build(); |
| return shader; |
| } |
| } |
| |
| /// TODO: Implement focal https://github.com/flutter/flutter/issues/76643. |
| class GradientConical extends GradientRadial { |
| GradientConical( |
| this.focal, |
| this.focalRadius, |
| ui.Offset center, |
| double radius, |
| List<ui.Color> colors, |
| List<double>? colorStops, |
| ui.TileMode tileMode, |
| Float32List? matrix4) |
| : super(center, radius, colors, colorStops, tileMode, matrix4); |
| |
| final ui.Offset focal; |
| final double focalRadius; |
| |
| @override |
| Object createPaintStyle(html.CanvasRenderingContext2D? ctx, |
| ui.Rect? shaderBounds, double density) { |
| if ((tileMode == ui.TileMode.clamp || tileMode == ui.TileMode.decal) && |
| focalRadius == 0.0 && |
| focal == const ui.Offset(0, 0)) { |
| return _createCanvasGradient(ctx, shaderBounds, density); |
| } else { |
| initWebGl(); |
| return _createGlGradient(ctx, shaderBounds, density); |
| } |
| } |
| |
| @override |
| String _createRadialFragmentShader( |
| NormalizedGradient gradient, ui.Rect shaderBounds, ui.TileMode tileMode) { |
| /// If distance between centers is nearly zero we can pretend we're radial |
| /// to prevent divide by zero in computing gradient. |
| final double centerDistanceX = center.dx - focal.dx; |
| final double centerDistanceY = center.dy - focal.dy; |
| final double centerDistanceSq = |
| centerDistanceX * centerDistanceX + centerDistanceY * centerDistanceY; |
| if (centerDistanceSq < kFltEpsilonSquared) { |
| return super |
| ._createRadialFragmentShader(gradient, shaderBounds, tileMode); |
| } |
| final double centerDistance = math.sqrt(centerDistanceSq); |
| double r0 = focalRadius / centerDistance; |
| double r1 = radius / centerDistance; |
| double fFocalX = r0 / (r0 - r1); |
| |
| if ((fFocalX - 1).abs() < SPath.scalarNearlyZero) { |
| // swap r0, r1 |
| final double temp = r0; |
| r0 = r1; |
| r1 = temp; |
| fFocalX = 0.0; // because r0 is now 0 |
| } |
| |
| final 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.kFloat, name: 'u_radius'); |
| builder.addUniform(ShaderType.kMat4, name: 'm_gradient'); |
| final ShaderDeclaration fragColor = builder.fragmentColor; |
| final 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 dist = length(localCoord);'); |
| final String f = (focalRadius / |
| (math.min(shaderBounds.width, shaderBounds.height) / 2.0)) |
| .toStringAsPrecision(8); |
| method.addStatement(focalRadius == 0.0 |
| ? 'float st = dist / u_radius;' |
| : 'float st = ((dist / u_radius) - $f) / (1.0 - $f);'); |
| if (tileMode == ui.TileMode.clamp) { |
| method.addStatement('if (st < 0.0) { st = -1.0; }'); |
| } |
| final String probeName = |
| _writeSharedGradientShader(builder, method, gradient, tileMode); |
| method.addStatement('${fragColor.name} = $probeName * scale + bias;'); |
| return builder.build(); |
| } |
| } |
| |
| /// Backend implementation of [ui.ImageFilter]. |
| /// |
| /// Currently only `blur` and `matrix` are supported. |
| abstract class EngineImageFilter implements ui.ImageFilter { |
| factory EngineImageFilter.blur({ |
| required double sigmaX, |
| required double sigmaY, |
| required ui.TileMode tileMode, |
| }) = _BlurEngineImageFilter; |
| |
| factory EngineImageFilter.matrix({ |
| required Float64List matrix, |
| required ui.FilterQuality filterQuality, |
| }) = _MatrixEngineImageFilter; |
| |
| EngineImageFilter._(); |
| |
| String get filterAttribute => ''; |
| String get transformAttribute => ''; |
| } |
| |
| class _BlurEngineImageFilter extends EngineImageFilter { |
| _BlurEngineImageFilter({ this.sigmaX = 0.0, this.sigmaY = 0.0, this.tileMode = ui.TileMode.clamp }) : super._(); |
| |
| final double sigmaX; |
| final double sigmaY; |
| final ui.TileMode tileMode; |
| |
| // TODO(flutter_web): implement TileMode. |
| @override |
| String get filterAttribute => blurSigmasToCssString(sigmaX, sigmaY); |
| |
| @override |
| bool operator ==(Object other) { |
| if (other.runtimeType != runtimeType) |
| return false; |
| return other is _BlurEngineImageFilter && |
| other.tileMode == tileMode && |
| other.sigmaX == sigmaX && |
| other.sigmaY == sigmaY; |
| } |
| |
| @override |
| int get hashCode => ui.hashValues(sigmaX, sigmaY, tileMode); |
| |
| @override |
| String toString() { |
| return 'ImageFilter.blur($sigmaX, $sigmaY, $tileMode)'; |
| } |
| } |
| |
| class _MatrixEngineImageFilter extends EngineImageFilter { |
| _MatrixEngineImageFilter({ required Float64List matrix, required this.filterQuality }) |
| : webMatrix = Float64List.fromList(matrix), |
| super._(); |
| |
| final Float64List webMatrix; |
| final ui.FilterQuality filterQuality; |
| |
| // TODO(flutter_web): implement FilterQuality. |
| @override |
| String get transformAttribute => float64ListToCssTransform(webMatrix); |
| |
| @override |
| bool operator ==(Object other) { |
| if (other.runtimeType != runtimeType) |
| return false; |
| return other is _MatrixEngineImageFilter |
| && other.filterQuality == filterQuality |
| && listEquals<double>(other.webMatrix, webMatrix); |
| } |
| |
| @override |
| int get hashCode => ui.hashValues(ui.hashList(webMatrix), filterQuality); |
| |
| @override |
| String toString() { |
| return 'ImageFilter.matrix($webMatrix, $filterQuality)'; |
| } |
| } |