blob: 3b918a17318ae6b96f7ca8866e9aee20f3cfa74a [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.
/// JavaScript API bindings for browser APIs.
///
/// The public surface of this API must be safe to use. In particular, using the
/// API of this library it must not be possible to execute arbitrary code from
/// strings by injecting it into HTML or URLs.
@JS()
library browser_api;
import 'dart:async';
import 'dart:js' as js;
import 'dart:js_util' as js_util;
import 'dart:math' as math;
import 'dart:typed_data';
import 'package:js/js.dart';
import 'package:ui/ui.dart' as ui;
import 'browser_detection.dart';
import 'dom.dart';
import 'platform_dispatcher.dart';
import 'vector_math.dart';
/// Creates JavaScript object populated with [properties].
///
/// This is equivalent to writing `{}` in plain JavaScript.
Object createPlainJsObject([Map<String, Object?>? properties]) {
if (properties != null) {
return js.JsObject.jsify(properties);
} else {
return js_util.newObject<Object>();
}
}
/// Returns true if [object] has property [name], false otherwise.
///
/// This is equivalent to writing `name in object` in plain JavaScript.
bool hasJsProperty(Object object, String name) {
return js_util.hasProperty(object, name);
}
/// Returns the value of property [name] from a JavaScript [object].
///
/// This is equivalent to writing `object.name` in plain JavaScript.
T getJsProperty<T>(Object object, String name) {
return js_util.getProperty<T>(object, name);
}
const Set<String> _safeJsProperties = <String>{
'decoding',
'__flutter_state',
};
/// Sets the value of property [name] on a JavaScript [object].
///
/// This is equivalent to writing `object.name = value` in plain JavaScript.
T setJsProperty<T>(Object object, String name, T value) {
assert(
_safeJsProperties.contains(name),
'Attempted to set property "$name" on a JavaScript object. This property '
'has not been checked for safety. Possible solutions to this problem:\n'
' - Do not set this property.\n'
' - Use a `js_util` API that does the same thing.\n'
' - Ensure that the property is safe then add it to _safeJsProperties set.',
);
return js_util.setProperty<T>(object, name, value);
}
/// Wraps function [f] to be callable from JavaScript.
F allowInterop<F extends Function>(F f) {
return js.allowInterop<F>(f);
}
/// Converts a JavaScript `Promise` into Dart [Future].
Future<T> promiseToFuture<T>(Object jsPromise) {
return js_util.promiseToFuture<T>(jsPromise);
}
/// A function that receives a benchmark [value] labeleb by [name].
typedef OnBenchmark = void Function(String name, double value);
/// Adds an event [listener] of type [type] to the [target].
///
/// [eventOptions] supply additional configuration parameters.
///
/// This is different from [DomElement.addEventListener] in that the listener
/// is added as a plain JavaScript function, as opposed to a Dart function.
///
/// To remove the listener, call [removeJsEventListener].
void addJsEventListener(Object target, String type, Function listener, Object eventOptions) {
js_util.callMethod<void>(
target,
'addEventListener', <dynamic>[
type,
listener,
eventOptions,
]
);
}
/// Removes an event listener that was added using [addJsEventListener].
void removeJsEventListener(Object target, String type, Function listener, Object eventOptions) {
js_util.callMethod<void>(
target,
'removeEventListener', <dynamic>[
type,
listener,
eventOptions,
]
);
}
/// The signature of the `parseFloat` JavaScript function.
typedef _JsParseFloat = num? Function(String source);
/// The JavaScript-side `parseFloat` function.
@JS('parseFloat')
external _JsParseFloat get _jsParseFloat;
/// Parses a string [source] into a double.
///
/// Uses the JavaScript `parseFloat` function instead of Dart's [double.parse]
/// because the latter can't parse strings like "20px".
///
/// Returns null if it fails to parse.
num? parseFloat(String source) {
// Using JavaScript's `parseFloat` here because it can parse values
// like "20px", while Dart's `double.tryParse` fails.
final num? result = _jsParseFloat(source);
if (result == null || result.isNaN) {
return null;
}
return result;
}
final bool supportsFontLoadingApi =
js_util.hasProperty(domWindow, 'FontFace');
final bool supportsFontsClearApi =
js_util.hasProperty(domDocument, 'fonts') &&
js_util.hasProperty(domDocument.fonts!, 'clear');
/// Used to decide if the browser tab still has the focus.
///
/// This information is useful for deciding on the blur behavior.
/// See [DefaultTextEditingStrategy].
///
/// This getter calls the `hasFocus` method of the `Document` interface.
/// See for more details:
/// https://developer.mozilla.org/en-US/docs/Web/API/Document/hasFocus
bool get windowHasFocus =>
js_util.callMethod<bool>(domDocument, 'hasFocus', <dynamic>[]);
/// Parses the font size of [element] and returns the value without a unit.
num? parseFontSize(DomElement element) {
num? fontSize;
if (hasJsProperty(element, 'computedStyleMap')) {
// Use the newer `computedStyleMap` API available on some browsers.
final Object? computedStyleMap =
js_util.callMethod<Object?>(element, 'computedStyleMap', const <Object?>[]);
if (computedStyleMap is Object) {
final Object? fontSizeObject =
js_util.callMethod<Object?>(computedStyleMap, 'get', <Object?>['font-size']);
if (fontSizeObject is Object) {
fontSize = js_util.getProperty<num>(fontSizeObject, 'value');
}
}
}
if (fontSize == null) {
// Fallback to `getComputedStyle`.
final String fontSizeString =
domWindow.getComputedStyle(element).getPropertyValue('font-size');
fontSize = parseFloat(fontSizeString);
}
return fontSize;
}
/// Provides haptic feedback.
void vibrate(int durationMs) {
final DomNavigator navigator = domWindow.navigator;
if (hasJsProperty(navigator, 'vibrate')) {
js_util.callMethod<void>(navigator, 'vibrate', <num>[durationMs]);
}
}
/// Creates a `<canvas>` but anticipates that the result may be null.
///
/// The [DomCanvasElement] factory assumes that element allocation will
/// succeed and will return a non-null element. This is not always true. For
/// example, when Safari on iOS runs out of memory it returns null.
DomCanvasElement? tryCreateCanvasElement(int width, int height) {
final DomCanvasElement? canvas = js_util.callMethod<DomCanvasElement?>(
domDocument,
'createElement',
<dynamic>['CANVAS'],
);
if (canvas == null) {
return null;
}
try {
canvas.width = width;
canvas.height = height;
} catch (e) {
// It seems the tribal knowledge of why we anticipate an exception while
// setting width/height on a non-null canvas and why it's OK to return null
// in this case has been lost. Kudos to the one who can recover it and leave
// a proper comment here!
return null;
}
return canvas;
}
@JS('window.ImageDecoder')
external Object? get _imageDecoderConstructor;
/// Environment variable that allows the developer to opt out of using browser's
/// `ImageDecoder` API, and use the WASM codecs bundled with CanvasKit.
///
/// While all reported severe issues with `ImageDecoder` have been fixed, this
/// API remains relatively new. This option will allow developers to opt out of
/// it, if they hit a severe bug that we did not anticipate.
// TODO(yjbanov): remove this flag once we're fully confident in the new API.
// https://github.com/flutter/flutter/issues/95277
const bool _browserImageDecodingEnabled = bool.fromEnvironment(
'BROWSER_IMAGE_DECODING_ENABLED',
defaultValue: true,
);
/// Whether the current browser supports `ImageDecoder`.
bool browserSupportsImageDecoder =
_browserImageDecodingEnabled &&
_imageDecoderConstructor != null &&
browserEngine == BrowserEngine.blink;
/// Sets the value of [browserSupportsImageDecoder] to its default value.
void debugResetBrowserSupportsImageDecoder() {
browserSupportsImageDecoder =
_imageDecoderConstructor != null;
}
/// Corresponds to JavaScript's `Promise`.
///
/// This type doesn't need any members. Instead, it should be first converted
/// to Dart's [Future] using [promiseToFuture] then interacted with through the
/// [Future] API.
@JS()
@anonymous
@staticInterop
class JsPromise {}
/// Corresponds to the browser's `ImageDecoder` type.
///
/// See also:
///
/// * https://www.w3.org/TR/webcodecs/#imagedecoder-interface
@JS('window.ImageDecoder')
@staticInterop
class ImageDecoder {
external factory ImageDecoder(ImageDecoderOptions options);
}
extension ImageDecoderExtension on ImageDecoder {
external ImageTrackList get tracks;
external bool get complete;
external JsPromise decode(DecodeOptions options);
external void close();
}
/// Options passed to the `ImageDecoder` constructor.
///
/// See also:
///
/// * https://www.w3.org/TR/webcodecs/#imagedecoderinit-interface
@JS()
@anonymous
@staticInterop
class ImageDecoderOptions {
external factory ImageDecoderOptions({
required String type,
required Uint8List data,
required String premultiplyAlpha,
required int? desiredWidth,
required int? desiredHeight,
required String colorSpaceConversion,
required bool preferAnimation,
});
}
/// The result of [ImageDecoder.decode].
///
/// See also:
///
/// * https://www.w3.org/TR/webcodecs/#imagedecoderesult-interface
@JS()
@anonymous
@staticInterop
class DecodeResult {}
extension DecodeResultExtension on DecodeResult {
external VideoFrame get image;
external bool get complete;
}
/// Options passed to [ImageDecoder.decode].
///
/// See also:
///
/// * https://www.w3.org/TR/webcodecs/#dictdef-imagedecodeoptions
@JS()
@anonymous
@staticInterop
class DecodeOptions {
external factory DecodeOptions({
required int frameIndex,
});
}
/// The only frame in a static image, or one of the frames in an animated one.
///
/// This class maps to the `VideoFrame` type provided by the browser.
///
/// See also:
///
/// * https://www.w3.org/TR/webcodecs/#videoframe-interface
@JS()
@anonymous
class VideoFrame implements DomCanvasImageSource {
external int allocationSize();
external JsPromise copyTo(Uint8List destination);
external String? get format;
external int get codedWidth;
external int get codedHeight;
external int get displayWidth;
external int get displayHeight;
external int? get duration;
external void close();
}
/// Corresponds to the browser's `ImageTrackList` type.
///
/// See also:
///
/// * https://www.w3.org/TR/webcodecs/#imagetracklist-interface
@JS()
@anonymous
@staticInterop
class ImageTrackList {}
extension ImageTrackListExtension on ImageTrackList {
external JsPromise get ready;
external ImageTrack? get selectedTrack;
}
/// Corresponds to the browser's `ImageTrack` type.
///
/// See also:
///
/// * https://www.w3.org/TR/webcodecs/#imagetrack
@JS()
@anonymous
@staticInterop
class ImageTrack {}
extension ImageTrackExtension on ImageTrack {
external int get repetitionCount;
external int get frameCount;
}
void scaleCanvas2D(Object context2d, num x, num y) {
js_util.callMethod<void>(context2d, 'scale', <dynamic>[x, y]);
}
void drawImageCanvas2D(Object context2d, Object imageSource, num width, num height) {
js_util.callMethod<void>(context2d, 'drawImage', <dynamic>[
imageSource,
width,
height,
]);
}
void vertexAttribPointerGlContext(
Object glContext,
Object index,
num size,
Object type,
bool normalized,
num stride,
num offset,
) {
js_util.callMethod<void>(glContext, 'vertexAttribPointer', <dynamic>[
index,
size,
type,
normalized,
stride,
offset,
]);
}
/// Compiled and cached gl program.
class GlProgram {
final Object program;
GlProgram(this.program);
}
/// JS Interop helper for webgl apis.
class GlContext {
final Object glContext;
final bool isOffscreen;
Object? _kCompileStatus;
Object? _kArrayBuffer;
Object? _kElementArrayBuffer;
Object? _kStaticDraw;
Object? _kFloat;
Object? _kColorBufferBit;
Object? _kTexture2D;
Object? _kTextureWrapS;
Object? _kTextureWrapT;
Object? _kRepeat;
Object? _kClampToEdge;
Object? _kMirroredRepeat;
Object? _kTriangles;
Object? _kLinkStatus;
Object? _kUnsignedByte;
Object? _kUnsignedShort;
Object? _kRGBA;
Object? _kLinear;
Object? _kTextureMinFilter;
int? _kTexture0;
Object? _canvas;
int? _widthInPixels;
int? _heightInPixels;
static late Map<String, GlProgram?> _programCache;
factory GlContext(OffScreenCanvas offScreenCanvas) {
return OffScreenCanvas.supported
? GlContext._fromOffscreenCanvas(offScreenCanvas.offScreenCanvas!)
: GlContext._fromCanvasElement(
offScreenCanvas.canvasElement!, webGLVersion == WebGLVersion.webgl1);
}
GlContext._fromOffscreenCanvas(DomOffscreenCanvas canvas)
: glContext = canvas.getContext('webgl2', <String, dynamic>{'premultipliedAlpha': false})!,
isOffscreen = true {
_programCache = <String, GlProgram?>{};
_canvas = canvas;
}
GlContext._fromCanvasElement(DomCanvasElement canvas, bool useWebGl1)
: glContext = canvas.getContext(useWebGl1 ? 'webgl' : 'webgl2',
<String, dynamic>{'premultipliedAlpha': false})!,
isOffscreen = false {
_programCache = <String, GlProgram?>{};
_canvas = canvas;
}
void setViewportSize(int width, int height) {
_widthInPixels = width;
_heightInPixels = height;
}
/// Draws Gl context contents to canvas context.
void drawImage(DomCanvasRenderingContext2D context,
double left, double top) {
// Actual size of canvas may be larger than viewport size. Use
// source/destination to draw part of the image data.
js_util.callMethod<void>(context, 'drawImage',
<dynamic>[_canvas, 0, 0, _widthInPixels, _heightInPixels,
left, top, _widthInPixels, _heightInPixels]);
}
GlProgram cacheProgram(
String vertexShaderSource, String fragmentShaderSource) {
final String cacheKey = '$vertexShaderSource||$fragmentShaderSource';
GlProgram? cachedProgram = _programCache[cacheKey];
if (cachedProgram == null) {
// Create and compile shaders.
final Object vertexShader = compileShader('VERTEX_SHADER', vertexShaderSource);
final Object fragmentShader =
compileShader('FRAGMENT_SHADER', fragmentShaderSource);
// Create a gl program and link shaders.
final Object program = createProgram();
attachShader(program, vertexShader);
attachShader(program, fragmentShader);
linkProgram(program);
cachedProgram = GlProgram(program);
_programCache[cacheKey] = cachedProgram;
}
return cachedProgram;
}
Object compileShader(String shaderType, String source) {
final Object? shader = _createShader(shaderType);
if (shader == null) {
throw Exception(error);
}
js_util.callMethod<void>(glContext, 'shaderSource', <dynamic>[shader, source]);
js_util.callMethod<void>(glContext, 'compileShader', <dynamic>[shader]);
final bool shaderStatus = js_util.callMethod<bool>(
glContext,
'getShaderParameter',
<dynamic>[shader, compileStatus],
);
if (!shaderStatus) {
throw Exception('Shader compilation failed: ${getShaderInfoLog(shader)}');
}
return shader;
}
Object createProgram() =>
js_util.callMethod<Object>(glContext, 'createProgram', const <dynamic>[]);
void attachShader(Object? program, Object shader) {
js_util.callMethod<void>(glContext, 'attachShader', <dynamic>[program, shader]);
}
void linkProgram(Object program) {
js_util.callMethod<void>(glContext, 'linkProgram', <dynamic>[program]);
final bool programStatus = js_util.callMethod<bool>(
glContext,
'getProgramParameter',
<dynamic>[program, kLinkStatus],
);
if (!programStatus) {
throw Exception(getProgramInfoLog(program));
}
}
void useProgram(GlProgram program) {
js_util.callMethod<void>(glContext, 'useProgram', <dynamic>[program.program]);
}
Object? createBuffer() =>
js_util.callMethod(glContext, 'createBuffer', const <dynamic>[]);
void bindArrayBuffer(Object? buffer) {
js_util.callMethod<void>(glContext, 'bindBuffer', <dynamic>[kArrayBuffer, buffer]);
}
Object? createVertexArray() =>
js_util.callMethod(glContext, 'createVertexArray', const <dynamic>[]);
void bindVertexArray(Object vertexObjectArray) {
js_util.callMethod<void>(glContext, 'bindVertexArray',
<dynamic>[vertexObjectArray]);
}
void unbindVertexArray() {
js_util.callMethod<void>(glContext, 'bindVertexArray',
<dynamic>[null]);
}
void bindElementArrayBuffer(Object? buffer) {
js_util.callMethod<void>(glContext, 'bindBuffer', <dynamic>[kElementArrayBuffer, buffer]);
}
Object? createTexture() =>
js_util.callMethod(glContext, 'createTexture', const <dynamic>[]);
void generateMipmap(dynamic target) =>
js_util.callMethod(glContext, 'generateMipmap', <dynamic>[target]);
void bindTexture(dynamic target, Object? buffer) {
js_util.callMethod<void>(glContext, 'bindTexture', <dynamic>[target, buffer]);
}
void activeTexture(int textureUnit) {
js_util.callMethod<void>(glContext, 'activeTexture', <dynamic>[textureUnit]);
}
void texImage2D(dynamic target, int level, dynamic internalFormat,
dynamic format, dynamic dataType,
dynamic pixels, {int? width, int? height, int border = 0}) {
if (width == null) {
js_util.callMethod<void>(glContext, 'texImage2D', <dynamic>[
target, level, internalFormat, format, dataType, pixels]);
} else {
js_util.callMethod<void>(glContext, 'texImage2D', <dynamic>[
target, level, internalFormat, width, height, border, format, dataType,
pixels]);
}
}
void texParameteri(dynamic target, dynamic parameterName, dynamic value) {
js_util.callMethod<void>(glContext, 'texParameteri', <dynamic>[
target, parameterName, value]);
}
void deleteBuffer(Object buffer) {
js_util.callMethod<void>(glContext, 'deleteBuffer', <dynamic>[buffer]);
}
void bufferData(TypedData? data, dynamic type) {
js_util.callMethod<void>(glContext, 'bufferData', <dynamic>[kArrayBuffer, data, type]);
}
void bufferElementData(TypedData? data, dynamic type) {
js_util.callMethod<void>(glContext, 'bufferData', <dynamic>[kElementArrayBuffer, data, type]);
}
void enableVertexAttribArray(dynamic index) {
js_util.callMethod<void>(glContext, 'enableVertexAttribArray', <dynamic>[index]);
}
/// Clear background.
void clear() {
js_util.callMethod<void>(glContext, 'clear', <dynamic>[kColorBufferBit]);
}
/// Destroys gl context.
void dispose() {
final Object? loseContextExtension = _getExtension('WEBGL_lose_context');
if (loseContextExtension != null) {
js_util.callMethod<void>(
loseContextExtension,
'loseContext',
const <dynamic>[],
);
}
}
void deleteProgram(Object program) {
js_util.callMethod<void>(glContext, 'deleteProgram', <dynamic>[program]);
}
void deleteShader(Object shader) {
js_util.callMethod<void>(glContext, 'deleteShader', <dynamic>[shader]);
}
Object? _getExtension(String extensionName) =>
js_util.callMethod<Object?>(glContext, 'getExtension', <dynamic>[extensionName]);
void drawTriangles(int triangleCount, ui.VertexMode vertexMode) {
final dynamic mode = _triangleTypeFromMode(vertexMode);
js_util.callMethod<void>(glContext, 'drawArrays', <dynamic>[mode, 0, triangleCount]);
}
void drawElements(dynamic type, int indexCount, dynamic indexType) {
js_util.callMethod<void>(glContext, 'drawElements', <dynamic>[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<void>(glContext, 'viewport', <dynamic>[x, y, width, height]);
}
Object _triangleTypeFromMode(ui.VertexMode mode) {
switch (mode) {
case ui.VertexMode.triangles:
return kTriangles;
case ui.VertexMode.triangleFan:
return kTriangleFan;
case ui.VertexMode.triangleStrip:
return kTriangleStrip;
}
}
Object? _createShader(String shaderType) => js_util.callMethod(
glContext, 'createShader', <Object?>[js_util.getProperty<Object?>(glContext, shaderType)]);
/// Error state of gl context.
Object? get error => js_util.callMethod(glContext, 'getError', const <dynamic>[]);
/// Shader compiler error, if this returns [kFalse], to get details use
/// [getShaderInfoLog].
Object? get compileStatus =>
_kCompileStatus ??= js_util.getProperty(glContext, 'COMPILE_STATUS');
Object? get kArrayBuffer =>
_kArrayBuffer ??= js_util.getProperty(glContext, 'ARRAY_BUFFER');
Object? get kElementArrayBuffer =>
_kElementArrayBuffer ??= js_util.getProperty(glContext,
'ELEMENT_ARRAY_BUFFER');
Object get kLinkStatus =>
_kLinkStatus ??= js_util.getProperty<Object>(glContext, 'LINK_STATUS');
Object get kFloat => _kFloat ??= js_util.getProperty<Object>(glContext, 'FLOAT');
Object? get kRGBA => _kRGBA ??= js_util.getProperty(glContext, 'RGBA');
Object get kUnsignedByte =>
_kUnsignedByte ??= js_util.getProperty<Object>(glContext, 'UNSIGNED_BYTE');
Object? get kUnsignedShort =>
_kUnsignedShort ??= js_util.getProperty(glContext, 'UNSIGNED_SHORT');
Object? get kStaticDraw =>
_kStaticDraw ??= js_util.getProperty(glContext, 'STATIC_DRAW');
Object get kTriangles =>
_kTriangles ??= js_util.getProperty<Object>(glContext, 'TRIANGLES');
Object get kTriangleFan =>
_kTriangles ??= js_util.getProperty<Object>(glContext, 'TRIANGLE_FAN');
Object get kTriangleStrip =>
_kTriangles ??= js_util.getProperty<Object>(glContext, 'TRIANGLE_STRIP');
Object? get kColorBufferBit =>
_kColorBufferBit ??= js_util.getProperty(glContext, 'COLOR_BUFFER_BIT');
Object? get kTexture2D =>
_kTexture2D ??= js_util.getProperty(glContext, 'TEXTURE_2D');
int get kTexture0 =>
_kTexture0 ??= js_util.getProperty<int>(glContext, 'TEXTURE0');
Object? get kTextureWrapS =>
_kTextureWrapS ??= js_util.getProperty(glContext, 'TEXTURE_WRAP_S');
Object? get kTextureWrapT =>
_kTextureWrapT ??= js_util.getProperty(glContext, 'TEXTURE_WRAP_T');
Object? get kRepeat =>
_kRepeat ??= js_util.getProperty(glContext, 'REPEAT');
Object? get kClampToEdge =>
_kClampToEdge ??= js_util.getProperty(glContext, 'CLAMP_TO_EDGE');
Object? get kMirroredRepeat =>
_kMirroredRepeat ??= js_util.getProperty(glContext, 'MIRRORED_REPEAT');
Object? get kLinear =>
_kLinear ??= js_util.getProperty(glContext, 'LINEAR');
Object? get kTextureMinFilter =>
_kTextureMinFilter ??= js_util.getProperty(glContext,
'TEXTURE_MIN_FILTER');
/// Returns reference to uniform in program.
Object getUniformLocation(Object program, String uniformName) {
final Object? res = js_util
.callMethod(glContext, 'getUniformLocation', <dynamic>[program, uniformName]);
if (res == null) {
throw Exception('$uniformName not found');
} else {
return res;
}
}
/// Returns true if uniform exists.
bool containsUniform(Object program, String uniformName) {
final Object? res = js_util
.callMethod(glContext, 'getUniformLocation', <dynamic>[program, uniformName]);
return res != null;
}
/// Returns reference to uniform in program.
Object getAttributeLocation(Object program, String attribName) {
final Object? res = js_util
.callMethod(glContext, 'getAttribLocation', <dynamic>[program, attribName]);
if (res == null) {
throw Exception('$attribName not found');
} else {
return res;
}
}
/// Sets float uniform value.
void setUniform1f(Object uniform, double value) {
js_util.callMethod<void>(glContext, 'uniform1f', <dynamic>[uniform, value]);
}
/// Sets vec2 uniform values.
void setUniform2f(Object uniform, double value1, double value2) {
js_util.callMethod<void>(glContext, 'uniform2f', <dynamic>[uniform, value1, value2]);
}
/// Sets vec4 uniform values.
void setUniform4f(Object uniform, double value1, double value2, double value3,
double value4) {
js_util.callMethod<void>(
glContext, 'uniform4f', <dynamic>[uniform, value1, value2, value3, value4]);
}
/// Sets mat4 uniform values.
void setUniformMatrix4fv(Object uniform, bool transpose, Float32List value) {
js_util.callMethod<void>(
glContext, 'uniformMatrix4fv', <dynamic>[uniform, transpose, value]);
}
/// Shader compile error log.
Object? getShaderInfoLog(Object glShader) {
return js_util.callMethod(glContext, 'getShaderInfoLog', <dynamic>[glShader]);
}
/// Errors that occurred during failed linking or validation of program
/// objects. Typically called after [linkProgram].
String? getProgramInfoLog(Object glProgram) {
return js_util.callMethod<String?>(glContext, 'getProgramInfoLog', <dynamic>[glProgram]);
}
int? get drawingBufferWidth =>
js_util.getProperty<int?>(glContext, 'drawingBufferWidth');
int? get drawingBufferHeight =>
js_util.getProperty<int?>(glContext, 'drawingBufferWidth');
/// Reads gl contents as image data.
///
/// Warning: data is read bottom up (flipped).
DomImageData readImageData() {
const int kBytesPerPixel = 4;
final int bufferWidth = _widthInPixels!;
final int bufferHeight = _heightInPixels!;
if (browserEngine == BrowserEngine.webkit ||
browserEngine == BrowserEngine.firefox) {
final Uint8List pixels =
Uint8List(bufferWidth * bufferHeight * kBytesPerPixel);
js_util.callMethod<void>(glContext, 'readPixels',
<dynamic>[0, 0, bufferWidth, bufferHeight, kRGBA, kUnsignedByte, pixels]);
return createDomImageData(
Uint8ClampedList.fromList(pixels), bufferWidth, bufferHeight);
} else {
final Uint8ClampedList pixels =
Uint8ClampedList(bufferWidth * bufferHeight * kBytesPerPixel);
js_util.callMethod<void>(glContext, 'readPixels',
<dynamic>[0, 0, bufferWidth, bufferHeight, kRGBA, kUnsignedByte, pixels]);
return createDomImageData(pixels, bufferWidth, bufferHeight);
}
}
/// Returns image data in a form that can be used to create Canvas
/// context patterns.
Object? readPatternData(bool isOpaque) {
// When using OffscreenCanvas and transferToImageBitmap is supported by
// browser create ImageBitmap otherwise use more expensive canvas
// allocation. However, transferToImageBitmap does not properly preserve
// the alpha channel, so only use it if the pattern is opaque.
if (_canvas != null &&
js_util.hasProperty(_canvas!, 'transferToImageBitmap') &&
isOpaque) {
// TODO(yjbanov): find out why we need to call getContext and ignore the return value.
js_util.callMethod<void>(_canvas!, 'getContext', <dynamic>['webgl2']);
final Object? imageBitmap = js_util.callMethod(_canvas!, 'transferToImageBitmap',
<dynamic>[]);
return imageBitmap;
} else {
final DomCanvasElement canvas = createDomCanvasElement(width: _widthInPixels, height: _heightInPixels);
final DomCanvasRenderingContext2D ctx = canvas.context2D;
drawImage(ctx, 0, 0);
return canvas;
}
}
/// Returns image data in data url format.
String toImageUrl() {
final DomCanvasElement canvas = createDomCanvasElement(width: _widthInPixels, height: _heightInPixels);
final DomCanvasRenderingContext2D ctx = canvas.context2D;
drawImage(ctx, 0, 0);
final String dataUrl = canvas.toDataURL();
canvas.width = 0;
canvas.height = 0;
return dataUrl;
}
}
// ignore: avoid_classes_with_only_static_members
/// 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;
_cachedContext = null;
_offScreenCanvas?.dispose();
}
static GlContext? createGlContext(int widthInPixels, int heightInPixels) {
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);
_cachedContext ??= GlContext(_offScreenCanvas!);
_cachedContext!.setViewportSize(widthInPixels, heightInPixels);
return _cachedContext;
}
}
void setupVertexTransforms(
GlContext gl,
GlProgram glProgram,
double offsetX,
double offsetY,
double widthInPixels,
double heightInPixels,
Matrix4 transform) {
final Object transformUniform =
gl.getUniformLocation(glProgram.program, 'u_ctransform');
final Matrix4 transformAtOffset = transform.clone()
..translate(-offsetX, -offsetY);
gl.setUniformMatrix4fv(transformUniform, false, transformAtOffset.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);
}
void setupTextureTransform(
GlContext gl, GlProgram glProgram, double offsetx, double offsety, double sx, double sy) {
final Object scalar = gl.getUniformLocation(glProgram.program, 'u_textransform');
gl.setUniform4f(scalar, sx, sy, offsetx, offsety);
}
void bufferVertexData(GlContext gl, Float32List positions,
double devicePixelRatio) {
if (devicePixelRatio == 1.0) {
gl.bufferData(positions, gl.kStaticDraw);
} else {
final int length = positions.length;
final Float32List scaledList = Float32List(length);
for (int i = 0; i < length; i++) {
scaledList[i] = positions[i] * devicePixelRatio;
}
gl.bufferData(scaledList, gl.kStaticDraw);
}
}
dynamic tileModeToGlWrapping(GlContext gl, ui.TileMode tileMode) {
switch (tileMode) {
case ui.TileMode.clamp:
return gl.kClampToEdge;
case ui.TileMode.decal:
return gl.kClampToEdge;
case ui.TileMode.mirror:
return gl.kMirroredRepeat;
case ui.TileMode.repeated:
return gl.kRepeat;
}
}
/// Polyfill for DomOffscreenCanvas that is not supported on some browsers.
class OffScreenCanvas {
DomOffscreenCanvas? offScreenCanvas;
DomCanvasElement? canvasElement;
int width;
int height;
static bool? _supported;
OffScreenCanvas(this.width, this.height) {
if (OffScreenCanvas.supported) {
offScreenCanvas = createDomOffscreenCanvas(width, height);
} else {
canvasElement = createDomCanvasElement(
width: width,
height: height,
);
canvasElement!.className = 'gl-canvas';
_updateCanvasCssSize(canvasElement!);
}
}
void _updateCanvasCssSize(DomCanvasElement element) {
final double cssWidth = width / EnginePlatformDispatcher.browserDevicePixelRatio;
final double cssHeight = height / EnginePlatformDispatcher.browserDevicePixelRatio;
element.style
..position = 'absolute'
..width = '${cssWidth}px'
..height = '${cssHeight}px';
}
void resize(int requestedWidth, int requestedHeight) {
if(requestedWidth != width && requestedHeight != height) {
width = requestedWidth;
height = requestedHeight;
if(offScreenCanvas != null) {
offScreenCanvas!.width = requestedWidth;
offScreenCanvas!.height = requestedHeight;
} else if (canvasElement != null) {
canvasElement!.width = requestedWidth;
canvasElement!.height = requestedHeight;
_updateCanvasCssSize(canvasElement!);
}
}
}
void dispose() {
offScreenCanvas = null;
canvasElement = null;
}
/// Returns CanvasRenderContext2D or OffscreenCanvasRenderingContext2D to
/// paint into.
Object? getContext2d() {
return offScreenCanvas != null
? offScreenCanvas!.getContext('2d')
: canvasElement!.getContext('2d');
}
/// Feature detection for transferToImageBitmap on OffscreenCanvas.
bool get transferToImageBitmapSupported =>
js_util.hasProperty(offScreenCanvas!, 'transferToImageBitmap');
/// Creates an ImageBitmap object from the most recently rendered image
/// of the OffscreenCanvas.
///
/// !Warning API still in experimental status, feature detect before using.
Object? transferToImageBitmap() {
return js_util.callMethod(offScreenCanvas!, 'transferToImageBitmap',
<dynamic>[]);
}
/// Draws canvas contents to a rendering context.
void transferImage(Object targetContext) {
// Actual size of canvas may be larger than viewport size. Use
// source/destination to draw part of the image data.
js_util.callMethod<void>(targetContext, 'drawImage',
<dynamic>[offScreenCanvas ?? canvasElement!, 0, 0, width, height,
0, 0, width, height]);
}
/// Converts canvas contents to an image and returns as data URL.
Future<String> toDataUrl() {
final Completer<String> completer = Completer<String>();
if (offScreenCanvas != null) {
offScreenCanvas!.convertToBlob().then((DomBlob value) {
final DomFileReader fileReader = createDomFileReader();
fileReader.addEventListener('load', allowInterop((DomEvent event) {
completer.complete(
js_util.getProperty<String>(js_util.getProperty<Object>(event, 'target'), 'result'),
);
}));
fileReader.readAsDataURL(value);
});
return completer.future;
} else {
return Future<String>.value(canvasElement!.toDataURL());
}
}
/// Draws an image to canvas for both offscreen canvas canvas context2d.
void drawImage(Object image, int x, int y, int width, int height) {
js_util.callMethod<void>(
getContext2d()!, 'drawImage', <dynamic>[image, x, y, width, height]);
}
/// Feature detects OffscreenCanvas.
static bool get supported => _supported ??=
js_util.hasProperty(domWindow, 'OffscreenCanvas');
}