From 09151ebfc2cfe6feb22e067cd75aea9f9d951125 Mon Sep 17 00:00:00 2001 From: Gary Qian Date: Thu, 27 Jun 2019 10:33:13 -0700 Subject: [PATCH] Manually draw remainder curve for wavy decorations (flutter/engine#9468) --- .../third_party/txt/src/txt/paragraph.cc | 56 +++++-- .../third_party/txt/src/txt/paragraph.h | 9 ++ .../txt/tests/paragraph_unittests.cc | 139 ++++++++++++++++++ 3 files changed, 193 insertions(+), 11 deletions(-) diff --git a/engine/src/flutter/third_party/txt/src/txt/paragraph.cc b/engine/src/flutter/third_party/txt/src/txt/paragraph.cc index aa497308911..4cb9fcfd8ba 100644 --- a/engine/src/flutter/third_party/txt/src/txt/paragraph.cc +++ b/engine/src/flutter/third_party/txt/src/txt/paragraph.cc @@ -1347,17 +1347,9 @@ void Paragraph::PaintDecorations(SkCanvas* canvas, break; } case TextDecorationStyle::kWavy: { - int wave_count = 0; - double x_start = 0; - double wavelength = - underline_thickness * record.style().decoration_thickness_multiplier; - path.moveTo(x, y); - while (x_start + wavelength * 2 < width) { - path.rQuadTo(wavelength, wave_count % 2 != 0 ? wavelength : -wavelength, - wavelength * 2, 0); - x_start += wavelength * 2; - ++wave_count; - } + ComputeWavyDecoration( + path, x, y, width, + underline_thickness * record.style().decoration_thickness_multiplier); break; } } @@ -1425,6 +1417,48 @@ void Paragraph::PaintDecorations(SkCanvas* canvas, } } +void Paragraph::ComputeWavyDecoration(SkPath& path, + double x, + double y, + double width, + double thickness) { + int wave_count = 0; + double x_start = 0; + // One full wavelength is 4 * thickness. + double quarter = thickness; + path.moveTo(x, y); + double remaining = width; + while (x_start + (quarter * 2) < width) { + path.rQuadTo(quarter, wave_count % 2 == 0 ? -quarter : quarter, quarter * 2, + 0); + x_start += quarter * 2; + remaining = width - x_start; + ++wave_count; + } + // Manually add a final partial quad for the remaining width that do + // not fit nicely into a half-wavelength. + // The following math is based off of quadratic bezier equations: + // + // * Let P(x) be the equation for the curve. + // * Let P0 = start, P1 = control point, P2 = end + // * P(x) = -2x^2 - 2x + // * P0 = (0, 0) + // * P1 = 2P(0.5) - 0.5 * P0 - 0.5 * P2 + // * P2 = P(remaining / (wavelength / 2)) + // + // Simplified implementation coursesy of @jim-flar at + // https://github.com/flutter/engine/pull/9468#discussion_r297872739 + // Unsimplified original version at + // https://github.com/flutter/engine/pull/9468#discussion_r297879129 + + double x1 = remaining / 2; + double y1 = remaining / 2 * (wave_count % 2 == 0 ? -1 : 1); + double x2 = remaining; + double y2 = (remaining - remaining * remaining / (quarter * 2)) * + (wave_count % 2 == 0 ? -1 : 1); + path.rQuadTo(x1, y1, x2, y2); +} + void Paragraph::PaintBackground(SkCanvas* canvas, const PaintRecord& record, SkPoint base_offset) { diff --git a/engine/src/flutter/third_party/txt/src/txt/paragraph.h b/engine/src/flutter/third_party/txt/src/txt/paragraph.h index c4922691012..2d6e30827ea 100644 --- a/engine/src/flutter/third_party/txt/src/txt/paragraph.h +++ b/engine/src/flutter/third_party/txt/src/txt/paragraph.h @@ -253,6 +253,7 @@ class Paragraph { FRIEND_TEST(ParagraphTest, RepeatLayoutParagraph); FRIEND_TEST(ParagraphTest, Ellipsize); FRIEND_TEST(ParagraphTest, UnderlineShiftParagraph); + FRIEND_TEST(ParagraphTest, WavyDecorationParagraph); FRIEND_TEST(ParagraphTest, SimpleShadow); FRIEND_TEST(ParagraphTest, ComplexShadow); FRIEND_TEST(ParagraphTest, FontFallbackParagraph); @@ -478,6 +479,14 @@ class Paragraph { const PaintRecord& record, SkPoint base_offset); + // Computes the beziers for a wavy decoration. The results will be + // applied to path. + void ComputeWavyDecoration(SkPath& path, + double x, + double y, + double width, + double thickness); + // Draws the background onto the canvas. void PaintBackground(SkCanvas* canvas, const PaintRecord& record, diff --git a/engine/src/flutter/third_party/txt/tests/paragraph_unittests.cc b/engine/src/flutter/third_party/txt/tests/paragraph_unittests.cc index 40a36d06fd7..19f4f53f11c 100644 --- a/engine/src/flutter/third_party/txt/tests/paragraph_unittests.cc +++ b/engine/src/flutter/third_party/txt/tests/paragraph_unittests.cc @@ -20,6 +20,7 @@ #include "render_test.h" #include "third_party/icu/source/common/unicode/unistr.h" #include "third_party/skia/include/core/SkColor.h" +#include "third_party/skia/include/core/SkPath.h" #include "txt/font_style.h" #include "txt/font_weight.h" #include "txt/paragraph.h" @@ -1902,6 +1903,144 @@ TEST_F(ParagraphTest, DecorationsParagraph) { 1.0); } +TEST_F(ParagraphTest, WavyDecorationParagraph) { + txt::ParagraphStyle paragraph_style; + paragraph_style.max_lines = 14; + paragraph_style.text_align = TextAlign::left; + txt::ParagraphBuilder builder(paragraph_style, GetTestFontCollection()); + + txt::TextStyle text_style; + text_style.font_families = std::vector(1, "Roboto"); + text_style.font_size = 26; + text_style.letter_spacing = 0; + text_style.word_spacing = 5; + text_style.color = SK_ColorBLACK; + text_style.height = 2; + text_style.decoration = TextDecoration::kUnderline | + TextDecoration::kOverline | + TextDecoration::kLineThrough; + + text_style.decoration_style = txt::TextDecorationStyle::kWavy; + text_style.decoration_color = SK_ColorRED; + text_style.decoration_thickness_multiplier = 1.0; + builder.PushStyle(text_style); + + builder.AddText(u" Otherwise, bad things happen."); + + builder.Pop(); + + auto paragraph = builder.Build(); + paragraph->Layout(GetTestCanvasWidth() - 100); + + paragraph->Paint(GetCanvas(), 0, 0); + + ASSERT_TRUE(Snapshot()); + ASSERT_EQ(paragraph->runs_.size(), 1ull); + ASSERT_EQ(paragraph->records_.size(), 1ull); + + for (size_t i = 0; i < 1; ++i) { + ASSERT_EQ(paragraph->records_[i].style().decoration, + TextDecoration::kUnderline | TextDecoration::kOverline | + TextDecoration::kLineThrough); + } + + ASSERT_EQ(paragraph->records_[0].style().decoration_style, + txt::TextDecorationStyle::kWavy); + + ASSERT_EQ(paragraph->records_[0].style().decoration_color, SK_ColorRED); + + ASSERT_EQ(paragraph->records_[0].style().decoration_thickness_multiplier, + 1.0); + + SkPath path0; + SkPath canonical_path0; + paragraph->ComputeWavyDecoration(path0, 1, 1, 9.56, 1); + + canonical_path0.moveTo(1, 1); + canonical_path0.rQuadTo(1, -1, 2, 0); + canonical_path0.rQuadTo(1, 1, 2, 0); + canonical_path0.rQuadTo(1, -1, 2, 0); + canonical_path0.rQuadTo(1, 1, 2, 0); + canonical_path0.rQuadTo(0.78, -0.78, 1.56, -0.3432); + + ASSERT_EQ(path0.countPoints(), canonical_path0.countPoints()); + for (int i = 0; i < canonical_path0.countPoints(); ++i) { + ASSERT_EQ(path0.getPoint(i).x(), canonical_path0.getPoint(i).x()); + ASSERT_EQ(path0.getPoint(i).y(), canonical_path0.getPoint(i).y()); + } + + SkPath path1; + SkPath canonical_path1; + paragraph->ComputeWavyDecoration(path1, 1, 1, 8.35, 1); + + canonical_path1.moveTo(1, 1); + canonical_path1.rQuadTo(1, -1, 2, 0); + canonical_path1.rQuadTo(1, 1, 2, 0); + canonical_path1.rQuadTo(1, -1, 2, 0); + canonical_path1.rQuadTo(1, 1, 2, 0); + canonical_path1.rQuadTo(0.175, -0.175, 0.35, -0.28875); + + ASSERT_EQ(path1.countPoints(), canonical_path1.countPoints()); + for (int i = 0; i < canonical_path1.countPoints(); ++i) { + ASSERT_EQ(path1.getPoint(i).x(), canonical_path1.getPoint(i).x()); + ASSERT_EQ(path1.getPoint(i).y(), canonical_path1.getPoint(i).y()); + } + + SkPath path2; + SkPath canonical_path2; + paragraph->ComputeWavyDecoration(path2, 1, 1, 10.59, 1); + + canonical_path2.moveTo(1, 1); + canonical_path2.rQuadTo(1, -1, 2, 0); + canonical_path2.rQuadTo(1, 1, 2, 0); + canonical_path2.rQuadTo(1, -1, 2, 0); + canonical_path2.rQuadTo(1, 1, 2, 0); + canonical_path2.rQuadTo(1, -1, 2, 0); + canonical_path2.rQuadTo(0.295, 0.295, 0.59, 0.41595); + + ASSERT_EQ(path2.countPoints(), canonical_path2.countPoints()); + for (int i = 0; i < canonical_path2.countPoints(); ++i) { + ASSERT_EQ(path2.getPoint(i).x(), canonical_path2.getPoint(i).x()); + ASSERT_EQ(path2.getPoint(i).y(), canonical_path2.getPoint(i).y()); + } + + SkPath path3; + SkPath canonical_path3; + paragraph->ComputeWavyDecoration(path3, 1, 1, 11.2, 1); + + canonical_path3.moveTo(1, 1); + canonical_path3.rQuadTo(1, -1, 2, 0); + canonical_path3.rQuadTo(1, 1, 2, 0); + canonical_path3.rQuadTo(1, -1, 2, 0); + canonical_path3.rQuadTo(1, 1, 2, 0); + canonical_path3.rQuadTo(1, -1, 2, 0); + canonical_path3.rQuadTo(0.6, 0.6, 1.2, 0.48); + + ASSERT_EQ(path3.countPoints(), canonical_path3.countPoints()); + for (int i = 0; i < canonical_path3.countPoints(); ++i) { + ASSERT_EQ(path3.getPoint(i).x(), canonical_path3.getPoint(i).x()); + ASSERT_EQ(path3.getPoint(i).y(), canonical_path3.getPoint(i).y()); + } + + SkPath path4; + SkPath canonical_path4; + paragraph->ComputeWavyDecoration(path4, 1, 1, 12, 1); + + canonical_path4.moveTo(1, 1); + canonical_path4.rQuadTo(1, -1, 2, 0); + canonical_path4.rQuadTo(1, 1, 2, 0); + canonical_path4.rQuadTo(1, -1, 2, 0); + canonical_path4.rQuadTo(1, 1, 2, 0); + canonical_path4.rQuadTo(1, -1, 2, 0); + canonical_path4.rQuadTo(1, 1, 2, 0); + + ASSERT_EQ(path4.countPoints(), canonical_path4.countPoints()); + for (int i = 0; i < canonical_path4.countPoints(); ++i) { + ASSERT_EQ(path4.getPoint(i).x(), canonical_path4.getPoint(i).x()); + ASSERT_EQ(path4.getPoint(i).y(), canonical_path4.getPoint(i).y()); + } +} + TEST_F(ParagraphTest, ItalicsParagraph) { txt::ParagraphStyle paragraph_style; txt::ParagraphBuilder builder(paragraph_style, GetTestFontCollection());