Chris Dalton | 392fa03 | 2020-06-09 16:26:50 -0600 | [diff] [blame^] | 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 { |
| 36 | SkString name() override { return SkString("FitCubicToCircle"); } |
| 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). |
| 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 | |
| 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. |
| 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 | // |
| 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 | |
| 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 | |
| 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: |
| 191 | Click(int ptIdx) : fPtIdx(ptIdx) {} |
| 192 | |
| 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 | |
| 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 | |
| 224 | bool SampleFitCubicToCircle::onClick(Sample::Click* click) { |
| 225 | Click* myClick = (Click*)click; |
| 226 | myClick->doClick(this); |
| 227 | return true; |
| 228 | } |
| 229 | |
| 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;) |