• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright 2020 Google Inc.
3  *
4  * Use of this source code is governed by a BSD-style license that can be
5  * found in the LICENSE file.
6  */
7 
8 #include "samplecode/Sample.h"
9 
10 #include "include/core/SkCanvas.h"
11 #include "include/core/SkFont.h"
12 #include "include/core/SkPaint.h"
13 #include "include/core/SkPath.h"
14 #include <tuple>
15 
16 // Math constants are not always defined.
17 #ifndef M_PI
18 #define M_PI 3.14159265358979323846264338327950288
19 #endif
20 
21 #ifndef M_SQRT2
22 #define M_SQRT2 1.41421356237309504880168872420969808
23 #endif
24 
25 constexpr static int kCenterX = 300;
26 constexpr static int kCenterY = 325;
27 constexpr static int kRadius = 250;
28 
29 // This sample fits a cubic to the arc between two interactive points on a circle. It also finds the
30 // T-coordinate of max error, and outputs it and its value in pixels. (It turns out that max error
31 // always occurs at T=0.21132486540519.)
32 //
33 // Press 'E' to iteratively cut the arc in half and report the improvement in max error after each
34 // halving. (It turns out that max error improves by exactly 64x on every halving.)
35 class SampleFitCubicToCircle : public Sample {
name()36     SkString name() override { return SkString("FitCubicToCircle"); }
onOnceBeforeDraw()37     void onOnceBeforeDraw() override { this->fitCubic(); }
38     void fitCubic();
39     void onDrawContent(SkCanvas*) override;
40     Sample::Click* onFindClickHandler(SkScalar x, SkScalar y, skui::ModifierKey) override;
41     bool onClick(Sample::Click*) override;
42     bool onChar(SkUnichar) override;
43 
44     // Coordinates of two points on the unit circle. These are the two endpoints of the arc we fit.
45     double fEndptsX[2] = {0, 1};
46     double fEndptsY[2] = {-1, 0};
47 
48     // Fitted cubic and info, set by fitCubic().
49     double fControlLength;  // Length of (p1 - p0) and/or (p3 - p2) in unit circle space.
50     double fMaxErrorT;  // T value where the cubic diverges most from the true arc.
51     std::array<double, 4> fCubicX;  // Screen space cubic control points.
52     std::array<double, 4> fCubicY;
53     double fMaxError;  // Max error (in pixels) between the cubic and the screen-space arc.
54     double fTheta;  // Angle of the arc. This is only used for informational purposes.
55     SkTArray<SkString> fInfoStrings;
56 
57     class Click;
58 };
59 
60 // Fits a cubic to an arc on the unit circle with endpoints (x0, y0) and (x1, y1). Using the
61 // following 3 constraints, we arrive at the formula used in the method:
62 //
63 //   1) The endpoints and tangent directions at the endpoints must match the arc.
64 //   2) The cubic must be symmetric (i.e., length(p1 - p0) == length(p3 - p2)).
65 //   3) The height of the cubic must match the height of the arc.
66 //
67 // Returns the "control length", or length of (p1 - p0) and/or (p3 - p2).
fit_cubic_to_unit_circle(double x0,double y0,double x1,double y1,std::array<double,4> * X,std::array<double,4> * Y)68 static float fit_cubic_to_unit_circle(double x0, double y0, double x1, double y1,
69                                       std::array<double, 4>* X, std::array<double, 4>* Y) {
70     constexpr static double kM = -4.0/3;
71     constexpr static double kA = 4*M_SQRT2/3;
72     double d = x0*x1 + y0*y1;
73     double c = (std::sqrt(1 + d) * kM + kA) / std::sqrt(1 - d);
74     *X = {x0, x0 - y0*c, x1 + y1*c, x1};
75     *Y = {y0, y0 + x0*c, y1 - x1*c, y1};
76     return c;
77 }
78 
lerp(double x,double y,double T)79 static double lerp(double x, double y, double T) {
80     return x + T*(y - x);
81 }
82 
83 // Evaluates the cubic and 1st and 2nd derivatives at T.
eval_cubic(double x[],double T)84 static std::tuple<double, double, double> eval_cubic(double x[], double T) {
85     // Use De Casteljau's algorithm for better accuracy and stability.
86     double ab = lerp(x[0], x[1], T);
87     double bc = lerp(x[1], x[2], T);
88     double cd = lerp(x[2], x[3], T);
89     double abc = lerp(ab, bc, T);
90     double bcd = lerp(bc, cd, T);
91     double abcd = lerp(abc, bcd, T);
92     return {abcd, 3 * (bcd - abc) /*1st derivative.*/, 6 * (cd - 2*bc + ab) /*2nd derivative.*/};
93 }
94 
95 // Uses newton-raphson convergence to find the point where the provided cubic diverges most from the
96 // unit circle. i.e., the point where the derivative of error == 0. For error we use:
97 //
98 //     error = x^2 + y^2 - 1
99 //     error' = 2xx' + 2yy'
100 //     error'' = 2xx'' + 2yy'' + 2x'^2 + 2y'^2
101 //
find_max_error_T(double cubicX[4],double cubicY[4])102 double find_max_error_T(double cubicX[4], double cubicY[4]) {
103     constexpr static double kInitialT = .25;
104     double T = kInitialT;
105     for (int i = 0; i < 64; ++i) {
106         auto [x, dx, ddx] = eval_cubic(cubicX, T);
107         auto [y, dy, ddy] = eval_cubic(cubicY, T);
108         double dError = 2*(x*dx + y*dy);
109         double ddError = 2*(x*ddx + y*ddy + dx*dx + dy*dy);
110         T -= dError / ddError;
111     }
112     return T;
113 }
114 
fitCubic()115 void SampleFitCubicToCircle::fitCubic() {
116     fInfoStrings.reset();
117 
118     std::array<double, 4> X, Y;
119     // "Control length" is the length of (p1 - p0) and/or (p3 - p2) in unit circle space.
120     fControlLength = fit_cubic_to_unit_circle(fEndptsX[0], fEndptsY[0], fEndptsX[1], fEndptsY[1],
121                                               &X, &Y);
122     fInfoStrings.push_back().printf("control length=%0.14f", fControlLength);
123 
124     fMaxErrorT = find_max_error_T(X.data(), Y.data());
125     fInfoStrings.push_back().printf("max error T=%0.14f", fMaxErrorT);
126 
127     for (int i = 0; i < 4; ++i) {
128         fCubicX[i] = X[i] * kRadius + kCenterX;
129         fCubicY[i] = Y[i] * kRadius + kCenterY;
130     }
131     double errX = std::get<0>(eval_cubic(fCubicX.data(), fMaxErrorT)) - kCenterX;
132     double errY = std::get<0>(eval_cubic(fCubicY.data(), fMaxErrorT)) - kCenterY;
133     fMaxError = std::sqrt(errX*errX + errY*errY) - kRadius;
134     fInfoStrings.push_back().printf("max error=%.5gpx", fMaxError);
135 
136     fTheta = std::atan2(fEndptsY[1], fEndptsX[1]) - std::atan2(fEndptsY[0], fEndptsX[0]);
137     fTheta = std::abs(fTheta * 180/M_PI);
138     if (fTheta > 180) {
139         fTheta = 360 - fTheta;
140     }
141     fInfoStrings.push_back().printf("(theta=%.2f)", fTheta);
142 
143     SkDebugf("\n");
144     for (const SkString& infoString : fInfoStrings) {
145         SkDebugf("%s\n", infoString.c_str());
146     }
147 }
148 
onDrawContent(SkCanvas * canvas)149 void SampleFitCubicToCircle::onDrawContent(SkCanvas* canvas) {
150     canvas->clear(SK_ColorBLACK);
151 
152     SkPaint circlePaint;
153     circlePaint.setColor(0x80ffffff);
154     circlePaint.setStyle(SkPaint::kStroke_Style);
155     circlePaint.setStrokeWidth(0);
156     circlePaint.setAntiAlias(true);
157     canvas->drawArc(SkRect::MakeXYWH(kCenterX - kRadius, kCenterY - kRadius, kRadius * 2,
158                                      kRadius * 2), 0, 360, false, circlePaint);
159 
160     SkPaint cubicPaint;
161     cubicPaint.setColor(SK_ColorGREEN);
162     cubicPaint.setStyle(SkPaint::kStroke_Style);
163     cubicPaint.setStrokeWidth(10);
164     cubicPaint.setAntiAlias(true);
165     SkPath cubicPath;
166     cubicPath.moveTo(fCubicX[0], fCubicY[0]);
167     cubicPath.cubicTo(fCubicX[1], fCubicY[1], fCubicX[2], fCubicY[2], fCubicX[3], fCubicY[3]);
168     canvas->drawPath(cubicPath, cubicPaint);
169 
170     SkPaint endpointsPaint;
171     endpointsPaint.setColor(SK_ColorBLUE);
172     endpointsPaint.setStrokeWidth(8);
173     endpointsPaint.setAntiAlias(true);
174     SkPoint points[2] = {{(float)fCubicX[0], (float)fCubicY[0]},
175                          {(float)fCubicX[3], (float)fCubicY[3]}};
176     canvas->drawPoints(SkCanvas::kPoints_PointMode, 2, points, endpointsPaint);
177 
178     SkPaint textPaint;
179     textPaint.setColor(SK_ColorWHITE);
180     constexpr static float kInfoTextSize = 16;
181     SkFont font(nullptr, kInfoTextSize);
182     int infoY = 10 + kInfoTextSize;
183     for (const SkString& infoString : fInfoStrings) {
184         canvas->drawString(infoString.c_str(), 10, infoY, font, textPaint);
185         infoY += kInfoTextSize * 3/2;
186     }
187 }
188 
189 class SampleFitCubicToCircle::Click : public Sample::Click {
190 public:
Click(int ptIdx)191     Click(int ptIdx) : fPtIdx(ptIdx) {}
192 
doClick(SampleFitCubicToCircle * that)193     void doClick(SampleFitCubicToCircle* that) {
194         double dx = fCurr.fX - kCenterX;
195         double dy = fCurr.fY - kCenterY;
196         double l = std::sqrt(dx*dx + dy*dy);
197         that->fEndptsX[fPtIdx] = dx/l;
198         that->fEndptsY[fPtIdx] = dy/l;
199         if (that->fEndptsX[0] * that->fEndptsY[1] - that->fEndptsY[0] * that->fEndptsX[1] < 0) {
200             std::swap(that->fEndptsX[0], that->fEndptsX[1]);
201             std::swap(that->fEndptsY[0], that->fEndptsY[1]);
202             fPtIdx = 1 - fPtIdx;
203         }
204         that->fitCubic();
205     }
206 
207 private:
208     int fPtIdx;
209 };
210 
onFindClickHandler(SkScalar x,SkScalar y,skui::ModifierKey)211 Sample::Click* SampleFitCubicToCircle::onFindClickHandler(SkScalar x, SkScalar y,
212                                                           skui::ModifierKey) {
213     double dx0 = x - fCubicX[0];
214     double dy0 = y - fCubicY[0];
215     double dx3 = x - fCubicX[3];
216     double dy3 = y - fCubicY[3];
217     if (dx0*dx0 + dy0*dy0 < dx3*dx3 + dy3*dy3) {
218         return new Click(0);
219     } else {
220         return new Click(1);
221     }
222 }
223 
onClick(Sample::Click * click)224 bool SampleFitCubicToCircle::onClick(Sample::Click* click) {
225     Click* myClick = (Click*)click;
226     myClick->doClick(this);
227     return true;
228 }
229 
onChar(SkUnichar unichar)230 bool SampleFitCubicToCircle::onChar(SkUnichar unichar) {
231     if (unichar == 'E') {
232         constexpr static double kMaxErrorT = 0.21132486540519;  // Always the same.
233         // Split the arc in half until error =~0, and report the improvement after each halving.
234         double lastError = -1;
235         for (double theta = fTheta; lastError != 0; theta /= 2) {
236             double rads = theta * M_PI/180;
237             std::array<double, 4> X, Y;
238             fit_cubic_to_unit_circle(1, 0, std::cos(rads), std::sin(rads), &X, &Y);
239             auto [x, dx, ddx] = eval_cubic(X.data(), kMaxErrorT);
240             auto [y, dy, ddy] = eval_cubic(Y.data(), kMaxErrorT);
241             double error = std::sqrt(x*x + y*y) * kRadius - kRadius;
242             if ((float)error <= 0) {
243                 error = 0;
244             }
245             SkDebugf("%6.2f degrees:   error= %10.5gpx", theta, error);
246             if (lastError > 0) {
247                 SkDebugf(" (%17.14fx improvement)", lastError / error);
248             }
249             SkDebugf("\n");
250             lastError = error;
251         }
252         return true;
253     }
254     return false;
255 }
256 
257 DEF_SAMPLE(return new SampleFitCubicToCircle;)
258