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