• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright 2013 The Flutter Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5// TODO(yjbanov): optimization opportunities (see also houdini_painter.js)
6// - collapse non-drawing paint operations
7// - avoid producing DOM-based clips if there is no text
8// - evaluate using stylesheets for static CSS properties
9// - evaluate reusing houdini canvases
10part of engine;
11
12/// A canvas that renders to a combination of HTML DOM and CSS Custom Paint API.
13///
14/// This canvas produces paint commands for houdini_painter.js to apply. This
15/// class must be kept in sync with houdini_painter.js.
16class HoudiniCanvas extends EngineCanvas with SaveElementStackTracking {
17  @override
18  final html.Element rootElement = html.Element.tag('flt-houdini');
19
20  /// The rectangle positioned relative to the parent layer's coordinate system
21  /// where this canvas paints.
22  ///
23  /// Painting outside the bounds of this rectangle is cropped.
24  final ui.Rect bounds;
25
26  HoudiniCanvas(this.bounds) {
27    // TODO(yjbanov): would it be faster to specify static values in a
28    //                stylesheet and let the browser apply them?
29    rootElement.style
30      ..position = 'absolute'
31      ..top = '0'
32      ..left = '0'
33      ..width = '${bounds.size.width}px'
34      ..height = '${bounds.size.height}px'
35      ..backgroundImage = 'paint(flt)';
36  }
37
38  /// Prepare to reuse this canvas by clearing it's current contents.
39  @override
40  void clear() {
41    super.clear();
42    _serializedCommands = <List<dynamic>>[];
43    // TODO(yjbanov): we should measure if reusing old elements is beneficial.
44    domRenderer.clearDom(rootElement);
45  }
46
47  /// Paint commands serialized for sending to the CSS custom painter.
48  List<List<dynamic>> _serializedCommands = <List<dynamic>>[];
49
50  void apply(PaintCommand command) {
51    // Some commands are applied purely in HTML DOM and do not need to be
52    // serialized.
53    if (command is! PaintDrawParagraph &&
54        command is! PaintDrawImageRect &&
55        command is! PaintTransform) {
56      command.serializeToCssPaint(_serializedCommands);
57    }
58    command.apply(this);
59  }
60
61  /// Sends the paint commands to the CSS custom painter for painting.
62  void commit() {
63    if (_serializedCommands.isNotEmpty) {
64      rootElement.style.setProperty('--flt', json.encode(_serializedCommands));
65    } else {
66      rootElement.style.removeProperty('--flt');
67    }
68  }
69
70  @override
71  void clipRect(ui.Rect rect) {
72    final html.Element clip = html.Element.tag('flt-clip-rect');
73    final String cssTransform = matrix4ToCssTransform(
74        transformWithOffset(currentTransform, ui.Offset(rect.left, rect.top)));
75    clip.style
76      ..overflow = 'hidden'
77      ..position = 'absolute'
78      ..transform = cssTransform
79      ..width = '${rect.width}px'
80      ..height = '${rect.height}px';
81
82    // The clipping element will translate the coordinate system as well, which
83    // is not what a clip should do. To offset that we translate in the opposite
84    // direction.
85    super.translate(-rect.left, -rect.top);
86
87    currentElement.append(clip);
88    pushElement(clip);
89  }
90
91  @override
92  void clipRRect(ui.RRect rrect) {
93    final ui.Rect outer = rrect.outerRect;
94    if (rrect.isRect) {
95      clipRect(outer);
96      return;
97    }
98
99    final html.Element clip = html.Element.tag('flt-clip-rrect');
100    final html.CssStyleDeclaration style = clip.style;
101    style
102      ..overflow = 'hidden'
103      ..position = 'absolute'
104      ..transform = 'translate(${outer.left}px, ${outer.right}px)'
105      ..width = '${outer.width}px'
106      ..height = '${outer.height}px';
107
108    if (rrect.tlRadiusY == rrect.tlRadiusX) {
109      style.borderTopLeftRadius = '${rrect.tlRadiusX}px';
110    } else {
111      style.borderTopLeftRadius = '${rrect.tlRadiusX}px ${rrect.tlRadiusY}px';
112    }
113
114    if (rrect.trRadiusY == rrect.trRadiusX) {
115      style.borderTopRightRadius = '${rrect.trRadiusX}px';
116    } else {
117      style.borderTopRightRadius = '${rrect.trRadiusX}px ${rrect.trRadiusY}px';
118    }
119
120    if (rrect.brRadiusY == rrect.brRadiusX) {
121      style.borderBottomRightRadius = '${rrect.brRadiusX}px';
122    } else {
123      style.borderBottomRightRadius =
124          '${rrect.brRadiusX}px ${rrect.brRadiusY}px';
125    }
126
127    if (rrect.blRadiusY == rrect.blRadiusX) {
128      style.borderBottomLeftRadius = '${rrect.blRadiusX}px';
129    } else {
130      style.borderBottomLeftRadius =
131          '${rrect.blRadiusX}px ${rrect.blRadiusY}px';
132    }
133
134    // The clipping element will translate the coordinate system as well, which
135    // is not what a clip should do. To offset that we translate in the opposite
136    // direction.
137    super.translate(-rrect.left, -rrect.top);
138
139    currentElement.append(clip);
140    pushElement(clip);
141  }
142
143  @override
144  void clipPath(ui.Path path) {
145    // TODO(yjbanov): implement.
146  }
147
148  @override
149  void drawColor(ui.Color color, ui.BlendMode blendMode) {
150    // Drawn using CSS Paint.
151  }
152
153  @override
154  void drawLine(ui.Offset p1, ui.Offset p2, ui.PaintData paint) {
155    // Drawn using CSS Paint.
156  }
157
158  @override
159  void drawPaint(ui.PaintData paint) {
160    // Drawn using CSS Paint.
161  }
162
163  @override
164  void drawRect(ui.Rect rect, ui.PaintData paint) {
165    // Drawn using CSS Paint.
166  }
167
168  @override
169  void drawRRect(ui.RRect rrect, ui.PaintData paint) {
170    // Drawn using CSS Paint.
171  }
172
173  @override
174  void drawDRRect(ui.RRect outer, ui.RRect inner, ui.PaintData paint) {
175    // Drawn using CSS Paint.
176  }
177
178  @override
179  void drawOval(ui.Rect rect, ui.PaintData paint) {
180    // Drawn using CSS Paint.
181  }
182
183  @override
184  void drawCircle(ui.Offset c, double radius, ui.PaintData paint) {
185    // Drawn using CSS Paint.
186  }
187
188  @override
189  void drawPath(ui.Path path, ui.PaintData paint) {
190    // Drawn using CSS Paint.
191  }
192
193  @override
194  void drawShadow(ui.Path path, ui.Color color, double elevation,
195      bool transparentOccluder) {
196    // Drawn using CSS Paint.
197  }
198
199  @override
200  void drawImage(ui.Image image, ui.Offset p, ui.PaintData paint) {
201    // TODO(yjbanov): implement.
202  }
203
204  @override
205  void drawImageRect(
206      ui.Image image, ui.Rect src, ui.Rect dst, ui.PaintData paint) {
207    // TODO(yjbanov): implement src rectangle
208    final HtmlImage htmlImage = image;
209    final html.Element imageBox = html.Element.tag('flt-img');
210    final String cssTransform = matrix4ToCssTransform(
211        transformWithOffset(currentTransform, ui.Offset(dst.left, dst.top)));
212    imageBox.style
213      ..position = 'absolute'
214      ..transformOrigin = '0 0 0'
215      ..width = '${dst.width.toInt()}px'
216      ..height = '${dst.height.toInt()}px'
217      ..transform = cssTransform
218      ..backgroundImage = 'url(${htmlImage.imgElement.src})'
219      ..backgroundRepeat = 'norepeat'
220      ..backgroundSize = '${dst.width}px ${dst.height}px';
221    currentElement.append(imageBox);
222  }
223
224  @override
225  void drawParagraph(ui.Paragraph paragraph, ui.Offset offset) {
226    final html.Element paragraphElement =
227        _drawParagraphElement(paragraph, offset, transform: currentTransform);
228    currentElement.append(paragraphElement);
229  }
230}
231
232class _SaveElementStackEntry {
233  _SaveElementStackEntry({
234    @required this.savedElement,
235    @required this.transform,
236  });
237
238  final html.Element savedElement;
239  final Matrix4 transform;
240}
241
242/// Provides save stack tracking functionality to implementations of
243/// [EngineCanvas].
244mixin SaveElementStackTracking on EngineCanvas {
245  static final Vector3 _unitZ = Vector3(0.0, 0.0, 1.0);
246
247  final List<_SaveElementStackEntry> _saveStack = <_SaveElementStackEntry>[];
248
249  /// The element at the top of the element stack, or [rootElement] if the stack
250  /// is empty.
251  html.Element get currentElement =>
252      _elementStack.isEmpty ? rootElement : _elementStack.last;
253
254  /// The stack that maintains the DOM elements used to express certain paint
255  /// operations, such as clips.
256  final List<html.Element> _elementStack = <html.Element>[];
257
258  /// Pushes the [element] onto the element stack for the purposes of applying
259  /// a paint effect using a DOM element, e.g. for clipping.
260  ///
261  /// The [restore] method automatically pops the element off the stack.
262  void pushElement(html.Element element) {
263    _elementStack.add(element);
264  }
265
266  /// Empties the save stack and the element stack, and resets the transform
267  /// and clip parameters.
268  ///
269  /// Classes that override this method must call `super.clear()`.
270  @override
271  void clear() {
272    _saveStack.clear();
273    _elementStack.clear();
274    _currentTransform = Matrix4.identity();
275  }
276
277  /// The current transformation matrix.
278  Matrix4 get currentTransform => _currentTransform;
279  Matrix4 _currentTransform = Matrix4.identity();
280
281  /// Saves current clip and transform on the save stack.
282  ///
283  /// Classes that override this method must call `super.save()`.
284  @override
285  void save() {
286    _saveStack.add(_SaveElementStackEntry(
287      savedElement: currentElement,
288      transform: _currentTransform.clone(),
289    ));
290  }
291
292  /// Restores current clip and transform from the save stack.
293  ///
294  /// Classes that override this method must call `super.restore()`.
295  @override
296  void restore() {
297    if (_saveStack.isEmpty) {
298      return;
299    }
300    final _SaveElementStackEntry entry = _saveStack.removeLast();
301    _currentTransform = entry.transform;
302
303    // Pop out of any clips.
304    while (currentElement != entry.savedElement) {
305      _elementStack.removeLast();
306    }
307  }
308
309  /// Multiplies the [currentTransform] matrix by a translation.
310  ///
311  /// Classes that override this method must call `super.translate()`.
312  @override
313  void translate(double dx, double dy) {
314    _currentTransform.translate(dx, dy);
315  }
316
317  /// Scales the [currentTransform] matrix.
318  ///
319  /// Classes that override this method must call `super.scale()`.
320  @override
321  void scale(double sx, double sy) {
322    _currentTransform.scale(sx, sy);
323  }
324
325  /// Rotates the [currentTransform] matrix.
326  ///
327  /// Classes that override this method must call `super.rotate()`.
328  @override
329  void rotate(double radians) {
330    _currentTransform.rotate(_unitZ, radians);
331  }
332
333  /// Skews the [currentTransform] matrix.
334  ///
335  /// Classes that override this method must call `super.skew()`.
336  @override
337  void skew(double sx, double sy) {
338    // DO NOT USE Matrix4.skew(sx, sy)! It treats sx and sy values as radians,
339    // but in our case they are transform matrix values.
340    final Matrix4 skewMatrix = Matrix4.identity();
341    final Float64List storage = skewMatrix.storage;
342    storage[1] = sy;
343    storage[4] = sx;
344    _currentTransform.multiply(skewMatrix);
345  }
346
347  /// Multiplies the [currentTransform] matrix by another matrix.
348  ///
349  /// Classes that override this method must call `super.transform()`.
350  @override
351  void transform(Float64List matrix4) {
352    _currentTransform.multiply(Matrix4.fromFloat64List(matrix4));
353  }
354}
355