blob: 7034c392a901a748d768b1501e8bd3ac67da946f [file] [log] [blame]
// 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:js_util' as js_util;
import 'dart:math' as math;
import 'dart:typed_data';
import 'package:ui/ui.dart' as ui;
import '../browser_detection.dart';
import '../util.dart';
import '../vector_math.dart';
import 'painting.dart';
import 'shaders/image_shader.dart';
import 'shaders/normalized_gradient.dart';
import 'shaders/shader_builder.dart';
import 'shaders/vertex_shaders.dart';
import 'shaders/webgl_context.dart';
GlRenderer? glRenderer;
class SurfaceVertices implements ui.Vertices {
final ui.VertexMode mode;
final Float32List positions;
final Int32List? colors;
final Uint16List? indices; // ignore: unused_field
SurfaceVertices(
this.mode,
List<ui.Offset> positions, {
List<ui.Color>? colors,
List<int>? indices,
}) : assert(mode != null), // ignore: unnecessary_null_comparison
assert(positions != null), // ignore: unnecessary_null_comparison
// ignore: unnecessary_this
this.colors = colors != null ? _int32ListFromColors(colors) : null,
// ignore: unnecessary_this
this.indices = indices != null ? Uint16List.fromList(indices) : null,
// ignore: unnecessary_this
this.positions = offsetListToFloat32List(positions) {
initWebGl();
}
SurfaceVertices.raw(
this.mode,
this.positions, {
this.colors,
this.indices,
}) : assert(mode != null), // ignore: unnecessary_null_comparison
assert(positions != null) { // ignore: unnecessary_null_comparison
initWebGl();
}
static Int32List _int32ListFromColors(List<ui.Color> colors) {
final Int32List list = Int32List(colors.length);
final int len = colors.length;
for (int i = 0; i < len; i++) {
list[i] = colors[i].value;
}
return list;
}
}
/// Lazily initializes web gl.
///
/// Used to treeshake WebGlRenderer when user doesn't create Vertices object
/// to use the API.
void initWebGl() {
glRenderer ??= _WebGlRenderer();
}
void disposeWebGl() {
GlContextCache.dispose();
glRenderer = null;
}
abstract class GlRenderer {
void drawVertices(
html.CanvasRenderingContext2D? context,
int canvasWidthInPixels,
int canvasHeightInPixels,
Matrix4 transform,
SurfaceVertices vertices,
ui.BlendMode blendMode,
SurfacePaintData paint);
Object? drawRect(ui.Rect targetRect, GlContext gl, GlProgram glProgram,
NormalizedGradient gradient, int widthInPixels, int heightInPixels);
String drawRectToImageUrl(
ui.Rect targetRect,
GlContext gl,
GlProgram glProgram,
NormalizedGradient gradient,
int widthInPixels,
int heightInPixels);
void drawHairline(html.CanvasRenderingContext2D? _ctx, Float32List positions);
}
/// Treeshakeable backend for rendering webgl on canvas.
///
/// 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 {
@override
void drawVertices(
html.CanvasRenderingContext2D? context,
int canvasWidthInPixels,
int canvasHeightInPixels,
Matrix4 transform,
SurfaceVertices vertices,
ui.BlendMode blendMode,
SurfacePaintData paint) {
// Compute bounds of vertices.
final Float32List positions = vertices.positions;
final ui.Rect bounds = _computeVerticesBounds(positions, transform);
final double minValueX = bounds.left;
final double minValueY = bounds.top;
final double maxValueX = bounds.right;
final double maxValueY = bounds.bottom;
double offsetX = 0;
double offsetY = 0;
int widthInPixels = canvasWidthInPixels;
int heightInPixels = canvasHeightInPixels;
// If vertices fall outside the bitmap area, cull.
if (maxValueX < 0 || maxValueY < 0) {
return;
}
if (minValueX > widthInPixels || minValueY > heightInPixels) {
return;
}
// If Vertices are is smaller than hosting canvas, allocate minimal
// offscreen canvas to reduce readPixels data size.
if ((maxValueX - minValueX) < widthInPixels &&
(maxValueY - minValueY) < heightInPixels) {
widthInPixels = maxValueX.ceil() - minValueX.floor();
heightInPixels = maxValueY.ceil() - minValueY.floor();
offsetX = minValueX.floor().toDouble();
offsetY = minValueY.floor().toDouble();
}
if (widthInPixels == 0 || heightInPixels == 0) {
return;
}
final bool isWebGl2 = webGLVersion == WebGLVersion.webgl2;
final EngineImageShader? imageShader =
paint.shader == null ? null : paint.shader! as EngineImageShader;
final String vertexShader = imageShader == null
? VertexShaders.writeBaseVertexShader()
: VertexShaders.writeTextureVertexShader();
final String fragmentShader = imageShader == null
? _writeVerticesFragmentShader()
: FragmentShaders.writeTextureFragmentShader(
isWebGl2, imageShader.tileModeX, imageShader.tileModeY);
final GlContext gl =
GlContextCache.createGlContext(widthInPixels, heightInPixels)!;
final GlProgram glProgram = gl.cacheProgram(vertexShader, fragmentShader);
gl.useProgram(glProgram);
final Object positionAttributeLocation =
gl.getAttributeLocation(glProgram.program, 'position');
setupVertexTransforms(gl, glProgram, offsetX, offsetY,
widthInPixels.toDouble(), heightInPixels.toDouble(), transform);
if (imageShader != null) {
/// To map from vertex position to texture coordinate in 0..1 range,
/// we setup scalar to be used in vertex shader.
setupTextureTransform(
gl,
glProgram,
0.0,
0.0,
1.0 / imageShader.image.width.toDouble(),
1.0 / imageShader.image.height.toDouble());
}
// Setup geometry.
//
// Create buffer for vertex coordinates.
final Object positionsBuffer = gl.createBuffer()!;
assert(positionsBuffer != null); // ignore: unnecessary_null_comparison
Object? vao;
if (imageShader != null) {
if (isWebGl2) {
// Create a vertex array object.
vao = gl.createVertexArray();
// Set vertex array object as active one.
gl.bindVertexArray(vao!);
}
}
// Turn on position attribute.
gl.enableVertexAttribArray(positionAttributeLocation);
// Bind buffer as position buffer and transfer data.
gl.bindArrayBuffer(positionsBuffer);
bufferVertexData(gl, positions, 1.0);
// Setup data format for attribute.
js_util.callMethod(gl.glContext, 'vertexAttribPointer', <dynamic>[
positionAttributeLocation,
2,
gl.kFloat,
false,
0,
0,
]);
final int vertexCount = positions.length ~/ 2;
Object? texture;
if (imageShader == null) {
// Setup color buffer.
final Object? colorsBuffer = gl.createBuffer();
gl.bindArrayBuffer(colorsBuffer);
// Buffer kBGRA_8888.
if (vertices.colors == null) {
final ui.Color color = paint.color ?? const ui.Color(0xFF000000);
final Uint32List vertexColors = Uint32List(vertexCount);
for (int i = 0; i < vertexCount; i++) {
vertexColors[i] = color.value;
}
gl.bufferData(vertexColors, gl.kStaticDraw);
} else {
gl.bufferData(vertices.colors, gl.kStaticDraw);
}
final Object colorLoc = gl.getAttributeLocation(glProgram.program, 'color');
js_util.callMethod(gl.glContext, 'vertexAttribPointer',
<dynamic>[colorLoc, 4, gl.kUnsignedByte, true, 0, 0]);
gl.enableVertexAttribArray(colorLoc);
} else {
// Copy image it to the texture.
texture = gl.createTexture();
// Texture units are a global array of references to the textures.
// By setting activeTexture, we associate the bound texture to a unit.
// Every time we call a texture function such as texImage2D with a target
// like TEXTURE_2D, it looks up texture by using the currently active
// unit.
// In our case we have a single texture unit 0.
gl.activeTexture(gl.kTexture0);
gl.bindTexture(gl.kTexture2D, texture);
gl.texImage2D(gl.kTexture2D, 0, gl.kRGBA, gl.kRGBA, gl.kUnsignedByte,
imageShader.image.imgElement);
if (isWebGl2) {
// Texture REPEAT and MIRROR is only supported in WebGL 2, for
// WebGL 1.0 we let shader compute correct uv coordinates.
gl.texParameteri(gl.kTexture2D, gl.kTextureWrapS,
tileModeToGlWrapping(gl, imageShader.tileModeX));
gl.texParameteri(gl.kTexture2D, gl.kTextureWrapT,
tileModeToGlWrapping(gl, imageShader.tileModeY));
// Mipmapping saves your texture in different resolutions
// so the graphics card can choose which resolution is optimal
// without artifacts.
gl.generateMipmap(gl.kTexture2D);
} else {
// For webgl1, if a texture is not mipmap complete, then the return
// value of a texel fetch is (0, 0, 0, 1), so we have to set
// minifying function to filter.
// See https://www.khronos.org/registry/webgl/specs/1.0.0/#5.13.8.
gl.texParameteri(gl.kTexture2D, gl.kTextureWrapS, gl.kClampToEdge);
gl.texParameteri(gl.kTexture2D, gl.kTextureWrapT, gl.kClampToEdge);
gl.texParameteri(gl.kTexture2D, gl.kTextureMinFilter, gl.kLinear);
}
}
// Finally render triangles.
gl.clear();
final Uint16List? indices = vertices.indices;
if (indices == null) {
gl.drawTriangles(vertexCount, vertices.mode);
} else {
/// If indices are specified to use shared vertices to reduce vertex
/// data transfer, use drawElements to map from vertex indices to
/// triangles.
final Object? indexBuffer = gl.createBuffer();
gl.bindElementArrayBuffer(indexBuffer);
gl.bufferElementData(indices, gl.kStaticDraw);
gl.drawElements(gl.kTriangles, indices.length, gl.kUnsignedShort);
}
if (vao != null) {
gl.unbindVertexArray();
}
context!.save();
context.resetTransform();
gl.drawImage(context, offsetX, offsetY);
context.restore();
}
/// 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.
@override
Object? drawRect(ui.Rect targetRect, GlContext gl, GlProgram glProgram,
NormalizedGradient gradient, int widthInPixels, int heightInPixels) {
drawRectToGl(
targetRect, gl, glProgram, gradient, widthInPixels, heightInPixels);
final Object? image = gl.readPatternData();
gl.bindArrayBuffer(null);
gl.bindElementArrayBuffer(null);
return image;
}
/// Renders a rectangle using given program into an image resource and returns
/// url.
@override
String drawRectToImageUrl(
ui.Rect targetRect,
GlContext gl,
GlProgram glProgram,
NormalizedGradient gradient,
int widthInPixels,
int heightInPixels) {
drawRectToGl(
targetRect, gl, glProgram, gradient, widthInPixels, heightInPixels);
final String imageUrl = gl.toImageUrl();
// Cleanup buffers.
gl.bindArrayBuffer(null);
gl.bindElementArrayBuffer(null);
return imageUrl;
}
/// Renders a rectangle using given program into [GlContext].
///
/// Caller has to cleanup gl array and element array buffers.
void drawRectToGl(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;
final 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.
final Object resolution = gl.getUniformLocation(glProgram.program, 'u_scale');
gl.setUniform4f(resolution, 2.0 / widthInPixels.toDouble(),
-2.0 / heightInPixels.toDouble(), 1, 1);
final Object shift = gl.getUniformLocation(glProgram.program, 'u_shift');
gl.setUniform4f(shift, -1, 1, 0, 0);
// Setup geometry.
final 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',
<dynamic>[0, 2, gl.kFloat, false, 0, 0]);
gl.enableVertexAttribArray(0);
// Setup color buffer.
final Object? colorsBuffer = gl.createBuffer();
gl.bindArrayBuffer(colorsBuffer);
// Buffer kBGRA_8888.
final Int32List colors = Int32List.fromList(<int>[
0xFF00FF00,
0xFF0000FF,
0xFFFFFF00,
0xFF00FFFF,
]);
gl.bufferData(colors, gl.kStaticDraw);
js_util.callMethod(gl.glContext, 'vertexAttribPointer',
<dynamic>[1, 4, gl.kUnsignedByte, true, 0, 0]);
gl.enableVertexAttribArray(1);
final Object? indexBuffer = gl.createBuffer();
gl.bindElementArrayBuffer(indexBuffer);
gl.bufferElementData(VertexShaders.vertexIndicesForRect, gl.kStaticDraw);
if (gl.containsUniform(glProgram.program, 'u_resolution')) {
final 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, VertexShaders.vertexIndicesForRect.length, gl.kUnsignedShort);
}
/// This fragment shader enables Int32List of colors to be passed directly
/// to gl context buffer for rendering by decoding RGBA8888.
/// #version 300 es
/// precision mediump float;
/// in vec4 vColor;
/// out vec4 fragColor;
/// void main() {
/// fragColor = vColor;
/// }
String _writeVerticesFragmentShader() {
final ShaderBuilder builder = ShaderBuilder.fragment(webGLVersion);
builder.floatPrecision = ShaderPrecision.kMedium;
builder.addIn(ShaderType.kVec4, name: 'v_color');
final ShaderMethod method = builder.addMethod('main');
method.addStatement('${builder.fragmentColor.name} = v_color;');
return builder.build();
}
@override
void drawHairline(
html.CanvasRenderingContext2D? _ctx, Float32List positions) {
assert(positions != null); // ignore: unnecessary_null_comparison
final int pointCount = positions.length ~/ 2;
_ctx!.lineWidth = 1.0;
_ctx.beginPath();
final int len = pointCount * 2;
for (int i = 0; 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();
}
}
}
}
}
ui.Rect _computeVerticesBounds(Float32List positions, Matrix4 transform) {
double minValueX, maxValueX, minValueY, maxValueY;
minValueX = maxValueX = positions[0];
minValueY = maxValueY = positions[1];
final int len = positions.length;
for (int i = 2; 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 ui.Rect.zero;
}
minValueX = math.min(minValueX, x);
maxValueX = math.max(maxValueX, x);
minValueY = math.min(minValueY, y);
maxValueY = math.max(maxValueY, y);
}
return _transformBounds(
transform, minValueX, minValueY, maxValueX, maxValueY);
}
ui.Rect _transformBounds(
Matrix4 transform, double left, double top, double right, double bottom) {
final Float32List storage = transform.storage;
final double m0 = storage[0];
final double m1 = storage[1];
final double m4 = storage[4];
final double m5 = storage[5];
final double m12 = storage[12];
final double m13 = storage[13];
final double x0 = (m0 * left) + (m4 * top) + m12;
final double y0 = (m1 * left) + (m5 * top) + m13;
final double x1 = (m0 * right) + (m4 * top) + m12;
final double y1 = (m1 * right) + (m5 * top) + m13;
final double x2 = (m0 * right) + (m4 * bottom) + m12;
final double y2 = (m1 * right) + (m5 * bottom) + m13;
final double x3 = (m0 * left) + (m4 * bottom) + m12;
final double y3 = (m1 * left) + (m5 * bottom) + m13;
return ui.Rect.fromLTRB(
math.min(x0, math.min(x1, math.min(x2, x3))),
math.min(y0, math.min(y1, math.min(y2, y3))),
math.max(x0, math.max(x1, math.max(x2, x3))),
math.max(y0, math.max(y1, math.max(y2, y3))));
}
/// 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);
final double centerX = positions[0];
final 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;
final 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;
}
}