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