blob: 9c444929ea4312a5f990f5a7cf28680446a833c6 [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:js_interop';
import 'package:ui/ui.dart' as ui;
import '../dom.dart';
import '../window.dart';
/// A visible (on-screen) canvas that can display bitmaps produced by CanvasKit
/// in the (off-screen) SkSurface which is backed by an OffscreenCanvas.
///
/// In a typical frame, the content will be rendered via CanvasKit in an
/// OffscreenCanvas, and then the contents will be transferred to the
/// RenderCanvas via `transferFromImageBitmap()`.
///
/// If we need more RenderCanvases, for example in the case where there are
/// platform views and we need overlays to render the frame correctly, then
/// we will create multiple RenderCanvases, but crucially still only have
/// one OffscreenCanvas which transfers bitmaps to all of the RenderCanvases.
///
/// To render into the OffscreenCanvas with CanvasKit we need to create a
/// WebGL context, which is not only expensive, but the browser has a limit
/// on the maximum amount of WebGL contexts which can be live at once. Using
/// a single OffscreenCanvas and multiple RenderCanvases allows us to only
/// create a single WebGL context.
class RenderCanvas {
RenderCanvas() {
canvasElement.setAttribute('aria-hidden', 'true');
canvasElement.style.position = 'absolute';
_updateLogicalHtmlCanvasSize();
htmlElement.append(canvasElement);
}
/// The root HTML element for this canvas.
///
/// This element contains the canvas used to draw the UI. Unlike the canvas,
/// this element is permanent. It is never replaced or deleted, until this
/// canvas is disposed of via [dispose].
///
/// Conversely, the canvas that lives inside this element can be swapped, for
/// example, when the screen size changes, or when the WebGL context is lost
/// due to the browser tab becoming dormant.
final DomElement htmlElement = createDomElement('flt-canvas-container');
/// The underlying `<canvas>` element used to display the pixels.
final DomCanvasElement canvasElement = createDomCanvasElement();
int _pixelWidth = 0;
int _pixelHeight = 0;
late final DomCanvasRenderingContextBitmapRenderer renderContext =
canvasElement.contextBitmapRenderer;
late final DomCanvasRenderingContext2D renderContext2d =
canvasElement.context2D;
double _currentDevicePixelRatio = -1;
/// Sets the CSS size of the canvas so that canvas pixels are 1:1 with device
/// pixels.
///
/// The logical size of the canvas is not based on the size of the window
/// but on the size of the canvas, which, due to `ceil()` above, may not be
/// the same as the window. We do not round/floor/ceil the logical size as
/// CSS pixels can contain more than one physical pixel and therefore to
/// match the size of the window precisely we use the most precise floating
/// point value we can get.
void _updateLogicalHtmlCanvasSize() {
final double logicalWidth = _pixelWidth / window.devicePixelRatio;
final double logicalHeight = _pixelHeight / window.devicePixelRatio;
final DomCSSStyleDeclaration style = canvasElement.style;
style.width = '${logicalWidth}px';
style.height = '${logicalHeight}px';
_currentDevicePixelRatio = window.devicePixelRatio;
}
/// Render the given [bitmap] with this [RenderCanvas].
///
/// The canvas will be resized to accomodate the bitmap immediately before
/// rendering it.
void render(DomImageBitmap bitmap) {
_ensureSize(ui.Size(bitmap.width.toDartDouble, bitmap.height.toDartDouble));
renderContext.transferFromImageBitmap(bitmap);
}
void renderWithNoBitmapSupport(
DomCanvasImageSource imageSource,
int sourceHeight,
ui.Size size,
) {
_ensureSize(size);
renderContext2d.drawImage(
imageSource,
0,
sourceHeight - size.height,
size.width,
size.height,
0,
0,
size.width,
size.height,
);
}
/// Ensures that this canvas can draw a frame of the given [size].
void _ensureSize(ui.Size size) {
// Check if the frame is the same size as before, and if so, we don't need
// to resize the canvas.
if (size.width.ceil() == _pixelWidth &&
size.height.ceil() == _pixelHeight) {
// The existing canvas doesn't need to be resized (unless the device pixel
// ratio changed).
if (window.devicePixelRatio != _currentDevicePixelRatio) {
_updateLogicalHtmlCanvasSize();
}
return;
}
// If the canvas is too large or too small, resize it to the exact size of
// the frame. We cannot allow the canvas to be larger than the screen
// because then when we call `transferFromImageBitmap()` the bitmap will
// be scaled to cover the entire canvas.
_pixelWidth = size.width.ceil();
_pixelHeight = size.height.ceil();
canvasElement.width = _pixelWidth.toDouble();
canvasElement.height = _pixelHeight.toDouble();
_updateLogicalHtmlCanvasSize();
}
void dispose() {
htmlElement.remove();
}
}