blob: 2b6fa31028ffdd06644f060138d07cc3cc184c04 [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.
part of engine;
/// A canvas that renders to DOM elements and CSS properties.
class DomCanvas extends EngineCanvas with SaveElementStackTracking {
@override
final html.Element rootElement;
DomCanvas(this.rootElement);
/// Prepare to reuse this canvas by clearing it's current contents.
@override
void clear() {
super.clear();
// TODO(yjbanov): we should measure if reusing old elements is beneficial.
domRenderer.clearDom(rootElement);
}
@override
void clipRect(ui.Rect rect, ui.ClipOp op) {
throw UnimplementedError();
}
@override
void clipRRect(ui.RRect rrect) {
throw UnimplementedError();
}
@override
void clipPath(ui.Path path) {
throw UnimplementedError();
}
@override
void drawColor(ui.Color color, ui.BlendMode blendMode) {
// TODO(yjbanov): implement blendMode
final html.Element box = html.Element.tag('draw-color');
box.style
..position = 'absolute'
..top = '0'
..right = '0'
..bottom = '0'
..left = '0'
..backgroundColor = colorToCssString(color);
currentElement.append(box);
}
@override
void drawLine(ui.Offset p1, ui.Offset p2, SurfacePaintData paint) {
throw UnimplementedError();
}
@override
void drawPaint(SurfacePaintData paint) {
throw UnimplementedError();
}
@override
void drawRect(ui.Rect rect, SurfacePaintData paint) {
currentElement.append(_buildDrawRectElement(rect, paint, 'draw-rect',
currentTransform));
}
@override
void drawRRect(ui.RRect rrect, SurfacePaintData paint) {
html.Element element = _buildDrawRectElement(rrect.outerRect,
paint, 'draw-rrect', currentTransform);
_applyRRectBorderRadius(element.style, rrect);
currentElement.append(element);
}
@override
void drawDRRect(ui.RRect outer, ui.RRect inner, SurfacePaintData paint) {
throw UnimplementedError();
}
@override
void drawOval(ui.Rect rect, SurfacePaintData paint) {
throw UnimplementedError();
}
@override
void drawCircle(ui.Offset c, double radius, SurfacePaintData paint) {
throw UnimplementedError();
}
@override
void drawPath(ui.Path path, SurfacePaintData paint) {
throw UnimplementedError();
}
@override
void drawShadow(ui.Path path, ui.Color color, double elevation,
bool transparentOccluder) {
throw UnimplementedError();
}
@override
void drawImage(ui.Image image, ui.Offset p, SurfacePaintData paint) {
throw UnimplementedError();
}
@override
void drawImageRect(
ui.Image image, ui.Rect src, ui.Rect dst, SurfacePaintData paint) {
throw UnimplementedError();
}
@override
void drawParagraph(ui.Paragraph paragraph, ui.Offset offset) {
final html.Element paragraphElement =
_drawParagraphElement(paragraph as EngineParagraph, offset, transform: currentTransform);
currentElement.append(paragraphElement);
}
@override
void drawVertices(
ui.Vertices vertices, ui.BlendMode blendMode, SurfacePaintData paint) {
throw UnimplementedError();
}
@override
void drawPoints(ui.PointMode pointMode, Float32List points, SurfacePaintData paint) {
throw UnimplementedError();
}
@override
void endOfPaint() {
// No reuse of elements yet to handle here. Noop.
}
}
/// Converts a shadow color specified by the framework to the color that should
/// actually be applied when rendering the element.
///
/// Returns a color for box-shadow based on blur filter at sigma.
ui.Color blurColor(ui.Color color, double sigma) {
final double strength = math.min(
math.sqrt(sigma) / (math.pi * 2.0), 1.0);
final int reducedAlpha = ((1.0 - strength) * color.alpha).round();
return ui.Color((reducedAlpha & 0xff) << 24 | (color.value & 0x00ffffff));
}
html.HtmlElement _buildDrawRectElement(ui.Rect rect, SurfacePaintData paint, String tagName,
Matrix4 transform) {
assert(paint.shader == null);
final html.HtmlElement rectangle = domRenderer.createElement(tagName) as html.HtmlElement;
assert(() {
rectangle.setAttribute('flt-rect', '$rect');
rectangle.setAttribute('flt-paint', '$paint');
return true;
}());
String effectiveTransform;
final bool isStroke = paint.style == ui.PaintingStyle.stroke;
final double strokeWidth = paint.strokeWidth ?? 0.0;
final double left = math.min(rect.left, rect.right);
final double right = math.max(rect.left, rect.right);
final double top = math.min(rect.top, rect.bottom);
final double bottom = math.max(rect.top, rect.bottom);
if (transform.isIdentity()) {
if (isStroke) {
effectiveTransform =
'translate(${left - (strokeWidth / 2.0)}px, ${top - (strokeWidth / 2.0)}px)';
} else {
effectiveTransform = 'translate(${left}px, ${top}px)';
}
} else {
// Clone to avoid mutating _transform.
final Matrix4 translated = transform.clone();
if (isStroke) {
translated.translate(
left - (strokeWidth / 2.0), top - (strokeWidth / 2.0));
} else {
translated.translate(left, top);
}
effectiveTransform = matrix4ToCssTransform(translated);
}
final html.CssStyleDeclaration style = rectangle.style;
style
..position = 'absolute'
..transformOrigin = '0 0 0'
..transform = effectiveTransform;
String cssColor =
paint.color == null ? '#000000' : colorToCssString(paint.color)!;
if (paint.maskFilter != null) {
final double sigma = paint.maskFilter!.webOnlySigma;
if (browserEngine == BrowserEngine.webkit && !isStroke) {
// A bug in webkit leaves artifacts when this element is animated
// with filter: blur, we use boxShadow instead.
style.boxShadow = '0px 0px ${sigma * 2.0}px $cssColor';
cssColor = colorToCssString(
blurColor(paint.color ?? const ui.Color(0xFF000000), sigma))!;
} else {
style.filter = 'blur(${sigma}px)';
}
}
if (isStroke) {
style
..width = '${right - left - strokeWidth}px'
..height = '${bottom - top - strokeWidth}px'
..border = '${_borderStrokeToCssUnit(strokeWidth)} solid $cssColor';
} else {
style
..width = '${right - left}px'
..height = '${bottom - top}px'
..backgroundColor = cssColor;
}
return rectangle;
}
void _applyRRectBorderRadius(html.CssStyleDeclaration style, ui.RRect rrect) {
if (rrect.tlRadiusX == rrect.trRadiusX &&
rrect.tlRadiusX == rrect.blRadiusX &&
rrect.tlRadiusX == rrect.brRadiusX &&
rrect.tlRadiusX == rrect.tlRadiusY &&
rrect.trRadiusX == rrect.trRadiusY &&
rrect.blRadiusX == rrect.blRadiusY &&
rrect.brRadiusX == rrect.brRadiusY) {
style.borderRadius = '${_borderStrokeToCssUnit(rrect.blRadiusX)}';
return;
}
// Non-uniform. Apply each corner radius.
style.borderTopLeftRadius = '${_borderStrokeToCssUnit(rrect.tlRadiusX)} '
'${_borderStrokeToCssUnit(rrect.tlRadiusY)}';
style.borderTopRightRadius = '${_borderStrokeToCssUnit(rrect.trRadiusX)} '
'${_borderStrokeToCssUnit(rrect.trRadiusY)}';
style.borderBottomLeftRadius = '${_borderStrokeToCssUnit(rrect.blRadiusX)} '
'${_borderStrokeToCssUnit(rrect.blRadiusY)}';
style.borderBottomRightRadius = '${_borderStrokeToCssUnit(rrect.brRadiusX)} '
'${_borderStrokeToCssUnit(rrect.brRadiusY)}';
}
String _borderStrokeToCssUnit(double value) {
if (value == 0) {
// TODO: hairline nees to take into account both dpi and density.
value = 1.0;
}
return '${value.toStringAsFixed(3)}px';
}
html.Element _pathToSvgElement(SurfacePath path, SurfacePaintData paint,
String width, String height) {
final StringBuffer sb = StringBuffer();
sb.write(
'<svg viewBox="0 0 $width $height" width="${width}px" height="${height}px">');
sb.write('<path ');
final ui.Color color = paint.color ?? const ui.Color(0xFF000000);
if (paint.style == ui.PaintingStyle.stroke ||
(paint.style != ui.PaintingStyle.fill &&
paint.strokeWidth != 0 && paint.strokeWidth != null)) {
sb.write('stroke="${colorToCssString(color)}" ');
sb.write('stroke-width="${paint.strokeWidth ?? 1.0}" ');
sb.write('fill="none" ');
} else if (paint.color != null) {
sb.write('fill="${colorToCssString(color)}" ');
} else {
sb.write('fill="#000000" ');
}
if (path.fillType == ui.PathFillType.evenOdd) {
sb.write('fill-rule="evenodd" ');
}
sb.write('d="');
pathToSvg(path, sb);
sb.write('"></path>');
sb.write('</svg>');
return html.Element.html(sb.toString(), treeSanitizer: _NullTreeSanitizer());
}