xref: /aosp_15_r20/external/skia/tools/viewer/GraphitePrimitivesSlide.cpp (revision c8dee2aa9b3f27cf6c858bd81872bdeb2c07ed17)
1 /*
2  * Copyright 2022 Google LLC
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 "include/core/SkCanvas.h"
9 #include "include/core/SkM44.h"
10 #include "include/core/SkPaint.h"
11 #include "include/core/SkRRect.h"
12 #include "include/core/SkVertices.h"
13 #include "include/private/base/SkTPin.h"
14 #include "tools/viewer/ClickHandlerSlide.h"
15 
16 #include <unordered_set>
17 
paint(SkColor color,float strokeWidth=-1.f,SkPaint::Join join=SkPaint::kMiter_Join)18 static SkPaint paint(SkColor color,
19                      float strokeWidth = -1.f,
20                      SkPaint::Join join = SkPaint::kMiter_Join) {
21     SkPaint paint;
22     paint.setColor(color);
23     paint.setAntiAlias(true);
24     if (strokeWidth >= 0.f) {
25         paint.setStyle(SkPaint::kStroke_Style);
26         paint.setStrokeWidth(strokeWidth);
27         paint.setStrokeJoin(join);
28     }
29     return paint;
30 }
31 
32 // Singular values for [a b][c d] 2x2 matrix, unordered.
singular_values(float a,float b,float c,float d)33 static std::pair<float, float> singular_values(float a, float b, float c, float d) {
34     float s1 = a*a + b*b + c*c + d*d;
35 
36     float e = a*a + b*b - c*c - d*d;
37     float f = a*c + b*d;
38     float s2 = SkScalarSqrt(e*e + 4*f*f);
39 
40     float singular1 = SkScalarSqrt(0.5f * (s1 + s2));
41     float singular2 = SkScalarSqrt(0.5f * (s1 - s2));
42 
43     return {singular1, singular2};
44 }
45 
46 static constexpr float kAARadius = 10.f;
47 
48 //              [m00 m01 * m03]                                 [f(u,v)]
49 // Assuming M = [m10 m11 * m13], define the projected p'(u,v) = [g(u,v)] where
50 //              [ *   *  *  * ]
51 //              [m30 m31 * m33]
52 //                                                        [x]     [u]
53 // f(u,v) = x(u,v) / w(u,v), g(u,v) = y(u,v) / w(u,v) and [y] = M*[v]
54 //                                                        [*] =   [0]
55 //                                                        [w]     [1]
56 //
57 // x(u,v) = m00*u + m01*v + m03
58 // y(u,v) = m10*u + m11*v + m13
59 // w(u,v) = m30*u + m31*v + m33
60 //
61 // dx/du = m00, dx/dv = m01,
62 // dy/du = m10, dy/dv = m11
63 // dw/du = m30, dw/dv = m31
64 //
65 // df/du = (dx/du*w - x*dw/du)/w^2 = (m00*w - m30*x)/w^2
66 // df/dv = (dx/dv*w - x*dw/dv)/w^2 = (m01*w - m31*x)/w^2
67 // dg/du = (dy/du*w - y*dw/du)/w^2 = (m10*w - m30*y)/w^2
68 // dg/dv = (dy/dv*w - y*dw/du)/w^2 = (m11*w - m31*y)/w^2
69 //
70 // Singular values of [df/du df/dv] define perspective correct minimum and maximum scale factors
71 //                    [dg/du dg/dv]
72 // for M evaluated at  (u,v)
local_aa_radius(const SkM44 & matrix,const SkV2 & p)73 static float local_aa_radius(const SkM44& matrix, const SkV2& p) {
74     SkV4 devP = matrix.map(p.x, p.y, 0.f, 1.f);
75 
76     const float dxdu = matrix.rc(0,0);
77     const float dxdv = matrix.rc(0,1);
78     const float dydu = matrix.rc(1,0);
79     const float dydv = matrix.rc(1,1);
80     const float dwdu = matrix.rc(3,0);
81     const float dwdv = matrix.rc(3,1);
82 
83     float invW2 = 1.f / (devP.w * devP.w);
84     // non-persp has invW2 = 1, devP.w = 1, dwdu = 0, dwdv = 0
85     float dfdu = (devP.w*dxdu - devP.x*dwdu) * invW2; // non-persp -> dxdu -> m00
86     float dfdv = (devP.w*dxdv - devP.x*dwdv) * invW2; // non-persp -> dxdv -> m01
87     float dgdu = (devP.w*dydu - devP.y*dwdu) * invW2; // non-persp -> dydu -> m10
88     float dgdv = (devP.w*dydv - devP.y*dwdv) * invW2; // non-persp -> dydv -> m11
89 
90     // no-persp, this is the singular values of [m00,m01][m10,m11], which is just the upper 2x2
91     // and equivalent to SkMatrix::getMinmaxScales().
92     auto [sv1, sv2] = singular_values(dfdu, dfdv, dgdu, dgdv);
93 
94     // The minimum and maximum singular values of the above matrix represent the min and maximum
95     // scale factors that could be applied by the 'matrix'. So if 'p' is moved 1px locally it will
96     // move between [min, max]px after transformation. Thus, moving 1/min px locally will move
97     // between [1, max/min]px after transformation, ensuring the device-space offset exceeds the
98     // minimum AA offset for analytic AA.
99     float minScale = std::min(sv1, sv2);
100     return kAARadius / minScale;
101 }
102 
103 static constexpr float kMiterScale = 1.f;
104 static constexpr float kBevelScale = 0.0f;
105 static constexpr float kRoundScale = SK_FloatSqrt2 - 1.f;
106 
107 struct LocalCornerVert {
108     SkV2 fPosition;     // In unit square that each corner is normalized to
109     SkV2 fNormal;       // Direction that AA outset is applied in
110 
111     float fStrokeScale; // Signed scale factor applied to external stroke radius, should be [-1,1]
112     float fMirrorScale; // Scale fPosition.yx, along with external join-scale, should be [0,1].
113     float fCenterWeight; // Added to external center scale, > 0 forces point to center instead.
114 
115     // 'cornerMapping' is a row-major 2x2 matrix [[x y], [z w]] to flip and rotate the normalized
116     // positions into the local coord space.
transformLocalCornerVert117     SkV3 transform(const SkM44& m, const SkV4& cornerMapping, const SkV2& cornerPt,
118                    const SkV2& cornerRadii, const SkV4& devCenter, float centerWeight,
119                    float strokeRadius, float joinScale, float localAARadius) const {
120         const bool snapToCenter = centerWeight + fCenterWeight > 0.f;
121         if (snapToCenter) {
122             return {devCenter.x, devCenter.y, devCenter.w};
123         } else {
124             // Normalized position before any additional AA offsets
125             SkV2 normalizedPos = fPosition + joinScale*fMirrorScale*SkV2{fPosition.y, fPosition.x};
126             // scales the normalized unit corner to the actual radii of the corner, before any AA
127             // offsets are added.
128             SkV2 scale = cornerRadii + SkV2{fStrokeScale*strokeRadius, fStrokeScale*strokeRadius};
129             normalizedPos = scale*normalizedPos - cornerRadii;
130 
131             if (fStrokeScale < 0.f) {
132                 // An inset, which means it might cross over or might be forced to the center
133                 SkV2 maxInset = scale - SkV2{localAARadius, localAARadius};
134                 if (maxInset.x < 0.f || maxInset.y < 0.f) {
135                     normalizedPos =
136                             SkV2{std::min(maxInset.x, 0.f), std::min(maxInset.y, 0.f)}
137                             - cornerRadii;
138                 } else {
139                     normalizedPos += localAARadius * fNormal;
140                 }
141             } // else no normal offsetting, or device-space offsetting
142 
143             SkV2 localPos =
144                    {cornerMapping.x*normalizedPos.x + cornerMapping.y*normalizedPos.y + cornerPt.x,
145                     cornerMapping.z*normalizedPos.x + cornerMapping.w*normalizedPos.y + cornerPt.y};
146             SkV4 devPos = m.map(localPos.x, localPos.y, 0.f, 1.f);
147 
148             const bool deviceSpaceNormal =
149                     fStrokeScale > 0.f && (fNormal.x > 0.f || fNormal.y > 0.f);
150             if (deviceSpaceNormal) {
151                 SkV2 devNorm;
152                 {
153                     // To calculate a device-space normal, we use the normal matrix (A^-1)^T where
154                     // A is CTM * T(cornerPt) * cornerMapping * scale. We inline the calculation
155                     // of (T(cornerPt)*cornerMapping*scale)^-1^T * [nx, ny, 0, 0] = N', which means
156                     // that CTM^-1^T * N' is equivalent to N'^T*CTM^-1, which can be calculated with
157                     // two dot products if the CTM inverse is uploaded to the GPU.
158 
159                     // We add epsilon so that rectangular corners are not degenerate, and circular
160                     // corners remain unmodified. This only slightly increases inaccuracy for
161                     // elliptical corners.
162                     float sx = (scale.y + SK_ScalarNearlyZero) / (scale.x + SK_ScalarNearlyZero);
163                     // Needed to calculate intermediate W of transformed normal.
164                     float px = cornerMapping.y*cornerPt.y - cornerMapping.w*cornerPt.x;
165                     float py = cornerMapping.z*cornerPt.x - cornerMapping.x*cornerPt.y;
166                     // Inverse CTM, presumably calculated once as a uniform
167                     SkM44 inv;
168                     SkAssertResult(m.invert(&inv));
169 
170                     SkV4 normX4 = { sx*cornerMapping.w*fNormal.x,
171                                    -sx*cornerMapping.y*fNormal.x,
172                                    0.f,
173                                    sx*px*fNormal.x};
174                     SkV4 normY4 = {-cornerMapping.z*fNormal.y,
175                                     cornerMapping.x*fNormal.y,
176                                    0.f,
177                                    py*fNormal.y};
178 
179                     SkV2 normX = {inv.col(0).dot(normX4), inv.col(1).dot(normX4)};
180                     SkV2 normY = {inv.col(0).dot(normY4), inv.col(1).dot(normY4)};
181 
182                     if (joinScale == kMiterScale && fNormal.x > 0.f && fNormal.y > 0.f) {
183                         // normX and normY represent adjacent edges' normals, so if we normalize
184                         // them before adding together, we'll have a vector that bisects the edge
185                         // normals instead of a vector matching fNormal, which is what we want when
186                         // we're at a miter corner.
187                         normX = normX.normalize();
188                         normY = normY.normalize();
189                         if (normX.dot(normY) < -0.8) {
190                             // Nearly opposite directions, so the sum could have cancellation, so
191                             // instead bisect orthogonal vectors and flip to keep consistent
192                             float sign = normX.cross(normY) >= 0.f ? 1.f : -1.f;
193                             normX = sign*SkV2{-normX.y, normX.x};
194                             normY = sign*SkV2{normY.y, -normY.x};
195                         }
196                     }
197 
198                     devNorm = (normX + normY).normalize();
199                 }
200 
201                 // The local coordinates for a device-space AA outset are clamped to the non-outset
202                 // point, which means we don't care about remaining in the same pre-homogenous
203                 // divide plane. This makes it very easy to determine a homogenous coordinate that
204                 // projects to the correct device-space position.
205                 devPos.x += devPos.w * kAARadius * devNorm.x;
206                 devPos.y += devPos.w * kAARadius * devNorm.y;
207             }
208 
209             return SkV3{devPos.x, devPos.y, devPos.w};
210         }
211     }
212 };
213 
214 static constexpr float kHR2 = SK_ScalarRoot2Over2; // "half root 2"
215 
216 static constexpr LocalCornerVert kCornerTemplate[19] = {
217     // Stroke-scale should be -1, 0, or 1.
218     // Mirror-scale should be 0 or 1.
219     // Center-weight should be -2 to never snap to center, -1 to snap when stroke coords would
220     //     overlap, and 0 to snap for fill-style or overlapping coords.
221     // Local-aa-scale should be 0 or 1.
222 
223     //  position,       normal,   stroke-scale   mirror-scale   center-weight
224     // Device-space AA outsets from outer curve
225     { {0.0f, 1.0f}, { 0.0f,  1.0f},  1.0f,          0.0f,           -2.f  },
226     { {0.0f, 1.0f}, { 0.0f,  1.0f},  1.0f,          1.0f,           -2.f  },
227     { {0.0f, 1.0f}, { kHR2,  kHR2},  1.0f,          1.0f,           -2.f  },
228     { {1.0f, 0.0f}, { kHR2,  kHR2},  1.0f,          1.0f,           -2.f  },
229     { {1.0f, 0.0f}, { 1.0f,  0.0f},  1.0f,          1.0f,           -2.f  },
230     { {1.0f, 0.0f}, { 1.0f,  0.0f},  1.0f,          0.0f,           -2.f  },
231 
232     // Outer anchors (no local or device-space normal outset)
233     { {0.0f, 1.0f}, { 0.0f,  0.0f},  1.0f,          0.0f,           -2.f  },
234     { {0.0f, 1.0f}, { 0.0f,  0.0f},  1.0f,          1.0f,           -2.f  },
235     { {1.0f, 0.0f}, { 0.0f,  0.0f},  1.0f,          1.0f,           -2.f  },
236     { {1.0f, 0.0f}, { 0.0f,  0.0f},  1.0f,          0.0f,           -2.f  },
237 
238     // Center of stroke (equivalent to outer anchors when filling)
239     { {0.0f, 1.0f}, { 0.0f,  0.0f},  0.0f,          0.0f,           -2.f  },
240     { {0.0f, 1.0f}, { 0.0f,  0.0f},  0.0f,          1.0f,           -2.f  },
241     { {1.0f, 0.0f}, { 0.0f,  0.0f},  0.0f,          1.0f,           -2.f  },
242     { {1.0f, 0.0f}, { 0.0f,  0.0f},  0.0f,          0.0f,           -2.f  },
243 
244     // Inner AA insets from inner curve
245     { {0.0f, 1.0f}, { 0.0f, -1.0f}, -1.0f,          0.0f,           -1.f  },
246     { {0.5f, 0.5f}, {-kHR2, -kHR2}, -1.0f,          1.0f,           -1.f  },
247     { {1.0f, 0.0f}, {-1.0f,  0.0f}, -1.0f,          0.0f,           -1.f  },
248 
249     // Center filling vertices (equal to inner AA insets unless center-weight = 1)
250     { {0.5f, 0.5f}, {-kHR2, -kHR2}, -1.0f,          1.0f,            0.f  },
251     { {1.0f, 0.0f}, {-1.0f,  0.0f}, -1.0f,          0.0f,            0.f  },
252 };
253 
compute_corner(SkV3 devPts[19],const SkM44 & m,const SkV4 & cornerMapping,const SkV2 & cornerPt,const SkV2 & cornerRadii,const SkV4 & center,float centerWeight,float localAARadius,float strokeRadius,SkPaint::Join join)254 static void compute_corner(SkV3 devPts[19], const SkM44& m, const SkV4& cornerMapping,
255                            const SkV2& cornerPt, const SkV2& cornerRadii, const SkV4& center,
256                            float centerWeight, float localAARadius, float strokeRadius,
257                            SkPaint::Join join) {
258     float joinScale;
259 
260     // TODO: checking against localAARadius can snap to rect corner unexpectedly under high skew
261     // because localAARadius gets so big, but would be nice to be fuzzy here.
262     if (cornerRadii.x <= 0.f || cornerRadii.y <= 0.f) {
263         // Effectively a rectangular corner
264         joinScale = kMiterScale; // default for rect corners
265         if (strokeRadius > 0.f) {
266             // Non-hairline strokes need to adjust the join scale factor to match style.
267             if (join == SkPaint::kBevel_Join) {
268                 joinScale = kBevelScale;
269             } else if (join == SkPaint::kRound_Join) {
270                 joinScale = kRoundScale;
271             }
272         }
273     } else {
274         // Rounded filled corner vertices are always positioned for a round join since the
275         // underlying geometry has no real tangent discontinuity.
276         joinScale = kRoundScale;
277     }
278 
279     for (size_t i = 0; i < std::size(kCornerTemplate); ++i) {
280         devPts[i] = kCornerTemplate[i].transform(m, cornerMapping, cornerPt, cornerRadii,
281                                                  center, centerWeight, strokeRadius, joinScale,
282                                                  localAARadius);
283     }
284 }
285 
286 static const uint16_t kBR = 0*std::size(kCornerTemplate);
287 static const uint16_t kTR = 1*std::size(kCornerTemplate);
288 static const uint16_t kTL = 2*std::size(kCornerTemplate);
289 static const uint16_t kBL = 3*std::size(kCornerTemplate);
290 static const size_t kVertexCount = 4*std::size(kCornerTemplate);
compute_vertices(SkV3 devPts[kVertexCount],const SkM44 & m,const SkRRect & rrect,float strokeRadius,SkPaint::Join join)291 static void compute_vertices(SkV3 devPts[kVertexCount],
292                              const SkM44& m,
293                              const SkRRect& rrect,
294                              float strokeRadius,
295                              SkPaint::Join join) {
296     SkV4 devCenter = m.map(rrect.getBounds().centerX(), rrect.getBounds().centerY(), 0.f, 1.f);
297 
298     float localAARadius = std::max({
299             local_aa_radius(m, {rrect.getBounds().fRight, rrect.getBounds().fBottom}),
300             local_aa_radius(m, {rrect.getBounds().fRight, rrect.getBounds().fTop}),
301             local_aa_radius(m, {rrect.getBounds().fLeft, rrect.getBounds().fTop}),
302             local_aa_radius(m, {rrect.getBounds().fLeft, rrect.getBounds().fBottom})
303         });
304 
305     float centerWeight = 0.f; // No center snapping
306     if (strokeRadius < 0.f) {
307         // A fill, so inner vertices need to snap to the center and then adjust the stroke radius
308         // to 0 for later math to work out nicely.
309         strokeRadius = 0.f;
310         centerWeight = 1.f;
311     }
312 
313     // Check if the inset amount (max stroke-radius + local-aa-radius) would interfere with the
314     // opposite edge's inset or interfere with the adjacent corner's curve. When this happens, snap
315     // all the interior vertices to the center and let the fragment shader work through it.
316     // TODO: Could force centerWeight = 2 for filled rects and quads for simplicity around non
317     // orthogonal inset overlap calculations.
318     float maxInset = strokeRadius + localAARadius;
319     if (maxInset >= rrect.width() - maxInset || // L/R stroke insets would cross over
320         maxInset >= rrect.height() - maxInset || // T/B stroke insets would cross over
321         maxInset >= rrect.width() - rrect.radii(SkRRect::kLowerLeft_Corner).fX || // X corner cross
322         maxInset >= rrect.width() - rrect.radii(SkRRect::kLowerRight_Corner).fX ||
323         maxInset >= rrect.width() - rrect.radii(SkRRect::kUpperLeft_Corner).fX ||
324         maxInset >= rrect.width() - rrect.radii(SkRRect::kUpperRight_Corner).fX ||
325         maxInset >= rrect.height() - rrect.radii(SkRRect::kLowerLeft_Corner).fY || // Y corner cross
326         maxInset >= rrect.height() - rrect.radii(SkRRect::kLowerRight_Corner).fY ||
327         maxInset >= rrect.height() - rrect.radii(SkRRect::kUpperLeft_Corner).fY ||
328         maxInset >= rrect.height() - rrect.radii(SkRRect::kUpperRight_Corner).fY) {
329         // All interior vertices need to snap to the center
330         centerWeight = 2.f;
331     }
332 
333     // The normalized corner template is defined relative to the quarter circle with positive X
334     // and positive Y, with a counter clockwise winding (if +Y points down). This corresponds to
335     // the bottom-right corner.
336     static constexpr SkV4 kBRBasis = { 1.f,  0.f,  0.f,  1.f};
337     static constexpr SkV4 kTRBasis = { 0.f,  1.f, -1.f,  0.f};
338     static constexpr SkV4 kTLBasis = {-1.f,  0.f,  0.f, -1.f};
339     static constexpr SkV4 kBLBasis = { 0.f, -1.f,  1.f,  0.f};
340 
341     compute_corner(devPts + kBR, m, kBRBasis,
342                    {rrect.getBounds().fRight,
343                     rrect.getBounds().fBottom},
344                    {rrect.radii(SkRRect::kLowerRight_Corner).fX,
345                     rrect.radii(SkRRect::kLowerRight_Corner).fY},
346                    devCenter, centerWeight, localAARadius, strokeRadius, join);
347     compute_corner(devPts + kTR, m, kTRBasis,
348                    {rrect.getBounds().fRight,
349                     rrect.getBounds().fTop},
350                    {rrect.radii(SkRRect::kUpperRight_Corner).fY,
351                     rrect.radii(SkRRect::kUpperRight_Corner).fX},
352                    devCenter, centerWeight, localAARadius,strokeRadius, join);
353     compute_corner(devPts + kTL, m, kTLBasis,
354                    {rrect.getBounds().fLeft,
355                     rrect.getBounds().fTop},
356                    {rrect.radii(SkRRect::kUpperLeft_Corner).fX,
357                     rrect.radii(SkRRect::kUpperLeft_Corner).fY},
358                    devCenter, centerWeight, localAARadius,strokeRadius, join);
359     compute_corner(devPts + kBL, m, kBLBasis,
360                    {rrect.getBounds().fLeft,
361                     rrect.getBounds().fBottom},
362                    {rrect.radii(SkRRect::kLowerLeft_Corner).fY,
363                     rrect.radii(SkRRect::kLowerLeft_Corner).fX},
364                    devCenter, centerWeight, localAARadius,strokeRadius, join);
365 }
366 
367 // All indices
368 static const uint16_t kIndices[] = {
369     // Exterior AA ramp outset
370     kBR+0,kBR+6,kBR+1,kBR+7,kBR+2,kBR+8,kBR+3,kBR+8,kBR+4,kBR+9,kBR+5,kBR+9,
371     kTR+0,kTR+6,kTR+1,kTR+7,kTR+2,kTR+8,kTR+3,kTR+8,kTR+4,kTR+9,kTR+5,kTR+9,
372     kTL+0,kTL+6,kTL+1,kTL+7,kTL+2,kTL+8,kTL+3,kTL+8,kTL+4,kTL+9,kTL+5,kTL+9,
373     kBL+0,kBL+6,kBL+1,kBL+7,kBL+2,kBL+8,kBL+3,kBL+8,kBL+4,kBL+9,kBL+5,kBL+9,
374     kBR+0,kBR+6,kBR+6, // close and extra vertex to jump to next strip
375     // Outer to central curve
376     kBR+6,kBR+10,kBR+7,kBR+11,kBR+8,kBR+12,kBR+9,kBR+13,
377     kTR+6,kTR+10,kTR+7,kTR+11,kTR+8,kTR+12,kTR+9,kTR+13,
378     kTL+6,kTL+10,kTL+7,kTL+11,kTL+8,kTL+12,kTL+9,kTL+13,
379     kBL+6,kBL+10,kBL+7,kBL+11,kBL+8,kBL+12,kBL+9,kBL+13,
380     kBR+6,kBR+10,kBR+10, // close and extra vertex to jump to next strip
381     // Center to inner curve's insets
382     kBR+10,kBR+14,kBR+11,kBR+15,kBR+12,kBR+16,kBR+13,kBR+16,
383     kTR+10,kTR+14,kTR+11,kTR+15,kTR+12,kTR+16,kTR+13,kTR+16,
384     kTL+10,kTL+14,kTL+11,kTL+15,kTL+12,kTL+16,kTL+13,kTL+16,
385     kBL+10,kBL+14,kBL+11,kBL+15,kBL+12,kBL+16,kBL+13,kBL+16,
386     kBR+10,kBR+14,kBR+14, // close and extra vertex to jump to next strip
387     // Inner inset to center of shape
388     kBR+14,kBR+17,kBR+15,kBR+17,kBR+16,kBR+16,kBR+18,kTR+14,
389     kTR+14,kTR+17,kTR+15,kTR+17,kTR+16,kTR+16,kTR+18,kTL+14,
390     kTL+14,kTL+17,kTL+15,kTL+17,kTL+16,kTL+16,kTL+18,kBL+14,
391     kBL+14,kBL+17,kBL+15,kBL+17,kBL+16,kBL+16,kBL+18,kBR+14 // close
392 };
393 
394 // Separated to draw with different colors (vs. duplicating vertices to change colors).
395 static const uint16_t kOuterCornerIndices[] = {
396     kBR+0,  kBR+0,kBR+6,kBR+1,kBR+7,kBR+2,kBR+8,kBR+3,kBR+8,kBR+4,kBR+9,kBR+5,  kBR+5,
397     kTR+0,  kTR+0,kTR+6,kTR+1,kTR+7,kTR+2,kTR+8,kTR+3,kTR+8,kTR+4,kTR+9,kTR+5,  kTR+5,
398     kTL+0,  kTL+0,kTL+6,kTL+1,kTL+7,kTL+2,kTL+8,kTL+3,kTL+8,kTL+4,kTL+9,kTL+5,  kTL+5,
399     kBL+0,  kBL+0,kBL+6,kBL+1,kBL+7,kBL+2,kBL+8,kBL+3,kBL+8,kBL+4,kBL+9,kBL+5,  kBL+5,
400 
401     kBR+6,  kBR+6,kBR+10,kBR+7,kBR+11,kBR+8,kBR+12,kBR+9,kBR+13,  kBR+13,
402     kTR+6,  kTR+6,kTR+10,kTR+7,kTR+11,kTR+8,kTR+12,kTR+9,kTR+13,  kTR+13,
403     kTL+6,  kTL+6,kTL+10,kTL+7,kTL+11,kTL+8,kTL+12,kTL+9,kTL+13,  kTL+13,
404     kBL+6,  kBL+6,kBL+10,kBL+7,kBL+11,kBL+8,kBL+12,kBL+9,kBL+13,  kBL+13
405 };
406 
407 static const uint16_t kInnerCornerIndices[] = {
408     kBR+10,  kBR+10,kBR+14,kBR+11,kBR+15,kBR+12,kBR+16,kBR+13,  kBR+13,
409     kTR+10,  kTR+10,kTR+14,kTR+11,kTR+15,kTR+12,kTR+16,kTR+13,  kTR+13,
410     kTL+10,  kTL+10,kTL+14,kTL+11,kTL+15,kTL+12,kTL+16,kTL+13,  kTL+13,
411     kBL+10,  kBL+10,kBL+14,kBL+11,kBL+15,kBL+12,kBL+16,kBL+13,  kBL+13,
412 };
413 
414 static const uint16_t kInteriorIndices[] = {
415     kBR+14,kBR+17,kBR+15,kBR+17,kBR+16,kBR+16,kBR+18,kTR+14,
416     kTR+14,kTR+17,kTR+15,kTR+17,kTR+16,kTR+16,kTR+18,kTL+14,
417     kTL+14,kTL+17,kTL+15,kTL+17,kTL+16,kTL+16,kTL+18,kBL+14,
418     kBL+14,kBL+17,kBL+15,kBL+17,kBL+16,kBL+16,kBL+18,kBR+14 // close
419 };
420 
421 // Implicit in the original mesh from the tri-strip connections between corners
422 static const uint16_t kEdgeIndices[] = {
423     kBR+5,   kBR+5,kBR+9,kTR+0,kTR+6,      kTR+6,
424     kBR+9,   kBR+9,kBR+13,kTR+6,kTR+10,    kTR+10,
425     kBR+13,  kBR+13,kBR+16,kTR+10,kTR+14,  kTR+14,
426 
427     kTR+5,   kTR+5,kTR+9,kTL+0,kTL+6,      kTL+6,
428     kTR+9,   kTR+9,kTR+13,kTL+6,kTL+10,    kTL+10,
429     kTR+13,  kTR+13,kTR+16,kTL+10,kTL+14,  kTL+14,
430 
431     kTL+5,   kTL+5,kTL+9,kBL+0,kBL+6,      kBL+6,
432     kTL+9,   kTL+9,kTL+13,kBL+6,kBL+10,    kBL+10,
433     kTL+13,  kTL+13,kTL+16,kBL+10,kBL+14,  kBL+14,
434 
435     kBL+5,   kBL+5,kBL+9,kBR+0,kBR+6,      kBR+6,
436     kBL+9,   kBL+9,kBL+13,kBR+6,kBR+10,    kBR+10,
437     kBL+13,  kBL+13,kBL+16,kBR+10,kBR+14,  kBR+14,
438 };
439 
440 class GraphitePrimitivesSlide : public ClickHandlerSlide {
441     static constexpr float kControlPointRadius = 3.f;
442     static constexpr float kBaseScale = 50.f;
443 
444 public:
GraphitePrimitivesSlide()445     GraphitePrimitivesSlide()
446         : fOrigin{300.f, 300.f}
447         , fXAxisPoint{300.f + kBaseScale, 300.f}
448         , fYAxisPoint{300.f, 300.f + kBaseScale}
449         , fStrokeWidth{10.f}
450         , fJoinMode(SkPaint::kMiter_Join)
451         , fMode(PrimitiveMode::kFillRect) {
452         fName = "GraphitePrimitives";
453     }
454 
draw(SkCanvas * canvas)455     void draw(SkCanvas* canvas) override {
456         canvas->save();
457         SkM44 viewMatrix = canvas->getLocalToDevice();
458 
459         canvas->concat(this->basisMatrix());
460 
461         SkM44 totalMatrix = canvas->getLocalToDevice();
462 
463             // Base shape + style
464             SkRRect rrect = this->primitiveShape();
465             canvas->drawRRect(rrect, paint(SK_ColorBLUE, this->strokeWidth(), fJoinMode));
466         canvas->restore();
467 
468         canvas->save();
469         canvas->resetMatrix();
470             // Draw the full mesh directly in device space
471             this->drawVertices(canvas, totalMatrix);
472             // Draw the controls in device space so we get consistent circles for the click points.
473             SkV4 origin = viewMatrix.map(fOrigin.x, fOrigin.y, 0.f, 1.f);
474             SkV4 xAxis = viewMatrix.map(fXAxisPoint.x, fXAxisPoint.y, 0.f, 1.f);
475             SkV4 yAxis = viewMatrix.map(fYAxisPoint.x, fYAxisPoint.y, 0.f, 1.f);
476 
477             // Axes
478             canvas->drawLine({origin.x/origin.w, origin.y/origin.w},
479                              {xAxis.x/xAxis.w, xAxis.y/xAxis.w}, paint(SK_ColorRED, 0.f));
480             canvas->drawLine({origin.x/origin.w, origin.y/origin.w},
481                              {yAxis.x/yAxis.w, yAxis.y/yAxis.w}, paint(SK_ColorGREEN, 0.f));
482 
483             // Control points
484             canvas->drawCircle({origin.x/origin.w, origin.y/origin.w},
485                                kControlPointRadius, paint(SK_ColorBLACK));
486             canvas->drawCircle({xAxis.x/xAxis.w, xAxis.y/xAxis.w},
487                                kControlPointRadius, paint(SK_ColorRED));
488             canvas->drawCircle({yAxis.x/yAxis.w, yAxis.y/yAxis.w},
489                                kControlPointRadius, paint(SK_ColorGREEN));
490         canvas->restore();
491     }
492 
493     bool onChar(SkUnichar) override;
494 
495 protected:
496     Click* onFindClickHandler(SkScalar x, SkScalar y, skui::ModifierKey) override;
497     bool onClick(Click*) override;
498 
499 private:
500     class Click;
501 
502     enum class PrimitiveMode {
503         kFillRect,
504         kFillRRect,
505         kStrokeRect,
506         kStrokeRRect
507     };
508 
509     // Computed from 3 control points. Concat with CTM to get total matrix.
basisMatrix() const510     SkM44 basisMatrix() const {
511         SkV2 xAxis = (fXAxisPoint - fOrigin) / kBaseScale;
512         SkV2 yAxis = (fYAxisPoint - fOrigin) / kBaseScale;
513 
514         return SkM44::Cols({xAxis.x, xAxis.y, 0.f, 0.f},
515                            {yAxis.x, yAxis.y, 0.f, 0.f},
516                            {0.f, 0.f, 1.f, 0.f},
517                            {fOrigin.x, fOrigin.y, 0.f, 1.f});
518     }
519 
strokeWidth() const520     float strokeWidth() const {
521         if (fMode == PrimitiveMode::kFillRect || fMode == PrimitiveMode::kFillRRect) {
522             return -1.f;
523         }
524         return fStrokeWidth;
525     }
526 
primitiveShape() const527     SkRRect primitiveShape() const {
528         static const SkRect kOuterBounds = SkRect::MakeLTRB(-kBaseScale, -kBaseScale,
529                                                             kBaseScale, kBaseScale);
530         // Filled rounded rects can have arbitrary corners
531         static const SkVector kOuterRadii[4] = { { 0.25f * kBaseScale, 0.75f * kBaseScale },
532                                                  { 0.f, 0.f},
533                                                  { 0.5f * kBaseScale, 0.5f * kBaseScale },
534                                                  { 0.75f * kBaseScale, 0.25f * kBaseScale } };
535         // // Stroked rounded rects will only have circular corners
536         static const SkVector kStrokeRadii[4] = { { 0.25f * kBaseScale, 0.25f * kBaseScale },
537                                                   { 0.f, 0.f },
538                                                   { 0.5f * kBaseScale, 0.5f * kBaseScale },
539                                                   { 0.75f * kBaseScale, 0.75f * kBaseScale } };
540 
541         float strokeRadius = 0.5f * fStrokeWidth;
542         switch(fMode) {
543             case PrimitiveMode::kFillRect:
544                 return SkRRect::MakeRect(kOuterBounds.makeOutset(strokeRadius, strokeRadius));
545             case PrimitiveMode::kFillRRect: {
546                 SkRRect rrect;
547                 rrect.setRectRadii(kOuterBounds, kOuterRadii);
548                 rrect.outset(strokeRadius, strokeRadius);
549                 return rrect; }
550             case PrimitiveMode::kStrokeRect:
551                 return SkRRect::MakeRect(kOuterBounds);
552             case PrimitiveMode::kStrokeRRect: {
553                 SkRRect rrect;
554                 rrect.setRectRadii(kOuterBounds, kStrokeRadii);
555                 return rrect;
556             }
557         }
558 
559         SkUNREACHABLE;
560     }
561 
drawVertices(SkCanvas * canvas,const SkM44 & ctm)562     void drawVertices(SkCanvas* canvas, const SkM44& ctm) {
563         SkRRect rrect = this->primitiveShape();
564         float strokeRadius = 0.5f * this->strokeWidth();
565 
566         SkV3 points[kVertexCount];
567         SkPoint vertices[kVertexCount];
568         compute_vertices(points, ctm, rrect, strokeRadius, fJoinMode);
569         // SkCanvas::drawVertices() wants SkPoint, but normally we'd let the GPU handle the
570         // perspective division and clipping.
571         for (size_t i = 0; i < kVertexCount; ++i) {
572             vertices[i] = SkPoint{points[i].x/points[i].z, points[i].y/points[i].z};
573         }
574 
575         auto drawMeshSubset = [vertices, canvas](SkColor color,
576                                                  const uint16_t* indices,
577                                                  size_t indexCount) {
578             sk_sp<SkVertices> mesh = SkVertices::MakeCopy(
579                     SkVertices::kTriangleStrip_VertexMode, kVertexCount, vertices,
580                     nullptr, nullptr, (int) indexCount, indices);
581             SkPaint meshPaint;
582             meshPaint.setColor(color);
583             meshPaint.setAlphaf(0.5f);
584             canvas->drawVertices(mesh, SkBlendMode::kSrc, meshPaint);
585         };
586         if (fColorize) {
587             drawMeshSubset(SK_ColorGRAY,
588                            kEdgeIndices,
589                            std::size(kEdgeIndices));
590             drawMeshSubset(SK_ColorDKGRAY,
591                            kInteriorIndices,
592                            std::size(kInteriorIndices));
593             drawMeshSubset(SK_ColorMAGENTA,
594                            kInnerCornerIndices,
595                            std::size(kInnerCornerIndices));
596             drawMeshSubset(SK_ColorCYAN,
597                            kOuterCornerIndices,
598                            std::size(kOuterCornerIndices));
599         } else {
600             drawMeshSubset(SK_ColorGRAY, kIndices, std::size(kIndices));
601         }
602 
603         // Draw the edges over the triangle strip mesh, but keep track of edges already drawn so
604         // that we don't oversaturate AA on edges shared by multiple triangles.
605         std::unordered_set<uint32_t> edges;
606         auto drawEdge = [&edges, vertices, canvas](uint16_t e0, uint16_t e1) {
607             uint32_t edgeID = (std::max(e0, e1) << 16) | std::min(e0, e1);
608             if (edges.find(edgeID) == edges.end()) {
609                 edges.insert(edgeID);
610                 if (SkScalarNearlyEqual(vertices[e0].fX, vertices[e1].fX) &&
611                     SkScalarNearlyEqual(vertices[e0].fY, vertices[e1].fY)) {
612                     return;
613                 }
614                 canvas->drawLine(vertices[e0], vertices[e1], paint(SK_ColorBLACK, 0.f));
615             }
616         };
617         for (size_t i = 2; i < std::size(kIndices); ++i) {
618             drawEdge(kIndices[i-1], kIndices[i]);
619             drawEdge(kIndices[i-2], kIndices[i]);
620         }
621     }
622 
623     // This Sample is responsive to the entire transform of the viewer slide, including the
624     // transform (rotation, scale, and perspective) selected from the widget. The 3 points below
625     // define the location and basis of the local coordinate space, relative to the viewer's
626     // coordinate space. This is used instead of the root canvas coordinate space because it aligns
627     // with the coordinate space that the click handler operates in.
628     SkV2 fOrigin;
629     SkV2 fXAxisPoint;
630     SkV2 fYAxisPoint;
631 
632     float fStrokeWidth;
633     SkPaint::Join fJoinMode;
634     PrimitiveMode fMode;
635     bool fColorize = true;
636 };
637 
638 class GraphitePrimitivesSlide::Click : public ClickHandlerSlide::Click {
639 public:
Click(SkV2 * point)640     Click(SkV2* point) : fPoint(point) {}
641 
drag()642     void drag() {
643         SkVector delta = fCurr - fPrev;
644         *fPoint += {delta.fX, delta.fY};
645     }
646 
647 private:
648     SkV2* fPoint;
649 };
650 
onFindClickHandler(SkScalar x,SkScalar y,skui::ModifierKey)651 ClickHandlerSlide::Click* GraphitePrimitivesSlide::onFindClickHandler(SkScalar x, SkScalar y,
652                                                                       skui::ModifierKey) {
653     auto selected = [x,y](const SkV2& p) {
654         return ((p - SkV2{x,y}).length() < kControlPointRadius);
655     };
656 
657     if (selected(fOrigin)) {
658         return new Click(&fOrigin);
659     } else if (selected(fXAxisPoint)) {
660         return new Click(&fXAxisPoint);
661     } else if (selected(fYAxisPoint)) {
662         return new Click(&fYAxisPoint);
663     } else {
664         return nullptr;
665     }
666 }
667 
onClick(ClickHandlerSlide::Click * click)668 bool GraphitePrimitivesSlide::onClick(ClickHandlerSlide::Click* click) {
669     Click* myClick = (Click*) click;
670     myClick->drag();
671     return true;
672 }
673 
onChar(SkUnichar code)674 bool GraphitePrimitivesSlide::onChar(SkUnichar code) {
675         switch(code) {
676             case '1':
677                 fMode = PrimitiveMode::kFillRect;
678                 return true;
679             case '2':
680                 fMode = PrimitiveMode::kFillRRect;
681                 return true;
682             case '3':
683                 fMode = PrimitiveMode::kStrokeRect;
684                 return true;
685             case '4':
686                 fMode = PrimitiveMode::kStrokeRRect;
687                 return true;
688             case '-':
689                 fStrokeWidth = std::max(0.f, fStrokeWidth - 0.4f);
690                 return true;
691             case '=':
692                 fStrokeWidth = std::min(5 * kBaseScale, fStrokeWidth + 0.4f);
693                 return true;
694             case 'q':
695                 fJoinMode = SkPaint::kRound_Join;
696                 return true;
697             case 'w':
698                 fJoinMode = SkPaint::kBevel_Join;
699                 return true;
700             case 'e':
701                 fJoinMode = SkPaint::kMiter_Join;
702                 return true;
703             case 'r':
704                 fStrokeWidth = 10.f;
705                 fOrigin = {300.f, 300.f};
706                 fXAxisPoint = {300.f + kBaseScale, 300.f};
707                 fYAxisPoint = {300.f, 300.f + kBaseScale};
708                 return true;
709             case 'c':
710                 fColorize = !fColorize;
711                 return true;
712         }
713         return false;
714 }
715 
716 DEF_SLIDE(return new GraphitePrimitivesSlide();)
717