blob: 41956fd11f06442feb25e57979393c01bdc8f678 [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:math' as math;
import 'dart:typed_data';
import 'package:ui/ui.dart' as ui;
import '../browser_detection.dart';
import '../dom.dart';
import '../engine_canvas.dart';
import '../svg.dart';
import '../text/canvas_paragraph.dart';
import '../util.dart';
import '../vector_math.dart';
import 'painting.dart';
import 'path/path.dart';
import 'path/path_to_svg.dart';
import 'shaders/image_shader.dart';
import 'shaders/shader.dart';
/// A canvas that renders to DOM elements and CSS properties.
class DomCanvas extends EngineCanvas with SaveElementStackTracking {
DomCanvas(this.rootElement);
@override
final DomElement rootElement;
/// Prepare to reuse this canvas by clearing it's current contents.
@override
void clear() {
super.clear();
removeAllChildren(rootElement);
}
@override
void clipRect(ui.Rect rect, ui.ClipOp clipOp) {
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 DomElement box = createDomElement('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) {
final DomElement 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 DomElement paragraphElement = drawParagraphElement(
paragraph as CanvasParagraph, 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));
}
DomHTMLElement buildDrawRectElement(
ui.Rect rect, SurfacePaintData paint, String tagName, Matrix4 transform) {
final DomHTMLElement rectangle = domDocument.createElement(tagName) as
DomHTMLElement;
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 DomCSSStyleDeclaration 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
..backgroundImage = _getBackgroundImageCssValue(paint.shader, rect);
}
return rectangle;
}
String _getBackgroundImageCssValue(ui.Shader? shader, ui.Rect bounds) {
final String url = _getBackgroundImageUrl(shader, bounds);
return (url != '') ? "url('$url'": '';
}
String _getBackgroundImageUrl(ui.Shader? shader, ui.Rect bounds) {
if(shader != null) {
if(shader is EngineImageShader) {
return shader.image.imgElement.src ?? '';
}
if(shader is EngineGradient) {
return shader.createImageBitmap(bounds, 1, true) as String;
}
}
return '';
}
void applyRRectBorderRadius(DomCSSStyleDeclaration 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(ferhat): hairline nees to take into account both dpi and density.
value = 1.0;
}
return '${value.toStringAsFixed(3)}px';
}
SVGSVGElement pathToSvgElement(
SurfacePath path, SurfacePaintData paint, String width, String height) {
// In Firefox some SVG typed attributes are returned as null without a
// setter. So we use strings here.
final SVGSVGElement root = createSVGSVGElement()
..setAttribute('width', '${width}px')
..setAttribute('height', '${height}px')
..setAttribute('viewBox', '0 0 $width $height');
final SVGPathElement svgPath = createSVGPathElement();
root.append(svgPath);
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)) {
svgPath.setAttribute('stroke', colorToCssString(color)!);
svgPath.setAttribute('stroke-width', '${paint.strokeWidth ?? 1.0}');
svgPath.setAttribute('fill', 'none');
} else if (paint.color != null) {
svgPath.setAttribute('fill', colorToCssString(color)!);
} else {
svgPath.setAttribute('fill', '#000000');
}
if (path.fillType == ui.PathFillType.evenOdd) {
svgPath.setAttribute('fill-rule', 'evenodd');
}
svgPath.setAttribute('d', pathToSvg(path.pathRef));
return root;
}