1 /*
2 * Copyright (C) 2012 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17 #define LOG_TAG "OpenGLRenderer"
18 #define LOG_NDEBUG 1
19 #define ATRACE_TAG ATRACE_TAG_VIEW
20
21 #define VERTEX_DEBUG 0
22
23 #if VERTEX_DEBUG
24 #define DEBUG_DUMP_ALPHA_BUFFER() \
25 for (unsigned int i = 0; i < vertexBuffer.getSize(); i++) { \
26 ALOGD("point %d at %f %f, alpha %f", \
27 i, buffer[i].x, buffer[i].y, buffer[i].alpha); \
28 }
29 #define DEBUG_DUMP_BUFFER() \
30 for (unsigned int i = 0; i < vertexBuffer.getSize(); i++) { \
31 ALOGD("point %d at %f %f", i, buffer[i].x, buffer[i].y); \
32 }
33 #else
34 #define DEBUG_DUMP_ALPHA_BUFFER()
35 #define DEBUG_DUMP_BUFFER()
36 #endif
37
38 #include <SkPath.h>
39 #include <SkPaint.h>
40
41 #include <stdlib.h>
42 #include <stdint.h>
43 #include <sys/types.h>
44
45 #include <utils/Log.h>
46 #include <utils/Trace.h>
47
48 #include "PathTessellator.h"
49 #include "Matrix.h"
50 #include "Vector.h"
51 #include "Vertex.h"
52 #include "utils/MathUtils.h"
53
54 namespace android {
55 namespace uirenderer {
56
57 #define OUTLINE_REFINE_THRESHOLD_SQUARED (0.5f * 0.5f)
58 #define ROUND_CAP_THRESH 0.25f
59 #define PI 3.1415926535897932f
60 #define MAX_DEPTH 15
61
62 /**
63 * Extracts the x and y scale from the transform as positive values, and clamps them
64 */
extractTessellationScales(const Matrix4 & transform,float * scaleX,float * scaleY)65 void PathTessellator::extractTessellationScales(const Matrix4& transform,
66 float* scaleX, float* scaleY) {
67 if (CC_LIKELY(transform.isPureTranslate())) {
68 *scaleX = 1.0f;
69 *scaleY = 1.0f;
70 } else {
71 float m00 = transform.data[Matrix4::kScaleX];
72 float m01 = transform.data[Matrix4::kSkewY];
73 float m10 = transform.data[Matrix4::kSkewX];
74 float m11 = transform.data[Matrix4::kScaleY];
75 *scaleX = MathUtils::clampTessellationScale(sqrt(m00 * m00 + m01 * m01));
76 *scaleY = MathUtils::clampTessellationScale(sqrt(m10 * m10 + m11 * m11));
77 }
78 }
79
80 /**
81 * Produces a pseudo-normal for a vertex, given the normals of the two incoming lines. If the offset
82 * from each vertex in a perimeter is calculated, the resultant lines connecting the offset vertices
83 * will be offset by 1.0
84 *
85 * Note that we can't add and normalize the two vectors, that would result in a rectangle having an
86 * offset of (sqrt(2)/2, sqrt(2)/2) at each corner, instead of (1, 1)
87 *
88 * NOTE: assumes angles between normals 90 degrees or less
89 */
totalOffsetFromNormals(const Vector2 & normalA,const Vector2 & normalB)90 inline static Vector2 totalOffsetFromNormals(const Vector2& normalA, const Vector2& normalB) {
91 return (normalA + normalB) / (1 + fabs(normalA.dot(normalB)));
92 }
93
94 /**
95 * Structure used for storing useful information about the SkPaint and scale used for tessellating
96 */
97 struct PaintInfo {
98 public:
PaintInfoandroid::uirenderer::PaintInfo99 PaintInfo(const SkPaint* paint, const mat4& transform) :
100 style(paint->getStyle()), cap(paint->getStrokeCap()), isAA(paint->isAntiAlias()),
101 halfStrokeWidth(paint->getStrokeWidth() * 0.5f), maxAlpha(1.0f) {
102 // compute inverse scales
103 if (CC_LIKELY(transform.isPureTranslate())) {
104 inverseScaleX = 1.0f;
105 inverseScaleY = 1.0f;
106 } else {
107 float scaleX, scaleY;
108 PathTessellator::extractTessellationScales(transform, &scaleX, &scaleY);
109 inverseScaleX = 1.0f / scaleX;
110 inverseScaleY = 1.0f / scaleY;
111 }
112
113 if (isAA && halfStrokeWidth != 0 && inverseScaleX == inverseScaleY &&
114 2 * halfStrokeWidth < inverseScaleX) {
115 // AA, with non-hairline stroke, width < 1 pixel. Scale alpha and treat as hairline.
116 maxAlpha *= (2 * halfStrokeWidth) / inverseScaleX;
117 halfStrokeWidth = 0.0f;
118 }
119 }
120
121 SkPaint::Style style;
122 SkPaint::Cap cap;
123 bool isAA;
124 float inverseScaleX;
125 float inverseScaleY;
126 float halfStrokeWidth;
127 float maxAlpha;
128
scaleOffsetForStrokeWidthandroid::uirenderer::PaintInfo129 inline void scaleOffsetForStrokeWidth(Vector2& offset) const {
130 if (halfStrokeWidth == 0.0f) {
131 // hairline - compensate for scale
132 offset.x *= 0.5f * inverseScaleX;
133 offset.y *= 0.5f * inverseScaleY;
134 } else {
135 offset *= halfStrokeWidth;
136 }
137 }
138
139 /**
140 * NOTE: the input will not always be a normal, especially for sharp edges - it should be the
141 * result of totalOffsetFromNormals (see documentation there)
142 */
deriveAAOffsetandroid::uirenderer::PaintInfo143 inline Vector2 deriveAAOffset(const Vector2& offset) const {
144 return (Vector2){offset.x * 0.5f * inverseScaleX, offset.y * 0.5f * inverseScaleY};
145 }
146
147 /**
148 * Returns the number of cap divisions beyond the minimum 2 (kButt_Cap/kSquareCap will return 0)
149 * Should only be used when stroking and drawing caps
150 */
capExtraDivisionsandroid::uirenderer::PaintInfo151 inline int capExtraDivisions() const {
152 if (cap == SkPaint::kRound_Cap) {
153 if (halfStrokeWidth == 0.0f) return 2;
154
155 // ROUND_CAP_THRESH is the maximum error for polygonal approximation of the round cap
156 const float errConst = (-ROUND_CAP_THRESH / halfStrokeWidth + 1);
157 const float targetCosVal = 2 * errConst * errConst - 1;
158 int neededDivisions = (int)(ceilf(PI / acos(targetCosVal)/2)) * 2;
159 return neededDivisions;
160 }
161 return 0;
162 }
163
164 /**
165 * Outset the bounds of point data (for line endpoints or points) to account for stroke
166 * geometry.
167 *
168 * bounds are in pre-scaled space.
169 */
expandBoundsForStrokeandroid::uirenderer::PaintInfo170 void expandBoundsForStroke(Rect* bounds) const {
171 if (halfStrokeWidth == 0) {
172 // hairline, outset by (0.5f + fudge factor) in post-scaling space
173 bounds->outset(fabs(inverseScaleX) * (0.5f + Vertex::GeometryFudgeFactor()),
174 fabs(inverseScaleY) * (0.5f + Vertex::GeometryFudgeFactor()));
175 } else {
176 // non hairline, outset by half stroke width pre-scaled, and fudge factor post scaled
177 bounds->outset(halfStrokeWidth + fabs(inverseScaleX) * Vertex::GeometryFudgeFactor(),
178 halfStrokeWidth + fabs(inverseScaleY) * Vertex::GeometryFudgeFactor());
179 }
180 }
181 };
182
getFillVerticesFromPerimeter(const Vector<Vertex> & perimeter,VertexBuffer & vertexBuffer)183 void getFillVerticesFromPerimeter(const Vector<Vertex>& perimeter, VertexBuffer& vertexBuffer) {
184 Vertex* buffer = vertexBuffer.alloc<Vertex>(perimeter.size());
185
186 int currentIndex = 0;
187 // zig zag between all previous points on the inside of the hull to create a
188 // triangle strip that fills the hull
189 int srcAindex = 0;
190 int srcBindex = perimeter.size() - 1;
191 while (srcAindex <= srcBindex) {
192 buffer[currentIndex++] = perimeter[srcAindex];
193 if (srcAindex == srcBindex) break;
194 buffer[currentIndex++] = perimeter[srcBindex];
195 srcAindex++;
196 srcBindex--;
197 }
198 }
199
200 /*
201 * Fills a vertexBuffer with non-alpha vertices, zig-zagging at each perimeter point to create a
202 * tri-strip as wide as the stroke.
203 *
204 * Uses an additional 2 vertices at the end to wrap around, closing the tri-strip
205 * (for a total of perimeter.size() * 2 + 2 vertices)
206 */
getStrokeVerticesFromPerimeter(const PaintInfo & paintInfo,const Vector<Vertex> & perimeter,VertexBuffer & vertexBuffer)207 void getStrokeVerticesFromPerimeter(const PaintInfo& paintInfo, const Vector<Vertex>& perimeter,
208 VertexBuffer& vertexBuffer) {
209 Vertex* buffer = vertexBuffer.alloc<Vertex>(perimeter.size() * 2 + 2);
210
211 int currentIndex = 0;
212 const Vertex* last = &(perimeter[perimeter.size() - 1]);
213 const Vertex* current = &(perimeter[0]);
214 Vector2 lastNormal = {current->y - last->y, last->x - current->x};
215 lastNormal.normalize();
216 for (unsigned int i = 0; i < perimeter.size(); i++) {
217 const Vertex* next = &(perimeter[i + 1 >= perimeter.size() ? 0 : i + 1]);
218 Vector2 nextNormal = {next->y - current->y, current->x - next->x};
219 nextNormal.normalize();
220
221 Vector2 totalOffset = totalOffsetFromNormals(lastNormal, nextNormal);
222 paintInfo.scaleOffsetForStrokeWidth(totalOffset);
223
224 Vertex::set(&buffer[currentIndex++],
225 current->x + totalOffset.x,
226 current->y + totalOffset.y);
227
228 Vertex::set(&buffer[currentIndex++],
229 current->x - totalOffset.x,
230 current->y - totalOffset.y);
231
232 last = current;
233 current = next;
234 lastNormal = nextNormal;
235 }
236
237 // wrap around to beginning
238 buffer[currentIndex++] = buffer[0];
239 buffer[currentIndex++] = buffer[1];
240
241 DEBUG_DUMP_BUFFER();
242 }
243
storeBeginEnd(const PaintInfo & paintInfo,const Vertex & center,const Vector2 & normal,Vertex * buffer,int & currentIndex,bool begin)244 static inline void storeBeginEnd(const PaintInfo& paintInfo, const Vertex& center,
245 const Vector2& normal, Vertex* buffer, int& currentIndex, bool begin) {
246 Vector2 strokeOffset = normal;
247 paintInfo.scaleOffsetForStrokeWidth(strokeOffset);
248
249 Vector2 referencePoint = {center.x, center.y};
250 if (paintInfo.cap == SkPaint::kSquare_Cap) {
251 Vector2 rotated = {-strokeOffset.y, strokeOffset.x};
252 referencePoint += rotated * (begin ? -1 : 1);
253 }
254
255 Vertex::set(&buffer[currentIndex++], referencePoint + strokeOffset);
256 Vertex::set(&buffer[currentIndex++], referencePoint - strokeOffset);
257 }
258
259 /**
260 * Fills a vertexBuffer with non-alpha vertices similar to getStrokeVerticesFromPerimeter, except:
261 *
262 * 1 - Doesn't need to wrap around, since the input vertices are unclosed
263 *
264 * 2 - can zig-zag across 'extra' vertices at either end, to create round caps
265 */
getStrokeVerticesFromUnclosedVertices(const PaintInfo & paintInfo,const Vector<Vertex> & vertices,VertexBuffer & vertexBuffer)266 void getStrokeVerticesFromUnclosedVertices(const PaintInfo& paintInfo,
267 const Vector<Vertex>& vertices, VertexBuffer& vertexBuffer) {
268 const int extra = paintInfo.capExtraDivisions();
269 const int allocSize = (vertices.size() + extra) * 2;
270 Vertex* buffer = vertexBuffer.alloc<Vertex>(allocSize);
271
272 const int lastIndex = vertices.size() - 1;
273 if (extra > 0) {
274 // tessellate both round caps
275 float beginTheta = atan2(
276 - (vertices[0].x - vertices[1].x),
277 vertices[0].y - vertices[1].y);
278 float endTheta = atan2(
279 - (vertices[lastIndex].x - vertices[lastIndex - 1].x),
280 vertices[lastIndex].y - vertices[lastIndex - 1].y);
281 const float dTheta = PI / (extra + 1);
282 const float radialScale = 2.0f / (1 + cos(dTheta));
283
284 int capOffset;
285 for (int i = 0; i < extra; i++) {
286 if (i < extra / 2) {
287 capOffset = extra - 2 * i - 1;
288 } else {
289 capOffset = 2 * i - extra;
290 }
291
292 beginTheta += dTheta;
293 Vector2 beginRadialOffset = {cos(beginTheta), sin(beginTheta)};
294 paintInfo.scaleOffsetForStrokeWidth(beginRadialOffset);
295 Vertex::set(&buffer[capOffset],
296 vertices[0].x + beginRadialOffset.x,
297 vertices[0].y + beginRadialOffset.y);
298
299 endTheta += dTheta;
300 Vector2 endRadialOffset = {cos(endTheta), sin(endTheta)};
301 paintInfo.scaleOffsetForStrokeWidth(endRadialOffset);
302 Vertex::set(&buffer[allocSize - 1 - capOffset],
303 vertices[lastIndex].x + endRadialOffset.x,
304 vertices[lastIndex].y + endRadialOffset.y);
305 }
306 }
307
308 int currentIndex = extra;
309 const Vertex* last = &(vertices[0]);
310 const Vertex* current = &(vertices[1]);
311 Vector2 lastNormal = {current->y - last->y, last->x - current->x};
312 lastNormal.normalize();
313
314 storeBeginEnd(paintInfo, vertices[0], lastNormal, buffer, currentIndex, true);
315
316 for (unsigned int i = 1; i < vertices.size() - 1; i++) {
317 const Vertex* next = &(vertices[i + 1]);
318 Vector2 nextNormal = {next->y - current->y, current->x - next->x};
319 nextNormal.normalize();
320
321 Vector2 strokeOffset = totalOffsetFromNormals(lastNormal, nextNormal);
322 paintInfo.scaleOffsetForStrokeWidth(strokeOffset);
323
324 Vector2 center = {current->x, current->y};
325 Vertex::set(&buffer[currentIndex++], center + strokeOffset);
326 Vertex::set(&buffer[currentIndex++], center - strokeOffset);
327
328 current = next;
329 lastNormal = nextNormal;
330 }
331
332 storeBeginEnd(paintInfo, vertices[lastIndex], lastNormal, buffer, currentIndex, false);
333
334 DEBUG_DUMP_BUFFER();
335 }
336
337 /**
338 * Populates a vertexBuffer with AlphaVertices to create an anti-aliased fill shape tessellation
339 *
340 * 1 - create the AA perimeter of unit width, by zig-zagging at each point around the perimeter of
341 * the shape (using 2 * perimeter.size() vertices)
342 *
343 * 2 - wrap around to the beginning to complete the perimeter (2 vertices)
344 *
345 * 3 - zig zag back and forth inside the shape to fill it (using perimeter.size() vertices)
346 */
getFillVerticesFromPerimeterAA(const PaintInfo & paintInfo,const Vector<Vertex> & perimeter,VertexBuffer & vertexBuffer,float maxAlpha=1.0f)347 void getFillVerticesFromPerimeterAA(const PaintInfo& paintInfo, const Vector<Vertex>& perimeter,
348 VertexBuffer& vertexBuffer, float maxAlpha = 1.0f) {
349 AlphaVertex* buffer = vertexBuffer.alloc<AlphaVertex>(perimeter.size() * 3 + 2);
350
351 // generate alpha points - fill Alpha vertex gaps in between each point with
352 // alpha 0 vertex, offset by a scaled normal.
353 int currentIndex = 0;
354 const Vertex* last = &(perimeter[perimeter.size() - 1]);
355 const Vertex* current = &(perimeter[0]);
356 Vector2 lastNormal = {current->y - last->y, last->x - current->x};
357 lastNormal.normalize();
358 for (unsigned int i = 0; i < perimeter.size(); i++) {
359 const Vertex* next = &(perimeter[i + 1 >= perimeter.size() ? 0 : i + 1]);
360 Vector2 nextNormal = {next->y - current->y, current->x - next->x};
361 nextNormal.normalize();
362
363 // AA point offset from original point is that point's normal, such that each side is offset
364 // by .5 pixels
365 Vector2 totalOffset = paintInfo.deriveAAOffset(totalOffsetFromNormals(lastNormal, nextNormal));
366
367 AlphaVertex::set(&buffer[currentIndex++],
368 current->x + totalOffset.x,
369 current->y + totalOffset.y,
370 0.0f);
371 AlphaVertex::set(&buffer[currentIndex++],
372 current->x - totalOffset.x,
373 current->y - totalOffset.y,
374 maxAlpha);
375
376 last = current;
377 current = next;
378 lastNormal = nextNormal;
379 }
380
381 // wrap around to beginning
382 buffer[currentIndex++] = buffer[0];
383 buffer[currentIndex++] = buffer[1];
384
385 // zig zag between all previous points on the inside of the hull to create a
386 // triangle strip that fills the hull, repeating the first inner point to
387 // create degenerate tris to start inside path
388 int srcAindex = 0;
389 int srcBindex = perimeter.size() - 1;
390 while (srcAindex <= srcBindex) {
391 buffer[currentIndex++] = buffer[srcAindex * 2 + 1];
392 if (srcAindex == srcBindex) break;
393 buffer[currentIndex++] = buffer[srcBindex * 2 + 1];
394 srcAindex++;
395 srcBindex--;
396 }
397
398 DEBUG_DUMP_BUFFER();
399 }
400
401 /**
402 * Stores geometry for a single, AA-perimeter (potentially rounded) cap
403 *
404 * For explanation of constants and general methodoloyg, see comments for
405 * getStrokeVerticesFromUnclosedVerticesAA() below.
406 */
storeCapAA(const PaintInfo & paintInfo,const Vector<Vertex> & vertices,AlphaVertex * buffer,bool isFirst,Vector2 normal,int offset)407 inline static void storeCapAA(const PaintInfo& paintInfo, const Vector<Vertex>& vertices,
408 AlphaVertex* buffer, bool isFirst, Vector2 normal, int offset) {
409 const int extra = paintInfo.capExtraDivisions();
410 const int extraOffset = (extra + 1) / 2;
411 const int capIndex = isFirst
412 ? 2 * offset + 6 + 2 * (extra + extraOffset)
413 : offset + 2 + 2 * extraOffset;
414 if (isFirst) normal *= -1;
415
416 // TODO: this normal should be scaled by radialScale if extra != 0, see totalOffsetFromNormals()
417 Vector2 AAOffset = paintInfo.deriveAAOffset(normal);
418
419 Vector2 strokeOffset = normal;
420 paintInfo.scaleOffsetForStrokeWidth(strokeOffset);
421 Vector2 outerOffset = strokeOffset + AAOffset;
422 Vector2 innerOffset = strokeOffset - AAOffset;
423
424 Vector2 capAAOffset = {0, 0};
425 if (paintInfo.cap != SkPaint::kRound_Cap) {
426 // if the cap is square or butt, the inside primary cap vertices will be inset in two
427 // directions - both normal to the stroke, and parallel to it.
428 capAAOffset = (Vector2){-AAOffset.y, AAOffset.x};
429 }
430
431 // determine referencePoint, the center point for the 4 primary cap vertices
432 const Vertex* point = isFirst ? vertices.begin() : (vertices.end() - 1);
433 Vector2 referencePoint = {point->x, point->y};
434 if (paintInfo.cap == SkPaint::kSquare_Cap) {
435 // To account for square cap, move the primary cap vertices (that create the AA edge) by the
436 // stroke offset vector (rotated to be parallel to the stroke)
437 Vector2 rotated = {-strokeOffset.y, strokeOffset.x};
438 referencePoint += rotated;
439 }
440
441 AlphaVertex::set(&buffer[capIndex + 0],
442 referencePoint.x + outerOffset.x + capAAOffset.x,
443 referencePoint.y + outerOffset.y + capAAOffset.y,
444 0.0f);
445 AlphaVertex::set(&buffer[capIndex + 1],
446 referencePoint.x + innerOffset.x - capAAOffset.x,
447 referencePoint.y + innerOffset.y - capAAOffset.y,
448 paintInfo.maxAlpha);
449
450 bool isRound = paintInfo.cap == SkPaint::kRound_Cap;
451
452 const int postCapIndex = (isRound && isFirst) ? (2 * extraOffset - 2) : capIndex + (2 * extra);
453 AlphaVertex::set(&buffer[postCapIndex + 2],
454 referencePoint.x - outerOffset.x + capAAOffset.x,
455 referencePoint.y - outerOffset.y + capAAOffset.y,
456 0.0f);
457 AlphaVertex::set(&buffer[postCapIndex + 3],
458 referencePoint.x - innerOffset.x - capAAOffset.x,
459 referencePoint.y - innerOffset.y - capAAOffset.y,
460 paintInfo.maxAlpha);
461
462 if (isRound) {
463 const float dTheta = PI / (extra + 1);
464 const float radialScale = 2.0f / (1 + cos(dTheta));
465 float theta = atan2(normal.y, normal.x);
466 int capPerimIndex = capIndex + 2;
467
468 for (int i = 0; i < extra; i++) {
469 theta += dTheta;
470
471 Vector2 radialOffset = {cos(theta), sin(theta)};
472
473 // scale to compensate for pinching at sharp angles, see totalOffsetFromNormals()
474 radialOffset *= radialScale;
475
476 AAOffset = paintInfo.deriveAAOffset(radialOffset);
477 paintInfo.scaleOffsetForStrokeWidth(radialOffset);
478 AlphaVertex::set(&buffer[capPerimIndex++],
479 referencePoint.x + radialOffset.x + AAOffset.x,
480 referencePoint.y + radialOffset.y + AAOffset.y,
481 0.0f);
482 AlphaVertex::set(&buffer[capPerimIndex++],
483 referencePoint.x + radialOffset.x - AAOffset.x,
484 referencePoint.y + radialOffset.y - AAOffset.y,
485 paintInfo.maxAlpha);
486
487 if (isFirst && i == extra - extraOffset) {
488 //copy most recent two points to first two points
489 buffer[0] = buffer[capPerimIndex - 2];
490 buffer[1] = buffer[capPerimIndex - 1];
491
492 capPerimIndex = 2; // start writing the rest of the round cap at index 2
493 }
494 }
495
496 if (isFirst) {
497 const int startCapFillIndex = capIndex + 2 * (extra - extraOffset) + 4;
498 int capFillIndex = startCapFillIndex;
499 for (int i = 0; i < extra + 2; i += 2) {
500 buffer[capFillIndex++] = buffer[1 + i];
501 // TODO: to support odd numbers of divisions, break here on the last iteration
502 buffer[capFillIndex++] = buffer[startCapFillIndex - 3 - i];
503 }
504 } else {
505 int capFillIndex = 6 * vertices.size() + 2 + 6 * extra - (extra + 2);
506 for (int i = 0; i < extra + 2; i += 2) {
507 buffer[capFillIndex++] = buffer[capIndex + 1 + i];
508 // TODO: to support odd numbers of divisions, break here on the last iteration
509 buffer[capFillIndex++] = buffer[capIndex + 3 + 2 * extra - i];
510 }
511 }
512 return;
513 }
514 if (isFirst) {
515 buffer[0] = buffer[postCapIndex + 2];
516 buffer[1] = buffer[postCapIndex + 3];
517 buffer[postCapIndex + 4] = buffer[1]; // degenerate tris (the only two!)
518 buffer[postCapIndex + 5] = buffer[postCapIndex + 1];
519 } else {
520 buffer[6 * vertices.size()] = buffer[postCapIndex + 1];
521 buffer[6 * vertices.size() + 1] = buffer[postCapIndex + 3];
522 }
523 }
524
525 /*
526 the geometry for an aa, capped stroke consists of the following:
527
528 # vertices | function
529 ----------------------------------------------------------------------
530 a) 2 | Start AA perimeter
531 b) 2, 2 * roundDivOff | First half of begin cap's perimeter
532 |
533 2 * middlePts | 'Outer' or 'Top' AA perimeter half (between caps)
534 |
535 a) 4 | End cap's
536 b) 2, 2 * roundDivs, 2 | AA perimeter
537 |
538 2 * middlePts | 'Inner' or 'bottom' AA perimeter half
539 |
540 a) 6 | Begin cap's perimeter
541 b) 2, 2*(rD - rDO + 1), | Last half of begin cap's perimeter
542 roundDivs, 2 |
543 |
544 2 * middlePts | Stroke's full opacity center strip
545 |
546 a) 2 | end stroke
547 b) 2, roundDivs | (and end cap fill, for round)
548
549 Notes:
550 * rows starting with 'a)' denote the Butt or Square cap vertex use, 'b)' denote Round
551
552 * 'middlePts' is (number of points in the unclosed input vertex list, minus 2) times two
553
554 * 'roundDivs' or 'rD' is the number of extra vertices (beyond the minimum of 2) that define the
555 round cap's shape, and is at least two. This will increase with cap size to sufficiently
556 define the cap's level of tessellation.
557
558 * 'roundDivOffset' or 'rDO' is the point about halfway along the start cap's round perimeter, where
559 the stream of vertices for the AA perimeter starts. By starting and ending the perimeter at
560 this offset, the fill of the stroke is drawn from this point with minimal extra vertices.
561
562 This means the outer perimeter starts at:
563 outerIndex = (2) OR (2 + 2 * roundDivOff)
564 the inner perimeter (since it is filled in reverse) starts at:
565 innerIndex = outerIndex + (4 * middlePts) + ((4) OR (4 + 2 * roundDivs)) - 1
566 the stroke starts at:
567 strokeIndex = innerIndex + 1 + ((6) OR (6 + 3 * roundDivs - 2 * roundDivOffset))
568
569 The total needed allocated space is either:
570 2 + 4 + 6 + 2 + 3 * (2 * middlePts) = 14 + 6 * middlePts = 2 + 6 * pts
571 or, for rounded caps:
572 (2 + 2 * rDO) + (4 + 2 * rD) + (2 * (rD - rDO + 1)
573 + roundDivs + 4) + (2 + roundDivs) + 3 * (2 * middlePts)
574 = 14 + 6 * middlePts + 6 * roundDivs
575 = 2 + 6 * pts + 6 * roundDivs
576 */
getStrokeVerticesFromUnclosedVerticesAA(const PaintInfo & paintInfo,const Vector<Vertex> & vertices,VertexBuffer & vertexBuffer)577 void getStrokeVerticesFromUnclosedVerticesAA(const PaintInfo& paintInfo,
578 const Vector<Vertex>& vertices, VertexBuffer& vertexBuffer) {
579
580 const int extra = paintInfo.capExtraDivisions();
581 const int allocSize = 6 * vertices.size() + 2 + 6 * extra;
582
583 AlphaVertex* buffer = vertexBuffer.alloc<AlphaVertex>(allocSize);
584
585 const int extraOffset = (extra + 1) / 2;
586 int offset = 2 * (vertices.size() - 2);
587 // there is no outer/inner here, using them for consistency with below approach
588 int currentAAOuterIndex = 2 + 2 * extraOffset;
589 int currentAAInnerIndex = currentAAOuterIndex + (2 * offset) + 3 + (2 * extra);
590 int currentStrokeIndex = currentAAInnerIndex + 7 + (3 * extra - 2 * extraOffset);
591
592 const Vertex* last = &(vertices[0]);
593 const Vertex* current = &(vertices[1]);
594 Vector2 lastNormal = {current->y - last->y, last->x - current->x};
595 lastNormal.normalize();
596
597 // TODO: use normal from bezier traversal for cap, instead of from vertices
598 storeCapAA(paintInfo, vertices, buffer, true, lastNormal, offset);
599
600 for (unsigned int i = 1; i < vertices.size() - 1; i++) {
601 const Vertex* next = &(vertices[i + 1]);
602 Vector2 nextNormal = {next->y - current->y, current->x - next->x};
603 nextNormal.normalize();
604
605 Vector2 totalOffset = totalOffsetFromNormals(lastNormal, nextNormal);
606 Vector2 AAOffset = paintInfo.deriveAAOffset(totalOffset);
607
608 Vector2 innerOffset = totalOffset;
609 paintInfo.scaleOffsetForStrokeWidth(innerOffset);
610 Vector2 outerOffset = innerOffset + AAOffset;
611 innerOffset -= AAOffset;
612
613 AlphaVertex::set(&buffer[currentAAOuterIndex++],
614 current->x + outerOffset.x,
615 current->y + outerOffset.y,
616 0.0f);
617 AlphaVertex::set(&buffer[currentAAOuterIndex++],
618 current->x + innerOffset.x,
619 current->y + innerOffset.y,
620 paintInfo.maxAlpha);
621
622 AlphaVertex::set(&buffer[currentStrokeIndex++],
623 current->x + innerOffset.x,
624 current->y + innerOffset.y,
625 paintInfo.maxAlpha);
626 AlphaVertex::set(&buffer[currentStrokeIndex++],
627 current->x - innerOffset.x,
628 current->y - innerOffset.y,
629 paintInfo.maxAlpha);
630
631 AlphaVertex::set(&buffer[currentAAInnerIndex--],
632 current->x - innerOffset.x,
633 current->y - innerOffset.y,
634 paintInfo.maxAlpha);
635 AlphaVertex::set(&buffer[currentAAInnerIndex--],
636 current->x - outerOffset.x,
637 current->y - outerOffset.y,
638 0.0f);
639
640 current = next;
641 lastNormal = nextNormal;
642 }
643
644 // TODO: use normal from bezier traversal for cap, instead of from vertices
645 storeCapAA(paintInfo, vertices, buffer, false, lastNormal, offset);
646
647 DEBUG_DUMP_ALPHA_BUFFER();
648 }
649
650
getStrokeVerticesFromPerimeterAA(const PaintInfo & paintInfo,const Vector<Vertex> & perimeter,VertexBuffer & vertexBuffer)651 void getStrokeVerticesFromPerimeterAA(const PaintInfo& paintInfo, const Vector<Vertex>& perimeter,
652 VertexBuffer& vertexBuffer) {
653 AlphaVertex* buffer = vertexBuffer.alloc<AlphaVertex>(6 * perimeter.size() + 8);
654
655 int offset = 2 * perimeter.size() + 3;
656 int currentAAOuterIndex = 0;
657 int currentStrokeIndex = offset;
658 int currentAAInnerIndex = offset * 2;
659
660 const Vertex* last = &(perimeter[perimeter.size() - 1]);
661 const Vertex* current = &(perimeter[0]);
662 Vector2 lastNormal = {current->y - last->y, last->x - current->x};
663 lastNormal.normalize();
664 for (unsigned int i = 0; i < perimeter.size(); i++) {
665 const Vertex* next = &(perimeter[i + 1 >= perimeter.size() ? 0 : i + 1]);
666 Vector2 nextNormal = {next->y - current->y, current->x - next->x};
667 nextNormal.normalize();
668
669 Vector2 totalOffset = totalOffsetFromNormals(lastNormal, nextNormal);
670 Vector2 AAOffset = paintInfo.deriveAAOffset(totalOffset);
671
672 Vector2 innerOffset = totalOffset;
673 paintInfo.scaleOffsetForStrokeWidth(innerOffset);
674 Vector2 outerOffset = innerOffset + AAOffset;
675 innerOffset -= AAOffset;
676
677 AlphaVertex::set(&buffer[currentAAOuterIndex++],
678 current->x + outerOffset.x,
679 current->y + outerOffset.y,
680 0.0f);
681 AlphaVertex::set(&buffer[currentAAOuterIndex++],
682 current->x + innerOffset.x,
683 current->y + innerOffset.y,
684 paintInfo.maxAlpha);
685
686 AlphaVertex::set(&buffer[currentStrokeIndex++],
687 current->x + innerOffset.x,
688 current->y + innerOffset.y,
689 paintInfo.maxAlpha);
690 AlphaVertex::set(&buffer[currentStrokeIndex++],
691 current->x - innerOffset.x,
692 current->y - innerOffset.y,
693 paintInfo.maxAlpha);
694
695 AlphaVertex::set(&buffer[currentAAInnerIndex++],
696 current->x - innerOffset.x,
697 current->y - innerOffset.y,
698 paintInfo.maxAlpha);
699 AlphaVertex::set(&buffer[currentAAInnerIndex++],
700 current->x - outerOffset.x,
701 current->y - outerOffset.y,
702 0.0f);
703
704 last = current;
705 current = next;
706 lastNormal = nextNormal;
707 }
708
709 // wrap each strip around to beginning, creating degenerate tris to bridge strips
710 buffer[currentAAOuterIndex++] = buffer[0];
711 buffer[currentAAOuterIndex++] = buffer[1];
712 buffer[currentAAOuterIndex++] = buffer[1];
713
714 buffer[currentStrokeIndex++] = buffer[offset];
715 buffer[currentStrokeIndex++] = buffer[offset + 1];
716 buffer[currentStrokeIndex++] = buffer[offset + 1];
717
718 buffer[currentAAInnerIndex++] = buffer[2 * offset];
719 buffer[currentAAInnerIndex++] = buffer[2 * offset + 1];
720 // don't need to create last degenerate tri
721
722 DEBUG_DUMP_ALPHA_BUFFER();
723 }
724
tessellatePath(const SkPath & path,const SkPaint * paint,const mat4 & transform,VertexBuffer & vertexBuffer)725 void PathTessellator::tessellatePath(const SkPath &path, const SkPaint* paint,
726 const mat4& transform, VertexBuffer& vertexBuffer) {
727 ATRACE_CALL();
728
729 const PaintInfo paintInfo(paint, transform);
730
731 Vector<Vertex> tempVertices;
732 float threshInvScaleX = paintInfo.inverseScaleX;
733 float threshInvScaleY = paintInfo.inverseScaleY;
734 if (paintInfo.style == SkPaint::kStroke_Style) {
735 // alter the bezier recursion threshold values we calculate in order to compensate for
736 // expansion done after the path vertices are found
737 SkRect bounds = path.getBounds();
738 if (!bounds.isEmpty()) {
739 threshInvScaleX *= bounds.width() / (bounds.width() + paint->getStrokeWidth());
740 threshInvScaleY *= bounds.height() / (bounds.height() + paint->getStrokeWidth());
741 }
742 }
743
744 // force close if we're filling the path, since fill path expects closed perimeter.
745 bool forceClose = paintInfo.style != SkPaint::kStroke_Style;
746 bool wasClosed = approximatePathOutlineVertices(path, forceClose,
747 threshInvScaleX * threshInvScaleX, threshInvScaleY * threshInvScaleY,
748 OUTLINE_REFINE_THRESHOLD_SQUARED, tempVertices);
749
750 if (!tempVertices.size()) {
751 // path was empty, return without allocating vertex buffer
752 return;
753 }
754
755 #if VERTEX_DEBUG
756 for (unsigned int i = 0; i < tempVertices.size(); i++) {
757 ALOGD("orig path: point at %f %f",
758 tempVertices[i].x, tempVertices[i].y);
759 }
760 #endif
761
762 if (paintInfo.style == SkPaint::kStroke_Style) {
763 if (!paintInfo.isAA) {
764 if (wasClosed) {
765 getStrokeVerticesFromPerimeter(paintInfo, tempVertices, vertexBuffer);
766 } else {
767 getStrokeVerticesFromUnclosedVertices(paintInfo, tempVertices, vertexBuffer);
768 }
769
770 } else {
771 if (wasClosed) {
772 getStrokeVerticesFromPerimeterAA(paintInfo, tempVertices, vertexBuffer);
773 } else {
774 getStrokeVerticesFromUnclosedVerticesAA(paintInfo, tempVertices, vertexBuffer);
775 }
776 }
777 } else {
778 // For kStrokeAndFill style, the path should be adjusted externally.
779 // It will be treated as a fill here.
780 if (!paintInfo.isAA) {
781 getFillVerticesFromPerimeter(tempVertices, vertexBuffer);
782 } else {
783 getFillVerticesFromPerimeterAA(paintInfo, tempVertices, vertexBuffer);
784 }
785 }
786
787 Rect bounds(path.getBounds());
788 paintInfo.expandBoundsForStroke(&bounds);
789 vertexBuffer.setBounds(bounds);
790 }
791
792 template <class TYPE>
instanceVertices(VertexBuffer & srcBuffer,VertexBuffer & dstBuffer,const float * points,int count,Rect & bounds)793 static void instanceVertices(VertexBuffer& srcBuffer, VertexBuffer& dstBuffer,
794 const float* points, int count, Rect& bounds) {
795 bounds.set(points[0], points[1], points[0], points[1]);
796
797 int numPoints = count / 2;
798 int verticesPerPoint = srcBuffer.getVertexCount();
799 dstBuffer.alloc<TYPE>(numPoints * verticesPerPoint + (numPoints - 1) * 2);
800
801 for (int i = 0; i < count; i += 2) {
802 bounds.expandToCoverVertex(points[i + 0], points[i + 1]);
803 dstBuffer.copyInto<TYPE>(srcBuffer, points[i + 0], points[i + 1]);
804 }
805 dstBuffer.createDegenerateSeparators<TYPE>(verticesPerPoint);
806 }
807
tessellatePoints(const float * points,int count,const SkPaint * paint,const mat4 & transform,VertexBuffer & vertexBuffer)808 void PathTessellator::tessellatePoints(const float* points, int count, const SkPaint* paint,
809 const mat4& transform, VertexBuffer& vertexBuffer) {
810 const PaintInfo paintInfo(paint, transform);
811
812 // determine point shape
813 SkPath path;
814 float radius = paintInfo.halfStrokeWidth;
815 if (radius == 0.0f) radius = 0.5f;
816
817 if (paintInfo.cap == SkPaint::kRound_Cap) {
818 path.addCircle(0, 0, radius);
819 } else {
820 path.addRect(-radius, -radius, radius, radius);
821 }
822
823 // calculate outline
824 Vector<Vertex> outlineVertices;
825 approximatePathOutlineVertices(path, true,
826 paintInfo.inverseScaleX * paintInfo.inverseScaleX,
827 paintInfo.inverseScaleY * paintInfo.inverseScaleY,
828 OUTLINE_REFINE_THRESHOLD_SQUARED, outlineVertices);
829
830 if (!outlineVertices.size()) return;
831
832 Rect bounds;
833 // tessellate, then duplicate outline across points
834 int numPoints = count / 2;
835 VertexBuffer tempBuffer;
836 if (!paintInfo.isAA) {
837 getFillVerticesFromPerimeter(outlineVertices, tempBuffer);
838 instanceVertices<Vertex>(tempBuffer, vertexBuffer, points, count, bounds);
839 } else {
840 // note: pass maxAlpha directly, since we want fill to be alpha modulated
841 getFillVerticesFromPerimeterAA(paintInfo, outlineVertices, tempBuffer, paintInfo.maxAlpha);
842 instanceVertices<AlphaVertex>(tempBuffer, vertexBuffer, points, count, bounds);
843 }
844
845 // expand bounds from vertex coords to pixel data
846 paintInfo.expandBoundsForStroke(&bounds);
847 vertexBuffer.setBounds(bounds);
848 }
849
tessellateLines(const float * points,int count,const SkPaint * paint,const mat4 & transform,VertexBuffer & vertexBuffer)850 void PathTessellator::tessellateLines(const float* points, int count, const SkPaint* paint,
851 const mat4& transform, VertexBuffer& vertexBuffer) {
852 ATRACE_CALL();
853 const PaintInfo paintInfo(paint, transform);
854
855 const int extra = paintInfo.capExtraDivisions();
856 int numLines = count / 4;
857 int lineAllocSize;
858 // pre-allocate space for lines in the buffer, and degenerate tris in between
859 if (paintInfo.isAA) {
860 lineAllocSize = 6 * (2) + 2 + 6 * extra;
861 vertexBuffer.alloc<AlphaVertex>(numLines * lineAllocSize + (numLines - 1) * 2);
862 } else {
863 lineAllocSize = 2 * ((2) + extra);
864 vertexBuffer.alloc<Vertex>(numLines * lineAllocSize + (numLines - 1) * 2);
865 }
866
867 Vector<Vertex> tempVertices;
868 tempVertices.push();
869 tempVertices.push();
870 Vertex* tempVerticesData = tempVertices.editArray();
871 Rect bounds;
872 bounds.set(points[0], points[1], points[0], points[1]);
873 for (int i = 0; i < count; i += 4) {
874 Vertex::set(&(tempVerticesData[0]), points[i + 0], points[i + 1]);
875 Vertex::set(&(tempVerticesData[1]), points[i + 2], points[i + 3]);
876
877 if (paintInfo.isAA) {
878 getStrokeVerticesFromUnclosedVerticesAA(paintInfo, tempVertices, vertexBuffer);
879 } else {
880 getStrokeVerticesFromUnclosedVertices(paintInfo, tempVertices, vertexBuffer);
881 }
882
883 // calculate bounds
884 bounds.expandToCoverVertex(tempVerticesData[0].x, tempVerticesData[0].y);
885 bounds.expandToCoverVertex(tempVerticesData[1].x, tempVerticesData[1].y);
886 }
887
888 // since multiple objects tessellated into buffer, separate them with degen tris
889 if (paintInfo.isAA) {
890 vertexBuffer.createDegenerateSeparators<AlphaVertex>(lineAllocSize);
891 } else {
892 vertexBuffer.createDegenerateSeparators<Vertex>(lineAllocSize);
893 }
894
895 // expand bounds from vertex coords to pixel data
896 paintInfo.expandBoundsForStroke(&bounds);
897 vertexBuffer.setBounds(bounds);
898 }
899
900 ///////////////////////////////////////////////////////////////////////////////
901 // Simple path line approximation
902 ///////////////////////////////////////////////////////////////////////////////
903
approximatePathOutlineVertices(const SkPath & path,float thresholdSquared,Vector<Vertex> & outputVertices)904 bool PathTessellator::approximatePathOutlineVertices(const SkPath& path, float thresholdSquared,
905 Vector<Vertex>& outputVertices) {
906 return approximatePathOutlineVertices(path, true, 1.0f, 1.0f, thresholdSquared, outputVertices);
907 }
908
pushToVector(Vector<Vertex> & vertices,float x,float y)909 void pushToVector(Vector<Vertex>& vertices, float x, float y) {
910 // TODO: make this not yuck
911 vertices.push();
912 Vertex* newVertex = &(vertices.editArray()[vertices.size() - 1]);
913 Vertex::set(newVertex, x, y);
914 }
915
approximatePathOutlineVertices(const SkPath & path,bool forceClose,float sqrInvScaleX,float sqrInvScaleY,float thresholdSquared,Vector<Vertex> & outputVertices)916 bool PathTessellator::approximatePathOutlineVertices(const SkPath& path, bool forceClose,
917 float sqrInvScaleX, float sqrInvScaleY, float thresholdSquared,
918 Vector<Vertex>& outputVertices) {
919 ATRACE_CALL();
920
921 // TODO: to support joins other than sharp miter, join vertices should be labelled in the
922 // perimeter, or resolved into more vertices. Reconsider forceClose-ing in that case.
923 SkPath::Iter iter(path, forceClose);
924 SkPoint pts[4];
925 SkPath::Verb v;
926 while (SkPath::kDone_Verb != (v = iter.next(pts))) {
927 switch (v) {
928 case SkPath::kMove_Verb:
929 pushToVector(outputVertices, pts[0].x(), pts[0].y());
930 ALOGV("Move to pos %f %f", pts[0].x(), pts[0].y());
931 break;
932 case SkPath::kClose_Verb:
933 ALOGV("Close at pos %f %f", pts[0].x(), pts[0].y());
934 break;
935 case SkPath::kLine_Verb:
936 ALOGV("kLine_Verb %f %f -> %f %f", pts[0].x(), pts[0].y(), pts[1].x(), pts[1].y());
937 pushToVector(outputVertices, pts[1].x(), pts[1].y());
938 break;
939 case SkPath::kQuad_Verb:
940 ALOGV("kQuad_Verb");
941 recursiveQuadraticBezierVertices(
942 pts[0].x(), pts[0].y(),
943 pts[2].x(), pts[2].y(),
944 pts[1].x(), pts[1].y(),
945 sqrInvScaleX, sqrInvScaleY, thresholdSquared, outputVertices);
946 break;
947 case SkPath::kCubic_Verb:
948 ALOGV("kCubic_Verb");
949 recursiveCubicBezierVertices(
950 pts[0].x(), pts[0].y(),
951 pts[1].x(), pts[1].y(),
952 pts[3].x(), pts[3].y(),
953 pts[2].x(), pts[2].y(),
954 sqrInvScaleX, sqrInvScaleY, thresholdSquared, outputVertices);
955 break;
956 default:
957 break;
958 }
959 }
960
961 int size = outputVertices.size();
962 if (size >= 2 && outputVertices[0].x == outputVertices[size - 1].x &&
963 outputVertices[0].y == outputVertices[size - 1].y) {
964 outputVertices.pop();
965 return true;
966 }
967 return false;
968 }
969
970 ///////////////////////////////////////////////////////////////////////////////
971 // Bezier approximation
972 ///////////////////////////////////////////////////////////////////////////////
973
recursiveCubicBezierVertices(float p1x,float p1y,float c1x,float c1y,float p2x,float p2y,float c2x,float c2y,float sqrInvScaleX,float sqrInvScaleY,float thresholdSquared,Vector<Vertex> & outputVertices,int depth)974 void PathTessellator::recursiveCubicBezierVertices(
975 float p1x, float p1y, float c1x, float c1y,
976 float p2x, float p2y, float c2x, float c2y,
977 float sqrInvScaleX, float sqrInvScaleY, float thresholdSquared,
978 Vector<Vertex>& outputVertices, int depth) {
979 float dx = p2x - p1x;
980 float dy = p2y - p1y;
981 float d1 = fabs((c1x - p2x) * dy - (c1y - p2y) * dx);
982 float d2 = fabs((c2x - p2x) * dy - (c2y - p2y) * dx);
983 float d = d1 + d2;
984
985 // multiplying by sqrInvScaleY/X equivalent to multiplying in dimensional scale factors
986 if (depth >= MAX_DEPTH
987 || d * d <= thresholdSquared * (dx * dx * sqrInvScaleY + dy * dy * sqrInvScaleX)) {
988 // below thresh, draw line by adding endpoint
989 pushToVector(outputVertices, p2x, p2y);
990 } else {
991 float p1c1x = (p1x + c1x) * 0.5f;
992 float p1c1y = (p1y + c1y) * 0.5f;
993 float p2c2x = (p2x + c2x) * 0.5f;
994 float p2c2y = (p2y + c2y) * 0.5f;
995
996 float c1c2x = (c1x + c2x) * 0.5f;
997 float c1c2y = (c1y + c2y) * 0.5f;
998
999 float p1c1c2x = (p1c1x + c1c2x) * 0.5f;
1000 float p1c1c2y = (p1c1y + c1c2y) * 0.5f;
1001
1002 float p2c1c2x = (p2c2x + c1c2x) * 0.5f;
1003 float p2c1c2y = (p2c2y + c1c2y) * 0.5f;
1004
1005 float mx = (p1c1c2x + p2c1c2x) * 0.5f;
1006 float my = (p1c1c2y + p2c1c2y) * 0.5f;
1007
1008 recursiveCubicBezierVertices(
1009 p1x, p1y, p1c1x, p1c1y,
1010 mx, my, p1c1c2x, p1c1c2y,
1011 sqrInvScaleX, sqrInvScaleY, thresholdSquared, outputVertices, depth + 1);
1012 recursiveCubicBezierVertices(
1013 mx, my, p2c1c2x, p2c1c2y,
1014 p2x, p2y, p2c2x, p2c2y,
1015 sqrInvScaleX, sqrInvScaleY, thresholdSquared, outputVertices, depth + 1);
1016 }
1017 }
1018
recursiveQuadraticBezierVertices(float ax,float ay,float bx,float by,float cx,float cy,float sqrInvScaleX,float sqrInvScaleY,float thresholdSquared,Vector<Vertex> & outputVertices,int depth)1019 void PathTessellator::recursiveQuadraticBezierVertices(
1020 float ax, float ay,
1021 float bx, float by,
1022 float cx, float cy,
1023 float sqrInvScaleX, float sqrInvScaleY, float thresholdSquared,
1024 Vector<Vertex>& outputVertices, int depth) {
1025 float dx = bx - ax;
1026 float dy = by - ay;
1027 float d = (cx - bx) * dy - (cy - by) * dx;
1028
1029 // multiplying by sqrInvScaleY/X equivalent to multiplying in dimensional scale factors
1030 if (depth >= MAX_DEPTH
1031 || d * d <= thresholdSquared * (dx * dx * sqrInvScaleY + dy * dy * sqrInvScaleX)) {
1032 // below thresh, draw line by adding endpoint
1033 pushToVector(outputVertices, bx, by);
1034 } else {
1035 float acx = (ax + cx) * 0.5f;
1036 float bcx = (bx + cx) * 0.5f;
1037 float acy = (ay + cy) * 0.5f;
1038 float bcy = (by + cy) * 0.5f;
1039
1040 // midpoint
1041 float mx = (acx + bcx) * 0.5f;
1042 float my = (acy + bcy) * 0.5f;
1043
1044 recursiveQuadraticBezierVertices(ax, ay, mx, my, acx, acy,
1045 sqrInvScaleX, sqrInvScaleY, thresholdSquared, outputVertices, depth + 1);
1046 recursiveQuadraticBezierVertices(mx, my, bx, by, bcx, bcy,
1047 sqrInvScaleX, sqrInvScaleY, thresholdSquared, outputVertices, depth + 1);
1048 }
1049 }
1050
1051 }; // namespace uirenderer
1052 }; // namespace android
1053