blob: dcf9e2a7c6d8630f7c9d165d3eb75b9d24d18efc [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.
// TODO(yjbanov): optimization opportunities (see also houdini_painter.js)
// - collapse non-drawing paint operations
// - avoid producing DOM-based clips if there is no text
// - evaluate using stylesheets for static CSS properties
// - evaluate reusing houdini canvases
// @dart = 2.6
part of engine;
/// A canvas that renders to a combination of HTML DOM and CSS Custom Paint API.
///
/// This canvas produces paint commands for houdini_painter.js to apply. This
/// class must be kept in sync with houdini_painter.js.
class HoudiniCanvas extends EngineCanvas with SaveElementStackTracking {
@override
final html.Element rootElement = html.Element.tag('flt-houdini');
/// The rectangle positioned relative to the parent layer's coordinate system
/// where this canvas paints.
///
/// Painting outside the bounds of this rectangle is cropped.
final ui.Rect bounds;
HoudiniCanvas(this.bounds) {
// TODO(yjbanov): would it be faster to specify static values in a
// stylesheet and let the browser apply them?
rootElement.style
..position = 'absolute'
..top = '0'
..left = '0'
..width = '${bounds.size.width}px'
..height = '${bounds.size.height}px'
..backgroundImage = 'paint(flt)';
}
/// Prepare to reuse this canvas by clearing it's current contents.
@override
void clear() {
super.clear();
_serializedCommands = <List<dynamic>>[];
// TODO(yjbanov): we should measure if reusing old elements is beneficial.
domRenderer.clearDom(rootElement);
}
/// Paint commands serialized for sending to the CSS custom painter.
List<List<dynamic>> _serializedCommands = <List<dynamic>>[];
void apply(PaintCommand command) {
// Some commands are applied purely in HTML DOM and do not need to be
// serialized.
if (command is! PaintDrawParagraph &&
command is! PaintDrawImageRect &&
command is! PaintTransform) {
command.serializeToCssPaint(_serializedCommands);
}
command.apply(this);
}
/// Sends the paint commands to the CSS custom painter for painting.
void commit() {
if (_serializedCommands.isNotEmpty) {
rootElement.style.setProperty('--flt', json.encode(_serializedCommands));
} else {
rootElement.style.removeProperty('--flt');
}
}
@override
void clipRect(ui.Rect rect) {
final html.Element clip = html.Element.tag('flt-clip-rect');
final String cssTransform = matrix4ToCssTransform(
transformWithOffset(currentTransform, ui.Offset(rect.left, rect.top)));
clip.style
..overflow = 'hidden'
..position = 'absolute'
..transform = cssTransform
..width = '${rect.width}px'
..height = '${rect.height}px';
// The clipping element will translate the coordinate system as well, which
// is not what a clip should do. To offset that we translate in the opposite
// direction.
super.translate(-rect.left, -rect.top);
currentElement.append(clip);
pushElement(clip);
}
@override
void clipRRect(ui.RRect rrect) {
final ui.Rect outer = rrect.outerRect;
if (rrect.isRect) {
clipRect(outer);
return;
}
final html.Element clip = html.Element.tag('flt-clip-rrect');
final html.CssStyleDeclaration style = clip.style;
style
..overflow = 'hidden'
..position = 'absolute'
..transform = 'translate(${outer.left}px, ${outer.right}px)'
..width = '${outer.width}px'
..height = '${outer.height}px';
if (rrect.tlRadiusY == rrect.tlRadiusX) {
style.borderTopLeftRadius = '${rrect.tlRadiusX}px';
} else {
style.borderTopLeftRadius = '${rrect.tlRadiusX}px ${rrect.tlRadiusY}px';
}
if (rrect.trRadiusY == rrect.trRadiusX) {
style.borderTopRightRadius = '${rrect.trRadiusX}px';
} else {
style.borderTopRightRadius = '${rrect.trRadiusX}px ${rrect.trRadiusY}px';
}
if (rrect.brRadiusY == rrect.brRadiusX) {
style.borderBottomRightRadius = '${rrect.brRadiusX}px';
} else {
style.borderBottomRightRadius =
'${rrect.brRadiusX}px ${rrect.brRadiusY}px';
}
if (rrect.blRadiusY == rrect.blRadiusX) {
style.borderBottomLeftRadius = '${rrect.blRadiusX}px';
} else {
style.borderBottomLeftRadius =
'${rrect.blRadiusX}px ${rrect.blRadiusY}px';
}
// The clipping element will translate the coordinate system as well, which
// is not what a clip should do. To offset that we translate in the opposite
// direction.
super.translate(-rrect.left, -rrect.top);
currentElement.append(clip);
pushElement(clip);
}
@override
void clipPath(ui.Path path) {
// TODO(yjbanov): implement.
}
@override
void drawColor(ui.Color color, ui.BlendMode blendMode) {
// Drawn using CSS Paint.
}
@override
void drawLine(ui.Offset p1, ui.Offset p2, SurfacePaintData paint) {
// Drawn using CSS Paint.
}
@override
void drawPaint(SurfacePaintData paint) {
// Drawn using CSS Paint.
}
@override
void drawRect(ui.Rect rect, SurfacePaintData paint) {
// Drawn using CSS Paint.
}
@override
void drawRRect(ui.RRect rrect, SurfacePaintData paint) {
// Drawn using CSS Paint.
}
@override
void drawDRRect(ui.RRect outer, ui.RRect inner, SurfacePaintData paint) {
// Drawn using CSS Paint.
}
@override
void drawOval(ui.Rect rect, SurfacePaintData paint) {
// Drawn using CSS Paint.
}
@override
void drawCircle(ui.Offset c, double radius, SurfacePaintData paint) {
// Drawn using CSS Paint.
}
@override
void drawPath(ui.Path path, SurfacePaintData paint) {
// Drawn using CSS Paint.
}
@override
void drawShadow(ui.Path path, ui.Color color, double elevation,
bool transparentOccluder) {
// Drawn using CSS Paint.
}
@override
void drawImage(ui.Image image, ui.Offset p, SurfacePaintData paint) {
// TODO(yjbanov): implement.
}
@override
void drawImageRect(
ui.Image image, ui.Rect src, ui.Rect dst, SurfacePaintData paint) {
// TODO(yjbanov): implement src rectangle
final HtmlImage htmlImage = image;
final html.Element imageBox = html.Element.tag('flt-img');
final String cssTransform = matrix4ToCssTransform(
transformWithOffset(currentTransform, ui.Offset(dst.left, dst.top)));
imageBox.style
..position = 'absolute'
..transformOrigin = '0 0 0'
..width = '${dst.width.toInt()}px'
..height = '${dst.height.toInt()}px'
..transform = cssTransform
..backgroundImage = 'url(${htmlImage.imgElement.src})'
..backgroundRepeat = 'norepeat'
..backgroundSize = '${dst.width}px ${dst.height}px';
currentElement.append(imageBox);
}
@override
void drawParagraph(ui.Paragraph paragraph, ui.Offset offset) {
final html.Element paragraphElement =
_drawParagraphElement(paragraph, offset, transform: currentTransform);
currentElement.append(paragraphElement);
}
@override
void drawVertices(
ui.Vertices vertices, ui.BlendMode blendMode, SurfacePaintData paint) {
// TODO(flutter_web): implement.
}
@override
void drawPoints(ui.PointMode pointMode, Float32List points, SurfacePaintData paint) {
// TODO(flutter_web): implement.
}
@override
void endOfPaint() {}
}
class _SaveElementStackEntry {
_SaveElementStackEntry({
@required this.savedElement,
@required this.transform,
});
final html.Element savedElement;
final Matrix4 transform;
}
/// Provides save stack tracking functionality to implementations of
/// [EngineCanvas].
mixin SaveElementStackTracking on EngineCanvas {
static final Vector3 _unitZ = Vector3(0.0, 0.0, 1.0);
final List<_SaveElementStackEntry> _saveStack = <_SaveElementStackEntry>[];
/// The element at the top of the element stack, or [rootElement] if the stack
/// is empty.
html.Element get currentElement =>
_elementStack.isEmpty ? rootElement : _elementStack.last;
/// The stack that maintains the DOM elements used to express certain paint
/// operations, such as clips.
final List<html.Element> _elementStack = <html.Element>[];
/// Pushes the [element] onto the element stack for the purposes of applying
/// a paint effect using a DOM element, e.g. for clipping.
///
/// The [restore] method automatically pops the element off the stack.
void pushElement(html.Element element) {
_elementStack.add(element);
}
/// Empties the save stack and the element stack, and resets the transform
/// and clip parameters.
///
/// Classes that override this method must call `super.clear()`.
@override
void clear() {
_saveStack.clear();
_elementStack.clear();
_currentTransform = Matrix4.identity();
}
/// The current transformation matrix.
Matrix4 get currentTransform => _currentTransform;
Matrix4 _currentTransform = Matrix4.identity();
/// Saves current clip and transform on the save stack.
///
/// Classes that override this method must call `super.save()`.
@override
void save() {
_saveStack.add(_SaveElementStackEntry(
savedElement: currentElement,
transform: _currentTransform.clone(),
));
}
/// Restores current clip and transform from the save stack.
///
/// Classes that override this method must call `super.restore()`.
@override
void restore() {
if (_saveStack.isEmpty) {
return;
}
final _SaveElementStackEntry entry = _saveStack.removeLast();
_currentTransform = entry.transform;
// Pop out of any clips.
while (currentElement != entry.savedElement) {
_elementStack.removeLast();
}
}
/// Multiplies the [currentTransform] matrix by a translation.
///
/// Classes that override this method must call `super.translate()`.
@override
void translate(double dx, double dy) {
_currentTransform.translate(dx, dy);
}
/// Scales the [currentTransform] matrix.
///
/// Classes that override this method must call `super.scale()`.
@override
void scale(double sx, double sy) {
_currentTransform.scale(sx, sy);
}
/// Rotates the [currentTransform] matrix.
///
/// Classes that override this method must call `super.rotate()`.
@override
void rotate(double radians) {
_currentTransform.rotate(_unitZ, radians);
}
/// Skews the [currentTransform] matrix.
///
/// Classes that override this method must call `super.skew()`.
@override
void skew(double sx, double sy) {
// DO NOT USE Matrix4.skew(sx, sy)! It treats sx and sy values as radians,
// but in our case they are transform matrix values.
final Matrix4 skewMatrix = Matrix4.identity();
final Float32List storage = skewMatrix.storage;
storage[1] = sy;
storage[4] = sx;
_currentTransform.multiply(skewMatrix);
}
/// Multiplies the [currentTransform] matrix by another matrix.
///
/// Classes that override this method must call `super.transform()`.
@override
void transform(Float32List matrix4) {
_currentTransform.multiply(Matrix4.fromFloat32List(matrix4));
}
}