blob: ca9873cddb728f0218377b0bd8746dae55efdffb [file] [log] [blame]
jvanverth@google.com8ffb7042012-12-13 19:53:18 +00001/*
2 * Copyright 2012 The Android Open Source Project
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#define LOG_TAG "PathRenderer"
9#define LOG_NDEBUG 1
10#define ATRACE_TAG ATRACE_TAG_GRAPHICS
11
12#define VERTEX_DEBUG 0
13
14#include <SkPath.h>
jvanverth@google.com74dda902013-01-09 21:04:52 +000015#include <SkStrokeRec.h>
jvanverth@google.com8ffb7042012-12-13 19:53:18 +000016
17#include <stdlib.h>
18#include <stdint.h>
19#include <sys/types.h>
20
jvanverth@google.com24b4f972012-12-18 14:13:46 +000021#include <SkTypes.h>
commit-bot@chromium.org933e65d2014-03-20 20:00:24 +000022#include <SkTraceEvent.h>
jvanverth@google.com24b4f972012-12-18 14:13:46 +000023#include <SkMatrix.h>
24#include <SkPoint.h>
jvanverth@google.com8ffb7042012-12-13 19:53:18 +000025
jvanverth@google.com24b4f972012-12-18 14:13:46 +000026#ifdef VERBOSE
27#define ALOGV SkDebugf
28#else
29#define ALOGV(x, ...)
30#endif
31
32#include "AndroidPathRenderer.h"
jvanverth@google.com8ffb7042012-12-13 19:53:18 +000033#include "Vertex.h"
34
35namespace android {
36namespace uirenderer {
37
38#define THRESHOLD 0.5f
39
jvanverth@google.com24b4f972012-12-18 14:13:46 +000040SkRect PathRenderer::ComputePathBounds(const SkPath& path, const SkPaint* paint) {
jvanverth@google.com8ffb7042012-12-13 19:53:18 +000041 SkRect bounds = path.getBounds();
42 if (paint->getStyle() != SkPaint::kFill_Style) {
43 float outset = paint->getStrokeWidth() * 0.5f;
44 bounds.outset(outset, outset);
45 }
46 return bounds;
47}
48
humper@google.com05af1af2013-01-07 16:47:43 +000049inline void computeInverseScales(const SkMatrix* transform, float &inverseScaleX, float& inverseScaleY) {
jvanverth@google.com77734b82012-12-18 14:41:18 +000050 if (transform && transform->getType() & (SkMatrix::kScale_Mask|SkMatrix::kAffine_Mask|SkMatrix::kPerspective_Mask)) {
jvanverth@google.com24b4f972012-12-18 14:13:46 +000051 float m00 = transform->getScaleX();
52 float m01 = transform->getSkewY();
53 float m10 = transform->getSkewX();
54 float m11 = transform->getScaleY();
reed@google.com140d7282013-01-07 20:25:04 +000055 float scaleX = sk_float_sqrt(m00 * m00 + m01 * m01);
56 float scaleY = sk_float_sqrt(m10 * m10 + m11 * m11);
jvanverth@google.com8ffb7042012-12-13 19:53:18 +000057 inverseScaleX = (scaleX != 0) ? (1.0f / scaleX) : 1.0f;
58 inverseScaleY = (scaleY != 0) ? (1.0f / scaleY) : 1.0f;
59 } else {
60 inverseScaleX = 1.0f;
61 inverseScaleY = 1.0f;
62 }
63}
64
65inline void copyVertex(Vertex* destPtr, const Vertex* srcPtr) {
66 Vertex::set(destPtr, srcPtr->position[0], srcPtr->position[1]);
67}
68
69inline void copyAlphaVertex(AlphaVertex* destPtr, const AlphaVertex* srcPtr) {
70 AlphaVertex::set(destPtr, srcPtr->position[0], srcPtr->position[1], srcPtr->alpha);
71}
72
73/**
74 * Produces a pseudo-normal for a vertex, given the normals of the two incoming lines. If the offset
75 * from each vertex in a perimeter is calculated, the resultant lines connecting the offset vertices
76 * will be offset by 1.0
77 *
78 * Note that we can't add and normalize the two vectors, that would result in a rectangle having an
79 * offset of (sqrt(2)/2, sqrt(2)/2) at each corner, instead of (1, 1)
80 *
81 * NOTE: assumes angles between normals 90 degrees or less
82 */
jvanverth@google.com24b4f972012-12-18 14:13:46 +000083inline SkVector totalOffsetFromNormals(const SkVector& normalA, const SkVector& normalB) {
84 SkVector pseudoNormal = normalA + normalB;
reed@google.com140d7282013-01-07 20:25:04 +000085 pseudoNormal.scale(1.0f / (1.0f + sk_float_abs(normalA.dot(normalB))));
jvanverth@google.com24b4f972012-12-18 14:13:46 +000086 return pseudoNormal;
jvanverth@google.com8ffb7042012-12-13 19:53:18 +000087}
88
jvanverth@google.com24b4f972012-12-18 14:13:46 +000089inline void scaleOffsetForStrokeWidth(SkVector& offset, float halfStrokeWidth,
jvanverth@google.com8ffb7042012-12-13 19:53:18 +000090 float inverseScaleX, float inverseScaleY) {
91 if (halfStrokeWidth == 0.0f) {
92 // hairline - compensate for scale
jvanverth@google.com24b4f972012-12-18 14:13:46 +000093 offset.fX *= 0.5f * inverseScaleX;
94 offset.fY *= 0.5f * inverseScaleY;
jvanverth@google.com8ffb7042012-12-13 19:53:18 +000095 } else {
jvanverth@google.com24b4f972012-12-18 14:13:46 +000096 offset.scale(halfStrokeWidth);
jvanverth@google.com8ffb7042012-12-13 19:53:18 +000097 }
98}
99
humper@google.com05af1af2013-01-07 16:47:43 +0000100static void getFillVerticesFromPerimeter(const SkTArray<Vertex, true>& perimeter, VertexBuffer* vertexBuffer) {
jvanverth@google.com24b4f972012-12-18 14:13:46 +0000101 Vertex* buffer = vertexBuffer->alloc<Vertex>(perimeter.count());
jvanverth@google.com8ffb7042012-12-13 19:53:18 +0000102
103 int currentIndex = 0;
104 // zig zag between all previous points on the inside of the hull to create a
105 // triangle strip that fills the hull
106 int srcAindex = 0;
jvanverth@google.com24b4f972012-12-18 14:13:46 +0000107 int srcBindex = perimeter.count() - 1;
jvanverth@google.com8ffb7042012-12-13 19:53:18 +0000108 while (srcAindex <= srcBindex) {
109 copyVertex(&buffer[currentIndex++], &perimeter[srcAindex]);
110 if (srcAindex == srcBindex) break;
111 copyVertex(&buffer[currentIndex++], &perimeter[srcBindex]);
112 srcAindex++;
113 srcBindex--;
114 }
115}
116
humper@google.com05af1af2013-01-07 16:47:43 +0000117static void getStrokeVerticesFromPerimeter(const SkTArray<Vertex, true>& perimeter, float halfStrokeWidth,
jvanverth@google.com24b4f972012-12-18 14:13:46 +0000118 VertexBuffer* vertexBuffer, float inverseScaleX, float inverseScaleY) {
119 Vertex* buffer = vertexBuffer->alloc<Vertex>(perimeter.count() * 2 + 2);
jvanverth@google.com8ffb7042012-12-13 19:53:18 +0000120
121 int currentIndex = 0;
jvanverth@google.com24b4f972012-12-18 14:13:46 +0000122 const Vertex* last = &(perimeter[perimeter.count() - 1]);
jvanverth@google.com8ffb7042012-12-13 19:53:18 +0000123 const Vertex* current = &(perimeter[0]);
jvanverth@google.com24b4f972012-12-18 14:13:46 +0000124 SkVector lastNormal;
125 lastNormal.set(current->position[1] - last->position[1],
126 last->position[0] - current->position[0]);
jvanverth@google.com8ffb7042012-12-13 19:53:18 +0000127 lastNormal.normalize();
jvanverth@google.com24b4f972012-12-18 14:13:46 +0000128 for (int i = 0; i < perimeter.count(); i++) {
129 const Vertex* next = &(perimeter[i + 1 >= perimeter.count() ? 0 : i + 1]);
130 SkVector nextNormal;
131 nextNormal.set(next->position[1] - current->position[1],
132 current->position[0] - next->position[0]);
jvanverth@google.com8ffb7042012-12-13 19:53:18 +0000133 nextNormal.normalize();
134
jvanverth@google.com24b4f972012-12-18 14:13:46 +0000135 SkVector totalOffset = totalOffsetFromNormals(lastNormal, nextNormal);
jvanverth@google.com8ffb7042012-12-13 19:53:18 +0000136 scaleOffsetForStrokeWidth(totalOffset, halfStrokeWidth, inverseScaleX, inverseScaleY);
137
138 Vertex::set(&buffer[currentIndex++],
jvanverth@google.com24b4f972012-12-18 14:13:46 +0000139 current->position[0] + totalOffset.fX,
140 current->position[1] + totalOffset.fY);
jvanverth@google.com8ffb7042012-12-13 19:53:18 +0000141
142 Vertex::set(&buffer[currentIndex++],
jvanverth@google.com24b4f972012-12-18 14:13:46 +0000143 current->position[0] - totalOffset.fX,
144 current->position[1] - totalOffset.fY);
jvanverth@google.com8ffb7042012-12-13 19:53:18 +0000145
146 last = current;
147 current = next;
148 lastNormal = nextNormal;
149 }
150
151 // wrap around to beginning
152 copyVertex(&buffer[currentIndex++], &buffer[0]);
153 copyVertex(&buffer[currentIndex++], &buffer[1]);
154}
155
humper@google.com05af1af2013-01-07 16:47:43 +0000156static void getStrokeVerticesFromUnclosedVertices(const SkTArray<Vertex, true>& vertices, float halfStrokeWidth,
jvanverth@google.com24b4f972012-12-18 14:13:46 +0000157 VertexBuffer* vertexBuffer, float inverseScaleX, float inverseScaleY) {
158 Vertex* buffer = vertexBuffer->alloc<Vertex>(vertices.count() * 2);
jvanverth@google.com8ffb7042012-12-13 19:53:18 +0000159
160 int currentIndex = 0;
161 const Vertex* current = &(vertices[0]);
jvanverth@google.com24b4f972012-12-18 14:13:46 +0000162 SkVector lastNormal;
163 for (int i = 0; i < vertices.count() - 1; i++) {
jvanverth@google.com8ffb7042012-12-13 19:53:18 +0000164 const Vertex* next = &(vertices[i + 1]);
jvanverth@google.com24b4f972012-12-18 14:13:46 +0000165 SkVector nextNormal;
166 nextNormal.set(next->position[1] - current->position[1],
167 current->position[0] - next->position[0]);
jvanverth@google.com8ffb7042012-12-13 19:53:18 +0000168 nextNormal.normalize();
169
jvanverth@google.com24b4f972012-12-18 14:13:46 +0000170 SkVector totalOffset;
jvanverth@google.com8ffb7042012-12-13 19:53:18 +0000171 if (i == 0) {
172 totalOffset = nextNormal;
173 } else {
174 totalOffset = totalOffsetFromNormals(lastNormal, nextNormal);
175 }
176 scaleOffsetForStrokeWidth(totalOffset, halfStrokeWidth, inverseScaleX, inverseScaleY);
177
178 Vertex::set(&buffer[currentIndex++],
jvanverth@google.com24b4f972012-12-18 14:13:46 +0000179 current->position[0] + totalOffset.fX,
180 current->position[1] + totalOffset.fY);
jvanverth@google.com8ffb7042012-12-13 19:53:18 +0000181
182 Vertex::set(&buffer[currentIndex++],
jvanverth@google.com24b4f972012-12-18 14:13:46 +0000183 current->position[0] - totalOffset.fX,
184 current->position[1] - totalOffset.fY);
jvanverth@google.com8ffb7042012-12-13 19:53:18 +0000185
186 current = next;
187 lastNormal = nextNormal;
188 }
189
jvanverth@google.com24b4f972012-12-18 14:13:46 +0000190 SkVector totalOffset = lastNormal;
jvanverth@google.com8ffb7042012-12-13 19:53:18 +0000191 scaleOffsetForStrokeWidth(totalOffset, halfStrokeWidth, inverseScaleX, inverseScaleY);
192
193 Vertex::set(&buffer[currentIndex++],
jvanverth@google.com24b4f972012-12-18 14:13:46 +0000194 current->position[0] + totalOffset.fX,
195 current->position[1] + totalOffset.fY);
jvanverth@google.com8ffb7042012-12-13 19:53:18 +0000196 Vertex::set(&buffer[currentIndex++],
jvanverth@google.com24b4f972012-12-18 14:13:46 +0000197 current->position[0] - totalOffset.fX,
198 current->position[1] - totalOffset.fY);
jvanverth@google.com8ffb7042012-12-13 19:53:18 +0000199#if VERTEX_DEBUG
200 for (unsigned int i = 0; i < vertexBuffer.getSize(); i++) {
jvanverth@google.com24b4f972012-12-18 14:13:46 +0000201 SkDebugf("point at %f %f", buffer[i].position[0], buffer[i].position[1]);
jvanverth@google.com8ffb7042012-12-13 19:53:18 +0000202 }
203#endif
204}
205
humper@google.com05af1af2013-01-07 16:47:43 +0000206static void getFillVerticesFromPerimeterAA(const SkTArray<Vertex, true>& perimeter, VertexBuffer* vertexBuffer,
jvanverth@google.com8ffb7042012-12-13 19:53:18 +0000207 float inverseScaleX, float inverseScaleY) {
jvanverth@google.com24b4f972012-12-18 14:13:46 +0000208 AlphaVertex* buffer = vertexBuffer->alloc<AlphaVertex>(perimeter.count() * 3 + 2);
jvanverth@google.com8ffb7042012-12-13 19:53:18 +0000209
210 // generate alpha points - fill Alpha vertex gaps in between each point with
211 // alpha 0 vertex, offset by a scaled normal.
212 int currentIndex = 0;
jvanverth@google.com24b4f972012-12-18 14:13:46 +0000213 const Vertex* last = &(perimeter[perimeter.count() - 1]);
jvanverth@google.com8ffb7042012-12-13 19:53:18 +0000214 const Vertex* current = &(perimeter[0]);
jvanverth@google.com24b4f972012-12-18 14:13:46 +0000215 SkVector lastNormal;
216 lastNormal.set(current->position[1] - last->position[1],
217 last->position[0] - current->position[0]);
jvanverth@google.com8ffb7042012-12-13 19:53:18 +0000218 lastNormal.normalize();
jvanverth@google.com24b4f972012-12-18 14:13:46 +0000219 for (int i = 0; i < perimeter.count(); i++) {
220 const Vertex* next = &(perimeter[i + 1 >= perimeter.count() ? 0 : i + 1]);
221 SkVector nextNormal;
222 nextNormal.set(next->position[1] - current->position[1],
223 current->position[0] - next->position[0]);
jvanverth@google.com8ffb7042012-12-13 19:53:18 +0000224 nextNormal.normalize();
225
226 // AA point offset from original point is that point's normal, such that each side is offset
227 // by .5 pixels
jvanverth@google.com24b4f972012-12-18 14:13:46 +0000228 SkVector totalOffset = totalOffsetFromNormals(lastNormal, nextNormal);
229 totalOffset.fX *= 0.5f * inverseScaleX;
230 totalOffset.fY *= 0.5f * inverseScaleY;
jvanverth@google.com8ffb7042012-12-13 19:53:18 +0000231
232 AlphaVertex::set(&buffer[currentIndex++],
jvanverth@google.com24b4f972012-12-18 14:13:46 +0000233 current->position[0] + totalOffset.fX,
234 current->position[1] + totalOffset.fY,
jvanverth@google.com8ffb7042012-12-13 19:53:18 +0000235 0.0f);
236 AlphaVertex::set(&buffer[currentIndex++],
jvanverth@google.com24b4f972012-12-18 14:13:46 +0000237 current->position[0] - totalOffset.fX,
238 current->position[1] - totalOffset.fY,
jvanverth@google.com8ffb7042012-12-13 19:53:18 +0000239 1.0f);
240
241 last = current;
242 current = next;
243 lastNormal = nextNormal;
244 }
245
246 // wrap around to beginning
247 copyAlphaVertex(&buffer[currentIndex++], &buffer[0]);
248 copyAlphaVertex(&buffer[currentIndex++], &buffer[1]);
249
250 // zig zag between all previous points on the inside of the hull to create a
251 // triangle strip that fills the hull, repeating the first inner point to
252 // create degenerate tris to start inside path
253 int srcAindex = 0;
jvanverth@google.com24b4f972012-12-18 14:13:46 +0000254 int srcBindex = perimeter.count() - 1;
jvanverth@google.com8ffb7042012-12-13 19:53:18 +0000255 while (srcAindex <= srcBindex) {
256 copyAlphaVertex(&buffer[currentIndex++], &buffer[srcAindex * 2 + 1]);
257 if (srcAindex == srcBindex) break;
258 copyAlphaVertex(&buffer[currentIndex++], &buffer[srcBindex * 2 + 1]);
259 srcAindex++;
260 srcBindex--;
261 }
262
263#if VERTEX_DEBUG
264 for (unsigned int i = 0; i < vertexBuffer.getSize(); i++) {
jvanverth@google.com24b4f972012-12-18 14:13:46 +0000265 SkDebugf("point at %f %f, alpha %f", buffer[i].position[0], buffer[i].position[1], buffer[i].alpha);
jvanverth@google.com8ffb7042012-12-13 19:53:18 +0000266 }
267#endif
268}
269
270
humper@google.com05af1af2013-01-07 16:47:43 +0000271static void getStrokeVerticesFromUnclosedVerticesAA(const SkTArray<Vertex, true>& vertices, float halfStrokeWidth,
jvanverth@google.com24b4f972012-12-18 14:13:46 +0000272 VertexBuffer* vertexBuffer, float inverseScaleX, float inverseScaleY) {
273 AlphaVertex* buffer = vertexBuffer->alloc<AlphaVertex>(6 * vertices.count() + 2);
jvanverth@google.com8ffb7042012-12-13 19:53:18 +0000274
275 // avoid lines smaller than hairline since they break triangle based sampling. instead reducing
276 // alpha value (TODO: support different X/Y scale)
277 float maxAlpha = 1.0f;
278 if (halfStrokeWidth != 0 && inverseScaleX == inverseScaleY &&
279 halfStrokeWidth * inverseScaleX < 0.5f) {
280 maxAlpha *= (2 * halfStrokeWidth) / inverseScaleX;
281 halfStrokeWidth = 0.0f;
282 }
283
284 // there is no outer/inner here, using them for consistency with below approach
jvanverth@google.com24b4f972012-12-18 14:13:46 +0000285 int offset = 2 * (vertices.count() - 2);
jvanverth@google.com8ffb7042012-12-13 19:53:18 +0000286 int currentAAOuterIndex = 2;
287 int currentAAInnerIndex = 2 * offset + 5; // reversed
288 int currentStrokeIndex = currentAAInnerIndex + 7;
289
290 const Vertex* last = &(vertices[0]);
291 const Vertex* current = &(vertices[1]);
jvanverth@google.com24b4f972012-12-18 14:13:46 +0000292 SkVector lastNormal;
293 lastNormal.set(current->position[1] - last->position[1],
294 last->position[0] - current->position[0]);
jvanverth@google.com8ffb7042012-12-13 19:53:18 +0000295 lastNormal.normalize();
296
297 {
298 // start cap
jvanverth@google.com24b4f972012-12-18 14:13:46 +0000299 SkVector totalOffset = lastNormal;
300 SkVector AAOffset = totalOffset;
301 AAOffset.fX *= 0.5f * inverseScaleX;
302 AAOffset.fY *= 0.5f * inverseScaleY;
jvanverth@google.com8ffb7042012-12-13 19:53:18 +0000303
jvanverth@google.com24b4f972012-12-18 14:13:46 +0000304 SkVector innerOffset = totalOffset;
jvanverth@google.com8ffb7042012-12-13 19:53:18 +0000305 scaleOffsetForStrokeWidth(innerOffset, halfStrokeWidth, inverseScaleX, inverseScaleY);
jvanverth@google.com24b4f972012-12-18 14:13:46 +0000306 SkVector outerOffset = innerOffset + AAOffset;
jvanverth@google.com8ffb7042012-12-13 19:53:18 +0000307 innerOffset -= AAOffset;
308
309 // TODO: support square cap by changing this offset to incorporate halfStrokeWidth
jvanverth@google.com24b4f972012-12-18 14:13:46 +0000310 SkVector capAAOffset;
311 capAAOffset.set(AAOffset.fY, -AAOffset.fX);
jvanverth@google.com8ffb7042012-12-13 19:53:18 +0000312 AlphaVertex::set(&buffer[0],
jvanverth@google.com24b4f972012-12-18 14:13:46 +0000313 last->position[0] + outerOffset.fX + capAAOffset.fX,
314 last->position[1] + outerOffset.fY + capAAOffset.fY,
jvanverth@google.com8ffb7042012-12-13 19:53:18 +0000315 0.0f);
316 AlphaVertex::set(&buffer[1],
jvanverth@google.com24b4f972012-12-18 14:13:46 +0000317 last->position[0] + innerOffset.fX - capAAOffset.fX,
318 last->position[1] + innerOffset.fY - capAAOffset.fY,
jvanverth@google.com8ffb7042012-12-13 19:53:18 +0000319 maxAlpha);
320
321 AlphaVertex::set(&buffer[2 * offset + 6],
jvanverth@google.com24b4f972012-12-18 14:13:46 +0000322 last->position[0] - outerOffset.fX + capAAOffset.fX,
323 last->position[1] - outerOffset.fY + capAAOffset.fY,
jvanverth@google.com8ffb7042012-12-13 19:53:18 +0000324 0.0f);
325 AlphaVertex::set(&buffer[2 * offset + 7],
jvanverth@google.com24b4f972012-12-18 14:13:46 +0000326 last->position[0] - innerOffset.fX - capAAOffset.fX,
327 last->position[1] - innerOffset.fY - capAAOffset.fY,
jvanverth@google.com8ffb7042012-12-13 19:53:18 +0000328 maxAlpha);
329 copyAlphaVertex(&buffer[2 * offset + 8], &buffer[0]);
330 copyAlphaVertex(&buffer[2 * offset + 9], &buffer[1]);
331 copyAlphaVertex(&buffer[2 * offset + 10], &buffer[1]); // degenerate tris (the only two!)
332 copyAlphaVertex(&buffer[2 * offset + 11], &buffer[2 * offset + 7]);
333 }
334
jvanverth@google.com24b4f972012-12-18 14:13:46 +0000335 for (int i = 1; i < vertices.count() - 1; i++) {
jvanverth@google.com8ffb7042012-12-13 19:53:18 +0000336 const Vertex* next = &(vertices[i + 1]);
jvanverth@google.com24b4f972012-12-18 14:13:46 +0000337 SkVector nextNormal;
338 nextNormal.set(next->position[1] - current->position[1],
339 current->position[0] - next->position[0]);
jvanverth@google.com8ffb7042012-12-13 19:53:18 +0000340 nextNormal.normalize();
341
jvanverth@google.com24b4f972012-12-18 14:13:46 +0000342 SkVector totalOffset = totalOffsetFromNormals(lastNormal, nextNormal);
343 SkVector AAOffset = totalOffset;
344 AAOffset.fX *= 0.5f * inverseScaleX;
345 AAOffset.fY *= 0.5f * inverseScaleY;
jvanverth@google.com8ffb7042012-12-13 19:53:18 +0000346
jvanverth@google.com24b4f972012-12-18 14:13:46 +0000347 SkVector innerOffset = totalOffset;
jvanverth@google.com8ffb7042012-12-13 19:53:18 +0000348 scaleOffsetForStrokeWidth(innerOffset, halfStrokeWidth, inverseScaleX, inverseScaleY);
jvanverth@google.com24b4f972012-12-18 14:13:46 +0000349 SkVector outerOffset = innerOffset + AAOffset;
jvanverth@google.com8ffb7042012-12-13 19:53:18 +0000350 innerOffset -= AAOffset;
351
352 AlphaVertex::set(&buffer[currentAAOuterIndex++],
jvanverth@google.com24b4f972012-12-18 14:13:46 +0000353 current->position[0] + outerOffset.fX,
354 current->position[1] + outerOffset.fY,
jvanverth@google.com8ffb7042012-12-13 19:53:18 +0000355 0.0f);
356 AlphaVertex::set(&buffer[currentAAOuterIndex++],
jvanverth@google.com24b4f972012-12-18 14:13:46 +0000357 current->position[0] + innerOffset.fX,
358 current->position[1] + innerOffset.fY,
jvanverth@google.com8ffb7042012-12-13 19:53:18 +0000359 maxAlpha);
360
361 AlphaVertex::set(&buffer[currentStrokeIndex++],
jvanverth@google.com24b4f972012-12-18 14:13:46 +0000362 current->position[0] + innerOffset.fX,
363 current->position[1] + innerOffset.fY,
jvanverth@google.com8ffb7042012-12-13 19:53:18 +0000364 maxAlpha);
365 AlphaVertex::set(&buffer[currentStrokeIndex++],
jvanverth@google.com24b4f972012-12-18 14:13:46 +0000366 current->position[0] - innerOffset.fX,
367 current->position[1] - innerOffset.fY,
jvanverth@google.com8ffb7042012-12-13 19:53:18 +0000368 maxAlpha);
369
370 AlphaVertex::set(&buffer[currentAAInnerIndex--],
jvanverth@google.com24b4f972012-12-18 14:13:46 +0000371 current->position[0] - innerOffset.fX,
372 current->position[1] - innerOffset.fY,
jvanverth@google.com8ffb7042012-12-13 19:53:18 +0000373 maxAlpha);
374 AlphaVertex::set(&buffer[currentAAInnerIndex--],
jvanverth@google.com24b4f972012-12-18 14:13:46 +0000375 current->position[0] - outerOffset.fX,
376 current->position[1] - outerOffset.fY,
jvanverth@google.com8ffb7042012-12-13 19:53:18 +0000377 0.0f);
378
379 last = current;
380 current = next;
381 lastNormal = nextNormal;
382 }
383
384 {
385 // end cap
jvanverth@google.com24b4f972012-12-18 14:13:46 +0000386 SkVector totalOffset = lastNormal;
387 SkVector AAOffset = totalOffset;
388 AAOffset.fX *= 0.5f * inverseScaleX;
389 AAOffset.fY *= 0.5f * inverseScaleY;
jvanverth@google.com8ffb7042012-12-13 19:53:18 +0000390
jvanverth@google.com24b4f972012-12-18 14:13:46 +0000391 SkVector innerOffset = totalOffset;
jvanverth@google.com8ffb7042012-12-13 19:53:18 +0000392 scaleOffsetForStrokeWidth(innerOffset, halfStrokeWidth, inverseScaleX, inverseScaleY);
jvanverth@google.com24b4f972012-12-18 14:13:46 +0000393 SkVector outerOffset = innerOffset + AAOffset;
jvanverth@google.com8ffb7042012-12-13 19:53:18 +0000394 innerOffset -= AAOffset;
395
396 // TODO: support square cap by changing this offset to incorporate halfStrokeWidth
jvanverth@google.com24b4f972012-12-18 14:13:46 +0000397 SkVector capAAOffset;
398 capAAOffset.set(-AAOffset.fY, AAOffset.fX);
jvanverth@google.com8ffb7042012-12-13 19:53:18 +0000399
400 AlphaVertex::set(&buffer[offset + 2],
jvanverth@google.com24b4f972012-12-18 14:13:46 +0000401 current->position[0] + outerOffset.fX + capAAOffset.fX,
402 current->position[1] + outerOffset.fY + capAAOffset.fY,
jvanverth@google.com8ffb7042012-12-13 19:53:18 +0000403 0.0f);
404 AlphaVertex::set(&buffer[offset + 3],
jvanverth@google.com24b4f972012-12-18 14:13:46 +0000405 current->position[0] + innerOffset.fX - capAAOffset.fX,
406 current->position[1] + innerOffset.fY - capAAOffset.fY,
jvanverth@google.com8ffb7042012-12-13 19:53:18 +0000407 maxAlpha);
408
409 AlphaVertex::set(&buffer[offset + 4],
jvanverth@google.com24b4f972012-12-18 14:13:46 +0000410 current->position[0] - outerOffset.fX + capAAOffset.fX,
411 current->position[1] - outerOffset.fY + capAAOffset.fY,
jvanverth@google.com8ffb7042012-12-13 19:53:18 +0000412 0.0f);
413 AlphaVertex::set(&buffer[offset + 5],
jvanverth@google.com24b4f972012-12-18 14:13:46 +0000414 current->position[0] - innerOffset.fX - capAAOffset.fX,
415 current->position[1] - innerOffset.fY - capAAOffset.fY,
jvanverth@google.com8ffb7042012-12-13 19:53:18 +0000416 maxAlpha);
417
jvanverth@google.com24b4f972012-12-18 14:13:46 +0000418 copyAlphaVertex(&buffer[vertexBuffer->getSize() - 2], &buffer[offset + 3]);
419 copyAlphaVertex(&buffer[vertexBuffer->getSize() - 1], &buffer[offset + 5]);
jvanverth@google.com8ffb7042012-12-13 19:53:18 +0000420 }
421
422#if VERTEX_DEBUG
423 for (unsigned int i = 0; i < vertexBuffer.getSize(); i++) {
jvanverth@google.com24b4f972012-12-18 14:13:46 +0000424 SkDebugf("point at %f %f, alpha %f", buffer[i].position[0], buffer[i].position[1], buffer[i].alpha);
jvanverth@google.com8ffb7042012-12-13 19:53:18 +0000425 }
426#endif
427}
428
429
humper@google.com05af1af2013-01-07 16:47:43 +0000430static void getStrokeVerticesFromPerimeterAA(const SkTArray<Vertex, true>& perimeter, float halfStrokeWidth,
jvanverth@google.com24b4f972012-12-18 14:13:46 +0000431 VertexBuffer* vertexBuffer, float inverseScaleX, float inverseScaleY) {
432 AlphaVertex* buffer = vertexBuffer->alloc<AlphaVertex>(6 * perimeter.count() + 8);
jvanverth@google.com8ffb7042012-12-13 19:53:18 +0000433
434 // avoid lines smaller than hairline since they break triangle based sampling. instead reducing
435 // alpha value (TODO: support different X/Y scale)
436 float maxAlpha = 1.0f;
437 if (halfStrokeWidth != 0 && inverseScaleX == inverseScaleY &&
438 halfStrokeWidth * inverseScaleX < 0.5f) {
439 maxAlpha *= (2 * halfStrokeWidth) / inverseScaleX;
440 halfStrokeWidth = 0.0f;
441 }
442
jvanverth@google.com24b4f972012-12-18 14:13:46 +0000443 int offset = 2 * perimeter.count() + 3;
jvanverth@google.com8ffb7042012-12-13 19:53:18 +0000444 int currentAAOuterIndex = 0;
445 int currentStrokeIndex = offset;
446 int currentAAInnerIndex = offset * 2;
447
jvanverth@google.com24b4f972012-12-18 14:13:46 +0000448 const Vertex* last = &(perimeter[perimeter.count() - 1]);
jvanverth@google.com8ffb7042012-12-13 19:53:18 +0000449 const Vertex* current = &(perimeter[0]);
jvanverth@google.com24b4f972012-12-18 14:13:46 +0000450 SkVector lastNormal;
451 lastNormal.set(current->position[1] - last->position[1],
452 last->position[0] - current->position[0]);
jvanverth@google.com8ffb7042012-12-13 19:53:18 +0000453 lastNormal.normalize();
jvanverth@google.com24b4f972012-12-18 14:13:46 +0000454 for (int i = 0; i < perimeter.count(); i++) {
455 const Vertex* next = &(perimeter[i + 1 >= perimeter.count() ? 0 : i + 1]);
456 SkVector nextNormal;
457 nextNormal.set(next->position[1] - current->position[1],
458 current->position[0] - next->position[0]);
jvanverth@google.com8ffb7042012-12-13 19:53:18 +0000459 nextNormal.normalize();
460
jvanverth@google.com24b4f972012-12-18 14:13:46 +0000461 SkVector totalOffset = totalOffsetFromNormals(lastNormal, nextNormal);
462 SkVector AAOffset = totalOffset;
463 AAOffset.fX *= 0.5f * inverseScaleX;
464 AAOffset.fY *= 0.5f * inverseScaleY;
jvanverth@google.com8ffb7042012-12-13 19:53:18 +0000465
jvanverth@google.com24b4f972012-12-18 14:13:46 +0000466 SkVector innerOffset = totalOffset;
jvanverth@google.com8ffb7042012-12-13 19:53:18 +0000467 scaleOffsetForStrokeWidth(innerOffset, halfStrokeWidth, inverseScaleX, inverseScaleY);
jvanverth@google.com24b4f972012-12-18 14:13:46 +0000468 SkVector outerOffset = innerOffset + AAOffset;
jvanverth@google.com8ffb7042012-12-13 19:53:18 +0000469 innerOffset -= AAOffset;
470
471 AlphaVertex::set(&buffer[currentAAOuterIndex++],
jvanverth@google.com24b4f972012-12-18 14:13:46 +0000472 current->position[0] + outerOffset.fX,
473 current->position[1] + outerOffset.fY,
jvanverth@google.com8ffb7042012-12-13 19:53:18 +0000474 0.0f);
475 AlphaVertex::set(&buffer[currentAAOuterIndex++],
jvanverth@google.com24b4f972012-12-18 14:13:46 +0000476 current->position[0] + innerOffset.fX,
477 current->position[1] + innerOffset.fY,
jvanverth@google.com8ffb7042012-12-13 19:53:18 +0000478 maxAlpha);
479
480 AlphaVertex::set(&buffer[currentStrokeIndex++],
jvanverth@google.com24b4f972012-12-18 14:13:46 +0000481 current->position[0] + innerOffset.fX,
482 current->position[1] + innerOffset.fY,
jvanverth@google.com8ffb7042012-12-13 19:53:18 +0000483 maxAlpha);
484 AlphaVertex::set(&buffer[currentStrokeIndex++],
jvanverth@google.com24b4f972012-12-18 14:13:46 +0000485 current->position[0] - innerOffset.fX,
486 current->position[1] - innerOffset.fY,
jvanverth@google.com8ffb7042012-12-13 19:53:18 +0000487 maxAlpha);
488
489 AlphaVertex::set(&buffer[currentAAInnerIndex++],
jvanverth@google.com24b4f972012-12-18 14:13:46 +0000490 current->position[0] - innerOffset.fX,
491 current->position[1] - innerOffset.fY,
jvanverth@google.com8ffb7042012-12-13 19:53:18 +0000492 maxAlpha);
493 AlphaVertex::set(&buffer[currentAAInnerIndex++],
jvanverth@google.com24b4f972012-12-18 14:13:46 +0000494 current->position[0] - outerOffset.fX,
495 current->position[1] - outerOffset.fY,
jvanverth@google.com8ffb7042012-12-13 19:53:18 +0000496 0.0f);
497
498 last = current;
499 current = next;
500 lastNormal = nextNormal;
501 }
502
503 // wrap each strip around to beginning, creating degenerate tris to bridge strips
504 copyAlphaVertex(&buffer[currentAAOuterIndex++], &buffer[0]);
505 copyAlphaVertex(&buffer[currentAAOuterIndex++], &buffer[1]);
506 copyAlphaVertex(&buffer[currentAAOuterIndex++], &buffer[1]);
507
508 copyAlphaVertex(&buffer[currentStrokeIndex++], &buffer[offset]);
509 copyAlphaVertex(&buffer[currentStrokeIndex++], &buffer[offset + 1]);
510 copyAlphaVertex(&buffer[currentStrokeIndex++], &buffer[offset + 1]);
511
512 copyAlphaVertex(&buffer[currentAAInnerIndex++], &buffer[2 * offset]);
513 copyAlphaVertex(&buffer[currentAAInnerIndex++], &buffer[2 * offset + 1]);
514 // don't need to create last degenerate tri
515
516#if VERTEX_DEBUG
517 for (unsigned int i = 0; i < vertexBuffer.getSize(); i++) {
jvanverth@google.com24b4f972012-12-18 14:13:46 +0000518 SkDebugf("point at %f %f, alpha %f", buffer[i].position[0], buffer[i].position[1], buffer[i].alpha);
jvanverth@google.com8ffb7042012-12-13 19:53:18 +0000519 }
520#endif
521}
522
jvanverth@google.com74dda902013-01-09 21:04:52 +0000523void PathRenderer::ConvexPathVertices(const SkPath &path, const SkStrokeRec& stroke, bool isAA,
jvanverth@google.com24b4f972012-12-18 14:13:46 +0000524 const SkMatrix* transform, VertexBuffer* vertexBuffer) {
jvanverth@google.com8ffb7042012-12-13 19:53:18 +0000525
jvanverth@google.com74dda902013-01-09 21:04:52 +0000526 SkStrokeRec::Style style = stroke.getStyle();
jvanverth@google.com8ffb7042012-12-13 19:53:18 +0000527
528 float inverseScaleX, inverseScaleY;
529 computeInverseScales(transform, inverseScaleX, inverseScaleY);
530
jvanverth@google.com24b4f972012-12-18 14:13:46 +0000531 SkTArray<Vertex, true> tempVertices;
jvanverth@google.com8ffb7042012-12-13 19:53:18 +0000532 float threshInvScaleX = inverseScaleX;
533 float threshInvScaleY = inverseScaleY;
jvanverth@google.com74dda902013-01-09 21:04:52 +0000534 if (style == SkStrokeRec::kStroke_Style) {
jvanverth@google.com8ffb7042012-12-13 19:53:18 +0000535 // alter the bezier recursion threshold values we calculate in order to compensate for
536 // expansion done after the path vertices are found
537 SkRect bounds = path.getBounds();
538 if (!bounds.isEmpty()) {
jvanverth@google.com74dda902013-01-09 21:04:52 +0000539 threshInvScaleX *= bounds.width() / (bounds.width() + stroke.getWidth());
540 threshInvScaleY *= bounds.height() / (bounds.height() + stroke.getWidth());
jvanverth@google.com8ffb7042012-12-13 19:53:18 +0000541 }
542 }
543
544 // force close if we're filling the path, since fill path expects closed perimeter.
jvanverth@google.com74dda902013-01-09 21:04:52 +0000545 bool forceClose = style != SkStrokeRec::kStroke_Style;
jvanverth@google.com24b4f972012-12-18 14:13:46 +0000546 bool wasClosed = ConvexPathPerimeterVertices(path, forceClose, threshInvScaleX * threshInvScaleX,
547 threshInvScaleY * threshInvScaleY, &tempVertices);
jvanverth@google.com8ffb7042012-12-13 19:53:18 +0000548
jvanverth@google.com24b4f972012-12-18 14:13:46 +0000549 if (!tempVertices.count()) {
jvanverth@google.com8ffb7042012-12-13 19:53:18 +0000550 // path was empty, return without allocating vertex buffer
551 return;
552 }
553
554#if VERTEX_DEBUG
jvanverth@google.com24b4f972012-12-18 14:13:46 +0000555 for (unsigned int i = 0; i < tempVertices.count(); i++) {
556 SkDebugf("orig path: point at %f %f", tempVertices[i].position[0], tempVertices[i].position[1]);
jvanverth@google.com8ffb7042012-12-13 19:53:18 +0000557 }
558#endif
559
jvanverth@google.com74dda902013-01-09 21:04:52 +0000560 if (style == SkStrokeRec::kStroke_Style) {
561 float halfStrokeWidth = stroke.getWidth() * 0.5f;
jvanverth@google.com8ffb7042012-12-13 19:53:18 +0000562 if (!isAA) {
563 if (wasClosed) {
564 getStrokeVerticesFromPerimeter(tempVertices, halfStrokeWidth, vertexBuffer,
565 inverseScaleX, inverseScaleY);
566 } else {
567 getStrokeVerticesFromUnclosedVertices(tempVertices, halfStrokeWidth, vertexBuffer,
568 inverseScaleX, inverseScaleY);
569 }
570
571 } else {
572 if (wasClosed) {
573 getStrokeVerticesFromPerimeterAA(tempVertices, halfStrokeWidth, vertexBuffer,
574 inverseScaleX, inverseScaleY);
575 } else {
576 getStrokeVerticesFromUnclosedVerticesAA(tempVertices, halfStrokeWidth, vertexBuffer,
577 inverseScaleX, inverseScaleY);
578 }
579 }
580 } else {
581 // For kStrokeAndFill style, the path should be adjusted externally, as it will be treated as a fill here.
582 if (!isAA) {
583 getFillVerticesFromPerimeter(tempVertices, vertexBuffer);
584 } else {
585 getFillVerticesFromPerimeterAA(tempVertices, vertexBuffer, inverseScaleX, inverseScaleY);
586 }
587 }
588}
589
590
humper@google.com05af1af2013-01-07 16:47:43 +0000591static void pushToVector(SkTArray<Vertex, true>* vertices, float x, float y) {
jvanverth@google.com8ffb7042012-12-13 19:53:18 +0000592 // TODO: make this not yuck
jvanverth@google.com24b4f972012-12-18 14:13:46 +0000593 vertices->push_back();
594 Vertex* newVertex = &((*vertices)[vertices->count() - 1]);
jvanverth@google.com8ffb7042012-12-13 19:53:18 +0000595 Vertex::set(newVertex, x, y);
596}
597
jvanverth@google.com24b4f972012-12-18 14:13:46 +0000598bool PathRenderer::ConvexPathPerimeterVertices(const SkPath& path, bool forceClose,
599 float sqrInvScaleX, float sqrInvScaleY, SkTArray<Vertex, true>* outputVertices) {
jvanverth@google.com24b4f972012-12-18 14:13:46 +0000600
jvanverth@google.com8ffb7042012-12-13 19:53:18 +0000601
602 // TODO: to support joins other than sharp miter, join vertices should be labelled in the
603 // perimeter, or resolved into more vertices. Reconsider forceClose-ing in that case.
604 SkPath::Iter iter(path, forceClose);
605 SkPoint pts[4];
606 SkPath::Verb v;
humper@google.com05af1af2013-01-07 16:47:43 +0000607
jvanverth@google.com8ffb7042012-12-13 19:53:18 +0000608 while (SkPath::kDone_Verb != (v = iter.next(pts))) {
609 switch (v) {
610 case SkPath::kMove_Verb:
611 pushToVector(outputVertices, pts[0].x(), pts[0].y());
612 ALOGV("Move to pos %f %f", pts[0].x(), pts[0].y());
613 break;
614 case SkPath::kClose_Verb:
615 ALOGV("Close at pos %f %f", pts[0].x(), pts[0].y());
616 break;
617 case SkPath::kLine_Verb:
618 ALOGV("kLine_Verb %f %f -> %f %f",
619 pts[0].x(), pts[0].y(),
620 pts[1].x(), pts[1].y());
621
622 pushToVector(outputVertices, pts[1].x(), pts[1].y());
623 break;
624 case SkPath::kQuad_Verb:
625 ALOGV("kQuad_Verb");
jvanverth@google.com24b4f972012-12-18 14:13:46 +0000626 RecursiveQuadraticBezierVertices(
jvanverth@google.com8ffb7042012-12-13 19:53:18 +0000627 pts[0].x(), pts[0].y(),
628 pts[2].x(), pts[2].y(),
629 pts[1].x(), pts[1].y(),
630 sqrInvScaleX, sqrInvScaleY, outputVertices);
631 break;
632 case SkPath::kCubic_Verb:
633 ALOGV("kCubic_Verb");
jvanverth@google.com24b4f972012-12-18 14:13:46 +0000634 RecursiveCubicBezierVertices(
jvanverth@google.com8ffb7042012-12-13 19:53:18 +0000635 pts[0].x(), pts[0].y(),
636 pts[1].x(), pts[1].y(),
637 pts[3].x(), pts[3].y(),
638 pts[2].x(), pts[2].y(),
639 sqrInvScaleX, sqrInvScaleY, outputVertices);
640 break;
641 default:
642 break;
643 }
644 }
645
jvanverth@google.com24b4f972012-12-18 14:13:46 +0000646 int size = outputVertices->count();
647 if (size >= 2 && (*outputVertices)[0].position[0] == (*outputVertices)[size - 1].position[0] &&
648 (*outputVertices)[0].position[1] == (*outputVertices)[size - 1].position[1]) {
649 outputVertices->pop_back();
jvanverth@google.com8ffb7042012-12-13 19:53:18 +0000650 return true;
651 }
652 return false;
653}
654
jvanverth@google.com24b4f972012-12-18 14:13:46 +0000655void PathRenderer::RecursiveCubicBezierVertices(
jvanverth@google.com8ffb7042012-12-13 19:53:18 +0000656 float p1x, float p1y, float c1x, float c1y,
657 float p2x, float p2y, float c2x, float c2y,
jvanverth@google.com24b4f972012-12-18 14:13:46 +0000658 float sqrInvScaleX, float sqrInvScaleY, SkTArray<Vertex, true>* outputVertices) {
jvanverth@google.com8ffb7042012-12-13 19:53:18 +0000659 float dx = p2x - p1x;
660 float dy = p2y - p1y;
reed@google.com140d7282013-01-07 20:25:04 +0000661 float d1 = sk_float_abs((c1x - p2x) * dy - (c1y - p2y) * dx);
662 float d2 = sk_float_abs((c2x - p2x) * dy - (c2y - p2y) * dx);
jvanverth@google.com8ffb7042012-12-13 19:53:18 +0000663 float d = d1 + d2;
664
665 // multiplying by sqrInvScaleY/X equivalent to multiplying in dimensional scale factors
666
667 if (d * d < THRESHOLD * THRESHOLD * (dx * dx * sqrInvScaleY + dy * dy * sqrInvScaleX)) {
668 // below thresh, draw line by adding endpoint
669 pushToVector(outputVertices, p2x, p2y);
670 } else {
671 float p1c1x = (p1x + c1x) * 0.5f;
672 float p1c1y = (p1y + c1y) * 0.5f;
673 float p2c2x = (p2x + c2x) * 0.5f;
674 float p2c2y = (p2y + c2y) * 0.5f;
675
676 float c1c2x = (c1x + c2x) * 0.5f;
677 float c1c2y = (c1y + c2y) * 0.5f;
678
679 float p1c1c2x = (p1c1x + c1c2x) * 0.5f;
680 float p1c1c2y = (p1c1y + c1c2y) * 0.5f;
681
682 float p2c1c2x = (p2c2x + c1c2x) * 0.5f;
683 float p2c1c2y = (p2c2y + c1c2y) * 0.5f;
684
685 float mx = (p1c1c2x + p2c1c2x) * 0.5f;
686 float my = (p1c1c2y + p2c1c2y) * 0.5f;
687
jvanverth@google.com24b4f972012-12-18 14:13:46 +0000688 RecursiveCubicBezierVertices(
jvanverth@google.com8ffb7042012-12-13 19:53:18 +0000689 p1x, p1y, p1c1x, p1c1y,
690 mx, my, p1c1c2x, p1c1c2y,
691 sqrInvScaleX, sqrInvScaleY, outputVertices);
jvanverth@google.com24b4f972012-12-18 14:13:46 +0000692 RecursiveCubicBezierVertices(
jvanverth@google.com8ffb7042012-12-13 19:53:18 +0000693 mx, my, p2c1c2x, p2c1c2y,
694 p2x, p2y, p2c2x, p2c2y,
695 sqrInvScaleX, sqrInvScaleY, outputVertices);
696 }
697}
698
jvanverth@google.com24b4f972012-12-18 14:13:46 +0000699void PathRenderer::RecursiveQuadraticBezierVertices(
jvanverth@google.com8ffb7042012-12-13 19:53:18 +0000700 float ax, float ay,
701 float bx, float by,
702 float cx, float cy,
jvanverth@google.com24b4f972012-12-18 14:13:46 +0000703 float sqrInvScaleX, float sqrInvScaleY, SkTArray<Vertex, true>* outputVertices) {
jvanverth@google.com8ffb7042012-12-13 19:53:18 +0000704 float dx = bx - ax;
705 float dy = by - ay;
706 float d = (cx - bx) * dy - (cy - by) * dx;
707
708 if (d * d < THRESHOLD * THRESHOLD * (dx * dx * sqrInvScaleY + dy * dy * sqrInvScaleX)) {
709 // below thresh, draw line by adding endpoint
710 pushToVector(outputVertices, bx, by);
711 } else {
712 float acx = (ax + cx) * 0.5f;
713 float bcx = (bx + cx) * 0.5f;
714 float acy = (ay + cy) * 0.5f;
715 float bcy = (by + cy) * 0.5f;
716
717 // midpoint
718 float mx = (acx + bcx) * 0.5f;
719 float my = (acy + bcy) * 0.5f;
720
jvanverth@google.com24b4f972012-12-18 14:13:46 +0000721 RecursiveQuadraticBezierVertices(ax, ay, mx, my, acx, acy,
jvanverth@google.com8ffb7042012-12-13 19:53:18 +0000722 sqrInvScaleX, sqrInvScaleY, outputVertices);
jvanverth@google.com24b4f972012-12-18 14:13:46 +0000723 RecursiveQuadraticBezierVertices(mx, my, bx, by, bcx, bcy,
jvanverth@google.com8ffb7042012-12-13 19:53:18 +0000724 sqrInvScaleX, sqrInvScaleY, outputVertices);
725 }
726}
727
728}; // namespace uirenderer
729}; // namespace android