1 /*
2 * Copyright (C) 2008 Emweb bv, Herent, Belgium.
3 *
4 * See the LICENSE file for terms of use.
5 */
6
7 #include "Wt/WApplication.h"
8 #include "Wt/WCanvasPaintDevice.h"
9 #include "Wt/WEnvironment.h"
10 #include "Wt/WException.h"
11 #include "Wt/WFontMetrics.h"
12 #include "Wt/WPainter.h"
13 #include "Wt/WPainterPath.h"
14 #include "Wt/WRectF.h"
15 #include "Wt/WStringStream.h"
16 #include "Wt/WWebWidget.h"
17
18 #include "DomElement.h"
19 #include "WebUtils.h"
20 #include "ServerSideFontMetrics.h"
21
22 #include <cmath>
23 #include <sstream>
24
25 #ifndef M_PI
26 #define M_PI 3.14159265358979323846
27 #endif
28
29 namespace {
30 static const double EPSILON = 1E-5;
31 }
32
33 namespace Wt {
34
35 namespace {
adjust360(double d)36 double adjust360(double d) {
37 if (d > 360.0)
38 return 360.0;
39 else if (d < -360.0)
40 return -360.0;
41 else
42 return d;
43 }
44
adjustPositive360(double d)45 double adjustPositive360(double d) {
46 const double result = std::fmod(d, 360.0);
47 if (result < 0)
48 return result + 360.0;
49 else
50 return result;
51 }
52
fequal(double d1,double d2)53 bool fequal(double d1, double d2) {
54 return std::fabs(d1 - d2) < 1E-5;
55 }
56
defineGradient(const Wt::WGradient & gradient,std::stringstream & js)57 std::string defineGradient(const Wt::WGradient& gradient,
58 std::stringstream& js) {
59 std::string jsRef = "grad";
60 if (gradient.style() == Wt::GradientStyle::Linear) {
61 const WLineF& gradVec = gradient.linearGradientVector();
62 js << "var " << jsRef << " = ctx.createLinearGradient("
63 << gradVec.x1() << ", " << gradVec.y1() << ", "
64 << gradVec.x2() << ", " << gradVec.y2()
65 << ");";
66 } else if (gradient.style() == Wt::GradientStyle::Radial) {
67 js << "var " << jsRef << " = ctx.createRadialGradient("
68 << gradient.radialFocalPoint().x() << ", "
69 << gradient.radialFocalPoint().y() << ","
70 << "0, "
71 << gradient.radialCenterPoint().x() << ", "
72 << gradient.radialCenterPoint().y() << ", "
73 << gradient.radialRadius() << ");";
74 }
75 for (unsigned i=0; i<gradient.colorstops().size(); i++) {
76 js << jsRef << ".addColorStop("
77 << gradient.colorstops()[i].position() << ","
78 << WWebWidget::jsStringLiteral
79 (gradient.colorstops()[i].color().cssText(true))
80 << ");";
81 }
82
83 return jsRef;
84 }
85 }
86
WCanvasPaintDevice(const WLength & width,const WLength & height,bool paintUpdate)87 WCanvasPaintDevice::WCanvasPaintDevice(const WLength& width,
88 const WLength& height,
89 bool paintUpdate)
90 : width_(width),
91 height_(height),
92 painter_(nullptr),
93 paintUpdate_(paintUpdate),
94 currentClippingEnabled_(false),
95 fontMetrics_(nullptr)
96 {
97 textMethod_ = TextMethod::Html5Text;
98
99 WApplication *app = WApplication::instance();
100
101 if (app) {
102 if (app->environment().agentIsChrome()) {
103 if (static_cast<unsigned int>(app->environment().agent()) <=
104 static_cast<unsigned int>(UserAgent::Chrome2))
105 textMethod_ = TextMethod::DomText;
106 } else if (app->environment().agentIsGecko()) {
107 if (static_cast<unsigned int>(app->environment().agent()) <
108 static_cast<unsigned int>(UserAgent::Firefox3_0))
109 textMethod_ = TextMethod::DomText;
110 else if (static_cast<unsigned int>(app->environment().agent())
111 < static_cast<unsigned int>(UserAgent::Firefox3_5))
112 textMethod_ = TextMethod::MozText;
113 } else if (app->environment().agentIsSafari()) {
114 if (app->environment().agent() == UserAgent::Safari3)
115 textMethod_ = TextMethod::DomText;
116 }
117 }
118 }
119
~WCanvasPaintDevice()120 WCanvasPaintDevice::~WCanvasPaintDevice()
121 {
122 delete fontMetrics_;
123 }
124
features()125 WFlags<PaintDeviceFeatureFlag> WCanvasPaintDevice::features() const
126 {
127 if (ServerSideFontMetrics::available())
128 return PaintDeviceFeatureFlag::FontMetrics;
129 else
130 return None;
131 }
132
render(const std::string & paintedWidgetJsRef,const std::string & canvasId,DomElement * text,const std::string & updateAreasJs)133 void WCanvasPaintDevice::render(const std::string& paintedWidgetJsRef,
134 const std::string& canvasId,
135 DomElement *text,
136 const std::string& updateAreasJs)
137 {
138 std::string canvasVar = WT_CLASS ".getElement('" + canvasId + "')";
139 std::string paintedWidgetObjRef = paintedWidgetJsRef + ".wtObj";
140
141 WStringStream tmp;
142
143 tmp << ";" // Extra ; to make sure that JavaScript interprets the next thing as
144 // a separate statement (for when ; was forgotten in another doJavaScript call)
145 "(function(){";
146 tmp << "var pF=function(){";
147
148 tmp <<
149 "if(" << canvasVar << ".getContext){";
150
151 if (!images_.empty()) {
152 tmp << "var images=" << paintedWidgetObjRef << ".images;";
153 }
154 tmp << "var ctx=" << canvasVar << ".getContext('2d');";
155 // Older browsers don't have setLineDash
156 tmp << "if (!ctx.setLineDash) {ctx.setLineDash = function(a){};}";
157
158 if (!paintUpdate_) {
159 tmp << "ctx.clearRect(0,0,"
160 << width().value() << "," << height().value() << ");";
161 }
162
163 lastTransformWasIdentity_ = true;
164
165 tmp << "ctx.save();" << js_.str()
166 << "ctx.restore();";
167
168 tmp << "}";
169
170 tmp << updateAreasJs;
171
172 tmp << "};";
173
174 if (!paintUpdate_) {
175 tmp << paintedWidgetObjRef << ".repaint=pF;";
176 tmp << "pF=function(){"
177 << paintedWidgetObjRef << ".repaint();"
178 << "};";
179 }
180
181 tmp << "var o=" << paintedWidgetObjRef << ";";
182 if (!paintUpdate_)
183 tmp << "o.cancelPreloaders();";
184 tmp << "if(" << canvasVar << ".getContext){";
185 tmp << "var l=new ";
186 tmp << wApp->javaScriptClass() << "._p_.ImagePreloader([";
187
188 for (unsigned i = 0; i < images_.size(); ++i) {
189 if (i != 0)
190 tmp << ',';
191 tmp << '\'' << images_[i] << '\'';
192 }
193
194 tmp << "],function(images){"
195 "if (!" << paintedWidgetJsRef << ")"
196 "return;"
197 "this.done = true;"
198 "var o=" << paintedWidgetObjRef << ";"
199 "if(o.imagePreloaders.length===0||"
200 "this===o.imagePreloaders[0]){"
201 "o.images=images;"
202 "pF();"
203 "o.imagePreloaders.shift();"
204 "}else{"
205 "while(o.imagePreloaders.length>0&&"
206 "o.imagePreloaders[0].done){"
207 "o.imagePreloaders[0].callback(o.imagePreloaders[0].images);"
208 "}"
209 "}"
210 "});"
211 "if(!l.done)"
212 "o.imagePreloaders.push(l);"
213 "}"
214 "})();";
215
216 text->callJavaScript(tmp.str());
217
218 for (unsigned i = 0; i < textElements_.size(); ++i)
219 text->addChild(textElements_[i]);
220 }
221
renderPaintCommands(std::stringstream & js_target,const std::string & canvasElement)222 void WCanvasPaintDevice::renderPaintCommands(std::stringstream& js_target,
223 const std::string& canvasElement)
224 {
225 js_target << "var ctx=" << canvasElement << ".getContext('2d');";
226 js_target << "if (!ctx.setLineDash) {ctx.setLineDash = function(a){};}";
227 js_target << "ctx.save();" << js_.str()
228 << "ctx.restore();";
229 }
230
init()231 void WCanvasPaintDevice::init()
232 {
233 lastTransformWasIdentity_ = true;
234 currentBrush_ = WBrush();
235 currentNoBrush_ = false;
236 currentPen_ = WPen();
237 currentNoPen_ = false;
238 currentPen_.setCapStyle(PenCapStyle::Flat);
239 currentShadow_ = WShadow();
240 currentFont_ = WFont();
241
242 changeFlags_ = PainterChangeFlag::Transform |
243 PainterChangeFlag::Pen |
244 PainterChangeFlag::Brush |
245 PainterChangeFlag::Shadow |
246 PainterChangeFlag::Font;
247 }
248
done()249 void WCanvasPaintDevice::done()
250 { }
251
drawArc(const WRectF & rect,double startAngle,double spanAngle)252 void WCanvasPaintDevice::drawArc(const WRectF& rect, double startAngle,
253 double spanAngle)
254 {
255 if (rect.width() < EPSILON || rect.height() < EPSILON)
256 return;
257
258 renderStateChanges(true);
259
260 const double rStartAngle = WTransform::degreesToRadians(adjustPositive360(-startAngle));
261 double rEndAngle;
262 if (spanAngle >= 360.0 || spanAngle <= -360.0) {
263 rEndAngle = rStartAngle - 2.0 * M_PI * (spanAngle > 0 ? 1.0 : -1.0);
264 } else {
265 rEndAngle = WTransform::degreesToRadians(adjustPositive360(-startAngle - adjust360(spanAngle)));
266 }
267 const bool anticlockwise = spanAngle > 0;
268
269 double sx, sy, r, lw;
270 if (rect.width() > rect.height()) {
271 sx = 1;
272 sy = std::max(0.005, rect.height() / rect.width());
273 r = rect.width()/2;
274 } else if (rect.width() < rect.height()) {
275 sx = std::max(0.005, rect.width() / rect.height());
276 sy = 1;
277 r = rect.height()/2;
278 } else {
279 sx = 1;
280 sy = 1;
281 r = rect.width() / 2;
282 }
283
284 const WPen& pen = painter()->pen();
285 if (pen.style() != PenStyle::None)
286 lw = painter()->normalizedPenWidth(pen.width(), true).value()
287 * 1 / std::min(sx, sy);
288 else
289 lw = 0;
290
291 char buf[30];
292
293 js_ << "ctx.save();"
294 << "ctx.translate(" << Utils::round_js_str(rect.center().x(), 3, buf);
295 js_ << "," << Utils::round_js_str(rect.center().y(), 3, buf);
296 js_ << ");"
297 << "ctx.scale(" << Utils::round_js_str(sx, 3, buf);
298 js_ << "," << Utils::round_js_str(sy, 3, buf) << ");";
299 js_ << "ctx.lineWidth = " << Utils::round_js_str(lw, 3, buf) << ";"
300 << "ctx.beginPath();";
301 js_ << "ctx.arc(0,0," << Utils::round_js_str(r, 3, buf);
302 js_ << ',' << Utils::round_js_str(rStartAngle, 6, buf);
303 js_ << ',' << Utils::round_js_str(rEndAngle, 6, buf) << ',';
304 js_ << (anticlockwise ? "true" : "false") << ");";
305
306 // restore comes before fill and stroke, otherwise the gradient will use
307 // this temporary coordinate system
308 js_ << "ctx.restore();";
309
310 if (painter_->brush().style() != BrushStyle::None) {
311 js_ << "ctx.fill();";
312 }
313
314 if (painter_->pen().style() != PenStyle::None) {
315 js_ << "ctx.stroke();";
316 }
317 }
318
createImage(const std::string & imgUri)319 int WCanvasPaintDevice::createImage(const std::string& imgUri)
320 {
321 images_.push_back(imgUri);
322 return images_.size() - 1;
323 }
324
drawImage(const WRectF & rect,const std::string & imageUri,int imgWidth,int imgHeight,const WRectF & sourceRect)325 void WCanvasPaintDevice::drawImage(const WRectF& rect,
326 const std::string& imageUri,
327 int imgWidth, int imgHeight,
328 const WRectF& sourceRect)
329 {
330 renderStateChanges(true);
331
332 WApplication *app = WApplication::instance();
333 std::string imgUri;
334 if (app)
335 imgUri = app->resolveRelativeUrl(imageUri);
336
337 int imageIndex = createImage(imgUri);
338
339 js_ << WT_CLASS ".gfxUtils.drawImage("
340 "ctx,"
341 "images[" << imageIndex << "],"
342 << Wt::WWebWidget::jsStringLiteral(imgUri) << ','
343 << sourceRect.jsRef() << ','
344 << rect.jsRef() << ");";
345 }
346
drawPlainPath(std::stringstream & out,const WPainterPath & path)347 void WCanvasPaintDevice::drawPlainPath(std::stringstream& out,
348 const WPainterPath& path)
349 {
350 char buf[30];
351
352 out << "ctx.beginPath();";
353
354 const std::vector<WPainterPath::Segment>& segments = path.segments();
355
356 if (segments.size() > 0
357 && segments[0].type() != MoveTo)
358 out << "ctx.moveTo(0,0);";
359
360 for (unsigned i = 0; i < segments.size(); ++i) {
361 const WPainterPath::Segment s = segments[i];
362
363 switch (s.type()) {
364 case MoveTo:
365 out << "ctx.moveTo(" << Utils::round_js_str(s.x() + pathTranslation_.x(),
366 3, buf);
367 out << ',' << Utils::round_js_str(s.y() + pathTranslation_.y(),
368 3, buf) << ");";
369 break;
370 case LineTo:
371 out << "ctx.lineTo(" << Utils::round_js_str(s.x() + pathTranslation_.x(),
372 3, buf);
373 out << ',' << Utils::round_js_str(s.y() + pathTranslation_.y(),
374 3, buf) << ");";
375 break;
376 case CubicC1:
377 out << "ctx.bezierCurveTo("
378 << Utils::round_js_str(s.x() + pathTranslation_.x(), 3, buf);
379 out << ',' << Utils::round_js_str(s.y() + pathTranslation_.y(), 3, buf);
380 break;
381 case CubicC2:
382 out << ',' << Utils::round_js_str(s.x() + pathTranslation_.x(), 3, buf)
383 << ',';
384 out << Utils::round_js_str(s.y() + pathTranslation_.y(), 3, buf);
385 break;
386 case CubicEnd:
387 out << ',' << Utils::round_js_str(s.x() + pathTranslation_.x(), 3, buf)
388 << ',';
389 out << Utils::round_js_str(s.y() + pathTranslation_.y(), 3, buf) << ");";
390 break;
391 case ArcC:
392 out << "ctx.arc(" << Utils::round_js_str(s.x() + pathTranslation_.x(), 3,
393 buf) << ',';
394 out << Utils::round_js_str(s.y() + pathTranslation_.y(), 3, buf);
395 break;
396 case ArcR:
397 out << ',' << Utils::round_js_str(std::max(0.0, s.x()), 3, buf);
398 break;
399 case ArcAngleSweep:
400 {
401 const double startAngle = s.x();
402 const double spanAngle = s.y();
403 const double rStartAngle = WTransform::degreesToRadians(adjustPositive360(-startAngle));
404 double rEndAngle;
405 if (spanAngle >= 360.0 || spanAngle <= -360.0) {
406 rEndAngle = rStartAngle - 2.0 * M_PI * (spanAngle > 0 ? 1.0 : -1.0);
407 } else {
408 rEndAngle = WTransform::degreesToRadians(adjustPositive360(-startAngle - adjust360(spanAngle)));
409 }
410 const bool anticlockwise = spanAngle > 0;
411
412 out << ',' << Utils::round_js_str(rStartAngle, 6, buf);
413 out << ',' << Utils::round_js_str(rEndAngle, 6, buf);
414 out << ',' << (anticlockwise ? "true" : "false") << ");";
415 }
416 break;
417 case QuadC: {
418 const double cpx = s.x();
419 const double cpy = s.y();
420 out << "ctx.quadraticCurveTo("
421 << Utils::round_js_str(cpx + pathTranslation_.x(), 3, buf) << ',';
422 out << Utils::round_js_str(cpy + pathTranslation_.y(), 3, buf);
423
424 break;
425 }
426 case QuadEnd:
427 out << ','
428 << Utils::round_js_str(s.x() + pathTranslation_.x(), 3, buf) << ',';
429 out << Utils::round_js_str(s.y() + pathTranslation_.y(), 3, buf) << ");";
430 }
431 }
432 }
433
finishPath()434 void WCanvasPaintDevice::finishPath()
435 {
436 if (!currentNoBrush_)
437 js_ << "ctx.fill();";
438
439 if (!currentNoPen_)
440 js_ << "ctx.stroke();";
441
442 js_ << '\n';
443 }
444
drawPath(const WPainterPath & path)445 void WCanvasPaintDevice::drawPath(const WPainterPath& path)
446 {
447 if (path.isJavaScriptBound()) {
448 renderStateChanges(true);
449 js_ << WT_CLASS ".gfxUtils.drawPath(ctx," << path.jsRef() << ","
450 << (currentNoBrush_ ? "false" : "true") << ","
451 << (currentNoPen_ ? "false" : "true") << ");";
452 } else {
453 renderStateChanges(false);
454 drawPlainPath(js_, path);
455 finishPath();
456 }
457 }
458
drawStencilAlongPath(const WPainterPath & stencil,const WPainterPath & path,bool softClipping)459 void WCanvasPaintDevice::drawStencilAlongPath(const WPainterPath &stencil,
460 const WPainterPath &path,
461 bool softClipping)
462 {
463 renderStateChanges(true);
464 js_ << WT_CLASS << ".gfxUtils.drawStencilAlongPath(ctx,"
465 << stencil.jsRef() << "," << path.jsRef() << ","
466 << (currentNoBrush_ ? "false" : "true") << ","
467 << (currentNoPen_ ? "false" : "true") << ","
468 << (softClipping ? "true" : "false") << ");";
469 }
470
drawRect(const WRectF & rectangle)471 void WCanvasPaintDevice::drawRect(const WRectF& rectangle)
472 {
473 if (rectangle.isJavaScriptBound()) {
474 renderStateChanges(true);
475 js_ << WT_CLASS << ".gfxUtils.drawRect(ctx," << rectangle.jsRef() << ","
476 << (currentNoBrush_ ? "false" : "true") << ","
477 << (currentNoPen_ ? "false" : "true") << ");";
478 } else {
479 drawPath(rectangle.toPath());
480 }
481 }
482
drawLine(double x1,double y1,double x2,double y2)483 void WCanvasPaintDevice::drawLine(double x1, double y1, double x2, double y2)
484 {
485 WPainterPath path;
486 path.moveTo(x1, y1);
487 path.lineTo(x2, y2);
488 drawPath(path);
489 }
490
drawText(const WRectF & rect,WFlags<AlignmentFlag> flags,TextFlag textFlag,const WString & text,const WPointF * clipPoint)491 void WCanvasPaintDevice::drawText(const WRectF& rect,
492 WFlags<AlignmentFlag> flags,
493 TextFlag textFlag,
494 const WString& text,
495 const WPointF *clipPoint)
496 {
497 if (textFlag == TextFlag::WordWrap)
498 throw WException("WCanvasPaintDevice::drawText() "
499 "WordWrap is not supported");
500
501 AlignmentFlag horizontalAlign = flags & AlignHorizontalMask;
502 AlignmentFlag verticalAlign = flags & AlignVerticalMask;
503
504 if (textMethod_ != TextMethod::DomText) {
505 renderStateChanges(true);
506 }
507
508 switch (textMethod_) {
509 case TextMethod::Html5Text:
510 {
511 js_ << WT_CLASS ".gfxUtils.drawText(ctx,"
512 << rect.jsRef() << ',' << flags.value() << ','
513 << text.jsStringLiteral();
514 if (clipPoint && painter()) {
515 js_ << ',' << painter()->worldTransform().map(*clipPoint).jsRef();
516 }
517 js_ << ");";
518 }
519 break;
520 case TextMethod::MozText:
521 {
522 std::string x;
523
524 switch (horizontalAlign) {
525 case AlignmentFlag::Left:
526 x = std::to_string(rect.left());
527 break;
528 case AlignmentFlag::Right:
529 x = std::to_string(rect.right())
530 + " - ctx.mozMeasureText(" + text.jsStringLiteral() + ")";
531 break;
532 case AlignmentFlag::Center:
533 x = std::to_string(rect.center().x())
534 + " - ctx.mozMeasureText(" + text.jsStringLiteral() + ")/2";
535 break;
536 default:
537 break;
538 }
539
540 double fontSize;
541 switch (painter()->font().size()) {
542 case FontSize::FixedSize:
543 fontSize = painter()->font().sizeLength().toPixels();
544 break;
545 default:
546 fontSize = 16;
547 }
548
549 double y = 0;
550 switch (verticalAlign) {
551 case AlignmentFlag::Top:
552 y = rect.top() + fontSize * 0.75; break;
553 case AlignmentFlag::Middle:
554 y = rect.center().y() + fontSize * 0.25; break;
555 case AlignmentFlag::Bottom:
556 y = rect.bottom() - fontSize * 0.25 ; break;
557 default:
558 break;
559 }
560
561 js_ << "ctx.save();";
562 js_ << "ctx.translate(" << x << ", " << y << ");";
563 if (currentPen_.isJavaScriptBound()) {
564 js_ << "ctx.fillStyle=" WT_CLASS ".gfxUtils.css_text(" << currentPen_.jsRef() << ".color);";
565 } else if (currentBrush_.color() != currentPen_.color() || currentBrush_.isJavaScriptBound())
566 js_ << "ctx.fillStyle="
567 << WWebWidget::jsStringLiteral(currentPen_.color().cssText(true))
568 << ";";
569 js_ << "ctx.mozDrawText(" << text.jsStringLiteral() << ");";
570 js_ << "ctx.restore();";
571 }
572 break;
573 case TextMethod::DomText:
574 {
575 WPointF pos = painter()->combinedTransform().map(rect.topLeft());
576
577 DomElement *e = DomElement::createNew(DomElementType::DIV);
578 e->setProperty(Property::StylePosition, "absolute");
579 e->setProperty(Property::StyleTop,
580 std::to_string(pos.y()) + "px");
581 e->setProperty(Property::StyleLeft,
582 std::to_string(pos.x()) + "px");
583 e->setProperty(Property::StyleWidth,
584 std::to_string(rect.width()) + "px");
585 e->setProperty(Property::StyleHeight,
586 std::to_string(rect.height()) + "px");
587
588 DomElement *t = e;
589
590 /*
591 * HTML tricks to center things vertically -- does not work on IE,
592 * (neither does canvas)
593 */
594 if (verticalAlign != AlignmentFlag::Top) {
595 t = DomElement::createNew(DomElementType::DIV);
596
597 if (verticalAlign == AlignmentFlag::Middle) {
598 e->setProperty(Property::StyleDisplay, "table");
599 t->setProperty(Property::StyleDisplay, "table-cell");
600 t->setProperty(Property::StyleVerticalAlign, "middle");
601 } else if (verticalAlign == AlignmentFlag::Bottom) {
602 t->setProperty(Property::StylePosition, "absolute");
603 t->setProperty(Property::StyleWidth, "100%");
604 t->setProperty(Property::StyleBottom, "0px");
605 }
606 }
607
608 t->setProperty(Property::InnerHTML,
609 WWebWidget::escapeText(text, true).toUTF8());
610
611 WFont f = painter()->font();
612 f.updateDomElement(*t, false, true);
613
614 t->setProperty(Property::StyleColor, painter()->pen().color().cssText());
615
616 if (horizontalAlign == AlignmentFlag::Right)
617 t->setProperty(Property::StyleTextAlign, "right");
618 else if (horizontalAlign == AlignmentFlag::Center)
619 t->setProperty(Property::StyleTextAlign, "center");
620 else
621 t->setProperty(Property::StyleTextAlign, "left");
622
623 if (t != e)
624 e->addChild(t);
625
626 textElements_.push_back(e);
627 }
628 }
629 }
630
drawTextOnPath(const WRectF & rect,WFlags<AlignmentFlag> alignmentFlags,const std::vector<WString> & text,const WTransform & transform,const WPainterPath & path,double angle,double lineHeight,bool softClipping)631 void WCanvasPaintDevice::drawTextOnPath(const WRectF &rect,
632 WFlags<AlignmentFlag> alignmentFlags,
633 const std::vector<WString> &text,
634 const WTransform &transform,
635 const WPainterPath &path,
636 double angle, double lineHeight,
637 bool softClipping)
638 {
639 renderStateChanges(true);
640 js_ << WT_CLASS ".gfxUtils.drawTextOnPath(ctx,[";
641 for (std::size_t i = 0; i < text.size(); ++i) {
642 if (i != 0)
643 js_ << ',';
644 js_ << text[i].jsStringLiteral();
645 }
646 js_ << "],";
647 js_ << rect.jsRef() << ',';
648 js_ << transform.jsRef() << ',';
649 js_ << path.jsRef() << ',';
650 char buf[30];
651 js_ << Utils::round_js_str(angle, 3, buf) << ',';
652 js_ << Utils::round_js_str(lineHeight, 3, buf) << ',';
653 js_ << alignmentFlags.value() << ',';
654 js_ << (softClipping ? "true" : "false") << ");";
655 }
656
measureText(const WString & text,double maxWidth,bool wordWrap)657 WTextItem WCanvasPaintDevice::measureText(const WString& text, double maxWidth,
658 bool wordWrap)
659 {
660 if (!fontMetrics_)
661 fontMetrics_ = new ServerSideFontMetrics();
662
663 return fontMetrics_->measureText(painter()->font(), text, maxWidth, wordWrap);
664 }
665
fontMetrics()666 WFontMetrics WCanvasPaintDevice::fontMetrics()
667 {
668 if (!fontMetrics_)
669 fontMetrics_ = new ServerSideFontMetrics();
670
671 return fontMetrics_->fontMetrics(painter()->font());
672 }
673
setChanged(WFlags<PainterChangeFlag> flags)674 void WCanvasPaintDevice::setChanged(WFlags<PainterChangeFlag> flags)
675 {
676 changeFlags_ |= flags;
677 }
678
renderTransform(std::stringstream & s,const WTransform & t)679 void WCanvasPaintDevice::renderTransform(std::stringstream& s,
680 const WTransform& t)
681 {
682 if (!(t.isIdentity() && lastTransformWasIdentity_)) {
683 s << "ctx.wtTransform=" << t.jsRef() << ';';
684 s << "ctx.setTransform.apply(ctx, ctx.wtTransform);";
685 }
686 lastTransformWasIdentity_ = t.isIdentity();
687 }
688
renderStateChanges(bool resetPathTranslation)689 void WCanvasPaintDevice::renderStateChanges(bool resetPathTranslation)
690 {
691 if (resetPathTranslation) {
692 if (!fequal(pathTranslation_.x(), 0) ||
693 !fequal(pathTranslation_.y(), 0))
694 changeFlags_ |= PainterChangeFlag::Transform;
695 }
696
697 if (!changeFlags_)
698 return;
699
700 /*
701 * For unclear reasons, Firefox on Linux reacts badly against very
702 * long painter paths (e.g. when drawing markers in charts) and by
703 * more prematurely rendering pen/brush changes we avoid that.
704 */
705 WApplication *app = WApplication::instance();
706 bool slowFirefox = app && app->environment().agentIsGecko();
707 if (slowFirefox &&
708 app->environment().userAgent().find("Linux") == std::string::npos)
709 slowFirefox = false;
710
711 bool brushChanged =
712 changeFlags_.test(PainterChangeFlag::Brush) &&
713 (currentBrush_ != painter()->brush()) &&
714 (slowFirefox || painter()->brush().style() != BrushStyle::None);
715
716 bool penChanged =
717 changeFlags_.test(PainterChangeFlag::Pen) &&
718 (currentPen_ != painter()->pen()) &&
719 (slowFirefox || painter()->pen().style() != PenStyle::None);
720
721 bool penColorChanged =
722 penChanged &&
723 (painter()->pen().isJavaScriptBound() ||
724 currentPen_.color() != painter()->pen().color() ||
725 currentPen_.gradient() != painter()->pen().gradient());
726
727 bool shadowChanged =
728 changeFlags_.test(PainterChangeFlag::Shadow)
729 && (currentShadow_ != painter()->shadow());
730
731 bool fontChanged = changeFlags_.test(PainterChangeFlag::Font)
732 && (currentFont_ != painter()->font());
733
734 bool clippingChanged = changeFlags_.test(PainterChangeFlag::Clipping)
735 && (currentClippingEnabled_ != painter()->hasClipping() ||
736 currentClipPath_ != painter()->clipPath() ||
737 currentClipTransform_ != painter()->clipPathTransform());
738
739 changeFlags_.clear(PainterChangeFlag::Clipping);
740
741 if (changeFlags_.test(PainterChangeFlag::Transform) || clippingChanged) {
742 bool resetTransform = false;
743
744 if (clippingChanged) {
745 js_ << "ctx.restore();ctx.save();";
746
747 lastTransformWasIdentity_ = true;
748 pathTranslation_.setX(0);
749 pathTranslation_.setY(0);
750
751 const WTransform& t = painter()->clipPathTransform();
752 const WPainterPath &p = painter()->clipPath();
753 if (!p.isEmpty()) {
754 js_ << WT_CLASS << ".gfxUtils.setClipPath(ctx," << p.jsRef() << "," << t.jsRef() << ","
755 << (painter()->hasClipping() ? "true" : "false") << ");";
756 } else {
757 js_ << WT_CLASS << ".gfxUtils.removeClipPath(ctx);";
758 }
759 currentClipTransform_ = t;
760 currentClipPath_ = p;
761
762 penChanged = true;
763 penColorChanged = true;
764 brushChanged = true;
765 shadowChanged = true;
766 fontChanged = true;
767 init();
768 resetTransform = true;
769
770 currentClippingEnabled_ = painter()->hasClipping();
771 } else if (changeFlags_.test(PainterChangeFlag::Transform)) {
772 WTransform f = painter()->combinedTransform();
773
774 resetTransform = currentTransform_ != f ||
775 ((!fequal(pathTranslation_.x(), 0) ||
776 !fequal(pathTranslation_.y(), 0)) && resetPathTranslation);
777
778 if (!painter()->brush().gradient().isEmpty() ||
779 !painter()->pen().gradient().isEmpty()) {
780 resetTransform = true;
781 } else if (!resetPathTranslation
782 && !currentTransform_.isJavaScriptBound()
783 && !f.isJavaScriptBound()
784 && fequal(f.m11(), currentTransform_.m11())
785 && fequal(f.m12(), currentTransform_.m12())
786 && fequal(f.m21(), currentTransform_.m21())
787 && fequal(f.m22(), currentTransform_.m22())) {
788 /*
789 * Invert scale/rotate to compute the delta needed
790 * before applying these transformations to get the
791 * same as the global translation.
792 *
793 * (This is an optimization that prevents the rendering from
794 * involving many ctx.transform calls, when only moves
795 * are performed.)
796 */
797 double det = f.m11() * f.m22() - f.m12() * f.m21();
798 double a11 = f.m22() / det;
799 double a12 = -f.m12() / det;
800 double a21 = -f.m21() / det;
801 double a22 = f.m11() / det;
802
803 double fdx = f.dx() * a11 + f.dy() * a21;
804 double fdy = f.dx() * a12 + f.dy() * a22;
805
806 const WTransform& g = currentTransform_;
807
808 double gdx = g.dx() * a11 + g.dy() * a21;
809 double gdy = g.dx() * a12 + g.dy() * a22;
810
811 double dx = fdx - gdx;
812 double dy = fdy - gdy;
813
814 pathTranslation_.setX(dx);
815 pathTranslation_.setY(dy);
816
817 changeFlags_ = None;
818
819 resetTransform = false;
820 }
821 }
822
823 if (resetTransform) {
824 currentTransform_ = painter()->combinedTransform();
825 renderTransform(js_, currentTransform_);
826 pathTranslation_.setX(0);
827 pathTranslation_.setY(0);
828 }
829 }
830
831 currentNoPen_ = painter()->pen().style() == PenStyle::None;
832 currentNoBrush_ = painter()->brush().style() == BrushStyle::None;
833
834 if (penChanged) {
835 if (penColorChanged) {
836 // prevent infinite recursion by applying new color to a temporary pen
837 PenCapStyle capStyle = currentPen_.capStyle();
838 PenJoinStyle joinStyle = currentPen_.joinStyle();
839 WPen tmpPen;
840 tmpPen.setCapStyle(capStyle);
841 tmpPen.setJoinStyle(joinStyle);
842 tmpPen.setColor(painter()->pen().color());
843 tmpPen.setGradient(painter()->pen().gradient());
844 currentPen_ = tmpPen;
845 if (!painter()->pen().gradient().isEmpty()) {
846 std::string gradientName = defineGradient(painter()->pen().gradient(),
847 js_);
848 js_ << "ctx.strokeStyle=" << gradientName << ";";
849 renderStateChanges(true);
850 } else {
851 if (painter()->pen().isJavaScriptBound()) {
852 js_ << "ctx.strokeStyle=" WT_CLASS ".gfxUtils.css_text(" << painter()->pen().jsRef() << ".color);";
853 } else {
854 js_ << "ctx.strokeStyle="
855 << WWebWidget::jsStringLiteral
856 (painter()->pen().color().cssText(true))
857 << ";";
858 }
859 }
860 }
861
862 char buf[30];
863 double lw = painter()->normalizedPenWidth(painter()->pen().width(), true).value();
864
865 switch (painter()->pen().style()) {
866 case PenStyle::SolidLine:
867 js_ << "ctx.setLineDash([]);";
868 break;
869 case PenStyle::DashLine:
870 js_ << "ctx.setLineDash([";
871 js_ << Utils::round_js_str(lw * 4.0, 3, buf) << ',';
872 js_ << Utils::round_js_str(lw * 2.0, 3, buf);
873 js_ << "]);";
874 break;
875 case PenStyle::DotLine:
876 js_ << "ctx.setLineDash([";
877 js_ << Utils::round_js_str(lw * 1.0, 3, buf) << ',';
878 js_ << Utils::round_js_str(lw * 2.0, 3, buf);
879 js_ << "]);";
880 break;
881 case PenStyle::DashDotLine:
882 js_ << "ctx.setLineDash([";
883 js_ << Utils::round_js_str(lw * 4.0, 3, buf) << ',';
884 js_ << Utils::round_js_str(lw * 2.0, 3, buf) << ',';
885 js_ << Utils::round_js_str(lw * 1.0, 3, buf) << ',';
886 js_ << Utils::round_js_str(lw * 2.0, 3, buf);
887 js_ << "]);";
888 break;
889 case PenStyle::DashDotDotLine:
890 js_ << "ctx.setLineDash([";
891 js_ << Utils::round_js_str(lw * 4.0, 3, buf) << ',';
892 js_ << Utils::round_js_str(lw * 2.0, 3, buf) << ',';
893 js_ << Utils::round_js_str(lw * 1.0, 3, buf) << ',';
894 js_ << Utils::round_js_str(lw * 2.0, 3, buf) << ',';
895 js_ << Utils::round_js_str(lw * 1.0, 3, buf) << ',';
896 js_ << Utils::round_js_str(lw * 2.0, 3, buf);
897 js_ << "]);";
898 break;
899 case PenStyle::None:
900 break;
901 }
902
903 js_ << "ctx.lineWidth="
904 << Utils::round_js_str(lw, 3, buf)
905 << ';';
906
907 if (currentPen_.capStyle() != painter()->pen().capStyle())
908 switch (painter()->pen().capStyle()) {
909 case PenCapStyle::Flat:
910 js_ << "ctx.lineCap='butt';";
911 break;
912 case PenCapStyle::Square:
913 js_ << "ctx.lineCap='square';";
914 break;
915 case PenCapStyle::Round:
916 js_ << "ctx.lineCap='round';";
917 }
918
919 if (currentPen_.joinStyle() != painter()->pen().joinStyle())
920 switch (painter()->pen().joinStyle()) {
921 case PenJoinStyle::Miter:
922 js_ << "ctx.lineJoin='miter';";
923 break;
924 case PenJoinStyle::Bevel:
925 js_ << "ctx.lineJoin='bevel';";
926 break;
927 case PenJoinStyle::Round:
928 js_ << "ctx.lineJoin='round';";
929 }
930
931 currentPen_ = painter()->pen();
932 }
933
934 if (brushChanged) {
935 currentBrush_ = painter_->brush();
936 if (!currentBrush_.gradient().isEmpty()) {
937 std::string gradientName = defineGradient(currentBrush_.gradient(), js_);
938 js_ << "ctx.fillStyle=" << gradientName << ";";
939 renderStateChanges(true);
940 } else {
941 if (currentBrush_.isJavaScriptBound()) {
942 js_ << "ctx.fillStyle=" WT_CLASS ".gfxUtils.css_text("
943 << currentBrush_.jsRef() << ".color);";
944 } else {
945 js_ << "ctx.fillStyle="
946 << WWebWidget::jsStringLiteral(currentBrush_.color().cssText(true))
947 << ";";
948 }
949 }
950 }
951
952 if (shadowChanged) {
953 currentShadow_ = painter_->shadow();
954
955 double offsetX = currentShadow_.offsetX();
956 double offsetY = currentShadow_.offsetY();
957 double blur = currentShadow_.blur();
958
959 char buf[30];
960 js_ << "ctx.shadowOffsetX=" << Utils::round_js_str(offsetX, 3, buf) << ';';
961 js_ << "ctx.shadowOffsetY=" << Utils::round_js_str(offsetY, 3, buf) << ';';
962 js_ << "ctx.shadowBlur=" << Utils::round_js_str(blur, 3, buf) << ';'
963 << "ctx.shadowColor="
964 << WWebWidget::jsStringLiteral(currentShadow_.color().cssText(true))
965 << ";";
966 }
967
968 if (fontChanged) {
969 currentFont_ = painter_->font();
970
971 switch (textMethod_) {
972 case TextMethod::Html5Text:
973 js_ << "ctx.font="
974 << WWebWidget::jsStringLiteral(painter()->font().cssText()) << ";";
975 break;
976 case TextMethod::MozText:
977 js_ << "ctx.mozTextStyle = "
978 << WWebWidget::jsStringLiteral(painter()->font().cssText()) << ";";
979 break;
980 case TextMethod::DomText:
981 break;
982 }
983 }
984
985 changeFlags_ = None;
986 }
987
988 }
989