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